一杯茶的時(shí)間腋粥,上手 Node.js 開發(fā)

Node.js 太火了晦雨,火到幾乎所有前端工程師都想學(xué)架曹,幾乎所有后端工程師也想學(xué)。一說到 Node.js闹瞧,我們馬上就會(huì)想到“異步”绑雄、“事件驅(qū)動(dòng)”、“非阻塞”奥邮、“性能優(yōu)良”這幾個(gè)特點(diǎn)万牺,但是你真的理解這些詞的含義嗎?這篇教程將帶你快速入門 Node.js洽腺,為后續(xù)的前端學(xué)習(xí)或是 Node.js 進(jìn)階打下堅(jiān)實(shí)的基礎(chǔ)脚粟。

此教程屬于Node.js 后端工程師學(xué)習(xí)路線的一部分,點(diǎn)擊可查看全部內(nèi)容蘸朋。

起步

什么是 Node核无?

簡單地說,Node(或者說 Node.js藕坯,兩者是等價(jià)的)是 JavaScript 的一種運(yùn)行環(huán)境团南。在此之前,我們知道 JavaScript 都是在瀏覽器中執(zhí)行的炼彪,用于給網(wǎng)頁添加各種動(dòng)態(tài)效果吐根,那么可以說瀏覽器也是 JavaScript 的運(yùn)行環(huán)境。那么這兩個(gè)運(yùn)行環(huán)境有哪些差異呢辐马?請(qǐng)看下圖:

兩個(gè)運(yùn)行環(huán)境共同包含了 ECMAScript拷橘,也就是剝離了所有運(yùn)行環(huán)境的 JavaScript 語言標(biāo)準(zhǔn)本身。現(xiàn)在 ECMAScript 的發(fā)展速度非常驚人喜爷,幾乎能夠做到每年發(fā)展一個(gè)版本膜楷。

提示

ECMAScript 和 JavaScript 的關(guān)系是,前者是后者的規(guī)格贞奋,后者是前者的一種實(shí)現(xiàn)赌厅。在日常場(chǎng)合,這兩個(gè)詞是可以互換的轿塔。更多背景知識(shí)可參考阮一峰的《JavaScript語言的歷史》特愿。

另一方面,瀏覽器端 JavaScript 還包括了:

  • 瀏覽器對(duì)象模型(Browser Object Model勾缭,簡稱 BOM)揍障,也就是 window 對(duì)象
  • 文檔對(duì)象模型(Document Object Model,簡稱 DOM)俩由,也就是 document 對(duì)象

而 Node.js 則是包括 V8 引擎毒嫡。V8 是 Chrome 瀏覽器中的 JavaScript 引擎,經(jīng)過多年的發(fā)展和優(yōu)化幻梯,性能和安全性都已經(jīng)達(dá)到了相當(dāng)?shù)母叨榷祷6?Node.js 則進(jìn)一步將 V8 引擎加工成可以在任何操作系統(tǒng)中運(yùn)行 JavaScript 的平臺(tái)努释。

預(yù)備知識(shí)

在正式開始這篇教程之前,我們希望你已經(jīng)做好了以下準(zhǔn)備:

  • 了解 JavaScript 語言的基礎(chǔ)知識(shí)咬摇,如果有過瀏覽器 JS 開發(fā)經(jīng)驗(yàn)就更好了
  • 已經(jīng)安裝了 Node.js管削,配置好了適合自己的編輯器或 IDE
  • 了解相對(duì)路徑和絕對(duì)路徑

學(xué)習(xí)目標(biāo)

這篇教程將會(huì)讓你學(xué)到:

  • 瀏覽器 JavaScript 與 Node.js 的關(guān)系與區(qū)別
  • 了解 Node.js 有哪些全局對(duì)象
  • 掌握 Node.js 如何導(dǎo)入和導(dǎo)出模塊遏暴,以及模塊機(jī)制的原理
  • 了解如何用 Node.js 開發(fā)簡單的命令行應(yīng)用
  • 學(xué)會(huì)利用 npm 社區(qū)的力量解決開發(fā)中遇到的難題,避免“重復(fù)造輪子”
  • 了解 npm scripts 的基本概念和使用
  • 初步了解 Node.js 的事件機(jī)制

運(yùn)行 Node 代碼

運(yùn)行 Node 代碼通常有兩種方式:1)在 REPL 中交互式輸入和運(yùn)行;2)將代碼寫入 JS 文件盆均,并用 Node 執(zhí)行铺根。

提示

REPL 的全稱是 Read Eval Print Loop(讀取-執(zhí)行-輸出-循環(huán))胚股,通惩ピ伲可以理解為交互式解釋器,你可以輸入任何表達(dá)式或語句芒珠,然后就會(huì)立刻執(zhí)行并返回結(jié)果烛卧。如果你用過 Python 的 REPL 一定會(huì)覺得很熟悉。

使用 REPL 快速體驗(yàn)

如果你已經(jīng)安裝好了 Node妓局,那么運(yùn)行以下命令就可以輸出 Node.js 的版本:

$ node -v
v12.10.0

然后总放,我們還可以進(jìn)入 Node REPL(直接輸入 node),然后輸入任何合法的 JavaScript 表達(dá)式或語句:

$ 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

有些行的開頭是 >好爬,代表輸入提示符局雄,因此 > 后面的都是我們要輸入的命令,其他行則是表達(dá)式的返回值或標(biāo)準(zhǔn)輸出(Standard Output存炮,stdout)炬搭。運(yùn)行的效果如下:

編寫 Node 腳本

REPL 通常用來進(jìn)行一些代碼的試驗(yàn)。在搭建具體應(yīng)用時(shí)穆桂,更多的還是創(chuàng)建 Node 文件宫盔。我們先創(chuàng)建一個(gè)最簡單的 Node.js 腳本文件,叫做 timer.js享完,代碼如下:

console.log('Hello World!');

然后用 Node 解釋器執(zhí)行這個(gè)文件:

$ node timer.js
Hello World!

看上去非常平淡無奇灼芭,但是這一行代碼卻凝聚了 Node.js 團(tuán)隊(duì)背后的心血。我們來對(duì)比一下般又,在瀏覽器和 Node 環(huán)境中執(zhí)行這行代碼有什么區(qū)別:

  • 在瀏覽器運(yùn)行 console.log 調(diào)用了 BOM彼绷,實(shí)際上執(zhí)行的是 window.console.log('Hello World!')
  • Node 首先在所處的操作系統(tǒng)中創(chuàng)建一個(gè)新的進(jìn)程,然后向標(biāo)準(zhǔn)輸出打印了指定的字符串茴迁, 實(shí)際上執(zhí)行的是 process.stdout.write('Hello World!\n')

簡而言之寄悯,Node 為我們提供了一個(gè)無需依賴瀏覽器、能夠直接與操作系統(tǒng)進(jìn)行交互的 JavaScript 代碼運(yùn)行環(huán)境堕义!

Node 全局對(duì)象初探

如果你有過編寫 JavaScript 的經(jīng)驗(yàn)猜旬,那么你一定對(duì)全局對(duì)象不陌生。在瀏覽器中,我們有 documentwindow 等全局對(duì)象洒擦;而 Node 只包含 ECMAScript 和 V8椿争,不包含 BOM 和 DOM,因此 Node 中不存在 documentwindow秘遏;取而代之丘薛,Node 專屬的全局對(duì)象是 process嘉竟。在這一節(jié)中邦危,我們將初步探索一番 Node 全局對(duì)象。

JavaScript 全局對(duì)象的分類

在此之前舍扰,我們先看一下 JavaScript 各個(gè)運(yùn)行環(huán)境的全局對(duì)象的比較倦蚪,如下圖所示:

可以看到 JavaScript 全局對(duì)象可以分為四類:

  1. 瀏覽器專屬,例如 window边苹、alert 等等陵且;
  2. Node 專屬,例如 process个束、Buffer慕购、__dirname__filename 等等茬底;
  3. 瀏覽器和 Node 共有沪悲,但是實(shí)現(xiàn)方式不同,例如 console(第一節(jié)中已提到)阱表、setTimeout殿如、setInterval 等;
  4. 瀏覽器和 Node 共有最爬,并且屬于 ECMAScript 語言定義的一部分涉馁,例如 DateString爱致、Promise 等烤送;

Node 專屬全局對(duì)象解析

process

process 全局對(duì)象可以說是 Node.js 的靈魂,它是管理當(dāng)前 Node.js 進(jìn)程狀態(tài)的對(duì)象糠悯,提供了與操作系統(tǒng)的簡單接口胯努。

首先我們探索一下 process 對(duì)象的重要屬性。打開 Node REPL逢防,然后我們查看一下 process 對(duì)象的一些屬性:

  • pid:進(jìn)程編號(hào)
  • env:系統(tǒng)環(huán)境變量
  • argv:命令行執(zhí)行此腳本時(shí)的輸入?yún)?shù)
  • platform:當(dāng)前操作系統(tǒng)的平臺(tái)

提示

可以在 Node REPL 中嘗試一下這些對(duì)象叶沛。像上面說的那樣進(jìn)入 REPL(你的輸出很有可能跟我的不一樣):

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

Buffer

Buffer 全局對(duì)象讓 JavaScript 也能夠輕松地處理二進(jìn)制數(shù)據(jù)流,結(jié)合 Node 的流接口(Stream)忘朝,能夠?qū)崿F(xiàn)高效的二進(jìn)制文件處理灰署。這篇教程不會(huì)涉及 Buffer

__filename__dirname

分別代表當(dāng)前所運(yùn)行 Node 腳本的文件路徑和所在目錄路徑。

警告

__filename__dirname 只能在 Node 腳本文件中使用溉箕,在 REPL 中是沒有定義的晦墙。

使用 Node 全局對(duì)象

接下來我們將在剛才寫的腳本文件中使用 Node 全局對(duì)象,分別涵蓋上面的三類:

  • Node 專屬:process
  • 實(shí)現(xiàn)方式不同的共有全局對(duì)象:consolesetTimeout
  • ECMAScript 語言定義的全局對(duì)象:Date

提示

setTimeout 用于在一定時(shí)間后執(zhí)行特定的邏輯肴茄,第一個(gè)參數(shù)為時(shí)間到了之后要執(zhí)行的函數(shù)(回調(diào)函數(shù))晌畅,第二個(gè)參數(shù)是等待時(shí)間。例如:

setTimeout(someFunction, 1000);

就會(huì)在 1000 毫秒后執(zhí)行 someFunction 函數(shù)寡痰。

代碼如下:

setTimeout(() => {
  console.log('Hello World!');
}, 3000);

console.log('當(dāng)前進(jìn)程 ID', process.pid);
console.log('當(dāng)前腳本路徑', __filename);

const time = new Date();
console.log('當(dāng)前時(shí)間', time.toLocaleString());

運(yùn)行以上腳本抗楔,在我機(jī)器上的輸出如下(Hello World! 會(huì)延遲三秒輸出):

$ node timer.js
當(dāng)前進(jìn)程 ID 7310
當(dāng)前腳本路徑 /Users/mRc/Tutorials/nodejs-quickstart/timer.js
當(dāng)前時(shí)間 12/4/2019, 9:49:28 AM
Hello World!

從上面的代碼中也可以一瞥 Node.js 異步的魅力:在 setTimeout 等待的 3 秒內(nèi),程序并沒有阻塞拦坠,而是繼續(xù)向下執(zhí)行连躏,這就是 Node.js 的異步非阻塞!

提示

在實(shí)際的應(yīng)用環(huán)境中贞滨,往往有很多 I/O 操作(例如網(wǎng)絡(luò)請(qǐng)求入热、數(shù)據(jù)庫查詢等等)需要耗費(fèi)相當(dāng)多的時(shí)間,而 Node.js 能夠在等待的同時(shí)繼續(xù)處理新的請(qǐng)求晓铆,大大提高了系統(tǒng)的吞吐率勺良。

在后續(xù)教程中,我們會(huì)出一篇深入講解 Node.js 異步編程的教程骄噪,敬請(qǐng)期待尚困!

理解 Node 模塊機(jī)制

Node.js 相比之前的瀏覽器 JavaScript 的另一個(gè)重點(diǎn)改變就是:模塊機(jī)制的引入。這一節(jié)內(nèi)容很長腰池,但卻是入門 Node.js 最為關(guān)鍵的一步尾组,加油吧??!

JavaScript 的模塊化之路

Eric Raymond 在《UNIX編程藝術(shù)》中定義了模塊性(Modularity)的規(guī)則:

開發(fā)人員應(yīng)使用通過定義明確的接口連接的簡單零件來構(gòu)建程序示弓,因此問題是局部的讳侨,可以在將來的版本中替換程序的某些部分以支持新功能。 該規(guī)則旨在節(jié)省調(diào)試復(fù)雜奏属、冗長且不可讀的復(fù)雜代碼的時(shí)間跨跨。

“分而治之”的思想在計(jì)算機(jī)的世界非常普遍,但是在 ES2015 標(biāo)準(zhǔn)出現(xiàn)以前(不了解沒關(guān)系囱皿,后面會(huì)講到)勇婴, JavaScript 語言定義本身并沒有模塊化的機(jī)制,構(gòu)建復(fù)雜應(yīng)用也沒有統(tǒng)一的接口標(biāo)準(zhǔn)嘱腥。人們通常使用一系列的 <script> 標(biāo)簽來導(dǎo)入相應(yīng)的模塊(依賴):

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

這種組織 JS 代碼的方式有很多問題耕渴,其中最顯著的包括:

  • 導(dǎo)入的多個(gè) JS 文件直接作用于全局命名空間,很容易產(chǎn)生命名沖突
  • 導(dǎo)入的 JS 文件之間不能相互訪問齿兔,例如 fileB.js 中無法訪問 fileA.js 中的內(nèi)容橱脸,很不方便
  • 導(dǎo)入的 <script> 無法被輕易去除或修改

人們漸漸認(rèn)識(shí)到了 JavaScript 模塊化機(jī)制的缺失帶來的問題础米,于是兩大模塊化規(guī)范被提出:

  1. AMD(Asynchronous Module Definition)規(guī)范,在瀏覽器中使用較為普遍添诉,最經(jīng)典的實(shí)現(xiàn)包括 RequireJS屁桑;
  2. CommonJS 規(guī)范,致力于為 JavaScript 生態(tài)圈提供統(tǒng)一的接口 API栏赴,Node.js 所實(shí)現(xiàn)的正是這一模塊標(biāo)準(zhǔn)蘑斧。

提示

ECMAScript 2015(也就是大家常說的 ES6)標(biāo)準(zhǔn)為 JavaScript 語言引入了全新的模塊機(jī)制(稱為 ES 模塊,全稱 ECMAScript Modules)须眷,并提供了 importexport 關(guān)鍵詞竖瘾,如果感興趣可參考這篇文章。但是截止目前柒爸,Node.js 對(duì) ES 模塊的支持還處于試驗(yàn)階段准浴,因此這篇文章不會(huì)講解事扭、也不提倡使用捎稚。

什么是 Node 模塊

在正式分析 Node 模塊機(jī)制之前,我們需要明確定義什么是 Node 模塊求橄。通常來說今野,Node 模塊可分為兩大類:

  • 核心模塊:Node 提供的內(nèi)置模塊,在安裝 Node 時(shí)已經(jīng)被編譯成二進(jìn)制可執(zhí)行文件
  • 文件模塊:用戶編寫的模塊罐农,可以是自己寫的条霜,也可以是通過 npm 安裝的(后面會(huì)講到)。

其中涵亏,文件模塊可以是一個(gè)單獨(dú)的文件(以 .js宰睡、.node.json 結(jié)尾),或者是一個(gè)目錄气筋。當(dāng)這個(gè)模塊是一個(gè)目錄時(shí)拆内,模塊名就是目錄名,有兩種情況:

  1. 目錄中有一個(gè) package.json 文件宠默,則這個(gè) Node 模塊的入口就是其中 main 字段指向的文件麸恍;
  2. 目錄中有一個(gè)名為 index 的文件,擴(kuò)展名為 .js搀矫、.node.json抹沪,此文件則為模塊入口文件。

一下子消化不了沒關(guān)系瓤球,可以先閱讀后面的內(nèi)容融欧,忘記了模塊的定義可以再回過來看看哦。

Node 模塊機(jī)制淺析

知道了 Node 模塊的具體定義后卦羡,我們來了解一下 Node 具體是怎樣實(shí)現(xiàn)模塊機(jī)制的噪馏。具體而言权她,Node 引入了三個(gè)新的全局對(duì)象(還是 Node 專屬哦):1)require;2) exports 和 3)module逝薪。下面我們逐一講解隅要。

require

require 用于導(dǎo)入其他 Node 模塊,其參數(shù)接受一個(gè)字符串代表模塊的名稱或路徑董济,通常被稱為模塊標(biāo)識(shí)符步清。具體有以下三種形式:

  • 直接寫模塊名稱,通常是核心模塊或第三方文件模塊虏肾,例如 os廓啊、express
  • 模塊的相對(duì)路徑,指向項(xiàng)目中其他 Node 模塊封豪,例如 ./utils
  • 模塊的絕對(duì)路徑(不推薦谴轮!),例如 /home/xxx/MyProject/utils

提示

在通過路徑導(dǎo)入模塊時(shí)吹埠,通常省略文件名中的 .js 后綴第步。

代碼示例如下:

// 導(dǎo)入內(nèi)置庫或第三方模塊
const os = require('os');
const express = require('express');

// 通過相對(duì)路徑導(dǎo)入其他模塊
const utils = require('./utils');

// 通過絕對(duì)路徑導(dǎo)入其他模塊
const utils = require('/home/xxx/MyProject/utils');

你也許會(huì)好奇,通過名稱導(dǎo)入 Node 模塊的時(shí)候(例如 express)缘琅,是從哪里找到這個(gè)模塊的粘都?實(shí)際上每個(gè)模塊都有個(gè)路徑搜索列表 module.paths,在后面講解 module 對(duì)象的時(shí)候就會(huì)一清二楚了刷袍。

exports

我們已經(jīng)學(xué)會(huì)了用 require 導(dǎo)入其他模塊中的內(nèi)容翩隧,那么怎么寫一個(gè) Node 模塊,并導(dǎo)出其中內(nèi)容呢呻纹?答案就是用 exports 對(duì)象堆生。

例如我們寫一個(gè) Node 模塊 myModule.js:

// myModule.js
function add(a, b) {
  return a + b;
}

// 導(dǎo)出函數(shù) add
exports.add = add;

通過將 add 函數(shù)添加到 exports 對(duì)象中,外面的模塊就可以通過以下代碼使用這個(gè)函數(shù)雷酪。在 myModule.js 旁邊創(chuàng)建一個(gè) main.js淑仆,代碼如下:

// main.js
const myModule = require('./myModule');

// 調(diào)用 myModule.js 中的 add 函數(shù)
myModule.add(1, 2);

提示

如果你熟悉 ECMAScript 6 中的解構(gòu)賦值,那么可以用更優(yōu)雅的方式獲取 add 函數(shù):

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

module

通過 requireexports太闺,我們已經(jīng)知道了如何導(dǎo)入糯景、導(dǎo)出 Node 模塊中的內(nèi)容,但是你可能還是覺得 Node 模塊機(jī)制有一絲絲神秘的感覺省骂。接下來蟀淮,我們將掀開這神秘的面紗,了解一下背后的主角——module 模塊對(duì)象钞澳。

我們可以在剛才的 myModule.js 文件的最后加上這一行代碼:

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

在 main.js 最后加上:

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

運(yùn)行后會(huì)打印出來這樣的內(nèi)容(左邊是 myModule怠惶,右邊是 module):

可以看到 module 對(duì)象有以下字段:

  • id:模塊的唯一標(biāo)識(shí)符,如果是被運(yùn)行的主程序(例如 main.js)則為 .轧粟,如果是被導(dǎo)入的模塊(例如 myModule.js)則等同于此文件名(即下面的 filename 字段)
  • pathfilename:模塊所在路徑和文件名策治,沒啥好說的
  • exports:模塊所導(dǎo)出的內(nèi)容脓魏,實(shí)際上之前的 exports 對(duì)象是指向 module.exports 的引用。例如對(duì)于 myModule.js通惫,剛才我們導(dǎo)出了 add 函數(shù)茂翔,因此出現(xiàn)在了這個(gè) exports 字段里面;而 main.js 沒有導(dǎo)出任何內(nèi)容履腋,因此 exports 字段為空
  • parentchildren:用于記錄模塊之間的導(dǎo)入關(guān)系珊燎,例如 main.js 中 require 了 myModule.js,那么 main 就是 myModule 的 parent遵湖,myModule 就是 main 的 children
  • loaded:模塊是否被加載悔政,從上圖中可以看出只有 children 中列出的模塊才會(huì)被加載
  • paths:這個(gè)就是 Node 搜索文件模塊的路徑列表,Node 會(huì)從第一個(gè)路徑到最后一個(gè)路徑依次搜索指定的 Node 模塊延旧,找到了則導(dǎo)入谋国,找不到就會(huì)報(bào)錯(cuò)

提示

如果你仔細(xì)觀察,會(huì)發(fā)現(xiàn) Node 文件模塊查找路徑(module.paths)的方式其實(shí)是這樣的:先找當(dāng)前目錄下的 node_modules迁沫,沒有的話再找上一級(jí)目錄的 node_modules芦瘾,還沒找到的話就一直向上找,直到根目錄下的 node_modules弯洗。

深入理解 module.exports

之前我們提到旅急,exports 對(duì)象本質(zhì)上是 module.exports 的引用逢勾。也就是說牡整,下面兩行代碼是等價(jià)的:

// 導(dǎo)出 add 函數(shù)
exports.add = add;

// 和上面一行代碼是一樣的
module.exports.add = add;

實(shí)際上還有第二種導(dǎo)出方式,直接把 add 函數(shù)賦給 module.exports 對(duì)象:

module.exports = add;

這樣寫和第一種導(dǎo)出方式有什么區(qū)別呢溺拱?第一種方式逃贝,在 exports 對(duì)象上添加一個(gè)屬性名為 add,該屬性的值為 add 函數(shù)迫摔;第二種方式沐扳,直接令 exports 對(duì)象為 add 函數(shù)【湔迹可能有點(diǎn)繞沪摄,但是請(qǐng)一定要理解這兩者的重大區(qū)別!

require 時(shí)纱烘,兩者的區(qū)別就很明顯了:

// 第一種導(dǎo)出方式杨拐,需要訪問 add 屬性獲取到 add 函數(shù)
const myModule = require('myModule');
myModule.add(1, 2);

// 第二種導(dǎo)出方式,可以直接使用 add 函數(shù)
const add = require('myModule');
add(1, 2);

警告

直接寫 exports = add; 無法導(dǎo)出 add 函數(shù)擂啥,因?yàn)?exports 本質(zhì)上是指向 moduleexports 屬性的引用哄陶,直接對(duì) exports 賦值只會(huì)改變 exports,對(duì) 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 設(shè)置 price 屬性,而直接對(duì) price 賦值并不能修改 apple.price至扰。

重構(gòu) timer 腳本

在聊了這么多關(guān)于 Node 模塊機(jī)制的內(nèi)容后鳍徽,是時(shí)候回到我們之前的定時(shí)器腳本 timer.js 了。我們首先創(chuàng)建一個(gè)新的 Node 模塊 info.js敢课,用于打印系統(tǒng)信息旬盯,代碼如下:

const os = require('os');

function printProgramInfo() {
  console.log('當(dāng)前用戶', os.userInfo().username);
  console.log('當(dāng)前進(jìn)程 ID', process.pid);
  console.log('當(dāng)前腳本路徑', __filename);
}

module.exports = printProgramInfo;

這里我們導(dǎo)入了 Node 內(nèi)置模塊 os,并通過 os.userInfo() 查詢到了系統(tǒng)用戶名翎猛,接著通過 module.exports 導(dǎo)出了 printProgramInfo 函數(shù)胖翰。

然后創(chuàng)建第二個(gè) Node 模塊 datetime.js,用于返回當(dāng)前的時(shí)間切厘,代碼如下:

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

exports.getCurrentTime = getCurrentTime;

上面的模塊中萨咳,我們選擇了通過 exports 導(dǎo)出 getCurrentTime 函數(shù)。

最后疫稿,我們?cè)?timer.js 中通過 require 導(dǎo)入剛才兩個(gè)模塊培他,并分別調(diào)用模塊中的函數(shù) printProgramInfogetCurrentTime,代碼如下:

const printProgramInfo = require('./info');
const datetime = require('./datetime');

setTimeout(() => {
  console.log('Hello World!');
}, 3000);

printProgramInfo();
console.log('當(dāng)前時(shí)間', datetime.getCurrentTime());

再運(yùn)行一下 timer.js遗座,輸出內(nèi)容應(yīng)該與之前完全一致舀凛。

讀到這里,我想先恭喜你渡過了 Node.js 入門最難的一關(guān)途蒋!如果你已經(jīng)真正地理解了 Node 模塊機(jī)制猛遍,那么我相信接下來的學(xué)習(xí)會(huì)無比輕松哦。

命令行開發(fā):接受輸入?yún)?shù)

Node.js 作為可以在操作系統(tǒng)中直接運(yùn)行 JavaScript 代碼的平臺(tái)号坡,為前端開發(fā)者開啟了無限可能懊烤,其中就包括一系列用于實(shí)現(xiàn)前端自動(dòng)化工作流的命令行工具,例如 Grunt宽堆、Gulp 還有大名鼎鼎的 Webpack腌紧。

從這一步開始,我們將把 timer.js 改造成一個(gè)命令行應(yīng)用畜隶。具體地壁肋,我們希望 timer.js 可以通過命令行參數(shù)指定等待的時(shí)間(time 選項(xiàng))和最終輸出的信息(message 選項(xiàng)):

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

通過 process.argv 讀取命令行參數(shù)

之前在講全局對(duì)象 process 時(shí)提到一個(gè) argv 屬性,能夠獲取命令行參數(shù)的數(shù)組籽慢。創(chuàng)建一個(gè) args.js 文件浸遗,代碼如下:

console.log(process.argv);

然后運(yùn)行以下命令:

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

輸出一個(gè)數(shù)組:

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

可以看到,process.argv 數(shù)組的第 0 個(gè)元素是 node 的實(shí)際路徑嗡综,第 1 個(gè)元素是 args.js 的路徑乙帮,后面則是輸入的所有參數(shù)。

實(shí)現(xiàn)命令行應(yīng)用

根據(jù)剛才的分析极景,我們可以非常簡單粗暴地獲取 process.argv 的第 3 個(gè)和第 5 個(gè)元素察净,分別可以得到 timemessage 參數(shù)驾茴。于是修改 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('當(dāng)前時(shí)間', datetime.getCurrentTime());

提醒一下,setTimeout 中時(shí)間的單位是毫秒氢卡,而我們指定的時(shí)間參數(shù)單位是秒锈至,因此要乘 1000。

運(yùn)行 timer.js译秦,加上剛才說的所有參數(shù):

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

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

不過很顯然筑悴,目前這個(gè)版本有很大的問題:輸入?yún)?shù)的格式是固定的们拙,很不靈活,比如說調(diào)換 timemessage 的輸入順序就會(huì)出錯(cuò)阁吝,也不能檢查用戶是否輸入了指定的參數(shù)砚婆,格式是否正確等等。如果要親自實(shí)現(xiàn)上面所說的功能突勇,那可得花很大的力氣装盯,說不定還會(huì)有不少 Bug。有沒有更好的方案呢甲馋?

npm:洪荒之力埂奈,都賜予你

從這一節(jié)開始,你將不再是一個(gè)人寫代碼定躏。你的背后將擁有百萬名 JavaScript 開發(fā)者的支持账磺,而這一切僅需要 npm 就可以實(shí)現(xiàn)。npm 包括:

  • npm 命令行工具(安裝 node 時(shí)也會(huì)附帶安裝)
  • npm 集中式依賴倉庫(registry)共屈,存放了其他 JavaScript 開發(fā)者分享的 npm 包
  • npm 網(wǎng)站绑谣,可以搜索需要的 npm 包、管理 npm 帳戶等

npm 初探

我們首先打開終端(命令行)拗引,檢查一下 npm 命令是否可用:

$ npm -v
6.10.3

然后在當(dāng)前目錄(也就是剛才編輯的 timer.js 所在的文件夾)運(yùn)行以下命令,把當(dāng)前項(xiàng)目初始化為 npm 項(xiàng)目:

$ npm init

這時(shí)候 npm 會(huì)提一系列問題幌衣,你可以一路回車下去矾削,也可以仔細(xì)回答,最終會(huì)創(chuàng)建一個(gè) package.json 文件豁护。package.json 文件是一個(gè) npm 項(xiàng)目的核心哼凯,記錄了這個(gè)項(xiàng)目所有的關(guān)鍵信息,內(nèi)容如下:

{
  "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 項(xiàng)目名稱断部、 version 版本號(hào)、description 描述班缎、author 作者等等蝴光。不過這個(gè) scripts 字段你可能會(huì)比較困惑她渴,我們會(huì)在下一節(jié)中詳細(xì)介紹。

安裝 npm 包

接下來我們將講解 npm 最最最常用的命令—— install蔑祟。沒錯(cuò)趁耗,毫不夸張地說,一個(gè) JavaScript 程序員用的最多的 npm 命令就是 npm install疆虚。

在安裝我們需要的 npm 包之前苛败,我們需要去探索一下有哪些包可以為我們所用。通常径簿,我們可以在 npm 官方網(wǎng)站 上進(jìn)行關(guān)鍵詞搜索(記得用英文哦)罢屈,比如說我們搜 command line:

出來的第一個(gè)結(jié)果 commander 就很符合我們的需要,點(diǎn)進(jìn)去就是安裝的說明和使用文檔篇亭。我們還想要一個(gè)“加載中”的動(dòng)畫效果儡遮,提高用戶的使用體驗(yàn),試著搜一下 loading 關(guān)鍵詞:

第二個(gè)結(jié)果 ora 也符合我們的需要暗赶。那我們現(xiàn)在就安裝這兩個(gè) npm 包:

$ npm install commander ora

少許等待后鄙币,可以看到 package.json 多了一個(gè)非常重要的 dependencies 字段:

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

這個(gè)字段中就記錄了我們這個(gè)項(xiàng)目的直接依賴。與直接依賴相對(duì)的就是間接依賴蹂随,例如 commander 和 ora 的依賴十嘿,我們通常不用關(guān)心。所有的 npm 包(直接依賴和間接依賴)全部都存放在項(xiàng)目的 node_modules 目錄中岳锁。

提示

node_modules 通常有很多的文件绩衷,因此不會(huì)加入到 Git 版本控制系統(tǒng)中,你從網(wǎng)上下載的 npm 項(xiàng)目一般也只會(huì)有 package.json激率,這時(shí)候只需運(yùn)行 npm install(后面不跟任何內(nèi)容)咳燕,就可以下載并安裝所有依賴了。

整個(gè) 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"
  }
}

關(guān)于版本號(hào)

在軟件開發(fā)中乒躺,版本號(hào)是一個(gè)非常重要的概念招盲,不同版本的軟件存在或大或小的差異。npm 采用了語義版本號(hào)(Semantic Versioning嘉冒,簡稱 semver)曹货,具體規(guī)定如下:

  • 版本格式為:主版本號(hào).次版本號(hào).修訂號(hào)
  • 主版本號(hào)的改變意味著不兼容的 API 修改
  • 次版本號(hào)的改變意味著做了向下兼容的功能性新增
  • 修訂號(hào)的改變意味著做了向下兼容的問題修正

提示

向下兼容的簡單理解就是功能只增不減

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

  • 精確版本:例如 1.0.0顶籽,一定只會(huì)安裝版本為 1.0.0 的依賴
  • 鎖定主版本和次版本:可以寫成 1.01.0.x~1.0.0银觅,那么可能會(huì)安裝例如 1.0.8 的依賴
  • 僅鎖定主版本:可以寫成 1礼饱、1.x^1.0.0npm install 默認(rèn)采用的形式),那么可能會(huì)安裝例如 1.1.0 的依賴
  • 最新版本:可以寫成 *x,那么直接安裝最新版本(不推薦)

你也許注意到了 npm 還創(chuàng)建了一個(gè) package-lock.json镊绪,這個(gè)文件就是用來鎖定全部直接依賴和間接依賴的精確版本號(hào)匀伏,或者說提供了關(guān)于 node_modules 目錄的精確描述,從而確保在這個(gè)項(xiàng)目中開發(fā)的所有人都能有完全一致的 npm 依賴镰吆。

站在巨人的肩膀上

我們?cè)诖笾伦x了一下 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>', '等待時(shí)間 (秒)', 3)
  .option('-m, --message <string>', '要輸出的信息', 'Hello World')
  .parse(process.argv);

setTimeout(() => {
  spinner.stop();
  console.log(program.message);
}, program.time * 1000);

printProgramInfo();
console.log('當(dāng)前時(shí)間', datetime.getCurrentTime());
const spinner = ora('正在加載中万皿,請(qǐng)稍后 ...').start();

這次摧找,我們?cè)俅芜\(yùn)行 timer.js:

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

轉(zhuǎn)起來了蹬耘!

嘗鮮 npm scripts

在本教程的最后一節(jié)中,我們將簡單地介紹一下 npm scripts减余,也就是 npm 腳本综苔。之前在 package.json 中提到,有個(gè)字段叫 scripts位岔,這個(gè)字段就定義了全部的 npm scripts如筛。我們發(fā)現(xiàn)在用 npm init 時(shí)創(chuàng)建的 package.json 文件默認(rèn)就添加了一個(gè) test 腳本:

"test": "echo \"Error: no test specified\" && exit 1"

那一串命令就是 test 腳本將要執(zhí)行的內(nèi)容,我們可以通過 npm test 命令執(zhí)行該腳本:

$ npm test

> timer@1.0.0 test /Users/mRc/Tutorials/nodejs-quickstart
> echo "Error: no test specified" && exit 1

Error: no test specified
npm ERR! Test failed.  See above for more details.

在初步體驗(yàn)了 npm scripts 之后抒抬,我們有必要了解一下 npm scripts 分為兩大類:

  • 預(yù)定義腳本:例如 test杨刨、startinstall擦剑、publish 等等妖胀,直接通過 npm <scriptName> 運(yùn)行,例如 npm test惠勒,所有預(yù)定義的腳本可查看文檔
  • 自定義腳本:除了以上自帶腳本的其他腳本赚抡,需要通過 npm run <scriptName> 運(yùn)行,例如 npm run custom

現(xiàn)在就讓我們開始為 timer 項(xiàng)目添加兩個(gè) npm scripts纠屋,分別是 startlint涂臣。第一個(gè)是預(yù)定義的,用于啟動(dòng)我們的 timer.js巾遭;第二個(gè)是靜態(tài)代碼檢查肉康,用于在開發(fā)時(shí)檢查我們的代碼。首先安裝 ESLint npm 包:

$ npm install eslint --save-dev
$ # 或者
$ npm install eslint -D

注意到我們加了一個(gè) -D--save-dev 選項(xiàng)灼舍,代表 eslint 是一個(gè)開發(fā)依賴,在實(shí)際項(xiàng)目發(fā)布或部署時(shí)不需要用到涨薪。npm 會(huì)把所有開發(fā)依賴添加到 devDependencies 字段中骑素。然后分別添加 startlint 腳本,代碼如下:

{
  "name": "timer",
  "version": "1.0.0",
  "description": "A cool timer",
  "main": "timer.js",
  "scripts": {
    "lint": "eslint **/*.js",
    "start": "node timer.js -m '上手了' -t 3",
    "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"
  },
  "devDependencies": {
    "eslint": "^6.7.2"
  }
}

ESLint 的使用需要一個(gè)配置文件刚夺,創(chuàng)建 .eslintrc.js 文件(注意最前面有一個(gè)點(diǎn))献丑,代碼如下:

module.exports = {
    "env": {
        "es6": true,
        "node": true,
    },
    "extends": "eslint:recommended",
};

運(yùn)行 npm start末捣,可以看到成功地運(yùn)行了我們的 timer.js 腳本;而運(yùn)行 npm run lint创橄,沒有輸出任何結(jié)果(代表靜態(tài)檢查通過)箩做。

npm scripts 看上去平淡無奇,但是卻能為項(xiàng)目開發(fā)提供非常便利的工作流妥畏。例如邦邦,之前構(gòu)建一個(gè)項(xiàng)目需要非常復(fù)雜的命令,但是如果你實(shí)現(xiàn)了一個(gè) build npm 腳本醉蚁,那么當(dāng)你的同事拿到這份代碼時(shí)燃辖,只需簡單地執(zhí)行 npm run build 就可以開始構(gòu)建,而無需關(guān)心背后的技術(shù)細(xì)節(jié)网棍。在后續(xù)的 Node.js 或是前端學(xué)習(xí)中,我們會(huì)在實(shí)際項(xiàng)目中使用各種 npm scripts 來定義我們的工作流,大家慢慢就會(huì)領(lǐng)會(huì)到它的強(qiáng)大了睁壁。

下次再見:監(jiān)聽 exit 事件

在這篇教程的最后一節(jié)中寒屯,我們將讓你簡單地感受 Node 的事件機(jī)制。Node 的事件機(jī)制是比較復(fù)雜的惑畴,足夠講半本書蛋欣,但這篇教程希望能通過一個(gè)非常簡單的實(shí)例,讓你對(duì) Node 事件有個(gè)初步的了解桨菜。

提示

如果你有過在網(wǎng)頁(或其他用戶界面)開發(fā)中編寫事件處理(例如鼠標(biāo)點(diǎn)擊)的經(jīng)驗(yàn)豁状,那么你一定會(huì)覺得 Node 中處理事件的方式似曾相識(shí)而又符合直覺。

我們?cè)谇懊婧唵蔚靥崃艘幌禄卣{(diào)函數(shù)倒得。實(shí)際上泻红,回調(diào)函數(shù)和事件機(jī)制共同組成了 Node 的異步世界。具體而言霞掺,Node 中的事件都是通過 events 核心模塊中的 EventEmitter 這個(gè)類實(shí)現(xiàn)的谊路。EventEmitter 包括兩個(gè)最關(guān)鍵的方法:

  • on:用來監(jiān)聽事件的發(fā)生
  • emit:用來觸發(fā)新的事件

請(qǐng)看下面這個(gè)代碼片段:

const EventEmitter = require('events').EventEmitter;
const emitter = new EventEmitter();

// 監(jiān)聽 connect 事件,注冊(cè)回調(diào)函數(shù)
emitter.on('connect', function (username) {
  console.log(username + '已連接');
});

// 觸發(fā) connect 事件菩彬,并且加上一個(gè)參數(shù)(即上面的 username)
emitter.emit('connect', '一只圖雀');

運(yùn)行上面的代碼缠劝,就會(huì)輸出以下內(nèi)容:

一只圖雀已連接

可以說,Node 中很多對(duì)象都繼承自 EventEmitter骗灶,包括我們熟悉的 process 全局對(duì)象惨恭。在之前的 timer.js 腳本中,我們監(jiān)聽 exit 事件(即 Node 進(jìn)程結(jié)束)耙旦,并添加一個(gè)自定義的回調(diào)函數(shù)打印“下次再見”的信息:

const program = require('commander');
const ora = require('ora');
const printProgramInfo = require('./info');
const datetime = require('./datetime');

program
  .option('-t, --time <number>', '等待時(shí)間 (秒)', 3)
  .option('-m, --message <string>', '要輸出的信息', 'Hello World')
  .parse(process.argv);

setTimeout(() => {
  spinner.stop();
  console.log(program.message);
}, program.time * 1000);

process.on('exit', () => {
  console.log('下次再見~');
});

printProgramInfo();
console.log('當(dāng)前時(shí)間', datetime.getCurrentTime());
const spinner = ora('正在加載中脱羡,請(qǐng)稍后 ...').start();

運(yùn)行后,會(huì)在程序退出后打印“下次再見~”的字符串。你可能會(huì)問锉罐,為啥不能在 setTimeout 的回調(diào)函數(shù)中添加程序退出的邏輯呢帆竹?因?yàn)槌苏_\(yùn)行結(jié)束(也就是等待了指定的時(shí)間),我們的程序很有可能會(huì)因?yàn)槠渌蛲顺觯ɡ鐠伋霎惓EЧ妫蛘哂?process.exit 強(qiáng)制退出)栽连,這時(shí)候通過監(jiān)聽 exit 事件,就可以在確保所有情況下都能執(zhí)行 exit 事件的回調(diào)函數(shù)侨舆。如果你覺得還是不能理解的話秒紧,可以看下面這張示意圖:

提示

process 對(duì)象還支持其他常用的事件,例如 SIGINT(用戶按 Ctrl+C 時(shí)觸發(fā))等等态罪,可參考這篇文檔噩茄。

這篇 Node.js 快速入門教程到這里就結(jié)束了,希望能夠成為你進(jìn)一步探索 Node.js 或是前端開發(fā)的基石复颈。exit 事件已經(jīng)觸發(fā)绩聘,那我們也下次再見啦~

想要學(xué)習(xí)更多精彩的實(shí)戰(zhàn)技術(shù)教程?來圖雀社區(qū)逛逛吧耗啦。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末凿菩,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子帜讲,更是在濱河造成了極大的恐慌衅谷,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,651評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件似将,死亡現(xiàn)場(chǎng)離奇詭異获黔,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)在验,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門玷氏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人腋舌,你說我怎么就攤上這事盏触。” “怎么了块饺?”我有些...
    開封第一講書人閱讀 162,931評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵赞辩,是天一觀的道長。 經(jīng)常有香客問我授艰,道長辨嗽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,218評(píng)論 1 292
  • 正文 為了忘掉前任淮腾,我火速辦了婚禮召庞,結(jié)果婚禮上岛心,老公的妹妹穿的比我還像新娘来破。我一直安慰自己篮灼,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評(píng)論 6 388
  • 文/花漫 我一把揭開白布徘禁。 她就那樣靜靜地躺著诅诱,像睡著了一般。 火紅的嫁衣襯著肌膚如雪送朱。 梳的紋絲不亂的頭發(fā)上娘荡,一...
    開封第一講書人閱讀 51,198評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音驶沼,去河邊找鬼炮沐。 笑死,一個(gè)胖子當(dāng)著我的面吹牛回怜,可吹牛的內(nèi)容都是我干的大年。 我是一名探鬼主播,決...
    沈念sama閱讀 40,084評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼玉雾,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼翔试!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起复旬,我...
    開封第一講書人閱讀 38,926評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤垦缅,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后驹碍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體壁涎,經(jīng)...
    沈念sama閱讀 45,341評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評(píng)論 2 333
  • 正文 我和宋清朗相戀三年志秃,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了怔球。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,731評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡洽损,死狀恐怖庞溜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情碑定,我是刑警寧澤流码,帶...
    沈念sama閱讀 35,430評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站延刘,受9級(jí)特大地震影響漫试,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜碘赖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評(píng)論 3 326
  • 文/蒙蒙 一驾荣、第九天 我趴在偏房一處隱蔽的房頂上張望外构。 院中可真熱鬧,春花似錦播掷、人聲如沸审编。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽垒酬。三九已至,卻和暖如春件炉,著一層夾襖步出監(jiān)牢的瞬間勘究,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評(píng)論 1 269
  • 我被黑心中介騙來泰國打工斟冕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留口糕,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,743評(píng)論 2 368
  • 正文 我出身青樓磕蛇,卻偏偏與公主長得像景描,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子孤里,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容