最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • TypeScript for React (Native) 进阶

    正文概述 掘金(snwrking)   2021-02-23   555

    这文章在19年11月发过, 但后来在掘金就找不到了. 只好再重发一次.

    I. 为何要用TypeScript

    我们公司在德国还有个团队. 我们这次要接他们的一个库. 其中的一个API要求我们传入参数, 这个API是这样定义的:

    /*
     * 
     * @param {Object} input The first object
     * @param {Object} options The second object
     * /
    function init(input, options){
      ...
    }  
    

    看到这样的代码, 我是崩溃的. 这个input是个Object类型, 很清楚, 可是Object在JavaScript世界里可是千变万化的, 这我到底要传一个什么样的值过去呢.

    这其实就是最典型的例子, 一个"为什么我们需要使用TypeScript"的例子.

    1). TypeScript(下方简称TS)帮助我们检测类型, 方便使用/阅读其它的模块 2). TypeScript是强制检查的, 是不腐烂的. 而JavaScript(下方简称JS)中你就是加一个注释说"option是{isA: boolean, id: nubmer}", 这也不太好. 因为你以后改了option的结构, 大概念你的注释是没有变的. 但TS不会. TS一改option的结构, 其调用处就会报错, 说已经对应不上了. 强制你修改过来 其实TS还有一些好处, 比如说类型很强大(因此也更难掌握啦), 支持一些现代语言的新特性(如泛型)… 这些我们在本文中就不赘述了. 我其实更想讲解一下在使用TypeScript开发React/ReactNative(下方简称R/RN)时的一些坑与注意点, 帮助你更平滑地过渡到TypeScript的世界里来.

    II. React

    JS世界里我们使用PropTypes来定义类型, 但它不是很精确, 如PropTypes.object就不能精确到这个object需要什么成员, 这样你一不小心传少了值, 就会有NPE错误.

    TS中对props, state都可以进行限制 - 这适用于类组件与函数组件.

    1. class组件

    interface IProps {
      name: string;
    }
    
    interface IState {
      offset: number;
    }
    
    class SomeScreen extends React.Component<IProps, IState> {
      state = { offset: 0 };
    
      constructor(props: IProps) {
        super(props);
        console.log(props.name);
      }
    
    }
    

    这里就限定了Props与state的精确类型了. 你不可能再传错或少传props了

    2. function组件

    interface IProps {
      name: string;
    }
    
    const SomeScreen = (props: IProps) => {
      const [offset, setOffset] = useState<number>(0);
      console.log(props.name);
    };
    

    3. 进阶: child view是flexible的场景

    这时其实就是我们child view可能有一个, 也可能有多个, 这个可能要根据数据来定的. 比如你给我一个array, 有几个item我就显示几个view.

    这时这些灵活的子View就可以被定义为JSX.Element类型.

    render() { const children : JSX.Element[] = this.props.data.map((item, index) => { return <Image source={{ uri: item.url }} style={styles.item} key={item${index}}/>; });

    return (
      <View style={[this.props.style, styles.container]}>
        {children}
      </View>
    );
    

    } 当然, 这些灵活的子View自然是要有个key了, 不然你会有一个yelloe box来警告你了.

    4. 默认属性值

    这个就要区分了. 类组件与函数组件写法还不一样.

    // 函数组件
    interface IProps {
      id: number;
      text?: string;
    }
    
    const MyView = (props: IProps) => {
      return ( <>....    </>  );
    };
    
    MyView.defaultProps = {
      text: "default"
    };
    = = = = = = = = = =
    
    // 类组件
    class MyScreen extends Component<IProps> {
        static defaultProps = {
            text: "default"
        };
        
    

    其实这里有个小坑. 就是你的defaultProps设定其实可以乱加乱写属性, 可以完全不按IProps来. 这个TS是没法限定的. 网上有专门解决这些问题的文章, 但在我看来都过于复杂, 反而不如这些写来得好看. 好在IProps能扛住大多数的检查, 我们使用也是使用IProps, 而不直接使用defaultProps.

    5. 引用(ref)

    5.1 React

    React中使用ref其实也有多种方式的, 比如说下面两种:

    // React (Approach 1)
    const MyView = () => {
      let viewRef : HTMLDivElement | null;
    
      return (
        <div ref={v => viewRef = v} />
      );
    };
    

    以及

    // React (Approach 2)
    const MyView = () => {
      const viewRef = createRef<HTMLDivElement | null>();
      return (
        <div ref={viewRef}/>
      );
    };
    

    5.2 React Native

    在ref这一块, React Native异于React的就是类型了, 它不再是HTML****Element了.

    const MyView = ()=>{
      let ref: View|null = null ;
      let imageRef = createRef<Image>();
    
      return (
        <View ref={ref}>
          <Image ref={imageRef} source={require("../a.png")} />
        </View>
      )
    }
    

    当然, 我们要注意, 涉及到函数组件, 使用ref是要小心些的. 详细可见React官网说明.

    6. 高阶组件(HoC)

    HoC说是高阶组件, 但它其实就是个函数.只不过入参与返回值都是组件而已. HoC也是一种组合多种组件的一种方式, 用得好了那重复代码大量减少, 逻辑分工明确.

    当然用得差了, 那就是HoC Hell, 比如说: TypeScript for React (Native) 进阶

    (图片来源: miro.medium.com/max/2586/1*…)

    不过在本文中我们还是紧贴TS来讲解. 使用TS来做HoC, 问题主要还是在类型上. 你传进来的组件与返回的新组件, 其类型是什么.

    一个给入参组件添加一个Loading效果的HoC, 可以这样写:

    interface IProps {
      loading: boolean;
    }
    
    const withLoader = <P extends object>(InputComponent: React.ComponentType<P>): React.FC<P & IProps> => {
      props.loading ? (... ) : (...)
      ...
    ;
    

    注意, 这里使用的是React.ComponentType, 这个类型的定义其实就是type ComponentTYpe<P = {}> = ComponentClass<P> | FunctionComponent<P>;, 即函数组件或类组件都行.

    另外, 也注意下Props的声明. 我们的入参因为可以是任意组件, 所以Props不要写死了, 也就是要用泛型. 至于我们的HoC要是有什么自己的需求, 那就可以用 P & IProps来组合.

    p.s. 这个A & B, A | B正是TypeScript的强大之处. 它的类型组件很容易. 这要是换成java, 肯定得再定义一个新类型叫C, 然后C中赋值A与B的所有属性 -- 这就有了重复代码了.

    7. 日常开发中常用的属性

    乍一听, 这好像不算是什么麻烦事. 但在TS中, 你要是没有定义type, 那就是寸步难行. 所以我们得知道一些常见库, 还有React中的常用属性到底是什么类型. 举个例子, react-navigation与redux中那几个dispatch, navigation 都是些什么类型啊?

    下面就是我写的一个成功的例子:

    interface IViewProps {
      // ... your own props
    }
    
    type IProps = IViewProps &
      ViewProps & 
      NavigationScreenProps & 
      ReturnType<typeof mapStateToProps> & 
      ReturnType<typeof mapDispatchToProps>
    
    class MyScreen extends React.Component<IProps, IState> {
      // ....
    }
    

    其中:

    ViewProps 就包含了style, children, onLayout, testID这些属性. 注意这是个react-native类 NavigationScreenProps: 它来自于react-navigation库, 具有navigation, screenProps, navigationOptions等属性 另两个ReturnType则是对应了redux生成的props. 这一个我们后面一章节会讲到

    III. Redux

    Redux, 这个大名鼎鼎的状态容器自然不用详细介绍了. 不过使用TypeScript版本的Redux还是有些地方要注意的.

    1. action

    Redux中有一个AnyAction的类型的, 表示任意Action都行. -- 当然也这要遵循基本法, 即flux中的标准action定义

    而一般在一个模块中, 我们都是说某一个模块是只处理特定一些action的. 如audioPlayer模块就只处理audio play相关的action. 这时我们可以这样:

    export interface IAddAction{
      type: "Add"
    }
    
    export interface IRemoveAction{
      type: "Remove",
      paylaod: {
        id: number
      }
    }
    
    export type MyAction = IAddAction | IRemoveAction
    

    我们可以组合不同的action, 变成一个总的Action. 这样后面的reducer()中就可以使用这个总Action. -- 否则的话, 使用范围更广的AnyAction就定位不准, 容易出错了

    2. state

    这里的state一定要加个类型. redux因为其是Single Source的缘故, 一般它存储的state都不小. 特别是我们有很多个reducer还要一一combine组合之后, 整个应用的全局state就十分大并有层次了. 要是没有一个明确的类型说明, 半年或一两年之后, 整个state就很乱, 不知道哪是哪了. 写过大型项目的同学肯定心有体会了.

    export interface IProduct {
      id: string;
      name: string;
      category: IProductCategory;
      sku: Sku;
    }
    
    export interface MyState {
      readonly products: IProduct | null;
    }
    

    3. reducer

    有了上面的state与action的定义, 现在我们的reducer就空前地清晰起来了. 在reducer里面使用state.某field也会有提示是否正确的, 减少了typo的笔误可能性.

    export const MyReducer : Reducer<MyState, MyAction> = (
      state = new MyState(),
      action: MyAction
    ) => {
      switch(action.type){
        ...
      }
      return state;
    }
    

    4. store

    这里store就麻烦些了, 不过也更清晰了. 麻烦还是主要麻烦在整个应用中的各个reducer可以以不同层次地组合起来. -- 这也将影响我们的state的布局.

    下面就讲一个最简单的例子, 就是只有一层combineReducer()的.

    export interface IAppState {
      products: MyState,
      books: AnotherState
    }
    
    const rootReducer = combineReducer<IAppState>({
      products: MyReducer,
      books: ANotherReducer
    })
    
    export const store = createStore(rootReducer, undefined, applyMiddleware(...));
    

    你要是说你的reducer层次很复杂, 比如说像这样:

    const RootReducer = combineReducer({
      oneReducer,
      combineReducer(
        twoReducer, 
        combineReducer(fourReducer, fiveReducer)),
    
    })
    

    然后要依样画葫芦地写state的层次, 是蛮累的. 所以你还可以这样来减少你的工作量: export type IAppState = ReturnType<typeof RootReducer>

    5. async action

    我在项目是使用Redux-Saga来做异步的. 不过你要是想用Thunx也容易, 就这样:

    export const fetches = async (): Promise<IProduct[]> => {
      await wait(1000);
      return products;
    }
    

    6. AnyAction

    前面讲过, 我们有一个built-in的AnyAction类型, 它的源码其实就是:

    export interface AnyAction extends Action {
      // Allows any extra properties to be defined in an action.
      [extraProps: string]: any
    }
    

    备注: 在TS中, [extraProps: string]: any中有前半截就是指任意key名字(只要其类型是string就行), 至于value是any类型就行.

    这个AnyAction还是少用, 这就像any要少用一样.

    7. Redux-Persist

    若你在项目中使用了Redux-Persist库, 那上面的IAppState的定义就有问题了. 因为Redux-Persist会在我们的appState里再加一个自己的定义, 所以TS会检测到类型不匹配而报错.

    举个栗子来说吧: 我们现在要存一个state是这样的: {book: {id: 22, name: "Harry" } } 但一旦使用了Redux-Persist, 那state就变成了: {book: {id: 22, name: "Harray", _persist: {....} } }

    所以这时我们需要这样改:

    interface IAppState {
    
      // book: IBookState  // ERROR!!!
    
      book: IBookState & PersistPartial;
    
    }
    

    8. React-Redux

    这个其实在上面讲过了, 就是使用ReturnType来做到灵活配置.

    type IProps = ReturnType<typeof mapStateToProps> 
            & ReturnType<typeof mapDispatchToProps>
            & ViewProps
    

    9. Middlewares

    我看到很多书或网页上都是这样定义中间件的:const middleware = store => next => action => {...}. 但其实现在的store真的不是指Redux中的那个store了. 其类型是一个新定义的类型: MiddlewareApi.

    看下它的源码: type MiddlewareAPI = {dispatch: Dispatch, getState: ()=> State} 哈哈, 好吧, 其实和store真的好像.

    那我们要如何用TypeScript来定义一个中间件呢? -- 其中的麻烦还是你不知道一些函数入参的类型. 下面这个小片段就是一个成功的例子:

    const myMiddleware = (store: MiddlewareAPI) 
      => (next: Dispatch<AnyAction>) 
      => (action: AnyAction) => {
          ... ...
    }
    

    注意: 我们在Dispatch中都使用了泛型, 不然编译通不过. 这里其实也是一个你可能会使用AnyAction的地方. 因为你确实不知道会有什么样的action会过来.

    IV. 测试

    先说结论哦, 使用TypeScript来写测试会比较麻烦. 因为TS会检测各种类型, 这样一些Mock的手段会过于hacky而被TS报错, 说类型不匹配.

    下面的例子就是我们使用jest.mock()来注入一些mock方法到Worker类中. 但TS会不知道Workder还有mockReturnThis()方法而报错.

    import { work } from "../Worker"
    
    jest.mock("../Worker")
    
    test("some...", ()=>{
      work.mockReturnThis(); // ERROR!!!, as TypeScript does not know this method exist
      ...
    })
    

    结果为了让其能运行, 你不得不加一个@ts-ignore:

      // @ts-ignore
      work.mockReturnThis()
    

    但加了@ts-ignore, 总是让人不舒服的. 所以我个人推荐, 测试还是用js文件吧.

    V. 其它

    1. lazy init

    TypeScript虽然强大, 也不是尽善尽美. 比如Kotlin中很好用的lateinit var, 在TS中就没有. TS像KotLin一样, 一开始声明const对象就得给值.

    不过我们其实可以走点偏锋.

    interface People {
      id: number,
      name: string
    }
    
    ...
    // const p = {}  // ERROR! `{}` and `People` are not compatilbe
    const p = {} as People
    
    // when time is ripe
    p.id = 100
    

    上面的as People, 指明了类型, 还不用所有属性都赋值, 是方便了. 但也请不要滥用哦, TS的static check正是我们要用它的地方. 像用了上面的这样技巧的地方, 我们最好都是要code review下的.

    备注: 要是使用any那就更不可取了. any基本上TS世界的一个大毒瘤, 不是万不得已不应该使用, 伤人更伤己啊. 以后我可能会专门就这个any, 来讲一下如何避免使用any

    2. 泛型

    泛型是个强大的工具, 用过java或swift的同学都有所了解. 对于js的同学可能比较新, 但也建议去学习一下.

    同样, 在TS中使用泛型要注意. 比如说下面的写法就报错了: TypeScript for React (Native) 进阶

    你去比照下TS官网上的泛型写法,一点都不带差的. 那怎么还报错啊?

    哈哈,这就是个坑了. 注意, 上面出错的代码是在一个.tsx文件里的.

    .tsx文件看到<>时, 首先反应就是, "这是个React的element", 于是想去加载组件.

    所以说:

    在.ts文件中, 上面的代码不会报错. 在.tsx文件中, 上面的代码会报错. 要想修复, 就得告诉TS编译器, "这是个泛型, 不是组件" 具体方法就是:

    // ***.tsx const example = (url: T) : number => { return 20; };

    VI. 总结

    好了, TypeScript的一些进阶技术就介绍完了. 主要还是一些不熟悉的三方库的类型, 和不熟悉的TS的用法 (和java/swift这些语言比起来, 差异性还是有些的). 以后我若有了更多技巧, 再介绍给大家. 多谢大家捧场~


    起源地下载网 » TypeScript for React (Native) 进阶

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元