Skip to content

Vite 深入浅出

作者:Atom
字数统计:6.8k 字
阅读时长:25 分钟

Vite是构建工具的高阶封装,它的核心是RollupEsbuild等构建工具

静态资源处理

资源引入方式

Vite会自动处理以下几种类型的静态资源, 在生产构建的时添加文件指纹hash

  • 在 HTML 或者 JSX 中,通过元素标签来加载资源,如:
html
<img src="../assets/image.png"></img>
<video src="../assets/surround.mp4"></video>
<audio src="../assets/AudioTest.ogg"></audio>
  • 在 CSS 中使用静态路径,如:
css
background-url: url('../assets/image.png') norepeat;
  • 在 JavaScript 中,通过脚本的方式动态指定使用静态资源,如:
js
// 通过import会自动处理文件hash
import img from '../assets/image1.png'

// 通过运行时import也能自动处理文件hash
import('../assets/image2.png')

// 直接将静态URL赋值给src也能自动处理文件hash
document.getElementById('hero-img').src = '../assets/image.png'

注意: 动态拼接URL的情况

如果业务开发中, 链接URL不是静态的, 而是通过变量来动态拼接出来的, 一定要注意以下几种场景

  • 如果使用静态import全量导入资源, 假设有多个文件就要引入多次, 不够优雅
  • 如果使用动态import('../assets/${val}.png')来拼接资源URL, 会在编译后的文件中产生大量的JS模块文件, 也不够优雅
  • 对于动态URL的场景下, 首选使用new URL('../assets/${val}.png', import.meta.url)来处理

@import alias

Vite 通过 postcss-import 预配置支持了 CSS @import 内联, 也在CSS/Less/Sass中支持了@import alias

Vite中配置好resolve.alias别名来查找模块, 然便可以在样式代码中使用下面的语法

css
@import '@/styles/index.css';
@import url('@/styles/index.css');

注意

Wepback体系下的打包工具中, 要使用alias, 必须在前面加上波浪号~

css
@import '~@styles/index.css'

css modules

Vite项目中, 只要以.module.css 为后缀名的 CSS 文件都被认为是一个 CSS modules 模块, 在使用的过程中就可以使用css modules的语法

css
.container {
  background-color: aqua;
}
vue
<template>
  <div :class="cssMod.container">
    Hello World
  </div>
</template>
<script setup>
  import cssMod from '@/styles/index.module.css';
</script>

TS集成

在Typescipt集成的过程中, 需要注意以下问题

只编译不校验

Vite只做ts的编译, 并不做ts的类型校验。因此需要借助IDEtsc --noEmit来做校验, 如果是vue项目使用vue-tsc --noEmit

配置文件

Vite的项目中, ts配置文件tsconfig.json有几个关键选项必须开启

  • Vite项目默认所有模块都是ESM, 因此必须开启 "module": "ESNext"

  • 如果是在Vue项目中使用Vite, 同时Vue中使用jsx语法, 那必须开启"jsx": "preserve""jsxImportSource": "vue"

注意

Vue 3.4 开始,Vue 不再隐式注册全局JSX命名空间。要指示TypeScript 使用 VueJSX 类型定义,请确保在您的配置文件tsconfig.json中包含以下内容"jsxImportSource": "vue"

  • 必须开启"isolatedModules": true, Vite在编译时只会编译单个文件中的ts语法变成js, 不会读取该文件所关联的其他ts模块的信息, 导致很多ts的功能无法使用

开启后能解决以下问题

  1. a.ts导入的外部类型A, 编译export { A }后的代码会报错。开启该选项后, 可以提前给出提示
  2. 开启后会在使用const enum Num 语法时提示报错。 因为Vite使用esbuild编译ts,编译后代码不会删除枚举的变量Num.flag, 却删除了枚举的定义ts代码, 导致报错。本质是设计上不支持const enum Num语法
  3. 开启isolatedModules后, 要求所有单个ts文件是一个模块

依赖预构建

自动重构建

Vite 将预构建的依赖项缓存到 node_modules/.vite 中。一旦以下选项的内容发生变化, 就会导致缓存失效, 再次重新运行预构建

  • 包管理器的锁文件内容,例如 package-lock.jsonyarn.lockpnpm-lock.yaml,或者 bun.lockb
  • 补丁文件夹的修改时间
  • vite.config.js 中的相关字段
  • NODE_ENV 的值

手动重构建

  • 手动删除 node_modules/.vite 缓存目录
  • 使用--force选项, 例如pnpm dev --force

包导入导出策略

Node支持ESM之后的版本(>=12.20)中, 有以下两种使用原生 ESM的方式:

  • 代码文件以.mjs结尾
  • package.json 中声明type: "module"

Node 在导入导出这些ESM版本的Npm包时, 过程是较为复杂的, 其中关键字段均在package.json中定义

条件导出

Node中, 如果使用ESM导入Npm模块, 需要看该Npm包中package.json的核心字段mainexports。且Node官方规定了在两者同时出现的场景下, exports 的优先级会比 main 更高, 最终将Npm模块的入口文件确定下来

main

json
{
  "name": "package-a",
  "main": "./dist/index.js"
}

exports

在使用exports导出的场景下, 主要包含默认导出、子路径导出和条件导出

json
// package.json
{
  "name": "package-a",
  "type": "module",
  "exports": {
    // 默认导出,使用方式: import a from 'package-a'
    ".": "./dist/index.js",
  }
}
json
// package.json
{
  "name": "package-a",
  "type": "module",
  "exports": {
    // 子路径导出,使用方式: import d from 'package-a/dist'
    "./dist": "./dist/index.js",
    "./dist/*": "./dist/*", // 这里可以使用 `*` 导出目录下所有的文件
  }
}
json
// package.json
{
  "name": "package-a",
  "type": "module",
  "exports": {
    // 条件导出,区分 ESM 和 CommonJS 引入的情况
    "./main": {
      "import": "./main.js",
      "require": "./main.cjs"
    },
  }
}

其中, 条件导出的情况最为复杂, 因为它的导出key关键字可以分为以下场景

  • node: 在 Node.js 环境下适用,可以定义为嵌套条件导出
  • import: 用于 import 方式导入的情况,如import("package-a")
  • require: 用于 require 方式导入的情况,如require("package-a")
  • default: 兜底方案,如果前面的条件都没命中,则使用 default 导出的路径
  • types: 该导出可由typescript用来解析给定的类型定义文件。此条件应始终首先包含在内
  • browser: 任何浏览器环境
  • development: 可用于定义仅开发环境入口点,例如提供额外的调试上下文,例如在开发模式下运行时提供更好的错误消息。必须始终与 互斥"production"
  • production: 可用于定义生产环境入口点。必须始终与 互斥"development"
json
{
  "main": "./dist/node/index.js",
  "types": "./dist/node/index.d.ts",
  "exports": {
    ".": {
      "import": {
        "types": "./dist/node/index.d.ts",
        "default": "./dist/node/index.js"
      },
      "require": {
        "types": "./index.d.cts",
        "default": "./index.cjs"
      }
    },
    "./client": {
      "types": "./client.d.ts"
    },
    "./dist/client/*": "./dist/client/*",
    "./types/*": {
      "types": "./types/*"
    },
    "./package.json": "./package.json"
  }
}
json
{
  "main": "index.js",
  "module": "dist/vue.runtime.esm-bundler.js",
  "types": "dist/vue.d.ts",
  "unpkg": "dist/vue.global.js",
  "jsdelivr": "dist/vue.global.js",
  "files": [
    "index.js",
    "index.mjs",
    "dist",
    "compiler-sfc",
    "server-renderer",
    "macros.d.ts",
    "macros-global.d.ts",
    "ref-macros.d.ts"
  ],
  "exports": {
    ".": {
      "import": {
        "node": "./index.mjs",
        "default": "./dist/vue.runtime.esm-bundler.js"
      },
      "require": "./index.js",
      "types": "./dist/vue.d.ts"
    },
    "./server-renderer": {
      "import": "./server-renderer/index.mjs",
      "require": "./server-renderer/index.js"
    },
    "./compiler-sfc": {
      "import": "./compiler-sfc/index.mjs",
      "require": "./compiler-sfc/index.js"
    },
    "./dist/*": "./dist/*",
    "./package.json": "./package.json",
    "./macros": "./macros.d.ts",
    "./macros-global": "./macros-global.d.ts",
    "./ref-macros": "./ref-macros.d.ts"
  }
}
json
{
  "type": "module",
  "exports": {
    "electron": {
      "node": {
        "development": {
          "module": "./index-electron-node-with-devtools.js",
          "import": "./wrapper-electron-node-with-devtools.js",
          "require": "./index-electron-node-with-devtools.cjs"
        },
        "production": {
          "module": "./index-electron-node-optimized.js",
          "import": "./wrapper-electron-node-optimized.js",
          "require": "./index-electron-node-optimized.cjs"
        },
        "default": "./wrapper-electron-node-process-env.cjs"
      },
      "development": "./index-electron-with-devtools.js",
      "production": "./index-electron-optimized.js",
      "default": "./index-electron-optimized.js"
    },
    "node": {
      "development": {
        "module": "./index-node-with-devtools.js",
        "import": "./wrapper-node-with-devtools.js",
        "require": "./index-node-with-devtools.cjs"
      },
      "production": {
        "module": "./index-node-optimized.js",
        "import": "./wrapper-node-optimized.js",
        "require": "./index-node-optimized.cjs"
      },
      "default": "./wrapper-node-process-env.cjs"
    },
    "development": "./index-with-devtools.js",
    "production": "./index-optimized.js",
    "default": "./index-optimized.js"
  }
}

条件导入

Npm包的条件导入由package.json 中的 imports字段来定义, 该字段的值是一个对象, 其中的key是导入的别名, value是导入的路径

这样相当于实现了路径别名的功能,不过与构建工具中的 alias 功能不同的是,"imports" 中声明的别名必须全量匹配

json
{
  "imports": {
    // key 一般以 # 开头
    // 也可以直接赋值为一个字符串: "#dep": "lodash-es"
    "#dep": {
      "node": "lodash-es",
      "default": "./dep-polyfill.js"
    },
  },
  "dependencies": {
    "lodash-es": "^4.17.21"
  }
}
js
// index.js
// 在执行的时候会将`#dep`定位到`lodash-es`这个第三方包,当然,你也可以将其定位到某个内部文件
import { cloneDeep } from "#dep";

const obj = { a: 1 };

// { a: 1 }
console.log(cloneDeep(obj));

Vite 插件开发

Vite的插件本质上是受限制的rollup插件

插件命名

  • 如果需要同时支持ViteRollup,那么插件的命名建议以rollup-plugin-开头,例如rollup-plugin-alias

  • 否则只支持vite,那么插件的命名必须以vite-开头,例如vite-virtual-module。 因为在Vite中有自有的钩子,将不再支持rollup

虚拟模块

作为构建工具,一般需要处理两种形式的模块,一种存在于真实的磁盘文件系统中,另一种并不在磁盘而在内存当中,也就是虚拟模块。通过虚拟模块,我们既可以把自己手写的一些代码字符串作为单独的模块内容,又可以将内存中某些经过计算得出的变量作为模块内容进行加载,非常灵活和方便

下面来实现一个简单的虚拟模块插件,首先准备一个脚手架搭建的基本项目,可以使用作者的demos(vite-vue3)

src/plugins目录下创建一个virtual-module.ts文件, 作为虚拟模块的入口文件

ts
// 这个模块用于来计算数组的和,
// plugins/virtual-plugin-calc.ts
import type { Plugin, ResolvedConfig } from 'vite';

// 虚拟模块名称
const virtualSumModuleId = 'virtual:calc';

// 在内部,使用了虚拟模块的插件在解析时应该将模块 ID 加上前缀 \0,这一约定来自 rollup 生态
const resolvedSumVirtualModuleId = '\0' + virtualSumModuleId;

export default function virtualSumModulePlugin(): Plugin {
  let config: ResolvedConfig | null = null;
  return {
    name: 'vite-plugin-virtual-calc',
    resolveId(id) {
      if (id === virtualSumModuleId) {
        return resolvedSumVirtualModuleId;
      }
    },
    load(id) {
      // 加载虚拟模块
      if (id === resolvedSumVirtualModuleId) {
        return 'export default function sum(arr, i = 0) { return i >= arr.length ? 0 : arr[i] + sum(arr, i + 1); }';
      }
    }
  }
}
ts
// vite.config.ts
import virtual from './plugins/virtual-module.ts'

// 配置插件
{
  plugins: [react(), virtual()]
}
ts
import { createApp } from 'vue'
import './assets/styles/style.css'
import App from './App.vue'
import sum from "virtual:calc"

console.log('%c------>[LOG:]', 'color: fuchsia', sum([1,3,5,7,9]))  // 25
createApp(App).mount('#app')
ts
// 全局添加虚拟模块的类型声明
declare module 'virtual:*' {
  const component: any;
  export default component;
}

兼容的Rollup钩子

注意

vite.config.ts配置文件的plugins中, 传入Rollup的插件, 仅会执行下面几个兼容的钩子, 其他钩子不会执行

如果需要配置符合Rollup执行方式的插件钩子, 请单独配置到vite.config.ts配置文件的选项build.rollupOptions.plugins中, 以达到更好的兼容性

特别注意, 开发环境中moduleParsed钩子不会被调用

  • 服务启动时: 仅会调用一次optionsbuildStart这两个钩子, 文件变化时不会执行, 跟文件更新无关
  • 每个传入模块请求时: resolveIdloadtransform三个代码完整编译的钩子
  • 服务关闭时: buildEndcloseBundle

开发Rollup插件的过程中, 如果需要兼容Vite, 那么必须满足以下条件

  • 没有使用moduleParsed钩子
  • 打包钩子和输出钩子之间没有很强的耦合(即输出阶段非常依赖build阶段的执行), 因为Vite在开发阶段, 只会执行build阶段的钩子

插件执行时机

  • pre: 在Rollup使用alias后就会被执行
  • normal: 核心插件执行之后, build执行构建之前, 代码编译未开始
  • post: build构建之后

独有钩子

  • config 多个插件实现这个钩子, 配置项将被深度合并到现有配置中的部分配置对象

  • configResolved 使用这个钩子能够读取和存储最终解析的配置, 即多个config合并结果

  • configureServer 配置本地开发服务器的钩子, 比如可以自定义前置中间件或者后置中间件

  • configurePreviewServer 配置本地预览服务器的钩子

  • transformIndexHtml 接收入口的 HTML 字符串和转换上下文的配置钩子

  • handleHotUpdate 执行自定义 HMR 更新处理

部分配置项

这块内容主要列出一些重要的配置项, 这些配置项易忘易错, 但是又很重要

resolve.dedupe

Vite中resolve.dedupe选项可以帮助我们消除重复的模块依赖,并提升构建性能。它可以在resolve.alias前对模块路径进行处理,将同一模块的不同版本定位到相同的地址,避免同一模块的多次加载。

使用方法:

ts
// vite.config.ts
export default defineConfig({
  // ...省略其他选项
  resolve: {
    // package1和package2表示需要去重的包名
    dedupe: ['package1', 'package2']
  }
})

esbuild.jsxInject

通过 esbuild.jsxInject 来自动为每一个被 esbuild 转换的文件注入 JSX helper。这征对React项目特别有用,在React项目中很多地方都需要导入React,配置这个选项后, 会自动帮我们导入

ts
export default defineConfig({
  esbuild: {
    jsxInject: `import React from 'react'`,
  },
})

json.namedExports

该选项默认开启,用于支持从 .json 文件中进行按名导入

ts
import { name, version } from 'package.json'

json.stringify

默认不开启为false,导入的 JSON 会被转换为export default JSON.parse("...")用来得到更好的性能。 因此无法对.json进行具名导入了。所以Vite会将json.namedExports关闭

assetsInclude

该选项指定其他文件类型作为静态资源处理, 默认只处理以下文件类型

ts
export const KNOWN_ASSET_TYPES = [
  // images
  'apng',
  'png',
  'jpe?g',
  'jfif',
  'pjpeg',
  'pjp',
  'gif',
  'svg',
  'ico',
  'webp',
  'avif',

  // media
  'mp4',
  'webm',
  'ogg',
  'mp3',
  'wav',
  'flac',
  'aac',
  'opus',
  'mov',
  'm4a',
  'vtt',

  // fonts
  'woff2?',
  'eot',
  'ttf',
  'otf',

  // other
  'webmanifest',
  'pdf',
  'txt',
]

assetsInclude选项值是采用picomatch匹配规则, 具体如下

通配符描述
*匹配零次或多次任何字符,不包括路径分隔符。不匹配路径分隔符或隐藏文件或目录(“点文件”),除非通过将 dot 选项设置为 true 显式启用
**匹配零次或多次任何字符,包括路径分隔符。请注意, ** 仅在路径段中仅包含路径分隔符( /\\(windows下的路径分隔符))时才匹配路径分隔符。因此, foo**/bar 等效于 foo*/barfoo/a**b/bar 等效于 foo/a*b/bar ,并且 glob 路径段中连续两个以上的星号被视为单个星号。因此, foo/***/bar 等效于 foo/*/bar
?匹配一次不包括路径分隔符的任何字符。不匹配路径分隔符或前导点
[abc]匹配方括号内的任何字符。例如, [abc] 将匹配字符 abc ,以及其他任何内容

envDir

默认目录为根目录root,这个配置用于设置.env环境变量(.env.development, .env.production, .env.test.local)的目录

envPrefix

envPrefix 开头的环境变量会通过 import.meta.env 暴露在你的客户端源码中。

如果需要在应用(业务)代码中暴露不含前缀VITE_的变量,只能使用define配置

ts
export default defineConfig({
  define: {
    'import.meta.env.ENV_VARIABLE': JSON.stringify(process.env.ENV_VARIABLE)
  }
})

构建选项主要是用于配置Vite的构建行为, 包含了各种资源的打包处理

build.cssTarget

此选项允许用户为 CSS 的压缩设置一个不同的浏览器 target,此处的 target 并非是用于 JavaScript 转写目标。

应只在针对非主流浏览器时使用。 最直观的示例是当你要兼容的场景是安卓微信中的 webview 时,它支持大多数现代的 JavaScript 功能,但并不支持 CSS 中的 #RGBA 十六进制颜色符号。 这种情况下,你需要将 build.cssTarget 设置为 chrome61,以防止 vitergba() 颜色转化为 #RGBA 十六进制符号的形式

build.dynamicImportVarsOptions

Vite官网中提到这个选项是用于传递给@rollup/plugin-dynamic-import-vars插件的,那么这个插件是干什么的?

@rollup/plugin-dynamic-import-vars

这个插件用于在动态引用表达式中,例如import('src/assets/${imgName}')。该插件会将这种运行时才能处理的导入解析为一个通配符路径,然后提前预处理资源的加载

比如下面的例子:

js
`./locales/${locale}.js` ===转化为===> './locales/*.js'

更多关于该插件的内容请查看@rollup/plugin-dynamic-import-vars

掘金也有相关的@rollup/plugin-dynamic-import-var源码分析

build.lib

Vite修改为库模式。

entry 是必需的,因为库不能使用 HTML 作为入口。name 则是暴露的全局变量,并且在 formats 包含 'umd' 或 'iife' 时是必需的。默认 formats 是 ['es', 'umd'],如果使用了多个配置入口,则是 ['es', 'cjs']。fileName 是输出的包文件名,默认 fileName 是 package.json 的 name 选项,同时,它还可以被定义为参数为 format 和 entryAlias 的函数。

与rollup打包库的比较

总的来说,两者都可以打包库,但Vite的集成化更加好一点,配置更简单一些。

Vite 相比,使用 Rollup 的一个优点是工具的复杂性和依赖性可能更低。比如您不需要开发服务器并且构建过程很简单,那么使用 Rollup 更好

optimizeDeps

这块内容主要是用于配置Vite的预构建依赖项, 也就是Vite在启动时, 会将这些依赖项提前构建好, 以提升启动速度

optimizeDeps.entries

Vite会将optimizeDeps.entries中的依赖项提前构建好, 以提升启动速度

optimizeDeps.esbuildOptions

在依赖扫描和优化过程中传递给 esbuild 的选项, 与Vite冲突的external选项将被忽略, pluginsVitedep插件合并

optimizeDeps.force

设置为 true : 强制开启新的依赖预构建,而忽略之前已经缓存过的、已经优化过的依赖

optimizeDeps.needsInterop

Vite进行依赖性导入分析,它会对需要预编译且为 CommonJS 的依赖导入代码进行重写

因为Vite是以ESM的方式运行的,但是很多第三方库都是以CommonJS的方式导出的,所以需要对这些库进行重写,且CommonJS并不支持命名方式的导出。

举个例子,当我们在 Vite 项目中使用 react 时:

ts
import React, { useState, createContext } from 'react'

// 此时 react 就需要配置 needsInterop 为 true,对代码进行重写以便支持ESM
import $viteCjsImport1_react from "/@modules/react.js";
const React = $viteCjsImport1_react;
const useState = $viteCjsImport1_react["useState"];
const createContext = $viteCjsImport1_react["createContext"];

Rollup

Vite是基于Rollup封装的高阶构建工具, 那么本段来记录一下Rollup的各种用法及插件开发

Rollup

Rollup是以ESM标准为目标的构建工具,默认只识别ESM模块文件,对于Commonjs的模块文件,默认是不支持的,只能使用插件来提供支持

插件顺序

Rollup配置多个plugins的情况下, 插件默认是从前往后执行, 因此项目中的插件必须配置一定的顺序。如果后续的插件依赖前面的插件, 需要注意先后顺序。一般的工程中,都会使用commonjsresolve等等插件, 所以这几个插件的顺序如下

js
// resolve让项目支持使用node_modules中的模块
import resolve from '@rollup/plugin-node-resolve';
// rollup使用的是es6的模块化, 插件commonjs能够让项目能够支持使用cjs源码的npm库
import commonjs from '@rollup/plugin-commonjs';

export default {
	input: 'main.js',
	plugins: [
		resolve(),
		commonjs()
	],
	output: {
		file: 'bundle.js',
		format: 'cjs'
	}
}

常用插件

下面罗列项目中常用的插件及配置, 更多插件请关注官方推荐插件列表

js
import path from 'path';
import alias from '@rollup/plugin-alias';
import commonjs from '@rollup/plugin-commonjs';
import postcss from 'rollup-plugin-postcss';
import serve from 'rollup-plugin-serve';
import livereload from 'rollup-plugin-livereload';
import del from 'rollup-plugin-delete';
import babel from '@rollup/plugin-babel';
import typescript from '@rollup/plugin-typescript';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import terser from '@rollup/plugin-terser';
import replace from '@rollup/plugin-replace';
import legacy from '@rollup/plugin-legacy';
import json from '@rollup/plugin-json';
import eslint from '@rollup/plugin-eslint';
import image from '@rollup/plugin-image';


export default {

  input: './src/main.js', // 入口文件

  output: {
    file: './dist/bundle.js', // 打包后的存放文件
    //dir: './dist', // 多入口打包必需加上的属性
    format: 'cjs', // 输出格式 amd umd es cjs iife system
    name: 'bundleName', // 如果iife,umd需要指定一个全局变量
    sourcemap: true, // 是否开启代码回溯
    banner: "/** 文件开头的声明内容、作者等信息 **/" // 打包后的文件头部添加注释
  },

  plugins: [
    replace({
      'process.env.NODE_ENV': JSON.stringify('production'),
      __buildDate__: () => JSON.stringify(new Date()),
      __buildVersion: 15
    }),

    // 支持从node_modules引入其他的包
    nodeResolve(),

    typescript({
      exclude: 'node_modules/**',
      include: 'src/**',
    }),

    // 支持common.js
    // 在 rollup 中引用 commonjs 规范的包。该插件的作用是将 commonjs 模块转成 es6 模块。
    commonjs({
      throwOnError: true,
    }),

    // 支持eslint
    eslint(),

    // 支持加载图片
    image(),

    // es6语法转义
    babel({
      exclude: 'node_modules/**',
      extensions: ['.js', '.jsx'],
      presets: ['@babel/preset-env', '@babel/preset-react'],
    }),

    // 让项目中可以导入json文件
    json(),

    // 支持加载css,添加前缀等
    postcss(),

    // 有时,您会发现旧时代的一段有用的代码片段,在像 npm 这样的新技术出现之前。
    // 这些脚本通常会将自己公开为var someLibrary = ...或window.someLibrary = ...,
    // 期望其他脚本将从全局命名空间获取对库的引用。
    // 将它们转换为模块通常很容易。但何苦呢?您只需添加legacy插件并进行相应配置,它就会自动变成模块。
    legacy({ 'vendor/some-library.js': 'someLibrary' }),

    // 打包前清空目标目录
    del({ targets: 'dist/*', verbose: true }),

    // 压缩js
    terser(),

    // 启动本地服务
    serve({
      contentBase: '', //服务器启动的文件夹,默认是项目根目录,需要在该文件下创建index.html
      port: 8020, //端口号,默认10001
    }),

    // watch目录,当目录中的文件发生变化时,刷新页面
    livereload('dist'),

    // 使用别名
    alias({
      entries: [{ find: '@', replacement: path.join(__dirname, 'src') }],
    }),
  ],
  // 告诉rollup不要将此lodash打包,而作为外部依赖,在使用该库时需要先安装相关依赖
  external: ['react']

};

插件钩子

Rollup的插件钩子主要分为两种, 即构建钩子、输出生成钩子。 这两种钩子的时序图如下

构建钩子流程图

构建钩子时序图

  1. 首先经历 options 钩子进行配置的转换,得到处理后的配置对象。

  2. 随之 Rollup 会调用buildStart钩子,正式开始构建流程。

  3. Rollup 先进入到 resolveId 钩子中解析文件路径。(从 input 配置指定的入口文件开始)。

  4. Rollup 通过调用load钩子加载模块内容。

  5. 紧接着 Rollup 执行所有的 transform 钩子来对模块内容进行进行自定义的转换,比如 babel 转译。

  6. 现在 Rollup 拿到最后的模块内容,进行 AST 分析,得到所有的 import 内容,调用 moduleParsed 钩子:

moduleParsed判断

  • 如果是普通的 import,则执行 resolveId 钩子,继续回到步骤3
  • 如果是动态 import,则执行 resolveDynamicImport 钩子解析路径,如果解析成功,则回到步骤4加载模块,否则回到步骤3通过 resolveId 解析路径。
  1. 直到所有的 import 都解析完毕,Rollup 执行buildEnd钩子,Build 阶段结束。

当然,在 Rollup 解析路径的时候,即执行resolveId或者resolveDynamicImport的时候,有些路径可能会被标记为external(翻译为排除),也就是说不参加 Rollup 打包过程,这个时候就不会进行loadtransform等等后续的处理了。

在流程图最上面,watchChangecloseWatcher这两个 Hook,对应了 rollup 的watch模式。当你使用 rollup --watch 指令或者在配置文件配有watch: true的属性时,代表开启了 Rollup 的watch打包模式,这个时候 Rollup 内部会初始化一个 watcher 对象,当文件内容发生变化时,watcher 对象会自动触发watchChange钩子执行并对项目进行重新构建。在当前打包过程结束时,Rollup 会自动清除 watcher 对象调用closeWacher钩子。

输出生成钩子流程图

构建钩子时序图

  1. 执行所有插件的 outputOptions 钩子函数,对 output 配置进行转换。

  2. 执行 renderStart,并发执行 renderStart 钩子,正式开始打包。

  3. 并发执行所有插件的bannerfooterintrooutro 钩子(底层用 Promise.all 包裹所有的这四种钩子函数),这四个钩子功能很简单,就是往打包产物的固定位置(比如头部和尾部)插入一些自定义的内容,比如协议声明内容、项目介绍等等。

  4. 从入口模块开始扫描,针对动态 import 语句执行 renderDynamicImport钩子,来自定义动态 import 的内容。

  5. 对每个即将生成的 chunk,执行 augmentChunkHash钩子,来决定是否更改 chunk 的哈希值,在 watch 模式下即可能会多次打包的场景下,这个钩子会比较适用。

  6. 如果没有遇到 import.meta 语句,则进入下一步,否则:

import.meta的处理流程

  • 对于 import.meta.url 语句调用 resolveFileUrl 来自定义 url 解析逻辑
  • 对于其他import.meta 属性,则调用 resolveImportMeta 来进行自定义的解析。
  1. 接着 Rollup 会生成所有 chunk 的内容,针对每个 chunk 会依次调用插件的renderChunk方法进行自定义操作,也就是说,在这里时候你可以直接操作打包产物了。

  2. 随后会调用 generateBundle 钩子,这个钩子的入参里面会包含所有的打包产物信息,包括 chunk (打包后的代码)、asset(最终的静态资源文件)。你可以在这里删除一些 chunk 或者 asset,最终这些内容将不会作为产物输出。

  3. 前面提到了rollup.rollup方法会返回一个bundle对象,这个对象是包含generatewrite两个方法,两个方法唯一的区别在于后者会将代码写入到磁盘中,同时会触发writeBundle钩子,传入所有的打包产物信息,包括 chunkasset,和 generateBundle钩子非常相似。不过值得注意的是,这个钩子执行的时候,产物已经输出了,而 generateBundle 执行的时候产物还并没有输出。顺序如下图所示:

  1. 当上述的bundleclose方法被调用时,会触发closeBundle钩子,到这里 Output 阶段正式结束。

注意

当打包过程中任何阶段出现错误,会触发 renderError 钩子,然后执行closeBundle钩子结束打包。

以上就是 Rollup 中完整的插件工作流程

参考资料