介紹
一個使用 vite
+ vue3
+ pinia
+ ant-design-vue
+ typescript
完整技術路線開發的項目,秒級開發更新啟動、新的vue3 composition api
結合 setup
縱享絲滑般的開發體驗、全新的 pinia
狀態管理器和優秀的設計體驗(1k
的size)、antd
無障礙過渡使用UI組件庫 ant-design-vue
、安全高效的 typescript
類型支持、代碼規范驗證、多級別的權限管理~
前言
前兩天接到了一個需求,就是把原來的一個項目的主要功能模塊和用戶模塊權限系統抽出來做一個新后臺項目,并迭代新增一些新功能,看起來好像也沒啥東西
拿到源碼看了下項目,好家伙,原項目是個微應用項目,主應用用戶模塊是react
技術棧,子應用模塊是vue2
技術棧,這直接 CV大法看樣子是不行了??,我這要做的畢竟是個單頁面應用,確定一個技術路線即可,具體看下代碼邏輯并跑起來看看
跑起來試了下,兩個項目基本都是1分鐘左右啟動,看代碼vue項目整個業務邏輯代碼都擰在一塊寫了
想到之前問老大要源碼的時候,說那個是老項目了,重新搭一個寫應該會快點
這話沒毛病啊,話不多說,直接開整,這次直接上 vite
+ vue3
特性
- ?腳手架工具:高效、快速的 Vite
- ??前端框架:眼下最時髦的 Vue3
- ??狀態管理器:
vue3
新秀 Pinia,猶如react zustand
般的體驗,友好的api和異步處理 - ??開發語言:政治正確 TypeScript
- ??UI組件:
antd
開發者無障礙過渡使用 ant-design-vue,熟悉的配方熟悉的味道 - ??css樣式:less 、
postcss
- ??代碼規范:Eslint、Prettier、Commitlint
- ??權限管理:頁面級、菜單級、按鈕級、接口級
- ?依賴按需加載:unplugin-auto-import,可自動導入使用到的
vue
、vue-router
等依賴 - ??組件按需導入:unplugin-vue-components,無論是第三方UI組件還是自定義組件都可實現自動按需導入以及
TS
語法提示
項目目錄
├── .husky // husky git hooks配置目錄
├── _ // husky 腳本生成的目錄文件
├── commit-msg // commit-msg鉤子,用于驗證 message格式
├── pre-commit // pre-commit鉤子,主要是和eslint配合
├── config // 全局配置文件
├── vite // vite 相關配置
├── constant.ts // 項目配置
├── themeConfig.ts // 主題配置
├── dist // 默認的 build 輸出目錄
├── mock // 前端數據mock
├── public // vite項目下的靜態目錄
└── src // 源碼目錄
├── api // 接口相關
├── assets // 公共的文件(如image、css、font等)
├── components // 項目組件
├── directives // 自定義 指令
├── enums // 自定義 常量(枚舉寫法)
├── hooks // 自定義 hooks
├── layout // 全局布局
├── router // 路由
├── store // 狀態管理器
├── utils // 工具庫
├── views // 頁面模塊目錄
├── login // login頁面模塊
├── ...
├── App.vue // vue頂層文件
├── auto-imports.d.ts // unplugin-auto-import 插件生成
├── components.d.d.ts // unplugin-vue-components 插件生成
├── main.ts // 項目入口文件
├── shimes-vue.d.ts // vite默認ts類型文件
├── types // 項目type類型定義文件夾
├── .editorconfig // IDE格式規范
├── .env // 環境變量
├── .eslintignore // eslint忽略
├── .eslintrc // eslint配置文件
├── .gitignore // git忽略
├── .npmrc // npm配置文件
├── .prettierignore // prettierc忽略
├── .prettierrc // prettierc配置文件
├── index.html // 入口文件
├── LICENSE.md // LICENSE
├── package.json // package
├── pnpm-lock.yaml // pnpm-lock
├── postcss.config.js // postcss
├── README.md // README
├── tsconfig.json // typescript配置文件
└── vite.config.ts // vite
開發
項目初始化
如果使用vscode編輯器開發vue3,請務必安裝Volar插件與vue3配合使用更佳(與原本的Vetur不兼容)
使用 vite cli 快速創建項目
yarn create vite project-name --template vue-ts
安裝相關依賴
推薦使用新一代 pnpm
包管理工具,性能和速度以及 node_modules
依賴管理都很優秀
建議配合 .npmrc
配置使用
# 提升一些依賴包至 node_modules
# 解決部分包模塊not found的問題
# 用于配合 pnpm
shamefully-hoist = true
# node-sass 下載問題
# sass_binary_site="https://npm.taobao.org/mirrors/node-sass/"
代碼規范
工具:husky
、eslint
、prettier
具體使用方式,網上很多,我在之前另一篇文章也有說過,這里不再贅述~
a Vite2 + Typescript + React + Antd + Less + Eslint + Prettier + Precommit template
主要就是自動化的概念,在一個合適的時機完成規定的事
- 結合VsCode編輯器(保存時自動執行格式化:
editor.formatOnSave: true
) - 配合Git hooks鉤子(commit前或提交前執行:
pre-commit
=>npm run lint:lint-staged
)
注意:
針對不同系統 commitlint
安裝方式有所不同 commitlint,安裝錯誤可能會無效哦~
# Install commitlint cli and conventional config
npm install --save-dev @commitlint/{config-conventional,cli}
# For Windows:
npm install --save-dev @commitlint/config-conventional @commitlint/cli
功能
vue能力支持
模板語法配合jsx語法,使用起來非常方便、靈活~
一些必須的插件
{
// "@vitejs/plugin-legacy": "^1.6.2", // 低版本瀏覽器兼容
"@vitejs/plugin-vue": "^1.9.3", // vue 支持
"@vitejs/plugin-vue-jsx": "^1.2.0", // jsx 支持
}
狀態管理器 Pinia
vue新一代狀態管理器,用過 react zustand
的同學應該會有很熟悉的感覺
Pinia是一個圍繞Vue 3 Composition API的封裝器。因此,你不必把它作為一個插件來初始化,除非你需要Vue devtools支持、SSR支持和webpack代碼分割的情況
- 非常輕量化,僅有 1 KB
- 直觀的API使用,符合直覺,易于學習
- 模塊化設計,便于拆分狀態
- 全面的TS支持
// ... 引入相關依賴
interface IUserInfoProps{
name: string;
avatar: string;
mobile: number;
auths: string[]
}
interface UserState {
userInfo: Nullable<IUserInfoProps>;
}
// 創建 store
export const useUserStore = defineStore({
id: 'app-user', // 唯一 ID,可以配合 Vue devtools 使用
state: (): UserState => ({
// userInfo
userInfo: null,
}),
getters: {
getUserInfo(): Nullable<IUserInfoProps> {
return this.userInfo || null;
},
},
actions: {
setUserInfo(info: Nullable<IUserInfoProps>) {
this.userInfo = info ?? null;
},
resetState() {
this.userInfo = null;
},
/**
* @description: fetchUserInfo
*/
async fetchUserInfo(params: ReqParams) {
const res = await fetchApi.userInfo(params);
if (res) {
this.setUserInfo(res);
}
},
},
})
組件中使用
// TS 類型推斷、異步函數使用都很方便
import { useHomeStore } from '/@/store/modules/home';
const store = useHomeStore();
const userInfo = computed(() => store.getUserInfo);
onMounted(async () => {
await store.fetchInfo(); // 異步函數
// ...
});
UI組件按需加載、自動導入
了解基本概念:vite 自帶按需加載(針對js),我們這里主要針對樣式做按需加載處理
方案一:vite-plugin-style-import
import styleImport from 'vite-plugin-style-import'
//
plugins:[
styleImport({
libs: [
{
libraryName: 'ant-design-vue',
esModule: true,
resolveStyle: (name) => {
return `ant-design-vue/es/${name}/style/index`
},
}
]
})
]
方案二:unplugin-vue-components
推薦使用 unplugin-vue-components 插件
該插件只需在 vite plugin中添加對應 AntDesignVueResolver
即可,也支持自定義的 components
自動注冊,很方便
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
import Components from 'unplugin-vue-components/vite';
// vite.config.ts plugins 添加如下配置
export default defineConfig({
plugins: [
Components({
resolvers: [
AntDesignVueResolver(), // ant-design-vue
// ElementPlusResolver(), // Element Plus
// VantResolver(), // Vant
]
})
]
})
當然這里如果沒有你使用的對應的UI框架的 Resolver
加載器,也沒關系,也支持自定義配置
Components({
resolvers: [
// example of importing Vant
(name) => {
// where `name` is always CapitalCase
if (name.startsWith('Van'))
return { importName: name.slice(3), path: 'vant' }
}
]
})
另一強悍功能:該插件不僅支持UI框架組件的按需導入,也支持項目組件的自動按需導入
具體表現就是:如我們使用 ant-design-vue的 Card組件或我們自己定義的 components/Icon 等其他組件時,我們不用導入,直接用即可,插件會為我們自動按需導入,結合 TS語法提示,開發效率杠杠的~
配置如下:
Components({
// allow auto load markdown components under `./src/components/`
extensions: ['vue'],
// allow auto import and register components
include: [/\.vue$/, /\.vue\?vue/],
dts: 'src/components.d.ts',
})
需要在src
目錄下添加 components.d.ts
文件配合使用,該文件會被插件自動更新
-
components.d.ts
作用
直接的作用是:在項目下生成對應.d.ts
type類型文件,用于語法提示與類型檢測通過
- 注意
"unplugin-vue-components": "^0.17.2"
當前版本已知問題:issues 174
對于 ant-design-vue
的 notification
/ message
組件,當在 js
中使用時,該插件不會執行自動導入能力(樣式不會被導入)
最終效果是:message.success('xx')
可以創建 DOM
元素,但是沒有相關樣式代碼
因為該插件的設計原理是根據 vue template
模板中的組件使用進行處理的,函數式調用時插件查詢不到
解決方案:
- 改用
vite-plugin-style-import
插件 - 手動全局引入 message組件樣式,
import 'ant-design-vue/es/message/style'
- 在vue組件的 template中手動添加
<a-message />
供插件索引依賴時使用
依賴按需自動導入
- unplugin-auto-import
vue相關 defineComponent
、computed
、watch
等模塊依賴根據使用,插件自動導入,你無需關心 import
,直接使用即可
該插件默認支持:
vue
vue-router
vue-i18n
@vueuse/head
@vueuse/core
- ...
當然你也可以自定義配置 unplugin-auto-import
用法如下:
import AutoImport from 'unplugin-auto-import/vite'
export default defineConfig({
// ...
plugins: [
AutoImport({
imports: [
'vue',
'vue-router',
'vue-i18n',
'@vueuse/head',
'@vueuse/core',
],
dts: 'src/auto-imports.d.ts',
})
]
})
需要在src
目錄下添加 auto-imports.d.ts
文件配合使用,該文件會被插件自動更新
最終效果為:
如 ref
方法我們可以直接使用并有相應的TS語法提示,而不需要手動的去 import { ref } from 'vue'
自定義主題
自定義主題設置參考官方文檔配置即可,兩種常規方式
- 按需加載配合
webpack/vite
loader屬性修改變量 - 全量引入,配合
variables.less
自定義樣式覆蓋框架主題樣式
這里我們采用第一種方法通過loader配置配合按需加載食用
vite項目下,請手動安裝 less,
pnpm add less -D
css: {
preprocessorOptions: {
less: {
modifyVars: { 'primary-color': 'red' },
javascriptEnabled: true, // 這是必須的
},
},
}
注意:在使用了 unplugin-vue-components
進行按需加載配置后,相關 less變量設置需要同步開啟 importStyle: 'less'
,unplugin-vue-components issues 160
AntDesignVueResolver({ importStyle: 'less' }) // 這里很重要
mock數據
- vite-plugin-mock 插件
vite plugin配置
viteMockServe({
ignore: /^\_/,
mockPath: 'mock',
localEnabled: true,
prodEnabled: false,
// 開發環境無需關心
// injectCode 只受prodEnabled影響
// https://github.com/anncwb/vite-plugin-mock/issues/9
// 下面這段代碼會被注入 main.ts
injectCode: `
import { setupProdMockServer } from '../mock/_createProductionServer';
setupProdMockServer();
`,
})
根目錄下創建 _createProductionServer.ts
文件
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer';
// 批量加載
const modules = import.meta.globEager('./**/*.ts');
const mockModules: any[] = [];
Object.keys(modules).forEach((key) => {
if (key.includes('/_')) {
return;
}
mockModules.push(...modules[key].default);
});
/**
* Used in a production environment. Need to manually import all modules
*/
export function setupProdMockServer() {
createProdMockServer(mockModules);
}
這樣mock目錄下的非 _
開頭文件都會被自動加載成mock文件
如:
import Mock from 'mockjs';
const data = Mock.mock({
'items|30': [
{
id: '@id',
title: '@sentence(10, 20)',
account: '@phone',
true_name: '@name',
created_at: '@datetime',
role_name: '@name',
},
],
});
export default [
{
url: '/table/list',
method: 'get',
response: () => {
const items = data.items;
return {
code: 0,
result: {
total: items.length,
list: items,
},
};
},
},
];
配置好代理直接請求 /api/table/list
就可以得到數據了
Proxy代理
import proxy from './config/vite/proxy';
export default defineConfig({
// server
server: {
hmr: { overlay: false }, // 禁用或配置 HMR 連接 設置 server.hmr.overlay 為 false 可以禁用服務器錯誤遮罩層
// 服務配置
port: VITE_PORT, // 類型: number 指定服務器端口;
open: false, // 類型: boolean | string在服務器啟動時自動在瀏覽器中打開應用程序;
cors: false, // 類型: boolean | CorsOptions 為開發服務器配置 CORS。默認啟用并允許任何源
host: '0.0.0.0', // 支持從IP啟動訪問
proxy,
},
})
proxy 如下
import {
API_BASE_URL,
API_TARGET_URL,
} from '../../config/constant';
import { ProxyOptions } from 'vite';
type ProxyTargetList = Record<string, ProxyOptions>;
const ret: ProxyTargetList = {
// test
[API_BASE_URL]: {
target: API_TARGET_URL,
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp(`^${API_BASE_URL}`), ''),
},
// mock
// [MOCK_API_BASE_URL]: {
// target: MOCK_API_TARGET_URL,
// changeOrigin: true,
// rewrite: (path) => path.replace(new RegExp(`^${MOCK_API_BASE_URL}`), '/api'),
// },
};
export default ret;
環境變量 .env
我這邊是把系統配置放到 config/constant.ts
管理了
為了方便管理不同環境的接口和參數配置,可以使用環境變量 .env,如 .env、.env.local、.env.development、.env.production
配合 dotenv
庫 使用還是很方便的
包依賴分析可視化
插件:rollup-plugin-visualizer
import visualizer from 'rollup-plugin-visualizer';
visualizer({
filename: './node_modules/.cache/visualizer/stats.html',
open: true,
gzipSize: true,
brotliSize: true,
})
代碼壓縮
插件:vite-plugin-compression
import compressPlugin from 'vite-plugin-compression';
compressPlugin({
ext: '.gz',
deleteOriginFile: false,
})
Chunk 拆包
如果想把類似 ant-design-vue
這樣的包依賴單獨拆分出來,也可以手動配置 manualChunks
屬性
// vite.config.ts
build: {
rollupOptions: {
output: {
manualChunks: configManualChunk
}
}
}
// optimizer.ts
const vendorLibs: { match: string[]; output: string }[] = [
{
match: ['ant-design-vue'],
output: 'antdv',
},
{
match: ['echarts'],
output: 'echarts',
},
];
export const configManualChunk = (id: string) => {
if (/[\\/]node_modules[\\/]/.test(id)) {
const matchItem = vendorLibs.find((item) => {
const reg = new RegExp(`[\\/]node_modules[\\/]_?(${item.match.join('|')})(.*)`, 'ig');
return reg.test(id);
});
return matchItem ? matchItem.output : null;
}
};
兼容處理
插件:@vitejs/plugin-legacy
兼容不支持 <script type="module">
特性的瀏覽器,或 IE瀏覽器
// Native ESM
legacy({
targets: ['defaults', 'not IE 11']
})
// IE11
// 需要 regenerator-runtime
legacy({
targets: ['ie >= 11'],
additionalLegacyPolyfills: ['regenerator-runtime/runtime']
})
效果圖
首頁
包依賴分析可視化,部分截圖
[圖片上傳失敗...(image-1df5e5-1642403544292)]
開啟壓縮、開啟兼容后生產打包的產物
路由和布局
// router/index.ts
import { createRouter, createWebHashHistory } from 'vue-router'
import routes from './router.config'
const router = createRouter({
history: createWebHashHistory(), //
routes,
})
// main.ts
app.use(router); // 掛載后可全局使用實列,如模板中 <div @click="$router.push('xx')"></div>
用法如下:
// router.config.ts
import BasicLayout from '/@/layouts/BasicLayout/index.vue'; // 基本布局
import BlankLayout from '/@/layouts/BlankLayout.vue'; // 空布局
import type { RouteRecordRaw } from 'vue-router';
const routerMap: RouteRecordRaw[] = [
{
path: '/app',
name: 'index',
component: BasicLayout,
redirect: '/app/home',
meta: { title: '首頁' },
children: [
{
path: '/app/home',
component: () => import('/@/views/home/index.vue'),
name: 'home',
meta: {
title: '首頁',
icon: 'liulanqi',
auth: ['home'],
},
},
{
path: '/app/others',
name: 'others',
component: BlankLayout,
redirect: '/app/others/about',
meta: {
title: '其他菜單',
icon: 'xitongrizhi',
auth: ['others'],
},
children: [
{
path: '/app/others/about',
name: 'about',
component: () => import('/@/views/others/about/index.vue'),
meta: { title: '關于', keepAlive: true, hiddenWrap: true },
},
{
path: '/app/others/antdv',
name: 'antdv',
component: () => import('/@/views/others/antdv/index.vue'),
meta: { title: '組件', keepAlive: true, breadcrumb: true },
},
],
},
]
}
...
]
權限
- 支持頁面和菜單級別的權限管理、路由管理
- 支持按鈕級別的權限管理
- 支持接口級別的權限管理
幾個關鍵詞:router.addRoutes
動態路由、v-auth
指令、axios
攔截
使用 router.beforeEach
全局路由鉤子
核心邏輯如下,詳情見倉庫代碼 router/permission.ts
// 沒有獲取,請求數據
await permissioStore.fetchAuths();
// 過濾權限路由
const routes = await permissioStore.buildRoutesAction();
// 404 路由一定要放在 權限路由后面
routes.forEach((route) => {
router.addRoute(route);
});
// hack 方法
// 不使用 next() 是因為,在執行完 router.addRoute 后,
// 原本的路由表內還沒有添加進去的路由,會 No match
// replace 使路由從新進入一遍,進行匹配即可
next({ ...to, replace: true });
使用v-auth
指令控制按鈕級別的權限
function isAuth(el: Element, binding: any) {
const { hasPermission } = usePermission();
const value = binding.value;
if (!value) return;
if (!hasPermission(value)) {
el.parentNode?.removeChild(el);
}
}
axios
攔截
在 axios
請求攔截器 interceptors.request.use
添加
// 接口權限攔截
const store = usePermissioStoreWithOut();
const { url = '' } = config;
if (!WhiteList.includes(url) && store.getIsAdmin === 0) {
if (!store.getAuths.includes(url)) {
return Promise.reject('沒有操作權限');
}
}
總結
在開始使用 vite
+ vue3
的時候,也是邊踩坑邊學習開發的過程,好在現在社區比較活躍,很多問題都有對應的解決方案,配合文檔和github issue
一起食用基本ok,該項目也是參考了 vue-vben-admin
的一些實現和代碼管理,本文作為 vue3
使用學習記錄~
使用過之后會發現 vue3
和 vue2
有著完全不同的開發體驗,現在的 vue3
對 TS
有著極好的支持,開發效率和質量上上升了一個層次啊,而且也支持 JSX語法,類似 React
的形式開發也是可行的,當然,配合 vue模板使用時,也有著極大的靈活性,可自行根據場景定制自己的代碼,在結合目前的 script setup
開發,直接爽到起飛呀~
在使用 vue3
的 composition api
開發模式時,一定要摒棄之前的 options api
的開發邏輯,配和 hooks
可以自由組合拆分代碼,靈活性極高,方便維護管理,不會再出現 vue2
時代的整個代碼都擰在一起的情況
一句話:vite
+ vue3
+ setup
+ ts
+ vscode volar
插件,誰用誰知道,爽的一批~
倉庫地址:https://github.com/JS-banana/vite-vue3-ts