看过白泽开源团队出品的baize-quick-study
的小伙伴们,可能或多或少都会有这样的一个问题,为啥一行<CodeDemo />
就可以让当前页的代码
显示到网页中?这到底是什么神奇
的写法
呀?
链接:baize-quick-study.pages.dev/main
源码:github.com/baizeteam/b…

莫慌莫慌,待我们来一步步揭秘。
拉好扶手,焊死车门,一个也不准下车!!!

代码渲染
咱们先找到CodeDemo
的代码,看看它里面做了些什么操作?
哦,easy,咱们可以看到里面划分了父子组件(CodeDemo 和 CodeDemoItem)。
然后其中的CodeDemoItem,不就是用highlight.js
对代码进行渲染吗?
秒了,完结撒花!!!


嘿嘿,看到这,老夫只想说一句:
年轻人是不是高兴的太早了?
年轻人是不是高兴的太早了?
年轻人是不是高兴的太早了?
哎呀,重要的事情就是容易不小心多说了两遍。反正撤不回了,那就继续往下讲吧。
是的,通过highlight.js
来渲染代码是没有问题的,但是数据
怎么来的?
咱们继续看父组件
CodeDemo,可以看到是从props中获取数据,然后传递给CodeDemoItem进行渲染的。

好像也没啥问题呀!
小伙伴们有没有发现遗漏了一个问题?
咱们外部使用组件时,也没有传入
codeData、codePath、fileListCode这些props啊,为啥它能拿到的?

糟糕,脑子好痒,好像要长脑子了!!!

emmm,确实常规写法
是要从外层组件中传入props的
但是,有没有一种可能,这不是常规写法呢?
众所周知,像react、vue这样的代码,直接放在浏览器中是无法直接运行的,需要通过一层转译
才可以执行的。又或者es6怎么运行在低版本的浏览器中?
一般来说,这层转译都是通过babel
来处理的,那你说有没有可能CodeDemo的数据也是这样获取的?
是的,你猜对了。
答案就是:vite 自定义插件
+ babel
+ ast
插件详解
插件入口
咱们可以在vite.config.ts
中找到插件的入口

下面用react版本的vite插件进行讲解
拦截 tsx
首先,我们需要在插件的 load
钩子中拦截所有后缀为 .tsx
的文件。通过自定义插件,我们可以让 Vite 在构建时对这些文件进行处理。
function viteRenderCode(): PluginOption {
return {
name: "vite-render-code",
enforce: "pre",
load(id) {
// 拦截所有的tsx文件
if (id.endsWith(".tsx")) {
}
}
}
注入props
通过解析文件的 AST
,我们可以检查每个 React 组件的定义。我们需要判断组件名称是否为 CodeDemo
。找到 CodeDemo
组件后,我们将提取其相关参数(例如 props
中的 codeData
和 codePath
)并对其进行修改或注入新的值。
const ast = parse(code, {
sourceType: "module",
plugins: ["typescript", "jsx"],
});
const currentPath = id;
traverse.default(ast, {
JSXOpeningElement(path) {
if (path.node.name.name === "CodeDemo") {
// 将 code 作为 props 注入到 CodeDemo 组件中
// 将fileList构建成对应的ast节点
}
},
});
显示其他文件
需要注意的是fileList
,这个主要是让我们可以将一些想显示的文件也一并显示出来。这里主要是通过ast语法树
来构建ast节点
。
const astFileListCode = t.jsxAttribute(
t.jsxIdentifier("fileListCode"),
t.jsxExpressionContainer(
t.arrayExpression(
fileListCode.map((item) =>
t.objectExpression([
t.objectProperty(t.stringLiteral("fileCode"), t.stringLiteral(item.fileCode)),
t.objectProperty(t.stringLiteral("filePath"), t.stringLiteral(item.filePath)),
]),
),
),
),
);
path.node.attributes.push(astFileListCode);
完整代码
// viteRenderCode.ts
import { join } from "path";
import { PluginOption } from "vite";
import { readFileSync } from "fs";
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
import generate from "@babel/generator";
import t from "@babel/types";
const htmlEntities: { [key: string]: string } = {
"&": "&",
"<": "<",
">": ">",
"'": "'",
'"': """,
"`": "`",
"^": "ˆ",
"~": "˜",
"—": "—",
"•": "•",
"–": "–",
"?": "?",
":": ":",
$: "$",
};
const escapeHtml = (str: string) => {
return str?.replace(/[&<>'"`^~—•–?:$]/g, (tag) => htmlEntities[tag] || tag);
};
const getReactComponentProps = ({ data, name }) => {
return {
type: "JSXAttribute",
name: {
type: "JSXIdentifier",
name: name,
},
value: {
type: "StringLiteral",
value: data,
},
};
};
const addReactCompoentProps = ({ path, data, name }) => {
const params = getReactComponentProps({
data,
name,
});
path.node.attributes.push(params);
};
function viteRenderCode(): PluginOption {
let _originalConfig;
let _resolvedConfig;
let _basePath = join(process.cwd(), "..");
return {
name: "vite-render-code",
enforce: "pre",
configResolved(resolvedConfig) {
_resolvedConfig = resolvedConfig;
},
config(config) {
_originalConfig = config;
},
load(id) {
if (id.endsWith(".tsx")) {
const code = readFileSync(id, "utf-8");
if (code.indexOf("<CodeDemo") !== -1 && id.indexOf("CodeDemo") === -1) {
const ast = parse(code, {
sourceType: "module",
plugins: ["typescript", "jsx"],
});
const currentPath = id; // id.replace(_basePath, "");
traverse.default(ast, {
JSXOpeningElement(path) {
if (path.node.name.name === "CodeDemo") {
// 将 code 作为 props 注入到 CodeDemo 组件中
const codeProp = path.node.attributes.find((attr) => attr.name.name === "codeData");
if (!codeProp) {
addReactCompoentProps({
path,
data: escapeHtml(code),
name: "codeData",
});
addReactCompoentProps({
path,
data: currentPath,
name: "codePath",
});
}
const fileListProp = path.node.attributes
.find((attr) => attr.name.name === "fileList")
?.value.expression.elements.map((item) => item.value);
if (fileListProp) {
const fileListCode = [];
for (let item of fileListProp) {
const curAlias = item.split("/")[0];
const filePath = item.replace(curAlias, _originalConfig.resolve.alias[curAlias]);
const fileCode = readFileSync(filePath, "utf-8");
fileListCode.push({
fileCode: escapeHtml(fileCode),
filePath: filePath, // filePath.replace(_basePath, ""),
});
}
const astFileListCode = t.jsxAttribute(
t.jsxIdentifier("fileListCode"),
t.jsxExpressionContainer(
t.arrayExpression(
fileListCode.map((item) =>
t.objectExpression([
t.objectProperty(t.stringLiteral("fileCode"), t.stringLiteral(item.fileCode)),
t.objectProperty(t.stringLiteral("filePath"), t.stringLiteral(item.filePath)),
]),
),
),
),
);
path.node.attributes.push(astFileListCode);
}
}
},
});
const { code: transformedCode } = generate.default(ast);
return transformedCode;
}
return code;
}
return null;
},
};
}
export default viteRenderCode;
小结
本文主要介绍了如何通过编写一个自定义的 Vite 插件
,结合AST
,将我们项目中的真实代码
动态展示到网页中。通过本文,希望小伙伴们可以学习到在构建过程中如何处理文件,解析其中的组件,并根据需要注入特定的属性或代码,进而掌握通过ast修改源码
以及开发vite插件
的能力。