本文通过以node.js为基础借助Koa框架模拟一个服务器,手把手搭建一个小型商城案例来梳理前后端交互的基本流程
前瞻回顾
koa是基于一种框架工具,详情可参考https://github.com/demopark/koa-docs-Zh-CNkoa-static-cache是用来设置静态文件资源代理的,详情可参考官网https://www.npmjs.com/package/koa-static-cache。
const app = new Koa();
//本商城的静态文件代理目录为public
app.use(KoaStaticCache('./public', {
prefix: '/public', //你希望代理的静态文件目录
gzip: true, //文件将被gzip压缩
dynamic: true //在初始化时不缓存的动态加载文件
}));
koa-router可以用来处理动态路由。
// 动态资源处理 koa-router
const router = new KoaRouter();
例如:
//http://localhost:8888/1 //手机页
//http://localhost:8888/2 //笔记本页
//http://localhost:8888/3 //电视机页
router.get('/:id(\\d*)', async ctx => {
...
})
app.use(router.routes());
app.listen(8888);
koa-body是对从服务器中post请求到服务器的数据进行封装以便使用的中间件。详情可参考官网https://www.npmjs.com/package/koa-body
//如果服务器通过post请求上传的是二进制数据(例如文件、图片等)的话需要如下配置
router.post('/upload', koaBody({
//设置koa-body能够解析enctype="multipart/form-data"的格式数据
multipart:true,
formidable:{
//上传的二进制文件存储在文件的位置
//上传后自动存储在执行目录下
uploadDir:'./public/attachments',
keepExtensions:true //保持文件拓展名(例如.jpg、.png等)
}
}),async ctx => {
...
})
ctx.request.body
=> koaBody 中间件解析请求正文后普通数据存储的位置
ctx.request.files
=> koaBody 中间件解析请求正文后二进制数据(文件、图片)存储的位置
mysql2可以用来操纵数据库,详情可参考官网https://www.npmjs.com/package/mysql2。
// 创建数据库连接
const connection = mysql2.createConnection({
host: '127.0.0.1',
user: 'xxx',
password: 'xxx',
database: 'xxx'
});
//封装一个异步操纵数据库的方法
function query(sql, prePared) {
return new Promise((resolve, reject) => {
connection.query(
sql,
prePared,
function (err, results, fields) {
if (err) {
reject(err);
} else {
resolve([results, fields]);
}
}
)
});
}
nunjucks是用来进行页面模板渲染的。详情可参考官网https://nunjucks.bootcss.com/
// 配置模板引擎--这里指定了模板都在templates目录下
nunjucks.configure('./templates', {
watch: true,
noCache: true
});
例如要渲染./templates/index.html可以使用
nunjucks.render('index.html', {data:data});
parse-filepath可以用来解析文件名。详情可以参考官网https://www.npmjs.com/package/parse-filepath。
例如我们需要解析下面的数据得到upload_3cbbc47b15e1f555d8a0a1f9479dcc34.jpg
==>path:'public\\attachments\\upload_3cbbc47b15e1f555d8a0a1f9479dcc34.jpg'
可以使用即可获得
=>console.log(parsePath(path))
cookie-parse可以用来解析cookie。详情可参考官网https://www.npmjs.com/package/cookie-parse。
例如我们可以获得cookie的数据但不是我们想要的数据格式
id=1
可以使用
cookieParse.parse(ctx.get('Cookie')) //{ id: '1' }
- 本文的小型商城案例需要依靠这些工具
const Koa = require('koa');
//静态资源目录
const KoaStaticCache = require('koa-static-cache');
//动态理由
const KoaRouter = require('koa-router');
//模板渲染
const nunjucks = require('nunjucks');
//nodejs操纵数据库
const mysql2 = require('mysql2');
//对post请求过来的数据进行封装
const koaBody = require('koa-body');
//解析文件名
const parsePath = require('parse-filepath');
//解析cookie
const cookieParse = require('cookie-parse');
小型商城案例的首页
- 实现思路流程:
- 1.我们访问127.0.0.1时默认分页显示数据库中所有的商品数据
- 2.我们访问127.0.0.1/1时显示类别为手机(数据库中id为1)的商品数据,我们访问127.0.0.1/2时显示类别为笔记本(数据库中id为2)的商品数据,我们访问127.0.0.1/3时显示类别为电视机(数据库中id为3)的商品数据。
- 3.我们访问127.0.1.1/?page=2显示商品数据(所有数据库商品数据)页中第二页的商品数据内容。我们访问127.0.1.1/1?page=2显示类别为手机的第二页商品数据。
- 借助nunjucks模板实现HTML页面之首页的渲染
{% extends "base.html" %}
{% block main %}
<ul class="items-list">
{% for item in items %}
<li class="panel">
<img src="/public/attachments/{{item.cover}}" alt="" class="cover">
<div class="name">{{item.name}}</div>
<div class="price">¥ {{(item.price).toFixed(2)}}</div>
</li>
{% endfor %}
</ul>
<div class="pagination-container">
<div class="pagination">
<a href="" class="prev">上一页</a>
<!-- pages是总共有几页(左闭右包) -->
{% for p in range(1, pages+1) %}
{% if p == page %}
<a href="" class="current">{{p}}</a>
{% else %}
<a href="?page={{p}}">{{p}}</a>
{% endif %}
{% endfor %}
<a href="" class="next">下一页</a>
</div>
</div>
{% endblock %}
- 借助路由和mysql实现动态页面之首页的逻辑代码
router.get('/:id(\\d*)', async ctx => {
// 1、获取当前页面所需要的动态数据:
// 商品分类数据:categories
// 商品列表数据:items
let categories = [];
// 2、获取当前页面动态路由的数据
// 对象中的key就是动态路由:后面的名称
// 127.0.0.1/1 其中1通过ctx.params.id获取
let categoryId = ctx.params.id;
// 3、 获取当前页面query的数据
// ctx.request.query 存储了url中?后面的内容
// 127.0.0.1/1?page=1 其中1通过ctx.request.query.page获取
let page = ctx.request.query.page;
//如果不存在指定分页则显示第一页
if (!page) {
page = 1;
}
// 每页显示的商品数据条数
// 这里为一页两条数据
let prepage = 2;
// 偏移量--用以数据库查询
// 即第三页的偏移量为4
let offset = (page - 1) * prepage;
// 查询所有商品数据的总条数
let sqlCount = 'SELECT count(id) as count FROM `items`';
// 解析总共查询到多少商品
let [
[{
count
}]
] = await query(sqlCount);
//pages是总共有几页
//依据数据库中的商品总数得出总页面数
let pages = Math.ceil((count / prepage));
//默认是以全部为目标来查询数据库中的商品数据
let sql = 'SELECT * FROM `items` limit ' + prepage + ' offset ' + offset;
//如果127.0.1.1/1?page=1则增加查询条件
//是以商品类目来查询数据库中的商品数据
if (categoryId) {
//如果存在类目id即/1或/2或/3则增加where语句
sql = 'SELECT * FROM `items` where `category_id`=? limit '+ prepage + ' offset ' + offset;
}
//首页即127.0.1.1默认是查询出数据库中所有的商品数据并渲染
[categories] = await query('SELECT * FROM `categories`');
// console.log([categories], '1111');
/**[categories]
* [ [ TextRow { name: '手机', id: 1 },
TextRow { name: '笔记本', id: 2 },
TextRow { name: '电视机', id: 3 } ] ]
*/
[items] = await query(sql, [categoryId]);
//最后通过后端模板引擎对数据和模板文件进行渲染
//得到最终返回给前端的页面
ctx.body = nunjucks.render('index.html', {
categories,
items,
pages,
page
});
});
小型商城案例的添加商品页
- 实现思路流程:
- 1.我们访问127.0.0.1/addItem页面时进入添加商品页,我们需要通过在提交给后端的信息有商品类别、商品名称、商品价格、商品图片(上传功能)。
- 2.我们需要上传图片这种数据需要用到HTML表单的数据格式即entype=multipart/form-data和HTML表单的name属性,这个属性是用来设置提交数据的key。
- 3.在HTML表单提交这段代码时:
<input type="file" name="cover" class="form-input">,提交到了服务器中的这个input中的value值里面存储的是这个上传时你文件的文件名称,而不是文件的二进制数据。即如果编码格式是enctype="application/x-www-form-urlencoded",那么发送的是像categoryId=1&name=aaa&price=69900&cover=0c36b11834fa2ce5.jpg这种格式的。
- 借助nunjucks模板实现HTML页面之添加商品页的渲染
{% extends "base.html" %}
{% block main %}
<div id="login" class="panel">
<h2>用户id为{{uid}} | 请你添加商品</h2>
<!--
如果表单中支持:
application/x-www-form-urlencoded:表单默认:url编码
multipart/form-data:formdata格式:二进制
text/plain:纯文本
-->
<!--
如果action=''留空则会把当前的页面做为提交的页面
即这里的post请求发送给了/addItem.html页面
-->
<!--
当我们把表单的 enctype="application/x-www-form-urlencoded"
浏览器(表单)提交的是表单控件中有name属性的value值
如果是file类型且提交的是文件是二进制数据
那么需要把 表单的 enctype 设置为:multipart/form-data
-->
<form action="" method="POST" enctype="multipart/form-data">
<div class="form-item">
<label>
<span class="txt">商品类别:</span>
<select name="categoryId">
<option value="">请选择一个类别</option>
{% for category in categories %}
<option value="{{category.id}}">{{category.name}}</option>
{% endfor %}
</select>
</label>
</div>
<div class="form-item">
<label>
<span class="txt">商品名称</span>
<input type="text" name="name" class="form-input">
</label>
</div>
<div class="form-item">
<label>
<span class="txt">商品价格</span>
<input type="text" name="price" class="form-input">
</label>
</div>
<div class="form-item">
<label>
<span class="txt">商品图片</span>
<input type="file" name="cover" id="" />
</label>
</div>
<div class="form-item">
<label>
<span class="txt"></span>
<button class="form-button primary">提交</button>
</label>
</div>
</form>
</div>
{% endblock %}
- 借助路由和mysql实现动态页面之添加商品页的逻辑代码
//vertify是用来利用cookie实现用户登录验证的自定义中间价
//通过get方式访问并返回一个添加新商品的页面
//通过点击添加商品按钮跳转过来的添加商品页面
//<option value="{{category.id}}">{{category.name}}</option>
//<option value="">请选择一个类别</option>
//在option中的value有两种可能一种是''(空字符),一种是1,2,3(即商品类别id)
router.get('/addItem', verify,async ctx => {
[categories] = await query('SELECT * FROM `categories`');
ctx.body = nunjucks.render('addItem.html', {
categories,
uid:ctx.state.uid
});
});
//post提交过来的数据进行处理
//相当于拦截这个页面的post请求
// post提交过来的数据进行处理
// 相当于拦截这个页面的post请求
router.post('/addItem', koaBody({
//设置koa-body能够解析enctype="multipart/form-data"的格式数据
multipart:true,
//设置上传的二进制文件的处理
formidable:{
//上传的二进制文件存储在文件的位置
//上传后的文件名称是随机命名的
//上传后文件名称尽量不要使用上传之前的原始文件的名称
//因为会有覆盖的问题:c盘:1.jpg d盘: 1.jpg
uploadDir:'./public/attachments',
//保持文件原来的拓展名
keepExtensions:true
}
}), async ctx => {
/**
* 数据 { categoryId: '1',
name: '荣耀20 PRO',
price: '229900',
cover: '62cf191f88e41447.jpg' }
*/
//改成了enctype="multipart/form-data"后在提交时报错
//报错代码为:Error: Column 'category_id' cannot be null
let {
categoryId,
name,
price
} = ctx.request.body;
//在koa-body中设置{multipart:true}除了cover获取不到其他数据都没问题了
// console.log(cover,'cover在ctx.request.files里面');
// console.log(ctx.request.files,'我们想要的cover在这里');
//数据库存储的是文件上传以后在服务器的名字
//即在path中需要通过解析才能拿到
// path:'public\\attachments\\upload_3cbbc47b15e1f555d8a0a1f9479dcc34.jpg',
// 可以使用第三方库帮忙解析其中的对象中的base属性就是我们想要的
// console.log(parsePath('public\\attachments\\upload_3cbbc47b15e1f555d8a0a1f9479dcc34.jpg'));
// 结果为upload_9ed2c3d065aee76b553efe616e36dd8c.jpg
let {base:cover}=parsePath(ctx.request.files.cover.path)
// 对数据进行合法性验证
// 验证通过,存储到数据库
// 解构获取结果中的第一个数组数据
//预查询
let [rs] = await query(
"INSERT INTO `items` (`category_id`, `name`, `price`, `cover`) VALUES (?, ?, ?, ?)",
[categoryId, name, price, cover]
);
let [categories] = await query('SELECT * FROM `categories`');
ctx.body = nunjucks.render('message.html', {
categories,
message:'<p>添加成功</p><p><hr/></a><a href="/addItem">继续添加商品</a> | <a href="/">回到首页</a></p>'
});
});
- 借助nunjucks模板实现HTML页面之消息提示页面的渲染(主要用于提示成功、失败信息页面)
{% extends "base.html" %}
{% block main %}
<div id="register" class="panel">
<h2>提示信息</h2>
<div>
{{message | safe}}
</div>
</div>
{% endblock %}
小型商城案例的上传图片页
- 实现思路流程:
- 1.我们访问127.0.0.1/upload页面时进入上传图片页。点击上传图片时本地图片被上传至服务器的目录下并把图片信息保存在数据库中。
- 借助路由和mysql实现动态页面之上传图片页的逻辑代码
router.get('/upload',async ctx=>{
[categories] = await query('SELECT * FROM `categories`');
ctx.body = nunjucks.render('upload.html', {
categories
});
})
router.post('/upload', koaBody({
multipart:true,
formidable:{
uploadDir:'./public/attachments',
keepExtensions:true
}
}),async ctx => {
let {
categoryId,
name,
price
} = ctx.request.body;
// console.log(ctx.request.files.filename,'1111');
let {base:filename}=parsePath(ctx.request.files.filename.path)
// console.log(filename,'filename');
let {size}=ctx.request.files.filename
// console.log(size,'size');
let {type}=ctx.request.files.filename
// console.log(type,'type');
// console.log(rs,'rsrsrsrsrs');
if(size>0){
let [rs] = await query(
"INSERT INTO `attachments` (`filename`, `type`, `size`) VALUES (?, ?, ?)",
[filename, type, size]
);
let [categories] = await query('SELECT * FROM `categories`');
return ctx.body = nunjucks.render('message.html', {
categories,
message:'<p>上传成功</p><hr/></a> <a href="/">回到首页</a></p>'
});
}else{
return ctx.body = nunjucks.render('message.html', {
categories,
message:'<p>上传失败</p> | <a href="/">回到首页</a></p>'
});
}
})
小型商城案例的注册页
- 实现思路流程:
- 1.我们访问127.0.0.1/register页面时进入注册页,用户填写信息后提示注册成功或者失败的消息页。若注册成功后把用户信息保存在数据库中。
- 借助nunjucks模板实现HTML页面之注册页的渲染
{% extends "base.html" %}
{% block main %}
<div id="register" class="panel">
<h2>注册</h2>
<form action="" method="POST" enctype="application/x-www-form-urlencoded">
<div class="form-item">
<label>
<span class="txt">姓名:</span>
<input type="text" class="form-input" name="username">
</label>
</div>
<div class="form-item">
<label>
<span class="txt">密码:</span>
<input type="password" class="form-input" name="password">
</label>
</div>
<div class="form-item">
<label>
<span class="txt">重复密码:</span>
<input type="password" class="form-input" name="repeatPassword">
</label>
</div>
<div class="form-item">
<label>
<span class="txt"></span>
<!-- <button class="form-button primary">登录</button> -->
<!-- button能够触发HTML页面表单的提交 -->
<button class="form-button">注册</button>
<a class="form-button" href="/login">立即登录</a>
</label>
</div>
</form>
</div>
{% endblock %}
- 借助路由和mysql实现动态页面之注册页的逻辑代码
//提供一个get方式接口localhost/register访问注册页面(form表单)
//注册页面
router.get('/register',async ctx=>{
[categories] = await query('SELECT * FROM `categories`');
ctx.body = nunjucks.render('register.html', {
categories
});
})
//注册逻辑
router.post('/register',koaBody(),async ctx => {
let {
username,
password,
repeatPassword
} = ctx.request.body;
let [categories] = await query('SELECT * FROM `categories`');
//处理一下数据校验
if(!username){
return ctx.body = nunjucks.render('message.html', {
categories,
message:'<p>用户名不能为空</p><p><hr/></a><a href="/register">重新注册</a> | <a href="/">回到首页</a></p>'
});
}
//检测用户名是否已经被注册
let [user] = await query(
'SELECT * FROM `users` where `username`=?',
[username]
);
if(user.length){
// console.log(user);
return ctx.body = nunjucks.render('message.html', {
categories,
message:'<p>用户名已经被注册</p><p><hr/></a><a href="/register">重新注册</a> | <a href="/">回到首页</a></p>'
});
}
//注册成功的时候
if(password===repeatPassword){
//预查询
let [result] = await query(
"INSERT INTO `users` (`username`, `password`) VALUES (?, ?)",
[username,password]
);
return ctx.body = nunjucks.render('message.html', {
categories,
message:'<p>注册成功</p><p><hr/></a><a href="/register">现在前去登录</a> | <a href="/">回到首页</a>| <a href="/register">还回到注册页</a></p>'
});
}
})
小型商城案例的登录页
- 实现思路流程:
- 1.我们访问127.0.0.1/login页面时进入登录页,用户填写信息后提示登录成功或者失败的消息页。若登录成功后让服务器给浏览器设置cookie标记用户的登录状态。
- 1.我们在这里新增一条规定:如果用户不登陆就无法进入添加商品页,被一直被重定向到登录页。
- 2.通过服务器给客户端标记cookie信息后,以后浏览器客户端在想服务器发送请求时都会自动携带着cookie信息。这种方法可以解决HTTP协议做为无状态协议,每次请求实际上是相对独立的,服务端无法获知接收到n次请求之间是否是有关联的问题。这里的cookie起到了携带存储和发送用户凭证的一个头信息的作用。
- 3.我们可以使用
ctx.set('Set-Cookie', 'id='+user.id);的方式设置cookie - 4.我们也可以使用koa框架给我们提供的设置cookie信息的方法,即
ctx.cookies.set('id',1,{signed:true}),这种方法还可以给cookie设置签名证书,这需要配合设置cookie密钥并且在设置cookie的时候带上密钥,即const key='lth'和app.keys=[key]这两句话。 - 5.如果使用方法3来设置cookie我们需要通过
let cookies=ctx.get('Cookie')来获取设置的cookie。此时我们可以利用第三方库cookie-parse来得到我们理想的数据格式cookie。使用方法为:let cookies = cookieParse.parse(ctx.get('Cookie')); - 6.如果我们使用方法4来设置cookie我们需要通过
let id = ctx.cookies.get('id');来获取设置的cookie(这里以设置的key为‘id’来举例)。 - 7.如果我们想要让中间件共享我们设置的cookie信息,我们封装一个中间件并将cookie挂载到koa提供的共享池(共享状态)中。即
ctx.state.uid = ctx.cookies.get('id',{signed:true});
- 借助nunjucks模板实现HTML页面之登录页的渲染
{% extends "base.html" %}
{% block main %}
<div id="login" class="panel">
<h2>登录</h2>
<form action="" method="post">
<div class="form-item">
<label>
<span class="txt">姓名:</span>
<input type="text" name="username" class="form-input">
</label>
</div>
<div class="form-item">
<label>
<span class="txt">密码:</span>
<input type="password" name="password" class="form-input">
</label>
</div>
<div class="form-item">
<label>
<span class="txt"></span>
<!-- 表单中button按钮会自动触发表单提交 -->
<button class="form-button primary">登录</button>
<a class="form-button" href="/register">我要注册</a>
</label>
</div>
</form>
</div>
{% endblock %}
- 封装一个中间件专门验证用户是否已经登录,未登录则一直被重定向至登录页。
async function verify(ctx, next) {
// let cookies=ctx.get('Cookie')
// console.log('cookies',cookies); //cookies id=1
//利用第三方库解析cookie cnpm i cookie-parse
// let cookies = cookieParse.parse(ctx.get('Cookie'));
// console.log('cookies',cookies); //cookies { id: '1' }
//利用koa框架设置cookie后不需要用第三方库来解析cookie了
// let id = ctx.cookies.get('id');
// console.log('id',id); //1
//想要共享出id给各处用
//根据uid从数据中获取具体的用户信息,
//存储在 ctx.state.user = {} 以供后续中间件去使用
// ctx.state.uid = ctx.cookies.get('id');
//还可以根据uid从数据库中获取具体的用户信息,存储在ctx.state.user
//现在来说客户端的cookie还是可以被随意篡改的
//于是我们需要用一种cookie密钥方法
ctx.state.uid = ctx.cookies.get('id',{
signed:true
});
if(ctx.state.uid){ //if(cookies.id){ 和 if(id){
await next()
}else{
//如果想要访问/addItem却检测到没有带cookie
//重定向到login页面
ctx.set('Location', '/login');
ctx.status = 302; //301
ctx.body = '';
// [categories] = await query('SELECT * FROM `categories`');
// ctx.body = nunjucks.render('message.html', {
// categories,
// //这样子是无法在其他请求中携带的
// //于是乎cookie就可以用上了
// //浏览器记得就行
// //响应首部Set-Cookie是被用来由服务器端向客户端发送cookie
// message: `<p>登陆后才可以添加商品</p><hr/><a href="/login">前往登录</a> | <a href="/">回到首页</a>`
// });
}
}
- 借助路由和mysql实现动态页面之登录页的逻辑代码
// 登录逻辑
router.post('/login', koaBody(), async ctx => {
[categories] = await query('SELECT * FROM `categories`');
let {username, password} = ctx.request.body;
let [[user]] = await query(
'SELECT * FROM `users` where `username`=? AND `password`=? limit 1',
[username, password]
);
if (!user) {
return ctx.body = nunjucks.render('message.html', {
categories,
message: '<p>用户名或者密码错误</p><p><hr/></a><a href="/login">重新登录</a> | <a href="/">回到首页</a></p>'
});
}
// 自己直接来设置cookie信息
// ctx.set('Set-Cookie', 'id='+user.id);
// ctx.set('Set-Cookie', 'user='+user.username);
// ctx.set('Set-Cookie', 'user='+user.username);
// 利用koa框架提供的设置cookie的方法
// 通过 signed 设置cookie签名,签名:证书
// ctx.cookies.set('id', 1, { signed: true });
//这里使用koa框架给我们提供的设置cookie信息的方法
//并且设置cookie签名证书
ctx.cookies.set('id',1,{signed:true})
ctx.body = nunjucks.render('message.html', {
categories,
//这样子是无法在其他请求中携带的
//于是乎cookie就可以用上了
//浏览器记得就行
//响应首部Set-Cookie是被用来由服务器端向客户端发送cookie
message: `<p>登录成功欢迎回来${user.username}你的id是${user.id}</p><hr/><a href="/addItem">登录后可以前往添加商品</a> | <a href="/">回到首页</a> |<a href="/login">回到登录页</a>`
});
})
- 基于cookie重写添加商品页的逻辑
router.get('/addItem', verify,async ctx => {
[categories] = await query('SELECT * FROM `categories`');
ctx.body = nunjucks.render('addItem.html', {
categories,
uid:ctx.state.uid
});
// 因为在这里是访问不到vertify里面的变量的
// 但是我们就是想要拿到uid并在页面显示用户登录的id
//方法一:
// let id = ctx.cookies.get('id');
// console.log(id,'idid');
//方法二:在共享池中(ctx.state.uid)获取
});
- 文末附上完整代码上传github的地址github.com/Lingtonghui…