├── .gitignore ├── README.md ├── SUMMARY.md ├── images ├── affix │ ├── affix_placeholder.png │ └── decorator.png ├── breadcrumb │ └── route3.png └── button │ └── button1.png ├── story ├── components │ ├── affix.md │ ├── breadcrumb.md │ ├── button.md │ ├── button_group.md │ ├── dropdown.md │ ├── form.md │ ├── grid.md │ ├── icon.md │ ├── notification.md │ ├── trigger_index.md │ └── trigger_popup.md └── index.js.md └── typeface.zip /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | !server/logs 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.DS_Store 11 | 12 | # testing 13 | **/coverage 14 | 15 | # node-waf configuration 16 | .lock-wscript 17 | 18 | # Compiled binary addons (http://nodejs.org/api/addons.html) 19 | build/Release 20 | ui/build 21 | server/build 22 | 23 | # Dependency directory 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 25 | node_modules 26 | 27 | # Bower 28 | bower_components/ 29 | dist 30 | 31 | # WebStorm文件 32 | *.idea/ 33 | 34 | # Emacs 35 | # tern(JS解析器, emacs里补全用的) 36 | .tern-port 37 | .#* 38 | *# 39 | *~ 40 | 41 | # 其他 42 | dump.rdb 43 | .DS_Store 44 | .env.local 45 | .env.development.local 46 | .env.test.local 47 | .env.production.local 48 | 49 | # 后端工程配置文件(敏感信息) 50 | server/config/default.yml 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > github: [地址](https://github.com/zhangzewei/read-antd-code) 2 | > gitbook: [地址](https://markzzw.gitbook.io/yue-du-antd-yuan-ma/) 3 | 4 | # 简介 5 | 本书主要是解读蚂蚁金服[Antd](https://ant.design/index-cn)的react框架源代码而写,为了学习其思想和代码技巧,如果有什么问题欢迎提出和添加 6 | 7 | # 须知 8 | 9 | 1. 本书主要研究其 `2.13.4` 版本的代码 10 | 2. 由于是解读源代码,所以请在观看本书的时候也将代码下载下来一同观看效果更佳 11 | 3. antd组件基本采用的是typescript语法,所以需要了解TP的语法 -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [本书简介](README.md) 4 | 5 | + Index 6 | + [Index.js](./story/index.js.md) 7 | 8 | + Icon 9 | + [Icon组件](./story/components/icon.md) 10 | 11 | + Button 12 | + [Button组件](./story/components/button.md) 13 | + [ButtonGroup组件](./story/components/button_group.md) 14 | 15 | + Layout 16 | + [Grid组件](./story/components/grid.md) 17 | 18 | + Navigation 19 | + [Affix组件](./story/components/affix.md) 20 | + [Breadcrumb组件](./story/components/breadcrumb.md) 21 | + [Dropdown组件](./story/components/dropdown.md) 22 | + [Trigger-index.js](./story/components/trigger_index.md) 23 | 24 | + Entry 25 | + [Form组件](./story/components/form.md) 26 | + [notification](./story/components/notification.md) -------------------------------------------------------------------------------- /images/affix/affix_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangzewei/read-antd-code/14e7e376417867bc75ee8ff3ade8765a00c58f7b/images/affix/affix_placeholder.png -------------------------------------------------------------------------------- /images/affix/decorator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangzewei/read-antd-code/14e7e376417867bc75ee8ff3ade8765a00c58f7b/images/affix/decorator.png -------------------------------------------------------------------------------- /images/breadcrumb/route3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangzewei/read-antd-code/14e7e376417867bc75ee8ff3ade8765a00c58f7b/images/breadcrumb/route3.png -------------------------------------------------------------------------------- /images/button/button1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangzewei/read-antd-code/14e7e376417867bc75ee8ff3ade8765a00c58f7b/images/button/button1.png -------------------------------------------------------------------------------- /story/components/affix.md: -------------------------------------------------------------------------------- 1 | # Affix 2 | 3 | 这个组件是一个图钉组件,使用的fixed布局,让组件固定在窗口的某一个位置上,并且可以在到达指定位置的时候才去固定。 4 | 5 | ## AffixProps 6 | 7 | 还是老样子,看一个组件首先我们先来看看他可以传入什么参数 8 | 9 | ```js 10 | // Affix 11 | export interface AffixProps { 12 | /** 13 | * 距离窗口顶部达到指定偏移量后触发 14 | */ 15 | offsetTop?: number; 16 | offset?: number; 17 | /** 距离窗口底部达到指定偏移量后触发 */ 18 | offsetBottom?: number; 19 | style?: React.CSSProperties; 20 | /** 固定状态改变时触发的回调函数 */ 21 | onChange?: (affixed?: boolean) => void; 22 | /** 设置 Affix 需要监听其滚动事件的元素,值为一个返回对应 DOM 元素的函数 */ 23 | target?: () => Window | HTMLElement; 24 | // class样式命名空间,可以定义自己的样式命名 25 | prefixCls?: string; 26 | } 27 | ``` 28 | 29 | ## Render() 30 | 31 | 看完传入参数之后,就到入口函数看看这里用到了什么参数 32 | 33 | ```js 34 | render() { 35 | // 构造当前组件的class样式 36 | const className = classNames({ 37 | [this.props.prefixCls || 'ant-affix']: this.state.affixStyle, 38 | }); 39 | // 这里和之前看的一样,忽略掉props中的在div标签上面不需要的一些属性 40 | // 但是貌似没有去掉offset,后面我还查了一下DIV上面能不能有offset 41 | // 但是没看见有offset,只看见offsetLeft, offsetHeight.... 42 | const props = omit(this.props, ['prefixCls', 'offsetTop', 'offsetBottom', 'target', 'onChange']); 43 | const placeholderStyle = { ...this.state.placeholderStyle, ...this.props.style }; 44 | return ( 45 | // 注意咯 看这里placeholder的作用了 如图 46 | // 这里的placeholder的作用是当这个组件样式变为fixed的时候, 47 | // 会脱离文档流,然后导致原本的dom结构变化,宽高都会有所变化 48 | // 所以这是后放一个占位元素来顶住这一组件脱离文档流的时候的影响 49 |
50 |
51 | {this.props.children} 52 |
53 |
54 | ); 55 | } 56 | ``` 57 | 58 | ![占位元素](../../images/affix/affix_placeholder.png) 59 | 60 | 接下来是重头戏,从render函数中我们应该看到了,控制当前组件的主要因素是两层div上的style这个属性,那么接下来我们就看看这两个style是如果构造的 61 | 62 | ## 从生命周期开始 63 | 64 | 这个小小的组件却有很多的代码,主要都是在处理状态的代码,乍一看下来很没有头绪,所以就想着从他们的生命周期开始深入了解,然后在生命周期中果然打开了新的世界,渐渐的理清楚了头绪,接下来我将带领大家一同来领略affix组件的风采: 65 | 66 | ```js 67 | // 这里就先将一些当前生命周期,组件做了什么吧 68 | // 首先是在Didmount的时候,这时候首先确定当前的一个固定节点是Window还是传入的DOM节点, 69 | // 然后利用setTargetEventListeners函数在这个固定节点上加上一些事件, 70 | // 然后设置一个当前组件的定时器,目的是希望在组件被销毁的时候能够将这些事件监听一并清除 71 | // 敲黑板,大家一定要注意了,自己写组件的时候如果存在什么事件监听的时候一定要在组件销毁 72 | // 的时候将其一并清除,不然会带来不必要的报错 73 | componentDidMount() { 74 | const target = this.props.target || getDefaultTarget; 75 | // Wait for parent component ref has its value 76 | this.timeout = setTimeout(() => { 77 | this.setTargetEventListeners(target); 78 | }); 79 | } 80 | 81 | // 接下来在接收到传入参数的时候,检查一下当前的固定节点是否和之前的一样, 82 | // 不一样的就重新给节点绑定事件,并且更新当前组件的位置 83 | componentWillReceiveProps(nextProps) { 84 | if (this.props.target !== nextProps.target) { 85 | this.clearEventListeners(); 86 | this.setTargetEventListeners(nextProps.target); 87 | 88 | // Mock Event object. 89 | this.updatePosition({}); 90 | } 91 | } 92 | 93 | // 在组件被销毁的时候清除左右的绑定事件 94 | componentWillUnmount() { 95 | this.clearEventListeners(); 96 | clearTimeout(this.timeout); 97 | (this.updatePosition as any).cancel(); 98 | } 99 | ``` 100 | 101 | 在这个三个生命周期中,我们看见了有这么几个函数,`setTargetEventListeners`,`clearEventListeners`,`updatePosition`, 102 | 我们就来看看他们都干了啥吧 103 | 104 | ## 三个函数 105 | 106 | ```js 107 | // 这里先放一些这些函数需要用到的一些东西 108 | function getTargetRect(target): ClientRect { 109 | return target !== window ? 110 | target.getBoundingClientRect() : 111 | { top: 0, left: 0, bottom: 0 }; 112 | } 113 | 114 | function getOffset(element: HTMLElement, target) { 115 | // 这里的getBoundingClientRect()是一个很有用的函数,获取页面元素位置 116 | /** 117 | * document.body.getBoundingClientRect() 118 | * DOMRect {x: 0, y: -675, width: 1280, height: 8704, top: -675, …} 119 | * 120 | */ 121 | const elemRect = element.getBoundingClientRect(); 122 | const targetRect = getTargetRect(target); 123 | 124 | const scrollTop = getScroll(target, true); 125 | const scrollLeft = getScroll(target, false); 126 | 127 | const docElem = window.document.body; 128 | const clientTop = docElem.clientTop || 0; 129 | const clientLeft = docElem.clientLeft || 0; 130 | 131 | return { 132 | top: elemRect.top - targetRect.top + 133 | scrollTop - clientTop, 134 | left: elemRect.left - targetRect.left + 135 | scrollLeft - clientLeft, 136 | width: elemRect.width, 137 | height: elemRect.height, 138 | }; 139 | } 140 | 141 | events = [ 142 | 'resize', 143 | 'scroll', 144 | 'touchstart', 145 | 'touchmove', 146 | 'touchend', 147 | 'pageshow', 148 | 'load', 149 | ]; 150 | 151 | eventHandlers = {}; 152 | 153 | setTargetEventListeners(getTarget) { 154 | // 得到当前固定节点 155 | const target = getTarget(); 156 | if (!target) { 157 | return; 158 | } 159 | // 将之前的事件全部清除 160 | this.clearEventListeners(); 161 | // 循环给当前固定节点绑定每一个事件 162 | this.events.forEach(eventName => { 163 | this.eventHandlers[eventName] = addEventListener(target, eventName, this.updatePosition); 164 | }); 165 | } 166 | 167 | // 将当前组件中的每一个事件移除 168 | clearEventListeners() { 169 | this.events.forEach(eventName => { 170 | const handler = this.eventHandlers[eventName]; 171 | if (handler && handler.remove) { 172 | handler.remove(); 173 | } 174 | }); 175 | } 176 | 177 | // 重点来了,划重点了,这段代码很长,但是总的来说是在计算组件和当前的固定节点之前的一个距离 178 | // 在最外层有一个有意思的东西 就是装饰器,等会我们可以单独酱酱这个装饰做了啥, 179 | // 如果对于装饰器不是很明白的同学可以去搜一下es6的装饰器语法糖和设计模式中的装饰器模式 180 | @throttleByAnimationFrameDecorator() 181 | updatePosition(e) { 182 | // 从props中获取到需要用到的参数 183 | let { offsetTop, offsetBottom, offset, target = getDefaultTarget } = this.props; 184 | const targetNode = target(); 185 | 186 | // Backwards support 187 | // 为了做到版本兼容,这里获取一下偏移量的值 188 | offsetTop = offsetTop || offset; 189 | // 获取到当前固定节点的滚动的距离 190 | //getScroll函数的第一参数是获取的滚动事件的dom元素 191 | // 第二个参数是x轴还是y轴上的滚动, y轴上的为true 192 | const scrollTop = getScroll(targetNode, true); 193 | // 找到当前组件的Dom节点 194 | const affixNode = ReactDOM.findDOMNode(this) as HTMLElement; 195 | // 获取当前组件Dom节点和当前固定节点的一个相对位置 196 | const elemOffset = getOffset(affixNode, targetNode); 197 | // 将当前的节点的宽高设置暂存,等会需要赋值给placeholder的样式 198 | const elemSize = { 199 | width: this.refs.fixedNode.offsetWidth, 200 | height: this.refs.fixedNode.offsetHeight, 201 | }; 202 | // 定义一个固定的模式,顶部还是底部 203 | const offsetMode = { 204 | top: false, 205 | bottom: false, 206 | }; 207 | // Default to `offsetTop=0`. 208 | if (typeof offsetTop !== 'number' && typeof offsetBottom !== 'number') { 209 | offsetMode.top = true; 210 | offsetTop = 0; 211 | } else { 212 | offsetMode.top = typeof offsetTop === 'number'; 213 | offsetMode.bottom = typeof offsetBottom === 'number'; 214 | } 215 | // 获取到固定节点的位置信息 216 | const targetRect = getTargetRect(targetNode); 217 | // 算出固定节点的高度 218 | const targetInnerHeight = 219 | (targetNode as Window).innerHeight || (targetNode as HTMLElement).clientHeight; 220 | // 如果滚动条的距离大于组件位置高度减去传入参数的高度,并且偏移模式为向上的时候,这时候就是固定在顶部 221 | if (scrollTop > elemOffset.top - (offsetTop as number) && offsetMode.top) { 222 | // Fixed Top 223 | const width = elemOffset.width; 224 | this.setAffixStyle(e, { 225 | position: 'fixed', 226 | top: targetRect.top + (offsetTop as number), 227 | left: targetRect.left + elemOffset.left, 228 | width, 229 | }); 230 | this.setPlaceholderStyle({ 231 | width, 232 | height: elemSize.height, 233 | }); 234 | } else if ( 235 | // 如果滚动距离小于组件位置高度减去组件高度和传入参数的高度并且偏移模式为向下的时候,为固定在底部 236 | scrollTop < elemOffset.top + elemSize.height + (offsetBottom as number) - targetInnerHeight && 237 | offsetMode.bottom 238 | ) { 239 | // Fixed Bottom 240 | const targetBottomOffet = targetNode === window ? 0 : (window.innerHeight - targetRect.bottom); 241 | const width = elemOffset.width; 242 | this.setAffixStyle(e, { 243 | position: 'fixed', 244 | bottom: targetBottomOffet + (offsetBottom as number), 245 | left: targetRect.left + elemOffset.left, 246 | width, 247 | }); 248 | this.setPlaceholderStyle({ 249 | width, 250 | height: elemOffset.height, 251 | }); 252 | } else { 253 | const { affixStyle } = this.state; 254 | // 如果上面两者都是不的时候,但是如果窗口resize了,那就重新计算,然后赋值给组件 255 | if (e.type === 'resize' && affixStyle && affixStyle.position === 'fixed' && affixNode.offsetWidth) { 256 | this.setAffixStyle(e, { ...affixStyle, width: affixNode.offsetWidth }); 257 | } else { 258 | // 如果以上情况都不是,那就样式不变 259 | this.setAffixStyle(e, null); 260 | } 261 | this.setPlaceholderStyle(null); 262 | } 263 | } 264 | ``` 265 | 266 | ## 用到的其他辅助函数 267 | 268 | 在上面这一块代码中,有几个函数是外部辅助函数,但是却是比较有意思的,因为这些辅助函数需要写的很有复用性才有作用,所以正是我们值得学的地方 269 | `getScroll()`, `throttleByAnimationFrameDecorator装饰器`,这两个东西是值得我们学习的,并且我们会一起学习装饰器模式 270 | 271 | ### getScroll() 272 | 273 | 这个函数主要是获取到传入的dom节点的滚动事件,其中需要讲解的是`window.document.documentElement` 274 | 它可以返回一个当前文档的一个根节点,详情可以查看[这里](http://www.cnblogs.com/ckmouse/archive/2012/01/30/2332070.html) 275 | ```js 276 | export default function getScroll(target, top): number { 277 | if (typeof window === 'undefined') { 278 | return 0; 279 | } 280 | // 为了兼容火狐浏览器,所以添加了这一句 281 | const prop = top ? 'pageYOffset' : 'pageXOffset'; 282 | const method = top ? 'scrollTop' : 'scrollLeft'; 283 | const isWindow = target === window; 284 | 285 | let ret = isWindow ? target[prop] : target[method]; 286 | // ie6,7,8 standard mode 287 | if (isWindow && typeof ret !== 'number') { 288 | ret = window.document.documentElement[method]; 289 | } 290 | 291 | return ret; 292 | } 293 | ``` 294 | 295 | ### throttleByAnimationFrameDecorator装饰器 296 | 297 | 首先我们需要知道装饰器的语法糖,可以查看[这里](http://es6.ruanyifeng.com/#docs/decorator) 298 | 299 | 还有[typescript装饰器](https://www.tslang.cn/docs/handbook/decorators.html) 300 | 301 | 接下来我们还需要知道为什么使用装饰器,我这里就是简单的说一下,装饰器模式主要就是为了动态的增减某一个 302 | 303 | 类的功能而存在的,详情可以查看[这里](https://segmentfault.com/a/1190000005331132) 304 | 305 | ```js 306 | // '../_util/getRequestAnimationFrame' 307 | // 由于下面的装饰器还使用了这个文件里面的函数,所以一并给搬过来了 308 | const availablePrefixs = ['moz', 'ms', 'webkit']; 309 | 310 | function requestAnimationFramePolyfill() { 311 | // 这个函数用来生成一个定时器的或者监听器ID,如果当前定时器不是window 312 | // 上面的requestAnimationFrame那就自己生成一个,用于以后清除定时器使用 313 | let lastTime = 0; 314 | return function(callback) { 315 | const currTime = new Date().getTime(); 316 | const timeToCall = Math.max(0, 16 - (currTime - lastTime)); 317 | const id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); 318 | lastTime = currTime + timeToCall; 319 | return id; 320 | }; 321 | } 322 | 323 | export default function getRequestAnimationFrame() { 324 | // 这个函数返回一个定时器或者监听器ID 325 | if (typeof window === 'undefined') { 326 | return () => {}; 327 | } 328 | if (window.requestAnimationFrame) { 329 | // https://github.com/vuejs/vue/issues/4465 330 | return window.requestAnimationFrame.bind(window); 331 | } 332 | // 做了浏览器兼容 333 | const prefix = availablePrefixs.filter(key => `${key}RequestAnimationFrame` in window)[0]; 334 | 335 | return prefix 336 | ? window[`${prefix}RequestAnimationFrame`] 337 | : requestAnimationFramePolyfill(); 338 | } 339 | 340 | export function cancelRequestAnimationFrame(id) { 341 | // 这个函数用来根据ID删除对应的定时器或者监听器 342 | if (typeof window === 'undefined') { 343 | return null; 344 | } 345 | if (window.cancelAnimationFrame) { 346 | return window.cancelAnimationFrame(id); 347 | } 348 | const prefix = availablePrefixs.filter(key => 349 | `${key}CancelAnimationFrame` in window || `${key}CancelRequestAnimationFrame` in window, 350 | )[0]; 351 | 352 | return prefix ? 353 | (window[`${prefix}CancelAnimationFrame`] || window[`${prefix}CancelRequestAnimationFrame`]).call(this, id) 354 | : clearTimeout(id); 355 | } 356 | ``` 357 | 358 | ```js 359 | import getRequestAnimationFrame, { cancelRequestAnimationFrame } from '../_util/getRequestAnimationFrame'; 360 | 361 | // 获得一个定时器或者监听器 362 | const reqAnimFrame = getRequestAnimationFrame(); 363 | // 这个函数收到一个函数 返回一个被放入监听其或者定时器额函数, 364 | // 也就是说给这个传入的函数绑定了一个id,让他成为唯一的一个, 365 | // 这样在消除他的时候也很方便 366 | export default function throttleByAnimationFrame(fn) { 367 | let requestId; 368 | 369 | const later = args => () => { 370 | requestId = null; 371 | fn(...args); 372 | }; 373 | 374 | const throttled = (...args) => { 375 | if (requestId == null) { 376 | // 获取定时器或者监听器ID,将监听事件传入 377 | requestId = reqAnimFrame(later(args)); 378 | } 379 | }; 380 | // 给这个函数添加上一个取消的函数 381 | (throttled as any).cancel = () => cancelRequestAnimationFrame(requestId); 382 | // 返回构造的新函数 383 | return throttled; 384 | } 385 | 386 | export function throttleByAnimationFrameDecorator() { 387 | return function(target, key, descriptor) { 388 | // 装饰器函数,传入typescript的方法构造器的三个参数 389 | // target: 当前函数(属性)属于的类 390 | // key: 当前函数(属性)名 391 | // dedescriptor: 当前属性的描述 392 | let fn = descriptor.value; 393 | let definingProperty = false; 394 | return { 395 | configurable: true, 396 | // 这里有一个疑惑 就是这个get()函数是在什么时候被执行的呢? 397 | // 因为从外部看来 这个函数最多只执行到了上一层的return,这一层的 398 | // 没有被执行,那么一下代码都不会走,但是却能够调用新函数里面的属性。。。 好神奇, 399 | // 希望有大神能够在此解说一下 万分感激 400 | get() { 401 | if (definingProperty || this === target.prototype || this.hasOwnProperty(key)) { 402 | return fn; 403 | } 404 | 405 | let boundFn = throttleByAnimationFrame(fn.bind(this)); 406 | definingProperty = true; 407 | // 重新将传入的函数定义成构造的新函数并且返回 408 | Object.defineProperty(this, key, { 409 | value: boundFn, 410 | configurable: true, 411 | writable: true, 412 | }); 413 | definingProperty = false; 414 | return boundFn; 415 | }, 416 | }; 417 | }; 418 | } 419 | 420 | ``` 421 | 422 | ## 补(装饰器中的get以及set解读) 423 | 424 | 下来之后我自己模拟了一下上面的装饰器,代码如下,并且通过查询一些资料,知道了get和set是在什么时候被调用的 425 | 426 | 在写装饰器代码的时候需要在tsconfig.json文件中的`compilerOptions`属性添加一下代码 427 | `"experimentalDecorators": true ` 428 | 429 | 这个get函数会在类被实例化的时候就进行调用,所以就能够将这些属性赋给外部的target 430 | 431 | 也就是在this.callDecorator的时候 432 | 433 | 顺带说一下set函数 会在 this.callDecorator = something 的时候调用 434 | 435 | ### Demo组件 436 | ```js 437 | import * as React from 'react'; 438 | import * as PropTypes from 'prop-types'; 439 | import { MyDecorator } from './Decorator'; 440 | 441 | export interface DemoProps { 442 | helloString?: string; 443 | } 444 | 445 | export default class DecoratorTest extends React.Component { 446 | static propTypes = { 447 | helloString: PropTypes.string, 448 | }; 449 | 450 | constructor(props) { 451 | super(props); 452 | } 453 | 454 | @MyDecorator() 455 | callDecorator() { 456 | console.log('I am in callDecorator'); 457 | } 458 | 459 | componentDidMount() { 460 | this.callDecorator(); 461 | (this.callDecorator as any).cancel(); 462 | } 463 | 464 | render() { 465 | return ( 466 |
467 | {this.props.helloString} 468 |
469 | ); 470 | } 471 | } 472 | ``` 473 | 474 | ### 装饰器代码 475 | 476 | ```js 477 | export default function decoratorTest(fn) { 478 | console.log('in definingProperty'); 479 | const throttled = () => { 480 | fn(); 481 | }; 482 | 483 | (throttled as any).cancel = () => console.log('cancel'); 484 | 485 | return throttled; 486 | } 487 | 488 | export function MyDecorator() { 489 | return function(target, key, descriptor) { 490 | let fn = descriptor.value; 491 | let definingProperty = false; 492 | console.log('before definingProperty'); 493 | return { 494 | configurable: true, 495 | // get: function()这样的写法也是可以执行 496 | get() { 497 | if (definingProperty || this === target.prototype || this.hasOwnProperty(key)) { 498 | return fn; 499 | } 500 | let boundFn = decoratorTest(fn.bind(this)); 501 | definingProperty = true; 502 | Object.defineProperty(this, key, { 503 | value: boundFn, 504 | configurable: true, 505 | writable: true, 506 | }); 507 | definingProperty = false; 508 | return boundFn; 509 | }, 510 | }; 511 | }; 512 | } 513 | ``` 514 | 515 | 输顺序结果如图 516 | 517 | ![decorator](../../images/affix/decorator.png) 518 | 519 | ## 完整代码 520 | 521 | ```js 522 | import React from 'react'; 523 | import ReactDOM from 'react-dom'; 524 | import PropTypes from 'prop-types'; 525 | import addEventListener from 'rc-util/lib/Dom/addEventListener'; 526 | import classNames from 'classnames'; 527 | import shallowequal from 'shallowequal'; 528 | import omit from 'omit.js'; 529 | import getScroll from '../_util/getScroll'; 530 | import { throttleByAnimationFrameDecorator } from '../_util/throttleByAnimationFrame'; 531 | 532 | function getTargetRect(target): ClientRect { 533 | return target !== window ? 534 | target.getBoundingClientRect() : 535 | { top: 0, left: 0, bottom: 0 }; 536 | } 537 | 538 | function getOffset(element: HTMLElement, target) { 539 | const elemRect = element.getBoundingClientRect(); 540 | const targetRect = getTargetRect(target); 541 | 542 | const scrollTop = getScroll(target, true); 543 | const scrollLeft = getScroll(target, false); 544 | 545 | const docElem = window.document.body; 546 | const clientTop = docElem.clientTop || 0; 547 | const clientLeft = docElem.clientLeft || 0; 548 | 549 | return { 550 | top: elemRect.top - targetRect.top + 551 | scrollTop - clientTop, 552 | left: elemRect.left - targetRect.left + 553 | scrollLeft - clientLeft, 554 | width: elemRect.width, 555 | height: elemRect.height, 556 | }; 557 | } 558 | 559 | function noop() {} 560 | 561 | function getDefaultTarget() { 562 | return typeof window !== 'undefined' ? 563 | window : null; 564 | } 565 | 566 | // Affix 567 | export interface AffixProps { 568 | /** 569 | * 距离窗口顶部达到指定偏移量后触发 570 | */ 571 | offsetTop?: number; 572 | offset?: number; 573 | /** 距离窗口底部达到指定偏移量后触发 */ 574 | offsetBottom?: number; 575 | style?: React.CSSProperties; 576 | /** 固定状态改变时触发的回调函数 */ 577 | onChange?: (affixed?: boolean) => void; 578 | /** 设置 Affix 需要监听其滚动事件的元素,值为一个返回对应 DOM 元素的函数 */ 579 | target?: () => Window | HTMLElement; 580 | prefixCls?: string; 581 | } 582 | 583 | export default class Affix extends React.Component { 584 | static propTypes = { 585 | offsetTop: PropTypes.number, 586 | offsetBottom: PropTypes.number, 587 | target: PropTypes.func, 588 | }; 589 | 590 | scrollEvent: any; 591 | resizeEvent: any; 592 | timeout: any; 593 | refs: { 594 | fixedNode: HTMLElement; 595 | }; 596 | 597 | events = [ 598 | 'resize', 599 | 'scroll', 600 | 'touchstart', 601 | 'touchmove', 602 | 'touchend', 603 | 'pageshow', 604 | 'load', 605 | ]; 606 | 607 | eventHandlers = {}; 608 | 609 | constructor(props) { 610 | super(props); 611 | this.state = { 612 | affixStyle: null, 613 | placeholderStyle: null, 614 | }; 615 | } 616 | 617 | setAffixStyle(e, affixStyle) { 618 | const { onChange = noop, target = getDefaultTarget } = this.props; 619 | const originalAffixStyle = this.state.affixStyle; 620 | const isWindow = target() === window; 621 | if (e.type === 'scroll' && originalAffixStyle && affixStyle && isWindow) { 622 | return; 623 | } 624 | if (shallowequal(affixStyle, originalAffixStyle)) { 625 | return; 626 | } 627 | this.setState({ affixStyle }, () => { 628 | const affixed = !!this.state.affixStyle; 629 | if ((affixStyle && !originalAffixStyle) || 630 | (!affixStyle && originalAffixStyle)) { 631 | onChange(affixed); 632 | } 633 | }); 634 | } 635 | 636 | setPlaceholderStyle(placeholderStyle) { 637 | const originalPlaceholderStyle = this.state.placeholderStyle; 638 | if (shallowequal(placeholderStyle, originalPlaceholderStyle)) { 639 | return; 640 | } 641 | this.setState({ placeholderStyle }); 642 | } 643 | 644 | @throttleByAnimationFrameDecorator() 645 | updatePosition(e) { 646 | let { offsetTop, offsetBottom, offset, target = getDefaultTarget } = this.props; 647 | const targetNode = target(); 648 | 649 | // Backwards support 650 | offsetTop = offsetTop || offset; 651 | const scrollTop = getScroll(targetNode, true); 652 | const affixNode = ReactDOM.findDOMNode(this) as HTMLElement; 653 | const elemOffset = getOffset(affixNode, targetNode); 654 | const elemSize = { 655 | width: this.refs.fixedNode.offsetWidth, 656 | height: this.refs.fixedNode.offsetHeight, 657 | }; 658 | 659 | const offsetMode = { 660 | top: false, 661 | bottom: false, 662 | }; 663 | // Default to `offsetTop=0`. 664 | if (typeof offsetTop !== 'number' && typeof offsetBottom !== 'number') { 665 | offsetMode.top = true; 666 | offsetTop = 0; 667 | } else { 668 | offsetMode.top = typeof offsetTop === 'number'; 669 | offsetMode.bottom = typeof offsetBottom === 'number'; 670 | } 671 | 672 | const targetRect = getTargetRect(targetNode); 673 | const targetInnerHeight = 674 | (targetNode as Window).innerHeight || (targetNode as HTMLElement).clientHeight; 675 | if (scrollTop > elemOffset.top - (offsetTop as number) && offsetMode.top) { 676 | // Fixed Top 677 | const width = elemOffset.width; 678 | this.setAffixStyle(e, { 679 | position: 'fixed', 680 | top: targetRect.top + (offsetTop as number), 681 | left: targetRect.left + elemOffset.left, 682 | width, 683 | }); 684 | this.setPlaceholderStyle({ 685 | width, 686 | height: elemSize.height, 687 | }); 688 | } else if ( 689 | scrollTop < elemOffset.top + elemSize.height + (offsetBottom as number) - targetInnerHeight && 690 | offsetMode.bottom 691 | ) { 692 | // Fixed Bottom 693 | const targetBottomOffet = targetNode === window ? 0 : (window.innerHeight - targetRect.bottom); 694 | const width = elemOffset.width; 695 | this.setAffixStyle(e, { 696 | position: 'fixed', 697 | bottom: targetBottomOffet + (offsetBottom as number), 698 | left: targetRect.left + elemOffset.left, 699 | width, 700 | }); 701 | this.setPlaceholderStyle({ 702 | width, 703 | height: elemOffset.height, 704 | }); 705 | } else { 706 | const { affixStyle } = this.state; 707 | if (e.type === 'resize' && affixStyle && affixStyle.position === 'fixed' && affixNode.offsetWidth) { 708 | this.setAffixStyle(e, { ...affixStyle, width: affixNode.offsetWidth }); 709 | } else { 710 | this.setAffixStyle(e, null); 711 | } 712 | this.setPlaceholderStyle(null); 713 | } 714 | } 715 | 716 | componentDidMount() { 717 | const target = this.props.target || getDefaultTarget; 718 | // Wait for parent component ref has its value 719 | this.timeout = setTimeout(() => { 720 | this.setTargetEventListeners(target); 721 | }); 722 | } 723 | 724 | componentWillReceiveProps(nextProps) { 725 | if (this.props.target !== nextProps.target) { 726 | this.clearEventListeners(); 727 | this.setTargetEventListeners(nextProps.target); 728 | 729 | // Mock Event object. 730 | this.updatePosition({}); 731 | } 732 | } 733 | 734 | componentWillUnmount() { 735 | this.clearEventListeners(); 736 | clearTimeout(this.timeout); 737 | (this.updatePosition as any).cancel(); 738 | } 739 | 740 | setTargetEventListeners(getTarget) { 741 | const target = getTarget(); 742 | if (!target) { 743 | return; 744 | } 745 | this.clearEventListeners(); 746 | 747 | this.events.forEach(eventName => { 748 | this.eventHandlers[eventName] = addEventListener(target, eventName, this.updatePosition); 749 | }); 750 | } 751 | 752 | clearEventListeners() { 753 | this.events.forEach(eventName => { 754 | const handler = this.eventHandlers[eventName]; 755 | if (handler && handler.remove) { 756 | handler.remove(); 757 | } 758 | }); 759 | } 760 | 761 | render() { 762 | const className = classNames({ 763 | [this.props.prefixCls || 'ant-affix']: this.state.affixStyle, 764 | }); 765 | 766 | const props = omit(this.props, ['prefixCls', 'offsetTop', 'offsetBottom', 'target', 'onChange']); 767 | const placeholderStyle = { ...this.state.placeholderStyle, ...this.props.style }; 768 | return ( 769 |
770 |
771 | {this.props.children} 772 |
773 |
774 | ); 775 | } 776 | } 777 | ``` -------------------------------------------------------------------------------- /story/components/breadcrumb.md: -------------------------------------------------------------------------------- 1 | # Breadcrumb 面包屑 2 | 3 | Antd的面包屑组件是一个能够结合路由进行跳转页面的组件,其中对于路由的支持是react-router@3以及react-router@4。 4 | 5 | ## BreadcrumbProps 6 | 7 | 一样的我们从组件传入参数开始入手,先弄清楚他接受那些参数,然后才知道这些参数在什么地方有什么作用 8 | 9 | ```js 10 | export interface BreadcrumbProps { 11 | prefixCls?: string; // 样式类的前缀 12 | routes?: Array; // 路由的数组集合 13 | params?: Object; // 路由的参数对象集合 14 | separator?: React.ReactNode; // 每个item之间的分隔符 15 | itemRender?: (route: any, params: any, routes: Array, paths: Array) => React.ReactNode; // 自定义的渲染item的函数 16 | style?: React.CSSProperties; // 行内样式 17 | className?: string; // 自定义样式名 18 | } 19 | ``` 20 | 21 | ## Render() 22 | 23 | 同样的一个组件的入口点就是他的render函数 24 | 25 | ```js 26 | render() { 27 | let crumbs; 28 | const { 29 | separator, prefixCls, style, className, routes, params = {}, 30 | children, itemRender = defaultItemRender, 31 | } = this.props; 32 | if (routes && routes.length > 0) { 33 | // 如果有routes这个参数存在,就需要利用routes来构造item 34 | const paths: string[] = []; // 定义一个路径数组为空 35 | // 便利传入的routes,从antd的官网例子上面可以看出在react-router版本是4以下的时候 36 | // react-router会给传入组件传递一些参数回来,但是到了react-router4版本的时候、 37 | // 就需要自己构造这些参数了,这里主要使用了routes,params,children 38 | crumbs = routes.map((route) => { 39 | // 从官网上的例子中可以看到每一个route下面的对应回传参数如图 40 | route.path = route.path || ''; 41 | // 将单个item路径按照‘/’分割成对应数组 42 | let path: string = route.path.replace(/^\//, ''); 43 | Object.keys(params).forEach(key => { 44 | // 将单个item的path中的对应参数换成传入参数对应的值 45 | path = path.replace(`:${key}`, params[key]); 46 | }); 47 | if (path) { 48 | // 最开始我没看懂这个地方为什么要不断的往paths这个数组之中插入路径,后来看了antd官网的实例 49 | // 知道了原来传入的routes数组的顺序是依次包含的关系,也就是第一个route是第一层路由,剩下的 50 | // 依次包含层级,但是这种传回方式仅限于react-router@3 51 | paths.push(path); 52 | } 53 | return ( 54 | // 然后传入itemrender函数进行items的渲染,等会解读这个函数的时候就知道paths的作用了 55 | 56 | {itemRender(route, params, routes, paths)} 57 | 58 | ); 59 | }); 60 | } else if (children) { 61 | // 如果是存在孩子节点的时候就渲染孩子节点,使用的方法依旧是React.Children.map+React.cloneElement() 62 | // 我大胆的推测这段代码是为了适配react-router@4新增的 63 | crumbs = React.Children.map(children, (element: any, index) => { 64 | if (!element) { 65 | return element; 66 | } 67 | // 这里的waring函数是antd自身根据https://www.npmjs.com/package/warning 68 | // 封装了一个浏览器console框打印警告的工具函数 69 | warning( 70 | element.type && element.type.__ANT_BREADCRUMB_ITEM, 71 | 'Breadcrumb only accepts Breadcrumb.Item as it\'s children', 72 | ); 73 | return cloneElement(element, { 74 | separator, 75 | key: index, 76 | }); 77 | }); 78 | } 79 | return ( 80 |
81 | {crumbs} 82 |
83 | ); 84 | } 85 | ``` 86 | 87 | ![route回传参数](../../images/breadcrumb/route3.png) 88 | 89 | ## 使用react-router@3需要的函数 90 | 91 | ```js 92 | // 获取当前路由上的名字 93 | function getBreadcrumbName(route, params) { 94 | if (!route.breadcrumbName) { 95 | return null; 96 | } 97 | // 将所有参数使用 '|'分割形成字符串,用于下面的正则匹配 98 | const paramsKeys = Object.keys(params).join('|'); 99 | // js的replace第一个参数可以是一个字符串或者是一个正则匹配, 100 | // 第二个参数可以是一个字符串或者是一个函数返回替换的字符串 101 | const name = route.breadcrumbName.replace( 102 | // 正则匹配参数,例如breadcrumbName='Application:id:name' 103 | // 这里的匹配正则串是:':id|name' 104 | // RegExp() 的第一个参数是正则匹配的表达式文本 105 | // 第二个参数是对于匹配的严格性的要求,g是代表全局匹配,而不是在匹配第一个后停止 106 | new RegExp(`:(${paramsKeys})`, 'g'), 107 | // 如果参数中这个参数有值就用这个值,没有就返回 108 | (replacement, key) => params[key] || replacement, 109 | ); 110 | return name; 111 | } 112 | 113 | // 渲染item 114 | function defaultItemRender(route, params, routes, paths) { 115 | // 如果是最后一个item就不添加连接 116 | const isLastItem = routes.indexOf(route) === routes.length - 1; 117 | const name = getBreadcrumbName(route, params); 118 | return isLastItem 119 | ? {name} 120 | : {name}; 121 | } 122 | ``` 123 | 124 | # BreadcrumbItem 125 | 126 | 单独的一个用来编写Breadcrumb的items的组件,我觉得是为了能够在react-router@4上面使用, 127 | 128 | 因为现在react-router@4是需要自己编写路由层级,所以这样的方式更加灵活 129 | 130 | ## BreadcrumbItemProps 131 | 132 | 虽然是子组件,但是也还是需要从他的参数开始了解 133 | 134 | ```js 135 | export interface BreadcrumbItemProps { 136 | prefixCls?: string; // 样式类的命名空间前缀 137 | separator?: React.ReactNode; // item的分隔符 138 | href?: string; // 连接 139 | } 140 | ``` 141 | 142 | ## Render() 143 | 144 | render函数比较简单 145 | 146 | ```js 147 | render() { 148 | const { prefixCls, separator, children, ...restProps } = this.props; 149 | let link; 150 | // 如果有href这个props,就使用a标签 151 | if ('href' in this.props) { 152 | link = {children}; 153 | } else { 154 | link = {children}; 155 | } 156 | if (children) { 157 | return ( 158 | 159 | {link} 160 | {separator} 161 | 162 | ); 163 | } 164 | return null; 165 | } 166 | ``` 167 | 168 | ## 组件的身份证明 169 | 170 | 这个组件中有一个东西代表了他的身份 171 | 172 | ```js 173 | export default class BreadcrumbItem extends React.Component { 174 | static __ANT_BREADCRUMB_ITEM = true; // 身份证明,代表他是一个antbreadcrumb的子元素 175 | ``` 176 | 177 | 接下来我们就看看他是怎么使用的吧,在上面的Breadcrumb的render代码中有一段代码 178 | 179 | ```js 180 | crumbs = React.Children.map(children, (element: any, index) => { 181 | if (!element) { 182 | return element; 183 | } 184 | warning( 185 | // 这里的element.type,想必大家一定在想是什么东西,那么我将为大家揭晓 186 | // 在react的官网上面有做React.Children.map的解释,其中就有element.type是什么 187 | // React.cloneElement() is almost equivalent to: 188 | // {children} 189 | // the type argument can be either a tag name string (such as 'div' or 'span'), 190 | // or a React component type (a class or a function). 191 | // 相信大家看了这段话,就知道这个身份证明怎么使用了吧,以后大家也可以这样标识自己 192 | // 制作的组件,是不是感觉很酷 193 | element.type && element.type.__ANT_BREADCRUMB_ITEM, 194 | 'Breadcrumb only accepts Breadcrumb.Item as it\'s children', 195 | ); 196 | return cloneElement(element, { 197 | separator, 198 | key: index, 199 | }); 200 | }); 201 | ``` -------------------------------------------------------------------------------- /story/components/button.md: -------------------------------------------------------------------------------- 1 | # Button 2 | 3 | `Button`包括了两个组件,`Button`与`ButtonGroup`。 4 | 5 | ## ButtonProps 6 | 7 | 看一个组件首先看的是他的传参也就是`props`,所以我们这里先看`Button`组件的`ButtonProps` 8 | 9 | ```js 10 | export type ButtonType = 'primary' | 'ghost' | 'dashed' | 'danger'; 11 | export type ButtonShape = 'circle' | 'circle-outline'; 12 | export type ButtonSize = 'small' | 'large'; 13 | 14 | // typescript语法,这里表示的是一些参数,参数后面跟上 ? 是可选参数的意思,不跟就是必须参数 15 | // 参数后面所跟的就是参数的类型,类型可以是自定义的类型,就如‘ButtonType’,‘ButtonShape’,‘ButtonSize’ 16 | // 也可以是函数或者类,如React.FormEventHandler 17 | // 详情请看这里 https://www.tslang.cn/docs/handbook/interfaces.html 18 | export interface ButtonProps { 19 | type?: ButtonType; 20 | htmlType?: string; 21 | icon?: string; 22 | shape?: ButtonShape; 23 | size?: ButtonSize; 24 | onClick?: React.FormEventHandler; 25 | onMouseUp?: React.FormEventHandler; 26 | onMouseDown?: React.FormEventHandler; 27 | loading?: boolean | { delay?: number }; 28 | disabled?: boolean; 29 | style?: React.CSSProperties; 30 | prefixCls?: string; 31 | className?: string; 32 | ghost?: boolean; 33 | } 34 | ``` 35 | 36 | ## Render() 37 | 38 | 看完其参数有哪些之后我们就直接跳过组件内部的其他的东西,直接看他的渲染函数,毕竟这里是执行的入口 39 | 这里顺带提一下这句代码 40 | 41 | ```js 42 | // 这里的意思是将传入两个参数,React.Component的参数第一个是Props,第二个是state, 43 | // 然后利用typescript的类型检查,Props类型需要时上面定义的ButtonProps中的可选参数中的变量名 44 | // state这里传入任意都行 45 | export default class Button extends React.Component 46 | ``` 47 | 48 | ```js 49 | // 接下来是render() 50 | render() { 51 | // 将参数从props解构出来 52 | const { 53 | type, shape, size = '', className, htmlType, children, icon, prefixCls, ghost, ...others, 54 | } = this.props; 55 | // 将loading和clicked两个状态从state解构 56 | const { loading, clicked } = this.state; 57 | 58 | // large => lg 59 | // small => sm 60 | let sizeCls = ''; 61 | switch (size) { 62 | case 'large': 63 | sizeCls = 'lg'; 64 | break; 65 | case 'small': 66 | sizeCls = 'sm'; 67 | default: 68 | break; 69 | } 70 | // 组建样式 71 | const classes = classNames(prefixCls, className, { 72 | [`${prefixCls}-${type}`]: type, 73 | [`${prefixCls}-${shape}`]: shape, 74 | [`${prefixCls}-${sizeCls}`]: sizeCls, 75 | [`${prefixCls}-icon-only`]: !children && icon, 76 | [`${prefixCls}-loading`]: loading, 77 | [`${prefixCls}-clicked`]: clicked, 78 | [`${prefixCls}-background-ghost`]: ghost, 79 | }); 80 | 81 | // 是否需要加载 82 | const iconType = loading ? 'loading' : icon; 83 | // 是否需要添加Icon,不过官方给的是如果需要用到icon的话最好自己写在里面 84 | 85 | const iconNode = iconType ? : null; 86 | 87 | const needInserted = React.Children.count(children) === 1 && (!iconType || iconType === 'loading'); 88 | 89 | // 重点在这里,敲黑板了 90 | // 这里引用了React.Children.map这个函数来对这个包裹在这个Button组件中的内容渲染出来 91 | // 其中insertSpace()这个函数也有意思,这个函数主要是为了在当组建中间写的是中文汉字的时 92 | // 候给其汉字之间添加一个空格作为分隔,这里有的同学会问为什么不用css里面的letter-space 93 | // 属性,这个我也不是很清楚。。。不过他不用的话可能是是不想在英文字母中间添加空格吧 94 | 95 | const kids = React.Children.map(children, child => insertSpace(child, needInserted)); 96 | 97 | return ( 98 | 107 | ); 108 | } 109 | 110 | ``` 111 | 112 | ## InsertSpace() 113 | 114 | 上面讲到了这个函数,这里就来仔细看看是干嘛的吧 115 | 116 | ```js 117 | const rxTwoCNChar = /^[\u4e00-\u9fa5]{2}$/; 118 | // 这里的bind有必要好好的理解一下 119 | const isTwoCNChar = rxTwoCNChar.test.bind(rxTwoCNChar); 120 | function isString(str: any) { 121 | return typeof str === 'string'; 122 | } 123 | 124 | // Insert one space between two chinese characters automatically. 125 | function insertSpace(child: React.ReactChild, needInserted: boolean) { 126 | // Check the child if is undefined or null. 127 | if (child == null) { 128 | return; 129 | } 130 | const SPACE = needInserted ? ' ' : ''; 131 | // strictNullChecks oops. 132 | // 这个判断的意思是当这个child不是字符串也不是数字并且child.type为字符串并且child的children是汉字的情况下 133 | // 给其加上空格,上面说的是代码直译,那么代码意译下来就是这样的一个情况 134 | // 这种情况(所以这里他才会有一个英文注释,说的是不是严格意义的检查,啊哈哈,尴尬的实现方法) 135 | // 138 | // 这里说明一下,child.type以及child.props.children是react在渲染的时候会给虚拟dom添加的一些属性,如图 139 | if (typeof child !== 'string' && typeof child !== 'number' && 140 | isString(child.type) && isTwoCNChar(child.props.children)) { 141 | return React.cloneElement(child, {}, 142 | child.props.children.split('').join(SPACE)); 143 | } 144 | // 这种情况就很明了了 就是Button组件中写的汉字 145 | if (typeof child === 'string') { 146 | if (isTwoCNChar(child)) { 147 | child = child.split('').join(SPACE); 148 | } 149 | return {child}; 150 | } 151 | return child; 152 | } 153 | ``` 154 | ![Button](../../images/button/button1.png) 155 | 156 | ## 完整源代码 157 | 158 | 剩下的都是一些简单的东西,也没有什么可以讲的了 159 | 160 | ```js 161 | import React from 'react'; 162 | import PropTypes from 'prop-types'; 163 | import classNames from 'classnames'; 164 | import omit from 'omit.js'; 165 | import Icon from '../icon'; 166 | import Group from './button-group'; 167 | 168 | const rxTwoCNChar = /^[\u4e00-\u9fa5]{2}$/; 169 | const isTwoCNChar = rxTwoCNChar.test.bind(rxTwoCNChar); 170 | function isString(str: any) { 171 | return typeof str === 'string'; 172 | } 173 | 174 | // Insert one space between two chinese characters automatically. 175 | function insertSpace(child: React.ReactChild, needInserted: boolean) { 176 | // Check the child if is undefined or null. 177 | if (child == null) { 178 | return; 179 | } 180 | const SPACE = needInserted ? ' ' : ''; 181 | // strictNullChecks oops. 182 | if (typeof child !== 'string' && typeof child !== 'number' && 183 | isString(child.type) && isTwoCNChar(child.props.children)) { 184 | return React.cloneElement(child, {}, 185 | child.props.children.split('').join(SPACE)); 186 | } 187 | if (typeof child === 'string') { 188 | if (isTwoCNChar(child)) { 189 | child = child.split('').join(SPACE); 190 | } 191 | return {child}; 192 | } 193 | return child; 194 | } 195 | 196 | export type ButtonType = 'primary' | 'ghost' | 'dashed' | 'danger'; 197 | export type ButtonShape = 'circle' | 'circle-outline'; 198 | export type ButtonSize = 'small' | 'large'; 199 | 200 | export interface ButtonProps { 201 | type?: ButtonType; 202 | htmlType?: string; 203 | icon?: string; 204 | shape?: ButtonShape; 205 | size?: ButtonSize; 206 | onClick?: React.FormEventHandler; 207 | onMouseUp?: React.FormEventHandler; 208 | onMouseDown?: React.FormEventHandler; 209 | loading?: boolean | { delay?: number }; 210 | disabled?: boolean; 211 | style?: React.CSSProperties; 212 | prefixCls?: string; 213 | className?: string; 214 | ghost?: boolean; 215 | } 216 | 217 | export default class Button extends React.Component { 218 | // 这里这样子写只是为了方便这样子写Button.Group来引用ButtonGroup这个组件,下一节将会讲解这个组件 219 | static Group: typeof Group; 220 | static __ANT_BUTTON = true; 221 | 222 | static defaultProps = { 223 | prefixCls: 'ant-btn', 224 | loading: false, 225 | clicked: false, 226 | ghost: false, 227 | }; 228 | 229 | static propTypes = { 230 | type: PropTypes.string, 231 | shape: PropTypes.oneOf(['circle', 'circle-outline']), 232 | size: PropTypes.oneOf(['large', 'default', 'small']), 233 | htmlType: PropTypes.oneOf(['submit', 'button', 'reset']), 234 | onClick: PropTypes.func, 235 | loading: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]), 236 | className: PropTypes.string, 237 | icon: PropTypes.string, 238 | }; 239 | 240 | timeout: number; 241 | delayTimeout: number; 242 | 243 | constructor(props: ButtonProps) { 244 | super(props); 245 | this.state = { 246 | loading: props.loading, 247 | }; 248 | } 249 | 250 | componentWillReceiveProps(nextProps: ButtonProps) { 251 | const currentLoading = this.props.loading; 252 | const loading = nextProps.loading; 253 | 254 | if (currentLoading) { 255 | clearTimeout(this.delayTimeout); 256 | } 257 | 258 | if (typeof loading !== 'boolean' && loading && loading.delay) { 259 | this.delayTimeout = setTimeout(() => this.setState({ loading }), loading.delay); 260 | } else { 261 | this.setState({ loading }); 262 | } 263 | } 264 | 265 | // 在组件销毁的时候一定要记得将定时器也一同销毁 266 | componentWillUnmount() { 267 | if (this.timeout) { 268 | clearTimeout(this.timeout); 269 | } 270 | if (this.delayTimeout) { 271 | clearTimeout(this.delayTimeout); 272 | } 273 | } 274 | 275 | handleClick = (e: React.MouseEvent) => { 276 | // Add click effect 277 | this.setState({ clicked: true }); 278 | clearTimeout(this.timeout); 279 | this.timeout = setTimeout(() => this.setState({ clicked: false }), 500); 280 | 281 | const onClick = this.props.onClick; 282 | if (onClick) { 283 | onClick(e); 284 | } 285 | } 286 | 287 | render() { 288 | const { 289 | type, shape, size = '', className, htmlType, children, icon, prefixCls, ghost, ...others, 290 | } = this.props; 291 | 292 | const { loading, clicked } = this.state; 293 | 294 | // large => lg 295 | // small => sm 296 | let sizeCls = ''; 297 | switch (size) { 298 | case 'large': 299 | sizeCls = 'lg'; 300 | break; 301 | case 'small': 302 | sizeCls = 'sm'; 303 | default: 304 | break; 305 | } 306 | 307 | const classes = classNames(prefixCls, className, { 308 | [`${prefixCls}-${type}`]: type, 309 | [`${prefixCls}-${shape}`]: shape, 310 | [`${prefixCls}-${sizeCls}`]: sizeCls, 311 | [`${prefixCls}-icon-only`]: !children && icon, 312 | [`${prefixCls}-loading`]: loading, 313 | [`${prefixCls}-clicked`]: clicked, 314 | [`${prefixCls}-background-ghost`]: ghost, 315 | }); 316 | 317 | const iconType = loading ? 'loading' : icon; 318 | const iconNode = iconType ? : null; 319 | const needInserted = React.Children.count(children) === 1 && (!iconType || iconType === 'loading'); 320 | const kids = React.Children.map(children, child => insertSpace(child, needInserted)); 321 | 322 | return ( 323 | 331 | ); 332 | } 333 | } 334 | ``` -------------------------------------------------------------------------------- /story/components/button_group.md: -------------------------------------------------------------------------------- 1 | # ButtonGroup 2 | 3 | 这个组件没有重点可以说,毕竟就只是一个将`Button`组件包裹起来的一个容器,但是这里还是有一个点可以值得一提 4 | ```js 5 | // 这里的React.SFC是 typescript 的对于 react 的StatelessComponent的一个interface的一个别称 6 | // 那么对于Stateless Functional Component,是一种不需要管理state的组件,也就是说这个组件中不会 7 | // 对state进行操作的组件,是一个纯函数组件,大家有兴趣可以去了解 8 | // 详情请看 https://medium.com/@iktakahiro/react-stateless-functional-component-with-typescript-ce5043466011 9 | const ButtonGroup: React.SFC = (props) => {} 10 | ``` 11 | 12 | ```tsx 13 | import React from 'react'; 14 | import classNames from 'classnames'; 15 | 16 | export type ButtonSize = 'small' | 'large'; 17 | 18 | export interface ButtonGroupProps { 19 | size?: ButtonSize; 20 | style?: React.CSSProperties; 21 | className?: string; 22 | prefixCls?: string; 23 | } 24 | 25 | const ButtonGroup: React.SFC = (props) => { 26 | const { prefixCls = 'ant-btn-group', size = '', className, ...others } = props; 27 | 28 | // large => lg 29 | // small => sm 30 | let sizeCls = ''; 31 | switch (size) { 32 | case 'large': 33 | sizeCls = 'lg'; 34 | break; 35 | case 'small': 36 | sizeCls = 'sm'; 37 | default: 38 | break; 39 | } 40 | 41 | const classes = classNames(prefixCls, { 42 | [`${prefixCls}-${sizeCls}`]: sizeCls, 43 | }, className); 44 | 45 | return
; 46 | }; 47 | 48 | export default ButtonGroup; 49 | ``` -------------------------------------------------------------------------------- /story/components/dropdown.md: -------------------------------------------------------------------------------- 1 | # DropDown 下拉菜单 2 | 3 | 下拉菜单组件是一个可以将页面上比较冗杂的操作收纳在一个点,以便节省页面空间,达到整洁美观的目的。 4 | 5 | Antd的下拉菜单组件中有一个点,就是他的内部元素必须是Antd的Menu组件,感觉有点捆绑的意思。 6 | 7 | ## DropDownProps 8 | 9 | 这里留一个小问题,为什么触发方式是一个数组,而不是单个的 10 | 11 | ```js 12 | export interface DropDownProps { 13 | trigger?: ('click' | 'hover')[]; // 触发方式 14 | overlay: React.ReactNode; // 下拉菜单所承载的内容元素,要求为Antd的Menu组件 15 | style?: React.CSSProperties; // 行内样式 16 | onVisibleChange?: (visible?: boolean) => void; // 监听下拉菜单出现/消失 17 | visible?: boolean; // 菜单是否显示 18 | disabled?: boolean; // 菜单是否可以用 19 | align?: Object; // 这个参数目前没有被使用 20 | getPopupContainer?: (triggerNode: Element) => HTMLElement; // 渲染的挂载点,默认为body 21 | prefixCls?: string; // 样式类的命名前缀 22 | className?: string; // 样式 23 | placement?: 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight'; 24 | // 弹出框与触发点的对齐方式 25 | } 26 | ``` 27 | 28 | ## 直接看代码 29 | 30 | 因为这个组件主要使用的是[rc-dropdown组件](https://github.com/react-component/dropdown) 31 | 所以这里只是对其参数做了一些封装,比较简单。 32 | 33 | ```js 34 | export default class Dropdown extends React.Component { 35 | static Button: typeof DropdownButton; 36 | static defaultProps = { 37 | prefixCls: 'ant-dropdown', 38 | mouseEnterDelay: 0.15, 39 | mouseLeaveDelay: 0.1, 40 | placement: 'bottomLeft', 41 | }; 42 | 43 | // 设定一个动画效果名称 44 | getTransitionName() { 45 | const { placement = '' } = this.props; 46 | // js的indexOf()可以使用在Array上也可以使用在String上 47 | // 使用方法一样,第一个参数是匹配的对象,第二个参数是从哪里开始匹配 48 | if (placement.indexOf('top') >= 0) { 49 | return 'slide-down'; 50 | } 51 | return 'slide-up'; 52 | } 53 | 54 | componentDidMount() { 55 | // 这里就在检测菜单内容是否是antd的menu组件,并且检测menu组件的样式 56 | const { overlay } = this.props; 57 | const overlayProps = (overlay as any).props as any; 58 | // warning函数还是和之前学习的一样的用法 59 | warning( 60 | !overlayProps.mode || overlayProps.mode === 'vertical', 61 | `mode="${overlayProps.mode}" is not supported for Dropdown\'s Menu.`, 62 | ); 63 | } 64 | 65 | render() { 66 | const { children, prefixCls, overlay, trigger, disabled } = this.props; 67 | // 将dropdown包裹的触发器加以封装,再渲染 68 | const dropdownTrigger = cloneElement(children as any, { 69 | className: classNames((children as any).props.className, `${prefixCls}-trigger`), 70 | disabled, 71 | }); 72 | // menu cannot be selectable in dropdown defaultly 73 | const overlayProps = overlay && (overlay as any).props; 74 | const selectable = (overlayProps && 'selectable' in overlayProps) 75 | ? overlayProps.selectable : false; 76 | // 同样的将dropdown包裹的内容加以封装,再渲染 77 | const fixedModeOverlay = cloneElement(overlay as any, { 78 | mode: 'vertical', 79 | selectable, 80 | }); 81 | return ( 82 | // 最后将所有参数传入rc-dropdown组件 83 | 89 | {dropdownTrigger} 90 | 91 | ); 92 | } 93 | } 94 | ``` 95 | 96 | # DropdownButton 97 | 98 | 这个组件是一个带按钮的下拉菜单组件,其实其原理就是使用的之前所讲的ButtonGroup来进行组合的一个组件。 99 | 100 | 想要了解ButtonGroup组件的可以点击[这里](./button_group.md) 101 | 102 | ## DropdownButtonProps 103 | 104 | 这个组件的props继承了两个其他组件的props,这是typescript的interface的一个特性,可以继承多个,来形成一个新的。 105 | 106 | ```js 107 | export interface DropdownButtonProps extends ButtonGroupProps, DropDownProps { 108 | type?: 'primary' | 'ghost' | 'dashed'; // 按钮类型 109 | disabled?: boolean; // 是否禁用 110 | onClick?: React.MouseEventHandler; // 点击事件 111 | children?: any; // 子节点 112 | } 113 | ``` 114 | 115 | ## Render() 116 | 117 | 从render函数就可以看出这个组件就是antd为了方便,为大家封装好了的一个带下拉菜单的按钮组件 118 | 119 | ```js 120 | render() { 121 | const { 122 | type, disabled, onClick, children, 123 | prefixCls, className, overlay, trigger, align, 124 | visible, onVisibleChange, placement, getPopupContainer, 125 | ...restProps, 126 | } = this.props; 127 | 128 | const dropdownProps = { 129 | align, 130 | overlay, 131 | trigger: disabled ? [] : trigger, 132 | onVisibleChange, 133 | placement, 134 | getPopupContainer, 135 | }; 136 | if ('visible' in this.props) { 137 | (dropdownProps as any).visible = visible; 138 | } 139 | 140 | return ( 141 | 145 | 152 | 153 | 156 | 157 | 158 | ); 159 | } 160 | ``` 161 | 162 | ## 这就完了?怎么可能,还不过瘾 163 | 164 | 写到这里就完了么?是滴,这一节就完了,因为查看了一下rc-dropdown的实现,然后根据平常看代码的习惯 165 | 166 | 从render()函数入口发现这一段代码 167 | 168 | ```js 169 | render() { 170 | const { 171 | prefixCls, children, 172 | transitionName, animation, 173 | align, placement, getPopupContainer, 174 | showAction, hideAction, 175 | overlayClassName, overlayStyle, 176 | trigger, ...otherProps, 177 | } = this.props; 178 | return ( 179 | 199 | {children} 200 | 201 | ); 202 | } 203 | ``` 204 | 205 | 然后再看到`Trigger`这个组件,居然是另外一个库[rc-trigger](https://github.com/react-component/trigger) 206 | 然后在看了`rc-trigger`的实现之后,才知道原来tigger组件才是下拉菜单的核心。 207 | 208 | 所以还没完呢,只是需要讲解的太多,不适合在一篇文章中讲全面,所以敬请期待下一篇文章, 209 | 210 | 我们将会学习rc-trigger组件的实现,之后会再写一篇rc-dropdown的组件实现解读, 211 | 212 | 最后看完这三篇文章,再倒过来重温一遍,你将会学到怎么样一层层的包装组件,将一个基础组件包装成为一个高级组件的过程。 -------------------------------------------------------------------------------- /story/components/form.md: -------------------------------------------------------------------------------- 1 | # Form 表单 2 | 3 | 这个组件貌似比较独特,在官网上面的每一个例子都是使用了`Form.create()`这个HOC的方法去进行的创建相应的Form组件 4 | 所以对于表单组件我们主要讲的应该就会是create这个高阶函数 5 | 6 | ## Form.create 7 | 8 | 这是一个高阶函数,传入的是react组件,返回一个新的react组件,在函数内部会对传入组件进行改造,添加上一定的方法用于进行一些秘密操作 9 | 如果有对高阶组件有想要深入的请移步[这里](http://www.jianshu.com/p/0aae7d4d9bc1),我们这里不做过多的深究。接下来我们直接看这个函数的代码 10 | 11 | ```js 12 | static create = function(options: FormCreateOption = {}): ComponentDecorator { 13 | const formWrapper = createDOMForm({ 14 | fieldNameProp: 'id', 15 | ...options, 16 | fieldMetaProp: FIELD_META_PROP, 17 | }); 18 | 19 | /* eslint-disable react/prefer-es6-class */ 20 | return (Component) => formWrapper(createReactClass({ 21 | propTypes: { 22 | form: PropTypes.object.isRequired, 23 | }, 24 | childContextTypes: { 25 | form: PropTypes.object.isRequired, 26 | }, 27 | getChildContext() { 28 | return { 29 | form: this.props.form, 30 | }; 31 | }, 32 | componentWillMount() { 33 | this.__getFieldProps = this.props.form.getFieldProps; 34 | }, 35 | deprecatedGetFieldProps(name, option) { 36 | warning( 37 | false, 38 | '`getFieldProps` is not recommended, please use `getFieldDecorator` instead, ' + 39 | 'see: https://u.ant.design/get-field-decorator', 40 | ); 41 | return this.__getFieldProps(name, option); 42 | }, 43 | render() { 44 | this.props.form.getFieldProps = this.deprecatedGetFieldProps; 45 | 46 | const withRef: any = {}; 47 | if (options.withRef) { 48 | withRef.ref = 'formWrappedComponent'; 49 | } else if (this.props.wrappedComponentRef) { 50 | withRef.ref = this.props.wrappedComponentRef; 51 | } 52 | return ; 53 | }, 54 | })); 55 | }; 56 | ``` 57 | 58 | 从代码看出这个函数返回的是一个函数,接受一个组件作为参数,但是返回什么不是很清楚,所以需要再看看`createDOMForm`创建的是一个什么 59 | 60 | `createDOMForm`是`rc-form`库中引用的,从代码一层层的查找下去发现创建一个form组件的主要代码是在`createBaseForm.js`这个文件中 61 | 62 | ```js 63 | function createBaseForm(option = {}, mixins = []) { 64 | const { ... } = option; 65 | 66 | return function decorate(WrappedComponent) { 67 | const Form = createReactClass({ ... }); 68 | 69 | return argumentContainer(Form, WrappedComponent); 70 | }; 71 | } 72 | ``` 73 | 74 | 这又是一个高阶函数,在这个函数中先创建了一个Form组件,然后使用`argumentContainer`函数进行包装在传出,传出的是一个新的组件 75 | 76 | 这个新的组件将会拥有传入组件以及高阶组件中的所有属性 77 | 78 | ```js 79 | import hoistStatics from 'hoist-non-react-statics'; 80 | 81 | export function argumentContainer(Container, WrappedComponent) { 82 | /* eslint no-param-reassign:0 */ 83 | Container.displayName = `Form(${getDisplayName(WrappedComponent)})`; 84 | Container.WrappedComponent = WrappedComponent; 85 | return hoistStatics(Container, WrappedComponent); 86 | } 87 | ``` 88 | `argumentContainer`函数使用了一个库`hoist-non-react-statics`,这个库是用于解决高阶组件不能够使用传入的组件的静态方法这个问题的 89 | 90 | 具体在react官网上面也有相应的[解释](https://reactjs.org/docs/higher-order-components.html#static-methods-must-be-copied-over),使用了这个方法就能够将传入组件的静态方法也完全拷贝到高阶函数返回的组件中。 91 | 92 | 从现在看来之前代码中的`formWrapper`就是一个接受传入组件,然后再将组件进行转化成为一个添加了antd自己的Form高阶组件。 93 | 94 | ## 总结 95 | 96 | 通过这个组件的这个函数,加深了我对HOC的使用和认识,也对装饰器有了更深认识,技能点+1。 -------------------------------------------------------------------------------- /story/components/grid.md: -------------------------------------------------------------------------------- 1 | # Grid 2 | 这个组件完全使用的是`flex`布局,如果还对`flex`布局不熟悉的同学可以看[这里](http://www.ruanyifeng.com/blog/2015/07/flex-grammar.html)。 3 | 4 | 该组件有两个部分,一个是`Row`,一个是`Col`,采用`Row`包裹`Col`的方法来实现栅格布局,并且栅格布局是遵从[Bootstrap 3](https://getbootstrap.com/docs/3.3/css/#responsive-utilities-classes)的标准。 5 | 6 | ## Row 7 | 8 | Row组件有一个比较特别的参数,就是`gutter`,这个参数是指的每个元素之间的间距,这个东西是在flex布局中没有存在的一个设定,所以对于比较熟悉的同学可以直接看他的间距的实现。 9 | 10 | 这里面还有两个需要注意的点,分别是:[React.Children](http://lib.csdn.net/article/react/12197),[React.cloneElement()](https://segmentfault.com/a/1190000010062928),这两个玩意儿在这react的对于子元素操作中非常常用,熟悉的同学可以跳过。 11 | ```js 12 | import React from 'react'; 13 | import { Children, cloneElement } from 'react'; 14 | import classNames from 'classnames'; 15 | import PropTypes from 'prop-types'; 16 | 17 | export interface RowProps { 18 | className?: string; 19 | gutter?: number; 20 | type?: 'flex'; 21 | align?: 'top' | 'middle' | 'bottom'; 22 | justify?: 'start' | 'end' | 'center' | 'space-around' | 'space-between'; 23 | style?: React.CSSProperties; 24 | prefixCls?: string; 25 | } 26 | 27 | export default class Row extends React.Component { 28 | static defaultProps = { 29 | gutter: 0, 30 | }; 31 | 32 | static propTypes = { 33 | type: PropTypes.string, 34 | align: PropTypes.string, 35 | justify: PropTypes.string, 36 | className: PropTypes.string, 37 | children: PropTypes.node, 38 | gutter: PropTypes.number, 39 | prefixCls: PropTypes.string, 40 | }; 41 | render() { 42 | const { type, justify, align, className, gutter, style, children, 43 | prefixCls = 'ant-row', ...others } = this.props; 44 | const classes = classNames({ 45 | [prefixCls]: !type, 46 | [`${prefixCls}-${type}`]: type, 47 | [`${prefixCls}-${type}-${justify}`]: type && justify, 48 | [`${prefixCls}-${type}-${align}`]: type && align, 49 | }, className); 50 | // 如果有gutter这个参数,就是需要添加间距,他的实现方法是给每一个Item添加左右的pading, 51 | // 但是又不想让第一个和最后一个Item也有这个内边距,所以在父级元素上面设置左右相同负值 52 | // 的margin,就能够抵消两端的padding。 53 | const rowStyle = (gutter as number) > 0 ? { 54 | marginLeft: (gutter as number) / -2, 55 | marginRight: (gutter as number) / -2, 56 | ...style, 57 | } : style; 58 | // 这里就用到了上面所说的Children 和 cloneElement 59 | // React.Children使用的时候不必担心内部子元素是否有嵌套关系 60 | const cols = Children.map(children, (col: React.ReactElement) => { 61 | if (!col) { 62 | return null; 63 | } 64 | // 判断一下col是否有props 65 | if (col.props && (gutter as number) > 0) { 66 | // 返回一个新的组件,会新增添加的其余的props或者修改过后的props 67 | return cloneElement(col, { 68 | style: { 69 | paddingLeft: (gutter as number) / 2, 70 | paddingRight: (gutter as number) / 2, 71 | ...col.props.style, 72 | }, 73 | }); 74 | } 75 | return col; 76 | }); 77 | return
{cols}
; 78 | } 79 | } 80 | ``` 81 | 82 | ## Col 83 | 84 | `Col`这个组件全部都是用的CSS以及flex实现的,唯一需要讲解的大概应该是`'xs', 'sm', 'md', 'lg', 'xl'`的使用,为什么会有传入对象的参数,因为这样子可以自定义自己的一个栅格布局,实现更加灵活的栅格布局,这样也使得`Col`这个组件更加灵活 85 | ```js 86 | import React from 'react'; 87 | import PropTypes from 'prop-types'; 88 | import classNames from 'classnames'; 89 | 90 | // 这里使用了PropTypys.oneOfType(),这个函数的意思就是使用其数组值周的任意的一种累心都可以 91 | const stringOrNumber = PropTypes.oneOfType([PropTypes.string, PropTypes.number]); 92 | const objectOrNumber = PropTypes.oneOfType([PropTypes.object, PropTypes.number]); 93 | 94 | export interface ColSize { 95 | span?: number; 96 | order?: number; 97 | offset?: number; 98 | push?: number; 99 | pull?: number; 100 | } 101 | 102 | export interface ColProps { 103 | className?: string; 104 | span?: number; 105 | order?: number; 106 | offset?: number; 107 | push?: number; 108 | pull?: number; 109 | xs?: number | ColSize; 110 | sm?: number | ColSize; 111 | md?: number | ColSize; 112 | lg?: number | ColSize; 113 | xl?: number | ColSize; 114 | prefixCls?: string; 115 | style?: React.CSSProperties; 116 | } 117 | 118 | export default class Col extends React.Component { 119 | static propTypes = { 120 | span: stringOrNumber, 121 | order: stringOrNumber, 122 | offset: stringOrNumber, 123 | push: stringOrNumber, 124 | pull: stringOrNumber, 125 | className: PropTypes.string, 126 | children: PropTypes.node, 127 | xs: objectOrNumber, 128 | sm: objectOrNumber, 129 | md: objectOrNumber, 130 | lg: objectOrNumber, 131 | xl: objectOrNumber, 132 | }; 133 | 134 | render() { 135 | const props = this.props; 136 | const { span, order, offset, push, pull, className, children, prefixCls = 'ant-col', ...others } = props; 137 | let sizeClassObj = {}; 138 | ['xs', 'sm', 'md', 'lg', 'xl'].forEach(size => { 139 | let sizeProps: ColSize = {}; 140 | // 当传入参数为对象的时候,就不仅可以定义span,还可以定义其他的参数,push, pull, older, offset 141 | if (typeof props[size] === 'number') { 142 | sizeProps.span = props[size]; 143 | } else if (typeof props[size] === 'object') { 144 | sizeProps = props[size] || {}; 145 | } 146 | 147 | delete others[size]; 148 | 149 | sizeClassObj = { 150 | ...sizeClassObj, 151 | [`${prefixCls}-${size}-${sizeProps.span}`]: sizeProps.span !== undefined, 152 | [`${prefixCls}-${size}-order-${sizeProps.order}`]: sizeProps.order || sizeProps.order === 0, 153 | [`${prefixCls}-${size}-offset-${sizeProps.offset}`]: sizeProps.offset || sizeProps.offset === 0, 154 | [`${prefixCls}-${size}-push-${sizeProps.push}`]: sizeProps.push || sizeProps.push === 0, 155 | [`${prefixCls}-${size}-pull-${sizeProps.pull}`]: sizeProps.pull || sizeProps.pull === 0, 156 | }; 157 | }); 158 | // 利用classnames这个库可以高效的合并并且覆盖元素样式 159 | const classes = classNames({ 160 | [`${prefixCls}-${span}`]: span !== undefined, 161 | [`${prefixCls}-order-${order}`]: order, 162 | [`${prefixCls}-offset-${offset}`]: offset, 163 | [`${prefixCls}-push-${push}`]: push, 164 | [`${prefixCls}-pull-${pull}`]: pull, 165 | }, className, sizeClassObj); 166 | 167 | return
{children}
; 168 | } 169 | } 170 | 171 | ``` -------------------------------------------------------------------------------- /story/components/icon.md: -------------------------------------------------------------------------------- 1 | # Icon 2 | 3 | `icon`作为开发当中使用相对频繁的一个组件,其实现也很简单,但是其中比较麻烦的一部分是icon字体的制作,可以参看这篇[文章](https://segmentfault.com/a/1190000008374352)。 4 | 5 | Antd的Icon组件使用了很简单的css来实现交互与动效 6 | 7 | ```js 8 | import React from 'react'; 9 | import classNames from 'classnames'; 10 | import omit from 'omit.js'; 11 | //classNames是条件判断输出className的值 12 | //omit是移出对象的指定属性,而实现浅拷贝 13 | 14 | //定义IconProps接口 15 | export interface IconProps { 16 | type: string; 17 | className?: string; 18 | title?: string; 19 | onClick?: React.MouseEventHandler; 20 | spin?: boolean; 21 | style?: React.CSSProperties; 22 | } 23 | 24 | const Icon = (props: IconProps) => { 25 | const { type, className = '', spin } = props; 26 | const classString = classNames({ 27 | anticon: true, 28 | 'anticon-spin': !!spin || type === 'loading', // 是否需要旋转动画,loading这个icon是默认加上旋转动效的 29 | [`anticon-${type}`]: true, 30 | }, className); 31 | 32 | // 这里说一下为什么要用omit():html的标签,其标准标签属性只有六种:id、class、title、style、dir、lang。 33 | // IconProps接口中的6种属性(方法),type、spin不属于上述六种。onClick为事件属性,可以; 34 | return ; 35 | }; 36 | 37 | export default Icon; 38 | ``` 39 | 40 | 大家也可以根据上面所提供的制作icon的方法和这样的方式来实现自己的Icon组件 41 | -------------------------------------------------------------------------------- /story/components/notification.md: -------------------------------------------------------------------------------- 1 | # Notification 2 | 这是一个全局变量的组件,可以在任意地方调用其函数就能够生成一个,我们就来看看这个组件又是用了什么奇巧淫技来实现的 3 | 4 | -- 注意:解读的源码版本为2.13.4 rc-notification版本为2.0.0 不要下载错了 5 | 6 | ## 本节讲点 7 | 8 | 1. 查看notification组件源码的文件顺序和入口点 9 | 2. rc-utils组件中的createChainedFunction函数 10 | 3. 缓存机制 11 | 4. ReactDOM.unmountComponentAtNode 12 | 13 | ## 快速阅读代码 14 | 15 | 我将带大家使用`略览`代码的方法来进行一个组件的快速通读,这就跟高中英语阅读时使用的一种阅读方法一样,快速阅读,略过细节,抓主线路,理清整个组件工作原理之后再去查看细节 16 | 17 | 1. antd-design-master/components/index.tsx 18 | 19 | 因为使用方法是直接使用的notification.api(config),所以想到先去看看是怎么抛出的 20 | `export { default as notification } from './notification'` 21 | 22 | 2. antd-design-master/components/notification/index.tsx 23 | 24 | 再看看引用的文件是怎么抛出的 25 | `export default api as NotificationApi;` 26 | 27 | 3. antd-design-master/components/notification/index.tsx 28 | 29 | 由下往上看代码,看到`api`的构成,再看到`api.notice`->`function notice`->`function getNotificationInstance`->`(Notification as any).newInstance`->`import Notification from 'rc-notification';` 30 | 31 | ```js 32 | getNotificationInstance( 33 | outerPrefixCls, 34 | args.placement || defaultPlacement 35 | ).notice({ 36 | content: ( 37 |
38 | {iconNode} 39 |
40 | {autoMarginTag} 41 | {args.message} 42 |
43 |
{args.description}
44 | {args.btn ? {args.btn} : null} 45 |
46 | ), 47 | duration, 48 | closable: true, 49 | onClose: args.onClose, 50 | key: args.key, 51 | style: args.style || {}, 52 | className: args.className, 53 | }) 54 | ``` 55 | 56 | 在这个文件中比较重要的一条代码线就是上面展示的这一条,剩下的代码可以一眼带过,比较特殊的就是他将生成的notification实例都存在一个全局常量中,方便第二次使用只要这个实例没有被destroy 57 | 58 | 4. rc-notification/src/index.js 59 | 60 | 找到入口文件`import Notification from './Notification';` 61 | 62 | 5. rc-notification/src/Notification.jsx 63 | 64 | 在上面第3条我们看到有的一个方法`newInstance`是用来创建新实例,所以我们在这个文件中也可以看到相应的代码`Notification.newInstance = function newNotificationInstance`,在这个函数中我们继续略览代码,看到`ReactDOM.render(, div);`我们知道这是将一个组件渲染在一个dom节点,所以下一个查看点就应该是`Notification`这个组件类 65 | 66 | 6. rc-notification/src/Notification.jsx 67 | 68 | 看到文件上面`class Notification extends Component`,可以看到整个组件的实现,我们可以在`render`函数中看到一个循环输出,那就是在循环输出`state`中存的`notice`,`state`中的`notice`是通过上面第3点展示的代码,获取实例之后使用`notice`函数调用的实例的`add`函数进行添加的 69 | 70 | ```js 71 | const onClose = createChainedFunction(this.remove.bind(this, notice.key), notice.onClose); 72 | return ( 77 | {notice.content} 78 | ); 79 | ``` 80 | 7. rc-notification/src/Notice.jsx 81 | 82 | ```js 83 | componentDidMount() { 84 | if (this.props.duration) { 85 | this.closeTimer = setTimeout(() => { 86 | this.close(); 87 | }, this.props.duration * 1000); 88 | } 89 | } 90 | 91 | componentWillUnmount() { 92 | this.clearCloseTimer(); 93 | } 94 | 95 | clearCloseTimer = () => { 96 | if (this.closeTimer) { 97 | clearTimeout(this.closeTimer); 98 | this.closeTimer = null; 99 | } 100 | } 101 | 102 | close = () => { 103 | this.clearCloseTimer(); 104 | this.props.onClose(); 105 | } 106 | ``` 107 | 108 | 这个文件中玄妙之处其实在于以上三个函数,在`componentDidMount`之时,添加了一个定时器,将在规定时间之后删除掉当前的这个提示窗,并且这个删除动作是交由给外层文件去删除当前这个提示框的实例进行的也就是第6点文件中的`remove`函数,在最新的(3.0.0)rc-notification中添加了以下代码,为了能够在鼠标移上去之后不让消息框消失,增加了用户体验度 109 | 110 | ```js 111 | componentDidMount() { 112 | this.startCloseTimer(); 113 | } 114 | 115 | componentWillUnmount() { 116 | this.clearCloseTimer(); 117 | } 118 | 119 | close = () => { 120 | this.clearCloseTimer(); 121 | this.props.onClose(); 122 | } 123 | 124 | startCloseTimer = () => { 125 | if (this.props.duration) { 126 | this.closeTimer = setTimeout(() => { 127 | this.close(); 128 | }, this.props.duration * 1000); 129 | } 130 | } 131 | 132 | clearCloseTimer = () => { 133 | if (this.closeTimer) { 134 | clearTimeout(this.closeTimer); 135 | this.closeTimer = null; 136 | } 137 | } 138 | 139 | render() { 140 | const props = this.props; 141 | const componentClass = `${props.prefixCls}-notice`; 142 | const className = { 143 | [`${componentClass}`]: 1, 144 | [`${componentClass}-closable`]: props.closable, 145 | [props.className]: !!props.className, 146 | }; 147 | return ( 148 |
151 |
{props.children}
152 | {props.closable ? 153 | 154 | 155 | : null 156 | } 157 |
158 | ); 159 | } 160 | ``` 161 | 162 | ## CreateChainedFunction 163 | 164 | 这个函数是使用在上面第6点,目的是为了能够删除当前的notification的缓存值,然后再执行外部传入的关闭回调函数,这个函数的实现在`rc-util`包中,这个包中有很多的方法是值得学习的,但是他在github上面的star数量却只有73个,这里软推一下吧。 165 | 166 | ```js 167 | export default function createChainedFunction() { 168 | const args = [].slice.call(arguments, 0); 169 | if (args.length === 1) { 170 | return args[0]; 171 | } 172 | 173 | return function chainedFunction() { 174 | for (let i = 0; i < args.length; i++) { 175 | if (args[i] && args[i].apply) { 176 | args[i].apply(this, arguments); 177 | } 178 | } 179 | }; 180 | } 181 | ``` 182 | 183 | 这个函数中使用了`call`来将传入的参数变成一个数组,然后使用`apply`将传入的函数一一执行,这样子就能够实现一个函数接受多个函数,然后按照顺序执行,并且在第6点的代码中`this.remove.bind(this, notice.key)`使用了`bind`函数制定了this和传入参数,方法很精妙也很经典。 184 | 185 | ## 缓存机制 186 | 187 | `notification`组件在`ant-design-master`中使用了 188 | 189 | ```js 190 | const notificationInstance = {}; 191 | 192 | destroy() { 193 | Object.keys(notificationInstance).forEach(cacheKey => { 194 | notificationInstance[cacheKey].destroy(); 195 | delete notificationInstance[cacheKey]; 196 | }); 197 | } 198 | ``` 199 | 来进行对创建实例的缓存,然后在销毁时将缓存的实例删除 200 | 201 | 在`notification 2.0.0`中也使用了缓存机制 202 | 203 | ```js 204 | add = (notice) => { 205 | const key = notice.key = notice.key || getUuid(); 206 | this.setState(previousState => { 207 | const notices = previousState.notices; 208 | if (!notices.filter(v => v.key === key).length) { 209 | return { 210 | notices: notices.concat(notice), 211 | }; 212 | } 213 | }); 214 | } 215 | 216 | remove = (key) => { 217 | this.setState(previousState => { 218 | return { 219 | notices: previousState.notices.filter(notice => notice.key !== key), 220 | }; 221 | }); 222 | } 223 | ``` 224 | 在这个代码中看到这个缓存机制是使用的数组的方式实现的,但是在外层封装却是用的是是对象的方式实现,我猜想这两个代码不是一个人写的。。。代码风格不同意呢。 225 | 226 | ## ReactDOM.unmountComponentAtNode 227 | 228 | ```js 229 | Notification.newInstance = function newNotificationInstance(properties) { 230 | const { getContainer, ...props } = properties || {}; 231 | let div; 232 | if (getContainer) { 233 | div = getContainer(); 234 | } else { 235 | div = document.createElement('div'); 236 | document.body.appendChild(div); 237 | } 238 | const notification = ReactDOM.render(, div); 239 | return { 240 | notice(noticeProps) { 241 | notification.add(noticeProps); 242 | }, 243 | removeNotice(key) { 244 | notification.remove(key); 245 | }, 246 | component: notification, 247 | destroy() { 248 | ReactDOM.unmountComponentAtNode(div); 249 | document.body.removeChild(div); 250 | }, 251 | }; 252 | }; 253 | ``` 254 | 255 | 从上面的代码中看出,`notification`组件使用`unmountComponentAtNode`函数将其进行销毁,这个方法适用于某些不能在当前组件中进行组件销毁的情况。 -------------------------------------------------------------------------------- /story/components/trigger_index.md: -------------------------------------------------------------------------------- 1 | # Trigger 2 | 3 | 这个组件的index文件就有很多代码,590行代码,而且在头部引入的额外文件特别的多,所以我们这一个组件就先从这些额外的组件中开始吧,先看看这些外部方法能够做些什么。 4 | 5 | > 强烈建议把tigger的代码下载下来自行查看,因为实在是太长了 6 | 7 | ```js 8 | // index.js 头部 9 | import PropTypes from 'prop-types'; 10 | import { findDOMNode, createPortal } from 'react-dom'; 11 | import createReactClass from 'create-react-class'; 12 | import contains from 'rc-util/lib/Dom/contains'; 13 | import addEventListener from 'rc-util/lib/Dom/addEventListener'; 14 | import Popup from './Popup'; 15 | import { getAlignFromPlacement, getPopupClassNameFromAlign } from './utils'; 16 | import getContainerRenderMixin from 'rc-util/lib/getContainerRenderMixin'; 17 | import Portal from 'rc-util/lib/Portal'; 18 | ``` 19 | 20 | ## createPortal 21 | 22 | 在官网[这里](https://reactjs.org/docs/react-dom.html#createportal)有这么一个解释 23 | 24 | ```js 25 | ReactDOM.createPortal(child, container) 26 | ``` 27 | 28 | `Creates a portal. Portals provide a way to render children into a DOM node that exists outside the hierarchy of the DOM component.` 29 | 30 | 这个函数是用来创建一个`portal`,而这个`Portal`是提供一个方法来在指定的dom元素渲染一些组件的方法。 31 | 32 | ## createReactClass 33 | 34 | 这个函数也是能够在官网[这里](https://reactjs.org/docs/react-without-es6.html)上找到的,是用来创建一个raect类而不是用es6语法的方法,在里面可以使用`getDefaultProps()`方法 35 | 36 | 创建当前组件的默认props,可以使用`getInitialState()`创建当前组件的初始state,并且在里面写的方法都会自动的绑定上`this`, 37 | 38 | 也就是他所说的`Autobinding`,还有一个最有用的属性`Mixins`,这个是能够在编写很多的能够使用的外部方法传入组件的属性。 39 | 40 | ## contains && addEventListener 41 | 42 | 这两个函数都是`rc-util/lib/Dom/`里面的工具函数,接下来我们分辨看看这两个函数能够做啥 43 | 44 | ```js 45 | // contains.js 46 | 47 | // 这个函数是用来判断传入根节点root是否包含传入节点n, 48 | // 如果包含则返回true,否者返回false 49 | export default function contains(root, n) { 50 | let node = n; 51 | while (node) { 52 | if (node === root) { 53 | return true; 54 | } 55 | node = node.parentNode; 56 | } 57 | 58 | return false; 59 | } 60 | ``` 61 | 62 | ```js 63 | // addEventListener.js 64 | // 这个函数主要的聚焦点是ReactDOM.unstable_batchedUpdates 65 | // 这个api是没有公开的一个api,但是可以使用,为了是想要将当前的组件状态强制性的 66 | // 更新到组件内部去并且,但是这样做的目的可能有点粗暴。。 67 | // 想要了解的可以看这篇文章,或许你有新的想法 68 | // https://zhuanlan.zhihu.com/p/20328570 69 | import addDOMEventListener from 'add-dom-event-listener'; 70 | import ReactDOM from 'react-dom'; 71 | 72 | export default function addEventListenerWrap(target, eventType, cb) { 73 | /* eslint camelcase: 2 */ 74 | const callback = ReactDOM.unstable_batchedUpdates ? function run(e) { 75 | ReactDOM.unstable_batchedUpdates(cb, e); 76 | } : cb; 77 | return addDOMEventListener(target, eventType, callback); 78 | } 79 | ``` 80 | 81 | ## getContainerRenderMixin && Portal 82 | 83 | 接下来是这两个函数,都是来自于`rc-util/lib/` 84 | 85 | ```js 86 | // getContainerRenderMixin.js 87 | 88 | import ReactDOM from 'react-dom'; 89 | 90 | function defaultGetContainer() { 91 | const container = document.createElement('div'); 92 | document.body.appendChild(container); 93 | return container; 94 | } 95 | 96 | export default function getContainerRenderMixin(config) { 97 | const { 98 | autoMount = true, 99 | autoDestroy = true, 100 | isVisible, 101 | getComponent, 102 | getContainer = defaultGetContainer, 103 | } = config; 104 | 105 | let mixin; 106 | 107 | function renderComponent(instance, componentArg, ready) { 108 | if (!isVisible || instance._component || isVisible(instance)) { 109 | // 如果有isVisible,并且传入的实例有_component,并且isVisible返回真则进行一下代码 110 | if (!instance._container) { 111 | // 如果传入实例没有_container,则为其添加一个默认的 112 | instance._container = getContainer(instance); 113 | } 114 | let component; 115 | if (instance.getComponent) { 116 | // 如果传入实例有getComponent,则将传入的参数传入实例的getComponent函数 117 | component = instance.getComponent(componentArg); 118 | } else { 119 | // 否则就进行就是用传入参数中的getComponent方法构造一个Component 120 | component = getComponent(instance, componentArg); 121 | } 122 | // unstable_renderSubtreeIntoContainer是更新组件到传入的DOM节点上 123 | // 可以使用它完成在组件内部实现跨组件的DOM操作 124 | // ReactComponent unstable_renderSubtreeIntoContainer( 125 | // parentComponent component, 126 | // ReactElement element, 127 | // DOMElement container, 128 | // [function callback] 129 | // ) 130 | ReactDOM.unstable_renderSubtreeIntoContainer(instance, 131 | component, instance._container, 132 | function callback() { 133 | instance._component = this; 134 | if (ready) { 135 | ready.call(this); 136 | } 137 | }); 138 | } 139 | } 140 | 141 | if (autoMount) { 142 | mixin = { 143 | ...mixin, 144 | // 如果是自动渲染组件,那就在DidMount和DidUpdate渲染组件 145 | componentDidMount() { 146 | renderComponent(this); 147 | }, 148 | componentDidUpdate() { 149 | renderComponent(this); 150 | }, 151 | }; 152 | } 153 | 154 | if (!autoMount || !autoDestroy) { 155 | mixin = { 156 | // 如果不是自动渲染的,那就在mixin中添加一个渲染函数 157 | ...mixin, 158 | renderComponent(componentArg, ready) { 159 | renderComponent(this, componentArg, ready); 160 | }, 161 | }; 162 | } 163 | 164 | function removeContainer(instance) { 165 | // 用于在挂载节点remove掉添加的组件 166 | if (instance._container) { 167 | const container = instance._container; 168 | // 先将组件unmount 169 | ReactDOM.unmountComponentAtNode(container); 170 | // 然后在删除挂载点 171 | container.parentNode.removeChild(container); 172 | instance._container = null; 173 | } 174 | } 175 | 176 | if (autoDestroy) { 177 | // 如果是自动销毁的,那就在WillUnmount的时候销毁 178 | mixin = { 179 | ...mixin, 180 | componentWillUnmount() { 181 | removeContainer(this); 182 | }, 183 | }; 184 | } else { 185 | mixin = { 186 | // 如果不是自动销毁,那就只是在mixin中添加一个销毁的函数 187 | ...mixin, 188 | removeContainer() { 189 | removeContainer(this); 190 | }, 191 | }; 192 | } 193 | // 最后返回构建好的mixin 194 | return mixin; 195 | } 196 | ``` 197 | 198 | ```js 199 | // Portal.js 200 | // 这个函数就像我们刚才上面所提到的Potal组件的一个编写,这样的组件非常有用 201 | // 我们可以利用这个组件创建在一些我们所需要创建组件的地方,比如在body节点创建 202 | // 模态框,或者在窗口节点创建fixed的定位的弹出框之类的。 203 | // 还有就是在用完这个组件也就是在componentWillUnmount的时候一定要将节点移除 204 | import React from 'react'; 205 | import PropTypes from 'prop-types'; 206 | import { createPortal } from 'react-dom'; 207 | 208 | export default class Portal extends React.Component { 209 | static propTypes = { 210 | getContainer: PropTypes.func.isRequired, 211 | children: PropTypes.node.isRequired, 212 | } 213 | 214 | componentDidMount() { 215 | this.createContainer(); 216 | } 217 | 218 | componentWillUnmount() { 219 | this.removeContainer(); 220 | } 221 | 222 | createContainer() { 223 | this._container = this.props.getContainer(); 224 | this.forceUpdate(); 225 | } 226 | 227 | removeContainer() { 228 | if (this._container) { 229 | this._container.parentNode.removeChild(this._container); 230 | } 231 | } 232 | 233 | render() { 234 | if (this._container) { 235 | return createPortal(this.props.children, this._container); 236 | } 237 | return null; 238 | } 239 | } 240 | ``` 241 | 242 | ## 在组件开始之前 243 | 244 | 在组件开始之前还有一些辅助的东西需要了解到 245 | 246 | ```js 247 | // 函数体的默认值 248 | function noop() { 249 | } 250 | 251 | function returnEmptyString() { 252 | return ''; 253 | } 254 | 255 | function returnDocument() { 256 | return window.document; 257 | } 258 | // 设置允许的事件,onContextMenu是右键菜单事件 259 | const ALL_HANDLERS = ['onClick', 'onMouseDown', 'onTouchStart', 'onMouseEnter', 260 | 'onMouseLeave', 'onFocus', 'onBlur', 'onContextMenu']; 261 | // 判断一下react的版本是不是react16 262 | const IS_REACT_16 = !!createPortal; 263 | // 判断是否是手机查看, 264 | // Navigator 对象包含有关浏览器的信息。 详情可以看这里http://www.w3school.com.cn/jsref/dom_obj_navigator.asp 265 | // 这里判断一下浏览器代理是不是移动端的代理。 266 | const isMobile = typeof navigator !== 'undefined' && !!navigator.userAgent.match( 267 | /(Android|iPhone|iPad|iPod|iOS|UCWEB)/i 268 | ); 269 | 270 | const mixins = []; 271 | // 判断一下,如果不是react16,就在mixin中自己添加一个类似于createPortal的函数 272 | if (!IS_REACT_16) { 273 | mixins.push( 274 | getContainerRenderMixin({ 275 | autoMount: false, 276 | 277 | isVisible(instance) { 278 | return instance.state.popupVisible; 279 | }, 280 | 281 | getContainer(instance) { 282 | return instance.getContainer(); 283 | }, 284 | }) 285 | ); 286 | } 287 | ``` 288 | ## Props 289 | 290 | 这个组件的传入参数非常的多,为了做兼容或者适应更多的使用者。 291 | ```js 292 | propTypes: { 293 | children: PropTypes.any, 294 | // 还记得我在dropdown里面留下的问题么,当时我问的是为什 295 | // 么触发可以试试一个数组,这里这个参数将会告诉你为什么, 296 | // 是可以让写在数组中的事件都成为其触发的事件。 297 | action: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), 298 | showAction: PropTypes.any, 299 | hideAction: PropTypes.any, 300 | getPopupClassNameFromAlign: PropTypes.any, 301 | onPopupVisibleChange: PropTypes.func, 302 | afterPopupVisibleChange: PropTypes.func, 303 | popup: PropTypes.oneOfType([ 304 | PropTypes.node, 305 | PropTypes.func, 306 | ]).isRequired, 307 | popupStyle: PropTypes.object, 308 | prefixCls: PropTypes.string, 309 | popupClassName: PropTypes.string, 310 | popupPlacement: PropTypes.string, 311 | builtinPlacements: PropTypes.object, 312 | popupTransitionName: PropTypes.oneOfType([ 313 | PropTypes.string, 314 | PropTypes.object, 315 | ]), 316 | popupAnimation: PropTypes.any, 317 | mouseEnterDelay: PropTypes.number, 318 | mouseLeaveDelay: PropTypes.number, 319 | zIndex: PropTypes.number, 320 | focusDelay: PropTypes.number, 321 | blurDelay: PropTypes.number, 322 | getPopupContainer: PropTypes.func, 323 | getDocument: PropTypes.func, 324 | destroyPopupOnHide: PropTypes.bool, 325 | mask: PropTypes.bool, 326 | maskClosable: PropTypes.bool, 327 | onPopupAlign: PropTypes.func, 328 | popupAlign: PropTypes.object, 329 | popupVisible: PropTypes.bool, 330 | maskTransitionName: PropTypes.oneOfType([ 331 | PropTypes.string, 332 | PropTypes.object, 333 | ]), 334 | maskAnimation: PropTypes.string, 335 | } 336 | ``` 337 | 338 | 参数很多,我直接将其参数作用拷贝过来了。 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 |
nametypedefaultdescription
popupClassNamestringadditional className added to popup
destroyPopupOnHidebooleanfalsewhether destroy popup when hide
getPopupClassNameFromAligngetPopupClassNameFromAlign(align: Object):Stringadditional className added to popup according to align
actionstring[]['hover']which actions cause popup shown. enum of 'hover','click','focus','contextMenu'
mouseEnterDelaynumber0delay time to show when mouse enter. unit: s.
mouseLeaveDelaynumber0.1delay time to hide when mouse leave. unit: s.
popupStyleObjectadditional style of popup
prefixClsStringrc-trigger-popupprefix class name
popupTransitionNameString|Objecthttps://github.com/react-component/animate
maskTransitionNameString|Objecthttps://github.com/react-component/animate
onPopupVisibleChangeFunctioncall when popup visible is changed
maskbooleanfalsewhether to support mask
maskClosablebooleantruewhether to support click mask to hide
popupVisiblebooleanwhether popup is visible
zIndexnumberpopup's zIndex
defaultPopupVisiblebooleanwhether popup is visible initially
popupAlignObject: alignConfig of [dom-align](https://github.com/yiminghe/dom-align)popup 's align config
onPopupAlignfunction(popupDomNode, align)callback when popup node is aligned
popupReact.Element | function() => React.Elementpopup content
getPopupContainergetPopupContainer(): HTMLElementfunction returning html node which will act as popup container
getDocumentgetDocument(): HTMLElementfunction returning document node which will be attached click event to close trigger
popupPlacementstringuse preset popup align config from builtinPlacements, can be merged by popupAlign prop
builtinPlacementsobjectbuiltin placement align map. used by placement prop
490 | 491 | 492 | ## Render() 493 | 494 | 我们依然还是从他的render函数作为突破点 495 | 496 | ```js 497 | render() { 498 | const { popupVisible } = this.state; 499 | const props = this.props; 500 | const children = props.children; 501 | // react.children.only 是检查是否只包含一个孩子节点,否则这个函数抛出错误 502 | const child = React.Children.only(children); 503 | // 这里添加key这个属性为了在后面返回数组的时候能够有一个key 504 | const newChildProps = { key: 'trigger' }; 505 | 506 | // 下面的所有的操作是给传出的trigger绑定事件 507 | 508 | if (this.isContextMenuToShow()) { 509 | newChildProps.onContextMenu = this.onContextMenu; 510 | } else { 511 | newChildProps.onContextMenu = this.createTwoChains('onContextMenu'); 512 | } 513 | 514 | if (this.isClickToHide() || this.isClickToShow()) { 515 | newChildProps.onClick = this.onClick; 516 | newChildProps.onMouseDown = this.onMouseDown; 517 | newChildProps.onTouchStart = this.onTouchStart; 518 | } else { 519 | newChildProps.onClick = this.createTwoChains('onClick'); 520 | newChildProps.onMouseDown = this.createTwoChains('onMouseDown'); 521 | newChildProps.onTouchStart = this.createTwoChains('onTouchStart'); 522 | } 523 | if (this.isMouseEnterToShow()) { 524 | newChildProps.onMouseEnter = this.onMouseEnter; 525 | } else { 526 | newChildProps.onMouseEnter = this.createTwoChains('onMouseEnter'); 527 | } 528 | if (this.isMouseLeaveToHide()) { 529 | newChildProps.onMouseLeave = this.onMouseLeave; 530 | } else { 531 | newChildProps.onMouseLeave = this.createTwoChains('onMouseLeave'); 532 | } 533 | if (this.isFocusToShow() || this.isBlurToHide()) { 534 | newChildProps.onFocus = this.onFocus; 535 | newChildProps.onBlur = this.onBlur; 536 | } else { 537 | newChildProps.onFocus = this.createTwoChains('onFocus'); 538 | newChildProps.onBlur = this.createTwoChains('onBlur'); 539 | } 540 | // 利用新的props构建一个新的trigger 541 | const trigger = React.cloneElement(child, newChildProps); 542 | 543 | // 判断是否是react16版本 不是就直接返回trigger 544 | if (!IS_REACT_16) { 545 | return trigger; 546 | } 547 | 548 | let portal; 549 | // prevent unmounting after it's rendered 550 | if (popupVisible || this._component) { 551 | portal = ( 552 | 556 | {this.getComponent()} 557 | 558 | ); 559 | } 560 | 561 | return [ 562 | trigger, 563 | portal, 564 | ]; 565 | }, 566 | ``` 567 | 568 | 在上面的代码中我们看这些函数 569 | 570 | ```js 571 | this.createTwoChains(); 572 | 573 | this.isContextMenuToShow(); 574 | 575 | this.isClickToHide(); 576 | 577 | this.isClickToShow(); 578 | 579 | this.isMouseEnterToShow(); 580 | 581 | this.isMouseLeaveToHide(); 582 | 583 | this.isFocusToShow(); 584 | 585 | this.isBlurToHide(); 586 | ``` 587 | 588 | 那么我们将来了解这些函数都干了什么 589 | 590 | + ### this.createTwoChains() 591 | ```js 592 | // 这个函数是给trigger组件绑定对应事件 593 | createTwoChains(event) { 594 | // 获取包裹元素的props 595 | const childPros = this.props.children.props; 596 | // 获取当前组件的props 597 | const props = this.props; 598 | // 如果子元素有这个事件类型并且trigger组件有这个事件类型 599 | // 就返回trigger组件中的对应的事件触发函数 600 | // 如果两者中有一方没有的话,就返回有的那一方的事件 601 | if (childPros[event] && props[event]) { 602 | return this[`fire${event}`]; 603 | } 604 | return childPros[event] || props[event]; 605 | } 606 | ``` 607 | + ### 判断事件是否需要添加 608 | ```js 609 | // 这几个函数的结构都是一样的 610 | this.isContextMenuToShow(); 611 | 612 | this.isClickToHide(); 613 | 614 | this.isClickToShow(); 615 | 616 | this.isMouseEnterToShow(); 617 | 618 | this.isMouseLeaveToHide(); 619 | 620 | this.isFocusToShow(); 621 | 622 | this.isBlurToHide(); 623 | 624 | // 这个函数是通过事件触发action来判断是否需要给组件绑定对应事件类型 625 | // 下面是伪代码 626 | isSomeEventToShowOrHide() { 627 | // 从传入props中的action和showAction中查询是否有这个事件类型 628 | // 有就返回true,否则返回false 629 | const { action, showActionOrHideAction } = this.props; 630 | return action.indexOf(event) !== -1 || showActionOrHideAction.indexOf(event) !== -1; 631 | } 632 | ``` 633 | 634 | ## 生命周期 635 | 636 | 在createTwoChains函数中我们又看见了一个新的函数`'this[`fire${event}`]'`, 637 | 638 | 这些函数都是在componentDidMount的时候构建成的,那么记下来顺理成章的我们应该转接到组件的生命周期 639 | 640 | 641 | ```js 642 | // 首先设置一个popupVisible作为state中的一个变量,方便下面使用 643 | getInitialState() { 644 | const props = this.props; 645 | let popupVisible; 646 | if ('popupVisible' in props) { 647 | popupVisible = !!props.popupVisible; 648 | } else { 649 | popupVisible = !!props.defaultPopupVisible; 650 | } 651 | return { 652 | popupVisible, 653 | }; 654 | }, 655 | 656 | componentWillMount() { 657 | // 给每一个事件都写上默认事件 658 | ALL_HANDLERS.forEach((h) => { 659 | this[`fire${h}`] = (e) => { 660 | this.fireEvents(h, e); 661 | }; 662 | }); 663 | }, 664 | 665 | componentDidMount() { 666 | // 在第一次渲染的时候强制性调用一下更新状态 667 | this.componentDidUpdate({}, { 668 | popupVisible: this.state.popupVisible, 669 | }); 670 | }, 671 | 672 | componentWillReceiveProps({ popupVisible }) { 673 | if (popupVisible !== undefined) { 674 | this.setState({ 675 | popupVisible, 676 | }); 677 | } 678 | }, 679 | 680 | componentDidUpdate(_, prevState) { 681 | const props = this.props; 682 | const state = this.state; 683 | const triggerAfterPopupVisibleChange = () => { 684 | if (prevState.popupVisible !== state.popupVisible) { 685 | props.afterPopupVisibleChange(state.popupVisible); 686 | } 687 | }; 688 | if (!IS_REACT_16) { 689 | // 如果不是react16版本就使用mixin中的函数渲染组件,并且能够执行外部afterPopupVisibleChange函数的回调 690 | this.renderComponent(null, triggerAfterPopupVisibleChange); 691 | } else { 692 | // 否则直接执行回调 693 | triggerAfterPopupVisibleChange(); 694 | } 695 | 696 | // We must listen to `mousedown`, edge case: 697 | // https://github.com/ant-design/ant-design/issues/5804 698 | // https://github.com/react-component/calendar/issues/250 699 | // https://github.com/react-component/trigger/issues/50 700 | if (state.popupVisible) { 701 | let currentDocument; 702 | if (!this.clickOutsideHandler && (this.isClickToHide() || this.isContextMenuToShow())) { 703 | currentDocument = props.getDocument(); 704 | this.clickOutsideHandler = addEventListener(currentDocument, 705 | 'mousedown', this.onDocumentClick); 706 | } 707 | // always hide on mobile 708 | // `isMobile` fix: mask clicked will cause below element events triggered 709 | // https://github.com/ant-design/ant-design-mobile/issues/1909 710 | // https://github.com/ant-design/ant-design-mobile/issues/1928 711 | if (!this.touchOutsideHandler && isMobile) { 712 | currentDocument = currentDocument || props.getDocument(); 713 | this.touchOutsideHandler = addEventListener(currentDocument, 714 | 'click', this.onDocumentClick); 715 | } 716 | // close popup when trigger type contains 'onContextMenu' and document is scrolling. 717 | if (!this.contextMenuOutsideHandler1 && this.isContextMenuToShow()) { 718 | currentDocument = currentDocument || props.getDocument(); 719 | this.contextMenuOutsideHandler1 = addEventListener(currentDocument, 720 | 'scroll', this.onContextMenuClose); 721 | } 722 | // close popup when trigger type contains 'onContextMenu' and window is blur. 723 | if (!this.contextMenuOutsideHandler2 && this.isContextMenuToShow()) { 724 | this.contextMenuOutsideHandler2 = addEventListener(window, 725 | 'blur', this.onContextMenuClose); 726 | } 727 | return; 728 | } 729 | // 清除所有外部的事件,因为上面为了解决一些issue而添加的事件 730 | this.clearOutsideHandler(); 731 | }, 732 | 733 | componentWillUnmount() { 734 | this.clearDelayTimer(); 735 | this.clearOutsideHandler(); 736 | }, 737 | ``` 738 | 739 | 可是看到现在也还是没有设么头绪,别忙这里先讲清楚一件事情,就是trigger这个组件的实现 740 | 741 | trigger组件由于其中的展示内容需要绝对定位,但是这些定位如果放在已经存在的dom结构中会很复杂很难实现统一,于是这里就将所有的需要定位的元素全部渲染在body的最后,这样子计算定位就很方便了,所以trigger组件的目的就是需要将呈现的东西给渲染在body之后,但是大家都知道,react的render只要一个入口,也就是最初的id为root的div,然后就是在这个div里面进行react开发,所以react为大家提供了一个函数,我们在上面的renderComponent()这个函数中也讲到`unstable_renderSubtreeIntoContainer()`,可以使用这个函数就能够将组件中的内容渲染在创造出的节点上并且追加在任何地方,一般是追加在body,也可以追加在指定的dom节点后面 742 | 743 | 接下来就是另一个分析的思路,因为我在看这些代码的时候开始也是混乱的,在经过查资料的过程中我也在思考,发现到一个点那就是这个组件在判断当前react版本是不是react16,并且根据上面所讲的trigger组件的实现原理,我恍然大悟,因为在react16之前没有`createPortal`这个API的,这个API其实就是trigger的原理实现,所以我就知道了,判断如果是react16版本的就使用react自己的API来创建挂载点,如果不是就利用mixin中的renderComponent()函数中的老的react的方法`unstable_renderSubtreeIntoContainer()`来创建挂载点以及挂载组件,那么接下来我们就来分析一下他的思路。 744 | 745 | ## IS__REACT__16? 746 | 747 | 上面既然说到了要从当前版本来进行操作,那么我就按照是与不是分别看看这个组件都做了哪些处理 748 | 749 | ### IS 750 | 751 | 首先就是从当前react版本是16开始,从render函数开始,在render函数中我们就谈到有一个判断 752 | 753 | ```js 754 | if (!IS_REACT_16) { 755 | return trigger; 756 | } 757 | 758 | let portal; 759 | // prevent unmounting after it's rendered 760 | if (popupVisible || this._component) { 761 | portal = ( 762 | 766 | {this.getComponent()} 767 | 768 | ); 769 | } 770 | 771 | return [ 772 | trigger, 773 | portal, 774 | ]; 775 | ``` 776 | 777 | 也就是在使用cloneElement生成完trigger组件之后,如果不是react16版本就直接返回了trigger,然后如果是react16版本就使用Protal组件将需要挂载的dom元素渲染出来,使用getContainer进行dom节点的创建,使用getComponent将弹出层渲染,最终挂载在getContainer创建的dom节点,然后append在body,这就是使用了react16版本的一个创建过程,其中Protal组件中就是用了react16中的createPortal,剩下的就是Popup,又是antd的另一个基层组件,需要去了解。 778 | 779 | ### NOT IS 780 | 781 | 如果大家和我一样看了源码之后也许会纳闷,如果不是react16版本的时候,就直接返回了trigger组件,那么他是在什么时刻去渲染弹出层以及弹出层的挂载节点的呢?接下来就是揭秘时间: 782 | 783 | ```js 784 | componentDidUpdate(_, prevState) { 785 | const props = this.props; 786 | const state = this.state; 787 | const triggerAfterPopupVisibleChange = () => { 788 | if (prevState.popupVisible !== state.popupVisible) { 789 | props.afterPopupVisibleChange(state.popupVisible); 790 | } 791 | }; 792 | if (!IS_REACT_16) { 793 | // 如果不是react16版本就使用mixin中的函数渲染组件,并且能够执行外部afterPopupVisibleChange函数的回调 794 | this.renderComponent(null, triggerAfterPopupVisibleChange); 795 | } else { 796 | // 否则直接执行回调 797 | triggerAfterPopupVisibleChange(); 798 | } 799 | 800 | // 一些无关紧要的code ... 801 | } 802 | ``` 803 | 804 | 在componentDidUpdate中在不是react16的时候使用了一个renderComponent函数,那么这个函数又是哪里来的呢,我们继续往上追溯,我们发现在上面讲到的getContainerRenderMixin中有这样的一断代码 805 | 806 | ```js 807 | if (!autoMount || !autoDestroy) { 808 | mixin = { 809 | // 如果不是自动渲染的,那就在mixin中添加一个渲染函数 810 | ...mixin, 811 | renderComponent(componentArg, ready) { 812 | renderComponent(this, componentArg, ready); 813 | }, 814 | }; 815 | } 816 | ``` 817 | 818 | 那么知道mixin作用的同学就应该知道了,上面的componentDidUpdate中使用的renderComponent函数是在哪里定义的了,接下来就直接分析这个mixin中干了什么 819 | 820 | 首先是我们在使用的时候传入了这些参数; 821 | 822 | ```js 823 | getContainerRenderMixin({ 824 | autoMount: false, 825 | 826 | isVisible(instance) { 827 | return instance.state.popupVisible; 828 | }, 829 | 830 | getContainer(instance) { 831 | return instance.getContainer(); 832 | }, 833 | }) 834 | ``` 835 | 836 | 这里不得不再讲一遍这个getContainerRenderMixin 837 | 838 | ```js 839 | import ReactDOM from 'react-dom'; 840 | 841 | function defaultGetContainer() { 842 | const container = document.createElement('div'); 843 | document.body.appendChild(container); 844 | return container; 845 | } 846 | 847 | export default function getContainerRenderMixin(config) { 848 | // 首先传了三个参数进来,autoMount = false, isVisible(func), getContainer(func) 849 | const { 850 | autoMount = true, 851 | autoDestroy = true, 852 | isVisible, 853 | getComponent, 854 | getContainer = defaultGetContainer, 855 | } = config; 856 | 857 | let mixin; 858 | 859 | function renderComponent(instance, componentArg, ready) { 860 | // 当外部传入的状态为显示,并且外部的实例有_component(这个_component是在传入外部的Popup组件的ref所指向的节点) 861 | if (!isVisible || instance._component || isVisible(instance)) { 862 | if (!instance._container) { 863 | // trigger组件没有_container,默认创建一个 864 | instance._container = getContainer(instance); 865 | } 866 | let component; 867 | if (instance.getComponent) { 868 | // 如果传入实例有getComponent,则将传入的参数传入实例的getComponent函数 869 | component = instance.getComponent(componentArg); 870 | } else { 871 | // 否则就进行就是用传入参数中的getComponent方法构造一个Component 872 | component = getComponent(instance, componentArg); 873 | } 874 | // unstable_renderSubtreeIntoContainer是更新组件到传入的DOM节点上 875 | // 可以使用它完成在组件内部实现跨组件的DOM操作 876 | // ReactComponent unstable_renderSubtreeIntoContainer( 877 | // parentComponent component, 878 | // ReactElement element, 879 | // DOMElement container, 880 | // [function callback] 881 | // ) 882 | // 最终使用这个方法将弹出层挂载点以及弹出层进行渲染,然后还能够触发一个弹出层弹出之后的回调,感觉这个回调走得好绕。。。 883 | ReactDOM.unstable_renderSubtreeIntoContainer(instance, 884 | component, instance._container, 885 | function callback() { 886 | instance._component = this; 887 | if (ready) { 888 | ready.call(this); 889 | } 890 | }); 891 | } 892 | } 893 | 894 | // trigger组件传入的autoMount为false所以这一段我们不需要再看 895 | if (autoMount) { 896 | mixin = { 897 | ...mixin, 898 | // 如果是自动渲染组件,那就在DidMount和DidUpdate渲染组件 899 | componentDidMount() { 900 | renderComponent(this); 901 | }, 902 | componentDidUpdate() { 903 | renderComponent(this); 904 | }, 905 | }; 906 | } 907 | 908 | // 这里是入口, 909 | if (!autoMount || !autoDestroy) { 910 | mixin = { 911 | // 如果不是自动渲染的,那就在mixin中添加一个渲染函数 912 | ...mixin, 913 | renderComponent(componentArg, ready) { 914 | // 这里的this也就是当前mixin插入的类,componentArg是外部传入的null,raedy是外部传入的callback 915 | // 再次回到上面的renderComponent函数 916 | renderComponent(this, componentArg, ready); 917 | }, 918 | }; 919 | } 920 | 921 | function removeContainer(instance) { 922 | // 用于在挂载节点remove掉添加的组件 923 | if (instance._container) { 924 | const container = instance._container; 925 | // 先将组件unmount 926 | ReactDOM.unmountComponentAtNode(container); 927 | // 然后在删除挂载点 928 | container.parentNode.removeChild(container); 929 | instance._container = null; 930 | } 931 | } 932 | 933 | if (autoDestroy) { 934 | // 如果是自动销毁的,那就在WillUnmount的时候销毁 935 | mixin = { 936 | ...mixin, 937 | componentWillUnmount() { 938 | removeContainer(this); 939 | }, 940 | }; 941 | } else { 942 | mixin = { 943 | // 如果不是自动销毁,那就只是在mixin中添加一个销毁的函数 944 | ...mixin, 945 | removeContainer() { 946 | removeContainer(this); 947 | }, 948 | }; 949 | } 950 | // 最后返回构建好的mixin 951 | return mixin; 952 | } 953 | ``` 954 | 955 | 这样trigger组件的一个大致构造思路以及大部分代码就已经进行了解读,剩余的部分都是进行的状态控制,antd为了适应手机还所以在状态控制上面写了很多函数,不多都是简单的函数,而且有的函数仅仅只是为了出一些出现的issue,感觉有点hotfix的意味,反正希望看完这一节对于大家制作react弹出层有一定的了解,这里我就提出两点 956 | 957 | 1. react16版本之前的需要自己写一个弹出层挂载点 958 | 2. react16版本之后的可以使用react提供的createPortal进行挂载点的处理 959 | 960 | 当然这个弹出层不仅仅是小的弹出层,可以制作很多东西,模态框,提醒框,下拉菜单,tooltip等等只要是需要绝对定位在某一个元素的某一个位置的场景,尽量发挥想象吧。 961 | -------------------------------------------------------------------------------- /story/components/trigger_popup.md: -------------------------------------------------------------------------------- 1 | # Trigger > Popup 2 | 3 | 在之前的trigger的index文件中,其渲染的弹出层就是这个Popup组件,这篇文章将会为你揭晓Popup。 4 | 5 | ## 提纲 6 | 7 | 这个组件中我们主要路线是这样的 8 | 9 | Popup组件使用了两个另外的rc-component,分别是`rc-align`,`rc-animate`,这两个组件我们目前只需要知道他们能够做什么就好了。 10 | 11 | 然后是这些重点 12 | 13 | ```js 14 | // 方法 15 | saveRef(); 16 | React.findDOMNode(); 17 | getPopupElement(); 18 | getMaskElement(); 19 | 20 | // 额外组件 21 | PopupInner 22 | LazyRenderBox 23 | ``` 24 | 25 | 下面的文章将围绕这些重点展开,解决了这些疑点之后,就可以对Popup组件有所了解 26 | 27 | ## 外部插件了解 28 | 29 | 这里又有两个外部的插件,也是antd写的基层组件,一个是rc-align组件,这个是用于定位的组件,另外一个rc-animate组件,是用于动画特效的组件,我们之后一定会讲到这两个组件的,因为这是基层组件,是整个antd都会使用到的。 30 | 31 | ### rc-align 32 | 33 | 对于组件,我们一般要用的话只需要了解他的api就好 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
nametypedefaultdescription
alignObjectsame with alignConfig from https://github.com/yiminghe/dom-align
onAlignfunction(source:HTMLElement, align:Object)called when align
targetfunction():HTMLElementfunction(){return window;}a function which returned value is used for target from https://github.com/yiminghe/dom-align
monitorWindowResizeBooleanfalsewhether realign when window is resized
71 | 72 | 打开了他所用的定位的[alignConfig](https://github.com/yiminghe/dom-align),不由得在想是不是他们自己写的一套定位的方案。。。 73 | 74 | ### rc-animate 75 |  76 | 这是一个动画组件,嵌套的组件可以被添加上动画特效 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 143 | 144 | 145 |
nametypedefaultdescription
componentReact.Element/String'span'wrap dom node or component for children. set to '' if you do not wrap for only one child
showPropStringusing prop for show and hide. [demo](http://react-component.github.io/animate/examples/hide-todo.html)
exclusiveBooleanwhether allow only one set of animations(enter and leave) at the same time.
transitionNameStringtransitionName, need to specify corresponding css
transitionAppearBooleanfalsewhether support transition appear anim
transitionEnterBooleantruewhether support transition enter anim
transitionLeaveBooleantruewhether support transition leave anim
onEndfunction(key:String, exists:Boolean)trueanimation end callback
animationObject{} 141 | to animate with js. see animation format below. 142 |
146 | 147 | 这里有一个参数`showProp`我开始没明白是啥意思,后面才看了他的实例才知道是使用哪一个prop作为显示还是隐藏的开关判断 -------------------------------------------------------------------------------- /story/index.js.md: -------------------------------------------------------------------------------- 1 | # Index.js 2 | 看一个代码的时候首先当然是从他的入口文件开始看起,所以第一份代码我们看的是`/index.js`文件 3 | 4 | # 开始 5 | 打开`index.js`文件,代码只有28行,其中包含了一个`camelCase`函数(看函数名就知道这是个给名称进行驼峰命名法的函数),一个`req`变量,以及这个的变量操作和`export`操作 6 | 7 | 在这个文件里面我首先查了`require.context()`这个函数的使用,可以参考[这里](https://juejin.im/entry/590c2777128fe10058392598),以及`exports`和`module.exports`的区别,可以参考[这里](https://cnodejs.org/topic/5231a630101e574521e45ef8),这里是一些铺垫,下面进入正题 8 | 9 | 通过上面两个铺垫,我们知道了`req`这个变量是用来循环抛出组件的一个对象,并且还抛出了每一个组件的样式文件 10 | 11 | ```js 12 | // index.js 13 | function camelCase(name) { 14 | return name.charAt(0).toUpperCase() + 15 | name.slice(1).replace(/-(\w)/g, (m, n) => { 16 | return n.toUpperCase(); 17 | }); 18 | } 19 | 20 | // 抛出样式 这个正则是匹配当前目录下的所有的/style/index.tsx文件 21 | const req = require.context('./components', true, /^\.\/[^_][\w-]+\/style\/index\.tsx?$/); 22 | 23 | req.keys().forEach((mod) => { 24 | let v = req(mod); 25 | if (v && v.default) { 26 | v = v.default; 27 | } 28 | // 抛出组件 这个正则是匹配当前目录下的素有index.tsx文件 29 | const match = mod.match(/^\.\/([^_][\w-]+)\/index\.tsx?$/); 30 | if (match && match[1]) { 31 | if (match[1] === 'message' || match[1] === 'notification') { 32 | // message & notification should not be capitalized 33 | exports[match[1]] = v; 34 | } else { 35 | exports[camelCase(match[1])] = v; 36 | } 37 | } 38 | }); 39 | 40 | module.exports = require('./components'); 41 | ``` 42 | 43 | 但是最后不知道为甚还需要加上对吼那一句`module.exports = require('./components');` 44 | 既然上面都已经抛出,为什么这里还需要再次抛出,不过好像是跟什么环境和打包之后的一些操作有关,所以这里一两次抛出。这个地方还需要向大家请教。 45 | 46 | -------------------------------------------------------------------------------- /typeface.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangzewei/read-antd-code/14e7e376417867bc75ee8ff3ade8765a00c58f7b/typeface.zip --------------------------------------------------------------------------------