webpack整体介绍

136 阅读7分钟

为什么选择webpack

社区⽣生态丰富 配置灵活和插件化扩展 官⽅方更更新迭代速度快

常用配置

entry

依赖图的⼊⼝

对于⾮非代码⽐比如图⽚片、字体依赖也会不不断加⼊入到依赖图中

Output

⽤来告诉 webpack 如何将编译后的⽂件输出到磁盘

文件指纹

打包后输出的⽂件名的后缀

Hash:和整个项⽬的构建相关,只要项⽬⽂件有修改,整个项⽬构建的 hash 值就会更改 Chunkhash:和 webpack 打包的 chunk 有关,不同的 entry 会⽣成不同的 chunkhash 值 Contenthash:根据⽂件内容来定义 hash ,⽂件内容不变,则 contenthash 不变

Loaders

webpack 开箱即用只支持JS 和JSON 两种文件类型,通过Loaders 去支持其它文 件类型并且把它们转化成有效的模块,并且可以添加到依赖图中。 本身是一个函数,接受源文件作为参数,返回转换的结果。

常用loader:

babel

的配置⽂文件是:.babelrc

postcss-loader

PostCSS 插件 autoprefixer ⾃动补⻬ CSS3 前缀

px2rem-loader

⻚页⾯面渲染时计算根元素的 font-size 值

也可以用淘宝的库:github.com/amfe/lib-fl…

raw-loader

内联 html、JS

style-loader

内联css

html-inline-css-webpack-plugin也可以

plugins

插件⽤用于 bundle ⽂件的优化,资源管理和环境变量量注⼊入 作⽤用于整个构建过程

常用plugin

基础库分离:

将 vue基础包通过 cdn 引⼊入,不不打⼊入 bundle 中

SplitChunksPlugin

公共脚本分离、分离基础包、分离公共页面

常用参数:

minChunks: 设置最⼩引⽤次数为2次

minuSize: 分离的包体积的⼤⼩

glob.sync

利⽤glob.sync动态获取 entry 和设置 html-webpack-plugin 数量

entry: glob.sync(path.join(__dirname, './src/*/index.js')),

Mode

Mode ⽤来指定当前的构建环境是:production、development 还是 none 设置 mode 可以使⽤用 webpack 内置的函数,默认值为 production

内置函数:

文件更新

监听模式

webpack 开启监听模式,有两种⽅方式:

  • 启动 webpack 命令时,带上 --watch 参数
  • 在配置 webpack.config.js 中设置 watch: true

缺点:

每次需要⼿动刷新浏览器

文件监听原理

轮询判断文件的最后编辑时间是否变化

某个文件发⽣了变化,并不会⽴刻告诉监听者,⽽是先缓存起来,等 aggregateTimeout

module.export = {
    //默认 false,也就是不不开启
    watch: true,
    //只有开启监听模式时,watchOptions才有意义
    wathcOptions: {
        //默认为空,不监听的文件或者文件夹,支持正则匹配
        ignored: /node_modules/,
        //监听到变化发生后会等300ms再去执行,默认300ms
        aggregateTimeout: 300,
        //判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
        poll: 1000
    }
}

HMR热更新

WDS 不刷新浏览器 WDS 不输出⽂文件,⽽是放在内存中 使⽤ HotModuleReplacementPlugin插件

热更新原理分析

WebpackCompile: 将 JS 编译成 Bundle

HMR Server: 将热更新的文件输出给 HMR Rumtime

Bundle server: 提供⽂件在浏览器器的访问

HMR Rumtime: 会被注入到浏览器,

更新⽂件的变化

bundle.js: 构建输出的⽂文件

source map

科普文:www.ruanyifeng.com/blog/2013/0…

tree shaking(摇树优化)

概念:

1 个模块可能有多个方法,只要其中的某个方法使用到了,则整个⽂件都会被打到bundle ⾥⾯去

tree shaking 就是只把⽤到的⽅法打⼊ bundle ,没⽤到的⽅法会在uglify 阶段被擦除掉。 使⽤用:webpack 默认⽀支持,在 .babelrc ⾥里里设置 modules: false 即可

要求:必须是 ES6 的语法,CJS 的⽅式不⽀持

production mode的情况下默认开启

Tree-shaking 原理

利⽤ ES6 模块的特点:只能作为模块顶层的语句出现

import 的模块名只能是字符串常量

import binding 是 immutable的

代码擦除: uglify 阶段删除⽆无⽤代码

scope hoisting

原理

juejin.cn/post/685041…

将所有模块的代码按照引⽤顺序放在一个函数作用域⾥,然后适当的重命名⼀些变量以防⽌变量名冲突

对⽐: 通过 scope hoisting 可以减少函数声明代码和内存开销

scope hoisting 使⽤

webpack mode 为 production 默认开启

必须是 ES6 语法,CJS 不不⽀支持

如何编写可维护的webpack构建配置

通用性

业务开发者无需关注构建配置

统一团队构建脚本

可维护性

构建配置合理的拆分

README 文档、ChangeLog 文档等

质量

冒烟测试、单元测试、测试覆盖率、持续集成

体积优化策略

Scope Hoisting

Tree-shaking

图片压缩

原理

pngquant: 是一款PNG压缩器,通过将图像转换为具有alpha通道(通常比24/32位PNG文件小60-80%)的更高效的8位PNG格式,可显著减小文件大小。

pngcrush:其主要目的是通过尝试不同的压缩级别和PNG过滤方法来降低PNG IDAT数据流的大小。

optipng:其设计灵感来自于pngcrush。optipng可将图像文件重新压缩为更小尺寸,而不会丢失任何信息。

tinypng:也是将24位png文件转化为更小有索引的8位图片,同时所有非必要的metadata也会被剥离掉

动态 Polyfill

编译原理

Webpack可以将其理解是一种基于事件流的编程范例,一系列的插件运行。

grep "entryOption" -rn ./node_modules/webpack

入口文件

npm会让命令行工具进入node_modules.bin 目录查找是否存在 webpack.sh 或者 webpack.cmd 文件,如果存在就执行,不存在就抛出错误。

实际的入口文件是:node_modules\webpack\bin\webpack.js

process.exitCode = 0;          //1. 正常执行返回 
const runCommand = (command, args) =>{...};      //2.  运行某个命令
const isInstalled = packageName =>{...};      //3.  判断某个包是否安装
const CLIs =[...];       //4.  webpack可用的CLI: webpack-cli 和 webpack-command
const installedClis = CLIs.filter(cli => cli.installed);  //5. 判断是否两个 ClI 是否安装了
if (installedClis.length === 0){...}else if            //6.  根据安装数量进行处理
       (installedClis.length === 1){...}else{...}. 

webpack 最终找到 webpack-cli (webpack-command) 这个 npm 包,并且执行 CLI

webpack-cli 做的事情

引入 yargs,对命令行进行定制分析命令行参数,对各个参数进行转换,组成编译配置项

引用webpack,根据配置项进行编译和构建

webpack-cli对配置文件和命令行参数进行转换最终生成配置选项参数 options

最终会根据配置参数实例化 webpack 对象,然后执行构建流程

Tapable

Tapable 是一个类似于 Node.js 的 EventEmitter 的库, 主要是控制钩子函数的发布与订阅,控制着 webpack 的插件系统。

// 核心对象 Compiler 继承 Tapable
class Compiler extends Tapable {
    // ...
}

// 核心对象 Compilation 继承 Tapable
class Compilation extends Tapable {
    // ...  
}

Tapable库暴露了很多 Hook(钩子)类,为插件提供挂载的钩子

const {
    SyncHook,           //同步钩子
    SyncBailHook,           //同步熔断钩子 遇见return就返回
    SyncWaterfallHook,        //同步流水钩子 结果可以给下一个钩子
    SyncLoopHook,             //同步循环钩子
    AsyncParallelHook,          //异步并发钩子
    AsyncParallelBailHook,    //异步并发熔断钩子
    AsyncSeriesHook,         //异步串行钩子
    AsyncSeriesBailHook,       //异步串行熔断钩子
    AsyncSeriesWaterfallHook    //异步串行流水钩子
 } = require("tapable");

Tabpack 提供了同步&异步绑定钩子的方法,并且他们都有绑定事件和执行事件对应的方法。

使用方法:

const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);
 
//绑定事件到webapck事件流
hook1.tap('hook1', (arg1, arg2, arg3) => console.log(arg1, arg2, arg3)) //1,2,3
 
//执行绑定的事件
hook1.call(1,2,3)

compiler

module.exports = class Compiler {
    constructor() {
        this.hooks = {
            accelerate: new SyncHook(['newspeed']),
            brake: new SyncHook(),
            calculateRoutes: new AsyncSeriesHook(["source", "target", "routesList"])
        }
    }
    run(){
        this.accelerate(10)
        this.break()
        this.calculateRoutes('Async', 'hook', 'demo')
    }
    accelerate(speed) {
        this.hooks.accelerate.call(speed);
    }
    break() {
        this.hooks.brake.call();
    }
    calculateRoutes() {
        this.hooks.calculateRoutes.promise(...arguments).then(() => {
        }, err => {
            console.error(err);
        });
    }
}

NormalModule

Build

使用 loader-runner 运行 loaders 构建js

通过 Parser 解析 (内部是 acron)( required 依赖分析出来)

ParserPlugins 添加依赖

compilation

模块编译打包优化

Compiler 调用 Compilation 生命周期方法

addEntry -> addModuleChain (添加入口)

finish (上报模块错误)

seal (资源生成 优化 输出)

Chunk 生成算法

webpack 先将 entry 中对应的 module 都生成一个新的 chunk

遍历 module 的依赖列表,将依赖的 module 也加入到 chunk 中

如果一个依赖 module 是动态引入的模块,那么就会根据这个 module 创建一个新的 chunk,继续遍历依赖

重复上面的过程,直至得到所有的 chunks

如何编写loader

如何编写plugin

// 插件结构
class MyPlugin{
    constructor() {
 
    }
    apply(compiler){
        compiler.hooks.brake.tap("WarningLampPlugin", () => console.log('WarningLampPlugin'));
        compiler.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to 
${newSpeed}`));
        compiler.hooks.calculateRoutes.tapPromise("calculateRoutes tapAsync", (source, target, routesList) 
=> {
            return new Promise((resolve,reject)=>{
                setTimeout(()=>{
                    console.log(`tapPromise to ${source} ${target} ${routesList}`)
                    resolve();
                },1000)
            });
        });
    }
// 配置文件
const myPlugin = new MyPlugin();
 
const options = {
    plugins: [myPlugin]
}

// 插件执行
const compiler = new Compiler();
for (const plugin of options.plugins) {
    if (typeof plugin === "function") {
        plugin.call(compiler, compiler);
    } else {
        plugin.apply(compiler);
    }
}
compiler.run();

升级webpack5遇见的问题

"css.requireModuleExtension" is not allowed

webpack5 不再支持requireModuleExtension选项

juejin.cn/post/703847…

TypeError: Cannot read property 'tap' of undefined

hard-source-webpack-plugin在webpack5中会报错

不支持disableHostCheck配置

删除了 disableHostCheck配置项,改为 allowedHosts:all

polyfill被移除了,需要手动引入

juejin.cn/post/690690…

babel-eslint 包废弃

新的eslint包:@babel/eslint-parser