文章内容输出来源:拉勾教育前端高薪训练营
打包工具解决的是前端整体的模块化,并不单指javascript模块化
模块打包工具的由来
- ES Modules存在环境兼容问题
- 模块文件过多,网络请求频繁
- 所有的前端资源都需要模块化
模块打包工具的目的
- 将开发阶段的es6+等新特性在生产阶段编译成es5
- 将开发阶段多个分散的多个模块文件在生产阶段打包成一个bundle.js
- 支持不同类型的资源模块打包
webpack使用
准备阶段
安装webpack依赖
$ yarn add webpack webpack-cli --dev
基础使用
// 不设置mode时默认为production模式
$ yarn webpack
// development模式
$ yarn webpack --mode development
// none模式
$ yarn webpack --mode none
或者在package.json中定义任务
"scripts": {
"build": "webpack"
}
$ yarn build
在使用webpack打包后的文件时,可以去掉script标签上的type="module“属性
loader加载器使用
- webpack内部默认只会处理js文件,其他类型文件需要使用loader加载器来解
- loader是webpack的核心,通过loader可以加载任何类型的文件
- webpack默认只会处理es6+的import和export,其他特性需要babel-loader来解析
$ yarn add babel-loader @babel/core @babel/preset-env --dev
webpack模块加载方式
- 遵循ES Modules标准的import声明
- 遵循CommonJS标准的require函数
- 遵循AMD标准的define函数和require函数
- 样式代码中的@import指令和url函数
- HTML代码中图片标签的src属性
开发一个loader
- loader工作就像是一个管道,可以执行多个loader
- 因为webpack解析资源打包后直接将代码拼接在了打包的js中,所以最终必须返回javascript代码,否则语法可能出错
md-loader.js,解析markdown文件
const marked = require('marked')
// 将.md直接解析成html并返回
module.exports = source => {
const html = marked(source)
// return `module.exports = ${JSON.stringify(html)}`
return `export default ${JSON.stringify(html)}`
}
// 或直接返回html,然后交给html-loader来处理
module.exports = source => {
const html = marked(source)
return html
}
插件机制
相比于loader,plugin拥有更宽的能力范围
常用插件
- clean-webpack-plugin 自动清除打包文件夹
- html-webpack-plugin 自动生成html,指定多个new HtmlWebpackPlugin()可生成多个html
- copy-webpack-plugin 拷贝文件到指定目录
- mini-css-extract-plugin 提取css到单个文件,使用时处理css不需要用style-loader,建议css文件大小超过150kb时使用此插件提取css
module.exports = {
module: {
rules: [
{
test: /.css$/,
use: [
// 'style-loader', // 使用mini-css-extract-plugin时,不需要style-loader来解析
MiniCssExtractPlugin.loader,
'css-loader',
],
},
]
}
}
- optimize-css-assets-webpack-plugin 压缩样式文件
- terser-webpack-plugin 压缩js文件
// 可以配置在plugins中,但是在plugins时随时都会开启压缩,所以一般压缩类插件建议配置在minimizer中,配置了minimizer后,js压缩也需要在minimizer中手动配置
module.exports = {
optimization:{
minimizer: [
new OptimizeCssAssetsWebpackPlugin(),
new TerserWebpackPlugin(),
],
},
}
自定义插件
- 自定义插件必须是一个函数或者是一个包含apply方法的对象
- 通过在生命周期的钩子中挂载函数实现扩展
clearCommentsWebpackPlugin.js打包后清除bundle.js中的注释
class ClearCommentsWebpackPlugin {
constructor(options = {}) {
this.options = options
}
apply (compiler) {
console.log('clear start')
compiler.hooks.emit.tap('ClearCommentsWebpackPlugin', compilation => {
for (const name in compilation.assets) {
if (name.endsWith('.js')) {
const contents = compilation.assets[name].source()
const clearComments = contents.replace(/\/\*\*+\*\//g, '')
compilation.assets[name] = {
source: () => clearComments, // 插件必须写的参数
size: () => clearComments.length, // 插件必须写的参数
}
}
}
})
}
}
module.exports = ClearCommentsWebpackPlugin
自动编译
watch工作模式
此模式需要手动刷新浏览器
$ yarn webpack --watch
browserSync自动刷新浏览器
$ browser-sync dist --files "**/*"
webpack-dev-server
- 提供了用于开发的http server,集成了“自动编译”和“自动刷新浏览器”等功能
- 默认只会serve打包输出文件,只要是webpack输出的文件都可以直接被访问到,静态资源需要配置devServer下的contentBase属性
webpack.config.js
devServer: {
contentBase: './public',
}
- 配置代理服务,解决跨域问题
webpack.config.js
proxy: {
'/api': {
// http://localhost:8080/api/user => https://api.github.com/api/users
target: 'https://api.github.com',
// http://localhost:8080/api/user => https://api.github.com/users
pathRewrite: {
'^/api': '',
},
// 不能使用loaclhost:8080作为请求主机名,为true时会以实际代理服务器作为主机名请求
changeOrigin: true,
},
}
# webpack4
$ yarn webpack-dev-server
# webpack5
$ yarn webpack serve --open
source map源代码地图
使用方式,js文件最后添加source map注释
//# sourceMappingURL=xxxxxxxx.min.map
webpack配置source map
//# sourceMappingURL=xxxxxxxx.min.map
webpack.config.js
devtool: 'source-map',
source map各配置对比
devtool | build | rebuild | production | qualty |
---|---|---|---|---|
(none) | fastest | fastest | yes | bundled code |
eval | fastest | fastest | no | generated code |
cheap-eval-source-map | fast | faster | no | transformed code(lines only) |
cheap-module-eval-source-map | slow | faster | no | original source(lines only) |
eval-source-map | slowest | fast | no | original source |
cheap-source-map | fast | slow | yes | transformed code(lines only) |
cheap-module-source-map | slow | slower | yes | original source(lines only) |
inline-cheap-source-map | fast | slow | no | transformed code(lines only) |
inline-cheap-module-source-map | slow | slower | no | original source(lines only) |
source-map | slowest | slowest | yes | original source |
inline-source-map | slowest | slowest | no | original source |
hidden-source-map | slowest | slowest | yes | original source |
nosources-source-map | slowest | slowest | yes | without source content |
- eval模式:将代码打包放到一个eval函数中执行,在eval函数的最后通过url方式说明对应的文件路径,此模式不会生成source map文件
- eval-source-map模式:相比于eval,生成了source map,可以定位到行和列
- cheap-eval-source-map模式:阉割版的eval-source-map,而且源代码经过了loader加工,只能定位到行
- cheap-module-eval-source-map模式:阉割版的eval-source-map,只能定位到行,但是源代码没有被loader加工,为手写的源代码
source map各名词解析
- eval-是否使用eval执行模块代码
- cheap-source map是否包含行信息
- module-是否能够得到loader处理之前的源代码
- inline-将source map以data-url方式嵌入到代码
- hidden-生成了source map文件,但是开发者工具中看不到,通常用于开发第三方包
- nosources-能看到行列信息,但是开发者工具中看不到源代码
选择合适的source map
- 开发环境中建议选择:cheap-module-eval-source-map
- 生产环境中建议选择:none
HMR(Hot Module Replacement)模块热替换
$ yarn webpack-dev-server --hot
或者
webpack.config.js
const webpack = require('webpack')
devServer: {
// hot: true,
hotOnly: true, // 结局hot模式HMR时代码报错自动刷新页面
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
],
此时只实现了样式文件热替换,js文件和媒体资源是无规律的,需要手动处理模块热替换逻辑
index.js
import createHeading from './heading.js'
let lastHeading = createHeading
// 手动热替换js
module.hot.accept('./heading', () => {
// 此处编写热替换代码逻辑
document.body.removeChild(createHeading)
const newCreateHeading = createHeading()
document.body.appendChild(newCreateHeading)
lastHeading = newCreateHeading
})
// 手动替换图片
module.hot.accept('./src/assets/image/icon_order_pay.png', () => {
// 此处编写热替换代码逻辑
img.src = icon1
})
注意事项:处理HMR的代码报错会导致自动刷新(使用hotOnly替换hot配置来解决)
根据环境配置不同的代码
配置方式
- 配置文件根据环境不同导出不同配置
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = (env, argv) => {
const config = {
// config配置
}
if (env === 'production') {
config.mode = 'production'
config.devtool = false
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: ['public'],
}),
]
}
return config
}
$ yarn webpack --env production
- 一个环境对应一个配置文件,公共配置放在webpack.common.js,生产配置放在webpack.prod.js,开发配置放在webpack.dev.js,通过webpack-merge来合并common和对应环境
module.exports = merge(common, {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: ['public'],
}),
],
})
$ yarn webpack --config webpack.prod.js
DefinePlugin
为代码注入全局成员,production模式默认启用,并注入process.env.NODE_ENV,可用于根据不同的运行环境匹配不同的参数,例如可以设置api请求host
module.exports = {
plugins: [
new webpack.DefinePlugin({
API_BASE_URL: '"https://api.example.com"', // 注入的字符串要加引号,因为打包后代码会直接引用,也可以JSON.stringify('https://api.example.com')
}),
],
}
Tree Shaking
production模式自带Tree Shaking
module.exports = {
optimization: {
usedExports: true, // 负责标记“枯树叶”
minimize: true, // 负责“摇掉”他们
concatenateModules: true, // 尽可能将所有模块合并输出到一个函数中,既提升来运行效率,又减少了代码的体积
},
}
Tree Shaking与babel-loader
Tree Shaking 的前提是ES Modules输出的代码,老版本的babel中默认会将ES Modules转换成CommonJS,新版本默认不会将ESM转换成CommonJS
强制转换ESM=>CommonJS
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { modules: 'commonjs' }], // modules: false 不转换
],
},
}, // es6+解析
},
sideEffects 副作用
副作用:模块执行时除了导出成员之外所作的事情,一般用于npm包标记是否有副作用,production模式下会自动开启
module.exports = {
optimization: {
sideEffects: true, // 副作用是否开启
},
}
// package.json
{
"sideEffects": false,// 标识代码是否有副作用
}
// or用数组标识出有副作用的文件
{
"sideEffects": [
"./src/index.js",
"*/main.css"
] // 标识代码是否有副作用
}
代码分包
所有的代码都被打包到一起会造成bundle体积过大,且并不是一开始就需要加载所有的模块
分包的方式:
- 多入口打包,适用于多页面应用,一个页面对应一个打包入口,公共部分单独提取
module.exports = {
entry: {
index: './src/index.js',
heading: './src/heading.js',
},
output: {
filename: '[name].bundle.js',
},
optimization: {
splitChunks: {
chunks: 'all', // 提取所有公共模块
},
},
plugins: [
new HtmlWebpackPlugin({
title: 'webpack sample',
template: './index.html',
filename: 'index.html',
chunks: ['index'],
}),
new HtmlWebpackPlugin({
title: 'webpack heanding',
template: './heanding.html',
filename: 'heanding.html',
chunks: ['heanding'],
}),
]
}
- 按需动态导入,需要用到某个模块时,再加载这个模块,动态导入的模块会被自动分包
// 使用时动态import组件
if (true) {
import('./src/heading.js').then(({ default: heading }) => {
// 处理逻辑
})
}
// 魔法注释,会使得打包的文件使用注释的名称作为打包文件的名字,如果多个模块都使用相同的魔法注释,则这几个模块都会被打包进同一个文件中
if (true) {
import(/* webpackChunkName: 'heanding' */'./src/heading.js').then(({ default: heading }) => {
// 处理逻辑
})
}
vue、react等单页应用,路由组件动态导入会自动分包
- 输出文件名Hash,在开启静态资源缓存时,及时替换客户端缓存文件,通过配置filename来实现,一般使用contenthash:8
/**
* hash:项目级别,项目中任一地方改都改,所有打包的名称都会改变
* chunkhash:同一chunk的文件改变,会重新打包生成新文件
* contenthash:文件级别,对应文件改变时,只会重新打包改变此文件
* :8代表hash长度
*/
output: {
filename: '[name]-[contenthash:8].bundle.js',
}
配置代码总结
webpack.config.js
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const ClearCommentsWebpackPlugin = require('./src/assets/js/clearCommentsWebpackPlugin')
module.exports = {
mode: 'none', // 打包模式development、production、none
entry: './src/index.js', // webpack打包入口文件,相对路径时'./'不能省略
output: { // webpack打包输出配置,为一个对象
filename: 'bundle.js', // 输出文件名称
path: path.join(__dirname, 'dist'), // 输出文件目录,为一个绝对路径
// publicPath: 'dist/', // 设置静态资源目录,/不能省略
},
module: { // 配置资源模块加载器
rules: [
{
test: /.md$/,
use: [
'html-loader',
'./src/assets/js/md-loader',
]
}, // 自定义解析markdown的loader
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
}, // es6+解析
},
// {
// test: /.html$/,
// use: {
// loader: 'html-loader',
// options: {
// // attrs: ['img:src', 'a:href'], // webpack4用法:默认只解析img:src,其他属性需要配置
// sources: { // webpack5用法
// list: [
// {
// tag: 'img',
// attribute: 'src',
// type: 'src',
// },
// {
// tag: 'a',
// attribute: 'href',
// type: 'src',
// },
// ],
// },
// },
// }, // html解析
// },
{
test: /.css$/,
use: [
'style-loader',
'css-loader',
], // css解析=>style解析,loader由后向前的顺序解析
},
// {
// test: /.png$/,
// use: 'file-loader', // 文件解析,直接拷贝物理文件
// },
{
test: /.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024, // 限制文件为10KB以下的转换为data-url,超过10KB由file-loader处理
},
} // 文件解析,转换成data-url编码
},
]
},
plugins: [
new CleanWebpackPlugin(), // 自动清除打包文件夹
new HtmlWebpackPlugin({
title: 'webpack sample',
template: './index.html',
}), // 自动生成html
// new CopyWebpackPlugin([
// 'public',
// ]), // 拷贝文件,webpack4用法
new CopyWebpackPlugin({
patterns: [
'public',
],
}), // 拷贝文件,webpack5用法
new ClearCommentsWebpackPlugin(),
],
}
$ yarn build