[譯]如何實現一個實時預覽的React編輯器

你是否好奇過那些像 codesandbox 和 codepen 的 在線 react 編輯器是如何實現的?你是否用過 semantic react react styleguidist,直接在瀏覽器中修改上面的例子,就能實時預覽。javascript

這週末我終於將零碎的概念彙總到了一塊兒並實現了一個簡單的方案。這篇文章就是此次實現過程的一個概覽。若是你仍不清楚最終的效果,建議你翻閱到文章的底部,先試試內嵌的代碼編輯器。css

好啦,那咱們直奔主題。java

咱們須要克服的挑戰

  • 在瀏覽器中轉換 JSX/ES6
  • 模塊處理,咱們可能在編輯器中引入模塊
  • 如何解析和修改 Javascript 代碼

使用到的依賴包

  • @babel/standalone 在瀏覽器中轉換 JSX/ES6
  • acorn 將 JS 解析成 AST
  • escodegen 將 修改後的 AST 轉回 JS
  • debounce, object-path

策略

這真的出人意料地簡單。如下是一些步驟:node

  1. 轉換 JSX/ES6 代碼
  2. 在轉換後的代碼中,找到一個 JSX 表達式。等咱們通過 AST 處理部分以後咱們再來了解它。
  3. 轉化 JSX 表達式,將它「包裝進」 render 方法內
  4. 建立一個函數,包含上面生成的代碼,而且將依賴做爲參數注入。
  5. 每當代碼修改,調用步驟 4 的函數


有點懵?別擔憂,咱們直接看示例。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 個參數:

  1. 將要渲染結果的DOM元素
  2. 一個模塊處理函數

重點實現是 getWrappedFunction。這裏引用了一張根據示例生成的 AST 樹,幫助你理解程序中咱們如何檢測並修改 JSX 表達式的。


能夠對比下上面的 AST 來理解 isReactNodefindReactNode是如何工做的。咱們使用任意的代碼串調用 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);複製代碼

你能夠在哪裏使用?

這真的是一個頗有趣的嘗試,我相信這項技術(實現)在下面的場景中將很是有用:

  • 組件文檔
  • 在線的 IDE
  • 一個簡單的 動態 JSX 渲染

你來決定咯~

連接

[![Edit react-live-editor](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/react-live-editor-xqw3b?fontsize=14)

最後

你也許注意到我沒有實現模塊處理部分。這真的很簡單,因此我把它留給個人讀者。

感謝你的閱讀!

相關文章
相關標籤/搜索