项目创建
我们创建如图几个文件夹
- main:主应用(采用vue3作为技术栈)
- react:子应用1
- vue2:子应用2
- vue3:子应用3
- service:服务端代码
vue2子应用:
我们在App.vue中写一点点东西
<template>
<div class="vue2">
<h1>vue2子应用</h1>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
}
</script>
<style scoped>
.vue2{
background: #F56C6C;
height: 90vh;
display: flex;
justify-content: center;
align-items: center;
color: #ffff;
}
</style>
然后,我们需要对做一些配置,创建vue.config.js配置文件
const path = require('path')
const { name } = require('./package.json')
function resolve (dir) {
return path.join(__dirname)
}
const port = 9004
module.exports = {
outputDir: 'dist', //打包输出目录
assetsDir: 'static', //静态资源目录
filenameHashing: true, //打包的文件名是否带哈希信息
publicPath: 'http://localhost:9004', //确保当前资源不会加载出错
devServer: {
contentBase: path.join(__dirname, 'dist'), //当前服务是通过dist来拿的
hot: true, //热更新
disableHostCheck: true, //热更新
port, //端口
headers: { //本地服务可以被跨域调用,主应用可以拿到子应用的数据
'Access-Control-Allow-Origin': '*',
},
},
// 自定义webpack配置
configureWebpack: {
resolve: {
alias: {
'@': resolve('src'), //@代表src目录
},
},
output: {
// 把子应用打包成 umd 库格式
libraryTarget: 'umd', //umd格式 支持comm.js引入 浏览器、node可以识别
filename: 'vue2.js', //打包出的名称
library: 'vue2', //全局可以通过window.vue2拿到的应用
jsonpFunction: `webpackJsonp_${name}`,
},
},
}
main.js改造
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
let instance =null
const render = () =>{
instance = new Vue({
render: h => h(App),
}).$mount('#app')
}
if(!window.__MICRO_WEB__){
render()
}
//开始加载
export const bootstrap = () => {
console.log('开始加载');
}
//渲染成功
export const mount = () => {
render()
console.log('渲染成功');
}
//卸载
export const unmount = () => {
console.log('卸载',instance);
}
主应用开发
主应用中央控制器的界面开发
界面效果
点击按钮时,路由有切换
main\src\App.vue
<template>
<div>
<MainHeader></MainHeader>
</div>
</template>
<script >
import MainHeader from './components/header.vue'
export default{
components:{ MainHeader },
}
</script>
<style lang="less" scoped>
</style>
main\src\components\header.vue
<template>
<header>
<button @click="home()">首页</button>
<button
v-for="(item,index) in navList"
:key="index"
@click="change(item,index)"
:class="{'select':index === selectIndex}"
>{{ item.name}}</button>
</header>
<main id="micro-container">
<div class="home" v-if="selectIndex == null">主-----------------------------页</div>
</main>
<footer>
京公网安备11000002000001号京ICP证030173号©2022
</footer>
</template>
<script>
import { watch ,ref} from 'vue'
//引入路由方法
import { useRoute, useRouter } from 'vue-router'
export default {
setup(props,context){
//router是路由的实例对象,包含全局路由信息
const router = useRouter()
//route是路由实例内容,包含子路由内容
const route = useRoute()
//当前选中按钮
let selectIndex = ref(null)
//导航菜单
const navList = [
{name:"应用1",url:"vue1"},
{name:"应用2",url:"vue2"},
{name:"应用3",url:"react"},
]
//点击导航菜单切换路由和央样式
function change(item,index) {
selectIndex.value = index
router.push(item.url)
}
//单击主页
function home(){
router.push('/')
selectIndex.value = null
}
//解决刷新界面时,路由和内容不匹配的异常
watch(route,(v)=>{
let index = navList.findIndex(res => v.fullPath.indexOf(res.url) > -1)
selectIndex.value = index === -1 ? null : index
},{ deep:true})
return {
navList,
change,
selectIndex,
router,
home
}
}
}
</script>
<style>
header{
height: 40px;
background: #409EFF;
display: flex;
justify-content: center;
align-items: center;
}
button {
margin-left: 15px;
}
main{
height: 850px;
}
footer{
height: 40px;
line-height: 40px;
text-align: center;
color: #fff;
background: #67C23A;
}
.select{
background: #67C23A;
border: none;
color: #fff;
}
.home{
display: flex;
justify-content: center;
align-items: center;
height: 100%;
font-size: 25px;
}
</style>
main\src\router\index.js
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/react',
component: () => import('../App.vue'),
},
{
path: '/vue2',
component: () => import('../App.vue'),
},
{
path: '/vue1',
component: () => import('../App.vue'),
},
];
const router = (basename = '') => createRouter({
history: createWebHistory(basename),
routes,
});
export default router;
main\src\main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
//挂载
app.use(router()).mount('#app')
子应用注册
实现微前端,我们一定需要一个配置文件,包含所有子应用的信息,如子应用名称、访问路径、对应路由等等信息。
然后,将这些信息储存在我们的主应用内。当然这不是优选,我们应该创建一个微前端框架,将我们的这个配置信息传入到微前端框架里去,然后再主应用里调用微前端框架里的方法就行了。
子应用配置文件的创建
首先,我们需要创建一个用于储存子应用配置信息的文件,这个文件一定是定义在我们的主应用里面的。
在sr目录下创建store文件夹并创建sub.js文件
//main\src\store\sub.js
//创建子应用信息
export const navList = [
{
name: 'react',
entry: '//localhost:9003/',
container: '#micro-container', //设定将来渲染的容器
activeRule: '/react',
},
{
name: 'vue1',
entry: '//localhost:9004/',
container: '#micro-container',
activeRule: '/vue1',
},
{
name: 'vue2',
entry: '//localhost:9005/',
container: '#micro-container',
activeRule: '/vue2',
},
];
微前端框架创建
我们首先在主应用main中创建个微前端micro文件夹(当然,这个文件夹和main才是最合理的),然后创建一个index.js入口文件。
index文件的逻辑一定是把我们封装的一些逻辑进行对外暴露的一个文件。因此,我们主逻辑应该写在其他文件里,index文件做一个引入。
那么,我们在main\micro\文件里创建 个start.js文件把,主要用来写我们框架的一些逻辑。我们先在start.js里面定义并暴露一个注册子应用的函数吧,这个函数要干什么,我们现在也不清楚。
//main\micro\start.js
//微前端框架核心逻辑------注册一个子应用
export const registerMicroApps = () => {
}
那么,index里面我们引入并对外暴露这个函数就行
//main\micro\index.js
export { registerMicroApps } from './start.js'
我们在主应用创建了一个配置文件,这个配置文件一定是要放入微前端框架里面的,因此,我们的 registerMicroApps函数可以先用来接受一个这样的配置列表,在合适的位置调用registerMicroApps(list)实现配置文件注入微前端框架即可。
为了实现模块化,我们的配置信息可以不放在main\micro\start.js进行储存,我们可以把所有的这种信息单独放在一个文件里进行统一管理。我们创建main\micro\const\subApps.js文件
//用于储存信息的声明
let list = []
//用于获取信息的函数
export const getList = () => list
//用于设置信息的函数
export const setList = appList => list = appList
这样,我们写入信息可以用 setList(list)
读取信息用 getList()
我们在start.js中,写下如下逻辑
import { setList } from "./const/subApps"
export const registerMicroApps = (appList) => {
setList(appList)
}
这样,我们在外部调用registerMicroApps()函数时,就可以将配置文件储存在我们的框架里。
我们可以在main.js中引入这个函数,并把配置文件传递进去
main\src\main.js
//引入的不再是Vue构造函数了,引入的是一个名为createApp的工厂函数
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { registerMicroApps } from '../micro/index'
import { navList } from './store/sub.js'
registerMicroApps(navList)
//创建应用实例对象——app(类似于之前Vue2中的vm,但app比vm更“轻”)
const app = createApp(App)
//挂载
app.use(router()).mount('#app')
ok,现在我们的子应用配置信息已经注册到我们的微前端框架里了。
微前端框架
实现微前端,我们的大致思路应该如下:
- 监视页面路由的变化
- 根据当前页面路由匹配子应用
- 主应用加载子应用
- 特定容器渲染子应用
准备工作
在qinkun里,主应用的main.js中引入两个函数registerMicroApps, start,仿照qinkun,我们创建自己的微前端框架文件夹,命名micro-fe , 我们在主应用的main.js仿照qiankun做出如下配置
//引入的不再是Vue构造函数了,引入的是一个名为createApp的工厂函数
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { registerMicroApps, start } from './micro-fe';
registerMicroApps([
{
name: 'vue1', // app name registered
entry: 'http://localhost:9005/',
container: '#micro-container',
activeRule: '/vue1',
},
{
name: 'vue2',
entry: 'http://localhost:9005/',
container: '#micro-container',
activeRule: '/vue2',
},
{
name: 'react',
entry: 'http://localhost:9005/',
container: '#micro-container',
activeRule: '/react',
},
]);
start();
createApp(App).use(router()).mount('#app')
我们的主应用src\micro-fe文件夹内应该有一个入口index.js文件,按照主应用的引入逻辑,我们应该暴露两个函数。
mainApp\src\micro-fe\index.js
//注册微前端
export const registerMicroApps = (apps) =>{
}
//启动微前端
export const start = () =>{}
主应用main.js中registerMicroApps函数在执行时,传入了各个子应用的配置文件apps,因此,在registerMicroApps函数里,我们可以将获取的apps配置列表进行储存。同时,我们可以定义一个获取配置列表的函数,方便以后拿到我们传入的apps配置列表。
//创建_apps变量储存主应用传递进来的子应用配置
let _apps = []
//子应用配置列表获取函数
export const getApps = () => _apps
//微前端框架核心逻辑-----注册函数
export const registerMicroApps = (apps) =>{
_apps = apps
}
//微前端核心逻辑------微前端的运行原理
export const start = () =>{}
监视路由变化
我们的satrt函数里,应该是我们微前端框架的核心逻辑,需要实现监视页面路由的变化、根据当前页面路由匹配子应用、主应用加载子应用、特定容器渲染子应用四个核心功能。
首先,我们实现路由的监听,路由分为hash路由和history路由。
hash路由
对于hash路由的变化,我们可以使用window.onhashchange来监听,比较容易。本实例中,我们暂时不考虑。
history路由
方法 | 说明 |
---|---|
back() | 参照当前页面,返回历史记录中的上一条记录(即返回上一页),您也可以通过点击浏览器工具栏中的←按钮来实现同样的效果。 |
forward() | 参照当前页面,前往历史记录中的下一条记录(即前进到下一页),您也可以通过点击浏览器工具栏中的→按钮来实现同样的效果。 |
go() | 参照当前页面,根据给定参数,打开指定的历史记录,例如 -1 表示返回上一页,1 表示返回下一页。 |
pushState() | 向浏览器的历史记录中插入一条新的历史记录。 |
replaceState() | 使用指定的数据、名称和 URL 来替换当前历史记录。 |
对于history路由, 其history.go、history.back、history.forword 等方法我们可以使用popstate事件进行监听。 (浏览器的前进后退会触发history.go、history.back方法)。
因此,我们的路由变化后,核心处理函数应该是
window.addEventListener('popstate',()=>{
console.log("1.路由history.go等方法被触发");
//做一些事情
})
但是,浏览器历史记录的pushState、及repalceState方法不会被popstate事件监听到,因此,我们需要重写pushState、及repalceState的原生方法。
const rawPushState = window.history.pushState
window.history.pushState = (...args) =>{
rawPushState.apply(window.history,args)
console.log("1.触发popstate事件");
}
const rawReplaceState = window.history.replaceState
window.history.repalceState = (...args) =>{
rawReplaceState .apply(window.history,args)
console.log("1.触发replaceState事件");
}
OK,要实现路由的监听,我们的start函数里应该首先执行这三个方法。为了逻辑更清晰,我们将这些函数封装到一个js文件里,在satrt中进行引入执行。
在src\micro-fe\文件夹下创建rewrite-router.js
export const rewriteRouter = () =>{
//1.监视路由的变化
// hash路由 window.onhashchange
// history路由
// history.go、history.back、history.forword 使用popstate事件:window.onpopstate
window.addEventListener('popstate',()=>{
console.log("1.路由history.go等方法被触发");
})
//pushState、及repalceState popstate事件监听不到,我们需要重写pushState、及repalceState的原生方法
const rawPushState = window.history.pushState
window.history.pushState = (...args) =>{
rawPushState.apply(window.history,args)
console.log("1.触发popstate事件");
}
const rawReplaceState = window.history.replaceState
window.history.repalceState = (...args) =>{
rawReplaceState .apply(window.history,args)
console.log("1.触发replaceState事件");
}
}
然后,在start函数里(mainApp\src\micro-fe\index.js)进行引入使用
import { rewriteRouter } from './rewrite-router'
//创建_apps变量储存主应用传递进来的子应用配置
let _apps = []
//子应用配置列表获取函数
export const getApps = () => _apps
//微前端框架核心逻辑-----注册函数
export const registerMicroApps = (apps) =>{
_apps = apps
}
//微前端核心逻辑------微前端的运行原理
export const start = () =>{
//1.监视路由的变化
rewriteRouter()
//2.匹配子应用
//3.加载子应用
//4.渲染子应用
}
根据路由变化,匹配子应用
创建路由匹配处理函数
在监听到路由的变化后,我们应该做一些处理
window.addEventListener('popstate',()=>{
// 路由处理相关逻辑
})
window.history.pushState = (...args) =>{
// 路由处理相关逻辑
})
window.history.repalceState = (...args) =>{
// 路由处理相关逻辑
})
为了结构更加清晰,我们将这些逻辑封装成一个函数,在rewrite-router.js文件里进行引入
在src\micro-fe\文件夹下创建handle-router.js文件
//处理路由变化
//获取子应用的配置列表,用于路由匹配
import { getApps } from "./index";
export const handleRouter = async () => {
console.log('2.路由已经发生变化,现在进行处理');
}
//处理路由相关变化逻辑
import { handleRouter } from "./handle-router";
export const rewriteRouter = () =>{
history.go、history.back、history.forword 使用popstate事件:window.onpopstate
window.addEventListener('popstate',()=>{
console.log("1.路由history.go等方法被触发");
handleRouter()
})
const rawPushState = window.history.pushState
window.history.pushState = (...args) =>{
rawPushState.apply(window.history,args)
handleRouter()
}
const rawReplaceState = window.history.replaceState
window.history.repalceState = (...args) =>{
rawReplaceState .apply(window.history,args)
handleRouter()
}
}
根据当前路由匹配子应用
匹配子应用的逻辑大致如下:
- 获取当前的路由路径
- 去 apps 里面查找
//处理路由变化的
import { getApps } from "./index";
import { importHTML } from './import-html'
export const handleRouter = async () => {
console.log('2.路由已经发生变化,现在进行处理');
//2.匹配子应用
// 2.1获取当前的路由路径
console.log('2.1获取当前路由路径',window.location.pathname);
// 2.2去 apps 里面查找
const apps = getApps()
const app = apps.find(item => window.location.pathname.startsWith(item.activeRule))
if(!app){
return
}
}
加载子应用
匹配到子应用后,我们可以获取到子应用的配置对象,像这样
{
name: 'vue1', // app name registered
entry: 'http://localhost:9005/',
container: '#micro-container',
activeRule: '/vue1',
},
现在,我们需要加载子应用的所有资源,如HTML、CSS及javasrcipt。
获取并渲染html内容
- 使用ajax获取子应用对应域名下的html
- 获取要渲染html的dom元素
- 将获取的html挂载在获取的dom元素上
//mainApp\src\micro-fe\handle-router.js
import { getApps } from "./index";
import { importHTML } from './import-html'
export const handleRouter = async () => {
//2.匹配子应用
const apps = getApps()
const app = apps.find(item => window.location.pathname.startsWith(item.activeRule))
if(!app){
return
}
//3.加载子应用
// 使用 fetch 获取子应用对应域名下的html
html = await fetch(app.entry).then(res => res.text())
//获取要渲染html的dom元素
const container = document.querySelector(app.container)
//将获取的html挂载在获取的dom元素上
container.innerHTML = html
}
观察控制台,我们发现html已经被成功挂载在主应用的容器里,但是,页面并没有如我们预期的那样显示出来
原因在于,页面的渲染需要通过执行 js代码来生成内容,浏览器出于安全考虑,innerHTML中生成的script不会被加载。因此,script标签的解析及处理是微前端框架的第一步核心操作。
资源请求函数封装
在乾坤框架里,对于html的资源请求,阿里做了单独的插件。这个插件包含三个核心对象
- template:用于获取html模板
- getExternalScripts()函数:获取所有的script标签代码
- execScripts()函数:获取并执行所有的js脚本
类似的,我们也可以封装一个这样的函数。由于html和js内容都是通过fetch进行请求的,我们可以先将fetch函数进行封装。
- 在src\micro-fe\文件夹下创建fetch-resource.js文件
export const fetchResource = url => fetch(url).then(res => res.text())
- 我们在src\micro-fe\文件夹下创建import-html.js文件。
import { fetchResource } from "./fetch-resource"
export const importHTML = async (url) =>{
//获取html模板
const html = await fetch(url).then(res => res.text())
const template = document.createElement('div')
template.innerHTML = html
//获取所有的script标签代码
function getExternalScripts () {
}
//执行所有的js脚本
function execScripts () {
}
return {
template,
getExternalScripts,
execScripts
}
}
- 最后,我们将封装好的 importHT ML函数导出在src\micro-fe\handle-router.js中引入使用
//处理路由变化的
import { getApps } from "./index";
import { importHTML } from './import-html'
export const handleRouter = async () => {
//2.匹配子应用
const apps = getApps()
const app = apps.find(item => window.location.pathname.startsWith(item.activeRule))
if(!app){
return
}
//3.加载子应用
// 请求子应用的资源:HTML、CSS、js
const { template,getExternalScripts,execScripts } = await importHTML(app.entry)
const container = document.querySelector(app.container)
container.appendChild(template)
}
和之前一样,这时的html可以被挂载到html页面上,但js不能被正确引入处理。
获取所有的script标签及js代码
- 获取所有的script标签:
constscripts = template.querySelectorAll('script')
注:获取到的constscripts是一个类数组,我们可以使用ES6的Array.from()方法将其转换成真数组
- 解析scripts报签内的js内容
对于scripts报签,有两种形式
//行内标签
<script>console.log('行内标签');</script>
//外链标签
<script src="./a.js"></script>
- 行内标签解析:直接使用innerHTML获取内容
- 外链标签解析:使用fecth函数请求内容
import-html.js文件内添加相应逻辑:
import { fetchResource } from "./fetch-resource"
export const importHTML = async (url) =>{
//--------------------------------------获取子应用html内容
const html = await fetchResource(url)
const template = document.createElement('div')
template.innerHTML = html
//--------------------------------------获取子应用所有js内容
const scripts = template.querySelectorAll('script')
//获取所有的script标签代码:返回一个[代码,代码]的数组
function getExternalScripts () {
return Promise.all(Array.from(scripts).map(script => {
//获取标签的src属性
const src = script.getAttribute('src')
//加载script标签的js代码
if(!src){
return Promise.resolve(script.innerHTML)
}else{
return fetchResource(
//有的标签没有域名,我们需要添加域名
src.startsWith('http') ? src : `${url}${src}`
)
}
}))
}
//打印获取的scrip标签内容
getExternalScripts().then((scripts) =>{
console.log('3.2获取所有scripts代码',scripts);
})
//获取并执行所有的js脚本
function execScripts () { }
return { template, getExternalScripts, execScripts }
}
观察浏览器控制台,可以看到,所有的js内容已经获取到了,虽然目前只是字符串形式
渲染子应用
现在,我们已经知道,执行完获取的子应用js代码后,页面上就应该能够渲染出我们的子应用界面。
执行获取的js内容
我们使用eval函数来执行这些脚本内容。
//mainApp\src\micro-fe\import-html.js
import { fetchResource } from "./fetch-resource"
export const importHTML = async (url) =>{
const html = await fetchResource(url)
const template = document.createElement('div')
template.innerHTML = html
const scripts = template.querySelectorAll('script')
function getExternalScripts () {
return Promise.all(Array.from(scripts).map(script => {
const src = script.getAttribute('src')
if(!src){
return Promise.resolve(script.innerHTML)
}else{
return fetchResource(
src.startsWith('http') ? src : `${url}${src}`
)
}
}))
}
//----------------------------------执行所有的js脚本
async function execScripts () {
const script = await getExternalScripts()
script.forEach(code => {
eval(code)
})
}
return {
template,
getExternalScripts,
execScripts
}
}
execScripts函数我们需要在handle-router.js内手动执行
//处理路由变化
import { getApps } from "./index";
import { importHTML } from './import-html'
export const handleRouter = async () => {
const apps = getApps()
const app = apps.find(item => window.location.pathname.startsWith(item.activeRule))
if(!app){
return
}
// 加载子应用
const { template,getExternalScripts,execScripts } = await importHTML(app.entry)
const container = document.querySelector(app.container)
container.appendChild(template)
// 渲染子应用
execScripts()
}
切换路由,我们会发现,我们的页面被成功渲染出来!!!但是,页面整体被替换成了vue2的内容,而不是渲染在我们指定的容器里。
这是为什么呢?其实很简单,当前我们的全局没有微前端的全局变量,vue2子应用在渲染时没有走微前端框架,直接渲染在了id = app这个容器里,而这个容器也是主应用的容器名称。
因此,解决办法也很简单,要么直接更换主应用的容器,或者我们使用我们的微前端框架来手动调用子应用的render函数。
使用微前端框架执行render函数
要使子应用运行在微前端框架里,我们只需要在handle-router.js中添加变量window.MICRO_WEB = true即可
//处理路由变化
import { getApps } from "./index";
import { importHTML } from './import-html'
export const handleRouter = async () => {
const apps = getApps()
const app = apps.find(item => window.location.pathname.startsWith(item.activeRule))
if(!app){
return
}
const { template,getExternalScripts,execScripts } = await importHTML(app.entry)
const container = document.querySelector(app.container)
container.appendChild(template)
//配置全局变量
window.__MICRO_WEB__ = true
// 3.2执行获取的js内容
execScripts()
}
此时,切换路由就不会出现app根节点被替换的问题,但由于vue2子应用的render函数没有执行,所以页面依旧不会有内容。我们需要做的就是拿到子应用的生命周期函数,手动执行render函数。
那现在核心的问题应该是我们如何拿到子应用的生命周期函数。
umd
子打包的时候,我们做了如下配置
output: {
// 把子应用打包成 umd 库格式
libraryTarget: 'umd', //umd格式 支持comm.js引入 浏览器、node可以识别
filename: 'vue2.js', //打包出的文件名称
library: 'vue2', //全局可以通过window.vue2拿到的应用
jsonpFunction: `webpackJsonp_${name}`,
},
我们这里将文件打包成了一个umd模块化的js文件
什么是UMD
UMD (Universal Module Definition),就是一种javascript通用模块定义规范,让你的模块能在javascript所有运行环境中发挥作用。
我们来学习一下umd模块化后的代码,我们在vue2的子应用配置文件vue.config.js中配置mode:‘development’
module.exports = {
configureWebpack: {
mode:'development',//这样打包可以看到未压缩的代码
...
},
...
}
然后,执行 npm run build 进行打包
观察打包后的vue2.js的代码,它的简化逻辑如下
vue2.js打包后的代码,这里实际是一个自执行函数,主要功能是兼容不同的模块规范
//umd的模块格式
(function webpackUniversalModuleDefinition(root, factory) {
//兼容Commojs模块规范 node环境
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
//兼容amd规范
else if(typeof define === 'function' && define.amd)
define([], factory);
//兼容ES6
else if(typeof exports === 'object')
exports["vue2"] = factory();
else
//挂载到去局Window对象上
root["vue2"] = factory();
})(window, function() {
//这里是自定义的内部代码
//最后会返回结果
});
简化逻辑
//umd的模块格式
function webpackUniversalModuleDefinition(root, factory) {
})
webpackUniversalModuleDefinition(window, factory)
//factory是框架内部的代码
根据以上知识,通过umd模块化打包后的js文件,我们可以通过window.拿到子应用的内容
获取子应用的生命周期函数
通过umd,我们在微前端框架里可以使用window对象来获取子应用的生命周期函数。
//3.2----------------------------------执行所有的js脚本
async function execScripts () {
const script = await getExternalScripts()
console.log('3.2获取所有scripts代码',script);
script.forEach(code => {
eval(code)
})
//子应用的所有信息
console.log(window['vue2']);
}
由于每个子应用打包后的名称不一样,这样获取子应用的属性需要知道打包后的子应用名称,因此,我们通过别的方式来获取这个子应用的对象。
通过umd的代码,我们可以知道子应用的信息也可以通过CommnJs的方式拿到,因此,我们手动创建个CommnJs环境。
//3.2----------------------------------执行所有的js脚本
async function execScripts () {
const script = await getExternalScripts()
console.log('3.2获取所有scripts代码',script);
//手动构建一个commonJs环境
const module = { exports:{}}
const exports = module.exports
script.forEach(code => {
//eval执行的代码可以访问外部变量
eval(code)
})
}
结合umd的封装函数
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
我们可以知道,通过 module.exports 就可以拿到所有的子应用属性。为了代码的简洁性,我们通过execScripts函数把这个对象暴露出去,在handle-router.js中获取这个值
import { fetchResource } from "./fetch-resource"
export const importHTML = async (url) =>{
const html = await fetchResource(url)
const template = document.createElement('div')
template.innerHTML = html
//--------------------------------------获取子应用所有js内容
const scripts = template.querySelectorAll('script')
//获取所有的script标签代码:返回一个[代码,代码]的数组
function getExternalScripts () {
return Promise.all(Array.from(scripts).map(script => {
const src = script.getAttribute('src')
if(!src){
return Promise.resolve(script.innerHTML)
}else{
return fetchResource(
src.startsWith('http') ? src : `${url}${src}`
)
}
}))
}
//3.2----------------------------------执行所有的js脚本
async function execScripts () {
const script = await getExternalScripts()
//手动构建一个commonJs环境
const module = { exports:{}}
const exports = module.exports
script.forEach(code => {
eval(code)
})
return module.exports
}
return {
template,
getExternalScripts,
execScripts
}
}
mainApp\src\micro-fe\handle-router.js
//处理路由变化
import { getApps } from "./index";
import { importHTML } from './import-html'
export const handleRouter = async () => {
//2.匹配子应用
const apps = getApps()
const app = apps.find(item => window.location.pathname.startsWith(item.activeRule))
if(!app){
return
}
//3.加载子应用
const { template,getExternalScripts,execScripts } = await importHTML(app.entry)
const container = document.querySelector(app.container)
container.appendChild(template)
//配置全局变量
window.__MICRO_WEB__ = true
// 4.渲染子应用
const appExPports = await execScripts()
}
可以看到,这里的值我们是正常拿到的。
这时,只要执行mount周期函数里的render函数,我们就可以实现子应用的挂载渲染。
执行子应用的生命周期函数
对获取到的生命周期函数信息,我们可以将其绑定在我们的配置列表app里,然后使用app.mount 对这个生命周期函数进行调用即可。为了便于函数逻辑的添加,我们可以把生命周期函数的调用都封装成函数。
- 获取子应用生命周期函数
const appExPports = await execScripts()
- 将函数绑定在app上
app.bootstrap = appExPports.bootstrap
app.mount = appExPports.mount
app.unmount = appExPports.unmount
- 封装相应生命周期执行函数
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())
}
- 执行封装的函数
await bootstrap(app)
await mount(app)
handle-router.js完成代码
//处理路由变化
import { getApps } from "./index";
import { importHTML } from './import-html'
export const handleRouter = async () => {
console.log('2.路由已经发生变化,现在进行处理');
//2.匹配子应用
// 2.1获取当前的路由路径
console.log('2.1获取当前路由路径',window.location.pathname);
// 2.2去 apps 里面查找
const apps = getApps()
const app = apps.find(item => window.location.pathname.startsWith(item.activeRule))
if(!app){
return
}
//3.加载子应用
// 3.1将获取的HTML插入容器里
const { template,getExternalScripts,execScripts } = await importHTML(app.entry)
const container = document.querySelector(app.container)
container.appendChild(template)
//配置全局变量
window.__MICRO_WEB__ = true
// 3.2执行获取的js内容;返回子应用的生命周期函数
const appExPports = await execScripts()
//3.3将获取的生命周期函数储存在app上
app.bootstrap = appExPports.bootstrap
app.mount = appExPports.mount
app.unmount = appExPports.unmount
//3.4执行生命周期函数
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())
}
切换路由,发现我们的基本功能已经实现了!!
卸载前一个子应用
当我们切换路由时,会发现页面内容不断变长,原因在于原先的子应用并没有被卸载。因此,我们需要实现应用切换时,卸载上一个子应用,实现这个功能,我们就需要得到浏览器的历史记录。
浏览器由于安全问题,没有提供路由的历史记录获取API,我们需要自己在路由监视的代码中自己维护一个路由记录。
- 创建两个变量,分别用于储存路由变化前的值及变化后的值
let prevRoute = '' //上一个路由
let nextRoute = window.location.pathname //下一个路由
- 变量赋值
- 对于pushState和replaceState的路由变化
在路由的真正历史记录前后分别对两个值进行赋值
const rawPushState = window.history.pushState
window.history.pushState = (...args) =>{
prevRoute = window.location.pathname
rawPushState.apply(window.history,args) //真正的改变历史记录
nextRoute = window.location.pathname
handleRouter()
}
const rawReplaceState = window.history.replaceState
window.history.repalceState = (...args) =>{
prevRoute = window.location.pathname
rawReplaceState .apply(window.history,args)
nextRoute = window.location.pathname
handleRouter()
}
- 对于popstate事件,我们需要将nextRoute的值赋给prevRoute,nextRoute = window.location.pathname
prevRoute = nextRoute
nextRoute = window.location.pathname
- 暴露出这两个变量,供其他地方使用
export const getPrevRoute = () => prevRoute
export const getNextRoute = () => nextRoute
- 根据获取的上一次子应用路由,匹配子应用信息,卸载子应用
const apps = getApps()
//------------------卸载上一个路由
const prevApp = apps.find(item => {
return getPrevRoute().startsWith(item.activeRule)
})
mainApp\src\micro-fe\rewrite-router.js
//处理路由相关变化逻辑
import { handleRouter } from "./handle-router";
let prevRoute = '' //上一个路由
let nextRoute = window.location.pathname //下一个路由
export const getPrevRoute = () => prevRoute
export const getNextRoute = () => nextRoute
export const rewriteRouter = () =>{
//1.监视路由的变化
// hash路由 window.onhashchange
// history路由
// history.go、history.back、history.forword 使用popstate事件:window.onpopstate
window.addEventListener('popstate',()=>{
prevRoute = nextRoute
nextRoute = window.location.pathname
handleRouter()
})
//pushState、及repalceState popstate事件监听不到,我们需要重写pushState、及repalceState的原生方法
const rawPushState = window.history.pushState
window.history.pushState = (...args) =>{
//导航前
prevRoute = window.location.pathname
rawPushState.apply(window.history,args) //真正的改变历史记录
//导航后
nextRoute = window.location.pathname
handleRouter()
}
const rawReplaceState = window.history.replaceState
window.history.repalceState = (...args) =>{
//导航前
prevRoute = window.location.pathname
rawReplaceState .apply(window.history,args)
//导航后
nextRoute = window.location.pathname
handleRouter()
}
}//处理路由变化
import { getApps } from "./index";
import { importHTML } from './import-html'
import { getNextRoute, getPrevRoute } from "./rewrite-router";
export const handleRouter = async () => {
//1.获取子路由配置信息
const apps = getApps()
//2.匹配子应用
//------------------卸载上一个路由
const prevApp = apps.find(item => {
return getPrevRoute().startsWith(item.activeRule)
})
if(prevApp) await unmount(prevApp)
//------------------加载下一个路由
const app = apps.find(item => getNextRoute().startsWith(item.activeRule))
if(!app) return
//3.加载子应用
const { template,getExternalScripts,execScripts } = await importHTML(app.entry)
const container = document.querySelector(app.container)
container.appendChild(template)
//配置全局变量
window.__MICRO_WEB__ = true
// 3.2获取子应用的生命周期函数
const appExPports = await execScripts()
//4.渲染子应用
//将获取的生命周期函数储存在app上
app.bootstrap = appExPports.bootstrap
app.mount = appExPports.mount
app.unmount = appExPports.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)
}))
}
mainApp\src\micro-fe\handle-router.js
//处理路由变化
import { getApps } from "./index";
import { importHTML } from './import-html'
import { getNextRoute, getPrevRoute } from "./rewrite-router";
export const handleRouter = async () => {
//1.获取子路由配置信息
const apps = getApps()
//2.匹配子应用
//------------------卸载上一个路由
const prevApp = apps.find(item => {
return getPrevRoute().startsWith(item.activeRule)
})
if(prevApp) await unmount(prevApp)
//------------------加载下一个路由
const app = apps.find(item => getNextRoute().startsWith(item.activeRule))
if(!app) return
//3.加载子应用
const { template,getExternalScripts,execScripts } = await importHTML(app.entry)
const container = document.querySelector(app.container)
container.appendChild(template)
//配置全局变量
window.__MICRO_WEB__ = true
// 3.2获取子应用的生命周期函数
const appExPports = await execScripts()
//4.渲染子应用
//将获取的生命周期函数储存在app上
app.bootstrap = appExPports.bootstrap
app.mount = appExPports.mount
app.unmount = appExPports.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)
}))
}
样式资源文件的加载
在微前端框架里的子应用图片资源是加载不到的。
因为图片的路径是子应用ip + url (9004 + url)
在主应用请求后就变成了主应用ip+ url (8080 + url)
要解决此问题,我们可以在子应用里把路径写死,像这样
module.exports = {
publicPath:'http://localhost:9004',
}
这当然不是个好办法。
webpack支持运行时的publicPath
import './public-path.js'
module.exports = {
publicPath:webpack_public_path,
}
//webpack在运行时生成的路径会自动拼接上这个全局变量,如果有的话
webpack_public_path = http://localhost:9004
我们使用微前端框架来提供
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
//配置全局变量
window.__MICRO_WEB__ = true
window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = app.entry + "/"