背景
众所周知,vite
作为一个新生的前端构建工具,它很快!
早在 2021 年,我本想将公司中正在使用的 webpack
更替为 vite
,让公司中的其他同事也能体验体验 vite
带来的快乐。
由于不支持 require()
方法, 模块化的样式文件必须是 .module.xxx
后缀...
问题当然远不止我上述所列举的这些,所以当一个项目想要迁移时,那工作量是非常大的。而且公司项目过多,所以到最后放弃了...
当时在探索迁移可否实现的几天中还是很乐观的,所以开始一个问题一个问题解决。
首先 是 css
模块化的问题,为了解决这个问题我向 vite 提交了一个 pr
,可给出的回复是要求使用插件解决。当时也没有现成插件,好吧,手写一个,于是写了一个 vite
插件 vite-plugin-css-modules用于解决问题,目前为止这个插件周下载量很少,也不是本文的重点,只是让我知道了让解决问题都必须开发 vite
插件。
然后 的大问题是项目中不能使用 require
这个方法。于是又继续写了一个插件 vite-plugin-require。本插件到目前为止周下载量都在 6k-1w 之间,接下来的文章内容将会展开叙述本插件。
插件实现思路
实现思路比较简单,不考虑 require.content
方法。
就五步,每一步实现代码都不会很多,所以标题中的 50 行代码都多了,用不了这么多,根本用不了...
- 首先是拿到代码中的
require(xxx)
- 然后在对
require
的参数进行解析,拿到一个文件路径 - 将拿到的文件路径放置到代码文件顶部使用
import
或者importMetaUrl
引入。 - 将最开始拿到的
require(xxx)
替换为定义在顶部的import\importMetaUrl
变量 - 生成并输出最终代码
实现代码
理论有了,那就来实践。 代码的操作使用babel
提供的AST
插件
typescript
import * as parser from "@babel/parser";
import traverse from "@babel/traverse";
import generate from "@babel/generator";
import { Plugin } from "vite";
import { importDeclaration, importDefaultSpecifier, stringLiteral, identifier } from "@babel/types";
export default function vitePluginRequire(opts?: { fileRegex?: RegExp; log?: (...arg: any[]) => void }): Plugin {
const { fileRegex = /(.jsx?|.tsx?|.vue)$/, log } = opts || {};
return {
name: "vite-plugin-require",
async transform(code: string, id: string) {
// Exclude files in node_modules
if (/\/node_modules\//g.test(id)) return;
let newCode = code;
if (fileRegex.test(id)) {
let plugins: parser.ParserPlugin[] = /(.vue)$/.test(id) ? [require("vue-loader")] : ["jsx"];
const ast = parser.parse(code, {
sourceType: "module",
plugins,
});
traverse(ast, {
enter(path) {
if (path.isIdentifier({ name: "require" })) {
if ((path.container as Record<string, any>)?.arguments?.[0]) {
path.node.name = "";
if ((path.container as Record<string, any>)?.arguments?.[0]?.value) {
// Insert import at the top to pack resources when vite packs
const realPath = `vitePluginRequire_${new Date().getTime()}_${parseInt(Math.random() * 10000 + 100 + "")}`;
const importAst = importDeclaration(
[importDefaultSpecifier(identifier(realPath))],
stringLiteral((path.container as Record<string, any>)?.arguments?.[0]?.value as string)
);
ast.program.body.unshift(importAst as any);
(path.container as Record<string, any>).arguments[0].value = realPath;
(path.container as Record<string, any>).arguments[0].extra.raw = realPath;
(path.container as Record<string, any>).arguments[0].extra.rawValue = realPath;
}
}
}
},
});
const output = generate(ast, {});
newCode = output.code;
}
return { code: newCode };
},
};
}
代码分析
- 23 行代码实现了 思路1
- 31 行代码实现了 思路2
- 33 行代码实现了 思路3
- 34 行代码实现了 思路4
- 45 行代码实现了 思路5
实现虽然实现了,可是会有很多的特殊情况,比如人家在 require
函数中传入的是一个变量参数,或者其他参数..., 又或者别人想使用 import.meta.url
的实现原理。
所以这个插件在发布后就不断地升级,到目前已经达到了 175 行代码。
最后
可以看到一旦使用场景增加,再小的插件也会面临很多的问题需要考虑。
仓库地址: github.com/wangzongmin...
开源与维护不易,记得给个 start
🌹