Web应用程序开发过程中, 文件上传功能的开发经常困扰着新同学.

这主要是因为文件从客户端来到服务器端的方式, 以及服务器端对文件的处理方式与普通数据字段不同, 所以常常让同学们摸不着头脑.

今天, 我们就以此为主题, 重点聊聊…

这个教程越写越长, 考虑再三, 还是把它分成了上 / 下 两部分, 本文是上部, 主要涉及基本理论和客户端实现, 服务器端的实现在 文件上传怎么做 (下) 中讲解.

这样同学可以同时打开两个窗口, 对照着看, 也方便不是吗 ?

1. 前言

1.1 服务器端如何存储文件 ?

很多同学常会走入一个误区, 认为客户上传的文件将会被存储到数据库中. 于是开始研究如何设计数据库的表结构, 以存储文件.

其实, 并非如此, 在大多数情况下, 文件都不是直接存储在数据库中的, 而是直接保存在文件系统 (磁盘) 里, 数据库中只是记录了文件的信息 ( 比如: 存储路径, 文件名 ). 看下图:

文件上传过程示意

如图所示, 客户选择的文件从客户端来到服务器端后, 一边将文件信息存储到了数据中, 另一边则将文件本身存储到文件系统 ( 磁盘 ).

在一些特殊场景下, 如果客户端上传的是一些尺寸较小的图片, 偶尔我们也会把图片数据进行BASE64编码, 然后直接存储到数据库里. 但这是非主流的做法.

1.2 文件怎么从客户端来到服务器端 ?

谈到这个问题, 我们就得说说 HTTP协议和RFC1867协议.

最初的 HTTP 协议并没有上传文件方面的功能. RFC1867 为 HTTP 协议添加了这个功能.

客户端的浏览器按照此规范将用户指定的文件发送到服务器, 服务器端程序则按照此规范, 解析出客户端发送来的文件数据.

RFC1867 协议在 HTTP 协议的基础上为 input 标签增加了 file 属性, 同时限定了表单 ( form ) 的 method 属性必须为 POST ( 即: 必须以POST方式提交表单 ),enctype 必须为 multipart/form-data.

在一般的 Web 应用程序中, 我们通常使用 <input type="file"> 标签, 由浏览器解析后它会产生一个文件浏览按钮, 单击此按钮将会出现操作系统的文件选择对话框.

如果我们在 HTML 文档中有如下代码:

1
2
3
4
<form action="..." method="POST">
<input type="text" name="userName" value="zhangsan">
<input type="number" name="userAge" value="18">
</form>

那么, 在提交此表单后 ( POST 方式 ), 输入框中的数据将被编码成 userName=zhangsan&userAge=18 的样子, 放在 HTTP 数据包的主体 ( Body ) 部分传输到服务器端.

现在, 我们再多加一个文件选择控件 ( 第4行 ):

1
2
3
4
5
<form action="..." method="POST" enctype="multipart/form-data">
<input type="text" name="userName" value="zhangsan">
<input type="number" name="userAge" value="18">
<input type="file" name="portrait">
</form>

这样, 用户就可以点击文件浏览按钮, 选择一张图片作为自己的头像, 随同姓名和年龄信息一起传输到服务器端了.

现在我们来看看, 加入了这个<input type="file"> 后 , 用户若填写并提交此表单, HTTP 数据包会发生什么变化 ?

(1) HTTP 数据包的头 ( Head ) 部分会由原来的:

1
content-type: application/x-www-form-urlencoded

变为

1
2
content-type: multipart/form-data; + +
boundary=---------------------------7d52b133509e2

content-type 的值变了. 同时增加了 boundary , 它其实就是华丽丽的分割线, 下文可看到 RFC1867 利用 boundary 分割 HTTP 数据包中的数据. boundary 中数字字符区是随机生成的.

(2) HTTP 数据包的主体部分

1
2
3
4
5
6
7
8
9
10
11
-----------------------------7d52b133509e2
Content-Disposition: form-data; name="file1"; filename="c:/portrait.jpg"
Content-Type: image/jpeg
图片文件的二进制数据将被放在这里
-----------------------------7d52b133509e2
Content-Disposition: form-data; name="userName"
zhangsan
-----------------------------7d52b133509e2
Content-Disposition: form-data; name="userAge"
18
-----------------------------7d52b133509e2

可以看到, 用户填的 姓名 / 年龄 会和头像文件的数据一起打包在 HTTP 数据包中, 只是其中使用了 boundary 进行分隔.

这样, 服务器端的技术 ( ASP / ASP.NET / PHP / J2EE …) 若也遵循相同的规范 ( RFC1867 ), 那么就可以解析出相应的数据了.

好了, 扯了这么多, 只是为了大概了解底层的工作原理. 其实, 还有很多细节, 我们就不再深究了…

下面, 我们直接来看简单例子, 这样你大概就可以依葫芦画瓢了…

对于 Web 应用程序来说, 只要 “接口” 对上了, 客户端和服务器端所使用的技术/方法是可以 “混搭” 的.

所以, 下文我们分为客户端程序 和 服务器端程序 两个部分来说, 你可以自由混搭.

2. 客户端实现

根据实际需要, 客户端可以是 HTML 表单提交, 也可以是 Ajax 方式提交, 当然也包括 Hybrid App, 微信小程序等.

下面我分别举例说明…

对于 2.1, 2.2 节的示例, 首先你需要使用喜欢的 IDE ( IntelliJ IDEA / Eclipse / … ) 创建一个动态网站项目, 客户端代码和服务器端代码要放在同一个项目下.

如果不明白上面这句话是什么意思, 那估计你得先补补课, 看一下这几个基础教程:

IntelliJ IDEA + Tomcat 搭建 Web 应用程序开发环境

Eclipse + Tomcat 搭建 Web 应用程序开发环境

前后端通信简明教程

2.1 HTML 表单

创建一个动态网站项目, 新建一个前端 HTML 页面, 代码如下:

Code 2.1: index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Upload Example</title>
</head>
<body>
<form action="/upload" method="POST" enctype="multipart/form-data">
<input type="text" name="userName" value="zhangsan">
<input type="number" name="userAge" value="18">
<input type="file" name="portrait">
<button type="submit">提交</button>
</form>
</body>
</html>

很简单, 以上代码就是在网页中放置了一个表单, 其中有 2 个输入框 ( userName 和 userAge ), 以及一个文件浏览控件 ( portrait ), 再加一个 “提交” 按钮.

注意 <form> 标签的 3 个属性:

action="/upload" 表明表单提交后由服务器端的谁来处理

method="POST" , enctype="multipart/form-data" 为什么要这样写? 思考一下, 不明白的话, 回头看下 1.2 节.

为了测试方便, userName 和 userAge 输入框中我们已经预先填入了 zhangsan 和 18, 省得测试的时候还要手动填写.

接下来, 就是服务器端的实现了, 可以直接跳到第 3 节继续观看…

2.2 Ajax 方式

创建一个动态网站项目, 新建一个前端 HTML 页面, 代码如下:

Code 2.1-1: index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Upload Example ( Ajax )</title>
<script src="http://libs.baidu.com/jquery/1.11.1/jquery.min.js"></script>
<script src="index.js"></script>
</head>
<body>
<form id="form1">
<input type="text" name="userName" value="zhangsan">
<input type="number" name="userAge" value="18">
<input id="filePortrait" type="file" name="portrait">
<button id="btnAjaxSubmit" type="button">Ajax方式提交</button>
</form>
</body>
</html>

第 6 行, 引入 jQuery 库, 这里使用的是百度提供的源, 所以你必须连接上 Internet .

第 7 行, 引入我们自己的 js 文件 ( 见下文 )

第 10 ~ 13 行的表单很简单. 我们这里使用 Ajax 方式提交, 所以 form 标签的 action, method, enctype 属性都用不着设置 ( 因为我们不是直接提交表单 ) . 注意 10, 13, 14 行这几个元素我们都加了 id 属性, 以便在 js 文件中引用.

OK, 接下来看 JavaScript 代码. 新建一个 js 文件: index.js , 代码如下:

Code 2.1-2: index.js

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
// 当网页就绪后, 注册 btnAjaxSubmit 按钮的点击(click)事件监听, 以触发后续的逻辑
$(document).ready(function(){
// 注册 btnAjaxSubmit 按钮的点击事件监听函数为 onAjaxSubmit
// onAjaxSubmit 是第 10 ~ 36 行定义的函数
// 注意不要手贱写成 onAjaxSubmit()
$('#btnAjaxSubmit').click(onAjaxSubmit);
});

// 以 Ajax 方式提交 (带文件)
function onAjaxSubmit() {
// 构建一个 FormData 对象
// 使用它来封装要传输到服务器端的数据和文件 ( FormData 是浏览器本来就有的东东 )
// $('#form1') 是jQuery对象, $('#form1')[0] 则是原始的表单对象
var formData = new FormData($('#form1')[0]);

// 将文件数据更新到 formData.portrait, 注意使用set方法
// 不要写成: formData.set("portrait", $('#filePortrait').files[0]);
// 可以写成: formData.set("portrait", $('#filePortrait')[0].files[0]);
formData.set("portrait", document.getElementById('filePortrait').files[0]);

// Ajax 方式提交数据到服务器端
$.ajax({
url: '/upload',
type: 'POST',
data: formData,
dataType: 'text', // 注意, 这里指的是服务器端返回的数据格式是普通文本(text)
contentType: false, // 或写为: 'multipart/form-data'
processData: false,
success: function (resp) {
alert(resp);
},
error: function () {
alert('Upload Error');
}
});
}

代码中大量的注释应该可以帮助你理解个大概了, 下面补充几点要注意的地方:

(1) 如第 1.2 节所述, 带文件的数据提交需要遵循 RFC1867 协议, 所以这里我们借助浏览器提供的 FormData 工具类来封装数据包 ( 参看第 14 行 ), 可以把它看作是一个容器.

(2) 最后就是第 22 ~ 35 行, 使用 Ajax 方法提交到服务器端了.

有一定基础的同学应该可以基本看懂了, 如果不明白, 可回炉重造, 传送门: 前后端通信简明教程

划重点:

  • 上行服务器端的所有数据 ( 含普通表单字段和文件 ) 都被封装成了一个 FormData 对象, 所以第 25 行这里只能是 data: formData , 不能再把数据散放.
  • RFC1867 协议规定, 前端上行数据包的 content-type 应为 multipart/form-data, 所以, 第 34 行这里, 要么写作 false , 表示由 jQuery 自行判断填充, 要么明确写明 'content-type: multipart/form-data'
  • 同样, 因为我们已经使用 FormData 对数据进行了封装, 所以要在第 28 行那里告诉 jQuery 不要再画蛇添足, 再对数据进行处理.

老师再次敲黑板了~ 上面 3 点要特别关注.

好了, 如果你是乖娃娃, 已经跟着把代码写好了, 那你现在可以直接飞到第 3 节, 给你的代码配个服务器端实现, 就可以跑起来了.

本节 ( 2.2 ) 和 2.1 节只是使用了不同的方法来实现客户端, 其中的代码配上相同的服务器端实现, 应会产生相同的结果. 本文最后部分(2.5节)补充了 vue + axios 实现客户端的做法。

可能有同学会想, 既然我们已经可以使用 Ajax 方式上传文件了, 能否不在页面上放置那个丑陋的 <input type=”file” > 标签? 而是直接让用户填写一个本地的路径 ( 或我们直接指定一个本地路径 ), 然后使用 JavaScript 代码从用户电脑的磁盘上读取文件数据, 上传服务器端 ?

答案是: NO!

浏览器出现安全考虑, 是不允许 JavaScript 直接去访问客户端的文件系统或执行客户端程序的 ( 专业点说, 你的 JavaScript 代码是运行于浏览器的安全沙箱 ( sandbox ) 中的, 没有那么大的权利 ). 所以只能说上面的想法太天真!

所以, 至少在浏览器环境下, 你是绕不开那个丑陋的文件浏览按钮的. 不过… 你可以把它隐藏, 然后使用一个漂亮的图片或是别的什么东东来代替, 然后监听你这个漂亮东东的 click 事件, 随后再触发文件浏览按钮的 click 事件. 比如这样: document.getElementById(“filePortrait”).click(); 嘿嘿, 我就不举例了, 如果不明白, 就再看一遍这段话, 还不明白的话… 那就算了.

至于在 Hybrid App 或 微信小程序中如何直接读取用户手机相册文件, 请看下节分解…

2.3 使用 HBuilderX 开发的 5+ App

如果你正在 HBuilderX 在开发所谓的 5+ App ( 其实就是 HyBrid App ) , 本节的小例子可供你参考如何从相册选择 ( 或相机拍摄) 一张图片上传到服务器端.

HBuilderX 是什么鬼? HBuilderX 的安装和使用方法, 以及所谓 5+ App 的基本概念和开发方法不在本文讨论范围, 可直接看这里: https://dcloud.io/

OK, 开始吧…

选择菜单 文件 → 新建 → 项目, 选择 5+ App, 填写项目名称, 创建一个新项目.

新建一个 HTML 文件 ( 本例直接使用自动创建的 index.html ) , 敲入如下代码:

Code 2.3: index.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
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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title></title>
<script type="text/javascript">

// 服务器端请求路径
var url = 'http://192.168.0.1:8080/upload';

// 上传文件
function uploadFile(path){
// 创建上传任务
var task = plus.uploader.createUpload(url, { method:'POST' },
function(resp, status){ // 上传完成回调函数
if (status == 200){
console.log(resp.responseText);
} else {
console.log('Upload Error:' + resp.responseText);
}
}
);

// 添加普通表单字段数据
task.addData('userName', 'zhangsan');
task.addData('userAge', '18');

// 提取文件名
var fileName = path.substr(path.lastIndexOf('/') + 1);
// 添加文件信息
task.addFile(path, { key: 'portrait', name: fileName });

// 启动上传任务
task.start();
}

// 拍照上传
function uploadByCamera(){
plus.camera.getCamera().captureImage(function(path){
uploadFile(path);
});
}

// 从相册选择上传
function uploadByGallery(){
plus.gallery.pick(function(path){
uploadFile(path);
});
}
</script>
</head>
<body>
<button onclick="uploadByCamera()">拍照上传</button>
<button onclick="uploadByGallery()">相册选取上传</button>
</body>
</html>

呵呵, 这种 HTML 和 JavaScript 混在一起的代码风格确实有点不符合有洁癖的程序员的审美 … 你可以像普通网页设计一样, 把 HTML, ,CSS, JavaScript 分离到不同的文件中.

我们倒着来看以上这段代码.

第 53 ~ 56 行, 页面展示部分, 很简单, 就是在页面上放了两个按钮, 点击后分别触发 使用相机拍照上传 ( uploadByCamera ) 和 从相册选择图片 ( uploadByGallery ) 上传.

第 39 ~ 43 行, 使用相机拍照上传, plus.camera.getCamera().captureImage() 是运行时环境提供的 API, 在真机调试环境下会打开手机摄像头让你拍一张照片. 我们在调用 captureImage() 时传入了一个回调函数 , 当用户拍照后会自动回调 , 同时将照片在客户端 ( 手机 ) 中的存储路径注入 ( path ). 然后, 调用 ( 触发) 第 13 ~ 36 行定义的 uploadFile() 函数, 完成图片上传.

第 46 ~ 50 行, 从相册选择图片上传, 代码逻辑与拍照上传类似 ( 参看上一段 ). 只是这里使用 plus.gallery.pick() 来触发从相册选择图片的动作.

plus.camera.getCamera().captureImage() 参见: https://www.html5plus.org/doc/zh_cn/camera.html

plus.gallery.pick() 参见: https://www.html5plus.org/doc/zh_cn/gallery.html

以上两个 API 回调时将带回图片在手机端的存储路径 ( path ), 形如: file:///…/1522437259-IMG_0006.jpg , 注意 path 中最后一个 “/“ 后的部分是图片在手机端的名称. 上述代码中第 30 行即从 path 中提取文件名部分.

若使用真机调试, 路径格式稍有不同, 但不影响代码实现逻辑.

OK, 来看核心部分, 第 13 ~ 36 行的 uploadFile() 函数:

首先, 使用 plus.uploader.createUpload() 创建一个文件上传任务 ( task ).

plus.uploader.createUpload() 参见: https://www.html5plus.org/doc/zh_cn/uploader.html

调用 createUpload() 函数需要传 3 个参数:

  • 第 1 个参数, 服务器端接口的 URL, 上述代码中我们将其在第 10 行暂存于变量 url, 以便修改.

    特别注意: 现在我们程序的客户端是运行于手机/模拟器中, 而服务器端程序运行于你的电脑, 也就是说, 客户端程序 和 服务器端程序并不是运行于同一个设备上, 也并非同一个域. 所以, 这里的服务器端接口的 url 必须使用完整路径 ( 如: http://192.168.0.1:8080/upload , 带主机头) , 而不能像一般的动态网站那样使用相对路路径 /upload. 也不能使用 http://localhost:8080/upload, 因为此时的 localhost 其实是手机/模拟器, 而不是服务器端程序所在的电脑.

    • 若使用模拟器调试程序, 可以查看一下你服务器端电脑的 IP 地址, 用服务器端电脑 IP 地址替换上述代码的 192.168.0.1 部分.

    • 若使用手机真机调试, 同样需要把上述代码的 192.168.0.1 部分替换成服务器端电脑 IP 地址.

      更重要的是, 要确保你的手机能正常访问你的服务器端.

      可以考虑两种方式:

      方式 1: 若你有无线路由器, 手机通过 WIFI 连接无线路由器, 电脑通过有线/无线方式连接同一个路由器, 让手机和电脑处于同一个通信子网. ( 可能还需要根据你的局域网建构方式做相应的配置 )

      方式 2: 打开手机热点, 电脑连接手机热点.

      注意你电脑的防火墙可能会阻止手机端访问.

  • 第 2 个参数, { method:’POST’ } 部分是一些配置信息, 这里我们只设置了提交方式. 其它的配置参数参见: https://www.html5plus.org/doc/zh_cn/uploader.html

  • 第 3 个参数是一个回调函数 ( 16 ~ 22 行 ). 文件上传任务完成后将回调此函数 ( 类似 jQuery 中 Ajax 请求的 complete 回调函数 ). 无论上传成功或失败均会回调此函数, 可以通过回调函数的第 2 个参数获得 HTTP 请求的状态码 ( 200 表示成功 ), 而其第 1 个参数则是服务器端返回的数据.

第 25 ~ 32 行, 将普通数据 ( userName, userAge ) 和 文件信息添加到上传任务中.

这段代码是不是似曾相识 ? 回头看一下 2.2 节 Code 2.1-2: index.js 中第 11 ~ 26 行的部分~

第 32 行, { key: ‘portrait’, name: fileName } 部分是随文件一起带到服务器端的关于文件的信息 ( 程序运行起来后, 对照看一下服务器端的控制台输出就能明白 )

第 35 行, 启动任务, 开始上传… ( 前面只是做准备, 此时才真正开始上传 )

好了, 跳到第 3 节, 给你的代码配个服务器端实现, 实测一下吧~

温馨提示:

(1) 调试时若提示权限不够, 应在 manifest.json 中进行相应设置

(2) 若真机调试, 手机询问是否允许使用网络时要记得”允许”哦

(3) 因客户端程序和服务器端处于不同的 “域”, 可能会出现跨域访问问题 ( CORS ). 若不幸遇到, 对服务器端作相应配置.

(4) JavaScript 代码是运行于浏览器的安全沙箱 ( sandbox ) 中的, 它无权直接去读取/操作手机磁盘上的文件. 必须通过运行环境提供的 API 进行处理 ( 如: plus.gallery.pick() ).

(5) Android / iOS 与 Windows 不同, 注意移风易俗. 虽然, 本质上来说, Hybrid App 大部分功能是使用网页开发技术实现的, 但涉及到与手机操作系统/硬件系统交互的部分, 应使用运行时环境提供的 API ( 本例即: HTML 5+ API ) 来实现.

(6) 音频的录制和上传可参考 https://www.html5plus.org/doc/zh_cn/audio.html ( plus.audio.getRecorder() ), 结合本例, 稍作改造即可.

2.4 微信小程序

微信小程序开发环境的搭建不在本文讨论范围, 若不了解, 可参看: https://mp.weixin.qq.com/cgi-bin/wx

在 “微信开发者工具” 中新建微信小程序项目.

新建项目

开发者工具应会自动创建一个 “首页”, 下面我们直接修改其首页代码, 演示在微信小程序中如何选择图片 ( 拍照 ) 上传.

修改 index.wxml 代码如下:

Code 2.4-1: index.wxml

1
2
3
<view class="container">
<button bindtap="chooseImage">选择图片</button>
</view>

非常简单的页面 ( 如下图 ). 注意第 2 行绑定了 “选择图片” 按钮的 tap 事件处理函数为 chooseImage , 见 Code 2.4-2: index.js

选择图片

修改 index.js 代码如下:

Code 2.4-2: index.js

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
Page({
// 拍照或从相册选取图片
chooseImage() {
var _this = this;
wx.chooseImage({
count: 1, // 最多选择1张图片
sizeType: ['original'], // 上传原图
sourceType: ['album', 'camera'], // 图片来源于相册或相机
success: function (res) {
const filePaths = res.tempFilePaths; // 选中的图片文件路径(数组)
if (filePaths && filePaths.length) {
_this.uploadImage(filePaths[0]);
}
}
});
},

// 上传图片
uploadImage(filePath) {
wx.uploadFile({
url: 'http://192.168.0.1:8080/upload', // 服务器端接口地址
filePath: filePath, // 图片文件的本地路径
name: 'portrait', // 图片字段名
formData: { userName: 'zhangsan', userAge: 18 }, // 其它数据字段
header: { "Content-Type": "multipart/form-data" },
success: function (res) { // API调用成功的回调函数
if (res.statusCode === 200) { // HTTP状态码为200表示通信正常
wx.showToast({ title: '上传成功' });
} else {
console.log('Upload ERROR.');
}
},
fail: function (res) { // API调用失败的回调函数
console.log('Upload ERROR.');
}
});
}
})

第 3 ~ 16 行的 chooseImage() 函数即为 “选择图片” 按钮的 tap 事件回调函数, 负责唤起拍照/从相册选取图片功能.

第 5 ~ 15 行, wx.chooseImage() 是微信提供的 API , 参看代码中的注释 和 微信官方文档 ( chooseImage ) .

第 9 ~ 14 行为用户选择/拍照后的回调函数, 其参数 res 携带了关于用户图片的信息, res.tempFilePaths 中即存放了用户选中的图片文件在用户手机端的临时存储路径. 微信小程序中用户可一次从相册中最多选中 9 张图片, 因此, res.tempFilePaths 为数组. 接下来的第 11 行判定用户确实选中了图片后, 在第 12 行调用 uploadImage 函数开始上传图片文件.

wx.chooseImage() 的 success 函数是由微信平台触发的异步回调函数, 在此函数中的 this 并非指向当前 Page , 所以不能使用 this.uploadImage( … ) 来调用上传图片函数 ( uploadImage ) . 因此, 在第 4 行暂存 this 于 _this, 以便在 success 函数中引用. 微信小程序开发中, 异步回调常使用此方法来解决 scope 问题.

第 19 ~ 37 行完成图片上传工作. 使用了微信平台提供的上传文件 API: wx.uploadFile(), 参看代码中的注释 和 微信官方文档 ( uploadFile ) .

注意几个地方:

  • 第 21 行的 url 为服务器端的接口地址, 不能像一般的动态网站那样使用相对路径 /upload , 也不可用 http://localhost:8080/upload . 小程序运行于手机, 并非服务器端, 因此, localhost 并不会指向服务器端. 小程序也并不与服务器端在同一个”域”, 因此不可直接使用相对路径. 而必须使用完整路径 (例如: http://192.168.0.1:8080/upload , 带主机头)

  • 若使用模拟器调试程序, 可以查看一下你服务器端电脑的 IP 地址, 用服务器端电脑 IP 地址替换上述代码的 192.168.0.1 部分.

  • 若使用手机真机调试, 同样需要把上述代码的 192.168.0.1 部分替换成服务器端电脑 IP 地址.

    **更重要的是, 要确保你的手机能正常访问你的服务器端 **

    可以考虑两种方式:

    方式 1: 若你有无线路由器, 手机通过 WIFI 连接无线路由器, 电脑通过有线/无线方式连接同一个路由器, 让手机和电脑处于同一个通信子网. ( 可能还需要根据你的局域网建构方式做相应的配置 )

    方式 2: 打开手机热点, 电脑连接手机热点.

    注意你电脑的防火墙可能会阻止手机端访问.

  • 第 24 行, 可使用 formData 属性携带额外的信息至服务器端 ( 参看服务器端控制台输出 )

  • 第 26 行的 success 函数是在调用微信小程序 API 成功后的回调, 并不代表整个通信过程成功. 须在回调函数中判断状态码是否 200 才能确定通信过程是否成功.

  • 微信小程序默认使用的是 HTTPS 协议, 但在开发阶段可使用 HTTP 协议: 点击微信开发者工具右上角的 详情 → 本地设置 → 选中 “不检验合法域名、web-view 版本、TLS 版本以及 HTTPS 证书”, 如下图.

    不使用HTTPS

    微信小程序正式发布版必须使用 HTTPS. 此外, 服务器端程序亦须部署于公网服务器, 并进入腾讯的微信小程序管理平台配置服务器合法域名.

2.5 Vue + Axios

与时俱进,补充 Vue + Axios 实现客户端

Code 2.5-1: index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!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>Upload Example(Vue + Axios)</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script defer src="./index.js"></script>
</head>
<body>
<div id="app">
<input type="text" v-model="userName">
<input type="number" v-model="userAge">
<input id="filePortrait" type="file">
<button type="button" @click="btnAjaxSubmit()">使用Axios提交</button>
</div>
</body>
</html>

Code 2.5-2: index.js

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
new Vue({
el: '#app',
data: {
userName: 'Johnny',
userAge: 18
},
methods: {
async btnAjaxSubmit() {
const filePortrait = document.getElementById('filePortrait').files[0]
const data = new FormData()
data.append('userName', this.userName)
data.append('userAge', this.userAge)
data.append('portrait', filePortrait)

const config = {
headers: {'Content-Type': 'multipart/form-data'}
}

try {
const result = await axios.post('/upload', data, config)
console.log(result.data)
} catch(e) {
console.error(e)
}
}
}
})

好了, 跳到下一部分, 搭配一个服务器端实现, 实测一下吧~