1. 引言

随着 Node.js 的发展,现在已经被很多人熟知, Node.js 已经成为了我们前端开发人员必备的技能。为了适应潮流和新技术的学习沉淀,以及和后端同事更好地交(si)流(bi),我们有必要对服务端开发进行一些了解。

本文将使用 express + mysql 来对一个博客系统服务端的开发全程进行介绍,主要包括:

  • 服务端开发介绍
  • Express介绍
  • 如何写接口
  • 如何进行数据存储

在开发实践中,本人分别使用原生 nodejs 和 express 框架都进行了一次开发

  1. 点击这里,可查看原生 nodejs 版本代码
  2. 点击这里,可查看 express 版本代码

可演示的页面部署版本,将在后续补上

2. 服务端开发介绍

服务端开发相较于前端开发有以下几点不同:

2.1 服务稳定性

  • 服务端可能会遭受各种恶意攻击和误操作
  • 单个客户端可以意外挂掉,但服务端不能

2.2 考虑内存和CPU

  • 客户端独占一个浏览器,内存和CPU都不是问题
  • 服务端要承载很多请求,内存和CPU都是稀缺资源

2.3 日志记录

  • 服务端需要记录日志、存储日志、分析日志

2.4 安全

  • 服务端要随时准备接受各种恶意攻击,如越权操作、SQL注入、XSS攻击等

2.5 集群和服务拆分

  • 随着产品的发展,当需要承载越来越大的客户流量时

后续将会在实践中各自讲解在服务端开发中如何解决这些问题

3. Express介绍

Express 的介绍就不说过多,它是 Node.js 上最流行的 Web 开发框架,使用它我们可以快速的开发一个 Web 应用

3.1 Express 快速入门

大家可以点击这里,前往官网查看简单的快速上手及大家耳熟能详的 Hello World

3.2 Express 脚手架使用及文件目录介绍

通过以下的命令即可安装 express 脚手架,以及使用脚手架新建和启动一个项目

  1. npm install express-generator -g
  2. express project-name
  3. npm install & npm start

脚手架搭建的项目中包括以下几个文件及文件夹:

  • bin: 存放可执行文件,用来启动服务器
  • node_modules
  • public: 存放image、css、js等静态文件
  • routes: 存放路由文件,响应客户端的网络请求
  • views: 存放模板文件,文件格式为.jade
  • app.js:入口文件
  • package.json

这里重点介绍入口文件 app.js

  1. var createError = require('http-errors');
  2. var express = require('express');
  3. var path = require('path');
  4. var cookieParser = require('cookie-parser');
  5. var logger = require('morgan');
  6. // 引用路由
  7. var indexRouter = require('./routes/index');
  8. var usersRouter = require('./routes/users');
  9. var app = express();// 初始化,生成实例
  10. // view engine setup
  11. app.set('views', path.join(__dirname, 'views'));
  12. app.set('view engine', 'jade');
  13. app.use(logger('dev'));// 自动生成日志记录
  14. app.use(express.json());// 封装了 post 请求数据为 json 格式时的处理方法,将数据保存在 req.body 中
  15. app.use(express.urlencoded({ extended: false }));// 功能类似上一行,处理除 json 格式外的数据
  16. app.use(cookieParser());// 解析 cookie
  17. app.use(express.static(path.join(__dirname, 'public')));
  18. // 注册、处理路由
  19. app.use('/', indexRouter);
  20. app.use('/users', usersRouter);
  21. // catch 404 and forward to error handler
  22. app.use(function(req, res, next) {
  23. next(createError(404));
  24. });
  25. // error handler
  26. app.use(function(err, req, res, next) {
  27. // set locals, only providing error in development
  28. res.locals.message = err.message;
  29. res.locals.error = req.app.get('env') === 'dev' ? err : {};
  30. // render the error page
  31. res.status(err.status || 500);
  32. res.render('error');
  33. });
  34. module.exports = app;

3.3 Express 中间件机制

我们会发现在 app.js 入口文件中有很多 app.use,还有在路由文件中的 next 参数,这两个代表了中间件的使用方式。如下所示,这里 router.get() 中的第二个函数参数,即是中间件

  1. /* GET home page. */
  2. router.get('/', function(req, res, next) {
  3. res.render('index', { title: 'Express' });
  4. });

此处待完善……

4. 如何写接口

写接口主要包括接口设计和接口处理,接口处理即是处理我们的路由,主要分为路由注册和路由的具体处理

4.1 接口设计

这里我简单用一个表格描述了一下主要接口的基本信息

描述 接口 方法 url参数 备注
获取博客列表 /api/blog/list get author: 作者,keyword: 搜索关键字 参数可为空
获取某篇博客内容 /api/blog/detail get id
新增博客 /api/blog/new post title, content, author postData 中有新增的信息
更新博客 /api/blog/update post id, title, content, author postData 中有更新的内容
删除博客 /api/blog/del post id, title, content, author
登录 /api/user/login post username, password postData 中有用户名、密码
注册 /api/user/register post postData 中有用户信息

4.2 路由注册

首先在 app.js 中来进行我们路由的注册

  1. ...
  2. var indexRouter = require('./routes/index');
  3. const blogRouter = require('./routes/blog');// 博文相关接口路由
  4. const userRouter = require('./routes/user');// 用户相关接口路由
  5. ...

app.use 这里第一个参数为根路由,第二个参数为目标路由文件

  1. ...
  2. app.use('/', indexRouter);
  3. app.use('/api/blog', blogRouter);
  4. app.use('/api/user', userRouter);
  5. ...

4.3 路由具体处理

这里我分别以获取某篇博客内容和登录接口为例,来说明 Get 和 Post 请求分别是如何进行处理。二者使用上的不同点,主要是取出入参的方式不同

  1. // blog.js
  2. var express = require('express');
  3. var router = express.Router();
  4. router.get('/detail', function(req, res, next) {
  5. // 框架自带的逻辑,已完成了将路径参数保存在 req.query 中,我们即可以直接取出
  6. const author = req.query.author || ''
  7. const keyword = req.query.keyword || ''
  8. res.json({
  9. errno: 0,
  10. data:['我是一个博客列表信息的假数据']
  11. })
  12. });
  13. module.exports = router;
  1. // user.js
  2. var express = require('express');
  3. var router = express.Router();
  4. router.post('/login', function(req, res, next) {
  5. // 框架自带的逻辑,已完成了将 post 请求的 data 保存在 req.body 中,我们即可以直接取出
  6. const { username, password } = req.body
  7. res.json({
  8. errno: 0,
  9. msg: '登录成功'
  10. })
  11. });
  12. module.exports = router;

5. 如何进行数据存储

谈到数据存储,自然离不开我们的数据库,这里我使用的是 mysql 数据库。这一小节主要内容为介绍以下三点:

  • 如何进行表结构设计
  • 如何使用和操作 mysql 数据库
  • node.js 如何连接数据库

5.1 表结构设计

本项目暂时主要涉及一个用户 users 表,和博客的 blogs 表

5.1.1 Users 表

column datatype pk(主键) nn(不为空) AI(自动增加) Default
id int Y Y Y
username varchar(20) Y
password varchar(20) Y
realname varchar(10) Y

5.1.2 Blogs 表

column datatype pk(主键) nn(不为空) AI(自动增加) Default
id int Y Y Y
title varchar(50) Y
content longtext Y
createtime bigint(20) Y 0
author varchar(20) Y

5.2 Mysql 数据库基本知识

首先需要安装 mysql 以及 workbench(其他的数据库可视化客户端也行),这里对于 mysql 的安装和配置比较容易,网上也很多手把手教程指南,不多作介绍。本小节主要介绍如何建库和建表,以及基本的数据库操作

5.2.1 Mysql 建库和建表

  1. // 1. 创建数据库
  2. CREATE DATABASE `myblog`; // 或 CREATE SCHEMA `myblog`;
  3. // 2. 使用数据库
  4. use myblog;
  5. // 3. 创建表
  6. CREATE TABLE `myblog`.`user` (
  7. `id` INT NOT NULL AUTO_INCREMENT,
  8. `username` VARCHAR(20) NOT NULL,
  9. `password` VARCHAR(20) NULL,
  10. `realname` VARCHAR(10) NULL,
  11. `usercol` VARCHAR(45) NULL,
  12. PRIMARY KEY (`id`));

5.2.2 基本数据库操作

基本数据库操作,大体上可以简要概括为 Crud 四种,即增删改查

  1. insert into users (username, `password`, realname) values ('malie', '123456', '马烈');

  1. delete from users where username='malie';

  1. update users set realname='马烈2' where username='malie';

  1. select * from users; // 查询 users 表所有信息
  2. select id, username from users; // 查询固定列
  3. select * from users where username='malie'; // 附带条件的查询
  4. select * from users where username like '%ma%'; // 模糊查询
  5. select * from users where password like '%1%' order by id desc; // 带排序查询

问题补充

数据删改时安全模式问题

更新或删除数据可能会报如下安全模式错误:

  1. Error Code: 1175. You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column To disable safe mode, toggle the option in Preferences -> SQL Editor and reconnect.

可使用如下语句解决:

  1. SET SQL_SAFE_UPDATES = 0;

5.3 Node.js 连接 mysql 及应用到 API

首先在使用前自然要进行 mysql 依赖的下载

  1. npm install mysql

5.3.1 如何连接Mysql,如何执行sql语句

  1. const mysql = require('mysql')
  2. // 创建链接对象,即连接数据的信息
  3. const con = mysql.createConnection({
  4. host: 'localhost',
  5. user: 'root',
  6. password: '',
  7. port: '3306',
  8. database: 'myblog'
  9. })
  10. // 开始连接
  11. con.connect()
  12. // 执行 SQL 语句
  13. // 增
  14. // const sql = `insert into users (username, password, realname) values ('malie', '123', '马烈');`
  15. // 删
  16. // const sql = `delete from users where username='malie';`
  17. // 改
  18. // const sql = `update users set realname='马烈2' where username='malie';`
  19. // 查
  20. const sql = `select * from users;`
  21. con.query(sql, (err, result) => {
  22. if (err) {
  23. console.err(err)
  24. return
  25. }
  26. console.log(result)
  27. })
  28. con.end()

返回结果示例及说明

  1. // 增、删、改
  2. // 新增重点关注新增数据的id
  3. // 删除和修改重点关注影响的行数和更新的行数
  4. {
  5. fieldCount: 0,
  6. affectedRows: 1, // 影响的行数
  7. insertId: 4, // 新增数据的id
  8. serverStatus: 2,
  9. warningCount: 0,
  10. message: '',
  11. protocol41: true,
  12. changedRows: 0 // 更新的行数
  13. }
  14. // 查
  15. [{ id: 1, username: 'zhangsan', password: '123', realname: '张三' }, { id: 5, username: 'malie', password: '123', realname: '马烈' }]

5.3.2 如何在 API 中应用

1. 根据NODE_ENV区分配置

我们通常需要根据环境进行不同的数据库配置,我这里在 conf 文件夹中的 db.js 对其进行配置

  1. const env = process.env.NODE_ENV // 环境参数
  2. // 配置
  3. let MYSQL_CONF
  4. if (env === 'dev') {
  5. MYSQL_CONF = {
  6. host: 'localhost',
  7. user: 'root',
  8. password: '',
  9. port: '3306',
  10. database: 'myblog'
  11. }
  12. }
  13. if (env === 'prod') {
  14. MYSQL_CONF = {
  15. host: 'http://117.48.197.41',
  16. user: 'malie',
  17. password: '',
  18. port: '3306',
  19. database: 'myblog'
  20. }
  21. }
  22. module.exports = {
  23. MYSQL_CONF
  24. }

2. 封装 exec 函数,API使用 exec 操作数据库

由于每一个 API 都会进行数据库操作,我们可以封装一个函数方法统一对 sql 语句进行处理

  1. // mysql.js
  2. const mysql = require('mysql')
  3. const { MYSQL_CONF } = require('../conf/db')
  4. // 创建链接对象
  5. const con = mysql.createConnection(MYSQL_CONF)
  6. // 开始连接
  7. con.connect()
  8. // 统一执行 sql 的函数
  9. function exec(sql) {
  10. const promise = new Promise((resolve, reject) => {
  11. con.query(sql, (err, result) => {
  12. if (err) {
  13. reject(err)
  14. return
  15. }
  16. resolve(result)
  17. })
  18. })
  19. return promise
  20. }
  21. module.exports = {
  22. exec
  23. }

3. 开发 controller 层

Controller 文件夹与路由文件夹 routes 平行,在同一目录下,并且与路由文件挨个对应。即路由中有一个 blog.js,在 controller 中也需要一个对应的 blog.js,这是为什么呢?它有什么用?

首先让我们来看一组例子,以获取博客列表为例:

路由文件

  1. // blog.js
  2. var express = require('express');
  3. var router = express.Router();
  4. const { getList } = require('../controller/blog') // 此处引入了在 controller 层中封装的 getList 方法
  5. const { SuccessModel, ErrorModel } = require('../model/resModel')
  6. router.get('/list', function(req, res, next) {
  7. const author = req.query.author || ''
  8. const keyword = req.query.keyword || ''
  9. // 在 controller 层中执行相应的 sql 语句后返回的结果
  10. const result = getList(author, keyword)
  11. return result.then(listData => {
  12. res.json(
  13. new SuccessModel(listData)
  14. )
  15. })
  16. });
  17. module.exports = router;

Controller 层中文件

  1. // blog.js
  2. const { exec } = require('../db/mysql')
  3. const getList = (author, keyword) => {
  4. let sql = `select * from blogs where 1=1 `
  5. if (author) {
  6. sql += `and author = '${author}'`
  7. }
  8. if (keyword) {
  9. sql += `and title like '%${keyword}%'`
  10. }
  11. sql += `order by createtime desc`
  12. // 返回 promise
  13. return exec(sql)
  14. }
  15. module.exports = {
  16. getList,
  17. }

通过以上两段代码,可以发现路由文件中专注于对路由进行处理,至于数据是如何处理获取、如何筛选以及数据的正确与否等均在 Controller 层中进行处理,即 Controller 只关心数据层面的问题。

换言之,对于一个庞大的项目,正是要使用类似的方式,对系统进行结构的划分,让开发人员在开发过程中能够更专注于某项的开发。这里仅举出获取博客列表接口完整开发的例子,其他例子大家可在源码中进行查看。

6. 总结

本次使用 Node.js 进行了一个博客系统服务端的开发,采用了 express + mysql 的技术栈,重点学习了解了一些服务端开发中的知识以及 Node.js 的实际应用。后续计划我将会继续优化登录注册接口,以及对服务端开发中日志、安全以及上线配置等知识继续进行学习实践和分享。

恳请大佬们批评指正,小弟提前拜谢,喵喵喵~