一个前端构建打包工具
Vite的更新比较频繁,要想能够使用的得心应手,首先要对Vite的版本迭代时间点,以及每一代相对于之前的变更,要有认知
Vite发布历史
-
2020.4.21 vite 0.1随vue3.0一同发布,1.0并未落地, 作者意识到vite可以与框架无关,作为一种更通用的cli
-
2021.2.16,vite 2.0发布
- vite和框架解耦
- 选择rollup兼容插件api
- 采用全新SSR运行时
- 基于esbuild依赖预打包方案
-
2022.7.13 , vite 3.0发布
-
2022.12.9, vite4发布, 2023.3.16, vite 4.2发布
-
2023.11.16,vite 5.0发布
Vite特点
- 配置使用比较简单,不用深入接触webpack那么多的配置
- 默认集成了dev-server
- 启动项目很快,编译很快
- 便于扩展,兼容rollup的插件
类似产品:
- Snowpack
- WMR
- @web/dev-server
区别: High Level API
- webpack,rollup关注于如何构建细节功能实现,如文件加载如何编译加载
- vite没有自己的编译能力
- 源于esbuild, rollup, 只是集成了rollup功能, 启动了dev-server
- 完全基于ESM加载方式的开发时
- 减少了很多配置量,如核心dev server,各种loader,内置build
不同于webpack从入口分析模块打包生成chunk, vite是从浏览器向dev server发起请求,他不关心文件后缀名,如script type="module" src="/src/main.jsx"
优势
不像webpack配置繁杂, cra需要eject, vue/cli通过configureWebpack和chainWebpack,vite有自身的插件系统,而且继承了rollup
常用命令
# 支持多种方式创建vite+vue应用,用vite创建的vue应用没有直接创建vue应用丰富
# 初始化vite应用,会安装@vitejs/create-app
npm init @vitejs/app # 如果报错了可以换一种方式
npm create vue@latest # 能选择更多选项(我的h5商城就是用这个创建,已经废弃了@vue/lick, 是vite)
npm create vite@latest vue-app -- --tempalte vue
# 启动服务
vite
# 预览
vite preview
# 打包
vite build
需要注意,vite2版本使用npm i 会出现esbuild的报错问题, 用yarn
插件注意事项:通过安装vscode插件vue-official可以避免写shims-vue.d.ts,注意vue3项目要禁用掉vue2的插件vetur,volar插件已废弃
两个重要插件
@vitejs/plugin-vue:加载vue文件入口
@vitejs/plugin-vue-jsx:加载jsx文件入口
import { defineComponent } from "vue"
export default defineComponent({
setup() {
return () => {
return <div>hello vue3 jsx</div>
}
}
}
// 然后mai.ts引入app.jsx
Vite创建vue2
npm init @vitejs/app
此方法没有vue2的预设项,借助underfin/vite-plugin-vue2
Vite创建React项目 Vite对React的支持比Vue2还优先,借助插件@vitejs/plugin-react-refresh, 内部是FastRefresh,和react-hot-loader有关
FastRefresh的优势
- 解决了很多rhl无法解决的问题
- 速度很快
- 支持局部更新
yarn create @vitejs/app
import { defineConifg } from "vite"
import reactRefresh from "@vitejs/plugin-react-refresh"
export default defineConifg({
plugins: [reactRefresh()]
})
nodejs版本过高可能会运行失败
Vite+CSS
- Vite默认支持css,不需要做额外配置,如在jsx中直接引入css即可
- 推荐使用css变量
- 已经集成了postcss
import './styles/index.css
声明postcss.config.js文件
module.exports = {
plugins: [require("@postcss-plugins/console")]
}
然后在样式表中就能够使用这个插件特性
:root {
--main-bg-color: red;
}
:root {
@console.error hello root
color: var(--main-bg-color);
}
@import alias / resolve alias
export default defineConfig({
resolve: {
alias: {
'@styles': '/src/styles' // src前面的/不能省略,项目根目录
}
}
})
css中的@import url也可以使用该alias
@import url("@styles/other.css")
css modules
默认提供支持
CSS pre-processors
天然支持,无需配置,只需要下载对应的包就好
yarn add less
typescript继承
Vite天然支持typescript,无需额外配置,开发环境中Vite使用esbuild,只编译,不校验,esbuild本身也是支持ts语法,如果想要进行ts校验
只能手动tsc --noEmit
改动下build命令, 如何集成eslint+prettier参考后文
"scripts": {
"build": "tsc --noEmit && vite build"
}
tsconfig.json
{
"compilerOptions": {
"target": "esnext",
// 必配
"module": "esnext",
// 通过node方式解析模块
"moduleResolution": "node",
"strict": true,
// ts解析jsx只会遵循react语法规范,不会使用插件
// 对于vue来说,和react jsx规范不符,必须使用vue jsx插件
"jsx": "preserve",
// 可以直接调试ts代码
"sourceMap": true,
// 是否可以直接import json
"resolveJsonModule": true,
// 避免import * as react from "react"
"esModuleInterop": true,
// 包括哪些类型库
"lib": ["esnext", "dom"]
},
// 必填,否则ts不知道校验哪里
"include": ["src/**/*.ts","src/**/*.d.ts","src/**/*.tsx"]
}
vue-tsc for SFC
TS是不会解析vue文件中的ts代码,需要借助插件vue-tsx
vue-tsc --noEmit -p tsconfig.*.json
ts的isolatedModules
因为vite提供的ts编译只是编译单文件语法, 而ts可以关联不同模块之间的信息
设置了isolatedModules: true以后, 有以下代码情形会ts抛错
- exports of non-value idenfifiers
当在一个ts文件中引入了另一个ts文件,如import {a}, export {a}, 使用起来会有问题
- references to const enum members
import { A } from "./types"
// 会被直接干掉,对于vite的es buil提供的ts编译器不识别这个语法
decalre const enum Num {
First = 0,
Second = 1
}
export const a: A = {
name: "jason",
age: Num.first
}
- non-module files
模块中必须import或export {}
client types
vite提供了一些内置对象,如import.meta.url。此时没有对应的类型声明
设置tsconfig.compilerOptions.types = ["vite/client"]
设置了这个types,以下内容的代码会的得到vscode的智能提示
- asset imports,没有client types无法引入png文件,会返回string类型
- env
- HMR API
import.meta.hot
其他特性
导入文件可以省略js等后缀名
静态文件处理
图片资源等静态资源
types:import x from '此处格式不同表现会不同'
- url
import { a } from 'test'
此时a是一个变量,如果改成
import test from 'test?url'
test是路径/src/test.js
- raw
返回文件内容
import test from 'test?raw'
返回文件内容字符串
- worker/worker inline
针对web work,vite里使用web worker很简单
导入json:
跟webpack中使用方式一致
导入web assembly: 借助assemblyscript.org 提供的asc命令可以将代码转换成assembly代码,wasm二进制文件
在vite中导入wasm, 返回一个promise
// export function fib ...
import init from './fib.wasm'
init().then((m) => {
m.fib(10)
})
Vite集成eslint和prettier
npm init @eslint/config
或者
yarn add eslint-config-stardard eslint-plugin-import eslint-plugin-promise eslint-plugin-node
- eslint-config-stardard
规范代码风格,stardardjs的标准,主要是一些基础规则和避免常见错误
- eslint-plugin-import
对 import导入的模块格式进行校验,如模块是否存在,路径大小写是否正确,是否使用正确别名等
- eslint-plugin-promise
为promise的使用提供最佳实践,检查不必要的错误
- eslint-plugin-node
在Node.js运行时中的规则校验,如模块循环引用,文件路径是否存在,错误捕获,全局对象是否安全等
module.exports = {
extends: ["standard"],
rules: {}
}
此外,还可以组合extends和plugins使用
- react可以使用react-app的预设规则即presets,vue3可以使用plugin:vue/vue3-essential的预设
- 对于typescript, 使用plugin:@typescript-eslint的预设
- plugins当中可以使用vue, @typescript-esilnt, prettier等插件, 甚至你可以指定eslint 特定的parser,如@typescript-eslint/parser, vue-eslint-parser等包,可以提供更准确的ts语法解析,这样在代码中可以使用一些新特性,装饰器,类型注解等, 这些不在本文讨论范围内哈
勾选上vscoce的format on save, 设置default formatter为prettier 则保存自动格式化
"scripts": {
"lint": "eslint --ext js src/"
}
针对eslint和prettier冲突问题, 如eslint报警告函数名后面要括号, 但是prettier不遵循这个规则,此时保存代码仍然eslint提示,可以通过修改eslint规则或者引入eslint-config-prettier
集成husky
yarn add husky -D
npx husky install
npx husky add .husky/pre-commit "npm run lint"
环境变量
内置环境变量import.meta.env挂载了如下属性
- MODE
- BASE_URL
- PROD: 是否正式环境
- DEV
- SSR: 是否服务端渲染环境
production环境是没有import.meta.env这个对象
自定义env
项目根目录下新建.env, 需要使用VITE_开头,包括不限于:
.env.production
.env.development
.env.development.local
如果要设置其他自定义环境, 如.env.test,则启动脚本需改为
vite --mode test
在代码中引入自定义的环境变量, 如VITE_TITLE=XXX, 没有类型提示, 需要:
在src/vite-env.d.ts中
/// <reference types="vite/client" />
interface ImportMetaEnv {
VITE_TITLE: string
}
以上是vite中一些开箱即用的功能,以下是一些更高级的功能
Vite中的热更新
vite cli生成的项目默认是开启热更新的,并且针对不同框架来实现的,如在vue3应用中是通过插件vueJsx实现
import { defineConfig } from "vite"
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
export default defineConfig({
plugins: [vue(), vueJsx()]
})
})
而对于vite cli生成的普通项目来说, 要手动实现HMR
export function render() {
document.querySelector("#app").innerHtml = `
<h1>hello vite</h1>
`
}
render()
if (import.meta.hot) {
// 代表这个文件会接受自己的热更新
import.meta.hot.accept((newModule) => {
newModule.render() // 由于闭包,不能直接render()
})
}
原理就是当修改main.js代码时,浏览器会重新请求main.js?import来替换老的js
glob import批量导入
来自于fast-glob库
const modules = import.meta.glob('./glob/*') // 会经过vite编译
// 或者globalEager
应用场景:多语言支持的json文件批量导入
Vite中的预编译优化
对于node_modules中的库会在首次启动之前,编译并缓存下来
CommonJs to ESM
如node_moduels/react/index.js中是采用commonjs管理,生成的.vite/react.js中,会做一些转化
import { defineConifg } from "vite"
import reactRefresh from "@vitejs/plugin-react-refresh"
export default defineConifg({
plugins: [reactRefresh()],
// 指定哪些项目需要经过/不经过预编译
optimizeDeps: {
// includes: [], vite会扫描,有些场景会扫描失败, 如插件产生的
exclude: []
}
})
零散文件打包到一起
比如引入了lodash-es, 如果配置忽略lodash-es,由于lodash模块中分成了很多包,导致浏览器在请求lodash时,会有很多的网络请求
观察网络请求会发现对于main.js其实设置了cache-control: no-cache。对于node_module中的包,时开启了强缓存
服务端集成/非nodejs服务
场景:有服务端模板引擎渲染的地方,html是服务返回的,如何把script插入html中,如有以下一段pug代码
html
head
title = title
body
div(id="app")
script(src type="module" src="http://localhost:3000/@vite/client")
script(src type="module" src="http://localhost:3000/src/main.js")
注意应用中图片路径会出问题,将前端应用放在后端目录下面即可
Node.js集成Vite开发时SSR
以react项目为例
项目根目录新建server.js
const exporess = require('express')
const app = express()
const { createServer: createViteServer } from 'vite'
createViteServer({
// 非常重要,相当于启动vite的dev server
server: {
middlewareMode: 'html' // 另一个值是ssr
}
}).then((vite) => {
app.use(vite.middlewares)
app.listen(4000)
})
于ssr mode, 读取html并返回到浏览器, 由于html不是vite server返回的,只有js,没有渲染过的内容,页面无法正常工作
const exporess = require('express')
const app = express()
const { createServer: createViteServer } from 'vite'
createViteServer({
// 非常重要,相当于启动vite的dev server
server: {
middlewareMode: 'ssr'
}
}).then((vite) => {
app.use(vite.middlewares)
// 注册路由
app.get('*', async (req, res) => {
// 1.fs读取index.html模板
let temlate = ***
const { render } = await vite.ssrLoadModule('src/server-entry.jsx')
const html = await render(req.url)
const res = template.replace('div id="root"中间的注释', html)
res.send(res)
})
app.listen(4000)
})
新建server-entry.jsx
import ReactDOMServer from 'react-dom/server'
import { StaticRouter } from 'react-router-dom'
import App from './App'
export function render(url, context) {
return ReactDOMServer.renderToString(
<StaticRouter location={url} context={context}>
<App />
</StaticRouter>
)
}
经过了以上步骤还不够,HMR会失效并且报错,你需要
// 注册路由
app.get('*', async (req, res) => {
// 1.fs读取html模板
let temlate = ***
// 这里
template = await vite.transformIndexHtml(req.url, tempalte)
const { render } = await vite.ssrLoadModule('src/server-entry.jsx')
const html = await render(req.url)
const res = template.replace('div id="root"中间的注释', html)
res.send(res)
})
HMR正常工作
Node.js集成Vite正式环境SSR
"scripts": {
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --outeDir dist/server --ssr src/server-entry.jsx"
}
改动点就是读取的html是打包client目录中的html
const fs = require('fs')
const template = fs.readFileSync('dist/client/index.html', 'utf-8')
app.get('*', async (req, res) => {
const render = require('./dist/server/server-entry.js').render
const context = {}
const html = await render(req.url, context)
const res = template.replace('div id="root"中间的注释', html)
res.send(res)
})
然而这会导致一个问题:请求/assets/vendors.js路径错误,没有做资源映射
app.use(express.static('dist/client'))
最后根据环境变量整合开发环境和生产环境逻辑
Vite其他配置项
以下配置只是简单列举了一下,具体的参数用法参照官网即可,可以跳到本文尾巴查看vite的变更历史细节: cn.vitejs.dev/config/
共享配置项
- root 项目根目录位置,默认process.cwd()
- base 类似webpack中的pubicPath
- mode
- define 类似于rollup的definePlugin,定义全局常量替换方式
export default defineConfig({ define: { APP_VERSION: JSON.stringify('v1.0.0'), API_URL: 'window.__backend_api_url', }, })
- publicDir 静态资源存放目录,默认”public“
- cacheDir 存储缓存文件的目录,默认node_modules/.vite
- resolve.dedupe 程序中安装了两个不同版本同分依赖, 指定引用版本
- resolve.conditions
- resolve.mainFields package.json中的入口文件名,即requir引入的
- resolve.extensions 要省略的扩展名
- css.modules, css.postcss, css.preprocessorOptions
- json.namedExports 关掉后就不能从json中直接导入某个变量
- json.stringify 大json文件时开启这个性能会好
- esbuild
- assetsInclude 告诉是一个静态文件,后续放到assets中可以引入
- logLevel 控制台日志级别,默认info
- clearScreen 是否清屏
- envDir 默认根目录
开发服务器选项
- server 构建选项
- build.target 构建的浏览器兼容目标,默认modules, 另一个选项esnext
- build.polyfillDynamicImport
- build.ourDir 输出路径
- build.assetDir 静态资源路径
- build.assetsInline_limit 内联引用资源大小
- build.cssCodeSplit 启用、禁用css代码拆分,默认true
- build.sourcemap
- build.rollupOptions
- build.commonjsOptions
- build.lib 是否构建为库
lib.formats: 'es' | 'cjs' | 'umd' | 'iife', 默认es + umd
lib: {
// Could also be a dictionary or array of multiple entry points
entry: resolve(__dirname, 'lib/main.js'),
name: 'MyLib',
// the proper extensions will be added
fileName: 'my-lib',
}
然后库下面的package.json中推荐配置
{
"name": "my-lib",
"type": "module",
"files": ["dist"],
"main": "./dist/my-lib.umd.cjs",
"module": "./dist/my-lib.js",
"exports": {
".": {
"import": "./dist/my-lib.js",
"require": "./dist/my-lib.umd.cjs"
}
}
}
- build.manifest
- build.minify boolean | 'terser' | 'esbuild'
- build.terserOptions 传递给 Terser 的更多 minify 选项。
- build.write 禁用磁盘写入
- build.emptyOutDir 构建之前是否清空outDir
- build.brotliSize 压缩大小报告
- build.chunkSizeWarningLimit chunk大小警告限制
- build.watch 开发插件使用
依赖优化选项
- optimizeDeps.entries 默认情况,vite会抓取index.html检测需要预构建的依赖项,如果指定build.rollupOptions.input, vite将会转而去抓取这些入口点,如果两者都不合你意,则使用此选项指定自定义条目
- optimizeDeps.include
- optimizeDeps.exclude 预构建中强制排除的依赖项
- optimizeDeps.keepNames 设置为true,可在函数和类上保留name,以防被重命名
SSR选项
- rollup
- rollup的一些简单入门可参考我的这篇文章:
- 开源类库优先选择
- 以ESM标准为目标的构建工具
- 开创了tree shaking
常用命令
rollup -i index.js # 输出结果到命令行, -i/--input可以指定多个,搭配--dir使用
rollup -i index.js --file dist.js
# --format umd/cjs/es/iife 指定模块化
# --watch 监听
# --plugin json 指定插件
rollup.config.js
export default {
// 入口文件路径
input: './index.js', // 如果定义多入口:input: {entry1: 'resolve(__dirname, 'index.html'),', entry2: ''}
// external: ['react'], 同webpack类似
// 输出配置, 相应的如果是多入口,output: {dir: 'dist', chunkFileNames: 'chunk格式[name].js', entryFileNames: '入口文件名[name].js'}
output:{
// 输出文件路径
file: 'dist/bundle.js'
// 输出格式
format: 'iife', // 默认是es + umd
// 代码最上面的说明注释
banner: '',
globals: {
// // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
vue: 'Vue'
}
},
plugins: []
}
可以导出对象, 数组 output也有plugin,是打包后的操作,如代码压缩
output也可以设置为一个数组
常用插件
- @rollup/plugin-alias
- @rollup/plugin-babel
- @rollup/plugin-replace
- @rollup/plugin-typescript 依赖typescript, tslib
- @rollup/plugin-eslint
- @rollup/plugin-image
- @rollup/plugin-strip
- @rollup/plugin-wasm
esbuild
esbuild index.js
# --outfile dist.js
# --bundle,打包操作,不仅仅是编译
# --target=esnext
# --platform=node
# --format=cjs
# --watch
# --define:TEST=12 # 类似于replacement
# --loader:.png=dataurl
- esbuild对es的支持是不完整的,const编译为es5报错
- 支持jsx,也有treeshaking功能,只在bundle时tree shaking
Vite 3相对Vite2的变更
不支持Nodejs12、13、15。14.18+ / 16+
支持的浏览器版本
Chrome >=87
Firefox >=78
Safari >=13
Edge >=88
配置项更改
- alias => resolve.alias
- dedupe => resolve.dedupe
- build.base => base
- build.brotliSize => build.reportCompressedSize
- build.cleanCssOptions: vite现在使用esbuild压缩css
dev server更改
- 默认端口变成了5173
- 现在的默认主机名是localhost而不是127.0.0.1
SSR 更改
- 使用ESM构建SSR,所有依赖都外部化
一般变更
- SSR和lib模式下的JS文件扩展名现在根据其格式和包类型为输出JS entries 和chunks使用有效的扩展名(JS, mjs或cjs)。
- Terser现在是一个可选的依赖项。如果您正在使用build。Minify: 'terser',你需要安装它。
import.meta.glob的一些使用变更
web assembly的用法
-import init from 'example.wasm'
+import init from 'example.wasm?init'
-init().then((exports) => {
+init().then(({ exports }) => {
exports.test()
})
https证书问题
vite 2中自动创建证书, vite3可以手动创建证书或者仍然可以采用自动生成
Vite 4相对于Vite 3的变更
- 使用Rollup 3
- Rollup 3基本与2兼容
主要变更
导入css为字符串
在Vite 3中,导入. CSS文件的默认导出可能会导致CSS的双重加载。
import cssString from './global.css'
这种双重加载可能会发生,因为将会发出一个。CSS文件,并且很可能CSS字符串也将被应用程序代码使用,例如,由框架运行时注入。从Vite 4开始,.css默认导出已被弃用。在这种情况下需要使用内联查询后缀修饰符,因为它不会发出导入的.css样式。
import stuff from './global.css?inline'
生产构建变更
Vite构建现在将始终为生产环境构建,而不管传递了 --mode。以前,将模式更改为生产模式以外的模式将导致开发构建。如果您仍然希望为开发构建,您可以在. ENV文件中设置NODE ENV=development。在.env.{mode}文件中
在这个变化中,如果已经定义了ENV,那么vite dev和vite build将不再覆盖process.env.NODE。因此,如果您在构建之前设置了process.env.NODE ENV = 'development',那么它也将为开发而构建。这在并行运行多个构建或开发服务器时提供了更多的控制。
环境变量
Vite现在使用dotenv 16和dotenv-expand 9(以前使用dotenv 14和dotenv-expand 5)。如果您有一个包含#或'的值,则需要用引号将它们括起来。
Vite 5相对于Vite 4的变更
Node.js 支持
不再支持Node.js 14/16/17/19, 需要Node.js 18/20+
Rollup4 支持
- Vite 5使用Rolleup 4
- 导入断言(assertions 属性)已被重命名为导入属性(attributes 属性)。
- 不再支持 Acorn 插件。
- 对于 Vite 插件,this.resolve 的 skipSelf 选项现在默认为 true。
- 对于 Vite 插件,this.parse 现在只支持 allowReturnOutsideFunction 选项。
废弃CommonJS Node API
vite.config.js 内容使用ESM 语法。或者确保package.json 有type: module选项,或者使用.mjs/.mts,如vite.config.mjs
重新设计 define 和 import.meta.env.* 的替换策略
在 Vite 4 中,define 和 import.meta.env.* 特性在开发和构建中使用的是不同的替换策略:
在开发时,这两个特性分别作为全局变量注入到 globalThis 和 import.meta 中。
在构建时,这两个特性都使用正则表达式进行静态替换。
这导致在尝试访问这些变量时,开发和构建存在一致性问题,有时甚至导致构建失败。例如:
// vite.config.js
export default defineConfig({
define: {
__APP_VERSION__: JSON.stringify('1.0.0'),
},
})
const data = { __APP_VERSION__ }
// 开发:{ __APP_VERSION__: "1.0.0" } ✅
// 构建:{ "1.0.0" } ❌
const docs = 'I like import.meta.env.MODE'
// 开发:"I like import.meta.env.MODE" ✅
// 构建:"I like "production"" ❌
Vite 5 通过在构建中使用 esbuild 来处理替换,使其与开发行为保持一致。
这个改动不应该影响大部分设置,因为已经在文档中说明了 define 的值应该遵循 esbuild 的语法: