Skip to content
On this page

快速上手

项目地址

https://github.com/pumelotea/happyboot-tiger

下载并运行

shell
git clone -b master https://github.com/pumelotea/happyboot-tiger.git
cd happyboot-tiger
pnpm
pnpm dev 

后端接口地址配置

修改工程根目录下的.env.dev文件,更换VITE_APP_API的值,替换为后端服务的地址。

NODE_ENV = 'development'
VITE_APP_OSS = 'https://oss.xxx.cn'
VITE_APP_API = 'https://api.xxx.cn'

对于接口的定义,默认可以编写在这里src/global/api/index.js

js
import request from '../http'

// demo 代码
// export default {
//   middleViewData: data => request.get('/jscApi/middleViewData', { data }), // 正常请求
//   cancelReq: data => request.get('http://localhost:3003/jscApi/middleViewData', { data, cancelRequest: true }), // 测试取消请求
//   reqAgainSend: data => request.get('/equ/equTypeList11', { data, retry: 3, retryDelay: 1000 }), // 测试请求重发,除了原请求外还会重发3次
//   cacheEquList: data => request.get('/equ/equList', { data, cache: true, setExpireTime: 30000 }), // 测试缓存请求带参数:setExpireTime 为缓存有效时间ms
//   cacheEquListParams: data => request.get('/equ/equList', { data, cache: true }) // 测试缓存请求参数值不一样
// };

export default {
  getCaptcha: () => request.get('/captcha')
}

菜单数据对接

对于菜单的数据结构设计基本上是树形结构的,但是每一位设计者设计出来的结构字段肯定都是不同的,因此需要开发者编写一个适配器,适配器的类型定义也会给出。 下面会给出菜单结构、转换器的定义和参考数据。

菜单数据结构定义

js
/**
 * 菜单类型
 */
declare type MenuType = 'menu' | 'point';
/**
 * 链接跳转类型
 */
declare type LinkTarget = 'self' | 'tab' | 'blank';
declare interface MenuItem {
    /**
     * 必须要有的数据
     */
    menuId: string;
    name: string;
    icon: string;
    path: string;
    view: string;
    isRouter: boolean;
    isKeepalive: boolean;
    type: MenuType;
    externalLink: boolean;
    linkTarget: LinkTarget;
    externalLinkAddress: string;
    hide: boolean;
    isHome: boolean;
    permissionKey: string;
    children: MenuItem[];
    /**
     * 预处理后的数据
     * 使用上面的数据经过预处理后的数据
     */
    routerPath: string;
    menuPath: MenuItem[];
    breadcrumb: MenuItem[];
    pointList: MenuItem[];
    pointsMap: Map<string, MenuItem>;
    [propName: string]: any;
}

/**
 * 菜单数据适配器
 */
declare interface MenuAdapter<T> {
  convert(rawData: any): {
    routeMappingList: T[];
    menuTreeConverted: T[];
    menuIdMappingMap: Map<string, T>;
  };
}

开发菜单数据适配器

js
/**
 * UUID生成
 * @returns {string}
 */
export function uuid () {
  const s = []
  const hexDigits = '0123456789abcdef'
  for (let i = 0; i < 36; i++) {
    s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1)
  }
  s[14] = '4' // bits 12-15 of the time_hi_and_version field to 0010
  // tslint:disable-next-line:no-bitwise
  s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1) // bits 6-7 of the clock_seq_hi_and_reserved to 01
  s[8] = s[13] = s[18] = s[23] = '-'
  return s.join('')
}

/**
 * 深度拷贝
 * @param source
 */
function deepClone (source) {
  if (!source && typeof source !== 'object') {
    throw new Error('error arguments shallowClone')
  }
  const targetObj = source.constructor === Array ? [] : {}
  for (const keys in source) {
    // eslint-disable-next-line no-prototype-builtins
    if (source.hasOwnProperty(keys)) {
      if (source[keys] && typeof source[keys] === 'object') {
        targetObj[keys] = source[keys].constructor === Array ? [] : {}
        targetObj[keys] = deepClone(source[keys])
      } else {
        targetObj[keys] = source[keys]
      }
    }
  }
  return targetObj
}

/**
 * 自定义菜单适配器
 * @returns {{menuTreeConverted: (*|*[]), routeMappingList: *[], menuIdMappingMap: Map<any, any>}|{convert(*): {menuTreeConverted, routeMappingList: [], menuIdMappingMap: *}}}
 */
function createMenuAdapter () {
  return {
    convert (menuTree) {
      const routeMappingList = []
      const menuIdMappingMap = new Map()
      const menuTreeConverted = []

      const menuTypeMap = {
        menu : 'menu',
        point: 'point'
      }

      const linkTargetMap = {
        _tab  : 'tab',
        _self : 'self',
        _blank: 'blank'
      }

      const forEachTree = (tree, pNode) => {
        for (let i = 0; i < tree.length; i++) {
          // 创建新的节点
          const treeNode = createEmptyMenuItem()
          treeNode.menuId = uuid()
          treeNode.name = tree[i].name || ''
          treeNode.path = tree[i].path || ''
          treeNode.icon = tree[i].icon || ''
          treeNode.view = tree[i].view || ''
          treeNode.isRouter = tree[i].isRouter || false
          treeNode.isKeepalive = tree[i].isKeepalive || false
          treeNode.type = tree[i].type || 'menu'
          treeNode.externalLink = tree[i].externalLink || false
          treeNode.linkTarget = linkTargetMap[tree[i].linkTarget] || 'tab'
          treeNode.externalLinkAddress = tree[i].externalLinkAddress || ''
          treeNode.hide = tree[i].hide || false
          treeNode.isHome = tree[i].isHome || false
          treeNode.permissionKey = tree[i].permissionKey || ''
          treeNode.budge = tree[i].budge || null

          if (!pNode) {
            pNode = createEmptyMenuItem()
            menuTreeConverted.push(pNode)
          }
          pNode.children.push(treeNode)
          // 拼接路由
          treeNode.routerPath = pNode.routerPath + treeNode.path
          // 预先生成菜单节点路径
          const tmpNode = deepClone(treeNode)
          tmpNode.children = []
          tmpNode.menuPath = []
          tmpNode.breadcrumb = []
          treeNode.menuPath = [ ...pNode.menuPath, tmpNode ]
          // breadcrumb
          treeNode.breadcrumb = [ ...pNode.breadcrumb, tmpNode ]

          // 记录id映射表
          menuIdMappingMap.set(treeNode.menuId, treeNode)

          if (treeNode.type === 'menu') {
            if (!treeNode.isRouter) {
              forEachTree(tree[i].children, treeNode)
            } else {
              // 收集权限点
              tree[i].children.forEach((e) => {
                const pointNode = createEmptyMenuItem()
                pointNode.menuId = uuid()
                pointNode.name = e.name || ''
                pointNode.path = e.path || ''
                pointNode.view = e.view || ''
                pointNode.isRouter = e.isRouter || false
                pointNode.isKeepalive = e.isKeepalive || false
                pointNode.type = menuTypeMap[e.type] || 'point'
                pointNode.externalLink = e.externalLink || false
                pointNode.linkTarget = linkTargetMap[e.externalLink] || 'tab'
                pointNode.externalLinkAddress = e.externalLinkAddress || ''
                pointNode.hide = e.hide || false
                pointNode.isHome = e.isHome || false
                pointNode.permissionKey = e.permissionKey || ''
                treeNode.pointList.push(pointNode)
                treeNode.pointsMap.set(pointNode.permissionKey, pointNode)
              })
              if (!treeNode.externalLink || (treeNode.externalLink && treeNode.linkTarget === 'tab')) {
                routeMappingList.push(treeNode)
              }
            }
          }
        }
      }
      forEachTree(menuTree)
      return {
        routeMappingList,
        menuTreeConverted: menuTreeConverted[0]?.children || [],
        menuIdMappingMap
      }
    }
  }
}

菜单适配器参考

https://github.com/pumelotea/happyboot-tiger/blob/master/src/global/framework.js

菜单数据参考

https://github.com/pumelotea/happyboot-tiger/blob/master/src/mock/routerData.js
如果使用上述链接中的数据结构,那么适配器就可以使用默认实现的。

使用菜单适配器

在引擎创建时候作为参数传入即可

js
const happyFramework = createHappyFramework({
  menuAdapter: createMenuAdapter()
})

修改数据加载方法

框架中默认是用Promise模拟的异步数据获取,实际使用是要调用接口的。那么以下代码换成常规的HTTP请求即可。

编辑路由配置文件src/global/router/config.js, 修改createDefaultRouterInterceptor调用中的option参数,其中

js
...
interceptorType: 'before',
  framework      : happyFramework,
  async dataLoader (to, from, next) {
    const result = { rawData: null, message: '' }
    try {
      // 实际开发环境应该从服务端拉取数据
+     const data = await new Promise((resolve, reject) => {
+       setTimeout(() => {
+         resolve({
+           code   : 0,
+           data   : routerData,
+           message: 'success'
+         })
+       }, 1000)
+     })
      result.rawData = data.data
      result.message = data.message
    } catch (e) {
      result.rawData = null
      result.message = e.message
    }
    return result
  },
...

注意

请求返回的数据最终会调用菜单适配器进行数据格式转换,并且自动完成路由的注入。

接口请求token拦截处理

对于http请求token失效的处理定义在该文件中src/global/http/index.js

提示

http请求的返回结构默认约定为:

js
{code: xxx, data: xxx, msg:'err message'}
js
// 返回结果处理
// 自定义约定接口返回{code: xxx, data: xxx, msg:'err message'}
const responseHandle = {
  0: response => {
    return response.data
  },
  401: response => {
    window.$message.error('登录状态已过期,请重新登录!')
    security.signOut()
    router.push('/login')
  },
  default: response => {
    window.$message.error(response.data.message)
    return Promise.reject(response)
  }
}

用户数据对接

登录

用户数据对接指的是用户的登录,常规调用接口登录完成后,调用以下方法进行框架登录。

js
happySecurity.signIn("tokensring",{id:"123",name:"xiaoming"})

方法签名

js
declare type User = any;

signIn(token: string, user: User): void;

Released under the Apache2.0 License.