git log 可以查看提交记录,按q退出

准备: cmd 进入到安装redis的文件夹,启动redis服务器。

运行redis-server

vcredist文件夹 _commonredist文件夹_redis


在项目文件夹安装redis

npm i redis --save

新建config文件夹,新建db.js写一些redis的配置,这个配置需要根据不同的当前环境而设置,所以需要一个环境变量,下面只是在开发环境下的存储配置。
js doc注释方式

/**
*@description 存储配置
*@author bokolin
*/

let REDIS_CONF = {
	port: 6379,
	host: '127.0.0.1'
}
module.exports ={
	REDIS_CONF
}

新建utils/env.js
这个文件主要是为了输出一个当前环境变量,它建立在cross-env的基础之上。
问:如何获取当前环境的变量。

const ENV = process.env.NODE_ENV
module.exports = {
    isDev: ENV === 'dev',
    notDev: ENV !== 'dev',
    isProd: ENV === 'production',
    notProd: ENV !== 'production'
}

新建cache文件夹,cache是缓存的意思,这个文件夹用来存放对redis的操作
在这里面新建一个_redis.js文件,问:下划线有什么意义么?通常用来表示内部文件,是自己用的
这个文件用来输出get 和set 方法,首先是创建redis客户端,然后才能提供get和set方法。
数据库是本身就有的,前端要做的就是连接数据库(配置+操作)
ps:使用jsdoc的注释形式

/**
*@description 连接redis的方法,get set
*@author bokolin
*/
const redis = require('redis')
const { REDIS_CONF } = require('../conf/db')
//创建客户端
const redisClient = redis.createClient(REDIS_CONF.port,REDIS_CONF.host)
/**
 * @param {string} key 键
 * @param {string} val 值
 * @param {number} timeout 过期时间 单位s 
 */
function set(key, val, timeout = 60*60){
    if(typeof val == 'object'){
    	// 
        val = JSON.stringify(val)
    }
    redisClient.set(key, val)
    redisClient.expire(key,timeout)
}
/**
 * 
 * @param {string} key 
 */
function get(key){
    return new Promise((resolve, reject)=>{
        redisClient.get(key,(err,val)=>{
            if(err) {
                reject(err)
                return
            }
            if(val ==  null) {
                resolve(null)
                return
            }
            try {
                resolve(
                    JSON.parse(val)
                )
            } catch (error) {
                resolve(val)
            }
        })
    })
}

问:在一个function声明函数之前,我们/**回车可以起到什么效果
问:怎么把一个对象序列化?
问:redis的get函数算不算一个异步操作呢?
问:尝试把val变成一个对象,如果变不成就直接返回

目前主流的实现登录的机制

1. 用cookie加上session的方式做登录
2. 使用jwt的方式

问: session又被称为什么?
问: session会存储每一个用户的什么?登录信息比如说用户登录之后的用户名,还有昵称、头像、如果是在cms系统中还有权限、是不是管理员等登录信息。 用户A从浏览器A登录,用户B从浏览器B登录,他们的登录信息都会存储到哪里? session session是一个集合,它像对象一样,有很多的什么? key 思考这个集合怎么跟客户端对应呢?怎么知道用户A访问和

这里是引用

用户B访问呢?依赖于什么机制?
比如用户A登录成功之后,浏览器A的cookie中有一个userid。每个用户在访问server的时候会把什么带到后端?这是浏览器本身符合http的一个做法。把cookie带过来之后我们判断这个cookie中的useid到底是谁,然后就可以根据userid从session中把这个用户信息取出来。这个就是cookie+session登录机制。
问:session为什么要存储到redis中?而不直接存储在nodejs的内存中。

1.操作系统会限制一个进程的最大可用内存
2.操作系统中进程与进程之间是有数据隔离的
3.session访问频繁,对性能要求极高
4.session存储在redis中不考虑断电丢失数据的问题(内存硬伤)
5.session数据量不会太大

redis和webServer拆分成两个单独的服务,双方独立可扩展

不使用mysql存储session的原因

为什么网站数据不使用redis存储?
首先redis是一个内存数据库,内存成本很高,redis断电丢失,但是网站数据断电不能丢失 网站的操作频率不高。

真正的使用-koa2怎么去配置session
安装依赖

npm i koa-redis koa-generic-session --save

koa-redis是koa连接redis的一个工具,koa-generic-session是koa生成session的工具。
session存储在redis中,因为redis是以键值对的形式存储数据的。

打开app.js,做一些配置

const session = require('koa-generic-session')
const redisStore = require('koa-redis')
const {REDIS_CONF} = require('./conf/db')

咋一看其实不知道引入REDIS_CONF的作用在哪,REDIS_CONF输出了redis服务器的host和port

接下来的代码需要在路由注册之上写,在注册路由之前需要把session给注册上。
server端不能信任客户端,所以userid不能用明文传递,需要加密一下。

//session配置,这个app.keys应该是用来加密的秘钥,加密的其实是sessonID,这个sessionId使我们自己命名的,
app.keys = ['UIs123@#zs7da']
app.use(session({
	key:'weibo.sid',//cookie的name,默认是koa.sid
	prefix:'weibo:sess',//redis key的前缀,默认是koa:sess
	cookie:{
		path:'/',
		httpOnly:true,//表示key(weibo.sid)这个值只能在server端修改,不能在客户端修改
		maxAge:24*60*60*1000, //cookie的过期时间,登录的过期时间
	},
	ttl:24*60*60*1000,//redis的过期时间,默认和cookie过期时间保持一致
	store:redisStore({
		all:`${REDIS_CONF.host}:${REDIS_CONF.port}`
	})
}))

用户每次访问,不管有没有登录server都会创建一个cookie给客户端保存,cookie的名字就是weibo.sid 客户端再访问的时候就带着这个cookie过来,会以weibo:sess为前缀创建一个redis的key,里面会存储一些比较基本的配置,因为这个时候用户还没有登录,所以我们也不知道谁是用户名。然后cookie里面存的值其实就是这个redis的key?

做测试:

npm run dev

ps:redis-server需要一直开着。

情景:访问页面,看cookie的值,cookie的值没看到(前缀是weibo.sid)?

思考:是不是还没有开始用redis,所以他还没有开始连。

vcredist文件夹 _commonredist文件夹_redis_02


到routes文件夹下的index.js

使用ctx.session就可以进入当前访问的一个会话

router.get('/json',aysnc (ctx,next)=>{
	const session = ctx.session;//一个对象,里面包含很多信息
	if(session.viewNum == null){
		session.viewNum =0
	}
	session.viewNum++
	ctx.body = {
		viewNum:session.viewNum //每个用户会话的数量是不一致的
	}
})

运行之后,在浏览器的控制台可以看到cookie的weibo.sid
我们再来到redis客户端窗口,输入keys * 就能够redis里面有前缀是 weibo:sess的两个key
session里面的有个key值是和cookie里面的值是对应上的。
get (key)可以获取到session的
通过ttl key可以看每个key的过期时间。

单元测试:

jest单元测试
对单个的功能或者接口给定输入,看输出是否符合要求。
手动编写测试用例代码,然后统一执行
能一次性执行所有单测,段时间内验证所有功能是否正常
需要为新作的功能编写单元测试,一次执行,证明新写的功能不能

jest
.test.js结尾的文件
常用的断言,判断输出是否符合要求
http接口测试

git diff可以查看当前文件与线上文件之间的变动

依赖安装:

npm i jest --save-dev

配置启动package.json

"test":"cross-env NODE_ENV=test jest --runInBand --forceExit --colors"

测试运行(此时没有测试用例,可能会报错,写demo)

npm run test

新建test文件夹(与src同级),新建demo.test.js

/**
* @description jest测试demo
* @author bokolin
*/
function sum(a,b){
	return a+b;
}
test('10+20应该等于30',()=>{
	const res = sum(10,20)
	expect(res).toBe(30)
	expect(res).not.toBe(40)
})

vcredist文件夹 _commonredist文件夹_vcredist文件夹_03


测试http接口

安装依赖

npm i supertest --save-dev

在test文件夹下新建server.js

const request = require('supertest')
const server = require('../src/app').callback()
module.exports = request(server)

同级文件夹下新建json.test.js

/**
 * @description  jest test
 * @author bokolin
 */
const server = require('./server')

test('json 接口返回数据格式正确',async ()=>{
    // 测试get类型接口
    const res = await server.get('/string')
    console.log(res.body)
    expect(res.body).toEqual({
        title:'koa2 string'
    })
    // 测试post类型接口 演示
    // const res = await server.post('/login').send({
    //     userName: 'zhangsan',
    //     password: '123'
    // })
})

运行 npm run test

vcredist文件夹 _commonredist文件夹_客户端_04

koa2开发环境配置

配置eslint,以及pre-commit
inspect 调试
404页和错误页

在src目录下新建db文件夹,新建seq.js
安装依赖

npm i mysql2 sequelize --save

看看package.json中的dependencies有这两个依赖。
创建sequelize的实例

/**
 * @description sequelize实例
 * @author bokolin
 */
const Sequelize = require('sequelize');
const conf = {
  host: 'localhost',
  dialect: 'mysql'
}
const seq = new Sequelize('flyingfish_db', 'root', '950210zsrtxdy', conf)
module.exports = seq

可以做的操作就是把里面的参数给提取出来,用来适应不同的环境
在config/db.js文件,添加MYSQL的环境配置

/**
 * @description 输出数据库连接配置
 * @author bokolin
 */
const { isProd } = require('../utils/env')

let REDIS_CONF = {
	port: 6379,
	host: '127.0.0.1'
}
// const seq = new Sequelize(database, user, password, conf)
let MYSQL_CONF = {
    host: 'localhost',
    port: 3306,
    user: 'root',
    password: '950210zsrtxdy',
    database:'flyingfish_db'
}
if(isProd){
    // 线上的配置
    REDIS_CONF = {
        port: 6379,
        host: '127.0.0.1'
    }
    MYSQL_CONF = {
        host: 'localhost',
        port: 3306,
        user: 'root',
        password: '950210zsrtxdy',
        database:'flyingfish_db'
    }
}
module.exports ={
    REDIS_CONF,
    MYSQL_CONF
}

然后回到seq.js

/**
 * @description sequelize实例
 * @author bokolin
 */
const Sequelize = require('sequelize');
const {database, user, password, host} =require('../config/db').MYSQL_CONF
const conf = {
  host,
  dialect: 'mysql'
}
const seq = new Sequelize(database, user, password, conf)
// const seq = new Sequelize('flyingfish_db', 'root', '950210zsrtxdy', conf)
module.exports = seq

如果是线上环境就需要连接池,所以我们添加

if(isProd){
	// 线上环境,使用连接池
conf.pool = {
    max: 5,
    min: 0,
    idle: 1000
}
}

在做单元测试的时候希望不要打印sql日志,在env.js文件新增两个变量isTest notTest

const ENV = process.env.NODE_ENV

module.exports = {
    isDev: ENV === 'dev',
    notDev: ENV !== 'dev',
    isProd: ENV === 'production',
    notProd: ENV !== 'production',
    isTest: ENV === 'test',
    notTest: ENV !== 'test',
}

回到seq.js,使不打印sql语句日志

if(isTest){
    conf.logging = ()=>{}
}

接下来进行同步
在db文件夹下新建sync.js,在执行同步时有{force: true}和没有的区别

const seq = require('./seq.js')
// require('./model.js')
// 3 测试连接
seq.authenticate()
    .then(() => {
        console.log('Connection has been established successfully.');
    })
    .catch(err => {
        console.error('Unable to connect to the database:', err);
    });

// 4 执行同步
seq.sync().then(()=>{
    console.log('sync ok')
    process.exit() //不退出sequelize会一直占用进程
})

配置eslint
新建.eslintrc.json .eslintignore

{   
    "parser": "babel-eslint",
    "env": {
        "es6": true,
        "node": true,
        "commonjs":true
    },
    "extends": "eslint:recommended",
    "rules": {
        "indent": [
            "error",
            "tab" //4
        ],
        "linebreak-style": [
            "error",
            "windows"
        ],
        "quotes": [
            "error",
            "single",
            {
                "allowTemplateLiterals":true //允许模板字符转
            }
        ],
        "semi": [
            "error",
            "never" //always 结尾要不要分号
        ]
        // "no-console": 0,//手动添加
        // "eqeqeq": 2//手动添加
    }
}

安装依赖
开发环境下

npm i eslint babel-eslint --save-dev

添加检查启动项

"lint": "eslint --ext .js ./src",

vscode如何配置eslint的自动检查的?

{
    "eslint.format.enable": true,
    "eslint.run": "onSave",
    // "editor.formatOnSave": true,
    "editor.codeActionsOnSave": {
        "source.fixAll.eslint": true
    },
}

参考

在eslint上配置 "no-unused-vars": "off",可以解决'next' is defined but never used的问题。再次运行就不会出现错误了

vcredist文件夹 _commonredist文件夹_客户端_05


有一种方式就是eslint过不了你就无法提交。

安装pre-commit依赖,意思是在git commit之前做什么。

npm i pre-commit --save-dev

在package.json下面

"pre-commit":[
    "lint"
  ]

开发环境的debug

inspect调试,在生产环境下进行调试,加上 --inspect=9229

"dev": "cross-env NODE_ENV=dev ./node_modules/.bin/nodemon --inspect=9229 bin/www",

vcredist文件夹 _commonredist文件夹_客户端_06


怎么打开,在chrome浏览器上打开

vcredist文件夹 _commonredist文件夹_vcredist文件夹_07


点击inspect,查看console

访问localhost:3000

vcredist文件夹 _commonredist文件夹_客户端_08


光用console不行,需要打断点调试

加断点就是加debugger

vcredist文件夹 _commonredist文件夹_客户端_09


debugger怎么调试?在source栏,跟chrome的调试貌似很相似

完善开发环境 -404和错误页 -路由
routes文件夹下新建view文件夹和api文件夹,在view文件夹下新建error.js

/**
 * @description error 404 页面路由
 * @author bokolin
 */
const router = require('koa-router')()
router.get('/error',async (ctx,next)=>{
	ctx.body = {
		title:'后端错误页面'
	}
})
router.get('*',async (ctx,next)=>{
	ctx.body = {
		title:'后端404页面'
	}
})
module.exports = router

在app.js里面注册这个路由

const errorViewRouter = require('./routes/views/error')
//因为有404路由,所以放在最下方
app.use(errorViewRouter.routes(), errorViewRouter.allowedMethods())

如果是开发环境,发生错误的时候就会输出错误信息,如果是生产环境发生错误时会跳转到错误页。

const {isProd} =require('./utils/env')

// error handler
let onErrorConf = {}
if(isProd){
	onErrorConf = {
		redirect:'/error'
	}
}
onerror(app,onErrorConf)

如何人为的去抛出一个错误

throw Error()

发生错误的时候跳转到了错误页
但是访问*页面的时候没有跳转到404页面,因为之前加了个前缀。