手把手教你手写 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 的修仙秘籍,欢迎大家关注~

目录
相关文章
|
7月前
|
设计模式 安全 Java
【分布式技术专题】「Tomcat技术专题」 探索Tomcat技术架构设计模式的奥秘(Server和Service组件原理分析)
【分布式技术专题】「Tomcat技术专题」 探索Tomcat技术架构设计模式的奥秘(Server和Service组件原理分析)
114 0
|
28天前
|
SQL 数据可视化 数据库
多维度解析低代码:从技术架构到插件生态
本文深入解析低代码平台,涵盖技术架构、插件生态及应用价值。通过图形化界面和模块化设计,低代码平台降低开发门槛,提升效率,支持企业快速响应市场变化。重点分析开源低代码平台的优势,如透明架构、兼容性与扩展性、可定制化开发等,探讨其在数据处理、功能模块、插件生态等方面的技术特点,以及未来发展趋势。
|
27天前
|
SQL 数据可视化 数据库
多维度解析低代码:从技术架构到插件生态
本文深入解析低代码平台,从技术架构到插件生态,探讨其在企业数字化转型中的作用。低代码平台通过图形化界面和模块化设计降低开发门槛,加速应用开发与部署,提高市场响应速度。文章重点分析开源低代码平台的优势,如透明架构、兼容性与扩展性、可定制化开发等,并详细介绍了核心技术架构、数据处理与功能模块、插件生态及数据可视化等方面,展示了低代码平台如何支持企业在数字化转型中实现更高灵活性和创新。
51 1
|
4月前
|
Kubernetes Serverless API
Kubernetes 的架构问题之利用不可变性来最小化对API Server的访问如何解决
Kubernetes 的架构问题之利用不可变性来最小化对API Server的访问如何解决
92 7
|
5月前
|
Web App开发 JavaScript 前端开发
Chrome插件实现问题之最新的 Chrome 浏览器架构有什么新的改变吗
Chrome插件实现问题之最新的 Chrome 浏览器架构有什么新的改变吗
|
5月前
|
JSON Go C++
开发与运维C++问题之在iLogtail新架构中在C++主程序中新增插件的概念如何解决
开发与运维C++问题之在iLogtail新架构中在C++主程序中新增插件的概念如何解决
51 1
|
6月前
|
JavaScript 前端开发 Java
信息打点-JS架构&框架识别&泄漏提取&API接口枚举&FUZZ&插件项目
信息打点-JS架构&框架识别&泄漏提取&API接口枚举&FUZZ&插件项目
|
7月前
|
Web App开发 JavaScript 前端开发
分析网站架构:浏览器插件
分析网站架构:浏览器插件
|
7月前
|
存储 前端开发 JavaScript
Java电子病历编辑器项目源码 采用B/S(Browser/Server)架构
Java电子病历编辑器项目源码 采用B/S(Browser/Server)架构
124 0
|
7月前
|
运维 Oracle 关系型数据库
LIS实验室信息管理系统功能模块(Oracle数据库、Client/Server架构)
LIS实验室信息管理系统功能模块(Oracle数据库、Client/Server架构)
126 0

热门文章

最新文章