Node.js 介绍

node.js 是什么?

  1. node.js 是一个开发平台,就像Java开发平台、.Net开发平台、PHP开发平台、Apple开发平台一样。
  • 何为开发平台?有对应的编程语言、有语言运行时、有能实现特定功能的API(SDK:Software Development Kit)
  1. 该平台使用的编程语言是 JavaScript 语言。
  2. node.js 平台是基于 Chrome V8 JavaScript 引擎构建。
  3. 基于 node.js 可以开发控制台程序(命令行程序、CLI程序)、桌面应用程序(GUI)(借助 node-webkit、electron 等框架实现)、Web 应用程序(网站)

PHP开发技术栈: LAMP - Linux Apache MySQL PHP

node.js 全栈开发技术栈: MEAN - MongoDB Express Angular Node.js

node.js 有哪些特点?

  1. 事件驱动(当事件被触发时,执行传递过去的回调函数)
  2. 非阻塞 I/O 模型(当执行I/O操作时,不会阻塞线程)
  3. 单线程
  4. 拥有世界最大的开源库生态系统 —— npm。

node.js 网站

  1. node.js官方网站
  2. node.js中文网
  3. node.js 中文社区

为什么要学习Node.js?

  1. 通过学习Node.js开发深入理解服务器开发Web请求和响应过程了解服务器端如何与客户端配合
  2. 学习服务器端渲染
  3. 学习服务器端为客户端编写接口
  4. 现在前端工程师面试,对 Node.js 开发有要求
  5. 补充提问:
  • 在Node.js平台开发时,能使用Dom API吗?比如:document.getElementById('id'); window.location 等?
  1. 复习 浏览器端 JavaScript 组成:ECMAscript、Dom、Bom

Node.js安装和配置

  1. 下载地址
  1. 官网术语解释
  • LTS 版本:Long-term Support 版本,长期支持版,即稳定版。
  • Current 版本:Latest Features 版本,最新版本,新特性会在该版本中最先加入。
  1. 注意:
  • 安装完毕后通过命令:node -v来确定是否安装成功【注意:打开”命令窗口”的时候建议使用”管理员方式”打开】
  • 如果需要则配置环境变量。

nvm 管理node版本

nvm (Linux、Unix、OS X)

nvm-windows (Windows)

  • https://github.com/coreybutler/nvm-windows

  • 常用命令:

    • nvm version
    • nvm install latest
    • nvm install 版本号
    • nvm uninstall 版本号
    • nvm list
    • nvm use 版本号

    在安装 node 环境前,先安装 nvm ,通过 nvm 安装node,可以管理 node。因为你可能会想使用多个版本的 node

node代码简单操作文件

在使用 node api 查询文档

Node 中文网 Node 英文网

  1. 读写操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 加载 fs 模块
var fs = require('fs');
// 读取文件
fs.readFile('./test.txt',function(err,data){
// 如果读取成功,err为null. 如果读取失败,err有内容
if(err){
// 读取失败
}else{
// 读取成功
}
})

// 文件写入操作

var msg = 'Hello China. 你好,中国。';
fs.writeFile('./test.txt', msg, 'utf8', function (err) {
if (err) {
console.log('文件写入失败,详细错误信息:' + err);
} else {
console.log('文件写入成功。');
}
});

文档writefile

  1. 创建文件夹:mkdir
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 导入 fs 模块
const fs = require('fs')
const path = require('path')
// 当前文件路径

let mkdirname = ['资料代码','课程代码','ppt演示','图片演示']

mkdirname.forEach(i => {
const dirname = path.join(__dirname,i)
fs.mkdir(dirname,err => {
if(err){
console.log('目前创建失败:'+err);
}
else{
console.log('创建成功');
}
})
});

获取路径 & 拼接路径

  1. 在读取文件等操作,如果自己写路径,在不同的操作系统,路径不一样,因此直接通过全局获取,就很便捷
1
2
3
// node 定义了一些全局的方法,例如 __dirname,__filename
console.log(__dirname) // 打印当前文件所在的路径下
console.log(__filename) // 打印该文件的路径
  1. 拼接路径的方法,需要先导入模块
1
2
3
4
var path = require('path')
// path 下有一个方法 join 用来拼接
var currentfilepath = path.join(__dirname,'public')
// 这样就可以获取当前 public 的目录,一些静态资源都可以放里面
  1. **__dirname __filename 并不是全局的 **

在执行 node app.js 这个文件时,会传入两个值

1
2
3
4
5
// 闭包的形式
(function(__dirname,__filename){
//代码体
})(路径,文件路径)
// 因此我们可以使用这两个并不是全局属性的值

REPL介绍

  1. REPL 全称: Read-Eval-Print-Loop(交互式解释器)
  • R 读取 - 读取用户输入,解析输入了Javascript 数据结构并存储在内存中。
  • E 执行 - 执行输入的数据结构
  • P 打印 - 输出结果
  • L 循环 - 循环操作以上步骤直到用户两次按下 ctrl-c 按钮退出。
  1. 在REPL中编写程序 (类似于浏览器开发人员工具中的控制台功能)
  • 直接在控制台输入 node 命令进入 REPL 环境
  1. 按两次 Control + C 退出REPL界面 或者 输入 .exit 退出 REPL 界面
  • 按住 control 键不要放开, 然后按两下 c 键

最简单的 http 服务程序

  1. 简单的请求服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 导入 http 模块
var http = require('http')

// 创建服务器
var app = http.createServer(function(requset,respont){
console.log('在访问...')
res.setHeader('Content-Type', 'text/html;charset=utf-8');
res.write('哈哈哈 <h1>hello</h1>');
res.write('<strong>你好 Node.js</strong>');
res.end(); // 结束请求
})

// 监听端口
app.listen(8080,()=>{
console.log('服务器启动:http://localhost:8080')
})
  1. 根据不同的路径,访问不同的页面
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
// 1. 导入 http 模块
const http = require('http')
const fs = require('fs')
const path = require('path')

http.createServer((req,res) => {
// 2. 定义方法
function readData(pathfile){
fs.readFile(path.join(__dirname,'htmls',pathfile),(err,data) => {
if(err){
throw err
}

// 把读取的数据,发送给浏览器解析
// data的是字节,buffer类型,end里面可以传入buffer类型
res.end(data)
})
}

// 3. 判断路径
if(req.url === '/' || req.url === '/index') {
readData('index.html')
}else if(req.url === '/login'){
readData('login.html')
}else if(req.url === '/list'){
readData('list.html')
}else if(req.url === '/register'){
readData('register.html')
}else{
// 404
readData('nofound.html')
}

}).listen(8080,()=>{
console.log('服务器启动');
console.log('http://localhost:8080');
})
  1. 使用 mime 第三方库,来获取文件类型
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
// 1. 导入 http
var http = require('http')
var path = require('path')
var fs = require('fs')
var mime = require('mime')

http.createServer(function(req,res){
// 2. 获取当前路径
var publicDir = path.join(__dirname,'public')
var filename = path.join(publicDir,req.url)

// 3. 判断一下,当前路径 localhost/ 或者 localhost/index
if(req.url === '/' || req.url === '/index'){
filename = path.join(publicDir,'index.html')
}
// 4. 读取文件
fs.readFile(filename,(err,data) =>{
if(err){
res.setHeader('Content-Type','text/html;charset=utf-8')
res.end('文件不存在')
}
else{
// 5. 设置读取文件类型,使用第三方模块 mime
res.setHeader('Content-Type',mime.getType(filename))

// 6. 返回数据data
res.end(data)
}
})

}).listen(9080,()=>{
console.log('http://localhost:9080');
})


Buffer 类型

一. 类型介绍

  1. JavaScript 语言没有读取或操作二进制数据流的机制。
  2. Node.js 中引入了 Buffer 类型使我们可以操作 TCP流 或 文件流。
  3. Buffer 类型的对象类似于整数数组,但 Buffer 的大小是固定的、且在 V8 堆外分配物理内存。 Buffer 的大小在被创建时确定,且无法调整。( buf.length 是固定的,不允许修改 )
  4. Buffer 是全局的,所以使用的时候无需 require() 的方式来加载

二. 如何创建一个 Buffer 对象

常见的 API 介绍

  1. 创建一个 Buffer 对象
1
2
3
4
5
6
7
8
9
10
11
12
// 1. 通过 Buffer.from() 创建一个 Buffer 对象

// 1.1 通过一个字节数组来创建一个 Buffer 对象
var array = [0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0xe4, 0xb8, 0x96, 0xe7, 0x95, 0x8c];
var buf = Buffer.from(array);
console.log(buf.toString('utf8'));

// 1.2 通过字符串来创建一个 Buffer 对象
// Buffer.from(string[, encoding])
var buf = Buffer.from('你好世界! Hello World!~');
console.log(buf);
console.log(buf.toString());
  1. 拼接多个 Buffer 对象为一个对象
1
2
3
// Buffer.concat(list[, totalLength])
var bufferList = [];
var buf = Buffer.concat(bufferList);
  1. 获取字符串对应的字节个数
1
2
3
4
// Buffer.byteLength(string[, encoding])

var len = Buffer.byteLength('你好世界Hello', 'utf8');
console.log(len);
  1. 判断一个对象是否是 Buffer 类型对象
1
2
3
4
// Buffer.isBuffer(obj)
// obj <Object>
// Returns: <boolean>
// Returns true if obj is a Buffer, false otherwise.
  1. 获取 Buffer 中的某个字节
1
2
// 根据索引获取 Buffer 中的某个字节(byte、octet)
// buf[index]
  1. 获取 Buffer 对象中的字节的个数
1
2
// buf.length
// 注意:length 属性不可修改
  1. 已过时的 API
1
2
3
4
5
6
7
// 以下 API 已全部过时
new Buffer(array)
new Buffer(buffer)
new Buffer(arrayBuffer[, byteOffset [, length]])
new Buffer(size)
new Buffer(string[, encoding])

三. Buffer 对象与编码

Node.js 目前支持的编码如下:

  1. ascii
  2. utf8
  3. utf16le
  • ucs2 是 utf16le 的别名
  1. base64
  2. latin1
  • binary 是 latin1 的别名
  1. hex
  • 用两位 16 进制来表示每个字节

示例代码:

1
2
3
4
5
var buf = Buffer.from('你好世界,Hello World!', 'utf8');

console.log(buf.toString('hex'));
console.log(buf.toString('base64'));
console.log(buf.toString('utf8'));

四、思考:为什么会有 Buffer 类型?

  1. Buffer 使用来临时存储一些数据(二进制数据)
  2. 当我们要把一大块数据从一个地方传输到另外一个地方的时候可以通过 Buffer 对象进行传输
  3. 通过 Buffer 每次可以传输小部分数据,直到所有数据都传输完毕。

五、补充

  1. Stream

  2. Writable Stream

  • 允许 node.js 写数据到流中
  1. Readable Stream
  • 允许 node.js 从流中读取数据



Node 服务端 HTTP

模拟Apache服务器

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
// 导入模块
var http = require('http');
var path = require('path');
var fs = require('fs');

// 加载 mime 模块,该模块作用就是根据用户请求的资源的后缀名,返回对应的 content-type 类型 mime 类型
var mime = require('mime');

var server = http.createServer(function (req, res) {

// 读取磁盘上的静态资源文件
fs.readFile(path.join(__dirname, 'public', req.url), function (err, data) {

if (err && err.code === 'ENOENT') {
res.end('404, not found.');
return;
}

console.log(mime.lookup(req.url));
// 把读取到的内容返回给用户
res.setHeader('Content-Type', mime.lookup(req.url));
res.end(data);
});
});

server.listen(9002, function () {
console.log('http://localhost:9002');
});

setHeader & writeHead 区别

  1. response.writeHead(statusCode [, statusMessage] [, headers])

发送一个响应头给请求。 状态码是一个三位数的 HTTP 状态码,如 404。 最后一个参数 headers 是响应头。 第二个参数 statusMessage 是可选的状态描述。

该方法在消息中只能被调用一次,且必须在 response.end() 被调用之前调用。

response.setHeader()设置的响应头会与 response.writeHead() 设置的响应头合并,且 response.writeHead() 的优先。

  1. request.setHeader(name, value)

为 headers 对象设置一个单一的 header 值。如果该 header 已经存在了,则将会被替换。

1
2
3
4
5
// 请求静态资源,不同的后缀需要使用相应的报文头
// 导入 mime 框架,去根据请求文件后缀,获取相应的报文头
var mime = require('mime')
res.setHeader('Content-Type',mime.getType(req.url))
// 使用 setHeader 发送给服务器报文头

Modules和Packages区别

  1. A module is any file or directory that can be loaded by Node.js’ require().
  • 模块可以是任何一个文件或目录(目录下可以有很多个文件),只要能被node.js通过require()即可。
  1. A package is a file or directory that is described by a package.json. This can happen in a bunch of different ways!
  • 包是一个文件或目录(目录下可以有多个文件)必须有一个package.json文件来描述,就可以是一个包。

node.js 错误调试:

  1. 当开启服务后,在浏览器中输入地址,如果出现浏览问题,首先要先看 服务器控制台是否报错。如果报错,直接根据服务器报错进行排错。

  2. 打开浏览器开发者工具中的 “网络” 部分,查看请求是否成功发出去了

  • 看一下请求报文是不是和我们想的一样
  • 响应状态码

npm 介绍

  • npm(全称Node Package Manager,即node包管理器)是Node.js默认的、以JavaScript编写的软件包管理系统。
  • npm 官方网站
  • npm 官方文档
  1. npm 和 node.js
  • npm是Node.js默认的软件包管理系统。安装完毕node后,会默认安装好npm
  • npm本身也是基于Node.js开发的包(软件)
  1. NPM 使用

  2. https://www.npmjs.com/ 网站找到需要的包

  3. 在项目的根目录下,执行npm install 包名称安装

  4. 在node.js代码中通过 require('包名'); 加载该模块

  5. 注意:通过npm install 包名安装的包,会自动下载到当前目录下的node_modules目录下,如果该目录不存在,则创建,如果已存在则直接下载进去。

  6. 在代码中通过 require('包名'); 加载该模块

上面说的这种方式叫做 本地安装。

  1. npm常用命令介绍

  2. install,安装包。npm install 包名

  3. uninstall,卸载包。·npm uninstall 包名`

  4. version,查看当前npm版本。npm versionnpm -v

  5. init,创建一个package.json文件。npm init

  6. 注意:当使用 npm init -y 的时候,如果当前文件夹(目录)的名字比较怪(有大写、有中文等等)就会影响npm init -y 的一步生成操作,此时需要 npm init 根据向导来生成

request 对象 和 response对象

request 对象

  • request 对象类型 <http.IncomingMessage>, 继承自stream.Readable

  • request 对象常用成员

    • request.headers
    • request.rawHeaders
    • request.httpVersion
    • request.method
    • request.url

response 对象

  • response 对象类型 <http.ServerResponse>

  • response 对象常用成员

    • response.writeHead(statusCode[, statusMessage][, headers])

      1. This method must only be called once on a message and it must be called before response.end() is called.
      • 这个方法在每次请求响应前都必须被调用(只能调用一次)。并且必须在end()方法调用前调用
2. If you call response.write() or response.end() before calling this, the implicit/mutable headers will be calculated and call this function for you.
- 如果在调用writeHead()方法之前调用了write() 或 end()方法,系统会自动帮你调用writeHead()方法,并且会生成默认的响应头



3. When headers have been set with response.setHeader(), they will be merged with any headers passed to response.writeHead(), with the headers passed to response.writeHead() given precedence.
- 如果通过 res.setHeader() 也设置了响应头,那么系统会将serHeader()设置的响应头和writeHead()设置的响应头合并。 并且writeHead()的设置优先
1
2
3
4
5
// 示例代码:
res.writeHead(200, 'OK', {
'Content-Type': 'text/html; charset=utf-8',
'Content-Length': Buffer.byteLength(msg)
});
  • response.write(chunk[, encoding][, callback])

    • 参数1:要写入的数据,可以是字符串或二进制数据,必填
    • 参数2:编码,默认是utf8,选填。
    • 参数3:回调函数,选填。
  • response.end([data][, encoding][, callback])

    • 结束响应。
    • This method signals to the server that all of the response headers and body have been sent; that server should consider this message complete. The method, response.end(), MUST be called on each response.
    • res.end()这个方法告诉服务器所有要发送的响应头和响应体都发送完毕了。可以人为这次响应结束了。
    • 同时每次响应都必须调用该方法,用来结束响应
* 参数1:结束响应前要发送的数据,选填。
* 参数2:编码,选填。
* 参数3:回调函数,选填。
  • response.setHeader(name, value)

    • 设置响应报文头
  • response.statusCode

    • 设置或读取http响应码
  • response.statusMessage

    • 设置或读取http响应状态消息

Node 初建服务器 HackerNews

HackerNews 源码,可以下载下来分析源码

封装文件读取方法,挂载res的protype上

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
// 封装读取方法
function render(filename,res){
// filename是读取文件的路径
fs.readFile(filename,function(err,data){
if(err){
res.writeHead(404,'Not Found',{
'Content-Type':'text/html;charset=utf-8'
})
return
}
res.setHeader('Content-Type',mime.getType(filename))
})
}

// 封装
res.render = function(filename){
// 不需要传入 res 对象,可以直接用
fs.readFile(filename,function(err,data){
if(err){
res.writeHead(404,'Not Found',{
'Content-Type':'text/html;charset=utf-8'
})
return
}
res.setHeader('Content-Type',mime.getType(filename))
})
}

Get请求数据,保存数据到data.json

在启动服务时,需要提前创建好 data 文件夹,writeFile 不会去创建文件夹。服务器重新启动,会重新创建 data.json 文件,会覆盖原来的。

用定义的数组去存放对象,将数组通过 JSON.stringify() 转化成string类型

数组list会覆盖,因此不是每次是定义,而是去读取data.json文件

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
var http = require("http");
// 4. 拼接路径 path 模块
var path = require("path");

// 3. 导入 fs 模块,去写入数据
var fs = require("fs");
var list = [];

// 2. 加载 node 内置的 url 模块
var url = require("url");
http
.createServer(function (req, res) {
// 1.在from表单下,请求地址:action='/add',请求方式 method="get"
// app.js 去获取请求的数据
// 伪代码
//if(req.url === '/add' && req.method === 'get') //请求的路径会携带参数,因此路径不会等于 /add
// 使用 startsWith 去截取 '/add' 存在返回 true
if (req.url.startsWith('/add') && req.method === 'GET') {
// req.url 可以获取一条包含数据的字符串,通过 url 模块可以解析出来
var urlObj = url.parse(req.url, true); // 数据就是query,但是仍然是字符串,加上第二个参数 true,就解析成对象
// 数据保存到数组里面
list.push(urlObj.query);
//把list 写入 data.json 文件
// writeFile 写入的数据类型是 string || buffer
console.log(urlObj);
fs.writeFile(
path.join(__dirname, "data", "data.json"),
JSON.stringify(list),
function (err) {
if (err) {
throw err;
}
console.log("ok");
res.end();
}
);
}else{
res.end("404, Page Not Found.");
}
})
.listen(8080, () => {
console.log("http://localhost:8080");
});

修改保存数据的代码

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
// 1. 导入模块
var http = require("http");
var path = require("path");
var fs = require("fs");
var url = require("url");

http.createServer(function (req, res){
// 2. startsWith 内容包含 '/add' 返回true 且请求方法是 GET
if (req.url.startsWith("/add") && req.method === "GET"){
var urlObj = url.parse(req.url, true) // url.parse 解析字符串路径,第二次参数,确定解析成对象

// 3. 防止数据 data.json 被覆盖,需要去读取文件
var filename = path.join(__dirname,'data','data.json')
fs.readFile(filename,'utf-8',function(err,data){
// 使用 utf-8 格式,不然 data 就是 buffer类型
if(err && err.code !== 'ENOENT'){
throw err
} // 如果没有 data.json 文件,那么直接条件是err 就会异常。
// 将没有文件的错误设置为条件,不会因此抛出异常
var list = JSON.parse(data || '[]') // 没有data文件就 underfind,
list.push(urlObj.query); // 插入新数据
// 4. 写入数据
// writeFile 写入是数据类型,是 string 和 buffer 类型,否则不行
// JSON.stringify(list) 将数据转化string类型
fs.writeFile(filename,JSON.stringify(list),function(err){
if(err){
throw err
}

// 设置状态码 和 消息
res.statusCode = 302
res.statusMessage = 'Found'
res.setHeader('Location','/')
// 结束
res.end()
})
})
}else{
res.end('Not Found Page')
}
}).listen(8080,function(){
console.log('http://localhost:8080')
})

重定向操作

服务器通过设置 http 响应报文头实现浏览器重定向操作。当提交数据后,我们肯定需要将网站跳转到其他页面去

1
2
3
4
5
6
7
8
9
10
11
12
13
// 伪代码
function (err) {
if (err) {
throw err;
}
console.log("ok");
// 当页面成功,需要跳转时
// 重定向 回到初始页
res.statusCode = 302 // 设置状态码
res.statusMessage = 'Found' // 消息
res.setHeader('Location','/')
res.end(); // 结束响应
}

实现 POST 请求

读取数据 data.json,和写入数据操作都和 get 请求一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var querystring = require('querystring')

// POST 请求
// 1. 获取用户 post 提交的数据,由于 post 提交数据时,可能分多次提交。因此需要去合并每次提交的数据
// 当执行了 res.end() 表示提交了全部数据
// 声明一个数组,去存储提交的数据
var array = []
// 2. 监听 data 事件,每次提交的数据就是 chunk 参数
req.on('data',function( chunk){
// chunk 参数是buffer对象
array.push(chunk)
})

// 3. 监听 end 事件
req.on('end',function(){
var postBody = Buffer.concat(array) // 合并 buffer 对象
postBody = postBody.toString('utf-8') // 将buffer转化成字符串对象

// 4. 需要导入 querystring 模块,将字符串对象转化成 json 对象
postBody = querystring.parse(postBody) // JSON 对象
res.end('over')
})

渲染页面,替换数据

使用 underscore 模块,中文网https://underscorejs.net/

Underscore是一个JavaScript实用库,提供了一整套函数式编程的实用功能,但是没有扩展任何JavaScript内置对象。它是这个问题的答案:“如果我在一个空白的HTML页面前坐下, 并希望立即开始工作, 我需要什么?“…它弥补了部分jQuery没有实现的功能,同时又是Backbone.js必不可少的部分。

1. 安装

1
npm install underscore

2.引入

1
var _ = require('underscore') // 以下划线去命名

3.运用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var _ = require('underscore')

// 演示
var names = ['张三','李四','老五']
var ages = [18,20,45]
var genders = ['男','女','女']

// 压缩
var result = _.zip(names,ages,genders)
console.log(result)

// 解压
result = _.unzip(result)
console.log(result)

1
2
3
4
5
6
7
8
9
10
11
12
// 模板替换
var _ = require('underscore')

// 声明了一段代码模板的 HTML 文档
var html = '<h2><%= name %></h2>'

// template() 函数的返回是一个函数
var fn = _.template(html)

// fn 接收一个数据对象,并用该数据对象,将 html 中的模板内容替换,生成最终的 html 代码
var html = fn({name: 'Simplelife'})
console.log(html)

<%%> html数据绑定

1
2
3
4
5
6
<!-- 伪代码 示例 -->
<% for (var i = 0; i < list.length; i++) { %>
<a href="/item?id=<%= list[i].id %>">
<%= list[i].title %>
</a>
<% } %>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// app.js
// 封装的 render 函数,增加第二参数 tplData 模板数据
res.render = function(filename, tplData) {

fs.readFile(filename, function(err, data) {
if (err) {
res.writeHead(404, 'Not Found', { 'Content-Type': 'text/html;charset=utf-8' });
res.end('404, not found.');
return;
}
// 因为有些解析的资源是不需要替换数据的,如解析照片……
// 如果用户传递了模板数据,那么就使用 underscore 的 template 方法进行替换
// 如果用户没有传递 模板数据,那么就不进行替换
if (tplData) {
// 如果用户传递了模板数据,表示要进行模板替换
var fn = _.template(data.toString('utf8'));
data = fn(tplData);
}

res.setHeader('Content-Type', mime.getType(filename));
res.end(data);
});
};
1
2
3
4
5
6
// underscore模板语法
if (tplData) {
// 如果用户传递了模板数据,表示要进行模板替换
var fn = _.template(data.toString('utf8'));
data = fn(tplData);
}

当渲染文件时,传入模板数据,就进行这代码。将页面数据转成字符串 data.toString(‘utf-8’) 的形式,返回的是函数的形式。将实参 tplData 传入函数,需要去替换数据,得到新的 data。

封装函数

1.封装读取文件函数

不能通过 return 去返回异步操作的内容。假如这样做,当进行异步操作开始就会返回,显然值还没有得出。如果在 function(err,data) 函数里面返回,虽然可以得到值,但是无法获取

1
2
3
4
5
6
7
8
9
10
11
12
13
function readNewsData(callback){
fs.readFile(path.join(__dirname,'data','data.json'),'utf8',function(err,data){
if(err && err.code !== 'ENOENT'){
throw err;
}
var list = JSON.parse(data || '[]');
// 通过调用回调函数 callback() 将读取到的数据 list,传递出去
callback(list)

// 1. return list 无法获取值
})
// 2. return list 无值
}

node.js 模块的分类

1.核心模块

Core Module、内置模块、原生模块

fs、http、path、url,加载模块是一个同步的过程,加载完一个加载下一个。

2.文件模块

按文件后缀来分,如果加载时,没有指定后缀名,那么就按照如下顺序依次加载相应的模块

  1. .js
  2. .json
  3. .node(C/C++编写的模块)

3.自定义模块

  1. mime
  2. cheerio
  3. moment
  4. mongo

加载模块,会去一层一层去找 node_modules,向C跟磁盘

require 加载模块过程

  • 看 require() 加载模块时传入的参数是否以 ‘./‘ 或 ‘../‘ 或 ‘/‘ 等等这样的路径方式开头(相对路径或绝对路径都可以)
  • 是,那么会按照传入的路径直接去查询对应的模块。
  • 传入的是否为具体的文件名

    • require(‘./test.js’) 是具体的文件名

      • 直接根据给定的路径去加载模块,找到了加载成功,找不到加载失败
    • require(‘./test’); 不是具体的文件名、

      • 第一步:根据给定的路径,依次添加文件后缀 .js、.json、.node进行匹配,如果找不到匹配执行第二步
      • 第二步:查找是否有 test 目录(尝试找 test 包)
        • 找不到:加载失败
        • 找到了:依次在 test 目录下查找 package.json 文件(找到该文件后尝试找 main 字段中的入口文件)、index.js、index.json、index.node,找不到则加载失败
  1. 不是,那么就认为传入的是 “模块名称”(比如:require(‘http’)、require(‘mime’))
  • 是核心模块:直接加载核心模块
  • 不是核心模块
    • 依次递归查找 node_modules 目录中是否有相应的包
      • 从当前目录开始,依次递归查找所有父目录下的 node_modules 目录中是否包含相应的包
      • 如果查找完毕磁盘根目录依然没有则加载失败
      • 打印输入 module.paths 查看
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// require('http')
// require('mime')

// 情况一:require() 的参数是一个路径
require('./index2.js')

// index2.js
// index2.json
// index2.node
// index2 文件夹 -> package.json -> main(入口文件 app.js -> index.js/index.json/index.node) -> 加载失败
require('ndex2')

// 情况二: require() 的参数不是路径,直接就是一个模块名称
// 1. 先在核心模块中查找,是否有和给定的名字一样的模块。如果有,则直接加载该核心模块。
// require('http')

// 2. 如果核心模块中没有该模块那么就会认为这个模块是一个第三方模块(自定义模块)
// 先会去当前js文件所在的目录下去找是否一个一个 node_modules 文件夹
// require('mime')

require 加载模块注意点

  1. 所有模块第一次加载完毕后都会有 缓存,二次加载直接读取缓存,避免了二次开销
  • 因为有 缓存,所以模块中的代码只在第一次加载的时候执行一次
  1. 每次加载模块的时候都优先从缓存中加载,缓存中没有的情况下才会按照 node.js 加载模块的规则去查找

  2. 核心模块在 Node.js 源码编译的时候,都已经编译为二进制执行文件,所以加载速度较快(核心模块加载的优先级仅次于 缓存加载)

  3. 核心模块都保存在 lib 目录下

  4. 试图加载一个和 核心模块 同名的 自定义模块(第三方模块)是不会成功的

  • 自定义模块要么名字不要与核心模块同名
  • 要么使用路径的方式加载
  1. 核心模块 只能通过 模块名称 来加载(错误示例:require(‘./http’); 这样是无法加载 核心模块 http的 )
  2. require() 加载模块使用 ./ 相对路径时,相对路径是相对当前模块,不受执行 node 命令的路径影响
  3. 建议加载文件模块的时候始终添加文件后缀名,不要省略。

补充CommonJS规范

  1. CommonJS 规范
  2. 模块的定义
  3. 总结:CommonJS 是为 JavaScript 语言制定的一种 模块规范、编程 API规范
  4. node.js 遵循了 CommonJS规范

1.模块通讯

1
2
3
4
5
6
7
8
9
// 加载 foo 模块
var foo = require('./foo.js')

// 导出 foo 模块
// foo.js
module.exports = {
name:function(){},
age:18 // 键值对的形式
}

2.module.exports 和 exports 暴露属性的区别

它们在栈里指向对象地址是同一个。

1
2
3
module.exports.name = '李四' //添加name属性

exports.age = 18 //添加 age 属性
1
2
3
4
此时暴露的对象{
name: '李四',
age:18
}

当改变 module.exports 的对象地址,因为 require 导入最终是 module.exports,因此 exports 导入最终地址也改变

PS:假如最终 exports 指向新的对象地址,最终还是以 module.exports 为准,它才是最终返回的

模块化开发思想,封装模块过程

将功能进行模块化封装,开发速度更开,维护成本也低,后期扩展更方便

模块化封装好的源码:HackerNews 源码,可以下载下来分析源码

模块分类

  1. 模块一(服务模块):负责启动服务
  2. 模块二(扩展模块):负责扩展 req 和 res 对象,为 req 和 res 增加以下更好用的API
  3. 模块三(路由模块):负责路由判断
  4. 模块四(业务模块):负责处理具体路由的业务代码
  5. 模块五(数据操作模块):负责进行数据库操作
  6. 模块六(配置模块):负责报错各种项目中用到的配置信息

PS:抽离代码,服务模块最后写,因为写好了其他模块,自然就写好了服务模块

步骤:

  1. 思考,该模块中要封装什么代码?

  2. 思考,这些代码有用到外部的数据吗?如果用到了,是否需要通过参数将这些数据传递到当前模块中

  3. 当前模块对外需要暴露的东西(module.exports的值)


模块二:context.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
// 导入需要的模块
var url = require('url');
var fs = require('fs');
var mime = require('mime');
var _ = require('underscore');

// 模块暴露的函数,需要从外面传递 req 和 res 参数
module.exports = function(req,res){
var urlObj = url.parse(req.url.toLowerCase(),true)
// 1. 为 req 添加 query 属性
req.query = urlObj.query
// 2. 为 req 添加 pathname 属性
req.pathname = urlObj.pathname
// 3. 把请求方法转换成小写
req.method = req.method.toLowerCase()

// 4. 给 res 添加 render 函数. filename 路径 tplData 替换<%%>内的数据
res.render = function(filename,tplData){
fs.readFile(filename,function(err,data){
if(err){
res.writeHead(404,'Not Found',{'Content-Type':'text/html;charset=utf-8'})
res.end('404,没找到页面')
return
}
// 如果用户传递了模板数据,表示要进行替换,使用 underscore 的 template 方法
if(tplData){
var fn = _.template(data.toString('utf8'))
data = fn(tplData)
}
res.setHeader('Content-Type',mime.getType(filename))
res.end(data)
})
}
}

模块三:router.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 加载 handler.js 模块 业务模块
var handler = require('./handler.js')

// 导出模块
module.exports = function(req,res){
// 先根据用户请求的路径(路由),将相应的HTML页面显示出来
if(req.pathname === '/' && req.method === 'get'){
handler.index(req,res)
} else if (req.pathname === '/submit' && req.method === 'get'){
handler.submit(req,res)
} else if (req.pathname === '/item' && req.method === 'get'){
handler.item(req,res)
} else if (req.pathname === '/add' && req.method === 'get'){
handler.addGet(req,res)
} else if (req.url === '/add' && req.method === 'post'){
handler.addPost(req,res)
} else if (req.url.startsWith('/resources') && req.method === 'get'){
handler.static(req,res)
} else {
handler.handleErrors(req,res)
}
}

模块四:handler.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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
// 导入模块
var fs = require('fs');
var path = require('path');
var querystring = require('querystring');
var config = require('./config.js');

// 1. 处理请求 '/' 和 '/index' 的业务方法
module.exports.index = function(req, res) {
// 1. 读取 data.json 文件中的数据,并将读取到的数据转换为 list 数组
readNewsData(function(list) {
// 2. 在服务器端使用模板引擎,将 list 中的数据和 index.html 文件中的内容结合 渲染给客户端
res.render(path.join(__dirname, 'views', 'index.html'), { list: list });
});
};

// 2. 处理请求 '/submit' 的业务方法
module.exports.submit = function(req,res){
// 读取 submit.html 返回
res.render(path.join(__dirname,'views','submit.html'))
}

// 3. 处理请求 '/item' 的业务方法
module.exports.item = function(req,res){
// 获取当前用户请求的 id
// 读取文件数据,根据id 找到内容
readNewsData(function(list_news){
var model = null
// 循环 list_news 中的数据,找到 id值相等的内容
for (var i = 0; i < list_news.length; i++){
// 判断集合内容是否有相等的id值
if(list_news[i].id.toString() === req.query.id){
model = list_news[i]
break // 结束这个循环,向下走
}
}
if(model){
// 调用 res.render() 函数进行模板引擎的渲染
res.render(path.join(config.viewPath,'details.html'),{item:model})
} else {
res.end('No Such Item')
}
})
}

// 4. 处理 get 方式添加新闻
module.exports.addGet = function(req,res){
// 读取 json 数据文件
readNewsData(function(list){
// 把新闻添加 id 属性
req.query.id = list.length
// push 添加data
list.push(req.query)

// 写入 data.json 文件
writeNewsData(JSON.stringify(list),function(){
// 重定向
res.statusCode = 302
res.statusMessage = 'Found'
res.setHeader('Location','/')
res.end()
})
})
}

// 5. 处理 post 方法添加新闻
module.exports.addPost = function(req,res){
readNewsData(function(list){
postBodyData(req,function(postData){
postData.id = list.length
list.push(postData)

writeNewsData(JSON.stringify(list),function(){
res.statusCode = 302
res.statusMessage = 'Found'
res.setHeader('Location','/')
res.end()
})
})
})
}

// 6. 处理静态资源请求
module.exports.static = function(req,res){
// 如果用户是以 '/resources' 开发,并且是 get 请求,就认为用户请求静态资源
res.render(path.join(__dirname,req.url))
}

// 7. 处理 404 错误请求
module.exports.handleErrors = function(req,res){
res.writeHead(404,'Not Found',{
'Content-Type':'text/html;charset=utf-8'
})
res.end('404,页面没有找到')
}

// 封装一个读取 data.json 文件的函数
function readNewsData(callback) {
fs.readFile(config.dataPath, 'utf8', function(err, data) {
if (err && err.code !== 'ENOENT') {
throw err;
}
var list = JSON.parse(data || '[]');
// 通过调用回调函数 callback() 将读取到的数据 list,传递出去
callback(list);
});
}

// 封装一个写入 data.json 文件的函数
function writeNewsData(data, callback) {
fs.writeFile(config.dataPath, data, function(err) {
if (err) {
throw err;
}
// 调用 callback() 来执行当写入数据完毕后的操作
callback();
});
}

// 封装一个获取用户 post 提交的数据的方法
function postBodyData(req, callback) {
var array = [];
req.on('data', function(chunk) {
array.push(chunk);
});
// 监听 request 对象的 end 事件
// 当 end 事件被触发的时候,表示上所有数据都已经提交完毕了
req.on('end', function() {
var postBody = Buffer.concat(array);
// 把 获取到的 buffer 对象转换为一个字符串
postBody = postBody.toString('utf8');
// 把 post 请求的查询字符串,转换为一个 json 对象
postBody = querystring.parse(postBody);
// 把用户 post 提交过来的数据传递出去
callback(postBody);
});
}

模块六:config.js

1
2
3
4
5
6
7
var path = require('path');

module.exports = {
"port": 9091,
"dataPath": path.join(__dirname, 'data', 'data.json'),
"viewPath": path.join(__dirname, 'views')
};

模块一:index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. 加载 http 模块
var http = require('http');
var context = require('./context.js');
var router = require('./router.js');
var config = require('./config.js');

// 2. 创建服务
http.createServer(function(req, res) {
// 调用 context.js 模块的返回值(函数),并将 req 和 res 对象传递给 context.js 模块
context(req, res);

// 调用 路由模块的返回值(函数),并将 req 和 res 对象传递给 router.js 模块
router(req, res);
}).listen(config.port, function() {
console.log('http://localhost:' + config.port);
});

ejs 模块引擎

地址:https://www.npmjs.com/package/ejs

官方文档:https://ejs.bootcss.com/

安装

1
npm install ejs --save

示例

1
2
3
<% if (user) { %>
<h2><%= user.name %></h2>
<% } %>

使用示例

1
2
3
4
5
6
7
8
9
10
let template = ejs.compile(str, options);
template(data);
// => 输出渲染后的 HTML 字符串

ejs.render(str, data, options);
// => 输出渲染后的 HTML 字符串

ejs.renderFile(filename, data, options, function(err, str){
// str => 输出渲染后的 HTML 字符串
});

演示

1
2
3
4
5
6
7
8
9
10
11
12
// 加载 ejs 模块
var ejs = require('ejs')
var path = require('path')
// 使用 render 渲染
var html = '<h1><%= username %></h1>'
var result = ejs.render(html,{username: '张三'})
console.log(result) // 打印 <h1>张三</h1>

// 使用 renderFile
ejs.renderFile(path.join(__dirname,'index.html'),{title: '这是一个标题',msg: 'Hello world'},function(err,result){
console.log(result)
})

Express 框架基础

1.什么是 express

  • 基于 NodeJS 平台开发的 “web 开发框架”,就是一个 nodejs 模块
  • express的作用:它提供一系列强大的特性,帮助你创建各种 web 和 移动设备应用

2.学习的理由

  • 为了让我们基于 Node.js 开发 web 应用程序更高效

3.官方网站

4.express 特点

  • 实现了路由功能
  • 中间件(函数)功能
  • 对 req、res 对象的扩展
  • 可以集成其他模板引擎,ejs,underscore

Express 基本使用

1.安装 epress

1
2
npm init -y //创建 package.json 文件
npm install express --save

2.使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// express 
var express = require('express')

// 创建 app 对象
var app = express()

// 注册路由,通过中间件监听指定的路由请求
app.get('/index',function(req,res){
res.end('hello world 你好世界') // 没有指定编码会乱码
res.send('hello world 你好世界') // 不会乱码,正常输出
})

// 启动服务
app.listen(9090,function(){
console.log('http://localhost:9090')
})

res.end() 和 res.send() 的区别

  1. 参数类型区别,res.send() 参数可以是 buffer obj string array 等类型

    res.end() 只能是 buffe 或 string

  2. res.send() 会发送更多的响应报文头 Content-Type:text/html;charset=utf8

app.get 和 app.use 的区别

1
2
3
4
5
// app.get 、app.post 都是限制请求的方式
// app.use 不限制请求的方式,get post 都行
// 请求路径也只是 以指定的路由开始就是行
// 例如 '/index' app.use 就需要以这个开头就行
// 不要求 pathname 完全匹配路由,app.get 需要

app.all

同样是可以注册路由的,但是和app.use的区别,就是需要严格按钮路由匹配

pathname 完全匹配路由,请求方法是不限定的

1
2
3
app.all('/index',function(){
// body
})

通过正则表达式注册路由

限制请求方式,不限制请求路径

要求是 get 请求,请求路径以 /index 开头就行

1
2
3
4
// app.get 配合正则表达式 就可以做到
app.get(/^\/index(\/.+)*$/,function(){
// ……
})

通过 req.params 获取路由中的参数

将参数设计到路由上,如 /index/2017/10/11

1
2
3
4
5
app.get('/index/:year/:month/:day',function(req,res){
res.send(req.params)
})
// 页面结果
{"year":"2017","month":"10","day":"11"} //json 数据

通过 Express 模拟Apche实现静态资源托管

1
2
3
4
5
6
7
8
9
// 静态资源托管
app.use('/',express.static(path.join(__dirname,'public')))

// 解释
app.use('/',function(){})
// 请求资源使用 app.use 任何路径
express 做了一个方法,用请求静态资源
var fn = express.static(path.join(__dirname,'public'))
// 因此得出了上面一行代码

问题

1
2
3
4
5
6
app.use('/xxx',express.static(path.join(__dirname,'public')))
app.use('/xxx',express.static(path.join(__dirname,'pp')))
// 请求路径一样,但有两个静态资源文件夹
// 当请求资源在 第一次public 没有找到,回去 pp 文件找
// 当第一次找到,就直接返回,不会去 pp 文件夹请求资源
// 因此如果有两个不同的资源文件,在两个文件夹,请求第一次请求到了就返回

res 常见的 API 方法

路由挂载 express 上

router 文件代码需要获取 req,res参数,如果直接传入app这个值就很危险,通过挂载 express上 就更安全

router.js

1
2
3
4
5
6
7
8
9
10
11
// 创建一个 router 对象 (router 对象即是一个对象,也是一个函数)
var express = require('express')
var router = express.Router()

// 通过 router 对象设置(挂载)路由
router.get('/',function(req,res){
// ……
})

// 返回 router 对象
module.exports = router

app.js

1
2
3
// 加载路由模块
var router = require('./router.js')
app.use('/',router)

NodeJS补充

npm 版本符合

package.json

1
2
3
4
5
6
7
8
# node package versions
- 13.4.6
- major: 16, minor: 4, patch: 6
# npm 版本符号
- ^ : 锁定major
- ~ :锁定minor
- 空 : 锁定patch
- * :最新版本

npm 自定义包

1. 注册账号

我的账号:simplelife0421

npm官网:https://www.npmjs.com/

2.创建自己的包

1
2
3
4
// index.js 入口
// 创建 package.json 文件
// npm init -y
// 自己的包也安装一些别的包的依赖
1
2
3
4
5
6
7
8
9
10
var _ = require('lodash')

function myChunk(arr,num){
var array = _.chunk(arr,num)
return array
}

module.exports = {
myChunk
}

3.上传自定义包

登录npm账号:npm adduser

如果是进入的是淘宝镜像就要切换源(坑:403 Forbidden)

1
2
3
查看npm源:npm config get registry
切换npm源方法一:npm config set registry http://registry.npmjs.org
切换npm源方法二:nrm use npm

4.发布包

npm publish

PS:如果你是刚创建的 npm 账号需要验证邮箱,不然无法上传

5.发布错误原因

  • 由于设置为了淘宝镜像导致
  • 未验证邮箱

process 进程

1. 变量

在 package.json 不仅能写配置,还能自定义配置。

1
2
3
4
5
6
// 引用配置,通过 process
"simple":{
"env": "echo hello"
}
const echo = process.env.npm_package_config_env
console.log(echo)