Skip to content

Webpack 与 Grunt, Gulp 的不同? ​

Grunt, Gulp 是基于任务运行的工具:它们会自动执行指定的任务,就像流水线,把资源放上去,然后通过不同的插件进行加工,社区活跃,有丰富的插件可用,能快速打造各种工作流。

Webpack 是基于模块化的打包工具:一切皆模块,当 Webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序所需的每个模块,然后将这些模块打包成一个或多个 bundle。

因此这是完全不同的两类工具,目前主流的方式是用 npm script 代替 Grunt、Gulp 来打造任务流。

Webpack 构建流程是什么? ​

初始化 -> 编译 -> 输出:

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

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

是否写过 Loader 和 Plugin?描述一下编写 Loader 或 Plugin 的思路? ​

Loader 是一个转换器,将 A 文件进行编译形成 B 文件,这里操作的是文件,比如将 A.scss 转换为 A.css,单纯的文件转换过程。

plugin 是一个扩展器,通过在构建流程里注入钩子,给 Webpack 带来了很大的灵活性,针对是 loader 结束后,Webpack 打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听 Webpack 打包过程中的某些节点,执行广泛的任务。

Loader 更像一个「翻译官」,把读到的源文件内容转成新的文件内容,并且每个 Loader 通过链式操作,将源文件一步步翻译成想要的样子。

举个栗子:

小程序多入口依赖分析处理插件 js/ts -> js, less/scss -> wxss

Webpack 的热更新是如何做到的?说明其原理? ​

  1. Webpack编译期,为需要热更新的 entry 注入热更新代码(EventSource 通信)
  2. 页面首次打开后,服务端与客户端通过 EventSource 建立通信渠道,把下一次的 hash 返回前端
  3. 客户端获取到 hash,这个 hash 将作为下一次请求服务端 hot-update.js 和 hot-update.json 的 hash
  4. 修改页面代码后,Webpack 监听到文件修改后,开始编译,编译完成后,发送 build 消息给客户端
  5. 客户端获取到 hash,成功后客户端构造 hot-update.js script 链接,然后插入主文档 hot-update.js 插入成功后,执行 hotAPI 的 createRecord 和 reload 方法,获取到 Vue 组件的 render 方法,重新 render 组件, 继而实现 UI 无刷新更新

参考资料:

如何提高 Webpack 的打包速度? ​

  1. 减小文件搜索范围
    • 配置 resolve.modules
    • 设置 Loader 的 test, include, exclude 参数
  2. 减少编译的文件数量
    • 设置 noParse 参数(如果你确定一个模块中,没有其它新的依赖,就可以配置这项,Webpack 将不再扫描这个文件中的依赖,这对于比较大型类库,将能促进性能表现)
    • 引入 DllPlugin 和 DllReferencePlugin 提前构建一些第三方库
  3. 缓存利用
    • 设置 Babel 的 cacheDirectory=true
    • 引入 cache-loader
  4. 开启多进程:用 thread-loader, Happypack 加速代码构建(多进程)
  5. 其他思路:延迟处理、Native Code (Esbuild, swc)

如何利用 Webpack 来优化资源体积(前端性能)? ​

可以利用 webpack-bundle-analyzer 插件来分析资源体积大的原因:

  1. 减小打包的整体体积
    • 利用 UglifyJS 压缩、混淆代码(干掉 console)
    • 利用 Tree-shaking 和 Scope Hoisting 来剔除多余代码
    • 选择可替代的体积较小的模块
  2. 缓存利用
    • 引入 DllPlugin 和 DllReferencePlugin
    • 引入外部扩展(externals)
  3. 分包、按需加载

Webpack 是如何在打包出的代码中实现模块化? ​

在同步依赖的设计与实现上基于 CommonJS 规范,但是 CommonJS 规范只适用于服务端环境,无法是按浏览器的异步加载,所以 Webpack 在 CommmonJS 的基础上做了改进,使之在浏览器环境也能实现异步加载。

Webpack 怎么配置单页应用?怎么配置多页应用? ​

Babel 原理是什么? ​

Babel 的转译过程分为3个阶段:

  1. 解析:将代码解析生成抽象语法树( 即AST ),即词法分析与语法分析的过程
  2. 转换:对于 AST 进行变换一系列的操作,Babel 接受得到 AST 并通过 babel-traverse 对其进行遍历,在此过程中进行添加、更新及移除等操作
  3. 生成:将变换后的 AST 再转换为 JS 代码,使用到的模块是 babel-generator

如何写一个 Babel 插件? ​

Babel 解析成 AST,然后插件更改 AST,最后由 Babel 输出代码

说说你对前端工程化的理解? ​

所有能降低成本,提高效率的事情总称为工程化。具体到前端工程化,我们要解决的事情有:

开发:

  • 规范化、模块化
  • 实时编译
  • 实时代码检查
  • proxy, mock

部署:

  • 压缩打包
  • 非覆盖式发布
  • 单元测试
  • 自动化部署

总结起来就是:代码可复用,易扩展,利于多人协作;开发流程要统一化、流程化。

具体到xxx业务,我们有20多个前端项目,几乎所有前端项目的脚手架都是基于同一套模板 copy,这套模板在业务实践中我们有多次的改进升级,旧项目就没法享受脚手架优化带来的红利,而且遇到重大 bug 我们还需要投入很多的人力去解决问题,这种方式非常低效。并且在各项目迭代中,开发、部署流程都开始越走越远,不利于多人、多项目协作。

要解决以上问题,我们就需要有统一的前端工程化方案:

  • 提供开发所需的一整套环境运行环境(和 IDE 有点类似)
  • 资源管理,包括资源获取、依赖管理、实时更新、按需加载、公共模块管理等
  • 打通研发链路的各个环节,debug, mock, proxy, test, build, deploy 等

其实,当时如果采用 Vue-cli 的话也可以解决我们的部分问题,但是考察之后还是选择了自己开发,主要的原因在于:

GD-cli 与 Vue-cli 的差别在哪儿?

  1. GD-cli 相比于 Vue-cli 会更轻量,配置更少 因为我们的项目都是基于同一套的工程化方案,研发链路中的各个环节都高度一致(比如 validate-commit-msg 插件及配置),大部分功能及配置都无需配置,直接内置即可,也可以说是扩展性上没有 Vue-cli 好。暴露出的可配置项基本都是业务相关,比如 移动/PC 端的设置,px2rem, postcss-write-svg 插件的启用
  2. 常用命令很少,开发同学可专注于业务 常用的都已经规范化为 npm run dev, npm run build 等 npm script 命令
  3. 可以通过项目中配置文件指定构建器,轻松实现基于 MPX 的小程序的工程化接入
  4. 我们对于工程化流程的理解,以及对于 webpack 的熟悉程度也允许我们做这件事情

ES6 Module 对于 Tree-Shaking 有什么优势? ​

基于 ES6 Module 的静态引用,Tree-Shaking 通过扫描所有 ES6 的 export,找出被 import 的内容并添加到最终代码中。Webpack 的实现是把所有 import 标记为有使用/无使用两种,在后续压缩时进行区别处理。

Source Map 有了解吗?它是如何工作的? ​

JavaScript 脚本正变得越来越复杂。大部分源码(尤其是各种函数库和框架)都要经过转换,才能投入生产环境。

常见的源码转换,包括压缩、合并、编译(ES6, TS 转换为兼容的 ES 代码)等。转换后的代码使得 debug 变得困难重重,而这就是 Source map 要帮我们解决的问题。

简单说,Source map 就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置。

有了它,出错的时候,除错工具将直接显示原始代码,而不是转换后的代码。这无疑给开发者带来了很大方便。

一个合格的 Monorepo 应当具备哪些能力? ​

  1. 依赖管理能力。随着依赖数量的增加,依旧能够保持依赖结构的正确性、稳定性以及安装效率。
  2. 任务编排能力。能够以最大的效率以及正确的顺序执行 Monorepo 内项目的任务(可以狭义理解为 npm scripts,如 build、test 以及 lint 等),且复杂度不会随着 Monorepo 内项目增多而增加。
  3. 版本发布能力。能够基于改动的项目,结合项目依赖关系,正确地进行版本号变更、CHANGELOG 生成以及项目发布。

如何选择合适自己的 Monorepo 工具链? ​

  1. Pnpm Workspace + Changesets:成本低,满足大多数场景
  2. Pnpm Workspace + Changesets + Turborepo/Lage:在 1 的基础上增强任务编排能力

组件库设计思路 ​

前端组件库百花齐放,antd、elementui 这些基础组件库已经很强大,使用于各种业务场景。但是这些基础组件的粒度是基于单个交互,而在交互与产品之间隔着各种各样的模块和业务场景,产品的汇聚源于各种基础组件在业务逻辑的沾粘下集成为一个个项目,一个团队或多或少会有项目或模块存在功能、交互流程的重复、本质上的同质化。

所以 antd、elementui 这类组件库是基于单个非连续性的交互组件,一个组件代表着一次人机无副作用的操作与响应,其不思考实体、用户、终端的状态,最小化的暴露和响应组件内部状态。对于连续性的交互通常来说与特点的业务场景有关,存在诸多的外部依赖,目前都是在各个业务模块由用户(coder)自行编写。

组件库的开发我们需要考虑:

  • 技术方案
    • 工程规范(目录规范、代码规范、提交规范、版本发布规范等)
    • 技术选型、基础框架
    • 单元测试
  • 组件实现
    • 设计思路、解决场景
    • 数据源、数据类型
    • 自定义属性
    • 交互事件
    • 钩子函数
    • 组件实例方法

基础技术:Axis + Vue

组件库工程化统一接入自研工程化工具 - Axis。

Webpack 中的 Module 是什么? ​

模块。Webpack 支持 ESModule,CommonJS,AMD,Assets(image, font, video, audio, json)

为什么 pnpm 没有彻底使用软链接,而是软硬结合? ​

一台机器上一个包在不同项目中可以有不同的依赖集。

在项目 A foo@1.0.0 可以具有一个依赖被解析为 bar@1.0.0,但在项目 B 中 foo 的依赖可能会被解析至 bar@1.1.0; 因此,pnpm 硬链接 foo@1.0.0 到每个使用它的项目,以便为其创建不同的依赖项集。