[译]如何实现一个实时预览的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)

最后

你也许注意到我没有实现模块处理部分。这真的很简单,因此我把它留给个人读者。

感谢你的阅读!

相关文章
相关标签/搜索