当前位置: 首页 > news >正文

老古董Lisp实用主义入门教程(8):挠痒痒先生建网站记

logo

是时候来个真正的应用

几位奇形怪状, 百无聊赖的先生, 用Common Lisp 搞东搞西一阵子, 总觉得没有干什么正经事.
一般而言, 学习编程语言总是应该先搞点计算, 让CPU燥起来.
但是Lisp搞计算总感觉有点不太对劲, 虽然颠倒先生已经尝试把数学公式改成中序以增强动力, 但是不行. 隔壁Matlab太好用, Python太低阻, 而且我通常用C++搞计算.

那么干点什么呢?

挠痒痒先生强行刷存在感: web, web, web.

好吧, 那就来个web应用吧.

选择工具

最重要就是什么来着, 原生部署, 无框架, 无库, 无依赖. 这些都是Lisp早就玩过的啊.

只需要安装一个, 服务器, 一个网页框架, 随便选一个 hunchentoot来做服务器, 一个cl-who就能解决问题. 部署的事情, 粗鲁先生都已经解决.

安装

(ql:quickload :hunchentoot)
(ql:quickload :cl-who)

Hunchentoot

Hunchentoot is a web server written in Common Lisp and at the same time a toolkit for building dynamic websites. As a stand-alone web server, Hunchentoot is capable of HTTP/1.1 chunking (both directions), persistent connections (keep-alive), and SSL.

挠痒痒先生中文:

Hunchentoot是用Common Lisp编写的web服务器, 同时也是动态网站构建工具包. 作为一个独立web服务器, Hunchentoot支持HTTP/1.1 chunking(双向), 持久连接(keep-alive), 和SSL.

Hunchentoot provides facilities like automatic session handling (with and without cookies), logging, customizable error handling, and easy access to GET and POST parameters sent by the client. It does not include functionality to programmatically generate HTML output. For this task you can use any library you like, e.g. (shameless self-plug) CL-WHO or HTML-TEMPLATE.

挠痒痒先生中文:

Hunchentoot提供大量工具, 类似于自动会话处理(有和无cookie), 日志, 可定制的错误处理, 以及方便的访问客户端发送的GET和POST参数. 它不含HTML生成功能. 可以使用相应库来完成, 比如CL-WHO(同一作者)或HTML-TEMPLATE.

Hunchentoot talks with its front-end or with the client over TCP/IP sockets and optionally uses multiprocessing to handle several requests at the same time. Therefore, it cannot be implemented completely in portable Common Lisp. It currently works with LispWorks and all Lisps which are supported by the compatibility layers usocket and Bordeaux Threads.

挠痒痒先生中文:

Hunchentoot通过TCP/IP套接字与前端或客户端通信, 可选择使用多进程同时处理多请求. 因此, 它无法完全在可移植的Common Lisp中实现. 它目前可以在LispWorks和所有由兼容层usocket和Bordeaux Threads支持的Lisp中工作.^1

Hunchentoot comes with a BSD-style license so you can basically do with it whatever you want.

挠痒痒先生中文:

Hunchentoot使用类BSD许可证, 非常自由.

CL-WHO

Common Lisp是产生结构文档的最佳语言, 没有之一. 当然这是完美先生的个人意见, 他简直被CL操作符号的能力震撼得不知所措.

CL-WHO是一个Common Lisp库, 用于生成HTML, XML, 和其他结构文档. 它是一个简单的DSL, 用于生成文档. 它的目标是生成可读性高, 可维护性强的文档. 它的输出是一个字符串, 可以直接发送给客户端. 同样CL-WHO也是BSD许可证,相当自由.

与Hunchentoot不同, 所有的Common Lisp实现都可以运行CL-WHO.

古早游戏投票系统

挠痒痒先生自己还没有学会用Common Lisp写游戏, 他擅长爬网站, 因为手长.

这不, 他找到大神的著作Lisp for Web, 这里有一个古早游戏投票系统的例子.

这个例子也很简单, 为经典的游戏投票, 在线投票的票数会实时更新. 也能增加自己喜欢但是没有列出的游戏.

完美先生觉得这个例子很适合, 他也喜欢古早游戏, 他也想要一个投票系统.

开发过程

Lisp的开发过程懒惰先生整理得很清楚, 这里就不再赘述.

(ql:quickload '(:hunchentoot :cl-who  :trivial-open-browser))(defpackage :retro-games(:nicknames :rg)(:use :cl :hunchentoot :cl-who)(:export :main))(in-package :retro-games)

这样, 就可以在文件中增加代码, 在代码中直接使用cl, hunchentoot, 和cl-who的函数. 当然, 所有的符号都会定义在retro-games包中, 这个包的别名叫rg. 这里还导出了一个函数main, 用于启动web服务器.

首先是后台部分

(defclass game ()((name :initarg :name:reader name)(votes :initform 0:accessor votes)))

这里定义了一个game类, 有两个slot, 一个是name, 一个是votes. name是游戏的名字, votes是投票数.


(defmethod vote-for (user-selected-game)(incf (votes user-selected-game)))

这里定义了一个方法, 用于投票. 传入一个游戏对象, 投票数加1.


(defmethod print-object ((object game) stream)(print-unreadable-object (object stream :type t)(with-slots (name votes) object(format stream "name: ~s with ~d votes" name votes))))

这里定义了一个方法, 用于打印对象. 传入一个游戏对象, 打印出游戏名字和投票数. 这个方法, 会被系统的print函数调用.


(defvar *games* '())(defun game-from-name (name)(find name *games* :key #'name :test #'string-equal))

这个函数用于根据游戏名字, 返回游戏对象.


(defun game-stored-p (name)(game-from-name name))

这个函数判断游戏是否已经存在.


(defun games ()(sort (copy-list *games*) #'> :key #'votes))

这个函数返回所有游戏, 按照投票数排序.


(defun add-game (name)(unless (game-stored-p name)(push (make-instance 'game :name name) *games*)))

最后是增加游戏的函数.

完美先生, 鲁莽先生, 粗鲁先生, 懒惰先生, 他们都是暴白, 所以, 你懂的.

(mapcar #'add-game '("魔兽世界" "魔兽争霸" "魔兽争霸2" "魔兽争霸3" "风暴英雄"))

风暴英雄今年必火!

(game-from-name "魔兽世界")
#<GAME name: "魔兽世界" with 0 votes>

这里返回的游戏对象, 其打印形式就是由print-object方法定义的.

其次是前台部分

(setf (html-mode) :html5)(defmacro standard-page ((&key title) &body body)`(with-html-output-to-string(*standard-output* nil :prologue t :indent t)(:html :lang "en"(:head(:meta :charset "utf-8")(:title ,title)(:link :type "text/css":rel "stylesheet":href "/retro.css"))(:body(:div :id "header" ; Retro games header(:img :src "/logo.png":alt "Comodore 64":class "logo")(:span :class "strapline""Vote on your favarourite retro games")),@body))))

前台的部分特别简单, 定义了一个宏standard-page, 用于生成标准页面. 页面包括了html, head, body, div, img, span等标签.

最终页面的本体, 也就是body部分, 由调用者传入.

完美先生认为挠痒痒先生就不用纠结, 用就行了.

(standard-page(:title "Page 1"))

这样就生成了一个页面, 但是没有内容.

<!DOCTYPE html><html lang='en'><head><meta charset='utf-8'><title>Page 1</title><link type='text/css' rel='stylesheet' href='/retro.css'></head><body><div id='header'><img src='/logo.png' alt='Comodore 64' class='logo'><span class='strapline'>Vote on your favarourite retro games</span></div></body>
</html>

增加内容, 也就是body部分.

(standard-page(:title "Page 1")(:h1 "Hello, World!"))

这样就生成了一个页面, 有一个标题.

<!DOCTYPE html><html lang='en'><head><meta charset='utf-8'><title>Page 2</title><link type='text/css' rel='stylesheet' href='/retro.css'></head><body><div id='header'><img src='/logo.png' alt='Comodore 64' class='logo'><span class='strapline'>Vote on your favarourite retro games</span></div><h1>Hello World</h1></body>
</html>

大概的情况就是这样, 完美先生编(抄)完这个完美的宏,感觉自己实在太完美.

最后是web服务器

Hunchentoot的结构很简单, 有一个对象ACCEPTOR, 用于接受请求.

(describe 'hunchentoot:acceptor)
HUNCHENTOOT:ACCEPTOR[symbol]
ACCEPTOR names the standard-class #<STANDARD-CLASS HUNCHENTOOT:ACCEPTOR>:Documentation:To create a Hunchentoot webserver, you make aninstance of this class and use the generic function START to start it(and STOP to stop it).  Use the :PORT initarg if you don't want tolisten on the default http port 80.  There are other initargs most ofwhich you probably won't need very often.  They are explained indetail in the docstrings of the slot definitions for this class.Unless you are in a Lisp without MP capabilities, you can have severalactive instances of ACCEPTOR (listening on different ports) at thesame time.Direct superclasses: STANDARD-OBJECTDirect subclasses: HUNCHENTOOT:EASY-ACCEPTOR, HUNCHENTOOT:SSL-ACCEPTORNot yet finalized.Direct slots:HUNCHENTOOT::PORTInitargs: :PORTReaders: HUNCHENTOOT:ACCEPTOR-PORTDocumentation:The port the acceptor is listening on.  Thedefault is 80.  Note that depending on your operating system you mightneed special privileges to listen on port 80.  When 0, the port will bechosen by the system the first time the acceptor is started.
;; 太长不列...

这个类的基本结构如上所示, 主要的slotport, address, name, request-class, reply-class, taskmaster, output-chunking-p, input-chunking-p, persistent-connections-p, read-timeout, write-timeout, listen-socket, listen-backlog, acceptor-shutdown-p, requests-in-progress, shutdown-queue, shutdown-lock, access-log-destination, message-log-destination, error-template-directory, document-root, 等等.

两个主要的子类是easy-acceptorssl-acceptor.

classDiagramclass ACCEPTOR {+port+address+name+document-root}ACCEPTOR <|-- EASY-ACCEPTORACCEPTOR <|-- SSL-ACCEPTOR

这里的例子, 就主要需要用到port, document-root这两个slot.

(defvar *hunchentoot-directory*(pathname (directory-namestring #.(or *compile-file-pathname* *load-truename*))))(defvar *server*(make-instance 'easy-acceptor:port 8080:document-root *hunchentoot-directory*))(defun start-server ()(start *server*))(defun stop-server ()(stop *server* :soft nil))

这个服务器,已经可以运行, 并且把当前目录作为document-root, 也就是说, 服务器会把当前目录下的文件作为静态文件提供.只有这样, 前面/retro.css/logo.png才能被访问到.

那么,其他的功能呢?在Common Lisp中,可以一边运行着服务器,一边增加功能,并且不需要重启服务器.这就是Common Lisp的强大之处.

首先是主页面, 用"/retro-games"作为URI, 这就是Hunchentoot提供的路由方式, 每定义一个路由, 就是一个easy-handler.

(define-easy-handler (retro-games :uri "/retro-games") ()(standard-page(:title "Top Tetro Games")(:h1 "Vote on your all time favorite retro games")(:p "Missiong a game? Make it available for votes"(:a :href "new-game" "here"))(:h2 "Current stand")(:div :id "chart"(:ol(dolist (game (games))(htm(:li (:a :href (format nil "vote?name=~a" (name game)) "Vote!")(fmt "~A with ~d votes" (escape-string (name game))(votes game)))))))))

除了显示主页面, 我们还需要增加一个给游戏投票的页面, 用"/vote?name=xxx"作为URI.

(define-easy-handler (vote :uri "/vote") (name)(when (game-stored-p name)(vote-for (game-from-name name)))(redirect "/retro-games"))

这个路由,增加游戏的投票数, 然后重定向到主页面.

最后, 增加一个页面, 用于增加游戏.

(define-easy-handler (new-game :uri "/new-game") ()(standard-page(:title "Add a new game")(:h1 "Add a new game to the chart")(:form :action "game-added" :method "post" :id "addform"(:p "What is the name of the game?" (:br))(:input :type "text" :name "name" :class "txt")(:p(:input :type "submit" :value "Add" :class "btn")))))

这个路由显示一个表单, 注意表单的定义, 也是CL-WHO提供的宏.表单的action是"/game-added", 'method’是"post", 这个表单提交后, 会调用"/game-added"这个路由.

(define-easy-handler (game-added :uri "/game-added") (name)(unless (or (null name) (zerop (length name)))(add-game name))(redirect "/retro-games"))

这个路由, 用于增加游戏,如果是空白字符串, 什么也不做, 否则调用前面定义的add-game函数, 然后重定向到主页面.

最后是部署

我们还是使用粗鲁先生的方法, 用sbcl启动REPL, 然后加载文件, 最后调用main函数.

这里提供一个打开测试页面的函数, 用于在启动服务器后, 打开浏览器.

; run open browser by ignoring the error
(defun open-browser (url)(ignore-errors(trivial-open-browser:open-browser url)))(defun wait-till-quit ()(sb-thread:join-thread (find-if(lambda (th)(search "hunchentoot-listener-" (sb-thread:thread-name th)))(sb-thread:list-all-threads))))(defun kill-server-thread ()(sb-thread:terminate-thread (find-if(lambda (th)(search "hunchentoot-listener-" (sb-thread:thread-name th)))(sb-thread:list-all-threads))))(defun main ()(start-server)(open-browser "http://localhost:8080/retro-games");; handle break signal(handler-case (wait-till-quit)(sb-sys:interactive-interrupt ()(format t "Try Stop the Server.~%")(stop-server)(when (started-p *server*)(kill-server-thread)(format t "Server Stopped.~%")))))

后面增加大量的用于关闭服务器的代码, 用于处理中断信号, 以及关闭服务器线程. 写了一大堆, 在windows上都不行……据说在linux上可以用。

你要问Windows上怎么关闭? 挠痒痒先生可以在楼下把手伸进来按100次Ctrl+C

编译成可执行文件以及丑化页面

;;;; build-retro-games.lisp
(load "retro-games.lisp")(sb-ext:save-lisp-and-die #p"retro-games.exe":toplevel #'rg:main:executable t)
$ sbcl --script build-retro-games.lisp

此外, 这里还用了一些css文件, 用于美化页面, 以及一个png文件, 用于图标.

/* set logo image */
.logo {width: auto;height: 30px;
}.strapline {font-family: 'Retro', sans-serif;font-size: 2em;color: #fff;text-shadow: 2px 2px 2px #000;
}/* set list with id chart */
/* list item style */
#chart {list-style-type: none;margin: 0;padding: 0;.li {margin: 0;padding: 0;border-bottom: 1px solid #fff;background-color: #000;color: #fff;font-family: 'Retro', sans-serif;font-size: 1.5em;text-shadow: 2px 2px 2px #000;}
}

大家都看不懂CSS, 也不知道是什么意思, 因为先生们都是美痴.

如果不信, 请看所有人投票选出来的Logo:

在这里插入图片描述

总结

最终的结果也很可喜, 一个简单的exe文件, 实现了超帅的web应用[^1]: 译者注: 本文中的代码, 以及部分内容, 参考了这里的内容.

完整代码文件:

  1. retro-games.lisp
  2. build-retro-games.lisp
  3. retro.css

在这里插入图片描述

相关文章:

  • C#通过ACE OLEDB驱动程序访问 Access和 Excel
  • 逻辑代数的基本规则
  • (Java入门)学生管理系统
  • 记忆化搜索【下】
  • 【论文阅读】CiteTracker: Correlating Image and Text for Visual Tracking
  • 输送线相机拍照信号触发(博途PLC高速计数器中断立即输出应用)
  • 解决npm i 安装报npm ERR! code E401
  • 2024年AMC10美国数学竞赛倒计时两个月:吃透1250道真题和知识点(持续)
  • mybatis框架基础以及自定义插件开发
  • 极米科技:走出舒适圈,推动数据架构现代化升级 | OceanBase 《DB大咖说》
  • JavaScript 根据关键字匹配数组项
  • 算法练习题17——leetcode54螺旋矩阵
  • Go语言设计与实现 学习笔记 第六章 并发编程(3)
  • python基础语法十一-赋值、浅拷贝、深拷贝
  • 零知识证明在BSV网络上的应用
  • “Material Design”设计规范在 ComponentOne For WinForm 的全新尝试!
  • 【技术性】Search知识
  • Apache的80端口被占用以及访问时报错403
  • ECMAScript入门(七)--Module语法
  • js如何打印object对象
  • JS专题之继承
  • mysql中InnoDB引擎中页的概念
  • orm2 中文文档 3.1 模型属性
  • Selenium实战教程系列(二)---元素定位
  • Spring声明式事务管理之一:五大属性分析
  • storm drpc实例
  • 前端临床手札——文件上传
  • 微信端页面使用-webkit-box和绝对定位时,元素上移的问题
  • 小程序、APP Store 需要的 SSL 证书是个什么东西?
  • 译自由幺半群
  • 走向全栈之MongoDB的使用
  • CMake 入门1/5:基于阿里云 ECS搭建体验环境
  • 测评:对于写作的人来说,Markdown是你最好的朋友 ...
  • 关于Android全面屏虚拟导航栏的适配总结
  • 如何用纯 CSS 创作一个菱形 loader 动画
  • 整理一些计算机基础知识!
  • ​Z时代时尚SUV新宠:起亚赛图斯值不值得年轻人买?
  • !$boo在php中什么意思,php前戏
  • # Redis 入门到精通(七)-- redis 删除策略
  • $.type 怎么精确判断对象类型的 --(源码学习2)
  • (8)STL算法之替换
  • (Matlab)遗传算法优化的BP神经网络实现回归预测
  • (Redis使用系列) Springboot 使用Redis+Session实现Session共享 ,简单的单点登录 五
  • (大众金融)SQL server面试题(1)-总销售量最少的3个型号的车及其总销售量
  • (二刷)代码随想录第15天|层序遍历 226.翻转二叉树 101.对称二叉树2
  • (附源码)springboot社区居家养老互助服务管理平台 毕业设计 062027
  • (十)DDRC架构组成、效率Efficiency及功能实现
  • (四)opengl函数加载和错误处理
  • .NET 命令行参数包含应用程序路径吗?
  • .NET 设计模式—简单工厂(Simple Factory Pattern)
  • .NET/C# 中你可以在代码中写多个 Main 函数,然后按需要随时切换
  • .NETCORE 开发登录接口MFA谷歌多因子身份验证
  • .net反编译的九款神器
  • .net中应用SQL缓存(实例使用)
  • /etc/fstab 只读无法修改的解决办法