我的简易记账本之React实现

464 阅读7分钟

前一阵学习 Vue 制作了自己的简易记账本,最近学习了 React,决定用 React 再实现一版,以此来巩固和练习 React 基础知识。 这个小项目的功能与上一版基本一致,每个页面的逻辑也大体相同,相关内容记录在我的另一篇博客《用 Vue 实现一个简易记账本》。这篇主要来记录用 React 实现过程的一些思路和知识点总结。

链接:点击预览 Github源代码

需求改进

根据自己对上一版本的使用情况,决定增加一个新的功能是在统计页对每一条显示的账单记录添加一个编辑功能,也就是添加一个新的路由到一个编辑页,提供删除或编辑该条记录的按钮。

项目记录

1. 项目的创建与配置

  • 使用 create-react-app 搭建项目,步骤如下:
yarn global add create-react-app // 或者使用 npm 安装
create-react-app 项目名 --template typescript 
cd 项目文件夹
yarn start // 会自动打开浏览器
  • css normalize 在 index.css 添加
@import-normalize;

即可,作用是保证页面在不同浏览器上默认样式相近。

  • scss 支持

根据 cra 官网,需要安装 node-sass

  • 设置 create-react-app 引用中使用绝对路径
  1. 在 tsconfig.json 中加入

image.png

  1. WebStorm 将 src 目录设置为 Resource Root

image.png

  1. 然后选择 Settings > Editor > Code Style > TypeScript, 进入 Imports tab 下勾选 Use paths relative to tsconfig.json

image.png

好了~ 测试一下:

image.png

image.png

image.png

生效 image.png

项目搭建和基本配置完成。更多可参考 CRA 官方文档

2. 一些用到的知识点

  • React Router 基本用法 首先根据需求,按照文档的基本用法,我写下了如下代码,用 Redirect 重定向到记账页作为app的主页。
<Router>
    <Route exact path="/tags" component={Tags}></Route>
    <Route exact path="/tags/:id" component={Tag}></Route>
    <Route exact path="/money" component={Money}></Route>
    <Route exact path="/statistics" component={Statistics}></Route>
    <Route exact path="/statistics/:recordId" component={Record}></Route>
    <Route exact path="/money/:recordId/edit" component={Money}></Route>
    <Redirect exact from="/" to="/money"/>
    <Route path="*" component={NoMatch}></Route>
<Router>

但是这样写却导致了死循环,再次查阅文档,文档中有这样的解释:

最后,路由算法会根据定义的顺序自顶向下匹配路由。因此,当你拥有两个兄弟路由节点配置时,你必须确认前一个路由不会匹配后一个路由中的路径。例如,千万不要这么做:

<Route path="/comments" ... />
<Redirect from="/comments" ... />

也就是说,根据匹配原则,它会认为每个页面都需要重新跳转到 /money 对应的页面,就造成了死循环。解放方法是用 Switch 路由组件,代码改写为:

<Router>
  <Switch>
    <Route exact path="/tags" component={Tags}>
    </Route>
    <Route exact path="/tags/:id" component={Tag}>
    </Route>
    <Route exact path="/money" component={Money}>
    </Route>
    <Route exact path="/statistics" component={Statistics}>
    </Route>
    <Route exact path="/statistics/:recordId" component={Record}>
    </Route>
    <Route exact path="/money/:recordId/edit" component={Money}>
    </Route>
    <Redirect exact from="/" to="/money"/>
    <Route path="*" component={NoMatch}>
    </Route>
  </Switch>
</Router>

可以在理解上认为没有 Switch 组件就相当于if else;有switch就是 JS 的 switch, 只会匹配一个。以后书写路由可以通过书写Switch组件节省性能。

在导航组件中添加 NavLink,用来切换路由。NavLink 是另一种版本的 Link,可以在匹配到路由后给标签自动添加样式属性,activeClassName自定义匹配到后添加的属性名。

<ul>
  <li>
    <NavLink to="/tags" activeClassName="selected">
      <Icon name="tag"/>
      标签
    </NavLink>
  </li>
  <li>
    <NavLink to="/money" activeClassName="selected">
      <Icon name="money"/>
      记一笔
    </NavLink>
  </li>
  <li>
    <NavLink to="/statistics" activeClassName="selected">
      <Icon name="statistics"/>
      统计
    </NavLink>
  </li>
</ul>
  • 批量引入所有 svg

和在 vue 项目的处理一样,需要安装 svg-sprite-loadersvgo-loader 并且进行相关配置。

引入单个 svg 的方式是:

import x from 'icons/id.svg';

<svg>
    <use xlinkHref="#id" />
</svg>

但是由于 Tree Shaking, 一些没有使用但被 import 的文件会在 webpack 打包的最终文件中删除, 所以只能借助 require 语法引入,这样这部分代码就不会应用 Tree Shaking。

require('icons/id.svg');

为了更快捷的应用 svg,试图找到一个方法,可以一次性将所有的 svg 引入,在使用时无需一一 import。经过一番搜索,可以通过以下方法实现:

  1. 运行 yarn eject 生成 config 文件夹
  2. webpack.config.js 中, 找到 module > rules > oneOf 下面添加
{
  test: /.svg$/,
  use: [
    { loader: 'svg-sprite-loader', options: {} },
    { loader: 'svgo-loader', options: {
        plugins:[
          {removeAttrs: {attrs: 'fill'}} // 在导入 icon 时自动去掉原有的 fill 颜色
        ]
      }
    }
  ]
},
  1. 在 引入 svg 的文件中添加:
let importAll = (requireContext: __WebpackModuleApi.RequireContext) => requireContext.keys().forEach(requireContext);
try {
    importAll(require.context('icons', true, /\.svg$/)); //第一个参数为放 svg 的文件夹
} catch (error) {
    console.log(error);
} 

做完这些,就可以直接通过 svg 的名字使用了!

<svg>
    <use xlinkHref="#id" />
</svg>
  • React Hooks 的使用 1. useState
const [state, setState] = useState(initialState);

返回一个 state,以及更新 state 的函数。

在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。

setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。

setState(newState);

在后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state。

2. useEffect

useEffect(didUpdate);

该 Hook 接收一个包含命令式、且可能有副作用代码的函数。

使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。

默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它在只有某些值改变的时候才执行。

effect 的执行时机:传给 useEffect 的函数会在浏览器完成布局与绘制之后,在一个延迟事件中被调用。虽然 useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。在开始新的更新前,React 总会先清除上一轮渲染的 effect。

默认情况下,effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发生变化,它就会被重新创建。为了避免一些不需要的执行,可以给 useEffect 传递第二个参数,它是 effect 所依赖的值数组

如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。

注意: 如果传入第二个参数,要确保数组中包含了所有外部作用域中会发生变化且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。

3. useRef

const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。

4. 自定义 Hooks

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。

我们可以将重复的逻辑处理提取到自定义 Hook 中,它就像一个正常的函数,但是它的名字必须以use开头,否则无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的Hook是否违反了Hook 规则。

自定义一个useUpdate Hook,实现第一次由 undefined 变为 0 时不执行函数,只有在后来发生变化执行函数的 Hook。

import {useEffect, useRef} from 'react';

export const useUpdate = (fn:()=>void, dependency:any[]) => {
  const count = useRef(0);
  useEffect(() => {
    count.current += 1;
  })
  useEffect(() => {
    if(count.current > 1) {
      fn();
    }
  }, [fn, dependency]);
}

更多查阅 React文档

  • React Router Hooks 的使用

1. useHistory

useHistory 可以访问可用于导航的历史记录实例。

2. useParams

useParams 返回 URL 参数的键/值对对象。使用它来访问当前 的 match.params。

更多查阅 React Router文档

  • React 中的数据不变性

react 强调数据不可变,不可以在原数据上进行变动,只能通过重新赋值实现数据的更新。

不可变性的好处:

  1. 简化复杂的功能:不可变性使得复杂的特性更容易实现。
  2. 跟踪数据的改变:如果直接修改数据,那么就很难跟踪到数据的改变。跟踪数据的改变需要可变对象可以与改变之前的版本进行对比,这样整个对象树都需要被遍历一次。跟踪不可变数据的变化相对来说就容易多了。如果发现对象变成了一个新对象,那么我们就可以说对象发生改变了。
  3. 确定在 React 中何时重新渲染:不可变性最主要的优势在于它可以帮助我们在 React 中创建 pure components。我们可以很轻松的确定不可变数据是否发生了改变,从而确定何时对组件进行重新渲染。
  • 数据管理

本地数据存入 localStorage, 使用自定义 Hook 封装对数据的操作 API,各个组件通过自定义 Hook 获取对数据的操作 API 即可。

总结

以上就是用 React 完成我的记账本的简单记录。通过这个小项目,让我对 React、React Hooks有了深刻的理解,还有很多不足之处,希望通过更多的学习提升自己。