Node.js知识梳理(一)——基础

以下内容来自对《从前端到全栈》一书的学习记录~

学习的时候用的是V16.20.018+之后的语法差别还是有的~

什么是Node.js

Node.js 是运行在操作系统中的 JavaScript 运行时环境,和浏览器的运行环境不太一样:

  • 不具备浏览器提供的DOM API,比如 Window 对象、Location 对象、Document 对象、HTMLElement 对象、Cookie 对象等;
  • 提供了特有的API,比如全局的 global 对象,也提供了当前进程信息的 Process 对象,操作文件的 fs 模块,以及创建 Web 服务的 http 模块。

常见的内置模块

通过这些内置模块,可以执行操作系统指令、读写文件、建立网络连接、调用操作系统中的其他服务等:

  • File System 模块:这是操作系统的目录和文件的模块,提供文件和目录的读、写、创建、删除、权限设置等等。
  • Net 模块:提供网络套接字 socket,用来创建 TCP 连接,TCP 连接可以用来访问后台数据库和其他持久化服务;
  • HTTP 模块:提供创建 HTTP 连接的能力,可以用来创建 Web 服务,也是 Node.js 在前端最常用的核心模块;
  • URL 模块:用来处理客户端请求的 URL 信息的辅助模块,可以解析 URL 字符串;
  • Path 模块:用来处理文件路径信息的辅助模块,可以解析文件路径的字符串;
  • Process 模块:用来获取进程信息;
  • Buffer 模块:用来处理二进制数据;
  • Console 模块:控制台模块,同浏览器的Console模块,用来输出信息到控制台;
  • Crypto 加密解密模块:用来处理需要用户授权的服务;
  • Events 模块:用来监听和派发用户事件。
// show_builtins.js
const {builtinModules} = require('module');
console.log(builtinModules);

# 查看所有内置模块明的信息
node show_builtins.js

模块化管理

模块化并不陌生,常见的模块化分类有ES ModuleCommonJSUMD等。在13.20.0+Node.js中,支持ES ModuleCommonJS写法。复习一下语法:

ES Module

ES Module

由于Node.js目前默认用CommonJS规范定义JS文件,所以我们的文件类型是:.mjs

// module.mjs
export const say = (text) => {
  console.log('它说', text);
}

// index.mjs
import { say } from './module.mjs'

const argv = process.argv;
say(argv[2] || 'Hello World');

# 它说 Hello World
node index.mjs

# 它说 Hi,Armouy
node index.mjs 'Hi,Armouy'

如果想用ES Module定义.js文件,修改package.json

"type": "module",

CommonJS

// module.js
const say = (text) => {
  console.log('它说', text);
}

module.exports = say;

const say = require('./module')

const argv = process.argv;
say(argv[2] || 'Hello World');

你之前可能还看见过用exports导出模块API的例子,它不能与module.exports混用,会被module.exports覆盖。exports属于早期的写法,现在推荐的都是module.exports

虽然ES Module可以兼容CommonJS,但是怕混淆,还是当作不知道这件事吧一个是我感觉不够规范,另一个是记这个没啥用

ES ModuleCommonJS的区别

  • 别名设置:
// ES Module
export {
  say as sayHello
}

// CommonJS
module.exports = {
  say: sayHello
}
  • 是否可以省略.js扩展名:
// ES Module 不可以
import { say } from './module.mjs'
import { say } from './module.js'


// CommonJS 可以
const say = require('./module')

  • ES Modulesimportexport都只能写在最外层,不能放在块级作用域或函数作用域中:
// ES Module 以下写法会报错
if(condition) {
  import {a} from './foo';
} else {
  import {a} from './bar';
}

// CommonJS 以下写法是正确的
let api;
if(condition) {
  api = require('./foo');
} else {
  api = require('./bar');
}

  • CommonJS require中支持动态拼接,ES Modules不允许:
const libPath = ENV.supportES6 ? './es6/' : './';
const myLib = require(`${libPath}mylib.js`);
  • ES Modules import支持动态加载:
(async function() {
  const {ziyue} = await import('./ziyue.mjs');
  
  const argv = process.argv;
  console.log(ziyue(argv[2] || '巧言令色,鮮矣仁!'));
}());

一般来说,在写 Node.js 模块的时候,我们更多采用静态的方式引入模块。但是动态加载模块在一些复杂的库中比较有用,尤其是跨平台开发中,我们可能需要针对不同平台的环境加载不同的模块,这个时候采用动态加载就是必须的了。

NPM

Node.js 提供了一个包管理工具,能够让我们以包的形式发布某个或某些模块到网络上的共享仓库中。关于这部分的内容不细说,主要记住npm run的原理~

我们通常会在package.json中的scripts配置命令,然后使用npm run xxx来执行。能成功是因为 NPM 在安装模块的时候,不仅将模块自身安装到node_modules目录下,还会在node_modules目录下创建一个.bin的子目录,将模块包中的命令行脚本安装到.bin目录下,并在 NPM Script 执行时设置系统的环境变量 PATH 包含node_modules/.bin目录,这样就能够正常执行脚本了~

npm run eslint
# 相当于
node ./node_modules/.bin/eslint ziyue.js

内置模块的学习

这里主要通过的一个文章生成器的项目来学习~

mkdir node-demo
cd node-demo
npm init -y

修改package.json:

type": "module",

新建corpus/data.json

{
  "title": [
    "一天掉多少根头发",
    "中午吃什么",
    "学生会退会",
    "好好学习",
    "生活的意义",
    "科学和人文谁更有意义",
    "熬夜一时爽"
  ],
  "famous":[
    "爱迪生{{said}},天才是百分之一的勤奋加百分之九十九的汗水。{{conclude}}",
    "查尔斯·史{{said}},一个人几乎可以在任何他怀有无限热忱的事情上成功。{{conclude}}",
    "培根说过,深窥自己的心,而后发觉一切的奇迹在你自己。{{conclude}}",
    "歌德曾经{{said}},流水在碰到底处时才会释放活力。{{conclude}}",
    "莎士比亚{{said}},那脑袋里的智慧,就像打火石里的火花一样,不去打它是不肯出来的。{{conclude}}",
    "戴尔·卡耐基{{said}},多数人都拥有自己不了解的能力和机会,都有可能做到未曾梦想的事情。{{conclude}}",
    "白哲特{{said}},坚强的信念能赢得强者的心,并使他们变得更坚强。{{conclude}}",
    "伏尔泰{{said}},不经巨大的困难,不会有伟大的事业。{{conclude}}",
    "富勒曾经{{said}},苦难磨炼一些人,也毁灭另一些人。{{conclude}}",
    "文森特·皮尔{{said}},改变你的想法,你就改变了自己的世界。{{conclude}}",
    "拿破仑·希尔{{said}},不要等待,时机永远不会恰到好处。{{conclude}}",
    "塞涅卡{{said}},生命如同寓言,其价值不在与长短,而在与内容。{{conclude}}",
    "奥普拉·温弗瑞{{said}},你相信什么,你就成为什么样的人。{{conclude}}",
    "吕凯特{{said}},生命不可能有两次,但许多人连一次也不善于度过。{{conclude}}",
    "莎士比亚{{said}},人的一生是短的,但如果卑劣地过这一生,就太长了。{{conclude}}",
    "笛卡儿{{said}},我的努力求学没有得到别的好处,只不过是愈来愈发觉自己的无知。{{conclude}}",
    "左拉{{said}},生活的道路一旦选定,就要勇敢地走到底,决不回头。{{conclude}}",
    "米歇潘{{said}},生命是一条艰险的峡谷,只有勇敢的人才能通过。{{conclude}}",
    "吉姆·罗恩{{said}},要么你主宰生活,要么你被生活主宰。{{conclude}}",
    "日本谚语{{said}},不幸可能成为通向幸福的桥梁。{{conclude}}",
    "海贝尔{{said}},人生就是学校。在那里,与其说好的教师是幸福,不如说好的教师是不幸。{{conclude}}",
    "杰纳勒尔·乔治·S·巴顿{{said}},接受挑战,就可以享受胜利的喜悦。{{conclude}}",
    "德谟克利特{{said}},节制使快乐增加并使享受加强。{{conclude}}",
    "裴斯泰洛齐{{said}},今天应做的事没有做,明天再早也是耽误了。{{conclude}}",
    "歌德{{said}},决定一个人的一生,以及整个命运的,只是一瞬之间。{{conclude}}",
    "卡耐基{{said}},一个不注意小事情的人,永远不会成就大事业。{{conclude}}",
    "卢梭{{said}},浪费时间是一桩大罪过。{{conclude}}",
    "康德{{said}},既然我已经踏上这条道路,那么,任何东西都不应妨碍我沿着这条路走下去。{{conclude}}",
    "克劳斯·莫瑟爵士{{said}},教育需要花费钱,而无知也是一样。{{conclude}}",
    "伏尔泰{{said}},坚持意志伟大的事业需要始终不渝的精神。{{conclude}}",
    "亚伯拉罕·林肯{{said}},你活了多少岁不算什么,重要的是你是如何度过这些岁月的。{{conclude}}",
    "韩非{{said}},内外相应,言行相称。{{conclude}}",
    "富兰克林{{said}},你热爱生命吗?那么别浪费时间,因为时间是组成生命的材料。{{conclude}}",
    "马尔顿{{said}},坚强的信心,能使平凡的人做出惊人的事业。{{conclude}}",
    "笛卡儿{{said}},读一切好书,就是和许多高尚的人谈话。{{conclude}}",
    "塞涅卡{{said}},真正的人生,只有在经过艰难卓绝的斗争之后才能实现。{{conclude}}",
    "易卜生{{said}},伟大的事业,需要决心,能力,组织和责任感。{{conclude}}",
    "歌德{{said}},没有人事先了解自己到底有多大的力量,直到他试过以后才知道。{{conclude}}",
    "达尔文{{said}},敢于浪费哪怕一个钟头时间的人,说明他还不懂得珍惜生命的全部价值。{{conclude}}",
    "佚名{{said}},感激每一个新的挑战,因为它会锻造你的意志和品格。{{conclude}}",
    "奥斯特洛夫斯基{{said}},共同的事业,共同的斗争,可以使人们产生忍受一切的力量。 {{conclude}}",
    "苏轼{{said}},古之立大事者,不惟有超世之才,亦必有坚忍不拔之志。{{conclude}}",
    "王阳明{{said}},故立志者,为学之心也;为学者,立志之事也。{{conclude}}",
    "歌德{{said}},读一本好书,就如同和一个高尚的人在交谈。{{conclude}}",
    "乌申斯基{{said}},学习是劳动,是充满思想的劳动。{{conclude}}",
    "别林斯基{{said}},好的书籍是最贵重的珍宝。{{conclude}}",
    "富兰克林{{said}},读书是易事,思索是难事,但两者缺一,便全无用处。{{conclude}}",
    "鲁巴金{{said}},读书是在别人思想的帮助下,建立起自己的思想。{{conclude}}",
    "培根{{said}},合理安排时间,就等于节约时间。{{conclude}}",
    "屠格涅夫{{said}},你想成为幸福的人吗?但愿你首先学会吃得起苦。{{conclude}}",
    "莎士比亚{{said}},抛弃时间的人,时间也抛弃他。{{conclude}}",
    "叔本华{{said}},普通人只想到如何度过时间,有才能的人设法利用时间。{{conclude}}",
    "博{{said}},一次失败,只是证明我们成功的决心还够坚强。 维{{conclude}}",
    "拉罗什夫科{{said}},取得成就时坚持不懈,要比遭到失败时顽强不屈更重要。{{conclude}}",
    "莎士比亚{{said}},人的一生是短的,但如果卑劣地过这一生,就太长了。{{conclude}}",
    "俾斯麦{{said}},失败是坚忍的最后考验。{{conclude}}",
    "池田大作{{said}},不要回避苦恼和困难,挺起身来向它挑战,进而克服它。{{conclude}}",
    "莎士比亚{{said}},那脑袋里的智慧,就像打火石里的火花一样,不去打它是不肯出来的。{{conclude}}",
    "希腊{{said}},最困难的事情就是认识自己。{{conclude}}",
    "黑塞{{said}},有勇气承担命运这才是英雄好汉。{{conclude}}",
    "非洲{{said}},最灵繁的人也看不见自己的背脊。{{conclude}}",
    "培根{{said}},阅读使人充实,会谈使人敏捷,写作使人精确。{{conclude}}",
    "斯宾诺莎{{said}},最大的骄傲于最大的自卑都表示心灵的最软弱无力。{{conclude}}",
    "西班牙{{said}},自知之明是最难得的知识。{{conclude}}",
    "塞内加{{said}},勇气通往天堂,怯懦通往地狱。{{conclude}}",
    "赫尔普斯{{said}},有时候读书是一种巧妙地避开思考的方法。{{conclude}}",
    "笛卡儿{{said}},阅读一切好书如同和过去最杰出的人谈话。{{conclude}}",
    "邓拓{{said}},越是没有本领的就越加自命不凡。{{conclude}}",
    "爱尔兰{{said}},越是无能的人,越喜欢挑剔别人的错儿。{{conclude}}",
    "老子{{said}},知人者智,自知者明。胜人者有力,自胜者强。{{conclude}}",
    "歌德{{said}},意志坚强的人能把世界放在手中像泥块一样任意揉捏。{{conclude}}",
    "迈克尔·F·斯特利{{said}},最具挑战性的挑战莫过于提升自我。{{conclude}}",
    "爱迪生{{said}},失败也是我需要的,它和成功对我一样有价值。{{conclude}}",
    "罗素·贝克{{said}},一个人即使已登上顶峰,也仍要自强不息。{{conclude}}",
    "马云{{said}},最大的挑战和突破在于用人,而用人最大的突破在于信任人。{{conclude}}",
    "雷锋{{said}},自己活着,就是为了使别人过得更美好。{{conclude}}",
    "布尔沃{{said}},要掌握书,莫被书掌握;要为生而读,莫为读而生。{{conclude}}",
    "培根{{said}},要知道对好事的称颂过于夸大,也会招来人们的反感轻蔑和嫉妒。{{conclude}}",
    "莫扎特{{said}},谁和我一样用功,谁就会和我一样成功。{{conclude}}",
    "马克思{{said}},一切节省,归根到底都归结为时间的节省。{{conclude}}",
    "莎士比亚{{said}},意志命运往往背道而驰,决心到最后会全部推倒。{{conclude}}",
    "卡莱尔{{said}},过去一切时代的精华尽在书中。{{conclude}}",
    "培根{{said}},深窥自己的心,而后发觉一切的奇迹在你自己。{{conclude}}",
    "罗曼·罗兰{{said}},只有把抱怨环境的心情,化为上进的力量,才是成功的保证。{{conclude}}",
    "孔子{{said}},知之者不如好之者,好之者不如乐之者。{{conclude}}",
    "达·芬奇{{said}},大胆和坚定的决心能够抵得上武器的精良。{{conclude}}",
    "叔本华{{said}},意志是一个强壮的盲人,倚靠在明眼的跛子肩上。{{conclude}}",
    "黑格尔{{said}},只有永远躺在泥坑里的人,才不会再掉进坑里。{{conclude}}",
    "普列姆昌德{{said}},希望的灯一旦熄灭,生活刹那间变成了一片黑暗。{{conclude}}",
    "维龙{{said}},要成功不需要什么特别的才能,只要把你能做的小事做得好就行了。{{conclude}}",
    "郭沫若{{said}},形成天才的决定因素应该是勤奋。{{conclude}}",
    "洛克{{said}},学到很多东西的诀窍,就是一下子不要学很多。{{conclude}}",
    "西班牙{{said}},自己的鞋子,自己知道紧在哪里。{{conclude}}",
    "拉罗什福科{{said}},我们唯一不会改正的缺点是软弱。{{conclude}}",
    "亚伯拉罕·林肯{{said}},我这个人走得很慢,但是我从不后退。{{conclude}}",
    "美华纳{{said}},勿问成功的秘诀为何,且尽全力做你应该做的事吧。{{conclude}}",
    "俾斯麦{{said}},对于不屈不挠的人来说,没有失败这回事。{{conclude}}",
    "阿卜·日·法拉兹{{said}},学问是异常珍贵的东西,从任何源泉吸收都不可耻。{{conclude}}",
    "白哲特{{said}},坚强的信念能赢得强者的心,并使他们变得更坚强。 {{conclude}}",
    "查尔斯·史考伯{{said}},一个人几乎可以在任何他怀有无限热忱的事情上成功。 {{conclude}}",
    "贝多芬{{said}},卓越的人一大优点是:在不利与艰难的遭遇里百折不饶。{{conclude}}",
    "莎士比亚{{said}},本来无望的事,大胆尝试,往往能成功。{{conclude}}",
    "卡耐基{{said}},我们若已接受最坏的,就再没有什么损失。{{conclude}}",
    "德国{{said}},只有在人群中间,才能认识自己。{{conclude}}",
    "史美尔斯{{said}},书籍把我们引入最美好的社会,使我们认识各个时代的伟大智者。{{conclude}}",
    "冯学峰{{said}},当一个人用工作去迎接光明,光明很快就会来照耀着他。{{conclude}}",
    "吉格·金克拉{{said}},如果你能做梦,你就能实现它。{{conclude}}"
  ],
  "bosh_before": [
    "既然如此,",
    "那么,",
    "我认为,",
    "一般来说,",
    "总结的来说,",
    "无论如何,",
    "经过上述讨论,",
    "这样看来,",
    "从这个角度来看,",
    "现在,解决{{title}}的问题,是非常非常重要的。 所以,",
    "每个人都不得不面对这些问题。在面对这种问题时,",
    "我们不得不面对一个非常尴尬的事实,那就是,",
    "而这些并不是完全重要,更加重要的问题是,",
    "我们不妨可以这样来想: "
  ],
  "bosh":[
    "{{title}}的发生,到底需要如何做到,不{{title}}的发生,又会如何产生。 ",
    "{{title}},到底应该如何实现。 ",
    "带着这些问题,我们来审视一下{{title}}。 ",
    "所谓{{title}},关键是{{title}}需要如何写。 ",
    "我们一般认为,抓住了问题的关键,其他一切则会迎刃而解。",
    "问题的关键究竟为何? ",
    "{{title}}因何而发生?",
    "一般来讲,我们都必须务必慎重的考虑考虑。 ",
    "要想清楚,{{title}},到底是一种怎么样的存在。 ",
    "了解清楚{{title}}到底是一种怎么样的存在,是解决一切问题的关键。",
    "就我个人来说,{{title}}对我的意义,不能不说非常重大。 ",
    "本人也是经过了深思熟虑,在每个日日夜夜思考这个问题。 ",
    "{{title}},发生了会如何,不发生又会如何。 ",
    "在这种困难的抉择下,本人思来想去,寝食难安。",
    "生活中,若{{title}}出现了,我们就不得不考虑它出现了的事实。 ",
    "这种事实对本人来说意义重大,相信对这个世界也是有一定意义的。",
    "我们都知道,只要有意义,那么就必须慎重考虑。",
    "这是不可避免的。 ",
    "可是,即使是这样,{{title}}的出现仍然代表了一定的意义。 ",
    "{{title}}似乎是一种巧合,但如果我们从一个更大的角度看待问题,这似乎是一种不可避免的事实。 ",
    "在这种不可避免的冲突下,我们必须解决这个问题。 ",
    "对我个人而言,{{title}}不仅仅是一个重大的事件,还可能会改变我的人生。 "
  ],
  "conclude":[
    "这不禁令我深思。 ",
    "带着这句话,我们还要更加慎重的审视这个问题: ",
    "这启发了我。",
    "我希望诸位也能好好地体会这句话。 ",
    "这句话语虽然很短,但令我浮想联翩。",
    "这句话看似简单,但其中的阴郁不禁让人深思。",
    "这句话把我们带到了一个新的维度去思考这个问题:",
    "这似乎解答了我的疑惑。"
  ],
  "said":[
    "曾经说过",
    "在不经意间这样说过",
    "说过一句著名的话",
    "曾经提到过",
    "说过一句富有哲理的话"
  ]
}

fs模块

fs模块主要是对文件进行读写操作,先来实现文件的读取,这里主要涉及两个API

  • readFile 异步地读取文件内容
  • readFileSync 同步地读取文件内容

一般我会用readFileSync,同步比较好维护代码~这里我们读取上述data.json文件,得注意,由于readFile 或者readFileSync 输出的都是Buffer对象,所以在输出的时候需要进行格式化~

// index.js
import { readFileSync } from 'fs';

const data = readFileSync('./corpus/data.json', { encoding: 'utf-8' });
console.log(data);

这里的文件路径采用的是相对路径,并不是很通用,当你在该项目外部执行node node-demo/index.js时会报错 ENOENT: no such file or directory, open './corpus/data.json',原因是./corpus/data.json会被理解成当前执行命令的文件下的corpus/data.json文件~

所以这里需要借助urlpath模块~将相对路径转为绝对路径:

import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';

// 获得当前脚本文件的 URL 地址, 类似:file:///.../node-demo/index.js
const url = import.meta.url;
/**
 * 当前脚本文件的 url 地址转化成文件路径
 * 通过 resolve 将相对路径转变成 data.json 文件的绝对路径
 * dirname方法可以获得当前 JS 文件的目录
 * resolve方法可以将 JS 文件目录和相对路径拼在一起
 */
const path = resolve(dirname(fileURLToPath(url)), './corpus/data.json');
const data = readFileSync(path, { encoding: 'utf-8' });
const corpus = JSON.parse(data);
console.log(corpus );

上述是ES Module的写法,关于转换路径这件事,CommonJS下直接一句话解决:

const path = resolve(__dirname, './corpus/data.json');

跳过随机模块的生成这一章,直接加入代码:

// lib/random.js
// 返回一定范围内的整数,用来控制随机生成的文章和段落的长度范围
export function randomInt(min = 0, max = 100) {
  const p = Math.random();
  return Math.floor(min * (1 - p) + max * p);
}

// 函数能够从语料库的数组中随机地选择元素,并返回
export function createRandomPicker(arr) {
  arr = [...arr]; // copy 数组,以免修改原始数据
  function randomPick() {
    const len = arr.length - 1;
    const index = randomInt(0, len);
    const picked = arr[index];
    [arr[index], arr[len]] = [arr[len], arr[index]];
    return picked;
  }
  randomPick(); // 抛弃第一次选择结果
  return randomPick;
}

// lib/generator.js
import {randomInt, createRandomPicker} from './random.js';

function sentence(pick, replacer) {
  let ret = pick();
  for(const key in replacer) {
    ret = ret.replace(new RegExp(`{{${key}}}`, 'g'),
      typeof replacer[key] === 'function' ? replacer[key]() : replacer[key]);
  }
  return ret;
}

export function generate(title, {
  corpus,
  min = 6000, // 文章最少字数
  max = 10000, // 文章最多字数
} = {}) {
  const articleLength = randomInt(min, max);
  const {famous, bosh_before, bosh, said, conclude} = corpus;
  const [pickFamous, pickBoshBefore, pickBosh, pickSaid, pickConclude]
    = [famous, bosh_before, bosh, said, conclude].map(createRandomPicker);

  const article = [];
  let totalLength = 0;

  while(totalLength < articleLength) {
    let section = '';
    const sectionLength = randomInt(200, 500); // 每段200到500字
    while(section.length < sectionLength || !/[。?]$/.test(section)) {
      const n = randomInt(0, 100);
      if(n < 20) {
        section += sentence(pickFamous, {said: pickSaid, conclude: pickConclude});
      } else if(n < 50) {
        section += sentence(pickBoshBefore, {title}) + sentence(pickBosh, {title});
      } else {
        section += sentence(pickBosh, {title});
      }
    }
    totalLength += section.length;
    article.push(section);
  }

  return article;
}

上述代码主要实现如何从data.json中获取随机片段,接下来我们要将生成的文章进行输出:

// index.js
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';

import { generate } from './lib/generator.js';
import { createRandomPicker } from './lib/random.js';

const __dirname = dirname(fileURLToPath(import.meta.url));

// 获取data.json
function loadCorpus(src) {
  const path = resolve(__dirname, src);
  const data = readFileSync(path, {encoding: 'utf-8'});
  return JSON.parse(data);
}

const corpus = loadCorpus('./corpus/data.json');
// 生成随机title
const pickTitle = createRandomPicker(corpus.title);
const title = pickTitle();
// 生成随机文章
const article = generate(title, {corpus});
console.log(`${title}\n\n    ${article.join('\n    ')}`);


借助fs的保存API,将生成的内容保存到文件中~

npm i moment --save
function saveCorpus(title, article) {
  // 设置输出目录的路径
  const outputDir = resolve(__dirname, 'output');
  // 是否存在输出目录,不存在则创建
  if(!existsSync(outputDir)) {
    mkdirSync(outputDir);
  }
  // 创建时间戳
  const time = moment().format('|YYYY-MM-DD|HH:mm:ss');
  // 获取输出的文件路径
  const outputFile = resolve(outputDir, `${title}${time}.txt`);
  // 获得输出内容
  const text = `${title}\n\n    ${article.join('\n    ')}`;
  // 如果文件不存在会自动创建,但是如果文件明有问题,会告诉你找不到该文件,所以起名慎重
  writeFileSync(outputFile, text);

  return outputFile;
}

loadCorpussaveCorpus抽取到lib/corpus.js中~

// index.js
import { loadCorpus, saveCorpus } from './lib/corpus.js';
import { generate } from './lib/generator.js';
import { createRandomPicker } from './lib/random.js';

const corpus = loadCorpus('corpus/data.json');
const pickTitle = createRandomPicker(corpus.title);
const title = pickTitle();
const article = generate(title, {corpus});
saveCorpus(title, article);

执行node.js会在output目录中保存文件~

process模块

这里主要是实现带参数的命令行交互。

之前提到过,我们可以这样读取变量:

const argv = process.argv;
// 执行node node.js的命令,
// 这时候process.argv的值是数组['node', 'node.js']

我们在命令行输入文字的标题、最小字数和最大字数,来优化我们的随机文章生成~

首先是我们定义一下可以输入的变量:

node index.js --min 100 --max 200  --title 文章标题

接下来我们需要对输入的变量做出检查,防止遇到没有被定义的变量~这里借助command-line-args

npm install command-line-args --save
import commandLineArgs from 'command-line-args';

// ...

const optionDefinitions = [
  { name: 'title', alias: 't', type: String },
  { name: 'min', type: Number},
  { name: 'max', type: Number},
];
// 从命令行获取输入
const options = commandLineArgs(optionDefinitions);

这里可以校验参数是否合法,然后后面我们可以把这些参数作为变量传进函数中~

正常的命令行交互,还需要有一个—help来提示用户有哪些可以输入的变量,这里借助 command-line-usage 来完成:

npm install command-line-args --save

把跟命令行相关的都封装在lib/cmd.js中:

import commandLineArgs from 'command-line-args';
import commandLineUsage from 'command-line-usage';

// 定义help提示
const sections = [
  {
    header: '狗屁不通文章生成器',
    content: '生成随机的文章段落用于测试',
  },
  {
    header: 'Options',
    optionList: [
      {
        name: 'title',
        typeLabel: '{underline string}',
        description: '文章的主题。',
      },
      {
        name: 'min',
        typeLabel: '{underline number}',
        description: '文章最小字数。',
      },
      {
        name: 'max',
        typeLabel: '{underline number}',
        description: '文章最大字数。',
      },
    ],
  },
];

const usage = commandLineUsage(sections);

// 定义可以输入的参数
const optionDefinitions = [
  {name: 'help'},
  {name: 'title', type: String},
  {name: 'min', type: Number},
  {name: 'max', type: Number},
];

const options = commandLineArgs(optionDefinitions);

if('help' in options) {
  console.log(usage);
  // 输入--help之后记得退出程序
  process.exit();
}

export { options } ;

// index.js
import { loadCorpus, saveCorpus } from './lib/corpus.js';
import { generate } from './lib/generator.js';
import { createRandomPicker } from './lib/random.js';
import {options} from './lib/cmd.js';

const corpus = loadCorpus('corpus/data.json');
const title = options.title || createRandomPicker(corpus.title)();
const article = generate(title, {corpus, ...options});
const output = saveCorpus(title, article);

console.log(`生成成功!文章保存于:${output}`);


node index.js --help

上述交互还不是很灵活,都是我们将参数写死在node index.js后面,接下来学习如何通过读取用户在cmd输入的参数来生成文章~这里主要使用到了process.stdinreadline

首先,设计一个问题数据结构:

[
  {text: '请输入文章主题', value: title},
  {text: '请输入最小字数', value: 6000},
  {text: '请输入最大字数', value: 10000},
]

接下来我们创建lib/interact.js

import readline from 'readline';

function question(rl, {text, value}){
  const q = `${text}(${value})\n`;
  return new Promise((resolve) => {
    rl.question(q, (answer) => {
      resolve(answer || value);
    });
  });
}

export async function interact(questions) {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
  });

  const answers = [];

  for(let i = 0; i < questions.length; i++) {
    const q = questions[i];
    // 每次输出一个提问并等待用户输入答案
    const answer = await question(rl, q);
    answers.push(answer);
  }

  rl.close();
  return answers;
}
// index.js
import { loadCorpus, saveCorpus } from './lib/corpus.js';
import { generate } from './lib/generator.js';
import { createRandomPicker } from './lib/random.js';
import { options } from './lib/cmd.js';
import { interact } from './lib/interact.js'

const corpus = loadCorpus('corpus/data.json');
let title = options.title || createRandomPicker(corpus.title)();

(async function () {
  if(Object.keys(options).length <= 0) {
    const answers = await interact([
      {text: '请输入文章主题', value: title},
      {text: '请输入最小字数', value: 6000},
      {text: '请输入最大字数', value: 10000},
    ]);
    title = answers[0];
    options.min = answers[1];
    options.max = answers[2];
  }

  const article = generate(title, {corpus, ...options});
  const output = saveCorpus(title, article);

  console.log(`生成成功!文章保存于:${output}`);
}());


# node index.js
请输入文章主题(科学和人文谁更有意义)
Armouy
请输入最小字数(6000)
100
请输入最大字数(10000)
200
生成成功!文章保存于:D:\...\node-demo\output\Armouy2023-07-03-11-48-58.txt

net模块

主要用这个模块实现一个TCP服务器。

import net from 'net';

function responseData(str, status = 200, desc = 'OK') {
  return `HTTP/1.1 ${status} ${desc}
Connection: keep-alive
Date: ${new Date()}
Content-Length: ${str.length}
Content-Type: text/html

${str}`;
}

const server = net.createServer((socket) => {
  socket.on('data', (data) => {
    const matched = data.toString('utf-8').match(/^GET ([/\w]+) HTTP/);
    if(matched) {
      const path = matched[1];
      // 如果路径是/,则返回 200,否则返回 404
      if(path === '/') { 
        socket.write(responseData('<h1>Hello world</h1>'));
      } else { 
        socket.write(responseData('<h1>Not Found</h1>', 404, 'NOT FOUND'));
      }
    }
    console.log(`DATA:\n\n${data}`);
  });

  socket.on('close', () => {
    console.log('connection closed, goodbye!\n\n\n');
  });
}).on('error', (err) => {
  throw err;
});

server.listen({
  host: '0.0.0.0',
  port: 8080,
}, () => {
  console.log('opened server on', server.address());
});

Http模块

我们前面用net模块搭建了TCP服务,通常情况下,我们直接用http模块搭建一个http服务器会更方便些~相比使用 TCP 服务来处理 HTTP 请求,使用 http 服务处理 HTTP 请求更加简单,因为我们不需要手动解析 HTTP 请求的报文,以及用字符串模板组织 HTTP 响应报文。

简单实现根据不同的url返回不同的页面内容:

// http-simple.js
import http from 'http';
import url from 'url';

// req 表示 HTTP 请求对象,res 表示 HTTP 响应对象
const server = http.createServer((req, res) => {
  const { pathname } = url.parse(`http://${req.headers.host}${req.url}`);
  /**
   * 在返回 HTTP 响应内容时,我们不再需要自己拼接模板字符串和计算Content-length。
   * http 模块会自动完成这个工作并将Content-length写入响应头。
   * 我们还可以直接用res.writeHead来写入其他 HTTP 响应头,
   * 用res.end来写入 HTTP 的 Body 部分。
   */
  if(pathname === '/') {
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.end('<h1>Hello world</h1>');
  }else {
    res.writeHead(404, {'Content-Type': 'text/html'});
    res.end('<h1>Not Found</h1>');
  }
})

server.on('clientError', (err, socket) => {
  socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
})

server.listen(8080, () => {
  console.log('opened server on', server.address());
});


接下来实现一下常用静态资源服务器,根据URL地址返回对应的文件:

先安装一个依赖包,我们在返回不同的文件类型的时候需要修改Content-Type,否则浏览器不会正确识别,所以需要借助这个包:

npm i mime --save

在项目中新建一个www文件夹,在内部放置三种文件:

- 1.jpg
- index.html
- test.js

我们可以通过这些路径去访问对应的资源:

- http://localhost:8080/1.jpg
- http://localhost:8080/ 或者 http://localhost:8080/index.html
- http://localhost:8080/test.js

接下来就是代码编写啦:

// http-static.js
import http from 'http';
import { fileURLToPath } from 'url';
import { dirname, resolve, join, parse } from 'path';
import fs from 'fs';
import mime from 'mime';

const __dirname = dirname(fileURLToPath(import.meta.url));

const server = http.createServer((req, res) => {
  // 将想要获取的文件路径格式化一下,转成绝对路径
  let filePath = resolve(__dirname, join('www', `${req.url}`));

  // 判断文件是否存在
  if(fs.existsSync(filePath)) {
    // 判断是否是文件目录
    const stats = fs.statSync(filePath);
    const isDir = stats.isDirectory();

    if(isDir) {
      // 如果是目录,则访问的是index.html
      filePath = join(filePath, 'index.html');
    }

    const content = fs.readFileSync(filePath);
    // 获取文件后缀
    const { ext } = parse(filePath);
    // 根据文件类型返回不同的Content-Type
    res.writeHead(200, {'Content-Type': mime.getType(ext)}); 
    return res.end(content); 
  }
  res.writeHead(404, {'Content-Type': 'text/html'});
  res.end('<h1>Not Found</h1>');
});

server.on('clientError', (err, socket) => {
  socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});

server.listen(8080, () => {
  console.log('opened server on', server.address());
});


stream模块

前端读取静态资源的时候我们需要等待整个文件的内容完全读取出来再发送,使用stream模块可以以流的方式读取文件,把接收到的数据不断地发送给客户端浏览器,这样就可以避免文件内容太大时,内存的消耗以及文件I/O导致阻塞。

const server = http.createServer((req, res) => {
  // 将想要获取的文件路径格式化一下,转成绝对路径
  let filePath = resolve(__dirname, join('www', `${req.url}`));

  // 判断文件是否存在
  if(fs.existsSync(filePath)) {
    // 判断是否是文件目录
    const stats = fs.statSync(filePath);
    const isDir = stats.isDirectory();

    if(isDir) {
      // 如果是目录,则访问的是index.html
      filePath = join(filePath, 'index.html');
    }

    // 获取文件后缀
    const { ext } = parse(filePath);
    // 根据文件类型返回不同的Content-Type
    res.writeHead(200, {'Content-Type': mime.getType(ext)}); 
    // 以流的方式读取
    const fileStream = fs.createReadStream(filePath);
    fileStream.pipe(res);
  }else {
    res.writeHead(404, {'Content-Type': 'text/html'});
    res.end('<h1>Not Found</h1>');
  }
});

(随机文章生成器到这里就结束啦,接下来的内容,是基于上述知识点的进阶,跟功能实现无关~)

总结

复习了Node.js与浏览器在运行JS上的不同之处,梳理了模块化的使用差别、常见的内置模块的使用~

参考链接

从前端到全栈


如果错误欢迎指出,感谢阅读~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值