一杯茶的时间,上手 Node.js - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
huan1043269994
V2EX    问与答

一杯茶的时间,上手 Node.js

  •  2
     
  •   huan1043269994 2020-04-20 20:11:24 +08:00 4957 次点击
    这是一个创建于 2024 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我们研发开源了一款基于 Git 进行技术实战教程写作的工具,我们图雀社区的所有教程都是用这款工具写作而成,欢迎 Star

    如果你想快速了解如何使用,欢迎阅读我们的 教程文档哦

    Node.js 太火了,火到几乎所有前端工程师都想学,几乎所有后端工程师也想学。一说到 Node.js ,我们马上就会想到“异步”、“事件驱动”、“非阻塞”、“性能优良”这几个特点,但是你真的理解这些词的含义吗?这篇教程将带你快速入门 Node.js ,为后续的前端学习或是 Node.js 进阶打下坚实的基础。

    此教程属于Node.js 后端工程师学习路线的一部分,欢迎来 Star 一波,鼓励我们继续创作出更好的教程!

    起步

    什么是 Node ?

    简单地说,Node (或者说 Node.js ,两者是等价的)是 Javascript 的一种运行环境。在此之前,我们知道 Javascript 都是在浏览器中执行的,用于给网页添加各种动态效果,那么可以说浏览器也是 Javascript 的运行环境。那么这两个运行环境有哪些差异呢?请看下图:

    两个运行环境共同包含了 ECMAScript,也就是剥离了所有运行环境的 Javascript 语言标准本身。现在 ECMAScript 的发展速度非常惊人,几乎能够做到每年发展一个版本。

    提示

    ECMAScript 和 Javascript 的关系是,前者是后者的规格,后者是前者的一种实现。在日常场合,这两个词是可以互换的。更多背景知识可参考阮一峰的《 Javascript 语言的历史》

    另一方面,浏览器端 Javascript 还包括了:

    • 浏览器对象模型( Browser Object Model,简称 BOM ),也就是 window 对象
    • 文档对象模型( Document Object Model,简称 DOM ),也就是 document 对象

    而 Node.js 则是包括 V8 引擎。V8 是 Chrome 浏览器中的 Javascript 引擎,经过多年的发展和优化,性能和安全性都已经达到了相当的高度。而 Node.js 则进一步将 V8 引擎加工成可以在任何操作系统中运行 Javascript 的平台。

    预备知识

    在正式开始这篇教程之前,我们希望你已经做好了以下准备:

    • 了解 Javascript 语言的基础知识,如果有过浏览器 JS 开发经验就更好了
    • 已经安装了 Node.js ,配置好了适合自己的编辑器或 IDE
    • 了解相对路径和绝对路径

    学习目标

    这篇教程将会让你学到:

    • 浏览器 Javascript 与 Node.js 的关系与区别
    • 了解 Node.js 有哪些全局对象
    • 掌握 Node.js 如何导入和导出模块,以及模块机制的原理
    • 了解如何用 Node.js 开发简单的命令行应用
    • 学会利用 npm 社区的力量解决开发中遇到的难题,避免“重复造轮子”
    • 了解 npm scripts 的基本概念和使用
    • 初步了解 Node.js 的事件机制

    运行 Node 代码

    运行 Node 代码通常有两种方式:1 )在 REPL 中交互式输入和运行; 2 )将代码写入 JS 文件,并用 Node 执行。

    提示

    REPL 的全称是 Read Eval Print Loop (读取-执行-输出-循环),通常可以理解为交互式解释器,你可以输入任何表达式或语句,然后就会立刻执行并返回结果。如果你用过 Python 的 REPL 一定会觉得很熟悉。

    使用 REPL 快速体验

    如果你已经安装好了 Node,那么运行以下命令就可以输出 Node.js 的版本:

    $ node -v v12.10.0 

    然后,我们还可以进入 Node REPL (直接输入 node),然后输入任何合法的 Javascript 表达式或语句:

    $ node Welcome to Node.js v12.10.0. Type ".help" for more information. > 1 + 2 3 > var x = 10; undefined > x + 20 30 > console.log('Hello World'); Hello World undefined 

    有些行的开头是 >,代表输入提示符,因此 > 后面的都是我们要输入的命令,其他行则是表达式的返回值或标准输出( Standard Output,stdout )。运行的效果如下:

    编写 Node 脚本

    REPL 通常用来进行一些代码的试验。在搭建具体应用时,更多的还是创建 Node 文件。我们先创建一个最简单的 Node.js 脚本文件,叫做 timer.js ,代码如下:

    console.log('Hello World!'); 

    然后用 Node 解释器执行这个文件:

    $ node timer.js Hello World! 

    看上去非常平淡无奇,但是这一行代码却凝聚了 Node.js 团队背后的心血。我们来对比一下,在浏览器和 Node 环境中执行这行代码有什么区别:

    • 在浏览器运行 console.log 调用了 BOM,实际上执行的是 window.console.log('Hello World!')
    • Node 首先在所处的操作系统中创建一个新的进程,然后向标准输出打印了指定的字符串, 实际上执行的是 process.stdout.write('Hello World!\n')

    简而言之,Node 为我们提供了一个无需依赖浏览器、能够直接与操作系统进行交互的 Javascript 代码运行环境!

    Node 全局对象初探

    如果你有过编写 Javascript 的经验,那么你一定对全局对象不陌生。在浏览器中,我们有 documentwindow 等全局对象;而 Node 只包含 ECMAScript 和 V8,不包含 BOM 和 DOM,因此 Node 中不存在 documentwindow;取而代之,Node 专属的全局对象是 process。在这一节中,我们将初步探索一番 Node 全局对象。

    Javascript 全局对象的分类

    在此之前,我们先看一下 Javascript 各个运行环境的全局对象的比较,如下图所示:

    可以看到 Javascript 全局对象可以分为四类:

    1. 浏览器专属,例如 windowalert 等等;
    2. Node 专属,例如 processBuffer__dirname__filename 等等;
    3. 浏览器和 Node 共有,但是实现方式不同,例如 console(第一节中已提到)、setTimeoutsetInterval 等;
    4. 浏览器和 Node 共有,并且属于 ECMAScript 语言定义的一部分,例如 DateStringPromise 等;

    Node 专属全局对象解析

    process

    process 全局对象可以说是 Node.js 的灵魂,它是管理当前 Node.js 进程状态的对象,提供了与操作系统的简单接口。

    首先我们探索一下 process 对象的重要属性。打开 Node REPL,然后我们查看一下 process 对象的一些属性:

    • pid:进程编号
    • env:系统环境变量
    • argv:命令行执行此脚本时的输入参数
    • platform:当前操作系统的平台

    提示

    可以在 Node REPL 中尝试一下这些对象。像上面说的那样进入 REPL (你的输出很有可能跟我的不一样):

    $ node Welcome to Node.js v12.10.0. Type ".help" for more information. > process.pid 3 > process.platform 'darwin' 

    Buffer

    Buffer 全局对象让 Javascript 也能够轻松地处理二进制数据流,结合 Node 的流接口( Stream ),能够实现高效的二进制文件处理。这篇教程不会涉及 Buffer

    __filename__dirname

    分别代表当前所运行 Node 脚本的文件路径和所在目录路径。

    警告

    __filename__dirname 只能在 Node 脚本文件中使用,在 REPL 中是没有定义的。

    使用 Node 全局对象

    接下来我们将在刚才写的脚本文件中使用 Node 全局对象,分别涵盖上面的三类:

    • Node 专属:process
    • 实现方式不同的共有全局对象:consolesetTimeout
    • ECMAScript 语言定义的全局对象:Date

    提示

    setTimeout 用于在一定时间后执行特定的逻辑,第一个参数为时间到了之后要执行的函数(回调函数),第二个参数是等待时间。例如:

    setTimeout(someFunction, 1000); 

    就会在 1000 毫秒后执行 someFunction 函数。

    代码如下:

    setTimeout(() => { console.log('Hello World!'); }, 3000); console.log('当前进程 ID', process.pid); console.log('当前脚本路径', __filename); const time = new Date(); console.log('当前时间', time.toLocaleString()); 

    运行以上脚本,在我机器上的输出如下( Hello World! 会延迟三秒输出):

    $ node timer.js 当前进程 ID 7310 当前脚本路径 /Users/mRc/Tutorials/nodejs-quickstart/timer.js 当前时间 12/4/2019, 9:49:28 AM Hello World! 

    从上面的代码中也可以一瞥 Node.js 异步的魅力:在 setTimeout 等待的 3 秒内,程序并没有阻塞,而是继续向下执行,这就是 Node.js 的异步非阻塞!

    提示

    在实际的应用环境中,往往有很多 I/O 操作(例如网络请求、数据库查询等等)需要耗费相当多的时间,而 Node.js 能够在等待的同时继续处理新的请求,大大提高了系统的吞吐率。

    在后续教程中,我们会出一篇深入讲解 Node.js 异步编程的教程,敬请期待!

    理解 Node 模块机制

    Node.js 相比之前的浏览器 Javascript 的另一个重点改变就是:模块机制的引入。这一节内容很长,但却是入门 Node.js 最为关键的一步,加油吧!

    Javascript 的模块化之路

    Eric Raymond 在《 UNIX 编程艺术》中定义了模块性( Modularity )的规则:

    开发人员应使用通过定义明确的接口连接的简单零件来构建程序,因此问题是局部的,可以在将来的版本中替换程序的某些部分以支持新功能。 该规则旨在节省调试复杂、冗长且不可读的复杂代码的时间。

    “分而治之”的思想在计算机的世界非常普遍,但是在 ES2015 标准出现以前(不了解没关系,后面会讲到),Javascript 语言定义本身并没有模块化的机制,构建复杂应用也没有统一的接口标准。人们通常使用一系列的 <script> 标签来导入相应的模块(依赖):

    <head> <script src="fileA.js"></script> <script src="fileB.js"></script> </head> 

    这种组织 JS 代码的方式有很多问题,其中最显著的包括:

    • 导入的多个 JS 文件直接作用于全局命名空间,很容易产生命名冲突
    • 导入的 JS 文件之间不能相互访问,例如 fileB.js 中无法访问 fileA.js 中的内容,很不方便
    • 导入的 <script> 无法被轻易去除或修改

    人们渐渐认识到了 Javascript 模块化机制的缺失带来的问题,于是两大模块化规范被提出:

    1. AMD ( Asynchronous Module Definition )规范,在浏览器中使用较为普遍,最经典的实现包括 RequireJS
    2. CommonJS 规范,致力于为 Javascript 生态圈提供统一的接口 API,Node.js 所实现的正是这一模块标准。

    提示

    ECMAScript 2015 (也就是大家常说的 ES6 )标准为 Javascript 语言引入了全新的模块机制(称为 ES 模块,全称 ECMAScript Modules ),并提供了 importexport 关键词,如果感兴趣可参考这篇文章。但是截止目前,Node.js 对 ES 模块的支持还处于试验阶段,因此这篇文章不会讲解、也不提倡使用。

    什么是 Node 模块

    在正式分析 Node 模块机制之前,我们需要明确定义什么是 Node 模块。通常来说,Node 模块可分为两大类:

    • 核心模块:Node 提供的内置模块,在安装 Node 时已经被编译成二进制可执行文件
    • 文件模块:用户编写的模块,可以是自己写的,也可以是通过 npm 安装的(后面会讲到)。

    其中,文件模块可以是一个单独的文件(以 .js.node.json 结尾),或者是一个目录。当这个模块是一个目录时,模块名就是目录名,有两种情况:

    1. 目录中有一个 package.json 文件,则这个 Node 模块的入口就是其中 main 字段指向的文件;
    2. 目录中有一个名为 index 的文件,扩展名为 .js.node.json,此文件则为模块入口文件。

    一下子消化不了没关系,可以先阅读后面的内容,忘记了模块的定义可以再回过来看看哦。

    Node 模块机制浅析

    知道了 Node 模块的具体定义后,我们来了解一下 Node 具体是怎样实现模块机制的。具体而言,Node 引入了三个新的全局对象(还是 Node 专属哦):1 )require; 2 ) exports 和 3 )module。下面我们逐一讲解。

    require

    require 用于导入其他 Node 模块,其参数接受一个字符串代表模块的名称或路径,通常被称为模块标识符。具体有以下三种形式:

    • 直接写模块名称,通常是核心模块或第三方文件模块,例如 osexpress
    • 模块的相对路径,指向项目中其他 Node 模块,例如 ./utils
    • 模块的绝对路径(不推荐!),例如 /home/xxx/MyProject/utils

    提示

    在通过路径导入模块时,通常省略文件名中的 .js 后缀。

    代码示例如下:

    // 导入内置库或第三方模块 const os = require('os'); const express = require('express'); // 通过相对路径导入其他模块 const utils = require('./utils'); // 通过绝对路径导入其他模块 const utils = require('/home/xxx/MyProject/utils'); 

    你也许会好奇,通过名称导入 Node 模块的时候(例如 express),是从哪里找到这个模块的?实际上每个模块都有个路径搜索列表 module.paths,在后面讲解 module 对象的时候就会一清二楚了。

    exports

    我们已经学会了用 require 导入其他模块中的内容,那么怎么写一个 Node 模块,并导出其中内容呢?答案就是用 exports 对象。

    例如我们写一个 Node 模块 myModule.js:

    // myModule.js function add(a, b) { return a + b; } // 导出函数 add exports.add = add; 

    通过将 add 函数添加到 exports 对象中,外面的模块就可以通过以下代码使用这个函数。在 myModule.js 旁边创建一个 main.js ,代码如下:

    // main.js const myModule = require('./myModule'); // 调用 myModule.js 中的 add 函数 myModule.add(1, 2); 

    提示

    如果你熟悉 ECMAScript 6 中的解构赋值,那么可以用更优雅的方式获取 add 函数:

    const { add } = require('./myModule'); 

    module

    通过 requireexports,我们已经知道了如何导入、导出 Node 模块中的内容,但是你可能还是觉得 Node 模块机制有一丝丝神秘的感觉。接下来,我们将掀开这神秘的面纱,了解一下背后的主角module 模块对象。

    我们可以在刚才的 myModule.js 文件的最后加上这一行代码:

    console.log('module myModule:', module); 

    在 main.js 最后加上:

    console.log('module main:', module); 

    运行后会打印出来这样的内容(左边是 myModule,右边是 module ):

    可以看到 module 对象有以下字段:

    • id:模块的唯一标识符,如果是被运行的主程序(例如 main.js )则为 .,如果是被导入的模块(例如 myModule.js )则等同于此文件名(即下面的 filename 字段)
    • pathfilename:模块所在路径和文件名,没啥好说的
    • exports:模块所导出的内容,实际上之前的 exports 对象是指向 module.exports 的引用。例如对于 myModule.js ,刚才我们导出了 add 函数,因此出现在了这个 exports 字段里面;而 main.js 没有导出任何内容,因此 exports 字段为空
    • parentchildren:用于记录模块之间的导入关系,例如 main.js 中 require 了 myModule.js ,那么 main 就是 myModule 的 parent,myModule 就是 main 的 children
    • loaded:模块是否被加载,从上图中可以看出只有 children 中列出的模块才会被加载
    • paths:这个就是 Node 搜索文件模块的路径列表,Node 会从第一个路径到最后一个路径依次搜索指定的 Node 模块,找到了则导入,找不到就会报错

    提示

    如果你仔细观察,会发现 Node 文件模块查找路径(module.paths)的方式其实是这样的:先找当前目录下的 node_modules,没有的话再找上一级目录的 node_modules,还没找到的话就一直向上找,直到根目录下的 node_modules 。

    深入理解 module.exports

    之前我们提到,exports 对象本质上是 module.exports 的引用。也就是说,下面两行代码是等价的:

    // 导出 add 函数 exports.add = add; // 和上面一行代码是一样的 module.exports.add = add; 

    实际上还有第二种导出方式,直接把 add 函数赋给 module.exports 对象:

    module.exports = add; 

    这样写和第一种导出方式有什么区别呢?第一种方式,在 exports 对象上添加一个属性名为 add,该属性的值为 add 函数;第二种方式,直接令 exports 对象为 add 函数。可能有点绕,但是请一定要理解这两者的重大区别!

    require 时,两者的区别就很明显了:

    // 第一种导出方式,需要访问 add 属性获取到 add 函数 const myModule = require('myModule'); myModule.add(1, 2); // 第二种导出方式,可以直接使用 add 函数 const add = require('myModule'); add(1, 2); 

    警告

    直接写 exports = add; 无法导出 add 函数,因为 exports 本质上是指向 moduleexports 属性的引用,直接对 exports 赋值只会改变 exports,对 module.exports 没有影响。如果你觉得难以理解,那我们用 appleprice 类比 moduleexports

    apple = { price: 1 }; // 想象 apple 就是 module price = apple.price; // 想象 price 就是 exports apple.price = 3; // 改变了 apple.price price = 3; // 只改变了 price,没有改变 apple.price 

    我们只能通过 apple.price = 1 设置 price 属性,而直接对 price 赋值并不能修改 apple.price

    重构 timer 脚本

    在聊了这么多关于 Node 模块机制的内容后,是时候回到我们之前的定时器脚本 timer.js 了。我们首先创建一个新的 Node 模块 info.js ,用于打印系统信息,代码如下:

    const os = require('os'); function printProgramInfo() { console.log('当前用户', os.userInfo().username); console.log('当前进程 ID', process.pid); console.log('当前脚本路径', __filename); } module.exports = printProgramInfo; 

    这里我们导入了 Node 内置模块 os,并通过 os.userInfo() 查询到了系统用户名,接着通过 module.exports 导出了 printProgramInfo 函数。

    然后创建第二个 Node 模块 datetime.js ,用于返回当前的时间,代码如下:

    function getCurrentTime() { const time = new Date(); return time.toLocaleString(); } exports.getCurrentTime = getCurrentTime; 

    上面的模块中,我们选择了通过 exports 导出 getCurrentTime 函数。

    最后,我们在 timer.js 中通过 require 导入刚才两个模块,并分别调用模块中的函数 printProgramInfogetCurrentTime,代码如下:

    const printProgramInfo = require('./inf'); const datetime = require('./datetime'); setTimeout(() => { console.log('Hello World!'); }, 3000); printProgramInfo(); console.log('当前时间', datetime.getCurrentTime()); 

    再运行一下 timer.js ,输出内容应该与之前完全一致。

    读到这里,我想先恭喜你渡过了 Node.js 入门最难的一关!如果你已经真正地理解了 Node 模块机制,那么我相信接下来的学习会无比轻松哦。

    命令行开发:接受输入参数

    Node.js 作为可以在操作系统中直接运行 Javascript 代码的平台,为前端开发者开启了无限可能,其中就包括一系列用于实现前端自动化工作流的命令行工具,例如 GruntGulp 还有大名鼎鼎的 Webpack

    从这一步开始,我们将把 timer.js 改造成一个命令行应用。具体地,我们希望 timer.js 可以通过命令行参数指定等待的时间(time 选项)和最终输出的信息(message 选项):

    $ node timer.js --time 5 --message "Hello Tuture" 

    通过 process.argv 读取命令行参数

    之前在讲全局对象 process 时提到一个 argv 属性,能够获取命令行参数的数组。创建一个 args.js 文件,代码如下:

    console.log(process.argv); 

    然后运行以下命令:

    $ node args.js --time 5 --message "Hello Tuture" 

    输出一个数组:

    [ '/Users/mRc/.nvm/versions/node/v12.10.0/bin/node', '/Users/mRc/Tutorials/nodejs-quickstart/args.js', '--time', '5', '--message', 'Hello Tuture' ] 

    可以看到,process.argv 数组的第 0 个元素是 node 的实际路径,第 1 个元素是 args.js 的路径,后面则是输入的所有参数。

    实现命令行应用

    根据刚才的分析,我们可以非常简单粗暴地获取 process.argv 的第 3 个和第 5 个元素,分别可以得到 timemessage 参数。于是修改 timer.js 的代码如下:

    const printProgramInfo = require('./info'); const datetime = require('./datetime'); const waitTime = Number(process.argv[3]); const message = process.argv[5]; setTimeout(() => { console.log(message); }, waitTime * 1000); printProgramInfo(); console.log('当前时间', datetime.getCurrentTime()); 

    提醒一下,setTimeout 中时间的单位是毫秒,而我们指定的时间参数单位是秒,因此要乘 1000 。

    运行 timer.js ,加上刚才说的所有参数:

    $ node timer.js --time 5 --message "Hello Tuture" 

    等待 5 秒钟后,你就看到了 Hello Tuture 的提示文本!

    不过很显然,目前这个版本有很大的问题:输入参数的格式是固定的,很不灵活,比如说调换 timemessage 的输入顺序就会出错,也不能检查用户是否输入了指定的参数,格式是否正确等等。如果要亲自实现上面所说的功能,那可得花很大的力气,说不定还会有不少 Bug 。有没有更好的方案呢?

    npm:洪荒之力,都赐予你

    从这一节开始,你将不再是一个人写代码。你的背后将拥有百万名 Javascript 开发者的支持,而这一切仅需要 npm 就可以实现。npm 包括:

    • npm 命令行工具(安装 node 时也会附带安装)
    • npm 集中式依赖仓库( registry ),存放了其他 Javascript 开发者分享的 npm 包
    • npm 网站,可以搜索需要的 npm 包、管理 npm 帐户等

    npm 初探

    我们首先打开终端(命令行),检查一下 npm 命令是否可用:

    $ npm -v 6.10.3 

    然后在当前目录(也就是刚才编辑的 timer.js 所在的文件夹)运行以下命令,把当前项目初始化为 npm 项目:

    $ npm init 

    这时候 npm 会提一系列问题,你可以一路回车下去,也可以仔细回答,最终会创建一个 package.json 文件。package.json 文件是一个 npm 项目的核心,记录了这个项目所有的关键信息,内容如下:

    { "name": "timer", "version": "1.0.0", "description": "A cool timer", "main": "timer.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "git+https://github.com/mRcfps/nodejs-quickstart.git" }, "author": "mRcfps", "license": "ISC", "bugs": { "url": "https://github.com/mRcfps/nodejs-quickstart/issues" }, "homepage": "https://github.com/mRcfps/nodejs-quickstart#readme" } 

    其中大部分字段的含义都很明确,例如 name 项目名称、 version 版本号、description 描述、author 作者等等。不过这个 scripts 字段你可能会比较困惑,我们会在下一节中详细介绍。

    安装 npm 包

    接下来我们将讲解 npm 最最最常用的命令 install。没错,毫不夸张地说,一个 Javascript 程序员用的最多的 npm 命令就是 npm install

    在安装我们需要的 npm 包之前,我们需要去探索一下有哪些包可以为我们所用。通常,我们可以在 npm 官方网站 上进行关键词搜索(记得用英文哦),比如说我们搜 command line:

    出来的第一个结果 commander 就很符合我们的需要,点进去就是安装的说明和使用文档。我们还想要一个“加载中”的动画效果,提高用户的使用体验,试着搜一下 loading 关键词:

    第二个结果 ora 也符合我们的需要。那我们现在就安装这两个 npm 包:

    $ npm install commander ora 

    少许等待后,可以看到 package.json 多了一个非常重要的 dependencies 字段:

    "dependencies": { "commander": "^4.0.1", "ora": "^4.0.3" } 

    这个字段中就记录了我们这个项目的直接依赖。与直接依赖相对的就是间接依赖,例如 commander 和 ora 的依赖,我们通常不用关心。所有的 npm 包(直接依赖和间接依赖)全部都存放在项目的 node_modules 目录中。

    提示

    node_modules 通常有很多的文件,因此不会加入到 Git 版本控制系统中,你从网上下载的 npm 项目一般也只会有 package.json,这时候只需运行 npm install(后面不跟任何内容),就可以下载并安装所有依赖了。

    整个 package.json 代码如下所示:

    { "name": "timer", "version": "1.0.0", "description": "A cool timer", "main": "timer.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "git+https://github.com/mRcfps/nodejs-quickstart.git" }, "author": "mRcfps", "license": "ISC", "bugs": { "url": "https://github.com/mRcfps/nodejs-quickstart/issues" }, "homepage": "https://github.com/mRcfps/nodejs-quickstart#readme", "dependencies": { "commander": "^4.0.1", "ora": "^4.0.3" } } 

    关于版本号

    在软件开发中,版本号是一个非常重要的概念,不同版本的软件存在或大或小的差异。npm 采用了语义版本号( Semantic Versioning,简称 semver),具体规定如下:

    • 版本格式为:主版本号.次版本号.修订号
    • 主版本号的改变意味着不兼容的 API 修改
    • 次版本号的改变意味着做了向下兼容的功能性新增
    • 修订号的改变意味着做了向下兼容的问题修正

    提示

    向下兼容的简单理解就是功能只增不减

    因此在 package.json 的 dependencies 字段中,可以通过以下方式指定版本:

    • 精确版本:例如 1.0.0,一定只会安装版本为 1.0.0 的依赖
    • 锁定主版本和次版本:可以写成 1.01.0.x~1.0.0,那么可能会安装例如 1.0.8 的依赖
    • 仅锁定主版本:可以写成 11.x^1.0.0npm install 默认采用的形式),那么可能会安装例如 1.1.0 的依赖
    • 最新版本:可以写成 *x,那么直接安装最新版本(不推荐)

    你也许注意到了 npm 还创建了一个 package-lock.json,这个文件就是用来锁定全部直接依赖和间接依赖的精确版本号,或者说提供了关于 node_modules 目录的精确描述,从而确保在这个项目中开发的所有人都能有完全一致的 npm 依赖。

    站在巨人的肩膀上

    我们在大致读了一下 commander 和 ora 的文档之后,就可以开始用起来了,修改 timer.js 代码如下:

    const program = require('commander'); const ora = require('ora'); const printProgramInfo = require('./info'); const datetime = require('./datetime'); program .option('-t, --time <number>', '等待时间 (秒)', 3) .option('-m, --message <string>', '要输出的信息', 'Hello World') .parse(process.argv); setTimeout(() => { spinner.stop(); console.log(program.message); }, program.time * 1000); printProgramInfo(); console.log('当前时间', datetime.getCurrentTime()); const spinner = ora('正在加载中,请稍后 ...').start(); 

    这次,我们再次运行 timer.js:

    $ node timer.js --message "洪荒之力!" --time 5 

    转起来了!

    因 V 站字数限制,下面还有两节:1 )尝试 npm scripts 2 )监听 exit 事件的内容如果大家有兴趣可以访问图雀社区主站查看

    想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。

    29 条回复    2020-04-22 11:08:35 +08:00
    Solace202
        1
    Solace202  
       2020-04-20 23:23:58 +08:00   1
    喝了两杯茶了。。。
    wszgrcy
        2
    wszgrcy  
       2020-04-20 23:33:16 +08:00 via Android
    吨吨吨,我喝完了
    narfnas
        3
    narfnas  
       2020-04-20 23:37:09 +08:00
    什么茶能喝这么久
    v2vTZ
        4
    v2vTZ  
       2020-04-21 01:44:30 +08:00 via iPhone
    楼主还有干货么?我看着买点茶叶
    Mac
        5
    Mac  
       2020-04-21 02:36:23 +08:00
    注意,你将浪费你人生中一杯茶的时间看介绍,请从起步开始看。。。
    JB18CM
        6
    JB18CM  
       2020-04-21 05:44:07 +08:00
    喝了都快吐了, 还要喝
    WilsonGGG
        7
    WilsonGGG  
       2020-04-21 09:03:57 +08:00
    soli
        8
    soli  
       2020-04-21 09:10:35 +08:00
    有人请喝茶,那就喝一杯。先干为敬。
    Leonard
        9
    Leonard  
       2020-04-21 09:13:21 +08:00
    一杯茶是要出系列么
    areless
        10
    areless  
       2020-04-21 09:13:27 +08:00 via Android
    实话实说,这杯茶没有隔壁 lua python 那几杯清淡。
    YanSep
        11
    YanSep  
       2020-04-21 09:15:00 +08:00 via Android
    你放错节点咯
    huan1043269994
        12
    huan1043269994  
    OP
       2020-04-21 09:28:42 +08:00
    @Flobit 我是放的 Node.js o()o,貌似被改了?
    huan1043269994
        13
    huan1043269994  
    OP
       2020-04-21 09:29:22 +08:00
    @Leonard 嗯嗯
    MaxTan
        14
    MaxTan  
       2020-04-21 10:34:10 +08:00
    又是你? 有没有一桶饭时间的教程,一杯茶太快了
    dinjufen
        15
    dinjufen  
       2020-04-21 11:00:43 +08:00
    真的是一杯茶,用来摸鱼挺不错的!
    alinwu05
        16
    alinwu05  
       2020-04-21 11:08:05 +08:00
    教程真不错!赞
    libook
        17
    libook  
       2020-04-21 11:20:10 +08:00 via Android
    看到“Node.js 太火了”这一句之后,我反复确认了一下发帖时间,确认不是 2014 年……

    其实学 Node 真的很快,因为 Node 本身没多少东西,官网刷一遍 Guides 和 API 就 OK 了。
    搞后端大量的知识都是关于系统架构和中间件。

    当然 JS 语言本身是个大坑。
    Sivan
        18
    Sivan  
       2020-04-21 11:22:34 +08:00
    你这茶是 1.25L 装的吧?
    zooo
        19
    zooo  
       2020-04-21 11:29:47 +08:00
    嗝,喝饱了。。
    Jafee
        20
    Jafee  
       2020-04-21 11:36:49 +08:00
    写的真的很好,赞!
    zengming00
        21
    zengming00  
       2020-04-21 11:45:48 +08:00
    技术的东西会的人越多越不值钱,程序猿总是努力使大家都失业
    ironMan1995
        22
    ironMan1995  
       2020-04-21 11:49:35 +08:00 via Android
    上手很简单,深入很难。我已经从 C 开始学了
    finely
        23
    finely  
       2020-04-21 11:52:26 +08:00
    这些教程都是你自己写的吗,很难得见到这样逻辑清晰言简意赅的教程了,希望能继续写下去,迟早会火
    huan1043269994
        24
    huan1043269994  
    OP
       2020-04-21 12:18:00 +08:00
    @finely 谢谢你,这些是由我们图雀社区的认证作者写的哦,我们是今年年初新创的一个专门传播实战技术教程的社区,主要研发开源了一款实战教程写作工具 Tuture,然后创建了一个社区用来汇聚这些教程,有兴趣可以查看我们的文档( https://www.yuque.com/tuture/product-manuals/wsv091 ),或者关注我们的公众号交流哦
    huangbangsheng
        25
    huangbangsheng  
       2020-04-21 12:21:46 +08:00
    只看了一盏茶的时间。“什么是 node ?”
    还不错
    yafoo
        26
    yafoo  
       2020-04-21 13:59:15 +08:00 via Android
    @libook 为什么说是大坑?有哪些大坑?
    reiji
        27
    reiji  
       2020-04-21 15:59:52 +08:00 via Android
    这个系列超级喜欢,爱你!
    huan1043269994
        28
    huan1043269994  
    OP
       2020-04-21 16:04:48 +08:00
    @reiji 爱你! Thanks(ω)
    libook
        29
    libook  
       2020-04-22 11:08:35 +08:00
    @yafoo JS 学习曲线平缓,上手也很快,但是上手之后要想达到精通需要很长时间。
    1. 语法格式对换行和缩进不敏感( C 语言风格),不像 Python 那样风格相对统一,想要写出优美的代码需要更多的经验积累。
    2. 提供的语法特性非常多,2015 年之前几年不更新一次,但是在 2015 年之后,几乎每个月多有多项新的特性增加,一直持续到现在,一个问题往往可以用不同的特性组合出不同的方案出来,不像 Go 那样“高度标准化”,开发人员花在代码选型方面的时间也更多。
    3. 弱类型(虽然现在正在逐渐补充一些类型),对于逻辑密集型的程序会非常高效,但对于类型密集的场景 Bug 风险会比较高。所以需要大量经验的积累才能确保写出可靠的代码。
    4. 可应用领域太多了,Web 应用开发、服务端开发、工具开发、数据库和中间件脚本、App 端开发、硬件开发(树莓派可以用 GPIO 的 JS API 开发)、桌面应用开发……每一个领域都有引擎、框架、库可以学。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     5340 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 30ms UTC 01:22 PVG 09:22 LAX 17:22 JFK 20:22
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86