Best practices for Express app structure

Node和Express没有一个相对严格的文件或者是文件夹结构,因此你可以按照自己的想法来构建你的web项目,特别是对一些小的项目来说,他很容易学习。

然而当你的项目变得越来越大,所面临的情况越来越复杂的时候,你的代码将变得很混乱。特别是当你的团队变大的时候,将会很难基于以前的代码工作,你必须要经常处理代码之间的冲突。

为了能够添加一些新的特性,处理一些新的场景,你就需要改变你的代码结构了。更重要的是,现在有需要方式来组织你的文件和你的代码,但是很难选择那种结果适合你。

 

你想要有一个项目结构:不同的文件或者是文件夹负责不同的任务。你想要你的项目在多人使用时变得简单,相互之间有尽量少的代码冲突,你想要你的代码看上去清洁优雅,而且想要你的项目能够很容易的添加一些新的特性。

同样的问题我们也遇到了,现在这些都可以解决了,下面有一个很好的方式来构建你的项目,这种结构能够改变你的现状解决上述遇到的问题。

我们的这个结构以 Model-View-Controller (MVC) 这种设计模式为基础,这种模式能够很好的分离项目不同模块的职责,使得你的代码干净,可维护。

下面就让我们来看一下,我们是怎样通过Express这个web框架来扩展上面说的MCV模式的。我们不会讨论MVC模式的优点,我们将会集中精力基于Express来实现这个模式,同时我们也会看到的其他很好的实践。

我们已经把这些模式应用在了许多的项目上,无论是大项目还是小项目,都表现的很好。

Example

让我们来看下面的例子:在这个例子中用户能够登录,注册并且给所有的人留言。下面是这个项目的文件结构:

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

这什么鬼,看上去这么混乱与臃肿,但是别担心哥们,当你完全读完这篇文章之后,你就会完全理解他们每部分的功能了,事实上是很简单的。

让我们来看一下在项目的根目录下,每一个文件或者是文件夹是关于什么的简要解释:

  • controllers/ :定义你的项目的路由以及他的逻辑
  • helpers/ :在项目中不同模块中共享的代码或者是功能
  • middlewares/ :Express的中间件:对一个请求传给路由之前的处理
  • models/:代表数据处理,扩展的业务逻辑以及数据的存储
  • public/:包含所有的静态文件,包括图片,CSS样式,javascript代码
  • views/ :提供了被路由渲染的模版
  • tests/:测试,用来测是其他文件夹里的代码
  • app.js:初始并连接整个程序
  • package.json:记住你的程序中使用的所有倚赖包和他们的版本

把他放在中间来说,他的重要性不仅因为他是如何构建你的项目结构的,更主要是了解每个文件的职责,提供该外界的功能。

Models

模型是一个与你的数据库互动的模块,他提供了所有的方法和功能来处理你的数据。他们不仅提供了创建,读取,更新,删除的方法,而且包含了额外的业务逻辑。例如,你有一个汽车的模型,你就应该有一个换轮胎的方法。在你的数据库中对于不同的数据你应该创建至少一个文件。在我们的例子中,我们有用户,评论,因此我们有用户模型与评论模型。有时一个文件太大了,我们需要根据他的内部逻辑来拆分成不同的文件。

你应该尽可能的使你的模型独立与外部其他内容,他不需要知道其他模型的逻辑,而且也不要具有包含,引用关系。他也不需要关心是那个控制器使用了他。他应该永远也得不到请求和响应对象。他也应该永远不要返回http错误,但是他应该返回模型中指定的错误。

所有的这些将会使你的模型更加容易维护。由于他和其他的内容具有很少的依赖所以也很容易测试。如果有需要也可以很好的迁移,可以被任何其他人使用。改变这个模型将不会对其他内容产生影响。

让我们来看一下我们例子中的模型,在基于上面讨论的每个点上他是怎么做扩展的。下面是评论模型:

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

用户模型没有被包含,我们关心的唯一的事情就是谁使用了这个模块中关于用户登录验证的功能,他有可能使用用户id,用户名,或者是其他的东西。评论模块是不关心这些的,他只关心自己的数据处理。

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

除了创建与管理用户的功能之外,应该也会有用户的识别,密码的管理等方法。同样,这个模块也不会知道其他模块,控制器或者是程序的其他部分的存在。

Views

这个文件里面包含所有的被程序渲染的模版,这通常是你们team中设计师工作的场所了。你的模版文件应该有相应的子文件夹来和每一个控制器相对应。这样可以把相同任务的模版进行分组。。。

选择一个合适的模板语言是很难的,因为我们的选择太多了。我们喜欢和一直在用的模板语言是jadeMustache, jade在生成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

在这个文件里面将会定义你的程序提供的服务的所有路由。你的Controllers将会处理web请求,为用户提供模板,和你的models层在数据处理和获取数据上做交互。他就像胶水一样用来连接和控制你的 app

在你的程序中通常至少有一个文件来处理你的每一个逻辑部分,例如,一个文件用来处理评论的动作(comments action),其他的文件用来处理关于用户的请求等等。来自同一个控制(Controller)下的所有的路由使用同一个前缀,这是一个很好的实践。例如:/comments/all 和 /comments/new

有时候很难确定什么应该运行在控制器(Controller)里,什么应该在模型(model)里做操作。一个很好的实践就是,控制器应该不会直接操作数据库,他不会使用像"write","update","fatch"等这些数据库提供的方法。例如,如果你有一个car模型,你想要添加4个轮子给这个汽车模型,控制器将不会调用db.update(id, {wheels: 4}),但是他将会调用像这样的一个接口: car.mountWheels(id, 4)

下面是一个用于对评论响应的控制器。

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

在控制器文件中还有一个index.js文件。这个文件提供加载其他的控制器,可能会定义一些路径,这些路径像主页路由一样没有具体的统一前缀。例如:

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

这个路由文件,保存着所有的路由。在你的程序启动的时候这是唯一必须加载的路由。

Middlewares

在这个文件里面保存着所有的Express使用到的中间件,中间件的目的就是扩展统一控制的代码,他应该运行在多个请求中,通常修改请求或者是响应对象。 就像控制器一样,中间件也不要之间操作数据库,同样你应该是使用模型(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()
  }
}

这个中间件使用用户模型(model),并且从不会直接操作数据库。

下面,授权中间件,它用来当你在某些路由上想要保护不被授权许可。

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

他没有任何额外的依赖,如果你看了上面的控制机器文件,你就会知道他是怎么提供服务的。

Helpers

这个文件里面包括一些工具代码,这些代码被用在多个模型,中间件,控制器中。通常你会有不同的文件来处理不同的任务,一个例子就是helper文件,他提供了一些处理时间与日期的方法。

Public

这个文件下只是用来存放静态文件,通常他会有一些子文件例如:css,libs,img等,用来存储CSS样式,图片和一些JavaScript库比如jquery。一个很好的实践就是这个文件不仅给应用程序提供服务,而且为Nginx和Apache提供服务。

Tests

每一个项目都需要测试。你需要所有的测试一起运行。为了更好的管理他们,你将会把他们分到一些字文件中。

Other files

我们这个结构中的最后几个文件是app.js, package.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.json文件主要是用来记录你在程序中使用的依赖库以及他们的版本,他还有一些其他的功能,它可以允许你使用npm start来运行你的app,测试你的app使用npm test。

What’s next?

所有上面提到的可能是在Express下构建程序的做好实践方式,但是在新项目设置他们是一个繁琐的任务,并且这些项都很容易遗忘。为了帮助你构建,我们在github上,创建了一个仓库,这个仓库包含了上面我们所说的所有内容。你可以fork他们,克隆他们,立刻把他们应用到你的新项目中。更重要的是我们会对这个项目的依赖进行更新,希望你的程序一直使用最好的最新的模块,点击下面的连接关注: https://github.com/terlici/base-express

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

posted @ 2016-07-27 17:49  都市烟火  阅读(357)  评论(0编辑  收藏  举报