qiankun 是主流的微前端方案,其他的还有京东的 micro-app、腾讯的 wujie 等。

微前端就是可以一个页面跑多个 vue、react 甚至 jquery 等不同项目,它之间的 JS、CSS 相互隔离运行,不会相互影响,但也有通信机制可以通信。

那微前端怎么实现呢?

其实也简单,一句话就可以说明白:当路由切换的时候,去下载对应应用的代码,然后跑在容器里。

比如 single-spa,它做的就是监听路由变化,路由切换的时候加载、卸载注册的应用的代码。

只不过 single-spa 的入口是一个 js 文件,需要代码里手动指定要加载啥 js、css 等,不方便维护。

qiankun 只是对 single-spa 的升级。

它升级了啥东西呢?

第一个就是入口,改为了 html 作为入口,解析 html,从中分析 js、css,然后再加载,这个是 import-html-entry 这个包实现的。

所以你在 qiankun 的 package.json 里可以看到 single-spa 和 import-html-entry 这俩依赖。

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_JavaScript

加载之后呢?

自然是放容器里运行呀。

这个容器 single-spa 也没做,qiankun 做了。

它是把 js 代码包裹了一层 function,然后再把内部的 window 用 Proxy 包一层,这样内部的代码就被完全隔离了,这样就实现了一个 JS 沙箱。

这部分代码在 import-html-entry 里,也就是加载后的 js 就被包裹了一层:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_CSS_02

原理很容易理解,就是 function 包裹了一层,所以代码放在了单独作用域跑,又用 with 修改了 window,所以 window 也被隔离了。

这是 qiankun 的 JS 沙箱实现方案,其他的微前端方式实现沙箱可能用 iframe、web components 等方式。

微前端方案的功能就那一句话:当路由切换的时候,去下载对应应用的代码,然后跑在容器里。只不过这个容器的实现方案有差异。

此外,还要设计一套通信机制,这个倒是很容易。

除了 JS 隔离,还有 CSS 的隔离,不得不说,qiankun 的样式隔离是真的坑,也是我这主要想吐槽的点。

哪里坑呢?

跑一下就知道了。

把 qiankun 代码 clone 下来,它有 examples,我们用这个来测:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_选择器_03

安装依赖,然后执行 examples:install 和 examples:start-multiple 这俩 npm scripts。

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_JavaScript_04

跑起来是这样的:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_css_05

在 react15 项目下引入 button 和 modal 这俩 antd 组件:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_选择器_06

然后再跑一下:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_JavaScript_07

大家可能会说,这很对呀,有什么问题?

那是因为现在是没启用 css 隔离的,所有的 css 都在全局:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_CSS_08

这样各应用的样式会相互影响,比如主应用和子应用。

比如,我们在主应用 main 里加一个样式:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_CSS_09

子应用会受到影响:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_选择器_10

在子应用 react15 加一个样式:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_JavaScript_11

其他应用也会受到影响:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_JavaScript_12

这样的微前端项目那还了得?各个应用样式都会相互影响。

除非命名给错开,通过 bem 等命名方案。

但这不靠谱,还是得通过框架层面解决。

qiankun 提供了两种样式隔离方案:shadow dom 和自己实现的 scoped。

shadow dom 是 web components 技术的一部分,其实就一个 attachShadow 的 api:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_选择器_13

web components 添加内容的时候,不直接 appendChild,而是先 attach 个 shadow,然后再在下面 appendChild。

效果就是这样的:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_选择器_14

qiankun 要在加载子应用的时候指定 strictStyleIsolation 才会开启这种样式隔离:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_css_15

那加了一层 shadow dom 有什么用呢?

试下就知道了:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_选择器_16

还是一样的代码,但是运行结果不一样了。

首先,父应用设置的那个绿色背景样式没有生效:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_前端_17

说明有了一层 shadow dom 以后,外界影响不了 shadow dom 内的样式。

然后,外面这些按钮也没有变红了:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_JavaScript_18

说明 shadow dom 内的样式也影响不了外界。

那为啥弹窗会变成这样呢?样式全没了:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_选择器_19

因为弹窗默认是挂在 body 上的,也就不在 shadow dom 里了,那 shadow dom 里给它加的样式自然就不生效了。

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_CSS_20

为啥弹窗要挂在 body 下,这个是为了避免被父元素的样式影响,比如父元素设置了 display:none,那这个弹窗是不是就死活弹不出来了?

还有样式也会被父元素继承过来的样式影响。

所以干脆独立出来,放到 body 下。

问题找到了,是 shadow dom 导致的,shadow dom 样式影响不了外界,外界样式也影响不了 shadow dom 内的元素。

也不能说是 shadow dom 有问题,人家本来就是这么设计的,只不过用来做微前端样式隔离还是不够的。

弹窗的样式问题怎么解决?

是通过通信机制把弹窗样式传过去么?那是不是改造成本又增加了?

所以 qiankun 的 shadow dom 的样式隔离方案是有问题的。

再来看另一种,这种是实验性的,所以叫 experimentalStyleIsolation:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_前端_21

它是怎么做的样式隔离呢?

借鉴了 scoped css 的思路。

也就是对所有样式加了一层 data-qiankun=“应用名” 的选择器来隔离:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_选择器_22

这样其他应用的样式能影响子应用了,但是子应用的样式还是影响不了父应用,看上面的弹窗就知道了。

为什么呢?

因为所有的样式都加了 data-qiankun 的限制,那就影响不了子应用外部了,所以挂在 body 的弹窗还是加不了样式。

有同学说,那支持声明 global 样式不就行了?

问题就在这,qiankun 并没有实现这个功能。

而且如果要在 qiankun 里实现全部的 scoped css 功能,那为啥不直接用 scoped css 或者类似的 css modules 呢?

scoped css 是 vue loader 实现的组件级样式隔离方案,用起来只要给单文件组件的 style 加一个 scoped 属性:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_前端_23

css 选择器就会加上 data-xx 的修饰,这样就限制了样式只会在组件范围内生效。

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_前端_24

只会在最后一个选择器加 data-xxx,因为这样足够了:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_CSS_25

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_CSS_26

此外它还支持 /deep/ 给子元素传样式:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_前端_27

这样 deep 后面的样式是不带 data-xx 的,可以影响子组件:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_JavaScript_28

此外,也可以再开一个 style 标签写全局的样式:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_css_29

综上,scoped css 支持组件级别样式隔离,还能传样式给子组件、设置全局样式等。

功能上比 qiankun 的那个应用级样式隔离完善多了。

有了 scoped css,还需要 qiankun 的样式隔离么?

完全不用。

再来看 css modules,react 项目基本都用这个。

css modules 是 css-loader 实现的功能,开启也是相当简单:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_JavaScript_30

比如这样的样式:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_CSS_31

开启后就会变成这样:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_CSS_32

在选择器名字上加了 hash。

那么问题来了,scoped css 是多了一个属性选择器而已,本身的 class name 没变:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_选择器_33

所以 class name 该怎么设怎么设,不受影响。

但是 css modules 是改变 class name 本身,加上了 hash:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_css_34

这时候还按照原来的方式写 class name 可以么?

不可以了。

因为最终的 class name 是编译后才生成的,所以要改成这样的方式:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_css_35

动态引入 className,这样编译生成最终的 className 时就会替换这里。

你还可以加个 :global() 把某个选择器变为全局的:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_前端_36

这样就相当于 scoped css 的 /deep/ 和全局样式功能了。

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_选择器_37

一般都这样用:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_JavaScript_38

顶层 class name 用 css modules 加上 hash,内部的 class name 该怎么用怎么用:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_前端_39

生成的 css 是这样的:

微前端方案 qiankun 的样式隔离能不用就别用吧,比较坑_JavaScript_40

既达到了组件样式隔离的目的,写起来还简单。

综上,css modules 和 scoped css 差不多,都能实现组件级别样式隔离,能设置子组件和全局样式。只是实现方式不同,导致了使用起来也有差异。

不管是 css modules 还是 scoped css 都比 qiankun 自带的样式隔离方案好用的多,那为什么微前端框架还要实现样式隔离呢?直接让应用自己去用 css modules 或者 scoped css 不就行了?

那是因为还有一些别的项目,比如 jquery 项目,你怎么用 css modules?就算用,是不是要改造,改造成本又上去了。

所以微前端框架还是要做样式隔离的。

只是现在的应用,不管是 vue 还是 react 基本都开启了组件级别样式隔离,qiankun 自带的样式隔离问题太多了,不如不用。

总结

微前端就是在路由变化的时候,加载对应应用的代码,并在容器内跑起来。

qiankun、wujie、micro-app 的区别主要还是实现容器(或者叫沙箱)上有区别,比如 qiankun 是 function + proxy + with,micro-app 是 web components,而 wujie 是 web components 和 iframe。

流程都是差不多的。

qiankun 做了样式隔离,有 shadow dom 和 scoped 两种方案,但都有问题:

  • shadow dom 自带样式隔离,但是 shadow dom 内的样式和外界互不影响,导致挂在弹窗的样式会加不上。父应用也没法设置子应用的样式。
  • scoped 的方案是给选择器加了一个 data-qiankun='应用名' 的选择器,这样父应用能设置子应用样式,这样能隔离样式,但是同样有挂在 body 的弹窗样式设置不上的问题,因为 qiankun 的 scoped 不支持全局样式

而 react 和 vue 项目本身都会用 scoped css 或者 css modules 的组件级别样式隔离方案,这俩方案都支持传递样式给子元素、设置全局样式等,只是实现和使用方式不同。

现在的 vue、react 项目基本都做了组件样式隔离了,有点全局样式也是可控的,真没必要用 qiankun 的那个。

qiankun 的样式隔离方案比较坑,能不用就别用吧。