Node全解05静态服务器

HTTP模块

一些有用的工具

node-dev

  • 当文件更新时自动重启 node
  • 避免每次改完代码都要重新运行的麻烦
  • 不宜在生产环境使用

ts-node

  • 让 node 支持直接运行 TS代码
  • 不宜在生产环境使用

ts-node-dev

  • 这个工具结合了上面两个工具
  • 可以用 TS 开发 Node.js 程序,且会自动重启
  • 不宜在生产环境使用,非常适合用来学习

你可以这样安装 yarn global add ts-node-dev

一些有用的工具2

WebStorm自动格式化

  • ReformatCode 设置为 Ctrl + L 或者其他键位
  • 可以快速帮你格式化 TS / JS 代码

WebStorm自动写代码

  • Complete Current Statement 设置为 Ctrl + Enter 或者其他键位
  • Show Context Actions 设置为 Alt + Enter 或者其他键位

VSCode配置

  • 开启自动保存 autosave
  • format on save
  • format

一些有用的工具3

WebStorm Git配置

  • 选择设置 =》 搜索 git => path选中你安装的git路径

VSCode配置

  • 设置 搜索 settings 打开设置 json
1
2
3
4
{
"git.enabled": true,
"git.path": "/usr/bin/git"
}

Curl命令

  • get: curl -v url
  • post: curl -v -d "name=xxx" url
  • 设置请求头: -H 'Content-Type:application/json'
  • 设置动词: -X PUT
  • JSON请求: curl -d '{"name":"bob"}' -H 'Content-Type:pplication/json' url

创建项目

  • 项目目录 node-server-demo
  • 由于我们写 ts 别忘了安装 yarn global add ts-node-dev
  • yarn init -y
  • yarn add –dev @types/node 安装 node 声明文件
  • 新建 index.ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import * as http from 'http';

    const server = http.createServer();

    server.on('request', (request, response) => {
    console.log('有人请求了')
    response.end("hi");
    })

    server.listen(8888);
  • 启动服务ts-node-dev index.ts

  • 发请求curl http://localhost:8888

server 是什么东西

http.Server 类的实例

  • 根据文档能知道 http.createServer 的返回值类型
  • 根据文档知道 server 拥有几个事件和方法
  • 其中目前能用上的 只有 request事件 和 listen()

继承自net.Server

  • 根据文档知道继承关系
  • 看到 net.Server 又有几个方法
  • 其中也就 error事件 和 address() 能用上

一些技巧

  • 有的时候 点不出来东西,那是因为 类型声明文件不够匹配可能写的是 any
  • 所以你可以通过 加类型声明 来达到 “点出来提示”
  • 如果你用的是 vscode 请安装 auto import

index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import * as http from 'http';
import { IncomingMessage, ServerResponse } from 'http';

const server = http.createServer();

server.on('request', (request: IncomingMessage, response: ServerResponse) => {
console.log(request.constructor);
console.log(response.constructor);
// 得知它是什么 然后声明它的类型 , 这样你在 request. 的时候才会有提示
console.log(request.httpVersion);
console.log(request.url);
response.end("hi");
})

server.listen(8888);

如何获取请求体的内容

  • request.on('data', fn)
  • request.on('end', fn)
  • curl -v -d "name=aaa" http://localhost:8888

有个问题:这个上传方式就算用户上传1G的东西都可以拿到,因为你是一小段一小段拿到然后拼起来,只要服务器内存够。就可以处理上传的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import * as http from 'http';
import { IncomingMessage, ServerResponse } from 'http';

const server = http.createServer();

server.on('request', (request: IncomingMessage, response: ServerResponse) => {
console.log(request.constructor);
console.log(response.constructor);
// 得知它是什么 然后声明它的类型 , 这样你在 request. 的时候才会有提示
console.log('request.httpVersion')
console.log(request.httpVersion);
console.log('request.url');
console.log(request.url);
console.log('request.headers');
console.log(request.headers);
console.log('request.method');
console.log(request.method);

// 获取请求体
const array = [];
request.on('data', (chunk) => {
array.push(chunk)
})
request.on('end', () => {
const body = Buffer.concat(array).toString();
console.log(body)
response.end("hi");
})

})

server.listen(8888);

request,response 是啥

找类

  • 根据文档, request 是 http. IncomingMessage 的实例
  • 根据文档, response 是 http. ServerResponse 的实例

Request

  • 拥有 headers, method , url 等属性
  • 从 stream.Readable 类继承了 data/end/error 事件
  • 为什么不直接拿到请求的消息体呢? 根TCP相关

Response

  • 拥有 getHeader/setHeader/end/write 等方法
  • 拥有 statusCode 属性,可读可写

    1
    2
    3
    4
    5
    6
    7
    8
    response.statusCode = 404;
    // 设置响应头
    response.setHeader('x-frank', 'I am frank')
    response.write('1\n');
    response.write('2\n');
    response.write('3\n');
    response.write('4\n');
    response.end(); // 如果你设置了 404 就 end里不传任何东西
    • 你也可以写回一个 image 但是要设置文件头标示它是一个 image
  • 继承了 Stream,目前用不上

项目1 根据url返回不同类型文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

import * as http from 'http';
import { IncomingMessage, ServerResponse } from 'http';
import * as fs from 'fs';
import * as p from 'path';

const server = http.createServer();
const publicDir = p.relative(__dirname, 'public');

server.on('request', (request: IncomingMessage, response: ServerResponse) => {
const { method, url, headers } = request;
switch (url) {
case '/index.html':
response.setHeader('Content-Type', 'text/html;charset=utf-8');
fs.readFile(p.resolve(publicDir, 'index.html'), (error, data) => {
if (error) throw error;
response.end(data.toString());
})
break;
case '/style.css':
response.setHeader('Content-Type', 'text/css;charset=utf-8');
fs.readFile(p.resolve(publicDir, 'style.css'), (error, data) => {
if (error) throw error;
response.end(data.toString());
})
break;
case '/main.js':
response.setHeader('Content-Type', 'text/javascript;charset=utf-8');
fs.readFile(p.resolve(publicDir, 'main.js'), (error, data) => {
if (error) throw error;
response.end(data.toString());
})
break;
}
})

server.listen(8888);

项目2 处理查询参数

1
2
3
4
5
6
7
8
9
import * as url from 'url';

server.on('request', (request: IncomingMessage, response: ServerResponse) => {
const { method, url: path, headers } = request;
const { pathname, search } = url.parse(path);
switch (pathname) {
...
}
});

项目3 匹配任意文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 匹配任意文件
import * as http from 'http';
import { IncomingMessage, ServerResponse } from 'http';
import * as fs from 'fs';
import * as p from 'path';
import * as url from 'url';

const server = http.createServer();
const publicDir = p.relative(__dirname, 'public');

server.on('request', (request: IncomingMessage, response: ServerResponse) => {
const { method, url: path, headers } = request;
const { pathname, search } = url.parse(path);

// /index.html ==> index.html
let filename = pathname.substr(1);
if (filename === '') {
filename = 'index.html'
}
response.setHeader('Content-Type', 'text/html;charset=utf-8');
fs.readFile(p.resolve(publicDir, filename), (error, data) => {
if (error) {
if (error.errno === -4058) {
response.statusCode = 404;
response.end('你要的文件不存在啊!!!');
} else if (error.errno === -4068) {
// http://localhost:8888/xxx
response.statusCode = 403;
response.end('无权限查看目录内容');
} else {
response.statusCode = 500;
response.end('服务器繁忙,请稍后再试');
}
} else {
response.end(data.toString());
}
})
})

server.listen(8888);

项目4 处理非get请求 添加缓存文件时间

1
2
3
4
5
6
7
8
9
10
if (method !== 'GET') {
response.statusCode = 200;
response.setHeader('Content-Type', 'text/html;charset=utf-8');
response.end("这是一个假的响应");
return;
}

...

response.setHeader('Cache-Control', `public,max-age=${cacheAge}`);

免费用 WebStorm的途径

申请条件

  • 开源项目的贡献者
  • 你的项目是开源的 如MIT
  • 你的项目是不盈利的
  • 你的项目至少活跃3个月
  • 你定期发布更新的版本
  • 满足条件就 点击申请按钮

填写材料

  • 邮箱地址一定不要错
  • No. of required licenses 填 1
  • 项目描述用 google 翻译即可
  • 正版 JetBrains 全家桶唾手可得,价值$649/年

项目地址

https://github.com/slTrust/node-server-demo

Node全解04调试工具

调试工具

WebStorm

  • 打开一个项目目录
  • 点击工具栏 Run
  • Edit Configurations
  • 点击 “+”
  • 然后配置你的测试配置
    • Name 你这个配置的名称
    • Node interpreter node环境
    • Wroking directory: 工作目录
    • JavaScript file: 测试的文件 xxx.js
    • Application parameters: 输入的参数
  • 点击OK
  • 再次点击 Run 子菜单里会多了一个 你刚刚的配置
    • Run xxx 直接运行
    • debug xxx 设置断点运行

VS Code

  • 侧边栏点击 小虫子那个按钮
  • 点击 启动程序 , 如果没反应 点击它右边有个 控制台的按钮
  • 会让你配置一个 json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    {
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
    {
    "type": "node",
    "request": "launch",
    // 启动程序
    "name": "Launch Program",
    "skipFiles": [
    "<node_internals>/**"
    ],
    // 入口文件
    "program": "${workspaceFolder}/cli.js",
    "args": ["add", "task", "100"]

    }
    ]
    }
  • 再次点击运行按钮

命令行 + Chrome 断点

第一种方式: 你可以在 js文件 添加 debugger 运行时就会停在当前行

第二种方式: 终端添加参数 --inspect-brk

1
2
3
4
5
// 你原来是这样
node cli.js add abc

// 现在你这样,它就会停下来等你
node --inspect-brk cli.js add abc
  • 运行上述命令后,打开 Chrome 右键调试
  • 你会看到弹出的工具栏 左上角 多了一个绿色 的 Node.js 图标 点击即可
  • 注意! 如果你直接添加的命令是 --inspect 它会直接执行 不停下来 ,所以要加 -brk 它就会等你连接

Node全解03_01jest

单元测试 JEST使用

代码使用我们上次写的Node命令行工具

如何测试

步骤

  • 选择测试工具: jest
  • 设计测试用例
  • 写测试,运行测试,改代码
  • 单元测试、功能测试、集成测试

重点

  • 单元测试不应该和外界打交道(那是集成测试做的) 应该 mock
    • 不能操作硬盘
    • 不能操作网络
    • 不能操作任何东西,只能操作你自己
  • 单元测试的对象是函数
  • 功能测试的对象是模块
  • 集成测试的对象是系统
  • 我们先搞定单元测试

直接 CRM学习法

  • Jest官网 Docs
  • yarn add --dev jest
  • 修改package.json

    1
    2
    3
    "scripts": {
    "test": "jest"
    },
  • 新建 __test__ 目录 并在它下面创建 db.spec.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const db = require("../db.js")
    describe("db",()=>{
    it("can read",()=>{
    expect(db.read instanceof Function).toBe(true)
    })

    it("can write",()=>{
    expect(db.write instanceof Function).toBe(true)
    })
    })
  • 运行 npm run test

如何测试读文件呢?

如果你想这样测试!!! 请放弃

  • 不该和外界打交道,万一你的文件已经存在了呢?这样就会有干扰
1
2
fs.writeFileSync('/xxx.json',`[{"title":"hi","done":true}]`)
db.read('/xxx.json')

请搜索 jest mock fs jest doc 的 Manual Mocks章节

1
jest.mock('fs') // 意思是 jest 接管 fs模块

Mock fs 模块

  • 新建 __mocks__/fs.js 代表 mock 一个假的 fs模块

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 声明是 fs的 jest 的假模块
    const fs = jest.genMockFromModule('fs');

    fs.x = ()=>{
    console.log('hi')
    return 'xxx';
    }

    module.exports = fs;
  • db.spec.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const db = require("../db.js")
    const fs = require('fs');
    jest.mock('fs');

    describe("db",()=>{
    it("can read",()=>{
    expect(fs.x()).toBe('xxx')
    })
    })
  • 运行 npm run test 成功

完整版 mock fs

__mocks__/fs.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 声明是 fs的 jest 的假模块
const fs = jest.genMockFromModule('fs');
const _fs = jest.requireActual('fs');

Object.assign(fs,_fs);

let readMocks = {};

fs.setReadFileMock = (path, error, data)=>{
readMocks[path] = [error,data];
}

// 覆盖 fs的 readFile()
fs.readFile = (path, options, callBack) => {
// 如果你 fs.readFile('xxx',fn) 代表第二个参数是 callBack
if(callBack === undefined){
callBack = options
}

if(path in readMocks){
callBack(...readMocks[path])
}else{
_fs.readFile(path, options, callBack)
}
}

let writeMocks = {}

fs.setWriteFileMock = (path, fn)=>{
writeMocks[path] = fn;
}

fs.writeFile = (path,data,options, callBack) => {
if(callBack === undefined){
callBack = options;
}
if(path in writeMocks){
writeMocks[path](path, data, options, callBack)
}else{
_fs.writeFile(path, data, options, callBack)
}

}

fs.clearMocks = ()=>{
readMocks = {};
writeMocks = {};
}

module.exports = fs;

__tests__/db.spec.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const db = require("../db.js")
const fs = require('fs');
jest.mock('fs');

describe("db",()=>{
afterEach(()=>{
// 每个it 执行之后
fs.clearMocks();
})
it("can read", async () =>{
const data = [{title:"hi",done:true}]
fs.setReadFileMock('/xxx',null,JSON.stringify(data));
const list = await db.read('/xxx');
expect(list).toStrictEqual(data)
})

it("can wriete", async () =>{
let fakeFile;
fs.setWriteFileMock('/yyy',(path, data, options, callBack)=>{
fakeFile = data;
callBack(null)
})
const list = [{title:"见欧阳娜娜",done:true}, {title:"见迪丽热巴",done:true}]
await db.write(list,'/yyy');
expect(fakeFile).toBe(JSON.stringify(list) + "\n");
})
})

代码仓库

Node全解02_02node命令行程序

继续完善代码

001 如果用户输入 node cli.js 就显示所有任务

cli.js

1
2
3
4
5
6
7
// 用户实际输入的 命令参数
// console.log(process.argv)

if(process.argv.length === 2){
// 说明用户直接运行 node cli.js
api.showAll()
}

index.js

1
2
3
4
5
6
module.exports.showAll = async (title) =>{
const list = await db.read();
list.forEach((task, index)=>{
console.log(`${task.done ? '[x]' : '[_]'}${index} - ${task.title}`)
})
}

002 在用户输入 node cli.js 时候 假如有很多任务 如何通过上下按钮 切换呢?

003 完成所有功能

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
const inquirer = require('inquirer');
const db = require('./db.js');

module.exports.add = async (title) =>{
// 读取之前的任务
const list = await db.read();
// 往里面添加一个任务
list.push({title,done:false});
// 存储任务到文件
console.log('read~~~~')
console.log(list)
await db.write(list);
}

module.exports.clear = async (title) =>{
await db.write([]);
}

module.exports.showAll = async (title) =>{
const list = await db.read();
inquirer
.prompt([
{
type: 'list',
name: 'index',
message: '请选择你想操作的任务',
choices: [
{name:'退出',value:'-1'},

...list.map((task, index)=>{
return {name: `${task.done ? '[x]' : '[_]'}${index} - ${task.title}`, value: index.toString()}
}),
{name:'创建任务',value:'-2'}
]
},
])
.then(answers => {
const index = parseInt(answers.index);
if(index >= 0){
inquirer
.prompt([
{
type: 'list',
name: 'action',
message: '请选择操作',
choices: [
{name:'退出',value:'quit'},
{name:'已完成',value:'makeAsDone'},
{name:'未完成',value:'maksAsUndone'},
{name:'改标题',value:'updateTitle'},
{name:'删除',value:'remove'},
]
},
])
.then(answers2 => {
switch(answers2.action){
case 'makeAsDone':
list[index].done = true;
db.write(list);
break;
case 'maksAsUndone':
list[index].done = false;
db.write(list);
break;
case 'updateTitle':
inquirer.prompt([
{type: 'input',
name: 'title',
message:'新的标题',
default: list[index].title
}]).then(answer => {
list[index].title = answer.title
db.write(list)
});
break;
case 'remove':
list.splice(index,1);
db.write(list);
break;
}
});
}else if( index === -2){
// 创建任务
inquirer.prompt([
{type: 'input',
name: 'title',
message:'输入任务标题'
}]).then(answer => {
list.push({
title: answer.title,
done: false
})
db.write(list);
});
}
});
}

004 优化代码

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
const inquirer = require('inquirer');
const db = require('./db.js');

module.exports.add = async (title) =>{
// 读取之前的任务
const list = await db.read();
// 往里面添加一个任务
list.push({title,done:false});
// 存储任务到文件
console.log('read~~~~')
console.log(list)
await db.write(list);
}

module.exports.clear = async () =>{
await db.write([]);
}

function askForCreateTask(list){
inquirer.prompt([
{type: 'input',
name: 'title',
message:'输入任务标题'
}]).then(answer => {
list.push({
title: answer.title,
done: false
})
db.write(list);
});
}

function makeAsDone(list,index){
list[index].done = true;
db.write(list);
}

function maksAsUndone(list,index){
list[index].done = false;
db.write(list);
}

function updateTitle(list,index){
inquirer.prompt([
{type: 'input',
name: 'title',
message:'新的标题',
default: list[index].title
}]).then(answer => {
list[index].title = answer.title
db.write(list)
});
}

function remove(list,index){
list.splice(index,1);
db.write(list);
}

function askForAction(list, index){
const actions = {
makeAsDone,
maksAsUndone,
updateTitle,
remove
};

inquirer
.prompt([
{
type: 'list',
name: 'action',
message: '请选择操作',
choices: [
{name:'退出',value:'quit'},
{name:'已完成',value:'makeAsDone'},
{name:'未完成',value:'maksAsUndone'},
{name:'改标题',value:'updateTitle'},
{name:'删除',value:'remove'},
]
},
])
.then(answers2 => {
const action = actions[answers2.action];
console.log(action);
action && action(list, index);
});
}

function printTasks(list){
inquirer
.prompt([
{
type: 'list',
name: 'index',
message: '请选择你想操作的任务',
choices: [
{name:'退出',value:'-1'},

...list.map((task, index)=>{
return {name: `${task.done ? '[x]' : '[_]'}${index} - ${task.title}`, value: index.toString()}
}),
{name:'创建任务',value:'-2'}
]
},
])
.then(answers => {
const index = parseInt(answers.index);
if(index >= 0){
askForAction(list,index);
}else if( index === -2){
// 创建任务
askForCreateTask(list);
}
});
}

module.exports.showAll = async (title) =>{
const list = await db.read();
printTasks(list);
}

优化技巧,给一坨代码起个名字法

005 发布代码

  • 修改 package.json
1
2
3
4
"bin":{
"t": "cli.js"
},
"files":["*.js"]
  • 添加 node shebang 在 cli.js 第一行
1
#!/usr/bin/env node
  • 如果你是 mac 添加文件可执行权限
    • chmod +x cli.js
  • nrm 安装

    1
    2
    3
    npm install -g nrm

    // 如果你是别的源 你就 nrm use npm
  • 发布你的代码

    • 如果是 yarn 你就yarn login 和 yarn publish
    • 如果是npm 你就npm adduser 和 npm puhlish
    • 注意把淘宝源切换为原始源

代码仓库

Node全解02_01node命令行程序

CRM学习 Node.js 之文件模块

目标 完成一个命令行工具

001 新建目录 node-todo-1

  • 打开这个目录终端 yarn init -y
  • 修改package.json 的版本号为 0.0.1
  • 新建 index.js 内容为console.log('hi')

002 使用库 commander.js

  • 抄文档开始干
  • npm install commander 或者你 yarn add commander
  • 安装后,继续抄

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const program = require('commander');

    program
    .option('-d, --debug', 'output extra debugging')
    .option('-s, --small', 'small pizza size')
    .option('-p, --pizza-type <type>', 'flavour of pizza');

    program.parse(process.argv);

    // 额外加一行
    console.log('hi')
  • 项目根目录运行 node index.js 只打印了 hi

  • 此时你运行 node index -h 会显示如下内容

    1
    2
    3
    4
    5
    6
    7
    Usage: index [options]

    Options:
    -d, --debug output extra debugging
    -s, --small small pizza size
    -p, --pizza-type <type> flavour of pizza
    -h, --help output usage information
  • 修改 index.js

    1
    2
    3
    4
    5
    6
    7
    // 修改后然后运行  node index -h 就会显示你修改的内容
    const program = require('commander');

    program
    .option('-x, --xxx', 'what the x')

    program.parse(process.argv);
  • 继续抄文档, 实现一个子命令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    const program = require('commander');

    // 设置它的选项
    program
    .option('-x, --xxx', 'what the x')

    // 设置子命令
    program
    .command('add <taskName>')
    .description('add a task')
    .action((x,y) => {
    console.log(x)
    console.log(y)
    });


    program.parse(process.argv);



    // 运行 node index add 会提示缺少一个 taskName

    // 运行 node index add aa 打印
    aa
    Command {
    commands: [],
    options: [],
    _execs: Set {},
    _allowUnknownOption: false,
    _args: [ { required: true, name: 'taskName', variadic: false } ],
    _name: 'add',
    _optionValues: {},
    _storeOptionsAsProperties: true,
    _passCommandToAction: true,
    _actionResults: [],
    _helpFlags: '-h, --help',
    _helpDescription: 'output usage information',
    _helpShortFlag: '-h',
    _helpLongFlag: '--help',
    _noHelp: false,
    _exitCallback: undefined,
    _executableFile: undefined,
    parent: Command {
    commands: [ [Circular] ],
    options: [ [Option] ],
    _execs: Set {},
    _allowUnknownOption: false,
    _args: [],
    _name: 'index',
    _optionValues: {},
    _storeOptionsAsProperties: true,
    _passCommandToAction: true,
    _actionResults: [],
    _helpFlags: '-h, --help',
    _helpDescription: 'output usage information',
    _helpShortFlag: '-h',
    _helpLongFlag: '--help',
    Command: [Function: Command],
    Option: [Function: Option],
    CommanderError: [Function: CommanderError],
    _events: [Object: null prototype] {
    'option:xxx': [Function],
    'command:add': [Function: listener]
    },
    _eventsCount: 2,
    rawArgs: [
    '/Users/hjx/.nvm/versions/node/v12.14.1/bin/node',
    '/Users/hjx/Desktop/node-todo-1/index.js',
    'add',
    'aa',
    'bb',
    'cc'
    ],
    args: [ 'aa', 'bb', 'cc' ]
    },
    _description: 'add a task',
    _argsDescription: undefined
    }
    // 你就可以自己领会了
  • index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const program = require('commander');

// 设置它的选项
program
.option('-x, --xxx', 'what the x')

// 设置子命令
program
.command('add <taskName>')
.description('add a task')
.action((firstArg,info) => {
let args = info.parent.args;
console.log(args);
});


program.parse(process.argv);

// node index aa bb cc dd
// 打印 aa bb cc dd

003 修改目录结构

cli.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const program = require('commander');
const api = require('./index.js')
// 设置它的选项
program
.option('-x, --xxx', 'what the x')

// 设置子命令
program
.command('add <taskName>')
.description('add a task')
.action((firstArg,info) => {
let words = info.parent.args;
console.log(words);
api.add(words);
});

program
.command('clear')
.description('clear all tasks')
.action(() => {
console.log('this is clear');
});


program.parse(process.argv);

index.js

1
2
3
module.exports.add = (title)=>{
console.log('add ' + title);
}

运行 node cli add aa bb cc 会显示相应的任务被添加

004 持久化任务内容

  • 但是没有数据库~
  • 使用一个文件吧! 存到用户home目录里 打算存在~/
  • google 搜索 nodejs get home directory
  • 得到答案const homedir = require('os').homedir()
  • 获取用户设置的 home 目录 const home = process.env.HOME

  • 修改 index.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const homedir = require('os').homedir()
    const home = process.env.HOME || homedir;
    module.exports.add = (title)=>{
    console.log('add ' + title);
    // 读取之前的任务
    fs.readFile(dbPath,{});
    // 往里面添加一个任务
    // 存储任务到文件

    }

005 如何拼目录 path

1
2
3
// 专门用来拼路径的 windows 是 \  mac 是 /
const p = require('path');
const dbPath = p.join(目录,目录');

006 利用https://devdocs.io/查fs模块使用

  • 搜索 fs.readFile 查看api
  • 看到 参数里 options 有个 flag 点进去
  • 得到各种模式 a / ax / a+ ...

007 实现创建任务功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const homedir = require('os').homedir()
const home = process.env.HOME || homedir;
// 专门用来拼路径的 windows 是 \ mac 是 /
const p = require('path');
const dbPath = p.join(home,'.todo');
const fs = require('fs');

module.exports.add = (title)=>{
// 读取之前的任务
fs.readFile(dbPath,{flag:'a+'},(error,data)=>{
if(error){
console.log(error)
}else{
let list;
try{
list = JSON.parse(data.toString())
}catch(error2){
list = []
}
const task = {
title: title,
done: false
}
list.push(task);
const string = JSON.stringify(list);
fs.writeFile(dbPath,string + "\n",(error3)=>{
if(error3){
console.log(error3)
}
});
console.log(list)
}
});
// 往里面添加一个任务
// 存储任务到文件
}

007 优化代码 面向接口编程

index.js

1
2
3
4
5
6
7
8
9
10
const db = require('./db.js');

module.exports.add = async (title) =>{
// 读取之前的任务
const list = await db.read();
// 往里面添加一个任务
list.push({title,done:false});
// 存储任务到文件
await db.write(list);
}

db.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const homedir = require('os').homedir()
const home = process.env.HOME || homedir;
// 专门用来拼路径的 windows 是 \ mac 是 /
const p = require('path');
const dbPath = p.join(home,'.todo');
const fs = require('fs');

const db = {
read(path = dbPath){
return new Promise((resolve,reject)=>{
fs.readFile(path,{flag:'a+'},(error,data)=>{
if(error){ return reject(error); }
let list;
try{
list = JSON.parse(data.toString())
}catch(error2){
list = []
}
resolve(list);
});
})
},
write(list, path = dbPath){
return new Promise((resolve,reject)=>{
const string = JSON.stringify(list);
fs.writeFile(path,string + "\n",(error)=>{
if(error){ return reject(error); }
resolve();
});
})

}
}

module.exports = db;

代码仓库

Node全解01_02如何学习Node

如何学习 Node.js

先看提供的 Node.js API

官方API文档

民间版本 https://devdocs.io/

非常推荐

  • 进入之后可以选择 Node.js 某个 LTS版本
  • 搜索功能非常方便
  • 可开启黑色主题
  • 可离线观看

API 到底有哪些

加粗的是重点需要看的

  • Assertion 断言
  • Testing 测试
  • Async Hooks 异步钩子
  • Buffer 一小段缓存
  • Child Processes 子进程
  • Cluster 集群
  • Console
  • Crypto 加密
  • Debugger 调试
  • DNS 获取IP
  • Errors 错误
  • Events 事件
  • File System 文件系统
  • Globals 全局变量
  • HTTP
  • HTTP/2
  • HTTPS
  • Inspector
  • i18n
  • Net
  • OS
  • Path 路径
  • Performance Hooks
  • Process 进程相关
  • Query Strings url处理
  • Readline
  • REPL
  • Report
  • Stream
  • String Decoder
  • Times setTimeout 那类
  • TLS/SSL
  • Trace Events
  • TTY
  • UDP/Datagram
  • URL
  • Utilites
  • V8
  • VM
  • Worker Threads node10之后的
  • Zlib

学习路线

基础 - Web - 框架

  • 先学基础,以任务为导向学习
  • 逐个学习 文件、HTTP、Stream 等模块
  • 在学 Web,学习数据库、AJAX 相关知识
  • 最后学框架,以项目为导向学习
  • 以 Express 为切入点,制作完整的网站

三个约定

  • 写博客,记笔记(可以参考 东京大学笔记法)
  • CRM学习法贯穿整个学习过程
  • 学习调试工具和思路

Node全解01_01Node简介

Node.js是什么

是一个平台

  • 将多种技术组合起来
  • 让JS 也能调用系统接口,开发后端应用

Node.js用到那些技术

  • V8引擎
  • libuv
  • C/C++实现的 c-ares、http-parser、OpenSSL、zlib等库

Node.js的技术架构

1
2
3
4
5
6
Node.js API (http模块 fs stream 等)
-------------------------------------
Node.js bindings | c/c++插件
让JS和 c/c++通讯 | 自定义其他能力
------------------------------------------------------------------
JS引擎 v8 | 跨平台的异步IO能力 libuv | DNS解析|加密解密OpenSSL | 其他。。。

随着Node.js的版本从 0.8升级到 12.11.1 架构也一直变化
如果你想看源代码,推进看 0.10版本
因为这一版使用了很长的时间,而且源代码比最新版少很多

如果你想了解更多,可以看 yjhjstz/deep-into-node

什么是 bindings

背景

  • C/C++实现了一个 http_parser 库,很高效
  • 你只会写JS,但是你想调用这个库
  • 直接调用肯定不行,你需要一个中间的桥梁

bindings

  • Node.js 用C++ 对 http_parser 进行封装,使它符合某些要求,封装的的文件叫做 http_parser_bindings.cpp
  • 用 Node.js提供的编译工具将其编译为 .node文件
  • JS代码可以直接 require 这个 .node文件
  • 这样JS就可以 调用 C++库,中间的桥梁就是 bindings
  • 由于 Node.js 提供了很多 binding, 所以叫做 bindings
  • 这就是 bindings

JS 和 C++ 交互

先看下 0.10版的 Node 依赖了什么

libuv是什么

背景

  • FreeBSD 系统有个 kqueue
  • Linux 有个 epoll
  • Windows 有个 IOCP
  • Ryan 为了一个跨平台的异步IO库,开始写 libuv
  • libuv 会根据系统自动选择合适的方案

功能

  • 可以用于TCP/UDP/DNS 文件等的异步操作

V8是什么

功能

  • 将JS源代码变成本地代码并执行
  • 维护调用栈,确保JS函数的执行顺序
  • 内存管理,为所有对象分配内存
  • 垃圾回收,重复利用无用的内存
  • 实现JS的标准库

注意

  • V8不提供DOM API
  • V8执行 JS 是单线程的, V8本身是多线程的
  • 可以开启两个线程分别执行JS
  • V8本身是包含多个线程的,如垃圾回收为单独线程
  • 自带 eventloop 但 Node.js 基于 libuv 自己做了一个

Event Loop 是什么

什么是 Event

  • setTimeout 的时间到了,就会产生一个事件
  • 文件可以读取了,读取出错了
  • socket 有内容了,关闭了

什么是 Loop

  • loop 就是循环,比如 while(true) 循环
  • 由于事件是分优先级的,所以处理起来也是分先后的
    • setTimeout(f1,100)
    • fs.readFile(‘1.txt’,f2)
    • server.on(‘close’,f3)
  • 所以 Node.js 需要按顺序轮询每种事件
  • 这种轮询往往都是循环的, 1=>2=>3=>1=>2=>3

Event Loop

  • 操作系统可以触发事件,JS可以处理事件
  • Event Loop 就是对事件处理顺序的管理

Event loop

重点阶段

  • times 检查计时器
  • poll 轮询,检查系统事件
  • check 检查 setImmediate 回调
  • 其他阶段可暂时不考虑

注意

  • 大部分时间, Node.js 都停留在 poll 轮询阶段
  • 大部分事件都在 poll 阶段被处理,如文件、网络请求

前面总结过Event loop 的知识点,可参考 Node-JS专精里的Eventloop章节

总结

  • 用 libuv 进行异步I/O操作
  • 用 eventloop 管理事件处理顺序
  • 用 C/C++ 库高效处理 DNS/HTTP …
  • 用 bindings 让 JS 和 C/C++ 沟通
  • 用 V8 运行 JS
  • 用 Node.js 标准库简化 JS 代码
  • 这就是 Node.js

Node-JS专精13_02继承和组合

继承和组合

现在给 Person 添加功能

  • person1.on(‘die’,fn)
  • person1.emit(‘die’,fn)
  • person1.off(‘die’,fn)
  • 让 Person 实例具有发布订阅功能,怎么做

可以加代码

1
2
3
4
5
6
7
8
9
10
11
class Person{
constructor(){}
sayHi()
cache = []
on(){}
off(){}
emit(){}
}

let p1 = new Person()
// 这样 p1 即是 人类 又能发布订阅

除了人还有另一个类,报社

1
2
3
4
5
6
7
8
9
10
11
class 报社{
constructor(){}
print()
cache = []
on(){}
off(){}
emit(){}
}

let 报社1 = new 报社()
// 这样 报社1 即是 报社 又能发布订阅

消除重复

Person 和报社有重复属性

  • 把重复属性提取出来,单独写个类 EventEmitter
  • 然后让 Person 和 报社 继承 EventEmitter

细节

  • constructor 要调用 super()
  • 来保证 EventEmitter 实例被初始化

继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class EventEmitter{
constructor(){}
cache = []
on(){}
off(){}
emit(){}
}

class Person extends EventEmitter {
name
constructor(){ super() }
sayHi(){}
}

class aa extends EventEmitter{
name
constructor(){ super() }
print(){}
}

继承的问题

  • 如果我需要多个功能怎么办?

两个选择

  • 让 EventEmitter 继承其他类 (非常不好,类就不单纯了)
  • 让 Person 继承两个类(多继承,C++ 才支持 ,也不行)

组合

组合没有固定的写法

让 Person 实现发布订阅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person{
eventEmitter = new EventEmitter()
name
sayHi(){}
on(eventName,fn){
this.eventEmitter.on(eventName,fn)
}
off(eventName,fn){
this.eventEmitter.off(eventName,fn)
}
emit(eventName,data){
this.eventEmitter.emit(eventName,data)
}
}

优化代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person{
eventEmitter = new EventEmitter()
name
sayHi(){}
}

let p1 = new Person('aa')

mixin(p1,new EventEmitter())

function mixin(to,from){
for(let key in from){
to[key] = from[key]
}
}
// 注意这里是 简化版的 mixin, 实际会更复杂的

这样你就可以有更多的功能

1
2
3
4
5
6
7
8
9
class Person{
eventEmitter = new EventEmitter()
name
sayHi(){}
}
let p1 = new Person('aa')
mixin(p1,new EventEmitter())
mixin(p1,new Flyer())
mixin(p1,new Killer())

有了组合之后,你可能不需要class了

  • 直接函数 + 闭包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 看到如下类
class robot{
run()
}
class murderRobot extends robot{
kill()
}
class cleaningRobot extends robot{
clean()
}

class animal{
poop()
}
class dog extends animal{
wangwang()
}
class cat extends animal{
miaomiao()
}

需求 狗型杀人机器人 (变态的需求总是不期而遇)

继承很难做到,我们用组合

组合模式来看

  • dog = poop() + wangwang()
  • cat = poop() + miaomiao()
  • cleaningRobot = run() + clean()
  • murderRobot = run() + kill()
  • 狗型杀人机器人 = run() + kill() + wangwang()

不用 class写 dog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const createWang = (state) =>({
wangwang: ()=>{
console.log(`汪汪,我是${state.name}`)
}
})

const createRun = (state) =>({
run: ()=> state.position += 1
})

const createKill = (state) =>({
kill: ()=> {
console.log(`kill`)
}
})

const createDog = (name) =>{
const state = {name,position:0}
return Object.assign(
{},
createWang(state),
createRun(state),
createKill(state)
)
}

const dog = createDog('小白')

这就是组合模式

  • 缺点:写法太灵活,而且这是JS写法,如果是 Java 肯定不这样

结论和共识

  • 组合优于继承
    • 能实现 扁平化的重新分配
    • 而继承 是一个垂直方向的,如果你的需求是两个垂直线交叉 很难做!

使用场景

什么情况用继承

  • 开发者不会用组合
  • 功能单一,没太多交叉的地方,一眼能看出继承关系
  • 前端库比较喜欢用继承

举例

  • class App extends React.component{…}
  • React.Component 内置了方便的复用代码(钩子)

什么情况用组合

  • 功能组合非常灵活,无法一眼看出继承关系
  • 插件模式
  • mixin 模式

举例

  • Vue.mixin()
  • Vue.use(plugin)
  • Vue 的 install 功能
  • React接受组件的组件
  • const vm = new Vue({…})
    • vm.$on / vm.$off / vm.$emit
  • Vue 组合了 EventEmiter

什么情况用这些

  • 基础库用继承
  • 业务开发使用组合
  • 不一定是对的 灵活选择

组合的内存性能问题

  • 结论就是:实际上没多大区别

之前的 狗型杀人机器人

1
2
3
4
5
const dog = createDog('小白')
const d1 = createDog('a')
const d2 = createDog('b')

d1.run === d2.run // false

解决方案:通过一些技巧优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const f1 = ()=>{}
createF1 = (state)=>({f1:f1})
const f2 = ()=>{}
createF2 = (state)=>({f2:f2})

createDog = (name)=>{
const state = {name}
return Object.assign({},createF1(),createF2())
}

d1 = createDog(1)
d2 = createDog(2)

d1.f1 === d2.f1 // true

在看继承:实际也不省空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A{
sayA(){}
}

class B extends A{
name
constructor(name){
super()
this.name = name
}
sayB(){}
}

// 实际就是原型链
// 但是 构建B的过程 要先构建 A 多了一层原型 所以也并不是非常的省内存

Node-JS专精13_01类

类知识简介

为什么有类

  • 不同对象的属性重复了,就有类

为什么有继承

  • 不同的类的属性重复类,就有继承

大部分编程技巧,都是为了解决重复

对象的属性重复了

  • 两个对象的属性重复
1
2
let p1 = {name:'aa',age:18,sayHi(){}}
let p2 = {name:'bb',age:18,sayHi(){}}

于是有了类和构造函数

1
2
3
4
5
6
7
8
9
10
11
12
class Pserson {
name
age
sayHi(){}
constructor(name,age){
this.name = name
this.age = age
}
}

let p1 = new Person('aa',18)
let p2 = new Person('bb',18)

改写成TS

1
2
3
4
5
6
7
class Person{
sayHi(): void {}
constructor(public name: string, public age: number){}
}

let p1 = new Person('aa',18)
let p2 = new Person('bb',18)

总结

  • 类就是把对象的属性提前写好,避免重复
  • 类里面的字段会变成对象的属性
  • 为了节约内存,所有函数都是 共用的
  • 而非函数属性是各个对象自有的
  • 使用 console.dir 可以看出来

构造函数

  • 属性名虽然可以提前写好,但是属性值不行
  • 所以需要构造函数接受参数,初始化属性值
  • 构造函数不需要写 return , 默认会 return 新对象

语法

  • JS的所有 class 语法可以在 MDN上看
  • TS的所有 class 语法可以在 TS英文/中文 官网看

不想所有函数都是共用的

  • 写成箭头函数
1
2
3
4
class Pserson {
sayHi(){} // 共用
myFn = () => {} // 自用
}

这玩意除了浪费内存还有啥用?

  • 你必须onclick 那里bind(this)否则报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from "react";
import ReactDOM from "react-dom";

class App extends React.Component {
name = "Frank";
sayHi(){
console.log(this);
console.log(`Hi, I'm ${this.name}`);
};
render() {
return (
<div>
<button onClick={this.sayHi.bind(this)}>say hi</button>
</div>
);
}
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
  • 而如果是各自的函数,就可以直接调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from "react";
import ReactDOM from "react-dom";

class App extends React.Component {
name = "Frank";
sayHi = () => {
console.log(this);
console.log(`Hi, I'm ${this.name}`);
};
render() {
return (
<div>
<button onClick={this.sayHi}>say hi</button>
</div>
);
}
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);