[译]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

相关文章
相关标签/搜索