npm install koa --save
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx) => {
ctx.body = 'Hello, world'
})
app.listen(3000, () => {
console.log('listening at 3000 端口')
})
koa很小,它与express 的区别在于,express集成了router,body解析等
但是 koa 很小,它拥有中间件机制,还有use的用法。其他的都不用,use的本质也是个中间件
koa-router 是专门管理路由的,他也是个中间件
const Koa = require('koa');
+const Router = require('koa-router');
+const app = new Koa();
+const router = new Router()
+router.get('/api', (ctx) => {
+ ctx.body = 'api'
+})
// 使用中间件
+app.use(router.routes())
app.listen(3000, () => {
console.log('listening at 3000 端口')
})
app.use 的用法,
app.use(functionA, functionB, functionC...)
app.use 中调用的都属于中间件。当然他也可以链式调用,例如
app.use(functionA).use(functionB)
koa-router中默认支持GET,POST请求,加上 allowedMethods 方法后,就能支持更多的请求,因为项目会使用Restful进行开发,所以此时先加上。
...
// 使用中间件
+app.use(router.routes(), router.allowedMethods())
// 或者 app.use(router.routes()).use(router.allowedMethods())
app.listen(3000, () => {
console.log('listening at 3000 端口')
})
记住,koa中use中的函数都为中间件
在根目录下建立 router 文件夹,在 router 文件夹中建立 articles.js(文章接口),categories.js(类目接口),comments.js(评论接口),home.js(首页接口),index.js,tags(标签接口),users(用户接口)
以用户接口为例,
const Router = require('koa-router');
const router = new Router();
router.prefix('/users')
router.get('/', (ctx) => {
ctx.body = '寻找某个用户'
})
router.post('/', (ctx) => {
ctx.body = '注册'
} )
module.exports = router;
每个文件及路由。再在index.js中做统一的路由注册
const userRouter = require('./users');
...
module.exports = app => {
app.use(userRouter.routes()).use(userRouter.allowedMethods())
...
};
但是文件一多就不好管理,这时,我们想到了nodejs中的fs模块,即文件模块,利用它,读取同目录下的文件,并逐一注册。代码如下:
const fs = require('fs');
module.exports = (app) => {
fs.readdirSync(__dirname).forEach(file => {
if(file === 'index.js') return
const route = require(`./${file}`)
app.use(route.routes()).use(route.allowedMethods())
})
}
我们知道当我们GET请求时,是在url后的 ?
后加参数,形式问key=value。
前端如果传 http://localhost:3000?name=张三&age=25
,在koa2中怎么样才能拿到呢?
答案是:ctx.query
或者是 cxt.request.query
app.use( async ( ctx ) => {
let url = ctx.url
// 从上下文的request对象中获取
let request = ctx.request
let req_query = request.query
let req_querystring = request.querystring
// 从上下文中直接获取
let ctx_query = ctx.query
let ctx_querystring = ctx.querystring
ctx.body = {
url,
req_query,
req_querystring,
ctx_query,
ctx_querystring
}
})
那 post 请求如何获取呢?虽然可以用手写的方式实现,但并非本次教程的初衷。有兴趣的同学可以前往google 查询。
使用 koa-bodyparser 对请求体进行解析
...
+const bodyParser = require('koa-bodyparser');
const routing = require('./router');
const app = new Koa();
+app.use(bodyParser())
从 ctx.request.body 中获取请求体;
router.post('/login', (ctx) => {
const result = ctx.request.body;
ctx.body = result
})
users.js
...
router.get('/', (ctx) => {
ctx.body = '用户列表'
})
router.get('/:id', ctx => {
ctx.body = '寻找某个用户'
})
router.patch('/:id', ctx => {
ctx.body = '更新某个用户'
})
router.delete('/:id', ctx => {
ctx.body = '删除某个用户'
})
router.post('/login', (ctx) => {
ctx.body = '用户注册'
})
...
articles.js
router.get('/', ctx => {
ctx.body = '获取文章列表'
})
router.post('/', ctx => {
ctx.body = '新建文章'
})
router.get('/:id', ctx => {
ctx.body = '某篇文章详情'
})
router.patch('/:id', ctx => {
ctx.body = '更新文章'
})
router.delete('/:id', ctx => {
ctx.body = "删除文章"
})
先写这两个主要的路由接口,如果此两个主要路由通,其他都可在后续添加
首先,mysql 是一种最常见的数据库,其次,对 mysql 感兴趣的可以移步XX
先下载 mysql 的包
npm i mysql --save
再下载 cross-env 包 来控制各个操作系统上的环境变量
npm i cross-env --save
修改 package.json 中的 script 脚本
...
"dev": "cross-env NODE_ENV=dev nodemon app.js"
...
在根目录下创建,conf 文件夹和 db 文件夹,conf 文件夹用来存放各种配置。db 文件夹用来启动 mysql
先进入 conf 文件夹,创建 index.js
const env = process.env.NODE_ENV // 环境变量
let MYSQL_CONF;
if(env === 'dev') {
MYSQL_CONF = {
host: 'localhost',
user: 'root',
password: '123456',
port: '3306',
database: 'lipingerblog'
}
}
module.exports = {
MYSQL_CONF
}
进入 db 文件夹,创建index.js
const mysql = require('mysql')
const { MYSQL_CONF } = require('../conf')
const con = mysql.createConnection(MYSQL_CONF)
con.connect();
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
}
如果使用 mysql 呢
这里需要说明一下,MongoDB 是对象数据库 ,而 mysql 是关系数据库,
如果使用 MongoDB,你可以直接在项目中创建你所需要的数据,比如id,username,password等数据字段
而用mysql的话你最好会使用mysql的语法,先创建数据库,在创建表,麻烦的要死。
为说明问题,本人大公无私,身先士卒,敢为人先
- 下载mysql
- 下载 Mysql Workbench 图形化工具
- 使用 Workbench 连接本地数据库
- 创建users表
- 创建articles表
- 增删改查
在sql文本中,输入 use lipingerblog,使用lipingerblog这个数据库,show tables为显示所有的表
use lipingerblog;
show tables
增
insert into users(username, `password`, realname) values('zhangsan', '123', '张三')
ps:如果此时出现1366错误,说明realname的编码格式错误,将其改为utf-8 即可
查
查users表所有信息
select * from users;
查users表中其中id和username的信息
select id, username from users;
查符合条件的项 where
select * from users where username='zhangsan'
查符合条件的项多个条件 and
和 or
select * from users where username='zhangsan' and realname='张三'
模糊查询 like
select * from users where username like '%zhang%'
查 排序 order by id
默认正序,如果倒序 在id后加 desc order by id desc
select * from users where username like '%zhang%' order by id desc;
ps:一般不用 * ,耗性能
更新 id为3的realname为张三
update users set realname='张三1' where id='3'
删
delete from users where realname='李四'
但一般来说不用delete,二是在users表中加一个状态,通过状态来判断他是否被删除。这种技术又称软删除
不是用删,而是用update更新他的状态
update users set state='0' where username='lisi'
PS:如果你的更新和删除出现 error:1175处于安全模式,先使用以下代码解除安全模式
SET SQL_SAFE_UPDATES=0;
同理,将articles的数据也一并倒腾好
此时,我们的workbench 教程告一段落
首先,改造articles文件,
const {exec} = require('../db');
const router = new Router();
router.prefix('/api/articles');
router.get('/', async (ctx) => {
let sql = `select * from articles`;
ctx.body = await exec(sql);
})
...
发现能拉取数据,说明koa已经和mysql已经连接了
目前的项目还比较简单,但是一旦开展起来,路由和控制应该要分开。以前叫MVC,model层,view层,controller层,但是因为此项目中没有model层和view层。但是router只管路由,也可以分层。所以在根目录下创建controller文件夹,在其目录下创建articles.js
和 users.js
,改造第一个文件
// controller/articles.js
const { exec } = require('../db');
class ArticleCtl {
async find(ctx) {
let sql = `select * from articles`;
ctx.body = await exec(sql);
}
}
module.exports = new ArticleCtl()
...
const { find } = require('../controller/articles');
const router = new Router();
router.prefix('/api/articles');
router.get('/', find)
...
跑起来后,目录结构清晰,为人大方,亲密可嘉
ps:一般商业项目都有十几,二十几个接口,所以一般都是要分离的。虽然文件变多了,但是目录看起来清晰
在做的过程中你会发现,有些地方需要登录才能操作,比如说你的更新自己的文章,你要删除自己的文章,那你是谁,你是否登录,一些列问题就出来了,这个时候我们不急,先用假数据把接口调通,下一步再实现登录认证。
class ArticleCtl {
// 查找所有文章
async find(ctx) {
let author = ctx.query.author || '';
const keyword = ctx.query.keyword || '';
let sql = `select * from articles where 1=1 `;
if (author) {
sql += `and author='${author}' `
}
if (keyword) {
sql += `and title like '%${keyword}%' `
}
sql += `order by createtime desc;`
ctx.body = await await exec(sql)
}
// 查找某一篇文章
async findById(ctx) {
const sql = `select * from articles where id='${ctx.params.id}'`
const rows = await exec(sql);
ctx.body = rows[0]
}
...
}
const Router = require('koa-router');
+const { find, findById, create, update, delete: del} = require('../controller/articles');
const router = new Router();
router.prefix('/api/articles');
router.get('/', find)
router.post('/', create)
+router.get('/:id', findById)
+router.patch('/:id', update)
+router.delete('/:id', del)
module.exports = router;
这里需要注意的是,因为我们使用的是restful Api,所以 像 router.get('/:id', findById)
需要取值的时候是用ctx.params 来取,而不是ctx.query
创建文章,更新文章,删除文章均需要用户登录,所以这边用假数据代替
async create(ctx) {
const { title = '', content = '' } = ctx.request.body;
const createtime = Date.now()
const author = 'zhangsan'
const sql = `
insert into articles (title, content, createtime, author)
values ('${title}', '${content}', ${createtime}, '${author}')
`
const result = await exec(sql)
ctx.body = {
id: result.insertId
}
}
async update(ctx) {
const { title = '', content = '' } = ctx.request.body
const id = ctx.params.id
const sql = `
update articles set title='${title}', content='${content}' where id=${id}
`
const resultData = await exec(sql)
if (resultData.affectedRows > 0) {
ctx.body = '更新成功'
} else {
ctx.body = '更新失败'
}
}
async delete(ctx) {
const id = ctx.params.id;
const author = 'zhangsan';
const sql = `delete from articles where id='${id}' and author='${author}'`
const resultData = await exec(sql)
if (resultData.affectedRows > 0) {
ctx.status = 204
} else {
ctx.body = '删除博客失败'
}
}
关于delete方法中的sql语句,想了想,因为我的blog比较小众,也不存在删人账号这一说法,干脆用来学习一下sql语句也好。
以上为 articles.js 中的方法,再在controller中创建 users.js,这里写关于用户的相关操作
const { exec } = require('../db');
class UsersCtl {
async create(ctx) {
const { username = '', password = '' } = ctx.request.body;
const sql = `
select username, realname from users where username='${username}' and password='${password}'
`
const resultData = await exec(sql);
if(resultData[0]) {
ctx.body = '登录成功'
return
}
ctx.body = '登录失败'
}
}
module.exports = new UsersCtl()
目前来说就只有登录这一接口
const { create } = require('../controller/users');
router.post('/login', create)
我不知道该怎么说明cookie 和 session
cookie其实就是在客户端发送http请求时,header中带有的某个key,传到服务端后,服务端能查看你的cookie,根据你的值判断你的人(你是谁)。这样就实现了用户的认证登录功能。
但是这并不安全,cookie是明文传输,黑客可以伪造你的cookie作恶,所以有了session,session 即 server端的存储用户信息。因为是在服务端,黑客就不轻易拿到,所以一般用cookie+session的方式来做用户认证
欲三更:也就是说 cookie 是一个实际存在的东西, http 协议中定义在 header 中的字段,可以认为是 session 的一种后端无状态实现。
cookie中存放 userid,server 端对应 username
说白了也很简单,就是 cookie 能实现客户端和服务端的会话,但是不够安全,那就定义一个 值(或叫userid,或叫sid),你客户端传这个值来一一对应用户,就做到了安全和会话。虽然麻烦,但是往往如此。
具体过程:用户首次访问网页,客户端发送请求给服务端,服务端收到后,查看http头,如果没有cookie,就用session做一个cookie来,然后返回cookie(cookie不仅可以在客户端设置,也可以在服务端设置),当用户第二次请求网页的时候,因为有cookie,服务器判断http头有cookie,就做出有cookie的展示...
http 本身是无状态的,session 是一种机制
session 从字面上讲,就是会话,cookie是浏览器本身自带的存储方式
例如:你请求我的登录页面,我用session 把你的信息存下来,如果你再请求我的其他页面,因为我的状态已经被session 存下来了,所以访问其他页面时,我们能请求到,如果session过期,那么我们会报错
PS:也是心血来潮才做koa+session会话实现的。session更适合 页面渲染的后端,即前后端不分离的项目,现在的项目一般都是前后端分离,用jwt才是正解。我在express项目中正是用到了jwt。
了解完cookie和session是什么后,那怎么把session和koa结合呢?
当然是自己下个npm包啦,理论上我们应该下载koa-session,但是人生总是这样,koa-session 不能和 koa-redis结合(起码我是没看出来,如果有错误请指正),所以我们使用koa-generic-session包, 和koa-redis做结合
不排斥使用koa-session 和 redis的结合,如果能手写那是更厉害的,但是本文的核心是介绍一篇通俗易懂的koa2博客项目,所以为了从大流,选择koa-generic-session与koa-redis的结合
先来看看redis吧
redis是内存数据库,一句话描述就是速度快,比起mysql等数据库,它因为存在内存中,所以速度快,一般网站都是用session和redis的结合,详见 redis 篇章
命令 | 解释 | 例子 |
---|---|---|
set key value | 设置key | set myname johan |
get key | 得到key | get myname |
del key | 删除key | del myname |
keys * | 显示所有的key | keys * |
下载 koa-generic-session
和 koa-redis
包
在app.js 文件中配置redis和session 的配置
app.js
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
+const session = require('koa-generic-session');
+const redisStore = require('koa-redis');
const routing = require('./router');
+const { REDIS_CONF, SECRETS } = require('./conf');
const app = new Koa();
app.keys = [ SECRETS ]
+app.use(session({
+ cookie: {
+ path: '/',
+ httpOnly: true,
+ maxAge: 24 * 60 * 60 * 1000
+ },
+ store: redisStore({
+ all: `${REDIS_CONF.host}:${REDIS_CONF.port}`
+ })
+}))
...
controller/users.js
const resultData = await exec(sql);
if (resultData[0].username) {
+ ctx.session.username = resultData[0].username;
+ ctx.session.realname = resultData[0].realname;
ctx.body = '登录成功'
return;
}
ctx.body = '登录失败'
让我们捋一下思路,当用户登录时,去数据库查数据,如果没有,就显示登录失败;如果有,那么保存下他的username和realname已经返回“登录成功”。
因为session保存的时间是24小时,所以在这24小时内,session 会话一直保持着(存在redis中)
我们已经把redis和session 都做好了,但是还不能检测出来,因为你必须要登录。所以我们需要先写前端页面。这里需要一些时间来找找模板,因为一个人的审美是很重要滴
筛选一番后,看重 bootstrap 官网中的中模板,因为我们此番目的是教学,所以简约美是我们的方向。下载首页(home)以及文章列表页(post),将其中的代码拷贝至项目static文件下,如下所需的文件。而我们的模板使用的 art-template。
因为过于简单,在这里贴出官网 链接,供大家查看,除此之外,还需要让服务加载静态资源,这里使用的是koa-static。
修改app.js
...
+ const path = require('path');
+ const render = require('koa-art-template');
+ const static = require('koa-static')
...
// 静态资源目录对于相对入口文件index.js的路径
+const staticPath = './static'
// 加载静态资源
+app.use(static(
+ path.join(__dirname, staticPath)
+))
+app.use(bodyParser())
+render(app, {
+ root: path.join(__dirname, 'views'),
+ extname: '.art',
+ debug: process.env.NODE_ENV !== 'production'
+});
routing(app);
...
这样只是做好了一小步,首页和文章的样式都已经加载好了,但是我们还需要注册和登录的页面,下一节,动手做出注册登录页面
import assert from 'assert';
import { KoaContext } from '../../types/koa';
/**
* 全局异常捕获
* 如果是通过 assert 主动抛出的异常, 则向客户端返回该异常消息
* 如果是其它异常, 则打印异常信息, 并返回 Server Error
*/
export default function catchError() {
return async (ctx: KoaContext, next: Function) => {
try {
await next();
} catch (err) {
if (err instanceof assert.AssertionError) {
ctx.res = err.message;
return;
}
ctx.res = `Server Error: ${err.message}`;
console.error('Unhandled Error\n', err);
}
};
}
git push 到远程仓库
在服务器中配置后 git 私钥后,git pull 远程仓库代码
如果您觉得这个项目能够帮助到您,可以给我个 star🌟,也可以推荐给您的朋友
持续更新中~ 🚀🚀🚀
MIT