找回密码
 会员注册
查看: 17|回复: 0

从0到1实现浏览器端沙盒运行环境

[复制链接]

2万

主题

0

回帖

6万

积分

超级版主

积分
64454
发表于 2024-9-20 20:33:57 | 显示全部楼层 |阅读模式
作者:easonruan,腾讯CSIG前端开发工程师本文的浏览器端Sandbox沙盒运行环境,大家可以快速理解为类似CodeSandbox一样,所有页面代码编译都在前端完成(不依赖后端),并且具备实时热更新功能。而本文终极目标就是实现这样的浏览器端Sandbox沙盒运行环境,可以轻松接入到大部分平台(尤其低代码平台),提升应用的预览速度和开发体验,效果如下:为什么需要浏览器端Sandbox沙盒运行环境?原因一:Demo体验流程的转变:繁琐痛苦→快速便捷如果你要体验AntDesign组件库里面Tree树组件的一个例子,并想修改部分参数查看效果,你需要做以下步骤:Step1.安装Node.js(已安装可忽略) Step2.初始化react项目npxcreate-react-appantd-tree-demo(必须) Step3.添加AntDesign并安装依赖npminstall(必须) Step4.修改项目代码为Demo例子代码(必须) Step5.启动项目npmstart(必须)而当有了浏览器端的前端Sandbox沙盒运行环境,只需一个步骤:Step1.点击打开一个链接即可快速体验到Demo,并且修改代码可实时看到效果。因此AntDesign组件库的每个组件例子都附带了CodeSandbox的链接:原因二:低代码平台场景需要实时查看并调试当前应用的真实效果用户在低代码平台开发时,如果应用实时预览的效果是与本地构建出来的效果是一致的,同时可以点击跳转到其他页面,查看整个业务流程的效果,那么整个开发体验都会有大幅度提升。比如家庭健康码流程,包含3个页面:首页入口→健康码列表→健康码详情(详见开头视频动图)第一个小目标:在浏览器上直接运行React源码文件渲染出Hello,Sandbox!源码如下:import React from 'react';import ReactDOM from 'react-dom';ReactDOM.render(  Hello, Sandbox!,  document.getElementById('root'));问题一:如何让源代码在浏览器上直接执行?直接在浏览器上面执行可以吗?显然不行原因1:浏览器不支持直接importNPM模块(目前支持加载服务端文件'/xx/xx.jsx')原因2:浏览器无法识别React的JSX语法虽然最新浏览器(Chrome67版本开始)已支持ESM模块的加载方式,但需要有以下两个前提条件:条件1:需要对源代码进行改造,改为相对或绝对路径,比如:importReactfrom'react'改成importReactfrom'/@module/react'条件2:需要本地启动服务器端Server,返回对应代码内容当import其他文件时,比importAppfrom'./App.jsx',因为import是系统关键词,我们无法直接模拟或者代理import,此时浏览器会直接发起一个请求,如果不依赖服务端,就必须另起一个serviceworker进行拦截。而serviceworker的注册必须要加载单独的js文件(静态服务),无法将sandbox整套方案打包成一个NPM库来使用,更新迭代较为繁琐,不适用于我目前开发的低代码平台项目。因此本文介绍的是更容易实现和管理的CommonJS格式规范,以require模块的形式来模拟执行环境。问题二:如何将ESM格式转换成CommonJS格式?没错,就是Babel,Babel有在线转译的Tryitout版本,大家可以点击https://babeljs.io/repl链接体验其代码转换效果如下:利用@babel/plugin-transform-modules-commonjs插件,将ESM语法转换成CommonJS格式规范解决浏览器不支持直接importNPM模块的问题利用@babel/plugin-transform-react-jsxBabel插件,将转换成React.createElement('div')函数解决浏览器无法直接识别ReactJSX语法的问题有了思路,我们立刻开始执行:    执行Babel转换后CommonJS规范的代码,发现吃了个闭门羹:原来是require函数没有定义,因为CommonJs规范就是利用require来加载模块的,既然现在没有定义,那我们就定义一个问题三:如何实现require函数?因为require是要引入react,react-dom两个NPM依赖库的,所以实现require函数之前,先插入已打包为UMD规范的文件路径,以获取React,ReactDom全局变量。    实现require函数也非常简单,需要拿哪个NPM依赖库,就直接把已加载到全局的库,返回回去即可。其中的externals是什么?相信熟悉webpack的同学应该比较了解,简单来说就是配置哪些库是在运行时(runtime),再去外部(全局)获取这些扩展依赖。详情请点击前期准备工作已经做完,我们将以下文件保存为index.html,然后本地打开看看效果    可以看到,第一个小目标已经完美完成!总结:Sandbox核心方法论经过上面简单例子的验证,不能发现,最小的例子都要不开以下三步,因此本文总结了浏览器端Sandbox沙盒的核心方法论:Step1.加载依赖加载Babel,React,ReactDOMStep2.转译模块利用Babel将ESM转CommonJS,转JSX语法Step3.执行代码构造CommonJS环境,如require加载模块函数所以看过本文的同学,其他知识点记不住没关系,将本文的Sandbox方法论三部曲记住就行,记住就已经算掌握一半浏览器端沙盒原理了。重要的事情说三次:Step1.加载依赖,Step2.转译模块,Step3.执行代码 Step1.加载依赖,Step2.转译模块,Step3.执行代码 Step1.加载依赖,Step2.转译模块,Step3.执行代码下面我们用Vue创建一个业务项目,让Vue中用Sandbox沙盒(Iframe形式)来加载另一个React应用,同时验证上述Sandbox方法论。第二个小目标:从0到1实现一个浏览器端的Sandbox沙盒运行环境由于我目前研发的是WeDa低代码平台(专有版),因此暂时起名WeSandbox。WeDa低代码平台(专有版)由于内网环境问题暂不放链接,后续合适时期将开放给公司内部体验,目前大家可以先体验WeDa公有云版本第二个小目标最终效果其有以下特点:可在Vue应用Sandbox里运行React代码ReactuseState等功能均正常修改JSON数据可热更新React组件(不丢失状态)修改CSS数据可热更新样式上图运行的是Vue应用,里面有个iframe承载着WeSandbox核心功能,其可以转译并运行React的代码。Vue应用代码{{item.path}} 下面我们带着问题来一一查看部分功能的核心源码:问题一:如何转译代码?本文第一个小目标已经分析过,可以利用Babel进行转译,第二个小目标我们加个文件类型判断:// Step2. 转译代码function Transpile(packageInfo) {  const codeMap = packageInfo.codeMap  Object.keys(codeMap).map(path => {    const code = codeMap[path].code    // Babel Loader    if (/\.jsx?$/.test(path)) {      codeMap[path].transpiledCode = Babel.transform(code, {        plugins: [          ['transform-modules-commonjs'],          ['transform-react-jsx'],        ]      }).code    }  })  return codeMap}问题二:如何模拟CommonJS执行环境?由于本文上部分只引入了React,没有引入js(x)源代码文件,而源代码文件一般会利用module.exports导出该模块的值的,因此我们需要构造出module和exports来存储代码模块eval执行后的结果,其核心代码如下:// transpiledCode 转译后的源代码// require 自定义的获取模块函数,看下文// module 是与当前源代码绑定的执行结果(一开始为空对象,eval执行后赋值)function evaluateCode(transpiledCode, require, module) {  // #1. 构建 require, module, exports 当前函数的上下文全局数据  const allGlobals = {    require,    module,    exports: module.exports,  };  const allGlobalKeys = Object.keys(allGlobals).join(', ')  const allGlobalValues = Object.values(allGlobals);  try {    // #2. 源代码外面加一层函数,构建函数的入参为 require, module, exports    const newCode = `(function evaluate(` + allGlobalKeys + `) {` + transpiledCode + `\n})`;    // #3. 利用 eval 执行此函数,并传入 require, module, exports    eval(newCode).apply(this, allGlobalValues);    return module.exports;  } catch (e) {    //  }}const defaultExternals = {  react: 'React',  'react-dom': 'ReactDOM',}function evaluateCodeModule(codeModule) {  codeModule.module = codeModule.module || getNewModule()  function require(moduleName) {    const extLib = window[defaultExternals[moduleName]]    if (extLib) {      return extLib    }  }  return evaluateCode(codeModule.transpiledCode, require, codeModule.module)}function getNewModule() {  const exports = {}  return {    exports,  }}至此,我们已经CommonJS必备三套件require获取依赖模块函数module存储模块执行结果exports存储模块执行结果但演示例子的代码存在importxfrom'./x'的写法,import React from 'react';import ReactDOM from 'react-dom';import App from './App';ReactDOM.render(  
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-27 14:44 , Processed in 0.470487 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表