微前端搭建
主应用
安装 qiankun
javascript
npm i qiankun -S
在主应用中注册微应用
当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子
javascript
import { registerMicroApps, start } from 'qiankun'
// 每次加载切换子应用都会调用此方法
const loader = loading => {
console.log('加载loading', loading)
}
// 1. 在基座中注册子应用
registerMicroApps(
[
{
name: 'vue2-gojs', // 微应用的名称,微应用之间必须确保唯一
entry: '//localhost:8001', // 微应用的入口
activeRule: '/gojs', // 微应用的激活规则,当路径以 /react 为前缀时启动
container: '#micro-container', // 微应用的容器节点的选择器或者 Element 实例
loader, // loading 状态发生变化时会调用的方法
props: { userInfo:{ name: 'burc', password: 'xxxxxx'} }, // 主应用需要传递给微应用的数据
},
{
name: 'vue2-map-drilling',
entry: '//localhost:8002',
activeRule: '/map',
container: '#micro-container',
loader,
props: {},
},
{
name: 'vue3-pdf',
entry: '//localhost:8003',
activeRule: '/pdf',
container: '#micro-container',
loader,
props: {},
},
],
)
start({})
子应用microApps
vue2+webpack
配置 webapck
配置 webpack,允许开发环境跨域和 umd
打包
javascript
// vue.config.js
const packageName = require('./package.json').name
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave: false,
// 我们在 public-path.js 修改了 运行时publicPath,这里是可以省略的
publicPath: process.env.NODE_ENV ? '/' : '/micro-apps/vue2-map-drilling/',
devServer: {
port: 8001,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
configureWebpack: {
output: {
libraryTarget: 'umd',
library: `${packageName}`,
},
},
})
修改运行时 public
新增 public-path.js
文件,用于修改运行时的 publicPath
,记得在 main.js 入口文件引入此文件
运行时publicPath是什么?参考 公共路径 | webpack、为什么微应用加载的资源会 404?
javascript
// public-path.js
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
路由改造
修改基路径为子应用的路由激活规则,即 activeRule
javascript
// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import routes from './routes'
Vue.use(VueRouter)
export default () => {
return new VueRouter({
routes,
mode: 'history',
base: window.__POWERED_BY_QIANKUN__ ? '/map/' : '/', // 子应用的 activeRule
})
}
入口文件改造
通过 window.POWERED_BY_QIANKUN
判断子应用是否在 qiankun 容器内运行
若在运行在 qiankun 容器中,则导出三个必要的生命周期函数,暴露接入协议
在 Qiankun 框架中,主应用负责加载和启动所有微前端子应用程序。
当子应用程序被加载时,Qiankun 框架会自动将 window.POWERED_BY_QIANKUN 变量注入到子应用程序的全局作用域中。这样,在子应用程序中就可以检测到当前应用程序是否在 Qiankun 容器内运行
javascript
// main.js
import './public-path.js'
import Vue from 'vue'
import App from './App.vue'
import createRouter from '@/router'
import '@/assets/styles/reset.less'
Vue.config.productionTip = false
let app
let router
function render (props) {
router = createRouter()
app = new Vue({
router,
render: h => h(App),
})
const container = props.container
app.$mount(container ? container.querySelector('#app') : '#app')
}
// 没有运行在乾坤容器中
if (!window.__POWERED_BY_QIANKUN__) {
console.log('子应用独立启动')
render({})
const ele = document.querySelector('.micro-app')
ele.style.height = '100vh'
}
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap () {
console.log('react app bootstraped')
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount (props) {
render(props)
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount (props) {
app.$destroy()
app = null
router = null
}
/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update (props) {
console.log('update props', props)
}
vue3+vite
配置 vite
安装 vite-plugin-qiankun
并配置 plugins
,vite-plugin-qiankun:帮助应用快速接入乾坤的vite插件
允许开发环境跨域和 umd
打包
设置 base
,生产环境需要设置非根路径。没有类似 webpack 中的 运行时publicPath
,也就是__webpack_public_path__,换句话说就是 vite 不支持运行时 publicPath
,必须设置 base`
javascript
// vite.config.ts
import qiankun from 'vite-plugin-qiankun'
import { name as packageName } from './package.json'
export default {
// 这里的 'vue3-pdf' 是子应用名,主应用注册时AppName需保持一致
plugins: [vue(), qiankun('vue3-pdf', { useDevMode: true })],
// 公共基础路径,等同 webpack 的 publicPath
base: process.env.NODE_ENV === 'development' ? '/' : '/micro-apps/vue3-pdf/',
server: {
port: 8003,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
build: {
target: 'esnext',
lib: {
name: `${packageName}-[name]`,
formats: ['umd'],
},
},
}
路由改造
修改基路径为子应用的路由激活规则,即 activeRule
javascript
import { createRouter, createWebHistory } from 'vue-router'
import { qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
import routes from './routes'
export default () => {
// 运行在 qiankun容器的话,添加前缀
const base = qiankunWindow.__POWERED_BY_QIANKUN__ ? '/pdf/' : '/'
return new createRouter({
history: createWebHistory(base),
routes
})
}
入口文件改造
通过 qiankunWindow.__POWERED_BY_QIANKUN__
判断子应用是否在 qiankun 容器内运行
若在运行在 qiankun 容器中,则导出三个必要的生命周期函数,暴露接入协议
qiankun官方是
window.__POWERED_BY_QIANKUN__
来检测到当前应用程序是否在 Qiankun 容器内运行,vite-plugin-qiankun 插件引用之后我们通过qiankunWindow.__POWERED_BY_QIANKUN__
来判断
javascript
import {
renderWithQiankun,
qiankunWindow,
} from 'vite-plugin-qiankun/dist/helper'
import { createApp } from 'vue'
import App from './App.vue'
import createRouter from './router'
import './assets/index.css'
import './assets/variable.less'
let instance = null
// 顶层作用域,如果写在 render 函数内部,重新挂载会报错
const router = createRouter()
function render(props) {
instance = createApp(App)
instance.use(router)
const container = props.container
instance.mount(container ? container.querySelector('#app') : '#app')
}
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
console.log('独立启动子应用')
render({})
const ele = document.querySelector('.micro-app')
ele.style.height = '100vh'
} else {
renderWithQiankun({
mount(props) {
console.log('mount')
render(props)
},
bootstrap() {
console.log('bootstrap')
},
unmount() {
instance.unmount()
instance = null
},
})
}
monorepo架构搭建
安装 pnpm
yaml
npm i pnpm -g
初始化
根目录运行 pnpm init
创建 package.json 文件
然后根目录新建一个文件夹 packages,用于存储子包
新建 packages/libc-desgin
( 公共组件包 ),即UI组件库,这里我们直接 clone 了 iview-ui-plus 代码。运行 pnpm install
安装依赖
修改 package.json 的 name 为 "@libc/desgin";修改 package.json 的 main 入口文件路径字段为 "src/index.js" 💥💥💥
shell
// package.json
{
"name": "@libc/desgin",
"version": "1.3.19",
"main": "src/index.js"
}
配置workspace
根目录新建一个 pnpm-workspace.yaml,将 【packages 下所有的目录】【主/子应用】【模块联邦】都作为包进行管理💥💥💥
此时,pnpm-workspace.yaml 工作空间下的每个子包都可以共享我们的公共依赖了
yaml
packages:
# packages 目录下的每一个目录都作为一个独立的模块
- 'packages/*'
# 主应用
- 'main'
# 微应用
- 'microApps/*'
# 模块联邦
- 'public-app'
如果是老项目的话,需要将 npm 迁移到 pnpm,后面有迁移步骤!
依赖
全局依赖
全局安装公共依赖 lodash
。需要加-w
(在工作空间的根目录中启动 pnpm)
shell
pnpm install lodash -w
这样,workspace 工作空间下所有的包 就都可以使用 lodash
库了
局部依赖
如果只有 vue3-pdf
项目用到了 lodash
,我们也可以安装到vue3-pdf
子应用内部,不作为公共依赖项,有两种方法可以实现
- cd 到
microApps/vue3-pdf
目录下,直接安装
shell
pnpm install lodash
- 在任意目录下,使用
--filter
参数进行安装;package_selector:package.json 对应的 name 字段
shell
pnpm install lodash --filter <package_selector>
共享子包
如何把子包 libc-desgin 共享出去?
子包之间可以通过 package.json
定义的 name
相互引用
包名是 package.json 中的 name,用--workspace
参数去安装共享子包,会去 workspace工作空间中找依赖项并安装
shell
pnpm install @libc/desgin --workspace -w
package.json
中就会自动添加如下依赖,"workspace:"
只会解析本地 workspace 包含的 package
shell
"dependencies": {
"@libc/desgin": "workspace:^"
}
工作空间下的项目都可以使用 @libc/desgin
里的组件,import 引入即可
shell
import { isObject } from '@libc/desgin'
如何将 npm 迁移到 pnpm?
卸载之前的 node_modules
shell
npm install rimraf -g
rimraf node_modules
创建 .npmrc
shell
// .npmrc
# pnpm 配置
shamefully-hoist=false
auto-install-peers=true
strict-peer-dependencies=false
这里重点说下shamefully-hoist
,默认 false。如果后续迁移失败可以考虑改为 true,删掉所有的 node_modules
重新 pnpm install 就行
- false:
node_modules
下只能看到直接依赖的套件,次级依赖在node_modules/.pnpm
目录下;无法访问其他子包局部安装的依赖项,例如,vue-dome2 安装的 lodash,vue-dome1 是访问不到的 - true:將所有套件都拉升到
node_modules
目錄下,能访问到其他子包局部安装的依赖项,例如,vue-dome2 安装的 lodash,vue-dome1 是能访问到的
转换 pnpm-lock.yaml
将 package-lock.json 和 yarn.lock 转成 pnpm-lock.yaml 文件,保证依赖版本不变
shell
pnpm import
安装依赖包
shell
pnpm install
最后,迁移完成!
在项目正常运行之后,可以删除原本的 package-lock.json 和 yarn.lock 文件,保持项目的整洁
模块联邦
一定要确保组件的提供方和消费方,是基于 webpack5 构建的!!!
组件提供方
javascript
// vue.config.js
const { defineConfig } = require('@vue/cli-service')
// 自动化注入
const fs = require('fs')
const MFROOT = './src/components'
const MFList = fs.readdirSync(MFROOT)
const exposes = {}
MFList.forEach(item => {
exposes[`./${item}`] = `${MFROOT}/${item}`
})
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
port: 9000,
headers: {
'Access-Control-Allow-Origin': '*'
}
},
// 设置 publicPath,要不然模块联邦报错 ChunkLoadError: Loading chunk xxx failed
publicPath: process.env.NODE_ENV === 'development' ? 'http://localhost:9000/' : '/public-app/',
// publicPath: 'auto', // 设置 auto 也可以,自动推断在运行时的公共路径
chainWebpack: config => {
// 模块联邦 与 splitChunks冲突,关闭 splitChunks(代码分割)
config.optimization.delete('splitChunks')
// 模块联邦
config
.plugin('module-feaderation-plugin')
.use(require('webpack').container.ModuleFederationPlugin, [
{
// 当前模块的名称
name: 'provider',
library: { type: 'umd', name: 'provider' },
// 构建输出的文件名,组件消费方引用:http://localhost:9000/remoteEntry.js
filename: 'remoteEntry.js',
// 作为提供方,暴露的组件
exposes
// 作为提供方,暴露的组件
// exposes: {
// './Page404': './src/components/Page404',
// './CustomModal': './src/components/CustomModal'
// }
}
])
}
})
组件消费方
基于 URL 配置的 Remote
javascript
// vue.config.js
const packageName = require('./package.json').name
const { defineConfig } = require('@vue/cli-service')
const path = require('path')
module.exports = defineConfig({
transpileDependencies: true,
// qiankun配置,我们在 public-path.js 修改了 运行时 publicPath,这里是可以省略的
// publicPath: process.env.NODE_ENV === 'development' ? '/' : '/micro-apps/vue2-map-drilling/',
devServer: {
port: 8002,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
configureWebpack: {
output: {
libraryTarget: 'umd',
library: `${packageName}-[name]`,
},
},
chainWebpack: config => {
// 模块联邦
config
.plugin('module-feaderation-plugin')
.use(require('webpack').container.ModuleFederationPlugin, [
{
// 当前模块的名称
name: 'consumer',
// 作为消费方,使用的时通过 ${remoteEntry}/${expose} 的方式使用, provider 是提供方 library 里的 name
remotes: {
remoteEntry: 'provider@http://localhost:9000/remoteEntry.js',
},
},
])
},
})
基于 Promise 的动态 Remote
一般来说,remote 是使用 URL 配置的,但是你也可以向 remote 传递一个 promise,其会在运行时被调用
javascript
// vue.config.js
module.exports = defineConfig({
//...
chainWebpack: config => {
// 模块联邦
config
.plugin('module-feaderation-plugin')
.use(require('webpack').container.ModuleFederationPlugin, [
{
// 当前模块的名称
name: 'consumer',
// 作为消费方,使用的时通过 ${remoteEntry}/${expose} 的方式使用, provider 是提供方 library 里的 name
remotes: {
remoteEntry: `promise new Promise(resolve => {
const origin = window.location.origin
// This part depends on how you plan on hosting and versioning your federated modules
let remoteUrlWithVersion
const NODE_ENV = origin.indexOf('localhost')>-1
if(NODE_ENV){
remoteUrlWithVersion = 'http://localhost:9000' + '/remoteEntry.js'
}else{
remoteUrlWithVersion = origin +'/public-app/remoteEntry.js'
}
const script = document.createElement('script')
script.src = remoteUrlWithVersion
script.onload = () => {
// the injected script has loaded and is available on window
// we can now resolve this Promise
const proxy = {
get: (request) => window.provider.get(request),
init: (arg) => {
try {
return window.provider.init(arg)
} catch(e) {
console.log('remote container already initialized')
}
}
}
resolve(proxy)
}
// inject this script with the src set to the versioned remoteEntry.js
document.head.appendChild(script);
})
`,
},
},
])
},
})