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 提供了事件模块,可以让我们自己去注册事件,触发事件。发布订阅模式 有三点需要注意:
- 大多数Node.js核心API都是采用惯用的异步事件驱动架构,例如fs/http;
- 所有能触发事件的对象都是EventEmitter类的实例;
- 事件流程:引入模块 - 创建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:
- fs.readFile():读文件
- fs.writeFile():写文件
- fs.unlink():删除文件
- fs.rename():重命名文件
- fs.copyFile():复制文件
- fs.existSync() 同步 / fs.access() 异步:判断文件是否存在
- fs.mkdir():创建文件夹
- fs.rmdir():删除文件夹
- fs.readdir():读取文件夹
- 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状态码
- 101:websocket 双向通信
- 200:成功
- 204:没有响应体
- 206:断点续传
- 301:永久重定向
- 302:临时重定向
- 304:缓存
- 401:没有登陆没有权限
- 403:登陆了没有权限
- 404:找不到资源
- 405:请求方法不存在
- 500:服务器错误
- 502:负载均衡
- 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');
常用的事件:
- myReadStream.on(‘open’, callback):监听文件打开,触发事件
- myReadStream.on(‘data’, callback):监听文件读取,返回每次读取到的值
- myReadStream.on(‘end’, callback):监听文件读取结束,触发事件
- myReadStream.on(‘close’, callback):监听文件关闭,触发事件
- myReadStream.on(‘error’, callback):监听文件读取错误,触发事件
- myReadStream.pipe(res):把读取到的写入到一个流里面。
5.1 buffer 缓冲器
buffer主要就是用在fs中,buffer就是把二进制表现成了10进制,可以和字符串无条件转化。 buffer代表了内存,不能扩展,需要有一个固定大小。
buffer和数组很类似,buffer[0]
取值
使用方法:Buffer.alloc
、Buffer.allocUnsafa
let buf = Buffer.alloc(10); //申请10个内存
let buf2 = Buffer.allocUnsafe(10); // 抓取10个内存,可能还没来得及释放,不安全
buf2.fill(0); // 填充,手动擦除内存
let buf3 = Buffer.from('buffer'); // 字符串填充,不可变。
常用的方法:
- buf.slice():截取值,和数组的类似。
- Buffer.isBuffer(buf):判断是不是buffer,和数组的isArray一样。
- copy(targetBuffer,targetStratIndex, sourceStartIndex, sourceEndIndex):拷贝buffer,buf1.copy(buf2,0,1,3);
- buf.toString():转化成字符串,可以设置进制数或者base64、utf8
- buf.concat():拼接,和数组的类似。
let newBuffer = Buffer.concat(bf1, bf2,10)
; 第三个参数为buffer的大小,如果不填则默认bf1+bf2的长度。 - buf.fill():填充。
- Buffer.from(‘any’):给buffer直接创建一个值。
- 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 跨域
如果想设置跨域可以设置一下header
: res.setHeader('Access-Control-Allow-Origin', '*')
;
*
通配符这里写具体的域名。
cors包解决跨域问题。
8.2 cookie
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
还是设置Header
:res.setHeader('Access-Control-Allow-Headers', 'xxheader1, xxheader2')
;
多个header用,
隔开。
8.4 请求
HTTP请求分为简单请求和复杂请求。
- 简单请求:请求方式为
HEAD/GET/POST
,请求头不超出Accept/ Accept-Language/ Content-Language/ last-Event-ID/ Content-Type(只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain)
- 复杂请求:不简单就复杂了。 复杂请求会先发一个预检请求
PreFlight 也就是OPTIONS
看看服务器能不能行,能行的话再发送正常请求。PS:OPTIONS
发送是有间隔的,可以配置header来设置缓存时长。 - 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的途径
- ——百度百科。
此时,客户端知道有代理服务器的存在,但是原始服务器并不知道是代理服务器在请求他,对原始服务器来说代理服务器是透明的,这就是正向代理。
正向代理主要做:
- 认证
- 权限验证
9.2 反向代理
- 反向代理服务器位于用户与目标服务器之间,但是对于用户而言,反向代理服务器就相当于目标服务器,即用户直接访问反向代理服务器就可以获得目标服务器的资源。
此时,客户端并不知道自己访问的是代理服务器还是目标服务器,所以代理服务器对于用户是透明的,所以是反响代理。
反向代理主要做:
- 节约了有限的IP资源
- 缓存,提高了访问速度,(CDN就是反向代理的实践)
- 跨域,前端必须提一嘴跨域。
- 提高了服务器的安全性。
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);