不知道有沒有同窗跟我同樣,學習了不少react
源碼,卻仍是寫不出更優雅的代碼。咱們知道了dom-diff
原理,瞭解setState
是如何更新狀態的,而後呢?仍是會寫出難以維護的代碼甚至bug一堆。
html
不少時候你寫出bug不是由於你不懂dom-diff
,而是由於你的屬性和狀態胡亂命名。
react
不少時候你的代碼難以維護,也不是由於你不懂dom-diff
,而是你的組件劃分太不合理了。
git
因此我開始讀一些應用層框架的源碼,也但願把學習的過程和你們分享。github
antd-design-mobile
是一個不錯的學習項目,能夠學習到如何更好的使用React
、TypeScript
、less
,也能夠學到優秀的團隊是如何思考可維護性、可擴展性和組件化的。
segmentfault
antd-design-mobile
項目集成了一些編譯工具,而且把各個組件分散在不通的項目中,看起來比較累。因此我建立了一個相對簡單且更適合閱讀的項目(不含動畫且從新設計了Ui),看原項目吃力的同窗能夠看這個:
安全
由於沒有綁定域名,微信打開會有問題,可使用支付寶等其餘應用antd
先看下Modal
組件總體的結構:
app
接下來詳細看一下各個組件內部的細節:
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 }
}
複製代碼
這個組件被放在了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;
}
複製代碼
這個組件主要作了三件事,一、渲染遮罩 二、渲染彈框 三、處理關閉事件 這裏渲染了彈出框的元素,代碼以下:
...
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
前綴的方式避免了全局樣式污染的問題。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軸縮放三分之一
}
}
}
}
複製代碼
有一段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…