美文网首页
nodejs入门

nodejs入门

作者: 杨志聪 | 来源:发表于2023-11-11 10:12 被阅读0次

    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服务的约定:

    1. POST:创建资源;

    2. PUT:修改资源;

    3. GET:获取资源;

    4. 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有一些内置的中间件函数:

    1. json()。将请求body转换为json。
    2. urlencoded()。将请求body转换为URL-encoded payload。
    3. 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数据。pugEJS等都是比较常用的模版引擎。

    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 (用来储存二进制数据), BooleanObjectID.

    当我们定义好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中要实现两个有关联的数据,有两种方式:

    1. 通过引用的方式。一个数据保存另一个数据的ObjectId;
    2. 通过嵌套的方式。一个数据中潜逃另一个数据。

    方式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程序运行时,有三种类型的错误:

    1. 请求处理pipeline中出现的错误。也就是中间件函数中出现的错误。

    2. uncaughtException。

    3. 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来实现这些需求。

    相关文章

      网友评论

          本文标题:nodejs入门

          本文链接:https://www.haomeiwen.com/subject/izdabdtx.html