Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

子应用通过css加载的图片无法显示 #19

Closed
Sapphire2k opened this issue Aug 1, 2022 · 19 comments
Closed

子应用通过css加载的图片无法显示 #19

Sapphire2k opened this issue Aug 1, 2022 · 19 comments

Comments

@Sapphire2k
Copy link

// Vue 2 / App.vue
<template>
    <div>
      [Test] img src
      <img src="./assets/logo.png" alt="">
    </div>
    <div>
      [Test] i background url
      <i class="icon-logo"></i>
    </div>
</template>
<style>
.icon-logo {
  display: inline-block;
  width: 100px;
  height: 100px;
  background: url("./assets/logo.png") 0 0 / 100px 100px no-repeat;
}
</style>

image

子应用独立运行可以正确显示 i 标签的图片
但是在主应用里运行子应用的时候就失效了

@Sapphire2k Sapphire2k changed the title 子应用通过css加载的icon无法显示 子应用通过css加载的图片无法显示 Aug 1, 2022
@yiludege
Copy link
Collaborator

yiludege commented Aug 1, 2022

已知问题:
1、对于js沙箱,子应用的 iframe 会设置 base 标签,所以在里面跑的script和接口请求相对都能够正确修正
2、对于dom沙箱,框架劫持了img、a、source、link、script等元素的原型方法,当这些元素设置地址的时候会将相对地址改写成绝对地址
3、对于在css内部的比如background-image: url('./images/my-image.png')框架确实没有处理,可以参考常见问题6

@yiludege
Copy link
Collaborator

yiludege commented Aug 1, 2022

目前需要用户采用css-loader来手动的将相对地址替换成绝对地址,后续官方会出一个plugin来解决这个问题

@yiludege yiludege closed this as completed Aug 1, 2022
@Sapphire2k
Copy link
Author

目前需要用户采用css-loader来手动的将相对地址替换成绝对地址,后续官方会出一个plugin来解决这个问题

好的感谢,所以目前css-loader只能通过正则匹配替换了?

@yiludege
Copy link
Collaborator

yiludege commented Aug 1, 2022

是的,如果这块框架做了的话,也是采用正则匹配来做的,不知道webpack有没有什么插件可以将这个相对地址变成绝对地址

@Sapphire2k
Copy link
Author

plugin cssLoader?: (code: string, url: string) => string;

似乎并没有返回当前子应用的url噢
css-loader undefined @font-face {

@yiludege
Copy link
Collaborator

yiludege commented Aug 2, 2022

这个url不一定有值的,比如内联的style标签,本地开发似乎走的是内联的方式,只有在html中的外联css才有url的,可以看看demo:
image
所以可以直接匹配 code ?

@Sapphire2k
Copy link
Author

我直接把plugins单独赋值,取当前的url

另一个问题,我用了flexible + px2rem, 第一次加载的时候是正常的,但是切换路由再次加载改子应用的时候,[data-dpr="1"] 的样式丢了。。

image

image

配置如下:
image

@Sapphire2k
Copy link
Author

再次加载子应用的时候 似乎不会重新载入css了

@yiludege
Copy link
Collaborator

yiludege commented Aug 2, 2022

这个和qiankun是一致的,子应用做了生命周期适配的话,切换子应用只是对比如vue实例的销毁和重建,js和css并不会重新执行,css框架会记录样式等切换的时候直接加上

@yiludege
Copy link
Collaborator

yiludege commented Aug 2, 2022

我直接把plugins单独赋值,取当前的url

另一个问题,我用了flexible + px2rem, 第一次加载的时候是正常的,但是切换路由再次加载改子应用的时候,[data-dpr="1"] 的样式丢了。。

这个地方确实可能存在bug,我需要验证一下

@yiludege
Copy link
Collaborator

yiludege commented Aug 2, 2022

这个和qiankun是一致的,子应用做了生命周期适配的话,切换子应用只是对比如vue实例的销毁和重建,js和css并不会重新执行,css框架会记录样式等切换的时候直接加上

data-dpr在切换子应用没有生效的原因不是对应的css丢失了,而是html丢失了data-dpr属性,因为flexible只会执行一次,然后计算dpr然后添加到html标签属性上,切走子应用后子应用的dom全部销毁,切换回来由于flexible不会运行导致html没有添加上data-dpr,

解决方案:
1、如果子应用是保活模式(alive=ture)、重建模式(alive=false && 没有做生命周期适配)都不会有这个问题
2、如果子应用是单例模式(做了生命周期适配),可以在beforeUnmount生命周期保存一下子应用的html data-dpr,然后在afterMount生命周期重新设置html data-dpr

let dataDpr = null

beforeUnmount = (appwindow) => dataDpr = appwindow.document.documentElement.documentElement.getAttribute('data-dpr')

afterMount = (appwindow) => dataDpr && appwindow.document.documentElement.documentElement.setAttribute('data-dpr', dataDpr)

@Sapphire2k
Copy link
Author

Sapphire2k commented Aug 8, 2022

感谢!搞定了dpr 但是又发现新问题了

问题:访问Vue2 / 弹窗 样式丢失
复现步骤:
情况一、预加载[开] 降级[关]
访问路径:介绍 -> vue2 -> Tab 弹窗 【样式丢失】-> 介绍 -> vue2 -> 弹窗 样式生效

情况二、预加载[关] 降级[关]

刷新 介绍 -> vue2 -> 弹窗 样式生效

刷新 介绍 -> vue2 -> 介绍 -> vue2 -> Tab 弹窗 【样式丢失】 -> Tab 首页 -> Tab 弹窗 【样式丢失】 -> 介绍 -> vue2 -> 弹窗 样式生效

总结:看起来在首次加载子应用的时候,未加载路由样式会在第二次重新访问的时候丢失,在第三次访问的时候恢复

样式显示如下图,略显粗暴😂:
// Dialog.vue

<style lang="less" scoped>
  .content {
    background-color: #0239d0;
    color: #fff;
  }
</style>

image

按照这个套路,给vue2第三个Tab 路由,也加上样式出现同样的情况,但是都在再次加载vue2之后恢复样式,应该还是有bug的。

// Location.vue

<style scoped>
  .about {
    text-align: center;
  }
  img {
    width: 200px;
    height: 200px;
  }
  .location-content {
    background-color: #ec8279;
  }
</style>

image

@yiludege
Copy link
Collaborator

yiludege commented Aug 8, 2022

@Sapphire2k 非常感谢提供重要的线索和复现路径

目前排查的出结果,涉及的代码如下:

代码地址:webpack:///node_modules/vue-style-loader/lib/addStylesClient.js

// 获取head,缓存下来
var head = hasDocument && (document.head || document.getElementsByTagName('head')[0])

// 将样式代码添加到head元素上去
function createStyleElement () {
  var styleElement = document.createElement('style')
  styleElement.type = 'text/css'
  head.appendChild(styleElement)
  return styleElement
}

代码缓存了head元素而不是实时通过document.head去获取

而对于无界子应用单例模式,每次切换子应用包括从预加载成功到正式渲染,子应用都会经历渲染、卸载、重新渲染的过程,承载子应用的 shadow 也是如此,导致shadow内部的 head 每次都是重新生成,所以往缓存下来的 head 里面添加样式完全无法生效

我考虑一下应该怎么做才对

@yiludege
Copy link
Collaborator

yiludege commented Aug 9, 2022

记录一下三种解决方案:

1、复用head和body元素,但是需要清除上面的事件(目前最好的方案)

2、proxy返回一个proxy,这样缓存的head也就是一个proxy,每次都可以拿到最新的head(兼容性差)

3、只针对head、body的 appendChild、insertBefore 单独做处理(只能解决这两个接口,无法解决缓存的问题)

@yiludege
Copy link
Collaborator

yiludege commented Aug 11, 2022

已经发版解决,采用方案 1

@Sapphire2k
Copy link
Author

Sapphire2k commented Aug 12, 2022

已经发版解决,采用方案 1

问题解决了,感谢!

又发现一个新问题,子应用Vue实例缓存了,导致再次加载的时候,body下动态创建的dom丢失了也不会重新创建,感觉应该是比较常见的情况🤔

首次进入:
image

切换应用,再次进入:
image

// vue2 / Home.vue

mounted() {
  this.$nextTick(() => {
      if (Vue.prototype.$dialogEle) {
        console.log(Vue.prototype.$dialogEle) // undefined
       return
      }
      let ele = document.createElement("div");
      ele.innerHTML = "body中插入position为fixed元素";
      ele.style.cssText =
          "position: fixed; z-index: 100; top: 0; left: 0; width: 400px; text-align: center; height: 100px; line-height: 100px; background-color: #41b883";
      document.body.appendChild(ele);
      Vue.prototype.$dialogEle = ele
    })
}

@yiludege
Copy link
Collaborator

yiludege commented Aug 13, 2022

@Sapphire2k 这个不是 bug,只能说使用微前端需要理解这个心智模型,用乾坤或者 micro-app 同样有这个问题

首先要理解的问题:为什么使用微前端子应用切换起来会比较快?

  • 子应用的加载分为:1、静态资源的请求下载,2、资源的运行,包括html的解析、js的解析执行,3、子应用的实例mount,详见

  • 对于做了生命周期适配的子应用,1、2 步只会执行一次,每次子应用的切换都是创建和销毁 Vue()实例,这个成本非常低,但是每次都要使用缓存在内存中的 2 (Vue 这样的构造函数在 2 中执行后就一直在内存中)。

  • 每次渲染 new Vue() 实例如果往 Vue 原型上放东西势必会污染到下一次的渲染,按道理子应用销毁,这个挂载上去 Vue.prototype.$dialogEle 副作用框架也需要清理掉

  • 但是框架无法知道到底会污染到什么地方,任何 import 的包都有可能挂载副作用,清理成本太高了(其他微前端就清理一个 window 都已经做了很多工作了),所以用户需要自行清理,就你这个为例:

// 清理副作用
  beforeDestroy() {
    Vue.prototype.$dialogEle = null;
  },
  • 还有没有其他办法?可以采用保活模式(子应用实例一直不销毁)、重建模式(每次切换子应用会重新执行2、3步)就不会存在这个问题了

@yiludege yiludege pinned this issue Aug 13, 2022
@Sapphire2k
Copy link
Author

@yiludege 明白的,但是原有项目里面的很多插件可能都得经过清理再重新挂载了,相对于一开始理解低成本接入子应用,这块成本比较高,最好在文档有所说明。

保活模式确实能够解决这个问题。

@yiludege yiludege unpinned this issue Aug 22, 2022
@ChanHaiWei
Copy link

plugin cssLoader?: (code: string, url: string) => string;

似乎并没有返回当前子应用的url噢 css-loader undefined @font-face {

请问你是怎么解决的,我尝试使用cssLoader,但是没有什么思路,cssLoader的参数都没什么用

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants