闽公网安备 35020302035485号

3.webpack5实现的Module Federation

//main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { registerApplication, start } from 'single-spa'
Vue.config.productionTip = false
const mountApp = (url) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = url
script.onload = resolve
script.onerror = reject
// 通过插入script标签的方式挂载子应用
const firstScript = document.getElementsByTagName('script')[0]
// 挂载子应用
firstScript.parentNode.insertBefore(script, firstScript)
})
}
const loadApp = (appRouter, appName) => {
// 远程加载子应用
return async () => {
//手动挂载子应用
await mountApp(appRouter + '/js/chunk-vendors.js')
await mountApp(appRouter + '/js/app.js')
// 获取子应用生命周期函数
return window[appName]
}
}
// 子应用列表
const appList = [
{
// 子应用名称
name: 'app1',
// 挂载子应用
app: loadApp('http://localhost:8083', 'app1'),
// 匹配该子路由的条件
activeWhen: location => location.pathname.startsWith('/app1'),
// 传递给子应用的对象
customProps: {}
},
{
name: 'app2',
app: loadApp('http://localhost:8082', 'app2'),
activeWhen: location => location.pathname.startsWith('/app2'),
customProps: {}
}
]
// 注册子应用
appList.map(item => {
registerApplication(item)
})
// 注册路由并启动基座
new Vue({
router,
mounted() {
start()
},
render: h => h(App)
}).$mount('#app')
构建基座的核心是:配置子应用信息,通过registerApplication注册子应用,在基座工程挂载阶段start启动基座。import Vue from 'vue'
import App from './App.vue'
import router from './router'
import singleSpaVue from 'single-spa-vue'
Vue.config.productionTip = false
const appOptions = {
el: '#microApp',
router,
render: h => h(App)
}
// 支持应用独立运行、部署,不依赖于基座应用
// 如果不是微应用环境,即启动自身挂载的方式
if (!process.env.isMicro) {
delete appOptions.el
new Vue(appOptions).$mount('#app')
}
// 基于基座应用,导出生命周期函数
const appLifecycle = singleSpaVue({
Vue,
appOptions
})
// 抛出子应用生命周期
// 启动生命周期函数
export const bootstrap = (props) => {
console.log('app2 bootstrap')
return appLifecycle.bootstrap(() => { })
}
// 挂载生命周期函数
export const mount = (props) => {
console.log('app2 mount')
return appLifecycle.mount(() => { })
}
// 卸载生命周期函数
export const unmount = (props) => {
console.log('app2 unmount')
return appLifecycle.unmount(() => { })
}
配置子应用为umd打包方式//vue.config.js
const package = require('./package.json')
module.exports = {
// 告诉子应用在这个地址加载静态资源,否则会去基座应用的域名下加载
publicPath: '//localhost:8082',
// 开发服务器
devServer: {
port: 8082
},
configureWebpack: {
// 导出umd格式的包,在全局对象上挂载属性package.name,基座应用需要通过这个
// 全局对象获取一些信息,比如子应用导出的生命周期函数
output: {
// library的值在所有子应用中需要唯一
library: package.name,
libraryTarget: 'umd'
}
}
配置子应用环境变量// .env.micro NODE_ENV=development VUE_APP_BASE_URL=/app2 isMicro=true子应用配置的核心是用singleSpaVue生成子路由配置后,必须要抛出其生命周期函数。用以上方式便可轻松实现一个简单的微前端应用了。那么我们有single-spa这种微前端解决方案,为什么还需要qiankun呢?
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'reactApp',
entry: '//localhost:3000',
container: '#container',
activeRule: '/app-react',
},
{
name: 'vueApp',
entry: '//localhost:8080',
container: '#container',
activeRule: '/app-vue',
},
{
name: 'angularApp',
entry: '//localhost:4200',
container: '#container',
activeRule: '/app-angular',
},
]);
// 启动 qiankun
start();
子应用配置 if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
2.设置 history 模式路由的 base: <BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/app-react' : '/'}>
3.入口文件 index.js 修改,为了避免根 id #root 与其他的 DOM 冲突,需要限制查找范围。 import './public-path';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
function render(props) {
const { container } = props;
ReactDOM.render(<App />, container ? container.querySelector('#root') :
document.querySelector('#root'));
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
export async function bootstrap() {
console.log('[react16] react app bootstraped');
}
export async function mount(props) {
console.log('[react16] props from main framework', props);
render(props);
}
export async function unmount(props) {
const { container } = props;
ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') :
document.querySelector('#root'));
}
4.修改 webpack 配置npm i -D @rescripts/cli根目录新增 .rescriptsrc.js:
const { name } = require('./package');
module.exports = {
webpack: (config) => {
config.output.library = `${name}-[name]`;
config.output.libraryTarget = 'umd';
config.output.jsonpFunction = `webpackJsonp_${name}`;
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;
},
};
以上对Qiankun的使用可以看出,与single-spa使用过程很相似。不同的是,Qiankun的使用过程更简便了。一些内置的操作交由给Qiankun内部实现。这是一种IOC思想的实现,我们只管面向容器化开发,其他操作交给Qiankun框架管理。// 基于HTMLElement自定义组件元素
class CounterElement extends HTMLElement {
// 在构造器中生成shadow节点
constructor() {
super();
this.counter = 0;
// 打开影子节点
// 影子节点是为了隔离外部元素的影响
const shadowRoot = this.attachShadow({ mode: 'open' });
// 定义组件内嵌样式
const styles = `
#counter-increment {
width: 60px;
height: 30px;
margin: 20px;
background: none;
border: 1px solid black;
}
`;
// 定义组件HTMl结构
shadowRoot.innerHTML = `
<style>${styles}</style>
<h3>Counter</h3>
<slot name='counter-content'>Button</slot>
<span id='counter-value'>; 0 </span>;
<button id='counter-increment'> + </button>
`;
// 获取+号按钮及数值内容
this.incrementButton = this.shadowRoot.querySelector('#counter-increment');
this.counterValue = this.shadowRoot.querySelector('#counter-value');
// 实现点击组件内事件驱动
this.incrementButton.addEventListener("click", this.decrement.bind(this));
}
increment() {
this.counter++
this.updateValue();
}
// 替换counter节点内容,达到更新数值的效果
updateValue() {
this.counterValue.innerHTML = this.counter;
}
}
// 在真实dom上,生成自定义组件元素
customElements.define('counter-element', CounterElement);
有了对WebComponent的理解,接下来,我们更明白了Micro-app的优势。
// index.js
import React from "react"
import ReactDOM from "react-dom"
import App from './App'
import microApp from '@micro-zoe/micro-app'
const appName = 'my-app'
// 预加载
microApp.preFetch([
{ name: appName, url: 'xxx' }
])
// 基座向子应用数据通信
microApp.setData(appName, { type: '新的数据' })
// 获取指定子应用数据
const childData = microApp.getData(appName)
microApp.start({
// 公共文件共享
globalAssets: {
js: ['js地址1', 'js地址2', ...], // js地址
css: ['css地址1', 'css地址2', ...], // css地址
}
})
分配一个路由给子应用// router.js
import { BrowserRouter, Switch, Route } from 'react-router-dom'
export default function AppRoute () {
return (
<BrowserRouter>
<Switch>
<Route path='/'>
<micro-app name='app1' url='http://localhost:3000/' baseroute='/'></micro-app>
</Route>
</Switch>
</BrowserRouter>
)
}
子应用的简易配置// index.js
import React from "react"
import ReactDOM from "react-dom"
import App from './App'
import microApp from '@micro-zoe/micro-app'
const appName = 'my-app'
// 子应用运行时,切换静态资源访问路径
if (window.__MICRO_APP_ENVIRONMENT__) {
__webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__
}
// 基子应用向基座发送数据
// dispatch只接受对象作为参数
window.microApp.dispatch({ type: '子应用发送的数据' })
// 获取基座数据
const data = window.microApp.getData() // 返回基座下发的data数据
//性能优化,umd模式
// 如果子应用渲染和卸载不频繁,那么使用默认模式即可,如果子应用渲染和卸载非常频繁建议使用umd模式
// 将渲染操作放入 mount 函数 -- 必填
export function mount() {
ReactDOM.render(<App />, document.getElementById("root"))
}
// 将卸载操作放入 unmount 函数 -- 必填
export function unmount() {
ReactDOM.unmountComponentAtNode(document.getElementById("root"))
}
// 微前端环境下,注册mount和unmount方法
if (window.__MICRO_APP_ENVIRONMENT__) {
window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount }
} else {
// 非微前端环境直接渲染
mount()
}
设置子应用路由import { BrowserRouter, Switch, Route } from 'react-router-dom'
export default function AppRoute () {
return (
// 设置基础路由,子应用可以通过window.__MICRO_APP_BASE_ROUTE__获取基座下发的baseroute,
// 如果没有设置baseroute属性,则此值默认为空字符串
<BrowserRouter basename={window.__MICRO_APP_BASE_ROUTE__ || '/'}>
...
</BrowserRouter>
)
}
以上便是Micro-app的用法。
// 配置webpack.config.js
const { ModuleFederationPlugin } = require("webpack").container;
new ModuleFederationPlugin({
name: "appA",
//出口文件
filename: "remoteEntry.js",
//暴露可访问的组件
exposes: {
"./input": "./src/input",
},
//或者其他模块的组件
//如果把这一模块当作基座模块的话,
//这里应该配置其他子应用模块的入口文件
remotes: {
appB: "appB@http://localhost:3002/remoteEntry.js",
},
//共享依赖,其他模块不需要再次下载,便可使用
shared: ['react', 'react-dom'],
})