Hapi.js入门教程,详细解释

2019-11-25 16:35:43

参考地址  Hapi.js 起步 - 写给前端开发的 Node Web 框架入门

重要还是参考hapi官方文档,有中文翻译。 Hapi官方文档

为什么选择 Hapi

或许你已经使用过 Express, Koa2 等 Node.js 的 WEB 框架,在构建 WEB 应用程序时,你的工作仅仅是产出 RESTFUL API,或者通过 Node 调用其他网络接口。你或许感觉到是不是有一种更简单的方式来处理请求,或在构建项目初期,有没有一种不必因为寻找使用哪个中间件而苦恼的 Node 框架。在对比多个框架后,我选择使用 Hapi 来重构我的 Koa2 项目。

Hapi 目前 Github star 10653,最新版本 17.5,release 版本 18.x。issues 数目 6,对,你没有看错,个位数。可以看出 Hapi 的关注度与维护状态都非常好。可以通过 Hapi 的官网来查看 Hapi 的最新动态,包括提交,修改了哪些 issues,一个简单介绍特性的教程,带有示例的 API 文档,使用 Hapi 的社区,插件和资源。Hapi 具有完整的构建 WEB 应用所需的插件,一些是官方提供的,一些是社区贡献的,而且通常这些插件是可以在任何你想要的地方使用而不依赖于 Hapi,如 Boom, Joi, Catbox。

如果想了解 Hapi,或者它与其他框架的不同,可以在 Google 中搜索相关信息,本文不会过多涉及框架的介绍。

node-frameworks-to-use

框架对比

Hapijs

适合什么样的读者

学习本教程,不需要你有任何的 Node 经验,你可以把它当做 Node 的入门课。如果你是一名前端开发人员,本教程会让你更清楚的了解 Node 可以做什么,前后端是如何交付各自工作的。你也可能尝试过其他 Node 框架的新手,你可以通过这个入门教程,来对比两个框架的不同。如果你已经是一名有经验的 Node 开发人员,那么这个教程并不适合你。

这个教程涵盖的概念较少,更多的是动手去尝试,所以哪怕你没有任何经验,你也可以开始学习。

准备

  • 安装 node

  • 创建项目

  • 初始化 package.json

  • 编辑器 推荐 vscode

  • 命令行工具 - Windows 推荐 cmder,Mac 推荐 iTerm2

npm init -y// ornpm init// -y 参数 以默认方式初始化 package.json复制代码
  • 安装 Hapi

npm i hapi// ornpm install hapi -D// i 为 install 的缩写,不带任何参数时,默认值为 -D复制代码

一个服务

// server.jsconst 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()复制代码

在命令行中执行

node server.js# Server running at: http://localhost:3000# 说明我们的服务已经启动了# 如果 3000 端口已经被占用了, 你可以修改 port 为其他端口复制代码

现在我们访问 http://localhost:3000,页面会显示 404,因为我们并没有配置任何的路由

1. 路由

// server.jsconst 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.jsserver.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 数据

2. 停一下

等等,你有没有发现,我们在每次修改文件之后,都要断开服务,手动重启,这样太糟糕了,现在我们要解决这个问题。

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,虽然它非常棒也很好用。

3. 参数

既然我们已经可以请求到服务器的数据了,我们还要将客户端的数据传给服务器,下面我们将介绍几种传递参数的形式。

我们假设几个场景,通过这些来理解如何获取参数。

  1. /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"复制代码
  1. name 这个参数有些多余,因为这个接口只接受这一个参数,那么现在省略到这个 name

// 修改路由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"// 结果是一样的复制代码
  1. 假设我们需要偶尔更换我们的欢迎词,但不是每次都去修改代码,那么我们需要一个替换欢迎词的接口,通过提交接口来更换欢迎词。

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 中的自定义参数。

4. 第二个服务

我们已经有了一个后端API服务,对应要有一个前端服务,可能这个服务是单页面的,也有可能传统的后端渲染页面,但是通常都是和你后端服务不在同一个端口的。我们创建另一个服务,用来渲染前端页面,为了更真实的模拟真实的场景。

+const client = Hapi.server({+    port: 3002,+    host: 'localhost'+})+-    server.route({+    client.route({+    await client.start()复制代码

增加一个新的服务,监听端口啊 3002,并将之前首页路由修改成 client 的首页。

访问 http://localhost:3002 查看效果

5. 静态文件

之前,我们直接渲染页面的方式是字符串,这样不利于编写和修改,我们把返回 HTML 的方式改为”模板“渲染。

# 安装所需依赖包npm i inert# 创建 public 文件夹mkdir public# 创建 index.htmltouch public/index.html# 创建 about.htmltouch 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

6. 跨域请求

我们不过多介绍浏览器的同源策略,现在已有的客户端(端口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.jsconst server = Hapi.server({port: 3000,host: 'localhost',routes: {cors: {origin: '*'}}})复制代码

保存后,你会发现终端会有以下错误

[1] "origin" must be an array

这就是 Hapi 的另一个优势,配置检查,因为 Hapi 作为以配置先行的框架,做了很多配置的检查,在你使用了不允许或不规范的配置时,会有相应的错误产生,方便你对于问题的捕捉和解决。

origin: ['*']复制代码

然后刷新页面,你会发现跨域的错误已经没有了。

关于跨域,我们还没有提及:

  • 允许指定的域名和多个域名

  • 允许携带cookie [Access-Control-Allow-Credentials]

  • 允许获取额外的头部信息 [Access-Control-Expose-Headers]

  • 允许携带的头部信息 [Access-Control-Allow-Headers]

7. 还缺什么?

目前我们拥有了一个 web 渲染的前端服务,一个提供接口的后端服务,而且他们是在不同的”域“(端口),前端页面或许有写单调,没有图片和样式,也没有 favicon。

  • 下载一个你喜欢的 favicon

  • 引入一个本地的 CSS

  • 引入一个本地的图像

帮他们都放在放在 /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" />...复制代码

8. Cookie

假设我们有一个登录 /login 接口,在登录成功后,设置一个 login 字段在 cookie 中, 前端可以通过这个 login 来判断你是否登录,并且可以通过 /logout 登出。

// ...server.state('login', {ttl: null, // 时效isSecure: false, // httpsisHttpOnly: false, // http Onlyencoding: 'none', // encodeclearInvalid: false, // 移除不可用的 cookiestrictHeader: true // 不允许违反 RFC 6265})// ...const init = async () => {// ...server.route({path: '/api/login',method: 'POST',handler (request, h) {let bodylet code// 获取 cookieconst isLogin = request.state.loginif (isLogin) {body = {msg: '已登录'}code = 200} else if (request.payload && request.payload.email === 'kenny@gmail.com' && request.payload.password === '123456') {// 设置 cookieh.state('login', 'true')body = {msg: '登录成功'}code = 200} else {code = 100body = {msg: '登录信息有误'}}return {code,success: true,data: body}}})复制代码
server.route({path: '/api/logout',method: 'POST',handler (request, h) {// 取消 cookieh.unstate('login')return {code: 200,success: true}}})复制代码

这个例子并不适合实际的业务场景,只是为了更简单的描述如何设置和取消cookie

9. 认证与授权

认证这个概念可能对于入门来说可能比较难以理解,比如比较常用的 JWT (JSON Web Token),这里不浪费时间去解释如何使用,如果想了解什么是JWT,传送门: Learn how to use JSON Web Tokens (JWT) for Authentication。在 Hapi 框架中,我们使用 hapi-auth-jwt2

这里讲一下 Hapi 中认证配置的方便之处。

在 Express/Koa2 中,你需要

  • 引入插件

  • 中间件处理 401

  • 中间件中匹配需要认证的路由,和排除不需要的认证路由。

当你项目的路由足够多时,这个匹配规则也会越来越复杂。或者你可以在路由的命名上做一些规划,这让完美主义者感觉很不好。在单个路由内做判断呢,又是重复的操作。

下面看下 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 中关于认证是配置在路由上的,这使得在管理认证和非认证模块时,只需配置相应规则,而无需担心是否错改了全局的配置。

10. 日志

在接受到请求,或者在服务上发起请求时,并没有可以让我们查看的地方,现在加入一个日志系统。

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 startedcreated: 1547736441341started: 1547736441424host: "localhost"port: 3000protocol: "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 completedreq: {"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复制代码

可以说非常全面的日志,而且带有着色效果。

11. 文档

随着开发的时间,你的项目中加入了越来越多的接口,当你与其他人员配合,或者想找到一个接口的定义时,一个好的文档会让你事倍功半

await server.register({plugin: require('lout')})复制代码

因为 Hapi 是以配置为中心的框架,所以文档也可以根据配置生成,只需要你对路由进行一定的描述,就会生成一个可用的文档。

访问 http://localhost:3000/docs 查看效果

12. 转发接口

未完成

如何使用示例

本文提及的内容都已经上传 github

你可以 clone 项目后查看代码。同时你也可以切换到不同的步骤中(git checkout HEAD)

# 查看commitgit log --pretty=online51b2a7eea55817c1b667a34bd2f5c5777bde2601 part 9 api docfbb1a43f0f1bf4d1b461c4c59bd93b27aabc3749 Part8 cookies00a4ca49f733894dafed4d02c5a7b937683ff98c Part7 staticea2e28f2e3d5ef91baa73443edf1a01a383cc563 Part7 corsa0caaedbf492f37a4650fdc33d456fa7c6ef46d3 Part6 html render12fce15043795949e5a1d0d9ceacac8adf0079e8 Part5 client server79c68c9c6eaa064a0f8c679ae30a8f851117d7e0 Part4 request.payloade3339ff34d308fd185187a55f599feed1e46753e Part4 request.queryaf40fc7ef236135e82128a3f00ec0c5e040d4b12 Part3 restart when file changed2b4bd9bddfe565fd99c7749224e14cc7752525b1 Part2 route 299a8f8426f43fea85f98bc9a3b189e5e3386abfe Part2 route047c805ca7fe44148bac85255282a4d581b5b8e1 Part1 server# 切换至 Part5git checkout 12fce15043795949e5a1d0d9ceacac8adf0079e8复制代码

结尾

目前教程完成度为 80%,因为目前精力有限,暂时更新到这里,后续根据读者的意见和建议会持续更新到一个满意的程度。



  • 2020-04-03 08:53:06

    使用自己的QQ邮箱发送自动发送邮件

    话说网上发送邮件的代码很多,但是我由于不细心,导致拿别人的代码发送邮件老是失败,今天就说说几个要注意的地方吧!!!

  • 2020-04-03 10:20:20

    Vue 项目性能优化

    Vue 框架通过数据双向绑定和虚拟 DOM 技术,帮我们处理了前端开发中最脏最累的 DOM 操作部分, 我们不再需要去考虑如何操作 DOM 以及如何最高效地操作 DOM;但 Vue 项目中仍然存在项目首屏优化、Webpack 编译配置优化等问题,所以我们仍然需要去关注 Vue 项目性能方面的优化,使项目具有更高效的性能、更好的用户体验。本文是作者通过实际项目的优化实践进行总结而来,希望读者读完本文,有一定的启发思考,从而对自己的项目进行优化起到帮助。本文内容分为以下三部分组成:

  • 2020-04-03 13:07:46

    flex布局与position:absolute/fixed的冲突问题

    导航栏内,平均分为四块,为了适配各种移动设备,使用了flex布局。 与此同时,产品经理要求:页面上滚越过封面图时,导航栏变为固定定位,浮在页面顶部。 拿到需求之后,思路就是先搞好布局,然后监听window.onscroll,当页面滚的距离大于封面图的时候,给ul加入position:fixed。

  • 2020-04-03 16:56:59

    Inkscape教程

    本教程演示了Inkscape基础使用。这是常规Inkscape文档,你可以预览、编辑、复制、保存。 本教程包括画布导航、管理文档、形状工具基础、选择技术、使用选择转换对象、分组、设置填充和画笔、对齐和Z顺序。有关更高级的主题请查看帮助菜单中的其它教程。

  • 2020-04-03 17:04:35

    Inkscape/SVG附中文教程PDF

    Inkscape中的终极工具是XML编辑器(Shift+Ctrl+X),可以实时显示整个文档的XML树形图。修改绘图时,你可以注意一下XML树形图中的变化。也可以在XML编辑器中修改文本、元素或者节点属性,然后在画图上查看效果。这是一个非常形象化的学习SVG格式的交互式工具。并且可以实现一些通常的编辑工具无法完成的功能。

  • 2020-04-03 19:09:31

    CryptoJS.enc.UTF8 中文乱码

    ret = CryptoJS.AES.encrypt(data,'secret key 123') content = ret.toString() result = CryptoJS.AES.decrypt(content,'secret key 123') print(result.toString(CryptoJS.enc.Utf8))

  • 2020-04-03 19:10:56

    nodejs与javascript中的aes加密

    aes加密简单来说,在密码学中又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。高级加密标准已然成为对称密钥加密中最流行的算法之一。

  • 2020-04-03 19:13:05

    Express-session的使用

    当浏览器访问服务器并发送第一次请求时,服务器端会创建一个 session 对象,生成一个类似于 key,value 的键值对,然后将 key(cookie)返回到浏览器(客户)端,浏览器下次再访问时,携带 key(cookie), 找到对应的 session(value)。 客户的信息都保存在 session 中

  • 2020-04-08 22:46:28

    Element的操作方法

    Element 是一个通用性非常强的基类,所有 Document 对象下的对象都继承自它。这个接口描述了所有相同种类的元素所普遍具有的方法和属性。一些接口继承自 Element 并且增加了一些额外功能的接口描述了具体的行为。例如, HTMLElement 接口是所有 HTML 元素的基本接口,而 SVGElement 接口是所有 SVG 元素的基础。大多数功能是在这个类的更深层级(hierarchy)的接口中被进一步制定的。