Node.js是目前非常火热的技术,但是它的诞生经历却很奇特。在Netscape设计出JavaScript后的短短几个月,JavaScript事实上已经是前端开发的唯一标准。后来,微软通过IE击败了Netscape后一统桌面,结果几年时间,浏览器毫无进步。
没有竞争就没有发展。微软认为IE6浏览器已经非常完善,几乎没有可改进之处,然后解散了IE6开发团队!而Google却认为支持现代Web应用的新一代浏览器才刚刚起步,尤其是浏览器负责运行JavaScript的引擎性能还可提升10倍。先是Mozilla借助已壮烈牺牲的Netscape遗产在2002年推出了Firefox浏览器,紧接着Apple于2003年在开源的KHTML浏览器的基础上推出了WebKit内核的Safari浏览器,不过仅限于Mac平台。随后,Google也开始创建自家的浏览器。他们也看中了WebKit内核,于是基于WebKit内核推出了Chrome浏览器。Chrome浏览器是跨Windows和Mac平台的,并且,Google认为要运行现代Web应用,浏览器必须有一个性能非常强劲的JavaScript引擎,于是Google自己开发了一个高性能JavaScript引擎,名字叫V8,以BSD许可证开源。
现代浏览器大战让微软的IE浏览器远远地落后了,因为他们解散了最有经验、战斗力最强的浏览器团队!回过头再追赶却发现,支持HTML5的WebKit已经成为手机端的标准了,IE浏览器从此与主流移动端设备绝缘。
那么浏览器发展历程和Node有何关系?
有个叫Ryan Dahl的人,他的工作是用C/C++写高性能Web服务。对于高性能,异步IO、事件驱动是基本原则,但是用C/C++写就太痛苦了。于是他开始设想用高级语言开发Web服务。他评估了很多种高级语言,发现很多语言虽然同时提供了同步IO和异步IO,但是开发人员一旦用了同步IO,他们就再也懒得写异步IO了,所以,最终,Ryan瞄向了JavaScript。因为JavaScript是单线程执行,根本不能进行同步IO操作,所以,JavaScript的这一“缺陷”导致了它只能使用异步IO。
选定了开发语言,还要有运行时引擎。他曾考虑过自己写一个,不过明智地放弃了,因为V8就是开源的JavaScript引擎。让Google投资去优化V8,他只负责改造一下拿来用,还不用付钱,这个买卖很划算。
于是在2009年,Ryan正式推出了基于JavaScript语言和V8引擎的开源Web服务器项目,命名为Node.js。虽然名字很土,但是,Node第一次把JavaScript带入到后端服务器开发,加上世界上已经有无数的JavaScript开发人员,所以Node一下子就火了起来。
在Node上运行的JavaScript相比其他后端开发语言有何优势?
最大的优势是借助JavaScript天生的事件驱动机制加V8高性能引擎,使编写高性能Web服务轻而易举。
其次,JavaScript语言本身是完善的函数式语言,在前端开发时,开发人员往往写得比较随意,让人感觉JavaScript就是个“玩具语言”。但是,在Node环境下,通过模块化的JavaScript代码,加上函数式编程,并且无需考虑浏览器兼容性问题,直接使用最新的ECMAScript 6标准,可以完全满足工程上的需求。
何为io.js?
因为Node.js是开源项目,虽然由社区推动,但幕后一直由Joyent公司资助。由于一群开发者对Joyent公司的策略不满,于2014年从Node.js项目fork出了io.js项目,决定单独发展,但两者实际上是兼容的。分家后没多久,Joyent公司表示要和解,于是,io.js项目又决定回归Node.js。最终他两的区别是:io.js是“尝鲜版”,而Node.js是线上稳定版。
由于Node.js平台是在后端运行JavaScript代码,所以,必须首先在本机安装Node环境。
安装Node.js
目前Node.js的最新版本是12.18.3。首先,从Node.js官网下载对应平台的安装程序。
在Windows上安装时务必选择全部组件,包括勾选Add to Path。
安装完成后,在Windows环境下,请打开命令提示符,然后输入node -v,如果安装正常,你应该看到v12.18.3这样的输出:
C:\Users\IEUser>node -v
v12.18.3
继续在命令提示符输入node,此刻你将进入Node.js的交互环境。在交互环境下,可以输入任意JavaScript语句,例如100+200,回车后将得到输出结果。
要退出Node.js环境,连按两次Ctrl+C。
在Mac或Linux环境下,打开终端,然后输入node -v,你应该看到如下输出:
$ node -v
v12.18.3
npm
在正式开始Node.js学习之前,先认识一下npm。npm是什么?npm其实是Node.js的包管理工具(package manager)。
为什么需要一个包管理工具呢?因为在Node.js上开发时,会用到很多别人写的JavaScript代码。如果我们要使用别人写的某个包,每次都根据名称搜索一下官方网站,下载代码,解压,再使用,非常繁琐。于是一个集中管理的工具应运而生:大家都把自己开发的模块打包后放到npm官网上,如果要使用,直接通过npm安装就可以直接用,不用管代码存在哪,应该从哪下载。
更重要的是,如果要使用模块A,而模块A又依赖于模块B,模块B又依赖于模块X和模块Y,npm可以根据依赖关系,把所有依赖的包都下载下来并管理起来。否则,靠自己手动管理,肯定既麻烦又容易出错。
npm已经在Node.js安装的时候顺带装好了。我们在命令提示符或者终端输入npm -v,应该看到类似的输出:
C:\>npm -v
6.14.6
如果直接输入npm,你会看到类似下面的输出:
C:\> npm
Usage: npm <command>
where <command> is one of:
...
我们要先选择一个文本编辑器来编写JavaScript代码,并且把它保存到本地硬盘的某个目录,才能够执行。
那么问题来了:文本编辑器到底怎么选?
首先,请注意,绝对不能用Word和写字板。Word和写字板保存的不是纯文本文件。如果我们要用记事本来编写JavaScript代码,要务必注意,记事本以UTF-8格式保存文件时,会自作聪明地在文件开始的地方加上几个特殊字符(UTF-8 BOM),结果经常会导致程序运行出现莫名其妙的错误。
所以,用记事本写代码时请注意,保存文件时使用ANSI编码,并且暂时不要输入中文。
如果电脑上已经安装了Sublime Text,或者Notepad++,也可以用来编写JavaScript代码,注意用UTF-8格式保存。
输入以下代码:
'use strict';
console.log('Hello, world.');
第一行总是写上’use strict’;是因为总是以严格模式运行JavaScript代码,避免各种潜在陷阱。
然后,选择一个目录,例如C:\Workspace,把文件保存为hello.js,就可以打开命令行窗口,把当前目录切换到hello.js所在目录,然后输入以下命令运行这个程序了:
C:\Workspace>node hello.js
Hello, world.
也可以保存为别的名字,比如first.js,但是必须要以.js结尾。此外,文件名只能是英文字母、数字和下划线的组合。如果当前目录下没有hello.js这个文件,运行node hello.js就会报错:
C:\Workspace>node hello.js
module.js:338
throw err;
^
Error: Cannot find module 'C:\Workspace\hello.js'
at Function.Module._resolveFilename
at Function.Module._load
at Function.Module.runMain
at startup
at node.js
报错的意思就是,没有找到hello.js这个文件,因为文件不存在。这个时候,就要检查一下当前目录下是否有这个文件了。
命令行模式和Node交互模式
请注意区分命令行模式和Node交互模式。
看到类似C:>是在Windows提供的命令行模式:
在命令行模式下,可以执行node进入Node交互式环境,也可以执行node hello.js运行一个.js文件。
看到>是在Node交互式环境下:
在Node交互式环境下,可以输入JavaScript代码并立刻执行。
此外,在命令行模式运行.js文件和在Node交互式环境下直接运行JavaScript代码有所不同。Node交互式环境会把每一行JavaScript代码的结果自动打印出来,但是,直接运行JavaScript文件却不会。
例如,在Node交互式环境下,输入:
> 100 + 200 + 300;
600
//直接可以看到结果600。
但是,写一个calc.js的文件,内容如下:
100 + 200 + 300;
然后在命令行模式下执行:
C:\Workspace>node calc.js
发现什么输出都没有。这是正常的。想要输出结果,必须自己用console.log()打印出来。把calc.js改造一下:
console.log(100 + 200 + 300);
再执行,就可以看到结果:
C:\Workspace>node calc.js
600
使用严格模式
如果在JavaScript文件开头写上’use strict’;,那么Node在执行该JavaScript时将使用严格模式。但是,在服务器环境下,如果有很多JavaScript文件,每个文件都写上’use strict’;很麻烦。可以给Nodejs传递一个参数,让Node直接为所有js文件开启严格模式:
node --use_strict calc.js
Node的交互模式和直接运行.js文件有什么区别呢?
直接输入node进入交互模式,相当于启动了Node解释器,但是等待你一行一行地输入源代码,每输入一行就执行一行。
直接运行node hello.js文件相当于启动了Node解释器,然后一次性把hello.js文件的源代码给执行了,你是没有机会以交互的方式输入源代码的。在编写JavaScript代码的时候,完全可以一边在文本编辑器里写代码,一边开一个Node交互式命令窗口,在写代码的过程中,把部分代码粘到命令行去验证,事半功倍!
使用文本编辑器来开发Node程序,最大的缺点是效率太低,运行Node程序还需要在命令行单独敲命令。如果还需要调试程序,就更加麻烦了。
所以需要一个IDE集成开发环境,让我们能在一个环境里编码、运行、调试,这样就可以大大提升开发效率。Java的集成开发环境有Eclipse,Intellij idea等,C#的集成开发环境有Visual Studio,那么问题又来了:Node.js的集成开发环境到底哪家强?
考察Node.js的集成开发环境,重点放在启动速度快,执行简单,调试方便这三点上。综合考察后,推荐Node.js集成开发环境:Visual Studio Code
Visual Studio Code由微软出品,但它不是那个大块头的Visual Studio,它是一个精简版的迷你Visual Studio,并且,Visual Studio Code可以跨平台,Windows、Mac和Linux通用。
安装Visual Studio Code
可以从Visual Studio Code的官方网站下载并安装最新的版本。
安装过程中,请务必钩上 将“通过Code打开”操作添加到Windows资源管理器目录上下文菜单这一栏,这将大大提升将来的操作快捷度。
运行和调试JavaScript
在VS Code中,可以非常方便地运行JavaScript文件。VS Code以文件夹作为工程目录(Workspace Dir),所有的JavaScript文件都存放在该目录下。此外,VS Code在工程目录下还需要一个.vscode
的配置目录,里面存放里VS Code需要的配置文件。
假设在C:\Work\目录下创建了一个hello目录作为工程目录,并编写了一个hello.js文件,则该工程目录的结构如下:
hello/ <-- workspace dir
|
+- hello.js <-- JavaScript file
|
+- .vscode/ <-- VS Code config
|
+- launch.json <-- VS Code config file for JavaScript
可以用VS Code快速创建launch.json,然后修改如下:
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"/**"
],
"program": "${workspaceFolder}/hello.js"
}
]
有了配置文件,即可使用VS Code调试JavaScript。
在计算机程序的开发过程中,随着程序代码越写越多,在一个文件里代码就会越来越长,越来越不容易维护。为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式。在Node环境中,一个.js文件就称之为一个模块(module)。
使用模块有什么好处?
最大的好处是大大提高了代码的可维护性。其次,编写代码不必从零开始。当一个模块编写完毕,就可以被其他地方引用。我们在编写程序的时候,也经常引用其他模块,包括Node内置的模块和来自第三方的模块。
使用模块还可以避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中,因此,我们自己在编写模块时,不必考虑名字会与其他模块冲突。
在上一节,编写了一个hello.js文件,这个hello.js文件就是一个模块,模块的名字就是文件名,所以hello.js文件就是名为hello的模块。我们把hello.js改造一下,创建一个函数,这样我们就可以在其他地方调用这个函数:
'use strict';
var s = 'Hello';
function greet(name) {
console.log(s + ', ' + name + '!');
}
module.exports = greet;
函数greet()是我们在hello模块中定义的,你可能注意到最后一行是一个奇怪的赋值语句,它的意思是,把函数greet作为模块的输出暴露出去,这样其他模块就可以使用greet函数了。
问题是其他模块怎么使用hello模块的这个greet函数呢?再编写一个main.js文件,调用hello模块的greet函数:
'use strict';
// 引入hello模块:
var greet = require('./hello');
var s = 'Michael';
greet(s); // Hello, Michael!
注意到引入hello模块用Node提供的require函数:
var greet = require('./hello');
引入的模块作为变量保存在greet变量中,那greet变量到底是什么东西?其实变量greet就是在hello.js中我们用module.exports = greet;输出的greet函数。所以,main.js就成功地引用了hello.js模块中定义的greet()函数,接下来就可以直接使用它了。
在使用require()引入模块的时候,请注意模块的相对路径。因为main.js和hello.js位于同一个目录,所以我们用了当前目录.:
var greet = require('./hello'); // 不要忘了写相对目录!
如果只写模块名:
var greet = require('hello');
则Node会依次在内置模块、全局模块和当前模块下查找hello.js,你很可能会得到一个错误:
module.js
throw err;
^
Error: Cannot find module 'hello'
at Function.Module._resolveFilename
at Function.Module._load
...
at Function.Module._load
at Function.Module.runMain
遇到这个错误,你要检查:
module.exports = variable;
输出的变量可以是任意对象、函数、数组等等。
要引入其他模块输出的对象,用:
var foo = require('other_module');
引入的对象具体是什么,取决于引入模块输出的对象。
了解模块原理
当编写JavaScript代码时,我们可以申明全局变量:
var s = 'global';
在浏览器中,大量使用全局变量可不好。如果你在a.js中使用了全局变量s,那么,在b.js中也使用全局变量s,将造成冲突,b.js中对s赋值会改变a.js的运行逻辑。也就是说,JavaScript语言本身并没有一种模块机制来保证不同模块可以使用相同的变量名。
那Node.js是如何实现这一点的?
其实要实现“模块”这个功能,并不需要语法层面的支持。Node.js也并不会增加任何JavaScript语法。实现“模块”功能的奥妙就在于JavaScript是一种函数式编程语言,它支持闭包。如果把一段JavaScript代码用一个函数包装起来,这段代码的所有“全局”变量就变成了函数内部的局部变量。
我们编写的hello.js代码是这样的:
var s = 'Hello';
var name = 'world';
console.log(s + ' ' + name + '!');
Node.js加载了hello.js后,它可以把代码包装一下,变成这样执行:
(function () {
// 读取的hello.js代码:
var s = 'Hello';
var name = 'world';
console.log(s + ' ' + name + '!');
// hello.js代码结束
})();
这样一来,原来的全局变量s现在变成了匿名函数内部的局部变量。如果Node.js继续加载其他模块,这些模块中定义的“全局”变量s也互不干扰。所以,Node利用JavaScript的函数式编程的特性,轻而易举地实现了模块的隔离。
但是,模块的输出module.exports怎么实现?
这个也很容易实现,Node可以先准备一个对象module:
// 准备module对象:
var module = {
id: 'hello',
exports: {}
};
var load = function (module) {
// 读取的hello.js代码:
function greet(name) {
console.log('Hello, ' + name + '!');
}
module.exports = greet;
// hello.js代码结束
return module.exports;
};
var exported = load(module);
// 保存module:
save(module, exported);
可见,变量module是Node在加载js文件前准备的一个变量,并将其传入加载函数,我们在hello.js中可以直接使用变量module原因就在于它实际上是函数的一个参数:
module.exports = greet;
通过把参数module传递给load()函数,hello.js就顺利地把一个变量传递给了Node执行环境,Node会把module变量保存到某个地方。由于Node保存了所有导入的module,当我们用require()获取module时,Node找到对应的module,把这个module的exports变量返回,这样,另一个模块就顺利拿到了模块的输出:
var greet = require('./hello');
以上是Node实现JavaScript模块的一个简单的原理。
module.exports vs exports
很多时候,在Node环境中,有两种方法可以在一个模块中输出变量:
方法一:对module.exports赋值:
// hello.js
function hello() {
console.log('Hello, world!');
}
function greet(name) {
console.log('Hello, ' + name + '!');
}
module.exports = {
hello: hello,
greet: greet
};
方法二:直接使用exports:
// hello.js
function hello() {
console.log('Hello, world!');
}
function greet(name) {
console.log('Hello, ' + name + '!');
}
function hello() {
console.log('Hello, world!');
}
exports.hello = hello;
exports.greet = greet;
但是你不可以直接对exports赋值:
// 代码可以执行,但是模块并没有输出任何变量:
exports = {
hello: hello,
greet: greet
};
如果你对上面的写法感到十分困惑,不要着急,我们来分析Node的加载机制:
首先,Node会把整个待加载的hello.js文件放入一个包装函数load中执行。在执行这个load()函数前,Node准备好了module变量:
var module = {
id: 'hello',
exports: {}
};
load()函数最终返回module.exports:
var load = function (exports, module) {
// hello.js的文件内容
...
// load函数返回:
return module.exports;
};
var exported = load(module.exports, module);
也就是说,默认情况下,Node准备的exports变量和module.exports变量实际上是同一个变量,并且初始化为空对象{},于是,我们可以写:
exports.foo = function () { return 'foo'; };
exports.bar = function () { return 'bar'; };
也可以写:
module.exports.foo = function () { return 'foo'; };
module.exports.bar = function () { return 'bar'; };
换句话说,Node默认给你准备了一个空对象{},这样你可以直接往里面加东西。但是,如果我们要输出的是一个函数或数组,那么,只能给module.exports赋值:
module.exports = function () { return 'foo'; };
给exports赋值是无效的,因为赋值后,module.exports仍然是空对象{}。
总结
所以可以得出结论:直接对module.exports赋值,可以应对任何情况:
module.exports = {
foo: function () { return 'foo'; }
};
或者:
module.exports = function () { return 'foo'; };