用 babel 和 nodemon 搭建一個功能齊全的 nodejs 開發環境

前言

最近在工作之餘, 一直在做數據可視化和 nodejs 方面的研究, 雖然之前的 web 工作中接觸過 nodejs 和可視化相關的內容, 但是沒有一個系統的總結和回顧, 所以爲了更深入的研究和覆盤我的 nodejs 和數據可視化之路, 筆者將會花兩個月的時間, 做一個徹底的覆盤.

Node.js 是一個事件驅動 I/O 服務端 JavaScript 環境,基於 Google 的 V8 引擎,V8 引擎執行 Javascript 的速度非常快,性能非常好。

可能很多朋友都或多或少的接觸過 nodejs, 筆者先來大致總結了一下 nodejs 的應用領域:

由上圖可以看出, nodejs 的應用前景還是非常廣泛的, 前幾年比較火的 IOT 物聯網技術, nodejs 也有一定的領域貢獻.

所以作爲一名前端工程師 (國際一點的叫法 Front-end engineer), 要想讓自己的未來有更多的想象空間, node 是必不可少的技能之一. 話不多說, 接下來筆者將帶大家一步步搭建一個高可用的 nodejs 開發環境, 以便讓大家能更快更好的上手 nodejs 的開發工作.

你將收穫

正文

在介紹正文之前,我想先談談前端項目的管理。就筆者的工作和管理經驗,衡量一個前端項目管理的好壞往往有以下幾個衡量點:

還原度和功能的完整性這兩個方面可以通過完善的測試體系去把控,對於代碼的擴展性,維護性和可讀性的評定,首先需要由團隊負責人去制定相應的代碼規範和規則,最大限度的保證同一個項目不同模塊的一致性。比如註釋規範,格式規範,目錄結構和文件命名等。其次放眼大局,公司如果有多個項目,或者多個項目會彼此聯繫,這時候我們更要從整個前端架構的角度去衡量和設計,所以前端項目不僅僅是泛泛而談,它對企業長遠的產品架構,技術架構上有着非常重要的作用。所以說制定團隊或者項目規範,可以說是項目開始最爲關鍵的一步。

1. 配置 eslint 來管理項目代碼規範

用過 eslint 的朋友都知道,eslint 主要是針對 javascript 代碼檢測用的插件化工具。它可以約束代碼的書寫格式,語法規範,比如保持代碼一致的縮進,代碼末尾有無分號,使用單引號還是雙引號等,我們通過一系列的配置,將會打造完全一致的代碼寫作風格,這樣對後期的代碼管理和維護有着非常重要的意義。說了這麼多,我們看看看怎麼使用在我們的 nodejs 項目中吧。

首先在 eslint 官網我們可以知道下載和安裝的方式,這裏我們採用全局安裝:

npm install eslint --global

然後我們就可以在項目中生成 eslint 的配置文件了,具體可選擇的配置文件類型有專屬的. eslintrc 的靜態 json 文件, 或者可動態配置的 eslintrc.js 文件,這裏筆者建議採用後者, 在當前項目下生成配置文件的命令如下:

eslint --init

這樣通過命令行的方法我們就可以生成我們想要的 eslint 配置文件了。首先筆者先上一份簡單的 eslint 配置文件:

module.exports = {
    "env": {
        "browser": true,
        "node": true,  // 啓用node環境
        "es6": true    // 啓用es6語法
    },
    "extends": "eslint:recommended",
    "globals": {
        "Atomics": "readonly",
        "SharedArrayBuffer": "readonly"
    },
    "parserOptions": {
        "ecmaVersion": 2018,
        "sourceType": "module"
    },
    "rules": {
        "semi": [2, "never"],  // 結尾不能有分號
        "eqeqeq": "warn",  // 要求使用 === 和 !==
        "no-irregular-whitespace": "warn",  // 禁止不規則的空白
        "no-empty-pattern": "warn",  // 禁止使用空解構模式
        "no-redeclare": "warn", // 禁止多次聲明同一變量
        "quotes": ["error", "single"],  // 代碼中使用單引號包裹字符串
        "indent": ["warn", 2],  // 代碼縮進爲2個空格
        "no-class-assign": "error",  // 禁止修改類聲明的變量
        "no-const-assign": "error",  // 禁止修改 const 聲明的變量
    }
};

其中 rules 中鍵的值分別表示:

這裏的 rule 規則大家可以採用市面上已有的規則文件或者可以根據自己的團隊風格自行配置,eslint 上有比較全面的規則配置表:

當我們的配置規則配置完畢後,我們只需要在 npm 的 scripts 腳本文件中添加執行代碼,eslint 就會自動幫我們校驗代碼:

"scripts": {
    "start": "eslint src && export NODE_ENV=development && nodemon -w src"
  }

上面代碼中 eslint src 表示對 src 目錄進行 eslint 語法規則和格式校驗,如果我們代碼有不符合規範的,那麼在控制檯將會顯示相應的錯誤。比如我們代碼中寫了雙引號,則運行項目的時候會出現如下錯誤:

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

我們都知道,nodejs 對 es 的支持還不夠完善,雖然在 10.0 + 已經支持大部分的 es 語法了,但是最重要的模塊化語法(import,export),類(class)和修飾器(Decorator)還不支持,作爲一名有追求的前端工程師,爲了讓代碼更優雅更簡潔,我們有理由去用最新的特性去編寫更加強大的代碼,所以完善的 es 的環境支持是搭建 nodejs 項目的第二步。

沒錯,爲了實現對 es 語法更全面的支持,babel 是我們的不二選擇。和 eslint 類似,編寫 babel 同樣也有幾種編寫配置文件的方式,這裏我們還是採用 js 的方式,這樣的好處是可以根據環境動態配置不同的編譯方式。

我們這裏統一採用 babel7 來給大家介紹如何配置 es 環境,如果你還在使用 babel6 或者更低的版本,可以查看對應文檔的版本進行配置。babel7 將很多功能都內置到了自己的模塊中,我們首先要配置環境,即 preset-env,我們可以使用 @babel/preset-env,對於 class 和 Decorator 的支持,我們需要安裝 @babel/plugin-proposal-class-properties 和 @babel/plugin-proposal-decorators 這兩個模塊。所以我們一共需要安裝如下幾個模塊:

關於 babel 的配置機制,官網上也寫的很詳細,大家感興趣的可以看一下,核心就是環境(presets)和插件(plugin)機制。官網對 preset-env 的解釋如下:

即 @babel/preset-env 是一個智能的允許我們使用最新 javascript 語法的代碼自動轉化工具。同時官網也列出了不同配置屬性對應的不同功能,爲了節約篇幅,我們直接上配置的代碼:

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
  }
}

這也是官方推薦的使用方式,更多靈活的配置大家可以參考官網配置。以上兩個 plugin 的作用不言而知,一個是用來編譯轉換修飾器屬性的,一個是用來編譯轉換 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 語法開發 nodejs 項目了, 在代碼編寫完成之後,我們執行 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);
}); // 全局註冊業務接口
// function autoRegister(path, )
_glob.default.sync((0, _path.resolve)(__dirname, './routes/*.js')).forEach(item => {
  app.use(require(item).default);
});
// ...

3. 如何使用 nodemon 來自動化實現 node 程序自動重啓

nodemon 的使用非常簡單,我們只需要按照官網文檔的配置來安裝和使用即可:

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 表示監聽 src 目錄下的文件變化,一旦文件變化將立刻重新啓動 node 程序。我們還可以專門寫一個 nodemon 的配置文件,實現不監聽某一個具體的文件變動,或者其他自定義的配置,如果服務上線,我們還可以用 forever 和 nodemon 結合來是實現持久化,當然主流的方式還是 pm2.

4. 如何劃分 node 目錄結構實現一個 node 通用服務類 Xoa 來實現經典的 MVC 架構

第四點是本文的核心和關鍵,目錄劃分往往考驗的是程序員對項目和架構的理解程度,對於服務端的目錄結構,筆者的經驗如下:

具體目錄如下:

當然不同目錄之間可以進一步細分,這個取決於項目規模。通過對項目有條理的結構化設計,團隊中不同的成員就可以有序的負責不同的模塊了。這種架構模式參考了傳統的 mvc 的模式,具體還是需要代碼層面進一步控制。

接下來筆者將用原生 javascript 實現一個簡單的 node 服務層的封裝,以實現更便捷的 node 開發,當然在實際項目中我們完全可以採用 koa,egg 這種成熟的框架來開發 node 應用,這裏筆者只是簡單實現一個例子方便大家對 node 開發有個更深入的認知。

我們都知道 nodejs 有 http 模塊方便我們快速創建一個 node 服務器,代碼可能長這個樣子:

import { createServer } from 'http'
createServer((req, res) => {
    res.end('hello world!')
}).listen(3000)

這樣就創建了一個簡單的服務器,當我們訪問 localhost:3000 的話我們就能看到頁面會顯示 hello world! 但是我們如果要想實現更復雜的功能,比如根據不同的路由處理不同的邏輯,我們該怎麼辦呢?也許你會說直接在 createServer 的回調中根據 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)

但是一旦業務邏輯複雜了,路由變多了,我們將寫大量的 if else 代碼,這對於維護性來說是一種極大的摧毀,我們希望將路由和業務邏輯劃分,分開來管理,這樣對於後期業務邏輯日漸複雜,頁面路由不斷增加才更加容易維護和管理。如何實現這一目標呢?我們可以參考 koa 的中間件機制,當我們要註冊一個路由時,我們只需要這樣寫:

app.use(routeA)

這樣是不是更優雅一點呢?所以我們基於以上需要來實現一個自己的小型服務框架

代碼實現如下:

import { createServer } from 'http'
class Xoa {
  constructor() {
    // 初始化中間鍵數組
    this.middleware = []
  }
  // 維持中間鍵數組
  use(func) {
    this.middleware.push(func)
  }
  // 創建服務器實例,並執行相應任務
  createServer() {
    const server = createServer((req, res) => {
      // 應用中間件
      this.middleware.forEach((fn) => fn(req, res))
    })
    return server
  }
  // 服務器監聽
  listen(port = 3000, cb) {
    this.createServer().listen(port, cb)
  }
}
export default Xoa

通過這樣的設計,我們就能優雅的使用中間件語法了:

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)

我們再來看另外一種場景,如果我們的路由很多,有負責頁面渲染的路由,也有負責輸出 api 數據的路由,那麼我們要每個都使用 use 來 use 一遍,這樣感覺太傻了,作爲一個有追求的程序員是不允許這種事情發生的,我們希望這一切都是自動完成的,自動註冊中間件,這該怎麼實現呢?
好在 node 社區提供了一個強大的第三方模塊 glob,我們可以通過 glob 來遍歷目錄實現自動化註冊路由,關於 glob 的用法這裏就不帶大家細說了,用法非常簡單。
比如我們的路由文件有如下幾個:

我們要保證路由目錄下面的路由文件都有導出,然後在 入口文件中我們可以這麼實現:

import glob from 'glob'
import { resolve } from 'path'
import Xoa from './lib/xoa.js'
import config from './config'
const app = new Xoa()
// 全局註冊業務接口
glob.sync(resolve(__dirname, './routes/*.js')).forEach(item => {
  app.use(require(item).default)
})
app.listen(config.serverPort, () => {
  console.log(`服務器地址:${config.protocol}//${config.host}:${config.serverPort}`)
})

通過 glob 的 sync 方法我們可以遍歷 routes 目錄並通過 require 加載路由文件,然後直接註冊到 app 上,這樣就不用我們手動一個個引入了,是不是非常簡單呢?(雖然這只是個極簡版的服務端封裝,對於實際項目需要做進一步的升級和擴展,但是設計思想希望大家能有所收穫)

對於負責項目我們可能還會考慮業務邏輯,我們會在 service 目錄下編寫我們的服務層代碼,在路由文件中使用,也有可能採用到數據庫模塊等,所以說這些都是比較有意思的實現,後面筆者將帶大家繼續做一個全棧項目,來感受 node 開發的魅力。

注: 本文代碼已傳到 github 上了,地址:https://github.com/MrXujiang/smart-node-tpl

歡迎大家多交流討論哈~

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/GvfCtI6B48iQrXTXYQfGfw