需求背景
- 在项目最初,学校管理和主站(即商城)就是两个单独的项目,并独立部署通过各自的域名访问,用户点击‘学校管理’的tab新打开窗口浏览页面。
- 在某次的需求中,产品要求将‘学校管理’放在主站当前页的tab下访问,由于排期时间较紧,就采取了iframe的方式将其嵌入到主站中。
- 由于每次从其他tab页切换到‘学校管理’时,总会出现一段白屏加载时间,则在后期换成了微前端的实现方式。
两种实现方式的效果图
1.iframe方式
2.single-spa方式
iframe和single-spa的优缺点
iframe的优点
- 完全隔离了css和js,避免了各个系统之间的样式和js污染。
- 可以在子系统完全不修改的情况下嵌入进来。
iframe的缺点
- 页面加载问题: 影响主页面加载,阻塞onload事件,本身加载也很慢,页面缓存过多会导致电脑卡顿。
- 布局问题:iframe必须给一个指定的高度,否则会塌陷。解决办法:子系统实时计算高度并通过postMessage发送给主页面,主页面动态设置高度,修改子系统或者代理插入脚本。有些情况会出现多个滚动条,用户体验不佳。
single-spa的优点
- 加载快,可以将所有系统共用的模块提取出来,实现按需加载,一次加载,其他的复用。
- 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染。
- http请求少,服务器压力小。
single-spa的缺点
- css和js需要制定规范,进行隔离。否则容易造成全局污染,尤其是vue的全局组件,全局钩子。
- 子系统需少量改动,是不影响子系统独立开发部署及其功能。
single-spa概览
Single-spa 是一个将多个单页面应用聚合为一个整体应用的 javascript 微前端框架。其包括以下内容:
- Applications,每个应用程序本身就是一个完整的 SPA (某种程度上)。 每个应用程序都可以响应 url 路由事件,并且必须知道如何从 DOM 中初始化、挂载和卸载自己。 传统 SPA 应用程序和 Single SPA 应用程序的主要区别在于,它们必须能够与其他应用程序共存,而且它们没有各自的 html 页面。
- 一个 single-spa-config配置, 这是html页面和向Single SPA注册应用程序的JavaScript。每个应用程序都注册了三件东西。
- A name
- A function (加载应用程序的代码)
- A function (确定应用程序何时处于活动状态/非活动状态)
single-spa的实现
父项目处理
- 依赖安装。
yarn add single-spa -D
- 在router.js中增加一条路由。
{
path: 'schoolManage*',
name: 'schoolManage',
component: schoolManage,
meta: {
role: ['校长', '教务', '魔法双师主管', '魔法双师-校区主管', '校区主管', 'AI主管'],
keepAlive: true
},
beforeEnter: requirePermission
}
- 在src目录下新增single-spa-confing.js文件。
singleSpa.registerApplication( // 注册微服务
'singleSchool',
async () => {
let singleSchool = null
await getManifest(`${JIAOWU_URL}/manifest.json?v=${new Date().getTime()}`, 'app').then(() => {
singleSchool = window.singleSchool
})
return singleSchool
},
location => location.pathname.startsWith('/schoolManage') // 配置微前端模块前缀
)
/*
* getManifest:远程加载manifest.json 文件,解析需要加载的js
* */
const getManifest = (url, bundle) => new Promise(async (resolve) => {
const { data } = await axios.get(url)
const { entrypoints, publicPath } = data
const assets = entrypoints[bundle].assets
for (let i = 0; i < assets.length; i++) {
await runScript(publicPath assets[i]).then(() => {
if (i === assets.length - 1) {
resolve()
}
})
}
})
const runScript = async (url) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = url
script.onload = resolve
script.onerror = reject
const firstScript = document.getElementsByTagName('script')[0]
firstScript.parentNode.insertBefore(script, firstScript)
})
}
子项目处理
- 安装依赖
yarn add single-spa-vue -D
- 在main.js中引入single-spa-vue。
const vueOptions = {
el: '#school-container',
router,
store,
render: h => h(App),
data: {
isViewSidebar: true
}
}
// singleSpaVue包装一个vue微前端服务对象
const vueLifecycles = singleSpaVue({
Vue,
appOptions: vueOptions
})
export const bootstrap = vueLifecycles.bootstrap // 启动时
export const mount = vueLifecycles.mount // 挂载时
export const unmount = vueLifecycles.unmount // 卸载时
webpack的处理
- 修改vue.config.js文件,将导出的微前端对象挂载到window上。
configureWebpack: (config) => {
const buildObj = {
name: name,
output: {
library: 'singleSchool',
libraryTarget: 'window'
}
}
}
- 安装stats-webpack-plugin依赖包,然后修改webpack的plugin配置,打包生成manifest.json文件(打包生成的静态资源文件目录表)。
plugins: [
new StatsPlugin('manifest.json', {
chunkModules: false,
entrypoints: true,
source: false,
chunks: false,
modules: false,
assets: false,
children: false,
exclude: [/node_modules/]
})
]
- 由于不同环境的域名不同,需修改publicPath的配置。
publicPath: process.env.NODE_ENV === 'development' ? '//localhost:8082/' : process.env.VUE_APP_PUBLIC_PATH,
- 样式隔离
- 安装postcss-selector-namespace插件。
yarn add postcss-selector-namespace -D
- 增加postcss中plugin的相关配置。
css: {
extract: false,
loaderOptions: {
sass: {
prependData: `
@import "@/styles/variables.scss";
`
},
postcss: {
plugins: [
require('postcss-selector-namespace')({
namespace (css) {
if (css.includes('noNamespace.css')) {
return ''
}
return '.single-spa-school'
}
})
]
}
}
}
项目之间通信处理
1、废弃方式:采用的是H5提供的postMessage方法进行通信,后期需优化。 例如:子项目之间页面相互跳转:
created () {
if (!window.ADD_LISTENNER_MESSAGE) {
window.addEventListener('message', this.handleUrl, false)
}
}
methods: {
handleUrl (data) {
if (data.data && data.data.fun && data.data.type === 'router') {
let query = data.data.query ? data.data.query : {}
this.$router.push({
name: data.data.fun,
query
})
}
}
}
window.postMessage({fun: 'setting.balance', type: 'router'}, '*')
2、新方式:采用从主项目中传入eventHub(在注册微服务的方法中的第四个参数中传入)给子项目,在主项目中监听时间,在子项目中触发时间即可。
singleSpa.registerApplication( // 注册微服务
'singleSchool',
async () => {
let singleSchool = null
await getManifest(`${JIAOWU_URL}/manifest.json?v=${new Date().getTime()}`, 'app').then(() => {
singleSchool = window.singleSchool
})
return singleSchool
},
location => location.pathname.startsWith('/schoolManage'), // 配置微前端模块前缀
{ eventHub }
)