最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 如何实现一个简单的基于React的在线编辑预览组件

    正文概述 掘金(BreadBear)   2021-04-05   760

    前言

    随着时间的推移,在线IDE应用越来越多,功能也越来越完善。比如

    • codesandbox: codesandbox.io
    • jsbin: jsbin.com/?html,outpu… (个人感觉这个很轻量)
    • codepen: codepen.io/pen/
    • jsfiddle: jsfiddle.net/
    • 菜鸟工具: c.runoob.com/
    • ...

    于是乎,就对这些在线IDE的实现原理产生了好奇,自己也想尝试实现一个类似的功能。这里有一篇文章详细讲述了codesandbox的实现原理:传送门

    我们尝试实现一个这样的功能,左边输入代码、右边预览效果。

    如何实现一个简单的基于React的在线编辑预览组件

    文件结构如下

    如何实现一个简单的基于React的在线编辑预览组件

    分别贴出代码内容

    (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的模块,并且把所有依赖的模块拼装起来,再进行执行。


    起源地下载网 » 如何实现一个简单的基于React的在线编辑预览组件

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元