在动态网站开发过程中,最常见的两种页面情况:
以下为一个非常经典的表单类请求时序图,可以清晰看到两次请求过程:
类似于之前的操作,我们还是需要调整页面上细节:
<form>
添加必要属性action="/admin/post-add.php"
:提交给页面自身。也可以在单独创建一个 php 文件,让 action 提交到这个单独的文件。method="post"
:1. 提交的数据篇幅比较大;2. 主观意义上也是给服务端发数据。enctype=""
:暂时不提,待会会用到,再说。<input>
添加必要属性name
属性,表单元素提交必要属性label
关联,每一个表单元素都必须有一个关联的 label
,不管界面上需要还是不需要。(现在的 Web 开发者很不讲究了,很少有人注意这个问题了)表单想要被提交必须要有提交按钮的点击,所以页面上必须有提交按钮
当然,我这里提供的 HTML 绝大多数问题都已经考虑了,所以不需要太多调整。
🚩 源代码: step-38
在这个页面中,业务的核心自然是:当表单被提交(POST)回来,接收表单上填写的数据,校验,然后保存起来,具体流程如下:
xxxxxxxxxx
211// 处理提交请求
2// ========================================
3
4if ($_SERVER['REQUEST_METHOD'] == 'POST') {
5 // 数据校验
6 // ------------------------------
7 if (empty($_POST['slug'])
8 || empty($_POST['title'])
9 || empty($_POST['created'])
10 || empty($_POST['content'])
11 || empty($_POST['status'])
12 || empty($_POST['category'])) {
13 // 缺少必要数据
14 $message = '请完整填写所有内容';
15 } else if (xiu_query(sprintf("select count(1) from posts where slug = '%s'", $_POST['slug']))[0][0] > 0) {
16 // slug 重复
17 $message = '别名已经存在,请修改别名';
18 } else {
19 // 数据合法
20 }
21}
作者 ID 可以从当前登录用户信息中获取
xxxxxxxxxx
111// 接收数据
2// ------------------------------
3
4$slug = $_POST['slug'];
5$title = $_POST['title'];
6$feature = ''; // 图片稍后再考虑
7$created = $_POST['created'];
8$content = $_POST['content'];
9$status = $_POST['status']; // 作者 ID 可以从当前登录用户信息中获取
10$user_id = $current_user['id'];
11$category_id = $_POST['category'];
xxxxxxxxxx
191// 拼接查询语句
2$sql = sprintf(
3 "insert into posts values (null, '%s', '%s', '%s', '%s', '%s', 0, 0, '%s', %d, %d)",
4 $slug,
5 $title,
6 $feature,
7 $created,
8 $content,
9 $status,
10 $user_id,
11 $category_id
12);
13
14// 执行 SQL 保存数据
15if (xiu_execute($sql) > 0) {
16 // 保存成功
17} else {
18 // 保存失败,请重试
19}
xxxxxxxxxx
51<?php if (isset($message)) : ?>
2<div class="alert alert-danger">
3 <strong>错误!</strong><?php echo $message; ?>
4</div>
5<?php endif; ?>
xxxxxxxxxx
31// 保存成功 跳转
2header('Location: /admin/posts.php');
3exit;
🚩 源代码: step-39
分类数据目前是在 HTML 中写死的,十分不合适,应该是在一开始的时候加载数据库中的分类,然后展示在界面上。
再由用户选择。
xxxxxxxxxx
131// 查询数据
2// ========================================
3
4// 查询全部分类数据
5$categories = xiu_query('select * from categories');
6
7...
8
9<select id="category" class="form-control" name="category">
10 <?php foreach ($categories as $item) { ?>
11 <option value="<?php echo $item['id']; ?>"><?php echo $item['name']; ?></option>
12 <?php } ?>
13</select>
🚩 源代码: step-40
目前,如果提交表单时发生任何错误,当浏览器再次显示表单时,之前的数据全部会被清空,为了提高使用过程的体验,所以需要保持之前的填写状态,具体方法参考之前登录功能:
在服务端渲染 HTML 时,给每一个表单元素加上 value 属性,值就从表单提交过来的数据中取。
例如文章标题:
xxxxxxxxxx
11<input id="title" class="form-control input-lg" name="title" type="text" value="<?php echo isset($_POST['title']) ? $_POST['title'] : '' ?>" placeholder="文章标题">
🚩 源代码: step-41
如果希望表单可以上传文件,必须将表单的 enctype
属性设置为 multipart/form-data
,
我们不可能在数据库中保存文件,一般情况下,我们都是将文件保存到一个目录中,在数据库中保存访问路径 URL:
默认情况下表单的编码类型为:application/x-www-form-urlencoded
,表示当前表单中的数据是以 urlencoded
方式提交的。
但是对于表单中有文件域的情况下,文件是无法用文本表述的,所以必须通过另外一种编码类型:multipart/form-data
xxxxxxxxxx
31<form class="row" action="/admin/post-add.php" method="post" enctype="multipart/form-data">
2 ...
3</form>
默认文件域允许选择任何文件,可以通过 accept
属性限制:
xxxxxxxxxx
11<input id="feature" class="form-control" name="feature" type="file" accept="image/*">
http://www.w3school.com.cn/tags/att_input_accept.asp image/* 指的是任意类型图片
在表单提交到服务端时,会自动把文件上传到服务器上,PHP(内部)执行时会自动将文件存到一个临时目录,然后将相关信息定义到 $_FILES
数组中。
知道了这些,我们接下来要做的就是:
xxxxxxxxxx
141// 接收文件并保存
2// ------------------------------
3
4// 如果选择了文件 $_FILES['feature']['error'] => 0
5if (empty($_FILES['feature']['error'])) {
6 // PHP 在会自动接收客户端上传的文件到一个临时的目录
7 $temp_file = $_FILES['feature']['tmp_name'];
8 // 我们只需要把文件保存到我们指定上传目录
9 $target_file = '../static/uploads/' . $_FILES['feature']['name'];
10 if (move_uploaded_file($temp_file, $target_file)) {
11 $image_file = '/static/uploads/' . $_FILES['feature']['name'];
12 var_dump($image_file);
13 }
14}
这里我们暂时注释掉保存数据的代码,防止保存太多没必要的数据,另外如果保存成功了,页面会跳转,我们看不到调试信息:
xxxxxxxxxx
91// // 执行 SQL 保存数据
2// if (xiu_execute($sql) > 0) {
3// // 保存成功 跳转
4// header('Location: /admin/posts.php');
5// exit;
6// } else {
7// // 保存失败
8// $message = '保存失败,请重试';
9// }
经过测试发现,保存文件不会成功,错误信息:
Warning: move_uploaded_file(../static/uploads/photo.jpg): failed to open stream: Permission denied in /.../baixiu/admin/post-add.php on line 45 Warning: move_uploaded_file(): Unable to move '/private/var/tmp/phpkIg9CC' to '../static/uploads/photo.jpg' in /.../baixiu/admin/post-add.php on line 45
通过阅读错误信息发现:应该是权限一类的错误。
在每一个操作系统中都有文件权限问题:
- 系统中每一个工作进程都是由一个用户运行的;
- 如果这个用户没有指定文件或者目录的访问权,那么这个进程就无法操作这个文件或者目录;
解决方法很简单,修改一下上传目录的读写权限即可,重点要理解为什么!
U系操作系统:
xxxxxxxxxx
11$ sudo chmod -R 777 /.../baixiu/static/uploads
Windows:
右键菜单找吧;
🚩 源代码: step-42
xxxxxxxxxx
21// 上述逻辑执行成功就会定义一个 $image_file 变量
2$feature = isset($image_file) ? $image_file : '';
🚩 源代码: step-43
上述过程已经完成了,当前发表文章功能的核心业务,但是很多地方的体验不佳,例如:
<textarea>
做纯文本编辑并不合理接下来我们可以利用我们之前的所学之长,解决这些问题。
声明:谁要是还在说前端只是写页面,我 TM 弄死谁!前端也是开发者,开发者的核心是实现功能服务用户。
本地图片预览的意思就是当选择图片后在界面上显示当前选中图片,显示图片肯定就是用 <img>
标签,<img>
标签需要工作就必须有一个图片 URL,所以核心就是怎么给选中图片一个 URL。
正常我们在页面上用的 URL 都是类似 http://www.demo.com/a.jpg
这样的地址,也就是说是服务器上的一个图片,但是我们目前的情况,图片在选中过后,还没有上传到服务端,所以不可能有这种 URL 地址。
选择图片这个过程中,我们都是在浏览器本地(客户端)单机操作的,没有跟服务交互,所以这个需求应该是用 JavaScript 处理。
解决这一类的问题,核心把握:我现在在哪?我手头上有什么(能拿到什么)?我想要什么?
我现在在哪? 客户端浏览器,用户正在操作。 我手头上有什么(能拿到什么)? 与这个业务有关的东西,我们只能拿到用户操作的文件域 DOM 对象 -> 选中的文件对象。 我想要什么? 我想要这个文件域中选择文件的 URL。
那么接下来就是解决:文件对象 -> URL
在了解了 HTML5 Web API 过后,我们知道 HTML5 提供了两种办法:
后续会详细学习到这些,我们这里通过 Object URL 解决目前这个问题,在页面中加入一段 JavaScript 代码:
xxxxxxxxxx
101// 当文件域文件选择发生改变过后,本地预览选择的图片
2$('#feature').on('change', function () {
3 var file = $(this).prop('files')[0]
4 // 为这个文件对象创建一个 Object URL
5 var url = URL.createObjectURL(file)
6 // url => blob:http://zce.me/65a03a19-3e3a-446a-9956-e91cb2b76e1f
7 // 不用奇怪 BLOB: binary large object block
8 // 将图片元素显示到界面上(预览)
9 $(this).siblings('.thumbnail').attr('src', url).fadeIn()
10})
🚩 源代码: step-44
✏️ 作业: 尝试使用一些 jQuery 插件,提高图片上传过程的体验
没什么好说的,直接看结果
xxxxxxxxxx
41// slug 预览
2$('#slug').on('input', function () {
3 $(this).next().children().text($(this).val())
4})
slug 是一个专有名词,用于表示 URL 中辨别标识,就是 ID 的作用,只是比 ID 更友好,更美观 例如:
🚩 源代码: step-45
具体使用方式,参考官方示例即可。
注意:autoDownloadFontAwesome 选项
🚩 源代码: step-46
✏️ 作业: 自学 Markdown,思考 Markdown 的优势。 说明: 必须学,一个开发者的基本素质,一手格式良好的 Markdown,不解释
由于 <input type="datetime-local">
标签的 value
属性要求是一个 yyyy-MM-ddThh:mm
格式的字符串,原生 JavaScript 格式化时间特别麻烦,我们可以通过社区提供的一个开源库 moment.js 解决。
xxxxxxxxxx
21// 发布时间初始值
2$('#created').val(moment().format('YYYY-MM-DDTHH:mm'))
🚩 源代码: step-47
✏️ 作业: 将表单中发布时间元素改为第三方提供的时间选择器插件。 参考: https://eonasdan.github.io/bootstrap-datetimepicker/