基于qiankun的微前端接入笔记

简介

微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。
这类问题在企业级 Web 应用中尤其常见。

  • 微前端架构具备以下几个核心价值:
  • 技术栈无关:主框架不限制接入应用的技术栈,微应用具备完全自主权
  • 独立开发、独立部署:微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
  • 增量升级:在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
  • 独立运行时:每个微应用之间状态隔离,运行时状态不共享

qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统

qiankun特性:

  • 基于 single-spa 封装,提供了更加开箱即用的 API
  • 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
  • HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
  • 样式隔离,确保微应用之间样式互相不干扰。
  • JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
  • 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
  • umi 插件,提供了 @umijs/plugin-qiankunumi 应用一键切换成微前端架构系统。

官方网站:qiankun

基本使用

这里以vue为例,使用vue-cli 4.x创建项目

主项目

  • 创建主项目:vue create main

  • 安装qiankun依赖:npm i qiankun --save

  • 修改src/main.js文件

import Vue from 'vue' import App from './App.vue' // 导入qiankun所需方法 import { registerMicroApps, // 注册子应用 runAfterFirstMounted, // 当第一个子应用装载完毕 setDefaultMountApp, // 设置默认装载的子应用 // initGlobalState, // 微前端之间的通信 start // 启动 } from 'qiankun' Vue.config.productionTip = false // 渲染主应用 new Vue({ render: h => h(App) }).$mount('#container') // 注册子应用 registerMicroApps([ { name: 'one', entry: '//localhost:6661', container: '#micro-view', activeRule: '/one' // activeRule: getActiveRule('/one') }, { name: 'two', entry: '//localhost:6662', container: '#micro-view', // activeRule: '/two' activeRule: getActiveRule('/two') } ], { beforeLoad: [ app => { console.log('before load') } ], beforeMount: [ app => { console.log('before mount') } ], afterMount: [ app => { console.log('after mount') } ], beforeUnmount: [ app => { console.log('before unmount') } ], afterUnmount: [ app => { console.log('after unmount') } ] }) function getActiveRule(routerPrefix) { return location => location.pathname.startsWith(`${routerPrefix}`) } // 设置默认app setDefaultMountApp('one') // 第一个子应用加载完毕后回调 runAfterFirstMounted(() => { console.log('第一个子应用加载完毕后的回调') }) // 启动qiankun start()
  • 修改public/index.html,将其中的id修改为container
<div id="container"></div>
  • 修改src/App.vue文件
<template> <div id="main"> 这是主应用 <br /> <button @click="changeView('/one')">子应用one</button> &nbsp; <button @click="changeView('/two')">子应用two</button> <hr> <!-- 这里的 micro-view 对应的是上面 main.js 里面的子应用配置的 container: '#micro-view' --> <div id="micro-view"></div> </div> </template>
export default { name: 'App', methods: { changeView(who) { window.history.pushState(null, who, who) } } }
  • 在根目录下添加vue.config.js文件,自定义主应用启动端口
module.exports = { devServer: { port: 3000 } }

子应用

备注:子应用是不安装qiankun依赖的,子应用只需要暴露对应的生命钩子即可

  • 创建子应用onevue create one

  • 修改src/router/index.js文件。将new VueRouter放到src/main.js文件中统一处理

import Home from '../views/Home.vue' 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
  • 修改src/main.js文件
import Vue from 'vue' import App from './App.vue' import VueRouter from 'vue-router' import routes from './router' import './public-path' Vue.use(VueRouter) Vue.config.productionTip = false let router = null let instance = null function render() { !router && (router = new VueRouter({ mode: 'history', base: window.__POWERED_BY_QIANKUN__ ? 'one' : '/', routes })) !instance && (instance = new Vue({ router, render: h => h(App) }).$mount('#app')) } // 生命周期 - 挂载前 export async function bootstrap(props) { console.log('one bootstrap') } // 生命周期 - 挂载后 export async function mount() { console.log('one mount') // 渲染 render() } // 生命周期 - 解除挂载 export async function unmount() { console.log('one unmount') instance.$destroy() instance = null router = null } // 本地调试 if (!window.__POWERED_BY_QIANKUN__) { render() }
  • src/目录下新建public-path.js文件
/* eslint-disable */ if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ } /* eslint-disable */
  • 在根目录下新建vue.config.js文件
const { name } = require('./package.json') const port = 6661 module.exports = { devServer: { port, // 允许被主应用跨域fetch请求到 headers: { 'Access-Control-Allow-Origin': '*' } }, configureWebpack: { output: { library: `${name}-[name]`, // 把子应用打包成umd库格式 // 当我们把 libraryTarget 设置为 umd 后,我们的 library 就暴露为所有的模块定义下都可运行的方式了,主应用就可以获取到微应用的生命周期钩子函数了 libraryTarget: 'umd', jsonpFunction: `webpackJsonp_${name}` } } }
  • 子应用two类似。为区分两个子应用,可以修改下对应的页面文件区分开

  • 分别启动这两个子应用。访问主项目地址即可看到结果

主应用下发参数

使用注册子应用时的props属性。将主应用需要传递给微应用的数据传递出去

  • 修改主应用的src/main.js文件的registerMicroApps,增加参数
// ... // 注册子应用 registerMicroApps([ { // ... // 增加props参数,内容是自定义的 // 这里简单写了两个属性 msg和fn props: { msg: { data: { mt: 'you are one' } }, fn: { show(msg) { console.log('one:', msg) } } } }, { // ... // 增加props参数 props: { msg: { data: { mt: 'you are two' } }, fn: { show(msg) { console.log('two:', msg) } } } } ], { // ... }) // ...
  • 修改子应用的src/main.js文件,修改导出的mount函数,将父级传入的fn绑定到Vue的原型上
// ... // 生命周期 - 挂载后 export async function mount() { // ... // 设置主应用下发的方法 // 将主应用传递过来的 props.msg 的 data 绑定到子应用的原型链上。这样后面就可以直接访问到了 Vue.prototype.$msg = props.msg.data // 主应用传递过来的 props.fn 是一个对象。循环遍历下,将各个属性绑定到子应用的原型链上 Object.keys(props.fn).forEach(method => { Vue.prototype[`$${method}`] = props.fn[method] }) // 渲染 render() } // ...
  • 修改子应用的src/views/About.vue文件
<template> <div class="about"> <h1>This is an two about</h1> <p>{{ $msg.mt }}</p> <br /> <button @click="testFn">测试事件</button> </div> </template>
export default { methods: { testFn() { this.$show('测试事件成功') } } }

应用间的通讯

使用initGlobalState方法

  • 修改主应用的src/main.js文件,增加通讯所需代码
// ... // 通讯 - 这部分代码放在设置默认app之前即可 const actions = initGlobalState({ // 初始化state,里面内容您随意 mt: 'init' }) // 在项目中任何需要监听的地方进行监听,在这里监听是为了方便 actions.onGlobalStateChange((state, prev) => { console.log('main state change', state, prev) }) // 将action对象绑到Vue原型上,为了项目中其他地方使用方便 Vue.prototype.$actions = actions // 设置默认app setDefaultMountApp('one') // ...
  • 修改主应用的src/App.vue,增加修改state的方式
<template> <div id="main"> <!-- ... --> <!-- 增加修改state的按钮 --> <button @click="changeState('1')">修改state = 1</button> &nbsp; <button @click="changeState('2')">修改state = 2</button> <hr> <!-- 这里的 micro-view 对应的是上面 main.js 里面的子应用配置的 container: '#micro-view' --> <div id="micro-view"></div> </div> </template>
export default { name: 'App', methods: { // ... changeState(value) { // 修改state this.$actions.setGlobalState({ mt: value }) } } }
  • 修改子应用的src/main.js文件
// ... // 生命周期 - 挂载后 export async function mount(props) { // ... // 设置通讯 Vue.prototype.$onGlobalStateChange = props.onGlobalStateChange Vue.prototype.$setGlobalState = props.setGlobalState // 渲染 render(props) } // ...
  • 修改子应用的src/views/About.vue文件,增加通讯监听
<p>this is my name --- {{ name }}</p>
export default { data() { return { name: 'init name' } }, mounted() { // 增加state监听,当mt数据发生变化的时候,我们修改name,体现在页面上 this.$onGlobalStateChange((state, prev) => { if (state.mt !== prev.mt) { this.name = state.mt } }) } }

常见的坑

样式隔离

start方法有个属性sandbox。该值默认为true

true,可以隔离子页面的样式,但是*htmlbody等公共的样式还是会影响到主应用

可以配置为 { strictStyleIsolation: true } 表示开启严格的样式隔离模式。
这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响。

这时候需要修改下子应用代码。否则会出现找不到#app的情况[Vue warn]: Cannot find element: #app

  • 修改子应用的src/main.js文件
// ... function render(props = {}) { const { container } = props new Vue({ router, render: h => h(App) }).$mount(container ? container.querySelector('#app') : '#app') } // ... // 生命周期 - 挂载后 export async function mount(props) { // ... // 渲染 render(props) } // ... // 本地调试 if (!window.__POWERED_BY_QIANKUN__) { render() }

子应用资源加载

  • 修改vue.config.js文件
// ... module.exports = { // ... publicPath:`//localhost:${port}`, // ... }

资源跨域访问

qiankun 是通过 fetch 去获取子应用注册时配置的静态资源url,所有静态资源必须是支持跨域的,那就得设置允许源了

简单的设置可以看下面

http {
  server {
    listen 80;
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Headers DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization;
    add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
    if ($request_method = 'OPTIONS') {
      return 204;
    }
    location / {
      root /data/www/html;
      index index.html index.htm;
    }
  }
}

window._POWERED_BY_QIANKUN

子应用中的这个全局变量来区分当前是否运行在 qiankun 的主应用中

  • 独立运行:window.__POWERED_BY_QIANKUN__false,执行render创建vue对象
  • 运行在qiankun: window.__POWERED_BY_QIANKUN__true,会执行mount周期函数,在这里创建vue对象

子应用中可以根据这个全局变量做一些特殊处理

  • 单独运行子应用,本地调试时
// 本地调试 if (!window.__POWERED_BY_QIANKUN__) { render() }
  • 处理路由的时候
router = new VueRouter({ mode: 'history', // 如果运行在qiankun的主应用中,添加对应的路由前缀。这里的 one 应该跟主应用中的子应用配置 activeRule 对应 base: window.__POWERED_BY_QIANKUN__ ? 'one' : '/', routes })

父应用共享util和data给子应用

通过注册子应用时的props属性。将主应用需要传递给微应用的数据传递出去

比如将主应用的store数据和工具类库

  • 主应用的src/main.js文件
import store from '@/store' let msg = { // 从主应用仓库读出的数据 data: store, // 从主应用读出的工具类库 utils: LibrayrJs }
  • 子应用在挂载前,将props数据导到子应用通过遍历赋值给到子应用vue原型中
export async function bootstrap(props = {}) { Vue.prototype.$mainStore = props.data Vue.prototype.$mainUtils = props.utils }

history路由模式,打包后刷新404

通过nginx配置加入try_files,对于找不到url的,将首页html返回

http {
  upstream gateway {
    server ranchernode001.xxx.com:31306;
    server ranchernode002.xxx.com:31306;
  }
  server {
    listen 80;
    location / {
      try_files $uri $uri/ /index.html;
      root /data/www/html;
      index index.html index.htm;
    }

    location /a {
      try_files $uri $uri/ /index.html;
    }
    location /a {
      try_files $uri $uri/ /index.html;
    }
    location /gateway {
      proxy_pass http://gateway;
    }
  }
}
  • try_files:用来解决nginx找不到client客户端所需要的资源时访问404的问题
  • proxy_pass:主要是用来配置接口网关反向代理,可以使得父子应用下访问的api是一致的,防止接口跨域问

创作不易,若本文对你有帮助,欢迎打赏支持作者!

 分享给好友: