webpack笔记

78 阅读21分钟

webpack 诞生

  • 解决跨浏览器兼容
  • 导出模式js导出方式不统一
  • 发布代码混淆压缩
  • 原生js不支持npm的问题
  • 读取操作文件

提供一个黑盒解决构建过程.让开发人员只需要管开发问题,不需要处理一系列的工作准备

webpack 是什么

webpack是一个前端构建工具,他将任何js和输入视作为模块,他以index.js为起点,根据js的依赖关系(import和require)构建整个依赖树,通过运行在node上的本地服务进行资源整合,构建出运行时的所需要的产物

输入 =>  webpack  => 产物

webpack 安装

  • webpack分为全局安装和项目安装,每个项目都可以指定对应的webpack版本,
    • 全局安装的话可以直接使用 webpack 命令
    • 按项目安装 需要使用 npx 调用 webpack 命令 或者 配置 script 命令
mkdir devop // 创建项目文件夹
npm init -y  //初始化npm
npm i webpack webpack-cli -D ///安装对应依赖
//创建  src/index.js
$ npx  webpack   // 执行命令,开始打包
// 默认是以src/index做为入口
// dist/main.js 做为产物

打包完成后的产物可以直接在html使用

核心模块

  • webpack
    • webpack功能具体实现的包,本身可以单独使用,可以通过函数调用的形式执行
  • webpack-cli
    • 提供命令行的形式执行webpack核心包
    • 配置文件的读取和默认配置合并
  • webpack-dev-server
    • webpack官方提供的开发模块提效包,支持热更新

调用webpack包实现cli

// build.js
const config = require("./webpack.config");
const webpack = require("webpack");
webpack(config, (err, stats) => {
  if (err || stats.hasErrors()) {
    console.log("构建失败");
  }
  console.log("构建成功");
});
// package.jon

scripts:{
  'build':"node ./build.js"
}

模块兼容性

在webpack中esmodule和commonjs可以混合使用,webpack会将两者互相转成能够识别的普通js(能够兼容一些第三方包)

//esmodule
export const age = 222

export default {
    name:"11"
}

// commonjs
module.exports = {
    name:"11"
}

//最终产物
Object [Module] { age: [Getter], default: [Getter] }
{ name: '11' }

// esmodule 会被解析成一个对象,export dafalut 会变成特殊属性
// commonjs 则会成一个普通对象

编译结果分析

(function (modules) {
  var moduleExports = {};
  function __webpack_require__(moduleId) {
    if (moduleExports[moduleId]) {
      return moduleExports[moduleId];
    }
    var fnc = modules[moduleId];
    var module = {
      exports: {},
    };
    fnc(module, module.exports, __webpack_require__);
    var result = module.exports;
    moduleExports[moduleId] = result;
    return result;
  }

  return __webpack_require__("./src/index.js");
})({
  "./src/a.js": function (module, exports) {
    console.log("module a");
    module.exports = "a";
  },
  "./src/index.js": function (module, exports, __webpack_require__) {
    console.log("index module");
    var a = __webpack_require__("./src/a.js");
    console.log(a);
  },
});
  • 编译结果实际上是把js模块转换成对象的形式传入立即执行函数,以避免全局变量污染

  • 模块路径作为唯一key,value为一个函数用于执行模块内的代码,在模块内导出数据,挂载到一个运行时局部变量上,实现模块数据共同存储但数据不会污染

  • 函数体内实现了模块quire的方法

    • commonjs缓存实现,通过模块id区分
    • 传入module,和module.exports 来支持数据导出
    • 导出当前函数以供后续模块使用module变量存储数据
    • 引入其他模块相当于调用其他模块的方法,其他模块执行完毕后会往module对象上插入数据
  • 产物分析

    • import

      • 会被转换成 require 的形式

      • import * as obj from "./es6";
        //---------------
        __webpack_require__.r(__webpack_exports__);
         var _es6__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/es6.js");
        // 在module上定义了
        __webpack_require__.r = (exports) => {
          if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
            Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
          }
          Object.defineProperty(exports, "__esModule", { value: true });
        };
        
    • esmodule(export default)

      • 会被转换成module.export

      • export const age = 222;
        
        export default {
          name: "11",
        };
        //----------- 以下是产物
        __webpack_require__.r(__webpack_exports__);
        
        __webpack_require__.d(__webpack_exports__, {
          age: () => age,
          default: () => __WEBPACK_DEFAULT_EXPORT__,
        });
        
        const age = 222;
        const __WEBPACK_DEFAULT_EXPORT__ = {
          name: "11",
        };
        //export default  会被当做一个属性的key
        
    • module.export

      • 正常导出

真实产物中模块内部实现使用evel执行,是因为方便调试,不然错误会报在产物里面,无法定位错误,使用eval会在js虚拟机执行并且可以通过注释告诉开发者错误产生的文件

 eval(
        'module.exports.a = {\n    name:"11"\n}\n\n \n\n//# sourceURL=webpack://devop/./src/commonjs.js?'
 );
//  //# sourceURL=webpack://devop/./src/commonjs.js?'
//  标识错误文件产生位置为 ./src/commmonjs.js

配置文件

webpack 命令行执行支持带参数

npx webpack --mode=development

Webpack 可以携带很多参数,但是为了简单,我们一般使用文件的形式去操作

webpack默认会读取根目录下的 webpack.config.js 文件,也可以通过命令的形式指定读取的文件

npx webpack --config 123.js
  • 配置文件最终只需要使用commonjs的形式导出一个对象,对象的属性就是webpack的配置(配置文件不能使用esmodule)
  • 编译阶段可以使用任何文件后缀,但是webpack在打包过程是使用的node服务,他只能使用require形式去引入这个配置文件
  • 编译的代码是不参与webpack运行的,它是经过编译之后,最后运行产物,所以可以使用任何后缀
    • 会将文件内容通过字符串的形式读出来,然后分析依赖关系
  • 当命令行参数和配置文件配置出现冲突时,以命令行为准

Devtool

source-map 是什么

由于构建工具会将多文件或者不同文件类型的js代码进行合并压缩混淆,这个最终的产物就很难以理解和调试,后面就出现了一个技术叫source-map,用于保存原始完整代码以及完整代码和产物代码的映射关系

// bundle.js
&***&***&***&***&***&***&***
  
//# sourceMappingURL=bundle.map 
//浏览器识别到这一行 就会去请求对应的source map文件

如果在压缩的代码出现错误之后,并且存在对应的sourcemap文件,浏览器会将其错误报在sourcemap对应的位置

最佳实践

  • 应在开发环境使用,做为一种调试手段
  • source map一般会非常大,不仅会导致额外的网络传输,还有可能泄漏源代码
  • 如果还需要在生产环境使用,则需要做一些特殊处理

webpack中使用source map

通过devtool 配置是否要构建source map

module.exports = {
	devtool:'source-map'   //开发环境默认是eval  
}

每一种配置有不同的速度和产物

编译过程

webpack的作用就是将目标文件编译(构建 | 打包)成目标产物

整个打包过程分为三个步骤

初始化

  • 此阶段会将cli参数,配置文件,默认配置进行融合,形成一个最终的配置对象
  • 读取配置
    1. 读取项目根目录下的webpack.config.js
    2. 基本配置
      1. entry
      2. output
      3. module
        1. rules
      4. plugins
  • 初始化compiler对象
    1. compiler
    2. compilation
  • 初始化插件
    1. webpack最核心的内容,通过tapable实现

编译(入口解析)

  1. 创建chunk

    1. chunk是在webpack内部构建过程中的一个概念(块),它表示通过某个入口找到的所以依赖的统称,默认会根据入口模块(./src/index.js)创建一个chunk(可能会有多个)
    2. 每个chunk都至少有两个属性
      1. name 默认为main
      2. id 唯一编号,开发环境和name相同,生产环境是一个数字,从零开始
  2. 构建所有依赖模块

    1. 根据入口文件开始读取内容,判断入口文件引用的依赖模块是否已经加载,

      1. 如果已加载会有记录,有记录则结束加载(这一步会反复进入,每一个文件都会进入)
      2. 这里会创建一个模块表格,用于记录模块是否已经加载,防止重复读取
    2. 读取对应依赖的模块内容和入口模块的内容

      1. 为字符串,这一步骤仅是node读取,并不运行)
      2. 查找和读取都是由webpack调用node模块读取文件内容
    3. 转成ast语法树,遍历ast语法树得到所有引用关系,并且记录依赖生成一个数组来存储-- dependencies

      1. 后续需要遍历该数组,完成对所有依赖的读取)

      2. //记录chunk使用的dependencies(用于模块查找自己的依赖)
        ['./src/b.js','./src/a.js']
        
    4. 在运行内存中替换依赖模块使用的 require/import 的函数代码,然后转换读取模块的内容(字符串形式)

      1. 修改引入,模块名

      2. //转换前代码:
        require('./a.js')
        
        //转换后代码
        模块id:"./src/a.js"
        模块内容:
        __webpack_require__('./src/a.js')
        

    ![image-20240829105526255](/Users/qg/Library/Application Support/typora-user-images/image-20240829105526255.png)

输出(产出 chunk assets)

  • 在第二步完成之后,chunk会成生一个模块列表,模块列表中包含了模块id 和模块转换后的代码

  • webpack会根据配置为 chunk 生成一个资源列表,即chunk assets,

    • 资源列表可以理解为就是生成到最终文件的文件名和文件内容
  • 为什么是列表 ? 因为webpack的一个chunk的产物可能会有js,js.map

    //打包文件配置
    ./dist/main.js
    文件内容 : (function(modules) {} )(module) 
    //文件内容在前面已经分析过了,调用函数内部可以直接执行或者使用eval,可以通过devtool配置
    // 数据来源,就是module从哪里来
    
    //第二步在构建依赖完毕之后,我们就有一个模块id和模块代码的一个数据,
    //webpack输出阶段会根据配置读取数组中的内容将模块组装成上面这个模版所需要的数据
    [
      {
        moduleId:"./src/a.js",
        moduleContent:"xx"
      } 
    ]
    //遍历上面的构建依赖所得到的模块数组最终得到产物对象
    //最终产物的module数据
    //放入出口配置模板函数
    {
      './src/a.js':function() {
          	_webpack_require__('./src/b.js')
        }
      	'/src/index.js':function() {
    			_webpack_require__('./src/a.js')
        }
    }
    
  • 多chunk 合并

    • webpack 会为每一个chunk assets 的内容生产一个hash字符串,
      • 在内容不变的情况下,每次的hash都会是一样的(把js,js.map等文件的内容合并一起生成一个hash值)
    • 如果有多个chunk ,webpack 就会把所有的chunk的内容合在一起生成一个总的hash
  • 最终输出

    • 根据编译产生的总的 assets 使用fs模块生产对应的文件

    • [
        {
          文件名:./dist/main.js
          文件内容: xx
        }
      ]
      

    ![image-20240829112540917](/Users/qg/Library/Application Support/typora-user-images/image-20240829112540917.png)

术语总结:

  1. module: 模块,分割的代码单元,webpack中的模块可以是任何内容的文件不仅限于js
  2. chunk: webpack内部构建模块的称呼,一个chunk中包含多个模块,这些模块是从入口文件通过依赖分析得来的
  3. bundle: chunk构建好之后模块会生成chunk的资源清单,清单中的每一项就是一个bundle,可以认为bundle就是最终生成的文件
  4. hash: 最终的资源清单所有内容联合生成的文件
  5. chunkhash: chunk生成的资源清单内容所联合生成的hash值
  6. chunkname: chunk的名称.如果没有配置则使用main
  7. id:通常指的是chunk的唯一编号,如果在开发环境下构建,和chunkname相同;如果是生产环境下构建,则使用一个从0开始的数据进行编号

特殊注意:

如果开始watch模块,则直接从编译开始运行,不会重新进行参数整合

入口和出口

出口配置

可以设置文件目录和合并后的js文件规则

const path = require('path')

module.exports = {
  output :{
    // 必须配置一个绝对路径,表示资源放置的目录
    path:path.resolve(__dirname,'target'),
    // 配置的合并的js文件规则
    // 可以使用 'a/c/bundle.js' 
    // 会自动创建文件夹
    filename:"bundle.js"
  }
}

出口规则配置

  • name配置

    • 配置chunkname

    • module.exports = {
        mode: "development",
        entry: {
          index: "./src/index.js",
          main:"./src/main.js"
        },
        output: {
          path: path.resolve(__dirname, "target"),
          filename: "[name].js", 
          //使用name占位符,打包后会取入口的name 替换
          //name 可以任意写任意数量  他最终会被替换
          filename: "[name]test.[name].js",
        },
      };
      
  • Hash 配置

    • 总的chunk的hash(多入口产物的文件hash一直,因为是总的内容产生的hash)

    • 通常用于解决文件缓存的问题(文件内容发生变化才会重新生成)

    • 但凡有一个文件产生变化,整个hash就会变化

    • output: {
          path: path.resolve(__dirname, "target"),
          // filename: "test.[hash].js",
          filename: "test.[hash:5].js", // 指定hash值的取的长度
      },
      
  • chunk hash

    • 只有当前的chunk产生变化 才会重新生成,多chunk就会互不影响

    • output: {
        	path: path.resolve(__dirname, "target"),
          filename: "test.[chunkhash:5].js", // 指定hash值的取的长度
      },
      

入口配置

多入口就是配置多个chunk的入口,需要注意的是如果多个chunk引用了公用代码,这个时候公用代码会往多个chunk里面分别打入

  • 指定有多少个chunk

    • 多个chunk 必须指定多个出口

    • module.exports = {
        //属性名: chunk名称  属性值: 入口模块
        // entry:'./src/index.js' 会被转成下面这种写法
      		entry : {
            main:'./src/index.js'
          }
      }
      
  • 指定多个启动模块

    • 多个启动模块只会有一个chunk产生

    • module.exports = {
      		entry : {
            main:['./src/index.js','./src/a.js']
          }
      }
      
  • 使用chunkid 不建议使用

    • 开发环境是 name, 生产环境是id

loader 是什么

loader本质上是一个函数,它的作用是将某个源码字符串转换成另一个源码字符串换回

// 原始代码
变量 a = 1
// 这样直接打包会在ast哪一步转换出现错误 因为无法识别代码
// 如果需要让代码正常运行 则需要使用loader将其转换成js能解析的代码
module.exports = function (sourceCode) {
	// sourceCodde => 变量 a = 1
  return 'var a = 1'
}

Loader 的处理是在ast解析之前,读取文件之后,在经过loader的处理完成之后继续交给ast解析

Loader 函数的入参是什么 ?

  • 经过上一次loader转译过后的代码
  • 目标文件的源代码(字符串)

loader使用

  • 判断当前模块是否满足否个loader的规则

    • 读取规则中对应的loaders
    • 返回中数组
  • 进行loader处理后返回

  • loader数组对源码的处理顺序

    • // 匹配到的loaders数组
      [loader1,loader2,loader3]
      
      loader3 => loader2 => loader1 => 输出
      

loader配置

module.exports = {
  	module:{
         //这里面写规则
      	rules:[
          {
          	test:"/index\.js$/", 
            //会拿模块的路径做正则校验  ./src/index.js
            //匹配到了之后使用那些loader
            use:[
              {
                loader:"./loaders/test-loader" //webpack 到时候使用require直接引入
              }
            ]  
        	}
        ], 
      	noParse:false
    }
}

  • loader 可以任意修改源代码,不光是做代码解析

  • loader 可以在use 时传递参数

    • rules:[
        	{
            test:"./src/index.js",
            use:[
              {
                loader:"./src/loaders/test-loader.js",
                options:{					// 传递自定义参数
                  changeVar:"未知数",   
                }
              }
            ]
          }
      ]
      
  • loader 函数里面可以使用this, 这个this是webpack运行的创建的执行上下文,里面有很多属性

    • 可以通过this来获取自定义的loader参数
  • 一般使用第三库来获取配置参数

    • webpack5 直接使用this.query就可以获取

laoder rule 简化写法

rules:[
  {
    test:"xxx",
    use:['loader','loader2']
  }
]

loader 执行顺序

  • 匹配顺序
    • 创建一个空数组,用于收集依赖
    • 从rules第一个开始匹配,找到符合规则的将对应的loader收集起来(push进依赖数组)
  • 解析顺序
    • 从loader数组的最后一项往前执行,将后面的loader结果交给前面一项反复执行知道数组第一项
  • 入口文件引用其他文件 执行顺序
    • 先执行入口文件解析 触发对应的loader,解析完成后根据ast结果得到dependens,继续解析依赖模块
    • 依赖模块进入解析,触发对应的loader,最后跟上面解析流程一样
    • 总结: 先执行入口文件loader 后根据依赖引入位置触发

手写loader

  1. 使用loader处理css文件,做为style引入,实现 style-loader

    1. //./src/index.css
      body {
          background-color: red;
      }
      // ./src/index.js
      // 由于在解析阶段,node不会管里面引用语法问题,所有引用css会被当做为引用一个模块
      require('./index.css')
      
      //loader配置
      rules:[
         {
            test:/.css$/,
            use:['./src/loaders/style-loader.js']
          }
      ]
      // style-loader.js
      // 生成一个代码片段,将css解析到的内容作为js执行插一个style标签
      module.exports = function (code) {
        var code = `var style = document.createElement('style')
          style.innerHTML = \`${code}\`
          document.head.appendChild(style)`;
        return code;
      };
      
      //------
      // 改造style-loader使其返回css文件里面的内容
      // return的字符串 增加     	module.exports = \`${code}\`,在引用的地址就能拿到返回值
      module.exports = function (code) {
        var code = `var style = document.createElement('style')
          style.innerHTML = \`${code}\`
          document.head.appendChild(style)
          	module.exports = \`${code}\`
          `;
        return code;
      };
      
      //index.js
      const body = require('./index.css')
      
      
  2. 图片处理loader

    1. 在入口模块引入图片,让loader处理这个依赖并且根据配置执行一下操作

      1. 生成新文件,返回图片路径
      2. 返回base64地址
    2. // ./src/index.js
      const res = require('./assets/img.jpg')
      var img = document.createElement('img')
      img.src = res
      img.width = 200
      img.height = 200
      document.body.appendChild(img)
      // webpack.config.js
       {
          test: /\.(png)|(jpg)$/,
          use: [
            {
              loader: "./src/loaders/img-loader.js",
              options: {
                limit: 3000,
                filename: "img-[contenthash].[ext]",
              },
            },
          ],
        },
      // img-loader.js
      var loaderUtils = require("loader-utils");
      
      function loader(buffer) {
        //读取options配置
        const { limit = 1000, filename = "[contenthash].[ext]" } = this.query || {};
      
        var content = "";
        //判断buffer流长度
        if (buffer.byteLength > limit) {
          content = getFilePath.call(this, buffer);
        } else {
          content = getBase64(buffer);
        }
      	//导出生成的地址
        return `module.exports = \`${content}\``;
      }
      
      //buffer转base64
      function getBase64(buffer) {
        return "data:image/jpg;base64," + buffer.toString("base64");
      }
      //生成文件并返回文件名
      function getFilePath(buffer) {
        //根据this  和 buffer 创建一个文件名
        var filename = loaderUtils.interpolateName(this, "[contenthash:5].[ext]", {
          content: buffer,
        });
        //输出这个文件到dist输出目录
        this.emitFile(filename, buffer);
        return filename
      }
      // 配置静态属性,webpack 识别到会将读取的到的buffer传进来,而不是传字符串
      loader.raw = true;
      module.exports = loader;
      

Plugin 是什么

loader的功能定位是转换代码,而一起其他的操作loader难以实现

  • 当webpack生成文件时顺便多生成一个说明描述文件
  • 当webpack编译启动时,控制到打印文案

这种类似的功能需要把功能嵌入到webpack的编流程中,而这种事情的实现是依托于plugin的

  • Webpack 在很多执行环节会广播事件,plugin 就是接收这些事件做一些处理
  • 也就是说plugin就是在某一个阶段做一个事件监听,当触发事件执行一些事情

创建plugin

plugin的本质是一个带有apply方法的对象

var plugin = {
	apply:function(compiler) {
    
  }
}
// 一般写成一个class
class Muplugin{
  apply(compiler) {
    
  }
}

const plugin = new Myplugin()

引入插件

plugins: [new MyPlugin()],

apply是在什么时候执行?

  • apply函数会在初始化阶段,创建好compiler对象后运行(也就是说注册的插件在创建好直接运行)
  • 执行顺序是按数组的顺序一次执行plugin中的apply方法
  • 当文件变化触发重新编译时,apply方法就不会重新执行
  • apply 的作用是用来注册一些时间监听,类似与window.onload

appy方法的参数compiler是什么 ?

  • compiler对象时在初始化阶段构建的
  • 整个webpack打包期间只有一个compiler对象
  • 后续完成打包工作的事compiler对象内部创建的compilation

apply方法会在创建好compiler对象后调用,并向方法传入一个compiler对象

plugin 怎么注册事件

class Muplugin{
  apply(compiler) {
      compiler.hooks.[事件名称].[事件类型]('插件名称(调试使用)', callback)
  }
}
  • 事件名称是webpack 官方在各个阶段定义的事件名

  • 事件类型是webpack 借助第三方库 tapable Api 实现的,它支持几种事件类型

    • tap 表示同步事件任务, 类似与addEventLisetener

      • compiler.hooks.done.tap(name,function(compilation) {
        
        })
        
      • 函数运行完了表示事件处理完了

    • tapAsync 异步任务

      • compiler.hooks.done.tapAsync(name,function(compilation,cb) {
        		cb()
        })
        
      • 支持传入回调函数,回调函数执行完成 表示事件执行完毕

    • tapPromise

      • 返回的是Promise,promise.resolve 表示处理完
  • Tapable 我认为就是处理webpack 事件任务的几种形式,当webpack广播事件后,我是以同步任务还是异步任务去处理,

手写plugin

  • 在webpack emit阶段完成时读取assets产物文件列表,获取文件名称和id,写入一个新的文件

  • //webpack.config.js
    //设置产物名称
      plugins: [new FileListPlugin('test-fileList.txt')],
    // plugin 代码
      module.exports = class FileListPlugin {
      constructor(filename) {
          this.filename = filename
      }
      apply(compiler) {
        //监听emit阶段事件
          compiler.hooks.emit.tap('fileListPlugin',compilation => {
            // 获取产物列表
              var assets = compilation.assets
              var result = []
              for (const key in assets) {
                  if (Object.prototype.hasOwnProperty.call(assets, key)) {
                      const element = assets[key];
                      result.push(`${element.size()} - ${key}`)
                  }
              }
              var str = result.join('\n')
              // 新增产物
              assets[this.filename] = {
                  size() {
                      return  str.length
                  },
                  source() {
                      return str
                  }
              }
          })
      }
    }
    
  • webpack assets 产物列表有两个属性

    • size : () => number .返回文件大小
    • source : () => buffer | string 文件内容
    • 新增的往assets数组插入就行
    • 获取的话 直接使用key 拿到这个资源对象

环境配置区分

  • 通过创建多个配置文件,通过脚本指定使用的配置文件

    • npx webpack --config webpack.dev.js
      
  • 通过配置文件导出函数来区分,这个是webpack 在执行这个函数的时候会将命令行里面配置的env传入

    • npx webpack  --env abc  // env => 'abc'
      npx webpack  --env.abc   // env => {abc : true}
      npx webpack  --env.abc  =1 // env => {abc : 1}
      npx webpack  --env.abc  =1 --env.bcd= 2 // env => {abc:1,bdc:2}
      
    • module.exports = function(env){
      		if(env && env.prod) {
            return {
              mode:"production"
            }
          }
        return {
          mode:"development"
        }
      }
      

Webpack 其他配置

不太重要的配置

context

  • 相当于路径别名,会影响entry和loaders的查找路径, (查找路径默认以当前执行路径为基准)

  • 修改后以修改后的路径为基准

  • // 这样就会以src就基准
    module.exports = {
       context:Path.resolve(__direname,'src')
    }
    

output

  • library

    • 将打包结果暴露出去,形成一个全局变量

    • output:{
        library:"abc"
      }
      // 编译后结果 
      var abc = (function(modules))(modules)
      

      一般用户和其他chunk交互或者第三方库使用

  • libraryTarget

    • 指定打包的暴露方式.一般和library 一起使用

    • output:{
        library:"abc",
         libraryTarget:"var"
      }
      
    • 默认是 var 全局变量

    • window 挂载到window对象上

      • window[abc] = xxx
        
    • commonjs 使用 export导出

      • export[abc] = xxx
        
  • target

    • 指定打包产物的运行环境
      • web
      • node
      • 如果是web .那么在chunk里面使用node原生模块在解析路径的时候就会找不到
  • noParse loader的配置

    • 不解析指定规则匹配到的模块.通常用于忽略大型单模块库

    • 直接跳过解析环境(ast解析,依赖分析)

    • module:{
        rules:[],
         noParse:/jquery/,
      }
      

    resolve 解析模块的配置

    • 配置模块查找路径 module

      • 引入模块默认就是node_modules

      • 修改之后就是以配置的模块为查找位置

      • resolve :{
          module:['node_modules']
        }
        // 先在abc目录查找,然后找src
        resolve :{
          module:['abc','./src']
        }
        
    • extensions

      • 补全文件后缀配置

      • 为什么没有写文件后缀,仍然可以找到文件

        • 因为webpack 会根据extensions的配置自动补全后缀名

        • // 查找文件后缀规则
          resolve:{
            extensions:['.js','.css','.jsx']
          }
          
    • alias 配置路径别名

      • 会影响导入模块的书写和查找

      • resolve :{
          alias :{
            "@":path.resolve(__dirname,'src'),
             "_":__dirname
          }
        }
        // 使用
        require('@/a')
        
    • externals

      • 告诉webpack遇到那个包不需要解析

      • externals :{
          jquery:"$",  //把引入jquery模块替换成对应的字符
           lodash:"_"
        }
        //
        quire('jquery')
        // 不使用external 配置的情况下 ,jquery整个包会被打包到产物里面
        // 如果使用配置
        // 产物就会变成
        // 直接导出这个对象
        'xxx/jquery':function(){
          module.exports = $
        }
        
      • 可以配合页面使用cdn,减少bundle体积,在源码使用也不影响

    • stats

      • 控制构建过程中控制台输入内容

      • 指定那些情况才会输出内容(错误,警告)

      • stats :{
        	  colors:true,		//输出内容带颜色
            modules:false // 不输出模块显示
        }
        

常用plugin

clean-webpack-plugin

  • 删除dist目录

  • 实现原理

    • 大概就是在emit阶段之前调用fs删除dist目录

    • var { CleanWebpackPlugin } = require("clean-webpack-plugin");
      
      
      new CleanWebpackPlugin(),
      

html-webpack-plugin

  • 自动生成html并且引入打包后的bundle文件

  • 实现原理

    • 在emit阶段,利用fs模块生产一个页面文件
    • 给文件内容的合适位置添加一个scripte元素
    • 元素的src路径引用打包后的js( compiler.compilation.assets )
    • 有多个入口会自动引用多个js
  • 配置项

    • template
      • 修改生成的html模板

      • new HtmlWebpackPlugin({
            chunks: ["index"],
            filename: "index.html",
        }),
        
        
    • chunks
      • 指定当前页面需要引入那些chunk的产物

      • new HtmlWebpackPlugin({
            template: "./public/index.html",
            chunks: ["index"],
            filename: "index.html",
          }),
        
  • 多个html

    • 使用多个实例即可

    • new HtmlWebpackPlugin({
          template: "./public/index.html",
          chunks: ["index"],
          filename: "index.html",
      }),
      new HtmlWebpackPlugin({
          template: "./public/index.html",
          chunks: ["main"],
          filename: "main.html",
      }),
      

copy-webpack-plugin

  • 复制资源到指定目录

  •  new CopyPlugin({
          patterns: [
            {
              from: "./public",
              to: "./",
            },
          ],
    }),
    

webpack-dev-server

  • 这个是webpack官方制作的一个单独的库,它既不是loader也不是plugin
// 安装
npm i webpack-dev-server
// 启动
npx webpack-dev-server
// 简化命令
npx webpack serve
// Webpack-dev-server 几乎支持所有webpack命令参数,例如--config,--env等,
npx webpack-dev-server --config webpack.config.2.js --env dev

当我们执行webpack-dev-server 命令时,它做了一下操作

  • 内部执行webpack命令,传递命令参数
  • 开启watch(监听文件变化)
  • 注册hooks: 类似与plugin,,webpack-dev-server回向webpack中注册一些钩子函数
    • 将资源列表(assets)保存起来,存在内存里面
    • 禁止webpack输出文件
      • compiler.compilation.asset = {} , 清空输出资源
    • 这样原始的assets被保存了,他还是能够通过服务返回
  • 使用express开启一个服务器,监听某个端口,当请求到达后,根据请求路径,给予相应的资源内容
    • 比如访问localhost:8080/index.html => 得到就是 assets/index.html

配置项

和webpack的配置放在一起

module.exports = {
  devServer:{
    port:3000, // 端口配置
    open:true, // 服务运行之后自动打开浏览器
    index:index.html,  //默认访问资源地址
    proxy:{     //配置请求代理
      "/api":"https://baidu.com"  //代理规则
    }
  }
}
// 最新版服务启动自动开发页面配置,可以同时打开多个页面
open: ['/my-page','/list.html'],

proxy

  • 正常我们本地访问资源dev-Server 是能处理的,但是如果不是资源或者获取不到,这个时候就会出现404

  • 我们本地访问一个接口因为浏览器跨域的限制,会被浏览器拦截,这个时候就需要使用dev-Server使用请求代理转发

  • localhost:8000/api/xx => baidu/api/xxx
    // 告诉dev-server 找不到并且是我规则里面的路由就给我进行转发请求
     proxy:{     //配置请求代理
      "/api":"https://baidu.com"  //代理规则   
    }
    // 路由路面包含api  就修改域名为我指定的
    
  • proxy:{     //配置请求代理
      "/api":{
        target:"https://baidu.com",
          changeOrigin:true // 更改请求头中的host和origin
      }
    }
    

普通文件处理

file-loader 匹配到符合的文件后,在编译阶段生成文件,导出文件地址

  • module :{
      rules:[
        {
          test:/.(png) | (gif)$/,
          use:[
            {
              loader:"file-loader",
              options :{
                name:"imgs/[name]-[hash].[ext]",
              }
            }
          ]
        }
      ]
    }
    
  • 这个时候option里面写的 占位符都是交给loader处理,因为还是没有解析完chunk是拿不到hash的

  • imgs/ 表示创建一个文件夹

  • 原理在之前已经实现过了,这里其实差不多,但是这里导出使用的esmodule导出

    • export default 'xxxxx.png'
      // 我们使用require引入 需要使用.default 获取
      require('xxx.png').default
      
    • esmodule的导出会被webpack转换成一个对象,default是一个特殊属性 对应 export default

url-loader

匹配到符合的文件后,在编译阶段生成对应的base64编码,然后导出

  • 同样使用的 esmodule 导出

  • {
          test:/.(png) | (gif)$/,
          use:[
            {
              loader:"url-loader",
              options :{
                limit:1000,  
                //设置不超过1000b 的文件就使用base64编码,
                //否则使用fileLoader
              }
            }
          ]
        }
    
  • 内部使用了 file-loader

    • 一般小的图片资源都可以使用base64处理

资源路径问题处理

有些情况,会有loader和plugin会使用相对地址创建资源,导致我们的产物去访问这些资源的时候访问不到

  • new HtmlWebpackPlugin({
      filename:"html/index.html"
    })
    //这样生成的产物会在dist/html/index.html
    //此时如果说我们使用file-loader去读取带有二级目录的资源
    use:[{
      loader:"file-loader",
      options:{
        	filename:"imgs/[name].[ext]"
      }
    }]
    
    //这个时候使用dev-server的时候就会访问不到
    	devServer:{
        openPage:"/html"
      }
    
    localhost/html/index.html  =>  localhost/html/imgs/index.png
    
    
  • 使用publicPath

    • webpack会在内部创建第一个变量用于记录publicPath,这个全量loader和plugin都可以使用

    • output:{
        	publicPath :"xxx"
      }
      
      // 编译产物
      __webpack_require__.p =  'xxxx'
      
      //在输入内容使用这个产物 这样使用在编译后会被转换成上面的变量
      __webpack_public_path__
      
    • 一般我们使用 '/' ,表示把根目录做为绝对地址入口

    • 如果说是loader 或者 plugin 需要单独的配置,一般是使用参数传入

webpack 内置对象

DefinePlugin

  • 在编译完成之后,产物里面会替换对应的常量

  • const webpack = require('webpack')
    
    //会把计算结果返回
    new webpack.DefinePlugin({
      PI:`Math.PI`,					// PI = Math.PI
      VERSION:`"1.0.0"`,   // VERSION = '1.0.0'
      DAMIN:JSON.stringifly('test.com')
    })
    

BannerPlugin

  • 为chunk增加注释信息

  • new webpack.BannerPlugin({
      banner:`
      	hahs:[hash]
      	chunkhash:[chunkhash]
      	name:[name]
      	author:ceshi
      	corporation:ceshi
      `
    })
    

ProvidePlugin

  • 自动加载模块,不需要手动import 或者 require

  • new webpack.ProvidePlugin({
    	$:"jquery",
      _:"lodash"
    })
    //在产物里面会被改写成
    (function($,_) {
      // 我们的代码
    }).call(this,__webpack_require('./xxxx/jquery.js',__webpack_require('./xxxx/lodash.js',))
    

css工程化

css的问题

  • 类名冲突
  • 重复样式
  • css文件细分

解决方案

  • 解决类名冲突
    • 命令规范
    • css in js (react native 使用)
    • css module
  • 解决样式重复
    • css in js
    • 预编译器
  • 解决css文件细分问题
    • 使用构建工具打包合并压缩css

使用webpack拆分css

css-loader

  • 会将css代码转换成js代码成为字符串,实现导出一段css

    • .red{
      	color:"red"
      }
      
      /// 转换后
      module.exports = `.red{
      	color:"red";
      }`
      
      // 如何使用图片做为背景
      .red{
      	background:("./bg.png")
      }
      //编译后
      const import1 = require('./bg.png')
      module.exports = `
      .red{
      	background:url("${import1}")
      }
      `
      // 需要先使用file-loader处理png文件,因为webpack检测到模块引入解析不了png
      
    • 解决类名冲突

      • 使用 module 属性,开启类名hash

        • use:[
            'style-loader',
             {
          		loader:"css-loader",
               options:{
                   module:true
               }
             }
          ]
          
      • 根据文件路径 + 类型 => 生成hash值

        • './src/assets/a.css' + .c1 => hash1
          
          './src/assets/b.css' + .c1 => hash2
          
      • 这个时候css-loader会增加一个属性记录对应的类名关系

        • const res = require('./src/a.css').default
          
          res.locals => { c1 : 'xxcxcxcxc' }  //对应的类名: hash 值
          
        • 这个时候页面上使用了该类名是不会生效的,因为识别不到原始的类名,需要在引用的地方手动修改

          • const res = require('./src/a.css').default
            
            const div1 = document.getElementById('div1')
            div1.className = res.locals.c1
            // 如果在css-loader的基础上 使用的style-loader,会帮我解析出locals
            // 那么引入css 的返回值直接就是类名映射
            
            
  • 在css内部导入其他css文件

    • //page.css
      	.page{
          color:"blue"
        }
      //
      @import './page.css'
      .red {
        color:"red"
      }
      //page.css 会被合并到index.css
      const import1 = require('./page.css')
      module.exports = `${import1}`
      .red{
       color:"red"
      }
      // 最后在index.css合并导出
      module.exports = `
      .red {
        color:"red"
      }
      .page{
          color:"blue"
      }
      `
      //在js 生成一个sytle 标签插入到head里面
      
  • style-loader

    • 将css-loader导出的字符串创建style并且加入到页面的style

    • //简化后的代码
      const res = require('./index.css').default
      const style = document.createElement('style')
      style.innerHTML = res.toString()
      document.head.appendChild(style)
      
    • 引入多个相同的css文件,因为webpack的机制,只会读取一次

    • 该loader最终导出的是一个 空对象

webpack 使用less

  • 由于css-loader 不会分析css文件内容,如果是less文件也会直接将其内容返回

  • 所以需要 将 less 转换成 css 再返回,这样才能给html识别

  • // 需要安装less 这个库 less的代码需要用到less库的能力
    npm i -D less-loader less
    //
    use:['style-loader','css-loader','less-loder']
    

使用postcss

  • postcss 是什么

    • 是一个使用工具和插件转换解析css的工具
  • 提供了一个转换函数api

    • let postcss = require('postcss')
      
      postcss(plugins).process(css, { from, to }).then(result => {
        console.log(result.css)
      })
      
  • 一般我们使用命令行工具进行文件转换,它帮我调用函数进行转换

    • npm i -D postcss-cli
      
      //编译文件到指定目录  -w 是观测文件改动
      postcss css/source.css -o css/out.css -w
      // 支持后缀名pcss  postcss css sss
      
    • postcss的配置文件

      • postcss.config.js

      • 跟webpack类型,使用commonjs导出一个对象

      • module.exports = {
          map:true , //关闭source-map
        }
        
    • 使用第三方插件

      • Postcsss-preset-env - 为postcss预设环境

        •  module.exports = {
            	//配置插件
             plugins: {
               //插件自己配置
              "postcss-preset-env": {},
            },
          }
          
        • 自动增加厂商前缀

          • ::placeholder {
                color:red;
            }
            // 转换为
            ::-moz-placeholder {
                color:red;
            }
            ::placeholder {
                color:red;
            }
            
          • 内置了autoprefixer库

          • 可以指定需要兼容的浏览器

            • 直接在plugin指定

               "postcss-preset-env": {
                 browsers:['last 2 version','> 1%']
               },
            
            • 创建.browserslistrc,填写配置内容,可以被其他插件使用

            • 在package.json 创建 browserslist

            • last 2 version  //兼容最近的两个浏览器版本
              > 1% in cn     // 匹配市场占有率1%以上的浏览器 (国内)
              not ie <= 8			// 排除ie版本 <= 8 的浏览器
              // 默认匹配的是并集
              
      • 设置对未来语法的兼容性

        •  "postcss-preset-env":{
             stage:0  // 0 - 4表示w3c 草案版本进度  越大越趋于稳定
           }
          
        •  // 定义变量
           // 原始代码
           :root {
             --lightColor: red;
           }
           
           .b {
             background-color: var(--lightColor);
           }
           //编译后代码
           :root {
             --lightColor: red;
           }
           // 会保留兼容性写法 
           .b {
             background-color: red;
             background-color: var(--lightColor);
           }
          
        • 通过修改perse 关闭对新的版本支持

        •  "postcss-preset-env":{
             stage:0,  // 0 - 4表示w3c 草案版本进度  越大越趋于稳定
             perseve:false
           }
          
  • 在webpack 使用 postcsss

    • npm i -D postcss-loder
      // webpack.config.js
      use:['style-loader','css-loader','postcss-loder']
      
      // 需要创建postcss.config.js
      module.exports = {
        map: false,
        plugins: {
          "postcss-preset-env": {
            stage: 0,
          },
        },
      };
      

抽离css

Mini-css-extract-plugin

  • 它提供了一个plugin 和 一个loader

    • plugin 用于生成最后的css文件
    • loader 用于存储css代码和导出css代码
  • const MiniCssExtractPlugin = require('mini-css-extract-plugin')
    {
        test:/.css$/,
        use:[MiniCssExtractPlugin.loader,'css=loader']
    },
        
      // pluin使用
      plugins:[
        new MiniCssExtractPlugin()
      ]
    
  • 他会把单个chunk的css模块合成一个css文件,

  • 如果是有多个chunk 就会生成多个css文件

  • 默认是使用 chunk 名做为文件名,通过配置修改

    • module.exports = {
        plugins:[
          new MiniCssExtractPlugin({
            filename:"css/[name]-[contenthash:5].css"
          })
        ]
      }
      

babel 是什么

实现类似less一样 进行代码解析转换的功能,将高版本或者低版本 转换成目标浏览器支持的js代码

  • 跟webpack类型 本身只提供解析功能,其他能力靠babel插件和babel预设完成完成
  • babel 预设和postcss 的预设含义一样,是多个插件的集合体

可以跟构建工具一起使用 也可以自己使用 ( 类似less )

// 单独使用
// @babel 是命令空间
npm i -D @babel/core @babel/cli
// @babel/core  核心库,提供了编译所需的所有api
// @babel/cli  提供了一个命令行工具,调用核心库的api完成编译

提供了两种编译方式

  • -w 使用watch模式

  • 指定文件编译

    • babel   指定文件  -o 编译结果文件
      
  • 指定目录编译

    • babel 指定目录 -d  编译结果文件目录
      

使用配置文件接入预设和插件

  • .babelrc

  • 使用json格式

  • {
      "presets":[],
      "plugins":[]
    }
    

babel的预设( 预设执行顺序 从后往前) ( 通过require 直接导入)

  • @babel/preset-env

    • 可以让你使用最新的js语法,而无需针对每种语法转换设置具体的插件

      • {
          "presets":["@babel/preset-env"],
          "plugins":[]
        }
        
    • 给预设进行配置

      • {
          "presets":[
            ["@babel/preset-env",{
               //这里进行配置
              //默认是false
              "useBuiltIns":false,
              // 开启设置
              "useBuiltIns":"usage",
              // 需要指定corejs 版本
              // 版本不一样 指定的路径不一样
              // npm i -S core-js@2
              "corejs":2
            }]
          ],
          "plugins":[]
        }
        
      • 需要注意的是useBuiltIns ,表示对新增的api 不进行注入,(原代码直接复制)

      • 如果设置为true,就会给你引对应api的降级试下

        • new Promise((resolve) => {
            console.log(2);
          });
          // 转换后的代码
          require("core-js/modules/es6.promise.js");
          new Promise(function (resolve) {
            console.log(2);
          });
          
  • 兼容的浏览器设置

    • 跟postcss一样 使用.browserslistrc 这个配置文件来描述

插件处理

  • @babel/polyfill 已经过时,目前已经被core-js替代

  • 除了预设可以转换代码之外插件也可以转换代码,执行顺序是

    • 插件在presets之前执行
    • 插件顺序是从前往后执行
    • presets顺序是从后往前
  • {
      "presets":['a','b'],
      "plguins":['c','d']
    }
    //执行顺序
    c => d => b => a
    
  • @babel/preset-env 只会转换已经在w3c形成正式标准的语法,对于某些处于确定阶段的语法不做转换

  • 单独使用一些插件

    • 插件使用配置,跟preset 是一样的

    • @babel/plugin-proposal-class-properties

      • 在css 中 书写初始化字段

      • class A {
           a = 1;
          constructor {
        			this.a = 3
          }
        }
        
    • @babel/plugin-proposal-optional-chaining

      • 对象属性安全性读取,如果是undefind就结束,没有的话继续读属性

      • const bz = obj?.foot?.bar?.baz
        
    • babel-plugin-transform-remove-console

      • 移除所有console
    • @babel/plugin-transform-runtime

      • 提供一些公共api帮助进行代码转换
      • 可以理解为 提取产物里面重复公共逻辑,在产物很多时公用代码就会很多份,使用这个插件就可以使用预设的公共api,减少代码体积

babel原理

  • 解析 Parse 将代码解析生成抽象语法树( 即AST ),也就是计算机理解我们代码的方式(扩展:一般来说每个 js 引擎都有自己的 AST,比如熟知的 v8,chrome 浏览器会把 js 源码转换为抽象语法树,再进一步转换为字节码或机器代码),而 babel 则是通过 babylon 实现的 。简单来说就是一个对于 JS 代码的一个编译过程,进行了词法分析与语法分析的过程。
  • 转换 Transform 对于 AST 进行变换一系列的操作,babel 接受得到 AST 并通过 babel-traverse 对其进行遍历,在此过程中进行添加、更新及移除等操作。
  • 生成 Generate 将变换后的 AST 再转换为 JS 代码, 使用到的模块是 babel-generator

先通过把静态代码解析成ast,然后根据预设和插件进行转换另外一个ast,最后把ast再转换成静态代码

babel 插件实现

  • babel的插件是一个函数,在babel配置里面放入plugin 配置数组就可以,在运行babel的时候,会执行这个函数
  • 执行的时候会往这个函数传入一个对象,就是babel提供的一系列解析能的api
  • 插件需要做的就是返回一个对象
    • name 插件名称
    • visitor 插件匹配规则
      • 他是一个对象,能够书写很多规则,每一个规则里面的return 可以返回操作后的代码块
//  实现 箭头函数 转换为 function 的插件
// types 包含了各种ast的方法

const transformFunction = ({ types: t }) => {
  return {
    name: "transformFunction",
    // 匹配规则  遇到 type 为 ArrowFunctionExpression 的就做处理
    visitor: {
      ArrowFunctionExpression(path) {
        console.log(path);
        // 箭头函数转function
        const node = path.node;
        // 需要处理的信息 async(是否是异步方法) params(函数参数) body(函数体)
        // 参数一  函数名称,传null表示匿名函数
        // 函数体需要转换 (普通函数的函数体类型是 BlockStatement )
        const arrowFunction = t.functionExpression(
          null,
          node.params,
          t.blockStatement([t.returnStatement(node.body)]),
          node.async
        );

        path.replaceWith(arrowFunction);
      },
    },
  };
};

const result = babel.transform(code, {
  presets: [],
  plugins: [transformFunction],
});

在webpack使用babel

类似于postcss 配置一样,使用babel-loader就行了

npm i --save-dev babel-loader @bable/core

配置

module:[
  rules:[
  	{
    test:/.js$/,
    use :['bable-loader']
 		 }
  ]
]

Swc

Babel 只能提供代码转换,并不能实现代码打包,所以一般情况下都是配合第三方打包工具使用

swc 的可以实现babel的功能,并且在其基础上支持打包的功能,例如代码压缩,代码优化(terser)等,天然支持ts,并且速度比babel块非常多,使用rust开发,效率非常快

import swc from "@swc/core";
const result = swc.transformFileSync("./app.jsx", {
  jsc: {
    parser: {
      syntax: "ecmascript",
      jsx: true,
    },
    target: "es5",
  },
});
console.log(result.code);

在webpack使用

配置文件 为 .swcrc

module: {
  rules: [
    {
      test: /\.m?js$/,
      exclude: /(node_modules)/,
      use: {
        //当与 babel-loader 一起使用时,parseMap 选项必须设置为 true。
        loader: "swc-loader",
        options: {
          parseMap: true
        }
      }
    }
  ]
}

性能优化概述

构建性能

  • 这里指的是开发阶段的构建性能
  • 优化的目标是降低从打包开始,到代码效果呈现所经过的时间
  • 构建性能越高,开发过程中的时间浪费越少

传输性能

  • 打包后的js代码传输到浏览器经过的时间
  • 控制总传输量: 所有js的内容加起来就是总传输量,重复代码少总数传输量就少
  • 文件数量: 传输的js文件数量越多http请求越多,响应速度越慢
  • 浏览器缓存: js文件会被浏览器缓存,被缓存的文件后续不会传输

运行性能

  • js代码在浏览器端的运行速度
  • 不需要过早的关注于性能,开发效率优先

解决方案

构建性能

  • 减少模块解析

    • 模块中无其他依赖 ( 一般是打包后的第三方库)

    • 如果不使用loader 那么源代码就是最终代码

    • 经过loader编译之后就是源代码,不进行内部依赖分析

    • module:{
        noParse:/jquery/
      }
      
  • 优化loader性能

    • 限制loader的应用范围

      • 例如babel-loader 就是对高版本js进行,但是有一些第三方库本身就是使用低版本写的,通过解析反而会增加不必要的消耗,所以我们要排除一些不需要解析的模块
      • module.rule.exclude(排除那些模块)
      • module.rule.include (仅处理那些模块),
      • 只能二选一
      module:{
        rules:[
          {
            test:/.js$/,
            use:"babel-loader",
            exclude:/lodash/,  
      			//  /node_modules/  排除 node_modules 目录
            //  include: /src/  仅对src目录进行检查
      		}
        ]
      }
      
    • 缓存loader结果

      • 使用 cache-loader

      • {
            test:/.js$/,
            use:[ {
              	loader:"cache-loader",
                options:{
                  //设置缓存文件目录  默认是临时目录
                  cacheDirectory:"./cache"
                }
            },"babel-loader"],
          }
        
      • cache-loader 放在最前面按逻辑应该是最后执行,内部是怎么实现控制的(pitch过程)

        • //cache-loader.js
          function loader(source) {
            	return `new source`
          }
          
          loader.pitch = function() {
            // 控制是否返回源代码
            // 如果返回可以返回源代码
          }
          module.exports = loader
          
        • use:["loader1","loader2"."loader3"]
          
        • pitch 可以中断loader的执行,直接输出缓存结果

          • 正常执行是 1 => 2 => 3 的流程读取pitch,如果有一个存在pitch就会进入loader执行,并且跳到有pitch的loader, ( 3 => 2 => 1)
          • 如果都没有pitch,那就是按正常流程执行
        • ![image-20240909141716906](/Users/qg/Library/Application Support/typora-user-images/image-20240909141716906.png)

    • Thread-loader 开启线程池

      • 把后续的loader放到线程池中运行

      • {
            test:/.js$/,
            use:[ 
              {
              	loader:"cache-loader",
                options:{
                  //设置缓存文件目录  默认是临时目录
                  cacheDirectory:"./cache"
                }
            	},
            //在该loader之后的都会放到线程池里面执行
            'thread-loader'
             "babel-loader",
            ],
          }
        
      • 使用线程池之后,部分webpack的能力无法使用

        • webpack api生成文件
        • 无法使用自定义的plugin api
        • 无法访问 webpack options
      • 开启和管理线程需要消耗时间,小型项目不建议使用

scope hoisting

  • scope hoisting 是webpack 的内置优化,它是针对模块的优化,在生产环境打包时会自动开启

  • 在未开启scope hoisting 时,webpack会将每个模块的代码放置在一个独立的函数环境中,这样是为了保证模块的作用域互不干扰

  • 而scope hoisting的作用是把多个模块的代码合并到一个函数环境中执行,在这一过程中webpack会按照顺序正确的合并模块代码,同时对涉及的标识符做适当处理以避免重名

    • 这样做的好处是减少函数调用,提升运行效率,降低打包体积
  • 遇到某些模块多次被其他模块引用,或者使用了动态导入的模块,或者是非esm模块,都不会有 scope hoisting

热替换 (HMR)

( Hot Module Replacement)

  • webpack-dev-server的弊端

    • 每次代码变动在重新打包之后,浏览器刷新会重新请求所有资源
    • 在刷新之后导致之前操作的数据和步骤都需要重新完成
  • 热更新支持 仅请求改动的资源

    • {
        devServer:{ 
          //开启配置
          hot:true
        },
          //可选,最新版本不需要
        plugins:[new webpack.HotModuleReplacementPlugin()]
      }
      
      // ./src/index.js
      if(module.hot) {
        module.hot.accept()
      }
      
    • 如果在index.js接入hot代码之后,webpack-dev-server会往打包结果中注入modue.hot属性

    • 默认情况下,dev-server不管是否开启了热更新,当重新打包后,都会调用location.reload刷新页面

    • 但是如果加了module.hot.accept(),会让dev-server通过socket管道,把服务器更新的内容发送到浏览器

    • 然后将结果交给插件HotModuleReplacementPlugin 注入的代码执行

    • HotModuleReplacementPlugin 会覆盖原始代码,然后让代码重新执行,所以热替换发生在代码运行期

  • 我理解 某个模块代码更新之后, 触发插件执行,覆盖模块的原始代码,重新运行入口模块代码(js代码变化,html没变,不会触发覆盖html的用户input等操作),用户后续执行的js代码就是修改后的了

  • 样式热替换

    • 对于样式也可以使用热替换,但是需要使用style-loader
    • 因为热替换发生时,插件只会简单的重新运行模块代码
    • 因此style-loader的代码运行 就会重新设置style元素中的样式
    • 而mini-css-extract-plugin 由于生生文件是在构建期间,运行期间无法改动文件.所以对热替换无效

传输性能

分包
  • 为什么需要分包

    • 多个chunk引用重复的模块代码(或者第三方库),导致产物出现大量重复的代码
    • 提取公共代码形成单独的js文件,使产物引用即可
  • 手动分包

    1. 先单独打包公共模块

      1. 公共模块会被打包成动态链接库(ddl Dynamic Link Library),并且生成资源清单

      2. jquery => dll/jquery.js,暴露变量jquery
        lodash => dll/lodash.js,暴露变量lodash
        // 资源清单
        manifest.json
        {
          jquery:juqery,dll/juqery.js,
          lodash:lodash,dll/lodash.js
        }
        
    2. 根据入口模块进行正常打包(正式开始)

      1. 遇到模块清单里面的模块,代码会被转化为直接使用导出全局变量

      2.   entry: {
            // 手动分包需要使用数组指定模块
            jquery: ["jquery"],
            lodash: ["lodash"],
          },
          output: {
            filename: "dll/[name].js",
            library: "[name]", // 暴露每个bundle 的全局变量名
          },
        
    3. 打包资源清单

      1. // 使用内置插件
        new webpack.DllPlugin({
              path: path.resolve(__dirname, 
                                 "./dll", "[name].manifest.json"),
              name: "[name]",
        }),
        
      2. 生成结果

      3. {
          "name": "lodash",
          "content": {
            "../node_modules/lodash/lodash.js": 
            { "id": 243, "buildMeta": {} }
          }
        }
        
    4. 使用公共模块

      1. //index.html 模板手动引入
        <script src="./dll/lodash.js"></script>
        <script src="./dll/jquery.js"></script>
        // clean-webpack-plugin 不再删除dll文件夹和内容
        new CleanWebpackPlugin({
          cleanOnceBeforeBuildPatterns: ["**/*", "!dll", "!dll/*"],
        }),
        
      2. // 指定资源清单
        new webpack.DllReferencePlugin({
          manifest: require("./dll/jquery.manifest.json"),
        }),
        
      3. 如果指定了资源清单,在解析阶段会忽略掉(因为模块id一样,能够匹配到),变成直接导出全局变量

      4. 因为打包后产物里面没有lodash这两个变量,所以需要再模板里面手动引入

    5. 优缺点

      • 缺点
        • 引用关系复杂不适合
        • 第三方库体积小不适合
        • 操作比较繁琐,相互引用需要
      • 优点
        • 减少产物代码体积,提高编译效率
  • 自动分包

    • 通过配置一个分包策略,webpack会自动按照策略分包

    • 实际上 webpack 在内部使用 SplitChunksPlugin 进行分包,用于配置一些优化信息,其中 splitChunks 就是分包策略配置

    • 分包时,webpack开启了一个新的 chunk ,对分离的模块进行打包

    • 打包结果中,公共的部分被提取出来形成了一个单独的文件,它是新的chunk的产物

    • optimization: {
        splitChunks: {
          maxSize:60000, //超过该大小就会再次分包
          chunks:'async',
          // 默认是配置的异步chunk才自动分包
          // 改为 'all' 会自动提取公共代码生成新的chunk产物
          // initial 仅针对普通chunk应用分包策略
        },
      },
      
    • maxSize

      • 默认是30000
      • 可以配置包的最大字节数,如果超出的话会尽可能的分成多个包
      • 但是分包是以模块为基础,如果一个完整的模块超过了该体积,就做不到再次切割
      • 所以开启了这个配置还是会出现模块超过配置字符
      • 拆分之后总体积还是没变化,但是会出现多个传输请求
    • minChunks:

      • 默认是1
      • 配置被多少个chunk 引用才算重复引用,才开启分包
    • 两个同时存在的时,条件就是两个的并集

    • 分包缓存组

      • splitChunks: {
          cacheGroups:{
            // 属性名是缓存组名称,会影响到分包的chunk名
            // 属性值是缓存组的配置,缓存组继承所有的全局配置,也有自己特殊的配置
            vendors:{
              // 当匹配到相应模块时,将这些模块进行单独打包
        			test:/[\\/]node_modules[\\/]/, 
              // 缓存组优先级,优先级越高,优先进行处理,默认为0
              priority:-10
            },
            default :{
              minChunks:2,  //覆盖全局全局最小引用数
              priority:-20, 
              reuseExistingChunk:true // 重用已经被分离出去的chunk
            }
          }
        }
        
      • // style 分包 重复css 分包
        cacheGroups: {
          styles: {
            minSize: 0,
            test: /.css$/,
            minChunks: 2,
          },
        },
        
    • 原理

      • 检查每个chunk编译的结果

      • 根据分包策略,找到那些满足策略的模块

      • 根据分包策略,生成新的chunk打包这些模块(代码变化)

      • 把打包出去的模块从原始包中移除,并修正原始包代码

      • 在代码层面,有以下变动

        • 在分包的代码中,加入一个全局变量了类型为数组,其中包含公共模块的代码

        • 原始包的代码中,使用数组中的公共代码

        • index
          			./src/index.js
          			./node_modules/jquery/index.js
          			./node_modules/lodash/index.js
          			./src/common.css
          main
          			./src/main.js
          			./node_modules/jquery/index.js
          			./node_modules/lodash/index.js
          			./src/common.css
          // 根据策略查找需要分出去的模块 ( 引用关系,大小 )
          // 新建一个chunk 把重复的引用打包进去
          // 移除老的包里面的内容
          
  • 单模块体积优化

    • 代码压缩

      • 减少代码体积;破快代码可读性;提升破解成本

      • 使用terser

        • 简化命名,移除单模块直接的无效代码,单模块内容代码压缩

        • module.exports = {
            	// 是否要开启压缩
            optimization:{
              	minimize:true,
              	minimizer:[
                  new TerserPlugin(),
                  new OptimizeCSSAssetsPlugin()
                ]
            }
          }
          // npm i -D  optimize-css-assets-webpack-plugin
          // 压缩css代码
          // terser-webpack-plugin
          // 压缩js
          
        • 使用terser 打包完成之后,用不到的变量会直接去除

        • 无法访问到的代码(dce dead code) 会被删除

        • function sum(a,b) {
            return a + b;
            var c = 1;
            var b = 2;
            console.log(a + b)
          }
          console.log(sum(1,2))
          console.log(sum(1,2))
          console.log(sum(1,2))
          // 在函数里面return 后的代码会被删除
          
    • tree sharking

      • 移除模块之间的无效代码,移除不会用到的导出代码

      • webpack2开始支持,在生产环境自动启用

      • 为什么选择es6的模块导入语句

        • 导入导出语句只能是顶层语句
        • import的模块名只能是字符串常量
        • import绑定的变量是可变的
      • 在分析依赖时,webpack会保证代码正常运行,然后尽量tree shaking

      • 如果导出的是一个对象,由于js的动态语言特性,webpack不能分析出依赖信息

        • 在编写代码尽量使用

        • export xx 导出,而不是用 export default {xxx}
          
        • import { xxx } from 'xxx'
          //不推荐使用以下
          import xx from 'xxx'
          
      • 在依赖分析完毕后,webpack会根据每个模块每个导出是否被使用,标记其他导出为dead code,然后由压缩工具最终移除调dead code 代码

      • 对于一些使用commonjs导出的第三方库是无法使用的,可以搜索该库的es版本

      • 有些某些引用的情况下,tree shaking不会分析作用域引用,无法准确的标记dead code,可以使用 webpack-deep-scope-plugin 进行深度分析(个人项目,有风险)

      • 原理分析

        • 从依赖图里面收集所有的esm导出,分为export 和 export default

        • 遍历所有导出得到是否被其他对象使用

        • 最后由terser对标记未标记使用的导出进行清除

      • 副作用分析

        • 分析时认为对外不产生影响的代码认为是无副作用代码,但是只会简单分析,

        • const n = Math.random()
          // 会被视作副作用代码
          // 在package.json 配置,告诉webpack整个项目不存在副作用
          {
            "sideEffects":false,
             // 配置为数组 指定文件
             "sideEffects":[!src/common.js] //标记指定这个文件无副作用
          }
          
        • 一般第三库内部自己指定

      • css tree shaking

        • webpack 无法对css 完成tree shaking, 因为css 无法支持es6

        • 因此需要借助第三方插件完成

          • Purgecss-webpack-plugin

          • 通过和页面上使用的css选择器进行对比,他只会跟模板html对比,不会对比js使用css,需要配置多个js文件

          • new Purgecss({
              paths:[
                path.resolve(__dirname,'public/index.html'),
                // 对比js使用的选择器
                path.resolve(__direname,'src/index.js')
              ]
            })
            
    • 模块懒加载

      • 使用require 是可以实现懒加载,但是无法被tree shaking 处理

      • 使用import 动态引入

        • const btn = document.querySelector('button')
          btn.onclick =  async function () {
          	//import() 是es6的草案  但是webpack支持
            // 浏览器会使用jsonp的方式去远程去读取一个js模块
            // import() 会返回一个promise ( * as obj)
            //  /*webpackChunkName:"lodash"*/  打包后的名称
            const { chunk } = await import(/*webpackChunkName:"lodash"*/ "lodash-es");
            const result = chunk([12,3,4,5,6],2)
            console.log(result)
          }
          
        • 实现原理有点类似于分包,webpack会创建一个全局变量收集这些异步模块,然后使用一个单独的chunk去加载模块,完成后写入这个全局变量,其他模块只需要引入这个全局变量的模块即可

      • 动态加载无法使用tree shaking,因为不是静态导入

        • // 通过使用本地模块引入后再动态引入,即可实现tree shaking
          // utils.js
          export { chunk } from 'lodash-es'
          // index.js
          const {chunk} = await import('./utils.js')
          
    • gzip 传输文件压缩

      • 在传统b/s架构上,一般是浏览器传输数据的请求头携带想要接收的压缩后的数据

        • Accept-Encoding:gzip,defalte,br
          
          
      • 服务器返回的时候需要设置请求头告诉浏览器本次传输的信息通过gzip压缩后的,否则浏览器会当做普通的文本传输处理

      • webpack 提供的gzip压缩是相当于预处理一样,内置的插件 compression-webpack-plugin 会对打包结果进行预压缩,可以移除服务器的压缩时间

      • Webpack 可以让产物直接gzip化,服务端获取到的直接是压缩后文件

        • new Compression-webpack-plugin
          
        • 他会生成对应的bundle 的gz 格式的压缩文件

      • 在服务器支持gzip的情况,当浏览器请求例如main,js的时候, 在带有请求头的情况下服务器会优先获取 main.js.gz, 如果读取到之后会直接返回该文件

其他优化

ESlint
  • 通常搭配编辑器使用,在vscode中安装eslint,该工具会自动检查工程中的js文件

  • 检查工作由eslint库完成,如果当前工程没有,咋会去全局找,如果没有找到那么则无法完成检查

  • 检查的依据是 eslint 配置文件 .eslintrc ,如果找不到工程中的配置文件,也无法完成检查

  • 配置项除了新建文件也可以放在package.json里面

  • 配置项

    • env 配置代码的运行环境

      • browser 代码是否运行在浏览器
      • Es6: 是否启用全局的es6 api,例如promise
    • parserOptions: 指定eslint 对那些语法的支持

      • ecmaVersion: 支持的es语法
      • sourceType
        • Script: 传统脚本
        • module: 模块化脚本
    • parser: 配置不同语言的解析器

      • eslint 的工作原理是将代码进行解析,然后按照规则分析
      • eslint 默认使用 espree 做为其解析器,还可以指定其他的解析
    • globals: 配置额外的全局变量

    • extends: 规则继承

      • 默认会继承recommend,也就是推荐的
    • ignoreFiles: 不需要对那些文件检查

      • 新建一个文件 .eslintignore
      • 需要放在最外层的项目根目录
    • rules: 规则集合

      • 每条规则仅影响某个方面的代码风格

      • 每条规则都有以下几个取值

        • off 或者 0 或者 false 表示关闭该规则的检查
        • warn 或者 1 或者 true 警告,不会导致程序退出
        • error 或者 2 : 错误,当被触发的时候,程序会退出
      • 可以在配置文件配置 也可以使用注释(仅对当前文件生效)

        • /* eslint eqeqeq: "off",curly:"error" */
          
      • Eslint 的规则有些支持自动修复,可以使用命令行修复

        • npx eslint --fix src/index.j
          
        • 一般在编辑器安装插件支持自动修复

    • plugins: 第三方规则的集合,使用一些框架方提供的lint支持

    • 原理

      • 读取配置(extends,rules等)
      • 加载plugin
      • 读取parser配置,生成的ast
      • 遍历ast(递归收集,递一次,归一次),收集ast的节点放入一个队列
        • 深度优先进行注册和对特点节点的代码修改
        • 递归结束回来的时候 收集最新的ast节点
      • 遍历eslint配置的规则,找到每条规则对应的rule对象(在eslintrc 配置的只是规则的开关,会根据每条规则的配置找到在ellint内部定义的规则对象具体实现)
      • 并且为每个规则对应的ast节点添加监听函数,以便后续触发规则校验
      • 遍历节点队列,触发监听函数
      • 收集所以eslint触发的问题
      • 结果二次处理(根据注释过滤)
Bundle analyzer
  • Webpack-bundle-analyzer 这个plugin

  • const WebpackBundleAnalyzer = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
    
  • 打包之后,生成一个本地服务器,呈现一个图形化的html展示模块文件信息

打包案例

不确定的动态依赖

const name = document.getElementById('text').value

// 这个情况会进入webpack会进入到里面分析,因为webpack执行代码只读代码,不会管条件判断
// 他会将整个文件进行ast分析,检查那些是依赖语句,然后加载对应的模块
if(Math.random() < 0.5)  {
  //但是如果是这种进行拼接的路径
  //webpack 无法判断具体的模块,所以会将该目录下所有模块打包进去
 	const module = require('./utils/' + name)  
  // 以上这句话会被转换成下面这个  会读取整个目录下的模块
  // 参数1. 指定打包目录
  // 参数2. 是否递归查询子目录,
  // 参数3. 正则表达式,满足匹配规则才会加到打包结果
  require.context('./utils',true,/.js$/)
  
  // 如果连路径都不写  则无法进行打包
  const module = require( name) 
}

// 产物分析
// 会读取所有依赖并且生成一个map 收集所有模块路径和别名
var map = {
  	"./":"./src/utils/index,js",
    ".b":"./src/utils/b,js",
}
const module = require.context("./utils");
console.log(module.keys());

// 使用require.context 能拿到打包后模块信息
// module.keys()  会输出 生成的map表里面的 key

require.context 一般用于批量导出和导入,不再需要手动一个个引入和导出,让webpack帮我们全部打包进入

// ./src/uitls/index.js
const context = require.context("./", true, /.js$/);

for (const key of context.keys()) {
  if (key !== "./index.js") {
    let filename = key.substr(2);
    filename = filename.substr(0, filename.length - 3);
    exports[filename] = context.resolve(key);
  }
}
// ./src/index.js
const utils = require("./utils/index");

console.log(utils);

搭建多页应用

// 定义配置文件规定入口和出口,模块配置
// ./pages.js
module.exports = {
  	index:{
      js:"./src/pages/index/index.js"
      html:"./src/pages/index/index.html",
      output:"index"
    },
  	main:{
      js:"./src/pages/index/main.js"
      html:"./src/pages/index/main.html",
      output:"main"
    },
}

// webpack.config.js
const pages = require('./pages')

// 读取配置 生成entry
const getEntry = () => {
  	let _entry = {}
    for (const key of pages) {
      _entry[key] :pages[key].js
    }
    return _entry
}	
// 生成多个html模板
const getHtmlPlugins = () => {
  const _htmlPlugins = []
  for  (const key of pages) { 
  		_htmlPlugins.push(
      	new HtmlWebpackPlugin({
						template:path.resolve(__direname,pages[key].html),
            chunks:[key],
          	filename:pages[key].out
        })
      )
  }
  return _htmlPlugins
}

module.exports = {
  	entry:getEntry(),
  plugins:[
    ...getHtmlPlugins()
  ]
}

搭建vue单页应用

vue项目需要使用.vue文件,需要单独使用vue-loader处理vue文件,vue-loader内部会调用其他的loader分析css,template, 内部依赖vue-template-complier解析template

npm i -D vue-loader vue vue-template-complier babel-preset-vue
// .babelrc
module.exports = {
  presets:['vue']
}
// webpack.config.js
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
  resolve:{
    	extensions:['.js','.vue']
  },
 	module:{
    rules:[
      {
        test:/.vue$/,
        use:'vue-loader'
      },
      {
         test:/.js$/,
        use:'babel-loader'
      }
    ]
  },
  plguins:[
    new vueLoaderPlugin()
  ]
}

搭建react应用

由于react本身就可以使用jsx 文件,所以可以使用babel进行解析

npm i -D @babel/preset-react react react-dom
// .babelrc
module.exports = {
  presets:['@babel/preset-react']
}
// webpack.config.js
module.exports = {
  resolve:{
    	extensions:['.js','.jsx']
  },
  node:false, // 不需要webpack模拟node 全局变量,使用node自己的  例如__dirname
 	module:{
    rules:[
      {
         test:/.(js) | (jsx)$/,
        	use:'babel-loader'
      }
    ]
  },
}

搭建node应用

node无法使用es模块化和commonjs混合, 使用webpack可以解决这一点

使用node + webpack 打包,服务器直接使用打包后的产物,服务端就不需要再次下载依赖

// package.json
{
  "script":{
    "dev":"nodemon --watch src --exec 'npm run dev:build && npm run dev:exec'",
    "dev:build":"cross-env NODE_ENV=development webpack"
    "dev:exec":"node dist/index",
    "build":"cross-env NODE_ENV=production webpack"
  }
}
// webpack.config.js
module.exports = {
  	target:"node", // 不设置的话 默认是web  会找不到node内置模块
}

全栈应用

  • 通过webpack把服务端代码打包后放入dist目录,client端通过修改该项目的outdir 使其产物放入dist,那么我们的通过访问服务就能拿到我们客户端的页面产物

  • 通过配置一个命令 将产物打包 和 服务打包同时运行

  • 在客户端和服务端都可以引用common里面的代码,因为最后会被webpack打包放入

  • project

    • server / - 服务端代码
    • client / - 客户端代码
    • dist/ - 最终产物
    • common/ - 公用代码
    • Webpack.config.js

webpack5更新了什么

分为看不见的优化 和 看得见的变化,看得见的变化 分为 小的变化 和 最大的一个变化 ( 联邦模块 )

小的变化

  • Webpack5 清除输出目录不再需要第三方插件,而是通过配置解决

    • module.exports = {
      	output:{
          clean:true
        }
      }
      
  • Top-level-await

    • webpack5允许在模块的顶层代码中直接使用await

    • // 开启配置 但是 只有esm 才能使用
      module.exports = {
      	experiments: {
          topLevelAwait: true,
        },
      } 
      // a.js
      const res = await (() => Promise.res(1)());
      
    • 打包后的代码会自动放入一个函数并且使用async 关键字

  • 打包优化

    • 智能tree-shaking判断
  • 打包缓存开箱即用

    • 在webpack4中 需要使用 cache-loader 缓存打包结果以优化之后的打包性能

    • 而在webpack5中,默认就已经开启了打包缓存,无需再安装 cache-loader

      • 默认情况下,webpack5是将模块的打包结果缓存到内存中,可以通过cache配置进行更改
    • const path = require('path')
      module.exports = {
        	cache:{
            type:"filesystem", // 缓存类型, 支持:memory,filesystem
            // 缓存目录,仅类型为 filesystem 有效
            cacheDirectory:path.resolve(__dirname,"node_modules/.cache/webpack")
          }
      }
      
  • 资源模块

    • 在webpack4中针对资源文件我们通常使用 file-loader,url-loader,raw-loader进行处理,由于大部分前端项目都会用到资源型文件,因此 webpack5 原生支持了资源型模块

    • output:{
         assetModuleFilename: 'images/[hash][ext][query]'
      },
      module:{
        rules:[
          {
          	test:/.$png/,
          	type:"asset/resource",// 作用类似于file-loader
           	//type:"asset/inline",//作用类似于 url-loader
           	//type:"asset/resource",// 作用类似于raw-loader
          },
          {
            test:/.gif/,
            type:"aseet", // 作用类似于url-loader 
            generator:{
              filename:"gif/[hash:5][ext]", // 该配置会覆盖 assetModuleFile
            },
            parser:{
             	dataUrlCondition:{
                maxSize: 4 * 1024 // 4kb以下使用data url 大于4kd使用文件
              } 
            }
          }
        ]
      }
      

模块联邦

解决微前端的架构问题,把项目中的某个区域或者功能模块做为单独的项目开发

  • 如何避免公共模块重复打包
  • 如何将某个项目中一部分模块分享出去,同时还要避免重复打包
  • 如何管理依赖的不同版本
  • 如何更新模块
// 使用webpack提供的内置插件 实现模块联邦
const moduleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
// 暴露模块
plugins:[
     new moduleFederationPlugin({
      // 模块联邦的名称
      // 该配置将成为一个全局变量,通过改变量可以获取当前联邦的所有暴露模块
      name: "home",
      // 模块联邦生成的文件名,全部变量会置入到该文件中
      filename: "home-entry.js",
      // 模块联邦暴露的所有模块
      exposes: {
        // key 相当于模块联邦的路径
        // 这里的 路径将决定该模块的访问路径
        // value 模块的具体路径
        "./now": "./src/now.js",
      },
    }),
]
// index.js 导入
import 'home/abc' 
 // 远程引入其他模块 
new moduleFederationPlugin({
  remotes: {
    // key 自定义远程暴露模块的联邦名 例如abc 则使用 import 'abc/模块名'
    // value 模块联邦名@模块联邦访问地址
    // 远程访问时,将从下面的地址加载
    // 写多个加载多个模块
    home:"home@http://localhost:8080/home-entry.js"
  },
}),
// 出现两个项目都有引用的项目时,会重复请求这些模块
// 配置共享模块
new moduleFederationPlugin({
  shared: {
    jquery:{
      singleton:true, // 全局唯一
    }
  },
}),