这是我参与「第四届青训营 」笔记创作活动的的第13天
浅谈构建工具
前端为什么需要构建工具?(前端工程的痛点)
前端项目的组成部分(核心要素) —— 资源:
模块化:
将项目拆分成不同的模块,分别进行开发和维护。
主要的规范: ESM 、 CommonJS 、 UMD 。
尽管现在已经有了一些常用的规范,但是还没有形成一个统一的规范,所以模块化在前端控制方面仍然是一个问题。
资源编译:
TS 、 JSX 、 Sass 等高级语法几乎成了项目的标配,但是浏览器不认识这些语法,所以我们需要把它们编译成浏览器可以识别的形式,这背后就涉及到一系列的工具链,因此对于各种工具链的集成也成为了刚需(浏览器的标准远远赶不上前端开发者的脑洞)。
产物质量:
在工作当中,我们需要考虑线上产物质量问题。
- 线上的代码需要压缩,如果代码不压缩的话,代码的体积就会比较庞大,这样会影响到线上的性能从而影响到用户体验。
- 对于未使用到的模块,我们需要将它们从构建产物当中剔除掉,以此来优化产物体积。
- 再者,兼容性的问题,因为对于一个需要兼容移动端的业务项目来说大部分情况是需要兼容到安卓、 iOS 等浏览器,高级的语法到了低端的浏览器就可能会产生一些白屏的事故。
- ……
开发效率:
比如说改动代码后想要立刻看到最新的页面效果,这样来提高开发效率和体验,但是没有构建工具这一层来做的话,提高开发效率和体验是没办法完成的
前端构建工具的意义(构建工具是如何解决以上问题的)
模块化方案
- 提供模块加载方案
- 兼容不同模块规范
Webpack 中的 Webpack runtime ,这个 runtime 就是一个模块加载器,来对每个模块的导入导出进行规范的统一。
语法转译
- 高级语法转译,如 Sass 、 TypeScript
- 资源加载,如图片、字体、 worker
产物质量
- 产物压缩
- 无用代码删除
- 语法降级
开发效率
- 热更新
Vite 概要介绍
Vite 概览
定位:新一代前端构建工具。
两大组成部分:
- No-bundle 开发服务,源文件无需打包。
- 生产环境基于 Rollup 的 Bundler 并且 Vite 也针对 Rollup 进行了深度的定制和优化。
核心特征:
- 高性能, dev 启动速度和热度
- 简单易用,开发者体验好
业界案例
从 Rollup 迁移到 Vite 的项目:
从 Webpack 迁移到 Vite 的项目:
当下问题
传统的构建工具(部分):
传统构建工具的问题:
- 缓慢的启动 -> 项目编译等待成本高
- 缓慢的热更新 -> 修改代码后不能实时更新
- 开发体验问题日渐显露
瓶颈在哪里?
- bundle 带来的性能开销
- JavaScript 语言的性能瓶颈
JavaScript 是解释性语言,也是单线程语言,不能像 Go 、 Rust 那样利用多线程的一些优势来进行一些性能优化,也就没有那么多的性能优化手段。
行业两大趋势
- 全球浏览器对原生 ESM 的普遍支持(目前占比92%以上)。
- 基于原生语言(Go 、 Rust)编写等前端编译工具链。
- 如 Go 语言编写的 Esbuild (Vite 核心的底层工具)、 Rust 编写的 SWC (对标 Babel 的 JS 编译器)
- 如 Go 语言编写的 Esbuild (Vite 核心的底层工具)、 Rust 编写的 SWC (对标 Babel 的 JS 编译器)
浏览器对原生 ESM 的支持
在 script 标签里面通过 import 引入的 foo.js 了,我们可以把它当作一个模块,然后浏览器发现 import 之后会发起一个请求,请求 foo.js 模块,然后解析和导出 foo.js 字段,所以你可以看到 foo 的内容是可以被打印处理的。简而言之就是浏览器支持让你编写<script type="module"><script>的标签,在这个标签里面编写 ESM 模块格式的一些语法。
两大要素:
- script 标签增加 type = "module" 属性;
- 使用 ESM 模块导入导出语法。
基于原生 ESM 的开发服务优势
- 无需打包项目源代码
- 天然的按需加载
- 可以利用文件级的浏览器缓存
- 当文件变更时,不会导致整个 bundle 失效了,只会导致当前请求的缓存失效,可以达到更细力度的浏览器缓存。
基于浏览器的功能, Vite 开发了一套 Dev Server , Dev Server 底层的原理就是
<script type="module"><script>,针对<script type="module"><script>浏览器会发送一个文件请求, Vite Dev Server 就会接受这个文件请求然后自定义的进行一些文件的编译、内容的响应,浏览器最后拿到能识别的 JS 内容。
基于 Esbuild 的编译性能优化
对于一个很大的工具库,编译耗时:
Esbuild —— 基于 Golang 开发的前端工具,具备如下能力:
- 打包器 Bundler ,对标 Webpack 工具
- 编译器 Transformer ,对标 Babel 工具
- 压缩器 Minifier ,对标 Terser 、 Uglify.js 工具
性能极高,在 Vite 中被深度使用。
内置 Web 构建能力
Vite 开箱即用(Vite 不做任何配置的情况)的功能等价于
- webpack
- webpack-dev-server
- css-loader
- style-loader
- less-loader
- sass-loader
- postcss-loader
- file-loader
- MiniCssExtractPlugin
- HTMLWebpackPlugin
- ……
webpack.config.ts :
webpack.config.ts 等价于 vite.config.ts :
Vite 上手实战
项目初始化
初始化命令之后,安装依赖之前,进入 Vite 项目:cd vite
使用 Sass/Scss & CSS Modules
目录结构:
安装 Sass :pnpm install sass -D
index.tsx 文件:
index.module.scss 文件:
引入 Header 组件, App.tsx 文件:
使用静态资源,以 svg 图片为例, App.tsx 文件:
使用 HMR
无需额外配置,自动开启。
生产环境 Tree Shaking
Tree Shaking 在 Vite 中无需配置,默认开启! 优化原理:
- 基于 ESM 的 import/export 语句依赖关系,与运行时状态无关
- 在构建阶段将未使用到的代码进行删除
CommonJS 格式不能做到 Tree Shaking ,因为 require 的部分可能依赖运行时计算的结果。
Vite 给我最直观的印象:
- 响应迅速
- 开箱即用
Vite 整体架构
主要分为两个部分(Vite 两块核心的内容):
- 开发阶段的内容(左边)。
- 生产环境的内容(右边)。
Vite Plugin Pipeline 是 Vite 的插件机制,这个插件机制对 Vite 也是一个核心,它是可以有很多的插件在开发阶段和生产环境之间共用的。
关键技术:依赖预打包
开发环境下的关键技术 —— 依赖预打包:
为什么要进行预打包:
- 避免 node_modules 过多的文件请求
- 将 CommonJS 格式转换为 ESM 格式
实现原理:
- 服务启动前扫描代码中用到的依赖
- 用 Esbuild 对依赖代码进行预打包
- 改写 import 语句,指定依赖为预构建产物路径
关键技术:单文件编译
用 Esbuild 编译 TS/JSX :
优势:编译速度提升 10-100x
局限性:
- 不支持类型检查,这也是为什么 Vite 在生产环境构建的时候要调用一次 tsc 。
- 不支持语法降级到 ES5 。
关键技术:代码压缩
Esbuild 作为默认压缩工具,替换传统的 Terser 、 Uglify.js 等压缩工具。
性能提升也是非常明显的,20-30x 甚至更多。
关键技术:插件机制
开发阶段 -> 模拟 Rollup 插件机制
生产环境 -> 直接使用 Rollup
Rollup 的插件不一定能够使用到 Vite 当中。
插件兼容性具体可查阅: vite-rollup-plugins.patak.dev/
Vite 进阶之路
深入双引擎
推荐学习顺序:
- 先了解基本使用,动手尝试各项常用配置
- 然后学习其插件开发
参考资料:
Esbuild 官方文档
Rollup 官方文档
为什么需要插件机制?
- 抽离核心逻辑
把 Server 端的能力给抽出来,然后把构建相关的能力封装为一个个的插件,这样可以达到解耦的效果,构建和 Dev Server 的逻辑是分开的,更容易维护。 - 易于扩展 社区可以给 Vite 贡献插件。
Vite 插件的钩子:
通过上述的 Hook ,我们可以在不同的构建阶段插入自定义的逻辑。
插件示例:
- 开发 Vite 插件
- 配置文件引入插件
先看文档,过一遍插件钩子的功能,然后多学习其它插件的实现,掌握套路。
参考资料:
Vite 插件开发文档
复杂度较低的插件: json 加载插件
复杂度中等的插件: Esbuild 接入插件
复杂度较高的插件:官方 React 插件
代码分割(拆包)
拆包前:
问题:
- 无法进行并发请求
以前的前端工程一般都只打出一个 Bundle 也就最后只有一个产物文件,只有一个产物文件的话就无法进行浏览器的并发请求。 - 缓存复用率低
一个文件改动,那么整个产物代码全部失效。
拆包(非常依赖于 Rollup 的打包功能,因为生产环境依赖于 Rollup)后:
参考资料:
cn.vitejs.dev/config/buil…
rollupjs.org/guide/en/#o…
JS 编译工具(Babel)
这个工具和 Vite 本身关系不大,但是对于前端工程化来讲却是一个必备的知识。
Babel 原理: Parse 把代码解析为 AST ,也就是抽象语法树,每一个单词都是语法树的节点,后续阶段对这颗树进行各种各样的操作,然后把它转化为一些低级语法的 AST ,最后通过 Babel 的生成器 Generator 把这些低级语法转化为代码,这也就达到了语法降级的功能。
出现原因:
- JavaScript 语法标准繁多,浏览器支持程度不一
- 开发者需要用到高级语法
Babel 所作的事情:开发者需要用到高级语法的基础上,我们采用一系列的编译工具来对这个语法进行降级,然后支持浏览器。
参考资料:
Babel 官方站点
Babel 插件手册
语法安全降级
图中报错问题可以直接导致页面白屏。
如何在构建产物中避免这类问题?
- 上层解决方案:
@vitejs/plugin-legacy - 底层原理
- 借助 Babel 进行语法自动降级
- 提前注入 Polyfill 实现,如 core-js 、 regenerator-runtime
参考资料:
@babel/preset-env 文档
Vite 官方降级插件文档
服务端渲染(SSR)
一种常见的渲染模式,用于提升首屏性能和 SEO 优化。
构建阶段:
代码执行阶段:
深入了解底层标准
重点特性:
- CJS 规范
- ESM 规范(走上了大一统的趋势了,不只是浏览器支持了 ESM 的原生加载,包括 Node 也支持了 ESM 的模块加载)
- HTTP 2.0 特性
参考资料:
hacks.mozilla.org/2018/03/es-…
antfu.me/posts/publi…
什么是 PureESM
Vite 社区生态
Github 40k+ star (可参考 webpack 61.3 K ,rollup 21,8 K)并且目前还在持续维护。
官方提供插件:
- @vitejs/plugin-vue ,提供 Vue 3 支持
- @vitejs/plugin-vue-jsx ,提供 Vue 3 JSX 支持
- @vitejs/plugin-react ,提供 React 支持
- @vitejs/plugin-legacy ,提供低版本浏览器降级支持
海量社区插件: github.com/vitejs/awes…
众多框架内置: