一起写个vite吧!(2) --插件机制开发
继上一章节一起写个vite吧!(1) --环境搭建+依赖预构建咱们继续开干哈,这一节咱们来搞
vite
的插件机制的实现,干货满满哈。vite
在开发的阶段实现了一个按需加载的服务器,每个文件请求进来都会经历一系列的编译流程,生产环境下,vite
同样会执行一系列编译过程,将编译结果交给Rollup
进行模块打包。这一系列的编译过程指的就是Vite
的插件工作流水线(Pipeline),而插件功能又是Vite
构建能力的核心,因此谈到阅读Vite
源码,我们永远绕不开插件的作用与实现原理。
- 在生产环境中
Vite
直接调用Rollup
进行打包,所以Rollup
可以调度各种插件; - 在开发环境中,
Vite
模拟了Rollup
的插件机制,设计了一个PluginContainer
对象来调度各个插件。
值得一提的是
Vite
从preact
的WMR
中得到了启发,把插件机制做成兼容 Rollup 的格式。简单的介绍一下Rollup
插件,其实插件这个东西,就是Rollup
对外提供一些运行过程中一些时机的钩子,还有一些工具方法,让用户去写一些配置代码,以此介入Rollup
运行的各个时机之中。而Vite
需要做的就是基于Rollup
设计的接口进行扩展,在保证Rollup
插件兼容的可能性的同时,再加入一些Vite
特有的钩子和属性来扩展。Rollup
的插件可以参考这篇文章Rollup 之插件机制浅析。
- 实现 Rollup 插件钩子的调度
- 实现插件钩子内部的 Context 上下文对象
Vite架构图:
对于vite
的插件API可以直接看一下官网的插件API介绍,任何可以自己编写一下vite
的插件,官网有一个虚拟模块插件的例子,可以看看自己跟着写一写--虚拟模块插件demo地址。
ok同学们在官网熟悉完插件后,咱们正式进入插件机制的开发,在这里我们并不会实现vite
的所以插件哈,只会实现部分。
pluginContainer的实现
插件容器需要接收 Vite 插件作为初始化参数,因此我们需要提前声明插件的类型,你可以继续新建src/node/plugin.ts
来声明如下的插件类型:
src/node/plugin.ts
import { LoadResult, PartialResolvedId, SourceDescription } from 'rollup'
import { ServerContext } from './server'
export type ServerHook = (
server: ServerContext
) => (() => void) | void | Promise<(() => void) | void>
// 只实现以下这几个钩子
export interface Plugin {
name: string
configureServer?: ServerHook
resolveId?: (
id: string,
importer?: string
) => Promise<PartialResolvedId | null> | PartialResolvedId | null
load?: (id: string) => Promise<LoadResult | null> | LoadResult | null
transform?: (
code: string,
id: string
) => Promise<SourceDescription | null> | SourceDescription | null
transformIndexHtml?: (raw: string) => Promise<string> | string
}
ok然后我们再新建src/node/pluginContainer.ts
文件,增加类型定义:
src/node/pluginContainer.ts
// rollup里的类型,vite是基于rollup的插件进行拓展的
import type {
LoadResult,
PartialResolvedId,
SourceDescription,
PluginContext as RollupPluginContext,
ResolvedId,
} from "rollup";
export interface PluginContainer {
resolveId(id: string, importer?: string): Promise<PartialResolvedId | null>;
load(id: string): Promise<LoadResult | null>;
transform(code: string, id: string): Promise<SourceDescription | null>;
}
ok,然后我们来实现插件机制的具体逻辑,主要集中在createPluginContainer
函数中:
src/node/pluginContainer.ts
import type {
LoadResult,
PartialResolvedId,
SourceDescription,
PluginContext as RollupPluginContext,
ResolvedId,
} from 'rollup'
import { Plugin } from './plugin'
export interface PluginContainer {
resolveId(id: string, importer?: string): Promise<PartialResolvedId | null>
load(id: string): Promise<LoadResult | null>
transform(code: string, id: string): Promise<SourceDescription | null>
}
export const createPluginContainer = (plugins: Plugin[]): PluginContainer => {
// 插件上下文对象
// 使用ts-ignore让ts不检查这里的Context,因为我们只实现上下文对象的 resolve 方法,会报错
// @ts-ignore
class Context implements RollupPluginContext {
async resolve(id: string, importer?: string) {
let out = await pluginContainer.resolveId(id, importer)
if (typeof out === 'string') out = { id: out }
return out as ResolvedId | null
}
}
// 插件容器
const pluginContainer: PluginContainer = {
async resolveId(id: string, importer?: string) {
const ctx = new Context() as any
for (const plugin of plugins) {
if (plugin.resolveId) {
const newId = await plugin.resolveId.call(ctx as any, id, importer)
if (newId) {
id = typeof newId === 'string' ? newId : newId.id
return { id }
}
}
}
return null
},
async load(id) {
const ctx = new Context() as any
for (const plugin of plugins) {
if (plugin.load) {
const result = await plugin.load.call(ctx, id)
if (result) {
return result
}
}
}
return null
},
async transform(code, id) {
const ctx = new Context() as any
for (const plugin of plugins) {
if (plugin.transform) {
const result = await plugin.transform.call(ctx, code, id)
if (!result) continue
if (typeof result === 'string') {
code = result
} else if (result.code) {
code = result.code
}
}
}
return { code }
},
}
return pluginContainer
}
很显然,
Vite
将Rollup
的PluginContext
对象重新实现了一遍,因为只是开发阶段用到,所以去除了一些打包相关的方法实现,我们这里则只实现上下文对象reslove
方法而已,在调用钩子用call
绑定this这样在钩子里可以通过this.XXXX
调用上下文里的XXXX
方法了。
ok然后我们再去完善服务器的逻辑
src/node/server/index.ts
import connect from 'connect'
// picocolors 是一个用来在命令行显示不同颜色文本的工具
import { blue, green } from 'picocolors'
import { optimize } from '../optimizer'
import { resolvePlugins } from '../plugins'
import { Plugin } from '../plugin'
import { createPluginContainer, PluginContainer } from '../pluginContainer'
// 服务器上下文的类型
export interface ServerContext {
root: string
pluginContainer: PluginContainer
app: connect.Server
plugins: Plugin[]
}
export async function startDevServer() {
const app = connect()
const root = process.cwd()
const startTime = Date.now()
const plugins = resolvePlugins()
const pluginContainer = createPluginContainer(plugins)
const serverContext: ServerContext = {
root: process.cwd(),
app,
pluginContainer,
plugins,
}
for (const plugin of plugins) {
if (plugin.configureServer) {
await plugin.configureServer(serverContext)
}
}
app.listen(3000, async () => {
await optimize(root)
console.log(
green('🚀 No-Bundle 服务已经成功启动!'),
`耗时: ${Date.now() - startTime}ms`
)
console.log(`> 本地访问路径: ${blue('http://localhost:3000')}`)
})
}
其中 resolvePlugins
方法我们还未定义,我们去新建一个src/node/plugins/index.ts
文件,内容如下:
import { Plugin } from "../plugin";
export function resolvePlugins(): Plugin[] {
// 下一部分会逐个补充插件逻辑
return [];
}
这个方法是用来加载插件的,后面我们编写的插件在这里引用并返回出去
入口HTML加载中间件
ok那我们首先要考虑的就是入口 HTML 如何编译和加载的问题,这里我们可以通过一个服务中间件,注意哈这个是中间件不是插件,是connect的中间件,跟koa和express的中间件是差不多的。然后配合我们的插件机制来实现。我们可以新建src/node/server/middlewares/indexHtml.ts
,内容如下:
src/node/server/middlewares/indexHtml.ts
import { NextHandleFunction } from 'connect'
import { ServerContext } from '..'
import path from 'path'
import { pathExists, readFile } from 'fs-extra'
export function indexHtmlMiddware(
serverContext: ServerContext
): NextHandleFunction {
return async (req, res, next) => {
if (req.url === '/') {
const { root } = serverContext
// 默认使用项目根目录下的 index.html
const indexHtmlPath = path.join(root, 'index.html')
if (await pathExists(indexHtmlPath)) {
const rawHtml = await readFile(indexHtmlPath, 'utf-8')
let html = rawHtml
// 通过执行插件的 transformIndexHtml 方法来对 HTML 进行自定义的修改
for (const plugin of serverContext.plugins) {
if (plugin.transformIndexHtml) {
html = await plugin.transformIndexHtml(html)
}
}
res.statusCode = 200
res.setHeader('Content-Type', 'text/html')
return res.end(html)
}
}
return next()
}
}
接着我们在服务端应用这个中间件:
src/node/server/index.ts
// 需要增加的引入语句
import { indexHtmlMiddware } from "./middlewares/indexHtml";
// 省略中间的代码
// 处理入口 HTML 资源
app.use(indexHtmlMiddware(serverContext));
app.listen(3000, async () => {
// 省略
});
然后我们可以通过pnpm dev
启动项目,然后访问http://localhost:3000
,从网络面板中你可以查看到 HTML 的内容已经成功返回:
不过当前的页面并没有任何内容,因为 HTML 中引入的 TSX 文件并没有被正确编译。接下来,我们就来可以去处理
TSX
文件的编译工作。
JS/TS/JSX/TSX编译的能力
ok那我们先新增一个中间件src/node/server/middlewares/transform.ts
,内容如下:
src/node/server/middlewares/transform.ts
import { NextHandleFunction } from 'connect'
import { isJSRequest, cleanUrl } from '../../utils'
import { ServerContext } from '../index'
import createDebug from 'debug'
const debug = createDebug('dev')
export async function transformRequest(
url: string,
serverContext: ServerContext
) {
const { pluginContainer } = serverContext
url = cleanUrl(url)
// 简单来说,就是依次调用插件容器的 resolveId、load、transform 方法
const resolvedResult = await pluginContainer.resolveId(url)
let transformResult
if (resolvedResult?.id) {
let code = await pluginContainer.load(resolvedResult.id)
if (typeof code === 'object' && code !== null) {
code = code.code
}
if (code) {
transformResult = await pluginContainer.transform(
code as string,
resolvedResult?.id
)
}
}
return transformResult
}
export function transformMiddleware(
serverContext: ServerContext
): NextHandleFunction {
return async (req, res, next) => {
if (req.method !== 'GET' || !req.url) {
return next()
}
const url = req.url
debug('transformMiddleware: %s', url)
// transform JS request
if (isJSRequest(url)) {
// 核心编译函数
let result = await transformRequest(url, serverContext)
if (!result) {
return next()
}
if (result && typeof result !== 'string') {
result = result.code as any
}
// 编译完成,返回响应给浏览器
res.statusCode = 200
res.setHeader('Content-Type', 'application/javascript')
return res.end(result)
}
next()
}
}
同时,我们也需要补充如下的工具函数和常量定义:
src/node/utils.ts
import { JS_TYPES_RE } from './constants.ts'
export const isJSRequest = (id: string): boolean => {
id = cleanUrl(id);
if (JS_TYPES_RE.test(id)) {
return true;
}
if (!path.extname(id) && !id.endsWith("/")) {
return true;
}
return false;
};
export const cleanUrl = (url: string): string =>
url.replace(HASH_RE, "").replace(QEURY_RE, "");
// src/node/constants.ts
export const JS_TYPES_RE = /.(?:j|t)sx?$|.mjs$/;
export const QEURY_RE = /?.*$/s;
export const HASH_RE = /#.*$/s;
从如上的核心编译函数
transformRequest
可以看出,Vite 对于 JS/TS/JSX/TSX 文件的编译流程主要是依次调用插件容器的如下方法:
- resolveId
- load
- transform
ok接下来我们就直接来开发这些插件拉
路径解析插件
当浏览器解析到如下的标签时:
<script type="module" src="/src/main.tsx"></script>
会自动发送一个路径为/src/main.tsx
的请求,但如果服务端不做任何处理,是无法定位到源文件的,随之会返回 404 状态码:
因此,我们需要开发一个路径解析插件,对请求的路径进行处理,使之能转换真实文件系统中的路径。我们直接新建文件src/node/plugins/resolve.ts
,内容如下:
src/node/plugins/resolve.ts
import resolve from "resolve";
import { Plugin } from "../plugin";
import { ServerContext } from "../server/index";
import path from "path";
import { pathExists } from "fs-extra";
import { DEFAULT_EXTERSIONS } from "../constants";
import { isWindows, resolveWindowPath } from "../utils";
export function resolvePlugin(): Plugin {
let serverContext: ServerContext;
return {
name: "m-vite:resolve",
configureServer(s) {
// 保存服务端上下文
serverContext = s;
},
async resolveId(id: string, importer?: string) {
// 1. 绝对路径
if (path.isAbsolute(id)) {
if (await pathExists(id)) {
return { id };
}
// 加上 root 路径前缀,处理 /src/main.tsx 的情况
id = path.join(serverContext.root, id);
if (await pathExists(id)) {
return { id };
}
}
// 2. 相对路径
else if (id.startsWith(".")) {
if (!importer) {
throw new Error("`importer` should not be undefined");
}
const hasExtension = path.extname(id).length > 1;
let resolvedId: string;
// 2.1 包含文件名后缀
// 如 ./App.tsx
if (hasExtension) {
resolvedId = resolve.sync(id, {
basedir: importer ? path.dirname(importer) : process.cwd(),
});
if (await pathExists(resolvedId)) {
return {
// 统一路径格式
id: isWindows ? resolveWindowPath(resolvedId) : resolvedId,
};
}
}
// 2.2 不包含文件名后缀
// 如 ./App
else {
// ./App -> ./App.tsx
for (const extname of DEFAULT_EXTERSIONS) {
try {
const withExtension = `${id}${extname}`;
resolvedId = resolve.sync(withExtension, {
basedir: importer ? path.dirname(importer) : process.cwd(),
});
if (await pathExists(resolvedId)) {
return {
// 统一路径格式
id: isWindows ? resolveWindowPath(resolvedId) : resolvedId,
};
}
} catch (e) {
continue;
}
}
}
}
return null;
},
};
}
ok我们先补充一下缺少的常量:
src/node/constants.ts
export const DEFAULT_EXTERSIONS = [".tsx", ".ts", ".jsx", "js"];
然后我们再补充一下工具函数:
src/node/utils.ts
export function resolveWindowPath(url: string) {
const normalurl = slash(url);
return path.posix.relative(slash(process.cwd()), normalurl);
}
ok,然后/src/main.tsx
就可以被解析成真实路径拉,模块在接下来的 load
钩子中能够正常加载。ok加载完路径了接下来该干嘛呢,当然是读取文件的内容拉,tsx
浏览器是没法识别的,我们要把文件编译成js语法,这时候我们可以使用esbuild
的transform
拉。
Esbuild 语法编译插件
这个插件的作用比较好理解,就是将 JS/TS/JSX/TSX 编译成浏览器可以识别的 JS 语法,我们直接新建src/node/plugins/esbuild.ts
文件,内容如下:
src/node/plugins/esbuild.ts
import { readFile } from 'fs-extra'
import { Plugin } from '../plugin'
import { isJSRequest } from '../utils'
import esbuild from 'esbuild'
import path from 'path'
export function esbuildTransformPlugin(): Plugin {
return {
name: 'm-vite:esbuild-transform',
// 加载模块
async load(id) {
if (isJSRequest(id)) {
try {
const code = await readFile(id, 'utf-8')
return code
} catch (e) {
return null
}
}
},
async transform(code, id) {
if (isJSRequest(id)) {
const extname = path.extname(id).slice(1)
const { code: transformedCode, map } = await esbuild.transform(code, {
target: 'esnext',
format: 'esm',
sourcemap: true,
loader: extname as 'js' | 'ts' | 'jsx' | 'tsx',
})
return {
code: transformedCode,
map,
}
}
return null
},
}
}
搞定了哇,在将 TSX 转换为浏览器可以识别的语法之后,是不是就可以直接返回给浏览器执行了呢?如果你觉得可以的话那我必须要骂你一声扑街了,别忘了我们上一章学的依赖预构建啊,如果tsx文件有里面第三方依赖路径(bare import),需要重写为预构建产物路径引用产物。而且对于绝对路径和相对路径,需要借助之前的路径解析插件进行解析。ok那我们需要写一个import的分析插件。
import分析插件
ok我们新建src/node/plugins/importAnalysis.ts
插件文件
src/node/plugins/importAnalysis.ts
import { init, parse } from 'es-module-lexer'
import { BARE_IMPORT_RE, PRE_BUNDLE_DIR } from '../constants'
import { isJSRequest, normalizePath } from '../utils'
// magic-string 用来作字符串编辑
import MagicString from 'magic-string'
import path from 'path'
import { Plugin } from '../plugin'
import { ServerContext } from '../server/index'
import type { PluginContext } from 'rollup'
export function importAnalysisPlugin(): Plugin {
let serverContext: ServerContext
return {
name: 'm-vite:import-analysis',
configureServer(s) {
// 保存服务端上下文
serverContext = s
},
async transform(this: PluginContext, code: string, id: string) {
// 只处理 JS 相关的请求
if (!isJSRequest(id)) {
return null
}
await init
// 解析 import 语句
const [imports] = parse(code)
const ms = new MagicString(code)
// 对每一个 import 语句依次进行分析
for (const importInfo of imports) {
// 举例说明: const str = `import React from 'react'`
// str.slice(s, e) => 'react'
const { s: modStart, e: modEnd, n: modSource } = importInfo
if (!modSource) continue
// 第三方库: 路径重写到预构建产物的路径
if (BARE_IMPORT_RE.test(modSource)) {
// const bundlePath = path.join(
// serverContext.root,
// PRE_BUNDLE_DIR,
// `${modSource}.js`
// )
const bundlePath = normalizePath(
path.join('/', PRE_BUNDLE_DIR, `${modSource}.js`)
)
ms.overwrite(modStart, modEnd, bundlePath)
} else if (modSource.startsWith('.') || modSource.startsWith('/')) {
// 直接调用插件上下文的 resolve 方法,会自动经过路径解析插件的处理
const resolved = await this.resolve(modSource, id)
if (resolved) {
ms.overwrite(modStart, modEnd, resolved.id)
}
}
}
return {
code: ms.toString(),
// 生成 SourceMap
map: ms.generateMap(),
}
},
}
}
ok,我们便完成了 JS 代码的 import
分析工作。接下来,我们把上面实现的三个插件进行注册,就是刚才的resolvePlugins
函数拉。
src/node/plugin/index.ts
import { esbuildTransformPlugin } from "./esbuild";
import { importAnalysisPlugin } from "./importAnalysis";
import { resolvePlugin } from "./resolve";
import { Plugin } from "../plugin";
export function resolvePlugins(): Plugin[] {
return [resolvePlugin(), esbuildTransformPlugin(), importAnalysisPlugin()];
}
接下来我们可以在playground
项目下执行pnpm dev
,,不过在执行之前要把不是tsx
、ts
、的文件先注释掉(比如css文件和静态资源文件,我们后面再处理),在浏览器里面访问http://localhost:3000
,见证奇迹的时刻到了哇,我们可以看到如下页面:
打开控制台看看main.tsx的内容,可以看到react和react-dom的引用都被换成与构建产物的地址了:
ok我们完成了 JS/TS/JSX/TSX 文件的编译拉,我们还差什么呢,页面页面一定是离不开css的对吧,我们的demo项目中的css代码是这样引入的:
但是浏览器没法识别这样的语法哇,我们需要将这个模块包装成浏览器可以识别的模块才行的哇,也就是js模块拉,其中模块加载和转换的逻辑我们可以通过插件来实现。当然,首先我们需要在 transform
中间件中允许对 CSS 的请求进行处理,内容如下:
src/node/server/middlewares/transform.ts
// 需要增加的导入语句
+ import { isCSSRequest } from '../../utils';
export function transformMiddleware(
serverContext: ServerContext
): NextHandleFunction {
return async (req, res, next) => {
if (req.method !== "GET" || !req.url) {
return next();
}
const url = req.url;
debug("transformMiddleware: %s", url);
// transform JS request
- if (isJSRequest(url)) {
+ if (isJSRequest(url) || isCSSRequest(url)) {
// 后续代码省略
}
next();
};
}
ok接下来我们来补充对应的工具函数:
src/node/utils.ts
export const isCSSRequest = (id: string): boolean =>
cleanUrl(id).endsWith(".css");
CSS编译插件
ok我们接下来要开发 CSS
的编译插件,没有css可不行,界面太丑了,我们直接新建一个src/node/plugins/css.ts
文件,内容如下:
src/node/plugins/css.ts
import { readFile } from "fs-extra";
import { Plugin } from "../plugin";
export function cssPlugin(): Plugin {
return {
name: "m-vite:css",
load(id) {
// 加载
if (id.endsWith(".css")) {
return readFile(id, "utf-8");
}
},
// 转换逻辑
async transform(code, id) {
if (id.endsWith(".css")) {
// 包装成 JS 模块
const jsContent = `
const css = '${code.replace(/\r\n/g, "")}';
const style = document.createElement("style");
style.setAttribute("type", "text/css");
style.innerHTML = css;
document.head.appendChild(style);
export default css;
`.trim();
return {
code: jsContent,
};
}
return null;
},
};
}
这个插件的逻辑比较简单,主要是将封装一层 JS
模板代码,将CSS
包装成一个 ES
模块,当浏览器执行这个模块的时候,会创建styl
e标签将CSS
文件字符串的内容给搞到标签里去,然后就可以在页面中生效了。
接着我们来注册这个 CSS 插件:
src/node/plugins/index.ts
import { esbuildTransformPlugin } from "./esbuild";
import { resolvePlugin } from "./resolve";
import { importAnalysisPlugin } from "./importAnalysis";
import { cssPlugin } from "./css";
import { Plugin } from "../plugin";
export function resolvePlugins(): Plugin[] {
return [
resolvePlugin(),
esbuildTransformPlugin(),
importAnalysisPlugin(),
cssPlugin(),
];
}
现在,我们通过pnpm dev
来启动 playground
项目,在tsx
将css
文引入(之前是注释掉的),ok,启动之后我们可以看到一个好看的界面,乱杀之前那个:
大家这时候可能会说了:诶,好像少了个react
的图标哇,没有灵魂了,不好看了,心情不好了,不想再看了,难受。别急,接下来我们就来实现静态资源的加载,让大大的react
图标出来哈。
静态资源加载插件
我们先来看看react图标在哪里哈:
站在 no-bundle 服务的角度,从如上的代码我们可以分析出静态资源的两种请求:
- import 请求。如
import logo from "./logo.svg"
。 - 资源内容请求。如 img 标签将资源 url 填入 src,那么浏览器会请求具体的资源内容。
因此,接下来为了实现静态资源的加载,我们需要做两手准备: 对静态资源的 import
请求返回资源的 url
;对于具体内容的请求,读取静态资源的文件内容,并响应给浏览器。
首先处理 import
请求,我们可以在 TSX
的 import
分析插件中,给静态资源相关的 import
语句做一个标记:
src/node/plugins/importAnalysis.ts
async transform(code, id) {
// 省略前面的代码
for (const importInfo of imports) {
const { s: modStart, e: modEnd, n: modSource } = importInfo;
if (!modSource) continue;
+ // 静态资源
+ if (modSource.endsWith(".svg")) {
+ // 加上 ?import 后缀,其他的静态资源同理
+ const resolvedUrl = normalizePath(
+ path.relative(
+ path.dirname(id),
+ path.resolve(path.dirname(id), modSource)
+ )
+ );
+ ms.overwrite(modStart, modEnd, `./${resolvedUrl}?import`);
+ continue;
}
}
}
ok我们看看编译后的App.tsx是什么样的:
ok我们可以看到路径后面多了个?import
的后缀,有这个后缀代表这是静态资源的请求,然后我们在transform
插件里对这种请求进行处理:
src/node/server/middlewares/transform.ts
// 需要增加的导入语句
+ import { isImportRequest } from '../../utils';
export function transformMiddleware(
serverContext: ServerContext
): NextHandleFunction {
return async (req, res, next) => {
if (req.method !== "GET" || !req.url) {
return next();
}
const url = req.url;
debug("transformMiddleware: %s", url);
// transform JS request
- if (isJSRequest(url) || isCSSRequest(url)) {
+ if (isJSRequest(url) || isCSSRequest(url) || isImportRequest(url)) {
// 后续代码省略
}
next();
};
}
然后我们补充工具函数
src/node/utils.ts
export function isImportRequest(url: string): boolean {
return url.endsWith("?import");
}
ok接下来我们就可以开发静态资源插件了。新建src/node/plugins/assets.ts
,内容如下:
src/node/plugins/assets.ts
import { Plugin } from "../plugin";
import { cleanUrl, normalizePath, removeImportQuery } from "../utils";
export function assetPlugin(): Plugin {
return {
name: "m-vite:asset",
async load(id) {
const cleanedId = removeImportQuery(cleanUrl(normalizePath(id)));
// 这里仅处理 svg
if (cleanedId.endsWith(".svg")) {
return {
// 包装成一个 JS 模块
// 因为这里的window用户路径有点问题,要盘符给去掉,mac用户不用replace
// window用户要将盘符去掉
code: isWindows
? `export default "${cleanedId.replace(/\D\:/g, "")}"`
: `export default "${cleanedId}"`,
};
}
},
};
}
我们补充一下工具函数:
src/node/utils.ts
export function removeImportQuery(url: string): string {
return url.replace(/\?import$/, "");
}
ok我们来注册这个插件:
src/node/plugins/index.ts
import { esbuildTransformPlugin } from "./esbuild";
import { resolvePlugin } from "./resolve";
import { importAnalysisPlugin } from "./importAnalysis";
import { cssPlugin } from "./css";
import { assetPlugin } from "./assets";
import { Plugin } from "../plugin";
export function resolvePlugins(): Plugin[] {
return [
resolvePlugin(),
esbuildTransformPlugin(),
importAnalysisPlugin(),
cssPlugin(),
assetPlugin(),
];
}
}
OK,我们处理完了静态资源的 import 请求,接着我们还需要处理非 import 请求,返回资源的具体内容。我们可以通过一个中间件来进行处理,我们新建src/node/server/middlewares/static.ts
这个文件,内容如下:
src/node/server/middlewares/static.ts
import { NextHandleFunction } from "connect";
import { isImportRequest } from "../../utils";
// 一个用于加载静态资源的中间件
import sirv from "sirv";
export function staticMiddleware(): NextHandleFunction {
const serveFromRoot = sirv("/", { dev: true });
return async (req, res, next) => {
if (!req.url) {
return;
}
// 不处理 import 请求
if (isImportRequest(req.url)) {
return;
}
serveFromRoot(req, res, next);
};
}
我们在服务端中注册这个中间件:
src/node/server/index.ts
// 需要添加的引入语句
+ import { staticMiddleware } from "./middlewares/static";
export async function startDevServer() {
// 前面的代码省略
+ app.use(staticMiddleware());
app.listen(3000, async () => {
// 省略实现
});
}
在playground里执行pnpm dev
运行项目,然后我们就可以看到大大的react图标了:
ok我们mini-vite
的插件机制的开发终于搞定了,怎么样是不是干货满满呢,有问题可以找我一起讨论哇!!!
往期文章
mini-vite仓库地址(里面有对应的提交记录,如果有兴趣可以拉下来跑一跑哇)
转载自:https://juejin.cn/post/7154381380406181896