Vite
依赖预构建
解决三个问题
1、不同的第三方包会有不同的模块规范(commonjs,esmodule)
2、对包路径的处理,统一使用**.vite/deps**,方便路径重写
3、网络多包传输性能问题(也是原生es module规范不支持node_modules的原因之一),有了依赖预构建以及 以后包中无论有多少额外的export import,vite 都会尽可能将这些modules集成,最后生成一个或几个module
关闭预构建
optimizeDeps:不参与依赖预构建的包名数组
Type:string[ ]
// 配置项...
optimizeDeps: {
exclude: ['xxx'] // 数组元素:不参与依赖预构建的包名
}
vite 配置文件
vite.config.ts
import { defineConfig } from 'vite'
import viteBaseConfig from './vite.base.config'
import viteDevConfig from './vite.dev.config'
import viteProdConfig from './vite.prod.config'
// 策略模式
const devResolver = {
build: () => {
console.log('build', 'production');
return ({ ...viteBaseConfig, ...viteProdConfig })
},
serve: () => {
console.log('serve', 'development');
return Object.assign({}, viteBaseConfig, viteDevConfig)
},
}
export default defineConfig(({ command }) => {
return devResolver[command]()
})
环境变量与模式
环境变量:会根据当前代码环境产生值的变化的变量
代码环境:1、开发环境,2、测试环境,3、预发布环境,4、灰度环境,5、生产环境等
vite 中的环境变量
vite 内置了dotenv module
dotenv: 自动读取项目下的.env文件,并解析,.env文件内的环境变量,并将其注入 node全局对象 process 下,但是vite考虑到和其他配置的一些冲突问题,不会直接注入到 process 对象上
此时涉及到 vite.config.js 的配置
—envDir:用来配置当前环境变量所在的文件地址
Type:string
Vite 提供了补偿措施:vite 原生方法 loadEnv 可以帮助我们确认是否是正确的env文件
/**
* @description: vite 配置文件
* @param {string} mode 环境名称
*/
export default defineConfig(({ mode }) => {
// console.log(process.env); // 这里是没有我们设置的环境变量
/**
* @description: 加载环境变量
* @param {string} mode 环境名称
* @param {string} root env环境变量所在的文件地址(绝对路径)
* @param {string} [prefixes] env文件名
* @return {Record<string, string>} 环境变量
* @example
* loadEnv('development', process.cwd())
* // => { VITE_PORT: '3000', VITE_PUBLIC_PATH: '/' }
*/
const env = loadEnv(mode, process.cwd(),'')
console.log(env); // { VITE_APP_BASE_URL: '/api', ...}
})
process.cwd( ) 返回当前node进程的工作目录(绝对路径)
环境变量文件名:
-
.env: 所有环境都用到的环境变量
-
.env.development : 开发环境用到的环境变量(vite 默认的开发环境 development)
-
.env.production: 生产环境用到的环境变量(vite 默认的生产环境 development)
-
.env.[mode]: mode 模式下用到的环境变量
环境变量生效方式
package.json
"scripts": {
"dev": "vite --mode development", //开发环境
"build": "vite --mode production", // 生产环境
"xxx":"vite --mode xxx", // xxx环境
},
此时
export default defineConfig(({ mode }) => {
// mode 与 package.json scripts -> vite --mode xxx强相关
// mode = 'development' || 'production' || 'xxx'
})
此时,调用loadEnv 会发生如下事情:
1、直接读取.env 文件,并解析其中的环境变量,并放在一个对象里面(假定:baseEnvConfig)
2、读取.env.[mode] 文件,并解析其中的环境变量,并放在一个对象里面(假定:modeEnvConfig)
3、合并两个envConfig 并生成的最终的 lastEnvConfig 对象并使用
const baseEnvConfig = .env 文件里的环境变量对象
const modeEnvConfig = .env.[mode] 文件里的环境变量对象
const lastEnvConfig = {...baseEnvConfig,...lastEnvConfig}
我们如何在自己的代码里面去使用环境变量呢?
1、如果是客户端,vite 会将此时对应环境的变量注入到 import.meta.env 对象中,全js文件可访问,此时似乎也访问不了,我们需要的环境变量,是因为vite考虑到我们也许会将隐私性的变量,直接放在env文件中,我们需要以VITE_为前缀的变量命名,才能在import.meta.env 对象访问。
例如:
.env
APP_KEY = 110
VITE_BASE_URL=http://test.api/
此时,import.meta.env 对象中只能访问 VITE_BASE_URL,不能访问APP_KEY
前缀可修改
envPrefix: 可访问变量的前缀 默认值: VITE_
Type:string
// 配置项...
envPrefix: 'CHERRY_'
小知识:为什么vite.config.js,可以书写成es module 规范 ?
这是因为,vite 在读取 vite.config.js 时,会率先用node 去解析 vite.config.js 文件语法(字符串),如果发现时es module 规范,会直接将 es module 规范,进行替换成commonjs规范(String.prototype.replace( ))
Vite中的css
vite 天生就支持对css文件的直接处理
-
1、vite在读取main.js 中引入的index.css
-
2、直接使用node fs内置模块去读取index.css文件内容
-
3、直接创建一个style标签,将index.css中读取的内容直接copy进style标签内
-
4、将style 标签 直接插入index.html的head中
-
5、将改css文件中的内容直接替换成js脚本(方便热更新,css模块化),并将文件响应头Content-Type 设置为text/js,浏览器可直接将其解析成js脚本执行.
但是当我们协同开发的时候,我们的class类名也许会冲突!
我们可以用css module 来解决这个问题
原理:
-
1、.module.css( module 表示一种约定,表示开启css模块化 )
-
2、模块化会将你的所有类名进行一定规则的替换(将footer 替换成 footer[hash值])
-
3、同时构建一个映射关系对象 { footer: footer[hash值] }
-
4、将替换后的内容塞进style标签里面此时style 中的类名footer[hash值] 类似,并插入head中
-
5、将改.module.css文件中的内容直接替换成js脚本(方便热更新,css模块化),并将文件响应头Content-Type 设置为text/js,浏览器可直接将其解析成js脚本执行
-
6、将创建的的映射关系对象在脚本中进行默认到处
vite.config.js 中 css配置
在vite.config.js 我们可以通过css option 去控制整个vite中css的处理行为
Modules
Modeles 选项用于配置css模块化的行为,最终会交给postcss处理
localsConvention
localsConvention 表示到处的映射关系对象的key 也就是类名的语法形式(中划线 或者 驼峰命名),默认值:dashes (中划线)
默认导出的映射关系对象
{
footer-content:footer_sst01
}
当localsConvention 的值为camelCase,允许css 类名为驼峰命名法
css: {
modules: {
// 生成的映射关系对象的key
localsConvention: 'dashes', // "camelCase" | "camelCaseOnly" | "dashes" | "dashesOnly"
}
}
映射关系对象:
{
footer-content:footer_sst01,
footerContent:footer_sst01
}
这里可以做一个性能优化,[ camelCase|dashes] Only, 仅生成其中一种类名的语法形式
scopeBehaviour
scopeBehaviour 表示是否开启模块化样式 默认值:local(开启)
Type : 'local' | 'global' default: 'local'
css: {
modules: {
// 生成的映射关系对象的key的语法形式
scopeBehaviour: 'local' // 'local' | 'global' default: 'local' 开启模块化样式 会影响全局样式
}
}
generateScopedName
generateScopedName: 构建出来的样式 类名的规则 可以传函数类型 详情见 postcss
Type : string | ((name: string, filename: string, css: string) => string)
css: {
modules: {
// 生成的映射关系对象的value的格式
generateScopedName: "[name]_[local]_[hash:5]" // ()=>(string)
}
}
hashPrefix
hashPrefix: 将你给的字符串打乱到生成的hash class 类名中去,增强类名唯一性
Type : string
css: {
modules: {
hashPrefix: "cherry"
}
}
globalModulePaths
globalModulePaths: 表示不想参与css模块化的css文件路径(绝对路径)
Type : string[ ]
css: {
modules: {
globalModulePaths: [path.resolve(__dirname, 'src/styles/global.css')]
}
}
preprocessorOptions
preprocessorOptions css 预处理器的配置
Type : Record<string, any>
css: {
preprocessorOptions: {
less: {
// ... 该less配置会传递给less-loader
},
scss: {
// ... 该scss配置会传递给sass-loader
},
},
}
less 常见配置
math: 表示是否开启less的数学运算
Type : 'always' | 'parens-division' | 'parens' | 'strict' 例: 100px / 2 -> 50px
css: {
preprocessorOptions: {
less: {
math: 'always', // 默认值:always 该配置会启用less的数学运算
},
},
}
globalVars: 表示全局样式变量
Type : Record<string, string>
css: {
preprocessorOptions: {
less: {
globalVars: {
primary: '#333',
},
},
},
}
devSouseMap
devSouseMap: 表示是否开启css的sourcemap,即开启css文件索引,方便调试,默认值:false
Type : boolean
css: {
preprocessorOptions: {
// ...
devSouseMap: true, // 默认值:false
},
}
postcss
-
1、对未来css属性的兼容性处理(降级)
-
2、前缀补全 (-webkit- -moz- -ms- -o-)
-
3、丰富的插件系统 供给流水线的每个阶段使用
-
4、css 语法检查
小知识:在node中读取文件的时候,如果文件中是相对路径,那么会以当前node进程中的工作的文件目录( process.cwd())为基准,去拼接相对路径,去查找文件,如果是绝对路径,那么会以根目录为基准,去查找文件
Vite中的静态资源处理
Vite中的静态资源是开箱即用的,不需要任何配置,只需要在代码中引入静态资源,Vite会自动处理
静态资源的引入
性能优化点:Tree Shaking ( 万不得已不要全体导入 ,最好使用解构导入 )摇树 优化
Tree shaking是 JavaScript 上下文中用于消除死代码的常用术语。它依赖于 ES2015 模块语法的静态结构,如果当你整体导入一个模块的时候,Vite无法知道你到底用了模块中的哪些内容,所以就无法进行Tree Shaking
// 1、导入整体
import * as _ from 'lodash'
// 2、导入默认
import _ from 'lodash'
// 3、导入解构
import { debounce } from 'lodash' // 有 Tree Shaking 效果
// 导入图片路径 xxx.png?url
import logo from './assets/logo.png' // 此时logo就是一个图片的路径 /src/assets/logo.png
// 原始数据
import logo from './assets/logo.png?raw' // 此时logo就是一个图片的二进制数据
路径别名
// vite.config.ts
import { defineConfig } from 'vite'
import path from 'path'
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
})
// 使用路径别名
import logo from '@/assets/logo.png'
生产环境的打包
Vite在生产环境下,会自动对静态资源进行打包,打包后的文件会放在dist目录下
打包后的资源为啥会有hash值?
-
1、浏览器缓存(浏览器的缓存机制:当静态资源名字不变,他就会直接使用缓存)
-
2、防止文件名重复
-
3、防止文件内容被篡改
当我们文件名不变,但是文件内容变化了,那么浏览器就会认为文件没有变化,就会直接使用缓存,这样就会导致我们的页面不会更新,所以我们需要在文件名中加入hash值,这样就可以保证文件名不变,文件内容变化了,那么hash值就会变化,浏览器就会认为文件变化了,就会重新请求文件
Vite中的打包配置
兼容rollup的配置
build: { // 打包配置
rollupOptions: { // 用于rollup配置最终打包的选项
output: { // 用于配置最终打包的输出
assetFileNames: '[hash]_[name].[ext]', // 静态资源文件名
}
},
assetsInlineLimit: 4096, // 小于4kb的静态文件会被转成base64
// brotliSize: false, // 是否开启brotli压缩
outDir: 'dist', // 打包输出目录
assetsDir: 'assets', // 静态资源目录
}
rollupOptions
rollupOptions是用于rollup配置最终打包的选项
// rollupOptions
rollupOptions: {
output: {
assetFileNames: '[hash]_[name].[ext]', // 静态资源文件名
}
}
assetsInlineLimit
assetsInlineLimit是用于配置小于多少kb的静态文件会被转成base64
build:{
assetsInlineLimit: 4096, // 小于4kb的静态文件会被转成base64
}
brotliSize
brotliSize是用于配置是否开启brotli压缩
build:{
brotliSize: false, // 是否开启brotli压缩
}
outDir
outDir是用于配置打包输出目录
build:{
outDir: 'dist', // 打包输出目录
}
assetsDir
assetsDir是用于配置静态资源目录
build:{
assetsDir: 'assets', // 静态资源目录
}
minify
minify 是否开启压缩
Type: boolean | "esbuild" | "terser" | undefined
build:{
minify: false, //关闭压缩
}
Vite中的插件
插件是什么?
插件是Vite的一个扩展,可以用来扩展Vite的功能 vite 会在生命周期中,根据配置的插件,依次执行插件中的方法以达到不同的目的
vite中的插件是一个对象
插件的使用
// vite.config.ts
import { defineConfig } from 'vite'
import path from 'path'
export default defineConfig({
plugins: [
// 插件
]
})
热更新
热更新是什么?
热更新是指在不刷新页面的情况下,更新页面的内容, 也就是说,我们在开发过程中,修改了代码,页面不用刷新,就会直接更新页面的内容
热更新的原理
热更新的原理是通过websocket来实现的,当我们修改了代码,vite会将修改的代码的文件(此时hash name 已经改变)通过websocket发送给浏览器,浏览器接收到文件后,会直接使用此文件
Vite 构建优化
分包策略
浏览器华缓存策略: 当我们的文件名未发生变化时,浏览器并不会去重新请求资源,而是直接使用这个文件,此时引入hash值为文件名,当内容未发生改变,文件名就不会变化,反之,文件名会发生变化,浏览器就会重新请求资源。
所以当我们不会常规更新的文件,进行单独打包,这样就可以减少打包的体积,同时也可以减少浏览器的多次请求该资源(文件名字未变化)
build: {
rollupOptions: {
output: {
manualChunks: {
lodash: ['lodash'], // lodash是一个包名,后面是一个数组,数组中是需要单独打包的文件
}
}
}
}
// 或者 写成一个函数调用
build: {
rollupOptions: {
output: {
manualChunks(id) { // id是每个文件(module)的路径
if (id.includes('node_modules')) {
return id.toString().split('node_modules/')[2].split('/')[0].toString()
}
// ...
}
}
}
}
gizp压缩
gizp压缩是一种压缩文件的方式,它可以将文件进行压缩,从而减少文件的大小,同时也可以减少浏览器的请求时间
// vite.config.ts
import { defineConfig } from 'vite'
import { compression } from 'vite-plugin-compression2'
export default defineConfig({
plugins: [
// ...your plugin
compression()
]
})
代码分割(动态导入)
代码分割是指将代码分割成多个文件,从而减少文件的大小,同时也可以减少浏览器的请求时间 常用于路由 懒加载 vite 中 直接使用import函数动态导入即可,webpack中需要使用webpack_require的魔法注释 webpack中使用魔法注释
import(/* webpackChunkName: "about" */ './views/about.vue')
原理: webpack会将代码分割成多个文件,当我们使用魔法注释时,webpack会将这个文件打包成一个单独的文件,并且把代码塞入一个 script 标签中,从而实现代码分割,并且在进入这个页面时,才会去请求这个文件,也就是,webpack 会在内部 webpack_require.e().then(()=>{ webpack_require('./views/about.vue') }),当我们没有进入页面,这个promise永远是pending状态,所以不会去请求这个文件,当我们进入这个页面时,promise会变成resolve状态,把构建出来的script 标签,插入到body,就会去请求这个文件
import home from './views/home.vue'
const routes = [
{
path: '/',
component: home
},
{
path: '/about',
component: () => import('./views/about.vue') // dynamic import 代码分割
}
]
tree shaking
Tree Shaking ( 万不得已不要全体导入 ,最好使用解构导入 )摇树 优化 Tree shaking是 JavaScript 上下文中用于消除死代码的常用术语。它依赖于 ES2015 模块语法的静态结构,如果当你整体导入一个模块的时候,Vite无法知道你到底用了模块中的哪些内容,所以就无法进行Tree Shaking
scope hoisting
scope hoisting是指将代码中的函数和变量进行提升
CDN 加速(内容分发网络)
CDN加速是指将静态资源放到CDN上,从而减少浏览器的请求时间
npm i vite-plugin-cdn-import -d
// vite.config.ts
import { defineConfig } from 'vite'
import importToCDN, { autoComplete } from 'vite-plugin-cdn-import'
export default defineConfig({
plugins: [
// ...your plugin
importToCDN({
modules: [
autoComplete('lodash'),
]
})
]
})
Vite 跨域
跨域是指浏览器的同源策略,同源策略是指协议,域名,端口都相同(一般发生在浏览器请求的响应阶段)
Server代理
// vite.config.ts
import { defineConfig } from 'vite''
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://baidu.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})
例子:
fetch('/api/user').then(res => res.json()).then(data => console.log(data))
vite开发服务器 在匹配 req.url 为 /api 开头的请求时,会将其代理到目标服务器 baidu.com 此时 path 为baidu.com/api/user 同时会执行server.proxy.rewrite(path) (此处是将 /api 替换为空字符串),最终请求的地址为 baidu.com/user
代理解决跨域的原理: 因为跨越问题是在浏览器发出请求的响应阶段发生的, 浏览器先拼接完整的URL地址:http://http://127.0.0.1:5173/api/user,此时浏览器去请求的服务器地址是本地的vite开发服务器,开发服务器会根据vite.config.js 中 server.proxy 的配置,生成一个 目标服务器的网络请求地址 baidu.com/user 转发到目标服务器,此时 是服务器与服务器的通信故不会发生跨域问题,目标服务器返回的数据会再次经过开发服务器,然后再响应给浏览器( 这里为什么没有出现跨域问题?是因为网页的地址 与开发服务器地址 同源 )
此解决方式,仅适用开发环境
生产环境的处理
-
ngnix 代理服务 : 类似 vite 开发服务器
-
配置身份标记
-
Access-Control-Allow-Origin : 表示那些源是允许访问的
-
xxx
-