从零搭建vue3.0项目框架与基本封装
一、先卸载旧版本
如果没有旧版本可直接跳过此步骤
npm uninstall vue-cli -g
二、安装Vue CLI 3
npm install -g @vue/cli
三、创建项目
进入要创建项目的目录
vue create name
执行上述命令后,出现如下界面,选择配置,这里不选默认,选择自己配置
根据自己的需求,选择相应的配置,这里按"a",全选,回车。
这里选择3x,回车。
这里问你是否使用class风格的组件语法,输入y,回车。
这里问你是否使用babel做转义,输入y,回车。
这里是问你路由是否使用历史模式? 这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面,这里根据需要选择,我选了y。
这里是问你使用什么css预编译器? 我选择的 node-scss。
这里是问你选择哪种代码检测:
tslint: typescript格式验证工具
eslint w…: 只进行报错提醒; 不考虑项目【选这个】
eslint + A…: 不严谨模式;
eslint + S…: 正常模式;
eslint + P…: 严格模式;严谨一点【选这个】
这里是问你监测时机,这里选第一个,保存就监测,不要等提交在监测,那样问题会很多。
单元测试工具,这里随便了。
vue-cli 一般来讲是将所有的依赖目录放在package.json文件里
这里是问你是否在以后的项目中使用以上配置?这里我选择N。
ps:如何安装过程中有报错请百度自行解决,大多数是你的配置环境问题。
可能出现的node-sass安装问题解决办法
1、安装python2.7
1.1 打开https://www.python.org/download/releases/2.7/ 找到 Windows X86-64 MSI Installer (2.7.0) [1] (sig) 后下载后安装
1.2 配置环境变量, 把C:\Python27追加到变量名:path后
2、配置 win32-x64-67_binding.node
2.1 https://github.com/sass/node-sass/releases/download/v4.11.0/win32-x64-67_binding.node 下载后放到某个目录下(比如:D:\software\win32-x64-67_binding.node)
2.2 配置环境变量 变量名:SASS_BINARY_PATH 变量值:D:\software\win32-x64-67_binding.node(文件全路径)
2.3 执行npm rebuild node-sass 重构一下
3、进入vue工程 执行 npm install
4、安装成功之后 vue工程执行下执行 npm run serve。
看到如下界面说明启动成功。
四、UI框架安装
这里本人选择的 element-ui。
ps: element-ui 对vue3.0d的安装方式与2.0有所不同。
安装命令
npm install element-plus --save
代码配置
import { createApp } from 'vue'
import App from './App.vue'
import './registerServiceWorker'
import router from './router'
import store from './store'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import locale from 'element-plus/lib/locale/lang/zh-cn'
createApp(App)
.use(store)
.use(ElementPlus, { locale })
.use(router)
.mount('#app')
五、封装axios
安装命令
npm install axios
封装
在src目录下创建request目录,创建interceptors.js与request.js。
interceptors.js该文件主要用来对请求与响应的拦截。
/**axios封装
* 请求拦截、相应拦截、错误统一处理
*/
import Axios from "axios";
import router from "@/router";
import { ElMessage } from "element-plus";
const axios = Axios.create({
baseURL: process.env.VUE_APP_URL, // url = base url + request url
timeout: 5000, // request timeout
});
// post请求头
axios.defaults.headers.post["Content-Type"] = "application/json;charset=UTF-8";
// request interceptor
axios.interceptors.request.use(
(config) => {
// do something before request is sent
if (sessionStorage.getItem("token")) {
config.headers["Authorization"] =
"bearer " + sessionStorage.getItem("token"); // 让每个请求携带自定义 token 请根据实际情况自行修改
}
return config;
},
(error) => {
// do something with request error
console.log(error); // for debug
return Promise.reject(error);
}
);
// http response 拦截器
axios.interceptors.response.use(
(response) => {
const data = response.data;
// 根据返回的code值来做不同的处理(和后端约定)
switch (data.code) {
case 401:
// 未登录 清除已登录状态
if (router.history.current.name !== "login") {
if (data.msg !== null) {
ElMessage({
message: data.msg,
type: "error",
duration: 5 * 1000,
});
} else {
ElMessage({
message: "未知错误,请重新登录",
type: "error",
duration: 5 * 1000,
});
}
router.push("/login");
}
break;
case 403:
// 没有权限
if (data.msg !== null) {
ElMessage({
message: data.msg,
type: "error",
duration: 5 * 1000,
});
} else {
ElMessage({
message: "未知错误,请重新登录",
type: "error",
duration: 5 * 1000,
});
}
break;
case 500:
// 错误
if (data.msg !== null) {
ElMessage({
message: data.msg,
type: "error",
duration: 5 * 1000,
});
} else {
ElMessage({
message: "未知错误,请重新登录",
type: "error",
duration: 5 * 1000,
});
}
break;
default:
return data;
}
return data;
},
(err) => {
// 返回状态码不为200时候的错误处理
ElMessage({
message: err.toString(),
type: "error",
duration: 5 * 1000,
});
return Promise.resolve(err);
}
);
export default axios;
request.js该文件主要是对get,post,put,delete方法的封装。
import instance from './interceptors'
import { ElMessage } from 'element-plus'
export default class baseRequest {
private url: any
private params: any
constructor (url: any, params: any) {
this.url = url
this.params = typeof params === 'undefined' ? {} : params
}
get () {
return instance
.get(this.url, { params: this.params })
.then((res: any) => {
if (res.code === 200) {
return Promise.resolve(res.data)
} else {
ElMessage({
message: res.message,
type: 'error',
duration: 5 * 1000
})
return Promise.resolve(false)
}
})
.catch((e) => {
ElMessage({
message: e,
type: 'error',
duration: 5 * 1000
})
Promise.resolve(false)
})
}
post () {
return instance
.post(this.url, this.params)
.then((res: any) => {
if (res.code === 200) {
return Promise.resolve(res.data)
} else {
ElMessage({
message: res.message,
type: 'error',
duration: 5 * 1000
})
Promise.resolve(false)
}
})
.catch((e) => {
ElMessage({
message: e,
type: 'error',
duration: 5 * 1000
})
Promise.resolve(false)
})
}
put () {
return instance
.put(this.url, this.params)
.then((res: any) => {
if (res.code === 200) {
return Promise.resolve(res.data)
} else {
ElMessage({
message: res.message,
type: 'error',
duration: 5 * 1000
})
Promise.resolve(false)
}
})
.catch((e) => {
ElMessage({
message: e,
type: 'error',
duration: 5 * 1000
})
Promise.resolve(false)
})
}
delete () {
return instance
.delete(this.url, { params: this.params })
.then((res: any) => {
if (res.code === 200) {
return Promise.resolve(res.data)
} else {
ElMessage({
message: res.message,
type: 'error',
duration: 5 * 1000
})
Promise.resolve(false)
}
})
.catch((e) => {
ElMessage({
message: e,
type: 'error',
duration: 5 * 1000
})
Promise.resolve(false)
})
}
upfile () {
return instance
.post(this.url, this.params, {
headers: {
'Content-Type': 'multipart/form-data',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then((res: any) => {
if (res.code === 200) {
return Promise.resolve(res.data)
} else {
ElMessage({
message: res.message,
type: 'error',
duration: 5 * 1000
})
Promise.resolve(false)
}
})
.catch((e) => {
ElMessage({
message: e,
type: 'error',
duration: 5 * 1000
})
Promise.resolve(false)
})
}
downfile () {
return instance
.post(this.url, this.params, { responseType: 'blob' })
.then((res: any) => {
const fileReader = new FileReader()
fileReader.onload = function (e: any) {
try {
const jsonData = JSON.parse(e.target.result) // 说明是普通对象数据,后台转换失败
if (jsonData.code) {
ElMessage({
message: jsonData.message,
type: 'error',
duration: 5 * 1000
})
Promise.resolve(false)
}
} catch (err) { // 解析成对象失败,说明是正常的文件流
const url = window.URL.createObjectURL(res)
const eleLink = document.createElement('a')
eleLink.href = url
eleLink.download = 'application.yml'
document.body.appendChild(eleLink)
eleLink.click()
window.URL.revokeObjectURL(url)
}
}
fileReader.readAsText(res)
})
.catch((e) => {
ElMessage({
message: e,
type: 'error',
duration: 5 * 1000
})
Promise.resolve(false)
})
}
}
如何使用?
在src目录下创建api文件夹,用来做项目的模块化管理。
这里创建了user.js,定义userApi 导出后,即可在vue.js文件中使用。
import baseRequest from "@/request/request.js";
// import qs from "qs";
const userApi = {
/**
* 获取数据来源-无参数
*/
source() {
let url = "/tmc/import/sourcea";
let baseRequestFun = new baseRequest(url);
return baseRequestFun.get();
},
/**
* 获取数据来源-有参数
*/
source2(params) {
let url = "/tmc/import/sourcea";
let baseRequestFun = new baseRequest(url,params);
return baseRequestFun.get();
},
};
export default userApi;
六、路由封装
路由封装包括登录权限请求等一系列操作;需要用到vuex;
如何配置vueX,请参考这篇文章
为了美观,我们使用NProgress在页面跳转时候在浏览器顶部添加进度条。
安装命令
npm install --save nprogress
使用nprogress
在src目录下创建permission.js文件,permission主要负责全局路由守卫和登录判断。
简单使用nprogress,在permission.js文件中引用NProgress 与router 。
import NProgress from "nprogress";
import "nprogress/nprogress.css";
import router from "@/router";
router.beforeEach((to, from, next) => {
NProgress.start();
next();
});
router.afterEach(() => {
NProgress.done();
});
在main.js中引入permission。
import { createApp } from "vue";
import App from "./App.vue";
import "./registerServiceWorker";
import router from "./router";
import store from "./store";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import "./permission"; // permission control
createApp(App).use(store).use(ElementPlus).use(router).mount("#app");
这里补充一个知识点:
一个js文件代表一个js模块 ,ES6引入外部模块分两种情况:
1.导入外部的变量或函数等;
import {firstName, lastName} from './test';
2.导入外部的模块,并立即执行
//执行test.js但不导入任何变量
import './test'
接下来,配置全局路由守卫和登录的逻辑判断。
我们修改一下permission.js中的内容
import NProgress from "nprogress"; // progress bar
import "nprogress/nprogress.css"; // progress bar style
import router, { resetRouter, constantRoutes } from "@/router";
import Home from "@/views/Layout";
import { loginApi } from "@/api/login/login";
import store from "./store";
// NProgress 的一些配置
NProgress.configure({ showSpinner: false });
//不需要权限验证的路径
const whiteList = ["/login", "/auth-redirect"];
router.beforeEach(async (to, from, next) => {
// 开始NProgress
NProgress.start();
// 获取token
const hasToken = sessionStorage.getItem("token");
if (hasToken) {
if (to.path === "/" || to.path === "/login") {
// 如果已经登录那么重定向到首页
next({ path: "/index" });
//NProgress结束
NProgress.done();
} else {
// 获取角色,这里角色是存在vuex中的
const hasRoles = store.getters.roleId;
if (hasRoles) {
next();
} else {
try {
//拿到用户名,通过用户名去查询用户角色
let name = sessionStorage.getItem("name");
//查询用户角色
const roleId = await loginApi.getRoleId(name);
// 将角色保存到vuex中
store.dispatch("setROLEID", roleId);
//根据角色获取对应的路由信息
const accessRoutes = await loginApi.getRouter(roleId);
//添加路由,这里是自己定义的添加路由的方法,可以根据自己公司定义的前后台传参方式进行自定义编写
await controlRouter(accessRoutes);
//hack方法以确保addRoute是完整的
//设置replace:true,这样导航将不会留下历史记录
next({ ...to, replace: true });
} catch (error) {
//删除令牌并转到登录页面重新登录
next(`/login?redirect=${to.path}`);
NProgress.done();
}
}
}
} else {
/* 没有token 也可以登录的页面*/
if (whiteList.indexOf(to.path) !== -1) {
// 在免登录白名单中,直接进入
next();
} else {
//没有访问权限的其他页面将重定向到登录页面
next(`/login?redirect=${to.path}`);
NProgress.done();
}
}
});
router.afterEach(() => {
//完成进度条
NProgress.done();
});
//路由器数据拼接,addRoutes方法在vue3中已经废弃了,所以这里使用的addRoute
export function controlRouter(accessRoutes) {
for (let i = 0; i < accessRoutes.length; i++) {
console.log(accessRoutes[i]);
let sumMenu = {
path: accessRoutes[i].MENU_URL,
component: Home,
name: accessRoutes[i].MENU_NAME,
iconCls: accessRoutes[i].MENU_ICON,
redirect: accessRoutes[i].MENU_REDIRECT,
};
if (accessRoutes[i].children) {
sumMenu["children"] = [];
for (let j = 0; j < accessRoutes[i].children.length; j++) {
//是否是iframe页面
if (accessRoutes[i].children[j].MENU_REPORT === "true") {
const MENU_REPORT_PATH = accessRoutes[i].children[j].MENU_REPORT_PATH;
sumMenu["children"][j] = {
props: () => {
return {
path: MENU_REPORT_PATH,
};
},
path: `${accessRoutes[i].children[j].MENU_PATH}`,
iconCls: accessRoutes[i].children[j].MENU_ICON,
component: (resolve) =>
require(["./../public/static/report/test"], resolve),
name: accessRoutes[i].children[j].MENU_NAME,
};
} else {
sumMenu["children"][j] = {
path: accessRoutes[i].children[j].MENU_PATH,
iconCls: accessRoutes[i].children[j].MENU_ICON,
component: () =>
import(`@/views/${accessRoutes[i].children[j].MENU_URL}`),
name: accessRoutes[i].children[j].MENU_NAME,
};
}
}
}
//this.$router不是响应式的,所以手动将路由元注入路由对象
resetRouter();
router.options.routes = constantRoutes;
router.options.routes.push(sumMenu);
router.addRoute(sumMenu); //添加到路由
}
}
这里贴出router.js文件
import { createRouter, createWebHistory } from "vue-router";
import Layout from "@/views/Layout";
// 该 js 实现了逻辑的第一步,配置两个路由
// 公共的路由
export const constantRoutes = [
{
path: "/login",
component: () => import("@/views/login/index"),
hidden: true, // 因为无需在侧边栏显示,可以用这个字段来控制隐藏
},
{
path: "/index",
component: Layout, // 这是一个框架组件,顶部和侧边栏会固定加载
redirect: "/homePage", // 重定向的就是中间的内容部分
children: [
{
path: "/homePage",
component: () => import("@/views/homePage"),
name: "homePage",
meta: { title: "homePage", icon: "homePage" },
},
],
},
];
// 先把公共路由添加进路由实例,动态的路由手动添加
const router = createRouter({
history: createWebHistory(),
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes,
});
export function resetRouter() {
const newRouter = createRouter({
history: createWebHistory(),
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes,
});
router.resolve = newRouter.resolve; // the relevant part
}
export default router;
七、页面布局封装
页面布局主要分成Header、Aside、Main、Bottom四部分,根据项目需要可以去掉Bottom部分。
这里重点讲一下Aside侧边栏封装动态菜单。
<template>
<div>
<el-menu
:default-active="$route.path"
class="el-menu-vertical-demo"
router
collapse-transition
show-timeout
hide-timeout
background-color="#2189d5"
text-color="#fff"
active-text-color="#ffd04b"
:collapse="isCollapse"
>
<template v-for="(item, index) in $router.options.routes">
<template v-if="!item.hidden">
<el-sub-menu :index="index+''" :key="index">
<template #title
><i :class="item.iconCls" style="color: white"></i>
<span>{{ item.name }}</span>
</template>
<template v-for="child in item.children">
<el-menu-item
:index="child.path"
:key="child.path"
v-if="!child.hidden"
class="el-menu-item-icon"
>
<i :class="child.iconCls" style="color: white"></i>
<span>{{ child.name }}</span>
</el-menu-item>
</template>
</el-sub-menu>
</template>
</template>
</el-menu>
</div>
</template>
<script>
export default {
name: "Aside",
data() {
return {
isCollapse: false,
};
},
};
</script>
<style>
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
min-height: 400px;
}
</style>
上述代码中只封装了二级菜单,如果想无限级封装可以使用递归函数来实现。
最后这里贴出路由请求的出参结构
就是这行代码的出参
const accessRoutes = await loginApi.getRouter(roleId);
[{
MENU_ICON: "el-icon-date",
MENU_ID: 45,
MENU_NAME: "今日动态",
MENU_ORDER: "1",
MENU_REDIRECT: "/query",
MENU_REPORT: "false",
MENU_TYPE: "2",
MENU_URL: "todayNews",
PARENT_ID: "0",
children:[{
MENU_ICON: "el-icon-date"
MENU_ID: 140
MENU_NAME: "门诊量(医生)"
MENU_ORDER: "1"
MENU_PATH: "/outpatientDoc"
MENU_REPORT: "false"
MENU_TYPE: "2"
MENU_URL: "todayNews/OutpatientDoc"
PARENT_ID: "45"
},
{MENU_ICON: "el-icon-document",
MENU_ID: 129,
MENU_NAME: "门诊量(科室)(皕杰)",
MENU_ORDER: "1",
MENU_PATH: "/bijie",
MENU_REPORT: "true",
MENU_REPORT_PATH: "http://192.168.17.152:8089/report/ReportEmitter?rpt=NewItem/NewItemNewItem2/AKS_econ_mzsjhz.brt",
MENU_TYPE: "1",
MENU_URL: "bijie.vue",
PARENT_ID: "45"}]
}]
八、vue.config配置
vue-cli 3.0 创建的项目,已经干掉了原有的webpack配置,取而代之的是,vue.config.js
module.exports = {
// 输出文件目录
outputDir: "dist", // 默认dist
// 用于嵌套生成的静态资产(js,css,img,fonts)目录
assetsDir: 'assets',
// 指定生成的 index.html 的输出路径 (相对于 outputDir)。也可以是一个绝对路径
indexPath: "index.html", // Default: 'index.html'
filenameHashing: true,
// 构建多页时使用
pages: undefined,
// eslint-loader是否在保存的时候检查
lintOnSave: true,
// 是否使用包含运行时编译器的Vue核心的构建
runtimeCompiler: false,
// 默认情况下 babel-loader 会忽略所有 node_modules 中的文件。如果你想要通过 Babel 显式转译一个依赖,可以在这个选项中列出来
transpileDependencies: [],
// 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。
productionSourceMap: false,
// css相关配置
css: {
},
//开发模式反向代理配置,生产模式请使用Nginx部署并配置反向代理
devServer: {
port: 8085,
proxy: {
"/api": {
//本地服务接口地址
//target: "http://192.168.3.169:8080",
target: "http://localhost:8083/admin",
//远程演示服务地址,可用于直接启动项目
ws: true,
pathRewrite: {
"^/api": '',
},
},
},
},
// PWA 插件相关配置
pwa: {},
// 第三方插件配置
pluginOptions: {
// ...
},
};
九、总结
到这里项目的基本就结束了,作为一个小白初次搭建项目肯定会有不足,后续会对其进行不断的完善,最后附上项目源码