前言
最近在工作之余,一直在做數據可視化和nodejs方面的研究,雖然之前的web工作中接觸過nodejs和可視化相關的內容,但是沒有一個系統的總結和回顧,所以為了更深入的研究和復盤我的nodejs和數據可視化之路,筆者將會花兩個月的時間,做一個徹底的復盤.
- Node.js是一個事件驅動I/O服務端JavaScript環境,基于Google的V8引擎,V8引擎執行Javascript的速度非??欤阅芊浅:?。
可能很多朋友都或多或少的接觸過nodejs,筆者先來大致總結了一下nodejs的應用領域:
由上圖可以看出,nodejs的應用前景還是非常廣泛的,前幾年比較火的IOT物聯網技術,nodejs也有一定的領域貢獻.所以作為一名前端工程師(國際一點的叫法Front-end engineer), 要想讓自己的未來有更多的想象空間,node是必不可少的技能之一.話不多說,接下來筆者將帶大家一步步搭建一個高可用的nodejs開發環境,以便讓大家能更快更好的上手nodejs的開發工作.
你將收獲
- 如何配置eslint來管理項目代碼規范
- 如何使用babel7來配置nodejs支持最新的es語法
- 如何使用nodemon來自動化實現node程序自動重啟
- 如何劃分node目錄結構實現一個node通用服務類Xoa來實現經典的MVC架構
正文
在介紹正文之前,我想先談談前端項目的管理。就筆者的工作和管理經驗,衡量一個前端項目管理的好壞往往有以下幾個衡量點:
還原度和功能的完整性這兩個方面可以通過完善的測試體系去把控,對于代碼的擴展性,維護性和可讀性的評定,首先需要由團隊負責人去制定相應的代碼規范和規則,最大限度的保證同一個項目不同模塊的一致性。比如注釋規范,格式規范,目錄結構和文件命名等。其次放眼大局,公司如果有多個項目,或者多個項目會彼此聯系,這時候我們更要從整個前端架構的角度去衡量和設計,所以前端項目不僅僅是泛泛而談,它對企業長遠的產品架構,技術架構上有著非常重要的作用。所以說制定團隊或者項目規范,可以說是項目開始最為關鍵的一步。
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中鍵的值分別表示:
“off” or 0 - 關閉規則
“warn” or 1 - 將規則視為一個警告(不會影響退出碼)
“error” or 2 - 將規則視為一個錯誤 (退出碼為1)
這里的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/cli
- @babel/core
- @babel/node
- @babel/plugin-proposal-class-properties
- @babel/plugin-proposal-decorators
- @babel/preset-env
關于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開發的魅力。
本文轉載自微信公眾號「趣談前端」
原文地址:https://mp.weixin.qq.com/s/VHqK--TmhYhMRgeT8PxZqw