大家好,我卡頌。
你是否好奇 —— codesandbox是如何在線運行代碼的?
要回答這個問題,我們先看看前端項目是如何在本地跑起來的。簡單來說分為3步:
- 執行npm install安裝依賴。
- 使用打包工具(比如webpack)打包、編譯代碼(如果使用Vite會省去打包的步驟,但會執行「預構建」)。
- 將步驟2的產物通過script標簽注入頁面。
codesandbox能在線運行代碼,顯然他也實現了上述步驟,具體來說,codesandbox內置了2個在線服務:
- npm解析服務 —— 用于實現上述步驟1。
- 在線打包服務 —— 用于實現上述步驟2、3。
本文我們來聊聊如何實現并部署自己的npm解析服務。
codesandbox簡要工作原理
下面是一個常見的codesandbox界面,包含兩部分:
- 左邊的文件系統、代碼編輯器。
- 右邊的效果預覽區域。
其中「效果預覽區域」是一個iframe,對于上圖中的例子,iframe的地址是https://pjdp86.csb.App/。如果你打開這個地址,會發現他就是代碼的預覽效果:
但這并不意味著codesandbox幫我們部署了項目。實際上,這個地址中前端代碼是在頁面打開后再編譯、打包的。
打開codesandbox項目時經常看到的下述界面,就是前端編譯代碼的畫面:
具體來說,當我們打開一個codesandbox項目,iframe對應地址初始化時,會執行如下操作:
- 下載項目代碼(即編輯器中顯示的代碼)。
- 根據項目package.json中指明的依賴,從「npm解析服務」下載項目依賴的代碼。
- 下載在線打包器(一個mini webpack)、編譯器(babel)相關代碼。
- 在線打包、編譯。
- 運行打包后的代碼。
正是有了在線打包、編譯的流程,codesandbox才能在線運行:
- React項目(需要編譯JSX)。
- TS項目(需要編譯TS語法)。
- Vue項目(需要編譯SFC文件)。
回到本文的主題 —— 「npm解析服務」。當我們從項目package.json中獲取到依賴庫的名稱后,完全可以從CDN直接請求依賴庫對應的代碼,為什么還需要一個獨立的「npm解析服務」呢?
npm解析服務的作用
之所以需要獨立的「npm解析服務」,主要是因為 —— npm包本身可能還依賴別的npm包,如果每次初始化iframe時依次下載:
- package.json中指定的依賴
- 依賴的依賴
- 依賴的依賴的依賴
- ...
那會極大拖慢項目初始化的時間。同時,這樣做也可能會下載大量實際不會使用的代碼。
所以,需要一個「npm解析服務」,當第一個用戶第一次請求某個庫時,依次完成:
- 從庫的入口代碼解析AST,分析其中的require語句,遞歸的解析這個庫的依賴。
- 下載依賴代碼,將所有依賴的代碼匯總到一個JSON文件。
- 將步驟2的JSON文件保存在對象存儲中。
- 返回步驟2的JSON文件。
那么,后續所有用戶在請求這個庫時,都能直接從對象存儲中直接獲取解析好的JSON文件,這能極大提高在線安裝依賴的速度。
比如,react@18.2.0經由「npm解析服務」解析后會返回如下JSON:
{
"contents": {
"/node_modules/react/index.js": {
// 庫的代碼
"content": "...省略",
"isModule": false,
// 依賴的其他模塊
"requires": [
"./cjs/react.production.min.js",
"./cjs/react.development.js"
]
},
"/node_modules/react/cjs/react.production.min.js": {/*省略*/},
"/node_modules/react/cjs/react.development.js": {/*省略*/},
"/node_modules/js-tokens/package.json": {/*省略*/},
"/node_modules/loose-envify/package.json": {/*省略*/},
"/node_modules/react/package.json": {/*省略*/}
},
// 庫的版本信息
"dependency": {
"name": "react",
"version": "18.2.0"
},
"peerDependencies": {},
// 依賴的依賴
"dependencyDependencies": {
"loose-envify": {/*省略*/},
"js-tokens": {/*省略*/}
},
"dependencyAliases": {}
}
上述JSON中,入口代碼在/node_modules/react/index.js,通過遞歸分析他的AST,發現他依賴了:
- "./cjs/react.production.min.js"
- "./cjs/react.development.js"
于是,這2個文件對應代碼也包含在JSON中。
當下一個用戶加載的項目依賴react@18.2.0,就能直接從對象存儲中獲取上述JSON。
npm解析服務的實現
codesandbox在線打包相關的代碼都是開源的,比如:
- 編輯器的部分對應sandpack-react[1]
- npm解析服務對應dependency-packager[2]
- 在線打包服務對應codesandbox-client[3]
所以,我們可以基于dependency-packager部署自己的「npm解析服務」。
dependency-packager是一個serverless服務,通過AWS Lambda部署。由于采用的是開源的serverless框架,所以我們可以很方便的將項目中AWS Lambda的部分替換成其他serverless服務商(比如阿里云函數計算)。
整個dependency-packager包含兩個serverless函數:
- api:實際對外提供的服務
- packager:根據包名和版本號生成JSON的服務
他們的關系如下:
其中,生成的JSON保存在AWS S3中。同樣,這里也可以替換成其他云服務廠家的存儲方案。
packager服務的工作流程如下:
其中,「驗證依賴的入口文件」會嘗試下面這些文件后綴:
const found = [
path.join(basedir, pkg.module),
path.join(basedir, pkg.module + ".js"),
path.join(basedir, pkg.module + ".cjs"),
path.join(basedir, pkg.module + ".mjs"),
path.join(basedir, pkg.module, "index.js"),
path.join(basedir, pkg.module, "index.mjs"),
].find((p) => {
try {
const l = fs.statSync(p);
return l.isFile();
} catch (e) {
return false;
}
});
驗證完成后,會以package.json中的module或mAIn字段作為入口文件,將代碼轉換為AST,分析AST中的require語句(cjs語法中引入模塊的語法),找到依賴的模塊。最終將這些模塊匯總在JSON中。
總結
codesandbox在線打包相關的代碼都是開源的,包括:
- 編輯器
- npm解析服務
- 在線打包服務
其中,npm解析服務作為一個serverless服務包括兩部分:
- api服務
- packager服務
packager服務代碼量不多,如果想嘗試部署自己的serverless服務,是個不錯的選擇。
參考資料
[1]sandpack-react:https://Github.com/codesandbox/sandpack/tree/main/sandpack-react。
[2]dependency-packager:https://github.com/codesandbox/dependency-packager。
[3]codesandbox-client:https://github.com/codesandbox/codesandbox-client。