如何手动实现一个微前端框架雏形
一、了解微前端
1. 什么是微前端
为了解决一整块儿庞大的前端服务所带来的变更和拓展方面的限制,将整体前端服务拆分成一些更小、更简单的,能够独立开发、测试部署的小块儿。但是在整体表现上还是一个整体的产品的服务称为微前端。
2. 为什么要学习微前端
2.1 关键优势
每个分模块的规模更小,更利于维护
松散各个模块之间的耦合。
可以实现增量升级,避免在重构的时候影响整体逻辑
技术栈无关,可以在每个子模块之间选取合适的技术栈进行开发
独立开发独立部署
2.2 为什么要学
在重构项目的时候,总会有各种各样的问题,如
项目技术栈落后,重构时混用多种技术栈,导致项目技术栈混杂。
各个模块之间耦合严重,动一处,可能影响整体项目运转
因为各种历史问题不得不做出各种妥协,或者是添加各种兼容条件限制。
当你也有以上问题的时候,不妨考虑使用微前端,不仅可以拜托繁重的历史包袱,让你可以进行轻松重构,而且不会出现重构不彻底的情况,可以根据需求的实际情况进行重构工作,而不是基于项目历史债务的问题进行考虑。
这是需要学习微前端一个很重要的前提,如果你的项目没有任何历史包袱,或者说项目是从零开始的,这样就不推荐你引入微前端这个东西,这样或许不能达到预期的目的,或许只会加重自己的开发负担。
3. 手写一个框架可以给我们带来什么
可以从框架作者的角度去考虑,为什么框架的架构要这么设计,从中学习作者的设计思想,对于模型概念的理解。
二、微前端实现方式对比
1.iframe
优势:
天生支持沙箱隔离、独立运行,这是最大的优势。不用做任何沙箱的处理。
劣势:
无法预加载缓存 iframe
无法共享基础库
事件通信限制较多
快捷键劫持
事件无法冒泡到顶层
跳转路径无法保持统一
登录状态无法共享
iframe 加载失败,主应用无法感知
性能问题难以计算
2.基于 SPA 的微前端架构
优势
可以规避 iframe 现存的问题点
可缓存和预加载
共享登录状态
主应用感知加载状态
快捷键劫持
通信设计
共享基础库
劣势:
实现难度较高。需要实现以下几项内容
路由系统
沙箱隔离
样式隔离
通信
html 加载和 js 解析能力
调试开发能力
三、项目介绍
主要用到koa、vue2、vue3、react15、react16
应用:主应用、子应用、后端服务和发布应用
主应用-选定vue3技术栈
vue2子应用:实现新能源页面
vue3子应用:首页、选车
react15子应用:资讯、视频、视频详情
react16子应用:新车、排行、登录
服务端接口:koa实现
发布应用:express
service
npm install koa-generator -g
koa -v
Koa2 service
// 监听修改文件自动重启
npm install supervisor —save-dev
处理跨域问题
npm install koa2-cors —save-dev
build>run.js快速启动所有的应用
四、子应用接入微前端
子应用接入微前端-vue2
devServer: {
contentBase: path.join(__dirname, 'dist'),
hot: false,
disableHostCheck: true,
port,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
configureWebpack: {
resolve: {
alias: {
'@': resolve('src'),
},
},
output: {
library: `${packageName}`,
libraryTarget: 'umd',
},
},
2.
const render = () => {
new Vue({
router,
render: h => h(App)
}).$mount('#app-vue')
}
if (!window.__MICRO_WEB__) {
render()
}
export async function bootstrap() {
console.log('bootstrap');
}
export async function mount() {
render()
}
export async function unmount(ctx) {
const { container } = ctx
if (container) {
document.querySelector(container).innerHTML = ''
}
}
子应用接入微前端 - vue3
配置与vue2同理
子应用接入微前端 - react15
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'react15.js',
library: 'react15',
libraryTarget: 'umd',
umdNamedDefine: true,
publicPath: 'http://localhost:9002/'
},
devServer: {
headers: { 'Access-Control-Allow-Origin': '*' },
contentBase: path.join(__dirname, 'dist'),
compress: true,
port: 9002,
historyApiFallback: true,
hot: true,
}
const render = () => {
ReactDOM.render((
<BasicMap />
), document.getElementById('app-react'))
}
if (!window.__MICRO_WEB__) {
render()
}
export const bootstrap = () => {
console.log('bootstrap')
}
export const mount = () => {
render()
}
export const unmount = () => {
console.log('卸载')
}
子应用接入微前端 - react16
webpack打包回打包成 (function(){……})() 配置library之后会打包成 var react16 = (function(){……})() 当前变量内容是存在全局的
五、微前端框架开发
主框架—子应用注册
const routes = [
{
path: '/',
component: () => import('../App.vue'),
},
{
path: '/react15',
component: () => import('../App.vue'),
},
{
path: '/react16',
component: () => import('../App.vue'),
},
{
path: '/vue2',
component: () => import('../App.vue'),
},
{
path: '/vue3',
component: () => import('../App.vue'),
},
];
import { subNavList } from './store/sub'
import { registerApp } from './util'
registerApp(subNavList)
export const subNavList = [
{
name: 'react15',
entry: '//localhost:9002/',
container: '#micro-container',
activeRule: '/react15',
},
{
name: 'react16',
entry: '//localhost:9003/',
container: '#micro-container',
activeRule: '/react16',
},
{
name: 'vue2',
entry: '//localhost:9004/',
container: '#micro-container',
activeRule: '/vue2',
},
{
name: 'vue3',
entry: '//localhost:9005/',
container: '#micro-container',
activeRule: '/vue3',
},
];
import { registerMicroApps } from '../../micro'
export const registerApp = (list) => {
registerMicroApps(list)
}
import { setList } from './const/subApps'
export const registerMicroApps = (appList) => {
setList(appList)
}
let list = []
export const getList = () => list
export const setList = appList => list = appList
微前端框架 - 路由拦截
import { rewriteRouter } from './router/rewriteRouter'
rewriteRouter()
- main > micro > start.js调用rewriteRouter路由拦截
import { rewriteRouter } from './router/rewriteRouter'
rewriteRouter()
import { patchRouter } from '../utils'
import { turnApp } from './routerHandle'
export const rewriteRouter = () => {
window.history.pushState = patchRouter(window.history.pushState, 'micro_push')
window.history.replaceState = patchRouter(window.history.replaceState, 'micro_replace')
window.addEventListener('micro_push', turnApp)
window.addEventListener('micro_replace', turnApp)
window.onpopstate = async function () {
await turnApp()
}
}
export const patchRouter = (globalEvent, eventName) => {
return function () {
const e = new Event(eventName)
globalEvent.apply(this, arguments)
window.dispatchEvent(e)
}
}
export const turnApp = async () => {
console.log('路由切换了')
}
微前端框架-获取首个子应用
import { registerMicroApps } from '../../micro'
export const registerApp = (list) => {
registerMicroApps(list)
start()
}
import { setList, getList } from './const/subApps'
import { currentApp } from './utils'
export const start = () => {
const apps = getList()
if (!apps.length) {
throw Error('子应用列表为空, 请正确注册')
}
const app = currentApp()
const { pathname, hash } = window.location
if (app) {
const url = pathname + hash
window.__CURRENT_SUB_APP__ = app.activeRule
window.history.pushState('', '', url)
}
}
export const currentApp = () => {
const currentUrl = window.location.pathname
return filterApp('activeRule', currentUrl)
}
export const filterApp = (key, value) => {
const currentApp = getList().filter(item => item[key] === value)
return currentApp && currentApp.length ? currentApp[0] : {}
}
import { isTurnChild } from '../utils'
export const turnApp = async () => {
if (isTurnChild()) {
console.log('路由切换了')
}
}
export const isTurnChild = () => {
if(window.__CURRENT_SUB_APP__ === window.location.pathname) {
return false
}
return true;
}
微前端框架主应用生命周期
import { registerMicroApps, start } from '../../micro'
import { loading } from '../store'
export const registerApp = (list) => {
registerMicroApps(list, {
beforeLoad: [
() => {
loading.changeLoading(true)
console.log('开始加载')
}
],
mounted: [
() => {
loading.changeLoading(false)
console.log('渲染完成')
}
],
destoryed: [
() => {
console.log('卸载完成')
}
]
})
start()
}
export const registerMicroApps = (appList, lifeCycle) => {
setList(appList)
setMainLifecycle(lifeCycle)
}
let lifecycle = {}
export const getMainLifecycle = () => lifecycle
export const setMainLifecycle = data => lifecycle = data
微前端框架-微前端生命周期
export const turnApp = async () => {
if (isTurnChild()) {
await lifecycle()
}
}
import { findAppByRoute } from '../utils'
import { getMainLifecycle } from '../const/mainLifeCycle'
export const lifecycle = async () => {
const prevApp = findAppByRoute(window.__ORIGIN_APP__)
const nextApp = findAppByRoute(window.__CURRENT_SUB_APP__)
if (!nextApp) {
return
}
if (prevApp && prevApp.unmount) {
await destoryed(prevApp)
}
const app = await beforeLoad(nextApp)
await mounted(app)
}
export const beforeLoad = async (app) => {
await runMainLifeCycle('beforeLoad')
app && app.beforeLoad && app.beforeLoad()
const appContext = null
return appContext
}
export const mounted = async (app) => {
app && app.mount && app.mount()
await runMainLifeCycle('mounted')
}
export const destoryed = async (app) => {
app && app.unmount && app.unmount()
await runMainLifeCycle('destoryed')
}
export const runMainLifeCycle = async (type) => {
const mainlife = getMainLifecycle()
await Promise.all(mainlife[type].map(async item => await item()))
}
export const isTurnChild = () => {
window.__ORIGIN_APP__ = window.__CURRENT_SUB_APP__;
if(window.__CURRENT_SUB_APP__ === window.location.pathname) {
return false
}
window.__CURRENT_SUB_APP__ = window.location.pathname
return true;
}
export const findAppByRoute = (router) => {
return filterApp('activeRule', router)
}
export const filterApp = (key, value) => {
const currentApp = getList().filter(item => item[key] === value)
return currentApp && currentApp.length ? currentApp[0] : {}
}
获取需要展示的页面 - 加载和解析html
import { loadHtml } from '../loader'
export const beforeLoad = async (app) => {
await runMainLifeCycle('beforeLoad')
app && app.beforeLoad && app.beforeLoad()
const subApp = await loadHtml(app)
subApp && subApp.beforeLoad && subApp.beforeLoad()
return subApp
}
import { fetchResource } from '../utils/fetchResource'
export const loadHtml = async (app) => {
let container = app.container
let entry = app.entry
const html = await parseHtml(entry)
const ct = document.querySelector(container)
if (!ct) {
throw new Error('容器不存在,请查看')
}
ct.innerHTML = html
return app
}
export const parseHtml = async (entry, name) => {
const html = await fetchResource(entry)
return html
}
export const fetchResource = url => fetch(url).then(async res => await res.text())
加载和解析js
export const loadHtml = async (app) => {
let container = app.container
let entry = app.entry
const [dom, scripts] = await parseHtml(entry, app.name)
const ct = document.querySelector(container)
if (!ct) {
throw new Error('容器不存在,请查看')
}
ct.innerHTML = dom
return app
}
const cache = {}
export const parseHtml = async (entry, name) => {
if (cache[name]) {
return cache[name]
}
const html = await fetchResource(entry)
let allScript = []
const div = document.createElement('div')
div.innerHTML = html
const [dom, scriptUrl, script] = await getResources(div, entry)
const fetchedScripts = await Promise.all(scriptUrl.map(async item => fetchResource(item)))
allScript = script.concat(fetchedScripts)
cache[name] = [dom, allScript]
return [dom, allScript]
}
export const getResources = async (root, entry) => {
const scriptUrl = []
const script = []
const dom = root.outerHTML
function deepParse(element) {
const children = element.children
const parent = element.parent;
if (element.nodeName.toLowerCase() === 'script') {
const src = element.getAttribute('src');
if (!src) {
script.push(element.outerHTML)
} else {
if (src.startsWith('http')) {
scriptUrl.push(src)
} else {
scriptUrl.push(`http:${entry}/${src}`)
}
}
if (parent) {
parent.replaceChild(document.createComment('此 js 文件已经被微前端替换'), element)
}
}
if (element.nodeName.toLowerCase() === 'link') {
const href = element.getAttribute('href');
if (href.endsWith('.js')) {
if (href.startsWith('http')) {
scriptUrl.push(href)
} else {
scriptUrl.push(`http:${entry}/${href}`)
}
}
}
for (let i = 0; i < children.length; i++) {
deepParse(children[i])
}
}
deepParse(root)
return [dom, scriptUrl, script]
}
执行js脚本
export const isTurnChild = () => {
const { pathname } = window.location
let prefix = pathname.match(/(\/\w+)/)
if(prefix) {
prefix = prefix[0]
}
window.__ORIGIN_APP__ = window.__CURRENT_SUB_APP__;
if (window.__CURRENT_SUB_APP__===prefix) {
return false
}
const currentSubApp = window.location.pathname.match(/(\/\w+)/)
if (!currentSubApp) {
return false
}
window.__CURRENT_SUB_APP__ = currentSubApp[0];
return true;
}
import { performScript } from "../sandbox/performScript";
export const loadHtml = async (app) => {
let container = app.container
let entry = app.entry
const [dom, scripts] = await parseHtml(entry, app.name)
const ct = document.querySelector(container)
if (!ct) {
throw new Error('容器不存在,请查看')
}
ct.innerHTML = dom
scripts.forEach(item => {
performScript(item)
})
return app
}
export const performScript = (script) => {
eval(script)
}
eval('var a=1;var b=2;console.log(a+b)')
var s = 'return `hello ${name}`'
var func = new Function('name', s)
func('小明')
微前端环境变量
import { sandBox } from "../sandbox";
export const loadHtml = async (app) => {
let container = app.container
let entry = app.entry
const [dom, scripts] = await parseHtml(entry, app.name)
const ct = document.querySelector(container)
if (!ct) {
throw new Error('容器不存在,请查看')
}
ct.innerHTML = dom
scripts.forEach(item => {
sandBox(app, item)
})
return app
}
export const sandBox = (app, script) => {
window.__MICRO_WEB__ = true
const lifecycle = performScriptForEval(script, app.name)
if (isCheckLifeCycle(lifecycle)) {
app.bootstrap = lifecycle.bootstrap
app.mount = lifecycle.mount
app.unmount = lifecycle.unmount
}
}
const isCheckLifeCycle = lifecycle => lifecycle &&
lifecycle.bootstrap &&
lifecycle.mount &&
lifecycle.unmount
export const performScriptForEval = (script, appName) => {
const scriptText = `
() => {
${script}
return window['${appName}']
}
`
return eval(scriptText).call(window,window)
}
export const performScriptForFunction = (script, appName) => {
const scriptText = `
${script}
return window['${appName}']
`
return new Function(scriptText).call(window,window)
}
运行环境隔离 - 快照沙箱
export class SnapShotSandbox {
constructor() {
this.proxy = window
this.active()
}
active() {
this.snapshot = new Map()
for(const key in window) {
this.snapshot[key] = window[key]
}
}
inactive () {
for (const key in window) {
if (window[key] !== this.snapshot[key]) {
window[key] = this.snapshot[key]
}
}
}
}
import { performScriptForEval } from './performScript'
import { SnapShotSandbox } from './snapShotSandbox'
export const sandBox = (app, script) => {
const proxy = new SnapShotSandbox()
if (!app.proxy) {
app.proxy = proxy
}
window.__MICRO_WEB__ = true
const lifecycle = performScriptForEval(script, app.name, app.proxy.proxy)
if (isCheckLifeCycle(lifecycle)) {
app.bootstrap = lifecycle.bootstrap
app.mount = lifecycle.mount
app.unmount = lifecycle.unmount
}
}
export const performScriptForEval = (script, appName,global) => {
const scriptText = `
() => {
${script}
return window['${appName}']
}
`
return eval(scriptText).call(global,global)
}
export const performScriptForFunction = (script, appName,global) => {
const scriptText = `
${script}
return window['${appName}']
`
return new Function(scriptText).call(global,global)
}
export const lifecycle = async () => {
const prevApp = findAppByRoute(window.__ORIGIN_APP__)
const nextApp = findAppByRoute(window.__CURRENT_SUB_APP__)
if (!nextApp) {
return
}
if (prevApp && prevApp.unmount) {
if (prevApp.proxy) {
prevApp.proxy.inactive()
}
await destoryed(prevApp)
}
const app = await beforeLoad(nextApp)
await mounted(app)
}
运行环境隔离 - 代理沙箱
let a = {}
const proxy = new Proxy(a, {
get() {
console.log(111)
},
set(){
console.log(222)
return true
}
})
console.log(proxy.b)
proxy.a = 1
let defaultValue = {}
export class ProxySandbox{
constructor() {
this.proxy = null;
this.active()
}
active() {
this.proxy = new Proxy(window, {
get(target, key) {
if (typeof target[key] === 'function') {
return target[key].bind(target)
}
return defaultValue[key] || target[key]
},
set(target, key, value) {
defaultValue[key] = value
return true
}
})
}
inactive () {
defaultValue = {}
}
}
import { performScriptForEval } from './performScript'
import { ProxySandbox } from './proxySandbox'
export const sandBox = (app, script) => {
const proxy = new ProxySandbox()
if (!app.proxy) {
app.proxy = proxy
}
window.__MICRO_WEB__ = true
const lifecycle = performScriptForEval(script, app.name, app.proxy.proxy)
if (isCheckLifeCycle(lifecycle)) {
app.bootstrap = lifecycle.bootstrap
app.mount = lifecycle.mount
app.unmount = lifecycle.unmount
}
}
export const performScriptForFunction = (script, appName, global) => {
window.proxy = global
console.log(global)
const scriptText = `
return ((window) => {
${script}
return window['${appName}']
})(window.proxy)
`
return new Function(scriptText)()
}
export const performScriptForEval = (script, appName, global) => {
window.proxy = global
const scriptText = `
((window) => {
${script}
return window['${appName}']
})(window.proxy)
`
return eval(scriptText)
}
const proxy = new Proxy(window,{})
proxy.addEventListener('a',()=>{
console.log(1)
})
const proxy = new Proxy(window,{
get(target, key) {
if (typeof target[key] === 'function') {
return target[key].bind(target)
}
},
})
css样式隔离
const box1 = document.getElementById('box1')
const shadow1 = box1.attachShadow({mode:'open'})
const one = document.createElement('div')
one.className = 'one'
one.innerText = '第一个内容'
const style1 = document.createElement('style')
style1.textContent = `
.one{
color:red;
}
`
shadow1.appendChild(one1);
shadow1.appendChild(style1);
const box2 = document.getElementById('box2')
const shadow2 = box2.attachShadow({mode:'open'})
const two = document.createElement('div')
two.className = 'one'
two.innerText = '第一个内容'
const style2 = document.createElement('style')
style2.textContent = `
.one{
color:blue;
}
`
shadow2.appendChild(one2);
shadow2.appendChild(style2);
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
{
test: /\.(cs|scs)s$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
},
应用间通信 - 父子通信
如登录状态父子应用不同步问题,主应用状态变更可以通过某种方法及时通知我们的子应用,子应用状态变更也要通过方法通知主应用进行修改
import { ref } from 'vue';
export const headerStatus = ref(true)
export const changeHeader = type => headerStatus.value = type;
import { ref } from 'vue';
export const navStatus = ref(true)
export const changeNav = type => navStatus.value = type;
export * as header from './header'
export * as nav from './nav'
import { loading } from '../store'
import * as appInfo from '../store'
export const subNavList = [
{
name: 'react15',
entry: '//localhost:9002/',
loading,
container: '#micro-container',
activeRule: '/react15',
appInfo,
},
{
name: 'react16',
entry: '//localhost:9003/',
loading,
container: '#micro-container',
activeRule: '/react16',
appInfo,
},
{
name: 'vue2',
entry: '//localhost:9004/',
loading,
container: '#micro-container',
activeRule: '/vue2',
appInfo,
},
{
name: 'vue3',
entry: '//localhost:9005/',
loading,
container: '#micro-container',
activeRule: '/vue3',
appInfo,
},
];
export const mounted = async (app) => {
app && app.mount && app.mount({
appInfo: app.appInfo,
entry: app.entry
})
await runMainLifeCycle('mounted')
}
export const mount = (app) => {
app.appInfo.header.changeHeader(false)
app.appInfo.nav.changeNav(false)
render()
}
let main = null
export const setMain = (data) => {
main = data
}
export const getMain = () => {
return main
}
import {setMain} from './src/utils/main';
export const mount = (app) => {
setMain(app)
render()
}
import { getMain } from '../../utils/main'
useEffect(() => {
const main = getMain()
main.appInfo.header.changeHeader(false)
main.appInfo.nav.changeNav(false)
}, [])
export class Custom {
on (name, cb) {
window.addEventListener(name, (e) => {
cb(e.detail)
})
}
emit(name, data) {
const event = new CustomEvent(name, {
detail: data
})
window.dispatchEvent(event)
}
}
import { Custom } from './customevent'
const custom = new Custom()
custom.on('test', (data) => {
console.log(data)
})
window.custom = custom
export const mount = () => {
window.custom.emit('test',{
a:1
})
render()
}
应用间通信 - 子应用间通信
对于微前端而言,需要获取上一个应用的内容,表明当前的应用设计是有一些缺陷的,理论上来说,我们每一个应用之间都不会依赖于上一个或者下一个应用间的信息,这些信息我们都可以通过主应用或者数据服务来获取,但也避免不了一些及其特殊的情况,我们需要用到上一个子应用处理过的信息,在这子应用里做其他处理
export const mount = () => {
window.custom.on('test1',(data)=>{
console.log(data)
})
render()
}
export const mount = () => {
window.custom.emit('test1',{
a:1
})
render()
}
export const mount = () => {
window.custom.on('test2',(data)=>{
console.log(data,'======')
})
window.custom.emit('test1',{
a:1
})
render()
}
export const mount = () => {
window.custom.on('test1',(data)=>{
window.custom.emit('test2',{
b:2
})
})
render()
}
全局状态管理 - 全局store
export const createStore = (initData = {}) => (() => {
let store = initData
const observers = []
const getStore = () => store
const update = (value) => {
if (value !== store) {
const oldValue = store
store = value
observers.forEach(async item => await item(store, oldValue))
}
}
const subscribe = (fn) => {
observers.push(fn)
}
return {
getStore,
update,
subscribe,
}
})()
import { createStore } from '../../micro'
const store = createStore()
window.store = store
store.subscribe((newValue, oldValue) => {
console.log(newValue, oldValue, '---')
})
export async function mount(app) {
const storeData = window.store.getStore()
window.store.update({
...storeData,
a:11
})
render();
}
提高加载性能 - 应用缓存
export const loadHtml = async (app) => {
let container = app.container
let entry = app.entry
const [dom, scripts] = await parseHtml(entry, app.name)
const ct = document.querySelector(container)
if (!ct) {
throw new Error('容器不存在,请查看')
}
ct.innerHTML = dom
return app
}
const cache = {}
export const parseHtml = async (entry, name) => {
if (cache[name]) {
return cache[name]
}
const html = await fetchResource(entry)
let allScript = []
const div = document.createElement('div')
div.innerHTML = html
const [dom, scriptUrl, script] = await getResources(div, entry)
const fetchedScripts = await Promise.all(scriptUrl.map(async item => fetchResource(item)))
allScript = script.concat(fetchedScripts)
cache[name] = [dom, allScript]
return [dom, allScript]
}
提高加载性能 - 预加载子应用
import { prefetch } from './loader/prefetch'
export const start = () => {
const apps = getList()
if (!apps.lenth) {
throw Error('子应用列表为空, 请正确注册')
}
const app = currentApp()
const { pathname, hash } = window.location
if (!hash) {
window.history.pushState(null, null, '/vue3#/index')
}
if (app && hash) {
const url = pathname + hash
window.__CURRENT_SUB_APP__ = app.activeRule
window.history.pushState('', '', url)
}
prefetch()
}
import { getList } from '../const/subApps';
import { parseHtml } from './index';
export const prefetch = async () => {
const list = getList().filter(item => !window.location.pathname.startsWith(item.activeRule))
await Promise.all(list.map(async item => await parseHtml(item.entry, item.name)))
}
六、框架发布 – 通过npm发布框架
如果其他主应用也想用微前端框架,只能copy过去使用,这样效率慢,准确性得不到保障,如果微前端框架需要升级,维护起来麻烦,每一份主应用都需要更改,可以将框架通过npm发布,以后主应用通过npm安装依赖就行
七、应用部署 - 创建自动部署平台
八、实现应用的自动化部署
九、质量保证 - 如何实现主子应用测试
十、使用qiankun重构项目
真正想在生辰环境实现一个微前端框架,要做的事情是很多的,要经过大量的试错,验证来验证自己的框架是没有问题的,现在有很多成熟的微前端框架,我们可以直接用
qiankun源码分析-应用注册
十一、使用single-spa重构项目
single-spa.js.org
qiankun与single-spa
|