一、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 errorstreamRead.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官网:/#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 = pile(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 = pile("<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还是postif (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);})}}//该路由不存在 直接endelse {res.send("无该路由")}}}app.get = (string, callback) => {obj._get[string] = callback;};app.post = (string, callback) => {obj._post[string] = callback;};module.exports = app;