Plugins
plugin
顾名思义为插件的意思,那么我们很容易就能想到 plugins
应该为插件集合。我们先看官方文档对 Plugins
给出的定义:webpack
有着丰富的插件接口,且 webpack
自身的多数功能都使用这个插件接口,这个插件接口使 webpack
变得极其灵活。这里先不做过多的解释,拿我们前面的项目先来看看使用场景。
在之前的操作中每次使用 npm run build
打包的时候都会生成一个 dist
文件夹,但是并不会主动在 dist
文件夹中生成对应的 index.html
页面,所以前面我们每次使用的时候都是先提前在 dist
文件夹中手动建一个 index.html
页面用来加载打包出来的 bundle.js
。如果我们希望每次打包构建后,不仅能帮我们生成 bundle.js
,同时也能帮我们生成 index.html
,那么我们就需要在 webpack.config.js
中进行 Plugins
的相关配置。
HtmlWebpackPlugin
依照官方文档,我们首先需要安装该插件
npm install --save-dev html-webpack-plugin
结合官网给出的基本用法,我们改造下 webpack.config.js
,毕竟里面很多内容都是前面配置 loader
写的代码,如果都放到这里可能太长了,不利于阅读理解。这里我将 module
里面的整体内容先删除了,但是该内容还是要有的啊,前面的文章里面都有该部分的整体代码,因为这里主要是配置 plugins
,所以以免内容过多影响学习。
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
main: './src/index.js'
}, // 项目打包的入口文件
plugins: [new HtmlWebpackPlugin()],
output: { // 打包的最终文件放在哪里
filename: 'bundle.js', // 打包生成的文件名
path: path.resolve(__dirname, 'dist') // 打包目录
}
}
我们在构建代码之前先把 dist
文件夹整个删除,使用上面的代码之后就会发现 webpack
帮我们构建了 bundle.js
的同时也帮我们构建了 index.html
,当然它构建的 index.html
虽然导入了 bundle.js
,但是 body
里面却没有我们的根节点 <div id="root"></div>
这一项。
我们回想一下使用 vue-cli
帮我们搭建的项目基础模板中好像 index.html
页面在根目录中一个叫 public
的文件夹中,那么我们可不可以也在 src
目录中新建一个 index.html
的模板文件,如果使用 webpack
自动构建的话就以这个 index.html
里面的内容为模板,而我们在打包的 dist
目录中的 index.html
可以直接整合外部模板里的内容。
有了思路我们就现在 src
目录下创建项目的模板文件 index.html
,然后修改 webpack.config.js
中的配置内容。
module.exports = {
entry: {
main: './src/index.js'
}, // 项目打包的入口文件
plugins: [new HtmlWebpackPlugin({
template: 'src/index.html'
})],
output: { // 打包的最终文件放在哪里
filename: 'bundle.js', // 打包生成的文件名
path: path.resolve(__dirname, 'dist') // 打包目录
}
}
这里我们先总结一下 HtmlWebpackPlugin
的作用
HtmlWebpackPlugin
会在打包结束后,自动生成一个html
文件,并把打包生成的js
自动引入到这个html
文件中。
此时回过头我们再来看看 plugin
的作用
plugin 可以在 webpack 运行到某个时刻的时候,帮你做一些事情。
回过头来再看问题,我们原来定义的打包生成的文件名为 bundle.js
,但是如果我们将其改为 dist.js
之后再次进行打包我们就会发现打包生成的 dist
目录下既有 bundle.js
也有 dist.js
,虽然这并不影响我们使用,但是极客精神告诉我们,如果我们每次打包之前都能先清理掉生成的 dist
文件夹,然后在重新生成,是不是会更好一点呢?
CleanWebpackPlugin
还是老规矩,先安装该插件
npm install clean-webpack-plugin -D
接着就是对 webpack.config.js
进行配置,同时记得修改打包输出文件的文件名进行验证哦~~~
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require("clean-webpack-plugin")
module.exports = {
entry: {
main: './src/index.js'
}, // 项目打包的入口文件
plugins: [new HtmlWebpackPlugin({
template: 'src/index.html'
}), new CleanWebpackPlugin()],
output: { // 打包的最终文件放在哪里
filename: 'dist.js', // 打包生成的文件名
path: path.resolve(__dirname, 'dist') // 打包目录
}
}
通过上述代码的执行我们就可以发现 cleanWebpackPlugin
会在每次打包运行前,自动删除 dist 目录。同时这个栗子也再次验证了我们先前得出的结论:plugin 可以在 webpack 运行到某个时刻的时候,帮你做一些事情。
Entry 和 Output
前面我们说过,在 entry
中做如下配置
module.exports = {
entry: {
main: './src/index.js'
}, // 项目打包的入口文件
output: { // 打包的最终文件放在哪里
filename: 'dist.js', // 打包生成的文件名
path: path.resolve(__dirname, 'dist') // 打包目录
}
}
其实等价于下面这种:
module.exports = {
entry: './src/index.js', // 项目打包的入口文件
output: { // 打包的最终文件放在哪里
filename: 'dist.js', // 打包生成的文件名
path: path.resolve(__dirname, 'dist') // 打包目录
}
}
如果我们不配置 output
中的 filename
,则默认生成的打包名为 main.js
。这里延伸出一个问题,如果我们希望对 index.js
进行多次打包,那么结果会怎么样呢?
module.exports = {
entry: {
main: './src/index.js',
sub: './src/index.js'
}, // 项目打包的入口文件
output: { // 打包的最终文件放在哪里
filename: 'dist.js', // 打包生成的文件名
path: path.resolve(__dirname, 'dist') // 打包目录
}
}
直接运行最后会报错,它会提示我们:打包生成的两个文件都使用了 dist.js
这个名字造成了命名冲突,所以此时我们需要在 output
中做一些配置修改:
module.exports = {
entry: {
main: './src/index.js',
sub: './src/index.js'
}, // 项目打包的入口文件
output: { // 打包的最终文件放在哪里
filename: '[name].js', // 打包生成的文件名,使用占位符匹配打包文件的 key 值
path: path.resolve(__dirname, 'dist') // 打包目录
}
}
运行上述代码,我们发现果然给我们在 dist
目录下生成了 main.js
和 sub.js
两个文件,我们来瞅一瞅生成的 index.html
文件
<body>
<div id="root"></div>
<script src="main.js"></script>
<script src="sub.js"></script>
</body>
在 body
的闭合处正确引入了我们想生成的两个 js
脚本,但是这里我们如果希望引入的脚本地址是一个线上 cdn
的地址如 https://www.baidu.com/main.js
这种,又应该怎么办呢?
module.exports = {
entry: {
main: './src/index.js',
sub: './src/index.js'
}, // 项目打包的入口文件
output: { // 打包的最终文件放在哪里
publicPath: 'https://www.baidu.com', // 配置打包资源的前缀路径
filename: '[name].js', // 打包生成的文件名,使用占位符匹配打包文件的 key 值
path: path.resolve(__dirname, 'dist') // 打包目录
}
}
我们可以在 output
对象中配置 publicPath
属性,写成我们想要加入的前缀地址即可,此时我们再来瞅瞅 dist
目下下的 index.html
中的内容:
<body>
<div id="root"></div>
<script src="https://www.baidu.com/main.js"></script>
<script src="https://www.baidu.com/sub.js"></script>
</body>
此处延伸学习文档:管理输出 | webpack
SourceMap
在了解它之前,我们先把代码结构清理一下,删除 dist
目录,删除 src
目录中除了 index.html
和 index.js
的所有其它文件,同时我们将 index.js
中随便写入一段错误的代码:
// index.js
consele.log('hello world') // console 打错了,故意留坑
接着我们在 webpack.config.js
进行 sourceMap
的相关配置
module.exports = {
mode: 'development',
devtool: 'none',
entry: {
main: './src/index.js',
}, // 项目打包的入口文件
output: { // 打包的最终文件放在哪里
filename: '[name].js', // 打包生成的文件名,使用占位符匹配打包文件的 key 值
path: path.resolve(__dirname, 'dist') // 打包目录
}
}
明明说的是配置 sourceMap
, 为啥只看到了一个 devtool
的配置呢?官方文档中 devtool
此选项控制是否生成以及如何生成 source map
,那么我们配置了 development: 'none'
有什么用呢?此时我们打开打包出来的 index.html
页面,发现我们故意留坑的报错在 main.js
的第 96
行,虽然说确实提示出来了,但是开发中我们可能更希望报错提示出现在打包前的脚本文件中,并精确告诉我们是哪一行,此时我们就可以修改 devtool
的基础配置:
module.exports = {
mode: 'development',
devtool: 'source-map',
entry: {
main: './src/index.js',
}, // 项目打包的入口文件
output: { // 打包的最终文件放在哪里
filename: '[name].js', // 打包生成的文件名,使用占位符匹配打包文件的 key 值
path: path.resolve(__dirname, 'dist') // 打包目录
}
}
将 devtool
的值设置为 source-map
之后我们发现报错信息确实出现在了 index.js
的第一行,也就是直接提示源代码中的错误地址,同时 dist
目录下也新增了一个 main.js.map
的文件,此时我们初步总结一下 sourceMap
的作用:
sourceMap
它是一个映射关系,它知道dist
目录下main.js
文件96
行实际上对应的是src
目录下index.js
文件中的第一行。同时所有对应的映射关系都会生成在一个.map
的脚本文件中。
官方文档中给出 devtool
的值多大十几个,不同的值打包速度不同,这里我们引用了官方给出的相关表格信息:
因为表格中的可选项太多了,我们拿几个有代表性的来说一说:
none
:不建立映射关系,报错主要体现在打包后的代码位置source-map
:建立映射关系,同时生成一个.map
的映射文件,报错主要体现在源代码中,打包速度慢inline-source-map
:建立映射关系,不生成.map
映射文件,而是直接以base64
链接的形式被引入到打包后的man.js
中,报错主要体现在源代码中,会告诉你在多少行多少列,打包速度慢inline-cheap-source-map
:只跟你写的业务代码建立映射关系,和inline-source-map
差不多,但是只会告诉你报错在源代码中的第几行,而不会告诉你对应的列数,打包速度较快。inline-cheap-module-source-map
:不仅跟你写的业务代码建立映射关系,同时也会对你引入的loader
和plugins
等进行校验映射,打包速度较慢。eval
:打包速度快,但是遇到复杂逻辑代码,报错提示不明显。
更多配置关系可能更复杂,我们大概了解,然后在 development
开发环境下尽量使用:
devtool: 'cheap-module-eval-source-map',
而在 production
线上环境中尽量使用:
devtool: 'cheap-module-source-map',
WebpackDevServer
前面我们每一次更改了本地代码,都需要使用 npm run build
打包之后才能看到效果,说实话体验感非常糟糕,如果我们希望每次更改代码之后不用手动打包就能自动帮我们打包好,而且能够自动将浏览器打开等等,那我们该怎么办呢?
写代码之前吐槽一下,照着官网文档弄得,总是报错,各种版本问题,我太难了!!!查了半天最后发现要想照着官方文档的栗子实现代码 webpack-cli
的版本不要超过 4.0
,最好只好卸载了 webpack-cli
重新安装,请小伙伴本牢记此坑~~~
如果你的 webpack-cli
版本超过 4.0.0
,第一步请卸载
npm uninstall webpack-cli
安装 webpack-cli@3.3.3
版本
npm install webpack-cli@3.3.2 -D
修改 package.json
文件中的 scripts
"scripts": {
"watch": "webpack --watch", // 观察者模式
"start": "webpack-dev-server", // 打包运行 npm run start 即可
"build": "webpack"
},
接着在 webpack.config.js
中配置开启的 web
服务器
module.exports = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
entry: {
main: './src/index.js',
},
devServer: { // 相关配置
contentBase: './dist', // 服务器启动在那个文件夹下
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
}
}
更多 devServer
的配置移步官方文档:开发中 Server(devServer)
模块热替换
感觉这是一个好高大上的词,有没有小伙伴好奇热模块是啥意思?官方给出的英文单词 Hot Module Replacement
简称 HMR
,高大上的气息铺面而来,咱们先不纠结具体什么意思,先来看个栗子:
- css 模块热替换
现在 index.js
随便编写一段代码
import './index.styl'
var btn = document.createElement('button')
btn.innerHTML = '新增'
btn.onclick = function () {
var div = document.createElement('div')
div.innerHTML = 'item'
document.body.appendChild(div)
}
document.body.appendChild(btn)
然后新建一个 index.styl
文件,将所有添加的偶数的 div
块给个背景颜色
div:nth-child(odd)
background: orange
此时我们运行 npm run start
之后其实就会有一个问题,就是如果我们每次修改 index.styl
的颜色值,页面就会刷新一次,而我们前面所有添加的按钮都要重新在添加一次。如果我们希望保留修改前的状态,css
在浏览器不刷新的情况下直接应用到已有的按钮上,这时候我们就需要用到这个高大上的名词了:Hot Module Replacement
。
修改 webpack.config.js
文件,进行相关配置:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require("clean-webpack-plugin")
const webpack = require('webpack')
module.exports = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
entry: {
main: './src/index.js',
},
devServer: { // 相关配置
contentBase: './dist', // 服务器启动在那个文件夹下
hot: true, // 开启 hot module replacement
hotOnly: true // 即便上面的 hmr 未生效也阻止浏览器的自动刷新
},
plugins: [new HtmlWebpackPlugin({
template: 'src/index.html'
}), new CleanWebpackPlugin(), new webpack.HotModuleReplacementPlugin()],
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
}
}
上述代码中,我们新增了 devServer
配置中的 hot
属性和 hotOnly
属性,同时对 plugins
新增了 new webpack.HotModuleReplacementPlugin()
。然后我们运行 npm run start
即可。此时即使我们已经在页面新增了 100 个按钮后再去修改 index.styl
,页面也不会自动刷新而是直接在当前状态下响应 css
的变化,有没有觉得很神奇!!!
- js 模块热替换
我们针对 js 文件同样也来尝试使用一下,我们在 src
目录中新建一个 counter.js
和一个 number.js
:
// counter.js
function counter() {
const div = document.createElement('div')
div.setAttribute('id', 'counter')
div.innerHTML = 1
div.onclick = function () {
div.innerHTML = parseInt(div.innerHTML, 10) + 1
}
document.body.appendChild(div)
}
export default counter
// number.js
function number() {
const div = document.createElement('div')
div.setAttribute('id', 'number')
div.innerHTML = 3000
document.body.appendChild(div)
}
export default number
然后在 index.js
中引入这两个脚本文件:
import counter from './counter'
import number from './number'
counter()
number()
运行代码后首先将 counter.js
中的数值增加到 10
,然后修改 number.js
中 div.innerHTML
的值为 2000
,奇怪的事情发生了,我们发现页面并没有响应变化!!!那么我们该如何监听这种变化呢,此处查阅官方文档可以看到官方有下面这段代码,我们修改一下引入进来:
import counter from './counter'
import number from './number'
counter()
number()
if (module.hot) { // 如果当前代码开启了 hmr 功能
// 监听 number.js 文件是否发生变化,如果发生变化执行后面的回调函数
module.hot.accept('./number', () => {
number()
})
}
加了如上代码之后确实能够直接监听到 number.js
的响应,但是这种响应并不完美,它会在页面的结尾处再次输出 number
的结果,也就是每修改一次都会叠加一次输出结果。这不是我们想要的功能,我们只好再来修改一下:
import counter from './counter'
import number from './number'
counter()
number()
if (module.hot) { // 如果当前代码开启了 hmr 功能
module.hot.accept('./number', () => {
// 回调每次执行前删除原先的节点元素
document.body.removeChild(document.getElementById('number'))
number()
})
}
好吧,感觉虽然完成了功能,但是有没有觉得 js
实现 HMR
感觉好繁琐的样子,为什么他不能像 css
一样自己完成监听和响应文件的变化呢?为什么我们平时写 vue
都是自动监听文件的响应和变化,而不需要手动实现这种繁琐的过程呢?
其实所有 HMR
功能的实现都是依据这个原理来做的,但是 vue-loader
和 css-loader
已经帮我们将这些功能封装起来了,所以我们可以直接使用,但是它们底层的实现原理基本也离不开这种模式。
此处延伸扩展学习文档:模块热替换 | API、模块热替换 | 指南、模块热替换 | 概念
Babel
Babel
是什么,相信大家都有基本的概念,我看先看官方给出的定义:Babel
是一个工具链,主要用于将 ECMAScript 2015+
版本的代码转换为向后兼容的 JavaScript
语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
我们清空前面写的代码,在 index.js
中写入一段 ES6
语法的代码:
const arr = [
new Promise(() => { }),
new Promise(() => { })
]
arr.map(item => {
console.log(item)
})
此时打包我们就不使用 npm run start
,因为通过 devServer
为了保证更快的打包效率会将代码直接打包到内存中,我们看不到实际打包过后的代码,所以我们还是使用老方法 npm run build
,通过生成的 dist
文件夹中 main.js
文件我们可以清楚的看到 ES6
语法有没有被转译。
上述代码如果在谷歌浏览器中直接运行,一般是不会出现啥问题,因为谷歌对于开发者来说是兼容性最好的浏览器,但是如果直接运行在 IE 11
以下的浏览器中,就会不认识 Promise
、map
、(item => {})
这些语法,此时我们就需要借助到 Babel
。
因为我们使用 Babel
的场景是 webpack
,所以大家可以参考这里的官方文档:Webpack 中使用 Babel 步骤 。
首先是安装:
npm install --save-dev babel-loader @babel/core
看到 loader
我们就应该知道,babel-loader
肯定是帮 webpack
用来打包的一个工具。babel/core
是什么呢?它是 babel
的一个核心库,它能够让 babel
去识别 js
代码的内容,然后把 js
代码转化成抽象语法树,然后再把抽象语法树转化成一些新的语法。
然后在 module => rules
中进行相关配置:
module: {
rules: [
{ test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }
]
}
如果检测到你的文件是 js
文件,就使用 babel-loader
去分析一下 js
文件中的语法到底是什么情况。exclude
表示 node_modules
中的 js
文件不需要使用 babel-loader
检测。
接下来文档提示我们继续安装 @babel/preset-env
npm install @babel/preset-env --save-dev
这又是个什么东西呢?当我们使用 babel-loader
处理文件的时候,实际上 babel-loader
只是 babel
和 webpack
做通信的一个桥梁,但是实际上 babel-loader
并不会帮你把 js
中的 ES6
语法翻译成 ES5
语法,我们还需要借助一些其它的模块,而 babel/preset-env
就是这样一个模块,这个文件包含了所有 ES6
转换 ES5
的一些规则。
在 babel-loader
下进行相关配置
rules: [{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"]
}
}]
然后我们再次打包代码,这是我们打开 main.js
就会发现所有的 const
都变成了 var
,箭头函数也都转化成了 function(){}
。但是 Promise
和 map
并没有进行转换,它们俩如果想在低版本浏览器中运行也是不行的,所以这个时候我们不仅要使用 babel/preset-env
做语法转换,还需要把它缺失的变量或者函数补充到低版本浏览器里。怎么补充呢?我们需要借助到 babel-polyfill 帮我们做这些变量或者函数在低版本浏览器中的补充。
我们首先安装 babel-polyfill
npm install --save @babel/polyfill
接下来我们只需要在所有代码引入之前引入 @babel/polyfill
即可,此时我们可以在 src
目录下的 index.js
最顶部引入如下代码:
import "@babel/polyfill"
此时我们可以再次进行打包,虽然完美解决了问题,但是又曝光出了一些新的问题。前面我们打包 main.js
的大小只有 30kb
,但是引入 @babel/polyfill
之后 main.js
的大小接近 1000kb
,我们明明只写了几行代码,确要引入接近 1M
的脚本文件。
这里我们可以做一些优化补充,如果我们这里只需要它帮我们实现 map
和 promise
的语法补充,不需要补充其他差异化语法,那我们又可以做如下优化:
rules: [{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: [['@babel/preset-env', {
useBuiltIns: 'usage'
}]]
}
}]
useBuiltIns: 'usage'
这个配置代表什么意思了?它是告诉 @babel/polyfill
去往低版本浏览器增加特性的时候不是把所有特性内容都加进来,而是根据你业务代码里面需要进行补充声明的语法特性来进行添加。此时我们再次打包发现 main.js
的大小只有 200kb
左右,最终完美实现所有效果。
当然上述代码如果在我们写入库或者 UI
组件的时候引用,使用 @babel/polyfill
会污染全局环境,所以此时我们不能使用上面的方法,此时我们应该怎么办呢?查看官方文档 transform-runtime 。
首先安装:
npm install --save-dev @babel/plugin-transform-runtime
接着安装:
npm install --save @babel/runtime
此时我们可以根据官方文档来做一些 options
的配置,但是如果在 webpack.config.js
中写相关配置项,会将这个文件下的代码拉扯的超级长,所以其实我们可以新建一个 .babelrc
文件用来管理 babel-loader
中配置的 options
。
首先我们删除 webpack.config.js
中关于 module => rules
中 babel-loader
配置的 options
rules: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
// options: { 整体删除
// presets: [['@babel/preset-env', {
// useBuiltIns: 'usage'
// }]]
// }
},
然后根目录下新建一个 .babelrc
文件,配置如下代码
{
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"absoluteRuntime": false,
"corejs": 2,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}
当然此时需要我们额外安装 runtime-corejs2
npm install --save @babel/runtime-corejs2
此时在运行 npm run build
其实跟之前没有啥区别,只是我们在构建 UI
组件库,或者造轮子的时候,可以使用 transform-runtime
这种方法。同时如果我们不想在 babel-loader
下的 options
对象中填写过多的配置代码,可以直接在项目根目录下新建 .babelrc
文件用来存放 options
里面的内容。
不知不觉又写的有点多了,结尾还是那句老话:此整理仅供记录学习,如果文中有不对的地方或者理解有误的地方欢迎大家提出并指正。每一天都要相对前一天进步一点,加油!!!