single-spa 文档https://zh-hans.single-spa.js.org/docs/getting-started-overview

single-spa 实现技术特点

1.项目独立部署 

2.兼容技术栈不同多语言 vue react 

3.新旧代码版本并行

single-SPA

实现原理 :路有劫持 和应用加载 

没有处理css样式和js 会导致样式重叠 和脏数据

下一期 会出qiankun的教程 其实乾坤是基于single-SPA进一步完善的前端微服务架构 使用也很简单 开箱即用的API (single-spa+sandbox+import-html-entry)父子应用协议接入(bootstrap mount unmount)

ifream 嵌套 其实也可以实现前端 的 微服务逻辑 但是最终还是被废弃掉了因为是有很多很多本质上的问题 比如

页面刷新状态丢失、url通信方式传递消息功能性较弱等问题

其实通信方式有很多比如:

利用浏览器默认方法 customevent

基于props 主子应用通信

全局变量通信 redux

CDN-externals

webpack 联邦模块等等  闲话少说  接下来我们开始我们的微前端的single-spa之旅

开始

创建父子应用parent-vue 和child-vue (最简单的包含router的 vue应用即可 )

父子应用分别安装 single-spa-vue

child-vue main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import singleSpaVue from 'single-spa-vue'

Vue.config.productionTip = false

const appOptions = {
  el: '#vue',
  router,
  render: h => h(App)
}
const vueLifeCycle = singleSpaVue({
  Vue,
  appOptions
})
// 判断是否是父应用引用我
if (window.singleSpaNavigate) {
  __webpack_public_path__ = 'http://localhost:10001/'
}
if (!window.singleSpaNavigate) {
  delete appOptions.el
  new Vue(appOptions).$mount('#app')
}

// 协议接入 我订好了协议 父应用会调用这些方法

export const bootstrap = vueLifeCycle.bootstrap
export const mount = vueLifeCycle.mount
export const unmount = vueLifeCycle.unmount

// new Vue({
//   router,
//   render: h => h(App)
// }).$mount('#app')

// 父应用加载子应用 将子应用打包成一个个lib 去给父应用使用
// 没有new vue 无法启动 所以 需要新建一个 vue.config.js 配置启动
// bootstrap  mount unmount
// single-spa  /single-spa-vue  single-spa-react

vue.config.js

module.exports = {
  configureWebpack: {
    output: {
      library: 'singleVue',
      libraryTarget: 'umd'
    },
    devServer: {
      port: 10000
    }
  }
}

讲解一下这里的配置是为了打包成umd模块  做过vue 组件开发的小伙伴应该熟悉 最后最后会把注册的几个bootstrap mount unmout 挂载在到window.singleVue下

parent-vue main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { registerApplication, start } from 'single-spa'

Vue.config.productionTip = false
async function loadScript (url) {
  return new Promise((resolve, reject) => {
    let script = document.createElement('script')
    script.src = url
    script.onload = resolve
    script.onerror = reject
    document.head.appendChild(script)
  })

}
// singlespa 缺陷 不够灵活 不能动态加载js文件 
// 样式不隔离 没有js沙箱的机制



registerApplication('myVueApp',
  async () => {
    console.log('加载子模块')
    await loadScript('http://localhost:10001/js/chunk-vendors.js')
    await loadScript('http://localhost:10001/js/app.js')
    return window.singleVue

  },
  location => location.pathname.startsWith('/vue'), // 检测用户切换到 /vue  路径下 需要实现刚刚定义的加载事件
  { //可以是方法或者是函数  传递给子应用实现父子的通信 mount unmout方法  CustomEvent 全局  url
    a: '1',
    b: '2'
  }
)
start()

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

这样基本上所有的业务逻辑都在这里了 接下里看一下效果

微服务架构 前后端分离 前端微服务singlespa_vue 微服务

微服务架构 前后端分离 前端微服务singlespa_微服务_02

微服务架构 前后端分离 前端微服务singlespa_微服务架构 前后端分离_03

微服务架构 前后端分离 前端微服务singlespa_微服务_04

上面说到的样式问题通过一些技术方案都是可以解决的例如 脏数据

如果 应用加载 刚开始我加载A应用 window.a B应用window.b

但应用切换 沙箱 创造一个干净的 环境给这个子应用 使用 当切换时 可以丢弃 属性和恢复属性

JS沙箱  proxy

快照沙箱 一年前拍一张照 现在再拍一张  (区别保存起来) 在回到一年前    ?进行差异对比? 不确定恢复

实现案例

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<title></title>
	</head>
	<body>
		<p>hello  world</p>
		<div id="shadow"></div>
		<script>
			/* 
			如果 应用加载 刚开始我加载A应用 window.a B应用window.b
			但应用切换 沙箱 创造一个干净的 环境给这个子应用 使用 当切换时 可以丢弃 属性和恢复属性
			
			JS沙箱  proxy
			
			快照沙箱 一年前拍一张照 现在再拍一张  (区别保存起来) 在回到一年前    ?进行差异对比? 不确定恢复
			
			 */
			
			class SnapshotSandbox{
				constructor() {
					this.proxy=window;//window属性
					this.modifyPropsMap={} // 记录在window上的修改
					this.active();
				}
				active(){
					this.windowSnapshot={} //拍照
					for(const prop in window){
						if(window.hasOwnProperty(prop)){
							this.windowSnapshot[prop]=window[prop]
						}
					}
					Object.keys(this.modifyPropsMap).forEach(p=>{
						window[p]=this.modifyPropsMap[p]
					})
				}
				inactive(){
					for(const prop in window){
						if(window.hasOwnProperty(prop)){
							if(window[prop]!==this.windowSnapshot[prop]){
								this.modifyPropsMap[prop]=window[prop]
								window[prop]=this.windowSnapshot[prop]
							}
						}
					}
				}
			}
			let sandbox=new SnapshotSandbox();
			((window)=>{
				window.a=1
				window.b=2
				window.e=function (){
					alert(1)
				}
				console.log(window.a,window.b,window.c,window.e)
				sandbox.inactive()
				console.log(window.a,window.b,window.c,window.e)
				window.c=3
				sandbox.active()
				console.log(window.a,window.b,window.c,window.e)
				window.b=4
				sandbox.inactive()
				window.b=4
				console.log(window.a,window.b,window.c,window.e)
				sandbox.active()
				console.log(window.a,window.b,window.c,window.e)
			})(sandbox.proxy) //sandbox.proxy 就是window
			// 如果是多个子应用的就不能这种方式 es6的proxy
			// 代理沙箱介意实现多应用沙箱 吧不同的应用用不同的代理来处理
			
			
		</script>
	</body>
</html>

子应用之间的样式的隔离 

1.Dynamic Stylesheet 动态样式表 根据当前的应用切换各自的样式 移除就样式

父子应用之间的样式隔离

BEM 预定项目前缀

Css-modules 打包时候生成不冲突的选择器名

shadow DOM 真正意义上的隔离 影子DOM video 按钮

CSS-in-js

实现案例

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<title></title>
	</head>
	<body>
		<p>hello  world</p>
		<div id="shadow"></div>
		<script>
			let shadowDom =shadow.attachShadow({mode:'closed'}) //外界无法访问 shadow dom
			let pElm=document.createElement('p')
			pElm.innerHTML='hello zf'
			let styleElm=document.createElement('style')
			styleElm.textContent='p{color:red}'
			shadowDom.appendChild(styleElm)
			shadowDom.appendChild(pElm)
			// document.body.appendChild(pElm) // react 项目 弹窗 
		</script>
	</body>
</html>

如果自己的实现效果不理想 这个是我的源代码 大家有兴趣的可以参考一下 由于是学习 记录我相信里面的额注释一定会对你很有帮助个人学习实现源码注释比较全面基于single-spa实现的的前端微服务架构内部含有single-spa不足技术点的解决方案相关的具体实现教程我-Web开发文档类资源