常用模組
常用模組有以下幾個:
fs-extra
使用 async/await 的前提是必須將介面封裝成 promise, 看一個簡單的例子:
const sleep = (milliseconds) => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(), milliseconds)
})
}
const main = async () => {
await sleep(5000);
console.log('5秒後...');
}
main();
複製程式碼
通過在 async 函式中使用 await + promise 的方式來組織非同步程式碼就像是同步程式碼一般,非常的自然和有助於我們分析程式碼的執行流程。
在 node 中, fs 模組是一個很常用的操作檔案的 native 模組,fs (file system) 模組提供了和檔案系統相關的一些同步和非同步的 api, 有時候使用同步 api 非常有必要,比如你要在一個自己寫的模組中的在訪問檔案後匯出一些介面時,這個時候用同步的 api 就很實用。看個例子:
const path = require('path');
const fs = require('fs-extra');
const { log4js } = require('../../config/log4jsConfig');
const log = log4js.getLogger('qupingce');
const createModels = () => {
const models = {};
const fileNames = fs.readdirSync(path.resolve(__dirname, '.'));
fileNames
.filter(fileName => fileName !== 'index.js')
.map(fileName => fileName.slice(0, -3))
.forEach(modelName => {
log.info(`Sequelize define model ${modelName}!`);
models[modelName] = require(path.resolve(__dirname, `./${modelName}.js`));
})
return models;
}
module.exports = createModels();
複製程式碼
這個模組訪問了當前目錄下所有的 model 模組並匯出 models, 如果是使用非同步介面也就是 fs.readdir, 這樣的話你在別的模組匯入這個模組是獲取不到 models 的, 原因是 require 是同步操作,而介面是非同步的,同步程式碼無法立即獲得非同步操作的結果。
為了充分發揮 node 非同步的優勢,我們還是應該儘量使用非同步介面。
我們完全可以使用 fs-extra 模組來代替 fs 模組, 類似的模組還有 mz。fs-extra 包含了所有的 fs 模組的介面,還對每個非同步介面提供了promise 支援,更棒的是 fs-extra 還提供了一些其它的實用檔案操作函式, 比如刪除移動檔案的操作。更詳細的介紹請檢視官方倉庫 fs-extra。
superagent
superagent 是一個 node 的 http client, 可以類比 java 中的 httpclient 和 okhttp, python 中的 requests。可以讓我們模擬 http 請求。superagent 這個庫有很多實用的特點。
- superagent 會根據 response 的 content-type 自動序列化,通過 response.body 就可以獲取到序列化後的返回內容
- 這個庫會自動快取和傳送 cookies, 不需要我們手動管理 cookies
- 再來就是它的 api 是鏈式呼叫風格的,呼叫起來很爽,不過使用的時候要注意呼叫順序
- 它的非同步 api 都是返回 promise的。
非常方便有木有?。官方文件就是很長的一頁,目錄清晰,很容易就搜尋到你需要的內容。最後,superagent 還支援外掛整合,比如你需要在超時後自動重發,可以使用 superagent-retry。更多外掛可以去 npm 官網上搜尋關鍵字 superagent-
。更多詳情檢視官方文件superagent
// 官方文件的一個呼叫示例
request
.post('/api/pet')
.send({ name: 'Manny', species: 'cat' })
.set('X-API-Key', 'foobar')
.set('Accept', 'application/json')
.then(res => {
alert('yay got ' + JSON.stringify(res.body));
});
複製程式碼
cheerio
寫過爬蟲的人都知道, 我們經常會有解析 html 的需求, 從網頁原始碼中爬取資訊應該是最基礎的爬蟲手段了。python 中有 beautifulsoup, java 中有 jsoup, node 中有 cheerio。
cheerio 是為伺服器端設計的,給你近乎完整的 jquery 體驗。使用 cheerio 來解析 html 獲取元素,呼叫方式和 jquery 操作 dom 元素用法完全一致。而且還提供了一些方便的介面, 比如獲取 html, 看一個例子:
const cheerio = require('cheerio')
const $ = cheerio.load('<h2 class="title">Hello world</h2>')
$('h2.title').text('Hello there!')
$('h2').addClass('welcome')
$.html()
//=> <h2 class="title welcome">Hello there!</h2>
複製程式碼
官方倉庫: cheerio
log4js
log4j 是一個為 node 設計的日誌模組。 場景簡單情況下可以考慮使用 debug 模組。 log4js 比較符合我對日誌庫的需求,其實他倆定位也不一樣,debug 模組是為除錯而設計的,log4js 則是一個日誌庫,肯定得提供檔案輸出和分級等常規功能。
log4js 模組看名字有點向 java 中很有名的日誌庫 log4j 看齊的節奏。log4j 有以下特點:
- 可以自定義 appender(輸出目標),lo4js 甚至提供了輸出到郵件等目標的 appender
- 通過組合不同的 appender, 可以實現不同目的的 logger(日誌器)
- 提供了日誌分級功能,官方的 FAQ 中提到了如果要對 appender 實現級別過濾,可以使用 logLevelFilter
- 提供了滾動日誌和自定義輸出格式
下面通過我最近一個爬蟲專案的配置檔案來感受以下這個庫的特點:
const log4js = require('log4js');
const path = require('path');
const fs = require('fs-extra');
const infoFilePath = path.resolve(__dirname, '../out/log/info.log');
const errorFilePath = path.resolve(__dirname, '../out/log/error.log');
log4js.configure({
appenders: {
dateFile: {
type: 'dateFile',
filename: infoFilePath,
pattern: 'yyyy-MM-dd',
compress: false
},
errorDateFile: {
type: 'dateFile',
filename: errorFilePath,
pattern: 'yyyy-MM-dd',
compress: false,
},
justErrorsToFile: {
type: 'logLevelFilter',
appender: 'errorDateFile',
level: 'error'
},
out: {
type: 'console'
}
},
categories: {
default: {
appenders: ['out'],
level: 'trace'
},
qupingce: {
appenders: ['out', 'dateFile', 'justErrorsToFile'],
level: 'trace'
}
}
});
const clear = async () => {
const files = await fs.readdir(path.resolve(__dirname, '../out/log'));
for (const fileName of files) {
fs.remove(path.resolve(__dirname, `../out/log/${fileName}`));
}
}
module.exports = {
log4js,
clear
}
複製程式碼
sequelize
寫專案我們往往會有持久化的需求,簡單的場景可以使用 JSON 儲存資料,如果資料量比較大還要便於管理,那麼我們就要考慮用資料庫了。如果是操作 mysql 和 sqllite 建議使用 sequelize, 如果是 mongodb, 我更推薦用專門為 mongodb 設計的 mongoose
sequelize 有幾點我覺得還是有點不太好,比如預設生成 id
(primary key), createdAt
和 updatedAt
。
拋開一些自作主張的小毛病,sequelize 設計的還是很好的。內建的操作符,hooks, 還有 validators 很有意思。sequelize 還提供了 promise 和 typescript 支援。如果是使用 typescript 開發專案,還有另外一個很好的 orm 選擇 : typeorm。更多內容可以檢視官方文件: sequelize
chalk
chalk 中文意思是粉筆的意思,這個模組是 node 很有特色和實用的一個模組,它可以為你輸出的內容新增顏色, 下劃線, 背景色等裝飾。當我們寫專案的時候往往需要記錄一些步驟和事件,比如開啟資料庫連結,發出 http 請求等。我們可以適當使用 chalk 來突出某些內容,例如請求的 url 加上下劃線。
const logRequest = (response, isDetailed = false) => {
const URL = chalk.underline.yellow(response.request.url);
const basicInfo = `${response.request.method} Status: ${response.status} Content-Type: ${response.type} URL=${URL}`;
if (!isDetailed) {
logger.info(basicInfo);
} else {
const detailInfo = `${basicInfo}\ntext: ${response.text}`;
logger.info(detailInfo);
}
};
複製程式碼
呼叫上面的 logRequest效果:
更多內容檢視官方倉庫chalk
puppeteer
如果這個庫沒聽說過,你可能聽說過 selenium。puppeteer 是 Google Chrome 團隊開源的一個通過 devtools 協議操縱 chrome 或者Chromium 的 node 模組。Google 出品,質量有所保證。這個模組提供了一些高階的 api, 預設情況下,這個庫操縱的瀏覽器使用者是看不到介面的,也就是所謂的無頭(headless)瀏覽器。當然可以通過配置一些引數來啟動有介面的模式。在 chrome 中還有一些專門錄製 puppeteer 操作的擴充套件, 比如Puppeteer Recorder。使用這個庫我們可以用來抓取一些通過 js 渲染而不是直接存在於頁面原始碼中的資訊。比如 spa 頁面,頁面內容都是 js 渲染出來的。這個時候 puppeteer 就為我們解決了這個問題,我們可以呼叫 puppeteer 在頁面某個標籤出現時獲取到頁面當時的渲染出來的 html。事實上,往往很多比較困難的爬蟲解決的最終法寶就是操縱瀏覽器。
前置的 js 語法
async/await
首先要提的就是 async/await, 因為 node 在很早的時候(node 8 LTS)就已經支援 async/await, 現在寫後端專案沒理由不用 async/await了。使用 async/await 可以讓我們從回撥煉獄的困境中解脫出來。這裡主要提一下關於使用async/await 時可能會碰到的問題
使用 async/await 怎樣併發?
來看一段測試程式碼:
const sleep = (milliseconds) => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(), milliseconds)
})
}
const test1 = async () => {
for (let i = 0, max = 3; i < max; i++) {
await sleep(1000);
}
}
const test2 = async () => {
Array.from({length: 3}).forEach(async () => {
await sleep(1000);
});
}
const main = async () => {
console.time('測試 for 迴圈使用 await');
await test1();
console.timeEnd('測試 for 迴圈使用 await');
console.time('測試 forEach 呼叫 async 函式')
await test2();
console.timeEnd('測試 forEach 呼叫 async 函式')
}
main();
複製程式碼
執行結果是:
測試 for 迴圈使用 await: 3003.905ms
測試 forEach 呼叫 async 函式: 0.372ms
複製程式碼
我想可能會有些人會認為測試 forEach 的結果會是 1 秒左右,事實上測試2等同於以下程式碼:
const test2 = async () => {
// Array.from({length: 3}).forEach(async () => {
// await sleep(1000);
// });
Array.from({length: 3}).forEach(() => {
sleep(1000);
});
}
複製程式碼
從上面的執行結果也可以看出直接在 for 迴圈中使用 await + promise, 這樣等同於同步呼叫, 所以耗時是 3 秒左右。如果要併發則應該直接呼叫 promise, 因為 forEach 是不會幫你 await 的,所以等價於上面的程式碼,三個任務直接非同步併發了。
處理多個非同步任務
上面的程式碼還有一個問題,那就是測試2中並沒有等待三個任務都執行完就直接結束了,有時候我們需要等待多個併發任務結束之後再執行後續任務。其實很簡單,利用下 Promise 提供的幾個工具函式就可以了。
const sleep = (milliseconds, id='') => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`任務${id}執行結束`)
resolve(id);
}, milliseconds)
})
}
const test2 = async () => {
const tasks = Array.from({length: 3}).map((ele, index) => sleep(1000, index));
const resultArray = await Promise.all(tasks);
console.log({ resultArray} )
console.log('所有任務執行結束');
}
const main = async () => {
console.time('使用 Promise.all 處理多個併發任務')
await test2();
console.timeEnd('使用 Promise.all 處理多個併發任務')
}
main()
複製程式碼
執行結果:
任務0執行結束
任務1執行結束
任務2執行結束
{ resultArray: [ 0, 1, 2 ] }
所有任務執行結束
使用 Promise.all 處理多個併發任務: 1018.628ms
複製程式碼
除了 Promise.all, Promise 還有 race 等介面,不過最常用應該就是 all 和 race 了。
正規表示式
正規表示式是處理字串強有力的工具。核心是匹配,由此衍生出提取,查詢, 替換的等操作。
有時候我們通過 cheerio 中獲取到某個標籤內的文字時,我們需要提取其中的部分資訊,這個時候正規表示式就該上場了。正規表示式的相關語法這裡就不詳細說明了, 入門推薦看廖雪峰的正規表示式教程。來看個例項:
// 伺服器返回的 img url 是: /GetFile/getUploadImg?fileName=9b1cc22c74bc44c8af78b46e0ca4c352.png
// 現在我只想提取檔名,字尾名也不要
const imgUrl = '/GetFile/getUploadImg?fileName=9b1cc22c74bc44c8af78b46e0ca4c352.png';
const imgReg = /\/GetFile\/getUploadImg\?fileName=(.+)\..+/;
const imgName = imgUrl.match(imgReg)[1];
console.log(imgName); // => 9b1cc22c74bc44c8af78b46e0ca4c352
複製程式碼
暫時先介紹到這裡了,後續有更多內容會繼續補充。
本文為原創內容,首發於個人部落格, 轉載請註明出處。如果有問題歡迎郵件騷擾 ytj2713151713@gmail.com。