vite 原理解析

1,428 阅读6分钟

一、为什么使用vite

  1. 浏览器支持es module,关键变化:index.html中的入口文件导入方式:

script-type-module的导入方式.png

因此,在开发环境下,不再需要先打包好所有用到的资源,再运行项目。而是,边运行,边加载用到的资源。因此,速度相比于构建式的(bundler)的开发服务器(webpack)要更快。

二、初始化项目

# npm 6.x
npm init @vitejs/app my-vue-app --template vue

# npm 7+, 需要额外的双横线:
npm init @vitejs/app my-vue-app -- --template vue

# yarn
yarn create @vitejs/app my-vue-app --template vue

支持的模板预设包括:

  • vanilla
  • vue
  • vue-ts
  • react
  • react-ts
  • preact
  • preact-ts
  • lit-element
  • lit-element-ts
  • svelte
  • svelte-ts

三、vite框架流程

vite总共有四个命令行命令

1. 默认命令--开发(serve)

截屏2021-04-24 上午11.00.05.png

createServer(创建server主要的运行流程)

vite 启动服务器主要进行了四个流程:

  1. 启动了文件监听,和websocket服务器,来启动热更新
  2. 创建了ViteDevServer对象
  3. 内部中间件挂载
  4. 重写了httpServer的listen函数,在调用listen之前,执行了buildStart插件钩子,以及预构建优化
async function createServer(
  inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
  // 1、定义了watcher
  // 2、定义了ViteDevServer
  const server: ViteDevServer = {}
  // 3、内部中间件的use,举个例子如下
  // main transform middleware
  middlewares.use(transformMiddleware(server))
  // 4、重写了httpServer的listen方法,在listen执行之前,运行了container.buildStart({})和runOptimize
  if (!middlewareMode && httpServer) {
    // overwrite listen to run optimizer before server start
    const listen = httpServer.listen.bind(httpServer)
    httpServer.listen = (async (port: number, ...args: any[]) => {
      try {
        await container.buildStart({})
        await runOptimize()
      } catch (e) {
        httpServer.emit('error', e)
        return
      }
      return listen(port, ...args)
    }) as any

    httpServer.once('listening', () => {
      // update actual port since this may be different from initial value
      serverConfig.port = (httpServer.address() as AddressInfo).port
    })
  } else {
    await runOptimize()
  }

  return server
}

inlineConfig(用户配置参数解析)

用户在命令行输入的config配置以及vite.config.js里的配置

interface InlineConfig extends UserConfig {
  configFile?: string | false
}
interface UserConfig {
  root?: string
  base?: string
  publicDir?: string
  mode?: string
  define?: Record<string, any>
  plugins?: (PluginOption | PluginOption[])[]
  resolve?: ResolveOptions & { alias?: AliasOptions }
  css?: CSSOptions
  json?: JsonOptions
  esbuild?: ESBuildOptions | false
  assetsInclude?: string | RegExp | (string | RegExp)[]
  server?: ServerOptions
  build?: BuildOptions
  optimizeDeps?: DepOptimizationOptions
  ssr?: SSROptions
  logLevel?: LogLevel
  clearScreen?: boolean
  alias?: AliasOptions
  dedupe?: string[]
}

ViteDevServer(server的参数解析)

export interface ViteDevServer {
  /**
   * 解析后的vite配置
   */
  config: ResolvedConfig
  /**
   * 一个 connect 应用实例.
   * - 能够用来给开发服务器新增自定义中间件.
   * - 还可以用作自定义http服务器的处理函数
   *   或作为中间件用于任何 connect 风格的 Node.js 框架
   *
   * https://github.com/senchalabs/connect#use-middleware
   */
  middlewares: Connect.Server
  /**
   * @deprecated use `server.middlewares` instead
   */
  app: Connect.Server
  /**
   * 本机 node http 服务器实例
   * 在中间件模式下的值是null
   */
  httpServer: http.Server | null
  /**
   * chokidar watcher 实例
   * https://github.com/paulmillr/chokidar#api
   */
  watcher: FSWatcher
  /**
   * web socket 服务器,带有 `send(payload)` 方法
   */
  ws: WebSocketServer
  /**
   * Rollup插件容器,可以针对给定文件运行插件钩子
   */
  pluginContainer: PluginContainer
  /**
   * 模块图:跟踪导入(import)关系, url到文件的映射以及热更新状态
   * 
   */
  moduleGraph: ModuleGraph
  /**
   * Programmatically resolve, load and transform a URL and get the result
   * without going through the http request pipeline.
   */
  transformRequest(
    url: string,
    options?: TransformOptions
  ): Promise<TransformResult | null>
  /**
   * Apply vite built-in HTML transforms and any plugin HTML transforms.
   */
  transformIndexHtml(url: string, html: string): Promise<string>
  /**
   * Util for transforming a file with esbuild.
   * Can be useful for certain plugins.
   */
  transformWithEsbuild(
    code: string,
    filename: string,
    options?: EsbuildTransformOptions,
    inMap?: object
  ): Promise<ESBuildTransformResult>
  /**
   * Load a given URL as an instantiated module for SSR.
   */
  ssrLoadModule(url: string): Promise<Record<string, any>>
  /**
   * Fix ssr error stacktrace
   */
  ssrFixStacktrace(e: Error): void
  /**
   * Start the server.
   */
  listen(port?: number, isRestart?: boolean): Promise<ViteDevServer>
  /**
   * Stop the server.
   */
  close(): Promise<void>
  /**
   * @internal
   */
  _optimizeDepsMetadata: DepOptimizationMetadata | null
  /**
   * Deps that are externalized
   * @internal
   */
  _ssrExternals: string[] | null
  /**
   * @internal
   */
  _globImporters: Record<
    string,
    {
      base: string
      pattern: string
      module: ModuleNode
    }
  >
  /**
   * @internal
   */
  _isRunningOptimizer: boolean
  /**
   * @internal
   */
  _registerMissingImport: ((id: string, resolved: string) => void) | null
  /**
   * @internal
   */
  _pendingReload: Promise<void> | null
}

2. 构建命令--生产(build)

生产构建就是用的rollup的方法:

截屏2021-04-24 上午11.00.36.png

3. 优化命令--预构建(optimize)

预构建并不是每次都会执行,只有在node_modules的依赖或者相关用户配置发生改变时,才会在启动服务器时,去scanImports进行构建。不然就要自己执行预构建命令行去强制预构建。预构建流程大致如下:

截屏2021-04-24 上午11.00.55.png

中间件

vite的中间件主要是依赖于connect(Connect is an extensible HTTP server framework for node using "plugins" known as middleware.)为httpServer扩展中间件。其中,用到的中间件有: indexHtmlMiddlewaretransformMiddlewarebaseMiddlewareserveRawFsMiddlewareservePublicMiddlewareserveStaticMiddlewareproxyMiddlewaredecodeURIMiddlewareerrorMiddlewaretimeMiddleware

其中,比较核心的中间件有:

  • indexHtmlMiddleware:对index.html做处理
  • transformMiddleware:对匹配到的.map/\.((j|t)sx?|mjs|vue)($|\?)//(\?|&)import(?:&|$)/\\.(css|less|sass|scss|styl|stylus|postcss)($|\\?)/\?html-proxy&index=(\d+)\.js$/文件,通过vite内置插件或外部引用插件,对其做处理

构建优化

预构建依赖

原因

原声ES引入不支持下面这样的裸模块导入

import { someMethod } from 'my-dep'

vite将在服务的所有源文件中检测此类裸模块导入,并执行以下操作:

  1. 第三方依赖模块预构建,存放在/node_modules/.vite/文件夹下

vite预编译.png 2. npm依赖解析

截屏2021-04-10 上午11.13.51.png vite预编译2.png

Vite插件

插件钩子

Vite插件继承了rollup插件的功能,扩展了自己独有的功能。

  1. 通用钩子
  • 服务器启动时:options、buildStart
  • 在每个传入模块请求时:resolveId、load、transform
  • 服务器关闭时:buildEnd、closeBundle
  1. Vite独有钩子
  • 在配置被解析之前,修改配置:config
  • 在解析配置之后:configResolved
  • 内部中间件被安装之前:configureServer注入后置中间件
  • 转换index.html:transformIndexHtml
  • 执行自定义热更新处理:handleHotUpdate
  1. 构建时钩子
  • 获取构建参数:outputOptions
  • renderChunk
  • 生成bunddle文件:generateBunddle

插件类型

vite插件可分为用户配置的插件和vite内置的插件。

用户配置的插件,按插件执行顺序,可分为三类:

  • prePlugins
  • normalPlugins
  • postPlugins 插件具体执行顺序,看vite源码如下:
export async function resolvePlugins(
  config: ResolvedConfig,
  prePlugins: Plugin[],
  normalPlugins: Plugin[],
  postPlugins: Plugin[]
): Promise<Plugin[]> {
  const isBuild = config.command === 'build'

  const buildPlugins = isBuild
    ? (await import('../build')).resolveBuildPlugins(config)
    : { pre: [], post: [] }

  return [
    isBuild ? null : preAliasPlugin(),
    aliasPlugin({ entries: config.resolve.alias }),
    ...prePlugins,
    config.build.polyfillDynamicImport
      ? dynamicImportPolyfillPlugin(config)
      : null,
    resolvePlugin({
      ...config.resolve,
      root: config.root,
      isProduction: config.isProduction,
      isBuild,
      asSrc: true
    }),
    htmlInlineScriptProxyPlugin(),
    cssPlugin(config),
    config.esbuild !== false ? esbuildPlugin(config.esbuild) : null,
    jsonPlugin(
      {
        namedExports: true,
        ...config.json
      },
      isBuild
    ),
    wasmPlugin(config),
    webWorkerPlugin(config),
    assetPlugin(config),
    ...normalPlugins,
    definePlugin(config),
    cssPostPlugin(config),
    ...buildPlugins.pre,
    ...postPlugins,
    ...buildPlugins.post,
    // internal server-only plugins are always applied after everything else
    ...(isBuild
      ? []
      : [clientInjectionsPlugin(config), importAnalysisPlugin(config)])
  ].filter(Boolean) as Plugin[]
}

vite所有的插件,执行顺序如下:

  • alias
  • prePlugins(带有 enforce: 'pre'用户插件
  • 【build.polyfillDynamicImport】dynamicImportPolyfillPlugin(是否需要动态引入的polyfill插件)
  • resolvePlugin(文件路径转换)
  • htmlInlineScriptProxyPlugin
  • cssPlugin
  • 【config.esbuild !== false】esbuildPlugin
  • jsonPlugin
  • wasmPlugin
  • webWorkerPlugin
  • assetPlugin
  • normalPlugins(没有 enforce 值的用户插件
  • definePlugin
  • cssPostPlugin
  • buildPlugins.pre(Vite 构建用的插件)
  • postPlugins(带有 enforce: 'post'用户插件)
  • buildPlugins.post(Vite 构建用的插件)
  • clientInjectionsPlugin(内置的 server-only plugins)
  • importAnalysisPlugin(内置的 server-only plugins,用来分析代码里import的内容)

四、Esbuild

An extremely fast javascript bundler

vite基于esbuild转换jsx和ts,以及runOptimize(预构建依赖)

ESbuild用go语言编写,构建速度是js编写的打包工具的10-100倍

截屏2021-04-18 下午10.11.00.png ESbuild快的惊人,并且已经是在一个构建库方面比较出色的工具,但一些针对构建应用的重要功能任然还在持续开发中,特别是—代码分割和css处理方面。因此,vite构建项目还是用的rollup,但也不排除以后会用esbuild。

五、webapck项目vite改造

  1. 不支持require 如下是yyx老师在vite的issue里的留言 截屏2021-04-10 上午10.41.32.png

截屏2021-04-10 上午10.44.28.png 2. 之前项目用到的webpack插件,html-webpack-plugin、expose-loader等,都要找相应的替代插件或方法。 还有webpack的require.ensure方法,要改成动态import的写法。

  1. vite分包问题 主要分了以下几种包:

(1)vendor包 (2)css文件 (3)index入口文件 (4)index.html (5)manifest.json(6)DynamicImport的模块

  • vendor包分割:outputOptions.manualChunks:
(id, { getModuleInfo }) => {
    if (
      id.includes('node_modules') &&
      !isCSSRequest(id) &&
      !hasDynamicImporter(id, getModuleInfo, cache)
    ) {
      return 'vendor'
    }
}
  • css、manifest.json、index.html文件,用的rollup的generateBundle钩子,调用this.emitFile 用rollup做代码分割属于比较新的功能(2020年03月发布2.0.0之后,功能才比较完善)
    • ongenerate: use generateBundle instead
    • onwrite: use writeBundle instead
    • transformBundle: use renderChunk instead
    • transformChunk: use renderChunk instead 分割,代码举例:
 generateBundle() {
    this.emitFile({
      type: 'asset',
      fileName: 'index.html',
      source: fs.readFileSync(
        path.resolve(__dirname, 'index.dist.html'),
        'utf-8'
      )
    })
  }
  1. 文件引入后缀名问题 类似.vue这样的后缀,建议写全称而不是省略后缀名。

  2. 环境变量问题 在vite中改为:

  • import.meta.env.PROD: boolean 应用是否运行在生产环境
  • import.meta.env.DEV: boolean 应用是否运行在开发环境 (永远与 import.meta.env.PROD 相反)