复杂 React 应用中的TypeScript 3.0实践

如果你了解,也许应该知道我已经很久没有编写没有类型系统的JavaScript了,我非常喜欢 React 这个库,更喜欢使用 TypeScript 来编写 React。虽然网络世界中有很多介绍 React 的文章,但极少发现有介绍如何应用 TypeScript 来编写 React 的文章,于是,将自己的一些使用经验记录下来。

这篇文章有些长,需要你静下心来慢慢阅读,最终你将收获使用 TypeScript 来高效编写复杂的 React 应用。

【WLM-TypeScript-React-Starter】:

准备开始

第一步初始化一个项目,然后我们准备将 TypeScript 和 tslint 安装到本地:

$ mkdir my-ts
$ cd my-ts && npm init -y
$ yarn add typescript tslint tslint-react --dev

创建 tsconfig.json 文件:

$ tsc --init

接着,安装 react,react-dom,react-router-dom,react-redux,redux,redux-thunk,history 以及它们的声明包 #types/history,#types/react,#types/react-dom,#types/react-redux,#types/react-router-dom。

$ yarn add react react-dom react-router-dom react-redux redux redux-thunk
$ yarn add #types/history #types/react #types/react-dom #types/react-redux #types/react-router-dom

注明:将#替换成at符号;

接着,安装 webpack 以及一些必要的 loader:

$ yarn add webpack awesome-typescript-loader --dev
$ yarn add webpack-cli source-map-loader --dev
$ yarn add less-loader style-loader css-loader less --dev

tsconfig.js 文件:

{
  "compilerOptions": {
    "target": "es5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
    "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    "lib": ["dom", "es2015", "es2015.promise"], /* Specify library files to be included in the compilation. */
    "jsx": "react",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
    "sourceMap": true,                     /* Generates corresponding '.map' file. */
    "strict": true,                           /* Enable all strict type-checking options. */
    "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
    "baseUrl": "src",                       /* Base directory to resolve non-absolute module names. */
    "paths": {
      "@/*": ["./*"],
    },                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
    "esModuleInterop": true                   /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
  },
  "include": [
    "./src/**/*"
  ]
}

最后,我们完成一份简单的 webpack.config.js 文件:

var fs = require('fs')
var path = require('path')
var webpack = require('webpack')
const { CheckerPlugin } = require('awesome-typescript-loader');
var ROOT = path.resolve(__dirname)

module.exports = {
  entry: './src/index.tsx',
  devtool: 'source-map',
  output: {
    path: ROOT + '/dist',
    filename: '[name].bundle.js',
    sourceMapFilename: '[name].bundle.map.js'
  },
  module: {
    rules: [
      { test: /\.ts[x]?$/, loader: "awesome-typescript-loader" },
      { enforce: "pre", test: /\.ts[x]$/, loader: "source-map-loader" },
      {
        test: /\.less$/,
        include: ROOT + '/src',
        use: [
          {
            loader: 'style-loader'
          },
          {
            loader: 'css-loader'
          },
          {
            loader: 'less-loader'
          }
        ]
      },
      {
        test: /\.png/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 1024*20
            }
          }
        ]
      }
    ]
  },
  resolve: {
    extensions: [".ts", ".tsx", ".js", ".json", ".png"],
    alias: {
      '@': ROOT + '/src'
    }
  },
  plugins: [
    new CheckerPlugin(),
  ]
}

当完成这些准备工作之后,我们就可以进入TypeScript的世界了。

无状态组件

我们在某些情况下会使用到无状态组件(也就是一个函数),这个无状态组件函数使用 TypeScript 来定义几乎与 JavaScript 很像,如:

import * as React from "react";

const TestPage: React.SFC = () => {
  return (
    <div>
      this is test page.
    </div>
  );
};

export default TestPage;

当我们需要传递 Props 时,只用定义一个 Props 接口,然后给 props 指明类型:

export interface IHeaderProps {
  localImageSrc: string;
  onLineImageSrc: string;
}

export const Header: React.SFC<IHeaderProps> = (props: IHeaderProps) => {
  const { localImageSrc, onLineImageSrc } = props;
  return (
    <div className={styles["header-container"]}>
      <img src={localImageSrc} />
      <img src={onLineImageSrc} />
    </div>
  );
};

有状态组件

假设当我们需要使用到一个有状态的组件,如:因为某些操作(onClick)来改变 state时,我们需要给 state 定义一个接口,与上述的 props 类似,在编写有状态组件时,需要给 React.Component的范型传递你的类型:

export interface IHomePageState {
  name: string;
}

class HomeComponent extends React.Component<{}, IHomePageState> {
  constructor(props: {}) {
    super(props);
    this.state = {
      name: "",
    };
  }

  public setName = () => {
    this.setState({
      name: "icepy",
    });
  }
  
  public render(){
    const { name } = this.state;
    return (
      <div>
         <div onClick={this.setName}> set name </div>
         <div>{name}</div>
      </div>
    )
  }
}

Props & State 组件

对于另外的一些需求,可能我们设计的组件是一个容器或者是什么别的,总之它既有Props又有State,其实从上述的有状态组件中,我们可以很清晰的察觉到 React.Component 第一个参数传的就是 Props 的类型,因此,当我们要使用 Props & State 组件时,就要如此:

export interface IHomePageState {
  name: string;
}

export interface IHomePageProps {
  home: string;
}

class HomeComponent extends React.Component<IHomePageProps, IHomePageState> {
  constructor(props: IHomePageProps) {
    super(props);
    this.state = {
      name: "",
    };
  }

  public setName = () => {
    this.setState({
      name: "icepy",
    });
  }
  
  public render(){
    const { name } = this.state;
    const { home } = this.props;
    return (
      <div>
         <div onClick={this.setName}> set name </div>
         <div>{name} {home}</div>
      </div>
    )
  }
}

Router 组件

当我们存在有多个页面时,就会用到 react-router-dom 路由库,因此在类型安全上,我们需要为我们的 Props 继承上 React-Router 的 Props,才能让编译通过。与上述的 Props & State 组件类似,我们要为我们定义的接口 IHomePageProps 继承 RouteComponentProps,如:

import { RouteComponentProps } from "react-router-dom";

export interface IHomePageProps extends RouteComponentProps<any>{
  home: string;
}

export interface IHomePageProps {
  home: string;
}

class HomeComponent extends React.Component<IHomePageProps, IHomePageState> {
  constructor(props: IHomePageProps) {
    super(props);
    this.state = {
      name: "",
    };
  }

  public setName = () => {
    this.setState({
      name: "icepy",
    });
  }
  
  public render(){
    const { name } = this.state;
    const { home } = this.props;
    return (
      <div>
         <div onClick={this.setName}> set name </div>
         <div>{name} {home}</div>
      </div>
    )
  }
}

页面级别的 Reducers

在我们度过了前面的几个组件之后,可能你的项目会越来越复杂,因此我们会使用到 Redux 来管理我们 React 应用的数据流,页面级别的 Reducers ,顾名思义,这是我们关联在页面容器组件里的 Action,通过这些 Action 和 Props 的结合,方便的管理数据流。

这些 Action 会分为 同步 Action 和 异步 Action,这也是我们为什么会用到 redux-thunk 的原因。

首先,我们来为类型安全定义接口:

// page 

import { Dispatch } from "redux";
import { RouteComponentProps } from "react-router-dom";

export interface IHomePageActionsProps {
  dataSync: () => void;
  dataAsync: (parameter: string) => (dispatch: Dispatch) => void;
}

export interface IHomePageProps extends RouteComponentProps<any>, IHomePageActionsProps {
  homePage: IHomePageStoreState;
}

export interface IHomePageStoreState {
  syncId: string;
  asyncId: string;
}

// global dir 
export interface IStoreState {
  homePage: IHomePageStoreState;
}

然后定义一个 mapStateToProps 函数(没有用装饰器的原因是让你能阅读明白):

const mapStateToProps = (state: IStoreState) => {
  const { homePage } = state;
  return {
    homePage,
  };
};

分别定义 Action 和 Reducers:

// action
import * as CONST from "./constants";
import { Dispatch } from "redux";

export function dataSync() {
  const syncData  = {
    type: CONST.SYNC_DATA,
    payload: {
      data: "syncId=https://github.com/icepy",
    },
  };
  return syncData;
}

export function dataAsync(parameter: string): (dispatch: Dispatch) => void {
  return (dispatch: Dispatch) => {
    const asyncData = {
      type: CONST.ASYNC_DATA,
      payload: {
        data: "asyncId=https://icepy.me",
      },
    };
    setTimeout(() => {
      dispatch(asyncData);
    }, 2000);
  };
}

// reducers
import { IAction } from "@/global/types";
import * as CONST from "./constants";
import * as TYPES from "./types";

const initState: TYPES.IHomePageStoreState = {
  syncId: "默认值",
  asyncId: "默认值",
};

export function homeReducers(state = initState, action: IAction): TYPES.IHomePageStoreState {
  const { type, payload } = action;
  switch (type) {
    case CONST.SYNC_DATA:
      return { ...state, syncId: payload.data };
    case CONST.ASYNC_DATA:
      return { ...state, asyncId: payload.data };
    default:
      return { ...state };
  }
}

在 Store 中 引入我们的 reducers,因为我们已经为 state 定义了类型,因此我们可以很方便的关联上,并且知道哪里有错误:

import { createStore, applyMiddleware, combineReducers, compose } from "redux";
import thunk from "redux-thunk";
import { homeReducers } from "@/pages/Home/flow/homeReducers";

/* eslint-disable no-underscore-dangle, no-undef */
const composeEnhancers = (window as any) && (window as any).REDUX_DEVTOOLS_EXTENSION_COMPOSE || compose;
const reducer = combineReducers({
  homePage: homeReducers,
});

export const configureStore = () => createStore(
  reducer,
  composeEnhancers(applyMiddleware(thunk)),
);

最后,我们使用 connect 函数将这些关联起来:

class HomeComponent extends React.Component<TYPES.IHomePageProps, TYPES.IHomePageState> {
   ... 省略 可自行访问 [WLM-TypeScript-React-Starter] 项目
}

export const HomePage = connect(mapStateToProps, actions)(HomeComponent);

Global级别的 Reducers

global 顾名思义,这是一种可以全局访问的 reducers ,我们要做的事情也和页面级别 reducers 非常类似,定义好 state 的接口,然后将 global 在 Store 中配置正确,如:

import { createStore, applyMiddleware, combineReducers, compose } from "redux";
import thunk from "redux-thunk";
import { homeReducers } from "@/pages/Home/flow/homeReducers";
import { globalReducers } from "./reducers";

/* eslint-disable no-underscore-dangle, no-undef */
const composeEnhancers = (window as any) && (window as any).REDUX_DEVTOOLS_EXTENSION_COMPOSE || compose;
const reducer = combineReducers({
  global: globalReducers,
  homePage: homeReducers,
});

export const configureStore = () => createStore(
  reducer,
  composeEnhancers(applyMiddleware(thunk)),
);

当我们需要访问 global 时,有两种方式:

  1. 在 mapStateToProps 函数中将 global 返回给页面级别的 Props
  2. 随意的调用 global 中的 Action ,只是需要手动的将 dispatch 函数传递给这些 Action
import * as React from "react";
import { Dispatch } from "redux";
import { connect } from "react-redux";
import { HashRouter as Router, Route, NavLink } from "react-router-dom";
import { IStoreState } from "./global/types";
import * as globalActions from "./global/actions";
import { HomePage } from "./pages/Home";
import { TestPage } from "./pages/TestPage";
import "./style.less";

interface IAppComponentProps {
  dispatch: Dispatch;
}

class AppComponent extends React.Component<IAppComponentProps> {
  constructor(props: IAppComponentProps) {
    super(props);
    globalActions.setGlobalSyncId(this.props.dispatch);
  }

  public render() {
    return (
      <Router >
        <div>
          <div className="nav-container">
            <NavLink to="/" >Home Page</NavLink>
            <NavLink to="/test">Test Page</NavLink>
          </div>
          <Route exact={true} path="/" component={HomePage} />
          <Route path="/test" component={TestPage} />
        </div>
      </Router>
    );
  }
}

const mapStateToProps = (state: IStoreState) => {
  const { global } = state;
  return {
    global,
  };
};

export const App = connect(mapStateToProps)(AppComponent);

到此为止,我们的这些组件使用,还不够为一个复杂的 React 应用“服务”,因为我们还需要一些额外的配置,如:tslint,editorconfig,local assets 的处理,yarn,pre-commit 等等,这些额外的集成为多人协作的复杂项目开了一个好头,因此,我们还需要进一步的去处理这些配置,如 tslint:

{
  "extends": ["tslint:recommended", "tslint-react"],
  "rules": {
      "jsx-alignment": true,
      "jsx-wrap-multiline": true,
      "jsx-self-close": true,
      "jsx-space-before-trailing-slash": true,
      "jsx-curly-spacing": "always",
      "jsx-boolean-value": false,
      "jsx-no-multiline-js": false,
      "object-literal-sort-keys": false,
      "ordered-imports": false,
      "no-implicit-dependencies": false,
      "no-submodule-imports": false,
      "no-var-requires": false
  }
}

总结

在使用 TypeScript 和 React 的过程中积累了不少经验,但还有一些使用的技巧没有介绍到,这就需要我们在之后的过程中去慢慢摸索了。好在我们给社区提供了一个开源的 Starter 项目,省去了你在使用中较为复杂的配置,只用按照约定根据范例进行编写即可,希望你也可以从中学习到一些有趣的知识,欢迎交流;

原文https://zhuanlan.zhihu.com/p/42141179

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值