级别: 中级 Nicholas Chase (ibmquestions@nicholaschase.com), 自由撰稿人, Backstop Media
2007 年 8 月 02 日 本 系列 共分 6 部分,演示了如何将 Xforms 的功能与 MySQL 和 PHP 结合使用,创建在线会计工具 X-Trapolate。每一项好的编程技术都有自己擅于解决的问题。本系列主要介绍了一些 Xforms 能有效解决的问题,例如对实时计算和高交互性的需求。本系列的第 6 部分将最后回顾一下所学习过的方法,确保最终的应用程序功能完善,并介绍未来可能出现的扩展。
概述
 | |
更多本系列文章和教程
-
第 1 部分 是对整个系列的介绍,概述了各个部分涵盖了 Xforms 规范的哪些方面。
-
第 2 部分 介绍了登陆和帐户管理。
-
第 3 部分 介绍了与资产管理有关的表单的开发。
-
第 4 部分 进一步介绍了资产管理表单的开发和业务各会计方面的报表。
-
第 5 部分 介绍了债务管理和更多功能增强。
- 第 6 部分将对系列所有文章和教程进行概括和总结,包括已开发的工具、一些改进的建议和工具集进一步的工作。
|
|
本文着重介绍了在使用 Xforms 创建应用程序过程中所遇到的一些实际问题,其中使用 PHP 作为后端并使用 MySQL 作为数据库用于存储和引用。要理解这篇文章,需要对 Xforms 和 PHP 比较熟悉。(有关这方面的入门知识,请参阅 参考资料。)
回顾
本系列涵盖了很多内容,我们首先回顾一下这些内容。与真实世界中的项目一样,X-Trapolate 应用程序的最终版本与我们在开始阶段所制订的方案稍微有些出入。幸运的是,最终完成的应用程序能执行我们最初所计划的功能,但是该应用程序中还存在着一些缺陷,因此让我们来检查一下。
本系列 第 1 部分 对应用程序进行了整体设计,每一部分分别涵盖了应用程序的各个方面。因此如果目前为止您没有完全阅读本系列之前的文章或教程,这部分内容将是一个很好的介绍。
第 2 部分 解决了用户验证功能。我们创建了一个基本的登录表单和一个注册表单用于创建新帐户。最初的登录表单还算差强人意,但是随后便被新功能完全取代。它并没有考虑用户的部门,而该因素对于资产管理至关重要,同时注册过程也无法接受用户所有的数据。最重要的是,这篇教程所处理的是一个完全隔离的表单,该表单并没能保护任何后续内容。在这篇文章中(第 6 部分),我将介绍如何改正所有这些问题,包括通过修补验证系统使只有成功登录的用户才能看到表单。
在 第 3 部分 中,我们开始着手公司运营过程中所涉及的财务方面的问题。我们一开始从帐单入手,并创建了一个能显示未结算帐单的表单。这个表单能让用户接受付款、发送帐单和把帐户引用为集合。还能使用户方便地打印出与这些操作有关的自定义文字。但该表单有一项功能没有实现,那就是创建公司应该支付的表单。可以通过集成一个单独的应用程序,或者在当前应用程序中添加该功能来解决这个问题。(请回顾 第 4 部分 中介绍的发票功能。)
第 3 部分 还介绍了如何创建预算表单,使部门用户能方便地查看他们的部门及子部门的利益和损失的预算值,并能方便地与真实数据进行比对。同时,这个表单还能使用户在应用程序中添加新的部门结构,这在很大程度上需要 Xforms 的设计方法,但是最终应该有可能在一个单独的帐户管理部分中得以解决。
第 4 部分 真正触及了这个应用程序的本质。本文所介绍的表单使用户能查看已存在的发票及其内容,但是该表单最后必须同一个外部帐单应用程序相结合,因为用户不能在该表单中创建发票。
这个问题在 第 4 部分 中的资产管理表单中依然存在,这个表单使用户能够报告设备中存在的问题,并能使采购用户查看所需解决的问题。然而,该表单并没有提供创建新资产的功能。
在 第 5 部分 中,我们也创建了两个表单。第一个表单是 payables 表单,它不仅能使用户查看公司所需支付的款项,而且还可以创建新的款项。该表单使用 Xpath 操作完成了一些相当绝妙的事情。第 5 部分 向读者展示了如何使用 Xforms 数据创建简单的条形图。尽管并不是必要的,但是通过使用 Scalable Vector Graphics (SVG) 之类的技术对这个表单进行扩展能够改善表单的外观。
添加新数据(如发票和资产)之类的功能中涉及了删除的操作,我们可以通过本系列中所介绍的技巧轻易地解决这些问题;在注册表单中修复删除操作时,您将回顾这些内容。基本上对于任何需要被添加至数据库中的 “内容”,您都可以使用这个技巧。
但是其中还留下了一些验证问题。这篇教程还介绍了完成登录和验证系统的过程。
修复注册功能块
由于这个技巧能应用于多种情况,因此我们先从修复注册表单开始,这样便可以包括所有需要的数据,这些数据并不仅仅是用户的姓、名、用户名称和密码。
这个应用程序的完整代码可以从源代码下载部分获取,因此我只强调相对表单的最初版本的一些变化。
第一步,在注册表单中添加缺少的数据,如清单 1 所示。
清单 1. register.xhtml 中增加的代码段
...
<xforms:model id="acctToolRegModel" >
<xforms:instance id="acctToolRegInst" >
<registerForm xmlns="">
<password/>
<username/>
<unameopt1/>
<unameopt2/>
<unameopt3/>
<prefix/>
<firstName/>
<lastName/>
<street />
<city />
<state />
<zip />
<phone />
<deptId />
<company />
<submitRegistration/>
</registerForm>
</xforms:instance>
...
<xforms:secret class="required" bind="passwordbind" >
<xforms:label>Password: </xforms:label>
</xforms:secret><br/>
<xforms:input ref="street" >
<xforms:label>Street: </xforms:label>
</xforms:input><br/>
<xforms:input ref="city" >
<xforms:label>City: </xforms:label>
</xforms:input><br/>
<xforms:input ref="state" >
<xforms:label>State: </xforms:label>
</xforms:input><br/>
<xforms:input ref="zip" >
<xforms:label>Zip: </xforms:label>
</xforms:input><br/>
<xforms:input ref="phone" >
<xforms:label>Phone: </xforms:label>
</xforms:input><br/>
<xforms:select1 ref="deptId">
<xforms:label>Department: </xforms:label>
<xforms:item>
<xforms:label>Procurement</xforms:label>
<xforms:value>123</xforms:value>
</xforms:item>
<xforms:item>
<xforms:label>Human Resources</xforms:label>
<xforms:value>129</xforms:value>
</xforms:item>
<xforms:item>
<xforms:label>Marketing</xforms:label>
<xforms:value>333</xforms:value>
</xforms:item>
</xforms:select1><br/>
<xforms:input ref="company" >
<xforms:label>Company: </xforms:label>
</xforms:input><br/>
<xforms:submit submission="submit_registration"
bind="submitRegistrationbind">
<xforms:label>Register</xforms:label>
</xforms:submit><br/>
<hr/>
</p>
</body>
</html>
|
此处 清单 1 中的大部分数据都是直接添加到实例和表单中的值。清注意,部门信息已经作为下拉菜单添加了,因此只能添加已存在的部门。此处,这些数据都是作为静态条目表示的,但是在实际的开发过程中,您可能会想要通过 PHP 脚本填充实例来从数据库中调用这些数据。(有关这一方面的例子,请参阅 提供正确的菜单项。)
显示结果如图 1 所示。
图 1. 新的注册表单
结果使用到了 PHP 脚本,该脚本中也需要添加额外的数据,如清单 2 所示。
清单 2. register.php 中增加的代码段
<?php
if (!isset($HTTP_RAW_POST_DATA)) $HTTP_RAW_POST_DATA =
file_get_contents("php://input");
$xml = $HTTP_RAW_POST_DATA;
$doc = new DomDocument('1.0');
$doc->loadXML($xml);
$sqldb = mysql_connect('localhost', 'root');
if (!$sqldb)
die('Could not connect to MySQL server at localhost because: '
. mysql_error());
$sqlseldb = mysql_select_db('acct1', $sqldb);
if (!$sqlseldb)
die ('Can\'t select the acct1 database on MySQL server @ ' .
'localhost because: ' . mysql_error());
// first check to see if that username is already taken
$uname = $doc->getElementsByTagName("username")->item(0)->nodeValue;
$sqlQuery = sprintf('SELECT * FROM accounts WHERE username=\'%s\'',
mysql_real_escape_string($uname));
$queryData = mysql_query($sqlQuery, $sqldb);
if (!$queryData)
die ('Could not query the accounts table in the acct db because:'
. mysql_error());
if(mysql_num_rows($queryData) > 0)
{
mysql_close($sqldb);
$doc->getElementsByTagName("message")->item(0)->nodeValue =
'The username ' . $uname .
' is already taken. Choose another.';
echo $doc->saveXML();
exit;
}
else
{
$pass = mysql_real_escape_string(
$doc->getElementsByTagName("password")->item(0)->nodeValue);
$fname = mysql_real_escape_string(
$doc->getElementsByTagName("firstName")->item(0)->nodeValue);
$prefix = mysql_real_escape_string(
$doc->getElementsByTagName("prefix")->item(0)->nodeValue);
$lname = mysql_real_escape_string(
$doc->getElementsByTagName("lastName")->item(0)->nodeValue);
$street = mysql_real_escape_string(
$doc->getElementsByTagName("street")->item(0)->nodeValue);
$city = mysql_real_escape_string(
$doc->getElementsByTagName("city")->item(0)->nodeValue);
$state = mysql_real_escape_string(
$doc->getElementsByTagName("state")->item(0)->nodeValue);
$zip = mysql_real_escape_string(
$doc->getElementsByTagName("zip")->item(0)->nodeValue);
$phone = mysql_real_escape_string(
$doc->getElementsByTagName("phone")->item(0)->nodeValue);
$deptId = mysql_real_escape_string(
$doc->getElementsByTagName("deptId")->item(0)->nodeValue);
$company = mysql_real_escape_string(
$doc->getElementsByTagName("company")->item(0)->nodeValue);
$sqlQuery = "INSERT INTO accounts ( username, password, " .
"firstName, lastName, prefix, street, city, ".
"state, zip, phone, deptId, company) VALUES ( ".
sprintf("'%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', ".
"'%s', '%s', '%s', '%s');",
$uname, $pass, $fname, $lname, $prefix, $street, $city,
$state, $zip, $phone, $deptId, $company);
$queryData = mysql_query($sqlQuery, $sqldb);
if (!$queryData)
die ('Could not insert token into accounts table on MySQL ".
"server because: ' . mysql_error());
mysql_close($sqldb);
$host= $_SERVER['HTTP_HOST'];
$reldir= rtrim(dirname($_SERVER['PHP_SELF']), '/\\');
header("Location: http://$host$reldir/login.xhtml");
exit;
}
?>
|
此处我列出了整个脚本的代码,因为代码整体都有变动,并且因为它能充当一个例子,以供在应用程序中添加其它数据。我们从加载提交的实例和连接数据库开始。这里,我们首先需要查看提交的用户名是否已经被占用。请注意 mysql_real_escape_string() 函数的作用,它能通过转义单引号、分号等来帮助防止 SQL 注入攻击。在构建 SQL 语句之前,一定要检查用户提交的数据。
如果提交的用户名已被占用,那么需要向 Xforms 表单发回一个消息。我们需要通过向将要返回的 XML 实例中添加消息来实现这一目的;表单中不会显示常规的文本错误消息。
如果用户名可用,则提取剩余的数据,再一次记住要检查它。然后,可以直接在 insert 语句中使用它向数据库中添加数据。完成这些之后,关闭数据库并使用 HTTP 标头强制浏览器打开登录页面。
可以使用这一技巧用于添加新资产、发票等等。接下来我们继续看看登录过程本身。
升级登录过程
在典型的应用程序中,都有一个单一的登录页面,用户可以通过该页面验证自己的身份,用户所访问的其它页面将会根据用户是否通过验证而显示出来。如果页面是由 PHP 实现的,这一过程将会非常简单,您可以检查一个会话变量或者其它的永久指示符。然而,如果页面是 XHTML 之类的静态表单,那么这一过程会稍微有点复杂。
其中的一种方法就是,动态地加载初始数据实例,并且使页面的内容以这个实例中包含的数据结构为基础。比方说,只有在实例中包含一个 loggedIn 元素的时候,数据才有可能全部相关。然而,在这种情况下,这意味着要对已完成的工作进行大量的修改。
第二种方法就是创建了一个 PHP “包装器”,只有在用户登录后才显示静态的 XHTML。这个方法的优势就是只需对已存在的代码进行少许的改动或者无需改动,因此我们选择使用这个方案。
第一步需要确保登录页面本身能执行完成这一过程所需的所有任务。最初,表单只是检查用户名和密码,如果正确无误则创建一个 loginToken。现在,我们需要表单执行更多的功能。
幸运的是,我们所需的大部分功能,如在这个会话中添加用户的部门 ID,已经在 login_logout.php 脚本中创建过了。因此,第一步需要在把登录表单指向这个脚本,如清单 3 所示。
Listing 3. 更改 login.xhtml
...
<xforms:submission id="submit_login"
action="login_logout.php" method="post"
instance="acctToolLoginInst"
replace="instance"
ref="instance('acctToolLoginInst')" >
</xforms:submission>
</xforms:model>
</head>
<body>
<p>
...
<xforms:output class="errormessage"
ref="instance('acctToolLoginInst')/message" />
<xforms:trigger
model="acctToolLoginModel"
ref="instance('acctToolLoginInst')/loggedIn">
<xforms:label>Menu</xforms:label>
<xforms:load ev:event="DOMActivate"
resource="appManager.php?module=appMenu"/>
</xforms:trigger><br/>
</p>
</body>
</html>
|
第一个变动相当地明显;这个表单指向了改进过的 PHP 登录脚本。但是如果用户成功登录后会发生什么呢? 第 2 部分.介绍了这一功能。现在我们将填入它们。
如果用户成功登录,那么响应中将包含一个新的 loggedIn 元素(您将很快看到这个元素),这样可以使 Menu 触发器出现。这个触发器能加载一个新的页面,该页面只包含对应于那个用户的选择。我们将在 保护页面 和 提供正确的菜单 部分中创建这个功能,但是在继续深入之前需要对 login_logout.php 脚本作一些重要的修改。
login_logout.php 脚本是作为 “一般作用” 脚本创建的,它设计用于使用户能从各自的页面登录或者注销。因为现在该脚本将用于通过某个单一的地方使用户登录,所以我们可以极大地简化这个脚本,如清单 4 所示。
Listing 4. login_logout.php
<?php
session_start();
unset($_SESSION['loginToken']);
unset($_SESSION['username']);
unset($_SESSION['at_deptId']);
unset($_SESSION['acctId']);
if (!isset($HTTP_RAW_POST_DATA))
$HTTP_RAW_POST_DATA = file_get_contents("php://input");
$xml = $HTTP_RAW_POST_DATA;
$doc = new DomDocument('1.0');
if ($xml != "") {
$doc->loadXML($xml);
} else {
header("Location: login.xhtml");
}
if ($doc->GetElementsByTagName('loginLogout')->item(0)->nodeValue
== 'Logout'){
$doc->GetElementsByTagName('deptId')->item(0)->nodeValue = "";
$doc->GetElementsByTagName('loginLogout')->item(0)->nodeValue =
"Login";
$doc->GetElementsByTagName("username")->item(0)->nodeValue = "";
$doc->GetElementsByTagName("loginToken")->item(0)->nodeValue = "";
$doc->GetElementsByTagName('login')->item(0)->nodeValue=1;
echo $doc->saveXML();
exit;
}
$pass = $doc->getElementsByTagName("password")->item(0)->nodeValue;
$uname = $doc->getElementsByTagName("username")->item(0)->nodeValue;
if ($uname == '' && $pass == ''){
echo $doc->saveXML();
exit;
}
$sqldb = mysql_connect('localhost', 'root');
if (!$sqldb)
die('Could not connect to MySQL server at localhost because: ' .
mysql_error());
$sqlseldb = mysql_select_db('acct1', $sqldb);
if (!$sqlseldb)
die ('Can\'t select the acct database on MySQL server @ ".
"localhost because: ' . mysql_error());
$sqlQuery = sprintf(
"SELECT * FROM accounts WHERE username='%s' and password='%s'",
mysql_real_escape_string($uname), mysql_real_escape_string($pass));
$queryData = mysql_query($sqlQuery, $sqldb);
if (!$queryData) die ('Could not insert token into tokentable on ".
"MySQL server because: ' . mysql_error());
$doc->getElementsByTagName("password")->item(0)->nodeValue = '';
$numRows = mysql_num_rows($queryData);
if($numRows <= 0)
{
mysql_close($sqldb);
$doc->getElementsByTagName("message")->item(0)->nodeValue =
'Invalid login credentials!';
echo $doc->saveXML();
exit;
}
else
{
$row = mysql_fetch_assoc($queryData);
$_SESSION['at_deptId'] = $row['deptId'];
$_SESSION['acctId'] = $row['acctId'];
$_SESSION['username'] = $uname;
$loginToken = rand();
$_SESSION['loginToken'] = $loginToken;
$now = localtime();
$datetime = sprintf("%04d-%02d-%02d",$now[5]+1900,$now[4],$now[3]);
$sqlQuery = sprintf(
"INSERT INTO logintokens (logintoken, username, creation)".
"VALUES ( '%d', '%s', '%s');",
$_SESSION['loginToken'],
mysql_real_escape_string($username),
$datetime);
$queryData = mysql_query($sqlQuery, $sqldb);
if (!$queryData)
die ('Could not insert token into tokentable on MySQL server".
" because: ' . mysql_error());
mysql_close($sqldb);
$doc->getElementsByTagName("password")->item(0)->nodeValue = '';
$doc->GetElementsByTagName("errorMessage")->item(0)->nodeValue =
sprintf('%s Logged In',$_SESSION['username']);
$doc->getElementsByTagName('loginLogout')->item(0)->nodeValue =
"Logout";
$doc->GetElementsByTagName("loginToken")->item(0)->nodeValue =
$_SESSION['loginToken'];
$doc->getElementsByTagName('deptId')->item(0)->nodeValue =
$_SESSION['at_deptId'];
$doc->GetElementsByTagName('login')->item(0)->nodeValue=0;
$notLoggedElement =
$doc->getElementsByTagname('notLoggedIn')->item(0);
$notLoggedElement->parentNode
->appendChild($doc->createElement('loggedIn'));
$notLoggedElement->parentNode->removeChild($notLoggedElement);
echo $doc->saveXML();
exit;
}
?>
|
第一步,确保用户是通过取消相关会话变量来进行注销的。如果用户成功登录,那么用户将得到另一个 loginToken。如果没有成功,则用户将保持注销状态。
接下来,检查并确保登录信息确实已经被提交。如果没有,则返回登录页面。
脚本仍然能够处理显式的注销请求,以及构建和返回正确的实例信息,因此个人页面上的链接还将有效。
如果用户并未提交用户名或密码,则脚本会把最初的实例返回给登录页面,但是如果有了这些信息,则通过数据库检查它们。如果用户凭证不正确,那么脚本会在 message 元素(相反情况则是 errormessage 元素)中插入一条消息,然后显示在页面上,如图 2 所示。
图 2. 无效的凭证
另一方面,如果凭证正确,则像之前一样填充会话、logintokens 表、返回的实例。然而,此处的一个变化是,您还需要在返回的实例中添加 loggedIn 元素,并且移除 notLoggedIn 元素。
对实例的这些修改会使登录表单不相关,并使菜单按钮相关,如图 3 所示。
图 3. 一次成功的登录
单击 Menu 按钮会向用户显示页面列表,但是如何才能避免尚未成功登录的用户直接访问该页面呢?
保护页面
Menu 按钮使用户转到如下 URL: http://<myhost>/appManager.php?module=appMenu.
这个页面是一个 PHP 脚本,它能检查并确保在提供敏感信息之前用户已经登录。其运作方式如下(如清单 5 所示):
清单 5. appManager.php
<?php
session_start();
$loggedIn = false;
if (isset($_SESSION['loginToken'])){
$sqldb = mysql_connect('localhost', 'root');
if (!$sqldb)
die('Could not connect to MySQL server at localhost: ' .
mysql_error());
$sqlseldb = mysql_select_db('acct1', $sqldb);
if (!$sqlseldb)
die ('Can\'t select the acct database on MySQL server '.
"@ localhost because: ' . mysql_error());
$sqlQuery =
sprintf("SELECT * FROM logintokens WHERE logintoken='%s'",
mysql_real_escape_string($_SESSION['loginToken']));
$queryData = mysql_query($sqlQuery, $sqldb);
if ( mysql_fetch_assoc($queryData)){
$loggedIn = true;
}
}
$module = $_GET['module'];
if ($loggedIn){
$filename = $module.".xhtml";
header("Content-type: text/xml");
echo file_get_contents($filename);
} else {
header("Location: login.xhtml");
}
?>
|
首先,初始化会话并设置 loggedIn 变量的值为 false。该脚本将保持这种方式,除非在会话中检测到适当的 loginToken,且在数据库中核实无误。(这一方法还能使您完成一些其它的任务,如终止数据库中的标记,等等)
然后,通过 querystring 检索页面,或者说是 “模块”。如果用户已登录,则向浏览器发送适当文件的内容,确保先要指定 XML。(浏览器认为这是一个 PHP 脚本,因此不会那样自动跳过。) 如果用户尚未登录,则返回登录页面。
现在在这个例子中,所有的页面都在相同的目录中,这似乎非常不安全,实则不然。但是没有什么能阻止用户把页面移至 Web 服务器无法访问的目录中。如果这样做的话,那么 appManager.php 会成为访问这些页面的惟一途径,并且如果用户尚未登录则无法访问它们。
您还可以通过扩展这个文件来检查用户的许可,只向用户提供所允许的文件。比如说,您可能会将预算页面限制为只允许管理人员访问。
现在,应用程序差不多已经完成,但是您仍然需要处理一下菜单。
提供正确的菜单
在这种情况下,菜单只允许已登录用户访问,但是如何才能保证用户只对经过授权的选项有访问权呢? 毕竟,菜单页面本身是一个静态的 XHTML 页面,如清单 6 所示。
清单 6. Menu 页面
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/D/tdxhtml1-strict.dtd">
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:xforms="http://www.w3.org/2002/xforms"
xmlns:ev="http://www.w3.org/2001/xml-events"
>
<head>
<title>XForms Accounting Menu Page</title>
<link rel="stylesheet" href="style.css" type="text/css"/>
<xforms:model id="menuPage" >
<xforms:instance xmlns="" src="appMenu.php" />
</xforms:model>
</head>
<body>
<p>
<xforms:repeat nodeset="//link">
<xforms:trigger>
<xforms:label ref="." />
<xforms:action ev:event="DOMActivate">
<xforms:load ref="./@href" show="replace" />
</xforms:action>
</xforms:trigger>
</xforms:repeat>
</p>
</body>
</html>
|
此处的关键位于实例中。采用从 PHP 脚本 appMenu.php 中接收数据的方法取代静态的选项列表。其中的每个元素都能够稍后用于创建触发器和加载元素,它们使用每个链接元素中的 href 属性来决定在哪里发送浏览器。
appMenu.php 脚本相当简单明了,如清单 7 所示。
清单 7. appMenu.php
<?php
header("Content-type: text/xml");
$deptId = $_SESSION['deptId'];
if ($deptId = 123){
?>
<links>
<link href="appManager.php?module=billing">Billing</link>
<link href="appManager.php?module=budget">Budget</link>
<link href="appManager.php?module=invoices">Invoices</link>
<link href=
"appManager.php?module=assetManagement">Asset Management</link>
<link href="appManager.php?module=payables">Payables</link>
<link href="appManager.php?module=analyze">Analyze</link>
</links>
<?php
} else {
?>
<links>
<link href="appManager.php?module=billing">Billing</link>
<link href="appManager.php?module=budget">Budget</link>
<link href="appManager.php?module=invoices">Invoices</link>
<link href="appManager.php?module=payables">Payables</link>
<link href="appManager.php?module=analyze">Analyze</link>
</links>
<?php
}
?>
|
既然 Xforms 表单只能通过 appManager.php 访问,那么此处我们可以信赖已经登录的用户。也就是说,我们可以使用部门 ID 来动态地决定提供哪个页面。请注意,这些链接中的每一个链接都是通过 appManager.php 脚本来提供页面的。结果如图 4 所示。
图 4. 菜单页面
当然,您可以根据自己的喜好来定制页面。
结束语
在这连续的六篇文章和教程中,我向大家介绍了如何使用 Xforms、PHP 和 MySQL 创建会计工具。这个工具能执行一些如查看发票和使用强大的 Xfroms 来帮助计划预算等操作,而判断用户是否已经登陆则由 PHP 解决。通过这种方式来设计这个应用程序,能使体系结构中的各个部分发挥各自的优势并减轻弱点。
这些就是用于实际开发的所有设计。
下载 | 描述 | 名字 | 大小 | 下载方法 |
|---|
| 第 6 部分示例代码 | xtrapolate.zip | 42KB | HTTP |
|---|
参考资料 学习
获得产品和技术
讨论
关于作者  | |  | Nicholas Chase 曾经参与多家公司的网站开发,包括 Lucent Technologies、Sun Microsystems、Oracle 和 Tampa Bay Buccaneers。Nick 曾经做过高中物理教师、低放射性废弃设备管理员、在线科幻杂志的编辑、多媒体工程师、Oracle 教员以及一家交互通信公司的首席技术官。他出版了多部著作,包括 XML Primer Plus(Sam's)。 |
对本文的评价
|