本文记录了从零搭建系统的过程及遇到的各种问题,框架使用Vue3.0 + ElementPlus
,使用Vite
搭建项目,Axios
请求数据,MockJs
模拟数据返回,Pinia
状态管理。将完整的搭建思路及过程中遇到的问题记录下来,将简易版本呈现出来,后续仍需打磨。
1.使用vite创建项目
兼容性注意,Vite 需要 Node.js 版本 >= 12.0.0。 使用 NPM:
npm init @vitejs/app
使用 Yarn:
yarn create @vitejs/app
然后按照提示操作即可!
按照红框的命令运行完成之后,出现熟悉的画面
打开package.json
,可以看到typescript
依赖已经安装好了,并且生成了env.d.ts
文件,为.vue
文件生成TypeScript
声明文件,无需再添加多余的对于ts的支持。
2.按需安装依赖
以下为我安装的依赖:
yarn add sass --dev
yarn add element-plus
yarn add vue-router@4
yarn add axios
在main.ts
中引入:
import { createApp } from "vue";
import router from "./router";//引入路由,需新建src/router/index.js,用来写入路由配置
import ElementPlus from "element-plus";//引入element-plus
import "element-plus/dist/index.css";//引入element-plus样式
import App from "./App.vue";
createApp(App).use(ElementPlus).use(router).mount("#app");
3.vite配置别名
修改vite
配置文件vite.config.ts
:为了方便我们用@/方式找到资源,添加别名配置。如果引入resolve
,显示找不到path
,需要先执行yarn add @types/node -D
引入对node
的依赖。
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { resolve } from "path";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
//引入resole,alias设置别名
//如果报错 请 yarn add @types/node -D
resolve: {
alias: {
"@": resolve(__dirname, "./src"),
},
},
});
4.添加全局sass颜色变量,配置预处理器指向
创建var.scss
,目录为src\assets\scss\var.scss
,内容如下,
$primary-color: #2f69eb;
直接在.vue
文件中使用会报错的,
[plugin:vite:css] Undefined variable.
╷
3 │ background-color: $primary-color;
│ ^^^^^^^^^^^^^^
╵
src\components\Layout.vue 3:21 root stylesheet
解决方法:
vite.config.ts
中添加css预处理器的指向,重新启动即可
import { defineConfig } from "vite";
import vue from '@vitejs/plugin-vue';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "./src/assets/scss/var.scss";`,
},
},
},
});
5.elementPlus国际化默认设置为中文
修改App.vue
:App.vue
是项目的主组件,页面入口文件。附ElementPlus官网文档地址>>
,我使用的是ConfigProvider组件方式。
vue3.0组合式Api有以下两种常见写法,官网也有两种不同示例:
方式一:单文件组件export default
默认导出:
<template>
<el-config-provider :locale="locale">
<router-view />
</el-config-provider>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { ElConfigProvider } from "element-plus";
import zhCn from "element-plus/es/locale/lang/zh-cn";
export default defineComponent({
components: {
ElConfigProvider,
},
setup() {
return {
locale: zhCn,
};
},
});
</script>
方式二:单文件组件<script setup>
:
<template>
<el-config-provider :locale="locale">
<router-view />
</el-config-provider>
</template>
<script setup lang="ts">
import { ElConfigProvider } from "element-plus";
import zhCn from "element-plus/es/locale/lang/zh-cn";
const locale = zhCn;
</script>
<style lang="scss" scoped>
html,
body {
margin: 0;
padding: 0;
}
</style>
6.页面布局
页面布局,常规的三部分,顶部导航栏+左侧菜单栏+主页面区域,将这三块放入layout.vue中。TopNav.vue
导航栏组件,SideMenu.vue
左侧菜单组件,MainContent.vue
主页面组件。
<template>
<el-container>
<el-header class="topContent"> <top-nav /></el-header>
<el-container>
<el-aside :class="store.isCollapse ? 'sideWidth' : ''"
><side-menu
/></el-aside>
<el-main><main-content /></el-main>
</el-container>
</el-container>
</template>
<script lang='ts'>
import { defineComponent, ref } from "vue";
import MainContent from "./MainContent.vue";
import SideMenu from "./SideMenu.vue";
import TopNav from "./TopNav.vue";
import { appStore } from "../store/appStore";
interface State {
collapseWidth: string;
}
export default defineComponent({
components: { SideMenu, TopNav, MainContent },
setup() {
const collapseWidth = ref<State>({
collapseWidth: "240px",
});
const store = appStore();
return {
collapseWidth,
store,
};
},
});
</script>
<style lang='scss' scoped>
.topContent {
background-color: $primary-color;
color: $white;
margin: 0;
display: flex;
align-items: center;
}
.sideWidth {
width: 80px;
overflow-x: hidden;
}
</style>
7.左侧菜单栏
实际工作过程中,路由信息可能通过后台读取,那么就需要考虑将静态路径映射到组件上。也有有的路由是不需要出现在左侧菜单的,那么可以在meta中添加标识,在菜单组件中去过滤掉无需渲染至左侧菜单的路由。
以下为简单的实现,思路就是拿着配置的路由信息routers/index.ts
,遍历渲染到el-menu
中,但要考虑,路由信息可能是嵌套多层,那么就需要el-sub-menu
递归实现,以下为简陋版。调用逻辑关系图所示:
1. 添加路由信息,创建路由实例
路由文件router/index.ts
添加路由信息,并通过createRouter
创建一个可以被Vue应用程序使用的路由实例。
import { createRouter, RouteRecordRaw, createWebHashHistory } from "vue-router";
export const routes: Array<RouteRecordRaw> = [
{
path: "/",
component: () => import("../components/Layout.vue"),
meta: {
icon: "carbon:user-role",
title: "第一級",
},
children: [
{
path: "/level",
name: "Level",
component: () => import("../components/MainContent.vue"),
redirect: "/level/menu1/menu1-1",
meta: {
icon: "carbon:user-role",
title: "第一級-1",
},
children: [
{
path: "menu1",
name: "Menu1Demo",
meta: {
title: "第一級-1-1",
},
component: () => import("../components/MainContent.vue"),
},
],
},
{
path: "/level2",
name: "Level2",
component: () => import("../components/MainContent.vue"),
meta: {
icon: "carbon:user-role",
title: "第一級-2-1",
},
children: [
{
path: "menu12",
name: "Menu1Demo",
meta: {
title: "Menu12",
},
component: () => import("../components/MainContent.vue"),
},
],
},
],
},
{
path: "/level",
name: "Level",
component: () => import("../components/MainContent.vue"),
meta: {
icon: "carbon:user-role",
title: "第二級",
},
},
];
export default createRouter({
history: createWebHashHistory(),//创建一个hash历史记录。
routes,
});
2. BaseSubMenu.vue
组件
<template>
<template v-for="routerItem in routes" :key="routerItem.path">
<el-sub-menu
v-if="routerItem.children"
:key="routerItem.path"
:index="routerItem.path"
>
<template #title>
<el-icon><location /></el-icon>
<span>{{ routerItem.meta.title }}</span>
</template>
<base-sub-menu :routes="routerItem.children" v-if="routerItem.children" />
<el-menu-item-group v-else :title="routerItem.meta.title">
<base-menu-item :routerItem="routerItem" />
</el-menu-item-group>
</el-sub-menu>
<base-menu-item v-else :routerItem="routerItem" />
</template>
</template>
<script lang='ts'>
import { defineComponent } from "vue";
import BaseMenuItem from "./BaseMenuItem.vue";
import { Location } from "@element-plus/icons";
export default defineComponent({
components: {
BaseMenuItem,
Location,
},
props: {
routes: {
type: Array,
require: true,
},
},
});
</script>
3. BaseMenuItem.vue
组件
<template>
<el-menu-item :index="routerItem.path">
<el-icon><setting /></el-icon>
<span>{{ routerItem.meta.title }}</span>
</el-menu-item>
</template>
<script lang='ts'>
import { defineComponent } from "vue";
import { Setting } from "@element-plus/icons";
export default defineComponent({
components: {
Setting,
},
props: {
routerItem: {
type: Object,
require: true,
},
},
});
</script>
4. 展示运行效果:
8.Pinia实践-控制左侧菜单展开折叠
Pinia是在2019年11月左右开始尝试用Composition API重新设计Vue Store的外观。从那时起,最初的原则仍然相同,但Pinia适用于Vue 2和Vue 3,并且不要求您使用组合API。除了安装和SSR之外,这两个API都是一样的,这些文档是针对Vue 3的,在必要时附带关于Vue 2的注释,以便Vue 2和Vue 3用户可以阅读! 为什么选用Pinia?有人说VueX4对TypeScript并不友好,但我仅仅是为了实践,选择VueX4还是Pinia完全可以根据个人喜好,我个人实践认为Pinia确实很轻便。
1. 安装Pinia:
yarn add pinia
# or with npm
npm install pinia
2. main.ts中引入pinia,并创建应用程序使用的Pinia实例
import { createPinia } from "pinia";
const pinia = createPinia();
3. 左侧菜单展开折叠的实现,需要跨组件的获取到状态
pinia
中 defineStore
调用后返回一个函数,调用该函数获得 Store
实体,参数id
是必须且唯一的。
store/appStore.ts
:
import { defineStore } from "pinia";
export const appStore = defineStore("appStoreId", {
state: () => {
return {
isCollapse: false,
};
},
});
4. 切换展开折叠状态
pinia
获取store
中的数据,无需像VueX
中通过.getters
,而是直接在暴露出来的数据中直接通过.
的方式。
<template>
<div @click="toggleCollapse">切换</div>
</template>
<script lang='ts'>
import { defineComponent } from "vue";
import { appStore } from "../store/appStore";
export default defineComponent({
setup() {
const store = appStore();
const toggleCollapse = () => {
store.isCollapse = !store.isCollapse;
};
return {
toggleCollapse,
};
},
});
</script>
5. 最终效果
9.安装Mock依赖
为了方便的模拟真实的业务场景,引入Mockjs来替代后台接口。
1. 安装MockJs
yarn add mockjs
2.Mock.mock()拦截数据请求
创建mock/index.ts
,内容如下
//引入mockjs
import Mock from "mockjs";
//使用mockjs模拟数据
Mock.mock("/getTableList", {
ret: 0,
data: {
time: "@datetime", //随机生成日期时间
"score|1-800": 800, //随机生成1-800的数字
"rank|1-100": 100, //随机生成1-100的数字
"stars|1-5": 5, //随机生成1-5的数字
nickname: "@cname", //随机生成中文名字
},
});
3. main.js引入文档
import "@/mock/index";
4. 测试mockjs返回
import { defineComponent } from "vue";
import axios from "axios"
export default defineComponent({
setup() {
axios.post('/getTableList').then((res:any)=>{
console.log(res)
}).catch((err:any)=>{
console.log(err);
})
},
});
控制台打印输出:
5. 报错解决
在第二步 mock/index.ts
中 import Mock from 'mockjs'
时若出现无法找到模块“mockjs”
的声明文件报错,可以点击快速修改或者运行yarn add @types/mockjs -D
。
@types/mockjs
是 mockjs
的 TypeScript
定义。
无法找到模块“mockjs”的声明文件。“/node_modules/mockjs/dist/mock.js”隐式拥有 "any" 类型。
10.请求封装
1 .封装公共的请求
封装公共的请求在我们做项目中非常的必要:处理公共请求头信息,拦截错误状态,统一处理token校验,加载遮罩等,也可以根据项目实际需要添加白名单、用户信息失效、加解密等。
import axios, {
AxiosRequestConfig,
AxiosRequestHeaders,
AxiosResponse,
} from "axios";
import { ElMessage, ElLoading, ILoadingInstance } from "element-plus";
const state = {
ok: "200",//请求成功状态码
};
//返回数据规则
interface IResponseData<T> {
status: number;
message?: string;
data: T;
code: string;
}
//请求默认配置规则
type TOption = {
baseURL: string;
timeout: number;
};
//默认配置
const config = {
baseURL: "/api",
timeout: 30 * 1000,
withCredentials: true,
};
//默认加载样式
let loading: ILoadingInstance = ElLoading.service({
lock: true,
text: "Loading",
spinner: "el-icon-loading",
});
class Http {
service: any;
constructor(config: TOption) {
//实例化请求配置
this.service = axios.create(config);
//请求拦截
this.service.interceptors.request.use(
(config: AxiosRequestConfig) => {
let stateToken = localStorage.getItem("token") as string;
if (stateToken) {
(config.headers as AxiosRequestHeaders).authorization = stateToken;
}
return config;
},
(error: any) => {
loading.close();
return Promise.reject(error);
}
);
//响应拦截
this.service.interceptors.response.use(
(response: AxiosResponse) => {
loading.close();
const data = response.data;
const { code } = data;
if (!code) {
//如果没有返回状态码,直接返回数据,针对于返回数据为blob类型
return response;
} else if (code !== state["ok"]) {
ElMessage.error(data.message && "请求异常");
return Promise.reject(data);
}
return response.data;
},
(error: any) => {
loading.close();
ElMessage.error("请求失败");
return Promise.reject(error);
}
);
}
get<T>(url: string, params?: object, data = {}): Promise<IResponseData<T>> {
return this.service.get(url, { params, ...data });
}
post<T>(url: string, params?: object, data = {}): Promise<IResponseData<T>> {
return this.service.post(url, params, data);
}
put<T>(url: string, params?: object, data = {}): Promise<IResponseData<T>> {
return this.service.put(url, params, data);
}
delete<T>(
url: string,
params?: object,
data = {}
): Promise<IResponseData<T>> {
return this.service.delete(url, { params, ...data });
}
}
export default new Http(config);
2. 添加请求
此时,我们可以将上面9-4中测试mockjs
返回数据的方法改为如下,(当然,直接开发时,我们通常将请求统一放到src/api/*
文件目录下,暴露出数据接口供业务页面引入使用)。
<script lang='ts'>
import { defineComponent } from "vue";
import http from "@/utils/http.ts";
interface TableType {
time: string;
score: string;
rank: number;
stars: number;
nickname: string;
}
export default defineComponent({
setup() {
http
.post<TableType>(
"/getTableList",
{/*请求的参数*/},
{/*添加默认请求配置如header,baseUrl等*/}
)
.then((res: any) => {
console.log(res);
})
.catch((err: any) => {
console.log(err);
});
},
});
</script>
11.配置server.proxy代理服务
假设我们有两种前缀开头的接口,分别部署在不同的服务上,那么我们就可以通过server.proxy
添加代理。一般的,我们为了方便的区分接口会在接口前添加特殊标识前缀,但后台真实接口未必有加,那么我们就可以在axios
中配置baseUrl,指定前缀。
修改`vite`配置文件`vite.config.ts`:
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { resolve } from "path";
export default defineConfig({
plugins: [vue()],
...
server: {
proxy: {
"/api": {
target: "http://134.221.140.220:9001",
changeOrigin: true,
},
"/auth": {
target: "http://134.221.140.192:9005/",
changeOrigin: true,
},
},
},
});
12.其他报错信息
1:windows平台缺少编译环境,删除node_modules,重新安装依赖
failed to load config from E:\codespace\vite-test\vite.config.ts
error when starting dev server:
Error: The package "esbuild-windows-64" could not be found, and is needed by esbuild.
If you are installing esbuild with npm, make sure that you don't specify the
by esbuild to install the correct binary executable for your current platform.
at generateBinPath
...
本意是搭建一个后台管理系统,以上为内容的第一部分