最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vite介绍和原理解析

    正文概述 掘金(头号前端)   2021-08-04   444

    Vite号称是 下一代的前端开发和构建工具 ,目前已经在前端社区里逐步开始流行起来了。它采用了全新的unbundle思想来提升整体的前端开发体验。比起传统的webpack构建,在性能速度上都有了质的提高。 那么接下来这篇文章,将主要介绍其使用方法和工作原理。

    是什么

    Vite名字来源于法语, 意思为rapid,quickly。正好反映了其核心卖点—— "快速" 。在整体功能上实现了类似于预配置的webpack加dev server的功能, 用于提高前端项目的整体构建速度。 根据测试,服务器启动速度和HMR基本上都可以达到毫秒级别。

    使用方法

    vite的使用方式十分简单,目前官方提供了脚手架来快速启动一个新项目:

    npm init @vitejs/app
    
    // yarn
    yarn create @vitejs/app
    

    接着就会进入交互式模式,让你选择对应的模板,输入项目名等操作。 如果需要手动指定模板和项目名,可以使用如下命令:

    npm init @vitejs/app my-vite-demo --template react
    

    这里指定的所有相关项目模板都可以在 github.com/vitejs/awes… 仓库中找到。 项目启动后,就可以直接使用如下命令进行启动和预览了

    # 安装依赖
    yarn install
    
    # 开发环境下使用
    yarn dev
    
    # 打包
    yarn run build
    # 用来预览打包后的效果
    yarn run serve
    

    插件机制

    vite主要使用插件进行扩展功能,可以看到上述最简单的初始化项目启动后,在其配置文件 vite.config.ts 文件下,有如下代码:

    import { defineConfig } from 'vite'
    import reactRefresh from '@vitejs/plugin-react-refresh'
    
    // [https:](https://vitejs.dev/config/)[//vitejs.dev/config/](https://vitejs.dev/config/)
    export default defineConfig({
      plugins: [reactRefresh()]
    })
    

    可以看到这里引用了一个名为 reactRefresh 的插件, 这个插件可以在修改react组件的时候,不丢失其状态。 同样的,如果有需要实现其他额外的功能,都可以借助vite的插件机制进行扩展。这些第三方插件模块可以通过 github.com/vitejs/awes… 这个仓库找到。 同时,由于vite插件扩展了rollup的接口,所以要实现一个自己的vite插件跟写rollup插件是类似的。此处,可以参考 插件 API | Vite 官方中文文档 。

    工作原理

    上面介绍了这么多,那么Vite是如何实现超快速的开发体验的呢? github.com/vitejs/vite… 我们都知道,传统打包构建工具,在服务器启动之前,需要从入口文件完整解析构建整个应用。因此,有大量的时间都花在了依赖生成,构建编译上。

    Vite介绍和原理解析

    而vite主要遵循的是使用ESM(Es modules模块)的规范来执行代码,由于现代浏览器基本上都支持了ESM规范,所以在开发阶段并不需要将代码打包编译成es5模块即可在浏览器上运行。我们只需要从入口文件出发, 在遇到对应的 import 语句时,将对应的模块加载到浏览器中就可以了。因此,这种不需要打包的特性,也是vite的速度能够如此快速的原因。

    Vite介绍和原理解析

    同时ts/jsx等文件的转译工作也会借助了esbuild来提升速度。 Vite在内部实现上,会启动一个dev server, 并接受独立模块的HTTP请求,并让浏览器自身去解析和处理模块加载。 下面以官方提供的demo为例,可以看到运行后,在访问对应页面的时候,不是加载一整个的bundle.js文件,而是按模块去加载。

    Vite介绍和原理解析

    从代码实现上,在允许 yarn dev 命令后,Vite就会启动一个dev server,然后加载各种中间件,进而监听对应的前端访问请求。 github.com/vitejs/vite…

    const { createServer } = await import('./server')
    try {
      const server = await createServer({
        root,
        base: options.base,
        mode: options.mode,
        configFile: options.config,
        logLevel: options.logLevel,
        clearScreen: options.clearScreen,
        server: cleanOptions(options) as ServerOptions
      })
      await server.listen()
    } catch (e) {
      createLogger(options.logLevel).error(
        chalk.red(`error when starting dev server:\n${e.stack}`)
      )
      process.exit(1)
    }
    

    同时,会在开发环境中注入Vite自身的client客户端代码,用于监听HMR等处理。 github.com/vitejs/vite…

    裸模块重写

    由于目前ESM不支持类似 import vue from "vue" 这样的裸模块加载(import maps 提案 github.com/WICG/import… 可解决这个问题,但还未实现),所以需要对模块加载地址进行重写操作。将其转换成类似于 import vue from "/ @modules/vue" 这种形式。 实现原理上主要通过 es-module-lexermagic-string 两个包进行替换,比起AST语义解析和转换,在性能上更有优势。 下面介绍一下这两个包:

    Es-module-lexer

    github.com/guybedford/… 虽然js代码的词法分析通常都使用babel, acorn等工具,但是针对ESM文件来说,使用es-module-lexer库在性能上能够有很大的提升,其压缩后的体积只有4kb,而且根据官方给出的例子720kb的Angular1库经过acorn解析要超过100ms,而使用es-module-lexer库只需要5ms, 在性能上提升了将近20倍。

    Magic-string

    github.com/rich-harris… vite中使用了大量这个库做一些字符串的替换工作,从而避免操作AST。具体代码可以参考 github.com/vitejs/vite… 整体思路大概类似于下面代码:

    import { init, parse as parseImports, ImportSpecifier } from 'es-module-lexer'
    
    // 借助es-module-lexer来分析import语句
    imports = parseImports(source)[0]
    
    // 接着在依赖分析及路径重写过程中利用magic-string来替换源码。
    let s: MagicString | undefined
    const str = () => s || (s = new MagicString(source))
    
    // 省略部分代码
    for (let index = 0; index < imports.length; index++) {
            const {
              s: start,
              e: end,
              ss: expStart,
              se: expEnd,
              d: dynamicIndex,
              n: specifier
            } = imports[index]
    
    // 省略部分代码
    
    // 解析代码
     const { imports, importsString, exp, endIndex, base, pattern } =
                  await transformImportGlob(
                    source,
                    start,
                    importer,
                    index,
                    root,
                    normalizeUrl
                  )
                str().prepend(importsString)
                str().overwrite(expStart, endIndex, exp)
                imports.forEach((url) => importedUrls.add(url.replace(base, '/')))
                if (!(importerModule.file! in server._globImporters)) {
                  server._globImporters[importerModule.file!] = {
                    module: importerModule,
                    importGlobs: []
                  }
                }
                server._globImporters[importerModule.file!].importGlobs.push({
                  base,
                  pattern
                })
    }
    
    // 最终返回处理过的代码 
    if (s) {
      return s.toString()
    } else {
      return source
    }       
    

    自定义区块处理

    这个功能是通过在模块后面链接 ?type= 的参数来区分不同区块。然后针对每个区块单独进行处理。

    Vite介绍和原理解析

    根据不同的区块类型,在transform的时候会使用不同的插件进行编译。 下面以json文件为例,在处理 xxx.json 为结尾的文件的时候,首先json插件会匹配模块的id名是否是json。接着再进行转译工作。

    
    // Custom json filter for vite
    const jsonExtRE = /\.json($|\?)(?!commonjs-proxy)/
    
    export function jsonPlugin(
      options: JsonOptions = {},
      isBuild: boolean
    ): Plugin {
      return {
        name: 'vite:json',
    
        transform(json, id) {
          if (!jsonExtRE.test(id)) return null
          if (SPECIAL_QUERY_RE.test(id)) return null
    
          try {
            if (options.stringify) {
              if (isBuild) {
                return {
                  code: `export default JSON.parse(${JSON.stringify(
                    JSON.stringify(JSON.parse(json))
                  )})`,
                  map: { mappings: '' }
                }
              } else {
                return `export default JSON.parse(${JSON.stringify(json)})`
              }
            }
    
            const parsed = JSON.parse(json)
            return {
              code: dataToEsm(parsed, {
                preferConst: true,
                namedExports: options.namedExports
              }),
              map: { mappings: '' }
            }
          } catch (e) {
            const errorMessageList = /[\d]+/.exec(e.message)
            const position = errorMessageList && parseInt(errorMessageList[0], 10)
            const msg = position
              ? `, invalid JSON syntax found at line ${position}`
              : `.`
            this.error(`Failed to parse JSON file` + msg, e.idx)
          }
        }
      }
    }
    

    HMR

    热更新是前端开发体验中很重要的一环,那么Vite中主要依赖以下几个步骤来实现HMR的功能:

    1. 在重写模块地址的时候,记录模块依赖链 importMaps 。 这样在后续更新的时候,可以知道哪些文件需要被热更新。

    Vite介绍和原理解析

    1. 代码中可以使用 import.meta.hot 接口来标记"HMR Boundary"。

    Vite介绍和原理解析

    1. 接着,当文件更新的时候,会沿着之前记录下 imoprtMaps 链式结构找到对应的"HMR Boundary", 再从此处重新加载对应更新的模块。

    Vite介绍和原理解析

    Vite介绍和原理解析

    1. 如果没有遇到对应的boundary, 则整个应用重新刷新。

    Vite介绍和原理解析

    使用方法如下:

    import foo from './foo.js'
    
    foo()
    
    if (import.meta.hot) {
        import.meta.hot.accept('./foo.js', (newFoo) => {
            newFoo.foo()
        })
    }
    

    下面将以具体代码进行介绍其原理。 客户端逻辑: github.com/vitejs/vite…

    // record for HMR import chain analysis
    // make sure to normalize away base
    importedUrls.add(url.replace(base, '/'))
    
    
    if (hasHMR && !ssr) {
      debugHmr(
        `${
          isSelfAccepting
            ? `[self-accepts]`
            : acceptedUrls.size
            ? `[accepts-deps]`
            : `[detected api usage]`
        } ${prettyImporter}`
      )
      // 在用户业务代码中注入Vite客户端代码
      str().prepend(
        `import { createHotContext as __vite__createHotContext } from "${clientPublicPath}";` +
          `import.meta.hot = __vite__createHotContext(${JSON.stringify(
            importerModule.url
          )});`
      )
    }
    

    github.com/vitejs/vite…

    case 'update':
         notifyListeners('vite:beforeUpdate', payload)
          // 发生错误的时候,重新加载整个页面
          if (isFirstUpdate && hasErrorOverlay()) {
            window.location.reload()
            return
          } else {
            clearErrorOverlay()
            isFirstUpdate = false
          }
          
          payload.updates.forEach((update) => {
            if (update.type === 'js-update') {
              // js更新逻辑, 会进入一个缓存队列,批量更新,从而保证更新顺序
              queueUpdate(fetchUpdate(update))
            } else {
              // css更新逻辑, 检测到更新的时候,直接替换对应模块的链接,重新发起请求
              let { path, timestamp } = update
              path = path.replace(/\?.*/, '')
    
              const el = (
                [].slice.call(
                  document.querySelectorAll(`link`)
                ) as HTMLLinkElement[]
              ).find((e) => e.href.includes(path))
              if (el) {
                const newPath = `${path}${
                  path.includes('?') ? '&' : '?'
                }t=${timestamp}`
                el.href = new URL(newPath, el.href).href
              }
              console.log(`[vite] css hot updated: ${path}`)
            }
          })
          break
    break
    

    服务端处理HMR模块更新逻辑: github.com/vitejs/vite…

    export async function handleHMRUpdate(
      file: string,
      server: ViteDevServer
    ): Promise<any> {
      const { ws, config, moduleGraph } = server
      const shortFile = getShortName(file, config.root)
    
      const isConfig = file === config.configFile
      const isConfigDependency = config.configFileDependencies.some(
        (name) => file === path.resolve(name)
      )
      const isEnv = config.inlineConfig.envFile !== false && file.endsWith('.env')
      if (isConfig || isConfigDependency || isEnv) {
        // 重启server
        await restartServer(server)
        return
      }
    
      // (dev only) the client itself cannot be hot updated.
      if (file.startsWith(normalizedClientDir)) {
        ws.send({
          type: 'full-reload',
          path: '*'
        })
        return
      }
    
      const mods = moduleGraph.getModulesByFile(file)
    
      // check if any plugin wants to perform custom HMR handling
      const timestamp = Date.now()
      const hmrContext: HmrContext = {
        file,
        timestamp,
        modules: mods ? [...mods] : [],
        read: () => readModifiedFile(file),
        server
      }
    
      for (const plugin of config.plugins) {
        if (plugin.handleHotUpdate) {
          const filteredModules = await plugin.handleHotUpdate(hmrContext)
          if (filteredModules) {
            hmrContext.modules = filteredModules
          }
        }
      }
    
      if (!hmrContext.modules.length) {
        // html file cannot be hot updated
        if (file.endsWith('.html')) {
          [config.logger.info](http://config.logger.info/)(chalk.green(`page reload `) + chalk.dim(shortFile), {
            clear: true,
            timestamp: true
          })
          ws.send({
            type: 'full-reload',
            path: config.server.middlewareMode
              ? '*'
              : '/' + normalizePath(path.relative(config.root, file))
          })
        } else {
          // loaded but not in the module graph, probably not js
          debugHmr(`[no modules matched] ${chalk.dim(shortFile)}`)
        }
        return
      }
    
      updateModules(shortFile, hmrContext.modules, timestamp, server)
    }
    
    function updateModules(
      file: string,
      modules: ModuleNode[],
      timestamp: number,
      { config, ws }: ViteDevServer
    ) {
      const updates: Update[] = []
      const invalidatedModules = new Set<ModuleNode>()
      let needFullReload = false
    
      for (const mod of modules) {
        invalidate(mod, timestamp, invalidatedModules)
        if (needFullReload) {
          continue
        }
    
        const boundaries = new Set<{
          boundary: ModuleNode
          acceptedVia: ModuleNode
        }>()
        
        // 向上传递更新,直到遇到边界
        const hasDeadEnd = propagateUpdate(mod, timestamp, boundaries)
        if (hasDeadEnd) {
          needFullReload = true
          continue
        }
    
        updates.push(
          ...[...boundaries].map(({ boundary, acceptedVia }) => ({
            type: `${boundary.type}-update` as Update['type'],
            timestamp,
            path: boundary.url,
            acceptedPath: acceptedVia.url
          }))
        )
      }
    
      if (needFullReload) {
        // 重刷页面
      } else {
       // 相ws客户端发送更新事件, Websocket 监听模块更新, 并且做对应的处理。
        ws.send({
          type: 'update',
          updates
        })
      }
    }
    

    Vite介绍和原理解析

    优化策略

    由于vite打包是让浏览器一个个模块去加载的,因此,就很容易存在http请求的瀑布流问题(浏览器并发一次最多6个请求)。此次,vite内部为了解决这个问题,主要采取了3个方案。

    1. 预打包,确保每个依赖只对应一个请求/文件。比如lodash。此处可以参考 github.com/vitejs/vite…

    2. 代码分割code split。可以借助rollup内置的 manualChunks 来实现。

    3. Etag 304状态码,让浏览器在重复加载的时候直接使用浏览器缓存。

    github.com/vitejs/vite…

    // check if we can return 304 early
    const ifNoneMatch = req.headers['if-none-match']
    if (
      ifNoneMatch &&
      (await moduleGraph.getModuleByUrl(url))?.transformResult?.etag ===
        ifNoneMatch
    ) {
      isDebug && debugCache(`[304] ${prettifyUrl(url, root)}`)
      res.statusCode = 304
      return res.end()
    }
    

    esbuild的使用

    github.com/vitejs/vite… 利用esbuild来转换ts/jsx文件,从而更快地提升编译速度。

    export async function transformWithEsbuild(
      code: string,
      filename: string,
      options?: TransformOptions,
      inMap?: object
    ): Promise<ESBuildTransformResult> {
      // if the id ends with a valid ext, use it (e.g. vue blocks)
      // otherwise, cleanup the query before checking the ext
      const ext = path.extname(
        /\.\w+$/.test(filename) ? filename : cleanUrl(filename)
      )
    
      let loader = ext.slice(1)
      if (loader === 'cjs' || loader === 'mjs') {
        loader = 'js'
      }
    
      const resolvedOptions = {
        loader: loader as Loader,
        sourcemap: true,
        // ensure source file name contains full query
        sourcefile: filename,
        ...options
      } as ESBuildOptions
    
      delete resolvedOptions.include
      delete resolvedOptions.exclude
      delete resolvedOptions.jsxInject
    
      try {
        const result = await transform(code, resolvedOptions)
        if (inMap) {
          const nextMap = JSON.parse(result.map)
          nextMap.sourcesContent = []
          return {
            ...result,
            map: combineSourcemaps(filename, [
              nextMap as RawSourceMap,
              inMap as RawSourceMap
            ]) as SourceMap
          }
        } else {
          return {
            ...result,
            map: JSON.parse(result.map)
          }
        }
      } catch (e) {
        debug(`esbuild error with options used: `, resolvedOptions)
        // patch error information
        if (e.errors) {
          e.frame = ''
          e.errors.forEach((m: Message) => {
            e.frame += `\n` + prettifyMessage(m, code)
          })
          e.loc = e.errors[0].location
        }
        throw e
      }
    }
    

    总结

    总体来说,Vite在前端构建工具领域上开辟了一条和webpack完全不同的道路,很好地解决了前端开发阶段构建速度慢的问题。预计将会使前端开发体验上更上一层楼。同时,vite.js的源码也在不停迭代过程中,如果有想要更加了解其具体的实现细节,还是希望能够亲自去阅读其源码。本文主要希望能够起到抛砖引玉的作用。

    参考文档

    cn.vitejs.dev/guide/#over…

    www.youtube.com/watch?v=xXr…

    www.youtube.com/watch?v=fgw…


    起源地下载网 » Vite介绍和原理解析

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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