目录
二、微前端自研框架
1、子应用接入
a、vue2
const path = require('path');
const {name} = require('./package');
function resolve(dir) {
return path.join(__dirname, dir);
}
const port = 9004;
module.exports = {
outputDir: 'dist', // 打包的目录
assetsDir: 'static', // 打包的静态资源
filenameHashing: true, // 打包出来的文件,会带有hash信息
publicPath: 'http://localhost:9004',
devServer: {
contentBase: path.join(__dirname, 'dist'),
hot: true,
disableHostCheck: true,
port,
headers: {
'Access-Control-Allow-Origin': '*', // 本地服务的跨域内容
},
},
// 自定义webpack配置
configureWebpack: {
resolve: {
alias: {
'@': resolve('src'),
},
},
output: {
// 把子应用打包成 umd 库格式 commonjs 浏览器,node环境
libraryTarget: 'umd',
filename: 'vue2.js',
library: 'vue2', // window.vue2
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
import Vue from 'vue'
import App from './App.vue'
import router from './router';
import store from './store';
Vue.config.productionTip = false
let instance = null;
const render = () => {
instance = new Vue({
router,
store,
render: h => h(App)
}).$mount('#app-vue')
}
if (!window.__MICRO_WEB__) {
render()
}
// 开始加载结构
export async function beforeLoad() {
console.log('beforeLoad');
}
export async function mounted() {
window.custom.on("test2", data => {
console.log(data)
})
window.custom.emit("test1", {
a: 1
})
render()
console.log('mounted')
}
export async function destoryed() {
console.log('destoryed', instance)
}
b、vue3
const path = require('path');
const {name} = require('./package');
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 库格式
libraryTarget: 'umd',
filename: 'vue3.js',
library: 'vue3',
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
import {createApp} from 'vue';
import App from './App.vue';
import router from './router';
let instance = null;
const render = () => {
instance = createApp(App);
instance.use(router).mount('#app');
}
if (!window.__MICRO_WEB__) {
render();
}
export async function beforeLoad() {
console.log('beforeLoad');
}
export async function mounted() {
const storeData = window.store.getStore()
window.store.update({
...storeData,
a: 11
})
// vue3 vue2 先有监听,再有触发
window.custom.on("test1", () => {
window.custom.emit("test2", {
a: 2
})
})
render();
console.log('mounted');
}
export async function destoryed() {
console.log('destoryed', instance);
}
c、react15
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
entry: {
path: ['./index.js']
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'react15.js',
library: 'react15',
libraryTarget: 'umd',
umdNamedDefine: true,
publicPath: 'http://localhost:9002/'
},
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'
})
],
devServer: {
headers: {'Access-Control-Allow-Origin': '*'},
contentBase: path.join(__dirname, 'dist'),
compress: true,
port: 9002,
historyApiFallback: true,
hot: true,
}
}
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 async function beforeLoad() {
console.log('beforeLoad')
}
export async function mounted() {
render()
console.log('mounted')
}
export async function destoryed() {
console.log('destoryed')
}
d、react16
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
}
}
import React from 'react'
import "./index.scss"
import ReactDOM from 'react-dom'
import BasicMap from './src/router';
import {setMain} from "./src/utils/main";
export const render = () => {
ReactDOM.render(<BasicMap/>, document.getElementById('app-react'))
}
if (!window.__MICRO_WEB__) {
render()
}
export async function beforeLoad() {
console.log("beforeLoad")
}
export async function mounted(app) {
setMain(app)
render()
}
export async function destoryed() {
console.log("destoryed")
}
2、主应用注册子应用
import {createApp} from 'vue'
import App from './App.vue'
import router from './router'
import {subNavList} from "./store/sub"
import {registerApp} from './utils';
// 注册、加载、启动子应用
registerApp(subNavList);
createApp(App).use(router()).mount('#micro_web_main_app')
import * as appInfo from "../store"
export const subNavList = [
{
name: "react15",
activeRule: "/react15",
container: "#micro-container",
entry: '//localhost:9002/',
appInfo
},
{
name: "react16",
activeRule: "/react16",
container: "#micro-container",
entry: '//localhost:9003/',
appInfo
},
{
name: "vue2",
activeRule: "/vue2",
container: "#micro-container",
entry: '//localhost:9004/',
appInfo
},
{
name: "vue3",
activeRule: "/vue3",
container: "#micro-container",
entry: '//localhost:9005/',
appInfo
}
]
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;
export const NAV_LIST = [
{
name: '首页',
status: true,
value: 0,
url: '/vue3#/index',
hash: '',
},
{
name: '资讯',
status: false,
value: 1,
url: '/react15#/information',
},
{
name: '视频',
status: false,
value: 2,
url: '/react15#/video',
hash: '',
},
{
name: '选车',
status: false,
value: 3,
url: '/vue3#/select',
hash: '',
},
{
name: '新能源',
status: false,
value: 4,
url: '/vue2#/energy',
hash: '',
},
{
name: '新车',
status: false,
value: 5,
url: '/react16#/new-car',
hash: '',
},
{
name: '排行',
status: false,
value: 6,
url: '/react16#/rank',
hash: '',
},
]
<template>
<div class="main-nav-container">
<div class="main-nav-content">
<!-- logo内容 -->
<div class="main-nav-logo">
<img src="" alt="">
</div>
<!-- 导航列表详情 -->
<div class="main-nav-list">
<div
v-for="(item, index) in NAV_LIST"
:class="{ 'main-nav-active': currentIndex === index }"
:key="index"
@click="setCurrentIndex(item, index)"
>
{{ item.name }}
</div>
</div>
<!-- 搜索 -->
<div class="main-nav-search">
<div class="main-nav-search-icon">
<img src="../../assets/blue-search.png" alt="">
</div>
<div class="main-nav-search-input">
<input
type="text"
id="main-nav-search"
v-if="searchStatus"
@blur="setSearchStatus(false)"
>
<div class="main-nav-search-input-fake" v-else @click="setSearchStatus(true)">
快速搜索
</div>
</div>
<div class="main-nav-search-button">
搜索
</div>
</div>
</div>
</div>
</template>
<script>
import {ref, watch} from 'vue'
import {NAV_LIST} from '../../const'
import {useRouter, useRoute} from 'vue-router'
export default {
name: 'nav',
setup() {
const currentIndex = ref(0)
const searchStatus = ref(true)
const router = useRouter();
const route = useRoute();
watch(route, (val) => {
NAV_LIST.forEach((item, index) => {
if (val.fullPath.indexOf(item.url) !== -1) {
currentIndex.value = index
}
})
}, {deep: true})
const setCurrentIndex = (data, index) => {
if (data.url === route.fullPath) {
return
}
currentIndex.value = index
router.push(data.url)
}
const setSearchStatus = (type) => {
searchStatus.value = type
}
return {
NAV_LIST,
currentIndex,
setCurrentIndex,
searchStatus,
setSearchStatus
}
}
};
</script>
<template>
<div class="micro-main-body-container">
<Loading v-show="loadingStatus"></Loading>
<!-- 子应用容器 -->
<div id="micro-container" v-show="!loadingStatus"></div>
</div>
</template>
<script>
import Loading from './loading.vue';
import {loading} from '@/store';
export default {
name: 'micro-body',
components: {
Loading,
},
setup() {
return {
loadingStatus: loading.loadingStatus,
};
},
};
</script>
3、主应用路由拦截
export {registerMicroApps, start} from "./start"
export {createStore} from "./store"
import {registerMicroApps, start, createStore} from "../../micro"
import {loading} from "../store"
const store = createStore()
window.store = store
store.subscribe((newValue, oldValue) => {
console.log(newValue, oldValue)
})
export const registerApp = (list) => {
// 注册到微前端框架里
registerMicroApps(list, {
beforeLoad: [
() => {
loading.changeLoading(true)
console.log("开始加载")
}
],
mounted: [
() => {
loading.changeLoading(false)
console.log("渲染完成")
}
],
destoryed: [
() => {
console.log("卸载完成")
}
]
})
// 开启微前端框架
start()
}
import {setList, getList} from "./const/subApps";
import {currentApp} from "./utils";
import {rewriteRouter} from "./router/rewriteRouter";
import {setMainLifeCycle} from "./const/mainLifeCycle";
import {Custom} from "./customeevent";
import {prefetch} from "./loader/prefetch";
const custom = new Custom()
custom.on("test", (data) => {
console.log(data)
})
window.custom = custom
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
}
// 预加载-加载接下来的所有子应用,但是不显示
prefetch()
}
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.onpopstate = function () {
turnApp()
}
window.addEventListener("micro_push", turnApp)
window.addEventListener("micro_replace", turnApp)
}
let list = []
export const getList = () => list
export const setList = appList => list = appList
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)
}
const filterApp = (key, value) => {
const currentApp = getList().filter(item => item[key] === value)
return currentApp && currentApp.length ? currentApp[0] : {}
}
// 子应用是否做了切换
export const isTurnChild = () => {
window.__ORIGIN_APP__ = window.__CURRENT_SUB_APP__
if (window.__CURRENT_SUB_APP__ === window.location.pathname) {
return false
}
const currentApp = window.location.pathname.match(/\/\w+/)
if (!currentApp) {
return
}
window.__CURRENT_SUB_APP__ = currentApp[0]
return true
}
import {isTurnChild} from "../utils";
import {lifecycle} from "../lifeCycle";
export const turnApp = async () => {
if (isTurnChild()) {
// 微前端的生命周期执行
await lifecycle()
}
}
4、主应用生命周期
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.destoryed) {
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.mounted && app.mounted({
appInfo: app.appInfo,
entry: app.entry
})
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()))
}
let lifeCycle = {}
export const getMainLifeCycle = () => lifeCycle
export const setMainLifeCycle = data => lifeCycle = data
import {ref} from 'vue';
export const loadingStatus = ref(false);
export const changeLoading = type => loadingStatus.value = type
export * as loading from './loading';
export * as header from './header';
export * as nav from './nav';
5、加载和解析html
import {fetchResource} from "../utils/fetchResource";
import {sandbox} from "../sandbox";
// 加载html的方法
export const loadHtml = async (app) => {
// 第一个,子应用需要显示在哪里
let container = app.container // #id 内容
// 子应用的入口
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
}
const cache = {} // 根据子应用的name来做缓存
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 fetchScripts = await Promise.all(scriptUrl.map(async item => fetchResource(item)))
allScript = script.concat(fetchScripts)
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
// 第一步处理位于 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]
}
// 执行js脚本
export const performScriptForFunction = (script, appName, global) => {
window.proxy = 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
}
export const fetchResource = url => fetch(url).then(async res => await res.text())
// import {performScriptForEval} from "./performScript";
import {performScriptForFunction} from "./performScript";
// import {SnapShotSandbox} from "./snapShotSandbox";
import {ProxySandbox} from "./proxySandbox";
const isCheckLifeCycle = lifecycle => lifecycle &&
lifecycle.beforeLoad &&
lifecycle.mounted &&
lifecycle.destoryed
// 子应用生命周期处理,环境变量设置
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)
const lifecycle = performScriptForFunction(script, app.name, app.proxy.proxy)
// 生命周期,挂载到app上
if (isCheckLifeCycle(lifecycle)) {
app.beforeLoad = lifecycle.beforeLoad
app.mounted = lifecycle.mounted
app.destoryed = lifecycle.destoryed
}
}
6、运行环境隔离
// 快照沙箱
// 应用场景:比较老版本的浏览器
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]
}
}
}
}
// 代理沙箱
const 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() {
}
}
7、css样式隔离
module.exports = {
module: {
rules: [
{
test: /\.(cs|scs)s$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
module: true
}
},
'sass-loader'
]
}
]
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>shadow dom</title>
</head>
<body>
<div id="box1"></div>
<div id="box2"></div>
<script>
const box1 = document.getElementById("box1")
// 开启shadow dom模式
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(one)
shadow1.appendChild(style1)
const box2 = document.getElementById("box2")
// 开启shadow dom模式
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(two)
shadow2.appendChild(style2)
</script>
</body>
</html>
8、应用间通信
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
<template>
<!-- 用户信息 -->
<UserInfo v-show="headerStatus"/>
<!-- 导航 -->
<MainNav v-show="navStatus"/>
</template>
<script>
import MainNav from './main-nav'
import UserInfo from './userInfo.vue'
import {header, nav} from "../../store"
export default {
name: 'MainHeader',
components: {
MainNav,
UserInfo
},
setup() {
return {
headerStatus: header.headerStatus,
navStatus: nav.navStatus
}
}
};
</script>
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
let main = null
export const setMain = (data) => {
main = data
}
export const getMain = () => {
return main
}
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)
}
}
9、全局状态管理
export const createStore = (initData = {}) => (() => {
let store = initData
const observers = [] // 管理所有的订阅者,依赖
// 获取store
const getStore = () => store
// 更新store
const update = (value) => {
if (value !== store) {
// 执行store的操作
const oldValue = store
// 将store更新
store = value
// 通知所有的订阅者,监听store的变化
observers.forEach(async item => await item(store, oldValue))
}
}
// 添加订阅者
const subscribe = (fn) => {
observers.push(fn)
}
return {
getStore,
update,
subscribe
}
})()
10、提高加载性能
import {getList} from "../const/subApps";
import {parseHtml} from "./index";
export const prefetch = async () => {
// 1、获取到所有子应用列表-不包括当前正在显示的
const list = getList().filter(item => !window.location.pathname.startsWith(item.activeRule))
// 2、预加载剩下的所有子应用
await Promise.all(list.map(async item => await parseHtml(item.entry, item.name)))
}
三、微前端现有框架
1、qiankun重构项目
* 官网:https://qiankun.umijs.org/zh
2、single-spa重构项目
* 官网:https://single-spa.js.org/