前面两篇文章写了我们这个项目是怎么去拆分的,拆分的思路以及碰到的问题和拆完之后拿到的收益,我把这篇文章称为项目拆分三部曲的第三部,也可能是最后一部了,这一部主要是介绍一下从项目上线之后的一些操作
项目优化
- 按照我们之前那种方式拆出来一个子系统之后删除了额外的业务之后,项目体量就变得很轻对于后续的操作就比较方便
- 做了webpack的升级从
V4升级到了V5
也体验到了V5带来的缓存效率整体提升非常明显,冷启动速度267009ms
---->24499ms
,到Jenkins打包速度10min 32s
----->55s
,并且为后续可以使用模块联邦的能力也打下了基础 - 去除了一些插件因为项目架构比较老项目体量又太大之前在主项目不太敢去动一些配置,拆出来之后动起来就比较容易一些
- 将css从js中分离出来提高每次js加载的速度,没有测具体提高多少因为拆出来的子项目不是特别大提升应该不会很明显但是这个操作是随着项目体量越大收益越高
- 有一个潜在的好处是因为当初子项目的架构和主项目完全一致,所以在子项目可以跑通的策略在主项目大概也可以,相当于提前用子项目去试水了,事实证明后来给主项目做升级做优化很大程度是依赖子项目已经跑通了,主项目才能更顺利一些
接入监控系统
因为之前主应用早就接过了Sentry做监控,所以想把子应用也给接一下,至于为啥没有在上线之前就把这个给接好有下面几点原因
- 因为做这些操作包括之前那些操作其实都是没有排期时间的,这也就意味着只能压缩一下正常写业务时间来做这件事
- 需求一直在不断地迭代中,在这个期间需要保证主系统原本的业务可以存在也要保证子系统的业务和主项目是一致的方便后面切子项目上线,保证代码一直是同步的也不是一个容易的事情
- 所以在子项目准备就绪之后也就是在灰度跑了一到两个月就尽快上线了,因为像Sentry或者是我上面说的一些优化本身是不影响原本业务执行的所以也就没有必要在上线之前完全弄好,毕竟时间这个东西不是一直有,而且步子跨大了确实容易扯到蛋
接入Sentry系统也不是特别顺利接入流程就不提了网上教程一堆,主要记录一下遇到的问题吧!!!
- 第一个问题是接入之后发现上报错误异常,从network看不到发起的监控请求,所以Sentry平台上也看不到抓到的异常,下面是我的一些配置
js
function render(props) {
const { container, Sentry } = props
if (process.env.NODE_ENV === 'test' && Sentry) {
// webapp-fat sentry project
Sentry.init({
dsn: 'https://xxxxxx/api/sentry/12',
tracesSampleRate: 0.2,
sampleRate: 0.1,
autoSessionTracking: false,
release: sentry_release,
})
} else if (process.env.NODE_ENV === 'production' && Sentry) {
// webapp-prod sentry project
Sentry.init({
dsn: 'https://xxxxxx/api/sentry/17',
sampleRate: 1,
tracesSampleRate: 0.2,
autoSessionTracking: false,
release: sentry_release,
})
}
ReactDOM.render(
<React.Suspense fallback={null}>
<RoutesComp {...props} />
</React.Suspense>,
container
? container.querySelector('#pay_public_container')
: document.querySelector('#pay_public_container')
)
return ReactDOM
}
先说一下产生这个问题的原因,是因为子应用单独引入了一个Sentry,然后上报的时候使用的是子应用的Sentry上报 的,这样的话就导致在当前环境下有两个Sentry实例一个是主应用的一个是子应用的,导致上传失败,那解法也很简单使用主应用传递下来的Sentry上报,不要使用子应用自己的Sentry,就可以正常看到上报的异常了,可以看上面的代码片段后面的 && Sentry
判断逻辑,这个就是从主应用传递下来的Sentry
- 第二个问题也很奇葩,就是怎么说呢这一路太不容易了,谁走谁知道,现象是在Sentry平台可以看到异常,但是看不到源码映射,这就好比博尔特呢让他跑步不给他鞋穿,东西虽好但是不能完全利用起来,这种感觉有点难受😭
- 使用
@sentry/webpack-plugin
做sourceMap
上传,这个也是上传源代码的基本操作(乘载源代码的池子怎么建立就不提了,感兴趣的同学可以自己搜一下都是常规操作比较简单)
js
const SentryWebpackPlugin = require("@sentry/webpack-plugin");
mode: 'production',
devtool: 'source-map',
plugins:[
new SentryWebpackPlugin({
url:"http://sentry.xxxxxxx.com",
org: "xxxxxx",
project,
include: "./app/bundles",
authToken: "xxxxxx",
ignore:["node_modules"],
configFile: "./.sentryclirc",
release: sentry_release,
urlPrefix: "~/publicpay/bundles/"
// Optionally uncomment the line below to override automatic release name detection
// release: process.env.RELEASE,
})
]
- 用过Sentry的同学都知道源码映射对于我们排查错误有非常大的帮助,然后又又又又开始了新一轮的问题排查了,下面是一些当时的报错截图
从报错来看老实说当时的我没有看出什么原因,可能也是因为之前没有接入过Sentry平台,当然这肯定是需要一定经验的,所以找了很多原因有说release匹配不上的有说路径匹配不对的种种原因把,但是很可惜到最后都不是
原因是因为qiankun对子应用开启了沙箱环境,了解过源码映射原理的可能都比较清楚,在打包完的代码后面会有一个sourceMapUrl这个对应的地址是源码的地址,当qiankun给她包裹上之后导致源码映射不到了,所以Sentry上就看不到映射的源码了,解法也比较简单
js
registerMicroApps([
{
name: 'PayPublic',
entry: BASE_URL.includes('localhost')
? '//localhost:8001/'
: BASE_URL + '/publicpay',
container: '#webapp-toPublicHome',
activeRule: location => {
const path = location.pathname.includes('/payPublic/')
// const path = location.pathname.startsWith(payPublicPath)
return path
},
props: { ...ShareToPayPublic },
},
])
start({ sandbox: false })
start({ sandbox: false })
可以从这个位置关闭qiankun沙箱,不要问我关闭有没有问题那得看你的环境有没有需要挂在系统全局的属性,如果没有我觉得是无所吊谓的!!!到这里子应用算是接入完了Sentry了,后面也可以正常抓到错误了
更改组件传递方式
目前传递组件的方式使用的是qiankun本身提供的props的方式,这种方式有些问题!!!
- q1 需要在主应用的index.js将传递的组件引入一遍,这就导致首次加载的资源包变大了
- q2 在子应用需要通过一些方式去注册到本地可能本地放个对象也好或者放在window上
- q3 上面这种存储方式就会导致在子应用使用的时候不是特别的方便而且不好管理因为他不能像正常组件那样去引入
所以要改变这种传递组件的方式,可以采用模块联邦的方式,当然他需要一些学习成本,还有如果当前你不是在webpack5的环境你还需要做个升级,但是从长远的来看肯定是利大于弊的,虽然你可能要花时间去学习新知识,要去踩坑但是最后对自己的成长是蛮大的,当然短期的方案可以按照我们现在这样也无可厚非
我们在主应用发放组件作为提供方,然后子应用作为消费方使用主应用发放的组件,官方文档说了,这个没有强制谁来做提供方谁来做消费方,甚至可以互相成为提供方或者消费方也是可以的,看你具体情况
提供方的使用
js
plugins: [
new ModuleFederationPlugin({
name: 'main',
filename: 'remoteEntry.js',
exposes: {
'./Test': path.resolve(__dirname, './app/scripts/test'),
},
//需要共享的依赖
shared: [
{
react: {
singleton: true,
eager: true,
requiredVersion: deps.react
},
"react-dom": {
singleton: true,
eager: true,
requiredVersion: deps["react-dom"]
},
}
]
}),
]
消费方的使用
js
new ModuleFederationPlugin({
name: "payPublic",
remotes: {
main: "main@http://localhost:8000/bundles/remoteEntry.js",
},
shared: [
{
react: {
singleton: true,
eager: true,
requiredVersion: deps.react
},
"react-dom": {
singleton: true,
eager: true,
requiredVersion: deps["react-dom"]
},
}
]
}),
]
上面在模块联邦中用到的属性我说一下我自己的理解,可能不一定对,有问题的可以在评论区说,或者大家感兴趣可以直接去看官方解释也可以
name
当前应用的名字应该是唯一的不然来回共享可能共享乱了filename
产出的路径后缀名,这个会最后拼在你的域名上exposes
作为提供方输出资源的配置,在消费方应用里面使用一般是这样的路径main/Test
,下面有完整的引入shared
这个属性还是挺重要的,他可以决定当前输出的组件使用谁的依赖,有几种情况 第一种要是不配这个的话,组件会按照当前环境的依赖去执行,如果配置了在看里面的属性singleton
设置为true保证只实例化一次,在各个应用之间,正常是要打开的除非你有特殊场景eager
如果设置为true你需要在index.js里面将资源引入改成dynamic import
,因为这个设置把chunk改成异步加载了,这样设置也是模块联邦推崇的行为
js
//这个bootstrap里面的内容是你原来index.js里面的内容
import('./bootstrap')
这种方式也有些好处他可以让你使用提供方组件的时候看起来和引用自己项目内的组件是一样的行为看下面
eager: false 如果设置为fasle,需要这样引入,其实看起来还好
jsx
import React from 'react'
const RemoteTest = React.lazy(() => import('main/Test'))
const SupplierHome = () => {
return (
<div className={Style.supplier_home_container}>
{/* 使用组件 */}
<RemoteTest />
</div>
)
}
eager: true 如果设置为true,可以这样引入了,是不是看起来就比较舒服了,当然这样引入对性能也是最好的
js
import React from 'react'
import RemoteTest from 'main/Test'
const SupplierHome = () => {
return (
<div className={Style.supplier_home_container}>
{/* 使用组件 */}
<RemoteTest />
</div>
)
}
属性和使用配置都说完了,那再来说说遇到的问题,没有问题的升级和改版是不完整的!!!
记录问题
先描述一下背景我们使用模块联邦传递普通的hooks组件没有任何问题,但是我们在传递hooks组件的时候再主应用给包裹了一层observer就像下面这样,这是组件传输使用背景
js
import React from 'react'
import { observer } from 'mobx-react'
import Store from './store'
function Test() {
return (
<div>
6666跑通了老铁辛苦了
<span>{Store.count}</span>
<button onClick={Store.changeCount}>点击一下</button>
</div>
)
}
//这里面嵌套了observer
export default observer(Test)
在没包上observer
这一层的时候都没问题,包上之后就开始出现异常了,看下面图
1、如果有看过我之前拆分项目文章的同学对这个错看起来应该很熟悉,他的意思是当前环境中存在两个react实例,然后会产生一个错误React和渲染器的版本可能不匹配
2、在回顾一下上一版做拆分的时候碰到这个错误,错误产生原因是通过props传递了hooks组件到子应用中使用然后产生了和和上面类似的错误
3、当时的解法是使用webpack提供的externals
在打包的时候将react依赖从子应用中排除掉,之后在主应用初始化的时候将react和react-dom挂载到window
4、大家都清楚在应用中通过impot * from *
这种方式做引入的时候,首先会找当前内存中的依赖如果找不到会找window上的依赖,那基于这种方式解决了上述的错误
ok 到这里既然我们已经用了externals排除了react和react-dom为什么还会用相同的问题出现呢?
先说原因是因为在子应用中我们虽然使用externals排除了react和react-dom,但是我们传递的hooks组件也是使用mobx-react提供的api进行包裹的也就是说他也是需要依赖react的,但是到子应用中因为依赖被排除掉了所以找不到依赖了,报了上面这个异常
这是我对这个错误的理解,不一定是对的大家有其他想法可以评论区指出来
解法就很简单去掉之前说的externals
这一段,遵循模块联邦的方式正确使用shrad共享组件就可以解决这个问题了
js
// if (process.env.NODE_ENV !== 'startNoReact') {
// WebpackCfg.externals = {
// 'react': 'React',
// 'react-dom': 'ReactDOM',
// }
// }
要讲的到这就差不多了~~~~
捋一下项目拆分的几个阶段
有一些很细节没有写,从开始到现在大概整体流程是这样子~~~~
总结一下
从去年到今年做这个项目拆分,其实刚开始没想到后面会有这么多操作,但是很多时候往往都是前期做完了计划,后面可能做着做着就觉得有更好的方案或者更好的想法,当然到了今天这个阶段虽然已经上线去跑了很久了,但是也不一定是一个最合适的方案,只是说它比较符合我们当下那个情况做出的选择,很多时候在公司每个阶段,其实可能都有不同的规则方案,反正觉得适合的方案那就是最好的
end have a nice day ☕️