開箱即用的Vite-Vue3工程化模板
前言
由於臨近畢業肝畢設和論文,停更有一段時間了,不過好在終於肝完了大部分內容,只剩下校對工作
畢設採用技術棧Vue3,Vite,TypeScript,Node,開發過程中產出了一些其它的東西,預計會出一系列的文章進行介紹
廢話不多了步入正題...
體驗模板
兩步到位
本地引入
# 方法一
npx degit atqq/vite-vue3-template#main my-project
cd my-project
# 方法二
git clone https://github.com/ATQQ/vite-vue3-template.git
cd vite-vue3-template
啟動
# 安裝依賴
yarn install
# 執行
yarn dev
模板介紹
已包含特性
- [x] vite
- [x] vue3
- [x] @vue/compiler-sfc
- [x] TypeScript
- [x] Vuex4.x
- [x] Vue-Router4.x
- [x] Axios
- [x] Provide/inject
- [x] polyfill.io
- [x] Element UI Plus
- [x] Sass
- [x] Eslint
- [x] Jest
- [x] Tencent CloudBase static page
- [x] Tencent CloudBase Github Action
內建了常見的工程化專案所用的內容,後文只對其中的一些特性做簡單介紹
目錄介紹
.
├── __tests__
├── dist # 構建結果
├── public # 公共靜態資源
├── src # 原始碼目錄
│ ├── apis
│ ├── assets
│ ├── components
│ ├── pages
│ ├── router
│ ├── store
│ ├── @types
│ ├── utils
│ ├── shims-vue.d.ts
│ ├── env.d.ts
│ ├── main.ts
│ └── App.vue
├── README.md
├── index.html # 應用入口
├── jest.config.ts
├── LICENSE
├── package.json
├── tsconfig.json
├── cloudbaserc.json # 騰訊雲CloudBase相關配置檔案
├── vite.config.ts # vite配置檔案
└── yarn.lock
Vite
Vite有多牛牪犇,我就不贅述了
簡單的vite.config.ts配置檔案
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
],
build: {
target: 'modules', // 預設值
// sourcemap: true,
},
server: {
port: 8080,
proxy: {
'/api/': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/api/, ''),
},
'/api-prod/': {
target: 'http://localhost:3001',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/api-prod/, ''),
},
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
},
},
})
@vue/compiler-sfc
這個就是前段時間比較爭議的一個提案,不過真香,進一步瞭解
Vuex
採用分業務模組的方案
目錄結構
src/store/
├── index.ts
└── modules
└── module1.ts
module1.ts
import { Module } from 'vuex'
interface State {
count: number
}
const store: Module<State, unknown> = {
namespaced: true,
state() {
return {
count: 0,
}
},
getters: {
isEven(state) {
return state.count % 2 === 0
},
},
// 只能同步
mutations: {
increase(state, num = 1) {
state.count += num
},
decrease(state) {
state.count -= 1
},
},
// 支援非同步,可以考慮引入API
actions: {
increase(context, payload) {
context.commit('increase', payload)
setTimeout(() => {
context.commit('decrease')
}, 1000)
},
},
}
export default store
index.ts
import { createStore } from 'vuex'
import module1 from './modules/module1'
// Create a new store instance.
const store = createStore({
modules: {
m1: module1,
},
})
export default store
main.ts中引入
import store from './store'
app.use(store)
檢視中呼叫
import { computed } from 'vue'
import { useStore } from 'vuex'
const store = useStore()
// state
const count = computed(() => store.state.m1.count)
// getters
const isEven = computed(() => store.getters['m1/isEven'])
// mutations
const add = () => store.commit('m1/increase')
// actions
const asyncAdd = () => store.dispatch('m1/increase')
Vue-Router
目錄結構
src/router/
├── index.ts
├── Interceptor
│ └── index.ts
└── routes
└── index.ts
攔截器與頁面路由相分離
Interceptor/index.ts
import { Router } from 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
// 是可選的
isAdmin?: boolean
// 是否需要登入
requireLogin?: boolean
}
}
function registerRouteGuard(router: Router) {
/**
* 全域性前置守衛
*/
router.beforeEach((to, from) => {
if (to.meta.requireLogin) {
if (from.path === '/') {
return from
}
return false
}
return true
})
/**
* 全域性解析守衛
*/
router.beforeResolve(async (to) => {
if (to.meta.isAdmin) {
try {
console.log(to)
} catch (error) {
// if (error instanceof NotAllowedError) {
// // ... 處理錯誤,然後取消導航
// return false
// } else {
// // 意料之外的錯誤,取消導航並把錯誤傳給全域性處理器
// throw error
// }
console.error(error)
}
}
})
/**
* 全域性後置守衛
*/
router.afterEach((to, from, failure) => {
// 改標題,監控上報一些基礎資訊
// sendToAnalytics(to.fullPath)
if (failure) {
console.error(failure)
}
})
}
export default registerRouteGuard
routes/index.ts
import { RouteRecordRaw } from 'vue-router'
import Home from '../../pages/home/index.vue'
import About from '../../pages/about/index.vue'
import Dynamic from '../../pages/dynamic/index.vue'
const NotFind = () => import('../../pages/404/index.vue')
const Index = () => import('../../pages/index/index.vue')
const Axios = () => import('../../pages/axios/index.vue')
const Element = () => import('../../pages/element/index.vue')
const routes: RouteRecordRaw[] = [
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFind },
{
path: '/',
name: 'index',
component: Index,
children: [
{ path: 'home', component: Home, name: 'home' },
{ path: 'about', component: About, name: 'about' },
{ path: 'axios', component: Axios, name: 'axios' },
{ path: 'element', component: Element, name: 'element' },
{
path: 'dynamic/:id',
component: Dynamic,
meta: {
requireLogin: false,
isAdmin: true,
},
name: 'dynamic',
},
],
},
]
export default routes
router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import registerRouteGuard from './Interceptor'
import routes from './routes'
const router = createRouter({
history: createWebHistory(import.meta.env.VITE_ROUTER_BASE as string),
routes,
})
// 註冊路由守衛
registerRouteGuard(router)
export default router
main.ts中引入
import router from './router'
app.use(router)
Axios
對axios的簡單包裝
ajax.ts
import axios from 'axios'
const instance = axios.create({
baseURL: import.meta.env.VITE_APP_AXIOS_BASE_URL,
})
/**
* 請求攔截
*/
instance.interceptors.request.use((config) => {
const { method, params } = config
// 附帶鑑權的token
const headers: any = {
token: localStorage.getItem('token'),
}
// 不快取get請求
if (method === 'get') {
headers['Cache-Control'] = 'no-cache'
}
// delete請求引數放入body中
if (method === 'delete') {
headers['Content-type'] = 'application/json;'
Object.assign(config, {
data: params,
params: {},
})
}
return ({
...config,
headers,
})
})
/**
* 響應攔截
*/
instance.interceptors.response.use((v) => {
if (v.data?.code === 401) {
localStorage.removeItem('token')
// alert('即將跳轉登入頁。。。', '登入過期')
// setTimeout(redirectHome, 1500)
return v.data
}
if (v.status === 200) {
return v.data
}
// alert(v.statusText, '網路錯誤')
return Promise.reject(v)
})
export default instance
api目錄結構
src/apis/
├── ajax.ts
├── index.ts
└── modules
└── public.ts
分業務模組編寫介面呼叫方法,通過apis/index.ts對外統一匯出
export { default as publicApi } from './modules/public'
注入全域性的Axios例項,Vue2中通常是往原型(prototype)上掛載相關方法,在Vue3中由於使用CreateApp建立例項,所以推薦使用provide/inject 來傳遞一些全域性的例項或者方法
main.ts
import Axios from './apis/ajax'
const app = createApp(App)
app.provide('$http', Axios)
檢視中使用
import { inject } from 'vue'
const $http = inject<AxiosInstance>('$http')
polyfill.io
部分瀏覽器可能對ES的新語法支援程度不一致,存在一定的相容問題,此時就需要使用polyfill(墊片)
polyfill.io是一個墊片服務,直接通過cdn按需引入墊片,不影響包體積
工作原理是通過解析客戶端的UA資訊,然後根據查詢引數,判斷是否需要墊片,不需要則不下發
簡單使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<script
src="https://polyfill.alicdn.com/polyfill.min.js?features=es2019%2Ces2018%2Ces2017%2Ces5%2Ces6%2Ces7%2Cdefault"></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
查詢引數線上生成->url-builder
由於官方服務是部署在非大陸,所以延遲較高,由於polyfill-service是開源的,所以可以自己進行搭建
國內大廠也有一些映象:
element UI Plus
Vue3版本的Element UI 元件庫,雖然有些坑,但勉強能用 O(∩_∩)O哈哈~
按需引入在使用過程中發現Dev和Prod環境下的樣式表現有差異,固採用全量引入的方式
utils/elementUI.ts
import { App } from '@vue/runtime-core'
// 全量引入
import ElementPlus from 'element-plus'
import 'element-plus/lib/theme-chalk/index.css'
import 'dayjs/locale/zh-cn'
import locale from 'element-plus/lib/locale/lang/zh-cn'
export default function mountElementUI(app: App<Element>) {
app.use(ElementPlus, { locale })
}
main.ts
import mountElementUI from './utils/elementUI'
mountElementUI(app)