脚手架设计

STAR 法则描述

1. 背景

团队中有 pc 业务和 h5 业务,两套业务都是使用的部门内其他团队针对移动端 h5 业务开发的脚手架,因此就会有很多冗余的逻辑,比如移动端独特的 px 转 rem 方案,darkmode 方案等等,这些对于 pc 业务是不需要的,而且如果我们想对脚手架做一些改动是很困难的,因为并不是我们团队负责的事情。同时旧的脚手架相对老旧,多年迭代之后在开发模式下启动速度会非常的慢,往往需要 10s 以上甚至更长。

2. 任务

基于这些原因,做了一套脚手架方案,最终目标是给我们的 pc 业务提供一套脚手架,打通整个开发流程,同时在启动速度上做一些优化。

3. 行动

基于上面的任务,调研对比了团队使用的旧脚手架方案,以及 Vue 技术栈经常使用的 vue-cli 方案,以及 Vue 比较新的 vite 脚手架方案。对这三者的主干源码都大致看了一遍,了解到这三者的技术方案之后,开始组建我们团队的脚手架方案。最终决定使用 vite 这套方案,然后通过写一些 vite 插件来实现我们的业务能正常对接上部门的发布系统。

4. 结果

使用之后,搭配上之前搞的 pc 业务基础库,基本上完成了开发过程中的一个统一。打通了从使用脚手架创建模版,启动项目,到发布系统发布整个流程。而且后续我们的项目如果有一些改动,比如推进业务的 ts,完全可以自行修改,拥有业务的脚手架是一个非常重要的基础。而且在启动速度上有了一个非常大的改观,从原来的 10s 以上,到现在的 0.5s 以内,开发体验上有了一个大大的提高。

技术细节

在团队里想要更换技术栈,必须要把前因后果讲清楚,然后还要有方案调研对比。所以这里我主要阐述一下对 vite、vue-cli、以及原来脚手架方案的一个对比。其中 vite 源码这部分阅读还是花了挺长时间的,才把整个主流程看差不多。

三者对比

  • gulp:流式操作,通过类似管道的概念,让文件经过一个一个的任务处理,最终输出到结果。
  • webpack:通过 loader 将非 js 文件转换成 js 格式的文件,然后在文件构建的过程中,使用 plugin 插件来处理所有过程,最终达成目标。
  • rollup:相比于 webpack,分析静态代码删除冗余逻辑的 tree-shaking 更强,对比来讲,打包出的体积会更小。

当然,总体来讲,这三者能达成的功能其实都是类似的,而且在社区的帮助下,很多插件都能相互实现对应的功能,其实差别并不是很大。而且 webpack 相对来讲是目前使用最广泛的,社区的一些能力也会更多一些。让我选择使用 vite 的最大原因是开发模式下速度会非常快,从体验上来讲会更加舒适。

潜在问题

  1. npm 依赖也需要是 es module 的格式,虽然 vite 会有一个依赖预构建的过程,一定程度上能做到 commonjs 转换 esm,但是还是会有一些情况没办法转换的,因此最好是 npm 依赖都是 esm 格式。因此如果切换 vite,后续的计划是改写一下团队的常用依赖,变成 esm 格式的。
  2. 开发模式和生产模式是不同的体系,两者会有一些区别,因为开发模式是让浏览器来解析处理,而生产模式是 rollup 打包,两者不同。但是目前来讲还没有遇到问题。

总的来说,使用 vite 一定是有价值的,遇到具体问题解决即可,因为从开发体验上来讲会更好。

旧团队脚手架

对于一个脚手架来讲,核心的命令一般是 init 创建项目、watch 启动项目、build 构建项目这三个步骤。核心是 gulp + webpack,大部分代码处理交给 gulp 来做,webpack 主要是用于开发环境启动项目,以及 build 构建项目。

init 创建项目会去拉本地全局安装的模板,复制到目标位置。

watch 启动项目会去全局拉 gulpfile 文件,watch 命令执行后,最后会执行 gulp gulpfile,后面会被 gulpfile 接管,gulp watch 主要分为 watch-html、watch-css。这里面可以做一些内容处理,比如 darkmode 方案(window.matchMedia(‘perfers-color-scheme’)),比如插入 script 标签引入一些外部的文件,px 转 rem 方案等,都可以通过 gulp 任务来处理。然后启动一个 webpack-dev-server 运行项目。

px 转 rem

举例 375px 设计稿,font-size 可以设置为 20px,然后媒体查询不同 min-width 的宽度可以根据 20 * (min-width) / 375 这么一个公式来写 font-size,然后计算对应 rem,把 px 转换成 rem 即可。

build 过程和 watch 中的差不多,不过会有一些构建过程中的独特步骤,比如 css、js 文件都上传 cdn,因此需要写任务在 html 内容中添加对应上上传路径、比如在 script 中插入一些关键数据比如版本等信息,都可以在这里处理。

vue-cli

vue-cli 的思路其实也是类同,init 去拉模版,比较核心的过程就是拉下来模板之后到最终生成项目的过程。中间主要做了三个核心的事情,可配置问答、可配置文件、可配置文件内容这几块。

1. 可配置问答

模板中又一个配置文件,里面配置了脚手架问答选项,比如项目名称、作者、技术选型等等,然后会把用户的选择记录到类似全局变量中,这个是后面可配置性的基础。

2. 可配置目录

获取配置文件中的 filters 过滤参数,然后在遍历模板的时候,会去逐个比对文件是否符合条件。举个例子,模板中是包含了 less 和 scss 的文件的,比如我在 less 和 scss 选择了 less,那么在这个过程中就会吧所有 scss 文件删除,只保留 less 文件,就实现了技术选型。

3. 可配置内容

这个是指文件的内容会根据技术选型的不同而不同,比如项目中选择了 less,那么就需要在 package.json 中添加 less 的相关 npm 包,这一部分则是使用了 handlebars 模板渲染来处理,和 ejs、pug 这些模板渲染引擎类似,模板文件中预留一些条件,然后 handlebars 就会根据这些条件判断是否渲染对应的内容,然后就实现了可配置文件内容。

vite

vite 我理解和之前的思路都不同,是有一个比较大的创新的。vite 的核心我总结下:

1. esm 的路径处理

vite 我理解是一个原生 esm 为原理的开发构建工具,在开发环境下是用了浏览器支持原生 ES imports 开发,通过 script type=”module”来设置,生产环境是用 Rollup 打包的。是真正的按需编译,因为他把这些处理都交给了浏览器来接管。

基本实现流程是:服务端解析判断 vue 单文件请求 -> 解析模块路径 -> 读取文件内容 -> 重写模块路径 -> 客户端代码注入。

1.1 重写路径模块原因

原因是因为浏览器对于类似 import Vue from 'vue'这种使用方法没办法去找到对应的位置,因此在开发环境需要对路径重写相对路径,以便浏览器找到目标依赖模块。是通过对 js 后缀的文件解析所有的 import 语句,然后把不符合要求的路径全部重写成/@modules开头的路径,具体的路径一般是根据 package.json 中的 main 字段来查找具体的位置。。

1.2 vue 单文件拆分成多个请求

对于 vue 单文件三段式 template、script、style,会对这三块分别进行处理,会使用 compiler-sfc 来对文件进行解析,对于 style 标签内的,会添加 import 关键字,并将这个 style 以请求的形式在后续进行处理,类似import '/src/App.vue?type=style'。对于 template 标签,则会类似import { render as __render } from '/src/App.vue?type=template'这种,最终加在一起拼到 script 中。对于 style 的请求,直接拿到标签内容然后设置响应类型,然后创建一个 style 标签插入到 head 中。对于 template 类型的请求,会使用 compiler-sfc 来编译一下拿到最终的 code。

2. vite HRM 热更新

vite 分为两块,一块是服务端,一块是客户端,客户端中的逻辑是在处理 html 的时候被写入代码中。热更新的原理是用 websocket 链接,当代码被修改时会触发对应的事件,比如 vue-reload 等,会触发 vue 重新加载,render 等

插件工作

由于我们部门的发布系统一般都是只发布 html 文件,对于引入的 js、css 等文件都是上传到 cdn 上,因此就需要一个插件来做这样的工作。vite 插件提供了 build 过程中,把 html 中引入的相对路径变成上传 cdn 后的路径。

主要实现是通过 rollup 暴露出的 hook,在输出 hook 中将输出的 js、css 文件打包上传 cdn 上,然后重写 html 文件的路径,大概这做了这么一个工作。

webpack 插件工作原理

和写 webpack 插件其实比较像,都是通过暴露出的对应钩子写对应的执行逻辑。webpack 执行流程,可以从暴露出的钩子来看流程,一些细节我记不太清了,说几个关键的 hook

  1. run:执行 compiler 的 run 方法,因此 berforeRun 会有一个初始化 webpack 的过程,其中最重要的就是实例化 compiler 对象,这个对象贯穿整个流程
  2. compile:开始编译,是由上一个 run 触发的,后面就开始整个编译的过程
  3. make:分析入口依赖,会不断循环出所有的依赖
  4. build:构建,构建过程则是使用各种 loader 对文件进行处理,处理成 webpack 能接受的文件,然后就是编译原理那一套,生成抽象语法树,遍历等等
  5. seal:封装构建结果,这一部分主要是对代码进行整理,比如一些优化 tree-shaking 等就是在这里进行的
  6. emit:输出结果到 dist 目录