前言
不知道有没有同学跟我一样,学习了很多react
源码,却还是写不出更优雅的代码。我们知道了dom-diff
原理,了解setState
是如何更新状态的,然后呢?还是会写出难以维护的代码甚至bug一堆。
很多时候你写出bug不是因为你不懂dom-diff
,而是因为你的属性和状态胡乱命名。
很多时候你的代码难以维护,也不是因为你不懂dom-diff
,而是你的组件划分太不合理了。
所以我开始读一些应用层框架的源码,也希望把学习的过程和大家分享。
为什么是antd-design-mobile
antd-design-mobile
是一个不错的学习项目,可以学习到如何更好的使用React
、TypeScript
、less
,也可以学到优秀的团队是如何思考可维护性、可扩展性和组件化的。
antd-design-mobile
项目集成了一些编译工具,并且把各个组件分散在不通的项目中,看起来比较累。所以我创建了一个相对简单且更适合阅读的项目(不含动画且重新设计了Ui),看原项目吃力的同学可以看这个:
项目地址
预览地址
预览二维码
因为没有绑定域名,微信打开会有问题,可以使用支付宝等其他应用
源码分析——以<Modal>为例
项目结构
入口
先看看入口文件里是什么内容 可以看到,除了这个提示按需引入的警告之外,其实就是导出了当前目录下的所有组件。下面我以Modal组件为例子继续分析。<Modal>组件解析
先看下Modal
组件整体的结构:
接下来详细看一下各个组件内部的细节:
Modal组件
Modal
组件是根组件,也就是默认导出的组件。下图从下往上看,文件导出的是Modal
组件,它继承成了ModalComponent抽象类,并传入了ModalProps泛型接口。
如果对TypeScript不太了解可以看 www.tslang.cn/docs/handbo… ,通读一下“手册指南”这一章基本就可以看懂Ts代码了
从接口定义上可以看出,这个组件允许接受prefixCls
、transitionName
等属性,并且需要挂载三个静态方法alert
、prompt
、operation
。
组件内部有cls
和一个renderFooterButton
方法,其中cls
结合prefixCls
处理了类名(prefixCls允许用户自定义前缀),以方便用户统一处理自定义的样式。而renderFooterButton
方法用来渲染弹窗中的按钮。
接下来就是alert
、prompt
、operation
三个静态方法(官网中有相应的使用方法)。他们的作用是通过方法唤出Modal
组件,以alert为例:
import { Modal } from 'antd-mobile';
const alert = Modal.alert;
const App = () => (
<Button
onClick={() =>
alert('Delete', 'Are you sure???', [
{ text: 'Cancel', onPress: () => console.log('cancel') },
{ text: 'Ok', onPress: () => console.log('ok') },
])
}
>
confirm
</Button>
)
从接口中还能看到,alert
、prompt
、operation
这三个方法都有一个函数类型的返回值close
,这个方法的作用是关闭当前Modal
。逻辑也比较简单,这个静态方法也比较简单,就是创建一个div元素然后插入到body中,再把Modal组件插入这个div
中,最后导出一个移除这个div
的close方法。核心代码如下:
export default function operation(){
...
const div = document.createElement("div");
document.body.appendChild(div);
ReactDOM.render(
<Modal
visible
operation
transparent
prefixCls={prefixCls}
onClose={close}
footer={footer}
className="d-modal-operation"
platform={platform}
wrapProps={{ onTouchStart: onWrapTouchStart }}
/>,
div
);
function close() {
ReactDOM.unmountComponentAtNode(div); //销毁指定容器内的所有React节点
if (div && div.parentNode) {
div.parentNode.removeChild(div);
}
}
return { close }
}
DialogWraaper
这个组件被放在了react-component
这个库里,这个库是一个基础组件库,antd和antd mobile这两个项目都依赖了它。
这层组件很轻,并没有处理逻辑,只是创建了一个portal,同时对react16以下的版本做了兼容,兼容写法是这样的:
...
const IS_REACT_16 = !!(ReactDOM as any).createPortal;
componentDidUpdate() {
if (!IS_REACT_16) {
this.renderDialog();
}
}
renderDialog() {
ReactDOM.unstable_renderSubtreeIntoContainer(
this,
this.getComponent(),
this.getContainer()
);
}
render() {
const { visible } = this.props;
if (IS_REACT_16 && visible ) {
return ReactDOM.createPortal(this.getComponent(), this.getContainer());
}
return null as any;
}
Dialog
这个组件主要做了三件事,1、渲染遮罩 2、渲染弹框 3、处理关闭事件 这里渲染了弹出框的元素,代码如下:
...
onMaskClick = (e: any) => {
if (e.target === e.currentTarget) {
//e.target和e.currentTarget的区别参考https://www.jianshu.com/p/1dd668ccc97a
this.close(e);
}
};
close = (e: any) => {
if (this.props.onClose) {
this.props.onClose(e);
}
};
render() {
const { props } = this;
const { prefixCls, maskClosable } = props;
return (
<div>
{this.getMaskElement()} //渲染遮罩
<div
className={`${prefixCls}-wrap ${props.wrapClassName || ""}`}
onClick={maskClosable ? this.onMaskClick : undefined}
{...props.wrapProps}
>
{this.getDialogElement()} //渲染弹框
</div>
</div>
);
}
在getMaskElement
和getDialogElement
两个方法渲染元素时,元素外层包裹了LazyRender
组件,意思很好理解,就是避免不必要的渲染。代码也很简单,就是在shouldComponentUpdate
中做是否更新的判断。
export default class LazyRender extends React.Component<lazyRenderProps, any> {
shouldComponentUpdate(nextProps: lazyRenderProps) {
return !!nextProps.visible;
}
render() {
const props: any = { ...this.props };
delete props.visible;
return <div {...props} />;
}
}
到此,这个组件已经结束了。上面只粘贴了部分代码,有表达不是很清晰的地方可以查看 github.com/alive1541/d… 相比于源码,这里的代码更加简单、清晰。
学到了什么
- 通过
react-component
库统一封装了基础组件,在这一层只处理了基本逻辑和样式。方便在antd-mobile这类上层库中通过prefixCls
统一重写样式,极大的提高了组件的复用能力。同时,如果也方便其他人使用这个库去开发自己的组件。 - 使用
TypeScript
,提高了代码的健壮性和可读性,这种代码维护起来会很轻松。 - 深度使用了
less
,通过变量、Mixin、函数等特性,对关键变量统一维护在了themes文件中的default.less中,对1px的处理封装在了hairline.less文件中。也通过prefixCls
前缀的方式避免了全局样式污染的问题。
其他小知识点
1px处理
antd-mobile
对1px的处理是通过transform
缩放来实现了,方法就是二倍屏缩小一半,三倍屏缩小到三分之一,详细可以查看我的项目中的/src/components/style/mixins/hairline.less
html:not([data-scale]) & {
@media (min-resolution: 2dppx) { //判断是2倍屏幕
border-left: none;
&::before {
width: 1px;
height: 100%;
transform-origin: 100% 50%; //设置缩放原点
transform: scaleX(0.5); //x轴缩放50%
@media (min-resolution: 3dppx) { //判断是3倍屏幕
transform: scaleX(0.33); //x轴缩放三分之一
}
}
}
}
iphoneX适配
有一段less是这样的
.@{prefixCls}-content {
padding-top: env(safe-area-inset-top);
}
这段代码的意思是设置padding-top
为iphoneX的顶部部安全区域,也就是iPhoneX的顶部刘海的高度。
除此之外还safe-area-inset-bottom
,因为手机有可能横屏使用,所以还有safe-area-inset-left
、safe-area-inset-right
,想详细了解的话可以看看这篇文章
segmentfault.com/a/119000001…
结语
以上是我学习的一些感悟和分享,希望一起学习交流。
参考链接
antd-mobile官网 mobile.ant.design/index-cn
antd-mobile仓库 github.com/ant-design/…
react-component仓库 github.com/react-compo…