【翻译】提升react编码速度的小技巧

396 阅读13分钟

本文来自Medium上Aleksey Kozin的文章,原文地址:Double Your React Coding Speed With This Simple Trick,本文主要内容为如何编写可复用的react组件,文章有删改,推荐react新手观看,有能力的同学建议观看原文。

如果你使用过react或者react native,应该感受过写代码的速度在减慢,导致这样的原因有很多,比如你会花费更多的时间在处理bug上,单个文件写了大量的业务代码并且维护起来非常麻烦,还有大量重复的逻辑,但你可以通过重构可复用的代码块来提升编码速度。

你将会在这篇文章学到什么?

我们从一个普通的React组件出发,一步步的重构成更高级的组件。希望这篇文章能为你带来收获,提高你的编码速度。

我们将重构的React原始组件

一个简单像我们平时开发的组件,大概会包括以下内容:

  • 从后端获取列表数据
  • 当页面需要加载时展示"loading"
  • 将已加载的数据用列表(列表卡片)的形式展示
  • 当用户点击列表的卡片时会展示弹窗并展示详情内容

这像不像你写的代码?

import React, { useEffect, useState } from 'react';
import { FlatList, Text, View, StyleSheet, Modal, TouchableOpacity } from 'react-native';

import AddModal from '../components/AddModal';
import LoadingIndicator from '../components/LoadingIndicator'
import BrowserItem from '../components/BrowserItem'

import colors from '../config/colors';

function Browsers() {

    const URL = 'https://google.com/myData.json'

    // Element of json
    // {"Browsers":[
    //     {
    //      "fullname": "Chrome",
    //      "linkToBrowser": "https://google.com",
    //      "image": "https://linktoimage.com/chrome.png",
    //      "minMemory": "1 GB",
    //      "currentVersion": "29.0.1",
    //      "minimumRAM": "2 GB",
    //      "description": "How much RAM do you have? Ha-ha",
    //      "windows": true,
    //      "mac": true,
    //      "linux": true,
    //      "ubuntu": true,
    //      "fedora": false,
    //      "stars": 4,
    //      "id":"chrome"
    //    },
    // ...
    // ]
    // }

    const [loading, setLoading] = useState(true)
    const [browsers, setBrowsers] = useState([])

    const [modalVisible, setModalVisible] = useState(false)
    const [description, setDescription] = useState("")

    const changeDescription = (description) => {
        setDescription(description)
        setModalVisible(!modalVisible)
    }

    const changeOpacity = () => {
        setModalVisible(!modalVisible)
        console.log('changeOpacity')
    }



    useEffect(() => {
        fetch(URL)
            .then((response) => response.json())
            .then((responseJson) => {
                return responseJson.Browsers
            })
            .then(browsers => {
                setBrowsers(browsers)
                // console.log(browsers)
                setLoading(false)
            })
            .catch(error => {
                console.log(error)
            })
            .finally(() => setLoading(false));
    }, [])


    return (
        <View style={styles.container}>
            {loading ? (
                <LoadingIndicator />
            ) : (
                  <View>
                    <AddModal 
                      modalVisible={modalVisible}
                      changeOpacity = {() => changeOpacity()}
                      description={description}
                    />
                    <FlatList
                      data={browsers}
                      keyExtractor={browser => browser.fullname}
                      renderItem={({ item }) =>
                        <BrowserItem
                          fullname={item.fullname}
                          image={item.image}
                          linkToBrowser={item.linkToBrowser}
                          minMemory={item.minMemory}
                          currentVersion={item.currentVersion}
                          minimumRAM={item.minimumRAM}
                          description={item.description}
                          windows={item.windows}
                          mac={item.mac}
                          linux={item.linux}
                          ubuntu={item.ubuntu}
                          fedora={item.fedora}
                          stars={item.stars}
                          changeDescription={() => changeDescription(item.description)}
                        />
                      }
                    />
                  </View>
                )
            }
        </View >
    );
};

const styles = StyleSheet.create({
    container: {
        justifyContent: 'center',
        alignItems: 'center'
    },
})

export default Browsers;

先来说说这段代码的优点

  • 它能工作,具有完整的下载和展示给用户的功能。
  • 它使用了React Hooks

这段代码的缺点

这个组件被设计成只能在一个App的一处地方使用一次(不能复用)

记住这个口头禅:

成为快速的开发者需要掌握开发可复用代码块的艺术

可复用的代码块和方法可以为你带来:

  • 减少书写代码的时间
  • 减少查找bug的时间
  • 减少阅读代码的时间
  • 对你复用的代码做更少的测试

如何使上面的那段代码变得可复用?

将常量移动到组件的props中

function Browsers({url = 'https://google.com/myData.json'}) {
  const URL = url
  ...
}

我们将常量URL移动到props中,这样我们就通过传入不同的URL复用组件。我们只做了一点小改动,产生了不错的效果。

热身结束,接下来我们来做点儿更cool的东西。

分离业务逻辑和视觉逻辑

这是这篇文章最重要的部分。如果你能从这篇文章学到一个东西,那一定是:

成为更快的React开发者一定是掌握了分离业务逻辑和视觉逻辑的人。

业务逻辑:处理和保存状态,比如<Browser />组件中写在return上面的代码。

UI逻辑:所有展示在显示器上的内容以及读取用户的输入,比如return中的所有代码。

让我们从分离这两部分逻辑开始,来看如何将我们的代码变得可复用。

我们将做什么:

创建一个自定义React HookuseBrowsers()来存放业务逻辑

  • 创建React组件<BrowsersList />,使用props来处理视觉逻辑。
import React, {useEffect, useState} from 'react'
import {FlatList, StyleSheet, View} from 'react-native'

import AddModal from '../components/AddModal'
import LoadingIndicator from '../components/LoadingIndicator'
import BrowserItem from '../components/BrowserItem'

const styles = StyleSheet.create({
  container: {
    justifyContent: 'center',
    alignItems: 'center',
  },
})

function useBrowsers(url) {
  const [loading, setLoading] = useState(true)
  const [browsers, setBrowsers] = useState([])

  const [modalVisible, setModalVisible] = useState(false)
  const [description, setDescription] = useState('')

  const changeDescription = (description) => {
    setDescription(description)
    setModalVisible(!modalVisible)
  }

  const changeOpacity = () => {
    setModalVisible(!modalVisible)
    console.log('changeOpacity')
  }

  useEffect(() => {
    fetch(URL)
      .then((response) => response.json())
      .then((responseJson) => {
        return responseJson.Browsers
      })
      .then((browsers) => {
        setBrowsers(browsers)
        // console.log(browsers)
        setLoading(false)
      })
      .catch((error) => {
        console.log(error)
      })
      .finally(() => setLoading(false))
  }, [])

  return {
    loading,
    browsers,
    modalVisible,
    description,
    changeDescription,
    changeOpacity,
  }
}

function BrowsersList({
  loading,
  browsers,
  modalVisible,
  description,
  changeDescription,
  changeOpacity,
}) {
  return (
    <View style={styles.container}>
      {loading ? (
        <LoadingIndicator />
      ) : (
        <View>
          <AddModal
            modalVisible={modalVisible}
            changeOpacity={() => changeOpacity()}
            description={description}
          />
          <FlatList
            data={browsers}
            keyExtractor={(browser) => browser.fullname}
            renderItem={({item}) => (
              <BrowserItem
                fullname={item.fullname}
                image={item.image}
                linkToBrowser={item.linkToBrowser}
                minMemory={item.minMemory}
                currentVersion={item.currentVersion}
                minimumRAM={item.minimumRAM}
                description={item.description}
                windows={item.windows}
                mac={item.mac}
                linux={item.linux}
                ubuntu={item.ubuntu}
                fedora={item.fedora}
                stars={item.stars}
                changeDescription={() => changeDescription(item.description)}
              />
            )}
          />
        </View>
      )}
    </View>
  )
}

function Browsers() {
  return <BrowsersList {...useBrowsers('https://google.com/myData.json')} />
}

export default Browsers

我们对代码做了一些移动,虽然离完美还很遥远,但它确实变得更容易复用了:

  • 我们可以让<BrowserList>使用不同的数据源。之前我们只能接受传入的HTTP请求的数据,现在我们可以接受来自内存,硬盘或者其它地方的数据了。
  • 我们可以在其它地方使用useBrowsers()了。

将代码分割成更小的可复用文件

首先,将代码分成两部分更有利于我们重构。将代码分成不同的文件/模块更有利于代码的阅读。这也可以使开发者更容易理解每个模块的代码是独立的。每个模块也更容易理解和复用。

这是个典型的React项目结构:

  • index.jsBrowsers.jsx导出<Browsers />组件。
  • componentshooks文件夹的代码与<Browsers/>组件相关。
  • BrowsersList.jsx也可以继续创建与之相关的hookscomponents以及index

React 项目结构像是一个递归树

关于更多React项目结构的内容可以看David Gilbertson

接下来看BrowsersList.jsx:

import React from 'react'
import {FlatList, StyleSheet, View} from 'react-native'
import AddModal from '../../../components/AddModal'
import LoadingIndicator from '../../../components/LoadingIndicator'
import BrowserItem from '../../../components/BrowserItem'

const styles = StyleSheet.create({
  container: {
    justifyContent: 'center',
    alignItems: 'center',
  },
})

export function BrowsersList({
  loading,
  browsers,
  modalVisible,
  description,
  changeDescription,
  changeOpacity,
}) {
  return (
    <View style={styles.container}>
      {loading ? (
        <LoadingIndicator />
      ) : (
        <View>
          <AddModal
            modalVisible={modalVisible}
            changeOpacity={() => changeOpacity()}
            description={description}
          />
          <FlatList
            data={browsers}
            keyExtractor={(browser) => browser.fullname}
            renderItem={({item}) => (
              <BrowserItem
                fullname={item.fullname}
                image={item.image}
                linkToBrowser={item.linkToBrowser}
                minMemory={item.minMemory}
                currentVersion={item.currentVersion}
                minimumRAM={item.minimumRAM}
                description={item.description}
                windows={item.windows}
                mac={item.mac}
                linux={item.linux}
                ubuntu={item.ubuntu}
                fedora={item.fedora}
                stars={item.stars}
                changeDescription={() => changeDescription(item.description)}
              />
            )}
          />
        </View>
      )}
    </View>
  )
}

ok,代码变得更少了,也没有这么杂乱无章了。现在我们可以集中精力重构这部分的代码。

但在处理这部分代码之前先要解决一个设计上的大问题。

关于复用组件的一个大问题

先看一下<BrowsersList />组件函数的签名:

  • 如果我们重命名一些props,比如说changeDescription改为setSelectedBrowser或者description改为browser
  • 或者我们删除一些props
  • 或者我们添加一些props

每个改动都会导致<BrowsersList / >组件崩溃。

每次修改组件的签名,每个引用这个组件的地方就会报错。你会经常修改签名,毕竟你不会一开始就把组件设计好。

你的IDE不能在你重构的过程中自动重命名。你不得不手动修改每个引用组件的地方。这将导致你编码的速度变慢。

最惨的部分是你不知道哪里会崩溃。唯一能捕获到错误的方法是:

  • 运行app
  • 手动跳转到崩溃的组件
  • 报错
  • 阅读报错信息
  • 修改bug然后再试一次

这是个又慢又乏味的任务,但这种情况又经常在生产环境上发生。

来看我们的Browsers.jsx文件,它会报错吗?

import React from 'react'
import {BrowsersList} from './components/BrowsersList'
import {useBrowsers} from './hooks/useBrowsers'

export function Browsers() {
  return <BrowsersList {...useBrowsers('https://google.com/myData.json')} />
}

谁知道呢!需要对比BrowsersList.jsxuseBrowsers.js的参数才能确定。

这会崩溃吗?

// useBrowsers.js
...
return {
    loading,
    browsers,
    modalVisible,
    descripton,
    changeDescription,
    changeOpacity,
  }
...
  
// BrowsersList.jsx
...
export function BrowsersList({
  loading,
  browsers,
  modalVisible,
  description,
  changeDescription,
  changeOpacity,
}) {
....

当然会,因为useBrowsers.js中的descripton缺少了一个'i',是个错字。

许多初级开发者花费很长时间去debug试图去寻找哪里出了问题,结果发现是props值时出了问题(本该使用string却使用了number)。这里有更快的解决这个问题的方法。

停止使用JS开发,开始拥抱TypeScript吧

2021年每个React/React Native的开发者都没有理由不使用TypeScript。

TypeScript很cool的特点

更少的bugs

更少的bugs = 更快的编码速度

通过TS我们很轻松的找到了错字:

你的IDE会自动提示你React Pros的参数,当你使用第三方库并且不知道props的名称时这将节省你大量时间:

你将会获得"重构名称"的新技能点。这将节省你重命名props的时间,并且一次修改后其它使用这个props的地方也会自动重命名。

你也不会再忘记添加null/undefined的检查了。

当然,在我们输入错误的props时我们也会接受到高亮的错误:

TS能轻松的为你减少时间的同时也会减少debug的压力。

设计流畅开发系统第一步:声明types

tips:本文不是TS的教程。如果你有不理解的地方,先简单了解一下TypeScript再回来看本文

让我们重新回到<BrowsersList />组件并且声明props的types(不要忘了将你的.jsx重命名为.tsx)。

// ...

export type Browser = {
  fullname: string // we expect "fullname" Browser field to be a string
  image: string
  linkToBrowser: string
  minMemory: string
  currentVersion: string
  minimumRAM: string
  description: string
  windows: boolean
  mac: boolean
  linux: boolean
  ubuntu: boolean
  fedora: boolean
  stars: number
}

export type BrowsersListProps = {
  loading: boolean
  browsers: Browser[] // we expect "browsers" prop to be an array of Browser
  modalVisible: boolean
  description: string
  
  // we expect "changeDescription" prop to be a function
  // taking a string as an input and returning nothing
  changeDescription: (description: string) => void
  
  // we expect "changeOpacity" prop to be a function
  // taking nothing and returning nothing
  changeOpacity: () => void
}

export function BrowsersList({
  loading,
  browsers,
  modalVisible,
  description,
  changeDescription,
  changeOpacity,
}: BrowsersListProps) {
  return (

// ...

之后,更新useBrowsers()的签名:

import {useEffect, useState} from 'react'
import {BrowsersListProps} from '../components/BrowsersList'

export function useBrowsers(url: string): BrowsersListProps {
  const [loading, setLoading] = useState(true)
  
 // ...

现在TS会校验useBrowsers()BrowsersList的参数是否兼容。如果我们修改过BrowsersList的props,我们会得到报错。仅此一项就可以保证减少生产环境中的错误。

快速的系统架构

BrowsersListProps目前看起来还是很乱:

export type BrowsersListProps = {
  loading: boolean
  browsers: Browser[]
  modalVisible: boolean
  description: string
  changeDescription: (description: string) => void
  changeOpacity: () => void
}

  • 组件要展示loading的状态,需要占用一行type声明
  • 组件要展示Browsers的数组,需要占用一行type声明
  • 组件要展示模态框的详细信息,这将占用四行type声明。模态框的详情可能会需要更多的字段(也许在将来),这种定义方法将限制我们展示更多的字段,记住,修改组件的签名的代价是非常高的。

我们可以缩短type的声明并重构,并且用Browser的type重构模态框的特性。

这将减少props的复杂性:

这个小的重构练习向你证明了TS的一个显著特征:快速的系统设计。

types非常简单。它不需要写很多代码,但可以为系统提供很多有用的信息,因此你不需要写太多的代码就可以使用types并且设计你的代码。减少你进行架构设计的时间,也可以推动你设计出更易用,更快的系统。

声明types之后开始填充组件主体

让我们修复我们的<BrowsersList />组件以便它可以处理BrowsersListProps的签名。我们可以重构<BrowserItem>以便它可以只使用两个参数。这将使代码变得更加易读和快速。

import React from 'react'
import {FlatList, StyleSheet, View} from 'react-native'
import AddModal from '../../../components/AddModal'
import LoadingIndicator from '../../../components/LoadingIndicator'
import BrowserItem from '../../../components/BrowserItem'

const styles = StyleSheet.create({
  container: {
    justifyContent: 'center',
    alignItems: 'center',
  },
})

export type Browser = {
  fullname: string
  image: string
  linkToBrowser: string
  minMemory: string
  currentVersion: string
  minimumRAM: string
  description: string
  windows: boolean
  mac: boolean
  linux: boolean
  ubuntu: boolean
  fedora: boolean
  stars: number
}

export type BrowsersListProps = {
  loading: boolean
  browsers?: Browser[]
  selectedBrowser?: Browser
  setSelectedBrowser: (browser?: Browser) => void
}

export function BrowsersList(props: BrowsersListProps) {
  const {loading, selectedBrowser, setSelectedBrowser, browsers} = props
  return (
    <View style={styles.container}>
      {loading ? (
        <LoadingIndicator />
      ) : (
        <View>
          <AddModal
            modalVisible={Boolean(selectedBrowser)}
            onClose={() => setSelectedBrowser(undefined)}
            description={selectedBrowser?.description}
          />
          <FlatList
            data={browsers}
            keyExtractor={(browser) => browser.fullname}
            renderItem={({item}) => (
              <BrowserItem
                browser={item}
                onPress={() => setSelectedBrowser(item)}
              />
            )}
          />
        </View>
      )}
    </View>
  )
}


这个组件已经看起来更加可读而且不那么可怕了。

从中抽离出可复用的逻辑

开发者通过观察组件会有不同的感受。比如,我觉得在中展示loading的状态是一项非常cool的功能,因为它可以在不同的组件中复用。

我们创建<UIFriendlyList/>组件取代<FlatList/>。这个新组件具有展示loading状态的能力。

与之前一样我们要创建类型:


type UIFriendlyListProps<T> = FlatListProps<T> & {loading?: boolean}

  • "T"是一个参数类型。参数的类型被叫做泛型。"T"和"UIFriendlyList"的关系和"arg"和"foo(arg)"方法的关系相同。如果你想依据一个类型构造另一个类型,可以使用泛型。
  • "&"是一个交叉类型。type X = A & B意味着X包含A和B。

关于这次改动:

  • 我们声明了UIFriendlyLsitProps的声明。
  • 定义了一个泛型
  • UIFriendlyListProps 扩展了 FlatListProps的 'loading' 状态
  • 我们通过FlatListProps{loading?: boolean}交叉类型定义了UIFriendlyListProps

让我们以此为主体创建一个UIFriendlyList.jsx文件:


import React from 'react'
import {FlatList, FlatListProps, Text} from 'react-native'
import LoadingIndicator from './LoadingIndicator'

export type UIFriendlyListProps<T> = FlatListProps<T> & {loading?: boolean}

export function UIFriendlyList<T>(props: UIFriendlyListProps<T>) {
  if (props.loading) {
    return <LoadingIndicator />
  }

  if (props?.data && props.data.length === 0) {
    return <Text>This list is empty (</Text>
  }

  return <FlatList {...props} />
}

我们还添加了一个空的状态。所以当用户知道列表为空时就不需要等待接口的请求了,非常友好的设计。

现在,让我们确认下:

import React from 'react'
import {StyleSheet, View} from 'react-native'
import AddModal from '../../../components/AddModal'
import BrowserItem from '../../../components/BrowserItem'
import {UIFriendlyList} from '../../../components/UIFriendlyList'

const styles = StyleSheet.create({
  container: {
    justifyContent: 'center',
    alignItems: 'center',
  },
})

export type Browser = {
  fullname: string
  image: string
  linkToBrowser: string
  minMemory: string
  currentVersion: string
  minimumRAM: string
  description: string
  windows: boolean
  mac: boolean
  linux: boolean
  ubuntu: boolean
  fedora: boolean
  stars: number
}

export type BrowsersListProps = {
  loading: boolean
  browsers?: Browser[]
  selectedBrowser?: Browser
  setSelectedBrowser: (browser?: Browser) => void
}

export function BrowsersList(props: BrowsersListProps) {
  const {loading, selectedBrowser, setSelectedBrowser, browsers} = props
  return (
    <View style={styles.container}>
      <AddModal
        modalVisible={Boolean(selectedBrowser)}
        onClose={() => setSelectedBrowser(undefined)}
        description={selectedBrowser?.description}
      />
      <UIFriendlyList
        loading={loading}
        data={browsers}
        renderItem={({item}) => (
          <BrowserItem
            key={item.fullname}
            browser={item}
            onPress={() => setSelectedBrowser(item)}
          />
        )}
      />
    </View>
  )
}

与之前的初始代码相比,现在的组件更容易理解了。 “抽象可复用的代码”是一项无休止的工作,所以一旦你觉得效果达到了预期,就该停止,去做其它工作了。

接下来,我们该把注意力放在业务逻辑上,开始处理useBrowsers() hook

重构业务逻辑

我们让useBrowsers()返回一个BrowsersListProps类型的对象。我也重构了Loading,现在它在fetch前为true,fetch之后变为false。

import {useEffect, useState} from 'react'
import {Browser, BrowsersListProps} from '../components/BrowsersList'

export function useBrowsers(url: string): BrowsersListProps {
  const [loading, setLoading] = useState(false)
  const [browsers, setBrowsers] = useState<Browser[]>([])
  const [selectedBrowser, setSelectedBrowser] = useState<Browser | undefined>(
    undefined,
  )

  useEffect(() => {
    setLoading(true)
    fetch(url)
      .then((response) => response.json())
      .then((responseJson) => {
        return responseJson.Browsers
      })
      .then((browsers) => {
        setBrowsers(browsers)
      })
      .catch((error) => {
        console.log(error)
      })
      .finally(() => setLoading(false))
  }, [url])

  return {
    loading,
    browsers,
    selectedBrowser,
    setSelectedBrowser,
  }
}

这还没结束,我们还可以更深入的抽象和构建可复用的代码块。“在接口请求数据并赋值的过程中一直在展示loading”这部分代码非常适合处理成代码块,我们可以把它和userBrowsers()分离。

与之前一样我们需要声明一些types。我们需要自定义一个useFetch()hook用来获取数据并且控制loading状态。我们也需要定义从服务端获取数据的结构(FetchBrowsersResults):

export type FetchBrowsersResults = {
  Browsers: Browser[]
}

export type UseFetch<T> = {
  loading: boolean
  
  // We use Generic. T - is a type argument that can be any type.
  // We can useFetch() with any type
  // ? means, that T can be undefined
  data?: T
}

export function useFetch<T>(url: string): UseFetch<T> {}

我们专门创建了一个useFetch.ts文件用来存放useFetch()的代码:

import {useEffect, useState} from 'react'
import {Alert} from 'react-native'

export type UseFetch<T> = {
  loading: boolean
  data?: T 
}

export function useFetch<T>(url: string): UseFetch<T> {
  const [loading, setLoading] = useState<boolean>(false)
  const [data, setData] = useState<T | undefined>(undefined)

  useEffect(() => {
    setLoading(true)
    fetch(url)
      .then((response) => response.json())
      .then(setData)
      .finally(() => setLoading(false))
      .catch((error) => Alert.alert('Fetch error', error))
  }, [url])

  return {
    loading,
    data,
  }
}

现在让我们重构useBrowsers()hook:

import {useState} from 'react'
import {Browser, BrowsersListProps} from '../components/BrowsersList'
import {useFetch} from '../../../hooks/useFetch'

export type FetchBrowsersResults = {
  Browsers: Browser[]
}

export function useBrowsers(url: string): BrowsersListProps {
  const {loading, data} = useFetch<FetchBrowsersResults>(url)
  const [selectedBrowser, setSelectedBrowser] = useState<Browser | undefined>(
    undefined,
  )

  return {
    loading,
    browsers: data?.Browsers,
    selectedBrowser,
    setSelectedBrowser,
  }
}

与之前相比,现在的代码更容易理解了。目前看来,代码也没有进一步抽象的必要了。