1.基本概念

实现一套微前端架构,可以把其分成四部分(参考:https://alili.tech/archive/11052bf4/)

加载器:也就是微前端架构的核心,主要用来调度子应用,决定何时展示哪个子应用, 可以把它理解成电源。

包装器:有了加载器,可以把现有的应用包装,使得加载器可以使用它们,它相当于电源适配器。

主应用:一般是包含所有子应用公共部分的项目—— 它相当于电器底座

子应用:众多展示在主应用内容区的应用—— 它相当于你要使用的电器

所以是这么个概念:电源(加载器)→电源适配器(包装器)→️电器底座(主应用)→️电器(子应用)️

总的来说是这样一个流程:用户访问index.html后,浏览器运行加载器的js文件,加载器去配置文件,然后注册配置文件中配置的各个子应用后,首先加载主应用(菜单等),再通过路由判定,动态远程加载子应用。

2.预备知识

2.1 SystemJs

SystemJS提供通用的模块导入途径,支持传统模块和ES6的模块。

SystemJs有两个版本,6.x版本是在浏览器中使用的,0.21版本的是在浏览器和node环境中使用的,两者的使用方式不同。(参考:https://github.com/systemjs/systemjs)

在微服务中主要充当加载器的角色。

2.2 singleSpa

single-spa是一个在前端应用程序中将多个javascript应用集合在一起的框架。主要充当包装器的角色。(参考:https://single-spa.js.org/docs/getting-started-overview.html

3.微服务实践

3.1 创建应用

首先创建一个主应用iframe,这个主应用只需要简单的起一个服务访问静态资源即可。

用npm init初始化,创建一个index.html,简单写个hello world,安装依赖 npm i serve --s。

在package.json中的scripts中增加启动命令"serve": "serve -s -l 7000"。运行后可以看到hello world。

然后在创建3个子应用,我用的是vue-cli2.0,分别创建navbar应用(用来写路由),program1(应用1),program2(应用2)。

navbar应用中只放两个链接就好了,如图:

  

子应用program1和program2的路由都相应的加上项目名称前缀,都增加一个about路由来作为子应用的路由切换,大致如下:

3.2 改造子应用

首先包装子应用,各个子应用都需要安装依赖 npm i single-spa-vue systemjs-webpack-interop,修改入口文件main.js如下:

其中 single-spa-vue是针对vue项目的包装器,systemjs-webpack-interop是社区维护的npm库,它可以帮助您使webpack和systemjs一起正常工作。

除此之外还需要在webpack配置中的output中增加设置

3.3 修改主应用文件

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<meta name="importmap-type" content="systemjs-importmap">
<script type="systemjs-importmap">
{
"imports": {
"navbar": "http://localhost:8080/app.js",
"program1": "http://localhost:8081/app.js",
"program2": "http://localhost:8082/app.js",
"single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js"
}
}
</script>
<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js" as="script" crossorigin="anonymous" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/system.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/amd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/named-exports.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/named-register.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/use-default.min.js"></script>
</head>
<body>
<script>
(function(){
System.import('single-spa')
.then((res)=>{
var singleSpa=res; singleSpa.registerApplication('navbar',()=>System.import('navbar'),location=>true); singleSpa.registerApplication('program1',()=>System.import('program1'),(location)=>{
return location.hash.startsWith(`#/program1`);
}); singleSpa.registerApplication('program2',()=>System.import('program2'),(location)=>{
return location.hash.startsWith(`#/program2`);
}); singleSpa.start();
})
})()
</script>
</body>
</html>
registerApplication函数包含四个参数,
appName: 注册的应用名称;
applicationOrLoadingFn:应用入口文件(必须是一个函数,返回一个函数或者一个promise);
activityFn:控制应用是否激活的函数(必须是一个纯函数,接受window.location作为参数,返回一个boolean);
customProps:在包装器生命周期函数中传递给子应用的props(应该是一个对象,可选)。
 
补充:有些小伙伴说运行demo会报错,这里说明下,因为后面把公共依赖抽离出去了,所以这里可能需要加上公共依赖。

 
 
start函数必须在子应用加载完后才能调用。在调用之前,子应用已经加载了只是未被渲染。
 
3.4 运行项目
分别运行navbar,program1,progarm2项目,然后运行iframe项目。iframe项目运行在7000端口,其他子应用分别运行在8080,8081,8082端口。从7000端口去请求其他端口的入口文件会跨域,所以在子应用中增加跨域设置。
在子应用的webpack.dev.config.js中找到devSever配置项,增加headers:{"Access-Control-Allow-Origin":"*"}配置
然后重新运行项目即可。
 

 4.项目整合

以上只是在开发环境中使用,接下来尝试不分别启动服务,只启用一个服务来跑项目。大体思路是使用express搭建一个服务,将子应用全部打包到项目上作为静态资源访问,入口html使用ejs模板来实现项目配置,而不再写死。

4.1 使用express生成器生成项目

4.2 修改子应用打包配置

这样子应用就全部打包到express应用中作为静态资源使用了。

4.3 增加应用配置文件

将iframe的index.html的内容复制到express的入口ejs中。增加配置文件apps.config.json和apps.config.js。

apps.config.js作用就是根据apps.config.json在静态资源文件夹下生成一份新的配置文件,将配置文件中的资源名称通过正则匹配成完整的资源路径。并且监听文件变化来更新静态资源文件夹下的配置文件。

生成的配置文件

4.4根据这个生成的配置文件去修改ejs,将项目注册过程循环出来,而不再是写死的。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<meta name="importmap-type" content="systemjs-importmap">
<script type="systemjs-importmap">
{
"imports": {
<%for(var i=0;i<apps.length;i++){%>
"<%= apps[i].name %>":"<%= apps[i].server %><%=apps[i].resourceEntryUrl %>",
<%}%>
"single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js"
}
}
</script>
<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js" as="script" crossorigin="anonymous" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/system.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/amd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/named-exports.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/named-register.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/use-default.min.js"></script>
</head>
<body>
<script>
(function(){
Promise.all([
System.import('single-spa'),
System.import('./apps.config.json')
])
.then((res)=>{
var singleSpa=res[0];
var configs=res[1].default; configs.apps.forEach( project => {
if(project.resource.length>0){
Promise.all(project.resource.map(i=>{
return System.import(project.server+i)
})).then(function(){
singleSpa.registerApplication(project.name,()=>System.import(project.name),(location)=>{
return project.base?true:location.hash.startsWith(`#/${project.name}`);
});
})
}else{
singleSpa.registerApplication(project.name,()=>System.import(project.name),(location)=>{
return project.base?true:location.hash.startsWith(`#/${project.name}`);
});
}
}); singleSpa.start();
})
})()
</script>
</body>
</html>

实际渲染出来是

4.5 优化打包配置

因为三个子项目都用到了相同的一部分依赖,可以考虑将公用的依赖不打包进去,改为在主项目主引入来提高打包效率

修改子应用的webpack.base.config.js,增加配置项

在主应用中引入依赖

4.5 增加react应用

同样是使用webpack打包,不同是包装器不一样。其他基本上是一样的思路即可。

最终demo

很多技术细节写的很马虎,就不拿出来献丑啦~

补充说明:关于应用间通信或者状态共享的问题,贴一下官方的说明:

通常,我们建议尝试避免这种情况-将这些应用程序耦合在一起。如果您发现自己经常在应用程序之间执行此操作,则可能要考虑那些单独的应用程序实际上应该只是一个应用程序。

通常,最好仅针对每个应用程序需要的数据发出API请求,即使其他应用程序已经请求了其中的一部分。实际上,如果您正确地设计了应用程序边界,最终将只有很少的真正共享的应用程序状态-例如,您的朋友列表与社交订阅源具有不同的数据要求。

但是,这并不意味着它无法完成。这里有几种方法:

1.创建一个共享的API请求库,该库可以缓存请求及其响应。如果somone命中某个API,然后另一个应用程序再次命中该API,则它仅使用缓存

2。公开共享状态作为导出,其他库可以导入它。可观察对象(如RxJS)在这里很有用,因为它们可以向订阅者流式传输新值。

3.使用自定义浏览器事件进行通信1.使用Cookie,本地/会话存储或其他类似方法来存储和读取该状态。这些方法最适用于不经常更改的事物,例如登录的用户信息。

已经上传到github,https://github.com/sc1992sc/singleSpa

前端微服务初试(singleSpa)的更多相关文章

  1. 前端微服务-面向web平台级应用的设计

    从去年开始,前端领域就出现了一个‘微应用’的名词,说的是前端架构的一种设计思路,业内都把它和后端的微服务进行类比,当时忙于公司的项目.没有静下心来好好了解,现在项目结束,再加上最近看的几篇关于前端微服 ...

  2. Mosaic 前端微服务框架

    Mosaic 是一系列的服务.库,集成在一起,定义了组件如何彼此交互,可以用来支持大规模的web 站点开发 一张架构图 说明 尽管上图中的一些组件已经迭代演化了(skipper 的route 配置,上 ...

  3. ASP.NET Core微服务+Tabler前端框架搭建个人博客1--开始前想说的话

    写在前面 本人为在读研究生,特别喜欢.NET,觉得.NET的编程方式.语法都特别友好,学习.NET Core已经差不多有一年半了,从一开始不知道如何入门到现在终于可以编写一些小的应用程序,想一想还是非 ...

  4. 最近整理出了有关大数据,微服务,分布式,Java,Python,Web前端,产品运营,交互等1.7G的学习资料,有视频教程,源码,课件,工具,面试题等等。这里将珍藏多年的资源免费分享给各位小伙伴们

    大数据,微服务,分布式,Java,Python,Web前端,产品运营,交互 领取方式在篇尾!!! 基础篇.互联网架构,高级程序员必备视频,Linux系统.JVM.大型分布式电商项目实战视频...... ...

  5. 阶段5 3.微服务项目【学成在线】_day02 CMS前端开发_16-CMS前端工程创建-导入系统管理前端工程

    提供了基于脚手架封装好的前端工程 H:\BaiDu\黑马传智JavaEE57期 2019最新基础+就业+在职加薪\阶段5 3.微服务项目[学成在线]·\day02 CMS前端开发\资料\xc-ui-p ...

  6. 微服务项目开发学成在线_day02 CMS前端开发

    1 Vue.js与Webpack研究 开发版的浏览器:https://www.google.cn/intl/zh-CN/chrome/dev/ 前端的开发框架:微服务项目开发学成在线_Vue.js与W ...

  7. ASP.NET Core微服务+Tabler前端框架搭建个人博客2--系统架构

    功能分析 在整个微服务架构的搭建过程中,我们需要做的第一步就是对服务进行拆分,将一个完整的系统模块化,通过对各个模块互联,共同完成一个系统的工作.既然要做到模块化,那么必须明白你的系统的需求到底是什么 ...

  8. Istio微服务架构初试

    感谢 http://blog.csdn.net/qq_34463875/article/details/77866072 看了一些文档,有些半懂不懂,所以还是需要helloworld一下.因为isti ...

  9. 前端云原生,以 Kubernetes 为基础设施的高可用 SSR(Vue.js) 渲染微服务初探(开源 Demo)

    背景 笔者在逛掘金的时候,有幸看到掘友狼族小狈开源的 genesis - 一个可以支持 SSR 和 CSR 渲染的微服务解决方案.总体来说思想不错,但是基于 Kubernetes 云原生部署方面一直没 ...

随机推荐

  1. mybatis日志,打印sql语句,输出sql

    mybatis日志,打印sql语句,输出sql<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE ...

  2. Big Data Hubris:"大数据傲慢"问题

    大数据的傲慢与偏见— 读后心得 数据模型研究者必看的书 原文链接: https://medium.com/@iven00000000/%E5%A4%A7%E6%95%B8%E6%93%9A%E7%9A ...

  3. Python 数学运算的函数

    不需要导入模块(内置函数) 函数 返回值 ( 描述 ) abs(x) 返回绝对值 max(x1, x2,...) 最大值,参数可以为序列. min(x1, x2,...) 最小值,参数可以为序列. p ...

  4. MyBatis的一级缓存、二级缓存演示以及讲解,序列化异常的处理

    MyBatis的缓存机制 缓存就是内存中的一个空间,通常用来提高查询效率 MyBatis支持两种缓存技术:一级缓存和二级缓存,其中一级缓存默认开启,二级缓存默认关闭 一级缓存 (1)一级缓存默认开启 ...

  5. Linux chage命令详解

    原文 chage命令用于密码实效管理,该是用来修改帐号和密码的有效期限,接下来通过本文给大家介绍Linux chage命令相关知识,本文介绍的非常详细,具有参考借鉴价值,感兴趣的朋友一起学习吧 lin ...

  6. 机器学习笔记6:K-Means

    目录 目标函数 目标函数的表现函数 针对u和r求解: 最优解的表达式的意义: K-means聚类的形象化展示 聚类前 第一轮循环 第二轮循环 第三轮循环 最终结果 演示代码: 关于K-means的几个 ...

  7. C++使用通配符查找文件(FindFirstFile)

    调用 FindFirstFile 和 FindNextFile 可搜索某个目录下的相应文件. BOOL SearchFilesByWildcard(WCHAR *wildcardPath) { HAN ...

  8. Linux Mysql创建新用户并允许远程连接

    第一步 登陆mysql: mysql-u 数据库用户名 -h 数据库IP -p 根据提示 输入数据库密码 第二步: GRANT ALL PRIVILEGES ON *.* TO '自定义用户名'@'% ...

  9. js定时器的应用

    定时器分为两种 一种是一次性的,时间到就执行 var timer=setTimeout(fun,毫秒数); 清除的方法 clearTimeout(timer) 第二种是周期性的,根据设定的时间周期进行 ...

  10. CodeForces - 1051E :Vasya and Big Integers(Z算法 & DP )

    题意:给定字符串S,A,B.现在让你对S进行切割,使得每个切割出来的部分在[A,B]范围内,问方案数. 思路:有方程,dp[i]=Σ dp[j]   (S[j+1,i]在合法范围内).    假设M和 ...