10分钟弄懂微应用框架——乾坤,真香!
Posted 前端开发博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了10分钟弄懂微应用框架——乾坤,真香!相关的知识,希望对你有一定的参考价值。
前言
今天刚刚学习了一个微前端框架——乾坤,正着热乎劲,写一篇入门博客。这篇文章不会讨论太多的原理和实现,只是一个入门写 Hello World 的教程。
文章的很多地方都参考官网,但是官网的教程太简洁了,个人觉得还是做不到无脑上手,希望这篇文章可以帮到正在入门微前端的伙伴。
想直接看代码的,我写了个比官网更简单的例子,点击即可学会。
什么是微前端
首先,来了解一下微前端是个啥。
当我们写了一个又一个的 SPA 应用。突然有一天,老板说要将这些应用合并,前端工程们就头大了——每个应用的代码都是一座摇摇欲坠的💩山,别说合并了,就算动都不敢动呀。
虽然很麻烦,但是前端工程师还是把这个问题解决了,而这个将多个 SPA 合并成一个 Web App 的解决方案就是微前端。
为什么要微前端
“多个 SPA 合并成一个 Web App?”,可能有人会想到用 <iframe/>
也可以实现一个网页里内嵌多个网页呀。原因有:
不感知 url 状态,比如前进、后退没法玩
UI 不同步、DOM 不同享。
<iframe/>
本质上是页面的硬隔离,所以如果你有个遮罩层,可能只能在那一小片区域才展示遮罩层页面之间的通信很麻烦
每次都要加载子应用,速度很慢
而微前端正好可以补足上面的缺点。
微前端的优势
除了解决了上面的问题,微前端还有如下的优点:
子应用技术栈无关,即类似上页说的页面硬隔离,但是是以 sandbox 的方式实现的
合并多个子应用,相对地,也可以将大应用拆解成多个应用,实现业务解耦
子应用高度自治,发布、报错、测试流程仅限于子应用,不会受别的业务影响,同时也不影响别的业务
乾坤由来
最原始的微前端框架并不是乾坤,而是 single-spa。但是这个框架只提供最基本的功能,而且全是英文,文档写得也很繁琐,应该没人想去看。
阿里的乾坤则是基于 single-spa 开发的又一个微前端框架,提供了更多的功能,也解决一些坑,官网也很简洁。
不过,个人觉得有点太简洁了,写 Hello World 的时候还是遇到一些坑,只能看 Github 的 /examples 目录学习。
主应用 VS 子应用
首先,要知道现在项目并不是只有一个了,而是区分出 主应用 和 子应用,关系如下:
两者区别:
主应用
概念:就是要统治各个子应用的应用,也即合并结果页面
负责子应用的注册、路由分发。可以简单理解为 React.js 和 Vue.js 里的 App 组件,主要做一些初始化、路由注册、全局状态注册、销毁时的动作
子应用
概念:各个 SPA 应用,可以理解为 SPA 里的页面组件
负责暴露一些函数,以此对接主应用,让主应用知道:哦,原来你是子应用,要和我对接。常见的对应函数有:
bootstrap
,mount
,unmount
项目创建
乾坤官网最推荐的做法是将主应用和子应用分成两个项目,各自管理。当然,也可以一个项目里分成不同的目录来存放。
├── main # 主应用
├── baidu # 子应用
└── taobao # 子应用
如果你觉得 官方的例子 太复杂,也可以看我自己建的 qiankun-bigass-app,子应用只有两个用 React.js 的项目。我把很多无关的代码都删了。
实现主应用
理清上面的关系后,我们直接干代码,先看主应用。
首先,我们弄一个 .html 文件出来,作为主页面的 HTML 模板:
<body>
<div class="mainapp">
<!-- 标题栏 -->
<header class="mainapp-header">
<h1>QianKun</h1>
</header>
<div class="mainapp-main">
<!-- 侧边栏 -->
<ul class="mainapp-sidemenu">
<li onclick="push('/taobao')">淘宝</li>
<li onclick="push('/baidu')">百度</li>
</ul>
<!-- 子应用 -->
<main id="subapp-container"></main>
</div>
</div>
<script>
function push(subapp) history.pushState(null, subapp, subapp)
</script>
</body>
然后,使用 Webpack,指定为 template HTML,并配置 dev server,注意一定要配置 headers,不然会有跨域的问题,子应用同理:
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports =
entry: './index.js',
devtool: 'source-map',
devServer:
open: true,
port: '7099',
clientLogLevel: 'warning',
disableHostCheck: true,
compress: true,
headers:
'Access-Control-Allow-Origin': '*',
,
historyApiFallback: true,
overlay: warnings: false, errors: true ,
,
output:
publicPath: '/',
,
mode: 'development',
resolve:
extensions: ['.js', '.jsx', '.ts', '.tsx'],
,
module:
rules: [
test: /\\.jsx?$/,
exclude: /node_modules/,
use:
loader: 'babel-loader',
options:
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-transform-react-jsx'],
,
,
,
test: /\\.(le|c)ss$/,
use: ['style-loader', 'css-loader', 'less-loader'],
,
],
,
plugins: [
new HtmlWebpackPlugin(
filename: 'index.html',
template: process.env.MODE === 'multiple' ? './multiple.html' : './index.html',
minify:
removeComments: true,
collapseWhitespace: true,
,
),
],
;
入口文件 index.js 就比较重要了,需要完成主应用的很多事情:
import registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start, initGlobalState from 'qiankun';
import './index.less';
/**
* 主应用 **可以使用任意技术栈**
* 以下分别是 React 和 Vue 的示例,可切换尝试
*/
import render from './Render';
// import render from './render/VueRender';
/**
* Step1 初始化应用(可选)
*/
render( loading: true );
const loader = loading => render( loading );
/**
* Step2 注册子应用
*/
registerMicroApps(
[
name: 'taobao',
entry: '//localhost:7101',
container: '#subapp-viewport',
loader,
activeRule: '/taobao',
,
name: 'baidu',
entry: '//localhost:7102',
container: '#subapp-viewport',
loader,
activeRule: '/baidu',
,
],
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), true);
setGlobalState(
ignore: 'master',
user:
name: 'master',
,
);
/**
* Step3 设置默认进入的子应用
*/
setDefaultMountApp('/taobao');
/**
* Step4 启动应用
*/
start();
runAfterFirstMounted(() =>
console.log('[MainApp] first app mounted');
);
上面主要完成:初始化、注册子应用、设置配置全局状态、设置默认进入子应用、启动应用。
至于初始渲染函数,可以这么写:
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);
实现子应用
子应用其实和官网的差不多,这里以 React.js 子应用举例。首先用 create-react-app
来创建子应用:
create-react-app baidu
在 src
目录下新增 public-path.js
:
if (window.__POWERED_BY_QIANKUN__)
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
设置 history 模式路由的 base:
const RouteExample = () =>
return (
<Router basename=window.__POWERED_BY_QIANKUN__ ? '/baidu' : '/'>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
<Suspense fallback=null>
<Switch>
<Route path="/" exact component=Home />
<Route path="/about" component=About />
</Switch>
</Suspense>
</Router>
);
;
export default function App()
return (
<div className="app-main">
<h1>淘宝Taobao</h1>
<hr/>
<RouteExample />
</div>
);
__POWERED_BY_QIANKUN__
用于判断现在是否作为子应用被访问,其它地方与普通 React.js App 没差别。
去掉一些无用的文件后,在入口配置子应用:
function render(props)
const container = props;
ReactDOM.render(<App />, container ? container.querySelector('#root') : document.querySelector('#root'));
if (!window.__POWERED_BY_QIANKUN__)
render();
// 监听全局状态
function storeTest(props)
props.onGlobalStateChange((value, prev) => console.log('淘宝', `[onGlobalStateChange - $props.name]:`, value, prev), true);
props.setGlobalState(
ignore: props.name,
user:
name: props.name,
,
);
export async function bootstrap()
console.log('[淘宝] react app bootstraped');
export async function mount(props)
console.log('[淘宝] props from main framework', props);
storeTest(props);
render(props);
export async function unmount(props)
const container = props;
ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'));
注意上面的 bootstrap
, mount
和 unmount
一定要 export 出去,不然没人知道这个是子应用。
下一步,是修改 Webpack 的配置。但是 creat-react-app 造出来的 React App 不 eject 出来就改不了,这里官网推荐使用 @rescripts/cli
来修改:
yarn add -D @rescript/cli
在根目录添加 .rescriptsrc.js
,并加上:
const name = require('./package');
module.exports =
webpack: config =>
config.output.library = `$name-[name]`;
config.output.libraryTarget = 'umd';
config.output.jsonpFunction = `webpackJsonp_$name`;
config.output.globalObject = 'window';
return config;
,
devServer: _ =>
const config = _;
config.headers =
'Access-Control-Allow-Origin': '*',
;
config.historyApiFallback = true;
config.hot = false;
config.watchContentBase = false;
config.liveReload = false;
return config;
,
;
Webpack 配置同样很重要,一个是配置 historyApiFallback
处理单页的 404 问题,另一个是通过 Access-Control-Allow-Origin
解决主应用访问子应用的跨域问题。
在上面的主应用里看到我们是要访问不同的端口的,那端口要怎么配置呢?可以通过 .env
来配置:
SKIP_PREFLIGHT_CHECK=true
BROWSER=none
PORT=7101
WDS_SOCKET_PORT=7102
更多框架的配置可见这里。
API 粗讲
乾坤的 API 也不是很多,详见这里。简单讲一下用处:
API | 用处 | 类比 |
---|---|---|
registerMicroApps | 主应用用来注册多个子应用的函数 | 类似于 Vue 和 React 的路由 |
start | 启动主应用 | 类似于 React.js 的 render 函数和 Vue.js 的 new Vue() |
loadMicroApp | 手动加载子应用 | 也类似于 React.js 的 render 函数和 Vue.js 的 new Vue() ,只不过更自由了 |
prefetchApps | 预加载子应用 | 类似于 Webpack 的 prefetch 功能 |
addGlobalUncaughtErrorHandler | 页面报错时可以用于上报和兜底 | - |
removeGlobalUncaughtErrorHandler | 都懂的 | - |
initGlobalState | 初始化全局状态 | 类似于 Redux 的 createStore 和 Vue 的 new Vue.Store() |
最后
关注前端开发博客,在后台回复以下关键字可以获取资源。
回复「小抄」,领取Vue、javascript 和 WebComponent 小抄 PDF
回复「Vue脑图」获取 Vue 相关脑图
回复「思维图」获取 JavaScript 相关思维图
回复「简历」获取简历制作建议
回复「简历模板」获取精选的简历模板
回复「加群」进入500人前端精英群
回复「电子书」下载我整理的大量前端资源,含面试、Vue实战项目、CSS和JavaScript电子书等。
回复「知识点」下载高清JavaScript知识点图谱
点赞和在看就是最大的支持❤️
以上是关于10分钟弄懂微应用框架——乾坤,真香!的主要内容,如果未能解决你的问题,请参考以下文章