在做中后台的前端时,经常会遇到这样的痛点:
(1)中后台的系统很多,功能上比较独立,但是运营人员在使用的时候还是希望统一入口。这样多个项目就不得不在一个仓库中来维护,久而久之,项目代码就会变得越来越庞大,难以管理。
(2)涉及到基础组建的升级,由于多个项目可能用到的都是同一个基础组件,所以不得不所有项目都对组件的升级做适配后,新组件才能被使用,不太灵活。
(3)有时我们想把不同技术栈的项目整合到一个前端入口页面中。
所以,我们可以通过微前端的思想来解决,微前端可以把每个系统拆成独立的服务,有自己独立的仓库,最后通过一个基座项目来在各个独立的子项目之间切换,并且可以给用户类似于一个单页面应用的顺滑体验。
效果如下:
下面我们就通过从0到1的搭建和部署一个极简微前端架构的过程,手把手教大家如何使用微前端。本文将会分两个流程来讲解:1.开发流程。2.部署流程。每个流程分三个步骤来介绍:1.基座项目。2.react子系统。3.vue子系统。
开发流程
基座项目
主应用(基座)不限技术栈,只需要提供一个容器 DOM,然后注册微应用并 start 即可。先安装 qiankun :
$ yarn add qiankun # 或者 npm i qiankun -S
复制代码
然后写打包入口文件index.js:
文件开头我们需要引入qiankun的一些库函数,引入主应用的样式文件和render函数。
// index.js
import { registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start, initGlobalState } from 'qiankun';
import './index.less';
/**
* 主应用 **可以使用任意技术栈**
* 以下分别是 React 和 Vue 的示例,可切换尝试
*/
import render from './render/ReactRender';
// import render from './render/VueRender';
复制代码
插一小段来介绍一下render函数,这里render函数可以使用react也可以使用vue,两种写法如下:
// render/ReactRender.jsx
import React from 'react';
import ReactDOM from 'react-dom';
/**
* 渲染子应用
*/
function Render(props) {
const { loading } = props;
return (
<>
{loading && <h4 className="subapp-loading">Loading...</h4>}
<div id="subapp-viewport" />
</>
);
}
export default function render({ loading }) {
const container = document.getElementById('subapp-container');
ReactDOM.render(<Render loading={loading} />, container);
}
复制代码
// render/VueRender.js
import Vue from 'vue/dist/vue.esm';
function vueRender({ loading }) {
return new Vue({
template: `
<div id="subapp-container">
<h4 v-if="loading" class="subapp-loading">Loading...</h4>
<div id="subapp-viewport"></div>
</div>
`,
el: '#subapp-container',
data() {
return {
loading,
};
},
});
}
let app = null;
export default function render({ loading }) {
if (!app) {
app = vueRender({ loading });
} else {
app.loading = loading;
}
}
复制代码
这里渲染函数的功能就是,定义出子系统挂载的元素:id=’subapp-viewport’。并且在子系统真正挂载到目标元素之前,渲染loading状态。这里两个应用所挂载到的元素为id=’subapp-container’,是在html模版中定义的:
//index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>全栈编程</title>
</head>
<body>
<div class="mainapp">
<!-- 标题栏 -->
<header class="mainapp-header">
<h1>全栈编程</h1>
</header>
<div class="mainapp-main">
<!-- 侧边栏 -->
<ul class="mainapp-sidemenu">
<li onclick="push('/reactapp')">reactapp</li>
<li onclick="push('/vue')">Vue</li>
</ul>
<!-- 子应用 -->
<main id="subapp-container"></main>
</div>
</div>
<script>
function push(subapp) { history.pushState(null, subapp, subapp) }
</script>
</body>
</html>
复制代码
两个render函数中的app会挂载到“子应用”元素之中。样式如下:
// index.less
// 主应用慎用 reset 样式
body {
margin: 0;
}
.mainapp {
// 防止被子应用的样式覆盖
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
line-height: 1;
}
.mainapp-header {
>h1 {
color: #333;
font-size: 36px;
font-weight: 700;
margin: 0;
padding: 36px;
}
}
.mainapp-main {
display: flex;
.mainapp-sidemenu {
width: 130px;
list-style: none;
margin: 0;
margin-left: 40px;
padding: 0;
border-right: 2px solid #aaa;
>li {
color: #aaa;
margin: 20px 0;
font-size: 18px;
font-weight: 400;
cursor: pointer;
&:hover {
color: #444;
}
&:first-child {
margin-top: 5px;
}
}
}
}
// 子应用区域
#subapp-container {
flex-grow: 1;
position: relative;
margin: 0 40px;
.subapp-loading {
color: #444;
font-size: 28px;
font-weight: 600;
text-align: center;
}
}
复制代码
由于我们在主应用中使用的是react方式,所以还需要install相关的包:
npm install react react-dom -S
复制代码
整个package.json文件如下,可以参考:
//package.json
{
"name": "main",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"devDependencies": {
"@babel/core": "^7.7.2",
"@babel/plugin-transform-react-jsx": "^7.7.0",
"@babel/preset-env": "^7.7.1",
"babel-loader": "^8.0.6",
"css-loader": "^3.2.0",
"html-webpack-plugin": "^3.2.0",
"less-loader": "^6.2.0",
"style-loader": "^1.0.0",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.9.0",
"cross-env": "^7.0.2"
},
"dependencies": {
"qiankun": "^2.4.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"vue": "^2.6.11"
}
}
复制代码
下面我们再回来说最重要的入口文件index.js,引入必要的包和函数后,第一步需要先加载loading状态和定义loader函数:
//index.js
/**
* Step1 初始化应用(可选)
*/
render({ loading: true });
const loader = loading => render({ loading });
复制代码
第二步通过registerMicroApps注册子应用,其中参数有子系统名称、入口,由于我们在开发流程中是分多个端口起的服务,所以开发阶段分别定为3000端口和9000端口。container即为上面render函数定义的渲染子系统的id。loader即为所定义的render函数,有一个参数为是否正在加载。activeRule即为激活子系统的路由。
//index.js
/**
* Step2 注册子应用
*/
registerMicroApps(
[
{
name: 'reactapp',
// entry: '/child/reactapp/',
entry: 'http://localhost:3000',
container: '#subapp-viewport',
loader,
activeRule: '/reactapp',
},
{
name: 'vue',
// entry: '/child/vue/',
entry: 'http://localhost:9000',
container: '#subapp-viewport',
loader,
activeRule: '/vue',
}
],
{
beforeLoad: [
app => {
console.log('[LifeCycle] before load %c%s', 'color: green;', app.name);
},
],
beforeMount: [
app => {
console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
},
],
afterUnmount: [
app => {
console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
},
],
},
);
const { onGlobalStateChange, setGlobalState } = initGlobalState({
user: 'qiankun',
});
onGlobalStateChange((value, prev) => console.log('[onGlobalStateChange - master]:', value, prev));
setGlobalState({
ignore: 'master',
user: {
name: 'master',
},
});
复制代码
第三步为设置默认激活的子系统:
//index.js
/**
* Step3 设置默认进入的子应用
*/
setDefaultMountApp('reactapp');
复制代码
第四步调用start(),并且设置runAfterFirstMounted钩子:
//index.js
/**
* Step4 启动应用
*/
start();
runAfterFirstMounted(() => {
console.log('[MainApp] first app mounted');
});
复制代码
下一篇,我们继续讲解如何配置react子系统。