Node.js
Node.js是运行在服务器端的JS,用来编写服务器。它是单线程的、异步且非阻塞的,统一了API。
CommonJS模块化规范
早期网页中,没有实质的模块规范,最原始的通过script标签引入多个js文件。比如早期的jquery库也是一个模块,但是使用它时,只能全部引入,无法按需引入。并且在复杂的模块场景下容易出错,比如顺序的问题。
直到2015年,JavaScript都没有一个内置的模块化系统。但是民间大神开始着手自定义模块化系统,CommonJS
就是其中的佼佼者,同时它也是Node.js
中默认使用的模块化标准。默认情况下,Node.js会将以下内容视为CommonJS模块:
- 使用
cjs
为扩展名的文件,cjs表示的是一个CommonJS标准的模块。 - 当前
package.json.js
文件没有type
属性,或者type
属性为commonjs
时。 - 文件的扩展名是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.js
,index.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的模块化,可以采用以下两种方案:
- 使用mjs作为文件扩展名。
- 修改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.comlicense
版权声明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('注册成功!');
})