下面的例子比較復雜,但是更加令人滿意。假裝建立一個基本的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
- 附加部分