在开发中为了增强webpack自动化的能力,我们需要安装一些插件来辅助开发。
Loader 是为了解决资源加载的问题,而Plugin是为了解决其他自动化的问题。
在webpack.config.js 文件中添加 plugins属性 该属性是一个数组,在这个数组填入插件的实例即可。
plugins: [
插件1,
插件2,
…
]
clean-webpack-plugin 通过安装使用该插件 来实现打包时自动清除上一次打包成果的功能
const {
CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
plugins:[
new CleanWebpackPlugin ()
]
}
如果html文件是由我们自己手动引入打包后的资源,那么会存在硬编码问题,即,如果修改打包配置修改文件名或者文件路径,就需要我们手动再去修改html中的文件引用
我们可以借助 html-webpack-plugin 来实现自动生成html 并自动完成打包后文件的注入,这样再运行打包之后,dist目录中就会出现html文件 并且自动引入对应的js文件了
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugins:[
new HtmlWebpackPlugin()
]
}
我们可以自定义输出的html文件内容,可以在new HtmlWebpackPlugin中添加配置对象,如果需要自定义的内容比较复杂,我们还可以使用template设置模板
plugins:[
new HtmlWebpackPlugin({
title:'Hello World!',
meta: {
viewport: 'width=device-width'
},
template:'./src/index'
})
]
如果要输出多个页面就继续添加 多个实例即可
plugins:[
new HtmlWebpackPlugin({
title:'Hello World!',
meta: {
viewport: 'width=device-width'
},
filename:'home.html',
template:'./src/index'
}),
new HtmlWebpackPlugin({
filename:'about.html'
}),
]
项目中的静态资源目录,比如 public 目录 想要在打包后也放在打包后的目录中,可以借助于这个 copy-webpack-plugin 插件来完成
const CopyWebpackPlugin = require('copy-webpack-plugin')
plugins:[
new CopyWebpackPlugin({
patterns:[ {
from:'目标',to:'生成到哪去' } ]
})
]
webpack的插件工作机制就是我们在软件开发中常见的钩子机制来实现。
webpack要插件必须是一个函数,或者包含apply方法的对象。
我们来实现一个去掉 /******/ 这种注释的插件
class MyPlugin {
apply (compiler) {
console.log('MyPlugin 启动')
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation => 可以理解为此次打包的上下文
for (const name in compilation.assets) {
if (name.endsWith('.js')) {
const contents = compilation.assets[name].source()
const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
compilation.assets[name] = {
source: () => withoutComments,
size: () => withoutComments.length
}
}
}
})
}
}
总结一下,我们使用webpack进行项目打包的流程:
四个步骤,依次进行,但是这种手动的方式过于原始,也会让开发的体验很差,我们就需要想办法提高开发的体验,使一些工作能够自动完成,让我们专注于代码编写上。
希望能够有以下几个方向的优化,来提高开发体验:
使用watch 工作模式,监听文件变化,自动重新打包。
执行 webpack --watch
这样webpack就会开始监听文件变化了
该工具提供一个 HTTP Server,并且这个工具集成了 「自动编译」和「自动刷新页面」 等功能
运行方式: webpack-dev-server
可以在 npm script 中添加该执行 并传入cli 参数
"scripts": {
"dev": "webpack-dev-server --open --host 127.0.0.1 --port 8888"
}
然后执行 npm run dev 即可
需要执行 webpack serve 指令来启动
在package.json应做如下配置
"dev": "webpack serve"
再执行npm run start就正常启动。
我们在开发的时候 CopyWebpackPlugin 一般只在最后添加 所以为了防止dev server访问不到静态资源
为 dev-server 指定要访问的静态资源目录,在 webpack.config.js中添加
devServer: {
contentBase: './public'
}
前端在发起接口请求时,由于浏览器的同源问题,会出现跨域问题。我们在开发的过程中可以利用devServer配置代理api 来绕开同源策略。当项目上线之后 交给运维人员去配置反向代理
devServer: {
contentBase: './public',
proxy: {
// 匹配以 /api 开头的请求
'/api': {
// http://localhost:8080/api/users -> https://api.github.com/api/users
target: 'https://api.github.com',
// 重写地址,不希望其中出现/api
// http://localhost:8080/api/users -> https://api.github.com/users
pathRewrite: {
'^/api': ''
},
// 不能使用 localhost:8080 作为请求 GitHub 的主机名
// changeOrigin默认是false:请求头中host仍然是浏览器发送过来的host
// 如果设置成true:发送请求头中host会设置成target
changeOrigin: true
}
}
},
因为运行代码都是经过打包编译之后的,跟编写的不同,不方便我们调试。Source Map就解决了这个问题 ,它利用map文件 映射出原代码,方便开发人员进行调试。
webpack配置SourceMap
devtool:"source-map"
注意在webpack 5 之下 该模式更名为 eval-cheap-module-source-map,
也就是要这样设置
devtool:"eval-cheap-module-source-map"
devServer可以在我们修改过代码后,自动刷新页面,但是这样也存在一个问题。如果页面中有一些表单元素,输入内容到一般,修改代码自动刷新就会出现输入的内容丢失。就又需要再次输入比较麻烦。
我们需要解决这个问题,这里可以使用到的就是 Hot Module Replacement (模块热更新)
HMR 指的是在程序运行过程中可以实时的替换掉某个模块,并且程序的运行状态不受影响.
热更新集成在了 webpack-dev-server 中 不需要额外安装其他模块。
可以通过cli 参数 运行 webpack serve --hot
添加 --hot来开启
也可以通过配置文件开启, 这里注意 修改的样式文件要是js 文件中引入的样式文件 这样才会触发热更新
注意 : 针对于 html js 图片文件 需要手动去设置hmr的逻辑,社区还提供许多其他 loader 和示例,可以使 HMR 与各种框架和库平滑地进行交互。
"scripts": {
"serve":"webpack serve --hot --open"
},
webpack的配置文件除了支持导出一个对象,还支持导出一个函数,函数接收两个形参: env 和 argv, 在这个函数中 返回我们需要的配置对象。
env 是我们通过cli(终端指令)传递的环境名参数
argv 是指我们运行cli过程中传递的所有参数
默认约定 生产环境的env的值是 “production” 比如 webpack --env production
module.exports = (env, argv) => {
const config = "具体配置对象"
if (env === 'production') {
config.mode = 'production'
config.devtool = false
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
}
return config
}
webpack-merge
来合并公共配置 const webpack = require('webpack')
const merge = require('webpack-merge')
const common = require('./webpack.common')
module.exports = merge(common, {
mode: 'development',
devtool: 'cheap-eval-module-source-map',
devServer: {
hot: true,
contentBase: 'public'
}
})
"scripts": {
"build": "webpack --config webpack.prod.js",
"serve": "webpack serve --hot"
},
这是webpack内置的一个插件,它可以为我们的代码注入全局成员
比如 我要注入api的公共域名
const webpack = require('webpack')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js'
},
plugins: [
new webpack.DefinePlugin({
// 值要求的是一个代码片段
// API_BASE_URL 就可以在全局使用了
API_BASE_URL: JSON.stringify('https://api.example.com')
})
]
}
顾名思义 “摇树” 去掉枯叶,在webpack里它的作用是「摇掉」代码中未引用的部分,未引用代码( dead-code ),生产模式中,webpack会自动检测未引用的代码 然后移除掉它们。
Tree Sharking 不是指某一个配置选项,它是一组功能搭配使用过后的优化效果,在生产模式中webpack会自动启动它
如果需要在开发模式中使用,我们可以通过两个配置来实现
optimization: {
usedExports: true,
minimize: true
}
tree sharking 要想生效,关键点在于 由webpack 打包的代码必须使用ESM,而我们有时候使用的babel版本比较低的话 就会把ESM 转化为 commonJS 规范 这样就会导致 tree sharking 失效,但是在最新版的babel中已经不存在这个问题了
concatenateModules 合并模块,原本webpack打包会把一个模块生产一个函数,使用了concatenateModules 可以把所有的模块都合并到一个函数中,进一步压缩体积。
开启了sideEffects配置后,webpack在打包时就会先检查当前代码所属的package.json中有没有sideEffects的标识,以此来判断这个模块是不是又副作用。如果这个模块没有副作用,这些没被用到的模块就不会被打包。(这个特性在production模式下会自动开启)
副作用: 模块执行时除了导出成员之外所作的事情。
sideEffects 一般用于 npm 包标记是否有副作用
比如说 一个模块中的代码 除了导出成员之外,还为Object 扩展了原型方法那么这就是副作用,我们需要标记这些有副作用的文件模块,不然它们是不会被webpack打包进来的。
使用sideEffects的前提就是确定你的代码真的没有副作用,否则的话,在webpack打包时,就会误删掉那些有副作用的代码。
webpack 可以把模块代码打包成一个文件,这样也存在一些弊端。如果应用非常复杂,模块很多,那么最终生成的文件就会特别的大。然而,在实际情况中,并不是每个模块在启动时都是必要的。
最理想的情况是,把代码分离到多个文件中,分包,根据需要按需加载。
当然这样有人会有疑问,就是说一开始模块就是分离的,既然要分离干脆别打包啊?
理由很简单,单一文件太大这样肯定不行,而如果文件数量太多太碎,也是一样不行的。
我们可以按照不同的业务功能来对模块进行分包,实现的方式有以下两种:
一般适用于传统的多页应用程序,最常见的是一个页面对应一个打包入口,公共部分单独提取。
module.exports = {
mode: 'none',
entry: {
index: './src/index.js',
album: './src/album.js'
},
output: {
filename: '[name].bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
chunks: ['index']
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html',
chunks: ['album']
})
]
}
公共模块提取
optimization: {
splitChunks: {
// 自动提取所有公共模块到单独 bundle
chunks: 'all'
}
},
动态导入的模块会被自动分包。
而实现动态导入模块的方法,其实就是利用ESM中动态导入模块的写法即可。只要使用动态导入的模块webpack就会自动实现打包后生成多个分包文件,并提取公共部分
import(./posts/posts').then(({
default: posts }) => {
})
而默认打包后的文件名字只是在前面添加了序号,我们可以在动态导入里添加一行魔法注释,这样生成的分包文件的名字就会使用注释中的名字了,并且添加了相同注释的动态导入会被打包到一起。
这一点在我们开发vue项目时,配置动态路由的时候尤为明显。
import(/* webpackChunkName: 'components' */'./posts/posts').then(({
default: posts }) => {
})
如果项目中的css文件体积过大,我们可以将css从js中抽离出来 还是以单独文件的形式引入,这就需要使用 MiniCssExtractPlugin这个插件了
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader', // 将样式通过 style 标签注入
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
}
webpack 生产模式下 只能对js文件进行压缩,如果需要对样式文件进行压缩,就需要借助于插件
optimization: {
minimizer: [
new OptimizeCssAssetsWebpackPlugin()
]
},