上一篇常见的webpack的基本配置写了一些webpack的基本知识,包括webpack的入口、出口以及loader和plugin等知识。但是觉得写的太啰嗦,现在补充其余知识的时候尽量做到简单易懂。
目录
1、dev-tool(source-map)
我们在运行webpack
打包运行项目的时候,实际执行的是打包后的代码,这样不利于找到错误代码在源代码的位置。webpack
提供了dev-tool
帮助开发者快速定位到错误发生的位置。具体方法即是通过生成map文件
将打包后的代码映射到源代码,这样在浏览器控制台的报错信息中可以看到错误代码在源代码的位置。
启动dev-tool
的方式很简单,在配置文件中添加"devtool"
字段即可。devtool
的值可以有多个,在开发环境中通常使用eval-source-map
,生产环境可以使用source-map
。它们的差别在于eval-source-map
不会单独生成map文件
,而是将数据打包在js
文件中,用eval
包裹,这样打包速度快但是会造成js
文件太大。而source-map
会生成单独的map
文件。
module.exports = {
devtool: "eval-source-map"
}
2、tree shaking(树摇)
对使用了export导出但是未被其他文件import的代码,通常是无效代码(不被项目依赖),不应该被打包进最终的文件。webpack通过树摇来去掉这些无效代码。树摇的触发条件有两个:
- 使用生产模式(mode为production)
- 使用ES Module(使用import和export导入导出)
由于我们项目中通常都会满足这两个条件,因此打包的时候默认是使用了树摇。但是有些代码并不需要引入,而是被全局使用(例如polyfill和css文件),这些代码不应该被删除。此时应该在package.json
中添加:
"sideEffects": [
"./src/polyfill.js",
"*.css"
]
这样polyfill.js
和css
文件不会被树摇去掉。
3、缓存
通过给babel
开启缓存,可以有效的提高打包的速度。只需通过给babel-loader
的配置添加cacheDirectory: true
即可。
module.exports = {
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
options:{
cacheDirectory: true // 开启babel缓存
}
}
]
}
}
4、代码分割(code split)
默认情况下,webpack
会将所有的代码打包进一个js文件
中,这样会造成文件体积过大,不利于加载。通过代码分割的方式可以把部分文件单独打包成一个js文件
。使用方式如下:
module.exports = {
optimization:{
splitChunks: 'all'
}
}
通过上面的代码,就可以将node_modules
里面的代码(例如vue)单独打包成一个文件。如果需要将指定文件打包成一个文件,需要使用import
函数的方式,如下:
// utils.js
export function add(a, b) {
return a + b
}
// main.js
// 引入utils.js文件并单独打包
import('utils.js').then(({
add }) => {
add(1, 2)
}).catch(e => {
})
从上图看出,vue.js
被打包到了一个单独的js文件
,大小为64.4kb
,而utils.js
文件被打包到2.d9f86948.js
中。由于默认情况下打包后的文件名字由output
里面的filename
控制,因此如果要改变打包后的名字,可以在output
中的chunkFilename
修改,并通过webpackChunkName
指定打包后的chunk
的名字(如果不指定则默认使用chunk序号表示)。
module.exports = {
output: {
path: resolve(__dirname, 'dist'),
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].[chunk].js'
}
}
// main.js
// 引入utils.js文件并单独打包 使用 注释/**/ 改变chunk的名字
import(/* webpackChunkName: 'utils' */'utils.js').then(({
add }) => {
add(1, 2)
}).catch(e => {
})
5、添加外部链接(externals)
有时我们希望某些代码来自外部链接,例如vue
。此时应该使用externals
指定来自外部链接的模块。
module.exports: {
externals: {
vue: 'Vue' // 注意:此时键名vue表示需要引入的模块的名字,Vue表示将来会导出一个变量Vue,因此要和库导出的变量名相同,否则会报错
}
}
此时再在index.html
中使用script
引入cdn链接
即可。使用vue
的时候,依旧使用import Vue from 'vue'
。此时vue
不会被打包进去。
从上图即可看出,最终的打包文件使用module.exports = Vue
导出了Vue
。因此我们可按照之前的用法正常使用Vue
。同时在全局环境下也存在一个变量Vue
。
6、Dll
Dll
和externals
都能指定哪些模块不需要打包,不同的是,externals
是通过外部链接的方式引入需要的模块,而Dll
则是提前打包好需要的模块,在最终打包的时候无需重新打包这些模块而是直接引入即可。配置如下:
// 首先配置打包需要的配置文件 webpack.dll.js
const {
resolve } = require('path')
const webpack = require('webpack')
const {
CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
entry: {
vue: ['vue'] // 注意这里需要用数组 数组里面的值和库的名字相同
},
output: {
path: resolve(__dirname, 'dll'),
filename: '[name].js', // 此时 name 为 vue,由entry的属性名指定
library: '[name]'
},
plugins: [
new CleanWebpackPlugin(), // 注意要先清空原先的文件
new webpack.DllPlugin({
name: '[name]', // 指定manifest中name的值,需要和library的值保持一致
path: resolve(__dirname, 'manifest.[name].json'), // 指定生成的manifest文件的路径
})
]
}
此时会生成以下文件:
然后在配置文件中添加如下代码:
const {
resolve } = require('path')
const webpack = require('webpack')
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin') // 用于给最终打包后的文件夹(比如dist文件夹)中添加文件,并将文件在index.html中引入,注意这个插件基于html-webpack-plugin,因此需要在其之后使用
module.exports = {
plugins: [
new webpack.DllReference({
manifest: resolve(__dirname, 'manifest.vue.json') // 指定manifest文件的路径,webpack通过这个文件即可知道哪些是不需要打包的模块
}),
new AddAssetHtmlWebpackPlugin({
filepath: resolve(__dirname, 'dll/vue.js')
})
]
}
注意,在生成manifest文件
的打包配置文件(webpack.dll.js
)中,要保持library
和DllPlugin
配置中的name
一致,因为打包后生成的vue.js
文件中会创建一个全局变量,这个变量名由library
指定。而在最终的打包配置中使用了manifest
中的name
,通过它确定哪些包是不需要打包而是直接引入,并在最终生成的main.js
文件中通过module.export
导出,导出的变量名即是manifest
中的name
。因此如果两者不一致,则在导出时会找不到变量。同时,在使用import
引入库的时候,名字也需要和manifest
中的name
一致。
生成的vue文件
生成的manifest文件
生成的main.js文件
7、懒加载和预加载
有时候我们不需要立即加载一个文件,而是在使用到的时候再加载,这时可以使用懒加载。用法很简单,使用import
函数即可。
// 懒加载的文件会被代码分割
const btn = document.createElement('button')
btn.innerHTML = 'click'
btn.onclick = function () {
// 在点击按钮时才加载 utils.js 文件
import('./utils.js').then(({
add }) => {
add(1, 2)
})
}
document.body.appendChild(btn)
懒加载存在一个问题:当需要加载的文件体积太大时,由于不能及时加载而会出现卡顿现象,因此可以使用预加载,通过推测可能需要加载的文件,当其他资源加载完以后,利用浏览器的空闲时间加载并缓存。这样在使用的时候就可以直接从缓存读取了。
const btn = document.createElement('button')
btn.innerHTML = 'click'
btn.onclick = function () {
// 在点击按钮时才加载 utils.js 文件
import(/* webpackPrefetch: true */'./utils.js').then(({
add }) => {
add(1, 2)
})
}
document.body.appendChild(btn)
从上图看出utils.js
被提前加载了,点击按钮后直接从缓存读取:
预加载存在兼容问题,需要慎用。
8、模块热替换
在开发环境下,修改文件会自动重新打包并刷新浏览器,为了防止每次都打包所有文件,可以使用模块热替换功能。使用方法很简单,一种方法是在package.json
文件中的脚本语言中添加--hot
,第二种是在devServer
中添加hot
字段。
"dev": "webpack-dev-server --hot"
module.exports = {
devServer: {
hot: true
}
}
对于css
文件来说,由于style-loader
和mini-css-extract-plugin
都提供了模块热替换功能,因此不需额外配置了。当修改css
文件时,只有被修改的css
文件会被打包更新。如果需要对js文件
实现模块热替换,需要如下配置:
// main.js
if (module.hot) {
// 对 utils.js 使用模块热替换
module.hot.accept('./utils.js', function () {
console.log('触发了模块热替换')
})
}
由于使用了模块热替换之后,html
文件的修改无法再触发更新,此时需要将entry
改成数组,将index.html
也作为入口文件(注意此时需要使用html-loader解析html文件)。
entry:['./main.js', './public/index.html']
注意:html文件无法进行模块热替换
9、PWA
PWA(渐进式网络开发应用程序),用于离线缓存网页数据。简单来说就是当断网时,可以从缓存中读取部分数据而不会白屏。
使用方式如下:
首先安装workbox-webpack-plugin
:
npm i -D workbox-webpack-plugin
接着在配置文件中添加如下配置:
const WorkboxWebpackPlugin = require('workbox-webpack-plugin')
module.exports = {
plugins: [
new WorkboxWebpackPlugin.GenerateSW({
clientsClaim: true,
skipWaiting: true
})
]
}
接着在入口文件中添加如下配置:
if ('serviceWorker' in navigator) {
// 处理兼容性问题
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/service-worker.js') // 注册serviceWorker
.then(() => {
console.log('sw注册成功了~');
})
.catch(() => {
console.log('sw注册失败了~');
});
});
}
打包后在dist
文件夹会生成service-worker.js
和workbox-3b5792f5.js
。由于service-worker
需要运行在服务器环境,这里可以全局安装serve
配置一个简易的服务器环境运行打包后文件:
npm i -g serve
运行命令:
serve dist
在浏览器打开服务器网址即可发现网页数据被service-worker
缓存了:
断网后刷新页面发现网页仍然可以正常显示。
10、配置环境变量
const webpack = require('webpack')
module.exports = {
plugins: [
// 配置后在打包的时候会将对应的字段直接替换成设置的值,由于是字符串替换,因此需要添加引号
webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development'),
'BASE_URL': '"http://localhost:3000"'
})
]
}
11、配置vue环境
配置简易的vue
环境非常简单,只需安装vue
、vue-loader
、vue-style-loader
、vue-template-compiler
即可。添加如下配置:
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
new VueLoaderPlugin()
]
}
这样即可使用单文件组件。
12、一些详细配置
补充介绍部分配置。
12.1 entry
entry
的值有三种形式,字符串
、数组
、对象
。
// 字符串
entry: './main.js' // 入口文件路径,chunk的名字是默认值main
// 数组
entry: ['./main.js', './public/index.html'] // 两个文件都被打包,且只生成一个js文件,chunk的名字为main
// 对象
entry: {
index: './index.js',
detail: './detail.js'
}
// 打包成两个js文件,chunk的名字分别是index和detail
12.2 output
output
常见配置如下:
output: {
path: resolve(__dirname, 'dist'), // 打包后的文件夹路径
filename: '[name].js', // 打包生成的文件名
publicPath: '/', // 指定公共路径,例如,在入口文件中引入文件的路径是'utils.js',则打包后会添加上'/',变成'/utils.js',在服务器环境下'/'指根路径,因此生产环境打包通常设置publicPath为 '/'
chunkFilename: '[name].[hash:8].js', // 指定非入口chunk的名称
library: '[name]', // 指定库暴露出去的变量名
libraryTarget: 'commonjs' // 指定打包运行环境,commonjs表明打包后文件将可以运行在node环境,其余环境还有amd、umd等
}
12.3 resolve
resolve: {
alias: {
// 配置路径别名 例如 vue 项目中常用的 @ 即是表示 src 文件夹的别名
'@': resolve(__dirname, 'src')
},
// 配置可省略的扩展名(例如vue项目中引入文件可以不写js和vue后缀名)
extensions: ['js', 'vue'],
// 指定webpack解析模块时查找的目录,默认是node_modules,此时可以指定一个绝对路径避免多次查找,为了避免查找错误,后面可再加上node_modules
modules: [resolve(__dirname, '../node_modules'), 'node_modules']
}
12.4 devServer
devServer: {
hot: true, // 启动模块热替换
contentBase: resolve(__dirname, 'public'), // 指定从浏览器输入路径获取静态资源时查找的文件目录,例如在根路径后输入'favicon.ico',则是从'public'这个文件目录查找图标,contentBase可以是个数组,指定多个查找目录,通常只在需要访问静态资源时指定contentBase
watchContentBase: true, // 监听contentBase指定的文件夹下文件的变化,当发生变化时会刷新页面
watchOptions: {
ignored: /node_modules/ // 忽略文件
},
compress: true, // 开启gzip压缩
host: 'localhost', // 域名
port: '8080', // 端口
open: true, // 自动打开浏览器
clientLogLevel: 'none', // 关闭控制台打印日志
quiet: true, // 除了一些基本启动信息外,不打印其余内容
overlay: false, // 错误信息不打印在浏览器全屏
proxy: {
// 配置代理解决开发环境下跨域问题
'/api': {
target: 'http://localhost:3000', // 当遇到 /api 开头的请求时会拦截请求,将协议、域名、端口转为target指定的路径,例如http://localhost:3000/api/login
pathRewrite: {
// 路径重写
'/api': '' // 去掉 /api
}
}
}
}
12.5 optimization
optimization: {
splitChunks: {
chunks: 'all', // 指定所有类型的 chunk,也可指定一个函数过滤不需要的chunk
miniSize: 30 * 1024, // 指定分割chunk的最小体积,这里指定大于30kb才生成chunk
maxSize: 0, // 指定分割chunk的最大体积,当一个文件可分割且大于maxSize和miniSize时,会以maxSize为标准分成多份,这里指定为 0 表示大于miniSize的任意体积的都打包成一个chunk
maxAsyncRequests: 5, // 按需加载时并行加载的文件的最大数量为5
maxInitialRequests: 3, // 入口js文件最大并行请求数量
automaticNameDelimiter: '~', // 名称连接符
name: true, // 可以使用命名规则
cacheGroups: {
// 分割chunk的组
vendors: {
// node_modules中的文件会被打包到vendors组的chunk中,--> vendors~xxx.js
// 满足上面的公共规则,大小超过30kb、至少被引用一次
test: /[\\/]node_modules[\\/]/,
// 优先级
priority: -10
},
default: {
// 要提取的chunk最少被引用2次
minChunks: 2,
prority: -20,
// 如果当前要打包的模块和之前已经被提取的模块是同一个,就会复用,而不是重新打包
reuseExistingChunk: true
}
}
}
}
文章评论