ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
下面的例子比较复杂,但是更加令人满意。假装建立一个基本的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文本语法 这些功能的实现和其他的,留给你作为练习。