Node

2019/01/02 node

node.js入门笔记

先说JavaScript吧,JavaScript是由ECMAScript/BOM/DOM组成,ECMAScript定义了js的语法,结构,内置了一些对象,BOM是操控浏览器的一些API,DOM是操控网页的一些API。

node.js也是由ECMAScript为组成的,只不过额外扩展了一些别的功能,比如OS/file/net/database..

node.js是模块化的,各个文件都是独立的模块。

node.js和JavaScript语法差不多。

node异步方法的第一个参数是错误参数。

1、node.js模块引用

假设根目录有两个文件 a.js / b.js, a引用b的方法

  // a.js
  let bObj = require('./b.js')
  console.log(bObj.name); //张三

  // b.js
  let name = "张三";
  module.export = {
      name
  }

2、事件模块

node.js 提供了事件模块,可以让我们自己去注册事件,触发事件。发布订阅模式 有三点需要注意:

  1. 大多数Node.js核心API都是采用惯用的异步事件驱动架构,例如fs/http;
  2. 所有能触发事件的对象都是EventEmitter类的实例;
  3. 事件流程:引入模块 - 创建EventEmitter对象 - 注册事件 - 触发事件

使用:

  // 引入
  let events = require('events');

  // 实例化一个事件对象
  let myEvent =new events.EventEmitter();

  // 注册事件
  myEvent.on('someEvent',  (agm) => {
    // 一些操作
    console.log('触发了someEvent');
    console.log(agm);
  })

  // 触发事件
  myEvent.emit('someEvent','hello someEvent');

2.1 简单原理实现

function EventEmitter(){
    this._events = Object.create(null);
}

EventEmitter.prototype.on = function(eventName,callback){
    // 如果有,就用,没有就赋值一个
    if(!this._events) this._events = Object.create(null);
    if(this._events[eventName]){
        this._events[eventName].push(callback);
    }else{
        this._events[eventName] = [callback];
    }
}

EventEmitter.prototype.emit= function(eventName,...args){
    if(this._events[eventName]){
        this._events[eventName].forEach(fn => fn(...args));
    }
}

3、文件系统 file system

node.js可以对文件进行读写、创建删除操作,使用的模块是fs模块,方法是同步+异步的。 使用:读写文件,读取文件,文件不存在会报错,写入文件,文件不存在会创建。

常用API:

  1. fs.readFile():读文件
  2. fs.writeFile():写文件
  3. fs.unlink():删除文件
  4. fs.rename():重命名文件
  5. fs.copyFile():复制文件
  6. fs.existSync() 同步 / fs.access() 异步:判断文件是否存在
  7. fs.mkdir():创建文件夹
  8. fs.rmdir():删除文件夹
  9. fs.readdir():读取文件夹
  10. fs.stat(url,cb(err, statObj){}):判断当前路径状态,callback接受一个参数,是一个状态对象,可以查看是否为文件或者文件夹(statObj.isFile)

使用:

let fs = require('fs');

  // 文件a.txt
  老酒馆的猫

  // 同步读取文件
  let file = fs.readFileSync('a.txt', 'utf8'); 
  console.log(readFile);

  // 同步写入文件,写入默认的是utf8,如果没有会创建,如果有会清空,如果想追加可以用第二个参数`flag:'a'`,或者使用fs.appendFile();
  fs.writeFileSync('write.txt', file );

  // 异步读取文件
  fs.readFile('a.txt', 'utf8', function(err, data){
    // callback
    if (err) throw err;

    console.log(data);
  })
  // 异步写入是同样的,去掉 Sync  第三个参数是callback


  // 创建文件夹  同步or异步
  fs.mkdirSync('stuff');
  fs.mkdir('stuff', callback);

  // 删除文件夹  同步or异步
  // 删除文件夹需要先删除文件夹内的文件,然后再删除文件夹;
  fs.rmdirSync('stuff');
  fs.rmdir('stuff', callback);

  // 删除文件
  fs.unlink('a.txt', callback); // 参数是路径, callback的参数是err

如果有多个文件嵌套的需要删掉,可以组合API来删掉

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

// 串联删除
function preDeep(url, cb){
  // 先判断当前路径是文件还是文件夹
  fs.stat(url, function(err, state){
    if (state.isDirectory()) {
      fs.readdir(url, function(err, files) {
        files = files.map(file => path.join(url, file));
        let index = 0;
        function next(){
          if(index == files.length) return fs.rmdir(url, cb)
          let currentPath = files[index++];
          preDeep(currentPath, next);
        }
        next();
      })
    } else {
      fs.unlink(url, cb);
    }
  })
}

preDeep('a', function() {
  console.log('删除完毕')
})

// 并行删除
function preDeep2(url, cb){
  // 先判断当前路径是文件还是文件夹
  fs.stat(url, function(err, state){
    if (state.isDirectory()) {
      fs.readdir(url, function(err, files) {
        files = files.map(file => path.join(url, file));
        if(files.length == 0) return fs.rmdir(url, cb);
        let index = 0;
        function count() {
          index++;
          if(index == files.length)  return fs.rmdir(url, cb);
        }
        files.forEach(file => {
          preDeep2(file, count)
        })
      })
    } else {
      fs.unlink(url, cb);
    }
  })
}

preDeep2('a', function() {
  console.log('删除完毕')
})

fs.readFile不适合大文件(一般指64K以上)来使用,否则可能会导致内存浪费,大文件使用数据流读取或使用buffer结合使用fs的其他方法读,fs.open打开文件,fs.read读取文件 fs.write写入文件 fs.close关闭文件。

3.1 分步读取

fs.open(file,flags,cb): file是文件的路径;flags是读写; cb是回调,有两个参数,err, fd,fd代表文件。 fs.open() 不进入同步阻塞调用。如果你想,则应该使用 fs.openSync()。

  • flags可以是:
  • r:以读取模式打开文件。如果文件不存在则发生异常。
  • r+ 以读写模式打开文件。如果文件不存在则发生异常。
  • rs+ 以同步读写模式打开文件。命令操作系统绕过本地文件系统缓存。
  • w 以写入模式打开文件。文件会被创建(如果文件不存在)或截断(如果文件存在)。
  • w+ 在写的基础上,读取,如果不存在会创建。
  • wx 类似 w,但如果 path 存在,则失败。
  • wx+ 类似 w+,但如果 path 存在,则失败。
  • a 以追加模式打开文件。如果文件不存在,则会被创建。
  • ax 类似于 ‘a’,但如果 path 存在,则失败。
  • a+ 以读取和追加模式打开文件。如果文件不存在,则会被创建。
  • ax+ 类似于 ‘a+’,但如果 path 存在,则失败。
let buffer = Buffer.alloc(3);
fs.open('./file1.txt', 'r', (err, fd)=>{
  // 把读到的文件写到buffer里面去
  // 打开要写入的文件,然后开始写
  fs.open('./target.txt', 'w', (err, wfd)=>{
    fs.read(fd, buffer, 0, 3, 0, function(err, byteRead){
      fs.closeSync(fd, ()=>{
        console.log('关闭');
      });
    })
  })
})

这种方式很不科学,耦合性很高,解耦可以用发布订阅模式解耦,fs模块针对这种情况产生了一个流的方法,下面会说到。

4、HTTP & 服务器

4.1 常见HTTP状态码

  1. 101:websocket 双向通信
  2. 200:成功
  3. 204:没有响应体
  4. 206:断点续传
  5. 301:永久重定向
  6. 302:临时重定向
  7. 304:缓存
  8. 401:没有登陆没有权限
  9. 403:登陆了没有权限
  10. 404:找不到资源
  11. 405:请求方法不存在
  12. 500:服务器错误
  13. 502:负载均衡
  14. 504:网关超时

4.2 node HTTP模块

http是内置模块,直接引用就可以使用。

  // 引入HTTP模块
  let http = require('http');

  // 创建服务器  
  let server = http.createServer((req, res) => {
    console.log('客户端发送请求:' + req.url)
    res.writeHead(200, {
      "Content-type": "text-plain"
    });

    // 返回数据
    res.end('server is working');
  })
  
  // 监听端口请求
  server.listen(8888, ()=>{
    console.log('server is running...')
  });

  // 8888端口可能被占用,监听,如果被占用就换一个
  server.on('error', (err)=>{
    if(err.errno === 'EADDRINUSE'){
      server.listen(8889);
    }
  })

Content-type的值:

  text/html : HTML格式
  text/plain :纯文本格式
  ext/xml : XML格式
  image/gif :gif图片格式
  image/jpeg :jpg图片格式
  image/png:png图片格式
  application/xhtml+xml :XHTML格式
  application/xml : XML数据格式
  application/atom+xml :Atom XML聚合格式
  application/json : JSON数据格式
  application/pdf :pdf格式
  application/msword : Word文档格式
  application/octet-stream : 二进制流数据(如常见的文件下载)
  application/x-www-form-urlencoded : <form encType=””>中默认的encType,form表单数据被编码为key/value格式发送到服务器(表单默认的提交数据的格式)
  multipart/form-data : 需要在表单中进行文件上传时,就需要使用该格式

监听用户请求可以在创建服务器的时候直接监听:

  let server = http.createServer((req, res) => {
    console.log('客户端发送请求:' + req.url)
    //  设置响应头
    res.writeHead(200, {
      "Content-type": "text-plain"
    });

    // 返回数据
    res.end('server is working');
  })

也可以去监听请求事件request,因为node中所有的事件都是event的实例,所以直接.on监听就好了,接受两个参数req, res,第一个是客户端请求的信息,第二个是服务端,可以设置响应的东西。

  • req是一个可读的流
  • res是一个可写的流。可以使用流的方法分别读写。
  • 读有data & end事件,写有write & end事件。
  • data是持续的读,可能会不触发,没东西就不触发。end是读取完毕,一定触发。
  • write是持续的写,不会关闭文件,end表示结束,如果传参了,是关闭文件后追加一次写入,一般使用end写。
  // req:客户端  res:服务端
  server.on('request', (req, res)=>{

    /**********请求 req ***********/
    console.log(req)
    console.log(req.mdthod); // 请求的方法名
    console.log(req.url); // 请求的路径,完整的url,不包含哈希`#`
    console.log(req.headers); // 请求头

    let arr = [];
    req.on('data', function(chunk){ // data 可能不触发,如果没有请求体(post请求的数据)就不触发
      console.log(chunk); // 读取到的每一块数据,可能有多块
      arr.push(chunk);
    })
    req.on('end', function(){ // 请求完成就触发end事件,一定会触发。
      console.log(Buffer.concat(arr).toString());  // 
    })

    /**********响应 res ***********/

    res.statusCode = 200;  // 设置响应状态码
    //  设置响应头
    res.writeHead(200, {
      "Content-type": "text-plain;charset=utf-8"
    });

    // 返回数据
    res.end('server is working');  // 如果要多次传输数据,可以使用`res.write`
  })

使用node发送http请求

let http = require('http');

let client = http.request({port: 8888, hostname:'localhost', headers: {a:1}, method: 'post'}, function(res){
  res.on('data', function(data) {
    console.log(data.toString())
  })
})
client.end();

4.3 url模块

node中解析请求路径的内置模块

使用:

  let url = require('url');
  let httpUrl = 'http://www.baidu.com/file?a=1#123'
  url.parse(httpUrl);

url解析出来的是一个对象,主要拥有下面几个字段

{
    protocol: 'http:',
    slashes: true,  // 是否有'/'
    host: 'www.baidu.com:80',
    port: '80',
    hostname: 'www.baidu.com', // 主机名
    hash: '#123', // hash
    query: 'a=1',  // 查询参数. url.paese()的第二个参数设置为true,这里会变成一个对象。
    pathname: '/s',  // 请求的资源路径
    path: '/file?a',
    href: 'http://www.baidu.com/file?a=1#123' 
}

4.4 nodemon

node开启服务,每次服务器代码更新都得重新启动服务才可以,可以使用一个插件nodemon来自动重启服务。本地使用nodemon,线上可以使用pm2

安装:npm install nodemon -g 运行:nodemon 文件名

5、数据流 & buffer

node.js读取数据流有一个缓存区,需要先了解buffer,node.js读取数据不是一次性全部读取出来的,数据量很大的情况下,会分批次读取,每次读取的一些东西存到buffer里面,然后再发送流,然后接着读取。 默认一次读取64*1024字节。

  let fs = require('fs');
  // 读取数据流对象 
  let myReadStream = fs.createReadStream(__dirname + '/readMe.txt', 'utf8');
  // 写入数据流对象
  let myWriteStream = fs.createWriteStream(__dirname + '/writeMe.txt');
  // 读取文件的事件, 每一个streame都是EventEmitter的实例
  let count = 0;
  myReadStream.on('data', function(chunk){
    count++;
    console.log(count); // 1 2 3 4 
    console.log(chunk);  // 每次读取到的部分数据

    // 写入数据流
    myWriteStream.write(chunk)
  })

写入数据流的时候有一个pipe事件,可以控制数据写到哪里,使用pipe事件来重新修改上面的代码

  let fs = require('fs');
  // 读取数据流对象 
  let myReadStream = fs.createReadStream(__dirname + '/readMe.txt', 'utf8');
  // 写入数据流对象
  let myWriteStream = fs.createWriteStream(__dirname + '/writeMe.txt');
  // 写入操作
  myReadStream.pipe(myWriteStream); 

会发现使用pipe很方便,可以写入文件里面,自然可以写入到页面里面去

  let fs = require('fs');
  let http = require('http');
  // 创建服务器
  let server = http.createServer(function (req, res) {
    res.writeHead(200, {"Content-type": "text/plain"})
    // 读取数据流对象 
    let myReadStream = fs.createReadStream(__dirname + '/readMe.txt', 'utf8');

    // 写入操作
    myReadStream.pipe(res)
  })

  // 监听
  server.listen(8888, '127.0.0.1');

既然可以读取纯文本就可以读取HTML 、 json

  // html
  res.writeHead(200, {"Content-type": "text/html"})
  // 读取数据流对象 
  let myReadStream = fs.createReadStream(__dirname + '/index.html', 'utf8');

  // json
  res.writeHead(200, {"Content-type": "application/json"})
  // 读取数据流对象 
  let myReadStream = fs.createReadStream(__dirname + '/json.json', 'utf8');

常用的事件:

  1. myReadStream.on(‘open’, callback):监听文件打开,触发事件
  2. myReadStream.on(‘data’, callback):监听文件读取,返回每次读取到的值
  3. myReadStream.on(‘end’, callback):监听文件读取结束,触发事件
  4. myReadStream.on(‘close’, callback):监听文件关闭,触发事件
  5. myReadStream.on(‘error’, callback):监听文件读取错误,触发事件
  6. myReadStream.pipe(res):把读取到的写入到一个流里面。

5.1 buffer 缓冲器

buffer主要就是用在fs中,buffer就是把二进制表现成了10进制,可以和字符串无条件转化。 buffer代表了内存,不能扩展,需要有一个固定大小。

buffer和数组很类似,buffer[0]取值

使用方法:Buffer.allocBuffer.allocUnsafa

  let buf = Buffer.alloc(10); //申请10个内存

  let buf2 = Buffer.allocUnsafe(10); // 抓取10个内存,可能还没来得及释放,不安全
  buf2.fill(0);  // 填充,手动擦除内存

  let buf3 = Buffer.from('buffer'); // 字符串填充,不可变。

常用的方法:

  1. buf.slice():截取值,和数组的类似。
  2. Buffer.isBuffer(buf):判断是不是buffer,和数组的isArray一样。
  3. copy(targetBuffer,targetStratIndex, sourceStartIndex, sourceEndIndex):拷贝buffer,buf1.copy(buf2,0,1,3);
  4. buf.toString():转化成字符串,可以设置进制数或者base64、utf8
  5. buf.concat():拼接,和数组的类似。let newBuffer = Buffer.concat(bf1, bf2,10); 第三个参数为buffer的大小,如果不填则默认bf1+bf2的长度。
  6. buf.fill():填充。
  7. Buffer.from(‘any’):给buffer直接创建一个值。
  8. Buffer.indexOf():查找某一个值的索引。
// copy原理
Buffer.prototype.copy = function(targer, targetIndex, sourceStrat=0, sourceEnd=this.length){
  for(let i = 0; i<sourceEnd-sourceStrat; i++){
    targer[targetIndex+i] = this[sourceStrat+i];
  }
}

// concat
Buffer.concat = function(list, length = list.reduce( (a,b)=> a+b.length,0 )){
  let buff = Buffer.alloc(length);
  let count = 0;
  list.forEach(item => {
    b.copy(item, count);
    count += item.length;
  });
  return buff;
}

6、路由

路由和vue里面的差不多,甚至更简化。在服务器里面的回调函数里面有两个参数,req, res, 我们可以获取req.url来获取路由。然后判断路由返回不同的HTML文件

    if(req.url === '/'){
      // ... 
      res.writeHead(200, {"Content-type": "text/html"})
      fs.createReadStream(__dirname + '/index.html').pipe(res);
    } else if(req.url === '/contact'){
      // ...
      res.writeHead(200, {"Content-type": "text/html"})
      fs.createReadStream(__dirname + '/home.html').pipe(res);
    }

7、NPC 包管理器

NPM是随同nodeJs一起安装的包管理工具,能解决node代码部署上的很多问题。

常见的使用场景: 1、允许用户从NPM服务器下载别人编写的第三方包到本地使用。 2、允许用户从NPM服务器下载并安装别人编写的命令行程序到本地使用。 3、允许用户将自己编写的包或命令行程序上传到NPM服务器供别人使用。

使用: 1、 需要使用NPM 安装一个东西的时候,必须有一个文件为 package.json, package用于定义项目中所需要的各种模块,以及项目的配置信息,比如名称、版本、许可证等元数据。 2、创建或安装 package.json文件

  npm init

然后会有一系列的设置,默认都回车,有特殊需要的特殊设置,需要注意的是项目的名字不允许有大写。 3、 安装各种插件或者库

  npm install xxx  --save

4、 卸载某一个插件或者库

  npm uninstall xxx

5、详细的看另一篇:https://catsaid.cn/2019/12/06/npm/

8、扩展

放一些常见问题的解决方法。

8.1 跨域

如果想设置跨域可以设置一下headerres.setHeader('Access-Control-Allow-Origin', '*')

*通配符这里写具体的域名。

cors包解决跨域问题。

cookie是解决HTTP的缺陷而诞生的,因为HTTP是无状态协议,需要记录住客户端,然后cookie就这样诞生了,是一个小型文本文件,以键值对存在的,大小不超过4KB,所以不能存大的东西。

cookie并不是严格遵守同源策略,所以可以被窃取,进行CSRF攻击,可以通过设置HTTPOnly属性防止js获取。

设置:

res.writeHead(200, {'Set-Cookie': 'myCookie=test; HttpOnly '});
// or
res.setHeader('Set-Cookie', [ 'cookie1=value1', 'cookie2=value2']);

有时候可能携带凭证被拒绝了,别问,还是设置Header

res.setHeader('Access-Control-Allow-Credentials', true);

8.3 自定义Header

还是设置Headerres.setHeader('Access-Control-Allow-Headers', 'xxheader1, xxheader2')

多个header用,隔开。

8.4 请求

HTTP请求分为简单请求和复杂请求。

  1. 简单请求:请求方式为HEAD/GET/POST,请求头不超出Accept/ Accept-Language/ Content-Language/ last-Event-ID/ Content-Type(只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain)
  2. 复杂请求:不简单就复杂了。 复杂请求会先发一个预检请求PreFlight 也就是OPTIONS看看服务器能不能行,能行的话再发送正常请求。PS: OPTIONS发送是有间隔的,可以配置header来设置缓存时长。
  3. axios 都是复杂请求,ajax 可以是简单请求

首先,复杂请求,拿PUT为例,服务器默认不支持,需要配置支持的复杂请求。当然还是配置header了。

res.setHeader('Access-Control-Allow-Methods', 'PUT, DELETE,...');

另外,因为会先发OPTIONS请求试试水,所以在node服务器里还得判断如果是OPTIONS就直接返回一个成功,莫的返回值,返了也没用。

if(req.method === 'OPTIONS'){
  return res.end();
}

设置OPTIONS的缓存时间:res.setHeader('Access-Control-Max-Age', '3600'); 值的单位是秒。

9、代理

代理分为反向代理、正向代理。

9.1 正向代理

  • 正向代理,意思是一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。客户端才能使用正向代理。
  • 正向代理的典型用途是为在防火墙内的局域网客户端提供访问Internet的途径
  • ——百度百科。

此时,客户端知道有代理服务器的存在,但是原始服务器并不知道是代理服务器在请求他,对原始服务器来说代理服务器是透明的,这就是正向代理。

正向代理主要做:

  1. 认证
  2. 权限验证

9.2 反向代理

  • 反向代理服务器位于用户与目标服务器之间,但是对于用户而言,反向代理服务器就相当于目标服务器,即用户直接访问反向代理服务器就可以获得目标服务器的资源。

此时,客户端并不知道自己访问的是代理服务器还是目标服务器,所以代理服务器对于用户是透明的,所以是反响代理。

反向代理主要做:

  1. 节约了有限的IP资源
  2. 缓存,提高了访问速度,(CDN就是反向代理的实践)
  3. 跨域,前端必须提一嘴跨域。
  4. 提高了服务器的安全性。

9.3 node做代理

node做代理可以使用一个包http-proxy

安装就不多说了。

使用

let httpProxy = require('http-proxy');
let http = require('http');
let proxy = httpProxy.createProxyServer();

http.createServer((req, res)=>{
  proxy.web(req, res, {target: req.headers.host});
}).listen(8080);

Search

    Table of Contents