前两篇分别整理了:
Application.cfc
CFM
CFC
Ajax
remote
JSON
这一篇不再单独讲概念。
直接做一个完整功能:
在学生管理页面输入学生姓名,点击搜索,在不刷新页面的情况下通过 Ajax 查询数据库,并显示结果。
最终结构:
student.cfm
│
│ Ajax
▼
StudentApi.cfc
│
▼
student_master
同时加入实际业务系统中必须考虑的:
Session
权限
SQL Injection
XSS
异常处理
Loading
空数据
JSON
1. 最终目录
假设项目:
/myapp
│
├─ Application.cfc
│
├─ student.cfm
│
│
└─ components/
└─ StudentApi.cfc
数据库表:
student_master
2. 数据库设计
简单设计:
CREATE TABLE student_master (
student_id INT PRIMARY KEY,
student_name VARCHAR(100) NOT NULL,
grade INT,
class_no INT,
student_number VARCHAR(20)
);
测试数据:
INSERT INTO student_master
(
student_id,
student_name,
grade,
class_no,
student_number
)
VALUES
(
1,
'山田 太郎',
1,
1,
'001'
),
(
2,
'山田 花子',
2,
3,
'010'
),
(
3,
'佐藤 一郎',
1,
2,
'002'
);
3. Application.cfc
首先建立:
Application.cfc
代码:
component {
this.name =
"ColdFusionStudyApp";
this.sessionManagement =
true;
this.sessionTimeout =
createTimeSpan(
0,
0,
30,
0
);
function onApplicationStart() {
application.datasource =
"mydb";
return true;
}
function onSessionStart() {
session.login = true;
session.userId = 1;
session.role = "ADMIN";
}
}
这里为了学习方便:
session.login = true;
实际系统当然应该通过:
登录认证
之后再设置。
4. 为什么把 Datasource 放 Application?
这里:
application.datasource = "mydb";
之后 CFC:
datasource =
application.datasource
即可。
这样避免整个项目到处写:
mydb
当然大型项目中还可以进一步使用:
Config
Environment Variable
Server Setting
管理。
5. StudentApi.cfc
建立:
components/StudentApi.cfc
完整代码:
component {
remote struct function search(
string keyword = ""
)
returnformat="json" {
local.result = {
success = false,
message = "",
data = []
};
try {
if (
!structKeyExists(
session,
"login"
)
||
!session.login
) {
local.result.message =
"ログインしてください。";
return local.result;
}
local.keyword =
trim(arguments.keyword);
if (
len(local.keyword) > 100
) {
local.result.message =
"検索条件が長すぎます。";
return local.result;
}
local.qStudent =
queryExecute(
"
SELECT
student_id,
student_name,
grade,
class_no,
student_number
FROM
student_master
WHERE
student_name
LIKE :keyword
ORDER BY
grade ASC,
class_no ASC,
student_number ASC
",
{
keyword = {
value =
"%" &
local.keyword &
"%",
cfsqltype =
"cf_sql_varchar"
}
},
{
datasource =
application.datasource
}
);
for (
local.student
in
local.qStudent
) {
arrayAppend(
local.result.data,
{
studentId =
local.student.student_id,
studentName =
local.student.student_name,
grade =
local.student.grade,
classNo =
local.student.class_no,
studentNumber =
local.student.student_number
}
);
}
local.result.success = true;
}
catch (any e) {
local.result.success = false;
local.result.message =
"検索処理中にエラーが発生しました。";
writeLog(
file = "student",
type = "error",
text =
e.message &
" / " &
e.detail
);
}
return local.result;
}
}
6. 为什么不用 SELECT *
这里故意不写:
SELECT *
而写:
SELECT
student_id,
student_name,
grade,
class_no,
student_number
原因有几个。
第一:
明确知道程序使用哪些字段
第二:
表增加字段时不会自动全部返回
第三:
减少不必要数据
第四:
代码 Review 更容易
业务系统里我越来越倾向于明确写字段。
7. 为什么使用 cfqueryparam / 参数绑定?
错误写法:
WHERE
student_name LIKE
'%#arguments.keyword#%'
如果输入:
' OR 1=1 --
可能改变 SQL 结构。
所以应该:
WHERE
student_name LIKE :keyword
参数:
{
keyword = {
value =
"%" &
local.keyword &
"%",
cfsqltype =
"cf_sql_varchar"
}
}
这样:
SQL
和:
Data
分开处理。
8. student.cfm
接下来制作页面。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>
学生検索
</title>
</head>
<body>
<h1>
学生検索
</h1>
<div>
<input
type="text"
id="keyword"
placeholder="学生名"
>
<button
type="button"
id="searchButton"
>
検索
</button>
</div>
<div
id="loading"
hidden
>
Loading...
</div>
<div
id="message"
>
</div>
<table
border="1"
id="resultTable"
>
<thead>
<tr>
<th>
学年
</th>
<th>
組
</th>
<th>
番号
</th>
<th>
氏名
</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<script>
const keyword =
document.querySelector(
"#keyword"
);
const searchButton =
document.querySelector(
"#searchButton"
);
const loading =
document.querySelector(
"#loading"
);
const message =
document.querySelector(
"#message"
);
const tbody =
document.querySelector(
"#resultTable tbody"
);
searchButton.addEventListener(
"click",
searchStudents
);
async function searchStudents() {
loading.hidden = false;
message.textContent = "";
tbody.innerHTML = "";
try {
const response =
await fetch(
"components/StudentApi.cfc",
{
method: "POST",
headers: {
"Content-Type":
"application/x-www-form-urlencoded"
},
body:
new URLSearchParams({
method:
"search",
keyword:
keyword.value
})
}
);
if (!response.ok) {
throw new Error(
"HTTP Error: "
+
response.status
);
}
const result =
await response.json();
if (!result.success) {
message.textContent =
result.message;
return;
}
if (
result.data.length === 0
) {
message.textContent =
"該当する学生はいません。";
return;
}
renderStudents(
result.data
);
}
catch (error) {
console.error(error);
message.textContent =
"通信エラーが発生しました。";
}
finally {
loading.hidden = true;
}
}
function renderStudents(
students
) {
for (
const student
of
students
) {
const tr =
document.createElement(
"tr"
);
appendCell(
tr,
student.grade
);
appendCell(
tr,
student.classNo
);
appendCell(
tr,
student.studentNumber
);
appendCell(
tr,
student.studentName
);
tbody.appendChild(tr);
}
}
function appendCell(
tr,
value
) {
const td =
document.createElement(
"td"
);
td.textContent =
value ?? "";
tr.appendChild(td);
}
</script>
</body>
</html>
9. 点击按钮之后发生什么?
用户点击:
検索
JavaScript:
searchStudents()
执行:
① Loading 显示
② 清空旧结果
③ fetch()
④ POST StudentApi.cfc
⑤ CFC search()
⑥ SQL
⑦ JSON
⑧ response.json()
⑨ renderStudents()
⑩ Loading 隐藏
10. Request 实际长什么样?
浏览器发送:
POST /components/StudentApi.cfc
Body:
method=search&keyword=山田
ColdFusion:
method = search
于是执行:
search()
参数:
keyword = 山田
于是:
arguments.keyword
就是:
山田
11. 为什么不用 innerHTML 插入学生姓名?
很多代码会写:
tbody.innerHTML += `
<tr>
<td>${student.studentName}</td>
</tr>
`;
如果数据库里恶意数据是:
<script>
alert(1)
</script>
就可能造成 XSS。
这一篇故意使用:
td.textContent = value;
浏览器会把内容当:
文字
而不是 HTML。
所以:
textContent
一般比直接拼 innerHTML 安全。
12. ColdFusion 页面输出也一样
CFM 中如果输出:
<cfoutput>
#student_name#
</cfoutput>
数据库内容如果包含:
<script>
也可能产生 XSS。
应该:
<cfoutput>
#EncodeForHTML(student_name)#
</cfoutput>
HTML 属性:
value="#EncodeForHTMLAttribute(value)#"
JavaScript Context:
EncodeForJavaScript()
URL:
EncodeForURL()
不能所有地方无脑使用同一种 Encode。
13. Loading 为什么需要 finally?
代码:
finally {
loading.hidden = true;
}
这是一个非常实用的写法。
如果只:
loading.hidden = false;
await fetch(...);
loading.hidden = true;
一旦:
fetch()
抛异常:
loading.hidden = true
可能根本执行不到。
结果页面一直:
Loading...
而:
finally
无论:
成功
失败
异常
都会执行。
14. 为什么 CFC catch 里不要直接把异常全部返回前端?
例如不要:
catch (any e) {
return {
message = e.detail
};
}
因为 e.detail 可能包含:
SQL
数据库表名
文件路径
服务器路径
Stack Trace
内部结构
这些都不应该暴露给用户。
正确思路:
用户
↓
通用错误消息
Log
↓
详细异常
所以:
writeLog(
file = "student",
type = "error",
text =
e.message &
" / " &
e.detail
);
用户只看到:
検索処理中にエラーが発生しました。
15. 权限检查应该放在哪里?
最重要的原则:
不要只在 CFM 隐藏按钮
例如:
<cfif session.role == "ADMIN">
<button>
Delete
</button>
</cfif>
这只能保证:
普通用户看不到按钮
不能阻止:
直接调用 CFC
所以 CFC 仍然必须:
if (
session.role != "ADMIN"
) {
return {
success = false,
message =
"権限がありません。"
};
}
16. CFM 验证和 CFC 验证的关系
可以两边都做。
前端:
if (
keyword.value.length > 100
) {
alert(
"100文字以内で入力してください。"
);
return;
}
这是:
UX
后端:
if (
len(local.keyword) > 100
) {
return ...
}
这是:
Security / Data Validation
前端验证:
让正常用户更方便
后端验证:
防止恶意请求
二者不是替代关系。
17. 排序时还需要注意数据类型
例如:
student_number
数据库定义:
VARCHAR
数据:
1
2
10
11
如果直接:
ORDER BY student_number
有可能结果:
1
10
11
2
因为数据库把它当:
字符串
而不是:
数字
如果业务要求数字排序,需要根据数据库进行:
ORDER BY CAST(student_number AS UNSIGNED)
或者从数据模型层面重新考虑:
这个字段究竟应该是字符串还是数字。
例如:
001
002
010
如果前导零有业务意义,那它本身就更像字符串。
所以不能只看到:
排序不对
就马上 CAST。
还需要先确认数据定义和业务含义。
18. 如果使用 jQuery
同一个功能也可以:
$("#searchButton").on(
"click",
function() {
$("#loading").show();
$.ajax({
url:
"components/StudentApi.cfc",
type:
"POST",
dataType:
"json",
data: {
method:
"search",
keyword:
$("#keyword").val()
}
})
.done(function(result) {
console.log(result);
})
.fail(function(xhr) {
console.error(
xhr.responseText
);
})
.always(function() {
$("#loading").hide();
});
}
);
对应关系:
fetch try
=
$.done
catch
=
$.fail
finally
=
$.always
19. 真正工作项目里我会进一步拆层
现在:
StudentApi.cfc
↓
SQL
学习阶段没问题。
但是项目变大以后可以继续拆:
StudentApi.cfc
│
▼
StudentService.cfc
│
▼
StudentDao.cfc
│
▼
Database
例如:
API
负责:
HTTP
参数
Session
JSON
Service:
业务规则
DAO:
SQL
这样就会越来越接近其他现代 Web Framework 的结构。
20. 最后的完整流程
最终一个学生搜索请求:
用户输入 山田
│
▼
student.cfm
│
▼
JavaScript
│
POST Ajax
│
▼
StudentApi.cfc
│
├─ Session Check
├─ Parameter Check
│
▼
queryExecute()
│
▼
student_master
│
▼
Query
│
▼
Array + Struct
│
▼
JSON
│
▼
Browser
│
▼
renderStudents()
│
▼
Table
到这里:
CFM
CFC
Application.cfc
Session
Ajax
JSON
SQL
XSS
Error Handling
这些 ColdFusion Web 开发最基础的知识点,就全部串起来了。

