乐趣区

关于qiankun:记一次qiankun落地遇到的问题

微前端系列之:
一、记一次微前端技术选型
二、清晰简略易懂的 qiankun 主流程剖析
三、记一次 qiankun 落地遇到的问题

本文是系列之三。

我的项目背景

  1. app 下架须要把所有页面都迁徙到企业微信 h5,作为主利用。
  2. 原本内嵌到 app webview 的 h5,以微利用的形式接入到主利用。
  3. 主利用技术选型:vite + vue3 + qiankun
  4. 子利用,有 vue、react

__webpack_public_path__相干

微利用 market-app-h5 背景常识,publicPath 配置如下:
本地开发 /marketapp
测试 /marketapp
预生产 /
生产:https://some.cdn.com/marketapp

微利用接入时,须要写这段代码,这是官网提供的 demo 代码。

if (window.__POWERED_BY_QIANKUN__) {__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;}

只管能够胜利加载入口 html,以及写在 html 文档的近程 script 和近程 style。但会遇到以下问题:

  1. 分包无奈加载,排查之后,发现是 publicPath 失落了。
  2. 在尝试批改配置时,搞不清楚主利用注册子利用入口、webpack 配置的 output.publicPath、__webpack_public_path__、window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__、router.base 的关系。

通过查资料梳理出了以下关系:

  1. 主利用注册微信利用入口,是入口 html 的地址。在这个微利用中入口如下:

    • 本地开发:http://local.some.domain:3000…
    • 测试:http://test.some.domain/marke…
    • 预生产:https://preprod.some.domain
    • 生产:https://prod.some.domain
  2. webpack.output.publicPath,决定了输入动态资源申请 url 前缀,如代码写了 ’/static/1.js’,配置了 output.publicPath = ‘/marketapp/’,那么打包进去的后果是 ‘/marketapp/static/1.js’
  3. __webpack_public_path__是运行时的 webpack.output.publicPath,能够动静批改其值,会笼罩 webpack.output.publicPath 的值
  4. window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__,是 qiankun 提供的。依据源码剖析,它会拿到微利用 html 入口 url 之后,将 pathname 的最初一项去掉,再组装起来。譬如,子利用入口配置:http://local.soame.domain/mar…,那么通过解决后,会变成 http://local.soame.domain/。所以在加载分包的时候,失落了前缀,而后导致 404。

所以,针对这种问题,还要辨别 publicPath 是否 cdn 地址,所以最初代码改成这样,就能够了

let STATIC_URL = '';
switch (process.env.NODE_ENV) {
case 'development':
    STATIC_URL = '/marketapp/';
    break;
case 'test':
    STATIC_URL = '/marketapp/';
    break;
case 'preprod':
    STATIC_URL = '/';
    break;
case 'production':
    STATIC_URL = '//some.cdn.com/marketapp/';
    break;
}

if (window.__POWERED_BY_QIANKUN__) {
    // 如果以后环境下 webpack 配置的 out.publicPath 的值是 cdn 地址,那么间接应用
    if (/(https?:)?\/\/.*$/.test(STATIC_URL)) {__webpack_public_path__ = STATIC_URL;} else {
    // 如果以后环境下 webpack 配置的 out.publicPath 的值不是 cdn 地址,如 / /marketapp /a/b/c
    // 须要对 window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__进行重新处理
        __webpack_public_path__ = new URL(window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__).origin + STATIC_URL;
    }
}

款式隔离相干

主利用的 UI 库是 vant3.x
微利用 marketapp 的 UI 库是 vant2.x
微利用 priceluck 的 UI 库是 antd-mobile

发现问题:微利用 priceluck,在独立运行时,是能够应用 toast 组件的,但 qiankun 环境下运行,toast 组件款式失落了,间接 append 在 body 底部。

通过排查,是 qiankun 的款式隔离导致的,无论应用 shadow dom 计划,还是 scoped 计划,都无奈解决这种状况。以 scoped 计划为例,原本是 .test{width: 100%;},scoped 之后变成div[data-qiankun=priceluck] .test{width: 100%:}。这种计划,益处是能够把业务款式限度在微利用容器中。然而通用组件款式,如挂载到 body 下的组件,如 toast、popup,是不失效的。要解决这样问题,我想到两种思路:

  1. 主利用通过 props 提供主利用的 toast、popup 等办法给微利用调用。能够肯定水平低解决这种状况,且款式对立;然而子利用批改起来很麻烦,而且有可能是 dialog 外面再套一些 antd-mobile 的组件,这种状况就很难兼容。
  2. 主利用在加载子利用时,进行款式隔离,针对 UI 组件的款式不要做隔离。间接在全局失效。一方面,UI 组件库的命名形式都是以 BEM 格调明明,antd-mobile 是以 .am-结尾的;vant 是以 .van- 结尾的;另一方面,咱们这个我的项目是单例模式,且后续也不思考多例模式。然而,主利用和微利用 marketapp 都是用 vant 的,款式会抵触。主须要对主利用的款式也做 scoped 解决就能够了。

    所以就采纳计划二。须要通过 vite 插件来实现改 qiankun 源码、主利用 vant 源码。

通过断点得悉 qiankun 是在 src/sandbox/patchers/css.ts 进行 scoped 款式隔离解决。

private ruleStyle(rule: CSSStyleRule, prefix: string) {const rootSelectorRE = /((?:[^\w\-.#]|^)(body|html|:root))/gm;
    const rootCombinationRE = /(html[^\w{[]+)/gm;

    const selector = rule.selectorText.trim();

    let {cssText} = rule;
    // handle html {...}
    // handle body {...}
    // handle :root {...}
    if (selector === 'html' || selector === 'body' || selector === ':root') {return cssText.replace(rootSelectorRE, prefix);
    }

    // handle html body {...}
    // handle html > body {...}
    if (rootCombinationRE.test(rule.selectorText)) {const siblingSelectorRE = /(html[^\w{]+)(\+|~)/gm;

      // since html + body is a non-standard rule for html
      // transformer will ignore it
      if (!siblingSelectorRE.test(rule.selectorText)) {cssText = cssText.replace(rootCombinationRE, '');
      }
    }

    // handle grouping selector, a,span,p,div {...}
    cssText = cssText.replace(/^[\s\S]+{/, (selectors) =>
      selectors.replace(/(^|,\n?)([^,]+)/g, (item, p, s) => {// handle div,body,span { ...}
        if (rootSelectorRE.test(item)) {return item.replace(rootSelectorRE, (m) => {// do not discard valid previous character, such as body,html or *:not(:root)
            const whitePrevChars = [',', '('];

            if (m && whitePrevChars.includes(m[0])) {return `${m[0]}${prefix}`;
            }

            // replace root selector with prefix
            return prefix;
          });
        }

        return `${p}${prefix} ${s.replace(/^ */, '')}`;
      }),
    );

    return cssText;
  }

重点在

return `${p}${prefix} ${s.replace(/^ */, '')}`;

在这里做款式名判断,如果是有 .van- 或者 .am-,则不加作用域前缀。否则就依照原来的逻辑加前缀。所以要改成:

let matchUILib = s.includes('.am-') || s.includes('.van-');
return matchUILib ? s.replace(/^ */, '') :"".concat(p).concat(prefix, "").concat(s.replace(/^ */,''));

而主利用 UI 库款式隔离,能够参考 vue 的 scoped 的原理,其实就是对元素增加一个自定义属性如data-v-mainapp;css 的款式名,加一个 [data-v-mainapp] 属性后缀,来作为作用域。

写成 vite 插件,是这样的:

import fs from 'fs';
import path from 'path';
export default function microAppsStyleHack() {
  return {
    name: 'vite-plugin-micro-apps-style-hack',
    transform(code, id) {
    // qiankun 款式隔离非凡解决
      if (id.includes('node_modules/qiankun/es/sandbox/patchers/css.js')) {
        code = code.replace(`return "".concat(p).concat(prefix," ").concat(s.replace(/^ */,''));`, 
            `let matchUILib = s.includes('.am-') || s.includes('.van-'); return matchUILib ? s.replace(/^ */, '') :"".concat(p).concat(prefix, "").concat(s.replace(/^ */,''));`);
      }
    // 主利用 vant 款式 scoped
      if (id.includes('vant')) {
       // js 文件,用 vue 的 createVNode 办法创立虚构节点的,因为每个 ui 组件都会有 class 属性,这里取巧,间接在 class 属性后面加一个自定义的属性
        if (id.includes('.js')) {
          code = code.replace(/((?:'|")?\bclass\b(?:'|")?:)/gm,
            `"data-v-mainapp":"",$1`
          );
        }
        // css 文件,间接对款式名做解决,能够点击 regex101 那两个 url,看看思考了哪几种状况
        if (id.includes('.css')) {
          // https://regex101.com/r/SAm0zC/1
          // https://www.w3school.com.cn/cssref/css_selectors.asp
          code = code.replace(/(\.van.*?)(\s|,|{|>|\+|~|:)/gm,
            `$1[data-v-mainapp]$2`
          );
          // https://regex101.com/r/ZRM6D4/1
          code = code.replace(/([a-zA-Z])(\.van)/gm, `$1[data-v-mainapp]$2`);
        }
      }
      return {
        code,
        map: null
      };
    }
  };
}

这里还有一个小插曲,vite 在开发环境没有走插件的逻辑,查资料得悉,是 vite依赖预构建 导致的,这个性能的益处是能够秒开。如果想走 transform 的逻辑,那么须要在 vite 配置中须要配置 optimizeDeps: {exclude: ['vant'] }。这里如果配置了 qiankun 会报对于 loadash 的错,所以我间接写了个 node 脚本来替换了。此处就不展现了。

主利用注册子利用相干

咱们这次是分批次接入微前端的,因为有些业务部门须要排期先做他们本人的业务。每接入一个微利用,主利用就要增加一个配置,跟着公布。主利用很被动。所以就让后端出一个接口,把微利用配置放到数据库上,主利用每次都拉一次配置就能够了。

vconsole 以及 fastclick

微利用原本接入了 vconsole、fastclick,在 qiankun 下会报错,说 window 对象不是原生的。这个很容易猜到就是因为他们拿到的全局对象是代理的对象。

且 vconsole 主利用也有接入。在微利用中判断如果是开发环境且不在 qiankun 环境下,才开启;

fastclick 间接去掉,官网曾经在 chrome32 的时候,以及 ios9.x 的时候曾经解决 300ms 提早问题。(历史的洪流,啊~~)

history 路由模式相干

之前比拟少配置 history 路由模式相干的配置。在这里记录一下。
对于 devServer 的配置

// 跨域设置
headers: {'Access-Control-Allow-Origin': '*'},
// 通过前缀来拜访
historyApiFallback: {
    rewrites: [
       {
           from: /^\/marketapp.*/,
           to: path.posix.join(config.dev.assetsPublicPath, 'app.html')
       }
   ]
},
// 通过 host 配置的域名拜访
disableHostCheck: true

对于 nginx 的配置

location ^~ /xxx {
    alias /data/XXX;
    index index.html;
    try_files $uri $uri/ @xxxredirect;
}
# 解决 history 模式刷新 404 问题
location @xxxredirect {rewrite ^.*$ /xxx/index.html last;}

以及留神下 router.base 的关系。new VueRouter({base: ‘/marketapp’}),这里的 base 的作用就是路由门路前缀,譬如,路由门路是 /path/to/view,配置了 base 之后,须要 /marketapp/path/to/view,才能够胜利跳转到路由。

后记

先记这么多,后续再出问题会持续补充到这里。

退出移动版