1. Tree Shaking
1.1 JS Tree Shaking
1.1.1 本地代码Tree Shaking
- 一个简单的打包示例
(1) 打包入口代码
src/index.js
import { add } from './math'
add(1,5)
src/math.js
export const add = (a, b) => {
console.log(a + b)
}
export const minus = (a, b) => {
console.log(a - b)
}
(2) 打包输出
npm run bundle
//...
/*! exports provided: add, minus */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "add", function() { return add; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "minus", function() { return minus; });
const add = (a, b) => {
console.log(a + b);
};
const minus = (a, b) => {
console.log(a - b);
};
//...
(3) 问题分析
src/index.js
仅引入了add
方法,但是却打包了add
方法和minus
方法。
-
Tree Shaking
tree shaking 是一个术语,通常用于描述移除JavaScript
上下文中的未引用代码(dead-code
)。
webpack 2.0
及之后版本支持Tree Shaking
。
webpack 3.X
版本开启Tree Shaking
方式与webpack 4.X
不同。
Tree Shaking
只支持ES Module
模块引入方式。不支持commonjs
模块引入方式。
-
development
模式开启Tree Shaking
(1) 编辑打包配置文件
webpack.dev.config.js
optimization: {
usedExports: true
}
(2) 将文件标记为side-effect-free
(无副作用)
编辑package.json
"sideEffects": ["*.css"]
side-effect-free
数组中标记的文件即使没有通过ES Module
,也会被打包输出。如果没有文件设置为side-effect-free
,则sideEffects
值设置为false
。
(3) 打包输出
/*! exports provided: add, minus */
/*! exports used: add */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return add; });
/* unused harmony export minus */
const add = (a, b) => {
console.log(a + b);
};
const minus = (a, b) => {
console.log(a - b);
};
/***/
-
production
模式开启Tree Shaking
生产模式自动开启Tree Shaking
,无需设置optimization
。
Tree Shaking
开启的关键在于JavaScript
代码压缩。在webpack3.X
版本中,通过UglifyJsPlugin
插件进行JavaScript
代码压缩。在webpack4.X
版本中,mode: production
生产模式默认进行JavaScript
代码压缩。
- 结论
你可以将应用程序想象成一棵树。绿色表示实际用到的source code
(源码) 和library
(库),是树上绿色的树叶。灰色表示未引用代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。
在以
import { add } from './math'
的方式引入模块时,Tree Shaking
能够将'./math'
中未被引入的模块过滤掉。
1.1.2 Lodash Tree Shaking
- 编辑打包入口文件
src/index.js
import { join } from 'lodash';
console.log(_.join(['1','2', '3'], '-'))
- 打包输出
Asset Size Chunks Chunk Names
index.html 199 bytes [emitted]
main.js 70.3 KiB 0 [emitted] app
只用到了lodash
中的join
方法,main.js
包大小为0.3 KiB
。很明显。Tree Shaking
并没有生效。
- 安装依赖
npm i babel-plugin-lodash -D
- 编辑打包配置文件
webapck.dev.config.js
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: [
[ "@babel/preset-env", {"useBuiltIns": "usage", "corejs": 2}]
],
plugins: [
"lodash" //对lodash进行Tree Shaking
]
}
}
- 打包输出
Asset Size Chunks Chunk Names
index.html 199 bytes [emitted]
main.js 1.08 KiB 0 [emitted] app
经过Tree Shaking
后,main.js
包大小为1.08 KiB
。
使用
babel-plugin-lodash
插件后,即使使用import lodash from 'lodash'
方式引入lodash
,Tree Shaking
仍然生效。
1.2 CSS Tree Shaking
- 安装依赖
npm i -D purifycss-webpack purify-css glob-all
- 编辑打包配置文件
webpack.dev.config.js
const PurifyCSS = require('purifycss-webpack');
const glob = require('glob-all');
module.exports = {
//...
plugins: [
new PurifyCSS({
paths: glob.sync([
path.join(__dirname, './src/*.js')
])
})
]
}
- 打包输出
生成的css
文件不包含./src/*.js
中使用不到的样式。
purify-css
和css modules
不可以同时使用。
2. webpack-merge
- 安装依赖
npm i webpack-merge -D
- 打包配置文件
(1)build/webpack.base.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')
module.exports = {
entry: {
app: path.resolve(__dirname, '../src/index.js')
},
output: {
publicPath: '',
filename: '[name].bundle.js',
path: path.resolve(__dirname, '../dist')
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader"
},
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[path][name]__[local]--[hash:base64:5]'
}
},
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: [ require('autoprefixer')]
}
}
]
},
{
test: /\.scss$/,
use: [
"style-loader",
"css-loader",
"sass-loader",
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: [ require('autoprefixer')]
}
}
]
},
{
test: /\.html$/,
use: [
{
loader: "html-loader",
options: {
attrs: [':src', ':data-src']
}
}
]
},
{
test: /\.(eot|ttf|svg|woff)$/,
use: {
loader: "file-loader",
options: {
name: '[name]-[hash:5].[ext]',
outputPath: 'font/'
}
}
},
{
test: /\.(png|svg|jpg|gif)$/,
use: {
loader:'url-loader',
options: {
name: '[name]-[hash:5].[ext]',
outputPath: 'images/',
limit: 4096
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../src/index.html')
}),
new CleanWebpackPlugin()
]
}
(2) build/webpack.dev.config.js
const webpack = require('webpack');
const merge = require('webpack-merge')
const baseConfig = require('./webpack.base.config')
const devConfig = {
mode: "development",
devtool: 'cheap-module-eval-source-map',
optimization: {
usedExports: true
},
devServer: {
open: true, //浏览器自动打开
port: 9000,
contentBase: './dist',
hot: true,
//hotOnly: true
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}
module.exports = merge(baseConfig, devConfig)
(3) build/webpack.prod.config.js
const merge = require('webpack-merge')
const baseConfig = require('./webpack.base.config')
const prodConfig = {
mode: "production",
devtool: 'cheap-module-source-map',
}
module.exports = merge(baseConfig, prodConfig)
webpack-merge
可以对module.rules
进行合并,但无法对单个rule
中的loader
进行合并。
- 创建打包命令
package.json
"scripts": {
"build": "webpack --config ./build/webpack.prod.config.js",
"dev": "webpack --config ./build/webpack.dev.config.js",
"start": "webpack-dev-server --config ./build/webpack.dev.config.js",
}
3. js代码分割(Code Splitting)
3.1 单独文件打包输出的缺点
- 安装
lodash
npm i --save lodash
- 编辑
src/index.js
import _ from 'lodash'
console.log(_.join(['1','2', '3'], '-'))
- 打包分析
Asset Size Chunks Chunk Names
app.bundle.js 1.38 MiB app [emitted] app
index.html 221 bytes [emitted]
Entrypoint app = app.bundle.js
将
lodash
和业务代码打包到一个文件app.bundle.js
。页面加载js
耗时时间久。页面代码更新时,app.bundle.js
全量更新。
3.2 多入口实现分包
- 编辑打包配置文件
entry: {
lodash: path.resolve(__dirname, '../src/lodash.js'),
app: path.resolve(__dirname, '../src/index.js')
}
- 编辑
src/lodash.js
import lodash from 'lodash'
window.lodash = lodash
- 编辑
src/index.js
console.log(window.lodash.join(['1','2', '3'], '-'))
- 打包分析
Asset Size Chunks Chunk Names
app.bundle.js 29.1 KiB app [emitted] app
index.html 284 bytes [emitted]
lodash.bundle.js 1.38 MiB lodash [emitted] lodash
Entrypoint lodash = lodash.bundle.js
Entrypoint app = app.bundle.js
entry
为多入口时,入口文件顺序即是html
模板引入对应输出文件的顺序。不同入口文件之间没有依赖关系。
3.3 SplitChunksPlugin配置
3.3.1 同步代码分割
- 通过
SplitChunksPlugin
实现同步代码分割。
webpack 4+
支持SplitChunksPlugin
。
- 编辑打包配置文件
webpack.base.config.js
optimization: {
splitChunks: {
chunks: "all"
}
}
- 编辑
src/index.js
import _ from 'lodash'
console.log(_.join(['1','2', '3'], '-'))
- 打包分析
npm run dev
Built at: 04/12/2019 9:37:25 AM
Asset Size Chunks Chunk Names
app.bundle.js 32.4 KiB app [emitted] app
index.html 289 bytes [emitted]
vendors~app.bundle.js 1.35 MiB vendors~app [emitted] vendors~app
Entrypoint app = vendors~app.bundle.js app.bundle.js
lodash
打包输出代码被分割到vendors~app.bundle.js
文件中。
chunk
表示打包输出模块,打包输出几个文件,chunks
就有几个。
同步代码分割可以通过浏览器缓存功能提升第二次页面加载速度。
- 指定代码分割打包输出文件名
(1) 编辑打包配置文件
output: {
//...
chunkFilename: '[name].chunk.js',
//...
}
在
html
页面中直接引入的资源文件(js
、css
)命名以filename
为规则。间接引用的资源文件命名以chunkFilename
为规则
(2) 打包分析
npm run dev
Built at: 04/12/2019 9:39:07 AM
Asset Size Chunks Chunk Names
app.bundle.js 32.4 KiB app [emitted] app
index.html 288 bytes [emitted]
vendors~app.chunk.js 1.35 MiB vendors~app [emitted] vendors~app
Entrypoint app = vendors~app.chunk.js app.bundle.js
3.3.2 异步代码分割
- 使用
@babel/plugin-syntax-dynamic-import
实现代码分割 - 安装依赖
npm i @babel/plugin-syntax-dynamic-import -D
- 编辑
babel
配置
"plugins": [
"@babel/plugin-syntax-dynamic-import"
]
- 编辑
src/index.js
import('lodash').then(({default : _}) => {
console.log(_.join(['1','2', '3'], '-'))
})
- 打包分析
npm run dev
Built at: 04/12/2019 9:29:30 AM
Asset Size Chunks Chunk Names
0.bundle.js 1.35 MiB 0 [emitted]
app.bundle.js 33.8 KiB app [emitted] app
index.html 221 bytes [emitted]
webpack
会自动对通过import()
方法异步加载的模块进行代码分割。
异步代码分割既可以通过浏览器缓存功能提升第二次页面加载速度,又可以通过懒加载的方式提升首次页面加载速度。
- 指定代码分割打包输出文件名
import(/* webpackChunkName: "lodash" */'lodash').then(({default : _}) => {
//...
import()
方法代码分割的底层还是通过SplitChunksPlugin
实现的,splitChunks
配置参数同样会影响import()
方法代码分割情况。
3.3.3 SplitChunksPlugin配置参数
-
optimization.splitChunks
默认配置项
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async',
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
};
webpack4.X
版本才支持SplitChunksPlugin
。在webpack3.X
中,使用CommonsChunkPlugin
进行代码分割。
-
optimization.splitChunks
配置项说明
optimization: {
splitChunks: {
chunks: 'all',
//initial只对同步代码分割,async(默认)只对异步代码分割、all所有代码都做代码分割
minSize: 30000,
//大于30000Bit的模块才做代码分割
maxSize: 0,
//当模块大于maxSize时,会对模块做二次代码分割。当设置为0时,不做二次分割。
minChunks: 1,
//当打包输出chunks文件引用该模块的次数达到一定数目时才做代码分割。
maxAsyncRequests: 5,
//异步加载的js文件最大数目为边界条件进行代码分割
maxInitialRequests: 3,
//以初始加载的js文件最大数目为边界条件进行代码分割
automaticNameDelimiter: '~',
//代码分割生成文件连接符
name: true,
//代码分割生成文件自动生成文件名
cacheGroups: {
//代码分割缓存组,被分割代码文件通过缓存组输出为一个文件。
vendors: {
test: /[\\/]node_modules[\\/]/,
//模块路径正则表达式
priority: -10,
//缓存组优先级,一个模块优先打包输出到优先级高的缓存组中。
name: 'vendor'
//代码分割打包输出文件名
},
lodash: {
test: /[\\/]lodash[\\/]/,
priority: -5,
},
jquery: {
test: /[\\/]jquery[\\/]/,
priority: -5,
},
default: {
//默认缓存组,一般设置priority优先级数值最小
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
//代码分割的模块A引用其他模块B,B已经被打包输出,则不再重新打包进入A
name: 'common',
chunks: 'all'
}
}
}
}
optimization.splitChunks.chunks
可设置,默认值为async
,表示默认只对动态import()
做代码分割。splitChunks.cacheGroups.{cacheGroup}.chunks
同样可以设置,默认值为all
,表示cacheGroups
分组代码分割优先级高于import()
。
3.4 懒加载(Lazy Loading)
-
Lazy Loading
文档。 -
import()
实现懒加载
const lazyConsole = async () => {
const {default : _} = await import(/* webpackChunkName: "lodash" */'lodash');
console.log(_.join(['1','2', '3'], '-'))
};
document.addEventListener('click', lazyConsole)
import()
动态加载不仅可以实现代码分割,还可以实现懒加载。
lodash
模块生成的vendors~lodash.bundle.js
文件在点击页面时才加载。
只有配置
chunkFilename
之后,webpackChunkName
才生效。
如果多个import()
的魔法注释webpackChunkName
指定同一个名字,则这多个import()
模块会打包成一个bundle
。
如果外部也引入了import()
方法中引入的模块,则该模块不会分割单独打包。
3.5 预取/预加载模块(prefetch/preload module)
3.5.1 查看页面代码利用率
-
chrome
浏览器打开网页 - 打开调试面板
-
commond + shift + p
-show coverage
-instrument coverage
image.png -
刷新网页
代码利用率 -
分析结果
红色表示加载并运行代码,绿色表示只加载未运行代码。
可以看到该页面加载的每一个文件的利用率以及综合利用率。
点击右侧横条,可以看到具体文件代码利用情况。
image.png
3.5.2 提高代码利用率
- 通过
import()
异步模块懒加载的方式可以提高首屏代码利用率。 - 未使用懒加载
src/index.js
document.addEventListener('click', () => {
const element = document.createElement('div');
element.innerHTML = 'Dell Li';
document.body.appendChild(element)
});
代码利用率为:77%
- 通过懒加载
src/index.js
document.addEventListener('click', () => {
import('./click').then(({default: click}) => {
click && click()
})
});
src/click.js
const handleClick = () => {
const element = document.createElement('div');
element.innerHTML = 'Dell Li';
document.body.appendChild(element)
};
export default handleClick
代码利用率为:81.5%
异步模块懒加载虽然可以减少首屏代码量,缩短网页首次加载时间,但等待用户交互后才请求对应
js
文件,会影响用户体验。可以通过prefetch/preload
方式解决该问题。
3.5.3 prefetch/preload module
-
webpack v4.6.0+
添加了预取和预加载(prefetch/preload module)的支持。 - 使用
prefetch
src/index.js
document.addEventListener('click', () => {
import(/* webpackPrefetch: true */ './click').then(({default: click}) => {
click && click()
})
});
这会生成 <link rel="prefetch" href="1.bundle.js">
并追加到页面头部,指示着浏览器在闲置时间预取1.bundle.js
文件。
-
prefetch
/preload
指令对比
-
preload chunk
会在父chunk
加载时,以并行方式开始加载。prefetch chunk
会在父chunk
加载结束后开始加载。 -
preload chunk
具有中等优先级,并立即下载。prefetch chunk
在浏览器闲置时下载。 -
preload chunk
会在父chunk
中立即请求,用于当下时刻。prefetch chunk
会用于未来的某个时刻。 - 浏览器支持程度不同。
4. CSS文件的代码分割
4.1 现有CSS打包分析
- 打包配置
webpack.base.config.js
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[path][name]__[local]--[hash:base64:5]'
}
},
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: [ require('autoprefixer')]
}
}
]
},
{
test: /\.scss$/,
use: [
"style-loader",
"css-loader",
"sass-loader",
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: [ require('autoprefixer')]
}
}
]
}
]
}
- 入口文件
src/index.js
import './style.css'
src/style.css
body {
background: yellow;
}
- 打包输出
npm run build
Built at: 04/12/2019 3:53:13 PM
Asset Size Chunks Chunk Names
app.bundle.js 6.79 KiB 0 [emitted] app
app.bundle.js.map 3.04 KiB 0 [emitted] app
index.html 221 bytes [emitted]
Entrypoint app = app.bundle.js app.bundle.js.map
- 存在的问题
没有打包输出css
文件,css
代码被打包到js
中。
4.2 MiniCssExtractPlugin
-
MiniCssExtractPlugin
文档介绍
该插件将CSS
分割到文件中。对每个js
文件中的css
代码创建一个css
文件。支持css
按需加载和sourcemap
。
MiniCssExtractPlugin
不支持HMR
(模块热更新),建议在生产环境中使用。
在
webpack4
版本中,我们之前首选使用的extract-text-webpack-plugin完成了其历史使命。推荐使用mini-css-extract-plugin。
- 安装
MiniCssExtractPlugin
npm install --save-dev mini-css-extract-plugin
-
webpack.base.config.js
中对css
和scss
文件的loader
处理移动到webpack.dev.config.js
中。 - 修改打包配置文件
webpack.pro.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
//...
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[path][name]__[local]--[hash:base64:5]'
}
},
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: [ require('autoprefixer')]
}
}
]
},
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
"sass-loader",
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: [ require('autoprefixer')]
}
}
]
},
]
},
plugins: [
new MiniCssExtractPlugin({})
]
与之前的配置相比做了两点修改,一个是引入
new MiniCssExtractPlugin({})
插件,一个是MiniCssExtractPlugin.loader
替换style-loader
。
由于webpack-merge
可以对module.rules
进行合并,但无法对单个rule
中的loader
进行合并。所以在webpack.pro.config.js
里写了完整的处理css
和sass
文件的rule
。也可以在webpack.base.config.js
通过环境变量的逻辑进行判断添加MiniCssExtractPlugin.loader
或者style-loader
。
- 打包输出
npm run build
Built at: 04/12/2019 4:16:56 PM
Asset Size Chunks Chunk Names
app.bundle.js 1010 bytes 0 [emitted] app
app.bundle.js.map 3.04 KiB 0 [emitted] app
app.css 66 bytes 0 [emitted] app
app.css.map 170 bytes 0 [emitted] app
index.html 259 bytes [emitted]
Entrypoint app = app.css app.bundle.js app.css.map app.bundle.js.map
打包输出了css
文件。
如果没有打包输出
css
文件。原因可能是production
自动开启Tree Shaking
,需要将css
文件标记为side-effect-free
(无副作用)。
"sideEffects"
:["*.css"]
-
css
文件命名规则
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[id].chunk.css"
})
]
-
css
文件压缩
(1) 安装依赖
npm install --save-dev optimize-css-assets-webpack-plugin
(2) 编辑配置文件
webpack.prod.config.js
var OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const prodConfig = {
//...
optimization: {
minimizer: [
new OptimizeCssAssetsPlugin({})
]
}
//...
}
(3) 打包输出
Built at: 04/12/2019 4:50:40 PM
Asset Size Chunks Chunk Names
app.bundle.js 4.1 KiB 0 [emitted] app
app.bundle.js.map 3.66 KiB 0 [emitted] app
app.css 56 bytes 0 [emitted] app
index.html 259 bytes [emitted]
Entrypoint app = app.css app.bundle.js app.bundle.js.map
- 可通过
cacheGroups
实现将所有js
文件中的css
打包到一个css
文件中(Extracting all CSS in a single file)和将一个入口文件对应的所有css
打包到一个css
文件中(Extracting CSS based on entry)。
5. 打包分析(bundle analysis)
5.1 打包分析工具介绍
- 如果我们以分离代码作为开始,那么就应该以检查模块的输出结果作为结束,对其进行分析是很有用处的。
-
官方提供分析工具 是一个好的初始选择。下面是一些可选择的社区支持工具:
(1) webpack-chart:webpack stats
可交互饼图。
(2) webpack-visualizer:可视化并分析你的bundle
,检查哪些模块占用空间,哪些可能是重复使用的。
(3) webpack-bundle-analyzer:一个plugin
和CLI
工具,它将bundle
内容展示为便捷的、交互式、可缩放的树状图形式。
(4) webpack bundle optimize helper:此工具会分析你的bundle
,并为你提供可操作的改进措施建议,以减少bundle
体积大小。
5.2 官方分析工具
-
analyse
文档 - 编辑打包命令
package.json
"scripts": {
"dev": "webpack --profile --json > stats.json --config ./build/webpack.dev.config.js"
}
- 打包输出
npm run dev
生成stats.json
文件,该文件中包含打包信息。 - 使用
analyse
分析打包结果
将stats.json
文件上传到analyse
分析地址,即可看到打包细节信息。
5.3 webpack-bundle-analyzer
- 安装依赖
npm install --save-dev webpack-bundle-analyzer
- 编辑打包配置文件
webpack.base.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
- 打包结果分析
image.png
如果打包生成的不同Asset
引入了相同的js
文件,则说明该js
文件被重复打包进两个不同的资源,需要修改配置将该js
文件进行分割。
6. Shimming
shimming
文档
6.1 shimming 全局变量
- 一些第三方的库(
library
)可能会引用一些全局依赖(例如jQuery
中的$
)。这些“不符合规范的模块”就是shimming
发挥作用的地方。 - 安装
jquery
npm i jquery lodash --save
- 修改打包配置文件
webpack.base.config.js
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
join: ['lodash', 'join']
})
]
当有模块使用
$
时,会自动import $ from 'jquery'
- 可直接使用
$
src/index.js
const dom = $('div')
dom.html(join(['hello', 'world'], ' '))
$('body').append(dom)
shimming
和alias
对比:alias
的作用是创建import
或require
的别名,来确保模块引入变得更简单。shimming
的作用是解决一些第三方的库(library
)可能会引用的一些全局依赖。即:alias
使模块引入更简单,不用写复杂路径;shimming
使模块不用引入,使用全局变量的方式。
6.2 imports-loader
- 打印现在模块中
this
指向
src/index.js
console.log(this === window); //false
- 安装依赖
npm i imports-loader -D
- 编辑打包配置文件
webpack.base.config.js
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{loader: "babel-loader"},
{loader: "imports-loader?this=>window"}
]
}
]
}
- 打印现在模块中
this
指向
src/index.js
console.log(this === window); //true
项目中配置
imports-loader?this=>window
可能导致打包错误,'import' and 'export' may only appear at the top level (4:0)
。
7. 环境变量
- 修改打包配置文件
webpack.dev.config.js
const devConfig = {
//...
}
module.exports = devConfig
webpack.prod.config.js
const prodConfig = {
//...
}
module.exports = prodConfig
webpack.base.config.js
const merge = require('webpack-merge')
const devConfig = require('./webpack.dev.config')
const prodConfig = require('./webpack.prod.config')
const baseConfig = {
//...
}
module.exports = (env) => {
if(env && env.production) {
return merge(baseConfig, prodConfig)
} else {
return merge(baseConfig, devConfig)
}
}
- 修改打包命令
package.json
"scripts": {
"build": "webpack --env.production --config ./build/webpack.base.config.js",
"dev": "webpack --config ./build/webpack.base.config.js",
"start": "webpack-dev-server --config ./build/webpack.base.config.js"
}
这里的
--env.production
与打包配置文件中的env && env.production
对应。
如果使用--env.production=abc
,则打包配置文件中需要使用env && env.production==='abc'
的写法。
如果使用--env production
,则打包配置文件中需要使用env === 'production'
的写法。
8. TypeScript
8.1 引入TypeScript
- 安装依赖
➜ webpack-operate npm i ts-loader typescript -D
- 项目根目录创建
TypeScript
配置文件
tsconfig.json
{
"compilerOptions": {
"module": "commonjs", //模块引入机制
"target": "es5", //转化为es5
"sourceMap": true, //支持sourceMap
"allowJs": true //支持js引入
},
"exclude": [
"node_modules"
]
}
- 创建入口文件
src/index.ts
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message
}
greet() {
return 'Hello' + this.greeting;
}
}
let greeter = new Greeter('world')
alert(greeter.greet())
- 编辑打包配置文件
webpack.config.base.js
entry: {
app: path.resolve(__dirname, '../src/index.ts'),
}
//...
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: "ts-loader"
}
]
//...
}
- 打包输出
npm run bundle
8.2 对库代码进行编译检查
-
查询
TypeScript
支持编译检查的库。 - 对库代码进行编译检查——以
lodash
为例
(1) 安装依赖
➜ webpack-operate npm i @types/lodash --save-dev
(2) 修改ts
文件
src/index.ts
import * as _ from 'lodash'
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message
}
greet() {
//return _.join(123) //传参不是数组,标红报错
return _.join([ 'Hello', this.greeting], ' ');
}
}
let greeter = new Greeter('world')
alert(greeter.greet())
9. Eslint
9.1 使用eslint
- 安装依赖
➜ webpack-operate npm i eslint -D
- 初始化
eslint
配置文件
npx eslint --init
自动生成.eslintrc.js
文件。
➜ webpack-operate npx eslint --init
? How would you like to use ESLint? To check syntax and find problems
? What type of modules does your project use? JavaScript modules (import/export)
? Which framework does your project use? React
? Where does your code run? (Press <space> to select, <a> to toggle all, <i> to invert selection)Browser
? What format do you want your config file to be in? JavaScript
The config that you've selected requires the following dependencies:
eslint-plugin-react@latest
? Would you like to install them now with npm? Yes
- 使用
airbnb
规则
(1) 安装依赖
➜ webpack-operate npm install eslint-config-airbnb eslint-plugin-import eslint-plugin-react eslint-plugin-jsx-a11y babel-eslint -D
(2) 修改.eslintrc.js
配置文件
"extends": "airbnb",
"parser": "babel-eslint"
- 检查代码
(1) 命令行检查方式
npx eslint XXX(文件夹名字)
(2) 编辑器检查方式
image.png - 使某些规则失效
编辑.eslintrc.js
规则文件
"rules": {
"no-unused-vars": 0
}
以上是在项目中使用
eslint
,与Webpack
无关。
9.2 Webpack中配置eslint
-
eslint-loader
文档 - 安装依赖
➜ webpack-operate npm i eslint-loader -D
- 编辑打包配置文件
webpack.base.config.js
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{loader: "babel-loader"},
{loader: "eslint-loader"}
]
}
]
}
webpack.dev.config.js
devServer: {
overlay: true
}
eslint-loader
的作用是打包时先使用eslint
检查规则,应该放在babel-loader
之后。overlay
的作用是使用webpack-dev-server
打包时,将报错信息显示在页面上。
- 对不符合规范的代码进行简单修复
{
loader: "eslint-loader",
options: {
fix: true
}
}
使用
eslint-loader
会在打包前对代码进行检查,降低打包效率。在实际项目开发中,一般使用eslint
与git
结合,在代码提交到git
仓库时对代码进行检查。
10. PWA
- 安装依赖
➜ webpack-operate npm i workbox-webpack-plugin -D
- 编辑生产环境打包配置文件
webpack.prod.config.js
const WorkBoxPlugin = require('workbox-webpack-plugin')
var prodConfig = {
//...
plugins: [
new WorkBoxPlugin.GenerateSW({
clientsClaim: true,
skipWaiting: true
})
]
//...
}
生产环境才需要使用
PWA
。
- 编辑入口文件
src/index.js
//业务代码
import('lodash').then(({default : _}) => {
console.log(_.join(['1','2', '3'], '-'))
})
//使用serviceWorker
if('serviceWorker' in navigator) { //如果浏览器支持serviceWorker
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(res => {
console.log('serviceWorker registed')
})
.catch(err => {
console.log('serviceWorker registe err')
})
})
}
service-worker.js
文件在打包时生成。
- 打包输出
npm run build
Asset Size Chunks Chunk Names
2.6c02624b28028221db11.chunk.js 529 KiB 2 [emitted]
2.6c02624b28028221db11.chunk.js.map 630 KiB 2 [emitted]
app.051fb24e16eb3c7493d6.chunk.js 812 bytes 0 [emitted] app
app.051fb24e16eb3c7493d6.chunk.js.map 783 bytes 0 [emitted] app
index.html 326 bytes [emitted]
precache-manifest.a8a4feb9efc884fe5d31eed9b7b76ac0.js 445 bytes [emitted]
runtime.a366ecc84e6df04acf79.bundle.js 8.81 KiB 1 [emitted] runtime
runtime.a366ecc84e6df04acf79.bundle.js.map 8.8 KiB 1 [emitted] runtime
service-worker.js 927 bytes [emitted]
Entrypoint app = runtime.a366ecc84e6df04acf79.bundle.js runtime.a366ecc84e6df04acf79.bundle.js.map app.051fb24e16eb3c7493d6.chunk.js app.051fb24e16eb3c7493d6.chunk.js.map
- 本地开启一个服务
➜ webpack-operate cd dist
➜ dist http-server
Starting up http-server, serving ./
Available on:
http://127.0.0.1:8080
http://192.168.1.3:8080
http://192.168.57.1:8080
Hit CTRL-C to stop the server
- 测试
PWA
打开http://127.0.0.1:8080
,可以看到html
页面。
关闭服务,刷新浏览器,仍然可以正常访问页面。
11. 编写并发布一个npm包
- 创建文件夹
nmw-lodash
- 将项目初始化为一个
npm
包
➜ nmw-lodash npm init -y
- 安装依赖
➜ nmw-lodash npm i webpack webpack-cli --save
➜ nmw-lodash npm i lodash --save
- 编写代码
src/index.js
import * as math from './math'
import * as string from './string'
export default {
math,
string
}
src/math.js
export function add(a, b) {
return a + b;
}
src/string.js
import _ from 'lodash'
export function join(a, b) {
return _.join([a, b], ' ')
}
- 创建并编辑打包配置文件
webpack.config.js
const path = require('path')
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'nmw-lodash.js',
library: "nmwLodash", //以script标签引入时,支持nmwLodash全局变量
libraryTarget: "umd" //支持umd规范引入
//libraryTarget: "global" nmwLodash挂载在global上。
},
externals: [
"lodash" //不打包lodash
]
}
- 创建打包命令
package.json
"scripts": {
"build": "webpack"
}
- 修改
npm
包入口文件
package.json
"main": "./dist/nmw-lodash.js",
- 打包输出
npm run build
- 在
npm
官网注册账号 - 添加账号密码
➜ nmw-lodash npm adduser
- 发布项目
npm publish
12. 打包性能优化
12.1 优化配置
- 跟上技术的迭代
Node
、Npm
、Yarn
- 在尽可能少的模块上应用
Loader
例如:使用exclude
和include
。
{
test: /\.js$/,
exclude: /node_modules/,
use: [{loader: "babel-loader"}]
}
exclude
表示不进行loader
编译的路径。
include
表示只进行loader
编译的路径。
-
Plugin
尽可能精简并确保可靠
例如:只在生产环境使用MiniCssExtractPlugin
对CSS
进行分割。
var MiniCssExtractPlugin = require("mini-css-extract-plugin");
//...
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[id].chunk.css"
})
]
-
resolve
参数合理配置 (文档)
(1)resolve.alias
:创建import
或require
的别名,来确保模块引入变得更简单。
例如:import Utility from 'Utilities';
(2)resolve.extensions
:自动解析确定的扩展。能够使用户在引入模块时不带扩展。
例如:import File from '../path/to/file';
(3)resolve.mainFiles
解析目录时要使用的文件名。
例如:resolve
配置如下
resolve: {
extensions: ['.js', '.jsx'],
mainFiles: ['index'], //默认配置
alias: {
child: path.resolve(__dirname, '../src/components/child')
}
}
模块引入方式如下:
import Child from 'child';
resolve
配置不宜过于复杂,否则会使模块查找时间增加,降低webpack
打包速度。
- 控制包文件大小
(1) 使用Tree Shaking
(2)Code Splitting
代码分割 -
thread-loader
、parallel-webpack
、happypack
多进程打包 - 合理使用
SourceMap
- 结合
state
分析打包结果(bundle analysis
) - 开发环境内存编译(
webpack-dev-server
) - 开发环境无用插件剔除
12.2 DIIPlugin
12.2.1 使用DIIPlugin
- 测试打包速度
npm run build
打包耗时约950ms
- 第三方模块没有必要频繁重新打包。可以将第三方模块打包输出,
webpack
进行项目打包时,直接使用已经被打包的第三方模块,不再重新打包。 - 创建并编辑打包配置文件
webpack.dll.config.js
const path = require('path');
const webpack = require('webpack')
module.exports = {
mode: 'production',
entry: {
vendors: ['react', 'react-dom', 'lodash'],
},
output: {
filename: "[name].dll.js",
path: path.resolve(__dirname, '../dll'),
library: "[name]" //以vendors全局变量的方式暴露
},
plugins: [
new webpack.DllPlugin({ //生成vendors.manifest.json映射文件
name: '[name]',
path: path.resolve(__dirname, '../dll/[name].manifest.json')
})
]
}
- 创建打包命令
package.json
"scripts": {
//...
"build:dll": "webpack --config ./build/webpack.dll.config.js",
//...
}
- 打包生成
vendors
包
npm run build:dll
生成vendors.dll.js
以及vendors.manifest.json
映射文件。 - 安装依赖
npm i add-asset-html-webpack-plugin -D
- 编辑打包配置文件
webpack.base.config.js
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')
//...
plugins: [
//...
new AddAssetHtmlPlugin({
//将vendors.dll.js插入html模板中
filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
}),
//打包代码时,第三方模块如果在vendors.manifest.json有映射,则直接在vendors全局变量中取。
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')
})
],
AddAssetHtmlPlugin
插件必须放在HtmlWebpackPlugin
后面。
- 测试打包速度
npm run build
打包耗时约670ms
- 总结
生成vendors
包及映射 - 将vendors
包插入html
模板 - 以vendors
全局变量暴露 - 使用vendors
包
12.2.2 多个DIIPlugin
- 编辑
dll
包打包配置文件
webpack.dll.config.js
//...
entry: {
vendors: ['lodash'],
react: ['react', 'react-dom']
}
//...
- 编辑打包配置文件
webpack.base.config.js
动态生成plugins
数组。
const fs = require('fs')
const plugins =[
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../src/index.html')
}),
new CleanWebpackPlugin()
//...
]
const files = fs.readdirSync(path.resolve(__dirname, '../dll'));
//根据 dll 目录中生成的文件,添加对应插件。
files.forEach(file => {
if(/.*\.dll\.js/.test(file)) {
//XXX.dll.js插入html模板中
plugins.push(new AddAssetHtmlPlugin({
filepath: path.resolve(__dirname, '../dll', file)
}))
}
if(/.*\.manifest\.json/.test(file)) {
//根据XXX.manifest.json映射,直接在XXX全局变量中获取第三方模块。
plugins.push(new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll', file)
}))
}
})
13. 多页面打包配置
13.1 介绍
- 多页面应用
① 生成多个html
文件。
② 各个html
文件引入对应的jsbundle
。 - 多页面应用的实现方式
(1) 多配置
对多个webpack
配置分别打包,生成多个html
页面。
(2) 单配置
对一个webpack
配置进行打包,生成多个html
页面。
html-webpack-plugin
文档
13.2 多配置
- 技术基础
(1)webpack
打包可以接收一个配置数组。
(2)parallel-webpack
提高打包速度。
直接使用
webpack
也可以接收一个配置数组,但串行打包过程速度比较慢。parallel-webpack
可以并行打包,提高打包速度。
- 特点
(1) 优点
可以使用parallel-webpack
提高打包速度。
配置之间更加独立、灵活。
(2) 缺点
不能多页面之间共享代码。 - 创建编辑模板文件
src/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
</body>
</html>
如果打包配置文件添加了
html-loader
,会正常解析html
文件作为模版,就会直接把<%= htmlWebpackPlugin.options.title %>
解析成字符串。
- 创建编辑入口文件
src/index.js
console.log('this is index.js');
src/list.js
console.log('this list.js');
- 编辑打包配置文件
webpack.pro.config.js
const baseConfig = require('./webpack.base.config');
//...
const prodConfig = {
//...
}
const buildConfig = merge(baseConfig, prodConfig);
const generatePage = function (
{ entry = '',
title = '',
name = '',
chunks = [],
template = path.resolve(__dirname, '../src/index.html')
} = {}) {
return {
entry,
plugins: [
new HtmlWebpackPlugin({
chunks,
template,
title,
filename: name + '.html'
})
]
}
};
const indexConfig = generatePage({
entry: {
app: path.resolve(__dirname, '../src/index.js')
},
title: 'page index',
name: 'index',
chunks: ['runtime','vendors','app']
});
const listConfig = generatePage({
entry: {
list: path.resolve(__dirname, '../src/list.js')
},
title: 'page list',
name: 'list',
chunks: ['runtime','vendors','list']
});
const pagesConfig = [indexConfig, listConfig];
module.exports = pagesConfig.map(pageConfig => merge(pageConfig, buildConfig));
多配置在同一个文件中,生成一个配置数组。
⭐️⭐️这里的chunks: ['runtime','vendors','app'/'list']
可以省略,因为是多配置,默认会插入所有chunks
。如果是13.3
中的单配置,入口有多个,那么就必须指定插入的chunks
。
- 打包
npm run build
Hash: 10dc11f107c648d35db3659186349535b844a395
Version: webpack 4.30.0
Child
Hash: 10dc11f107c648d35db3
Time: 876ms
Built at: 06/01/2019 4:38:36 PM
Asset Size Chunks Chunk Names
app.bundle.js 963 bytes 0 [emitted] app
index.html 190 bytes [emitted]
Child
Hash: 659186349535b844a395
Time: 850ms
Built at: 06/01/2019 4:38:36 PM
Asset Size Chunks Chunk Names
list.bundle.js 962 bytes 0 [emitted] list
list.html 191 bytes [emitted]
多配置打包不可以使用
clean-webpack-plugin
,否则后一个打包会清除前一个打包结果。
- 使用
parallel-webpack
打包
(1) 安装
npm i parallel-webpack -D
(2) 打包
./node_modules/parallel-webpack/bin/run.js --config=build/webpack.prod.config.js
13.3 单配置
- 特点
(1) 优点
可以共享各个entry
之间的公用代码。
(2) 缺点
打包比较慢。
输出的内容比较复杂。
配置不够独立,相互耦合。例如:无法实现对不同入口设置不同的splitChunks
代码分割规则、无法实现对不同入口设置不同的动态路由(splitChunks
会将公共代码提出来,提前加载)。
单配置时,
webpack
打包配置对不同入口的所有chunks
都生效。只要有一个入口的同步代码依赖树中含有某一个模块,该模块就不会被动态路由异步加载。
- 编辑打包配置文件
webpack.pro.config.js
//...
const buildConfig = merge(baseConfig, prodConfig);
//...
const pagesConfig = [indexConfig, listConfig];
module.exports = merge(pagesConfig.concat(buildConfig));
webpack-merge
可以接收多个参数merge(object1, object2, object3, ...)
,也可以接收一个数组merge([object1, object2, object3, ...])
。
- 打包
npm run build
Version: webpack 4.30.0
Time: 668ms
Built at: 06/01/2019 4:54:57 PM
Asset Size Chunks Chunk Names
app.bundle.js 963 bytes 0 [emitted] app
index.html 190 bytes [emitted]
list.bundle.js 963 bytes 1 [emitted] list
list.html 191 bytes [emitted]
Entrypoint app = app.bundle.js
Entrypoint list = list.bundle.js
网友评论