当然实际上,我们可以把整个 Server
上下文对象,都提供出去,这样插件就能访问到整个 Server
上下文对象的属性了,其中 server.app
就是 Dev Server
的实例了。
于是架构,就会变成如下:
我们将这个钩子命名为 configureServer
,因为 Vite 中也是叫这个名字,提供的能力也是一样的。
我们先来定义一下钩子函数的类型:
export type ServerHook = (server: ViteDevServer) => void | Promise<void>; export interface Plugin { configureServer?: ServerHook; }
configureServer
提供 server
上下文对象,不接受任何的返回值,支持异步调用。
接下来我们实现钩子的执行时机:
export async function createServer() { const plugins = loadInternalPlugins(); const app = connect(); // server 对象,作为上下文对象,用于保存一些状态和对象,将会在 Dev Server 的各个流程中被使用 const server: ViteDevServer = { plugins, app }; + // 在创建 server 对象后,执行钩子 + for (const plugin of plugins) { + plugin?.configureServer?.(server); + } http.createServer(app).listen(3000); console.log('open http://localhost:3000/'); }
那么我们将之前写好的中间件,改造成插件:
import {Plugin} from '../server/plugin'; export function transformPlugin(): Plugin{ return { configureServer(server){ server.app.use(transformMiddleware()); } }; } • CSS:
import {Plugin} from '../server/plugin'; export function cssPlugin(): Plugin{ return { configureServer(server){ server.app.use(cssMiddleware()); } }; } • static:
import {Plugin} from '../server/plugin'; export function staticPlugin(): Plugin{ return { configureServer(server){ server.app.use(staticMiddleware()); } }; }
然后这样使用即可:
export function loadInternalPlugins(): Plugin[]{ return [ + transformPlugin(), + cssPlugin(), + staticPlugin(), ]; }
我们来看看效果:
可以看出页面已经能够渲染出来。
到这一步,我们已经实现了较为完整的插件架构,我们可以通过新增内置插件,来扩展 Dev Server 的能力,而不需要修改最核心的代码 server/index.ts
。
当然,其实内置插件的注册表,其实也算是内核的一部分,如果我们要想做到在不修改内核代码的情况下,扩展 Dev Server,就需要使用到外部注册表。
实现外部插件的注册
Vite 的外部插件,是通过配置文件注册的。
import { defineConfig } from 'vite'; export default defineConfig({ plugins: [ // 外部插件 ], });
因此,我们需要实现配置读取的能力。
为了简单,我们这里只实现 ES6 module
的 js
配置文件的读取,因为要支持其他格式的读取,还要经过比较复杂的处理,感兴趣的可以查看我之前写的文章,《五千字剖析 vite 是如何对配置文件进行解析的》
我们创建 resolveConfig
函数用于读取配置:
// src/node/config.ts import { pathToFileURL } from 'url'; // 配置的类型 export type ResolvedConfig = Readonly<{ plugins?: Plugin[] }> export async function resolveConfig(): Promise<ResolvedConfig>{ const configFilePath = pathToFileURL(resolve(process.cwd(), './vite.config.js')); const config = await import(configFilePath.href); return config.default.default; }
由于只支持 ES6 module
的 js
配置文件,我们这里直接用 import
函数引入即可。
resolveConfig
函数的使用方式如下:
export async function createServer() { + const config = await resolveConfig(); - const plugins = loadInternalPlugins(); + const plugins = [...(config.plugins || []), ...loadInternalPlugins()]; const app = connect(); // server 作为上下文对象,用于保存一些状态和对象,将会在 Server 的各个流程中被使用 const server: ViteDevServer = { plugins, app, + config, }; for (const plugin of plugins) { plugin?.configureServer?.(server); } http.createServer(app).listen(3000); console.log('open http://localhost:3000/'); }
将配置文件读取后,将内部插件和外部插件合并(我们这里并没有处理顺序),然后将配置也保存到 server 对象中。这样插件也能通过 configureServer
钩子中,拿到整个 Vite 的配置了。
最后再实现 defineConfig
函数:
export interface UserConfig { root?: string; plugins?: Plugin[]; } export type UserConfigExport = UserConfig; export function defineConfig(config: UserConfigExport) { return config; }
其实 defineConfig
并没有做任何处理,只是用来提供类型检查。
实现外部插件
这一小节,我们来在外部实现一个支持 Less 语法的插件,在 Vite 配置文件中注册使用。
Less 是一种 CSS 扩展语言,由于浏览器无法识别 Less 语法,开发时使用 Less 语法,在浏览器运行前需要将 Less 语法转换成 CSS 语法。这个处理过程一般称为预处理,因此 Less 也被称为一种 CSS 的预处理器。
Less 插件的实现如下:
export function lessPlugin(): Plugin { return { configureServer(server) { server.app.use(lessMiddleware()); }, }; }
核心还是 Less 中间件的实现:
import postcss from 'postcss'; import atImport from 'postcss-import'; import less from 'less'; import {dirname} from 'path'; function lessMiddleware(): NextHandleFunction { return async function viteLessMiddleware(req, res, next) { if (req.method !== 'GET') { return next(); } const url: string = cleanUrl(req.url!); if (isLessRequest(url)) { // 解析文件路径 const filePath = url.startsWith('/') ? '.' + url : url; // 读取文件,获取代码的字符串 const rawCode = await readFile(filePath, 'utf-8'); // 预处理器处理 less const lessResult = await less.render(rawCode, { // 用于 @import 查找路径 paths: [dirname(filePath)] }); // 后处理器处理 css const postcssResult = await postcss([atImport()]).process(lessResult.css, { from: filePath, // 用于 @import 查找路径 to: filePath, // 用于 @import 查找路径 }); res.setHeader('Content-Type', 'application/javascript'); return res.end(` var style = document.createElement('style') style.setAttribute('type', 'text/css') style.innerHTML = \`${postcssResult.css} \` document.head.appendChild(style) `); } next(); }; }
Less 中间件的实现和 CSS 中间件的实现几乎相同,只是在 PostCSS 处理前,将 Less 进行编译,这就是预处理器的执行时机。
我们构造两个 Less 文件来测试一下
// less-test.less @import "style-imported.css"; body{ font-size: 24px; font-weight: 700; font-style: italic; }
// less-import.less @fontSize: 50px;
然后在 main.ts
进行引入:
import './style/less-test.less';
然后在 vite.config.js
中使用该插件
import { defineConfig } from 'my-vite-middleware-plugins'; import { lessPlugin } from './plugins/less'; export default defineConfig({ plugins: [lessPlugin()], });
运行效果如下:
总结
本篇文章,先介绍了插件化的架构的相关概念,根据概念,我们的 Vite 插件化改造,需要解决插件管理和插件连接两个核心问题。
接下来细化了流程,先实现内部插件的注册和加载;
然后是进行插件钩子的设计,在这过程中,回顾了上篇文章的例子,并一步步进行推演出要实现相关能力所需要钩子 —— 需要一个钩子提供 Dev Server
实例用于注册中间件,然后将中间件分别抽离到插件中实现。
接下来实现外部插件的注册,核心是读取配置文件,并从配置文件中获取注册的插件。
最后,实现了一个外部插件 —— Less 插件,让项目能够支持 less 文件的引入
这整个改造过程完成之后,我们的手写 Vite 已经是拥有了一套插件化的架构,但是这套插件架构够不够好呢?
答案是否定的。
敏感的小伙伴可能会发现,在这个过程中,其实很多代码都是没有被复用的:
- 路径解析、文件加载,是每个中间件分别实现的
- CSS 中间件和 Less 中间件的大部分代码是相同的
总的来说,本次我们实现的插件化架构,颗粒度较大,那么能复用的内容就小。我们只实现了中间件级别(颗粒度)的插件化,没有对更底层的逻辑进行抽象。例如文件的加载和读取:
- 如果我们要实现配置项
root
(项目根目录),这个会影响到所有中间件的读取文件逻辑,这时候每个中间件都得去读取root
配置项,才能正确的读取到文件。
那能不能够将这些内容,在 Vite 内部处理好呢?
答案是可以的,这也是本系列下一篇文章要讲述的内容 —— 如何设计一个更好的插件化架构系统,敬请期待
参考文章
最后
如果这篇文章对您有所帮助,请帮忙点个赞👍,您的鼓励是我创作路上的最大的动力。
最近注册了一个公众号,刚刚起步,名字叫:Candy 的修仙秘籍,欢迎大家关注~