<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          ESModule 系列 (二):構(gòu)建下一代基礎(chǔ)設(shè)施 PDN

          共 8644字,需瀏覽 18分鐘

           ·

          2021-09-04 23:29

          借助包的分發(fā)服務(wù),我們甚至能將本地安裝依賴的速度提升10倍

          ESM包的分發(fā)

          什么是ESM包的分發(fā)?參考一下下面的幾個網(wǎng)站

          • https://esm.sh/[1]
          • https://cdn.skypack.dev/[2]
          • https://jspm.org/[3]

          簡單來講,這些站點都做了一件事情:將 npm 倉庫上的包轉(zhuǎn)化成支持 esmodule 的版本并通過 url 來進(jìn)行分發(fā)。

          為什么需要分發(fā)

          • 為了迎合瀏覽器的發(fā)展浪潮。隨著 ECMAScript 2015 提出 ECMAScript Module 規(guī)范以來,各個瀏覽器都在積極地推進(jìn)著瀏覽器模塊系統(tǒng)的實現(xiàn)?,F(xiàn)今(2021年),各個主流瀏覽器已經(jīng)基本全面實現(xiàn)并內(nèi)置了 ESModule 系統(tǒng),為了更好的利用以往用 CMD 或者 AMD 規(guī)范開發(fā)的眾多 NPM 包,ESM包的分發(fā)網(wǎng)站應(yīng)運而生。
          • ESM 可以替換掉之前使用UMD加載組件庫(或其他包)的場景
          • 隨著 HTTP 2/3 的發(fā)展,5G 網(wǎng)絡(luò)的普及,網(wǎng)絡(luò)延時在 Web 交互中的權(quán)重會不斷的降低,而上一代 Web 開發(fā)范式(即利用 bundle 工具如 webpack 等將源代碼打成一個大的 bundle )會逐漸被瀏覽器原生的模塊加載機(jī)制所取代
          • 借助 CDN ,可以對一個特定版本的 NPM 包 轉(zhuǎn)化而來的 ESM 包做永久存儲。因為對于 NPM 的每一個包都會有版本號控制,版本號不變內(nèi)容就不會變。而一個 package@version 一旦轉(zhuǎn)化成 ESM 包后就可以被永久化存儲
          • 可以配合 Esbuild 等新一代構(gòu)建工具提升本地依賴的安裝速度(定一個小目標(biāo):提速20倍)

          原理

          將一個 NPM 包轉(zhuǎn)化為一個支持 ESM 規(guī)范的包,需要做的其實就是針對模塊語法進(jìn)行升級,將傳統(tǒng)的 ADM/CMD/UMD 語法,通過 AST 的解析,將其轉(zhuǎn)化為 ESModule 語法。

          困境

          模塊語法的轉(zhuǎn)化,不同于用 babel 將 ES6 轉(zhuǎn)化為 ES5,從 ES6 到 ES5 是語法上的降級,而從 ADM/CMD/UMD 模塊語法到 ESM 語法的轉(zhuǎn)化,是屬于語法的升級,升級過程中勢必會遇到很多語法兼容問題。

          1. CMD模塊語法的動態(tài)導(dǎo)入導(dǎo)出問題

          眾所周知,Commonjs 模塊語法是動態(tài)執(zhí)行的,即 require() 執(zhí)行之后拿到的模塊有哪些屬性,只有代碼真正執(zhí)行到 require 函數(shù)調(diào)用的那一行時才能知道,而 ESModule 模塊語法規(guī)范中,模塊的引入和導(dǎo)出在源代碼執(zhí)行之前就已經(jīng)通過靜態(tài)語法解析完成。

          // exports.cjs
          module.exports = {}
          // require.cjs
          console.info('start require')
          const { keyA } = require('./exports.cjs')
          console.info('require done')

          // log
          start require
          require done

          [CJS]

          // exports.mjs
          export default {
              KeyA,
              keyB,
          }

          // imports.mjs
          console.info('start import')
          import { keyA } from './exports.mjs'
          console.info('import done')

          // log
          error, 'keyA' is not exported by './exports.mjs'

          [ESM]

          可以看到,ESM 模塊語法在代碼執(zhí)行前就會通過靜態(tài)語法檢測,解析出子模塊的具名導(dǎo)出變量和默認(rèn)導(dǎo)出變量,然后會根據(jù)導(dǎo)入語法,在代碼真正執(zhí)行前先進(jìn)行一次校驗,如果引入了錯誤的變量,會直接拋出錯誤;而 CJS 模塊語法不會預(yù)先進(jìn)行語法檢測,而是運行源代碼,運行到 require 函數(shù)被調(diào)用時才會去處理子模塊的導(dǎo)出。而 CJS 和 ESM 的模塊導(dǎo)出機(jī)制也是不同的。在 CJS 中, module.exports 和 exports 對象其實是同一個引用,即,不論用戶用什么語法來導(dǎo)出屬性,最終導(dǎo)出的屬性全是掛在了一個對象的引用上,而其他模塊引用這個模塊時,require 執(zhí)行之后拿到的其實就是這個引用對象。而在 ESM 中,export default 和 export {} 屬于兩種完全不同的導(dǎo)出語法,通過默認(rèn)導(dǎo)出語法 export default 導(dǎo)出的值,只能通過 import A 或者 import { default as A } 來導(dǎo)入,通過具名導(dǎo)出語法 export { A } 導(dǎo)出的值,只能通過 import { A } 導(dǎo)入。這兩種導(dǎo)入導(dǎo)出方式不能混用,若錯誤使用,瀏覽器底層會直接拋出錯誤,而在 CJS 中,由于導(dǎo)出的值一直是一個對象,所以通過 require 引入模塊時,是不會拋出語法錯誤的(除非模塊不存在)。而目前生態(tài)最成熟的 ESM 轉(zhuǎn)化工具比如 Rollup 和 Esbuild,他們對于 CJS 模塊的轉(zhuǎn)化支持也不是很友好。

          // react.production.js
          module.exports = {
              createElement,
              ...React
          }

          // react.production.transpiled.mjs
          const ReactLib = _commonjs(() => {
              return {
                  createElement,
                  ... React
              }
          })
          export default ReactLib

          [React的ESM轉(zhuǎn)化]

          可以看到,React 的 cjs 代碼經(jīng)過 Rollup 或者 Esbuild 轉(zhuǎn)化之后,會直接被編譯成只有一個默認(rèn)導(dǎo)出的模塊,通過這樣的轉(zhuǎn)化,在使用 React 時,會與我們常規(guī)的使用習(xí)慣有所沖突。

          // Success
          import React from 'react.production.traspiled.mjs'
          React.createElement(xxx)

          // Error: 'createElement' is not exported from 'react.production.traspiled.mjs'
          import { createElement } from 'react.production.traspiled.mjs'
          1. 循環(huán)引入,動態(tài)引入語法在 ESM 中沒有與 CMD 對等的語法轉(zhuǎn)化

          在 CJS 中,由于 require 本身就是動態(tài)的同步函數(shù),所以 CJS 本身是支持動態(tài)引入的,而在 ESM 中,原生不支持同步的動態(tài)引入,想要在 ESM 中使用動態(tài)引入語法,只能通過 import().then() 的異步引入來模擬。但是這兩者其實語法并不能做等價,其中,require 是同步執(zhí)行的語法,返回結(jié)果是引入的對象;而 import() 是異步執(zhí)行的語法,返回結(jié)果是一個 Promise

          // cjs
          module.exports = {
              Modulerequire('Module')
          }

          // esm
          import Module from 'Module'
          export default {
              Module
          }

          [非嚴(yán)格意義上的動態(tài)引入轉(zhuǎn)化]

          通過以上方案轉(zhuǎn)化來的動態(tài)引入,原語義是希望在使用的時候再引用,而轉(zhuǎn)化之后的 ESM 語法將其變?yōu)榱?,先引用,再使用,可能?dǎo)致 'Module' 模塊內(nèi)部實例化未完成的情況下就已經(jīng)被使用,導(dǎo)致出現(xiàn) Module.xxx is not defined 的問題。

          • 比如 protobufjs,參考 https://cdn.skypack.dev/-/[email protected]/dist=es2020,mode=imports/optimized/protobufjs.js[4]
          1. 共享 Context 重復(fù)打包的問題

          由 CMD 轉(zhuǎn)化為 ESM 的過程中,分發(fā)網(wǎng)絡(luò)通常會使用 Rollup 等工具,將依賴包的源代碼全部打包到一起,最后提供一個 ESM 單文件,這樣可以顯著的減少網(wǎng)絡(luò)請求量(比如,請求 antd 包,如果不打包源碼,可能需要遞歸引入 antd/es/** 下的所有文件,這樣網(wǎng)絡(luò)請求數(shù)量可能達(dá)到數(shù)百級別)。

          import * as Module from 'antd.mjs'

          同樣的,如果引用 ESM 包的不同路徑文件時,比如 [email protected]/es/index.js[email protected]/esm/components/core/update , 若這兩個路徑的 ESM 單文件中引用了同樣的 Context (比如 React Context),那么最終每個路徑的文件里面都會包含一份 Context 的代碼,這就導(dǎo)致最終的運行結(jié)果不符合預(yù)期。

          // [email protected]/es/index.js
          import Context from '/common/Context'
          Context.setContext({ ... })
          // [email protected]/esm/components/core/update
          import Context from '/common/Context'
          Context.setContext({ ... })

          // ESM 轉(zhuǎn)化結(jié)果
          // [email protected]/es/index.js
          Context = React.createContext()
          Context.setContext()
          //[email protected]/esm/components/core/update
          Context = React.createContext()
          Context.setContext()

          可以看到,以上兩個同 ESM 包的不同路徑,但是打包了兩份一樣的 Context。

          1. 其他問題...

          解決方案

          1. 通過 AST 等方案,直接動態(tài)解析出所有 exports.xxxObject.definedProperty(exports, 'xxx') 等語句,手動將其編譯成具名導(dǎo)出語法 export { xxx }

          2. 通過在 Node.js 中模擬一個 Browser Context,在 Context 中嘗試調(diào)用 require('Module'),通過 CJS 加載方式拿到模塊的導(dǎo)出對象,將其手動編譯成具名導(dǎo)出和默認(rèn)導(dǎo)出方案

          with (BrowserContext) {
              try {
                  const Module = require(ModuleName)
                  code += `\n export {`
                  Object.keys(Module).forEach(namedExport => {
                      code += `${namedExport}, `
                  })
                  code += '}'
              } catch (e) {}
          }
          1. 通過動態(tài)白名單的方式,針對有動態(tài)引入的 NPM 包,在轉(zhuǎn)化成 ESM 包之前,首先用 Webpack 將其 bundle 一次,然后在進(jìn)行 ESM 轉(zhuǎn)化。

          2. 通過動態(tài)白名單的方式,針對有共享 Context 的 NPM 包,不再打包所有源碼

          3. 其他解決方案...

          在漫長的踩坑與實踐中,我們內(nèi)部已經(jīng)基本實現(xiàn)了 NPM 包轉(zhuǎn)化 ESM 的分發(fā)服務(wù)(相比較市面上的分發(fā)服務(wù),該服務(wù)將轉(zhuǎn)化過程中遇到的問題進(jìn)一步抽象,實現(xiàn)了一層修復(fù)層,可以支持動態(tài)修復(fù))。

          下一代開發(fā)工具

          引用:漸進(jìn)式 Unbundled 開發(fā)工具探索之路

          前幾期我們已經(jīng)有同學(xué)介紹了如何開發(fā)一個 unbudnled 開發(fā)工具;在這里,「下一代」開發(fā)工具指的就是「unbundle」開發(fā)工具,下面要講的,就是圍繞「unbundle」這個詞。

          原理

          目前市面上流行的 unbundle 開發(fā)工具,比如 Vite,Snowpack,它們的底層核心架構(gòu)基本都是一致的,即將源碼與第三方依賴分開單獨做處理。

          在 dev server 啟動前,開發(fā)工具首先會遍歷源碼目錄,解析每個源碼文件 AST 中所有的 ImportDeclaration,拿到所有的第三方依賴路徑;然后將解析出的第三方依賴路徑作為 entryPoints 傳入傳統(tǒng)的構(gòu)建工具(Webpack,Rollup,Esbuild 等),打出一個多入口的另類 node_modules,在這個 node_modules 中,除了傳入的 entryPoints 繼續(xù)作為目標(biāo)文件存在外,其他的公共依賴部分都會被打成一個大的 chunks。

          [原始node_modules]

          [bundle后的node_modules]

          在 node_modules 處理完之后,接下來工具對源碼不會做任何處理,直接啟動 dev server,通常在 unbundle 開發(fā)工具中,默認(rèn)的首頁模板通常會包含下面這樣的代碼

          <script type="module" src="/index.js" />

          這樣,在用戶訪問首頁時,已經(jīng)實現(xiàn)了 ECMAScript Module 機(jī)制的瀏覽器會自動去請求 /index.js 文件,請求會被 dev server 做攔截,同時代理到源代碼中的 src/index.js 文件上。

          優(yōu)勢

          基于瀏覽器的 ESModule 加載機(jī)制,開發(fā)工具可以不用在每次啟動 dev server 時都去打包源代碼,基于這個思路,將第三方依賴和源代碼區(qū)分開,對第三方依賴單獨打包,而且由于第三方依賴是持久不變的,可以一次打包,次次使用(不新增新的依賴的情況下)。

          在這種架構(gòu)下,當(dāng)?shù)谌揭蕾囈呀?jīng)被預(yù)處理之后的情況下,理論上每次啟動 dev server 的時間可以達(dá)到秒級,對于傳統(tǒng)的構(gòu)建工具(Webpack,Rollup),開發(fā)服務(wù)器的啟動速度可以說是提升了2個數(shù)量級。

          思考

          與分發(fā)服務(wù)結(jié)合,不安裝依賴,快速開發(fā)

          試想一下,在 Snowpack / Vite 的基礎(chǔ)之上。我如果直接在源代碼里面引用一個沒有安裝在本地的依賴,然后 dev server 直接連接到 ESM 分發(fā)服務(wù),直接使用線上的包,同時檢測一下這個依賴的版本,自動更新到 package.json 中,并在后臺自動運行 install 進(jìn)程。

          在這個過程中,我沒有安裝新的依賴,但是可以直接在源代碼中使用,所見即所得,無需等待。同時在開發(fā)過程中,這個依賴也會經(jīng)由開發(fā)工具自動檢測并安裝到本地,在后續(xù) dev server 重啟的過程中會自動同步最新的本地依賴狀況。

          快速安裝依賴

          上一點說到,可以通過將 ESM 包分發(fā)服務(wù)與下一代開發(fā)工具結(jié)合,來實現(xiàn)本地開發(fā)體驗的巨大飛躍。更激進(jìn)一點,能不能通過 ESM 包的服務(wù)直接干掉 node_modules,或者說,換一個更精簡,更快就能安裝下來的 node_modules 呢?答案當(dāng)然是肯定的。

          通過分析 Vite 和 Snowpack 的源碼,可以發(fā)現(xiàn),這一類開發(fā)工具底層處理 node_modules 的方案,都是通過 Rollup / Esbuild,傳入 entryPoints 的方式來對 node_moduels 進(jìn)行預(yù)處理,從而構(gòu)建出一個全 ESM 化的 node_modules。

          那么我們可以直接在這一步的基礎(chǔ)之上,通過開發(fā) Rollup / Esbuild 插件,將讀取本地文件的過程全部代理到 ESM 包的分發(fā)服務(wù)上去。而由于 ESM 包的分發(fā)服務(wù)對每個包的處理是將包的源碼進(jìn)行打包,因此在文件數(shù)量上會呈現(xiàn)數(shù)十倍的下降;而打包結(jié)果會永久存儲到CDN上,等于一次安裝,永久使用,相較于本地npm安裝依賴時每次都需要下載依賴的整個 zip 包,網(wǎng)絡(luò) I/O 的耗時也會呈現(xiàn)數(shù)倍的下降。

          基于這樣一種思路實現(xiàn)的依賴安裝工具,不僅可以完整還原 node_moduels 的目錄結(jié)構(gòu),而且安裝速度相較于 yarn/npm/pnpm ,也會有數(shù)倍的提升,尤其是在有鎖文件的情況下,安裝速度提升十倍也不是不可能。

          [沒有鎖文件的情況下,通過 yarn 安裝依賴的速度]

          [沒有鎖文件的情況下,通過上述方案安裝依賴的速度]

          [有鎖文件的情況下,通過通過 yarn 安裝依賴的速度]

          [有鎖文件的情況下,通過上述方案安裝依賴的速度]

          目前,新一代依賴管理工具和新一代開發(fā)工具的工作還處于初期,整個工程還有巨大的優(yōu)化空間,包括安裝速度的進(jìn)一步提升,對本地緩存的進(jìn)一步利用,對 monorepo 的支持等...

          后續(xù)的進(jìn)展我們會持續(xù)與大家進(jìn)行分享;當(dāng)然,如果屏幕前的你對這些工作有興趣,歡迎掃描下方的二維碼加入我們一起建設(shè)。

          參考資料

          [1]

          https://esm.sh/: https://esm.sh/

          [2]

          https://cdn.skypack.dev/: https://cdn.skypack.dev/

          [3]

          https://jspm.org/: https://jspm.org/

          [4]

          https://cdn.skypack.dev/-/[email protected]/dist=es2020,mode=imports/optimized/protobufjs.js: https://cdn.skypack.dev/-/[email protected]/dist=es2020,mode=imports/optimized/protobufjs.js

          - END -



          內(nèi)推社群


          我組建了一個氛圍特別好的騰訊內(nèi)推社群,如果你對加入騰訊感興趣的話(后續(xù)有計劃也可以),我們可以一起進(jìn)行面試相關(guān)的答疑、聊聊面試的故事、并且在你準(zhǔn)備好的時候隨時幫你內(nèi)推。下方加 winty 好友回復(fù)「面試」即可。





          往期推薦


          大廠面試過程復(fù)盤(微信/阿里/頭條,附答案篇)
          面試題:說說事件循環(huán)機(jī)制(滿分答案來了)
          專心工作只想搞錢的前端女程序員的2020


          瀏覽 61
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲电影在线2 | 国产字幕中文 | 色吊丝一区二区 | 成人狼友网址 | 后入极品在线 |