Tree Shaking
tree shaking就是利用ES6的这一特点,本质就是对静态的模块代码进行分析,所以需要 在构建过程中确定哪些模块的代码能利用到,哪些模块不需要进行区分。通过标识不需要 使用的代码在uglify阶段删除无用代码。
tree shaking就是通过在构建过程,如果一个模块存在多个方法,如果只有其中的某个方法 使用到,则将一些没有引用到的代码在这个打包过程移除,只把用到的方法打入bundle中。
通过开启mode:'production'即可。其中,只支持ES6的语法,commonjs的方式(即require方式)不支持使用。
speed-measure-webpack-plugin
通过speed-measure-webpack-plugin插件进行分析,可以很方便的分析出每个 loader和plugin执行时的耗时,其能有效的分析了整个打包的总耗时及插件和loader 耗时情况,能有效的提供有效信息。
npm i speed-measure-webpack-plugin -D
//webpack.prod.js
const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasureWebpackPlugin()
modue.exports = smp.wrap({...})
webpack-bundle-analyzer
通过webpack-bundle-analyzer
将打包后项目进行可视化的分析,其中可分析依赖 的第三方模块文件大小和业务组件代码大小,进而对大文件分析是否可进行组件 替换或者CDN方式抽取。
npm i webpack-bundle-anaylzer -D
//webpack.prod.js
const WebpackBundleAnalyzer = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
plugins: [
new WebpackBundleAnalyzer()
]
}
HappyPack解析资源
HappyPack解析原理即每次webpack解析一个模块,HappyPack会将它及它的依赖分配给worker线程。
其实质是HappyPack会在webpack执行compiler.run后,进行初始化并创建HappyThreadPool线程池, 线程池则会将构建模块的任务进行分配线程,所以一个线程池中会运行多个线程,并对模块或者依赖进行处理, 处理完成后会将资源传说给HappyPack主进程,完成构建。
npm i happypack -D
//webpack.prod.js
const Happypack = require('happypack')
module.exports ={
module: {
rules: [
{
test: /.js$/,
use: [
'happypack/loader'
/*'babel-loader'*/
]
},
]
},
plugins: [
new Happypack({
loaders: ['babel-loader']
})
]
}
thread-loader解析资源(推荐使用)
thread-loader是webpack4官方提供的,其原理也是通过每次webpack解析一个模块,thread-loader 将它及它的依赖分配给worker线程实现多线程构建。
npm i thread-loader -D
// webpack.prod.js
module.exports = {
module:{
rules:[
{
test: /.js$/,
exclude: /node_modules/,
use: [
{
loader: 'thread-loader',
options: {
workers:3
}
},
'babel-loader'
]
},
]
}
}
分包
设置Externals
通过CDN方式引入,不打入到bundle中,实现基础库的分离.但是,这样也会由于一个基础库指向一个CDN,则需要配置多个scripts引入导致多次的请求。而如果借助 之前说的splitChunks实现分包也会使构建项目时进行基础包的分析,加大构建时长
html-webpack-externals-plugin
npm i html-webpack-externals-plugin -D
//webpack.prod.js
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin')
module.exports = {
plugins: [
new HtmlWebpackExternalsPlugin({
externals:[
{
module:'react',
entry: 'https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/cjs/react.production.min.js',
global: 'react',
},
{
module:'react-dom',
entry: 'https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/cjs/react-dom.production.min.js',
global: 'ReactDom',
}
]
})
]
}
预编译资源模块:DLLPlugin + DllReferencePlugin推荐)
其中DLLPlugin通过将 多个基础包或者业务基础包提取,并生成一个包文件和manifest.json(对分离包的一个描述),然后 在实际应用中可以借助DllReferencePlugin对manifest.json引用即可实现分离包的关联。
新建webpack.dll.js并配置scripts命令,使用DLLPlugin进行分包
//package.json{
"scripts":{
"dll": "webpack --config webpack.dll.js"
}
}
//webpack.dll.js
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
library: [
// 分离基础包,如果需要分离业务基础包可以配置多个
'react',
'react-dom'
]
},
output: {
filename: '[name]_[chunkhash:8].dll.js',
path: path.join(__dirname, './build/library'),
//避免build时候清理dist目录
library: '[name]'
},
plugins: [
new webpack.DllPlugin({
//提供manifest引用
name: '[name]_[hash:8]',
path: path.join(__dirname, './build/library/[name].json')
})
]}
使用DllReferencePlugin引用manifest.json
//webpack.prod.js
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
manifest: require('./build/library/library.json')
})
]}
缓存
开启构建项目时候的缓存可以提升二次构建的速度。其中,可以通过babel-loader开启缓存 则下次进行babel转换JS、JSX语法时直接读取缓存的内容;在代码压缩阶段可以使用UglifyJsPlugin 或TerserWebpackPlugin开启缓存;通过cache-loader或者hard-source-webpack-plugin 可以提升模块转换阶段的缓存。
对于的缓存内容可以在node_modules下的.cache目录。
//webpack.prod.js
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
module.exports = {
module: {
rules:[
{
test: /.js$/,
use:[
{
loader: 'thread-loader',
options: {
workers:3
}
},
'babel-loader?cacheDirectory=true'
//开启babel缓存
]
}
]
},
plugins: [
new HardSourceWebpackPlugin()
],
optimization:{
minimizer: [
new TerserPlugin({
parallel: true,
cache: true
// 开启缓存
})
]
}
}
缩小构建目标
-
少构建模块:对于一些第三方的模块,我们可以不需要进行进一步的解析,比如babel-loader可以不用解析node_modules 的一些例如UI库等一些第三方包,因为其质量也是有了保证的
module.exports = { module:{ rules:[ test: /.js$/, loader: 'happypack/loader', exclude: 'node_modules' //include:path.resolve('src') ] } } -
减少文件搜索范围:优化resolve.modules配置,较少模块搜索层级;优化resolve.mainFields配置,优化入口配置; 优化resolve.extensions配置,优化查找文件对于的后缀;合理使用alias等。
//webpack.prod.js module.exports ={ resolve: { alias: { 'react': path.resolve(__dirname,'./node_modules/react/umd/react.production.min.js'), 'react-dom': path.resolve(__dirname,'./node_modules/react/umd/react-dom.production.min.js') }, extensions: ['.js'], mainFields: ['main'] } }
构建体积优化
tree shaking 摇树优化
删除无效的CSS
- PurifyCSS:遍历代码,识别已经用到的CSS class,通过对用到和没有使用到的class标记。 目前purifycss-webpack已停止更新,需替换为在webpack4中目前需要通过 purgecss-webpack-plugin结合mini-css-extract-plugin配合使用。
npm i purgecss-webpack-plugin -D
//webpack.prod.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const PurgecssPlugin = require('purgecss-webpack-plugin')
const PATHS = {
src: path.join(__dirname,'src')
}
module.exports ={
module:{
rules: [
{
test: /.css$/,
use: [ MiniCssExtractPlugin.loader, 'css-loader' ]
},
]
},
plugins: [
new MiniCssExtractPlugin({ filename:} '[name]_[contenthash:8].css'}),
new PurgecssPlugin({
path: glob.sync(`${PATHS.src}/**/*`, {nodir:true})
// 绝对路径
}),
]
}
- uncss:其中要求HTML需要通过jsdom加载并且所有的样式通过PostCSS解析,这样才能通过 document.querySelector来识别在html文件里面不存在的选择器。
图片压缩
image-webpack-loader
基于Node的imagemin,可以通过配置image-webpack-loader来实现,其中,Imagemin可提供定制选项,例如可以 引入更多第三方优化插件,pngquant等,并且也支持多种图片格式压缩
npm i image-webpack-loader -D
module.exports = {
module: {
rules: [{
test: /.(png|jpg|gif|jpeg)$/,
use: [{
loader: 'file-loader',
options: {
name: '[name]_[hash:8].[ext]'
}
},
{
loader: 'image-webpack-loader',
options: {
mozjpeg: {
progressive: true,
quality: 65
},
// optipng.enabled: false will disable optipng
optipng: {
enabled: false,
},
pngquant: {
quality: [0.65, 0.90],
speed: 4
},
gifsicle: {
interlaced: false,
},
// the webp option will enable WEBP
webp: {
quality: 75
}
}
},
]
}, ]
}
}
设置动态Polyfill
对于一些es6的语法,不同浏览器可能需要不同的兼容处理,而polyfill就为我们提供了 兼容的方法,但是,完整的polyfill需要处理的兼容语法比较多,这就会导致构建的 体积比较大。
运行webpack发生了什么?
通过npm scripts运行webpack时候,npm其实是通过让命令行工具进入node_modules/.bin目录 查找是否存在webpack.cmd或者webpack.sh文件,如果存在就执行,不存在则抛出错误。node_modules/bin目录下有这个命令,则如果局部安装依赖,则需要在依赖配置package.json中指定bin字段, 所以实际查找的入口文件就是 node_modules/webpack/bin/webpack.js
- 所以,npm scripts运行命令行,最终会通过webpack查找webpack-cli/webpack-command,并且执行CLI。
- webpack会分析不需要编译的命令,例如init、info等命令并不会实例化webpack对象, webpack不需要经过构建编译的过程。
- 分析命令行参数,对各个参数进行转换,组成编译配置项
- 引用webpack,根据配置项进行编译和构建
- 所以,webpack-cli执行的结果就是对配置文件和命令行参数进行转换,最终生成配置选项参数options。 然后根据配置参数实例化webpack对象,执行构建流程
webpack插件机制
- 如果按照了CLI就调用webpack-cli对参数分组转换为webpack可识别的编译配置项,最后再次 调用webpack执行compiler实例
- 通过webpack源码我们可以了解到,webpack函数会创建compiler实例。
- 而从Compiler.js源码可以看到,核心对象Compiler是对Tapable的继承和扩展。
- 通过Comiler源码了解到,Compiler通过Compilation创建来的实例,分析Compilation.js源码可以看到,Compilation 通用继承至Tapable。
- webpack的核心对象Compiler和Compilation都是继承至Tapable,实现Tapable和webpack的关联
- 而Tapable主要是暴露出钩子函数,为插件提供挂载的钩子,换言之,主要是控制钩子函数的发布与订阅, 控制着webpack的插件系统。
- webpack可以理解为一种基于事件流的编程范例,一系列的插件运行流程。通过监听插件上定义的 compiler和compilation的关键节点实行相应的事件操作。
Tapable
Tapable是一个发布订阅的库,主要功能是提供钩子函数的发布与订阅, 也是webpack插件系统的实现。
const {
SyncHook, //同步钩子
SyncBailHook, //同步熔断钩子
SyncWaterfallHook, //同步流水钩子
SyncLoopHook, //同步循环钩子
AsyncParallelHook, //异步并发钩子
AsyncParallelBailHook, //异步并发熔断钩子
AsyncSeriesHook, //异步串行钩子
AsyncSeriesBailHook, //异步串行熔断钩子
AsyncSeriesWaterfallHook //异步串行流水钩子
} = require("tapable");
| 类型 | 方法 |
|---|---|
| Hook | 所有钩子的后缀 |
| Waterfall | 同步方法,但是会传值给下一个函数 |
| Bail | 熔断:当函数有任何返回值,就会在当前执行函数停止 |
| Loop | 监听函数返回true表示继续循环,返回undefine表示结束循环 |
| Sync | 同步方法 |
| AsyncSeries | 异步串行钩子 |
| AsyncParallel | 异步并行执行钩子 |
Tapable的使用
- new Hook新建钩子由于Tapable暴露出来的是类方法,所有通过new新建钩子函数。其中,类方法接受数组参数options(非必传)。类方法根据传参,接受通用数量的参数。
// 创建同步钩子
const hook = new SyncHook(["arg1", "arg2", "arg3"])
- 钩子的发布与订阅,Tapable提供了同步和异步绑定钩子的方法,并且它们都有绑定事件和执行事件对应的方法
异步 tapAsync 同步绑定:tapPromise
tap执行:callAsync/promise 执行:call
-
hook基本用法:
// 创建同步钩子 const hook1 = new SyncHook(["arg1", "arg2", "arg3"]) // 绑定事件到事件流 hook1.tap('hook1',(arg1,arg2,arg3)=>console.log(arg1,arg2,arg3)) //1,2,3 // 执行绑定的事件hook1.call(1,2,3)创建Car类,并定义同步钩子、异步钩子并在钩子上绑定和执行方法。
const {SyncHook,AsyncSeriesHook} = require('tapable') class Car { constructor(){ this.hooks = { accelerate: new SyncHook(['newspeed']), brake: new SyncHook(), calculateRoutes: new AsyncSeriesHook(["source", "target", "routesList"]) } } } const myCar = new Car() // 绑定同步钩子 myCar.hooks.brake.tap("WarningLampPlugin", () => console.log('WarningLampPlugin')) // 绑定同步钩子并传参 myCar.hooks.accelerate.tap('LoggerPlugin', newSpeed => console.log('Accelerating to'+newSpeed)) //绑定一个异步Promise钩子 myCar.hooks.calculateRoutes.tapPromise('calculateRoutes tapPromise',(source, target, routesList)=>{ console.log('source',source) return new Promise((resolve,reject)=>{ setTimeout(()=>{ console.log(`tapPromise to ${source} ${target} ${routesList}`) resolve() },1000) } ) }) //执行同步钩子 myCar.hooks.brake.call()myCar.hooks.accelerate.call(10) //执行异步钩子 myCar.hooks.calculateRoutes.promise('Async','hook','demo').then(()=>{ console.log('succ')},err=>{ console.err(err)})
webpack编译流程分析
webpack的编译阶段可以分为准备阶段,将options转换为webpack 可识别的options配置项;构建阶段,其中,Compiler提供了构建各阶段的钩子函数,代码优化阶段, 而Compilation则提供了模块优化构建的流程。
-
准备阶段
webpack准备阶段会将一些插件挂载到compiler实例上,同时进行EntryOptionPlugin的一些初始化。 接着进入Compiler,完成Compilation以及NormalModuleFactory、ContextModuleFactory工厂 函数的创建,最终进入compiler.run阶段。
-
构建阶段
- 进入Compiler,实例化Compiler时候会创建Compilation对象,NormalModuleFactory、ContextModuleFactory工厂方法, 执行run方法,触发beforeRun后则执行compile。 其中,与Compiler流程相关的钩子有:beforeRun/run,beforeCompile/AfterCompile,make,afterEmit/emit,done等, 而与监听有关的钩子有watchRun,watchClose
- Compilation就是负责模块的构建和构建优化的过程。 其中,Compilation相关的钩子有: 模块构建相关:buildModule,failModule,succeedModule 资源生成相关:moduleAsset,chunkAsset 优化相关的:optimize,afterOptimzeModules等。
-
文件生成
最后Compilation完成seal和优化之后,会回到Compiler执行emitAssets,将内容输出到磁盘上。
-
可以简单的总结为,webpack的编译按照钩子调用顺序执行的流程: 初始化EntryOptions,进入Compiler.run开始编译,hooks.make会调用Compilation的addEntry钩子,
-
从entry开始递归的 分析依赖,对每个依赖模块进行build,对模块位置进行解析beforeResolve,然后开始构建模块buildModule,
-
通过normalModuleLoader 将loader加载完成的module进行编译,生成AST抽象语法树,接着遍历AST,对require等一些调用进行依赖收集,
-
最后将所有依赖构建 完成后,执行seal和优化,并回到Compiler.emit执行磁盘输出,完成编译流程。
loader编译原理
loader实际就是一个JavaScript方法,通过传入source源码,而输出也是返回新的源码
module.exports = function(source) { return source}
简单用法
- 当一个 loader 在资源中使用,这个 loader 只能传入一个参数 - 这个参数是一个包含包含资源文件内容的字符串
- 同步 loader 可以简单的返回一个代表模块转化后的值。在更复杂的情况下,loader 也可以通过使用
this.callback(err, values...)函数,返回任意数量的值。错误要么传递给这个this.callback函数,要么扔进同步 loader 中。 - loader 会返回一个或者两个值。第一个值的类型是 JavaScript 代码的字符串或者 buffer。第二个参数值是 SourceMap,它是个 JavaScript 对象。
复杂用法
当链式调用多个 loader 的时候,请记住它们会以相反的顺序执行。取决于数组写法格式,从右向左或者从下向上执行。
- 最后的 loader 最早调用,将会传入原始资源内容。
- 第一个 loader 最后调用,期望值是传出 JavaScript 和 source map(可选)。
- 中间的 loader 执行时,会传入前一个 loader 传出的结果。
所以,在接下来的例子,foo-loader 被传入原始资源,bar-loader 将接收 foo-loader 的产出,返回最终转化后的模块和一个 source map(可选)