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 -gexpress project-namenpm 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 setupapp.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());// 解析 cookieapp.use(express.static(path.join(__dirname, 'public')));// 注册、处理路由app.use('/', indexRouter);app.use('/users', usersRouter);// catch 404 and forward to error handlerapp.use(function(req, res, next) {next(createError(404));});// error handlerapp.use(function(err, req, res, next) {// set locals, only providing error in developmentres.locals.message = err.message;res.locals.error = req.app.get('env') === 'dev' ? err : {};// render the error pageres.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.jsvar 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.jsvar express = require('express');var router = express.Router();router.post('/login', function(req, res, next) {// 框架自带的逻辑,已完成了将 post 请求的 data 保存在 req.body 中,我们即可以直接取出const { username, password } = req.bodyres.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, // 新增数据的idserverStatus: 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_CONFif (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.jsconst 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.jsvar 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.jsconst { 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`// 返回 promisereturn exec(sql)}module.exports = {getList,}
通过以上两段代码,可以发现路由文件中专注于对路由进行处理,至于数据是如何处理获取、如何筛选以及数据的正确与否等均在 Controller 层中进行处理,即 Controller 只关心数据层面的问题。
换言之,对于一个庞大的项目,正是要使用类似的方式,对系统进行结构的划分,让开发人员在开发过程中能够更专注于某项的开发。这里仅举出获取博客列表接口完整开发的例子,其他例子大家可在源码中进行查看。
6. 总结
本次使用 Node.js 进行了一个博客系统服务端的开发,采用了 express + mysql 的技术栈,重点学习了解了一些服务端开发中的知识以及 Node.js 的实际应用。后续计划我将会继续优化登录注册接口,以及对服务端开发中日志、安全以及上线配置等知识继续进行学习实践和分享。
恳请大佬们批评指正,小弟提前拜谢,喵喵喵~
