node学习笔记

258 阅读7分钟

node

  1. js的执行环境

  2. 主要两大块:网络,文件

  3. 主要做的事情: 高性能web服务器(单线程异步), 前端工程化(转义[es6->es5, less/sass->css],压缩,混淆-通过改变形参名字让代码不易阅读,打包), 爬虫,命令行工具(命令行软件)等 Electron => 可以做传统软件。把浏览器的UI功能和node的网络,文件读取,进程管理等功能组合起来制作传统软件。

  4. node之所以用js适配,因为js就是异步的。node打算也做异步的I/O(异步本身底层实现是通过多线程) I/O: 跟硬盘或者网络交互时的输入输出 同步IO: 遇到异步代码比如readFile(), 代码卡在readFile()上,CPU调用读取程序读取内容再存在另外一片内存里,一边还去调用别的线程,在各线程内来来回回等回到读取程序发现全部存完了,readFile把从内存里读取的内容返回给程序继续后续处理。 异步IO: 异步操作在后台(另外线程)执行的同时主线程是不会暂停的会继续执行下去。等异步操作执行完再通知主线程执行完了。

  5. 层: os(硬件)-> Browser -> vm -> JS
    os -> node -> vm -> JS os -> vm -> python browser提供给js的方法和node提供给js的方法是不一样的

  6. 如果没有给node传文件,就会REPL(交互式命令行):read,evaluate, print, loop 例如: $node

    1 + 1 2 [-1,-2,-3].map(Math.abs) [1,2,3]

  7. process是像console一样的全局变量,提供很多方法控制当前的进程 比如 退出: process.exit(0) 括号里是状态码,告诉外界怎么退出的。一般有标准,你以什么理由退出就返回相应的状态码 读取命令行文件接收的参数:process.argv 文件名 get-command-line-args.js 里面写代码 console.log(process.argv) 在node执行这个文件时传参数 node get-command-line-args.js foo bar baz 会读取到 后面几个参数 argv[0]是node的完整路径, argv[1]是文件的完整路径,argv[2]就是foo。。。

var num = parseInt(process.argv[2])
console.log(num + ':')
for(var i = 2 ; ; i++) {
  while (num % i == 0) {
    console.log(i)
    num = num / i
    if (num == 1) {
      process.exit(0)
    }
  }
}
  1. node中没有浏览器提供的功能比如document,alert. 全局对象浏览器是window,node中是global

  2. node的调试 方法1:node --inspect-brk factor.js 15 (暂停在代码的第一行)/ node --inspect factor.js 原理:ws协议监听一个端口,打开谷歌浏览器,调试器左上角的node图标点击 写的代码被node当成模块运行 方法2:vs code的调试 方法3: npm i -g ndb ndb == node debug 用法: ndb node --insepect factor.js 15

  3. require路径查找 如果是以点或者斜杠开始,相对于目前文件路径查找对应文件, 如果不以点或斜杠开始,加载内置的模块或者下载的模块 如果是下载的模块 1-如果没有找到相同名字的文件夹就会试图加载成文件, ,js, json, node 2-去当前文件夹的node_modules文件夹里找这个名字的文件夹,加载index.js 3- 如果没找到这个名字的文件夹 1)这个文件夹里有package.json就会去读取这个文件里的main字段所指向的文件 2)这个文件夹里有index.js就会把这个文件的导出的东西加载进来。 4-如果在当前文件夹的node_modules文件夹里未找到想要找的名字的文件夹就会去父级文件夹继续找

  4. NPM: JS模块仓库 node package manage npm下载的时候发生的事: 这个包把自己所依赖的其它的包都声明在自己文件夹里的package.json的dependencies字段里,下载的时候会把这个字段里的所有依赖一并下载下来。

  5. fs文件模块 file system:操作文件和文件夹的功能函数,本质就是把操作系统提供的函数 1)readFile 注意:如果不传第二个参数编码方式(一般传UTF-8),node自动返回原始字节Buffer对象,是类数组对象本身有length以及下标
    2)wirteFile 因为是写入到硬件里,回调函数只传一个err就行。 默认以UTF-8写入
    3)readdir : 文件夹里的所有文件以及文件夹的名字,以数组形式返回
    4)stat:返回对象,里面包括此文件的详细信息,方法isFile,isDirectory,isSymbolicLink(类似快捷方式,它不是硬链接(两个地方指向同一个文件),它是符号连接(源文件删除它也自动被删除)),isSocket(网络),isFIFO(先进先出,TCP连接)
    5)rename 重命名文件,以及移动文件
    6)unlink 删除文件
    7)fs里总共有四个版本的函数,同步,回调函数的异步,promise版本,还有f开头的函数。 其中f开头的函数中第一个参数fd是什么?是file directory 文件描述符,每一次open file的时候系统自动给我们返回一个文件描述符。 也就是说f开头的函数不用传路径,传文件描述符
    8) chmode =》 change mode 对文件的权限改变
    9)FSWatcher 文件监控

  6. node里的IO相关的一般是异步函数,一般是错误优先。 如果成功就会传null,result。 如果失败就传error

  7. 异步函数后面加Sync(比如readFileSync)就会变成同步

  8. 异步函数可以封装成promise版本,像同步一样使用 例: read-file-promise.js

var fs = require('fs')
function readFilePromise(...args) {
  return new Promise((resolve, reject) => {
    fs.readFile(...args,(err, data) => {
      if (err) {
        reject(err)
      } else {
        resolve(data)
      }
    })
  })
}
//用法
readFilePromise('a.js').then(data => {

}).catch(err => {

})

将一个基于回调的函数转换为promise的函数

用法:readFilePromise = promidify(fs.readFile)

function promisify(callbackBasedFunction) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      callbackBasedFunction(...args, (err, result) => {
        if (err) {
          reject(err)
        } else {
          resolve(result)
        }
      })
    })
  }
}

将一个promise函数转换为基于回调函数的函数

function callbackify(promisedBased) {
  return function(...args) {
    var cb = args.pop()
    promisedBased(...args).then((result)=>{
      cb(null, result)
    }).catch((reason)=>{
      cb(reason)
    })
  }
}

  1. 以上两个转换函数node提供现成的 方法1:通过util(工具),每个函数单独转换 var util = require('util') var readFilePromise = util.promisify(fs.readFile) var readFileCallback = util.callbackify(readFilePromise) 方法2:直接通过 .promises 获取整个模块的异步函数们 fsp = require('fs').promises

  2. npm i 和 npm i -g的区别 -g == --global 全局安装:不是让你在任何一个地方require的,而是以命令行工具的形式装在全局 命令行一般跟软件包名一样

node常用模块

  1. path:操作路径
    提前要知道的:window和linux下的路径表示不一样,在window是用\反斜杠,linux用/正斜杠。 路径分隔符window是冒号:,linux是;分号。 所以path返回的有两个版本的对象,一个是window的,另一个是posix。最外层的就是目前的版本 如果在window开启node并打印path,window对象本身有两个指针一个w指针指向自己(win32:circular),另一个p指针指向p对象 如果在linux开启node并打印path,posix对象也本身有两个指针一个w指向window对象,p指针指向自己

1)basename 拿到文件名 path.basename('/foo/bar.jpg','.jpg') => bar
2) path.delimiter => 在linux 打印; , 在window打印:
3) path.dirname 拿到路径=>path.dirname('/foo/baz/bar.jpg','.jpg') => /foo/baz
4) path.extname('/foo/a.png') => .png
5) path.parse 路径分解成对象 {root: '/', dir:'',base:'',ext:'',name:''}
6) path.format 与5相反
7) isAbsolute: 以斜杠开头或者以盘开头就是绝对路径
8) join 把几个路径拼起来
9) path.normalize('foo/bat/../././') 化简=》 foo
10)path.resolve('/foo/bar/baz', '../1') => c:\foo\bar\1
path.resolve('/foo/bar/baz', '/../1') => c:\1
11)path.relative('/foo/bar/baz/baa/','/foo/bar/1/2') 从第一个路径通过什么路径可以到达第二个路径 =》 ..\..\1\2
12)path.sep 在window反斜杠,在linux正斜杠

  1. util

    util.type判断类型的

  2. URL
    URL对象:能控制url的各个部分
    u = new URL('a:b@www.baidu.com:8889/foo/bar?a=b…')
    URL {
    href: 'a:b@www.baidu.com:8889/foo/bar?a=b…',
    origin: 'www.baidu.com:8889',
    protocol: 'https:',
    username: 'a',
    password: 'b',
    host: 'www.baidu.com:8889',
    hostname: 'www.baidu.com',
    port: '8889',
    pathname: '/foo/bar',
    search: '?a=b&c=d',
    searchParams: URLSearchParams { 'a' => 'b', 'c' => 'd' },
    hash: '#foobar'
    } url.SearchParams
    url.domain相关(中国字互转unicode)

querystring模块解析query部分

  1. http模块
//服务端:
const http = require('http')
const server = http.createServer()
server.on('request',(request, response) => { // 背后连接tcp连接,收到正确的http报文的时候才会响应这个函数
request.socket.remoteAddress //socket是net的模块, 表示tcp连接, 这个可以获取对方的地址
request.method
request.headers
request.url
response.write('')
})
server.listen(port, () => {})

//客户端(跟XMLHttpRequest相似,可是这个不被跨域限制)发请求
const http = require('http')
var request = http.request({
  hostname: "a.net",
  path:'/a.html',
  method: 'GET',
  headers: {Accept: "text/html"}
}, function(response) {

})
http.get()//收请求
request.end()


const server = new http.Server(requestHandler) //http模块主要做的事情
class HTTPServer {
  constructor(requestHandler) {
    var net = require('net')                   //1.建立tcp连接
    this.server = net.createServer(conn => {   //2. 建立成功后
      var head = parse()//parse data comming from conn //3. 解析收到的请求
      if(isHttp(conn)) {  //4. 如果是http请求就会如下反应
        requestHandler(new RequestObject(conn), new ResponseObject(conn)) //5. 创造基于tcp连接的request对象和response对象, 来操作本次连接,按照报文形式给我们传入传出
      } else {
        conn.end()
      }
    })
  }
  listen(port, f) {
    this.server.listen(port, f)
  }
}
  1. __dirname 无论在哪个模块里都能访问到这个全局变量,当前模块所在文件夹的完整路径
  2. 把代码当脚本命令行使用的方法 linux里 1)echo $path 这个目录下的文件能直接执行 2)cd 其中一个路径里 3)文件放进去 (记得文件最上面加上一句告诉linux执行的时候用node执行,#!/usr/bin/env node) 4)sudo chmod 777 sfs(文件名) //修改权限 ,777可读可写可执行 5)使用,任何地方输入sfs(文件名)就直接执行那个代码了
  3. decodeURIComponent 路径是汉语,解析成汉语 是js的不是node的
  4. globals 当前模块下的 node里的setTimeout/setInterval返回一个对象,这个对象的unref方法,让该timer的未执行的状态不影响程序的结束. ref方法则相反
  5. os 操作系统 os.EOL 转行字符 \n \r\n
  6. Events 事件监听触发器 创造event,绑定事件e.on('',),触发事件emite(在浏览器是dispatchEvent),事件取消e.off
class EventEmmiter{
  constructor() {
    this.eventListeners = {}
  }
  on(type, handler) {
    if(type in this.eventListeners) {
      this.eventListeners[type].push(handler)
    } else {
      this.eventListeners[type] = handler
    }
    return this
  }
  off(type, handler) {
    var listeners = this.eventListeners[type]
    this.eventListeners[type] = listeners.filter(it => it != handler)
    return this
  }
  emit(type, ...args) {     
    var listeners = this.eventListeners[type]
    if (listeners) {
      for (var i = 0 ; i < listeners.length; i++) {
        var handler = listeners[i]
        handler.call(this, ...args)
      }
    }
  }
}


var emmiter = new EventEmmiter()
emmiter.on('foo',() => {})
emiter.emit('foo')
emiter.off()

流stream

  1. 流的模型: 搬砖,中间站一排人, 每个人旁边都有缓存区。就算中间有人走开一会,也不会断开。 模型2: 水通过管子往下面的水池子流,持续层层往下流。 通过水龙头控制流,下面快满了水龙头关,下面快枯了没谁了就开。

  2. 流主要解决的问题1):内存不够的问题,不管是数据的传输还是读写转换,不能一下子处理都需要一段一段处理,通过流,内存就会占用的变小。 问题2);如果有多个流(可读,可写,双工,转换)流,可串起来 file => rs => gzip => conn => conn => gunzip => ws => file

rs.pipe(gzip).pipe(ws).pipe(conn)

  1. 流的类型 可读流:从文件出来数据 可写流:只接收数据,最后处理这个数据(一般只负责写到硬盘里或者网络里, 到达网路的另一端后就不再管了) 双工流:可读可写流, 最典型的就是TCP连接。 进来的和出去的不一定有关系 转换流:出来的数据是进去的数据转换来的,比如zip 压缩流

  2. 压缩流 zlib 的 Gzip

const fs = require('fs')
const zlib = require('zlib')

var compressStream = zlib.createGzip()

var file = './3.rmb'

var rs = fs.createReadStream(file)
var ws = fs.createWriteStream('./3.gzip')

rs.on('data', data => {
  if (compressStream.write(data) === false) { 
    rs.pause() 

  }
})

compressStream.on('drain', () => { 
  rs.resume() 
})

compressStream.on('data', data => {
  if(ws.write(data) === false) {
    compressStream.pause()
  }
})
ws.on('drain', data => {
  compressStream.resume()
})

rs.on('end', () => {
  compressStream.end()
})
compressStream.on('end', () => {
  ws.end()
})

//以上可写成如下一句
rs.pipe(compressStream).pipe(ws)
  1. createReadStream,createWriteStream

var fd = fs.open(path)
//fs.read 可以一点一点读出来
fs.read(fd, buffer,offset, length, position, callback)
(从这个文件里,放入这个buf的,这个位置开始的位置上写入,读取1024个字节,这个位置开始读,(读完触发)=> {})
position >= filesize时一定要this.push(null)

fs.write(fd, chunk, 0, chunk.length, position, callback)

  1. 进程process
    process.title
    peocess.cwd() //打印当前工作目录 current working directory
    == path.resolve('.')
    pocess.constructor
    process.chdir() //changedirectory

homw, homepath等属性

pid 进程id

ppid 谁启动这个进程

主要方法: stdout标准输出流 stderr标准错误流 stdin标准输入流 能接受别人发给这个进程的数据,来达成进程间通讯的目的

foo | bar | baz foo进程的输出变成bar输入,bar的输出变成baz的输入,baz输出到控制台

pipe到后面端口里 echo hello | md5sum.exe 算出来hello的md5被输入控制台上

echo hello | md5sum.exe | split -b 10 输出的md5的每10个字节为一个文件导出

例子:

//文件1 : 拿到可写流后加上i后输出
process-stream1.js 

var i = 0
setInterval(() => {
  process.stdout.write('a' + i++) 
} ,1000)
//对一个进程来说可写流,出去之后对操作系统来说时可读流


//文件2: 可读流接收到后log出来
process-stream2.js
process.stdin.on('data', d => {
  console.log(d.toString())
})

//文件3: 小写转大写转换
process-in-out.js

process.stdin.on('data', data => {
  process.stdout.write(data.toString().toUpperCase())
})


//执行:
 process-stream1.js  | process-in-out.js | process-stream2.js



 例子2:两个端口同步
 //一个文件
 const net = require('net')
 const fs = require('fs')

 const server = net.createServer(conn => {
   conn.on('error;, () => {})
   process.stdin.pipe(conn)
   conn.pipe(process.stdout)
 
 })

 server.listen(999, () => {
   console.log(server.address())
 })

//另一个文件
 const net = require('net')
 net.connect(9999, '10.1.1.1').pipe(process.stdout)

npm的package.json链接

  1. name: 包的名字字符串
  2. version: 版本号三位数 "0.0.1"
  3. description: 字符串描述 keywords: 数组,被搜索的关键词
  4. homepage: 链接
  5. bugs: 对象里面包括url提交bug的页面,email可以把bug用邮件发
  6. license:
  7. files:
  8. main:包的入口文件
  9. bin:对象里又多个字段,{"myapp":"文件路径", "myfoo":"文件路径"} 命令行工具 ,可执行文件
  10. man:帮助文档
  11. scripts:对象 "scripts":{ "foo":"node foo.js", "bar":"ls -lha", "start": "node ./bin/www", "test": , "build": , "dev": , } 当npm run foo的时候等于执行node foo.js 也就是npm run中的run等于scrips字段里相应的内容。只有test和start可以略过run, npm start即可
  12. dependencies:{自己依赖的第三方代码} 通过npm install下载自己所有的依赖包。^兼容版本, 主版本号第一个数不改变就行。

express框架

用函数队列的方式来处理每一个网络请求

node的event loop

使得Node.js可以在单线程执行非阻塞I/O操作(非阻塞: 遇到异步操作调一下就离开,把自己的异步操作卸载给操作系统)。 等系统执行完异步操作后通知Node.js 可以把回调放到poll队列去执行。

步骤:

  1. INIT (当Node.js开始初始event loop)
  2. INPUT SCRIPT (执行初始输入代码)
  3. 每遇到异步代码往各个事件循环的不同阶段(每个阶段都是先进先出的队列)安排函数
 - timers:执行被setTimeout() 【注意如果设置0自动被改成1毫秒】 和setInterval()设定的到时间的函数.
 - pending callbacks:被延迟到下一圈的I/O回调函数,poll阶段来不及执行的函数
 - idle, prepare: 内部系统
 - poll:  第一件是计算在这个阶段可以停顿多久以及等待I/O;
 第二件是执行除了setTimeout,setInterval, setImmediate,close以外的所有回调函数。
 - check: setImmediate()的回调函
 - close callbacks:  close的回调函数 
 - 额外:nextTick()的函数不管当前是什么阶段,走下一个阶段之前先执行。也就是每个阶段间隙之间执行。特殊:如果nextTick里又安排nextTick无限安排会导致程序一直卡在间隙里。其它异步函数是安排在下一轮。nextTick优先于promise.then。 
  1. 开始进入事件循环。从timer开始执行自己阶段里被安排的到时间的函数,执行时如果又遇到异步代码继续往各个阶段安排函数。把自己阶段里的所有函数都执行完继续走下一个阶段。
  2. 一直循环执行,等到所有阶段都没有被安排函数就会退出node