接上一篇Webpack优化之后,现在和小伙伴们一起来聊一聊Vite,聊的主要内容是为什么选用Vite和Vite的使用规则。
我第一次使用Vite是去年4月份的时候,当时上海正在封控中,同时公司开始筹备做一个CMS系统。经过多轮的项目评审和调研后,最后大家一拍即合决定用Vue3+Vite+Typescript+Element-plus+Pinia。之所以选择这样的技术栈原因如下:
- 历史项目上分析,公司现存的两个项目都在用Vue2+Webpack构建,但是随着后期功能的不断增加,打包速度变的会越来越慢,在现有功能上由于类型的不规范定义,维护起来也很吃力。
- 对比Vite和Webpack的优缺点,这里先说说这两者的打包原理,webpack执行过程是:根据配置查找入口>逐层识别依赖>分析/转换/编译/输出代码>打包后的代码,其原理是从入口文件开始,逐层递归识别依赖,构建依赖图谱,转换成AST抽象树,处理代码,最终转换成浏览器能识别的代码。Vite的原理是基于浏览器的ES Module特性,碰见import就会发送一个HTTP请求去加载文件。Vite启动一个connect 服务器拦截这些请求,并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再以ESM格式返回返回给浏览器。整个过程中没有对文件进行打包编译。结合它们两者的工作原理,得出Vite的优点是:启动快 ,热更新快。
- Vue3语法结合TypeScript规范,可以构建易维护、可扩展的项目。
接下来说说如何一步步开始搭建项目。
- 使用Vite创建项目
- 配置根目录下的vite.config.ts文件,可以参考下面代码片段的一些基本且实用的配置。
ts
//vite.config.ts
import { defineConfig, ConfigEnv, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import ElementPlus from 'unplugin-element-plus/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import OptimizationPersist from 'vite-plugin-optimize-persist'
import PkgConfig from 'vite-plugin-package-config'
const path = require('path')
const envConfig = require('./env.config')
// https://vitejs.dev/config/
// @ts-ignore
export default defineConfig((mode: ConfigEnv) => {
// @ts-ignore
return {
server: {
host: '0.0.0.0', // resolve vite use `--host` to expose
port: '8080',
open: true
},
resolve: {
alias: [
{
find: '@',
replacement: resolve(__dirname, 'src')
}
]
},
css: {
// css预处理器
preprocessorOptions: {
scss: {
// 引入 var.scss 这样就可以在全局中使用 var.scss中预定义的变量了
additionalData: '@use "./src/styles/variables.scss" as *;'
}
}
},
build: {
chunkSizeWarningLimit: true,
brotliSize: false,
rollupOptions: {
output: {
manualChunks(id) {
// 将pinia的全局库实例打包进vendor,避免和页面一起打包造成资源重复引入
if (id.includes(path.resolve(__dirname, '/src/store/index.ts'))) {
return 'vendor'
}
}
}
}
},
plugins: [
vue(),
ElementPlus({
// 引入的样式的类型,可以是css、sass、less等,
importStyle: 'css',
useSource: true
}),
AutoImport({
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver()]
}),
createSvgIconsPlugin({
// 指定需要缓存的图标文件夹
iconDirs: [resolve(process.cwd()), 'src/icons/svg'],
// 指定格式
symbolId: 'icon-[dir]-[name]'
}),
PkgConfig(),
OptimizationPersist()
]
}
})
- 配置环境变量
这里以dev环境为例,其他环境如法炮制。首先在根目录下新建.env.dev文件,变量的定义需要以VITE_APP为前缀,参考如下代码片段。
.env.dev
# 开发环境变量
VITE_APP_API_ADDRESS='//keeper-api-dev.***.ink/'
VITE_APP_STORAGE_PREFIX='***.cms.dev'
如何引用环境变量
ts
//引用api_address环境变量
import.meta.env.VITE_APP_API_ADDRESS
最后在打包命令时配上相应的模式参数,这里的参数一一对应根目录下的env.mode文件配置。
json
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"build:dev": "vue-tsc --noEmit && vite build --mode dev",
"build:test": "vue-tsc --noEmit && vite build --mode test",
"build:pre": "vue-tsc --noEmit && vite build --mode pre",
"build:prod": "vue-tsc --noEmit && vite build --mode production",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.ts,.jsx,.tsx --fix"
},
- 安装和配置路由
安装
bash
npm i vue-router@4
基本用法和配置和vue2项目没有太大差别,这里重点介绍路由安全守卫的逻辑,具体如下:
ts
import router from '@/router'
import { getToken, setToken } from '@/utils/cookies'
import { whiteList } from '@/config/white-list'
import { userStore } from '@/store/modules/user'
import { permissionStore } from '@/store/modules/permission'
import { appStore } from '@/store/modules/app'
import tfrMessage from '@/utils/tfrMessage'
const $tfrMessage = tfrMessage
router.beforeEach(async (to, from, next) => {
const useUserStore = userStore()
const usePermissionStore = permissionStore()
const useAppStore = appStore()
const token = getToken()
if (token) {
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
try {
if (JSON.stringify(useUserStore.user) === '{}') {
// 检查用户是否已获得权限角色
await useUserStore.getInfoHttp()
useAppStore.token = token // 刷新后从设置token
usePermissionStore.setRoutes()
usePermissionStore.dynamicRoutes.forEach(v => {
router.addRoute(v)
})
next({ ...to, replace: true })
} else {
next()
}
} catch (e) {
setToken('')
next(`/login?redirect=${to.path}`)
}
}
} else if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
$tfrMessage({
message: 'Please Sign In',
type: 'error'
})
next(`/login?redirect=${to.path}`)
}
})
router.afterEach(() => {
// NProgress.done()
})
- 状态管理工具 Pinia
安装
bash
npm i pinia
main.ts
ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
store/modules/app.ts
js
/**
* 一般在容器中做这4件事
* 1. 定义容器并导出
* 2. 使用容器中的state
* 3. 修改容器中的state
* 4. 使用容器中的action
*/
import { defineStore } from 'pinia'
import getDevice from '@/utils/device'
/**
* 1. 定义容器并导出
* 参数一: 容器ID, 唯一, 将来 Pinia 会把所有的容器挂载到根容器
* 参数二: 选项对象
* 返回值: 函数, 调用的时候要空参调用, 返回容器实例
*/
export const appStore = defineStore({
id: 'app',
/**
* 类似组件的 data, 用于存储全局的的状态
* 注意:
* 1.必须是函数, 为了在服务端渲染的时候避免交叉请求导致的数据交叉污染
* 2.必须是箭头函数, 为了更好的 TS 类型推导
*/
state: () => {
return {
device: getDevice(), // desktop ipad mobile
token: ''
}
},
/**
* 类似组件的 computed, 用来封装计算属性, 具有缓存特性
*/
getters: {},
/**
* 类似组件的 methods, 封装业务逻辑, 修改state
* 注意: 里面的函数不能定义成箭头函数(函数体中会用到this)
*/
actions: {}
})
layout/index.ts
ts
<template>
<div v-if="device !== 'mobile'" class="common-layout">
<el-aside :width="`${menuWidth}`">
<SideBar />
</el-aside>
<el-container :style="{ paddingLeft: menuWidth, height: '100%' }">
<el-header :style="{ left: menuWidth }">
<PlatformControl />
</el-header>
<el-main>
<div class="main_content">
<router-view />
</div>
</el-main>
</el-container>
</div>
<div v-else class="common-layout">
<MobileSideBar />
<el-main :style="{ paddingTop: mobileMainPaddingTop + 'px' }">
<router-view />
</el-main>
</div>
</template>
<script setup lang="ts">
import SideBar from '@/layout/component/sidebar/index.vue'
import MobileSideBar from '@/layout/component/sidebar/mobile.vue'
import PlatformControl from '@/components/PlatformControl/index.vue'
import { appStore } from '@/store/modules/app'
import { menuStore } from '@/store/modules/menu'
import { storeToRefs } from 'pinia'
const useMenuStore = menuStore()
// 通过storeToRefs转换为响应式对象解构可正常使用
const { menuWidth } = storeToRefs(menuStore())
const { mobileMainPaddingTop } = storeToRefs(menuStore())
const { device } = storeToRefs(appStore())
// useUserStore.setMenuWidth(routeName)
useMenuStore.setMenuWidth()
device.value === 'mobile' && useMenuStore.setMobileMainPaddingTop()
</script>
写在最后
到此项目搭建基本完成,期望能对大家有所帮助,不断学习,共同进步。参考资料:
Vite 官网 vitejs.dev/config/ 。
Vue Router router.vuejs.org/zh/installa... 。