不使用数据库
传统方式动态换数据
const Koa = require('koa');
const staticCache = require('koa-static-cache');
const Router = require('@koa/router');
const fs = require('fs');
const app = new Koa();
app.use(staticCache('./public', {
prefix: '/public',
dynamic: true,
gzip: true
}))
const router = new Router();
app.use(router.routes());
// 商品首页
router.get('/', async ctx => {
let data = {
title: '欢迎来到开课吧'
};
let templateContent = fs.readFileSync('./template/index.html').toString();
// 对模板一些可变的地方进行替换,生成一个最终的页面,返还给客户端
templateContent = templateContent.replace(/\{\{(\w+)\}\}/g, ($0, $1) => {
// console.log('$1', $1);
return data[$1];
})
ctx.body = templateContent;
})
app.listen(8888, ctx => {
console.log('服务器启动成功:http://localhost:8888');
})
使用nunjucks模板引擎换数据
const Koa = require('koa');
const staticCache = require('koa-static-cache');
const Router = require('@koa/router');
const nunjucks = require('nunjucks')
nunjucks.configure('template', {
noCache: true ,// 开发环境下,设置noCache为true,有利于测试效果,开发完毕后关闭节约性能
watch: true // 当模板变化时重新加载
});
const app = new Koa();
app.use(staticCache('./public', {
prefix: '/public',
dynamic: true,
gzip: true
}))
const router = new Router();
app.use(router.routes());
// 商品首页
router.get('/', async ctx => {
let data = {
title: '欢迎来到开课吧',
}
// let templateContent = fs.readFileSync('./template/index.html').toString();
// 对模板一些可变的地方进行替换,生成一个最终的页面,返还给客户端
// templateContent = templateContent.replace(/\{\{(\w+)\}\}/g, ($0, $1) => {
// // console.log('$1', $1);
// return data[$1];
// })
let templateContent = nunjucks.render('index.html', data)
ctx.body = templateContent;
})
app.listen(8888, ctx => {
console.log('服务器启动成功:http://localhost:8888');
})
动态路由
const Koa = require('koa');
const KoaStaticCache = require('koa-static-cache');
const KoaRouter = require('@koa/router');
const Nunjucks = require('nunjucks');
const categories = require('./data/categories.json');
const items = require('./data/items.json');
const app = new Koa();
Nunjucks.configure('./template', {
// 开发环境下,设置 noCache 为 true,有利于测试看效果
noCache: true,
watch: true
});
// http://localhost:8888/public/css/css.css
app.use(KoaStaticCache('./public', {
prefix: '/public',
dynamic: true,
gzip: true
}));
const router = new KoaRouter();
// 大部分的业务都放在下面处理了
// 商品首页
router.get('/', async ctx => {
// console.log('categories', categories);
ctx.body = Nunjucks.render('index.html', {
categories,
items
});
});
// 商品详情
// :id 表示是一个动态路由,:id 是可变的,具体根据请求来决定
// 如果一个请求是以 /item 开始 然后,后面跟着 / 任意内容,就能满足该路由的规则
// :id 是一个占位符 变量,名称可以自己定,但是在中间件里面用的话需要使用这个名称
// (\\d+) 是对可变数据的 约束
router.get('/item/:id(\\d+)', async ctx => {
// ctx.params => 对象,是 router 中间件根据当前url解析后的数据,里面存储了路由中动态部分当前实际内容
// console.log('ctx', ctx.params);
let id = Number(ctx.params.id);
let item = items.find( d => d.id === id );
ctx.body = Nunjucks.render('item.html', {
categories,
item
});
});
app.use(router.routes());
app.listen(8888, () => {
console.log(`服务启动成功:http://localhost:8888`);
});
// item.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="/public/css/css.css" />
</head>
<body>
<div id="app">
<header id="header">
<a href="/" id="logo"></a>
<nav id="nav">
<!-- <a href="">- {{title}} -</a> -->
{% for category in categories %}
<a href="">{{category.name}}</a>
{% endfor %}
</nav>
<div id="user">
<a href="">登录</a>
<a href="">注册</a>
</div>
</header>
<div id="main">
<h1>{{item.name}}</h1>
<hr>
<img src="/public/items/images/{{item.cover}}" alt="{{item.name}}">
</div>
</div>
</body>
</html>
分页功能完整代码
const Koa = require('koa');
const staticCache = require('koa-static-cache');
const Router = require('@koa/router');
const nunjucks = require('nunjucks')
const categories = require('./data/categories.json');
const items = require('./data/items.json');
nunjucks.configure('template', {
noCache: true,
watch: true
});
const app = new Koa();
app.use(staticCache('./public', {
prefix: '/public',
dynamic: true,
gzip: true
}))
const router = new Router();
app.use(router.routes());
// 商品首页
router.get('/', async ctx => {
// 对数据进行分页展示
// ctx.query 自动解析 当前 url 中 queryString 的部分,也就是url中 ? 后面的内容
let page = Number(ctx.query.page) || 1;
let prepage = 8;
let start = (page - 1) * prepage;
let end = start + prepage;
let pages = Math.ceil(items.length / 8);
let data = items.filter((item, index) => index >= start && index < end);
ctx.body = nunjucks.render('index.html', {
categories,
data,
page,
pages
});
})
// 商品详情
router.get('/item/:id(\\d+)', async ctx => {
let id = Number(ctx.params.id);
let item = items.find(d => d.id === id);
console.log(item);
ctx.body = nunjucks.render('item.html', {
categories,
item
});
})
app.listen(8888, ctx => {
console.log('服务器启动成功:http://localhost:8888');
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="./public/css/css.css" />
</head>
<body>
<div id="app">
<header id="header">
<a href="/" id="logo"></a>
<nav id="nav">
{% for category in categories %}
<a href="">{{category.name}}</a>
{% endfor %}
</nav>
<div id="user">
<a href="">登录</a>
<a href="">注册</a>
</div>
</header>
<div id="main">
<ul class="items-list">
{% for item in data %}
<li class="panel">
<a href="/item/{{item.id}}">
<img src="/public/items/images/{{item.cover}}" alt="{{item.name}}" class="cover">
</a>
<div class="name">{{item.name}}</div>
<div class="price">¥{{(item.price/100).toFixed(2)}}</div>
</li>
{% endfor %}
</ul>
<div class="pagination-container">
<div class="pagination">
<a href="/?page={{page-1}}" class="prev">上一页</a>
{% for i in range(1, pages+1) -%}
{% if i == page %}
<a href="/?page={{i}}" class="current">{{i}}</a>
{% else %}
<a href="/?page={{i}}">{{i}}</a>
{% endif %}
{%- endfor %}
<a href="/?page={{page+1}}" class="next">下一页</a>
</div>
</div>
</div>
</div>
</body>
</html>
使用数据库
新增注册、登录、添加功能
js完整代码
const Koa = require('koa');
const KoaStaticCache = require('koa-static-cache');
const KoaRouter = require('@koa/router');
const Nunjucks = require('nunjucks');
const mysql = require('mysql2');
const KoaBody = require('koa-body');
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: '12345678',
database: 'kkb_12'
});
const app = new Koa();
Nunjucks.configure('./template', {
noCache: true,
watch: true
});
app.use(KoaStaticCache('./public', {
prefix: '/public',
dynamic: true,
gzip: true
}));
const router = new KoaRouter();
// 商品首页
router.get('/', getCategories, async ctx => {
let [{count}] = await query(
'select count(*) as count from `items`',
);
// console.log('count', count);
let page = Number(ctx.query.page) || 1;
let prepage = 4;
let start = (page - 1) * prepage;
// let end = start + prepage;
let pages = Math.ceil(count / prepage);
let data = await query(
'select * from `items` limit ? offset ?',
[
prepage,
start
]
);
ctx.body = Nunjucks.render('index.html', {
categories: ctx.state.categories,
data,
page,
pages
});
});
router.get('/item/:id(\\d+)', getCategories, async ctx => {
// ctx.params => 对象,是 router 中间件根据当前url解析后的数据,里面存储了路由中动态部分当前实际内容
// console.log('ctx', ctx.params);
let id = Number(ctx.params.id);
let [item] = await query(
'select * from `items` where `id`=?',
[id]
);
// console.log('item', item);
ctx.body = Nunjucks.render('item.html', {
categories: ctx.state.categories,
item
});
});
// 渲染表单页面
router.get('/additem', getCategories, async ctx => {
// let categories = await query(
// 'select * from `categories`'
// );
ctx.body = Nunjucks.render('additem.html', {
categories: ctx.state.categories
});
});
// 处理提交数据请求
let addItemKoaBodyOptions = {
// 能解析multipart格式提交过来的普通文本型数据,二进制数据不在这里设置
multipart: true,
// 设置处理二进制流数据,formidable使用了第三方库:https://github.com/node-formidable/formidable,通过这个库可以实现对文件的解析和保存处理
formidable: {
// 解析后的文件数据存储的路径
uploadDir: './public/items/images',
// 是否保存上传后的文件扩展名
keepExtensions: true
}
};
router.post('/additem', KoaBody(addItemKoaBodyOptions), async ctx => {
// console.log('headers', ctx.headers);
// let {category_id: categoryId, name, price, cover} = ctx.request.body;
let {category_id: categoryId, name, price} = ctx.request.body;
// multipart 提交的数据,普通文本数据存储在 ctx.request.body 中
// multipart 提交的二进制数据,是存储在 ctx.request.files 中
let coverObject = ctx.request.files.cover; // cover 里面存储的每一个文件的 File 对象
// 我们数据库中存储的是上传后的文件名称,而不是原始的文件名
let lastIndexPoint = coverObject.path.lastIndexOf('/');
let cover = coverObject.path.substring(lastIndexPoint + 1);
// console.log(categoryId, name, price, cover);
let rs = await query(
"insert into `items` (`category_id`, `name`, `price`, `cover`) values (?, ?, ?, ?)",
[
categoryId,
name,
price,
cover
]
);
ctx.body = '添加成功';
});
// 注册页面
router.get('/register', getCategories, async ctx => {
ctx.body = Nunjucks.render('register.html', {
categories: ctx.state.categories
});
});
router.post('/register', getCategories, KoaBody(), async ctx => {
let {username, password, repassword} = ctx.request.body;
if (!username || !password) {
return ctx.body = '用户名和密码必须填写';
}
if (password !== repassword) {
return ctx.body = '两次输入密码不一致';
// ctx.body = Nunjucks.render('message.html', {
// categories: ctx.state.categories,
// message: '两次输入密码不一致'
// });
}
// 去数据库中查询当前注册用户名是否已经存在了
let user = await query(
"select * from `users` where `username`=?",
[username]
);
if (user.length) {
return ctx.body = '该用户名已经被注册了';
}
let rs = await query(
"insert into `users` (`username`, `password`) values (?, ?)",
[
username,
password
]
)
ctx.body = '注册成功';
});
// 登录页面
router.get('/login', getCategories, async ctx => {
ctx.body = Nunjucks.render('login.html', {
categories: ctx.state.categories
});
});
router.post('/login', KoaBody(), async ctx => {
let {username, password} = ctx.request.body;
if (!username || !password) {
return ctx.body = '用户名和密码必须填写';
}
// 去数据库中查询当前注册用户名是否已经存在了
let [user] = await query(
"select * from `users` where `username`=?",
[username]
);
if (!user) {
return ctx.body = '该用户不存在';
}
if (user.password !== password) {
return ctx.body = '密码错误';
}
ctx.body = '登录成功';
})
app.use(router.routes());
app.listen(8888, () => {
console.log(`服务启动成功:http://localhost:8888`);
});
async function getCategories(ctx, next) {
ctx.state.categories = await query(
'select * from `categories`'
);
await next();
}
function query(sql, data) {
return new Promise( (resolve, reject) => {
connection.query(sql, data, function(err, ...data) {
if (err) {
reject(err);
} else {
resolve(...data);
}
})
} );
}
添加功能html代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="/public/css/css.css" />
</head>
<body>
<!--
实际上表单的提交就是一个上传数据的过程(每一次请求就是数据的上传)
服务器的每一次响应实际上就是一个下载过程
但是根据上传的数据的类型不同,上传的时候需要进行一些特定的设置(规范),为了能够更好的识别当前上传数据,以方便后端进行不同的识别和处理,所以上传的时候除了可以携带上传的必要数据以外,还可以通过 请求头 content-type 类设置(告诉服务器)当前我携带过去的数据是什么类型的:mime
在表单中有一个enctype属性,可以设置当前通过表单来发送请求的时候,提交过去的数据的content-type的类型
application/x-www-form-urlencoded:默认,数据是一个 urlencoded
category_id=1&name=asdsa&price=123123&cover=1b0c619e070aed76.jpg
text/plain:纯文本
multipart/form-data:formData格式,二进制,如果提交的数据中包含了不能被application/x-www-form-urlencoded或者text/plain处理的特殊格式(二进制文件)
-->
{% include "nav.html" %}
<div id="main">
<div id="login" class="panel">
<h2>添加新商品</h2>
<form action="/additem" method="post" enctype="multipart/form-data">
<div class="form-item">
<label>
<span class="txt">分类:</span>
<select name="category_id">
{% 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="text" name="cover" class="form-input"> -->
<div class="form-button primary" id="uploaderBtn">
请选择一张封面图片进行上传
</div>
</label>
<input multiple type="file" name="cover" id="cover" style="display: none;" />
</div>
<div class="form-item">
<label>
<span class="txt"></span>
<button class="form-button success">添加</button>
</label>
</div>
</form>
</div>
</div>
</body>
</html>
<script>
let uploaderBtn = document.querySelector('#uploaderBtn');
let coverElement = document.querySelector('#cover');
uploaderBtn.onclick = function() {
coverElement.click();
}
coverElement.onchange = function() {
// value 存储的是字符串,是这个文件的文件名(包含路径的)
// value 不是文件本身的二进制数据,而是文件名称
// console.log(this.value);
// uploaderBtn.innerHTML = this.value;
// files 存储才是具体的文件二进制数据对象
// console.log(this.files);
uploaderBtn.innerHTML = this.files[0].name;
}
</script>
注册功能html代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="/public/css/css.css" />
</head>
<body>
{% include "nav.html" %}
<div id="main">
<div id="register" 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>
<input type="password" name="repassword" class="form-input">
</label>
</div>
<div class="form-item">
<label>
<span class="txt"></span>
<button class="form-button primary">登录</button>
<button class="form-button">注册</button>
</label>
</div>
</form>
</div>
</div>
</body>
</html>
登录功能html代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="/public/css/css.css" />
</head>
<body>
{% include "nav.html" %}
<div id="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 class="form-button primary">登录</button>
<button class="form-button">注册</button>
</label>
</div>
</form>
</div>
</div>
</body>
</html>