或許你已經使用過 Express, Koa2 等 Node.js 的 WEB 框架,在構建 WEB 應用程序時,你的工做僅僅是產出 RESTFUL API,或者經過 Node 調用其餘網絡接口。你或許感受到是否是有一種更簡單的方式來處理請求,或在構建項目初期,有沒有一種沒必要由於尋找使用哪一個中間件而苦惱的 Node 框架。在對比多個框架後,我選擇使用 Hapi 來重構個人 Koa2 項目。css
Hapi 目前 Github star 10653,最新版本 17.5,release 版本 18.x。issues 數目 6,對,你沒有看錯,個位數。能夠看出 Hapi 的關注度與維護狀態都很是好。能夠經過 Hapi 的官網來查看 Hapi 的最新動態,包括提交,修改了哪些 issues,一個簡單介紹特性的教程,帶有示例的 API 文檔,使用 Hapi 的社區,插件和資源。Hapi 具備完整的構建 WEB 應用所需的插件,一些是官方提供的,一些是社區貢獻的,並且一般這些插件是能夠在任何你想要的地方使用而不依賴於 Hapi,如 Boom, Joi, Catbox。html
若是想了解 Hapi,或者它與其餘框架的不一樣,能夠在 Google 中搜索相關信息,本文不會過多涉及框架的介紹。前端
框架對比jquery
Hapijsios
學習本教程,不須要你有任何的 Node 經驗,你能夠把它當作 Node 的入門課。若是你是一名前端開發人員,本教程會讓你更清楚的瞭解 Node 能夠作什麼,先後端是如何交付各自工做的。你也可能嘗試過其餘 Node 框架的新手,你能夠經過這個入門教程,來對比兩個框架的不一樣。若是你已是一名有經驗的 Node 開發人員,那麼這個教程並不適合你。git
這個教程涵蓋的概念較少,更多的是動手去嘗試,因此哪怕你沒有任何經驗,你也能夠開始學習。github
npm init -y
// or
npm init
// -y 參數 以默認方式初始化 package.json
複製代碼
npm i hapi
// or
npm install hapi -D
// i 爲 install 的縮寫,不帶任何參數時,默認值爲 -D
複製代碼
// server.js
const Hapi = require('hapi')
const server = Hapi.server({
port: 3000,
host: 'localhost'
})
const init = async () => {
await server.start()
console.log(`Server running at: ${server.info.uri}`)
}
init()
複製代碼
在命令行中執行web
node server.js
# Server running at: http://localhost:3000
# 說明咱們的服務已經啓動了
# 若是 3000 端口已經被佔用了, 你能夠修改 port 爲其餘端口
複製代碼
如今咱們訪問 http://localhost:3000,頁面會顯示 404,由於咱們並無配置任何的路由
。ajax
// server.js
const init = async () => {
server.route({
path: '/',
method: 'GET',
handler () {
return 'Hapi world'
}
})
await server.start()
console.log(`Server running at: ${server.info.uri}`)
}
複製代碼
如今從新啓動服務, 咱們能夠看到頁面上的內容了。
接下來咱們建立一個 API 接口,能夠返回一個 json
數據
// server.js
server.route({
path: '/api/welcome',
method: 'GET',
handler () {
return {
code: 200,
success: true,
data: {
msg: 'welcome'
}
}
}
})
複製代碼
重啓服務,咱們訪問 http://localhost:3000/api/welcome
咱們獲得了一個 content-type
爲 application/json
的數據,咱們能夠經過 XMLHttpRequest
的庫好比(jQuery Ajax、Axios、Fetch)來請求這個接口,獲得一個 JSON 數據
等等,你有沒有發現,咱們在每次修改文件以後,都要斷開服務,手動重啓,這樣太糟糕了,如今咱們要解決這個問題。
npm i onchange
# 增長 onchange 模塊
複製代碼
// package.json
"scripts": {
"dev": "node server.js",
"watch": "onchange -i -k '**/*.js' -- npm run dev"
},
複製代碼
咱們在 package.json 文件的 scripts 字段中增長一個 dev 執行。這樣,咱們執行 npm run dev
就至關於執行了以前 node server.js
。使用 onchange
包,監控個人 js 文件變更,當文件發生改變時,從新啓動服務。
試一下
npm run watch
複製代碼
而後咱們修改一下 api/welcome 的返回結果
刷新一下瀏覽器
看!不須要手動重啓服務了,每次改動,只須要從新刷新瀏覽器就看到結果了
如今咱們並不須要太早的引入 Nodemon,雖然它很是棒也很好用。
既然咱們已經能夠請求到服務器的數據了,咱們還要將客戶端的數據傳給服務器,下面咱們將介紹幾種傳遞參數的形式。
咱們假設幾個場景,經過這些來理解如何獲取參數。
/api/welcome
咱們但願它能返回傳入的名字// 修改路由
server.route({
path: '/api/welcome',
method: 'GET',
handler (request) {
return {
code: 200,
success: true,
data: {
msg: `welcome ${request.query.name}`
}
}
}
})
// 請求 http://localhost:3000/api/welcome?name=kenny
// msg: "welcome kenny"
複製代碼
// 修改路由
server.route({
path: '/api/welcome/{name}',
method: 'GET',
handler (request) {
return {
code: 200,
success: true,
data: {
msg: `welcome ${request.params.name}`
}
}
}
})
// http://localhost:3000/api/welcome/kenny
// msg: "welcome kenny"
// 結果是同樣的
複製代碼
let speech = {
value: 'welcome',
set (val) {
this.value = val
}
}
server.route({
path: '/api/welcome/{name}',
method: 'GET',
handler (request) {
return {
code: 200,
success: true,
data: {
msg: `${speech.value} ${request.params.name}`
}
}
}
})
server.route({
path: '/api/speech',
method: 'POST',
handler (request) {
speech.set(request.payload.word)
return {
code: 200,
success: true,
data: {
msg: `speech is *${speech.value}* now`
}
}
}
})
複製代碼
驗證一下
# 使用 curl 來驗證一個 POST 接口,你也可使用 Ajax,POSTMAN...等等 你所喜歡的方式。
curl --form word=你好 \
http://localhost:3000/api/speech
# {"code":200,"success":true,"data":{"msg":"speech is *你好* now"}}%
curl http://localhost:3000/api/welcome/kenny
# {"code":200,"success":true,"data":{"msg":"你好 kenny"}}%
複製代碼
這裏須要注意一下,content-type
application/x-www-form-urlencoded 與 multipart/form-data 的區別。
總結一下,可使用 request.query
來獲取 url querystring 的數據,request.payload
獲取 POST 接口的 request body 數據,request.params
獲取 url 中的自定義參數。
咱們已經有了一個後端API服務,對應要有一個前端服務,可能這個服務是單頁面的,也有可能傳統的後端渲染頁面,可是一般都是和你後端服務不在同一個端口的。咱們建立另外一個服務,用來渲染前端頁面,爲了更真實的模擬真實的場景。
+const client = Hapi.server({
+ port: 3002,
+ host: 'localhost'
+})
+
- server.route({
+ client.route({
+ await client.start()
複製代碼
增長一個新的服務,監聽端口啊 3002,並將以前首頁路由修改爲 client 的首頁。
訪問 http://localhost:3002 查看效果
以前,咱們直接渲染頁面的方式是字符串,這樣不利於編寫和修改,咱們把返回 HTML 的方式改成」模板「渲染。
# 安裝所需依賴包
npm i inert
# 建立 public 文件夾
mkdir public
# 建立 index.html
touch public/index.html
# 建立 about.html
touch public/about.html
複製代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<title>Document</title>
</head>
<body>
<h1>Hapi world</h1>
</body>
</html>
複製代碼
// ...
const client = Hapi.server({
port: 3002,
host: 'localhost',
routes: {
files: {
relativeTo: Path.join(__dirname, 'public')
}
}
})
// ...
// const init = async () => {
await client.register(Inert)
client.route({
path: '/{param*}',
method: 'GET',
handler: {
directory: {
path: '.',
index: true,
}
}
})
// ...
複製代碼
依次訪問查看效果
/index.html 這種帶着擴展名的路徑看似不那麼專業,咱們修改一下 directory
的配置
directory: {
+ defaultExtension: 'html'
複製代碼
訪問 http://localhost:3002/index
咱們不過多介紹瀏覽器的同源策略,如今已有的客戶端(端口3002)在發起 XHRHttpRequest 請求服務端(端口3000)接口時,就會遇到 CORS 問題,接下來咱們要在服務端容許來自客戶端的請求,經過設置 Access-Control-Allow-Origin
等響應頭,使跨域請求被容許。
// index.html
$.ajax({
url: 'http://localhost:3000/api/welcome/kenny'
}).then(function (data) {
console.log(data)
})
複製代碼
訪問 http://localhost:3002/index 會報 js 的跨域錯誤
Access to XMLHttpRequest at 'http://localhost:3000/api/welcome/kenny' from origin 'http://localhost:3002' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
// server.js
const server = Hapi.server({
port: 3000,
host: 'localhost',
routes: {
cors: {
origin: '*'
}
}
})
複製代碼
保存後,你會發現終端會有如下錯誤
[1] "origin" must be an array
這就是 Hapi 的另外一個優點,配置檢查,由於 Hapi 做爲以配置先行的框架,作了不少配置的檢查,在你使用了不容許或不規範的配置時,會有相應的錯誤產生,方便你對於問題的捕捉和解決。
origin: ['*']
複製代碼
而後刷新頁面,你會發現跨域的錯誤已經沒有了。
關於跨域,咱們尚未說起:
目前咱們擁有了一個 web 渲染的前端服務,一個提供接口的後端服務,並且他們是在不一樣的」域「(端口),前端頁面或許有寫單調,沒有圖片和樣式,也沒有 favicon。
幫他們都放在放在 /public 目錄下
...
<head>
...
<link rel="icon" type="image/png" href="/favicon.png">
<link rel="stylesheet" href="/bulma.min.css">
</head>
...
<html>
<img class="logo" src="/logo.svg" />
...
複製代碼
假設咱們有一個登陸 /login
接口,在登陸成功後,設置一個 login 字段在 cookie 中, 前端能夠經過這個 login 來判斷你是否登陸,而且能夠經過 /logout
登出。
// ...
server.state('login', {
ttl: null, // 時效
isSecure: false, // https
isHttpOnly: false, // http Only
encoding: 'none', // encode
clearInvalid: false, // 移除不可用的 cookie
strictHeader: true // 不容許違反 RFC 6265
})
// ...
const init = async () => {
// ...
server.route({
path: '/api/login',
method: 'POST',
handler (request, h) {
let body
let code
// 獲取 cookie
const isLogin = request.state.login
if (isLogin) {
body = {
msg: '已登陸'
}
code = 200
} else if (request.payload && request.payload.email === 'kenny@gmail.com' && request.payload.password === '123456') {
// 設置 cookie
h.state('login', 'true')
body = {
msg: '登陸成功'
}
code = 200
} else {
code = 100
body = {
msg: '登陸信息有誤'
}
}
return {
code,
success: true,
data: body
}
}
})
複製代碼
server.route({
path: '/api/logout',
method: 'POST',
handler (request, h) {
// 取消 cookie
h.unstate('login')
return {
code: 200,
success: true
}
}
})
複製代碼
這個例子並不適合實際的業務場景,只是爲了更簡單的描述如何設置和取消cookie
認證這個概念可能對於入門來講可能比較難以理解,好比比較經常使用的 JWT (JSON Web Token),這裏不浪費時間去解釋如何使用,若是想了解什麼是JWT,傳送門: Learn how to use JSON Web Tokens (JWT) for Authentication。在 Hapi 框架中,咱們使用 hapi-auth-jwt2
這裏講一下 Hapi 中認證配置的方便之處。
在 Express/Koa2 中,你須要
當你項目的路由足夠多時,這個匹配規則也會愈來愈複雜。或者你能夠在路由的命名上作一些規劃,這讓完美主義者感受很很差。在單個路由內作判斷呢,又是重複的操做。
下面看下 Hapi 的使用。
// 引入插件
await server.register(require('hapi-auth-jwt2'))
// 自定義一個你的認證方法
const validate = async function (decoded, request) {
return {
isValid: true
}
}
// 設置認證
server.auth.strategy('jwt', 'jwt', {
key: 'your secret key',
validate,
verifyOptions: {
algorithms: ['HS256']
},
cookieKey: 'token'
})
// 一個須要認證的路由
server.route({
path: '/user/info',
method: 'GET',
options: {
auth: 'jwt'
},
// ...
})
// 一個須要認證可選的路由
server.route({
path: '/list/recommond',
method: 'GET',
options: {
auth: {
strategy: 'jwt',
mode: 'optional'
}
},
// ...
})
// 一個須要認證嘗試的路由
server.route({
path: '/list/recommond',
method: 'GET',
options: {
auth: {
strategy: 'jwt',
mode: 'try'
}
},
// ...
})
複製代碼
其中 try 與 optional 的區別在於認證錯誤後的返回, optional 的認證規則爲你能夠沒有,可是有那就必須是正確的。 try 則是無所謂,都不會返回 401 錯誤。
能夠看出,Hapi 中關於認證是配置在路由上的,這使得在管理認證和非認證模塊時,只需配置相應規則,而無需擔憂是否錯改了全局的配置。
在接受到請求,或者在服務上發起請求時,並無可讓咱們查看的地方,如今加入一個日誌系統。
npm i hapi-pino
複製代碼
await server.register({
plugin: require('hapi-pino'),
options: {
prettyPrint: true // 格式化輸出
}
})
複製代碼
從新服務,而且訪問 '/api/logout'
查看一下終端的顯式
[1547736441445] INFO (82164 on MacBook-Pro-3.local): server started
created: 1547736441341
started: 1547736441424
host: "localhost"
port: 3000
protocol: "http"
id: "MacBook-Pro-3.local:82164:jr0qbda5"
uri: "http://localhost:3000"
address: "127.0.0.1"
Server running at: http://localhost:3000
[1547736459475] INFO (82164 on MacBook-Pro-3.local): request completed
req: {
"id": "1547736459459:MacBook-Pro-3.local:82164:jr0qbda5:10000",
"method": "post",
"url": "/api/logout",
"headers": {
"cache-control": "no-cache",
"postman-token": "b4c72a2f-38ab-4c5c-9559-211e0669e6cf",
"user-agent": "PostmanRuntime/7.4.0",
"accept": "*/*",
"host": "localhost:3000",
"accept-encoding": "gzip, deflate",
"content-length": "0",
"connection": "keep-alive"
}
}
res: {
"statusCode": 200,
"headers": {
"content-type": "application/json; charset=utf-8",
"vary": "origin",
"access-control-expose-headers": "WWW-Authenticate,Server-Authorization",
"cache-control": "no-cache",
"set-cookie": [
"login=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Strict"
],
"content-length": 27
}
}
responseTime: 16
複製代碼
能夠說很是全面的日誌,並且帶有着色效果。
隨着開發的時間,你的項目中加入了愈來愈多的接口,當你與其餘人員配合,或者想找到一個接口的定義時,一個好的文檔會讓你事倍功半
await server.register({
plugin: require('lout')
})
複製代碼
由於 Hapi 是以配置爲中心的框架,因此文檔也能夠根據配置生成,只須要你對路由進行必定的描述,就會生成一個可用的文檔。
訪問 http://localhost:3000/docs 查看效果
未完成
本文說起的內容都已經上傳 github
你能夠 clone 項目後查看代碼。同時你也能夠切換到不一樣的步驟中(git checkout HEAD)
# 查看commit
git log --pretty=online
51b2a7eea55817c1b667a34bd2f5c5777bde2601 part 9 api doc
fbb1a43f0f1bf4d1b461c4c59bd93b27aabc3749 Part8 cookies
00a4ca49f733894dafed4d02c5a7b937683ff98c Part7 static
ea2e28f2e3d5ef91baa73443edf1a01a383cc563 Part7 cors
a0caaedbf492f37a4650fdc33d456fa7c6ef46d3 Part6 html render
12fce15043795949e5a1d0d9ceacac8adf0079e8 Part5 client server
79c68c9c6eaa064a0f8c679ae30a8f851117d7e0 Part4 request.payload
e3339ff34d308fd185187a55f599feed1e46753e Part4 request.query
af40fc7ef236135e82128a3f00ec0c5e040d4b12 Part3 restart when file changed
2b4bd9bddfe565fd99c7749224e14cc7752525b1 Part2 route 2
99a8f8426f43fea85f98bc9a3b189e5e3386abfe Part2 route
047c805ca7fe44148bac85255282a4d581b5b8e1 Part1 server
# 切換至 Part5
git checkout 12fce15043795949e5a1d0d9ceacac8adf0079e8
複製代碼
目前教程完成度爲 80%,由於目前精力有限,暫時更新到這裏,後續根據讀者的意見和建議會持續更新到一個滿意的程度。
再次感謝你的閱讀,若是以爲這個教程對你有所幫助,歡迎轉發評論。固然也能夠打賞一下。
若是你對本教程有更好的建議,請與我聯繫。