likes
comments
collection
share

乾坤切换应用样式丢失-动态修复

作者站长头像
站长
· 阅读数 9

!注:样式丢失只存在于按需加载的情况,把所有 import() 改成import from 也是可以解决问题的,但是这样包会很大,影响首屏加载

qiankun在子应用之间切换(或者多次切换),如果子应用的路由是按需加载的,很大概率动态插入的 link[rel="stylesheet"] 缺少,会导致子应用部分样式丢失

经过分析,使用 webpack 打包的项目

  • mini-css-extract-plugin 输出单独的CSS

  • webpackPrefetch 属性(vue-cli4 默认开启),看下 index.html<head> 会插入标签如下 <link href="{cdn}/{chunkName}.{hash}.css" rel="prefetch"> 用来预下载资源

正常情况下(这里单指 css 资源

  • 一个资源至少有两个 link,其中 link[rel="prefetch"] 用来预下载资源;link[rel="stylesheet"] 用来加载样式;
  • 相同的样式可能会有多个 chunk 合并成一个文件;
  • 包含同一个 chunkNamelink 一个是 link[rel="prefetch"],另一个是 link[rel="stylesheet"] 才是正常的;
  • 当样式丢失的时候,至少有一个 link[rel="prefetch"] 不存在对应的 link[rel="stylesheet"]

这里就可以利用这个 link[rel="prefetch"] 来实现样式重载

前置准备

按需加载调整

  • 配置 webpackChunkName

    import(/* webpackChunkName: "404" */ "@/components/404.vue");
    

    这样打包后输出的文件如下: 404.75102c64.css

  • tsconfig.json

    检查下 compilerOptions,确保 removeComments 不要启用

    webpackChunkName 尽量精简命名,打包输出的内容可能会包含多个 chunk,此时文件名也是由多个 chunk 使用~拼接的,会有点长,可以通过 config.optimization.splitChunks.minSize = 100 * 1024; 配置 chunk 最小文件大小

路由对应 chunkName

  • links HTMLElement[]: 当前 chunkNamehead 下的所有 link 标签
  • paths string[]: 路由 path 的部分路径(或者完整路径),足够准确即可

路由配置:

  {
    path: '/aaa/create/test1',
    name: 'test1',
    component: () => import(/* webpackChunkName: "test" */ '@/components/test1.vue')
  },
  {
    path: '/aaa/create/test2',
    name: 'test2',
    component: () => import(/* webpackChunkName: "test" */ '@/components/test2.vue')
  },

缓存配置:

const chunkNameCacheMap: {
  [chunkName: string]: { links: HTMLElement[]; paths: string[] };
} = {
  test: {
    links: [],
    paths: ["/create/test1", "/create/test2"],
  },
};

动态修复

在合适的位置执行 fixDynamicCSS(router)

关键步骤

  • webpackChunkName 要跟路由对应,确保唯一

  • 监听路由变化,通过模糊但准确的当前 path 信息匹配到 chunkName

  • querySelectorAll 对应 chunkName 的所有 link css 标签

    这里利用 querySelector 原生支持的CSS选择器,匹配到 *.css 同时包含 chunkName 查找所有元素

    chunkItem.links = Array.from(
      document.head.querySelectorAll(`link[href*=".css"][href*="${chunkName}"]`)
    );
    
  • 遍历找到的所有 links,如果找不到当前 link[rel="prefetch"] 相同的 href 对应的 link[rel="stylesheet"],则设置为 rel="stylesheet",这样就会被浏览器作为样式加载了

修复方法

import { Router } from "vue-router";

const isProduction = process.env.NODE_ENV === "production";

/** 这里注意与router下按需加载的路由有关,webpackChunkName要跟路由对应,确保唯一 */
const chunkNameCacheMap: {
  [chunkName: string]: { links: HTMLElement[]; paths: string[] };
} = {
  test: { links: [], paths: ["/aaa/create/test1"] },
};

/**
 * 按需加载时样式丢失检测与修复
 */
export function fixDynamicCSS(router: Router) {
  if (!isProduction) return;
  router.afterEach(({ path }) => {
    const chunkNameCacheItem = Object.entries(chunkNameCacheMap).filter(
      ([_, item]) => item.paths.some((i) => path.includes(i))
    );
    if (!chunkNameCacheItem?.length) return;
    chunkNameCacheItem.forEach(([chunkName, chunkItem]) => {
      // 打包后的文件路径如 index.7e74b2d9.css
      if (!chunkItem.links?.length) {
        chunkItem.links = Array.from(
          document.head.querySelectorAll(
            `link[href*=".css"][href*="${chunkName}"]`
          )
        );
      }
      chunkItem.links.forEach((item) => {
        if (item.getAttribute("rel") === "prefetch") {
          const hasOneStyleSheet = chunkItem.links.find(
            (i) =>
              i.getAttribute("href") === item.getAttribute("href") &&
              i.getAttribute("rel") === "stylesheet"
          );
          if (!hasOneStyleSheet) item.setAttribute("rel", "stylesheet");
        }
      });
    });
  });
}
转载自:https://juejin.cn/post/7237441562664730681
评论
请登录