你是否好奇過那些像 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);複製代碼
這真的是一個頗有趣的嘗試,我相信這項技術(實現)在下面的場景中將很是有用:
你來決定咯~
[![Edit react-live-editor](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/react-live-editor-xqw3b?fontsize=14)
你也許注意到我沒有實現模塊處理部分。這真的很簡單,因此我把它留給個人讀者。
感謝你的閱讀!