Logo
“在浏览器中编译运行 React”的封面

在浏览器中编译运行 React

Avatar

Skyone

科技爱好者

一直很好奇 React 官网的在线编辑器是如何实现的,于是稍微研究了一下,通过 TypeScript 和 Babel 实现在浏览器中编译 React。

这个项目本身也是使用 TypeScript, Webpack 和 Babel 构建的,源码在 react-online - Skyone Git

在开始之前,请确保你了解了 React 的代码如何从 TSX 格式转换为浏览器可以执行的 JavaScript 代码,如果不了解,可以参考 TypeScript 官网Babel 官网。当然,打包工具 Webpack 也必须要了解。

项目结构

由于整个程序就一个页面,没有路由等麻烦事,所以就按最简单的方式来实现。

项目结构差不多是这样的:

├── src
│   ├── index.ts
│   └── style.css
├── public
├── scripts
└── package.json

直到最后,我用到了如下依赖:

{
  "devDependencies": {
    "@babel/core": "^7.23.6",
    "@babel/preset-env": "^7.23.6",
    "@codemirror/commands": "^6.3.2",
    "@codemirror/lang-javascript": "^6.2.1",
    "@codemirror/language": "^6.9.3",
    "@codemirror/view": "^6.22.3",
    "@types/node": "^20.10.5",
    "@types/webpack-env": "^1.18.4",
    "@uiw/codemirror-theme-github": "^4.21.21",
    "autoprefixer": "^10.4.16",
    "babel-loader": "^9.1.3",
    "clean-webpack-plugin": "^4.0.0",
    "codemirror": "^6.0.1",
    "copy-webpack-plugin": "^11.0.0",
    "cross-env": "^7.0.3",
    "css-loader": "^6.8.1",
    "css-minimizer-webpack-plugin": "^5.0.1",
    "html-webpack-plugin": "^5.6.0",
    "mini-css-extract-plugin": "^2.7.6",
    "postcss": "^8.4.32",
    "postcss-loader": "^7.3.3",
    "style-loader": "^3.3.3",
    "tailwindcss": "^3.4.0",
    "terser-webpack-plugin": "^5.3.9",
    "ts-loader": "^9.5.1",
    "typescript": "^5.3.3",
    "webpack": "^5.89.0",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^4.15.1",
    "webpack-merge": "^5.10.0",
    "webpackbar": "^6.0.0"
  }
}

画 UI

首把 UI 画好再搞逻辑,这个 UI 应该有两个部分,一个是编辑器,一个是预览区域。这里我使用 tailwindcss 来实现。

奈何实在是不会设计 UI ,将就着看吧。

┌───────────────────────┬───────────────────────┐
│       Options         │                       │
├───────────────────────┤                       │
│                       │                       │
│                       │        Preview        │
│        Editor         │                       │
│                       │                       │
│                       │                       │
└───────────────────────┴───────────────────────┘

编译 TSX 代码

有两个选择,一个是使用 BabelTSX 代码转换为 JS 代码,但是这个过程中并没有进行任何类型检查,只是做语法检查。不像在 IDE 中会有实时的 typescript 类型检查,浏览器中只进行类型检查还不如直接使用 javascript

另一个是先用 typescriptTSX 转换为 ESNext 代码,然后再使用 BabelESNext 转换为 ES5 代码。这个过程中会进行类型检查。

我选择了第二种方式,因为只有这样才可以在浏览器中进行类型检查。

(移除 JSX 语法)
 typescript       Babel
      |             |
TSX   ->   ESNext   ->   ES5

查了一些这两个库的文档,不做过多其他配置的情况下,编译很简单。

其中 typescript 使用 umd 引入后会挂载到全局的 ts 字段里,编译函数是 transpileModule(code, tsconfig)

function compile(code) {
    const js = window.ts.transpileModule(code, {
        compilerOptions: {
            target: "ESNext",
            jsx: "preserve",
            sourceMap: false,
        },
    }).outputText;
    // ...
}

Babel 使用 umd 引入后会挂载到全局的 Babel 字段里,编译函数是 transform(code, options),所以有:

function compile(code) {
    // ...
    return transform(js, {presets: ["env", "react"]}).code;
}

核心代码就这几行,其他的就是一些 UI 的操作了。

语法高亮编辑器

这里我使用了 codemirror@6 来实现。说实话,我就没见到过文档写的这么乱的库,找了半天愣是找不到一个完整的例子,全是代码片段...

对于一般用户,谁会慢慢看 API Reference 啊,我只需要最简单的实现就行了,可官网上连个最简单的例子都没有。

总之,最后还是勉强整出来了,虽然功能不多,但对我来说,只有有实时的语法高亮就行了。

import {defaultKeymap, indentWithTab} from "@codemirror/commands";
import {javascript} from "@codemirror/lang-javascript";
import {indentUnit} from "@codemirror/language";
import {keymap} from "@codemirror/view";
import {githubLight} from "@uiw/codemirror-theme-github";
import {basicSetup, EditorView} from "codemirror";

const textarea = document.getElementById("input-code") as HTMLDivElement;

const editor = new EditorView({
    parent: textarea,
    extensions: [
        basicSetup,
        javascript({typescript: true, jsx: true}),
        githubLight,
        keymap.of([...defaultKeymap, indentWithTab]),
        indentUnit.of("    "),
    ],
});

下面是读取和写入值的方法:

function getValue() {
    return editor.state.doc.toString();
}

function setValue(value: string) {
    editor.dispatch({
        changes: {
            from: 0,
            to: editor.state.doc.length,
            insert: value,
        },
    });
}

了解这么多应该就够用了。

实现预览

这里我使用了 iframe 来实现,因为不能让 react 等库的代码污染到全局,所以使用 iframe 来隔离环境。说的复杂,其实也就是创建一个 iframe,然后将编译后的代码放进去就行了。

因为没有跨域问题, JavaScript 可以拿到 iframe 里的 window 对象,各种操作其实都很简单。大概像这样:

function applyChaneg() {
    const code = editor.state.doc.toString();
    // compile code ...
    const html = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>React 演练场</title>
    <script src="/script/[email protected]"></script>
    <script src="/script/[email protected]"></script>
    ${libraries.join("\n    ")}
</head>
<body>
    <div id="root"></div>
    <script>${result} </script>
</body>
</html>
    `.trim();
    const contentDocument = page.contentDocument!;
    contentDocument.open();
    contentDocument.write(html);
    contentDocument.close();
}
// 监听 Ctrl + S 进行编译
window.addEventListener("keydown", (e) => {
    if (e.ctrlKey && e.key === "s") {
        e.preventDefault();
        applyChaneg();
    }
});
// 监听按钮点击进行编译

button.addEventListener("click", async (e) => {
    applyChange();
});

总结

看到这里,你应该觉得也就这种程度嘛~,其实,在一边查资料一边写真的很浪费时间,我用了两天才写完,大概 10 个小时左右,如果你直接看这篇文章,应该只需要 1 个小时左右就能写完。

最后,贴一个例子吧:

这个项目的源码在 react-online - Skyone Git,欢迎大家提出建议。


隐私政策

Copyright © Skyone 2025