Vite
Vite (法语意为 “快速的”,发音
/vit/
) 是一种新型前端构建工具,能够显著提升前端开发体验。
特点
- 快速的冷启动(将应用中的模块区分为 依赖 和 源码 两类,改进了开发服务器启动时间。源码模块的请求会根据
304 Not Modified
进行协商缓存,而依赖模块请求则会通过Cache-Control: max-age=31536000,immutable
进行强缓存) - 即时的模块热更新 HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失效(大多数时候只需要模块本身),使 HMR 更新始终快速,无论应用的大小。
- 真正的按需加载 Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入的代码,即只在当前屏幕上实际使用时才会被处理。
主要组成部分
- 一个开发服务器:基于 原生 ES 模块 提供了 丰富的内建功能。
- npm依赖解析和预构建。原生 ES 引入不支持裸模块导入,Vite 将在服务的所有源文件中检测此类裸模块导入,并通过esbuild预构建它们以提升页面重载速度,重写导入为合法的 URL,例如
/node_modules/.vite/my-dep.js?v=f3sf2ebd
以便浏览器能够正确导入它们。 - 依赖是强缓存的。
- 支持模块热重载
- 支持typescript
- 为 Vue 提供第一优先级支持:Vue 3 单文件组件支持:@vitejs/plugin-vue、Vue 3 JSX 支持:@vitejs/plugin-vue-jsx、Vue 2 支持:underfin/vite-plugin-vue2
- 支持JSX
- css支持
@import
内联和重命名、接受postcss.config.js配置、支持CSS Modules、支持css预处理器.scss .sass .less .styl .stylus - 导入一个静态资源会返回解析后的 URL
- json文件支持具名导入
- 支持Glob导入
- 支持Web Assembly
- web worker 脚本可以直接通过添加一个
?worker
查询参数来导入。默认导出将是一个自定义的 worker 构造器。worker 脚本也可以使用import
语句来替代importScripts()
(目前只在 Chrome 中适用,而在生产版本中,它已经被编译掉了)
- npm依赖解析和预构建。原生 ES 引入不支持裸模块导入,Vite 将在服务的所有源文件中检测此类裸模块导入,并通过esbuild预构建它们以提升页面重载速度,重写导入为合法的 URL,例如
- 一套构建指令:使用 Rollup 打包代码,并且是预配置的,可以输出用于生产环境的优化过的静态资源。
- 对动态导入的Polyfill Vite 自动会生成一个轻量级的 对动态导入的 polyfill
- css代码分割 Vite 会自动地将一个异步 chunk 模块中使用到的 CSS 代码抽取出来并为其生成一个单独的文件。这个 CSS 文件将在该异步 chunk 加载完成时自动通过一个
<link>
标签载入 - 预加载指令生成 Vite 会为入口 chunk 和它们在打包出的 HTML 中的直接引入自动生成
<link rel="modulepreload">
指令。 - 异步加载chunk优化 优化将跟踪所有的直接导入,无论导入深度如何,都完全消除不必要的往返。
兼容性
- Vite 需要 Node.js 版本 >= 12.0.0。
- 开发环境:需要在支持原生ES模块动态导入的浏览器中使用。
- 生产环境:默认支持的浏览器需要支持 通过脚本标签来引入原生 ES 模块 。可以通过官方插件 @vitejs/plugin-legacy 支持旧浏览器。
项目搭建
#npm
npm init @vitejs/app
#yarn
yarn create @vitejs/app
复制代码
# npm 6.x
npm init @vitejs/app [project-name] --template vue
# npm 7+, 需要额外的双横线:
npm init @vitejs/app [project-name] -- --template vue
# yarn
yarn create @vitejs/app [project-name] --template vue
复制代码
支持的模板预设包括:
vanilla
vue
vue-ts
react
react-ts
preact
preact-ts
lit-element
lit-element-ts
svelte
svelte-ts
index.html
在开发期间 Vite 是一个服务器,而 index.html
是该 Vite 项目的入口文件
Vite 将 index.html
视为源码和模块图的一部分。Vite 解析 <script type="module" src="https://juejin.cn/post/...">
,这个标签指向你的 JavaScript 源码。甚至内联引入 JavaScript 的 <script type="module" src="https://juejin.cn/post/...">
和引用 CSS 的 <link href>
也能利用 Vite 特有的功能被解析。另外,index.html
中的 URL 将被自动转换,因此不再需要 %PUBLIC_URL%
占位符了。
启动命令
{
"scripts": {
"dev": "vite", // 启动开发服务器
"build": "vite build", // 为生产环境构建产物
"serve": "vite preview" // 本地预览生产构建产物
}
}
复制代码
常用配置
默认配置文件vite.config.js,可以显示的通过–config指定配置文件
vite --config my-config.js
复制代码
Vite 也直接支持 TS 配置文件。你可以在 vite.config.ts
中使用 defineConfig
帮手函数。
如果配置文件需要基于(serve
或 build
)命令或者不同的 模式 来决定选项,则可以选择导出这样一个函数:
export default ({ command, mode }) => {
if (command === 'serve') {
return {
// serve 独有配置
}
} else {
return {
// build 独有配置
}
}
}
复制代码
如果配置需要调用一个异步函数,也可以转而导出一个异步函数:
export default async ({ command, mode }) => {
const data = await asyncFunction()
return {
// 构建模式所需的特有配置
}
}
复制代码
共享配置
配置项 | 类型 | 默认值 | 说明 |
---|---|---|---|
base | string | ‘/’ | 开发或生产环境服务的公共基础路径 |
plugins | (PluginOption|PluginOption[])[] | ||
publicDir | string | ‘public’ | 作为静态资源服务的文件夹。这个目录中的文件会在开发中被服务于 / ,在构建模式时,会被拷贝到 outDir 的根目录,并没有转换,永远只是复制到这里。 |
cacheDir | string | ‘node_modules/.vite’ | 存储预打包的依赖项或 vite 生成的某些缓存文件 |
resolve.alias | |||
resolve.extensions | string[] | [‘.mjs’, ‘.js’, ‘.ts’, ‘.jsx’, ‘.tsx’, ‘.json’] | 不 建议忽略自定义导入类型的扩展名(例如:.vue ),因为它会干扰 IDE 和类型支持。 |
json.namedExports | boolean | true | 是否支持从 .json 文件中进行按名导入。 |
json.stringify | boolean | false | 若设置为 true ,导入的 JSON 会被转换为 export default JSON.parse("...") 会比转译成对象字面量性能更好,尤其是当 JSON 文件较大的时候。开启此项,则会禁用按名导入。 |
Server Options
配置项 | 类型 | 说明 |
---|---|---|
server.host | string | 指定服务器主机名 |
server.port | number | 指定服务器端口。注意:如果端口已经被使用,Vite 会自动尝试下一个可用的端口,所以这可能不是服务器最终监听的实际端口。 |
server.strictPort | boolean | 设为 true 时若端口已被占用则会直接退出,而不是尝试下一个可用端口。 |
server.https | boolean|https.ServerOptions | 启用 TLS + HTTP/2 |
server.open | boolean|string | 在服务器启动时自动在浏览器中打开应用程序。当此值为字符串时,会被用作 URL 的路径名。 |
server.proxy | Record<string, string|ProxyOptions> | |
server.cors | boolean|CorsOptions | 为开发服务器配置 CORS。默认启用并允许任何源,传递一个 选项对象 来调整行为或设为 false 表示禁用。 |
server.force | boolean | 设置为 true 强制使依赖预构建。 |
Build Options
配置项 | 类型 | 默认值 | 说明 |
---|---|---|---|
build.target | string | ’modules‘ | 设置最终构建的浏览器兼容目标。默认值是一个 Vite 特有的值,'modules' ,这是指 支持原生 ES 模块的浏览器。 |
build.polyfillDynamicImport | boolean | true | 决定是否自动注入 对动态导入的 polyfill。该 polyfill 将被自动注入进每个 index.html 入口的代理模块中。 |
build.outDir | string | dist | 指定输出路径(相对于 项目根目录). |
build.assetsDir | string | assets | 指定生成静态资源的存放路径(相对于 build.outDir )。 |
build.assetsInlineLimit | number | 4096(4kb) | 小于此阈值的导入或引用资源将内联为 base64 编码,以避免额外的 http 请求。设置为 0 可以完全禁用此项。 |
build.cssCodeSplit | boolean | true | 启用/禁用 CSS 代码拆分。当启用时,在异步 chunk 中导入的 CSS 将内联到异步 chunk 本身,并在块加载时插入。如果禁用,整个项目中的所有 CSS 将被提取到一个 CSS 文件中。 |
build.sourcemap | boolean|’inline’ | false | 构建后是否生成 source map 文件。 |
build.rollupOptions | RolluoOptions | 自定义底层的 Rollup 打包配置。这与从 Rollup 配置文件导出的选项相同,并将与 Vite 的内部 Rollup 选项合并。 | |
build.minify | boolean|’terser’|’esbuild’ | ‘terser’ | 设置为 false 可以禁用最小化混淆,或是用来指定使用哪种混淆器。Terser 相对较慢,但大多数情况下构建后的文件体积更小。ESbuild 最小化混淆更快但构建后的文件相对更大。 |
build.emptyoutDir | boolean | 若 outDir 在 root 目录下,则为 true |
默认情况下,若 outDir 在 root 目录下,则 Vite 会在构建时清空该目录。若 outDir 在根目录之外则会抛出一个警告避免意外删除掉重要的文件。 |
build.chunkSizeWarningLimit | number | 500 | chunk 大小警告的限制(以 kbs 为单位)。 |
依赖优化选项
配置项 | 类型 | 说明 |
---|---|---|
optimizeDeps.entries | string|string[] | 默认情况下,Vite 会抓取你的 index.html 来检测需要预构建的依赖项。如果指定了 build.rollupOptions.input ,Vite 将转而去抓取这些入口点。如果这两者都不适合你的需要,则可以使用此选项指定自定义条目 – 该值需要遵循 fast-glob 模式 ,或者是相对于 vite 项目根的模式数组。这将覆盖掉默认条目推断。 |
optimizeDeps.exclude | string[] | 在预构建中强制排除的依赖项。 |
optimizeDeps.include | string[] | 默认情况下,不在 node_modules 中的,链接的包不会被预构建。使用此选项可强制预构建链接的包。 |
SSR选项
配置项 | 类型 | 说明 |
---|---|---|
ssr.external | string[] | 列出的是要为 SSR 强制外部化的依赖。 |
ssr.noExternal | string[] | 列出的是防止被 SSR 外部化依赖项。 |
环境变量
Vite 在一个特殊的 import.meta.env
对象上暴露环境变量。
在生产环境中,这些环境变量会在构建时被静态替换,因此请在引用它们时使用完全静态的字符串。动态的 key 将无法生效。例如,动态 key 取值
import.meta.env[key]
是无效的。
.env文件
Vite 使用 dotenv 在项目根目录下从以下文件加载额外的环境变量
.env # 所有情况下都会加载
.env.local # 所有情况下都会加载,但会被 git 忽略
.env.[mode] # 只在指定模式下加载
.env.[mode].local # 只在指定模式下加载,但会被 git 忽略
复制代码
加载的环境变量也会通过 import.meta.env
暴露给客户端源码。为了防止意外地将一些环境变量泄漏到客户端,只有以 VITE_
为前缀的变量才会暴露给经过 vite 处理的代码。
Vite插件API
默认情况下插件在部署(serve)和构建(build)模式中都会调用。如果插件只需要在服务或构建期间有条件地应用,请使用 apply
属性指明它们仅在 'build'
或 'serve'
模式时调用:
// vite.config.js
import typescript2 from 'rollup-plugin-typescript2'
export default {
plugins: [
{
...typescript2(),
apply: 'build'
}
]
}
复制代码
通用钩子
- 在服务器启动时被调用 options、buildStart
- 在每个传入模块请求时被调用 resolveId load transform
- 服务器关闭时被调用 buildEnd closeBundle
Vite特有钩子
开发时,vite创建一个插件容器按照顺序调用的各个钩子
- config:修改vite配置
- configResolved:vite配置确认
- configureServer:用于配置dev server
- transformIndexHtml:用于转换宿主页
- resolveId:创建自定义确认函数,常用语定位第三方依赖
- load:创建自定义加载函数,可用于返回自定义的内容
- transform:可用于转换已加载的模块内容
- handleHotUpdate:自定义HMR更新时调用
原理浅析
开启一个服务器,对不同请求进行响应,重写裸模块路径。
const Koa = require('koa')
const fs = require('fs')
const path = require('path')
const compilerSfc = require('@vue/compiler-sfc')
const compilerDom = require('@vue/compiler-dom')
const app = new Koa()
function rewriteImport (content) {
return content.replace(/from ['"]([^'"]+)['"]/g, function ($0, $1) {
if ($1.startsWith('./') || $1.startsWith('../') || $1.startsWith('/')) {
return $0
}
return ` from '/@modules/${$1}'`
})
}
app.use(async ctx => {
const url = ctx.request.url
const query = ctx.request.query
if (url === '/') {
// 处理html文件
ctx.type = 'text/html'
ctx.body = fs.readFileSync('./index.html', 'utf-8')
} else if (url.endsWith('.js')) {
// 处理js文件
const filePath = path.join(__dirname, url)
ctx.type = 'application/javascript'
ctx.body = rewriteImport(fs.readFileSync(filePath, 'utf-8'))
} else if (url.startsWith('/@modules')) {
// 处理依赖
const moduleName = url.replace('/@modules/', '')
const prefix = path.join(__dirname, './node_modules', moduleName)
const module = require(path.join(prefix, '/package.json')).module
const modulePath = path.join(prefix, module)
const file = fs.readFileSync(modulePath, 'utf-8')
ctx.type = 'application/javascript'
ctx.body = rewriteImport(file)
} else if (url.indexOf('.vue') > -1) {
// 处理sfc文件
const filePath = path.join(__dirname, url.split('?')[0])
const res = compilerSfc.parse(fs.readFileSync(filePath, 'utf-8'))
console.log(res.descriptor, 'descriptor')
if (!query.type) {
const scriptContent = res.descriptor.script.content
const script = scriptContent.replace('export default ', 'const __script = ')
// 返回App.vue解析结果
ctx.type = 'application/javascript'
ctx.body = `
${rewriteImport(script)}
import { render as __render } from '${url}?type=template'
__script.render = __render
export default __script
`
}else if (query.type === 'template') {
// 模板内容
const template = res.descriptor.template.content
// 编译为render
const render = compilerDom.compile(template, { mode: 'module' }).code
ctx.type = 'application/javascript'
ctx.body = rewriteImport(render)
}
}
})
app.listen(3001, () => {
console.log('simple-vite start at port 3001')
})
复制代码
plugin-vue插件部分源码
function vuePlugin(rawOptions = {}) {
let options = __assign(__assign({
isProduction: process.env.NODE_ENV === "production"
}, rawOptions), {
root: process.cwd()
});
const filter = createFilter(rawOptions.include || /\.vue$/, rawOptions.exclude);
return {
name: "vite:vue",
handleHotUpdate(ctx) {
if (!filter(ctx.file)) {
return;
}
return handleHotUpdate(ctx);
},
config(config) {
return {
define: __assign({
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false
}, config.define),
ssr: {
external: ["vue", "@vue/server-renderer"]
}
};
},
configResolved(config) {
options = __assign(__assign({}, options), {
root: config.root,
isProduction: config.isProduction
});
},
configureServer(server) {
options.devServer = server;
},
async resolveId(id, importer) {
if (parseVueRequest(id).query.vue) {
return id;
}
},
load(id, ssr = !!options.ssr) {
const {filename, query} = parseVueRequest(id);
if (query.vue) {
if (query.src) {
return import_fs.default.readFileSync(filename, "utf-8");
}
const descriptor = getDescriptor(filename);
let block;
if (query.type === "script") {
block = getResolvedScript(descriptor, ssr);
} else if (query.type === "template") {
block = descriptor.template;
} else if (query.type === "style") {
block = descriptor.styles[query.index];
} else if (query.index != null) {
block = descriptor.customBlocks[query.index];
}
if (block) {
return {
code: block.content,
map: block.map
};
}
}
},
transform(code, id, ssr = !!options.ssr) {
const {filename, query} = parseVueRequest(id);
if (!query.vue && !filter(filename) || query.raw) {
return;
}
if (!query.vue) {
return transformMain(code, filename, options, this, ssr);
} else {
const descriptor = getDescriptor(filename);
if (query.type === "template") {
return transformTemplateAsModule(code, descriptor, options, this, ssr);
} else if (query.type === "style") {
return transformStyle(code, descriptor, Number(query.index), options, this);
}
}
}
};
}
复制代码
SSR
SSR 支持还处于试验阶段,你可能会遇到 bug 和不受支持的用例。请考虑你可能承担的风险。
源码结构:
一个典型的 SSR 应用应该有如下的源文件结构:
- index.html
- src/
- main.js # 导出环境无关的(通用的)应用代码
- entry-client.js # 将应用挂载到一个 DOM 元素上
- entry-server.js # 使用某框架的 SSR API 渲染该应用
复制代码
`
index.html
将需要引用 entry-client.js
并包含一个占位标记供给服务端渲染时注入:
<div id="app"><!--ssr-outlet--></div>
<script type="module" src="/src/entry-client.js"></script>
复制代码
const fs = require('fs')
const path = require('path')
const express = require('express')
const { createServer: createViteServer } = require('vite')
async function createServer() {
const app = express()
// 以中间件模式创建 vite 应用,这将禁用 Vite 自身的 HTML 服务逻辑
// 并让上级服务器接管控制
const vite = await createViteServer({
server: { middlewareMode: true }
})
// 使用 vite 的 Connect 实例作为中间件
app.use(vite.middlewares)
app.use('*', async (req, res) => {
const url = req.originalUrl
try {
// 1. 读取 index.html
let template = fs.readFileSync(
path.resolve(__dirname, 'index.html'),
'utf-8'
)
// 2. 应用 vite HTML 转换。这将会注入 vite HMR 客户端,and
// 同时也会从 Vite 插件应用 HTML 转换。
// 例如:@vitejs/plugin-react-refresh 中的 global preambles
template = await vite.transformIndexHtml(url, template)
// 3. 加载服务器入口。vite.ssrLoadModule 将自动转换
// 你的 ESM 源码将在 Node.js 也可用了!无需打包
// 并提供类似 HMR 的根据情况随时失效。
const { render } = await vite.ssrLoadModule('/src/entry-server.js')
// 4. 渲染应用的 HTML。这假设 entry-server.js 导出的 `render`
// 函数调用了相应 framework 的 SSR API。
// 例如 ReactDOMServer.renderToString()
const appHtml = await render(url)
// 5. 注入应用渲染的 HTML 到模板中。
const html = template.replace(`<!--ssr-outlet-->`, appHtml)
// 6. 将渲染完成的 HTML 返回
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
} catch (e) {
// 如果捕获到了一个错误,让 vite 来修复该堆栈,这样它就可以映射回
// 你的实际源码中。
vite.ssrFixStacktrace(e)
console.error(e)
res.status(500).end(e.message)
}
})
app.listen(3000)
}
createServer()
复制代码
生产构建:
{
"scripts": {
"dev": "node server",
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --outDir dist/server --ssr src/entry-server.js "
}
}
复制代码
注意使用 --ssr
标志表明这将会是一个 SSR 构建。它也应该能指明 SSR 入口。