作者:easonruan,腾讯 CSIG 前端开发工程师
本文的浏览器端 Sandbox 沙盒运行环境,大家可以快速理解为类似 CodeSandbox 一样,所有页面代码编译都在前端完成(不依赖后端),并且具备实时热更新功能。
而本文终极目标就是实现这样的浏览器端 Sandbox 沙盒运行环境,可以轻松接入到大部分平台(尤其低代码平台),提升应用的预览速度和开发体验,效果如下:
为什么需要浏览器端 Sandbox 沙盒运行环境?
原因一:Demo 体验流程的转变:繁琐痛苦 → 快速便捷
如果你要体验 Ant Design 组件库里面 Tree 树组件的一个例子,并想修改部分参数查看效果,你需要做以下步骤:
Step1. 安装 Node.js (已安装可忽略)
Step2. 初始化 react 项目 npx create-react-app antd-tree-demo
(必须)
Step3. 添加 Ant Design 并安装依赖 npm install
(必须)
Step4. 修改项目代码为 Demo 例子代码 (必须)
Step5. 启动项目 npm start
(必须)
而当有了浏览器端的前端 Sandbox 沙盒运行环境,只需一个步骤:
Step1. 点击打开一个链接
即可快速体验到 Demo,并且修改代码可实时看到效果。因此 Ant Design 组件库的每个组件例子都附带了 CodeSandbox 的链接:
原因二:低代码平台场景需要实时查看并调试当前应用的真实效果
用户在低代码平台开发时,如果应用实时预览的效果是与本地构建出来的效果是一致的,同时可以点击跳转到其他页面,查看整个业务流程的效果,那么整个开发体验都会有大幅度提升。
比如家庭健康码流程,包含 3 个页面:首页入口 → 健康码列表 → 健康码详情(详见开头视频动图)
第一个小目标:在浏览器上直接运行 React
源码文件渲染出 Hello, Sandbox!
源码如下:
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(
<div>Hello, Sandbox!</div>,
document.getElementById('root')
);
问题一:如何让源代码在浏览器上直接执行?
直接在浏览器上面执行可以吗?显然不行
原因 1:浏览器不支持直接 import NPM 模块 (目前支持加载服务端文件 '/xx/xx.jsx')
原因 2:浏览器无法识别 React 的 JSX 语法
虽然最新浏览器 (Chrome 67 版本开始) 已支持 ESM 模块的加载方式,但需要有以下两个前提条件:
条件 1:需要对源代码进行改造,改为相对或绝对路径,比如:
import React from 'react'
改成import React from '/@module/react'
条件 2:需要本地启动服务器端 Server,返回对应代码内容
当 import 其他文件时,比 import App from './App.jsx'
,因为 import 是系统关键词,我们无法直接模拟或者代理 import,此时浏览器会直接发起一个请求,
如果不依赖服务端,就必须另起一个 service worker
进行拦截。
而 service worker
的注册必须要加载单独的 js 文件(静态服务),无法将 sandbox 整套方案打包成一个 NPM 库来使用,更新迭代较为繁琐,不适用于我目前开发的低代码平台项目。
因此本文介绍的是更容易实现和管理的 CommonJS 格式规范
,以 require 模块的形式来模拟执行环境。
问题二:如何将 ESM 格式转换成 CommonJS 格式?
没错,就是 Babel
,Babel 有在线转译的 Try it out
版本,大家可以点击 https://babeljs.io/repl 链接体验
其代码转换效果如下:
利用 @babel/plugin-transform-modules-commonjs 插件,将 ESM 语法转换成 CommonJS 格式规范
解决浏览器不支持直接 import NPM 模块的问题
利用 @babel/plugin-transform-react-jsx Babel 插件,将
<div />
转换成React.createElement('div')
函数解决浏览器无法直接识别 React JSX 语法的问题
有了思路,我们立刻开始执行:
<!DOCTYPE html>
<html>
<head>
<!-- ① 依赖 -->
<script src="https://unpkg.com/@babel/standalone@7.13.12/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script>
const code = `
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(
<React.StrictMode>
<div>Hello, Sandbox!</div>
</React.StrictMode>,
document.getElementById('root')
);`
// ② 转译
// 此时代码已转为 CJS 格式,import 变成了 require 函数
const transpiledCode = Babel.transform(code, {
plugins: [
['transform-modules-commonjs'],
['transform-react-jsx'],
]
}).code
// ③ 执行
eval(transpiledCode)
</script>
</body>
</html>
执行 Babel 转换后 CommonJS 规范的代码,发现吃了个闭门羹:
原来是 require
函数没有定义,因为 CommonJs 规范就是利用 require 来加载模块的,既然现在没有定义,那我们就定义一个
问题三:如何实现 require 函数?
因为 require 是要引入 react, react-dom 两个 NPM 依赖库的,所以实现 require 函数之前,先插入已打包为 UMD 规范的文件路径,以获取 React, ReactDom
全局变量。
<!DOCTYPE html>
<html>
<head>
<!-- ① 依赖 -->
<script src="https://unpkg.com/@babel/standalone@7.13.12/babel.min.js"></script>
<script src="https://unpkg.com/react@16.14.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.14.0/umd/react-dom.development.js"></script>
<!-- 此时 react, react-dom 库已挂载到 window['React'], window['ReactDOM'] -->
</head>
<body>
<div id="root"></div>
<script>
const externals = {
react: 'React',
'react-dom': 'ReactDOM'
}
function require(moduleName) {
return window[externals[moduleName]]
}
</script>
</body>
</html>
实现 require
函数也非常简单,需要拿哪个 NPM 依赖库,就直接把已加载到全局的库,返回回去即可。
其中的 externals
是什么?
相信熟悉 webpack 的同学应该比较了解,简单来说就是配置哪些库是在运行时(runtime),再去外部(全局)获取这些扩展依赖。详情请点击
前期准备工作已经做完,我们将以下文件保存为 index.html
,然后本地打开看看效果
<!DOCTYPE html>
<html>
<head>
<!-- ① 依赖 -->
<script src="https://unpkg.com/@babel/standalone@7.13.12/babel.min.js"></script>
<script src="https://unpkg.com/react@16.14.0/umd/react.development.js"></script&