🔥为啥一行代码就可以让当前页代码显示到网页中?

看过白泽开源团队出品的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 在构建时对这些文件进行处理。

typescript 复制代码
function viteRenderCode(): PluginOption {
  return {
    name: "vite-render-code",
    enforce: "pre",
    load(id) {
        // 拦截所有的tsx文件
        if (id.endsWith(".tsx")) {
        }
    }
  }

注入props

通过解析文件的 AST,我们可以检查每个 React 组件的定义。我们需要判断组件名称是否为 CodeDemo。找到 CodeDemo 组件后,我们将提取其相关参数(例如 props 中的 codeDatacodePath)并对其进行修改或注入新的值。

typescript 复制代码
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节点

typescript 复制代码
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);

完整代码

typescript 复制代码
// 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 } = {
  "&": "&amp;",
  "<": "&lt;",
  ">": "&gt;",
  "'": "&#39;",
  '"': "&quot;",
  "`": "&grave;",
  "^": "&circ;",
  "~": "&tilde;",
  "---": "&mdash;",
  "•": "&bull;",
  "--": "&ndash;",
  "?": "&#63;",
  ":": "&#58;",
  $: "&#36;",
};

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插件的能力。

相关推荐
Edward Nygma30 分钟前
springboot3+vue3融合项目实战-大事件文章管理系统-更新用户密码
android·开发语言·javascript
sunbyte1 小时前
Three.js + React 实战系列 - 职业经历区实现解析 Experience 组件✨(互动动作 + 3D 角色 + 点击切换动画)
javascript·react.js·3d
2401_831943321 小时前
Element Plus对话框(ElDialog)全面指南:打造灵活弹窗交互
前端·vue.js·交互
strongwyy1 小时前
DA14585墨水屏学习(2)
前端·javascript·学习
好青崧1 小时前
冒泡排序的原理
前端
椒盐螺丝钉1 小时前
CSS 基础知识分享:从入门到注意事项
前端·css
球球和皮皮2 小时前
Babylon.js学习之路《一、初识 Babylon.js:什么是 3D 开发与 WebGL 的完美结合?》
javascript·3d·前端框架·ar·vr
冬阳春晖2 小时前
web animation API 锋利的css动画控制器 (更新中)
前端·javascript·css
Python私教3 小时前
使用FastAPI和React以及MongoDB构建全栈Web应用05 FastAPI快速入门
前端·react.js·fastapi
浪裡遊3 小时前
Typescript中的对象类型
开发语言·前端·javascript·vue.js·typescript·ecmascript