赞
踩
使用:两个插件均使用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。
我们的项目也将由这个最简单的服务展开。
给原始代码加入路由判断,类似于
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
./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的数据
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, }
还需要一个执行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 }
./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。
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 "登录失败"
})
}
在处理路由之前,我们拿到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 => { /** 省略 **/ }) }
显然,用户信息直接这样存在客户端,是很不安全的。于是,出现了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是存储在内存中的,每次项目重启,都会导致SESSION_DATA这个变量丢失。显然session放到内存中是不行的。而且session的访问频率很高,而且大小通常较小,放到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 }
./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 }
我们需要在解析路由之前需要从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('登录失败') }) }
如果前端页面没有用npm管理,只是简单的一个html文件,为了做到前后端分离,我们可以给前端静态页面单独起一个服务。需要用到依赖http-server,全局安装依赖。
npm install http-server -g
然后在html文件的目录下执行
http-server -p 8001
然后就可以访问localhost:8001来访问我们的静态页面。如:localhost:8001/index.html。
此时,前后端服务不在同一个端口,会有跨域问题,可以采用cors等方法去解决跨域问题,也可以用到nginx来分别代理两个端口到同一个端口上,就不会有跨域问题。
将根域名/…下的的访问都指向前端静态资源,将/api/…下的访问都指向后端端口。
下载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访问前端页面,并成功调取后端接口
有了上面读写文件的基础理解,可以很轻松地实现一个写入日志的函数。
增加一个文件
./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 }
./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')
/** 省略 **/
}
通常我们的日志文件需要按日期进行分割,需要用到一个简单的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
然后服务器会在每天零点拷贝日志。
我们可以写一些函数来读取并分析日志里的内容,举例子,分析访问的客户端浏览器情况,统计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) })
使用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] || {})
})
}
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] || {})
})
}
用户名、密码不能明文存在数据库,因为数据库一旦泄漏,就会很危险。因此需要加密处理,我们采用简单的加密举例,使用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'))
在用户注册的时候我们就存入加密后的密码,登录之后也是加密之后跟数据库进行比较,这样服务端全程都不会知道用户真实的密码。
别看了,就是完事儿了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。