你是否好奇过那些像 codesandbox 和 codepen 的 在线 react 编辑器是如何实现的?你是否用过 semantic react 或 react styleguidist,直接在浏览器中修改上面的例子,就能实时预览。javascript
这周末我终于将零碎的概念汇总到了一块儿并实现了一个简单的方案。这篇文章就是此次实现过程的一个概览。若是你仍不清楚最终的效果,建议你翻阅到文章的底部,先试试内嵌的代码编辑器。css
好啦,那咱们直奔主题。java
@babel/standalone
在浏览器中转换 JSX/ES6
acorn
将 JS 解析成 ASTescodegen
将 修改后的 AST 转回 JSdebounce, object-path
这真的出人意料地简单。如下是一些步骤:node
JSX/ES6
代码JSX
表达式,将它“包装进” render
方法内有点懵?别担忧,咱们直接看示例。react
假设咱们从这样的一段代码开始入手:express
如何让这段代码可以在咱们的网页上渲染?浏览器
咱们如今的任务是转换上面的代码,处理引入的 button
组件,而且渲染第 12 行的 JSX。bash
下面是转换后的版本:babel
下面是咱们须要“动态”生成的:app
当咱们生成上面的函数后,咱们能够经过传递 一个 React 对象,一个渲染函数,一个模块处理函数做为参数,调用这个函数。
同时,注意咱们将转化后的代码的第 10 行包含在了渲染函数的调用中。
但愿你已经 get 到了整个思路。那么咱们看一些具体的代码。
import React from "react";import ReactDOM from "react-dom";import ObjPath from "object-path";import * as Acorn from "acorn";import { generate as generateJs } from "escodegen";import { transform as babelTransform } from "@babel/standalone";function isReactNode(node) { const type = node.type; //"ExpressionStatement" 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 findReactNode(ast) { const { body } = ast; return body.find(isReactNode);}export function createEditor(domElement, moduleResolver = () => null) { function render(node) { ReactDOM.render(node, domElement); } function require(moduleName) { return moduleResolver(moduleName); } function getWrapperFunction(code) { try { // 1. transform code const tcode = babelTransform(code, { presets: ["es2015", "react"] }) .code; // 2. get AST const ast = Acorn.parse(tcode, { sourceType: "module" }); // 3. find React.createElement expression in the body of program const rnode = findReactNode(ast); if (rnode) { const nodeIndex = ast.body.indexOf(rnode); // 4. convert the React.createElement invocation to source and remove the trailing semicolon const createElSrc = generateJs(rnode).slice(0, -1); // 5. transform React.createElement(...) to render(React.createElement(...)), // where render is a callback passed from outside const renderCallAst = Acorn.parse(`render(${createElSrc})`) .body[0]; ast.body[nodeIndex] = renderCallAst; } // 6. create a new wrapper function with all dependency as parameters return new Function("React", "render", "require", generateJs(ast)); } catch (ex) { // in case of exception render the exception message render(<pre style={{ color: "red" }}>{ex.message}</pre>); } } return { // returns transpiled code in a wrapper function which can be invoked later compile(code) { return getWrapperFunction(code); }, // compiles and invokes the wrapper function run(code) { this.compile(code)(React, render, require); }, // just compiles and returns the stringified wrapper function getCompiledCode(code) { return getWrapperFunction(code).toString(); } };}复制代码
当咱们调用 createEditor
函数的时候,咱们就建立了一个 编辑器 实例。这个函数接受 2 个参数:
重点实现是 getWrappedFunction
。这里引用了一张根据示例生成的 AST 树,帮助你理解程序中咱们如何检测并修改 JSX 表达式的。
能够对比下上面的 AST 来理解 isReactNode
和 findReactNode
是如何工做的。咱们使用任意的代码串调用 Acorn.parse
方法,它将代码当作一段完整的 javascript 程序,所以解析后的结果包含了全部语句。咱们须要从中找到 React.createElement
这一句。
下面,咱们再看一下(完整的)实现:
import "./styles.scss";import React from "react";import ReactDOM from "react-dom";import { createEditor } from "./editor";import debounce from "debounce";// default code const code = `import x from 'x';// edit this examplefunction Greet() { return <span>Hello World!</span>}<Greet />`;class SandBox extends React.Component { state = { code }; editor = null; el = null; componentDidMount() { this.editor = createEditor(this.el); this.editor.run(code); } onCodeChange = ({ target: { value } }) => { this.setState({ code: value }); this.run(value); }; run = debounce(() => { const { code } = this.state; this.editor.run(code); }, 500); render() { const { code } = this.state; return ( <div className="app"> <div className="split-view"> <div className="code-editor"> <textarea value={code} onChange={this.onCodeChange} /> </div> <div className="preview" ref={el => (this.el = el)} /> </div> </div> ); }}const rootElement = document.getElementById("root");ReactDOM.render(<SandBox />, rootElement);复制代码
这真的是一个颇有趣的尝试,我相信这项技术(实现)在下面的场景中将很是有用:
你来决定咯~
[](https://codesandbox.io/s/react-live-editor-xqw3b?fontsize=14)
你也许注意到我没有实现模块处理部分。这真的很简单,因此我把它留给个人读者。
感谢你的阅读!