通过全程参与,可以加深对VUE项目的理解。
近期做的一个项目,前端除了UI外,没有使用什么框架。不使用现成的框架是无奈之举,因为找不到合适的。之前用的框架,比较老旧,还是vue2的;新的吧,有学习成本,怕耽误时间,也不知道效果怎么样,存在一定的风险。利用最基本的“空白”项目,按需添加基础功能,代码可控,进度也较有保证,同时还能够消除无框架不会工作恐惧症。现在记录一下心得,以后可以反复使用。
记录重点有:
0、整体结构
1、路由
2、导航条及子菜单
3、ajax请求封装
4、vue.config.js及系统配置
5、登录及退出
6、身份认证与访问控制
一、整体结构
1、创建项目
首先是新创建一个vue3项目。方法是
vue create 项目名称
- 1
然后选择合适的选项。具体可参考拙作
vue3多个项目共享开发和单个项目独立打包的解决方案
1)按默认方案创建
项目结构:
2)创建时增加路由及store支持
多了router、store以及一些页面。
3)我们项目的整体结构
实际项目中,当然还会夹带一些私货,额外增加一些东东。
2、项目入口main.js
注意项目的入口不是App.vue,而是main.js。
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
createApp(App).use(store).use(router).mount('#app')
- 1
- 2
- 3
- 4
- 5
- 6
main.js是系统约定好的名称,正如一般程序的入口是函数void main()一样。整个项目的入口是main.js,然后每个模块的入口可以是index.js。都是js。本质上,vue是一个大的js语法糖。它有这样那样的结构,让人只把杭州作汴州,但归根到底,它最终是要编译成原始的js,才能被浏览器识别、运行。
从上述代码可以知道,main.js的作用是加载App.vue,引入store、路由,然后绑定到页面id="app"的div。这样该div就是整个项目的活动区域,即展示页面内容的区域了:
vue项目是单页面应用,只有一张html页面。我们看到的所有内容,都展示在id="app"的这个div上!完全由js控制。
3、我们项目里的main.js
main.js这里的引用,都是全局性的。除了路由,store,还可以引入ui框架,css,全局性组件等等。比如我们项目里的main.js是这样写:
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import PerfectScrollbar from "vue3-perfect-scrollbar";//滚动条美化
import "vue3-perfect-scrollbar/dist/vue3-perfect-scrollbar.css";
import Antd from "ant-design-vue";//ant design vue,UI框架
import "ant-design-vue/dist/antd.css";
import "@/assets/css/default.css"; //自定义的全局css
createApp(App).use(Antd).use(PerfectScrollbar).use(router).mount("#app");
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
二、路由
简单来说,路由就是菜单配置,将菜单项的id,路径,都集中写在了一个配置文件里。
1、系统自动生成的路由
import { createRouter, createWebHashHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
以上完全是系统自动生成的代码。看上去也不难理解。其中路由表routes对应的是导航条
2、实际项目中的路由
见本文第六章第一条
3、代码中使用路由
如果想多加几个导航条,可以依葫芦画瓢,很容易就能实现。每个路由,name是唯一的ID,在代码里控制跳转的话,引用name就可以,可以不再重复写这个path。比如在某个vue里,可以这样使用路由:
import { defineComponent } from "vue";
import { useRouter } from "vue-router"; //引入useRouter
export default defineComponent({
setup() {
const router = useRouter();
const browseIt = (fd) => {
const to = router.resolve({
name: "about", //这里是跳转页面的name,要与路由设置保持一致
params: { id: fd.id },
});
window.open(to.href, "_blank");//新开一个页面,打开about
};
return {
browseIt,
};
},
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
三、导航条及子菜单
1、导航条
系统生成的代码,已经做了很好的示范:
<template>
<!-- 导航条 -->
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</nav>
<!-- 导航的目的地。注意该部分必不可少 -->
<router-view/>
</template>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
通常,我们的菜单可以从服务器端返回,然后利用循环语句输出。
2、子菜单
导航条是一级菜单,二级或以下是子菜单。子菜单,可以利用UI框架来完成。比如我们利用ant design vue的menu组件来完成
四、ajax请求封装
我们做的项目,总免不了要从服务器端请求数据。前后端分离,理论上前端的数据都来自于服务器端。
目前一般是结合第三方组件axios对ajax请求进行封装。理由主要有2个:
1)ajax一般有超时、返回代码区别对待等共性操作,封装一个ajax处理方法,统一调用,方便维护和修改;除此之外,axios还可以对ajax请求进行拦截,使得请求前、请求后、结果返回做相应处理。
2)解决跨域问题。axios本身似乎并不解决跨域问题,但由于第一点,它对ajax请求进行了封装,我们可以利用一个统一方法,结合api路径前缀做转发,使得浏览器以为api所在路径与前端是同一台服务器,因而不存在跨域。注意所谓跨域问题,是浏览器的一个安全设置,是一种保护措施。只要它不觉得跨域,那跨域就不存在。
1、统一的ajax封装方法
1)ajax封装代码(src/request/index.js)
import axios from "axios";
// 创建一个 axios 实例
const service = axios.create({
baseURL: "/api", // 所有的请求地址前缀部分
timeout: 60000, // 请求超时时间毫秒
withCredentials: true, // 异步请求携带cookie
headers: {
// 设置后端需要的传参类型
"Content-Type": "application/json",
//'token': 'your token',
"X-Requested-With": "XMLHttpRequest",
},
});
// 添加请求拦截器
service.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
return config;
},
function (error) {
// 对请求错误做些什么
console.log(error);
return Promise.reject(error);
}
);
// 添加响应拦截器
service.interceptors.response.use(
function (response) {
console.log(response);
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
// dataAxios 是 axios 返回数据中的 data
const dataAxios = response.data;
// 这个状态码是和后端约定的
//const code = dataAxios.reset;
return dataAxios;
},
function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
console.log(error);
return Promise.reject(error);
}
);
export default service;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
2)使用ajax统一封装方法
import request from "@/request";
export const userLogin = (params) => {
return request({ //request就是统一封装好的方法
url: "/sys/login",//注意request方法会在前面加上前缀“/api”,变成实质上是请求 “/api/sys/login”
params,
method: 'post'
})
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
2、开发环境中,利用vue.config.js做api路径转发
如上所属,ajax的封装方法request,会在api请求路径前加上前缀“/api”,可以利用这一点来设置转发。转发是为了避免跨域。其原理,表面上,我们请求的是当前服务器的路径“/api/a/b/c”,但我们设置凡“/api”开头的路径,都转发到另一台服务器(即后端所在服务器)。浏览器蒙在鼓里,并没有察觉,因此不会触发所谓跨域警告。
devServer: {
//devServer 只是一个webpack插件 只能用于开发环境
proxy: {
"/api": {
target: "192.168.0.22",
pathRewrite: {
"^/api": "",
},
},
},
},
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
3、生产环境中,利用nginx做api路径转发
location /api/ {
proxy_pass http://192.168.0.22:8090/;#必须斜杠/结尾
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
}
- 1
- 2
- 3
- 4
- 5
- 6
五、vue.config.js及系统配置
有关vue.config.js里面的配置,上面约略提到了一些,这里给出完整代码:
1、系统生成的vue.config.js代码
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true
})
- 1
- 2
- 3
- 4
2、按项目需要修改后的代码
const { defineConfig } = require("@vue/cli-service");
const path = require("path");
const appConfig = require("./public/config");
const resolve = (dir) => {
return path.join(__dirname, dir);
};
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
//devServer 只是一个webpack插件 只能用于开发环境
proxy: {
"/api": {
target: appConfig.server,
pathRewrite: {
"^/api": "",
},
},
},
},
// 项目部署基础
// 默认情况下,我们假设你的应用将被部署在域的根目录下,
// 例如:https://www.my-app.com/
// 默认:'/'
// 如果您的应用程序部署在子路径中,则需要在这指定子路径
// 例如:https://www.foobar.com/my-app/
// 需要将它改为'/my-app/'
publicPath: "/",
chainWebpack: (config) => {
config.resolve.alias
.set("@", resolve("src")) // key,value自行定义,比如.set('@@', resolve('src/components'))
.set("_c", resolve("src/components"));
config.plugin("html").tap((args) => {
args[0].title = appConfig.app.name;
return args;
});
},
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
值得一提的是,里面的devServer元素设置,针对的是开发环境。详见拙作:vue.config.js中的devServer。
3、真正的项目配置
按我的理解,vue.config.js只在开发阶段和发布时有用,之后就像被消费了的耗材,没啥用处了。同一项目中,真正的项目配置,是/public/config/index.js,即使是发布、部署到生产环境,仍然可以修改,是真正意义上的配置文件。
/public/config/index.js
exports.app = {
name: "订餐拿饭抓阄系统",
owner: "蓬蓬养猪场",
developer: "一群饭桶",
};
- 1
- 2
- 3
- 4
- 5
- 6
详见拙作:vue项目读取全局配置
六、登录及退出
主要是路由的应用。原理是:
将页面分为无须登录可浏览和必须登录方可浏览2种。在路由表中做过滤。当转向必须登录页面时,检查登录状态,如果已登录,放行;未登录,转向登录页。注意登录页要设为无须登录可浏览。流程很简单,大家都明白,就不画流程图了。
1、路由表(/src/router/index.js)
完整的路由表。有关登录控制部分,见所谓“路由守卫”。
import { createRouter, createWebHashHistory } from "vue-router";
import Home from "../views/home/PageIndex.vue";
const routes = [
{
path: "/login",
name: "login",
component: () => import("../views/login/PageIndex.vue"),
meta: {
noLogin: true, //无须登录即可浏览。自定义属性
},
},
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/map",
name: "Map",
component: () => import("../views/map/PageIndex.vue"),
},
{
path: "/resource",
name: "Resource",
component: () => import("../views/resource/PageIndex.vue"),
},
{
path: "/resource/detail/:id",
name: "ResourceDetail",
component: () => import("../views/resource/PageDetail.vue"),
},
{
path: "/sys",
name: "Sys",
component: () => import("../views/sys/PageIndex.vue"),
},
];
const router = createRouter({
history: createWebHashHistory(),
routes,
});
// 路由守卫
router.beforeEach((to, from, next) => {
const isLogin = localStorage.isLogin ? true : false;
if (isLogin) {
//已经登录的情况下,不能再打开登录页
to.path === "/login" ? next("home") : next();
} else {
//如果无须登录则直接打开,否则转向登录页面
to.meta.noLogin || to.path === "/login" ? next() : next("/login");
}
});
export default router;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
2、登录及登出
注意要使用router本身的方法,不可用原始的js,如window.location.href = *** 这种方法,否则部署到nginx会报错。
1)登录
const router = useRouter();
localStorage.setItem("isLogin", true);
document.cookie = "token=" + res.token;
router.replace({ path: "/" });//转向首页。使用replace,避免登录后回退问题
- 1
- 2
- 3
- 4
2)登出
const router = useRouter();
localStorage.removeItem("isLogin");
router.replace({ path: "/login" });
- 1
- 2
- 3
七、store
vuex store。
其作用,主要是提供数据共享,使得各组件之间方便传递数据。vue基于组件开发,但组件之间传递参数甚为繁琐。有了store,那么所有组件都能直接从store读取,十分方便。其次,store还可以用来实现数据缓存。比如获取数据时,先看看store里有没有,有的话,直接读取返回;没有的话,从数据库读取,然后存入store再返回。缓存的话,前端存入localStorage也可以,但有个数据过期问题,需要手动清除,或者使用时间戳进行比较。而store,刷新页面即消失,或者关掉浏览器也消失,一般没有数据老化的问题。
1、引入store
在main.js中引入
2、定义store
如果在创建vue项目的时候,选上支持store,系统自动生成一个文件src/store/index.js,
其代码是这样的:
import { createStore } from "vuex";
export default createStore({
state: {},
getters: {},
mutations: {},
actions: {},
modules: {
},
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
1)全局性定义
这是全局性的。为了细分,可以分模块进行定义,比如A模块一个store定义,B模块一个,然后都放在modules这里。如图所示:
2)模块定义
/src/store/category.js
export default {
namespaced: true,
state: {
v: "hello store", // for test
items: [],
},
getters: {
v: (state) => state.v,
items: (state) => {
return state.items;
},
},
mutations: {
// 同步操作
setV(state, val) {
state.v = val;
},
setItems(state, val) {
state.items = val;
},
},
actions: {
keepItems: ({ commit }, val) => {
commit("setItems", val);
},
},
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
用法可参考拙作:vuex动态注册嵌套module提供模块内部组件间的数据共享
3、使用store
1)读
import store from "@/store";
export const getCategory = (callback) => {
let items = store.state.category.items;//读取<----------------------
if (items.length > 0) {
callback(items);
} else {
。。。
//写入<----------------------
store.dispatch("category/keepItems", items).then(() => {
callback(items);
});
}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
2)写
见上例
八、身份认证与访问控制
1、身份认证
是指向后台请求数据时的身份认证。我们采用JWT机制。登录时,得到token,存放在localStorage;然后在向后台请求或提交请求时,则在http 头部附上该token。这个工作,在上面 第四章ajax请求封装 中实现。
import axios from "axios";
import { getToken } from '@/libs/util'
// 创建一个 axios 实例
const service = axios.create({
baseURL: "/api", // 所有的请求地址前缀部分
timeout: 60000, // 请求超时时间毫秒
withCredentials: true, // 异步请求携带cookie
headers: {
// 设置后端需要的传参类型
"Content-Type": "application/json",
//'token': 'your token',
"X-Requested-With": "XMLHttpRequest",
},
});
// 添加请求拦截器
service.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
const token = getToken();
if (token) {
config.headers['Authorization'] = 'Bearer ' + token;//<----------------------------
}
return config;
},
function (error) {
// 对请求错误做些什么
console.log(error);
return Promise.reject(error);
}
);
。。。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
2、访问控制
前端的菜单访问控制,在路由中控制;按钮级别的访问控制,自然可以在每个组件里实现。