vite 原理解析通过vite源码解析了vite的大致的运行流程,及功能关键点。同时,提及了webapck项目改造成vi
一、为什么使用vite
- 浏览器支持es module,关键变化:index.html中的入口文件导入方式:

因此,在开发环境下,不再需要先打包好所有用到的资源,再运行项目。而是,边运行,边加载用到的资源。因此,速度相比于构建式的(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)

createServer(创建server主要的运行流程)
vite 启动服务器主要进行了四个流程:
- 启动了文件监听,和websocket服务器,来启动热更新
- 创建了ViteDevServer对象
- 内部中间件挂载
- 重写了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的方法:

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

中间件
vite的中间件主要是依赖于connect(Connect is an extensible HTTP server framework for node using "plugins" known as middleware.)为httpServer扩展中间件。其中,用到的中间件有:
indexHtmlMiddleware,transformMiddleware,baseMiddleware,serveRawFsMiddleware,servePublicMiddleware,serveStaticMiddleware,proxyMiddleware,decodeURIMiddleware,errorMiddleware,timeMiddleware。
其中,比较核心的中间件有:
- 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将在服务的所有源文件中检测此类裸模块导入,并执行以下操作:
- 第三方依赖模块预构建,存放在/node_modules/.vite/文件夹下
 2. npm依赖解析
2. npm依赖解析
 

Vite插件
插件钩子
Vite插件继承了rollup插件的功能,扩展了自己独有的功能。
- 通用钩子
- 服务器启动时:options、buildStart
- 在每个传入模块请求时:resolveId、load、transform
- 服务器关闭时:buildEnd、closeBundle
- Vite独有钩子
- 在配置被解析之前,修改配置:config
- 在解析配置之后:configResolved
- 内部中间件被安装之前:configureServer注入后置中间件
- 转换index.html:transformIndexHtml
- 执行自定义热更新处理:handleHotUpdate
- 构建时钩子
- 获取构建参数: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倍
 ESbuild快的惊人,并且已经是在一个构建库方面比较出色的工具,但一些针对构建应用的重要功能任然还在持续开发中,特别是—代码分割和css处理方面。因此,vite构建项目还是用的rollup,但也不排除以后会用esbuild。
ESbuild快的惊人,并且已经是在一个构建库方面比较出色的工具,但一些针对构建应用的重要功能任然还在持续开发中,特别是—代码分割和css处理方面。因此,vite构建项目还是用的rollup,但也不排除以后会用esbuild。
五、webapck项目vite改造
- 不支持require
如下是yyx老师在vite的issue里的留言
 
 2. 之前项目用到的webpack插件,html-webpack-plugin、expose-loader等,都要找相应的替代插件或方法。
还有webpack的require.ensure方法,要改成动态import的写法。
2. 之前项目用到的webpack插件,html-webpack-plugin、expose-loader等,都要找相应的替代插件或方法。
还有webpack的require.ensure方法,要改成动态import的写法。
- 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 generateBundleinstead
- onwrite: use writeBundleinstead
- transformBundle: use renderChunkinstead
- transformChunk: use renderChunkinstead 分割,代码举例:
 
- ongenerate: use 
 generateBundle() {
    this.emitFile({
      type: 'asset',
      fileName: 'index.html',
      source: fs.readFileSync(
        path.resolve(__dirname, 'index.dist.html'),
        'utf-8'
      )
    })
  }
- 
文件引入后缀名问题 类似 .vue这样的后缀,建议写全称而不是省略后缀名。
- 
环境变量问题 在vite中改为: 
- import.meta.env.PROD: boolean 应用是否运行在生产环境
- import.meta.env.DEV: boolean 应用是否运行在开发环境 (永远与 import.meta.env.PROD 相反)
转载自:https://juejin.cn/post/6954671175318863879




