【node】nodejs原生搭建后端服务

一、开发前的准备

  1. cross-env 设置环境变量
  2. nodemon 热更新(修改代码之后立即重启项目)

使用:两个插件均使用npm安装即可,安装好之后在package.json的脚本中插入以下两个脚本

"scripts": {
    "dev": "cross-env NODE_ENV=dev nodemon ./bin/www.js",
    "prd": "cross-env NODE_ENV=production nodemon ./bin/www.js"
  },

用dev脚本举例,该脚本使用cross-env修改NODE_ENV变量为dev,并且使用nodemon热更新启动,入口文件为./bin/www.js。
注意:cross-env修改的是node全局变量Process.env,NODE_ENV则是自己定义的,如果Process.env.NODE_ENV存在,则修改,不存在则创建。之后,整个node项目都可以使用process.env.NODE_ENV来判断当前的环境。

贴一下项目文件目录
在这里插入图片描述

二、最简单的服务

最简单的服务就是下面这几行代码。

const http = require('http')
const server = http.createServer((req, res) => {
    res.end('hello word')
})
server.listen(8000)

执行上面的代码,访问localhost:8000即可看到hello word。
我们的项目也将由这个最简单的服务展开。

三、开发路由

1. 服务模块、路由模块分离

给原始代码加入路由判断,类似于

const server = http.createServer((req, res) => {
    if(req.method === 'GET' && req.path === '/api/blog/list'){
        return {
        	data: '获取到博客列表',
        	code: 200
        }
     }
})

模块化提取一下,然后加入response格式设置,404处理。

./bin/www.js

const http = require('http')

const PORT = 8000
const serverHandle = require('../app')

const server = http.createServer(serverHandle)
server.listen(PORT)

./app.js

const queryString = require('querystring')
const handleUserRouter = require('./src/router/user')

const severHandle = (req, res) => {
    // 设置返回格式JSON
    res.setHeader('Content-type','application/json')
    
    const userData = handleUserRouter(req, res)
    if(userData){
        userData.then(u => {
            res.end(JSON.stringify(u))
        })
        return
    }
	
	/** 还可以插入其他路由,从上到下,命中一个就停止 **/

     // 未命中路由
    res.writeHead(404, {"Content-type": "text/plain"})
    res.write("404 Not Found ~\n")
    res.end()
}

module.exports = severHandle

./src/router/user.js

const handleUserRouter = (req, res) => {
    if(req.method === 'POST' && req.path === '/api/user/login'){
        return '登录成功'
    }
}

module.exports = handleUserRouter
2. 处理request中query和post数据

./app.js

const queryString = require('querystring')
const handleUserRouter = require('./src/router/user')

const getPostData = (req) => {
    return 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)
            )
        })
    })
}

const severHandle = (req, res) => {
    // 设置返回格式JSON
    res.setHeader('Content-type','application/json')
 
    const url = req.url
    req.path = url.split('?')[0]

    // 解析query
    req.query = queryString.parse(url.split('?')[1])

    getPostData(req).then(postData => {
        req.body = postData

        const userData = handleUserRouter(req, res)
        if(userData){
            userData.then(u => {
                res.end(JSON.stringify(u))
            })
            return
        }

         // 未命中路由
        res.writeHead(404, {"Content-type": "text/plain"})
        res.write("404 Not Found ~\n")
        res.end()
    })
}

module.exports = severHandle

这样可以通过req.body访问post的数据,通过req.query访问到query的数据

四、数据库mysql

1. mysql配置文件

npm安装mysql依赖,并添加一个mysql的配置文件,输出链接mysql需要的用户名、密码、数据库端口、名称等必要信息。

./src/config/db.js

const env = process.env.NODE_ENV
let MYSQL_CONFIG = {}

if(env === 'dev'){
    // mysql
    MYSQL_CONFIG = {
        host: 'localhost',
        user: 'root',
        password: '123456',
        port: '3306',
        database: 'myblog'
    }
}

module.exports = {
    MYSQL_CONFIG,
}
2. sql执行函数

还需要一个执行sql语句的函数,我们也单独抽出来。
./src/db/mysql.js

const mysql = require('mysql')
const { MYSQL_CONFIG } = require('../config/db')    // 来自上面的配置文件

// 创建一个mysql实例,因为这是一个单例,所以不会用到退出数据库
const con = mysql.createConnection(MYSQL_CONFIG)
// 连接数据库
con.connect()

// con.query是一个异步,且是回调的形式,所以我们将其封装成promise的形式,更方便使用
const exec = (sql) => {
    return new Promise((resolve, reject) => {
        con.query(sql, (err, result) => {
            if(err){
                reject(err)
                return
            }
            resolve(result)
        })
    })
}

module.exports = {
    exec,
    escape: mysql.escape
}
3. controller调用sql执行语句

./src/controller/user.js

const { exec, escape } = require('../db/mysql')

const login = (username, password) => {
	// 拼接sql语句
    const sql = `select username, realName from users where username=${username} and password=${password}`
    // 调用我们封装的sql语句执行函数,依旧返回一个promise。
    return exec(sql).then(rows => {
    	// sql查出来是“行”数组形式
        return (rows[0] || {})
    })
}

module.exports = {
    login
}

然后我们就可以在router里面去调用login函数判断用户名、密码是否正确。

./src/router/user.js

if(req.method === 'POST' && req.path === '/api/user/login'){
   	const {username, password} = req.body
    const result = login(username, password)
    
    return result.then(loginData => {
        if(loginData){
            return "登录成功"
        }else{
            return "登录失败"
        } 
    }).catch(err => {
        return "登录失败"
    })
}

五、cookie和session

1. cookie

用户登录之后,访问其他页面也需要能验证对方是已处于登录状态,有些页面还需要读取当前用户的信息。此时就需要用到cookie。
cookie存储在浏览器(客户端),大小4kb,有跨域限制。每次发送请求都会自动把cookie带给服务端。我们可以在用户登录之后,把用户信息写到cookie,之后此域名下所有页面都能访问到这个用户信息。
可以在控制台看到详细的cookie信息。
在这里插入图片描述
在登录成功之后,在cookie中写入用户信息。
./src/router/user.js

if(req.method === 'POST' && req.path === '/api/user/login'){
   	const {username, password} = req.body
    const result = login(username, password)
    
    return result.then(loginData => {
        if(loginData){
        	res.setHeader('Set-Cookie', `username=${username}; path=/; httpOnly; expires=${1000000000}`)
            return "登录成功"
        }else{
            return "登录失败"
        } 
    }).catch(err => {
        return "登录失败"
    })
}
  • path=/ 表示所有路由都可以访问到这个cookie。
  • httpOnly 正常情况,客户端可以通过document.cookie或其他方法去修改cookie,加上httpOnly之后,客户端无法修改。
  • expires 是cookie的过期时间

在处理路由之前,我们拿到cookie,进行解析,然后在其他页面就可以通过cookie拿到用户名,以此来判断用户是否登陆了。
./app.js

const severHandle = (req, res) => {
    res.setHeader('Content-type','application/json')
 
    const url = req.url
    req.path = url.split('?')[0]

    // 解析query
    req.query = queryString.parse(url.split('?')[1])
    
 	// 解析cookie
    req.cookie = {}
    const cookieStr = req.headers.cookie || ''
    cookieStr.split(';').forEach(item => {
        if(!item){
            return
        }
        const arr = item.split('=')
        const key = arr[0].trim()
        const val = arr[1].trim()
        req.cookie[key] = val
    })

    getPostData(req).then(postData => {
      /** 省略 **/
    })
}
2. session

显然,用户信息直接这样存在客户端,是很不安全的。于是,出现了session。
session不同于cookie,cookie是http中自带的,而session只是约定俗成的一个变量,存储在服务端。
我们可以把用户信息存在session中,然后cookie中存放对应的一个key,可以去session中查到对应的信息。
例如:

session = {
	'jskqus6hdk9k': {
		username: 'zhangsan',
		email: 'zhangsan@gmail.com'
	}
}

cookie = {
	sessionId: 'jskqus6hdk9k'
}

用户登录,服务端生成随机且唯一的key,用户信息作为value,存储到session,session存在服务端(放到内存、redis 或 mysql中)。同时把这个随机key设置到cookie中。
用户访问其他页面而发送请求时,会自动把cookie发送给服务端。服务端可以看是否有这个随机key,来判断用户是否登陆,切能拿到该用户的信息。

./app.js


// 全局session对象
const SESSION_DATA = {}

const severHandle = (req, res) => {
    /** 省略 **/

    // 解析session
    let neeSetCookie = false
    let userId = req.cookie.userId
    if(userId){
        console.log('SESSION_DATA',SESSION_DATA)
        if(!SESSION_DATA[userId]){
            SESSION_DATA[userId] = {}
        }
    }else{
        neeSetCookie = true
        userId = `${Date.now()}_ ${Math.random()}`  // 作为sessionId,保证不重复就行
        SESSION_DATA[userId] = {}
    }
    req.session = SESSION_DATA[userId]

    getPostData(req).then(postData => {
        req.body = postData

        // 处理路由
        const userData = handleUserRouter(req, res)
        if(userData){
            userData.then(u => {
                if(needSetCookie){
                    res.setHeader('Set-Cookie', `userId=${userId}; path=/; httpOnly; expires=${getCookieExpires()}`)
                }
                res.end(JSON.stringify(u))
            })
            return
        }
    })
}

// 获取 cookie 的过期时间
const getCookieExpires = () => {
    const d = new Date()
    d.setTime(d.getTime() + (24 * 60 * 60 * 1000))
    console.log(d.toGMTString())
    return d.toGMTString()
}

./src/router/user.js

if(method === 'POST' && req.path === '/api/user/login'){
        const {username, password} = req.body
        const result = login(username, password)
        return result.then(loginData => {
            if(loginData.username){
                // 登录成功,设置session
                req.session.username = loginData.username
                req.session.realName = loginData.realName

			/** 省略 **/
        })
    }

六、session存入redis

上述方案中,session是存储在内存中的,每次项目重启,都会导致SESSION_DATA这个变量丢失。显然session放到内存中是不行的。而且session的访问频率很高,而且大小通常较小,放到redis是一个很不错的选择。

1. redis配置文件

首先npm安装redis依赖。并在配置文件中加入redis的相关配置。

./src/config/db.js

const env = process.env.NODE_ENV

let MYSQL_CONFIG = {}
let REDIS_CONFIG = {}

if(env === 'dev'){
    // mysql
    MYSQL_CONFIG = {
    }

    // redis
    REDIS_CONFIG = {
        port: 6379,
        host: '127.0.0.1'
    }
}

/** 可以加入其他环境的配置 **/

module.exports = {
    MYSQL_CONFIG,
    REDIS_CONFIG
}
2. 封装redis的执行函数

./src/db/redis.js

const redis = require('redis')
const { REDIS_CONFIG } = require('../config/db')

// 创建客户端
const redisClient = redis.createClient(REDIS_CONFIG.port, REDIS_CONFIG.host)
redisClient.on('error', err => console.log(err))

const set = (key,val) => {
    if(typeof val === 'object'){
        val = JSON.stringify(val)
    }
    redisClient.set(key, val, redis.print)
}

const get = (key) => {
    return new Promise((resolve, reject) => {
        redisClient.get(key, (err, val) => {
            if(err){
                console.log(err)
                return
            }
            if(val === null){
                resolve(val)
                return
            }
            try {
                resolve(JSON.parse(val)) // 是JSON格式就转化一下
            } catch(ex) {
                resolve(val)  // 抛错证明不是JSON
            }
        })
    })
}

module.exports = {
    set,
    get
}
3. 把session存入redis

我们需要在解析路由之前需要从redis里拿出session,并解析

/** 省略 **/
// 解析 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 {
          // 设置 session
          req.session = sessionData
      }
      // 处理 post data
      return getPostData(req)
  })
  .then(postData => {
      req.body = postData

      // 处理路由
/** 省略 **/

其次在用户登录成功之后需要设置最新的session
./src/router/user.js

if(method === 'GET' && req.path === '/api/user/login'){
        const {username, password} = req.query
        const result = login(username, password)
        return result.then(loginData => {
            if(loginData.username){
                // 设置session
                req.session.username = loginData.username
                req.session.realName = loginData.realName
                // 同步到 redis
                set(req.sessionId, req.session)

                return new SuccessModel(loginData)
            }else{
                return new ErrorModel('登录失败')
            } 
        }).catch(err => {
            return new ErrorModel('登录失败')
        })
    }

七、nginx配置

1. 给前端html启动一个服务

如果前端页面没有用npm管理,只是简单的一个html文件,为了做到前后端分离,我们可以给前端静态页面单独起一个服务。需要用到依赖http-server,全局安装依赖。

npm install http-server -g

然后在html文件的目录下执行

http-server -p 8001

然后就可以访问localhost:8001来访问我们的静态页面。如:localhost:8001/index.html。
此时,前后端服务不在同一个端口,会有跨域问题,可以采用cors等方法去解决跨域问题,也可以用到nginx来分别代理两个端口到同一个端口上,就不会有跨域问题。

2.nginx反向代理

在这里插入图片描述
将根域名/…下的的访问都指向前端静态资源,将/api/…下的访问都指向后端端口。

3. nginx使用

下载nginx,windows去官网,mac直接运行

brew install nginx

打开nginx配置文件,window采用记事本,mac使用vi或者vim。

windows: C:\nginx\conf\nginx.conf
Mac: /usr/local/etc/nginx/nginx.conf
sudo vi /usr/local/etc/nginx/nginx.conf

修改server中listen为8080,注释掉旧的location,加入两个新的location

              #全局块

events {         #events块
  ...
}

http      #http块
{
    ...  #http全局块
    server        #server块
    { 
        listen		8080;
        server_name localhost;

		#charset koi8-r;
		
		#access_log  log/host.accsess.log	main;
		
		# 注释掉旧的
        #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;
        }

        location [PATTERN] 
        {
            ...
        }
    }
    server
    {
	     ...
    }
     #http全局块
}

配置完成之后

nginx -t  // 测试配置文件格式是否正确
nginx  // 启动nginx

然后就可以通过localhost:8080访问前端页面,并成功调取后端接口

八、写入日志

1. 先看另一篇

【node】nodejs读取文件与文件传输的简单实现

2. 写日志函数

有了上面读写文件的基础理解,可以很轻松地实现一个写入日志的函数。
增加一个文件
./src/utils/log.js

const fs = require('fs')
const path = require('path')

// 写日志
const writeLog = (writeStream, log) => {
    writeStream.write(log + '\n')
}
// 生成write stream
const createWriteStream = (fileName) => {
	// 日志的输出目录
    const fullFileName = path.join(__dirname, '../../', 'logs', fileName)
    const writeStream = fs.createWriteStream(fullFileName, {
        flags: 'a'     // 追加写入, 覆盖用 ‘w’
    })
    return writeStream
}

// 写访问日志
const accessWriteStream = createWriteStream('access.log')
const access = log => {
    writeLog(accessWriteStream, log)
}

// 写xx日志
// ......

module.exports = {
    access
}
3. 服务入口处增加访问日志记录

./app.js

const severHandle = (req, res) => {
    console.log('进入serverHandle',SESSION_DATA)
    // 记录 access log
    access(`${req.method} -- ${req.url} -- ${req.headers['user-agent']} -- ${Date.now()}`)
    
    // 设置返回格式JSON
    res.setHeader('Content-type','application/json')
    /** 省略 **/
}
4. 分割日志文件

通常我们的日志文件需要按日期进行分割,需要用到一个简单的shell脚本

#!/bin/sh
cd /Users/Documents/blog-1/logs        # 这里指向项目的logs文件夹
# 拷贝access.log文件到新的文件,新的文件命名是2021-10-27-19.access这种格式,具体可以搜shell脚本获取日期的方法
cp access.log $(date +%Y-%m-%d-%H).access.log    
echo "" > access.log            # 将空内容写入access.log,即清空access.log文件

还需要一个定时器,每天定时执行这个脚本。实现方式就是linux的crontab命令。(服务端基本都是部署在linux系统上,所以不考虑window的情况)
在terminal中执行

crontab -e

在打开的编辑器中加入一个定时任务。(具体含义可以查看linux crontab命令)

* 0 * * * sh /Users/Documents/blog-1/src/utils/copy.sh     # 指向项目中的copy.sh

然后服务器会在每天零点拷贝日志。

5. 分析日志

我们可以写一些函数来读取并分析日志里的内容,举例子,分析访问的客户端浏览器情况,统计chrome浏览器的占比。 需要用到readline函数
增加一个文件
./src/utils/readlog.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)

// 创建readline对象
const rl = readline.createInterface({
    input: readStream
})

let chromeNum = 0
let sum = 0

// 计算chrome浏览器访问的占比
rl.on('line', (lineData) => {
    if(!lineData){
        return
    }
    sum ++ 
    const arr = lineData.split(' -- ')
    if(arr[2] && arr[2].indexOf('Chrome') > 0){
        chromeNum ++
    }
})
// 监听读取完成
rl.on('close', () => {
    console.log('chrome 占比:' + chromeNum / sum)
})

九、安全处理

1. sql注入

使用mysql依赖自带的escape函数

./src/db/mysql.js

// 直接输出mysq的escape函数
module.exports = {
    exec,
    escape: mysql.escape
}

以登陆函数举例
./src/controller/user.js

const { exec, escape } = require('../db/mysql')

const login = (username, password) => {
	// 用escape函数把传入的参数包一下,这样子里面任何sql的特殊符号都会被转义
    username = escape(username)
    password = escape(password)
    const sql = `select username, realName from users where username=${username} and password=${password}`
    return exec(sql).then(rows => {
        return (rows[0] || {})
    })
}
2. xss攻击

xss主要是插入恶意的

const { exec, escape } = require('../db/mysql')
const xss = require('xss')

const login = (username, password) => {
    username = escape(username)
    password = escape(password)
    // 用xss函数包裹一下传入的变量就好了,但是要注意,包裹之后的变量外面需要加一层引号,可以对比一下password和username两个地方
    const sql = `select username, realName from users where username='${xss(username)} 'and password=${password}`
    return exec(sql).then(rows => {
        return (rows[0] || {})
    })
}
3. 数据库敏感信息加密

用户名、密码不能明文存在数据库,因为数据库一旦泄漏,就会很危险。因此需要加密处理,我们采用简单的加密举例,使用node自带的crypto函数,进行简单的md5加密。
新建一个文件
./src/utils/cryp.js

const crypto = require('crypto') // nodejs自身提供的加密的库
// 自己随便定义一个密钥
const SECRET_KEY = 'WjsakIG_876'

const md5 = (content) => {
    let md5 = crypto.createHash('md5')
    return md5.update(content).digest('hex') // 把输出变成16进制
}

const genPassword = (password) => {
	// 可以自己随便定义一个把密钥包含进去的密码规则,然后再由md5进行加密
    const str = `password=${password}&key=${SECRET_KEY}`
    return md5(str)
}

console.log(genPassword('123'))

在用户注册的时候我们就存入加密后的密码,登录之后也是加密之后跟数据库进行比较,这样服务端全程都不会知道用户真实的密码。

十、完结

别看了,就是完事儿了。

  • 8
    点赞
  • 115
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值