Node.js是开源的跨平台的软件,意味着Node.js的源码是公开的,且支持多种操作系统和硬件平台。本小节将介绍Node.js的安装和基本使用方法。
Node.js的官方网站是nodejs.org,这里提供了多种平台的安装包和二进制文件下载,下载完成后即可安装或运行。同时也提供了源码下载,根据网站上的说明,可以通过编译源码得到可执行程序,但过程可能比较繁琐。
通常可以选择直接下载安装包或二进制文件。官网提供了长期维护版(LTS)和最新版。长期维护版是比较稳定的版本,推荐使用这个版本。最新版则可能包含更多新功能。
安装完成后,可在命令提示符或终端中通过node命令启动Node.js的交互式环境,可以通过键盘输入和执行JavaScript代码。效果如下图。
要退出交互式环境可输入.exit并按回车。使用Node.js交互式环境的体验和浏览器开发者工具中的控制台非常相似。不同的是Node.js环境中没有DOM树,故没有window、document等内置对象,但却有其它浏览器中不具备的内置库和API。
JavaScript在Node.js和浏览器中所遵照的语法是一致的。但两者提供的接口有显著差异,例如,前面提到的Node.js中没有浏览器中常用的window对象,但也有相同的对象,比如两者都具有console对象。
Node.js中有如http等网络通信模块和文件操作相关模块,这是浏览器中JavaScript所不具备的。
因为在浏览器环境中,JavaScript的主要任务之一是控制视图,即控制用户看到的内容,window对象就是浏览器窗口显示的内容。但Node.js主要被设计用于后端程序开发,更多的要处理业务逻辑,而不需要考虑显示或界面的问题。
Node.js对于HTML5开发者来说容易学习,并扩宽了JavaScript语言的应用范围。让JavaScript程序不仅可以用于控制HTML5页面,还能开发系统工具、服务器端程序,甚至物联网程序。
另一方面由于Node.js运行的是HTML5开发中常用的JavaScript语言,同时又具有开发工具程序的能力。目前有大量HTML5应用或者前端应用开发工具使用Node.js运行,并可以方便的通过npm下载和安装。
npm是一个JavaScript语言的包管理(package manage)工具,是Node.js的默认包管理器。npm上的JavaScript代码不一定仅能在Node.js中运行,也有很多可以在浏览器中运行。npm由安装在用户计算机中的npm命令行工具、npm网站(www.npmjs.com)和在线的数据库组成。
Npm命令行工具是Node.js的默认包管理器,其功能是管理Node.js的代码包,它可在命令提示符窗口或终端运行。npm命令行工具通常随Node.js一起安装。npm命令行工具常用命令有用来安装包的“npm install”,用来删除已安装包的“npm uninstall“,用来搜索可用包的“npm search”,用来查看已经安装的包“npm ls”等。
使用“npm install”命令可以通过多种途径安装包。如安装本地文件夹中的包、安装本地压缩包中的包,或者通过网址安装网络上的包。
使用“npm install 包名”命令可以自动通过npm数据库寻找、下载和安装指定的包。包名后可通过@符号添加版本号,安装指定版本的包,如npm install vue@3.2.47。
通过“npm install”命令安装包时,默认的安装路径是当前目录下的node_modules文件夹,所安装的包也只在当前路径下可用。增加-g参数可把包安装在全局环境中;不加任何参数,直接运行“npm install”命令会根据当前路径下的package.json安装它所依赖的包
创建npm包的目的不仅是为了发布代码,创建npm包更大的意义在于管理项目,如记录项目依赖、定义项目信息,如项目名称、版本、作者等,还可以定义包的命令,使包可以像7.2.2小节中的qrcode一样直接运行。
要发布npm包首先需要在本地准备好要发布的包,注册npm账号并在本地的npm命令行工具登录,然后使用“npm publish”命令即可发布。
简要介绍Node.js后,从这一节起我们再次回到JavaScript语言本身。事件是JavaScript中最重要的概念之一。JavaScript对浏览器中的用户操作、网络请求数据结果的处理都依赖于事件。
第四章已经初步的介绍和使用了事件。JavaScript中事件可以定义为能被JavaScript侦测到的动作。例如,来自用户的事件有单击界面上的按钮、拖动滚动条、在打开浏览器界面时按键盘上的按键;还有其他事件,如网络请求结果的返回、网络请求出错等。
如期望HTML5应用做出反应,比如用户单击按钮后,希望HTML5做出反应,就可以通过JavaScript的事件机制给相应的事件绑定JavaScript函数。当JavaScript或者浏览器检测到这个事件后,就会调用绑定的函数,函数中的代码将对这个事件做出反应。
HTML5应用或Node.js程序运行过程中会产生事件,但JavaScript不能同时处理一个以上的事件,如果有大量事件,它们就要排队等待处理。
当JavaScript忙于处理事件时,浏览器的页面会停止响应,用户将无法操作页面,页面也不能出现任何变化。在Node.js中也是一样的,当程序运行时,事件无法被处理。
可以认为JavaScript中存在一个事件队列。各个来源的事件都进入这个队列中,按一定的顺序排队。JavaScript解释器每次从事件队列中取出一个事件执行,执行完毕后再取另一个事件执行。如下图:
绑定事件有三种方法,分别是HTML标签的事件处理程序属性,JavaScript中元素对象的事件处理器属性和JavaScript中元素的addEventListener方法。
事件上下文(context)就是一个事件的“前因后果”和状态,事件上下文包括事件的类型,如“单击”“按键盘按键”“页面加载完成”等;事件的目标元素,比如单击事件中被单击的元素;事件本身的属性可分为通用的属性(如事件发生的时间)和特殊的属性(比如键盘被按下事件中被按的按键的编码等)。
有些事件本身有默认行为,比如右键单击通常会弹出菜单,这个菜单被称为快捷菜单,这个事件被称为contextmenu事件。如希望右键点击某元素不弹出快捷菜单,则需要阻止这个默认行为。如果仅绑定事件而不阻止默认行为,那么事件发生时,默认行为和绑定的事件处理函数都会触发。如下面代码。
<script>
function say_hi() {
alert("hi!");
}
let btn = document.getElementById('hi_btn');
btn.oncontextmenu = say_hi
</script>
运行代码,右击按钮后,即会调用say_hi函数弹出弹窗,还会弹出浏览器默认的快捷菜单。但如果在say_hi函数中增加下面语句,则可阻止浏览器快捷菜单的弹出。
event.preventDefault();
另一个经常需要阻止默认事件的场景是表单提交事件。这个事件在用户提交表单时触发,如用户点击提交按钮或在表单内按下回车时。有时会使用这个事件检查表单输入,当输入不符合规则时要提示用户并阻止表单提交。
下面的HTML代码在一个<div>中定义无序列表,其中有三个元素。
下面是列表:
下面是列表元素:
- 1
- 2
- 3
这段代码的运行效果如下图所示。
以单击事件为例,<li>2</li>元素本身也是<ul>元素的一部分,也是再上一层<div>元素的一部分,所以当用户单击<li>2</li>元素的时候,会依次触发<li>2</li>元素、<ul>元素和<div>元素的click(单击)事件。这种机制被称为事件冒泡机制,即一个事件不仅在实际发生事件的元素上被触发,还在这个元素的父亲元素上被触发。
使用下面的代码给div以及div内所有元素绑定事件。
<script>
function on_event() {
alert("事件类型:" + event.type + "目标元素:" + this.tagName + "元素内的文字:" + this.innerText);
}
// 下面函数的功能是给一个元素以及它所有的孩子元素都绑定指定事件处理函数
// 第一个参数是元素对象,第二个是要绑定的事件类型,第三个是要绑定的函数
function bindEventForAllChild(element, event_type, func) {
// 先给当前的元素绑定事件
element.addEventListener(event_type, func);
// 遍历当前元素的每个孩子元素
for (let child of element.children) {
// 递归调用函数,给孩子元素绑定事件
bindEventForAllChild(child, event_type, func);
}
}
// 给 div 元素及它所有孩子元素的 click 事件绑定 on_event 函数
bindEventForAllChild(document.querySelector('div'), 'click', on_event);
// 给 div 元素及它所有孩子元素的 contextmenu 事件绑定 on_event 函数
bindEventForAllChild(document.querySelector('div'), 'contextmenu', on_event);
</script>
bindEventForAllChild是一个递归调用函数,它调用addEventListener给当前元素的指定事件绑定事件处理函数,然后遍历当前元素的所有子元素,对每个子元素递归调用函数addEventListener,最终实现了为一个元素和它所有的子元素绑定事件处理函数。
这个例子分别给div元素的click和contextmenu事件绑定了处理函数on_event。on_event处理函数则通过this和event变量获取上下文,通过alert显示了当前触发的事件类型、发生事件的目标元素以及元素内的文字。
打开页面,单击页面上的数字3。将依次看到3个弹窗,如下图所示
如果要中断事件冒泡机制,则可以使用下面的代码。
event.stopPropagation();
把这行代码添加到on_event函数中后,单击上述任何元素都仅会弹出一个弹窗,因为在每次调用on_event时,stopPropagation方法都会阻止事件通过冒泡的方式向上传播。
事件冒泡的一个应用是只给父元素绑定事件处理函数,在事件处理函数中通过event.target判断触发事件的具体元素,这个方法被称为事件托管。
程序可以看成是一系列预先设计的“动作”或“事件”。运行程序就是执行或触发这些动作或事件的过程,运行程序的方式可以分为同步和异步两种。本节将介绍JavaScript中的同步和异步。
假设要实现把用户输入的字符串中的字母转成大写显示出来,将非字母的字符原样输出,比如用户输入“abc123!”,输出“ABC123!”;还要实时显示结果,即输入或修改了内容要马上显示对应结果。下面的代码创建了一个输入框和一个用于输出大写结果的<P>标签。
转大写后的结果:
用户在输入框输入,结果显示在id为“result”的<p>元素中。
先分解以上任务的事件或动作,这些事件和动作可分为用户输入内容、程序读取输入框的内容、程序把用户输入的内容转成大写、程序把转换后的内容显示出来。
如果按照完全同步的方法来实现程序,组织这些事件或动作的方法可以用下图表示。程序要不停的读取输入框的内容,如果输入框内容没有变化,就继续在读取和检查,因为用户随时可能编辑输入框中的内容,所以要保证显示结果的实时性,就必须不停的检查。若发现输入内容变化则将输入字符串中的字母转换成大写,更新显示内容,然后继续检查用户输入有没有新的变化
因为不停检查的过程没法结束(页面只要没被关闭,用户就有可能输入内容),所以这个流程图只有开始框,没有结束框。
同步过程简单清楚,虽然很容易用JavaScript代码实现这个过程,但该代码无法实现预期功能。因为不断检查输入框的内容会导致浏览器一直处于忙碌状态而失去响应,即用户一切操作都无法生效。示例代码如下(但请不要使用这段代码,它会导致浏览器页面失去响应)。
<script>
// 这段代码会导致浏览器页面卡住,而无法真正实现功能
let content = "";
let input_box = document.getElementById('input_box');
let result = document.getElementById('result');
while (true) {
let current_content = input_box.value;
if (current_content !== content) { // 检测到输入变化
// 转换成大写
let output = current_content.toUpperCase();
// 更新输出内容
result.innerText = output;
}
}
</script>
下图展示异步过程下的程序组织方式。异步过程中,每个步骤都很简单,异步事件调度器(或者可以看作系统)在特定的时间调用了它们。每个步骤结束后,如有后续步骤,就会通知异步事件调度器,然后结束,如无后续步骤就直接结束。点击查看代码
以上程序流程很简单,但事实上,异步情况下的循环和判断的部分都由系统“代劳”了。在同步的情况下,系统只需要一步一步地执行程序即可。在异步情况下,系统要负责查看输入框的内容有没有变化,如有变化,则调用读取输入框内容的程序。当读取成功后,调用字母转换大写的程序,转换完成后,调用显示内容的程序。
前面介绍的JavaScript事件模型事实上就是一种异步的实现方式。程序可以通过addEventListener方法告诉系统,在什么情况下要调用哪个函数,从而实现了异步
与用户和网络相关的事件都非常适合通过异步的方式处理。因为用户的操作程序无法预测,而且用户的反应比程序慢很多,用户输入内容的时长,可能是几秒、几分钟甚至几小时都有可能,好的方式是让系统侦测到用户的操作后,直接调用程序,而不是让程序通过循环一遍一遍地检查用户没有操作。
这个异步的例子为了描述清晰把所有步骤都分开让系统调度。但实际实现时,通常会使用异步和同步结合的方式。而且更进一步,异步的程序里也总会含有同步的操作,几乎不可能由“完全异步”的做法。实际中合理的流程如下图所示。
下面的代码实现了绑定
<script>
let input_box = document.getElementById('input_box');
let result = document.getElementById('result');
function outputUpperCase() {
let current_content = input_box.value;
// 转换成大写
let output = current_content.toUpperCase();
// 更新输出内容
result.innerText = output;
}
input_box.addEventListener("keyup", outputUpperCase);
</script>
这段程序在输入框的“keyup”事件上绑定了函数“outputUpperCase”,该函数实现了读取输入框内容,转换成大写,和输出到<p>标签的操作。实现结果如下图所示
回调是指用函数作为参数,等到合适的时机再调被调用。JavaScript的事件的异步处理就是通过回调实现的。
回调是实现异步的方式之一,也是JavaScript中传统的异步实现方法。
Promise是另一种异步的实现方式,是现代JavaScript中异步编程的基础。Promise用于创建异步函数。根据第四章的内容,调用函数后,等函数中的操作执行完毕后,函数才会返回。但异步函数返回时,函数不一定已经执行完成。
Promise是一个英语单词,可以直译为承诺。可以理解为,异步函数的操作没有真正执行完成,调用异步函数,拿到的只是一个承诺,但承诺最终不一定能实现。
Promise对象包含异步函数的执行状态,其状态有待定(Pending)、已实现(fulfilled)和被拒绝(rejected)三种,分别表示还未执行完毕、执行成功和执行失败。
创建Promise对象需要一个函数作为参数,这个函数是Promise要执行的工作,这个函数又接受两个参数,一个是resolve函数,另一个是reject函数。当这个函数成功的完成了工作时需要调用resolve,可以传递一个参数回去,并把该Promise的状态标记为已实现。reject也可以传递参数,并把Promise的状态标记为被拒绝。用法示例如下。
new Promise((resolve, reject) => {
// 当成功时调用 resolve,例如 resolve(); 或者可以传入任意参数
// 失败时调用reject,也可传参数。
});
调用异步函数,得到的返回结果是Promise。想要拿到Promise中操作的返回值(即resolve函数的参数),需要通过Promise对象的then方法,该方法意味着,待Promise制定的操作成功后执行一个函数,then方法接受的参数是一个函数,这个函数接受一个参数,这个参数就是Promise的返回值。而catch方法则用于处理reject的情况。
如果要等待多个Promise都完成或者失败,则可以使用Promise.all([promise1, promise2]).then或.catch。要等待多个Promise中的任意一个,则使用Promise.any。
本章从Node.js开始,介绍了JavaScript编程中的事件模型,异步编程等高级用法。这些用法在前面章节虽然已经有过一些接触和使用,本章从理论上阐述了它们的概念和工作原理。
JavaScript是专门为浏览器环境设计的程序设计语言,它的一大特点是事件驱动和单线程,所有的操作都需要在主线程中完成。Node.js利用和发扬了这一特点,并使JavaScript可以在更多场景中发挥这一优势。学习JavaScript高级功能和Node.js可以更深入了解JavaScript原理。
本节将开发一个可以把任意文字转换为二维码的HTM5应用。
创建一个空文件夹,然后使用npm命令把这个目录初始化为一个包。
npm init -y
这条命令会在当前目录下,使用默认配置创建一个package.json文件。可以用文本编辑器打开这个文件编辑包名、版本、作者等信息。可以删掉其中“"main": "index.js",”一行
然后安装webpack及其依赖,命令如下。
npm install webpack webpack-cli --save-dev
webpack安装完毕后,目录中出现package-lock.json和node_modules。再创建两个文件夹src和dist,其中src存放源代码,dist存放发布后的代码。
在src目录下创建index.js,在dist目录下创建index.html。其中index.js的内容如下。
let btn = document.querySelector("#buttonGenerate"); // 按钮元素
let textArea = document.querySelector("#inputString"); // 多行文本输入区域
let outputArea = document.querySelector("#outputArea"); // 输出区域<div>元素
// 给按钮绑定点击事件
btn.addEventListener('click', () => {
let str = textArea.value;
outputArea.innerText = str;
})
这里绑定的事件处理函数仅仅把多行文本输入区域中的文字输出到了输出区域的<div>元素。仅仅是为了验证webpack能够正常工作。
在项目的根目录创建webpack的配置文件webpack.config.js内容如下。
const path = require('path');
module.exports = {
entry: './src/index.js',
mode: 'development',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
};
使用webpack构建当前项目。
npm run build
命令执行成功后,可以看到disk目录下出现了main.js。此时可直接在浏览器打开index.html。输入文字后点击按钮可以看到输入的文本被显示在按钮下方。效果如下图所示。
使用下面的命令安装qrcode包。
npm install qrcode --save-dev
安装成功后,qrcode包被装在node_modules目录中,qrcode也会被作为项目依赖写入package.json中。
在index.js中使用require语法引入qrcode依赖。
let QRCode = require('qrcode');
再向输出区域的div中添加一个canvas元素用于展示二维码,一个p元素用于展示文字。
outputArea.innerHTML = '';
最后参考qrcode的文档,修改事件处理函数。更新后的index.js文件内容如下所示。
let QRCode = require('qrcode');
let btn = document.querySelector("#buttonGenerate");
let textArea = document.querySelector("#inputString");
let outputArea = document.querySelector("#outputArea");
outputArea.innerHTML = '';
btn.addEventListener('click', () => {
let str = textArea.value;
let outputText = document.querySelector("#outputText");
var canvas = document.querySelector('#canvas')
QRCode.toCanvas(canvas, str, function (error) {
if (error) console.error(error)
})
outputText.innerText += "当前二维码对应的文字是:" + str;
})
再次执行npm run build命令重新构建前端后,打开index.html。可以看到应用已经可以正常工作了,效果如下图所示。
开发过程中,webpack配置中的mode设置如下所示。
mode: 'development',
这意味着webpack输出的源代码是开发版本。生成的main.js文件体积较大,对于当前代码输出的main.js的大小是98KB。现编辑webpack.config.js,修改mode为“production”,再次运行npm run build。
mode设为“production”后,webpack的构建速度变慢,但生成的main.js大小变为25KB。