Open soda-x opened 5 years ago
CodeSandbox 是一个在线的代码编辑器,主要聚焦于创建 Web 应用项目。当前已经进化为可以同时支持浏览器端以及服务端的 web 应用。
先来看下目前 CodSandbox 它支持的 client 端的应用类型
server 端的应用类型
这个项目的起源来源于 Ives van Hoorne 当初在一家名为 Catawiki 公司工作,那会儿他们正在把 Ruby on Rails pages 迁移到 React 上来,某一天恰好他正在休假,然后公司的同事问他一个关于 React 的相关问题,而他又没带电脑,整个沟通过程全部建立在想象之上,所以沟通异常困难,所以 codesandbox 最初的想法就诞生了。
接下来看看这些年来如何一步步推进整个进程的:
Apr, 2017 - CodeSandbox — An online React editor
未来规划
发布后的两个月,改进 npm 体验
这里可以延伸得到的是 CodeSandbox 在发布之后,npm 的这块的支持在效率上是有缺陷的,同时期还有一个社区产品叫 Webpackbin ,因为他们同时都在做 npm 支持这块内容。所以 Ives van Hoorne 联系了Webpackbin 的作者 Christian Alfoni ,表达了想要合力来完成这块内容。所以自然而然他们走到了一起,建立了一个通用的项目来同时给 Webpackbin 和 CodeSandbox 提供包服务。Christian Alfoni 为此写过一篇文章。这边衍生出来另外两个库 webpackdll 和 webpack-packager。而这之后,Christian Alfoni 很快的废弃了 Webpackbin 方案。相信他们应该是合力开发 CodeSandbox 了。
Feature:
Aug 16, 2017 - CodeSandbox 1.5
重点:CodeSandbox 是如何基于浏览器创建一个能并行的,支持离线的,同时又有扩展能力的 bundler 的
以往的做法
这使得作者需要重新考虑整个的打包流程
作者第一个想到的是让 webpack 跑在浏览器里面,这应该是非常容易得出的结论,事实上他也这么做了,因为 webpack 从当前来看在市场占有率上来讲具有绝对的优势,另外任何的项目支持可能一个 webpack.config.js 都可以搞定。看上去非常美好。事实也这样,作者让 webpack 成功的跑在了浏览器端。但问题是被 uglify 后的 webpack 大小有 3.5MB,同时还需要提供大量的 pollyfill,由于动态引用的关系,compilation 还会报一堆的警告。在作者测试中他让一半的 loader 跑在了浏览器端。另外由于使用 webpack 时其实假定了一个 nodejs 环境,所以后期可能会需要消耗大量的经历在模拟 nodejs 环境中,关键效果还可能差强人意,作者觉得要做的事情实在太多了,收益又太小,另外,基于 CodeSandbox 本身平台的考虑(浏览器,动态加载等),或许去构造一个适合 CodeSandbox 的构建工具可能更加适合 ,因为所有优化都是可以基于这个平台来做的,所以最终他放弃了这个让 webpack 跑在浏览器的原始想法。
作者的第二个想法就是自己做一个打包工具,但是在 loader 的 API 设计时尽量接近 webpack,这一点和我在设计 Gravity 时不谋而合了。这种设计的优点是,就感觉像在用 webpack,甚至有些 webpack 的 loader 可以无痛移植过来,而有痛那些我们只要摘除了 SSR, Node, Production 的逻辑后基本都可以跑起来了。另外由于我们是浏览器的环境,所以 Web Workers, Service Workers 和 code splitting 我们就可以随意使用了。
最终作者在实现该打包工具时尽力做好了两件事情,第一件事情是 loader 的 API 设计尽量往 webpack 靠,第二件事情就是尽力优化在 CodeSandbox 中的表现。最终这个 bundler 分为了三个阶段:configuration, transpilation 和 evaluation.
Configuration:
在该方案中每一种项目类型都会被定义一个 preset,这个 preset 主要来描述一种文件类型是需要如何 resolve 以及这种文件类型的需要被什么 loader 加载。
Transpilation:
顾名思义,这个阶段主要做 transpilation,另外还有还会负责一件非常重要的事情 - 构建依赖树。每个被 transpile 的文件都会被进行语法分析 得到 AST,该 AST 便于我们找到 require 申明,并且把这些加到树结构中。这不操作不仅仅限制在 js 文件,对 typescript, sass, less 和 stylus 文件也会做同样的事情。在 Transpilation 阶段做构建依赖树的好处是,我们只需要要对文件构建一次 AST 语法树。编译后的内容会被存放在 TranspiledModule 这个对象中,另外需要了解的是,一个文件可能关联了多个 TranspiledModule,原因就在于 require(‘raw-loader!./Hello.js’) 并不等价于 require(‘./Hello.js’)。
另外这次重构,彻底释放了 web worker 的潜力,因为我们可以通过 web 端的 web worker 能力进行并发的编译流程,所以这里可以推断是作者在实现时,应该是有个 web worker pool 管理。这种做法也会大大加快 UI 端的渲染,因为 UI 层 和 编译层进行了分离。这部分来源初始于 reactjs - core- team 成员 bvaughn 的一次对 babel 的优化。 另外基于动态按需加载的特性,所有被加载的文件全部都是所需的内容,不过最终所有的内容,(比如 额外的 loader )还是会被 SW 下载到本地,以用来支持更好的 offline 体验。
Evaluation:
虽然作者把这个工具称之为 bundler 但实际上并没有真正意义上 bundle 的过程。这边的方式和 systemjs 其实基本是一致的。最后需要做的就是执行文件就可以了。另外和 webpack runtime 或者 systemjs 一样也提供了自己的 require 方法,该方法本质都是扩展到缓存,而 CodeSandbox 这边则是去获得 TranspiledModule。
另外关于 HMR,大家知道 module.hot 是 webpack 的方法,在这边要实现 hot reload 方法本质上是做不到的,看到作者做了一点小技巧,当文件变更后,促使关联文件失效,引发重新编译,最后其实他应该是重新执行了入口那个函数。这个处理和 systemjs 的处理方式如出一辙,在 systemjs 在内存链路里面需要手动引发一个 invalid 操作来标记失效文件,但 systemjs 不足的地方在于他们的文件数结构是扁平的,不是真正意义上的树形结构,所以在引发链路更新的时候,一个文件的副作用很难确定出来,或许我还没足够了解。
重点:CodeSandbox 是如何把 npm 在浏览器里面执行起来的
在作者描述中,起初他们并没有想要把 npm 考虑进来,因为他们觉得这不太可能。到现在来看 npm 支持应该说是 CodeSandbox 非常重要的一项特性。
首个版本:
严格意义上来讲并不算支持了 npm 了,作者做法是在本地下载了相关的依赖,然后把调用的依赖指向到了本地,这种方案显然是不可用的方式。
基于 webpack 的版本:
后续作者在偶然看到了 https://esnextb.in/ ,这个小产品对作者影响很大,因为他一直认为 npm 模块不可能真正意义上在浏览器里面使用起来,但这款小的产品做到了。所以作者开始回过头来重新思考里面的问题。
这是作者想到的第一种实现架构,有点过于复杂。而后他也意识到了当中的复杂性,然后机缘巧合,他看到了 webpack dll plugin,通过这种方式可以单独打包依赖文件,单独生成一个依赖文件,以及一个 manifest json 文件,该文件内描述了依赖文件的 module id,我们可以通过 module id 来得到某个文件的 exports。
基于如上这个想法作者对其进行了实现
这种方式应该比第一种想到架构会简单很多。
但是使用这种方案会有个缺陷,webpack 的依赖树是真正被引用到的文件才会出现在依赖树结构中,这意味着如果你需要依赖一个 npm 模块内的某个脚本,但是该模块并不在 main 的依赖树中,那么我们在实际使用中将无法 require 到这个脚本拿不到对应的 exports。后续才会有了CodeSandbox 作者和 WebpackBin 作者 Christian Alfoni 合作开发的事情,根本上他们想要一起合作解决这个限制。最终他们也解决了这个问题。
新系统的诞生也让他们对整体架构做了升级,他们把 dll 这个功能做成了一个 service,该 service 上跑了多个 npm 打包服务。更多这块内容被记录在了一篇博文中 。
这种方案看上去很棒,但也有一些限制和缺陷,当 CodeSandbox 越来越有名后,使用的人也越来越多,服务端的开销也就越来越多了。与此同时他们对缓存处理是对整个包的出发的,当添加一个文件后,原有的缓存全部会失效,因为他们需要重新构建,原因在于 module id 这些全部会发生变更。
Serverless 的出现:
作者受一篇 serverless 文章的影响,便在自己的系统中开始尝试使用 serverless 。通过 serverless 可以定义一个将在请求时执行的函数,该函数可以处理该请求,并在一段时间内终结自身。这种可伸缩性对 CodeSandbox 来说是比较有用的。后续作者通过一个名叫 Serverless framework 快速实现了三个 serverless 函数。
通过如上优化,大幅降低了 CodeSandbox 在服务端的支出,同时让响应速度提升了40% - 700%。
事情总是很曲折,在对这次重构跑了一段时间后发现了新的问题,一个 lambda 函数最多只能有 500 MB 的磁盘空间,这意味着有些 combination 将不能安装,这个问题是致命性的,会导致服务完全不可用。所以作者又重新开始了新的一轮优化。
因为架构设计的原因,bundler 和 packger 拆分处在不同的环境下,bundler 执行在浏览器端,处理真正依赖关系,而 packager 则是单纯对 npm 依赖进行了处理。基于 bundler 的设计,本质上 bundler 完全有能力去处理 npm 级的文件,而 packager 只是单纯去把 npm 依赖梳理清楚然后全部下发给浏览器端,而最后让 bundler 来处理最终如 resolve,执行等操作。这样一来,就会更加大幅度的提升性能,因为服务端的 webpack dll server 就可以被废弃掉了,废弃的好处还在于不用每次依赖更新时需要更新整个 compilation(受限于 webpack),而只需单单关注新增依赖的下发。
架构进一步得到了简化。但是知道 unpkg,或者 jsdeliver 的同学,或许有个困惑,因为还可以有更快的方式,比如如上这个流程完全可以拆分到 unpkg,或者 jsdeliver 来实现。其实说白了就是 stackblitz 的 turbo 方案。
作者还是想要保持这套架构的原因在于他想要离线化,当只有你有所有文件的的控制权时,这些才有可能实现。
结论:
当前一个组合 deps 发生请求时,事先会对这个组合进行确认,确认是否已经在 S3 上存在,如果不存在则会走到 API Service, 这个服务会对这个组合进行拆分形成独立的依赖,并对这个独立的依赖请求 Packager,Packager 会使用 yarn 进行依赖的安装,并且对基于入口文件的 AST 递归分析最终获取所有相关文件的依赖拓扑,最终 Packager 会把结果存储到 S3 上 。一旦 API Service 返回 200,那么就会再去 S3 上去获取最终的结果。
另外 Packager 做 AST 分析时额外做了附加做了一件事情就是把文件 resolve 关系也一并记录了,原因就在于浏览器端没有 nodejs resolve 算法,另外如需支持 bower 类型的模块,resolve 规则会更麻烦一点。这一点并不是不能通过浏览器端实现,而是能让整个过程更加顺畅,这一点我在做 Gravity resolve 时深有感触。
改造优点:
这一部分处理作者把它开源了,详见 https://github.com/CompuIves/dependency-packager。
Nov 17, 2017 CodeSandbox 2.0
Feb 7, 2018 CodeSandbox 2.5
Mar 27, 2018 实时协同编辑
为了解决潜在的冲突,作者使用了 operational transformation 这项技术。在前端上作者使用了 ot.js 而在后端实现上则是使用了 ot_ex. 当然这当中有很多的定制化的内容,作者表示这是他有史以来做过的最最有趣的需求,因为这当中有非常多的技术挑战以及需要解决很多竞争态。
Sep 28, 2018 支持 container
言下之意就是我们本地跑的任何的项目都可以使用 CodeSandbox Container 跑起来,因为在容器的沙箱环境下本质上和本地环境并无多大差异,这种方案也可以让我们肆无忌惮的使用 npm scripts,甚至我们也可以使用远端的 terminal。
Mar 19,2019 CodeSandbox 发布 3.0 版本
怎么让它支持 less 呢 添加了less loader 没有生效
@sgxin 默认就支持的吧
CodSandbox Review
先来看下目前 CodSandbox 它支持的 client 端的应用类型
server 端的应用类型
这个项目的起源来源于 Ives van Hoorne 当初在一家名为 Catawiki 公司工作,那会儿他们正在把 Ruby on Rails pages 迁移到 React 上来,某一天恰好他正在休假,然后公司的同事问他一个关于 React 的相关问题,而他又没带电脑,整个沟通过程全部建立在想象之上,所以沟通异常困难,所以 codesandbox 最初的想法就诞生了。
接下来看看这些年来如何一步步推进整个进程的:
Apr, 2017 - CodeSandbox — An online React editor
未来规划
发布后的两个月,改进 npm 体验
这里可以延伸得到的是 CodeSandbox 在发布之后,npm 的这块的支持在效率上是有缺陷的,同时期还有一个社区产品叫 Webpackbin ,因为他们同时都在做 npm 支持这块内容。所以 Ives van Hoorne 联系了Webpackbin 的作者 Christian Alfoni ,表达了想要合力来完成这块内容。所以自然而然他们走到了一起,建立了一个通用的项目来同时给 Webpackbin 和 CodeSandbox 提供包服务。Christian Alfoni 为此写过一篇文章。这边衍生出来另外两个库 webpackdll 和 webpack-packager。而这之后,Christian Alfoni 很快的废弃了 Webpackbin 方案。相信他们应该是合力开发 CodeSandbox 了。
Feature:
Aug 16, 2017 - CodeSandbox 1.5
重点:CodeSandbox 是如何基于浏览器创建一个能并行的,支持离线的,同时又有扩展能力的 bundler 的
以往的做法
这使得作者需要重新考虑整个的打包流程
作者第一个想到的是让 webpack 跑在浏览器里面,这应该是非常容易得出的结论,事实上他也这么做了,因为 webpack 从当前来看在市场占有率上来讲具有绝对的优势,另外任何的项目支持可能一个 webpack.config.js 都可以搞定。看上去非常美好。事实也这样,作者让 webpack 成功的跑在了浏览器端。但问题是被 uglify 后的 webpack 大小有 3.5MB,同时还需要提供大量的 pollyfill,由于动态引用的关系,compilation 还会报一堆的警告。在作者测试中他让一半的 loader 跑在了浏览器端。另外由于使用 webpack 时其实假定了一个 nodejs 环境,所以后期可能会需要消耗大量的经历在模拟 nodejs 环境中,关键效果还可能差强人意,作者觉得要做的事情实在太多了,收益又太小,另外,基于 CodeSandbox 本身平台的考虑(浏览器,动态加载等),或许去构造一个适合 CodeSandbox 的构建工具可能更加适合 ,因为所有优化都是可以基于这个平台来做的,所以最终他放弃了这个让 webpack 跑在浏览器的原始想法。
作者的第二个想法就是自己做一个打包工具,但是在 loader 的 API 设计时尽量接近 webpack,这一点和我在设计 Gravity 时不谋而合了。这种设计的优点是,就感觉像在用 webpack,甚至有些 webpack 的 loader 可以无痛移植过来,而有痛那些我们只要摘除了 SSR, Node, Production 的逻辑后基本都可以跑起来了。另外由于我们是浏览器的环境,所以 Web Workers, Service Workers 和 code splitting 我们就可以随意使用了。
最终作者在实现该打包工具时尽力做好了两件事情,第一件事情是 loader 的 API 设计尽量往 webpack 靠,第二件事情就是尽力优化在 CodeSandbox 中的表现。最终这个 bundler 分为了三个阶段:configuration, transpilation 和 evaluation.
Configuration:
在该方案中每一种项目类型都会被定义一个 preset,这个 preset 主要来描述一种文件类型是需要如何 resolve 以及这种文件类型的需要被什么 loader 加载。
Transpilation:
顾名思义,这个阶段主要做 transpilation,另外还有还会负责一件非常重要的事情 - 构建依赖树。每个被 transpile 的文件都会被进行语法分析 得到 AST,该 AST 便于我们找到 require 申明,并且把这些加到树结构中。这不操作不仅仅限制在 js 文件,对 typescript, sass, less 和 stylus 文件也会做同样的事情。在 Transpilation 阶段做构建依赖树的好处是,我们只需要要对文件构建一次 AST 语法树。编译后的内容会被存放在 TranspiledModule 这个对象中,另外需要了解的是,一个文件可能关联了多个 TranspiledModule,原因就在于 require(‘raw-loader!./Hello.js’) 并不等价于 require(‘./Hello.js’)。
另外这次重构,彻底释放了 web worker 的潜力,因为我们可以通过 web 端的 web worker 能力进行并发的编译流程,所以这里可以推断是作者在实现时,应该是有个 web worker pool 管理。这种做法也会大大加快 UI 端的渲染,因为 UI 层 和 编译层进行了分离。这部分来源初始于 reactjs - core- team 成员 bvaughn 的一次对 babel 的优化。 另外基于动态按需加载的特性,所有被加载的文件全部都是所需的内容,不过最终所有的内容,(比如 额外的 loader )还是会被 SW 下载到本地,以用来支持更好的 offline 体验。
Evaluation:
虽然作者把这个工具称之为 bundler 但实际上并没有真正意义上 bundle 的过程。这边的方式和 systemjs 其实基本是一致的。最后需要做的就是执行文件就可以了。另外和 webpack runtime 或者 systemjs 一样也提供了自己的 require 方法,该方法本质都是扩展到缓存,而 CodeSandbox 这边则是去获得 TranspiledModule。
另外关于 HMR,大家知道 module.hot 是 webpack 的方法,在这边要实现 hot reload 方法本质上是做不到的,看到作者做了一点小技巧,当文件变更后,促使关联文件失效,引发重新编译,最后其实他应该是重新执行了入口那个函数。这个处理和 systemjs 的处理方式如出一辙,在 systemjs 在内存链路里面需要手动引发一个 invalid 操作来标记失效文件,但 systemjs 不足的地方在于他们的文件数结构是扁平的,不是真正意义上的树形结构,所以在引发链路更新的时候,一个文件的副作用很难确定出来,或许我还没足够了解。
重点:CodeSandbox 是如何把 npm 在浏览器里面执行起来的
在作者描述中,起初他们并没有想要把 npm 考虑进来,因为他们觉得这不太可能。到现在来看 npm 支持应该说是 CodeSandbox 非常重要的一项特性。
首个版本:
严格意义上来讲并不算支持了 npm 了,作者做法是在本地下载了相关的依赖,然后把调用的依赖指向到了本地,这种方案显然是不可用的方式。
基于 webpack 的版本:
后续作者在偶然看到了 https://esnextb.in/ ,这个小产品对作者影响很大,因为他一直认为 npm 模块不可能真正意义上在浏览器里面使用起来,但这款小的产品做到了。所以作者开始回过头来重新思考里面的问题。
这是作者想到的第一种实现架构,有点过于复杂。而后他也意识到了当中的复杂性,然后机缘巧合,他看到了 webpack dll plugin,通过这种方式可以单独打包依赖文件,单独生成一个依赖文件,以及一个 manifest json 文件,该文件内描述了依赖文件的 module id,我们可以通过 module id 来得到某个文件的 exports。
基于如上这个想法作者对其进行了实现
这种方式应该比第一种想到架构会简单很多。
但是使用这种方案会有个缺陷,webpack 的依赖树是真正被引用到的文件才会出现在依赖树结构中,这意味着如果你需要依赖一个 npm 模块内的某个脚本,但是该模块并不在 main 的依赖树中,那么我们在实际使用中将无法 require 到这个脚本拿不到对应的 exports。后续才会有了CodeSandbox 作者和 WebpackBin 作者 Christian Alfoni 合作开发的事情,根本上他们想要一起合作解决这个限制。最终他们也解决了这个问题。
新系统的诞生也让他们对整体架构做了升级,他们把 dll 这个功能做成了一个 service,该 service 上跑了多个 npm 打包服务。更多这块内容被记录在了一篇博文中 。
这种方案看上去很棒,但也有一些限制和缺陷,当 CodeSandbox 越来越有名后,使用的人也越来越多,服务端的开销也就越来越多了。与此同时他们对缓存处理是对整个包的出发的,当添加一个文件后,原有的缓存全部会失效,因为他们需要重新构建,原因在于 module id 这些全部会发生变更。
Serverless 的出现:
作者受一篇 serverless 文章的影响,便在自己的系统中开始尝试使用 serverless 。通过 serverless 可以定义一个将在请求时执行的函数,该函数可以处理该请求,并在一段时间内终结自身。这种可伸缩性对 CodeSandbox 来说是比较有用的。后续作者通过一个名叫 Serverless framework 快速实现了三个 serverless 函数。
通过如上优化,大幅降低了 CodeSandbox 在服务端的支出,同时让响应速度提升了40% - 700%。
事情总是很曲折,在对这次重构跑了一段时间后发现了新的问题,一个 lambda 函数最多只能有 500 MB 的磁盘空间,这意味着有些 combination 将不能安装,这个问题是致命性的,会导致服务完全不可用。所以作者又重新开始了新的一轮优化。
因为架构设计的原因,bundler 和 packger 拆分处在不同的环境下,bundler 执行在浏览器端,处理真正依赖关系,而 packager 则是单纯对 npm 依赖进行了处理。基于 bundler 的设计,本质上 bundler 完全有能力去处理 npm 级的文件,而 packager 只是单纯去把 npm 依赖梳理清楚然后全部下发给浏览器端,而最后让 bundler 来处理最终如 resolve,执行等操作。这样一来,就会更加大幅度的提升性能,因为服务端的 webpack dll server 就可以被废弃掉了,废弃的好处还在于不用每次依赖更新时需要更新整个 compilation(受限于 webpack),而只需单单关注新增依赖的下发。
架构进一步得到了简化。但是知道 unpkg,或者 jsdeliver 的同学,或许有个困惑,因为还可以有更快的方式,比如如上这个流程完全可以拆分到 unpkg,或者 jsdeliver 来实现。其实说白了就是 stackblitz 的 turbo 方案。
作者还是想要保持这套架构的原因在于他想要离线化,当只有你有所有文件的的控制权时,这些才有可能实现。
结论:
当前一个组合 deps 发生请求时,事先会对这个组合进行确认,确认是否已经在 S3 上存在,如果不存在则会走到 API Service, 这个服务会对这个组合进行拆分形成独立的依赖,并对这个独立的依赖请求 Packager,Packager 会使用 yarn 进行依赖的安装,并且对基于入口文件的 AST 递归分析最终获取所有相关文件的依赖拓扑,最终 Packager 会把结果存储到 S3 上 。一旦 API Service 返回 200,那么就会再去 S3 上去获取最终的结果。
另外 Packager 做 AST 分析时额外做了附加做了一件事情就是把文件 resolve 关系也一并记录了,原因就在于浏览器端没有 nodejs resolve 算法,另外如需支持 bower 类型的模块,resolve 规则会更麻烦一点。这一点并不是不能通过浏览器端实现,而是能让整个过程更加顺畅,这一点我在做 Gravity resolve 时深有感触。
改造优点:
这一部分处理作者把它开源了,详见 https://github.com/CompuIves/dependency-packager。
Nov 17, 2017 CodeSandbox 2.0
Feb 7, 2018 CodeSandbox 2.5
Mar 27, 2018 实时协同编辑
为了解决潜在的冲突,作者使用了 operational transformation 这项技术。在前端上作者使用了 ot.js 而在后端实现上则是使用了 ot_ex. 当然这当中有很多的定制化的内容,作者表示这是他有史以来做过的最最有趣的需求,因为这当中有非常多的技术挑战以及需要解决很多竞争态。
Sep 28, 2018 支持 container
言下之意就是我们本地跑的任何的项目都可以使用 CodeSandbox Container 跑起来,因为在容器的沙箱环境下本质上和本地环境并无多大差异,这种方案也可以让我们肆无忌惮的使用 npm scripts,甚至我们也可以使用远端的 terminal。
Mar 19,2019 CodeSandbox 发布 3.0 版本