熟练掌握webpack

490 阅读18分钟

关于webpack零零散散总结了很长一段时间了,今天才来梳理成文,里面主要包含了如下知识点:

  • webpack怎么配置(区分打包环境、启动热更新、动态导入、模块解析等)
  • source map包含哪些?开发环境和生产环境怎么选择?
  • tree-shaking怎么配置
  • 缓存机制设置等
  • webpack4和webpack5的区别
  • 热更新HRM原理
  • webpack构建流程
  • webpack打包文件详解

集中其他相关篇幅:

  1. loader和plugin专题 (juejin.cn)
  • plugin和loader的区别;
  • 怎么写plugin?其原理;
  • 怎么写loader?其原理;
  • 常用的plugin和loader;
  1. 10多种方案从webpack角度考虑前端优化 (juejin.cn)

希望对大家有帮助,同时发现错误欢迎指正,谢谢!!!

一、webpack进阶配置

细节可参考官网,以下是一些重要点提取出来 概念 | webpack 中文网 (webpackjs.com)

资源模块(Asset Modules,webpack5新引入的的)

功能:

  • 是一种 模块类型,它允许使用资源文件,而无需配置额外loader。
  • 资源文件:字体、图片、图标、HTML。。。
  • 不用file-loader、url-loader也能加载图片和字体。

webpack4操作:

  • raw-loader :将文件导入为字符串
  • file-loader: 将文件发送到输出目录
  • url-loader:将文件发送到输出目录,或转为Data URI(base64)内联到bundle中

webpack5操作:

  • asset/resource:发送一个单独的文件并导出URL(之前通过使用file-loader实现)
  • asset/inline:导出一个资源的data URI (之前通过使用url-loader实现)
  • asset/source:导出资源的源代码(之前通过使用raw-loader 实现)
  • asset:在导出一个data URI 和发送一个单独的文件之间自动选择(url-loader)

Webpack Dev Serve

作用:发布web服务,提高开发效率

webpack4:webpack-dev-server ...
webpack5:webpack server...

webpack4热更新:

hot:true

webpack5热更新:

 webpack5新加的
liveReload:true  (不能再使用hot)
target: 'web'  (热更新只适用于web相关的targets)

proxy配置接口代理

changeOrigin:true

区分打包环境

  1. 通过环境变量区分
  • webpack --env.production
  • webpack.config.js中判断env
  1. 通过配置文件区分
  • webpack.dev.conf.js
  • webpack.prod.conf.js
  • webpack.base.conf.js (公共配置)

webpack-merge 将多个配置合并在一起

命令行中设置环境变量

  • webpack4:webpack --env.production
  • webpack5:webpack --env production (没有点)

webpack.config.js

  • 读取环境变量env.production
  • 根据环境变量指定不同的配置
webpack.config.js 

module.exports = (env, argv) => {
    cosnt config = {mode:'development'}
    if(env production){
        config.mode = 'production'
        ...
    }
    return config
}

提取公共模块

optimization: {
    splitChunks: {
        chunks:'all'
    }
}

动态导入

懒加载:默认不加载,事件触发后才加载。

webpackChunkName: '加载名称'

document.getElementId('btn').onclick = function(){
//import 启动懒加载
//webpackChunkName: 'desc'  指定懒加载的文件名称
//webpackPrefetch: true  启动预加载
    import(/*webpackChunkName: 'desc', webpackPrefetch: true */'test').then(()=>{
        console.lo('此处才加载了test文件,才能调用它文件中的东西')
    })
}

预加载:先等待其他资源加载,浏览器空闲时,再加载 webpackPrefetch: true 缺点:在移动端有兼容性问题

源码映射(source map)

映射模式(devtool的值)

  • 不同映射模式的报错定位效果和打包执行速度不同
  • webpack4中,一共有13种不同的映射模式
  • webpack5中,一共有26种不同的映射模式

webpack5中的命名更新严格

^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$

模式描述
cheap只定位错误,不定位报错列
hidden生成.map文件,但是.js的末尾没有关联.map,报错后需手动关联,然后定位报错
inline不生成.map文件,映射以base64-VLQs的形式添加到.js最后
eval不生成.map文件,映射信息追加到eval函数的最后,来关联处理前后的对应关系
module不但映射工程师自己写的代码,还支持对loader和第三方模块的映射
nosources生成.map中不包含sourceContent,定位错误时看不到源码(更安全)

webpack4的13个常见模式

image.png

webpack5 有26种模式虽然更丰富,但是关键词不变,多了几个排列组合, 虽然模式多,但是很多模式现在还没效果,webpack5更新内容多,是为了以后做铺垫的。

image.png

如何选择合适的映射模式(这是个人建议,但是不绝对)

  • 开发环境:eval-cheap-module-source-map
  • 生产环境:none | nosources-source-map

如果想在生成环境也想看到错误定位,也可以在生成环境使用 cheap-module-source-map

这样选择的原因:

  • eval的rebuild速度快,因此我们可以在本地环境中增加eval属性。
  • 使用eval-source-map会使打包后的文件太大,因此在生产环境中不会使用

tree-shaking

Tree-shaking 较早由 Rich_Harris 的 rollup 实现,后来,webpack2 也引入了tree-shaking 的功能,本质是消除无用的JavaScript代码,(支持CSS消除吗?我觉得不支持,CSS又不是ES Modules规范)。

tree-shaking原理:

将所有代码打包到一个作用域下,然后遍历所有作用域,去除没使用的作用域(webpack原理:遍历所有引入模块,把它们打包成一个文件,在这个过程中,就知道哪些export的模块被使用到)

基于ES6的静态引用,treeshaking通过扫描所有ES6的export,找出被import的内容并添加到最终代码中。

注意点:

  • 使用ES Modules规范的模块,才能执行tree-shaking。因为tree-shaking依赖于ES Modules的静态语法分析
  • 不能删除立即执行函数,避免使用IFEE
  • 如果使用第三方的模块,可以尝试直接从文件路劲引用的方式使用

Tree shaking为什么用ES6:

因为ES6模块的出现,ES6模块依赖关系是确定的,和运行时的状态无关可以进行可靠的静态分析,这样方便甩掉重复代码,这就是Tree shaking的基础。

如何使用:

  • 生产模式:tree-shaking会自动开启
  • 开发模式:
  1. usedExports
  2. sideEffects

1. usedExports

optimization: {
    //标记未被使用的代码,打包后的未使用的代码会带上注释/*unused harmony export xxxx*/
    usedExports:true,
    //删除unused harmony export xxxx标记的代码
    minimize:true,
    //terser-webpack-plugin压缩插件:webpack4需要单独安装,webpack5无需安装,但需要引入
    minimizer: [new TerserPlugin()]
}

optimization.usedExports (标记没用的代码) ,打包后的未使用的代码会带上注释/*unused harmony export xxxx*/

optimization.minimize:true (删除unused harmony export xxxx标记的代码)

terser-webpack-plugin (去掉项目多余的debuger)
webpack4需要单独安装terser-webpack-plugin (webpack5无需安装,但需要引入)

Tree Shaking与 Source Map存在兼容性问题:

Tree Shaking仅仅支持 devtool: source-map | inline-source-map | hidden-source-map | nosources-source-map;

因为eval模式,将JS输出为字符串 (不是ES module规范),导致Tree Shaking失效

2. sideEffects 副作用

无副作用:如果一个模块单纯的导入导出变量,那它就无副作用

有副作用:如果一个模块还修改其他模块或者全局的一些东西,就有副作用

  • 修改全局变量
  • 在原型上扩展方法
  • css的引入(比如作用于html)

sideEffects的作用:把未使用但无副作用的模块一并删除

对于没有副作用的模块,未使用代码不会被打包(相当于压缩了输出内容)

开启副作用:

optimization: {
    sideEffects:true
}

标识代码是否有副作用(在package.json中设置sideEffects):

  • true: 所有代码都有副作用
  • false: 所有代码都没有副作用(告诉webpack可以安全地删除未用的exports)
  • 数组:(告诉webpack哪些模块有副作用,不删除)
//比如 为true
  sideEffects:true
//比如 为数组
   sideEffects: ['.src/test.js','*.css']

Webpack Tree shaking 深入探究 (juejin.cn)

缓存机制

babel缓存:

  • cacheDirectory:true 第二次构建时,会读取之前的缓存

文件资源缓存:

  • 如果代码在缓存期内,代码更新后看不到实时效果
  • 方案:将代码文件名称,设置为哈希名称,名称发生变化时,就加载最新内容

webpack哈希值:

  • hash 每次webpack打包生成的hash值
  • chunkhash 不同chunk的hash值不同,同一次打包可能生成不同的chunk
  • contenthash 不同内容的hash值不同,同一个chunk中可能有不同的内容
//8代表hash名称位数
[name].[contenthash:8].js
[name].[contenthash:8].css

模块解析resolve

  • 配置模块解析的规则
  • alias:配置模块加载的路径别名
alias:{'@':resolve('src')}
  • extensions:引入模块时,可以省略哪些后缀
extensions:['js','json']

还有其他,可看官网

排除打包externals

  • 排除打包依赖,防止对某个依赖项进行打包
  • 一般,一些成熟的第三方库,是不需要打包的,比如jquery,可以直接引入CDN

模块联邦

Webpack5 模块联邦让 Webpack 达到了线上 Runtime 的效果,让代码直接在项目间利用 CDN 直接共享,不再需要本地安装 Npm 包、构建再发布。

const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

module.exports = {
  // other webpack configs...
  plugins: [
    new ModuleFederationPlugin({
      name: "app_one_remote",
      remotes: {
        app_two: "app_two_remote",
        app_three: "app_three_remote"
      },
      exposes: {
        AppContainer: "./src/App"
      },
      shared: ["react", "react-dom", "react-router-dom"]
    }),
    new HtmlWebpackPlugin({
      template: "./public/index.html",
      chunks: ["main"]
    })
  ]
};
调用
import { Search } from "app_two/Search";

ModuleFederationPlugin插件的几个重要参数:

  1. name当前应用名称,需要全局唯一
  2. remotes可以将其他项目的name映射到当前项目中
  3. exposes表示导出的模块,只有在此申明的模块才可以作为远程依赖被使用
  4. shared是非常重要的参数,可以让远程加载的模块对应依赖改为使用本地项目的React或ReactDOM

二、webpack高阶详解知识

webpack4和webpack5的区别

  1. 热更新 webpack4:webpack-dev-server ...
    webpack5:webpack server...

webpack4热更新:

hot:true

webpack5热更新:

 webpack5新加的
liveReload:true  (不能再使用hot)
target: 'web'  (热更新只适用于web相关的targets)
  1. source map模式写法不一样

webpack4的13个常见模式

webpack5的26个常见模式

具体查看官网

  1. sideEffects 使用 usedExports
optimization: {
    //标记未被使用的代码,打包后的未使用的代码会带上注释/*unused harmony export xxxx*/
    usedExports:true,
    //删除unused harmony export xxxx标记的代码
    minimize:true,
    //terser-webpack-plugin压缩插件:webpack4需要单独安装,webpack5无需安装,但需要引入
    minimizer: [new TerserPlugin()]
}

webpack4需要单独安装terser-webpack-plugin (webpack5无需安装,但需要引入使用)

  1. webpack5内置缓存功能 Webpack5 内置缓存方案探索_追逐丶的博客-CSDN博客_webpack5 缓存

webpack4的缓存方案:cache-loaderdll
webpack5的缓存方案:`

  • IdleFileCachePlugin:持久化到本地磁盘
  • MemoryCachePlugin:持久化到内存

webpack5的内置缓存方案无论从性能上还是安全性上都要好于cache-loader

  • 性能上:由于所以被webpack处理的模块都会被缓存,缓存的覆盖率要高的多
  • 安全上:由于cache-loader使用了基于mtime的缓存验证机制,导致在CI环境中缓存经常会失效,但是Webpack5改用了基于文件内容etag的缓存验证机制,解决了这个问题。具体使用的Webpack5配置官网已经给出了。
  1. webpack5增加了模块联邦

  2. 关闭url-loader默认的ES Modules规范,强制url-loader使用CommonJS规范进行打包

webpack4中只需要url-loader配置esModule:false

webpack5需要html-loader和url-loader都配置esModule:false

热更新(HRM)原理

Webpack HMR 原理解析 - 知乎 (zhihu.com)

总结主要几点:

  1. 浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端
  2. 通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码

优化 Webpack 的构建速度?

Webpack5 性能优化 - 优化构建速度 - 云+社区 - 腾讯云 (tencent.com)

如何对bundle体积进行监控和分析?

  1. VSCode 中有一个插件 import cost 可以帮助我们对引入模块的大小进行实时监测

  2. webpack-bundle-analyzer生成 bundle 的模块组成图,显示所占体积

webpack构建流程(打包原理终版)

webpack打包后产生的文件是个立即自执行函数,自执行函数的入参是个数组,这个数组包含了所有的模块,包裹在函数中。

Webpack 实际上为每个模块创造了一个可以导出和导入的环境,本质上并没有修改 代码的执行逻辑,代码执行顺序与模块加载顺序也完全一致。

从webpack配置文件中配置的entry入口文件开始解析文件构建AST语法树,找出每个文件所依赖的文件,递归下去,最终打包成一个文件。

简单webpack打包原理:

  1. 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler
  2. 编译:从 Entry 出发,针对每个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译处理
  3. 输出:将编译后的 Module 组合成 Chunk,将 Chunk转换成文件,输出到文件系统中
/**
 * 初始化准备工作,获取模块内容,分析模块,收集依赖,通过babel把es6转化为es5形成更AST,递归获取所有依赖
 * 
 * 初始化准备工作,获取模块内容
 * 分析模块:将获取到的模块内容 解析成AST语法树( @babel/parser),AST是个引用路劲
 * 收集依赖:将用import语句引入的文件路径收集起来。我们将收集起来的路径放到deps里,将file目录路径跟获得的value值拼接成相对路劲放在deps里(@babel/traverse)
 * ES6转成ES5(AST): 把获得的ES6的AST转化成ES5,这样获取到代码 (@babel/core @babel/preset-env)
 * 递归获取所有依赖:这个对象包括该模块的路径(file),该模块的依赖(deps),该模块转化成es5的代码;
 * 并且得处理成:以文件的路径为key,{code,deps}为值的形式存储
 * 整合代码:目的就是要生成一个bundle.js文件,也就是打包后的一个文件。其实思路很简单,就是把index.js的内容和它的依赖模块整合起来。然后把代码写到一个新建的js文件。
 * 形成可执行的文件:放到立即执行函数里
 * 

初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
确定入口:根据配置中的 entry 找出所有的入口文件
编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统


最终表达:
 
 webpack的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:根据配置文件初始化参数,加载所有配置的插件,执行对象的 run 方法开始执行编译;
 * 与此同时,根据配置确定该入口文件,从入口文件出发,调用所有配置的 Loader 对模块进行翻译(翻译包括css转化成js、es6转化成es5),并找出该模块依赖的模块,递归处理;
 * 再根据输出的源码、文件依赖关系,最终打包成一个文件
 */

webpack打包文件详解

如果该模块是es6,会在导出对象中加入__esModule = true CommonJS加载ES Module

__webpack_require__是引入输入模块,并返回模块的导出对象,最开始是引入入口文件,后面怎么引入其他依赖文件的呢?

是一个立即执行函数,参数是依赖文件

异步加载的chunk最终还是同步加载,它被添加到 modules 对象中,这样就可通过 modules[moduleId].call(module.exports, module, module.exports, webpack_require) 来同步加载chunk,也就是 foo.bundle.js(异步加载的模块) 中第一个 then 执行的内容,传入模块的路径,使用 webpack_require 进行同步加载。

懒加载代码块:原理使用script标签(可以理解为jsonp原理?),返回Promise.all(promises),再在then中使用它

异步就是:只有import的时候,会加载它的内容

ES6模块:会块标识为 ES Module,并且将函数内容定义挂在 default 上

打包最终生成的文件:最终打包出的是一个自执行函数;

自执行函数入参是一个对象modules,其key为打包的模块文件的路径,对应的value为一个函数,其内部为模块文件定义的内容

自执行函数体返回 __webpack_require__(__webpack_require__.s = "./src/index.js") 这段代码,此处为加载入口模块并返回模块的导出对象。

function __webpack_require__(moduleId) {
}

其中包含缓存机制

var module = installedModules[moduleId] = {
  i: moduleId,
  l: false,
  exports: {}
};

模块间的引用,webpack怎么打包的

  • CommonJS 加载 CommonJS
 ({
  "./src/foo.js":
  (function(module, exports) {
    module.exports = 'foo';
  }),
  "./src/index.js":
  (function(module, exports, __webpack_require__) {
    const foo = __webpack_require__("./src/foo.js");
    console.log(foo)
  })
})
  • CommonJS 加载 ES module
({
  "./src/foo.js":
  (function(module, __webpack_exports__, __webpack_require__) {
    __webpack_require__.r(__webpack_exports__); //将传入的对象标识上__esModule=true,即表明该模块为es6模块
    __webpack_exports__["default"] = ('foo'); //将模块的内容挂在__webpack_exports__的default属性上
  }),
  "./src/index.js":
  (function(module, exports, __webpack_require__) {
    const foo = __webpack_require__("./src/foo.js");
    console.log(foo)
  })
})
  • ES module 加载 ES module
({
  "./src/foo.js":
  (function(module, __webpack_exports__, __webpack_require__) {
    __webpack_require__.r(__webpack_exports__);
    __webpack_exports__["default"] = ('foo');         
  }),
  "./src/index.js":
  (function(module, __webpack_exports__, __webpack_require__) {
    __webpack_require__.r(__webpack_exports__);
    var _foo_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/foo.js");
    console.log(_foo_js__WEBPACK_IMPORTED_MODULE_0__["default"])
    //_foo_js__WEBPACK_IMPORTED_MODULE_0__用来接收导入的文件,并通过default属性获取到文件的默认导出内容
  })
})

webpack_require.n则是用于获取模块的默认导出对象,兼容 CommonJS 和 ES module 两种方式。

  • ES module 加载 CommonJS
({
  "./src/foo.js":
  (function(module, exports) {
    module.exports = 'foo';
  }),
  "./src/index.js":
  (function(module, __webpack_exports__, __webpack_require__) {
    __webpack_require__.r(__webpack_exports__);
    var _foo_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/foo.js");
    var _foo_js__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_foo_js__WEBPACK_IMPORTED_MODULE_0__);
    console.log(_foo_js__WEBPACK_IMPORTED_MODULE_0___default.a)
  })
})

当入口文件index.js以es module的方式加载遵循commonjs规范的foo.js时,通过__webpack_require__加载传入的模块,将得到的模块_foo_js__WEBPACK_IMPORTED_MODULE_0__再传入__webpack_require__.n方法获取到该模块的默认导出对象。因为foo.js中的内容是通过export导出,而非export default导出。因此foo被挂在了default的一个a属性上。

异步按需加载

懒加载代码块:原理使用script标签(可以理解为jsonp原理?),返回Promise.all(promises),再在then中使用它

异步就是:只有import的时候,会加载它的内容

 /**
   * 该对象用于存储已经加载和正在加载中的chunks
   * undefined:表示chunk未加载
   * null:表示chunk预加载 / 预获取
   * Promise:表示chunk正在加载中
   * 0: 表示chunk已经加载了
   */
  var installedChunks = {
    "index": 0, // 默认入口模块已经加载完毕
  };

webpack 打包文件分析(上) (juejin.cn)

webpack 打包文件分析(下) (juejin.cn)

AST

从babel讲到AST (juejin.cn)

AST 抽象语法树 (juejin.cn)

从代码生成AST的关键:词法分析和语法分析

github.com/CodeLittleP…
github.com/jamiebuilds…

我开发过的plugin

  1. 很多需求涉及到时间,但是因为时间有兼容性,写了plugin统一处理date格式。
  2. 图片资源路径统一处理:提测后(因为本地图片调试方便,也方便更改),上传本地图片到阿里云,本地图片替换成cdn链接图片,上传完之后进行删除,防止项目文件太多,减少项目总体积。
    思路:获取本地文件,取出文件路径,读出文件流,上传到oss,得到链接,然后进行替换

可参考loader和plugin专题 (juejin.cn)

我开发过的loader

  1. 兼容以前自适应方式,改了之前的计算方式

我的总结

webpack 是一个模块打包工具

根据业务需要需要进行不同的得配置,当然配置好它,可以进行一些优化,比如减少包的大小、提高打包速度、减少代码中重复写代码、分析文件大小 (具体等着面试官来问对吧?)

webpack构建流程最终表达:

webpack的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:根据配置文件初始化参数,加载所有配置的插件,执行对象的 run 方法开始执行编译;
与此同时,根据配置确定该入口文件,从入口文件出发,调用所有配置的 Loader 对模块进行翻译(翻译包括css转化成js、es6转化成es5),并找出该模块依赖的模块,递归处理;
再根据输出的源码、文件依赖关系,最终打包成一个文件。

juejin.im/post/684490…

juejin.im/post/684490… (webpack)

juejin.im/entry/68449… (webpack配置)

www.cnblogs.com/HYZhou2018/… (优化)

juejin.im/post/685457… (源码分析)

juejin.im/post/685457… (源码分析)

www.yuque.com/yijiangxili… (面试总结)

Webpack从入门到精通-进阶篇 (juejin.cn)

尚硅谷前端Webpack5教程(高级进阶篇)_哔哩哔哩_bilibili

Plugins | webpack 中文网 (webpackjs.com)