react基础
jsx语法规则:
1.定义虚拟DOM时,不要写引号。<br /> 2.标签中混入JS表达式时要用{}。表达式非(if,for,switch语句)<br /> 3.标签的类名指定不要用class,要用className。<br /> 4.内联样式,要用style={{key:value}}的形式去写style={{color:'red',fontSize:'50px'}}去掉短线采用驼峰命名。<br /> 5.只有一个根标签,对于自结束标签在后面主动加上/<br /> 6.标签必须闭合<br /> 7.标签首字母<br /> (1).若小写字母开头,则将该标签转为html中同名元素,若html中无该标签对应的同名元素,则报错。<br /> (2).若大写字母开头,react就去渲染对应的组件,若组件没有定义,则报错。 <br /> 8.组件标签首字母一定要大写 <br />1) 浏览器不能直接解析JSX代码, 需要babel转译为纯JS的代码才能运行<br />2) 只要用了JSX,都要加上type="text/babel", 声明需要babel来处理<br />安装开发者工具调试
- 组件名必须首字母大写
2. 虚拟DOM元素只能有一个根元素
3. 虚拟DOM元素必须有结束标签
- React内部会创建组件实例对象
2. 调用render()得到虚拟DOM, 并解析为真实DOM
3. 插入到指定的页面元素内部
创建类式组件
class MyComponent extends React.Component{
//这里没有属于自己的属性和方法 不用写构造器和super
render() {
//render是放在MyComponent的原型对象上,供实例使用。
//render中的this是谁?——MyComponent的实例对象 <=> MyComponent组件实例对象。
console.log(‘render中的this:’,this);
return
我是用类定义的组件(适用于【复杂组件】的定义)
}
}
ReactDOM.render(
// 执行了ReactDOM.render(
// react解析组件标签,找到了MyComponent组件对象。发现该组件是用类定义的,随后new出来该类的实例,并通过该实例去调用原型上的render方法。将render返回的虚拟DOM转为真实DOM,随后呈现在页面中。
class WeatherextendsReact.Component {
//构造器调用几次? ———— 1次
constructor(props) {
super(props)
this.state = {isHot:false, wind:’微风’}//往组件实例对象上放东西 也是初始化状态
this.changeWeather = this.changeWeather.bind(this)//解决changeWeather中this指向问题,它的this是undefined 用bind方法将this改为组件实例 然后bind返回一个新的函数 用新的changeWeather接收放在实例对象上
}
//render调用几次? ———— 1+n次 1是初始化的那次 n是状态更新的次数
render() {
console.log(‘render’)
//读取状态
const {isHot, wind} = this.state//解构赋值 为了更方便读取
//从组件实例对象上读出来 因为系统自动创建实例 所以this是组件实例
return
}
//changeWeather调用几次? ———— 点几次调几次
changeWeather() {
//changeWeather放在哪里? ———— Weather的原型对象上,供实例使用
//由于changeWeather是作为onClick的回调(直接让onClick也指向这个函数 最后触发事件后通过onClick()这样调用),所以不是通过实例调用的,是直接调用 this是window
//但类中的方法默认开启了局部的严格模式,所以changeWeather中的this为undefined
//获取原来的isHot值
const isHot = this.state.isHot
//严重注意:状态必须通过setState进行更新,且更新是一种合并,不是替换。
this.setState({isHot: !isHot})
console.log(this);
//严重注意:状态(state)不可直接更改,下面这行就是直接更改!!!
//this.state.isHot = !isHot //这是错误的写法
}
}
//2.渲染组件到页面
ReactDOM.render(
- 组件中render方法中的this为组件实例对象
2. 组件自定义的方法中this为undefined,如何解决?
a) 强制绑定this: 通过函数对象的bind()
b) 箭头函数
3. 状态数据,不能直接修改或更新 用setState
1. 每个组件对象都会有props(properties的简写)属性
2. 组件标签的所有属性都保存在props中
- props里面的属性是只读的不能修改
构造器是否接收props,是否传递给super,取决于:是否希望在构造器中通过this访问props
扩展属性: 将对象的所有属性通过props传递
class Demo extends React.Component{
//构造器是否接收props,是否传递给super,取决于:是否希望在构造器中通过this访问props
//构造器能不写就不写
constructor(props){
super(props)
console.log(this.props)
}
render(){
补充类的理解
1、类中的构造器不是必须写的,要对实例进行一些初始化的操作时,如添加一些指定属性时才写。如果在Student的构造器中加一行 this.job = ‘程序员’也是可以的,参数列表不需要变,它表示Student缔造的实例对象的工作都是程序员
2、如果A类继承B类,且A类中写了构造器有自己的私有属性,那么A类构造器中super是必须要调用的
3、类中定义的方法都是放在了类的原型对象上,供实例使用 (箭头函数+赋值语句的方法是写在实例对象上的,如同写在构造器里面的方法和属性 。只是赋值语句的属性如同写在构造器里面的固定写法 this.job = ‘程序员’一样)
ES6-类的操作:
类的声明,属性以及构造方法的书写,方法的继承,属性的继承,子类对父类的重写,静态成员(只属于函数对象的方法和属性,实例对象访问不到)
类中的赋值语句直接放在实例对象中
如a = 1直接放在实例对象中 ,如箭头函数
直接将属性写在构造器里面 是写在实例对象上的,将方法写在构造器外面是写在原型上的 ,实例化之后两个不同的实例调用原型上的方法是相同的,调用各自的方法是不同的
写里面,等同于以下代码
function A(){
this.show = function(){}
}
最后输出每个实例对象上面都会有一个show方法
写外面
function A(){…}
A.prototype.show = function(){}
最后输出每个实例对象的__proto上面都会有一个show方法
类里面的赋值语句 如箭头函数和 类似于a = 1这样的写法都是写在实例对象上的
<!DOCTYPE html>
// 声明一个类
class People{
// 实例属性,也叫实例属性
head = 1;
// 私有属性(加_),也叫实例方法
_mouth = 1;
// 静态属性
static nose = 1;
// 原型方法
talk(){
console.log(‘我会说话’);
}
// 私有属性
_eat(){
console.log(‘会吃饭’); //会吃饭
}
// 静态方法
static sing(){
console.log(‘会唱歌’);
}
// 构造函数
constructor(name,sex){
// 公有属性
this.name = name;
this.sex = sex;
// 共有方法 在实例对象上
this.talk = function(){
console.log(‘会走路’);
}
// 调用私有方法
this._eat();
// 调用私有属性
this._mouth;
}
}
let jack = new People(‘捧场王’,’男’);
console.log(jack);
/*
head: 1 //声明在构造函数外
name: undefined //声明在构造函数内
sex: undefined
talk: ƒ ()
_mouth: 1
实例属性/方法
私有属性/方法
公有属性/方法
原型属性
没有继承 私有方法 和 静态属性
*/
// 实例属性
console.log(jack.head,jack.name,jack.sex); // “捧场王” “男”
// 实例方法
jack.talk(); // 会走路
// 原型方法
jack.talk(); // 会走路
// 静态方法
People.nose;
console.log(People.nose); //1
// 静态方法
People.sing();
/*
实例方法(公有方法):
在类的构造函数中,用this绑定的方法,是这个类的实例方法,每个实例化的对象都拥有实例方法
静态方法:
使用static声明,只能由类本身来访问。
原型方法:
直接在类里声明,不需要加关键词,每个实例化出来的对象都可以调用。
私有方法:
使用 _ 声明,可以在构造函数内被使用
*/
事件处理
- 通过onXxx属性指定事件处理函数(注意大小写)
1) React使用的是自定义(合成)事件, 而不是使用的原生DOM事件
2) React中的事件是通过事件委托方式处理的(委托给组件最外层的元素)
2. 通过event.target得到发生事件的DOM元素对象
通过event.target得到发生事件的DOM元素对象 ——————————不要过度使用ref(如果是点击事件之后操作的是本身 就可以不用ref 直接用event,target实现(发生事件的元素正好是你要操作的元素时))
受控组件 页面中输入的dom 放到state中 当用的时候再取出来就叫受控组件 一个ref都没有
只要提到受控,那就是需要对其设置状态
生命周期 重点
1. 初始化阶段: 由ReactDOM.render()触发—-初次渲染
1. constructor()
2. componentWillMount()
3. render()
4. componentDidMount()
2. 更新阶段: 由组件内部this.setSate()或父组件重新render触发
1. shouldComponentUpdate()
2. componentWillUpdate()
3. render()
4. componentDidUpdate()
3. 卸载组件: 由ReactDOM.unmountComponentAtNode()触发
1. componentWillUnmount()
- componentDidMount() =====> 常用,一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
- ** componentWillUnmount() =====>
- 要获取之前设置的state就要在setState({},()=>{})的第二个参数中获取 或者进行设置,也可以在componentDidUpdate中设置
- componentWillReceiveProps(模块将接受新的数据)第一次解析时不执行,当后面的属性(props发生改变)发生改变,就会执行这个钩子函数;
- 在使用componentWillReceiveProps之前 要使用componentDidMount()将初始数据接收一下,否则开始会没有任何的props变化

setState 使用触发的钩子函数
render() ———— 每次组件渲染都会触发
注意:render()中不要调用 setState 方法,会发生死循环。
componentDidUpdate() ———— 组件更新(完成DOM渲染)后触发 setState后render后触发
虽说 setState 很多人说是异步的,但是它本身的执行过程和代码是同步的,只是它在合并数据与钩子函数的调用顺序在更新后无法拿到值,形成了 异步 ,我们可以通过第二个参数拿到更新后的结果。
/callback是可选的回调函数,它在状态更新完毕,界面也更新后(render调用后)才被调用
changeText() {
this.setState({
age: 20
}, () => {
console.log(this.state.age); // 文本已经被改变,得到的是20
});
}
也可以通过钩子函数获取修改后的值
componentDidUpdate() {
console.log(this.state.age);
}
也就是说要获取之前设置的state就要在setState({},()=>{})的第二个参数中获取 或者进行设置,也可以在componentDidUpdate中设置
什么时候“同步”,什么时候“异步”?
一般情况下(常见在生命周期或者是合成事件处理函数中),通过 setState() 方法来更新状态,表现是 异步 的。
state = { count: 1 }
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 1
// 通过 DOM 不能立马获取更新后的值
当执行到 setState 时,React 出于性能考虑,并不会立马执行修改 state ,而是先把当前更新对象以及后续的合并到一起,放在一个更新队列里进行合并操作,这期间并不会影响后续代码执行。且多次调用会合并,以最后一次渲染为主,以此来进行性能优化。
this.setState({
count: this.state.count + 1,
})
this.setState({
count: this.state.count + 2,
})
this.setState({
count: this.state.count + 3,
})
// 最后实现更新的是最后一个代码,前面的都会被覆盖
或者
如果想要实现一个一个实现,可以利用 setState 的第二个参数,第二个参数是一个回调函数,在更新完之后会执行
// setState 有两个参数,第二个参数可以立即拿到更新后的值
this.setState({}, () => {
console.log(‘这个回调函数会在状态更新后立即执行’)
})
还有就是在循环中setState也会使得循环跑完最后只渲染一次,只有最后一个数据被添加进去,如下:(合并数组)
for(let i = 0; i < 3; i++){
this.setState({data:[i,…this.state.data]})
}
最终data中只有一个2
正确的添加方式如果不在循环中只加入一个数据可以如上这样添加,但是如果有循环,则如下:
let data = this.state.data
let data2=[]
for(let i = 0; i < 3; i++){
data2.push(i)
}
data2.push(…data)
this.setState({data:data2})
因为react的state的属性变化是看地址的,我们直接用原来的变量,地址没发生改变 react不会渲染页面,虽然说数据变了。
但是我们如果将要改变的属性提前变化了,再用setState就可以使得页面渲染。
this.setState(() => ({
count: this.state.count + 1
}))
// prevStart 是最近一次的更新的数据
this.setState((prevStart) => ({
count: prevStart.count + 2
}))
this.setState((prevStart) => ({
count: prevStart.count + 3
}))
上面确实可以累加,把累加的结果渲染到页面上,但依然不能改变异步更新的表现形式
如果在 setTimeout/setInterval 或者原生事件(比如点击事件等)的回调中,或者是在 setState 前面遇到 await 后续表现都为 同步 的。
利用定时器
import React, { Component } from ‘react’
export default class App extends Component {
state = {
count: 1,
}
componentDidMount() {
setTimeout(() => {
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 2
})
}
render() {
return
}
}
react中如何修改state值
根据State类型 更新
当状态发生变化时,如何创建新的状态?根据状态的类型,可以分成三种情况:
如果是一般类型 number string boolean
这种情况最简单,直接给要修改的状态赋一个新值即可,如果是数组或者对象就用如下方式
//原state
this.state = {
count: 0,
title : ‘React’,
success:false
}
//改变state
this.setState({
count: 1,
title: ‘bty’,
success: true
})
2、 状态的类型是数组
数组是一个引用,React 执行 diff 算法时比较的是两个引用,而不是引用的对象。所以直接修改原对象,引用值不发生改变的话,React 不会重新渲染。因此,修改状态的数组或对象时,要返回一个新的数组或对象。
(1)增加
如有一个数组类型的状态books,当向books中增加一本书(chinese)时,使用数组的concat方法或ES6的数组扩展语法
// 方法一:将state先赋值给另外的变量,然后使用concat创建新数组
let books = this.state.books;
this.setState({
books: books.concat([‘chinese’])
})
// 方法二:使用preState、concat创建新数组
this.setState(preState => ({
books: preState.books.concat([‘chinese’])
}))
// 方法三:ES6 spread syntax
this.setState(preState => ({
books: […preState.books, ‘chinese’]
})) 合并数组
(2)截取
当从books中截取部分元素作为新状态时,使用数组的slice方法:
数组截取state
// 方法一:将state先赋值给另外的变量,然后使用slice创建新数组
let books = this.state.books;
this.setState({
books: books.slice(1,3)
})
//
// 方法二:使用preState、slice创建新数组
this.setState(preState => ({
books: preState.books.slice(1,3)
}))
(3)条件过滤
当从books中过滤部分元素后,作为新状态时,使用数组的filter方法:
数组过滤state
// 方法一:将state先赋值给另外的变量,然后使用filter创建新数组
var books = this.state.books;
this.setState({
books: books.filter(item => {
return item != ‘React’;
})
})
// 方法二:使用preState、filter创建新数组
this.setState(preState => ({
books: preState.books.filter(item => {
return item != ‘React’;
})
}))
注意:不要使用push、pop、shift、unshift、splice等方法修改数组类型的状态,因为这些方法都是在原数组的基础上修改,而concat、slice、filter会返回一个新的数组。数组的地址会发生变化
但是:
还有就是在循环中setState也会使得循环跑完最后只渲染一次,只有最后一个数据被添加进去,如下:(合并数组)
for(let i = 0; i < 3; i++){
this.setState({data:[i,…this.state.data]})
}
最终data中只有一个2
正确的添加方式如果不在循环中只加入一个数据可以如上这样添加,但是如果有循环,则如下:
let data = this.state.data
let data2=[]
for(let i = 0; i < 3; i++){
data2.push(i)
}
data2.push(…data)
this.setState({data:data2})
因为react的state的属性变化是看地址的,我们直接用原来的变量,地址没发生改变 react不会渲染页面,虽然说数据变了。
但是我们如果将要改变的属性提前变化了,再用setState就可以使得页面渲染。
3、状态的类型是普通对象(不包含字符串、数组)
对象是一个引用,React 执行 diff 算法时比较的是两个引用,而不是引用的对象。所以直接修改原对象,引用值不发生改变的话,React 不会重新渲染。因此,修改状态的数组或对象时,要返回一个新的对象。
使用ES6 的Object.assgin方法
// 方法一:将state先赋值给另外的变量,然后使用Object.assign创建新对象
var owner = this.state.owner;
this.setState({
owner: Object.assign({}, owner, {name: ‘Jason’})
})
// 方法二:使用preState、Object.assign创建新对象
this.setState(preState => ({
owner: Object.assign({}, preState.owner, {name: ‘Jason’})
}))
使用对象扩展语法(object spread properties)
对象改变state
// 方法一:将state先赋值给另外的变量,然后使用对象扩展语法创建新对象
var owner = this.state.owner;
this.setState({
owner: {…owner, name: ‘Jason’}
})
后面的会属性会覆盖前面展开的属性
追加属性: 可以将state中的对象拿出来 给对象追加属性并赋值 最后再把对象放回去
const {owner} = this.state
owner.name = ‘Jason’
this.setState({owner})
// 方法二:使用preState、对象扩展语法创建新对象
this.setState(preState => ({
owner: {…preState.owner, name: ‘Jason’}
}))
ES6补充:
扩展运算符的作用及使用场景
(1)对象扩展运算符
对象的扩展运算符(…)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中。
let bar = { a: 1, b: 2 }; let baz = { …bar }; // { a: 1, b: 2 } 复制代码
上述方法实际上等价于:
let bar = { a: 1, b: 2 }; let baz = Object.assign({}, bar); // { a: 1, b: 2 } 复制代码
Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。(如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性)。
同样,如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。
let bar = {a: 1, b: 2}; let baz = {…bar, …{a:2, b: 4}}; // {a: 2, b: 4} 复制代码
利用上述特性就可以很方便的修改对象的部分属性。
在redux中的reducer函数规定必须是一个纯函数,reducer中的state对象要求不能直接修改,可以通过扩展运算符把修改路径的对象都复制一遍,然后产生一个新的对象返回。将原来的属性用扩展运算符展开,将要替换的属性放在其后面。this.setState({p:{…p,a:5}})修改p里面的a属性。
如果需要进行数组添加 需要用这种方式,不能用unshift 因为这种方式 虽然改变原数组 但是原数组的地址没变,我们需要生成一个新数组,可以用扩展运算符的浅拷贝方式也可以用filter这种不改变原数组但返回新数组的方式。所以用对象字面量的形式创建,不是原来的数组是新的数组 ,利用数组的扩展字符串的合并
const {name} = this.state
this.setState({name:[‘xiaoLiu’,…this.state.name]})
需要注意:扩展运算符对对象实例的拷贝属于浅拷贝。
(2)数组扩展运算符
数组的扩展运算符可以将一个数组转为用逗号分隔的参数序列,且每次只能展开一层数组。
console.log(…[1, 2, 3]) // 1 2 3 console.log(…[1, [2, 3, 4], 5]) // 1 [2, 3, 4] 5 复制代码
下面是数组的扩展运算符的应用:
- 将数组转换为参数序列
function add(x, y) { return x + y; } const numbers = [1, 2]; add(…numbers) // 3 复制代码
- 复制数组
const arr1 = [1, 2]; const arr2 = […arr1]; 复制代码
要记住:扩展运算符(…)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中,这里参数对象是个数组,数组里面的所有对象都是基础数据类型,将所有基础数据类型重新拷贝到新的数组中。
- 合并数组
- 如果想在数组内合并数组,可以这样:
- const arr1 = [‘two’, ‘three’];const arr2 = [‘one’, …arr1, ‘four’, ‘five’];// [“one”, “two”, “three”, “four”, “five”] 复制代码
- 扩展运算符与解构赋值结合起来,用于生成数组
const [first, …rest] = [1, 2, 3, 4, 5];first // 1rest // [2, 3, 4, 5] 复制代码
需要注意:解构赋值+扩展运算符解构出来如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。
const […rest, last] = [1, 2, 3, 4, 5]; // 报错const [first, …rest, last] = [1, 2, 3, 4, 5]; // 报错 复制代码
- 将字符串转为真正的数组
[…’hello’] // [ “h”, “e”, “l”, “l”, “o” ] 复制代码
- 任何 Iterator 接口的对象,都可以用扩展运算符转为真正的数组
比较常见的应用是可以将某些数据结构转为数组:
// arguments对象 function foo() { const args = […arguments]; } 复制代码
用于替换es5中的Array.prototype.slice.call(arguments)写法。
- 使用Math函数获取数组中特定的值
const numbers = [9, 4, 7, 1]; Math.min(…numbers); // 1 Math.max(…numbers); // 9
js高级补充:
原型链:
○ Fun.prototype和fn.proto保存的都是地址值(引用变量),而且地址值是一样的
○ 显式原型属性是在执行函数定义的时候产生的
○ 实例调用方法时找的是隐式原型属性(读取),而添加方法时用的是显式原型属性(添加);互不干扰
○ 函数的prototype属性是在函数定义时自动添加的,默认是一个空的object对象(但object自身不满足)
○ 对象的proto属性是在创建实例对象时自动添加的,默认值为构造函数的prototype属性值
○ 程序员能直接操作的是显式原型,但不能操作隐式原型
○ 所有的实例对象都有proto属性, 它指向的就是原型对象
○ 这样通过proto属性就形成了一个链的结构——>原型链
○ 当查找对象内部的属性/方法时, js引擎自动沿着这个原型链查找
○ 当给对象属性赋值时不会使用原型链, 而只是在当前对象中进行操作
○ 原型链是用来查找对象的属性的
○ 当设置属性时不看原型链obj.test=xxx,属性一般通过构造函数定义在对象自身上(在构造中和在类中用等号赋值的方式),方法一般在原型上。因为方法大多一样,而属性一般不一样
○ 也就是说原型上的属性和方法就是给实例对象来用的,利用的是原型链
○ 函数的prototype属性是在函数定义时自动添加的,默认是一个空的object对象(但object自身不满足),也就是说除了Object自身之外,剩下的所有函数都是Object的实例
○ console.log(Object instanceof Object) 推导:Object.proto== Function.prototype, Function.prototype.proto == Object.prototype(所有函数的原型对象默认都是Object的实例,但Object除外)。所以:Object instanceof Object
● 总结:查找对象的属性是按照隐式原型链进行的,与显式原型链没有关系,所以原型链的别名为隐式原型链
○ 由于任何函数都是Function的实例,所以任何函数都有隐式原型属性和显式原型属性,其隐式原型属性指向Function的显式原型,因此所有函数的proto都是一样的,都指向Function的prototype; 且Object的proto也指向Function的prototype,因为Object也是Function的实例
○ 由于Function是new自身创建的:var Function= new Function(),所以Function的显式原型属性指向Function的隐式原型属性 Function.prototype === Function.proto
○ Object的原型对象是原型链的尽头 Object.prototype.proto===null
原型链的指向:
p.proto // Person.prototype
Person.prototype.proto // Object.prototype
p.proto.proto //Object.prototype
p.proto.constructor.prototype.proto // Object.prototype Person.prototype.constructor.prototype.proto // Object.prototype
p1.proto.constructor // Person
Person.prototype.constructor // Person




b.n=1,b.m=’undefined’,c.n=2,c.m=3
闭包
作用:
- 使用函数内部的变量在函数执行完之后仍然在内存中(延长局部变量的生命周期)
- 让函数外部能操作(读写)内部的数据(变量/函数)
mobx中store的用法:
公共的store中 引入PublicStore 导出PublicStore 再导出实例化的publicStore
在入口组件中引入实例化的整体store 添加给组件整体
目的是在要使用publicStore的组件中 从store中可以引入PublicStore(props的类型),使用的话还是要通过props接收
公共store中
import { PublicStore } from “./PublicStore”
export { PublicStore } from “./PublicStore”
export const stores = {
publicStore: new PublicStore(),
}
、
入口文件处:
import { Provider } from ‘mobx-react’
import { stores } from “./store/stores”
ReactDOM.render(
传给需要store的组件
, document.getElementById(‘root’))
使用store的组件中:
import { PublicStore } from “../../store/stores” //目的是在props中充当类型
interface SCBrowserProps {
接收到入口文件传进来的实例化的store 和 自己从store中调的类型
publicStore?: PublicStore
}
@inject(“publicStore”)
@observer
componentWillReceiveProps里监控不到strore中的数据变化
要监控stroe中的数据变化,要么通过props传递
要么在render方法中监控
要么用mobx提供的函数监控
但是:
render方法中直接用,就是最新的值
但是要注意render调用比较频繁,所以需要加判断,防止你的方法触发多次
最好是用mobx的autorun 、reaction等方法
开发时总结的点:
1. 两个图共用一个组件只是传的参数不同
如可视化的两个柱状图 共用一个GoPathWayChart GoPathWayChart里面有柱状图的高亮效果,为了不使点击一个柱状图影响另一个柱状图的高亮,必须分开调用GoPathWayChart里面的高亮函数,将GoPathWayChart里面的高亮函数封装一下,在两个柱状图放GoPathWayChart组件的地方 分别用不同的ref(Go和Pathway)表示两个GoPathWayChart 在外面再写个函数:
highLight = (type) =>{
if(type == ‘Go’){
this.Go.setMarkBar()
}
if(type == ‘Pathway’){
this.Pathway.setMarkBar()
}
}
最后需要高亮的地方调用这个函数并且传参数(类型)作为判断,目的就是为了让其调用自己上面的方法 不影响另一个使用该组件的地方
2.有返回值的函数加括号
立马执行的函数加括号,需要传值的加括号并用箭头函数返回 以下面这种方式
someFunction()当您要调用该函数并立即返回其结果时,将使用该函数。在React中,这通常是在将JSX代码的一部分拆分为单独的函数时看到的。出于可读性或可重用性的原因。
函数中
class Demo extends React.Component{
state = {
username:’’,
password:’’
}
render() {
return (
用户名:this.saveFormatData(“username”,event)} type=”text”/>
密码:this.saveFormatData(“password”,event)} type=”password”/>
)
}
//这样保存可以减少代码的冗余 将所有数据都保存到saveFormatData中 其实调用的是内层函数 设置的值是自己的value值
用(e)=> 调用函数的方式 是因为里面函数需要传参数 只有这样调用才不会成为立即执行函数 而且第一个参数event是默认参数 需要单独传递,不能和其他参数混合传递
saveFormatData=(typeData,event)=>{
this.setState({[typeData]:event.target.value})
}
showData=()=>{
event.preventDefault();//取消跳转默认事件
console.log(this.state.username,this.state.password)
}
}
return(
}>
<List.Item.Meta
className={styles.ListItem}
description={item.fileName}
/>
)
这里的onClick写成箭头函数是因为如果我们直接写this.downLoadFile(item)的话它就相当于一个立即执行函数
因此如果我们既想它不立即执行,又要传参数的话,就可以用这样的写法
3.新旧名称有对应的map 键值对 newNameMap
现在要将旧的名称替换显示新的名称
const newName = []
const preSetList = () => {
currentDataList.forEach(element => {
if (newNameMap.has(element) ){
newName.push(newNameMap.get(element))
}
})
return newName
}
需要展示这个新名称的地方就可以直接 调用 this.preSetList()
4.如果祖孙之间传递数据层层用props嵌套麻烦的话可以使用以下的方式进行传递
a=1
在A组件中可以使用children接收{this.props.children}
这样避免了如下的传递方式,在A组件中
在B组件中用this.props.a去接收 这样的层级关系太深
5.异步发送请求:
可以用async和await await后面跟promise对象,接收的值是成功后返回的值,后面可以接then
/ 获取文件数据 */
getFileDataSource = async (fileNamePath: string) => {
const data: ResultJson
if (data.state) {
// 返回成功的非promise对象
return this.props.sortFile(data.result)
} else {
notification.open({
message: ‘error’,
description: data.message,
})
}
}
/ 初始化文件数据 /
initGetFileDataSource = () => {
/** 异步的then链式调用 /
this.getFileDataSource(‘’).then(value => {
this.setState({ treeDataSource: value })
})
}
6.树形文件列表的异步加载数据:
给父文件加children子节点
/ 异步加载数据,给父元素节点 加上children */
onLoadData = (treeNode: AntTreeNode) => {
return new Promise(resolve => {
if (treeNode.props.children) {
resolve(‘’)
return
}
// 发送请求 用then的链式调用接收数据
setTimeout(() => {
this.getFileDataSource(treeNode.props.dataRef.fileName).then(value => {
treeNode.props.dataRef.children = value
this.setState({
treeDataSource: […this.state.treeDataSource],
})
})**
resolve(‘’)
}, 0)
})
}
7.如果树形文件列表有子文件,则递归渲染。
/ renderTreeNodes 如果有子文件 递归遍历展示 */
renderTreeNodes = (dataSource: File[]) =>
dataSource.map(item => {
if (item.children && item.children.length > 0) {
return (
icon={
isLeaf={item.folder ? false : true}
>
{this.renderTreeNodes(item.children)}**
)
}
return
})
8.拖拽文件夹上传(异步+递归+修改File只读属性)
如何在拖拽文件夹上传后能够在后台组装成文件夹(由于拖拽文件夹内的子文件的相对路径是空的,所以无法拖拽文件夹上传后无法在后台形成文件夹)
- 获取文件夹中所有文件
- 上传文件。(文件中需要包含对应文件的相对路径!!!)
- 根据文件的路径需要后端在服务器创建对应文件夹,然后放置对应文件。(将文件存放至对应的文件夹)
- 这里的思路就是递归层层获取拖拽后文件夹内的所有子文件,递归+异步的方式获取。最后将文件夹的名字、大小、子文件全部传过去组装,组装完成后,上传的时候还是以子文件的方式上传到后台,因为每个子文件都有相对路径,所以说会在后台形成成型的文件夹。然而组装后的文件夹只有文件夹名和文件夹的大小两个属性,用于上传时在前端页面的显示和进度条的展示
- 代码示例
onDrageDrop = (event: React.DragEvent): any => {
event.preventDefault()
event.stopPropagation()
this.uploadTop.classList.remove('dropIn-top')
this.uploadCenter.classList.remove('dropIn-center')
this.uploadBottom.classList.remove('dropIn-bottom')
// 判断拖拽是文件还是文件夹(event.dataTransfer.items)里面可以拿到第一层文件,要判断是否为文件夹
let items: DataTransferItemList = event.dataTransfer.items
for (let i = 0; i <= items.length - 1; i++) {
let item: DataTransferItem = items[i]
if (item.kind === "file") {
let entry: FileSystemEntry = item.webkitGetAsEntry();
if (!entry.isDirectory) {
if (this.props.uploadType !== UPLOAD_TYPE.FOLDER) {
// 如果是文件列表(event.dataTransfer.files)可以直接拿到文件列表
this.handleFileList([...event.dataTransfer.files])
} else {
message.warning(getLanguage(`只能拖拽${this.props.uploadType}`))
}
} else {
if (this.props.uploadType === UPLOAD_TYPE.FOLDER) {
// 拖拽文件夹名字
let dropFolderName: string = entry.name
this.asyncDropFolder(entry, dropFolderName)
} else {
message.warning(getLanguage(`只能拖拽${this.props.uploadType}`))
}
}
}
}
}
/** 拖拽文件夹的异步获取 */
asyncDropFolder = async (entry: FileSystemEntry, dropFolderName: string) => {
// 存储拖拽文件夹列表
let FolderLists: UploadFile[] = []
// 拖拽文件夹的大小
let dropFolderSize: number = 0
let result: unknown = await this.getFileFromEntryRecursively(entry, FolderLists)
setTimeout(() => {
// unknown类型的判断
if (result instanceof Array) {
for (const iterator of result) {
dropFolderSize += iterator.size
}
// 组装文件夹(将子文件,文件夹名字,文件夹大小全部传过去进行组装)
this.getFolderList([...result], dropFolderName, dropFolderSize)
}
}, 10)
}
getFileFromEntryRecursively = (entry: FileSystemEntry, FolderLists: UploadFile[]) => {
return new Promise(resolve => {
if (entry.isDirectory) {
/**
* 如果是文件夹的话,entry的具体类型就是 FileSystemDirectoryEntry,可以使用createReader方法获得一个 FileSystemDirectoryReader对象。reader的readEntries方法,获取这个entry下的子级entries。
*/
const dirEntry: FileSystemDirectoryEntry = entry as FileSystemDirectoryEntry
let reader: FileSystemDirectoryReader = dirEntry.createReader()
reader.readEntries(
(entries: FileSystemEntry[]) => {
for (const iterator of entries) {
if (iterator.isFile) {
(iterator as FileSystemFileEntry).file((file) => {
/** 由于拖拽的文件夹没有相对路径,在后端无法形成文件夹。所以这里需要重新实例化File类型去更改子文件名为路径 */
let reNameFile: UploadFile = new File([file], `${iterator.fullPath}`);
FolderLists.push(reNameFile)
resolve(FolderLists)
})
}
}
// 递归
entries.forEach(entry => this.getFileFromEntryRecursively(entry, FolderLists))
}
)
}
})
}
9.子组件给父组件传递数据 有两种方式
第一种在父组件中用子组件的ref直接读取子组件上的函数和属性
第二种父组件上写一个函数 将函数传给子组件 子组件调用这个函数 再将子组件上的函数和属性 当作参数传给父组件上的函数 最终以函数参数的形式 父组件就拿到了子组件身上的数据, 但是有一个很重要的问题 子组件什么时候调用父组件传过来的函数 并且为这个函数传递参数 可以放在 子组件内部的render中
10.最大化之后是一半屏幕
当判断当前最大化图是当前选中的图时 先给整个画图区域的父div加类 类的总宽度为屏幕的一半,再将画图区的宽度弄为100%
11.最大化时连带展示别的视图,而且带视图切换功能:









12.antd的table 中设置分页的方法
// bordered去掉边框线
bordered={false}
rowClassName={() => “rowStyle”}
rowKey={(record) => record.id}
dataSource={userDataTableSource}
columns={this.columns}
pagination={pagination}
onChange={this.tableOnChange}
/>
/* 表格分页、排序、筛选变化时触发 /
tableOnChange = (pagination: PaginationConfig) => {
const currentPagination: PaginationConfig = { …this.state.pagination }
if (pagination.current != currentPagination.current || pagination.pageSize != currentPagination.pageSize) {
currentPagination.current = pagination.current
currentPagination.pageSize = pagination.pageSize
this.setState({
pagination: currentPagination,
})
}
}
// 会自动按照表格中的数据将数据进行分段
// 下面是分页参数
pagination: {
current: 1,//当前页
pageSize: parseInt(this.pageSizeOptions[0]),//每页展示多少条数据
showSizeChanger: true,//是否可以改变每页有多少条数据 与onShowSizeChange配合使用(每页多少条数据变化的回调)
pageSizeOptions: this.pageSizeOptions,// 选择每页多少条数据的选择项
onChange: page => this.handlePageChange(page), //改变页码的函数
position: “bottom”,
simple: false,//精简版本的分页
size: “small”,
showTotal: ((total) => { //在获取数据时就可以设置total
return
共 ${total} 条;}),
},
// 改变页码
handlePageChange = (page: number) => {
const currentPagination: PaginationConfig = { …this.state.pagination }
if (page != currentPagination.current) {
currentPagination.current = page
this.setState({
pagination: currentPagination,
})
}
}
13.单独写Pagination组件
pagination: {
showSizeChanger: false,
total: 0,
current: 1, // 默认当前页数
pageSize: 10,
size: “small”,
onChange: (page) => this.handlePageChange(page), //改变页码的函数
},
/ 改变页码 / 但是: 这里有一个重要的地方在于:要得到调用自己的结果,那么自己是一个异步函数,就会出现得不到自己的结果这种情况。因为异步函数会在同步代码执行完之后执行,也就是说,递归不起来了 这样一来,我们就可以通过 async 和 await 来想像同步一样来调用这个异步函数了 注意,async函数 返回的结果是一个 Promise ,想要获取结果可以通过 .then() 或者 async + await const fetchFileNames = dfsFileNames(‘./‘) 1、场景 2、需求 class TaskQueue { const taskQueue = new TaskQueue(); 想要让一个元素变成可释放区域,必须在 dragover 中阻止默认行为: dropArea.addEventListener(“dragover”, e => { 拖拽外部文件进入浏览器并释放的时候,浏览器会执行一些默认行为。比如说,chrome 会预览图片文件,下载压缩包。所以需要在 drop 中阻止默认行为。 dropArea.addEventListener(“drop”, e => { 鼠标从父元素进入子元素时,会触发父元素的 dragleave e.dataTransfer.setData(“text/plain”, “hello”); 如果想要存对象,自己转成字符串: e.dataTransfer.setData( “text/plain”, JSON.stringify({ name: “emily”, age: 11, }) ); 1 效果说明 2 分析 3 为什么不能用 e.dataTransfer.files ? 最终,我们拿到的是一个个 File 对象。但是,无法判断一个 File 对象是文件夹还是内容。 dropArea.addEventListener(“drop”, e => { 每个对象,可能是文件,也可能是字符串,通过 kind 属性可以判断。 使用 webkitGetAsEntry 方法:获取到一个 FileSystemFileEntry 对象或 FileSystemDirectoryEntry 对象。这两种都继承自 FileSystemEntry。 getFileFromEntryRecursively 方法 getFileFromEntryRecursively(entry) { 如果是文件的话,entry 的具体类型就是 FileSystemFileEntry,用 file 方法获得一个 File 对象: 如果是文件夹的话,entry 的具体类型就是 FileSystemDirectoryEntry ,可以使用 createReader 方法获得一个 FileSystemDirectoryReader 对象。reader 的 readEntries 方法,获取这个 entry 下的子级 entries。 getFileFromEntryRecursively(entry) { onDrageDrop = (event: React.DragEvent) => { / 页面挂载时绑定节流函数 */ / 删除 */ // 保存 record表示这个数据对象,index:表示数据对象在数组中是第几个元素 // 一键保存所有数据 / 搜索功能 */ } else { /* 将input的onchange事件调用在空的div上(uploadCardPop-center上面) / let uniqueFileList: UploadFile[] = fileList.filter((v) => uploadFileList.every((val) => val.name != v.name)) const { uploadFileList } = this.state componentDidMount(): void { 首先props继承RouteComponentProps 如下: interface StageListProps extends RouteComponentProps, FormComponentProps { / 路由跳转时传递search参数 */ 优势 : 刷新地址栏,参数依然存在 // 路由页面: <Route path=‘/home/message/detail/:id/:title’ component={Detail}/> //注意要配置 /:id //路由跳转并传递参数: 优势:传参优雅,传递参数可传对象;但是state 传参的方式只支持Browserrouter路由,不支持hashrouter //路由页面: <Route path=“/home/message/detail” component={Detail}/> //无需配置 //路由页面: <Route path=“/home/message/detail” component={Detail}/> //无需配置 优势 : 刷新地址栏,参数依然存在 //路由页面: <Route path=‘/home/message/detail’ component={Detail}/> event.preventDefault() width: calc((100vw - 50px)/2); 而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型特有的属性或方法,比如: 上面的例子中,获取 此时可以使用类型断言,将 这样就可以解决访问 let icon=ReactDom.findDOMNode(document.getElementById(‘collect’)) as HTMLElement; 当使用历史记录的时候存储数据的时候也需要将表格数据的分页总数存储进去,因为历史记录每到一步时当前这一步的数据总数不一定相同,如若不将数据总数分开来存储,那么他的分页组件就会只用第一次得到的数据总数total,就会出问题。 forward = () => { /* 后退 / 进入新文件夹。文件夹图标的单击事件: addHistory = (path: string) => { 获取文件数据请求中也是需要历史记录(1.用来记录初始记录。2.用来记录进入新文件夹请求时的记录): 由于每个表格的行展开的子项都是一个表格,他们的分页会互相影响,最好的处理办法就是将请求来的数据源与分页数据总数放在一个对象中存储,这样每次请求的数据都会携带自己的数据总数不会互相影响 数据源的存储方式: 分页组件获得文件总数: 展开项发送请求的时间,如果该展开项已经发送过请求则不需要再次发送,否则就发送请求: //将每次展开项的id传过来 首先通过判断给表格加类名 high-light-1是高亮效果,high-light-0不是高亮效果。这里传入的record是当前所有的数据行的数据对象,需要在函数里面进行判断。当外面点击的数据行id等于这里的哪个一行的数据行id时这里的哪一行加加上高亮的类。从而高亮 /* selectedRawDataFileList: [{ rawDataId: ‘’, rawDataResultFile: [], total: 0 }] // 展示方式:在整个数据源数组中查找展开的那一项id的数据rawDataResultFile,再进行分页拼接 。find()方法如果能找到就返回true 一样的,上面的那个回调函数传出来的参数,可以自己选择要哪些参数,下面的那个就是直接使用回调函数里的全部参数传递给handlexxx private history: Array<{ path: string, dataSource: File[], total: number }> = [] 对象数组的申明方式 . await起什么作用 因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。 function getSomething() { async function testAsync() { async function test() { test(); //something hello async 如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。并且阻塞await后面的代码,先执行async外的同步代码(在这之前先看看await中函数的同步代码,先把同步代码执行完),等待同步代码执行完之后,再回到async内部继续执行。 如果它等到的是一个 Promise 对象,await 就忙起来了,await会阻塞下面代码(当前async代码块的代码),会先执行async外的同步代码(在这之前先看看await中函数的同步代码,先把同步代码执行完),等待同步代码执行完之后,再回到async内部等promise状态达到fulfill的时候再继续执行下面的代码。等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。 看个例子 async function fun1() { 联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。 所以,async 函数返回的是一个 Promise 对象。从文档中也可以得到这个信息。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象。如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。
Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。 当引用前人写的代码时,失去样式 或者失去一些动画效果的时候有可能是没有引用前人写的样式或者一些bootstarp.
参考vsCode左边侧边栏的点击时,当点击空白处时此时的路径就是根目录下。但是树形控件由于是antd已经封装好的控件。所以不能凭借点击包裹树形控件的div来达到预想的效果,因为需要排除点击树形的每一行时不能触发根目录的点击事件。 文件展开行时不发送请求: 在render中去遍历 原生js的做法: 3000端口给5000发送请求存在跨域问题: 请求路径中要配置代理,站在自己的端口给自己发的话localhos:3000可以省略不写,请求关键词用模板字符串传入请求参数中,则 消息订阅与发布机制:兄弟元素间传递数据 并且适用于任何组件间传递数据 每个窗口,每个框架(frame)都有自己的history对象,它可以通过go()方法在不知道URL的情况下做出浏览器前进后退的功能 NavLink组件: V5: 可以将原来NavLink中传的所有的参数 在MyNavLink组件中用{…this.props}展开 public文件是localhost:3000内置服务器dev-server下的根路径 请求啥就给啥 如果我们所访问的地址(localhost:3000/qcq/home)根本就不存在那就默认访问public中index.html,index.html是默认兜底的 注册路由和实现路由 的路径默认是模糊匹配的(输入的路径必须包含匹配的路径,且顺序一致) 要实现精准匹配 则 在注册路由中加一个属性 exact={true} 多级路由或者嵌套路由: 向路由组件传递消息数据: 向路由组件传递search参数: 安装qs插件cnpm install qs 在页面引入 import qs from ‘qs’ 向路由组件传递state参数: 1.默认路由跳转都是push模式(路径压栈模式) push能够留下痕迹 默认开启push模式, 如何让一般组件用上路由组件上的api: 用的时候再加载,最多的就是路由组件的懒加载, 组件优化: render props如何向组件内部动态传入带内容的结构(标签) 一种组件间的通信方式,常用于【祖组件】与【后代组件】之间的通信 (1).State Hook让函数组件也可以有state状态,并进行状态数据的读写操作 (1). Effect Hook 可以让你在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子) (1). Ref Hook可以在函数组件中存储/查找组件内的标签或任意其它数据 在index.html页面引入css时不要点,防止出现二级路由样式丢失的问题 useRoutes 函数组件的路由表,当我们生成路由表之后,在注册路由的时候就不再 书写重复的代码 直接用路由表即可 {/ end 子级路由如果匹配了,自己本身会失去高亮 /} 路由组件之间的传递消息,传递Params参数 路由组件之间的传递消息,传递Search参数 路由组件之间的传递消息,传递state参数 Link和NavLink 必须要点击才能实现路由的跳转。 useInRouterContext() 如果组件被BrowserRouter包裹 那这个组件就是处于路由的上下文组件。 初识react 相关js库引入 脚手架安装 安装express npm install express express服务器 消息订阅与发布机制:cnpm install pubsub-js —save 安装路由库 cnpm install react-router-dom 传递search消息 截取search传递参数安装:cnpm install qs import qs from ‘qs’ 安装antd库: cnpm install antd 安装redux:cnpm install —save redux 安装react-redux: cnpm add react-redux 自动创建id cnpm install nanoid 安装redux开发者工具实时观察里面的 各种状态成员(安装chrome浏览器插件) 。要让开发者工具工作起来,下载工具依赖包npm install —save-dev redux-devtools-extension REACT脚手架集成TYPESCRIPT React有create-react-app,这种脚手架的方式好处很明显,支持ES6新增语法、api,每次保存自动更新改变的内容(不用再每次手动关闭重启Node服务器),所以弄出脚手架: $ npx create-react-app react_typescript 安装Typescript: $ npm install typescript { { 现在需要将Typescript集成到React脚手架的构建工具(默认webpack),也就是说ts零件已经制造完成,需要安装到脚手架机器上,这一步比较坑,因为脚手架已经集成了webpack,如果我们还本地安装webpack,执行npm start会报错,解决方案: 因为脚手架会自动搜索根目录下的webpack配置文件webpack.config.js,并将其配置合并到总配置中,所以我们需要在根目录下创建webpack.config.js,写入配置如下: 在webpack中使用要先安装相关loader,官方建议安装awesome-typescript-loader(因为它比ts-loader更快): npm install awesome-typescript-loader source-map-loader —save-dev module.exports = { 然后安装source-map-loader,awesome-typescript-loader: $ npm install awesome-typescript-loader source-map-loader 模块名的格式为: @types/module-name,例如 $ npm install @types/react 成功截图 挖坑成功! 构建成功! “dependencies”: { //enum:枚举的使用 //对象类型 函数类型:let fun1:(a:number,b:number)=>number=(a,b)=>{ 数组类型: //联合类型 //接口 // 而接口就是用来定义一个类的结构即类中应该包含哪些属性和方法(我个人理解为,实际上接口也可能看成一种自定义类型,该类型一定要包含接口的规范) // 实际上又不一定仅限于定义类的结构,也可以作为一种类型去使用,比如用 :myInterface规范类型, 所以才导致出现type 和 interface都可以使用的场景,所以接口也可以当成类型声明去使用 // 自定义类型 type myType2 = { interface myInterface{ // 接口中所有属性都不能有实际的值(但是抽象类可以定义实际的值) interface myInterface{ // (1)接口 VS 自定义类型 VS抽象类 interface myInterface{ // 接口中所有属性都不能有实际的值(但是抽象类可以定义实际的值) interface myInterface{ super指的是当前类的父类 属性的封装 promise内部是同步执行的 **async函数返回一个Promise对象。 async function test () { 在使用form表单的时候,一旦点击提交触发submit事件,一般会使得页面跳转,页面间的跳转等行为的控制权往往在后端,后端会控制页面的跳转及数据传递,但是在某些时候不希望页面跳转,或者说想要将控制权放在前端,通过js来操作页面的跳转或数据变化。 点击登录按钮后,即触发form表单的提交事件,数据传输至后端,由后端控制页面跳转和数据。 几点区别如下: 2、form中的登录按钮处type=‘submit’,而ajax中的登录按钮处type=‘button’且要加上οnclick=‘login()’函数,并在JavaScript代码中完成login()函数的功能 通过$(‘#postForm’).serialize()可以对form表单进行序列化,从而将form表单中的所有参数传递到服务端。 FormData是什么呢?我们来看看Mozilla上的介绍。 Constructor W3c草案提供了三种方案来获取或修改FormData。 方案1:创建一个空的FormData对象,然后再用append方法逐个添加键值对: | formdata.append(“url”, “http://www.baidu.com/“); |
| —- | 方案2:取得form元素对象,将它作为参数传入FormData对象中! 方案3:利用form元素对象的getFormData方法生成它! formData.append(name, value); formData.append(name, value, filename);
Mobile HTML代码 javascript代码 这里要注意几点: 上传后,服务器端代码需要使用从查询参数名为file获取文件输入流对象,因为中声明的是name=”file”。 HTML代码 javascript代码 这里有几处不一样: 从代码$(‘#file’)[0].files[0]中可以看到一个标签能够上传多个文件, 从Servlet 3.0 开始,可以通过 request.getPart() 或 request.getPars() 两个接口获取上传的文件。 FormData 简单介绍 我们打印这个构造函数看一眼 ƒ FormData() FormData的主要用处 网络请求中处理表单数据 ◆ new FormData ( HTMLFormElement: ele) 在使用FormData构造函数创建实例对象的时候,可以传递一个HTML表单元素,该表单元素允许是任何形式的表单控件,包括文件输入框、复选框等。 //列出创建formData实例对象的几种方式 //通过调用对象的方法来设置数据(模拟表单) //002 获取表单标签传递给FormData构造函数 注意:表单标签必须要添加name属性才能获取其数据 说明: 在上面的示例代码中介绍了两种创建(获取)formData实例对象的方式,可以先创建一个空的实例对象也可以直接通过页面中的表单标签来进行初始化处理。 当formData数据装填好之后,可以直接通过ajax方法提交到服务器端,下面给出上面代码的执行结果。 name 文顶顶 如上图所示,FormData构造函数的原型对象上面定义了一堆方法。这些方法使用方式都很简单,接下来我们通过代码的方式简单介绍他们。 //01 创建空的formData实例对象 代码说明 formData 对象的这些方法其实不用进行过多的赘述,上面的代码和说明简单易懂。总体上来说,它提供了一整套的操作数据的方法囊括了添加(set)、修改、查询和删除等操作,append方法和set方法的不同之处在于它不会覆盖而是以数组push的方式来处理同名的数据。 formData 对象的keys()、values()和entries()方法使用类似,调用后将得到一个Iterator类型的迭代器对象,该对象能够能够调用next()方法来进行迭代操作,打印结果中的done使用布尔类型的值来进行标记,如果迭代结束那么值为true。 formData 对象的forEach()接收一个回调函数参数,其中第一个参数为当前遍历数据的value值,第二个参数为key(同数组的forEach方法一致)。如果是Ajax发送GET请求,需要通过formData的方式来提交表单数据,那么可以借助该方法来拼接查询字符串。 FormData的典型用法 POST请求 POST请求进行文件上传 这里顺便贴出测试文件上传写的Node代码以及文件上传后的监听结果。 //备注:node文件名称为uploadServer.js var app = express(); //02 监听网络请求并设置打印接收到的参数信息 wendingding$ node uploadServer.js
让时间为你证明
handlePageChange = (page: number) => {
const currentPagination: PaginationConfig = { …this.state.pagination }
if (page != currentPagination.current) {
currentPagination.current = page
this.setState({
pagination: currentPagination,
})
}
}
// 通过计算表格的scroll 从而设置给外层div 用来固定表格的高度 record.toString()}
className={‘multiple-‘ + customGeneListStore.isMultiple}
columns={this.columns}
// 这里的dataSource必须进行分割 当页码变化时让 表格的数据进行变化 每页展示的数据的分割点为: 比如第一页 每页10条数据 那第一页展示0-9.分割slice的第一个参数是0,第二个参数是9 **
dataSource={
customGeneListStore.selectedGeneSet
? customGeneListStore.geneSetId2GeneList[customGeneListStore.selectedGeneSet.getId()].slice(pagination.current pagination.pageSize - pagination.pageSize, pagination.current pagination.pageSize) || []
: []}
rowSelection={rowSelection}
// 关闭表格自带的分页设置
pagination={false}
// onChange={this.paginationOnChange}
scroll={{
y: containerDomRect ? containerDomRect.height - 20 - 30 - 35 : 0, // 表头和分页组件高度都是30,多减5px防止内容溢出,出现滚动条
}}
onRow={this.onTableRow}
/>
*// 这里单独使用分页组件 总数的设置 是根据数据的长度进行设置。为什么在render中写total呢。原因是,这里的数据是从store中获取的可以进行下拉框选中的数据 。
14.componentWillReceiveProps里监控不到strore中的数据变化(从store中拿的数据是动态的)
要监控stroe中的数据变化,要么通过props传递
要么在render方法中监控
要么用mobx提供的函数监控
render方法中直接用,就是最新的值
但是要注意render调用比较频繁,所以需要加判断,防止你的方法触发多次
最好是用mobx的autorun 、reaction等方法
: [].length}/>
15.异步+递归
解决方法
js 提供了 Promise 类,可以解决这个问题
假设我们要通过 fs.readdir() 这个异步方法来递归读取某个路径下的所有文件名,我们可以先通过 Promise 把 fs.readdir() 这个异步方法封装起来
new Promise((resolve, reject) => {
fs.readdir(path, ‘utf-8’, function (err, data) {
if (err) reject(err);
else resolve(data)
})
})
由于递归时我们会多次调用 fs.readdir() 函数,每次的访问的路径都是不一样的,所以我们需要一个Promise工厂
function asyncReadDir (path) {
return new Promise((resolve, reject) => {
fs.readdir(path, ‘utf-8’, function (err, data) {
if (err) reject(err);
else resolve(data)
})
})
}
这样一来我们就可以通过这个工厂函数,轻松的得到一个封装后的 fs.readdir() 函数了,有了这个工厂函数和 async 和 await ,我们可以简单实现一个递归操作实现需求
async function dfsFileNames (path) {
const data = await asyncReadDir(path)
const res = []
for (const fileName of data) {
res.push(fileName)
if (!fileName.includes(‘.’)) {
res.push(await dfsFileNames(${path}/${fileName}))
}
}
return res
}
完整代码
const fs = require(‘fs’);
function asyncReadDir (path) {
return new Promise((resolve, reject) => {
fs.readdir(path, ‘utf-8’, function (err, data) {
if (err) reject(err);
else resolve(data)
})
})
}
async function dfsFileNames (path) {
const data = await asyncReadDir(path)
const res = []
for (const fileName of data) {
res.push(fileName)
if (!fileName.includes(‘.’)) {
res.push(await dfsFileNames(${path}/${fileName}))
}
}
return res
}
fetchFileNames.then(res => {
console.log(res);
})16.任务队列
在前端页面中需要同时发送20个请求,但是服务端有限制,需要前端控制并发数,保证最多只能同时发送5个请求。
1、最多同时执行的任务数为10个
2、当前任务执行完成后,释放队列空间,自动执行下一个任务
3、所有任务添加到任务队列后,自动开始执行任务
function createTask(i) {
// 创建任务,返回一个promise类型的函数
return () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(i);
}, 2000);
});
};
}
constructor() {
this.max = 5;
this.taskList = [];
setTimeout(() => {
this.run();
});
}
addTask(task) {
this.taskList.push(task);
}
run() {
let len = this.taskList.length;
if (!len) return false;
let min = Math.min(this.max, len);
for (let i = 0; i < min; i++) {
// 开始占用一个任务的空间
this.max—;
let task = this.taskList.shift();
task().then((res => {
console.log(‘result:’, res);
})).catch(error => {
throw new Error(error);
}).finally(() => {
// 释放一个任务空间
this.max++;
this.run();
});
}
}
}
for (let i = 0; i < 20; i++) {
const task = createTask(i + 1);
taskQueue.addTask(task);
}17.拖放踩坑

HTML 拖放(Drag and Drop)功能的踩坑总结
让一个元素被拖动,需要添加 draggable 属性:
// 必须阻止默认事件,这里才会变成可释放区域。
// 如果不写这一句,那么松手的时候,也不会触发相应的 drop 事件
e.preventDefault();
});
e.preventDefault();
});
dataTransfer.setData(format, data) 的两个参数并不是 key 和 value !!!
format:数据的类型。只能有两种值 “text/plain” 、”text/uri-list”
dataTransfer.getData(format) 也是一样,format 也是只能为上面两种值
e.dataTransfer.setData(“text/plain”, “world”);
const data = e.dataTransfer.getData(“text/plain”);
console.log(data); // world
const data = JSON.parse(e.dataTransfer.getData(“text/plain”));
console.log(data);18.拖拽本地文件夹到浏览器中
从本地拖拽文件(夹)到浏览器中,展示包含的所有文件。
需要先获取拖拽的内容,得到一个文件(夹) list 。循环 list ,对于其中的每一项,如果是文件,那么直接获取;如果是文件夹,则需要递归读取内部文件。
在 drop 事件中可以得到 e.dataTransfer ,它有两个属性可以获得拖拽内容:
files :被拖动到浏览器窗口中的本地文件列表
items :拖动操作中被拖动项的 DataTransferItem 对象。(拖动项可能是文件,也可能是别的)
先说结论:不能用 e.dataTransfer.files ,必须用 e.dataTransfer.items
e.dataTransfer.files 是 FileList 对象,是伪数组对象(有 length 属性,可通过索引获取元素)。遍历得到 File 对象
dropArea.addEventListener(“drop”, e => {
let files = e.dataTransfer.files; // FileList 对象
for (let i = 0; i <= files.length - 1; i++) {
let file = files.item(i); // File 对象
console.log(file);
}
e.preventDefault();
});
File 对象属性如下:
不能用 type 来判断,因为不靠谱:
type 属性:浏览器不会实际读取文件的字节流,而是根据文件的扩展名来判断。而且,file.type 仅仅对常见文件类型可靠,不常见的文件扩展名会返回空字符串。
总结:用 e.dataTransfer.files 最终获取到的是 File 对象,无法判断一个 File 对象是文件还是文件夹,所以不能用!
4 用 e.dataTransfer.items
e.dataTransfer.items 是 DataTransferItemList 对象,是伪数组对象(有 length 属性,可通过索引获取元素)。遍历得到 DataTransferItem 对象。
// DataTransferItemList 对象,是伪数组对象
let items = e.dataTransfer.items;
for (let i = 0; i <= items.length - 1; i++) {
// DataTransferItem 对象
let item = items[i];
}
e.preventDefault();
});
dropArea.addEventListener(“drop”, e => {
let items = e.dataTransfer.items;
for (let i = 0; i <= items.length - 1; i++) {
let item = items[i];
// 通过 kind 属性可以判断当前的 DataTransferItem 对象是文件还是字符串
if (item.kind === “file”) {
}
}
e.preventDefault();
});
dropArea.addEventListener(“drop”, e => {
let items = e.dataTransfer.items;
for (let i = 0; i <= items.length - 1; i++) {
let item = items[i];
if (item.kind === “file”) {
// FileSystemFileEntry 或 FileSystemDirectoryEntry 对象
let entry = item.webkitGetAsEntry();
// 递归地获取entry下包含的所有File
this.getFileFromEntryRecursively(entry);
}
}
e.preventDefault();
});
使用 FileSystemEntry 对象的 isFile 属性,判断是文件还是文件夹。
if (entry.isFile) {
// 文件
} else {
// 文件夹
}
}
FileSystemFileEntry.file(successCallback[, errorCallback]);
successCallback 中会传入 File 对象。注意 :这个 File 对象的相对路径是空(webkitRelativePath是空字符串),所以如果想要保留拖拽的层级结构,只能从 entry 中获取
getFileFromEntryRecursively(entry) {
if (entry.isFile) {
// 文件
entry.file(
//
file => {
// 想要保留拖拽的层级结构的话,只能从 entry 中获取
this.addFileToList({ file, path: entry.fullPath });
},
e => { console.log(e); }
);
} else {
// 文件夹
}
}
if (entry.isFile) {
entry.file(
file => {
this.addFileToList({ file, path: entry.fullPath });
},
e => { console.log(e); }
);
} else {
let reader = entry.createReader();
reader.readEntries(
entries => {
entries.forEach(entry => this.getFileFromEntryRecursively(entry));
},
e => { console.log(e); }
);
}
}个人开发中的拖拽获取文件或者文件夹
event.preventDefault()
event.stopPropagation()
this.uploadTop.classList.remove(‘dropIn-top’)
this.uploadCenter.classList.remove(‘dropIn-center’)
this.uploadBottom.classList.remove(‘dropIn-bottom’) // 判断拖拽是文件还是文件夹
let items: DataTransferItemList = event.dataTransfer.items
for (let i = 0; i <= items.length - 1; i++) {
let item: DataTransferItem = items[i]
if (item.kind === "file") {
let entry: FileSystemEntry = item.webkitGetAsEntry()
// 只判断第一层
if (!entry.isDirectory) {
if (this.props.uploadType !== UPLOAD_TYPE.FOLDER) {
// 如果是文件列表
this.handleFileList([...event.dataTransfer.files])
} else {
message.warning(getLanguage(`只能拖拽${UPLOAD_INFO['FOLDER']}`))
}
} else {
if (this.props.uploadType === UPLOAD_TYPE.FOLDER) {
// 拖拽文件夹名字
let dropFolderName: string = entry.name
this.getDropFolder(entry, dropFolderName)
} else {
message.warning(getLanguage(`只能拖拽${UPLOAD_INFO['FILE']}`))
}
}
}
}
}
/**
* 获取拖拽文件夹
* @param entry
* @param dropFolderName
*/
getDropFolder = async (entry: FileSystemEntry, dropFolderName: string) => {
// 存储拖拽文件夹列表
let folderLists: UploadFile[] = []
// 拖拽文件夹的大小
let dropFolderSize: number = 0
await this.getFileFromEntryRecursively(entry, folderLists)
// 等待await后面的语句执行完成之后再执行下面(先执行所有的同步代码,再回到async内部等promise状态达到fulfill的时候再继续执行下面的代码),拿到folderLists的结果
for (const iterator of folderLists) {
dropFolderSize += iterator.size
}
// 组装文件夹
this.getFolderList([...folderLists], dropFolderName, dropFolderSize)
}
/**
* 递归查找文件夹下的所有子文件
* @param entry 拖拽获取的FileSystemEntry类型的文件,它里面包括FileSystemDirectoryEntry和FileSystemFileEntry两个类型,通过递归逐层判断,如果是FileSystemFileEntry类型说明到了文件夹的最后一层,拿到该层的子文件。如果是FileSystemDirectoryEntry类型,说明该文件夹里面还存在嵌套文件夹,继续递归查找子文件
* @param folderLists 存放文件夹中所有子文件列表
* @returns
*/
getFileFromEntryRecursively = (entry: FileSystemEntry, folderLists: UploadFile[]) => {
return new Promise((resolve, reject) => {
if (entry.isDirectory) {
let entryDirectory: FileSystemDirectoryEntry = entry as FileSystemDirectoryEntry
const directoryReader: FileSystemDirectoryReader = entryDirectory.createReader()
// readEntries里面有两个回调函数successCallback: FileSystemEntriesCallback, 和errorCallback?: ErrorCallback 均为异步执行
directoryReader.readEntries(
// 该async是为了执行此回调函数下的抛出异常,因为async函数里如果没有await的话和普通函数的区别不大,只是可以返回promise对象。如去掉该async的话也可执行。下面entryFile.file上的async的意义也是如此
async (entries: FileSystemEntry[]) => {
entries.forEach(
async (entry: FileSystemEntry, index: number) => {
// 若该层文件夹下若还是文件夹则调用递归,这里的await目的是为了将多次的递归的任务执行变为同步执行
await this.getFileFromEntryRecursively(entry, folderLists)
if (index === entries.length - 1) {
resolve(1)
}
})
},
(e: DOMException) => {
reject(e)
}
)
} else {
let entryFile: FileSystemFileEntry = entry as FileSystemFileEntry
entryFile.file(
async (file: File) => {
/**
* 由于拖拽的文件夹没有相对路径,在后端无法形成文件夹。所以这里需要重新实例化File类型去更改子文件名为路径
*/
let reNameFile: UploadFile = new File([file], `${entry.fullPath}`)
folderLists.push(reNameFile)
resolve(1)
},
(e: DOMException) => {
reject(e)
}
)
}
})
}
19.页面挂在时绑定节流函数
privatethrottle: (() =>void) & _.Cancelable
/ 节流函数,页面加载时绑定,this.handleUpload事件触发函数 /
this.throttle = _.throttle(this.handleUpload, 5000)
*在dom事件中调用onChange = this.throttle
20.删除文件列表中的元素
handleDelete = (record: File) => {
const userDataTableSource: File[] = […this.state.userDataTableSource]
this.setState({ userDataTableSource: userDataTableSource.filter(item => item.id !== record.id) })**
}
21.保存一行数据
handleSave = (record: File, index: number) => {
const { editArr, userDataTableSource } = this.state
const newData: File[] = […userDataTableSource]
// splice三个元素时替换元素,用editArr中的数据替换record最后替换dataSource中数据
newData.splice(index, 1, {
…record,
…editArr[index],
isEdit: false,// 编辑状态关闭
})
this.setState({ userDataTableSource: newData })
}22.保存所有元素
// saveAll = () => {
// const { dataSource } = this.state
// const arr = dataSource.map((item) => {
// return {
// …item,
// isEdit: false,
// }
// })
// this.setState({ dataSource: arr }, () => {
// console.log(dataSource, “—dataSource”)
// })
// }
23.前端搜索功能
onSearchFile = (value: string) => {
const { userDataTableSource } = this.state
let searchDataSource: File[] = []
if (value) {
for (let i = 0; i < userDataTableSource.length; i++) {
if (userDataTableSource[i].fileName.match(value)) {
searchDataSource = […searchDataSource, userDataTableSource[i]]
}
} this.setState({
userDataTableSource: searchDataSource
})**
this.setState({ userDataTableSource: this.state.userDataTableSource })
}
}
24.字符串的最后一个字符换为空字符
let endSizeSearch: string = this.state.endSizeSearch<br /> let length: number = endSizeSearch.length<br /> let endSizeArr: string[] = endSizeSearch.split('')<br /> endSizeArr[length - 1] = ''<br /> endSizeSearch = endSizeArr.join('')
25.点击空白处调用其他dom元素上的事件(ref)
uploadShowList = () => {
this.fileInput.click()
}
26.相同文件去重处理
27.删除上传文件
let newFileList: UploadFile[] = []
let index: number = 0
uploadFileList.forEach((element: { name: string; }, item: number) => {
if (uploadFileList[item].name === fileName)
index = item
})
// 拷贝文件数组
newFileList = […uploadFileList]
newFileList.splice(index, 1)
this.setState({ uploadFileList: newFileList })
28.dom绑定和去除属性
/ 如果上传类型为文件夹上传,则绑定webkitdirectory属性,否则去掉这个属性 */
if (this.props.uploadType === UPLOAD_TYPE.FOLDER) {
this.fileInput.setAttribute(‘webkitdirectory’, ‘true’)
} else {
this.fileInput.toggleAttribute(‘webkitdirectory’, false)
}
/ 节流函数,页面加载时绑定 */
this.throttle = _.throttle(this.handleUpload, 5000)
}
29.编程式的路由跳转
import { Route, RouteComponentProps, withRouter } from’react-router-dom’
}
private searchParams = () => {
const {search} = this.props.location
const {stageAnalysis,stageHeat} = qs.parse(search.slice(1))
console.log(stageAnalysis, stageHeat)
}
可以直接使用this.props.history, this.props.location, this**.props.match.params 等路由组件的属性,一般组件需要用withRouter进行包裹。1、params传参
缺点:只能传字符串,并且,如果传的值太多的话,url会变得长而丑陋。
//链接方式: <Link to={/home/message/detail/${id}/${title}}>{title}</Link>
//或: <Link to={{pathname:/home/message/detail/${id}/${title}}}>{title}</Link>
//js方式: this.props.history.push(/home/message/detail/${id}/${title})
// 或: this.props.history.push({pathname:/home/message/detail/${id}/${title}})
//获取参数: const {id,title} = this.props.match.params //注意这里是match而非history
2、query与state传参
缺点:刷新地址栏,参数丢失
//路由跳转并传递参数:
// 链接方式: <Link to={{ pathname: ‘/home/message/detail’, query: {id:id, title: title} }}>{title}</Link>
//js方式: this.props.history.push({ pathname: ‘/home/message/detail’, query: {id, title} })
//获取参数: const {id, title} = this.props.location.query ||{}
//路由跳转并传递参数:
// 链接方式: <Link to={{ pathname: ‘/home/message/detail’, state: {id:id, title: title} }}>{title}</Link>
//js方式: this.props.history.push({ pathname: ‘/home/message/detail’, state: {id, title} })
//获取参数: const {id, title} = this.props.location.state ||{}
3、search传参
缺点:只能传字符串,并且,如果传的值太多的话,url会变得长而丑陋。
可以直接通过search传递参数 传递id过去,最终通过id来重新获取请求来得到这个id下的所有数据。避免了 一次性传递好多参数。
//路由跳转并传递参数:
// 链接方式: <Link to={/home/message/detail/?id=${id}&title=${title}}>{title}</Link> //无需配置
//js方式: this.props.history.replace(/home/message/detail/?id=${id}&title=${title})
//获取参数: const {search} = this.props.location const {id, title} = qs.parse(search.slice(1)) //slice去掉前面的’?’
/*
跳转到流程入口页签
@param stageData 每条分析流程的数据
/
public jumpStageEntrance = (stageData: StageModel) => {
const { stageAnalysis, stageHeat } = stageData
this.props.history.push({
pathname: /stageList/stageEntry,
search: qs.stringify({ stageAnalysis, stageHeat }),
})
// 传递state参数
// this.props.history.push({pathname: ‘/stageList/stageEntry’,
// state: stageData})
}30.dom元素阻止默认事件
event.stopPropagation()
31.宽高动态计算
100%宽的视口-50(两个内边距) 再/2取一半32.当一个表格组件共用为两个不同数据类型的组件时,需要断言。
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function isFish(animal: Cat | Fish) {
if (typeof animal.swim === 'function') {
return true;
}
return false;
}
// index.ts:11:23 - error TS2339: Property 'swim' does not exist on type 'Cat | Fish'.
// Property 'swim' does not exist on type 'Cat'.
animal.swim 的时候会报错。animal 断言成 Fish:interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
//使用 值 as 类型
function isFish(animal: Cat | Fish) {
if (typeof (animal as Fish).swim === 'function') {
return true;
}
return false;
}
animal.swim 时报错的问题了。
33.操作dom节点时的ts报错处理
Property ‘className’ does not exist on type ‘Element | Text’. Property ‘className’ does not exist on type ‘Text’.
原因:这是typescript的类型检查导致的,需要在querySelector或者其他查询语句方法前面加个类型断言。
icon.className=IconStyles.iconfont+” “+IconStyles.icon_shoucang134.文件层级历史记录以及前进回退功能以及每层数据的分页
首先声明:
/ 保存文件层级历史路径,以及该路径下对应的文件数据和文件数目 */
private history: Array<{ path: string, dataSource: File[], total: number }> = []
/ 保存历史记录步骤index /
private historyIndex: number = -1
前进和后退功能:
/** 前进 /
// 前进的那一步必须要存在,否则直接返回
if (this.historyIndex >= this.history.length - 1) {
return
}
this.historyIndex++
// 直接从前进的这一步中拿到数据和数据总数避免重新请求
this.setState({
fileDataSource: this.history[this.historyIndex].dataSource,
pagination: { …this.state.pagination, total: this.history[this.historyIndex].total }
}, () => {
// 更改页面显示路径
this.freshPath(this.history[this.historyIndex].path)
})
}
back = () => {
if (this.historyIndex < 1) {
return
}
this.historyIndex—
this.setState({
fileDataSource: this.history[this.historyIndex].dataSource,
pagination: { …this.state.pagination, total: this.history[this.historyIndex].total }
}, () => {
this.freshPath(this.history[this.historyIndex].path)
})
}
/* 进入新的文件夹 /
// 如果在一直前进的过程中有回退再重新进入新文件夹,由于此文件夹之前进去过,则执行if里面的条件,直接从历史记录里面拿到数据
if (this.historyIndex < this.history.length - 1 && path === this.history[this.historyIndex + 1].path) {
this.historyIndex++
this.setState({
fileDataSource: this.history[this.historyIndex].dataSource,
pagination: { …this.state.pagination, total: this.history[this.historyIndex].total }
})
return
}
this.history = this.history.slice(0, this.historyIndex + 1)
// 否则一直进入新文件夹,则直接请求数据并且增加历史记录
this.getFileData(path)
// 更新显示路径
this.freshPath(path)
}
/ 获取文件数据请求 */
getFileData = (parentPath: string) => {
this.setState({ loading: true })
StageApi.getPublicFile(parentPath).then((data: ResultJson
if (data.state) {
this.setState({
loading: false,
fileDataSource: data.result,
pagination: { …this.state.pagination, total: data.result.length }
})
this.history.push({
path: ‘/‘,
dataSource: data.result,
total: data.result.length
})
this.historyIndex = 0
} else {
this.history.push({
path: parentPath,
dataSource: data.result,
total: data.result.length
})
this.historyIndex++
}**
} else {
notification.open({
message: ‘error’,
description: data.message,
})
}
}).catch((e) => {
console.log(e)
notification.open({
message: ‘error’,
description: ‘data load error.’,
})
})
}35.表格展开子表格的分页
存储数据源

36.表格数据展开项只在第一次展开时请求,请求过的不再请求,加子项表格的分页
/
已选择文件列表数据源, 类型是一个对象数组
rawDataId:将要展开的rawDataId也就是表格数据将要展开的这一行的id,
rawDataResultFile: 展开的rawData下的文件数据源,
total:展开的rawData下的文件总数(由于每个rawData下的文件数不一定相同所以分页的total要分别存储)
/
selectedRawDataFileList: [{ rawDataId: string, rawDataResultFile: File[], total: number }]
发送请求获得数据:
initRawDataFileList = (expandedRowKey: string) => {
this.setState({ loading: true })
StageApi.getRawDataFile({
type: ‘task’,
projectId: this.props.projectId,
taskId: expandedRowKey && expandedRowKey,
stageId: ‘’
}).then(data => {
// 重新声明一个数组用来存储
let selectedRawDataFile: [{ rawDataId: string; rawDataResultFile: File[]; total: number }] = [{ rawDataId: ‘’, rawDataResultFile: [], total: 0 }]
selectedRawDataFile.push({ rawDataId: expandedRowKey, rawDataResultFile: data.result.content, total: data.result.content.length })
selectedRawDataFile.push(…this.state.selectedRawDataFileList)
this.setState({
selectedRawDataFileList: selectedRawDataFile,
loading: false,
})
})*
}
将要展示分页组件时,利用当前将要展开项的id去数据源里面查,当查到时就将该展开项的total拿出来传给分页组件
/**
totalData: 数据总数
pageSizeData: 列表每页条数
currentData: 当前页数
@returns
*/
paginationConfig = () => {
const { current, pageSize } = this.state.pagination
const { expandedRowKey } = this.props
let total: number = 0
// 数据源
let selectedRawDataFileList = […this.state.selectedRawDataFileList]
selectedRawDataFileList.forEach(element => {
if (element && element.rawDataId === expandedRowKey) {
total = element.total
}
})
return {
totalData: total,
pageSizeData: pageSize,
currentData: current,
size: ‘small’
}
}
/**
@paramexpandedRowKey 展开的rawData
/
getFileDataSource = (expandedRowKey: string) => {
const { selectedRawDataFileList } = this.state
// 数据源里面会有多个不同id的对应文件数据,所以循环查找,但凡有一次能在数据源中找到 该展开项的id,就给flag标记为true,说明已经请求过了。
selectedRawDataFileList.forEach((fileDataEle, index) => {
if (fileDataEle.rawDataId === expandedRowKey) {
this.flag = true
}
// 展开闭合时都会有expandedRowKey产生,闭合时expandedRowKey是undefined。所以当每一次传过来expandedRowKey时都要判断, 只要expandedRowKey不是undefined 并且 index为循环的最后一次说明该数据源从头到尾查询了一遍,并且flag为false说明该展开行id没有发送过请求,那就调用发送请求
if (expandedRowKey && index === selectedRawDataFileList.length - 1 && this.flag === false) {
this.initRawDataFileList(expandedRowKey)
}
})
}
componentDidMount(): void {
this.props.setCurrentStep(1)
this.getFileDataSource(this.props.expandedRowKey)
}
componentWillReceiveProps(nextProps: Readonly
if (this.props.expandedRowKey !== nextProps.expandedRowKey) {
this.getFileDataSource(nextProps.expandedRowKey)
}
}37.表格行的高亮

判断高亮的方法
38.对象数组中,对象属性也是数组,如何在表格中展示该数组,展示rawDataResultFile
已选择文件列表数据源, 类型是一个对象数组
rawDataId:将要展开的rawData,
rawDataResultFile: 展开的rawData下的文件,
total:展开的rawData下的文件总数(由于每个rawData下的文件数y不一定相同所以分页的total要分别存储)
/
selectedRawDataFileList: Array<{ rawDataId: string, rawDataResultFile: File[], total: number }>
dataSource={selectedRawDataFileList && selectedRawDataFileList.find(rawDataEle => rawDataEle.rawDataId === this.props.expandedRowKey)?.rawDataResultFile.slice(current pageSize - pageSize, current pageSize)}
39.(x)=>{this.add(x)} 与{this.add}的区别


还可以这么理解上面的那个是先定义了onSelected为()=>{} 函数,函数里面执行了 handle,下面的直接吧onSelected定义为handle
40.对象数组的声明方式
41.async和await的理解
一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。
return “something”;
}
return Promise.resolve(“hello async”);
}
const v1 = await getSomething();
const v2 = await testAsync();
console.log(v1, v2);
}
await 等到了它要等的东西,一个 Promise 对象,或者其它值,然后呢?我不得不先说,await 是个运算符,用于组成表达式,await 表达式的运算结果取决于它等的东西。
console.log(“It is fun1”)
let value1 = await fun2();
let value2 = await fun3();
console.log(value1,value2);
}
function fun2() {
return “hello”;
}
function fun3() {
return Promise.resolve(“guguji”);
}
fun1();
console.log(“outer1”);
console.log(“outer2”);
console.log(“outer3”);
console.log(“outer4”);
console.log(“outer5”);
//It is fun1 先async内部同步代码
//outer1
//outer2
//outer3
//outer4
//outer5
//hello guguj补充知识点 [2020-06-04]
async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,我们当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样
testAsync().then(v => { console.log(v); // 输出 hello async });
现在回过头来想下,如果 async 函数没有返回值,又该如何?很容易想到,它会返回 Promise.resolve(undefined)。
联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。42.引入样式和bootStarp
43.点击树形控件空白处时回到根目录
因此:如下图。给最外层包裹点击事件,给里面的标题栏添加阻止冒泡事件,给树形控件外面包裹一层div之后再给其加上阻止冒泡事件。
44.table组件每次只展开一行数据
/**
* 设置每次只能展开一行数据
* @param expanded 是否展开
* @param record 展开行数据
*/
onExpand = (expanded: any, record: { taskId: string }) => {
if (expanded) {
this.setState({ expandedRowKeys: [record.taskId] })
this.props.handleTaskOnClick(record.taskId)
} else {
this.setState({ expandedRowKeys: [] })
}
}
45.表格展开行的表格组件中选中文件再删除,弹出是否删除弹窗
/** 判断要删除的文件是否被选中,选中之后打开确认删除弹框 */
isSelectFile = (removeRowId: string) => {
const { expandRawDataSelectFile } = this.state
if (expandRawDataSelectFile.length < 1) {
message.warning(getLanguage('请先选择要删除的文件!'))
} else {
// 判断展开行中是否有 没有选择过文件或者有选择过但是又全部取消了的情况
let flag: boolean = expandRawDataSelectFile.some(ele => ele?.rawDataId === removeRowId && ele?.selectFile.length > 0)
if (flag) {
this.setState({ confirmVisible: true })
} else {
message.warning(getLanguage('请先选择要删除的文件!'))
}
}
}
/** 删除展开行选中的文件 */
removeExpandRowFile = (removeRowId: string) => {
// 将确认删除弹框关闭
this.setState({ confirmVisible: false }, () => {
const { expandRawDataSelectFile } = this.state
expandRawDataSelectFile.forEach(ele => {
if (ele.rawDataId === removeRowId) {
this.deleteFileRequest(removeRowId, 'removeFile', ele.selectFile)
}
})
// 删除文件之后取消对应的选中
this.fileSelection([])
// 导入完成之后刷新页面
setTimeout(() => {
this.expandRowRequest(removeRowId)
}, 1000);
})
}
/** 删除文件请求 */
deleteFileRequest = (taskId: string, modType: string, lsRawData: string[]) => {
let url: string = ''
if (this.props.type === 'pipeline') {
url = 'pipelineTaskOperation_saveInputParam'
} else {
url = 'taskOperation_saveInputParam'
}
$.post(
url,
{
taskId,
modType,
lsRawData
}
)
}
{
title: "",
className: "dataTable_Column",
dataIndex: "operate",
ellipsis: true,
key: "operate",
width: 30,
render: (text: any, record: NBCTaskDetailView, index: number) =>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Tooltip overlay={'文件导入'} placement='top'>
<Icon type="plus-circle" className="column_toolbar" onClick={() => this.isFileImportVisible(true, record)} />
</Tooltip>
<Icon type="question-circle" className="column_toolbar" onClick={e => { window.open(``) }} />
<span style={{ display: `${this.state.expandedRowKeys[0] === record.taskId ? '' : 'none'}` }} >
<Popconfirm title="确定要删除吗?" onConfirm={() => this.removeExpandRowFile(record.taskId)} visible={this.state.confirmVisible} >
<Tooltip overlay={'文件删除'} placement='bottom'>
<img className="toolbar-icon" src={Icons.Remvoe} onClick={() => this.isSelectFile(record.taskId)} />
</Tooltip>
</Popconfirm>
</span>
</div>
},
46.表格中展开也是表格(独立的表格组件)
/**
* 表格展开行
*/
expandedRow = (record: NBCTaskDetailView, index: number, indent: number, expanded: boolean) => {
return (
<InputFileExpand
setCurrentStep={this.props.setCurrentStep}
rawDataList={this.state.rawDataList}
projectId={this.props.projectId}
// expandedRowKey={record.taskId}
expandedRowKey={record.taskId}
expanded={ expanded}
ref={v => this.inputFileExpand = v}
fileSelection={this.fileSelection}
expandRawDataSelectFile={this.state.expandRawDataSelectFile} />
)
}
47.展开行表格中选中的文件
/**
* 选中展开行的文件,由于每个rawData的展开项都是一个个独立的table,所以为了储存所有可能选中的文件,只能将选中文件的操作放在当前inputFile类中
* @param selectionKeys
*/
fileSelection = (selectionKeys: string[] | number[]) => {
const { expandRawDataSelectFile, expandedRowKeys } = this.state
// 将之前同一展开行下的之前选中文件删除,避免重复存储
let newSelectFiles: {
rawDataId: string;
selectFile: string[];
}[] = expandRawDataSelectFile.filter(item => item.rawDataId !== expandedRowKeys[0])
newSelectFiles.push({ rawDataId: expandedRowKeys[0], selectFile: selectionKeys })
this.setState({ expandRawDataSelectFile: newSelectFiles })
}
componentWillReceiveProps(nextProps: Readonly<InputFileExpandProps>, nextContext: any): void {
if (this.props.expanded !== nextProps.expanded && nextProps.expanded) {
this.initRawDataFileList(nextProps.expandedRowKey)
}
// 设定选中文件受控的目的是 由于删除文件之后再加入相同文件会默认选中加入后与删除前同名的文件,这里需要对选择文件进行受控操作
if (this.props.expandRawDataSelectFile !== nextProps.expandRawDataSelectFile) {
nextProps.expandRawDataSelectFile.forEach(ele => {
if (ele.rawDataId === nextProps.expandedRowKey) {
this.selectFileArr = ele.selectFile
}
})
}
}
return (
<div className='expand-table'>
<Table
size="default"
loading={loading}
bordered={false}
rowClassName={() => "expandRowStyle"}
rowKey={(record) => record.id}
dataSource={expandRawDataFileList && expandRawDataFileList.find(rawDataEle => rawDataEle.rawDataId === expandedRowKey)?.rawDataResultFile.slice(current * pageSize - pageSize, current * pageSize)}
columns={this.expendedColumns}
pagination={false}
rowSelection={
{
onChange: (selectedRowKeys: string[] | number[], selectedRows: FileModel[]) => {
fileSelection(selectedRowKeys)
},
selectedRowKeys: this.selectFileArr
}
}
/>
<div className='footer-pagination'>
<FooterPagination getCurrentAndpageSize={this.getCurrentAndpageSize} paginationConfig={this.paginationConfig()} />
</div>
</div>
componentWillReceiveProps(nextProps: Readonly<InputFileExpandProps>, nextContext: any): void {
if (this.props.expanded !== nextProps.expanded && nextProps.expanded) {
this.initRawDataFileList(nextProps.expandedRowKey)
}
48.利用lodash遍历数组对象,返回一个对象。对象的key是对象数组中某一属性名,value是对象数组中所有符合这一属性名的对象组成的数组
// 其他版本列表
let otherIleVersionList: { [fileType: string]: SpeciesFileView[] } = _.groupBy(lsSpeciesFileView, speciesFileEle => speciesFileEle.fileType)
// 将对象的键全部放到otherIleVersionLable中
this.otherIleVersionLable = Object.keys(otherIleVersionList)
{this.otherIleVersionLable && this.otherIleVersionLable.map(otherIleVersionEle => {
return (<Form.Item label={
<span>
<Icon type="question-circle" style={{ color: '#1890ff' }}
onClick={e => { window.open(`https://docsite.novelbrain.com/doc/001_%E5%B8%AE%E5%8A%A9%E6%96%87%E6%A1%A3/05_%E5%BC%80%E5%8F%91%E8%80%85%E5%B7%A5%E5%85%B7/02.4_%E7%89%A9%E7%A7%8D%E7%AE%A1%E7%90%86--%E5%BB%BA%E7%B4%A2%E5%BC%95.md`) }}
/>
{otherIleVersionEle}
</span>
}>
{getFieldDecorator('otherFileVersion', {})(
<Select onSelect={null} onFocus={() => this.props.setCurrentStep(0)} onPopupScroll={(event) => this.preventSelectDefault(event)}>
{otherIleVersionList[otherIleVersionEle].map((data: any, index: number) => {
return <Select.Option key={index} value={data.fileVersion}>{data.fileVersion}</Select.Option>
})}
</Select>
)}
</Form.Item>)
})}

脚手架-类和函数组件
请求跨域问题
脚手架配置代理:客户端3000端口 给 服务端5000端口 发请求 ,中间设置一个代理人(没有ajax引擎:产生跨域的本质是ajax引擎拦截了请求 不存在跨域问题,同源策略不限制)
中间人的端口:为客户端的端口
客户端先给3000发 中间人接住请求 发给5000端口 没有跨域 5000端口返回数据
配置方法1:
package.json中加一个配置:”proxy”:”http://localhost:5000“
然后将自己的发送页面的发送目标地址改为自己的地址3000 先给自己发
axios.get(‘http://localhost:3000/students')先在自己端口里找没有students 才发给5000
并不是配置了代理,所有的请求都会通过代理,而是本地端口没有的请求才会询问代理端口。
但是这种方法只能解决一个请求 一个代理的问题
配置方法2:
可以配置多个代理
不用在package.json中写任何东西,在src下创建一个名叫setupProxy.js文件里面用 CJS的方式写:
const {createProxyMiddleware} = require(‘http-proxy-middleware’);
module.exports = function(app) {
app.use(
createProxyMiddleware(‘/api1’,{//遇见api1前缀的请求,就会触发该代理配置
target: ‘http://localhost:5000',//转发给5000
changeOrigin: true,//控制服务器收到的请求头中host的值 在服务器端可以输出 该请求来自于request.get(‘Host’)
pathRewrite: {‘^/api1’:’’}//将api1换成空格 重写请求路径 正则匹配
}),
createProxyMiddleware(‘/api2’,{
target: ‘http://localhost:5001',//下一个代理
changeOrigin: true,
pathRewrite: {‘^/api2’:’’}
})
)
}
前端代码中
axios.get(‘http://localhost:3000/api1/students')必须在3000后面写api1 如果3000下没有自己所要的数据 就走api1配置的路径下在5000中找
axios.get(/api1/search/users2?q=${keyWord}).then(response =>
console.log(‘成功了’,response.data)
,error=>{console.log(‘失败了’,error)}
)
消息订阅与发布:
订阅消息:1.消息名 2.发布消息 工具库PubSubJS
下载:cnpm install pubsub-js —save
使用:1.import PubSub from ‘pubsub-js’//引入
2. componentDidMount(){
this.token = PubSub.subscribe(‘state’,(,stateObj)=>{//state是消息名, 也是消息名占位符
this.setState(stateObj)
})
}
componentWillUnmount(){
this.PubSub.unsubscrib(this.token)
}//订阅 接收数据的组件去订阅消息,如果有人发布了state这个消息那么调用回调
token是可以通过这个值去清除此次订阅相当于id
PubSub.unsubscrib(token)取消订阅
3.PubSub.publish(‘state’,{isFrist:false,isLoading:true})//发布消息 发布消息的同时把数据带过去 把所有的状态数据放到展示数据的组件中,展示数据的组件中(谁接数据)只要一挂载就订阅消息,并且把数据处理放在消息订阅中,在componentWillUnmout中取消订阅.
发送请求除了XHR还有fetch fetch是关注分离的思路 返回的是promise对象 可以用两个await处理 第一个接受是否成功,第二个才接受成功的返回数据 最后用async和try catch处理
路由基本规则
SPA理解:整个应用只有一个完整页面,点击页面中的链接不会刷新页面,只会做页面的局部更新,数据都需要用ajax请求获取,并在前端异步展现<br /> 单页面多组件<br /> ** 路由原理:靠的就是BOM的history**<br />** 每一个地址对应一个组件 当我们点击页面的链接时 地址栏path会发生改变 但是页面不跳转 通过查看地址栏path 找到对应地址的组件加载到页面**<br />** 一个路由就是一个映射关系key就是path value就是组件conpenment或者函数**<br />** history有两种工作模式 **<br />**1.直接使用h5推出的BrowserRouter,主要是history API **<br />**2.hash值(锚点)url中有#号使用的是URL的哈希值,兼容性更好**<br />** react-xxx都是react插件库**<br /> <br /> 基本使用:<br /> 1.明确好界面中的导航区、展示区 <br /> 2.展示导航区的a标签改为Link标签 <br /> 3.展示区写Route标签进行路径匹配 <br /> 4.App的最外侧包裹了一个<BrowserRouter></BrowserRouter><br /> 将所有的组件都交给一个路由器去管理 所以要在index入口页面引入路由import{BrowserRouter} from 'react-router-dom'<br /> <BrowserRouter><App/></BrowserRouter>,document.getElementById('root') <br /> 在点击切换页面 实现切换路由 和注册路由<br /> 在App组装页面将 路由组件和注册路由组件部分写成以下形式:<br /> import {Link,Route,Routes} from 'react-router-dom'<br /> 切换路由 <LinkclassName="list-group-item"to="/about">About</Link><br /> <LinkclassName="list-group-item"to="/home">Home</Link><br /> 其实Link标签最终被解析为a标签<br /> 注册路由:(V6版本)<br /> <Routes><br /> <Routepath="/about"element={<About/>}> </Route><br /> <Routepath="/home"element={<Home/>}> </Route><br /> </Routes><br /> (V5版本)<br /> <Routepath="/about"component={About}/><br /> <Route path="/home" component={Home}/><br /> 哈希router地址会出现一个# #后面的参数不会发给服务器<br />路由组件放在pages文件夹中 路由组件是路由器帮我们渲染的 路由组件会收到 路由器给传递的三个重要信息:history location match<br /> 一般组件与路由组件的异同:<br /> 1.写法不同 2.放置位置不同pages 3.接收到的props不同<br /> 路由组件的props上有<br />** history:go(),goBack(),goForward(),push(),replace()**<br />** location:pathname:'/about',search:'',state:undefined**<br />** match:params:(),path:'/about',url:'/about'**
location对象
history对象
history.go() 可以传递数字也可以传递字符串
//传递字符串的的话 有可能前进,也有可能后退
history.go(“baidu.com”) //跳转到距离当前页最近的baidu.com网页
另外还有两个简写的history.go()方法
history.back() //等于history.go(-1)
history.forward() //等于history.go(1)切换路由的高亮
import {NavLink} from’react-router-dom’
NavLink可以实现路由链接的高亮,list-group-item他两是标签本身统一的类名 activeStyle是高亮类名,当点击哪个时 activeStyle就会给谁加到身上 加到统一类名的后面
V6:
V5:
其他路由配置
封装NavLink组件:
单独写一个MyNavLink(父)组件 暴露出去,这个组件里面还要用NavLink(子)标签
在NavLink中传一个to=”/About” 和一个title
在MyNavlink封装中要接受一个this.props.to 和一个this.props.title表示往哪里跳转和导航栏的名字
但是要将MyNavLink不写成自结束的标签 要写成普通的标签形式 ,有一个特点是 标签体内容也可以通过props传过去是一个特殊的标签体属性 比如在about
标签体也是一个特殊的标签属性:标签体就是中的about,可以这样写:
。
about也会在NavLink中通过props中接收到会放在一个自动创建的key为children上 所以可以将NavLink写成自结束标签 所有的属性通过 {…this.props}收到
通过this.props.children可以获取 到标签体内容Switch组件:

如果有两个path一样的路由,那么这两个匹配的路由对应的组件都会显示:
用switch之后只会在一个路径里匹配一次
导入Switch组件 from ‘react-router-dom’
将所有注册的路由用Switch组件包起来 注册路由数量在一个以上再包
但是v6版本已经解决了这个问题,所以不再写Switch标签
(V5版本)
解决样式丢失问题:
问题:当路由是多级结构时 一点刷新样式可能会丢失 是因为会认为多级结构中的目录名都是 这个主机名下的地址 但是如果你所给的路径不存在的话 ,他会在你给出的主机名路径下去寻找,找不到这个路径从而展示index页面兜底
解决:办法1.在index.html中去掉引用css路径的点 href=”/css/bootstrap.css”意思就是不在当前路径下去寻找,不写的话就是直接去localhost:3000下去请求
办法2.在/css的/前面加%PUBLIC_URL%最终为 %PUBLIC_URL%/css/bootstrap.css 由于用这个的话css的路径是绝对路径 所以请求css路径时不会有问题
办法3在入口index.js中用HashRouter包裹 路径后面会出现# #后面的东西不会再看 所以请求是成功的
模糊匹配
如果页面东西正常不要开启严格匹配,,如果出现诡异状况 好多东西都往一个地方跳 则是模糊匹配引发的问题 ,开启严格匹配。v5版本用exact开启精准匹配,默认模糊匹配 ,v6有了Routes后,默认开启精准匹配,但是加”/*”可以开启模糊匹配
Redirect:重定向
当页面打开导航栏默认勾选一个借助 react-dom内置组件Redirect,将Redirect组件放在注册路由的最下面 指定一个路径
如果Redirect上面的路由匹配不上 就走Redirect指定的to=”about” 自结束标签 也就是一打开的默认勾选的组件,
但是v6的写法为:
导入Navigate,
我们一般将他写在注册路由的最下方,当其他所有路由都无法匹配时,那么自动跳转到它所指定的路由
v5: 多级路由
react中路由的注册是有顺序的,页面一打开看到的主导航的路由注册先注册,每当你点击一个路由链接要改地址的时候也就是说要跳转到当前主导航路由下的下一个路由,系统都会从最开始注册的路由进行逐层匹配,也就是说当第二次改变路由地址时 和任何一个注册的路由都没有匹配上,就会跳转到那个兜底的路由链接上。路由的匹配都是从最开始注册 到最后注册这个流程走下去的。
输入的是/home/news之后会在主导航栏里进行注册路由的匹配 主导航里有home所以模糊匹配上了,所以展示home组件 所以主导航栏home组件内的内容才没有丢失,所以home组件挂载上去,然后又注册路由 再进行匹配注册路由/home/news 完全匹配。
所以当开启严格匹配模式时不能匹配多级路由,主导航栏的home组件也匹配不上 默认展示重定向的兜底路由。
总而言之,子组件的注册路由地址中要将父组件的地址带着
V6改了,一级注册路由在path后面加/(”/“可以开启模糊匹配),二级导航和老师一样路径写全,二级注册路由path只用写子路由
有问题时以下面写法为主
//一级注册
App中
//二级导航
Home中
//二级注册
{/ 重定向 /}
也可以在APP中直接写:
V5:
Home中
路由组件间的参数传递
传递params参数
向路由组件传递params参数,首先在声明组件的时候要先传递,(id和title)
其次在注册路由时要接收
v5:
最后在要展示的组件中接收this.props.match.params中解构赋值拿到
//接收params参数
const { id, title } = this.props.match.params
find() 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。
传递search参数
key = value && key = value 这种编码方式为urlencoded编码方式
它的作用就是在对象形式的写法和 urlencoded写法之间能够互相转化,如下:
let obj = {name:’qcq’,age:24}
console.log(qs.stringify(obj))将转换为urlencoded的形式
将urlencoded编码形式写为对象形式为 qs.parse(urlencodedObj)
1. 向路由组件传递search参数 /?id=xxx&title=xxx
2. search参数无需声明接收,因为有?这个参数存在 所以正常注册路由即可
v5:
3. 如何接收search参数
const {search} = this.props.location
const { id, title } = qs.parse(search.slice(1))
传递state参数
参数不会在地址栏体现比较安全,由于我们使用的是browserRoute 它一直在操作history,所以会一直帮我们记住所传递的参数 所以点击刷新之后数据依然存在
location.state默认开始是undefined
但是我们在清理缓存的时候 它从state上读的参数是undefined 所以会报错,我们要进行空值判断,如果清理缓存后state真的没有值那么不会报错
刷新也能保留参数,其他两个传递参数的方式 在路径中显示参数 刷新不会丢失参数
1. 向路由组件传递state参数
2. state参数无需声明接收, 所以正常注册路由即可
v5:
3.接收state参数:
const {id, title} = this.props.location.state || {}
push能够留下痕迹 默认开启push模式,想开启replace模式的话 直接在导航显示的地方加一个replace就可以 浏览页面不留痕迹
编程式路由
想开启replace模式的话(替换掉路径栈顶) 直接在路由链接的地方加一个replace就可以 浏览页面不留痕迹
2.编程式路由导航:
history是路由组件上的api
点击按钮后实现replace或push跳转用this.props.history.replace+三种传参方式或者this.props.history.push+三种传参方式:
this.props.history.replace(/home/message/detail/${id}/${title}) 携带params参数(在单击事件 的函数中 传值,函数中的这一句相当于导航链接)
点击事件onClick函数中传id和title用柯里化
当选择的传参方式为state时 push(path,state)和 replace(path,state)方法中可以携带state参数 如下方法中写:
this.props.history.replace(/home/message/detail,{ id, title})
注册路由和声明接收的方式与三种方式传参一样
前进和后退以及跳转:
方法中写this.props.history.goForward()和
this.props.history.goBack()和
this.props.history.go()
新的需求当页面一打开等两秒自动跳转到指定组件:当组件一挂载时开启定时器 在里面this.props.history.push(/home/message/detail)
withRouter 一般组件也要路由组件上的属性

将路由组件上控制子组件 前进回退的按钮放在一般组件中的父组件上时不会有作用,因为一般组件上面没有history和location等,它的props是空对象。
在一般组件上导入withRouter:
import {withRouter} from ‘react-router-dom’
改变导出组件方式:export default withRouter(Header)
withRouter的作用:能够接受一般组件,然后会在一般组件的身上加上路由组件所特有的三个特殊的api,返回值是一个新组件。
其他的写法和在路由组件中的写法相同,都会具有history上的go(),back(),forward()等属性
export default withRouter(Header)
//withRouter可以加工一般组件,让一般组件具备路由组件所特有的API
//withRouter的返回值是一个新组件
v6已经没有了withRoute的写法hashRoute and browserRoute
1.底层原理不一样:<br /> BrowserRouter使用的是H5的history API,不兼容IE9及以下版本。<br /> HashRouter使用的是URL的哈希值。<br /> 2.url表现形式不一样BrowserRouter的路径中没有#,例如:localhost:3000/demo/test<br /> HashRouter的路径包含#,例如:localhost:3000/#/demo/test<br /> 3.刷新后对路由state参数的影响<br /> (1).BrowserRouter没有任何影响,因为state保存在history对象中。<br /> (2).HashRouter刷新后会导致路由state参数的丢失。<br /> 4.备注:HashRouter可以用于解决一些路径错误相关的问题。
lazyLoad-优化
将需要进行懒加载的组件用如下方式导入
import React,{Component, lazy} from ‘react’
const Home = lazy(() => import(‘./pages/Home/Home’))
const About = lazy(() => import(‘./pages/About/About’))
如果还未加载出来 要给定加载中的样式
{/ 注册路由 路由组件放在pages中 /}
PureComponent-优化
Component的2个问题
1.只要执行setState(),即使不改变状态数据,组件也会重新render() ==>效率低
2.只当前组件重新render(),就会自动重新render子组件,纵使子组件没有用到父组件的任何数据==>效率低
效率高的做法只有当组件的state或props数据发生改变时才重新render()
原因Component中的shouldComponentUpdate()总是返回true,相当于更新阀门
解决办法1:
重写shouldComponentUpdate()方法比较新旧state或props数据,如果有变化才返回true,如果没有返回false
shouldComponentUpdate(nextProps,nextState){
if( this.state.name === nextState.name){
return false
}else{
return true
}
}
办法2: 使用PureComponent
PureComponent重写了 shou1dComponentUpdate(), 只有state或props数据有变化才返回true
注意:
只是进行state和props数据的浅比较,如果只是对象内部数据变了(还是之前哪个对象,只是改了属性值),返回false。不要直接修改state数据,而是要产生新数据。项目中一般使用PureComponent来优化
情况1:
state = {name:’qcq’}
const obj = this.state
obj.name = ‘lr’
setState(obj)
这里的obj还是之前的哪个对象,是之前的state PureComponent阀门会自动返回false
必须如下这样改,{}里面创建的是一个新对象,与之前的 state={name:’qcq’}地址不同
this.setState({name:’lr’})
如果需要进行添加 需要用这种方式,不能用unshift 因为这种方式 虽然改变了原数组,但是只改变了值,数组的地址没有变。应该产生新的数组 用如下方式,或者新声明一个数组 在外面添加 添加完了再setState
const {name} = this.state
this.setState({name:[‘xiaoLiu’,…this.state.name]})
Render props(插槽技术)
Vue中:
使用s1ot技术,也就是通过组件标签体传入结构
React中:
使用children props:通过组件标签体传入结构
使用render props:通过组件标签属性传入结构,而且可以携带数据,一般用render函数属性
children props:
xxxx
{this.props.children}
问题:如果B组件需要A组件内的数据,==>做不到
render props:
App中:给A组件先传一个函数,函数返回的是B组件,A组件将要传给B组件的数据 先通过函数先给App组件 再传给B组件,B组件就拿到了A组件的数据
< A render={(data)=>{return }}>
给A组件当中的数据传到B中,这里要的是App中函数的返回值,返回的是B组件 将B组件放在这里
A组件:{this.props.render(this.state.data)}
B组件:读取A组件传入的数据显示{this.props,data}
类似于插槽技术:在一个组件的指定位置 预留好一个位置,把第三方组件放在这个位置 并传递给它数据。 在A组件的指定位置预留好 放B组件的地方 并把要准备传出的数据传出。那么在App中就直接指定要在A组件中放哪个插件,那个需要接收A组件的什么值
conText 常用于封装插件
一般来说相邻两个父子组件之间 用state和props直接通信,涉及到父组件给孙组件之间的通信可用此方式,可直接绕开子组件。
方法:
1.创建组件上下文文件:
import React from ‘react’;
//创建context对象
export const myContext = React.createContext()
2.父组件引入myContext
//导入myContext的声明
import {myContext} from ‘./myContext’
将要传递的后代组件用myContext.Provider 标签包裹,将要传递的信息 state解构赋值后,放在myContext.Provider的value中,此后B组件及其后代组件都可以访问的到。
3.后代组件接收(类组件专用)
//导入myContext的声明
import {myContext} from ‘./myContext’
// 接收myContext
static contextType = myContext
//显示A组件数据
{this.context.name}
类组件和函数组件通用方式接收:
//导入myContext的声明
import {myContext} from ‘./myContext’
//用
{
value=>{
return 我是C组件,A组件的姓名是${value.name},年龄是${value.age}
}
}
Fragment
它的用法和<></>相似 只是在<>里面不能写属性,在进行遍历的时候用脚手架-函数式组件
state-Hook
(2).
语法:const[name,setName] = React.useState(initValue)
(3).
useState()说明:
参数:第一次初始化指定的值在内作缓存返回值:包含2个元素的数组,第1个为当前状态值,第2个为更新状态值的函数
(4),setXxx2种写法:
setName(‘Jack’):参数为非函数值,直接指定新的状态值,内部用其覆盖原来的状态值
setName(name => name=’Jack’):参数为函数,接收原本的状态值,返回新的状态值,内部用其覆盖原来的状态值
Effect-Hook
(2). React中的副作用操作:发ajax请求数据获取设置订阅/启动定时器手动更改真实DOM
(3).语法和说明:
会传入一个函数,相当于一个生命周期钩子
React.useEffect (() => {//在此可以执行任何带副作用操作
return () => {
// 在组件卸载前执行 函数中所返回的那个函数就是componentwillUnmount
//在此做一些收尾工作,比如清除定时器/取消订阅等
}
}, [stateValue]) // 如果指定的是[],回调函数只会在第一次render ()后执行
如果完全不写第二个参数,则所有的可变参数都会受到监视。
如果[]中没有值,空数组。谁也不检测,相当于一挂载就显示 显示1次 componentDidMount
如果[]有一个参数,则只会监视一个,一挂载就显示,参数改变也显示。显示n+1次 componentDidupdate
函数中所返回的那个函数就是componentwillUnmount
(4).可以把useEffect Hook 看做如下三个函数的组合
componentDidMount()
componentDidupdate()
componentwillUnmount()Ref-Hook
(2). 语法: const myRef = React.useRef() 取出数据myRef.current.value 标签:
读取值:myRef.current.value
(3). 作用:保存标签对象,功能与React.createRef()一样V6-Router
路由基本规则
路由原理:靠的就是BOM的history 每一个地址对应一个组件 当我们点击页面的链接时 地址栏path会发生改变 但是页面不跳转 通过查看地址栏path 找到对应地址的组件加载到页面
一个路由就是一个映射关系key就是path value就是组件conpenment或者函数
history有两种工作模式 1.直接使用h5推出的BrowserRouter,主要是history API
2.hash值(锚点)url中有#号使用的是URL的哈希值,兼容性更好
react-xxx都是react插件库
基本使用:
1.明确好界面中的导航区、展示区
2.展示导航区的a标签改为Link标签
3.展示区写Route标签进行路径匹配
4.index.js中App的最外侧包裹了一个
将所有的组件都交给一个路由器去管理 所以要在index入口页面引入路由import{BrowserRouter} from ‘react-router-dom’
在点击切换页面 实现切换路由 和注册路由
在App组装页面将 路由组件和注册路由组件部分写成以下形式:
import {Link,Route,Routes} from ‘react-router-dom’
切换路由
其实Link标签最终被解析为a标签
注册路由:(V6版本)
哈希router地址会出现一个# #后面的参数不会发给服务器
路由组件放在pages文件夹中 路由组件是路由器帮我们渲染的 路由组件会收到 路由器给传递的三个重要信息:history location match
一般组件与路由组件的异同:
1.写法不同 2.放置位置不同pages 3.接收到的props不同
路由组件的props上有
history:go(),goBack(),goForward(),push(),replace()
location:pathname:’/about’,search:’’,state:undefined
match:params:(),path:’/about’,url:’/about’useRouter-Hook 路由表(函数式组件)
也可以在另外的页面封装:
高亮封装MyNavLink:(activeStyle是自己指定的高亮类名)
在MyNavLink组件中 引入NavLink,用this.props接收即可:
展示导航的使用方式:
路由表代码:
//路由表 键值对
export const routers = [
{
path:’/about’,
element:
},
{
path:’/home’,
element:
},
{
path:’/‘,
element:
}
]
App中:
// 根据路由表生成对应的路由规则
const element = useRoutes(routers)
// 在注册路由的地方 调用element即可
{element}
Navigate:只要Navigate被渲染就会引起视图的切换 修改路径,Navigate里面还可以指定跳转的形式
NavLink的高亮方式:
参数是一个对象{isActive:true},当点击他所对应的导航时,参数isActive为true,如果点击的是另外一个导航时 isActive为false,所以下面解构取出isActive。当这个导航亮时isActive为true,那类名除了list-group-item之外,再加上 我们设置的activeStyle类,否则加空。
也可以在本页面将NavLink的类名封装起来,直接调用即可:
function MyNavLink({ isActive }){
return “list-group-item” + (isActive ? “ activeStyle” : “”)
}路由表子路由+其他配置
路由导航 路径to只需要写组件路径,不用/ 和写父路由
{/ 只写当前路径,前面有父级路径不需要写/,否则会在根目录下找 /}
路由表中将所有的子路由 放在对应的父级路由下:
export const routers = [
{
path:’/about’,
element:
},
{
path:’/home’,
element:
//Home组件下的子组件,前面有父级路径不需要写/,否则会在根目录下找
children:[{
path:’news’,
element:
},{
path:’message’,
element:
}]
},
{
path:’/‘,
element:
}
]
在需要放子路由的地方,用Outlet呈现
{/ 注册路由,Outlet指定路由组件呈现的位置 注意有子路由的组件/}
函数式路由组件间的参数传递
传递Params参数(useParams()-Hook)
1.父组件中导航连接,将消息用/分割:
{items.title}
2.注册路由时接收参数在路由表中 用/:分割: 在注册路由的地方用
// message下的子组件detail,给路由组件传递数据
children:[{
path:’detail/:id/:title/:content’,
element:
}]
3.在子组件中用useParams()接收params参数:
路由表参数的接收方式:
exportconstrouters = [
{
path:’/about’,
element:
},
{
path:’/home’,
element:
//Home组件下的子组件,前面有父级路径不需要写/,否则会在根目录下找
children:[{
path:’news’,
element:
},{
path:’message’,
element:
// message下的子组件detail,给路由组件传递数据
children:[{
path:’detail/:id/:title/:content’,
element:
}]
}]
},
{
path:’/‘,
element:
}
]
传递search参数(useSearchParams()-Hook)
1.父组件中导航连接,将消息用?id=${items.id}&title=${items.title}分割:
{items.title}
2.注册路由时不用接收参数在路由表中 在注册路由的地方用
3.在子组件中用useSearchParams接收search参数:
//useSearchParams()的用法形似于useState(),search参数指的是里面的参数,setSearch指的是操作参数的方法。
const [search,setSearch] = useSearchParams()
// 读取参数值用search.get(‘参数名’)
{search.get(‘id’)}
// 更新参数
function updateSearch(){
setSearch({id:’005’,title:’消息5’,content:’qcq’})
}
传递state参数 (useLocation()-Hook)
1.父组件中导航连接,将消息用state={{id:items.id,title:items.title,content:items.content}} 用对象传递:
{items.title}
2.注册路由时不用接收参数在路由表中 在注册路由的地方用
3.在子组件中用useLocation接收state参数:
// 用useLocation().state接收Message组件传递的数据
//useLocation()上面可以拿到search和state参数,这个取决于当时的参数是采用哪种方式传递的
传递state参数时的读取方式:
编程式路由导航(useNavigate()-Hook)
而Navigate必须要渲染到页面上才能实现路由的跳转和视图的切换。
使用useNavigate()实现编程式的路由导航,可以实现点击button 再跳转到对应的路由组件,也可以一边跳转并传过去数据,还可以实现push和replace跳转,也可以前进和后退
目前只支持state参数传递,如果需要传递params和search参数 需要在路由导航的路径里面带入
navigate(‘detail’, 在’’里面写参数 同时如果携带params参数 在注册路由时需要接收
// 用useNavigate()进行编程式路由导航,可以指定跳转方式,携带参数,还可以前进后退
const navigate = useNavigate()
function showDetail(items){
navigate(‘detail’,
{
replace:false,
state:{
id:items.id,
title:items.title,
content:items.content
}
})
}
前进后退:
// 编程式路由导航 useNavigate进行前进和后退
const navigate = useNavigate()
function back(){
navigate(-1)
}
function forward(){
navigate(1)
}
其他路由Hook
如果处于上下文 就会返回true否则脱离路由器测管理则返回false。
useNavigationType() 返回当前的导航类型(用户是如何来到当前页面的)
返回值:pop刷新页面来到的(在浏览器中直接打开了这个页面来到的) push replace
useOutlet() 作用:用来呈现当前组件中的嵌套路由组件。 当嵌套的组件没有挂载时会输出null,当挂载了之后,会输出 嵌套组件的 组件对象相关信息。
useResolvedPath()用来解析路径的 当点击了对应的组件 会输出里面的参数路径
脚手架搭建过程
浏览器插件开发者工具安装react_dev_tools
vscode插件:名称: Reactjs code snippets 代码片段
ES7+ React/Redux/React-Native snippets脚手架代码片段
fehelper前端助手 浏览器插件
1.全局安装 cnpm install -g create-react-app
2.切换到想创项目的目录 create-react-app myReact
3.进入项目文件夹下的终端启动 npm start
Success
yarn start开启开发者服务器 输入命令后自动打开浏览器 自动打开页面
yarn build 把写完的项目最后一次打包 写完项目之后执行生成静态文件
yarn eject 默认将所有webpack相关配置文件隐藏 怕破坏,用了这个命令后会把相关的所有文件暴露出来
安装axios npm install axios
安装v5:npm i react-router-dom@5
antd按需引入和自定义样式:cnpm install @craco/craco 和 cnpm install craco-less
打包运行redux项目 cnpm run build
serve这个库可以让指定文件夹快速开启一个服务器,把某一个文件夹作为开启服务器的根目录
cnpm install serve -greact+ts脚手架搭建过程
最近一直想搭建个集成React,TypeScript的开发环境,但是无从下手,一番摸索后总算折腾出来了,记录下步骤。
$ cd react_typescript
有了脚手架,然后需要集成typescript,查阅官方文档从Javascript迁移到Typescript,步骤如下:
根目录下创建配置文件tsconfig.json,写入以下内容:
“compilerOptions”: {
“target”: “es5”,
“lib”: [
“dom”,
“dom.iterable”,
“esnext”
],
“allowJs”: true,
“skipLibCheck”: true,
“esModuleInterop”: true,
“allowSyntheticDefaultImports”: true,
“strict”: true,
“forceConsistentCasingInFileNames”: true,
“module”: “esnext”,
“moduleResolution”: “node”,
“resolveJsonModule”: true,
“isolatedModules”: true,
“noEmit”: true,
“jsx”: “preserve”
},
“include”: [
“src”
]
}
“compilerOptions”: {
//输出目录为build
“outDir”: “./built”,
//接受js作为输入
“allowJs”: true,
//转换为es5
“target”: “es5”
//下面为可选的
//模块引用方式为commonjs
“module”: “commonjs”,
//用mode进行模块解析
“moduleResolution”: “node”,
//使用sourceMap
“sourceMap”: true,
//启用实验性的metadata API
“emitDecoratorMetadata”: true,
//启用实验性的装饰器
“experimentalDecorators”: true,
//不删去注释
“removeComments”: false,
//不启用严格检查
“noImplicitAny”: false
},
“include”: [
//读取src目录下的所有文件
“./src/*/“
]
}
webpack.config.js设置如下(要把awesome-typescript-loader放在ts的所有loader之前)。
entry: “./src/index.ts”,
output: {
filename: “./dist/bundle.js”,
},
// Enable sourcemaps for debugging webpack’s output.
devtool: “source-map”,
resolve: {
// Add ‘.ts’ and ‘.tsx’ as resolvable extensions.
extensions: [“”, “.webpack.js”, “.web.js”, “.ts”, “.tsx”, “.js”]
},
module: {
loaders: [
// All files with a ‘.ts’ or ‘.tsx’ extension will be handled by ‘awesome-typescript-loader’.
{ test: /.tsx?$/, loader: “awesome-typescript-loader” }
],
preLoaders: [
// All output ‘.js’ files will have any sourcemaps re-processed by ‘source-map-loader’.
{ test: /.js$/, loader: “source-map-loader” }
]
},
// Other options…
};
集成了React,Typescript的脚手架到此就搭建成功了,修改App.js为App.tsx,出现很多语法错误,是因为Typescript没有找到react,react-dom等依赖的类型声明,安装对应的类型声明模块:
安装完各种声明模块之后,npm start运行,http://localhost:3000出现界面:
我的依赖版本如下:
“@types/jest”: “24.0.12”,
“@types/node”: “11.13.8”,
“@types/react”: “^16.9.11”,
“@types/react-dom”: “16.8.4”,
“@types/react-router-dom”: “^5.1.0”,
“awesome-typescript-loader”: “^5.2.1”,
“react”: “^16.11.0”,
“react-dom”: “^16.11.0”,
“react-router-dom”: “^5.1.2”,
“react-scripts”: “3.2.0”,
“source-map-loader”: “^0.2.4”,
“typescript”: “^3.6.4”
}
配置文件如下:
webpack.config.jsTs常用类型
// 平时我们存储男、女这些字符串,数据库占用空间大,像这种在几个值之间选择的情况,可以用枚举替代字符串
// 当然,我们可以不写值,此时Male、Female默认0、1
enum Gender {
Male = 1,
Female = 0
}
let i: { name: string, gender: Gender } = {
name: ‘Allen’,
gender: Gender.Male
}
console.log(i.gender === Gender.Male); //true
console.log(Gender[‘Male’]); //1
console.log(Gender[1]);
//一般来说,我们对于对象的类型检查一般是检查对象中有哪些属性,属性是什么类型。可以用{} 来指定对象中可以包含哪些属性 语法:{属性名: 属性类型, …}
let person:object
person={
name:’qcq’,
age:18
}
// person = 1//会报错
//定义对象
let obj1:{
name:string,
age:number
}={
name:’qcq’,
age:18
}
//可选属性,在属性名后面加一个 ?(可以对这个属性赋值,也可以不赋值)
let obj2:{
name:string,
age?:number
}
obj2 = {name:’lr’,age:20}
obj2 = {name:’ha’}
return a+b
}
fun1(1,2)
//数组类型 用来限制数组元素的类型
// 比较常用的就是这种方式: 在元素类型后面接上 []:
const arr:number[] =[1,2,3,4]
const arr2:string[] =[‘1’,’2’,’3’]
//可以给一个变量定义一个联合类型,类型名之间用 | 隔开 。这个时候,字面量类型就有用了,此时sex只能被赋值成两个值
let sex:’male’ | ‘female’
sex = ‘male’
sex = ‘female’
// sex = ‘females’//会报错
// 在typescript基础中,我们学习到了自定义类型的写法;
name: string,
age: number
}
const obj13: myType2 = {
name:”allen”,
age:18
}
// (1)接口 VS 自定义类型 VS抽象类
// 1.接口可以同名进行重复声明:比如之前定义了 type myType,后面不能重复定义该类型;而前面个定义了 interface myInterface,后面依旧可以再次定义 interface myInterface(这两个 myInterface会进行合并)
name: string;
age:number
}
interface myInterface{
gender:string
} //两个会发生合并,这种语法在TS里是合理的
// 2.接口可以在定义类的时候,限制类的结构(这一点有点像在继承抽象类)
// 接口中的方法都是抽象方法(但是抽象类可以有非抽象方法)
// 定义类时让类去实现(implement)这个接口
name: string;
saySomething(): void; //抽象方法
}
// 实现接口,实现接口就是使类满足接口要求
// class MyClass implements myInterface{
// name: string;
// constructor(name:string) {
// this.name = name;
// }
// saySomething(): void {
// throw new Error(“Method not implemented.”);
// }
// }
// 1.接口可以同名进行重复声明:比如之前定义了 type myType,后面不能重复定义该类型;而前面个定义了 interface myInterface,后面依旧可以再次定义 interface myInterface(这两个 myInterface会进行合并)
name: string;
age:number
}
interface myInterface{
gender:string
} //两个会发生合并,这种语法在TS里是合理的
// 2.接口可以在定义类的时候,限制类的结构(这一点有点像在继承抽象类)
// 接口中的方法都是抽象方法(但是抽象类可以有非抽象方法)
// 定义类时让类去实现(implement)这个接口
name: string;
saySomething(): void; //抽象方法
}
// 实现接口,实现接口就是使类满足接口要求
// class MyClass implements myInterface{
// name: string;
// constructor(name:string) {
// this.name = name;
// }
// saySomething(): void {
// throw new Error(“Method not implemented.”);
// }
// }
抽象类就是其他类的父类就是被用来继承的
接口就是用来定义 类的结构
interface开头 myinterface{
name:string,
age:number
}
规定类的结构 和内部属性 类中应该包含哪些属性和方法 接口也可以当成类型声明去使用 接口可以重复声明
限制类的结构 所有属性都不能有实际的值 只定义对象的结构 而不考虑实际值 像似于抽象类
但抽象类可以有普通方法也可以有抽象方法 是需要继承extends的 而接口不能有自己的方法是实现的implement
用类实现接口 就是让类满足接口的要求 接口就是定义的一个规范
属性可以被任意修改将会导致对象中的数据变得非常不安全
私有属性可以在类内部使用get和set方法去操作属性 可以在set方法里去设置 当这个值小于0时 不让设置if(value>=0){this._age = value}
procted只能在类中访问 包括当前类和当前类的子类如何使用promise
new Promise(excutor) excutor称为执行器函数,在Promise内部是同步调用的(如图)
Promise构造函数: Promise (excutor) {}
Promise.prototype.then方法: (onResolved, onRejected) => {}
Promise.prototype.catch方法: (onRejected) => {}(只能执行失败的回调,不能执行成功的回调,相当于半个then方法)
Promise.resolve方法: (value) => {}(是属于promise函数对象的,并不属于实例对象。作用是为了更快的得到promise对象,而且
还能封装一个值,将这个值转化为promise对象;结果:非promise类型的对象则返回成功的promise,如果是promise类型则返回结果由promise类型决定)
Promise.reject方法: (reason) => {}(描述同上,不同的是它返回的一直都是失败的promise类型的对象,失败的值为你传入的参数,
即便传入的是成功的promise对象,结果也是失败)
Promise.all方法: (promises) => {}(接收一个promise数组,返回的结果是一个promise对象,只有所有的promise对象都是成功的才会返回成功的promise数组,
否则有一个失败,则返回的结果是失败的那个promise对象的状态和结果)
Promise.allSettle方法:(promises)=> {}(promise.allSettled:接收一个promise数组,返回的结果是一个promise对象,也永远是一个成功的状态,
里面的值是每一个promise对象成功的状态和结果。)
promise.allSettled和promise.all都可以做批量异步任务的场景,如果想得到每个任务的结果 用promise.allSettled,如果想每个任务都成功才能执行下去 用promise.all
Promise.race方法: (promises) => {}(它的结果由第一个改变状态的promise对象来决定,接收也是数组)
如何改变promise的状态?1.resolve() 2.reject() 3.throw
一个promise指定多个成功/失败回调函数, 都会调用吗? 当promise改变对应的状态时都会调用,不改变状态不调用(指定回调就是then方法)
promise.then()返回的新promise的结果状态由什么决定?由指定回调函数(then内部的回调函数)的执行结果决定
改变promise状态和指定回调函数谁先谁后? 都有可能 1.先改变状态后指定回调:当执行器函数里面是一个同步任务时(里面是resolve()和reject()时),
当指定回调更晚执行时 2.先指定回调后改变状态:当执行器函数是个异步任务时(加了定时器)
什么时候拿到数据? 指的是什么时候执行回调代码 1.当先改变状态后指定回调时立即执行回调代码 2.当先指定回调后改变状态时,
状态改变完之后执行回调代码。
promise如何串连多个操作任务? 通过then的链式调用串联多个同步异步任务
promise异常传(穿)透? 当使用了promise的then链式调用时,可以在最后指定失败的回调(一般是catch方法),
前面任何操作出了异常,都会传到最后失败的回调中处理
中断promise链?.then().then().then()就是promise链 在回调函数中返回一个pending状态的promise对象(因为状态不改变,后面的回调都不能执行)
return new Promise(()=>{})即可
JS引擎首先必须先执行所有的初始化同步任务代码
每次准备取出第一个宏任务执行前, 都要将所有的微任务一个一个取出来执行
async函数内部return语句返回的值,会成为then方法回调函数的参数。
for await…of方法被称为异步迭代器,该方法是主要用来遍历异步对象。他是ES2018中引入方法。
for await…of 语句会在异步或者同步可迭代对象上创建一个迭代循环,包括 String,Array,类数组,Map, Set和自定义的异步或者同步可迭代对象。这个语句只能在 async function内使用。
function Gen (time) {
return new Promise((resolve,reject) => {
setTimeout(function () {
resolve(time)
},time)
})
}
let arr = [Gen(2000),Gen(100),Gen(3000)]
for await (let item of arr) {
console.log(Date.now(),item)
}
}
test()form和ajax表单提交方式的区别
一般这种异步的操作,我们都会想到ajax方式,因此在实现了功能后,就整理了这篇文章,通过ajax方式实现form表单的提交并进行后续的异步操作。
常见的form表单提交方式
<!DOCTYPE HTML PUBLIC “-//W3C//DTD HTML 4.01 Transitional//EN”>
ajax实现form提交方式
修改完成后代码如下:
<!DOCTYPE HTML PUBLIC “-//W3C//DTD HTML 4.01 Transitional//EN”>
](http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js">)
1、两种方法的路由都是/users/login,如果后台服务在路由函数的最后return render_template(‘index.html’),那么form表单提交会自动跳转到index页面,而ajax提交页面不会跳转,只是会返回这个页面,从fiddler中抓包可以在webview中看到index页面
3、form中的请求参数url在
不过传统的form表单提交会导致页面刷新,但是在有些情况下,我们不希望页面被刷新,这种时候我们都是使用Ajax的方式进行请求的。
通常我们提交(使用submit button)时,会把form中的所有表格元素的name与value组成一个queryString,提交到后台。这用jQuery的方法来说,就是serialize。Ajax的方式进行请求:
但是上述方式,只能传递一般的参数,上传文件的文件流是无法被序列化并传递的。
不过如今主流浏览器都开始支持一个叫做FormData的对象,有了这个FormData,我们就可以轻松地使用Ajax方式进行文件上传了。关于FormData及其用法
XMLHttpRequest Level 2添加了一个新的接口FormData.利用FormData对象,我们可以通过JavaScript用一些键值对来模拟一系列表单控件,我们还可以使用XMLHttpRequest的send()方法来异步的提交这个”表单”.比起普通的ajax,使用FormData的最大优点就是我们可以异步上传一个二进制文件.
参见:https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/FormData
所有主流浏览器的较新版本都已经支持这个对象了,比如Chrome 7+、Firefox 4+、IE 10+、Opera 12+、Safari 5+。
FormData()
想得到一个FormData对象:
var formdata = new FormData();
var formdata = new FormData();
formdata.append(“name”, “呵呵”);
var formobj = document.getElementById(“form”);
var formdata = new FormData(formobj);
var formobj = document.getElementById(“form”);
var formdata = formobj.getFormData()
Method
FormData.append
语法
注: 通过 FormData.append())方法赋给字段的值若是数字会被自动转换为字符(字段的值可以是一个Blob对象,一个File对象,或者一个字符串,剩下其他类型的值都会被自动转换成字符串).
参数解释
键 (key), 对应表单域
表单域的值
The filename reported to the server (a USVString), when a Blob or File is passed as the second parameter. The default filename for Blob objects is “blob”.
FormData.delete
将一对键和值从 FormData 对象中删除。
formData.delete(username);
FormData.get
返回给定键的第一个值
formData.append(‘username’, ‘Justin’); formData.append(‘username’, ‘Chris’); formData.get(username); // “Justin”
FormData.getAll
返回给定键的所有值
formData.append(‘username’, ‘Justin’); formData.append(‘username’, ‘Chris’); formData.getAll(username); // [“Justin”, “Chris”]
FormData.has
检查是否包含给定键,返回 true 或 false
formData.has(username);
FormData.set
设置给定键的值
formData.set(name, value); formData.set(name, value, filename);
浏览器兼容情况
来自 MDN:
Desktop
Feature
Chrome
Firfox(Gecko)
Intenet Explorer
Opera
Safari
Basic support
7+
4.0(2.0)
10+
12+
5+
append with filename
(Yes)
22.0(22.0)
?
?
?
delete, get, getAll, has, set
Behind Flag
Not supported
Not supported
(Yes)
Not supported
Feature
Android
Chrome Android
Firfox Mobile (Gecko)
Firfox OS (Gecko)
IE Mobile
Opera Mobile
Safari Mobile
Basic support
3.0
?
4.0(2.0)
1.0.1
?
12+
?
append with filename
?
?
22.0(22.0)
1.2
?
?
?
delete, get, getAll, has, set
(Yes)
(Yes)
Not supported
Not supported
Not supported
(Yes)
Not supported
Ajax通过FormData上传文件
1.使用
$.ajax({ url: ‘/upload’,
type: ‘POST’,
cache: false,
data: new FormData($(‘#uploadForm’)[0]),
processData: false, contentType: false }).done(function(res) { }).fail(function(res) {});
如果不是用表单构造FormData对象又该怎么做呢?
2.使用FormData对象添加字段方式上传文件
这里没有标签,也没有enctype=”multipart/form-data”属性。
var formData = new FormData();
formData.append(‘file’, $(‘#file’)[0].files[0]);
$.ajax({ url: ‘/upload’,
type: ‘POST’, cache: false, data: formData, processData: false, contentType: false }).done(function(res) { }).fail(function(res) {});
只需要在里添加multiple或multiple=”multiple”属性。
3.服务器端读文件
FormData是Ajax 2.0对象用以将数据编译成键值对,以便于XMLHttpRequest来发送数据。XMLHttpRequest Level 2提供的一个接口对象,可以使用该对象来模拟和处理表单并方便的进行文件上传操作。
arguments: null
caller: null
length: 0
name: “FormData”
prototype: FormData
append: ƒ append()
delete: ƒ delete()
entries: ƒ entries()
forEach: ƒ forEach()
get: ƒ ()
getAll: ƒ getAll()
has: ƒ has()
keys: ƒ keys()
set: ƒ ()
values: ƒ values()
constructor: ƒ FormData()
Symbol(Symbol.iterator): ƒ entries()
Symbol(Symbol.toStringTag): “FormData”
proto: Object
proto: ƒ ()
[[Scopes]]: Scopes[0]
通过打印并查看formData的结构,可以发现该接口对象本身非常简单。在formData构造函数原型对象上只有append、forEach、keys等少数方法。
网络请求中处理用来异步的上传文件
FormData 实例的创建
//001 通过构造函数创建不传递任务参数
var formData1 = new FormData(); //空的实例对象
//设置数据
formData1.set(“name”,”文顶顶”);
formData1.set(“email”,”wendingding_ios@126.com”);
formData1.set(“friends”,”熊大”);
//设置数据(追加)
formData1.append(“friends”,”光头强”);
formData1.append(“friends”,”萝卜头”);
//查看实例数据
formData1.forEach(function(value,key){
console.log(key,value);
})
console.log(“—————————————————“);
var formData2 = new FormData(document.getElementById(“formTest”))
formData2.forEach(function(value,key){
console.log(key,value);
})
email wendingding_ios@126.com
friends 熊大
friends 光头强
friends 萝卜头
—————————————————
user wendingding
pass 123456789
FormData 的主要方法
var data = new FormData();//02 设置数据(添加)<br /> data.set("age",18);<br /> data.set("name","LiuY");<br /> data.set("type","法师");<br /> data.set("address","泉水中心");<br /> //03 设置数据(修改和删除)<br /> data.set("name","MiTaoer");<br /> data.delete("address");<br /> //04 设置数据(追加)<br /> data.append("type","战士");<br /> data.append("type","辅助");<br />===========================================<br /> //05 读取数据(指定key-one)<br /> console.log(data.get("name")); //MiTaoer<br /> console.log(data.get("type")); //法师
//06 读取数据(指定key-All)<br /> console.log(data.getAll("type")); //["法师", "战士", "辅助"]<br /> <br /> //07 检查是否拥有指定的key<br /> console.log(data.has("age")); //true<br /> console.log(data.has("email")); //false
//08 迭代器的基本使用(keys)<br /> var keyIterator = data.keys() //获取迭代器对象<br /> console.log(keyIterator.next()); //{done: false, value: "age"}<br /> console.log(keyIterator.next()); //{done: false, value: "name"}<br /> console.log(keyIterator.next()); //{done: false, value: "type"}<br /> console.log(keyIterator.next()); //{done: false, value: "type"}<br /> console.log(keyIterator.next()); //{done: false, value: "type"}<br /> console.log(keyIterator.next()); //{done: true, value: undefined}<br /> <br /> console.log("___________________");
//09 迭代器的基本使用(values)<br /> var valueIterator = data.values(); //获取迭代器对象<br /> console.log(valueIterator.next()); //{done: false, value: "18"}<br /> console.log(valueIterator.next()); //{done: false, value: "MiTaoer"}<br /> console.log(valueIterator.next()); //{done: false, value: "法师"}<br /> console.log(valueIterator.next()); //{done: false, value: "战士"}<br /> console.log(valueIterator.next()); //{done: false, value: "辅助"}<br /> console.log(valueIterator.next()); //{done: true, value: undefined}
//10 迭代器的基本使用(entries)<br /> console.log(data.entries().next()); //{done: false, value: ["age", "18"]}
//11 formData对象的遍历<br /> data.forEach(function(value,key){<br /> //输出结果<br /> // age 18<br /> // name MiTaoer<br /> // type 法师<br /> // type 战士<br /> // type 辅助<br /> console.log(key,value);<br /> })
这里给定如下的表单数据,然后介绍如何使用FormData来处理表单数据发送GET和POST请求。
```//01 获取页面中的btn标签<br /> var oBtn = document.getElementsByTagName("button")[0];<br /> <br /> //02 给按钮标签添加点击事件<br /> oBtn.onclick = function(){
//03 使用Ajax发送GET请求<br /> var xhr = new XMLHttpRequest();<br /> xhr.open("GET","[http://127.0.0.1:3000?"+getData(),true);](http://127.0.0.1:3000?"+getData(),true);)<br /> xhr.send();<br /> xhr.onreadystatechange = function(){<br /> if(xhr.status >= 200 && xhr.status <=300 || xhr.status == 304)<br /> {<br /> console.log("请求成功"+xhr.responseText);<br /> }else{<br /> console.log("请求失败"+xhr.statusText);<br /> }<br /> }<br /> }
//获取页面中的表单数据并处理为查询字符串<br /> function getData(){<br /> var arr = [];<br /> var data = new FormData(document.forms.namedItem("formTest"));<br /> data.append("age",18);<br /> data.forEach(function(value,key){<br /> arr.push(key+"="+value);<br /> })<br /> return arr.join("&");<br /> }<br />通过上面的代码示例可以发现,使用formData来处理表单数据发送GET请求并没有什么优势,也需要通过循环来处理然后把键值对转换为查询字符串的形式拼接在URL字符串的后面。
//01 获取页面中的btn标签<br /> var oBtn = document.getElementsByTagName("button")[0];<br /> <br /> //02 给按钮标签添加点击事件<br /> oBtn.onclick = function(){
//03 处理参数<br /> //方式(1) 模拟表单数据<br /> var data = new FormData();<br /> data.set("name","文顶顶");<br /> data.set("color","red");<br /> data.set("email","yangyong@520it.com");<br /> data.append("email","wendingding_ios@126.com");
//方式(2) 获取表单数据<br /> //var data = new FormData(document.forms.namedItem("formTest"));
//04 使用Ajax发送GET请求<br /> var xhr = new XMLHttpRequest();<br /> xhr.open("POST","http://127.0.0.1:3000",true);<br /> xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');<br /> xhr.send(data);<br /> xhr.onreadystatechange = function(){<br /> if(xhr.status >= 200 && xhr.status <=300 || xhr.status == 304)<br /> {<br /> console.log("请求成功"+xhr.responseText);<br /> }else{<br /> console.log("请求失败"+xhr.statusText);<br /> }<br /> }<br /> }<br />**如果发送的是POST请求,那么提交表单数据需要通过setRequestHeader方法来设置'Content-Type', 'application/x-www-form-urlencoded',而formData数据直接作为send方法的参数来进行提交即可。POST请求通过formData提交给服务器端的数据,如果是Node服务器端则很难处理(同文件一样)。formData最主要的用途其实是用来异步的进行文件上传。**
//01 获取页面中的btn标签<br /> var oBtn = document.getElementsByTagName("button")[0];<br /> var oUser = document.getElementById("userID");<br /> var oFileID = document.getElementById("fileID");
//02 给按钮标签添加点击事件<br /> oBtn.onclick = function(){
//03 获取表单中的文件内容<br /> var data = new FormData();<br /> data.set("user",oUser.value);<br /> Array.from(oFileID.files).forEach(function(file){<br /> data.append("fileName",file);<br /> })
//04 使用Ajax发送GET请求<br /> var xhr = new XMLHttpRequest();<br /> xhr.open("POST","[http://127.0.0.1:5000/api",true);](http://127.0.0.1:5000/api",true);)<br /> xhr.send(data);<br /> xhr.onreadystatechange = function(){<br /> if(xhr.status >= 200 && xhr.status <=300 || xhr.status == 304)<br /> {<br /> console.log("请求成功"+xhr.responseText);<br /> }else{<br /> console.log("请求失败"+xhr.statusText);<br /> }<br /> }<br /> }
//01 导入模块(需先通过npm来进行安装)
var express = require(‘express’);
var multer = require(‘multer’);
var body = require(‘body-parser’);
app.listen(5000);
app.use(body.urlencoded( { extended: false } ));
app.use(multer( { dest: ‘./upload/‘ } ).any());
app.post(‘/api’, function (req,res){
res.setHeader(‘Access-Control-Allow-Origin’, ‘*’);
res.send(“Nice ! 上传成功 ~ “);
console.log(req.body); //普通POST数据
console.log(req.files); //文件POST数据
});
app.use(express.static(‘./html/‘));
代码说明 需要先通过npm install express multer body-parser命令在当前路径中安装对应的模块。
{ user: ‘wen’ }
[ { fieldname: ‘fileName’,
originalname: ‘formData.png’,
encoding: ‘7bit’,
mimetype: ‘image/png’,
destination: ‘./upload/‘,
filename: ‘f416da3b522ece9e4cc2eccd5b7a62e8’,
path: ‘upload/f416da3b522ece9e4cc2eccd5b7a62e8’,
size: 50002 },
{ fieldname: ‘fileName’,
originalname: ‘Snip20190107_1.png’,
encoding: ‘7bit’,
mimetype: ‘image/png’,
destination: ‘./upload/‘,
filename: ‘2a2dd60e217b9cc08f2cc0048a1d27ab’,
path: ‘upload/2a2dd60e217b9cc08f2cc0048a1d27ab’,
size: 1309894 } ]
