本文最后更新于:2022年3月22日 下午
🧨 大家好,我是 Smooth,一名大二的 SCAU 前端er
🏆 本篇文章会带你入门 Webpack 并对基本配置以及进阶配置做比较通俗易懂的介绍!
🙌 如文章有误,恳请评论区指正,谢谢!
自定义 Loader 内容于 2022/02/25 更新
自定义 Plugin 内容于 2022/02/26 更新
Webpack
本教程包管理方式统一使用 npm
进行讲解
学习背景
由于在平时使用 vue、react 框架进行项目开发时,vue-cli 和 create-react-app 脚手架已经为你默认配置了 webpack 的常用参数,所以没有额外需求不用另外配置 webpack。
但在一次项目开发中,突然自己想给项目增加一个优化打包速度和体积的需求,所以就开始学习起了 webpack,逐渐明白了这打包工具的强大之处,在查看了脚手架给我们默认配置好的 webpack 配置文件后,也明白了脚手架这种东西的便捷之处。
当然,系统学习后 webpack,你也可以做到去阉割脚手架的 webpack 配置文件用不到的一些配置选项,并增加一些你需要的配置,例如优化打包体积、提升打包速度等等。
此篇文章便为你进行系统地讲解 webpack 的基本使用
PS:对于 webpack 的配置文件,vue-cli 可以通过修改 vue.config.js 进行配置修改,create-react-app 需要通过 craco 覆盖,或 eject 进行暴露。
webpack介绍
webpack
是什么
bundler:模块打包工具
webpack
作用
对项目进行打包,明确项目入口,文件层次结构,翻译代码(将代码翻译成浏览器认识的代码,例如import/export)
webpack
环境配置
webpack
安装前提:已安装 node
(node安装在此不做赘述),用指令 node -v
和 npm -v
来测试node安装有没成功
1 2 3 4 5 6 7 8 9 10 11 12
| npm install webpack webpack-cli --save-dev // 推荐,--save-dev结尾(或直接一个 -D),该项目内安装 npm install webpack webpack-cli -g // 不推荐,-g结尾,全局安装(如果两个项目用的两个webpack版本,会造成版本冲突)
安装后查询版本: webpack -v:查找全局的 webpack 版本,非 -g 全局安装是找不到的 npx webpack -v:查找该项目下的 webpack 版本
其他指令: npm init -y:初始化 npm 仓库,-y 后缀意思是创建package.json文件时默认所有选项都为yes npm info webpack:查询 webpack 有哪些版本号 npm install webpack@版本号 webpack-cli -D:安装指定版本号的webpack npx webpack;进行打包
|
webpack-cli
和 webpack
区别:
webpack-cli
能让我们在命令行运行webpack
相关指令,例如 webpack
, npx webpack
等等
webpack
配置文件
默认配置文件:webpack.config.js
1 2 3 4 5 6 7 8 9
| const path = require('path'); module.exports = { mode: "production", // 环境,默认 production 即生产环境,打包出来的文件经过压缩(可以不写),development没压缩 entry: 'index.js', // 入口文件(要写路径) output: { // 出口位置 filename: 'bundle.js', // 出口文件名 path: path.resolve(__dirname, 'bundle'), // 出口文件打包到哪个文件夹下,参数(绝对路径根目录下,文件名) } }
|
如果想让 webpack
按其他配置文件规则进行打包,比如叫做 webpackconfig.js
小问题:
为什么使用react
、vue
框架打包项目文件时不是输入 npx webpack
而是输入 npm start/npm run dev
等等?
原因:更改 package.json
文件里的 scripts 脚本指令(该文件:项目的说明,包括所需依赖、可运行脚本、项目名、版本号等等)
1 2 3 4 5
| { "scripts": { "bundle": "webpack" // 运行 npm run 脚本名,相当于运行 原始指令,即 `npm run bundle -> webpack` } }
|
回顾:
1 2 3
| webpack index.js // 全局安装 webpack 后,单独对这个js文件进行打包 npx webpack index.js // 局部(项目内)安装 webpack 后,单独对这个js文件进行打包 npm run bundle -> webpack // 运行脚本,进行 webpack 打包,先在项目内查找webpack进行打包,没有再全局----前两者融合
|
后面开始用 npm run bundle
代替 webpack
进行打包
Webpack 基本概念
webpack
的 Concepts
板块
官方文档
Loader
Loader 是什么?
由于 webpack 默认只认识、支持打包js文件,想要拓展其能力进行打包 css文件、图片文件等等,需要安装 Loader 进行拓展
Loader 的使用
在 webpack.config.js
的配置文件中进行配置
在文件中新增 module
字段,module
中新增 rules
的数组,进行一系列规则的配置,每个规则对象有两个字段
test
:匹配所有以 xxx 为后缀的文件的打包,用正则表达式进行匹配
use
:指明要使用的 loader 名称 ,且要对该 loader 进行安装
拓展,use
还有 options
可选择字段,name
指明打包后的文件命名,[name].[ext]
代表打包后和打包前 命名
和 后缀
一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| { module: { rules: [ { test: /.jpg$/, use: { loader: 'file-loader', options: { name: '[name].[ext]' } } } ] } }
|
在项目根目录下使用 npm install loader名字
或 yarn add loader名字
进行所需 loader 的安装
常用 Loader 推荐
babel-loader
、style-loader
、css-loader
、less-loader
、sass-loader
、postcss-loader
、url-loader
、file-loader
等等
图片(Images)
打包图片文件
图片静态资源,所以都对应 file-loader
,且一般项目中这些静态资源被放到 images
文件夹,通过 use
字段配置额外参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| { module: { rules: [ { test: /.(jpg|png|gif)$/, use: { loader: 'file-loader', options: { name: '[name].[ext]', outputPath: 'images/' // 匹配到上面后缀的文件时,都打包到的的文件夹路径 } } } ] } }
|
当然,对于 file-loader
,url-loader
会更具拓展性
推荐用 url-loader
进行替换,因为可以设置limit
参数,当图片大于对应字节大小,会打包到指定文件夹目录,若小于,则会生成base64
(不会打包图片到文件夹下,而是生成 base64
到 output
的js
文件里 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| { module: { rules: [ { test: /.(jpg|png|gif)$/, use: { loader: 'url-loader', options: { name: '[name].[ext]', outputPath: 'images/', // 匹配到上面后缀的文件时,都打包到该文件夹路径 limit: 2048 // 指定大小 } } } ] } }
|
样式(CSS)
打包样式文件
需要 css-loader
和 style-loader
,在 use
字段进行配置
说明:设置 css 样式后,挂载到 style 的属性上,所以要两个
1 2 3 4 5 6 7 8 9 10
| { module: { rules: [ { test: /.css$/, use: ['style-loader', 'css-loader'] } ] } }
|
对于 scss
文件,除了上面两个 loader 以外,还要在 use 中额外配置 sass-loader
,然后安装两个文件
npm install sass-loader node-sass webpack --save-dev
注意事项:
use
中的 loader
数组,是有打包顺序的,按从右到左,从上到下,即 scss,要先 style,然后 css,最后sass,从右到左
1
| use: ['style-loader', 'css-loader', 'sass-loader']
|
postcss.loader
对于样式,如果老版本的浏览器可能需要兼容,即在 css 属性中加 -webkit
等前缀,可以通过 postcss.loader
实现,在上面例子在后面加上这个 loader 并进行下载后,新建一个 postcss.config.js
文件进行该 loader 的配置即可
1 2 3 4 5
| module.exports = { plugins: [ require('autoprefixer') ] }
|
样式拓展
如何让 webpack 识别 less 文件内再引入的 less 文件,并进行打包?
如何模块化导出和使用样式?(css in js)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| { module: { rules: [ { test: /.scss$/, use: ['style-loader', { loader: 'css-loader', options: { importLoaders: 2, // 允许less文件内引入less文件 modules: true // 允许模块化导入导出使用css,css in js 同理 } }, 'sass-loader, 'postcss-loader' ] } ] } }
|
字体(Fonts)
打包字体文件(借助iconfont)
从 iconfont 网站下载对应图标的字体文件并压缩到目录后,会发现由于下载的 iconfont.css
文件内部又引入了 eot、ttf、svg
文件,webpack无法识别,引入需给这三个后缀的文件再配置打包规则,用 file-loader
即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| { module: { rules: [ { test: /.scss$/, use: ['style-loader', { loader: 'css-loader', options: { importLoaders: 2, // 允许less文件内引入less文件 modules: true // 允许模块化导入导出使用css,css in js 同理 } }, 'sass-loade, 'postcss-loader' ] }, { // 配置这个规则即可 test: /.(eot|ttf|svg)$/, use: { loader: 'file-loader' } } ] } }
|
自定义 Loader
首先明确最基本的,编写 Loader
其实就是编写一个函数并暴露出去给 Webpack
使用
例如编写一个 replaceLoader
,作用是当遇到某个字符时替换成其他字符,例如遇到 hello
字符串时,替换成 hi
1 2 3 4 5
| // 在根目录的 loaders 文件夹下的 replaceLoader.js 即路径:'./loaders/replaceLoader.js'
module.exports = function(source) { return source.replace('hello', 'hi'); }
|
这样,一个简易的 Loader 就写好啦
注意
暴露的函数不能写成箭头函数,即不能写成如下:
1 2 3 4 5
|
module.exports = (source) => { return source.replace('hello', 'hi'); }
|
由于箭头函数没有 this
指针,而 Webpack
在使用 Loader
时会做些变更,绑定一些方法到 this
上,所以会没法调用原本属于 this
的一些方法了。
例如:获取传入 Loader
的参数是通过 this.query
获取
当然,要用你自定义的 Loader,除了上面的编写 Loader 外,还需要对他进行使用,在 Webpack
配置文件进行相关配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| // webpack.config.js
const path = require('path');
module.exports = { mode: 'development', entry: { main: './src/index.js', }, module: { rules: [{ test: /.js/, use: [ path.resolve(__dirname, './loaders/replaceLoader.js') // 这里要书写该 js 文件的路径 ] }] }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js' } }
|
如果你想往你的自定义 Loader 传入一些参数,传参的方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| // webpack.config.js
const path = require('path');
module.exports = { mode: 'development', entry: { main: './src/index.js', }, module: { rules: [{ test: /.js/, use: [ { loader: path.resolve(__dirname, './loaders/replaceLoader.js'), // 这里要书写该 js 文件的路径 options: { name: 'hi' } } ] }] }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js' } }
|
这样,Webpack
在进行打包时,会将 {name: 'hi'}
参数传入 replaceLoader.js
在 replaceLoader.js
中,参数的接收形式如下:
1 2 3 4 5
|
module.exports = (source) => { return source.replace('hello', this.query.name); }
|
这样,原项目所有 js 文件中的 hello
字符串都被替换成了 hi
这样,一个简易的 Loader
就完成啦
更多
loader-utils
但有时往自定义 Loader 传参时会比较诡异,例如上述例子,传入的明明是一个对象,但可能变成只有一个字符串 ,此时就需要用到 loader-utils
模块,对传入的参数进行分析,解析成正确的内容
使用方法
先运行 npm install loader-utils --save-dev
安装,然后
1 2 3 4 5 6 7 8
|
const loaderUtils = require('loader-utils');
module.exports = function(source) { const options = loaderUtils.getOptions(this); return source.replace('hello', options.name); }
|
callback()
有时,除了用自定义 Loader 对原项目做出更改以外,如果启用了 sourceMap
,还希望 sourceMap
对应的映射也发生更改,
由于该函数只返回了项目内容的更改,而没返回 sourceMap
的更改,所以要用 callback
做一些配置
1 2 3 4 5 6
| this.callback( err: Error | null, content: string | Buffer, sourceMap?: SourceMap, meta?: any )
|
通过该函数进行回调,可以返回除了项目内容更改外,还可以返回 sourceMap 、错误、meta 的更改
由于,我只需要返回项目内容以及 sourceMap
的更改,所以配置示例如下:
1 2 3 4 5 6 7 8 9 10
|
const loaderUtils = require('loader-utils');
module.exports = function(source) { const options = loaderUtils.getOptions(this); const result = source.replace('hello', options.name); this.callback(null, result, source); }
|
async()
自定义 Loader 中有时会有异步操作,例如设置延时器1s后再进行打包(方便摸鱼),那如果直接 setTimeout(),设置一个延时器再返回肯定是不行的,会报错无返回内容,因为正常来说是不允许在延时器中返回内容的。
我们可以通过 async() 来解决,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
const loaderUtils = require('loader-utils');
module.exports = function(source) { const options = loaderUtils.getOptions(this); const callback = this.async(); setTimeout(() => { const result = source.replace('hello', options.name); callback(null, result); }, 1000); }
|
可以看出,其实 async()
跟 callback()
很类似,只不过用于异步返回而已
同时自定义多个 Loader
例如想实现一个需求:打包后项目先是将项目中的所有字符串 hello
替换成 hi
,再把 hi
替换成 Wow
那么就要编写两个 Loader,第一个将 hello
替换成 hi
,第二个将 hi
替换成 Wow
第一个 replaceLoader.js
1 2 3 4 5 6 7 8 9 10 11 12 13
|
const loaderUtils = require('loader-utils');
module.exports = function(source) { const options = loaderUtils.getOptions(this); const callback = this.async(); setTimeout(() => { const result = source.replace('hello', options.name); callback(null, result); }, 1000); }
|
第二个 replaceLoader2.js
1 2 3 4 5
|
module.exports = function(source) { return source.replace('hi', 'wow'); }
|
同时对 webpack.config.js
进行配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| // webpack.config.js
const path = require('path');
module.exports = { mode: 'development', entry: { main: './src/index.js', }, module: { rules: [{ test: /.js/, use: [ { loader: path.resolve(__dirname, './loaders/replaceLoader2.js') }, { loader: path.resolve(__dirname, './loaders/replaceLoader.js') // 这里要书写该 js 文件的路径, options: { name: 'hi' } }, ] }] }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js' } }
|
要注意的地方
由于前面提到 Loader
执行顺序是从下到上,从右到左,所以要将第一个写在下面,第二个写在上面
Loader 引入转换成官方的引入方式
在上面的例子中,引入 loader
时,方式都是
1
| loader: path.resolve(__dirname, './loaders/replaceLoader2.js')
|
太长了,太麻烦了,不美观,想更换成官方的引入方式,该怎么做呢
Webpack
配置文件中配置 resolveLoader
字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| // webpack.config.js
const path = require('path');
module.exports = { mode: 'development', entry: { main: './src/index.js', }, resolveLoader: { modules: ['node_modules', './loaders'] }, module: { rules: [{ test: /.js/, use: [ { loader: 'replaceLoader2' }, { loader: 'replaceLoader', // 这里要书写该 js 文件的路径, options: { name: 'hi' } }, ] }] }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js' } }
|
参数意思:如果在 node_modules
文件夹下没找到配置的 Loader,那么就会进入同级目录下的 loaders
文件夹进行查找
更多 Loader 的设计思考
推荐一些自定义的实用的 loader
- 全局异常监控,思路:给所有函数外面包裹
try{} catch(err) {console.log(err)}
语句
- style-loader
1 2 3 4 5 6 7 8
| module.exports = function(source) { const style = ` let style = document.createElement("style"); style.innerHTML = ${JSON.stringify(source)}; document.head.appendChild(style) ` return style; }
|
Plugins
使用插件让打包更快捷、多样化
在打包的某个生命周期,插件会帮助你做一些事情
下面介绍几个常用插件
html-webpack-plugin
作用:由于 webpack 默认打包不会生成 index.html 文件, htmlWebpackPlugin
会在打包结束后,自动生成一个html文件,并把打包生成的js自动引入到这个html文件中
插件运行生命周期:打包之后
参数:对象
template
指定一个模板,打包生成的 html 文件根据这个模板来生成,比如会多生成一个 <div id="root"></div>
1 2 3
| new HtmlWebpackPlugin({ template: 'index.html' })
|
clean-webpack-plugin
作用:打包前删除某个目录下的所有内容,主要用于删除之前的打包内容,防止重复
插件运行生命周期:打包之前
参数:数组形式
[‘要删除的文件夹名’]
1
| new HtmlWebpackPlugin(['dist'])
|
自定义一个 Plugin
首先明确最基本的,编写 plugin
其实就是编写一个类并暴露出去给 Webpack
在打包的某个生命周期进行相关操作。
例如编写一个 copyright-webpack-plugin
1 2 3 4 5 6 7 8 9 10 11 12 13
|
class CopyrightWebpackPlugin { constructor() { console.log('插件被使用了') } apply(compiler) { } }
module.exports = CopyrightWebpackPlugin;
|
webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| // webpack.config.js
const path = require('path'); const CopyRightWebpackPlugin = require('./plugins/copyright-webpack-plugin');
module.exports = { mode: 'development', entry: { main: './src/index.js', }, plugins: [ new CopyRightWebpackPlugin() ], output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js' } }
|
这样,一个简易的 Plugin
就完成啦
更多
往插件里传参
在 Webpack 配置文件创建插件实例时,同时传入参数就行
1 2 3 4 5 6
| plugins: [ new CopyRightWebpackPlugin({ name: 'Smoothzjc' }) ],
|
这样,就可以在类的构造函数中接收到该参数了
1 2 3 4 5 6 7 8 9 10 11
| class CopyrightWebpackPlugin { constructor(options) { console.log('我是', options.name) } apply(compiler) { } }
module.exports = CopyrightWebpackPlugin;
|
不同生命周期
前面我有提到,在打包的某个生命周期,插件会帮助你做一些事情,
所以我们可以在 Webpack
打包的不同生命周期时,写一些想让 Webpack 帮我们做的事
常用生命周期:
emit
异步钩子,打包完成准备将打包内容放到生成目录前,即打包完成的最后时刻
compile
同步钩子,准备进行打包前
下面示例的一些参数解释:
compiler
配置的所有内容,包括打包相关的内容
compilation
本次打包的所有内容
如果你想在打包完成前新加一个文件到打包目录下,可以配置 compilation
的 assets
属性
相关代码运行在 apply
属性中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| class CopyrightWebpackPlugin { constructor(options) { console.log('我是', options.name) } apply(compiler) { compiler.hooks.compile.tap('CopyrightWebpackPlugin', () => { console.log('同步钩子 compile 生效'); }) compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => { compilation.assets['copyright.txt'] = { source: funciton() { return 'copyright write by Smoothzjc' }, size: function() { return 28 } } cb(); }) } }
module.exports = CopyrightWebpackPlugin;
|
编写插件时进行调试
大部分调试工具都是基于 node 编写,在此我举个例子,如何在编写 plugin
时使用调试工具进行 debug
- 先添加脚本指令,通过
node
运行调试工具
1 2 3 4 5 6 7 8
|
{ "scripts": { "debug": node --inspect --inspect-brk node_modules/webpack/bin/webpack.js, "build": "webpack" } }
|
- 在需要调试的地方打断点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class CopyrightWebpackPlugin { constructor(options) { console.log('我是', options.name) } apply(compiler) { compiler.hooks.compile.tap('CopyrightWebpackPlugin', () => { console.log('compiler'); }) compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => { debugger; compilation.assets['copyright.txt'] = { source: funciton() { return 'copyright write by Smoothzjc' }, size: function() { return 28 } } cb(); }) } }
module.exports = CopyrightWebpackPlugin;
|
控制台运行 debug
指令 npm run debug
后
打开浏览器按 F12 打开控制台,可以在开发者工具左上角看到 node 图标,点击即可进入 webpack 打包时经过的一些页面
- 可以将鼠标放上想查看的变量的属性
- 或在右边的
Watch
属性输入想查看的属性名称进行查看
Entry
打包的入口文件,并指定打包后生成的 js文件名
参数:字符串 或 一个对象,默认生成的文件名是 main
,即 生成的文件名:'入口文件路径'
1 2 3 4 5 6
| entry: './src/index.js' 或 entry: { main: './src/index.js' } 同时上面跟下面的等价
|
同时也可以打包成多个 js 文件,即多入口
1 2 3 4
| entry:{ main: './src/index.js', sub: './src/index.js' }
|
Output
输出js文件名
参数:
filename
最后打包出来的 js 文件名,可以直接 bundle.js
指定,也可以 [name].js
根据 entry 指定的名字,也可以 [hash].js
根据 entry 指定的哈希值
chunkFilename
通过异步引入的文件的文件名
publicPath
给打包出来的 js 文件的 src 引入路径都加个前缀,一般用于 cdn 配置
publicPath 详解
例如:
index.html
中引入 js 板块的代码
1
| <script type="text/javascript" src="main.js"></script>
|
如果想将打包后的js 文件都放到 cdn上,减少打包后体积(此时该js就不用放在打包后的文件夹中了),例如
1
| <script type="text/javascript" src="http://cdn.com.cn/main.js"></script>
|
可配置成如下
output配置示例
1 2 3 4 5 6
| output: { publicPath: 'http://cdn.com.cn', filename: '[name].js', chunkFilename: '[name].chunk.js', path: path.resolve(__dirname, 'dist') }
|
resolve
模块引入时的更多拓展操作
extensions
模块引入时的后缀名查找
alias
为路径配置别名
1 2 3 4 5 6
| resolve: { extensions: ['.css', '.jpg', '.js', '.jsx'], alias: { '@': '/src/pages' // 当输入 @ 时,自动替换为 /src/pages } }
|
extensions
在项目中以下面的方式引入模块时
1
| import Child from './child'
|
由于没写文件后缀名,会通过上面的配置按序查找,先找 child.css
存不存在如果存在就引入这个,如果不存在,则继续找 child.jpg
存不存在,一直找到 child.jsx
,如果还不存在,就会报错
alias
为路径配置别名
1 2 3
| alias: { '@': '/src/pages' }
|
解释:当输入 @ 时,自动替换为 /src/pages
常用场景:当你频繁使用根路径的方式引用某些文件,比如:
1 2 3 4
| import a from '/src/pages/a.js'; import b from '/src/pages/b.js'; import c from '/src/pages/c.js'; import d from '/src/pages/d.js';
|
写多次 /src/pages
会很麻烦,用 @
代替 /src/pages
会简化了输入,提高开发效率
注意:resolve
要进行合理配置,不然会降低性能,因为如果你要找 child.jsx
,按照上面配置,要经过前面三个没必要的步骤
SourceMap
打包后的文件是否开启映射关系,他知道打包后文件与打包前源代码文件的代码映射
例如:知道 dist 目录下 main.js 文件96行报错,实际上对应的是 src 目录下 index.js 文件中的第一行
通常不用开启,默认 none
关闭,因为开启后会减缓打包速度和增大打包体积
参数
1 2 3 4 5 6 7
| devtool: '参数'
'none' // 不开启source-map 'source-map' // 开启source-map进行映射 'inline-source-map' // 开启source-map进行映射的前提下,精确到哪一行哪一列 'cheap-source-map' // 开启source-map进行映射的前提下,只精确到哪一行 'eval' // 通过 eval 开启映射,效率最快,但不全面
|
推荐:
开发环境:devtool: 'cheap-module-eval-source-map'
生产环境(线上环境):devtool: 'cheap-module-source-map'
生产环境一般不用配置 devtool,但如果想报错时快速定位错误,可开启,建议使用上面推荐的参数
mode: 'development'
是开发环境
mode: 'production'
是生产环境
更多其他参数查看下表
SourceMap 配置示例
1
| devtool: 'cheap-module-source-map'
|
WebpackDevServer
开启一个本地web服务器,可提高开发效率
webpack
指令(一般直接配置第二个脚本就行)
1 2
| 1. webpack --watch 保存后自动重新打包 2. webpack-dev-server 启动一个web服务器,并将对应目录资源进行打开,对应目录资源修改后保存会重新进行打包,且自动对网页进行刷新
|
我们下载的每个项目都经过两条指令(安装依赖 + 打包运行),下面以 create-react-app 脚手架生成的 react 项目为例
1 2
| npm install npm run start
|
第二步,其实就是运行 WebpackDevServer
,你会发现,start 后会直接打开浏览器,且每次保存后都会自动重新打包、重新刷新网页。
WebpackDevServer
隐藏特性:打包后的资源不会生成一个 dist 文件夹,而是将打包后资源放在电脑内存中,能有效提高打包速度
参数
contentBase
将哪个目录下的文件放到 web 服务器上进行打开
open
是否在打包时同时打开浏览器访问项目对应预览 url
port
端口号
proxy
设置代理
WebpackDevServer 配置示例
1 2 3 4 5 6 7 8
| webpackDevServer: { contentBase: './dist', open: true, port: 8080, proxy: { 'api': 'xxxxx' } }
|
拓展内容
其实相当于自己手写一个 webpack-dev-server,但人家官方已经帮我们写好一个各配置项都齐全的一个了,自己不用手写了,只是带大家进行拓展,理解一下 webpack-dev-server 背后的源码是如何搭配 node 实现的
在 node 中使用 webpack
查看官方文档 的 Node.js API
板块
在命令行中使用 webpack
查看官方文档 的 Command Line Interface
板块
Hot Module Replacement
热模块更新 HMR
当内容发生更改时,只有更改的那部分发生变化,其他已加载的部分不会重新加载(例如修改css样式,只有对应样式更改,js不会改变)
参数
hot
开启热模块更新
hotOnly
设置为 true 后,无论热更新是否开启,都禁用 webpackDevServer
的保存后自动刷新浏览器功能
也是配置到 webpackDevServer
里
HMR
配置示例
1 2 3 4 5 6 7 8
| const webpack = require('webpack'); devServer: { hot: true, hotOnly: true } plugins: [ new webpack.HotModuleReplacementPlugin() ]
|
上面是让 HMR
生效,下面是对 HMR
进行使用
1 2 3 4 5 6 7 8
| // 例子:当 number.js 发生更改时,会调用函数 import number from './number'; number(); if(module.hot) { module.hot.accept('./number', () => { number(); }) }
|
但上面的 使用 一般不用写,因为其实很多地方都已经写好了,内置了 HMR
组件,例如css的话 css-loader
里面给你写好了,vue 的话 vue-loader
里写好了,react 的话 babel-preset
写好了。
如果你要引入比较冷门的数据文件,没有内置 HMR
,就需要写。
使用 Babel 处理 ES6 语法
babel-loader
、@babel/preset-env
、@babel/polyfill
- babel-loader 的配置选项可以单独写进
.babelrc
文件里
- 除了将 ES6 转换成 ES5 还不够,有些低版本浏览器还需要将 Promise、Array.map 注入额外代码,需要引入
@babel/polyfill
1 2 3 4 5 6 7
| @babel/preset-env 的参数
useBuiltIns: 'usage' // 对于使用的代码,才转译成 ES5 并打包至 dist 文件夹 targets: 该代码运行环境,根据环境来判定是否要做 ES6 的转化 { chrome: '67' // 谷歌浏览器版本大于67,对 ES6 能直接正常编译,所以没必要做 ES5 的转换了 }
|
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| module: { rules: [ { test: /.js$/, exclude: /node_modules/, loader: "babel-loader", options: { presets: [["@babel/preset-env", { targets: { edge: '17', firefox: '60', chrome: '67', safari: '11.1' }, useBuiltIns: 'usage' }]] plugins: [["babel/plugin-transform-runtime", { "corejs": 2, "helpers": true, "regenerator": true, "useESModules": false }]] } } ] }
|
如果你想在 react 使用 babel
实现对 React 框架代码的打包
下载 @babel/preset-react
.babelrc 文件
1 2 3 4 5 6 7 8 9 10 11 12 13
| { presets: [ [ "babel/preset-env", { targets: { chrome: "67", }, useBuiltIns: "usage" } ], "@babel/preset-react" ] }
|
Webpack 高级概念
webpack
的 Guides
板块
官方文档
Tree Shaking
只会对引入进行使用的代码进行打包,没引入进行使用的代码不会打包(可减少代码体积),webpack 2.0 之后默认开启该功能
1 2
| import { } from '' const xxx = require('')
|
development(开发环境) 默认不打开 Tree Shaking
因为如果开发环境进行调试时,如果每次重新编译打包后的代码都进行了 Tree Shaking
,那就会让 debug 时代码行数对不上,不利于调试
如果你想开发环境打开
1 2 3 4 5 6 7 8 9 10
| { "sideEffects": false 或 数组 }
如果是数组,则配置你不想哪些代码进行 Tree Shaking 例如:如果不想 css 文件进行 Tree Shaking,则 { "sideEffects": ["*.css"] }
|
Development 和 Production 模式的区分打包
通常来说,两个环境的 webpack 配置文件不会有变化,但如果非得区分,可以不同文件形式
webpack.dev.js
根据名字可知,是 development(开发环境)
webpack.proud.js
根据名字可知,是 production(生产环境)
打包脚本也要更改,如果区别开
1 2 3 4 5 6 7 8 9 10
| { "scripts": { "dev-build": webpack --config webpack.dev.js, 或 "proud-build": webpack --config webpack.proud.js, } }
|
Webpack 和 Code Splitting
为什么要进行代码分割?
如果用户一个页面要加载的 js 文件很大,足足有2MB,那么用户每次访问这个页面,都要加载完2MB的资源页面才能正常显示,但其中可能有很多代码块是当前页面不需要使用的,那么将没使用的代码分割成其他 js 文件,当要使用的时候再进行加载、当页面变更时只有那部分进行重新加载,这样可以大大加快页面加载速度。
即通过配置进行合理的代码分割,能让文件结构更清晰,项目运行更快,比如如果用到 lodash 库,就分割出来。
下面通过一个例子进行解释:
1 2 3 4 5 6 7 8 9
| 假设现在有 main.js(2MB),里面含有 lodash.js(1MB) 1. 该种方式 首次访问页面时,加载 main.js(2MB) 当页面业务逻辑发生变化时,又要重新加载2MB内容
2. 将 lodash.js 抽离出来,即现在是 main.js(1MB) 和 lodash.js(1MB) 由于浏览器的并行机制,首次访问页面时,并行渲染两个1MB的文件是要比只渲染一个2MB的文件要快的。 其次,当页面业务逻辑发生变化时,只要重新加载 main.js(1MB) 即可。
|
同时,如果对于两个文件,如果都有用到某个模块,如果两个文件各自写一次这个模块,就会有重复,此时如果将这个公共模块抽离出来,两个文件分别去引用他,那么就会减少一次该模块的撰写(减少包体积)。
即对代码进行合理分割,还可以加快首屏加载速度,加快重新打包速度(包体积减少)
代码分割:通俗解释就是将一坨代码分割成多个 js 文件
代码分割自己可以手动,例如我们平时的抽离公共组件,但为什么现在 webpack
几乎跟 Code Splitting
绑定在一起了呢?
因为 webpack
中有一个插件 SplitChunksPlugin
,会让代码分割变得非常简单, 这也是 webpack
的一个强大的竞争力点
1 2 3 4 5 6 7 8
| { optimization: { splitChunks: { chunks: 'all' } } }
|
总结一下
Webpack
进行 Code Splitting
有两种方式
1. 通过配置插件 SplitChunksPlugin
1 2 3 4 5 6 7 8
| { optimization: { splitChunks: { chunks: 'all' } } }
|
然后编写同步代码
1 2
| import _ from 'lodash';
|
2. 通过异步地动态引入
1 2 3 4 5 6 7 8 9 10 11
| function getComponent() { return import('lodash').then(({ default: _ }) => { let element = document.createElement('div'); element.innerHTML = _.join(['zjc', 'handsome'], '-'); return element; }) }
getComponent().then(element => { document.body.appendChild(element); })
|
当然,想要支持异步地动态引入某个模块,需要先下载 babel-plugin-dynamic-import-webpack
然后
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| // .babelrc { presets: [ [ "@babel/preset-env", { targets: { chrome: "67" }, useBuiltIns: 'usage' } ], "@babel/preset-react" ], plugins: ["dynamic-import-webpack"] }
|
SplitChunksPlugin
该板块会对该插件配置参数进行详解
重要作用是可以减少包体积,缓存组中的 reuseExistingChunk
属性:开启true后,如果要分割进该组的模块在之前已经被缓存到了某个组内,那就不会再缓存
配置示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| module.exports = { //... optimization: { splitChunks: { chunks: 'all', // 只对哪些代码进行分割(all 的话全部都,async 的话只对异步代码进行分割) minSize: 30000, // 要做代码分割的引入库的最小大小,即30000代表如果你引入的库大小超过30KB才做代码分割 minRemainingSize: 0, minChunks: 1, // 一个库被引入至少多少次才做代码分割 maxAsyncRequests: 5, // 同时分割的库数 maxInitialRequests: 3, // 最多能分割出多少个 js 文件 automaticNameDelimiter: '~', // 代码分割出来的 js 文件名 和下面的组名 用什么符号进行连接 name: 'true' // 当为 true 时,下面组的 filename 属性才会生效 enforceSizeThreshold: 50000, // 缓存组,当打包同步代码时,除了走完上面的设置流程,还会额外再走进下面的组设置,即代码分割进下面符合要求的各组 cacheGroups: { vendors: { test: /[\/]node_modules[\/]/, // 只有 node_modules 里面的库被引入时,才做代码分割到 vendor 组 priority: -10, // 优先级,越大优先级越高 reuseExistingChunk: true, filename: 'vendors.js', // vendors 组的代码分割都分割到 filename 文件内 name: 'vendors' // 生成 vendors.chunk.js,该属性和上面的 filename 写一个就行 }, default: { minChunks: 2, priority: -20, reuseExistingChunk: true, // 开启true后,如果要分割进该组的模块在之前已经被缓存到了某个组内,那就不会再缓存 }, }, }, }, };
|
更多配置项请看官方文档
Lazy Loading
通过异步地动态引入某个模块,通常在路由配置页面,对引入的组件进行懒加载
1 2 3 4 5 6 7 8 9 10 11
| function getComponent() { return import('lodash').then(({ default: _ }) => { let element = document.createElement('div'); element.innerHTML = _.join(['zjc', 'handsome'], '-'); return element; }) }
getComponent().then(element => { document.body.appendChild(element); })
|
打包分析
应用 webpack
官方工具 Bundle Analysis
Preloading、Prefetching
Preloading
懒加载,当进行某个事件时,才会引入某个组件,例如点击某个元素时,才会 import
引入组件
PreFetching
预加载,当主页面的核心功能和交互都加载完成后,如果网络空闲,那么就会预先加载某个组件,这样在某个时刻引入该组件时,就能一下子打开
实现预加载:
webpack
搭配魔法注释,在引入的路径前加上 webpackPrefetch: xxx
1 2 3 4 5
| document.addEventListener('click', () => { import( './click.js').then((func) => { func() }) })
|
这也是 webpack
最为推荐的首屏加载优化手段,异步引入 + 预加载
CSS 文件的代码分割
前面的 Code splitting
都是针对 js 的,将 js 文件进行代码分割,而打包出来的 css 都在 js 文件里
如果想 CSS 文件也代码分割出来,可以使用 MiniCssExtractPlugin
插件,该插件由于依赖热更新,所以只能运行在线上打包环境中
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = { plugins: [ new MiniCssExtractPlugin() ], module: { rules: [ { test: /.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"], }, ], }, };
|
且默认将引入的各 css 文件合并到同一个 css 文件里
如果你想代码分割出来的 css 文件做代码压缩,重复属性合并到一起,可以使用 OptimizeCSSAssetsPlugin
插件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = { optimization: { minimizer: [ new OptimizeCSSAssetsPlugin({}) ] }, plugins: [ new MiniCssExtractPlugin() ], module: { rules: [ { test: /.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"], }, ], }, }
|
如果想多入口引入的 css文件也合并在一起,同样需要用到代码分割
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = { optimization: { splitChunks: { cacheGroups: { styles: { name: 'styles', test: /.css$/, chunks: 'all', enforce: true } } }, plugins: [ new MiniCssExtractPlugin() ], module: { rules: [ { test: /.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"], }, ], }, } }
|
Webpack 与浏览器缓存(Cache)
当浏览器加载过某项资源时会在本地进行缓存记忆,这样当用户下次再重新访问该页面加载该资源时,浏览器可以根据缓存过的文件快速加载该资源,直到该文件名发生改变,浏览器才知道该文件发生改变,需要重新渲染。
浏览器该特性的作用
可以加快加载速度,当该页面某个部分发生改变时,可以只重新渲染改变的部分,做到局部渲染
问题:如何保证每次打包时只有做了更改的文件的文件名发生更改,没做更改的文件的文件名不变?
如果在 webpack
配置文件中的 output
属性设置为如下
1 2 3 4
| output: { filename: '[name].js', chunkFilename: '[name].chunk.js' }
|
如果对项目中文件做了更改,而文件名没变,打包的 filename 没变,由于浏览器已经加载过该文件,缓存了这个文件名,当你该文件发生更改而文件名没改变时,浏览器不会重新渲染,为了让每次文件更改后文件名都发生改变,可以使用哈希值命名
1 2 3 4
| output: { filename: '[name].[contenthash].js', chunkFilename: '[name].chunk.js' }
|
让 webpack
根据文件内容创建对应独立的一个哈希值,同时在文件内容发生改变时,由于哈希值也会改变,所以文件名也会改变
拓展
在老版本 webpack
中(webpack 4.0 以下),如果在每次运行 npm run build
进行打包时,发现即使文件没变更,每次重新打包他们的哈希值都会变,可以通过配置 runtimeChunk
解决
1 2 3 4 5 6
| optimization: { runtimeChunk: { name: 'runtime' } }
|
runtimeChunk 原理
假设我通过 webpack
打包出来有两个 js文件,A文件是业务逻辑相关代码,B文件是作代码分割时的库代码(例如 lodash),由于业务逻辑中有引入库的操作,所以他们之间会有关联,而这个关联的相关代码同时存在于A和B文件(这种关联我们一般称之为 manifest
),而在每次打包时,manifest
内置的包和包的关系、js和js文件的嵌套关系会发生微小改变,所以即使A和B文件没做更改时,打包出来的哈希值还是会发生变化。
通过配置 runtimeChunk
,可以将这些 manifest
相关的代码抽离出来单独放在 runtimeChunk
中,因此每次重新打包,改变的只有runtime.hash.js
,A文件只有业务逻辑,B文件只有库文件,A和B文件内都不会有任何 manifest
的代码了,这样A和B文件都不会发生改变了,因此哈希值就不会变了
Shimming
打包兼容,自动引入
在你页面使用到某个库,但没进行引入时,webpack
打包后的代码会帮你自动、”偷偷”进行引入
1 2 3 4 5 6 7
| plugins: [ new webpack.ProvidePlugin({ $: 'jquery', // 当使用 $ 时,会自动在那个页面引入 jquery 库 _: 'lodash', // 当使用 _ 时,会自动在那个页面引入 lodash 库 _join: ['lodash', 'join'] // 当输入 _join 时,会引入 lodash库的 join 方法 }) ]
|
更多
环境变量的使用
对于开发环境和生产环境,可能有时真的需要单独写不同的 webpack
配置文件进行配置
而单独写,肯定有许多属性是重复的,又不想多写,怎么办呢?
例如A和B文件是两个环境中不同的配置参数,而C文件是共同的配置文件,那么开发环境打包时希望按照 A+C 的打包规则,生产环境打包时希望按照 B+C的打包规则
可以通过 webpack-merge
配置 开发环境 和 生产环境 的 不同配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
const merge = require('webpack-merge'); const devConfig = require('./webpack.dev.js'); const prodConfig = require('./webpack.prod.js'); const commonConfig = { }
module.exports = (env) => { if(env && env.production) { return merge(commonConfig, prodConfig); } else { return merge(commonConfig, devConfig); } }
|
同时 package.json
文件中修改配置
1 2 3 4 5 6
| { scripts: { "dev": "webpack-dev-server --config webpack.common.js", "build": "webpack --env.production --config webpack.common.js" // 通过--env.production 传递参数进文件,执行线上环境的打包配置文件 } }
|
当然,脚本配置时,向配置文件传入参数也可以如下方式:
1
| "build": "webpack --env production --config webpack.common.js"
|
同时,webpack.common.js
参数判断时也要改成
1 2 3 4 5 6 7 8
| module.exports = (env, production) => { // 如果 env 参数存在,且传进来了 production 属性,说明是生产环境 if(env && production) { return merge(commonConfig, prodConfig); } else { return merge(commonConfig, devConfig); } }
|
🎁 谢谢你读完本篇文章,希望对你能有所帮助,如有问题欢迎各位指正。
🎁 我是 Smoothzjc,如果觉得写得可以的话,请点个赞吧❤
🎁 我也会在今后努力产出更多好文。
🎁 感兴趣的小伙伴也可以关注我的公众号:Smooth前端成长记录,公众号同步更新
写作不易,「点赞」+「收藏」+「转发」 谢谢支持❤
往期推荐
《都2022年了还不考虑来学React Hook吗?6k字带你从入门到吃透》
《Github + hexo 实现自己的个人博客、配置主题(超详细)》
《10分钟让你彻底理解如何配置子域名来部署多个项目》
《一文理解配置伪静态解决 部署项目刷新页面404问题
《带你3分钟掌握常见的水平垂直居中面试题》
《React实战:使用Antd+EMOJIALL 实现emoji表情符号的输入》
《【建议收藏】长达万字的git常用指令总结!!!适合小白及在工作中想要对git基本指令有所了解的人群》
《浅谈javascript的原型和原型链(新手懵懂想学会原型链?看这篇文章就足够啦!!!)》