实现一个支持 react/vue2/vue3 等多种语法在线编辑预览的 web playground

介绍

本文参考了 Vue SFC Playground,基于 importmap 实现了一个在浏览器端的 web playground ------ CodePlayer。在此之上,CodePlayer 还将文件的依赖构建、代码编译等工作抽象为了一个 hooks 和插件系统组成的编译器,并支持了 vue、react(.tsx/.jsx)、html、ts/js、 less/scss/css、json 等多种语法的编译解析。

在线体验:play.fe-dev.cn/

源码仓库:github.com/zh-lx/codep...

架构设计

CodePlayer 整体由架构设计如下图所示,由 UI 层 和 构建层 组成。

  • UI 层由以下四部分组成:
    • 工具栏(Toolbar):主要包括主题、编辑器的字号、UI 层其他区域的显隐等外观设置,以及刷新预览器和分享等功能
    • 文件列表(File List):当前虚拟文件系统中的文件列表
    • 代码编辑器(Editor):基于 Monaco 封装的代码编辑器,具备多种语法的代码高亮和代码提示等功能
    • 预览区(Preview):用 iframe 去运行编译后的代码,展示效果
  • 构建层由虚拟文件系统和编译器两部分组成:
    • 虚拟文件系统(Virtual File System):存储文件列表中的文件及代码,一个虚拟文件指的就是一个 File 类型的 js 对象,其包含了文件名(路径)、原始代码和编译后的代码。

    • 编译器(Compiler):用于将虚拟文件系统中的源代码,编译为浏览器可执行的 js/css 代码。

运行过程

CodePlayer 的整体运行过程如下所示:

当在代码编辑器中编辑代码、或者通过文件列表新增/删除/重命名/更改入口文件时,都会触发虚拟文件系统的更新。Compiler 会监听虚拟文件系统的变动,当发生变动时,Compiler 就会重新编译变动的文件,并将更新后的代码重新注入到 iframe 中,从而更新预览区的视图。

Compiler 的设计

Compiler 的设计借鉴了 webpack 的思想,由 hooks 和 插件系统两部分组成:

  • hooks 是由一连串 hook 组成的生命周期,它是基于 hookable 这个库去实现的(与 webpack 底层使用的 tapable 功能类似)
  • 插件系统:不同的插件会在不同的 hook 上被调用,执行不同的功能,如编译代码、构建模块依赖、向 iframe 注入编译后的代码等等

Compiler 内部有 init 和 run 两个方法:init 方法只会在初始化时调用一次,在不同的 hook 上注册插件;run 方法则是每次在虚拟文件系统发生变化时执行,按照 hooks 顺序去执行各个 hook 对应的插件的功能

内置插件的实现

从上图可知,CodePlayer 内置了 3 个三个插件:transform plugin、compile-module plugin 和 emit plugin,这三个插件也是 CodePlayer 最核心的部分,所以下面会详细介绍一下这三个插件的具体实现。

transform plugin

transform plugin 的作用是将不同类型的文件代码进行编译浏览器能识别的 .js/.css 代码,内部会根据不同类型的文件后缀,调用不同的编译函数。

css 的转换

js 的转换

  • js 文件:代码无需处理
  • ts 文件的转换:调用 sucrase 库的 sucrase.transform 方法,并添加 transform: ['typescript'] 选项(sucrase 是一个类似 babel 的库,与 babel 相比,sucrase 着重于转换 typescript 和 jsx,舍弃了部分场景例如 IE 浏览器特性的兼容)
  • jsx/tsx 文件的转换:同样是调用 sucrase 库的 sucrase.transform 方法,在前面的基础上,额外增加一个 jsx 选项:transform: ['typescript', 'jsx'] 选项

vue 文件的转换

vue 文件的转换过程最麻烦,因为一个 vue 文件包含了 template、script 和 style 三部分,好在 @vue/compiler-sfc 为我们提供了这些能力。

对于转换后的 template、script 和 style,可能还包含着 ts、jsx 或者 less/scss 等语法,我们可以借助前面实现的转换能力做进一步转换。

补充知识点:vue 单文件的 scoped 是如何实现的?

  • 对于 template 部分:Vue 组件编译后是一个 js 对象,当给这个对象上面挂载一个 __scopeId 属性时,@vue/runtime 为这个 js 对象创建对应的 dom 时将对应的值添加为 dom 的属性。
  • 对于 style 部分:在 compileStyle 时会将刚刚生成的 id 作为参数传入时,会对每个选择器添加一个 data-v-xxx 的标签选择器,从而保证样式只对对应组件内的生成的 dom 生效。

compile-module plugin

因为 CodePlayer 中的文件是虚拟文件,需要对于 import 和 export 进行额外处理,compiler-module 主要是对上一步转换后的 js 代码中的 import 和 export 进行处理,构建文件之间的依赖关系。

如何处理文件的依赖关系?

compile-module 构建文件依赖关系过程是 DFS 递归执行的,主要做两件事情:

  1. 通过 babel 获取 js 代码的 ast 结构,通过 import 语句,确定文件之间的依赖关系。
  2. 通过文件的依赖关系,构建 js 代码的执行顺序。

import 和 export 语法是如何处理的?

  • export 的处理:我们通过 window.modules 全局对象来记录所有有导出模块的文件:以文件名作为 window.modules 中的键,值为一个对象,对象中的属性为文件中通过 export 导出的变量。对应的转换关系如下图所示:
  • import 的处理:import 引入模块时也根据引入模块的文件名在 window.modules 中查找对应的键值。对应的转换规则如下图所示:

第三方 npm 包的处理

若导入的模块不是以 ./ 开头,我们视其为第三方 npm 包,即一个远程模块,浏览器原生支持通过 importmap 处理这种引入。

importmap 是一个 JSON 对象,允许开发者在导入 JavaScript 模块时,控制浏览器如何解析模块标识符,它会在解析标识符时与要替换的文本之间建立映射。注意:导入的映射值必须为一个 esm 规范的 js 文件

编译后的代码等同于下图:

提供两个网站可以对 npm 包生成 esm 规范的文件:

emit plugin

compile-module 阶段结束后,最终会将虚拟文件编译为三个数组:links、styles、processed。

emit 阶段主要是将这三个数组生成对应的 html element。在首次渲染时,preview 预览器会创建一个 iframe, emit plugin 会将这些 html elements 写入 iframe 中;更新时不需要再写入整段 html,而是将标记有 replace 的标签删除掉,并将最新的 html element 插入到 html 中。

与主流 Web IDE 的对比

与大家熟知的 WebIDE 如 StackBlitzCodeSandboxVue SFC Playground 等相比,CodePlayer 也具备一些自己的优势:

语法支持的多样性 编译速度
CodePlayer
Vue SFC Playground
StackBlitz
CodeSandbox

待优化项

目前 CodePlayer 可以作为小型 demo 的演示站很合适,但对于文件数量多的大型 demo 而言,还有以下待改进项:

  • 循环依赖:对于循环依赖的文件,文件中的变量处理会有误
  • 热更新:目前修改代码后,会走全量编译的过程,可以通过只对变动的模块重新编译提升性能

总结

以上就是 CodePlayer 实现的全部内容,介绍了其架构设计、编译器实现以及核心的插件具体实现,希望能给大家带去一些学习和帮助,有任何问题欢迎在评论区留言。

相关推荐
yqcoder2 分钟前
electron 监听窗口高端变化
前端·javascript·vue.js
Python私教18 分钟前
Flutter主题最佳实践
前端·javascript·flutter
wangshuai092726 分钟前
vue使用prototype
vue.js
GDAL37 分钟前
HTML入门教程7:HTML样式
前端·html
生命几十年3万天1 小时前
解决edge浏览器无法同步问题
前端·edge
杨荧1 小时前
【JAVA毕业设计】基于Vue和SpringBoot的校园美食分享平台
java·开发语言·前端·vue.js·spring boot·java-ee·美食
API199701081101 小时前
京东平台接口技术详解及示例代码
开发语言·前端·python
前端热爱者1 小时前
axios post请求body为字符串时的解决方法
开发语言·前端·javascript
Monly212 小时前
JS:JSON操作
前端·javascript·json