场景
问:为什么吾辈要使用 React?
答:React 拥有更加庞大的生态,以及对 TypeScript 的更好支持。 前者让需求实现变得更加简单,例如目前使用 Vue 做的后台管理系统使用了 Ant Design Vue 这个 UI 库,而它的上游 Ant Design 实际上官方维护的是 React 版本,而 Vue 并不是 亲儿子 ,导致一些问题并不像官方那么快解决。 后者强大的类型系统能降低维护成本,虽然开发时代码添加类型会稍加工作量,但可以降低维护成本,便于后续的修改、重构,同时 IDE 对其支持是 JavaScript 无法相提并论的。
问:那 React 相比于 Vue 而言有什么区别?
答:更强大、复杂、酷,对于没有现代前端开发经验的人而言可能非常困难,但一旦熟悉,则会非常喜欢它。组件化(React Component/JSX)、函数式(React Hooks)、不可变(immutable)都是非常有趣的思想,理解之后确实都能发现具体使用场景。
Vue 作者说 React + Mobx 就是更复杂的 Vue ,这句话确实有道理,下面在 状态管理 那里也进行了说明,但同时,相比于 Vue + Vuex,避免引入 Redux 的 React + Mobx 将是更简单的。
问:有大公司在用么?
答:作为能够支撑 Facebook 这种级别公司的 Web 产品的基础,显然它拥有相当多的生产环境实践。
工程与周边生态 以下皆基于 cra(create-react-app) 创建的 ts + react + mobx + react-router + immer 技术栈进行说明,虽然完全不了解以上内容亦可,但最好了解一下它们是做什么的,下面第一次提及时也会简单说明一下。
创建项目 使用 create-react-app 创建 react 项目,但和 vue-cli 有一点明显区别:不提供很多配置,只是简单的项目生成。
状态管理 此处使用 mobx 对标,mobx 是一个状态管理库,以响应式、可变、简单方便为最大卖点。本质上可以认为为每个页面(页面内的所有组件)提供了一个全局对象,并实现了 vue 中的 computed 和 watch,所以 vue 的作者说 vue 是更简单的 react + mobx 确实有些道理,实际上这两个加起来能做到的事情不比原生 vue 多多少。
但它们之间也有几点不同
vue 基于组件级别实现的 computed 和 watch,而 mobx 则是全局的
vue 是基于组件级别自动初始化和销毁,而 mobx 则是手动的
vue 基于组件但也受限于组件级别,全局状态仍要使用 vuex 这种 大炮 ,而 mobx 此时则是统一的
不使用 vuex 的情况下一些组件很难进行拆分,因为拆分后各组件的一些数据仍然需要共享和修改,这种时候单用 vue 的 data/props 就显得有些力不从心
是否需要支持 es5?
路由
递归菜单栏
使用高阶组件包装路由组件 withRouter()
获取当前路由信息:this.props.match
使用编程式的路由导航:this.props.history
异步组件和 vue 稍微有点差别,虽然也是需要 import() 语法,但却需要使用高阶组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import React from 'react' function AsyncRoute ( importComponent: () => PromiseLike<{ default : any }> | { default : any }, ) { class AsyncComponent extends React.Component <any, any> { constructor (props: any ) { super (props) this .state = { component : null , } } async componentDidMount ( ) { const { default : component } = await importComponent () this .setState ({ component : component, }) } render ( ) { const C = this .state .component return C ? <C {...this.props } /> : null } } return AsyncComponent } export default AsyncRoute
然后使用高阶组件包装即可
1 2 3 4 <Route path={'/system/task' } component={AsyncRoute (() => import ('../../index/HelloWorld' ))} />
注:高阶组件和高阶函数类似,指的是接收一个组件/返回一个组件的组件。
代码组织
导出:tsx/jsx 使用默认导出,避免需要高阶函数包装的场景
优先使用函数式组件
src
pages:页面级的组件
components:非页面相关的通用组件
assets:静态资源
命名规范
React
组件:必须使用大写驼峰,包括使用组件亦然
store
必须使用 .store 后缀以区分普通 ts 文件
组件级 store 必须与组件名保持一致,例如 Login 组件对应的即为 Login.store.ts
CSS
css 中的 class 必须使用小写驼峰命名法,避免 css module 找不到(cra 不会自动处理转换)
优先使用 .module.css 而非 .css,避免全局样式污染
页面级 css 必须与组件名保持一致,例如 Login 组件对应的即为 Login.module.css
非 css module 的代码必须使用 "" 而非 {''}
修改默认配置 cra 默认提供了脚本命令 reject,用于将 cra 封装的配置全部解压出来 – 当然,此操作是不可逆的!但除了这种破坏性的方式之外,也有人找到了和 vue-cli 中类似的方式,不过需要第三方包 react-app-rewired 的支持。
并在根目录添加配置文件 config-overrides.js,里面暴露出一个函数,即可修改 cra 的默认配置了。
下面是一个简单的示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const WorkerPlugin = require ('worker-plugin' )module .exports = function override (config, env ) { config.output .globalObject = 'this' if (!config.plugins ) { config.plugins = [] } config.plugins .push (new WorkerPlugin ()) return config }
重点 this 的值 在 class 中使用箭头函数以直接绑定当前组件的实例,尽量不要使用 function,否则 this 可能是不明确的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import React , { Component } from 'react' class HelloWorld extends Component { logMsg = () => { console .log ('msg' ) } render ( ) { return ( <div > <h1 > hello world</h1 > <button onClick ={this.logMsg} > 打印</button > </div > ) } } export default HelloWorld
CSS 样式隔离 cra 创建的项目默认支持 css module,是 react 项目流行的一种 CSS 隔离方案。
使用步骤
为需要的 css 文件使用 .module.css 后缀名
通过 import styles from '*.module.css' 在 tsx 中引入
在 className={styles.*} 使用 class 样式
示例
1 2 3 .helloWorld { background-color : #000 ; }
1 2 3 4 5 import styles from 'HelloWorld.module.css' export default function HelloWorld ( ) { return <div className ={styles.helloWorld} > hello world</div > }
注:
此处的 import styles from '*.module.css' 不支持命名导入
此处实现的逻辑和 Vue 是一致的,只要使用了其中一个样式 class,则整个文件都会引入
CSS 只要被引入了,就不会被删除,即便组件被销毁了亦然,所以页面内的 CSS 只会增加,不会减少
如何添加多个,默认使用 cra 创建的项目支持使用模板字符串
1 className={`${styles.className1} ${styles.className2} ` }
看起来很丑?可以试试 classnames
1 2 3 import classNames from 'classnames' className={classNames (globalStyles.global , globalStyles.margin )}
但仍然很丑,正如 Sindre Sorhus 所说:React 把简单的事情变复杂,复杂的事情变简单
另一种个 API 是 classNames.bind
1 2 3 import classNames from 'classnames' const cx={classNames.bind (globalStyles)}className={cx ('global' , 'margin' )}
但这会让 WebStorm 损失所有的 CSS 关联,影响了包括代码提示/跳转/重构等功能,考虑到维护成本实在得不偿失。
还有人提出了 typed css module,为所有的 .module.css 生成 .d.ts 类型定义,但这会和 css in js 一样丧失 css 预处理器的优势 —- 并且,所有的工具链都需要重新支持这种关联,将之认为是 css。
参考
引入图片 在任何一个项目中,都少不了引入图片的需求。而 React 中,并未像 Vue 对 img, audio, video 这些标签进行特殊处理,以支持直接使用路径即可将对应的媒体文件打包进来,React 需要使用 import img from '*' 的原始 形式让 webpack 知道 这是一个需要打包的资源。
例如
1 2 3 4 5 import img from 'img.png' function render ( ) { reutrn (<img src ={img} alt ="img" /> ) }
而对于 SVG 类型的图片,React 支持使用组件的形式引入。
1 2 3 4 5 import { ReactComponent as IconAudio } from '../../assets/icon/icon-audio.svg' function render ( ) { reutrn (<IconAudio /> ) }
slot 两种方案
使用 {props.children},和 vue 的 slot:default 几乎一样,只是不能通过子组件传递参数。
如果需要传递多个命名 slot,则可以直接为 props 属性赋值为组件。例如 title={<div>hello world</div>}
如果需要使用子组件传参的话需要使用函数式组件的形式。例如 title={value => <div>{value}</div>}
吐槽:函数式已经是政治正确了。
使用函数式的 slot 时必须检查函数是否存在,如果不存在则不要调用,不像是 vue 中的 slot 是自动处理这一步的。
1 2 3 { this .props .tableOperate && this .props .tableOperate (this .state .innerValue ) }
watch 监听 props 1 2 3 4 5 6 7 8 componentDidUpdate (prevProps: PropsType ) { if (this .props .value !== prevProps.value ) { this .setState ({ innerValue : this .props .value , }) } }
生命周期 相比于 Vue 的生命周期,React 显然更加复杂 与混乱 ,而且,老实说有时候真的很难用。
例如非常常见的生命周期 componentWillUpdate,它承担的责任实在是太多了,不仅 watch state/props 中的数据要用这个,连 React Router 的路由变化同样依赖于此。
下面列出最常用的一些生命周期以及典型用例
render:只要 state 变化就会触发重新渲染,等价于 vue 中渲染模板 HTML 中的内容
shouldComponentUpdate:state 或者 props 变化前 就会执行,时机上早于 render。等价于 vue beforeUpdate,但可以在这个方法内 return false 阻止视图更新。
componentDidUpdate:state 或者 props 变化后 就会执行,时机上晚于 render。等价于 vue updated。 多用于监听一些数据变化执行一些副作用操作,但包含的种类可能会非常多。
注:此处 vue 中将之分为 updated/watch/beforeRouteUpdate,而在 React 中,全部由 componentDidMount 承担这个责任。
componentDidMount:当组件渲染完成后 就会执行,时机上晚于 render,等价于 vue mounted。 多用于执行一些初始化操作,除非逻辑特别简单,否则不要在这个函数里放具体执行逻辑代码,而是专门写初始化函数在这里调用。
UNSAFE_componentWillMount:在组件渲染完成前 就会执行,等价于 vue beforeMount,但被废弃了,替代解决方案参考 怎么在没有 created 生命周期的情况下初始化数据并保证用户看不到默认空数据 。
componentWillUnmount: 组件即将被销毁前调用,等价于 vue beforeDestroy。
简化 state 修改 使用 setState 很烦的一点是当你需要深度为某个属性赋值的时候,要为该属性上面所有的对象全部使用 ... 或者其他方式拷贝一遍。
例如
1 2 3 4 5 6 7 8 9 this .setState ({ user : { ...this .state .user , address : { ...this .state .user .address , city : newCity, }, }, })
当有多个属性需要赋值时就尤其的繁琐,而 immerjs 正好可以解决这种痛点
使用 immer 重构之后
1 2 3 this .setState (produce (this .state , draft => { draft.user .address .city = newCity })
代码变得很简单了,虽然看起来是直接赋值,不过 immer 使用了 Proxy 和 Object.freeze 实现了对使用者友好的不可变数据修改,具体参考 。
React Hooks React Hooks 是在 React 16.8 之后添加的一项新特性,一如既往,很多人又是吹的天花乱坠。老实说最开始 React Hooks 流行并且在各个社群被大肆宣传时,吾辈非常讨厌它,因为它又向函数式靠近了一步—-函数式政治正确真的很讨厌!所以,自正式学习 React 以来,吾辈都没有接触过 React Hooks,都是用 Class Component 实现组件。 但现在,架不住好奇心和一些朋友的推荐,吾辈稍微看了一下这个新特性。
它提供了两个主要的 API:
useState:声明一些可变状态
useEffect:声明一些副作用代码
使用看起来很简单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import React , { useEffect, useRef, useState } from 'react' const HelloHooks : React .FC = function ( ) { const [count, setCount] = useState (1 ) const [list, setList] = useState<string[]>([]) const countAdd = ( ) => setCount (count + 1 ) useEffect (() => { console .log ('count changed: ' , count) setList ( Array (count) .fill (0 ) .map ((_, i ) => `第 ${i + 1 } 个元素` ), ) }, [count]) const inputRef = useRef<HTMLInputElement >(null as any) useEffect (() => { inputRef.current .focus () }, []) return ( <div > <div > <input ref ={inputRef} value ={count} type ="number" onChange ={e => setCount(parseInt(e.target.value))} /> <button onClick ={countAdd} > 增加</button > </div > <ul > {list.map((v, i) => ( <li key ={i} > {v}</li > ))} </ul > </div > ) } export default HelloHooks
可以看到,上面用 useState/useEffect 两个函数实现了一些常见的功能:state, componentDidMount, componentDidUpdate,useEffect 甚至默认支持类似 vue watch 的使用方式。
同时,使用 Hooks 可以轻易地封装出 useModel, vue 中的 watch/computed 甚至原生自带了!
useMemo:当依赖变化时计算属性
useCallback:当依赖变化时执行回调
然而,Hooks 终究不是万能。
使用 Hooks 封装控制 DOM 相关的代码做不到,例如使用高阶组件实现的根据某些条件控制组件是否加载。
使用 Hooks 无法实现全部的生命周期,例如 shouldComponentUpdate。
Hooks useEffect 中调用的外部函数,无法即时获取到所有最新的 state,即便它们与 useEffect 同级,必须在 useEffect 声明正确的依赖才行,即便调用的函数在外部依赖的亦然如此(老实说这点并不简单)。示例:
Hooks 中 useState/useReducer 修改并不能即时反应出来。例如 const [count, setCount] = useState(0) 声明一个状态,使用 setCount(count + 1) 之后立刻取 count 的值仍然是 0,需要等到下次渲染才会生效。而 useRef 声明的可变状态,虽然是即时修改的,但组件却不会因为它的变化重新渲染。示例:https://codesandbox.io/s/react-hooks-usestate-async-change-5hlz0
使用 Hooks 会让函数变得很大,对开发人员的要求比之前更高(与 vue 3 的函数式 API 一样,都是由开发者自己完全控制代码块的分割)
更多有关 React Hooks 的介绍,请参考:
常见问题 React 有什么缺点
CSS 局部化有很多方案,但没有一统天下的
vue 在组件创建/销毁时会自动初始化/销毁状态及监听器,而 mobx 会一直保留需要手动初始化/清理
注: 这点还未找到解决方案
更新:可以使用 context 部分解决这个问题
使用 AntD 时可能遇到样式覆盖不了的问题,需要混合使用 className, style 两个属性。
react 会在开发阶段报错比较多,主要是一些低级错误,尤其是加上 ts 之后尤其如此
怎么在没有 created 生命周期的情况下初始化数据并保证用户看不到默认空数据 在 vue 中,吾辈经常在 created 生命周期中加载数据,避免用户看到默认的空数据,然而,React 中却没有这个生命周期,所以需要额外的处理。
注:其实 vue 中用户也有可能看到空数据,但因为 Ajax 请求数据的速度也比较快,所以默认可以不用处理。
一个可能解决方案是在指定元素外面包一层,在页面数据未加载之前,在元素上方添加一个 Loading 遮罩层提示正在加载中 ,等到数据加载完成后删除浮层。
下面是一个简单的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 .componentLoadingDialog { position : absolute; left : 0 ; top : 0 ; width : 100% ; height : 100% ; display : flex; justify-content : center; align-items : center; z-index : 10 ; background-color : #fff ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import React from 'react' import { Spin } from 'antd' import styles from './ComponentLoading.module.css' type PropsType = { isLoading : boolean tip?: string } const ComponentLoading : React .FC <PropsType > = function (props ) { const { isLoading, tip = '正在加载中。。。' } = props return ( <div style ={{ position: 'relative ' }}> {isLoading && ( <div className ={styles.componentLoadingDialog} > <Spin tip ={tip} /> </div > )} {/*注:默认会渲染 children 组件*/} {props.children} </div > ) } export default ComponentLoading
React 和 vue 3 的对比 vue 3 新增了 Function-base 的组件,看起来很像 React Hooks,但目前仍然无法在生产中实用。
目前看来有以下缺点
IDE 支持不好
TS 不能解决 Vue 中的一些问题,尤其是对于模板层面简直无能为力
Vue 3 函数式组件没有覆盖之前所有的功能
周边生态目前没有早期支持的迹象
关于第二和第三点,吾辈认为这是 Vue 使用模板带来的一些天然的问题,几乎不可能解决。而 React 和 TS 结合比 Vue 要完善很多,包括类型校验完全使用 TS 而非自定义运行时校验机制。
下面是 vue 作者谈及 vue 3 与其他框架的对比
VIDEO
参考:
开发环境代理 开发环境配置代理几乎是必用功能,相比于 vue-cli 将全部配置统一在 vue.config.js 中,cra 看起来仍是分散式的。
步骤
安装中间件 yarn add -D http-proxy-middleware @types/
创建文件 src/setupProxy.ts
编写配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import proxy from 'http-proxy-middleware' const fs = require ('fs-extra' )const path = require ('path' )module .exports = function (app : any ) { const proxyConfig : proxy.Config = { target : 'https://localhost:8000' , changeOrigin : true , secure : false , ws : true , pathRewrite (api : string ) { const mockApiList = fs.readJSONSync ( path.resolve (__dirname, './config/mockApiList.json' ), ) return ( api + (mockApiList.some ((mockApi : string ) => api.includes (mockApi)) ? '?mock=default&errCode=200' : '' ) ) }, } app.use (proxy ('/api' , proxyConfig)) }
参考: