1. 引言
随着 Node.js 的发展,现在已经被很多人熟知, Node.js 已经成为了我们前端开发人员必备的技能。为了适应潮流和新技术的学习沉淀,以及和后端同事更好地交(si)流(bi),我们有必要对服务端开发进行一些了解。
本文将使用 express + mysql 来对一个博客系统服务端的开发全程进行介绍,主要包括:
- 服务端开发介绍
- Express介绍
- 如何写接口
- 如何进行数据存储
在开发实践中,本人分别使用原生 nodejs 和 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 脚手架,以及使用脚手架新建和启动一个项目
npm install express-generator -g
express project-name
npm install & npm start
脚手架搭建的项目中包括以下几个文件及文件夹:
- bin: 存放可执行文件,用来启动服务器
- node_modules
- public: 存放image、css、js等静态文件
- routes: 存放路由文件,响应客户端的网络请求
- views: 存放模板文件,文件格式为.jade
- app.js:入口文件
- package.json
这里重点介绍入口文件 app.js
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
// 引用路由
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var app = express();// 初始化,生成实例
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(logger('dev'));// 自动生成日志记录
app.use(express.json());// 封装了 post 请求数据为 json 格式时的处理方法,将数据保存在 req.body 中
app.use(express.urlencoded({ extended: false }));// 功能类似上一行,处理除 json 格式外的数据
app.use(cookieParser());// 解析 cookie
app.use(express.static(path.join(__dirname, 'public')));
// 注册、处理路由
app.use('/', indexRouter);
app.use('/users', usersRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'dev' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
3.3 Express 中间件机制
我们会发现在 app.js 入口文件中有很多 app.use,还有在路由文件中的 next 参数,这两个代表了中间件的使用方式。如下所示,这里 router.get() 中的第二个函数参数,即是中间件
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
此处待完善……
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 中来进行我们路由的注册
...
var indexRouter = require('./routes/index');
const blogRouter = require('./routes/blog');// 博文相关接口路由
const userRouter = require('./routes/user');// 用户相关接口路由
...
app.use 这里第一个参数为根路由,第二个参数为目标路由文件
...
app.use('/', indexRouter);
app.use('/api/blog', blogRouter);
app.use('/api/user', userRouter);
...
4.3 路由具体处理
这里我分别以获取某篇博客内容和登录接口为例,来说明 Get 和 Post 请求分别是如何进行处理。二者使用上的不同点,主要是取出入参的方式不同
// blog.js
var express = require('express');
var router = express.Router();
router.get('/detail', function(req, res, next) {
// 框架自带的逻辑,已完成了将路径参数保存在 req.query 中,我们即可以直接取出
const author = req.query.author || ''
const keyword = req.query.keyword || ''
res.json({
errno: 0,
data:['我是一个博客列表信息的假数据']
})
});
module.exports = router;
// user.js
var express = require('express');
var router = express.Router();
router.post('/login', function(req, res, next) {
// 框架自带的逻辑,已完成了将 post 请求的 data 保存在 req.body 中,我们即可以直接取出
const { username, password } = req.body
res.json({
errno: 0,
msg: '登录成功'
})
});
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. 创建数据库
CREATE DATABASE `myblog`; // 或 CREATE SCHEMA `myblog`;
// 2. 使用数据库
use myblog;
// 3. 创建表
CREATE TABLE `myblog`.`user` (
`id` INT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(20) NOT NULL,
`password` VARCHAR(20) NULL,
`realname` VARCHAR(10) NULL,
`usercol` VARCHAR(45) NULL,
PRIMARY KEY (`id`));
5.2.2 基本数据库操作
基本数据库操作,大体上可以简要概括为 Crud 四种,即增删改查
增
insert into users (username, `password`, realname) values ('malie', '123456', '马烈');
删
delete from users where username='malie';
改
update users set realname='马烈2' where username='malie';
查
select * from users; // 查询 users 表所有信息
select id, username from users; // 查询固定列
select * from users where username='malie'; // 附带条件的查询
select * from users where username like '%ma%'; // 模糊查询
select * from users where password like '%1%' order by id desc; // 带排序查询
问题补充
数据删改时安全模式问题
更新或删除数据可能会报如下安全模式错误:
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.
可使用如下语句解决:
SET SQL_SAFE_UPDATES = 0;
5.3 Node.js 连接 mysql 及应用到 API
首先在使用前自然要进行 mysql 依赖的下载
npm install mysql
5.3.1 如何连接Mysql,如何执行sql语句
const mysql = require('mysql')
// 创建链接对象,即连接数据的信息
const con = mysql.createConnection({
host: 'localhost',
user: 'root',
password: '',
port: '3306',
database: 'myblog'
})
// 开始连接
con.connect()
// 执行 SQL 语句
// 增
// const sql = `insert into users (username, password, realname) values ('malie', '123', '马烈');`
// 删
// const sql = `delete from users where username='malie';`
// 改
// const sql = `update users set realname='马烈2' where username='malie';`
// 查
const sql = `select * from users;`
con.query(sql, (err, result) => {
if (err) {
console.err(err)
return
}
console.log(result)
})
con.end()
返回结果示例及说明
// 增、删、改
// 新增重点关注新增数据的id
// 删除和修改重点关注影响的行数和更新的行数
{
fieldCount: 0,
affectedRows: 1, // 影响的行数
insertId: 4, // 新增数据的id
serverStatus: 2,
warningCount: 0,
message: '',
protocol41: true,
changedRows: 0 // 更新的行数
}
// 查
[{ id: 1, username: 'zhangsan', password: '123', realname: '张三' }, { id: 5, username: 'malie', password: '123', realname: '马烈' }]
5.3.2 如何在 API 中应用
1. 根据NODE_ENV区分配置
我们通常需要根据环境进行不同的数据库配置,我这里在 conf 文件夹中的 db.js 对其进行配置
const env = process.env.NODE_ENV // 环境参数
// 配置
let MYSQL_CONF
if (env === 'dev') {
MYSQL_CONF = {
host: 'localhost',
user: 'root',
password: '',
port: '3306',
database: 'myblog'
}
}
if (env === 'prod') {
MYSQL_CONF = {
host: 'http://117.48.197.41',
user: 'malie',
password: '',
port: '3306',
database: 'myblog'
}
}
module.exports = {
MYSQL_CONF
}
2. 封装 exec 函数,API使用 exec 操作数据库
由于每一个 API 都会进行数据库操作,我们可以封装一个函数方法统一对 sql 语句进行处理
// mysql.js
const mysql = require('mysql')
const { MYSQL_CONF } = require('../conf/db')
// 创建链接对象
const con = mysql.createConnection(MYSQL_CONF)
// 开始连接
con.connect()
// 统一执行 sql 的函数
function exec(sql) {
const promise = new Promise((resolve, reject) => {
con.query(sql, (err, result) => {
if (err) {
reject(err)
return
}
resolve(result)
})
})
return promise
}
module.exports = {
exec
}
3. 开发 controller 层
Controller 文件夹与路由文件夹 routes 平行,在同一目录下,并且与路由文件挨个对应。即路由中有一个 blog.js,在 controller 中也需要一个对应的 blog.js,这是为什么呢?它有什么用?
首先让我们来看一组例子,以获取博客列表为例:
路由文件
// blog.js
var express = require('express');
var router = express.Router();
const { getList } = require('../controller/blog') // 此处引入了在 controller 层中封装的 getList 方法
const { SuccessModel, ErrorModel } = require('../model/resModel')
router.get('/list', function(req, res, next) {
const author = req.query.author || ''
const keyword = req.query.keyword || ''
// 在 controller 层中执行相应的 sql 语句后返回的结果
const result = getList(author, keyword)
return result.then(listData => {
res.json(
new SuccessModel(listData)
)
})
});
module.exports = router;
Controller 层中文件
// blog.js
const { exec } = require('../db/mysql')
const getList = (author, keyword) => {
let sql = `select * from blogs where 1=1 `
if (author) {
sql += `and author = '${author}'`
}
if (keyword) {
sql += `and title like '%${keyword}%'`
}
sql += `order by createtime desc`
// 返回 promise
return exec(sql)
}
module.exports = {
getList,
}
通过以上两段代码,可以发现路由文件中专注于对路由进行处理,至于数据是如何处理获取、如何筛选以及数据的正确与否等均在 Controller 层中进行处理,即 Controller 只关心数据层面的问题。
换言之,对于一个庞大的项目,正是要使用类似的方式,对系统进行结构的划分,让开发人员在开发过程中能够更专注于某项的开发。这里仅举出获取博客列表接口完整开发的例子,其他例子大家可在源码中进行查看。
6. 总结
本次使用 Node.js 进行了一个博客系统服务端的开发,采用了 express + mysql 的技术栈,重点学习了解了一些服务端开发中的知识以及 Node.js 的实际应用。后续计划我将会继续优化登录注册接口,以及对服务端开发中日志、安全以及上线配置等知识继续进行学习实践和分享。
恳请大佬们批评指正,小弟提前拜谢,喵喵喵~