主应用文档

-
2025-06-30

目录结构

一个基础的 主应用 项目大致是这样的

|-- 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:falsemodalData: {}
 }
 
 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));
 

目录