或许你已经使用过 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%,由于目前精力有限,暂时更新到这里,后续根据读者的意见和建议会持续更新到一个满意的程度。
再次感谢你的阅读,若是以为这个教程对你有所帮助,欢迎转发评论。固然也能够打赏一下。
若是你对本教程有更好的建议,请与我联系。