玩过玻璃球么?也就是弹珠,里面带花纹的那种。
每到过年,小编总能收拾出一些自己曾经爱不释手的玩具,而花花绿绿的弹珠正是其中之一。在我的记忆中,小时候对「弹溜溜」的沉迷程度不亚于现在玩《王者荣耀》开黑,在地上画个圈就能和小伙伴们战上一个下午。
不过在玩耍的同时,你是否和小编思考过同样的问题:弹珠里面的花纹到底是怎么放进去的?
日本科普节目THE MAKING为我们很好的还原了制造流程。首先玻璃弹珠的材料通常来自废旧玻璃瓶,这类瓶子会根据颜色分类,然后碾成细碎状作为材料使用:
在这一过程中,流水线机器会去除掉玻璃上的纸制标签等物品,包括利用磁石去除各种金属材料残渣:
经过加工过滤后,废品玻璃就变成了纯色玻璃残渣,它们会作为原材料被运往玻璃弹珠制作的流水线。
让这些材料变成弹珠的第一步,是软化。弹珠原料也就是玻璃碎渣在进入流水线后会被排放进溶解炉,这里的温度高达1200-1400摄氏度,可以将材料软化的恰到好处,使其既可以流动同时也可以进行切割。
说起来溶解炉的构造很像是饮水机+化粪池的结合体,经过高温软化后,不需要的原料残渣会上浮或者下沉,真正可以使用的材料则会从中段流入到出口,向外排放。
接着需要对材料进行切割。排放出的玻璃材料需要机械帮助将其裁剪成合适、统一的大小:
每一份都会剪切的非常均匀,切割过程看着异常舒适···甚至有点看馋了:
切好后的弹珠材料需要进行成型与冷却。成型很好理解,剪切下来的弹珠材料并不是球形,需要进一步加工处理。常见的机器是下面这台:
这是一台由两个持续旋转的槽筒组成输送机。弹珠会在两个槽筒的夹缝中不断旋转、打磨,与此同时机器会将它们运往下一处流水线:
当然,弹珠在整个打磨与运输的过程中,都在做降温处理:
在弹珠成型后,「赛车时间」就到了。弹珠会在各种弯道上快速飙车,并将自己的体温进一步降低:
最终到达赛道终点,集体飞进了货箱中,透明的弹珠这就造好了。
那么弹珠里面的花纹又是怎么出来的呢?这种弹珠又被称为猫眼弹珠,常因为稀有的花色和外表被小伙伴们视若珍宝,但制作过程其实并没有我们想象的那么复杂。
制作这类弹珠,就要从制作弹珠的第一步开始准备,事实上里面的花纹同样取材自玻璃,只不过是彩色玻璃。
制作的秘诀就在于,两种不同颜色的玻璃在软化时,放置的位置不同。通常来说,在熔炉中彩色弹珠的材料需要放置在透明弹珠的中间,也就是所谓的「夹心」:
两种材料在向外流出时,一同倒出,此时透明玻璃的材料会正好包裹着有色材料。裁剪时你可以清晰的看到它们的分层:
两种材料会一同进入成型与冷却阶段,此时弹珠在机器的影响下会不断旋转,内部的有色材料同时被不断甩动,并在冷却前逐渐被转成了我们常见的猫眼状。这样,弹珠里的色彩就添加好了。
最后无论是哪种弹珠,都要进行检查与包装,然后送到我们手上。
当然这种流水线出来的弹珠很难控制花纹的样式,想要弹珠拥有更加漂亮的花纹,则需要手工制作。
简单来说,手工玻璃弹珠的制作可以理解为,一个不断软化与不断塑形的过程。例如下面这位小哥的演示,首先也是将透明玻璃材料进行高温软化,然后将其盘成圆柱状:
接着裁剪一些想要的有色玻璃芯,例如纯白色,然后就像蘸酱一样,间隔均匀的滚动一番,让玻璃芯粘在玻璃主体上:
然后再度软化,让白色玻璃芯与透明材料成为一体:
通过不断的旋转拉伸,把白色玻璃芯拉出网纹花纹:
接着再次软化并再粘上一层玻璃芯:
接着继续软化,然后处理玻璃芯的形状,使其变得扁平:
再一次将它们软化后,再来一层彩色玻璃芯,并重复之前的步骤,继续盘它:
这时,弹珠材料已经拥有多层花纹,而且被盘的又细又长,尤其是在前端,弹珠的雏形已经被盘出来了:
接着敲断弹珠与材料的连接点,获得弹珠,但此刻弹珠上方还有裂痕:
直接用高温软化,让断裂痕迹消失:
最后放置冷却,一颗多层花纹弹珠就做好了:
当然,手工制作弹珠时不同的花纹有不同的制作步骤与工序。下面的做法就更像是做汉堡,先把芯烧好,然后两面夹上透明玻璃材料:
最后无限拉伸,软化、裁剪,滚上一层玻璃芯:
接着开始打磨,盘它:
最后成型、冷却,内部的花纹同样精美:
另外一些能力超群的大触还能够直接在弹珠中雕刻。例如这位Vicki Schneider,他通过不断软化弹珠,然后将其它材料不断与弹珠进行融合:
最终制作的效果简直让这颗玻璃球变成了透明的精灵球:
另一位大佬William Grout则在弹珠内部制造了一片海底世界:
看完以上大佬们的手艺,相信你已经发现了,除了我们常见的透明和猫眼外,弹珠的种类还有很多。例如刚才我们最初提过手工作品就属于Latticino core,它的特点是弹珠中会有螺旋网状花纹结构:
下面这类被称为Indian Swirls,也就是印度漩涡,不过这类弹珠其实和印度没有半毛钱关系:
(素材来源自Mary Costello)
另外就艺术感而言,Winlock Marbles系列也很出名。这些弹珠犹如注入了水彩一样,颜色绚烂夺目。
Fritts art glass同样是备受好评的弹珠系列之一,它们的花纹更加细腻,线条也更加扭曲,给我一种蝴蝶翅膀的感觉:
当然,近年来比较火的是宇宙弹珠。把宇宙握在手里是什么感觉?看看Satoshi Tomizu的作品就知道了:
他利用如蛋白石等其他材料在弹珠中制造了各种行星,金、银颗粒点缀出了璀璨银河,灯光一开,效果不要太美:
提到宇宙弹珠,就不得不提Gateson Recko,他创造的星空更加深邃神秘:
这是动态效果,小小的弹珠中简直包藏着星辰大海:
另外小编还在Gateson Recko的官网发现另一个神仙作品,下面的弹珠中藏了一座岛屿:
怎么样,看完是不是很想来一颗?当然,这些弹珠的售价也很「仙」,就拿Gateson Recko的系列作品来说,价格在400到1200美金,也就是最贵8000人民币左右。
感觉有点贵的离谱?但对于稀有弹珠来说,这些数字根本不算什么。例如下面这颗经典lutzes弹珠。它由知名弹珠公司Christensen Agate打造,据了解因为其颜色极为罕见,且有一定的年代价值,曾被拍卖至25000美元,是世界上最贵的弹珠之一。
没错,一颗小球价值16万多人民币。所以,手里有弹珠的别忘了互相转告下,不要轻易抛弃它们,毕竟没准其中哪一颗就是绝世孤品,发家致富,就靠它了。
参考资料:
https://www.imarbles.com/makingmarbles.php
https://en.wikipedia.org/wiki/Marble_(toy)
https://www.youtube.com/watch?v=p8JKnubjnJo
https://www.youtube.com/watch?v=PSjd1opk3i4
https://gazettereview.com/2018/09/top-10-most-expensive-marbles/
http://www.winlockmarbles.com/single_marbles.htm
http://www.frittsartglass.com/gallery.html
https://universemarbles.bigcartel.com/product/desert-island-marble-skull-island-4
来源:狂丸科学
编辑:重光
近期热门文章Top10
↓ 点击标题即可查看 ↓
1. 大龄单身狗返乡过年期间瞬时压力激增现象及其应对措施研究
2. 12个革命性的公式
3. 最小有多小?最大有多大?
4. 一幅图读懂量子力学(大神的战争)
5. WiFi穿墙完全指南
6. 为什么你吃的食物跟广告上的永远不一样?
7. 你知道爱因斯坦人生中发表的第一篇论文是什么吗?
8. 出生在显赫世家是怎样的体验?
9. 理论物理学家费纸,实验物理学家费电,理论实验物理学家费?
10. 这些东西,看过的人都转疯了!
是利用完全二叉树的结构来维护一组数据,然后进行相关操作,一般的操作进行一次的时间复杂度在
O(1)~O(logn)之间。
可谓是相当的引领时尚潮流啊(我不信学信息学的你看到log和1的时间复杂度不会激动一下下)!。
什么是完全二叉树呢?别急着去百度啊,要百度我帮你百度:
若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中
在最左边,这就是完全二叉树。我们知道二叉树可以用数组模拟,堆自然也可以。
现在让我们来画一棵完全二叉树:
从图中可以看出,元素的父亲节点数组下标是本身的1/2(只取整数部分),所以我们很容易去模拟,也很
容易证明其所有操作都为log级别~~
堆还分为两种类型:大根堆、小根堆
顾名思义,就是保证根节点是所有数据中最大/小,并且尽力让小的节点在上方
不过有一点需要注意:堆内的元素并不一定数组下标顺序来排序的!!很多的初学者会错误的认为大/小根堆中
下标为1就是第一大/小,2是第二大/小……
原因会在后面解释,现在你只需要深深地记住这一点!
我们刚刚画的完全二叉树中并没有任何元素,现在让我们加入一组数据吧!
下标从1到9分别加入:{8,5,2,10,3,7,1,4,6}。
如下图所示
(不要问我怎么加,想想你是怎么读入数组的。)
我们可以发现这组数据是杂乱无章的,我们该如何去维护呢?
现在我就来介绍一下堆的几个基本操作:
学习C/C++的同学有福利了,堆的代码一般十分之长,而我们伟大的STL模板库给我们提供了两种简单方便堆操作的方式,
想学习的可以看看这个:http://www.cnblogs.com/helloworld-c/p/4854463.html 密码: abcd111
我个人建议吧,起码知道一下实现的过程,STL只能是锦上添花,绝不可以雪中送炭!!
万一哪天要你模拟堆的某一操作过程,而你只知道STL却不知道原理,看不出这个题目是堆,事后和其他OIer
讨论出题解,那岂不是砍舌头吃苦瓜,哭得笑哈哈。
那么我们开始讲解操作过程吧,我们以小根堆为例
刚刚那组未处理过的数据中我们很容易就能看出,根节点1元素8绝对不是最小的
我们很容易发现它的一个儿子节点3(元素2)比它来的小,我们怎么将它放到最高点呢?很简单,直接交换嘛~~
但是,我们又发现了,3的一个儿子节点7(元素1)似乎更适合在根节点。
这时候我们是无法直接和根节点交换的,那我们就需要一个操作来实现这个交换过程,那就是上浮 shift_up。
操作过程如下:
从当前结点开始,和它的父亲节点比较,若是比父亲节点来的小,就交换,
然后将当前询问的节点下标更新为原父亲节点下标;否则退出。
模拟操作图示:
伪代码如下:
Shift_up( i )
{
while( i / 2 >=1)
{
if( 堆数组名[ i ] < 堆数组名[ i/2 ] )
{
swap( 堆数组名[ i ] , 堆数组名[ i/2 ]) ;
i=i / 2;
}
else break;
}
这一次上浮完毕之后呢,我们又发现了一个问题,貌似节点3(元素8)不太合适放在那,而它的子节点7(元素2)
好像才应该在那个位置。
此时的你应该会说:“赐予我力量,让节点7上浮吧,我是OIer!”
然而,上帝(我很不要脸的说是我)赐予你另外一种力量,让节点3下沉!
那么问题来了:节点3应该往哪下沉呢?
我们知道,小根堆是尽力要让小的元素在较上方的节点,而下沉与上浮一样要以交换来不断操作,所以我们应该
让节点7与其交换。
由此我们可以得出下沉的算法了:
让当前结点的左右儿子(如果有的话)作比较,哪个比较小就和它交换,
并更新询问节点的下标为被交换的儿子节点下标,否则退出。
模拟操作图示:
伪代码如下:
Shift_down( i , n ) //n表示当前有n个节点
{
while( i * 2 <=n)
{
T=i * 2 ;
if( T + 1 <=n && 堆数组名[ T + 1 ] < 堆数组名[ T ])
T++;
if( 堆数组名[ i ] < 堆数组名[ T ] )
{
swap( 堆数组名[ i ] , 堆数组名[ T ] );
i=T;
}
else break;
}
讲完了上浮和下沉,接下来就是插入操作了~~~~
我们前面用的插入是直接插入,所以数据才会杂乱无章,那么我们如何在插入的时候边维护堆呢?
其实很简单,每次插入的时候呢,我们都往最后一个插入,让后使它上浮。
(这个不需要图示了吧…)
伪代码如下:
Push ( x )
{
n++;
堆数组名[ n ]=x;
Shift_up( n );
}
咳咳,说完了插入,我们总需要会弹出吧~~~~~
弹出,顾名思义就是把顶元素弹掉,但是,弹掉以后不是群龙无首吗??
我们如何去维护这堆数据呢?
稍加思考,我们不难得出一个十分巧妙的算法:
让根节点元素和尾节点进行交换,然后让现在的根元素下沉就可以了!
(这个也不需要图示吧…)
伪代码如下:
Pop ( x )
{
swap( 堆数组名[1] , 堆数组名[ n ] );
n--;
Shift_down( 1 );
}
接下来是取顶…..我想不需要说什么了吧,根节点数组下标必定是1,返回堆[ 1 ]就OK了~~
注意:每次取顶要判断堆内是否有元素,否则..你懂的
图示和伪代码省略,如果你这都不会那你可以重新开始学信息学了,当然如果你是小白….这种稍微高级的数据
结构还是以后再说吧。
说完这些,我们再来说说堆排序。之前说过堆是无法以数组下标的顺序来来排序的对吧?
所以我个人认为呢,并不存在堆排序这样的操作,即便网上有很多堆排序的算法,但是我这里有个更加方便的算法:
开一个新的数组,每次取堆顶元素放进去,然后弹掉堆顶就OK了~
伪代码如下:
Heap_sort( a[] )
{
k=0;
while( size > 0 )
{
k++;
a[ k ]=top();
pop();
}
}
堆排序的时间复杂度是O(nlogn)理论上是十分稳定的,但是对于我们来说并没有什么卵用。
我们要排序的话,直接使用快排即可,时间更快,用堆排还需要O(2*n)的空间。这也是为什么我说堆的操作
时间复杂度在O(1)~O(logn)。
讲完到这里,堆也基本介绍完了,那么它有什么用呢??
举个粒子,比如当我们每次都要取某一些元素的最小值,而取出来操作后要再放回去,重复做这样的事情。
我们若是用快排的话,最坏的情况需要O(q*n^2),而若是堆,仅需要O(q*logn),时间复杂度瞬间低了不少。
还有一种最短路算法——Dijkstra,需要用到堆来优化,这个算法我后面会找个时间介绍给大家。
文就某一个具体的类型场景,着重介绍微前端架构可以带来什么价值以及具体实践过程中需要关注的技术决策,并辅以具体代码,从而帮助读者构建一个生产可用的微前端架构系统。
Techniques, strategies and recipes for building a modern web app with multiple teams using different JavaScript frameworks. — Micro Frontends
前言
目前社区有很多关于微前端架构的介绍,但大多停留在概念介绍的阶段。而本文会就某一个具体的类型场景,着重介绍微前端架构可以带来什么价值以及具体实践过程中需要关注的技术决策,并辅以具体代码,从而能真正意义上帮助你构建一个生产可用的微前端架构系统。
而对于微前端的概念感兴趣或不熟悉的同学,可以通过搜索引擎来获取更多信息,如 知乎上的相关内容, 本文不再做过多介绍。
微前端的价值
微前端架构具备以下几个核心价值:
微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用 (Frontend Monolith) 后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。
针对中后台应用的解决方案
中后台应用由于其应用生命周期长 (动辄 3+ 年) 等特点,最后演变成一个巨石应用的概率往往高于其他类型的 web 应用。而从技术实现角度,微前端架构解决方案大概分为两类场景:
本文将着重介绍单实例场景下的微前端架构实践方案(基于 single-spa),因为这个场景更贴近大部分中后台应用。
行业现状
传统的云控制台应用,几乎都会面临业务快速发展之后,单体应用进化成巨石应用的问题。为了解决产品研发之间各种耦合的问题,大部分企业也都会有自己的解决方案。笔者于 17 年底,针对国内外几个著名的云产品控制台,做过这样一个技术调研:
MPA 方案的优点在于部署简单、各应用之间硬隔离,天生具备技术栈无关、独立开发、独立部署的特性。缺点则也很明显,应用之间切换会造成浏览器重刷,由于产品域名之间相互跳转,流程体验上会存在断点。
SPA 则天生具备体验上的优势,应用直接无刷新切换,能极大的保证多产品之间流程操作串联时的流程性。缺点则在于各应用技术栈之间是强耦合的。
那我们有没有可能将 MPA 和 SPA 两者的优势结合起来,构建出一个相对完善的微前端架构方案呢?
JSConf China 2016 大会上,ucloud 的同学分享了他们的基于 angularjs 的方案(单页应用“联邦制”实践),里面提到的 "联邦制" 概念很贴切,可以认为是早期的基于耦合技术栈的微前端架构实践。
微前端架构实践中的问题
可以发现,微前端架构的优势,正是 MPA 与 SPA 架构优势的合集。即保证应用具备独立开发权的同时,又有将它们整合到一起保证产品完整的流程体验的能力。
这样一套模式下,应用的架构就会变成:
Stitching layer 作为主框架的核心成员,充当调度者的角色,由它来决定在不同的条件下激活不同的子应用。因此主框架的定位则仅仅是:导航路由 + 资源加载框架。
而具体要实现这样一套架构,我们需要解决以下几个技术问题:
路由系统及 Future State
我们在一个实现了微前端内核的产品中,正常访问一个子应用的页面时,可能会有这样一个链路:
由于我们的子应用都是 lazy load 的,当浏览器重新刷新时,主框架的资源会被重新加载,同时异步 load 子应用的静态资源,由于此时主应用的路由系统已经激活,但子应用的资源可能还没有完全加载完毕,从而导致路由注册表里发现没有能匹配子应用 /subApp/123/detail 的规则,这时候就会导致跳 NotFound 页或者直接路由报错。
这个问题在所有 lazy load 方式加载子应用的方案中都会碰到,早些年前 angularjs 社区把这个问题统一称之为 Future State: https://ui-router.github.io/guide/lazyloading#future-states
解决的思路也很简单,我们需要设计这样一套路由机制:
主框架配置子应用的路由为 subApp: { url: '/subApp/**', entry: './subApp.js' },则当浏览器的地址为 /subApp/abc 时,框架需要先加载 entry 资源,待 entry 资源加载完毕,确保子应用的路由系统注册进主框架之后后,再去由子应用的路由系统接管 url change 事件。同时在子应用路由切出时,主框架需要触发相应的 destroy 事件,子应用在监听到该事件时,调用自己的卸载方法卸载应用,如 React 场景下 destroy=()=> ReactDOM.unmountAtNode(container)。
要实现这样一套机制,我们可以自己去劫持 url change 事件从而实现自己的路由系统,也可以基于社区已有的 ui router library,尤其是 react-router 在 v4 之后实现了 Dynamic Routing 能力,我们只需要复写一部分路由发现的逻辑即可。这里我们推荐直接选择社区比较完善的相关实践 single-spa。
App Entry
解决了路由问题后,主框架与子应用集成的方式,也会成为一个需要重点关注的技术决策。
构建时组合 VS 运行时组合
微前端架构模式下,子应用打包的方式,基本分为两种:
两者的优缺点也很明显:
很显然,要实现真正的技术栈无关跟独立部署两个核心目标,大部分场景下我们需要使用运行时加载子应用这种方案。
JS Entry vs HTML Entry
在确定了运行时载入的方案后,另一个需要决策的点是,我们需要子应用提供什么形式的资源作为渲染入口?
JS Entry 的方式通常是子应用将资源打成一个 entry script,比如 single-spa 的 example 中的方式。但这个方案的限制也颇多,如要求子应用的所有资源打包到一个 js bundle 里,包括 css、图片等资源。除了打出来的包可能体积庞大之外的问题之外,资源的并行加载等特性也无法利用上。
HTML Entry 则更加灵活,直接将子应用打出来 HTML 作为入口,主框架可以通过 fetch html 的方式获取子应用的静态资源,同时将 HTML document 作为子节点塞到主框架的容器中。这样不仅可以极大的减少主应用的接入成本,子应用的开发方式及打包方式基本上也不需要调整,而且可以天然的解决子应用之间样式隔离的问题 (后面提到)。想象一下这样一个场景:
<!-- 子应用 index.html --> <script src="//unpkg/antd.min.js"></script> <body> <main id="root"></main> </body> // 子应用入口 ReactDOM.render(<App/>, document.getElementById('root'))
如果是 JS Entry 方案,主框架需要在子应用加载之前构建好相应的容器节点 (比如这里的 "#root" 节点),不然子应用加载时会因为找不到 container 报错。但问题在于,主应用并不能保证子应用使用的容器节点为某一特定标记元素。而 HTML Entry 的方案则天然能解决这一问题,保留子应用完整的环境上下文,从而确保子应用有良好的开发体验。
HTML Entry 方案下,主框架注册子应用的方式则变成:
framework.registerApp('subApp1', { entry: '//abc.alipay.com/index.html'})
本质上这里 HTML 充当的是应用静态资源表的角色,在某些场景下,我们也可以将 HTML Entry 的方案优化成 Config Entry,从而减少一次请求,如:
framework.registerApp('subApp1', { html: '', scripts: ['//abc.alipay.com/index.js'], css: ['//abc.alipay.com/index.css']})
总结一下:
模块导入
微前端架构下,我们需要获取到子应用暴露出的一些钩子引用,如 bootstrap、mount、unmout 等 (参考 single-spa),从而能对接入应用有一个完整的生命周期控制。而由于子应用通常又有集成部署、独立部署两种模式同时支持的需求,使得我们只能选择 umd 这种兼容性的模块格式打包我们的子应用。如何在浏览器运行时获取远程脚本中导出的模块引用也是一个需要解决的问题。
通常我们第一反应的解法,也是最简单的解法就是与子应用与主框架之间约定好一个全局变量,把导出的钩子引用挂载到这个全局变量上,然后主应用从这里面取生命周期函数。
这个方案很好用,但是最大的问题是,主应用与子应用之间存在一种强约定的打包协议。那我们是否能找出一种松耦合的解决方案呢?
很简单,我们只需要走 umd 包格式中的 global export 方式获取子应用的导出即可,大体的思路是通过给 window 变量打标记,记住每次最后添加的全局变量,这个变量一般就是应用 export 后挂载到 global 上的变量。实现方式可以参考 systemjs global import,这里不再赘述,相关链接: https://github.com/systemjs/systemjs/blob/master/src/extras/global.js
应用隔离
微前端架构方案中有两个非常关键的问题,有没有解决这两个问题将直接标志你的方案是否真的生产可用。比较遗憾的是此前社区在这个问题上的处理都会不约而同选择”绕道“的方式,比如通过主子应用之间的一些默认约定去规避冲突。而今天我们会尝试从纯技术角度,更智能的解决应用之间可能冲突的问题。
样式隔离
由于微前端场景下,不同技术栈的子应用会被集成到同一个运行时中,所以我们必须在框架层确保各个子应用之间不会出现样式互相干扰的问题。
针对 "Isolated Styles" 这个问题,如果不考虑浏览器兼容性,通常第一个浮现到我们脑海里的方案会是 Web Components。基于 Web Components 的 Shadow DOM 能力,我们可以将每个子应用包裹到一个 Shadow DOM 中,保证其运行时的样式的绝对隔离。
但 Shadow DOM 方案在工程实践中会碰到一个常见问题,比如我们这样去构建了一个在 Shadow DOM 里渲染的子应用:
const shadow=document.querySelector('#hostElement').attachShadow({mode: 'open'}); shadow.innerHTML='<sub-app>Here is some new text</sub-app><link rel="stylesheet" href="//unpkg.com/antd/antd.min.css">';
由于子应用的样式作用域仅在 shadow 元素下,那么一旦子应用中出现运行时越界跑到外面构建 DOM 的场景,必定会导致构建出来的 DOM 无法应用子应用的样式的情况。
比如 sub-app 里调用了 antd modal 组件,由于 modal 是动态挂载到 document.body 的,而由于 Shadow DOM 的特性 antd 的样式只会在 shadow 这个作用域下生效,结果就是弹出框无法应用到 antd 的样式。解决的办法是把 antd 样式上浮一层,丢到主文档里,但这么做意味着子应用的样式直接泄露到主文档了。
社区通常的实践是通过约定 css 前缀的方式来避免样式冲突,即各个子应用使用特定的前缀来命名 class,或者直接基于 css module 方案写样式。对于一个全新的项目,这样当然是可行,但是通常微前端架构更多的目标是解决存量 / 遗产 应用的接入问题。很显然遗产应用通常是很难有动力做大幅改造的。
最主要的是,约定的方式有一个无法解决的问题,假如子应用中使用了三方的组件库,三方库在写入了大量的全局样式的同时又不支持定制化前缀?比如 a 应用引入了 antd 2.x,而 b 应用引入了 antd 3.x,两个版本的 antd 都写入了全局的 .menu class,但又彼此不兼容怎么办?
解决方案其实很简单,我们只需要在应用切出 / 卸载后,同时卸载掉其样式表即可,原理是浏览器会对所有的样式表的插入、移除做整个 CSSOM 的重构,从而达到 插入、卸载 样式的目的。这样即能保证,在一个时间点里,只有一个应用的样式表是生效的。
上文提到的 HTML Entry 方案则天生具备样式隔离的特性,因为应用卸载后会直接移除去 HTML 结构,从而自动移除了其样式表。
比如 HTML Entry 模式下,子应用加载完成的后的 DOM 结构可能长这样:
<html> <body> <main id="subApp"> // 子应用完整的 html 结构 <link rel="stylesheet" href="//alipay.com/subapp.css"> <div id="root">....</div> </main> </body> </html>
当子应用被替换或卸载时,subApp 节点的 innerHTML 也会被复写,//alipay.com/subapp.css 也就自然被移除样式也随之卸载了。
JS 隔离
解决了样式隔离的问题后,有一个更关键的问题我们还没有解决:如何确保各个子应用之间的全局变量不会互相干扰,从而保证每个子应用之间的软隔离?
这个问题比样式隔离的问题更棘手,社区的普遍玩法是给一些全局副作用加各种前缀从而避免冲突。但其实我们都明白,这种通过团队间的”口头“约定的方式往往低效且易碎,所有依赖人为约束的方案都很难避免由于人的疏忽导致的线上 bug。那么我们是否有可能打造出一个好用的且完全无约束的 JS 隔离方案呢?
针对 JS 隔离的问题,我们独创了一个运行时的 JS 沙箱。简单画了个架构图:
即在应用的 bootstrap 及 mount 两个生命周期开始之前分别给全局状态打下快照,然后当应用切出 / 卸载时,将状态回滚至 bootstrap 开始之前的阶段,确保应用对全局状态的污染全部清零。而当应用二次进入时则再恢复至 mount 前的状态的,从而确保应用在 remount 时拥有跟第一次 mount 时一致的全局上下文。
当然沙箱里做的事情还远不止这些,其他的还包括一些对全局事件监听的劫持等,以确保应用在切出之后,对全局事件的监听能得到完整的卸载,同时也会在 remount 时重新监听这些全局事件,从而模拟出与应用独立运行时一致的沙箱环境。
蚂蚁的微前端落地实践
自去年年底伊始,我们便尝试基于微前端架构模式,构建出一套全链路的面向中后台场景的产品接入平台,目的是解决不同产品之间集成困难、流程割裂的问题,希望接入平台后的应用,不论使用哪种技术栈,在运行时都可以通过自定义配置,实现不同应用之间页面级别的自由组合,从而生成一个千人千面的个性化控制台。
目前这套平台已在蚂蚁生产环境运行半年多,同时接入了多个产品线的 40+ 应用、4+ 不同类型的技术栈。过程中针对大量微前端实践中的问题,我们总结出了一套完整的解决方案:
在内部得到充分的技术验证和线上考验之后,我们决定将这套解决方案开源出来!
qiankun - 一套完整的微前端解决方案
取名 qiankun,意为统一。我们希望通过 qiankun 这种技术手段,让你能很方便的将一个巨石应用改造成一个基于微前端架构的系统,并且不再需要去关注各种过程中的技术细节,做到真正的开箱即用和生产可用。
对于 umi 用户我们也提供了配套的 qiankun 插件 @umijs/plugin-qiankun ,以便于 umi 应用能几乎零成本的接入 qiankun。
qiankun 开源: https://github.com/umijs/qiankun
@umijs/plugin-qiankun: https://github.com/umijs/umi-plugin-qiankun/
这里有一份独家晋升指南:
部分VIP资料:
最后说一下的,也就是以上教程的获取方式!
领取方法:
还是以往不变的老规矩!
1.评论文章,没字数限制,一个字都行!然后转发出去!
2.关注小编,成为小编的粉丝!
3.私信小编:“前端教程”即可!
通过申请后会逐个开通权限,小助手精力有限,手慢无哦
视频的价值取决于领取后的行动,大家千万别做收藏党。和志同道合的人一起深入讨论与学习 Web前端技术,也欢迎转给需要的朋友!
*请认真填写需求信息,我们会在24小时内与您取得联系。