NodeJS Bolg实战 笔记

262 阅读12分钟

一 接口

1.1 node处理http请求

  • 从浏览器输入地址 到 显示 的过程是什么?
    • DNS解析,建立TCP连接,发送HTTP请求
    • server 接收到http请求,处理并返回
    • 客户端接收到返回的数据,处理数据、渲染页面
  • 处理HTTP请求
    • npm init 初始化项目

    •   const http = require(“http”);  
          
        const server = http.createServer((req, res) => {  
            res.end(“hello world”)  
        });  
        server.listen(8000)  
      
    • get请求和querystring

      • index.js

        const http = require("http");  
        const querystring = require("querystring");  
          
        const server = http.createServer((req, res) => {  
          console.log("method: ", req.method); //==> GET  
          const url = req.url;  
          console.log("url: ", url);  
          req.query = querystring.parse(url.split("?")[1]);  
          console.log("qurey: ", req.query);  
          res.end(JSON.stringify(req.query));  
        });  
        server.listen(8000);  
        
      • node index.js

      • 浏览器访问 http://localhost:8000/?a=100&b=200

    • post请求和postdata

      • Postman / chrome 插件 模拟post请求
      • index.js
        const http = require("http");  
          
        const server = http.createServer((req, res) => {  
          if (req.method === "POST") {  
            // 数据格式  
            console.log("content-type:", req.headers["content-type"]);  
            // 接收数据  
            let postData = "";  
            req.on("data", (chunk) => {  
              postData += chunk.toString();  
            });  
            req.on("end", () => {  
              console.log(postData);  
              res.end("hello world");  
            });  
          }  
        });  
        server.listen(8000);  
        
    • 路由

    • 综合示例

      •   const http = require("http");  
          const querystring = require("querystring");  
            
          const server = http.createServer((req, res) => {  
            const { method, url } = req;  
            const path = url.split("?")[0];  
            const query = querystring.parse(url.split("?")[1]);  
            
            // 设置返回格式为JSON  
            res.setHeader("Content-type", "application/json");  
            
            // 返回的数据  
            const resData = {  
              method,  
              url,  
              path,  
              query,  
            };  
            
            if (method === "GET") {  
              res.end(JSON.stringify(resData));  
            }  
            if (method === "POST") {  
              let postData = "";  
              req.on("data", (chunk) => {  
                postData += chunk.toString();  
              });  
              req.on("end", () => {  
                resData.postData = postData;  
                res.end(JSON.stringify(resData));  
              });  
            }  
          });  
          server.listen(8000);  
        

1.2 搭建开发环境

  • nodemon监测文件变化,自动重启node

  • cross-env设置环境变量,兼容mac / window / linux

  • package.json

    {  
      "main": "bin/www.js",  
      “scripts”:{  
        “dev”: “cross-env NODE_ENV=dev nodemon ./bin/www.js”,  
        “prd”: “cross-env NODE_ENV=production nodemon ./bin/www.js”  
      }  
    }  
    
    • process.env.NODE_ENV
  • bin/www.js
    只关心HTTP纯技术层

    const http = require('http')  
    const serverHandle = require('../app')  
      
    const PORT = 8000  
      
    const server = http.createServer(serverHandle)  
      
    server.listen(PORT)  
    
  • app.js

    const serverHandle = (req, res) => {  
      // 设置返回格式为JSON  
      res.setHeader('Content-type', 'application/json')  
      
      // 获取 path  
      req.path = req.url.split('?')[0]  
      
      // 解析 query  
      req.query = querystring.parse(req.url.split('?')[1])  
    })  
    

1.3 开发接口(暂不连数据库,不考虑登录)

  • 初始化路由
    • app.js

      const handleBlogRouter = require('./src/router/blog')  
      const handleUserRouter = require('./src/router/user')  
        
      const serverHandle = (req, res) => {  
          
        // 设置返回格式为JSON  
        res.setHeader('Content-type', 'application/json’)};  
        
        // 获取 path  
        const url = req.url;  
        req.path = url.split(‘?’)[0]  
        
        // 解析 query  
        req.query = querystring.parse(req.url.split('?')[1])  
        
        // 处理blog路由  
        const blogData = handleBlogRouter(req, res);  
        if(blogData){  
          res.end(JSON.stringify(blogData));  
          return;  
        }    
        
        // 处理user  
        const userData = handleUserRouter(req, res);  
        if(userData){  
          res.end(JSON.stringify(userData));  
          return;  
        }  
        
        // 未命中路由,返回404  
        res.writeHead(404, {“Content-type”: “text-plain”});  
        res.write(“404 Not Found\n”);  
        res.end();  
        
      }  
      module.exports = serverHandle  
      
      • src/router/blog.js

        const handleBlogRouter = (req, res) => {  
          const method = req.method  // GET POST  
          const id = req.query.id  
          
          // 获取博客列表  
          if(method === 'GET' && req.path === '/api/blog/list') {  
            return {  
              msg: 'this is blog list api'  
            }  
          }  
          // ...  
        }  
          
        module.exports = handleBlogRouter  
        
      • src/router/user.js

        const handleUserRouter = (req, res) => {  
          const method = req.method  // GET POST  
          const id = req.query.id  
          
          // 登录  
          if(method === ‘POST’ && req.path === '/api/user/login’) {  
            return {  
              msg: 'this is login api'  
            }  
          }  
          // ...  
        }  
          
        module.exports = handleUserRouter  
        
    • src/model/resModel.js
      // 只关心状态

      class BaseModel {  
        constructor(data, message) {  
          if(typeof data === 'string') {  
            this.message = data  
            data = null  
            message = null  
          }  
          if(data) {  
            this.data = data  
          }  
          if(message) {  
            this.message = message  
          }  
        }  
      }  
        
      class SuccessModel extends BaseModel {  
        constructor(data, message) {  
          super(data, message)  
          this.errno = 0  
        }  
      }  
        
      class ErrorModel extends BaseModel {  
        constructor(data, message) {  
          super(data, message)  
          this.errno = -1  
        }  
      }  
      module.exports = {  
        SuccessModel,  
        ErrorModel  
      }  
      
      • src/router/blog.js
        // 只关心路由
        const { getList} = require('../controller/blog’);  
        const { SuccessModel, ErrorModel } = require('../model/resModel’);  
        const handleBlogRouter = (req, res) => {  
          const method = req.method  // GET POST  
          const id = req.query.id  
          
          // 获取博客列表  
          if(method === 'GET' && req.path === '/api/blog/list') {  
            const author = req.query.author || ‘’;  
            const keyword = req.query.keyword || '' ;  
          
            const result = getList(author, keyword)  
            return result.then(listData => {  
              return new SuccessModel(listData)  
            })    
          }  
          // ...  
        }  
          
        module.exports = handleBlogRouter  
        
  • 返回假数据
    • src/controller/blog.js
      只关心数据 操作数据库

      const getList = (author, keyword)=>{  
        return [  
          {  
            id:1,  
            title: “title1”,  
            content: “test text”,  
            createTime: 1546620491112,  
            author: ‘z3’  
          },  
          // ...  
        ]  
      }  
      module.exports = {  
        getList  
      }  
      
    • app.js

      // 处理 post data  
      const getPostData = (req) => {  
        const promise = new Promise((resolve, reject) => {  
          if(req.method !== 'POST') {  
            resolve({})  
            return  
          }  
          if(req.headers['content-type'] !== 'application/json') {  
            resolve({})  
            return  
          }  
        
          let postData = ''  
          req.on('data', chunk => {  
            postData += chunk.toString()  
          })  
          req.on('end', () => {  
            if(!postData) {  
              resolve({})  
              return  
            }  
            resolve(  
              JSON.parse(postData)  
            )  
          })  
        })  
        return promise  
      }  
        
      
      •   const serverHandle = (req, res) => {  
            // 设置返回格式为JSON  
            res.setHeader('Content-type', 'application/json')  
            
            // 获取 path  
            req.path = req.url.split('?')[0]  
            
            // 解析 query  
            req.query = querystring.parse(req.url.split('?')[1])  
            
            // post data  
            getPostData(req).then(postData => {  
              req.body = postData  
            
              // 登录路由  
              const userResult = handleUserRouter(req, res)  
              if(userResult) {  
                userResult.then(userData => {  
                  res.end(  
                    JSON.stringify(userData)  
                  )  
                })  
                return  
              }  
            
              // 处理博客路由  
              const blogResult = handleBlogRouter(req, res)  
              if(blogResult) {  
                blogResult.then(blogData => {  
                  res.end(  
                    JSON.stringify(blogData)  
                  )  
                })  
                return  
              }  
            
              // 未命中路由  
              res.writeHead(404, {'Content-type': 'text/plain'})  
              res.write('404 Not Found!\n')  
              res.end()  
            
            })  
            
          }  
            
        

二 MySQL数据存储

2.1 建库

  • 在MySQLWorkbench中 点库图标建库
    CREATE SCHEMA `myblog` ;

2.2 建表

  • 在MySQLWorkbench中, 左侧myblog > Tables > 右键 > create table > users or blogs
  • datatype
    • varchar(50) 长度50
    • longtext 长文本 最多4G大小
    • int 整数
    • bigint(20)
  • PK主建 不能重复
  • NN不能为空
  • AI自动增加
  • 点击右下 apply 按钮 完成

2.3 表操作

  • 使用sql语句
    •   use myblog;  
          
        show tables;  
        -- 注释  
          
        -- 如报错 Error Code: 1175. You are using safe update mode an... 执行下面语句  
        SET SQL_SAFE_UPDATES=0  
      
    •   insert into users(username, `password`, realname) values('zhang3', '123', '张三');  
        insert into users(username, `password`, realname) values('li4', '123', '李四');  
      
    •   -- 删  
        delete from users where username='li4';  
        -- 软删  
        update users set state='0' where username='li4';  
        -- 查state不等于0 使用<>  
        select * from users where state<>0’;  
      
    •   -- 改  
        update users set realname="李四2" where username='li4';  
      
    •   -- 查  
        select * from users;  
        select id, username from users;  
        -- 查 where 条件查询  
        select * from users where username="zhang3";  
        select * from users where username="zhang3" and `password`='123';  
        select * from users where username="zhang3" or `password`='123';  
        -- 查 like 模糊查询  
        select * from users where username like "%zhang%";  
        -- 查 order by 排序  
        select * from users order by id;  
        -- 查 order desc 倒序  
        select * from users order by id desc;  
      

2.4 Node操作mySQL

  • npm i mysql

  • index.js

    const mysql = require("mysql");  
      
    //创建链接对象  
    const con = mysql.createConnection({  
      host: "localhost",  
      user: "root",  
      password: "XXXXXX",  
      port: "3306",  
      database: "myblog",  
    });  
      
    // 开始连接  
    con.connect();  
      
    // 执行 sql 语句  
    const sql = "select * fromt users";  
    con.query(sql, (err, result) => {  
      if (err) {  
        console.error(err);  
        return;  
      }  
      console.log(result);  
    });  
      
    // 关闭连接  
    con.end();  
    
  •   -- 解决VSCode无法连接 errno: 1064  
      use mysql;  
      alter user 'root'@'localhost' identified with mysql_native_password by 'XXXXXX';  
      flush privileges;  
    
  • src/conf/db.js

    const env = process.env.NODE_ENV  
      
    let MYSQL_CONF  
      
    if(env === 'dev') {  
      MYSQL_CONF = {  
        host: 'localhost',  
        user: 'root',  
        password: ‘XXXXXX’,  
        port: '3306',  
        database: 'myblog'  
      }  
    }  
      
    if(env === 'production') {  
      MYSQL_CONF = {  
        host: 'localhost',  
        user: 'root',  
        password: 'XXXXXX',  
        port: '3306',  
        database: 'myblog'  
      }  
    }  
      
    module.exports = {  
      MYSQL_CONF  
    }  
    
  • src/db/mysql.js

    const mysql = require('mysql')  
    const { MYSQL_CONF } = require('./../conf/db')  
      
    // 创建链接对象  
    const con = mysql.createConnection(MYSQL_CONF)  
      
    // 开始连接  
    con.connect()  
      
    // 统一执行 sql 的函数  
    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  
    }  
    

2.5 API对接mySQL

  • src/controller/blog.js
    只关心数据 操作数据库

    const { exec } = require('./../db/mysql')  
      
    const getList = (author, keyword) => {  
      let sql = `select * from blogs where 1=1 `  
      if(author) {  
        sql += `and anthor='${author}' `  
      }  
      if(keyword) {  
        sql += `and title like '%${keyword}%' `  
      }  
      sql += `order by createtime desc;`  
      
      return exec(sql)  
    }  
      
    // ...  
      
    module.exports = {  
      getList  
    }  
    
  • src/router/blog.js
    // 只关心路由

    const { getList} = require('../controller/blog’);  
    const { SuccessModel, ErrorModel } = require('../model/resModel’);  
    const handleBlogRouter = (req, res) => {  
      const method = req.method  // GET POST  
      const id = req.query.id  
      
      // 获取博客列表  
      if(method === 'GET' && req.path === '/api/blog/list') {  
        const author = req.query.author || ‘’;  
        const keyword = req.query.keyword || '' ;  
      
        const result = getList(author, keyword)  
        return result.then(listData => {  
          return new SuccessModel(listData)  
        })    
      }  
      // ...  
    }  
      
    module.exports = handleBlogRouter  
    
  • app.js

    const handleBlogRouter = require('./src/router/blog')  
    const serverHandle = (req, res) => {  
      // ...  
      
      // 处理blog路由  
      const blogData = handleBlogRouter(req, res);  
      if(blogData){  
        res.end(JSON.stringify(blogData));  
        return;  
      }    
      
      // 处理user  
      // 未命中路由,返回404  
      
    }  
    module.exports = serverHandle  
    

三 登录

3.1 cookie 和 session

  • cookie
      • cookie是存在浏览器的一段字符串(最大5kb)
        • 跨域不共享
        • 格式如k1=v1;k2=v2;k3=k3; 因此可存储结构化数据
        • 每次发送http请求,会将请求域的cookie一起发送给server
        • server 可修改cookie并返回给浏览器
        • 浏览器中也可通过JS修改cookie(有限制)
    • JS操作cookie
      •   // 累加  
          document.cookie = ‘k1=100;’  
        
    • server端操作cookie
      •   // 解析cookie  
            req.cookie = {};  
            const cookieStr = req.headers.cookie || "";  
            // console.log(cookieStr);  
            cookieStr.split(";").map(item => {  
              if (!item) return;  
              const arr = item.split("=");  
              const key = arr[0].trim();  
              const value = arr[1].trim();  
              req.cookie[key] = value;  
            });  
        
      • 使用httpOnly限制前端修改cookie, expires设置过期时间

         res.setHeader(  
           "Set-Cookie",  
           `userid=${userId};path='/';httpOnly;expires=${getCookieExpires()}`  
        );  
        
    • cookie 登录流程
      • 1.前端发送 username password in cookie 到 后端
        2. 后端做验证
        3. 验证成功 userid写入cookie,设置httpOnly 和 expires给前端
        4. 前端在次访问后端,浏览器将自动带userid cookid
        5. 后端验证userid 判断是否登录
  • session
    • cookie 存在的问题
      • 问题:会暴露username,很危险。
        解决:可在cookie中存储userid,server端对应username -- 此为session
    • session代码示例
      •   // session数据  
          const SESSION_DATA = {};  
        
      •   // 解析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];  
        
      • router/user.js
        后端设置用户数据到req.session, 保证安全

        // 登陆  
          if (method === "POST" && req.path === "/api/user/login") {  
            const {  
              username,  
              password  
            } = req.body;  
            const result = login(username, password);  
            return result.then(data => {  
              if (data.username) {  
                // 设置session  
                req.session.username = data.username;  
                req.session.realname = data.realname;  
                return new SuccessModel(data);  
              }  
              return new ErrorModel("登录失败");  
            });  
          }  
        

3.2 session写入redis

  • session的问题
    • SESSION_DATA 占用nodeJS内存
      1. 访问量过大,内存暴增
      2. 正式上线后运行是多进程,进程之间内存无法共享
  • redis
      • 数据存放在内存中
        • 相比mysql,访问速度快
        • 成本更高,可存储的数据更少
  • 为何sessin适合用redis
      • session访问频繁,对性能要求极高
        • session可不考虑断电丢失数据的问题
        • session数据量不会太大
  • 为何网站数据不适用redis
      • 操作频率不高
        • 断电不能丢失,必须保留
        • 数据量太大,内存成本太高
  • redis基础
    • 安装

      brew install redis  
      

      启动server

      redis-server  
      

      启动cli
      redis-cli

      set get

      set myname zhang3  
      get myname  
        
      查看所有key  
      ```keys *```  
        
      删除key  
      ```del myname  
      

3.3 开发登录功能 node链接redis

  • demo
    • npm i redis --save
    •   const redis = require('redis')  
          
        const client = redis.createClient(6379,'127.0.0.1');  
          
        client.on('error',err=>{  
            console.error(err)  
        })  
          
        client.set('myname','zhangsan2',redis.print)  
        client.get('myname',(err,val)=>{  
            if(err){  
                console.log(err);  
                return;  
            }  
            console.log('val:',val);  
            client.quit()  
        })  
      
  • 封装工具函数
    • config/db.js

      const env = process.env.NODE_ENV  
        
      // 配置  
      let REDIS_CONF  
        
      if (env === 'dev') {  
        REDIS_CONF = {  
          port: 6379,  
          host: '127.0.0.1'  
        }  
      }  
        
      if (env === 'production') {  
         // redis  
        REDIS_CONF = {  
          port: 6379,  
          host: '127.0.0.1'  
        }  
      }  
        
      module.exports = {  
        REDIS_CONF  
      }  
      
    • db/redis.js

      const redis = require("redis");  
      const { REDIS_CONF } = require("../config/db");  
        
      // 创建客户端, 不quit() 因为是单例模式  
      const client = redis.createClient(REDIS_CONF.port, REDIS_CONF.host);  
      client.on("error", err => {  
        console.error(err);  
      });  
        
      const set = (key, val) => {  
        if (typeof val === "object") {  
          // 这边需要val是个string  
          val = JSON.stringify(val);  
        }  
        client.set(key, val, redis.print);  
      };  
        
      const get = key => {  
        const promise = new Promise((resolve, reject) => {  
          client.get(key, (err, val) => {  
            if (err) {  
              reject(err);  
              return;  
            }  
            // 未取到数据时,返回null  
            if (val === void 0) {  
              resolve(null);  
              return;  
            }  
            // try catch 是为了兼容JSON.parse  
            try {  
              resolve(JSON.parse(val));  
            } catch (error) {  
              resolve(val);  
            }  
              
          });  
        });  
        return promise;  
      };  
        
      module.exports = {  
        set,  
        get  
      };  
      
    • router/blog.js

      const loginCheck = req => {  
        console.log("req.session",req.session);  
        if (!req.session.username) {  
          return Promise.resolve(  
            new ErrorModel('尚未登录')  
          )  
        }  
      }  
        
      // 新建博客接口  
        if (method === "POST" && req.path === "/api/blog/new") {  
        
          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)  
          })  
        }  
      
    • app.js

      const { get, set } = require("./src/db/redis");  
        
      // 解析session(使用redis)  
        let needSetCookie = false;  
        let userId = req.cookie.userid;  
        console.log("userId", userId);  
        if (!userId) {  
          needSetCookie = true;  
          userId = `${Date.now()}_${Math.random()}`;  
          // 初始化redis 中session的初始值  
          set(userId, {});  
        }  
        // 为req创建一个sessionId属性,  
        req.sessionId = userId;  
        get(req.sessionId)  
          .then((sessionData) => {  
            if (!sessionData) {  
              // 初始化redis 中session的初始值  
              set(req.sessionId, {});  
              // 设置session  
              req.session = {};  
            } else {  
              req.session = sessionData;  
            }  
            return 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;  
            }  
            // 未命中路由: 返回404  
            res.writeHead(404, {  
              "Content-type": "text/plain",  
            });  
            res.write("404 Not Found\n");  
            res.end();  
          });  
      

四 日志

4.1 日志类型

  • 访问日志 access log
  • 自定义日志(包括自定义事件、错误记录等)

4.2 node文件操作 stream

  •   const fs = require("fs");  
      const path = require("path");  
        
      const fileName = path.resolve(__dirname, "data.jon.txt");  
        
      // read file  
      fs.readFile(fileName, (err, data) => {  
        if (err) {  
          console.error(err);  
          return;  
        }  
        // data为二进制,需转换为字符串  
        console.log(data.toString());  
      });  
        
      // write file  
      const content = "new content\n";  
      const opt = {  
        flag: "a", // 追加, 覆盖用 w  
      };  
      fs.writeFile(fileName, content, opt, (err) => {  
        if (err) {  
          console.error(err);  
          return;  
        }  
      });  
        
      // exists  
      fs.exists(fileName, (exist) => {  
        console.log(exist);  
      });  
    
    • pipe

      const path = require("path")  
      const fs = require("fs")  
        
      const fileName1 = path.resolve(__dirname,'data.txt')  
      const fileName2 = path.resolve(__dirname,'data-back.txt')  
        
      const readSteam = fs.createReadStream(fileName1)  
      const writeSteam = fs.createWriteStream(fileName2)  
        
      readSteam.pipe(writeSteam)  
      readSteam.on('data',chunk=>{  
          console.log(chunk.toString());  
      })  
      readSteam.on('end',()=>{  
          console.log("copy done");  
      })  
      
    •   const http = require('http');  
        const path = require("path")  
        const fs = require("fs")  
        const fileName1 = path.resolve(__dirname,'data.txt')  
        const server = http.createServer((req, res)=>{  
            if(req.method === ‘GET’){  
                const readSteam = fs.createReadStream(fileName1)  
                readSteam.pipe(res)  
            }  
        })  
        server.listen(8000)  
      

4.3 日志功能开发和使用

  • src/logs/access.log
    src/logs/error.log
    src/logs/event.log

  • src/utils/log.js

    const path = require('path')  
    const fs = require("fs")  
      
    // 写日志  
    function writeLog(writeStream, log) {  
        writeStream.write(log + '\n')  
    }  
      
    // 生成writeStream  
    function createWriteStream(fileName) {  
        const fullFileName = path.join(__dirname, "../../logs/", fileName)  
        const writeStream = fs.createWriteStream(fullFileName, {  
            flags: 'a'  
        })  
        return writeStream;  
    }  
      
    // 写访问日志  
    const accessWriteStream = createWriteStream('access.log')  
      
    function access(log) {  
        writeLog(accessWriteStream, log)  
    }  
      
    module.exports = {  
        access  
    }  
    
  • src/app.js

    const serverHandle = (req, res) => {  
      // 记录 access log  
      access(  
        `${req.method} -- ${req.url} -- ${  
          req.headers["user-agent"]  
        } -- ${Date.now()}`  
      );  
    //...  
    

4.4 日志文件拆分、日志内容分析

    • 日志内容会慢慢积累,放在一个文件中不好处理
      • 按时间划分日志文件:2020-10-10.access.log
      • 实现方式:linux的crontab命令
  • crontab
    • 设置定时任务,格式,*****command
    • 将access.log拷贝并重命名为2020-10-10.access.log
    • 清空access.log文件,继续积累日志
    •   !/bin/sh  
        cd /Uders/code-demo/logs.log  
        cp access.log $(data +%Y-%m-%d).access.log  
        echo “” > access.log  
      
  • 日志分析
    • 如针对access.log日志,分析chrome的占比
    • 日志是按行存储的,一行就是一条日志
    • 使用nodejs的readline(基于stream,效率高)
    • src/utils/readline.js
      const fs = require("fs");  
      const path = require("path");  
      const readline = require("readline");  
        
      // 文件名  
      const fileName = path.join(__dirname, "../../logs", "access.log");  
      // 创建 read stream  
      const readStream = fs.createReadStream(fileName);  
        
      // 创建 radline 对象  
      const rl = readline.createInterface({  
        input: readStream,  
      });  
        
      let chromeNum = 0;  
      let sum = 0;  
        
      //逐行读取  
      rl.on("line", (lineData) => {  
        if (!lineData) {  
          return;  
        }  
        //记录总行数  
        sum++;  
        
        const arr = lineData.split(" -- ");  
        if (arr[2] && arr[2].indexOf("Chrome") > 0) {  
          // 累加 chrome 数量  
          chromeNum++;  
        }  
      });  
        
      rl.on("close", () => {  
        console.log("chrome 占比:" + chromeNum / sum);  
      });  
      

五 安全

* server端攻击方式非常多,预防手段也非常多

  • 本文只讲解常见能通过web server(nodejs)层面预防的
  • 有些攻击需要硬件和服务来支持(需要OP支持),如DDOS

5.1 SQL注入

    • 最原始、最简单的攻击
      • 攻击方式:输入一个sql片段,最终拼接成一段攻击代码
      • 预防措施:使用mysql的escape函数处理输入内容即可
      •   const mysql = require('mysql')  
          const login = (username, password) => {  
            username = mysql.escape(username);  
            password = mysql.escape(password);  
            // ...  
          };  
        

5.2 XSS攻击

    • 攻击方式:在页面展示内容中掺杂JS代码,以获取网页信息
      • 预防措施:转换生成JS的特殊字符

      •   & => &amp;  
          < => &lt;  
          > => &gt;  
          “ => &quot;  
          ‘ => &#x27;  
          / => &#x2f;  
        
      • blog.js

        const xss = require('xss');  
        const newBlog = (blogData = {}) => {  
          const title = xss(blogData.title)  
          //...  
        }  
        

5.3 密码加密

    • 攻击方式:获取用户名和密码,再去尝试登录其他系统
      • 预防措施:将密码加密
      •   const crypto = require("crypto");  
            
          // 密匙  
          const SECRET_KEY = "AbEdQe_123#$";  
            
          // md5加密  
          function md5(content) {  
            let md5 = crypto.createHash("md5");  
            return md5.update(content).digest("hex");  
          }  
            
          // 加密函数  
          function genPassword(password) {  
            const str = `password=${password}&key=${SECRET_KEY}`;  
            return md5(str);  
          }  
        

六 使用express重构

6.1 中间件原理

  • 安装
    npm i express-generator -g
    创建目录
    express blog-express

    package.json

    “scripts”:{  
      “dev”: “cross-env NODE_ENV=dev nodemon ./bin/www”  
    }  
    
  •   app.use((req, res, next) => {  
        console.log("请求开始...", req.method, req.url);  
        next();  
      });  
        
      app.use('/api', (req, res, next)=>{  
        console.log('处理 /api 路由')  
        next()  
      })  
        
      app.get("/api/get-cookit", (req, res, next) => {  
        console.log("get /api/get-cookit 路由");  
        res.json({ erron: 0, data: "ok" });  
      });  
        
      app.post("/api/get-post-data", (req, res, next) => {  
        console.log("post /api/get-post-data 路由");  
        res.json({ erron: 0, data: "ok" });  
      });  
        
      app.use((req, res, next) => {  
        // 404  
        res.json({ erron: 1, data: 404 });  
      });  
        
        
        
    
    • 通过next() 往下串连
    • 可传多和函数
      app.get("/api/get-cookit", loginCheck, (req, res, next) => {  
        console.log("get /api/get-cookit 路由");  
        res.json({ erron: 0, data: "ok" });  
      });  
        
      

6.2 使用中间件机制

  • app.js

      
    // 将GET, 解析到req.cookie  
    var cookieParser = require('cookie-parser');  
      
    // 解析JSON POST数据,挂载到req.body  
    app.use(express.json());  
      
    // 解析非JSON格式,挂载到req.body  
    app.use(express.urlencoded({ extended: false }));  
    
  • app.js 日志

    // 记录log  
    var logger = require('morgan');  
      
    const ENV = process.env.NODE_ENV;  
    if (ENV !== "production") {  
      // 开发环境 / 测试环境  
      app.use(logger("dev"));  
    } else {  
      // 线上环境  
      const logFileName = path.join(__dirname, "logs", "access.log");  
      const writeStream = fs.createWriteStream(logFileName, {  
        flags: "a",  
      });  
      app.use(  
        logger("combined", {  
          stream: writeStream,  
        })  
      );  
    }  
    
    • 开发环境输出到控制台
      线上环境输出到access.log
  • app.js session

    // session redis  
    const session = require('express-session')  
    const RedisStore = require('connect-redis')(session)  
      
    const redisClient = require("./db/redis");  
    const sessionStore = new RedisStore({  
      client: redisClient,  
    });  
    app.use(  
      session({  
        secret: "WJiol#23123_",  
        cookie: {  
          // path: '/',   // 默认配置  
          // httpOnly: true,  // 默认配置  
          maxAge: 24 * 60 * 60 * 1000,  
        },  
        store: sessionStore,  
      })  
    );  
    
    • db/redis.js
      const redis = require('redis')  
      const { REDIS_CONF } = require('../conf/db.js')  
        
      // 创建客户端  
      const redisClient = redis.createClient(REDIS_CONF.port, REDIS_CONF.host)  
      redisClient.on('error', err => {  
          console.error(err)  
      })  
        
      module.exports = redisClient  
      
  • app.js router

    const userRouter = require('./routes/user’);  
    app.use('/api/user', userRouter);  
    
    • routes/index.js

      var express = require('express');  
      var router = express.Router();  
        
      // 与上层app.use中 ’/api/blog’一起拼接成了 ‘/api/blog/list’  
      router.get(‘/list’, function(req, res, next) {  
        res.json({ errno: 0, data:’OK’ });  
      });  
      
    • routes/user.js

      var express = require('express');  
      var router = express.Router();  
      const { login } = require('../controller/user')  
      const { SuccessModel, ErrorModel } = require('../model/resModel')  
        
      router.post('/login', function(req, res, next) {  
          const { username, password } = req.body  
          const result = login(username, password)  
          return result.then(data => {  
              if (data.username) {  
                  // 设置 session  
                  req.session.username = data.username  
                  req.session.realname = data.realname  
        
                  res.json(  
                      new SuccessModel()  
                  )  
                  return  
              }  
              res.json(  
                  new ErrorModel('登录失败')  
              )  
          })  
      });  
        
      module.exports = router;  
      
    • router/blog.js

      const loginCheck = require('../middleware/loginCheck')  
      router.post('/new', loginCheck, (req, res, next) => {  
          req.body.author = req.session.username  
          const result = newBlog(req.body)  
          return result.then(data => {  
              res.json(  
                  new SuccessModel(data)  
              )  
          })  
      })  
      
      • loginCheck.js
        const { ErrorModel } = require('../model/resModel')  
          
        module.exports = (req, res, next) => {  
            if (req.session.username) {  
                next()  
                return  
            }  
            res.json(  
                new ErrorModel('未登录')  
            )  
        }  
        
  • app.js 404

    // 优化404页面提示文本  
    var createError = require('http-errors');  
    app.use(function(req, res, next) {  
      next(createError(404));  
    });  
    

6.3 中间件实现思路

  • 分析
    • 通过app.use注册
    • 遇到http请求,根据path和method判断触发哪些
    • 实现next机制,即上一个通过next触发下一个

七 使用Koa2重构

7.1 开发koa2

  • 安装

    npm i koa-generator -g  
    Koa2 koa2-test  
    npm i & npm run dev  
    
  • app.js 登录

    const session = require('koa-generic-session')  
    const redisStore = require('koa-redis')  
    const user = require('./routes/user')  
      
    // session 配置  
    app.keys = ['WJiol#23123_']  
    app.use(session({  
      // 配置 cookie  
      cookie: {  
        path: '/',  
        httpOnly: true,  
        maxAge: 24 * 60 * 60 * 1000  
      },  
      // 配置 redis  
      store: redisStore({  
        // all: '127.0.0.1:6379'   // 写死本地的 redis  
        all: `${REDIS_CONF.host}:${REDIS_CONF.port}`  
      })  
    }))  
      
    app.use(user.routes(), user.allowedMethods())  
    
    • routes/user.js
      const router = require('koa-router')()  
      const { login } = require('../controller/user')  
      const { SuccessModel, ErrorModel } = require('../model/resModel')  
      // 路由前缀  
      router.prefix('/api/user')  
        
      router.post('/login', async function (ctx, next) {  
          const { username, password } = ctx.request.body  
          const data = await login(username, password)  
          if (data.username) {  
              // 设置 session  
              ctx.session.username = data.username  
              ctx.session.realname = data.realname  
              // 返回前端数据  
              ctx.body = new SuccessModel()  
              return  
          }  
          ctx.body = new ErrorModel('登录失败')  
      })  
        
      module.exports = router  
      
  • app.js logger

    const logger = require('koa-logger')  
    const path = require('path')  
    const fs = require('fs')  
    const morgan = require('koa-morgan')  
      
    // logger  
    app.use(async (ctx, next) => {  
      const start = new Date()  
      await next()  
      const ms = new Date() - start  
      console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)  
    })  
      
    const ENV = process.env.NODE_ENV  
    if (ENV !== 'production') {  
      // 开发环境 / 测试环境  
      app.use(morgan('dev'));  
    } else {  
      // 线上环境  
      const logFileName = path.join(__dirname, 'logs', 'access.log')  
      const writeStream = fs.createWriteStream(logFileName, {  
        flags: 'a'  
      })  
      app.use(morgan('combined', {  
        stream: writeStream  
      }));  
    }  
    

7.2 分析koa2中间件原理

八 上线配置

8.1 线上环境的挑战

  • 服务器稳定性
  • 充分利用服务器硬件资源,以便提高性能
  • 线上日志记录

8.2 PM2可解决以上问题

  • 进程守护,系统崩溃自动重启
  • 启动多进程,充分利用CPU和内存
  • 自带日志记录功能

8.3 PM2

  • 下载安装
    •   npm i pm2 -g  
        pm2 --version  
      
  • 基本使用
    • package.json
      "scripts": {  
          "prd": "cross-env NODE_ENV=production pm2 start bin/www",  
         },  
      
  • 常用命令
    • pm2 start app.js
    • pm2 list
    • pm2 restart <AppName>/<id>
    • pm2 stop <AppName>/<id>
    • pm2 delete <AppName>/<id>
    • pm2 info <AppName>/<id>
    • pm2 log <AppName>/<id>
    • pm2 moint <AppName>/<id>
  • 进程守护
    • 模拟错误

      if(req.url === ‘/err’){  
        throw new Error()  
      }  
      
    • pm2 list
      查看到 restart 变多,知道重启了

      pm2 log app
      通过pm2 log 查看日志分析原因

  • 常用配置
    • pm2.conf.json
      {  
          "apps": {  
              "name": "pm2-test-server",  
              "script": "app.js",  
              "watch": true,  
              "ignore_watch": [  
                  "node_modules",  
                  "logs"  
              ],  
              "instances": 4,   // 多进程  
              "error_file": "logs/err.log",  
              "out_file": "logs/out.log",  
              "log_date_format": "YYYY-MM-DD HH:mm:ss"  
          }  
      }