Node.js+Express+Koa2开发接口学习笔记(四)
cookie介绍
- 存储在浏览器的一段字符串(最大5kb)
- 跨域不共享
- 格式如k1=v1;k2=v2;k3=v3;因此可以存储结构化数据
- 每次发送http请求,会将请求域的cookie一起发送给server
- server可以修改cookie并返回给浏览器
- 浏览器中也可以通过js修改cookie(有限制)
//每次都会往cookie里追加,不是直接替换原来的值
document.cookie = 'k1=100;'
document.cookie = 'k2=200;'
cookie用于登录验证
在app.js可以在请求头拿到cookie,可以将其解析保存下来
const serverHandle = (req,res)=>{
...
// 解析cookie
req.cookie = {};
const cookieStr = req.headers.cookie || ""; // k1=v1;k2=v2;k3=v3
cookieStr.split(";").forEach((item) => {
if (!item) {
return;
}
const arr = item.split("=");
const key = arr[0].trim(); // 如果是已经有cookie追加时key值前面会有空格,需要清除空格
const val = arr[1]
req.cookie[key] = val;
});
console.log("req.cookie is", req.cookie);
...
}
解析cookie后,可以假设只要cookie中带有username就认为用户已经登录了,在router->user.js路由中编写一个用于登录验证的测试
const { login } = require("../controller/user");
const { SuccessModel, ErrorModel } = require("../model/resModel");
const handleUserRouter = (req, res) => {
const method = req.method; // GET POST
// 登录
if (method === "POST" && req.path === "/api/user/login") {
const result = login(req.body);
return result.then((data) => {
if (data.username) {
return new SuccessModel();
}
return new ErrorModel("登录失败");
});
}
// 登录验证的测试
if (method === "GET" && req.path === "/api/user/login-test") {
if (req.cookie.username) {
return Promise.resolve(new SuccessModel());
}
return Promise.resolve(new ErrorModel("尚未登录"));
}
};
module.exports = handleUserRouter;
在没有cookie值前,访问http://localhost:8000/api/user/login-text,可以看到服务器返回了尚未登录的错误信息。
在浏览器修改cookie:document.cookie = 'username=zhangsan'
,重新访问没有返回错误信息。但是这种在浏览器修改cookie是很不可靠的,需要后端帮助我们修改请求域的cookie。
为了便于在浏览器测试,需要将登录方法改成GET方法。
// 登录
if (method === "GET" && req.path === "/api/user/login") {
// const result = login(req.body);
const result = login(req.query);
return result.then((data) => {
if (data.username) {
// 操作cookie
res.setHeader("Set-Cookie", `username=${data.username}; path=/`);
return new SuccessModel();
}
return new ErrorModel("登录失败");
});
}
path=/
表示这个cookie适用于根目录,适用于该网站下的所有路由网页,如果不设置path=/
,由于是在路由"/api/user/login"
设置cookie,那path值就是该路由,访问其他路由时该cookie就会失效。
测试前,记得先去开发工具者的Application把Cookies清空,然后访问http://localhost:8000/api/user/login?username=zhangsan&password=123
在浏览器控制台打印document.cookie
也可以看到cookie值。
cookie做限制
前面假设只要cookie中带有username就认为用户已经登录了,那么如果在前端随意修改cookie中username的值就会可以随便访问他人数据,这样是不行的。我们需要在服务端对cookie做限制。
res.setHeader(
"Set-Cookie",
`username=${data.username}; path=/; httpOnly`
);
httpOnly
就是限制cookie只能由后端改,不能在前端改,并且在前端打印document.cookie是不会显示值的。
除此之外,如果不对cookie做时间限制,那么它是永久有效的。
// 获取cookie的过期时间
const getCookieExpires = () => {
const d = new Date();
d.setTime(d.getTime() + 24 * 60 * 60 * 1000); // 设置有效期是一天
return d.toGMTString(); // cookie规定的GMT格式
};
const handleUserRouter = (req, res) => {
const method = req.method; // GET POST
// 登录
if (method === "GET" && req.path === "/api/user/login") {
// const result = login(req.body);
const result = login(req.query);
return result.then((data) => {
if (data.username) {
// 操作cookie
// path=/表示这个cookie适用于根目录,适用于该网站下的所有路由网页,如果不设置path=/,由于是在路由"/api/user/login"设置cookie,那path值就是该路由,访问其他路由时该cookie就会失效
res.setHeader(
"Set-Cookie",
`username=${
data.username
}; path=/; httpOnly;expires=${getCookieExpires()}`
);
return new SuccessModel();
}
return new ErrorModel("登录失败");
});
}
// 登录验证的测试
if (method === "GET" && req.path === "/api/user/login-test") {
if (req.cookie.username) {
return Promise.resolve(
new SuccessModel({ username: req.cookie.username })
);
}
return Promise.resolve(new ErrorModel("尚未登录"));
}
};
session
上述使用cookie导致的缺点:会暴露username(或者其他属性值)个人信息,很危险。
如何解决:cookie存储userid,server端对应username
解决方案:session,即server端存储用户信息
//app.js
// session数据
const SESSION_DATA = {};
// 获取cookie的过期时间
const getCookieExpires = () => {
const d = new Date();
d.setTime(d.getTime() + 24 * 60 * 60 * 1000); // 设置有效期是一天
return d.toGMTString();
};
const serverHandle = (req, res) => {
...
// 解析cookie
...
// 解析session
let needSetCookie = false;
let userId = req.cookie.userid;
if (userId) {
if (!SESSION_DATA[userId]) {
SESSION_DATA[userId] = {};
}
} else {
needSetCookie = true;
userId = `${Date.now()}_${Math.random()}`;
SESSION_DATA[userId] = {};
}
req.session = SESSION_DATA[userId];
//处理post data
getPostData(req).then((postData) => {
req.body = postData;
const blogResult = handleBlogRouter(req, res);
if (blogResult) {
blogResult.then((blogData) => {
if (needSetCookie) {
res.setHeader(
"Set-Cookie",
`userid=${userId}; path=/; httpOnly;expires=${getCookieExpires()}`
);
}
res.end(JSON.stringify(blogData));
});
return;
}
const userResult = handleUserRouter(req, res);
if (userResult) {
userResult.then((userData) => {
if (needSetCookie) {
res.setHeader(
"Set-Cookie",
`userid=${userId}; path=/; httpOnly;expires=${getCookieExpires()}`
);
}
res.end(JSON.stringify(userData));
});
return;
}
}
修改user.js
const { login } = require("../controller/user");
const { SuccessModel, ErrorModel } = require("../model/resModel");
const handleUserRouter = (req, res) => {
const method = req.method; // GET POST
// 登录
if (method === "GET" && req.path === "/api/user/login") {
// const result = login(req.body);
const result = login(req.query);
return result.then((data) => {
if (data.username) {
// 设置session
req.session.username = data.username;
req.session.realname = data.realname;
console.log("req.session is ", req.session);
return new SuccessModel();
}
return new ErrorModel("登录失败");
});
}
// 登录验证的测试
if (method === "GET" && req.path === "/api/user/login-test") {
if (req.session) {
return Promise.resolve(new SuccessModel({ session: req.session }));
}
return Promise.resolve(new ErrorModel("尚未登录"));
}
};
module.exports = handleUserRouter;
从session到redis
目前session
直接是js变量,放在nodejs进程内存中,这种方式会导致以下问题:
- 操作系统会限制一个进程的最大可用内存,进程内存有限,访问量过大,内存暴增怎么办
- 正式线上运行是多进程,进程之间内存无法共享
解决方案:使用redis
,将session存储在redis中,可以解决进程被挤爆以及进程数据无法共享的问题。
- web server 最常用的缓存数据库,数据存放在内存中
- 相比于mysql,访问速度快(内存和硬盘不是一个数量级的)
- 但是成本更高,可存储的数据量更小(内存的硬伤)
为何session
适合用redis
?
- session访问频繁,对性能要求极高
- session可不考虑断电丢失数据的问题(内存的硬伤)
- session数据量不会太大(相比于mysql中存储的数据)
为何网站数据不适合用redis?
- 操作频率不是太高(相比于session操作)
- 断电不能丢失,必须保留
- 数据量太大,内存成本太高
redis安装
- Windows http://www.runoob.com/redis/redis-install.html
- Mac使用
brew install redis
nodejs链接redis
首先需要redis-server
命令将redis服务启动起来
然后在项目中安装npm i redis --save
在index.js链接redis,
const redis = require("redis");
// !为了分割这一行和上一行,如果上一行没有;结尾
!(async function () {
// 创建客户端
const redisClient = redis.createClient(6379, "127.0.0.1");
// 连接
await redisClient
.connect()
.then(() => console.log("redis connect sucess!"))
.catch(console.error);
await redisClient.set("myname", "zhangsan");
// get
const myname = await redisClient.get("myname");
console.log("myname", myname);
// 退出
redisClient.quit();
})();
运行index,js文件发现在控制台打印了信息
redis connect sucess!
myname zhangsan
打开cmd进入redis-cli
,命令get myname
成功打印出刚才添加的值,说明nodejs链接redis成功
可以将上述代码封装成工具函数。在之前的conf->db.js封装redis的配置
const env = process.env.NODE_ENV; //环境变量
// 配置
let MYSQL_CONF;
let REDIS_CONF;
if (env === "dev") {
// mysql
MYSQL_CONF = {
host: "localhost",
user: "root",
password: "123456",
port: 3306,
database: "myblog",
};
// redis
REDIS_CONF = {
port: 6379,
host: "127.0.0.1",
};
}
if (env === "production") {
MYSQL_CONF = {
host: "localhost",
user: "root",
password: "123456",
port: 3306,
database: "myblog",
};
// redis
REDIS_CONF = {
port: 6379,
host: "127.0.0.1",
};
}
module.exports = {
MYSQL_CONF,
REDIS_CONF,
};
在db->redis.js里连接redis并对外提供get和set方法
const redis = require("redis");
const { REDIS_CONF } = require("../conf/db");
// 创建客户端
const redisClient = redis.createClient(REDIS_CONF.port, REDIS_CONF.host);
// 连接数据库
!(async function () {
// 连接
await redisClient
.connect()
.then(() => console.log("redis connect sucess!"))
.catch(console.error);
})();
// set
async function set(key, val) {
let objVal;
if (typeof val === "object") {
objVal = JSON.stringify(val);
} else {
objVal = val;
}
await redisClient.set(key, objVal);
}
// get
async function get(key) {
try {
let val = await redisClient.get(key);
if (val == null) return val;
try {
val = JSON.parse(val); // 尝试转换对象
} catch (err) {}
return val;
} catch (err) {
throw err;
}
}
module.exports = { set, get };
session存入redis
链接redis后,在app.js将之前存储session的写法进行改动
const querystring = require("querystring");
const { get, set } = require("./src/db/redis");
const handleBlogRouter = require("./src/router/blog");
const handleUserRouter = require("./src/router/user");
// // session数据
// const SESSION_DATA = {};
// 获取cookie的过期时间
const getCookieExpires = () => {
const d = new Date();
d.setTime(d.getTime() + 24 * 60 * 60 * 1000); // 设置有效期是一天
return d.toGMTString();
};
...
const serverHandle = (req, res) => {
...
// 解析session (使用redis)
let needSetCookie = false;
let userId = req.cookie.userid;
if (!userId) {
needSetCookie = true;
userId = `${Date.now()}_${Math.random()}`;
//初始化redis中的session值
set(userId, {});
}
//获取session
req.sessionId = userId;
get(req.sessionId)
.then((sessionData) => {
if (sessionData == null) {
//初始化redis中的session值
set(req.sessionId, {});
// 设置session
req.session = {};
} else {
req.session = sessionData;
}
console.log("req.session ", req.session);
// 处理post data
return getPostData(req);
})
.then((postData) => {
...
});
};
module.exports = serverHandle;
修改router->user.js
const { set } = require("../db/redis");
// 登录
if (method === "POST" && req.path === "/api/user/login") {
// const result = login(req.query);
const result = login(req.body);
return result.then((data) => {
if (data.username) {
// 设置session
req.session.username = data.username;
req.session.realname = data.realname;
//同步到 redis
set(req.sessionId, req.session);
console.log("req.session is ", req.session);
return new SuccessModel();
}
return new ErrorModel("登录失败");
});
}
完成server端登录的代码
有了redis存储sessionId对应的登录用户信息后,之前更新博客、新建博客和删除博客都可以对登录进行验证,还可以拿到当前登录的作者信息。
// 统一的登录验证函数
const loginCheck = (req) => {
if (!req.session.username) {
return Promise.resolve(new ErrorModel("尚未登录"));
}
};
const handleBlogRouter = (req, res) => {
const method = req.method; // GET POST
const id = req.query.id;
...
// 新建一篇博客
if (method === "POST" && req.path === "/api/blog/new") {
// const data = newBlog(req.body);
// return new SuccessModel(data);
const loginCheckResult = loginCheck(req);
if (loginCheckResult) {
return loginCheckResult;
}
req.body.author = req.session.username;
const result = newBlog(req.body);
return result.then((data) => {
return new SuccessModel(data);
});
}
// 更新一篇博客
if (method === "POST" && req.path === "/api/blog/update") {
const loginCheckResult = loginCheck(req);
if (loginCheckResult) {
return loginCheckResult;
}
const result = updateBlog(id, req.body);
return result.then((val) => {
if (val) {
return new SuccessModel("更新博客成功");
} else {
return new ErrorModel("更新博客失败");
}
});
}
// 删除博客
if (method === "POST" && req.path === "/api/blog/del") {
const loginCheckResult = loginCheck(req);
if (loginCheckResult) {
return loginCheckResult;
}
const author = req.session.username; // 假数据,待开发登录时再改成真实数据
const result = delBlog(id, author);
console.log("result", result);
return result.then((val) => {
if (val) {
return new SuccessModel();
} else {
return new ErrorModel("删除博客失败");
}
});
}
};
和前端联调
- 登录功能依赖cookie,必须用浏览器来联调
- cookie跨域不共享的,前端和server端必须同域
- 需要用到nignx做代理,让前后端同域
创建一个html-test项目,分别创建以下几个文件,获取api数据显示在页面上
admin.html -- 管理中心页面
detail.html -- 博客详情页面
edit.html -- 编辑博客页面
index.html -- 首页
login.html -- 登录页面
new.html --新建博客页面
在index.html尝试请求博客列表,由于跨域无法访问,所以需要nginx配置进行反向代理解决。
<body>
<h1>博客首页</h1>
<div id="blog-container">
<ul id="blog-list"></ul>
</div>
<script>
function getUrlParams() {
let paramStr = location.href.split("?")[1] || "";
paramStr = paramStr.split("#")[0];
const result = {};
paramStr.split("&").forEach((itemStr) => {
const arr = itemStr.split("=");
const key = arr[0];
const val = arr[1];
result[key] = val;
});
return result;
}
const url = "/api/blog/list";
const urlParams = getUrlParams();
let dataList = [];
if (urlParams.author) {
url += "?author=" + urlParams.author;
}
fetch(url)
.then((res) => {
if (res.status == 200) {
return res.json();
} else {
return Promise.reject(res.json());
}
})
.then(function (data) {
const container = document.getElementById("blog-list");
dataList = data.data;
for (let i = 0; i < dataList.length; i++) {
const li = document.createElement("li");
const data = dataList[i];
li.textContent = `${data.id} - ${data.title} - ${data.author} `;
container.appendChild(li);
}
})
.catch((err) => {
console.log("err", err);
});
</script>
</body>
nginx介绍
- 高性能的web服务器,开源免费
- 一般用于做静态服务、负载均衡
- 反向代理
下载:
- Windows:http://nginx.org/en/download.html
- Mac:brew install nginx
配置:
- Windows:C:\nginx\conf\nginx.conf
- Mac:/usr/local/etc/nginx/nginx.conf
命令(windows和mac有出入,可自行百度):
- 测试配置文件格式是否正确 nginx -t
- 启动nginx;重启nginx -s reload
- 停止nginx -s stop
修改nginx的配置文件
server {
listen 8080;
server_name localhost;
#location / {
#root html;
#index index.html index.htm;
#}
location / {
proxy_pass http://localhost:8001;
}
location /api/ {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
}
}
listen 8080
是将服务器的接口地址以及前端页面共同指向为8080端口,所以联调时,应当访问8080端口。
proxy_pass
是反向代理的地址,根据实际情况进行修改即可。
修改完毕后,可以测试配置文件格式是否正确 nginx -t,然后start nginx启动nginx。
接着启动服务器8000端口,前端页面8001端口,在浏览器访问http://127.0.0.1:8001/index.html,接口依旧跨域无法访问,但是访问http://127.0.0.1:8080/index.html 却可以成功拿到接口数据,说明nginx配置成功。
所有页面的联调
在admin.html需要在请求数据时添加一个isadmin
标志,因为是管理中心,只能管理自己的页面
let url = "/api/blog/list?isadmin=1"; // 增加一个 isadmin=1 参数,使用登录者的用户名
并且需要修改后端代码
// 获取博客列表
if (method === "GET" && req.path === "/api/blog/list") {
let author = req.query.author || "";
const keyword = req.query.keyword || "";
if (req.query.isadmin) {
const loginCheckResult = loginCheck(req);
if (loginCheckResult) {
return loginCheckResult;
}
// 强制查询自己的博客
author = req.session.username;
}
const result = getList(author, keyword);
return result.then((listData) => {
return new SuccessModel(listData);
});
}
admin.html
<!DOCTYPE html>
<html>
<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" />
<title>管理中心</title>
<style type="text/css">
body {
margin: 0 20px;
line-height: 1;
}
a {
text-decoration-line: none;
cursor: pointer;
}
table {
border: 1px solid #ccc;
}
table th {
text-align: left;
background-color: #f1f1f1;
}
table td:nth-child(1) {
width: 300px;
}
</style>
</head>
<body>
<h1 style="border-bottom: 1px solid #ccc; padding-bottom: 10px">
管理中心
</h1>
<p>
<a href="/new.html">新建博客</a>
</p>
<div style="margin-bottom: 10px">
<input id="text-keyword" />
<button id="btn-search">搜索</button>
</div>
<table id="table-container">
<tr>
<th>博客标题</th>
<th colspan="2">操作</th>
</tr>
</table>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
// 发送 get 请求
function get(url) {
return $.get(url);
}
// 发送 post 请求
function post(url, data = {}) {
return $.ajax({
type: "post",
url,
data: JSON.stringify(data),
contentType: "application/json",
});
}
// 获取 url 参数
function getUrlParams() {
let paramStr = location.href.split("?")[1] || "";
paramStr = paramStr.split("#")[0];
const result = {};
paramStr.split("&").forEach((itemStr) => {
const arr = itemStr.split("=");
const key = arr[0];
const val = arr[1];
result[key] = val;
});
return result;
}
// 获取 dom 元素
const $textKeyword = $("#text-keyword");
const $btnSearch = $("#btn-search");
const $tableContainer = $("#table-container");
// 拼接接口 url
let url = "/api/blog/list?isadmin=1"; // 增加一个 isadmin=1 参数,使用登录者的用户名,后端也需要修改 !!!
const urlParams = getUrlParams();
if (urlParams.keyword) {
url += "&keyword=" + urlParams.keyword;
}
// 加载数据
get(url).then((res) => {
if (res.errno !== 0) {
alert("数据错误");
return;
}
// 显示数据
const data = res.data || [];
data.forEach((item) => {
$tableContainer.append(
$(`
<tr>
<td>
<a href="/detail.html?id=${
item.id || item._id
}" target="_blank">${item.title}</a>
</td>
<td>
<a href="/edit.html?id=${
item.id || item._id
}">编辑</a>
</td>
<td>
<a data-id="${
item.id || item._id
}" class="item-del">删除</a>
</td>
</tr>
`)
);
});
});
// 搜索
$btnSearch.click(() => {
const keyword = $textKeyword.val();
location.href = "/admin.html?keyword=" + keyword;
});
// 删除
$tableContainer.click((e) => {
const $target = $(e.target);
if ($target.hasClass("item-del") === false) {
return;
}
if (confirm("确定删除?")) {
const url = "/api/blog/del?id=" + $target.attr("data-id");
post(url).then((res) => {
if (res.errno !== 0) {
alert("操作错误");
return;
}
location.href = location.href;
});
}
});
</script>
</body>
</html>
detail.html
<!DOCTYPE html>
<html>
<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" />
<title>博客详情页</title>
<style type="text/css">
body {
margin: 0 20px;
line-height: 1;
}
a {
text-decoration-line: none;
}
.title {
font-size: 20px;
font-weight: bold;
}
.info-container span,
.info-container a {
font-size: 14px;
color: #999;
}
.content-wrapper {
margin-top: 20px;
border-top: 1px solid #ccc;
}
</style>
</head>
<body>
<h1 id="title" class="title"></h1>
<div id="info-container" class="info-container"></div>
<div class="content-wrapper">
<p id="content"></p>
</div>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script src="https://cdn.bootcss.com/moment.js/2.23.0/moment.min.js"></script>
<script src="https://cdn.bootcss.com/moment.js/2.23.0/locale/zh-cn.js"></script>
<script>
// 发送 get 请求
function get(url) {
return $.get(url);
}
// 显示格式化的时间
function getFormatDate(dt) {
return moment(dt).format("LL");
}
// 获取 url 参数
function getUrlParams() {
let paramStr = location.href.split("?")[1] || "";
paramStr = paramStr.split("#")[0];
const result = {};
paramStr.split("&").forEach((itemStr) => {
const arr = itemStr.split("=");
const key = arr[0];
const val = arr[1];
result[key] = val;
});
return result;
}
// 获取 dom 元素
const $title = $("#title");
const $infoContainer = $("#info-container");
const $content = $("#content");
// 获取数据
const urlParams = getUrlParams();
const url = "/api/blog/detail?id=" + urlParams.id;
get(url).then((res) => {
if (res.errno !== 0) {
alert("数据错误");
return;
}
// 显示数据
const data = res.data || {};
$title.text(data.title);
$content.text(data.content);
$infoContainer.append(
$(`
<span>
<a href="/index.html?author=${data.author}">${
data.author
}</a>
</span>
<span>${getFormatDate(data.createtime)}</span>
`)
);
});
</script>
</body>
</html>
edit.html
<!DOCTYPE html>
<html>
<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" />
<title>编辑博客</title>
<style type="text/css">
body {
margin: 0 20px;
line-height: 1;
}
a {
text-decoration-line: none;
}
.title-wrapper {
margin-bottom: 10px;
}
.title-wrapper input {
width: 300px;
}
.content-wrapper textarea {
width: 300px;
height: 150px;
}
</style>
</head>
<body>
<h1 style="border-bottom: 1px solid #ccc; padding-bottom: 10px">
编辑博客
</h1>
<div>
<div class="title-wrapper">
<input id="text-title" />
</div>
<div class="content-wrapper">
<textarea id="text-content"></textarea>
</div>
<div>
<button id="btn-update">保存</button>
</div>
</div>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
// 发送 get 请求
function get(url) {
return $.get(url);
}
// 发送 post 请求
function post(url, data = {}) {
return $.ajax({
type: "post",
url,
data: JSON.stringify(data),
contentType: "application/json",
});
}
// 获取 url 参数
function getUrlParams() {
let paramStr = location.href.split("?")[1] || "";
paramStr = paramStr.split("#")[0];
const result = {};
paramStr.split("&").forEach((itemStr) => {
const arr = itemStr.split("=");
const key = arr[0];
const val = arr[1];
result[key] = val;
});
return result;
}
// 获取 dom 元素
const $textTitle = $("#text-title");
const $textContent = $("#text-content");
const $btnUpdate = $("#btn-update");
// 获取博客内容
const urlParams = getUrlParams();
const url = "/api/blog/detail?id=" + urlParams.id;
get(url).then((res) => {
if (res.errno !== 0) {
alert("操作错误");
return;
}
// 显示数据
const data = res.data || {};
$textTitle.val(data.title);
$textContent.val(data.content);
$btnUpdate.attr("data-id", data.id || data._id);
});
// 提交修改内容
$btnUpdate.click(function () {
const $this = $(this);
const id = $this.attr("data-id");
const title = $textTitle.val();
const content = $textContent.val();
const url = "/api/blog/update?id=" + id;
const data = {
title,
content,
};
post(url, data).then((res) => {
if (res.errno !== 0) {
alert("操作错误");
return;
}
alert("更新成功");
location.href = "/admin.html";
});
});
</script>
</body>
</html>
index.html
<!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" />
<title>首页</title>
<style type="text/css">
body {
margin: 0 20px;
line-height: 1;
}
a {
text-decoration-line: none;
}
.title-wrapper {
padding: 15px 0;
border-top: 1px solid #ccc;
}
.title-wrapper .title {
font-size: 20px;
font-weight: bold;
}
.title-wrapper .info-wrapper span,
.title-wrapper .info-wrapper a {
font-size: 14px;
color: #999;
}
</style>
</head>
<body>
<h1>博客首页</h1>
<div id="blog-container"></div>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script src="https://cdn.bootcss.com/moment.js/2.23.0/moment.min.js"></script>
<script src="https://cdn.bootcss.com/moment.js/2.23.0/locale/zh-cn.js"></script>
<script>
// 发送 get 请求
function get(url) {
return $.get(url);
}
// 显示格式化的时间
function getFormatDate(dt) {
return moment(dt).format("LLL");
}
// 获取 url 参数
function getUrlParams() {
let paramStr = location.href.split("?")[1] || "";
paramStr = paramStr.split("#")[0];
const result = {};
paramStr.split("&").forEach((itemStr) => {
const arr = itemStr.split("=");
const key = arr[0];
const val = arr[1];
result[key] = val;
});
return result;
}
// 获取 dom 元素
const $container = $("#blog-container");
// 拼接接口 url
let url = "/api/blog/list";
const urlParams = getUrlParams();
if (urlParams.author) {
url += "?author=" + urlParams.author;
}
// 加载数据
get(url).then((res) => {
if (res.errno !== 0) {
alert("数据错误");
return;
}
// 遍历博客列表,并显示
const data = res.data || [];
data.forEach((item) => {
$container.append(
$(`
<div class="title-wrapper">
<p class="title">
<a href="/detail.html?id=${
item.id || item._id
}" target="_blank">${item.title}</a>
</p>
<div class="info-wrapper">
<span>
<a href="/index.html?author=${item.author}">${
item.author
}</a>
</span>
<span>${getFormatDate(item.createdAt)}</span>
</div>
</div>
`)
);
});
});
</script>
</body>
</html>
login.html
<!DOCTYPE html>
<html>
<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" />
<title>登录</title>
</head>
<body>
<div>
<label> 用户名 <input type="text" id="textUsername" /> </label>
<label> 密码 <input type="password" id="textPassword" /> </label>
<button id="btnLogin">登录</button>
</div>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
// 发送 post 请求
function post(url, data = {}) {
return $.ajax({
type: "post",
url,
data: JSON.stringify(data),
contentType: "application/json",
});
}
$("#btnLogin").click(() => {
const username = $("#textUsername").val();
const password = $("#textPassword").val();
const url = "/api/user/login";
const data = {
username,
password,
};
post(url, data).then((res) => {
if (res.errno === 0) {
// 登录成功
location.href = "./admin.html";
} else {
// 登录失败
alert(res.message);
}
});
});
</script>
</body>
</html>
new.html
<!DOCTYPE html>
<html>
<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" />
<title>创建博客</title>
<style type="text/css">
body {
margin: 0 20px;
line-height: 1;
}
a {
text-decoration-line: none;
}
.title-wrapper {
margin-bottom: 10px;
}
.title-wrapper input {
width: 300px;
}
.content-wrapper textarea {
width: 300px;
height: 150px;
}
</style>
</head>
<body>
<h1 style="border-bottom: 1px solid #ccc; padding-bottom: 10px">
创建博客
</h1>
<div>
<div class="title-wrapper">
<input id="text-title" />
</div>
<div class="content-wrapper">
<textarea id="text-content"></textarea>
</div>
<div>
<button id="btn-create">创建</button>
</div>
</div>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
// 发送 post 请求
function post(url, data = {}) {
return $.ajax({
type: "post",
url,
data: JSON.stringify(data),
contentType: "application/json",
});
}
// 获取 dom 元素
$textTitle = $("#text-title");
$textContent = $("#text-content");
$btnCreate = $("#btn-create");
// 提交数据
$btnCreate.click(() => {
const title = $textTitle.val().trim();
const content = $textContent.val().trim();
if (title === "" || content === "") {
alert("标题或内容不能为空");
return;
}
const url = "/api/blog/new";
const data = {
title,
content,
};
post(url, data).then((res) => {
if (res.errno !== 0) {
alert("操作错误");
return;
}
alert("创建成功");
location.href = "/admin.html";
});
});
</script>
</body>
</html>
通过CORS实现跨域
- HTTP协议的规范,现代浏览器都支持
- 前端和服务端直接通讯,不用nginx做转发
- 通过服务端设置header来实现
Response setHeader
- Access-Control-Allow-Credentials
- Access-Control-Allow-Origin
- Access-Control-Allow-Methods
编写cors.html,然后访问http://localhost:8001/cors.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CORS test</title>
</head>
<body>
<p>CORS test - 查看网络请求</p>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
// 跨域请求
$.get("http://localhost:8000/api/blog/list", (res) => {
console.log("res", res);
});
</script>
</body>
</html>
这时候访问cors.html,控制台会显示跨域的错误。
在app.js中的serverHandle方法里在header里添加以上三个跨域信息
res.setHeader("Access-Control-Allow-Credentials", true); // 允许跨域传递 cookie
res.setHeader("Access-Control-Allow-Origin", "*"); // 允许跨域的origin,*代表所有的域
res.setHeader(
"Access-Control-Allow-Methods",
"GET,POST,OPTIONS,PUT,PATCH,DELETE"
); // 被允许跨域的Http方法
此时重新访问,可以看到控制台打印出接口数据。
Express框架 - 使用第三方中间件cors
//实例
const express = require('express')
const cors = require('cors')
cosnt app = express()
//CORS 允许跨域
app.use(
cors({
origin:'*' // 或设置单个 origin
//其他配置参考 https://www.npmjs.com/package/cors
})
)
//路由
app.get('/',(req,res,next)=>{
res.json({
errno:0,
msg:'CORS express'
})
})
app.listen(8000,()=>{
console.log('server is running on port 8000')
})
Koa2框架 - 使用第三方中间件 koa-cors
//实例
const Koa = require('koa')
const cors = require('koa-cors')
const app = new Koa()
//CORS 允许跨域
app.use(
cors({
origin:'*' // 或设置单个 origin
//其他配置参考 https://www.npmjs.com/package/koa-cors
})
)
app.use(async ctx=>{
ctx.body = {
errno:0,
msg:'CORS koa2'
}
})
app.listen(8000)