第2109期CodeSandbox是如何让npm上的模块直接在浏览器端运行的

Posted 前端早读课

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第2109期CodeSandbox是如何让npm上的模块直接在浏览器端运行的相关的知识,希望对你有一定的参考价值。

前言

前几天无意中看到CSB,有点意思。今日早读文章由字节跳动@众乐翻译授权分享。

@众乐(Ryan),目前就职于今日头条前端业务架构团队,通过平台、基建、工具、文档规范等手段不断挖掘业务中台通用能力,赋能上层业务及团队研发。同时,也在前端智能化、中后台研发提效、Serverless落地业务等方向执着探索。

正文从这开始~~

下文中提到的类似 支持npm相关的词都是指 在端实现对npm模块的加载、编译、打包、渲染&预览。

在开发CodeSandbox的初期,我总是将npm依赖模块的支持列在我们的支持范围之外。我认为在浏览器中安装任意数量的npm包是不可能实现的,即使去想一下我都觉得绝不会有实现思路。

而今天,对npm的支持已经是CodeSandbox最核心的特性之一,所以不管怎么样,我们都已经实现了它。我们做了大量的迭代来让它在任意的场景下都能正常运行;同时,我们也做了非常多的重写,甚至是在该功能已经在线上顺畅运行的今天,我们依然可以对其实现逻辑做改进。在这篇文章中,我首先会介绍刚开始我们是怎么做的,现在我们做到了一个什么程度,以及我们还可以继续做哪些改进。

'第一个'版本

当时我真的不知道如何去达成这一切,所以我做了一个非常简单的版本来在浏览器端能支持npm:

这个版本对npm的支持非常简单。它甚至不是真正的npm支持,我只是局部安装了npm依赖,然后以已经安装的某个依赖项为入口找到它所有的依赖项,然后去一个个的执行(英文原文:I just installed the dependencies locally and stubbed every dependency call with an already installed dependency. 我理解其实现过程大致如下,比如package.on依赖了a模块,a本身依赖了b跟c,c依赖了d,在执行a的时候就需要先找到a的所有依赖项b,c,d,然后一层层的执行下去,有点像cmd规范的sea.js或者commonjs规范的require.js这些模块加载器的执行机制)。这种实现方式,对于依赖模块达到4000个并且有不同版本诉求的场景的话,很明显是非常不具备可扩展性的。

尽管这个版本不是很具备可用性,但是它却给了我勇气让我可以看到,在一个沙箱环境中至少是可以让两个依赖模块正常运行起来的。

webpack版本

我其实对第一个版本非常满意,并且我觉得它值得一个MVP(对于CodeSandbox的第一版发布)。我甚至认为,如果不施展魔法的话,在浏览器端去安装任意的一个模块是不可能的,直到偶然的机会我看到了https://esnextb.in。他们已经支持从npm上安装任意模块,你只需要在package.json中定义他们,就可以魔法般的正常运行了!

对于我来说,这是一个重要的学习时刻!我从来甚至都不敢去想能支持npm,因为始终觉得那不可能。只有在看到对于“可能”的活生生的证据之后,我才开始对它进行更多的思考。在我消除 支持npm这一想法之前,我确实应该首先去探索下它的可能性才对。

那么理所当然的,我开始思考如何去达成这一目的。而且刚开始我觉得把它给过渡复杂化了。第一个版本我觉得仅仅是在脑子里想的话我是完全hold不住的,因此我不得不把它画下来:

【第2109期】CodeSandbox是如何让npm上的模块直接在浏览器端运行的

这种过于复杂的方案却有一个优点,那就是:最终的实现要比预想的简单的多!

我了解到,webpack的DLLPlugin可以打包依赖项,并且使用一个manifest清单来标记打出的js包包含哪些依赖项。这份清单看起来是这样的:

 
   
   
 
  1. {

  2. "name": "dll_bundle",

  3. "content": {

  4. "./node_modules/fbjs/lib/emptyFunction.js": 0,

  5. "./node_modules/fbjs/lib/invariant.js": 1,

  6. "./node_modules/fbjs/lib/warning.js": 2,

  7. "./node_modules/react": 3,

  8. "./node_modules/fbjs/lib/emptyObject.js": 4,

  9. "./node_modules/object-assign/index.js": 5,

  10. "./node_modules/prop-types/checkPropTypes.js": 6,

  11. "./node_modules/prop-types/lib/ReactPropTypesSecret.js": 7,

  12. "./node_modules/react/cjs/react.development.js": 8

  13. }

  14. }

每一个路径都映射一个模块id。如果我想引入 React,我只需要调用 dll_bundle(3),然后我就得到了React!这对我们的需求来说简直就是完美,于是我开始行动,并思考出了一个下面的系统:

【第2109期】CodeSandbox是如何让npm上的模块直接在浏览器端运行的

注:服务的源代码可以在这里找到:https://github.com/CompuIves/codesandbox-bundler 这个服务还包含了将任意的CodeSandbox项目代码发布到npm上(这真的很酷)的代码,我们后来取消了这个功能。

对于打包的每一个请求,我将在 tmp/:hash下面创建一个新的目录,接着运行 yarn add ${dependencyList},然后让 webpack做打包处理即可。同时作为一种缓存方案,我会将打出的新包保存至gcloud。这看起来比上面的方案图要简单的多,更多的是因为我使用yarn来安装依赖模块并使用 webpack做打包来作为前一个实现版本中的替代方案。

译者注:这里可能比较难理解,稍作解释。在作者刚开始画的方案图中,依赖项的递归查找跟打包都是作者自己想的一个方案,然后作者在了解了webpack的DLLPlugin方案后,开始使用这套方案来作为替代方案,来做这块事情。

当你在开始运行一个用例的时候,在真正执行(代码)之前我们会首先确认我们必须有manifest以及js bundle(译者注:这两个物料都是webpack dllplugin打包出来的)。在真正执行的过程中,对于每一个依赖项,我们会通过 dll_bundle(:id)来获取对应的代码并执行。这一切都工作的很好,我实现了有临时加载npm模块依赖的第一个实现版本!

【第2109期】CodeSandbox是如何让npm上的模块直接在浏览器端运行的

但是,这套系统仍然有一个非常大的限制,它不支持引入 不在webpack依赖关系图中的文件。这就意味着像是下面的这个例子:

 
   
   
 
  1. require('react-icons/lib/fa/fa-beer')

将不能正常运行,因为从依赖项的入口开始自始至终都不需要它,也就不会被打包进去。(译者注:webpack的打包是基于package.json里的依赖模块以及各个依赖模块的依赖项去做打包的,不被包含在这个体系里的文件则不会被打包进去)

尽管如此,我还是发布了这个版本的CodeSandbox,并且跟WebpackBin的作者Christian Alfoni取得了联系。我们使用了非常类似的系统来支持npm依赖项的获取,并且我们有相同的局限性。所以我们决定联手打造终极打包方案!

带有入口的webpack(webpack with entries)

“终极”打包方案保留了跟我们原来的打包方案相同的功能,除了Christian创建了一个算法,会按照依赖项的重要性,将文件加入到最终打包出的bundle中这一个差别。这意味着我们手动的增加了入口配置,以确保 webpack也可以将这些文件能够打包进去。在对这个方案做了非常多的调整之后,这个系统已经可以支持任意(?译者注:作者这里加了问号,表示并不太确定支持任意)组合的打包需求。因此你也可以去加载 react-icons,css文件也是可以的。

以上是关于第2109期CodeSandbox是如何让npm上的模块直接在浏览器端运行的的主要内容,如果未能解决你的问题,请参考以下文章

有没有办法在 codesandbox.io 中使用 npm 脚本?

第878期Node, NPM, Yarn 和 package.json

第1173期npm 2017 JavaScript 框架报告之前端框架

第112期:NLP—如何让爱保鲜

如何在“codesandbox.io”上配置 ESLint / Prettier 格式规则

第849期如何让前端更安全?——XSS攻击和防御详解