图:Reijo Palmiste
我们要开发一个应用,一般都不太会从零开始做,可以找一些库(Library),或者基于框架(Framework)或者系统(System)来做。库里面一般提供的就是一些零部件,框架里边儿除了提供应用需要的一些零部件以外,它还会给我们定一些规矩,帮我们做出一些决定,还会提供一套开发的方法与流程。 系统,就是一个现成的能用的东西,比如常见的就是内容管理系统(CMS)。它们三个的关系大概是这样的,在框架里边儿可能会用到一些库里面边儿的零部件,一个系统可能是基于某个应用框架做出来的。
如果我们把应用开发比做成是要造去一辆汽车,那这个库,就相当于是提供了汽车需要的一些零部件还有一些工具,你自个儿造汽车的时候可以使用这些工具还有零部件。框架就相当于是除了提供这些工具还有零部件以外,还有一些更大的部件,比如一套制动系统,另外还会给你一些设计方法还有设计图纸之类的东西。系统就相当于是直接给你提供一辆能开的汽车,你可以改造改造,把它变成自个儿想要的汽车就行了。
不同的语言,不同类型的应用,都有各自的一些可以选择使用的库,框架,还有系统。 我推荐大家可以先选择学习一款框架,因为框架比库更强一些,又比系统更简单,更灵活一些。我们决定基于 Node.js 来做一个服务端应用,所以就可以去找一款 Node.js 的服务端应用框架。
不夸张地讲,Node.js 拥有地球上最大的技术生态社区,可以选择的应用框架非常多。Egg.js,Express.js,Nest.js 等等,因为 Node.js 应用都是用 JavaScript 语言写出来的,所以这些框架都会带着一个 .js 后缀,一看到这个后缀你就知道这是用 JavaScript 语言写出来的东西。
在后面我们要选择使用的是 Express.js,它是一款轻型框架,本身带的东西不多,灵活度要高于其它的框架。并不是说越灵活就越好,实际上在开发应用的时候多点规矩是好事,这些规矩与规范有助于团队协作,也不容易失控。之所以选择 Express.js 这个框架来学习,是因为它比其它的框架更简单一些,我们可以利用这个特点,学习开发服务端应用的一些最关键的概念,因为这样在学习过程里面的干扰更小一些,也更好理解这些概念。
如果现在让我选择开发一个应用,我可能会选择更重型的应用框架,比如 Nest.js。因为我需要更多的规矩,规范,工具,更稳定的架构等等。但是如果我们一开始就选择学习这种重型框架,可能会比较懵,它里面涉及到的概念太多了,如果没有一个良好的基础,我们也很难理解它们。所以我们先选择学习一款轻型框架,打好基础,再去学习那些重型的应用框架。
在很多重型应用框架里面,可能会用到 Express.js 框架,比如在 Nest.js 框架里,就可以选择使用 Express.js 框架为应用提供 Web 服务。所以不会白学 Express.js 这个框架,而且我们更重要的任务是理解服务端应用的开发基础,这些基础知识适用于任何的技术平台,不仅限于 Node.js 应用。
准备
任务:准备项目<express>
在项目里创建一个 express 分支,然后在这个分支上完成这一章里的任务,每做完一个任务,就要做一次提交。
1:确定分支
先确定是在我们之间创建的 web-server 分支上,执行:
git branch
2:切换分支
git checkout develop
3:合并分支
git merge web-server
4:创建分支
git checkout -b express
语言
filter():过滤数据项目
在 JavaScript 语言里,数组(Array)类型的数据上面天生就有一个 filter()
方法,你可以在任何的数组上面调用它的这个方法。这个方法的作用就是筛选出数组里符合条件的数据项目,这个方法制造出来的东西会是一个新的数组,里面的数据项目就是符合条件的数据项目。
示例:
const data = [ { title: '肖申克的救赎', rating: 9.7, }, { title: '霸王别姬', rating: 9.6, }, { title: '蝴蝶效应', rating: 8.8, }, ];
上面是一组电影数据,title
是电影的名字,rating
表示这部电影的评分。要在这组数据里筛选出评分大于 9 分的电影,可以这样做:
const results = data.filter(item => item.rating > 9);
用一下 data
这个数组上的 filter()
方法,然后设置一个筛选器(函数),data
这组数据里的每一个项目都会经过这个筛选器的检查。这个筛选器函数的参数是我们给当前数据项目起的一个名字,这里我们叫它 item
,你可以随便定义这个参数的名字。在函数的主体里面,我们让筛选器返回 rating
属性大于数字 9
的项目。
因为筛选器主体只有一行,所以我用了简单的写法,完整的写法应该是这样的:
const results = data.filter(item => { return item.rating > 9; });
主体放在大括号里,然后明确的使用 return
来返回符合条件的数据项目。
最终我们把筛选结果交给了 results
,它里面的数据项目里的 rating
属性的值都是大于 9
的,结果就是:
[ { title: '肖申克的救赎', rating: 9.7, }, { title: '霸王别姬', rating: 9.6, }, ];
Express.js
我们要一块儿开发的这个照片分享应用的服务端部分要基于一个叫 Express.js 的应用框架来做,这是一款简单的轻型框架,非常适合初学者学习服务端应用开发。
任务:安装 Express 应用框架
在我们的项目里安装一下 Express 这个应用框架。
1:安装 express
在终端,先进入到项目所在的目录。执行:
cd ~/desktop/xb2-node
然后用 npm install 安装一下 express 这个包,安装的时候指定一个具体的版本。执行:
npm install express@4.17.1
如果在安装的时候不设置具体的版本, npm 会给我们安装最新的 express,但是我们最好先保持一致。
2:观察 node_modules 目录
完成以后你可以查看项目根目录下的 node_modules 目录里的东西,npm 会把安装的包放在这个目录的下面。
ls -la node_modules
3:观察 package.json 文件
然后再打开 package.json 文件观察一下,在 dependencies 属性里面,会记录刚才我们安装的 express 这个包。
"dependencies": { "express": "^4.17.1" }
任务:创建 Web 服务器
之前我们用 Node.js 的 HTTP 模块创建了一个 Web 服务器,现在我们可以用 Express 框架提供的功能创建一个 Web 服务器。
1:重新定义 Web 服务
在编辑器,打开 src/main.js 文件,先清空一下里面的内容,然后输入下面这段代码:
src/main.js<修改>
const express = require('express'); const app = express(); const port = 3000; app.listen(port, () => { console.log('🚀 服务已启动!'); });
上面这段代码就是用了之前在项目里安装的 express 这个应用框架创建了一个 Web 服务。如果你做过前面几章的任务,你应该这段代码的写法不陌生。不过下面我们可以再复习一下。
1.1:导入 express
const express = require('express'); ;
在 src/main.js 文件里,需要使用之前安装的 express 应用框架提供的功能,所以文件的一开始先要导入 express 这个包里提供的东西。在 Node.js 里面,导入包(模块)提供的东西,可以使用 require()
,把要导入的包(模块)的名字告诉它就行了。
const express,就是声明了一个叫 express 的东西。在 JavaScript 语言里,用 const
声明的东西叫 常量(Constant),具体它表示的值是等号右边的东西。这里 express 这个常量的值,就是导入的 express 这个包提供的东西。
1.2:创建应用
const app = express();
之前从 express 这个包导入的 express 是个函数,执行它的时候可以得到一个 express 应用,我们把这个应用交给 app。
用 require 导入的 express 这个包,提供的东西是个 函数(Function),因为之前导入 express 的时候,把它交给了一个叫 express 的常量,所以这个常量就是一个函数。
定义好一个函数,让它可以去做一些事情,只有执行它的时候它才会去做这些事情,执行函数用的形式就是在函数名字的后面加上一组括号。express()
,意思就是执行一下这个叫 express 的函数。
执行这个 express 函数,会得到一个东西,我们把这个东西交给了 app 这个常量。这些常量的名字,你可以随便命名,不过尽量要让它有意义,这样看到它的时候大概就可以判断出它表示的东西是什么。
1.3:声明一个端口
const port = 3000;
定义一个叫 port 的东西(常量),它的值是 3000。这是一个 数字(Number)类型的值,在 JavaScript 语言里,文字(String)类型的值的周围要用引号包装,数字类型的值不需要使用引号包装。这个 port(端口) 稍后会用到。
1.4:创建 Web 服务
app.listen(port, () => { console.log('🚀 服务已启动!'); });
执行 express 包提供的功能,返回了一个函数,我们把这个函数交给了 express,执行它的时候会返回一个东西,我们给这个东西起了个名字叫 app。这个 app 是一个对象(Object),对象就是一种东西或者物件,在对象里面可以提供一些数据或者功能,提供的数据在对象里叫属性(Property),对象里提供的功能叫方法(Method),方法其实就是函数,只不过把函数放在对象里,它的名字就变成方法了。
app.listen()
,意思是执行一下 app 对象里的 listen 方法,这个方法的功能就是创建一个 Web 服务。方法(函数)可能需要一些参数,定义方法的时候会设置它需要的参数,使用这个方法的时候要提供这些参数的具体的值。在方法的内部,会根据执行它的时候,提供的参数值来决定方法的一些行为。
执行 app.listen()
的时候,给它提供了两个它需要的参数,参数之间用逗号分隔开。第一个参数是 Web 服务需要用的端口号,第二个参数是 Web 服务起动以后要执行的函数。() => {}
,这是 JavaScript 语言里的箭头函数这种写法,函数做的事情可以放在大括号里面,在这个函数里,执行了一下 console.log()
,在控制台上输出一行文字:🚀 服务已启动!
2:启动 Web 服务
在终端,项目所在目录的下面,执行:
node src/main.js
运行一下 src/main.js,现在运行它的时候会创建一个 Web 服务,成功以后你会看到输出的 🚀 服务已启动!。说明定义的 Web 服务已经启动了,它会一直运行,如果要停止运行这个 Web 服务,可以使用 ctrl + C 这个按键组合。
3:测试 Web 服务
下面可以找一个客户端,访问一下之前我们创建的 Web 服务。可以用浏览器,访问一下 http://localhost:3000 ,你大概会看到页面上会显示 Cannot GET /,这里的 GET 指的是通过 HTTP 协议发出请求用的一种方法(动作), / 指的是一个地址,这引起东西在后面我们会做详细的介绍。
现在我们只是创建了一个 Web 服务,启动了这个服务,但是应用本身还不能做什么时候,下面我们可以在应用里定义一个接口,先让这个接口简单的响应一行文字。
应用接口 / 路由
我们要给服务端应用设计开发一些功能,可以给这些功能定义一些接口,这样在客户端就可以通过这些接口来使用服务端应用提供的功能了。
接口(Interface),这个词经常会在应用开发里出现,在不同的语境下它可能表示不同的东西。从字面上理解这个词,接口就是跟其它东西对接的一种方法,或者一种东西。API(Application Programming Interface),指的是应用接口,通过这些应用接口可以使用应用提供的功能。
我们平时生活中用的很多东西,也都是通过这些东西的接口使用它们的。比如电动牙刷,它上面有个按钮,按下这个按钮可以启动或者关闭电动牙刷,这个按钮就是电动牙刷提供的一个接口,通过这个接口可以使用电动牙刷提供的功能。电动牙刷的内部可能会很复杂,但对于用户来说并不需要关心这些,只需要知道按下按钮它就开动,再按一下它就会关闭。
在服务端应用里面提供的应用接口都有一个地址,客户端可以通过这些地址使用应用的不同的接口,所以有时候接口也叫路由(Routes 或 Router)。
接口应该是站在客户端的角度出发引出来的一个概念,服务端定义了一些接口,客户端可以使用这些接口。而路由应该是站在服务端这里引出来的概念,在服务端这里设计规划了一些路由(路线),有请求过来,路由会根据请求的地址把它带到应用里指定的地方去处理。在后面我可能会交替使用这两个概念,如果我说定义一个应用接口或者定义一条路由,大概指的是一个意思。
虽然在不同服务端应用框架里,定义接口或者路由的方法都不太一样,但是有一些通用的概念。比如应用接口会支持使用某一种特定的 HTTP 方法来使用,所以在定义它们的时候要指定接口或路由支持的 HTTP 方法,接口都有一个地址,接口也都有一个处理器。在客户端那里可以请求使用应用的接口,请求里要带着一个地址,服务端收到请求会根据请求里的地址来判断客户端要使用的是哪一个应用接口,这样应用就会去执行特定的功能。
任务:定义应用接口(路由)
下面我们通过一个例子,理解一下应用的接口与路由这两个概念。
1:定义接口
src/main.js<添加>
在 *src/main.js *文件里,添加下面内容,定义一个应用接口:
app.get('/', (request, response) => { response.send('你好'); });
上面这段代码就是定义了一个应用接口,也可以说定义了一条路由,地址是 / ,这个地址表示应用的根。
如果用接口这个概念解释我们上面做的事情,就是我们在应用里定义了一个接口,这个接口提供的功能或者说它要做的事情就是响应一行文字,内容是 你好。如果用路由的概念来解释,就是我们在应用里定义了一条路由,如果客户端访问应用的时候使用的地址是 / ,就把这个访问带到一个特定的函数里面来处理,这个函数就是路由设置的处理器,这个函数做的事情就是给客户端响应一行文字,内容是 你好。
定义路由的时候用的是 app
上的 get()
这个方法,app
是我们创建一个 Express 应用,它上面有一些方法可以做一些事情。这里用了一下它的 get()
这个方法,这个方法可以定义一条支持 HTTP 的 GET 方法使用的路由。
之前我们在服务端创建了一个 Web 服务,启动了这个服务以后,在客户端那里就可以通过 HTTP 协议跟这个服务交流。HTTP 协议提供了几种不同的方法,比如 GET、POST、PATCH、DELETE。客户端在发出请求的时候,除了要说明请求的地址,还需要设置使用一种特定的 HTTP 方法。
我们在定义上面这个接口的时候,用的是 app.get()
这个方法,所以这个接口只支持使用 HTTP 的 GET 这个方法来使用它。一般客户端要获取数据的时候,都会使用 HTTP 的 GET 方法对服务端发出请求。
这个接口的地址我们设置成了 /,它表示的是应用的根。这个地址可以作为 get(
) 方法的第一个参数。然后我们又设置了一个处理器,这里就是用了一个没有名字的函数。在 Express 框架里面,路由的处理器有两个参数,第一个参数表示的是请求,第二个参数表示的是响应,在请求参数里会带着一些客户端请求相关的数据,在响应参数里面提供了一些方法可以处理给客户端作出的响应。
处理器里的这两个参数的名字你可以随便定义,这里我用的是 request 表示请求,用 response 表示响应。你也可以用 req 表示请求,用 res 表示响应。
在定义的这个接口的处理器里面,可以设置这个接口要做的一些事情,比如到应用的数据仓库里,提取一些数据,再把它们发送给客户端。暂时只是为了演示接口还有路由的定义,所以可以简单的给客户端直接响应一行文字,这里做出响应的时候,用的是 response 参数上的 send()
这个方法,把要响应的数据交给这个方法就可以了。
2:重新启动服务
修改了应用之后需要重新启动服务才能生效,在终端,之前运行 Web 服务的地方,按一下 ctrl + C 可以停止运行服务,然后重新执行 node src/main.js 。
3:测试使用接口(路由)
打开浏览器,访问 http://localhost:3000/ ,你会在页面上看到 你好 这两个字。在地址栏输入地址,按下回车。就相当于是用 HTTP 的 GET 方法对指定的地址发出请求,请求的地址是应用里的 / ,也就是应用的根。在我们的服务端正好设计了这样一条路由,所以服务端可以处理客户端的这个使用了 HTTP 的 GET 方法,对 / 这个地址的请求。
在我们的应用里面,收到了这个请求要做的事情就是给客户端响应了一行文字,内容就是 你好。
任务:准备演示数据
在真实的应用里,这些数据可能来自应用的数据仓库,在后面的旅程中,我们会练习如何在数据仓库里存取数据。暂时为了演示接口概念,我们就先简单的手工准备了一组数据。
src/main.js<添加>
const data = [ { id: 1, title: '关山月', content: '明月出天山,苍茫云海间', }, { id: 2, title: '望岳', content: '会当凌绝顶,一览众山小', }, { id: 3, title: '忆江南', content: '日出江花红胜火,春来江水绿如蓝', }, ];
上面定义了一个叫 data
的东西,它的值是一个数组,里面有三个数据项目,每个数据项目都是一个对象,每个对象里面有三个属性,分别是 id
,title
还有 content
。
任务:定义响应 JSON 数据的应用接口
服务端应用一般都会给客户端响应 JSON 格式的数据,之前我们试过,要先把数据转换成 JSON 格式的,然后再把它响应给客户端,做出响应的时候需要设置 Content-Type 这个头部,告诉客户端数据的格式是 JSON。使用 Express 框架,这些事情不需要我们自己操心,框架本身就会处理好。
比如我们要定义一个接口,可以响应一组 JSON 格式的数据,可以先定义一组数据。
src/main.js<添加>
app.get('/posts', (request, response) => { response.send(data); });
定义这个接口同样用的是 app.get()
方法,这样这个接口就支持客户端用 HTTP 的 GET 方法来请求使用。我们给这个接口设置的地址是 /posts
,在这个接口的处理器里面,用了一下 response.send()
方法,把之前准备好的一组数据响应给客户端。也就是现在如果有人请求应用的 /posts
这个地址,它就会得到一组 JSON 格式的数据。
Posts 在这里指的是 “贴子” 的意思,表示的是用户发布的内容。
地址参数
先想一下我们的照片分享应用,如果客户端需要某一张照片相关的数据,那这个接口应该如何定义?我们不可能为每一张照片都单独定义一个接口,可以这样,就是在定义接口的时候,在设置的接口地址里面定义一些参数。这样在客户端那里请求使用这个接口的时候可以设置参数的值,接口的处理器会根据这个参数值决定应该把什么内容发给客户端。
任务:定义带参数的应用接口
1:定义带参数的应用接口
src/main.js<添加>
app.get('/posts/:postId', (request, response) => { console.log(request.params); // 获取内容 ID const { postId } = request.params; // 查找具体内容 const posts = data.filter(item => item.id == postId); // 作出响应 response.send(posts[0]); });
上面这段代码定义了一个支持用 HTTP 的 GET 方法使用的接口,接口地址里添加了一个叫 postId 的参数,在客户端使用这个接口的时候可以设置这个地址参数的值。
定义这个接口用的是 app.get()
,给接口设置的地址是 /posts/:postId,注意这回给接口设置的地址里面包含了一个参数,参数的名字叫 :postId 。地址参数就是用冒号开头的,然后是参数的名字,在处理器那里可以用这个参数的名字获取到参数的值。在接口里设置的地址参数可以有很多个,中间用 / 分隔开就行了。
因为接口是用 app.get()
方法定义的,所以这个接口支持使用 HTTP 的 GET 方法来使用。使用的时候设置的地址应该是 /posts/ 后面还要再加上一个参数值,这个值就是给接口的 :postId 这个参数准备的。也就是如果地址设置成 /posts/1,意思就是给 :postId 这个参数的值是 1。在接口的处理器里面,我们可以得到地址参数的值,然后根据这个参数的值去调取对应的数据。
1.1:输出地址参数
console.log(request.params);
接口的地址参数的值,可以在 request 参数里的 params 这个属性里得到,你可以把这个属性输出到控制台上检查一下。
1.2:获取地址参数
在接口的处理器里面,可以从请求里获取到地址里的参数的值,这样处理器就可以根据这个参数的值去为客户端准备对应的内容了。
// 获取内容 ID const { postId } = request.params;
上面用了一个解构的写法,把 request.params 这个对象里的 postId 属性解构出来,交给 postId。在这个接口的处理器里面,我们给表示请求的参数起的名字叫 request,Express 框架会把一些东西放在这个 request 上面。比如,如果请求的地址里带着参数,它就会把这些参数还有它的值放到 request 里的 params 这个属性里面。
比如在客户端那里请求 /posts/1 这个地址,在这个接口处理器里面的 request 参数里的 params 属性里面,会有一个 postId 属性,对应的值就是 '1'
。之所以在 params 里面有名字是 postId 的属性,是因为我们在设计这个接口(路由)的时候给这个地址参数起的名字就是 postId。也就是这个 params 的值看起来会像这样:
{ postId: '1'; }
1.3:根据地址参数调取数据
获取到请求的地址参数里的内容 ID(postId) 这个参数的值以后,我们就可以在处理器里面利用这个参数的值,把客户端想要的数据找出来,再交给客户端。在真实的应用里,应该就是根据这个内容的 ID 去查询应用的数据仓库,然后把得到的数据响应给客户端。
这里我们可以暂时从之前手工准备的那组数据里面,把一个特定 id
值的数据项目找出来,交给客户端,继续在处理器里输入:
// 查找具体内容 const posts = data.filter(item => item.id == postId);
上面用了 data
这个数组上天生就有一个叫 filter
的方法,它可以把数组里面符合条件的项目筛选出来,在 JavaScript 里面,数组上面天生就有这个方法。筛选的结果是一个新的数组,里面包含了符合条件的数据项目。这里我们就是把 id
属性的值等于 postId
这个地址参数值的数据项目从 data
这个数组里筛选出来,再把结果交给了 posts
。
1.4:把数据发给客户端
最后可以让接口的处理器给客户端做出响应:
// 作出响应 response.send(posts[0]);
因为之前筛选的结果是个数组,我们知道这个数组里应该就只有一个数组项目,也就是客户端想要的指定 id
的数据项目。所以在作出响应的时候可以把这个数组里的第一个数据项目作为响应里的数据。
2:重新启动服务
在终端,先停止运行服务,然后重新再运行一下,这样刚才定义的接口才会生效。
3:测试应用接口
在浏览器,可以访问一下 http://localhost:3000/posts/1,从服务端那里得到的响应的数据应该就是 id
属性的值是 1
的这个数据项目。
{ "id": 1, "title": "关山月", "content": "明月出天山,苍茫云海间" }
总结
Express 是一款灵活的轻型服务端应用框架,你可以基于这个框架按照自己的想法来创建服务端应用,它是一个包,所以我们可以使用 npm 把它安装在自己的项目里使用。导入 Express ,创建一个应用,再用它创建一个 Web 服务,然后就可以使用一些方法去定义服务的接口。比如 app.get(
) 可以定义支持 HTTP 协议的 GET 方法使用的接口。
接口都有一个地址,客户端可以利用这个地址来使用服务端的这个接口,接口还需要一个处理器,在处理器里我们可以设置接口要做的一些事情。接口地址有固定的部分,也可以设置一些地址参数,在客户端可以设置这些地址参数的值,服务端可以利用地址参数的值来判断到底要去做什么事情。
下一站,我们要去定义各种不同类型的接口,再深入地理解一下客户端与服务端之间的沟通方式。
任务:整理项目
确定之前对项目做的修改全部做了提交,并且项目当前没有任何修改。然后把当前分支切换到 develop,执行:
git checkout develop
在 develop 分支上做一次合并:
git merge express
如果你为项目添加了远程,可以把 express 分支推送到远程,执行:
git push origin express