前端工程化二-webpack原理
# webpack原理
# tree shaking原理
Tree-shaking的本质是消除无用的js代码。无用代码消除在广泛存在于传统的编程语言编译器中,编译器可以判断出某些代码根本不影响输出,然后消除这些代码,这个称之为DCE(dead code elimination)
Tree-shaking 是 DCE 的一种新的实现,Javascript同传统的编程语言不同的是,javascript绝大多数情况需要通过网络进行加载,然后执行,加载的文件大小越小,整体执行时间更短,所以去除无用代码以减少文件体积,对javascript来说更有意义。
Tree-shaking 和传统的 DCE的方法又不太一样,传统的DCE 消灭不可能执行的代码,而Tree-shaking 更关注宇消除没有用到的代码。
Dead Code 一般具有以下几个特征
•代码不会被执行,不可到达
•代码执行的结果不会被用到
•代码只会影响死变量(只写不读)
JavaScript代码的tree shaking不是rollup,webpack,cc做的,而是著名的代码压缩优化工具uglify,uglify完成了javascript的DCE。
模块必须采用ES6Module语法,因为treeShaking依赖ES6的静态语法:import 和 export。如果项目中使用了babel的话, @babel/preset-env
默认将模块转换成CommonJs语法,因此需要设置module:false
,webpack2后已经支持ESModule。
ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是tree-shaking的基础。
所谓静态分析就是不执行代码,从字面量上对代码进行分析,ES6之前的模块化,比如我们可以动态require一个模块,只有执行后才知道引用的什么模块,这个就不能通过静态分析去做优化。
原理总结:
- 只能作为模块顶层的语句出现
- import的模块名只能是字符串常量
- import binding 是 immutable的,引入的模块不能再进行修改
代码删除:
- uglify:判断程序流,判断变量是否被使用和引用,进而删除代码
tree shaking具体能做的事情:
1.Webpack Tree shaking从ES6顶层模块开始分析,可以清除未使用的模块
2.Webpack Tree shaking会对多层调用的模块进行重构,提取其中的代码,简化函数的调用结构
3.Webpack Tree shaking不会清除IIFE(立即调用函数表达式):因为IIFE比较特殊,它在被翻译时(JS并非编译型的语言)就会被执行,Webpack不做程序流分析,它不知道IIFE会做什么特别的事情,所以不会删除这部分代码
4.Webpack Tree shaking对于IIFE的返回函数,如果未使用会被清除
5.Webpack Tree shaking结合第三方包使用,引入第三方包时不同的方式会造成不同的优化
//全部引入,webpack不能清理
import _ from 'lodash'
import {last} from 'lodash'
//第三种打包体积减少
import last from 'lodash/last';
2
3
4
5
链接:https://juejin.cn/post/6844903544756109319side effects
是指那些当import
的时候会执行一些动作,但是不一定会有任何export
。比如ployfill
,ployfills
不对外暴露方法给主程序使用。
tree shaking
不能自动的识别哪些代码属于side effects
,因此手动指定这些代码显得非常重要,如果不指定可能会出现一些意想不到的问题。
在webapck中
,是通过package.json
的sideEffects
属性来实现的。
{
"name": "tree-shaking",
"sideEffects": false
}
2
3
4
如果所有代码都不包含副作用,我们就可以简单地将该属性标记为false
,来告知 webpack
,它可以安全地删除未用到的export
导出。
如果你的代码确实有一些副作用,那么可以改为提供一个数组:
{
"name": "tree-shaking",
"sideEffects": [
"./src/common/polyfill.js"
]
}
2
3
4
5
6
# HMR原理
Hot Module Replace是webpack一个重要的特性,当代码文件修改并保存之后,webapck通过watch监听到文件发生变化,会对代码文件重新打包生成两个模块补丁文件manifest(js)和一个(或多个)updated chunk(js),将结果存储在内存文件系统中,通过websocket通信机制将重新打包的模块发送到浏览器端,浏览器动态的获取新的模块补丁替换旧的模块,浏览器不需要刷新页面就可以实现应用的更新。
HMR的工作原理
1.webpack --watch启动监听模式之后,webpack第一次编译项目,并将结果存储在内存文件系统,相比较磁盘文件读写方式内存文件管理速度更快,内存webpack服务器通知浏览器加载资源,浏览器获取的静态资源除了JS code内容之外,还有一部分通过webpack-dev-server注入的的 HMR runtime代码,作为浏览器和webpack服务器通信的客户端( webpack-hot-middleware提供类似的功能)。
2.文件系统中一个文件(或者模块)发生变化,webpack监听到文件变化对文件重新编译打包,每次编译生成唯一的hash值,根据变化的内容生成两个补丁文件:说明变化内容的manifest(文件格式是hash.hot-update.json,包含了hash和chundId用来说明变化的内容)和chunk js(hash.hot-update.js)模块。
3.hrm-server通过websocket将manifest推送给浏览器
浏览器接受到最新的hotCurrentHash,触发 hotDownloadManifest 函数,获取manifest json 文件。
4.浏览器端hmr runtime根据manifest的hash和chunkId使用ajax拉取最新的更新模块chunk
5.触发render流程实现局部热重载
HMR runtime 调用window["webpackHotUpdate"] 方法,调用hotAddUpdateChunk
HMR相关的中间件
- webpack-dev-middleware
本质上是一个容器,将webpack处理后的文件传递个服务器。
webpack-dev-middleware 是一个 express 中间件,核心实现两个功能:第一通过file-loader内部集成了node的 monery-fs/memfs 内部文件系统,,直接将资源存储在内存;第二是通过watch监听文件的变化,动态编译。
2.webpack-hot-middleware
核心是给webpack提高服务端和客户端之间的通信机制,内部使用windoe.EventSocurce实现。
在webpack第一次打包的时候,除了代码本身之外,还包含一部分HMRruntime订阅服务代码,HMRruntime 订阅服务端的更新变化,触发HMR runtime API拉取最新的资源模块。
webpack-hot-middleware实现页面的热重载。
3. webpack-dev-server
内置了webpack-dev-middleware和express服务器,利用webpack-dev-middleware提供文件的监听和编译,利用express提供http服务,底层利用websocket代替EventSource实现了webpack-hot-middleware提供的客户端和服务器之间的通信机制。
# webpack生命周期/构建原理
从启动构建到输出结果一系列过程:
(1)初始化参数:解析webpack配置参数,合并shell传入和webpack.config.js文件配置的参数,形成最后的配置结果。
(2)开始编译:上一步得到的参数初始化compiler对象,注册所有配置的插件,插件监听webpack构建生命周期的事件节点,做出相应的反应,执行对象的 run 方法开始执行编译。
(3)确定入口:从配置的entry入口,开始解析文件构建AST语法树,找出依赖,递归下去。
(4)编译模块:递归中根据文件类型和loader配置,调用所有配置的loader对文件进行转换,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
(5)完成模块编译并输出:递归完事后,得到每个文件结果,包含每个模块以及他们之间的依赖关系,根据entry配置生成代码块chunk。
(6)输出完成:输出所有的chunk到文件系统。
注意:在构建生命周期中有一系列插件在做合适的时机做合适事情,比如UglifyPlugin会在loader转换递归完对结果使用UglifyJs压缩覆盖之前的结果。
# webpack与gulp、grunt区别
webpack 是 module bundle
gulp 是 tast runner
Rollup 是在 Webpack 流行后出现的替代品。Rollup 在用于打包 JavaScript 库时比 Webpack 更加有优势,因为其打包出来的代码更小更快。 但功能不够完善,很多场景都找不到现成的解决方案。
# 分离/合并配置
安装webpack-merge
npm i webpack-merge -D
在webpackjs中引入别的webpack配置
const {merge} = require('webpack-merge');
const commonConfig = require('./webpack.common');
const prodConfig = {
mode:'production',
//可以找出报错代码的源代码路径
//cheap 只报告行号(不加会报告列) module 会报告loader及第三方依赖的错误位置
//加了inline就不产生映射文件了
//生产环境不需要eval
devtool:'cheap-module-source-map',
plugins: [
]
}
module.exports = merge(commonConfig,prodConfig);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# webpack-chain
使用链式的方式调用webpack的api
安装
npm install --save-dev webpack-chain
使用
const Config = require('webpack-chain');
const config = new Config();
config
// Interact with entry points
.entry('index')
.add('src/index.js')
.end()
// Modify output settings
.output
.path('dist')
.filename('[name].bundle.js');
// Create named rules which can be modified later
config.module
.rule('lint')
.test(/\.js$/)
.pre()
.include
.add('src')
.end()
// Even create named uses (loaders)
.use('eslint')
.loader('eslint-loader')
.options({
rules: {
semi: 'off'
}
});
config.module
.rule('compile')
.test(/\.js$/)
.include
.add('src')
.add('test')
.end()
.use('babel')
.loader('babel-loader')
.options({
presets: [
['@babel/preset-env', { modules: false }]
]
});
// Create named plugins too!
config
.plugin('clean')
.use(CleanPlugin, [['dist'], { root: '/dir' }]);
// Export the completed configuration object to be consumed by webpack
module.exports = config.toConfig();
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
# Webpack插件汇总
# 功能类
html-webpack-plugin
自动生成html,基本用法:
new HtmlWebpackPlugin({
filename: 'index.html', // 生成文件名
template: path.join(process.cwd(), './index.html') // 模班文件
})
2
3
4
copy-webpack-plugin
拷贝资源插件,copy-webpack-plugin不是为了复制构建过程中生成的文件,相反,它是复制源树中已经存在的文件,作为构建过程的一部分
基本用法:
new CopyWebpackPlugin([
{
from: path.join(process.cwd(), './vendor/'),
to: path.join(process.cwd(), './dist/'),
ignore: ['*.json']
}
])
2
3
4
5
6
7
webpack-manifest-plugin && assets-webpack-plugin
俩个插件效果一致,都是生成编译结果的资源单,只是资源单的数据结构不一致而已。
webpack-manifest-plugin 基本用法:
module.exports = {
plugins: [
new ManifestPlugin()
]
}
2
3
4
5
assets-webpack-plugin 基本用法:
module.exports = {
plugins: [
new AssetsPlugin()
]
}
2
3
4
5
clean-webpack-plugin
在编译之前清理指定目录指定内容,基本用法
// 清理目录
const pathsToClean = [
'dist',
'build'
]
// 清理参数
const cleanOptions = {
exclude: ['shared.js'], // 跳过文件
}
module.exports = {
// ...
plugins: [
new CleanWebpackPlugin(pathsToClean, cleanOptions)
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
compression-webpack-plugin
提供带 Content-Encoding 编码的压缩版的资源,基本用法
module.exports = {
plugins: [
new CompressionPlugin()
]
}
2
3
4
5
progress-bar-webpack-plugin
编译进度条插件,基本用法
module.exports = {
//...
plugins: [
new ProgressBarPlugin()
]
}
2
3
4
5
6
speed-measure-webpack-plugin
进行构建速度分析,可以看到各个 loader、plugin 的构建时长,后续可针对耗时 loader、plugin 进行优化。
npm i -D speed-measure-webpack-plugin
使用
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
// ...webpack config...
})
2
3
4
5
# 代码相关类
webpack.ProvidePlugin
自动加载模块,如 出现,就会自动加载模块;出现,就会自动加载模块; 默认为'jquery'的exports,用法:
new webpack.ProvidePlugin({
$: 'jquery',
})
2
3
webpack.DefinePlugin
定义全局常量,用法:
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV)
}
})
2
3
4
5
mini-css-extract-plugin && extract-text-webpack-plugin
提取css样式,对比:
- mini-css-extract-plugin 为webpack4及以上提供的plugin,支持css chunk
- extract-text-webpack-plugin 只能在webpack3 及一下的版本使用,不支持css chunk
extract-text-webpack-plugin基本用法 :
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
}
]
},
plugins: [
new ExtractTextPlugin("styles.css"),
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mini-css-extract-plugin 基本用法:
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '/' // chunk publicPath
}
},
"css-loader"
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css", // 主文件名
chunkFilename: "[id].css" // chunk文件名
})
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 编译结果优化类
wbepack.IgnorePlugin
忽略regExp匹配的模块,用法
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
uglifyjs-webpack-plugin
代码丑化,用于js压缩,
module.exports = {
//...
optimization: {
minimizer: [new UglifyJsPlugin({
cache: true, // 开启缓存
parallel: true, // 开启多线程编译
sourceMap: true, // 是否sourceMap
uglifyOptions: { // 丑化参数
comments: false,
warnings: false,
compress: {
unused: true,
dead_code: true,
collapse_vars: true,
reduce_vars: true
},
output: {
comments: false
}
}
}]
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
optimize-css-assets-webpack-plugin
css压缩,主要使用 cssnano (opens new window) 压缩器,用法
module.exports = {
//...
optimization: {
minimizer: [new OptimizeCssAssetsPlugin({
cssProcessor: require('cssnano'), // css 压缩优化器
cssProcessorOptions: { discardComments: { removeAll: true } } // 去除所有注释
})]
}
};
2
3
4
5
6
7
8
9
webpack-md5-hash
使你的chunk根据内容生成md5,用这个md5取代 webpack chunkhash,基本用法
var WebpackMd5Hash = require('webpack-md5-hash');
module.exports = {
// ...
output: {
//...
chunkFilename: "[chunkhash].[id].chunk.js"
},
plugins: [
new WebpackMd5Hash()
]
};
2
3
4
5
6
7
8
9
10
11
12
SplitChunksPlugin
CommonChunkPlugin 的后世,用于chunk切割。
webpack 把 chunk 分为两种类型,一种是初始加载initial chunk,另外一种是异步加载 async chunk,如果不配置SplitChunksPlugin,webpack会在production的模式下自动开启,默认情况下,webpack会将 node_modules 下的所有模块定义为异步加载模块,并分析你的 entry、动态加载(import()、require.ensure)模块,找出这些模块之间共用的node_modules下的模块,并将这些模块提取到单独的chunk中,在需要的时候异步加载到页面当中,其中默认配置如下:
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async', // 异步加载chunk
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~', // 文件名中chunk分隔符
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/, //
priority: -10
},
default: {
minChunks: 2, // 最小的共享chunk数
priority: -20,
reuseExistingChunk: true
}
}
}
}
};
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
# 编译优化类
DllPlugin && DllReferencePlugin && autodll-webpack-plugin
dllPlugin 将模块预先编译,DllReferencePlugin 将预先编译好的模块关联到当前编译中,当 webpack 解析到这些模块时,会直接使用预先编译好的模块。
autodll-webpack-plugin 相当于 dllPlugin 和 DllReferencePlugin 的简化版,其实本质也是使用 dllPlugin && DllReferencePlugin,它会在第一次编译的时候将配置好的需要预先编译的模块编译在缓存中,第二次编译的时候,解析到这些模块就直接使用缓存,而不是去编译这些模块。
dllPlugin 基本用法:
const output = {
filename: '[name].js',
library: '[name]_library',
path: './vendor/'
}
module.exports = {
entry: {
vendor: ['react', 'react-dom'] // 我们需要事先编译的模块,用entry表示
},
output: output,
plugins: [
new webpack.DllPlugin({ // 使用dllPlugin
path: path.join(output.path, `${output.filename}.json`),
name: output.library // 全局变量名, 也就是 window 下 的 [output.library]
})
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DllReferencePlugin 基本用法:
const manifest = path.resolve(process.cwd(), 'vendor', 'vendor.js.json')
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
manifest: require(manifest), // 引进dllPlugin编译的json文件
name: 'vendor_library' // 全局变量名,与dllPlugin声明的一致
}
]
}
2
3
4
5
6
7
8
9
10
autodll-webpack-plugin 基本用法:
module.exports = {
plugins: [
new AutoDllPlugin({
inject: true, // 与 html-webpack-plugin 结合使用,注入html中
filename: '[name].js',
entry: {
vendor: [
'react',
'react-dom'
]
}
})
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
happypack && thread-loader
多线程编译,加快编译速度,thread-loader不可以和 mini-css-extract-plugin 结合使用。
happypack 基本用法:
const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
const happyLoaderId = 'happypack-for-react-babel-loader';
module.exports = {
module: {
rules: [{
test: /\.jsx?$/,
loader: 'happypack/loader',
query: {
id: happyLoaderId
},
include: [path.resolve(process.cwd(), 'src')]
}]
},
plugins: [new HappyPack({
id: happyLoaderId,
threadPool: happyThreadPool,
loaders: ['babel-loader']
})]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
thread-loader 基本用法:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve("src"),
use: [
"thread-loader",
// your expensive loader (e.g babel-loader)
"babel-loader"
]
}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
hard-source-webpack-plugin && cache-loader
使用模块编译缓存,加快编译速度。
hard-source-webpack-plugin ,基本用法:
module.exports = {
plugins: [
new HardSourceWebpackPlugin()
]
}
2
3
4
5
cache-loader 基本用法:
module.exports = {
module: {
rules: [
{
test: /\.ext$/,
use: [
'cache-loader',
...loaders
],
include: path.resolve('src')
}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 编译分析类
webpack-bundle-analyzer
编译模块分析插件,基本用法
new BundleAnalyzerPlugin({
analyzerMode: 'server',
analyzerHost: '127.0.0.1',
analyzerPort: 8889,
reportFilename: 'report.html',
defaultSizes: 'parsed',
generateStatsFile: false,
statsFilename: 'stats.json',
statsOptions: null,
logLevel: 'info'
}),
2
3
4
5
6
7
8
9
10
11
stats-webpack-plugin && PrefetchPlugin
stats-webpack-plugin 将构建的统计信息写入文件,该文件可在 http://webpack.github.io/analyse中上传进行编译分析,并根据分析结果,可使用 PrefetchPlugin 对部分模块进行预解析编译
stats-webpack-plugin 基本用法:
module.exports = {
plugins: [
new StatsPlugin('stats.json', {
chunkModules: true,
exclude: [/node_modules[\\\/]react/]
})
]
};
2
3
4
5
6
7
8
PrefetchPlugin 基本用法:
module.exports = {
plugins: [
new webpack.PrefetchPlugin('/web/', 'app/modules/HeaderNav.jsx'),
new webpack.PrefetchPlugin('/web/', 'app/pages/FrontPage.jsx')
];
}
2
3
4
5
6
speed-measure-webpack-plugin
统计编译过程中,各loader和plugin使用的时间,基本用法
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const webpackConfig = {
plugins: [
new MyPlugin(),
new MyOtherPlugin()
]
}
module.exports = smp.wrap(webpackConfig);
2
3
4
5
6
7
8
9
10
11
# react相关
react-refresh-webpack-plugin
热更新 react 组件。
npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh
使用
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = {
plugins: [
new webpack.HotModuleReplacementPlugin(),
new ReactRefreshWebpackPlugin(),
]
}
2
3
4
5
6
7
8
# 上传oss
如果希望webpack可以自动上传静态资源(如js,css等)到阿里云oss上,可以使用webpack-aliyun-oss这款插件
安装
npm i webpack-aliyun-oss -D
配置
const WebpackAliyunOss = require('webpack-aliyun-oss');
const webpackConfig = {
// ... 省略其他
plugins: [new WebpackAliyunOss({
from: ['./build/**', '!./build/**/*.html'],//排除html文件
dist: 'path/in/alioss',
region: 'your region',
accessKeyId: 'your key',
accessKeySecret: 'your secret',
bucket: 'your bucket',
// 如果希望重新组织上传路径,可以传这个函数
// 否则按构建目录的结构上传
setOssPath(filePath) {
// filePath为当前文件路径,函数应该返回路径+文件名,如/new/path/to/file.js,则最终上传路径为 path/in/alioss/new/path/to/file.js
return '/new/path/to/file.js';
},
// 如果想定义header就传
setHeaders(filePath) {
// 定义当前文件header,可选
return {
'Cache-Control': 'max-age=31536000'
}
}
})]
}
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
from
1: 上传哪些文件,支持类似gulp.src的glob方法,如'./build/**', 可以为glob字符串或者数组。
- 作为插件使用时:可选,默认为output.path下所有的文件。
- 独立使用时:必须,否则不知道从哪里取图片:)
dist
: 上传到oss哪个目录下,默认为oss根目录。可作为路径前缀使用。region
: 阿里云上传区域accessKeyId
: 阿里云的授权accessKeyIdaccessKeySecret
: 阿里云的授权accessKeySecretbucket
: 上传到哪个buckettimeout
: oss超时设置,默认为30秒(30000)verbose
: 是否显示上传日志,默认为truedeletOrigin
: 上传完成是否删除原文件,默认falsedeleteEmptyDir
: 如果某个目录下的文件都上传到cdn了,是否删除此目录。deleteOrigin为true时候生效。默认false。setOssPath
: 自定义上传路径的函数。接收参数为当前文件路径。不传,或者所传函数返回false则按默认路径上传。(默认为output.path下文件路径)setHeaders
: 配置headers的函数。接收参数为当前文件路径。不传,或者所传函数返回false则不设置header。test
: 测试,仅显示要上传的文件,但是不执行上传操作。默认false
# 利用source-map还原代码
webpack打包时如果有配置sourcemap,可能在生成js时生成对应的map文件,而利用这个文件甚至可以还原js和组件
reverse-sourcemap
npm install --global reverse-sourcemap
找到webpack生成的js对应的map文件,一般为js.map文件,进行还原
# webpack学习资源
wepack:https://www.kancloud.cn/sllyli/webpack/1242354
深入浅出webpack:https://webpack.wuhaolin.cn/