node.js学习及项目搭建过程
1 我的环境
win 10 mysql vscode 服务器自建
2 环境的安装(不讲)
3 简单服务器的尝试
命令行:>>node xxx.js
简单服务器的搭建
var http = require(“http”);
http.createServer(
function(request,response){
response.writeHead(200,{‘Content-Type’:“html”});
response.end(“Hello world\n”);
}).listen(8888);
console.log(“success”);
然后打开CMD找到文件地址运行node xxx.js输出success表示服务器运行成功,在浏览器上输入http://localhost:8888
输出Hello world表示成功
4 学习Node.js的基本目录结构
,
node_modules:用来存放项目的依赖库。
public:用来存放静态文件(css,js,img)。
routes:路由控制器。
views:视图目录(相当于MVC中的V)。
app.js:项目入口及程序启动文件。
package.json:包描述文件及开发者信息。
-----------------------------------------------其他,自己创建使用------------------------------
models:数据模型(相当于MVC中的M)。
controllers:控制器,对请求的操作(相当于MVC中的C)。
tools:工具库。
config:配置目录。
test:测试目录。
README.md:项目说明文件。
包结构
包实际上是一个存档文件,即一个目录直接打包为.zip或tar.gz格式的文件,安装后解压还原为目录。完全符合CommonJS规范的包目录应该包含如下这些文件。
1、package.json:包描述文件
2、bin:用于存放可执行二进制文件的目录
3、lib:用于存放JavaScript代码的目录
4、doc:用于存放文档的目录
5、test:用于存放单元测试用例的代码
包描述文件package.json
package.json文件,定义了项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证等元数据)。npm install命令根据这个配置文件,自动下载所需的模块,也就是配置项目所需的运行和开发环境。
有了package.json文件,直接使用npm install或yarn install,就会在当前目录中安装所需要的模块。
如果一个模块不在package.json文件之中,可以单独安装这个模块,并使用相应的参数,将其写入package.json文件之中。
基本字段
{
“name”: “api”,
“version”: “1.0.0”,
“description”: “”,
“main”: “index.js”,
“scripts”: {
“test”: “echo “Error: no test specified” && exit 1”
},
“keywords”: [],
“author”: “”,
“license”: “ISC”
}
1、name——包名。规范定义它需要由小写的字母和数字组成,可以包含.、_和-,但不允许出现空格。包名必须是唯一的,以免对外公布时产生重名冲突的误解。除此之外,NPM还建议不要在包名中附带上node或js来重复标识它是JavaScript或Node模块 。
2、version——版本号。一个语义化的版本号,这在http://semver.org/上有详细定义,通常为major.minor.revision格式。该版本号十分重要,常常用于一些版本控制的场合。
【必需字段】
1、description——包简介。方便别人了解该模块作用,搜索的时候也有用
“description”: “包介绍”
2、keywords——关键词数组,NPM中主要用来做分类搜索。一个好的关键词数组有利于用户快速找到该包。
3、maintainers——包维护者列表。每个维护者由name、email和web地址这3个属性组成。
4、contributors——贡献者列表。在开源社区中,为开源项目提供代码是经常出现的事情,如果名字能出现在知名项目的contributors列表中,是一件比较有荣誉感的事。列表中的第一个贡献应当是包的作者本人。它的格式与维护者列表相同。
5、bugs——一个可以反馈bug的网页地址或邮件地址。
6、licenses——当前包所使用的许可证列表,表示这个包可以在哪些许可证下使用。
7、repositories——托管源代码的位置列表,表明可以通过哪些方式和地址访问包的源代码。
8、dependencies——使用当前包所需要依赖的包列表。这个属性十分重要,NPM会通过这个属性帮助自动加载依赖的包。详细介绍请见npm简单使用。
【可选字段】
1、homepage——当前包的网站地址
“homepage”: “https://github.com/napcs/node-livereload#readme”
2、os——操作系统支持列表。这些操作系统的取值包括aix、freebsd、linux、macos、solaris、vxworks、windows。如果设置了列表为空,则不对操作系统做任何假设。
3、cpu——CPU架构的支持列表,有效的架构名称有arm、mips、ppc、sparc、x86和x86_64。同os一样,如果列表为空,则不对CPU架构做任何假设。
4、engine——支持的JavaScript引擎列表,有效的引擎取值包括ejs、flusspferd、gpsee、jsc、spidermonkey、narwhal、node和v8。
“engines”: {
“node”: “>=0.4.0”
}
5、builtin——标志当前包是否是内建在底层系统的标准组件。
6、directories——包目录说明
“directories”: {}
7、implements——实现规范的列表。标志当前包实现了CommonJS的哪些规范。
8、scripts——脚本说明对象。它主要被包管理器用来安装、编译、测试和卸载包。scripts指定了运行脚本命令的npm命令行缩写,比如start指定了运行npm run start时,所要执行的命令
“scripts”: {
“test”: “mocha”
}
【其他字段】
1、author——包作者
2、bin——指定各个内部命令对应的可执行文件的位置,这样就不需要配置环境变量啦。
“bin”: {
“livereload”: “./bin/livereload.js”
}
3、main——加载的入口文件。模块引入方法require()在引入包时,会优先检查这个字段,并将其作为包中其余模块的入口。如果不存在这个字段,require()方法会查找包目录下的index.js、index.node、index.json文件作为默认入口
“main”: “./lib/livereload.js”
4、devDependencies——项目开发所需要的模块。一些模块只在开发时需要依赖。配置这个属性,可以提示包的后续开发者安装依赖包。类比于dependencies字段
“devDependencies”: {
“coffee-script”: “>= 1.8.0”,
“mocha”: “>= 1.0.3”,
“request”: “>= 2.9.203”,
“should”: “>= 0.6.3”,
“sinon”: “^1.17.4”
}
Node模块
与JS比较
NodeJS是区别于JS的,在JS中的顶层对象是window,而在Node中的顶层对象是global。
实际上,JS也存在global对象,只是其并不对外访问,而使用window对象指向global对象而已。
在JS中,通过var a = 100;是可以通过window.a来得到100的。但在NodeJS中,是不能通过global.a来访问,得到的是undefined。这是因为var a = 100;这个语句中的变量a,只是模块范围内的变量a,而不是global对象下的a。
在NodeJS中,一个文件就是一个模块,每个模块都有自己的作用域。使用var来声明的一个变量,它并不是全局的,而是属于当前模块下。
模块
Node中模块分为两类:一类是Node提供的模块,称为核心模块;另一类是用户编写的模块,称为文件模块。
核心模块部分在Node源代码的编译过程中,编译成了二进制执行文件。在Node进程启动时,部分核心模块就被直接加载进内存中,所以这部分核心模块引入时,文件定位和编译执行这两个步骤可以省略掉,并且在路径分析中优先判断,所以它的加载速度是最快的。
文件模块则是在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢。
模块加载
在JS中,加载模块使用script标签即可,而在NodeJS中,在一个模块中,使用require()方法加载另一个模块。
【缓存加载】
再展开介绍require()方法的标识符分析之前,需要知道,与前端浏览器会缓存静态脚本文件以提高性能一样,Node对引入过的模块都会进行缓存,以减少二次引入时的开销。不同的地方在于,浏览器仅仅缓存文件,而Node缓存的是编译和执行之后的对象。
不论是核心模块还是文件模块,require()方法对相同模块的二次加载都一律采用缓存优先的方式,这是第一优先级的。不同之处在于核心模块的缓存检查先于文件模块的缓存检查。
【标识符分析】
require()方法接受一个标识符作为参数。在Node实现中,正是基于这样一个标识符进行模块查找的。模块标识符在Node中主要分为以下几类:
核心模块,如http、fs、path等;
或…开始的相对路径文件模块;
以/开始的绝对路径文件模块;
非路径形式的文件模块,如自定义的connect模块。
根据参数的不同格式,require命令去不同路径寻找模块文件
1、如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。比如,require(’/home/marco/foo.js’)将加载/home/marco/foo.js。
2、如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,require(’./circle’)将加载当前脚本同一目录的circle.js。
3、如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)。
[注意]如果是当前路径下的文件模块,一定要以./开头,否则nodejs会试图去加载核心模块,或node_modules内的模块。
【文件扩展名分析】
require()在分析标识符的过程中,会出现标识符中不包含文件扩展名的情况。CommonJS模块规范也允许在标识符中不包含文件扩展名,这种情况下,Node会先查找是否存在没有后缀的该文件,如果没有,再按.js、.json、.node的次序补足扩展名,依次尝试。
在尝试的过程中,需要调用fs模块同步阻塞式地判断文件是否存在。因为Node是单线程的,所以这里是一个会引起性能问题的地方。小诀窍是:如果是.node和.json文件,在传递给require()的标识符中带上扩展名,会加快一点速度。另一个诀窍是:同步配合缓存,可以大幅度缓解Node单线程中阻塞式调用的缺陷。
【目录分析和包】
在分析标识符的过程中,require()通过分析文件扩展名之后,可能没有查找到对应文件,但却得到一个目录,这在引入自定义模块和逐个模块路径进行查找时经常会出现,此时Node会将目录当做一个包来处理。
在这个过程中,Node对CommonJS包规范进行了一定程度的支持。首先,Node在当前目录下查找package.json(CommonJS包规范定义的包描述文件),通过JSON.parse()解析出包描述对象,从中取出main属性指定的文件名进行定位。如果文件名缺少扩展名,将会进入扩展名分析的步骤。
而如果main属性指定的文件名错误,或者压根没有package.json文件,Node会将index当做默认文件名,然后依次查找index.js、index.json、index.node。
如果在目录分析的过程中没有定位成功任何文件,则自定义模块进入下一个模块路径进行查找。如果模块路径数组都被遍历完毕,依然没有查找到目标文件,则会抛出查找失败的异常。
访问变量
如何在一个模块中访问另外一个模块中定义的变量呢?
【global】
最容易想到的方法,把一个模块定义的变量复制到全局环境global中,然后另一个模块访问全局环境即可。
//a.js
var a = 100;
global.a = a;
//b.js
require(’./a’);
console.log(global.a);//100
这种方法虽然简单,但由于会污染全局环境,不推荐使用。
【module】
而常用的方法是使用nodejs提供的模块对象Module,该对象保存了当前模块相关的一些信息。
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
if (parent && parent.children) {
parent.children.push(this);
}
this.filename = null;
this.loaded = false;
this.children = [];
}
module.id 模块的识别符,通常是带有绝对路径的模块文件名。
module.filename 模块的文件名,带有绝对路径。
module.loaded 返回一个布尔值,表示模块是否已经完成加载。
module.parent 返回一个对象,表示调用该模块的模块。
module.children 返回一个数组,表示该模块要用到的其他模块。
module.exports 表示模块对外输出的值。
【exports】
module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。
//a.js
var a = 100;
module.exports.a = a;
//b.js
var result = require(’./a’);
console.log(result);//’{ a: 100 }’
为了方便,Node为每个模块提供一个exports变量,指向module.exports。造成的结果是,在对外输出模块接口时,可以向exports对象添加方法。
console.log(module.exports === exports);//true
[注意]不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系。
模块编译
编译和执行是模块实现的最后一个阶段,定位到具体的文件后,Node会新建一个模块对象,然后根据路径载入并编译。对于不同的文件扩展名,其载入方法也有所不同,具体如下所示。
js文件——通过fs模块同步读取文件后编译执行。
node文件——这是用C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件。
json文件——通过fs模块同步读取文件后,用JSON.parse()解析返回结果。
其余扩展名文件——它们都被当做.js文件载入。
每一个编译成功的模块都会将其文件路径作为索引缓存在Module._cache对象上,以提高二次引入的性能。
根据不同的文件扩展名,Node会调用不同的读取方式,如.json文件的调用如下:
// Native extension for .json
Module._extensions[’.json’] = function(module, filename) {
var content = NativeModule.require(‘fs’).readFileSync(filename, ‘utf8’);
try {
module.exports = JSON.parse(stripBOM(content));
} catch (err) {
err.message = filename + ': ’ + err.message;
throw err;
}
};
其中,Module._extensions会被赋值给require()的extensions属性,所以通过在代码中访问require.extensions可以知道系统中已有的扩展加载方式。编写如下代码测试一下:
console.log(require.extensions);
得到的执行结果如下:
{ ‘.js’: [Function], ‘.json’: [Function], ‘.node’: [Function] }
在确定文件的扩展名之后,Node将调用具体的编译方式来将文件执行后返回给调用者。
【JavaScript模块的编译】
回到CommonJS模块规范,我们知道每个模块文件中存在着require、exports、module这3个变量,但是它们在模块文件中并没有定义,那么从何而来呢?甚至在Node的API文档中,我们知道每个模块中还有filename、dirname这两个变量的存在,它们又是从何而来的呢?如果我们把直接定义模块的过程放诸在浏览器端,会存在污染全局变量的情况。
事实上,在编译的过程中,Node对获取的JavaScript文件内容进行了头尾包装。在头部添加了(function(exports, require, module, filename, dirname) {\n,在尾部添加了\n});
一个正常的JavaScript文件会被包装成如下的样子:
(function (exports, require, module, filename, dirname) {
var math = require(‘math’);
exports.area = function (radius) {
return Math.PI * radius * radius;
};
});
这样每个模块文件之间都进行了作用域隔离。包装之后的代码会通过vm原生模块的runInThisContext()方法执行(类似eval,只是具有明确上下文,不污染全局),返回一个具体的function对象。最后,将当前模块对象的exports属性、require()方法、module(模块对象自身),以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function()执行。
这就是这些变量并没有定义在每个模块文件中却存在的原因。在执行之后,模块的exports属性被返回给了调用方。exports属性上的任何方法和属性都可以被外部调用到,但是模块中的其余变量或属性则不可直接被调用。
至此,require、exports、module的流程已经完整,这就是Node对CommonJS模块规范的实现。
【C/C++模块的编译】
Node调用process.dlopen()方法进行加载和执行。在Node的架构下,dlopen()方法在Windows和*nix平台下分别有不同的实现,通过libuv兼容层进行了封装。
实际上,.node的模块文件并不需要编译,因为它是编写C/C++模块之后编译生成的,所以这里只有加载和执行的过程。在执行的过程中,模块的exports对象与.node模块产生联系,然后返回给调用者。
C/C++模块给Node使用者带来的优势主要是执行效率方面的,劣势则是C/C++模块的编写门槛比JavaScript高。
【JSON文件的编译】
.json文件的编译是3种编译方式中最简单的。Node利用fs模块同步读取JSON文件的内容之后,调用JSON.parse()方法得到对象,然后将它赋给模块对象的exports,以供外部调用。
JSON文件在用作项目的配置文件时比较有用。如果你定义了一个JSON文件作为配置,那就不必调用fs模块去异步读取和解析,直接调用require()引入即可。此外,你还可以享受到模块缓存的便利,并且二次引入时也没有性能影响。
CommonJS
在介绍完Node的模块实现之后,回过头来再学习下CommonJS规范,相对容易理解。
CommonJS规范的提出,主要是为了弥补当前JS没有标准的缺陷,使其具备开发大型应用的基础能力,而不是停留在小脚本程序的阶段。
CommonJS对模块的定义十分简单,主要分为模块引用、模块定义和模块标识3个部分。
【模块引用】
var math = require(‘math’);
在CommonJS规范中,存在require()方法,这个方法接受模块标识,以此引入一个模块的API到当前上下文中。
【模块定义】
在模块中,上下文提供require()方法来引入外部模块。对应引入的功能,上下文提供了exports对象用于导出当前模块的方法或者变量,并且它是唯一导出的出口。在模块中,还存在一个module对象,它代表模块自身,而exports是module的属性。在Node中,一个文件就是一个模块,将方法挂载在exports对象上作为属性即可定义导出的方式:
// math.js
exports.add = function () {
var sum = 0, i = 0,args = arguments, l = args.length;
while (i < l) {
sum += args[i++];
}
return sum;
};
在另一个文件中,我们通过require()方法引入模块后,就能调用定义的属性或方法了
// program.js
var math = require(‘math’);
exports.increment = function (val) {
return math.add(val, 1);
};
【模块标识】
模块标识其实就是传递给require()方法的参数,它必须是符合小驼峰命名的字符串,或者以.、…开头的相对路径,或者绝对路径,它可以没有文件名后缀.js。