likes
comments
collection
share

react + ts 的项目初始化

作者站长头像
站长
· 阅读数 21

前言

由于我接触的项目大多数都是以 vue 为主的,所以每次在学习 react 的过程中都无法快速切换,都要花几天时间熟悉语法逐渐适应。

我相信知识是互通的,有了 Vue 的基础再去学习任何一个框架都会变得更加容易和顺畅。 Vue 和 React 的共通之处:

  • 它们都采用了组件化的开发方式,都支持虚拟 DOM 技术,
  • 都有丰富的生命周期钩子函数等。

它们也有一些不同之处:

  • Vue 更加注重模板和指令的使用,而 React 更加注重 JSX 语法和函数式编程的思想。

如果你已经掌握了 Vue 的基础知识,那么学习 React 的过程中,可以将两者进行对比和类比,找出它们之间的异同点,从而更好地理解 React 的设计思想和使用方式。

那么接下来就跟着我的节奏从头开始创建一个 react 项目吧 !!!

相比函数组组件而言,类式组件的学习成本相对高一点,由于平时在学习 react 的时候基本使用函数式组件,对类的使用相对较少,所以本文主要以类式组件的方式来介绍,希望能够对 class 能更加熟悉。

初始化项目

首先要确保当前的环境已经安装了 Node.js 和 npm(或者使用 yarn)

大多数时候我们会先安装 create-react-app 通过 npm 安装项目的 create-react-app 脚手架,它可以帮助我们快速创建一个 React 项目,同时也会自动配置一些常用的工具和依赖项,比如 Babel、Webpack、ESLint 等。

# Mac 下可能需要加 sudo
npm install -g create-react-app

**不过这个方案官方并不推荐,**官方推荐我们卸载掉全局的 create-react-app

npm uninstall -g create-react-app

再使用 npx 去创建项目,因为这个可以保证我们使用的 create-react-app 是最新的

npx create-react-app myreact

npx 是 npm 5.2.0 版本引入的一个命令行工具,它可以帮助我们执行本地安装的 npm 包中的可执行文件(即 bin 目录下的文件)。

同时 npx 中还有其他一些功能,比如执行远程 npm 包中的可执行文件,或者在不安装全局模块的情况下使用全局模块中的可执行文件等。

所以我们可以通过 npx 命令来创建一个 React 项目:

  • 项目创建完成后的目录结构
  • package.json:包管理文件
  • node_modules:局部安装的第三方模块
  • .gitignore:被git忽略的目录或者文件
  • src:开发目录
  • index.js:整个项目的入口文件
  • app.js:根组件
  • public:存放项目模板文件,以及通过href或者src引入的外部文件
  • readme.md:说明文档
  • vscode插件:ES7 React/Redux
  • react-devtools

jsx

jsx 是一种语法糖,是 js 和 xml 的混合语法,将组建结构、数据、样式聚合在一起,最终通过babel 转译成 React.createElement 语法,其中 @babel/preset-react 会负责把HTML标签转成JS代码。

  • JSX 是React 的核心内容
  • JSX 表示在JS代码中写HTML结构,是React声明式的体现
  • 插值表达式、class 属性、事件绑定使用 { } 进行绑定
  • jsx 中 style 的绑定的 {{ }} 双括号,是表示内联样式的对象
  • 事件绑定使用 onClick,在使用事件修饰符的情况下使用 nativeOnClick,或使用对象的方式进行绑定
  • 自定义组件在引入后可直接使用
  • JSX 是React 的核心内容
  • JSX 表示在JS代码中写HTML结构,是React声明式的体现
  • 在JSX中,直接渲染对象或者数组时,会产生 "Objects are not valid as a React child" 错误
    • jsx 必须渲染原始值,比如 字符串及数值。
    • 可以通过 JSON.stringify(value) 序列化展示,但会省略 函数、undefined、symbol 值(以上类型的数据不是有效的 JSON 值)

jsx 中的事件绑定

jsx 中事件绑定的五种方式:

  1. 普通绑定

    直接调用并且不需要加(),因此无法传参

 class App extends React.Component{
     // 事件函数
     fn1(event){
         console.log('fn1')
         console.log(this) // undefined
         console.log(event); // 事件对象
     }
     render(){
         <button onClick={ this.fn1 }>按钮1</button>
     }
 }

注意 onClick={this.handleSubmit} 末尾没有括号! 不要调用事件处理函数:你只需要传递下去。当用户单击按钮时,React 将调用您的事件处理程序。

  1. bind 绑定:
    • 通过bind改变点击事件内的this指向外部组件内this
<input
  type="email"
  name="email"
  value={this.state.email}
  onChange={(e) => this.handleChange(e)} // 通过使用箭头函数来指向外部组件内this
/>
  • 通过在构造函数constructor内使用bind对函数内的this重定向
class App extends React.Component{
   constructor(){
       super() // ES6 类继承类, constructor() 里面用 this 必须调用 super() 函数
       // this 指向当前类组件
       // 调用公用的 属性 和 方法 必须加 this
       this.fn3 = this.fn3.bind(this)
   }
   // 事件函数
   fn3(event){
       console.log('fn3');
       console.log(this); 
       console.log(event);
   }
   render(){
       <button onClick={ this.fn3 }>按钮3</button>
   }
}

  1. 箭头函数绑定
    • 通过箭头函数来指向外部组件内的 this
class App extends React.Component{
    // 不是真实的事件函数
    fn4(){
        console.log('fn4');
        console.log(this); // Class
    }
    render(){
        // 箭头函数才是事件函数
        <button onClick={ () => this.fn4() }>按钮4</button>
    }
}

  • 将事件函数写成箭头函数来指向外部组件内this
class App extends React.Component{
    fn5 = () => {
        console.log('fn5');
        console.log(this); 
    }
    render(){
        <button onClick={ this.fn5 }>按钮5</button>
    }
}

如何在 jsx 中实现 v-if、 v-else 效果呢?

虽然 v-if 和 v-else 是 Vue 中的指令,但是在 React 中我们也可以使用运算符来实现类似功能,根据表达式的值来动态地添加或移除元素。

  1. 对 v-if 的实现可以借助 && 逻辑运算符

当 actived 为 true 时才会渲染所要展示的元素

render(){
	return (
    { actived && <button onClick={confirm}>确定</button>}
  )
}
  1. 借助三元表达式实现 v-if、v-else 的效果

使用三元表达式实现条件渲染

render(){
	return (
    { actived ? <button onClick={confirm}>确定</button> : <button onClick={cancle}>取消</button>}
  )
}

配置

react 核心包安装

  • react 包为react的核心代码
  • react-dom 是React剥离出的涉及DOM操作的部分,react-dom保证数据与页面保持一致。
npm install react react-dom react-redux--save

修改默认打包配置

使用 react-app-rewired 对 webpack 的配置进行复写 react-app-rewired@2.x 版本需要搭配 customize-cra 使用。 customize-cra 能帮助你自定义 react 脚手架 2.x 版本配置

npm install react-app-rewired customize-cra --save-dev

在 package.json 同级目录下新建 config-overides.js 进行配置

// config-overrides.js
/* eslint-disable no-useless-computed-key */
const {
  override,
  addWebpackAlias,
  addWebpackResolve,
  fixBabelImports,
  addLessLoader,
  adjustStyleLoaders,
  addWebpackPlugin,
  addWebpackModuleRule,
} = require('customize-cra');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); // 代码压缩
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); // 大文件定位
const ProgressBarPlugin = require('progress-bar-webpack-plugin'); // 打包进度
const CompressionPlugin = require('compression-webpack-plugin'); // gzip压缩
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // css压缩
const path = require('path');

module.exports = override(
  // 导入文件的时候可以不用添加文件的后缀名
  addWebpackResolve({
    extensions: ['.tsx', '.ts', '.js', '.jsx', '.json'],
  }),
  // 路径别名
  addWebpackAlias({
    ['@']: path.resolve(__dirname, 'src'),
  }),
  // less预加载器配置
  addLessLoader({
    strictMath: true,
    noIeCompat: true,
    modifyVars: {
      '@primary-color': '#1DA57A', // for example, you use Ant Design to change theme color.
    },
    cssLoaderOptions: {}, // .less file used css-loader option, not all CSS file.
    cssModules: {
      localIdentName: '[path][name]__[local]--[hash:base64:5]', // if you use CSS Modules, and custom `localIdentName`, default is '[local]--[hash:base64:5]'.
    },
  }),
  // 注意是production环境启动该plugin
  process.env.NODE_ENV === 'production' &&
  addWebpackPlugin(
    new UglifyJsPlugin({
      // 开启打包缓存
      cache: true,
      // 开启多线程打包
      parallel: true,
      uglifyOptions: {
        // 删除警告
        warnings: false,
        // 压缩
        compress: {
          // 移除console
          drop_console: true,
          // 移除debugger
          drop_debugger: true,
        },
      },
    })
  ),
  addWebpackPlugin(new MiniCssExtractPlugin()),
  // 判断环境变量ANALYZER参数的值
  process.env.ANALYZER && addWebpackPlugin(new BundleAnalyzerPlugin()),
  // 打包进度条
  addWebpackPlugin(new ProgressBarPlugin()),
  // 需要nginx配合
  // addWebpackPlugin(
  //   new CompressionPlugin({
  //     filename: '[path][base].gz',
  //     algorithm: 'gzip',
  //     test: /\.js$|\.html$|\.css/,
  //     threshold: 10240, // 只有大小大于该值的资源会被处理 10240字节
  //     minRatio: 1, // 只有压缩率小于这个值的资源才会被处理
  //     deleteOriginalAssets: false, // 删除原文件
  //   })
  // )
  addWebpackModuleRule({
    test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
    loader: require.resolve('url-loader'),
    options: {
      limit: 64,
      name: 'static/media/[name].[hash:8].[ext]',
    },
  })
  addWebpackModuleRule({
  test: [/\.css$/, /\.less$/], // 可以打包后缀为sass/scss/css的文件
  use: ['style-loader', 'css-loader', 'less-loader'],
})
);

  • addWebpackAlias 相当于 webpack的Plugin
  • addWebpackModuleRule 相当于webpack里Module的rules
  • addWebpackAlias 相当于 webpack里resolve的alias,添加路径别名
  • addWebpackExternals 相当于 webpack里Externals,添加外部扩展CDN

改写package.json 的启动命令


原来的:
"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject"

}

修改后的:
"scripts": {
  "start": "react-app-rewired start",
  "build": "react-app-rewired build",
  "test": "react-app-rewired test",
  "eject": "react-scripts eject"
}

打包过程中对静态资源的处理:

  • 如果需要引入外部文件
    • 放在assets目录下,assets 下的资源会参与wepback的打包流程
    • 使用相对路径或者@
  • 放在public目录下的文件不会参与到打包构建中
    • 路径抬头是 %PUBLIC_URL%/

typescript 配置

一般 react 项目中搭配 typesScript 使用效果最佳,在编译时检查代码中的类型错误,及时的帮助我们发现和解决问题提高代码质量,在开发过程中明确声明的变量和函数的类型更有易于项目的阅读和维护。

接下来就为我们的项目接入 typeScript 配置:

  • 安装 typScript 相关库
npm install typescript --save 
  • 安装@types声明库
npm i @types/react @types/react-dom @types/react-redux --save-dev

react + ts 的项目初始化

路由、sass安装

  • 使用路由
npm install react-router-dom --save
  • 使用sass
npm install node-sass --save-dev

将css文件全部改成scss

  • 使用多个类名

classnames 是对 React的classname的属性扩展,因为原生的React的并不支持动态的添加多个类名

npm install classnames --save

当设置样式时需要同时添加静态类名和动态类名可以使用模板字符串来添加设置

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

function MyComponent(props) {
  const { isActive } = props;
  const staticClass = "my-component";
  const dynamicClass = isActive ? "active" : "";

  return (
    <div className={`${staticClass} ${dynamicClass}`}>
      {/* Your component content */}
    </div>
  );
}

prettier + eslint 配置

配置 prettier

prettier:代码格式化工具,统一代码风格,Prettier会处理格式规则

npm install prettier -D

pnpm add prettier -D

安装完成后 声明两个文件,.prettierignore表示prettier忽略不需要去检查的目录(可以理解为对了错了是我的事,不需要你管),而.prettierrc.js就是我们的prettier规则定义的地方。

# Ignore artifacts:
node_modules
dist
.vscode
.prettierignore

定义 prettier 的格式化规则

module.exports = {
  printWidth: 140,
  tabWidth: 2,
  semi: true,
  singleQuote: true,
  trailingComma: 'none',
  bracketSpacing: true,
  jsxBracketSameLine: true,
  arrowParens: 'always',
  insertPragma: false,
  requirePragma: false,
  useTabs: false
};

安装 prettier 的 插件 react + ts 的项目初始化 在 setting.json 中配置

{
	// prettier规则使用当前目录的.prettierrc.js
	"prettier.configPath": ".prettierrc.js",
	// 保存的时候自动格式化
	"editor.formatOnSave": true,
}

eslint: 主要用于解决代码质量问题,通过AST 来分析我们代码,从而提供两种提示:

  • 代码质量问题:使用方式有可能有问题(problematic patterns)
  • 代码风格问题:风格不符合一定规则 (doesn’t adhere to certain style guidelines)

代码质量规则 (code-quality rules):

no-unused-vars

no-extra-bind

no-implicit-globals

prefer-promise-reject-errors


代码风格规则 (code-formatting rules):

max-len

no-mixed-spaces-and-tabs

keyword-spacing

comma-style

安装 eslint

npm i eslint -D

pnpm add eslint -D

声明一个 .eslintrc.js 文件:

module.exports = {
  parser: '@typescript-eslint/parser', // 定义ESLint的解析器
  extends: ['eslint:recommended', 'plugin:prettier/recommended', 'react-app'], //定义文件继承的子规范
  plugins: ['@typescript-eslint', 'react-hooks', 'eslint-plugin-react'], //定义了该eslint文件所依赖的插件
  env: {
    //指定代码的运行环境
    browser: true,
    node: true
  },
  settings: {
    //自动发现React的版本,从而进行规范react代码
    react: {
      pragma: 'React',
      version: 'detect'
    }
  },
  parserOptions: {
    //指定ESLint可以解析JSX语法
    ecmaVersion: 2019,
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true
    }
  },
  rules: {
    // 自定义的一些规则
    'prettier/prettier': 'error',
    'linebreak-style': ['error', 'unix'],
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'warn',
    'react/jsx-uses-react': 'error',
    'react/jsx-uses-vars': 'error',
    'react/react-in-jsx-scope': 'error',
    'valid-typeof': [
      'warn',
      {
        requireStringLiterals: false
      }
    ]
  }
};
  • parser配置:插件@typescript-eslint/parser让ESLint 对 TypeScript 的进行解析。
npm i @typescript-eslint/parser -D 
  • extends配置:为了防止eslint和prettier的规则发生冲突,我们需要集成两者则设置为['plugin:prettier/recommended']。
npm i eslint-config-prettier eslint-plugin-prettier -D
  • plugins配置:
    • @typescript-eslint:包含了各类定义好的检测Typescript代码的规范。
    • react-hooks:为了检测和规范React hooks的代码规范检查的插件。
    • eslint-plugin-react:为了检测和规范React代码的书写的插件。
npm i eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/eslint-plugin -D

最后再修改下settings.json

{
	"prettier.configPath": ".prettierrc.js",
	"eslint.options": {
        "extensions": [".js", ".ts", ".tsx", "jsx", "html"]
    },
    "editor.codeActionsOnSave": {
    	// 保存时使用eslint进行格式化
        "source.fixAll.eslint": true
    }
}
  • 配置完成 -- 启动项目
npm run start

路由

安装路由 reac-router-dom

pnpm add react-router-dom
  • react-router: 实现了路由的核心功能
  • react-router-dom: 基于react-router,加入了在浏览器运行环境下的一些功能,
    • 例如:Link组件,会渲染一个a标签,Link组件源码a标签行;
    • BrowserRouter和HashRouter组件,前者使用pushState和popState事件构建路由,后者使用window.location.hash和hashchange事件构建路由。

配置路由信息表

方式一:


import { Routes, Route } from "react-router-dom";
import React, { lazy } from 'react'


// 路由懒加载
const Home = lazy(() => import('@/pages/home') )
const List = lazy(() => import('@/pages/list'))

// 路由表
export default function route () {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/list" element={<List />} />
    </Routes>
  );
}
  • index.js 中引入
import {HashRouter} from 'react-router-dom'
  • 使用
<HashRouter><App /></HashRouter>
  • App.js 引入 route
import {Route,Switch,Link,NavLink} from 'react-router-dom'
import routes from './routes.js'
  • 使用
<Switch>
{
	routes.map(e => <Route {...e} />)
}
</Switch>
  • routes.js
  • 从vies目录引入路由组件
  • 在routes中添加这个路由的path、key和component
  • 添加exact为true,表示严格匹配

本次路由使用 v6 版本 使用方式参考  www.bilibili.com/read/cv1440…

方式二:

配置路由表


import { Routes, Route } from "react-router-dom";
import React, { lazy } from 'react'


// 路由懒加载
const Home = lazy(() => import('@/pages/home') )
const List = lazy(() => import('@/pages/list'))
const Parent = lazy(()=> import('@/pages/parent'))

const routes = [
  {
    name:'home',
    path:'/',
    component: Home
  },
  {
    name:'list',
    path:'/list',
    component: List
  },
  {
    name:'parent',
    path:'/parent',
    component: Parent
  },
]
export default routes

index.js中引入 路由表 匹配路由信息

import React from 'react';
import ReactDOM from 'react-dom/client';
import '@/style/reset.css';
import { BrowserRouter, Routes,Route } from "react-router-dom";
import reportWebVitals from './reportWebVitals';
import routers from "./router";

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <BrowserRouter>
    <Routes>
      {
        routers.map((item,key)=>{
          return(
            <Route key={key} path ={item.path} element ={<item.component/>} ></Route>
          )
        })
      }
    </Routes>
  </BrowserRouter>
);

reportWebVitals();

嵌套路由


import { createBrowserRouter } from "react-router-dom";
import React, { lazy } from 'react'


// 路由懒加载
const Home = lazy(() => import('@/pages/home') )
const List = lazy(() => import('@/pages/list'))
const Lifecycle = lazy(()=> import('@/pages/lifecycle'))
const App = lazy(()=>import('@/App'))


const routes = createBrowserRouter([
  {
    path:'/',
    element:<App/>,
    children:[
      {
        name:'home',
        path:'',
        element: <Home/>
      },
      {
        name:'list',
        path:'list',
        element: <List/>
      },
      {
        name:'lifecycle',
        path:'lifecycle',
        element: <Lifecycle/>
      },
    ]
   
  },
])
export default routes

在index.js中引入路由表

import React from 'react';
import ReactDOM from 'react-dom/client';
import '@/style/reset.css';
import { BrowserRouter, Routes, Route ,RouterProvider} from "react-router-dom";
import reportWebVitals from './reportWebVitals';
// import RouterComponent from "./router";
import routers from "./router";


const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <RouterProvider router={routers} />,
);
reportWebVitals();

state

React 把用户界面当作简单状态机。通过与用户的交互,实现不同状态,然后渲染 UI,让用户界面和数据保持一致。只需要更新组件的 state,根据新的 state 重新渲染用户界面。

State 的主要作用是用于组件保存、控制、修改自身的可变状态。State 在组件内部初始化,可以被组件自身修改,而外部不能访问也不能修改。所以 State 是一个局部的、只能被组件自身控制的数据源。

State 中状态可以通过 setState 方法进行更新,数据的更新会导致组件的重新渲染。

组件中用到的一个变量是不是应该作为组件 state,可以通过下面的 4 条依据进行判断:

  1. 这个变量是否通过 props 从父组件中获取?如果是,那么它不是一个状态。
  2. 这个变量是否在组件的整个生命周期中都保持不变?如果是,那么它不是一个状态。
  3. 这个变量是否可以通过其他状态(State)或者属性(Props)计算得到?如果是,那么它不是一个状态。
  4. 这个变量是否在组件的 render() 方法中使用?如果不是,那么它不是一个状态。

这种情况下,这个变量更适合定义为组件的一个普通属性,例如组件中用到的定时器,就应该直接定义为 this.timer,而不是 this.state.timer。

setState

  • setState 不会立即更改 React 组件内状态
  • setState 通过引发一次组件的更新过程来引发重新渲染
    • shouldComponentUpdate(被调用时,this.state 没有更新;如果返回 false,生命周期中断,但 this.state 仍会更新)
    • componentWillUpdate(被调用时 this.state 没有更新)
    • render(被调用时 this.setState 得到更新)
    • componentDidUpdate
  • setState 的多次调用产生的效果将被合并
  • setState 本身不是异步的,而是React的批处理机制造成的异步假象

setState 的同步和异步主要取决于它被调用的环境

  • 如果 setState 在 React 能够控制的范围被调用,它就是异步的。
    • 如:合成事件处理函数, 生命周期函数, 此时会进行批量更新, 也就是将状态合并后再进行 DOM 更新。
  • 如果 setState 在原生 JavaScript 控制的范围被调用,它就是同步的。
    • 如:原生事件处理函数中, 定时器回调函数中, Ajax 回调函数中, 此时 setState 被调用后会立即更新 DOM 。

setState 的异步

在调用setState之后,React并不会立即更新组件的状态和DOM。它会将更新添加到一个队列中,并在未来的某个时间点批量处理、合并所有的更新。这样做可以提高性能,减少不必要的DOM操作。

如果需要在setState执行完毕后立即获取最新的状态值,可以使用回调函数或者 componentDidUpdate生命周期方法来实现

setState 异步的一个重要动机就是避免频繁的 re-render,从而达到性能优化的目地。

import React, { Component } from 'react'

class App extends Component {
  state = {
    count: 1,
  }

  handleClick = () => {
    this.setState({
      count: this.state.count + 1,
    })
    console.log(this.state.count) // 1
    
    this.setState({
      count: this.state.count + 1,
    })
    console.log(this.state.count) // 1
  }
  render() {
    return (
      <>
        <button onClick={this.handleClick}>加1</button>
        <div>{this.state.count}</div>
      </>
    )
  }
}

export default App

setState 的同步

setState同步的情况:

  • 如果在其他异步代码中调用setState,更新将立即发生例如setTimeout或原生事件处理程序,那么setState将是同步的,因为React不会等待异步代码完成后再更新状态。
  • 通过setState()接受一个函数作为参数,函数的首个参数就是上一次的state,以达到 “同步”化。
import React, { Component } from 'react'

class App extends Component {
  state = {
    count: 1,
  }

  handleClick = () => {
    this.setState(prevState => {
      return {count: prevState.count + 1};
    });
    console.log(this.state.count) // 2
    
    this.setState(prevState => {
      return {count: prevState.count + 1};
    });
    console.log(this.state.count) // 3
  }
  render() {
    return (
      <>
        <button onClick={this.handleClick}>加1</button>
        <div>{this.state.count}</div>
      </>
    )
  }
}

export default App

setState 流程

避免了重复无谓的刷新组件。它的主要流程如下

  1. enqueueSetState 将 state 放入队列中,并调用 enqueueUpdate 处理要更新的 Component
  2. 如果组件当前正处于 update 事务中,则先将 Component 存入 dirtyComponent 中。否则调用 batchedUpdates 处理。
  3. batchedUpdates 发起一次 transaction.perform() 事务
  4. 开始执行事务初始化,运行,结束三个阶段
    • 初始化:事务初始化阶段没有注册方法,故无方法要执行
    • 运行:执行 setSate 时传入的 callback 方法,一般不会传 callback 参数
    • 结束:更新 isBatchingUpdates 为 false,并执行 FLUSH_BATCHED_UPDATES 这个 wrapper 中的 close 方法
  5. FLUSH_BATCHED_UPDATES 在 close 阶段,会循环遍历所有的 dirtyComponents,调用 updateComponent 刷新组件,并执行它的 pendingCallbacks, 也就是 setState 中设置的 callback。

例题

lass Example extends React.Component {
  constructor() {
    super();
    this.state = {
      val: 0
    };
  }
  
  componentDidMount() {
    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 1 次 log
 
    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 2 次 log
 
    setTimeout(() => {
      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 3 次 log
 
      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 4 次 log
    }, 0);
  }
 
  render() {
    return null;
  }
};

输出为 [0,0,2,3]

  • 前两次是输出在 生命周期中,并不会直接执行更新操作,而是会将setState中的累加合并,只执行一次。设置完成后 state.val 值为 1。
  • setTimeout 中的 setState 同步执行,能够直接进行更新,连续输出 2、3。

组件

react 的组件类型:

  • 无状态组件
  • 有状态组件
  • 容器组件
  • 高阶组件
  • 渲染回调组件

无状态组件

没有状态的影响所以就是纯静态展示的作用,

它的基本组成结构就是属性(props)加上一个渲染函数(render)。由于不涉及到状态的更新,所以这种组件的复用性也最强。

  • 只负责接收 props,渲染 DOM,没有state,不能访问生命周期,不会被实例化
const PureComponent = props => <div>Hello world!</div>;

有状态组件

组件内部包含状态(state)且状态随着事件或者外部的消息而发生改变的时候,这就构成了有状态组件(Stateful Component)。

有状态组件通常会带有生命周期(Lifecycle),用以在不同的时刻触发状态的更新。

class StatefulComponent extends Component {

    constructor(props) {
        super(props);
        this.state = {
            //定义状态
        }
    }

    componentWillMount() {
        //do something
    }

    componentDidMount() {
        //do something
    }

    // 其他生命周期

    render() {
        return (
            //render
        );
    }
}

容器组件

将数据获取以及处理的逻辑放在容器组件中,使得组件的耦合性进一步地降低。

容器组件主要负责数据获取然后以 props 的形式传递给 UserList 组件来渲染。容器组件也不会在页面中渲染出具体的 DOM 节点,因此,它通常就充当数据源的角色。

class UserListContainer extends React.Component {
  getInitialState() {
    return {
      users: [],
    };
  }

  componentDidMount() {
    var _this = this;
    axios.get('/path/to/user-api').then(function(response) {
      _this.setState({ users: response.data });
    });
  }

  render() {
    return <UserList users={this.state.users} />;
  }
}

组件的实现

  • 函数式组件:一个函数就相当于一个组件

    • 无状态:只能接收props,没有state、ref和生命周期
    • 受控:当props改变,组件的内容才会更新
    • 展示:充当UI组件
  • 类式组件:使用es6的class定义一个组件

    • 有状态:既能接收props,又能定义state,还有ref和生命周期
    • 非受控:除了props,改变state,视图也会变
    • 处理逻辑:调用函数,改变状态,渲染视图

父组件中引入的组件时无法添加className,只能在原来的组件添加。

import React from 'react'
import Search from './components/search'
  • 样式
    • 设置全局样式

    import './index.scss'
  • 引入模块化样式
import styles from './index.module.scss'
  • 标签

    • 可以在多个标签的外层添加一对小括号
    • 有且仅有一个顶层标签
  • 样式

    • 类名不是class,而是className
    • 内联:style={{color: 'red', backgroundColor: 'skyblue' }}, 在花括号中设置样式的 json对象
  • 类名:使用classNames,当成函数来调用,但是引入的自定义组件不能使用 className 来设置。

  • 事件

    • 属性名是小驼峰,属性值是花括号,再接一个函数
    • 事件监听函数中没有this,需要设置this,方法有3种
        1. 在花括号中,对函数使用bind
        1. 在contructor中,对函数使用bind
        1. 使用箭头函数
    • 参数
      • 默认参数是事件对象
      • 可以从外部传入参数,方法有两种
        • 对函数使用bind方法,第一个参数是this,后面就是传入的参数
        • 在回调函数的外层套上一个函数,父函数的默认参数是事件对象,子函数接收所有参数
  • 列表渲染

    • 使用map方法生成react元素
    • 每个元素必须有key,并且不同出现重复
  • 导出

    export default 组件名

  • 条件渲染

    • 不能在花括号里写if条件语句,只能在函数表达式中用 if 语句
    • 可以在花括号里写逻辑运算符
    • && 和 ||
    • 三元表达式

props 传参

Props 是一个从外部传入组件的参数,主要用于接收组件外部传递的数据。

它具有只读性和不可变属性,子组件内部无法控制也无法修改传入的 props 数据,从而提现了react 中数据的单向流动。

单向数据流保证了数据的可控性,规范数据的流向,数据由外层组件向内层组件进行传递和更新。

propTypes

propTypes是React中组件的属性。设置它可以为组件的 props 属性进行类型的检查。

设置组件的 propTypes 属性需要借助 PropTypes 对象(props-type 库)来规定外来属性的类型。

yarn add propTypes

propsTypes 是类式组件中的静态属性,属于类本身的属性而不是实例上的属性,通过类名可以直接访问,而不需要先创建类的实例。

而 PropTypes 才是本次需要引入的校验库,除了限定类型和必填之外,prop-types 还提供了许多其他的验证规则

import PropTypes from 'prop-types'

Child.propTypes ={
  name: PropTypes.string,
  count: PropTypes.number.isRequired,
}

defaultProps

react 中还有一个特殊的属性 defaultProps ,可以为 Class 组件添加默认 props。这一般用于 props 未赋值,但又不能为 null 的情况,当组件被引用时没有被传入props,则使用 defaultProps 设置的默认值。

defaultProps 设置的属性值,只会在 reder 的时候创建一次,不能够随着 state 的改变而改变

import PropTypes from "prop-types";
import React, { Component } from "react";

type propsType = {
  name: string;
  count: number;
};
// Child 是一个继承自 Component 的类组件并指定 接收的 props 及 state 类型 <用于指定该组件接受的props的类型, 指定该组件的state的类型>
export default class Child extends Component<propsType> {
  static propTypes = {
    name: PropTypes.string.isRequired,
    count: PropTypes.number.isRequired,
  };
  constructor(props: propsType) {
    super(props);
  }
  // 通过 defaultProps 设置默认的 prop 属性
  static defaultProps = {
    count: 3, // 如果外界没有传递 count,则将其值初始化为0
  };

  render() {
    console.log(this.props);
    const { count, name } = this.props;
    return (
      <div>
        <div>这里是子组件哦</div>
        <p>
          父组件{name}传过来的属性 count: {count}
        </p>
      </div>
    );
  }
}


静态属性 defaultProps:在组件创建之前,会先初始化默认的 props 属性,这在全局调用一次。 在组件被创建并加载之后,会调用 constructor 构造器中的 this.state = {} 对组件的状态进行初始化。

生命周期调用次数能否使用 setState()
static defaultProps = {}全局调用 1 次
this.state = {}1
componentWillMount1
render>=1
componentDidMount1
componentWillReceiveProps>=0
shouldComponentUpdate>=0

但是需要注意的是 react 团队正在准备弃用 defaultProps ,因为默认属性可能会导致一些潜在的问题,例如难以调试和理解代码

相反,React团队建议使用ES6默认参数语法来设置默认值。

函数式组件中的默认赋值:

function Child({ count = 0 }) {
  // ...
}

类式组件中也可以采用判断赋值的方式

class Child extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      count: props.count || 0
    };
  }

  // ...
}

实现组件传参

父传子

父组件向子组件传值,将自身的 count 属性传递给子组件

import React, { Component } from 'react';
import Child from './child';
export default class Parent extends Component {
  constructor() {
    super();
    this.state = {
      count: 0,
    };
  }
  render() {
    const { count } = this.state;
    return (
      <div>
        <h3>父组件</h3>

        {/* 父传子 */}
         <Child count={count} />
      </div>
    );
  }
}

子组件 通过 props 接收父组件传递来的 count 数据

import React, { Component } from 'react';

const childStyle = {
  padding: 20,
  margin: 20,
  backgroundColor: 'LightSkyBlue'
};

const NAME = 'Child 组件:';

export default class Child extends Component {
  constructor() {
    super();
    console.log(NAME, 'constructor');
    this.state = {
      counter: 0
    };
  }

  render() {
    const { count } = this.props;
    return (
      <div style={childStyle}>
        <h3>子组件</h3>
        <p>父组件传过来的属性 count : {count}</p>
      </div>
    );
  }
}

子传父

利用回调函数,

  • 父组件提供回调函数,
  • 子组件调用,将传递的数据,作为回调函数的参数传递给父组件
  • 从未触发父组件的数据变更
import React, { Component } from 'react';
import Child from './child';
export default class Parent extends Component {
  constructor() {
    super();
    this.state = {
      count: 0
    };
  }
 
  /**
   * 修改传给子组件属性 count 的方法
   */
  changeNum = () => {
    let { count } = this.state;
    this.setState({
      count: ++count
    });
  };


  render() {
    const { count } = this.state;
    return (
      <div style={parentStyle}>
        <div>
          <h3>父组件</h3>
          
        {/* 子传父的实现: 
            1. 父组件提供回调函数用于接收数据,将该函数作为属性传递给子组件以供其调用,
            2. 子组件在调用时将数据传给回调函数
        */}
        <Child count={count} setParentNumber={this.changeNum} /> 
      </div>
    );
  }
}


子组件中接受父组件的回调函数,在触发数据变更时,将更新的数据作为参数传递给回调函数

import React, { Component } from 'react';

export default class Child extends Component {
  constructor() {
    super();
    this.state = {
      counter: 0
    };
  }

  render() {
    console.log(NAME, 'render');
    const { count, setParentNumber } = this.props;
    const { counter } = this.state;
    return (
      <div style={childStyle}>
        <h3>子组件</h3>
        <p>父组件传过来的属性 count : {count}</p>
  
        {/*  子传父 */}
        <Button className="btn" onClick={setParentNumber}>
          改变父组件状态 counter
        </Button>
      </div>
    );
  }
}

refs

Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素,提供了一个父组件用来访问子组件中数据或者方法的桥梁

React 的核心思想是每次对于变更 props 或 state时,触发新旧虚拟 DOM 进行 diff(协调算法),对比出变化的地方,通过 render 重新渲染界面。而 Refs 为我们提供了一种绕过状态更新和重新渲染时访问元素的方法

  • 在construtor中,创建一个ref变量
  • 在jsx中,给目标节点添加一个ref属性,等于这个ref变量
  • 获取这个变量,结果的current属性就是要获取的目标节点
import React, { Component } from 'react';

export default class Parent extends Component {
  constructor() {
    super();
    this.state = {
      inputValue: ''
    };
  }
  inputRef = React.createRef(null);
  // 点击事件 获取输入框输入的数据
  search = () => {
    const inpVal = this.inputRef.current.value;
    console.log(inpVal);
    this.setState({
      inputValue: inpVal
    });
  };
  render() {
    console.log(NAME, 'render');
    const { count, mountChild, myNumber, inputValue } = this.state;
    return (
      <div style={parentStyle}>
          {/* 使用defaultValue表示组件的默认状态,此时它只会被渲染一次,后续的渲染不起作用;input的值不随外部的改变而改变,由自己状态改变。 */}
          <input type="text" ref={this.inputRef} defaultValue="Hello" />
          <button onClick={this.search}>search</button>
      </div>
    );
  }
}

在项目开发中,如果我们可以使用声明式或提升 state 所在的组件层级(状态提升)的方法来更新组件,最好不要使用 refs。

  • React 将会在组件挂载时将 DOM 元素分配给 current 属性,并且为防止内存泄漏,在组件被卸载时,将 current 属性重置为 null
  • ref 将会在 componentDidMount 和 componentDidUpdate 生命中器钩子前被更新
  • React.findDOMNode 和 refs 都无法用于无状态组件中。因为,无状态组件挂载时只是方法调用,并没有创建实例。
  • 对于 React 组件来讲,refs 会指向一个组件类实例,所以可以调用该类定义的任何方法。如果需要访问该组件的真实 DOM,可以用 ReactDOM.findDOMNode 来找到 DOM 节点,但并不推荐这样做,因为这大部分情况下都打破了封装性,而且通常都能用更清晰的方法在 React 中构建代码。

创建 ref

  1. 传入字符串,使用时通过 this.refs 传入的字符串的格式获取对应的元素
class MyComponent extends React.Component { 
  constructor(props) { 
    super(props); 
    this.myRef = React.createRef(); 
  } 
  render() { 
    return <div ref="myRef" />; 
  } 
} 

访问当前节点

this.refs.myref.innerHTML = "hello";

2. 传入对象,对象是通过 React.createRef() 方式创建出来,使用时获取到创建的对象中存在 current 属性就是对应的元素

class MyComponent extends React.Component { 
  constructor(props) { 
    super(props); 
    this.myRef = React.createRef(); 
  } 
  render() { 
    return <div ref={this.myRef} />; 
  } 
} 

当 ref 被传递给 render 中的元素时,通过 current 获取当前 ref 的节点

const node = this.myRef.current; 
  1. 传入函数,该函数会在 DOM 被挂载时进行回调,在渲染过程中,回调函数参数会传入一个元素对象,然后通过实例将对象进行保存
class MyComponent extends React.Component { 
  constructor(props) { 
    super(props); 
    this.myRef = React.createRef(); 
  } 
  render() { 
    return <div ref={element => this.myref = element} />; 
  } 
} 

获取ref对象只需要通过先前存储的对象即可

const node = this.myref 
  1. 传入 Hook,在函数式组件中通过 useRef() 方式创建,使用时通过生成 Hook 对象的 current 属性就是对应的元素
function App(props) { 
  const myref = useRef<HTMLDivElement>(null);
  return ( 
    <> 
      <div ref={myref}></div> 
    </> 
  ) 
} 

通过 current 属性获取 ref

const node = myref.current; 

注意:

  • 如果 ref 设置的组件为一个类组件的时候,ref 对象接收到的是组件的挂载实例。
  • ref 属性不能在函数组件上使用,因为他们并没有实例,但是可以通过 useRef 创建。

应用场景

  • 管理焦点(如文本选择)或处理表单数据:refs 将管理文本框当前焦点选中,或文本框其它属性。
  • 媒体播放:基于 Reac 的音乐或视频播放器可以利用 Refs 来管理其当前状态(播放/咱 ing)。或管理播放进度等。这些更新不需要进行状态管理。
  • 触发强制动画:如果要在元素上触发过强制动画时,可以使用 Refs 来执行此操作
  • 集成第三方 DOM 库

context

Context 提供了一个局部的全局作用域,使用Context 上下文的公共状态解决了 props 层层传递的问题。

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法,在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props,实现了祖孙组件间的跨组件数据传递。

context 使用方式

  1. React.createContext提供的和
  2. 在函数式组件中 React.createContext提供的Provider和useContext钩子
  3. 在类式组件中:React.createContext提供的Provider和class的contextType属性

context 的使用

1. 创建 Context 的使用环境

首先创建 Context 的使用环境,后续的 contxt 的使用都基于此环境

//创建一个名为 context.js 的文件导出createContext()的返回值
import { createContext } from 'react'

const MyContext = createContext<any>(null)

export default MyContext

2. 向后代组件注入数据

使用 context 提供的 Provider 组件进行包裹,圈定局部的全局作用域,传值后提供给子组件进行消费。

可以选择向后代组件注入共享 数据 及 修改数据的方法供后代使用,该方法支持祖先组件及后代组件对 context 值的修改。

  • 祖先组件将 state、 setState 方法向下传递。
  • 订阅了 context 的后代组件接收 constext 传递的数据,通过调用 setState 触发数据更新。
  • 调用 setState 方法触发数据的更新及页面的重新渲染。

不同组件写法的注入方式

  • 函数式组件注入方式
import { useState } from 'react'
import MyContext from './context'
import ChildA from './ChildA'
import ChildB from './ChildB'

function parent() {
  const [count, setCount] = useState(1)
  return (
    <div className="layout">
       // Provider 组件接收一个value属性,此处传入 Context 传递的值
      <MyContext.Provider value={{ count, setCount }}>
        <ChildA/>
        <ChildB/>
      </MyContext.Provider>
    </div>
  )
}

  • 类式组件注入方式
import React, { Component } from 'react';
import MyContext from './context';
import ChildC from './childC';  // 类式组件的使用

export default class Parent extends Component {
  constructor() {
    super();
    this.state = {
      count: 0,'
    };
  }
  /**
   *  传给子组件属性 count 的方法
   */
  changeNum = () => {
    let { count } = this.state;
    this.setState({
      count: ++count
    });
  };

  render() {
    const { count } = this.state;
    const changeNum = this.changeNum;
    return (
      <div style={parentStyle}>
          <MyContext.Provider value={{ count, changeNum }}>
            <ChildC/>
          </MyContext.Provider>
      </div>
    );
  }
}

当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。

3. 获取传递的数据

  • 使用 createContext 提供的Consumer组件

使用其提供的Consumer组件来订阅Context的变更,需要一个函数作为子元素,函数的第一个形参便是 Provider组件提供的value值。

import React from "react";
import MyContext from "./context";

const ChildA = () => {
  return (
    //
    <MyContext.Consumer>
      {( value ) => {
        return (
          <div>
            第一种使用Context方式获取的值:{JSON.stringify(value)}
          </div>
        );
      }}
    </MyContext.Consumer>
  );
};
  • 函数式组件中通过 useContext 获取

导入useContext钩子函数,该函数接收createContext()的返回值, 当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。

import { useContext } from 'react'
import MyContext from './context'

function Child() {
  const { count } = useContext(MyContext)

  return (
    <div className="nav-container">
        <p>
          第二种使用useContext方式获取的值{count}
        </p>
      </div>
    </div>
  )
}
  • 类式组件中的 contextType

挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。

import React, { Component } from "react";
import MyContext from "./context";

class ChildC extends Component {
  static contextType = MyContext;
  render() {
    const value = this.context;
    return (
      <div>
        第三种使用Context方式获取的值:{JSON.stringify(value)}
        <button onClick={value.changeNum}>click</button>
      </div>
    );
  }
}

使用 this.context 来消费最近的 Context 上的值。可以在任何生命周期中访问它,包括 render 函数中。

使用static关键字添加静态属性,和直接在class添加属性效果一致,最终都会添加到类上,而不是类的实例上。

antd 引入

ant-design 较为常用的 react UI 组件库,先安装。

  • 安装
npm install antd --save
npm install antd-mobile --save  // 适用于 移动端开发
  • 在config-overrides.js文件中添加如下代码

fixBabelImports('import', {
  libraryName: 'antd',
  style: 'css',
})

配置按需加载

使用 babel-plugin-import 来实现组件代码和样式的按需加载。

babel-plugin-import 是一个用于按需加载组件代码和样式的 babel 插件

插件安装

npm install babel-plugin-import --save-dev

yarn add babel-plugin-import -D

使用插件后仍然可以用:import { Button } from 'antd'; 来引入组件,插件会帮你转换成 antd/lib/xxx 的写法。另外此插件配合 style 属性可以做到模块样式的按需自动加载。

注意,babel-plugin-import 的 style 属性除了引入对应组件的样式,也会引入一些必要的全局样式。如果你不需要它们,建议不要使用此属性。你可以 import 'antd/dist/antd.css' 手动引入,并覆盖全局样式。

配置方式 babel 的配置方式有两种:

  1. 创建 .babelrc 文件并配置 babel规则
{
  "plugins": [
    // "style": true 会加载 less 文件,如果没有使用less可以修改为 "style": "css"
    [ "import",{ "libraryName": "antd","libraryDirectory": "es","style": true}]
  ],
  "presets": [      
    "react-app"
  ] 
}

  1. 在 config-overrides.js 打包配置中设置 fixBabelImports 的规则
const { override, fixBabelImports } = require('customize-cra');
module.exports = override(
  fixBabelImports('import', {
    libraryName: 'antd',
    libraryDirectory: 'es', //es6版本
    style: true, // "style": true 会加载 less 文件,如果没有使用less可以修改为 "style": "css"
  })
)

配置完按需加载后在jsx中引入相关组件,便可直接当成标签来使用

import React from 'react';
import { Button, Switch, Menu } from 'antd';

function Editor() {

  return (
    <div className="layout">
      <Button type="primary" onClick={sendMessage}> 弹出消息 </Button>
      <Menu
          onClick={this.onClick}
          defaultSelectedKeys={[router]}
          defaultOpenKeys={['sub1']}
          mode="inline"
          theme="dark"
          items={items}
      />
    </div>
  );
}
export default Editor;

meneu 组件的使用

import React from 'react';
import { Menu, SubMenu } from 'antd';

function Editor() {

  return (
    <div className="layout">
      <Button type="primary" onClick={sendMessage}> 弹出消息 </Button>
      <Menu
          onClick={this.onClick}
          defaultSelectedKeys={[router]}
          defaultOpenKeys={['sub1']}
          mode="inline"
          theme="dark"
          items={items}
      />
    </div>
  );
}
export default Editor;
  • defaultSelectedKeys:初始选中的菜单项 key 数组,  只能被修改一次
  • selectedKeys: 当前选中的菜单项 key 数组, 可被多次修改  解决路由重定向时的选中菜单项

递归渲染侧边菜单栏

getMenuNodes_map = (menuList) =>{
    return menuList.map( item =>{
      if(!item.children) {
        return (
          <Menu.Item key={item.key}>
            <Link to={item.key}>
              <Icon type={item.icon}/>
              <span>{item.title}</span>
            </Link>
          </Menu.Item>
        )
      } else {
        return (
          <SubMenu
          key={item.key}
          title={
            <span>
            <Icon type={item.icon}/>
            <span>{item.title}</span>
          </span>
          }>
            {/* 根据 map() + 递归调用  渲染子节点
            {this.getMenuNodes_map(item.children)} */}
            

            {/*  使用reduce() + 递归调用  */}
            {this.getMenuNodes(item.children)}
          </SubMenu>
        )
      }
    })
 }


/*
  根据menu的数据数组生成对应的标签数组
  使用reduce() + 递归调用
  */
  getMenuNodes = (menuList) => {
    // 得到当前请求的路由路径
    const path = this.props.location.pathname

    return menuList.reduce((pre, item) => {

      // 如果当前用户有item对应的权限, 才需要显示对应的菜单项
      if (this.hasAuth(item)) {
        // 向pre添加<Menu.Item>
        if(!item.children) {
          pre.push((
            <Menu.Item key={item.key}>
              <Link to={item.key}>
                <Icon type={item.icon}/>
                <span>{item.title}</span>
              </Link>
            </Menu.Item>
          ))
        } else {

          // 查找一个与当前请求路径匹配的子Item
          const cItem = item.children.find(cItem => path.indexOf(cItem.key)===0)
          // 如果存在, 说明当前item的子列表需要打开
          if (cItem) {
            this.openKey = item.key
          }


          // 向pre添加<SubMenu>
          pre.push((
            <SubMenu
              key={item.key}
              title={
                <span>
              <Icon type={item.icon}/>
              <span>{item.title}</span>
            </span>
              }
            >
              {this.getMenuNodes(item.children)}
            </SubMenu>
          ))
        }
      }

      return pre
    }, [])
  }

axios

在 react 项目中与后台交互获取数据通常使用 axios 来配置接口请求,它是基于promise的http库,可运行在浏览器端和node.js中。他有很多优秀的特性,例如拦截请求和响应、取消请求、转换json、客户端防御XSRF等。

  • 安装axios
npm install axios --save
  • 封装 axios 接口请求

新建 request.js 文件,用于封装接口请求并配置相应的拦截器。

import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
import {message } from 'antd'

// 创建axios实例  
const service: AxiosInstance = axios.create({
  // baseURL 为相对路径时 匹配相应的代理配置,将请求转发到代理服务器上
  baseURL: '/api', // 如果接口请求的 url 是相对路径,baseURL 将自动拼接在请求的路径前,
  // 请求超时时间
  timeout: 10 * 60 * 1000,
})

// 请求拦截器
service.interceptors.request.use(config => { 
  // 设置请求头
 config.headers = {
    ...config.headers,
    // 防止接口请求缓存。
    'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',
  }
  return config
}, error => { 
    throw new RequestError(error)
})


// 响应拦截器
service.interceptors.response.use(response => {
  // 根据返回不同的状态码做相应的处理
  const { status, data, message } = response.data
  if (status === 'success' || status === '00000') {
    //  请求成功
    return data
  }

  return Promise.reject(new RequestError(message))
},error =>{
  throw new RequestError(error.message)
})


export default service
  • 配置请求拦截器
 ...

// 获取请求内容字符串
const generateDataStr = (config: AxiosRequestConfig) => {
  const { method, data, params } = config
  let dataStr = ''
  if (method === 'get' && params) {
    dataStr = typeof params !== 'string' ? JSON.stringify(params) : params
  }
  if (method === 'post' && data) {
    dataStr = typeof data !== 'string' ? JSON.stringify(data) : data
  }
  return dataStr
}

// request 请求拦截器 避免重复请求
service.interceptors.request.use(config => {
  // 如果当前网络有问题,直接报错
  if (!window.navigator.onLine) {
    throw new RequestError('请检查您的网络情况')
  }
  // 清除无用的cache
  Object.keys(requestKey).forEach(item => {
    const { isResponse, timestamp } = requestKey[item]
    if (isResponse && Date.now() - timestamp >= 300) {
      delete requestKey[item]
    }
  })
  // 阻止频繁请求和重复请求
  const { method, url, data, unique } = config
  const dataStr = generateDataStr(config)
  const hash = `${method}${url}${dataStr}${unique ? Math.random() : ''}`
  // 重复请求
  if (requestKey[hash]) {
    const { timestamp, isResponse, method, url } = requestKey[hash]
    // 距离上次请求不足300毫秒
    if (Date.now() - timestamp < 300) {
      console.warn('请求过于频繁', method, url)
      throw new RequestError('请求过于频繁')
    }
    // 请求尚未返回
    if (!isResponse) {
      console.warn('请勿重复提交', method, url)
      throw new RequestError('请勿重复提交')
    }
  }
  // 新请求初始化
  requestKey[hash] = {
    method,
    url,
    data,
    isResponse: false,
    timestamp: Date.now(),

  }
  config.headers = {
    ...config.headers,
    // 防止接口请求缓存。https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control
    'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',
  }
  return config
}, error => {
  throw new RequestError(error)
})
 
  • 配置响应拦截器
...
//  response 响应拦截器
service.interceptors.response.use(async response => {
  // 如果请求返回,对应的requestKey.isResponse设置为true
  const {
    method,
    url,
    // responseType
  } = response.config
  const dataStr = generateDataStr(response.config)
  const hash = `${method}${url}${dataStr}`
  if (requestKey[hash]) {
    requestKey[hash].isResponse = true
  }

  // 数据处理
  const { status, data, message } = response.data
  if (status === 'success' || status === '00000') {
    // console.log('data==>', data)

    return data
  }

  return Promise.reject(new RequestError(message))
}, error => {
  // 自定义错误类直接抛出
  if (error instanceof RequestError) {
    throw error
  } else {
    // 如果请求返回,无论错误失败,对应的requestKey.isResponse设置为true
    const { config: { method, url }, response: { status } } = error
    const dataStr = generateDataStr(error.config)
    const hash = `${method}${url}${dataStr}`
    if (requestKey[hash]) {
      requestKey[hash].isResponse = true
    }
    // 超时
    if (status === 401) {
      // const currentPath = window.location.href
      // window.location.href = process.env.VUE_APP_ENV !== 'development' ? `/kylinsite/users/cas?redirect=${currentPath}` : `${process.env.VUE_APP_LOGIN_URL}?service = ${currentPath}`
      throw new RequestError('401_UNAUTHORIZED')
    }
    // 处理HTTP 错误 如404
    throw new RequestError(error.message)
  }
})
  • api 的封装

创建 api.ts 文件用于接口的统一管理

//  引入封装的 axios
import axios from '@/utils/request';

// 接口的参数配置, 在未声明的 method 的情况下,默认采用 get 请求
export function getNavigationListApi<T>() {
  return axios.request<T>({
    url: '/environment/list'
  })
}

//  post 请求及 body 传参
export function saveModuleApi<T>(data: ModuleProps) {
  return axios.request<T>({
    url: '/environment/add_environment',
    method: 'post',
    data
  })
}

//  get 请求及 query 传参
export function getLogApi<T>(params: LogProps) {
  return axios.request<T>({
    url: '/log/list',
    params
  })
}

//  接口传参类型
interface ModuleProps {
  id?: number
  name: string
}

interface LogProps {
  beforeDate: string
  afterDate: string
}

  • 接口调用
import {} from './api.ts
const data = await getNavigationListApi<CardListInterface[]>()

const module = await saveModuleApi<ModuleProps>(moduleData)

const res = await getLogApi<LogInterface[]>({
      beforeDate: dayjs(beforeDate).format('YYYY-MM-DD'),
      afterDate: dayjs(afterDate).format('YYYY-MM-DD'),
    })

Proxy 配置

后端与前端开发分离的项目中,有时前端需要访问跨域资源,此时配置代理服务器就非常有用了,通过配置代理服务器来解决请求跨域问题。 在 config-overrides.js 中配置代理服务

const {
  override,
  addDecoratorsLegacy,
  disableEsLint,
  addBundleVisualizer,
  addWebpackAlias,
  adjustWorkbox,
  fixBabelImports,
  overrideDevServer
} = require('customize-cra');
const path = require('path');

// 配置请求代理
const devServerConfig = () => (config) => {
  return {
    ...config,
    proxy: {
      // 接口代理: 匹配 /api 开头的接口代理到 target 域名下
      '/api': {
        target: 'http://www.xxx.com',  // target 替换成项目域名或后端服务地址
        changeOrigin: true,
        secure: false,
        pathRewrite: {
          '^/api': ''
        }
      },
    }
  };
};

module.exports = {
  webpack: override(
    // enable legacy decorators babel plugin
    addDecoratorsLegacy(),

    // disable eslint in webpack
    disableEsLint(),

    // add webpack bundle visualizer if BUNDLE_VISUALIZE flag is enabled
    process.env.BUNDLE_VISUALIZE == 1 && addBundleVisualizer(),

    // 配置别名 add an alias for "ag-grid-react" imports
    addWebpackAlias({
      'ag-grid-react$': path.resolve(__dirname, 'src/shared/agGridWrapper.js'),
      '@': path.resolve(__dirname, 'src')
    }),

    // adjust the underlying workbox
    adjustWorkbox((wb) =>
      Object.assign(wb, {
        skipWaiting: true,
        exclude: (wb.exclude || []).concat('index.html')
      })
                 ),
    fixBabelImports('import', {
      libraryName: 'antd',
      libraryDirectory: 'es', //es6版本
      style: false // "style": true 会加载 less 文件,如果没有使用less可以修改为 "style": "css"
    }),
  ),
  // 配置代理
  devServer: overrideDevServer(devServerConfig())
};

图片

react 项目中使用图片的方式

  • img标签
    • 如果src的值是写死的,直接写图片的相对路径,也可以将相对路径改成@开头
    • 无论src的值是否写死,都必须结合require方法
  • 背景图片
    • 如果将样式写在style中,url里面的路径必须是相对路径
    • 如果将样式写在标签上,必须结合require方法

React 中在加载本地图片时,如果直接使用相对路径引入图片会出现报错,需要采用以下方式导入图片后再渲染。

本地图片引入方式

  • 使用 import 引入图片
import image from '@/assets/img/banner.png'
<img src={image} alt=""/>
  • 结合 require 引入
<img src={require('@/assets/img/bilibili.png')} alt="" className="icon" />
  • 行内样式中背景图片
<div style={{width:"100%",height:"100%",backgroundImage:`url(${require('@/assets/img/bilibili.png')})`,backgroundSize:"cover"}}></div>

  • 背景图片样式的设置
background: url(${require("@/assets/img/bilibili.png")}) center/cover;

服务器返回的图片

后端不要返回请求路径前面的域名和端口号时,浏览器会自动拼接上 http://域名/(项目名/)

src的链接若不是http, https, ftp…等开头的会被判断为相对路径,浏览器会自动拼接上 http://域名/(项目名/) 绝对路径使用 //

react + ts 的项目初始化 解决接口返回相对路径的问题 方式一:配置http请求的公共路径,前端在服务器返回的图片路径前手动拼接对应的 域名跟端口

const commonUrl = `http://${process.env.NODE_ENV == 'development' ? 'localhost' : 'www.xxx.com'}:8888`

方式二:开启图片资源的代理服务 配置代理后,前端请求资源时可以不加任何前缀,配置图片路径的代理,将图片资源的请求转发至指定的服务器上获取图片资源。

// 配置代理
const devServerConfig = () => (config) => {
  return {
    ...config,
    proxy: {
      // 接口代理: 匹配api开头的接口代理到 target 域名下
      '/api': {
        target: 'http://www.xxx.com',  // target 替换成项目域名或后端服务地址
        changeOrigin: true,
        secure: false,
        pathRewrite: {
          '^/api': ''
        }
      },
      // 图片路径的代理
      '/upload': {
        target: 'http://www.xxx.com',
        changeOrigin: true,
        secure: false
      }
    }
  };
};

react 项目中的环境变量

create-react-app 会自动配置和管理 React 项目中的各种依赖项和配置项,包括环境变量。 在创建 React 应用程序时,内置了环境变量 NODE_ENVPUBLIC_URL,可以通过.env文件来配置环境变量。

.env 是默认的全局配置文件,可以定义不同环境中的配置项,并且这些环境变量会被自动加载到React应用程序中。

需要注意的是:.evn 文件中的环境变量必须以 REACT_APP_开头避免与其他环境变量冲突

.env 文件的配置

NODE_ENV=development   // 通过 process.env.NODE_ENV 获取用于判断当前运行环境
REACT_APP_ENV = development

同时 create-react-app 支持多个环境配置文件的:

  • .env:默认的全局配置文件。
  • .env.local:本地覆盖。除 test 之外的所有环境都加载此文件
  • .env.development, .env.test, .env.production:设置特定环境。
  • .env.development.local, .env.test.local, .env.production.local:在运行特定环境中 loacl 的本地覆盖。

在不同的环境中执行对应的打包指令

在 react 项目中 packag.json 可以通过配置 cross-env 来指定项目运行的环境。

"scripts": {
  "serve": "concurrently \" cross-env  BROWSER=none npm run start\" \"wait-on http://localhost:3000 && npm run start-electron\" ",
  "start": "react-app-rewired start",
  "build": "react-app-rewired build",
  "test": "react-app-rewired test",
  "eject": "react-app-rewired eject",
  "start-electron": " cross-env NODE_ENV=development electron .",  // cross-env 指定运行时的环境为 development
  "start-electron-prod": "electron .",
  "build-win": "electron-builder --win --x64"
},

项目中 通过 process.env.NODE_ENV === 'development' 可以用来判断当前是否为开发环境。

如果想要了解更多关于 env 的知识点可以参考这篇文章:你真的了解 env 吗?

总结

这篇文章是我在学习 React 过程中所积累的一些实践,在学习的过程中不断的完善,遇到了许多问题也参考了很多文章,希望通过本文能够帮助其他初学者更好地理解和应用 TypeScript 在 React 项目中的使用方法。

希望对你们能有所帮助哦 !!!

参考

eslint 配置: juejin.cn/post/684490…

evn 的配置:  mp.weixin.qq.com/s/THm8_FnQ8…

react环境变量:blog.csdn.net/liuxiao7238…

从0到1搭建TS+React环境:juejin.cn/post/695566…

React ref的基本使用:juejin.cn/post/684490…

React Guidebook:tsejx.github.io/react-guide…

React中使用Context的3种方式: juejin.cn/post/692450…

React 导航:fe.azhubaby.com/React/

React技术揭秘:react.iamkasong.com/