npm i vite -Dpackage.json
{
"scripts": {
"dev": "vite",
"build": "vite build",
}
}处理过程中遇到非绝对路径、相对路径的引用,则尝试补全路径,最终路径会变为"/node_modules/.vite/**/*.js"
源码:
import _ from 'lodash'补全后:
import __vite__cjsImport1_lodash from "/node_modules/.vite/deps/lodash.js?v=b9b872b6";
寻找依赖的过程自当前目录开始向父级查找,为了方便浏览器根据补全的路径来发起请求,vite 会对依赖包进行预构建
因为开发环境和生产环境下 Vite 的运行是不同的
开发环境中,每次启动 vite 都会进行一次预构建,因此重构的相对路径一定是正确的
生产环境中 vite 会全权交由 rollup 库去完成生产打包,而不会去进行预构建的处理
依赖预构建:
vite 在找到依赖包后,调用 esbuild 将代码进行规范,然后输出到/node_modules/.vite/deps/目录下
预构建解决的三个问题:
- 将不同的包的不同导出方式统一到 ES6 module
- 对路径的重构可以统一为
/node_modules/.vite/deps/,方便了路径的重写 - 解决了网络的多包传输性能问题
vite.config.js
import { defineConfig } from 'vite'
// 可以直接导出对象 但是为了代码规范和提示 建议使用defineConfig
// defineConfig的参数可以是对象 也可以是一个函数 这个函数的返回值必须是UserConfig类型
export default defineConfig({
// ...
})规范的第二种方式
/**
* @type import('vite').UserConfig
*/
const viteConfig = {
}
export default viteConfig在项目中将基础配置vite.base.config.js、开发配置vite.dev.config.js和生产配置vite.prod.config.js写到不同文件中
vite.config.js
import { defineConfig } from 'vite'
import viteBaseConfig from './vite.base.config'
import viteDevConfig from './vite.dev.config'
import viteProdConfig from './vite.prod.config'
const envResolver = {
'build': () => Object.assign({}, viteBaseConfig, viteDevConfig),
'serve': () => Object.assign({}, viteBaseConfig, viteProdConfig),
}
// 函数可以接受参数 通过该参数的command属性可以获取开发环境
// command: 'build' | 'serve'
export default defineConfig(({ command }) => {
return envResolver[command]()
})用于在开发、测试、预发布、灰度和生产等环境下,区分根据环境发生变化的变量值
vite 会使用第三方库 dotenv
dotenv会自动读取.env文件,解析其中的环境变量
如果是服务器(Node.js,如 vite.config.js),会将其注入到process对象下(process.env)
但是 vite.config.js 文件中会返回项目有关的配置,这些配置可能影响到 .env 的解析,因此在 vite.config.js 中无法通过process.env去读取环境变量,因为这个时候环境变量还没挂载到process上。相关配置包括:
- root
- envDir
如果希望立即读取环境变量,那么可以通过loadEnv(mode: string, envDir: string, prefixes?: string | string[])加载 env 文件内容
其中:
mode:按后缀名覆盖.env文件的变量envDir:指定配置文件所在目录,一般为项目根目录prefixes:只加载指定前缀的变量,如果为''则表示全部加载
loadEnv()运行时,首先将读取指定目录的.env文件,不解析
import { defineConfig, loadEnv } from 'vite'
// mode参数相当于 npm run dev --mode <mode>
// 开发环境下默认为 development
// 生产环境下默认为 production
export default defineConfig(({mode}) => {
const env = loadEnv(mode, process.cwd(), '')
console.log(env)
})如果是客户端,vite 会将环境变量注入到import.meta.env中
而 vite 做了拦截,防止我们将隐私变量注入到 env 中
因此默认前缀为 VITE_ 的变量才会被挂载
如果希望自定义前缀,可以通过 envPrefix 去配置
import { defineConfig } from 'vite'
export default defineConfig({
envPrefix: 'VITE_',
})vite 天生支持对 css 的处理,在 vite 遇到 css 文件时会进行如下操作:
如果是<link>发起的请求,则原封不动返回 css 文件
如果是在 js 文件中通过模块引用的 css 文件,则会进行如下操作:
- 在接收到 css 文件请求后,通过 fs 模块去读取对应的 css 文件内容
- 创建一个
<style>,然后将 css 文件内容直接复制到标签中 - 将
<style>插入到 html 文件的<head>中 - 将该 css 文件中的内容直接替换为 js 脚本,便于热更新或 css 模块化,同时设置 Content-Type 为 text/javascript,从而让浏览器以 js 的形式去解析该 css 后缀的文件
关于 css 的模块化,设计目的在于解决多人开发时,不同组件下 css 文件类名冲突的问题
当文件名后缀为.module.css时,表示需要开启 css 模块化
在 js 文件中引入.module.css文件时(import CssModule from 'a.module.css'),会进行如下操作:
- 将所有类名进行一定的规则替换(如
.footer ===> ._footer_i22st_1) - 创建一个映射对象,将
footer映射到_footer_i22st_1 - 将替换后的内容写入
<script>中,再插入<head> - 将
a.module.css的内容全部抹除,替换为 js 脚本,默认导出映射对象
如果希望使用的是 less,那么需要自己安装插件,帮助解析 less,如果需要对 less 进行模块化,需要将后缀改为.module.less
在 vite 中可以通过 css 配置项来控制整个项目对 css 文件的处理方式
css 配置项最终会传给 postcss-modules,所以配置项可以在官方文档查询
export default defineConfig({
css: {
modules: {
// css文件默认导出的对象上属性的类型 默认包含dashes
// 'camelCase' | 'camelCaseOnly' | 'dashes' | 'dashesOnly'
localsConvention: '<type>',
// 一般不用 默认为local 会开启模块化
// 值为global时关闭css文件默认导出 同时css直接插入<head>
// 'local' | 'global'
scopeBehaviour: 'local',
// 定义类名展示的格式 具体占位方式去官方文档看
generateScopedName: '', // string || (name, filename, css) => string
// 设定哈希前缀 参与类名哈希的生成
hashPrefix: '<prefix>',
// 希望挂至全局的css文件路径
globalModulePaths: [],
}
}
})通过 preprocessorOptions 配置项可以设定 css 的预处理器参数,参数具体如何设置看各预处理器的官网,如 less
export default defineConfig({
css: {
preprocessorOptions: {
less: {
},
sass: {
}
}
}
})关于 source map 解决的问题:因为代码经过编译后,会压缩为一行简单的代码,通过这行压缩的代码运行前端程序,此时如果发生报错,显示出错的行为压缩代码的行数
如果添加了 source map,那么编译后的代码会添加一行注释,指向原文件的索引,当发生报错时,会返回原文件实际行数
在 vite 中 devSourcemap 默认为false,可以配置开启
export default defineConfig({
css: {
devSourcemap: true
}
})关于 postcss
由于 vite 对 postcss 有良好的支持,所以需要对 postcss 有足够的了解
postcss 设计的目的类比于 bable,也就是保证 css 代码运行的万无一失
预处理器无法考虑到大部分问题,比如:
- 对 css 新语法的降级处理
- 前缀补齐(
--webkit等),实现兼容性
postcss 希望维护一个管道,通过这个管道实现 css 的处理,使得 css 能够通过一些插件编译成目标文件,规避运行时可能出现的大部分问题(原本计划取代 less 与 sass 等预处理器,后来考虑到预处理器更新时自身插件也需要同步更新,因此民间维护的预处理器插件停止维护,此后 postcss 被称为后处理器)
postcss 所做的工作,就是将开发者提供的 css --> 利用 less、sass 等预处理器将语法编译 --> 对未来高级 css 语法进行降级 --> 前缀补全 --> 浏览器客户端
# 安装脚手架和 postcss 本体
# 其中脚手架提供了一系列命令
npm i postcss-cli postcss -D配置项具体参考 postcss-cli
插件预设的目的是,集成一系列开发常用的插件,帮助开发者快速搭建好 postcss 环境,插件预设详见 https://github.com/postcss/postcss/blob/main/docs/plugins.md
# 假设安装插件
npm i postcss-preset-env -Dpostcss.config.js
const postcssPresetEnv = require('postcss-preset-env')
module.exports = {
plugins: [postcssPresetEnv(/* pluginOptions */)],
}postcss index.css -o result.css可以直接在css.postcss中对 postcss 进行配置
也可以在 postcss.config.js 配置文件中编辑,但是优先级方面,vite.config.js 更高一些
export default defineConfig({
css: {
postcss: {
}
}
})通过别名可以简化路径配置,vite 内部会通过一系列字符串替换,将我们设定的别名替换为实际路径
vite.config.js
import { defineConfig } from 'vite'
import path from 'path'
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@assets': path.resolve(__dirname, './src/assets'),
}
}
})在服务器视角,除了动态 API,大部分的资源都可以被视作静态资源
而 vite 为了服务客户端,默认下静态资源应该包括视频、图像资源
vite 对于静态资源是开箱即用的
加载文件路径
import imgUrl from './assets/images/test.png'
const img = document.createElement('img')
img.src = imgUrl加载二进制流
// 不提供参数时 默认参数为url 此时提供参数raw
import bf from './assets/images/test.png?raw' // 此时获取到的是 bufferimport student from './assets/json/test.json'
// or
import { name, age } from './assets/json/test.json'vite 对 svg(scalable vector graphics,可伸缩矢量图)也是开箱即用的
svg 特点包括:
- svg 不会失真
- 尺寸小
缺点:无法包含更多图层以凸显层次丰富的图片信息
加载方式:
- 直接加载
import svgIconUrl from './assets/svgs/test.svg'
const img = document.createElement('img')
img.src = svgIconUrl- 加载标签
import svgRaw from './assets/svgs/test.svg/raw'
// 此时获取到的是<svg>的字符串形式
document.body.innerHTML = svgRaw
// 如此一来可以通过svg特有方法来控制样式等
const svgElement = document.querySelector('svg')
svgElement.onmouseenter = function () {
this.style.fill = 'cyan'
}关于打包后的文件会包含 hash:
因为浏览器有缓存机制,如果请求的文件和之前缓存的文件同名,那么不会发起请求,而是去缓存中查找文件,因此打包的结果要避免文件同名
哈希的产生和文件内容有关,如果内容没有发生变化,产生的哈希值就一致
export default defineConfig({
build: {
rollupOptions: { // rollup 相关配置
output: {
assetFileNames: '[name].[hash].[ext]', // 控制输出静态资源命名格式
}
},
assetsInlineLimit: 4096, // 当静态资源体积小于4kb时会转为 base64
outDir: 'dist', // 输出目录名称
assetsDir: 'assets', // 静态资源目录名称
emptyOutDir: true, // 每次构建项目前先清空输出目录
}
})vite 会在生命周期不同阶段调用不同的插件,达到不同的目的
export default function () {
return {
config(config, env) {
// config为vite所读取但是未生效的vite config
// env下包含两个属性
// env.mode: string ===> production || development
// env.command: string ===> build || serve
return { /* 你所希望新增或修改的配置 */ }
}
}
}export default function (options) {
return {
transformIndexHtml(html, ctx) {
// html为index.html的原始内容
// ctx为vite请求的执行期上下文
// 返回的内容会应用到真实的html当中
return html
}
}
}调整插件的执行顺序
export default function (options) {
return {
transformIndexHtml: {
enforce: 'pre',
transform(html, ctx) {
// html为index.html的原始内容
// ctx为vite请求的执行期上下文
// 返回的内容会应用到真实的html当中
return html
}
}
}
}export default function (options) {
return {
configureServer(server) {
// 可以对vite服务器注册中间件
server.middlewares.use((req, res, next) => {
// ...
})
}
}
}可以通过监测当前目录下包括 src 在内的所有目录,帮助我们自动生成别名
npm i vite-aliases -Dpackage.json
{
"type": "module"
}vite.config.js
import { defineConfig } from 'vite'
import { ViteAliases } from 'vite-aliases'
export default defineConfig({
plugins: [
ViteAliases()
]
})假设目录结构如下:
src
assets
components
pages
store
utils
则会对应地生成别名:
[
{
find: '@',
replacement: '${your_project_path}/src'
},
{
find: '@assets',
replacement: '${your_project_path}/src/assets'
},
{
find: '@components',
replacement: '${your_project_path}/src/components'
},
{
find: '@pages',
replacement: '${your_project_path}/src/pages'
},
{
find: '@store',
replacement: '${your_project_path}/src/store'
},
{
find: '@utils',
replacement: '${your_project_path}/src/utils'
},
]主要帮助我们控制index.html模板
npm i vite-plugin-html -Dvite.config.js
import { defineConfig } from 'vite'
import { createHtmlPlugin } from 'vite-plugin-html'
export default defineConfig({
plugins: [
createHtmlPlugin({
inject: { // 可以通过ejs注入数据
data: {
title: '首页',
},
},
})
]
})index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title><%= title %></title>
</head>
<body>
<script type="module" src="./test.js"></script>
</body>
</html>利用 mockjs 模拟海量数据
写死数据的缺陷:
- 无法进行海量数据测试
- 无法获取标准数据
- 无法感知 http 请求的异常
npm i mockjs -S
npm i vite-plugin-mock -Dvite.config.js
import { defineConfig } from 'vite'
import { viteMockServe } from 'vite-plugin-mock'
export default defineConfig({
plugins: [
viteMockServe({
mockPath: 'mock', // 目录
localEnabled: 'serve', // 限定在开发环境下 才启用mock拦截请求
})
]
})mock/index.js
import mockjs from "mockjs"
const userList = mockjs.mock({
'list|100': [{
'id|+1': 1,
'name': '@cname',
'age|1-100': 1,
'email': '@email',
'phone': /^1[3-9]\d{9}$/,
'address': '@county(true)',
'createTime': '@datetime',
'updateTime': '@datetime'
}]
})
export default [
{
method: 'post',
url: '/api/users',
response: ({ body }) => {
return {
code: 200,
msg: 'success',
data: userList
}
}
}
]借助 ts 为项目引入类型检查和语法提示
npm i vite-plugin-checker -D
npm i typescript -D
tsc --initvite.config.js
import { defineConfig } from 'vite'
import VitePluginChecker from 'vite-plugin-checker'
export default defineConfig({
plugins: [
VitePluginChecker({
typescript: true
})
]
})当发生错误时,禁止生产环境的输出:
package.json
{
"script": {
"build": "tsc --noEmit && vite build"
}
}我们在客户端使用 .env 配置环境变量时,需要通过import.meta.env去读取,因此需要额外配置以下信息
vite-env.d.ts
/// <reference types='vite/client' />
interface ImportMetaEnv {
readonly <Key>: string
}性能优化包括以下几个方面
- 页面性能指标
- 首屏渲染时:fcp(first content paint)
- 懒加载:通过代码实现
- http 优化
- 强缓存:服务器给响应头追加字段(expires),客户端在截止日期前只从缓存里读取数据以代替请求发起
- 协商缓存:对于浏览器的请求,服务器比对文件的修改日期,如果文件未变化,则响应 304
- 页面中最大元素的响应时长:lcp(largest content paint)
- 首屏渲染时:fcp(first content paint)
- js
- 注意组件计时器的清楚,如果组件在频繁的挂载和卸载中没有对计时器进行特殊处理,则有可能在一次挂载中开启多个线程
- 动画上活用 requestAnimationFrame、requestIdleCallback 等,对齐浏览器帧率(16.6ms)
- 防抖、节流以及数组遍历等方法尽量使用 lodash
- css
- 能继承就不重复写
- 避免层级过深的嵌套
- 构建优化
- 优化体积:压缩、treeshaking、图片资源压缩、cnd 加载、分包
浏览器缓存策略基于文件名称,如果文件名称未变化则不会重新请求资源
而有相当一部分代码(尤指第三方库)在整个工程中不会经常发生变化,我们可以对这些资源进行单独打包处理,避免浏览器对这些资源的反复请求
vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
// id为被打包的所有文件的路径
// 对路径包含node_modules的目录返回vendor
// 则第三方库的文件会统一打包到vendor.[hash].js中
if (id.includes('node_modules')) return 'vendor'
}
}
}
}
})分包原先有另一个好处,特别体现在多入口文件场景
vite.config.js
import { defineConfig } from 'vite'
import path from 'path'
export default defineConfig({
build: {
rollupOptions: {
input: {
main: path.resolve(__dirname, './index.html'),
product: path.resolve(__dirname, './product.html')
}
}
}
})如果像这样配置了多入口,且没有做好分包,然后在入口文件中都对同名 js/ts 文件进行引用,那么对于脚本文件中引入的相同第三方库,原先 vite 会将第三方库的内容分别打包到两个 js 文件中,这样会造成代码重复的资源占用问题
但是现在 vite 做出了优化:将第三方库打包结果输出到一个 js 文件中,然后其他文件import { l as lodash } from './product.[hash].js'
打包中块的概念可以大致等效于输出的文件,需要注意虽然表面上输出的文件后缀为 js,但他们是块而不是 js 文件
首先下载插件 vite-plugin-compression
npm i vite-plugin-compression -Dvite.config.js
import ViteCompression from 'vite-plugin-compression'
export default defineConfig({
plugins: [
ViteCompression()
]
})后续打包时,会同时输出较大文件的 .gz 文件
此时服务器响应该 js 文件时,不需要进行额外压缩操作,然后还需要设置响应头为content-encoding: gzip,使得浏览器识别出压缩文件并进行解压
动态导入是 ES6 的一个新特性
import app from './app'
{
component: app
}
// 将以上代码改为
{
component: import('./app')
}cdn(content delivery network,内容分发网络)
将第三方库包装为 cdn 的形式,使得代码体积变小,且请求速度快
npm i vite-plugin-cdn-import -Dvite.config.js
import { defineConfig } from 'vite'
import ImportToCDN from 'vite-plugin-cdn-import'
export default defineConfig({
plugins: [
importToCDN({
modules: [
{
name: 'lodash',
var: '_',
path: 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js'
}
]
})
]
})原理:在开发环境下通过<script>注入 cdn 地址,在生产环境下不会插入<script>,而是更改 rollup config:
{
build: {
rollupOptions: {
external: ['lodash'],
externalGlobal: {
var: '_',
path: 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js'
}
}
}
}跨域仅发生在浏览器上,当出现不同源请求时,浏览器会允许请求的发出,对方服务器也可以对请求进行响应,但是如果响应头不包含标记信息,则浏览器会截获包并提示跨域问题
在 vite 中可以配置以下选项,实现在生产环境下绕过同源策略:
以下配置中,匹配配置的请求地址,在浏览器向这些请求地址发起请求后,vite 会接收请求,然后转而向目标地址发起真正的请求,然后将响应信息响应给浏览器,达到绕过同源的目的
import { defineConfig } from 'vite'
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://www.baidu.com',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
})fetch('/api').then(data => {
// 此时拿到的是https://www.baidu.com的数据
})跨域的解决方案:
- 开发:利用构建工具,或脚手架,或第三方库的 proxy 代理配置,或自己搭建服务器去解决跨域
- 生产:交给后端处理跨域问题
- nginx:代理服务,本质上和 vite 服务器处理跨域问题方式一样
- 配置身份标记:
Access-Control-Allow-Origin