本项目基于Koa框架,依赖以下第三方库:
项目结构:
新建项目文件夹并安装第三方库:
>>> mkdir koa-blog-examples // 创建项目文件夹 >>> cd koa-blog-examples // 切换到项目文件夹下 >>> npm init // 初始化环境 >>> npm install fs bluebird koa koa-router koa-ejs koa-bodyparser --save // 安装依赖库
然后你需要创建middlewares
, routes
, services
, templates
这四个文件夹,然后开始工作。
middlewares/authenticate.js
文件,将登陆状态挂载到ctx.state
上,供后续使用。// 认证中间件 module.exports = async function (ctx, next){ // ctx即context;next指下一个中间件 const logged = ctx.cookies.get('logged', {signed: true}); ctx.state.logged = !!logged; // !!logged表示强制将logged转换为true或者false await next(); // 异步等待 };
services/post.js
,博客文章相关服务,提供文章的 发布 / 编辑 / 详情 / 删除 等功能。const fs = require('fs'); const bluebird = require('bluebird'); bluebird.promisifyAll(fs); // 异步化 // 文章数据 const posts = []; //文章ID let postId = 1; //发表文章 exports.publish = function (title, content) { const item = { id: postId++, title: title, content: content, time: (new Date()).toLocaleString() }; posts.push(item); // 将新的博客放入文章数据列表中 return item; // 返回新的博客 }; //查看文章 exports.show = function (id){ // 根据博客id返回博客,如果没有找到返回null id = Number(id); for (const post of posts) { if (post.id === id){ return post } } return null; }; //编辑文章 exports.update = function (id, title, content){ // 改变指定id的博客的标题和内容。 id = Number(id); posts.forEach((post)=>{ if (post.id === id){ post.title = title; post.content = content; } }); }; //删除文章 exports.delete = function (id){ // 根据id找到博客并删除 id = Number(id); let index = -1; posts.forEach((post, i)=>{ if (post.id === id){ index = i; } }); if (index > -1){ posts.splice(index, 1); // 在位置为index的地方,删除1个项目 } }; //文章列表 exports.list = function (){ return posts.map(item => item); // 返回一个博客列表(不会改变原始博客列表) };
routes/post.js
文件,博客文章相关路由,提供文章的 发布 / 编辑 / 详情 / 删除 等功能。const Router = require('koa-router'); const postService = require('../services/post'); const router = new Router(); // 发布表单页 router.get('/publish', async(ctx)=>{ await ctx.render('publish'); }); // 发布处理 router.post('/publish', async(ctx)=>{ const data = ctx.request.body; if (!data.title || !data.content){ ctx.throw(400, "您的请求有误!"); } const item = postService.publish(data.title, data.content); ctx.redirect(`/post/${item.id}`); }); // 详情页 router.get('/post/:postId', async(ctx)=>{ const post = postService.show(ctx.params.postId); if (!post){ ctx.throw(400, '文章不存在'); } await ctx.render('post', {post: post}); }); // 编辑表单页 router.get('/update/:postId', async(ctx)=>{ const post = postService.show(ctx.params.postId); if (!post){ ctx.throw(400, '文章不存在'); } await ctx.render('update', {post: post}); }); // 编辑处理 router.post('/update/:postId', async (ctx)=>{ const data = ctx.request.body; if (!data.title || !data.content){ ctx.throw(400, '您的请求有误'); } const postId = ctx.params.postId; postService.update(postId, data.title, data.content); ctx.redirect(`/update/${postId}`); }); // 删除 router.get('/delete/:postId', async (ctx)=>{ postService.delete(ctx.params.postId); ctx.redirect('/'); }); module.exports = router;
templates/post.ejs
文章详情页。<div> <h1><%= post.title %></h1> <time>发表时间: <%= post.time %></time> <hr> <div><%= post.content %></div> </div>
routes/site.js
网站首页,负责读取文章列表并渲染到HTML上。const Router = require('koa-router'); const postService = require('../services/post'); const router = new Router(); //网站首页 router.get('/', async (ctx)=>{ const list = postService.list(); await ctx.render('index', {list: list}); }); module.exports = router;
services/user.js
用户业务服务,负责用户登录检测。const user = { Joe: 'password' // 用户:密码 }; exports.login = function (username, password){ if (user[username] === undefined){ return false; } return user[username] === password; };
routes/user.js
用户相关路由,负责登录和退出登录。const Router = require('koa-router'); const userService = require('../services/user'); const router = new Router(); // 登录表单页 router.get('/login', async (ctx)=>{ await ctx.render('login'); }); // 登录处理 router.post('/login', async (ctx)=>{ const data = ctx.request.body; if (!data.username || !data.password){ ctx.throw(400, '您的请求有误'); } const logged = userService.login(data.username, data.password); if (!logged){ ctx.throw(400, '账号或密码错误'); } ctx.cookies.set('logged', 1, {signed: true, httpOnly: true}); ctx.redirect('/', '登陆成功'); }); // 退出登陆 router.get('/logout', async (ctx)=>{ ctx.cookies.set('logged', 0, {maxAge: -1, signed: true}); ctx.redirect('/', '退出登录成功'); }); module.exports = router;
templates/index.ejs
网站首页模板,负责渲染文章列表。<p>文章列表</p> <% if(list.length == 0) { %> <p>没有文章发表</p> <% } else { %> <table> <tr> <th>ID</th> <th>标题</th> <th>发表时间</th> <% if (logged){ %> <th>操作</th> <% } %> </tr> <% list.forEach((post) => { %> <td><%= post.id %></td> <td><a href="/post/<%= post.id %>"><%= post.title %></a><td> <td><%= post.time %></td> <% if (logged) { %> <th> <a href="/update/<%= post.id %>">编辑</a> <a href="/delete/<%= post.id %>" οnclick="return confirm('确认删除吗?')">删除</a> </th> <% } %> <% }) %> </table> <% } %>
templates/login.ejs
用户登录页面,负责收集用户信息并发送给服务器。<form action="/login" method="POST" enctype="application/x-www-form-urlencoded"> <fieldset> <legend>登录</legend> <div> <label for="username">账号</label> <input type="text" name="username" id="username" required> </div> <div> <label for="password">密码</label> <input type="password" name="password" id="password" required> </div> <div> <button type="submit">登录</button> </div> </fieldset> </form>
templates/main.ejs
根据登录状态显示不同的导航页<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/5.1.1/css/bootstrap.min.css"> <script src="https://cdn.staticfile.org/popper.js/2.9.3/umd/popper.min.js"></script> <script src="https://cdn.staticfile.org/twitter-bootstrap/5.1.1/js/bootstrap.min.js"></script> <title>博客</title> </head> <body style="background-color: #dddddd"> <div class="p-3 mb-3 border-bottom" style="background-color: #333"> <div class="container-fluid"> <div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start"> <ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0"> <% if(logged){ %> <a href="/">首页</a> <a href="/publish">发表文章</a> <a href="/logout">退出登录</a> <% } else { %> <a href="/login">登录</a> <% } %> </ul> </div> </div> </div> <%- body%> </body> </html>
templates/publish.ejs
文章发布页,收集博客信息发送给服务器。<form action="/publish" method="POST" enctype="application/x-www-form-urlencoded"> <fieldset> <legend>发表文章</legend> <div> <label for="title">标题</label> <input type="text" name="title" id="title" required> </div> <div> <label for="content">内容</label> <textarea name="content" id="content" required></textarea> </div> <div> <button type="submit">发布</button> <button type="reset">重置</button> </div> </fieldset> </form>
templates/update.ejs
文章编辑器,填充已有博客信息到输入框,并将新输入的博客信息提交给服务器。<form action="/update/<%= post.id %>" method="POST" enctype="application/x-www-form-urlencoded"> <field> <legend>编辑文章</legend> <div> <label for="title">标题</label> <input type="text" value="<%= post.title %>" name="title" id="title" required> </div> <div> <label for="content">内容</label> <textarea name="content" id="content" required><%= post.content %></textarea> </div> <div> <button type="submit">发布</button> <button type="reset">重置</button> </div> </field> </form>
index.js
程序入口JS文件,负责挂载中间件、路由、应用配置和启动服务器。const Koa = require("koa"); const render = require("koa-ejs"); const bodyParser = require("koa-bodyparser"); const authenticate = require("./middlewares/authenticate"); // 路由 const siteRoute = require("./routes/site"); const userRoute = require("./routes/user"); const postRoute = require("./routes/post"); const app = new Koa(); app.keys = ["jiohd4654nidh46-05+/*69d"]; // 使用中间件 app.use(bodyParser()); // 解析请求体的中间件 app.use(authenticate); // 挂载登录状态 render(app, { root: './templates', // 网页模板位置 layout: 'main', // 网页布局/主题风格使用main.ejs viewExt: 'ejs' }); // 挂载路由 app.use(siteRoute.routes()).use(siteRoute.allowedMethods()); app.use(userRoute.routes()).use(userRoute.allowedMethods()); app.use(postRoute.routes()).use(postRoute.allowedMethods()); app.listen(3000, ()=> { console.log("listen on 3000"); });
最后,在终端输入node index.js
再打开浏览器的:localhost:3000
就可以看到登录内容。