刷一刷webpack文档

webpack的核心定义

一个模块打包器(能识别ES Moudule和CommonJS规范和其他如css等格式的文件)

提高构建和打包效率

  • 让node和webpack版本最新

package.json文件

1
2
3
4
{
"private": true, // 私有项目, 不会被发布到npm线上仓库
"main": "index.js", // 被外部引用的js文件
}

npm script

1
2
3
$ npm init -y # 用默认配置初始项目
$ npm info webpack # 查看包的历史版本号
$ npx webpack -v # 使用nodemodules里的包

entry config

1
2
3
4
5
6
entry: './src/index.js'
// 其实是如下的简写
entry: {
main: './src/index.js',
sub: './src/index.js',
}

output config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// https://webpack.js.org/guides/output-management
output: {
publicPath: 'http://cdn.com.cn',
// name会对应 main 和 sub
filename: '[name].[hash].bundle.js',
path: path.resolve(__dirname, 'dist'),
chunkFilename: '[name].[chunkhash].chunk.js'
}

// 对不同的entry, 打包的output文件名称不一样, 如有的加hash, 有的不需要hash
// https://webpack.js.org/configuration/output/#outputfilename
module.exports = {
//...
output: {
filename: (chunkData) => {
return chunkData.chunk.name === 'main' ? '[name].js': '[name]/[name].js'
},
}
}

mode config

1
mode: 'development' // 代码不会被压缩, 默认为 production, 代码会被压缩

loader 非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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
module: {
rules: [
{
test: /\.(jpg|png|gif)$/,
use: {
// 会把图片转成base64, 如果不配置limit会直接放到js里
loader: 'url-loader',
options: {
// placeholder 占位符语法
name: '[name]_[hash].[ext]',
outputPath: 'images/',
// limit: 1024 // file-loader没有这个配置项
}
}
},
{
// 字体文件
test: /\.(eot|ttf|svg)$/,
use: {
loader: 'file-loader',
}
},
{
test: /\.css$/,
use: [
'style-loader', // 将css内容挂载到head
'css-loader', // 分析css文件的引用关系
]
},
{
test: /\.scss$/,
// use 顺序, 从下到上, 从右到左
use: [
'style-loader',
{
loader: 'css-loader',
options: {
// 在scss文件里再@import scss文件时用后面两个loader
importLoaders: 2,
// 开启css的模块化打包, 不然样式都是全局的
// import style from './index.scss'
// img.classList.add(style.avatar)
modules: true
}
},
// https://webpack.js.org/loaders/sass-loader
// npm install sass-loader node-sass webpack --save-dev
'sass-loader',
// https://webpack.js.org/loaders/postcss-loader
'postcss-loader' // 自动添加厂商前缀等
]
}
]
}

plugins 可以在webpack运行到某个时刻, 做一些事情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
plugins: [
// 打包结束后, 挂载js到模板
// npm install --save-dev html-webpack-plugin
// 对于多个入口需要挂载到多个模板上, 可以new 多个HtmlWebpackPlugin
new HtmlWebpackPlugin({
template: '../src/index.html'
}),
// npm i clean-webpack-plugin -D
new CleanWebpackPlugin([
'dist'
],{
root: path.resolve(__dirname, '../')
}),
new webpack.HotModuleReplacementPlugin()
]

postcss

1
2
3
4
5
6
// postcss.config.js
module.exports = {
plugins: [
require('autoprefixer')
]
}

sourcemap 源码映射

1
2
3
4
5
6
7
8
9
// https://webpack.js.org/configuration/devtool#devtool
devtool: 'none' // 关闭sourcemap
devtool: 'source-map' // 会生成一个.map文件
devtool: 'inline-source-map' // .map文件会被打包到js文件里, 错误提示会精确到第几行第几列
devtool: 'cheap-inline-source-map' // 只精确到行, 不精确到列, 提示性能, 而且只会提示业务代码的错误, 不提示loader和第三方模块的错误
devtool: 'cheap-module-inline-source-map' // 提示loader和第三方模块的错误
devtool: 'eval' // 用js eval效率最高, 提示不全面
devtool: 'cheap-module-eval-source-map' // 开发时的最佳实践
devtool: 'cheap-module-source-map' // 生产时的最佳实践

webpack devServer

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
// https://webpack.js.org/configuration/dev-server
// npm i webpack-dev-server -D
// 会将打包的内容放到内存
devServer: {
contentBase: './dist', // 服务器根路径
open: true, // 自动打开浏览器
hot: true, // 开启 HMR
hotOnly: true, // 即使HMR不生效, 浏览器也不刷新
}

// package.json
"scripts": {
"start": "webpack-dev-server"
}

// server.js
// npm i express webpack-dev-middleware -D
const express = require('express')
const webpack = require('webpack')
const webpackDevMiddleware = require('webpack-dev-middleware')
const config = require('./webpack.config.js')
// https://webpack.js.org/api/node
// 在node中使用webpack
const complier = webpack(config)

const app = express()
app.use(webpackDevMiddleware(complier, {
publicPath: config.output.publicPath
}))
app.listen(3000, () => {

})

HMR 页面无刷新更新视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// https://webpack.js.org/guides/hot-module-replacement
// https://webpack.js.org/api/hot-module-replacement
// https://webpack.js.org/concepts/hot-module-replacement
import number from './number'

number()

// 如果有HMR
if (module.hot) {
// 监测 ./number 模块
module.hot.accept('./number', () => {
number()
})
}
// 以上代码已在 css-loader, vue-loader, babel-preset等里已做处理

babel

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// https://babeljs.io/setup#installation
// npm install --save-dev babel-loader @babel/core
// npm i -D @babel/core@^7.0.0-0
// babel-loader 只是webpack和babel做通信的一个桥梁
// webpack并不会把es6语法翻译成es5语法
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
targets: {
chrome: '67' // 生产运行的环境
},
// babel集成了polyfill,
// 根据业务代码按需做polyfill,
// 而且不需要手动import '@babel/polyfill'
useBuiltIns: 'usage',
]
]
}
}
]
}
// npm install @babel/preset-env --save-dev
// 这个模块把es6语法翻译成es5语法等
// .babelrc.json
{
"presets": ["@babel/preset-env"]
}
// 为兼容更低版本的浏览器
// https://babeljs.io/docs/en/babel-polyfill
// npm install --save @babel/polyfill
import '@babel/polyfill'

// 打包组件库和类库时, 不污染全局
// https://babeljs.io/docs/en/babel-plugin-transform-runtime
// npm install --save-dev @babel/plugin-transform-runtime
// npm install --save @babel/runtime
// 可把options内容放到.babelrc
options: {
// presets: [
// [
// '@babel/preset-env',
// targets: {
// chrome: '67' // 生产运行的环境
// },
// useBuiltIns: 'usage', // 根据业务代码按需做polyfill
// ]
// ]
'plugins': [
'@babel/plugin-transform-runtime',
{
'corejs': 2, // npm install --save @babel/runtime-corejs2
'helpers': true,
'regenerator': true,
'useESModules': false
}
]
}

tree shaking 根据引入的按需打包, 摇晃掉模块里无用的部分, 根树没有关联的模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Tree Shaking只支持 ES Module(静态引入), 不支持Common JS(动态引入)
// development mode 默认没有tree shaking
// production mode 不需要这个optimization
optimization: {
usedExports: true
}

// package.json
// 不然打包时会忽略 @babel/polly-fill, 因为其没有导出对象, 只在window上挂载了对象
"sideEffects": false, // false时对所有模块摇树
"sideEffects": [
"@babel/polly-fill",
"*.css" // 对css不摇树
],

mode

1
2
3
4
5
6
7
8
9
// package.json
"scripts": {
"dev": "webpack-dev-server --config webpack.dev.js"
"dev-build": "webpack --config webpack.dev.js"
"build": "webpack --config webpack.prod.js"
}
// npm i webpack-merge -D
const merge = require('webpack-merge')
merge(commonConfig, devConfig)

code splitting 代码分割

  • 分割业务代码和库代码, 不然打包文件会很大, 首次访问加载时间会很长
  • 而且如果不分割, 修改业务代码后, 重新访问, 又全部得重新加载库代码
  • 两种分割方式: 配置 + 同步引入 与 异步引入(无需做任何配置)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function getComponent () {
// jsonp引入
// 动态的import, 实验性的语法
// npm i babel-plugin-dynamic-import-webpack -D
return import('lodash').then(({ default: _ }) => {
var element = document.createElement('div')
element.innerHTML = _.join(['a', 'b'], '*')
return element
})
}

getComponent().then(element => {
document.body.appendChild(element)
})

// .babelrc 动态引入
// npm i -D babel-plugin-dynamic-import-webpack
{
plugins: ['dynamic-import-webpack']
}

magic comment

1
2
3
4
5
6
7
8
9
10
11
12
13
// webpack官方提供的动态引入插件
// npm i -D @babel/plugin-syntax-dynamic-import
{
plugins: ['@babel/plugin-syntax-dynamic-import']
}

function getComponent () {
return import(/* webpackChunckName: 'lodash' */'lodash').then(({ default: _ }) => {
var element = document.createElement('div')
element.innerHTML = _.join(['a', 'b'], '*')
return element
})
}

splitPlugin 配置

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
optimization: {
// SplitChunksPlugin config
// 如下是官方默认配置
splitChunks: {
// async 只对异步代码生效
// all 对同步异步都做代码分割, 但是同步代码还需cacheGrops配置
// initial 只对同步代码做分割
chunks: 'async',
// 如果引入的模块大于minSize才做代码分割
minSize: 30000,
// 对于大于maxsize的模块尝试进行二次代码分割
maxSize: 0,
// 打包后的文件至少有多少个chunk文件引入这个模块才进行代码分割
minChunks: 1,
// 同时加载的模块数量,
// 在打包前5个库的时候会生成5个js文件,
// 超过5个就不再做代码分割
maxAsyncRequests: 5,
// 入口文件做代码分割的最大文件数量
maxInitialRequests: 3,
// 自动命名定界符
automaticNameDelimiter: '~',
// 让cacheGroups里的filename生效
name: true,
// 缓存组, 把库文件先放到缓存里, 再根据test规则分组合并打包
cacheGroups: {
// vendors: false
vendors: {
// 如果是node_modules里面的文件, 就打包到vendors组里
test: /[\\/]node_modules[\\/]/,
// 分组时的优先级
priority: -10
// // 组文件的名字 vendors.js, 不然会是 vendors~main.js
// filename: 'vendors.js'
},
// 被分割的代码的默认的配置, 没有test, 所有模块都符合要求
default: {
// 至少被引用了2次
minChunks: 2,
priority: -20,
// 复用已被分割打包过了的模块
reuseExistingChunk: true,
// // 组的文件名
// filename: 'common.js',
}
}
}
}

lazy loading

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
// 点击页面才会加载lodash代码
function getComponent () {
// 懒加载并不是webpack里面的一个概念, 而是ES的import语法,
// webpack能识别这种语法, 对import引入的模块做代码分割
return import(/* webpackChunckName: 'lodash' */'lodash').then(({ default: _ }) => {
var element = document.createElement('div')
element.innerHTML = _.join(['a', 'b'], '*')
return element
})
}

async function getComponent () {
const { default: _ } = await import(/* webpackChunckName: 'lodash' */'lodash')
// 懒加载并不是webpack里面的一个概念, 而是ES的import语法,
// webpack能识别这种语法, 对import引入的模块做代码分割
const element = document.createElement('div')
element.innerHTML = _.join(['a', 'b'], '*')
return element
}



window.document.addEventListener('click', () => {
getComponent().then(el => window.document.body.appendChild(el))
})

chunk

  • 每一个文件都是一个chunk

打包分析

1
2
# 生成stats.json文件
$ webpack --profile --json > stats.json

prefeching/preloading

实现第一次加载的时候就是最快的, webpack推荐交互的代码放到异步加载的模块里去写
prefeching/preloading可实现网页空闲时预先加载异步模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// console > Sources > command + shift + P > Show Coverage > 录屏
// 输入 coverage 代码利用率

// click.js
export default funciton handleClick () {
console.log('clicked')
}

// index.js
window.document.addEventListener('click', () => {
// prefetch会等待核心代码加载完成, 页面空闲时去加载prefetch的文件
// webpackPreload会和核心代码一起加载
import(/* webpackPrefetch: true */'./click.js').then({ default: func } => func())
})

css代码分割

1
2
3
4
5
6
7
8
output: {
publicPath: 'http://cdn.com.cn',
// name会对应入口文件entry里的 main 和 sub
filename: '[name].[hash].bundle.js',
path: path.resolve(__dirname, 'dist'),
// entry之外的, 被间接引入的模块走chunkFilename
chunkFilename: '[name].[chunkhash].chunk.js'
}
1
$ npm install --save-dev mini-css-extract-plugin
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// prodConfig
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader', // 分析css文件的引用关系
]
},
{
test: /\.scss$/,
// use 顺序, 从下到上, 从右到左
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
// 在scss文件里再@import scss文件时用后面两个loader
importLoaders: 2,
// 开启css的模块化打包, 不然样式都是全局的
// import style from './index.scss'
// img.classList.add(style.avatar)
modules: true
}
},
// https://webpack.js.org/loaders/sass-loader
// npm install sass-loader node-sass webpack --save-dev
'sass-loader',
// https://webpack.js.org/loaders/postcss-loader
'postcss-loader' // 自动添加厂商前缀等
]
}
]
}

optimization: {
// https://github.com/NMFR/optimize-css-assets-webpack-plugin
// npm install --save-dev optimize-css-assets-webpack-plugin
// 压缩与合并css代码
new OptimizeCSSAssetsPlugin({}),
splitChunks: {
cacheGroups: {
// 把所有入口的css打包
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'all',
// 强制代码拆分, 不管minSize之类的设置
enforce: true,
},
// https://webpack.js.org/plugins/mini-css-extract-plugin
// mini-css-extract-plugin也依赖splitChunksPlugin
// 将foo和bar的样式打包到不同的文件夹里面
fooStyles: {
name: 'foo',
test: (m, c, entry = 'foo') =>
m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
chunks: 'all',
enforce: true,
},
barStyles: {
name: 'bar',
test: (m, c, entry = 'bar') =>
m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
chunks: 'all',
enforce: true,
},
},
},
// 用于摇树
usedExports: true
}

plugins: [
new MiniCssExtractPlugin({
// filename 被html直接引用
filename: '[name].css',
// chunkFilename 被html间接引用
chunkFilename: '[name].chunk.css'
})
]

// package.json tree shaking时忽略css文件
{
"sideEffects": [
"*.css"
]
}

webpack与浏览器缓存

利用contenthash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// webpackConfig
// 关闭性能提示
performance: false

// 老版本, 避免每次打包, 即使没有更改内容, hash也不一样
// 业务逻辑与库代码之间的关联, 放在manifest里
// 默认manifest既存在main.js也存在vendors.js里
// 旧版webpack打包, 每次manifest会有差异
// 配置了runtimeChunk, 打包时, 会把manifest抽离到runtime.js里
optimization: {
rutimeChunk: {
name: 'runtime'
}
}

output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js',
}

shimming

垫片, 兼容, 实现webpack原始实现不了的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
plugins: [
new webpack.ProvidePlugin({
// 发现jQuery.ui.js模块里面用了$, 就会自动在这个模块里引入jQuery库
$: 'jquery',
// _join = lodash.join
_join: ['lodash', 'join']
})
]


use: [
{
loader: 'babel-loader'
},
// npm i -D imports-loader
// 让模块中的this指向window
{
loader: 'imports-loader?this=>window'
}
]

Library的打包

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
34
35
36
37
38
39
40
41
42
43
44
// 希望满足如下引入方式
import library from 'lib'
const lib = require('lib')
require(['lib'], function () {
})
<script src="lib.js"></script>
lib.math

output: {
// 打包后的代码挂载到lib这全局变量上
library: 'lib',
// u universal 通用
libraryTarget: 'umd'
}

output: {
// 打包后的代码挂载到lib这全局变量上
library: 'lib',
// lib挂载在this上或者window, global上
libraryTarget: 'this' // 或者 'window', 'global'
}

// 打包时忽略lodash库
externals: ['lodash']

externals: {
lodash: 'lodash'
}

externals: {
// 引入时为 const lodash = require('lodash')
lodash: {
commonjs: 'lodash'
}
}

// package.json
{
// 给别人使用时的入口
"main": "./dist/index.js",
}

// npm add user
// npm publish

打包PWA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// npm i -D workbox-webpack-plugin
const WorkboxPlugin = require('workbox-webpack-plugin')
plugins: [
// 利用service worker技术, 相当于一个另类的缓存
// 打包后会多出来, service-worker.js和precache-manifest.js文件
new WorkboxPlugin.GenerateSW({
clientsClaim: true,
skipWaiting: true
})
]

if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('service-worker registed')
})
.catch(err => {
console.log('service-worker register error')
})
})
}

TypeScript项目配置

TS提高项目可维护性

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
// npm i -D ts-loader typescript
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
}
]
}

// tsconfig.json
{
"compilerOptions": {
// 配了output, 写不写都行
"outDir": "./dist",
// 引入模块时用 import
"module": "es6",
// 打包最终转换的形式
"target": "es5",
// 允许引入JS文件
"allowJs": true,

}
}

// 使用lodash需 npm i -D @types/lodash
// 安装类型定义文件
// https://github.com/DefinitelyTyped/DefinitelyTyped
// https://microsoft.github.io/TypeSearch/

import * as _ from 'lodash'

proxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
axios.get('/react/api/header.json')

devSever: {
proxy: {
'/react/api': {
// 突破对origin的限制
changeOrigin: true,
// 转发https的网址
secure: 'false',
target: 'http://server.com',
pathRewrite: {
// 请求header.json时去请求demo.json
'header.json': 'demo.json'
}
}
}
}