一个“够用就好”的浏览器端实时预览编辑器

分享一个前段时间开发的小玩具,是一个"网页端在线编辑 + 实时预览"的 Playground,聊一聊为什么会做它以及里面一些比较有意思的技术实现

项目地址:browser-playground

预览截图:

可以看到,他的主要功能就是在页面中嵌入一个代码编辑器和一个实时预览器,类似于一个非常简版的IDE + dev-server,市场其实已经有一些类似的解决方案,但是他们要么太重(CodeSandbox)要么太简陋,于是就有了这款 browser-playground。


1. 背景和简介

如果你做过组件库文档/Design System/内部平台,应该遇到过类似需求:

  • 文档里放一段代码示例,读者改两行就能看到效果(live demo)
  • 需要多文件结构(components/*utils/*),而不是单文件粘贴
  • 希望把"可用能力"拆成插件:有的页面只要 React,有的页面要 Vue,有的要表单双向联动

这套 SDK 主要面向"对性能、安全性要求不高的示意代码片段"。它不是严格沙箱:当前预览执行采用 new Function(...),因此不适合直接执行不可信用户代码

如果你就是想要"在文档页上放个 Live Demo",那它基本就是为你准备的。

使用方式也很简单,以下是一个支持 ts 类型提示的基础示例:

tsx 复制代码
import { Playground } from '@browser-playground/core';
import { typesPlugin } from '@browser-playground/plugin-types';

<Playground
  entryFile="/src/App.tsx"
  initialFiles={{
    '/src/App.tsx': `export default function App(){ return <div style={{ padding: 8 }}>Hello</div> }`
  }}
  plugins={[typesPlugin()]}
/>;

除此之外,browser-playground 还提供了一些更为炫酷的效果,比如代码<-->表单的双向联动可以让你的 demo 既适合程序员也适合产品,没法上传视频贴个示意图好了:


2. 设计理念

2.1 轻量化

如果你想要的是一个足够全面的 Web IDE,那市面上已经有很多可供选择的方案,比如 CodeSandbox,browser-playground 的定位是"够用就好"

  • 编译在浏览器里完成:@rollup/browser + @babel/standalone,没有任何服务端
  • 预览执行简单直接:new Function 执行 bundle,拿到默认导出
  • 默认限制导入:只允许相对/绝对路径;裸导入必须显式允许(防止随便 import 'xxx' 失控)

取舍很明确:它要的是"文档 demo 的开发体验",不是"生产级沙箱/大工程构建器"。

2.2 可插拔

playground-core 只提供最基础的 React 语法的编辑和渲染能力,更多能力可以通过插件进行拓展,目前自带的插件包括:

  • plugins/plugin-vue:Vue SFC 编译 + 高亮
  • plugins/plugin-types:Monaco TS 类型注入(React/JSX + 自定义三方包 .d.ts

你也完全可以自定义插件来拓展 playground 的能力

2.3 可组合

SDK 的核心输入输出其实就几样:

  • initialFiles / entryFile:虚拟文件系统入口
  • plugins:按需叠加能力
  • dependencies:宿主注入三方包(共享运行时)
  • formValue / onFormValueChange:表单双向联动(SDK 不提供表单 UI)

你可以只要最小闭环,也可以逐步把能力叠上去,你可以自由选择任意的表单方案,也可以自由组合编辑器和渲染器的位置和样式,一切都是可组合的


3. 关键技术实现

3.1 编辑器和虚拟文件系统

编辑器我选择的是 Monaco,也就是 vscode 的背后实现,monaco 提供了完善的能力、健全的生态和很强的拓展能力,完美符合我们的场景

虚拟文件系统建模很朴素:

ts 复制代码
type VirtualFileSystem = Record<string, string>; // '/src/App.tsx' -> code

关键点不在建模,而在 Monaco:必须把每个虚拟文件注入为 model,否则 ts 语言服务根本不知道还有别的文件存在(Monaco 的 ts 类型解析运行在一个独立的 worker 中)

做法就是在初始化/文件变更时:

  • 对每个文件 createModel(code, language, uri)
  • 已存在则同步内容,并确保语言 id 正确(.tsx.vue 等)

对于那些外部依赖,例如 react 和一些三方包,plugin-types 里会把 .d.ts 处理成纯文本,通过 typescriptDefaults.addExtraLib 注入,来保证整体的类型完备

3.2 实时渲染

渲染包括两步,一是处理文件系统,二是处理语法转换,整体流程大概是:

  1. transformVirtualFiles(可通过插件扩展):例如 .vue -> ESM,并生成虚拟 entry
  2. Rollup 虚拟模块插件:
    • resolveId:只允许相对/绝对导入
    • load:从 files[id] 读取源码
    • transform:Babel 把 TS/TSX/JSX 转成 ESM
  3. 输出 iife:generate({ format: 'iife', globals })
  4. 执行:new Function(...runtimeGlobalNames, code)(...runtimeGlobals),取 default export

为了让 React/Vue 都能跑,我把运行时抽象成两类:

  • react:default export 是 React Component
  • dom:default export 是 { mount(el), unmount?() }(Vue 插件走这个)

3.3 "共享运行时"三方包注入

既然 Playground 跑在宿主页面里,那第三方依赖没必要在浏览器里重复打包。

做法是:

  • 宿主安装并 import 真实模块对象
  • 通过 <Playground dependencies={{ dayjs }} /> 传入
  • 编译时把 dayjs 加入 Rollup external/globals
  • 执行时把模块对象作为 new Function 参数注入进去

这样"三方包能不能用、能用哪些"完全由业务控制,Playground 只负责消费。

3.4 双向联动

映射声明长这样:

ts 复制代码
// @pg-mapping ['info', 'name']
const name = 'jack';

含义是:name 绑定到 formValue.info.name

实现上我用 Babel AST 做两件事:

  • 表单 -> 代码:定位被标注的变量初始化表达式(init),按 start/end 做最小文本替换(避免无意义格式化)
  • 代码 -> 表单:从 AST 抽取字面量值,合并成 patch,通过 onFormValueChange 回传

后续可能会做的一些事情:

  • 把文件系统处理、映射、编译等过程放到单独的 worker 里面,主要是防止 playground 的错误上升影响到页面本身
  • 更多的插件,比如可以通过 WASM 支持 python、rust 等其他语言
  • 更好的编辑体验,比如内置 console、devtools 等

如果你也在做文档网站的 live demo,或者想做一个"让非研发也能改出效果"的示意区,欢迎试试

相关推荐
coding随想6 小时前
前端革命:自定义元素如何让HTML元素“活“起来,重构你的开发体验!
前端·重构·html
爱上妖精的尾巴6 小时前
6-5 WPS JS宏 集合成员迭代(随机生成试题)
开发语言·前端·javascript
是你的小橘呀6 小时前
React 组件通信:组件间的 "悄悄话" 指南
前端·javascript
ycgg6 小时前
Webpack vs Vite 根本设计原理深度解析:为什么两者差异这么大?
前端
xrkhy6 小时前
canal1.1.8+mysql8.0+jdk17+rabbitMQ+redis的使用02
前端·redis·rabbitmq
Han.miracle7 小时前
HTML 核心基础与常用标签全解析
前端·html
几何心凉7 小时前
AI推理加速:openFuyao算力释放的核心引擎
前端
abcefg_h7 小时前
GO Web开发详细流程(无框架,restful风格,MVC架构)
开发语言·前端·golang
码界奇点7 小时前
基于Spring Cloud Alibaba与Vue.js的分布式在线教育系统设计与实现
前端·vue.js·分布式·spring cloud·架构·毕业设计·源代码管理