赞
踩
2013年Facebook推出的开源的函数式编程的框架,只支持IE8及以上。
16版本之后成为React Fiber(React16版本或者说是16版本中的一些底层事件),react底层在事件循环中加入了优先级的概念,可以利用事件循环的一些碎片时间执行一些高优先级的用户交互,提高react.js使用过程中的用户体验
与Vue.js的比较:react.js灵活性更大一些,所以处理非常复杂的业务时,技术方案有更多的选择,用于复杂度比较高的项目;vue.js提供了更多的API,实现功能更简单,但是API多,所以灵活性就有一定的限制了,多用于面向用户端不太复杂的项目,当然也可以做一些大型复杂的项目
//安装脚手架工具create-react-app npm i -g create-react-app //创建项目 create-react-app 项目名字 ( 报错: 创建react项目的时候node版本太低,用nvm添加了新版本之后,执行命令create-react-app myproject报错 'create-react-app' 不是内部或外部命令,也不是可运行的程序或批处理文件。 解决方法: 可以使用npx create-react-app myproject命令(这是官网上新版本的命令), 如果还是报错,是关于npx的错误,把当前使用的node版本的路径D:\nvm\nvm-setup\installation\nvm\v16.13.2加到环境变量中去, 其他错误的话,再执行一次npx create-react-app myproject命令, 我是第一次没成功,第二次又执行一次才成功的 ) //启动项目 yarn start 或者 npm run start //在localhost:3000地址展示,借助webpack-dev-server开启的地址 //开发完项目,最终打包 yarn build yarn test //不用 yarn eject //项目自动把有关webpack相关的文件都隐藏了,怕你自己修改后webpack崩了。执行这个语句,会把webpack相关文件都展示出来,例如webpack.config.js。一旦执行了这个命令,就不能再返回到隐藏状态了。
附:工程化:如果在项目中用到了像webpack这样全自动化的构建工具,你写了一段代码,它能帮你进行语法检查、代码压缩、语法转换、兼容性处理等等一系列的自动的东西
附:浏览器,按住shift点击刷新就是强制刷新
git仓库
yarn的一些缓存文件,能让你在下一次下载这些包的时候速度更快,其中也有项目依赖安装包的一些版本号,不要改动
项目说明文件,markdown写的,可以自己修改
代表这个脚手架工具是一个node的包文件,其实是node里的一些内容,比如项目的介绍、依赖于的第三方的包、调用的指令,可以把项目变成一个node的包
用git管理代码时,有一些文件不想传到git仓库上,把这些文件定义在这个文件里面
项目依赖的第三方的包/模块,不要改动
网站最上面的小图标
是项目的html主文件,项目首页的html模板。
link引入favicon.ico,%PUBLIC_URL%是React脚手架的关键词写法,就代表public这个文件夹的路径,ref内部也可以用相对路径的形式,%PUBLIC_URL%也有一定的优势,在使用路由的时候能感受到
<link rel="icon" ref="%PUBLIC_URL%/favicon.ico" />
meta标签,name=“viewport”,开启理想视口,用于做移动端网页的适配
meta标签,name=“theme-color”,用于配置浏览器页签(favicon.ico图标那个位置)和地址栏的颜色,仅支持安卓手机浏览器。
meta标签,name=“description”,描述网站信息的,利于SEO
link标签,ref=“apple-touch-icon”,将手机浏览器的网页添加到手机桌面时(向一个APP一样可以点击进入网页),在桌面上展示的图标,只支持苹果手机
<link rel="apple-touch-icon" ref="%PUBLIC_URL%/logo193.png" />
link标签,rel=“manifest”,引入应用加壳时的配置文件(在html网页的套一个安卓的壳,就变成了安卓手机上的应用,生成一个.apk;套一个ios的壳,就变成了苹果手机应用),在下面的manifest.json和src/index.js部分也有讲解。
<link rel="manifest" ref="%PUBLIC_URL%/manifest.json" />
noscript标签作用是如果llq(浏览器)把script禁掉了,不支持js脚本的运行,给用户提示这个标签内的内容,用于容错的标签;
id为root的div标签是程序的主入口。
爬虫规则文件,什么可以被爬,什么不可以
配置加壳应用在手机上的权限,应用的图标等。(结合src文件夹下的index.js文件里的registerServiceWorker)如果网页可以当一个app来使用,就可以把它存储在桌面上,有一个快捷方式,可以直接进入这个网址。
放的是项目的所有的源代码
是所有代码的主入口文件(webpack如果想使用,需要一个入口文件),整个程序从index.js里逐行执行,所有组件都从这里挂载。引入中没写文件名后缀,会优先寻找引入目录下的js文件。
里面引入了react、react-dom(处理React和DOM之间的相关事宜的)等第三方模块(在package.json可以看到已经安装的第三方的包)。
还引入了.css文件(react一个非常重要的设计理念: all in js,文件都可以像模块一样引入,vue、angular也是)。
还引入组件,例如<App />,组件的文件是App.js。
还引入了registerServiceWorker,一个概念叫PWA(progressive web app),通过写网页的形式写一些手机的app应用。registerServiceWorker帮助我们借助网页写手机app,加入引用了它,写了一个网页并上线到一台支持https的服务器上,我们写的网页就有这样的特性了,用户第一次连接网页需要联网,但是突然断网了,此时用户第二次访问页面,依然可以看到之前访问的页面,registerServiceWorker会把之前的网页存储在我们浏览器内,有缓存,下一次即使没有网络也可以把该网页当成一个app来使用。
自己创建的组件需要挂载到DOM节点下,ReactDOM是第三方模块,使用**ReactDOM.render()**方法就可以把我们自己写好的组件挂载到DOM节点下,第一个参数是引入的组件,第二个参数是DOM标签,第三个参数可以是registerServiceWorker(),可选。
index.js(官网最新版,和上面的文件介绍稍微有些出入)
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; ReactDOM.render( <React.StrictMode> //不是ES5中的严格模式,而是检查App组件和其子组件的内容是否合理 <App /> </React.StrictMode>, document.getElementById('root') ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals();
index.css 放一些页面通用样式,例如body {…}
logo.svg 是一个图片
reportWebVitals.js 用于记录页面上的性能的,里面用web-vitals这个库实现页面性能的检测
setupTest.js 用于做组件测试的(应用的整体测试或者一个一个模块拼在一起时一个一个的单元测试),用的是jest-dom库
App组件。项目中自己写的组件通常放在src下的components文件夹下。
App.js
//组件必须引入Component这个基类并继承它,引入用到了JSX语法,所以也要引入react import React from 'react'; class App extends React.Component { render() { //组件文件中,render()方法内部是该组件最终渲染的结果,render函数return什么就展示什么内容 return ( <div> <h1>hello</h1> <p>111</p> </div> ) } } export default App //另一种形式 import React, { Component } from 'react'; //使用ES6的解构赋值 class App extends Component { ... }
自动化测试文件,因为做React或者Vue项目的时候,因为会涉及到一些函数式编程,所以会做一些自动化测试。
顺序是:index.js执行到需要root节点这里,去public文件夹下找index.html中的root节点(react中的webpack配置将index.js和index.html进行关联),于是App组件就渲染到页面上了,组件里面引入了样式也就生效了。
函数式组件
<!--script标签分别引入react核心库、react-dom、babel(用于将JSX转为JS的)-->
<script type="text/babel">
//创建函数式组件
function MyComponent() {
console.log(this); //undefined,因为babel编译后开启了严格模式
//严格模式禁止this指向window,this值为undefined
return <h2>函数定义的组件,适用于【简单组件(没有状态state)】的定义</h2>
}
//渲染组件到页面
ReactDom.render(<MyComponent />, document.getElementById('test'))
//ReactDom.render(<MyComponent />, document.getElementById('test'))之后发生了什么?
//1.React解析组件标签,找到了MyComponent组件。
//2.发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,随后呈现在页面中
</script>
附:
<script> class Person { constructor(name, age) { //类的构造器中的this是指类的实例对象 this.name = name; this.age = age; } //类中定义的方法都是放在类的原型对象上,供实例去使用 speak() { //speak方法放在类的原型对象上,供实例使用 //通过Person实例调用speak时,speak中的this就是Person实例 console.log(`我叫${this.name},我的年龄是${this.age}`) } } const p1 = new Person('tom', 12) console.log(p1) //得到Person{...},这里Person是斜体,表示是类Person的实例对象 p1.speak() //继承 class Student extends Person { constructor(name, age, grade) { super(name, age) //相当于调用了父类的constructor this.grade = grade } //重写父类继承的方法 speak() { ... } //自己的方法 study() { ... } } </script>
类式组件
<!--script标签分别引入react核心库、react-dom、babel(用于将JSX转为JS的)--> <script type="text/babel"> //创建类式组件 class MyComponent extends React.Component { render() { //render函数放在MyComponent的原型对象上,供实例使用 //render中的this指的是MyComponent的实例对象,或者叫MyComponent组件实例对象 return <h2>类定义的组件,适用于【复杂组件(有状态state)】的定义</h2> } } //渲染组件到页面 ReactDOM.render(<MyComponent />, document.getElementById('test')) //ReactDom.render(<MyComponent />, document.getElementById('test'))之后发生了什么? //1.React解析组件标签,找到了MyComponent组件。 //2.发现组件是使用函数定义的,随后new出来该类的实例,并通过该实例调用到原型上的render方法 //3.将render返回的虚拟DOM转为真实DOM,随后呈现在页面中 </script>
是JavaScript的语法扩展,在js文件中使用了标签的形式:使用自己创建的组件的自定义标签、render函数return内使用的自定义标签或者html标签等。
自定义标签第一个字母必须大写,区别于原始的html标签,如果小写的话,JSX语法是不支持的
返回的内容必须包含在一个大的元素内部,当然这个标签也会显示在html文档中,如果不想增加过多的标签,可以使用React提供的Fragment占位符,把需要return的内容都放在<Fragment>…</Fragment>里
import React, { Component, Fragment } from 'react'; //这里是export和export default一起引入的样式,不是React的解构赋值写法 class App extends Component { render() { return ( <Fragment> //占位符 <h1>hello</h1> <p>111</p> </Fragment> //其实可以用空标签也能起效果,例如下面,但是Fragment标签是可以写key属性用于遍历,而空标签不可以添加任何属性。key这个标签属性是唯一的,因为Fragment最终会丢失掉,所以传其他属性没有意义,Fragment也不接收。 //<> // <h1>hello</h1> //</> ) } } export default App //也可直接创建并暴露App组件 export default class App extends Component { render() { return ( <Fragment> //占位符 <h1>hello</h1> <p>111</p> </Fragment> ) } }
如果在文件中使用了JSX的语法,一定要引入react,import React from ‘react’,不引入是没办法编译JSX语法的。
在JSX模板中循环返回一个内容的时候,返回的内容不止是一条的话(注释也算),也必须有一个最外层的包裹元素
<ul>
{
this.state.list.map((item, index) => {
return (
<div>
<TodoItem />
{/*
注释内容
*/}
</div>
)
})
}
</ul>
如果在JSX中使用js表达式或者js变量,形式是**{JavaScript表达式Expressions}**
附:js语句与js表达式
1.表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方,例如这些:
(1) a
(2) a + b
(3) demo(1)
(4) arr.map()
(5) function test() {}
2.语句,例如这些
(1) if() {}
(2) for() {}
(3) switch() {case: …}
在JSX里写注释
多行注释
{\*...\*}
单行注释,不能把这三行写在一行,因为写在一行会把后面的}也认为是注释的一部分
{
//...
}
className在react中因为class这个关键词已经用于定义组件了,所以在元素或者组件上定义样式用className
<input className="input" />
不转义设置页面的内容,比如在input框内填写<h1>haha</h1>,想要输出就是一个大大的标题,而不是转义过后的带有标签的原样输出<h1>haha</h1>,可以用dangerouslySetInnerHTML,但这样会有XSS攻击的风险,但有时必须需要这样做
<li
key={index}
onClick={this.handleItemDelete.bind(this, index)}
dangerouslySetInnerHTML = {{__html : item}}
//外层花括号是JSX语法的花括号,表示里面需要一个js表达式,内层的花括号表示这个js表达式就是一个js对象
//item为不转义显示在页面的内容
>
//{item},上面已经有item了,这里之前的item就没必要写了
</li>
附:
<div style={{marginTop: '10px', marginLeft: '10px'}}>
//在标签上加样式用双{{}},最外层为JSX语法,里面为样式的那个对象形式
label标签的作用是扩大点击的区域,如果想点击label标签光标自动聚焦到input框,可以用htmlFor
<label for="insertArea">输入内容</label>
<input id="insertArea" />
//这样会报警告,因为react中for认为是循环的那个for,所以如果label标签上要改为htmlFor
<label htmlFor="insertArea">输入内容</label>
不要操作DOM,操作数据,React会感知数据的变化,自动的生成DOM
组件内部的数据可以动态修改
this.setState()函数是更新state数据的唯一途径。react中有个immutable概念,是说state不允许我们做任何的改变,不要直接改state里面的内容,否则后面做性能优化的时候就会有问题,必须先拷贝一份,改拷贝里的内容
附:展开运算符复制数据和添加数据[…this.state.list, this.state.inputValue];splice用法
注意点:{JavaScript表达式Expressions};事件绑定的时候需要对函数的作用域进行变更this.increaseLikes = this.increaseLikes.bind(this)或者onClick={() => { this.increaseLikes() }} 还有带参数的onClick={this.handleItemDelete.bind(this, index)}
import react from "react"; class LikesButton extends React.Component { //构造器是否接收props,是否super传递props,取决于:是否希望在构造器中通过实例this访问props。如果不接收props也不传递props,构造器中使用this.props则为undefined constructor(props) { super(props)//调用一次父类的构造函数 this.state = { likes: 0, list: ['li', 'xin'] } // this.increaseLikes = this.increaseLikes.bind(this) //这句和下面第二种方式二选一,是确定this指向LikeButton组件的,否则this为undefined //其实这句是提取出来了,实际在onClick事件那里写也行,如下handleItemDelete.bind那里 //通过this.事件名 = this.事件名.bind(this),写在constructor中,这样写会节约性能 //分析:右边this是LikesButton的实例对象,引用原型对象上的increaseLikes方法,bind做了两件事,一个是生成新的函数,二是改了函数里的this,改成什么看传了什么,这里传了的是LikesButton的实例对象,所以右边返回一个函数,函数的this指的是LikesButton的实例对象。然后把这个函数放到了左侧this实例的自身,给函数起了一个名字叫increaseLikes。所以下面使用onClick={this.increaseLikes}的时候相当于使用的实例对象自身多增加的increaseLikes方法,并不会再去找原型对象上的increaseLikes方法。紧接着下面有附例子 } increaseLikes() { //this.setState({ // likes: ++this.state.likes //使用数据 //}) //方法有参数e,是event对象,e.target就是该方法所在的DOM节点, //如果标签是input的话,e.target.value可以获得input框的value值 //新版react的setState是一个函数的形式,返回一个对象 this.setState(() => { return { likes: ++this.state.likes //以对象的形式返回 } }) //es6函数直接简化为这样 //this.setState(() => ({ // likes: ++this.state.likes //}) //setState更新状态时是一个异步为setState,为了性能的提升,但是异步的话,假如像这样使用e.target.value就会出现问题 /// //handleInputChange(e) { // this.setState(() => { // return { // inputValue: e.target.value // } // }) //} //要把e.target.value提出来 //handleInputChange(e) { // const value = e.target.value // this.setState(() => { // return { // inputValue: value // } // }) //} //this.setState(prevState) //其实setState有参数prevState,是修改数据之前的数据,等价于list:[...this.state.list, this.state.like]中的this.state,所以直接换为list:[...prevState.list, prevState.like]就行,这样写更靠谱,避免我们不小心改变了state的状态 //判断setState()更新状态时异步还是同步的,主要是看执行setState的位置, //在React控制的回调函数中(生命周期钩子,react事件监听回调)这种情况是异步的; //在非react控制的异步回调函数中(定时器回调/原生事件监听回调/promise回调)这种情况是同步的。 } handleItemDelete(index) { console.log(index) } render() { return ( <Fragment> <p>{this.state.likes}</p> <button type="button" //onClick={this.increaseLikes} //第一种方式,需要结合上面的this.increaseLikes = this.increaseLikes.bind(this)改变this指向,或者直接按照下面这么写 //onClick={this.increaseLikes.bind(this)} onClick={() => {this.increaseLikes()}} //箭头函数确定this指向,第二种方式 //原生的事件绑定是onclike、onchange,在react中要使用驼峰形式onChange、onClick //react中事件的处理 //1.通过onXxx属性指定事件处理函数(驼峰)。React使用的是自定义(合成)事件,而不是使用的原生DOM事件(为了更好的兼容性);React中的事件是通过事件委托方式处理的(委托给组件最外层的元素)(为了高效)。 //2.通过event.target得到发生事件(例如点击、失去焦点等)的DOM元素对象(避免过度使用ref) > </button> <ul> { this.state.list.map((item, index) => { //数组的map方法 return <li key={index} onClick={this.handleItemDelete.bind(this, index)} //给方法传递了参数 > {item} </li> //必须加key值 }) } </ul> </Fragment> ) } } export default LikesButton
附:
<script>
function demo() {
console.log(this)
}
demo() //Window{}斜体的Window,是个Window实例对象
const x = demo.bind({a: 1, b: 2}) //bind会返回一个新函数;改了函数里的this,改成什么看传了什么,这里传了的是{a: 1, b: 2},所以this指的就是{a: 1, b: 2}。返回的新函数需要调用才能执行
x() //{a: 1, b: 2}
</script>
一般会把一部分拆分为函数的形式,例如
<ul>
{ this.getTodoItem() } //这里直接执行这个函数就行了,注意这里不用this指向的设置
</ul>
getTodoItem() {
return this.state.list.map((item, index) => {
return (
<li key={index} //key应该放在循环的最外层的元素上,假如外层有个div标签,要将key加在div上
onClick={this.handleItemDelete.bind(this, index)}
>
{item}
</li>
)
})
}
setState更新状态的两种写法:
1.setState(stateChange, [callback])------对象式的setState
setChange为状态改变对象(该对象可以体现出状态的改变);callback是可选的回调函数,它在状态更新完毕、界面也更新后(render调用后)才被调用。
2.setState(updater, [callback])------函数式的setState
updater为返回stateChange对象的函数;updater可以接收到state和props;callback是可选的回调函数,它在状态更新完毕、界面也更新后(render调用后)才被调用。
对象式的setState是函数式的setState的简写方式(语法糖),他俩的使用原则(不绝对,以实际情况而定就行)是:
如果新状态不依赖原状态,使用对象方式;
如果新状态依赖于原状态,使用函数方式;
注意:如果需要在setState()执行后获取最新的状态数据,要在第二个callback函数中读取。
附:
<script> class Person { constructor(name, age) { this.name = name, this.age = age } study() { console.log(this) } } const p1 = new Person('tom', 18) p1.study() //Person{}斜体的Person,实例调用类中方法,方法中的this就是Person实例 const x = p1.study //赋值语句 x() //undefined,直接调用,为什么不是window,因为类中所有定义的方法,在局部都开启了严格模式,类似于下面例子 //function study() { // 'use strict' // console.log(this) //严格模式的this禁止指向window,只能是undefined //} </script>
<script> class Car { constructor(name, price) { this.name = name //外部传过来的属性值必须写在构造器中 this.price = price //this.wheel = 4 } wheel = 4 //往实例对象上增加一个属性wheel,值为4。不必写在constructor中 static demo = 100 //给类添加一个属性,第二种方式如下Car.demo = 10 } const c1 = new Car('奔驰', 800) console.log(c1) //精简形式 //所以写在constructor中的this.state = {...}可以写在constructor外面,直接写state = {...} //初始化状态 state = { ... } //constructor中也可以不写this.increaseLikes = this.increaseLikes.bind(this)改变this指向这句代码了,使用下面的赋值写法,和上面wheel一样,相当于在实例对象上增加一个属性,而之前的写法是在类的原型对象上增加increaseLikes方法 //自定义方法--要用赋值语句的形式+箭头函数 increaseLikes = () => { //这里必须用箭头函数,因为箭头函数没有自己的this,但是如果在箭头函数中使用this这个关键字,找其外层函数的this作为箭头函数的this来使用,this就是外层类的实例对象 ... } //上面写法使得constructor内部是空了,可以去掉了 //开发中基本不写构造器 //如果给类自身加一个属性可以这样 //Car.demo = 10 //给类Car添加属性demo,不是给实例添加 //可以不写在外面,写在Class里面,如上static </script>
附:展开运算符
//1展开数组 let arr1 = [1, 2] let arr2 = [9, 4] console.log(...arr1) //1, 2 展开一个数组 let arr3 = [...arr1, ...arr2] //用在等号右边 //2用在形参中 function sum(...nums) { //求和,nums是数组[1, 3, 5, 6] return nums.reduce(preValue, currentValue) => { return preValue + currentValue } } console.log(sum(1, 3, 5, 6)) //展开运算符不能用于对象 let person = {name: 'tom', age: 23} console.log(...person) //报错,因为展开运算符不能用于对象,...person不行,不能像数组一样这样使用 //但是 //...person外面加一层{}得到{...person}就可以使用,这是对象的特殊语法 let person2 = {...person} //{name: 'tom', age: 23},深拷贝 //而且还可以合并 let person3 = {name: 'li', grade: 19} let mergePerson = {...person, ...person3} //{name: 'li', age: 23, grade: 19} 一样的属性后者会覆盖前者 let person4 = {...person, age: 9} //{name: 'tom', age: 9} //注意 const p = {name: 'tom', age: 16} //展开运算符应用在React组件上为组件添加属性时,这里的{...p}的{}不是和上面讲解的对象的自身花括号,而是JSX语法的花括号,其内部必须是js表达式,而...p可以这样应用对象的展开运算符,是因为引入babel和react后就可以这样使用对象的展开运算符,React中对象的特殊语法 //但是这种语法不能随意的使用,比如下面的句子就会不起作用,这种使用方式仅仅适用于标签属性的传递,别的地方都不行 //console.log('@', ...p) //什么也不展示,因为这种语法不能随意的使用,仅仅适用于标签属性的传递,别的地方都不行 ReactDOM.render(<Person {...p}/>, document.getElementById('test')) //其实标签中{...p}这些就是props,所以传递标签属性也叫传递props,props就是组件的实例对象的所有属性,所以组件取属性值的时候用this.props.name,this是组件的实例对象
附:JS中reduce()用法
数组的reduce函数,用于数组的条件统计、条件求和、筛选最值
//语法 arr.reduce(function(prev,cur,index,arr){ ... }, init); //prev 表示上一次调用回调时的返回值,或者初始值 init; //cur 表示当前正在处理的数组元素; //index 表示当前正在处理的数组元素的索引,若提供 init 值,则索引为0,否则索引为1; //arr 表示原数组; //init 表示初始值。 //实例 var arr = [3,9,4,3,6,0,9]; //1. 求数组项之和 var sum = arr.reduce(function (prev, cur) { return prev + cur; },0); //传入了初始值0,所以开始时prev的值为0,cur的值为数组第一项3,相加之后返回值为3作为下一轮回调的prev值,然后再继续与下一个数组项相加,以此类推,直至完成所有数组项的和并返回。 //2. 求数组项最大值 var max = arr.reduce(function (prev, cur) { return Math.max(prev,cur); }); //由于未传入初始值,所以开始时prev的值为数组第一项3,cur的值为数组第二项9,取两值最大值后继续进入下一轮回调。 //3. 数组去重 var newArr = arr.reduce(function (prev, cur) { prev.indexOf(cur) === -1 && prev.push(cur); //相当于if的写法 return prev; },[]);
TodoList.js父组件
父组件通过属性的方式向子组件传值
this.state.list.map((item, index) => {
return (
<TodoItem
key={item} //这是循环需要的key
content={item} //父组件通过属性的方式向子组件传值,传过去的值叫content
index={index} //这是父组件向子组件传值
deleteItem = {this.handleItemDelete.bind(this)} //父组件向子组件传递方法时必须在有bind指向,因为这样在子组件内部使用deleteItem方法时,this才会指向父组件
/>
)
当组件的 state或者props发生改变的时候,自己的render函数就会重新执行;
当父组件的render函数被运行时,它的子组件的render函数都将重新被允许一次
TodoItem.js子组件
子组件通过 this.props.属性名 的方式来接受值
props是只读的,不能修改
render() {
return <div onClick={this.handleClick}>{this.props.content}</div>
//可以把this.props的值用ES6语法解构
//const { content } = this.props;
//return <div onClick={this.handleClick}>{content}</div> //这里直接用content
}
handleClick() {
this.props.deleteItem(this.props.index) //子组件调用父组件传入的方法,并使用父组件传入的参数
//实际用下面这种方式
//const { deleteItem, index } = this.props;
//deleteItem(index)
//解构赋值 在各个部位分别做解构赋值,不要全部写在一处
}
不允许子组件直接修改父组件的内容,但子组件可以调用父组件的方法,通过父组件的方法进而改变父组件里的数据
附:函数式组件因为内部this为undefined,所以不能使用state,refs,但是可以使用props,因为函数可以传参数
<script>
function Person(props) { //可以传入props接收属性传递的参数
const {name, age} = props
return (
<ul>
<li>姓名:{name}</li>
<li>年龄:{age}</li>
</ul>
)
}
ReactDOM.render(<Person name='tom' age={18} />, document.getElementById('test'))
</script>
是声明式代码(操作数据) ,对应的是命令式代码(直接操作Dom);
可以与其他框架并存,因为它只负责id为root标签的那部分渲染;
组件化;
单项数据流(父组件可以向子组件传值,但是子组件只能使用不能修改这个值,修改了会报错) 为了测试和开发方便,假如没有单项数据流的话,很多子组件使用同一个父组件内的值,一旦报错没办法定位哪个子组件出错;
视图层框架,只解决数据和页面渲染方面,可配合一些数据型框架redux等解决react中组件间的复杂传值问题;
函数式编程,constructor、render等等都是函数的形式,维护起来比较容易,更容易前端自动化测试
Chrome浏览器=》更多工具=》扩展程序=》商店=》react developer tools开发调试工具,安装后浏览器右上角有个红色的图标
红色,本地开发版本,黑色,线上版本。线上版本的代码相对于本地版本的代码,js css代码进行了压缩,也剔除了一些警告的问题,更加精悍
会在控制台最右边多个React菜单,方便进行组件结构的查询,右侧有该组件值的内容,就不需要反复console.log打印信息看内容是否符合我们的预期,只需要实时监测这个工具右侧的state的变化就可以了。当然也有其他功能
PropTypes对参数做校验、DefaultProps定义参数的默认值
TodoItem.js组件内,引入PropTypes
import PropsTypes from 'props-types'; //脚手架工具里自带了这个包 ... 组件名.propsTypes = { //propsTypes这里小写,给类添加属性propsTypes,此时React就知道要用propsTypes来限制参数形式了,下面defaultProps同样意思 //该组件接收到的参数校验 content: PropsTypes.string, //content必须是字符串类型,PropsTypes和上面引入的一样,大写 deleteItems: PropsTypes.func.isRequired, //函数,且是必传的,不传会报错,没有isRequired不传不会报错 item: PropsTypes.oneOfType([PropsTypes.number, PropsTypes.string]) //或者 arrayItem: PropsTypes.arrayOf(...) //数组 speak: PropsTypes.func //方法 } //定义参数的默认值默认值 组件名.defaultProps = { constent : 'hello' //如果上面这个参数是isRequired,有默认值了即使父组件没传值也不会报错了 } //上面的相当于给类自身添加两个属性,可以不放在类Class外面写,全部放在Class里面 Class Person extends React.Component { static propsTypes = { ... } static defaultProps = { ... } render() { return ( ... ) } }
官网文档有更多的关于PropTypes的校验设置
虚拟DOM本质就是js对象Object,(Vue中的虚拟DOM机制和React的虚拟DOM差不多完全一致)
1.state数据
2.JSX 模板
3.数据+模板生成虚拟DOM
(虚拟DOM就是一个js对象,可以理解为数组结构的js对象,用它来描述真实DOM)
[‘div’, {id: ‘abc’}, [‘span’, {}, ‘hello’]]
4.用虚拟DOM的结构来生成真实的DOM,来显示
<div id=‘abc’><span>hello</span> </div>
5.state发生变化
6.数据+模板生成新的虚拟DOM(极大提高了性能)
[‘div’, {id: ‘abc’}, [‘span’, {}, ‘byebye’]]
(js创建一个js对象很简单,但是js创建一个DOM对象性能损耗是比较大的)
7.比较原始虚拟DOM和新的虚拟DOM的区别,找到区别是span中的内容(极大提高了性能)(减少了对真实DOM的创建以及减少了真实DOM的对比,取而代之,创建的都是js对象,对比的也是js对象,js比较js对象不怎么耗性能,但是比较真实的DOM比较耗性能)
setState里面写成函数,设计成异步函数初衷是为了提高react的底层性能,比如多次修改state的时间间隔很小,只做一次虚拟DOM的修改。
Diff算法,同层比较,一层一层进行比对,以标签为最小比较单位,假如一个标签内部的内容变了但内部里还有其他标签,其他标签没变,那么其他标签还复用之前的虚拟DOM,内容生成新的虚拟DOM。同层中根据key值做比对,key值是唯一的,所以比对起来就很节省性能,所以不能用索引值index代替,因为这样原始的虚拟DOM树上的key值就没办法和新的虚拟DOM树的key值一致了,比对就必须循环一次了。
8.直接操作DOM,改变span中的内容
流程是: JSX => React.createElement(‘div’, {id: ‘abc’}, React.createElement(‘span’, {}, ‘hello’))方法 => 虚拟DOM(js对象) =>真实DOM
JSX的语句可经过babel编译为React语法,所以JSX就是语法糖,编译为React.createElement()方法
优点:
1.性能提升了,DOM的比对变成了js的比对
2.它使得跨端的应用得以实现。React Native用react 语法去写原生应用,真实DOM在浏览器端渲染是没有问题的,但在移动端的原生应用中是不存在DOM的概念的,没有虚拟DOM则没法在移动端使用,虚拟DOM是个js对象,在浏览器和原生应用中都可以被识别,在浏览器中虚拟DOM变成真实DOM,在原生应用中虚拟DOM不让它生成DOM,而生成一些原生应用的组件,就可以在移动端显示出来了
附:
问:react/vue中的key有什么作用(key的内部原理是什么)/为什么遍历列表时,key最好不要用index?
答:当状态中的数据发生变化时,react会根据新数据生成新的虚拟DOM,随后react进行新的虚拟DOM与旧的虚拟DOM的diff比较,比较规则是:
1.旧的虚拟DOM中找到了与新虚拟DOM相同的key,若虚拟DOM中内容没变,直接使用之前的真实DOM,若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM;
2.旧的虚拟DOM中未找到与新的虚拟DOM相同的key,根据数据创建新的真实DOM,随后渲染到页面。
用index作为key可能会引发的问题:
1.若对数据进行逆序添加、逆序删除等破坏顺序的操作,会产生没有必要的真实DOM更新,界面效果没问题,但效率低。
2.如果结构中还包含输入类DOM,会产生错误DOM更新,界面就会有问题。
可以使用某元素的方法中的e.target获取元素节点,也可以使用ref获取元素节点
//第一种写法,回调函数形式的ref,回调函数(满足三个特点:1.自己定义的函数 2.没调用 3.这函数最终执行了) <input id="insertArea" value={this.state.inputValue} onChange={this.handleInputChange} //ref={(input) => {this.input = input}} //构建了一个ref引用,这个引用叫做this.input,指向input这个标签,可以直接用 //这个回调函数接收参数就是ref所在的元素节点,这里参数input也可以用别的名字。把这个节点放在了组件实例自身上,叫做input,所以就是this.input,箭头函数没有自身的this,像外找找到render,render里的this就是组件的实例对象 //render()执行,执行里面的JSX,发现ref是一个回调函数的形式,直接触发这个回调函数的执行 //直接在标签上写函数,这种形式叫内联函数 //上面那句ref简写为 ref={input => this.input = input} //回调ref中函数调用次数的问题 //官网中有说明:如果ref回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数null,然后第二次会传入参数DOM元素。这是因为在每次渲染时会创建一个新的函数实例,所以React清空旧的ref并且设置新的(意思是当数据更新,重新调用render函数时,到ref这里了,第一次不知道之前的参数是啥,为了不产生bug,将其重新置为null,执行了一遍函数,然后在以该DOM元素为参数再执行一遍这个回调函数)。通过将ref的回调函数定义成class的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的(按照正常上面那种形式写就行)。用下面的class的绑定函数的方式,ref中的回调函数就不会出现执行两次的情况,只执行一次。 //ref={this.saveInput} /> //和上面class的绑定函数的方式这里配套使用 //saveInput = (input) => { //还是传入DOM元素作为参数 // this.input = input //} //handleInputChange(e) { // const value = e.target.value //e.target就是input元素;e.target不能放在setState里面,上面有讲解 // this.setState(() => ({ // inputValue: value // })) //} //ref的写法 handleInputChange() { const value = this.input.value //也可用结构赋值的方式 //const { input } = this //const value = input.value this.setState(() => ({ inputValue: value })) } //第二种写法,React.createRef,目前React最推荐的方式 class Demo extends React.Component { myRef = React.createRef() //React.createRef调用后返回一个容器,该容器可以存储被ref所表示的节点。这个容器给了实例的myRef属性。该容器是“专人专用”的,里面只能存一个,如果多处使用却使用同一个容器,后放进去的节点就把前面的替换了。JSX中需要用到几个ref,就要写几个容器。 render() { return ( <div> <input ref={this.myRef} type="text" /> {/*render函数执行到input标签的时候,发现ref使用的是React.createRef的形式,把当前ref所在的节点直接存储到了容器里,此时this.myRef值为{current: input},通过this.myRef.current就可以拿到这个input节点了*/} </div> ) } } //第三种写法,字符串形式的ref,废弃了,可能在未来的版本中移除,因为用多了会产生效率问题 <input id="insertArea" value={this.state.inputValue} onChange={this.handleInputChange} ref="input1" //这样写组件实例的refs属性就等于{input1: input},键input1就是你自己定义的,值input就是所在的标签 /> handleInputChange() { //const value = this.refs.input1.value //this.refs.input1可以拿到input标签 //下面用的解构赋值 const { input1 } = this.refs const value = input1.value debugger //打断点,代码运行到这停止,附加的,和这里没关系 this.setState(() => ({ inputValue: value })) }
尽量别用ref(发生事件(例如点击、失去焦点等)的元素正好是要操作事件的元素(onChange={this.handleInputChange}),就可以之前的函数中event.target来代替ref的使用),因为react建议用数据驱动的形式编写代码,尽量不要直接操作DOM,有时候使用ref会出现问题,例如ref和setState合用的时候有时会出现一些坑,因为在方法中,使用setState是一个异步函数,不会立即执行,会和其他代码有个执行顺序,一些代码即使写在下面,但是是同步代码,会在setState运行之前运行的,例如下面这种情况
handleBtnClick() { this.setState((preState) => ({ list: [...preState.list, preState], inputValue: '' })) console.log(this.ul.querySelectAll('div'.length)) //想要先执行setState数据改变后再执行这句获取页面的DOM信息,结果这句是同步代码,先执行了,没达到预期效果 //this.ul是上面ref的写法 } //可以这样改,setState有第二个参数,也是一个回调函数,执行完第一个回调函数后会执行第二个回调函数 handleBtnClick() { this.setState((preState) => ({ list: [...preState.list, preState], inputValue: '' }), () => { console.log(this.ul.querySelectAll('div'.length)) }) }
附:解构赋值的连续写法
//取this实例对象的keywordElement属性下的value
const {value} = this.keywordElement
console.log(value)
//连续解构赋值
const {keywordElement: {value}} = this //这种形式还可以继续嵌套实现更深层级的连续解构赋值
console.log(value)
console.log(keywordElement) //报错,说明keywordElement没有被解构出来,只把value解构出来了
//连续解构赋值的同时重命名
const {keywordElement: {value: data}} = this
console.log(data) //把value解构出来后改名为data
页面中所有输入类的DOM(例如input,radio等),随着输入就能把输入值维护到状态state中去,需要用数据的地方直接从状态中取出来,就属于受控组件。(和Vue中的双向绑定类似)
表单内输入类DOM的值,数据现用现取(需要用数据的时候取节点,再取节点的value),就属于非受控组件。
项目中更偏向于受控组件,因为非受控组件有几个需要输入的元素就要写几个ref。
一类特别的函数:只要是同样的输入(实参),必定得到同样的输出(返回)
必须遵守的约束:
不得改写参数数据;
不会产生任何副作用,例如网络请求,输入和输出设备;
不能调用Date.now()或者Math.random()等不纯的方法
redux的reducer函数必须是一个纯函数
//#region /* 如果在代码中发现没办法折叠显示了,用region这种方式进行折叠 下面例子中saveFormDate就是高阶函数,也应用了函数的柯里化 高阶函数:满足下面规范中任意一个就是了 1.若A函数,接收到的参数是一个函数,那么A就可以称之为高阶函数 2.若A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数 常见的高阶函数有:Promise、setTimeout、arr.map等等 函数的柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式。 function sum(a) { return (b) => { return (c) => { return a + b + c } } } const result = sum(a)(b)(c) console.log(result) */ //#endregion //实现多个input框同用一个函数名处理数据 state = { username: '', password: '' } saveFormDate = (dataType) => { //相当于把下面input框内saveFormDate方法的参数username或者password传给dataType,例如dataType = username return (event) => { //返回值是函数,将这个函数绑定在input框的onChange事件上,可以通过event获取到event对象,input框的值为event.target.value this.setState({ [dataType]: event.target.value //对象的键如果使用读取到的变量来赋值,就需要用[]的方式,下面有讲解 }) } } //这里用了函数的柯里化,因为下面this.saveFormDate的时候不能将这些参数同时调用('username', event),event是React自动帮你实现的事件对象 handleSubmit = (event) => { event.preventDefault() //阻止表单提交 const {username, password} = this.state alert(`你输入的用户名是:${username},你输入的密码是${password}`) } render() { return( <form onSubmit={this.handleSubmit}> //action属性值为表单提交的地址 用户名:<input onChange={this.saveFormDate('username')} type="text" name="username" /> //name="username"最终会展示在页面url的地址的参数中,例如localhost:3000?username=123&password=345 密码:<input onChange={this.saveFormDate('password')} type="password" name="password" /> //这俩输入框通用一个方法,假如是onChange={this.saveFormDate}是将实例的saveFormDate方法给输入框,输入框一旦触发onChange,saveFormDate执行,必须把一个函数交给onChange作为回调。但是this.saveFormDate('password')这种就是相当于this.saveFormDate()函数直接执行了,并把参数'password'传过去,方法执行的返回值返回给输入框,输入框触发onChange的时候,执行这个返回值,所以上面saveFormDate方法的返回值是一个函数 <button>登录</button> ) } //上面的其实不用柯里化也能实现 saveFormDate = (dataType, event) => { this.setState({ [dataType]: event.target.value }) } } 用户名:<input onChange={(event) => {this.saveFormDate('username', event)}} type="text" name="username" /> //onChange需要一个函数作为回调,这里直接onChange直接写成函数的形式,input框内数据变化,执行onChange的回调函数,进而执行了saveFormDate方法 //箭头函数简化 <input onChange={event => this.saveFormDate('username', event)} type="text" name="username" />
附:对象通过读取变量来赋值对象的键的方式
let a = 'name'
let obj = {} //想要对象里为{name: 'tom'}
//obj.name = 'tom' //这样就可以
//但是如果想读取变量a读出来(就是通过a来获得键),不能用.,要用[]
//如果用.
//obj.a = 'tom' //得到的是{a: 'tom'}
//要用[]
obj[a] = 'tom' //{name: 'tom'}
生命周期函数指在某一时刻组件会自动执行的函数
组件初始化;组件更新;组件卸载
当组件创建的那一刻,constructor会被自动地执行,用于initialization初始化数据set props and state,但它并不是react独有的,是es6中自带的函数,所以可不算react的生命周期函数,但它和react生命周期函数没有太大区别
附:
//渲染组件
ReactDOM.render(<Life />, document.getElementById('test'))
//卸载组件(可以用在方法里)
death = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('test'))
}
页面挂载
组件第一次被放到页面上叫做挂载。static getDerivedStateFromProps(props,state):静态方法生命周期钩子。不是给实例用的,所以要加static,给类自身用的。getDerivedStateFromProps 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。必须有返回值,它应返回一个state状态对象来更新 state,如果返回 null 则不更新任何内容。为什么这个生命周期钩子要设计成静态方法呢?因为这样开发者就访问不到this也就是实例了,也就不能在里面调用实例方法或者setState了,用一个静态函数getDerivedStateFromProps来取代被废弃的其他几个生命周期函数,就是强制开发者在render之前只做无副作用的操作,而且能做的操作局限在根据props和state决定新的state 而已。第一个参数props是组件的props对象,第二个参数state是现在的state对象。该方法适用于罕见的用例,即state的值在任何时候都取决于props;componentDidMount(组件被挂载到页面之后,自动被执行)(常用,做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息),获取Ajax数据的代码在componentDidMount中获取,如果在render里面写Ajax代码,render会重复执行,导致代码重复执行,假如在componentWillMount里面写代码的话,虽然这个生命周期函数也只执行一次,但是写React Native或者用React做服务器端的同构等更深的技术时,可能会和以后更高端的技术产生冲突。发送Ajax请求,需要安装axios。
//安装axios
yarn add axios
//引入
import axios from 'axios'
//在componentDidMount内部获取Ajax数据
componentDidMount() {
axios.get('api/todoList').then((res) => {
this.setState(() => ({
list : [...res.data] // [...res.data]这样写比直接写res.data更好,避免一不小心把res的data数据做了修改造成的不可预知数据改变的情况
})
}).catch(() => {...})
}
组件更新
shouldComponentUpdate (nextProps, nextState) (组件被更新之前,自动被执行,需要返回一个布尔类型的返回结果) 。其他生命周期函数删掉都可以,render删掉则会报错,因为组件是继承自React.Component,内部默认内置了其他生命周期函数,唯一没有内置render函数的默认实现,所以需要我们自己定义render。父组件的render函数被执行,子组件的render函数也会被执行,即使子组件内容不变也会被执行,所以会发生性能损耗,可以在子组件中加入这个
shouldComponentUpdate (nextProps, nextState) {
if (nextProps.content !== this.props.content) {
return true;
}else {
return false;
}
}
;getSnapshotBeforeUpdate(prevProps,prevState):保存状态快照。必须有返回值,返回一个snapshotValue(任何值都可以是快照值,比如字符串、数字等)或者null。它是用来代替componentWillUpdate生命周期钩子函数的。在 React 开启异步渲染模式后,在 render 阶段读取到的 DOM 元素状态并不总是和 commit 阶段相同,这就导致在componentDidUpdate 中使componentWillUpdate 中读取到的 DOM 元素状态是不安全的,因为这时的值很有可能已经失效了。当render阶段完成后,意味着在内存中构建的workInProgress树所有更新工作已经完成,这包括树中fiber节点的更新、diff、effectTag的标记、effectList的收集。循环effectList链表将有更新的fiber节点应用到页面上是commit阶段的主要工作。getSnapshotBeforeUpdate 中读取到的 DOM 元素状态是可以保证与 componentDidUpdate 中一致的。此生命周期返回的任何值都将作为参数传递给componentDidUpdate(); componemtDidUpdate(prevProps,prevState, , snapshotValue)(组件更新完成之后,它会自动被执行)
强制更新
this.forceUpdate()使用之后页面强制更新一次,不改状态也能更新,和this.setState()类似使用
Unmounting
componentWillUnmount(当这个组件即将被从页面中剔除的时候,会被执行)(常用,做一些收尾的事,例如:关闭定时器、取消订阅消息)
React性能优化的点:
1.this.handleClick.bind(this)改变作用域的话,放在constructor中来做,整个函数作用域绑定的操作只会执行一次,也可以避免组件的无畏地渲染
2.react底层的setState为异步的函数,将多次数据的改变结合成一次,降低虚拟DOM的频率
3.react底层用了虚拟DOM,diff算法同层比对,key值,来提升虚拟DOM的比对速度,从而提升react的性能
4.借助shouldComponentUpdate提高组件的性能,避免无畏的组件的render函数的运行
5.获取Ajax数据在componentDidMount中获取,如果在render里面写代码,render会重复执行,导致代码重复执行,假如在componentWillMount里面写代码的话,虽然这个生命周期函数也只执行一次,但是写React Native或者用React做服务器端的同构等更深的技术时,可能会和以后更高端的技术产生冲突
//CSS3的过渡动画 .show { opacity: 1; transistion: all 1s ease-in; } .hide { opacity: 0; } //CSS动画效果 .show { opacity: 1; transistion: all 1s ease-in; } .hide { animation: hide-item 1s ease-in forwards; /*forwards保存动画最后一帧的样式*/ @keyframes hide-item { 0% { opacity: 1; } 100% { opacity: 0; } }
CSSTransition组件给单个元素增加样式
App.js
//安装 yarn add react-transition-group //有这三个Transition、CSSTransition、TransitionGroup方面 //引入CSSTransition这个动画组件 import { CSSTransition } from 'react-transition-group' <CSSTransition in = {this.state.show} //可变化的布尔值,用于判断是出场动画还是入场动画 timeout = {300} //动画执行时间 classNames = {fade} //注意这里有s unmountOnExit //标签隐藏之后对应的dom节点也移除了 onEntered = {(el) => {el.style.color = 'blue'}} //钩子函数,el为内部包裹的元素。不光可以使用css,还可以使用js。onEntered是入场动画执行结束之后,和css的.fade-enter-done一样。还有onEnter、onEntering、onExit、onExiting、onExited钩子 appear = {true} //第一次展示在页面就有动画效果。在入场动画的第一帧增加fade-appear这个class,入场动画的第二个时刻,到入场动画执行完成增加fade-appear-active这个class > //把JSX元素放在CSSTransition里,CSSTransition自动帮我们添加做一些class名字的增加或者移除的工作,自动挂载一些CSS样式 <div>hello</div> </CSSTransition>
style.css
/*CSSTransition会自动做一些class的增加和移除*/ /*入场动画增加的class*/ /*入场动画执行的第一个时刻,加到包裹的标签上这个.fade-enter;fade是class前缀,需要在上面的classNames这里设置,随便起名*/ .fade-enter, .fade-appear{ opacity: 0; } /*入场动画的第二个时刻,到入场动画执行完成*/ .fade-enter-active, .fade-appear-active{ opacity: 1; transition: opacity 2s ease-in; /*transition也可以写在.fade-enter内部*/ } /*整个入场动画完成之后*/ .fade-enter-done{ opacity: 1; /*这里用上面js的方式也能实现*/ color: res; } /*出场动画增加的class*/ /*出场动画执行的第一个时刻*/ .fade-exit{ opacity: 1; } /*出场动画执行过程中一直存在*/ .fade-exit-active{ opacity: 0; transition: opacity 2s ease-in; } /*整个出场动画完成之后*/ .fade-exit-done{ opacity: 0; }
Transition相对于CSSTransition是更底层的一个组件,当CSSTransition一些动画实现不了的时候,可以查一下Transition这个api
TransitionGroup 组件给多个元素增加样式
App.js
import { CSSTransition, TransitionGroup } from 'react-transition-group' <TransitionGroup> //外层 { this.state.map((item, index) => { return ( //内层,在每个具体的标签上还要加CSSTransition,和上面差不多的各种设置,没有了in={},把循环的key不放在之前的标签上,放在CSSTransition设置上 <CSSTransition timeout = {300} classNames = {fade} unmountOnExit onEntered = {(el) => {el.style.color = 'blue'}} appear ={true} key={index} //注意循环的key要写在这里,不写在下面的div标签上 > <div>{item}</div> </CSSTransition> ) }) } </TransitionGroup>
//安装antd
yarn add antd
TodoList.js
import React, { Component } from 'react'; import 'antd/dist/antd.css'; //引入antd的css样式文件,安装之后在node_modules里,但是这是所有的样式,如果只是使用几个组件,就没必要引入全部样式,所以要进行按需引入 import { Input, Button } from 'antd'; //input button list等组件怎么用,看官网的各部分代码介绍 class TodoList extends Component { render() { return ( <div style={{marginTop: '10px', marginLeft: '10px'}}> <div> <Input placehoder='todo info' style={{width: '300px', marginRight: '10px'}} /> <Button type="primary">提交</Button> </div> </div> ) } } export default TodoList;
按需引入样式
在antd官网找到3.x版本(这个介绍的更详细)=>在create-react-app中使用,往下翻到高级配置,进行配置后就不需要import ‘antd/dist/antd.css’; 了
自定义主题(需要先配置上面的按需引入样式)
antd是用less写的样式,要自定义主题,在antd官方在less文件里所写的主题颜色,把这个颜色改掉。按照官网上设置会有一个报错,是因为less-loader升级了,把javascriptEnabled和modifyVars放在lessOptions:{…}里就行了
在刚刚的高级配置再往下翻,有个自定义主题,
UI框架还有ElementUI for React(饿了么)、vantUI(适合移动端,有赞团队)、material-ui(国外的)
附:plugin是插件的意思
真实项目中多个组件经常会分享相同的数据,这种情况下最好将这份共享的状态提升到最近的父组件上进行管理,这种做法称为状态提升
和双向绑定的区别:
单项数据流比双向绑定要写更多的模板代码,比如要用回调函数的方式;好处是可以更快和寻找定位bug,每个组件自己才能操作属于自己的状态数据,也可以使用自定义逻辑来拒绝或者更改用户的输入
Props属性是由上到下单项传递的,很多时候中间组件是不需要这个数据的,而Context提供了在组件中共享此类值的方法,而不必通过组件的每个层级显示地传递props,设计目的是共享那些对于组件来说全局的数据,但是不要因为仅仅为了避免在几个层级下的组件传递props而使用context,context的.js文件本身不是一个component而是object,所以文件放在src层级下,不放在components文件夹下
在应用开发中一般用context,一般都用它的封装react插件,例如react-redux
context.js //context对象就创建完毕了,必须放在祖辈组件和孙子组件都能获取到的地方
import React from 'react'
//创建Context容器对象,ThemeContext大写的原因是组件标签必须大写
const ThemeContext = React.createContext()
export default ThemeContext
App.js里,祖父组件
//引入上面创建的context对象
import ThemeContext from '../theme-context/context.js'
<ThemeContext.Provider value={...}> //属性名必须是value,里面传对象、传字符串啥的都行
包裹子组件并且根据value传入数据
</ThemeContext.Provider>
在子孙组件中。父子组件用props最方便,祖孙组件间采用这种方式
//类组件和函数组件都可以用 import ThemeContext from '../theme-context/context.js' <ThemeContext.Comsumer> { //这里要一个函数 value => { //参数value就是context中的value数据 return value.name //要显示的内容 } } </ThemeContext.Comsumer> //第二种方式,只能类组件使用 //声明接收Context static contextType = ThemeContext render() { return( //this.context就可以取到value传过来的值了 ) }
Redux = Reducer + Flux
Flux是官方推出的最原始的辅助React使用的数据层框架,有些缺点比如数据依赖的问题,升级到Redux
redux是一个专门用于做状态管理的JS库(不是react插件库),可以用在react、vue、angular等项目中,但基本与react配合使用,作用:集中式管理react应用中多个组件共享的状态,使用场景:1.某个组件的状态,需要让其他组件可以随时拿到(共享)2.一个组件需要改变另一个组件的状态(通信)
使用原则:能不用就不用,如果不用比较吃力才考虑使用
//安装redux
yarn add redux
src文件夹下创建一个store文件夹,里再创建index.js,里面存放store代码
index.js
import { createStore } from 'redux'; //createStore是个方法
import reducer from './reducer'; //把数据的记录传给store
const store = createStore(reducer); //创建store这个数据公共存储仓库
export default store;
store文件夹下的reducer.js返回的是一个函数
reducer.js
const defaultState = {
inputValue: '12',
list: [4, 5, 6]
};
export default (state = defaultState, action) => {return state;}
//state就是store仓库里存储的数据
reducer初始化状态,被第一次调用时是store自动触发的,传递的preState是undefined,传递的action是{type:@@REDUX/INIT_随机数};其他的是加工状态,加工时根据旧的state和action,产生新的state的纯函数。
附:数字的字符串变为数字,可以乘以1,例如’2’*1为数字2
附:箭头函数返回一个对象
//不能直接这样写
const a = b => {} //这样写不对,因为{}会认为是函数体的花括号
//可以这样写
const a = b => ({})
//或者这样写
const a = b => {return {}}
TodoList.js
import store from './store'; //引入store仓库(寻找index.js文件,可以简写,不用写全,会自动找index.js)
class TodoList extends Component {
constructor(props) {
super(props);
this.state = store.getState(); //store提供一个getState()方法,获取store中的state数据
}
}
Redux-DevTools–Redux中间件
安装Redux DevTools插件,Chrome浏览器更多工具=》扩展程序=》应用商店搜索Redux DevTools
使用方法(最新版):比下面的这种简单
//安装
yarn add redux-devtools-extension
index.js
import { createStore } from 'redux';
import reducer from './reducer';
//引入
import {composeWithDevtools} from 'redux-devtools-extension'
//如果不用redux中间件,直接写在第二个参数就可以
//const store = createStore(reducer, compostWithDevtools());
//如果使用中间件
const store = createStore(reducer, compostWithDevtools(applyMiddleware(thunk)));
export default store;
这样就可以使用redux-devtools了。下面的操作是之前的方式
在控制台会有Redux菜单,点击instructions,按照提示在创建store的第二个参数添加代码后才能使用。数据一目了然
import {createStore } from 'redux';
const store = createStore(
reducer,
window.__REDUX_DEVTOOLS_ETENSION__&&window.__REDUX_DEVTOOLS_ETENSION__()
//如果有window.__REDUX_DEVTOOLS_ETENSION__就执行
)
创建action并dispatch
TodoList.js
handleInputChange(e) {
const action = { //这条命令是要修改store里的value值为e.target.value
//action是一个对象,具体传给reducer什么东西视情况而定
type: 'change_value', //描述要做的事情是什么
value: e.target.value
}
store.dispatch(action) //store的dispatch方法就把这个命令传给store
}
store会把接收到的action命令直接传递给reducer
reducer.js
export default (state = defaultState, action) => { //接收action
//state是上一次存储的数据,action是用户传过来的那句命令
if (action.type === 'change_value') {
const newState = JSON.parse(JSON.stringify(state)); //深拷贝之前state的内容
//reducer可以接收state,但决不能修改state,所以要进行深拷贝一份
//还有一个原因是,react会做一个浅比较,假如当state值是数组或者对象直接在原始数据上修改之后,react浅比较后认为数据没变化(因为地址没改变),页面就不更新。而且如果要直接改参数的话就相当于违反了纯函数的第一个约束,不得改写参数数据,redux中规定reducer必须是纯函数。
//要像上面那种深拷贝一份或者使用[...state, date]这种方式也行。不能直接state.push(data)这种方式
newState.inputValue == action.value;
return newState; //newState返回给了store,用这个返回的新数据替换掉之前的老数据
}
return state;
}
TodoList.js
import store from './store';
class TodoList extends Component {
constructor(props) {
super(props);
this.state = store.getState();
this.handleInputChange = this.handleInputChange.bind(this);
this.handleStoreChang = this.handleStoreChang.bind(this);
store.subscribe(this.handleStoreChang()); //这个组件订阅store,store里的数据只要发生改变,订阅里的函数就会自动执行
}
handleStoreChange() {
this.setState(store.getState()); //当感知到store发生变化时,store.getState()重新从state取一次数据,再用setState替换掉当前组件的数据,这样组件里的数据就和最新的store里的数据同步了,就变成最新的数据了
}
}
对象类型的action叫做同步action,函数类型的action叫做异步action,异步action中一般都会调用同步action,异步action不是一个必须要用的东西,把异步的代码放在某组件的index.js文件中执行方法的里面写也行
ActionTypes的拆分
store文件夹下新建actionTypes.js用于ActionTypes的拆分
actionTypes.js
//创建常量
export const CHANGE_VALUE = 'change_value';
需要在组件文件TodoList.js、reducer.js文件和下面的actionCreator.js文件内import { 常量名 } from来引入常量,替换掉之前的action的types,例如change_value。目的在于假如你把action的types值拼错了,可以直接找到报错来源,之前的action的type用字符串的那种方式假如拼错不会报错,但是却没有应有的使用效果,处理bug但是找它很费劲。
actionCreator统一创建action
store文件夹下创建一个actionCreators.js
目的:提高代码可维护性,前端的自动化测试也会很方便
actionCreators.js
import { CHANGE_VALUE } from './store/actionTypes'
export const getInputChangeAction = (value) => {
type: CHANGE_VALUE,
value
}
TodoList.js
//之前的样子 handleInputChange(e) { const action = { type: 'change_value', value: e.target.value } store.dispatch(action) } //之前的上面的函数变为下面的样子 import { getInputChangeAction } from './store/actionCreators' //因为TodoList.js不需要再引入CHANGE_VALUE这些常量了,所以把import { CHANGE_VALUE } from './store/actionTypes'这句可以在TodoList.js中删掉了 handleInputChange(e) { const action = getInputChangeAction(e.target.value); store.dispatch(action) }
redux设计和使用时有三个基本原则:
store是唯一的;
只有store能够改变自己的内容,而不是reducer,store把数据给了reducer,reducer把数据进行深拷贝后处理复制的这一份数据,再把处理后的数据返回给store,store是自己改变自身的内容的;
Reducer必须是纯函数(纯函数:给定固定的输入,不管在任何时刻都会有固定的输出,假如函数里有setTimeout这种异步的操作或者new Date这种和日期相关的内容,就不算纯函数了,而且纯函数不会有任何的副作用,比如不能对传入的store的state的数据做修改)
UI组件(傻瓜组件)和容器组件(聪明组件)
UI组件主要负责页面的渲染,容器组件主要负责页面的逻辑
src文件夹下创建TodoListUI.js,把TodoList.js里关于页面渲染的部分全部放在这里,是一个TodoListUI组件
在TodoList.js里import这个TodoListUI组件,把需要引入的部分转移到TodoListUI.js中
父子组件间传值,父组件向子组件传递带有参数的方法时,不能用之前的this.handleItemDelete.bind(this, index)了,父组件还是用this.handleItemDelete = this.handleItemDelete.bind(this),handleItemDelete() = {this.handleItemDelete()}父组件通过属性方式向子组件传值,子组件要用函数的形式来写,如下把index传给父组件
<List //List是antd的一个组件,这里关注onClick的() => {this.props.handleItemDelete(index)}这个写法就行,这里this.props.handleItemDelete(index)和之前写法一致,只不过不是直接作为函数来执行,而是用在了新的函数的函数体内
renderItem={(item, index) => (
<List.Item onClick = {() => {this.props.handleItemDelete(index)}}>{item}</List.Item>
)}
/>
无状态组件
一个组件内部只有一个render函数的时候就可以定义为无状态组件,就是一个函数,一般用在UI组件这种负责页面渲染不负责逻辑的情况
优点:性能比较高,因为它就是一个函数,里面没有任何的生命周期函数,也不会生成真正的组件的实例,而其他组件是一个类,里面还会有生命周期函数,执行起来远比一个函数形式的组件多得多
TodoListUI.js
import ...
const ToDoList = ( props ) => { //接收props
return ( //返回JSX
//正常组件的render函数的内容,记得this.props处换为props
)
}
(react是Facebook的,redux是其他团队的,redux-thunk是Facebook的,方便更加舒服地在react中使用redux)
Redux-thunk–Redux中间件–在action中写函数–进行ajax请求发送
redux-thunk将axios异步请求或者复杂的逻辑放在action中进行处理
//安装redux-thunk
yarn add redux-thunk
在store文件夹下的index.js文件中使用,通过Redux创建store时要用中间件
index.js
import { createStore, applyMiddleware, compose } from 'redux'; //引入applyMiddleware //compose是一个包装函数,可以向这个函数传递很多方法,传递的方法会依次执行 import thunk from 'redux-thunk'; //引入thunk //常规使用 const store = createStore(reducer, applyMiddleware(thunk)) //但是要和redux-devtools一起使用的话,需要到github上的redux devtools extension上寻找高级设置代码 import {createStore, applyMiddleware, compose } from 'redux'; const componentEnhancers = windows.__ReDUX_DEVTOOLS_EXTENSION ?window.__ReDUX_DEVTOOLS_EXTENSION():compose //这里两个中间件一起用了 const enhancer = componentEnhancers(applyMiddleware(thunk)); const store = createStore(reducer, enhancer); export default store;
之前actionCreator返回的内容是一个对象,使用redux-thunk之后返回的内容不仅仅可以是一个对象了,还可以是一个函数,函数里面返回异步axios请求的数据,函数执行结束再action传给store。不应该将异步代码放在声明周期函数里,容易使得异步代码越来越多,组件就会越来越大,所以要放在actionCreators这里更合适
之前是这样的TodoList.js
componentDidMount() {
axios.get('./list.json').then((res) => {
const data = res.data;
const action = initListAction(data);
store.dispatch(action)
}
}
现在是这样的
TodoList.js
componentDidMount() {
const action = getToDoList(); //getToDoList()执行后返回一个函数
store.dispatch(action); //本来dispatch只能接受对象形式的action,但是使用了redux-thunk后,dispatch可以接收函数形式的action了,发现是函数形式的action,把action发给store时action就会自动执行
}
actionCreates.js
import axios from 'axios'
export const getToDoList = () => {
return (dispatch) => { //这个函数能够接收到dispatch方法
axios.get('./list.json').then((res) => {
const data = res.data; //获取完数据后要改变store里的数据,继续用action
const action = initListAction(data);
dispatch(action)
})
}
}
当是函数时能够接收到store的dispatch方法
getToDoList还需要在组件文件的componentDidMount内执行action,dispatch,也就是函数执行结束才把action传给store
这样做有益于生命周期函数内部代码的简洁性和自动化测试
redux中间件的中间指的是action和store中间,中间件其实就是对store的dispatch的一个升级
中间件比较火的有redux-thunk(处理异步操作,把异步操作放在action中) redux-logger redux-saga(也是处理异步操作的,单独把异步的操作拆分出来放在另外的文件中管理)
Redux Data Flow
Redux的标准流程:View在Redux中会派发一个action,action通过Store的dispatch方法派发给store,store接收action和之前的state一起传给reducer,reducer返回一个新的数据给store,store改变自己的state
Redux中间件指的是action和store中间,action通过dispatch方法传递给store,所以action和store中间就是dispatch方法,中间件就是对dispatch方法的封装或者说对dispatch方法的升级,升级后的dispatch接收到的action是对象的话直接把action传递给store,如果接收到的action是函数的话,会先执行action这个函数,执行完之后需要调用store的时候,这个函数再调用store
还有其他Redux的中间件,比如Redux-logger可以记录action每次派发的日志,也是对dispatch进行升级,每次派发action的时候,把action通过console.log打印在控制台中;Redux-saga也是解决Redux中异步问题的中间件,不同于Redux-thunk把异步操作放在action中进行处理,而是单独把异步逻辑拆分出来放到单独的文件进行管理,
中间件指的是action和store的中间,
详细讲解在github上搜redux-saga
//安装redux-saga
yarn add redux-saga
store下的index.js
import { createStore, applyMiddleware, compose } from 'redux'; //引入applyMiddleware import reducer from './reducer'; //引入 import createSagaMiddleware from 'redux-saga'; //引入saga文件 import toDoSaga from './sagas'; //创建中间件 const sagaMiddleware = createSagaMiddleware(); //这里使用和redux-thunk差不多一致 const componentEnhancers = windows.windows.__ReDUX_DEVTOOLS_EXTENSION ? window.windows.__ReDUX_DEVTOOLS_EXTENSION() :compose const enhancer = composeEnhancers(applyMiddleware(sagaMiddleware)); const store = createStore(reducer, enhancer); //把saga文件执行起来 sagaMiddleware(toDoSaga); export default store;
创建sagas.js文件单独存放异步逻辑,把异步的代码都写在saga.js文件中,根据官网代码写,saga.js内部使用的是generator函数,经过saga在创建store文件内底层配置后,一旦dispatch后,reducer内和saga.js内都可以接收到派发的action,在saga.js内部处理异步函数
sagas.js
//引入,看官网 import { takeEvery, put } from 'redux-saga/effects'; import { GET_INIT_LIST } from './actionTypes'; import { initListAction } from './actionCreators'; import axios from 'axios'; function* getInitList() { //这里可以是普通方法也可以是generator函数,建议使用generator函数 try{ const res = yield axios.get('/list.json'); const action = initListAction(res.data); yield put(action); //相当于dispatch }catch(e) { console.log("list.json网络请求失败"); //Ajax获取数据失败 } } function* mySaga() { //takeEvery捕获到每次dispatch的action,和reducer.js也能捕获到这个action一样。当接收的action类型是GET_INIT_LIST类型的时候,会执行getInitList这个方法 yield takeEvery(GET_INIT_LIST, getInitList); } export default mySaga;
TodoList.js
componentDidMount() {
const action = getInitList();
store.dispatch(action);
}
actionCreators.js
import { GET_INIT_LIST } from './actionTypes';
export const getInitList = () => ({
type: GET_INIT_LIST
})
export const initListAction = (value) => ({
type: INIT_LISST_ACTION,
value
})
actionTypes.js
export const GET_INIT_LIST = 'getInitList';
redux-saga远比redux-thunk复杂的多,内置api非常多,由于它的这种拆分为另一个文件,处理非常大型项目时感觉更优于redux-thunk,redux-thunk使用更加简单
UI组件:不能使用任何redux的api,只负责页面的呈现、交互等。
容器组件:负责和redex通信,将结果交给UI组件。
容器组件是UI组件的父组件,通过props传值
附:
上面的监测store.subscribe()可以放在主入口文件index.js中
只要redux里发生任何的变化,App组件就重新调用一下render(),即使只有一个地方改了也会重新调用一次,但不会有太大的效率问题,因为虚拟DOM的diffing算法
import store from './redux/store'
ReactDOM.render(<App/>, document.getElementById('root'))
//监测redux中状态的改变,如redux的状态发生了改变,那么重新渲染App组件
store.subscribe(() => {
ReactDOM.render(<App/>, document.getElementById('root'))
})
//但是有了容器组件就不需要上面的store.subscribe()了,容器组件直接帮你监测了
第三方模块,在React中更加方便地使用Redux,将组件和store做连接
//安装react-redux
yarn add react-redux
index.js–项目的入口文件
import React from 'react'; import ReactDOM from 'react-dom'; import TodoList from './TodoList'; import { Provider } from 'react-redux'; //Provider就是一个组件,一个react-redux核心的api import store from './store'; //在这里引入store const App = ( //创建App组件 //Provider的属性store就等于引入的store //提供器Provider连接了store,Provider里面的所有组件都能拿到store数据 <Provider store={store}> <ToDoList/> //这里的组件就能获取store里的内容,在这个组件里面进行获取 ...其他组件 </Provider> ); ReactDOM.render(App, document.getElementById('root'));
TodoList.js
//之前获取store里的数据是用store.getState() constructor(props) { super(props); this.state = store.getState(); } //使用react-redux后获取store里的数据 import { connect } from 'react-redux'; class TodoList extends Component { render() { return ( <div> <div> //因为mapStateToProps(),这里就是this.props.inputValue而不是this.state.了 //因为mapDispatchToProps(),在props中使用changeInputValue()方法 <input value={this.props.inputValue} onChange={this.props.changeInputValue()} /> <button>提交</button> </div> </div> ) } } const mapStateToProps = (state) =>{ //state是store里的数据;mapStateToProps把store里的数据映射给组件,然后state.inputValue变成组件的props的inputValue //mapStateToProps函数返回的是一个对象,返回的对象中的key作为传递给UI组件props的key,value就作为传递给UI组件props的value,相当于父组件给子组件传递了状态 return { inputValue: state.inputValue } } //把store.dispatch方法挂载到props,mapDispatch里面的方法可以dispatch(action)。 //mapDispatchToProps函数返回的是一个对象,返回的对象中的key作为传递给UI组件props的key,value就作为传递给UI组件props的value,相当于父组件给子组件传递了操作状态的方法 const mapDispatchToProps = (dispatch) => { //dispatch就是store.dispatch方法 return { changeInputValue = (e) => { //这里应该放在actionCreators.js和actionType.js文件里,像之前一样 const action = { type: 'change_input_value', value: e.target.value } dispatch(action); } } } export default connect(mapStateToProps, mapDispatchToProps)(ToDoList); //导出connect方法,把ToDoList传给这个方法。意思是将ToDoList组件和store做连接,通过connect方法连接,连接的规则在mapStateToProps里。 //注意这种简写形式 export default connect( state => ({count: state}), //mapStateToProps一般写法 //dispath => ({ // jia: number => dispatch(createIncrementAction(number)), // jian: number => dispatch(createDecrementAction(number)), // jiaAsync: (number, time) => dispatch(createIncrementAsyncAction(number, time)) //}) //mapStateToProps简写形式 { //mapStateToProps除了可以是函数,还可以是对象,是对象的时候可以简写 jia: createIncrementAction, //UI组件调用jia方法时直接执行了createIncrementAction方法,并且传递的参数也传过去了,到action这步了,react-redux直接dispatch了 jian: createDecrementAction, jiaAsync: createIncrementAsyncAction } )(ToDoList)
代码变得更加精简
render() {
const { inputValue, changeInputValue } = this.props; //解构赋值,下面的this.props.inputValue直接写成inputValue,changeInputValue也是
}
//ToDoList组件这时只负责页面的渲染,是一个UI组件,里面只有一个render函数,可以写成无状态组件。
//connect把UI组件和数据和业务逻辑相结合,返回的内容是一个容器组件,即export default导出的内容就是一个容器组件
//安装 yarn add pubsub-js //在需要引入的组件内引入 import PubSub from 'pubsub-js' componentDidMount() { //在需要接收数据的组件里要订阅消息,放在componentDidMount钩子里 this.token = PubSub.subscribe('MY TOPIC', mySubscriber); //接收到了这个消息'MY TOPIC', 执行mySubscriber回调函数 //定义一样token变量的意思就像定时器timer一样,想要取消订阅的时候取消token } var mySubscriber = function(msg, data) { //msg是消息名'MY TOPIC', data就是别人想要交给你的数据 ... } //在componentWillUnmount钩子里取消订阅 componentWillUnmount() { PubSub.unsubscribe(this.token) } //发布消息 PubSub.publish('MY TOPIC', 'hello world');
jQuery(容易产生回调地狱)和axios(Promise风格)都是用XMLHTTPRequest对象提交ajax请求,而windows内置的fetch不用xhr也能发ajax请求,而且本身也是Promise风格的,fetch用的关注分离思想(把一个总体进行拆分,分别关注是否成功)
fetch使用率不高,因为兼容性问题,老版本可能不支持
附:promise可以统一处理错误,就是链状的.then可以在最后只写一个.catch用于处理报错情况
附:为了分清组件的.js文件和普通的.js文件,组件的.js文件首字母要大写,还可以将组件的.js文件改为.jsx文件,文件的小图标就变了,和普通的.js文件的图标就不一样了。项目的总的组件App.js可以不改。
附:在文件引入的时候,可以省略的是.js或者.jsx这个文件后缀;如果在组件文件夹下的组件名为index.js或者index.jsx,引入的时候直接引入到整个组件文件夹名就可以,index.js或index.jsx可以省略,也能找到。怎么使用看情况,也可以直接在组件文件夹下的组件用组件.js,看公司的规定。
1.拆分组件:拆分界面,抽取组件
2.实现静态组件:使用组件实现静态页面效果
3.实现动态组件
3.1动态显示初始化数据
3.1.1数据类型
3.1.2数据名称
3.1.3保存在哪个组件
3.2交互(从绑定事件监听开始)
<Item id={todo.id} name={todo.name} done={todo.done} />
//像这样的给组件的props传入多个属性,如果有几十个属性,很麻烦
//直接这样写
<Item {...todo} />
//{...todo}用这种语法直接将todo对象的属性全部传给Item组件的props
附:
//input标签的checked属性设置之后,在页面不能通过点击进行修改是否被选中,如果想要修改必须调用onchange方法
<input type="checkbox" checked={true} onChange={...} />
//如果是defaultChecked属性被设置了,可以在页面通过鼠标点击进行修改,但是defaultChecked只在第一次起作用,页面刚呈现那一次起作用,checked始终以最后一次为主
<input type="checkbox" defaultChecked={true} />
//类似的还有defaultValue和value
附:
handleKeyUp = (event) => { //解构赋值获取keyCode, target const {keyCode, target} = event //判断是否是回车按键 if(keyCode !== 13) return //内容不能为空,这里用去除字符串空格后判断一下 if(target.value.trim() === '') { alert('输入不能为空') } ... } //键盘的点击事件是onkeyup <input onKeyUp={this.handleKeyUp} type="text" placeholder="输入任务名称" /> //鼠标有移入和移出的事件 <li onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}> <label> <input type="checkbox" defaultChecked={done} /> <span>{name}</span> </label> </li> //input标签的回调函数中可以通过event.target.value拿到里面的值,如果类型是checkbox的话,可以通过event.target.checked拿到是否勾选的布尔值
附:uuid、nanoid
//对于需要自己设定一个id值的地方可以使用uuid,会返回一个全球独一无二的字符串
//安装
yarn add uuid
//uuid库有点大,可以使用nanoid这个库
//安装
yarn add nanoid
//引入
import {nanoid} from 'nanoid'
//使用,执行这个函数会返回一个唯一的字符串
id = nanoid()
附:状态在哪里,操作状态的方法就在哪里,例如状态state在App组件内,那么操作状态的所有方法都要放在App组件内
附:数组的filter函数
const todo = [2, 3, 5, 3]
//把数组中的3去掉
const newTodo = todo.filter((item) => {
//数组中不为3的项返回
return item !== 3
})
附:因为react的JSX中只能写js表达式,所以不能写if语句,可以用三元表达式实现if语句效果,而且三元表达式还可以嵌套使用
附:删除对象的属性delete
let obj = {a: 1, b: 4}
delete obj.a
console.log(obj) //{b: 4}
附:在react中如果想用confirm(‘确定删除吗?’),直接写会报错,应该用window.confirm(‘确定删除吗?’),用户点击之后会返回一个布尔值,
第一种方法:
在package.json文件里添加代理
{
{
},
"proxy": "http://localhost:5000" //localhost:5000是请求数据的服务器地址,客服端地址localhost:3000发送请求,代理也是3000端口,代理因为没有ajax发送引擎,所以代理向5000端口请求数据,数据可以返回给代理
//客户端这边axios.get('http://localhost:3000/students').then(),经过上面配置,相当于在localhost:5000请求数据
//注意:因为客户端这边是3000,所以请求的数据如果是3000/index.html,直接就去本地这边获取了,因为请求的/students本地没有,所以才会去服务端那边获取
}
优点:配置简单,前端请求资源时可以不加任何前缀
缺点:只能配置一个服务器代理地址,假如要从多个服务器上获取数据,这种方法就不可以了
工作方式:当请求了3000不存在的资源时,那么该请求会转发给5000(优先匹配前端资源)
第二种方法:
在项目的src文件夹下创建setupProxy.js文件,文件名不能改动,文件里不能用ES6的语法写,要用CJS写。这个文件不是给前端代码用于执行的,react脚手架会找到这个文件,把这个文件加到webpack配置里面,webpack里面都是CJS语法。
//用到时
axios.get('http://localhost:3000/api1/students').then() //api1/是setupProxy.js文件里配置的路径,localhost:3000的public文件夹下没有/api1/,所以去找代理那里,遇见/api1前缀的请求就会触发该代理配置
//如果你所处的网址是localhost:3000,同时也是给3000发请求的话,可以直接省略前面直接用/api1/students
axios.get('/api1/students').then()
setupProxy.js
优点:可以配置多个代理,可以灵活的控制请求是否走代理(上图,如果不写/api1,就不走5000的代理,如果不写/api2,就不走5001的代理)。
缺点:配置繁琐,前端请求资源是必须加前缀
Hello文件夹下放置Hello组件index.jsx,样式组件放置全局内有类名相同时冲突,可以样式文件为index.module.css,然后在index.jsx中引入时这样写
import React, {Component} from 'react'
import hello from './index.module.css'
//之前引入时这样的
//import './index.css'
export default class Hello extends Component {
render() {
return <h2 className={hello.title}>Hello, React!</h2>
{/*这里要这样写hello.title,之前是<h2 className="title">Hello, React!</h2>*/}
}
}
也可以用下面的styled-components避免类名冲突的问题
.css文件一旦在一个文件中引入了,是全局生效的,可能会造成不同组件内的css样式冲突
styled-components对样式进行管理,组件自己的样式只对自己生效
//安装
yarn add styled-components
index.js
//在入口文件index.js中之前引入index.css文件,现在改为引入style.js文件
import './style.js';
全局样式的书写
style.js
//之前在index.css文件中的全局样式可以这样写 body { margin: 0; padding: 0; font-family: sans-serif; } //现在在style.js中不可以这样写了,需要这么写 import { injectGlobal } from 'styled-components'; injectGloabl` body { margin: 0; padding: 0; font-family: sans-serif; } `
reset.css
使用reset.css进行样式统一,因为不同浏览器内核对标签的默认样式是不同的
把reset.css的代码粘贴到上面全局样式的style.js的injectGlobal中,设置全局样式
在src文件夹下创建common文件夹,用于存放像Header这样的公共组件,common下创建header文件夹,文件夹内创建index.js来书写Header组件;创建style.js书写样式
index.js
import React, { Component } from 'react'; import { HeaderWrapper, Logo } from './style'; class Header extends Component { constructor(props) { super(props); focused: false; } render() { return ( <HeaderWrapper> <Logo href='/' /> //点击后回到首页,这是第一种方式,第二种方式style.js里面设置 <NavItem className="left active">首页</Navitem> <NavItem className="left"> <i className="iconfont"></i> //iconfont图标 </Navitem> <NavItem className="right">下载</Navitem> <NavSearch className={this.state.focused ? focused : ""}></NavSearch> </HeaderWrapper> ) } } export default Header;
style.js
import styled from 'styled-components'; //引入图片 import logoPic from '../../statics/logo.png'; //由styled-components这个工具创建一个HeaderWrapper组件,这个组件其实就是一个div标签 export const HeaderWrapper = styled.div` position: relative; height: 56px; background: red; `; //一张具有跳转功能的图片 //里面这样写background: url(../../statics/logo.png);是不行的,原因是webpack打包时不知道我们当前的工程目录是什么样的,会把这个路径解析为字符串,所以用下面的形式 export const Logo = styled.a` position: absolute; top: 0; left: 0; display: block; width: 100px; height: 56px; background: url(${logoPic}); //``字符串引入变量的写法 background-size: contain; //图片刚好占满区域 `; //第二种方式,点击后回到首页 export const Logo = styled.a.attrs({ //属性设置 href: '/' })` ... `; //NavItem标签的不同样式 export const NavItem = styled.div` position: relative; line-height: 56px; padding: 0 15px; font-size: 17px; color: #333; &.left { //NavItem组件有left这个class的话 float: left; } &.right { float: right; color: #969696; } &.active { color: #ea6fja; } .iconfont { //NavItem下的iconfont类的样式 position: absolute; top: 0; left: 0; } `; export const NavSearch = styled.input.attrs({ placehoder: '搜索' })` width: 160px; height: 38px; ... box-sizing: border-box; &..placeholder { //对组件NavSearch下面的placeholder的样式进行设置 color: #999; } &.focused { width: 200px; } `;
//安装
install bootstrap --save
//在index.js文件引入
import 'bootstrap/dist/css/bootstrap.min.css'
路径是这样的,public文件夹下有css文件夹(内部有bootstrap.css)和favicon.ico和index.html
脚手架内置个服务器,就是通过webpack dev server开启的服务器,react脚手架里通过webpack配置的public文件夹就是localhost:3000这台内置服务器的根路径,开启服务器运行项目目的是为了得到一个最真实的开发场景,项目上线都是通过主机名加端口号的形式访问的
地址栏输入localhost:3000/css/bootstrap.css就可以访问到public/css/bootstrap.css这个文件了,假如请求的东西不存在,就会访问public/index.html文件,这个文件其实就是一个兜底文件,如果你访问的东西不存在,就把这个给你。如果只访问localhost:3000,就会把public/index.html给你
路由路径是多级的结构<Route path=‘/lala/about’ exact component={Home}></ Route>,页面刷新就会样式丢失(shift + 浏览器刷新按钮,是强制刷新,不走缓存按钮),因为浏览器认为多级路径中的/lala也是3000中的路径,获取文件的时候localhost:3000/css/bootstrap.css地址就变为localhost:3000/lala/css/bootstrap.css,获取不到就会把public/index.html给你,样式就丢失了
解决方法:
1.(常用)在index.html引入样式<link rel=“stylesheet” href=“./css/bootstrap.css” />中的.去掉,变为<link rel=“stylesheet” href=“/css/bootstrap.css” />就好了,因为加.了就是相对路径了,从当前文件夹出发找文件,认为多级路径中的/lala也是3000中的路径,就会加上路径上多的lala/css/bootstrap.css,不加.的意思是从localhost:3000下面取东西,也就是public,进而public下的css/bootstrap.css
2.(常用,但是适用于React脚手架工具)<link rel=“stylesheet” href=“./css/bootstrap.css” />换成<link rel=“stylesheet” href=“%PUBLIC_URL%/css/bootstrap.css” />,%PULIC_URL%代表的就是public的绝对路径
3.(少见)<link rel=“stylesheet” href=“./css/bootstrap.css” />不变,使用路由的地方将BrowserRouter改为HashRouter,地址栏就是localhost:3000/#/lala/about这个路由,该地址发请求的时候,自动忽略#后面的东西,因为#后面的东西都是哈希值,请求的时候不带/lala,以在index.html中的href=“./css/bootstrap.css” 为主
在src文件夹下创建static文件夹用于存放图片等静态资源
.css .eot .svg .ttf .woff这五个文件有用
在static文件夹下创建iconfont文件夹,把这五个放进去。
.css文件内有四个iconfont开头的前面需要加上./相对路径,中间的base64文件不需要加相对路径,将后面几个没用的class删掉,只剩下两大段(@font-size和.iconfont这两部分),为了全局生效,把.css文件改为.js文件,最上面引入injectGlobal,import { injectGlobal } from “styled-components”;并把这两块包含在injectGlobal`…`中。在入口文件index.js中import引入这个iconfont.js文件
在组件的中插入iconfont图标<i className=“iconfont”>&16进制;</i>
一个reducer.js存放太多数据可能造成代码的不可维护,把一个reducer拆成多个reducer再进行整合。和header相关的数据放在header中去,在header文件夹下创建一个store文件夹,再创建一个reducer.js
在总的store文件夹下reducer.js内这样写
import { combineReducers } from 'redux'; //combineReducers用于合并reducer
import headerReducer from '../common/header/store/reducer'; //header/store文件夹下导出
const reducer = combineReducers({
header: headerReducer //在需要使用的地方要使用state.header.focused (focused是headerReducer里的数据)
})
export default reducer;
header文件夹下的index.js
const mapStateToProps = (state) => {
return {
focused: state.header.focused; //mapStateToProps取数据时多加一层地址
}
}
在总的reducer.js内引入小的reducer.js时路径会非常的长,在header下的store下创建一个index.js,相当于header的store部分的一个出口文件,引入同目录下的reducer.js。也就是总的store/reduce.js通过引用header/store/index.js间接地引用header里的reducer.js
header/store/index.js
import reducer from './reducer.js';
export { reducer };
在总的reducer.js内部里,就可以这样地址简化了
import { reducer as headerReducer } from '../common/header/store';
将Header组件需要所有的action放在header/store下的actionCreators.js中集中写
在header/store/index.js
import * as actionCreators from './store/actionCreators'; //注意:这里没有花括弧,export引出import {... as ...}引入,但是用*的话没有花括号
//使用时可以直接把需要的action派发出去
dispatch(actionCreators.searchFocus());
把action需要的常量字符串都放在header/store/contants.js(或者命名为actionTypes.js)中,在actionCreators.js和reducer.js中都引入
header/store/actionCreators.js
import * from contants from './contants';
在header文件夹下store下的index.js中引入同目录下的actionCreator.js和contants.js,让index.js成为一个出口文件
header/store/index.js
import reducer from './reducer';
import * as actionCreators from './actionCreators';
import * as contants from './contants';
export { reducer, actionCreator, contants };
在header/index.js文件中,把引入actionCreators这里换一下
//之前是
import * as actionCreators from './store/actionCreators';
//现在是
import { actionCreators } from './store'; //获取store里找index.js这个文件
header/store/reducer.js内部state是不允许修改的,要用原始的state表达方法,要时刻警惕state不被修改,会存在风险,所以要变成immutable对象管理store中的数据
immutable.js是一个第三方模块,可以生成一个immutable对象,
//安装
yarn add immutable
header/store/reducer.js
import * as constants from './constants'; import { fromJS } from 'immutable'; //提供一个fromJS方法,把js对象转化为一个immutable对象 const defaultState = fromJS{( focused: false; }); export default (state=defaultState, action) = { if(action.type == 'constants.SEARCH_FOCUS') { //之前返回js对象的时候这样写 //return { // focused: true //} //现在上面通过formJS已经把focused改为immutable对象了,所以要改数据的时候不能用原来的方式了,必须要返回immutable对象 return state.set('focused', true); //这里不是修改了原本的state,immutable对象的set方法,会结合之前immutable对象的值和设置的值返回一个全新的对象 } if() { } return state; }
header/index.js
const mapStateToProps = (state) => {
return {
//因为获取到的state数据现在是immutable对象,所以.的方式获取数据就不行了
//focused: state.header.focused;
//要这样写,用get方法获取
focused: state.header.get('focused');
}
}
上面既使用了.又使用了get,这样写格式不统一,所以将总的reducer.js的state也换成immutable对象
//安装
yarn add redux-immutable;
在总的store文件夹下reducer.js内这样写
import { combineReducers } from 'redux-immutable'; //combineReducers从redux-immutable中引入
import headerReducer from '../common/header/store/reducer';
const reducer = combineReducers({
header: headerReducer //此时combineReducers里面的内容就是immutable形式
})
export default reducer;
header/index.js
const mapStateToProps = (state) => {
return {
//因为state也是immutable对象了,所以之前这么写就报错了
//focused: state.header.get('focused');
//改为这样写
//focused: state.get('header').get('focused');
//简写为
focused: state.getIn(['header', 'focused']);
}
}
如果想要添加模拟的数据文件,在public文件夹下创建api文件夹,放在这里。可以在网址栏通过输入对应路径进行查看
一个函数内部可以由多个dispatch(action),都会派发
项目中使用Redux-thunk发送Ajax数据,获取到数据之后存到store里,再在页面展示
header/store/reducer.js
const defaultState = fromJS({
focused: false,
list: [] //Ajax获取数据存在list中
})
header/store/reducer.js内
return state.set('list', action.data);
//上面list是immutable数组,必须把action.data是axios获取到的数据,必须也变成immutable数组
在header/store/actionCreator.js内部axios获取data的时候也要变为immutable形式的
import { fromJS } from 'immutable'; const changeList = (data) => ({ type: constants.CHANGE_LIST, data: fromJS(data), //变为immutable对象 totalPage: Math.ceil(data.length / 10) //点击换一批,换页 }) export const getList = () => { return (dispatch) => { axios.get('/api/headerList.json').then((res) => { const data = res.data; dispatch(changeList(data.data)); }).catch(() => { console.log('error'); }) } }
immutable形式的数据也有map方法
this.props.list.map((item) => {
return <SearchItem key={item}>{item}</SearchItem>
})
header/store/reducer.js内部可以用switch精简繁多的if代码
export default (state = defaultState, action) => {
switch (action.type) {
case constants.SEARCH_FOCUS :
//return state.set('focused', true).set('totalPage', action.totalPage); //每一项这里本来应该都有一个break,因为这里用的是return,所以到这里就return出去了,后面代码也就不会执行了
//上面这句和这一句等价
return state.merge({
focused: true,
totalPage: action.totalPage
})
case ...
default:
return state;
}
}
换一批的实现,数据的处理方式(7-14)和图标点击旋转的实现(7-15)
异步请求问题(7-16)
一个immutable数组list转化为普通数组jsList
jsList = list.toJS();
代码简化
if(list.length === 0) {
dispatch(actionCreators.getList());
}
//可以简化为
(list.length === 0)&&dispatch(actionCreators.getList());
React和Vue都是SPA(single page app)单页面应用,应用只有一个完整的页面(.html),点击页面中的链接不会刷新页面,只会做页面的局部刷新,数据都需要通过ajax请求获取,并在前端异步展现。
前端路由就是点击页面某处,地址栏的地址变化,被捕捉到然后将对应的组件展现到页面上。利用的是BOM的history。
基本使用:
1.明确好界面的导航区、展示区
2.导航区的a标签改为Link标签
3.展示区写Route标签进行路径的匹配
4.<App />的最外侧包裹一个<BrowserRouter>或<HashRouter>
//安装
yarn add react-router-dom
在总的App.js里
import { BrowserRouter, Route, Switch } from 'react-router-dom'; //BrowserRouter代表的是路由, Route代表的是一个一个的路由规则 //react-router-dom中的Router有两种,一种是BrowserRouter,一种是HashRouter(地址栏的路径是中带#的,localhost:3000/#/home,#后面的是哈希值,特点是#后面的哈希值不会作为资源发送给服务器的),如果用HashRouter的话,只是地址栏路径多个#/,别的路由设置和BrowserRouter一样 class App extends Component { render() { return { <Provider store={store}> <div> //Provider标签里的元素都要放在一个标签下 <Header /> //Header组件是公共组件,在哪个路由中都需要 <BrowserRouter> //这里面的内容要使用路由了 //BrowserRouter标签里的元素都要放在一个标签下 <Switch> //匹配到一个路由后不会继续往下匹配了,如果不用Switch,匹配一个路由后还会继续匹配下一个路由,所以多个路由的时候再Switch包裹起来,一个路由的时候不用Switch //注册路由 <Route Path='/' exact render{() => <div>home</div>}></Route> //render()函数作为属性,表示要渲染什么 //路径Path的值只要输入的url包含,就会展示该路由效果,例如localhost:3000/detail首页和详情页的内容都会展示出来,exact的意思是路径不能包含,只能完全一致。其实exact是exact={true}的缩写,叫严格匹配,不使用exact叫模糊匹配。而且模糊匹配只能是应该匹配到的放在最前面,放在中间也不行。 //使用exact的原则是如果不使用也不耽误页面的呈现,那就不开启严格匹配,如果不开启严格会引发一些问题,再开启。因为有些时候开启严格匹配的话导致无法继续匹配二级路由。 <Route Path='/detail' exact render{() => <div>detail</div>}></Route> </Switch> </BroeserRouter> </div> </Provider> } } }
在src目录下创建pages文件夹,代表这个项目有多少页面,里面创建home和detail文件夹,代表有两个页面,里面分别创建index.js,里面创建组件
在总的App.js中引入
import Home from './pages/home';
//路由这样写
<Route path='/' exact component={Home}></ Route> //访问这个url,展现home组件
index.jsx
//防止第一次加载页面的时候所有组件都直接加载过来,组件太多容易卡顿,可以用组件的懒加载 import React, { Component, lazy, Suspense} from 'react' ... //之前是这样的 //import Home from './Home' const Home = lazy(() => import('./Home')) //lazy函数里面要用函数形式 //Suspense需要用的组件这里不能用懒加载,必须要用的时候早期就已经加载回来了 import Loading from './Loading' //注册路由这里要用Suspense包裹 //必须添加Suspense,用于当懒加载的组件还没有返回的时候,页面展示的内容,可以有两种方式,如下。 //<Suspense fallback={<h2>加载中...</h2>} <Suspense fallback={<loading />} //注册路由 <Route path="/about" component={About} /> <Route path="/home" component={Home} /> </Suspense>
1.底层原理不一样:
BrowserRouter使用的是H5的history API(这个history不是this.props.history,这个history是React的封装,BrowserRouter使用的是H5自带的一个,React对它进行二次封装),不兼容IE9及以下版本。
HashRouter使用的是URL的哈希值。
2.path表现形式不一样
BrowserRouter的路径中没有#,例如:localhost:3000/demo/test
HashRouter的路径包含#,例如:localhost:3000/#/demo/test
3.刷新后对路由state参数的影响
BrowserRouter没有任何影响,因为state保存在history对象中。
HashRouter刷新后会导致路由state参数的丢失。因为它没有使用history这个API
4.HashRouter可以用于解决一些路径错误相关的问题,例如样式丢失问题。
路由组件和一般组件
1.写法不同
这里的Home组件叫路由组件,需要靠路由匹配上才展示的;正常使用<Home />这种样式的组件叫做一般组件
2.存放位置不同
路由组件不应该放在src/components文件夹下,应该放在src/pages文件夹下
3.接收到的props不同(路由组件和一般组件最大的区别)
一般组件写组件标签时传递了什么就能收到什么,如果属性不传值,this.props为空对象;而路由组件会收到路由器给传递的三个最重要的porps信息,分别为history(go goBack goForward push replace)
location(pathname search state)
match(params path url)
把Home页面拆分为多个组件,home文件夹下有个大的index.js和style.js和一个compoments文件夹,component文件夹下有多个组件文件List.js Topic.js等,将这些小组件引入到Home下的index.js里进行显示。这些小组件功能很少很单一,避免过度设计,将样式直接在Home下的style.js下设计就行
Home文件夹下创建store文件夹,下面有index.js和red1ucer.js,将这个小的reducer.js引入到同目录下的index.js内,使得出口文件都是index.js,再由index.js传到最外层的大的reducer.js内
最外层的reducer.js
import { reducer as homeReducer } from '../pages/home/store'; //路径直接到这里就行,会自动找到store文件夹下的index.js这个出口文件
const reducer = combineReducers({
header: headerReducer,
home: homeReducer
});
export default reducer;
每当点击一个路由的时候,都从最开始注册的路由重新匹配,继续到嵌套路由中进行组件内部下一级路由的匹配
localhost:3000/home/news
App.jsx
<Route path="/home" component={Home} />
<Route path="/detail" component={Detail} />
<Redict to="/home" />
//如果这里用严格模式exact就不能继续匹配二级路由了,因为没匹配上
src/pages/Home/index.jsx(Home文件夹下除了index.jsx,还有News和Messages文件夹是Home组件的子组件)
<MyLink to="/home/news">News</MyLink>
<MyLink to="/home/messages">Messages</MyLink>
//注册路由
<Route path="/home/news" component={News} />
<Route path="/home/messages" component={Messages} />
<Redict to="/home/news" /> //重定向,当地址只是localhost:3000/home时,上个代码框内的路由匹配后,到这里,/home/news和/home/messages都不匹配,直接重定向
图片地址的组件–style-componnts的一个语法
组件Recommend.js中
<RecommendItem imgUrl="地址" />
style.js中
export const RecommendItem = styled.div`
//其他样式
background: url(${(props) => props.imgUrl});
//${}是多行字符串嵌套的表达式的写法,里面可以写一个函数,接收props这个参数,可以通过props.imgUrl使用上面传递过来的url
`;
JSON格式
键值对除了数字和布尔值,都需要双引号包裹,例如字符串
异步获取不同的文件
axios.get('/api/homeList.json?page=' + page).then((res) =>{...});
//page为变量
执行函数直接将页面返回顶部
handleScrollTop () {
window.scrollTo(0, 0);
}
附:
//一个滚动效果的容器,想要向上滚动30px
const list = document.getElementByClassName('list')[0]
list.scrollTop = 30px
//容器的整体高度(包含看不到的部分)
list.scrollHeight
window监听滚动
window.addEventListener('scroll', this.props.函数名);
当前距离页面顶部的距离(滚动页面的话值会越来越大)
document.documentElemnt.scrollTop
对于window上操作的事件方法,页面移除时也一定要在window上移除
componentWillUnmount() {
window.removeEventListener('scroll', this.props.函数名);
}
对于多个组件的connect和store进行连接,一旦store内部修改了,那么每个组件都会重新渲染,也就是每个组件的render都会重新执行,解决这个问题可以使用之前的shouldComponentUpdate,react内部有PureComponent解决这个问题,PureComponent底层自己有shouldComponentUpdate。只要把之前的引入包括使用Component的地方都改为PureComponent,项目中一般使用PureComponent来优化。
import React, { PureComponent } from 'react';
因为使用了immutable.js这个框架,PureComponent和immutable.js能完美结合,但是如果不使用immutable.js就使用PureComponent的话,那么会遇到一些底层问题的坑,所以如果不使用immutable.js,最好自己写一个shouldCompomentUpdate来进行代码优化,不要使用PureComponent
export default Class Parent extends PureComponent {
state = {carName: '奔驰'}
changeCar = () => {
//this.setState({carName: '迈巴赫'}) //这里的{carName: '迈巴赫'}和上面的{carName: '奔驰'}是两个对象
const obj = this.state
obj.carName = '迈巴赫'
setState(obj) //这里的obj和上面的{carName: '奔驰'}是一个对象,只是引用不同,PureComponent底层做了一个浅对比,不去看对象里面的东西变没变,只去分析这个obj是不是原来的对象,如果是的话组件就不会更新了,这也是数组不应该用push、unshift等方法来改变获得新数组的原因。
}
}
单页面应用,只加载一次html文件,优点是当从首页里点击到详情页的时候也不会出现页面加载卡顿的情况
单页面应用就不能用<a href=‘/detail’>…</a>,这种,每次跳转到详情页就会重新发送请求
要使用Link标签,其实该标签在页面上展示的话会转化为a标签,to属性会变为href属性
import { Link } from 'react-router-dom';
//用Link代替a标签,href改为to
//React靠路由链接实现切换组件--编写路由链接
<Link to='/detail'>...</Link>
注意:使用Link标签的组件必须放在总的App.js文件的<BrowserRouter></BrowserRouter>内部,<BrowserRouter>相当于一个路由器,一个页面只能使用一个路由器,使用多个的话没办法沟通,所以直接把整个<App />组件包裹起来
index.js
import {BrowserRouter} from 'react-router-dom'
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
)
使用bootstrap.css,如果点击Link就改变样式,相当于点击后增加一个active类,此时就不能用Link了,要用NavLink标签
import { NavLink } from 'react-router-dom';
<NavLink activeClassName="active" className="list-group-item" to="/about">About</NavLink>
<NavLink className="list-group-item" to="/about">About</NavLink>
//activeClassName="active"点击标签后会增加这个属性,如果不写默认值就是active,所以上下两句是一样的,也可以选择点击后增加其他类属性,
<NavLink activeClassName="lala" className="list-group-item" to="/about">About</NavLink>
//bootstrap的样式权重有点高,自己加样式后点击可能出现页面样式闪烁的小bug,给自己的样式加上!important就可以解决了
MyNavLink.js一般标签
//如果有很多NavLink的话,出了to属性内容和标签体About内容不一样,其他都一样,太冗余了,可以封装一个自己的标签叫MyNavLink //一个好的封装要和想要封装的标签的样式一致,比如NavLink标签是含有标签体的,那么咱们封装的标签页也最好不是自闭合标签,也是要有标签体的 //标签体也是属于标签属性的,props属性是children,值为标签体,就是this.props.children就可以拿到标签体 import React, {Component} from 'react' import {NavLink} from 'react-router-dom' export default class MyNavLink extends Component { render() { const {children} = this.props return ( <NavLink activeClassName="lala" className="list-group-item" {...this.props} /> //就不用像下面这样写标签体了,{...this.props}已经包含to="/about" a={1} b={2}这些属性以及children属性了 //<NavLink activeClassName="lala" className="list-group-item" {...this.props}>{this.props.children}</NavLink> ) } }
App.jsx
<MyNavLik to="/about" a={1} b={2}>About</MyNavLik>
<MyNavLik to="/home">Home</MyNavLik>
//1路由链接(携带参数),和Nodejs的express的params参数一样
<Link to={`/home/messages/${mesgObj.id}/${mesgObj.title}`}>Messages</Link>
//2注册路由(声明接收)
<Route path="/home/news/:id/:title" component={News} /> //News组件内部this.props就能接收传递过来的id和title
//在News组件中接收this.props.match.params.id,就能获取id值
附:数组的find方法
News组件的index.jsx
const DetailData = [
{id:'01', content: 'nihao'},
{id:'02', content: 'nio'},
{id:'03', content: 'niha'},
]
//3接收参数
const {id, title} = this.props.match.params
const findResult = DetailData.find((detailObj) => { //detailObj是数组中的每一项,findResult匹配的该项
return detailObj.id === id
})
//1路由链接(携带参数),和nodejs的express的query参数一样
<Link to={`/home/messages/?id=${mesgObj.id}&title=${mesgObj.title}`}>Messages</Link>
//2注册路由(search参数无需声明接收,正常注册路由即可)
<Route path="/home/news" component={News} />
News组件的index.jsx
import qs from 'querystring' //一般都叫做qs
//key1=value1&key2=value2, name=tom&age=18,这种编码形式叫做urlencoded编码
let obj = {
name: 'tom',
age: 18
}
qs.stringify(obj) //name=tom&age=18
let str = 'name=tom&age=18'
qs.parse(str) //{name: "tom", age: "18"}
//3接收参数
//this.props.location.search //?id=01&title='haha',是一个urlencoded编码字符串,需要借助querystring解析
const {search} = this.props.location
const {id, title} = qs.parse(search.slice(1)) //search.slice(1)把前面的?去掉
//1路由链接(传递的参数不在地址栏中显示,Link标签的to属性的值不是字符串了,而是对象)
//即使地址栏是localhost:3000/home/news,没有参数,刷新的话,也不会丢掉参数,因为一直在用BrowserRouter,它一直在维护浏览器的API,history.xxx
<Link to={{pathname: '/home/news', state: {id: mesObj.id, title: mesObj.title}}}>Messages</Link>
//其实params和search的to这里也可以写成对象,就是把字符串作为pathname的值就可以了,不过完全没有必要变得那么麻烦,所以就以字符串的形式就可以
//2注册路由(state参数无需声明接收,正常注册路由即可)
<Route path="/home/news" component={News} />
News组件的index.jsx
//3接收参数
//this.props.location.state //{id: mesObj.id, title: mesObj.title}
const {id, title} = this.props.location.state || {} //如果不写这个||{},清理浏览器缓存后再在地址栏输入localhost:3000/home/news查找出来的id、title是undefined而报错
路由有两种模式,默认的是push模式,就是每次页面的点击,都相当于在栈中进行记录(A=>B=>C),点击浏览器回退按钮,页面依次回退并把栈顶弹出(放在另一个栈中记录用于之后的前进一步);而replace模式,是你点击页面,页面跳转之后,把这条记录直接替换掉之前的栈顶记录(A=>B=>C,再点击D后,栈中为A=>B=>D,下一次单击回退时退回到B中),所以,假如项目中全是replace模式,那么浏览器的前进和后退图标就一直是灰色的
//开启replace模式
<Link replace to={`/home/messages/${mesgObj.id}/${mesgObj.title}`}>Messages</Link>
//其全写方式为下面这样,和严格匹配exact一样
<Link replace={true} to={`/home/messages/${mesgObj.id}/${mesgObj.title}`}>Messages</Link>
其他标签也可以像Link标签一样进行路由跳转
用原生属性,例如this.props.history.push(path, state) this.props.history.replace(path, state)
回退this.props.history.goBack() 前进this.props.history.goForward() this.props.history.go(n) n为正整数,就前进n步,n为负整数,就后退n步
附:获取当前路由路径的方式有this.props.location.pathname(比如"/about")this.props.match.path this.props.match.url
只有路由组件才有history location match,就是上面的这些
Header/index.jsx
import React, {Component} from 'react' import {withRouter} from 'react-router-dom' //withRouter是一个函数 class Header extends Component { back = () => { this.props.history.goBack() //可以使用路由组件的history了 } render() { return ( <div> <h2>Header</h2> <button onClick={this.back}>回退</button> </div> ) } } export default withRouter(Header) //withRouter能够接收一般组件,然后就在一般组件的身上加上路由组件所特有的history location match,withRouter的返回值是一个新组件
第一种方式:
动态路由
根据路由参数的不同获取不同页面的信息
List.js组件
... //外层是一个循环
<Link to={'/detail/' + item.get('id')}></ Link> //网页为localhost:3000/detail/3
App.js
<Route path='/detail/:id' exact component={Detail} />
//访问detail路径还要额外传递一个id
在跳转到的页面的Detail组件的index.js中可以通过this.props.match.params.id获取这个id值
index.js–再异步获取数据
componentDidMount({
this.props.getDetail(this.props.match.params.id);
}
const mapDispatch = (dispatch) => ({
getDetail(id) { //把id传递过去
dispatch(actionCreators.getDetail(id))
}
})
actionCreators.js–Detail组件的
axios.get('/api/detail.json?id=' + id).then() //把id传递给后端
第二种实现方式(不推荐)
需要手动解析参数值
List.js组件
... //外层是一个循环
<Link to={'/detail?id=' + item.get('id')}></ Link>
//?id=这种形式,网页为localhost:3000/detail?id=4
App.js
<Route path='/detail' exact component={Detail} />
//访问detail路径就是之前的样子就能匹配的上
在跳转到的页面的Detail组件的index.js中可以通过this.props.location.search获取到?id=2,需要手动提取这个参数2,之后给后端传递参数就和第一种方式一样了
innerRef
使用ref获取数据时如果为styledComponent时是没有办法拿到内容的,将ref改为innerRef就可以获取标签了
在登录页面需要把账号和密码给后台
index.js–Login组件
<Input placehoder='账号' innerRef={(input) => {this.account = input}} />
//this.account就是真实的dom元素
<Input placehoder='密码' type='password' innerRef={(input) => {this.password = input}} />
<Button onClick={() => this.props.login(this.account, this.password)}>登录</Button>
const mapDispatch = (dispatch) => ({
login(accountElem, passwordElem) { //账号和密码为accountElem.value、passwordElem.value
dispatch(actionCreators.login(accountElem.value, passwordElem.value))
}
})
actionCreators.js
axios.get('api/login.json?account=' + account + '&password=' + password).then()
//应该用post
重定向–react-router-dom的Redirect
登录退出用loginStatus这个布尔值进行设置,最开始登录页面,loginStatus为false,最上面显示登录字样,点击登录字样,跳转到登录页面,输入用户名密码,点击登录按钮,loginStatus变为true,跳转到首页,上面变为退出字样,点击退出,loginStatus为false,上面变为登录字样,点击登录字样,跳转到登录页面
header/index.js
loginStatus ? 退出标签(点击修改loginStatus这个布尔值) : 登录标签(点击跳转到登录页面的Link标签)
login/index.js
import Redirect from 'react-router-dom';
const { loginStatus } = this.props; //loginStatus是布尔值
if(!loginStatus) {
//展示登录页面
}
else{
return <Redirect to='/' /> //重定向到首页
}
异步组件
react-loadable
访问首页时只加载首页的代码,访问其他页面时只加载该页面代码,好处是加载速度快,使用异步组件实现
//安装
yarn add react-loadable
detail文件夹下创建loadable.js文件,下面怎么引入看组件的语法
import React from 'react'; //下面使用了JSX语法
import Loadable from 'react-loadable';
const LoadableComponemt = Loadable({
loader: () => import(./) //异步加载当前页面的index.js,也就是detail文件夹下的index.js
loading() { //加载时显示这个
return <div>正在加载</div>
}
});
export default () => <LoadCompoment />
在最外层的App.js中引入
//没改之前是
import Deatil from './pages/detail'
//改之后是
import Deatil from './pages/detail/loadable.js'
通过上面的操作就把detail组件变成了异步组件
但是这样detail下的index.js就没办法直接获取路由的内容了,this.props.match.params.id就会报错,因为App.js中引入的loadable.js不是Route路由标签直接对应的组件,可以这样解决
react-router-dom的withRouter
detail/index.js中引入
import { withRouter } from 'react-router-dom';
export default connect(mapState, mapDispatch)(withRouter(Detail));
//意思是让Detail组件有能力获取Route标签里的所有的参数和内容
附:Chrome浏览器的FeHelper插件自动帮我们整理json格式的文件
可以用XAMPP进行后端的数据模拟
npm run build
生成的build文件夹里的文件就是前端项目上线需要的文件,把这些文件复制一份,放在后端服务器生成的api文件夹同级,api内放的是前端代码需要异步获取的各种.json数据文件,此时运行后端接口localhost:8080简写为localhost就可以在浏览器展示页面效果了
第三方的库serve
把你快速搭建一条服务器
npm i serve -g
//你想以哪个文件夹作为根路径,例如使用上面打包后的build文件夹,serve build
serve 文件夹
Hook是React 16.8.0版本增加的新特性/新语法,可以让你在函数组件中使用state以及其他的React特性
State Hook: React.useState()
Effect Hook: React.useEffect()
Ref Hook: React.useRef()
注意:函数组件中this是undefined
State Hook: React.useState() 函数组件中可以使用状态并且可以更新状态
import React from 'react' function Demo() { //Demo组件就如Class形式的render,会调用n+1次,初次渲染调用一次,以后状态每次改变都渲染一次 //const a = React.useState() //React.useState()是个数组,数组中只包含两个元素,第一个就是状态,第二个是用于更新状态的方法 //React.useState(0)状态中count的初始值是0 const [count, setCount] = React.useState(0) //数组的解构赋值 //React底层进行了处理,第一次调用这句的时候,就把count存了下来,等以后Demo重新渲染的时候,这句也执行了,但不会因为再次执行把上一次渲染得到的count值覆盖掉 const [name, setName] = React.useState('tom') //加的回调 function add() { //第一种写法,setXxx(newValue) setCount(count + 1) //count + 1是新返回的newState的值 //第二种写法,里面是一个函数,setXxx(value => newVlaue) //setCount(count => count + 1) } function changeName() { setName('Li') } return ( <div> <h2>当前求和为:{count}</h2> <h2>我的名字是:{name}</h2> <button onClick={add}>点我+1</button> <button onClick={changeName}>点我改名</button> </div> ) } export default Demo
Effect Hook: React.useEffect() 可以在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子),React中的副作用操作:发ajax请求数据获取;设置订阅/启动定时器;手动更改真实DOM
import React from 'react' import ReactDOM from 'react-dom' function Demo() { const [count, setCount] = React.useState(0) //当参数只是一个函数,在组件挂载和更新的时候都执行,自此可以执行任何带副作用操作。当状态改变的时候,React底层就会帮你调用这个函数,监测所有的状态的改变。相当于componentDidMount和componentDidUpdate。 //React.useEffect(() => { // console.log('@@@') //}) //没传第二个参数相当于监测所有状态,第二个参数是空数组[],那就是谁也不监测,只在组件挂载的时候执行一次第一个参数那个函数,相当于componentDidMount //React.useEffect(() => { // setInterVal(() => { // setCount(count + 1) // }, 1000) //}, []) React.useEffect(() => { let timer = setInterVal(() => { setCount(count + 1) }, 1000) return () => { //React.useEffect第一个参数返回时的函数相当于componentWillUnmount,可以在这里清除定时器/取消订阅等 clearInterval(timer) } }, []) //数组中有状态,如这里,相当于只监测count。相当于conponentDidMount和监测count值。监测多个值[count, name] //React.useEffect(() => { // console.log('@@@') //}, [count]) function add() { setCount(count + 1) } //卸载组件的回调,组件卸载了必须把定时器也关掉,否则会报错 function unmount() { ReactDOM.unmountComponentAtNode(document.getElementById('root')) } return ( <div> <h2>当前求和为:{count}</h2> <button onClick={add}>点我+1</button> <button onClick={unmount}>卸载组件</button> </div> ) } export default Demo
Ref Hook: React.useRef() 可以在函数组件中存储/查找组件内的标签或者其他任意数据
import React from 'react' function Demo() { //类组件中使用ref的对象的方式用的是React.createRef(),这里其他语法一样,也是“专人专用” const myRef = React.useRef() //提示输入的回调 show() { alert(myRef.current.value) //和类组件一样要用.current获取数据 } return ( <div> <input type="text" ref="myRef" /> {/*这里用ref对象的方式写的*/} <button onClick={show}>点我提示数据</button> </div> ) } export default Demo
useMemo
useMemo是针对一个函数,是否多次执行
useMemo主要用来解决使用React hooks产生的无用渲染的性能问题
在方法函数,由于不能使用shouldComponentUpdate处理性能问题,react hooks新增了useMemo
useMemo使用
如果useMemo(fn, arr)第二个参数匹配,并且其值发生改变,才会多次执行执行,否则只执行一次,如果为空数组[],fn只执行一次
举例说明:
第一次进来时,控制台显示rich child,当无限点击按钮时,控制台不会打印rich child。但是当改变props.name为props.isChild时,每点击一次按钮,控制台就会打印一次rich child
export default () => { let [isChild, setChild] = useState(false); return ( <div> <Child isChild={isChild} name="child" /> <button onClick={() => setChild(!isChild)}>改变Child</button> </div> ); } let Child = (props) => { let getRichChild = () => { console.log('rich child'); return 'rich child'; } let richChild = useMemo(() => { //执行相应的函数 return getRichChild(); }, [props.name]); return ( <div> isChild: {props.isChild ? 'true' : 'false'}<br /> {richChild} </div> ); }
组件如果不写成自闭合标签,那么标签体的内容要通过this.props.children拿到,相当于组件标签的children属性
//在A的父组件中 <A>hello</A> //在A组件中。这种叫做childrenProps,通过组件标签传入结构,但如果子组件想要获取父组件里的数据就没办法做到,要用到下面的,叫renderProps。他俩都是React中向组件内部动态传入带内容的结构/标签。Vue中使用slot插槽技术,也就是通过组件标签体传入结构<A><B/><A/> {this.props.children} //hello //有两种方式成为父子组件 //一种就是上面的那种 //在A的父组件中 <A> <B /> </A> //在A组件中通过{this.props.children}展示B组件的内容 //第二种就是普通的情况,在A的父组件中使用<A />,在A组件中使用<B /> //使用第一种方式,A组件如果想要给B组件传递数据。这种叫做renderProps,通过组件标签属性传入结构,而且可以携带数据,一般用render函数属性 import React, { Component } from 'react' export default class Parent extends Component { render() { return ( <div> <h3>我是Parent组件</h3> {/*这种方式如果没有下面这三行,单看A和B组件看不出来其关系。往下看 <A> <B /> </A> */} {/*给A标签添加一个render属性,属性值为一个函数,函数返回值为B组件,就可以给B组件传值了。传什么值在A组件中设置。叫renderProps的原因是这个render不是组件中的render,而是标签属性里的render,是给A的props里添加属性render,这个render名字不是必须交render,叫其他也可以,但一般都叫render,职场习惯,*/} <A render={(name) => <B name={name}/>}/> </div> ) } } class A extends Component { state = {name: 'tom'} render() { //解构赋值 const {name} = this.state return ( <div> <h3>我是A组件</h3> {/*这里传递给B值。这种方式优点在于,这里预留一个位置,这个位置放哪个组件先不确定,通过上面A标签中返回的是哪个组件,这里就展示哪个组件,这就是Vue中的插槽技术,<A><B/><A/>*/} {this.props.render(name)} </div> ) } } class B extends Component { render() { return ( <div> {/*B标签就收到了A组件传递的name值*/} <h3>我是B组件,{this.props.name}</h3> </div> ) } }
不要让一个子组件的错误(例如子组件返回的服务器端的数据不是想要的格式)导致整个页面都出不来,边界的意思就是把错误控制在一定范围之内,用错误边界,需要在容易发生错误的组件的父组件内部设置
注意:只能捕获后代组件的生命周期产生的错误,不能捕获自己组件产生的错误和其他组件在合成时间、定时器中产生的错误;只适用于生产环境,不适用于开发环境
Parent.jsx
import React, { Component } from 'react' import Child from './Child' export default class Parent extends Component { state = { hasError: '' //用于表示子组件是否产生错误 } //如果Parent的子组件出现了任何的报错,都会执行这个钩子,并携带错误信息 static getDerivedStateFromError(error) { return {hasError: error} } //加上这个更加详细 componentDidCatch() { //组件在渲染的整个过程中,由于子组件出现问题引发错误,就会调用这个钩子 console.log('统计错误次数,反馈给服务器,用于通知编码人员进行bug的解决') } render() { return( <div> <h2>我是Parent组件</h2> {this.state.hasError ? <h2>当前网络不稳定,稍后再试</h2> : <Child />} </div> ) } }
通讯方式:
1.props
children props和render props
2.消息订阅-发布
有很多库,例如pubs-sub(用于js)、event(主要在C#中应用)等
3.集中式管理
redux、dva等
4.conTexts生产者-消费者模式
组件间关系及与通讯方式比较好的搭配:
父子组件:props
兄弟组件(非嵌套组件):消息订阅-发布、集中式管理
祖孙组件(跨级组件):消息订阅-发布、集中式管理、conText(开发用的少,封装插件用的多)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。