微前端系列之:
一、记一次微前端技术选型
二、清晰简略易懂的qiankun主流程剖析
三、记一次qiankun落地遇到的问题
本文是系列之三。
我的项目背景
- app下架须要把所有页面都迁徙到企业微信h5,作为主利用。
- 原本内嵌到app webview的h5,以微利用的形式接入到主利用。
- 主利用技术选型:vite + vue3 + qiankun
- 子利用,有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。但会遇到以下问题:
- 分包无奈加载,排查之后,发现是publicPath失落了。
- 在尝试批改配置时,搞不清楚主利用注册子利用入口、webpack配置的output.publicPath、__webpack_public_path__、window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__、router.base的关系。
通过查资料梳理出了以下关系:
主利用注册微信利用入口,是入口html的地址。在这个微利用中入口如下:
- 本地开发:http://local.some.domain:3000...
- 测试:http://test.some.domain/marke...
- 预生产:https://preprod.some.domain
- 生产:https://prod.some.domain
- webpack.output.publicPath,决定了输入动态资源申请url前缀,如代码写了'/static/1.js',配置了output.publicPath = '/marketapp/',那么打包进去的后果是 '/marketapp/static/1.js'
- __webpack_public_path__是运行时的webpack.output.publicPath,能够动静批改其值,会笼罩webpack.output.publicPath的值
- 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,是不失效的。要解决这样问题,我想到两种思路:
- 主利用通过props提供主利用的toast、popup等办法给微利用调用。能够肯定水平低解决这种状况,且款式对立;然而子利用批改起来很麻烦,而且有可能是dialog外面再套一些antd-mobile的组件,这种状况就很难兼容。
主利用在加载子利用时,进行款式隔离,针对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,才能够胜利跳转到路由。
后记
先记这么多,后续再出问题会持续补充到这里。