二. 模塊化
Node.js所有的API都是基于模塊發(fā)布和使用的,因此在真正的學(xué)習(xí)Node.js之前,我們需要先了解模塊化開發(fā)。
2.1 模塊化的由來(lái)
JavaScript誕生初期,不像成熟的面向?qū)ο笳Z(yǔ)言(如Java)擁有與生俱來(lái)的模塊系統(tǒng),甚至沒(méi)有人把它當(dāng)做一門真正的編程語(yǔ)言。
JavaScript初期引入模塊的方式都是通過(guò)script標(biāo)簽來(lái)引入代碼,但這樣很容易產(chǎn)生如下問(wèn)題:
全局的變量污染,并且無(wú)法找到源頭。
過(guò)多的script標(biāo)簽會(huì)造成雜亂無(wú)章的代碼。
使用某個(gè)庫(kù),卻爆出沒(méi)有相關(guān)庫(kù)的依賴。
無(wú)法追蹤的錯(cuò)誤和報(bào)錯(cuò)位置。
為了解決JavaScript模塊化問(wèn)題,程序員們一開始采用「命令空間」的方式來(lái)約束代碼,讓看起來(lái)凌亂的編程現(xiàn)狀有所緩解。但是這僅僅屬于一種約定,并沒(méi)有從技術(shù)上解決根本問(wèn)題。
2.2 CommonJS規(guī)范
Node.js首先采用了CommonJS規(guī)范,CommonJS并非一門新的API,也不是標(biāo)準(zhǔn)模塊系統(tǒng),只不過(guò)是目前Node.js使用最廣泛的模塊系統(tǒng)(規(guī)范)。CommonJS 規(guī)范是為了解決 JavaScript 的作用域問(wèn)題而定義的模塊形式,可以使每個(gè)模塊它自身的命名空間中執(zhí)行。
2.2.1 CommonJs 基本使用規(guī)則
該規(guī)范的主要內(nèi)容是:
每個(gè)js文件就是一個(gè)模塊,模塊有自己的作用域。在一個(gè)文件里面定義的變量、函數(shù)、類,都是私有的,對(duì)其他文件不可見。
每個(gè)模塊內(nèi)部,module變量(默認(rèn)擁有)代表當(dāng)前模塊。這個(gè)變量是一個(gè)對(duì)象,它的exports屬性(即module.exports)是對(duì)外的接口。加載某個(gè)模塊,其實(shí)是加載該模塊的module.exports屬性。
模塊必須通過(guò) module.exports 導(dǎo)出對(duì)外的變量或接口。
// moduleA.js
varx=5;
varaddX=function(value) {
returnvalue+x;
};
module.exports.x=x;
module.exports.addX=addX;
通過(guò) require() 來(lái)導(dǎo)入其他模塊的輸出到當(dāng)前模塊作用域中。
// moduleB.js
varexample=require('./moduleA.js');
console.log(example.x);// 5
console.log(example.addX(1));// 6
CommonJS模塊的特點(diǎn)如下。
所有代碼都運(yùn)行在模塊作用域,不會(huì)污染全局作用域。
模塊可以多次加載,但是只會(huì)在第一次加載時(shí)運(yùn)行一次,然后運(yùn)行結(jié)果就被緩存了,以后再加載,就直接讀取緩存結(jié)果。要想讓模塊再次運(yùn)行,必須清除緩存。
模塊加載的順序,按照其在代碼中出現(xiàn)的順序。
2.2.2 內(nèi)部實(shí)現(xiàn)原理
正如上面所說(shuō),node執(zhí)行js文件時(shí)會(huì)默認(rèn)把文件代碼封裝到一個(gè)module變量中,它是Module的一個(gè)實(shí)例。
functionModule(id,parent) {
this.id=id;
this.exports={};
this.parent=parent;
// ...
}
每個(gè)模塊內(nèi)部,都有一個(gè)module對(duì)象,代表當(dāng)前模塊。它有以下屬性:
module.id 模塊的識(shí)別符,通常是帶有絕對(duì)路徑的模塊文件名。
module.filename 模塊的文件名,帶有絕對(duì)路徑。
module.loaded 返回一個(gè)布爾值,表示模塊是否已經(jīng)完成加載。
module.parent 返回一個(gè)對(duì)象,表示調(diào)用該模塊的模塊。
module.children 返回一個(gè)數(shù)組,表示該模塊要用到的其他模塊。
module.exports 表示模塊對(duì)外輸出的值。
嘗試執(zhí)行以下代碼:
// example.js
varjquery=require('jquery');
exports.$=jquery;
console.log(module);
打印信息:
{id:'.',
exports: {'$': [Function] },
parent:null,
filename:'/path/to/example.js',
loaded:false,
children:
[ {id:'/path/to/node_modules/jquery/dist/jquery.js',
exports: [Function],
parent: [Circular],
filename:'/path/to/node_modules/jquery/dist/jquery.js',
loaded:true,
children: [],
paths: [Object] } ],
paths:
['/home/user/deleted/node_modules',
'/home/user/node_modules',
'/home/node_modules',
'/node_modules']
}
module.exports屬性表示當(dāng)前模塊對(duì)外輸出的接口,其他文件加載該模塊,實(shí)際上就是讀取module.exports變量。為了方便,Node為每個(gè)模塊提供一個(gè)exports變量,指向module.exports。這等同在每個(gè)模塊頭部,有一行這樣的命令。
varexports=module.exports;
可以直接使用exports,但注意,不能直接將exports變量指向一個(gè)值,因?yàn)檫@樣等于切斷了exports與module.exports的聯(lián)系:
exports.area=function(r) {
returnMath.PI*r*r;
};
exports.circumference=function(r) {
return2*Math.PI*r;
};
exports=function(x) {console.log(x)};//這樣做不行,因?yàn)橄喈?dāng)于在文件內(nèi)部定義一個(gè)私有變量了,通過(guò)require無(wú)法拿到
CommonJS加載是同步的,因此使用CommonJS無(wú)法做到按需加載。
2.3 ES6中的模塊化
ES6模塊是ECMA組織從語(yǔ)言層面提出的標(biāo)準(zhǔn)模塊化API,未來(lái)完全可以取代CommonJS和其他的模塊化系統(tǒng)。
2.3.1 基本使用方式
ES6模塊的語(yǔ)法完全不同于CommonJS,因?yàn)镋S6 模塊不是對(duì)象,而是通過(guò)export命令顯式指定導(dǎo)出的代碼,再通過(guò)import命令導(dǎo)入:
// person.js
exportdefaultconstperson={}
exportage=30
使用
importperson,{age}from'person'
2.3.2 模塊的加載原理
import 方式導(dǎo)入模塊是基于“編譯時(shí)加載”或者靜態(tài)加載,即 ES6 可以在編譯時(shí)就完成模塊加載,效率要比 CommonJS 模塊的加載方式高。
它具有如下優(yōu)勢(shì):
基于編譯時(shí)加載,可以讓模塊在編譯階段就能進(jìn)行靜態(tài)語(yǔ)法分析,從而提前預(yù)警語(yǔ)法錯(cuò)誤或者做類型校驗(yàn)。
未來(lái)Node.js和瀏覽器都將支持,可以一統(tǒng)天下。
ES6模塊默認(rèn)在嚴(yán)格模式下執(zhí)行,即默認(rèn)在模塊頭部加入:"use strict"。
## 附:嚴(yán)格模式主要有以下限制。
- 變量必須聲明后再使用
- 函數(shù)的參數(shù)不能有同名屬性,否則報(bào)錯(cuò)
- 不能使用`with`語(yǔ)句
- 不能對(duì)只讀屬性賦值,否則報(bào)錯(cuò)
- 不能使用前綴 0 表示八進(jìn)制數(shù),否則報(bào)錯(cuò)
- 不能刪除不可刪除的屬性,否則報(bào)錯(cuò)
- 不能刪除變量`delete prop`,會(huì)報(bào)錯(cuò),只能刪除屬性`delete global[prop]`
- `eval`不會(huì)在它的外層作用域引入變量
- `eval`和`arguments`不能被重新賦值
- `arguments`不會(huì)自動(dòng)反映函數(shù)參數(shù)的變化
- 不能使用`arguments.callee`
- 不能使用`arguments.caller`
- 禁止`this`指向全局對(duì)象
- 不能使用`fn.caller`和`fn.arguments`獲取函數(shù)調(diào)用的堆棧
- 增加了保留字(比如`protected`、`static`和`interface`)
尤其注意:頂層的this指向undefined,不應(yīng)該在頂層代碼使用this。
2.3.3 詳解import和export命令
模塊功能主要由兩個(gè)命令構(gòu)成:export和import。
export命令用于規(guī)定模塊的對(duì)外接口。
一個(gè)模塊就是一個(gè)獨(dú)立的文件。該文件內(nèi)部的所有變量,外部無(wú)法獲取,除非通過(guò)export導(dǎo)出:
// profile.js
exportvarfirstName='Michael';
exportvarlastName='Jackson';
exportvaryear=1958;
或者寫成:
// profile.js
varfirstName='Michael';
varlastName='Jackson';
varyear=1958;
?
export{firstName,lastName,yearasage};
export 中的as關(guān)鍵字可以重命名導(dǎo)出的變量名。
export無(wú)法直接導(dǎo)出值或變量:
// 報(bào)錯(cuò)
export1;
// 報(bào)錯(cuò)
varm=1;
exportm;
?
// 下面是正確的寫法
?
// 寫法一
exportvarm=1;
// 寫法二
varm=1;
export{m};
// 寫法三
varn=1;
export{nasm};
export必須在頂層作用域使用,無(wú)法在其他塊級(jí)作用域使用:
functionfoo() {
exportdefault'bar'// 語(yǔ)法錯(cuò)誤
}
foo()
export default 命令可以導(dǎo)出默認(rèn)的變量,import使用時(shí)可以指定任意名字
exportdefaultfunctionabc(){}
或者
functionabc(){}
export{abcasdefault}
使用
importcustomNamefrom'./export-default.js'
或者
import{defaultasabc}from'./export-default.js'
import`命令用于輸入其他模塊提供的功能。
export導(dǎo)出的接口都通過(guò)import來(lái)導(dǎo)入,import中也可以通過(guò)as對(duì)導(dǎo)入的變量進(jìn)行重命名。
import{lastNameassurname}from'./profile.js';
import導(dǎo)入的變量都是只讀的,無(wú)法修改。
import{a}from'./xxx.js'
?
a={};// Syntax Error : 'a' is read-only;
import 會(huì)導(dǎo)致導(dǎo)入的模塊自動(dòng)加載執(zhí)行,并且多次重復(fù)執(zhí)行同一句import語(yǔ)句,只會(huì)執(zhí)行一次:
import'lodash';
import'lodash';//這句就不執(zhí)行了
模塊整體加載可以使用*號(hào):
import*aspersonfromperson
import()方法的動(dòng)態(tài)加載
import命令是基于編譯時(shí)加載,如果想使用運(yùn)行時(shí)加載該怎么辦?
可以使用全局的import()方法,注意:該方法是ES6提案中引入的方法,和import命令完全不是一碼事。
import()方法支持傳入一個(gè)模塊路徑,返回一個(gè)Promise,在then方法中可以拿到模塊的引用。
import('./dialogBox.js')
.then(dialogBox=>{
dialogBox.open();
? })
.catch(error=>{
/* Error handling */
? })
2.4 其他模塊化解決方案
除了CommonJS被Node.js定為官方模塊化標(biāo)準(zhǔn),以及ES6 Module被ECMA組織定為瀏覽器支持的原生標(biāo)準(zhǔn)外,在模塊化探索年代還產(chǎn)生過(guò)一些其他的模塊化規(guī)范,不過(guò)隨著時(shí)間的流逝,這些都被人們廢棄了。
2.4.1 AMD規(guī)范
require.js實(shí)現(xiàn)了AMD規(guī)范的
在ES6模塊化方案提出之前,瀏覽器實(shí)現(xiàn)異步加載模塊,都是基于AMD規(guī)范來(lái)實(shí)現(xiàn),其寫法如下:
// lib模塊
define(['package/lib'],function(lib){
functionhello(){
lib.log('hello world!');
? }
?
return{
foo:foo
? };
});
使用模塊:
require(['lib'],function(lib) {
lib.foo()
});
AMD規(guī)范必須一次性把所有依賴加載完畢,也無(wú)法做到按需加載,但可以做到異步加載。
2.4.2 CMD規(guī)范
sea.js實(shí)現(xiàn)了CMD規(guī)范
CMD規(guī)范是AMD規(guī)范的一個(gè)變種,為了讓AMD規(guī)范兼容CommonJS的風(fēng)格。
define(function(require,exports,module) {
var$=require('jquery');
require.async(['./b','./c'],function(b) {
b.doSomething();
c.doSomething();
? });
exports.doSomething=...
module.exports=...
})
使用模塊也是按照如上的方式使用。
CMD相比AMD可以做到異步加載,但目前也幾乎每人使用,因?yàn)闀鴮懱闊?,而且ES6已經(jīng)把模塊規(guī)范化。
三. Node.js常用的內(nèi)置模塊
Node.js中提供了一些原生的模塊,我們稱之為內(nèi)置模塊。此外,也可以通過(guò)NPM命令安裝第三方模塊,安裝完畢后使用方式和內(nèi)置模塊沒(méi)有什么不同。
Node.js中的原生模塊直接通過(guò)require就能獲得,參考Node.js API文檔來(lái)使用這些原生模塊。接下來(lái),針對(duì)一些常用模塊進(jìn)行介紹。
3.1 fs模塊
fs 模塊以 POSIX 標(biāo)準(zhǔn)函數(shù)的方式提供了與文件系統(tǒng)進(jìn)行交互的API,fs中的每個(gè)方法都分同步和異步兩種方式調(diào)用。通過(guò)fs模塊可以獲取一個(gè)目錄結(jié)構(gòu),及其子目錄或目錄樹下文件的特征,甚至可以讀寫、復(fù)制或者刪除文件。
什么是POSIX標(biāo)準(zhǔn)
可移植操作系統(tǒng)接口(Portable Operating System Interface of UNIX,縮寫為 POSIX ),POSIX標(biāo)準(zhǔn)定義了操作系統(tǒng)應(yīng)該為應(yīng)用程序提供的接口標(biāo)準(zhǔn),保證了接口的可移植。
fs模塊中操作文件或文件夾的方式可以基于:同步、異步回調(diào)或者異步Promise三種模式。還記得Promise嗎?
文件夾讀取示例三種方式
constfs=require('fs')
// 使用同步的方式
constdirent=fs.readdirSync(__dirname)
console.log(dirent)
// 使用異步回調(diào)的方式
fs.readdir(__dirname, (err,dirent)=>{
if(err) {
console.error(err)
}else{
console.log(dirent)
?? }
})
// 使用Promise的方式,目前還不穩(wěn)定
fs.promises.readdir(__dirname).then((dirent)=>{
console.log(dirent)
}).catch((err)=>{
console.error(err)
})
和文件系統(tǒng)有關(guān)的全局變量
__dirname是Node.js提供的全局變量,表示當(dāng)前正在執(zhí)行的代碼文件所在的目錄絕對(duì)路徑。
__filename表示當(dāng)前正在執(zhí)行的代碼文件的絕對(duì)路徑。
注意以上變量和執(zhí)行環(huán)境是無(wú)關(guān)的,只和代碼文件的保存位置有關(guān)。
在不同路徑的js文件中加入下面代碼,通過(guò)node命令執(zhí)行看看
console.log(__dirname)
console.log(__filename)
3.1.1 文件夾的操作
fs.access():檢查文件或文件夾是否存在。
fs.readdir():讀取文件夾。
fs.mkdir():創(chuàng)建文件夾,如果文件夾已存在會(huì)導(dǎo)致錯(cuò)誤。
fs.rmdir():刪除文件夾,如果文件夾內(nèi)部有子文件會(huì)導(dǎo)致錯(cuò)誤。
fs.rename():修改文件或文件夾名字。
constfs=require('fs')
// 以u(píng)tf-8格式讀取目錄的子文件名字
fs.readdir('/Users', {encoding:'utf-8'}, (err,files)=>{
if(err) {
console.error(err)
}else{
console.log(files)
?? }
})
// 檢查文件是否存在
fs.access(__dirname+'/temp',fs.constants.F_OK, (err)=>{
if(err) {
// 報(bào)錯(cuò)表示不存在
// 創(chuàng)建文件夾
fs.mkdir(__dirname+'/temp', (err)=>{
if(err) {
console.error(err)
}else{
setTimeout(()=>{
// 2秒之后修改文件夾名為temp2
fs.rename(__dirname+'/temp',__dirname+'/temp2', (err)=>{
if(err)console.error(err)
? ? ? ? ? ? ? ? ?? })
},2000);
? ? ? ? ?? }
?
? ? ?? })
}else{
// 刪除文件夾
fs.rmdir(__dirname+'/temp', (err)=>{
if(err) {
console.error(err)
? ? ? ? ?? }
? ? ?? })
?? }
})
3.1.2 文件的操作
fs.writeFile():讀取或者創(chuàng)建文件。
fs.unlink():刪除文件及其文件鏈接(快捷方式)。
constfs=require('fs')
// 新建文件
fs.writeFile(__dirname+'/text.txt','', (err)=>{
if(err)throwerr
setTimeout(()=>{
// 刪除文件
fs.unlink(__dirname+'/text.txt', (err)=>{
if(err)throwerr
? ? ?? })
},3000);
})
3.1.3 文件屬性的獲取
通過(guò)fs.lstat()方法可以讀取文件屬性,文件屬性保存在一個(gè)fs.Stats對(duì)象里面。
constfs=require('fs')
// 獲取文件屬性
fs.lstat(__filename, (err,stats)=>{
if(err)throwerr
if(stats.isFile) {
console.log(`${__filename}是文件,創(chuàng)建于${stats.birthtime},大小${stats.size}`)
?? }
})
3.1.4 大文件的讀寫
對(duì)于比較大型的文件(如視頻文件),拷貝文件或讀取文件是分塊的以流的方式來(lái)讀取的,因?yàn)橹苯幼x取整個(gè)文件會(huì)很容易超過(guò)內(nèi)存容量。
const stream = fs.createReadStream('c:\\demo\1.txt');
let data = ''
stream.on('data', (trunk) => {
? data += trunk;
});
stream.on('end', () => {
? console.log(data);
});
文件流的讀寫比較復(fù)雜,推薦直接使用第三方庫(kù):cp-file
3.2 path模塊
path模塊也是Node.js中經(jīng)常使用的模塊,主要用于路徑的處理,由于Windows和Unix路徑的格式有差異,因此在不同平臺(tái)上調(diào)用的方法可能得到只針對(duì)平臺(tái)的路徑。盡量不要使用和平臺(tái)相關(guān)的path API。
path.dirname():返回所在目錄名。
path.dirname('/foo/bar/baz/asdf/quux');
// 返回: '/foo/bar/baz/asdf'
path.extname:返回?cái)U(kuò)展名。
path.extname('index.html');
// 返回: '.html'
path.join:可以使用平臺(tái)特定的分隔符作為定界符將所有給定的 path 片段連接在一起,然后規(guī)范化生成的路徑。
path.join('/foo','bar','baz/asdf','quux','..');
// 返回: '/foo/bar/baz/asdf'
path.normalize() :方法規(guī)范化給定的 path,解析 '..' 和 '.' 片段。
path.normalize('C:\\temp\\\\foo\\bar\\..\\');
// 返回: 'C:\\temp\\foo\\'
path.resolve() 方法將路徑或路徑片段的序列解析為絕對(duì)路徑。
規(guī)則:
給定的路徑序列從右到左進(jìn)行處理,每個(gè)后續(xù)的 path 前置,直到構(gòu)造出一個(gè)絕對(duì)路徑。
如果在處理完所有給定的 path 片段之后還未生成絕對(duì)路徑,則再加上當(dāng)前工作目錄。
生成的路徑已規(guī)范化,并且除非將路徑解析為根目錄,否則將刪除尾部斜杠。
constpath=require('path')
console.log(path.resolve('/foo','/bar','./baz'))
// '/bar/baz'
console.log(path.resolve('/foo/bar','./baz'))
// '/foo/bar/baz'
console.log(path.resolve('foo/bar','./baz/'))
// '/<當(dāng)前執(zhí)行路徑>/foo/bar/baz'
3.4 url模塊
url模塊主要處理URL(統(tǒng)一資源定位符),URL的格式構(gòu)成:
協(xié)議+認(rèn)證+主機(jī)+端口+路徑+查詢字符串+哈希值。
可以參照下圖:
url模塊主要使用以下兩個(gè)API接口:
url.format():負(fù)責(zé)把一個(gè)url的json對(duì)象轉(zhuǎn)換成合法的url地址。
url.parse():負(fù)責(zé)把一個(gè)url地址轉(zhuǎn)換成一個(gè)url的json對(duì)象。
consturl=require('url')
constpath=url.format({
protocol:'https',
hostname:'example.com',
pathname:'/some/path',
query: {
page:1,
format:'json'
?? }
});
console.log(path)
// => 'https://example.com/some/path?page=1&format=json'
console.log(url.parse(path))
// Url {
// ? protocol: 'https:',
// ? slashes: true,
// ? auth: null,
// ? host: 'example.com',
// ? port: null,
// ? hostname: 'example.com',
// ? hash: null,
// ? search: '?page=1&format=json',
// ? query: 'page=1&format=json',
// ? pathname: '/some/path',
// ? path: '/some/path?page=1&format=json',
// ? href: 'https://example.com/some/path?page=1&format=json' }
3.5 querystring模塊
通過(guò)url.parse轉(zhuǎn)換的url對(duì)象中的query對(duì)象是一個(gè)字符串,如果想進(jìn)一步拿到查詢字符串的鍵值對(duì),需要再通過(guò)querystring來(lái)轉(zhuǎn)換。
querystring.stringify():可以把查詢字符串對(duì)象轉(zhuǎn)換成字符串。querystring.stringify默認(rèn)會(huì)對(duì)非ASCII字符進(jìn)行百分比編碼,即內(nèi)部調(diào)用querystring.escape() 方法。
querystring.parse():可以把查詢字符串轉(zhuǎn)換成對(duì)象。querystring.parse默認(rèn)會(huì)對(duì)經(jīng)過(guò)百分比編碼的字符進(jìn)行解碼操作,即內(nèi)部調(diào)用了querystring.unescape() 。
constqs=require('querystring')
conststr=qs.stringify({
name:'小明',
age:30,
description: ['動(dòng)物','人']
})
console.log(str)
// name=%E5%B0%8F%E6%98%8E&age=30&description=%E5%8A%A8%E7%89%A9&description=%E4%BA%BA
constobj=qs.parse(str)
console.log(obj)
// [Object: null prototype] { name: '小明', age: '30', description: ['動(dòng)物', '人'] }
3.6 http模塊
http/https模塊是Node.js中和網(wǎng)絡(luò)請(qǐng)求相關(guān)的核心模塊,類似瀏覽器中的ajax,但是它除了可以請(qǐng)求數(shù)據(jù),還可以通過(guò)它搭建HTTP(S)服務(wù)。
接下來(lái),我們使用http模塊模擬一個(gè)爬蟲案例。
什么是爬蟲?
對(duì)于一個(gè) 害怕蟲子的開發(fā)者來(lái)說(shuō),估計(jì)選擇爬蟲行業(yè)可以是一個(gè)磨煉意志的機(jī)會(huì)。
爬蟲就是一段自動(dòng)抓取互聯(lián)網(wǎng)信息的程序,從互聯(lián)網(wǎng)上抓取對(duì)于我們有價(jià)值的信息。
事實(shí)上,搜索引擎就是一個(gè)巨型的爬蟲,它可以輔助我們完成信息的檢索和歸類,讓我們以最快的速度找到我們最想要的信息。
接下來(lái),我們只是簡(jiǎn)單地使用https模塊和第三方cheer.io模塊來(lái)爬去某一個(gè)網(wǎng)站的有用信息。
因?yàn)槟壳按蟛糠志W(wǎng)站都是基于https協(xié)議的,所以就是用https模塊,它和http協(xié)議最大的區(qū)別就是https會(huì)對(duì)請(qǐng)求和資源加密解密。
編寫http代碼,實(shí)現(xiàn)資源獲取。
varhttp=require('http')
varhttps=require('https')
functiongetUrlResource(url,callback) {
varclient
if(url.startsWith('https')) {
client=https
}elseif(url.startsWith('http')) {
client=http
}else{
callback(newError('只能請(qǐng)求http/https協(xié)議的內(nèi)容'))
return;
?? }
client.get(url, (res)=>{
const{statusCode}=res;
constcontentType=res.headers['content-type'];
?
leterror;
if(statusCode!==200) {
error=newError('請(qǐng)求失敗\n'+
`狀態(tài)碼: ${statusCode}`);
}elseif(!/^text\/html/.test(contentType)) {
error=newError('無(wú)效的 content-type.\n'+
`期望的是 text/html 但接收到的是 ${contentType}`);
? ? ?? }
if(error) {
// 消費(fèi)響應(yīng)數(shù)據(jù)來(lái)釋放內(nèi)存。
res.resume();
callback(error)
return;
? ? ?? }
?
res.setEncoding('utf8');
letrawData='';
res.on('data', (chunk)=>{rawData+=chunk; });
res.on('end', ()=>{
callback(null,rawData)
? ? ?? });
}).on('error', (e)=>{
callback(e)
?? });
}
編寫爬蟲代碼
varcheerio=require('cheerio')
getUrlResource('https://www.baidu.com/',function(e,data) {
if(e) {
console.error(e)
return
?? }
?
var$=cheerio.load(data)
var$imgs=$('img[src]')
if($imgs.length===0) {
console.log('沒(méi)有找到圖片資源')
return
?? }
// 注意執(zhí)行g(shù)et才能拿到數(shù)組
varimgSrcValues=$imgs.map(function(i,el) {
return$(el).attr('src');
}).get()
varimgUrls=imgSrcValues.join(',\n');
console.log(imgUrls)
})
四. Express框架
Express 是Node.js開發(fā)中使用最廣泛的服務(wù)器框架,通過(guò)它可以快速的搭建一個(gè)Web容器并向外界提供Web服務(wù)。
Express基于Node.js的http/https搭建服務(wù)器,通過(guò)中間件的即插即用方式來(lái)擴(kuò)展功能。
4.1 基本使用
創(chuàng)建一個(gè)項(xiàng)目文件夾,命名為express-demo并通過(guò)cd命令進(jìn)入到express-demo目錄。
使用npm init -y初始化項(xiàng)目,生成package.json項(xiàng)目描述文件。
使用npm install -save express安裝express。
創(chuàng)建一個(gè)index.js文件,寫入以下代碼訪問(wèn)瀏覽器即可查看效果。
constexpress=require('express')
constapp=express()
app.get('/', (req,res)=>res.send('你好!'))
app.listen(3000, ()=>{
console.log('服務(wù)器創(chuàng)建完畢,監(jiān)聽3000端口')
})
打開終端,在express-demo目錄下執(zhí)行:
nodeindex.js。
此外,如果希望不在console中調(diào)試,而是在Chrome中通過(guò)dev tools調(diào)試,可以使用:
node--inspectindex.js
然后打開開發(fā)者工具,里面有個(gè)Node圖標(biāo),點(diǎn)擊即可調(diào)試。
假如你希望每一次修改代碼都自動(dòng)重啟Express,可以全局安裝nodemon,它和node的命令幾乎一樣。
npm install -g nodemon
nodemon --inspect index.js
打開瀏覽器,輸入:http://本機(jī)ip/ 查看效果。
為了方便自動(dòng)的檢測(cè)本機(jī)ip和自動(dòng)打開瀏覽器測(cè)試,可以安裝兩個(gè)第三方npm包:open和address,portFinder。然后服務(wù)器啟動(dòng)成功之后可以自動(dòng)打開瀏覽器測(cè)試。
npminstall-saveopen address portfinder
constexpress=require('express')
consturl=require('url')
constopen=require('open')
constaddress=require('address')
constportfinder=require('portfinder')
constapp=express()
app.get('/', (req,res)=>res.send('你好!'))
// 防止3000端口被占用
portfinder.getPort({
port:3000,// minimum port
stopPort:3333// maximum port
},function(err,port){
if(err){
thrownewError('沒(méi)有找到可以啟動(dòng)的端口')
? }
app.listen(port, ()=>{
console.log('服務(wù)器創(chuàng)建完畢,監(jiān)聽'+port+'端口')
? // 自動(dòng)打開Chrome瀏覽器加載網(wǎng)頁(yè)
open(url.format({
protocol:'http',
port:port,
hostname:address.ip(),pathname:'/'
}), {app: ['google chrome','--incognito'] })
? });
});
可以把node index.js命令加入到package.json的scripts字段里面,通過(guò)npm start啟動(dòng)。
{
"name":"express-demo",
"main":"index.js",
"scripts": {
"start":"node index.js"
? },
"dependencies": {
"address":"^1.1.2",
"express":"^4.17.1",
"open":"^7.0.0"
? }
}
npmstart