[譯]Express應用結構的最佳實踐

前言

NodeExpress並不嚴格要求它的應用的文件結構。你能夠以任意的結構來組織你的web應用。這對於小應用來講,一般是不錯的,十分易於學習和實驗。javascript

可是,當你的應用在體積和複雜性上都變得愈來愈高時,狀況就變得複雜了。你的代碼可能會變得凌亂。當你的團隊人數增長時,向在同一個代碼庫內寫代碼變得愈發困難,每次合併代碼時均可能會出現各類各樣的衝突。爲應用增長新的特性和處理新的狀況可能都會改變文件的結構。css

一個好的文件結構,應該是每個不一樣的文件或文件夾,都分別負責處理不一樣的任務。這樣,在添加新特性時纔會變得不會有衝突。html

最佳實踐

這裏所推薦的結構是基於MVC設計模式的。這個模式在職責分離方面作得很是好,因此讓你的代碼更具備可維護性。在這裏咱們不會去過多地討論MVC的優勢,而是更多地討論若是使用它來創建你的Express應用的文件結構。java

例子:node

讓咱們來看下面這個例子。這是一個用戶能夠登陸,註冊,留下評論的應用。如下是他的文件結構。git

project/
  controllers/
    comments.js
    index.js
    users.js
  helpers/
    dates.js
  middlewares/
    auth.js
    users.js
  models/
    comment.js
    user.js
  public/
    libs/
    css/
    img/
  views/
    comments/
      comment.jade
    users/
    index.jade
  tests/
    controllers/
    models/
      comment.js
    middlewares/
    integration/
    ui/
  .gitignore
  app.js
  package.json

這看上去可能有點複雜,但不要擔憂。在讀完這篇文章以後,你將會完徹底全地理解它。它本質上是十分簡單的。web

如下是對這個應用中的根文件(夾)的做用的簡介:數據庫

  • controllers/ – 定義你應用的路由和它們的邏輯express

  • helpers/ – 能夠被應用的其餘部分所共享的代碼和功能npm

  • middlewares/ – 處理請求的Express中間件

  • models/ – 表明了實現了業務邏輯的數據

  • public/ – 包含了如圖片,樣式,javascript這樣的靜態文件

  • views/ – 提供了供路由渲染的頁面模板

  • tests/ – 用於測試其餘文件夾的代碼

  • app.js – 初始化你的應用,並將因此部分聯接在一塊兒

  • package.json – 記錄你的應用的依賴庫以及它們的版本

須要提醒的是,除了文件的結構自己,它們所表明的職責也是十分重要的。

Models

你應該在modules中處理與數據庫的交互,裏面的文件包含了處理數據全部方法。它們不只提供了對數據的增,刪,改,查方法,還提供了額外的業務邏輯。例如,若是你有一個汽車model,它也應該包含一個mountTyres方法。

對於數據庫中的每一類數據,你都至少應該建立一個對應的文件。對應到咱們的例子裏,有用戶以及評論,因此咱們至少要有一個user model和一個comment model。當一個model變得過於臃腫時,基於它的內部邏輯將它拆分紅多個不一樣的文件一般是一個更好的作法。

你應該保持你的各個model之間保持相對獨立,它們應相互不知道對方的存在,也不該引用對方。它們也不須要知道controllers的存在,也永遠不要接受HTTP請求和響應對象,和返回HTTP錯誤,可是,咱們能夠返回特定的model錯誤。

這些都會使你的model變得更容易維護。因爲它們之間相互沒有依賴,因此也容易進行測試,對其中一個model進行改變也不會影響到其餘model

如下是咱們的評論model

var db = require('../db')

// Create new comment in your database and return its id
exports.create = function(user, text, cb) {
  var comment = {
    user: user,
    text: text,
    date: new Date().toString()
  }

  db.save(comment, cb)
}

// Get a particular comment
exports.get = function(id, cb) {
  db.fetch({id:id}, function(err, docs) {
    if (err) return cb(err)
    cb(null, docs[0])
  })
}

// Get all comments
exports.all = function(cb) {
  db.fetch({}, cb)
}

// Get all comments by a particular user
exports.allByUser = function(user, cb) {
  db.fetch({user: user}, cb)
}

它並不引用用戶model。它僅僅須要一個提供用戶信息的user參數,可能包含用戶ID或用戶名。評論model並不關心它究竟是什麼,它只關心這能夠被存儲。

var db = require('../db')
  , crypto = require('crypto')

hash = function(password) {
  return crypto.createHash('sha1').update(password).digest('base64')
}

exports.create = function(name, email, password, cb) {
  var user = {
    name: name,
    email: email,
    password: hash(password),
  }

  db.save(user, cb)
}

exports.get = function(id, cb) {
  db.fetch({id:id}, function(err, docs) {
    if (err) return cb(err)
    cb(null, docs[0])
  })
}

exports.authenticate = function(email, password) {
  db.fetch({email:email}, function(err, docs) {
    if (err) return cb(err)
    if (docs.length === 0) return cb()

    user = docs[0]

    if (user.password === hash(password)) {
      cb(null, docs[0])
    } else {
      cb()
    }
  })
}

exports.changePassword = function(id, password, cb) {
  db.update({id:id}, {password: hash(password)}, function(err, affected) {
    if (err) return cb(err)
    cb(null, affected > 0)
  })
}

除了建立和管理用戶的方法,用戶的model還應該提供身份驗證和密碼管理的方法。再次重申,這些model之間必須相互不知道對方的存在。

Views

這個文件夾內包含了全部你的應用須要渲染的模板,一般都是由你團隊內的設計師設計的。

當選擇模板語言時,你可能會有些困難,由於當下可選擇的模板語言太多了。我最喜歡的兩個模板語言是JadeMustacheJade很是擅於生成HTML,它相對於HTML更簡短以及更可讀。它對JavaScript的條件和迭代語法也有強大的支持。Mustache則徹底相反,它更專一於模板渲染,而不是很關心邏輯操做。

寫一個模板的最佳實踐是,不要在模板中處理數據。若是你須要在模板中展現處理後的數據,你應該在controller處理它們。一樣地,多餘的邏輯操做也應該被移到controller中。

doctype html
html
  head
    title Your comment web app
  body
    h1 Welcome and leave your comment
    each comment in comments
      article.Comment
        .Comment-date= comment.date
        .Comment-text= comment.text

Controllers

在這個文件夾裏的是全部你定義的路由。它們處理web請求,處理數據,渲染模板,而後將其返回給用戶。它們是你的應用中的其餘部分的粘合劑。

一般狀況下,你應該至少爲你應用的每個邏輯部分寫一個路由。例如,一個路由來處理評論,另外一個路由來處理用戶,等等。同一類的路由最好有相同的前綴,如/comments/all/comments/new

有時,什麼代碼該寫進controller,什麼代碼該寫進model是容易混淆的。最佳的實踐是,永遠不要在controller裏直接調用數據庫,應該使用model提供方法來代替之。例如,若是你有一個汽車model,而後想要在某輛車上安上四個輪子,你不該該直接調用db.update(id, {wheels: 4}),而是應該調用相似car.mountWheels(id, 4)這樣的model方法。

如下是關於評論的controller代碼:

var express = require('express')
  , router = express.Router()
  , Comment = require('../models/comment')
  , auth = require('../middlewares/auth')

router.post('/', auth, function(req, res) {
  user = req.user.id
  text = req.body.text

  Comment.create(user, text, function (err, comment) {
    res.redirect('/')
  })
})

router.get('/:id', function(req, res) {
  Comment.get(req.params.id, function (err, comment) {
    res.render('comments/comment', {comment: comment})
  })
})

module.exports = router

一般在controller文件夾中,有一個index.js。它用來加載其餘的controller,而且定義一些沒有常規前綴的路由,如首頁路由:

var express = require('express')
  , router = express.Router()
  , Comment = require('../models/comment')

router.use('/comments', require('./comments'))
router.use('/users', require('./users'))

router.get('/', function(req, res) {
  Comments.all(function(err, comments) {
    res.render('index', {comments: comments})
  })
})

module.exports = router

這個文件的router加載了你的全部路由。因此你的應用在啓動時,只須要引用它既可。

Middlewares

全部的Express中間件都會保存在這個文件夾中。中間件存在的目的,就是提取出一些controller中,處理請求和響應對象的共有的代碼。

controller同樣,一個middleware也不該該直接調用數據庫方法。而應使用model方法。

如下例子是middlewares/users.js,它用來在請求時加載用戶信息。

User = require('../models/user')

module.exports = function(req, res, next) {
  if (req.session && req.session.user) {
    User.get(req.session.user, function(err, user) {
      if (user) {
        req.user = user
      } else {
        delete req.user
        delete req.session.user
      }

      next()
    })
  } else {
    next()
  }
}

這個middleware使用了用戶model,而不是直接操做數據庫。

下面,是一個身份認證中間件。經過它來阻止未認證的用戶進入某些路由:

module.exports = function(req, res, next) {
  if (req.user) {
    next()
  } else {
    res.status(401).end()
  }
}

它沒有任何外部依賴。若是你看了上文controller中的例子,你必定已經明白它是如何運做的。

Helpers

這個文件夾中包含一些實用的工具方法,被用於model,middlewarecontroller中。一般對於不一樣的任務,這些工具方法會保存在不一樣的文件中。

Public

這個文件只用於存放靜態文件。一般是css, 圖片,JS庫(如jQuery)。提供靜態文件的最佳實踐是使用NginxApache做爲靜態文件服務器,在這方面,它們一般比Node更出色。

Tests

全部的項目都須要有測試,你須要將全部的測試代碼放在一個地方。爲了方便管理,你可能須要將這些測試代碼放於幾個不一樣的子文件夾中。

  • controllers

  • helpers

  • models

  • middleware

  • integration

  • ui

controllershelpersmodelsmiddlewares都十分清晰。這些文件夾裏的文件都應該與源被測試的文件夾中的文件一一對應,且名字相同。這樣更易於維護。

在上面這四個文件夾中,主要的測試代碼將是單元測試,這意味着你須要將被測試的代碼與應用分離開來。可是,integration文件夾內的代碼則主要用來測試你的各部分代碼是否被正確得粘合。例如,是否在正確的地方,調用了正確的中間件。這些代碼一般會比單元測試更慢一些。

ui文件夾內包含了則是UI測試,它與integration文件夾內的測試代碼類似,由於它們的目標都是保證各個部分被正確地粘合。可是,UI測試一般運行在瀏覽器上,而且還須要模擬用戶的操做。因此,一般它比集成測試更慢。

在前四個文件夾的測試代碼,一般都須要儘可能多包含各類邊際狀況。而集成測試則沒必要那麼細緻,你只需保證功能的正確性。UI測試也同樣,它也只須要保證每個UI組件都正確工做便可。

Other files

還剩下app.jspackage.json這兩個文件。

app.js是你應用的起始點。它加載其餘的一切,而後開始接收用戶的請求。

var express = require('express')
  , app = express()

app.engine('jade', require('jade').__express)
app.set('view engine', 'jade')

app.use(express.static(__dirname + '/public'))
app.use(require('./middlewares/users'))
app.use(require('./controllers'))

app.listen(3000, function() {
  console.log('Listening on port 3000...')
})

package.son文件的主要目的,則是記錄你的應用的各個依賴庫,以及它們的版本號。

{
  "name": "Comments App",
  "version": "1.0.0",
  "description": "Comments for everyone.",
  "main": "app.js",
  "scripts": {
    "start": "node app.js",
    "test": "node_modules/.bin/mocha tests/**"
  },
  "keywords": [
    "comments",
    "users",
    "node",
    "express",
    "structure"
  ],
  "author": "Terlici Ltd.",
  "license": "MIT",
  "dependencies": {
    "express": "^4.9.5",
    "jade": "^1.7.0"
  },
  "devDependencies": {
    "mocha": "^1.21.4",
    "should": "^4.0.4"
  }
}

你的應用也能夠經過正確地配置package.json,而後使用npm startnam test來啓動和測試你的代碼。詳情參閱:http://browsenpm.org/package.json

最後

原文連接:https://www.terlici.com/2014/08/25/best-practices-express-structure.html

相關文章
相關標籤/搜索