V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
huan1043269994
V2EX  ›  问与答

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

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

    我们研发开源了一款基于 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('./info');
    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 那几杯清淡。
    Flobit
        11
    Flobit  
       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   ·   实用小工具   ·   5691 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 33ms · UTC 03:37 · PVG 11:37 · LAX 19:37 · JFK 22:37
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.