一、node.js 中的 Stream(流)
1、什么是 Stream ?
Stream
是一个抽象接口,Node 中有很多对象实现了这个接口。例如,对http
服务器发起请求的request
对象就是一个 Stream,还有stdout
(标准输出)。
Node.js,Stream 有四种 流类型:
Readable
- 可读操作。
Writable
- 可写操作。
Duplex
- 可读可写操作.
Transform
- 操作被写入数据,然后读出结果。
所有的 Stream
对象都是 EventEmitter
的实例。常用的 事件 有:
data
- 当有数据可读时触发。
end
- 没有更多的数据可读时触发。
error
- 在接收和写入过程中发生错误时触发。
finish
- 所有数据已被写入到底层系统时触发。
2、从流中读取数据
步骤:
创建读取流——设置读取流的编码——处理流事件(data
, end
, error
)。
流的事件是
Event.Emit
驱动的。
const fs = require('fs');
let Data = "";
//创建读取流
let streamRead = fs.createReadStream('./stu.txt');
//设置流的编码
streamRead.setEncoding('utf-8');
//处理流事件 --> data end error
streamRead.on('data', (chunk) => {
Data += chunk;
});
streamRead.on('end', () => {
console.log(Data);
});
streamRead.on('error', (err) => {
console.log(err);
})
3、写入流
写入流 可以写在 读取流的end()
事件里边。
步骤:
创建一个可以写入的流——使用指定的编码写入数据——标记文件末尾(结束写入流)——处理流的事件(finish
、 error
)。
const fs = require('fs');
//创建一个可以写入的流 写入到指定的文件中
let writeStream = fs.createWriteStream('./data.txt');
//使用指定的编码格式写入流
writeStream.write("写入流的数据", "utf-8", (err) => {
if (err)
console.log("写入失败!");
});
//标记文件末尾 --结束流
writeStream.end();
//处理流事件
writeStream.on('finish', () => {
console.log("写入完成!");
})
查看 data.txt 文件的内容:
cat data.txt
4、管道流
什么是管道流?
管道提供了一个输出流到输入流的机制。通常我们用于从一个流中获取数据并将数据传递到另外一个流中。(会覆盖)
步骤:
创建一个可读流——创建一个可写流——管道读写操作
let readStm = fs.createReadStream('./stu.txt');
let writeStm = fs.createWriteStream('./data.txt');
//管道读写操作
//读取stu.txt文件的内容 并将内容写到data.txt文件中
readStm.pipe(writeStm);
5、链式流
什么是链式流?
链式是通过连接输出流到另外一个流并创建多个流操作链的机制。链式流一般用于管道操作。
比如,用管道和链式来压缩和解压文件。
let readStream = fs.createReadStream('./stu.txt');
//压缩 ./stu.txt 文件为 ./stu.txt.zip 文件
readStream.pipe(zip.createGzip()).pipe(fs.createWriteStream('./stu.txt.zip'));
二、web服务器静态文件托管
使用http模块、url模块、fs模块、path模块、自定义的extName模块创建一个静态的web服务器:
静态web服务器的目录结构:
代码如下:
server.js 代码:
const http = require('http');
const fs = require('fs');
const url = require('url');
//path内置模块 可以取出路径名的后缀名称
const path = require('path');
//引入自定义模块 解析后缀名称为对应的文件格式
let extname = require('extName');
let port = 8000;
let hostname = 'localhost';
const server = http.createServer((req, res) => {
let pathname = url.parse(req.url, true).pathname;
if (url != '/favicon.ico') {
if (pathname == "/") {
pathname = "/index.html";
}
//这里要解析后缀名 根据后缀名取对应的文件格式 比如html对应"text/html" 用来设置请求头
//先取出路径名的后缀名称
let exname = path.extname(pathname);
//使用后缀命名解析模块 解析后缀名
//使用promise传递
// extname(exname).then((lastname) => {
// res.setHeader("Content-Type", `${lastname};charset=utf8`);
// fs.readFile("./static" + pathname, { flag: "r+" }, (err, str) => {
// if (err)
// throw err;
// res.end(str)
// })
// })
//使用callback传递
extname(exname, (lastname) => {
res.setHeader("Content-Type", `${lastname};charset=utf8`);
fs.readFile("./static" + pathname, { flag: "r+" }, (err, str) => {
if (err)
throw err;
res.end(str)
})
})
}
});
server.listen(port, hostname, () => {
console.log(`服务器运行在:http://${hostname}:${port}/`);
})
extName.js 代码如下:
let fs = require('fs');
//使用promise传递
// module.exports = (name) => {
// let promise = new Promise((resolve, reject) => {
// fs.readFile(__dirname + "/mime.json", (err, str) => {
// if (err)
// throw err;
// let namelist = JSON.parse(str.toString());
// resolve(namelist[name]);
// });
// });
// return promise;
// }
//使用callback 传递
module.exports = (name,callback) => {
fs.readFile(__dirname + "/mime.json", (err, str) => {
if (err)
throw err;
let namelist = JSON.parse(str.toString());
callback(namelist[name]);
});
}
关于 extName 自定义模块:
(1)mime.json 是后缀名对应文件格式的json
;
(2)在extName.js 文件里,暴露出一个方法(解析后缀名),该方法返回一个promise
对象,通过读取 mime.json 文件之后,根据后缀名取到对应的文件格式,resolve
出去;
(3)传递 namelist[name]
可以使用promise
,也可以使用callback
,或者其他异步方式。
三、事件驱动 EventEmitter
Node.js的三大特点: 单线程、非阻塞式IO(异步)、事件驱动.
处理异步请求,外边获取请求到的数据的方式:
回调函数、使用node.js内置的events模块进行数据的广播和监听(事件驱动EventEmitter)。
(1)回调函数获取异步请求到的数据:
//1.使用回调函数来获取数据
function getMime(callback){
fs.readFile("mime.json",function (err,res){
if(err) throw err;
callback(res.toString());
});
}
console.log(1);
//这样就可以在外部获取异步的数据 也可以暴露出去别的文件使用
module.exports=getMime(function (data){
console.log(data);
});
(2)EventEmitter
什么是Node.js EventEmitter?
Node.js 所有的异步 I/O 操作在完成时都会发送一个事件到事件队列。
Node.js 里面的许多对象都会分发事件:一个 net.Server 对象会在每次有新连接时触发一个事件, 一个 fs.readStream 对象会在文件被打开的时候触发一个事件。 所有这些产生事件的对象都是 events.EventEmitter 的实例。
EventEmitter 类:
events 模块只提供了一个对象: events.EventEmitter。EventEmitter 的核心就是事件触发与事件监听器功能的封装。
使用 EventEmitter:
// 引入 events 模块
var events = require('events');
// 创建 eventEmitter 对象
var eventEmitter = new events.EventEmitter();
对于每个事件,
EventEmitter
支持 若干个事件监听器。
当事件触发时,注册到这个事件的事件监听器被依次调用,事件参数作为回调函数参数传递。
下边是一个通过EventEmitter 进行数据的广播和监听的例子:
var fs=require("fs");
var event=require("events");
//实例化事件的对象
var EventEmitter=new event.EventEmitter();
//使用events模块 获取异步的数据
//在请求完数据之后进行广播
fs.readFile("mime.json",function (err,res){
if(err) throw err;
EventEmitter.emit("mime",res.toString());
});
//在外部进行监听广播
EventEmitter.on("mime",function (data){
console.log(data);
});
注意:
先监听(
on
) ,后分发(emit
)(分发到事件队列里边)。
上边的on
是先与emit
执行的。
在不同的文件之间广播和监听使用都可以获取数据。
(3)EventEmitter 类的方法和属性
EventEmitter
提供了多个属性,如 on
和 emit
。on
函数用于绑定事件函数,emit
属性用于触发一个事件。
下边是一些其它的属性:
方法:
addListener(event, listener)
为指定事件添加一个监听器到监听器数组的尾部。
let fun1=()=>{
console.log("1");
}
eventemitter.addListener("collection",fun1);
eventemitter.emit("collection");
once(event, listener)
为指定事件注册一个单次监听器,即 监听器最多只会触发一次,触发后立刻解除该监听器。
removeListener(event, listener)
移除指定事件的某个监听器,监听器必须是该事件已经注册过的监听器。它接受两个参数,第一个是事件名称,第二个是回调函数名称。
removeAllListeners([event])
移除所有事件的所有监听器, 如果指定事件,则移除指定事件的所有监听器。
setMaxListeners(n)
默认情况下, EventEmitters 如果你添加的监听器超过 10 个就会输出警告信息。 setMaxListeners 函数用于提高监听器的默认限制的数量。
listeners(event)
返回指定事件的监听器数组。
emit(event, [arg1], [arg2], [...])
按监听器的顺序执行执行每个监听器,如果事件有注册监听返回 true,否则返回 false。
类方法:
listenerCount(emitter, event)
返回指定事件的监听器数量。
eventemitter.listenerCount(eventName)
error 事件:
EventEmitter 定义了一个特殊的事件 error
,它包含了错误的语义,我们在遇到 异常的时候通常会触发 error 事件。
当 error 被触发时,EventEmitter 规定如果没有响 应的监听器,Node.js 会把它当作异常,退出程序并输出错误信息。
我们一般要为会触发 error 事件的对象设置监听器,避免遇到错误后整个程序崩溃。例如:
var events = require('events');
var emitter = new events.EventEmitter();
emitter.emit('error');
上边的代码 没有设置监听器,会报错。
四、web服务器静态文件托管+get、post路由传值
静态web服务器结构:
index.html 代码如下:
<form action="/login" method="POST">
账号:<input type="text" name="id">
密码:<input type="password" name="pwd">
<button>登录</button>
</form>
server.js 代码如下:
const http = require('http');
const url = require('url');
let fs = require('fs');
let port = 8000;
let hostname = 'localhost';
let server = http.createServer((req, res) => {
let parse = url.parse(req.url, true);
let path = parse.pathname;
if (req.url != '/favicon.ico') {
res.setHeader('Content-Type', 'text/html;charset=utf8');
if (path == '/') {
fs.readFile('./static/index.html', (err, str) => {
if (err)
throw err;
else {
res.end(str);
}
})
}
else if (path == '/login') {
let info = "";
if (req.method == 'GET') {
info = parse.query.id + "---" + parse.query.pwd;
res.end("结果:" + info);
}
else {
req.on('data', (s) => {
info += s;
});
req.on('end', () => {
res.end("结果:" + info);
})
}
}
}
});
server.listen(port, hostname, () => {
console.log(`服务器运行在http://${hostname}:${port}/`);
})
如果将以上代码的路由部分封装起来:
目录结构:
router.js 代码如下:
暴露出一个对象,键名为路径
let fs = require('fs');
module.exports = {
'/': (req, res) => {
fs.readFile('./static/index.html', (err, str) => {
if (err)
throw err;
else {
res.end(str);
}
})
},
'/login': (req, res) => {
let info = "";
if (req.method == 'GET') {
info = parse.query.id + "---" + parse.query.pwd;
res.end("结果:" + info);
}
else {
req.on('data', (s) => {
info += s;
});
req.on('end', () => {
res.end("结果:" + info);
})
}
}
}
server.js 代码如下:
执行路径对应的函数,传参
const http = require('http');
const url = require('url');
let router = require('./router');
let port = 8000;
let hostname = 'localhost';
let server = http.createServer((req, res) => {
let parse = url.parse(req.url, true);
let path = parse.pathname;
if (req.url != '/favicon.ico') {
res.setHeader('Content-Type', 'text/html;charset=utf8');
router[path](req, res);
}
});
server.listen(port, hostname, () => {
console.log(`服务器运行在http://${hostname}:${port}/`);
})
五、ejs 模板引擎
——从后台渲染数据到页面
ejs官网:https://ejs.bootcss.com/#install
需要掌握标签含义、用法、include、缓存(lru-cache);了解options参数的使用。
安装:
cnpm install --save-dev ejs
引入和使用:
let ejs = require('ejs'),
people = ['geddy', 'neil', 'alex'],
html = ejs.render('<%= people.join(", "); %>', {people: people});
下边是需要掌握的 ejs 基本用法:
1、标签含义
<%
‘脚本’ 标签,用于流程控制,无输出。
<%_
删除其前面的空格符
<%=
输出数据到模板(输出是转义 HTML 标签)
<%-
输出非转义的数据到模板
<%#
注释标签,不执行、不输出内容
<%%
输出字符串 '<%'
%>
一般结束标签
-%>
删除紧随其后的换行符
_%>
将结束标签后面的空格符删除
2、用法
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 字符串
});
完整的用法示例:
server.js 代码:
const http = require('http');
const ejs = require('ejs');
let url = require('url');
let port = 8000;
let hostname = 'localhost';
const server = http.createServer((req, res) => {
//路径解析
let parse = url.parse(req.url, true);
let pathname = parse.pathname;
if (req.url != '/favicon.ico') {
res.setHeader('Content-Type', 'text/html;charset=utf8');
if (pathname == '/') {
let menu = ["首页", "导航"];
//用法一
// let html = ejs.render("<div><%=people%></div>", { people: "张翠花"});
// res.end(html);
//用法二
ejs.renderFile(__dirname + '/view/index.ejs', { menu: menu }, (err, str) => {
if (err)
throw err;
res.end(str);
});
//用法三
// let template = ejs.compile("<div><%=people%></div>");
// let html = template({ people: "张翠花" });
// res.end(html);
}
}
});
server.listen(port, hostname, () => {
console.log(`服务器运行在:http://${hostname}:${port}/`);
})
index.html 代码:
<%for(var i=0;i<menu.length;i++){%>
<div><%=menu[i]%></div>
<%}%>
3、include
通过 include
指令将相对于模板路径中的模板片段包含进来。
可以传值,服务端传给页面,页面再传给该模板片段。
如,在页面中插入模板片段 “title.ejs”:
<%-include("./module/title.ejs",{menu:menu})%>
4、缓存(lru-cache)
EJS 附带了一个基本的进程内缓存,用于缓在渲染模板过程中所生成的临时 JavaScript 函数。
使用方式:
通过 Node 的 lru-cache
库加入 LRU 缓存:
先下载lru-cache
,接下来:
let ejs = require('ejs'),
LRU = require('lru-cache');
ejs.cache = new LRU(100); // 具有 100 条内容限制的 LRU 缓存
如果要清除 EJS 缓存,调用 ejs.clearCache
即可。如果你正在使用的是 LRU 缓存并且需要设置不同的限额,则只需要将 ejs.cache
重置为 一个新的 LRU 实例即可。
六、模拟express 封装路由模块
server.js 文件—— 模拟 express 路由模块的使用:
let http = require('http');
let app = require('./app');
let ejs = require('ejs');
let server = http.createServer(app);
server.listen(8000, () => {
console.log('http://localhost:8000');
})
//模拟express的路由
app.get("/", (req, res) => {
ejs.renderFile(__dirname+"/view/index.ejs", (err, str) => {
res.send(str);
});
})
app.get("/login", (req, res) => {
console.log(req.query);
res.send('登录');
})
app.post("/login",(req,res)=>{
console.log(req.body);
res.send("denglu")
})
app.js 文件——模拟 express 封装路由模块:
let obj = this;
obj._get = {};//存储get请求的回调函数
obj._post = {};//存储post请求的回调函数
let url = require("url")
let app = (req, res) => {
res.send = (str) => {
res.writeHead(200, { "Content-Type": "text/html;charset=utf8" });
res.end(str);
}
let urlparse = url.parse(req.url, true);
let pathname = urlparse.pathname;
let method = req.method.toLowerCase();
if (pathname != "/favicon.ico") {
//检测该路由存在
if (obj["_" + method][pathname]) {
//判断路由是get还是post
if (method == "get") {
req.query = urlparse.query;
obj["_" + method][pathname](req, res);
}
else {
let info = "/?";
req.on("data", (s) => {
info += s;
})
req.on("end", () => {
req.body = url.parse(info, true).query;
obj["_" + method][pathname](req, res);
})
}
}
//该路由不存在 直接end
else {
res.send("无该路由")
}
}
}
app.get = (string, callback) => {
obj._get[string] = callback;
};
app.post = (string, callback) => {
obj._post[string] = callback;
};
module.exports = app;