前言:写了千篇一律的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/…
作者:FelixCoder
链接:https://juejin.cn/post/6898865634982297613
来源:掘金