0%

表单,FormData 对象

目录 [隐藏]

表单概述

表单(<form>)用来收集用户提交的数据,发送到服务器。比如,用户提交用户名和密码,让服务器验证,就要通过表单。表单提供多种控件,让开发者使用,具体的控件种类和用法请参考 HTML 语言的教程。本章主要介绍 JavaScript 与表单的交互。

1
2
3
4
5
6
7
8
9
10
11
12
13
<form action="/handling-page" method="post">
<div>
<label for="name">用户名:</label>
<input type="text" id="name" name="user_name" />
</div>
<div>
<label for="passwd">密码:</label>
<input type="password" id="passwd" name="user_passwd" />
</div>
<div>
<input type="submit" id="submit" name="submit_button" value="提交" />
</div>
</form>

上面代码就是一个简单的表单,包含三个控件:用户名输入框、密码输入框和提交按钮。

用户点击“提交”按钮,每一个控件都会生成一个键值对,键名是控件的name属性,键值是控件的value属性,键名和键值之间由等号连接。比如,用户名输入框的name属性是user_namevalue属性是用户输入的值,假定是“张三”,提交到服务器的时候,就会生成一个键值对user_name=张三

所有的键值对都会提交到服务器。但是,提交的数据格式跟<form>元素的method属性有关。该属性指定了提交数据的 HTTP 方法。如果是 GET 方法,所有键值对会以 URL 的查询字符串形式,提交到服务器,比如/handling-page?user_name=张三&user_passwd=123&submit_button=提交。下面就是 GET 请求的 HTTP 头信息。

1
2
GET /handling-page?user_name=张三&user_passwd=123&submit_button=提交
Host: example.com

如果是 POST 方法,所有键值对会连接成一行,作为 HTTP 请求的数据体发送到服务器,比如user_name=张三&user_passwd=123&submit_button=提交。下面就是 POST 请求的头信息。

1
2
3
4
5
6
POST /handling-page HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 74

user_name=张三&user_passwd=123&submit_button=提交

注意,实际提交的时候,只要键值不是 URL 的合法字符(比如汉字“张三”和“提交”),浏览器会自动对其进行编码。

点击submit控件,就可以提交表单。

1
2
3
<form>
<input type="submit" value="提交">
</form>

上面表单就包含一个submit控件,点击这个控件,浏览器就会把表单数据向服务器提交。

注意,表单里面的<button>元素如果没有用type属性指定类型,那么默认就是submit控件。

1
2
3
<form>
<button>提交</button>
</form>

上面表单的<button>元素,点击以后也会提交表单。

除了点击submit控件提交表单,还可以用表单元素的submit()方法,通过脚本提交表单。

1
formElement.submit();

表单元素的reset()方法可以重置所有控件的值(重置为默认值)。

1
formElement.reset()

FormData 对象

概述

表单数据以键值对的形式向服务器发送,这个过程是浏览器自动完成的。但是有时候,我们希望通过脚本完成这个过程,构造或编辑表单的键值对,然后通过脚本发送给服务器。浏览器原生提供了 FormData 对象来完成这项工作。

FormData()首先是一个构造函数,用来生成表单的实例。

1
var formdata = new FormData(form);

FormData()构造函数的参数是一个 DOM 的表单元素,构造函数会自动处理表单的键值对。这个参数是可选的,如果省略该参数,就表示一个空的表单。

下面是一个表单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<form id="myForm" name="myForm">
<div>
<label for="username">用户名:</label>
<input type="text" id="username" name="username">
</div>
<div>
<label for="useracc">账号:</label>
<input type="text" id="useracc" name="useracc">
</div>
<div>
<label for="userfile">上传文件:</label>
<input type="file" id="userfile" name="userfile">
</div>
<input type="submit" value="Submit!">
</form>

我们用FormData()处理上面这个表单。

1
2
3
4
5
6
7
8
9
10
var myForm = document.getElementById('myForm');
var formData = new FormData(myForm);

// 获取某个控件的值
formData.get('username') // ""

// 设置某个控件的值
formData.set('username', '张三');

formData.get('username') // "张三"

实例方法

FormData 提供以下实例方法。

  • FormData.get(key):获取指定键名对应的键值,参数为键名。如果有多个同名的键值对,则返回第一个键值对的键值。
  • FormData.getAll(key):返回一个数组,表示指定键名对应的所有键值。如果有多个同名的键值对,数组会包含所有的键值。
  • FormData.set(key, value):设置指定键名的键值,参数为键名。如果键名不存在,会添加这个键值对,否则会更新指定键名的键值。如果第二个参数是文件,还可以使用第三个参数,表示文件名。
  • FormData.delete(key):删除一个键值对,参数为键名。
  • FormData.append(key, value):添加一个键值对。如果键名重复,则会生成两个相同键名的键值对。如果第二个参数是文件,还可以使用第三个参数,表示文件名。
  • FormData.has(key):返回一个布尔值,表示是否具有该键名的键值对。
  • FormData.keys():返回一个遍历器对象,用于for...of循环遍历所有的键名。
  • FormData.values():返回一个遍历器对象,用于for...of循环遍历所有的键值。
  • FormData.entries():返回一个遍历器对象,用于for...of循环遍历所有的键值对。如果直接用for...of循环遍历 FormData 实例,默认就会调用这个方法。

下面是get()getAll()set()append()方法的例子。

1
2
3
4
5
6
7
8
9
var formData = new FormData();

formData.set('username', '张三');
formData.append('username', '李四');
formData.get('username') // "张三"
formData.getAll('username') // ["张三", "李四"]

formData.append('userpic[]', myFileInput.files[0], 'user1.jpg');
formData.append('userpic[]', myFileInput.files[1], 'user2.jpg');

下面是遍历器的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var formData = new FormData();
formData.append('key1', 'value1');
formData.append('key2', 'value2');

for (var key of formData.keys()) {
console.log(key);
}
// "key1"
// "key2"

for (var value of formData.values()) {
console.log(value);
}
// "value1"
// "value2"

for (var pair of formData.entries()) {
console.log(pair[0] + ': ' + pair[1]);
}
// key1: value1
// key2: value2

// 等同于遍历 formData.entries()
for (var pair of formData) {
console.log(pair[0] + ': ' + pair[1]);
}
// key1: value1
// key2: value2

表单的内置验证

自动校验

表单提交的时候,浏览器允许开发者指定一些条件,它会自动验证各个表单控件的值是否符合条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 必填 -->
<input required>

<!-- 必须符合正则表达式 -->
<input pattern="banana|cherry">

<!-- 字符串长度必须为6个字符 -->
<input minlength="6" maxlength="6">

<!-- 数值必须在1到10之间 -->
<input type="number" min="1" max="10">

<!-- 必须填入 Email 地址 -->
<input type="email">

<!-- 必须填入 URL -->
<input type="URL">

如果一个控件通过验证,它就会匹配:valid的 CSS 伪类,浏览器会继续进行表单提交的流程。如果没有通过验证,该控件就会匹配:invalid的 CSS 伪类,浏览器会终止表单提交,并显示一个错误信息。

1
2
3
4
5
6
7
input:invalid {
border-color: red;
}
input,
input:valid {
border-color: #ccc;
}

checkValidity()

除了提交表单的时候,浏览器自动校验表单,还可以手动触发表单的校验。表单元素和表单控件都有checkValidity()方法,用于手动触发校验。

1
2
3
4
5
// 触发整个表单的校验
form.checkValidity()

// 触发单个表单控件的校验
formControl.checkValidity()

checkValidity()方法返回一个布尔值,true表示通过校验,false表示没有通过校验。因此,提交表单可以封装为下面的函数。

1
2
3
4
5
6
7
function submitForm(action) {
var form = document.getElementById('form');
form.action = action;
if (form.checkValidity()) {
form.submit();
}
}

willValidate 属性

控件元素的willValidate属性是一个布尔值,表示该控件是否会在提交时进行校验。

1
2
3
4
5
6
7
// HTML 代码如下
// <form novalidate>
// <input id="name" name="name" required />
// </form>

var input = document.querySelector('#name');
input.willValidate // true

validationMessage 属性

控件元素的validationMessage属性返回一个字符串,表示控件不满足校验条件时,浏览器显示的提示文本。以下两种情况,该属性返回空字符串。

  • 该控件不会在提交时自动校验
  • 该控件满足校验条件
1
2
3
4
// HTML 代码如下
// <form><input type="text" required></form>
document.querySelector('form input').validationMessage
// "请填写此字段。"

下面是另一个例子。

1
2
3
4
var myInput = document.getElementById('myinput');
if (!myInput.checkValidity()) {
document.getElementById('prompt').innerHTML = myInput.validationMessage;
}

setCustomValidity()

控件元素的setCustomValidity()方法用来定制校验失败时的报错信息。它接受一个字符串作为参数,该字符串就是定制的报错信息。如果参数为空字符串,则上次设置的报错信息被清除。

这个方法可以替换浏览器内置的表单验证报错信息,参数就是要显示的报错信息。

1
2
3
4
5
6
7
8
9
10
<form action="somefile.php">
<input
type="text"
name="username"
placeholder="Username"
pattern="[a-z]{1,15}"
id="username"
>
<input type="submit">
</form>

上面的表单输入框,要求只能输入小写字母,且不得超过15个字符。如果输入不符合要求(比如输入“ABC”),提交表单的时候,Chrome 浏览器会弹出报错信息“Please match the requested format.”,禁止表单提交。下面使用setCustomValidity()方法替换掉报错信息。

1
2
3
4
5
6
var input = document.getElementById('username');
input.oninvalid = function (event) {
event.target.setCustomValidity(
'用户名必须是小写字母,不能为空,最长不超过15个字符'
);
}

上面代码中,setCustomValidity()方法是在invalid事件的监听函数里面调用。该方法也可以直接调用,这时如果参数不为空字符串,浏览器就会认为该控件没有通过校验,就会立刻显示该方法设置的报错信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* HTML 代码如下
<form>
<p><input type="file" id="fs"></p>
<p><input type="submit"></p>
</form>
*/

document.getElementById('fs').onchange = checkFileSize;

function checkFileSize() {
var fs = document.getElementById('fs');
var files = fs.files;
if (files.length > 0) {
if (files[0].size > 75 * 1024) {
fs.setCustomValidity('文件不能大于 75KB');
return;
}
}
fs.setCustomValidity('');
}

上面代码一旦发现文件大于 75KB,就会设置校验失败,同时给出自定义的报错信息。然后,点击提交按钮时,就会显示报错信息。这种校验失败是不会自动消除的,所以如果所有文件都符合条件,要将报错信息设为空字符串,手动消除校验失败的状态。

validity 属性

控件元素的属性validity属性返回一个ValidityState对象,包含当前校验状态的信息。

该对象有以下属性,全部为只读属性。

  • ValidityState.badInput:布尔值,表示浏览器是否不能将用户的输入转换成正确的类型,比如用户在数值框里面输入字符串。
  • ValidityState.customError:布尔值,表示是否已经调用setCustomValidity()方法,将校验信息设置为一个非空字符串。
  • ValidityState.patternMismatch:布尔值,表示用户输入的值是否不满足模式的要求。
  • ValidityState.rangeOverflow:布尔值,表示用户输入的值是否大于最大范围。
  • ValidityState.rangeUnderflow:布尔值,表示用户输入的值是否小于最小范围。
  • ValidityState.stepMismatch:布尔值,表示用户输入的值不符合步长的设置(即不能被步长值整除)。
  • ValidityState.tooLong:布尔值,表示用户输入的字数超出了最长字数。
  • ValidityState.tooShort:布尔值,表示用户输入的字符少于最短字数。
  • ValidityState.typeMismatch:布尔值,表示用户填入的值不符合类型要求(主要是类型为 Email 或 URL 的情况)。
  • ValidityState.valid:布尔值,表示用户是否满足所有校验条件。
  • ValidityState.valueMissing:布尔值,表示用户没有填入必填的值。

下面是一个例子。

1
2
3
4
5
6
var input = document.getElementById('myinput');
if (input.validity.valid) {
console.log('通过校验');
} else {
console.log('校验失败');
}

下面是另外一个例子。

1
2
3
4
5
var txt = '';
if (document.getElementById('myInput').validity.rangeOverflow) {
txt = '数值超过上限';
}
document.getElementById('prompt').innerHTML = txt;

如果想禁止浏览器弹出表单验证的报错信息,可以监听invalid事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var input = document.getElementById('username');
var form = document.getElementById('form');

var elem = document.createElement('div');
elem.id = 'notify';
elem.style.display = 'none';
form.appendChild(elem);

input.addEventListener('invalid', function (event) {
event.preventDefault();
if (!event.target.validity.valid) {
elem.textContent = '用户名必须是小写字母';
elem.className = 'error';
elem.style.display = 'block';
input.className = 'invalid animated shake';
}
});

input.addEventListener('input', function(event){
if ( 'block' === elem.style.display ) {
input.className = '';
elem.style.display = 'none';
}
});

上面代码中,一旦发生invalid事件(表单验证失败),event.preventDefault()用来禁止浏览器弹出默认的验证失败提示,然后设置定制的报错提示框。

表单的 novalidate 属性

表单元素的 HTML 属性novalidate,可以关闭浏览器的自动校验。

1
2
<form novalidate>
</form>

这个属性也可以在脚本里设置。

1
form.noValidate = true;

如果表单元素没有设置novalidate属性,那么提交按钮(<button><input>元素)的formnovalidate属性也有同样的作用。

1
2
3
<form>
<input type="submit" value="submit" formnovalidate>
</form>

enctype 属性

表单能够用四种编码,向服务器发送数据。编码格式由表单的enctype属性决定。

假定表单有两个字段,分别是foobaz,其中foo字段的值等于barbaz字段的值是一个分为两行的字符串。

1
2
The first line.
The second line.

下面四种格式,都可以将这个表单发送到服务器。

(1)GET 方法

如果表单使用GET方法发送数据,enctype属性无效。

1
2
3
4
5
6
<form
action="register.php"
method="get"
onsubmit="AJAXSubmit(this); return false;"
>
</form>

数据将以 URL 的查询字符串发出。

1
?foo=bar&baz=The%20first%20line.%0AThe%20second%20line.

(2)application/x-www-form-urlencoded

如果表单用POST方法发送数据,并省略enctype属性,那么数据以application/x-www-form-urlencoded格式发送(因为这是默认值)。

1
2
3
4
5
6
<form
action="register.php"
method="post"
onsubmit="AJAXSubmit(this); return false;"
>
</form>

发送的 HTTP 请求如下。

1
2
3
Content-Type: application/x-www-form-urlencoded

foo=bar&baz=The+first+line.%0D%0AThe+second+line.%0D%0A

上面代码中,数据体里面的%0D%0A代表换行符(\r\n)。

(3)text/plain

如果表单使用POST方法发送数据,enctype属性为text/plain,那么数据将以纯文本格式发送。

1
2
3
4
5
6
7
<form
action="register.php"
method="post"
enctype="text/plain"
onsubmit="AJAXSubmit(this); return false;"
>
</form>

发送的 HTTP 请求如下。

1
2
3
4
5
Content-Type: text/plain

foo=bar
baz=The first line.
The second line.

(4)multipart/form-data

如果表单使用POST方法,enctype属性为multipart/form-data,那么数据将以混合的格式发送。

1
2
3
4
5
6
7
<form
action="register.php"
method="post"
enctype="multipart/form-data"
onsubmit="AJAXSubmit(this); return false;"
>
</form>

发送的 HTTP 请求如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
Content-Type: multipart/form-data; boundary=---------------------------314911788813839

-----------------------------314911788813839
Content-Disposition: form-data; name="foo"

bar
-----------------------------314911788813839
Content-Disposition: form-data; name="baz"

The first line.
The second line.

-----------------------------314911788813839--

这种格式也是文件上传的格式。

文件上传

用户上传文件,也是通过表单。具体来说,就是通过文件输入框选择本地文件,提交表单的时候,浏览器就会把这个文件发送到服务器。

1
<input type="file" id="file" name="myFile">

此外,还需要将表单<form>元素的method属性设为POSTenctype属性设为multipart/form-data。其中,enctype属性决定了 HTTP 头信息的Content-Type字段的值,默认情况下这个字段的值是application/x-www-form-urlencoded,但是文件上传的时候要改成multipart/form-data

1
2
3
4
5
6
7
8
9
<form method="post" enctype="multipart/form-data">
<div>
<label for="file">选择一个文件</label>
<input type="file" id="file" name="myFile" multiple>
</div>
<div>
<input type="submit" id="submit" name="submit_button" value="上传" />
</div>
</form>

上面的 HTML 代码中,file 控件的multiple属性,指定可以一次选择多个文件;如果没有这个属性,则一次只能选择一个文件。

1
2
var fileSelect = document.getElementById('file');
var files = fileSelect.files;

然后,新建一个 FormData 实例对象,模拟发送到服务器的表单数据,把选中的文件添加到这个对象上面。

1
2
3
4
5
6
7
8
9
10
11
12
var formData = new FormData();

for (var i = 0; i < files.length; i++) {
var file = files[i];

// 只上传图片文件
if (!file.type.match('image.*')) {
continue;
}

formData.append('photos[]', file, file.name);
}

最后,使用 Ajax 向服务器上传文件。

1
2
3
4
5
6
7
8
9
10
11
var xhr = new XMLHttpRequest();

xhr.open('POST', 'handler.php', true);

xhr.onload = function () {
if (xhr.status !== 200) {
console.log('An error occurred!');
}
};

xhr.send(formData);

除了发送 FormData 实例,也可以直接 AJAX 发送文件。

1
2
3
4
5
6
var file = document.getElementById('test-input').files[0];
var xhr = new XMLHttpRequest();

xhr.open('POST', 'myserver/uploads');
xhr.setRequestHeader('Content-Type', file.type);
xhr.send(file);

参考链接

首先,先了解一下关于http协议里定义的四种常见数据的post方法,分别是:
application/www-form-ulrencoded
multipart/form-data
application/json
text/xml

Express依赖bodyParser对请求的包体进行解析。默认支持application/json,application/www-form-urlencoded,multipart/form-data.单数对xml没有支持。需要自己代码来实现

用axios post请求提交数据的时候需要清楚发送请求的类型是哪种,后台用相应的方法解析

1 使用FormData提交表单数据

坑一:使用FormData时一定要在先绑定submit事件,在submit事件触发时发送axios post请求,否则获取不到formData里面的值

坑二:axios请求里面如果直接使用data:formData,浏览器会自动帮我们加上 Content-Type: multipart/form-data ,因此请求要这样设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var form = document.getElementById("myForm");
// 用表单来初始化
var formData = new FormData(form);
// 我们可以根据name来访问表单中的字段
var username = formData.get("username"); // 获取名字
var password = formData.get("password"); // 获取密码
axios({
method:"post",
url:'http://127.0.0.1/api/login',
data:{
username:username,
password:password
}

})

这样默认Content-type:application/json,

如果表单属性过多,可以先用一个对象接收:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var form = document.getElementById("myForm");
// 用表单来初始化
var formData = new FormData(form);
// 我们可以根据name来访问表单中的字段
let myFormData={};
//如果表单属性过多可以先遍历
for(let pair of formData.entries()){
myFormData[pair[0]]=pair[1];
}
axios({
method:"post",
url:'http://127.0.0.1/api/login',
data:myFormData

})

后台接收使用中间件body-parser解析表单体数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
const express=require('express');
const session=require('express-session');
const cors=require('cors');
const bodyParser=require('body-parser')
const app=express();
app.use(cors());
app.use(
session({
secret:'xiaoqi',
resave:false,
saveUninitialized:true
})
)
//托管静态页面
app.use(express.static('./pages'));
//自定义解析Post提交过来的表单数据
app.use(bodyParser.urlencoded({extended:false}))
app.use(bodyParser.json())
//登录API接口
app.post('/api/login',(req,res)=>{
//判断用户提交的登录信息是否正确
//console.log(req.body)
if(req.body.username!=="admin"||req.body.password!=="888888"){
return res.send({status:1,msg:'登录失败'})
}
//登录成功则把信息存储在Session中
console.log(req.body);
req.session.user=req.body;//用户登录信息
req.session.islogin=true;//用户登录状态
res.send({status:0,msg:'登录成功'})

})
//获取用户姓名的接口
app.get('/api/username',(req,res)=>{
//从Session中获取用户姓名响应给客户端
if(!req.session.islogin){
return res.send({status:1,msg:'fail'})
}
res.send({
status:0,
msg:'success',
username:req.session.user.username,
})

})
//退出登录的接口
app.post('/api/logout',(req,res)=>{
//清空当前客户端的session信息
req.session.destroy()
res.send({
status:0,
msg:'退出登录成功'
})

})
app.listen(80,()=>{
console.log('运行在http://127.0.0.1')
})


使用MySql Workbench管理数据库

table中的数据类型:

int:整数

varchar(len):字符串

tinyint(1):布尔值

字段的特殊标识:

PK(Primary Key):主键,唯一标识

NN(Not Null):值不允许为空

UQ(Unique):值唯一

AI(Auto Increment):值自动增长

Sql语句

增:

insert into 表名 (列名,列名…) values(‘值’,…)

1
insert into users (username,password) values('李四','54321');

删:

delete from 表名 where 列 运算符 值

1
2
-- 删除id为4的用户
delete from users where id='4';

改:

update 表名 set 列=新值 where 列 运算符 值

1
2
-- update用于修改数据
update users set password ='88888888' where id='4';

查:

select 列名 from 表名

1
2
3
SELECT * FROM users
-- 从users表把username,password两列查询
select username, password from users;

where子句:

用于限定选择的标准,常见运算符:

等于:=

不等于:<>或者!=

大于:>

小于:<

在某个范围:BETWEEN

搜索某种样式:LIKE

OR,AND:

1
2
select * from users where status=0 and id>1
select * from users where status=0 or id>1

实现排序:

order by:用于根据指定列对结果集进行排序,默认按照升序排序,想按照降序排序用desc关键字

1
2
3
4
5
6
7
-- order by asc实现升序排序
select * from users order by status;
select * from users order by status asc;
-- order by desc实现降序排序
select * from users order by status desc;
-- 多重排序
select * from users order by status desc,username asc;

count(*)统计

1
2
3
4
5
count(*)统计
-- 查询users表中状态为0的用户总数量
select count(*) from users where status='0';
-- as 将列名改为total
select count(*) as total from users where status=0;

使用mysql模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const mysql=require('mysql')
//建立MYSQL与数据库的联系
const db=mysql.createPool({
host:'127.0.0.1',//数据库的ip地址
user:'root',
password:'123456',
database:'my_db_01',//指定要操作哪个数据库
})
//测试mysql能否正常工作
db.query('select 1',(err,result)=>{
//mysql工作期间报错
if(err)return console.log(err.message)
//能成功执行SQL语句
console.log(result);
})

增:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//要插入到users表中的数据对象
const users={username:'王五',password:'ww123'};
//待执行的SQL语句,其中英文?代表占位符
const sqlStr='insert into users (username,password) values(?,?)'
//使用数组的形式为占位符指定具体的值,返回的result是一个对象,通过affectedRows判读是否插入成功
db.query(sqlStr,[users.username,users.password],(err,result)=>{
if(err)return console.log(err.message)
if(result.affectedRows===1){console.log('插入数据成功')}//成功
})
//插入数据的便捷方式
const user={username:'小煜',password:'ww123'};
const sqlStr="insert into users set ?";
db.query(sqlStr,user,(err,result)=>{
if(err)return console.log(err.message)
if(result.affectedRows===1){console.log(result)}
})

删:

1
2
3
4
5
6
7
8
//删除数据
const sqlStr="delete from users where id=?"
//如果SQL语句有多个占位符则必须使用数组为每个占位符指定具体的值
//如果SQL语句只有一个占位符,则可以省略数组
db.query(sqlStr,12,(err,res)=>{
if(err)return console.log(err.message)
if(res.affectedRows===1){console.log(res);}
})

标记删除:使用delete语句会把数据从表中真正的删除,为了保险起见。推荐使用标记删除的形式来模拟删除的动作,就是在表中设置类似于status这样的状态字段,来标记当前这条数据是否被删除

当用户执行删除操作时,并没有执行DELETE语句把数据删除,而是执行了UPDATE语句,将这条数据对应的status字段标记为删除即可

改:

1
2
3
4
5
6
const user={id:11,username:'王五',password:'ww000'};
const sqlStr="update users set ? where id=?";
db.query(sqlStr,[user,user.id],(err,result)=>{
if(err)return console.log(err.message)
if(result.affectedRows===1){console.log(result)}
})

报错信息

本人系统安装的是mysql-installer-community-8.0.18.0.msi这个版本,然后我本地使用node-mysql去连接数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const mysql=require('mysql')
//建立MYSQL与数据库的联系
const db=mysql.createPool({
host:'127.0.0.1',//数据库的ip地址
user:'root',
password:'123456',
database:'my_db_01',//指定要操作哪个数据库
})
//测试mysql能否正常工作
db.query('select 1',(err,result)=>{
//mysql工作期间报错
if(err)return console.log(err.message)
//能成功执行SQL语句
console.log(result);
})

运行db.js

1
2
PS D:\Vue-chat\Server> node db.js
ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication protocol requested by server; consider upgrading MySQL client

报错原因

mysql8.0以上加密方式,Node还不支持。

解决

第一步谷歌
查到了 https://stackoverflow.com/questions/50093144/mysql-8-0-client-does-not-support-authentication-protocol-requested-by-server
这个答案,和我报错的步骤基本一样,按照这个进行操作,登录MySql Command Line Client使用这个

1
2
3
4
5
mysql> alter user 'root'@'localhost' identified with mysql_native_password by '123456';
Query OK, 0 rows affected (0.27 sec)

mysql> flush privileges;
Query OK, 0 rows affected (0.08 sec)

解决用git push origin master时出现的问题
不知道小伙伴们在使用git push origin master时有没有遇到下面的问题,今天我遇到了,原因一般是相同的,下面详细分析一下

报错内容:
To github.com:/
! [rejected] master -> master (non-fast-forward)
error: failed to push some refs to ‘git@github.com:***/***’
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: ‘git pull …’) before pushing again.
hint: See the ‘Note about fast-forwards’ in ‘git push –help’ for details.

大致翻译为:
错误:无法将一些引用推送到’git@github.com:****** / Demo.git’
提示:由于当前分支的尖端位于其远程对应的后面,因此更新被拒绝。 合并远程更改(例如’git pull’),然后再次推送。 有关详细信息,请参见“ git push –help”中的“关于fast-forwards的注意事项”。

这个报错的原因是因为远程repository和本地的repository有冲突,下面有3种解决办法:

按上面报错内容中所给的提示先用git pull之后再用git push
$ git pull origin master
$ git push origin master

使用强制push的方法,但是这会使远程修改丢失,一般是不可取的,尤其是多人协作开发的时候
$ git push origin master -f

如果不想merge远程和本地修改,可以先创建新的分支,然后再push
$ git branch [name]
$ git push origin [name]

Vim可以分为三种模式,分别为:

命令行模式(Command mode)

插入模式(Insert mode)

底行模式(Lastline mode)

命令行模式

控制屏幕光标的移动,字符、字或行的删除,移动复制某区段及进入Insert mode下,或者到 last line mode。

插入模式

只有在Insert mode下,才可以做文字输入,按「ESC」键可回到命令行模式。

底行模式

将文件保存或退出vim,也可以设置编辑环境,如寻找字符串、列出行号……等。

不过,一般我们在使用时把vi简化成两个模式,就是将底行模式(last line mode)也算入命令行模式command mode)。


5. Vim的基本操作

a) 进入Vim

在系统光标提示符后,输入vim及文件名后,回车,进入Vim编辑画面。

img

img

特别注意,进入vim之后,是处于「命令行模式(command mode)」,要切换到「插入模式(Insert mode)」才能够输入文字。

b) 切换至插入模式(Insert mode)编辑文件

「命令行模式(command mode)」按一下字母「i」就可以进入「插入模式(Insert mode)」,这时候你就可以开始输入文字了。

img

c) 插入模式(Insert mode)切换到命令行模式(Command mode)

在插入模式下,按一下「ESC」键即可。

d) 退出vim及保存文件

在命令行模式(Command mode)下,按冒号「:」键,进入底行模式(Last Line mode)

常见底行模式下的命令行:

:w filename 文件以filename为文件名保存

:wq 文件存盘并退出vim

:q 文件不存盘并退出vim

:q! 文件不存盘强制退出vim


6. 命令行模式下的功能键(部分)

a) 从命令行模式切换为插入模式的三种方式

「 i 」:从当前光标位置进入插入模式

「 a 」:从当前光标位置的下一个字符进入插入模式

「 o 」:从当前光标位置的下一行进入插入模式

b) 从插入模式切换为命令行模式

按「ESC」键

c) 移动光标

vi可以直接用键盘上的光标来上下左右移动,但正规的vi是用小写英文字母「h」、「j」、「k」、「l」,分别控制光标左、下、上、右移一格。

d) 行尾追加

不管光标在该行的什么位置,直接按大写字母「A」即可

e) 删除单词

dw:把光标停留在需要删除的单词前面,依次按下「d」「w」,即可

de:把光标停留在需要删除的单词前面(无视单词前面的空格),依次按下「d」「e」,即可

f) 从当前光标处删除到行末

把光标停留在需要删除的句子前面,依次按下「d」「$」,即可

g) 快速跳转到行首

按下数字「0」,即可

h) 使用计数以删除单词

示例:this ABC DE line FGHI JK LMN OP of words is Q RS TUV cleaned up.

依次将鼠标定位到ABC前,按「d」「2」「w」,可以删除:ABC DE

将鼠标定位到FGHI前,按「d」「4」「w」,可以删除:FGHI JK LMN OP

将鼠标定位到Q前,按「d」「3」「w」,可以删除:Q RS TUV

得到句子:

this line of words is cleaned up.

i) 删除当前行

鉴于删除当前行这一操作的高频性,vim设计者简化了快捷键,按「d」「d」即可。

连续删除两行,从当前行开始,则为:「2」「d」「d」

j) 撤销更改

a. 按小写字母「u」,撤销上一步的更改操作;

b. 按大写字母「U」,撤销整行的更改操作;

c. 「Ctrl+R」撤销上一步「撤销操作」。

k) 将刚删除的内容置入其他行

之前刚刚删除的内容,会放入Vim编辑器的寄存器中。基于如此原理,可以将之认为成Windows操作系统的「剪切」操作。

先在想删除的行,按「d」「d」,进行删除操作;

再在想插入那行的前一行按「p」键即可。

l) 替换

将光标放到需要改的字符前,先按「r」键,再输入想更改的字符即可。

m) 跳转

顶部跳转命令:先按「Control」+「g」键,在终端底部调出一栏,然后输入「g」「g」即可;

底部跳转命令:先按「Control」+「g」键,在终端底部调出一栏,然后输入大写字母「g」即可;

特定行跳转命令:直接输入行号,然后输入大写字母「g」即可。

n) 查找命令

字符串查找:

输入英文斜杠「/」,后面紧接需要搜索的字符串即可。

查找下一个相同字符串,就紧接着按小写字母「n」;

查找上一个相同字符串,就紧接着按大写字母「N」。

配对括号查找:

在编程语言里,配对括号「 ( 」「 ) 」「 { 」「 } 」很常见,当定位到一个配对括号的一半时(注意光标要定位在该括号之前),按「%」,即可自动跳转到与之匹配的第一个配对括号处。

o) 替换字符串

当前行替换命令

在标准命令行模式下,光标定位到当前行,按英文冒号「 :」,再输入s/oldstring/newstring/g 回车,即可。

完整语句为:

:s/dogg/dog/g

特定几行替换命令

在标准命令行模式下,按英文冒号「 :」,再输入#,#s/oldstring/newstring/g 回车,即可(其中#代表特定的首尾两行的行号)。

完整语句为:

***:*5,8s/dogg/dog/g

全文替换命令

在标准命令行模式下,按英文冒号「 :」,再输入%s/oldstring/newstring/g 回车,即可(其中#代表特定的首尾两行的行号)。

完整语句为:

*:*%s/dogg/dog/g

全文替换命令(每个匹配项替换前需确认)

在标准命令行模式下,按英文冒号「 :」,再输入%s/oldstring/newstring/gc 回车,即可(其中#代表特定的首尾两行的行号)。

完整语句为:

*:*%s/dogg/dog/gc

作者:饥人谷_李维超
链接:https://www.jianshu.com/p/8b679b35c9d5
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。Vim可以分为三种模式,分别为:

命令行模式(Command mode)

插入模式(Insert mode)

底行模式(Lastline mode)

命令行模式

控制屏幕光标的移动,字符、字或行的删除,移动复制某区段及进入Insert mode下,或者到 last line mode。

插入模式

只有在Insert mode下,才可以做文字输入,按「ESC」键可回到命令行模式。

底行模式

将文件保存或退出vim,也可以设置编辑环境,如寻找字符串、列出行号……等。

不过,一般我们在使用时把vi简化成两个模式,就是将底行模式(last line mode)也算入命令行模式command mode)。


5. Vim的基本操作

a) 进入Vim

在系统光标提示符后,输入vim及文件名后,回车,进入Vim编辑画面。

img

img

特别注意,进入vim之后,是处于「命令行模式(command mode)」,要切换到「插入模式(Insert mode)」才能够输入文字。

b) 切换至插入模式(Insert mode)编辑文件

「命令行模式(command mode)」按一下字母「i」就可以进入「插入模式(Insert mode)」,这时候你就可以开始输入文字了。

img

c) 插入模式(Insert mode)切换到命令行模式(Command mode)

在插入模式下,按一下「ESC」键即可。

d) 退出vim及保存文件

在命令行模式(Command mode)下,按冒号「:」键,进入底行模式(Last Line mode)

常见底行模式下的命令行:

:w filename 文件以filename为文件名保存

:wq 文件存盘并退出vim

:q 文件不存盘并退出vim

:q! 文件不存盘强制退出vim


6. 命令行模式下的功能键(部分)

a) 从命令行模式切换为插入模式的三种方式

「 i 」:从当前光标位置进入插入模式

「 a 」:从当前光标位置的下一个字符进入插入模式

「 o 」:从当前光标位置的下一行进入插入模式

b) 从插入模式切换为命令行模式

按「ESC」键

c) 移动光标

vi可以直接用键盘上的光标来上下左右移动,但正规的vi是用小写英文字母「h」、「j」、「k」、「l」,分别控制光标左、下、上、右移一格。

d) 行尾追加

不管光标在该行的什么位置,直接按大写字母「A」即可

e) 删除单词

dw:把光标停留在需要删除的单词前面,依次按下「d」「w」,即可

de:把光标停留在需要删除的单词前面(无视单词前面的空格),依次按下「d」「e」,即可

f) 从当前光标处删除到行末

把光标停留在需要删除的句子前面,依次按下「d」「$」,即可

g) 快速跳转到行首

按下数字「0」,即可

h) 使用计数以删除单词

示例:this ABC DE line FGHI JK LMN OP of words is Q RS TUV cleaned up.

依次将鼠标定位到ABC前,按「d」「2」「w」,可以删除:ABC DE

将鼠标定位到FGHI前,按「d」「4」「w」,可以删除:FGHI JK LMN OP

将鼠标定位到Q前,按「d」「3」「w」,可以删除:Q RS TUV

得到句子:

this line of words is cleaned up.

i) 删除当前行

鉴于删除当前行这一操作的高频性,vim设计者简化了快捷键,按「d」「d」即可。

连续删除两行,从当前行开始,则为:「2」「d」「d」

j) 撤销更改

a. 按小写字母「u」,撤销上一步的更改操作;

b. 按大写字母「U」,撤销整行的更改操作;

c. 「Ctrl+R」撤销上一步「撤销操作」。

k) 将刚删除的内容置入其他行

之前刚刚删除的内容,会放入Vim编辑器的寄存器中。基于如此原理,可以将之认为成Windows操作系统的「剪切」操作。

先在想删除的行,按「d」「d」,进行删除操作;

再在想插入那行的前一行按「p」键即可。

l) 替换

将光标放到需要改的字符前,先按「r」键,再输入想更改的字符即可。

m) 跳转

顶部跳转命令:先按「Control」+「g」键,在终端底部调出一栏,然后输入「g」「g」即可;

底部跳转命令:先按「Control」+「g」键,在终端底部调出一栏,然后输入大写字母「g」即可;

特定行跳转命令:直接输入行号,然后输入大写字母「g」即可。

n) 查找命令

字符串查找:

输入英文斜杠「/」,后面紧接需要搜索的字符串即可。

查找下一个相同字符串,就紧接着按小写字母「n」;

查找上一个相同字符串,就紧接着按大写字母「N」。

配对括号查找:

在编程语言里,配对括号「 ( 」「 ) 」「 { 」「 } 」很常见,当定位到一个配对括号的一半时(注意光标要定位在该括号之前),按「%」,即可自动跳转到与之匹配的第一个配对括号处。

o) 替换字符串

当前行替换命令

在标准命令行模式下,光标定位到当前行,按英文冒号「 :」,再输入s/oldstring/newstring/g 回车,即可。

完整语句为:

:s/dogg/dog/g

特定几行替换命令

在标准命令行模式下,按英文冒号「 :」,再输入#,#s/oldstring/newstring/g 回车,即可(其中#代表特定的首尾两行的行号)。

完整语句为:

***:*5,8s/dogg/dog/g

全文替换命令

在标准命令行模式下,按英文冒号「 :」,再输入%s/oldstring/newstring/g 回车,即可(其中#代表特定的首尾两行的行号)。

完整语句为:

*:*%s/dogg/dog/g

全文替换命令(每个匹配项替换前需确认)

在标准命令行模式下,按英文冒号「 :」,再输入%s/oldstring/newstring/gc 回车,即可(其中#代表特定的首尾两行的行号)。

完整语句为:

*:*%s/dogg/dog/gc

概念:

浏览器通过script标签的src属性,请求服务器上的数据,同时服务器返回一个函数的调用,这种请求数据的方式叫做JSONP

如果项目中已经配置了CORS跨域资源共享,为防止冲突,必须在配置CORS中间件之前声明JSONP的接口,否则JSONP接口会被处理成开启了CORS的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const express=require('express')
//创建web服务器
const app=express()
//导入路由模块
const apirouter=require('./apirouter')
//在配置cors中间件之前定义JSONP的接口
app.get('/api/jsonp',(req,res)=>{
//1 得到客户端请求的回调函数名称
const funcName=req.query.callback
//2 定义发送到客户端的JSON格式数据对象
const data={name:'zs',age:20}
//3 拼接一个函数调用的字符串
const scriptStr=`${funcName}(${JSON.stringify(data)})`
//4 把拼接的字符串响应给客户端的<script>标签进行解析执行
res.send(scriptStr)

})
app.use(express.urlencoded({extended:false}))
//在路由之前配置cors中间件
const cors=require('cors')
app.use(cors())

//将路由模块注册为全局中间件
app.use('/api',apirouter)
app.listen(80,()=>{
console.log('运行在http://127.0.0.1')
})

发请求:HTML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.staticfile.org/jquery/1.10.0/jquery.min.js">
</script>
</head>
<body>
<button id="btnJsonp">Jsonp</button>
<script>
$('#btnJsonp').on('click',function(){
$.ajax({
type:'GET',
url:'http://127.0.0.1/api/jsonp',
dataType:'jsonp',
success:function(res){
console.log(res)
}
})
})
</script>

</body>
</html>

JSONP调用是通过动态创建script元素并为src属性指定跨域URL实现的,此时script和img元素类似,能够不受限制地从其他域加载资源,因为JSONP是有效的JavaScript,所以JSONP响应在被加载完成之后会立即执行,比如以上例子中,服务器返回了带有src的script标签,客户端·收到响应后,就会去请求http://127.0.0.1/api/jsonp?callback(data)数据

缺点:

  1. JSONP是从不同域拉取可执行代码,如果这个域不可信,则可能在响应中加入恶意内容
  2. 不好确定JSONP请求是否失败,虽然HTML5规定了script元素的onerror时间处理程序,但还没有被任何浏览器实现,为此,开发者经常需要使用计时器决定是否放弃等待响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
apirouter.js:
const express=require('express')
//创建web服务器实例
const router=express.Router()
//挂载对应路由
router.get('/get',(req,res)=>{
res.send({
status:0,//0表示成功,1表示失败
msg:'Get请求成功',
data:req.query
})
})
//定义post接口
router.post('/post',(req,res)=>{
res.send({
status:0,//0表示成功,1表示失败
msg:'Post请求成功',
data:req.body

})
})
//定义delete接口
router.delete('/delete',(req,res)=>{
res.send({
status:0,//0表示成功,1表示失败
msg:'Delete请求成功',

})
})
module.exports=router

Get ,Post,Delete接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const express=require('express')
//创建web服务器
const app=express()
//导入路由模块
const apirouter=require('./apirouter')
app.use(express.urlencoded({extended:false}))
//在路由之前配置cors中间件,解决跨域问题
const cors=require('cors')
app.use(cors())

//将路由模块注册为全局中间件
app.use('/api',apirouter)
app.listen(80,()=>{
console.log('运行在http://127.0.0.1')
})

解决接口跨域问题

CORS(主流方案)

JSONP(有缺陷:只支持Get请求)

什么是CORS

CORS(Cross-Origin Resource Sharing,跨域资源共享),由一系列Http响应头组成,这些Http响应头决定浏览器是否阻止前端JS代码跨域获取资源。

浏览器的同源安全策略默认会阻止网页“跨域”获取资源,但如果接口服务器配置了CORS相关的HTTP响应头,就可以解决浏览器端的跨域访问限制。

浏览器:网页==》跨域请求 接口服务器(配置Access-Control-Allow-*相关响应头)

​ 《== 响应

CORS响应头部

Access-Control-Allow-Origin

响应头部可以携带一个Access-Control-Allow-Origin字段,语法如下:

Access-Control-Allow-Origin:|*

其中origin参数指定了允许访问该资源的外域URL

例如:下面字段只允许访问来自http://itcast.cn的请求

res.setHeader(‘Access-Control-Allow-Origin’,’http://itcast.cn')

*通配符表示允许来自任何域的请求

Access-Control-Allow-Headers

默认情况下,CORS仅支持客户端向服务器发送9个请求头:

Accept,Accept-Language,Content-Language,DPR,Downlink,Save-Data,Viewport-Width,Width,Content-Type(仅限于text-plain,multipart/form-data,application/x-www-form-urlencoded三者之一)

如果向服务器发送了额外的请求头信息,则需要在服务器端,通过Access-Control-Allow-Headers对额外的请求头进行声明,否则这次请求会失败

例如,允许客户端向服务器发送Content-Type请求头和X-Custom-Header请求头

res.setHeader(‘Access-Control-Allow-Header’,’Content-Type’,’X-Custom-Header’)

Access-Control-Allow-Methods

默认情况下,CORS仅支持客户端发起GET,POST,HEAD请求。如果客户端希望通过PUT,DELETE等方式请求服务器的资源,则需要在服务器端,通过Access-Control-Allow-Methods来指明实际请求所允许的HTTP方法

1
2
3
res.setHeader('Access-Control-Allow-Methods','POST,GET,DELETE,HEAD')
//允许所有HTTP请求方法
res.setHeader('Access-Control-Allow-Methods','*')

请求类型

简单请求

满足两个条件即为简单请求:

1 请求方式为GET,POST,HEAD三者之一

2 HTTP头部信息不超过以下几种字段:无定义头部字段,Accept,Accept-Language,Content-Language,Save-Data,Viewport-Width,Width,Content-Type(仅限于text-plain,multipart/form-data,application/x-www-form-urlencoded三者之一)

特点:客户端与服务器只发生一次请求

预检请求

1 请求方式为GET,POST,HEAD之外的请求方法

2 请求头中包含自定义头部字段

3 向服务器发送了application/jso格式的数据

在浏览器服务器的正式通信之前,浏览器会先发送OPTION请求进行预检,以获知该服务器是否允许该实际请求,这一次OPTION称为预检请求,服务器成功响应预检请求后才会发送真正的请求并且携带真正的数据。

特点:客户端与服务器只发生两次请求

CORS注意事项

1 CORS主要在服务器端进行配置,客户端浏览器无需做任何额外的配置,即可请求开启CORS的接口

2 CORS在浏览器中有兼容性,只有支持XMLHttpRequest Level2的浏览器才能正常访问开启了CORS的服务端接口(IE10+,Chrome4+等)

1
2
3
4
5
6
7
8
9
10
11
12
13
const express=require('express')
//创建web服务器实例
const app=express()
//导入处理查询字符串的querystring,通过这个模块提供的parse()函数,可以查询字符串,解析成对象的格式
//const qs=require('querystring')
const customBodyParser=require('./custom-body-parser')
app.use(customBodyParser)
app.post('/user',(req,res)=>{
res.send(req.body)
})
app.listen(80,()=>{
console.log('express server running at http://127.0.0.1')
})

customBodyParser.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const qs=require('querystring')
const bodyParser=(req,res,next)=>{
//1 定义一个str字符串,专门用来存储客户端发送过来的请求体数据
let str=""
//监听req的data事件
req.on('data',(chunk)=>{
str+=chunk
})
//监听req的end事件
req.on('end',()=>{
//在str存放的是完整的请求体数据,解析成对象格式 调用qs.parse()方法,把查询字符串解析为对象
const body=qs.parse(str)
req.body=body//将解析处的请求体数据挂载为req.body
next()
})
}
module.exports=bodyParser