本文是由我們的客座作者Azat Mardan寫的。SitePoint引入客座帖子的目的是希望能給你帶來web社區(qū)里著名作者和演講者的有趣內(nèi)容贬派。
在2012年急但,我加入了Storify并開始使用Node作為我的主要語言。從那以后搞乏,我從未回首過去并覺得我錯過了Python波桩,Ruby,Java以及PHP请敦,這些在過去10年里镐躲,我在web開發(fā)過程中使用的語言储玫。
Storify提供給我一個很有趣的工作,因?yàn)镾torify和其他的公司不太一樣匀油,Storify之前(可能到現(xiàn)在也是)所有的代碼都是由JavaScript編寫的缘缚。而大多數(shù)公司,特別是大公司敌蚜,例如PayPal桥滨,Walmart(沃爾瑪)或者Capital One(第一資本),只是在某一些特定的部分使用了Node弛车。通常齐媒,他們使用Node作為API接口或者用在業(yè)務(wù)流程層,這樣做是很好的纷跛。但是作為一個軟件工程師喻括,沒什么比得上能夠完全沉浸在Node環(huán)境里。
下面我將列出10條建議贫奠,這些建議可以幫助你在2017年成為一個更好的Node開發(fā)者唬血。其中一些建議是我在日常實(shí)踐中所學(xué)到的,另一些是從那些寫了最流行的Node和npm模塊的人們身上學(xué)到的唤崭。 下面是我們將要介紹的內(nèi)容:
避免復(fù)雜性 — 盡可能將你的代碼塊拆到最小拷恨,要小到極致。
使用異步編程 — 像躲避瘟疫般避免使用同步代碼谢肾。
避免require阻塞 — 把你所有的require聲明都放在文件的頂部腕侄,因?yàn)閞equire是同步的,會阻塞代碼運(yùn)行芦疏。
了解require緩存 — 了解它則可以利用它冕杠,否則它可能會帶來bug。
始終檢查錯誤 — 錯誤不是足球酸茴,任何時候都不要拋出錯誤或者跳過錯誤檢查分预。
只在同步代碼中使用try…catch — 在異步代碼中
try...catch
是沒有作用的。V8引擎針對try...catch
無法進(jìn)行優(yōu)化弊决。返回callbacks或者使用if … else — 返回一個callback只是為了確保不繼續(xù)執(zhí)行噪舀。
監(jiān)聽錯誤事件 — 幾乎所有的Node的類/對象都有event emitter(觀察者模式)并且會廣播
error
事件,確保你監(jiān)聽了它們飘诗。了解你的npm — 使用
-S
或者-D
來安裝模塊來代替--save或者
--save-dev`与倡。在package.json中使用精確的版本號: npm在使用
-S
來安裝模塊時會自動使用默認(rèn)的版本號,你需要手動修改去鎖定版本號昆稿。除非是開源模塊纺座,否者不要相信你的項(xiàng)目中的SemVer(語義化版本標(biāo)準(zhǔn))。加分 — 使用不同的依賴溉潭。把項(xiàng)目在開發(fā)階段需要的東西放在 devDependencies 中净响,記得使用 npm i --production少欺。多余的依賴越多,出現(xiàn)問題的風(fēng)險(xiǎn)就越大馋贤。
好的赞别,接下來讓我們一個個單獨(dú)地去了解上面的每一點(diǎn)。
避免復(fù)雜性
讓我看一眼npm的創(chuàng)造者Isaac Z. Schlueter寫的一些模塊配乓,例如仿滔,use-strict,這個模塊是用來在Javascript中強(qiáng)制使用嚴(yán)格模式犹芹,這個模塊僅僅只有三行代碼:
var module = require('module')
module.wrapper[0] += '"use strict";'
Object.freeze(module.wrap)
所以我們?yōu)槭裁匆苊鈴?fù)雜性呢? 一個起源于美國海軍的著名短語:KEEP IT SIMPLE STUPID(或者是“Keep it simple, stupid”)崎页。這就是原因。事實(shí)說明腰埂,人類大腦在任何一個時間只能在其工作記憶中保持五到七個項(xiàng)目飒焦。
把你的代碼模塊化成一個更加小的部分,你和其他的開發(fā)者會更加好的理解它屿笼。你也可以更加好的去測試它牺荠。如下例子,
app.use(function(req, res, next) {
if (req.session.admin === true) return next()
else return next(new Error('Not authorized'))
}, function(req, res, next) {
req.db = db
next()
})
或者是
const auth = require('./middleware/auth.js')
const db = require('./middleware/db.js')(db)
app.use(auth, db)
我相信大多數(shù)人都會喜歡第二個例子驴一,特別是光看名字就能了解其作用志电。當(dāng)日,在你編寫代碼的時候蛔趴,你可能認(rèn)為你知道代碼是如何運(yùn)行的。甚至你想要展示你把幾個功能連接在一起寫在同一行中是多么的機(jī)智例朱。但是孝情,這樣你是寫了一段愚蠢的代碼。如果你思考的很復(fù)雜去寫這代碼洒嗤,那么今后你再去看這段代碼將會很難去理解箫荡。保證你的代碼簡單,特別是在Node的異步代碼中渔隶。
當(dāng)然也會有left-pad 事件羔挡,但是其實(shí)它只是影響了依賴于left-pad模塊的項(xiàng)目而且11分鐘后就發(fā)布了替代品。代碼的最小化帶來的好處超過了它的缺點(diǎn)间唉。npm已經(jīng)改變了發(fā)布策略,任何重要的項(xiàng)目都應(yīng)該使用緩存或私有的源(作為臨時解決方案)绞灼。
使用異步編程
在Node中同步代碼只要很小的一部分。這些代碼大多數(shù)都是用于命令行工具或者其他與web應(yīng)用無關(guān)的腳本呈野。Node開發(fā)者大多數(shù)都是編寫web應(yīng)用低矮,因此使用異步代碼可以避免阻塞現(xiàn)場。
例如被冒,當(dāng)你在編寫一個數(shù)據(jù)庫的腳本或者是一個不需要控制并行的任務(wù)時军掂,下面這種寫法可能是可以的:
let data = fs.readFileSync('./acconts.json')
db.collection('accounts').insert(data, (results))=>{
fs.writeFileSync('./accountIDs.json', results, ()=>{process.exit(1)})
})
但是當(dāng)你創(chuàng)建一個web應(yīng)用時轮蜕,下面這個寫法會更好:
app.use('/seed/:name', (req, res) => {
let data = fs.readFile(`./${req.params.name}.json`, ()=>{
db.collection(req.params.name).insert(data, (results))=>{
fs.writeFile(`./${req.params.name}IDs.json`, results, ()={res.status(201).send()})
})
})
})
這個區(qū)別在于你是否需要編寫一個并發(fā)(通常是長期運(yùn)行)或者非并發(fā)(短期運(yùn)行)的系統(tǒng)。根據(jù)經(jīng)驗(yàn)來說蝗锥,總是要在Node中使用異步代碼跃洛。
避免require阻塞
Node有一個使用了CommonJS模塊格式的簡單的模塊加載系統(tǒng)。它是基于require
函數(shù)终议,require
函數(shù)可以很方便的在不同的文件中引入模塊汇竭。和AMD/requirejs不同,Node/CommonJS的模塊加載時同步的痊剖。require
的工作方式是:引入一個模塊或者一個文件export的內(nèi)容:
`const react = require('react')`
但是大多數(shù)的開發(fā)者并不知道require
是會被緩存的韩玩。因此,只要解析的文件名(resolved filename)沒有劇烈的變化(比如npm模塊不存在的情況)陆馁,模塊的代碼只會被執(zhí)行并存入變量中一次(在當(dāng)前進(jìn)程中)找颓。這是一個很好的優(yōu)化。當(dāng)然叮贩,即使有了緩存击狮,你最好還是把你的require聲明寫在開頭。下面這段代碼益老,它在路由中真正使用到了axios
模塊的時候才加載彪蓬。當(dāng)請求發(fā)送的時候/connect
會因?yàn)樾枰虞d模塊所以會變得慢。
app.post('/connect', (req, res) => {
const axios = require('axios')
axios.post('/api/authorize', req.body.auth)
.then((response)=>res.send(response))
})
一個更好捺萌,性能更優(yōu)的方式是在服務(wù)定義之前就引入模塊而不是在路由中:
const axios = require('axios')
const express = require('express')
app = express()
app.post('/connect', (req, res) => {
axios.post('/api/authorize', req.body.auth)
.then((response)=>res.send(response))
})
知道require會被緩存
我在上面一節(jié)已經(jīng)提到了require
會被緩存档冬,但是有趣的是我們在module.exports
之外也會有代碼。舉例來說:
console.log('I will not be cached and only run once, the first time')
module.exports = () => {
console.log('I will be cached and will run every time this module is invoked')
}
從中我們了解到有一些代碼只會運(yùn)行一次桃纯,你可以使用這個特性來優(yōu)化你的代碼酷誓。
始終檢查錯誤
Node不是Java。在Java中态坦,你可以拋出錯誤盐数,因?yàn)槿绻l(fā)生了錯誤那么你會希望應(yīng)用不在繼續(xù)執(zhí)行。在Java中伞梯,你可以在外層僅僅使用一個簡單的try...catch
就可以處理多個錯誤玫氢。
但是在Node中并不是這樣的。自從Node使用了事件循環(huán)和異步執(zhí)行后谜诫,任何的錯誤發(fā)生時都會與錯誤處理器(例如try...catch
)的上下文分離漾峡,下面這樣做在Node中是沒有用的:
try {
request.get('/accounts', (error, response)=>{
data = JSON.parse(response)
})
} catch(error) {
// Will NOT be called
console.error(error)
}
但是try...catch
在同步代碼中是可以被用的。前面的代碼片段可以被更好的重構(gòu)為:
request.get('/accounts', (error, response)=>{
try {
data = JSON.parse(response)
} catch(error) {
// Will be called
console.error(error)
}
})
如果我們無法將request
的返回內(nèi)容包裹在try...catch
中猜绣,那么我們將沒有辦法去處理請求的錯誤灰殴。Node的開發(fā)者通過在返回的參數(shù)里面加上error
來解決了這個問題。因此,我們需要在每一個回調(diào)中手動去處理錯誤牺陶。你可以去檢查這些錯誤(判斷error
不是null
),然后展示錯誤信息給用戶或者展示在客戶端上并且記錄它伟阔, 或者你可以通過調(diào)用 callback ,給它傳 error 參數(shù)掰伸,將錯誤傳回給上一級調(diào)用棧(如果你在調(diào)用棧之上有另一個回調(diào)函數(shù))皱炉。
request.get('/accounts', (error, response)=>{
if (error) return console.error(error)
try {
data = JSON.parse(response)
} catch(error) {
console.error(error)
}
})
一個小技巧是你可以使用okay庫。你可以像下面的例子一樣使用它去避免在回調(diào)地獄中手動去檢查錯誤(你好, 回調(diào)地獄).
var ok = require('okay')
request.get('/accounts', ok(console.error, (response)=>{
try {
data = JSON.parse(response)
} catch(error) {
console.error(error)
}
}))
返回回調(diào)或者使用if … else
Node是并行的狮鸭。但是如果你不夠細(xì)心也會因?yàn)檫@個特性產(chǎn)生bug合搅。 為了安全起見,應(yīng)該要使用return來終止代碼的繼續(xù)執(zhí)行:
let error = true
if (error) return callback(error)
console.log('I will never run - good.')
這樣可以避免一些因?yàn)榇a邏輯的處理不當(dāng)導(dǎo)致一些不應(yīng)該執(zhí)行的內(nèi)容(或者錯誤)被執(zhí)行歧蕉。
let error = true
if (error) callback(error)
console.log('I will run. Not good!')
請確保使用return
去阻止代碼的繼續(xù)執(zhí)行灾部。
監(jiān)聽 error
事件
Node中幾乎所有的類/對象都有事件分發(fā)器(觀察者模式)并且會廣播 error
事件。 這是一個很好的特性惯退,可以使開發(fā)者在這些討厭的錯誤造成巨大后果之前捕捉到它們赌髓。
養(yǎng)成一個通過.on()
來創(chuàng)建error
事件監(jiān)聽的好習(xí)慣:
var req = http.request(options, (res) => {
if (('' + res.statusCode).match(/^2\d\d$/)) {
// Success, process response
} else if (('' + res.statusCode).match(/^5\d\d$/))
// Server error, not the same as req error. Req was ok.
}
})
req.on('error', (error) => {
// Can't even make a request: general error, e.g. ECONNRESET, ECONNREFUSED, HPE_INVALID_VERSION
console.log(error)
})
了解你的npm
很多的Node和前端的開發(fā)者知道在安裝模塊的時候使用--save
會在安裝模塊的同時,會在package.json
保存一條含有模塊版本信息的條目催跪。當(dāng)然锁蠕,還有--save-dev
可以用于安裝devDependencies
(在生成環(huán)境中不需要的模塊)。但是你知道用-S
和-D
是否可以代替--save
和--save-dev
么懊蒸?答案是可以的荣倾。
當(dāng)你安裝模塊的時候,你需要刪除-S
和-D
自動為你模塊的版本號添加的^
標(biāo)簽骑丸。否者當(dāng)你使用npm install
(或者npm i
)安裝模塊的時候舌仍,就會自動拉取最新的鏡像(版本號的第二位數(shù)字)。例如v6.1.0就是v6.2.0的一個鏡像分支通危。
npm團(tuán)隊(duì)推薦使用semver抡笼,但是你最好不要這樣。npm團(tuán)隊(duì)認(rèn)為開源開發(fā)者會遵守semver所以他們在npm安裝時自動加上了^
黄鳍。沒有人可以去保證,所以最好是鎖定你的版本號平匈。更好的辦法是使用shrinkwrap:npm shrinkwrap
會生成一個包含依賴的具體版本的文件框沟。
結(jié)束語
這篇文章是兩部分的第一部分,我們已經(jīng)提到了很多方面增炭,從使用callbacks和異步代碼忍燥,到核查錯誤和鎖定依賴。希望你們可以從中學(xué)習(xí)到一些新的隙姿,或者有用的信息梅垄。敬請期待即將推出的第二部分。
同時输玷,告訴我你的想法队丝。我是否遺漏了什么靡馁?你是否有不一樣的做法?在下面的評論區(qū)告訴我你的想法吧机久。