注:首先请确保客官您的电脑已经正确安装和配置好node环境!
zhinian@192 ~ % node -v
v16.0.0
zhinian@192 ~ % npm -v
7.10.0
npm create vite
zhinian@192 % npm create vite
Need to install the following packages:
create-vite
Ok to proceed? (y) y
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE package: 'create-vite@5.0.0',
npm WARN EBADENGINE required: { node: '^18.0.0 || >=20.0.0' },
npm WARN EBADENGINE current: { node: 'v16.0.0', npm: '7.10.0' }
npm WARN EBADENGINE }
? Project name: … zhinian_vue_tpl
? Select a framework: ? Vue
? Select a variant: ? TypeScript
Scaffolding project in /Users/zhinian/vscode/zhinian_vue_tpl...
Done. Now run:
cd zhinian_vue_tpl
npm install
npm run dev
依次填写项目名称,选择Vue框架,选择TypeScript语言。
初始化完成之后,将工程拖入到vs code编辑器,查看工程项目组织结构,并且在package.json文件中可以看到项目的相关脚本指令。
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview"
},
先安装依赖 npm install -D @types/node
vite.config.ts 增加 resolve 配置:
import { resolve } from 'path';
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
}
})
安装依赖:
npm install element-plus --save
//选择按需导入,官网推荐
npm install -D unplugin-vue-components unplugin-auto-import
// vite.config.ts
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
// ...
plugins: [
// ...
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})
//测试
<el-button>我是 ElButton</el-button>
在vite.config.ts中添加serve配置
//安装依赖
npm install
//运行程序
npm run dev
// 访问: http://localhost:8088
在初始化时其实已经安装过依赖(不够准确),这里重新操作一下
npm install -D @types/node
安装完成后项目src文件夹下会自动生成一个vite-env.d.ts文件。
安装完@types/node后,我们需要在vite.config.ts文件中进行相关配置,具体代码如下
//引入
import { resolve } from "path";
//进行配置
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
// 类型: string[] 导入时想要省略的扩展名列表。
extensions: ['.js', '.ts', '.jsx', '.tsx', '.json', '.vue', '.mjs']
}
这时候就可以用 @/路径
的形式来简化引用src文件夹下的文件了。
我们在src文件夹下新建utils文件夹,在该文件夹下新建文件alias.ts,随便写一个方法如下:
然后在HelloWorld.vue中引入该方法并调用
//引入
import alias from "@/utils/alias"
//调用
<button type="button" @click="alias">点击触发引用</button>
npm run serve
运行后点击按钮可以看到方法被正常执行了
在配置完成后,我们可以看到编辑器有错误提示的标记,错误内容“找不到模块 "@/utils/alias" 或其相应的类型声明
虽然不影响程序正常运行,但是看着也是烦人,所以我们也要解决这个错误显示,解决方法也很简单,在tsconfig.json文件添加上如下代码就ok了!
"baseUrl": "./",
"paths": {
"@/*":["src/*"]
}
至此,别名设置完成
//安装vue-router
npm i vue-router -S
在src文件夹下新建views文件夹,在views文件夹下新建index.vue文件,这个文件就是我们在切换路由时要显示的页面,写入代码如下:
<template>
<div class="main-box">
<h1>进行路由模块测试界面</h1>
</div>
</template>
在src文件夹下新建router文件夹,在router文件夹下新建index.ts文件,写入代码如下:
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
// import Home from '@/views/index.vue'
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: () => import('@/views/index.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
上面创建好路由文件后需要在main.ts文件中引入,具体如下:
//引入路由
import router from './router'
//调用路由
createApp(App).use(router).mount('#app')
重写App.vue文件,设置初始路由为我们配置的Home,具体如下:
<template>
<router-view />
</template>
OK,路由配置完毕!
//安装sass库
npm install -D sass sass-loader
我们修改src/views文件夹下的index.vue文件,添加样式处理内容,具体代码如下:
<template>
<div class="main-box">
<h1>进行路由模块测试界面</h1>
</div>
</template>
<style lang="scss">
.main-box {
width: 650px;
height: 120px;
background: rgb(170, 170, 161);
padding: 25px;
h1 {
color: purple;
font-weight: bold;
}
}
</style>
至此,CSS样式预处理器sass安装配置完成!
pinia是官方推出的新的vuex版本,比起之前的版本使用起来简单方便,而且模块化更加清晰,维护成本更低,所以更推荐大家使用pinia来做状态管理。
//安装pinia
npm i pinia -D
安装完后,我们在main.ts文件中进行引入和使用,代码如下:
//引入createPinia方法从pinia
import { createPinia } from "pinia";
//拿到pinia实例
const pinia = createPinia()
//使用pinia
createApp(App).use(router).use(pinia).mount('#app')
在项目src下创建store文件夹,以后项目中所有的状态管理部分文件都将放到store文件夹下。
在store文件夹下新建types文件夹,types文件夹为状态模块类型管理文件夹,创建模块类型文件home.ts,写入代码如下:
export type storeHome = {
count: number,
status: boolean
}
在store文件夹下新建modules文件夹,modules文件夹为状态模块管理文件夹,创建模块文件home.ts,写入代码如下:
import { defineStore } from 'pinia'
import { storeHome } from '../types/home'
export const useHomeStore = defineStore('index', {
state: (): storeHome => {
return {
count: 0,
status: false
}
},
getters: {
curCount(): number {
return this.count * 3
},
curStatus(): boolean {
return this.status
}
},
actions: {
updatecount(val: number) {
this.count = val
},
changeStatus(val: boolean) {
this.status = val
}
}
})
修改views/index.vue代码如下:
<template>
<div class="main-box">
<h1>状态管理测试界面</h1>
<h1>状态count:{{ count }}</h1>
<h1>状态curCount:{{ calculateCount }}</h1>
<button @click="initCount">归零</button>
<button @click="changeCount">随机</button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed } from 'vue';
import { useHomeStore } from "@/store/modules/home";
export default defineComponent({
name: "Home",
setup() {
const homeStore = useHomeStore()
let count = computed(() => homeStore.count)
let calculateCount = computed({
get(): number {
return homeStore.curCount
},
set(val: number): void {
homeStore.updatecount(val)
}
})
const initCount = () => {
calculateCount.value = 0
}
const changeCount = () => {
const num = Math.random() * 100
calculateCount.value = Math.floor(num)
}
return { count, calculateCount, initCount, changeCount }
}
})
</script>
<style lang="scss">
.main-box {
width: 650px;
background: rgb(170, 170, 161);
padding: 25px;
h1 {
color: purple;
font-weight: bold;
}
}
</style>
ok,状态管理器配置完毕!
Element Plus官网提供了全局导入和按需导入两种形式,为了避免打包时占用的体积过大,个人还是推荐使用按需导入的形式来引入Element Plus组件的。
npm i element-plus -S
首先需要安装 unplugin-vue-components
和 unplugin-auto-import
这两款插件
npm i unplugin-vue-components unplugin-auto-import -D
插件安装完成后,在vite.config.ts中添加配置代码
//引入插件
import AutoImport from "unplugin-auto-import/vite"
import Components from "unplugin-vue-components/vite"
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
//使用插件
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
})
修改 views/index.vue
文件,添加普通组件 el-button
和 api
组件 ElMessage
<template>
<div class="main-box">
<div>
<h1>状态管理测试界面</h1>
<h1>状态count:{{ count }}</h1>
<h1>状态curCount:{{ calculateCount }}</h1>
<button @click="initCount">归零</button>
<button @click="changeCount">随机</button>
</div>
<div>
<el-button @click="showMessage" size="small">显示弹窗</el-button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed } from 'vue';
import { useHomeStore } from "@/store/modules/home";
import { ElMessage } from "element-plus"
export default defineComponent({
name: "Home",
setup() {
const homeStore = useHomeStore()
let count = computed(() => homeStore.count)
let calculateCount = computed({
get(): number {
return homeStore.curCount
},
set(val: number): void {
homeStore.updatecount(val)
}
})
const initCount = () => {
calculateCount.value = 0
}
const changeCount = () => {
const num = Math.random() * 100
calculateCount.value = Math.floor(num)
}
const showMessage = () => {
ElMessage({
showClose: true,
message: "登录注册成功",
type: "success",
});
}
return { count, calculateCount, initCount, changeCount, showMessage }
}
})
</script>
<style lang="scss">
.main-box {
width: 650px;
background: rgb(170, 170, 161);
padding: 25px;
margin: 0 auto;
h1 {
color: purple;
font-weight: bold;
}
}
</style>
运行后发现,类似el-button这类普通组件可以直接使用,但是ElMessage这类API组件使用时样式出现了问题,无法正常显示
其实处理方法很简单,因为我们已经利用插件来按需导入了,所以就不需要再在界面引入element-plus,把下面的这段代码删掉
import { ElMessage } from "element-plus"
去掉之后,会发现提示错误,找不到“找不到名称 "ElMessage ts(2304)"
。
解决方案有两个办法:
我们只需要在tsconfig.json中对这两个文件进行配置即可,具体如下图
OK,至此Element Plus安装配置完成!
有时候可能需要对请求或者相应参数进行解析或者格式转换,所以除了axios库之外,一般我们还会安装qs(一个流行的查询参数序列化和解析库),qs可以将一个普通的object序列化成一个查询字符串,或者反过来将一个查询字符串解析成一个object,而且支持复杂的嵌套。
//安装axios
npm i axios -S
//安装qs
npm i qs -S
项目根目录下创建文件 .env.dev
(开发测试环境)和文件 .env.prod
(生产环境)
.env.dev文件代码如下
#(.env.dev 测试/开发环境变量配置)
VITE_ENV = development
# base api
# 初始地址, 注意端口号要与vite.config.ts的server部分一致
VITE_SOURCE_URL = http://localhost:8088
# 基础域名
VITE_BASE_API = http://localhost:8088
# 服务地址
VITE_SERVE_ADD = /api
.env.prod文件代码如下
#(.env.prod 生产环境变量配置)
VITE_ENV = development
# base api
# 初始地址, 注意端口号要与vite.config.ts的server部分一致
VITE_SOURCE_URL = http://192.168.5.106:8088
# 基础域名
VITE_BASE_API = http://192.168.5.106:8088
# 服务地址
VITE_SERVE_ADD = /api
修改package.js的脚本命令内容
"scripts": {
"dev": "vite --mode dev",
"prod": "vite --mode prod",
"build": "vue-tsc && vite build --mode prod",
"preview": "vite preview"
},
这样配置完成后,运行npm run dev或npm run prod或build时会把自定义的环境变量载入进去,避免了每次打包和开发都要临时修改变量的尴尬。
上面我们已经创建好了环境变量,在src目录下新建config.ts文件来接收环境变量以及后面可能出现的公用配置。
/** 环境变量 */
const ENV = import.meta.env; // vite是以这种方式获取环境变量
/** 基础域名 */
export const SOURCE_URL = ENV.VITE_SOURCE_URL;
export const BASE_URL = ENV.VITE_BASE_API;
/** 基础服务地址 */
export const URL = BASE_URL + '/api';
/** 超时时间 */
export const TIMEOUT = 6000;
这样在后面创建axios实例和封装api的时候,就可以直接通过config.ts文件来读取配置了。
在项目src/utils目录下(没有的话创建即可)新建request.ts文件,这个文件就是我们要书写axios的api封装。封装过程中用到了Message组件,刚好上一篇我们已经介绍了安装及配置ElementPlus的过程。同时引入token管理。
import axios, { AxiosRequestConfig } from 'axios'
import qs from 'qs'
import { setLocalStorage, getLocalStorage } from './localstorage'
import { BASE_URL, TIMEOUT, SOURCE_URL } from "@/config";
导入qs的时候一般情况下,编辑器会报错 “无法找到模块"qs"的声明文件”
因为TypeScript中s不能直接这样引入js类型库,解决方法:在src目录下新建一个globe.d.ts,添加如下代码即可
declare module "qs"
const instance = axios.create({
baseURL: BASE_URL,
timeout: TIMEOUT,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
// http request 请求拦截器
instance.interceptors.request.use(
config => {
config.headers.AcceptLanguage = getLocalStorage("locale");
if (localStorage.myToken) {
config.headers.Authorization = getLocalStorage("myToken");
}
return config
},
err => {
return Promise.reject(err)
}
)
// http response 响应拦截器
instance.interceptors.response.use(
response => {
return handleData(response.data)
},
error => {
const errData = error.response.data
if (errData.status === 500) {
setLocalStorage('myToken');
window.location.href = sourceUrl;
}
let err = errData.message;
if (err != '' && err != null && err != undefined) {
ElMessage({
type: 'error',
message: errData.message
})
return Promise.reject(errData)
} else {
ElMessage({
type: 'error',
message: 'HTTP:服务器遇到错误,请求失败。'
})
}
}
)
// API封装
const get = async (url: string) => {
/**
......
可以在这里自定义封装处理方法
......
*/
try {
return await instance
.get(url)
} catch (error) {
return handleError(error)
}
}
request.ts
import axios, { AxiosRequestConfig } from 'axios'
import qs from 'qs'
import { setLocalStorage, getLocalStorage } from './localstorage'
import { BASE_URL, TIMEOUT, SOURCE_URL } from "@/config";
const instance = axios.create({
baseURL: BASE_URL,
timeout: TIMEOUT,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
// http request 请求拦截器
instance.interceptors.request.use(
config => {
config.headers.AcceptLanguage = getLocalStorage("locale");
if (localStorage.myToken) {
config.headers.Authorization = getLocalStorage("myToken");
}
return config
},
err => {
return Promise.reject(err)
}
)
// http response 响应拦截器
instance.interceptors.response.use(
response => {
return handleData(response.data)
},
error => {
const errData = error.response.data
if (errData.status === 500) {
setLocalStorage('myToken');
window.location.href = SOURCE_URL;
}
let err = errData.message;
if (err != '' && err != null && err != undefined) {
ElMessage({
type: 'error',
message: errData.message
})
return Promise.reject(errData)
} else {
ElMessage({
type: 'error',
message: 'HTTP:服务器遇到错误,请求失败。'
})
}
}
)
// API封装
const get = async (url: string) => {
/**
......
可以在这里自定义封装处理方法
......
*/
try {
return await instance
.get(url)
} catch (error) {
return handleError(error)
}
}
const post = async (url: string, data?: any, config?: AxiosRequestConfig<any> | undefined) => {
/**
......
可以在这里自定义封装处理方法
......
*/
try {
return await instance
.post(url, data, config)
} catch (error) {
return handleError(error)
}
}
const deleteFn = async (url: string, config?: AxiosRequestConfig<any> | undefined) => {
/**
......
可以在这里自定义封装处理方法
......
*/
try {
return await instance
.delete(url, config)
} catch (error) {
return handleError(error)
}
}
const postJSON = async (url: string, data?: any, config?: AxiosRequestConfig<any> | undefined) => {
/**
......
可以在这里自定义封装处理方法
......
*/
data = qs.stringify(data);
try {
return await instance
.post(url, data, config)
} catch (error) {
return handleError(error)
}
}
const patchFn = async (url: string, data?: any, config?: AxiosRequestConfig<any> | undefined) => {
/**
......
可以在这里自定义封装处理方法
......
*/
try {
return await instance
.patch(url, data, config)
} catch (error) {
return handleError(error)
}
}
// 对请求返回的错误进行自处理
function handleError(error: any) {
return error
}
// 对响应的数据进行自处理
function handleData(data: any) {
return data
}
export default {
get: get,
post: post,
postJSON: postJSON,
delete: deleteFn,
patch: patchFn
}
localstorage.ts
export const setLocalStorage = (key: string, value?: string, hours?: number) => {
value = JSON.stringify(value);
// 设置过期原则
if (!value) {
localStorage.removeItem(key)
} else {
let Hours = hours || 24; // 以小时为单位,默认24小时
let exp = new Date();
localStorage[key] = JSON.stringify({
value,
expires: exp.getTime() + Hours * 1000 * 60 * 60,//失效时间
})
}
}
export const getLocalStorage = (key: string) => {
try {
let ls = JSON.parse(localStorage[key]);
if (!ls || ls.expires < Date.now()) {
return ''
} else {
return JSON.parse(ls.value)
}
} catch (e) {
// 兼容其他localstorage
return localStorage[key]
}
}
config.ts
/** 环境变量 */
const ENV = import.meta.env; // vite是以这种方式获取环境变量
/** 基础域名 */
export const SOURCE_URL = ENV.VITE_SOURCE_URL;
export const BASE_URL = ENV.VITE_BASE_API;
/** 基础服务地址 */
export const URL = BASE_URL + '/api';
/** 超时时间 */
export const TIMEOUT = 6000;
OK,这样基本就完成了axios的安装和api封装了!
上一章我们介绍了Vite+Vue3+TypeScript项目中axios的安装和配置,并手动封装了api。本篇我们来在上篇基础上介绍如何引入mock,并在本地模拟后台接口请求来达到本地测试的目的。
在现在前后端分离的开发模式中,前端页面很多渲染的数据都需要通过http请求异步从服务器获取,前端很多开发工作都要依赖于后端的接口。但是实际项目上往往前端和后台的开发并行,甚至前端都做了很多工作了,后台还没开始,这就没办法满足前端对后台接口的需求。所以前端需要一种方式可以来模拟数据请求,从而更多的掌握主动权独立开发项目,mockjs可以以无侵入的方式拦截 ajax 请求,通过模拟服务器端响应来返回数据,废话不所说,大家撸起来…
项目中mockjs主要用途一般为模拟后台数据接口,所以安装为开发依赖就可以,生产环境下失效。
npm i mockjs vite-plugin-mock -D
在src目录下新建mock文件夹,新建index.ts,代码如下
import { MockMethod } from "vite-plugin-mock"
const mock: Array<MockMethod> = [
{
// 接口路径
url: '/api/test',
// 接口方法
method: 'get',
// 返回数据
response: () => {
return {
status: 200,
message: 'success',
data: 'mock模拟数据请求成功!'
}
}
}
]
export default mock
在src目录下新建api文件夹,新建index.ts,代码如下
import request from "@/utils/request";
export const testApi = () => {
return request.get("/api/test")
}
为了正常要使用mock,还需要在vite.config.ts文件对应位置对mock进行如下配置,在vite启动项目的同时启动mock服务。
//引入mock
import { viteMockServe } from "vite-plugin-mock";
//启动mock服务
viteMockServe({
supportTs: false,
logger: false,
mockPath: "./src/mock/"
})
supportTs
有红波浪线:
对象字面量只能指定已知属性,并且“supportTs”不在类型“ViteMockOptions”中。
解决:
卸载vite-plugin-mock
npm uninstall vite-plugin-mock
从新安装 2.9.6版本
npm install mockjs vite-plugin-mock@2.9.6 -D
成功解决。
在src/views/index.vue文件添加如下代码进行接口测试
<div>
<h1>mock接口测试</h1>
<el-button size="small" @click="mockTest">testApi</el-button>
</div>
const mockTest = () => {
testApi().then((datas) => {
console.log(datas)
})
}
启动项目后,点击按钮,发现模拟接口返回数据正常
至此,mockjs安装和配置完成!
屏幕适配方案,简单说来就是要最大程度上保证我们的界面在各种各样的终端设备上显示正常。
通用的屏幕适配方案有两种:
① 基于rem 适配(推荐,也是本篇要实现的方案)
适用场景:不固定宽高比的Web应用,适用于绝大部分业务场景
② 基于 scale 适配
适用场景:固定宽高比的Web应用,如大屏或者固定窗口业务应用
个人还是比较推荐基于rem适配屏幕方案的,就算是大屏,还得分个屏大屏小呢,更何况比例也经常不一致,4:3的,16:9的,我还见过3:2的呢,瞅着不就是个大电视吗?废话少说,撸起来…
rem适配方案主要用到三个依赖包:
postcss-pxtorem:PostCSS的插件,用于将像素单元生成rem单位
autoprefixer:浏览器前缀处理
amfe-flexible:可伸缩布局方案,替代了原先lib-flexible,选用了当前众多浏览器兼容的viewport
//安装依赖
npm i postcss-pxtorem autoprefixer amfe-flexible -S
在vite.config.ts文件中引入对应依赖包,并配置代码如下(因为vite已经内联了postcss,所以并不需要再去创建什么postcss.config.js文件,直接在vite.config.ts中配置css即可)
//引入依赖
import autoprefixer from "autoprefixer"
import postcsspxtorem from "postcss-pxtorem"
//配置适配方案
css: {
postcss: {
plugins: [
autoprefixer({
overrideBrowserslist: [
"Android 4.1",
"iOS 7.1",
"Chrome > 31",
"ff > 31",
"ie >= 8",
"last 10 versions", // 所有主流浏览器最近10版本用
],
grid: true
}),
postcsspxtorem({
rootValue: 192, // 设计稿宽度的1/ 10 例如设计稿按照 1920设计 此处就为192
propList: ["*", "!border"], // 除 border 外所有px 转 rem
selectorBlackList: [".el-"], // 过滤掉.el-开头的class,不进行rem转换
})
],
},
}
在main.ts文件中导入依赖
import "amfe-flexible/index.js";
运行效果如下:
OK,至此屏幕适配完成!
本章我们来介绍封装SVG图标组件。
svg特征
Preloading所有图标都是在项目运行时生成的,只需要操作一次dom即可。
高性能内置缓存,仅在文件被修改时才会重新生成。
//安装依赖
npm i vite-plugin-svg-icons -D
在vite.config.ts中添加相关配置内容
//引入依赖
import path from "path";
import { createSvgIconsPlugin } from "vite-plugin-svg-icons";
//启用插件
createSvgIconsPlugin({
// 指定图标文件夹,绝对路径(NODE代码)
iconDirs: [path.resolve(process.cwd(), "src/svgs")],
}),
项目src文件夹下新建svgs文件夹,随便导入一个svg图标文件,这里我下载了两个图标user.svg和pwd.svg
在components文件夹下创建SvgIcon组件,代码如下
<template>
<svg aria-hidden="true" class="svg-icon" :style="{ width: width + 'px', height: height + 'px', color: color }">
<use :xlink:href="symbolId" />
</svg>
</template>
<script lang="ts">
import { defineComponent, computed } from "vue";
export default defineComponent({
name: "SvgIcon",
props: {
// 使用的svg图标名称,也就是svg文件名
name: {
type: String,
required: true,
},
prefix: {
type: String,
default: "icon",
},
color: {
type: String,
default: "#fff",
},
width: {
type: String,
default: '32'
},
height: {
type: String,
default: '32'
}
},
setup(props) {
const symbolId = computed(() => `#${props.prefix}-${props.name}`);
return { symbolId };
},
});
</script>
<style scope>
.svg-icon {
fill: currentColor;
}
</style>
创建完组建后,我们需要在main.ts中对组件进行全局引入
// 引入Svg组件
import "virtual:svg-icons-register";
import SvgIcon from "./components/SvgIcon.vue";
这样我们就可以在项目中直接使用SvgIcon组件来展示图标了
<svg-icon name="pwd" width="48" height="48" color="#ffff00"></svg-icon>
在 src/views/index.vue
中添加 svg
图片,引入图标规则,svgs文件夹下的直接 name=“文件名” 即可,如果存在文件夹包裹,则遵循规则 name=“文件夹名-文件名”。
<div>
<h1>SVG 图标使用</h1>
<svg-icon name="user" width="64" height="64"></svg-icon>
<svg-icon name="login-pwd" width="48" height="48" color="#ffff00"></svg-icon>
</div>
注意:如果想要让color属性生效(修改图标颜色),首先要svg图标支持fill属性修改才可以。我们可以在编辑器打开svg,然后把fill或者strock的值改成currentColor即可。
至此,SvgIcon组件封装完成!
前面章节我们介绍的都是Vite+Vue3+TypeScript项目中环境相关的配置,接下来我们开始进入系统搭建部分。本篇我们来介绍登录界面搭建及动态路由配置,大家一起撸起来…
项目登陆接口是通过mockjs前端来模拟的
首先在src/mock文件夹下新建login.ts文件,模拟两个服务接口(验证码获取+用户登录)
import { MockMethod } from 'vite-plugin-mock';
export const LoginApi: Array<MockMethod> = [
{
url: '/api/captchaImage',
method: 'get',
response: () => {
return {
msg: 'OK',
img: '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',
code: 200,
uuid: '37e7a189a9b14be6a5cbae80af43abaa',
};
},
},
{
url: '/api/login',
method: 'post',
response: () => {
return {
msg: 'OK',
code: 200,
token:
'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6Ijc2YzYzNzczLWY2ZWEtNDlkMC05MDIyLTg4ZmUxNjI1NmMzNyJ9.XUeTaH-VZu_Mm0Rm1m_lST4YH1-ovX5Gg9w_Z4nA04agzxzeTdb5XxKCIhMr8pPatCKmiCql9E7afMY96oGYfQ',
};
},
},
];
修改src/mock文件夹下的index.ts文件,添加内容如下
import { LoginApi } from './login';
const mock: Array<MockMethod> = [...LoginApi];
在src/api文件夹下新建login.ts文件,创建获取验证码接口captchaImage和用户登录api接口login
import request from '@/utils/request';
export const captchaImage = () => {
return request.get('/api/captchaImage');
};
export const login = () => {
return request.post('/api/login');
};
在src/views路径下新建Login.vue文件,这个文件就是我们的登录界面文件
<template>
<div class="login-main">
<el-form ref="ruleFormRef" :model="ruleForm" status-icon :rules="rules" label-width="120px" class="ruleForm">
<el-form-item prop="user">
<el-input :prefix-icon="User" v-model="ruleForm.user" clearable />
</el-form-item>
<el-form-item prop="pass">
<el-input :prefix-icon="Lock" v-model="ruleForm.pass" type="password" />
</el-form-item>
<el-form-item prop="code">
<el-input :prefix-icon="Lock" v-model="ruleForm.code" class="code-value" />
<img :src="img" alt="" class="code-img">
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm(ruleFormRef)">登录</el-button>
<el-button @click="resetForm(ruleFormRef)">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang='ts'>
// 方法引入
import { reactive, ref, onMounted } from 'vue'
import router from '@/router';
import { setLocalStorage } from "@/utils/localstorage";
import type { FormInstance } from 'element-plus'
// 组件引入
import { User, Lock } from '@element-plus/icons-vue'
// 接口引入
import { captchaImage, login } from "@/api/login";
onMounted(() => {
captchaImage().then(datas => {
console.log(datas)
img.value = 'data:image/gif;base64,' + datas.img
uuid.value = datas.uuid
})
})
let img = ref<any>("")
let uuid = ref<string>("")
const ruleFormRef = ref<FormInstance>()
const validateUser = (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error('用户名不能为空'))
} else {
callback()
}
}
const validatePass = (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error('密码不能为空'))
} else {
callback()
}
}
const validateCode = (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error('验证码不能为空'))
} else {
callback()
}
}
const ruleForm = reactive({
user: 'wangjianlei',
pass: '123456',
code: '4'
})
const rules = reactive({
pass: [{ validator: validatePass, trigger: 'blur' }],
user: [{ validator: validateUser, trigger: 'blur' }],
code: [{ validator: validateCode, trigger: 'blur' }],
})
const submitForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.validate((valid) => {
if (valid) {
login()
.then(res => {
console.log(res)
setLocalStorage("LH_TOKEN", res.token)
router.push("/")
})
.catch(err => {
throw new Error(err);
})
} else {
return false
}
})
}
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
}
</script>
<style lang="scss" scoped>
.login-main {
display: flex;
padding: 25px;
.ruleForm {
width: 500px;
.code-value {
width: 260px;
}
.code-img {
margin-left: 10px;
width: 75px;
height: 30px;
}
}
}
</style>
项目动态路由接口同样是通过mockjs前端来模拟的
首先在src/mock文件夹下新建home.ts文件,模拟动态路由返回服务接口
import { MockMethod } from 'vite-plugin-mock';
export const HomeApi: Array<MockMethod> = [
{
url: '/api/routerList',
method: 'get',
response: () => {
const routes = [
{
path: '/main/PageOne',
name: 'PageOne',
component: 'PageOne.vue',
},
{
path: '/main/PageTwo',
name: 'PageTwo',
component: 'PageTwo.vue',
},
{
path: '/main/PageThree',
name: 'PageThree',
component: 'PageThree.vue',
},
];
return {
msg: 'OK',
code: 200,
data: routes,
};
},
},
];
这里我们可以看到共返回了三个路由 “PageOne”,“PageTwo” 和 “PageThree”。
修改src/mock文件夹下的index.ts文件,添加内容如下
import { MockMethod } from 'vite-plugin-mock';
import { HomeApi } from './home';
import { LoginApi } from './login';
const mock: Array<MockMethod> = [...LoginApi, ...HomeApi];
export default mock;
在src/api文件夹下新建home.ts文件,创建获取动态路由接口GetDynamicRoutes
import request from '@/utils/request';
export const GetDynamicRoutes = () => {
return request.get('/api/routerList');
};
根据我们利用mockjs模拟的动态路由接口返回的数据,在src/views文件夹下创建modules文件夹,新建PageOne.vue,PageTwo.vue和PageThree.vue界面文件。
我们的动态路由数据应由一个公共的地方进行管理,所以呢这里我选择利用vue的状态管理器pinia来实现这个功能。
首先我们在pinia的state中(src/store/modules/homedir.ts 中的state)添加一个路由项routes(RouteRecordRaw类型的数组):
state: (): storeHome => {
return {
//路由表
routes: [],
};
},
然后在action中还需要添加一个根据路由数据加载动态路由的方法(updateRoutes),方法所需的路由数据和router对象由外部传入。(这里用外部传入router是为了避免循环调用router,毕竟需要进行加载动态路由的地方基本都有个router的示例对象,而外部传入的话,只需要调用然后传入一次router就可以了)
在src/store/type/home.ts添加状态项routes类型定义:
import { RouteRecordRaw } from 'vue-router';
export type storeHome = {
routes: Array<RouteRecordRaw>;
};
加载路由的思路其实也很简单,首先解析咱调用接口后传入的路由数据,根据路由的数据类型生成对应的路由表,并存储到pinia中,然后直接遍历这个pinia中的路由表,使用router.addRoute()方法将路由加载进去。router.addRoute()方法支持传如两个参数,方便我们在指定位置的路由中插入children,这种情况下第一个参数是父级路由的name,第二个参数就是要添加的children路由对象。
需要我们注意的是:vite使用动态路由,在动态导入路由组件的时候,需要特别注意不能将页面路径直接作为component导入,虽然开发环境一般是能正常加载,但是打包到生产环境的时候十有八九会报错,所以我们需要添加以下代码:
//根据自己项目实际目录结构组织,注意这里的“../../”不能用“@”别名代替
let modules = import.meta.glob('../../views/modules/*.vue');
然后用modules形式引入,完整动态路由的pinia代码如下:
import { defineStore } from 'pinia';
import { storeHome } from '../types/home';
let modules = import.meta.glob('../../views/modules/*.vue');
export const useHomeStore = defineStore('index', {
state: (): storeHome => {
return {
//路由表
routes: [],
};
},
getters: {},
actions: {
updateRoutes(data: Array<any>, router: any) {
this.routes = [];
data.forEach((el) => {
this.routes.push({
path: el.path,
name: el.name,
component: modules[`../../views/modules/${el.component}`],
});
});
this.routes.forEach((el) => {
router.addRoute('Home', el);
// router.addRoute();
});
},
},
});
上面我们已经配置好了路由接口和加载路由的方法,加载动态路由的思路也很简单,在我们的初始页面中调用路由的数据接口,在获取到数据之后调用加载的方法即可。
// 方法引入
import router from '@/router';
// 接口引入
import { GetDynamicRoutes } from "@/api/home"
// 状态管理器引入
import { useHomeStore } from "@/store/modules/home";
const homeStore = useHomeStore()
onBeforeMount(() => {
if (!getLocalStorage("LH_TOKEN")) {
router.push("/login")
} else {
alert("登陆成功")
GetDynamicRoutes().then(res => {
homeStore.updateRoutes(res.data, router)
})
finish.value = true
}
})
为验证我们的路由是否被加载成功,我们可以在调用动态路由同时创建对应的按钮,以便我们进行路由跳转。
<el-button v-for="item in routes" :key="item.name" @click="handleClick(item.path)">{{ item.name }}</el-button>
const routes = computed(() => homeStore.routes)
// 路由按钮点击事件
const handleClick = (path: string) => {
router.push(path)
}
完整页面代码如下:
<template>
<div class="home-main" v-if="finish">
<div>
<el-button v-for="item in routes" :key="item.name" @click="handleClick(item.path)">{{ item.name }}</el-button>
</div>
<RouterView />
</div>
</template>
<script setup lang='ts'>
// 方法引入
import { reactive, ref, onBeforeMount, onMounted } from 'vue'
import { computed } from "@vue/reactivity";
import { getLocalStorage } from '@/utils/localstorage'
import router from '@/router';
// 组件引入
import HomeHeader from './home/HomeHeader.vue';
// 接口引入
import { GetDynamicRoutes } from "@/api/home"
// 状态管理器引入
import { useHomeStore } from "@/store/modules/home";
const homeStore = useHomeStore()
onBeforeMount(() => {
if (!getLocalStorage("LH_TOKEN")) {
router.push("/login")
} else {
alert("登陆成功")
GetDynamicRoutes().then(res => {
homeStore.updateRoutes(res.data, router)
})
finish.value = true
}
})
const routes = computed(() => homeStore.routes)
// 路由按钮点击事件
const handleClick = (path: string) => {
router.push(path)
}
let finish = ref(false)
</script>
<style lang="scss" scoped>
.home-main {}
</style>
其实截止上面,我们的动态路由已经加载成功了。不过仔细测试会发现,还会有一个问题bug,假如我们刷新跳转后的页面,或者直接使用动态路由的路径进行跳转,就会出现报错“no match found for location with path ‘/PageOne’ ”。添加的动态路由失效了,页面也没有显示,这是因为我们的路由和状态管理器pinia在刷新之后都会被重置,而我们加载路由的方法是在系统初始页面被调用的,当我们直接F5刷新页面或者直接输入路由路径的时候,初始页面其实并没有被加载,也就是说我们的动态路由并没有被加载上去,自然这个动态的页面也就丢失了。
这里我们可以通过添加路由守卫的方式来解决这个问题,大致思路:假如我们的页面请求路径不是我们定义的初始路径的时候,我们就在路由守卫中要求在跳转之前先去查询状态管理器中是否存在我们的动态路由,或者该动态路由是否满足我们的初始页面跳转要求,若不满足则请求动态路由接口并加载我们的动态路由,在加载完成后再继续执行页面跳转操作。
路由守卫代码如下:
// 路由守卫
router.beforeEach((to, from, next) => {
if (to.path !== '/main' && to.path !== '/') {
const store = useHomeStore();
if (store.routes.length < 1) {
GetDynamicRoutes()
.then((res) => {
store.updateRoutes(res.data, router);
next({ path: to.path, replace: true });
})
.catch((_) => {
next();
});
} else {
next();
}
} else {
next();
}
});
至此,登录界面和动态路由基本搭建就完成了。
当你进入一个没有声明/匹配的路由页面时就会跳到404页面,比如访问了 http://localhost:8088/123456 无此页面,就会跳到404页面,如果没有声明一个404页面,那就会跳到一个空白页面。空白页面不友好,咱们来设置一个404页面。
新建页面,404.vue
<template>
<div>
<div>当前网页不存在,请检查网址是否正常</div>
<div>
<router-link to="/">返回首页</router-link>
</div>
</div>
</template>
在Vue3路由上添加下面的路由
{
path: '/404',
name: 'notfount',
component: () => import(/* webpackChunkName: "404" */ '@/views/NotFound.vue')
},
{
// path: "*", // 这样用,vue3已经不支持,得下面的方式
path: "/:pathMath(.*)", // 此处需特别注意置于最底部
redirect: "/404"
}
报错:Cannot find module ‘vue’. Did you mean to set the ‘moduleResolution’ option to ‘node’, or to add aliases to the ‘paths’ option?
解决:
将tsconfig.json里的"moduleResolution": “bundler"改成"moduleResolution”: “node”