# Node+Express服务搭建

用Node写项目就像手撸原生代码一样,模板框架什么的都需要自己写,而Express  (opens new window)为我们进一步封装和提供了一些中间件,使开发更快捷。其核心分为两大块路由和中间件,路由可以让我们更好的操作访问请求,中间件可以让我们更方便的处理请求和响应。

# 基础内容

在搭建Express服务时,我们先来了解一下Express的两大核心功能路由和中间件。

路由介绍

express实例为我们提供了响应客户端请求的方法,我们不用去写服务和一个个去区分路由了,直接调用方法去响应对应的路由。该方法派生自HTTP请求,所以和HTTP方法类似。

// app.js
var express = require('express')
var app = express()


// 响应首页
app.get('/', (req, res) => {
  res.send('Hello World!')
})


// 响应根路由(/)上的 POST 请求
app.post('/', (req, res) => {
  res.send('Got a POST request')
})


// 响应对 /user 路由的 PUT 请求
app.put('/user', (req, res) => {
  res.send('Got a PUT request at /user')
})
// 响应对 /user 路由的 DELETE 请求
app.delete('/user', (req, res) => {
  res.send('Got a DELETE request at /user')
})


// 无论使用 GET、POST、PUT、DELETE 还是任何其他 HTTP 请求方法,对 /secret 的请求都会执行以下回调
app.all('/secret', function (req, res, next) {
  console.log('Accessing the secret section ...')
  next()
})


app.listen(3000, function () {
  console.log('server is running')
})

中间件介绍

我们服务所有操作都是对各种请求进行不同的响应,所有操作都是围绕请求来的,所以,中间件的概念就是对这些请求进行拦截、过滤、修改、响应等操作进行封装成某些功能。就像TS的装饰器一样。

它是一个可以访问请求对象和响应对象的函数,中间件函数必须提供next函数。它的执行顺序很重要,先加载的中间件函数也先执行。

第三方中间件:Express middleware (opens new window)

// app.js
const express = require('express')
const app = express()


// 创建中间件
const myLogger = function (req, res, next) {
  console.log('LOGGED')
  next()
}
// 使用中间件
app.use(myLogger)


app.get('/', (req, res) => {
  res.send('Hello World!')
})


app.listen(3000)

app.use使用中间件函数,然后每个请求进来都会执行myLogger函数。

中间件有应用级中间件 (opens new window)路由级中间件 (opens new window)错误处理中间件 (opens new window)内置中间件 (opens new window)第三方中间件 (opens new window),我们可以根据不同的情况来使用对应的中间件。

# 项目创建

创建一个项目文件夹来搭建Node+Express服务,通过npm init来初始化项目,初始化好在该文件下创建一个app.js文件当做服务入口文件。然后在package.json文件中配置服务运行命令。

// package.json
{
  "name": "express_tem",
  "version": "1.0.0",
  "description": "express模板",
  "main": "index.js",
  "scripts": {
    "dev": "node app.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.21.0"
  }
}

我们在app.js中启动一个简单的服务

// app.js
var express = require('express')


var app = express()


// 路由
app.get('/', function (req, res) {
  res.send('index page')
})


app.listen(3000, function () {
  console.log('server is running')
})

然后,在终端执行下面命令服务就跑起来了。

npm run dev

# 项目结构设计

├── public                   # 资源文件
│   ├── css                  # css文件
│   ├── imgs                 # 图片
│   ├── js                   # js
│   └── lib                  # 资源文件
├── routes                   # 请求路由
│   ├── index.js             # 首页请求
│   └── mine.js              # 我的页面请求
├── views                    # 页面模板文件
│   ├── common               # 公共模板
│   │   ├── header.html      # 项目图片
│   │   └── footer.html      # 公共less/sass文件
│   ├── index.html           # 首页模板
│   ├── post.html            # 其他页面
│   └── 404.4html            # 404页面
├── .gitignore               # git忽略
├── app.js                   # 服务入口文件
├── nodemon.json             # 文件监听
├── package.json             # 包管理
├── README.md

# 动态监听文件

服务启动后,如果更改文件,需要重新执行启动命令才会生效,每次重启太麻烦了,我们使用nodemon插件来自动监听文件变化更新服务。

安装:

npm i nodemon -D

在根目录创建nodemon.json文件:

{
  "restartable": "rs",
  "ignore": [".git", ".svn", "node_modules/**/node_modules"],
  "verbose": true,
  "execMap": {
    "js": "node --harmony"
  },
  "watch": [],
  "env": {
    "NODE_ENV": "development"
  },
  "ext": "js json njk css js "
}

在package.json文件添加启动命令:

{
  "name": "node_tem",
  "version": "1.0.0",
  "description": "node项目",
  "main": "index.js",
  "scripts": {
    "start": "nodemon app.js",
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "art-template": "^4.13.2"
  },
  "devDependencies": {
    "nodemon": "^3.1.7"
  }
}


然后执行npm run start命令就可以自动监听文件改变了。

# 路由模块化

express为我们提供了内置路由中间件 express.Router() 来单独处理请求响应,我们根据单一职责把路由区分为一个个请求模块,每个模块负责专门的页面路由和接口请求处理。

A页面:

// routes/index.js


var express = require('express')
var router = express.Router()


router.get('/', function (req, res, next) {
  res.send('首页')
})


module.exports = router

B页面:

// routes/users.js


var express = require('express')
var router = express.Router()


router.get('/', function (req, res, next) {
  res.send('respond with a resource')
})


// 留言接口地址
router.post('/comments', function(req, res) {
  // ...
  res.redirect('/')
})


module.exports = router

在app.js进行引入添加:

// app.js
var express = require('express')
var path = require('path')
var app = express()


// 页面路由
var indexRouter = require('./routes/index')
var usersRouter = require('./routes/users')
// 路由
app.use('/', indexRouter)
app.use('/users', usersRouter)


app.listen(3000, function () {
  console.log('server is running')
})

在使用时是需要拼接前面一个模块的地址才是能请求:

<form action="/post/comments" method="POST">
  ...
  <button type="submit" class="btn btn-default">发表</button>
</form>

如果内容比较多,还可以在A页面或者B页面中继续拆分模块引入,增加嵌套层级,使用时把前面的层级地址拼上就可以了。

# 静态文件

获取服务静态文件,例如图片、CSS 文件和 JS文件,可以使用 Express 提供的内置中间件函数express.static来设置。

// 访问public下的静态文件
app.use(express.static('public'))


// 访问地址,无法显示public
http://localhost:3000/css/index.css
http://localhost:3000/image/logo.png

显示静态文件的虚拟路径:

app.use('/public', express.static('public'))


// 访问地址,显示public
http://localhost:3000/public/css/index.css

设置使用服务器的绝对根路径: express.static 直接使用的路径是相对于你启动 node 进程的目录的。如果从另一个目录运行 express 应用,要动态更改为服务器的绝对路径。

__dirname可以用来获取当前文件模块所属目录的绝对路径文件中操作的路径都是相对于node执行命令所在的路径所以,使用__dirname获取当前的绝对路径,无论在哪执行都可以使用

var express = require('express')
var path = require('path')


var app = express()


// 静态文件地址
app.use('/public', express.static(path.join(__dirname, 'public')))


app.listen(3000, function () {
  console.log('server is running')
})

# 模板引擎

Express使用的默认模板引擎是Pug,我们改为我们熟练的art-template模板引擎进行加载HTML文件。

安装

npm i art-template
npm i express-art-template

指定.html文件解析引擎

使用app.engine方法,指定html结尾的文件使用express-art-template插件进行解析。

app.engine('解析文件的后缀名', require('express-art-template'))

修改默认渲染路径

express-art-template 默认的渲染的目录是 views下面的,我们app.set设置目录为当前服务器的绝对路径。

app.set('views', path.join(__dirname, 'views'))

在app.js引入:

// app.js
var express = require('express')
var path = require('path')


var app = express()


// 引入模板引擎,指定html文件渲染插件
app.engine('html', require('express-art-template'))
app.set('views', path.join(__dirname, './views/')) // 模板引擎默认渲染目录


// 静态文件地址
app.use('/public', express.static(path.join(__dirname, 'public')))


app.listen(3000, function () {
  console.log('server is running')
})

在请求里渲染HTML页面

express 为 res 对象 提供了一个render方法渲染HTML页面,render默认不可用,但配置模板引擎就可以用了,第一个参数不能写路径,默认会找 views 目录中的模板文件,所以约定所有视图模板文件都放到views目中。第二个参数为模板数据,传入到模板HTML文件中使用。

// index


// render('html模板名’, {模板数据})


var express = require('express')
var router = express.Router()


router.get('/', function (req, res, next) {
  res.render('index.html', {
    title: '我是首页标题'
  })
})


module.exports = router

子模板

我们开发网站有很多公共部分,比如头部和底部这些公共组件,就可以创建子模板通过include来引入。新建header和footer文件,在需要的页面通过include引入。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>首页</title>
    <link rel="stylesheet" href="/public/css/reset.css" />
    <link rel="stylesheet" href="/public/lib/css/bootstrap.css" />
    <link rel="stylesheet" href="/public/css/index.css" />
  </head>
  <body>
    <div class="index">
      <!-- 头部 -->
      {{include './_partials/header.html'}}
      
      <div class="index-container"></div>
      
      <!-- 底部 -->
      {{include './_partials/footer.html'}}
    </div>
  </body>
</html>

模板继承

多个页面都引入头部和底部,只是中间的内容不同,这种重复的内容,就可以定义一个模板,

其他页面继承这个模板,只需要写中间不同的内容就行。相当于定义一个插槽组件,不同的地方通过插槽来填充。

先定义一个公共布局模板:

<!-- 公共布局模板 -->


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <!-- 标题插槽 -->
    <title>{{block 'title'}}默认标题{{/block}}</title>
    <link rel="stylesheet" href="/public/css/reset.css" />
    <link rel="stylesheet" href="/public/lib/css/bootstrap.css" />
    <!-- 头部插槽 -->
    {{block 'head'}}{{/block}}
  </head>
  <body>
    <div class="index">
      <!-- 头部 -->
      {{include './_partials/header.html'}}
      
      <!-- 内容插槽 -->
      {{block 'container'}}{{/block}}
      
      <!-- 底部 -->
      {{include './_partials/footer.html'}}
    </div>
    
    <script src="/public/lib/js/jquery.js"></script>
    <script src="/public/lib/js/bootstrap.js"></script>


    <!-- js插槽 -->
    {{block 'script'}}{{/block}}
  </body>
</html>

在首页中进行引入:

<!-- 模板继承 -->
{{extend './_layouts/home.html'}}


<!-- 标题 -->
{{block 'title'}}
  {{'首页'}}
{{/block}}


<!-- 头部插槽 -->
{{block 'head'}}
  <link rel="stylesheet" href="/public/css/index.css" />
{{/block}}


<!-- 内容插槽 -->
{{block 'container'}}
  <div class="index-container">
    <div class="container-wrap">
      <h1>{{title}}</h1>
    </div>
  </div>
{{/block}}


<!-- js插槽 -->
{{block 'script'}}
  <script>
    console.log(11111)
  </script>
{{/block}}

# 统一错误处理

// app.js
var express = require('express')
var app = express()


// 模板引擎引入...
// 路由引入...


// 路由错误处理,上面所有use方法都无法调用时,跳转到404页面
app.use(function(req, res) {
  res.render('404.html')
})


// 统一进行请求错误处理 4个参数
app.use(function(err, req, res, next) {
  console.log(err)
  res.status(500).json({
    err_code: 500,
    message: err.message
  })
})


app.listen(3000, function () {
  console.log('server is running')
})

# 请求数据处理

node请求是无法获取post请求体,我们使用中间件body-parser来解析http请求体,来获取post提交数据并返回json数据。

安装:

npm install body-parser --save

使用:

// app.js
var express = require('express')
var bodyParser = require('body-parser')
var app = express()


// 配置body-parser中间件
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())


// 模板引擎引入...
// 路由引入...
// 错误处理...


app.listen(3000, function () {
  console.log('server is running')
})

form表单提交和Ajax请求的区别:

from表单具有默认的提交行为,默认是同步的,同步表单提交,浏览器会锁死(转圈儿)等待服务端的响应结果。 表单的同步提交之后,无论服务端响应的是什么,都会直接把响应的结果覆盖掉当前页面。

为了解决这种问题,通过在提交表单后在服务器重定位到相同的页面,通过模板引擎将要提示的信息渲染到页面上(现在仍有网站使用这种方式),但这种效果不友好,页面会被刷新。

而ajax提交是异步进行,网页不需要刷新即可获取服务器响应数据,它不是通过表单提交,而是通过js的方法实现和浏览器表单无关。

# 使用Ajax请求

<!DOCTYPE html>
<html lang="en">
<body>
  <div class="main">
    <form id="login_form">
      <div class="form-group">
        <label for="">邮箱</label>
        <input type="email" class="form-control" id="" name="email" placeholder="Email" autofocus>
      </div>
      <div class="form-group">
        <label for="">密码</label>
        <a class="pull-right" href="">忘记密码?</a>
        <input type="password" class="form-control" id="" name="password" placeholder="Password">
      </div>
      <div class="checkbox">
        <label>
          <input type="checkbox">记住我
        </label>
      </div>
      <button type="submit" class="btn btn-success btn-block">登录</button>
    </form>
  </div>
  <script src="/public/lib/js/jquery.js"></script>
  <script>
    $('#login_form').on('submit', function (e) {
      e.preventDefault()
      var formData = $(this).serialize()
      console.log(formData)
      $.ajax({
        url: '/login',
        type: 'post',
        data: formData,
        dataType: 'json',
        success: function (data) {
          console.log(data)
        }
      })
    })
  </script>
</body>
</html>

# 连接MongoDB数据库

连接mongodb首先要安装mongodb软件启动mongodb服务或者使用云存储进行连接, 要先搭建服务器才行,不然无法进行连接和存储数据。这里建议使用云存储来连接,可以参考链接 MongoDB使用 来创建服务器。

mongodb服务启动后,或者云存储创建好后,我们就可以在node服务里进行连接和操作数据库了,在node中连接MongoDB数据库,一般使用第三方插件mongoose (opens new window) 对象文档模型(ODM)库来操作。在项目中创建一个models文件夹来处理数据库数据,在models下新建一个js文件,然后进行连接数据库和初始化表。

参考链接:mongoose文档 (opens new window)

1、连接数据库

// 引入mongoose包
var mongoose = require('mongoose');


// test数据库不需要存在,当插入第一条时会自动创建
mongoose.connect('mongodb://localhost/test');
// 或者连接云存储
mongoose.connect('mongodb+srv://xingchen:密码@mongotest.3kbmj.mongodb.net/?retryWrites=true&w=majority&appName=mongotest')


// 监听连接
const db = mongoose.connection


// 检查是否连接成功
db.on('error', function (error) {
  console.log('数据库连接失败:' + error)
})
db.on('open', function (error) {
  console.log('数据库连接成功')
})

数据库连接好后我们需要先创建表,把表设计好后就可以对数据进行操作增删改查了。创建表结构、发布表结构、操作表

2、设计表结构

通过mongoose.Schema设计一个名为userSchema的表

// 设计表结构
var userSchema = new mongoose.Schema({
  id: {
    type: String,
    required: true
  },
  // 约束
  username: {
    type: String,
    required: true
  },
  created_time: {
    type: Date,
    default: Date.now
  },
  status: {
    type: Number,
    // 0 没有权限限制
    // 1 不可以评论
    // 2 不可以登录
    enum: [0, 1, 2],
    default: 0
  }
});

3、将文档发布为模型

使用mongoose.model 方法就将设计的表结构发布为模型,第一个参数是传入大写名词单词字符串用来表示数据库名称,mongoose会自动将大写单词转化成小写负数的集合名称,例如: User最终会变为users集合;第二个参数:设计的表结构 Schema,然后会返回模型构造函数

// 发布模型
var User = mongoose.model('User', userSchema);

4、操作表

模型发布后后,我们就可以在路由模块中引入该模型对数据进行操作增删改查了。

添加数据:

// routes/user.js
var express = require('express')
var router = express.Router()


// 引入用户模型
var UserModel = require('../models/user')


// 添加用户
router.post('/addUser', function(req, res) {
  // 获取传入参数,根据User集合创建一个数据
  var admin = new UserModel(req.body)
  // 保存到数据库


  admin.save().then(function(data, err) {
    if (err) {
      return res.status(500).send('Server Error')
    }
    // res.redirect('/')
    // 返回给页面,让页面自己处理结果
    res.status(200).json({
      code: 200,
      message: '保存成功',
      data
    })
  })
})


查找数据:

使用User.find或者User.findOne, 第一个参数按条件查询,不加第一个参数查询所有。

// 用户列表页
router.get('/user', function (req, res, next) {
  // 从数据库中查找数据
  UserModel.find().then(function(data, err) {
    if (err) {
      return res.status(500).send('Server Error')
    }


    console.log('列表数据', data)
    // 返回数据
    res.render('index.html', {
      comments: data
    })
  })
})

删除数据:

使用User.deleteMany或者User.deleteOne,第一个参数按条件删除,不加第一个参数删除所有。

// 删除用户
router.post('/delUser', function(req, res) {
  var id = req.body.id
  UserModel.deleteOne({
    id: id
  }).then(function(data, err) {
    if (err) {
      return res.render('Not Found File')
    }
    res.status(200).json({
      code: 200,
      message: '删除成功',
      data
    })
  })
})

更新数据:

User.findByIdAndUpdate,第一个参数根据id更新数据,第二个参数更新的对象。

// 更新用户数据
router.post('/updateUser', function(req, res) {
  var id = req.body.id
  var name = req.body.name


  UserModel.findByIdAndUpdate(id, {
    username: name
  }).then(function(data, err) {
    if (err) {
      return res.render('Not Found File')
    }
    res.status(200).json({
      code: 200,
      message: '更新成功',
      data
    })
  })
})

# 部署到服务器

1、安装node

在服务器上准备好node环境,参考服务器使用教程

2、安装mongodb

在服务器上安装mongodb数据库,按照官网给的步骤来,在 Ubuntu 上安装 MongoDB (opens new window),安装好后执行下面命令启动mongodb服务。

sudo systemctl start mongod

验证 MongoDB 是否已成功启动

sudo systemctl status mongod

3、部署代码

将node+express整个项目代码上传到/www/wwwroot/目录下,执行npm i安装好依赖包。

4、运行服务

在项目目录下使用pm2工具持久化运行服务

# 执行
pm2 app.js


# 或者运行npm命令
pm2 start npm --name express -- run start

如果有端口冲突可以通过top获取端口的pid,然后通过kill命令杀掉对应端口进程,然后在重新启动。

# 项目地址

Node+Express服务项目 (opens new window)