用babel和nodemon搭建一個功能齊全的nodejs開發(fā)環(huán)境
前言
Node.js是一個事件驅(qū)動I/O服務(wù)端JavaScript環(huán)境,基于Google的V8引擎,V8引擎執(zhí)行Javascript的速度非常快,性能非常好。

你將收獲
如何配置eslint來管理項(xiàng)目代碼規(guī)范 如何使用babel7來配置nodejs支持最新的es語法 如何使用nodemon來自動化實(shí)現(xiàn)node程序自動重啟 如何劃分node目錄結(jié)構(gòu)實(shí)現(xiàn)一個node通用服務(wù)類Xoa來實(shí)現(xiàn)經(jīng)典的MVC架構(gòu)
正文

1.配置eslint來管理項(xiàng)目代碼規(guī)范

用過eslint的朋友都知道,eslint主要是針對javascript代碼檢測用的插件化工具。它可以約束代碼的書寫格式,語法規(guī)范,比如保持代碼一致的縮進(jìn),代碼末尾有無分號,使用單引號還是雙引號等,我們通過一系列的配置,將會打造完全一致的代碼寫作風(fēng)格,這樣對后期的代碼管理和維護(hù)有著非常重要的意義。說了這么多,我們看看看怎么使用在我們的nodejs項(xiàng)目中吧。
首先在eslint官網(wǎng)我們可以知道下載和安裝的方式,這里我們采用全局安裝:
npm install eslint --global
然后我們就可以在項(xiàng)目中生成eslint的配置文件了,具體可選擇的配置文件類型有專屬的.eslintrc的靜態(tài)json文件, 或者可動態(tài)配置的eslintrc.js文件,這里筆者建議采用后者, 在當(dāng)前項(xiàng)目下生成配置文件的命令如下:
eslint --init
這樣通過命令行的方法我們就可以生成我們想要的eslint配置文件了。首先筆者先上一份簡單的eslint配置文件:
module.exports = {"env": {"browser": true,"node": true, // 啟用node環(huán)境"es6": true // 啟用es6語法},"extends": "eslint:recommended","globals": {"Atomics": "readonly","SharedArrayBuffer": "readonly"},"parserOptions": {"ecmaVersion": 2018,"sourceType": "module"},"rules": {"semi": [2, "never"], // 結(jié)尾不能有分號"eqeqeq": "warn", // 要求使用 === 和 !=="no-irregular-whitespace": "warn", // 禁止不規(guī)則的空白"no-empty-pattern": "warn", // 禁止使用空解構(gòu)模式"no-redeclare": "warn", // 禁止多次聲明同一變量"quotes": ["error", "single"], // 代碼中使用單引號包裹字符串"indent": ["warn", 2], // 代碼縮進(jìn)為2個空格"no-class-assign": "error", // 禁止修改類聲明的變量"no-const-assign": "error", // 禁止修改 const 聲明的變量}};
其中rules中鍵的值分別表示:
“off” or 0 - 關(guān)閉規(guī)則 “warn” or 1 - 將規(guī)則視為一個警告(不會影響退出碼) “error” or 2 - 將規(guī)則視為一個錯誤 (退出碼為1)
這里的rule規(guī)則大家可以采用市面上已有的規(guī)則文件或者可以根據(jù)自己的團(tuán)隊(duì)風(fēng)格自行配置,eslint上有比較全面的規(guī)則配置表:

當(dāng)我們的配置規(guī)則配置完畢后,我們只需要在npm的scripts腳本文件中添加執(zhí)行代碼,eslint就會自動幫我們校驗(yàn)代碼:
"scripts": {"start": "eslint src && export NODE_ENV=development && nodemon -w src"??}

2.如何使用babel7來配置nodejs支持最新的es語法

@babel/cli @babel/core @babel/node @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/preset-env
關(guān)于babel的配置機(jī)制,官網(wǎng)上也寫的很詳細(xì),大家感興趣的可以看一下,核心就是環(huán)境(presets)和插件(plugin)機(jī)制。官網(wǎng)對preset-env的解釋如下:

即@babel/preset-env是一個智能的允許我們使用最新javascript語法的代碼自動轉(zhuǎn)化工具。同時官網(wǎng)也列出了不同配置屬性對應(yīng)的不同功能,為了節(jié)約篇幅,我們直接上配置的代碼:
module.exports = function (api) {api.cache(true)const presets = [['@babel/preset-env',{'targets': {'node': 'current'}}]]const plugins = [['@babel/plugin-proposal-decorators', { 'legacy': true }],['@babel/plugin-proposal-class-properties', { 'loose' : true }]]return {presets,plugins}}
這也是官方推薦的使用方式,更多靈活的配置大家可以參考官網(wǎng)配置。以上兩個plugin的作用不言而知,一個是用來編譯轉(zhuǎn)換修飾器屬性的,一個是用來編譯轉(zhuǎn)換class語法的。最后一步就是在package.json中的腳本文件中使用我們的babel工具:
"scripts": {"start": "eslint src && nodemon -w src --exec \"babel-node src\"","build": "babel src --out-dir dist"}
babel-node src指定了需要編譯的node目錄為src目錄,其他文件和目錄無需編譯。
通過這樣的配置,我們就能開心的用最新的javascript語法開發(fā)nodejs項(xiàng)目了,在代碼編寫完成之后,我們執(zhí)行npm run build即可將src的代碼打包編譯到dist目錄下。編譯后的代碼如下:
"use strict";var _glob = _interopRequireDefault(require("glob"));var _path = require("path");var _xoa = _interopRequireDefault(require("./lib/xoa.js"));var _config = _interopRequireDefault(require("./config"));function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }const app = new _xoa.default();app.use((req, res) => {console.log(req.url, req.method);}); // 全局注冊業(yè)務(wù)接口// function autoRegister(path, )_glob.default.sync((0, _path.resolve)(__dirname, './routes/*.js')).forEach(item => {app.use(require(item).default);});// ...
3.如何使用nodemon來自動化實(shí)現(xiàn)node程序自動重啟
nodemon的使用非常簡單,我們只需要按照官網(wǎng)文檔的配置來安裝和使用即可:
npm install --save-dev nodemon然后在package.json的腳本文件中如下配置:
"scripts": {"start": "eslint src && export NODE_ENV=development && nodemon -w src --exec \"babel-node src\"","build": "babel src --out-dir dist","buildR": "node dist","test": "echo \"Error: no test specified\" && exit 1"}
nodemon -w src 表示監(jiān)聽src目錄下的文件變化,一旦文件變化將立刻重新啟動node程序。我們還可以專門寫一個nodemon的配置文件,實(shí)現(xiàn)不監(jiān)聽某一個具體的文件變動,或者其他自定義的配置,如果服務(wù)上線,我們還可以用forever和nodemon結(jié)合來是實(shí)現(xiàn)持久化,當(dāng)然主流的方式還是pm2.
4.如何劃分node目錄結(jié)構(gòu)實(shí)現(xiàn)一個node通用服務(wù)類Xoa來實(shí)現(xiàn)經(jīng)典的MVC架構(gòu)
第四點(diǎn)是本文的核心和關(guān)鍵,目錄劃分往往考驗(yàn)的是程序員對項(xiàng)目和架構(gòu)的理解程度,對于服務(wù)端的目錄結(jié)構(gòu),筆者的經(jīng)驗(yàn)如下:

具體目錄如下:

當(dāng)然不同目錄之間可以進(jìn)一步細(xì)分,這個取決于項(xiàng)目規(guī)模。通過對項(xiàng)目有條理的結(jié)構(gòu)化設(shè)計(jì),團(tuán)隊(duì)中不同的成員就可以有序的負(fù)責(zé)不同的模塊了。這種架構(gòu)模式參考了傳統(tǒng)的mvc的模式,具體還是需要代碼層面進(jìn)一步控制。
import { createServer } from 'http'createServer((req, res) => {res.end('hello world!')}).listen(3000)
這樣就創(chuàng)建了一個簡單的服務(wù)器,當(dāng)我們訪問localhost:3000的話我們就能看到頁面會顯示hello world! 但是我們?nèi)绻雽?shí)現(xiàn)更復(fù)雜的功能,比如根據(jù)不同的路由處理不同的邏輯,我們該怎么辦呢?也許你會說直接在createServer的回調(diào)中根據(jù)req.url來判斷,代碼如下:
import { createServer } from 'http'createServer((req, res) => {if(req.url === 'A') {// A的邏輯}else if(req.url === 'B') {// B的邏輯}else if(req.url === 'C') {// C的邏輯}// ...}).listen(3000)
但是一旦業(yè)務(wù)邏輯復(fù)雜了,路由變多了,我們將寫大量的if else代碼,這對于維護(hù)性來說是一種極大的摧毀,我們希望將路由和業(yè)務(wù)邏輯劃分,分開來管理,這樣對于后期業(yè)務(wù)邏輯日漸復(fù)雜,頁面路由不斷增加才更加容易維護(hù)和管理。如何實(shí)現(xiàn)這一目標(biāo)呢?我們可以參考koa的中間件機(jī)制,當(dāng)我們要注冊一個路由時,我們只需要這樣寫:
app.use(routeA)這樣是不是更優(yōu)雅一點(diǎn)呢?所以我們基于以上需要來實(shí)現(xiàn)一個自己的小型服務(wù)框架

import { createServer } from 'http'class Xoa {constructor() {// 初始化中間鍵數(shù)組this.middleware = []}// 維持中間鍵數(shù)組use(func) {this.middleware.push(func)}// 創(chuàng)建服務(wù)器實(shí)例,并執(zhí)行相應(yīng)任務(wù)createServer() {const server = createServer((req, res) => {// 應(yīng)用中間件this.middleware.forEach((fn) => fn(req, res))})return server}// 服務(wù)器監(jiān)聽listen(port = 3000, cb) {this.createServer().listen(port, cb)}}export default Xoa
通過這樣的設(shè)計(jì),我們就能優(yōu)雅的使用中間件語法了:
import Xoa from './lib/xoa.js'const app = new Xoa()app.use((req, res) => {console.log(req.url, req.method)res.end('A')})app.use((req, res) => {res.end('B')})app.listen(3000)
我們再來看另外一種場景,如果我們的路由很多,有負(fù)責(zé)頁面渲染的路由,也有負(fù)責(zé)輸出api數(shù)據(jù)的路由,那么我們要每個都使用use來use一遍,這樣感覺太傻了,作為一個有追求的程序員是不允許這種事情發(fā)生的,我們希望這一切都是自動完成的,自動注冊中間件,這該怎么實(shí)現(xiàn)呢?
好在node社區(qū)提供了一個強(qiáng)大的第三方模塊glob,我們可以通過glob來遍歷目錄實(shí)現(xiàn)自動化注冊路由,關(guān)于glob的用法這里就不帶大家細(xì)說了,用法非常簡單。
比如我們的路由文件有如下幾個:

import glob from 'glob'import { resolve } from 'path'import Xoa from './lib/xoa.js'import config from './config'const app = new Xoa()// 全局注冊業(yè)務(wù)接口glob.sync(resolve(__dirname, './routes/*.js')).forEach(item => {app.use(require(item).default)})app.listen(config.serverPort, () => {console.log(`服務(wù)器地址:${config.protocol}//${config.host}:${config.serverPort}`)})
通過glob的sync方法我們可以遍歷routes目錄并通過require加載路由文件,然后直接注冊到app上,這樣就不用我們手動一個個引入了,是不是非常簡單呢?(雖然這只是個極簡版的服務(wù)端封裝,對于實(shí)際項(xiàng)目需要做進(jìn)一步的升級和擴(kuò)展,但是設(shè)計(jì)思想希望大家能有所收獲)
對于負(fù)責(zé)項(xiàng)目我們可能還會考慮業(yè)務(wù)邏輯,我們會在service目錄下編寫我們的服務(wù)層代碼,在路由文件中使用,也有可能采用到數(shù)據(jù)庫模塊等,所以說這些都是比較有意思的實(shí)現(xiàn),后面筆者將帶大家繼續(xù)做一個全棧項(xiàng)目,來感受node開發(fā)的魅力。
注: 本文代碼已傳到github上了,地址:https://github.com/MrXujiang/smart-node-tpl
歡迎大家多交流討論哈~

