阅读 787

后台项目总结

之前在公司主要负责后台项目,就趁着即将离职回顾和总结自己踩过的坑

目录结构

初始化目录使用的是 Vue CLI 生成的,但是默认的目录结构不能满足对项目需求,下面是拓展后的结构

├── public                          // 静态页面
├── scripts                         // 相关脚本配置
├── src                             // 主目录
    ├── assets                      // 静态资源
    ├── api                         // 接口信息
    ├── filters                     // 过滤
    ├── lib                         // 全局插件
    ├── router                      // 路由配置
    ├── store                       // vuex 配置
    ├── styles                      // 样式
    ├── utils                       // 工具方法(axios封装,全局方法等)
    ├── views                       // 页面
    ├── App.vue                     // 页面主入口
    ├── main.js                     // 脚本主入口
├── tests                           // 测试用例
├── .editorconfig                   // 编辑相关配置
├── .postcssrc.js                   // postcss 配置
├── babel.config.js                 // preset 记录
├── package.json                    // 依赖
├── .eslintrc.js                    // eslint相关配置
├── README.md                       // 项目 readme
└── vue.config.js                   // webpack 配置
复制代码

权限管理

一个中大型的后台必然少不了权限的控制,其中RBAC模型是需要了解的,更复杂的模型就是在此基础上演化而来的,因为之前写过相关文章,这里就不具体介绍了,可以点击查看漫谈一下权限设计相关

axios 和 mock

axios

这里axios是重点说的部分,axios 是一个 HTTP 库,可以用在浏览器和 node.js 中,在使用 srr 同构的时候也会经常使用它,下面说一下基本的封装方式,具体情况根据业务调整。

// utils/config.js
import http from "http";
import https from "https";
import qs from "qs";

const axiosConfig = {
  // 注意生产环境下要区分,这里只做演示
  baseURL: "/mock/",
  // 请求后的数据处理
  transformResponse: [
    function(data) {
      return data;
    }
  ],
  // 查询对象序列化函数
  paramsSerializer: function(params) {
    return qs.stringify(params);
  },
  // 超时设置
  timeout: 30000,
  // 跨域是否带Token
  withCredentials: true,
  responseType: "json",
  // xsrf 设置
  xsrfCookieName: "XSRF-TOKEN",
  xsrfHeaderName: "X-XSRF-TOKEN",
  // 最多转发数,用于node.js
  maxRedirects: 5,
  // 最大响应数据大小
  maxContentLength: 2000,
  // 自定义错误状态码范围
  validateStatus: function(status) {
    return status >= 200 && status < 300;
  },
  // 用于node.js
  httpAgent: new http.Agent({ keepAlive: true }),
  httpsAgent: new https.Agent({ keepAlive: true })
};

export default axiosConfig;
复制代码

上面定义的是配置文件,之所以不全局调用axios或者修改配置的原因是会造成污染,下面还需要对拦截器这块做下处理,取消重复请求(如果有业务代码也可以在里面配置,比如需要统一发送token等参数)。

// utils/api.js
import axios from "axios";
import config from "./config";

// 取消重复请求
let pending = [];
const cancelToken = axios.CancelToken;
const removePending = config => {
  for (let p in pending) {
    let item = p;
    let list = pending[p];
    // 当前请求在数组中存在时执行函数体
    if (list.url === config.url + "&request_type=" + config.method) {
      // 执行取消操作
      list.cancel();
      // 从数组中移除记录
      pending.splice(item, 1);
    }
  }
};

const service = axios.create(config);

// 添加请求拦截器
service.interceptors.request.use(
  config => {
    removePending(config);
    config.cancelToken = new cancelToken(c => {
      pending.push({
        url: config.url + "&request_type=" + config.method,
        cancel: c
      });
    });
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);

// 返回状态判断(添加响应拦截器)
service.interceptors.response.use(
  res => {
    removePending(res.config);
    return res;
  },
  error => {
    return Promise.reject(error);
  }
);

export default service;
复制代码

OK,基本封装到这一步就说完了,在具体开发中可以在通过引入vuexAction来调用axios

mock

mock 在前后端开发中很常见,为了解耦与后端之前不必要的等待时间,通过约定文档接口的形式,让前端可以快速开发。

这里简单说一下如何配置,具体的接口等信息根据业务来调整,首先在src创建一个mock文件夹,作为 mock 存放的信息,如果业务分为多层记得做好划分,之后通过暴露index.js文件,在src/main.js中全局引入,下面是一个简单的 demo 例子

// src/mock/index.js
import Mock from "mockjs";
// 获取 mock.Random 对象
const Random = Mock.Random;
// 设置异步等待时间
Mock.setup({
  timeout: "200-2000"
});
const produceNewsData = function() {
  let newNewsObject = {
    title: Random.ctitle(), //  Random.ctitle( min, max ) 随机产生一个中文标题,长度默认在3-7之间
    content: Random.cparagraph(), // Random.cparagraph(min, max) 随机生成一个中文段落,段落里的句子个数默认3-7个
    createdTime: Random.date() // Random.date()指示生成的日期字符串的格式,默认为yyyy-MM-dd;
  };

  return newNewsObject;
};
// 随便演示的请求图片
const demoOther = function() {
  let newsList = [];
  for (let i = 0; i < 20; i++) {
    let newNewsObject = {
      img: Random.dataImage("300x250")
    };
    newsList.push(newNewsObject);
  }

  return newsList;
};
// 请求这个url就会被mock
Mock.mock("/mock/user", produceNewsData);
Mock.mock("/mock/other", demoOther);
复制代码
// src/main.js
// 省略一下其他代码
import "./mock";
复制代码

上面封装axios配置文件config.js下填写了baseURL,所以可以直接省略mock的前缀,注意记得区分开发和生产环境

全局 loading

先说一下常见的 loading 集中管理方式

  • 手动管理,每个页面引入一个 loading,在即将发送的请求的时候展开,请求结束关闭,除了啰嗦和修改麻烦之外没有问题
  • 通过一些中间件来完成拦截,例如dav-loading
  • 通过拦截器拦截,例如axios

这里介绍第三种的具体实现,原因在于是后台项目 loading 实质上只有一种,所以没有必要引入第二种影响划分组件的灵活性。

实现思路很简单,借助api目录的划分将需要 loading 的接口单独放置在一个文件中,通过axios请求、响应拦截器匹配url完成显示隐藏 loading,下面是具体的实现

// src/utils/app.js
import axios from "axios";
import config from "./config";
import vue from "vue";
import * as loadingAPI from "@/api/loading";

// 通过这个变化,来提醒loading显示与否
const state = vue.observable({ content: 0 });

function changeState(config, add = true) {
  const { url } = config;
  const value = Object.values(loadingAPI);

  if (!value.includes(url)) {
    return;
  }
  if (add) {
    state.content += 1;
    return;
  }
  state.content -= 1;
  return;
}

const service = axios.create(config);

// 添加请求拦截器
service.interceptors.request.use(
  config => {
    changeState(config);
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);

// 返回状态判断(添加响应拦截器)
service.interceptors.response.use(
  res => {
    changeState(res.config, false);
    try {
      const { data } = res;
      Reflect.set(res, "data", JSON.parse(data));
    } catch (e) {
      //
    }
    return res;
  },
  error => {
    return Promise.reject(error);
  }
);
// 省略一些代码
export default service;
export { state };
复制代码

借助vue.observable生成一个可供组件用于渲染函数和计算属性内的对象,通过改变这个对象内部content值来完成 loading 的显示和隐藏, 上面引入的 api 接口就是全部需要管理 loading 的接口地址,这也是上面为什么强调需要单独划分api目录的原因,之后再页面中引入 loading组件 和state对象

<template>
  <div>
    <loading v-model="show" />
    <router-view class="router"></router-view>
  </div>
</template>

<script>
  import loading from "./loading";
  import { state } from "@/utils/server";
  export default {
    components: {
      loading
    },
    data: () => ({ state }),
    computed: {
      show() {
        return this.state.content > 0;
      }
    }
  };
</script>
复制代码

具体 loading 组件实现细节这里就跳过了,最后说一下页面的请求直接像正常页面一致即可,例如

serve.get(user).then(user => {
  this.content = user.data.content;
  this.title = user.data.title;
});
// 一些其他操作不会影响到loading的
Promise.all([serve.get(other)]).then(all => {
  console.log(all);
});
复制代码

虚拟列表

听名字可能有点不知所然,抛砖引玉之前写项目遇到的一个场景,在后台项目(根据业务)图片管理功能可能经常遇到,后端返回给你一定数量的图片的地址然后你滚动加载这些信息,这段逻辑很简单,不过需要思考这样会不会有性能上的问题?显然会的,当滚动的数量足够多的时候,之前的dom 节点一直存在就会造成浏览器的卡顿(吐槽一下知乎的无限滚动)。

虚拟列表解决的就是类似的场景,用户感知的区域永远是有限的,只需要在需要变化的时候更换展示的数据就可以了,这里贴一下之前写的文章浅谈一下列表优化

路由组件自定义缓存

一个很常见的场景就是在电商页面中,当你在列表中点击进入详情在返回的时候,列表信息一定是存在没有被清空的,也就是

主页->列表->详情

在列表前往详情的时候返回我们显然希望这个页面的信息不回被清空,从列表返回到主页再进入列表,这一过程列表的信息不希望保留,下面就聊聊如何实现

keep-alive

vue 内置了组件keep-alive,它的作用就是缓存组件的信息,在官方指南中它和动态组件结合节省了不必要的渲染,同时它也是可以与<router-view></router-view>结合做到缓存路由页面的。

<keep-alive>
  <router-view></router-view>
</keep-alive>
复制代码

不过单纯使用和这个只能做到缓存,但是却做不到我们要求的指定某个页面缓存,还有其他方法么?

其实也是有的可以定义两个<router-view/>根据标志来区分是否需要缓存,然后利用keep-alive内置组件的include属性动态调整就可以实现我们的需求了。

说的有点抽象了,先看一个例子吧

1

// route
// 省略部分代码
 {
      path: "/keepAlive/",
      component: keepAlive,
      children: [
        {
          path: "",
          component: () =>
            import(/* webpackChunkName: "keep-alive-home" */ "@/view/keep-alive/home"),
          name: "keepAlive",
          meta: {
            depth: 1
          }
        },
        {
          path: "list",
          component: () =>
            import(/* webpackChunkName: "keep-alive-list" */ "@/view/keep-alive/list"),
          name: "list",
          meta: {
            name: "list",
            keepAlive: true,
            depth: 2
          }
        },
        {
          path: "details",
          component: () =>
            import(/* webpackChunkName: "keep-alive-details" */ "@/view/keep-alive/details"),
          name: "details",
          meta: {
            depth: 3
          }
        }
      ]
    },
复制代码

上面的路由信息keepAlive代表是否需要缓存,depth代表深度,是我们对比的时候使用,在定义<router-view/>的 vue 文件内,通过监听$route来实现具体逻辑

// index.vue
<template>
  <!-- 定义两个出口,一个可以被缓存,一个普通路由 -->
  <div>
    <keep-alive :include="include">
      <router-view v-if="$route.meta.keepAlive"></router-view>
    </keep-alive>
    <router-view v-if="!$route.meta.keepAlive"></router-view>
  </div>
</template>

<script>
  export default {
    data: () => ({ include: [] }),
    watch: {
      $route(to, from) {
        // 判断进入的情况
        const { keepAlive, name, depth } = to.meta;
        if (keepAlive && !this.include.includes(name)) {
          this.include.push(name);
        }
        // 判断回退的情况
        if (from.meta.keepAlive && from.meta.depth > depth) {
          // 删除
          const index = this.include.indexOf(from.meta.name);
          if (index !== -1) {
            this.include.splice(index, 1);
          }
        }
      }
    }
  };
</script>
复制代码

这里需要注意一点! 就是做缓存的页面一定要有name属性值

include: 匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components选项的键值)。匿名组件不能被匹配。

上述为了方便管理,将组件的 name 属性和路由的meta属性做到了一致

最后

上面演示的例子,放到了我的仓库,有兴趣可以 clone 下来自己调试。

我本人正在求职,如果小伙伴有合适的岗位拜托帮忙内推下,具体简历可以私聊评论我,或者联系我的邮箱yangboses@gmail.com

关注下面的标签,发现更多相似文章
评论