最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 浅析Vite2.0-依赖预打包

    正文概述 掘金(The_fury)   2021-03-08   694

    浅析Vite2.0-依赖预打包

    开始

    最近在做业务的时候,了解到了一个叫imove开源项目,比较适合我现在做的业务 ,便了解了一下,发现它也借鉴了Vite的思想:即利用浏览器支持ESM 模块的特点,让我们的import/export 代码直接在浏览器中跑起来。结合之前社区的讨论,同时也让我对Vite有了兴趣,遂对它的代码进行了一些研究。 如果你对Vite还没有大概的了解,可以先看看这篇中文文档:关于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篇技术文档。也慢慢开始有了一些关于写技术文档的一些思考,这也算是一些’副作用’吧。 你如果有什么疑问或者建议都欢迎在下方留言。


    起源地下载网 » 浅析Vite2.0-依赖预打包

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元