React和TS学习

wxvirus2023年7月15日
大约 13 分钟

React + Ts 学习

准备工作

  • 安装nodejs,16 往上都是可以的

  • 使用国内的npm镜像,镜像地址open in new window

    使用方式:

    $ npm install -g cnpm --registry=https://registry.npmmirror.com
    
  • 编辑器使用vscode

  • 安装git进行代码版本管理

创建项目

资料

create-react-app 中文官网open in new window

vitejs 官网open in new window

使用 create-react-app 创建项目

npx create-react-app my-app

加上--template typescript使用typescript语言模板。

启动项目

npm start

停止项目

ctrl c

使用 vite 创建项目

npm create vite@latest my-vue-app --template vue

默认使用模板为vue,可以改成react-ts

npm create vite@latest react-demo-vite --template react-ts
  • 选择React框架
  • 选择Typescript
  • 进入项目目录,npm install
  • 启动项目: npm run dev

最终选择

选择create-react-app

  • React官网推荐
  • 时间更久,可查找资源更多
  • 稳定性较强,求稳不求新

使用 prettier 来规范代码的风格

安装插件

进入项目控制台

npm install prettier eslint-config-prettier eslint-plugin-prettier eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-react --save-dev

创建配置文件

创建一个.eslintrc.js文件进行配置

module.exports = {
    root: true,
    env: {
        browser: true,
        es2021: true,
        node: true,
    },
    extends: ['eslint:recommended', 'plugin:react/recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
    overrides: [],
    parser: '@typescript-eslint/parser',
    parserOptions: {
        esmaVersion: 'latest',
        sourceType: 'module',
    },
    plugins: ['react', '@typescript-eslint'],
    rules: {},
};

vscode里安装prettier格式化插件

配置.vscode/settings.json在代码保存的时候自动格式化

{
    "editor.codeActionsOnSave": {
        "source.fixAll.eslint": true
    }
}

最后配置.prettierrc.js

module.exports = {
    // 箭头函数只有一个参数的时候可以忽略括号
    arrowParens: 'avoid',
    // 括号内部不要出现空格
    bracketSpacing: true,
    // 行结束符使用 unix 格式
    endOfLine: 'lf',
    jsxBracketSameLine: false,
    // 行宽
    printWidth: 100,
    // 换行方式
    proseWrap: 'preserve',
    // 分号
    semi: false,
    // 使用单引号
    singleQuote: true,
    // 缩进
    tabWidth: 2,
    // 使用 tab 缩进
    useTabs: false,
    // 后置逗号,多行对象,数组在最后一行加逗号
    trailingComma: 'es5',
    parser: 'typescript',
};

如果遇到啥无效的时候,重启 vscode 尝试

创建命令并使用

"scripts": {
        "start": "react-scripts start",
        "build": "react-scripts build",
        "test": "react-scripts test",
        "eject": "react-scripts eject",
        "lint": "eslint 'src/**/*.+(ts|ts|jsx|tsx)' ",
        "format": " prettier --write 'src/**/*.+(ts|ts|jsx|tsx)' "
    }
# 这样就可以直接格式化对应的代码
npm run format

提交到 git 仓库

选择仓库

  • 工作中:使用公司内部git仓库
  • 正式的开源项目,需要积累star,使用github
  • 个人学习项目,建议选择国内平台,coding.net
  • 配置好仓库之后将本地代码添加并推送到远程仓库

使用 husky 来规范流程

  • 一个git hook工具
  • git commit之前执行自定义的命令
  • 在提交代码之前执行代码风格的检查,避免提交非规范代码
  • github 地址open in new window

安装 husky

npm install husky -D

使用

npm pkg set scripts.prepare="husky install"
npm run prepare

添加一个 hook

我们使用npm run lint来检测代码风格,使用npm run format来将上一步检测的代码进行重新格式化,最后如果上一步成功了,然后将代码添加到本地仓库。

npx husky add .husky/pre-commit "npm run lint"
npx husky add .husky/pre-commit "npm run format"
npx husky add .husky/pre-commit "git add ."

可以看到.husky/pre-commit文件内容

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run lint
npm run format
git add .

commit-lint

前提是安装了 husky

github 地址open in new window

安装(我是 mac,对应的 github 官网地址又对应的文档区分)

npm install --save-dev @commitlint/{config-conventional,cli}

配置

echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js

添加 hook

npx husky add .husky/commit-msg  'npx --no -- commitlint --edit ${1}'

.husky/commit-msg

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx --no -- commitlint --edit ${1}

此时会在提交的commit -m 里的描述进行检测是否规范。

这样提交的信息例如:

git commit -m "chore: commit-lint"

这样才能通过规范。

主要有这么些可选type

'build',
'chore',
'ci',
'docs',
'feat',
'fix',
'perf',
'refactor',
'revert',
'style',
'test',

vite 和 webpack 的区别

  • webpack 是一个非常流行的前端打包工具
  • Create-React-App 内部使用的是 webpack 进行打包
  • Vite 即是构建工具,又是打包工具
  • webpack 会把目录下的 js 都合并到一个文件里进行打包

Vite 的特点

  • Vite 比 Create-React-App 打包项目更快(启动、代码更新)
  • Vite 使用了 ES Module 的语法(仅开发环境)
  • 打包方式,可以在script引入的地方加上一个type="module"<script src="./js/main.js" type="module"></script>

JSX 和组件

JSX 语法

  • JSX- JS 的扩展,卸载 JS 代码里,组件的 UI 结构
  • 语法和 HTML 很相似
  • 已成为 ES 规范(非 React 独有),可用于其他框架,如 Vu3

文件格式:

  • js
  • jsx
  • ts
  • tsx

标签

基本和 HTML 的标签差不多,区别如下:

  • 大小写,小写就是 HTML 标签,大写是自定义组件
  • 标签必须要闭合,如<input> 没有/jsx中是非法的
  • 每一片段只能有一个根节点,避免多个组合到一起回产生垃圾标签,还有一个<></>这种方式来替换

属性

也和 HTML 的属性差不多,也有一些区别:

  • class要换成className,因为在ts或js语法中class都是一个类关键字,这个时候就不能使用class

  • style要使用 JS 对象,不能是string,而且key要使用驼峰写法,例如:<a style={{backgroundColor: "red"}}></a>

  • for要改为htmlFor,因为还是关键字,不是很常用,例如:

    <div>
        <label htmlFor='username'>姓名</label>
        <input id='username' />
    </div>
    

事件

  • 使用onXxx的形式
  • 必须传入一个函数(是fn而非fn())
  • 注意Typescript类型(初学不深究)
import React from 'react';
import type { MouseEvent } from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
    const fn = (event: MouseEvent<HTMLButtonElement>) => {
        // 阻止默认行为
        event.preventDefault();
        // 阻止冒泡
        event.stopPropagation();
        console.log('clicked');
    };
    return (
        <div className='App'>
            <header className='App-header'>
                <img src={logo} className='App-logo' alt='logo' />
                <p>
                    Edit <code>src/App.tsx</code> and save to reload.
                </p>
                <a className='App-link' href='https://reactjs.org' target='_blank' rel='noopener noreferrer' style={{ backgroundColor: 'red' }}>
                    Learn React edit
                </a>
                <div>
                    <button onClick={fn}>click</button>
                </div>
            </header>
        </div>
    );
}

export default App;

Typescript类型

练习官网open in new window

function print<T>(info: T) {
    console.log(info);
}

// 给这个 T 传了一个 string info 就是 string 类型
print<String>('hello');

对于学习 Java 的后端来说,这个在 Java 里是泛型的意思。看使用这个函数的人传递什么类型的。

class Foo {
    info: string;
    setInfo(newInfo: string) {
        this.info = newInfo;
    }
}

const f1: Foo = new Foo();
f1.setInfo('hello world');

react中可以使用import type {} from 'react'来获取类型,内置的不用获取。


多参数传递调用

import React from 'react';
import type { MouseEvent } from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
    const fn = (event: MouseEvent<HTMLButtonElement>, name: string) => {
        // 阻止默认行为
        event.preventDefault();
        // 阻止冒泡
        event.stopPropagation();
        console.log(name);
    };
    return (
        <div className='App'>
            <header className='App-header'>
                <img src={logo} className='App-logo' alt='logo' />
                <p>
                    Edit <code>src/App.tsx</code> and save to reload.
                </p>
                <a className='App-link' href='https://reactjs.org' target='_blank' rel='noopener noreferrer' style={{ backgroundColor: 'red' }}>
                    Learn React edit
                </a>
                <div>
                    <button
                        onClick={event => {
                            fn(event, '无解'); // 里面还是函数
                        }}
                    >
                        click
                    </button>
                </div>
            </header>
        </div>
    );
}

export default App;

插入 JS 变量

前面的代码demo里都有一个外置的大括号

<a className='App-link' href='https://reactjs.org' target='_blank' rel='noopener noreferrer' style={{ backgroundColor: 'red' }}></a>

{ backgroundColor: 'red' }这一段本身是 JS 对象,而外面的大括号就是用于可插入 JS 函数以及其他内容。

  • 使用{xxx}可以插入 JS 变量、函数、表达式
  • 可插入普通文本、属性
  • 可用于注释: {/* <p>{logo}</p> */},注释必须闭合的

条件判断

通常都是使用if else elseif来判断;在jsx/tsx里使用方式:

  • 使用 &&
  • 使用三元表达式
  • 使用函数
const flag = true;

function Hello() {
    if (flag) {
        return <p>Hello</p>;
    } else {
        return <p>你好</p>;
    }
}

return (
    <div>
        <div>
            {flag && <p>Hello</p>}
            {flag ? <p>Hello</p> : <p>你好</p>}
            <Hello></Hello> {/* 标签首字母大写,自定义组件 */}
        </div>
    </div>
);

循环

  • 使用数组
  • 每个item元素需要key属性
  • key在同级别需要唯一性
import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
    const list = [
        { username: '无解', name: 'wujie' },
        { username: '张三', name: 'zhangsan' },
        { username: '李四', name: 'lisi' },
    ];
    return (
        <div className='App'>
            <header className='App-header'>
                <ul>
                    {list.map((user, index) => {
                        const { username, name } = user;
                        return (
                            <li key={username} data-index={index}>
                                {name}
                            </li>
                        );
                    })}
                </ul>
            </header>
        </div>
    );
}

export default App;

注意不能使用index作为key,虽然简短的代码看不出问题,后续修改的时候可能不会唯一

代码演练:开发列表页

import React from 'react';
import './App.css';

function App() {
    // 列表页

    // 问卷列表数组
    const questionList = [
        { id: 1, title: '问卷 1', isPublished: true },
        { id: 2, title: '问卷 2', isPublished: false },
        { id: 3, title: '问卷 3', isPublished: true },
        { id: 4, title: '问卷 4', isPublished: false },
    ];

    function edit(id: number) {
        console.log('edit', id);
    }
    return (
        <div>
            <h1>问卷列表页</h1>
            <div>
                {questionList.map(question => {
                    const { id, title, isPublished } = question;
                    return (
                        <div key={id}>
                            <strong>{title}</strong> &nbsp;
                            {/* 条件判断 */}
                            {isPublished ? <span style={{ color: 'green' }}>已发布</span> : <span>未发布</span>}
                            &nbsp;
                            <button
                                onClick={() => {
                                    edit(id);
                                }}
                            >
                                编辑问卷
                            </button>
                        </div>
                    );
                })}
            </div>
        </div>
    );
}

export default App;

组件

React 一切皆是组件

  • 组件就是一个 UI 片段
  • 拥有独立的逻辑和显示
  • 组件可大可小,可嵌套

组件的价值和意义

  • 组件嵌套来组织 UI 结构,和 HTML 一样,无学习成本
  • 组件拆分,利于代码维护,和多人协作开发
  • 可封装公共组件(或第三方组件)复用代码,提高开发效率

组件形式

  • class组件
  • 函数组件
  • React16 之后比较推崇函数组件和 Hooks

将前面的列表代码封装成一个组件独立出去

List1.tsx

import React, { FC } from 'react';
import './List1.css';

const List1: FC = () => {
    // 问卷列表数组
    const questionList = [
        { id: 1, title: '问卷 1', isPublished: true },
        { id: 2, title: '问卷 2', isPublished: false },
        { id: 3, title: '问卷 3', isPublished: true },
        { id: 4, title: '问卷 4', isPublished: false },
    ];

    function edit(id: number) {
        console.log('edit', id);
    }
    return (
        <div>
            <h1>问卷列表页</h1>
            <div>
                {questionList.map(question => {
                    const { id, title, isPublished } = question;
                    return (
                        <div key={id} className='list-item'>
                            <strong>{title}</strong> &nbsp;
                            {/* 条件判断 */}
                            {isPublished ? <span style={{ color: 'green' }}>已发布</span> : <span>未发布</span>}
                            &nbsp;
                            <button
                                onClick={() => {
                                    edit(id);
                                }}
                            >
                                编辑问卷
                            </button>
                        </div>
                    );
                })}
            </div>
        </div>
    );
};

export default List1;

App.tsx

import React from 'react';
import List1 from './List1';

function App() {
    return (
        <>
            <List1 />
        </>
    );
}

export default App;

React 开发者工具,chrome 插件

百度搜索:chrome react dev tools

谷歌下载地址open in new window

国内访问可以通过bing去搜索,如果有资源可以上网,那就使用谷歌搜索出来的

JSX 和 Vue 模板的区别

  • 判断,Vue 模板中使用v-if指令,JSX 使用三元表达式,函数和&&
  • 循环,Vue 模板中使用的是v-for指令,JSX 使用map函数
  • React - 能用 JS 的就用 JS 的,需要 JS 基础比较扎实的使用
  • Vue - 简单使用iffor就可以方便初学者使用和记忆
  • 现在 Vue3 也能很好的支持 JSX 语法,同时还是继承前面的指令

React Hooks

  • React 内置的 Hooks
    • useState
    • useEffect
    • 其他内置
  • 自定义 Hooks (复用代码)
  • 使用第三方 Hooks(提高效率)

注意事项

  • Hooks 是 React 最重要的内容
  • 必须练习,练习,练习!
  • Hooks 有很对规则,遇到错误时,先查看是否违反规则

useState

需求:让页面“动”起来

  • 点击一个button,累加数量
  • 用普通变量,无法实现
  • 使用useState实现
  • 用普通的 JS 变量无法触发组件的更新
import React, { useState } from 'react';

function App() {
    // let count = 0
    const [count, setCount] = useState(0);

    function add() {
        setCount(count + 1);
    }
    return (
        <>
            <div>
                <button onClick={add}>add {count}</button>
            </div>
        </>
    );
}

export default App;

useState设置返回的结果是一个数组,第一个元素是这个变量,第二个是改变这个变量的函数,必须传入的是一个新值。

state一个组件的“独家记忆”

  • props父组件传递过来的信息
  • state组件内部的状态信息,不对外
  • state变化,触发组件更新,重新渲染rerender页面

上面代码中,count相当于statestate的变化就相当于setCount方法。

state的几个特点

  • 异步更新: 无法拿到最新的state的值

    import React, { FC, useState } from 'react';
    
    const Demo: FC = () => {
        const [count, setCount] = useState(0);
    
        function add() {
            // setCount(count + 1)
            setCount(count => count + 1);
            console.log('cur count', count);
        }
    
        return (
            <div>
                <button onClick={add}>add {count}</button>
            </div>
        );
    };
    
    export default Demo;
    
  • 注意,如果设置的一个变量没有在tsx/jsx里引用,按理说就不需要使用setstate来管理它,使用useRef

  • 可能会被合并

    import React, { FC, useState } from 'react';
    
    const Demo: FC = () => {
        const [count, setCount] = useState(0);
    
        function add() {
            setCount(count + 1);
            setCount(count + 1);
            setCount(count + 1);
            setCount(count + 1);
            setCount(count + 1);
            console.log('cur count', count);
        }
    
        return (
            <div>
                <button onClick={add}>add {count}</button>
            </div>
        );
    };
    
    export default Demo;
    

    上面每次都是0+1,所以不会打印 5,也可以使用函数的方式来更新,就会是 5

    import React, { FC, useState } from 'react';
    
    const Demo: FC = () => {
        const [count, setCount] = useState(0);
    
        function add() {
            setCount(count => count + 1);
            setCount(count => count + 1);
            setCount(count => count + 1);
            setCount(count => count + 1);
            setCount(count => count + 1);
            console.log('cur count', count);
        }
    
        return (
            <div>
                <button onClick={add}>add {count}</button>
            </div>
        );
    };
    
    export default Demo;
    

    使用函数更新 state 不会被合并

  • 不可变数据

    import React, { FC, useState } from 'react'
    
    const Demo2: FC = () => {
      const [userInfo, setUserInfo] = useState({ name: '无解', age: 12 })
    
      function changeAge() {
        // 不可变数据
        // 不去修改 state 的值,而是传入一个新的值
        // ES6 解构语法
        setUserInfo({ ...userInfo, age: 21 })
      }
    
      const [list, setList] = useState(['x', 'y'])
    
      function addItem() {
        // 不可变数据
        // push 返回的是插入数据的位置
        // list.push('z')
        // concat 返回的是一个新数组
        setList(list.concat('z'))
        // 解构赋值语法
        setList([...list, 'z'])
      }
    
      return (
        <div>
          <h2>state 不可变数据</h2>
          <div>{JSON.stringify(userInfo)}</div>
          <button onClick={changeAge}>change age</button>
          <div>{JSON.stringify(list)}</div>
          <button onClick={addItem}>add item</button>
        </div>
      )
    }
    
    export default Demo2
    
    

重构列表页

import React, { FC, useState } from 'react'
import QuestionCard from './components/QuestionCard'

const List2: FC = () => {
  const [questionList, setQuestionList] = useState([
    { id: '1', title: '问卷 1', isPublished: true },
    { id: '2', title: '问卷 2', isPublished: false },
    { id: '3', title: '问卷 3', isPublished: true },
    { id: '4', title: '问卷 4', isPublished: false },
  ])

  function add() {
    // 随机数
    const r = Math.random().toString().slice(-3)
    setQuestionList(
      questionList.concat({
        id: 'q' + r,
        title: '问卷5' + r,
        isPublished: false,
      })
    )
  }

  function deleteQuestion(id: string) {
    // state 是不可变的数据
    // 删除如何操作 使用 filter 过滤掉 id 对应的数据
    setQuestionList(
      questionList.filter(q => {
        if (q.id === id) return false
        else return true
      })
    )
  }

  function publishQuestion(id: string) {
    // 修改使用 map
    setQuestionList(
      questionList.map(q => {
        // 不等于要修改的直接返回
        if (q.id !== id) return q
        return {
          ...q,
          isPublished: true,
        }
      })
    )
  }

  return (
    <div>
      <h2>问卷列表 2</h2>
      <div>
        {questionList.map(question => {
          const { id, title, isPublished } = question
          return (
            <QuestionCard
              key={id}
              id={id}
              title={title}
              isPublished={isPublished}
              deleteQuestion={deleteQuestion}
              publishQuestion={publishQuestion}
            />
          )
        })}
      </div>
      <div>
        <button onClick={add}>新增问卷</button>
      </div>
    </div>
  )
}

export default List2

组件QuestionCard

import React, { FC } from 'react'

// ts 自定义类型
type PropsType = {
  id: string
  title: string
  isPublished: boolean
  // 函数类型 可选属性类型
  deleteQuestion?: (id: string) => void
  publishQuestion?: (id: string) => void
}

const QuestionCard: FC<PropsType> = props => {
  const { id, title, isPublished, deleteQuestion, publishQuestion } = props

  function pub(id: string) {
    // 组件的状态提升
    publishQuestion && publishQuestion(id)
  }

  function del(id: string) {
    // 需要通知父组件进行删除
    // 执行父组件的删除操作
    deleteQuestion && deleteQuestion(id)
  }

  return (
    <div key={id} className="list-item">
      <strong>{title}</strong>
      {isPublished ? <span style={{ color: 'green' }}>已发布</span> : <span>未发布</span>}
      <button
        onClick={() => {
          pub(id)
        }}
      >
        发布问卷
      </button>
      &nbsp;
      <button onClick={() => del(id)}>删除</button>
    </div>
  )
}

export default QuestionCard

使用 immer

  • state是不可变数据
  • 操作成本高,有很大的不稳定性

就可以使用immer可以避免这些问题

安装

npm install immer --save

ImmerDemo.tsx

import React, { FC, useState } from 'react'
import { produce } from 'immer'

const Demo: FC = () => {
  const [userInfo, setUserInfo] = useState({ name: '无解', age: 12 })
  const [list, setList] = useState(['x', 'y'])

  function changeAge() {
    // setUserInfo({ ...userInfo, age: 21 })
    setUserInfo(
      // 可以直接修改某个字段
      produce(draft => {
        draft.age = 21
        draft.name = '无解的游戏'
      })
    )
  }

  function addItem() {
    setList(
      produce(draft => {
        draft.push('z')
      })
    )
  }

  return (
    <div>
      <h2>state 不可变数据</h2>
      <div>{JSON.stringify(userInfo)}</div>
      <button onClick={changeAge}>change age</button>
      <div>{JSON.stringify(list)}</div>
      <button onClick={addItem}>add item</button>
    </div>
  )
}

export default Demo

现在修改我们的内容都可以使用js的一些特有的方法和习惯。

Loading...