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

分享一个前段时间开发的小玩具,是一个"网页端在线编辑 + 实时预览"的 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,或者想做一个"让非研发也能改出效果"的示意区,欢迎试试

相关推荐
cipher15 小时前
ERC-4626 通胀攻击:DeFi 金库的"捐款陷阱"
前端·后端·安全
UrbanJazzerati15 小时前
非常友好的Vue 3 生命周期详解
前端·面试
AAA阿giao15 小时前
从零构建一个现代登录页:深入解析 Tailwind CSS + Vite + Lucide React 的完整技术栈
前端·css·react.js
兆子龙16 小时前
像 React Hook 一样「自动触发」:用 Git Hook 拦住忘删的测试代码与其它翻车现场
前端·架构
兆子龙16 小时前
用 Auto.js 实现挂机脚本:从找图点击到循环自动化
前端·架构
SuperEugene16 小时前
表单最佳实践:从 v-model 到自定义表单组件(含校验)
前端·javascript·vue.js
昨晚我输给了一辆AE8616 小时前
为什么现在不推荐使用 React.FC 了?
前端·react.js·typescript
不会敲代码116 小时前
深入浅出 React 闭包陷阱:从现象到原理
前端·react.js
不会敲代码116 小时前
React性能优化:深入理解useMemo和useCallback
前端·javascript·react.js
Dilettante25816 小时前
我的 Monorepo 实践经验:从基础概念到最佳实践
前端·前端工程化