webpack笔记

230 阅读7分钟

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

  1. 所以,npm scripts运行命令行,最终会通过webpack查找webpack-cli/webpack-command,并且执行CLI。
  2. webpack会分析不需要编译的命令,例如init、info等命令并不会实例化webpack对象, webpack不需要经过构建编译的过程。
  3. 分析命令行参数,对各个参数进行转换,组成编译配置项
  4. 引用webpack,根据配置项进行编译和构建
  5. 所以,webpack-cli执行的结果就是对配置文件和命令行参数进行转换,最终生成配置选项参数options。 然后根据配置参数实例化webpack对象,执行构建流程

webpack插件机制

  1. 如果按照了CLI就调用webpack-cli对参数分组转换为webpack可识别的编译配置项,最后再次 调用webpack执行compiler实例
  2. 通过webpack源码我们可以了解到,webpack函数会创建compiler实例。
  3. 而从Compiler.js源码可以看到,核心对象Compiler是对Tapable的继承和扩展。
  4. 通过Comiler源码了解到,Compiler通过Compilation创建来的实例,分析Compilation.js源码可以看到,Compilation 通用继承至Tapable。
  5. webpack的核心对象Compiler和Compilation都是继承至Tapable,实现Tapable和webpack的关联
  6. 而Tapable主要是暴露出钩子函数,为插件提供挂载的钩子,换言之,主要是控制钩子函数的发布与订阅, 控制着webpack的插件系统。
  7. 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的使用

  1. new Hook新建钩子由于Tapable暴露出来的是类方法,所有通过new新建钩子函数。其中,类方法接受数组参数options(非必传)。类方法根据传参,接受通用数量的参数。
    // 创建同步钩子
    const hook = new SyncHook(["arg1", "arg2", "arg3"])
  1. 钩子的发布与订阅,Tapable提供了同步和异步绑定钩子的方法,并且它们都有绑定事件和执行事件对应的方法
    异步 tapAsync	同步绑定:tapPromise
    tap执行:callAsync/promise	执行:call
  1. 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则提供了模块优化构建的流程。

  1. 准备阶段

    webpack准备阶段会将一些插件挂载到compiler实例上,同时进行EntryOptionPlugin的一些初始化。 接着进入Compiler,完成Compilation以及NormalModuleFactory、ContextModuleFactory工厂 函数的创建,最终进入compiler.run阶段。

  2. 构建阶段

    1. 进入Compiler,实例化Compiler时候会创建Compilation对象,NormalModuleFactory、ContextModuleFactory工厂方法, 执行run方法,触发beforeRun后则执行compile。 其中,与Compiler流程相关的钩子有:beforeRun/run,beforeCompile/AfterCompile,make,afterEmit/emit,done等, 而与监听有关的钩子有watchRun,watchClose
    2. Compilation就是负责模块的构建和构建优化的过程。 其中,Compilation相关的钩子有: 模块构建相关:buildModule,failModule,succeedModule 资源生成相关:moduleAsset,chunkAsset 优化相关的:optimize,afterOptimzeModules等。
  3. 文件生成

    最后Compilation完成seal和优化之后,会回到Compiler执行emitAssets,将内容输出到磁盘上。

  4. 可以简单的总结为,webpack的编译按照钩子调用顺序执行的流程: 初始化EntryOptions,进入Compiler.run开始编译,hooks.make会调用Compilation的addEntry钩子,

  5. 从entry开始递归的 分析依赖,对每个依赖模块进行build,对模块位置进行解析beforeResolve,然后开始构建模块buildModule,

  6. 通过normalModuleLoader 将loader加载完成的module进行编译,生成AST抽象语法树,接着遍历AST,对require等一些调用进行依赖收集,

  7. 最后将所有依赖构建 完成后,执行seal和优化,并回到Compiler.emit执行磁盘输出,完成编译流程。

loader编译原理

​ loader实际就是一个JavaScript方法,通过传入source源码,而输出也是返回新的源码

module.exports = function(source) {  return source}

简单用法

  1. 当一个 loader 在资源中使用,这个 loader 只能传入一个参数 - 这个参数是一个包含包含资源文件内容的字符串
  2. 同步 loader 可以简单的返回一个代表模块转化后的值。在更复杂的情况下,loader 也可以通过使用 this.callback(err, values...) 函数,返回任意数量的值。错误要么传递给这个 this.callback 函数,要么扔进同步 loader 中。
  3. loader 会返回一个或者两个值。第一个值的类型是 JavaScript 代码的字符串或者 buffer。第二个参数值是 SourceMap,它是个 JavaScript 对象。

复杂用法

​ 当链式调用多个 loader 的时候,请记住它们会以相反的顺序执行。取决于数组写法格式,从右向左或者从下向上执行。

  • 最后的 loader 最早调用,将会传入原始资源内容。
  • 第一个 loader 最后调用,期望值是传出 JavaScript 和 source map(可选)。
  • 中间的 loader 执行时,会传入前一个 loader 传出的结果。

所以,在接下来的例子,foo-loader 被传入原始资源,bar-loader 将接收 foo-loader 的产出,返回最终转化后的模块和一个 source map(可选)