nodejs入门
node是一个用来执行javascript代码的运行时环境。本质上,node是一个集成了Chrome v8引擎的C++程序。
Chrome v8是目前世界上最快的javascript引擎,最早是为Chrome浏览器开发的。后来一个天才程序员Ryan Dahl把Chrome v8集成在一个C++程序中(这就是node的起源),于是javascript就从只能在浏览器环境中运行的脚本代码,变成可以直接在电脑上跑,并可以调用电脑的文件系统,网络等资源的代码了。于是javascript也可以做后端开发了。
我们使用node来开发高性能,可拓展的网络程序。node是开发RESTful服务的完美选择。
node是单线程的。意味着我们将会在一个线程中服务所有的客户。
node程序默认是异步的(或者理解为非阻塞)。这意味着node在处理I/O操作(网络请求或者访问文件系统)时,线程不会等待操作的结果,它会被释放去服务其他的客户。
所以,node的异步架构使它成为开发IO密集型程序的理想方案。
注意:由于node是单线程的,所以不要用node来开发CPU密集型的程序(例如视频编解码等),因为CPU密集型的操作会长时间占用线程,导致node异步的优势无法发挥出来。
node环境和浏览器环境的全局变量是不一样的。node中没有window和document等对象,相反,node有很多浏览器环境没有的对象,例如处理文件系统的对象,处理网络的对象,处理操作系统的对象等。
node核心概念
node没有window等浏览器环境存在的全局对象,不过node有一个全局对象叫global。
在浏览器环境中,变量默认会被添加在window全局对象中,但是node环境不会。在node中,每一个js文件都是一个模块。每一个js文件中声明的变量,其作用域只在该文件中,除非我们导出它:
module.exports = sayHi;
然后我们可以在另一个文件中导入使用:
const sayHi = require('./sayHi');
其实就是commonjs模块。
node有很多内置的模块,例如访问文件系统的模块,访问网络的模块等。EventEmitter
是核心模块之一,很多内置的模块都是基于它完成的。通过继承EventEmitter
,可以让我们的对象获得订阅和发布消息的能力。
NPM
所有的node程序都有一个package.json
文件。package.json
记录我们node程序的元数据,例如程序名字,程序版本,依赖包等。
我们使用npm从NPM社区下载第三方包,所有的第三方包(和第三方包自己的依赖包)都会保持在node_modules
文件夹里。
node_modules
文件夹默认从源代码管理中排除。
常用node命令:
// Install a package
npm i <packageName>
// Install a specific version of a package
npm i <packageName>@<version>
// Install a package as a development dependency
npm i <packageName> —save-dev
// Uninstall a package
npm un <packageName>
// List installed packages
npm list —depth=0
// View outdated packages
npm outdated
// Update packages
npm update
//To install/uninstall packages globally, use -g flag.
使用Express来搭建RESTful服务
REST定义了一组用于创建HTTP服务的约定:
-
POST:创建资源;
-
PUT:修改资源;
-
GET:获取资源;
-
DELETE:删除资源。
Express是一个简单、简约、轻量级的web构建框架服务器。使用Express可以很方便地构建一个RESTful服务。
安装Express:
npm i express
简单使用:
const express = require("express");
const app = express();
// Creating a course
app.post("/api/courses", (req, res) => {
// Create the course and return the course object
resn.send(course);
});
// Getting all the courses
app.get("/api/courses", (req, res) => {
// To read query string parameters (?sortBy=name)
const sortBy = req.query.sortBy;
// Return the courses
res.send(courses);
});
// Getting a single course
app.get("/api/courses/:id", (req, res) => {
const courseId = req.params.id;
// Lookup the course
// If not found, return 404
res.status(404).send("Course not found.");
// Else, return the course object
res.send(course);
});
// Updating a course
app.put("/api/courses/:id", (req, res) => {
// If course not found, return 404, otherwise update it
// and return the updated object.
});
// Deleting a course
app.delete("/api/courses/:id", (req, res) => {
// If course not found, return 404, otherwise delete it
// and return the deleted object.
});
// Listen on port 3000
app.listen(3000, () => console.log("Listening..."));
我们可以使用nodemon来监听javascript代码的更改和自动重启node程序。
我们可以使用环境变量来存储node程序的各种设置。在代码中我们可以通过process.env
来访问环境变量。
// Reading the port from an environment variable
const port = process.env.PORT || 3000;
app.listen(port);
我们不能信任客户发送的任何数据!所以在保存客户的数据时,需要先验证一下数据是否有问题。Joi可以帮我们完成数据验证的工作。
Express进阶
中间件函数,是一个可以接收请求对象的函数,它可以提前结束一个请求,或者将这个当前请求传递给下一个中间件函数。
Expess有一些内置的中间件函数:
- json()。将请求body转换为json。
- urlencoded()。将请求body转换为URL-encoded payload。
- static()。支持静态文件服务。
通过中间件函数,我们可以实现日志打印,用户授权等功能。
// Custom middleware (applied on all routes)
app.use(function (req, res, next) {
// ...
next();
});
// Custom middleware (applied on routes starting with /api/admin)
app.use("/api/admin", function (req, res, next) {
// ...
next();
});
可以使用config来管理node程序的配置信息。
可以使用debug来添加node程序的调试信息(代替console.log)
通过模版引擎,我们可以为客户端返回HTML数据。pug和EJS等都是比较常用的模版引擎。
mongodb
MongoDB是一个开源的文档数据库,它使用灵活的,类似JSON格式的文档来储存数据。
在关系型数据库里(例如MySql),我们有tables和rows,在MongoDB里我们有collections和documents。一个document可以包含sub-documents。
建议采用mongoose来操作mongoDB。
// Connecting to MongoDB
const mongoose = require("mongoose");
mongoose
.connect("mongodb://localhost/playground")
.then(() => console.log("Connected..."))
.catch((err) => console.error("Connection failed..."));
要使用mongoDB储存数据,首先我们要定义一个mongoose schema,mongoose schema定义了MongoDB中document的形状。
// Defining a schema
const courseSchema = new mongoose.Schema({
name: String,
price: Number,
});
我们还可以使用SchemaType object来为mongoose schema添加更多属性:
// Using a SchemaType object
const courseSchema = new mongoose.Schema({
isPublished: { type: Boolean, default: false },
});
mongoose schema支持的类型有
String
,Number
,Date
,Buffer
(用来储存二进制数据),Boolean
和ObjectID
.
当我们定义好schema后,还需要将它转换成一个modal。modal可以看作是一个class,它是创建object的蓝图:
// Creating a model
const Course = mongoose.model("Course", courseSchema);
CRUD操作:
// Saving a document
let course = new Course({ name: "..." });
course = await course.save();
// Querying documents
const courses = await Course.find({ author: "Mosh", isPublished: true })
.skip(10)
.limit(10)
.sort({ name: 1, price: -1 })
.select({ name: 1, price: 1 });
// Updating a document (query first)
const course = await Course.findById(id);
if (!course) return;
course.set({ name: "..." });
course.save();
// Updating a document (update first)
const result = await Course.update({ _id: id }, { $set: { name: "..." } });
// Updating a document (update first) and return it
const result = await Course.findByIdAndUpdate(
{ _id: id },
{ $set: { name: "..." } },
{ new: true }
);
// Removing a document
const result = await Course.deleteOne({ _id: id });
const result = await Course.deleteMany({ _id: id });
const course = await Course.findByIdAndRemove(id);
在定义schema的时候,还可以通过SchemaType object对属性定义验证要求:
// Adding validation
new mongoose.Schema({
name: { type: String, required: true },
});
Mongoose会在保持数据到mongoDB之前执行验证逻辑。我们也可以通过调用validate()
方法手动执行验证逻辑。
内置的验证方法:
- Strings: minlength, maxlength, match, enum
- Numbers: min, max
- Dates: min, max
- All types: required
我们也可以自定义验证方法:
const userSchema = new Schema({
phone: {
type: String,
validate: {
validator: function (v) {
return /\d{3}-\d{3}-\d{4}/.test(v);
},
message: (props) => `${props.value} is not a valid phone number!`,
},
required: [true, "User phone number required"],
},
});
验证方法可以定义为异步的(有些验证方法可能需要执行从数据库读取数据等异步操作):
validate: {
isAsync: true
validator: function(v, callback) {
// Do the validation, when the result is ready, call the callback
callback(isValid);
}
}
其他SchemaType比较常用的属性:
- Strings: lowercase, uppercase, trim
- All types: get, set (to define a custom getter/setter)
price: {
type: Number,
get: v => Math.round(v),
set: v => Math.round(v)
}
mongoDB进阶
关联数据
mongoDB是非关系型数据库,所以它没有类似于MySql的JOIN等操作方式进行联表查询。在mongoDB中要实现两个有关联的数据,有两种方式:
- 通过引用的方式。一个数据保存另一个数据的ObjectId;
- 通过嵌套的方式。一个数据中潜逃另一个数据。
方式1的优点是能保持两个数据的独立性,缺点是查询速度变慢,因为要查询两个记录。
方式2的优点是查询速度快(只需要查询一个记录),缺点是被嵌套的数据如果不只嵌套在一个地方,那么要保证这个数据的同步更新!如果同步的过程出现错误,那么这个数据在不同的地方可能是不一致的。
使用那种关联方式,要看你是否能接受数据可能不同步的情况,如果不能接受,就使用方式1。
通过引用的方式关联数据:
// Referencing a document
const courseSchema = new mongoose.Schema({
author: {
type: mongoose.Schema.Types.ObjectId,
ref: "Author",
},
});
通过嵌套的方式关联数据:
// Referencing a document
const courseSchema = new mongoose.Schema({
author: {
type: new mongoose.Schema({
name: String,
bio: String,
}),
},
});
被嵌套的的documents没有自己的save方法,它们只能在它的parent的上下文中保存。
// Updating an embedded document
const course = await Course.findById(courseId);
course.author.name = "New Name";
course.save();
事件
在MySql之类的数据库中,可以通过事件机制,可以保证若干个不同的表同时更新数据。mongoDB没有事件机制。为了实现类似事件的机制,我们可以使用一种叫“Two Phase Commit”的保存方式。可以使用Fawn来实现事件的效果。
ObjectID
ObjectID由MongoDB driver生成,用来唯一标记一个document,它由12个字节组成:
- 4 bytes: timestamp
- 3 bytes: machine identifier
- 2 bytes: process identifier
- 3 byes: counter
尽管使用了这么严密的方式来保证ObjectID的唯一性,但是还是有1/16,000,000的几率会生成两个一样的ObjectID!
我们可以使用joi-objectid 来验证一个ObjectID是否有效。
验证和授权
验证(Authentication):一般是通过账号和密码,验证一个用户是否有效。
授权(Authorization):决定一个用户是否有权限进行某项操作。
在保存用户信息时,对于用户密码,不能把密码原文保存在数据库!一般都是保存密码的hash值。
我们可以使用bcrypt来hash密码:
// Hashing passwords
const salt = await bcrypt.genSalt(10);
const hashed = await bcrypt.hash("88888888", salt);
// Validating passwords
const isValid = await bcrypt.compare("88888888", hashed);
验证通过后,我们在服务器中生成一个JSON Web Token (JWT) 返回给客户端,客户端后面每次发起请求时,都要携带JWT参数,服务器通过验证JWT中携带的参数,来决定这个请求的权限。
JSON Web Token (JWT)是一个被编码成一个长字符串的JSON数据,它类似于通行证或者司机的驾照,它包含了用户的信息(例如用户ID,用户身份等)。它不能被篡改,因为修改JWT需要重新生成数字签名。
一般JWT不要保存在服务端,否则一旦服务器被攻击,JWT落入黑客手里就危险了。JWT由客户端保存就行了。
我们可以使用jsonwebtoken来生成JWT。
// Generating a JWT
const jwt = require(‘jsonwebtoken’);
// Generating a JWT
const jwt = require('jsonwebtoken');
const token = jwt.sign({ _id: user._id}, 'privateKey');
永远不要储存私钥和其他密码在代码中!要储存在环境变量中!
授权(Authorization)操作可以放在中间件函数中执行。当JWT无效时,返回401,当JWT有效,但是没有操作权限时,返回403。
处理和打印错误
错误是无法避免的,无论是代码的bug,还是其他不可抗因素。作为一个优秀的开发者,需要把程序运行的错误信息记录下来。
node程序运行时,有三种类型的错误:
-
请求处理pipeline中出现的错误。也就是中间件函数中出现的错误。
-
uncaughtException。
-
unhandledRejection。
要捕捉第一种错误,可以在所有路由的最后面,注册error middleware:
app.use(function (err, req, res, next) {
// Log the exception and return a friendly error to the client.
res.status(500).send("Something failed");
});
而且要保证每个中间件函数都把错误传递下去:
app.use(function (req, res, next) {
try {
// do something
} catch (error) {
// 把错误信息传递下去!
next(error);
}
});
每个中间件函数都要手动添加trycatch
非常不方便,可以使用express-async-errors解决这个问题。
我们可以使用process.on('uncaughtException')
和process.on('unhandledRejection')
来捕捉另外两种错误。当这两种错误出现后,最好重新启动程序,因为这意味着程序运行的环境可能不干净了。
捕捉到错误信息后,可以把信息打印在控制台,也可以保存在文件,也可以保存在数据库里,或者开发环境和发布环境采用不一样的打印策略。可以使用winston来实现这些需求。