I’ve heard Swift on Server for a while, but never try it before. So I’d like to try to use it and memo some in this blog. Here is some benefits they wrote in the doc:
- Cloud services built with Swift have a small memory footprint (measured in MB)–especially when compared to other popular server languages with automatic memory management.
- Services built with Swift are also CPU-efficient, given the language’s focus on performance.
- Using Swift also helps streamline continuous delivery pipelines, since you incur less wait time for new versions of the service fleet to come online.
- quick boot times make Swift a perfect fit for serveless applications such as Cloud Functions or Lambda with negligible cold start times.
After doing some research, I notice the major Swift framework is Vapor. That’s I’d like to try it at first. Basically I will follow with the guide, and will memo some if I met any error.
Hello World Vapor
# install
$ brew install vapor
# new project
$ vapor new Try-Vapor-Hello-World -n
$ cd Try-Vapor-Hello-World
# open in Xcode
$ open Package.swift
Run with My Mac
target, check console:
[ NOTICE ] Server starting on http://127.0.0.1:8080
with ⬇️ routes
settings
open http://127.0.0.1:8080/
⬇️
open http://localhost:8080/hello
⬇️
It will also show request-id info in console as bellow ⬇️
Terminal
Swift Package can also run in terminal by using:
$ swift run
It will also build and run project. It also show dependency fetching info and at the end, show connection info like ⬇️
[1511/1511] Build complete!
[ NOTICE ] Server starting on http://127.0.0.1:8080
Here is my test project: https://github.com/HevaWu/Try-Vapor-Hello-World.
Shiba Random Vapor
Next step, I’d like to try to use Leaf in Vapor. Here is my test project: https://github.com/HevaWu/Vapor-Shiba-Random
It will run like:
Same as above, create one new project by:
$ vapor new Vapor-Shiba-Random
# select `y` at ``Would you like to use Leaf?``
The generated template have bellow problem for now:
routes.swift
has a redundant route, we need to remove it to usePackage.swift
target dependency missingLeaf
module, we need to manually add it- manually add
app.views.use(.leaf)
inconfigure.swift
to tell system we are using leaf
Next step, we want to build it, after running, we notice the http://127.0.0.1:8080/index
show error:
open(file:oFlag:mode:): No such file or directory (errno: 2)
It means it cannot find proper leaf template place. For solving it, we need to set custom working directory to let vapor find it.
For now, the template paths should show well. We can try to add more template and render it now!
Add Welcome Page
First, for testing adding images and embedded child leaf, I add one /welcome
for try.
I set my base.leaf
and welcome.leaf
as bellow:
<!-- base.leaf -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>#import("title")</title>
<link rel="stylesheet" href="/styles/syntax.css">
</head>
<body>
<h1>Hello! #import("title")</h1>
#import("body")
</body>
</html>
<!-- welcome.leaf -->
#extend("base"):
#export("title"):
It works!
#endexport
#export("body"):
<div class="welcome">
<img src="/images/hello.png">
</div>
#endexport
#endextend
Here are several notes:
- manually create
/Public
folder first, and put required server sharing files into it - by using
/styles
and/images
, we need to enable/Public
FileMiddleware first. - use
extend
,export
,import
for sharing data between parent and child pages. (if we directly search online, there are some site is usingset
get
which vapor is not support them anymore)
Shiba List Page
Next Step, I’d like to fetch get
request from the http://shibe.online/api/shibes?count=100&urls=true&httpsUrls=true
.
NOTE: Vapor API changed a lot for each major version. For now, the Data(body.readableBytesView)
works at here. But, this behavior might be changed in later version.
let future = req.client
.get("http://shibe.online/api/shibes?count=100&urls=true&httpsUrls=true")
.flatMapThrowing { res -> ShibaImageURLList? in
guard let body = res.body else {
return nil
}
let data = Data(body.readableBytesView)
guard let jsonAsArr = try JSONSerialization.jsonObject(with: data, options: []) as? [Any] else {
return nil
}
let list = jsonAsArr.compactMap { object -> ShibaImageURL? in
if let str = object as? String {
return ShibaImageURL(urlString: str)
} else {
return nil
}
}
return ShibaImageURLList(list: list)
}
This code will return a EventLoopFuture<ShibaImageURLList?>
object now. And next step will be show list on our website.
.flatMap { list in
req.view.render("shibalist", list)
}
NOTE: NOT using req.view.render("shibalist", future)
. It will throw error about EventLoopFuture<Type> not from Encodable
. This actually means we should not directly use future object, instead, use flatMap
to filter and render object properly.
NOTE: We need to use a ShibaImageURLList
to wrap final response at here. If we directly use [ShibaImageURL]
, system will throw error at the render page LeafKit.LeafError.Reason.unsupportedFeature(\"You must use a top level dictionary or type for the context. Arrays are not allowed.\")
to inform we have to wrap it to a final response object.
For now, the list can show well now. But I’d like to try if we can clean some code by using Controller. Vapor’s Folder Structure doc mentioned:
Controllers are great way of grouping together application logic. Most controllers have many functions that accept a request and return some sort of response.
For this request, I tried to make a Controller like:
final class ShibaController {
func index(req: Request) -> EventLoopFuture<View> {
let uri = URI(string: "http://shibe.online/api/shibes?count=100&urls=true&httpsUrls=true")
return req.client
.get(uri)
.flatMapThrowing { res -> ShibaImageURLList in
guard let body = res.body else {
return ShibaImageURLList.empty
}
let data = Data(body.readableBytesView)
guard let jsonAsArr = try JSONSerialization.jsonObject(with: data, options: []) as? [Any] else {
return ShibaImageURLList.empty
}
let list = jsonAsArr
.compactMap { object -> ShibaImageURL? in
if let str = object as? String {
return ShibaImageURL(urlString: str)
} else {
return nil
}
}
return ShibaImageURLList(list: list)
}
.flatMap { list in
req.view.render("shibalist", list)
}
}
}
And call it in routes like:
let shibaController = ShibaController()
app.get("shibalist", use: shibaController.index(req:))
The rendered page is working same as previous code. Now, the page is good. But, we find we can customize the parameter in this GET
request, ex: count, urls, etc. Maybe we can try to make a user input form, and parse parameter to this request to get list.
// define at Controller
func index(req: Request) -> EventLoopFuture<View> {
return req.view.render("shibalistGetParam")
}
func getList(req: Request, uri: URI) -> EventLoopFuture<View> {
return req.client
.get(uri)
...
}
func postParams(req: Request) throws -> EventLoopFuture<View> {
let content = try req.content.decode(ShibaListGetRequestContent.self)
if var components = URLComponents(string: Self.baseURLString) {
components.queryItems = [
URLQueryItem(name: "count", value: content.count),
URLQueryItem(name: "urls", value: content.urls.description),
URLQueryItem(name: "httpUrls", value: content.httpsUrls.description),
]
if let uriStr = components.string {
let uri = URI(string: uriStr)
return getList(req: req, uri: uri)
}
}
// parse URI failed, back to welcome page
return req.view.render("welcome")
}
// call in routes
app.post("shibalist-submit", use: shibaController.postParams(req:))
DONE!
The End
It’s very easy to use Vapor for generating some website, its logic is clean. But I’m thinking it might still under improvement/design, so its configuration is keep changing. We’d better to read its latest document to do our design, rather than search some existed project. (That’s why bump major version changes 🤣)
Well, in general, as a Swift programmer, I’m very happy to see we can use Swift on Server development, and I’d like to keep watching its updates! 🤠
Comments
Join the discussion for this article at here . Our comments is using Github Issues. All of posted comments will display at this page instantly.