Nodejs基础1
在 react基础2 react基础3表单与后端 react基础4授权与部署
这三篇博客中,我们讨论了vidly(一个电影展示项目)的客户端,但是我们在博客中仅仅调用了后端的api,对后端如何构建并没有提及。而Nodejs构造的后端是期末大作业中必不可少的一部分,所以这个系列让我们来谈谈如何用Nodejs+express+mongodb来构建一个vidly后端。
Building RESTful API’s Using Express
Introduction
下面利用express框架搭建后端
RESTful Services
大多数的网站结构如下:
RESTful 就是 Representational State Transfer 的缩写,是用来创建这些规范的HTTP服务的。我们利用简单的Http语言来进行增删改查,也就是CRUD operations
大多数的网站url如下
我们常常在.com后看到/api,这不是必要的规定,但是可以见到很多公司都遵循这个范式。customer代表了应用中用户的集合。在RESTful中我们把它看成是一种资源。我们可以把诸如用户,电影,租金或者各种资源都开放出去。所以可以是/rentals,/movies 不同的后缀代表操纵不同的用户资源。这个/customer可以对用户资源进行操作比如说创建用户,更新用户信息。
所有的HTTP请求都有所谓的动作或者方法,取决于他们的类型和目的
每个动作的模型如下
GET:获取到所有的用户的信息,返回一个列表。如果想单独得到一个用户的信息,需要这么写
/api/customers/1,这样就只返回一个用户的信息
PUT:用户更新
DELETE:删除操作
POST:新建操作
接下来我们把关注点集中在创建http服务上面。因此不适用数据库,就使用简单的数组在内存中保存数据
Introducing Express
express可以更方便的帮助我们管理路由
Express框架可以在良好维护性的前提下创建很多路由规则
新建一个express-demo文件夹,npm init —yes 新建package-json
npm install express
Building Your First Web Server
这里说一嘴 require和import的区别
import和require都是被模块化使用
a. require是CommonJs的语法(AMD规范引入方式),CommonJs的模块是对象。
b. import是es6的一个语法标准(浏览器不支持,本质是使用node中的babel将es6转码为es5再执行,import会被转码为require),es6模块不是对象a. require是运行时加载整个模块(即模块中所有方法),生成一个对象,再从对象上读取它的方法(只有运行时才能得到这 个对象,不能在编译时做到静态化),理论上可以用在代码的任何地方
b. import是编译时调用,确定模块的依赖关系,输入变量(es6模块不是对象,而是通过export命令指定输出代码,再通过 import输入,只加载import中导的方法,其他方法不加载),import具有提升效果,会提升到模块的头部(编译时执行)
export和import可以位于模块中的任何位置,但是必须是在模块顶层,如果在其他作用域内,会报错
es6这样的设计可以提高编译器效率,但没法实现运行时加载a. require是赋值过程,把require的结果(对象,数字,函数等),默认是export的一个对象,赋给某个变量(复制或浅拷贝)
b. import是解构过程(需要谁,加载谁)
1 | //我们首先引入express |
Nodemon
Nodemon可以实时监控后端更改的内容并更新服务器。所以就不用每次修改以后重启服务器了
npm install -g nodemon
nodemon index.js 让nodemon检测该文件夹中所有文件、任何文件名和任何扩展名的改动
Environment Variables
我们现在是规定了在3000端口,但是可能会产生端口冲突,为了解决这个问题我们可以使用环境变量
1 | const express = require("express"); |
然后设置 set PORT=5000,那么就有两种选择了
Route Parameters
我们现在想获得单一课程的信息也就是 /api/courses/1
我们在 react基础2 的路由部分有所提及 路由变量
1 | app.get("/api/courses/:id", (req, res) => { |
也可以有多个变量
1 | app.get("/api/posts/:year/:month", (req, res) => { |
当我们传入这个的时候,http://localhost:3000/api/posts/2020/6 会显示如下对象
使用这种表达式也可以读取查询字符串。也就是在url后面的参数
http://localhost:3000/api/posts/2020/6?sortBy=name
这个表达式的意思获取所有2020年6 月的帖子,然后根据名字来排序。sortBy=name 是查询字符串。我们使用它来向后端服务传递额外的参数。
所以我们用路由参数提供路由必须的数据与值,使用查询字符串传递附加的内容。
后端可以这么来读取查询字符串
1 | app.get("/api/posts/:year/:month", (req, res) => { |
Handling HTTP GET Requests
1 | //... |
入读,我们id=1,匹配到,那么就返回这个对象。否则返回 The course with the given ID was not found
Handling HTTP POST Requests
现在我们利用post请求创建新的课程
1 | /* |
Calling Endpoints Using Postman
我们利用google插件postman,在react基础3表单与后端 中有所介绍
这就是如何使用postman测试http终端。在实现过程中我们假设请求体中包含一个name属性的对象。
Input Validation
但是如果用户传入了不合法的数据,那么我们需要对其验证。
从安全角度,我们永远不要相信客户输入的内容,所以我们要永远验证输入的内容
在这里我们可以简单写一个验证逻辑
1 | app.post("/api/courses", (req, res) => { |
如果不想写复杂的逻辑,我们可以导入一个包 在react基础3表单与后端 中有提到Joi。在后端也可以使用
npm i joi@13.1.0
1 | app.post("/api/courses", (req, res) => { |
如果正确,那么我们就打印出来的是一个对象,对象中error属性为null
反之我们对象中就有一个错误了
我们如果res.status(400).send(result.error);会显示一个比较复杂的对象,我们整整需要的是details中的message
所以我们改成这样res.status(400).send(result.error.details[0].message);当再次发送空的name或者name.length<3 的话,在postman中我们得到这两个报错
“name” is required
“name” length must be at least 3 characters long
Handling HTTP PUT Requests
这里重构了一下,把验证单独抽离出来成为一个函数。
逻辑是这样的,先验证是否存在这个id,再验证要修改的内容是否合法,最后进行更新和返回
1 | function validateCourse(course) { |
Handling HTTP Delete Requests
删除的逻辑:首先找到movie.id,找不到返回404,找到了我们就取这个课程的id。通过splice函数删除
splice(从第几个元素开始,删除几个元素)
1 | app.delete("/api/courses/:id", (req, res) => { |
如果传入http://localhost:3000/api/courses/10
得到 The course with the given ID was not found
我们先来删除,再获取,发现已经找不到courses/1了
代码还需要做一个小改进,就是当发现错误的时候,立即return
1 | const express = require("express"); |
Project- Build the Genres API
现在我们来船舰vidly应用的后端
首先我们创建一个管理电影分类的终端。每个电影都有自己的分类
1 | const Joi = require('joi'); |
A Quick Note
Express- Advanced Topics
Introduction
在这章节我们会学Middleware,Configuration,Debugging,Template Engines
Middleware
express中的一个核心概念就是中间件(中间件函数) 。一个中间件技术上说就是:我们得到一个请求对象,要么返回客户端,要么传递给一个中间件。
在Express当中,所有route handler都是中间件:因为它需要传入一个请求对象,并且在这里向客户端返回数据。 express.json() 这个函数返回一个中间件。这个函数的作用就是读取请求,如果请求体是一个JSON格式的对象。他就会格式化这个JSON对象并以此设置req.body属性
处理模型大致是这样的。当服务器收到一个请求,请求就进入一个管道,我们将这个管道成为请求处理管道(Request Processing Pipeline) 管道之中由一个或者多个中间件。每个中间件要么根据请求向客户端返回数据,要么将控制权交给其他的中间件
之前的代码中这个 RPP中由两个中间件,一个是把请求转化成一个JSON格式对象,它并没有终结Request Response Circle,所以他把控制权交给了下一个中间件,也就是route handler 。route handler中 req.body属性已经设置好了,这样就可以进行一些操作了。 最后向客户端发送反馈来终结 Request Response Circle
Express中有内部中间件函数,我们同样也可以在RPP中添加自定义的中间件函数
使用自定义的中间件,我们可以创建 Crosscutting Concerns,比如实现登录验证等功能。
Creating Custom Middleware
我们通过调用use方法在RPP中安插一个中间件。传入一个函数,里面有三个参数。next表示管道中下一个中间件的引用。这里写了两个中间件。注意,一定要写next()把控制权交给下一个中间件,否则的话我们没有闭合 Request Response Circle,那么就一直显示Loading…
中间件是按照顺序调用的,首先调用logging函数,其次调用 Authenticating函数
1 | app.use(function(req,res,next){ |
为了让代码变得更加简明,当我们创建中间件的时候我们不要写在index中。我们要把每个中间件放在各自独立的文件当中
新建一个logger.js存放我们的中间件
1 | function log(req, res, next) { |
在index中导入
1 | const logger = require("./logger"); |
现在理解了express.json()的意思了吧。当我们调用它时,他返回了一个需要三个参数的函数。req,res,next
他格式化了请求体,如果是一个JSON对象他就设置为req.body属性
Built-in Middleware
Express有很多Built-in 中间件,比如 express.json()
还有类似的中间件app.use(express.urlencode());
app.use(express.static(’public‘)); 我们可以把所有静态文件(css,图片等)放到这里去,static中间件是从根目录开始起作用的
这样就可以通过url访问这些文件了
Third-party Middleware 第三方中间件
官方文档中有很多中间件
这里我们使用helmet 中间件,Helps secure your apps by setting various HTTP headers
npm i helmet
1 | const helmet = require("helmet"); |
另一个有用的中间件是Morgan,我们利用它来进行HTTP请求的日志记录.这里是用最简单的格式,tiny
1 | const morgan = require("morgan"); |
使用了Morgan以后,我们再进行操作的话,后端就会出现日志
顺便说,Morgan默认请求实在控制台记录日志。同样也可以设置它写在日志文件当中。我可以有一个配置文件,在某些特定的场景下可以短时间开启这个功能。(开启后对运行效率有影响)
Environments
也许我们想依照环境类型决定是否开关某功能。例如我们只想再开发环境当中开启对HTTP请求的日志记录
我们利用process对象,他是node的全局对象,通过它可以访问当前的进程,这个process对象有一个env属性
它提供我们环境变量的值。有一个标准的环境变量是NODE_ENV。 这个值返回当前node所在环境的值。如果他没有被设置,那么他就会返回未定义。同样我们可以在外部设置它,把它设置为production,testing等等
还有一种方法可以获得默认环境,是app的一个方法app.get(“env”),这其实默认调用了NODE_ENV,但是当NODE_ENV未定义的时候,这个方法默认返回开发环境的值
我们来打印一下默认的环境
1 | console.log(`NODE_ENV:${process.env.NODE_ENV}`); |
NODE_ENV:undefined
app:development
在这个例子当中我们只想在开发环境使用日志。我们就可以这么写代码
1 | if(app.get('env')==='development'){ |
但是我们如果把环境设为生产,Morgan就不会起作用了
在Windows通过set命令来设置环境变量
set NODE_ENV = production
Configuration
现在来讨论如何保存应用的配置数据。并且在不同的环境中复写对应配置
比如在测试环境,我可以需要使用一个不同的数据库或者邮件服务器。
我们可以利用 rc 包
https://www.npmjs.com/package/rc
也可以使用 config包
https://www.npmjs.com/package/config
npm i config
我们先在根目录下新建一个config文件夹
里面有三个JSON文件:default,development,production
1 | { |
1 | { |
1 | { |
然后再index.js中导入这个包。利用get方法就会自动根据当前的环境变量找到相应的文件
1 | const config = require("config"); |
输出:
Application Name:My Express App =Development
Maile Server Name:dev-mail-server
利用这个模块我们可以轻松的设置不同的环境的配置
但是不能把密码等机密放在json文件夹当中。因为这样很不安全,每个人都能看见。。
我们应该把他们保存在环境变量当中。我们来设置一下
终端中输入
1 | set app_password=1234 |
然后我们新建一个 custom-environment-variables (名字一定要写对)
在这个文件只有映射关系,映射环境变量和应用配置的关系
在这里只有password 到app_password 的映射关系
1 | { |
Debugging
如果用console.log()来调试,会很麻烦,改来改去。所以更好的在控制台调试的方式是使用node的debug模块
也就是说用debug函数替换所有的console.log 这样就不需要注释掉他们,可以通过在外部修改环境变量实现
1 | const startupDebugger = require("debug")("app:startup"); |
然后通过
1 | set DEBUG=app:startup |
查看Debug信息
Templating Engines
有时候我们需要返回HTML标记语言文本到客户端而不是JSON文件,那么我们就需要模板引擎了
那么我们新建一个views文件夹,然后在里面新建一个index.pug文件
1 | html |
在index.js中配置
1 | app.set("view engine", "pug"); |
虽有这样修改,可以看到Helloworld已经被渲染出来了
但是我们后端不需要什么视图引擎或者模板引擎
Database Integration
Authentication
Express没有验证功能
Structuring Express Applications
在现实开发中我们肯定不会把所有的东西放到index.js当中去。所以我们要正确的结构化我的应用
第一件事就是清理所有设计courses的接口,并且把它放到一个独立的文件当中去。换句话说,每个独立的api终端的逻辑代码,都要转为一个独立的文件或者模块。所有的coureses的路由都要丢到一个courses.js文件当中去
我们在根目录新建一个新的routes文件夹里面有文件courses.js。把所有包含courses.js的文件全部放到courses.js中
courses.js的配置 把app都换成router
1 | const express = require("express"); |
同样的,把home.js 抽离出来
1 | const express = require("express"); |
最后,把logger放到middleware文件夹当中
…
index.js中
1 | const startupDebugger = require("debug")("app:startup"); |
Project- Restructure the App
现在我们重构之前的vidly项目,
index.js
1 | const Joi = require("joi"); |
genres.js 替换app为route, 替换/api/genres为/
1 | const Joi = require("joi"); |