手把手实践qiankun微前端的开发和部署
文章开始之前,我们先提出几个问题,大家不妨带着问题来学习:
1、什么是微前端?有哪几种常见的解决方案?
2、为什么要是使用微前端,有什么样的场景使用微前端?
3、qiankun方案怎么实现微前端?
什么是微前端?有哪几种常见的解决方案?
说说个人的理解。随着微服务开发模式的兴起,前端开发也提出了微前端的开发模式。主要是为了将复杂的大应用进行解耦,拆分成一个主应用下挂载多个子应用,有那么一点分而治之的思想。就像古代分封诸侯,皇帝如果一个人亲身亲为治理一个国家,大事小事都由皇帝处理的话,一来会把皇帝累死,二来可能精力有限而不能很好治理,于是提出了分封制,皇帝分封诸侯国,诸侯国治理自己封地的事情,定期向皇帝上报。诸侯国与中央的通讯就像子项目与主项目之间的通讯。
目前常见的解决方案有:
- iframe方案
- single-spa方案
- qiankun方案
其中,single-spa方案是早几年提出的方案,qiankun方案是基于single-spa封装的,上手简单,比single-spa更具优势。qiankun是蚂蚁金服开源的一款框架,目前已在蚂蚁内部服务了超过 200+ 线上应用,值得尝试。至于为什么不使用iframe,可以先阅读一下《Why Not Iframe》这篇文章。如果你懒得访问,这里浓缩一下重点:
- url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
- UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中.
- 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
- 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
文中也提到如果不考虑体验问题,iframe 几乎是最完美的微前端解决方案了,其实iframe方案也不是一无是处,我们不能对它抱有偏见。iframe方案是接入成本最廉价的选择,同时也支持通过possMassage实现父子之间的通讯。它也是微前端的一种实现方式,在页面上无弹窗、无全屏等操作的时候,iframe 也是很好用的,加上配置缓存和 cdn 加速,如果是内网访问,也不会很慢。而且,对于陈年已久的Jquery多页面的老项目,qiankun似乎对多页应用没有很好的解决办法。每个页面都去修改,成本很大也很麻烦,但是使用 iframe 嵌入这些老项目就比较方便。与其折腾对接可能还会有很多配置上的问题,还不如用iframe干干净净接入,所以技术方案选择的同时,也要结合项目自身的情况。
当然,已经2020年了,我们没有老项目了,Jquery已经不去讨论了,全部梭哈三大框架。我们都是单页面,所以,qiankun方案再适合不过了。
为什么要是使用微前端,有什么样的场景使用微前端?
如果需要开发这样的项目:
其中有多个模块,每一个模块都可以看成一个应用,每一个应用都是一个完整的项目。
如果不采用微前端,整个项目集中在一起,不但不好分离,还会导致项目越来越大,同时参与的人员也会越来越多,非常不好管理,代码也不好维护。如果有个需求:需要子应用脱离平台独立运行,如果是微前端的方式,不费吹灰之力,子应用可以直接运行。但要是整合在一起,就比较费力。
还有一种场景,开发中的平台项目,突然需要嵌入别的项目。重新开发是不可能的了,不但开发成本过高,还要维护多套代码,所以也只能采用微前端的形式了。(巨无霸的Jquery老项目建议使用iframe的形式)
所以,在以上场景的时候,使用微前端是非常合适的。
qiankun方案怎么实现微前端?
这个是我们文章的重点。我们慢慢探讨。
说在前面:
- qiankun的使用与技术栈无关,同时子应用也是可以自由选择开发的框架,可以自己制定开发规范。
- 本次例子主项目是以vue开发的,子项目一个是vue一个是react。
- qiankun在开发环境下,主项目和全部子项目都会运行起来,如果子项目没有运行起来,当主项目菜单切到该子项目的时候,会打不开。所以在开发环境就会跑起多个服务。
- 部署的时候,主项目和子项目都需要分别打包,通常在主项目创建一个文件夹,子项目都打包后,放在主项目文件夹下面。这样之后跑起一个服务,同时可以使用子项目的路径,独自运行子项目。
——————————————————————————————————————————————
一、初始化项目
1、创建一个qiankun-test的文件夹,在下面依次创建主项目、vue项目和react项目
vue create main(main项目直接简单创建,不需要vuex和router)
vue create sub-vue(sub-vue带vuex和router)
npx create-react-app sub-react
2、给qiankun-test创建package.json文件,方便执行全部安装运行打包等脚本
npm init –yes
3、安装npm-run-all,并修改package.json:
npm i npm-run-all –save-dev
- 1 {
- 2 "name": "qiankun-test",
- 3 "version": "1.0.0",
- 4 "description": "",
- 5 "main": "index.js",
- 6 "scripts": {
- 7 "install": "npm-run-all --serial install:*",
- 8 "install:main": "cd main && yarn install",
- 9 "install:sub-vue": "cd sub-vue && yarn install",
- 10 "install:sub-react": "cd sub-react && yarn install",
- 11 "start": "npm-run-all --parallel start:*",
- 12 "start:sub-react": "cd sub-react && yarn start",
- 13 "start:sub-vue": "cd sub-vue && yarn serve",
- 14 "start:main": "cd main && yarn serve",
- 15 "build": "npm-run-all --serial build:*",
- 16 "build:main": "cd main && yarn build",
- 17 "build:sub-vue": "cd sub-vue && yarn build",
- 18 "build:sub-react": "cd sub-react && yarn build",
- 19 "test": "echo \"Error: no test specified\" && exit 1"
- 20 },
- 21 "keywords": [],
- 22 "author": "",
- 23 "license": "ISC",
- 24 "devDependencies": {
- 25 "npm-run-all": "^4.1.5"
- 26 }
- 27 }
npm-run-all
提供了多种运行多个命令的方式,常用的有以下几个:
-
--parallel
: 并行运行多个命令,例如:npm-run-all --parallel lint build
-
--serial
: 多个命令按排列顺序执行,例如:npm-run-all --serial clean lint build:**
-
--continue-on-error
: 是否忽略错误,添加此参数npm-run-all
会自动退出出错的命令,继续运行正常的 -
--race
: 添加此参数之后,只要有一个命令运行出错,那么npm-run-all
就会结束掉全部的命
我们在qiankun-test下面执行npm start后,会启动主项目和2个子项目
二、配置主项目
1、子项目的端口号必须固定,不然端口号不同导致匹配不上。
新建2个环境配置文件
.env.development
- 1 VUE_APP_SUB_VUE=http://localhost:5501
- 2 VUE_APP_SUB_REACT=http://localhost:5502
.env.production
- 1 VUE_APP_SUB_VUE=http://localhost:5050/subapp/sub-vue/
- 2 VUE_APP_SUB_REACT=http://localhost:5050/subapp/sub-react/
我们将开发环境子应用端口固定好,并将生产环境(http://localhost:5050)中的域名和子应用的访问路径写好(后面会新建个subapp文件夹存放打包后的子项目)
2、主项目安装qiankun,子项目不需要
cd main && npm i qiankun –save
同时顺便也固定主项目的端口(可选),修改下主项目的vue.config.js
- 1 module.exports = {
- 2 devServer: {
- 3 port: 5500,
- 4 },
- 5 chainWebpack: config => {
- 6 config.plugin(\'html\')
- 7 .tap((args) => {
- 8 args[0].title = \'qiankun-test\'
- 9 return args
- 10 })
- 11 }
- 12 };
3、注册子项目
在main主项目的src下新建micro-app.js:
- 1 const microApps = [
- 2 {
- 3 name: \'sub-vue\',
- 4 entry: process.env.VUE_APP_SUB_VUE,
- 5 activeRule: \'/sub-vue\'
- 6 },
- 7 {
- 8 name: \'sub-react\',
- 9 entry: process.env.VUE_APP_SUB_REACT,
- 10 activeRule: \'/sub-react\'
- 11 }
- 12 ]
- 13
- 14 const apps = microApps.map(item => {
- 15 return {
- 16 ...item,
- 17 container: \'#subapp-viewport\', // 子应用挂载的div
- 18 props: {
- 19 routerBase: item.activeRule, // 下发基础路由
- 20 }
- 21 }
- 22 })
- 23
- 24 export default apps
- 建议name与子项目的package里的name字段保持一致,保持唯一性
- entry是子项目入口,生产环境和开发环境地址是不一样的,这里使用了我们之前创建的环境文件中的值
- activeRule是子项目在主项目中的路由地址,建议后面也是项目名,统一一下会没那么乱
- container是主项目中的挂载容器id
- routerBase是主项目下发到子项目,可以在子项目中获取的到,这个到时候在子应用的路由中需要用到,用于设置路由的base属性
4、主项目main.js加载qiankun配置并启动
- 1 import Vue from \'vue\'
- 2 import App from \'./App.vue\'
- 3 import { registerMicroApps, start, setDefaultMountApp } from \'qiankun\'
- 4 import microApps from \'./micro-app\'
- 5
- 6 Vue.config.productionTip = false
- 7
- 8 new Vue({
- 9 render: h => h(App),
- 10 }).$mount(\'#app\')
- 11
- 12 const config = {
- 13 beforeLoad: [
- 14 app => {
- 15 console.log("%c before load",
- 16 \'background:#0f0 ; padding: 1px; border-radius: 3px; color: #fff\',
- 17 app);
- 18 }
- 19 ], // 挂载前回调
- 20 beforeMount: [
- 21 app => {
- 22 console.log("%c before mount",
- 23 \'background:#f1f ; padding: 1px; border-radius: 3px; color: #fff\',
- 24 app);
- 25 }
- 26 ], // 挂载后回调
- 27 afterUnmount: [
- 28 app => {
- 29 console.log("%c after unload",
- 30 \'background:#a7a ; padding: 1px; border-radius: 3px; color: #fff\',
- 31 app);
- 32 }
- 33 ] // 卸载后回调
- 34 }
- 35
- 36 registerMicroApps(microApps, config)
- 37 setDefaultMountApp(microApps[0].activeRoule) // 默认打开第一个子项目
- 38 start()
5、主项目公共菜单切换部分和容器部分
修改App.vue
- 1 <template>
- 2 <div id="app">
- 3 <div class="layout-header">
- 4 <div class="logo">QIANKUN-WUZHIQUAN</div>
- 5 <ul class="sub-apps">
- 6 <li v-for="item in microApps" :class="{active: item.activeRule === current}" :key="item.name" @click="goto(item)">{{ item.name }}</li>
- 7 </ul>
- 8 </div>
- 9 <div id="subapp-viewport"></div>
- 10 </div>
- 11 </template>
- 12
- 13 <script>
- 14 import microApps from \'./micro-app\'
- 15
- 16 export default {
- 17 name: \'App\',
- 18 data () {
- 19 return {
- 20 microApps,
- 21 current: \'/sub-vue\'
- 22 }
- 23 },
- 24 methods: {
- 25 goto (item) {
- 26 console.log(item)
- 27 this.current = item.activeRule
- 28 history.pushState(null, item.activeRule, item.activeRule) // 没引入路由,所以不能用路由切换
- 29 },
- 30 },
- 31 created() {
- 32 const path = window.location.pathname
- 33 if (this.microApps.findIndex(item => item.activeRule === path) >= 0) {
- 34 this.current = path
- 35 }
- 36 },
- 37 }
- 38 </script>
- 39
- 40 <style>
- 41 html, body{
- 42 margin: 0 !important;
- 43 padding: 0;
- 44 }
- 45 .layout-header{
- 46 height: 50px;
- 47 width: 100%;
- 48 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- 49 line-height: 50px;
- 50 position: relative;
- 51 }
- 52 .logo {
- 53 float: left;
- 54 margin: 0 50px;
- 55 }
- 56 .sub-apps {
- 57 list-style: none;
- 58 margin: 0;
- 59 overflow: hidden;
- 60 }
- 61 .sub-apps li{
- 62 list-style: none;
- 63 padding: 0 20px;
- 64 cursor: pointer;
- 65 float: left;
- 66 }
- 67 .sub-apps li.active {
- 68 color: #42b983;
- 69 text-decoration: underline;
- 70 }
- 71 </style>
6、启动主项目看看效果
cd mian && npm run serve
三、修改sub-vue子项目
子应用主要修改3个文件,一个是vue.config.js,一个是main.js,还有router下的index.js
1、vue.config.js
- 1 const port = 5501;
- 2 const { name } = require(\'../package.json\')
- 3 module.exports = {
- 4 publicPath: "./",
- 5 devServer: {
- 6 port,
- 7 headers: {
- 8 \'Access-Control-Allow-Origin\': \'*\'
- 9 }
- 10 },
- 11 configureWebpack: {
- 12 output: {
- 13 // 把子应用打包成 umd 库格式
- 14 library: `${name}-[name]`,
- 15 libraryTarget: \'umd\',
- 16 jsonpFunction: `webpackJsonp_${name}`
- 17 }
- 18 }
- 19 };
- qiankun 是通过 fetch 去获取子应用注册时配置的静态资源url,所有静态资源必须是支持跨域的,那就得设置允许源了
- 涉及到子应用名称的,都统一使用package中的name字段,官方也是推荐使用的这个name
- 需要打包成umd格式,是为了让 qiankun 拿到子应用export 的生命周期函数
2、src/router/index.js改为只暴露routes,new Router改到main.js中声明
- import Vue from "vue";
- import VueRouter from "vue-router";
- import Home from "../views/Home.vue";
- Vue.use(VueRouter);
- const routes = [
- {
- path: "/",
- name: "Home",
- component: Home
- },
- {
- path: "/about",
- name: "About",
- // route level code-splitting
- // this generates a separate chunk (about.[hash].js) for this route
- // which is lazy-loaded when the route is visited.
- component: () =>
- import(/* webpackChunkName: "about" */ "../views/About.vue")
- }
- ];
- export default routes;
3、main.js
- 1 import Vue from "vue";
- 2 import App from "./App.vue";
- 3 import routes from "./router";
- 4 import store from "./store";
- 5 import VueRouter from "vue-router";
- 6
- 7 Vue.config.productionTip = false;
- 8
- 9 let install = null;
- 10 function render(props = {}) {
- 11 const { container, routerBase } = props;
- 12 const router = new VueRouter({
- 13 base: window.__POWERED_BY_QIANKUN__ ? routerBase : process.env.BASE_URL,
- 14 mode: "history",
- 15 routes
- 16 });
- 17 install = new Vue({
- 18 router,
- 19 store,
- 20 render: h => h(App)
- 21 }).$mount(container ? container.querySelector("#app") : "#app");
- 22 }
- 23 if (window.__POWERED_BY_QIANKUN__) {
- 24 // eslint-disable-next-line
- 25 __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
- 26 } else {
- 27 render();
- 28 }
- 29
- 30 export async function bootstrap() {}
- 31
- 32 export async function mount(props) {
- 33 render(props);
- 34 }
- 35 export async function unmount() {
- 36 install.$destroy();
- 37 install.$el.innerHTML = ""; // 子项目内存泄露问题
- 38 install = null;
- 39 }
- 需要暴露qiankun的生命周期函数
- 注意销毁,防止内存泄漏
- 独立运行:window.__POWERED_BY_QIANKUN__为false,执行render创建vue对象;运行在qiankun: window.__POWERED_BY_QIANKUN__为true,会执行mount周期函数,在这里创建vue对象
- history模式下需要设置路由的base,值是子项目中的activeRule对应的值,在qiankun环境下使用。
4、下面拓展官网对声明周期的解释
5、对此sub-vue对接完成,运行看看效果:
同时,已经可以在主项目中看得到挂载的sub-vue子项目
四、修改sub-recat
1、新增.env文件添加PORT变量,端口号与父应用配置的保持一致
.env.development
- 1 SKIP_PREFLIGHT_CHECK=true
- 2 PORT=5502
- 3 PUBLIC_URL=/
.env.production
- 1 PUBLIC_URL=/subapp/sub-react
2、为了不eject所有webpack配置,我们用react-app-rewired方案复写webpack就可以了。
npm install react-app-rewired –save-dev
3、使用react-app-rewired运行,修改package.json中的script
- "scripts": {
- "start": "react-app-rewired start",
- "build": "react-app-rewired build",
- "test": "react-app-rewired test",
- "eject": "react-scripts eject"
- }
4、在sub-react下创建config-overrides.js文件
config-overrides.js
- 1 const { name } = require(\'./package.json\');
- 2
- 3 module.exports = {
- 4 webpack: function override(config, env) {
- 5 config.entry = config.entry.filter(
- 6 (e) => !e.includes(\'webpackHotDevClient\')
- 7 );
- 8
- 9 config.output.library = `${name}-[name]`;
- 10 config.output.libraryTarget = \'umd\';
- 11 config.output.jsonpFunction = `webpackJsonp_${name}`;
- 12 return config;
- 13 },
- 14 devServer: (configFunction) => {
- 15 return function (proxy, allowedHost) {
- 16 const config = configFunction(proxy, allowedHost);
- 17 config.open = false;
- 18 config.hot = false;
- 19 config.headers = {
- 20 \'Access-Control-Allow-Origin\': \'*\',
- 21 };
- 22 // Return your customised Webpack Development Server config.
- 23 return config;
- 24 };
- 25 },
- 26 };
注意:5-7行为了解决react子应用启动后,主应用第一次渲染后会挂掉的问题,原因是热更新引起的,所以需要在复写react的webpack时禁用掉热重载(加了下面配置禁用后会导致没法热重载,react应用在开发时得手动刷新了),见https://github.com/umijs/qiankun/issues/340
5、在src下面新建public-path.js
public-path.js
- 1 if (window.__POWERED_BY_QIANKUN__) {
- 2 // eslint-disable-next-line
- 3 __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
- 4 //__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
- 5 }
6、修改src下面的index.js
index.js
- 1 import \'./public-path\'
- 2 import React from \'react\';
- 3 import ReactDOM from \'react-dom\';
- 4 import \'./index.css\';
- 5 import App from \'./App\';
- 6
- 7 function render() {
- 8 ReactDOM.render(
- 9 <App />,
- 10 document.getElementById(\'root\')
- 11 );
- 12 }
- 13
- 14 if (!window.__POWERED_BY_QIANKUN__) {
- 15 render();
- 16 }
- 17 /**
- 18 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
- 19 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
- 20 */
- 21 export async function bootstrap() {
- 22 console.log(\'react app bootstraped\');
- 23 }
- 24 /**
- 25 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
- 26 */
- 27 export async function mount(props) {
- 28 render();
- 29 }
- 30 /**
- 31 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
- 32 */
- 33 export async function unmount() {
- 34 ReactDOM.unmountComponentAtNode(document.getElementById(\'root\'));
- 35 }
- 36 /**
- 37 * 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
- 38 */
- 39 export async function update(props) {
- 40 console.log(\'update props\', props);
- 41 }
7、一切就绪,我们运行一下看看:
但你满满信心以为能跑起来,结果却给你当头一棒,duang报错了!!!
提示:config.entry.filter is not a function
找了半天,感觉应该是版本的问题,将sub-react项目package.json中react相关依赖的版本写成下面的之后,重新安装就可以跑起来了(建议删掉node_modules包后重新yarn install):
注意:修改了版本后App.js 要引入import React from \’react\’;
在主项目中查看:
一切都对接完毕,以上是使用的history路由模式。下面介绍一下hash模式的改造
一、sub-vue项目调整
1、修改sub-vue项目路由方式,改为哈希路由,同时增加路由判断,当应用运行在qiankun里时,为所有路由和在路由跳转前为跳转路由path加上micrApp前缀
router/index.js
2、sub-vue子项目的main.js增加路由判断
main.js
对于主项目和子项目都是hash模式的话,主项目和子项目会共同接管路由,所以需要在子项目的所有路由前加上这个前缀。举个例子:
-
/#/vue/home
: 会加载vue
子项目的home
页面,但是其实,单独访问这个子项目的home
页面的完整路由就是/#/vue/home
-
/#/react/about
: 会加载react
子项目的about
页面,同样,单独访问这个子项目的about
页面的完整路由就是/#/react/about
-
/#/about
: 会加载主项目的about
页面
二、main主项目调整
1、修改主项目注册子项目时的路由匹配规则和增加主应用路由
micro-app.js
2、修改下App.vue页面的地址匹配规则,history匹配的是pathname,hash匹配hash
三、回到qiankun-test下面执行npm start全部运行看看效果
项目部署
我们在main项目的.env.production配置了线上地址是http://localhost:5050,子项目存放在subapp文件夹下面,我们分别对main、sub-vue和sub-react进行打包,回到qiankun-test执行npm run build即可
启动一个http://localhost:5050服务,将代码跑起来就好了:
也可以单独运行子应用:
http://localhost:5050/subapp/sub-react/
http://localhost:5050/subapp/sub-vue/#/
qiankun项目优化
一、解决IE11兼容性问题
安装以下
- import \'whatwg-fetch\';
- import \'custom-event-polyfill\';
- import \'core-js/stable/promise\';
- import \'core-js/stable/symbol\';
- import \'core-js/stable/string/starts-with\';
- import \'core-js/web/url\';
二、引入nprogress,子项目加载的时候,有进度条
主项目的main.js
主项目的App.vue
时间有限,没能接着讲下父子应用间的通讯,完整代码在我的github
地址:https://github.com/wuzhiquan/qiankun
simple-hash分支是hash模式
simple-history分支是history模式
complete是完整的代码,包括了父子间的状态通讯
感谢大家的star!