首页 > 应用开发 > > 落地微前端 qiankun 理论与实践指北
“一千个人眼里有一千个哈姆雷特” 本文仅是作者这段时间对微前端的思考与感悟,文笔拙劣,多多海涵。微前端的实现方式有很多种,但是微前端并不完美。只有适合自己、适合团队的才是最佳实践。
由于本文较长(PS:本人懒得分篇),目录一至目录七偏理论知识,目录七之后为实践操作与思考,大家可以适当的跳跃来看
微前端是什么?

微前端能解决我们什么问题?
举例: 一个持续多年的应用,经历几年的业务的更新迭代,当项目发展到一定程度的时候就会遇到以下问题
有没有一种可以分解复杂度,提升协作效率,支持灵活扩展的架构模式?微前端应运而生—— “更友好的iframe” 将一个巨无霸应用拆解为一个个独立的微应用应用,而用户又是无感知的!
微前端核心原则:
这意味着我们可以循序渐进的进行巨石应用的拆解,去技术升级、去架构尝试、去业务拆解等等。以低成本、低风险的进行,为项目带来更多可能性
我们的项目适不适合改造成微前端项目模式?
看我们的项目满足不满足微前端化,先看能不能满足以下几点即可。
注意:没有迫切的需求接入微前端,只会带来额外的负担,我们要知道我们使用微前端是为了什么?
微前端实现方案对比
| 技术方案 | 描述 | 技术栈 | 优点 | 缺点 | 单独构建 / 部署 | 构建速度 | SPA 体验 | 项目侵入性 | 学习成本 | 通信难度 |
|---|---|---|---|---|---|---|---|---|---|---|
| iframe | 每个微应用独立开发部署,通过 iframe的方式将这些应用嵌入到父应用系统中 | 无限制 | 1. 技术栈无关,子应用独立构建部署 2. 实现简单,子应用之间自带沙箱,天然隔离,互不影响 |
体验差、路由无法记忆、页面适配困难、无法监控、依赖无法复用,兼容性等都具有局限性,资源开销巨大,通信困难 | 支持 | 正常 | 不支持 | 高 | 低 | 高 |
| Nginx 路由转发 | 通过Nginx配置实现不同路径映射到不同应用 | 无限制 | 简单、快速、易配置 | 在切换应用时触发发页面刷新,通信不易 | 支持 | 正常 | 不支持 | 正常 | 低 | 高 |
| Npm 集成 | 将微应用抽离成包的方式,发布Npm中,由父应用依赖的方式使用,构建时候集成进项目中 | 无限制 | 1. 编译阶段的应用,在项目运行阶段无需加载,体验流畅 2.开发与接入成本低,容易理解 |
1. 影响主应用编译速度和打包后的体积 2. 不支持动态下发,npm包更新后,需要重新更新包,主应用需要重新发布部署 |
不支持 | 慢 | 支持 | 高 | 高 | 正常 |
| 通用中心路由基座式 | 微应用可以使用不同技术栈;微应用之间完全独立,互不依赖。统一由基座工程进行管理,按照DOM节点的注册、挂载、卸载来完成。 | 无限制 | 子应用独立构建,用户体验好,可控性强,适应快速迭代 | 学习与实现的成本比较高,需要额外处理依赖复用 | 支持 | 正常 | 支持 | 高 | 高 | 正常 |
| 特定中心路由基座式 | 微应用业务线之间使用相同技术栈;基座工程和微应用可以单独开发单独部署;微应用有能力复用基座工程的公共基建。 | 统一技术栈 | 子应用独立构建,用户体验好,可控性强,适应快速迭代 | 学习与实现的成本比较高,需要额外处理依赖复用 | 支持 | 正常 | 支持 | 高 | 高 | 正常 |
| webpack5 模块联邦 | webpack5 模块联邦 去中心模式、脱离基座模式。每个应用是单独部署在各自的服务器,每个应用都可以引用其他应用,也能被其他应用所引用 | 统一技术栈 | 基于webpack5,无需引入新框架,学习成本低,像引入第三方库一样方便,各个应用的资源都可以相互共享应用间松耦合,各应用平行的关系 | 需要升级Webpack5技术栈必须保持一致改造旧项目难度大 | 支持 | 正常 | 支持 | 低 | 低 | 正常 |
对于选择困难同学来说,可以参考以下纬度进行方案技术的选型
| 参考纬度 | 是否能支持未来的迭代 |
|---|---|
| 稳定性 | 该方案是否经历了社区的考验,有较多的成熟案例,同时保持较高的 活跃性 |
| 可拓展性 | 支持定制化开发,提供较高的可拓展能力,同时成本可以在接受范围内 |
| 可控性 | 发生问题后,能够在第一时间内进行问题排查,以最快的响应速度来处理问题,修复的方案是否会依赖于外部环境 |
市面框架对比:
综合以上方案对比之后,我们确定采用了 qiankun 特定中心路由基座式的开发方案,原因如下:
微前端并不是万能的”解药“,没有正确治理,所有的 codebase 的归宿都是”屎山”
qiankun不是一个完整的微前端解决方案!
qiankun不是一个完整的微前端解决方案!!
qiankun不是一个完整的微前端解决方案!!!
1.微前端的运行时容器
4. 微前端初尝试
7. 标准化才能提升生产力
9. qiankun 不支持 Vite !!!
10. qiankun并不难
微应用的拆与合思考:拆的是系统复杂度,合的是系统复用度 核心原则:高内聚,低耦合
微应用的拆解没有具体规则,但是以下规则应该可以给你在进行系统拆分时提供一些依据。
“尽量减少彼此的通信和依赖“,微前端的通信交互、链接跳转等操作所带来等成本其实是很大的,所以在拆分的时候尽量“完全独立,互不依赖”
微应用的拆分的时候切忌“盲目细致拆分”,过度拆分会导致 “做的很牛逼,但是没有用的困局”,微应用的拆分并不是一步到位的,我们要根据实际情况逐步拆分。如果一开始不知道应该划分多细,可以先粗粒度划分,然后随着需求的发展,逐步拆分。
在拆分的时候我们应该尽量考虑未来场景:渐变式技术栈迁移,前端应用聚合、多系统业务复用,如何做业务解耦和代码复用。
应用之间应该尽量解耦,子应用的事情就应该由子应用来做。
建议按照业务域来做拆分

选择基座的模式?
以下案例是以Vue技术栈作为应用技术栈,建议应用之间还是统一技术栈,降低维护、上手、学习成本。越是不同技术、不同库的版本不同需要做的处理就越更多。
activeRule - string | (location: Location) => boolean | Array<string | (location: Location) => boolean> 必选,微应用的激活规则。
支持直接配置字符串或字符串数组,如 activeRule: '/app1' 或 activeRule: ['/app1', '/app2'],当配置为字符串时会直接跟 url 中的路径部分做前缀匹配,匹配成功表明当前应用会被激活。
支持配置一个 active function 函数或一组 active function。函数会传入当前 location 作为参数,函数返回 true 时表明当前微应用会被激活。如 location => location.pathname.startsWith('/app1')
yarn add qiankun // ps:只需要主应用安装即可
// 主应用/scr/main.js
import { registerMicroApps, start } from 'qiankun';
// 1. 获取微应用配置
const MICRO_CONFIG = [
{
name: 'vue app', // 应用的名字 必填 唯一
entry: '//localhost:7100', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
container: '#yourContainer', // 挂载具体容器 ID
// 3. 根据路由匹配,激活的子应用
activeRule: '/yourActiveRule',
props: {
xxxx: '/' // 下发给子应用
}
}
]
// 2. 注册微应用
registerMicroApps(MICRO_CONFIG)
start() // 启动微服务
activeRule 规则示例:此处拿官网的举例~ctiveRule:'/app1'
activeRule:'/users/:userId/profile'
activeRule:'/pathname/#/hash'
activeRule:['/pathname/#/hash', '/app1']
✅ https://app.com/pathname/#/hash/route/nested
✅ https://app.com/app1/anything/everything
🚫 https://app.com/pathname/app1
🚫 https://app.com/app2
💡 当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑。所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。
由于registerMicroApps的特性,会导致路由的keep alive 失效,故本文使用 loadMicroAp + router.beforeEach 进行来达到自动注册的目的。
如果微应用不是直接跟路由关联的时候,你可以选择手动加载微应用的方式会更加灵活。
手动挂载: loadMicroApps
// 任意页面都可以注册
import { loadMicroApp } from 'qiankun';
// 获取应用配置并手动挂载,挂载后返回挂载对象
this.microApp = loadMicroApp({
name: 'vue app', // 应用的名字 必填 唯一
entry: '//localhost:7100', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
container: '#yourContainer', // 挂载具体容器 ID
activeRule: '/yourActiveRule', // 根据路由 激活的子应用
props: {
xxxx: '/' // 下发给子应用
}
})
this.microApp.unmount() // 手动销毁~
微应用可以挂载在页面的任意位置,微应用、微项目、微页面、微组件,一切皆有可能。
第一种:路由页内挂载,把子应用内嵌页入使用
// 主应用/src/views/About.vue
<template>
<div class="about">
<div id="sub-app-container"></div>
</div>
</template>
第二种:根DOM中与主应用同级挂载,切换的时候隐藏应用,显示当前应用
// 主应用/scr/App.vue
<template>
<div id="app">
<!-- 不同的微应用 -->
<div v-show="location.hash.startsWith('#/operation')" id="sub-operation-container"></div>
<div v-show="location.hash.startsWith('#/inventory')" id="sub-inventory-container"></div>
</div>
</template>
简易的图示了qiankun是如何通过 import-html-entry 加载微应用的

简易流程:
html/head/body 等标签,其他资源保持原样styles 外链URL对象通过 fetch 获取 css,并将 css 内容以 <style> 的方式替换到原来 link标签的位置async 标识 ,会使用 requestIdleCallback 方法延迟执行。第一步:微应用的入口文件 修改 webpack_public_path
src 目录新增 public-path.jswebpack 默认的 publicPath 为 "" 空字符串,会基于当前路径来加载资源。但是我们在主应用中加载微应用资源的时候会导致资源丢失,所以需要重新设置 __webpack_public_path__ 的值// 微应用/src/const/public-path.js
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
第二步:微应用webpack 新增配置
const { name } = require('./package.json')
module.exports = {
devServer: {
port: 8081, // 父应用配置微应用端口,要与微应用端口一致
disableHostCheck: true, // 关闭主机检查,使微应用可以被 fetch
headers: {
'Access-Control-Allow-Origin': '*' //因为qiankun内部请求都是fetch来请求资源,所以子应用必须允许跨域
}
},
configureWebpack: {
output: {
library: `${name}-[name]`, // 微应用的包名,这里与主应用中注册的微应用名称一致
libraryTarget: 'umd', // 这里设置为umd意思是在 AMD 或 CommonJS 的 require 之后可访问。
jsonpFunction: `webpackJsonp_${name}` // webpack用来异步加载chunk的JSONP 函数。
}
}
}
第三步:微应用添加生命周期
微应用需要在自己的入口文件,添加
bootstrap、mount、unmount三个生命周期钩子,供主应用在适当的时机调用。
// 微应用/scr/main.js
import './public-path.js'
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
let instance = null
// 1. 将注册方法用函数包裹,供后续主应用与独立运行调用
function render(props = {}) {
const { container } = props
instance = new Vue({
router,
store,
render: h => h(App),
}).$mount(container ? container.querySelector('#app-micro') : '#app-micro')
}
// 判断是否在乾坤环境下,非乾坤环境下独立运行
if (!window.__POWERED_BY_QIANKUN__) {
render()
}
// 2. 导出的生命周期
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log('[vue] vue app bootstraped')
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
console.log('[vue] props from main framework', props)
render(props);
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount() {
instance.$destroy()
instance.$el.innerHTML = ''
instance = null
}
/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props) {
console.log('update props', props)
}
小结经历这几步,qiankun 父应用与微应用就接入完成了。当父应用完成加载微应用的时候,微应用就会遵循对应的解析 规则,插入到父应用的HMTL中了。

预先请求子应用的 HTML、JS、CSS 等静态资源,等切换子应用时,可以直接从缓存中读取这些静态资源,从而加快渲染子应用。
registerMicroApps 模式下在 start 方法配置预加载应用
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([...AppsConfig])
start({ prefetch: "all" }) // 配置预加载
prefetch - boolean | 'all' | string[] | (( apps: RegistrableApp[] ) => { criticalAppNames: string[]; minorAppsName: string[] }) - 可选,是否开启预加载,默认为 true。
配置为 true 则会在第一个微应用 mount 完成后开始预加载其他微应用的静态资源
配置为 all 则主应用 start 后即开始预加载所有微应用静态资源
配置为 string[] 则会在第一个微应用 mounted 后开始加载数组内的微应用资源
配置为 function 则可完全自定义应用的资源加载时机 (首屏应用及次屏应用)
loadMicroApps 模式下
import { prefetchApps } from 'qiankun';
export const MICRO_PREFETCH_APPS = [
{ name: 'vue-child', entry: '//localhost:7101/' },
{ name: 'vue-app', entry: '//localhost:8081/' }
]
prefetchApps(MICRO_PREFETCH_APPS)
// 基座/src/const/micro/application-list.js
export const MICRO_CONFIG = [
{
name: 'you app name', // 应用的名字
entry: '//localhost:7286/', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
container: '#yuo-container-container', // 容器id
activeRule: '/your-prefix', // 根据路由激活的路径
isPreload: true, // !! 是否开启预加载 !!
}
]
import { prefetchApps } from 'qiankun';
import { MICRO_CONFIG } from '@/const/micro/application-list.js';
// 获取配置的 isPreload 字段,并生成加载对应的格式
const MICRO_PREFETCH_APPS = MICRO_CONFIG.reduce(
(total, { isPreload, name, entry }) => (isPreload ? [...total, { name, entry }] : total),
[]
)
// 预加载应用
prefetchApps(MICRO_PREFETCH_APPS)
最好的路由模式就是主应用、子应用都统一模式,可以减少不同模式之间的兼容工作
本文选择统一为:父子路由hash 模式
| 主模式 | 子模式 | 推荐 | 接入影响 | 解决方案 | 备注 |
|---|---|---|---|---|---|
| hash | hash | 强烈推荐 | 无 | ||
| hash | history | 不推荐 | 有 | history.pushState | 改造成本大 |
| history | history | 强烈推荐 | 无 | ||
| history | hash | 推荐 | 无 |
PS: 每个模式之间的组合并不是接入就可以完成的,都需要一些改造,如:增加路由前缀,路由配置base设置,不同的模式activeRule的规则都不同
新增前缀不是微应用必须的,但是为了从 URL 上与其他应用隔离,也是为了接入旧应用的时候,能让 activeRule 方法能识别并激活应用,故新增路由前缀。
父应用路由表
[
// 主应用 router.js:如果想匹配任意路径,我们可以使用通配符 (*):
{
path: '/your-prefix',
name: 'Home',
component: Home
},
// 特定页面兜底 会匹配以 `/your-prefix` 开头的任意路径
// 如:/your-prefix/404 , /your-prefix/no-permission ....
{
path: '/your-prefix/*',
name: 'Home',
component: Home
}
]
PS:子应用路由切换,由于应用与路由都是通过 URL 注册与销毁的,当子应用路由跳转地址,无法与父应用的路由地址匹配上的时候页面会销毁,需要注意路由匹配,或者增加路由兜底。
子应用 hash 模式
// hash 模式不能使用base,只能改前缀
new VueRouter({
mode: 'hash',
routes: [
{
//增加路由前缀判断
path: `${ window.__POWERED_BY_QIANKUN__ ? 'your-prefix' : ''}/login`,
component: _import('login/index.vue')
}
]
})
子应用 history 模式
new VueRouter({
mode: 'history',
// **针对子应用是 history模式的时候,只用设置 router base 就好了,不用像hash 这么麻烦**
base: window.__POWERED_BY_QIANKUN__ ? 'your-prefix' : null,
routes: [
{
path: '/login',
component: _import('login/index.vue')
}
]
})
但是由于笔者是接入的是旧项目并且又是 hash 路由模式想顺利接入,一个个加三元则改动太多路由表了,为了减少对于旧项目接入时的影响仅在以下三处做修改(ps:因为懒)
const routes = [
{
path: '/home',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
redirect: 'home',
component: () => import('../views/About.vue')
},
{
path: '/about',
name: 'about',
alias: '/user/about',
component: () => import('../views/About.vue')
}
]
let SUN_ROUTER_PATH_LIST = [] // 所有被格式化的路由都记录一下
// 1. 格式化路由器参数 递归格式化
export function formatRouterParams(parameter) {
// 判断如果不是 qiankun 环境内容则原样返回
if (!window.__POWERED_BY_QIANKUN__) {
return parameter.data
}
// 递归函数:data:数据源,params:需要替换的参数(数组、字符串),value需要替换的值(函数),deepKey:判断是否需要递归的参数名
const recursionData = ({ data, params, value, deepKey }) => {
return data.reduce((total, item) => {
item = formatData({ item, params, value })
// 判断是否需要继续递归
if (deepKey && item[deepKey] && Array.isArray(item[deepKey])) {
item[deepKey] = recursionData({
data: item[deepKey],
params,
value,
deepKey
})
}
return [...total, item]
}, [])
}
return recursionData(parameter)
}
// 2. 格式化路由数据增加前缀
export function formatData({ item, params, value }) {
if (!item) return
// 如果params是数组:[path, ...more] 遍历增加前缀
if (Array.isArray(params)) {
params.forEach(key => {
if (Object.prototype.hasOwnProperty.call(item, key)) {
item[key] = geRouterValue(value, item[key])
}
})
} else if (params) {
item[params] = geRouterValue(value, item[params])
}
// 记录格式化路由路径
SUN_ROUTER_PATH_LIST.push(item)
return item
}
// 3. 判断value是函数还是值,如果是函数则调用函数 返回对应的value 值
export function geRouterValue(value, key) {
return typeof value === 'function' ? value(key) : value
}
const BASE_ROUTER_PATH = 'your-prefix'
const router = new VueRouter({
mode: 'hash',
// 调用方法格式化路由表参数!!!
routes: formatRouterParams({
data: route,
deepKey: 'children',
params: ['path', 'redirect', 'alias'],
value: value => { // value 格式化的方法
if (window.__POWERED_BY_QIANKUN__ && typeof value === 'string') {
const path = value[] === '/' ? value : `/${value}`
return BASE_ROUTER_PATH + path
}
return value
}
})
})
遍历结果返回-路由表,统一增加“your-prefix”前缀啦
[
{
"path": "your-prefix/home",
"name": "home",
"component": "Home"
},
{
"path": "your-prefix/about",
"name": "about",
"redirect": "your-prefix/home",
"component": ""
},
{
"path": "your-prefix/about",
"name": "about",
"alias": "your-prefix/user/about",
"component": ""
}
]
跳转的时候调用检查跳转函数,判断是否需要增加前缀
// 路由全局守卫
// ps: 在内部判断,如果跳转的路由与当前子应用无关,直接放行即可
router.beforeEach((to, from, next) => {
checkLink(to, next, () => {
next()
})
})
// LINK_MICRO_APP_LIST, SUN_ROUTER_PATH_LIST 变量上文有记录
const { name } = require('../../package.json')
export const BASE_ROUTER_PATH = `/${name}` // 此处笔者用了 package.json 的 name 做为 “your-prefix” 方便后期维护
// 跳转的时候检查,判断是否需要增加前缀
export function checkLink(to, next, callback) {
// 是否存在qinakun环境中
const IS_HAVE_QIANKUN = window.__POWERED_BY_QIANKUN__
// 是否跳转其他微应用
const IS_JUMP_TO_MICRO_APP = Object.values(LINK_MICRO_APP_LIST).includes(to.path)
// 是否跳转的是根路径
const IS_BASE_PATH_SYMBOL = to.path === '/'
// 根路径是否匹配一致
const IS_HAVE_BASE_ROUTER_PATH = getBasePath(to.path, '/') === getBasePath(BASE_ROUTER_PATH, '/')
// 判断以上情况来确定是否需要给路由动态增加前缀
const IS_ADD_PREFIX = IS_HAVE_QIANKUN && !IS_JUMP_TO_MICRO_APP && !IS_HAVE_BASE_ROUTER_PATH
if (IS_ADD_PREFIX || IS_BASE_PATH_SYMBOL) {
const path = `${BASE_ROUTER_PATH}${to.path}`
// 当前拼接的地址与 当前格式化的路由是否一致,一致才是跳转内部路由
if (SUN_ROUTER_PATH_LIST.some(e => [e.path, e.redirect, e.alias].includes(path))) {
next({ path })
}
}
// 执行回调函数
callback && callback()
}
// 获取当前的基础路径 如: getBasePath('/user/age/xxx', '/') => '/user'
export function getBasePath(path, prefix = '') {
if (!path) return
const pathArray = String(path).split('/').filter(item => item)
const basePath = prefix + pathArray[]
return basePath
}
// 因为改变了next()的地址, push方法会提示报错,这里过滤一下~
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
}
ps: 其可以直接在push中改写, 省略 router.befroeEach
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
const IS_JUMP_TO_MICRO_APP = Object.values(LINK_MICRO_APP_LIST).includes(location.path)
const IS_HAVE_QIANKUN = window.__POWERED_BY_QIANKUN__
const IS_BASE_PATH_SYMBOL = location.path === '/'
const IS_HAVE_BASE_ROUTER_PATH = getBasePath(location.path, '/') === BASE_ROUTER_PATH
const IS_ADD_PREFIX = IS_HAVE_QIANKUN && !IS_JUMP_TO_MICRO_APP && !IS_BASE_PATH_SYMBOL && !IS_HAVE_BASE_ROUTER_PATH
if (IS_ADD_PREFIX) {
location.path = `${BASE_ROUTER_PATH}${location.path}`
}
return originalPush.call(this, location)
}
跳转方式: 路由跳转与正常使用无异
跳转其他微应用
// 微应用/scr/const/link-micro-app-list
export const LINK_MICRO_APP_LIST = {
CHILD_VUE: '/child/vue', // vue 微应用地址
CHILD_REACT: '/child/react', // react 微应用地址
USER_INFO: '/user/info' // 跳转父应用用户信息页地址
}
使用场景:
// 微应用/router.js
// 1. 判断是否跳转其他应用
const IS_JUMP_TO_MICRO_APP = Object.values(LINK_MICRO_APP_LIST).includes(to.path)
// 2. 定义路由的时候通过路由列表地址获取
let routes = [
{
path: '/about',
name: 'about',
// 别名 重定向 凡是跳转其他应用的都应该在这里统一管理 !!!
redirect: LINK_MICRO_APP_LIST['CHILD_VUE'],
component: () => import('../views/About.vue')
}
]
// router.beforeEach
router.beforeEach((to, from, next) => {
if (IS_JUMP_TO_MICRO_APP) {
next(false) // 禁止跳转其他ying yon
}
// ... more code
})
loadMicroApp不能根据路由规则来挂载应用不是qiankun的问题,是我们的问题~
使用 router.afterEach + loadMicroApp 的解决应用 keep alive,思路是通过判断路由守卫的地址,如果是符合激活规则的则激活应用
1. 主应用 router 路由守卫
// 主应用/src/router/index.js
// 1. 全局后置钩子调用微应用加载方法
// 为什么笔者会在这里调用呢,其实是笔者利用了JavaScript机制的宏任务,目的就是为了在路由页获取是微应用的容器是否挂载了微应用,因为有时候微应用会因为作者系统的路由切换而被替换掉,所以用这个方式解决
router.afterEach(to => {
setTimeout(() => { // setTimeout 是宏任务的一种
microApplicationLoading(to.path) // 把当前跳转的路径传入
})
})
2. 判断微应用加载的方法 microApplicationLoading
// **src/const/micro/application-list.js**
export const microApplicationList [
{
name: 'you app name', // 应用的名字
entry: '//localhost:7286/', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
container: '#yuo-container-container', // 容器id
activeRule: '/your-prefix', // 根据路由激活的路径
**isPreload: true, // !! 是否开启预加载 !!
isRouteStart: true, // 是否需要路由启动**
props: { // 下发子应用的资源
router: router,
store: store,
parentEventHub: parentEventHub
}
}
]
// 主应用/src/const/micro/qianun-utils.js
// 加载微应用方法
import { loadMicroApp } from 'qiankun'
export async function microApplicationLoading(path) {
// 1. 根据路由地址加载当前应用配置
let currentActiveMicroConfig = await store.dispatch('d2admin/micro/GET_FIND_MICRO_CONFIG', path)
// 2. 从 vuex 获取缓存的微应用列表
const microApplicationList = store.getters['d2admin/micro/microApplicationList']
// 3. 如果没有匹配应用配置则代表跳转的不是微应用 or 微应用配置不需要路由启动的属性
if (!currentActiveMicroConfig || !currentActiveMicroConfig.isRouteStart) {
return
}
// 4. 根据应用配置 获取缓存的应用
const cacheMicro = microApplicationList.get(currentActiveMicroConfig.activeRule)
// 5. 判断当前挂载的是否有内容
const containerNode = getContainerNode(currentActiveMicroConfig.container)
const isNoTNodeContents = containerNode !== -1 && !containerNode
// 6. 如果没有dom节点 or 没有缓存应用配置 注册一下
if (isNoTNodeContents || !cacheMicro) {
// 如果有缓存应用配置,但是容器没有应用挂载,先卸载缓存应用再注册微应用
if (cacheMicro) {
cacheMicro.unmount()
cacheMicro.unmountPromise.then(() => {
loadRouterMicroApp(currentActiveMicroConfig)
})
return
}
// 加载应用
loadRouterMicroApp(currentActiveMicroConfig)
}
}
// 加载微应用
export function loadRouterMicroApp(currentApp) {
const micro = loadMicroApp(currentApp)
micro.mountPromise.then(() => {
// 挂载完成 设置一下vuex微应用列表
store.dispatch('d2admin/micro/SET_MICRO_APPLICATION_LIST', {
key: currentApp.activeRule,
value: micro
})
})
}
// 获取容器节点
export function getContainerNode(container) {
const containerNode = container && document.querySelector(container)
if (containerNode) {
return containerNode.childNodes.length
}
return -1
}
vuex 方法 记录一下注册应用对象
// 主应用/src/store/modules/d2admin/modules/micro.js
import MICRO_CONFIG from '@/const/micro/application-list.js '
export default {
state: {
microApplicationList: new Map([]) // 已经注册的微应用列表
},
getters: {
microApplicationList(state) {
return state.microApplicationList
}
},
actions: {
// 设置微应用程序列表
SET_MICRO_APPLICATION_LIST({ state, dispatch }, { key, value }) {
state.microApplicationList.set(key, value)
},
// 通过路径获取微应用配置
GET_FIND_MICRO_CONFIG({ state }, path) {
return MICRO_CONFIG.find(e => {
return getPathPrefix(path, '/') === getPathPrefix(e.activeRule, '/')
})
}
}
}
// 获取当前的基础路径 如: getPathPrefix('/user/age/xxx', '/') => '/user'
export function getPathPrefix(path, prefix = '') {
if (!path) return
const pathArray = String(path).split('/').filter(item => item)
const basePath = prefix + pathArray[]
return basePath
}
🚀 Link: 更多的 keep-alive 解决方案
微前端对于样式隔离问题,目前相关配套还不是很成熟

本文采取的样式隔离的最佳实践是:采用约定式隔离,用 CSS 命名空间。备选:CSS Module、css-in-js 等工程化手段,建立约束:如:避免写全局样式,子应用不能侵入(如动态增加全局样式等)修改除本应用外的样式,子应用样式写在以子应用名作为命名空间的类里等。
默认沙箱
严格样式隔离的沙箱模式
start({
sandbox: {
strictStyleIsolation: true // 严格沙箱模式
}
})
qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响,基于 ShadowDOM 的严格样式隔离并不是一个可以无脑使用的方案,大部分情况下都需要接入应用做一些适配后才能正常在 ShadowDOM 中运行起来

qiankun 还提供了一个实验性的样式隔离特性 experimentalStyleIsolation

BEM (Block Element Module)规范命名约束
特定规则链接:
模块: .Block
模块多单词: .Header-Block
模块_状态: .Block_Modifier
模块__子元素: .Block__Element
模块__子元素_状态:.Block__Element_Modifier
Block 一个独立的模块,一个本身就有意义的独立实体 比如:header、menu、containerElement 元素,块的一部分但是自身没有独立的含义 比如:header title、container inputModifier 修饰符,块或者元素的一些状态或者属性标志 比如:small、checkedCSS Modules
CSS In JS
postcss 增加命名空间
npm i postcss-plugin-namespace -D
module.exports = ctx => {
return {
plugins: [
require('postcss-plugin-namespace')('#your-prefix', {
ignore: ['html', /body/]
})
]
}
}
<html id="your-prefix"></html>
在项目根目录创建postcss.config.js文件
该插件会将全局所有class前加上统一前缀,并过滤掉ignore内的标签;ignore内可以写字符串,可以写正则表达式。但每次编译前都会运行,所以可能会增加编译时间
注意:如果用/body/这样的正则,会将所有带body的class都过滤掉,比如el-drawer__body、el-dialog__body等。
qiankun框架为了实现 JavaScript隔离,提供了三种不同场景使用的沙箱,分别是 snapshotSandbox、proxySandbox、legacySandbox
qiankun的快照沙箱是基于diff来实现的,主要用于不支持window.Proxy的低版本浏览器(IE 浏览器),而且也只适应单个的子应用qiankun基于es6的Proxy实现了两种应用场景不同的沙箱,legacySandbox(单例)proxySandbox(多例)
// 示例
// 1. 子应用-改写了 setItem 方法
window.localStorage.setItem = function() {
console.log('Hi child')
}
// 2. 兄弟应用-调用 setItem
console.log(window.localStorage.setItem) // ƒ () {console.log('Hi child')} 污染了全局
// 3. 主应用-调用 setItem
console.log(window.localStorage.setItem) // ƒ () {console.log('Hi child')} 污染了全局

// 1. 主应用
window.user = {
my: {
name: 'I m your father'
}
}
// 2. 微应用 这里第一次输出还是会继承父的 数据
console.log(window.user) // {my: {name: "I m your father"}}
window.user = { // 改变数据
my: {
name: 'child'
}
}
console.log(window.user) // {my: {name: "child"}}
// 3. 兄弟应用 因为有沙箱隔离不会影响到 window
console.log(window.user) // {my: {name: "I m your father"}}
微前端中最多的问题就是在沙箱中了,无论是 CSS 还是 JavaScript 沙箱都不是十全十美的,我们只能通过各种约束来避免沙箱出现问题的可能。例如:建立团队前缀,命名空间 CSS、事件、本地存储和 Cookie,以避免冲突并明确所有权。
笔者整理了一下经常出现问题的场景。
PS:
//定义需要遍历的常量
const storageMap = [
{
storage: sessionStorage,
method: 'getItem'
},
{
storage: sessionStorage,
method: 'setItem'
},
{
storage: localStorage,
method: 'getItem'
},
{
storage: localStorage,
method: 'setItem'
}
]
// 改写方法
function formatItem(storage, method) {
storage[method] = function(key, value, isGlobal = false) { // isGlobal 是否存储or查找全局
if (window.__POWERED_BY_QIANKUN__ && !isGlobal) { // 如果是qiankun则追加前缀
key = BASE_ROUTER_PATH + key
}
Object.getPrototypeOf(storage)[method].call(this, key, value) // 引用原方法改动最小化
}
}
// 遍历方法
storageMap.forEach(({ storage, method }) => {
formatItem(storage, method)
})

对于应用之间的资源共享,笔者认为这个与微前端的概念是有矛盾的。
微前端的概念:
矛盾思考:
以下是笔者整理的共享模块的方式,
git submodule 和 git subtree 都是很好的子仓库管理方案,但缺点是每次子应用变更后,聚合库还得同步一次变更,考虑到并不是所有人都会使用该聚合仓库,子仓库独立开发时往往不会主动同步到聚合库,使用聚合库的同学就得经常做同步的操作,比较耗时耗力,不算特别完美。多版本共存的情况qiankun不建议共享依赖,担心原型链污染等问题,如果一定要使用:推荐使用webpack的 Externals 来共享依赖库。
使用场景:
vue、axios、vue-router、element 等) 可以用 externals 的方式来引入,减少加载重复包导致资源浪费,一个项目使用了之后,另一个项目使用不再重复加载,可以直接复用这个文件。使用原理:
qiankun 将子项目的外链 script 标签,内容请求到之后,会记录到一个全局变量中,下次再次使用,他会先从这个全局变量中取。这样就会实现内容的复用,只要保证两个链接的 url 一致即可。使用方式:
微应用之间使用
webpack 的 externals,并在 index.html 中使用外链 script 引入这些公共依赖,只要这些公共依赖URL一致即可,请求的时候会优先从缓存中读取,类似HTTP缓存微应用使用基座依赖
ignore 属性(这是自定义的属性,非标准属性)。qiankun 在入口解析的时候会判断如果有这个属性就忽略。子项目独立运行,这些 js/css 仍能被加载,如此,便实现了“子项目复用主项目的依赖”。module.exports = {
configureWebpack: {
externals: {
'vue': 'Vue',
'vue-router': 'VueRouter',
'vuex': 'Vuex',
'element-ui': 'ELEMENT'
}
}
}
<link ignore rel="stylesheet" href="//cnd.com/antd.css">
<script ignore src="//cnd.com/antd.js"></script>
PS:主项目使用externals 后,子项目可以复用它的依赖,但是不复用依赖的子项目会报错。
🚀 Link # [Bug]公共依赖提取的时候,qiankun,代理window访问并没有先访问微应用的window,再访问主应用的window
上面的方案都是业内比较成熟的方案,还需开发者深入了解,笔者采用的是:NPM、webpack external。对外的且稳定的组件或封装,推荐 npm 包方式。
主应用的下发资源的核心就是:注册的时候通过 props 下发
props 传递给子应用,子应用在 bootstrap 或者 mount 钩子函数中获取// 主应用/src/const/micro/application-list.js
import { layout, assets, config, layout, public } from '/lib'
export default [{
name: 'you-app-name', // 应用的名字
entry: '//localhost:7286/', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
container: '#you-app-name-container', // 容器id
activeRule: '/you-app-name', // 根据路由激活的路径,这里注意要与子应用对应的 package ==> name 文件一致
props: { // 下发微应用的入口, 如果是固定确认的资源可以维护在应用注册列表
hideLayout: true, // 是否隐藏子应用侧边栏、导航栏
defaultPath: '', // 默认跳转地址
commonComponent: {}, // 下发的组件
public: public, // 父亲应用公共文件
assets: assets, // 父应用资源文件
config: config, // 父应用配置文件
layout: layout // 父应用布局组件
}
}]
如果是动态数据可以注册的时候传递下发
import store from '@/store/index'
import router from '@/router'
// 在微应用 props 属性 动态下发配置。
currentApp.props = {
...currentApp.props,
router: router, // 下发父应用路由
store: store // 下发父应用vuex
}
loadMicroApp(currentApp) // 注册应用的时候在子应用可以在微应用的生命周期中获取
// 微应用 mount 中接收
import childStore from '@/store/index'
import childRouter from '@/router'
export async function mount(props) {
render(props)
}
// 动态挂载 通过 this.$root.xxx 使用 data 的数据
function render(props = {}) {
// 获取父应用下发的资源,并存储在 data 上
const { container, router,store,layout, config, assets, public, commonComponent } = props
instance = new Vue({
childRouter,
childStore,
data() {
return {
parentRouter: router, // 父应用路由
parentVuex: store, // 父应用 vuex
parentLayout: layout, // 父应用布局组件
parentConfig: config, // 父应用配置文件
parentAssets: assets, // 父应用资源文件
parentPublic: public, // 父亲应用公共文件
parentCommonComponent: commonComponent // 下发的组件
}
},
render: h => h(App)
}).$mount(container ? container.querySelector('#you-micro') : '#you-micro')
}
window 上,子项目直接注册即可主项目入口文件:
import HelloWorld from '@/components/HelloWorld.vue'
window.commonComponent = { HelloWorld };
子项目直接使用:
components: {
HelloWorld: window.__POWERED_BY_QIANKUN__ ? window.commonComponent.HelloWorld : import('@/components/HelloWorld.vue'))
}
子项目本身自己也有这个组件,当别的子项目已经加载过了,就复用别人的组件,如果别的子项目未加载,就使用自己的这个组件
适用场景就是避免组件的重复加载,这个组件可能并不是全局的,只是某个页面使用。做法分三步:
1.由于子项目之间的全局变量不共享,主项目提供一个全局变量,用来存放组件
props 传给需要共享组件的子项目。// 主应用
import HelloWorld from '@/components/HelloWorld.vue'
props: {
commonComponent: {
HelloWorld
}
}
2.子项目拿到这个变量挂载到 window 上
export async function mount(props) {
window.commonComponent = props.data.commonComponent
render(props.data)
}
3.子项目中的共享组件写成异步组件,异步组件需要返回 Promise.resolve()
components: {
HelloWorld: async () => {
if (!window.commonComponent) {
window.commonComponent = {} // 独立运行时
}
const HelloWorld = window.commonComponent.HelloWorld
return HelloWorld || (window.commonComponent.HelloWorld = import('@/components/HelloWorld.vue'))
}
}
通信设计原则
微前端通信方式
localStorage、sessionStorage 实现的通信方式基于URL、Props 、LocalStorage 的方式就不讲述了上文都有对应的说明,以下只对 发布/订阅模式,状态管理模式进行讲解
笔者这里的设计模式是,主应用注册 EventBus,然后通过 props 下发微应用,这样微应用既有主应用的EventBus 也可以有自己的 EventBus
Vue.prototype.$eventBus = new Vue()
export const parentEventBus = Vue.prototype.$eventBus
// 在微应用 props 属性 动态下发配置。
import { parentEventBus } from '@/main'
currentApp.props = {
...currentActiveMicroConfig.props,
parentEventBus: parentEventBus // 下发主应用的 EventBus
}
loadMicroApp(currentApp)
// 微应用 mount 中接收
export async function mount(props) {
render(props)
}
// 动态挂载 通过 this.$root.xxx 使用data的数据
function render(props = {}) {
const { parentEventBus } = props
Vue.prototype.$eventBus = new Vue() // 子应用的独享的 EventBus
Vue.prototype.$parentEventBus = parentEventBus // 主应用下发的 EventBus
// 注册操作省略 ...
}
this.$parentEventBus.$off('you-event') // 关闭
this.$parentEventBus.$on('you-event', data => { // 监听
// xxxx code action
})
this.$parentEventBus.$emit('you-event', {...}) // 发布
// src/const/micro/actions.js
import { initGlobalState } from 'qiankun'
export const initialState = {}
const actions = initGlobalState(initialState)
export default actions
import actions from '@/const/micro/actions'
// 设置
actions.setGlobalState({
xxxxDataKey: xxxValue
})
// 监听全局
actions.onGlobalStateChange((state, prev) => {
console.log(state, prev, '子应用的 state: 变更后的状态; prev 变更前的状态')
})
// src/const/micro/actions.js 封装一下到时候引入使用方便
function emptyAction() {
// 警告:提示当前使用的是空 Action
console.warn('Current execute action is empty!')
}
class Actions {
// 默认值为空 Action
actions = {
onGlobalStateChange: emptyAction,
setGlobalState: emptyAction
}
// 设置 actions
setActions(actions) {
this.actions = actions
}
// 映射监听
onGlobalStateChange(...args) {
return this.actions.onGlobalStateChange(...args)
}
// 映射设置
setGlobalState(...args) {
return this.actions.setGlobalState(...args)
}
}
const actions = new Actions()
export default actions
import actions from './const/micro/actions'
export async function mount(props) {
actions.setActions(props) // 设置一下 actions 对象
}
actions.onGlobalStateChange((state, prev) => {
// 监听公共应用下发 state: 变更后的状态; prev 变更前的状态
})
// 在微应用 props 属性 动态下发配置。
import store from '@/store/index'
currentApp.props = {
...currentActiveMicroConfig.props,
store: store // 下发主应用的 store
}
loadMicroApp(currentApp)
// 微应用 mount 中接收
export async function mount(props) {
render(props)
}
// 动态挂载 通过 this.$root.xxx 使用data的数据
function render(props = {}) {
const { container, store } = props
instance = new Vue({
childStore,
data() {
return {
parentVuex: store, // 父应用 vuex
}
},
render: h => h(App)
}).$mount(container ? container.querySelector('#you-micro') : '#you-micro')
}
this.$root.parentVuex.state.xxxx // 读
this.$root.parentVuex.commit('xxxx', {}) // 写

1. 卸载时清空无用实例
export async function unmount() {
instance.$destroy()
instance.$el.innerHTML = '' // 关键
instance = null
route = null
// ... more
}
2. 设置过期时间与最大运行数,这里的的内容可以结合上面的内容来看,上文有对应的说明
// src/const/micro/index.js
export const MAX_RUN_MICRO_NUMBER = 5 // 最大运行微应用数量
// 主应用/src/const/micro/application-list.js
export default [{
name: 'you-app-name', // 应用的名字
// ... micro app config
// 自动销毁时间 单位:3000(ms) or (Infinity = 永久不会销毁)
unmountTime: '300000'
}]
注册的时候记录时间
// src/const/micro/qianun-utils.js
// 注册应用的方法
export function loadRouterMicroApp(currentApp) {
// 1. 返回注册应用对象
const micro = loadMicroApp(currentApp)
// 2. 微应用挂载完成
micro.mountPromise.then(() => {
// 3. 在应用对象,增加开始时间字段,记录微应用挂载时间
micro.createTime = new Date().getTime()
// 4. 在应用对象,增加卸载时间字段 记录应用卸载时间,如果时间是空默认 永不销毁
micro.unmountTime = currentApp.unmountTime || 'Infinity'
// 5. 设置当前应用列表, 记录挂载应用挂载信息,后期路由会匹配是否是否需要卸载
store.dispatch('d2admin/micro/SET_MICRO_APPLICATION_LIST', {
key: currentApp.activeRule,
value: micro
})
})
}
路由守卫的时候判断是否需要卸载
router.afterEach(to => {
microApplicationLoading(to.path)
})
// 主应用/src/const/micro/qianun-utils.js
// 加载微应用方法
export async function microApplicationLoading(path) {
// 1. 根据路由地址加载当前应用配置
let currentActiveMicroConfig = await store.dispatch('d2admin/micro/GET_FIND_MICRO_CONFIG', path)
// 2. 获取微应用列表
const microApplicationList = store.getters['d2admin/micro/microApplicationList']
// 3. 判断应用运行时间销毁应用
store.dispatch('d2admin/micro/CHECK_UNMOUNT_MICRO', { microApplicationList, currentActiveMicroConfig })
// ... code 后面注册判断操作就省略了
}
判断是否最大堆栈、判断是否超时销毁
// 主应用/src/store/modules/d2admin/modules/micro.js
export default {
state: {
microApplicationList: new Map([]),
},
actions: {
// 检查一下是否需要卸载微应用 依据时间来判断 microApplicationList:缓存微应用列表,currentActiveMicroConfig:当前URL匹配的微应用配置
CHECK_UNMOUNT_MICRO({ state, dispatch }, { microApplicationList, currentActiveMicroConfig }) {
// 1. 判断时候有缓存列表
if (!microApplicationList.size) {
return
}
// 2. 获取当前时间
const currentTime = new Date().getTime()
// 3. 遍历缓存应用列表,判断应用是否需要销毁了~
Array.from(microApplicationList).forEach(([key, item]) => {
// 4. 获取应用运行时间
const runningTime = currentTime - item.createTime
// 5. 获取应用卸载时间
const unmountTime = item.unmountTime
// 6. 如果有微应用配置,这说明跳转就是已经挂载过的微应用了,刷新应用时间与取消应用销毁(续费一下,避免销毁有激活重复开销)
if (currentActiveMicroConfig) {
item.createTime = new Date().getTime()
// !!!设置一下当前缓存应用列表,更新应用时间,判断是否达到最大堆栈,是否需要清除应用!!!
dispatch('SET_MICRO_APPLICATION_LIST', {
key: item.activeRule,
value: item
})
return
}
// 7. 如果运行时大于销毁时间则销毁对应应用,并且不是 Infinity 关键字
if (runningTime >= unmountTime && unmountTime !== 'Infinity') {
dispatch('DELETE_MICRO_APPLICATION_LIST', key)
}
})
},
// 删除微应用程序列表
DELETE_MICRO_APPLICATION_LIST({ state }, key) {
const micro = state.microApplicationList.get(key)
micro && micro.unmount()
state.microApplicationList.delete(key)
},
// 设置微应用程序列表
SET_MICRO_APPLICATION_LIST({ state, dispatch }, { key, value }) {
// 判断是否达到最大堆栈,清除应用
dispatch('CLEAR_MICRO_STACK')
state.microApplicationList.set(key, value)
},
// 检查是否需要清空堆栈
CLEAR_MICRO_STACK({ state, dispatch }) {
// 判断是否是 Infinity 无堆栈限制
if (MAX_RUN_MICRO_NUMBER === 'Infinity') {
return
}
// 判断是否达到最大堆栈
if (state.microApplicationList.size < MAX_RUN_MICRO_NUMBER) {
return
}
// 获取MAP的第一个应用销毁并删除vuex信息
const key = state.microApplicationList.keys().next().value
dispatch('DELETE_MICRO_APPLICATION_LIST', key)
}
}
}
loadMicroApp 来加载。momery 路由。vue-router 使用 abstract 模式,react-router 使用 memory history 模式,angular-router 不支持。push、 replace、 go) 在各类路由模式(history、 hash 和 abstract) 下表现一致。abstract 是vue路由中的第三种模式,本身是用来在不支持浏览器API的环境中,充当fallback,无论 hash还是history模式都会对浏览器上的url产生作用,于是我们利用到了abstract这种与浏览器分离的路由模式解决多应用路由冲突的问题。function render({ data = {} , container, defaultPath } = {}) {
router = new VueRouter({
mode: 'abstract', // 不会被URL所影响
routes
})
instance = new Vue({
router,
store,
render: h => h(App)
}).$mount(container ? container.querySelector('#appVueHash') : '#appVueHash')
if (defaultPath) {
router.push(defaultPath)
}
}
建议在开发与部署的时候,所有的微应用都放在一个目录,虽然qiankun的应用只需提供微应用URL地址即可,从理论上来说项目放在那里都是没有影响的。但是出于管理维护的目的,我们还是推荐:
└── micro-app-container # 根文件夹
├── main/ # 基座应用/主应用
├── child/ # 存放所有微应用的文件夹
| ├── vue-hash/ # 存放微应用 vue-hash 的文件夹
| ├── vue-history/ # 存放微应用 vue-history 的文件夹
├── package.json # 公共文件的 index.html 执行命令
├── node_modules/ # 公共文件依赖
根据上面的结构一个一个 启动or打包应用太麻烦了,使用npm-run-all 命令 解决npm run 命令无法同时运行多个脚本的问题
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 就会结束掉全部的命令安装依赖
npm install npm-run-all --save-dev
// or
yarn add npm-run-all --dev
配置命令 package.json, 一键给所有应用安装依赖
// 执行 install: 的命令~ 可以批量执行相同命令的前缀,可以异步、同步执行命令
// 例如:npm run install-all 就给所有项目安装依赖
"scripts": {
"install:child-hash": "cd child/child-hash && yarn",
"install:child-history": "cd child/child-history && yarn",
"install:main": "cd main && yarn",
"install-all": "npm-run-all install:*", // 全局安装依赖
"start:child-hash": "cd child/child-hash && npm run serve",
"start:child-history": "cd child/child-history && npm run serve",
"start:main": "cd main && npm run serve",
"serve-all": "npm-run-all --parallel start:*", // 全局启动
"build:child-hash": "cd child/child-hash && npm run build",
"build:child-history": "cd child/child-history && npm run build",
"build:main": "cd main && npm run build",
"build-all": "npm-run-all --parallel build:*" // 全局打包
}
或者配合脚本可以自己写一写简单的 shell 脚本
# script/clone-all.sh
# 相关项目地址
# xxx 项目
git clone http:/xxxxxxx.git
# xxx 项目
git clone http://xxxxxxxx.git
package.json 中增加命令执行
"clone:all": "bash ./scripts/clone-all.sh" // npm run clone:all 便可以批量克隆项目了
部署的时候也和开发的时候一样,不过可以直接放在基座应用里面使用
└── html/ # 根文件夹
|
├── child/ # 存放所有微应用的文件夹
| ├── vue-hash/ # 存放微应用 vue-hash 的文件夹
| ├── vue-history/ # 存放微应用 vue-history 的文件夹
├── index.html # 主应用的index.html
├── css/ # 主应用的css文件夹
├── js/ # 主应用的js文件夹
此时需要设置微应用构建时的 publicPath 和 history 模式的路由 base,然后才能打包放到对应的目录里。构建的时候切记要修改 webpack 中的 publicPath 地址!!!
| 项目 | 路由 base | publicPath | 真实访问路径 |
|---|---|---|---|
| vue-hash | 无 | /child/vue-hash/ | http://localhost:8080/child/vue-hash/ |
| vue-history | /child/vue-history/ | /child/vue-history/ | http://localhost:8080/child/vue-history/ |
vue-history 微应用
base: window.__POWERED_BY_QIANKUN__ ? '/app-vue-history/' : '/child/vue-history/',
module.exports = {
publicPath: '/child/vue-history/',
};
vue.config.js):但是笔者觉得这样写真的不优雅,笔者希望有关于微前端的所有配置都在一个微前端配置页里面维护,并且清晰可见。
所以笔者的做法是方法判断环境传入一个对象,减少三元的丑陋与混乱,把路由前缀动态的下发给微应用,但微应用注册的时候,在动态把前缀加上
// 主应用 src/const/micro/application-list.js
// 获取不同环境的入口
function getEentry({ prodPath, devPath }) {
const isProduction = process.env.NODE_ENV === 'production'
return isProduction ? prodPath : devPath
}
// 注册微应用列表
export default [
{
name: 'your-name', // 应用的名字
// 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
entry: getEentry({
devPath: '//localhost:7286/', // 开发环境地址
prodPath: `/child/your-name/` // 生产环境地址
}),
props: { // 下发微应用的入口
routeBase: '/app-vue-history/', // 动态下发路由前缀
}
}
]
以上文章来源于稀土掘金技术社区 ,作者InfinityTomorrow
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/813/viewspace-3141445/ ,如需转载,请注明出处,否则将追究法律责任。