前言
随着时间的推移,在线IDE应用越来越多,功能也越来越完善。比如
- codesandbox: codesandbox.io
- jsbin: jsbin.com/?html,outpu… (个人感觉这个很轻量)
- codepen: codepen.io/pen/
- jsfiddle: jsfiddle.net/
- 菜鸟工具: c.runoob.com/
- ...
于是乎,就对这些在线IDE的实现原理产生了好奇,自己也想尝试实现一个类似的功能。这里有一篇文章详细讲述了codesandbox的实现原理:传送门
我们尝试实现一个这样的功能,左边输入代码、右边预览效果。
文件结构如下
分别贴出代码内容
(1)package.json
{
"name": "react",
"version": "1.0.0",
"description": "React example starter project",
"keywords": [
"react",
"starter"
],
"main": "src/index.js",
"dependencies": {
"@babel/runtime": "7.13.8",
"@babel/standalone": "7.13.11",
"acorn": "8.1.0",
"escodegen": "2.0.0",
"lodash": "4.17.21",
"object-path": "0.11.5",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-monaco-editor": "0.43.0",
"react-scripts": "4.0.0"
},
"devDependencies": {
"typescript": "4.1.3"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
(2)index.js
import { StrictMode } from "react";
import ReactDOM from "react-dom";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<App />
</StrictMode>,
rootElement
);
(3)App.jsx
import "./styles.css";
import SandBox from './SandBox';
export default function App() {
return (
<div className="App">
<SandBox />
</div>
);
}
(4)SandBox.jsx
import React, { useState, useEffect, useRef } from "react";
import _debounce from "lodash/debounce";
import { createEditor } from "./util";
// import MonacoEditor from "react-monaco-editor";
function SandBox() {
const viewRef = useRef(null);
const runtimeRef = useRef(null);
const [code, setCode] = useState(`
function HolyCow() {
return <span>HolyCow, My God!</span>
}
<HolyCow />
`);
useEffect(() => {
runtimeRef.current = createEditor(viewRef.current);
runtimeRef.current.run(code);
}, []);
const run = _debounce((newCode) => {
runtimeRef.current.run(newCode || code);
}, 500);
const onCodeChange = ({ target: { value } }) => {
setCode(value);
run(value);
};
return (
<div className="container" style={{ display: "flex" }}>
<div className="code-editor" style={{ flex: 1 }}>
<textarea value={code} onChange={onCodeChange} />
</div>
<div style={{ flex: 1 }}>
<div className="preview" ref={viewRef} />
</div>
</div>
);
}
export default SandBox;
(5)util.js
import React from "react";
import ReactDOM from "react-dom";
import ObjPath from "object-path";
import { parse } from "acorn";
import { generate as generateJs } from "escodegen";
import { transform as babelTransform } from "@babel/standalone";
// 搜索目标节点
export function findReactNode(ast) {
// ast标准结构 body
const { body } = ast;
// 自定义一个迭代器
return body.find((node) => {
// 根据React.createElement匹配吧~
const { type } = node;
// 这个ObjPath类似lodash的get
const obj = ObjPath.get(node, "expression.callee.object.name");
const func = ObjPath.get(node, "expression.callee.property.name");
return (
type === "ExpressionStatement" &&
obj === "React" &&
func === "createElement"
);
});
}
// 动态创建方法
export function createEditor(domElement, moduleResolver = () => null) {
// 运行时的入参,带入方法用的
function render(node) {
ReactDOM.render(node, domElement);
}
// 同上
function require(moduleName) {
return moduleResolver(moduleName);
}
// 核心
function getWrapperFunction(code) {
try {
// 1. 一大窜React&ES6代码谁认识,先得降级吧
const esCode = babelTransform(code, { presets: ["es2015", "react"] })
.code;
// 2. 原生代码toAst(这里暂用acorn、babel、eslint 都符合 ESTree Spec标准, 传送门:https://github.com/estree/estree)
const ast = parse(esCode, {
sourceType: "module"
});
// 3. 我们的目的是把jsx => js并且运行React.createElement
// 所以得先到jsx装在的部分
const rnode = findReactNode(ast);
// 4. 如果找到了运行语句,接下来必须要包装render方法在React.createElemnet外面才能运行吧
if (rnode) {
// 先找到位置,便于后面直接替换
const nodeIndex = ast.body.indexOf(rnode);
// 生成字符串,截掉没用的信息
const createElSrc = generateJs(rnode).slice(0, -1);
// 重新生成改造后的ast - 可以执行的语句
const renderCallAst = parse(`render(${createElSrc})`).body[0];
ast.body[nodeIndex] = renderCallAst;
}
// 5. 完事具备运行起来吧,eval效率贼低不说还不安全,new Function吧
// 运行时方法很多,尤其在node端 vm库 - runInThisContext等
// 前面三个入参,后面是函数体
return new Function("React", "render", "require", generateJs(ast));
} catch ({ message }) {
// 兜底
render(<pre style={{ color: "red" }}>{message}</pre>);
}
}
// 妈的前面的核心不能暴露,还是返回方法吧
return {
// 查看编译结果
compile(code) {
return getWrapperFunction(code);
},
// 直接运行
run(code) {
this.compile(code)(React, render, require);
},
// 查看生成的字符串
getCompiledCode(code) {
return getWrapperFunction(code).toString();
}
};
}
(6)styles.css
.App {
font-family: sans-serif;
text-align: center;
}
.container {
width: 100vw;
height: 100vh;
}
.code-editor {
width: 50%;
}
.code-editor textarea {
padding: 1em;
width: 100%;
height: 100%;
border: solid 1px #000;
min-height: 30em;
outline: none;
font-family: "Monaco";
font-size: 14px;
background: #333;
color: #74b9ff;
}
源码传送门
我们看到AST作为一个承上启下的作用,目前我们只是实现了简单的单一文件的编辑预览,如果是多模块我们又改怎么处理呢?无外乎还是需要AST查找,遍历出import的模块,并且把所有依赖的模块拼装起来,再进行执行。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!