跳到主要内容
稻草人
プロフィール

EP.02

💻Technology

ColdFusion 学习笔记①

从 Application.cfc 到 CFM、CFC,先理解一个 CF 项目是怎么运行的

BY 稻, 草人

我实际接触 ColdFusion 项目之后,最开始最容易困惑的并不是语法。

真正让我困惑的是:

  • Application.cfc 到底什么时候执行?
  • .cfm 和 .cfc 有什么区别?
  • 为什么浏览器访问的是 .cfm,但实际业务逻辑却在 .cfc?
  • session、request、application 到底有什么区别?
  • 一个请求从浏览器进入 ColdFusion 后,到底经历了什么?

如果这些基础关系没有弄清楚,即使能够照着已有代码修改功能,也很容易变成“代码能改,但是不知道为什么这样写”。

所以这一篇先不急着做复杂功能,而是从 ColdFusion Web 应用的整体结构开始。

目次

1. ColdFusion 到底是什么?

ColdFusion 是一种运行在服务器端的 Web 应用开发平台。

我们写的:

index.cfm
login.cfm
user.cfc
Application.cfc

并不是直接在浏览器里运行。

浏览器访问:

https://example.com/index.cfm

实际过程大致是:

浏览器
  ↓
Web Server
  ↓
ColdFusion Server
  ↓
解析 CFML
  ↓
访问数据库 / Session / 文件等
  ↓
生成 HTML / JSON
  ↓
返回浏览器

和 PHP 很像。

例如:

<cfset name = "Li">

<h1>Hello, #name#</h1>

ColdFusion 会先在服务器上执行:

<cfset name = "Li">

然后最终给浏览器返回:

<h1>Hello, Li</h1>

浏览器根本看不到原来的 CFML。

2. .cfm 是什么?

.cfm 可以理解成:

ColdFusion 的页面文件。

例如:

login.cfm
index.cfm
user_list.cfm
report.cfm

通常负责:

  • 页面显示
  • HTML
  • 表单
  • JavaScript
  • 调用 CFC
  • 接收 URL/Form 参数
  • 输出最终页面

例如:

<cfparam name="url.id" default="">

<cfquery name="qUser" datasource="mydb">
    SELECT
        id,
        user_name
    FROM
        users
    WHERE
        id = <cfqueryparam value="#url.id#" cfsqltype="cf_sql_integer">
</cfquery>

<h1>
    <cfoutput>#EncodeForHTML(qUser.user_name)#</cfoutput>
</h1>

这里的 .cfm 同时包含:

输入
↓
数据库
↓
HTML

这种写法在老 ColdFusion 项目里非常常见。

但是随着系统越来越复杂,通常会把数据库和业务逻辑逐渐拆到 .cfc。

3. .cfc 是什么?

.cfc 全称:

ColdFusion Component

可以把它理解成:

ColdFusion 的“类 / Service / API 组件”。

例如:

User.cfc
Auth.cfc
Report.cfc
Common.cfc
Student.cfc

基本结构:

<cfcomponent>

    <cffunction name="getUser" access="public" returntype="struct">

        <cfargument
            name="userId"
            type="numeric"
            required="true"
        >

        <cfset var result = {}>

        <cfset result.id = arguments.userId>
        <cfset result.name = "Li">

        <cfreturn result>

    </cffunction>

</cfcomponent>

这里:

<cfcomponent>

代表定义一个组件。

而:

<cffunction>

代表组件中的方法。

这和 Java:

class UserService {

    public User getUser(int userId) {

    }

}

概念上比较接近。

4. CFM 和 CFC 最大的区别

可以先用一句非常简单的话理解:

CFM = 页面
CFC = 功能

例如:

user_list.cfm

负责显示:

用户列表页面

而:

UserService.cfc

负责:

查询用户
新增用户
更新用户
删除用户

结构可以是:

user_list.cfm
    ↓
UserService.cfc
    ↓
Database

例如:


<cfset userService = createObject(
    "component",
    "components.UserService"
)>

<cfset users = userService.getUsers()>

然后:


<cfcomponent>

    <cffunction name="getUsers" access="public" returntype="query">

        <cfquery name="qUsers" datasource="mydb">
            SELECT
                id,
                user_name,
                email
            FROM
                users
            ORDER BY
                id
        </cfquery>

        <cfreturn qUsers>

    </cffunction>

</cfcomponent>

这样页面和业务逻辑就分开了。

5. CFC 不只是“后台文件”

这里有一个很重要的地方。

CFC 的函数可以有不同的 access。

例如:

<cffunction
    name="getUser"
    access="public"
>

常见值包括:

private
package
public
remote

其中实际 Web 开发里特别需要理解的是:

public
remote

6. public 和 remote 的区别

public

<cffunction
    name="getUser"
    access="public"
>

表示这个方法可以被 ColdFusion 内部其他组件或页面调用。

例如:

<cfset obj = createObject(
    "component",
    "UserService"
)>

<cfset result = obj.getUser(1)>

remote

<cffunction
    name="getUser"
    access="remote"
>

表示:

可以通过 HTTP 从浏览器外部调用。

例如:

/user/UserService.cfc?method=getUser&id=1

或者通过 JavaScript Ajax 调用。

因此:

remote

相当于暴露了一个接口。

这也是为什么在处理安全问题时,access="remote" 必须特别注意。

因为:

public

只是内部方法。

而:

remote

有可能直接成为 Web API。

7. Application.cfc 是什么?

这是整个 ColdFusion 项目里非常重要的文件。

通常位于项目根目录:

/project
    Application.cfc
    index.cfm
    login.cfm
    components/

ColdFusion 收到请求时,会自动寻找:

Application.cfc

它相当于整个 Web 应用的生命周期控制器。

可以控制:

  • 应用初始化
  • Session
  • 请求处理
  • 错误处理
  • 登录检查
  • Datasource
  • 全局配置
  • URL 重写相关逻辑
  • 安全处理

8. 一个最基础的 Application.cfc

例如:

component {

    this.name = "MyApplication";

    this.sessionManagement = true;

    this.sessionTimeout = createTimeSpan(
        0,
        0,
        30,
        0
    );

}

这里:

this.name

定义应用名称。

例如:

this.name = "MyApplication";

ColdFusion 会根据这个名字区分 Application Scope。

9. 开启 Session

例如:

this.sessionManagement = true;

这样才能使用:

session.userId
session.userName
session.login

例如登录成功之后:

session.userId = qUser.id;
session.userName = qUser.user_name;

之后其他页面:

<cfoutput>
    #session.userName#
</cfoutput>

仍然可以取得。

10. Application.cfc 的生命周期

这是理解 ColdFusion 项目非常关键的一部分。

常见方法包括:

onApplicationStart()
onApplicationEnd()

onSessionStart()
onSessionEnd()

onRequestStart()
onRequest()
onRequestEnd()

onError()

可以理解成:

ColdFusion Application
│
├─ Application 启动
│   └─ onApplicationStart()
│
├─ 用户 Session 开始
│   └─ onSessionStart()
│
├─ Request
│   ├─ onRequestStart()
│   ├─ xxx.cfm
│   └─ onRequestEnd()
│
└─ Error
    └─ onError()

11. onApplicationStart()

Application 第一次启动时执行。

例如:

function onApplicationStart() {

    application.appName = "My System";

    application.version = "1.0.0";

    return true;

}

可以存储:

整个应用共享的数据

比如:

application.systemName
application.config
application.masterData

12. onSessionStart()

某个用户第一次建立 Session 时执行。

例如:

function onSessionStart() {

    session.login = false;

    session.userId = "";

}

这样每个新 Session 都会默认:

未登录

13. onRequestStart()

这个方法非常重要。

几乎:

每一次请求都会执行。

例如:

function onRequestStart(targetPage) {

    if (
        !structKeyExists(session, "login")
        ||
        !session.login
    ) {

        if (
            !findNoCase(
                "login.cfm",
                arguments.targetPage
            )
        ) {

            location(
                url = "/login.cfm",
                addToken = false
            );

        }

    }

    return true;

}

意思就是:

访问任意页面
    ↓
检查 Session
    ↓
没有登录
    ↓
跳转 login.cfm

所以有时候你看到:

明明只是访问某个 cfm

结果却发生:

302
302
302

很有可能问题并不在那个页面本身,而是在:

Application.cfc
onRequestStart()

这里。

14. 为什么 Application.cfc 会造成 302 循环?

假设:

function onRequestStart(targetPage) {

    if (!session.login) {

        location("/login.cfm");

    }

}

那么访问:

/user.cfm

会:

user.cfm
↓
session.login = false
↓
302 login.cfm

然后:

login.cfm
↓
又执行 onRequestStart()
↓
session.login = false
↓
再次 302 login.cfm

于是形成:

login.cfm
↓
login.cfm
↓
login.cfm
↓
login.cfm

也就是:

Too Many Redirects

所以通常必须排除登录页面:

if (!findNoCase("login.cfm", targetPage)) {

}

15. Application Scope

Application Scope:

application.xxx

是:

所有用户共同使用。

例如:

application.systemName = "SmartFORCE";

所有用户都能读:

application.systemName

适合保存:

系统名称
共通配置
缓存
Master 数据
系统版本

16. Session Scope

Session:

session.xxx

是:

每个用户自己的会话数据。

例如:

session.userId
session.userName
session.role

用户 A:

session.userId = 100

用户 B:

session.userId = 200

互不影响。

17. Request Scope

Request:

request.xxx

生命周期非常短。

只在:

当前 HTTP Request

中有效。

例如:

request.startTime = getTickCount();

请求结束之后就消失。

18. Variables Scope

普通页面里经常看到:

variables.xxx

在 CFC 里面:

variables

通常属于:

Component Instance

例如:

component {

    variables.datasource = "mydb";

}

函数:

function getUser() {

    queryExecute(
        "
        SELECT *
        FROM users
        ",
        {},
        {
            datasource = variables.datasource
        }
    );

}

19. Arguments Scope

函数参数:

function getUser(required numeric userId) {

}

里面应该使用:

arguments.userId

例如:

if (arguments.userId <= 0) {

}

这样比直接写:

userId

更加明确。

20. Local Scope

函数内部变量最好使用:

local

例如:

function getUser(required numeric userId) {

    local.result = {};

    local.query = queryExecute(
        "
        SELECT
            id,
            user_name
        FROM
            users
        WHERE
            id = :id
        ",
        {
            id = {
                value = arguments.userId,
                cfsqltype = "cf_sql_integer"
            }
        }
    );

    return local.query;

}

旧式 CFML 里也经常看到:

<cfset var result = "">

这是为了避免变量进入 variables Scope。

21. 一个请求到底是怎么执行的?

例如浏览器访问:

https://example.com/user/list.cfm

ColdFusion 大致会:

① 找 Application.cfc

② 如果 Application 未启动
   onApplicationStart()

③ 如果是新 Session
   onSessionStart()

④ onRequestStart()

⑤ 执行 user/list.cfm

⑥ list.cfm 可能调用 UserService.cfc

⑦ UserService.cfc 查询 DB

⑧ list.cfm 生成 HTML

⑨ onRequestEnd()

⑩ 返回浏览器

画成结构:

Browser
   │
   ▼
Application.cfc
   │
   ├─ onRequestStart()
   │
   ▼
list.cfm
   │
   ▼
UserService.cfc
   │
   ▼
Database
   │
   ▼
list.cfm
   │
   ▼
HTML
   │
   ▼
Browser

22. 我现在对 CFM / CFC / Application.cfc 的理解

经过实际项目开发,我目前会把它们理解成:

Application.cfc
=
整个应用的入口和生命周期管理
CFM
=
页面层
CFC
=
业务逻辑 / Component / API

例如:

Application.cfc
       │
       ▼
student_list.cfm
       │
       ▼
StudentService.cfc
       │
       ▼
Database

当理解了这一层之后,再去看大型 ColdFusion 项目,就不会只是看到:

几百个 cfm
几百个 cfc

而是能够开始判断:

这个是页面
这个是业务逻辑
这个是 API
这个是全局处理
这个是 Session
这个是 Request

这也是我觉得学习 ColdFusion 时非常重要的第一步。