<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

          共 8665字,需瀏覽 18分鐘

           ·

          2021-09-27 23:13

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

          ESM包的分發(fā)

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

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

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

          為什么需要分發(fā)

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

          原理

          將一個(gè) NPM 包轉(zhuǎn)化為一個(gè)支持 ESM 規(guī)范的包,需要做的其實(shí)就是針對模塊語法進(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)化,是屬于語法的升級,升級過程中勢必會(huì)遇到很多語法兼容問題。

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

          眾所周知,Commonjs 模塊語法是動(dòng)態(tài)執(zhí)行的,即 require() 執(zhí)行之后拿到的模塊有哪些屬性,只有代碼真正執(zhí)行到 require 函數(shù)調(diào)用的那一行時(shí)才能知道,而 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í)行前就會(huì)通過靜態(tài)語法檢測,解析出子模塊的具名導(dǎo)出變量和默認(rèn)導(dǎo)出變量,然后會(huì)根據(jù)導(dǎo)入語法,在代碼真正執(zhí)行前先進(jìn)行一次校驗(yàn),如果引入了錯(cuò)誤的變量,會(huì)直接拋出錯(cuò)誤;而 CJS 模塊語法不會(huì)預(yù)先進(jìn)行語法檢測,而是運(yùn)行源代碼,運(yùn)行到 require 函數(shù)被調(diào)用時(shí)才會(huì)去處理子模塊的導(dǎo)出。而 CJS 和 ESM 的模塊導(dǎo)出機(jī)制也是不同的。在 CJS 中, module.exports 和 exports 對象其實(shí)是同一個(gè)引用,即,不論用戶用什么語法來導(dǎo)出屬性,最終導(dǎo)出的屬性全是掛在了一個(gè)對象的引用上,而其他模塊引用這個(gè)模塊時(shí),require 執(zhí)行之后拿到的其實(shí)就是這個(gè)引用對象。而在 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)出方式不能混用,若錯(cuò)誤使用,瀏覽器底層會(huì)直接拋出錯(cuò)誤,而在 CJS 中,由于導(dǎo)出的值一直是一個(gè)對象,所以通過 require 引入模塊時(shí),是不會(huì)拋出語法錯(cuò)誤的(除非模塊不存在)。而目前生態(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)化之后,會(huì)直接被編譯成只有一個(gè)默認(rèn)導(dǎo)出的模塊,通過這樣的轉(zhuǎn)化,在使用 React 時(shí),會(huì)與我們常規(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)引入,動(dòng)態(tài)引入語法在 ESM 中沒有與 CMD 對等的語法轉(zhuǎn)化

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

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

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

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

          通過以上方案轉(zhuǎn)化來的動(dòng)態(tài)引入,原語義是希望在使用的時(shí)候再引用,而轉(zhuǎn)化之后的 ESM 語法將其變?yōu)榱耍纫茫偈褂茫赡軐?dǎo)致 'Module' 模塊內(nèi)部實(shí)例化未完成的情況下就已經(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ò)通常會(huì)使用 Rollup 等工具,將依賴包的源代碼全部打包到一起,最后提供一個(gè) ESM 單文件,這樣可以顯著的減少網(wǎng)絡(luò)請求量(比如,請求 antd 包,如果不打包源碼,可能需要遞歸引入 antd/es/** 下的所有文件,這樣網(wǎng)絡(luò)請求數(shù)量可能達(dá)到數(shù)百級別)。

          import * as Module from 'antd.mjs'

          同樣的,如果引用 ESM 包的不同路徑文件時(shí),比如 [email protected]/es/index.js[email protected]/esm/components/core/update , 若這兩個(gè)路徑的 ESM 單文件中引用了同樣的 Context (比如 React Context),那么最終每個(gè)路徑的文件里面都會(huì)包含一份 Context 的代碼,這就導(dǎo)致最終的運(yùn)行結(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()

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

          1. 其他問題...

          解決方案

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

          2. 通過在 Node.js 中模擬一個(gè) Browser Context,在 Context 中嘗試調(diào)用 require('Module'),通過 CJS 加載方式拿到模塊的導(dǎo)出對象,將其手動(dòng)編譯成具名導(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. 通過動(dòng)態(tài)白名單的方式,針對有動(dòng)態(tài)引入的 NPM 包,在轉(zhuǎn)化成 ESM 包之前,首先用 Webpack 將其 bundle 一次,然后在進(jìn)行 ESM 轉(zhuǎn)化。

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

          3. 其他解決方案...

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

          下一代開發(fā)工具

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

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

          原理

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

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

          [原始node_modules]

          [bundle后的node_modules]

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

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

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

          優(yōu)勢

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

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

          思考

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

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

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

          快速安裝依賴

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

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

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

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

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

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

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

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

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

          后續(xù)的進(jìn)展我們會(huì)持續(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 -




          • 后臺回復(fù):typescript,獲取我寫的 typescript 系列文章,絕對精品
          • 后臺回復(fù):電子書,自動(dòng)獲取我為大家整理的大量經(jīng)典電子書,省去你篩選以及下載的時(shí)間
          • 后臺回復(fù):不一樣的前端,自動(dòng)獲取精選優(yōu)質(zhì)前端文章。
          • 后臺回復(fù):算法,自動(dòng)獲取精選算法文章。另外也可關(guān)注我的另外一個(gè)專注算法的公眾號力扣加加
          • 后臺回復(fù):每日一薦,自動(dòng)獲取我為大家總結(jié)的每日一薦月刊,內(nèi)含精品文章,實(shí)用技巧,高效工具等等



          瀏覽 67
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  精品无码产区一区二 | 亚洲激情无码视频 | 色婷婷国产视频 | 在线亚洲欧美 | 色五月婷婷色 |