下载APP

NodeJS

Node.js

Node.js是运行在服务器端的JS,用来编写服务器。它是单线程的、异步且非阻塞的,统一了API。

CommonJS模块化规范

早期网页中,没有实质的模块规范,最原始的通过script标签引入多个js文件。比如早期的jquery库也是一个模块,但是使用它时,只能全部引入,无法按需引入。并且在复杂的模块场景下容易出错,比如顺序的问题。

直到2015年,JavaScript都没有一个内置的模块化系统。但是民间大神开始着手自定义模块化系统,CommonJS就是其中的佼佼者,同时它也是Node.js中默认使用的模块化标准。默认情况下,Node.js会将以下内容视为CommonJS模块:

  1. 使用cjs为扩展名的文件,cjs表示的是一个CommonJS标准的模块。
  2. 当前package.json.js文件没有type属性,或者type属性为commonjs时。
  3. 文件的扩展名是mjs、cjs、json、node,js以外的值且type不是module时。

CommonJS模块化规范中,一个js文件就是一个模块,模块与模块之间是相互隔离的,模块中的内容默认是无法被外部查看的。

exports

我们可以通过exports来着设置向外部暴露的内容,exports是module对象的一个属性,并且属性值是一个空对象,访问它的方式有两种。

  • 直接访问exports
  • 通过module访问
        console.log(exports); // {}
        console.log(module.exports); // {}

我们可以将希望暴露给外部模块的内容设置为exports对象的属性。

        exports.a = 1;
        exports.b = 2;
        exports.obj = {
            name: '小松',
            age: 10
        };

也可以直接通过module.exports同时暴露多个属性。

        module.exports = {
            name: '小松',
            age: 10,
            printName(){
                console.log(this.name);
            }
        };

require

require("src")方法 用于引入模块,返回值就是exports对象,src是js模块的路径,如果是自定义模块需要以 ./ 或 ../ 开头。

        const m1 = require("./m1.js");

        console.log(m1); // { name: '小松', age: 10, printName: [Function: printName] }

        m1.printName(); // '小松'

如果需要引入模块中的某一个属性,可以使用.语法或者解构赋值。

  • .语法
        const name = require('./m1.js').name;
        console.log(name); // '小松'
  • 解构赋值
        const {name, age} = require('./m1.js');
        console.log(name); // '小松'
        console.log(age); // 10

如果需要引入核心模块(内置模块),则直接写模块名即可,也可以在核心模块前添加node字段(加快查询速度)。

        const path = require('path');

当使用一个文件夹作为模块时,文件夹中必须有一个模块的主文件,如果文件夹中含有package.json文件且设置了main属性,则main属性指定的文件会作为主文件,导入时就导入该文件。如果没有package.json,则node会按照index.jsindex.node的顺序寻找主文件,如果都没有,会报错。

值得注意的是require()方法 是同步加载模块的方法,所以无法用来加载ES6的模块,当我们需要在CommonJS中加载ES6模块时,需要通过import()方法加载。

CommonJS模块原理

所有的CommonJS的模块都会包装到一个函数中。

        (function(exports, require, module, __filename, __dirname){
            // 模块中的代码会被放到这里
        });

所以我们之所以能在CommonJS模块中使用exports和require并不是因为它们是全局变量,它们实际上是以参数的形式传递进模块的。

  • exports 设置模块向外暴露的内容
  • require 引入模块
  • module 当前模块的引用
  • __filename 当前模块的绝对路径
  • __dirname 当前模块所在的绝对路径

ES6模块化规范

2015年随着ES6标准的发布,ES的内置模块化系统也应运而生,并且在Node.js中同样也支持ES6标准的模块化。ES模块化在浏览器中同样支持使用,但通常情况下我们不会直接使用,而是结合打包工具编译后使用。

默认情况下,node中的模块化标准是CommonJS,如果想使用ES的模块化,可以采用以下两种方案:

  1. 使用mjs作为文件扩展名。
  2. 修改package.json文件,type属性值设为module,项目下所有的js文件会以ES模块化为标准。

export

在ES6中,通过export导出模块。

        export let a = 10;
        export let b = 20;
        export let obj = {
            name: '小松',
            age: 18
        }

export default设置默认导出,默认导出的内容必须是一个值,一个模块中只能有一个默认导出。

        export default function sum (a, b){
            return a + b;
        }

import

在ES6中,可以用import进行解构赋值导入模块,属性名要与指定模块中的属性名一一对应,通过ES模块化导入的内容都是常量,无法修改,并且运行在严格模式下。

        import {a, b, obj} from "./m1.mjs";

        console.log(a, b, obj); // 10 20 { name: '小松', age: 18 }

也可以通过as指定属性的别名。

        import {a as num1, b, obj} from "./m1.mjs";

        console.log(num1); // 10

通过*可以导入模块中的所有东西并且包裹在一个对象中,不过开发中尽量避免使用这种方式,占内存。

        import * as m1 from "./m1.mjs";

        console.log(m1); // { a: 10, b: 20, obj: { name: '小松', age: 18 } }

如果采用的是默认暴露export default,那么在导入时可以随意定义属性名。

        import add from "./m1.mjs"

        console.log(add(1, 2)); // 3

核心模块

核心模块是Node.js中的内置模块,这些模块有的可以直接在node中使用,有的需要引入后使用。这里会例举一些常用的核心模块,比如Process、Path,Fs。

global 是Node.js中的全局对象,作用类似于浏览器中的window,ES标准下全局对象的标准名是globalThis

Process

process模块 用于表示和控制当前的node进程,通过该对象可以获取进程的信息,对进程做各种操作。它是一个全局变量,可以直接使用。

process.exit(num)方法 结束当前进程,num参数表示状态码,通常不用。

        console.log(1);
        console.log(2);
        process.exit();
        console.log(3);

process.nextTick(callback[, ...args])方法 将函数插入到tick队列中,tick队列中的代码会在下一次事件循环之前执行,也就意味着会在微任务队列和宏任务队列之前执行。

Path

Path模块 用于获取各种路径,需要对其进行引入才能使用。

path.resolve([...paths])方法 用于生成一个绝对路径,不传参直接调用会返回当前的工作目录,通过不同的方式执行代码时,工作目录可能不同。

        const path = require("node:path");
        const result = path.resolve();

        console.log(result); // 'E:\Course\NodeJS'

一般会将一个绝对路径作为第一个参数,相对路径作为第二个参数,自动计算出最终路径。

  • __dirname属性 表示当前的工作目录。
        const path = require("node:path");
        const result = path.resolve(__dirname, "./hello.js");

        console.log(result); // 'E:\Course\NodeJS\hello.js'

Fs

fs(file system)模块 用于帮助node操作磁盘中的文件,文件的操作也就是所谓的I/O,input output。该模块也需要引入后才能使用。

fs.readFileSync(src)方法 同步的读取文件的方法,会阻塞后面代码的执行,src参数是路径。读取到的数据会以Buffer对象的形式返回,Buffer是一个临时存储数据的缓冲区。

        const fs = require("node:fs");
        const path = require("node:path");
        const content = fs.readFileSync(path.resolve(__dirname, "./hello.txt"));

        console.log(content); // <Buffer 68 65 6c 6c 6f 77 6f 72 6c 64 21>
  • 通过toString()方法获取需要的内容
        console.log(content.toString()); // HelloWorld!

fs.readFile(src, callback)方法 异步的读取文件的方法,src参数是路径,callback是一个回调函数。

  • err参数是出错提示信息
  • buffer是返回值
        const fs = require("node:fs");
        const path = require("node:path");
        fs.readFile(
            path.resolve(__dirname, "./hello.txt"),
            (err, buffer) => {
                if(err){
                    console.log('出错了');
                }else{
                    console.log(buffer.toString());
                }
            }
        );

Promise版本的fs方法

  • 通过then()方法的调用
        const fs = require("node:fs/promises");
        const path = require("node:path");
        fs.readFile(path.resolve(__dirname, "./hello.txt"))
        .then(buffer => {
            console.log(buffer.toString()); 
        })
        .catch(err => {
            console.log('出错了');
        });
  • async语法糖的调用
        const fs = require("node:fs/promises");
        const path = require("node:path");

        ;(async () => {
            try{
                const buffer = await fs.readFile(path.resolve(__dirname, "./hello.txt"));
                console.log(buffer.toString());
            }catch(e){
                console.log('出错了');
            }
        })()

其它的一些方法

  • fs.appendFile()方法 创建新文件或将数据添加到已有文件中
  • fa.mkdir()方法 删除目录
  • fs.rmdir()方法 删除目录
  • fs.rm()方法 删除文件
  • fs.rename()方法 重命名
  • fs.copyFile()方法 拷贝文件

包管理器

在开发中不可能所有代码都要手动去写,有时需要将一些现成的代码引入到项目中使用,就像jQuery这种外部代码我们称之为包。随着引入的包数量增多,包管理的问题也需要解决,比如下载、更新和删除等。包管理器就是帮助我们解决这个问题的工具。

Node中常用的包管理器叫做npm(node package manage),npm是世界上最大的包管理库。作为开发人员,我们可以将自己开发的包上传到npm中给别人使用,也可以从npm中下载别人开发的包。

npm有一些常用的命令

  • npm init 初始化项目,创建package.json文件(需要设置相关属性)。
  • npm init -y 初始化项目,创建package.json文件(所有属性值都采用默认值)。
  • npm install 自动安装所有依赖文件。
  • npm uninstall 卸载文件

package.json文件顾名思义,是一个用于描述包的json文件。它里面需要一个json格式的数据对象,在json文件中通过属性描述包的相关信息,比如包的名字、版本,依赖等,以下就是一个初始化的package.json文件。

        {
          "name": "test",
          "version": "1.0.0",
          "description": "",
          "main": "核心模块1.js",
          "scripts": {
            "test": "echo \"Error: no test specified\" && exit 1"
          },
          "author": "",
          "license": "ISC"
        }

package.json文件中有一些常用属性,其中name属性和version属性是必备的。

  • name 包的名称,可以包含小写字母,_和-。
  • version 包的版本,需要遵循xx.xx.xx的格式。
  • description 包的描述
  • main 入口文件
  • author 包的作者,格式:Your Name email@example.com
  • license 版权声明
  • repository 仓库地址(git/gitee)
  • dependencies 依赖文件

package.json文件中有个scripts属性,其中可以自定义一些命令,并且可以直接通过npm指令调用,比如test和start,其它命令需要通过npm run xxx执行。

        npm test
        npm start

package-lock.json文件是一个帮助加速npm下载的。

express

express是node中的服务器框架,通过express可以快速的搭建一个web服务器。

先引入express模块,然后创建它的实例对象。

        // 引入express模块
        const express = require('express');
        // 获取服务器对象实例对象app
        const app = express();

因为服务器需要监听计算机中的端口号,所以通过app.listen()开启监听端口。

        app.listen(3000, () => {
            console.log('服务器3000端口监听中......');
        })

路由

如果希望服务器可以正常访问,还需要为服务器设置路由,路由可以根据不同的请求方式和请求地址处理用户的请求。在express中,我们可以使用app.get()方法或者app.post()方法等设置路由。

设置路由时需要传递两个参数,第一个参数是路径,第二个参数是回调函数。

回调函数执行时,需要接收到三个参数。request表示用户的请求信息,response表示响应报文信息。在路由中主要做两件事,读取用户请求根据请求返回响应。

  • senStatus()方法 向客户端发送响应状态码。
  • status()方法 用于设置响应状态码,但是不发送。
  • send()方法 用于设置并发送响应体。
        app.get('/', (request, response) => {
            console.log('用户通过get访问了')
            console.log(request.url);
            // response.sendStatus(404);
            response.status(200);
            response.send('请求已发送,但是无法显示。')

        })

get请求发送参数的第二种方式,路径中以冒号命名的部分我们称为param,在get请求中可以被解析为请求参数。

以下代码可以使得当用户在地址栏访问localhost:xxx/hello/xxx时就触发,param可以为任意字符串。

        app.get('/hello/:id', (req, res) => {
            console.log(req.params); // { id: '1' }
            res.send('<h1>这是hello路由</h1>');
        })

中间件

在express中我们可以使用app.use()定义一个中间件,中间件和路由类似。主要是为了处理访问过程中的一些重复性操作,降低代码量,它和路由的区别在于:

  • 它不会检查请求类型。
  • 路径设置父级目录。

next()函数 是中间件的回调函数的第三个参数,用于触发后续的中间件。它不能在响应处理完毕后调用,会报错。

        app.use('/', (request, response, next) => {
            next();
        })

        app.use('/', (request, response) => {
            response.send('2');
        })

静态资源

服务器中的代码,对于外部来说都是不可见的,所以我们写的html页面,浏览器无法直接访问,如果希望浏览器可以访问,则需要将页面所在的目录设置为静态资源目录,服务器默认把静态资源目录作为根目录,所以可以直接通过localhost:xxxx访问,并且默认访问的就是index.html文件。

在express中我们通过中间件配合static方法配置静态目录路径。

        app.use(express.static(path.resolve(__dirname, 'public')));

模拟使用get请求登录,通过request的query属性获取字符串中的数据。

        app.get('/login', (req, res) => {
            let message = req.query;
            console.log(message); // { username: 'smt', password: '123' }
            if(message.username === 'smt' && message.password === '123'){
                res.send('登录成功!');
            }else{
                res.send('登录失败!');
            }
        })

模拟使用post请求登录,通过requset的body属性获取请求体相关属性。注意post请求中默认情况下express不会自动解析请求体,所以需要通过中间件为其增加共功能。

        app.use(express.urlencoded());
        app.post('/login', (req, res) => {
            console.log(req.body); // { username: 'smt', password: '123456' }
            const username = req.body.username;
            const password = req.body.password;

            if(username === 'smt' && password === '123456'){
                res.send('登录成功!');
            }else{
                res.send('登陆失败!');
            }
        })

模拟登录

        const users = [
            {
                username: 'admin',
                password: '123456',
                nickname: '超级管理员'
            },
            {
                username: 'smt',
                password: '123456',
                nickname: '小松'
            }
        ];
        app.post('/login', (req, res) => {
            const username = req.body.username;
            const password = req.body.password;

            // for(const user of users) {
            //     if(user.username === username){
            //         if(user.password === password){
            //             res.send(`<h1>${user.nickname}登录成功!</h1>`);
            //             return;
            //         }
            //     }
            // }

            const loginUser = users.find((item) => {
                return item.username === username && item.password === password;
            });

            if(loginUser){
                res.send(`<h1>${loginUser.nickname}登录成功!</h1>`);
            }else{
                res.send('登陆失败!');
            }
        });

模拟注册页面

        app.post('/regist', (req, res) => {
            // 获取用户数据
            const {username, password, repwd} = req.body;
            const user = users.find(item => {
                return item.username === username;
            });

            if(user){
                console.log('用户名重复,请重新输入');
                return;
            }else{
                users.push({
                    username,
                    password
                })
            }
            res.send('注册成功!');
        })
在线举报