# vue-node-project
**Repository Path**: StellaYangFan/vue-node-project
## Basic Information
- **Project Name**: vue-node-project
- **Description**: A web project developed with Vue and node both in JavaScript.
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 70
- **Created**: 2020-06-03
- **Last Updated**: 2020-12-19
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Vue 构建 Web 运用
项目实践目标:
1. 建立 Vue CLI4 版本的环境搭建
2. 路由配置的模块化管理
3. element-UI 的应用和组件模块化管理
4. axios 的二次封装
5. vuex 的模块化配置
6. 获取后台数据应用到
7. 轮播图小案例
8. 用户注册和登录权限的应用
9. 验证码的应用
10. 路由和菜单权限的渲染和处理
11. 对 webSocket 的封装和使用
## 二次封装 axios
基于 `axios` 二次封装 `Http` 请求类,内部封装好请求和响应的拦截 (**interceptors**),实现基础功能:
- `get`(url, config)
- `post`(url, data)
- `request`(config)
## vuex 模块化配置
配置 `store` 仓库,实现全局组件的数据共享,通过 `require.context` 动态引入子模块 (module),包括
- user module
- artile module
- rootModule
## 接口管理
- 编写调用后天接口,获取数据
- 接口调用位置
### 编写调用后天接口,获取数据
构建 api 目录
该目录下,放置与后台交互的接口文件
- **config** 目录,配置接口的路径信息,如:**/public/getSlider**, **/user/login**
- **public.js** 引入 `axios` (二次封装后的) 发起后台请求,获取数据,公共数据接口
- **user.js** 与用户相关接口数据,如:登录、注册、注销、忘记密码等
### 接口调用位置
- 页面中直接调用
- vuex 中
- 当数据是全局时
- 提高复用性
- 做缓存功能
**vuex 中调用接口**
在 store 目录下,新增 action-types 文件,存放派发动作的名称。
原因:
- mutations, actions 中编写的动作名
- 会在调用的时候再次书写,避免出错
代码示例:
```js
new Vue({
computed: {
...mapState([ 'sliders' ]),
},
methods: {
...mapActions([ SET_SLIDER ]),
},
async created() {
if (this.sliders) return;
await this[SET_SLIDER]();
console.log(this.sliders);
},
})
```
> 注意:在派发 **SET_SLIDERS** 动作时,如果想立即拿到获取后的 sliders,需要 async... await,由于是异步请求。
### 获取验证码
- 登陆页,利用 uuid 产生唯一标识去后台获取验证码
- 用户名/密码/验证码,后台验证登陆通过后,返回用户信息,携带 token
- 保存 token, 并修改全局状态 userInfo, hasPermission
- 根据 permission 状态,动态渲染顶部右侧菜单栏
### 新增 validate 接口
实现
```js
export const validate = () => axios.get(user.validate);
```
调用
```js
async [USER_VALIDATE]({ dispatch }) {
if (!getLocal('token')) return false;
try {
let result = await validate();
dispatch(SET_USER, { payload: result.data, permisson: true });
return true;
} catch(err) {
dispatch(SET_USER, { payload:{}, permisson: false });
return false;
}
},
```
### 新增 router hooks
全局路由钩子函数 beforeEach,添加:
- 登陆权限钩子 loginPermission(to, from, next)
## 路由/菜单权限
在登录权限后,新增菜单权限,根据当前登录的用户类别,动态添加路由
- 管理员
- 普通用户
**router/hooks.js**
```js
export const menuPermission = async function(to, from, next) {
if (store.state.user.hasPermission) {
if (!store.state.user.menuPermission) {
store.dispatch(`user/${ADD_ROUTE}`);
next({ ...to, replace: true });
}
next();
} else {
next();
}
}
```
**store/module/user.js**
```js
/* dynamic router */
async [ADD_ROUTE]({ commit, state }) {
let authList = state.userInfo.authList;
if (authList) {
// server responded authList
let routes = filterRouter(authList);
let route = router.options.routes.find(item => item.path === '/manager');
route.children = routes;
console.log(route);
router.addRoutes([route]); // add routes
commit(SET_MENU_PERMISSION, true);
} else {
commit(SET_MENU_PERMISSION, true);
}
}
```
**递归过滤符合权限的菜单路由**
```js
const filterRouter = authList => {
// pid: -1
// _id: "5edcc4d71f9b5a73e1f77293"
// name: "我的文章"
// auth: "myArticle"
// id: 9
// role: "5ec73bc0c514dc322467cd63"
// path: "/manager/myArticle"
let auths = authList.map(auth => auth.auth);
function filter(routes) {
return routes.filter(route => {
if (auths.includes(route.meta.auth)) {
if (route.children) {
route.children = filter(route.children);
}
return route;
}
})
};
return filter(permission);
};
```
**MyMenu**
```js
import { createNamespacedHelpers } from 'vuex';
const { mapState } = createNamespacedHelpers('user');
export default {
name: 'my-menu',
data() {
return {
list: [],
}
},
computed: {
...mapState(['userInfo']),
},
methods: {
getMenuList(authList) {
// pid: -1
// _id: "5edcc4d71f9b5a73e1f77293"
// name: "我的文章"
// auth: "myArticle"
// id: 9
// role: "5ec73bc0c514dc322467cd63"
// path: "/manager/myArticle"
let menu = [];
let map = {};
authList.forEach(m => {
m.children = [];
map[m.id] = m;
if (m.pid == -1) {
menu.push(m);
} else {
map[m.pid] && map[m.pid].children.push(m)
}
});
return menu;
}
},
mounted() {
this.list = this.getMenuList(this.userInfo.authList);
},
render() {
let renderChildren = list => {
return list.map(child => {
return child.children.length ?
{ child.name }
{/* 递归 */}
{ renderChildren(child.children) }
:
{ child.name }
})
}
return
},
}
```
## WebSocket
特性:
- HTML5 提供的单个 TCP 连接全双工协议 (bi-directional messages)
- 允许服务端主动向客户端推送数据
- 浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输
- 相比 Ajax 轮轮询,节省服务器资源和带宽,实时通讯
- 支持跨域
- 不能修改 header 信息,可传递对象字符串
## 踩坑记录
### 登录 hook
- 登陆捕获错误,密码或验证码错误时,状态码也是 200
- 需要手动判断 result.err (1)
- **返回** `Promise.reject(reason)`
- 避免调用时报错
- `Uncaught (in promise) 用户名密码错误`
```js
async [USER_LOGIN]({ dispatch }, payload) {
try {
let result = await login(payload);
if (result.err) {
return Promise.reject(result.data);
} else {
dispatch(SET_USER, { payload: result.data, permisson: true });
}
} catch(error) {
return Promise.reject(error);
}
},
```
### 子菜单递归数据遍历
按照服务端返回数据,如果不重新排序,会导致当前遍历的 id:-1,还未添加到 map[m.id],无法将归属于 m.id 的子菜单获取渲染
```js
getMenuList(authList) {
let menu = [];
let map = {}; // { 1: 用户权限,2:文章管理 }
+ authList.sort((a,b) => a.id - b.id);
authList.forEach(m => {
m.children = [];
map[m.id] = m;
if (m.pid == -1) {
menu.push(m);
} else {
map[m.pid] && map[m.pid].children.push(m);
}
});
return menu;
}
```
添加 `authList.sort((a,b) => a.id - b.id)` 重新排序。