Call Me By Your Name - node.js的小美好

node的出现,真是让用惯js的前端工程师碰见了爱情,进而大踏步的走向了后端,尽管有时候会被质疑,不被理解。那又有什么关系。javascript

本文是《一站到底 ---前端基础之网络》 代码的整理。但也是一篇独立的node零基础学习笔记。 首先你须要安装node环境。你们本身去看教程 就好。本文和函数式编程那篇文章是同样的思路。咱们先用先实现。若是有机会咱们回过头再来补理论,其实API也没啥须要补,有时间咱们写写node异步队列和DC的算法,但你有什么不明白的能够随着查看文档 。好的,老规矩,咱们看看,本文都完成了那些内容。css

本文代码在github html

  • 用node搭建TCP服务器
  • 用node搭建HTTP服务器
  • 用node文件fs模块对文件读取,并用流的方式写入
  • 用url路径模块,完成了node路由
  • path模块判断文件类型
  • 用gzip对文件进行压缩
  • 浏览器缓存协议的实现
  • node处理跨域
  • https的node服务器的搭建
  • http2的node服务器的搭建

1 node建立TCP服务器

const net = require('net');

let server = net.createServer((socket)=>{
   socket.on('data',function (res) {
      console.log(res.toString())
   });
});

server.listen({
   host: 'localhost',
   port: 8080
});
复制代码
  • 首先你要知道node用了模块化的思想,你能够require一些模块,
  • net是一个TCP网络 API。咱们首先用它来建立一个TCP服务器
  • 咱们引入net模块,经过createServer的方法建立了一个服务
  • 接收到数据的时触发"data"事件,并将拼接好的报文以参数的形式给咱们。
  • 报文是二进制的buffer数据,咱们须要toString方法转化成字符串
  • 而后咱们搭建了一个TCP服务,让监听localhost,8080端口
  • 咱们在terminal中执行 node tcp1.js,这个服务器就启动啦
  • 咱们如今在浏览器里面访问localhost:8080
  • 服务器收到数据后会触发‘data’事件
  • 咱们在terminal中看到了请求头

这里咱们讲一下node的事件机制:前端

//events 模块只提供了一个对象: events.EventEmitter
    //EventEmitter 的核心就是事件触发与事件监听器功能的封装。
    var EventEmitter = require('events').EventEmitter; 

    //一个socket对象
    var socket = new EventEmitter();

    //咱们在socket对象上绑定data事件,若是是多个函数会被前后调用
    socket.on('data', function(res) { 
        console.log(res); 
    }); 

    socket.on('data', function(res) { 
        console.log(res + '111'); 
    }); 

    //咱们用emit的方法去触发事件,在1秒后咱们出发,咱们触发事件时,能够传递参数。
    setTimeout(function() { 
        socket.emit('data' , "hello" ); 
    }, 1000); 

复制代码

咱们会在控制台看到下面的信息。java

这时咱们会过头来看,浏览器左下角,是否是一直在显示等待响应,是由于咱们尚未返回数据啊,那咱们给 它返回一些数据。咱们知道要符合http格式。node

咱们将一段符合http格式的数据用socket.write(responseDataTpl)去返回数据git

let responseDataTpl = `HTTP/1.1 200 OK
Connection:keep-alive
Date: ${new Date()}
Content-Length: 12
Content-Type: text/plain

Hello world!
`;
复制代码
  • 咱们触发 node 01-tcp02.js
  • 在浏览器中,咱们就能看到返回的Hello world!

问题:咱们已经发现了写出固定格式的http响应报文杯仍是比较麻烦的,咱们为何不能封装一层呢?github

2 node建立HTTP服务器

2.1 建立HTTP服务器

const http = require('http');

const server = http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('hello world');    //  发送响应数据 
})

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

server.listen(10080) 
复制代码
  • 咱们引入了一个node中http模块,监听10080端口(默认地址localhost)
  • 咱们建立了一个http服务,在请求成功是,返回200状态码
  • res.end('hello world')是发送响应数据的时候带上'hello world'
  • 咱们在terminal中执行 node 02-http-simple.js,这个服务器就启动啦
  • 咱们如今在浏览器里面访问localhost:10080
  • 咱们看到浏览器上显示'hello world 啊'

问题:那么这个时候,若是我但愿传进去是一个文件而不是字符串,改怎么办呢?ajax

2.2 node文件模块(fs)

node的文件模块是很是强大的,能够对文件进行读取,增删改查。这里咱们先讲如何读取的。读取分两种一种同步,一种异步。算法

const fs = require('fs'); 
const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/html' });

  // 同步
  // let  data = fs.readFileSync('index.html');
  // res.write(data);    
  // res.end();     //  发送响应数据 
  
  // 异步
  fs.readFile('index.html', function (err, data) {
     res.write(data);    
     res.end();     //  发送响应数据 
  })
})

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

server.listen(8088) 
复制代码
  • 咱们引入文件模块,const fs = require('fs');
  • 同步的时候,咱们先读取,执行后边的写入和发送函数
  • 异步的时候,咱们在异步读取的回调函数中执行写入和发送

问题:那么如今有一个问题,不管是同步仍是异步,咱们都须要先读文件,再写入,那么文件很大时,对内存的压力就会很是大。咱们有没有什么办法,边读取边写入?

2.3 node流(Stream)

Stream 是一个抽象接口,做用就是能把文件,读一点写一点。这样不就不用占很大内存了。咱们来看看怎么实现的?

const fs = require('fs');
const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/html' });
  // let resStream = fs.createReadStream('index.html');
  // resStream.pipe(res);
  //流是能够支持链式操做的
  fs.createReadStream('index.html').pipe(res)
})

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

server.listen(10080)
复制代码
  • 用fs.createReadStream('index.html')建立一个可读流。
  • 用resStream.pipe(res);管道读写操做,写入响应报文
  • 你会发现上面代码中咱们并无用res.end(); 发送数据 。由于默认状况下,当数据传送完毕,会自动触发'end'事件
  • 最后流是支持链式操做的,因此你能够一行代码就搞定啦

问题:在咱们解决了内存问题后,你会发现,咱们index.html中是有一张图片没有加载出来的。缘由很简单。由于不管发送什么请求,咱们都只返回一样的操做。那么咱们能如何区分不一样的请求呢?

2.4 node路由

咱们知道在应用成协议中用URL来表示文件的位置。区分不一样请求的一个重要任务就是区分路径。那么对路径的处理node中提供了一个url模块,让咱们来看看吧。

const fs = require('fs');
const http = require('http');
const url = require("url");

const server = http.createServer((req, res) => {
  //pathname是取到端口号后面的地址
  let pathname = url.parse(req.url).pathname;
  if(pathname === '/') pathname = '/index.html';
  let resPath = '.' + pathname; 

  //判断路径是否存在
  if(!fs.existsSync(resPath)){
    res.writeHead(404, {'Content-Type': 'text/html'});
    return res.end('<h1>404 Not Found</h1>');
  }
  //若是存在,将在路径下的文件返回给页面
  res.writeHead(200, { 'Content-Type': 'text/html' });
  fs.createReadStream(resPath).pipe(res)
})

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

server.listen(10080) 
复制代码
  • 咱们引入了一个url模块,帮助咱们去处理路径
  • url.parse(req.url)是将一个路径,帮咱们处理成对象,它包含咱们经常使用的路径属性
  • 其中有一个属性是pathname,就是URL端口号和参数之间的路径,也就是咱们访问的路径
  • 若是咱们直接访问网站后面不加路径,咱们给默认指向/index.html
  • 相对路径访问咱们给前面加一个'.'
  • 而后咱们用文件模块提供的existsSync方法去判断服务器上是否有这个文件
  • 若是没有咱们返回404,告诉没有找到文件。有就将文件返回。

问题:那么如今,咱们就能在浏览器上看见咱们美丽的大娟的图片了,可是咱们在学http的时候知道Content-Type是处理文件类型的,那么图片类型确定不会是'text/html' ,虽然浏览器很智能帮我显示出来了,可是咱们仍是要把这样的错误改过来。

2.5 path模块判断文件类型

咱们知道,只要改变 'Content-Type'的文件类型便可。

function getFileType(resPath){
  const EXT_FILE_TYPES = {
    'default': 'text/html',
    '.js': 'text/javascript',
    '.css': 'text/css',
    '.json': 'text/json',

    '.jpeg': 'image/jpeg',
    '.jpg': 'image/jpg',
    '.png': 'image/png',
    //...
  }

  let path = require('path');
  let mime_type = EXT_FILE_TYPES[path.extname(resPath)] || EXT_FILE_TYPES['default'];
  return mime_type;
}
复制代码
  • 咱们定义了一个getFileType函数,并给出经常使用的文件类型和它们Content-Type的值
  • 咱们应用了path模块,用path模块上的extname方法取出扩展名
  • 而后跟咱们定义的对象去匹配,若是没有找到,咱们就给一个默认的值

你每次修改完node文件都须要去终端启动是否是很麻烦。如今再交你们一个热启动的小技巧。

sudo npm install supervisor -g

supervisor 02-http-fs-url.js 
复制代码
  • 在全局安装supervisor
  • 用supervisor代替node去启动文件
  • 这样你在修改node文件的时候,就不用每次手动去重动终端了

问题:咱们大娟的图片才只有一百多K,若是是图片很大咱们是能够先压缩再传输的

2.5 用gzip对文件进行压缩

(1)咱们先取出请求头中的accept-encoding参数,若是参数不存在,咱们赋值成''

let acceptEncoding = req.headers['accept-encoding'];
 if (!acceptEncoding) { acceptEncoding = '';};
复制代码

(2)而后咱们用正则去判断acceptEncoding是否用了gzip压缩,固然这里能够有多个判断压缩格式。这里咱们只写一个。

if(/\bgzip\b/.test(acceptEncoding)){
      //执行压缩,并在响应头中告诉浏览器压缩的格式
  }else{
      //不执行压缩
  }
复制代码

(3)咱们须要引用zlib模块对文件进行压缩。这里咱们用Gzip,就调用Gzip的方法。 而后咱们对文件流先进行一步压缩,在写到响应体中。

const zlib = require('zlib');

let raw = fs.createReadStream(resPath);
raw.pipe(zlib.createGzip()).pipe(res);
复制代码

(4)最后咱们还须要在响应头中告诉浏览器个人文件已经给你压缩成什么格式啦

'Content-Encoding': gzip
复制代码

而后咱们开两个终端分别用启动有gzip和没有gzip压缩的

home文件中放了一张我在颐和园用相机拍的5M的图片

你能够打开多个浏览器窗口,分别先访问两个文件,能够多测几遍,你会发现有gzip压缩的明显要慢

为何会这样呢,道理很简单,由于咱们的服务器和浏览器都在同一台电脑上,传输速度很快。因此压缩和解压的时间就被放大啦。这也告诉咱们并非什么场景都适合对文件进行压缩的。

  • 若是你浏览器没有时间的选项,你能够点击导航栏调出。
  • 在测试的时候,能够把清除缓存打开。

2.6 浏览器缓存协议的实现

这一节没有node的新知识,咱们对http浏览器缓存协议进行一个实现。咱们也不须要进行压缩,因此上一节压缩的内容不会加。

**(1)强缓存 ** 强缓存咱们在响应头中给一个一周的过时时间 参考代码cache.js

Cache-Control : max-age = 604800' 复制代码

  • 咱们能够看到在第二次刷新的时候,文件中的资源就会从浏览的缓存中取。
  • 若是不想从缓存中取,能够强制刷新,或打开Disable Cache
  • 强刷的时候,你再看localhost请求头中会带上 Cache-Control: no-cache
  • 你会普通刷新资源文件会有Cache-Control: no-cache,这是由于资源文件是从缓存中取的,而Cache-Control: no-cache是你上次强刷的时候带上去的。
  • 若是新打开一个窗口,再次访问同一个网页,不用从缓存中取
  • 这就是为何,有时候你在开发时,改了js文件没有生效,但在另外一个窗口打开看到的是最新文件的缘由

**(2)弱缓存 ** 参考代码cache2.js

etag须要一个双引号的字符串,而后咱们把它写入响应头中

let etagStr = "dajuan";  //etag 要加双引号

 res.writeHead(200, { 
    'Content-Type': getFileType(resPath),
    'etag' : etagStr
  });
复制代码

当再次访问的时候咱们须要判断一下,if-none-match带的值于如今etagStr值是否一致。若是一致直接返回304,不用在返回文件。浏览器看到304,就知道了要从缓存中拿。

let etagStr = "dajuan";  //etag 要加双引号
   if(req.headers['if-none-match'] === etagStr){
    res.writeHead(304, { 
      'Content-Type': getFileType(resPath),
      'etag' : etagStr
    });
   res.end();
 }
复制代码

固然,这里咱们只是举了一个最简单的例子,真实项目中是不可能把全部的文件都返回同一个字符串的。

2.7 node处理post和get请求

(1)咱们首先分别用get 和 post 写一个表单提交,让其点击都跳转到form_result.html,有一行你好,name

//form.html
  <form action="form_result.html" method="get">
       <p> get: <input type="text" name="name" /></p>
       <input type="submit" value="Submit" />
  </form>
   <form action="form_result.html" method="post">
       <p> post: <input type="text" name="name" /></p>
       <input type="submit" value="Submit" />
  </form>

  //form_result.html
  <div>你好,name</div>
复制代码

(2)get方法去处理 参考代码method.js

let pathAll = url.parse(req.url);
 let getArgument = pathAll.query;     //取出参数 name=XXX

 if(pathname === '/form_result.html' && getArgument != undefined){
   let text = fs.readFileSync('form_result.html').toString().replace(/name/, getArgument)
   fs.writeFileSync('form_result.html',text)
 }
复制代码
  • 咱们知道url.parsl()能读取url,query就是get方法带的的参数
  • 当要跳转的路径是是'/form_result.html'而且getArgument有值时
  • 咱们用文件模块同步读取出'form_result.html'的内容
  • 转换成字符串以后,在将表单中的name替换成name=XXX

这时候get提交的表单能够去处理啦,可是post的参数并无在URL中,因此对post没有影响

(3)post方法去处理 参考代码method2.js

req.on('data',(data)=>{
    let text = fs.readFileSync('form_result.html').toString().replace(/name/, 'post'+ data)
    fs.writeFileSync('form_result.html',text) 
  })
复制代码
  • post方法是在请求头中监听data事件的,请求报文中,有请求体时,被触发
  • 因此咱们在监听到‘data’事件被触发时,咱们也是执行上面操做
  • 而这个时候若是发送get请求,就不会被响应
  • 咱们学事件知道,咱们能够给‘data’绑定多个事件,而每次post请求必然会触发。这就是对服务器形成的反作用。

这里咱们留一个问题,咱们在处理文件的时候是同步处理的,若是异步处理咱们改怎么作?

2.8 node处理跨域

参考代码:cors.js cors2.js

if(req.headers['origin'] ) {
    res.writeHead(200, { 
      'Access-Control-Allow-Origin': 'http://localhost:5000',
      'Content-Type': 'text/html'
    });
    return fs.createReadStream(resPath).pipe(res)
  };  

复制代码
  • 咱们分别在本地启动了两个服务
  • 让一个端口是5000,另外一个端口是9088
  • 咱们在5000的端口访问,cors.html
  • 在html中,咱们ajax调用9088端口的data.json
  • 这样就造成了跨域,咱们容许5000端口访问,就会返回数据
  • 若是咱们把不填,或者不写5000端口,你会看到收不到数据

注: 这里仍是有点小问题,第一我只在第一次访问时,若是端口不符合提示报错了。我怀疑是否是浏览器給服务器地址加入白名单了。第二为何不是书上写的两次求情啊。我第一次即便不写数据,也不会发起第二次请求。不过跨域的效果仍是实现了的。

3 https与http2

3.1 https的node服务器的搭建

知道了原理后,咱们在终端生成证书和私钥吧。

(1)openssl genrsa -out server.key 1024 //生成服务器私钥

(2)openssl rsa -in server.key -pubout -out server.pem  // 生成公钥

  //本身扮演CA机构,给本身服务器颁发证书,CA机构也须要本身私钥,CSR文件(证书签名请求文件),和证书

 (3)  openssl genrsa -out ca.key 1024            //生成CA 私钥
      openssl req -new -key ca.key -out ca.csr   //生成CA CSR文件
      openssl x509 -req -in ca.csr -signkey ca.key  -out ca.crt  //生成CA 证书

 //生成证书签名请求文件
 (4) openssl req -new -key server.key -out server.csr //生成server CSR文件
  
 //向本身的机构请求生成证书
 (5) openssl x509 -req -CA  ca.crt -CAkey ca.key -CAcreateserial -in server.csr   -out server.crt   //生成server 证书

复制代码

注意:信息随便填,但提示里有格式要注意啊,宝宝们。。。

const https = require('https');
const fs = require('fs');

const options = {
  key: fs.readFileSync('./key/server.key'),
  cert: fs.readFileSync('./key/server.crt')
};

https.createServer(options, (req, res) => {
  res.writeHead(200);
  res.end('hello world\n');
}).listen(8000);

复制代码
  • 咱们引入https模块,填好咱们证书和私钥
  • 剩下的代码如今看起来是否是很简单

服务器访问: https://localhost:8000/

  • 这样咱们访问https就能请求到网页了
  • 固然会提示咱们不安全,继续就好啦
  • 为啥会提示咱们不安全,刚才本身怎么填的证书,内心没数嘛。哈哈哈

3.2 http2的node服务器的搭建

node的http2是试验的API。若是node版本比较低,请先升级。个人是v8.11.3

const http2 = require('http2');
const fs = require('fs');

const server = http2.createSecureServer({
  key: fs.readFileSync('./key/server.key'),
  cert: fs.readFileSync('./key/server.crt')
});
server.on('error', (err) => console.error(err));

server.on('stream', (stream, headers) => {
  // stream is a Duplex
  stream.respond({
    'content-type': 'text/html',
    ':status': 200
  });
  stream.end('<h1>Hello World</h1>');
});

server.listen(8443);
复制代码
  • 咱们仍是引入https时建立的私钥和证书
  • 咱们建立http2的服务
  • 在http2中时流的概念。因此咱们写入请求头。并返回请求体
  • 咱们在浏览器上访问:https://localhost:8443/

这样咱们就完成了一个最简单的http2的访问啦。

相关文章
相关标签/搜索