Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Webpack #57

Open
wuyanqian0503 opened this issue Jun 28, 2021 · 8 comments
Open

Webpack #57

wuyanqian0503 opened this issue Jun 28, 2021 · 8 comments

Comments

@wuyanqian0503
Copy link
Owner

wuyanqian0503 commented Jun 28, 2021

webpack 是什么?

webpack 是一个现代 JavaScript 应用程序的静态模块打包器,当 webpack 处理应用程序时,会递归构建一个依赖关系图,其中包含应用程序需要的每个模块,然后将这些模块打包成一个或多个 bundle。

👆🏻上面是官网上的说明,具体到应用场景中,webpack有如下的作用:

  1. 模块打包。可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。利用打包我们就可以在开发的时候根据我们自己的业务自由划分文件模块,保证项目结构的清晰和可读性。
  2. 编译兼容。在前端的“上古时期”,手写一堆浏览器兼容代码一直是令前端工程师头皮发麻的事情,而在今天这个问题被大大的弱化了,通过webpack的Loader机制,不仅仅可以帮助我们对代码做polyfill,还可以编译转换诸如.less, .vue, .jsx这类在浏览器无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提高开发效率。
  3. 能力扩展。通过webpack的Plugin机制,我们在实现模块化打包和编译兼容的基础上,可以进一步实现诸如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。

webpack 的核心概念

  1. entry: 入口
  2. output: 输出
  3. loader: 模块转换器,用于把模块原内容按照需求转换成新内容
  4. 插件(plugins): 扩展插件,在webpack构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要做的事情

模块打包运行原理

如果面试官问你Webpack是如何把这些模块合并到一起,并且保证其正常工作的,你是否了解呢?
首先我们应该简单了解一下webpack的整个打包流程:

1、读取webpack的配置参数(从[email protected]版本开始,就可以不需要配置文件了,有默认配置项);
2、启动webpack,创建Compiler对象并开始解析项目;
3、从**入口文件(entry)**开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树
4、对不同文件类型的依赖模块文件使用对应的Loader进行编译,最终转为Javascript文件;
5、整个过程中webpack会通过发布订阅模式,向外抛出一些hooks,而webpack的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的。

其中文件的解析与构建是一个比较复杂的过程,在webpack源码中主要依赖于compiler和compilation两个核心对象实现。

  • compiler:compiler对象是一个全局单例,他负责把控整个webpack打包的构建流程。
  • compilation:compilation对象是每一次构建的上下文对象,它包含了当次构建所需要的所有信息,每次热更新和重新构建,compiler都会重新生成一个新的compilation对象,负责此次更新的构建过程。

而每个模块间的依赖关系,则依赖于AST语法树。每个模块文件在通过Loader解析完成之后,会通过acorn库生成模块代码的AST语法树,通过语法树就可以分析这个模块是否还有依赖的模块,进而继续循环执行下一个模块的编译解析。

最终Webpack打包出来的bundle文件是一个IIFE的执行函数(立即执行函数)。

**立即执行函数:**一个匿名的立即执行的函数,由于其匿名函数的关系,在其执行后会立即被释放,所以不会污染全局对象。

和webpack4相比,webpack5打包出来的bundle做了相当的精简。在上面的打包demo中,整个立即执行函数里边只有三个变量和一个函数方法,__webpack_modules__存放了编译后的各个文件模块的JS内容,__webpack_module_cache__ 用来做模块缓存,__webpack_require__是Webpack内部实现的一套依赖引入函数。最后一句则是代码运行的起点,从入口文件开始,启动整个项目。
其中值得一提的是__webpack_require__模块引入函数,我们在模块化开发的时候,通常会使用ES Module或者CommonJS规范导出/引入依赖模块,webpack打包编译的时候,会统一替换成自己的__webpack_require__来实现模块的引入和导出,从而实现模块缓存机制,以及抹平不同模块规范之间的一些差异性。

你知道sourceMap是什么吗?

sourceMap是一项将编译、打包、压缩后的代码映射回源代码的技术,由于打包压缩后的代码并没有阅读性可言,一旦在开发中报错或者遇到问题,直接在混淆代码中debug问题会带来非常糟糕的体验,sourceMap可以帮助我们快速定位到源代码的位置,提高我们的开发效率。sourceMap其实并不是Webpack特有的功能,而是Webpack支持sourceMap,像JQuery也支持souceMap。
既然是一种源码的映射,那必然就需要有一份映射的文件,来标记混淆代码里对应的源码的位置,通常这份映射文件以.map结尾,里边的数据结构大概长这样:

{
  "version" : 3,                          // Source Map版本
  "file": "out.js",                       // 输出文件(可选)
  "sourceRoot": "",                       // 源文件根目录(可选)
  "sources": ["foo.js", "bar.js"],        // 源文件列表
  "sourcesContent": [null, null],         // 源内容列表(可选,和源文件列表顺序一致)
  "names": ["src", "maps", "are", "fun"], // mappings使用的符号名称列表
  "mappings": "A,AAAB;;ABCDE;"            // 带有编码映射数据的字符串
}

是否写过Loader?简单描述一下编写loader的思路?

loader其实是一个函数,接收一段js代码字符串或者buffer,进行操作后再将js代码字符串输出给到下一个loader


// webpack.config.js
module.exports = {
  // ...other config
  module: {
    rules: [
      {
        test: /^your-regExp$/,
        use: [
          {
             loader: 'loader-name-A',
          }, 
          {
             loader: 'loader-name-B',
          }
        ]
      },
    ]
  }
}

是否写过Plugin?简单描述一下编写plugin的思路?

如果说Loader负责文件转换,那么Plugin便是负责功能扩展。Loader和Plugin作为Webpack的两个重要组成部分,承担着两部分不同的职责。
上文已经说过,webpack基于发布订阅模式,在运行的生命周期中会广播出许多事件,插件通过监听这些事件,就可以在特定的阶段执行自己的插件任务,从而实现自己想要的功能。
既然基于发布订阅模式,那么知道Webpack到底提供了哪些事件钩子供插件开发者使用是非常重要的,上文提到过compiler和compilation是Webpack两个非常核心的对象,其中compiler暴露了和 Webpack整个生命周期相关的钩子(compiler-hooks),而compilation则暴露了与模块和依赖有关的粒度更小的事件钩子(Compilation Hooks)。

Plugin的开发和开发Loader一样,需要遵循一些开发上的规范和原则:

  • 插件必须是一个函数或者是一个包含 apply 方法的对象,这样才能访问compiler实例;
  • 传给每个插件的 compiler 和 compilation 对象都是同一个引用,若在一个插件中修改了它们身上的属性,会影响后面的插件;
  • 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住;
class MyPlugin {
  apply (compiler) {
    // 找到合适的事件钩子,实现自己的插件功能
    compiler.hooks.emit.tap('MyPlugin', compilation => {
        // compilation: 当前打包构建流程的上下文
        console.log(compilation);
        
        // do something...
    })
  }
}
@wuyanqian0503
Copy link
Owner Author

wuyanqian0503 commented Jun 29, 2021

有哪些常见的Loader,你用过哪些

首先我们要知道webpack默认只会处理JS之间的依赖关系。

但是webpack 可以使用 loader 来预处理文件。这允许你打包除 JavaScript 之外的任何静态资源。根据这个原因,所以我们可以很直接地推断,对于不同类型的文件一定有相应的loader,事实上也是如此,存在file-loader、image-loader、json-loader、style-loader、sass-loader、css-loader、postcss-loader等。

你也可以使用 Node.js 来很简单地编写自己的 loader。

  • file-loader:把文件输出到一个文件夹中,并且在代码中通过相对的url去引用它(通常使用exclude来排除js、ts、html等文件,用于处理图片和字体)
  • url-loader:与file-loader相似,区别是可以设置一个文件大小阈值,大于这个阈值会交给file-loader处理,小于这个阈值会将这个文件处理成base64的形式编码(通常用于处理体积较小的图片和字体,减少http请求数量)
  • image-loader:加载并压缩图片文件
  • json-loader:加载json文件
  • bable-loader:使用bable对代码进行编译,通常用于将ES6+的高版本JS代码编译成向后兼容的代码
  • ts-loader:将ts转为js
  • awesome-typescript-loader:将 TypeScript 转换成 JavaScript,性能优于 ts-loader
    -sass-loader:将SCSS/SASS代码转换成CSS
  • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
  • style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS
  • postcss-loader:配合 stylelint 校验 css 语法,自动增加浏览器前缀 autoprefixer,编译 css next 的语法
  • eslint-loader:通过 ESLint 检查 JavaScript 代码
  • tslint-loader:通过 TSLint检查 TypeScript 代码
  • mocha-loader:加载 Mocha 测试用例的代码
  • i18n-loader: 国际化

用过file-loader, url-loader, ts-loader, bable-loader, less-loader,postcss-loader, css-loader, style-loader

其中,style-loader和css-loade的区别是什么呢

由于每个loader的功能都是单一的,各自拆分独立。而webpack默认只能处理js模块间的依赖关系。

所以css-loader主要负责就是css模块间的依赖关系以及js对css的引用,它能够处理@import和url语句,处理css-modules,并将结果作为一个js模块返回。

css-loader返回的不是css样式代码的文本,而是一个js模块的代码。

那么这时就需要style-loader来将这些js代码形式的样式来整合到style标签中让他们生效。

有哪些常见的Plugin,你用过哪些

  • define-plugin:定义环境变量 (Webpack4 之后指定 mode 会自动配置)
  • html-webpack-plugin:简化 HTML 文件创建 (依赖于 html-loader)
  • uglifyjs-webpack-plugin:不支持 ES6 压缩 (Webpack4 以前)
  • terser-webpack-plugin: 支持压缩 ES6 (Webpack4)
  • mini-css-extract-plugin: 分离样式文件,CSS 提取为独立文件,支持按需加载
  • webpack-bundle-analyzer: 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块)
  • IgnorePlugin:用于忽略一些不需要的模块文件,例如通常我们会用它来忽略第三方模块中的本地化文件,比如忽略moment中的locale文件夹
  • HotModuleReplacementPlugin:模块热替换插件

用过define-plugin、html-webpack-plugin,mini-css-extract-plugin,IgnorePlugin

Loader 和Plugin 的区别

Loader本质上就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。 因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。

Plugin本质上是一个插件,基于Tapable事件流,通过访问compiler和compilation上的hooks来监听构建流程中的生命周期,达到在特定时机执行自己想做的事情来干预构建结果。

Loader 在 module.rules 中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)、loader、options (参数)等属性。

Plugin 在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。

是否写过Loader?简单描述一下编写loader的思路?

  • Loader 支持链式调用,所以开发上需要严格遵循“单一职责”,每个 Loader 只负责自己需要负责的事情。
  • Loader 运行在 Node.js 中,我们可以调用任意 Node.js 自带的 API 或者安装第三方模块进行调用Webpack
  • 传给 Loader 的原内容都是 UTF-8 格式编码的字符串,当某些场景下 Loader 处理二进制文件时,需要通过 exports.raw = true 告诉 Webpack 该 Loader 是否需要二进制数据尽可能的异步化 Loader,如果计算量很小,同步也可以
  • Loader 是无状态的,我们不应该在 Loader 中保留状态
  • 使用 loader-utils 和 schema-utils 为我们提供的实用工具

是否写过Plugin?简单描述一下编写Plugin的思路?

webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在特定的阶段钩入想要添加的自定义功能。Webpack 的 Tapable 事件流机制保证了插件的有序性,使得整个系统扩展性良好。

  • compiler 暴露了和 Webpack 整个生命周期相关的钩子
  • compilation 暴露了与模块和依赖有关的粒度更小的事件
  • 钩子插件需要在其原型上绑定apply方法,才能访问
  • compiler 实例传给每个插件的 compiler 和 compilation对象都是同一个引用,若在一个插件中修改了它们身上的属性,会影响后面的插件找出合适的事件点去完成想要的功能
  • emit 事件发生时,可以读取到最终输出的资源、代码块、模块及其依赖,并进行修改(emit 事件是修改 Webpack 输出资源的最后时机)
  • watch-run 当依赖的文件发生变化时会触发
  • 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住

@wuyanqian0503
Copy link
Owner Author

webpack 构建流程

  1. 初始化配置,从配置文件和命令参数中得到完整的配置
  2. 开始编译,利用参数初始化compiler,加载所有配置的插件,执行run方法
  3. 确定entry 入口文件
  4. 编译模块,从entry开始对文件使用对应loader进行翻译,并且通过生成的ast语法书分析文件中引用到的其他模块,继续递归翻译,得到依赖关系树
  5. 输出资源,根据上面得到的依赖关系树将所有模块组装成一个或者多个chunk,再把每个chunk准备成单独的文件内容,加入输出列表
  6. 输出完成,将chunk的内容根据配置好的路径和文件名规则,写入文件系统中

线上环境怎么使用 source map

source map 是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre map。
map文件只要不打开开发者工具,浏览器是不会加载的。
线上环境一般有三种处理方案:

hidden-source-map:借助第三方错误监控平台 Sentry 使用
nosources-source-map:只会显示具体行数以及查看源代码的错误栈。安全性比 sourcemap 高
sourcemap:通过 nginx 设置将 .map 文件只对白名单开放(公司内网)

注意:避免在生产中使用 inline- 和 eval-,因为它们会增加 bundle 体积大小,并降低整体性能。

@wuyanqian0503
Copy link
Owner Author

文件监听原理

在发现源码发生变化时,自动重新构建出新的输出文件。
Webpack开启监听模式,有两种方式:

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

**缺点:**每次需要手动刷新浏览器

**原理:**轮询判断文件的最后编辑时间是否变化,如果某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等 aggregateTimeout 后再执行构建。

@wuyanqian0503
Copy link
Owner Author

wuyanqian0503 commented Jun 30, 2021

Webpack 热更新

热更新的表现

是基于WDS (Webpack-dev-server)的模块热替换,只需要局部刷新页面上发生变化的模块,同时可以保留当前的页面状态,比如复选框的选中状态、输入框的输入等。

如何开启热更新

HMR是Webpack内置的功能,通过 HotModuleReplacement 或者 --hot 就可以开启

热更新原理

每一次构建后控制台都会输出三个信息

新的Hash值:a61bdd6e82294ed06fa3
新的json文件: a93fd735d02d98633356.hot-update.json
新的js文件:index.a93fd735d02d98633356.hot-update.js

首先,我们知道Hash值代表每一次编译的标识。其次,根据新生成文件名可以发现,上次输出的Hash值会作为本次编译新生成的文件标识。依次类推,本次输出的Hash值会被作为下次热更新的标识。

具体流程如下:

  1. webpack-dev-server启动webpack,并且启动express和websocket
  2. webpack-dev-server修改entry,向打包内容中增加了和服务端websocket通信的逻辑
  3. webpack-dev-middleware监听文件改动触发重新编译
  4. webpack-dev-server监听webpack编译完成的事件,并且通过websocket通知浏览器
  5. 客户端的websoket消息列表中有两个事件,一个是hash,另一个是ok,hash事件用于更新浏览器中存储的hash,而ok事件则是真正的通知更新模块,当触发ok事件时会拿到最新的hash,向服务端获取更新的文件列表的json以及通过jsonp的形式获取最新的模块代码,然后再进一步刷新页面或者对模块进行热更新

参考:
https://zhuanlan.zhihu.com/p/30669007
https://juejin.cn/post/6844904008432222215#heading-8

@wuyanqian0503
Copy link
Owner Author

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

VSCode 中有一个插件 Import Cost 可以帮助我们对引入模块的大小进行实时监测,还可以使用 webpack-bundle-analyzer 生成 bundle 的模块组成图,显示所占体积。

bundlesize 工具包可以进行自动化资源体积监控。

@wuyanqian0503
Copy link
Owner Author

wuyanqian0503 commented Jun 30, 2021

文件指纹是什么?怎么用?

文件指纹是每次打包完成后文件名的后缀。

hash和chunkhash通常被用来作为文件指纹。

  • hash:是每一次构建生成的唯一hash
  • chunkhash:是根据每个chunk生成的唯一hash,不同的entry也会有不同的chunkhash
  • Contenthash:根据文件内容来定义 hash,文件内容不变,则 contenthash 不变

JS的文件指纹设置

设置 output 的 filename,用 chunkhash。

因为js内容最终会被合并打包成一个或者躲过chunk,所以当文件内容发生改变时,只有对应的chunk的内容会随之改变,所以需要使用chunkhash。

CSS的文件指纹设置

设置 MiniCssExtractPlugin 的 filename,使用 contenthash。

Webpack把所有类型的文件都以js为汇聚点打成一个bundle,改了css也会导致整个js的hash发生改变,所以最好通过MiniCssExtractPlugin 把css独立抽取出来,所以使用contenthash。

图片的文件指纹设置

设置file-loader的name,使用hash。

图片资源的名称是否需要变更取决于自身的内容是否发生改变,所以使用文件内容的hash,

占位符名称及含义:

ext 资源后缀名
name 文件名称
path 文件的相对路径
folder 文件所在的文件夹
contenthash 文件的内容hash,默认是md5算法生成
hash 文件内容的hash,默认是md5算法生成
emoji 一个随机的指代文件内容的emoj

总结

总的来说,文件指纹是用来为静态资源更新做增量更新的,目的是在资源发生变更的时候,能够通过文件名来区分版本,避免文件内容已经变更,但是由于浏览器缓存导致页面没有变化。

由于由于js会被打包成不同的chunk,所以对于js应该使用chunkhash,而CSS通过MiniCssExtractPlugin抽离后,在css发生变更时并不会使chunkhash发生变更,所以可以使用contenthash,css和js隔离也可以避免不必要的更新。图片资源则使用file-loader处理,使用文件内容的hash

@wuyanqian0503
Copy link
Owner Author

如何优化webpack的构建速度

1、使用高版本的 Webpack 和 Node.js
2、压缩代码

  • 多进程并行压缩

webpack-paralle-uglify-plugin
uglifyjs-webpack-plugin 开启 parallel 参数 (不支持ES6)
terser-webpack-plugin 开启 parallel 参数

  • 通过 mini-css-extract-plugin 提取 Chunk 中的 CSS 代码到单独文件
  • 通过 css-loader 的 minimize 选项开启 cssnano 压缩 CSS。

3、图片压缩

  • 使用基于 Node 库的 imagemin (很多定制选项、可以处理多种图片格式)
  • 配置 image-webpack-loader

4、缩小打包作用域

  • exclude
  • resolve.modules 指明第三方模块的绝对路径 (减少不必要的查找)
  • resolve.extensions 尽可能减少后缀尝试的可能性
  • IgnorePlugin 排除模块,或忽略本地化文件
  • 合理使用alias

5、提取页面公共资源

  • html-webpack-externals-plugin:基础包通过CDN引入,或者如果开发的是一个公共组件并且业务线代码中已经有引入一些库,则不需要打包,例如可以不需要打包React或者Vue

6、DLLPlugin分包
通过分包,让一些不需要改动的内容提前打包成静态资源,避免反复打包

7、HashedModuleIdsPlugin
避免模块id不必要的改动导致反复打包

8、充分利用缓存提升二次构建速度

  • babel-loader 开启缓存
  • terser-webpack-plugin 开启缓存
  • 使用 cache-loader 或者 hard-source-webpack-plugin

9、Tree shaking

打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Modlue生效)

开发中尽可能使用ES6 Module的模块,提高tree shaking效率

禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking

使用 PurifyCSS(不在维护) 或者 uncss 去除无用 CSS 代码

purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建议)

@wuyanqian0503
Copy link
Owner Author

bable的原理

大多数JavaScript Parser遵循 estree 规范,Babel 最初基于 acorn项目(轻量级现代 JavaScript 解析器)

Babel大概分为三大部分:

1、解析:将代码转换成 AST

  • 词法分析:将代码(字符串)分割为token流,即语法单元成的数组
  • 语法分析:分析token流(上面生成的数组)并生成 AST

2、转换:访问 AST 的节点进行变换操作生产新的 AST
3、生成:以新的 AST 为基础生成目标代码

Taro就是利用 babel 完成的小程序语法转换

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant