微前端
概念介绍:
- 核心:拆分巨型应用,允许技术栈不同【不建议同时使用多个不同技术栈】,并且最后聚合为一;
- 拆分的应用可以独立开发、独立部署;
- 类似于微服务架构,但微服务只拆分不用聚合;
- 最佳使用场景:一些B端管理系统,既能兼容集成历史系统,也可以将新的系统集成进来,且不影响原先的交互体验;
- 缺点:
- 应用拆分依赖基础设施构建,一旦大量应用依赖于同一基础设施,维护会变成一个挑战;
- 拆分粒度越小,架构会变得复杂,维护成本变高;
- 技术栈一旦多元化,意味着技术栈混乱;
架构实现:
中心化:基座模式
- Single-Spa --2018年
- Qiankun --2019年 基于Single-Spa + sandbox + import-html-entry
去中心化:自组织模式
- webpack5 模块联邦 --2020年 诞生背景:跨应用加载模块
- EMP基于 Module Federation (模块联邦) --2020年,接入成本低,解决第三方依赖包问题
- lcestark 阿里飞冰微前端框架
qiankun框架介绍
- 主应用要安装qiankun,子应用不需要安装qiankun;
- 子应用要注意解决跨域问题,必须打包出
umd库格式;
微前端运行原理:
项目环境准备:
- 准备一个项目,项目包含中心基座父应用(vue2),以及子应用(vue2、react);
- 子组件在入口文件中完成
bootstrap、mount、unmount三个钩子函数导出;以及配置文件中完成导出格式配置,开启跨域解决跨域问题;
import './public-path' // 注意:动态设置 public-path要放在入口文件的顶部
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'
Vue.config.productionTip = false
let instance = null
let router = null
let store = null
function render(props) {
console.log(props)
router = createRouter()
store = createStore()
instance = new Vue({
router,
store,
render: h => h(App)
}).$mount(props.container
? props.container.querySelector('#app') // 渲染到子应用
: '#app' // 子应用独立运行
)
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
// 不需要安装任何依赖
// 只需要在入口文件导出三个必须的钩子函数,给主应用使用
// 钩子函数必须返回 Promise
// 启动调用
export const bootstrap = async () => {
console.log('app-vue2 bootstrap')
}
// 挂载调用
export const mount = async (props) => {
console.log('app-vue2 mount')
// props.onGlobalStateChange((state, prev)=>{
// console.log('app-vue2 =>',state, prev)
// }, true) // 传一个true 一上来就调用
render(props)
}
// 卸载调用
export const unmount = async () => {
console.log('app-vue2 unmount')
// 清除Vue实例运行期间产生的内容
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
// 清除路由实例
router = null;
// 清除容器实例
store = null;
}
-
父组件在入口文件需要完成注册子应用和启动微前端;
micro-fe是自定义的微前端模块,用于完成理解微前端架构原理
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { registerMicroApps, start } from './micro-fe'
new Vue({
router,
render: h => h(App)
}).$mount('#app')
// 1. 注册子应用
registerMicroApps([
{
// 当访问 activeRule 的时候,用js请求加载 entry,然后渲染到 container 上
name: 'vue2App', // app name registered
entry: '//localhost:8082',
container: '#subapp-container',
activeRule: '/app-vue2',
},
// {
// name: 'vue3App', // app name registered
// entry: '//localhost:8083',
// container: '#subapp-container',
// activeRule: '/yourActiveRule',
// },
{
name: 'reactApp', // app name registered
entry: '//localhost:8081',
container: '#subapp-container',
activeRule: '/app-react',
},
])
// 2.启动微前端架构系统
start()
微前端原理简单实现
微前端简单运行原理:
- 监视路由变化;2.匹配子应用;3.加载子应用;4.渲染子应用;
-
监视路由变化:此部分在到
rewrite-router.js模块中let prevRoute = ''; // 记录上一个路由 let nextRoute = ''; // 记录当前路由 也就是上一个路由跳转后的下个路由 export const getPrevRoute = () => prevRoute export const getNextRoute = () => nextRoute export const rewriteRouter = () =>{ // 完成1.监视路由变化;2.匹配子应用 // ... }-
hash 路由:
window.onhashchange -
history 路由:
用户在浏览器前进后退的行为调用的history.go、history.back、history.forward的方法,以使用 popstate 事件劫持:window.onpopstate rewriteRouter方法部分代码:
const rewriteRouter = () =>{ // ... window.addEventListener('popstate', ()=>{ console.log('popstate') hanleRouter() // ... }) }通过点击行为调用的pushState、replaceState方法,需要通过函数重写的方式进行劫持;
// ... // pushState const rawPushState = window.history.pushState; //做备份 window.history.pushState = (...args) =>{ // 重写pushState rawPushState.apply(window.history, args) console.log('监视到pushState的变化') hanleRouter() } // replaceState const rawReplaceState = window.history.replaceState; //做备份 window.history.replaceState = (...args) =>{ // 重写pushState rawReplaceState.apply(window.history, args) hanleRouter() console.log('监视到replaceState的变化') } // ...
-
-
匹配子应用;
此部分在到
rewrite-router.js模块中:处理路由变化
hanleRouter()方法,此方法在监视路由后执行;export const handleRouter = () =>{}- 获取当前路由路径
window.location.pathname- 在传进来的 apps 配置中查找是否匹配
import { getApps } from '.' // import ... export const handleRouter = async () =>{ const apps = getApps() const app = apps.find(item => window.loacation.pathname.startsWith(item.activeRule)) if(!app) return }- 完成前进后退行为
// ... // 引入获取到的当前路由以及上一个路由 import { getPrevRoute,getNextRoute } from './rewrite-router'; export const handleRouter = async () =>{ // ... // 卸载上一个应用 const prevApp = apps.find(item => getPrevRoute().startsWith(item.activeRule)) // 如果有上一个应用,则先销毁 if(prevApp) { await unmount(prevApp) } // ... } -
加载子应用;
请求获取子应用的资源:HTML、CSS、JSexport const handleRouter = async () =>{ // ... // 加载子应用到父应用指定的容器中 const html = await fetch(app.entry).then(res => res.text()) const container = document.querySelector(app.container); container.innerHTML = html; // 此时并不能成功显示子应用,但已经获取到了html文本 }上述方式并不能成功显示子应用,原因是:
-
客户端渲染需要执行 JavaScript 来生成内容
-
浏览器出于安全考虑,innerHTML 中的script不会加载执行,需要手动加载;
// 处理子应用中HTML的script内容 import { fetchResource } from './fetch-resource' // 获取子应用入口html文本 export const importHtml = async (url) => { // qiankun中加载文档html使用封装的库是import-html-entry // import-html-entry => 其会返回的几个重要的数据:template 处理之后的html模板字符串、getExternalScript 所有script脚本代码等... const html = await fetchResource(url); const template = document.createElement('div'); // template.innerHTML = '<p>hello</p>' // test template.innerHTML = html // 将HTML文本转出DOM,就可以进行下一步获取script脚本并手动加载 // 获取script脚本 const scripts = template.querySelectorAll('script') // 获取所有script标签的代码文本,因为是DOM节点返回伪数组[code, code, ...] function getExternalScripts() { // 将伪数组转为数组 return Promise.all(Array.from(scripts).map(script => { //有两种 script 代码,一种是src引用,另一种是在标签体内 const src = script.getAttribute('src') if (!src) { // 如果是引用直接返回在promise参数 return Promise.resolve(script.innerHTML) } else { // 如果是标签体内,改写成完整的url返回promise参数 return fetchResource( src.startsWith('http') ? src : `${url}${src}` ) } })) } // 获取并执行所有的script 脚本代码 async function execScripts() { // 获取改造后的 scripts 数组 const scripts = await getExternalScripts() // 手动加载 script 代码 // ... } // ... return { template, getExternalScripts, // execScripts } }完成手动加载 script 代码前提,要了解umd库打包格式;
vue2子应用配置
mode:'development'npm run build打包后观察dist中的js文件// umd打包库格式 => 兼容不同的模块规范 (function webpackUniversalModuleDefinition(root, factory) { // root => window // factory => function() { // 子应用代码 ... // 导出处结果 return { ... } } // 判断是否是CommonJS 模块规范 if (typeof exports === 'object' && typeof module === 'object') module.exports = factory(); // 判断是否是 AMD 模块规范 else if (typeof define === 'function' && define.amd) define([], factory); // 判断是否是 CommonJS 模块规范 else if (typeof exports === 'object') exports["vue2App"] = factory(); // window[xxx] = factory() else //判断是否是属性名xxx子组件 root["vue2App"] = factory(); })(window, function () { // 这是vue2子组件内部的code // ... // 最后返回的导出结果 return { a: 1, b: 2 } });我们如果使用匹配名字就无法兼顾多个子应用,所有直接手动构造一个 CommonJS 模块的环境
// ... // 获取并执行所有的script 脚本代码 async function execScripts() { const scripts = await getExternalScripts() // 手动构造一个 CommonJS 模块环境 const module = {exports: {}} const exports = module.exports console.log(scripts) scripts.forEach(code=>{ eval(code) }) // 获取子应用代码执行后导出的三个钩子 console.log(module.exports) return module.exports } return { template, getExternalScripts, execScripts } // ...handle-router.js引用调用// ... import { importHtml } from './import-html'; export const handleRouter = async () =>{ // ... // 3.加载子应用到父应用指定的容器中 const container = document.querySelector(app.container); const { template, execScripts } = await importHtml(app.entry); container.appendChild(template); } -
-
渲染子应用
import { importHtml } from './import-html'; export const handleRouter = async () =>{ // ... const appExports = await execScripts(); app.bootstrap = appExports.bootstrap app.mount = appExports.mount app.unmount = appExports.unmount await bootstrap(app) await mount(app) // ... } // 封装一层钩子函数 async function bootstrap(app) { app.bootstrap && (await app.bootstrap()) } async function mount(app) { app.mount && (await app.mount({ // 将容器传入 container: document.querySelector(app.container) })) } async function unmount(app) { app.unmount && (await app.unmount({ // 将容器传入 container: document.querySelector(app.container) })) } -
配置全局变量,解决子应用独立运行与静态资源加载问题
handle-router.js中// ... handleRouter // 类似 qiankun 配置子应用全局变量 子应用用于判断是否独立运行 window.__POWERED_BY_QIANKUN__ = true; window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = app.entry + '/'; // ... handleRouter