Vite 作为新一代的前端构建工具,在发布之初便备受关注,在今年发布 Vite 2.0 后更是如日中天,广受吹捧。Vite 使用 esbuild 实现高速的依赖预构建,并借助现代浏览器对 ES 模块的原生支持提供单个模块级别的按需加载和 HMR,提高首次构建以及迭代构建的速度,带来更加优秀的前端开发体验。
MV3 有一个重要改动就是将原本可以作为页面运行的 background 改为只能使用 service worker 模式运行了。
页面脚本和 service worker 脚本在支持的 API 上有少许不同,因此我们需要把 background 脚本修改成 service worker 所支持的版本。这通常来说不是什么问题,background 只负责一些核心的交互逻辑就可以了,这些逻辑基本上都是 service worker 所支持的。
好,以下假设我们的 background 脚本已经是完全兼容 service worker 模式了。
4.2 修改 Vite 配置文件把 background 的入口点 html 改为 js 形式
Vite 原生提供对库模式项目的支持,我们只要按照库模式就可以生成以 js 脚本作为入口点的打包代码。
但是这里也有个问题就是 Vite 是为单项目设计的,虽然提供了多入口模式,但是却不能在一份配置中同时打包库模式和多页面应用模式的代码。因此我们在这里把 background 脚本的打包配置从扩展打包配置中单独拿出来叫做 vite.config.ext.bg.ts。
注:该文撰写于 2021 年 11 月 25 日,因此有部分表述和方案已经过时,仅留做参考。
1 预热
1.1 HMR(模块热替换)
最早听到 HMR 这个词,是在使用前端构建工具 webpack 的时候。在 HMR 出现之前,我们需要在每次修改代码之后,完全重新构建代码并刷新页面以反映最新结果,这是一件十分费时费力地工作。而 HMR 允许在运行时更新所有类型的模块,无需完全刷新,极大地提升了前端开发效率。打包器在开发过程中会检测代码的更新并自动根据改动重新打包代码,以打补丁或直接替换的方式更新网页中运行的代码,且在这个过程中尽量不造成页面的完全刷新。
图片来源:Webpack's Hot Module Replacement Feature Explained
然而当构建的应用越来越庞大后,具有海量模块的代码极大地拖慢了基于全量构建的 webpack 等构建工具启动开发服务器以及 HMR 的速度。
1.2 Vite
Vite 作为新一代的前端构建工具,在发布之初便备受关注,在今年发布 Vite 2.0 后更是如日中天,广受吹捧。Vite 使用 esbuild 实现高速的依赖预构建,并借助现代浏览器对 ES 模块的原生支持提供单个模块级别的按需加载和 HMR,提高首次构建以及迭代构建的速度,带来更加优秀的前端开发体验。
esbuild
我们也已在之前几个线上前端项目中在使用 create-react-app (使用 webpack)构建的基础上,集成了 Vite 以解决开发服务器启动慢,热更新慢的问题,显著地改善了开发体验,间接提升了迭代速度。
1.3 浏览器扩展开发
近期,在新的 POC 项目中,我们需要在开发网页应用的同时,开发一个浏览器扩展以完成与网页应用地对接和与后端地交互。然而浏览器扩展的社区生态相比网页应用差了许多个数量级,开源配套工具少之又少,自己摸索出现了问题也鲜能找到解决方案。
这篇文章就将把作者通过将 Vite 集成到现有的基于 create-react-app + parcel 的浏览器扩展项目上摸着石头过河的经验分享出来,希望能够让大家少走一些弯路,还可以稍微理解一下实现的机制原理。
1.4 正文前
本文灵感来源和实现大量参考开源模板 vitesse-webext,根据实际情况筛掉了不必要的部分,同时解决了一些原项目未考虑的问题如 React 框架适配,兼容 manifest v3 等。如果你想使用其他框架如 Vue 或 Svelte 来开发浏览器扩展,同时也对如 WindiCSS、pnpm 等感兴趣的话,不妨直接以此开源模板为支点来进行研究或开发,本文仅当作理解这种解决方案原理的参考即可。
另外声明,本人才疏学浅,可能会有理解错误的地方,若大家对文章观点有不同见解或发现文章描述情况与事实不符,欢迎指正,共同进步,谢谢。
2 支持开发模式 HMR
这章主要介绍开发模式相关的实现,为了让理解曲线相对平缓,会尽量忽略掉支持生产构建的部分,因此读者可能会感觉到代码不完整。先别急,我们将在后面的章节改写成支持生产构建的完全体代码。
另外由于我们的项目目前只考虑支持(较新版本的) Chrome 浏览器,因此只使用
chrome.runtime.Port
相关 API 进行网页应用与浏览器扩展的通信,暂时没有用到 content scripts,所以本文也没有包含这部分的实现。还希望大家见谅。2.1 为啥浏览器扩展不好做 HMR
各大构建工具,无论 webpack 还是 Vite,都是通过以下手段来达成 HMR 的:
这些操作都有个前提是浏览器不是通过直接访问文件系统获得静态代码,而是通过访问构建工具建立的开发服务器去获取打包后代码。然而对于浏览器扩展,根据规范,开发者需要提供一个包含所有页面入口点的
manifest.json
,这些入口点的路径必须是扩展文件夹下的静态路径。所以现有构建工具通过本地服务器提供的打包后代码就无法直接被指定为扩展入口点,加大了扩展做 HMR 的难度。2.2 如何绕过浏览器扩展只能访问静态代码的难题
解决方案其实很直接,既然你不能直接指定开发服务器作为入口点,那么我就写一个静态的入口点,然后在静态入口点里请求开发服务器获取脚本就行了。
这里以
popup/index.html
为例介绍如何制作一个可以访问开发服务器的扩展入口点。这里我们修改了两行代码,其中第二行很好理解,就是将原本的相对路径引入改成了开发服务器的路径。
那么第一行是干什么用的呢?其实这跟 Vite 的 React 插件的实现细节有一点关系。
2.3 在扩展 HMR 中适配 React 框架
Vite 在实现 React HMR 与 Vue/Svelte 的 HMR 上是有些许不同的。Vue/Svelte 插件的实现中只需要全局引入
/@vite/client
就可以完成 HMR 的工作了,但是 React 的实现中则需要额外注入一些方法和变量到浏览器全局的window
中,并在每个.jsx
文件中使用它们。在通常的网页应用开发中,这个注入是 Vite 的 React 插件来帮我们完成的。插件会在入口点的 HTML 文件(即使是库模式,在开发过程中也需要提供一个 HTML 的入口点)中添加一个<script>
标签并将注入脚本作为行内脚本插入进去并通过开发服务器提供给浏览器。然而我们为了解决扩展只能访问静态代码的问题时,需要提供一个静态的 HTML 代码,注入自然也就无法在 HTML 中动态完成了。对于这个问题,我的解决方案是把需要注入的代码保存在一个
hmr.js
文件中,并在每个入口点引入它已达到与动态注入行内脚本同样的效果。这里相比于直接把行内脚本写在 HTML 文件中,通过文件引入更好,理由有以下几点:
2.4 编写 Vite 配置文件
关于 Vite + React 通用的配置问题社区已经有很多相关文档文章教程了,作者团队之前也写过一篇文章记录这些问题,这里就不做过多赘述了。这一小节主要记录下做浏览器扩展开发模式相关的配置。
首先,为了方便编写和维护,我们先准备一些通用配置和方法:
然后就可以编写
vite.config.ext.ts
了:这里为啥不直接叫
vite.config.ts
呢?因为我们这个项目是把网页应用和浏览器扩展包含在一个代码仓库中,同时又没有使用像 monorepo 这样的项目管理策略,因此会同时需要多个 Vite 配置文件来管理不同项目,自然就需要不同的配置文件名了。如果你的项目是一个纯粹的浏览器扩展,那么包括一部分配置在内的代码都可以有一定程度上的简化,请读者根据实际情况进行调整。2.5 生成 manifest.json
建立
manifest.ts
脚本用于生成manifest.json
并将其写入扩展目录。可能有一些配置只针对 Chrome 扩展生效,请酌情参考:2.6 将 HTML 和 manifest.json 放入扩展目录
创建
watch.ts
的脚本用于管理开发模式下的静态代码(HTML 和 manifest.json):2.7 启动开发模式
run-p
是 CLI 工具库[npm-run-all](https://github.com/mysticatea/npm-run-all)
中提供的脚本,指并行运行列出的 npm 脚本,我们也可以使用类似run-p dev:ext:*
的 wild card 形式来运行所有满足特定格式的 npm 脚本。esno
是 CLI 工具库[esno](https://github.com/antfu/esno)
中提供的脚本,它的基本作用是执行 Node.js 脚本,但它提供了对 TypeScript 以及 ESNext 脚本的运行时支持。这里也有对它的简单介绍。开发时只需要运行
npm run dev:ext
就 OK 啦。3 支持生产模式构建
在上一章,我们已经能够在支持 HMR 的环境下进行浏览器扩展开发了。在实现过程中,我们把原本的生产模式构建流程破坏了一部分,接下来我们再来把生产模式的构建修补好。
3.1 使 HTML 文件支持生产模式构建
在上一章中,我们将使用相对路径导入脚本的 HTML 改成了开发服务器的路径。现在我们需要让它们能够根据是开发模式还是生产模式自动生成对应的路径。
首先我们把 HTML 中的路径改回能够支持生产模式构建的相对路径,这里仍以 popup 举例:
然后修改仅在开发模式下执行的脚本
watch.ts
使其支持将 HTML 魔改为开发模式版本。只需修改其中的stubIndexHtml()
方法即可:3.2 使 Vite 配置文件支持生产模式构建
上一章中我们只为 Vite 配置了开发模式的配置项,这里我们补全生产模式的配置。将
vite.config.ext.ts
修改如下:3.3 使 manifest.json 支持生产模式构建
实际上 manifest 部分不改也支持生产模式构建,不过由于开发模式使用了一些生产模式用不到的权限,因此可以在生产模式种去除。修改
manifest.ts
如下:3.4 进行生产模式构建
执行
npm run build:ext
即可进行生产模式构建啦。4 支持 Manifest V3(MV3)下的开发和构建
眼尖的同学可能已经发现了,我们在上面章节介绍的流程使用的 Manifest 版本是 V2(MV2),而 MV3 已经在2020年发布,且最早可能在2022年1月份,Chrome 扩展商店就不接受新的 MV2 扩展上线了。我们为什么不直接使用 MV3 呢?这是因为 MV3 为了提高扩展的安全性,禁止了直接修改
script-src
允许外部脚本的配置项,而上文正是使用这个配置项来实现 HMR 的。Chrome 对 MV2 的支持时间线
好在 MV3 和 MV2 在 API 上的改动不大,因此我们可以通过平时基于 MV2 开发(虽然不接受新的 MV2 扩展上线,但是仅在本地 MV2 开发在短期内还是会继续支持的),仅在构建时使用 MV3,同时提供不支持 HMR 的 MV3 开发环境来作为调查 MV3 相关问题的环境来尽量保持开发体验。这样即使在 MV2 被完全弃用后,我们仍然可以退化成一个不包含 HMR 的开发和构建环境继续迭代。
另外,关于 MV3 在这方面的改动,仍然有争议和 bug。也就是说,这一块还没有完全定稿,接下来可能还会有改动。因此在完全定稿之前,我们也不会花太大精力在研究如何做迁移上。
4.1 将 background 相关脚本转换成 service worker 模式
MV3 有一个重要改动就是将原本可以作为页面运行的 background 改为只能使用 service worker 模式运行了。
页面脚本和 service worker 脚本在支持的 API 上有少许不同,因此我们需要把 background 脚本修改成 service worker 所支持的版本。这通常来说不是什么问题,background 只负责一些核心的交互逻辑就可以了,这些逻辑基本上都是 service worker 所支持的。
好,以下假设我们的 background 脚本已经是完全兼容 service worker 模式了。
4.2 修改 Vite 配置文件把 background 的入口点 html 改为 js 形式
Vite 原生提供对库模式项目的支持,我们只要按照库模式就可以生成以 js 脚本作为入口点的打包代码。
但是这里也有个问题就是 Vite 是为单项目设计的,虽然提供了多入口模式,但是却不能在一份配置中同时打包库模式和多页面应用模式的代码。因此我们在这里把 background 脚本的打包配置从扩展打包配置中单独拿出来叫做
vite.config.ext.bg.ts
。4.3 支持生成 MV3 的 manifest.json
还是只要修改
manifest.ts
就行了:4.4 修改生产构建脚本并添加 MV3 环境开发模式脚本
生产构建脚本的改动主要是将
vite build
部分的代码从一条拆分成了两条。MV3 环境开发模式脚本也很简单,就是在
vite build --watch
模式的基础上将模式设为development
而已(覆写process.env.NODE_ENV
这个环境变量)。这样就可以通过
npm run watch:ext
来启动 MV3 环境下的开发模式了。当然,这个开发模式是不包含 HMR 功能的,调试的时候就需要手动刷新扩展页面(对于 popup 或 options 页面)或是点击扩展管理中的刷新按钮(对于 background 脚本)了。5 结语
其实在这个环境搭建过程中,还遇到过很多小问题,有些已经包含在本文的解决方案中,还有一些和本文的主题无关,是各个工具使用的问题就没有记录在其中。为了防止篇幅过长造成阅读负担,这里就尽量不记录不必要的内容了,相信大家遇到这些小问题都有自己解决的能力。
本来还想把解决问题的思路总结成图画出来的,后来发现自己的画图功底实在是太烂,画出来自己都看不下去,就变成几乎纯文字的描述了。希望自己以后能把自己的画图技能点多加几点。