介绍
qiankun 基于 single-spa 实现; 什么是微前端?个人理解类似于后端的微服务;不会说某些代码层面的错误会导致整体宕机。这也对照上官网对 qiankun 特性的介绍;
-
独立开发、独立部署:微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新;
-
独立运行:开发时可以只运行某一个子应用;
-
技术栈无关:任意前端技术都可接入;
-
增量更新:与独立部署应该是差不多意思,可以单独更新某个子应用;
快速上手
开始之前先安利每日优鲜团队微前端实践;绝对的最佳实践。 下面示例中均使用 Vue 技术栈;
主应用
项目创建完成后,主应用安装qiankun;
npm install qiankun -s
在 main.js 中注册子应用;
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import { registerMicroApps, start } from 'qiankun';
Vue.config.productionTip = false;
new Vue({
router,
render: h => h(App)
}).$mount('#app');
/**
* @param {*} app配置对象
* @param {*} 生命周期钩子
*/
registerMicroApps([
{
name: 'vueApp',
entry: '//localhost:3000',
container: '#container',
activeRule: '/vueApp'
}
]);
start();
然后再 app.vue 中给子应用放一个容器;这个容器的 id 就是 registerMicroApps
中第一个参数配置对象中的 container;其他参数后面在介绍。
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
<div id="container"></div>
</div>
</template>
子应用
子应用不需要下载 qiankun 整体来说需要修改四个地方;
1,项目 src 新增 public-path.js 文件
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
2,在入口文件最顶部引入 public-path.js,修改并导出三个生命周期函数
import './public-path';
import Vue from 'vue';
import App from './App.vue';
import router from './router';
Vue.config.productionTip = false;
let instance = null;
function render(props = {}) {
const { container } = props;
instance = new Vue({
router,
render: h => h(App)
}).$mount(container ? container.querySelector('#app') : '#app');
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
// 必须把生命周期钩子导出
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
export async function mount(props) {
console.log('[vue] props from main framework', props);
render(props);
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
}
4,微应用建议使用 history 模式的路由,需要设置路由 base,值和它的 activeRule 是一样的
const router = new VueRouter({
mode: 'history',
// 独立运行时使用后面的那个,单独运行时使用第一个
// base 要和 主应用中 registerMicroApps中的 activeRule 相同
base: window.__POWERED_BY_QIANKUN__ ? '/vueApp/' : '/child/vue-app/',
routes
});
export default router;
5,修改 webpack 打包,允许开发环境跨域和 umd 打包。新增 vue.config.js
const { name } = require('./package');
module.exports = {
// 打包到指定的文件夹,部署的时候方便分辨当前是哪个应用
outputDir: 'sub-vue',
// 设置独立运行时的路由base
publicPath: '/child/vue-app/',
devServer: {
port: 9099,
headers: {
'Access-Control-Allow-Origin': '*'
}
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd', // 把微应用打包成 umd 库格式
jsonpFunction: `webpackJsonp_${name}`
}
}
};
最后一步,我们在修改主应用 registerMicroApps
配置中的 entry
然后再加一个路由跳转;
<!-- 主应用app.vue -->
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link> |
<router-link to="/appVue">goToSubVue</router-link>
</div>
<router-view/>
<div id="container"></div>
</div>
</template>
// 主应用 main.js
registerMicroApps([
{
name: 'vueApp',
entry: '//localhost:9099/child/vue-app/',
container: '#container',
activeRule: '/vueApp'
}
]);
预览
然后启动项目预览效果;记得先单独启动子应用进行测试不要在阴沟里翻了船!
生命周期
主应用生命周期
提到主应用的声明周期必然离不开 regsterMicroApps
这个方法;
registerMicroApps(apps, lifeCycles)
;
registerMicroApps(
[
{
name: '',
entry: '',
container: '',
activeRule: '',
loader(loading) {},
props: {}
}
]
)
name
:子应用的名称,保证各个微应用之前不能重复entry
:子应用的入口,和子应用的启动服务保持一致,比如子应用启动后192.168.10.1:8088/child/app-vue/
那么entry
则可以使//localhost:8088/child/app-vue/
或者/child/app-vue/
。container
:对应子应用的需要载入哪个DOM容器中,父应用中去配置。activeRule
:微应用的激活入口,要与子应用的路由window.__POWERED_BY_QIANKUN__
为 true 是配置一致; 比如子应用/vueApp/
那么主应用的activeRule
为/vueApp
。loader
:可选参数 子应用的 loading 效果。props
:此值可以在子应用的 mount 钩子中拿到,注意:props在子应用中获取到的并不是一个空对象,注意键名冲突
registerMicroApps(
[{}],
{
beforeLoad(app) {},
beforeMount(app) {},
afterMount(app) {},
beforeUnmount(app) {},
afterUnmount: [app => console.log(app)]
}
)
以上声明周期钩子中 app 均为可选参数,获取当前被激活的子应用信息;当加载子应用时触发 beforeLoad
,beforeMount
,afterMount
三个钩子,当离开子应用时触发 beforeUnmount
,afterUnmount
。
子应用声明周期
bootstrap
:只有在子应用初始化的时候才会触发,其他时候不会触发;mount
:接收一个props
即父应用中传递的props
,每次进入子应用都会调用此钩子;ps:如果是 react 或者 vue 应用可以在此钩子中去挂载应用;unmount
:接收一个props
即父应用中传递的props
,应用每次离开都会触发这个钩子;ps:可以做一些清空数据恢复状态的操作;update
:接收一个props
即父应用中传递的props
,仅使用 loadMicroApp 方式加载微应用时生效。此方法是可选的;
注意:子应用的钩子是独立的 Promise
函数,需要单独调用,下面以 Vue 项目中 main.js
为例;
父子应用通信
想要深入了解 qiankun 的通信推荐 qiankun父子通信;
根据 qiankun 官方说的方式;有两种通信方式;
1,基于 initGlobalState
方法通信;使用此种方式我们先对主子应用稍微改装下。使用此种方式,我个人理解与 vue 组件之间通信类似;主应用负责维护数据,子应用只做消费;
initGlobalState
:该方法接收一个 state,返回三个通信方法。
-
onGlobalStateChange(cb, fireImmediately)
:当前应用监听全局状态,有变更触发 callback,fireImmediately = true 立即触发 callback -
setGlobalState(state)
:按一级属性设置全局状态,微应用中只能修改已存在的一级属性 -
offGlobalStateChange
:移除当前应用的状态监听,微应用 umount 时会默认调用
// 主应用:shared/actions.js
import { initGlobalState } from 'qiankun';
const state = {};
const actions = initGlobalState(state);
export default actions;
// 子应用:shared/actions.js
function emptyAction () {
// 提示当前使用的是空 Action
console.warn('Current execute action is empty!');
}
class Actions {
// 默认值空 action
actions = {
onGlobalStateChange: emptyAction,
setGlobalState: emptyAction,
};
/**
* 设置 actions
*/
setActions(actions) {
this.actions = actions;
}
/**
* 映射
*/
onGlobalStateChange(...args) {
return this.actions.onGlobalStateChange(...args);
}
/**
* 映射
*/
setGlobalState(...args) {
return this.actions.setGlobalState(...args);
}
};
const actions = new Actions();
export default actions;
完成以上配置,需要在子应用的 mount
钩子中去设置这个 actions;
import actions from '../shared/actions';
// ......
export async function mount (props) {
console.log('[vue] props from main framework', props);
actions.setActions(props);
render(props);
}
做完上面的操作,我们来写个示例
// 主应用Home.vue
<template>
<div>
<fieldset>
<legend>formData</legend>
username: <input type="text" v-model="formData.username" /><br />
password: <input type="text" v-model="formData.password" /> <br />
<button @click="submit">submit</button>
</fieldset>
</div>
</template>
<script>
import actions from '../../shared/actions';
export default {
data() {
return {
formData: {
username: '',
password: ''
}
}
},
methods: {
submit() {
actions.setGlobalState(this.formData)
}
}
}
</script>
// 子应用 Home.vue
<template>
<div>{{ form }}</div>
</template>
<script>
export default {
data() {
return {
form: {}
};
},
mounted() {
// 由于我们是在父应用的Home中跳转到子应用;
// 实际上是做了 / 跳转到 /vueApp 的操作,但是还要能够拿到 / 中设置的内容;虽然我们设置成功了,但是子应用挂载的时候已经没有change事件了;
// 所以加上第二个参数 true,类似于 watch 中的 immediate 。
actions.onGlobalStateChange((state, prev) => {
this.form = state;
}, true);
}
}
</script>
看到上面还是有点问题的如果我们想要从 /vueApp 中返回 / 时 输入框内仍然存在我们输入的值;这时需要在 主应用的 Home.vue
再次监听一下 change 事件
export default {
// ....
mounted() {
// 这里同样要加上 true
actions.onGlobalStateChange((state, prev) => {
this.formData = state;
}, true)
}
}
在尝试这个通信的时候发现 qiankun 还有一个机制,在一个应用中存在多个 change 事件,只会最后一个生效,防止内存爆炸;
总结: 实际操作下来;感觉不足的地方,当然大概率是我菜。
- 更新 state 的方式;也就是说通过
setGlobalState
但是官网对于这个方法的介绍是更新一级属性;按照我的示例更新,岂不是每次都等同于覆盖了原有的 state?在假设如果我们有 2个以上的子应用,state 应该如何维护?希望评论区也有人解答这个问题; - 假设上面的问题成立;我现在有两个子应用,一个是需要更新 state 中 userInfo 的信息,一个是需要更新 state 中 systemInfo 的信息;(子给父传值) 此时怎么保证能够更新数据,并且不用全量修改整个 state?
2,基于 registerMicroApps
中 app 配置对象中的 props
进行通信;参考上面的通信链接;不写了。不过也可能是比较优解的方案。
部署
以阿里云为例 先安装 nginx 可以通过 yum 安装
yum install nginx
nginx 安装成功后 配置文件在 /etc/nginx/nginx.conf
;
静态资源在/usr/local/html
;
官网提供了两种部署方式;
1,微应用放在一个特殊的文件夹下(不会和微应用重名)这种也是比较推荐的;
└── html/ # 根文件夹
|
├── child/ # 存放所有微应用的文件夹
| ├── vue-app/ # 存放微应用 vue-app 的文件夹 打包时通过 outputDir 指定
├── index.html # 主应用的index.html
├── css/ # 主应用的css文件夹
├── js/ # 主应用的js文件夹
server {
listen 80;
listen [::]:80;
server_name _;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods *;
add_header Access-Control-Allow-Headers *;
# 这里会去dist中去寻找主应用的静态资源
location / {
root /usr/local/html/dist;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# 这里其实访问的是子应用的 publickPath,同时这个path要和 主应用的 registerMicroApps 中配置对象中的 entry 一致
location /child/appVue {
root /user/local/html/dist;
index index.html index.htm;
try_files $uri $uri/ /child/app-vue/index.html;
}
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
}
此时 主应用中 registerMicroApps 的配置可以是下面这样;
但是这里有个不解的地方;如果我本地启动启动所有服务 entry 如果没有 //localhost:9099
就会运行不起来,但是发布到生产环境就可以;
registerMicroApps([
{
name: 'vueApp',
// entry: '//localhost:9099/child/vue-app/',
entry: '/child/vue-app/',
container: '#container',
activeRule: '/vueApp'
}
]);
2,就是直接放到二级目录,不用在 dist 中创建 child 文件夹了,操作基本一致,只需要简单改下 linux 配置即可
└── html/ # 根文件夹
|
├── vue-app/ # 存放微应用 vue-app 的文件夹
├── index.html # 主应用的index.html
├── css/ # 主应用的css文件夹
├── js/ # 主应用的js文件夹
# 这里其实访问的是子应用的 publickPath,同时这个path要和 主应用的 registerMicroApps 中配置对象中的 entry 一致
location /child/appVue {
root /user/local/html/dist;
index index.html index.htm;
# 这里把 /child去掉直接去 app-vue中查找入口文件
try_files $uri $uri/ /app-vue/index.html;
}
3,还有一种情况是子应用和主应用不在一个服务器,这个真心没法测试,兜里没钱😂;不同服务器部署微应用