# 前端手册
# 快速开始
# 环境准备
nodejs 14.X 及以上版本
# 安装依赖
推荐使用 yarn
进行依赖管理
# 全局安装 yarn
npm install -g yarn
# 依赖安装
yarn install
# 如果很慢,设置为国内源
yarn config set registry http://registry.npm.taobao.org/
# 然后再重新 yarn
yarn
# 项目启动
yarn run serve
# 新建模块
页面文件在 src/views
中新建文件夹, 最好采用小驼峰形式的命名
API 请求在 src/api
中按照模块新建文件或文件夹,存放 api 路径以及请求方法
本地开发,路由设置在 src/config/router.config.js
中添加新页面路由, 等后端对接完成后,可以上平台的应用管理-权限管理-菜单管理中自行添加,实现动态路由
# 打包项目
yarn run build
# 请求拦截
请求统一处理, 采用axios
作为请求插件,拦截器在src/utils/request.js
中声明
本拦截器中, 针对失败、各项异常做了处理, 如果有个性化需要,可自由调整修改
# 动态路由
动态路由,是指将本地的配置上云, 在权限模块的菜单管理中来管理前端的所有路由
本地路由菜单配置: src/config/router.config.js
路由拦截器: src/utils/routerUtil.js
路由配置: src/router/index.js
# 前置条件
- 平台建立应用进行管理
- 需要后端对接好对应的菜单接口, 得到接口之后, 可以在
utils/routerUtil.js
里, 修改函数getRouterByUser
中所请求的地址来获取路由数据 - 在
main.js
中的函数initApp
中添加本应用的APP_ID
, 添加方式如下,id的获取,在平台的管理页面中,查看 url 会带有对应的 id - 基座下的应用因为对外提供服务的端口并不一定会有多个,因此所有前端服务将通过统一的 nginx 代理,每一个需要上线的应用,都需要配置本应用自己的
uri
前缀,就是vue.config.js
中的publicPath
, 使用基座所提供的前端框架可以直接在.env
文件中修改VUE_APP_PUBLIC_PATH
,具体的值可以直接使用项目名称,可以和基座的前端负责人提前约定 - 现场环境所使用的
APP_ID
可能和测试环境不一致,那么在开发的时候可以在main.js
中的initApp
函数中增加判断,通过获取 url 地址进行判断,判断为测试环境就设置为测试环境的APP_ID
,其余情况就设置为生产环境的APP_ID
cookieUtil.set('applicationId', APP_ID)
store.commit('SET_APP_INFO', { applicationId: APP_ID })
# 在线菜单配置要点
- 菜单类型分为三种,
目录
、菜单
、按钮
其中只有菜单是对应页面级别, 可以直接访问页面的类型 目录
主要用于归类某一类型功能页面合并在期下面, 也同时支持布局组件NavLayout
,详情可以看下面的布局组件文档菜单
用于展示页面, 组件字段的填写与前端工程结构相挂钩,是在src/views
中根据配置的路径获取页面组件,如组件存放在src/views/result/Success.vue
那要将本页面通过菜单配置, 就在组件字段填写result/Success
即可
# 动态路由配置
将 src/permission.js
中 addRouterAndNext
函数里面确认是否有以下片段,如果没有就加进去,确保使用动态路由
router.addRoutes(store.getters.addRouters)
# 组件使用文档
本前端工程模版中,内置了部分组件,可通过以下文档进行熟悉和使用
# 布局组件 Layout
- NavLayout: 将菜单配置为目录类型, 组件中填写「NavLayout」即可实现侧边目录, 侧边目录的数据来源于本菜单下的所有菜单
- UserLayout: 将菜单配置为目录类型,组件中填写本组件,一般作为登录页面的布局, 已经配置好对应的应用标题, 图标和底部内容, 只需要登录表单即可
- BlankLayout: 将菜单配置为目录类型,组件中填写本组件,空白布局
- PageView: 独立页面,会填入菜单标题,无需配置组件, 菜单类型为「菜单」的独立页面会自动使用本布局组件
# 权限指令 action
//具体代码看 src/core/directives/action.js
//按钮权限校验
<a-button v-has="'user:add'" >添加用户</a-button>
<a-button v-has="['user:edit']">删除用户</a-button>
//角色校验
<a-button v-hasRole="'test'" >test用户</a-button>
<a-button v-hasRole="['test']">test用户</a-button>
# 导入导出Excel组件
本组件主要用 Excel 的导入导出,可控制导入导出的字段等信息
# Excel 导出组件 ExportExcel
用于一站式通用的导出组件
// api.js
export function getZds(data) {
return request({
url: '/your/api/export/getZds',
method: 'get',
params: data,
loadingLoader: 'loadingLoader'
})
}
export function getExport(data) {
return download('/your/api/export', data)
}
// vue
import ExportExcel from '@/components/ExportExcel'
import { getZds, getExport } from '@/api/ExportExcel'
/**
* zds-api 是一个 axios 请求,接口是获取需要导出的字段,由后端提供,参考getZds的写法
* export-api 是一个 axios 请求,接口是导出的接口,由后端提供,参考getExport的写法
* query-params 是页面导出的分页参数和搜索参数
*/
<ExportExcel
:zds-api="getZds"
:export-api="getExport"
:query-params="{ ...pageInfo, ...query }"
/>
# Excel 导入组件 ImportExcel
用于一站式通用的导入组件
// vue
import ImportExcel from '@/components/ImportExcel'
import { downloadTpl, getImportZd, importData } from '@/api/importExport'
/**
* text
* currentType 导入的类型,在@/api/importExport中需要先行定义相关类型的接口map
* zds-api 是一个 axios 请求, 参考@/api/importExport中的写法
* download-template 是一个 axios 请求, 参考@/api/importExport中的写法
* import-api 是一个 axios 请求, 参考@/api/importExport中的写法
* refreshTable 页面列表的刷新方法
*/
<ImportExcel
text="导入按钮的名称"
currentType="type"
:zds-api="getImportZd"
:download-template="downloadTpl"
:import-api="importData"
:refreshTable="() => $refs.table.refresh(true)"
/>
# 全局过滤器 filter
使用 Vue.filter
独立文件存放
请看 src\utils\filter.js
# 请求 loading
管理 loading
api 层,例如
export function listExp(query) {
return axios({
url: '/activiti/exp/list',
method: 'get',
params: query,
loadingLoader: 'listExp', //增加这个字段
})
}
可使用 $wait.is('listExp')
返回值为 true
或 false
,常用于 Spin
组件
也可以增加loadingTips
字段,顶部会自动使用 antd
的 message
的 loading
提示
return axios({
url: '/activiti/exp/list',
method: 'get',
params: query,
loadingTips: '请求列表中', //增加这个字段
})
# 字典调用 dict
this.$store.dispatch('dict/getDict', 'service_hall_type')
service_hall_type
为字典 key
, 调用此方法后,字典会存在 dict store
里
# 工作流 activiti
# 新建流程
产品新建流程。
流程查看在第三方应用认证/流程中心组件/流程设计
页面,点击设计查看。
弹窗流程下一步候选人,没有需要后端加节点
# 关键参数
- type-name: 流程分类
- processInstanceId: 流程id, 关联一个完整流程
- taskId: 审批节点id, 多个审批节点组成一个流程id
- taskName: 审批名字, 根据这个判断流程节点
关键参数的获取,可以在一站式的第三方应用认证中,流程中心组件-流程设计,查看流程分类 通过/api/activiti/workFlow/procList?typeName=XXX 查询对应的流程 id, 审批节点id,审批名称等 发起流程是processParams中填入 procDefKey(调用/api/activiti/flow/list 传入typeName获取)、action(0保存草稿 9提交)、assigneeList参数 发给你们定义的业务后端接口,后端调用WorkFlowUtils工具类去发起
# 组件介绍
- 按钮
按钮一般和弹窗联动,按钮选择action后打开弹窗
- authority: 后端按钮权限字段
传入ApprovalModal的值
- processInstanceId: 流程实例id
- taskId: 一般工作流结合表单使用,此参数一般为表单的值
常用代码及权限表
自己发起-保存时权限按钮一般为编辑,自己发起-保存为审批
<ApprovalButtons :authority="authority">
<a-button slot="cancel" @click="handleCancel">取消</a-button>
<a-button v-if="!disabled" type="primary" slot="edit" :loading="confirmLoading" @click="handleSubmit(0)">保存</a-button>
<a-button v-if="!disabled" type="primary" slot="edit" :loading="confirmLoading" @click="handleSubmit(9)">提交</a-button>
<a-button v-if="!disabled" type="primary" slot="che" :loading="confirmLoading" @click="handleSubmit(3)">撤回</a-button>
<a-button v-if="!disabled" type="primary" slot="tui" :loading="confirmLoading" @click="handleSubmit(4)">退回</a-button>
<a-button v-if="!disabled" type="primary" slot="approval" :loading="confirmLoading" @click="handleSubmit(2)">拒绝</a-button>
<a-button v-if="!disabled" type="primary" slot="approval" :loading="confirmLoading" @click="handleSubmit(1)">同意</a-button>
<a-button v-if="!disabled" type="primary" slot="again" :loading="confirmLoading">重新申请</a-button>
</ApprovalButtons>
<script>
export default {
data() {
return {
disabled: false,
action: 0,
validatedForm: {},
visible: false,
confirmLoading: $wait.is('save'),// confirm 保存的loading, 自己填写
}
},
methods: {
handleSubmit(action) {
this.form.validate((err, value) => {
if (!err) {
this.$refs.approvalModal.startApproval(value, action)
.then(res => {
this.confirmed(res)
})
}
})
}
}
}
</script>
- 弹窗
- confirmed: 点击弹窗确认后回调,会把 validatedForm 合并 processParam 作为第一个参数回调
- processParam: 流程参数
<ApprovalModal
:type-name="'学籍异动'"
:processInstanceId="mdl.processInstanceId"
:taskId="mdl.taskId"
ref="approvalModal"
/>
<script>
export default {
confirmed(data) {
// 带着工作流参数
save(data).then()
}
}
</script>
- 流程步骤图
<ApprovalProcess
:processInstanceId="mdl.processInstanceId"
></ApprovalProcess>
# 3. 列表筛选
todo 待完善,可能不准
查看审批状态筛选有三个状态, 见src/utils/constant.js
中的workflowTodoType
, 一般在查询list的接口,传listType
参数过滤
能看到审批数据情况
- 待办事项: 自己发起-保存 或 别人发起-审批人是自己 或 别人-拒绝/退回
- 已办事项: 自己发起-提交 或 别人发起-自己同意/退回
- 关于我的: 自己发起-流程结束
# 文件传输组件 transfer
上传:见src/components/SUpload
可以使用v-model
或v-decorator
双向绑定数据
下载:见src/utils/download.js
内置封装了一些下载函数,包括通用下载,流下载,blob 下载等,可以直接调用
文件预览:有独立的文件预览系统,通过接口获取预览地址,然后直接打开即可,下面是使用范例
// @/api/common
export function preview(parameter) {
return axios({
url: '/file/file/viewUrl',
method: 'get',
params: parameter
})
}
// preview.vue
import { preview } from '@/api/common'
// 这里的id 是文件 id
preview({ id }).then((res) => {
if (res.code == 0) {
// 获取到预览地址,直接跳转
window.open('' + res.msg)
}
})
# 消息通知 Notice
后端会对接相关接口,这里主要说明前端如何接入
// @/api/notice
export function saveNotice(parameter) {
// return axios.post(parameter.id ? api.editNotice: api.addNotice,parameter);
return axios({
url: '/api/apicenter/api/notice/push',
method: 'post',
data: parameter,
loadingLoader: 'upDataNotice'
})
}
// notice.vue
import { saveNotice } from '@/api/notice'
saveNotice({
userName: 'admin',
title:'测试标题',
content:'内容',
availableTimeStart:'2022-10-10',
availableTimeEnd:'2023-10-10',
userIds:'1',
channelCodes:'MC00001'
}).then((res) => {
if (res.code == 0) {
console.log('新增成功')
}
})
# 前端微服务接入 micro
当需要将一个已有的项目, 作为微服务接入到一站式平台时, 可以借助本方案完成接入
# 本地代码调整
微应用不需要额外安装任何其他依赖即可接入门户主应用。
# 导出相应的生命周期钩子
微应用需要在自己的入口 js (通常就是你配置的 webpack
的 entry
js
) 导出 bootstrap
、mount
、unmount
三个生命周期钩子,以供主应用在适当的时机调用。
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log('react app bootstraped');
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount({ data = {}, state, appInfo, routerBase, actions } = {}) {
ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount(props) {
ReactDOM.unmountComponentAtNode(
props.container ? props.container.querySelector('#root') : document.getElementById('root'),
);
}
/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props) {
console.log('update props', props);
}
# 配置微应用的打包工具
除了代码中暴露出相应的生命周期钩子之外,为了让主应用能正确识别微应用暴露出来的一些信息,微应用的打包工具需要增加如下配置:
# webpack
const packageName = require('./package.json').name;
module.exports = {
output: {
library: `${packageName}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${packageName}`,
},
};
相关配置介绍可以查看 webpack 相关文档 (opens new window)。
# 项目实践
# 微应用
微应用分为有 webpack
构建和无 webpack
构建项目,有 webpack
的微应用(主要是指 Vue、React、Angular)需要做的事情有:
- 新增
public-path.js
文件,用于修改运行时的publicPath
。什么是运行时的 publicPath ? (opens new window); - 微应用建议使用
history
模式的路由,需要设置路由base
,值和它的activeRule
是一样的; - 在入口文件最顶部引入
public-path.js
,修改并导出三个生命周期函数。 - 修改
webpack
打包,允许开发环境跨域和umd
打包。
注意:运行时的 publicPath 和构建时的 publicPath 是不同的,两者不能等价替代。
主要的修改就是以上四个,可能会根据项目的不同情况而改变。例如,你的项目是 index.html
和其他的所有文件分开部署的,说明你们已经将构建时的 publicPath
设置为了完整路径,则不用修改运行时的 publicPath
(第一步操作可省)。
无 webpack
构建的微应用直接将 lifecycles
挂载到 window
上即可。
# React 微应用
以 create react app
生成的 react 16
项目为例,搭配 react-router-dom
5.x。
一、 入口文件 index.js
修改,为了避免根 id #root
与其他的 DOM 冲突,需要限制查找范围。
import './public-path';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
function render(props) {
const { container } = props;
ReactDOM.render(<App />, container ? container.querySelector('#root') : document.querySelector('#root'));
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
export async function bootstrap() {
console.log('[react16] react app bootstraped');
}
export async function mount(props) {
console.log('[react16] props from main framework', props);
render(props);
}
export async function unmount(props) {
const { container } = props;
ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'));
}
这里需要特别注意的是,通过 ReactDOM.render 挂载子应用时,需要保证每次子应用加载都应使用一个新的路由实例。
二、 修改 webpack
配置
安装插件 @rescripts/cli
,当然也可以选择其他的插件,例如 react-app-rewired
。
npm i -D @rescripts/cli
根目录新增 .rescriptsrc.js
:
const { name } = require('./package');
module.exports = {
webpack: (config) => {
config.output.library = `${name}-[name]`;
config.output.libraryTarget = 'umd';
config.output.jsonpFunction = `webpackJsonp_${name}`;
config.output.globalObject = 'window';
return config;
},
devServer: (_) => {
const config = _;
config.headers = {
'Access-Control-Allow-Origin': '*',
};
config.historyApiFallback = true;
config.hot = false;
config.watchContentBase = false;
config.liveReload = false;
return config;
},
};
修改 package.json
:
- "start": "react-scripts start",
+ "start": "rescripts start",
- "build": "react-scripts build",
+ "build": "rescripts build",
- "test": "react-scripts test",
+ "test": "rescripts test",
- "eject": "react-scripts eject"
# Vue 微应用
以 vue-cli 3+
生成的 vue 2.x
项目为例,vue 3
版本等稳定后再补充。
在
src
目录新增public-path.js
:if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }
入口文件
main.js
修改,为了避免根 id#app
与其他的 DOM 冲突,需要限制查找范围。import './public-path'; import Vue from 'vue'; import VueRouter from 'vue-router'; import App from './App.vue'; import routes from './router'; import store from './store'; Vue.config.productionTip = false; let router = null; let instance = null; function render(props = {}) { const { container } = props; router = new VueRouter({ base: window.__POWERED_BY_QIANKUN__ ? '/app-vue/' : '/', mode: 'history', routes, }); instance = new Vue({ router, store, render: (h) => h(App), }).$mount(container ? container.querySelector('#app') : '#app'); } // 独立运行时 if (!window.__POWERED_BY_QIANKUN__) { render(); } export async function bootstrap() { console.log('[vue] vue app bootstraped'); } export async function mount(props) { console.log('[vue] props from main framework', props); render(props); } export async function unmount() { instance.$destroy(); instance.$el.innerHTML = ''; instance = null; router = null; }
打包配置修改(
vue.config.js
):const { name } = require('./package'); module.exports = { devServer: { headers: { 'Access-Control-Allow-Origin': '*', }, }, configureWebpack: { output: { library: `${name}-[name]`, libraryTarget: 'umd', // 把微应用打包成 umd 库格式 jsonpFunction: `webpackJsonp_${name}`, }, }, };
# Angular 微应用
以 Angular-cli 9
生成的 angular 9
项目为例,其他版本的 angular
后续会逐渐补充。
在
src
目录新增public-path.js
文件,内容为:if (window.__POWERED_BY_QIANKUN__) { // eslint-disable-next-line no-undef __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }
设置
history
模式路由的base
,src/app/app-routing.module.ts
文件:+ import { APP_BASE_HREF } from '@angular/common'; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], // @ts-ignore + providers: [{ provide: APP_BASE_HREF, useValue: window.__POWERED_BY_QIANKUN__ ? '/app-angular' : '/' }] })
修改入口文件,
src/main.ts
文件。import './public-path'; import { enableProdMode, NgModuleRef } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } let app: void | NgModuleRef<AppModule>; async function render() { app = await platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); } if (!(window as any).__POWERED_BY_QIANKUN__) { render(); } export async function bootstrap(props: Object) { console.log(props); } export async function mount(props: Object) { render(); } export async function unmount(props: Object) { console.log(props); // @ts-ignore app.destroy(); }
修改
webpack
打包配置先安装
@angular-builders/custom-webpack
插件,注意:angular 9
项目只能安装9.x
版本,angular 10
项目可以安装最新版。npm i @angular-builders/custom-webpack@9.2.0 -D
在根目录增加
custom-webpack.config.js
,内容为:const appName = require('./package.json').name; module.exports = { devServer: { headers: { 'Access-Control-Allow-Origin': '*', }, }, output: { library: `${appName}-[name]`, libraryTarget: 'umd', jsonpFunction: `webpackJsonp_${appName}`, }, };
修改
angular.json
,将[packageName] > architect > build > builder
和[packageName] > architect > serve > builder
的值改为我们安装的插件,将我们的打包配置文件加入到[packageName] > architect > build > options
。- "builder": "@angular-devkit/build-angular:browser", + "builder": "@angular-builders/custom-webpack:browser", "options": { + "customWebpackConfig": { + "path": "./custom-webpack.config.js" + } }
- "builder": "@angular-devkit/build-angular:dev-server", + "builder": "@angular-builders/custom-webpack:dev-server",
解决
zone.js
的问题在父应用引入
zone.js
,需要在import qiankun
之前引入。将微应用的
src/polyfills.ts
里面的引入zone.js
代码删掉。- import 'zone.js/dist/zone';
在微应用的
src/index.html
里面的<head>
标签加上下面内容,微应用独立访问时使用。<!-- 也可以使用其他的CDN/本地的包 --> <script src="https://unpkg.com/zone.js" ignore></script>
修正
ng build
打包报错问题,修改tsconfig.json
文件,参考issues/431 (opens new window)- "target": "es2015", + "target": "es5", + "typeRoots": [ + "node_modules/@types" + ],
为了防止主应用或其他微应用也为
angular
时,<app-root></app-root>
会冲突的问题,建议给<app-root>
加上一个唯一的 id,比如说当前应用名称。src/index.html :
- <app-root></app-root> + <app-root id="angular9"></app-root>
src/app/app.component.ts :
- selector: 'app-root', + selector: '#angular9 app-root',
当然,也可以选择使用 single-spa-angular
插件,参考single-spa-angular 的官网 (opens new window) 和 angular demo (opens new window)
(补充)angular7 项目除了第 4 步以外,其他的步骤和 angular9 一模一样。angular7 修改 webpack
打包配置的步骤如下:
除了安装 angular-builders/custom-webpack
插件的 7.x 版本外,还需要安装 angular-builders/dev-server
。
npm i @angular-builders/custom-webpack@7 -D
npm i @angular-builders/dev-server -D
在根目录增加 custom-webpack.config.js
,内容同上。
修改 angular.json
, [packageName] > architect > build > builder
的修改和 angular9 一样, [packageName] > architect > serve > builder
的修改和 angular9 不同。
- "builder": "@angular-devkit/build-angular:browser",
+ "builder": "@angular-builders/custom-webpack:browser",
"options": {
+ "customWebpackConfig": {
+ "path": "./custom-webpack.config.js"
+ }
}
- "builder": "@angular-devkit/build-angular:dev-server",
+ "builder": "@angular-builders/dev-server:generic",
# 非 webpack 构建的微应用
一些非 webpack
构建的项目,例如 jQuery
项目、jsp
项目,都可以按照这个处理。
接入之前请确保你的项目里的图片、音视频等资源能正常加载,如果这些资源的地址都是完整路径(例如 https://qiankun.umijs.org/logo.png
),则没问题。如果都是相对路径,需要先将这些资源上传到服务器,使用完整路径。
接入非常简单,只需要额外声明一个 script
,用于 export
相对应的 lifecycles
。例如:
声明 entry 入口
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Purehtml Example</title> </head> <body> <div> Purehtml Example </div> </body> + <script src="//yourhost/entry.js" entry></script> </html>
在 entry js 里声明 lifecycles
const render = ($) => { $('#purehtml-container').html('Hello, render with jQuery'); return Promise.resolve(); }; ((global) => { global['purehtml'] = { bootstrap: () => { console.log('purehtml bootstrap'); return Promise.resolve(); }, mount: () => { console.log('purehtml mount'); return render($); }, unmount: () => { console.log('purehtml unmount'); return Promise.resolve(); }, }; })(window);
你也可以直接参照 examples 中 purehtml 部分的代码 (opens new window)
同时,你也需要开启相关资源的 CORS,具体请参照此处
# umi-qiankun 项目
umi-qiankun
的教程请移步 umi 官网 (opens new window) 和 umi-qiankun 的官方 demo (opens new window)