├── .babelrc ├── .gitignore ├── README.md ├── images ├── button-with-default-props.png ├── generic-toggleable-demo.gif ├── hoc-toggleable.png ├── inject-component-toggleable.png ├── render-callback-collapse-demo.gif ├── render-callback-collapse.png ├── render-callback-panel.png ├── render-callback-toggleable-children.png ├── render-callback-toggleable-render.png ├── render-callback.png ├── state-readonly.gif ├── stateful.png ├── stateless.png └── with-default-props.png ├── package-lock.json ├── package.json ├── src ├── Demo.tsx ├── defaultProps │ └── ButtonWithDefaultProps.tsx ├── generic │ ├── Collapse.tsx │ ├── PanelItem.tsx │ ├── PanelViaInjection.tsx │ └── Toggleable.tsx ├── hoc │ ├── Collapse.tsx │ ├── PanelItem.tsx │ ├── PanelViaHOC.tsx │ ├── Toggleable.tsx │ └── withToggleable.tsx ├── injectedComponent │ ├── Collapse.tsx │ ├── PanelItem.tsx │ ├── PanelViaInjection.tsx │ └── Toggleable.tsx ├── renderCallback │ ├── Collapse.tsx │ ├── Panel.tsx │ └── Toggleable.tsx ├── server │ ├── server.js │ ├── views │ │ └── demo.ejs │ └── www.js ├── stateful │ └── CounterButton.tsx ├── stateless │ └── Button.tsx ├── tsconfig.json ├── types │ └── global.d.ts └── utils.ts └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "react", 5 | "stage-0" 6 | ] 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # CMake 7 | cmake-build-debug/ 8 | cmake-build-release/ 9 | 10 | # Mongo Explorer plugin 11 | .idea/**/mongoSettings.xml 12 | 13 | # File-based project format 14 | *.iws 15 | 16 | # IntelliJ 17 | out/ 18 | 19 | # mpeltonen/sbt-idea plugin 20 | .idea_modules/ 21 | 22 | # JIRA plugin 23 | atlassian-ide-plugin.xml 24 | 25 | # Cursive Clojure plugin 26 | .idea/replstate.xml 27 | 28 | # Crashlytics plugin (for Android Studio and IntelliJ) 29 | com_crashlytics_export_strings.xml 30 | crashlytics.properties 31 | crashlytics-build.properties 32 | fabric.properties 33 | 34 | # Editor-based Rest Client 35 | .idea/httpRequests 36 | ### macOS template 37 | # General 38 | .DS_Store 39 | .AppleDouble 40 | .LSOverride 41 | 42 | # Icon must end with two \r 43 | Icon 44 | 45 | # Thumbnails 46 | ._* 47 | 48 | # Files that might appear in the root of a volume 49 | .DocumentRevisions-V100 50 | .fseventsd 51 | .Spotlight-V100 52 | .TemporaryItems 53 | .Trashes 54 | .VolumeIcon.icns 55 | .com.apple.timemachine.donotpresent 56 | 57 | # Directories potentially created on remote AFP share 58 | .AppleDB 59 | .AppleDesktop 60 | Network Trash Folder 61 | Temporary Items 62 | .apdisk 63 | ### Windows template 64 | # Windows thumbnail cache files 65 | Thumbs.db 66 | ehthumbs.db 67 | ehthumbs_vista.db 68 | 69 | # Dump file 70 | *.stackdump 71 | 72 | # Folder config file 73 | [Dd]esktop.ini 74 | 75 | # Recycle Bin used on file shares 76 | $RECYCLE.BIN/ 77 | 78 | # Windows Installer files 79 | *.cab 80 | *.msi 81 | *.msix 82 | *.msm 83 | *.msp 84 | 85 | # Windows shortcuts 86 | *.lnk 87 | 88 | .idea/ 89 | ### Node template 90 | # Logs 91 | logs 92 | *.log 93 | npm-debug.log* 94 | yarn-debug.log* 95 | yarn-error.log* 96 | 97 | # Runtime data 98 | pids 99 | *.pid 100 | *.seed 101 | *.pid.lock 102 | 103 | # Directory for instrumented libs generated by jscoverage/JSCover 104 | lib-cov 105 | 106 | # Coverage directory used by tools like istanbul 107 | coverage 108 | 109 | # nyc test coverage 110 | .nyc_output 111 | 112 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 113 | .grunt 114 | 115 | # Bower dependency directory (https://bower.io/) 116 | bower_components 117 | 118 | # node-waf configuration 119 | .lock-wscript 120 | 121 | # Compiled binary addons (https://nodejs.org/api/addons.html) 122 | build/Release 123 | 124 | # Dependency directories 125 | node_modules/ 126 | jspm_packages/ 127 | 128 | # TypeScript v1 declaration files 129 | typings/ 130 | 131 | # Optional npm cache directory 132 | .npm 133 | 134 | # Optional eslint cache 135 | .eslintcache 136 | 137 | # Optional REPL history 138 | .node_repl_history 139 | 140 | # Output of 'npm pack' 141 | *.tgz 142 | 143 | # Yarn Integrity file 144 | .yarn-integrity 145 | 146 | # dotenv environment variables file 147 | .env 148 | 149 | # next.js build output 150 | .next 151 | 152 | /dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## With TypeScript 2.8+ :更好的 React 组件开发模式 2 | 3 | 近两年来一直在关注 React 开发,最近也开始全面应用 TypeScript 。国内有很多讲解 React 和 TypeScript 的教程,但如何将 TypeScript 更好地应用到 React 组件开发模式的文章却几乎没有(也可能是我没找到),特别是 TS 的一些新特性,如:条件类型、条件类型中的类型引用等。这些新特性如何应用到 React 组件开发?没办法只能去翻一些国外的文章,结合 TS 的官方文档慢慢摸索... 于是就有了想法把这个过程整理成文档。 4 | 5 | 本文内容很长,希望你有个舒服的椅子,我们马上开始。 6 | 7 | > 所有代码均使用 React 16.3、TypeScript 2.9 + strict mode 编写 8 | 9 | 10 | 11 | ## 开始 12 | 13 | 本文假设你已经对 React、TypeScript 有一定的了解。我不会讲到例如:webpack 打包、Babel 转码、TypeScript 编译选项这一类的问题,而将一切焦点放在如何将 TS 2.8+ 更好地应用到 React 组件设计模式中。 14 | 15 | 首先,我们从无状态组件开始。 16 | 17 | 18 | 19 | 20 | 21 | ## 无状态组件 22 | 23 | 无状态组件就是没有 `state` 的,通常我们也叫做纯函数组件。用原生 JS 我们可以这样写一个按钮组件: 24 | 25 | ```typescript 26 | import React from 'react'; 27 | 28 | const Button = ({onClick: handleClick, children}) => ( 29 | 30 | ); 31 | ``` 32 | 33 | 34 | 35 | 如果你把代码直接放到 `.tsx` 文件中,`tsc` 编译器马上会提示错误:有隐含的 any 类型,因为用了严格模式。我们必须明确的定义组件属性,修改一下: 36 | 37 | ```typescript 38 | import React, { MouseEvent, ReactNode } from 'react'; 39 | 40 | interface Props { 41 | onClick(e: MouseEvent): void; 42 | children?: ReactNode; 43 | }; 44 | 45 | const Button = ({ onClick: handleClick, children }: Props) => ( 46 | 47 | ); 48 | ``` 49 | 50 | 51 | 52 | OK,错误没有了!好像已经完事了?其实再花点心思可以做的更好。 53 | 54 | React 中有个预定义的类型,`SFC` : 55 | 56 | ```typescript 57 | type SFC

= StatelessComponent

; 58 | ``` 59 | 60 | 他是 `StatelessComponent` 的一个别名,而 `StatelessComponent` 声明了纯函数组件的一些预定义示例属性和静态属性,如:`children`、`defaultProps`、`displayName` 等,所以我们不需要自己写所有的东西! 61 | 62 | 63 | 64 | 最后我们的代码是这样的: 65 | 66 | ![](images/stateless.png) 67 | 68 | 69 | 70 | 71 | 72 | ## 有状态的类组件 73 | 74 | 接着我们来创建一个计数器按钮组件。首先我们定义初始状态: 75 | 76 | ```typescript 77 | const initialState = {count: 0}; 78 | ``` 79 | 80 | 81 | 82 | 然后,定义一个别名 `State` 并用 TS 推断出类型: 83 | 84 | ```typescript 85 | type State = Readonly; 86 | ``` 87 | 88 | > 知识点:这样做不用分开维护接口声明和实现代码,比较实用的技巧 89 | 90 | 91 | 92 | 同时应该注意到,我们将所有的状态属性声明为 `readonly` 。然后我们需要明确定义 state 为组件的实例属性: 93 | 94 | ```typescript 95 | readonly state: State = initialState; 96 | ``` 97 | 98 | 为什么要这样做?我们知道在 React 中我们不能直接改变 `State` 的属性值或者 `State` 本身: 99 | 100 | ```typescript 101 | this.state.count = 1; 102 | this.state = {count: 1}; 103 | ``` 104 | 105 | 如果这样做在运行时将会抛出错误,但在编写代码时却不会。所以我们需要明确的声明 `readonly` ,这样 TS 会让我们知道如果执行了这种操作就会出错了: 106 | 107 | ![](images/state-readonly.gif) 108 | 109 | 110 | 111 | 下面是完整的代码: 112 | 113 | > 这个组件不需要外部传递任何 `Props` ,所以泛型的第一个参数给的是不带任何属性的对象 114 | 115 | ![](images/stateful.png) 116 | 117 | 118 | 119 | 120 | 121 | ## 属性默认值 122 | 123 | 让我们来扩展一下纯函数按钮组件,加上一个颜色属性: 124 | 125 | ```typescript 126 | interface Props { 127 | onClick(e: MouseEvent): void; 128 | color: string; 129 | } 130 | ``` 131 | 132 | 如果想要定义属性默认值的话,我们知道可以通过 `Button.defaultProps = {...}` 做到。并且我们需要把这个属性声明为可选属性:(注意属性名后的 `?` ) 133 | 134 | ```typescript 135 | interface Props { 136 | onClick(e: MouseEvent): void; 137 | color?: string; 138 | } 139 | ``` 140 | 141 | 142 | 143 | 那么组件现在看起来是这样的: 144 | 145 | ```typescript 146 | const Button: SFC = ({onClick: handleClick, color, children}) => ( 147 | 148 | ); 149 | ``` 150 | 151 | 一切看起来好像都很简单,但是这里有一个“痛点”。注意我们使用了 TS 的严格模式,`color?: string` 这个可选属性的类型现在是联合类型 -- `string | undefined` 。 152 | 153 | 这意味着什么?如果你要对这种属性进行一些操作,比如:`substr()` ,TS 编译器会直接报错,因为类型有可能是 `undefined` ,TS 并不知道属性默认值会由 `Component.defaultProps` 来创建。 154 | 155 | 156 | 157 | 碰到这种情况我们一般用两种方式来解决: 158 | 159 | - 使用类型断言手动去除,添加 `!` 后缀,像这样:`color!.substr(...)` 。 160 | - 使用条件判断或者三元操作符让 TS 编译器知道这个属性不是 undefined,比如: `if (color) ...` 。 161 | 162 | 163 | 164 | 以上的方式虽然可以工作但有种多此一举的感觉,毕竟默认值已经有了只是 TS 编译器“不知道”而已。下面来说一种可重用的方案:我们写一个 `withDefaultProps` 函数,利用 TS 2.8 的条件类型映射,可以很简单的完成: 165 | 166 | ![](images/with-default-props.png) 167 | 168 | 这里涉及到两个 type 定义,写在 `src/types/global.d.ts` 文件里面: 169 | 170 | ```typescript 171 | declare type DiffPropertyNames = 172 | { [P in T]: P extends U ? never: P }[T]; 173 | 174 | declare type Omit = Pick>; 175 | ``` 176 | 177 | 看一下 [TS 2.8 的新特性说明](http://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html) 关于 `Conditional Types` 的说明,就知道这两个 `type` 的原理了。 178 | 179 | > 注意 TS 2.9 的新变化:`keyof T` 的类型是 `string | number | symbol ` 的结构子类型。 180 | 181 | 182 | 183 | 现在我们可以利用 `withDefaultProps` 函数来写一个有属性默认值的组件了: 184 | 185 | ![](images/button-with-default-props.png) 186 | 187 | 现在使用这个组件时默认值属性已经发生作用,是可选的;并且在组件内部使用这些默认值属性不用再手动断言了,这些默认值属性就是必填属性!感觉还不错对吧 :smile: 188 | 189 | > `withDefautProps` 函数同样可以应用在 `stateful` 有状态的类组件上。 190 | 191 | 192 | 193 | 194 | 195 | ## 渲染回调模式 196 | 197 | 有一种重用组件逻辑的设计方式是:把组件的 `children` 写成渲染回调函数或者暴露一个 `render` 函数属性出来。我们将用这种思路来做一个折叠面板的场景应用。 198 | 199 | 首先我们先写一个 `Toggleable` 组件,完整的代码如下: 200 | 201 | ![](images/render-callback.png) 202 | 203 | 204 | 205 | 下面我们来逐段解释下这段代码,首先先看到组件的属性声明相关部分: 206 | 207 | ```typescript 208 | type Props = Partial<{ 209 | children: RenderCallback; 210 | render: RenderCallback; 211 | }>; 212 | 213 | type RenderCallback = (args: ToggleableRenderArgs) => React.ReactNode; 214 | 215 | type ToggleableRenderArgs = { 216 | show: boolean; 217 | toggle: Toggleable['toggle']; 218 | } 219 | ``` 220 | 221 | 我们需要同时支持 `children` 或 `render` 函数属性,所以这两个要声明为可选的属性。注意这里用了 `Partial` 映射类型,这样就不需要每个手动 `?` 操作符来声明可选了。 222 | 223 | 为了保持 ***DRY*** 原则(Don't repeat yourself ),我们还声明了 `RenderCallback` 类型。 224 | 225 | 最后,我们将这个回调函数的参数声明为一个独立的类型:`ToggleableRenderArgs` 。 226 | 227 | 注意我们使用了 TS 的**查找类型**(*lookup types* ),这样 `toggle` 的类型将和类中定义的同名方法类型保持一致: 228 | 229 | ```typescript 230 | private toggle = (event: MouseEvent) => { 231 | this.setState(prevState => ({show: !prevState.show})); 232 | }; 233 | ``` 234 | 235 | > 同样是为了 DRY ,TS 非常给力! 236 | 237 | 238 | 239 | 接下来是 State 相关的: 240 | 241 | ```typescript 242 | const initialState = {show: false}; 243 | type State = Readonly; 244 | ``` 245 | 246 | 这个没什么特别的,跟前面的例子一样。 247 | 248 | 249 | 250 | 剩下的部分就是 渲染回调 设计模式了,代码很好理解: 251 | 252 | ```typescript 253 | class Toggleable extends Component { 254 | 255 | // ... 256 | 257 | render() { 258 | const {children, render} = this.props; 259 | const {show} = this.state; 260 | const renderArgs = {show, toggle: this.toggle}; 261 | 262 | if (render) { 263 | return render(renderArgs); 264 | } else if (isFunction(children)) { 265 | return children(renderArgs); 266 | } else { 267 | return null; 268 | } 269 | } 270 | 271 | // ... 272 | } 273 | ``` 274 | 275 | 276 | 277 | 现在我们可以将 children 作为一个渲染函数传递给 Toggleable 组件: 278 | 279 | ![](images/render-callback-toggleable-children.png) 280 | 281 | 或者将渲染函数传递给 render 属性: 282 | 283 | ![](images/render-callback-toggleable-render.png) 284 | 285 | 286 | 287 | 下面我们来完成折叠面板剩下的工作,先写一个 Panel 组件来重用 Toggleable 的逻辑: 288 | 289 | ![](images/render-callback-panel.png) 290 | 291 | 最后写一个 Collapse 组件来完成这个应用: 292 | 293 | ![](images/render-callback-collapse.png) 294 | 295 | 296 | 297 | 这里我们不谈样式的事情,运行起来看看,跟期待的效果是否一致? 298 | 299 | ![](images/render-callback-collapse-demo.gif) 300 | 301 | > 这种方式对于需要扩展渲染内容时非常有用:Toggleable 组件并不知道也不关心具体的渲染内容,但他控制着显示状态逻辑! 302 | 303 | 304 | 305 | 306 | 307 | ## 组件注入模式 308 | 309 | 为了使组件逻辑更具伸缩性,下面我们来说说组件注入模式。 310 | 311 | 312 | 313 | 那么什么是组件注入模式呢?如果你用过 `React-Router` ,你已经使用过这种模式来定义路由了: 314 | 315 | ```jsx 316 | 317 | ``` 318 | 319 | 320 | 321 | 不同于渲染回调模式,我们使用 `component` 属性“注入”一个组件。为了演示这个模式是如何工作的,我们将重构折叠面板这个场景,首先写一个可重用的 PanelItem 组件: 322 | 323 | ```typescript 324 | import { ToggleableComponentProps } from './Toggleable'; 325 | 326 | type PanelItemProps = { title: string }; 327 | 328 | const PanelItem: SFC = props => { 329 | const {title, children, show, toggle} = props; 330 | 331 | return ( 332 |

333 |

{title}

334 | {show ? children : null} 335 |
336 | ); 337 | }; 338 | ``` 339 | 340 | 341 | 342 | 然后重构 Toggleable 组件:加入新的 `component` 属性。对比先头的代码,我们需要做出如下变化: 343 | 344 | - `children` 属性类型更改为 function 或者 ReactNode(当使用 `component` 属性时) 345 | - `component` 属性将传递一个组件注入进去,这个注入组件的属性定义上需要有 `ToggleableComponentProps` (其实是原来的 `ToggleableRenderArgs` ,还记得吗?) 346 | - 还需要定义一个 ` props` 属性,这个属性将用来传递注入组件需要的属性值。我们会设置 `props` 可以拥有任意的属性,因为我们并不知道注入组件会有哪些属性,当然这样我们会丢失 TS 的严格类型检查... 347 | 348 | ```typescript 349 | const defaultInjectedProps = {props: {} as { [propName: string]: any }}; 350 | type DefaultInjectedProps = typeof defaultInjectedProps; 351 | type Props = Partial<{ 352 | children: RenderCallback | ReactNode; 353 | render: RenderCallback; 354 | component: ComponentType> 355 | } & DefaultInjectedProps>; 356 | ``` 357 | 358 | 359 | 360 | 下一步我们把原来的 `ToggleableRenderArgs` 修改为 `ToggleableComponentProps` ,允许将注入组件需要的属性通过 ` ` 这样来传递: 361 | 362 | ```typescript 363 | type ToggleableComponentProps

= { 364 | show: boolean; 365 | toggle: Toggleable['toggle']; 366 | } & P; 367 | ``` 368 | 369 | 370 | 371 | 现在我们还需要重构一下 `render` 方法: 372 | 373 | ```typescript 374 | render() { 375 | const {component: InjectedComponent, children, render, props} = this.props; 376 | const {show} = this.state; 377 | const renderProps = {show, toggle: this.toggle}; 378 | 379 | if (InjectedComponent) { 380 | return ( 381 | 382 | {children} 383 | 384 | ); 385 | } 386 | 387 | if (render) { 388 | return render(renderProps); 389 | } else if (isFunction(children)) { 390 | return children(renderProps); 391 | } else { 392 | return null; 393 | } 394 | } 395 | ``` 396 | 397 | 398 | 399 | 我们已经完成了整个 Toggleable 组件的修改,下面是完整的代码: 400 | 401 | ![](images/inject-component-toggleable.png) 402 | 403 | 404 | 405 | 最后我们写一个 `PanelViaInjection` 组件来应用组件注入模式: 406 | 407 | ```typescript 408 | import React, { SFC } from 'react'; 409 | import { Toggleable } from './Toggleable'; 410 | import { PanelItemProps, PanelItem } from './PanelItem'; 411 | 412 | const PanelViaInjection: SFC = ({title, children}) => ( 413 | 414 | {children} 415 | 416 | ); 417 | ``` 418 | 419 | > 注意:`props` 属性没有类型安全检查,因为他被定义为了包含任意属性的可索引类型: 420 | > `{ [propName: string]: any }` 421 | 422 | 423 | 424 | 现在我们可以利用这种方式来重现折叠面板场景了: 425 | 426 | ```typescript 427 | class Collapse extends Component { 428 | 429 | render() { 430 | return ( 431 |

432 |

内容1

433 |

内容2

434 |

内容3

435 |
436 | ); 437 | } 438 | } 439 | ``` 440 | 441 | 442 | 443 | 444 | 445 | ## 泛型组件 446 | 447 | 在组件注入模式的例子中,`props` 属性丢失了类型安全检查,我们如何去修复这个问题呢?估计你已经猜出来了,我们可以把 Toggleable 组件重构为泛型组件! 448 | 449 | 450 | 451 | 下来我们开始重构 Toggleable 组件。首先我们需要让 `props` 支持泛型: 452 | 453 | ```typescript 454 | type DefaultInjectedProps

= { props: P }; 455 | const defaultInjectedProps: DefaultInjectedProps = {props: {}}; 456 | 457 | type Props

= Partial<{ 458 | children: RenderCallback | ReactNode; 459 | render: RenderCallback; 460 | component: ComponentType> 461 | } & DefaultInjectedProps

>; 462 | ``` 463 | 464 | 465 | 466 | 然后让 Toggleable 的 class 也支持泛型: 467 | 468 | ```typescript 469 | class Toggleable extends Component, State> {} 470 | ``` 471 | 472 | 看起来好像已经搞定了!如果你是用的 TS 2.9,可以直接这样用: 473 | 474 | ```typescript 475 | const PanelViaInjection: SFC = ({title, children}) => ( 476 | component={PanelItem} props={{title}}> 477 | {children} 478 | 479 | ); 480 | ``` 481 | 482 | 483 | 484 | 但是如果 <= TS 2.8 ... JSX 里面不能直接应用泛型参数 :worried: 那么我们还有一步工作要做,加入一个静态方法 `ofType` ,用来进行构造函数的类型转换: 485 | 486 | ```typescript 487 | static ofType() { 488 | return Toggleable as Constructor>; 489 | } 490 | ``` 491 | 492 | 这里用到一个 type:`Constructor`,依然定义在 `src/types/global.d.ts` 里面: 493 | 494 | ```typescript 495 | declare type Constructor = { new(...args: any[]): T }; 496 | ``` 497 | 498 | 499 | 500 | 好了,我们完成了所有的工作,下面是 Toggleable 重构后的完整代码: 501 | 502 | 503 | 504 | 现在我们来看看怎么使用这个泛型组件,重构下原来的 PanelViaInjection 组件: 505 | 506 | ```typescript 507 | import React, { SFC } from 'react'; 508 | import { Toggleable } from './Toggleable'; 509 | import { PanelItemProps, PanelItem } from './PanelItem'; 510 | 511 | const ToggleableOfPanelItem = Toggleable.ofType(); 512 | 513 | const PanelViaInjection: SFC = ({title, children}) => ( 514 | 515 | {children} 516 | 517 | ); 518 | ``` 519 | 520 | 所有的功能都能像原来的代码一样工作,并且现在 `props` 属性也支持 TS 类型检查了,很棒有木有! :smiley: 521 | 522 | ![](images/generic-toggleable-demo.gif) 523 | 524 | 525 | 526 | 527 | 528 | ## 高阶组件 529 | 530 | 最后我们来看下 HOC 。前面我们已经实现了 Toggleable 的渲染回调模式,那么很自然的我们可以衍生出一个 HOC 组件。 531 | 532 | > 如果对 HOC 不熟悉的话,可以先看下 React 官方文档对于 [HOC](https://reactjs.org/docs/higher-order-components.html) 的说明。 533 | 534 | 535 | 536 | 先来看看定义 HOC 我们需要做哪些工作: 537 | 538 | - `displayName` (方便在 devtools 里面进行调试) 539 | - `WrappedComponent ` (可以访问原始的组件 -- 有利于调试) 540 | - 引入 [hoist-non-react-statics](https://github.com/mridgway/hoist-non-react-statics) 包,将原始组件的静态方法全部“复制”到 HOC 组件上 541 | 542 | 543 | 544 | 下面直接上代码 -- `withToggleable` 高阶组件: 545 | 546 | ![](images/hoc-toggleable.png) 547 | 548 | 549 | 550 | 现在我们来用 HOC 重写一个 Panel : 551 | 552 | ```typescript 553 | import { PanelItem } from './PanelItem'; 554 | import withToggleable from './withToggleable'; 555 | 556 | const PanelViaHOC = withToggleable(PanelItem); 557 | ``` 558 | 559 | 560 | 561 | 然后,又可以实现折叠面板了 :smile: 562 | 563 | ```typescript 564 | class Collapse extends Component { 565 | 566 | render() { 567 | return ( 568 |

569 |

内容1

570 |

内容2

571 |
572 | ); 573 | } 574 | } 575 | ``` 576 | 577 | 578 | 579 | ## 尾声 580 | 581 | 感谢能坚持看完的朋友,你们真的很棒! 582 | 583 | 如果觉得还不错请帮忙给个 :star: 584 | 585 | 586 | 587 | 最后,感谢 Anders Hejlsberg 和所有的 TS 贡献者 :thumbsup: 588 | 589 | -------------------------------------------------------------------------------- /images/button-with-default-props.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/button-with-default-props.png -------------------------------------------------------------------------------- /images/generic-toggleable-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/generic-toggleable-demo.gif -------------------------------------------------------------------------------- /images/hoc-toggleable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/hoc-toggleable.png -------------------------------------------------------------------------------- /images/inject-component-toggleable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/inject-component-toggleable.png -------------------------------------------------------------------------------- /images/render-callback-collapse-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/render-callback-collapse-demo.gif -------------------------------------------------------------------------------- /images/render-callback-collapse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/render-callback-collapse.png -------------------------------------------------------------------------------- /images/render-callback-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/render-callback-panel.png -------------------------------------------------------------------------------- /images/render-callback-toggleable-children.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/render-callback-toggleable-children.png -------------------------------------------------------------------------------- /images/render-callback-toggleable-render.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/render-callback-toggleable-render.png -------------------------------------------------------------------------------- /images/render-callback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/render-callback.png -------------------------------------------------------------------------------- /images/state-readonly.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/state-readonly.gif -------------------------------------------------------------------------------- /images/stateful.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/stateful.png -------------------------------------------------------------------------------- /images/stateless.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/stateless.png -------------------------------------------------------------------------------- /images/with-default-props.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/with-default-props.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-react-component-patterns", 3 | "version": "1.0.0", 4 | "author": "Xie Kai <6261625@qq.com>", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "webpack --colors --config ./webpack.config.js", 8 | "build-watch": "webpack --watch --colors --config ./webpack.config.js" 9 | }, 10 | "dependencies": { 11 | "babel-polyfill": "^6.23.0", 12 | "babel-runtime": "6.26.0", 13 | "classnames": "^2.2.5", 14 | "debug": "~2.2.0", 15 | "ejs": "^2.5.7", 16 | "hoist-non-react-statics": "^2.5.0", 17 | "koa": "^2.5.1", 18 | "koa-mount": "^3.0.0", 19 | "koa-static": "^4.0.2", 20 | "koa-views": "^6.1.3", 21 | "react": "^16.3.2", 22 | "react-dom": "^16.3.2" 23 | }, 24 | "devDependencies": { 25 | "@types/classnames": "^2.2.3", 26 | "@types/koa": "^2.0.45", 27 | "@types/koa-mount": "^2.0.19", 28 | "@types/koa-static": "^3.0.2", 29 | "@types/koa-views": "^2.0.3", 30 | "@types/react": "^16.3.14", 31 | "@types/react-dom": "^16.0.5", 32 | "autoprefixer": "^7.1.1", 33 | "babel-cli": "6.26.0", 34 | "babel-core": "6.26.0", 35 | "babel-loader": "7.1.2", 36 | "babel-plugin-transform-runtime": "6.23.0", 37 | "babel-preset-env": "1.6.1", 38 | "babel-preset-react": "6.24.1", 39 | "babel-preset-stage-0": "6.24.1", 40 | "babel-register": "6.26.0", 41 | "css-loader": "^0.28.7", 42 | "del": "^2.2.2", 43 | "html-webpack-plugin": "^2.24.1", 44 | "postcss-loader": "^2.0.6", 45 | "style-loader": "^0.13.1", 46 | "ts-loader": "^3.5.0", 47 | "typescript": "^2.9.1", 48 | "webpack": "^3.11.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Demo.tsx: -------------------------------------------------------------------------------- 1 | import React, { SFC, Fragment } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Button from './stateless/Button'; 4 | import Collapse from './renderCallback/Collapse'; 5 | import CollapseWithPanelViaInjection from './injectedComponent/Collapse'; 6 | import CollapseWithGeneric from './generic/Collapse'; 7 | import CollapseWithHOC from './hoc/Collapse'; 8 | 9 | const Demo: SFC = () => ( 10 | 11 |

折叠面板:渲染回调模式

12 | 13 |

折叠面板:组件注入模式

14 | 15 |

折叠面板:泛型组件

16 | 17 |

折叠面板:HOC

18 | 19 |
20 | ); 21 | 22 | ReactDOM.render(, document.getElementById('root')); -------------------------------------------------------------------------------- /src/defaultProps/ButtonWithDefaultProps.tsx: -------------------------------------------------------------------------------- 1 | import React, { SFC, MouseEvent } from 'react'; 2 | import { withDefaultProps } from '../utils'; 3 | 4 | const defaultProps = {color: 'blue'}; 5 | type DefaultProps = typeof defaultProps; 6 | 7 | type Props = { onClick(e: MouseEvent): void; } & DefaultProps; 8 | 9 | const Button: SFC = ({onClick: handleClick, color, children}) => ( 10 | 11 | ); 12 | 13 | export default withDefaultProps(defaultProps, Button); -------------------------------------------------------------------------------- /src/generic/Collapse.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PanelViaInjection from './PanelViaInjection'; 3 | 4 | class Collapse extends Component { 5 | 6 | render() { 7 | return ( 8 |
9 |

内容1

10 |

内容2

11 |

内容3

12 |

内容4

13 |

内容5

14 |
15 | ); 16 | } 17 | } 18 | 19 | export default Collapse; -------------------------------------------------------------------------------- /src/generic/PanelItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { SFC } from 'react'; 2 | import { ToggleableComponentProps } from './Toggleable'; 3 | 4 | type PanelItemProps = { title: string }; 5 | 6 | const PanelItem: SFC = props => { 7 | const {title, children, show, toggle} = props; 8 | 9 | return ( 10 |
11 |

{title}

12 | {show ? children : null} 13 |
14 | ); 15 | }; 16 | 17 | export { PanelItemProps, PanelItem }; -------------------------------------------------------------------------------- /src/generic/PanelViaInjection.tsx: -------------------------------------------------------------------------------- 1 | import React, { SFC } from 'react'; 2 | import { Toggleable } from './Toggleable'; 3 | import { PanelItemProps, PanelItem } from './PanelItem'; 4 | 5 | const ToggleableOfPanelItem = Toggleable.ofType(); 6 | 7 | const PanelViaInjection: SFC = ({title, children}) => ( 8 | 9 | {children} 10 | 11 | ); 12 | 13 | // TS 2.9 可以直接这样写 14 | // const PanelViaInjection: SFC = ({title, children}) => ( 15 | // component={PanelItem} props={{title}}> 16 | // {children} 17 | //
18 | // ); 19 | 20 | export default PanelViaInjection; -------------------------------------------------------------------------------- /src/generic/Toggleable.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, ComponentType, ReactNode, MouseEvent } from 'react'; 2 | import { isFunction } from '../utils'; 3 | 4 | type DefaultInjectedProps

= { props: P }; 5 | const defaultInjectedProps: DefaultInjectedProps = {props: {}}; 6 | 7 | type Props

= Partial<{ 8 | children: RenderCallback | ReactNode; 9 | render: RenderCallback; 10 | component: ComponentType> 11 | } & DefaultInjectedProps

>; 12 | 13 | type RenderCallback = (args: ToggleableComponentProps) => ReactNode; 14 | type ToggleableComponentProps

= { 15 | show: boolean; 16 | toggle: Toggleable['toggle']; 17 | } & P; 18 | 19 | const initialState = {show: false}; 20 | type State = Readonly; 21 | 22 | class Toggleable extends Component, State> { 23 | 24 | static ofType() { 25 | return Toggleable as Constructor>; 26 | } 27 | static readonly defaultProps: Props = defaultInjectedProps; 28 | readonly state: State = initialState; 29 | 30 | render() { 31 | const {component: InjectedComponent, children, render, props} = this.props; 32 | const {show} = this.state; 33 | const renderProps = {show, toggle: this.toggle}; 34 | 35 | if (InjectedComponent) { 36 | return ( 37 | 38 | {children} 39 | 40 | ); 41 | } 42 | 43 | if (render) { 44 | return render(renderProps); 45 | } else if (isFunction(children)) { 46 | return children(renderProps); 47 | } else { 48 | return null; 49 | } 50 | } 51 | 52 | private toggle = (event: MouseEvent) => { 53 | this.setState(prevState => ({show: !prevState.show})); 54 | }; 55 | } 56 | 57 | export { ToggleableComponentProps, Toggleable }; -------------------------------------------------------------------------------- /src/hoc/Collapse.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PanelViaHOC from './PanelViaHOC'; 3 | 4 | class Collapse extends Component { 5 | 6 | render() { 7 | return ( 8 |

9 |

内容1

10 |

内容2

11 |
12 | ); 13 | } 14 | } 15 | 16 | export default Collapse; -------------------------------------------------------------------------------- /src/hoc/PanelItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { SFC } from 'react'; 2 | import { ToggleableComponentProps } from './Toggleable'; 3 | 4 | type PanelItemProps = { title: string }; 5 | 6 | const PanelItem: SFC = props => { 7 | const {title, children, show, toggle} = props; 8 | 9 | return ( 10 |
11 |

{title}

12 | {show ? children : null} 13 |
14 | ); 15 | }; 16 | 17 | export { PanelItemProps, PanelItem }; -------------------------------------------------------------------------------- /src/hoc/PanelViaHOC.tsx: -------------------------------------------------------------------------------- 1 | import { PanelItem } from './PanelItem'; 2 | import withToggleable from './withToggleable'; 3 | 4 | const PanelViaHOC = withToggleable(PanelItem); 5 | 6 | export default PanelViaHOC; -------------------------------------------------------------------------------- /src/hoc/Toggleable.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, ComponentType, ReactNode, MouseEvent } from 'react'; 2 | import { isFunction } from '../utils'; 3 | 4 | type DefaultInjectedProps

= { props: P }; 5 | const defaultInjectedProps: DefaultInjectedProps = {props: {}}; 6 | 7 | type Props

= Partial<{ 8 | children: RenderCallback | ReactNode; 9 | render: RenderCallback; 10 | component: ComponentType 11 | } & DefaultInjectedProps

>; 12 | 13 | type RenderCallback = (args: ToggleableComponentProps) => ReactNode; 14 | type ToggleableComponentProps = { 15 | show: boolean; 16 | toggle: Toggleable['toggle']; 17 | }; 18 | 19 | const initialState = {show: false}; 20 | type State = Readonly; 21 | 22 | class Toggleable extends Component, State> { 23 | 24 | static ofType() { 25 | return Toggleable as Constructor>; 26 | } 27 | static readonly defaultProps: Props = defaultInjectedProps; 28 | readonly state: State = initialState; 29 | 30 | render() { 31 | const {component: InjectedComponent, children, render, props} = this.props; 32 | const {show} = this.state; 33 | const renderProps = {show, toggle: this.toggle}; 34 | 35 | if (InjectedComponent) { 36 | return ( 37 | 38 | {children} 39 | 40 | ); 41 | } 42 | 43 | if (render) { 44 | return render(renderProps); 45 | } else if (isFunction(children)) { 46 | return children(renderProps); 47 | } else { 48 | return null; 49 | } 50 | } 51 | 52 | private toggle = (event: MouseEvent) => { 53 | this.setState(prevState => ({show: !prevState.show})); 54 | }; 55 | } 56 | 57 | export { ToggleableComponentProps, Toggleable }; -------------------------------------------------------------------------------- /src/hoc/withToggleable.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, ComponentType } from 'react'; 2 | import hoistNonReactStatics from 'hoist-non-react-statics'; 3 | import { Toggleable, ToggleableComponentProps } from './Toggleable'; 4 | import { getHOCComponentName } from '../utils'; 5 | 6 | function withToggleable( 7 | WrappedComponent: ComponentType) { 8 | 9 | type Props = Omit; 10 | 11 | class WithToggleable extends Component { 12 | 13 | static readonly displayName = getHOCComponentName('WithToggleable', WrappedComponent); 14 | static readonly WrappedComponent = WrappedComponent; 15 | 16 | render() { 17 | return ( 18 | }/> 19 | ); 20 | } 21 | } 22 | 23 | return hoistNonReactStatics(WithToggleable, WrappedComponent); 24 | } 25 | 26 | export default withToggleable; -------------------------------------------------------------------------------- /src/injectedComponent/Collapse.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PanelViaInjection from './PanelViaInjection'; 3 | 4 | class Collapse extends Component { 5 | 6 | render() { 7 | return ( 8 |

9 |

内容1

10 |

内容2

11 |

内容3

12 |
13 | ); 14 | } 15 | } 16 | 17 | export default Collapse; -------------------------------------------------------------------------------- /src/injectedComponent/PanelItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { SFC } from 'react'; 2 | import { ToggleableComponentProps } from './Toggleable'; 3 | 4 | type PanelItemProps = { title: string }; 5 | 6 | const PanelItem: SFC = props => { 7 | const {title, children, show, toggle} = props; 8 | 9 | return ( 10 |
11 |

{title}

12 | {show ? children : null} 13 |
14 | ); 15 | }; 16 | 17 | export { PanelItemProps, PanelItem }; -------------------------------------------------------------------------------- /src/injectedComponent/PanelViaInjection.tsx: -------------------------------------------------------------------------------- 1 | import React, { SFC } from 'react'; 2 | import { Toggleable } from './Toggleable'; 3 | import { PanelItemProps, PanelItem } from './PanelItem'; 4 | 5 | const PanelViaInjection: SFC = ({title, children}) => ( 6 | 7 | {children} 8 | 9 | ); 10 | 11 | export default PanelViaInjection; -------------------------------------------------------------------------------- /src/injectedComponent/Toggleable.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, ComponentType, ReactNode, MouseEvent } from 'react'; 2 | import { isFunction } from '../utils'; 3 | 4 | const defaultInjectedProps = {props: {} as { [propName: string]: any }}; 5 | type DefaultInjectedProps = typeof defaultInjectedProps; 6 | type Props = Partial<{ 7 | children: RenderCallback | ReactNode; 8 | render: RenderCallback; 9 | component: ComponentType> 10 | } & DefaultInjectedProps>; 11 | 12 | type RenderCallback = (args: ToggleableComponentProps) => ReactNode; 13 | type ToggleableComponentProps

= { 14 | show: boolean; 15 | toggle: Toggleable['toggle']; 16 | } & P; 17 | 18 | const initialState = {show: false}; 19 | type State = Readonly; 20 | 21 | class Toggleable extends Component { 22 | 23 | static readonly defaultProps: Props = defaultInjectedProps; 24 | readonly state: State = initialState; 25 | 26 | render() { 27 | const {component: InjectedComponent, children, render, props} = this.props; 28 | const {show} = this.state; 29 | const renderProps = {show, toggle: this.toggle}; 30 | 31 | if (InjectedComponent) { 32 | return ( 33 | 34 | {children} 35 | 36 | ); 37 | } 38 | 39 | if (render) { 40 | return render(renderProps); 41 | } else if (isFunction(children)) { 42 | return children(renderProps); 43 | } else { 44 | return null; 45 | } 46 | } 47 | 48 | private toggle = (event: MouseEvent) => { 49 | this.setState(prevState => ({show: !prevState.show})); 50 | }; 51 | } 52 | 53 | export { ToggleableComponentProps, Toggleable }; -------------------------------------------------------------------------------- /src/renderCallback/Collapse.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Panel from './Panel'; 3 | 4 | class Collapse extends Component { 5 | 6 | render() { 7 | return ( 8 |

9 |

内容1

10 |

内容2

11 |

内容3

12 |

内容4

13 |
14 | ); 15 | } 16 | } 17 | 18 | export default Collapse; -------------------------------------------------------------------------------- /src/renderCallback/Panel.tsx: -------------------------------------------------------------------------------- 1 | import React, { SFC } from 'react'; 2 | import Toggleable from './Toggleable'; 3 | 4 | type Props = { title: string }; 5 | 6 | const Panel: SFC = ({title, children}) => ( 7 | ( 9 |
10 |

{title}

11 | {show ? children : null} 12 |
13 | )} 14 | /> 15 | ); 16 | 17 | export default Panel; 18 | 19 | -------------------------------------------------------------------------------- /src/renderCallback/Toggleable.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, ReactNode, MouseEvent } from 'react'; 2 | import { isFunction } from '../utils'; 3 | 4 | // 属性相关 5 | type Props = Partial<{ 6 | children: RenderCallback; 7 | render: RenderCallback; 8 | }>; 9 | type RenderCallback = (args: ToggleableRenderArgs) => ReactNode; 10 | type ToggleableRenderArgs = { 11 | show: boolean; 12 | toggle: Toggleable['toggle']; 13 | }; 14 | 15 | // 状态相关 16 | const initialState = {show: false}; 17 | type State = Readonly; 18 | 19 | class Toggleable extends Component { 20 | 21 | readonly state: State = initialState; 22 | 23 | render() { 24 | const {children, render} = this.props; 25 | const {show} = this.state; 26 | const renderArgs = {show, toggle: this.toggle}; 27 | 28 | if (render) { 29 | return render(renderArgs); 30 | } else if (isFunction(children)) { 31 | return children(renderArgs); 32 | } else { 33 | return null; 34 | } 35 | } 36 | 37 | private toggle = (event: MouseEvent) => { 38 | this.setState(prevState => ({show: !prevState.show})); 39 | }; 40 | } 41 | 42 | export default Toggleable; -------------------------------------------------------------------------------- /src/server/server.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import Koa from 'koa'; 3 | import mount from 'koa-mount'; 4 | import serve from 'koa-static'; 5 | import views from 'koa-views'; 6 | 7 | const cwd = process.cwd(); 8 | const app = new Koa(); 9 | const listenPort = 3000; 10 | 11 | app.use(mount('/dist', serve(path.join(cwd, 'dist')))); 12 | 13 | app.use(views(path.join(cwd, 'src/server/views'), {extension: 'ejs'})); 14 | 15 | app.use(mount('/', async ctx => await ctx.render('demo'))); 16 | 17 | app.listen(listenPort, () => console.log('node server is listening on port: ' + listenPort)); -------------------------------------------------------------------------------- /src/server/views/demo.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ts-react-component-patterns-demo 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/server/www.js: -------------------------------------------------------------------------------- 1 | require('babel-polyfill'); 2 | require('babel-register'); 3 | require('./server'); 4 | -------------------------------------------------------------------------------- /src/stateful/CounterButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | const initialState = {count: 0}; 4 | type State = Readonly; 5 | 6 | class CounterButton extends Component<{}, State> { 7 | 8 | readonly state: State = initialState; 9 | 10 | render() { 11 | const {count} = this.state; 12 | 13 | return ( 14 |
15 | 16 |

当前计数:{count}

17 |
18 | ) 19 | } 20 | 21 | private handleIncrement = () => this.setState(prevState => ({count: prevState.count + 1})); 22 | } 23 | 24 | export default CounterButton; 25 | 26 | -------------------------------------------------------------------------------- /src/stateless/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { SFC, MouseEvent } from 'react'; 2 | 3 | interface Props { 4 | onClick(e: MouseEvent): void; 5 | } 6 | 7 | const Button: SFC = ({onClick: handleClick, children}) => ( 8 | 9 | ); 10 | 11 | export default Button; -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "es2015", 5 | "target": "es2015", 6 | "lib": [ 7 | "es2015", 8 | "dom" 9 | ], 10 | "jsx": "preserve", 11 | "allowSyntheticDefaultImports": true, 12 | "strict": true, 13 | "baseUrl": "." 14 | }, 15 | "include": [ 16 | "./**/*" 17 | ] 18 | } -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare type DiffPropertyNames = 2 | { [P in T]: P extends U ? never: P }[T]; 3 | 4 | declare type Omit = Pick>; 5 | 6 | declare type Constructor = { new(...args: any[]): T }; -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { ComponentType } from 'react'; 2 | 3 | function withDefaultProps(defaultProps: DP, Cmp: ComponentType

) { 4 | // 首先将必填属性抽取出来 5 | type RequiredProps = Omit; 6 | // 然后重新构造属性类型定义,可选的默认值属性 + 必填属性 7 | type Props = Partial & RequiredProps; 8 | 9 | // 把默认值设置好 10 | Cmp.defaultProps = defaultProps; 11 | // 返回处理好的组件类型 12 | return (Cmp as ComponentType) as ComponentType; 13 | } 14 | 15 | function isFunction(value: any): value is T { 16 | return typeof value === 'function'; 17 | } 18 | 19 | function getComponentName(component: ComponentType) { 20 | return component.displayName || (component as any).name; 21 | } 22 | 23 | function getHOCComponentName(hocName: string, component: ComponentType) { 24 | return `${hocName}(${getComponentName(component)})`; 25 | } 26 | 27 | export { withDefaultProps, isFunction, getComponentName, getHOCComponentName }; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const rootDir = __dirname; 4 | 5 | module.exports = { 6 | context: rootDir, 7 | 8 | devtool: 'cheap-module-source-map', 9 | 10 | entry: {'demo': './src/Demo.tsx'}, 11 | 12 | output: { 13 | path: path.join(rootDir, 'dist'), 14 | publicPath: '/dist/', 15 | filename: '[name].js', 16 | chunkFilename: '[name].js', 17 | }, 18 | 19 | resolve: { 20 | extensions: ['.ts', '.tsx', '.js', '.jsx'] 21 | }, 22 | 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.css$/, 27 | use: ['style-loader', 'css-loader'] 28 | }, 29 | { 30 | test: /\.tsx?$/, 31 | use: [ 32 | { 33 | loader: 'babel-loader', 34 | options: { 35 | cacheDirectory: true 36 | } 37 | }, 38 | { 39 | loader: 'ts-loader', 40 | options: { 41 | configFile: path.join(rootDir, 'src/tsconfig.json'), 42 | } 43 | } 44 | ] 45 | } 46 | ] 47 | } 48 | }; --------------------------------------------------------------------------------