目录结构※
一个基础的 主应用 项目大致是这样的
|-- package.json
|-- .env
|-- craco.config.js
|-- yarn.lock
|-- node_modules.zip
|-- build
|-- public
|-- src
|-- api
|-- assets
|-- components
|-- pages
|-- router
|-- store
|-- styles
|-- utils
|-- index.js
|-- index.less
根目录※
package.json
包含插件和插件集※
.env
环境变量
比如
PORT=9999
WDS_SOCKET_PORT=9999
craco.config.js
配置文件,包含webpack功能与插件的配置
yarn.lock
依赖包版本锁定文件
node_modules.zip
项目的依赖压缩包,解压即可使用
build目录※
执行 npm run build 后,产物默认会存放在这里
public目录※
此目录下所有文件会被 copy 到输出路径。
/src 目录※
api目录
与后端接口交互文件夹
assets目录
静态文件存放文件夹
components目录
公共组件存放文件夹
pages目录
业务代码存放文件夹
router目录
工程路由定义文件夹
store目录
工程 全局状态管理 文件夹
styles目录
工程 全局样式 文件夹
utils目录
工程工具函数文件夹
index.js
工程 入口文件
index.less
工程整体 样式文件
项目基础环境要求※
NODE :12.22.10及以上
NPM :最新的即可
主应用配置※
安装 qiankun※
$ yarn add qiankun 或者 npm i qiankun -S
在主应用中注册微应用※
在主应用的入口文件中引入 qiankun 微应用框架加载微应用
import { registerMicroApps, start } from "qiankun"; // 引入 qiankun 框架方法
const apps = [
{
name: "developManage", // 微应用 package name
entry: "http://27.196.10.156:8090/", // 微应用 要跨域 fetch 微应用部署地址
container: "#pub_micao_wrap", // 微应用挂载节点
activeRule: "/base/developManage" // 微应用激活路径
}
];
registerMicroApps(apps); // 注册微应用
start({
singular: false, // 应用多开
}); // 开启微应用当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。
如果微应用不是直接跟路由关联的时候,你也可以选择手动加载微应用的方式:
import { loadMicroApp } from 'qiankun'; // 引入手动加载方法
loadMicroApp({
name: 'developManage', // 微应用 package name
entry: 'http://27.196.10.156:8090/', // 微应用 要跨域 fetch 微应用部署地址
container: '#pub_micao_wrap', // 微应用挂载节点
});微应用配置※
微应用不需要额外安装任何其他依赖即可接入 qiankun 主应用。
导出相应的生命周期钩子※
微应用需要在自己的入口 js (通常就是你配置的 webpack 的 entry js) 导出 bootstrap、mount、unmount 三个生命周期钩子,以供主应用在适当的时机调用。
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap(props) {
console.log('react app bootstraped');
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
render(props); // 渲染方法
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount(props) {
ReactDOM.unmountComponentAtNode(document.getElementById("microRoot")); // 卸载应用
}
/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props){
console.log('update props', props); // 获取更新的值, 在props中存在 state 和 methods
}配置微应用的打包工具※
除了代码中暴露出相应的生命周期钩子之外,为了让主应用能正确识别微应用暴露出来的一些信息,微应用的打包工具需要增加如下配置
Config-overrides.js webpack:
const { override, fixBabelImports, addLessLoader, overrideDevServer, watchAll, addWebpackPlugin } = require('customize-cra')
const packageName = require('./package.json').name;
module.exports = override(
(config) => {
config.output.library = `${packageName}`
config.output.libraryTarget = 'umd' // 打包模式为 umd
config.output.publicPath = '/';
config.output.jsonpFunction = `webpackJsonp_${packageName}`
return config
},
addLessLoader({
lessOptions: {
javascriptEnabled: true,
modifyVars: { '@primary-color': '#3366ff' }
},
}),
'devServer': overrideDevServer(
(config) => {
config.headers = config.headers || {}
config.port = 9999;
config.headers['Access-Control-Allow-Origin'] = '*' // 设置 devServer 跨域请求
return config
},
watchAll()
)主子应用之间的通信※
因为主应用和微应用是运行在同一个页面框架内的,所以主应用和微应用之间的通信采取的是 sessionStorage 方式将要通信的信息存储到 session 里,随着会话的结束,通信的信息相应删除
主应用手动加载微应用update方法传值
主应用配置手动加载※
import { loadMicroApp } from "qiankun"; // 引入手动加载方法
class Index extends React.Component {
loadApp = null; // this 上挂载变量 loadApp
gotoDetails = (item) => { // 手动加载触发事件
if (!this.loadApp) { // 判断是不是已经加载了应用
this.loadApp = loadMicroApp({
name: "developManage",
entry: "http://27.196.10.156:8090/index.html?v=1.0.6", // 子应用 要跨域 fetch
container: "#agencyWrap_app",
});
this.loadApp.mountPromise.then(() => {
this.loadApp.update({ // 触发微应用 update 方法 传值给微应用
todoList: item,
});
});
this.loadApp.unmountPromise.then(() => {
// 微应用关闭触发方法, 将变量 loadApp 赋值为 null
this.loadApp = null;
});
}
}
render(){ // 组件渲染方法
return (
<div>
<div id="agencyWrap_app"></div> <!-- 手动挂载的节点 -->
</div>
)
}
}微应用配置方法接收并打开弹窗※
import PubSub from 'pubsub-js' // 引入 发布定阅 插件
class App extends React.Component {
state = {
flowModalShow:false,
modalData: {}
}
closeFlowModalShow = () => {
this.setState({ flowModalShow: false }) // 关闭弹窗
this.state.modalData.unmountSelf() // 卸载自身应用
}
componentDidMount() {
// 订阅方法 获取 传递过来的对象
PubSub.subscribe('modalData', (msg, data) => {
if (data) {
console.log(msg) // 这里将会输出对应设置的 pubsubID
console.log(data) // 这里将会输出对应设置的参数
this.setState({ modalData: data }) // 传值给 modalData
this.setState({ flowModalShow: true }) // 打开弹窗
}
})
}
}
function renderPatch(props) {
// 触发 modalData 广播方法 传递 props 对象
PubSub.publish('modalData', props)
}
// 增加 update 钩子以便主应用手动更新微应用
export async function update(props) {
renderPatch(props);
}启动应用※
// 解压 node_module.zip
// 执行 npm start
// 访问 http://localhost:9000
// 打包 npm run build 打包代码 -> 部署上线 ( 参考部署文档 )
项目开发※
在 src/index.js 入口文件中这样操作
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route, Switch, BrowserRouter } from "react-router-dom";
import { ConfigProvider } from "antd";
import zhCN from "antd/lib/locale/zh_CN";
import { registerMicroApps, start } from "qiankun";
import { Provider } from "react-redux"
const apps = [ // 注册微应用的 列表
{
name: "developManage",
entry: "http://27.196.10.156:8090/", // 子应用 要跨域 fetch
container: "#pub_micao_wrap",
activeRule: "/base/developManage"
},
{
name: "auditlog",
entry: "http://27.196.10.156:8091/", // 子应用 要跨域 fetch
container: "#pub_micao_wrap",
activeRule: "/base/auditlog",
},
{
name: "standardResource",
entry: "http://27.196.10.156:8106/", // 子应用 要跨域 fetch
container: "#pub_micao_wrap_bz",
activeRule: "/base/stanRes",
}
];
registerMicroApps(apps); // 注册应用
class App extends React.Component {
render() {
return (
<ConfigProvider locale={zhCN}>
<div className="AppBase" ref={this.appRef}>
<Switch>
<BrowserRouter>
<Provider store={store}>
<Route path="/" exact component={Stage} />
<Route path="/login" component={Login}></Route>
<Route path="/base" component={Wrap}></Route>
</Provider>
</BrowserRouter>
</Switch>
</div>
</ConfigProvider>
)
}
}
start({
singular: false,
}); // 开启应用
ReactDOM.render(
<Router>
<App />
</Router>,
document.getElementById("rootBase")
);在 src/api/api.js 文件里修改与后端交互的 网关地址 代码如下
import axios from "axios";
import { message } from 'antd'
const publicIp = "http://27.196.10.180:18080" // 项目的网关地址更改为 业务网关地址
let hide = null
const instance = axios.create({ //创建axios实例,在这里可以设置请求的默认配置
timeout: 180000, // 设置超时时间10s
baseURL: publicIp //根据自己配置的反向代理去设置不同环境的baeUrl
})
// 文档中的统一设置post请求头。下面会说到post请求的几种'Content-Type'
instance.defaults.headers.post['Content-Type'] = 'application/json'
/** 添加请求拦截器 **/
instance.interceptors.request.use(config => {
config.headers['token'] = sessionStorage.getItem('access_token') || ''
if(config.loading){
hide = message.loading({ content: 'Loading...', duration: 0 });
}
if(config.type === "fileUpload"){
config.headers['Content-Type'] = 'multipart/form-data'
}
if (config.type === "download") {
config["responseType"] = "blob";
}
// 在这里:可以根据业务需求可以在发送请求之前做些什么:例如我这个是导出文件的接口,因为返回的是二进制流,所以需要设置请求响应类型为blob,就可以在此处设置。
/*if (config.url.includes('pur/contract/export')) {
config['responseType'] = 'blob'
}
// 我这里是文件上传,发送的是二进制流,所以需要设置请求头的'Content-Type'
if (config.url.includes('pur/contract/upload')) {
config.headers['Content-Type'] = 'multipart/form-data'
}*/
return config
}, error => {
// 对请求错误做些什么
return Promise.reject(error)
})
/** 添加响应拦截器 **/
instance.interceptors.response.use(response => {
hide && hide()
if (response.status === 200) { // 响应结果里的statusText: ok是我与后台的约定,大家可以根据实际情况去做对应的判断
if(response.data.code == "200"){
return Promise.resolve(response.data)
}else{
message.error(response.data.message)
if(response.data.code == "401"){
setTimeout(() => {
sessionStorage.clear()
window.location.href = '/login'
}, 3000);
}
return Promise.reject(null)
}
}else{
message.error('响应超时')
return Promise.reject(response.data.message)
}
}, error => {
hide && hide()
if (error.response) {
// 根据请求失败的http状态码去给用户相应的提示
let tips = error.response.status in httpCode ? httpCode[error.response.status] : error.response.data.message
message.error(tips)
if (error.response.status === 401) { // token或者登陆失效情况下跳转到登录页面,根据实际情况,在这里可以根据不同的响应错误结果,做对应的事。这里我以401判断为例
//针对框架跳转到登陆页面
this.props.history.push("/login");
}
return Promise.reject(error)
} else {
message.error('请求超时, 请刷新重试')
return Promise.reject('请求超时, 请刷新重试')
}
})
/* 统一封装get请求 */
export const get = (url, params, config = {}) => {
return new Promise((resolve, reject) => {
instance({
method: 'get',
url,
params,
...config
}).then(response => {
resolve(response)
}).catch(error => {
reject(error)
})
})
}
/* 统一封装post请求 */
export const post = (url, data, config = {}) => {
return new Promise((resolve, reject) => {
instance({
method: 'post',
url,
data,
...config
}).then(response => {
resolve(response)
}).catch(error => {
reject(error)
})
})
}主应用的所有业务页面写在了 src/components/common/wrap/index.js 文件当中,代码如下
import React from "react";
import { Layout, message } from "antd";
import DrawerMenu from "../DrawerMenu";
import Header from "../Header";
import { Route, withRouter, Switch } from "react-router-dom";
import { Scrollbars } from "react-custom-scrollbars";
import Home from "../../../pages/backstage";
import projectCenter from "../../../pages/backstage/projectCenter";
import standardProcess from "../../../pages/backstage/standardProcess";
import componentCenter from "../../../pages/backstage/componentCenter";
import modelBase from "../../../pages/backstage/modelBase";
import templateLibrary from "../../../pages/backstage/templateLibrary";
import interfaceLibrary from "../../../pages/backstage/interfaceLibrary";
import userManagement from "../../../pages/backstage/userManagement";
import organizationManagement from "../../../pages/backstage/organizationManagement";
import resourcesManagement from "../../../pages/backstage/resourcesManagement";
import roleManagement from "../../../pages/backstage/roleManagement";
import menuManagement from "../../../pages/backstage/menuManagement";
import contentManagement from "../../../pages/backstage/contentManagement";
import resouseCenter from "../../../pages/backstage/resouseCenter";
import AgencyList from "../../../pages/backstage/AgencyList";
import redirectLogin from "../redirect";
import goHome from "../goHome";
import { connect } from "react-redux";
import { CloseOutlined, HomeOutlined } from "@ant-design/icons";
import "./index.less";
const { Content, Footer } = Layout;
class index extends React.Component {
state = {
title: "项目中心",
collapsed: false,
};
componentDidMount() {
const { history } = this.props;
if (!sessionStorage.getItem("access_token")) {
message.error("登录失效,请重新登录!").then(() => {
history.replace("/base/redirectLogin");
});
}
}
removeTab = (item, index) => {
const { removeTab, selectedKeys, tabList, history, changeSelectedkey } =
this.props;
removeTab(item);
if (selectedKeys === item.key && tabList.length !== 1) {
if (tabList[index - 1]) {
history.push(tabList[index - 1].path, { key: tabList[index - 1].key });
changeSelectedkey(tabList[index - 1].key);
} else {
history.push(tabList[index + 1].path, { key: tabList[index + 1].key });
changeSelectedkey(tabList[index + 1].key);
}
} else if (selectedKeys !== item.key) {
} else {
this.gotoHome();
}
};
gotoPath = (item) => {
const { history, changeSelectedkey } = this.props;
this.setState({ title: item.title });
changeSelectedkey(item.key);
history.push(item.path, { key: item.key });
};
gotoHome = () => {
const { history, changeSelectedkey } = this.props;
changeSelectedkey("home");
history.push("/base", { key: "home" });
};
render() {
const { collapsed, title } = this.state;
const { selectedKeys } = this.props;
return (
<Layout className="app">
<Header />
<Layout className="drawer-warp" hasSider>
<DrawerMenu
collapsed={collapsed}
onMenuSelect={(pathname, title) => {
this.setState({ title });
}}
/>
<Layout>
<div className="app-tab">
<Scrollbars style={{ width: "100%", height: "100%" }} autoHide>
<a
onClick={() => this.gotoHome()}
className={
selectedKeys === "home" ? "affhome active" : "affhome"
}
>
<HomeOutlined
style={{ float: "left", margin: "9px 5px 0 0" }}
/>
<span>首页</span>
</a>
{this.props.tabList.map((item, index) => {
return (
<a
key={index}
className={selectedKeys === item.key ? "active" : null}
>
<span onClick={() => this.gotoPath(item)}>
{item.title}
</span>
<CloseOutlined
className="close"
onClick={() => this.removeTab(item, index)}
/>
</a>
);
})}
</Scrollbars>
</div>
<Content
className={
selectedKeys === "home"
? "app-content app-home-content"
: "app-content"
}
>
<Switch>
<Route exact path="/base" component={Home} />
<Route path="/base/projectCenter" component={projectCenter} />
<Route
path="/base/standardProcess"
component={standardProcess}
/>
<Route
path="/base/componentCenter"
component={componentCenter}
/>
<Route path="/base/yunManage" component={modelBase} />
<Route
path="/base/templateLibrary"
component={templateLibrary}
/>
<Route
path="/base/interfaceLibrary"
component={interfaceLibrary}
/>
<Route path="/base/userManagement" component={userManagement} />
<Route
path="/base/organizationManagement"
component={organizationManagement}
/>
<Route
path="/base/resourcesManagement"
component={resourcesManagement}
/>
<Route path="/base/roleManagement" component={roleManagement} />
<Route path="/base/menuManagement" component={menuManagement} />
<Route
path="/base/contentManagement"
component={contentManagement}
/>
<Route path="/base/todo" component={AgencyList} />
<Route path="/base/resouseCenter" component={resouseCenter} />
<Route path="/base/redirectLogin" component={redirectLogin} />
<Route path="/base/goHome" component={goHome} />
{/* <Route path="/base/*" component={Not}></Route> */}
</Switch>
<div id="pub_micao_wrap"></div>
<div id="pub_micao_wrap_bz"></div>
<div id="pub_micao_wrap_dat"></div>
</Content>
<Footer className="app-footer">
版权所有 © 2022 STATE GRID INFO & TELECOM GROUP
</Footer>
</Layout>
</Layout>
</Layout>
);
}
}
export default connect(
(state) => {
return {
tabList: state.tabReducers,
selectedKeys: state.selectedKeyReducers,
};
},
(dispatch) => {
return {
addTabList: (data) => dispatch({ type: "ADDTAB", data }),
removeTab: (data) => dispatch({ type: "MINUSTAB", data }),
changeSelectedkey: (data) => dispatch({ type: "CHANGE", data }),
};
}
)(withRouter(index));