背景
AngularJS已经是10年前的产物了,官方早已不在维护,而且生态薄弱,组件库极少,这给产品的迭代带来极大的开发成本,交互体验上也落后一个时代。
得益于Web Components带来的custom element和shadow DOM能力,使得在angularjs页面上开发react+antd页面成为了可能,无需整个项目重构,渐进式升级。
改造方案
整体结构
由于整个项目历史悠久,业务复杂,代码量相当庞大,不可能完全推翻angularjs,重构一套,所以主体依然是angularjs,而新的或者需要修改的页面可以用React开发。
angularjs的MVC模式依然保留,但是view层没有任何逻辑,只负责引入对应页面的web component,其中嵌入了用react开发的实际页面。
框架选择
上述的嵌套结构必然会涉及框架间通信以及生命周期的问题,使用原生的web component实现过于繁琐,另外考虑到兼容性问题,选择了Lit2来作为web component层的框架,业务部分遵循团队的规范使用React+Antd4。
web component挂载react组件
这里的关键是创建挂载点,然后挂载组件
首先在lit的render函数中创建挂载点
render() {
return html`
<div id="root">
</div>
`;
}
然后在首次渲染完成后挂载react组件
let rootEle = this.renderRoot.querySelector('#root')
this.rootContainer = ReactDOM.createRoot(rootEle);
this.rootContainer.render((
<ConfigProvider locale={zhCN}> // antd相关
<AppInner shadowRoot={rootEle} // AppInner为传入的react组件
{...props}
/>
</ConfigProvider>
));
通信
这里分为两部分,Angularjs与web component通信以及web component与react通信。
web component原生支持监听attribute方式传入的属性,但这种方式只能传入字符串,另外一种方式是通过DOM对象属性赋值的方式传递,支持所有数据类型,因此在Angularjs的控制器中只需要拿到相应的dom对象,然后传入控制器中的数据即可,这里可以抽象为一个通用方法。
通过web component的名字获取到dom对象,然后将需要传入的数据赋值到dom对象上,同时在控制器中需要监听对应数据的变化,及时更新
let e = document.querySelector('custom element name')
for (let key in attributes) {
if(attributes.hasOwnProperty(key)) {
if(typeof attributes[key] === 'string') {
e[key] = this[attributes[key]]
this.$scope.$watch(() => this[attributes[key]], (newValue, oldValue) => {
if(newValue === oldValue) {
return
}
e[key] = newValue
})
}
if (typeof attributes[key] === 'function') {
e[key] = attributes[key]
}
}
}
在web component这一层要做的就是监听属性变化,然后重新渲染react组件。
这一点可以通过lit框架提供的updated的生命周期钩子实现。
let props = {}
for(let key in properties) {
props[key] = this[key]
}
// 更新react组件
let rootEle = this.renderRoot.querySelector('#root')
this.rootContainer.render((
<ConfigProvider locale={zhCN}>
<AppInner shadowRoot={rootEle}
{...props}
/>
</ConfigProvider>
));
此外还有web component的disconnectedCallback事件需要处理,同时卸载react组件,避免定时器等无法清理。
disconnectedCallback() {
this.rootContainer.unmount()
super.disconnectedCallback()
}
antd样式
这里有两种方案,link标签引入和style标签引入。
前者的优点是打包后体积小,缺点是需要等待样式加载完成,否则会出现样式延后生效的问题,后者通过webpack的raw-loader将antd的style文件内容引入,不会出现样式延后生效的问题,缺点是打包体积会变大
antd的root container修正
默认情况下,antd中的弹层相关的组件都会使用document.body作为根,在web component使用时,会出现位置错乱并且无样式的问题,因此需要修正。
弹层相关组件都提供了getContainer方法,注意上面的代码中的<AppInner shadowRoot={rootEle}
,这里将shadowRoot作为props传入了react中,因此只需要在getContainer方法中返回props.shadowRoot即可修正
样式统一
原Angularjs页面使用呃是Bootstrap3,风格与antd大相径庭,采取的方案是仿造antd修改了原来的Angularjs页面的主题样式,使得整个系统风格趋近一致,避免体验割裂