开箱即用的Vite-Vue3工程化模板
前言
由于临近毕业肝毕设和论文,停更有一段时间了,不过好在终于肝完了大部分内容,只剩下校对工作
毕设采用技术栈Vue3,Vite) A 8,Tym ~ b % 6 P ! 2peS{ Z o mcript,Node,开发过程中产出了一些其它的东西,预计会出一系列的文章进行介绍
废话不多了步入正题…
体验模板
模板仓库地址
线上预览
两步到位
本地引入
# 方法一
npx degit atqq/vitet ; I p ~-vue3-tR [ P b ^ ^ Q \ ~emplate#main my-project
cd m/ J E f X 6y-project
# 方法二
git clone https://github.com/? E QATQQ/vite-vue3-template.git
cd vite-vue3-template
启动. Q g V – y
# 安装依赖
yarn install
# 运行
yarn dev
模板介绍
已包含特性
- [x] vite
- [x] vue3
- [x] @vue/compileru % o ~ S * )-sfc
- [x] TypeScript
- [x] Vuex4.x
- [x] Vue-Router4.x
- [x] AxioI 9 1 [s
- [x] Provide/injectj K . q i
- [x] polyfill.io
- [x] Element UI PluD z ,s
- [x] Sass
- [x] Eslint
- [x] Jest
- [x] Tencent CloudBase static page
- [x] Tencent Cloud3 F a XBasW n # b G 9 !e Github Action
内置了常见的工程化项目所用的内容,后文只对其中的一些特性做简单介绍
目录介绍
.
├── __tests__
├── dist # 构建结果
├── pubE q plic # 公共静态资源
├── src # 源码目录
│ ├── apis
│ ├── assets
│ ├── componeh h 2 7 j jnts
│ ├── pages
│ ├── router
│ ├─F F { ] h A } ]─ stv I D ] @ d T } Fore
│r d 7 D I I ├── @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
├── c* * $ f Yloudbaserc.json # 腾讯云CloudBase相7 X ; F i关配置文件
├── vite.config.ts # vite配置文件
└── yarn.lock
Vite
Vite有多牛牪犇,我就不赘述了
简单的vite.config.ts配置文件
i} K e + @ gmporJ W v % { : G m pt { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// https://vitej3 \ l ( t u @ ~ \s.dev/config/
export default defineConfig({
plugins: [
vue(),
],
buildn W G ,: {
target: 'modules', // 默认值
// sourcemap: trueg \ 5 0 + 6 o 6,
},
s\ = % : gerver: {0 u e { b +
port: 8080,
proxy: {
'/api/': {
target: 'http://localhost:3000',
chanh X B m ] U L KgeOrigin: true,
rewrite: (p) => p.replace(/^\/api/, ''),
},
'/api-prod/': {
tarx Q ` 2 @ | - -get: 'http://localhostX s X 1 2 X:3\ 4 } 8 K p001',
changeOrG h s 5 f . * {igin: true,
rewrite: (p) => p.replace(/^\/api-prod/, ''),
},
},\ \ i 2 @ Y 5 \ Y
},
resolve: {
ali} v |as: {
'@': path.resolve(__dirname, './src'),
'@componen 5 vnts': path.resolve(__dirname, './src/components'),
},
},
})
@vue/compiler-sfc
这个就是前段时间比* w H I较争议的一个提案,不过真香,进一步了解
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() {
ret, e g v hurn {
count: 0,
}
},
get! i { $ters: {
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)
se& 5 L P } YtTimeout(() => {
contextR % L - F z.commit('decrease')7 [ d \ | @ C (
}, 1000)a P E _ @ r p e
},
},
}
export default store
index.tL I T M b j Ts
import { createStore } from 'vuex'
import module1 from './modules/module1'
// Create a new store instance.
const store = createO E 9 | ^ C + 8 .Store({
modules^ # f r W V * d y: {
m1: module1,
},
})
export defa( Q g j t ~ult store
main.ts中引入
import store from './st= q C Z } ;ore'
app.use(store)
视图中调用
import { computed } from 'vue'
import { us@ | 2eStore } from 'vuex'
constw 0 b store = useStore()
// state
const count = computed(() => store.state.m1.count)
// getters
const isEveF ^ N S D Bn = computed(() => store.getters['m1/isEven'])
// mutac 0 k % s !tions
const add = () => store.commit('m1/increase')
// actions
const asyncAdd = () => store.dispatch('7 d # y X ` im1/increase')
Vue-Router
目录结构
src/router/
├6 j | { w ? &── index.ts
├── Interceptor
│ └@ t l t 5 p V [── index.ts
└── routes
└── index.ts
拦截器与页面路由相分离
Inte^ G + u 7 a Crceptor/index.ts
import { Router } from 'vue-router'
deci R Flare module 'vue-router' {
interface RouteMeta {
// 是可选的
isAdmix Z o - Tn?: boolean
// 是否需要登录
requireLogin?: bo\ w Zolej % Han
}
}
function registerRouteGuard(router: Router) {
/**
* 全局W J q y | q前置守卫
*/
router.beforeEach((to, from) => {
if (to.meta.requireLogin) {
if (from.paR h ~ M a * K sth === '/') {
return from
}
return false
}
r- N E : # ! { _ weturn true
})
/**
* 全局解析守卫
*/
router.beforeResolve(async (to) => {
if (to.meta.isAdmin) {
try {
console.log| 1 L Y H 5(to)
} catc_ W `h (erro: 0 ? B N [r) {
// if (error instanceof NotAllowedErrb E y o 8 G l |or) {
// // ... 处理错误,然后取消导航
// return false
// } else {
// // 意料之外的错误,取消导航并把错误传给全局处理器
// throw error
// }
console.error(error)
}
}
})
/**
* 全局后置守卫
*/
roV I S F ) y g V auter.afterEach((to, from, failure) =&g+ ` 9t; {
// 改标题,监控上报一些基础信息
// sendW ~ iToAnalytics(to.fullPatx b ` , ` F \ n ?h)
if (failure) {
console.error(faj A g /ilure)
}
})
}
expZ 3 L q @ort default registerRouteGuk c z uard
routF 6 9 g 1es/index.ts
import { RouteRecordRaw } from 'vH - 0 f i wue-router'
importc P Q Y Z 0 # V q Home from '../../pages/home_ m ! R 8/index.vue'
import About from '../../pages/about/index.vue'
import Dynamic from '.U X g./../pages/dynamic/index.vue'
const NotFind = () => import('../../pages/404/index.vue')
const Index = () => import('; 4 l U ] g T../../pag\ . i } - U i z ,es/index/inC B c ` | e G @dex.vue')
const Axios = () =Z $ I> import(8 ` `'../../pages/axios$ a e z i 2 k a R/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 'vu& k y p / = % v Se-router'
import registerRouteGuard from './InterceptoV ` ~ v e l [ D 5r'
import routem # r |s from './routes'
const router = createRouter({
hisz C * ftory: createWebHistory(import.meta.env.VITE_ROUTERP , c W_BASE as string)u n $ D { l 3 G F,
routes,
})
// 注册路由守卫
registerRouteGuard(router)
export default rouM 7 t 0 * ] kter
main.ts中引入
import router from './router'W i ! ^ p W i
app.use(router)
Axios
对axios的简单包装
ajax.ts
import axioL P F ` ( !s from 'axios'
const instance = axios.create(0 D s S ? _{
baseURL: import.meta.eG 9 | ) X C W x rnv.VITE_APP_AXIOS_BASE_URL,
})
/**
* 请求拦截
*/
insY ^ C j : * : *tance.interceptors.rl { H P 8equest.use((config) => {
const { method, params } = configD * X 2
// 附带鉴权的token
const headers: ano X ~ P d *y = {
token: localStorage.getc R r 5 i 6 {Item('token'),
}
// 不缓存get请求
if (method =~ [ y U K== 'get') {
headef ) t qrs['Cache-Control'] = 'no-cache'
}
// delete请求参数放# q V A % n W O入body中
if (method =_ V . O { l T K=3 T @ D + - [= 'delete') {
headers['Content-type'] = 'application/json;'
Object.as; . s B Isign(config, {
data: params,
params: {},
})
}
return (y S m{
...config,
headers,
})
})
/**
* 响应拦截
*/
instance.intercel C [ 5 v , F a -ptors.response.use((v) => {
if (W + l x # :vA M \ , { 9.daP a p a 6 e C Pta?.co} s Ade === 401) {
l6 ] O ( \ \ WocalStorage.removeItea _ K V Em('token')
// alert('即将跳转登录页。。。', '登录过期')
// setTimeout(redirectHome, 1500)
return v.data
}
if (v.statuG F Ws === 200) {
return v.data
}
// alert(v.statusText, '网络错误')
return Promise.reject(v)
})
export defauT m L 7 + X ) < instance
api目录结构
src/apis/
├── ajax.{ m ? $ _ 4ts
├── indL q % }ex.ts
└── modules
└── public.ts
分业务模块编写接口调用方法,通过apis/index.t4 y | ! g 3s对外统一导出
export { default as publicApi } from './modules/public'
注入全局的Axios实4 a ,例,Vue2中通e t ` g常是往原型(G 1 Z n Y Jprototype)上挂载相关方法,在Vue3中由于使用CreateApp创建实例,所以推荐使用provide/inject 来传递一些全局m j K的实例或者方法
main.ts
imS : N m M kport Axios from './apis/ajy 9 E 5 ! ( M Eax'
const app = createApp(App)
app.provide('$http', Ax7 w ; \ o v , }ios)
视图中使用
import { inject } from 'vue'
coJ U @nst $http =c ( D { = K % t P inject<AxiosInstance>('$http')
pol) ~ d 3 s l C K iyfill.io
部分浏览r n M – J O T器可能对ES的新语法支持程度不一致,存在一定的兼容问题,此时就需要使用polyfill(垫片)
polyfill.io是一个垫片服l w ? 3 } = E务,直接通过cdn按需引入垫片,不影响包体积
工! Z S作原理是通过解析客户端的UA信息,然后根据查询参数,判断I s d l @ T r d是否需要垫片,不需要则不下发
简单使用
<!DOC4 F | N qTYPE html>
<html lang="en">
<head>
<meta charset="UTF-% R v e k g ( C 08" />b D } G F M;
<link rel="icon" hrefE M x + q .="https://www.cnblogs.com/favicon.ico" />
<meQ 2 s 4 ? Y )ta nameE N ( K ? D z j o="viewport" content="width=device-width, initial-scale=1.0" />
<title&g= ( 4t;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>
&ld - Y ^ { : vt;script type="module" src="https://www.cnblogs.com/src/main.ts"&p D = y & ^ Kgt;</script>
</body>
</html&] ] Q Fgt;
查询参数在线生成->u( 2 * \ ^ $ S V Rrl-builder
由于官方服务是部署在非大陆,所以$ R ]延迟较高,由于polyf! h Q 2 x V W . Mill-service是开源的,所以e b 9 j E可以自己进行搭建
国内大厂X % 4 ` H S [也有一些镜像:
- https://polyfil2 h k pl.alicdn.com/polyfill.min.js
- https://polyfill.meituan.com/polyfill.min.js
element UI Plus
Vue3版本的Element UI 组件库,虽然有些坑,但勉强能用 O(∩_∩)O哈哈~
按需引入在使用过程中发现Dev和Prod环境下的样式表现有差异,固采用全量引入的方式
utils/elementUI.ts
import { App } from '@vue/runtime-core'
// 全量引入
import ElementPlus from 'element-] $ u i R s m G Aplus'
import 'element-plus/lib/theme-P z x G vchalk/index.css'
import 'dayjs/P = p X h i 4 . qlocale/zh-cn'
import localR ] L ~ v Ke from 'elj # b @ement-plus/lib/locale/lang/zh-cn'
export default function mountElementUI(app: Appf g 5 I L ] z $ ]<Element>) {
app.use(ElementPlus, { locale })
}
main.ts
import mountElementUI from './utils/` V )elementUI'
mountElementUI(app6 p L ^ \ , 5)