先来张图片压压惊
image.png
在线demo:wzs.bengdada.com/
单独访问在线子应用:
一.导读
1.什么是微前端
- 微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。
- 微前端架构具备以下几个核心价值:
技术栈无关: 主框架不限制接入应用的技术栈,微应用具备完全自主权独立开发、独立部署: 微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新增量升级:在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略独立运行时: 每个微应用之间状态隔离,运行时状态不共享 - 微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。
2.qiankun是什么
- qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。
官网: https://qiankun.umijs.org/zh - qiankun特性
基于 single-spa 封装,提供了更加开箱即用的 API。
技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
样式隔离,确保微应用之间样式互相不干扰。
JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
⚡️ 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。 - 了解完理论基础,让我们动手实践一下···
二.建立项目
如图: 我建立了一个主应用和三个子应用
主应用 main vue3搭建
"vue": "^3.0.0",
子应用 micro-react react18搭建
"react": "^18.1.0",
子应用 micro-vue2 vue2搭建
"vue": "^2.6.11",
子应用 micro-vue3 vue3搭建
"vue": "^3.0.0",
注意 : vue3技术选型我使用的是vue3 + webpack ,vite目前对于qiankun还不是太友好 ,硬要搞vite代价会很大,后续等官网优化后我们在去使用vite
由于搭建项目太简单我就不说明了 ~ ovo
三.主应用
注意: qiankun 需要一个主应用 来注入所有的子应用
先安装乾坤的依赖包
yarn add qiankun # 或者 npm i qiankun -S
目前乾坤是2.0版本 安装后package.json 是2.72版本
在安装 element-plus 把项目的布局简单做一下
npm install element-plus --save
注意: vue3 安装element-plus, vue2安装element-ui
src下新建micro-app.js 用于存放所有子应用
const microApps = [ { name: 'micro-react', //应用名 项目名最好也是这个 entry: '//localhost:20000', //默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)内部用的fetch activeRule: '/react', // 激活的路径 container: '#micro-react', // 容器名 props: {}, //父子应用通信 }, { name: 'micro-vue2', entry: '//localhost:30000', activeRule: '/vue2', container: '#micro-vue2', props: {}, }, { name: 'micro-vue3', entry: '//localhost:40000', activeRule: '/vue3', container: '#micro-vue3', props: {}, }, ]; export default microApps;
新建vue.config.js
module.exports = { devServer: { port: 8000, headers: { // 重点1: 允许跨域访问子应用页面 'Access-Control-Allow-Origin': '*', }, }, };
Main页面
import { createApp } from 'vue' import App from './App.vue' import router from './router' import store from './store' // createApp(App).use(store).use(router).mount('#app') //-----------------------上面是原先的,下面是新增的----------------------------- import ElementPlus from 'element-plus'; //element-plus import 'element-plus/dist/index.css'; //element-plus import { registerMicroApps, start } from 'qiankun'; import microApps from './micro-app'; let app = createApp(App); app.use(store); app.use(router); app.use(ElementPlus); app.mount('#app'); registerMicroApps(microApps, { //还有一些生命周期 如果需要可以根据官网文档按需加入 beforeMount(app) { console.log('挂载前', app); }, afterMount(app) { console.log('卸载后', app); }, }); start({ prefetch: false, //取消预加载 sandbox: { experimentalStyleIsolation: true }, //沙盒模式 });
进入App页面简单调下布局
<template> <el-menu :router="true" mode="horizontal"> <!-- 子应用的跳转路径 --> <el-menu-item index="/">主应用 main</el-menu-item> <el-menu-item index="/react">子应用 react18</el-menu-item> <el-menu-item index="/vue2">子应用 vue2</el-menu-item> <el-menu-item index="/vue3">子应用 vue3</el-menu-item> <el-menu-item index="/about">{{ $store.state.GlobalData }}</el-menu-item> </el-menu> <router-view /> <!-- 子应用的容器 --> <div id="micro-react"></div> <div id="micro-vue2"></div> <div id="micro-vue3"></div> </template> <script> export default { name: 'App', setup() { return {}; }, }; </script> <style lang="less"> html, body, #app { width: 100%; height: 100%; margin: 0; } #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; color: #2c3e50; } </style>
需要注意: app里的容器名和跳转路径都不是随便起的 需要和micro-app.js 定义好的子应用一一对应
image.png
到此主应用搭建完毕~~~ovo
四.子应用
1.react
安装npm install react-app-rewired 重写默认的react配置文件
npm install react-app-rewired --save
修改package.json,原本的react-script 改为react-app-rewired
"scripts": { "start": "react-app-rewired start", "build": "react-app-rewired build", "test": "react-app-rewired test", "eject": "react-app-rewired eject" },
安装npm i react-router-dom 我安装的是最新版本 "react-router-dom": "^6.3.0"
npm i react-router-dom --save
根目录下新建.env文件
PORT=20000 # 防止热更新出错 WDS_SOCKET_PORT=20000
src下新建public-path.js (用于修改运行时的 publicPath)
//判断是否是qiankun加载 if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }
src下新建 config-overrides.js
const { name } = require('./package'); module.exports = { webpack: config => { config.output.library = `${name}-[name]`; config.output.libraryTarget = 'umd'; 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; }, };
进入src下index.js
// import logo from './logo.svg'; // import './App.css'; // function App() { // return ( // <div className="App"> // <header className="App-header"> // <img src={logo} className="App-logo" alt="logo" /> // <p> // Edit <code>src/App.js</code> and save to reload. // </p> // <a // className="App-link" // href="https://reactjs.org" // target="_blank" // rel="noopener noreferrer" // > // Learn React // </a> // </header> // </div> // ); // } // export default App; // ------------------------上面原先的,下面最新的------------------------------------ import logo from './logo.svg'; import './App.css'; import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom'; function App() { return ( <> {/* basename 判断如果是qiankun加载 basename为react 相当于加个标识*/} <Router basename={window.__POWERED_BY_QIANKUN__ ? '/react' : '/'}> {/* */} <Link to="/">首页</Link> <Link to="/about">关于页面</Link> <Routes> <Route path="/" element={<Home />}></Route> <Route path="/about" element={<About />}></Route> </Routes> </Router> </> ); } function About() { return <div>about</div>; } function Home() { return ( <div> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer"> Learn React </a> </header> </div> ); } export default App;
2.vue2
src下新建public-path.js 用于修改运行时的 publicPath
// eslint-disable-next-line no-undef if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }
在main页面 引入public-path.js文件
// import Vue from 'vue'; // import App from './App.vue'; // import router from './router'; // Vue.config.productionTip = false // new Vue({ // router, // render: h => h(App) // }).$mount('#app') // ·················上面原先的 下面新增的····················· import './public-path'; import Vue from 'vue'; import App from './App.vue'; import router from './router'; // Vue.config.productionTip = false let instance = null; function render(props = {}) { const { container } = props; instance = new Vue({ router, render: (h) => h(App), }).$mount(container ? container.querySelector('#app') : '#app'); } // 如何独立运行微应用? if (!window.__POWERED_BY_QIANKUN__) { render(); } export async function bootstrap(props) { // 启动 } export async function mount(props) { // 挂载 onGlobalStateChange 可通过这个属性来进行父子应用通信 发布订阅机制 render(props); } export async function unmount(props) { // 卸载 instance.$destroy(); }
新增vue.config.js文件
const { name } = require('./package'); module.exports = { devServer: { port: 30000, headers: { 'Access-Control-Allow-Origin': '*', //开发时增加跨域 表示所有人都可以访问我的服务器 }, }, configureWebpack: { output: { library: `${name}-[name]`, libraryTarget: 'umd', // 把子应用打包成 umd 库格式 jsonpFunction: `webpackJsonp_${name}`, }, }, };
router.js文件
const router = new VueRouter({ mode: 'history', // base: process.env.BASE_URL, base: '/vue2', routes, });
3.vue3
src下新建public-path.js 用于修改运行时的 publicPath
// eslint-disable-next-line no-undef if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }
在main页面 引入public-path.js文件
import './public-path'; // 注意需要引入public-path import { createApp } from 'vue'; import App from './App.vue'; import router from './router'; import store from './store'; let instance = null; function render({ container } = {}) { instance = createApp(App); instance.use(router); instance.use(store); instance.mount(container ? container.querySelector('#app') : '#app'); } // 如何独立运行微应用? if (!window.__POWERED_BY_QIANKUN__) { render(); } export async function bootstrap(props) { // 启动 } export async function mount(props) { // 挂载 render(props); } export async function unmount(props) { // 卸载 instance.unmount(); instance = null; }
新增vue.config.js文件
const { name } = require('./package'); module.exports = { devServer: { port: 40000, headers: { 'Access-Control-Allow-Origin': '*', //开发时增加跨域 表示所有人都可以访问我的服务器 }, }, configureWebpack: { output: { library: `${name}-[name]`, libraryTarget: 'umd', // 把子应用打包成 umd 库格式 jsonpFunction: `webpackJsonp_${name}`, }, }, };
到这里项目搭建完毕,基础跳转没有问题 ,可以在主应用和子应用跳转
bug:主应用和子应用使用不同版本的vue后路由切换报错 ?bug:主应用样式与子应用样式冲突 ?需求:父子组件传参如何实现 ?需求:如何部署 ?
别担心 下面我一一解答
5.bug
[Bug]主应用和子应用使用不同版本的vue后路由切换报错
问题的原因 : vue-router 3.x与vue-router 4.x设置的history.state的数据结构不同
低版本的 vue-router 在 pushState 的时候,会覆盖丢失主路由的 history.state,导致主路由跳转异常解决办法 : 主应用监听router.beforEach 手动修改history.state数据结构
import _ from "lodash" router.beforeEach((to, from, next) => { if (_.isEmpty(history.state.current)) { _.assign(history.state, { current: from.fullPath }); } next(); });
[Bug]主应用样式与子应用样式冲突
可以通过给css样式名加前缀来实现隔离
https://blog.csdn.net/zjscy666/article/details/107864891
https://blog.csdn.net/m0_54854484/article/details/123442168
6.需求
[需求] 父子组件传参如何实现
qiankun通过initGlobalState, onGlobalStateChange, setGlobalState实现主应用的全局状态管理,然后默认会通过props将通信方法传递给子应用。先看下官方的示例用法:
主应用
// main/src/main.js import { initGlobalState } from 'qiankun'; // 初始化 state const initialState = { user: {} // 用户信息 }; const actions = initGlobalState(initialState); actions.onGlobalStateChange((state, prev) => { // state: 变更后的状态; prev 变更前的状态 console.log(state, prev); }); actions.setGlobalState(state); actions.offGlobalStateChange();
子应用
// 从生命周期 mount 中获取通信方法,props默认会有onGlobalStateChange和setGlobalState两个api export function mount(props) { props.onGlobalStateChange((state, prev) => { // state: 变更后的状态; prev 变更前的状态 console.log(state, prev); }); props.setGlobalState(state); }
这两段代码不难理解,父子应用通过onGlobalStateChange这个方法进行通信,这其实是一个发布-订阅的设计模式。
ok,官方的示例用法很简单也完全够用,纯JavaScript的语法,不涉及任何的vue或react的东西,开发者可自由定制。
如果我们直接使用官方的这个示例,那么数据会比较松散且调用复杂,所有子应用都得声明onGlobalStateChange对状态进行监听,再通过setGlobalState进行更新数据。
因此,我们很有必要对数据状态做进一步的封装设计
主应用src下新建actions.js
//src/actions.js // 父子应用通信 import { initGlobalState } from 'qiankun'; import store from './store'; const state = { //这里写初始化数据 name: 'wang', age: 123, count: 0, }; const actions = initGlobalState(state); actions.onGlobalStateChange((state, prev) => { console.log('主应用变更前:', state); console.log('主应用变更后:', prev); store.commit('setGlobalData', state); }); store.commit('setGlobalData', state); export default actions;
将初始化的数据存到vuex中 如果数据变更了 在将变更后的数据存到vuex
主应用main store文件夹下index.js中
//store/index.js import { createStore } from 'vuex'; export default createStore({ state: { GlobalData: {}, }, mutations: { setGlobalData(state, value) { state.GlobalData = value; }, }, actions: {}, modules: {}, });
最后在main.js 中导入
//main.js import './actions.js'
子应用 (vue3)
核心:通过将主应用的onGlobalStateChange,setGlobalState方法挂载到全局就可以使用了
import './public-path'; // 注意需要引入public-path import { createApp } from 'vue'; import App from './App.vue'; import router from './router'; import store from './store'; let instance = null; //核心 function render(props) { const { container, onGlobalStateChange, setGlobalState } = props; console.log(props); instance = createApp(App); instance.config.globalProperties.$onGlobalStateChange = onGlobalStateChange; instance.config.globalProperties.$setGlobalState = setGlobalState; instance.use(router); instance.use(store); instance.mount(container ? container.querySelector('#app') : '#app'); } // 如何独立运行微应用? if (!window.__POWERED_BY_QIANKUN__) { render(); } export async function bootstrap(props) { // 启动 } export async function mount(props) { // 挂载 render(props); } export async function unmount(props) { // 卸载 instance.unmount(); instance = null; }
使用
主应用
<template> <div> <h1>主应用/vue3子应用 的全局数据</h1> <div>姓名 : {{ $store.state.GlobalData.name }}</div> <div>年龄 : {{ $store.state.GlobalData.age }}</div> <div>数量 : {{ $store.state.GlobalData.count }}</div> <el-button type="primary" @click="revampData">修改全局数据</el-button> </div> </template> <script setup> import actions from '../actions'; const revampData = () => { actions.setGlobalState({ name: '主应用' }); }; </script>
子应用(vue3)
<template> <div>我是vue3项目</div> <button @click="revampData">修改全局数据</button> </template> <script> import { getCurrentInstance } from 'vue'; export default { name: 'Home', components: { HelloWorld, }, setup() { const { proxy } = getCurrentInstance(); const revampData = () => { proxy.$setGlobalState({ name: 'vue3子应用应用' }); }; return { revampData, }; }, }; </script>
[需求] 如何部署
qiankun部署的帖子网上根本找不到, 可能是感觉简单就都不想说了吧,笔者这里也是部署了很多遍才跑通,这里说下我的思路。
考虑到主应用和子应用共用域名时可能会存在路由冲突的问题,子应用可能会源源不断地添加进来,因此我们将子应用都放在xx.com/subapp/这个二级目录下,根路径/留给主应用。
步骤如下:
1.主应用main和所有子应用都打包出一份html,css,js,static,分目录上传到服务器,子应用统一放到subapp目录下,最终如:
├── main │ └── index.html └── subapp ├── sub-react │ └── index.html └── sub-vue └── index.html
2.配置nginx,预期是xx.com根路径指向主应用,xx.com/subapp指向子应用,子应用的配置只需写一份,以后新增子应用也不需要改nginx配置,以下应该是微应用部署的最简洁的一份nginx配置了。
server{ listen 80; #侦听端口 server_name http://wzs.bengdada.com/; #定义使用www.xx.com访问 charset utf-8; location / { root /data/wzs/main; # 主应用所在的目录 try_files $uri $uri/ /index.html; } location /subapp { alias /data/wzs/subapp; # 主应用所在的目录 try_files $uri $uri/ /index.html; } }
nginx -s reload后就可以了。
本文特地做了线上demo展示:
整站(主应用):wzs.bengdada.com/
单独访问子应用:
最后
本人从开始弄微前端反复查阅大量资料和视频,踩过很多坑,忍不住感叹 : 真是学无止境.....
最后的最后,喜欢本文的同学还请能顺手给个赞鼓励一下,非常感谢看到这里。