计算机应用从 CS 架构向 BS 架构转型后,基本上每一门语言都会有人问,能做 Web 吗?Julia 似乎也逃脱不了这个问题。Julia 目前生态依然在发展当中,与 Python 相比,还没有出现 Flask、Django 那样的成熟框架,但是 Genie 似乎是有这个打算和野心。让我们拭目以待~
安装
输入 ]
进入包管理模式:
add https://github.com/essenciary/Genie.jl
安装完后,使用 usingGenie
便可以加载到作用域下。
Hello, World
开始我们第一个请求,只返回一段静态的文本“Hello, World”:
using Genie
import Genie.Router: route
route("/") do
"
Hello, World
Hello, World
"
end
这个语法还是非常优雅的:路由(route)端点 /
做(do)返回“Hello, World”的事情。这段代码虽然非常简单,但它就是 Web 请求的基本模式。客户端(浏览器)向服务器请求站点,对请求的处理就是在这个 route
函数中,只不过大部分任务没有这么简单。 接下来,启动服务器:
Genie.AppServer.startup()
Genie 默认监听 8000 端口,可以通过 127.0.0.1:8000 或者 localhost:8000 访问到刚刚注册的路由 /
。
Genie 在控制台也会提示请求的头部等信息。由于 Julia 的编译特性,第一次请求会稍微慢一点。除了使用 do
语法,也可以传入一个函数来处理请求:
function hello_world()
"
Hello World!
Hello World!
"
end
route("hello/world", hello_world)
这样,我们通过 127.0.0.1:8000/hello/world 可以访问到这个页面(内容是一样的)。
动态路由
上面的 /
, hello/world
静态路由,很多时候我们也需要动态路由。比如获取用户信息的时候, /user/
,不可能为每一个用户建立一个端点。因此需要一个能进行模式匹配的 route
。最简单的动态路由是这样的:
import Genie.Router: @params
route("/echo/:message") do
"
Hello, "
Hello, "
* @params(:message) * ""
end
这样我们就可以随意地替换 message
,比如:你可以使用 /echo/Julia
、 /echo/Genie
等等。
除此之外,可以使用 ::
指定参数的类型,最常见的就是整型,甚至用来进行数学运算。
route("/sum/:x::Int/:y::Int") do
@params(:x) + @params(:y)
end
现在,它还不能正常工作。默认情况下,Genie 将参数提取为 SubString{String}
,加了类型约束后,Genie 会试图转换成其它类型,但现在它还不会转换,因此必须显式编写转换函数,利用多分派的特点,只需要再编写一个 convert
就可以了。
import Base.convert
convert(::Type{Int}, s::SubString{String}) = parse(Int, s)
现在,可以正常工作了:
json 请求
Genie 作为一个全栈的 MVC 架构,这点上和 Django 很像。随着 Web 应用日趋复杂,很多任务不再只由一个人去做,分成了前端和后台。服务器将前端代码发送给浏览器后,浏览器异步地向服务器发送请求,获取数据,在浏览器上进行渲染。这个数据交互的过程,比较流行的就是使用 json。Genie 编写 json 请求也非常简单:
import Genie.Renderer: json!
route("/json") do
(Dict(:message => "Hi there!", :status_code => 200)) |> json!
end
Genie项目
上面的操作都可以在 REPL 中执行,虽然很方便,但是一旦代码量上了规模后,就不适合使用这种方式,而比较适合组织成项目。可以切换到需要的工作目录,在该目录下创建一个 Genie 项目:
Genie.REPL.new_app("genie_hello")
执行这一条命令,Genie 将会创建在工作目录下创建app,安装所有依赖,自动加载这个app到REPL,启动 genie>
会话,最后启动服务器并监听 8000
端口。从 Genie 创建的项目文件上,还是相当有点重量级的:
启动服务器,在浏览器中访问 127.0.0.1:8000 页面,可以看到一个欢迎页面:
加载Genie项目
对于现有的 Genie 项目,切换到项目 App 文件夹下,可以使用下面的命令加载到当前环境中:
julia> using Genie
julia> Genie.REPL.load_app()
之后使用 Genie.AppServer.startup()
启动服务器。
路由
在 config/routes.jl
( config
包含路由,这命名有点...)中添加或修改路由。我们在 routes.jl
末尾添加:
# config/routes.jl
route("/hello") do
"Welcome to Genie!"
end
访问 127.0.0.1/hello 即可。动态路由前面也介绍了,不再重复。
添加自己的库函数
根据业务需求,往往需要封装一些工具函数。这些代码可以放到 lib
文件夹下,它会自动加载到 Genie 中。 比如在 lib/MyLib.jl
中编写一个判断今天是否是周五的函数:
module MyLib
using Dates
function isitfriday()
Dates.dayofweek(Dates.now()) == Dates.Friday
end
这样,就可以在路由函数中使用:
using Genie.Router
using MyLib
route("/friday") do
MyLib.isitfriday() ? "Yes, it's Friday!" : "No, not yet :("
end
end
控制器
MVC 模式围绕资源展开,M:资源(模型)提高数据支持,V:提供界面渲染,C:提供业务逻辑。在 Genie 中创建控制器,只需要使用这样一行命令:
genie> Genie.REPL.new_controller("Books")
[info]: New controller created at app/resources/books/BooksController.jl
一个名为 Book
的资源和控制器就创建好了。在新创建的 BookController.jl 中写入数据和控制数据的操作,比如比尔盖茨的推荐书单:
# app/resources/books/BooksController.jl
module BooksController
struct Book
title::String
author::String
end
const BillGatesBooks = Book[
Book("The Best We Could Do", "Thi Bui"),
Book("Evicted: Poverty and Profit in the American City", "Matthew Desmond"),
Book("Believe Me: A Memoir of Love, Death, and Jazz Chickens", "Eddie Izzard"),
Book("The Sympathizer", "Viet Thanh Nguyen"),
Book("Energy and Civilization, A History", "Vaclav Smil")
]
function billgatesbooks()
response = "
Bill Gates' list of recommended books
Bill Gates' list of recommended books
$( mapreduce(b -> "$(b.title) by $(b.author)", *, BillGatesBooks) )
"
end
end
billgatesbooks
包含了模板语法,可以在 HTML 中嵌入 Julia 语法。
$( mapreduce(b -> "$(b.title) by $(b.author)", *, BillGatesBooks) )
在这一行代码中,遍历 BillGatesBooks 元素,并生成 HTML 标签。为了检查结果的正确性,可以在 REPL 中调用这个方法:
genie> BooksController.billgatesbooks()
genie> "\n
Bill Gates' list of recommended books
Bill Gates' list of recommended books
\n
\n The Best We Could Do by Thi BuiEvicted: Poverty and Profit in the American City by Matthew DesmondBelieve Me: A Memoir of Love, Death, and Jazz Chickens by Eddie IzzardThe Sympathizer by Viet Thanh NguyenEnergy and Civilization, A History by Vaclav Smil\n \n "
这样,可以确保它按照预期的输出。为了可以通过浏览器访问这个页面,需要设置一个路由,并将它作为方法:
# config/routes.jl
using Genie.Router
using MyLib
using BooksController
route("/bgbooks", BooksController.billgatesbooks)
这样访问 /bgbooks
,可以看到:
分离视图与控制器
传统意义上,HTML 的内容属于视图层(V),而处理请求的控制器显然是控制层(C)。在上面的方法中,两者显然耦合在了一起。因此可以把 HTML 部分单独放到 view
中。在 Julia 的 REPL 中,我们可以直接用 mkdir
和 touch
命令创建文件夹和空 HTML 文件:
genie> mkdir("app/resources/books/views")
genie> touch("app/resources/books/views/billgatesbooks.jl.html")
JavaScript、CSS、HTML 三者各种分离是很符合直觉的,较早出现的 Angular 框架就这样做的,然而在这几年的实践中,开发者们却认为放到一起可能会更好一些,比如React、Vue。后者认为三种语言放在三种文件中,实际上是把技术分开管理,而不是逻辑上的分而治之。(不评论,大家看着办)
这样,可以把控制器中的 HTML 代码抽离到新建的 html 文件中:
Bill Gates' top $( length(@vars(:books)) ) recommended books
@foreach(@vars(:books)) do book
"$(book.title) by $(book.author)"
end
%>
可以使用 %>
包含复杂的多行表达式,而使用 $(...)
简单输出值。其中比较特殊的是 @vars
,它用来访问从控制器传入到视图的变量。要正确地渲染出值,需要把要渲染的变量传入到视图层,将控制器的代码改成:
using Genie.Renderer
# BooksController.jl
function billgatesbooks()
html!(:books, :billgatesbooks, books = BillGatesBooks)
end
:books
指明资源所在的文件夹, :billgatebooks
指定 view
的名称(不需要后缀名)。 books
是视图 @vars(:books)
指定的参数。
Makedown
Genie 支持用 Makedown 写网页是有点令人意外。但是,Makedown 终究要编译成 HTML,而且不能缓存,因此性能上会有影响。另外,Makedown 不支持 %>
,只支持 $(...)
。在 view
文件夹下新建 billgatesbooks.jl.md
:
# Bill Gates' $( length(@vars(:books)) ) recommended books
$(
@foreach(@vars(:books)) do book
"* $(book.title) by $(book.author)"
end
)
在控制器中显式调用 .md
:
function billgatesbooks()
html!(:books, Symbol("billgatesbooks.jl.md"), books = BillGatesBooks)
end
网站布局
向导航栏、页脚这些共同的元素,可以抽离到一个公有的文件中,每个页面只要引入该文件,这样可以避免重复编码。试着创建一个 app/layouts/admin.jl.html
文件,并写入:
lang="en">
Genie Admin
Books admin
@yield
%>
在 html!
中的第三个参数(也是传入符号变量)指定布局:
function billgatesbooks()
html!(:books, :billgatesbooks, :admin, books = BillGatesBooks)
end
可以看到多了一个 Books admin。
json视图
Genie 还支持 JSON 视图,后缀是 .json.jl
,在 views
中创建一个 billgatesbooks.json.jl
,写入:
Dict(
"Bill Gates' list of recommended books" => @vars(:books)
)
同样,在控制器中渲染视图:
function billgatesbooks()
json!(:books, :billgatesbooks, books = BooksController.BillGatesBooks)
end
json!
功能和 html!
是类似的,没有什么意外的。
最后
Genie 项目还配备了一个 ORM 库 SearchLight
,但限于篇幅和时间,大家可以自行了解一下。Genie 的生态也并不算成熟,对 Web 开发感兴趣的朋友也可以考虑贡献一下代码咯。不过,现阶段,Web 开发还是找成熟一点的生态和应用比较好。想要快速上手 Web,还是比较推荐使用 Python 的 Flask 或 Django,或者考虑这几年挺火的 Go 语言。