如何从一个成熟项目中抽象开发环境(下)

前言

继续上篇文章的内容,这篇主要是前端相关的代码。

在进行抽离的时候发现,需要删除的内容非常多,然后需要学习的东西也不少。 因此先分成几个模块,并一步步建立核心的开发流程。


1、前端路由
2、身份鉴权
3、数据管理
4、动态权限管理

前端路由

常规路由基本就是写一个组件,然后去总的目录下增加一个。 这里是通过配制式开发,将所有路由维护在一个目录下,暴露出指定格式的内容,比如:


import Welcome from '../pages/Welcome';

export default [
  {component: Welcome, path: 'home'}
];

然后在路由设置文件routesSet.tsx中,导入该类文件

export default [
  'login',
  'welcome'
];

入口文件index.tsx如下写:


import routesSet from './routesSet';

let childRoutes = [];
routesSet.forEach(element => {
  const route = require(`./${element}.routes`).default; // 加载同目录下的对应路由配置
  childRoutes = childRoutes.concat(route); // 将其拼接成一个数组
});

export const routes = childRoutes; // 赋值给routes并暴露出去

const routeConfig = [{
  path: '/',
  component: App,
  exact: true, // 如果为true,则仅在路径与location.pathname完全匹配时才匹配。
  childRoutes: [
    ...childRoutes,
    {name: 'Page not found', component: PageNotFound, path: '*'}
  ].filter(r => r['component'] || (r['childRoutes'] && r['childRoutes'].length > 0))
}]; // 过滤掉不匹配格式的项

// 生产配置json,其中每个路由配置还只是string,需要处理
const handleIndexRoute = route => {
   // 处理子路由
  if (!route.childRoutes || !route.childRoutes.length) {
    return;
  }
  const indexRoute = route.childRoutes.find(child => child.isIndex);
  if (indexRoute) {
    const first = { ...indexRoute };
    first.path = route.path;
    first.exact = true;
    first.autoIndexRoute = true;
    route.childRoutes.unshift(first); // 在对应路由下的子路由前端添加
  }
  route.childRoutes.forEach(handleIndexRoute); // 继续向内检索
};

routeConfig.forEach(handleIndexRoute); // 遍历每一项路由看看有没有子路由
export default routeConfig; // 将完整的路由表暴露出去


身份鉴权

这部分主要是node端做完鉴权的操作后,前端做个存储的动作,并在发送请求中加入鉴权的token。

主要是渲染的时候node端新加一个节点,然后前端从这个节点取数据后删除这个节点。


/** save csrfToken and remove the element */
const csrfToken = document.querySelector('#csrfToken');
csrfToken && (
  window['csrfToken'] = csrfToken['value'],
  csrfToken.remove()
);


封装自己的http请求,在最终请求中都带上token


  // 最终body拼接
  const finalOptions = Object.assign({}, {
    headers: {
      'Content-Type': 'application/json',
      'csrf-token': window['csrfToken'],
      'crypt-key': (window['crypt'] || {}).cryptkey
    },
    credentials: 'same-origin',
    method
  }, options);
  if (needBody(method)) {
    finalOptions.body = JSON.stringify(data);
  }

数据管理

数据管理的主要内容和之前node端的api_generate很相似。通过配置生成思想,利用rxjs的一些特性来实现。



// 定义动作类型。主要是异步动作,比如http请求
export const actionTypes = {
  CUSTOM: 'custom',
  DATA: 'data',
  VALUE_DATA: 'valueData',
  BOOL_DATA: 'boolData',
  ACTION: 'action',
  ASYNC_ACTION: 'asyncAction'
};

export default (entityName: string, configs: IConfigs) => {
  const result: any = {};

  /** every entity has a common actionLoading */
  // 共同的 loading 初始化为false, 并且每个动作都有其debug输出的动作。
  const actionLoading$ = new BehaviorSubject(false).debug(`[${entityName}]:[actionLoading]`);
  result.actionLoading$ = actionLoading$;

  // 遍历每个配置
  configs.forEach(config => {
    const {
      /** common */
      type, name, custom,

      /** data, valueData, boolData */
      init,

      /** boolData */
      privateName,

      /** action */
      handler,

      /** asyncAction */
      privateLoading, actionLoading, confirmLoading, bindLoading,
      requestOpts, filter, data, pagination
    } = config;

    // 对每个动作的类型进行判断操作,创建对应的BehaviorSubject
    switch (config.type) {
      // 如果是custom,说明是自定义流
      case actionTypes.CUSTOM:
        result[`${name}$`] = custom(result).debug(`[${entityName}]:[${name}]`);
        break;
      case actionTypes.DATA:
        /** create data Observable */
        result[`${name}$`] = new BehaviorSubject(init || false).debug(`[${entityName}]:[${name}]`);
        break;

      case actionTypes.VALUE_DATA:
        /** create data Observable */
        result[`${name}$`] = new BehaviorSubject(init || 0).debug(`[${entityName}]:[${name}]`);

        /** set data operation */
        // 更新对应BehaviorSubject的当前值
        result[`set${toCamel(name)}`] = params => {
          result[`${name}$`].next(params);
        };
        break;

      case actionTypes.BOOL_DATA:
        /** create data Observable. If privateName is true, it'll use ${name} to be the name */
        if (privateName) {
          result[`${name}$`] = new BehaviorSubject(init || false).debug(`[${entityName}]:[${name}]`);
        } else {
          result[`${name}Show$`] = new BehaviorSubject(init || false).debug(`[${entityName}]:[${name}Show]`);
        }

        /** private loading,you can emit data wherever you need */
        if (privateLoading) {
          result[`${name}Loading$`] = new BehaviorSubject(false).debug(`[${entityName}]:[${name}Loading]`);
        }

        /** show operation */
        result[`show${toCamel(name)}`] = () => {
          if (privateName) {
            result[`${name}$`].next(true);
          } else {
            result[`${name}Show$`].next(true);
          }
        };

        /** hide operation */
        result[`hide${toCamel(name)}`] = () => {
          if (privateName) {
            result[`${name}$`].next(false);
          } else {
            result[`${name}Show$`].next(false);
          }
        };
        break;

      case actionTypes.ACTION:
        /** create operation */
        result[`${name}`] = params => {
          handler(params, result);
        };
        break;

      /** async action */
      case actionTypes.ASYNC_ACTION:

        /** private loading,auto emit data in async operation */
        if (privateLoading) {
          result[`${name}Loading$`] = new BehaviorSubject(false).debug(`[${entityName}]:[${name}Loading]`);
        }

        /** create data Observable,auto emit data when request success */
        // 创建数据流,每当它请求成功都自动发送数据
        if (data) {
          result[`${name}Data$`] = new BehaviorSubject(data.init).debug(`[${entityName}]:[${name}Data]`);
          result[`set${toCamel(name)}Data`] = data => {
            result[`${name}Data$`].next(data);
          };
          result[`merge${toCamel(name)}Data`] = (key, data, isArray) => {
            result[`${name}Data$`].next(
              Object.assign(isArray ? [] : {}, result[`${name}Data$`].value, { [key]: data })
            );
          };
        }

        /** create filter Observable */
        if (filter) {
          result[`${name}FilterData$`] = new BehaviorSubject(filter.init).debug(`[${entityName}]:[${name}FilterData]`);
          result[`set${toCamel(name)}FilterData`] = data => {
            result[`${name}FilterData$`].next(data);
          };
        }

        if (pagination) {
          // 默认第一项
          result[`${name}CurPage$`] = new BehaviorSubject(1).debug(`[${entityName}]:[${name}CurPage]`);
          result[`set${toCamel(name)}CurPage`] = data => {
            result[`${name}CurPage$`].next(data);
          };

          result[`${name}PageSize$`] =
            new BehaviorSubject(pagination.pageSize || 20).debug(`[${entityName}]:[${name}PageSize]`);
          result[`set${toCamel(name)}PageSize`] = data => {
            result[`${name}PageSize$`].next(data);
          };

          result[`${name}Total$`] = new BehaviorSubject(0).debug(`[${entityName}]:[${name}Total]`);
        }

        /** create the main operation */
        result[name] = (params?) => {
          /** loading => true */
          actionLoading && result[`actionLoading$`].next(true);
          privateLoading && result[`${name}Loading$`].next(true);
          confirmLoading && common.showConfirmLoading();

          /** bind custom loading */
          // 更细颗粒度的绑定,与公共组件的loading区分开
          bindLoading && result[`${bindLoading}`].next(true);

          requestOpts.before && requestOpts.before(params, result);

          request({
            method: requestOpts.method,
            url: requestOpts.url(params, result),
            errNotification: requestOpts.errNotification,

            /** requestOpts.data is a function that return data according to params and the entity */
            // omit 删除对象中给定的 keys 对应的属性,后台不接受suc,err等字段
            data: requestOpts.data && requestOpts.data(omit(['sucCallback', 'errCallback'], params), result),

            sucCallback: response => {
              /** loading => false */
              actionLoading && result[`actionLoading$`].next(false);
              privateLoading && result[`${name}Loading$`].next(false);
              confirmLoading && common.hideConfirm();

              /** bind custom loading */
              bindLoading && result[`${bindLoading}`].next(false);

              /** set data according to response */
              data && result[`${name}Data$`].next(data.handler(params, response, result));

              /** set total according to response if pagination */
              pagination && result[`${name}Total$`].next(pagination.total(params, response, result));

              /** do callback if needed */
              requestOpts.sucCallback && requestOpts.sucCallback(params, response, result);
              params && params.sucCallback && params.sucCallback(params, response, result);
            },
            errCallback: err => {
              /** loading => false */
              actionLoading && result[`actionLoading$`].next(false);
              privateLoading && result[`${name}Loading$`].next(false);
              confirmLoading && common.hideConfirmLoading();

              /** bind custom loading */
              bindLoading && result[`${bindLoading}`].next(false);

              /** do callback if needed */
              requestOpts.errCallback && requestOpts.errCallback(params, err, result);
              params && params.errCallback && params.errCallback(params, err, result);
            }
          });
        };

        break;
    }
  });

  return result;
};

之后只要暴露类似配置,让其自动生成即可


  {
    type: actionTypes.ASYNC_ACTION,
    name: 'login',
    privateLoading: true,
    requestOpts: {
      method: 'POST',
      url: (params, entity) => `/apis/user/login`,
      data: (params, entity) => cipher(params),
      sucCallback: (params, data, entity) => {
        /** 登陆后设置用户信息 */
        setValue('userInfo', JSON.stringify(data));
        console.log(data);
      }
    }
  },

动态权限管理

这个其实主要思路是前后端约定权限列表的内容。包括视图和功能模块。 每次登陆后返回一份列表,前端存到本地。读取需要显示的内容。 这样就做到了动态的权限分配。

Table of Contents