用node.js写一个博客网站
本项目基于Koa框架,依赖以下第三方库:
- fs
- bluebird
- koa
- koa-router
- koa-ejs
- koa-bodyparser
项目结构:
新建项目文件夹并安装第三方库:
>>> 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
就可以看到登录内容。