本文共 12729 字,大约阅读时间需要 42 分钟。
本人是个新手,写下博客用于自我复习、自我总结。 如有错误之处,请各位大佬指出。 学习资料来源于:尚硅谷
• Node.js是一个能够在服务器端运行JavaScript的开放源代码、跨平台JavaScript运行环境。
• Node是对ES标准一个实现,Node也是一个JS引擎。(Node仅仅对ES标准进行了实现,所以在Node中不包含DOM 和 BOM)
• Node大部分基本模块都用JavaScript编写。在Node出现之前,JS通常作为客户端程序设计语言使用,以JS写出的程序常在用户的浏览器上运行。(它的底层是使用c++的编写的)
• Node的中js引擎使用的是chrome的v8引擎
• Node主要用于编写像Web服务器一样的网络应用,这和PHP和Python是类似的。但是Node与其他语言最大的不同之处在于,PHP等语言是阻塞的而Node是非阻塞的。
• Node是事件驱动的。开发者可以在不使用线程的情况下开发出一个能够承载高并发的服务器。其他服务器端语言难以开发高并发应用,而且即使开发出来,性能也不尽人意。Node正是在这个前提下被创造出来。
在学习NodeJS前,需要先提到命令行窗口。相信大家对命令行窗口应该不陌生:
C:\WINDOWS\system32 也就是当前所在目录。在>
后可以写一些指令,按下Enter
即可执行。(按下Tab
可以帮助我们补全需要的文件名) 相信大家都设置过环境变量: hello.js:
在 idea 中,左下角可以调出Terminal,和直接使用命令行没什么区别。
一般情况下,它会帮我们自动配置,达到的效果是直接run就可以运行。如果没配置成功,我们可以去配置:
点开idea的settings,搜索node就可以找到如下页面。然后将node设置到下载的位置即可。 现在,我们就可以直接运行js文件了。• 当项目功能越来越多,代码量便也会越来越多,后期的维护难度会增大,此时在JS方面就会考虑使用模块化规范去管理。
• 模块化可以有多种形式,但至少应该提供能够将代码分割为多个源文件的机制,这样方便管理与维护。(虽然在引入的时候,需要通过script标签引入很多的js文件,比较麻烦)
(当然,目前es6已经更新了模块化的功能,在此之前JS是没有模块化的,所以就提出了各种规范:Common和AMD)• CommonJS 的模块功能可以帮我们解决该问题。
• CommonJS规范的提出,主要是为了弥补当前JavaScript没有模块化标准的缺陷。
• CommonJS规范为JS指定了一个美好的愿景,希望JS能够在任何地方运行。
(相关内容在之后的JS模块化篇章会详细说明)
/* 模块化 - 在Node中,一个js文件就是一个模块 - 在Node中,每一个js文件中的js代码都是独立运行在一个函数中 而不是全局作用域,所以一个模块的中的变量和函数在其他模块中无法访问 */console.log("我是一个模块,我是02.module.js");/*外部不可见*/var a = 10 ; /*我们可以通过 exports 来向外部暴露变量和方法 只需要将需要暴露给外部的变量或方法设置为exports的属性即可 *///向外部暴露属性或方法exports.x = "我是02.module.js中的x";exports.y = "我是y";exports.fn = function () { };
//引入其他的模块/* 在node中,通过require()函数来引入外部的模块 require()可以传递一个文件的路径作为参数,node将会自动根据该路径来引入外部模块 这里路径,如果使用相对路径,必须以.或..开头 使用require()引入模块以后,该函数会返回一个对象,这个对象代表的是引入的模块 */var md = require("./02.module");console.log(md);为什么选择向外部暴露变量和方法的方式?如果引入的文件中的变量和函数是全局作用域,那么在主页面中,我们也许会进行误操作,更改了这个值(因为变量名有可能相同)。这样就会导致结果出现差错。我们需要的是,一个模块的中的某些变量和函数在其他模块中无法访问。
根据上例,其实CommonJS对模块的定义十分简单:模块引用、模块定义、模块标识。
require()
函数来引入一个模块var 变量 = require("模块的标识");
exports
例子: exports.属性 = 属性值;
exports.方法 = 函数;
module.exports
例子: module.exports.属性 = 属性值;
module.exports.方法 = 函数;
module.exports = {};
var fs = require("fs");
var express = require("express");
对于自定义的文件模块,需要通过文件的路径来对模块进行引入。 路径可以是绝对路径,如果是相对路径必须以./或 …/开头。 var router = require("./router");
math.js
/* 定义一个模块 math - 在该模块中提供两个方法 add(a , b); //求两个数的和 mul(a , b); //求两个数的积 */module.exports.add = function (a , b) { return a+b;};module.exports.mul = function (a , b) { return a*b;};
//引入其他的模块/* 在node中,通过require()函数来引入外部的模块 require()可以传递一个文件的路径作为参数,node将会自动根据该路径来引入外部模块 这里路径,如果使用相对路径,必须以.或..开头 使用require()引入模块以后,该函数会返回一个对象,这个对象代表的是引入的模块 我们使用require()引入外部模块时,使用的就是模块标识,我们可以通过模块标识来找到指定的模块 - 模块分成两大类 核心模块 - 由node引擎提供的模块 - 核心模块的标识就是,模块的名字 文件模块 - 由用户自己创建的模块 - 文件模块的标识就是文件的路径(绝对路径,相对路径) 相对路径使用.或..开头 */var math = require("./math");var fs = require("fs");console.log(math.add(123,456));//console.log(fs);
接下来证明不用模块定义,外部的模块确实无法访问:
var a = 10;/* 在node中有一个全局对象 global,它的作用和网页中window类似 在全局中创建的变量都会作为global的属性保存 在全局中创建的函数都会作为global的方法保存* */console.log(global.a); //undefined
当然,当我们不设var
的话,它就会是全局变量。
a = 10;console.log(global.a); //10
为什么能办到?具体的原因是:
当node在执行模块中的代码时,它会首先在代码的最顶部,添加如下代码
function (exports, require, module, __filename, __dirname) {
在代码的最底部,添加如下代码 }
也就是说,虽然在上面的所有例子中,没有任何函数包围,但实际上模块中的代码都是包装在一个函数中执行的,并且在函数执行时,同时传递进了5个实参:
exports
:该对象用来将变量或函数暴露到外部require
:函数,用来引入外部的模块module
: module
代表的是当前模块本身, 那么exports
就是module
的属性exports
导出,也可以使用module.exports
导出__filename
:当前模块的完整路径。如:D:\Projects\01.node\04.module.js
__dirname
:当前模块所在文件夹的完整路径。如:D:\Projects\01.node
//console.log(arguments); //会看到五个实参/* arguments.callee - 这个属性保存的是当前执行的函数对象* */console.log(arguments.callee + "");console.log(arguments.length);console.log(exports);console.log(module.exports == exports); //trueconsole.log(__dirname);
之前提到可以使用 exports
导出,也可以使用module.exports
导出。其实它们之间的用法是有区别的:
helloModule.js
module.exports.name = "孙悟空";module.exports.age = 18;module.exports.sayName = function () { console.log("我是孙悟空");};/* 无法使用这种方式,会报错。但module.exports可以使用这种方式exports = { name:"猪八戒", age:28, sayName:function () { console.log("我是猪八戒"); }};*/
var hello = require("./helloModule");/* exports 和 module.exports - 通过exports只能使用.的方式来向外暴露内部变量 exports.xxx = xxx - 而module.exports既可以通过.的形式,也可以直接赋值 module.exports.xxx = xxxx module.exports = {} */console.log(hello.name);console.log(hello.age);hello.sayName();
• CommonJS的包规范允许我们将一组相关的模块组合到一起,形成一组完整的工具。
• CommonJS的包规范由包结构和包描述文件两个部分组成。
• 包结构:用于组织包中的各种文件(其中package.json是必需的)
• 包描述文件:描述包的相关信息,以供外部读取分析 简例:package.json:(注意:json文件中不能写注释!){ "name": "01.node", "version": "0.0.1", "dependencies": { }}
(当我们看项目的时候,比较靠谱的都会有package.json)
CommonJS包规范是理论,NPM(Node Package Manager)是其中一种实践。对于Node而言,NPM帮助其完成了第三方模块的发布、安装和依赖等。借助NPM,Node与第三方模块之间形成了很好的一个生态系统。(它就类似于某软件管家,我们只要在其中去搜,就可以得到相关信息)(当你下载node的时候,NPM也在其中)
查看npm的版本
查看所有模块的版本
搜索包(我们可以搜索到相关的全部的包,包括描述和作者名等)(需要联网)
安装包 (在安装包时,它起初会在指令当前目录中安装包,但是它会检测其中有没有package.json,如果没有,它会在安装在其他的位置。但有一些包不需要这么判断就会下载到当前目录下。所以如果要安装包,建议在想要安装的目录下,写好一个package.json。(使用npm init
))
/* 通过npm下载的包都放到node_modules文件夹中 我们通过npm下载的包,直接通过包名引入即可 node在使用模块名字来引入模块时,它会首先在当前目录的node_modules中寻找是否含有该模块 如果有则直接使用,如果没有则去上一级目录的node_modules中寻找 如果有则直接使用,如果没有则再去上一级目录寻找,直到找到为止 直到找到磁盘的根目录,如果依然没有,则报错 */var math = require("math");console.log(math);console.log(math.add(123,456));
删除包
安装包,并添加到依赖中(常用)
常用是因为:node项目中,很少有人传node_modules,因为用的包可能会很多,这会影响下载速度,同时也不能保证多年后,它的版本是最新版。所以当我们下载别人的项目时,没有node_modules,程序显然不会运行,这时我们想去下载这些包,之前还得一个一个去下载,但现在把需要的包放在了配置信息里,我们只需要npm install
它就可以把配置信息里的这些包全部下载出来。
添加前:
添加后:全局安装包(全局安装的包一般都是一些工具,是给计算机用的,不是给项目用的)
毕竟npm服务器不在国内,我们下载包可能会出现问题。帮我们解决了这个问题。这是一个完整 npmjs.org
镜像,你可以用此代替官方版本(只读),同步频率目前为10分钟一次以保证尽量与官方服务同步。当然,如果完全替代,用起来肯定不太灵活,有的时候可能还是想去国外npm上下载,所以就出现了cnpm的方案。当我们想连淘宝服务器就用cnpm,想连海外服务器就用npm。(cnpm的指令和npm一样)
配置:(安装完成即可)
• 从结构上看Buffer非常像一个数组,操作的方法也和数组类似。
• 数组中不能存储二进制的文件,而Buffer就是专门用来存储二进制数据。虽然Buffer中存储的都是二进制数据,但是在显示时都是以十六进制的形式显示。
• 使用Buffer不需要引入模块,直接使用即可。
• Buffer作用:服务器端接收到一些用户发来的数据,可以暂时存储在缓存区中。
• 这里只介绍以下一些简单的用法,更多的用法可以自行百度。
/* Buffer(缓冲区) - Buffer的结构和数组很像,操作的方法也和数组类似 - 数组中不能存储二进制的文件,而buffer就是专门用来存储二进制数据 - 使用buffer不需要引入模块,直接使用即可 - 在buffer中存储的都是二进制数据,但是在显示时都是以16进制的形式显示 */var str = "Hello 你好";//将一个字符串保存到buffer中var buf = Buffer.from(str);console.log(buf);console.log(buf.length); //占用内存的大小console.log(str.length); //字符串的长度
输出结果:
• 从输出结果可以看出来,buf.length
输出的是占用内存的大小。实际上Buffer中的一个元素,占用内存中的一个字节。(汉字占三个字节) /* Buffer的大小一旦确定,则不能修改,Buffer实际上是对底层内存的直接操作 *///创建一个指定大小的buffer//buffer构造函数都是不推荐使用的//var buf2 = new Buffer(10);//10个字节的buffer//console.log(buf2.length);//创建一个10个字节的buffervar buf2 = Buffer.alloc(10);//通过索引,来操作buf中的元素buf2[0] = 88;buf2[1] = 255;buf2[2] = 0xaa;buf2[3] = 255;//buf2[15] = 15; //存放不了//buf2[4] = 256; //溢出,舍弃多的位数(从前舍弃)// 1 00000000 舍弃1console.log(buf2);//只要数字在控制台或页面中输出一定是10进制console.log(buf2[2]);console.log(buf2[0].toString());console.log(buf2[0].toString(16));//也可以手动修改输出的进制console.log("-----------------");for(var i=0 ; i
输出结果:
• 需要注意的是:Buffer的大小一旦确定,则不能修改。因为Buffer中的内存不是通过JavaScript分配的,而是在底层通过C++申请的。也就是说Buffer实际上是对底层内存的直接操作,它会帮我们分配一段连续的空间。如果能修改,在使用过程中,新加入的元素不一定能和之前的Buffer占有连续空间,这样随意存储,会导致性能下降。• 一定要留意存储元素的时候,想要存储的元素是几进制,因为输出的时候都会转换成十六进制。但是,只要数字在控制台或页面中输出一定是十进制。同时toString()
方法也可以通过加入参数,修改输出的进制。
• Buffer数组中的元素,十进制数不能超过255。因为一个元素占据一个字节,一个字节有8位,也就是二进制:(从 00000000 到 11111111),也就是十进制:(从0到255)。
创建一个指定大小的Buffer,但是Buffer中可能含有敏感数据。这是因为内存空间肯定是反复去用的,在使用Buffer会给内存空间清零,但是这种构建方法不会去清理,所以可能会读取到用户存储的信息,出现安全隐患,所以它是Unsafe
。既然如此,为什么这种方法还能存在?就是因为它不清理,所以效率高。当然平常依然使用alloc
去构建。
//Buffer.allocUnsafe(size) 创建一个指定大小的buffer,但是buffer中可能含有敏感数据var buf3 = Buffer.allocUnsafe(10);console.log(buf3);
将缓冲区中的数据转换为字符串。
/* buf.toString() 将缓冲区中的数据转换为字符串 */var buf4 = Buffer.from("我是一段文本数据");console.log(buf4.toString());
• 文件系统(File System) 简单来说就是通过Node来操作系统中的文件。在Node中,与文件系统的交互是非常重要的,因为服务器的本质就将本地的文件发送给远程的客户端。
• 使用文件系统,需要先引入fs模块,fs是核心模块,直接引入不需要下载。
• fs模块中所有的操作都有两种形式可供选择同步和异步。
• 同步文件系统会阻塞程序的执行,也就是除非操作完毕,否则不会向下执行代码。
• 异步文件系统不会阻塞程序的执行,而是在操作完成时,通过回调函数将结果返回。
/* 文件系统(File System) - 文件系统简单来说就是通过Node来操作系统中的文件 - 使用文件系统,需要先引入fs模块,fs是核心模块,直接引入不需要下载 同步文件的写入 - 手动操作的步骤 1.打开文件 fs.openSync(path, flags[, mode]) - path 要打开文件的路径 - flags 打开文件要做的操作的类型 r 只读的 w 可写的 - mode 设置文件的操作权限,一般不传 返回值: - 该方法会返回一个文件的描述符作为结果,我们可以通过该描述符来对文件进行各种操作 2.向文件中写入内容 fs.writeSync(fd, string[, position[, encoding]]) - fd 文件的描述符,需要传递要写入的文件的描述符 - string 要写入的内容 - position 写入的起始位置 - encoding 写入的编码,默认utf-8 3.保存并关闭文件 fs.closeSync(fd) - fd 要关闭的文件的描述符 */var fs = require("fs");//打开文件var fd = fs.openSync("hello.txt" , "w");//向文件中写入内容fs.writeSync(fd , "今天天气真不错~~~", 3);//关闭文件fs.closeSync(fd);console.log("程序向下执行~~~");
/** 异步文件写入 fs.open(path, flags[, mode], callback) - 用来打开一个文件 - 异步调用的方法,结果都是通过回调函数的参数返回的 - 回调函数两个参数: err 错误对象,如果没有错误则为null fd 文件的描述符 fs.write(fd, string[, position[, encoding]], callback) - 用来异步写入一个文件 fs.close(fd, callback) - 用来关闭文件 *///引入fs模块var fs = require("fs");//打开文件fs.open("hello2.txt","w",function (err , fd) { //判断是否出错 if(!err){ //如果没有出错,则对文件进行写入操作 fs.write(fd,"这是异步写入的内容",function (err) { if(!err){ console.log("写入成功~~"); } //关闭文件 fs.close(fd , function (err) { if(!err){ console.log("文件已关闭~~~"); } }); }); }else{ console.log(err); }});console.log("程序向下执行~~~");异步文件系统不会阻塞程序的执行:
(但实际上同步写入和异步写入还是比较麻烦的,开发中也不常用)
/* 简单文件写入 fs.writeFile(file, data[, options], callback) fs.writeFileSync(file, data[, options]) - file 要操作的文件的路径 - data 要写入的数据 - options 选项,可以对写入进行一些设置 - flag(常用的三个:r w a) r 只读 w 可写 a 追加 (除了flag还有其他设置,不过不常用。一般不写options,直接按默认的即可) - callback 当写入完成以后执行的函数 *///引入fs模块var fs = require("fs");fs.writeFile("hello4.txt","这是通过writeFile写入的内容",{ flag:"w"} , function (err) { if(!err){ console.log("写入成功~~~"); }else{ console.log(err); }});
同步、异步、简单文件的写入都不适合大文件的写入,假如要写入的是个100MB的文件,那根据之前,我们还得先把它放在缓存区里。这样一来性能较差,还容易导致内存溢出。
/* 同步、异步、简单文件的写入都不适合大文件的写入,性能较差,容易导致内存溢出 */var fs = require("fs");//流式文件写入//创建一个可写流/* fs.createWriteStream(path[, options]) - 可以用来创建一个可写流 - path,文件路径 - options 配置的参数 */var ws = fs.createWriteStream("hello3.txt");//可以通过监听流的open和close事件来监听流的打开和关闭/* on(事件字符串,回调函数) - 可以为对象绑定一个事件 once(事件字符串,回调函数) - 可以为对象绑定一个一次性的事件,该事件将会在触发一次以后自动失效* */ws.once("open",function () { console.log("流打开了~~~");});ws.once("close",function () { console.log("流关闭了~~~");});//通过ws向文件中输出内容ws.write("通过可写流写入文件的内容");ws.write("今天天气真不错");ws.write("锄禾日当午");ws.write("汗滴禾下土");//关闭流ws.end();
/* 简单文件读取 fs.readFile(path[, options], callback) fs.readFileSync(path[, options]) - path 要读取的文件的路径 - options 读取的选项 - callback回调函数,通过回调函数将读取到内容返回(err , data) err 错误对象 data 读取到的数据,会返回一个Buffer */var fs = require("fs");fs.readFile("hello3.txt" , function (err , data) { if(!err){ console.log(data); console.log(data.toString()); //将data写入到文件中 fs.writeFile("D:/hello.txt",data,function(err){ if(!err){ console.log("文件写入成功"); } } ); }});
输出结果:
Buffer未显示完全:现在就可以说明一个问题:为什么要先把数据先放在Buffer里?
因为读取数据的时候,读取的不一定是文本,还有可能是jpg、mp3等格式的文件,接下来看代码:
var fs = require("fs");fs.readFile("an.jpg" , function (err , data) { if(!err){ console.log(data); console.log(data.toString()); //将data写入到文件中 fs.writeFile("D:/hello.jpg",data,function(err){ if(!err){ console.log("文件写入成功"); } } ); }});
未显示完全:
我们可以看到输出data依然是Buffer,而用utf-8对图片编码,显然会出现乱码。也正是因为data是Buffer,所以它的兼容性很高,当我们再把它转存的时候,图片依然清晰可见。
流式文件读取也适用于一些比较大的文件,可以分多次将文件读取到内存中。
/* 流式文件读取也适用于一些比较大的文件,可以分多次将文件读取到内存中 (每一组大小为65535) */var fs = require("fs");//创建一个可读流var rs = fs.createReadStream("a.mp3");//创建一个可写流var ws = fs.createWriteStream("b.mp3");//监听流的开启和关闭rs.once("open",function () { console.log("可读流打开了~~");});rs.once("close",function () { console.log("可读流关闭了~~"); //数据读取完毕,关闭可写流 ws.end();});//如果要读取一个可读流中的数据,必须要为可读流绑定一个data事件,data事件绑定完毕,它会自动开始读取数据rs.on("data", function (data) { //console.log(data); console.log(data.length); //将读取到的数据写入到可写流中 ws.write(data);});ws.once("open",function () { console.log("可写流打开了~~");});ws.once("close",function () { console.log("可写流关闭了~~");});(中间省略很多65536) 还有更简便方法:
var fs = require("fs");//创建一个可读流var rs = fs.createReadStream("a.mp3");//创建一个可写流var ws = fs.createWriteStream("b.mp3");//pipe()可以将可读流中的内容,直接输出到可写流中rs.pipe(ws);
转载地址:http://hoyki.baihongyu.com/