淺析Vite2.0-依賴預打包

淺析Vite2.0-依賴預打包

開始

最近在作業務的時候,瞭解到了一個叫imove開源項目,比較適合我如今作的業務 ,便了解了一下,發現它也借鑑了Vite的思想:即利用瀏覽器支持ESM 模塊的特色,讓咱們的import/export 代碼直接在瀏覽器中跑起來。結合以前社區的討論,同時也讓我對Vite有了興趣,遂對它的代碼進行了一些研究。
若是你對Vite尚未大概的瞭解,能夠先看看這篇中文文檔:關於Vite的一些介紹
在我看來比較重要的點是:html

Vite 以 原生 ESM 方式服務源碼。這其實是讓瀏覽器接管了打包程序的部分工做:Vite 只須要在瀏覽器請求源碼時進行轉換並按需提供源碼。根據情景動態導入的代碼,即只在當前屏幕上實際使用時纔會被處理
同時我關注 到 Vite 2.0 發佈了 ,其中幾個特性仍是比較有意思,接下來就分析一下 更新的特性之一: 基於 esbuild 的依賴預打包

依賴預打包的緣由

關於這一點,Vite的文檔上已經說得比較清楚了
1.CommonJS 和 UMD 兼容性: 開發階段中,Vite 的開發服務器將全部代碼視爲原生 ES 模塊。所以,Vite 必須先將做爲 CommonJS 或 UMD 發佈的依賴項轉換爲 ESM。
2.Vite 將有許多內部模塊的 ESM 依賴關係轉換爲單個模塊,以提升後續頁面加載性能。node

總體流程

首先 在使用vite建立的項目中,咱們能夠看到有以下幾個命令:react

"scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "serve": "vite preview"
  },

能夠得知,本地運行時啓動的就是默認命令vite。
在vite項目中找到對應的cli.ts 代碼(爲了看起來更清晰,本文檔中貼出來的代碼相比原文件作了刪減)ios

cli
  .command('[root]') // default command
  .alias('serve')
  .action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {
    const { createServer } = await import('./server')
    const server = await createServer({ root })
    await server.listen()

咱們能夠看到vite本地運行的時候,簡單來講,就是在建立服務。
固然更具體的來說,createServer這個方法中作的事包括: 初始化配置,HMR(熱更新) ,預打包 等等,咱們此次重點關注的是預打包。
來看看這一塊的代碼:git

// overwrite listen to run optimizer before server start
    const listen = httpServer.listen.bind(httpServer)
    httpServer.listen = (async (port: number, ...args: any[]) => {
      await container.buildStart({}); // REVIEW 簡單測試了下 爲空函數 貌似沒什麼卵用?
      await runOptimize() 
      return listen(port, ...args)
    }) as any
    const runOptimize = async () => {
        if (config.optimizeCacheDir) server._optimizeDepsMetadata = await optimizeDeps(config);
    }
  }

上面的代碼中咱們能夠了解到,具體的 預打包代碼的實現邏輯就是在 optimizeDeps 這個方法中。同時 config.optimizeCacheDir 默認爲node_modules/.vite,Vite 會將預構建的依賴緩存到這個文件夾下,判斷是否須要使用到緩存的條件,咱們後面隨着代碼深刻講到。
預構建的流程在我看來,分爲三個步驟github

第一步 判斷緩存是否失效

判斷緩存是否失效的重要依據是 經過getDepHash這個方法生成的hash值,主要就是按順序查找 const lockfileFormats = [‘package-lock.json’, ‘yarn.lock’, ‘pnpm-lock.yaml’] 這三個文件,如有其中一個存在,則返回其文件內容。再經過 += 部分config值,生成文件的hash值。
簡單來講,就是經過判斷項目的依賴是否有改動,從而決定了緩存是否有效。
其次還有 browserHash,主要用於優化請求數量,避免太多的請求影響性能。npm

function getDepHash(root: string, config: ResolvedConfig): string {
  let content = lookupFile(root, lockfileFormats) || '';
  // also take config into account
  // only a subset of config options that can affect dep optimization
  content += JSON.stringify(
    {
      mode: config.mode,
      root: config.root,
      resolve: config.resolve,
      assetsInclude: config.assetsInclude,
  )
  return createHash('sha256').update(content).digest('hex').substr(0, 8)
}

經過下面的代碼能夠看到,對依賴的緩存具體路徑都寫在optimized這個字段中,optimized 中的file,src分別表明緩存路徑和源文件路徑,needsInterop表明是否須要轉換爲ESMjson

// cacheDir 默認爲 node_modules/.vite
  const dataPath = path.join(cacheDir, '_metadata.json') 
  const mainHash = getDepHash(root, config)
// data即存入 _metadata.json的文件內容 主要包括下面三個字段
  const data: DepOptimizationMetadata = {
    hash: mainHash,  // mainHash 利用文件簽名以及部分config屬性是否改變,判斷是否須要從新打包
    browserHash: mainHash, // browserHash 主要用於優化請求數量,避免太多的請求影響性能
    optimized: {}  // 全部依賴項
        //eg: "optimized": {"axios": 
        //{"file": "/Users/guoyunxin/github/my-react-app/node_modules/.vite/axios.js",
      //"src": "/Users/guoyunxin/github/my-react-app/node_modules/axios/index.js",
      //"needsInterop": true }
 }
  // update browser hash
  data.browserHash = createHash('sha256')
    .update(data.hash + JSON.stringify(deps))
    .digest('hex')
    .substr(0, 8)

第二步 收集依賴模塊路徑

收集依賴模塊路徑的核心方法是 scanImports 其本質上仍是經過esbuildService.build方法 以index.html文件爲入口,構建出一個臨時文件夾。在build.onResolve的時候拿到其全部的依賴,並在最後構建完成時,刪除本次的構建產物。axios

export async function scanImports(
  config: ResolvedConfig
): Promise<{
  deps: Record<string, string>
  missing: Record<string, string>
}> {
  entries = await globEntries('**/*.html', config)
  const tempDir = path.join(config.optimizeCacheDir!, 'temp')
  const deps: Record<string, string> = {}
  const missing: Record<string, string> = {}
  const plugin = esbuildScanPlugin(config, container, deps, missing, entries)
  await Promise.all(
    entries.map((entry) =>
      esbuildService.build({
        entryPoints: [entry]
        })
    )
  )
  emptyDir(tempDir)
  fs.rmdirSync(tempDir) 
  return {
    deps, //依賴模塊路徑
     missing // missing爲 引入但不能成功解析的模塊
  }
}

最終獲得的數據結構爲瀏覽器

deps =  {
   react: ‘/Users/guoyunxin/github/my-react-app/node_modules/react/index.js’,
   ‘react-dom’: ‘/Users/guoyunxin/github/my-react-app/node_modules/react-dom/index.js’,
   axios: ‘/Users/guoyunxin/github/my-react-app/node_modules/axios/index.js’
   }

第三步 esbuild 打包模塊

最終打包的產物都是會在.vite/_esbuild.json 文件中
以react-dom爲例 經過 inputs中的文件打包構建出的產物爲 .vite/react-dom.js

"outputs":{
     "node_modules/.vite/react-dom.js": {
      "imports": [
        {
          "path": "node_modules/.vite/chunk.FM3E67PX.js",
          "kind": "import-statement"
        },
        {
          "path": "node_modules/.vite/chunk.2VCUNPV2.js",
          "kind": "import-statement"
        }
      ],
      "exports": [
        "default"
      ],
      "entryPoint": "dep:react-dom",
      "inputs": {
        "node_modules/scheduler/cjs/scheduler.development.js": {
          "bytesInOutput": 22414
        },
        "node_modules/scheduler/index.js": {
          "bytesInOutput": 189
        },
        "node_modules/scheduler/cjs/scheduler-tracing.development.js": {
          "bytesInOutput": 9238
        },
        "node_modules/scheduler/tracing.js": {
          "bytesInOutput": 195
        },
        "node_modules/react-dom/cjs/react-dom.development.js": {
          "bytesInOutput": 739631
        },
        "node_modules/react-dom/index.js": {
          "bytesInOutput": 205
        },
        "dep:react-dom": {
          "bytesInOutput": 45
        }
      },
      "bytes": 772434
    },

     }

如下爲具體打包實現流程

export async function optimizeDeps(
  config: ResolvedConfig,
  force = config.server.force,
  asCommand = false,
  newDeps?: Record<string, string> // missing imports encountered after server has started
): Promise<DepOptimizationMetadata | null> {
  
  const esbuildMetaPath = path.join(cacheDir, '_esbuild.json')
  await esbuildService.build({
    entryPoints: Object.keys(flatIdDeps), // 以收集到的依賴包爲入口 即 Object.keys(deps)
    metafile: esbuildMetaPath, // _esbuild.json中保存着構建的結果 output 
    plugins: [esbuildDepPlugin(flatIdDeps, flatIdToExports, config)]
  })

  const meta = JSON.parse(fs.readFileSync(esbuildMetaPath, 'utf-8'))
  
  for (const id in deps) {
    const entry = deps[id]
    data.optimized[id] = {
      file: normalizePath(path.resolve(cacheDir, flattenId(id) + '.js')),
      src: entry,
      needsInterop: needsInterop(id, idToExports[id], meta.outputs)
    }
  }
  writeFile(dataPath, JSON.stringify(data, null, 2)) // 
  return data
}

結尾

本次的文檔相交於以前本身研究的axios core-js sentry來講,複雜度會顯得稍高一些,並且現有能夠查到的關於vite的文檔基本都是1.x版本的,能夠借鑑參考的也很少。相比起以前研究的源碼,本次的顯得會難一些,因此也是花費了較多的時間來作,還好最後仍是寫出來了 233。以前研究源碼,都是興趣使然,選的方向都比較隨意。最近1v1事後,思考了一下技術體系的問題,因此後續應該會是以興趣+體系化的方式來選擇要研究的源碼。同時最近3個多月,更新了5篇技術文檔。也慢慢開始有了一些關於寫技術文檔的一些思考,這也算是一些’反作用’吧。你若是有什麼疑問或者建議都歡迎在下方留言。

相關文章
相關標籤/搜索