likes
comments
collection
share

一起写个vite吧!(2) --插件机制开发

作者站长头像
站长
· 阅读数 43

继上一章节一起写个vite吧!(1) --环境搭建+依赖预构建咱们继续开干哈,这一节咱们来搞vite的插件机制的实现,干货满满哈。vite在开发的阶段实现了一个按需加载的服务器,每个文件请求进来都会经历一系列的编译流程,生产环境下,vite同样会执行一系列编译过程,将编译结果交给 Rollup 进行模块打包。这一系列的编译过程指的就是 Vite 的插件工作流水线(Pipeline),而插件功能又是 Vite 构建能力的核心,因此谈到阅读 Vite 源码,我们永远绕不开插件的作用与实现原理。

  • 在生产环境中 Vite 直接调用 Rollup 进行打包,所以 Rollup 可以调度各种插件;
  • 在开发环境中,Vite 模拟了 Rollup 的插件机制,设计了一个PluginContainer 对象来调度各个插件。

值得一提的是 VitepreactWMR 中得到了启发,把插件机制做成兼容 Rollup 的格式。简单的介绍一下 Rollup 插件,其实插件这个东西,就是 Rollup 对外提供一些运行过程中一些时机的钩子,还有一些工具方法,让用户去写一些配置代码,以此介入 Rollup 运行的各个时机之中。而 Vite 需要做的就是基于 Rollup 设计的接口进行扩展,在保证 Rollup 插件兼容的可能性的同时,再加入一些 Vite 特有的钩子和属性来扩展。Rollup的插件可以参考这篇文章Rollup 之插件机制浅析

  1. 实现 Rollup 插件钩子的调度
  2. 实现插件钩子内部的 Context 上下文对象

Vite架构图:

一起写个vite吧!(2) --插件机制开发

对于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
}

很显然,ViteRollupPluginContext 对象重新实现了一遍,因为只是开发阶段用到,所以去除了一些打包相关的方法实现,我们这里则只实现上下文对象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 的内容已经成功返回:

一起写个vite吧!(2) --插件机制开发

不过当前的页面并没有任何内容,因为 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 状态码:

一起写个vite吧!(2) --插件机制开发

因此,我们需要开发一个路径解析插件,对请求的路径进行处理,使之能转换真实文件系统中的路径。我们直接新建文件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语法,这时候我们可以使用esbuildtransform拉。

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,,不过在执行之前要把不是tsxts、的文件先注释掉(比如css文件和静态资源文件,我们后面再处理),在浏览器里面访问http://localhost:3000,见证奇迹的时刻到了哇,我们可以看到如下页面:

一起写个vite吧!(2) --插件机制开发

打开控制台看看main.tsx的内容,可以看到react和react-dom的引用都被换成与构建产物的地址了:

一起写个vite吧!(2) --插件机制开发

ok我们完成了 JS/TS/JSX/TSX 文件的编译拉,我们还差什么呢,页面页面一定是离不开css的对吧,我们的demo项目中的css代码是这样引入的:

一起写个vite吧!(2) --插件机制开发

但是浏览器没法识别这样的语法哇,我们需要将这个模块包装成浏览器可以识别的模块才行的哇,也就是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 模块,当浏览器执行这个模块的时候,会创建style标签将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 项目,在tsxcss文引入(之前是注释掉的),ok,启动之后我们可以看到一个好看的界面,乱杀之前那个:

一起写个vite吧!(2) --插件机制开发

大家这时候可能会说了:诶,好像少了个react的图标哇,没有灵魂了,不好看了,心情不好了,不想再看了,难受。别急,接下来我们就来实现静态资源的加载,让大大的react图标出来哈。

静态资源加载插件

我们先来看看react图标在哪里哈:

一起写个vite吧!(2) --插件机制开发

站在 no-bundle 服务的角度,从如上的代码我们可以分析出静态资源的两种请求:

  • import 请求。如 import logo from "./logo.svg"
  • 资源内容请求。如 img 标签将资源 url 填入 src,那么浏览器会请求具体的资源内容。

因此,接下来为了实现静态资源的加载,我们需要做两手准备: 对静态资源的 import 请求返回资源的 url;对于具体内容的请求,读取静态资源的文件内容,并响应给浏览器。

首先处理 import 请求,我们可以在 TSXimport 分析插件中,给静态资源相关的 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是什么样的:

一起写个vite吧!(2) --插件机制开发

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图标了:

一起写个vite吧!(2) --插件机制开发

ok我们mini-vite的插件机制的开发终于搞定了,怎么样是不是干货满满呢,有问题可以找我一起讨论哇!!!

往期文章

mini-vite仓库地址(里面有对应的提交记录,如果有兴趣可以拉下来跑一跑哇)

转载自:https://juejin.cn/post/7154381380406181896
评论
请登录