跳转到内容
新建笔记

Node.js 无标题混合片段

Node.js 是一个开源、跨平台的JavaScript运行时环境,它允许开发者在服务器端运行JavaScript代码。由Ryan Dahl在2009年创建,Node.js 的设计目标是提供一种高效、轻量级的方式来构建可扩展的网络应用程序。

序号特点说明
1开源Node.js 是一个开源项目,任何人都可以自由使用和修改。
2跨平台支持在 Windows、Linux 和 macOS 等多个平台上运行。
3事件驱动采用事件驱动模型,适合处理大量并发连接。
4非阻塞I/O非阻塞I/O模型使得 Node.js 能够高效地处理 I/O 密集型任务。
5单线程JavaScript 代码在单个线程上运行,通过事件循环处理并发。
6V8引擎使用 Google 的 V8 引擎执行 JavaScript 代码,提供高性能。
7NPM拥有强大的包管理器 npm,方便安装和管理第三方库。
8适用于多种应用可用于构建 Web 服务器、命令行工具、桌面应用等。
9JavaScript Everywhere允许使用 JavaScript 编写前端和后端代码,简化开发流程。
10模块化鼓励使用模块化设计,提高代码的可重用性和可维护性。
11ES6及更新特性支持支持最新的 JavaScript 语言特性,包括 ES6 和更新的 ECMAScript 标准。

于是一个集中管理的工具应运而生:大家都把自己开发的模块打包后放到npm官网上,如果要使用,直接通过npm安装就可以直接用,不用管代码存在哪,应该从哪下载。

(1)使用模块

'use strict';
// 引入hello模块:
const greet = require('./hello'); // 要写相对目录!如果只写模块名,则Node会依次在内置模块、全局模块和当前模块下查找hello.js,很可能会得到一个错误
let s = 'Michael';
greet(s); // Hello, Michael!
// module.exports = variable;
module.exports = variable;
// 要引入其他模块输出的对象,用:
const foo = require('other_module');
// Node.js加载了hello.js后,它可以把代码包装一下。原来的全局变量s现在变成了匿名函数内部的局部变量。不影响同名其他模块变量
(function () {
// 读取的hello.js代码:
let s = 'Hello';
let name = 'world';
console.log(s + ' ' + name + '!');
// hello.js代码结束
})();
// 模块的输出module.exports实现
// 准备module对象:
let module = {
id: 'hello',
exports: {}
};
let load = function (module) {
// 读取的hello.js代码:
function greet(name) {
console.log('Hello, ' + name + '!');
}
module.exports = greet;
// hello.js代码结束
return module.exports;
};
let exported = load(module);
// 保存module:
save(module, exported);
// 可见,变量module是Node在加载js文件前准备的一个变量,并将其传入加载函数,我们在hello.js中可以直接使用变量module原因就在于它实际上是函数的一个参数:
module.exports = greet;
// 通过把参数module传递给load()函数,hello.js就顺利地把一个变量传递给了Node执行环境,Node会把module变量保存到某个地方。
// 由于Node保存了所有导入的module,当我们用require()获取module时,Node找到对应的module,把这个module的exports变量返回,这样,另一个模块就顺利拿到了模块的输出:
const greet = require('./hello');
// 在Node环境中,有两种方法可以在一个模块中输出变量:
// 对module.exports赋值
// hello.js
function hello() {
console.log('Hello, world!');
}
function greet(name) {
console.log('Hello, ' + name + '!');
}
module.exports = {
hello: hello,
greet: greet
};
// 直接使用exports
// hello.js
function hello() {
console.log('Hello, world!');
}
function greet(name) {
console.log('Hello, ' + name + '!');
}
function hello() {
console.log('Hello, world!');
}
exports.hello = hello;
exports.greet = greet;
// 分析Node的加载机制
// 首先,Node会把整个待加载的hello.js文件放入一个包装函数load中执行。在执行这个load()函数前,Node准备好了module变量:
let module = {
id: 'hello',
exports: {}
};
// load()函数最终返回module.exports:
let load = function (exports, module) {
// hello.js的文件内容
...
// load函数返回:
return module.exports;
};
let exported = load(module.exports, module);
// 也就是说,默认情况下,Node准备的exports变量和module.exports变量实际上是同一个变量,并且初始化为空对象{},于是,我们可以写:
exports.foo = function () { return 'foo'; };
exports.bar = function () { return 'bar'; };
// huo
module.exports.foo = function () { return 'foo'; };
module.exports.bar = function () { return 'bar'; };
// 如果我们要输出的是一个函数或数组,那么,只能给module.exports赋值:
module.exports = function () { return 'foo'; };
// 可以得出结论:直接对module.exports赋值,可以应对任何情况:
module.exports = {
foo: function () { return 'foo'; }
};
// huo
module.exports = function () { return 'foo'; };

(2)ESM

随着ES 6标准的推出,JavaScript语言本身终于也迎来了原生内置的模块支持,称为ECMAScript Modules(简称ESM),不仅可以直接在浏览器中使用模块,也可以在Node.js中使用ESM模块。

// 不使用ESM模块时,我们用module.exports导出可供外部使用的JS对象,例如,以下模块导出了两个函数
'use strict';
let s = 'Hello';
function out(prompt, name) {
console.log(`${prompt}, ${name}!`);
}
function greet(name) {
out(s, name);
}
function hi(name) {
out('Hi', name);
}
module.exports = {
greet: greet,
hi: hi
};
// 要把上述代码改为ESM模块,我们用export标识需要导出的函数:
// 并将其保存为hello.mjs文件,注意扩展名不是.js,而是.mjs。
let s = 'Hello';
// out是模块内部函数,模块外部不可见:
function out(prompt, name) {
console.log(`${prompt}, ${name}!`);
}
// greet是导出函数,可被外部调用:
export function greet(name) {
out(s, name);
}
// hi是导出函数,可被外部调用:
export function hi(name) {
out('Hi', name);
}
// 可以再编写一个main.mjs文件来调用hello模块:
// 可见,ESM模块用export关键字导出一个JS对象,用import关键字导入一个模块的导出对象。
import { greet, hi } from './hello.mjs';
let name = 'Bob';
greet(name);
hi(name);
// 单个函数导出,ESM模块默认启用严格模式
module.exports = greet;
export default function greet(name) {
...
}
// 相应的,导入代码修改为:
import greet from './hello.mjs';
// 浏览器来说,也可以直接使用ESM模块
<html>
<head>
<script type="module" src="./example.js"></script>
<script type="module">
greet('Bob');
</script>
</head>
...
</html>
// 或者
<html>
<head>
<script type="module">
import { greet } from './example.js';
greet('Bob');
</script>
</head>
...
</html>

(3)基本模块

// 导入模块有两种方法
const { randomInt } = require('node:crypto');
const n = randomInt(0, 100); // 0~100之间的随机数
console.log(n);
// 使用import关键字导入ESM模块
import { randomInt } from 'node:crypto';
const n = randomInt(0, 100); // 0~100之间的随机数
console.log(n);
模块名称功能描述使用场景代码示例
globalNode.js中的全局对象,不同于浏览器中的window对象访问全局变量和功能,如global.processconsole.log(global.process);
process表示当前Node.js进程的对象,包含进程信息和控制控制进程行为,如退出代码、工作目录等console.log(process.cwd());
fs文件系统模块,用于读写文件文件操作,包括异步和同步方法fs.readFile('file.txt', 'utf-8', (err, data) => { console.log(data); });
stream支持“流”的数据结构,用于处理连续的数据数据流的读取和写入,如文件流、网络流等let rs = fs.createReadStream('file.txt'); rs.on('data', (chunk) => { console.log(chunk); });
http提供HTTP服务器和客户端功能的模块创建Web服务器,处理HTTP请求和响应const server = http.createServer((req, res) => { res.end('Hello World'); }); server.listen(8080);
crypto提供加密和哈希算法的模块数据加密、哈希计算等安全功能const hash = crypto.createHash('md5'); hash.update('some data'); console.log(hash.digest('hex'));

在前面的JavaScript课程中,我们已经知道,JavaScript有且仅有一个全局对象,在浏览器中,叫window对象。而在Node.js环境中,也有唯一的全局对象,但不叫window,而叫global,这个对象的属性和方法也和浏览器环境的window不同。进入Node.js交互环境,可以直接输入:

> global.console
Object [console] {
log: [Function: log],
warn: [Function: warn],
dir: [Function: dir],
time: [Function: time],
...
}

process也是Node.js提供的一个对象,它代表当前Node.js进程。通过process对象可以拿到许多有用信息:

> process === global.process;
true
> process.version;
'v22.3.0'
> process.platform;
'darwin'
> process.arch;
'x64'
> process.cwd(); //返回当前工作目录
'/Users/michael'
> process.chdir('/private/tmp'); // 切换当前工作目录
undefined
> process.cwd();
'/private/tmp'

1)fs

因为JavaScript的单线程模型,执行IO操作时,JavaScript代码无需等待,而是传入回调函数后,继续执行后续JavaScript代码。比如jQuery提供的getJSON()操作:

$.getJSON('http://example.com/ajax', function (data) {
console.log('IO结果返回后执行...');
});
console.log('不等待IO结果直接执行后续代码...');
// 同步的IO操作则需要等待函数返回
// 根据网络耗时,函数将执行几十毫秒~几秒不等:
let data = getJSONSync('http://example.com/ajax');
// 异步读文件
// read-text-file-async.mjs
import { readFile } from 'node:fs';
console.log('BEGIN');
readFile('sample.txt', 'utf-8', function (err, data) {
if (err) {
console.log(err);
} else {
console.log(data);
}
});
console.log('END');
// 当读取二进制文件时,不传入文件编码时,回调函数的data参数将返回一个Buffer对象。在Node.js中,Buffer对象就是一个包含零个或任意个字节的数组(注意和Array不同)。
// Buffer -> String
let text = data.toString('utf-8');
console.log(text);
// 或者把一个String转换成Buffer:
// String -> Buffer
let buf = Buffer.from(text, 'utf-8');
console.log(buf);
// 同步读文件
// read-text-file-sync.mjs
import { readFileSync } from 'node:fs';
console.log('BEGIN');
try {
let s = readFileSync('sample.txt', 'utf-8');
console.log(s);
} catch (err) {
console.log(err);
}
console.log('END');
// 写文件
// write-file-async.mjs
import { writeFile } from 'node:fs';
let data = 'Hello, Node.js';
writeFile('output.txt', data, function (err) {
if (err) {
console.log(err);
}
});
// 如果我们要获取文件大小,创建时间等信息,可以使用fs.stat(),它返回一个Stat对象,能告诉我们文件或目录的详细信息:
// read-stat-async.mjs
import { stat } from 'node:fs';
stat('sample.png', function (err, st) {
if (err) {
console.log(err);
} else {
// 是否是文件:
console.log('isFile: ' + st.isFile());
// 是否是目录:
console.log('isDirectory: ' + st.isDirectory());
if (st.isFile()) {
// 文件大小:
console.log('size: ' + st.size);
// 创建时间, Date对象:
console.log('birth time: ' + st.birthtime);
// 修改时间, Date对象:
console.log('modified time: ' + st.mtime);
}
}
});
// Node还提供Promise版本的fs,可以用如下代码在async函数中读取文件:
// async-read.mjs
import { readFile } from 'node:fs/promises';
async function readTextFile(path) {
return await readFile(path, 'utf-8');
}
readTextFile('sample.txt').then(s => console.log(s));

2)stream

stream是Node.js提供的又一个仅在服务区端可用的模块,目的是支持“流”这种数据结构。

什么是流?流是一种抽象的数据结构。想象水流,当在水管中流动时,就可以从某个地方(例如自来水厂)源源不断地到达另一个地方(比如你家的洗手池)。我们也可以把数据看成是数据流,比如你敲键盘的时候,就可以把每个字符依次连起来,看成字符流。这个流是从键盘输入到应用程序,实际上它还对应着一个名字:标准输入流(stdin)。流的特点是数据是有序的,而且必须依次读取,或者依次写入,不能像Array那样随机定位。

在Node.js中,流也是一个对象,我们只需要响应流的事件就可以了:data事件表示流的数据已经可以读取了,end事件表示这个流已经到末尾了,没有数据可以读取了,error事件表示出错了。

import { createReadStream } from 'node:fs';
// 打开流:
let rs = createReadStream('sample.txt', 'utf-8');
// 读取到数据:
rs.on('data', (chunk) => { // data事件可能会有多次,每次传递的chunk是流的一部分数据。
console.log('---- chunk ----');
console.log(chunk);
});
// 结束读取:
rs.on('end', () => {
console.log('---- end ----');
});
// 出错:
rs.on('error', err => {
console.log(err);
});
// 要以流的形式写入文件,只需要不断调用write()方法,最后以end()结束:
import { createWriteStream } from 'node:fs';
let ws = createWriteStream('output.txt', 'utf-8');
ws.write('使用Stream写入文本数据...\n');
ws.write('继续写入...\n');
ws.write('DONE.\n');
ws.end(); // 结束写入
// 写入二进制数据:
let b64array = [ ... ];
let ws2 = createWriteStream('output.png');
for (let b64 of b64array) {
let buf = Buffer.from(b64, 'base64');
ws2.write(buf); // 写入Buffer对象
}
ws2.end(); // 结束写入

就像可以把两个水管串成一个更长的水管一样,两个流也可以串起来。一个Readable流和一个Writable流串起来后,所有的数据自动从Readable流进入Writable流,这种操作叫pipe。在Node.js中,Readable流有一个pipe()方法,就是用来干这件事的:

rs.pipe(ws);
// Node还提供了pipeline功能,它可以将一个流输出到另一个流。以下是一个复制文件的程序:
import { createReadStream, createWriteStream } from "node:fs";
import { pipeline } from 'node:stream/promises';
async function copy(src, dest) {
let rs = createReadStream(src);
let ws = createWriteStream(dest);
await pipeline(rs, ws);
}
copy('sample.txt', 'output.txt')
.then(() => console.log('copied.'))
.catch(err => console.log(err));

3)http

因为JavaScript实际上已经统治了浏览器端的脚本,其优势就是有世界上数量最多的前端开发人员。如果已经掌握了JavaScript前端开发,再学习一下如何将JavaScript应用在后端开发,就是名副其实的全栈了。

应用程序并不直接和HTTP协议打交道,而是操作http模块提供的request和response对象。

request对象封装了HTTP请求,我们调用request对象的属性和方法就可以拿到所有HTTP请求的信息;

response对象封装了HTTP响应,我们操作response对象的方法,就可以把HTTP响应返回给浏览器。

// 导入http模块:
import http from 'node:http';
// 创建http server,并传入回调函数:
const server = http.createServer((request, response) => {
// 回调函数接收request和response对象,
// 获得HTTP请求的method和url:
console.log(request.method + ': ' + request.url);
// 将HTTP响应200写入response, 同时设置Content-Type: text/html:
response.writeHead(200, {'Content-Type': 'text/html'});
// 将HTTP响应的HTML内容写入response:
response.end('<h1>Hello world!</h1>');
});
// 出错时返回400:
server.on('clientError', (err, socket) => {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
// 让服务器监听8080端口:
server.listen(8080);
console.log('Server is running at http://127.0.0.1:8080/');
// cmd
$ node simple-server.mjs
Server is running at http://127.0.0.1:8080/
// 文件服务器,设定一个目录,然后让Web程序变成一个文件服务器
let url = new URL('http://localost' + '/index.html?v=1');
let pathname = url.pathname;
console.log(pathname); // index.html
import path from 'node:path';
// 解析当前目录:
let workDir = path.resolve('.'); // '/Users/michael'
// 组合完整的文件路径:当前目录+'pub'+'index.html':
let filePath = path.join(workDir, 'pub', 'index.html');
// '/Users/michael/pub/index.html'
// 实现一个文件服务器
// 导入http模块:
import http from 'node:http';
import path from 'node:path';
import { createReadStream } from 'node:fs';
import { stat } from 'node:fs/promises';
// 设定www根目录为当前目录:
const wwwRoot = path.resolve('.');
console.log(`set www root: ${wwwRoot}`);
// 根据扩展名确定MIME类型:
function guessMime(pathname) {
// FIXME:
return 'text/html';
}
// 创建http file server,并传入回调函数:
const server = http.createServer((request, response) => {
// 获得HTTP请求的method和url:
console.log(request.method + ': ' + request.url);
if (request.method !== 'GET') {
response.writeHead(400, { 'Content-Type': 'text/html' });
response.end('<h1>400 Bad Request</h1>');
} else {
// 解析path:
let url = new URL(`http://localhost${request.url}`);
let pathname = url.pathname;
let filepath = path.join(wwwRoot, pathname);
// TODO: 必要的安全检查
// 检查文件状态:
stat(filepath).then(st => {
if (st.isFile()) {
console.log('200 OK');
// 发送200响应:
response.writeHead(200, { 'Content-Type': guessMime(pathname) });
// 将文件流导向response:
createReadStream(filepath).pipe(response);
} else {
console.log('404 Not Found');
response.writeHead(404, { 'Content-Type': 'text/html' });
response.end('<h1>404 Not Found</h1>');
}
}).catch(err => {
console.log('404 Not Found');
response.writeHead(404, { 'Content-Type': 'text/html' });
response.end('<h1>404 Not Found</h1>');
});
}
});
// 出错时返回400:
server.on('clientError', (err, socket) => {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
// 让服务器监听8080端口:
server.listen(8080);
console.log('Server is running at http://127.0.0.1:8080/');

4)cryto

crypto模块的目的是为了提供通用的加密和哈希算法。用纯JavaScript代码实现这些功能不是不可能,但速度会非常慢。Nodejs用C/C++实现这些算法后,通过cypto这个模块暴露为JavaScript接口,这样用起来方便,运行速度也快。

//MD5是一种常用的哈希算法,用于给任意数据一个“签名”。这个签名通常用一个十六进制的字符串表示:
import crypto from 'node:crypto';
const hash = crypto.createHash('md5');
// 可任意多次调用update():
hash.update('Hello, world!');
hash.update('Hello, nodejs!');
console.log(hash.digest('hex')); // 7e1977739c748beac0c0fd14fd26a544
// 如果要计算SHA1,只需要把'md5'改成'sha1',就可以得到SHA1的结果。还可以使用更安全的sha256和sha512
// Hmac算法也是一种哈希算法,它可以利用MD5或SHA1等哈希算法。不同的是,Hmac还需要一个密钥:import crypto from 'node:crypto';
const hmac = crypto.createHmac('sha256', 'secret-key');
hmac.update('Hello, world!');
hmac.update('Hello, nodejs!');
console.log(hmac.digest('hex')); // 80f7e22570...
// AES是一种常用的对称加密算法,加解密都用同一个密钥。crypto模块提供了AES支持,但是需要自己封装好函数,便于使用:
import crypto from 'node:crypto';
function aes_encrypt(key, iv, msg) {
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
// input encoding: utf8
// output encoding: hex
let encrypted = cipher.update(msg, 'utf8', 'hex');
encrypted += cipher.final('hex');
return encrypted;
}
function aes_decrypt(key, iv, encrypted) {
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// key的长度必须为32bytes:
let key = 'Passw0rdPassw0rdPassw0rdPassw0rd';
// iv的长度必须为16bytes:
let iv = 'a1b2c3d4e5f6g7h8';
let msg = 'Hello, world!';
// 加密:
let encrypted_msg = aes_encrypt(key, iv, msg);
// 解密:
let decrypted_msg = aes_decrypt(key, iv, encrypted_msg);
console.log(`AES encrypt: ${encrypted_msg}`);
console.log(`AES decrypt: ${decrypted_msg}`);
// Diffie-Hellman,DH算法是一种密钥交换协议,它可以让双方在不泄漏密钥的情况下协商出一个密钥来。DH算法基于数学原理
// RSA算法是一种非对称加密算法,即由一个私钥和一个公钥构成的密钥对,通过私钥加密,公钥解密,或者通过公钥加密,私钥解密。其中,公钥可以公开,私钥必须保密。
// 使用crypto模块提供的方法,即可实现非对称加解密。
import fs from 'node:fs';
import crypto from 'node:crypto';
// 从文件加载key:
function loadKey(file) {
// key实际上就是PEM编码的字符串:
return fs.readFileSync(file, 'utf8');
}
let
prvKey = loadKey('./rsa-prv.pem'),
pubKey = loadKey('./rsa-pub.pem'),
message = 'Hello, world!';
// 使用私钥加密:
let enc_by_prv = crypto.privateEncrypt(prvKey, Buffer.from(message, 'utf8'));
console.log(enc_by_prv.toString('hex'));
let dec_by_pub = crypto.publicDecrypt(pubKey, enc_by_prv);
console.log(dec_by_pub.toString('utf8'));
// 使用公钥加密:
let enc_by_pub = crypto.publicEncrypt(pubKey, Buffer.from(message, 'utf8'));
console.log(enc_by_pub.toString('hex'));
// 使用私钥解密:
let dec_by_prv = crypto.privateDecrypt(prvKey, enc_by_pub);
console.log(dec_by_prv.toString('utf8'));

**(4)Web开发 **

1)koa

koa是Express的下一代基于Node.js的web框架,目前有1.x和2.0两个版本。

image-20240811051809641

package.json包含了npm的依赖信息,以及项目描述等信息,package-lock.json是针对当前所有依赖的一个快照,目的是锁定各个依赖项的版本号。

package.json
{
"name": "hello-koa",
"version": "1.0",
"description": "Hello koa webapp.",
"type": "module",
"dependencies": {
"koa": "^2.15.3"
}
}

node_modules是所有依赖项安装的地方,可以随时删除node_modules目录,然后用npm install重新安装。

直接运行npm install则根据package.json的dependencies信息下载安装依赖项,运行npm install xyz则将xyz添加到dependencies然后再安装xyz及其依赖项。因此,熟练的开发者可以先自己编辑dependencies内容,然后删除node_modules后重新安装所有依赖项。此外,package.json和package-lock.json应当添加至版本控制系统中,而node_modules则无需添加。

// 创建app.mjs文件
// 导入koa,注意导入的是大写开头的class:
import Koa from 'koa';
// 创建一个koa实例表示webapp本身:
const app = new Koa();
// 对于任何请求,app将调用该异步函数处理请求:
app.use(async (ctx, next) => {
await next();
// 设置响应类型和文本:
ctx.response.type = 'text/html';
ctx.response.body = '<h1>Hello Koa!</h1>';
});
// 在端口3000监听:
app.listen(3000);
console.log('app started at port 3000...');
// koa就会调用通过app.use()注册的async函数,并传入ctx和next参数。用await next()来调用下一个async函数。我们把每个async函数称为middleware,这些middleware可以组合起来,完成很多有用的功能。可以用以下3个middleware组成处理链,依次打印日志,记录处理时间,输出HTML
app.use(async (ctx, next) => {
console.log(`${ctx.request.method} ${ctx.request.url}`); // 打印URL
await next(); // 调用下一个middleware
});
app.use(async (ctx, next) => {
const start = Date.now(); // 当前时间
await next(); // 调用下一个middleware
const ms = Date.now() - start; // 耗费时间
console.log(`Time: ${ms}ms`); // 打印耗费时间
});
app.use(async (ctx, next) => {
await next();
ctx.response.type = 'text/html';
ctx.response.body = '<h1>Hello Koa!</h1>';
});
app.use(async (ctx, next) => {
if (await checkUserPermission(ctx)) {
await next();
} else {
ctx.response.status = 403;
}
});

处理url

import Koa from 'koa';
import Router from '@koa/router';
const app = new Koa();
const router = new Router();
// log url:
app.use(async (ctx, next) => {
console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
await next();
});
// 解析request.body:
app.use(bodyParser());
// 设置路由: 处理 /
router.get('/', async (ctx, next) => {
ctx.response.type = 'text/html';
ctx.response.body = '<h1>Index Page</h1>';
});
// 设置路由: 处理 /hello/:name
router.get('/hello/:name', async (ctx, next) => {
// 获取:name参数:
let s = ctx.params.name;
ctx.response.type = 'text/html';
ctx.response.body = `<h1>Hello, ${s}</h1>`;
});
// 使用router:
app.use(router.routes());
app.listen(3000);
console.log('app started at port 3000...');
// 处理post请求,写一个简单的登录表单
router.get('/', async (ctx, next) => {
ctx.response.type = 'text/html';
ctx.response.body = `
<h1>Index Page</h1>
<form action="/signin" method="post">
<p>Name: <input name="name" value="koa"></p>
<p>Password: <input name="password" type="password"></p>
<p><button type="submit">Submit</button></p>
</form>
`;
});
router.post('/signin', async (ctx, next) => {
let name = ctx.request.body.name || '';
let password = ctx.request.body.password || '';
console.log(`try signin: ${name}, password: ${password}`);
if (name === 'koa' && password === '12345') {
ctx.response.type = 'text/html';
ctx.response.body = `<h1>Welcome, ${name}!</h1>`;
} else {
ctx.response.type = 'text/html';
ctx.response.body = '<h1>Signin failed!</h1><p><a href="/">Retry</a></p>';
}
});

image-20240811053448823

signin.mjs
// 重构,如果能把URL处理函数集中到某个js文件,或者某几个js文件中就好了,然后让app.mjs自动导入所有处理URL的函数。
// GET /
async function index(ctx, next) {
ctx.response.type = 'text/html';
ctx.response.body = `
<h1>Index Page</h1>
<form action="/signin" method="post">
<p>Name: <input name="name" value="koa"></p>
<p>Password: <input name="password" type="password"></p>
<p><button type="submit">Submit</button></p>
</form>
`;
}
// POST /signin
async function signin(ctx, next) {
let name = ctx.request.body.name || '';
let password = ctx.request.body.password || '';
console.log(`try signin: ${name}, password: ${password}`);
if (name === 'koa' && password === '12345') {
ctx.response.type = 'text/html';
ctx.response.body = `<h1>Welcome, ${name}!</h1>`;
} else {
ctx.response.type = 'text/html';
ctx.response.body = '<h1>Signin failed!</h1><p><a href="/">Retry</a></p>';
}
}
// 导出处理函数:
export default {
'GET /': index,
'POST /signin': signin
};
// hello.mjs
async function hello(ctx, next) {
// 获取:name参数:
let s = ctx.params.name;
ctx.response.type = 'text/html';
ctx.response.body = `<h1>Hello, ${s}</h1>`;
}
export default {
'GET /hello/:name': hello
}
// app.mjs
// 扫描controller目录:
const dirname = path.dirname(fileURLToPath(import.meta.url));
console.log(`scan dir ${dirname}...`);
// 列举 *.mjs 文件:
let files = readdirSync(path.join(dirname, 'controller')).filter(f => f.endsWith('.mjs'));
for (let file of files) {
// 导入模块:
console.log(`import controller/${file}...`);
let { default: mapping } = await import(`./controller/${file}`);
// 把每个URL映射添加到router:
for (let url in mapping) {
if (url.startsWith('GET ')) {
// 类似 'GET /hello/:name'
let p = url.substring(4);
router.get(p, mapping[url]);
console.log(`mapping: GET ${p}`);
} else if (url.startsWith('POST ')) {
// 类似 'POST /signin'
let p = url.substring(5);
router.post(p, mapping[url]);
console.log(`mapping: POST ${p}`);
} else {
console.warn(`invalid mapping: ${url}`);
}
}
}
// controller.mjs
// controller.mjs:
async function scan(router, controllerDir) {
// 扫描controller目录:
const dirname = path.dirname(fileURLToPath(import.meta.url));
console.log(`scan dir ${dirname}...`);
let files = readdirSync(path.join(dirname, controllerDir)).filter(f => f.endsWith('.mjs'));
for (let file of files) {
// 导入模块:
console.log(`import controller/${file}...`);
let { default: mapping } = await import(`./${controllerDir}/${file}`);
// 把每个URL映射添加到router:
for (let url in mapping) {
if (url.startsWith('GET ')) {
let p = url.substring(4);
router.get(p, mapping[url]);
console.log(`mapping: GET ${p}`);
} else if (url.startsWith('POST ')) {
let p = url.substring(5);
router.post(p, mapping[url]);
console.log(`mapping: POST ${p}`);
} else {
console.warn(`invalid mapping: ${url}`);
}
}
}
}
// 默认扫描目录为 controller:
export default async function (controllerDir = 'controller') {
const router = new Router();
await scan(router, controllerDir);
return router.routes();
}
// app.mjs
import controller from './controller.mjs';
...
// 使用controller(), 注意controller模块导出的是async函数,要通过await调用:
app.use(await controller());
...

unjuncks

模板引擎就是基于模板配合数据构造出字符串输出的一个组件。

function examResult (data) {
return `${data.name}同学一年级期末考试语文${data.chinese}分,数学${data.math}分,位于年级第${data.ranking}名。`
}
examResult({
name: '小明',
chinese: 78,
math: 87,
ranking: 999
});
// 模板引擎把模板字符串里面对应的变量替换以后,就可以得到以下输出
小明同学一年级期末考试语文78分,数学87分,位于年级第999名。

MVC

我们已经可以用koa处理不同的URL,还可以用Nunjucks渲染模板。现在,是时候把这两者结合起来了!当用户通过浏览器请求一个URL时,koa将调用某个异步函数处理该URL。在这个异步函数内部,我们用一行代码:

ctx.render('home.html', { name: 'Michael' });

通过Nunjucks把数据用指定的模板渲染成HTML,然后输出给浏览器,用户就可以看到渲染后的页面了:

image-20240811054525958

这就是传说中的MVC:Model-View-Controller,中文名“模型-视图-控制器”。异步函数是C:Controller,Controller负责业务逻辑,比如检查用户名是否存在,取出用户信息等等;包含变量{{ name }}的模板就是V:View,View负责显示逻辑,通过简单地替换一些变量,View最终输出的就是用户看到的HTML。MVC中的Model在哪?Model是用来传给View的,这样View在替换变量的时候,就可以从Model中取出相应的数据。

reset

如果我们想要获取某个电商网站的某个商品,输入http://localhost:3000/products/123,就可以看到id为123的商品页面,但这个结果是HTML页面,它同时混合包含了Product的数据和Product的展示两个部分。对于用户来说,阅读起来没有问题,但是,如果机器读取,就很难从HTML中解析出Product的数据。

如果一个URL返回的不是HTML,而是机器能直接解析的数据,这个URL就可以看成是一个Web API。比如,读取http://localhost:3000/api/products/123,如果能直接返回Product的数据,那么机器就可以直接读取。

REST就是一种设计API的模式。最常用的数据格式是JSON。由于JSON能直接被JavaScript读取,所以,以JSON格式编写的REST风格的API具有简单、易读、易用的特点。

2)数据库

  • MySQL,大家都在用,一般错不了;
  • PostgreSQL,学术气息有点重,其实挺不错,但知名度没有MySQL高;
  • SQLite,嵌入式数据库,适合桌面和移动应用。