[TOC]

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 />安装开发者工具调试
  1. 组件名必须首字母大写
    2. 虚拟DOM元素只能有一个根元素
    3. 虚拟DOM元素必须有结束标签
  1. 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(,document.getElementById(‘test’))
// 执行了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今天天气很{isHot ? ‘炎热’ : ‘凉爽’},{wind}
}
//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(, document.getElementById(‘test’))

  1. 组件中render方法中的this为组件实例对象
    2. 组件自定义的方法中this为undefined,如何解决?
    a) 强制绑定this: 通过函数对象的bind()
    b) 箭头函数
    3. 状态数据,不能直接修改或更新 用setState

1. 每个组件对象都会有props(properties的简写)属性
2. 组件标签的所有属性都保存在props中

  1. props里面的属性是只读的不能修改
  2. 构造器是否接收props,是否传递给super,取决于:是否希望在构造器中通过this访问props

  3. 扩展属性: 将对象的所有属性通过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>









事件处理

  1. 通过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变化

image.png

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

{this.state.count}

}
}

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
image.png

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
image.png

image.png
image.png
image.png
image.png
b.n=1,b.m=’undefined’,c.n=2,c.m=3

闭包

微信图片_20220819101512.jpg微信图片_20220819101519.jpg

作用:

  • 使用函数内部的变量在函数执行完之后仍然在内存中(延长局部变量的生命周期)
  • 让函数外部能操作(读写)内部的数据(变量/函数)

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代码的一部分拆分为单独的函数时看到的。出于可读性或可重用性的原因。

函数中
)





用户名:


密 码:






几点区别如下:
1、两种方法的路由都是/users/login,如果后台服务在路由函数的最后return render_template(‘index.html’),那么form表单提交会自动跳转到index页面,而ajax提交页面不会跳转,只是会返回这个页面,从fiddler中抓包可以在webview中看到index页面

2、form中的登录按钮处type=‘submit’,而ajax中的登录按钮处type=‘button’且要加上οnclick=‘login()’函数,并在JavaScript代码中完成login()函数的功能
3、form中的请求参数url在

的action=’ /users/login’,请求方法默认为get,如果要设置加上method=’post’,请求数据会自动提取form体中的所有数据;ajax中的请求参数url,data,method都是在$.ajax方法中设置
4、form提交的请求的contentype为 application/x-www-form-urlencoded,而ajax是自己设置的一般为 application/json,这两种格式的请求方式在后端的数据使用方式请参见博客 application/x-www-form-urlencoded类型如何获取表单数据及ImmutableMultiDict如何使用

  1. 如果表单提交后不需要页面跳转,或者想把跳转的控制权放在前端的话,用ajax提交更方便,这样页面不用刷新,只局部刷新,加载速度快,网络带宽占用低,数据也不会闪屏
  2. ajax在提交、请求、接收时,是异步进行的,不影响页面其他部分
  3. ajax提交时,是在后台新建一个请求
  4. ajax必须要用js来实现,存在调试麻烦、浏览器兼容问题、而且不启用js的浏览器,无法完成操作
  5. ajax在提交、请求、接收时,整个过程需要使用程序来对其进行数据处理

  6. 如果表单提交以后需要重新跳转页面,或者要将跳转行为放在后端时,用表单提交更合适点,点击触发submit事件,后端控制页面的跳转及数据的传递

  7. form表单提交会新建一个页面,会在控制器和模板之间传递更多参数
  8. form是放弃本页面,然后再请求
  9. form表单提交,是根据表单结构自动完成,不需要代码干预,用submit提交
  10. form表单的属性中有校验的字段,用户只需要添加正则表达式的校验规则

通过Ajax方式上传文件(input file),使用FormData进行Ajax请求

通过传统的form表单提交的方式上传文件:

  1. 测试通过Rest接口上传文件

  2. 指定文件名:

  3. 上传文件:

  4. 关键字1:

  5. 关键字2:

  6. 关键字3:


不过传统的form表单提交会导致页面刷新,但是在有些情况下,我们不希望页面被刷新,这种时候我们都是使用Ajax的方式进行请求的。
通常我们提交(使用submit button)时,会把form中的所有表格元素的name与value组成一个queryString,提交到后台。这用jQuery的方法来说,就是serialize。



Ajax的方式进行请求:

  1. $.ajax({
  2. url : “http://localhost:8080/STS/rest/user“,
  3. type : “POST”,
  4. data : $( ‘#postForm’).serialize(),
  5. success : function(data) {
  6. $( ‘#serverResponse’).html(data);
  7. },
  8. error : function(data) {
  9. $( ‘#serverResponse’).html(data.status + “ : “ + data.statusText + “ : “ + data.responseText);
  10. }
  11. });

通过$(‘#postForm’).serialize()可以对form表单进行序列化,从而将form表单中的所有参数传递到服务端。

但是上述方式,只能传递一般的参数,上传文件的文件流是无法被序列化并传递的。
不过如今主流浏览器都开始支持一个叫做FormData的对象,有了这个FormData,我们就可以轻松地使用Ajax方式进行文件上传了。

关于FormData及其用法

FormData是什么呢?我们来看看Mozilla上的介绍。

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+。

Constructor
FormData()
想得到一个FormData对象:

var formdata = new FormData();

W3c草案提供了三种方案来获取或修改FormData。

方案1:创建一个空的FormData对象,然后再用append方法逐个添加键值对:

var formdata = new FormData();
formdata.append(“name”, “呵呵”);

| formdata.append(“url”, “http://www.baidu.com/“);

| | —- |

方案2:取得form元素对象,将它作为参数传入FormData对象中!

var formobj = document.getElementById(“form”);
var formdata = new FormData(formobj);

方案3:利用form元素对象的getFormData方法生成它!

var formobj = document.getElementById(“form”);
var formdata = formobj.getFormData()

Method

FormData.append

本方法用于向已存在的键添加新的值,如该键不存在,新建之。

语法

formData.append(name, value); formData.append(name, value, filename);

注: 通过 FormData.append())方法赋给字段的值若是数字会被自动转换为字符(字段的值可以是一个Blob对象,一个File对象,或者一个字符串,剩下其他类型的值都会被自动转换成字符串).

参数解释

  • name
    键 (key), 对应表单域
  • value
    表单域的值
  • filename (optional)
    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

Mobile

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.使用
表单初始化FormData对象方式上传文件

HTML代码

javascript代码
$.ajax({ url: ‘/upload’,
type: ‘POST’,
cache: false,
data: new FormData($(‘#uploadForm’)[0]),
processData: false, contentType: false }).done(function(res) { }).fail(function(res) {});

这里要注意几点:

  • processData设置为false。因为data值是FormData对象,不需要对数据做处理。
  • 标签添加enctype=”multipart/form-data”属性。
  • cache设置为false,上传文件不需要缓存。
  • contentType设置为false,不设置contentType值,因为是由表单构造的FormData对象,且已经声明了属性enctype=”multipart/form-data”,所以这里设置为false。

上传后,服务器端代码需要使用从查询参数名为file获取文件输入流对象,因为中声明的是name=”file”。
如果不是用表单构造FormData对象又该怎么做呢?





2.使用FormData对象添加字段方式上传文件

HTML代码



这里没有标签,也没有enctype=”multipart/form-data”属性。

javascript代码
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) {});

这里有几处不一样:

  • append()的第二个参数应是文件对象,即$(‘#file’)[0].files[0]。
  • contentType也要设置为‘false’。

从代码$(‘#file’)[0].files[0]中可以看到一个标签能够上传多个文件,
只需要在里添加multiple或multiple=”multiple”属性。



3.服务器端读文件

从Servlet 3.0 开始,可以通过 request.getPart() 或 request.getPars() 两个接口获取上传的文件。


FormData 简单介绍
FormData是Ajax 2.0对象用以将数据编译成键值对,以便于XMLHttpRequest来发送数据。XMLHttpRequest Level 2提供的一个接口对象,可以使用该对象来模拟和处理表单并方便的进行文件上传操作。

我们打印这个构造函数看一眼

ƒ FormData()
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的主要用处

网络请求中处理表单数据
网络请求中处理用来异步的上传文件
FormData 实例的创建

◆ new FormData ( HTMLFormElement: ele)

在使用FormData构造函数创建实例对象的时候,可以传递一个HTML表单元素,该表单元素允许是任何形式的表单控件,包括文件输入框、复选框等。




//列出创建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(“—————————————————“);

//002 获取表单标签传递给FormData构造函数
var formData2 = new FormData(document.getElementById(“formTest”))
formData2.forEach(function(value,key){
console.log(key,value);
})

注意:表单标签必须要添加name属性才能获取其数据

说明: 在上面的示例代码中介绍了两种创建(获取)formData实例对象的方式,可以先创建一个空的实例对象也可以直接通过页面中的表单标签来进行初始化处理。

当formData数据装填好之后,可以直接通过ajax方法提交到服务器端,下面给出上面代码的执行结果。

name 文顶顶
email wendingding_ios@126.com
friends 熊大
friends 光头强
friends 萝卜头
—————————————————
user wendingding
pass 123456789
FormData 的主要方法

如上图所示,FormData构造函数的原型对象上面定义了一堆方法。这些方法使用方式都很简单,接下来我们通过代码的方式简单介绍他们。

//01 创建空的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 对象的这些方法其实不用进行过多的赘述,上面的代码和说明简单易懂。总体上来说,它提供了一整套的操作数据的方法囊括了添加(set)、修改、查询和删除等操作,append方法和set方法的不同之处在于它不会覆盖而是以数组push的方式来处理同名的数据。

formData 对象的keys()、values()和entries()方法使用类似,调用后将得到一个Iterator类型的迭代器对象,该对象能够能够调用next()方法来进行迭代操作,打印结果中的done使用布尔类型的值来进行标记,如果迭代结束那么值为true。

formData 对象的forEach()接收一个回调函数参数,其中第一个参数为当前遍历数据的value值,第二个参数为key(同数组的forEach方法一致)。如果是Ajax发送GET请求,需要通过formData的方式来提交表单数据,那么可以借助该方法来拼接查询字符串。

FormData的典型用法
这里给定如下的表单数据,然后介绍如何使用FormData来处理表单数据发送GET和POST请求。








是否勾选


```
GET请求

//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字符串的后面。

POST请求

//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最主要的用途其实是用来异步的进行文件上传。**

POST请求进行文件上传





//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 />    }

这里顺便贴出测试文件上传写的Node代码以及文件上传后的监听结果。

//备注:node文件名称为uploadServer.js
//01 导入模块(需先通过npm来进行安装)
var express = require(‘express’);
var multer = require(‘multer’);
var body = require(‘body-parser’);

var app = express();
app.listen(5000);
app.use(body.urlencoded( { extended: false } ));
app.use(multer( { dest: ‘./upload/‘ } ).any());

//02 监听网络请求并设置打印接收到的参数信息
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命令在当前路径中安装对应的模块。

wendingding$ node uploadServer.js
{ 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 } ]

**