项目启动
根目录下的package.json配置如下
{
"name": "micro-web-project",
"version": "1.0.0",
"description": "create micro project for myself",
"main": "index.js",
"scripts": {
"start": "node ./build/run.js"
},
"author": "yancy",
"license": "ISC",
"devDependencies": {},
"dependencies": {}
}
其中"start": "node ./build/run.js"文件创建多个子进程进行多个项目启动
const childProcess = require('child_process')
const path = require('path')
const filePath = {
vue2: path.join(__dirname, '../vue2'),
vue3: path.join(__dirname, '../vue3'),
react15: path.join(__dirname, '../react15'),
react16: path.join(__dirname, '../react16'),
// 启动主应用;稍后主应用生命周期构建后取消注释
// main: path.join(__dirname, '../main')
}
// cd 子应用的目录 npm start 启动项目
function runChild () {
Object.values(filePath).forEach(item => {
childProcess.spawn(`cd ${item} && npm start`, { stdio: "inherit", shell: true })
})
}
runChild()
子应用打包配置
vue2
vue.config.js
const path = require('path');
function resolve(dir) {
return path.join(__dirname, dir);
}
const packageName = 'vue2';
const port = 9004;
module.exports = {
outputDir: 'dist', // 打包的目录
assetsDir: 'static', // 打包的静态资源
filenameHashing: true, // 打包出来的文件,会带有hash信息
publicPath: 'http://localhost:9004',
devServer: {
contentBase: path.join(__dirname, 'dist'),
hot: false,
disableHostCheck: true,
port,
headers: {
'Access-Control-Allow-Origin': '*', // 本地服务的跨域内容
},
},
// 自定义webpack配置
configureWebpack: {
resolve: {
alias: {
'@': resolve('src'),
},
},
output: {
// 把子应用打包成 umd 库格式 commonjs 浏览器,node环境
library: `${packageName}`,
libraryTarget: 'umd',
},
},
};
vue3
const path = require('path');
const packageName = 'vue3'
function resolve(dir) {
return path.join(__dirname, dir);
}
const port = 9005;
module.exports = {
outputDir: 'dist',
assetsDir: 'static',
filenameHashing: true,
publicPath: 'http://localhost:9005',
devServer: {
contentBase: path.join(__dirname, 'dist'),
hot: true,
disableHostCheck: true,
port,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
// 自定义webpack配置
configureWebpack: {
resolve: {
alias: {
'@': resolve('src'),
},
},
output: {
// 把子应用打包成 umd 库格式
filename: 'vue3.js',
library: `${packageName}`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${packageName}`,
},
},
};
react15
webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
entry: {
path: ['./index.js']
},
module: {
rules: [
{
test: /\.js(|x)$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
}
},
{
test: /\.(c|sc)ss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
},
{
test: /\.(png|svg|jpg|gif)$/,
use: {
loader: 'url-loader',
}
}
]
},
optimization: {
splitChunks: false,
minimize: false
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
}),
new MiniCssExtractPlugin({
filename: '[name].css'
})
],
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,
}
}
react16
webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
entry: { path: ['regenerator-runtime/runtime', './index.js'] },
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'react16.js',
library: 'react16',
libraryTarget: 'umd',
umdNamedDefine: true,
publicPath: 'http://localhost:9003'
},
module: {
rules: [
{
test: /\.js(|x)$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
}
},
{
test: /\.(cs|scs)s$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
},
]
},
optimization: {
splitChunks: false,
minimize: false
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
}),
new MiniCssExtractPlugin({
filename: '[name].css'
})
],
devServer: {
headers: { 'Access-Control-Allow-Origin': '*' },
contentBase: path.join(__dirname, 'dist'),
compress: true,
port: 9003,
historyApiFallback: true,
hot: true
}
}
子应用入口
vue2的mian.js入口
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
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的mian.js入口
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import { setMain } from './utils/global'
let instance = null;
function render() {
instance = createApp(App);
instance
.use(router)
.mount('#app');
}
if (!window.__MICRO_WEB__) {
render();
}
export async function bootstrap() {
console.log('vue3.0 app bootstrap');
}
export async function mount(app) {
setMain(app)
render();
}
export async function unmount(ctx) {
instance.unmount();
instance = null;
const { container } = ctx
if (container) {
document.querySelector(container).innerHTML = ''
}
}
setMain所在文件代码如下
export let main = {}
export const setMain = (data) => {
main = data
}
react15的index.js入口
import React from 'react'
import ReactDOM from 'react-dom'
import BasicMap from './src/router/index.jsx';
import "./index.scss"
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和15一致
子应用注册
在主应用中注册子应用
主应用vue3的mian.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { subNavList } from './store/sub'
import { registerApp } from './util'
registerApp(subNavList)
createApp(App).use(router()).mount('#micro_web_main_app')
需要注册的导航菜单,注册到微前端框架
import { subNavList } from './store/sub'中的sub
import { loading } from '../store'
import * as appInfo from '../store'
export const subNavList = [
{
name: 'react15',// 唯一
entry: '//localhost:9002/', // 子应用的入口
loading,
container: '#micro-container', // 子应用注册到的容器,即App.vue中放置微前端子应用的dom容器
<!-- <template>
<Header v-show="headerStatus"/>
<MainNav v-show="navStatus"/>
<div class="sub-container">
<Loading v-show="loading"/>
<div v-show="!loading" id="micro-container">子应用内容</div>
</div>
</template> -->
activeRule: '/react15', // 活动路由,即非hash前面的window.pathname。如www.baidu.com/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,
},
];
路由配置
import router from './router'
活动路由,即非hash前面的window.pathname。如www.baidu.com/react15/#/。其最终都从App.vue进入
import { createRouter, createWebHistory } from 'vue-router';
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'),
},
];
const router = (basename = '') => createRouter({
history: createWebHistory(basename),
routes,
});
export default router;
注册子应用(main/micro)微前端中处理
在主应用下创建micro文件夹用于微前端
registerMicroApps方法提供给主应用调用
micro/index.js
export { registerMicroApps, start } from './start'
启动微前端
micro/start.js
import { setList, getList } from './const/subApps'
export const registerMicroApps = (appList) => {
setList(appList)
}
存储appList
micro/const/subApps.js
let list = []
export const getList = () => list
export const setList = appList => list = appList
注册子应用(main/src/main.js)主应用中处理,调用main/micro的注册
registerApp(subNavList)
import { registerApp } from './util'
util/index.js
import { registerMicroApps } from '../../micro'
export const registerApp = (list) => {
// 注册到微前端框架里
registerMicroApps(list)
}
路由拦截
micro/start.js
// 实现路由拦截
+ import { rewriteRouter } from './router/rewriteRouter'
import { setList, getList } from './const/subApps'
+ rewriteRouter()
export const registerMicroApps = (appList) => {
setList(appList)
}
micro/router/rewriteRouter.js
import { patchRouter } from '../utils'
import { turnApp } from './routerHandle'
// 重写window的路由跳转
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()
}
}
import { turnApp } from './routerHandle'
micro/router/routerHandle.js
export const turnApp = async () => {
console.log('isrunning')
}
import { patchRouter } from 'micro/utils
// 给当前的路由跳转打补丁
export const patchRouter = (globalEvent, eventName) => {
return function () {
const e = new Event(eventName)
globalEvent.apply(this, arguments)
window.dispatchEvent(e)
}
}
获取首个子应用
在主应用的src/util/index.js中
+ import { registerMicroApps, start } from '../../micro'
export const registerApp = (list) => {
// 注册到微前端框架里
registerMicroApps(list)
// 开启微前端框架
+ start()
}
micro/start.js
// 实现路由拦截
import { rewriteRouter } from './router/rewriteRouter'
+ import { currentApp } from './utils'
import { setList, getList } from './const/subApps'
rewriteRouter()
export const registerMicroApps = (appList) => {
setList(appList)
}
+ // 启动微前端框架
+ export const start = () => {
+
+ // 首先验证当前子应用列表是否为空
+ const apps = getList()
+
+ if (!apps.length) {
+ // 子应用列表为空
+ throw Error('子应用列表为空, 请正确注册')
+ }
+
+ // 有子应用的内容, 查找到符合当前路由的子应用
+ const app = currentApp()
+ if (app) {
+ const { pathname, hash } = window.location;
+ const url = pathname + hash;
+ window.history.pushState('', '', url)
+ }
+
+ window.__CURRENT_SUB_APP__ = app.activeRule
+ }
import { currentApp } from './utils'
micro/utils/index.js
+ import { getList } from "../const/subApps";
// 给当前的路由跳转打补丁
export const patchRouter = (globalEvent, eventName) => {
return function () {
const e = new Event(eventName)
globalEvent.apply(this, arguments)
window.dispatchEvent(e)
}
}
+ export const currentApp = () => {
// window.location.pathname获取的www.baidu.com/react15#/xxx中的/react15
+ 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] : {}
+ }
// 子应用是否做了切换
export const isTurnChild = () => {
if(window.location.pathname === window.__CURRENT_SUB_APP__) {
return false
}
return true;
}
micro/router/routerHandle.js
在routerHandle.js中控制路由切换,避免重复执行
+ import isTurnChild from 'micro/utils/index.js'
export const turnApp = async () => {
+ if(!isTurnChild()) {
+ retun;
+ }
console.log('isrunning')
}
主应用生命周期
在主应用的src/util/index.js中
import { registerMicroApps, start } from '../../micro'
export const registerApp = (list) => {
// 注册到微前端框架里
registerMicroApps(list, {
+ beforeLoad: [
+ () => {
+ console.log('开始加载')
+ }
+ ],
+ mounted: [
+ () => {
+ console.log('渲染完成')
+ }
+ ],
+ destoryed: [
+ () => {
+ console.log('卸载完成')
+ }
+ ]
})
// 开启微前端框架
start()
}
micro/start.js
// 实现路由拦截
import { rewriteRouter } from './router/rewriteRouter'
import { currentApp } from './utils'
import { setList, getList } from './const/subApps'
+ import { setMainLifecycle } from './const/mainLifeCycle'
rewriteRouter()
+ export const registerMicroApps = (appList, lifeCycle) => {
setList(appList)
+ setMainLifecycle(lifeCycle)
}
// 启动微前端框架
export const start = () => {
// 首先验证当前子应用列表是否为空
const apps = getList()
if (!apps.length) {
// 子应用列表为空
throw Error('子应用列表为空, 请正确注册')
}
// 有子应用的内容, 查找到符合当前路由的子应用
const app = currentApp()
if (app) {
const { pathname, hash } = window.location;
const url = pathname + hash;
window.history.pushState('', '', url)
}
window.__CURRENT_SUB_APP__ = app.activeRule
}
import { setMainLifecycle } from './const/mainLifeCycle'
let lifecycle = {}
export const getMainLifecycle = () => lifecycle
export const setMainLifecycle = data => lifecycle = data
通过生命周期控制loading
src/App.vue
<template>
<Header v-show="headerStatus"/>
<MainNav v-show="navStatus"/>
<div class="sub-container">
<Loading v-show="loading"/>
<div v-show="!loading" id="micro-container">子应用内容</div>
</div>
</template>
<script>
import Header from "./components/Header";
import MainNav from "./components/MainNav";
import Loading from "./components/Loading";
import { loading, header, nav } from './store'
export default {
name: 'App',
components: {
Header,
MainNav,
Loading,
},
setup() {
return {
loading: loading.loadingStatus,
headerStatus: header.headerStatus,
navStatus: nav.navStatus,
}
}
}
</script>
<style>
html, body, #micro_web_main_app{
width: 100%;
/*height: 100%;*/
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.sub-container{
min-height: 100%;
position: relative;
}
#micro-container{
min-height: 100%;
width: 100%;
}
</style>
import { loading, header, nav } from './store'
src/store/loading.js
import { ref } from 'vue'
export let loadingStatus = ref(true)
export const changeLoading = type => loadingStatus.value = type
然后在生命周期中改变loading状态
src/util/index.js
import { registerMicroApps, start, createStore } 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()
}
micro/start.js
// 实现路由拦截
import { rewriteRouter } from './router/rewriteRouter'
import { currentApp } from './utils'
import { setList, getList } from './const/subApps'
+ import { setMainLifecycle } from './const/mainLifeCycle'
rewriteRouter()
export const registerMicroApps = (appList, lifeCycle) => {
setList(appList)
lifeCycle.beforeLoad[0]()
setTimeout(() => {
lifeCycle.mounted[0]()
}, 3000)
setMainLifecycle(lifeCycle)
}
// 启动微前端框架
export const start = () => {
// 首先验证当前子应用列表是否为空
const apps = getList()
if (!apps.length) {
// 子应用列表为空
throw Error('子应用列表为空, 请正确注册')
}
// 有子应用的内容, 查找到符合当前路由的子应用
const app = currentApp()
if (app) {
const { pathname, hash } = window.location;
const url = pathname + hash;
window.history.pushState('', '', url)
}
window.__CURRENT_SUB_APP__ = app.activeRule
}
微前端生命周期
micro/router/routerHandle.js
import { isTurnChild } from '../utils'
+ import { lifecycle } from '../lifeCycle'
export const turnApp = async () => {
if (isTurnChild()) {
// 微前端的生命周期执行
+ await lifecycle()
}
}
import { lifecycle } from '../lifeCycle'
随后解释,先设置路由切换前后应用标志activeRole
micro/start.js
中const app = currentApp();window.CURRENT_SUB_APP = app.activeRule 设置当前使用的app唯一标识activeRule
micro/util/index.js
import { getList } from "../const/subApps";
// 给当前的路由跳转打补丁
export const patchRouter = (globalEvent, eventName) => {
return function () {
const e = new Event(eventName)
globalEvent.apply(this, arguments)
window.dispatchEvent(e)
}
}
export const currentApp = () => {
const currentUrl = window.location.pathname
return filterApp('activeRule', currentUrl)
}
+ 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] : {}
}
// 子应用是否做了切换
export const isTurnChild = () => {
const { pathname, hash } = window.location
const url = pathname + hash
// 当前路由无改变。
+ const currentPrefix = url.match(/(\/\w+)/g)
+ if (
+ currentPrefix &&
+ (currentPrefix[0] === window.__CURRENT_SUB_APP__) &&
+ hash === window.__CURRENT_HASH__
+ ) {
+ return false;
+ }
// 保存上一步的路由状态window.__ORIGIN_APP__
+ window.__ORIGIN_APP__ = window.__CURRENT_SUB_APP__;
+ const currentSubApp = window.location.pathname.match(/(\/\w+)/)
if (!currentSubApp) {
return false
}
// 当前路由以改变,修改当前路由
window.__CURRENT_SUB_APP__ = currentSubApp[0];
// 判断当前hash值是否改变
window.__CURRENT_HASH__ = hash
return true;
}
micro/lifeCycle/index.js
import { lifecycle } from '../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) {
+ 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.destoryed && app.destoryed()
+
+ // 对应的执行以下主应用的生命周期
+ await runMainLifeCycle('destoryed')
+ }
+
+ export const runMainLifeCycle = async (type) => {
+ const mainlife = getMainLifecycle()
+
+ await Promise.all(mainlife[type].map(async item => await item()))
+ }
加载和解析html
在项目加载时加载html
即在 micro/lifeCycle/index.js下
import { findAppByRoute } from '../utils'
import { getMainLifecycle } from '../const/mainLifeCycle'
import { loadHtml } from '../loader'
export const lifecycle = async () => {
// 获取到上一个子应用
const prevApp = findAppByRoute(window.__ORIGIN_APP__)
// 获取到要跳转到的子应用
const nextApp = findAppByRoute(window.__CURRENT_SUB_APP__)
if (!nextApp) {
return
}
if (prevApp) {
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;
+ const appContext = await loadHtml(app);
return appContext
}
export const mounted = async (app) => {
app && app.mount && app.mount()
await runMainLifeCycle('mounted')
}
export const destoryed = async (app) => {
app && app.destoryed && app.destoryed()
// 对应的执行以下主应用的生命周期
await runMainLifeCycle('destoryed')
}
export const runMainLifeCycle = async (type) => {
const mainlife = getMainLifecycle()
await Promise.all(mainlife[type].map(async item => await item()))
}
loadHtml方法存在如下文件下
micro/loader/index.js
之前启动的都是子应用并没有启动主应用;放开文章头部注释内容
获取子应用html文本的方法
micro/utils/fetchResource.js
+ export const fetchResource = url => fetch(url).then(async res => await res.text())
加载html
+ import { fetchResource } from 'micro/utils/fetchResource'
+ // 加载html的方法
+ export const loadHtml = async(app) => {
+ // 子应用需要显示在哪里
+ let container = app.container; //#id内容
+ // 子应用的入口
+ let entry = app.entry;
+
+ const html = await parseHtml(entry)
+
+ const ct = document.querySelector(container)
+
+ if (!ct) {
+ throw new Error('容器不存在,请查看')
+ }
+ ct.innerHTML = html;
+
+ return app;
}
+ // 解析html
+ export const parseHtml = async (entry) => {
+ const html = await fetchResource(entry)
+
+ const div = document.createElement('div',)
+ div.innerHTML = html
+ // 标签,link,script
+ const [dom, scriptUrl, script] = await getResource(div)
+ console.log(dom, scriptUrl, script)
+ return html
+ }
+
+ export const getResource = async() => {
+ return ['', '', '']
+ }
loadHtml方法实际返回处理后的子应用。因此beforeLoad方法执行时传递的subApp
因此之前改写如下
在 micro/lifeCycle/index.js下
import { findAppByRoute } from '../utils'
import { getMainLifecycle } from '../const/mainLifeCycle'
import { loadHtml } from '../loader'
export const lifecycle = async () => {
// 获取到上一个子应用
const prevApp = findAppByRoute(window.__ORIGIN_APP__)
// 获取到要跳转到的子应用
const nextApp = findAppByRoute(window.__CURRENT_SUB_APP__)
if (!nextApp) {
return
}
if (prevApp) {
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 subApp = await loadHtml(app); // 获取的是子应用
+ subApp && subApp.beforeLoad && subApp.beforeLoad()
-+ return subApp
}
export const mounted = async (app) => {
app && app.mount && app.mount()
await runMainLifeCycle('mounted')
}
export const destoryed = async (app) => {
app && app.destoryed && app.destoryed()
// 对应的执行以下主应用的生命周期
await runMainLifeCycle('destoryed')
}
export const runMainLifeCycle = async (type) => {
const mainlife = getMainLifecycle()
await Promise.all(mainlife[type].map(async item => await item()))
}
getResource方法;获取js执行脚本
micro/loader/index.js
import { fetchResource } from 'micro/utils/fetchResource'
// 加载html的方法
export const loadHtml = async(app) => {
// 子应用需要显示在哪里
let container = app.container; //#id内容
// 子应用的入口
let entry = app.entry;
const [dom, script] = await parseHtml(entry)
const ct = document.querySelector(container)
if (!ct) {
throw new Error('容器不存在,请查看')
}
ct.innerHTML = html;
return app;
}
// 解析html
export const parseHtml = async (entry) => {
const html = await fetchResource(entry)
+ let allScript = []
const div = document.createElement('div',)
div.innerHTML = html
// 标签,link,script
-+ const [dom, scriptUrl, script] = await getResource(div, entry)
+ console.log(dom, scriptUrl, script)
+ const fetchedScript = await Promise.all(scriptUrl.map(async item => fetchResource(item)))
+ allScript = script.concat(fetchedScript)
+ return [dom, allScript]
}
+ export const getResource = async(root, entry) => {
+ const scriptUrl = [] // js 链接: src href
+ const script = [] // 卸载script中的js脚本内容
+ const dom = root.outerHTML
+
+ // 深度解析
+ function deepParse(element) {
+ const children = element.children
+ const parent = element.parentNode;
+ // 第一步处理位于 script 中的内容
+ 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)
+ }
+
+ // link 也会有js内容
+ 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]
}
目前获取到dom和所有的script但是还未执行。运行js脚本
import { performScriptForFunction } from 'micro/sandBox/performScript.js'
import { fetchResource } from 'micro/utils/fetchResource'
+ import { performScriptForFunction } from 'micro/sandBox/performScript.js'
// 加载html的方法
export const loadHtml = async(app) => {
// 子应用需要显示在哪里
let container = app.container; //#id内容
// 子应用的入口
let entry = app.entry;
const [dom, script] = await parseHtml(entry)
const ct = document.querySelector(container)
if (!ct) {
throw new Error('容器不存在,请查看')
}
ct.innerHTML = html;
// 执行脚本
+ scripts.forEach(item => {
+ performScriptForFunction(item)
+ })
return app;
}
执行js脚本
micro/sandBox/performScript.js
import { performScriptForFunction } from 'micro/sandBox/performScript.js'
export const performScriptForFunction = (script) => {
// 方式一:new Function
new Function(script).call(window, window)
}
export const performScriptForEval = (script) => {
// 方式二:eval
// eval(script) 会重复执行 内部执行触发history不会拦截 => isTrueChild再执行
eval(script)
}
为了避免重复isTrueChild再执行执行,需要修改
micro/util/index.js
import { getList } from "../const/subApps";
// 给当前的路由跳转打补丁
export const patchRouter = (globalEvent, eventName) => {
return function () {
const e = new Event(eventName)
globalEvent.apply(this, arguments)
window.dispatchEvent(e)
}
}
export const currentApp = () => {
const currentUrl = window.location.pathname
return filterApp('activeRule', currentUrl)
}
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] : {}
}
// 子应用是否做了切换
export const isTurnChild = () => {
const { pathname, hash } = window.location
const url = pathname + hash
// 当前路由无改变。
+ const currentPrefix = url.match(/(\/\w+)/g)
+ if (
+ currentPrefix &&
+ (currentPrefix[0] === window.__CURRENT_SUB_APP__) &&
+ hash === window.__CURRENT_HASH__
+ ) {
+ return false;
+ }
// 保存上一步的路由状态window.__ORIGIN_APP__
+ window.__ORIGIN_APP__ = window.__CURRENT_SUB_APP__;
+ const currentSubApp = window.location.pathname.match(/(\/\w+)/)
+ if (!currentSubApp) {
+ return false
+ }
+ // 当前路由以改变,修改当前路由
+ window.__CURRENT_SUB_APP__ = currentSubApp[0];
// 判断当前hash值是否改变
window.__CURRENT_HASH__ = hash
return true;
}
微前端环境变量设置
- 替换执行方法为
micro/sandBox/index.js
micro/loader/index.js
import { fetchResource } from 'micro/utils/fetchResource'
- import { performScriptForFunction } from 'micro/sandBox/performScript.js'
+ import { sandBox } from "../sandbox";
// 加载html的方法
export const loadHtml = async(app) => {
// 子应用需要显示在哪里
let container = app.container; //#id内容
// 子应用的入口
let entry = app.entry;
const [dom, script] = await parseHtml(entry)
const ct = document.querySelector(container)
if (!ct) {
throw new Error('容器不存在,请查看')
}
ct.innerHTML = html;
// 执行脚本
- scripts.forEach(item => {
- performScriptForFunction(item)
- })
+ scripts.forEach(item => {
+ sandBox(app, item)
+ })
return app;
}
// 解析html
export const parseHtml = async (entry) => {
const html = await fetchResource(entry)
let allScript = []
const div = document.createElement('div',)
div.innerHTML = html
// 标签,link,script
const [dom, scriptUrl, script] = await getResource(div, entry)
console.log(dom, scriptUrl, script)
const fetchedScript = await Promise.all(scriptUrl.map(async item => fetchResource(item)))
allScript = script.concat(fetchedScript)
return [dom, allScript]
}
export const getResource = async(root, entry) => {
const scriptUrl = [] // js 链接: src href
const script = [] // 卸载script中的js脚本内容
const dom = root.outerHTML
// 深度解析
function deepParse(element) {
const children = element.children
const parent = element.parentNode;
// 第一步处理位于 script 中的内容
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)
}
// link 也会有js内容
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]
}
- 挂载生命周期和标志
micro/sandBox/index.js
import { sandBox } from "../sandbox";
+ import { performScriptForEval } from './performScript'
+
+ const isCheckLifeCycle = lifecycle => lifecycle &&
+ lifecycle.bootstrap &&
+ lifecycle.mount &&
+ lifecycle.unmount
+
+ // 子应用生命周期处理, 环境变量设置
+ export const sandBox = (app, script) => {
+ // 1. 设置环境变量
+ window.__MICRO_WEB__ = true
+
+ // 2. 运行js文件
+ const lifecycle = performScriptForEval(script, app.name)
+
+ // 生命周期,挂载到app上
+ if (isCheckLifeCycle(lifecycle)) {
+ app.bootstrap = lifecycle.bootstrap
+ app.mount = lifecycle.mount
+ app.unmount = lifecycle.unmount
+ }
+ }
- 修改执行js的方法,使其能够在window上获取bootstrap,mount,unmount生命周期,并将其返回
micro/sandBox/performScript.js
// 执行js脚本
export const performScriptForFunction = (script, appName) => {
const scriptText = `
${script}
return window['${appName}']
`
return new Function(scriptText).call(window, window)
}
export const performScriptForEval = (script, appName) => {
// library window.appName
const scriptText = `
() => {
${script}
return window['${appName}']
}
`
return eval(scriptText).call(window, window)// app module mount
}
window.appName是来自src/store/sub.js
下的配置项。该配置项已经通过之前步骤注入到微前端框架。app中既是其中内容
export const subNavList = [
{
name: 'react15',// 唯一
entry: '//localhost:9002/',
loading,
container: '#micro-container',
activeRule: '/react15',
appInfo,
},
{
// 与webpack.config.js中的library一致
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,
},
];
以上配置中的name要与各个子应用中的打包配置library一致
用react16为例:webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
entry: { path: ['regenerator-runtime/runtime', './index.js'] },
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'react16.js',
// 此处的library配置项会将该子应用的js导出的全局属性暴露在window.react16上
// 例如子应用的 export const bootstrap;export const mount;export const unmount
library: 'react16',
libraryTarget: 'umd',
umdNamedDefine: true,
publicPath: 'http://localhost:9003'
},
module: {
rules: [
{
test: /\.js(|x)$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
}
},
{
test: /\.(cs|scs)s$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
},
]
},
optimization: {
splitChunks: false,
minimize: false
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
}),
new MiniCssExtractPlugin({
filename: '[name].css'
})
],
devServer: {
headers: { 'Access-Control-Allow-Origin': '*' },
contentBase: path.join(__dirname, 'dist'),
compress: true,
port: 9003,
historyApiFallback: true,
hot: true
}
}
以上完成之后在
micro/lifeCycle/index.js
中的app.mount等生命周期才会执行
运行环境隔离 --- 快照沙箱
新建 micro/sandbox/snapShotSandbox.js
export class SnapShotSandbox {
constructor() {
// 1. 代理对象
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]
}
}
}
}
然后在环境变量设置与运行js文件时,由原来的window变为当前的proxy上
micro/sandbox/index.js
import { performScriptForEval } from './performScript'
+ import { SnapShotSandbox } from './snapShotSandbox'
const isCheckLifeCycle = lifecycle => lifecycle &&
lifecycle.bootstrap &&
lifecycle.mount &&
lifecycle.unmount
// 子应用生命周期处理, 环境变量设置
export const sandBox = (app, script) => {
+ const proxy = new SnapShotSandbox()
+
+ if (!app.proxy) {
+ app.proxy = proxy
+ }
// 1. 设置环境变量
window.__MICRO_WEB__ = true
// 2. 运行js文件
- const lifecycle = performScriptForEval(script, app.name)
+ const lifecycle = performScriptForEval(script, app.name, app.proxy.proxy)
// 生命周期,挂载到app上
if (isCheckLifeCycle(lifecycle)) {
app.bootstrap = lifecycle.bootstrap
app.mount = lifecycle.mount
app.unmount = lifecycle.unmount
}
}
performScriptForEval(script, app.name, app.proxy.proxy)改造如下
micro/sandBox/performScript.js
// 执行js脚本
-+ export const performScriptForFunction = (script, appName, global) => {
const scriptText = `
${script}
return window['${appName}']
`
- return new Function(scriptText).call(window, window)
+ return new Function(scriptText).call(global, global)
}
-+ export const performScriptForEval = (script, appName, global) => {
// library window.appName
const scriptText = `
() => {
${script}
return window['${appName}']
}
`
return eval(scriptText).call(global, global)// app module mount
}
执行卸载沙箱操作
在 micro/lifeCycle/index.js下
import { findAppByRoute } from '../utils'
import { getMainLifecycle } from '../const/mainLifeCycle'
import { loadHtml } from '../loader'
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)
}
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
}
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()))
}
快照沙箱适用于老版本浏览器(性能不好)
运行环境隔离 --- 代理沙箱
micro/sandbox/proxySandBox.js
// 代理沙箱
let defaultValue = {} // 子应用的沙箱容器
export class ProxySandbox{
constructor() {
this.proxy = null;
this.active()
}
// 沙箱激活
active() {
// 子应用需要设置属性,
this.proxy = new Proxy(window, {
get(target: window, key) {
// window上的方法会存在非法操作
if (typeof target[key] === 'function') {
return target[key].bind(target)
}
return defaultValue[key] || target[key] // 当获取window本身的属性时。使用target[key]
},
set(target, key, value) {
defaultValue[key] = value
return true
}
})
}
// 沙箱销毁
inactive () {
defaultValue = {}
}
}
micro/sandbox/index.js
import { performScriptForEval } from './performScript'
- import { SnapShotSandbox } from './snapShotSandbox'
+ import { proxySandBox } from './proxySandBox'
const isCheckLifeCycle = lifecycle => lifecycle &&
lifecycle.bootstrap &&
lifecycle.mount &&
lifecycle.unmount
// 子应用生命周期处理, 环境变量设置
export const sandBox = (app, script) => {
- const proxy = new SnapShotSandbox()
+ const proxy = new proxySandBox()
if (!app.proxy) {
app.proxy = proxy
}
// 1. 设置环境变量
window.__MICRO_WEB__ = true
// 2. 运行js文件
const lifecycle = performScriptForEval(script, app.name, app.proxy.proxy)
// 生命周期,挂载到app上
if (isCheckLifeCycle(lifecycle)) {
app.bootstrap = lifecycle.bootstrap
app.mount = lifecycle.mount
app.unmount = lifecycle.unmount
}
}
performScriptForEval(script, app.name, app.proxy.proxy)改造如下
micro/sandBox/performScript.js
// 执行js脚本
// 执行js脚本
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) => {
// library window.appName
window.proxy = global
const scriptText = `
((window) => {
${script}
return window['${appName}']
})(window.proxy)
`
return eval(scriptText)// app module mount
}
css样式隔离
- css module
- shadow dom
- mode: attachShadow->shadow
- minicss:webpack打包单独的css文件
- css-in-js
主应用和子应用通信(props方式)
好莱坞原则: 不用联系我,当我需要的时候会打电话给你依赖注入:主应用的显示隐藏,注入到子应用内部,通过子应用内部的方法进行调用
main/src/mian.js 注册(通过main.js的registerApp(subNavList)注册微前端框架,js的registerApp调用微前端micro/index.js的注册方法,微前端注册方法registerApp实现appList和生命周期的注册。而appList为主应用传递的subNavList内容)时携带子路由配置信息
import { subNavList } from './store/sub'
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,
},
];
主应用appInfo信息
import * as appInfo from '../store'
main/src/store/index.js
// 暴露loading的方法
export * as loading from './loading'
// 暴露header的方法
export * as header from './header'
// 暴露nav的方法
export * as nav from './nav'
main/src/store/loading.js
main/src/store/header.js
main/src/store/nav.js
三个文件存放主路由存在的变量,变量用于控制头部header和导航栏nav控制
在登录的子应用时,控制隐藏
// 主应用为vue3的代码:设置显示和隐藏
import { ref } from 'vue';
export const headerStatus = ref(true)
export const changeHeader = type => headerStatus.value = type;
主应用中通过header和nav的状态控制显示隐藏
main/src/App.vue
<template>
+ // 状态控制显示隐藏
+ <Header v-show="headerStatus"/>
+ <MainNav v-show="navStatus"/>
<div class="sub-container">
<Loading v-show="loading"/>
<div v-show="!loading" id="micro-container">子应用内容</div>
</div>
</template>
<script>
import Header from "./components/Header";
import MainNav from "./components/MainNav";
import Loading from "./components/Loading";
import { loading, header, nav } from './store'
export default {
name: 'App',
components: {
Header,
MainNav,
Loading,
},
setup() {
return {
+ loading: loading.loadingStatus,
+ headerStatus: header.headerStatus,
+ navStatus: nav.navStatus,
}
}
}
</script>
<style>
html, body, #micro_web_main_app{
width: 100%;
/*height: 100%;*/
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.sub-container{
min-height: 100%;
position: relative;
}
#micro-container{
min-height: 100%;
width: 100%;
}
</style>
在子应用中改变显示隐藏状态实现对主应用的header和nav控制
main/micro/index.js中注册路由时(registerMicroApps携带appInfo)后续通过微前端生命周期,在mounted时将appInfo传入子应用
micro/lifeCycle.js
import { findAppByRoute } from '../utils'
import { getMainLifecycle } from '../const/mainLifeCycle'
import { loadHtml } from '../loader'
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)
}
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
}
export const mounted = async (app) => {
app && app.mount && app.mount({
+ // 只需要subNavList的appInfo和entry即可
+ appInfo: app.appInfo,
+ entry: app.entry
})
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()))
}
然后react16的子应用入口中引入appInfo并改变值,并在登陆页面时实现header和nav隐藏
/react16/index.js
+ import { setMain } from './utils/main.js';
...省略代码
export const mount = (app) => {
// 此处的app即为上述微前端注入子应用生命周期的内容
// app.mount({
// + // 只需要subNavList的appInfo和entry即可
// + appInfo: app.appInfo,
// + entry: app.entry
// })
+ setMain(app)
render();
}
...省略代码
/react16/utils/main.js
let main = null
export const setMain = (data) => {
main = data
}
export const getMain = () => {
return main
}
然后在进入login界面时实现header和nav隐藏
/react16/src/pages/login/ind.js
import React, {useEffect} from 'react';
import globalConfig from "../../config/globalConfig";
import LoginPanel from "./components/LoginPanel.jsx";
+ import { getMain } from '../../utils/main'
import "./index.scss"
const Login = () => {
useEffect(() => {
+ const main = getMain()
+ main.appInfo.header.changeHeader(false)
+ main.appInfo.nav.changeNav(false)
}, [])
return (
<div className="login">
<img className="loginBackground" src={`${globalConfig.baseUrl}/login-background.png`}/>
<LoginPanel/>
</div>
)
}
export default Login
主应用和子应用通信(customEvent方式)
main/micro/customevent/index.js
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)
}
}
然后在micro/start.js运行时添加事件监听
+ import { Custom } from './customevent'
+
+ const custom = new Custom()
+ custom.on('test', (data) => {
+ console.log(data)
+ })
+
+ window.custom = custom
最后子应用通过emit触发.触发后主应用事件监听获取值的变化。此时值可以控制显示隐藏
/react16/index.js
...省略代码
export const mount = (app) => {
// 此处的app即为上述微前端注入子应用生命周期的内容
// app.mount({
// + // 只需要subNavList的appInfo和entry即可
// + appInfo: app.appInfo,
// + entry: app.entry
// })
+ window.custom.emit('test', data: {
+ headerShow: false,
+ navShow: false
+ })
render();
}
子应用间通信
viue3 ---> vue2
vue2/src/main.js
export const mount = () => {
window.custom.emit('test1', {
b: 2
})
window.custom.on('test2', (data) => {
console.log(data)
})
render()
}
vue3/src/main.js
export const mount = () => {
// vue3 ---> vue2
// 先监听后触发
window.custom.on('test1', () => {
window.custom.emit('test2', {
b: 2
})
})
render()
}