【React自学笔记02】React必学~工欲善其事必先利其器

三、React应用

3.1. 创建react应用

3.1.1. react脚手架

  1. xxx脚手架: 用来帮助程序员快速创建一个基于xxx库的模板项目

    创建基于React脚手架的模板项目-简称创建脚手架

  2. 包含了所有需要的配置(语法检查、jsx编译、devServer…)

  3. 下载好了所有相关的依赖

  4. 可以直接运行一个简单效果

  5. react提供了一个用于创建react项目的脚手架库: create-react-app

  6. 项目的整体技术架构为: react + webpack + es6 + eslint

  7. 使用脚手架开发的项目的特点: 模块化, 组件化, 工程化

3.1.2. 创建项目并启动

第一步,全局安装:npm i -g create-react-app

第二步,切换到想创项目的目录,使用命令:create-react-app hello-react

第三步,进入项目文件夹:cd hello-react

第四步,启动项目:npm start

3.1.3. react脚手架项目结构

🚩public ---- 静态资源文件夹

  • favicon.icon ------ 网站页签图标 %PUBLIC_URL%代表public文件夹的路径
  • index.html -------- 主页面
  • logo192.png ------- logo图
  • logo512.png ------- logo图
  • manifest.json ----- 应用加壳的配置文件
  • robots.txt -------- 爬虫协议文件

🚩 src ---- 源码文件夹

  • App.css -------- App组件的样式
  • App.js --------- App组件
  • App.test.js ---- 用于给App做测试
  • index.css ------ 样式
  • index.js ------- 入口文件
  • logo.svg ------- logo图
  • reportWebVitals.js --- 页面性能分析文件(需要web-vitals库的支持)
  • setupTests.js ---- 组件单元测试的文件(需要jest-dom库的支持)

补充:样式模块化,防止引入的样式产生冲突

  1. 将css样式文件改名为XXX.module.css
  2. 修改引入样式文件 import hello from './index.module.css'
  3. 添加样式

3.1.4. 功能界面的组件化编码流程

  1. 拆分组件: 拆分界面,抽取组件

  2. 实现静态组件: 使用组件实现静态页面效果

  3. 实现动态组件

  • 动态显示初始化数据
    • 数据类型
    • 数据名称
    • 保存在哪个组件?
  • 交互(从绑定事件监听开始)

3.2. 组件的组合使用

案例TodoList

功能: 组件化实现此功能

  1. 显示所有todo列表

  2. 输入文本,点击按钮显示到列表的首位,并清除输入的文本

💕 总结:

  1. 拆分组件、实现静态组件,注意:className、style的写法

  2. 动态初始化列表,如何确定将数据放在那个组件的state中

    ——某个组件中使用:放在自身的state中

    ——某些组件使用:放在 他们共同的父组件state中(官方称此操作为:状态提升)

  3. 关于父子之间通信:

    ——父传子:标签属性props

    ——子传父:父组件写好方法,通过标签属性传给子组件(父提前给子传递一个函数),子组件通过this.props.方法名调用该方法

  4. 注意defaultChecked和checked的区别:

    ——defaultChecked只能指定一次,之后再改变勾选状态不起效

    ——checked要配合onChange使用

  5. 状态在哪里,操作状态的方法就在哪里

  6. 勾选、取消勾选修改item的状态

    • 如果是input输入框 --event.target.value

    • 如果是input多选框 --event.target.checked

🌀 代码片段

//App.js
import React, { Component } from 'react'
import Header from './components/Header'
import List from "./components/List";
import Footer from './components/Footer'
import './index.css'

export default class App extends Component {
  state = {
    todos: [{
      id: '001', content: '吃饭', ischecked: false
    }, {
      id: '002', content: '睡觉', ischecked: true
    }, {
      id: '003', content: '写代码', ischecked: true
    }]
  }
    
  addTodo = (todoObj) => {
    if (todoObj.content.trim() === '') return
    const newTodos = [todoObj, ...this.state.todos]
    this.setState({ todos: newTodos })
  }

  delTodo = (id) => {
    let { todos } = this.state
    const newTodos = todos.filter((todo) => {
      return todo.id !== id
    })
    this.setState({ todos: newTodos })
  }

  changeTodo = (id, ischecked) => {
    let { todos } = this.state
    todos.forEach((todo) => {
      if (id === todo.id) {
        todo.ischecked = !ischecked
      }
    })
    console.log(todos);
    this.setState({ todos: todos })
  }

  updateCheck = (ischecked) => {
    let { todos } = this.state
    todos.forEach((todo) => {
      if (todo.ischecked !== ischecked) {
        todo.ischecked = !todo.ischecked
      }
    })
    this.setState({ todos: todos })
  }

  delAllTodos = () => {
    let { todos } = this.state
    let newTodos = todos.filter((todo) => {
      return !todo.ischecked
    })
    this.setState({ todos: newTodos })
  }

  render() {
    const { todos } = this.state
    return (
      <div className="todo-container">
        <div className="todo-wrap">
          <Header addTodo={this.addTodo} />
          <List todos={todos} delTodo={this.delTodo} changeTodo={this.changeTodo} />
          <Footer todos={todos} updateCheck={this.updateCheck} delAllTodos={this.delAllTodos} />
        </div>
      </div>
    )
  }
}

//Header.jsx
import React, { Component } from 'react'
import { nanoid } from 'nanoid'
import PropTypes from 'prop-types'
import './index.css'

export default class Header extends Component {
  static propTypes = {
    addTodo: PropTypes.func.isRequired
  }
  handleKeyup = (event) => {
    if ((event.keyCode || event.which) !== 13)
      return;
    const todoObj = { id: nanoid(), content: event.target.value, ischecked: false }
    this.props.addTodo(todoObj)
    event.target.value = ''
  }

  render() {
    return (
      <div>
        <div className="todo-header">
          <input onKeyUp={this.handleKeyup} type="text" placeholder="请输入你的任务名称,按回车键确认" />
        </div>
      </div>
    )
  }
}
//List.js
import React, { Component } from 'react'
import Item from '../Item'
import './index.css'

export default class List extends Component {
  render() {
    const { todos, delTodo, changeTodo } = this.props
    return (
      <ul className="todo-main">
        {
          todos.map((todo) => {
            return <Item key={todo.id} todo={todo} delTodo={delTodo} changeTodo={changeTodo} />
          })
        }
      </ul>
    )
  }
}
//Item.jsx
import React, { Component } from 'react'
import './index.css'

export default class Item extends Component {
  state = { mouse: false }

  handleMouse = () => {
    const { mouse } = this.state
    this.setState({ mouse: !mouse })
  }

  handleDel = (id) => {
    return () => {
      if (window.confirm('确定删除吗?')) {
        this.props.delTodo(id)
      }
    }
  }
  handleChecked = (id, ischecked) => {
    return () => {
      this.props.changeTodo(id, ischecked)
    }
  }

  render() {
    const { todo } = this.props
    let { mouse } = this.state
    return (
      <li style={{ backgroundColor: mouse ? '#ddd' : 'white' }} onMouseEnter={this.handleMouse} onMouseLeave={this.handleMouse}>
        <label>
          <input type="checkbox" checked={todo.ischecked} onChange={this.handleChecked(todo.id, todo.ischecked)} />
          <span>{todo.content}</span>
        </label>
        <button onClick={this.handleDel(todo.id)} className="btn btn-danger" style={{ display: mouse ? 'block' : 'none' }}>删除</button>
      </li>
    )
  }
}
//Footer.jsx
import React, { Component } from 'react'
import './index.css'

export default class Footer extends Component {
  handleChecked = (event) => {
    this.props.updateCheck(event.target.checked)
  }
  handleDel = () => {
    if (window.confirm('确定删除吗?')) {
      this.props.delAllTodos()
    }
  }
  render() {
    const { todos } = this.props
    let countNum = 0;
    todos.forEach((todo) => {
      if (todo.ischecked) countNum++
    })
    return (
      <div className="todo-footer">
        <label>
          <input type="checkbox" checked={(countNum === todos.length) && todos.length !== 0 ? true : false} onChange={this.handleChecked} />
        </label>
        <span>
          <span>已完成{countNum}</span> / 全部{todos.length}
        </span>
        <button className="btn btn-danger" onClick={this.handleDel}>清除已完成任务</button>
      </div>
    )
  }
}

四、React Ajax

4.1 理解

4.1.1 前置说明

  1. React本身只关注于界面, 并不包含发送ajax请求的代码

  2. 前端应用需要通过ajax请求与后台进行交互(json数据)

  3. react应用中需要集成第三方ajax库(或自己封装)

4.1.2 常用的ajax请求库

  1. jQuery: 比较重, 如果需要另外引入不建议使用

  2. axios: 轻量级, 建议使用

    (1)封装XmlHttpRequest对象的ajax

    (2)promise风格

    (3)可以用在浏览器端和node服务器端

——————————————记录于2022.05.19——————————————

4.2 react脚手架配置代理

跨域问题产生的本质:Ajax引擎拦截响应

代理没有Ajax引擎,不受同源策略的限制,代理通过请求转发的形式接收数据

方法一

在package.json中同级追加如下配置,重启脚手架

"proxy":"http://localhost:5000"

说明:

  1. 优点:配置简单,前端请求资源时可以不加任何前缀。
  2. 缺点:适合配置一个代理,不能配置多个代理。
  3. 工作方式:上述方式配置代理,当请求了3000不存在的资源时,那么该请求会转发给5000 (优先匹配前端资源)

方法二

  1. 第一步:创建代理配置文件,react脚手架自动找

    在src下创建配置文件:src/setupProxy.js
    
  2. 编写setupProxy.js配置具体代理规则:

    const proxy = require('http-proxy-middleware')
    
    module.exports = function(app) {
      app.use(
        proxy('/api1', {  //api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000)
          target: 'http://localhost:5000', //配置转发目标地址(能返回数据的服务器地址)
          changeOrigin: true, //控制服务器接收到的请求头中host字段的值
          /*
          	changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
          	changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:3000
          	changeOrigin默认值为false,但我们一般将changeOrigin值设为true
          */
          pathRewrite: {'^/api1': ''} //重写请求路径,去除请求前缀,将请求路径中的/api1替换为'',保证交给后台服务器的是正常请求地址(必须配置)
        }),
        proxy('/api2', { 
          target: 'http://localhost:5001',
          changeOrigin: true,
          pathRewrite: {'^/api2': ''}
        })
      )
    }
    

    若使用新版本http-proxy-middleware最新写法:

    const { createProxyMiddleware } = require('http-proxy-middleware');
    module.exports = function (app) {
      app.use(
        createProxyMiddleware('/api1', {
          target: 'http://localhost:5000',
          changeOrigin: true,
          pathRewrite: { '^/api1': '' }
        })
      )
    };
    
  3. 前端请求数据时

    //如果3000没有要请求的数据,那么就找5000下的
    axios.get('http://localhost:3000/api1/students').then(
    	response=>{console.log('成功了',response.data);},
        error=>{console.log('失败了',error);}
    )
    

说明:

  1. 优点:可以配置多个代理,可以灵活的控制请求是否走代理。
  2. 缺点:配置繁琐,前端请求资源时必须加前缀。

开启后端服务器

const express = require('express')
const app = express()
app.use((request,response,next)=>{
	console.log('有人请求服务器1了');
	console.log('请求来自于',request.get('Host'));
	console.log('请求的地址',request.url);
	next()
})
app.get('/students',(request,response)=>{
	const students = [
		{id:'001',name:'tom',age:18},
		{id:'002',name:'jerry',age:19},
		{id:'003',name:'tony',age:120},
	]
	response.send(students)
})
app.listen(5000,(err)=>{
	if(!err) console.log('服务器1启动成功了,请求学生信息地址为:http://localhost:5000/students');
})

4.3 案例-GitHub搜索栏

该工程分为Search组件和LIst组件:

  1. search组件负责对搜索信息进行搜索 ,通过axios发起get请求,由于跨域限制,需要配置代理。将请求返回的数据传给父组件App
  2. App父组件收到Search发来的数据后保存到state中
  3. App组件再将数据通过props传送给List组件,在List组件中展示数据
//1.数据请求阶段,请求成功发送给父组件
import React, { Component } from 'react'
import axios from 'axios'

export default class Search extends Component {
  search = () => {
    const { keywordEle: { value: keyword } } = this;
    axios.get(`http://localhost:5000/search/users2/?q=${keyword}`).then(
      response => { this.props.saveUsers(response.data.items) },
      error => { console.log('失败了', error); }
    )
  }
  render() {
    return (
      <section className="jumbotron">
        <h3 className="jumbotron-heading">Search Github Users</h3>
        <div>
          <input ref={cNode => this.keywordEle = cNode} type="text" placeholder="enter the name you search" />&nbsp;
          <button onClick={this.search}>Search</button>
        </div>
      </section>
    )
  }
}

💕 问题:

  • 站在3000访问5000的数据产生跨域问题,需要设置代理

  • 使用第二种方式配置代理后,重启脚手架

      search = () => {
        const { keywordEle: { value: keyword } } = this;
        axios.get(`http://localhost:3000/api1/search/users2/?q=${keyword}`).then(
          response => { this.props.saveUsers(response.data.items) },
          error => { console.log('失败了', error); }
        )
      }
    

    注意:代理也是属于3000的

//2.父组件接收并放到state中
import React, { Component } from 'react'
import List from './components/List'
import Search from './components/Search'

export default class App extends Component {
  state = { users: [] }
  saveUsers = (usersObj) => {
    this.setState({ users: usersObj })
  }
  render() {
    return (
      <div className="container">
        <Search saveUsers={this.saveUsers} />
        <List users={this.state.users} />
      </div>
    )
  }
}
//3.List组件接收数据并展示
import React, { Component } from 'react'
import './index.css'
export default class List extends Component {
  render() {
    const { users } = this.props
    return (
      <div className="row">
        {
          users.map((userObj) => {
            return (
              <div className="card" key={userObj.id}>
                <a href={userObj.html_url} target="_blank" rel="noopener noreferrer">
                  <img src={userObj.avatar_url} style={{ width: '100px' }} alt="#" />
                </a>
                <p className="card-text">{userObj.login}</p>
              </div>
            )
          })
        }
      </div>
    )
  }
}

——————————————记录于2022.05.20——————————————

4.4 任意组件通信

4.4.1 消息订阅-发布机制

  1. 工具库: PubSubJS

  2. 下载: npm install pubsub-js --save

  3. 使用:

​ (1) import PubSub from 'pubsub-js' //引入

​ (2)PubSub.subscribe('delete', function(data){ }); //订阅

​ (3)PubSub.publish('delete', data) //发布消息

4.4.2 修改gitHub案例

  1. 取消App组件充当接收传递数据的职责:把之前存放在App组件中的数据给最需要它的组件,放到该组件中;不在子组件中添加标签属性

    //app.jsx
    export default class App extends Component {
      render() {
        return (
          <div className="container">
            <Search  />
            <List />
          </div>
        )
      }
    }
    
  2. List最需要该数据,所以把state定义到List组件中

    //List.app  
    state = {
        users: [],
        isFirst: true,
        isLoading: false,
        err: '',
      }
    
  3. Search组件不再把数据发送给App组件,而是发送给兄弟组件;

    Search发送请求前通知List更新状态;

    请求成功后通知LIst更新状态;

    请求失败后通知List更新状态;

    //Search.jsx
    
    

    List要接收数据,先要订阅,在组件一放到页面时订阅所以放到钩子 函数里(初始化、订阅消息);

    //List.jsx
    import PubSub from 'pubsub-js'
    componentDidMount(){
       //只要发布delete消息,就调用后面的函数 
        PubSub.subscribe('delete', function (data) { });
      }
    

完整代码如下:

//List.jsx
import React, { Component } from 'react'
import PubSub from 'pubsub-js'
import './index.css'
export default class List extends Component {
  state = {
    users: [],
    isFirst: true,
    isLoading: false,
    err: '',
  }

  componentDidMount() {
    PubSub.subscribe('updateState', (_, stateObj) => {
      this.setState(stateObj)

    });
  }

  render() {
    const { users, isFirst, isLoading, err } = this.state
    return (
      <div className="row">
        {
          isFirst ? <h2>Welcome to use,please input and search</h2> :
            isLoading ? <h2>Loading......</h2> :
              err ? <h2 style={{ color: 'red' }}>{err}</h2> :
                users.map((userObj) => {
                  return (
                    <div className="card" key={userObj.id}>
                      <a href={userObj.html_url} target="_blank" rel="noopener noreferrer">
                        <img src={userObj.avatar_url} style={{ width: '100px' }} alt="#" />
                      </a>
                      <p className="card-text">{userObj.login}</p>
                    </div>
                  )
                })
        }
      </div>
    )
  }
}
//Search.jsx
import React, { Component } from 'react'
import axios from 'axios'
import PubSub from 'pubsub-js'

export default class Search extends Component {
  search = () => {
    const { keywordEle: { value: keyword } } = this;
    PubSub.publish('updateState', { isFirst: false, isLoading: true })
    axios.get(`http://localhost:3000/api1/search/users2/?q=${keyword}`).then(
      response => {
        PubSub.publish('updateState', { users: response.data.items, isLoading: false })
      },
      error => { PubSub.publish('updateState', { isLoading: false, err: error.message }) }
    )
  }

  render() {
    return (
      <section className="jumbotron">
        <h3 className="jumbotron-heading">Search Github Users</h3>
        <div>
          <input ref={cNode => this.keywordEle = cNode} type="text" placeholder="enter the name you search" />&nbsp;
          <button onClick={this.search}>Search</button>
        </div>
      </section>
    )
  }
}

4.5 fetch发送请求

发送Ajax请求

  • xhr

    • axios:第三方库,对xhr的封装

    • jquery

  • fetch:内置,不是第三方库,promise风格

第一步:确定是否与服务器建立连接

第二步:进行请求数据

fetch未优化写法:

fetch(`/api1/search/users2/?q=${keyword}`).then(
	response=>{
        console.log('联系服务器成功了');
        return response.json()
    }
).then(
	response=>{console.log('获取数据成功了',response);}
).catch(
	error=>{
        console.log('请求出错',error);
    }
)

fetch终极优化写法:

try {
    const response=await fetch(`/api1/search/users2/?q=${keyword}`)
    const data=await response.json()
    PubSub.publish('updateState', { users: data.items, isLoading: false })
} catch(error) {
    console.log('请求出错',error)
    PubSub.publish('updateState', { isLoading: false, err: error.message })
}

💕 总结:GitHub搜索栏案例相关知识点

  1. ES6解构赋值+重命名

    let obj={a:{b:1}}
    const {a}=obj;//传统结构赋值
    const {a:{b}}=obj;//连续解构赋值
    const {a:{b:value}}=obj;//连续结构赋值+重命名
    
  2. 消息订阅与发布机制

    • 先订阅再发布
    • 适用于任意组件间通信
    • 要在 组件的componentWillUnmount中取消订阅
  3. fetch发送请求(关注分离的设计思想)

——————————————2022.05.21 本章笔记已完成——————————————

posted @ 2022-05-17 17:49  Lu西西  阅读(60)  评论(0编辑  收藏  举报