2020最新React Hooks+TS项目最佳实践

18,881 阅读6分钟

前言:写了千篇一律的React项目。突然想玩点新的花样。平时用JS比较多。但团队配合,TS才是最好的方式。所以这个小项目采用TS。再结合RecoilJs + Swr组合来打造数据处理层。 单元测试说很重要,但真正实行的公司确很少。配合Enzyme+Jtest 来测试react组件,确实很爽。所以将整个过程记录下来。 我们一起学习吧。

一.关键知识扫盲

上面提到了几个关键的框架,我下面分别简单介绍一些,具体的细节可以去他们的官方GitHub上去了解。

1.Recoiljs facebook针对 react hooks新出的状态管理框架,比较轻,好上手。几大优点:灵活共享 state,并保持高性能,高效可靠地根据变化的 state 进行计算,Atom操作只是对可订阅可变state影响,避免全局rerender。还有 Cross-App Observation 跨页面的状态传递。

2. Swr 是提供远程数据请求的React Hooks库,它也能很好的结合axios一起使用。主要特点有:自动间隔轮询,自动重试请求,避免写async和await这种语法糖,也没有回调,结合React hook是比较好用。

3. Enzyme 是 Airbnb 开源的专为 React 服务的测试框架,它的 Api 像 Jquery 一样灵活,因为 Enzyme 是用 cheerio 库解析 html,cheerio 经常用于 node 爬虫解析页面,因此也被成为服务端的 Jquery。Enzyme 实现了一套类似于 Jquery 的 Api,它的核心代码是构建了一个 ReactWrapper 类或者 ShallowWrapper 包裹 React 组件,区别在于 ShallowWrapper 的类只会渲染组件的第一层,不渲染自组件,所以是一种浅渲染。当然它还提供了Mount方法,可以做深渲染。

二. 构建项目基本结构

1.快速创建基本的webpack配置

const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/entry.tsx',
  devtool: "inline-source-map",
  devServer:{
    historyApiFallback: true,
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js']
  },
  module: {
    rules: [{
      test: /\.css$/,
      use: [
        {loader: 'style-loader'},
        {loader: 'css-loader'}
      ]
    }, {
      test: /\.tsx?$/,
      loader: 'ts-loader',
      exclude: /node_modules/
    }]
  },
  plugins: [
    new HtmlWebpackPlugin()
  ]
}

2.定义package.json,并且安装相关依赖

{
  "scripts": {
    "dev": "webpack-dev-server --open",
    "test": "jest"
  },
  "dependencies": {
    "@types/axios": "^0.14.0",
    "@types/enzyme": "^3.10.8",
    "@types/mockjs": "^1.0.3",
    "@types/react-router-dom": "^5.1.6",
    "axios": "^0.21.0",
    "enzyme": "^3.11.0",
    "enzyme-adapter-react-16": "^1.15.5",
    "jest": "^26.6.3",
    "mockjs": "^1.1.0",
    "react": "16.9.0",
    "react-dom": "16.9.0",
    "react-router-dom": "^5.2.0",
    "recoil": "^0.1.2",
    "swr": "^0.3.9"
  },
  "devDependencies": {
    "@babel/preset-env": "^7.12.7",
    "@babel/preset-react": "^7.12.7",
    "@types/enzyme-adapter-react-16": "^1.0.6",
    "@types/jest": "^26.0.15",
    "@types/node": "12.7.2",
    "@types/react": "16.9.2",
    "@types/react-dom": "16.9.0",
    "babel-jest": "^26.6.3",
    "css-loader": "3.2.0",
    "html-webpack-plugin": "3.2.0",
    "identity-obj-proxy": "^3.0.0",
    "react-addons-test-utils": "^15.6.2",
    "react-recoil-hooks-testing-library": "^0.0.8",
    "react-test-renderer": "^17.0.1",
    "source-map-loader": "0.2.4",
    "style-loader": "1.0.0",
    "ts-jest": "^26.4.4",
    "ts-loader": "6.0.4",
    "typescript": "^4.1.2",
    "webpack": "4.39.3",
    "webpack-cli": "3.3.7",
    "webpack-dev-server": "3.8.0"
  }
}

然后在yarn install 即可

3. 构建项目的目录结构和基本的页面

三. 构建接口请求和mock数据

1.先创建一个mock接口,在Mock文件夹下面创建一个Index.ts

import Mock from 'mockjs'

//建立一个mocker数据
Mock.mock("get/options.mock",{
    code:0,
    "data|9-19":[
        {label: /[a-z]{3,5}/, "value|+1": 99,},
    ]
})

解释一下 “data|9-19” 返回的data数据是一个数组,最小9个,最大19个。

label:/[a-z]{3,5}/ : 代表里面的label值是3到5字母,并且随机生成。

value|+1:99 : 代表从value的值从99开始自增

mockjs非常强大,可以构建丰富的数据类型和结构,大家可以去官网了解详细用法。

然后在entry.tsx入口文件中引入这个mock,才能起作用

//entry.tsx page
import React from 'react'
import ReactDOM from 'react-dom'
import './Mock/Index' //引入mock数据

import App from './App';
var reactapp = document.createElement("div");
document.body.appendChild(reactapp);
ReactDOM.render(<App/>, reactapp);
 

2.结合axios构建Swf请求封装

import useSWR, { ConfigInterface, responseInterface } from 'swr'
import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
interface Repository {
  label: string;
  value: string;
}

//创建axios实例
const api = axios.create({
});
type JsonData = {
  code: number,
  data: Repository[]
}

export type GetRequest = AxiosRequestConfig
//定义返回类型
export interface Return<JsonData, Error>
  extends Pick<
  responseInterface<AxiosResponse<JsonData>, AxiosError<Error>>,
  'isValidating' | 'revalidate' | 'error'
  > {
  data: JsonData | undefined
  response: AxiosResponse<JsonData> | undefined
  requestKey: string
}

export interface Config<JsonData = unknown, Error = unknown>
  extends Omit<
  ConfigInterface<AxiosResponse<JsonData>, AxiosError<Error>>,
  'initialData'
  > {
  initialData?: JsonData
}
//useRequest 封装swr的请求hooks函数
export default function useRequest<Error = unknown>(
  request: GetRequest,
  { initialData, ...config }: Config<JsonData, Error> = {}
): Return<JsonData, Error> {
  //如果是开发模式,这里做环境判断,url后面加上".mock"就会走mock数据
  if (process.env.NODE_ENV === "development") {
    request.url += ".mock"
  }
  const requestKey = request && JSON.stringify(request);

  const { data: response, error, isValidating, revalidate } = useSWR<
    AxiosResponse<JsonData>,
    AxiosError<Error>
  >(requestKey, () => api(request), {
    ...config,
    initialData: initialData && {
      status: 200,
      statusText: 'InitialData',
      config: request,
      headers: {},
      data: initialData
    }
  })
  // if(response?.data.code !==0){ //handler request error
  //     throw  "request wrong!"
  // }
  return {
    data: response?.data,
    requestKey,
    response,
    error,
    isValidating,
    revalidate
  }
}

useRequest 的作用其实很简单,就是在hooks组件里面做react请求。他可以这样使用

 const { data: data } = useRequest<Repository[]>({
    url: "get/options"
 })

data 是axios返回的数据 response数据 。

这里还可以这样使用,调出requestKey

 const { data: data,requestKey } = useRequest<Repository[]>({
    url: "get/options"
 })

requestKey 的作用在于,可以使用mutate手动的更新数据并且执行数据修改

//如果这样可以手动刷新数据
mutate(requestKey) 

//如果这样,那么会执行post请求到原来请求,修改数据
mutate(requestKey,{...newData}) 

//这样那么会先调updateFetch promise修改数据后,然后更新requestKey对应的请求
mutate(requestKey,updateFetch(newData)) 

看起来swr还比较强大。 更多用法,请去他的官方github网站上了解。这里就不细讲了 。

四.利用RecoilJS进行状态管理

1.创建state数据

import { atom } from "recoil"
export interface Repository {
  label: string;
  value: string;
}
export const OptionsState = atom<Repository[] | undefined>({
  key: "options",
  default: []
})
export const SelectedState = atom<string[]>({
  key: "selectedValues",
  default: []
})

atom是 recoil创建state 最基本的操作, 其实这里还有一个selector , 它的功能要比atom稍微强大一些

const productCount = selector({
  key: 'productCount',
  get: ({get}) => {
    const products = get(productAtom)
    return products.reduce((count, productItem) => count + productItem.count, 0)
  },
  set?:({set, reset, get}, newValue) => {
   set(productAtom, newValue)
 }
})

这里要注意一点,selector 如果没有set方法, 那么就是这个只读的RecoilValue类型 ,不可修改,如果有get和set才是RecoilState类型 。

get方法可以是异步的 ,类似下面这种,可以进行数据请求。

const productCount = selector({
  key: 'productCount',
  get: aysnc ({get}) => {
      await fetch()
  },
  set?:({set, reset, get}, newValue) => {
   set(productAtom, newValue)
 }
})

Rocoil状态管理的功能非常强大,我这里只抛一个砖,更多细节请看github 。

五 利用Enzyme进行单元测试

利用enzymejs ,可以简单模拟真实用户的行为的去测试组件。 而不是把只能对函数的测试。

提高了测试的覆盖度。 这里主要讲他的三种渲染方式

1.浅渲染(shallow)

  describe('initialize', () => {
    const wrapper = shallow(<MultiCheck {...props} />)
    it('renders the label if label provided', () => {
      expect(wrapper.find(".status").text()).toMatch("test-my-label")
    });

    it(" test is the columns show correctly", () => {
      expect(wrapper.find(".content").get(0).props.style.width).toEqual(160)
    })

  });

wrapper拿到之后就可以各种dom操作了,还可以模拟用户点击,下面代码就先找到一个input,模拟change事件,并发送了一个eventTarget。

  it(" test onChange if click select all", () => {
      let selectAllBtn = wrapper.find(".item").at(0).find("input")
      expect(selectAllBtn).toHaveLength(1)
      selectAllBtn.simulate("change", { target: { checked: true } })
      expect(props.onChange.mock.calls.length).toEqual(1);
   })

2. 完全渲染(mount)

describe('Home', () => {
    const wrapper = mount(<RecoilRoot><Home /></RecoilRoot>)
    it(" test all checkout status if click select all", () => {
        let selectAllBtn = wrapper.find(".item").at(0).find("input")
        expect(selectAllBtn).toHaveLength(1)
        selectAllBtn.simulate("change", { target: { checked: true } })
        wrapper.find(".item").forEach(ele => {
            expect(ele.find('input').get(0).props.checked).toEqual(true)
        })
    })
})

什么时候需要深层渲染, 比如,我们的组件是对RecoilRoot有依赖的, 是一个嵌套组件,如果靠浅渲染,是拿不到子组件的Dom结构的。 所以要用mount。

3 . 静态渲染(render)

describe('<Foo />', () => {
  it('renders three `.foo-bar`s', () => {
    const wrapper = render(<Foo />);
    expect(wrapper.find('.foo-bar')).to.have.lengthOf(3);
  });

  it('renders the title', () => {
    const wrapper = render(<Foo title="unique" />);
    expect(wrapper.text()).to.contain('unique');
  });
});

这种渲染就无法进行事件模拟了,只能对文本进行判断了。

以上几种渲染,在jtest根据项目实际情况来,灵活搭配可以提高测试效率。

六 结语

通过这几个框架的学习,感觉有好处,也有弊端,配合swr和recoil确实能提高开发效率,毕竟redux还是太重了。 但是swr和recoil不能友好的互相结合,selector里面就不能直接用useSwr,这个是一个问题。recoil还是太新。小项目玩一玩可以,大项目上的话还是需要谨慎而为。 好了,告诉大家项目源码地址吧:github.com/tanghui315/…