下面的例子比较复杂,但是更加令人满意。假装建立一个基本的Wiki应用:一个网站内容可以由用户快速直接在浏览器中修改。wiki 这个单词来源于 夏威夷Wiki - 这个表述,意味着快速。Wiki的功能希望是:
* 每个页面都可以通过点击 编辑链接立即修改。编辑页面是由一个简单的表单构成,允许对内容的改变。
* 新的页面可以被添加,通过导航到伪装的位置,并点击编辑按钮。
* 页面内容的语法是一个标准的HTML片段(仅有可以被包含到body的元素被允许)。
* 支持多层树结构。每个页面都可以有无限的子页面。
* 当前的面包屑自动生成。在web开发中,一个面包屑表示当前页面在一个复杂结构中的位置。当使用面包屑时,可以从当前页面导航到父级容器。
* 每个页面的标题来自它的URI 。
* 使用文本文件存储创建的页面。
像之前做过的,可以准备一个项目目录 wiki 。在其中创建 web, src 和pages 目录。pages 目录将包含动态创建的Wiki内容。鉴于此,确保这个目录被正确的允许Neko的读写权限。
整个使应用运行的代码在 src 目录。
入口类是 WikiApplication ,它定义在 WikiAppllication.hx 文件:
~~~
import WikiController;
class WikiApplication
{
public static function main()
{
var uri = neko.Sys.getUri();
var repositoryPath = neko.Sys.getCwd() + "../pages";
var params = neko.Web.getParams();
var action = switch( params.get("action") )
{
case "edit":
Edit;
case "save":
Save( params.get("content") );
default:
View;
}
var controller = new WikiController(uri, repositoryPath, action);
controller.execute();
}
}
~~~
类包含了main方法,在每次web服务器调用 Wiki应用的时候都会被调用。要生成一个有效的响应,一些信息必须获得;首先,当前请求页面的URL必须知道,所以它可以加载匹配的内容。记住所有请求都会重定义到你的WikiApplication 。然后这个部分,它会解释如何通知web服务器重定义所有的页面调用到同样的执行单元。neko.Web.getUri() 方法返回相对当前页面的一个相对路径,所以如果调用URI是:
~~~
http://localhost:2000/mydirectory/mypage?param=value
~~~
返回的值将是:
~~~
/mydirectory/mypage
~~~
如果地址是Web服务器的基础地址 http://localhost:20000/ 返回值将是一个简单的斜线 / 。
应用需要的其他信息是实际内容所在的目录。现在路径相对于当前的工作目录,但是可以简单的改变它符合你的需求,也许从一个配置文件加载这个值。
最终,应用必须了解与Web服务器所做的请求关联的每一个参数。neko.Web.getParams() 返回一个map对象包括所有的 GET和POST变量。需要处理请求的参数是执行动作的一种(查看,编辑或者保存),要保存内容那么操作就是 save 。当一个请求被做出,而没有action参数,或者带有一个不可用的值,默认则使用 view 操作。
三个动作非常直观:view显示页面内容,edit会展示一个表单来编辑页面内容,而save则保存内容并显示新修改的内容,带有一个save 确认信息。
当使用一个请求时,总是假定发送的信息可以被恶意操作;传输输入参数到一个 enum 是一个确定的方式来阻止错误和意外结果。
WikiController.hx,位于 src目录下,包括WikiController类的定义,和RequestAction的枚举。每个文本内容被分配到一个私有静态变量。这是一个好的实践,使得开发人员更加容易,当他需要随后去改变一些值的时候,因为不需要他滚动整个代码区寻找可能需要改变的位置。
在类的构造函数中,函数参数被存储在实例变量供后面使用。本例中的URI,值被修改为特定的值 /root 当请求的URI是基本的根地址的时候。通过这个方式,你得到一个引用名称 root 作为主页,同样可以用于任何动态创建的页面。
你可能注意到,在WikiApplication类main方法中,WikiController是被实例化然后方法 execute()被调用。在这个方法中真正的操作发生。execute 方法决定哪个视图被渲染并实例化相应的类。所有实际生成输出的类,HTML代码,都是抽象类 Page的子类,而且,因此,他们都共享相同的方法 render(),最终生成所需的结果。结果被发送到请求它的用户代理,通过使用 neko.Lib.print()方法。在save动作情况下,execute() 方法也可以用于调用savePage()或者removePage()方法。removePage()方法当用户发送一个空的内容时发生。文件被移除而不是简单的留空内容,避免代码库生成垃圾文件。
getPageConent() 方法是一个公共方法,用在Page 类来获得页面的内容。默认的值可能被提供为方法的参数;这在请求的页面不存在时使用。getTitle和getBreadcrumbLinks()方法也用在再Page类,他们返回页面的标题,用它的URI分隔,和一个对象列表包含关于当前页面和他的祖先的链接信息。其他的私有方法支持前面描述的操作,而且是自解释的。
~~~
import haxe.Stack;
import neko.FileSystem;
import neko.io.File;
import neko.io.FileOutput;
import neko.Lib;
import Page;
class WikiController
{
private static var FILE_EXTENSION = ".wiki";
private static var ROOT_PAGE = "/root";
private static var ROOT_URI = "/";
private static var DEFAULT_EDIT_TEXT = "";
private static var DEFAULT_VIEW_TEXT = "不存在的页面,点击编辑创建页面";
private static var SAVE_MESSAGE = "页面内容保存成功";
private static var HOME_TITLE = "主页";
public var uri(default, null):String;
private var dir:String;
private var action : RequestAction;
public function new(uri:String, dir:String, action:RequestAction)
{
if(uri == ROOT_URI)
uri = ROOT_PAGE;
if(uri.substr(uri.length - ROOT_URI.length) == ROOT_URI)
uri = uri.substr(0, uri.length - ROOT_URI.length);
this.uri = uri;
this.dir = dir;
this.action = action;
}
public function execute():Void
{
var page:Page;
switch (action) {
case Edit:
page = new PageEdit(this, DEFAULT_EDIT_TEXT);
case View:
page = new PageView(this, DEFAULT_VIEW_TEXT);
case Save(content):
if(content == "")
removePage();
else
savePage(content);
page = new PageView(this,null,SAVE_MESSAGE);
}
Lib.print(page.render());
}
public function getPageContent(alternative:String):String
{
if(pageExists())
return neko.io.File.getContent(getPageFile());
else
return alternative;
}
public function getTitle():String
{
if(uri==ROOT_PAGE)
return HOME_TITLE;
else
return StringTools.urlDecode(uri.substr(uri.lastIndexOf("/",0) + 1));
}
public function getBreadcrumbLinks()
{
var list = new Array<LinkItem>();
if(uri != ROOT_PAGE)
{
var path = getPageFile();
while(path.length > dir.length)
{
if(FileSystem.exists(path))
list.unshift( {title:titleFromPath(path),uri:uriFromPath(path)} );
else
list.unshift( {title:titleFromPath(path), uri:null} );
path = path.substr(0, path.lastIndexOf("/"));
if(path == dir)
break;
path += FILE_EXTENSION;
}
}
list.unshift({title:HOME_TITLE, uri: ROOT_URI});
return list;
}
private function getPageFile():String
{
return dir + uri + FILE_EXTENSION;
}
private function getPageDirectory():String
{
return dir + getPageNamespace();
}
private function getPageNamespace():String
{
return uri.substr(0, uri.lastIndexOf("/"));
}
private function pageExists():Bool
{
return neko.FileSystem.exists(getPageFile());
}
private function uriFromPath(path:String)
{
var relative = path.substr(dir.length);
return relative.substr(0, relative.length - FILE_EXTENSION.length);
}
private function titleFromPath(path:String)
{
var file = StringTools.urlDecode(path.substr(path.lastIndexOf("/") + 1));
return if(file.substr(file.length - FILE_EXTENSION.length) == FILE_EXTENSION)
file.substr(0, file.length - FILE_EXTENSION.length);
else
file;
}
private function savePage(content:String)
{
ensureDirectoryExists(getPageDirectory);
var out = File.write(getPageFile(), true);
out.write(content);
out.close();
}
private function removePage()
{
FileSystem.deleteFile(getPageFile);
if(uri != ROOT_PAGE)
removeEmptyDirectories(getPageDirectory, dir);
}
private static function ensureDirectoryExists(dir:String)
{
var base = if(dir.substr(0,2)=="//")
"//"
else
dir.substr(0, dir.indexOf("\\")+1);
var path = dir.substr(base.length);
var parts = (~/[\/\\]/g).split(path);
for(part in parts)
{
base += '/' + part;
if(!FileSystem.exists(base))
FileSystem.createDirectory(base);
}
}
private static function removeEmptyDirectories(dir:String, root:String)
{
var d = dir;
while( d!=root && FileSystem.exists(d) && FileSystem.readDirectory(d).length==0 )
{
FileSystem.deleteDirectory(d);
d=d.substr(0, d.lastIndexOf("/"));
}
}
}
enum RequestAction
{
Edit;
View;
Save(content:String);
}
~~~
要使示例简短,一些安全处理的实践被省略了,但是小心畸形的URI。例如,取决于你如何处理它们,一个URI可能包含序列 %00 ,会阻止进一步的添加扩展,或者 ../ 会导航潜在的文件系统跳出公共可访问web目录。好的方式是使用正则匹配URI的正确性,详细在第8章介绍。
src 目录还包含 Page.hx ,里面包括了抽象类Page的定义,PageView.hx和PageEdit.hx文件包含双关的类定义。麻烦一点的在于在Page类,而其他两个只是添加一些针对查看和编辑特别的内容。页面构造函数包括一个数组的链接,被显示到页面头部。存在的值可以被修改同时新的页面可以被添加来适应所需。
render 方法只是从其他的 renderX 方法组合输出。这些方法被单独定义所以派生类可以分别重载它们,避免代码重复。
~~~
class Page
{
private static var WIKI_HOME_PAGE = ‘Wiki - Home Page’;
private static var LOGO_PATH = ‘/assets/logo.png’;
private static var LOGO_ALT = ‘logo wiki’;
private static var BREADCRUMBS_TEXT = ‘Where am I?’;
var controller : WikiController;
var altcontent : String;
var mainlinks : Array < LinkItem > ;
private function new(controller : WikiController, altcontent : String)
{
this.controller = controller;
this.altcontent = altcontent;
mainlinks = new Array();
mainlinks.push(
{title : “title”, uri : “/uri”}
);
}
public function render() : String
{
return renderHeader() + renderContent() + renderFooter();
}
private function renderHeader() : String
{
var b = new StringBuf();
b.add(‘ < !DOCTYPE HTML PUBLIC “-//W3C//DTD HTML 4.01//EN”\n’);
b.add(‘ “http://www.w3.org/TR/html4/strict.dtd”> \n’);
b.add(‘ < html > \n’); b.add(‘ < head > \n’);
b.add(‘ < title > ’ + getTitle() + ‘ < /title > \n’);
b.add(‘ < link href=”/assets/main.css” type=”text/css” rel=”stylesheet” /> \n’);
b.add(‘ < /head > \n’);
b.add(‘ < body > \n’);
b.add(‘ < div id=”header” > \n’);
b.add(‘ < div id=”wiki-header” > < a href=”/” title=”’ + WIKI_HOME_PAGE + ‘” > ’);
b.add(‘ < img src=”’ + LOGO_PATH + ‘” alt=”’ + LOGO_ALT + ‘” /> ’);
b.add(‘ < /a > < /div > \n’);
b.add(renderMainLinks());
b.add(‘ < /div > \n’);
b.add(‘ < div id=”main” > \n’);
b.add(renderBreadCrumbs());
b.add(‘ < div id=”content” > \n’);
return b.toString();
}
private function renderContent() : String
{
return controller.getPageContent(altcontent);
}
private function renderFooter() : String
{
var b = new StringBuf();
b.add(‘\n < /div > \n’);
b.add(‘ < /div > \n’);
b.add(‘ < /body > \n’);
b.add(‘ < /html > ’);
return b.toString();
}
private function renderBreadCrumbs() : String
{
var b : StringBuf = new StringBuf();
b.add(‘ < div id=\”breadcrumbs\” > ’ + BREADCRUMBS_TEXT + ‘ \n’);
b.add(‘ < ul > \n’);
var list = controller.getBreadcrumbLinks();
for(i in 0 ... list.length)
{
if(i == list.length -1)
b.add(‘ < li > ’ + list[i].title + “ < /li > \n”);
else if(list[i].uri == null)
b.add(‘ < li > ’ + list[i].title + “ » < /li > \n”);
else
b.add(‘ < li > < a href=”’ + list[i].uri + ‘” > ’ + list[i].title + “ < /a > »< /li > \n”);
}
b.add(‘ < /ul > \n’);
b.add(‘ < /div > \n’);
return b.toString();
}
private function renderMainLinks() : String
{
var b = new StringBuf();
b.add(‘ < div id=”main-links” > \n < ul > \n’);
for(item in mainlinks)
b.add(‘ < li > < a href=”’ + item.uri + ‘” > ’ + item.title + ‘ < /a > < /li > \n’);
b.add(‘ < /ul > \n’);
b.add(‘ < /div > \n’);
return b.toString();
}
private function getTitle() : String
{
return controller.getTitle();
}
}
typedef LinkItem ={
title : String,
uri : String
}
~~~
PageView.hx 文件只是添加了一个盒子到内容区域,用来显示当一个信息传递到构造器。这个消息用来传递保存确认消息。
~~~
class PageView extends Page
{
private var message : String;
public function new(controller : WikiController, altcontent : String, ?message : String)
{
super(controller, altcontent);
this.message = message;
}
private override function renderContent()
{
var result : String = ‘’;
if(message != ‘’ & & message != null)
{
result += ‘ < div class=”message” > ’ + message + ‘ < /div > ’;
}
return result + super.renderContent();
}
private override function renderFooter()
{
var b = new StringBuf();
b.add(‘\n < /div > \n’);
b.add(‘ < div id=”page-links” > \n’);
b.add(‘ < a href=”’ + controller.uri + ‘?action=edit” > edit < /a > ’);
b.add(super.renderFooter());
return b.toString();
}
}
~~~
PageEdit.hx 文件添加一个包装和一些控件来跟页面内容交互。
~~~
class PageEdit extends Page
{
private static var EDIT_TITLE_PREFIX = ‘Edit: ‘;
private static var CONTENT_LABEL = ‘The page content goes here:’;
public function new(controller : WikiController, altcontent : String)
{
super(controller, altcontent);
}
private override function getTitle()
{
return EDIT_TITLE_PREFIX + super.getTitle();
}
private override function renderHeader()
{
var b = new StringBuf();
b.add(super.renderHeader());
b.add(‘ < form action=”’ + controller.uri + ‘?action=save” ‘);
b.add(‘method=”post” > \n’);
b.add(‘ < div class=”control” > \n’);
b.add(‘ < label for=”content” > ’ + CONTENT_LABEL + ‘ < /label > \n’);
b.add(‘ < textarea name=”content” > ’);
return b.toString();
}
private override function renderFooter()
{
var b = new StringBuf();
b.add(‘ < /textarea > \n’);
b.add(‘ < /div > \n’);
b.add(‘ < div class=”control” > \n’);
b.add(‘ < input type=”button” ‘);
b.add(‘onclick=”window.location=\’’ + controller.uri + ‘\’” ‘);
b.add(‘name=”cancel” value=”Cancel” / > \n’);
b.add(‘ < input type=”submit” name=”submit” value=”Save” /> \n’);
b.add(‘ < /div > \n’); b.add(‘ < /form > ’);
b.add(super.renderFooter());
return b.toString();
}
}
~~~
Wiki应用的整个代码都写好了。现在是时候编译查看结果了。
添加 Wiki.hxml 文件到项目目录。内容和上一个例子中的十分相似:
~~~
-cp src
-neko web/index.n
-main WikiApplication
~~~
这次 main 类是 WikiApplication ,编译单元为 index.n 。注意,无论何时一个HTTP相对的目录被调用,Web服务器(NekoTools服务器默认,mod_neko如果正确配置也是)会寻找一个 index.n 文件在目录中,并执行它。因为这个理由,你可以访问 index.n 不用指定整个的文件名,但是只有web服务器执行点是web目录的时候才可以使用。
~~~
http://localhost:2000/
~~~
现在你可以看到主页的内容,实际上是一个没有内容存在的页面。点击编辑按钮,但是等待,PAGE NOT FOUND?为什么呐?这是因为,如前面观察到的,web服务器必须被指示,所有的调用直接访问你的 index.n 文件。要做到这点,必须添加一个新的文件 .htaccess 到web 目录。内容必须是:
~~~
< FilesMatch “^([_a-z0-9A-Z-])+$” >
RewriteEngine On
RewriteRule (.*) /index.n
< /FilesMatch >
~~~
这个简单的文件表示web服务器使每个URI不匹配一个存在的文件都会跳转到 idnex.n 文件。这个功能在Apache中需要启动 mod_rewrite ,或者在Neko服务器需要开启 -rewrite 开关。因此,你需要停止你的Neko服务器然后重启它:
~~~
nekotools server -rewrite
~~~
现在你可以刷新编辑页面,插入一些内容到提供的表单并确认提交。这个页面内容现在被修改保存和可视化。视觉的记过不是很好但是你可以通过添加一个样式表和一些小的logo图片迅速改善它。这些文件的引用已经在Page类产生的代码中了。添加一个目录 assets 到 web 目录下,并创建一个图片,名字是 logo.png(可以使用你的图片编辑器作图)。对于样式表,添加文件 main.css ,样式如下:
~~~
* {
margin: 0;
padding: 0;
font-size: 9pt;
font-family: Verdana, sans-serif;
}
img {
border: 0;
}
div.message {
margin: 10px 0;
padding: 4px 4px 4px 32px;
font-weight: bold;
border: 1px solid;
}
div.message {
background-color: #d5ffaa;
border-color: #4a9500;
}
#breadcrumbs {
border-bottom: 1px dashed #ccc;
padding: 0 0 4px;
margin: 0 0 16px;
}
#breadcrumbs ul {
display: inline;
}
#breadcrumbs li {
display: inline;
margin-right: 4px;
font-weight: bold;
}
#main {
padding: 20px;
}
#main-links {
padding: 10px;
text-align: right;
background-color: #f3f3f3;
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
}
#main-links li {
display: inline;
}
#main-links a {
border: 1px solid #999;
text-decoration: none;
padding: 2px 6px;
background-color: #fff;
color: #000;
}
#main-links a:hover {
background-color: #aaa;
color: #fff;
}
#page-links {
margin-top: 60px;
border-top: 1px solid #ccc;
padding: 4px 0 0;
}
label {
display: block;
margin: 0 0 8px;
}
textarea {
width: 98%;
height: 240px;
padding: 8px;
font-family: monospace;
}
div.control {
border: 1px solid #ccc;
margin: 0 0 12px;
padding: 8px;
text-align: center;
background-color: #eee;
}
h1 {
font-size: 1.5em;
margin-bottom: 1em;
}
h2 {
font-size: 1.2em;
margin: 0.5em 0;
}
h3 {
font-size: 1.1em;
margin: 0.5em 0;
}
p {
margin-bottom: 0.5em;
}
pre{
background-color: #eeeeee;
padding: 1em;
font-family: monospace;
}
~~~
Wiki应用可以通过添加功能大大的改进,例如:
* 支持文档历史版本
* 限制用户认证来添加编辑Wiki
* 解决安全问题
* Wiki文本语法
这些功能的实现和其他的,留给你作为练习。
- 本书目录
- 第一章:Haxe介绍
- 互联网开发的一个问题
- Haxe是什么,为什么产生
- Haxe编译工具
- Haxe语言
- Haxe如何工作
- 那么Neko是什么
- Haxe和Neko的必须条件
- 本章摘要
- 第二章:安装、使用Haxe和Neko
- 安装Haxe
- 使用Haxe安装程序
- 在Windows上手动安装Haxe
- Linux上手动安装Haxe
- 安装Neko
- Windows上手动安装Neko
- 在Linux上安装Neko
- Hello world! 一式三份
- 编译你的第一个Haxe应用
- 你的程序如何编译
- HXML编译文件
- 编译到Neko
- 编译为JavaScript
- 程序结构
- 编译工具开关
- 本章摘要
- 第三章:基础知识学习
- Haxe层级结构
- 标准数据类型
- 变量
- 类型推断
- 常数变量
- 简单的值类型
- 浮点类型
- 整型
- 选择数值类型
- 布尔类型
- 字符串类型
- 抽象类型
- Void 和 Null
- 动态类型
- unknown类型
- 使用untyped绕过静态类型
- 注释代码
- 转换数据类型
- Haxe数组
- Array
- List
- Map
- Haxe中使用日期时间
- 创建一个时间对象
- Date组件
- DateTools类
- 操作数据
- 操作符
- Math类
- 使用String函数
- 本章摘要
- 第四章:信息流控制
- 数据存放之外
- 条件语句
- if语句
- switch语句
- 从条件语句返回值
- 循环
- while循环
- for循环
- 循环集合
- Break和Continue
- 函数
- 类的函数
- 局部函数
- Lambda类
- 本章摘要
- 第五章:深入面向对象编程
- 类和对象
- 实例字段
- 静态字段
- 理解继承
- Super
- 函数重载
- 构造器重载
- toString()
- 抽象类和抽象方法
- 静态字段,实例变量和继承
- 继承规则
- 使用接口
- 高级类和对象特性
- 类的实现
- 类型参数
- 匿名对象
- 实现动态
- Typedef
- 扩展
- 枚举
- 构造器参数
- 本章摘要
- 第六章:组织你的代码
- 编写可重用代码
- 使用包
- 声明一个包
- 隐式导入
- 显式导入
- 枚举和包
- 类型查找顺序
- 导入一个完整的包
- 导入库
- Haxe标准库
- Haxelib库
- 其他项目中的库
- 外部库
- 使用资源
- 文档化代码
- 离线文档
- 在线文档
- 单元测试
- haxe.unit包
- 编写测试
- 本章摘要
- 第七章:错误调试
- trace函数
- trace输出
- haxe的trace和ActionScript的trace
- 异常
- 异常处理
- CallStack和ExceptionStack
- 异常管理类
- 创建完全的异常处理类
- 异常类代码
- 本章摘要
- 第八章:跨平台工具
- XML
- XML剖析
- Haxe XML API
- 正则表达式
- EReg类
- 模式
- 定时器
- 延迟动作
- 队列动作
- MD5
- 本章摘要
- 第九章:使用Haxe构建网站
- Web开发介绍
- Web 服务器
- 使用Web服务器发布内容
- HTML速成课程
- Haxe和HTML的区别
- NekoTools Web Server
- Apache安装mod_neko
- Windows安装Apache和mod_neko
- Linux安装Apache和Mod_Neko
- 第一个Haxe网站
- 使用Neko作为网页Controller
- neko.Web类
- Neko作为前端控制器
- 本章摘要
- 第十章:使用模板进行分离式设计
- 什么是模板
- Template类
- Template语法
- 使用资产
- 何时在模板中使用代码
- 服务器端模板的Templo
- 安装Templo
- 使用Templo
- haxe.Template和mtwin.Templo表达式上的区别
- Attr表达式
- Raw表达式
- 逻辑表达式
- 循环表达式
- set, fill, 和 use表达式
- Templo中使用宏
- 手动编译模版
- 第十一章:执行服务端技巧
- 第十二章:使用Flash构建交互内容
- 第十三章:使用IDE
- 第十四章:通过JavaScript制作更多交互内容
- 第十五章:通过Haxe远程通信连接所学
- 第十六章:Haxe高级话题
- 第十七章:Neko开发桌面应用
- 第十八章:用SWHX开发桌面Flash
- 第十九章:多媒体和Neko
- 第二十章:使用C/C++扩展Haxe
- 附加部分