图片起源:https://unsplash.com/photos/m…
本文作者:伍六一
文章首发于我的博客 https://github.com/mcuking/bl…
背景
间隔公布如何私有化部署 CodeSandbox 沙箱的文章《搭建一个属于本人的在线 IDE》曾经过了一年多的工夫,最开始是为了在区块复用平台上可能实时构建前端代码并预览成果。不过在去年云音乐外部启动的基于源码的低代码平台我的项目中,同样有 在线实时构建前端利用 的需要,最后是采纳从零开发沙箱的形式,不过自研沙箱存在以下几点问题:
-
灵活性较差
被构建利用的 npm 依赖须要提前被打包到沙箱自身的代码中,无奈做到在构建过程中动静从服务获取利用依赖内容;
-
兼容性较差
被构建利用的技术选型比拟受限,比方不反对应用 less 等;
-
未实现与平台的隔离
低代码平台和沙箱没有用相似 iframe 作为隔离,会存在沙箱构建页面的全局变量或者款式上被内部的低代码平台净化的问题。
当然如果持续在这个自研沙箱上持续开发,下面提到的问题还是能够逐渐被解决的,只是须要投入更多的人力。
而 CodeSandbox 作为目最支流且成熟度较高的在线构建沙箱,不存在下面列出的问题。而且实现代码全副开源,也不存在平安问题。于是便决定采纳私有化部署的 CodeSandbox 来替换低代码平台的自研沙箱,期间工作次要分为上面两方面:
-
针对低代码平台的定制化需要
例如为了实现组件的拖拽到沙箱构建的页面中,须要对沙箱构建好的页面进行跨 iframe 的原生事件监听,以便进一步计算拖拽的精确地位。
-
晋升沙箱构建速度
因为低代码平台须要在线搭建利用,存在两个特点:首先是须要构建残缺的前端利用代码而非某些代码片段,其次是须要频繁地批改利用代码并实时查看成果,因而对沙箱的构建性能有较高要求。
其中在晋升沙箱构建速度的过程中一波三折:从最后破费靠近 2 分钟构建一个蕴含 antd
依赖的简略中后盾利用,一步步优化到 1 秒左右实现秒开,甚至曾经比 CodeSandbox 官网的沙箱构建速度还要更快。
补充:下面提到两个平台的文章介绍如下,感兴趣的能够自行查看:
低代码平台:网易云音乐低代码体系建设思考与实际
区块复用平台:跨我的项目区块复用计划实际
上面就来介绍下 CodeSandbox 沙箱性能优化过程,在正式开始之前,为了不便读者更容易了解,先简要介绍下沙箱的构建过程。
沙箱构建过程
CodeSandbox 实质上是在浏览器中运行的简化版 Webpack,上面是整个沙箱的架构图,次要蕴含两局部:在线 Bundler 局部和 Packager 服务。
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13530612155/ab13/1b1f/15b9/fea41fd4ea01649aa3a0216b4ea01aa6.png” width=”800″/>
其中应用方只需引入封装好的 Sandbox 组件即可,组件外部会创立 iframe 标签来加载部署好的沙箱页面,页面中的 js 代码就是沙箱的外围局部 — 在线 Bundler。沙箱构建流程中首先是 Sandbox 组件将须要蕴含被构建利用源代码的 compile 指令通过 postMessage 传递给 iframe 内的在线 Bundler,在线 Bundler 在接管到 compile 指令后便开始构建利用,最开始会事后从 npm 打包服务获取利用的 npm 依赖内容。
上面别离对沙箱构建的三个阶段 — 依赖预加载阶段、编译阶段、执行阶段,进行具体论述。
依赖预加载阶段(Npm Preload)
为什么须要依赖预加载阶段
因为在浏览器环境中很难装置前端利用的 node_modules
资源,所以编译阶段须要从服务端获取依赖的 npm 包的模块资源,通过 npm 包的入口文件字段(package#main
等)和 meta 信息计算 npm 包中指定模块在 CDN 上的具体门路,而后申请获取模块内容。举个例子:
如果前端利用的某视图模块 demo.js
援用了 react
依赖,如下图:
import React from 'react';
const Demo = () => (<div>Demo</div>);
export default Demo;
在编译完 demo.js
模块后会持续编译该模块的依赖 react
,首先会从 CDN 上获取 react
的 package.json
模块内容和 react
的 meta 信息:
https://unpkg.com/react@17.0….
https://unpkg.com/react@17.0….
而后计算失去 react
包入口文件的具体门路(整个过程也就是 file resolve 的过程),从 CDN 上申请该模块内容:
https://unpkg.com/react@17.0….
接着持续编译该模块及其依赖,如此递归编译直到将利用中所有被援用到的依赖模块编译实现。
可见浏览器端实现的沙箱在整个编译利用过程中须要一直从 CDN 上获取 npm 包的模块内容,产生十分多的 HTTP 申请,也就是传说中的 HTTP 申请瀑布流。又因为浏览器对同一域名下的并发 HTTP 申请数量有限度(例如针对 HTTP/1.x 版本的 HTTP 申请,其中 Chrome 浏览器限度数量为 6 个),最终导致整个编译过程十分耗时。
依赖预加载阶段的运行机制
为了解决这个问题,于是便有了 依赖预加载阶段 — 即在开始编译利用之前,沙箱先从 npm 打包服务中申请利用依赖的 npm 包内容,而打包服务会将 npm 包的被导出的模块打包成一个 JSON 模块返回,该模块也被称为 Manifest。 例如上面就是 react 包的 Manifest 模块的链接和截图:
https://prod-packager-package…
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13388866719/4546/50e9/506b/559ff560a3edb5f7025560418da0d502.png” width=”600″/>
这样获取每个 npm 包的内容只须要发送一个 HTTP 申请就能够了。
在依赖预加载阶段,沙箱会申请利用中所有依赖包的 Manifest,而后合并成一个 Manifest。目标是为了在接下来的编译阶段,沙箱只须要从 Manifest 中查找 npm 包的某个具体模块即可。当然如果在 Manifest 中找不到,沙箱还是会从 CDN 上申请该模块以确保编译过程顺利进行。
Packager 服务的原理
下面提到的 npm 打包服务(也称 Packager 服务)的基本原理如下:
先通过 yarn 将指定 npm 包装置到磁盘上,而后解析 npm 包入口文件的 AST 中的 require 语句,接着递归解析被 require 模块,最终将所有被援用的模块打包到 Manifest 文件中输入(目标是为了剔除 npm 包中多余模块,例如文档等)。
简而言之 依赖预加载阶段就是为了防止在编译阶段产生大量申请导致编译工夫过长。和 Vite 的依赖预构建的局部指标是雷同的 — 依赖预构建。
留神:这里之所以如此具体地介绍依赖预加载阶段存在的必要性和运行机制,次要是为了前面论述沙箱性能优化局部做铺垫。读者读到性能优化局部有些不了解的话,能够再返回来复习下。
编译阶段(Transpilation)
简略来说 编译阶段就是从利用的入口文件开始, 对源代码进行编译, 解析 AST,找出上级依赖模块,而后递归编译,最终造成一个依赖关系图。其中模块之间相互援用遵循的是 CommonJS 标准。
补充:对于模仿 CommonJS 的内容能够参考上面对于 Webpack 的文章,因为篇幅问题这里就不开展了:webpack 系列 —— 模块化原理 -CommonJS
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13389097802/ba88/1444/48ab/01fae62a0cdd8e9a77c5c68e0d3821bd.png” width=”600″/>
执行阶段(Evaluation)
和编译阶段一样,也是 从入口文件开始,应用 eval 执行入口文件,如果执行过程中调用了 require,则递归 eval 被依赖的模块。
到此沙箱的构建过程就论述完了,更多具体内容可参考以下文章:
- CodeSandbox 如何工作? 上篇
- 从 0 到 1 实现浏览器端沙盒运行环境
晋升沙箱构建速度
接下来就进入到本文的主题 — 如何晋升沙箱的构建速度。整个过程会以文章结尾提到的蕴含 antd
依赖的简略中后盾利用的构建为例,论述如何逐渐将构建速度从 2 分钟优化到 1s 左右。次要有以下四个方面:
- 缓存 Packager 服务打包后果
- 缩小编译阶段单个 npm 包模块申请数量
- 开启 Service-Worker + CacheStorage 缓存
- 实现类 Webpack Externals 性能
缓存 Packager 服务打包后果
通过对沙箱构建利用过程的剖析,首先发现的问题是在依赖预加载阶段从 Packager 服务申请 antd
包的 Manifest 耗时 1 分钟左右,有时甚至会有申请超时的状况。依据后面对 Packager 服务原理的论述,能够判断出导致耗时的起因次要是 antd
包(包含其依赖)体积较大,无论是下载 antd
包还是从 antd
包入口文件递归打包所有援用的模块都会十分耗时。
对此能够将 Packager 服务的打包后果缓存起来,沙箱再次申请时则间接从缓存中读取并返回,无需再走下载 + 打包的过程。其中缓存的具体形式读者可依据本身状况来决定。至于首次打包过慢问题,能够针对罕用的 npm 包提前申请 Packager 服务来触发打包,以保障在构建利用过程中能够疾速获取到 npm 包的 Manifest。
在缓存了 Packager 服务打包后果之后,利用的构建工夫就从近 2 分钟优化到了 70s 左右。
缩小编译阶段单个 npm 包模块申请数量
持续剖析沙箱在编译阶段的网络申请时,会发现会有大量的 antd
包和 @babel/runtime
包相干的模块申请,如下图所示:
<img src=”https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13126402203/7d79/45aa/2a18/b0f77c569c2d4615d70b9fd453c4cf4f.png” width=”600″ />
依据下面沙箱原理局部的解说能够晓得,依赖预加载阶段就是为了防止在编译阶段产生大量 npm 单模块申请而设计的,那为什么还会有这么多的申请呢?起因总结来说有两个:
- Packager 服务和沙箱构建时确定 npm 包的入口文件不同
- npm 包自身没有指定入口文件或入口文件不能关联所有编译时会用到的模块
Packager 服务和沙箱构建时确定 npm 包的入口文件不同
以 antd
包的为例,该包自身的依赖大部分为外部组件 rc-xxx
,其 package.json
同时蕴含两个字段 main
和 module
,以 rc-slider
为例,上面是该包的 package.json
无关入口文件定义局部(留神其中入口文件名没有后缀):
{
"main": "./lib/index",
"module": "./es/index",
"name": "rc-slider",
"version": "10.0.0-alpha.4"
}
咱们曾经晓得了 Packager 服务是从 npm 包的入口文件开始,递归将所有被援用的模块打包成 Manifest 返回的。其中 module
字段优先级高于 main
字段,所以 Packager 服务会以 ./es/index.js
作为入口文件开始打包。但在实现 Manifest 打包后和正式返回给沙箱前,还会校验 package.json
中 module
字段定义的入口文件是否在 npm 包中实在存在,如果不存在则会将 module
字段从 package.json
中删除。
可怜的是测验入口文件是否实在存在的逻辑中没有思考到文件名没有后缀的状况,而恰好该 npm 包的 module 字段没有写文件后缀,所以在返回的 Manifest 中 rc-slider
的 package.json
的 module
字段被删除了。
接下来是浏览器侧的沙箱开始编译利用,编译到 rc-slider
依赖时,因为 rc-slider
的 package.json
的 module
字段被删除,所以是依照 main
字段指定的 ./lib/index.js
模块作为入口文件开始编译,然而 Manifest 中只有 es
目录下的模块,所以只能在编译过程中从 CDN 动静申请 lib
下的模块,由此产生了大量 HTTP 申请阻塞编译。
<img src=”https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13129381222/e38d/833d/f66c/94ec5d312a65607be0d4d151d49dc882.png” width=”600″>
无关 Packager 服务没有兼容入口文件名无后缀的问题,笔者曾经向 CodeSandbox 官网提交 PR 修复了,点击查看。
接下来再看另外一个例子 — ramda
包的 package.json
中无关入口文件局部:
{
"exports": {
".": {
"require": "./src/index.js",
"import": "./es/index.js",
"default": "./src/index.js"
},
"./es/": "./es/",
"./src/": "./src/",
"./dist/": "./dist/"
},
"main": "./src/index.js",
"module": "./es/index.js",
"name": "ramda",
"version": "0.28.0"
}
Packager 服务是 module
字段指定的 ./es/index.js
作为入口开始打包的,但编译阶段中沙箱却最终抉择 export
中 .
的 default
指定的 ./src/index.js
作为入口开始编译,进而也产生了大量的单个模块的申请。
问题的实质就是【Packager 服务打包 npm 包时】和【沙箱构建利用时】确定 npm 包入口文件的策略并不完全一致,想要根治该问题就要对其两侧的确定入口文件的策略。
沙箱侧确定入口文件的逻辑在 packages/sandpack-core/src/resolver/utils/pkg-json.ts 中。Packager 服务侧相干逻辑在 functions/packager/packages/find-package-infos.ts / functions/packager/packages/resolve-required-files.ts / functions/packager/utils/resolver.ts 中。
读者可自行决定抉择 以 Packager 服务侧还是沙箱侧的 npm 入口文件的确定策略 作为统一标准,总之肯定要保障两侧的策略是统一的。
npm 包自身没有入口文件或入口文件不能关联所有编译时会用到的模块
首先剖析下 @babel/runtime
包,通过该包的 package.json
能够发现其并没有定义入口文件,个别应用该包都是间接援用包中的具体模块,例如 var _classCallCheck = require("@babel/runtime/helpers/classCallCheck");
,所以依照 Packager 服务的打包原理是无奈将该包中的 编译时会用到的模块 打包到 Manifest 中的,最终导致编译阶段产生大量单个模块的申请。
对此笔者也只是采纳非凡状况非凡解决的形式:在打包没有定义入口文件或入口文件不能关联所有编译时会用到的模块的 npm 包时,在 npm 打包过程中手动将指定目录下或指定模块打包到 Manifest 中。例如对于 @babel/runtime
包来说,就是在打包过程中将其根目录下的所有文件都手动的打包到 Manifest 中。目前还没有更好的解法,如果读者有更好的解法欢送留言。
当然如果是外部的 npm 包,也能够在 package.json
中减少相似 sandpackEntries
的自定义字段,即指定多个入口文件,便于 Packager 服务将编译阶段用到的模块尽可能都打包到 Manifest 中。例如针对低代码平台的组件可能会分为失常模式和设计模式,其中设计模式是为了在低代码平台更不便的拖动组件和配置组件参数等,会在 index.js 之外再定义 designer.js 作为设计模式下组件入口文件,这种状况就能够指定多个入口文件(多个入口概念仅针对 Packager 服务)。相干革新是在 functions/packager/packages/resolve-required-files.ts
中的 resolveRequiredFiles
函数,如下图所示:
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13402508017/07af/9721/9b29/ede40ce68f0c4790962bc29fede7359f.png” width=”500″>
通过缩小编译阶段单个 npm 包模块申请数量,利用的构建工夫从 70s 左右降到了 35s 左右。
开启 Service-Worker + CacheStorage 缓存
笔者在剖析大量 npm 包单个模块申请问题时,也在 CodeSandbox 官方站点的沙箱中构建完全相同的利用,并没有遇到这个问题,起初才发现官网只是将曾经申请过的资源缓存起来。也就是说在第一次应用 CodeSandbox 或在浏览器隐身模式下构建利用,还是会遇到大量 HTTP 申请问题。
那么官网是如何缓存的呢?首先通过 Service-Worker 拦挡利用构建过程中的申请,如果发现是须要被缓存的资源,则先从 CacheStorage 中查找是否已缓存过,没有则持续申请远端服务,并将申请返回的内容缓存一份到 CacheStorage 中;如果查找到对应缓存,则间接从 CacheStorage 读取并返回,从而缩小申请工夫。
如下图所示,CodeSandbox 缓存内容次要包含:
- 沙箱页面的动态资源模块
- 从 Packager 服务申请的 npm 包的 Manifest
- 从 CDN 申请的 npm 包单个模块内容
<img src=”https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13395055869/19d6/a503/2598/2e30656534e446e7bcca5c114e3e3da3.png” width=”600″ />
不过 CodeSandbox 在对外提供的沙箱版本中将缓存性能敞开了,咱们须要开启该性能,相干代码在 packages/app/src/sandbox/index.ts
中,如下图所示:
<img src=”https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13395408070/618b/9fc5/c09a/7ee95d09d1c2ee933aa1b0bf1951f762.png” width=”400″ />
另外该缓存性能是通过 SWPrecacheWebpackPlugin
插件实现的 — 在打包 CodeSandbox 沙箱代码时,启用 SWPrecacheWebpackPlugin
插件并向其传入具体的缓存策略配置,而后会在构建物中主动生成 service-worker.js
脚本,最初在沙箱运行时注册执行该脚本即可开启缓存性能。这里咱们须要做的是将其中缓存策略的地址批改成咱们私有化部署的沙箱对应地址即可,具体模块在 packages/app/config/webpack.prod.js
中:
<img src=”https://p5.music.126.net/obj/wo3DlcOGw6DClTvDisK1/14649934730/d72a/7409/b4f7/467e00f4d867235a1f3712c42d11e5fd.png” width=”500″ />
补充:SWPrecacheWebpackPlugin 插件次要是作用防止手动编写 Service Worker 脚本,开发者只须要提供具体的缓存策略即可,更多细节可点击上面链接:https://www.npmjs.com/package…
开启浏览器侧的缓存之后,利用的构建工夫根本能够稳固到 12s 左右。
实现类 Webpack Externals 性能
以上三个方面的优化根本都是在网络方面 — 或减少缓存或缩小申请数量。那么编译和执行代码自身是否能够进一步优化呢?接下来就一起来剖析下。
笔者在应用浏览器调试工具调试沙箱的编译过程时发现一个问题:即便利用中仅仅应用了 antd
包的一个组件,例如:
import React from 'react';
import {Button} from 'antd';
const Btn = () => (<Button>Click Me</Button>);
export default Btn;
但仍会编译 antd
包内所有组件关联的模块,最终导致编译工夫过长。通过排查发现次要起因是 antd
的入口文件中援用了全副组件。上面是 es 模式下的入口文件 antd/es/index.js
的局部代码:
export {default as Affix} from './affix';
export {default as Anchor} from './anchor';
export {default as AutoComplete} from './auto-complete';
...
依据下面编译阶段和执行阶段的解说咱们能够晓得,沙箱会从 antd
入口文件开始对所有被援用的模块进行递归编译和执行。
因为沙箱也应用 babel 编译 js 文件,所以笔者最开始想到的是在编译 js 文件时集成 babel-plugin-import
插件,该插件的作用就是实现组件的按需引入,点击查看插件更多细节。上面的代码编译成果会更直观一些:
import {Button} from 'antd';
↓ ↓ ↓ ↓ ↓ ↓
var _button = require('antd/lib/button');
集成该插件后发现沙箱构建速度确实有所晋升,但随着利用应用的组件增多,构建速度会越慢。那么是否有更好的形式来缩小甚至不需编要译模块呢?有,实现类 Webpack Externals 性能,上面是整个性能的原理:
1. 在编译阶段跳过 antd
包的编译,以缩小编译工夫。
2. 在执行阶段开始之前先通过 script 标签全局加载和执行 antd
的 umd 模式的构建物,如此以来 antd
包中导出的内容就被挂载到 window 对象上了。接下来在执行编译后的代码时,如果发现须要援用的antd
包中的组件,则从 window 对象获取返回即可。因为不再须要执行 antd
包所有组件关联的模块,所以执行阶段的工夫也会缩小。
注:这里波及到 Webpack Externals 和 umd 模块标准的概念,因为篇幅问题就不在这里细说了,有趣味可通过上面链接理解:
- 内部扩大(Externals)
- UMD:AMD 和 CommonJS 的糅合
思路有了,接下来就开始对 CodeSandbox 源码进行革新:
首先是编译阶段的革新,当编译完某个模块时,会增加该模块的依赖而后持续编译。在增加依赖时,判断如果依赖是被 external 的 npm 包则间接退出,以阻断进一步对该依赖的编译。
具体代码在 packages/sandpack-core/src/transpiled-module/transpiled-module.ts
,改变如下图所示:
<img src=”https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13397619213/4563/6f5d/596b/caeb1d7e2836d9c9c774c1fe38568509.png” width=”500″ />
而后是执行阶段的革新,因为 CodeSandbox 最终是将所有模块编译成 CommonJS 模块而后模仿 CommonJS 的环境来执行(下面的沙箱构建过程局部有提到)。所以只须要在模仿的 require 函数中判断如果是被 external 的 npm 包援用模块,间接从 window 对象获取返回即可。
具体代码在 packages/sandpack-core/src/transpiled-module/transpiled-module.ts
,改变如下图所示:
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13397805723/a322/5a77/0685/0212cb02a47c8631af551a0f24fdad2f.png” width=”500″ />
另外在沙箱开始执行编译后的代码之前,须要动态创建 script 标签来加载和执行 antd
包 umd 模式的构建物,侥幸的是 CodeSandbox 曾经提供了动静加载内部 js/css 资源的能力,不须要额定开发。只须要将须要 js/css 资源的链接通过 externalResources 参数传给沙箱即可。
最初就须要在 sandbox.config.json
文件中配置相干参数即可,如下图所示:
{
"externals": {
"react": "React",
"react-dom": "ReactDOM",
"antd": "antd"
},
"externalResources": [
"https://unpkg.com/react@17.0.2/umd/react.development.js",
"https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js",
"https://unpkg.com/antd@4.18.3/dist/antd.min.js",
"https://unpkg.fn.netease.com/antd@4.18.3/dist/antd.css"
]
}
补充:
sandbox.config.json
文件中的内容会在沙箱构建获取到,该文件是放在被构建利用的根目录下。点击查看 configuration 详情。
最终通过下面四个方面的优化,沙箱只需 1s 左右即可实现对整个利用的构建,成果如下图所示:
将来布局
那么沙箱的构建性能优化计划是否就曾经靠近完满了呢?
答案当然是否定的,读者能够试想下,随着构建利用的规模变大,须要编译和执行的模块也会增多,CodeSandbox 沙箱这种 通过利用的入口文件递归编译所有援用模块,而后再从利用入口文件递归执行所有援用模块 的模式,必然还会导致整个构建工夫不可避免地减少。
那么是否有更好的形式呢?最近很风行的 Vite 提供了一种思路:在利用代码执行过程中,通过 ES Module 形式援用了其余模块,浏览器会发动一个申请获取该模块,服务器拦挡申请匹配到对应模块后对其进行编译并返回。这种不须要对利用模块进行提前全量编译,按需动静编译的形式会极大缩利用构建工夫,利用越简单构建速度的劣势越显著。
笔者正在尝试革新 Vite 使其可能运行在浏览器中,过程中的播种会总结到沙箱系列下一篇文章中 —《搭建一个浏览器版 Vite 沙箱》,沙箱原型的实现代码也会同步到 https://github.com/mcuking/vi… 中,敬请期待!
结束语
在用户端的浏览器中实现能够运行代码(涵盖前端 / Node 服务等利用的代码)的沙箱环境,绝对在服务端容器中运行代码的形式,具备不占用服务资源、经营成本低、启动速度快等劣势,在很多利用场景下都能够发明可观的价值。另外浏览器版沙箱也是为数不多的富前端利用,整个沙箱利用的主体性能都是在浏览器中实现,对前端开发工作提出了更大的挑战。
下图是笔者这两年在沙箱畛域的一些尝试,欢送感兴趣的同学一起交换:https://github.com/mcuking/blog
<img src=”https://p5.music.126.net/obj/wo3DlcOGw6DClTvDisK1/13992673209/34ae/2e68/4c95/5e7760ab26d65ad5426fe90546d79b41.png” width=800/>
参考资料
- 搭建一个属于本人的在线 IDE
- CodeSandbox 如何工作? 上篇
- 从 0 到 1 实现浏览器端沙盒运行环境
- 网易云音乐低代码体系建设思考与实际
- 跨我的项目区块复用计划实际
本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!