single-spa
一个基于JavaScript的 微前端 框架,他能够用于构建可共存的微前端应用,每一个前端应用均可以用本身的框架编写,完美支持 Vue React Angular。能够实现 服务注册 事件监听 子父组件通讯 等功能。javascript
用于 父项目 集成子项目使用css
single-spa-vue
是提供给使用vue子项目使用的npm
包。他能够快速和sigle-spa父项目集成,并提供了一些比较便携的api。html
用于 子项目 使用前端
咱们父项目和子项目都使用vue-cli进行集成。父项目为了美化,用ant-design-vue作前端框架。vue
新建一个项目,名称叫 parent
。咱们为了方便,暂时不引入vuex
和eslint
。记得,父项目的 vue-router
要开启history模式。java
接着咱们安装ant-design-vue
和 single-spa
,而后启动项目。node
npm install ant-design-vue single-spa --save -d
复制代码
咱们注册一个子服务路由,只是注册, 不填写component
字段。react
{
path: '/vue',
name: 'vue',
}
复制代码
咱们在父项目的入口 vue组件,简单地写一下咱们的基础布局。左边为菜单栏,右边是布局栏。webpack
左边菜单栏内有一项vue
列表项,vue
里面有2个路由。分别是子项目的 home
和 about
. 右侧内容栏内,增长一个id为 single-vue
的dom元素,这是咱们稍后子项目要挂载的目标dom元素。ios
<template>
<a-layout id="components-layout-demo-custom-trigger">
<a-layout-sider :trigger="null" collapsible v-model="collapsed">
<div class="logo" />
<a-menu theme="dark" mode="inline">
<a-sub-menu key="1">
<span slot="title">
<a-icon type="user" />
<span>Vue</span>
</span>
<a-menu-item key="1-1">
<a href="/vue#">
Home
</a>
</a-menu-item>
<a-menu-item key="1-2">
<a href="/vue#/about">
About
</a>
</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
<a-layout>
<a-layout-header style="background: #fff; padding: 0" />
<a-layout-content :style="{ margin: '24px 16px', padding: '24px', background: '#fff', minHeight: '280px' }">
<div class="content">
<!--这是右侧内容栏-->
<div id="single-vue" class="single-spa-vue">
<div id="vue"></div>
</div>
</div>
</a-layout-content>
</a-layout>
</a-layout>
</template>
<script>
export default {
data() {
return {
collapsed: false,
};
}
};
</script>
<style>
#components-layout-demo-custom-trigger .trigger {
font-size: 18px;
line-height: 64px;
padding: 0 24px;
cursor: pointer;
transition: color 0.3s;
}
#components-layout-demo-custom-trigger .trigger:hover {
color: #1890ff;
}
#components-layout-demo-custom-trigger .logo {
height: 32px;
background: rgba(255, 255, 255, 0.2);
margin: 16px;
}
</style>
复制代码
这里就是咱们的重头戏:如何使用single-spa注册子项目。在注册以前,咱们先了解一下2个api:
singleSpa.registerApplication:这是注册子项目的方法。参数以下:
location
对象,能够写自定义匹配路由加载规则。singleSpa.start:这是启动函数。
咱们新建一个 single-spa-config.js
,并在main.js
内引入。
// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import Ant from 'ant-design-vue';
import './single-spa-config.js'
import 'ant-design-vue/dist/antd.css';
Vue.config.productionTip = false;
Vue.use(Ant);
new Vue({
router,
render: h => h(App)
}).$mount('#app')
复制代码
single-spa-config.js:
// single-spa-config.js
import * as singleSpa from 'single-spa'; //导入single-spa
/*
* runScript:一个promise同步方法。能够代替建立一个script标签,而后加载服务
* */
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);
});
};
singleSpa.registerApplication( //注册微前端服务
'singleDemo',
async () => {
await runScript('http://127.0.0.1:3000/js/chunk-vendors.js');
await runScript('http://127.0.0.1:3000/js/app.js');
return window.singleVue;
},
location => location.pathname.startsWith('/vue') // 配置微前端模块前缀
);
singleSpa.start(); // 启动
复制代码
与官方文档不一样的是,咱们这里使用了 远程加载。远程加载的原理,咱们后面会单独写。
父项目就处理完毕了,接下来咱们处理子项目。
子项目的处理,比父项目就稍微复杂一些。
咱们仍是新建一个项目,叫作vue-child
,使用 vue create vue-child
建立。子项目的建立过程,就随意了,这里咱们忽略过程。
另外,咱们须要安装一个叫作 single-spa-vue
的npm包。
npm install single-spa-vue --save -d
复制代码
若是想注册为一个子项目,还须要 single-spa-vue
的包装。
在main.js
中引入 single-spa-vue
,传入Vue对象和vue.js挂载参数,就能够实现注册。它会返回一个对象,里面有single-spa 须要的生命周期函数。使用export
导出便可
import singleSpaVue from "single-spa-vue";
import Vue from 'vue'
const vueOptions = {
el: "#vue",
router,
store,
render: h => h(App)
};
// singleSpaVue包装一个vue微前端服务对象
const vueLifecycles = singleSpaVue({
Vue,
appOptions: vueOptions
});
// 导出生命周期对象
export const bootstrap = vueLifecycles.bootstrap; // 启动时
export const mount = vueLifecycles.mount; // 挂载时
export const unmount = vueLifecycles.unmount; // 卸载时
export default vueLifecycles;
复制代码
只是导出了,还须要挂载到window
。
在项目目录下新建 vue.config.js
, 修改咱们的webpack配置。咱们修改webpack output
内的 library
和 libraryTarget
字段。
同时,由于咱们是远程调用,还须要设置 publicPath
字段为你的真实服务地址。不然加载子chunk
时,会去当前浏览器域名的根路径寻找,有404问题。 由于咱们本地的服务启动是localhost:3000
,因此咱们就设置 //localhost:3000
。
module.exports = {
publicPath: "//localhost:3000/",
// css在全部环境下,都不单独打包为文件。这样是为了保证最小引入(只引入js)
css: {
extract: false
},
configureWebpack: {
devtool: 'none', // 不打包sourcemap
output: {
library: "singleVue", // 导出名称
libraryTarget: "window", //挂载目标
}
},
devServer: {
contentBase: './',
compress: true,
}
};
复制代码
咱们执行 vue-cli-service serve --port 3000
后,就能够看到一直等待的界面了~
其中,左侧能够切换子项目中的路由。右侧联网加载。
这样,咱们的初版就大功告成了。接下来,咱们作进一步优化和分享
样式隔离这块,咱们使用postcss
的一个插件:postcss-selector-namespace。 他会把你项目里的全部css都会添加一个类名前缀。这样就能够实现命名空间隔离。
首先,咱们先安装这个插件:npm install postcss-selector-namespace --save -d
项目目录下新建 postcss.config.js
,使用插件:
// postcss.config.js
module.exports = {
plugins: {
// postcss-selector-namespace: 给全部css添加统一前缀,而后父项目添加命名空间
'postcss-selector-namespace': {
namespace(css) {
// element-ui的样式不须要添加命名空间
if (css.includes('element-variables.scss')) return '';
return '.single-spa-vue' // 返回要添加的类名
}
},
}
}
复制代码
在父项目要挂载的区块,添加咱们的命名空间。结束
你们可能会发现,咱们的子服务如今是没法独立运行的,如今咱们改造为能够独立 + 集成双模式运行。
single-spa
有个属性,叫作 window.singleSpaNavigate
。若是为true,表明就是single-spa模式。若是false,就能够独立渲染。
咱们改造一会儿项目的main.js
:
// main.js
const vueOptions = {
el: "#vue",
router,
render: h => h(App)
};
/**** 添加这里 ****/
if (!window.singleSpaNavigate) { // 若是不是single-spa模式
delete vueOptions.el;
new Vue(vueOptions).$mount('#vue');
}
/**** 结束 ****/
// singleSpaVue包装一个vue微前端服务对象
const vueLifecycles = singleSpaVue({
Vue,
appOptions: vueOptions
});
复制代码
这样,咱们就能够独立访问子服务的 index.html
。不要忘记在public/index.html
里面添加命名空间,不然会丢失样式。
<div class="single-spa-vue">
<div id="app"></div>
</div>
复制代码
在这里,咱们的远程加载使用的是async await
构建一个同步执行任务。
建立一个script
标签,等script
加载后,返回script
加载到window
上面的对象。
/*
* runScript:一个promise同步方法。能够代替建立一个script标签,而后加载服务
* */
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);
});
};
复制代码
Vue 2.x的dom挂载,采起的是 覆盖Dom挂载 的方式。例如,组件要挂载到#app
上,那么它会用组件覆盖掉#app
元素。
可是React/Angular不一样,它们的挂载方式是在目标挂载元素的内部添加元素
,而不是直接覆盖掉。 例如组件要挂载到#app
上,那么他会在#app
内部挂载组件,#app
还存在。
这样就形成了一个问题,当我从 vue子项目 => react项目 => vue子项目时,就会找不到要挂载的dom元素,从而抛出错误。
解决这个问题的方案是,让 vue项目组件的根元素类名/ID名和要挂载的元素一致 就能够。
例如咱们要挂载到 #app
这个dom上,那么咱们子项目内部的app.vue,最顶部的dom元素id名也应该叫 #app
。
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
</div>
</template>
复制代码
在上面父项目加载子项目的代码中,咱们能够看到。咱们要注册一个子服务,须要一次性加载2个JS文件。若是须要加载的JS更多,甚至生产环境的 bundle 有惟一hash, 那咱们还能写死文件名和列表吗?
singleSpa.registerApplication(
'singleVue',
async () => {
await runScript('http://127.0.0.1:3000/js/chunk-vendors.js'); // 写死的文件列表
await runScript('http://127.0.0.1:3000/js/app.js');
return window.singleVue;
},
location => location.pathname.startsWith('/vue')
);
复制代码
咱们的实现思路,就是让子项目使用 stats-webpack-plugin
插件,每次打包后都输出一个 只包含重要信息的manifest.json
文件。父项目先ajax 请求 这个json文件,从中读取出须要加载的js目录,而后同步加载。
这里就不得不提到这个webpack plugin了。它能够在你每次打包结束后,都生成一个manifest.json
文件,里面存放着本次打包的 public_path
bundle list
chunk list
文件大小依赖等等信息。
{
"errors": [],
"warnings": [],
"version": "4.41.4",
"hash": "d0601ce74a7b9821751e",
"publicPath": "//localhost:3000/",
"outputPath": "/Users/janlay/juejin-single/vue-chlid/dist",
"entrypoints": { // 只使用这个字段
"app": {
"chunks": [
"chunk-vendors",
"app"
],
"assets": [
"js/chunk-vendors.75fba470.js",
"js/app.3249afbe.js"
],
"children": {},
"childAssets": {}
}
... ...
}
复制代码
咱们切换到子项目的目录,安装这个webpack插件:
npm install stats-webpack-plugin --save -d
复制代码
在vue.config.js
中使用:
{
configureWebpack: {
devtool: 'none',
output: {
library: "singleVue",
libraryTarget: "window",
},
/**** 添加开头 ****/
plugins: [
new StatsPlugin('manifest.json', {
chunkModules: false,
entrypoints: true,
source: false,
chunks: false,
modules: false,
assets: false,
children: false,
exclude: [/node_modules/]
}),
]
/**** 添加结尾 ****/
}
}
复制代码
具体的配置项,能够访问 webpack 中文文档 - configuration - stats 查阅
固然,父项目中的单runScript已经没法支持使用了,写个getManifest方法,处理一下。
/*
* getManifest:远程加载manifest.json 文件,解析须要加载的js
* url: manifest.json 连接
* bundle:entry名称
* */
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()
}
})
}
});
复制代码
咱们首先ajax到 manifest.json
文件,解构出里面的 entrypoints
publicPath
字段,遍历出真实的js路径,而后按照顺序加载。
async () => {
let singleVue = null;
await getManifest('http://127.0.0.1:3000/manifest.json', 'app').then(() => {
singleVue = window.singleVue;
});
return singleVue;
},
复制代码
其实,若是要作一个 微前端管理平台,也是靠这个实现。
这里推荐一位dalao的原理分享:连接
可使用发布订阅模式实现,也能够实现一个相似于vuex的状态管理
这个可使用proxy 进行监听,切换保存,切入还原状态
本文代码仓库:码云
欢迎加入微前端讨论群,一块儿研究。