05月26, 2019

admin模板与API

这周开始折腾后台admin的模板开发。之前其实有搞过一版,但后面UI这一块调整了一下,再者我也准备将route这一块单独拿出来。

目录结构

这是src的目录结构

  • pages 用来放页面(容器)组件
  • components 用来放公共组件(如头、尾、Sidebar等)
  • request 请求相关代码
  • util 公共代码(比如方法啥的)
  • assets 公用的资源文件夹
  • index.js 入口文件
  • index.scss 全局样式配置
  • history.js 可以用它来实现路由跳转
  • menuConfig.js 菜单路由配置表

一些约定

外层容器的路由

<Router history={history}>
  <Switch>
      <Route exact path="/login" component={Login} />
      <Route path="/" component={Home} />
  </Switch>
</Router>

登录用户名称

localStorage里面获取username,如果没有该值,则重定向到login页面。

在登录成功后,往localStorage里面写入username

在登录退出后,删除localStorage里面的username

当然毕竟不是ssr的项目,所以存在一种可能是本地有username,但接口响应通过某个特定的code来告知session已失效,这里也应该重定向到login页面。

菜单路由

用过ant-design-pro的人,可能会觉得它的路由搞的比较好。

我个人持保留意见,因为我觉得route4已经变成了组件的概念,所以不应该写成像3那样的。但有些地方可以配置出来的,就是左侧菜单的路由,因此就有了menuConfig.js

它里面有两个东西:

  • RedirectUrl (登录成功后会往哪个模块里面跳)
  • menuList

示例如下:

// 当该值不为空时,会使用该值,默认使用下面数组的第一个菜单
const RedirectUrl = "";

// 必须要保证menuList有一个类似下面的json,必须有children属性,且children里面有一个对象要包含path,不然程序会报错
// componentPath是从pages路径开始找起的
const menuList = [
  {
    icon: "",
    name: "Demo",
    children: [
      {
        path: "/demo/menu1",
        name: "菜单一",
        icon: "",
        componentPath: "demo/menu1"
      },
      {
        path: "/demo/menu2",
        name: "菜单二",
        icon: "",
        componentPath: "demo/menu2"
      }
    ]
  },
  {
    icon: "",
    name: "应用中心",
    children: [
      {
        path: "/application/list",
        name: "应用列表",
        icon: "",
        componentPath: "application/list"
      }
    ]
  }
];

export default menuList;

export const redirectUrl = RedirectUrl !== "" ? RedirectUrl : menuList[0].children[0].path;

alt

这里面要考虑每一个地方都有icon,所以需要有这个配置提供。

componentPath则是为了实现动态加载路由。

鉴权

我觉得这一块没有必要非要放在路由里面来做。因为光路由肯定还不够的。

比如有些操作:新增、删除,这个要在页面里面判断根据权限来显示隐藏按钮。

所以我觉得可以提供一个useAuth的hooks,返回boolean值,来确定当前的一些操作是否可用。

请求

之前我有提过类似这样的写法:

// api.js
const prefix = '/api';

module.exports = {
  'login': {
    method: 'get',
    url: `${prefix}/user/login`,
    desc: '登录'
  },
  'logout': {
    url: `${prefix}/user/logout`,
    desc: '登出'
  }
};

每个对象里面包含以下几个配置:

  • url
  • method:默认为get
  • desc
  • contentType:可选值为:json/form/file,默认为json
  • timeout:默认为5000

看似好像没啥问题,但只考虑了固定URL的情况,有一种URL叫resful,就是类似/detail/:id这样的。

那么那种URL怎么来实现?很简单,通过占位符和占位对象来生成这个URL。

比如URL是/detail/:id,那么在请求的时候,需要传一个占位对象:

API.getDetail({/* 数据data */},  {
    placeholder: {
         id: 1
    }
})

以往只需要一个参数就行了,现在多加一个参数,在里面传placeholder即可。

function isPlainObject(val) {
  return toString.call(val) === '[object Object]'
}
function getUrl(url, placeholder) {
  // 如果有占用对象,说明url里面有占位符,类似detail/:id这样的
  if (placeholder && isPlainObject(placeholder)) {
    url = url.replace(/(:\w+)/g, ($1) => {
      if ($1 !== "") {
        return placeholder[$1.slice(1)] // $1.slice(1),为去掉冒号后的值
      }
      return $1;
    })
  }
  return url;
}

axios的封装

这个话题之前说过很多次了,这次想讲的是cancel请求。

有这样的一种场景,分页列表。用户点击第二页,在还未得到响应的情况下,点了第三页,第三页响应比较快,马上就回来了,但在第三页回来之后,第二页的请求也得到响应,那么最终页面会呈现第二页的结果,然而页码却是第三页。

还有一些情况,比如按钮点击请求,我们是希望他在得到响应之后才能被再次点击,而不是每次点击都有一个请求出去,都能得到响应。

转成需求,也就是:在每次发请求的时候,要把上一次相同的url + method的请求给cancel掉:

function httpErrorHandler(error) {

  if (!error.response) {
    if (error.message) {
      return Promise.reject({
        msg: '网络连接超时',
        error
      });
    }
    return Promise.reject({
      msg: "取消请求"
    });
  };

  let msg = '';
  const status = error.response.status;

  switch (status) {
    case 400:
        msg = '错误的请求参数';
        break;
    case 401:
        msg = '没有该操作权限';
        break;
    case 404:
        msg = '错误的请求地址';
        break;
    case 500:
    case 501:
    case 502:
    case 503:
    case 504:
        msg = '服务器错误';
        break;
    default:
        msg = '未知错误' + status;
  }
  return Promise.reject({
      msg,
      error
  });
}

const instance = axios.create();

// 请求队列
const queue = [];
// axios内置的中断ajax的方法
const cancelToken = axios.CancelToken;
// 拼接请求的url和方法,同样的url+方法可以视为相同的请求
const token = (config) =>{
  return `${config.url}_${config.method}`
}
// 中断重复的请求,并从队列中移除
const removeQueue = (config) => {
  for(let i=0, size = queue.length; i < size; i++){
    const task = queue[i];
    if(task.token === token(config)) {
      task.cancel();
      queue.splice(i, 1);
    }
  }
}

//添加请求拦截器
instance.interceptors.request.use(config=>{
  removeQueue(config); // 中断之前的同名请求
  // 添加cancelToken
  config.cancelToken = new cancelToken((c)=>{
    queue.push({ token: token(config), cancel: c });
});
  return config;
}, error => {
  return Promise.reject(error);
});

//添加响应拦截器
instance.interceptors.response.use(response=>{
  // 在请求完成后,自动移出队列
  removeQueue(response.config);
  return response.data
}, httpErrorHandler);

mock

我的同事写了一个强大的cui-mock-middleware

简单说一下它的用法:

const { resolve } = require('path');
const cuiMockMiddleware = require('cui-mock-middleware');
const mockTable = require('./mock/mock-table');

// 这一段可以放到webpack devServer的before配置中
app.all(['/api/*'], cuiMockMiddleware({
  prefix: "", // 统一前缀
  enable: true, // 是否启用,默认为true
  dir: resolve(__dirname, 'mock'), // mockTable里面的文件查找的路径
  mockTable: mockTable,
  pathRewrite: {},
}));

它需要提供一个mockTable的配置文件。

类似这样的:

module.exports = {
    // 静态路径
  "/api/user/list": "user.json",
  // 路径中携带参数,动态路径匹配,与vue的路由匹配类似,都使用path-to-regexp将规则转为正则来做匹配  
  "/api/user/:id": "user.js",
  // 匹配的可以是一个mock函数,返回对象,query是请求传入的参数对象,包括路径中的参数
  "/api/user/function": function(query) {
    return {
      success: true,
      data: {}
    }
  }
}

如果mockTable中没有请求路径匹配,中间件会根据请求路径到配置的dir目录下匹配文件,匹配${pathname}.json或是${pathname}.js文件。路径过长时可以配合pathRewrite使用,移除上下文。

pathRewrite这个功能,我觉得还是不错的,之前另外一个项目请求突然加了一层,这就意味着我之前的mock里面的目录都得调整一下,有了这个pathRewrite,我只要将新加的这一层重写为空即可。

这个mock的中间件还支持了promise的数据返回,有了这个功能,我就能通过本地mock测试以下两个功能:

  • timeout:超时
  • cancel:是否能取消相同的请求

本文链接:www.my-fe.pub/post/template-and-api.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。