手把手教你手写 Vite Server(二)—— 插件架构设计(下)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 手把手教你手写 Vite Server(二)—— 插件架构设计(下)

当然实际上,我们可以把整个 Server 上下文对象,都提供出去,这样插件就能访问到整个 Server 上下文对象的属性了,其中 server.app 就是 Dev Server 的实例了。


于是架构,就会变成如下:

1686394463849.png


我们将这个钩子命名为 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(),
  ];
}

我们来看看效果:

1686394397531.png

可以看出页面已经能够渲染出来。

到这一步,我们已经实现了较为完整的插件架构,我们可以通过新增内置插件,来扩展 Dev Server 的能力,而不需要修改最核心的代码 server/index.ts

当然,其实内置插件的注册表,其实也算是内核的一部分,如果我们要想做到在不修改内核代码的情况下,扩展 Dev Server,就需要使用到外部注册表。


实现外部插件的注册


Vite 的外部插件,是通过配置文件注册的。


import { defineConfig } from 'vite';
export default defineConfig({
  plugins: [
      // 外部插件
  ],
});

因此,我们需要实现配置读取的能力

为了简单,我们这里只实现 ES6 modulejs 配置文件的读取,因为要支持其他格式的读取,还要经过比较复杂的处理,感兴趣的可以查看我之前写的文章,《五千字剖析 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 modulejs 配置文件,我们这里直接用 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 的预处理器

1686394347070.png

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()],
});

运行效果如下:

1686394276640.png

总结


本篇文章,先介绍了插件化的架构的相关概念,根据概念,我们的 Vite 插件化改造,需要解决插件管理插件连接两个核心问题。

接下来细化了流程,先实现内部插件的注册和加载

然后是进行插件钩子的设计,在这过程中,回顾了上篇文章的例子,并一步步进行推演出要实现相关能力所需要钩子 —— 需要一个钩子提供 Dev Server 实例用于注册中间件,然后将中间件分别抽离到插件中实现。

接下来实现外部插件的注册核心是读取配置文件,并从配置文件中获取注册的插件。

最后,实现了一个外部插件 —— Less 插件,让项目能够支持 less 文件的引入

这整个改造过程完成之后,我们的手写 Vite 已经是拥有了一套插件化的架构,但是这套插件架构够不够好呢?

答案是否定的

敏感的小伙伴可能会发现,在这个过程中,其实很多代码都是没有被复用的

  • 路径解析、文件加载,是每个中间件分别实现的
  • CSS 中间件和 Less 中间件的大部分代码是相同的

总的来说,本次我们实现的插件化架构,颗粒度较大,那么能复用的内容就小。我们只实现了中间件级别(颗粒度)的插件化,没有对更底层的逻辑进行抽象。例如文件的加载和读取:

  • 如果我们要实现配置项 root(项目根目录),这个会影响到所有中间件的读取文件逻辑,这时候每个中间件都得去读取 root 配置项,才能正确的读取到文件。

那能不能够将这些内容,在 Vite 内部处理好呢?

答案是可以的,这也是本系列下一篇文章要讲述的内容 —— 如何设计一个更好的插件化架构系统,敬请期待


参考文章


最后


如果这篇文章对您有所帮助,请帮忙点个赞👍,您的鼓励是我创作路上的最大的动力。

最近注册了一个公众号,刚刚起步,名字叫:Candy 的修仙秘籍,欢迎大家关注~

目录
相关文章
|
6月前
|
设计模式 安全 Java
【分布式技术专题】「Tomcat技术专题」 探索Tomcat技术架构设计模式的奥秘(Server和Service组件原理分析)
【分布式技术专题】「Tomcat技术专题」 探索Tomcat技术架构设计模式的奥秘(Server和Service组件原理分析)
105 0
|
3月前
|
Kubernetes Serverless API
Kubernetes 的架构问题之利用不可变性来最小化对API Server的访问如何解决
Kubernetes 的架构问题之利用不可变性来最小化对API Server的访问如何解决
80 7
|
4月前
|
Web App开发 JavaScript 前端开发
Chrome插件实现问题之最新的 Chrome 浏览器架构有什么新的改变吗
Chrome插件实现问题之最新的 Chrome 浏览器架构有什么新的改变吗
|
4月前
|
JSON Go C++
开发与运维C++问题之在iLogtail新架构中在C++主程序中新增插件的概念如何解决
开发与运维C++问题之在iLogtail新架构中在C++主程序中新增插件的概念如何解决
45 1
|
5月前
|
JavaScript 前端开发 Java
信息打点-JS架构&框架识别&泄漏提取&API接口枚举&FUZZ&插件项目
信息打点-JS架构&框架识别&泄漏提取&API接口枚举&FUZZ&插件项目
|
6月前
|
Web App开发 JavaScript 前端开发
分析网站架构:浏览器插件
分析网站架构:浏览器插件
|
6月前
|
存储 前端开发 JavaScript
Java电子病历编辑器项目源码 采用B/S(Browser/Server)架构
Java电子病历编辑器项目源码 采用B/S(Browser/Server)架构
105 0
|
6月前
|
运维 Oracle 关系型数据库
LIS实验室信息管理系统功能模块(Oracle数据库、Client/Server架构)
LIS实验室信息管理系统功能模块(Oracle数据库、Client/Server架构)
116 0
|
数据采集 数据库
医院LIS系统源码,SaaS架构的Client/Server应用
LIS系统集申请、采样、核收、计费、检验、审核、发布、质控、查询、耗材控制等检验科工作为一体的网络管理系统。它的开发和应用将加快检验科管理的统一化、网络化、标准化的进程。
|
Web App开发 Java 测试技术
2022 PlantUML 这款 IDEA 插件能搞,流程图、架构图,N种图... 简直神器!
2022 PlantUML 这款 IDEA 插件能搞,流程图、架构图,N种图... 简直神器!
617 0

热门文章

最新文章