用 Koa 寫服務體驗

jopen 9年前發布 | 64K 次閱讀 Koa Web框架

用 Koa 寫服務體驗

曬一下自己用 Koa next generation web framework for node.js 寫的一個 web 服務

這個 web 服務主要是做內容的列表展示和搜索的 (可能說得比較抽象,但確實是 web 服務最常需要做的事情) 主要的文件一共就2個:

  • app.js主程序
  • lib/model.js數據層

其中model.js是和具體業務邏輯相關的,就不多介紹了,這也不是 Koa 的核心;而app.js的代碼可以體現 Koa 的很多優點,也使得代碼可以寫得非常簡練而去清晰——這是我自己都完全沒有想到的事情

加載資源和相關依賴庫

// resources

var koa = require('koa')
var app = koa()

var logger = require('koa-logger')
var route = require('koa-route')

var fs = require('fs')
var path = require('path')
var extname = path.extname

var views = require('co-views')
var render = views('./views', {
  map: { html: 'ejs' }
})

var model = require('./lib/model')

其中:

  1. koa是最核心的庫,app是koa生成的 web 服務主程序
  2. koa-loggerkoa-route 都是koa官方開發的“中間件”,分別用來打印日志和路由設置,路由設置稍后還會提到
  3. fs和path都是 Node 的官方包,用來進行本地文件和路徑相關的處理,輔助性質的
  4. co-views 是用來渲染模板的庫,而render是它生成的實例,這個用法也跟傳統用法不太一樣,稍后會提及

Web 服務工作流

// workflow

app.use(logger())

app.use(route.get('/', list))
app.use(route.get('/page/:page', list))
app.use(route.get('/search/:keywords', search))
app.use(route.get('/search/:keywords/:page', search))

app.use(function *(next) {
  if (!this.path.match(/^\/assets\//)) {
    yield* next
    return
  }
  var path = __dirname + this.path
  var fstat = yield stat(path)

  if (fstat.isFile()) {
    this.type = extname(path)
    this.body = fs.createReadStream(path)
  }
})

app.use(function *(next) {
  if (this.needRendered) {
    this.body = yield render(this.templateView, {cache: false, data: this.templateModel})
  }
  yield* next
})


// utils

function stat(file) {
  return function (done) {
    fs.stat(file, done)
  }
}

這部分代碼是用來規劃服務器工作流的,從請求被接受到響應被發出,整個過程都在這段代碼里一覽無余。工作流設計的主要的用法是app.use(...)。里面的參數其實就是一個 generator。

  1. 首先是打開日志
  2. 然后是分發路由,這里可以看到,有首頁、列表、搜索、搜索列表 4 種設計,分別對應到了各自的處理方,list和search其實都是在利用lib/model在生成數據,準備給模板進行渲染。這里的原理也有特殊之處,稍后會看到
  3. 再看緊隨其后的兩個app.use,分別是處理靜態資源目錄assets和對模板+數據進行渲染

所以完整的工作流可以理解為:

  1. 請求頁面 (列表或搜索) ->logger-> 路由分發 ->list或search-> 模板渲染 -> 回應
  2. 請求靜態資源 ->logger-> 找到對應的assets文件 -> 回應

function *() {}和yield是啥?

這個其實是 Koa 的精髓所在,在介紹它之前,我們先把list和search的代碼也貼出來:

// routes

function *list(page, next) {
  next = arguments[arguments.length - 1]
  this.templateView = 'page'
  this.templateModel = yield model.list({page: page})
  this.needRendered = true
  yield *next
}

function *search(keywords, page, next) {
  next = arguments[arguments.length - 1]
  this.templateView = 'search'
  this.templateModel = yield model.search({keywords: keywords, page: page})
  this.needRendered = true
  yield *next
}

大家會發現,首先app.use(...)和route.get(path, ...)傳入的參數都是一種寫得很像函數的東西,但不同之處是函數的寫法是function foo() {...},而這里的寫法多了一個星號,即function *foo() {}。這種寫法其實就是 ES6 里的 generator。而yield正是配合這個寫法的一種語法。

有關 ES6 generator 的基礎知識,建議大家來 @兔哥 的這個 ES6 教程網頁 來學習,這里不做原理方面的贅述。但我想說的是,由于 web 服務的處理本身就是“一層一層”的,并且有些處理是可以同步的,有些是只能異步的,我們不免要精心設計很多中間件并保障它的可擴展性,同時盡量簡化異步操作的寫法保障它的可讀性。

有了 ES6 generator 和yield之后,我們的每一層中間件都可以從流程上看成一個以yield *next語句切分出來的 “三明治”:

function *(next) {
  // 下一步之前的操作
  yield *next // 進行下一步
  // 所有邏輯處理完之后的補充操作
}

而且這個“下一步”是不介意是不是異步行為,都可以這樣簡單描述清楚的。

用 Koa 寫服務體驗

后頭看我們設計的整個工作流的實現:

用 Koa 寫服務體驗

我們這里的邏輯基于全部是出現在yield *next之前的,但是如果你需要在臨發出響應之前做點什么,就可以寫在其后面了

co-views的用法

co-views其實是對通用模板引擎渲染平臺 consolidate 的封裝,consolidate 應該算是 express.js 時代非常重要的一個庫,它支持包括 ejs, mustache, swig 等各種模板渲染并提供統一的 api 調用方法。根據對co-views源碼的分析,它把 consolidate 統一的 api 又封裝成了return function (done) {...}的形態,這樣源代碼中的yield render(view, model)就能夠融入 generator 的邏輯之中。

值得一提的是,源代碼中yield render(view, model)這里的model傳入了一個{cache: false}的參數,這會意味著模板不會被緩存,每次修改模板文件之后,在不重啟服務的情況下,刷新頁面就可以看到最新的效果。這個選項是針對開發環境設置的,為了保障線上環境的運行性能和效率,這個選項應該是不需要的。

lib/model的用法

同上,我們在lib/model.js里封裝的yield model.list({page: page})和yield model.search({keywords: keywords, page: page})也都會生成形如return function (done) {...}的返回值,以融入 generator 的邏輯之中。

最后,監聽端口

// listen

app.listen(3000)
console.log('listening on port 3000')

That's it

后記

在首次嘗試用 generator 的方式編寫 web 服務的時候,我自己一開始總會把yield的位置、yield后面要不要加星號、function后面要不要加星號、app.use()的調用順序這幾件事情弄得亂糟糟的,可能還是對 generator 和 koa 的理解不夠深入,不過逐漸寫著寫著,感受到了更多的爽和快感。到最后用如此簡單的一個 js 文件完成了全部的功能和邏輯串聯,還是覺得很興奮的。大家如果感興趣也可以搞來玩一玩,寫點自己平時用得到用不到的小玩意兒體驗一下:)

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!