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'], })