微前端是将微服务的技术理念营运至前端后的技术实践,将前端应用拆分成一些更小、更简单的模块,且这些模块能够由多个团队独立开发、部署、测试
比如我们有一个新项目,要用到项目和项目 B 的模块,除了 copy,目前还有哪些方法,你一定会说 npm 仓库,确实是一种办法,但是要把业务组件封装成 npm,领导可能会 C 掉你,虽然 React 提倡一切皆组件,一切皆函数,但是对于组件复用,多个项目仍存在很大的局限性
就拿我们 node 来举例,比如 express,我们可以使用模板语法,include 其他 html 到主模板
例如如下的 package.json 文件
{
"name": "duanx.com",
"version": "1.0.0",
"description": "duanxl blob",
"dependencies": {
"@duanxl/menu-list": "^1.0.0",
"@duanxl/container": "^1.0.0",
"@duanxl/nav-bar": "^1.0.0"
}
}
比如在 angular 新版本中,createCustomElement 就可以实现 Web Component 组件的形式
基于上面的一些可改造方式,我们来看微前端需要解决的一些问题
问题 | 描述 |
---|---|
独立开发 | 独立开发,而不受影响 |
独立部署 | 能作为一个服务来单独部署 |
支持不同框架 | 可以同时使用不同的框架,如 Angular、Vue、React |
优化 | 能消除未使用的代码 |
环境隔离 | 应用间的上下文不受干扰 |
多个应用同时运行 | 不同应用可以同时运行 |
共用依赖 | 不同应用是否共用底层依赖库 |
依赖冲突 | 依赖的不同版本是否导致冲突 |
集成编译 | 应用最后被编译成一个整体,而不是分开构建 |
微应用的注册、异步加载、生命周期 | 应该有统一的生命周期,去解决不同技术栈的差异 |
消息通信 | 子应用之间的消息通信 |
微应用独立开发 | 各个子应用之间,应具有个子的开发调试能力 |
发布流程 | 各个应用独立发布,不存在相互依赖 |
渐进式 | 如何平滑的升级老项目 |
下面借鉴大佬成功案例分享 ,分别对应项目运行期、部署期、构建期以及开发期的行动方案
父项目:映射多个后端内容,统一授权登录、控制全局菜单等
子项目:项目菜单、权限、角色隔离
父项目:重新生成引用文件、更新全局入口文件、重启服务
子项目:替换静态资源
父项目:生成全局路由、生成全局入口引用
子项目:替换公共依赖、css 添加作用域、生成静态资源
父项目:路由控制、模块加载、JS 公用库替换
子项目:输出数据流、输出路由、输出功能
提到了 SystemJs,垃圾的我,还是看了两眼,SystemJs 会根据 import 保存文件路径,使用 fetch + eval 加载 JS 文件并执行,最后将函数导出,而 import 执行的 System 文件,register 会添加到 System 队列
SystemJs 是针对浏览器端的,会直接返回加载文件的内容,具体操作是会动态创建 script,监听onload事件,移除 script 标签,将 register 回调传回去,这时候 import 的 Promise 会被 resolve 掉,在业务中,就会触发我们的 then 方法
2.接下来,就是创建子应用,子应用就是一个 vue 项目,和上面的想法一样,我们要编译 vue,并打包成 SystemJs register Api 的 js
// webpack.config.js
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const WebpackSystemRegister = require('webpack-system-register');
module.exports = {
entry: {
// buylist: './src/BuyList.vue',
buy: './src/buy.vue',
},
module: {
rules: [
{
test: /\.css$/i,
use: ['vue-style-loader', 'css-loader'],
},
{
test: /\.vue$/,
loader: 'vue-loader',
},
],
},
plugins: [new VueLoaderPlugin(), new WebpackSystemRegister({})],
};
接着就是简单子项目的业务组件
接着,我们编译 buy.vue,在 dist 目录下启动服务,注意,需要解决跨域问题,这里我使用的 http-server,并且启动主站服务
http-server -p 9000 --cors
接下来,我们看组件间的通讯如何处理
/**
* listener
* @notice this
* @notice Bom/Dom
*/
(() => {
const EXECTIME = 50 // debounce exec
const DELAY = 25
// source
let that = {}
let slice = [].slice
let channelList = {}
const on = (channel, type, callback, context) => {
let currentChannel = channelList[channel]
if (!currentChannel) {
currentChannel = channelList[channel] = Object.create(null)
}
currentChannel[type] = currentChannel[type] || []
currentChannel[type].push({
func: callback,
context: context || that
})
}
const once = (channel, type, callback, context) => {
const _once = () => {
that.off(channel, type, _once)
return callback.apply(context || that, arguments)
}
on(channel, type, _once, context)
}
const tigger = (channel, type) => {
if (channelList[channel] && channelList[channel][type] && channelList[channel][type].length) {
const taskList = channelList[channel][type]
let currentHanders = []
for (let i = taskList.length; i--;) {
currentHanders.push({
hander: taskList[i],
args: slice.call(arguments, 1)
})
}
(() => {
const startDate = +Date.now()
do {
const currentTask = currentHanders.unshift()
const hander = currentTask.hander
try {
hander.func.apply(hander.context, currentTask.args)
} catch (e) {
console.log(e)
}
} while (currentHanders.length && (Date.now() - startDate < EXECTIME))
if (currentHanders.length > 0) {
setTimeout(arguments.callee, DELAY);
}
})()
}
}
const off = (channel, type, callback, context) => {
context = context || that
if (channelList[channel] && channelList[channel][type] && channelList[channel][type].length) {
const taskList = channelList[channel][type]
let hander
for (var i = taskList.length; i--;) {
hander = taskList[i]
if (hander.func === callback && hander.context === context) {
taskList.splice(i, 1)
}
}
}
}
that.on = on
that.once = once
that.tigger = tigger
that.off = off
globalThis.listener = globalThis.listener || that
})()
思考,如果我们在子应用中的生命周期,使用上面的消息机制,是否也会实现不同技术栈的统一生命周期
微前端也不是解决一切的利器,是否需要用微前端,还有诸多因素,排去推动、团队之间等问题,就在技术而言,也有着一些不足
微前端的由来,与微服务密不可分,厉害的大佬都在搞 ServerLess,由于自己太菜,所以也在学习这方面的知识
在前几年,ECS 是大部分企业必备利器,而在应用层面,先后不断的促进热启动等技术,但是随着行业发展,Docker、ServerLess 的火热,也出现了很多让开发者更加便捷的手段
ECS 虽然功能强大,但是成本较高,在硬件、性价比等基础上,FAAS、BAAS、PAAS、DAAS 等技术也应用而生
就拿 AWS 的 FAAS 来说,在应用层面上,不断开拓冷启动、全球节点、次数计费,成为了成本很低、而又方便的解决方案,免费版一个月 100W 次请求,对于一些小项目而言,无论在解决服务压力、负载方面,都有了诸多便利
我们以 cloundflare,展示下简单的用法
import { handleRequest } from './handler'
addEventListener('fetch', (event) => {
event.respondWith(handleRequest(event.request))
})
// handler
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS',
'Access-Control-Max-Age': '86400',
'Access-Control-Allow-Headers': '*',
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
}
export async function handleRequest(request: Request): Promise<Response> {
const value = await MY_KV.get('name')
return new Response(JSON.stringify(value), {
headers: corsHeaders,
status: 200,
})
}
import React, { useEffect, useState } from 'react';
import logo from './logo.svg';
import './App.css';
function App() {
const [name, setName]=useState('')
const init = async () => {
// const result = await fetch('https://duanxl.18210219235.workers.dev/')
const result = await fetch('http://127.0.0.1:8787')
setName(await result.json())
}
useEffect(() => {
init()
}, [])
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code> 测试数据:</code> {name}
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
一个简单的 ServerLess 实战已完成,虽然遍历许多,但是也有着一些缺点,比如存在冷启动时间等问题,当然,这就伴随而来需要做 PWA、缓存等优化,最简单的,可以集成 Workbox,提升用户体验,至此,一个最简单的 ServerLess 小实战就完成了,如果想白嫖建站的,可以试一下
接着在初识微前端(下篇),会实践一些 webpack5 的联邦模块,现有流行的微前端库是如何处理这些问题的,还有就是 webComponent 这块,主要围绕 ServerLess
预计更新时间:2022.06