背景
在前端项目持续更新迭代后,整个项目基于业务或者后端服务变更,前端项目也会变得臃肿。繁重的前端工程自然可以参照后端微服务思想,应用于前端。即独立开发交付多个前端应用,组成一个全新整体的项目。

一、lerna初始化

使用lerna对项目公共包进行管理,多个项目可以公用learn管理下的相同的依赖包

  1. lerna 安装
    npm i lerna -g

  2. learn创建项目
    lerna init

  3. 修改lerna.json配置

     {"packages": ["project/*"
    ],
    "workspaces": ["project/*"
    ],
    "version": "independent",
    "command": {"publish": {"message": "chore(release): publish %s"},"bootstrap": {"npmClientArgs": ["--no-package-lock","--legacy-peer-deps"]}
    },
    "npmClient": "npm"
    }
    

    依赖解决–legacy-peer-deps 解决旧版本依赖问题,彻底决定不安装任何旧版本的依赖项。
    安装依赖包出现如下提示时

    npm ERR! code ERESOLVE
    npm ERR! ERESOLVE unable to resolve dependency tree
    npm ERR!
    npm ERR! While resolving: nexttwin@0.1.0
    npm ERR! Found: react@17.0.1
    npm ERR! node_modules/react
    npm ERR!   react@"17.0.1" from the root project
    npm ERR!
    npm ERR! Could not resolve dependency:
    npm ERR! peer react@"^16.8.0" from react-hook-mousetrap@2.0.4
    npm ERR! node_modules/react-hook-mousetrap
    npm ERR!   react-hook-mousetrap@"*" from the root project
    npm ERR!
    
  4. umi创建项目
    创建项目包括,主应用,两个子应用

    npm create umi app-container
    npm create umi module-a
    npm create umi module-b
    

    根据提示选择下一步

    项目创建完如下

    对package.json里依赖包进行按需配置

    "dependencies": {"redux-persist": "^6.0.0","react-dev-inspector": "^1.1.1","@ant-design/icons": "^4.5.0","@ant-design/pro-descriptions": "^1.6.8","@ant-design/pro-form": "^1.18.3","@ant-design/pro-layout": "^6.26.6","@ant-design/pro-table": "^2.30.8","@types/lodash.debounce": "^4.0.6","@umijs/route-utils": "^1.0.36","ahooks": "^2.0.0","antd": "^4.14.0","bizcharts": "^4.1.14","bizcharts-plugin-slider": "^3.0.1","classnames": "^2.2.6","lodash": "^4.17.11","lodash-decorators": "^6.0.0","moment": "^2.25.3","numeral": "^2.0.6","nzh": "^1.0.3","omit.js": "^2.0.2","react": "^17.0.2","react-dom": "^17.0.2","react-helmet-async": "^1.0.4","react-router": "^5.2.1","umi": "^3.5.0","umi-serve": "^1.9.10"},"devDependencies": {"pont-engine": "^1.0.13","loose-envify": "^1.4.0","husky": "^4.3.6","babel-eslint": "^11.0.0-beta.2","@ant-design/pro-cli": "^2.0.2","@types/express": "^4.17.0","@types/history": "^4.7.2","@types/jest": "^27.0.2","@types/lodash": "^4.14.144","@types/react": "^17.0.0","@types/react-dom": "^17.0.0","@types/react-helmet": "^6.1.0","@umijs/fabric": "^2.6.2","@umijs/openapi": "^1.1.14","@umijs/plugin-blocks": "^2.0.5","@umijs/plugin-esbuild": "^1.3.1","@umijs/plugin-openapi": "^1.2.0","@umijs/preset-ant-design-pro": "^1.2.0","@umijs/preset-dumi": "^1.1.7","@umijs/preset-react": "^1.8.24","@umijs/yorkie": "^2.0.3","@umijs/plugin-qiankun": "^2.34.1","carlo": "^0.9.46","cross-env": "^7.0.0","cross-port-killer": "^1.1.1","detect-installer": "^1.0.1","enzyme": "^3.11.0","esbuild-loader": "^2.16.0","eslint": "^8.0.0","express": "^4.17.1","gh-pages": "^3.0.0","jsdom-global": "^3.0.2","lerna": "^4.0.0","lint-staged": "^11.2.3","mockjs": "^1.0.1-beta3","prettier": "^2.3.2","puppeteer-core": "^10.4.0","stylelint": "^13.0.0","typescript": "^4.2.2"},
    
  5. lerna项目根目录下package.json配置
    主要配置依赖包和启动命令,依赖包为project下所有项目功能依赖包,整个项目启动npm run start,项目依赖安装npm run boots

    //安装包里面有多个基础依赖包,可以使用 --hoist 把NPM包安装到根目录,提升性能
    // --parallel 并行执行
    {"name": "root","private": true,"scripts": {"clone:all": "bash ./cli/clone-all.sh","boots": "lerna bootstrap --hoist", "start": "lerna run --parallel  start"},"dependencies": {"redux-persist": "^6.0.0","react-dev-inspector": "^1.1.1","@ant-design/icons": "^4.5.0","@ant-design/pro-descriptions": "^1.6.8","@ant-design/pro-form": "^1.18.3","@ant-design/pro-layout": "^6.26.6","@ant-design/pro-table": "^2.30.8","@types/lodash.debounce": "^4.0.6","@umijs/route-utils": "^1.0.36","ahooks": "^2.0.0","antd": "^4.14.0","bizcharts": "^4.1.14","bizcharts-plugin-slider": "^3.0.1","classnames": "^2.2.6","lodash": "^4.17.11","lodash-decorators": "^6.0.0","moment": "^2.25.3","numeral": "^2.0.6","nzh": "^1.0.3","omit.js": "^2.0.2","react": "^17.0.2","react-dom": "^17.0.2","react-helmet-async": "^1.0.4","react-router": "^5.2.1","umi": "^3.5.0","umi-serve": "^1.9.10"},"devDependencies": {"pont-engine": "^1.0.13","loose-envify": "^1.4.0","@ant-design/pro-cli": "^2.0.2","@types/express": "^4.17.0","@types/history": "^4.7.2","@types/jest": "^27.0.2","@types/lodash": "^4.14.144","@types/react": "^17.0.0","@types/react-dom": "^17.0.0","@types/react-helmet": "^6.1.0","@umijs/fabric": "^2.6.2","@umijs/openapi": "^1.1.14","@umijs/plugin-blocks": "^2.0.5","@umijs/plugin-esbuild": "^1.3.1","@umijs/plugin-openapi": "^1.2.0","@umijs/preset-ant-design-pro": "^1.2.0","@umijs/preset-dumi": "^1.1.7","@umijs/preset-react": "^1.8.24","@umijs/yorkie": "^2.0.3","@umijs/plugin-qiankun": "^2.34.1","carlo": "^0.9.46","cross-env": "^7.0.0","cross-port-killer": "^1.1.1","detect-installer": "^1.0.1","enzyme": "^3.11.0","esbuild-loader": "^2.16.0","eslint": "^8.0.0","express": "^4.17.1","gh-pages": "^3.0.0","jsdom-global": "^3.0.2","lerna": "^4.0.0","lint-staged": "^11.2.3","mockjs": "^1.0.1-beta3","prettier": "^2.3.2","puppeteer-core": "^10.4.0","stylelint": "^13.0.0","typescript": "^4.2.2"}
    }
    

二、主应用配置(基于路由实现)

  1. 主应用config目录下配置qiankun.ts
    用于集成和注册子应用信息

    export default {master: {apps: [{name: 'module-a', // 模块aentry: '//localhost:9002',props: {token: 'XXXXXXX',},},{name: 'module-b', // 模块bentry: '//localhost:9001',props: {token: 'XXXXXXX',},},],// singular: false,jsSandbox: true, // 是否启用 js 沙箱,默认为 falseprefetch: true, // 是否启用 prefetch 特性,默认为 true},
    };
    
  2. config.ts配置微服务

    import microApp from './qiankun';
    export default defineConfig({...,qiankun: microApp,
    })
    
  3. config.ts路由配置

    import routes from './routes';
    export default defineConfig({...,routes: routes,
    })
    
  4. routes.ts 包含子应用对应页面路由
    /module-a/****/****** 这样配置能在主应用里展示子应用对应页面
    /module-a 未配置跳转打开子应用

    export default [{path: '/index',component: '@/layouts/index',name: '欢迎',routes: [{ path: '/index', component: '@/pages/index' },]},{path: '/home',component: '@/pages/home/index',name:'首页',meta: { title: '首页' },},// 子应用配置{name: '子应用A', //须要与下面qiankun配置的name一直path: '/module-a', microApp: 'module-a',routes: [{ path: '/module-a/****/******', name: '子应用A模块'},] // /module-a 第一个为在主应用里的 路径 /****/****** 为子应用路径},//  运营活动{name: '子应用B',path: '/module-b',microApp: 'module-b',},{path: '/exception',routes: [{path: '/exception/404',component: '@/pages/exception/404',meta: { title: '404' },},],},
    ]
    

5.主应用app.js通过配置props传递参数给子应用

export const qiankun = fetch('/config').then((config) => {return {apps: [{name: 'module-a',entry: '//localhost:9002',props: {onClick: () => {console.log(21321312)},name: 'xx',age: 1,token: "module-centertoken"},},{name: 'module-b',entry: '//localhost:9001',props: {onClick: (event) => {console.log(21321312)},name: 'xx',age: 1,token: 'module-activitiestoken'},},],};
});

三、子应用配置

1.子应用config.ts配置

export default defineConfig({...,qiankun: {slave: {},},
})

2.子应用app.ts接收主应用参数

export const qiankun = {// 应用加载之前async bootstrap(props) {console.log('module-a bootstrap', props);},// 应用 render 之前触发async mount(props) {console.log('module-a mount', props);},// 应用卸载之后触发async unmount(props) {console.log('module-a unmount', props);},
};

3.两个子应用路由routes.ts

export default [{path: '/',component: '@/layouts/index',exact: true,routes: [{ path: '/', component: '@/pages/index' },]},{ name: '策略中心',path: '/operation',component: '@/layouts/index',routes: [{ path: '/module-a/****', name: '子应用a页面',component: '@/pages/module-a××××××'},]}
]
export default [{path: '/',component: '@/layouts/index',routes: [{ path: '/', component: '@/pages/index' }]},
]

四、基于MicroApp组件实现方式

  1. layouts页面布局,分为顶部header,左侧菜单,右侧内容
    非必要代码用 … 省略
const Index = (props) => {....return (<div className={styles.normal}>{/* 顶部 */}<Header /><Layout className={styles.body}>{/* 左侧菜单 */}<Sider><Menus/></Sider>{/* 右侧内容页面 */}<Layout><Tabs/></Layout></Layout></div>);
};export default Index;

2.左侧菜单

包含主应用与子应用菜单,点击菜单时页面切换,展示子应用页面时,主应用需要传递不同路由或者页面路径

3.右侧页面Tab切换展示
子应用Tabs.js模块通过MicroApp组件加载

import React, { useState, useEffect, memo } from 'react';
import { MicroApp } from 'umi';
import Home from '@/pages/home';import styles from './index.less';const Tabs = memo((props) => {const { tabPanes = [], activeKey, currentMenu, onTabClick } = props;const [panes, setPanes] = useState([]);const tabClick = (data) => {onTabClick(Number(data.id));}// 微服务加载const loadMicroApp = (appName) => {return <MicroAppname={appName}currentMenu={currentMenu}autoSetLoading={true}/>}// 渲染面板const renderPanes = () => {let renderList = [];let microAppName = [];tabPanes.forEach(item => {if (!item.appName) {// 非微服务renderList.push(item)} else {if (!microAppName.includes(item.appName)) {// 微服务加载唯一性renderList.push(item)}}})setPanes(tabPanes);}useEffect(() => {renderPanes();}, [activeKey])return <div><div className={styles['tabs-nav']}>{tabPanes.map(item => {return  <div className={Number(activeKey) === Number(item.id) ? `${styles['tabs-tab']} ${styles['tabs-tab-active']}` : styles['tabs-tab']} key={item.id}onClick={() => tabClick(item)}                    >{item.name}</div>})}</div><div className={styles['tabs-content']}>{panes.map(item => {return <div key={item.id} className={Number(item.id) === Number(activeKey) ? '' : styles['tabs-pane-active']}>{item.appName && loadMicroApp(item.appName)}{item.code === 'home' && <Home/>}</div>})}</div></div>
})Tabs.propTypes = {tabPanes: PropTypes.array, // 菜单点击切换activeKey: PropTypes.oneOfType([PropTypes.number, PropTypes.string,]), // 当前显示面板currentMenu: PropTypes.object, // 当前菜单onTabClick: PropTypes.func, //tab切换回调
}export default Tabs;
  1. 子应用开发
    1.config目录下路由配置
export default [{path: '/',component: '@/layouts/index',},
]

2.qiankun监听全局数据变化

import {initGlobalState,OnGlobalStateChangeCallback,MicroAppStateActions,
} from 'qiankun';class Actions {// 默认值为空 Actionactions: MicroAppStateActions = initGlobalState({});/*** 设置 actions*/setActions(actions: any) {this.actions = actions;}/*** 监听变化* 获得更新的数据传递给redux或者子应用其他组件*/onGlobalStateChange(cb: OnGlobalStateChangeCallback,fireImmediately?: boolean,) {return this.actions.onGlobalStateChange(cb, fireImmediately);}/*** 映射*/setGlobalState(state: Record<string, any>) {return this.actions.setGlobalState(state);}/*** 卸载监听* @returns */offGlobalStateChange() {return this.actions.offGlobalStateChange();}
}
/*** 暴露出Actions实例*/
const actions = new Actions();
export default actions;

3.app.tsx入口配置
须在bootstrap时就把props传递过来所需要的数据传递给action实例,在子应用mount后执行监听数据变化,再传递给action实例, 在bootstrap时getDvaApp获取不到该对象

import action from '@/global/action';
// import { getDvaApp } from 'umi';export const qiankun = {// 应用加载之前async bootstrap(props: any) {action.setGlobalState({currentMenu: props.currentMenu,});},// 应用 render前触发async mount(props: any) {props.onGlobalStateChange((state: any, prev: any) => {// state: 变更后的状态; prev 变更前的状态action.setGlobalState(state);});},// 应用更新async update(props: any) {//  action.setActions(props);},// 应用卸载后触发async unmount(props: any) {action.offGlobalStateChange();// console.log('module-a  unmount', props)},
}

4.layouts.tsx获得主应用数据
通过action实例的onGlobalStateChange监听数据变化传递给redux

import React, { useEffect } from 'react';
import { connect } from 'umi';
import action from '@/global/action';
import MicroPageLayout from './components/MicroPageLayout';const BasicLayout: React.FC = (props: any) => {const { dispatch } = props;// 通过主任务启动 监听 actionconst addActionListener = () => {action.onGlobalStateChange((state, prev) => {console.log('page', state, prev);dispatch({type: 'global/setCurrentMenu',payload: state})}, true);}useEffect(() => {addActionListener();}, [])return <MicroPageLayout />
};export default connect()(BasicLayout);

5.models/global.tsx顶层数据

export default {namespace: 'global',state: {currentMenu: {}},effects: {*setCurrentMenu({ payload }: any, { put }: any) {yield put({type: 'updateCurrentMenu',payload: payload,});}},reducers: {updateCurrentMenu(state: any, { payload }: any) {return {...state,...payload,};},},
};

6.页面渲染

import React, { memo } from 'react';
import { connect } from 'umi';
import RenderPage from './RenderPage';const PageLayout: React.FC = memo((props: any) => {const { currentMenu = {} } = props;return <RenderPage currentMenu={currentMenu}/>
})export default connect((state: any) => {return {currentMenu: state.global.currentMenu,}
})(PageLayout);

五、子应用独立启动

  1. 启动命令
    package.json 配置环境参数 SELF=self,区分项目启动
"start:self": "cross-env PORT=9002 REACT_APP_ENV=dev SELF=self MOCK=none UMI_ENV=dev umi dev",
  1. config.ts配置环境参数
import { defineConfig } from 'umi';
const { REACT_APP_ENV } = process.env;export default defineConfig({...,// 给页面代码提供环境参数变量define: {'process.env': process.env},
});
  1. 页面获得环境参数进行判断
    例如app.tsx
import action from '@/global/action'; const { SELF } = process.env;export const qiankun = {// 应用加载之前async bootstrap(props: any) {if (SELF === 'self') return;action.setGlobalState({currentMenu: props.currentMenu,});},// 应用 render前触发async mount(props: any) {if (SELF === 'self') return;// 子应用未完全加载完毕 getDvaApp() 拿不到 store实例if (props && props.onGlobalStateChange) {props.onGlobalStateChange((state: any, prev: any) => {// state: 变更后的状态; prev 变更前的状态action.setGlobalState(state);});}},// 应用更新async update(props: any) {//  action.setActions(props);},// 应用卸载后触发async unmount(props: any) {if (SELF === 'self') return;if (props && props.offGlobalStateChange) {props.offGlobalStateChange();}action.offGlobalStateChange();},
}

或者layouts/index.tsx 通过 SELF === ‘self’ 判断页面展示方式

  1. qiankun环境判断
    通过 const { SELF } = process.env; 不能解决子项目随子项目发布后单独访问。可以修改为判断是否为qiankun环境, 如下:
const POWERED_BY_QIANKUN = window.__POWERED_BY_QIANKUN__;
async bootstrap(props: any) {//if (SELF === 'self') return;if (!POWERED_BY_QIANKUN) return;action.setGlobalState({currentMenu: props ? props.currentMenu : {},});
},

六、nginx部署
nginx.conf配置location,三个项目可配置三个server,分别对应项目编译后的dist/index.html文件。端口与开发时配置端口一致。
如下,在windows下本地部署,只作为部分参考

server { # 主应用listen       9000; # 端口与开发环境配置时一直server_name  localhost;location / {root   D:\umi-qiankun-crm\project\app-container\dist;index  index.html index.htm;try_files $uri $uri/ /index.html;add_header Access-Control-Allow-Origin *;  add_header Access-Control-Allow-Credentials true;       add_header Access-Control-Allow-Methods GET,PUT,POST,DELETE,OPTIONS;  add_header Access-Control-Allow-Headers Content-Type,*;  add_header Access-Control-Allow-Headers Authorization;add_header Access-Control-Allow-Headers Cookie;}error_page   500 502 503 504  /50x.html;location = /50x.html {root   html;}
}
server {listen       9002;server_name  localhost;location / {root   D:\umi-qiankun-crm\project\module-a\dist;index  index.html index.htm;try_files $uri $uri/ /index.html;add_header Access-Control-Allow-Origin *;  add_header Access-Control-Allow-Credentials true;       add_header Access-Control-Allow-Methods GET,PUT,POST,DELETE,OPTIONS;  add_header Access-Control-Allow-Headers Content-Type,*;  add_header Access-Control-Allow-Headers Authorization;add_header Access-Control-Allow-Headers Cookie;}error_page   500 502 503 504  /50x.html;location = /50x.html {root   html;}
}
server {listen       9001;server_name  localhost;location / {root   D:\umi-qiankun-crm\project\module-b\dist;index  index.html index.htm;try_files $uri $uri/ /index.html;add_header Access-Control-Allow-Origin *;  add_header Access-Control-Allow-Credentials true;       add_header Access-Control-Allow-Methods GET,PUT,POST,DELETE,OPTIONS;  add_header Access-Control-Allow-Headers Content-Type,*;  add_header Access-Control-Allow-Headers Authorization;add_header Access-Control-Allow-Headers Cookie;}error_page   500 502 503 504  /50x.html;location = /50x.html {root   html;}
}

六、补充

运行 npm run start 启动异常

 Error: spawn D:\FrontendWork\umi-qiankun\node_modules\@umijs\plugin-esbuild\node_modules\esbuild\esbuild.exe ENOENT
node ./D:FrontendWork/umi-qiankun/node_modules/@umijs/plugin-esbuild/node_modules/esbuild/install.js

基于umijs+lerna+qiankun的微前端实现相关推荐

  1. 微前端应用及基于qiankun的微前端实践

    示例代码仓库: yl-qiankun-base:https://gitee.com/dongche/yl-qiankun-base.git yl-qiankun-child-vue:https://g ...

  2. jsp给前端注入值失败_基于 qiankun 的微前端最佳实践(图文并茂) 应用间通信篇...

    引言 大家好~ 本文是基于 qiankun 的微前端最佳实践系列文章之 应用间通信篇,本文将分享在 qiankun 中如何进行应用间通信. 在开始介绍 qiankun 的应用通信之前,我们需要先了解微 ...

  3. 基于 qiankun 的微前端最佳实践(万字长文) - 从 0 到 1 篇

    写在开头 微前端系列文章: 基于 qiankun 的微前端最佳实践(万字长文) - 从 0 到 1 篇 基于 qiankun 的微前端最佳实践(图文并茂) - 应用间通信篇 万字长文+图文并茂+全面解 ...

  4. qiankun 部署微前端-vue2(一)

    自从前后端分离以来,一直都有个困惑,就是随着项目的功能的不断拓展,项目变得不断臃肿,每次打包编译,都要把整个项目编译,非常耗时.如果前端也能像后端一样,在项目搭建初期,有类似微服务的功能,那就好了.现 ...

  5. 基于 qiankun 的微前端应用实践

    业务背景 云音乐广告 Dsp(需求方平台)平台分为合约平台(Vue 框架)和竞价平台(React 框架),因历史原因框架选型未能统一,最近来了新需求,需要同时在两个平台增加一样的模块,因为都是 Dsp ...

  6. 基于 qiankun 的微前端实践

    前言 微前端(Micro-Frontends)是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用. 微前端并不是前端领域的 ...

  7. 基于qiankun的微前端最佳实践 -(同时加载多个微应用)

    介绍 qiankun 在正式介绍 qiankun 之前,我们需要知道,qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单.无痛的构建一个生产可用微前端架构系统. 微 ...

  8. vue项目落地(qiankun.js)微前端服务

    什么是微前端? 网上抄的: 微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将单页面前端应用由单一的单体应用转变为多个小型前端应用聚合为一的应用.各个前端应用还可以独立开发.独立部署 ...

  9. 【微前端】手把手带你从零开始搭建一个qiankun+vue微前端应用

    前言 小伙伴们大家好.上一篇文章中我们分享了qiankun的一些概念和特性,以及基于qiankun实现有一个微前端应用的大概步骤,最后以qiankun加vue2.0为例给出了快速搭建qiankun微前 ...

  10. 两个umijs/max项目使用微前端简单示例

    本人使用umijs/max搭建项目(内置了qiankun插件) 主应用 .umirc.ts中defineConfig qiankun: {master: {apps: [{name: 'app1',e ...

最新文章

  1. 中小企业低成本快速建站的秘诀——模板建站
  2. 两万字的数据库面试题,不看绝对后悔
  3. python多久能学会爬虫-上海多久可以学会python
  4. Day 4:PredictionIO——如何创建一个博客推荐器
  5. [深入浅出WP8.1(Runtime)]Socket编程之UDP协议
  6. malloc与new,free与delete
  7. python gevent async_python的异步初体验(gevent、async、await)
  8. 云数据库RDS基础版的优势及适用场景
  9. 【LeetCode】【HOT 100】2. 两数相加
  10. java 双重检查锁定_Java双重检查锁定
  11. B - Dungeon Master
  12. python standardscaler_定制便携python工具箱
  13. Rust : ? 操作符(待续)
  14. 像计算机科学家一样思考(C++)
  15. 大明湖畔的正则表达式,你还记得么?
  16. java格林时间转换_Java 格林威治时间字符串转本地Date对象
  17. 小米组织架构调整成立技术委员会 强化技术引领 增强互联网成色
  18. linux jpg图片转base64,html中的图片直接使用base64编码后的字符串代替
  19. Web IDE优势在哪?详解Web版数据库管理工具SQL Studio
  20. 百度阅读赚钱秘法 人人皆可操作

热门文章

  1. 清明上河图对计算机技术的启发,走进清明上河图沉浸式数字音画项目在京发布-微软亚洲研究院.PDF...
  2. Windows源码编译运行pgAdmin4
  3. 极光笔记|极光推送在APICloud平台的使用教程
  4. keil编程和c语言,C语言编程及keil软件使用.ppt
  5. visio安装问题总结
  6. 数值计算之 插值法(2)多项式插值——牛顿插值法
  7. 常用电子面单接口API demo下载
  8. STK之Commu模块之仿真同步卫星与地面站通信
  9. java进阶(1)之Euraka和Feign的结合使用
  10. gridview隐藏列的方法