Express+Koa2学习笔记(四)

发布时间 2024-01-01 23:10:55作者: 小风车吱呀转

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

piqYQZ6.png

在浏览器控制台打印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安装

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: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配置成功。

piXTz7j.png

所有页面的联调

在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)