浅析Vite2.0-依赖预打包

开始

最近在做业务的时候,理解到了一个叫imove开源我的项目,比拟适宜我当初做的业务 ,便理解了一下,发现它也借鉴了Vite的思维:即利用浏览器反对ESM 模块的特点,让咱们的import/export 代码间接在浏览器中跑起来。联合之前社区的探讨,同时也让我对Vite有了趣味,遂对它的代码进行了一些钻研。
如果你对Vite还没有大略的理解,能够先看看这篇中文文档:对于Vite的一些介绍。
在我看来比拟重要的点是:

Vite 以 原生 ESM 形式服务源码。这实际上是让浏览器接管了打包程序的局部工作:Vite 只须要在浏览器申请源码时进行转换并按需提供源码。依据情景动静导入的代码,即只在以后屏幕上理论应用时才会被解决
同时我关注 到Vite 2.0 公布了 ,其中几个个性还是比拟有意思,接下来就剖析一下 更新的个性之一:基于 esbuild 的依赖预打包

依赖预打包的起因

对于这一点,Vite的文档上曾经说得比较清楚了
1.CommonJS 和 UMD 兼容性: 开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因而,Vite 必须先将作为 CommonJS 或 UMD 公布的依赖项转换为 ESM。
2.Vite 将有许多外部模块的 ESM 依赖关系转换为单个模块,以进步后续页面加载性能。

整体流程

首先 在应用vite创立的我的项目中,咱们能够看到有如下几个命令:

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

能够得悉,本地运行时启动的就是默认命令vite。
在vite我的项目中找到对应的cli.ts 代码(为了看起来更清晰,本文档中贴出来的代码相比原文件做了删减)

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(热更新) ,预打包 等等,咱们这次重点关注的是预打包。
来看看这一块的代码:

    // 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 会将预构建的依赖缓存到这个文件夹下,判断是否须要应用到缓存的条件,咱们前面随着代码深刻讲到。
预构建的流程在我看来,分为三个步骤

第一步 判断缓存是否生效

判断缓存是否生效的重要依据是 通过getDepHash这个办法生成的hash值,次要就是按程序查找 const lockfileFormats = [‘package-lock.json’, ‘yarn.lock’, ‘pnpm-lock.yaml’] 这三个文件,若有其中一个存在,则返回其文件内容。再通过 += 局部config值,生成文件的hash值。
简略来说,就是通过判断我的项目的依赖是否有改变,从而决定了缓存是否无效。
其次还有 browserHash,次要用于优化申请数量,防止太多的申请影响性能。

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代表是否须要转换为ESM

  // 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的时候拿到其所有的依赖,并在最初构建实现时,删除本次的构建产物。

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篇技术文档。也缓缓开始有了一些对于写技术文档的一些思考,这也算是一些’副作用’吧。
你如果有什么疑难或者倡议都欢送在下方留言。