Koa2学习笔记

  目录
  1. 1. koa2开始
    1. 1.1. koa2 快速开始
      1. 1.1.1. 环境准备
      2. 1.1.2. 快速开始
        1. 1.1.2.1. 安装koa2
        2. 1.1.2.2. hello world 代码
        3. 1.1.2.3. 启动demo
    2. 1.2. async/await使用
      1. 1.2.1. 快速上手理解
        1. 1.2.1.1. 在chrome的console中执行结果如下
        2. 1.2.1.2. 从上述例子可以看出 async/await 的特点:
    3. 1.3. koa2简析结构
      1. 1.3.1. 源码文件
      2. 1.3.2. koa2特性
    4. 1.4. koa中间件开发和使用
      1. 1.4.1. generator中间件开发
        1. 1.4.1.1. generator中间件开发
        2. 1.4.1.2. generator中间件在koa@1中的使用
        3. 1.4.1.3. generator中间件在koa@2中的使用
      2. 1.4.2. async中间件开发
        1. 1.4.2.1. async 中间件开发
        2. 1.4.2.2. async 中间件在koa@2中使用
  2. 2. 路由
    1. 2.1. koa2 原生路由实现
      1. 2.1.1. 简单例子
      2. 2.1.2. 定制化的路由
        1. 2.1.2.1. 源码文件目录
        2. 2.1.2.2. demo源码
        3. 2.1.2.3. 运行demo
          1. 2.1.2.3.1. 执行运行脚本
          2. 2.1.2.3.2. 运行效果如下
      3. 2.1.3. 安装koa-router中间件
      4. 2.1.4. 快速使用koa-router
  3. 3. 请求数据获取
    1. 3.1. GET请求数据获取
      1. 3.1.1. 使用方法
      2. 3.1.2. 举个例子
        1. 3.1.2.1. 例子代码
        2. 3.1.2.2. 执行程序
    2. 3.2. POST请求参数获取
      1. 3.2.1. 原理
        1. 3.2.1.1. 解析出POST请求上下文中的表单数据
      2. 3.2.2. 举个例子
        1. 3.2.2.1. 例子代码
        2. 3.2.2.2. 启动例子
        3. 3.2.2.3. 访问页面
        4. 3.2.2.4. 提交表单发起POST请求结果显示
    3. 3.3. koa-bodyparser中间件
      1. 3.3.1. 原理
        1. 3.3.1.1. 安装koa2版本的koa-bodyparser@3中间件
      2. 3.3.2. 举个例子
        1. 3.3.2.1. 例子代码
        2. 3.3.2.2. 启动例子
        3. 3.3.2.3. 访问页面
        4. 3.3.2.4. 提交表单发起POST请求结果显示
  4. 4. 静态资源加载
    1. 4.1. 原生koa2实现静态资源服务器
      1. 4.1.1. 前言
      2. 4.1.2. 原生koa2 静态资源服务器例子
        1. 4.1.2.1. 代码目录
        2. 4.1.2.2. 代码解析
          1. 4.1.2.2.1. index.js
          2. 4.1.2.2.2. util/content.js
          3. 4.1.2.2.3. util/dir.js
          4. 4.1.2.2.4. util/file.js
          5. 4.1.2.2.5. util/walk.js
          6. 4.1.2.2.6. util/mime.js
        3. 4.1.2.3. 运行效果
          1. 4.1.2.3.1. 启动服务
          2. 4.1.2.3.2. 效果
            1. 4.1.2.3.2.1. 访问http://localhost:3000
            2. 4.1.2.3.2.2. 访问http://localhost:3000/index.html
            3. 4.1.2.3.2.3. 访问http://localhost:3000/js/index.js
    2. 4.2. koa-static中间件使用
      1. 4.2.1. 使用例子
        1. 4.2.1.0.1. 效果
          1. 4.2.1.0.1.1. 访问http://localhost:3000
          2. 4.2.1.0.1.2. 访问http://localhost:3000/index.html
          3. 4.2.1.0.1.3. 访问http://localhost:3000/js/index.js
  • 5. cookie/session
    1. 5.1. koa2使用cookie
      1. 5.1.1. 使用方法
      2. 5.1.2. 例子代码
      3. 5.1.3. 运行例子
        1. 5.1.3.1. 执行脚本
        2. 5.1.3.2. 运行结果
          1. 5.1.3.2.1. 访问http://localhost:3000/index
    2. 5.2. koa2实现session
      1. 5.2.1. 前言
      2. 5.2.2. 数据库存储方案
      3. 5.2.3. 快速使用
        1. 5.2.3.1. 例子代码
        2. 5.2.3.2. 运行例子
          1. 5.2.3.2.1. 执行命令
          2. 5.2.3.2.2. 访问连接设置session
          3. 5.2.3.2.3. 查看数据库session是否存储
          4. 5.2.3.2.4. 查看cookie中是否种下了sessionId
  • 6. 模板引擎
    1. 6.1. koa2加载模板引擎
      1. 6.1.1. 快速开始
        1. 6.1.1.1. 安装模块
        2. 6.1.1.2. 使用模板引擎
          1. 6.1.1.2.1. 文件目录
          2. 6.1.1.2.2. ./index.js文件
          3. 6.1.1.2.3. ./view/index.ejs 模板
    2. 6.2. ejs模板引擎
      1. 6.2.1. 具体查看ejs官方文档
  • 7. 文件上传
    1. 7.1. busboy模块
      1. 7.1.1. 快速开始
        1. 7.1.1.1. 安装
        2. 7.1.1.2. 模块简介
        3. 7.1.1.3. 开始使用
      2. 7.1.2. 更多模块信息
    2. 7.2. 上传文件简单实现
      1. 7.2.1. 依赖模块
        1. 7.2.1.1. 安装依赖
      2. 7.2.2. 例子源码
        1. 7.2.2.1. 封装上传文件到写入服务的方法
        2. 7.2.2.2. 入口文件
        3. 7.2.2.3. 运行结果
    3. 7.3. 异步上传图片实现
      1. 7.3.1. 快速上手
      2. 7.3.2. 源码理解
        1. 7.3.2.1. demo源码目录
        2. 7.3.2.2. 后端代码
        3. 7.3.2.3. 前端代码
        4. 7.3.2.4. 运行效果
  • 8. 数据库mysql
    1. 8.1. mysql模块
      1. 8.1.1. 快速开始
        1. 8.1.1.1. 安装MySQL数据库
        2. 8.1.1.2. 安装 node.js的mysql模块
        3. 8.1.1.3. 模块介绍
        4. 8.1.1.4. 开始使用
          1. 8.1.1.4.1. 创建数据库会话
          2. 8.1.1.4.2. 创建数据连接池
      2. 8.1.2. 更多模块信息
    2. 8.2. async/await封装使用mysql
      1. 8.2.1. 前言
        1. 8.2.1.1. Promise封装mysql模块
          1. 8.2.1.1.1. Promise封装 ./async-db
          2. 8.2.1.1.2. async/await使用
    3. 8.3. 建表初始化
      1. 8.3.1. 前言
      2. 8.3.2. 快速开始
        1. 8.3.2.1. 源码目录
        2. 8.3.2.2. 具体流程
      3. 8.3.3. 源码详解
        1. 8.3.3.1. 数据库操作文件 ./util/db.js
        2. 8.3.3.2. 获取所有sql脚本内容 ./util/get-sql-content-map.js
        3. 8.3.3.3. 获取sql目录详情 ./util/get-sql-map.js
        4. 8.3.3.4. 遍历目录操作 ./util/walk-file.js
        5. 8.3.3.5. 入口文件 ./index.js
        6. 8.3.3.6. sql脚本文件 ./sql/data.sql
        7. 8.3.3.7. sql脚本文件 ./sql/user.sql
      4. 8.3.4. 效果
        1. 8.3.4.1. 执行脚本
        2. 8.3.4.2. 执行结果
        3. 8.3.4.3. 查看数据库写入数据
  • 9. JSONP实现
    1. 9.1. 原生koa2实现jsonp
      1. 9.1.1. 前言
      2. 9.1.2. 实现JSONP
        1. 9.1.2.1. 具体原理
        2. 9.1.2.2. 解析原理
        3. 9.1.2.3. 效果截图
          1. 9.1.2.3.1. 同域访问JSON请求
          2. 9.1.2.3.2. 跨域访问JSON请求
        4. 9.1.2.4. 完整demo代码
        5. 9.1.2.5. 简单例子
  • 10. 测试
    1. 10.1. 单元测试
      1. 10.1.1. 前言
      2. 10.1.2. 准备工作
        1. 10.1.2.1. 安装测试相关框架
      3. 10.1.3. 测试例子
        1. 10.1.3.1. 例子目录
        2. 10.1.3.2. 所需测试demo
        3. 10.1.3.3. 开始写测试用例
        4. 10.1.3.4. 执行测试用例
        5. 10.1.3.5. 用例详解
          1. 10.1.3.5.1. 服务入口加载
          2. 10.1.3.5.2. 测试套件、用例
  • 11. debug
    1. 11.1. 开发debug
      1. 11.1.1. 快速开始
        1. 11.1.1.1. 环境
        2. 11.1.1.2. 启动脚本
          1. 11.1.1.2.1. 调试demo
          2. 11.1.1.2.2. 指令框显示
          3. 11.1.1.2.3. 访问chrome浏览器调试server
          4. 11.1.1.2.4. 打开chrome浏览器的node调试窗口
          5. 11.1.1.2.5. 可以自定义打断点调试了
  • 12. 项目框架搭建
    1. 12.1. 项目demo
      1. 12.1.1. 快速启动
        1. 12.1.1.1. demo地址
        2. 12.1.1.2. 环境准备
        3. 12.1.1.3. 初始化数据库
        4. 12.1.1.4. 启动脚本
        5. 12.1.1.5. 访问项目demo
    2. 12.2. 框架设计
      1. 12.2.1. 实现概要
      2. 12.2.2. 文件目录设计
      3. 12.2.3. 入口文件预览
    3. 12.3. 数据库设计
      1. 12.3.1. 初始化数据库脚本
        1. 12.3.1.1. 脚本目录
    4. 12.4. 路由设计
      1. 12.4.1. 使用koa-router中间件
        1. 12.4.1.1. 路由目录
        2. 12.4.1.2. 子路由配置
        3. 12.4.1.3. resetful API 子路由
          1. 12.4.1.3.1. 子路由汇总
          2. 12.4.1.3.2. app.js加载路由中间件
    5. 12.5. webpack4 环境搭建
      1. 12.5.1. 前言
      2. 12.5.2. webpack4
        1. 12.5.2.1. 安装和文档
      3. 12.5.3. 配置webpack4编译react.js + less + sass + antd 环境
        1. 12.5.3.1. 文件目录
        2. 12.5.3.2. webpack4 编译基础配置
          1. 12.5.3.2.1. babel@7 配置
          2. 12.5.3.2.2. webpack.base.config.js
        3. 12.5.3.3. 配置开发&生产环境webpack4 编译设置
          1. 12.5.3.3.1. 开发环境配置 wepack.dev.config.js
          2. 12.5.3.3.2. 编译环境配置 wepack.prod.config.js
    6. 12.6. 使用react.js
      1. 12.6.1. react.js简介
      2. 12.6.2. 编译使用
      3. 12.6.3. 前端待编译源文件目录
        1. 12.6.3.1. react.js页面应用文件
        2. 12.6.3.2. react.js执行render渲染
        3. 12.6.3.3. 静态页面引用react.js编译后文件
        4. 12.6.3.4. 页面渲染效果
    7. 12.7. 登录注册功能实现
      1. 12.7.1. 用户模型dao操作
        1. 12.7.1.1. 业务层操作
        2. 12.7.1.2. controller 操作
        3. 12.7.1.3. api路由操作
      2. 12.7.2. 前端用react.js实现效果
    8. 12.8. session登录态判断处理
      1. 12.8.1. 使用session中间件
      2. 12.8.2. 登录成功后设置session到MySQL和设置sessionId到cookie
      3. 12.8.3. 需要判断登录态页面进行session判断
  • 13. 其他进阶
    1. 13.0.1. 前言
    2. 13.0.2. Node 9下import/export使用简单须知
    3. 13.0.3. 使用简述
      1. 13.0.3.1. 与require()区别
    4. 13.0.4. Loader Hooks模式使用
      1. 13.0.4.1. Loader Hooks 使用步骤
    5. 13.0.5. Koa2 直接使用import/export
      1. 13.0.5.1. 自定义loader规则优化
      2. 13.0.5.2. loader规则优化解析
      3. 13.0.5.3. 规则总结
  • mysql学习笔记整理-账号管理

    koa2开始

    koa2 快速开始

    环境准备

    快速开始

    安装koa2

    1
    2
    3
    4
    5
    ## 初始化package.json
    npm init

    ## 安装koa2
    npm install koa

    hello world 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const Koa = require('koa')
    const app = new Koa()

    app.use( async ( ctx ) => {
    ctx.body = 'hello koa2'
    })

    app.listen(3000)
    console.log('[demo] start-quick is starting at port 3000')

    启动demo

    由于koa2是基于async/await操作中间件,目前node.js 7.x的harmony模式下才能使用,所以启动的时的脚本如下:

    1
    node index.js

    访问http:localhost:3000,效果如下

    start-result-01

    async/await使用

    快速上手理解

    先复制以下这段代码,在粘贴在chrome的控制台console中,按回车键执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    function getSyncTime() {
    return new Promise((resolve, reject) => {
    try {
    let startTime = new Date().getTime()
    setTimeout(() => {
    let endTime = new Date().getTime()
    let data = endTime - startTime
    resolve( data )
    }, 500)
    } catch ( err ) {
    reject( err )
    }
    })
    }

    async function getSyncData() {
    let time = await getSyncTime()
    let data = `endTime - startTime = ${time}`
    return data
    }

    async function getData() {
    let data = await getSyncData()
    console.log( data )
    }

    getData()

    在chrome的console中执行结果如下

    async

    从上述例子可以看出 async/await 的特点:

    • 可以让异步逻辑用同步写法实现
    • 最底层的await返回需要是Promise对象
    • 可以通过多层 async function 的同步写法代替传统的callback嵌套

      koa2简析结构

    源码文件

    1
    2
    3
    4
    5
    6
    ├── lib
    │   ├── application.js
    │   ├── context.js
    │   ├── request.js
    │   └── response.js
    └── package.json

    这个就是 GitHub https://github.com/koajs/koa上开源的koa2源码的源文件结构,核心代码就是lib目录下的四个文件

    • application.js 是整个koa2 的入口文件,封装了context,request,response,以及最核心的中间件处理流程。
    • context.js 处理应用上下文,里面直接封装部分request.js和response.js的方法
    • request.js 处理http请求
    • response.js 处理http响应

    koa2特性

    • 只提供封装好http上下文、请求、响应,以及基于async/await的中间件容器。
    • 利用ES7的async/await的来处理传统回调嵌套问题和代替koa@1的generator,但是需要在node.js 7.x的harmony模式下才能支持async/await
    • 中间件只支持 async/await 封装的,如果要使用koa@1基于generator中间件,需要通过中间件koa-convert封装一下才能使用。

      koa中间件开发和使用

    注:原文地址在我的博客issue里https://github.com/ChenShenhai/blog/issues/15

    • koa v1和v2中使用到的中间件的开发和使用
    • generator 中间件开发在koa v1和v2中使用
    • async await 中间件开发和只能在koa v2中使用

    generator中间件开发

    generator中间件开发

    generator中间件返回的应该是function * () 函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /* ./middleware/logger-generator.js */
    function log( ctx ) {
    console.log( ctx.method, ctx.header.host + ctx.url )
    }

    module.exports = function () {
    return function * ( next ) {

    // 执行中间件的操作
    log( this )

    if ( next ) {
    yield next
    }
    }
    }

    generator中间件在koa@1中的使用

    generator 中间件在koa v1中可以直接use使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const koa = require('koa')  // koa v1
    const loggerGenerator = require('./middleware/logger-generator')
    const app = koa()

    app.use(loggerGenerator())

    app.use(function *( ) {
    this.body = 'hello world!'
    })

    app.listen(3000)
    console.log('the server is starting at port 3000')

    generator中间件在koa@2中的使用

    generator 中间件在koa v2中需要用koa-convert封装一下才能使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const Koa = require('koa') // koa v2
    const convert = require('koa-convert')
    const loggerGenerator = require('./middleware/logger-generator')
    const app = new Koa()

    app.use(convert(loggerGenerator()))

    app.use(( ctx ) => {
    ctx.body = 'hello world!'
    })

    app.listen(3000)
    console.log('the server is starting at port 3000')

    async中间件开发

    async 中间件开发

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /* ./middleware/logger-async.js */

    function log( ctx ) {
    console.log( ctx.method, ctx.header.host + ctx.url )
    }

    module.exports = function () {
    return async function ( ctx, next ) {
    log(ctx);
    await next()
    }
    }

    async 中间件在koa@2中使用

    async 中间件只能在 koa v2中使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const Koa = require('koa') // koa v2
    const loggerAsync = require('./middleware/logger-async')
    const app = new Koa()

    app.use(loggerAsync())

    app.use(( ctx ) => {
    ctx.body = 'hello world!'
    })

    app.listen(3000)
    console.log('the server is starting at port 3000')

    路由

    koa2 原生路由实现

    简单例子

    1
    2
    3
    4
    5
    6
    7
    8
    const Koa = require('koa')
    const app = new Koa()

    app.use( async ( ctx ) => {
    let url = ctx.request.url
    ctx.body = url
    })
    app.listen(3000)

    访问 http://localhost:3000/hello/world 页面会输出 /hello/world,也就是说上下文的请求request对象中url之就是当前访问的路径名称,可以根据ctx.request.url 通过一定的判断或者正则匹配就可以定制出所需要的路由。

    定制化的路由

    demo源码

    https://github.com/ChenShenhai/koa2-note/tree/master/demo/route-simple

    源码文件目录

    1
    2
    3
    4
    5
    6
    7
    .
    ├── index.js
    ├── package.json
    └── view
    ├── 404.html
    ├── index.html
    └── todo.html

    demo源码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    const Koa = require('koa')
    const fs = require('fs')
    const app = new Koa()

    /**
    * 用Promise封装异步读取文件方法
    * @param {string} page html文件名称
    * @return {promise}
    */
    function render( page ) {
    return new Promise(( resolve, reject ) => {
    let viewUrl = `./view/${page}`
    fs.readFile(viewUrl, "binary", ( err, data ) => {
    if ( err ) {
    reject( err )
    } else {
    resolve( data )
    }
    })
    })
    }

    /**
    * 根据URL获取HTML内容
    * @param {string} url koa2上下文的url,ctx.url
    * @return {string} 获取HTML文件内容
    */
    async function route( url ) {
    let view = '404.html'
    switch ( url ) {
    case '/':
    view = 'index.html'
    break
    case '/index':
    view = 'index.html'
    break
    case '/todo':
    view = 'todo.html'
    break
    case '/404':
    view = '404.html'
    break
    default:
    break
    }
    let html = await render( view )
    return html
    }

    app.use( async ( ctx ) => {
    let url = ctx.request.url
    let html = await route( url )
    ctx.body = html
    })

    app.listen(3000)
    console.log('[demo] route-simple is starting at port 3000')

    运行demo

    执行运行脚本
    1
    node -harmony index.js
    运行效果如下

    访问http://localhost:3000/index
    route-result-01## koa-router中间件

    如果依靠ctx.request.url去手动处理路由,将会写很多处理代码,这时候就需要对应的路由的中间件对路由进行控制,这里介绍一个比较好用的路由中间件koa-router

    安装koa-router中间件

    1
    2
    ## koa2 对应的版本是 7.x
    npm install --save koa-router@7

    快速使用koa-router

    demo源码

    https://github.com/ChenShenhai/koa2-note/tree/master/demo/route-use-middleware

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    const Koa = require('koa')
    const fs = require('fs')
    const app = new Koa()

    const Router = require('koa-router')

    let home = new Router()

    // 子路由1
    home.get('/', async ( ctx )=>{
    let html = `
    <ul>
    <li><a href="/page/helloworld">/page/helloworld</a></li>
    <li><a href="/page/404">/page/404</a></li>
    </ul>
    `
    ctx.body = html
    })

    // 子路由2
    let page = new Router()
    page.get('/404', async ( ctx )=>{
    ctx.body = '404 page!'
    }).get('/helloworld', async ( ctx )=>{
    ctx.body = 'helloworld page!'
    })

    // 装载所有子路由
    let router = new Router()
    router.use('/', home.routes(), home.allowedMethods())
    router.use('/page', page.routes(), page.allowedMethods())

    // 加载路由中间件
    app.use(router.routes()).use(router.allowedMethods())

    app.listen(3000, () => {
    console.log('[demo] route-use-middleware is starting at port 3000')
    })

    请求数据获取

    GET请求数据获取

    使用方法

    在koa中,获取GET请求数据源头是koa中request对象中的query方法或querystring方法,query返回是格式化好的参数对象,querystring返回的是请求字符串,由于ctx对request的API有直接引用的方式,所以获取GET请求数据有两个途径。

    • 1.是从上下文中直接获取
      • 请求对象ctx.query,返回如 { a:1, b:2 }
      • 请求字符串 ctx.querystring,返回如 a=1&b=2
    • 2.是从上下文的request对象中获取
      • 请求对象ctx.request.query,返回如 { a:1, b:2 }
      • 请求字符串 ctx.request.querystring,返回如 a=1&b=2

    举个例子

    demo源码

    https://github.com/ChenShenhai/koa2-note/blob/master/demo/request/get.js

    例子代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    const Koa = require('koa')
    const app = new Koa()

    app.use( async ( ctx ) => {
    let url = ctx.url
    // 从上下文的request对象中获取
    let request = ctx.request
    let req_query = request.query
    let req_querystring = request.querystring

    // 从上下文中直接获取
    let ctx_query = ctx.query
    let ctx_querystring = ctx.querystring

    ctx.body = {
    url,
    req_query,
    req_querystring,
    ctx_query,
    ctx_querystring
    }
    })

    app.listen(3000, () => {
    console.log('[demo] request get is starting at port 3000')
    })

    执行程序

    1
    node get.js

    执行后程序后,用chrome访问 http://localhost:3000/page/user?a=1&b=2 会出现以下情况

    注意:我是用了chrome的json格式化插件才会显示json的格式化

    request-get

    POST请求参数获取

    原理

    对于POST请求的处理,koa2没有封装获取参数的方法,需要通过解析上下文context中的原生node.js请求对象req,将POST表单数据解析成query string(例如:a=1&b=2&c=3),再将query string 解析成JSON格式(例如:{"a":"1", "b":"2", "c":"3"}

    注意:ctx.request是context经过封装的请求对象,ctx.req是context提供的node.js原生HTTP请求对象,同理ctx.response是context经过封装的响应对象,ctx.res是context提供的node.js原生HTTP响应对象。

    具体koa2 API文档可见 https://github.com/koajs/koa/blob/master/docs/api/context.md#ctxreq

    解析出POST请求上下文中的表单数据

    demo源码

    https://github.com/ChenShenhai/koa2-note/blob/master/demo/request/post.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // 解析上下文里node原生请求的POST参数
    function parsePostData( ctx ) {
    return new Promise((resolve, reject) => {
    try {
    let postdata = "";
    ctx.req.addListener('data', (data) => {
    postdata += data
    })
    ctx.req.addListener("end",function(){
    let parseData = parseQueryStr( postdata )
    resolve( parseData )
    })
    } catch ( err ) {
    reject(err)
    }
    })
    }

    // 将POST请求参数字符串解析成JSON
    function parseQueryStr( queryStr ) {
    let queryData = {}
    let queryStrList = queryStr.split('&')
    console.log( queryStrList )
    for ( let [ index, queryStr ] of queryStrList.entries() ) {
    let itemList = queryStr.split('=')
    queryData[ itemList[0] ] = decodeURIComponent(itemList[1])
    }
    return queryData
    }

    举个例子

    源码在 /demos/request/post.js中

    例子代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    const Koa = require('koa')
    const app = new Koa()

    app.use( async ( ctx ) => {

    if ( ctx.url === '/' && ctx.method === 'GET' ) {
    // 当GET请求时候返回表单页面
    let html = `
    <h1>koa2 request post demo</h1>
    <form method="POST" action="/">
    <p>userName</p>
    <input name="userName" /><br/>
    <p>nickName</p>
    <input name="nickName" /><br/>
    <p>email</p>
    <input name="email" /><br/>
    <button type="submit">submit</button>
    </form>
    `
    ctx.body = html
    } else if ( ctx.url === '/' && ctx.method === 'POST' ) {
    // 当POST请求的时候,解析POST表单里的数据,并显示出来
    let postData = await parsePostData( ctx )
    ctx.body = postData
    } else {
    // 其他请求显示404
    ctx.body = '<h1>404!!! o(╯□╰)o</h1>'
    }
    })

    // 解析上下文里node原生请求的POST参数
    function parsePostData( ctx ) {
    return new Promise((resolve, reject) => {
    try {
    let postdata = "";
    ctx.req.addListener('data', (data) => {
    postdata += data
    })
    ctx.req.addListener("end",function(){
    let parseData = parseQueryStr( postdata )
    resolve( parseData )
    })
    } catch ( err ) {
    reject(err)
    }
    })
    }

    // 将POST请求参数字符串解析成JSON
    function parseQueryStr( queryStr ) {
    let queryData = {}
    let queryStrList = queryStr.split('&')
    console.log( queryStrList )
    for ( let [ index, queryStr ] of queryStrList.entries() ) {
    let itemList = queryStr.split('=')
    queryData[ itemList[0] ] = decodeURIComponent(itemList[1])
    }
    return queryData
    }

    app.listen(3000, () => {
    console.log('[demo] request post is starting at port 3000')
    })

    启动例子

    1
    node post.js

    访问页面

    request-post-form

    提交表单发起POST请求结果显示

    request-post-result

    koa-bodyparser中间件

    原理

    对于POST请求的处理,koa-bodyparser中间件可以把koa2上下文的formData数据解析到ctx.request.body中

    安装koa2版本的koa-bodyparser@3中间件

    1
    npm install --save koa-bodyparser@3

    举个例子

    例子代码

    demo源码

    https://github.com/ChenShenhai/koa2-note/blob/master/demo/request/post-middleware.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    const Koa = require('koa')
    const app = new Koa()
    const bodyParser = require('koa-bodyparser')

    // 使用ctx.body解析中间件
    app.use(bodyParser())

    app.use( async ( ctx ) => {

    if ( ctx.url === '/' && ctx.method === 'GET' ) {
    // 当GET请求时候返回表单页面
    let html = `
    <h1>koa2 request post demo</h1>
    <form method="POST" action="/">
    <p>userName</p>
    <input name="userName" /><br/>
    <p>nickName</p>
    <input name="nickName" /><br/>
    <p>email</p>
    <input name="email" /><br/>
    <button type="submit">submit</button>
    </form>
    `
    ctx.body = html
    } else if ( ctx.url === '/' && ctx.method === 'POST' ) {
    // 当POST请求的时候,中间件koa-bodyparser解析POST表单里的数据,并显示出来
    let postData = ctx.request.body
    ctx.body = postData
    } else {
    // 其他请求显示404
    ctx.body = '<h1>404!!! o(╯□╰)o</h1>'
    }
    })

    app.listen(3000, () => {
    console.log('[demo] request post is starting at port 3000')
    })

    启动例子

    1
    node post-middleware.js

    访问页面

    request-post-form

    提交表单发起POST请求结果显示

    request-post-result

    静态资源加载

    原生koa2实现静态资源服务器

    前言

    一个http请求访问web服务静态资源,一般响应结果有三种情况

    • 访问文本,例如js,css,png,jpg,gif
    • 访问静态目录
    • 找不到资源,抛出404错误

    原生koa2 静态资源服务器例子

    demo源码

    https://github.com/ChenShenhai/koa2-note/blob/master/demo/static-server/

    代码目录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ├── static ## 静态资源目录
    │   ├── css/
    │   ├── image/
    │   ├── js/
    │   └── index.html
    ├── util ## 工具代码
    │ ├── content.js ## 读取请求内容
    │ ├── dir.js ## 读取目录内容
    │ ├── file.js ## 读取文件内容
    │ ├── mimes.js ## 文件类型列表
    │ └── walk.js ## 遍历目录内容
    └── index.js ## 启动入口文件

    代码解析

    index.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    const Koa = require('koa')
    const path = require('path')
    const content = require('./util/content')
    const mimes = require('./util/mimes')

    const app = new Koa()

    // 静态资源目录对于相对入口文件index.js的路径
    const staticPath = './static'

    // 解析资源类型
    function parseMime( url ) {
    let extName = path.extname( url )
    extName = extName ? extName.slice(1) : 'unknown'
    return mimes[ extName ]
    }

    app.use( async ( ctx ) => {
    // 静态资源目录在本地的绝对路径
    let fullStaticPath = path.join(__dirname, staticPath)

    // 获取静态资源内容,有可能是文件内容,目录,或404
    let _content = await content( ctx, fullStaticPath )

    // 解析请求内容的类型
    let _mime = parseMime( ctx.url )

    // 如果有对应的文件类型,就配置上下文的类型
    if ( _mime ) {
    ctx.type = _mime
    }

    // 输出静态资源内容
    if ( _mime && _mime.indexOf('image/') >= 0 ) {
    // 如果是图片,则用node原生res,输出二进制数据
    ctx.res.writeHead(200)
    ctx.res.write(_content, 'binary')
    ctx.res.end()
    } else {
    // 其他则输出文本
    ctx.body = _content
    }
    })

    app.listen(3000)
    console.log('[demo] static-server is starting at port 3000')
    util/content.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    const path = require('path')
    const fs = require('fs')

    // 封装读取目录内容方法
    const dir = require('./dir')

    // 封装读取文件内容方法
    const file = require('./file')


    /**
    * 获取静态资源内容
    * @param {object} ctx koa上下文
    * @param {string} 静态资源目录在本地的绝对路径
    * @return {string} 请求获取到的本地内容
    */
    async function content( ctx, fullStaticPath ) {

    // 封装请求资源的完绝对径
    let reqPath = path.join(fullStaticPath, ctx.url)

    // 判断请求路径是否为存在目录或者文件
    let exist = fs.existsSync( reqPath )

    // 返回请求内容, 默认为空
    let content = ''

    if( !exist ) {
    //如果请求路径不存在,返回404
    content = '404 Not Found! o(╯□╰)o!'
    } else {
    //判断访问地址是文件夹还是文件
    let stat = fs.statSync( reqPath )

    if( stat.isDirectory() ) {
    //如果为目录,则渲读取目录内容
    content = dir( ctx.url, reqPath )

    } else {
    // 如果请求为文件,则读取文件内容
    content = await file( reqPath )
    }
    }

    return content
    }

    module.exports = content
    util/dir.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    const url = require('url')
    const fs = require('fs')
    const path = require('path')

    // 遍历读取目录内容方法
    const walk = require('./walk')

    /**
    * 封装目录内容
    * @param {string} url 当前请求的上下文中的url,即ctx.url
    * @param {string} reqPath 请求静态资源的完整本地路径
    * @return {string} 返回目录内容,封装成HTML
    */
    function dir ( url, reqPath ) {

    // 遍历读取当前目录下的文件、子目录
    let contentList = walk( reqPath )

    let html = `<ul>`
    for ( let [ index, item ] of contentList.entries() ) {
    html = `${html}<li><a href="${url === '/' ? '' : url}/${item}">${item}</a>`
    }
    html = `${html}</ul>`

    return html
    }

    module.exports = dir
    util/file.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const fs = require('fs')

    /**
    * 读取文件方法
    * @param {string} 文件本地的绝对路径
    * @return {string|binary}
    */
    function file ( filePath ) {

    let content = fs.readFileSync(filePath, 'binary' )
    return content
    }

    module.exports = file
    util/walk.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    const fs = require('fs')
    const mimes = require('./mimes')

    /**
    * 遍历读取目录内容(子目录,文件名)
    * @param {string} reqPath 请求资源的绝对路径
    * @return {array} 目录内容列表
    */
    function walk( reqPath ){

    let files = fs.readdirSync( reqPath );

    let dirList = [], fileList = [];
    for( let i=0, len=files.length; i<len; i++ ) {
    let item = files[i];
    let itemArr = item.split("\.");
    let itemMime = ( itemArr.length > 1 ) ? itemArr[ itemArr.length - 1 ] : "undefined";

    if( typeof mimes[ itemMime ] === "undefined" ) {
    dirList.push( files[i] );
    } else {
    fileList.push( files[i] );
    }
    }


    let result = dirList.concat( fileList );

    return result;
    };

    module.exports = walk;
    util/mime.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    let mimes = {
    'css': 'text/css',
    'less': 'text/css',
    'gif': 'image/gif',
    'html': 'text/html',
    'ico': 'image/x-icon',
    'jpeg': 'image/jpeg',
    'jpg': 'image/jpeg',
    'js': 'text/javascript',
    'json': 'application/json',
    'pdf': 'application/pdf',
    'png': 'image/png',
    'svg': 'image/svg+xml',
    'swf': 'application/x-shockwave-flash',
    'tiff': 'image/tiff',
    'txt': 'text/plain',
    'wav': 'audio/x-wav',
    'wma': 'audio/x-ms-wma',
    'wmv': 'video/x-ms-wmv',
    'xml': 'text/xml'
    }

    module.exports = mimes

    运行效果

    启动服务
    1
    node index.js
    效果
    访问http://localhost:3000

    static-server-result

    访问http://localhost:3000/index.html

    static-server-result

    访问http://localhost:3000/js/index.js

    static-server-result

    koa-static中间件使用

    使用例子

    demo源码

    https://github.com/ChenShenhai/koa2-note/blob/master/demo/static-use-middleware/

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    const Koa = require('koa')
    const path = require('path')
    const static = require('koa-static')

    const app = new Koa()

    // 静态资源目录对于相对入口文件index.js的路径
    const staticPath = './static'

    app.use(static(
    path.join( __dirname, staticPath)
    ))


    app.use( async ( ctx ) => {
    ctx.body = 'hello world'
    })

    app.listen(3000, () => {
    console.log('[demo] static-use-middleware is starting at port 3000')
    })
    效果
    访问http://localhost:3000

    static-server-result

    访问http://localhost:3000/index.html

    static-server-result

    访问http://localhost:3000/js/index.js

    static-server-result

    cookie/session

    koa2使用cookie

    使用方法

    koa提供了从上下文直接读取、写入cookie的方法

    • ctx.cookies.get(name, [options]) 读取上下文请求中的cookie
    • ctx.cookies.set(name, value, [options]) 在上下文中写入cookie

    koa2 中操作的cookies是使用了npm的cookies模块,源码在https://github.com/pillarjs/cookies,所以在读写cookie的使用参数与该模块的使用一致。

    例子代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    const Koa = require('koa')
    const app = new Koa()

    app.use( async ( ctx ) => {

    if ( ctx.url === '/index' ) {
    ctx.cookies.set(
    'cid',
    'hello world',
    {
    domain: 'localhost', // 写cookie所在的域名
    path: '/index', // 写cookie所在的路径
    maxAge: 10 * 60 * 1000, // cookie有效时长
    expires: new Date('2017-02-15'), // cookie失效时间
    httpOnly: false, // 是否只用于http请求中获取
    overwrite: false // 是否允许重写
    }
    )
    ctx.body = 'cookie is ok'
    } else {
    ctx.body = 'hello world'
    }

    })

    app.listen(3000, () => {
    console.log('[demo] cookie is starting at port 3000')
    })

    运行例子

    执行脚本

    1
    node index.js

    运行结果

    访问http://localhost:3000/index
    • 可以在控制台的cookie列表中中看到写在页面上的cookie
    • 在控制台的console中使用document.cookie可以打印出在页面的所有cookie(需要是httpOnly设置false才能显示)

    cookie-result-01

    koa2实现session

    前言

    koa2原生功能只提供了cookie的操作,但是没有提供session操作。session就只用自己实现或者通过第三方中间件实现。在koa2中实现session的方案有一下几种

    • 如果session数据量很小,可以直接存在内存中
    • 如果session数据量很大,则需要存储介质存放session数据

    数据库存储方案

    • 将session存放在MySQL数据库中
    • 需要用到中间件
      • koa-session-minimal 适用于koa2 的session中间件,提供存储介质的读写接口 。
      • koa-mysql-session 为koa-session-minimal中间件提供MySQL数据库的session数据读写操作。
      • 将sessionId和对应的数据存到数据库
    • 将数据库的存储的sessionId存到页面的cookie中
    • 根据cookie的sessionId去获取对于的session信息

    快速使用

    demo源码

    https://github.com/ChenShenhai/koa2-note/blob/master/demo/session/index.js

    例子代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    const Koa = require('koa')
    const session = require('koa-session-minimal')
    const MysqlSession = require('koa-mysql-session')

    const app = new Koa()

    // 配置存储session信息的mysql
    let store = new MysqlSession({
    user: 'root',
    password: 'abc123',
    database: 'koa_demo',
    host: '127.0.0.1',
    })

    // 存放sessionId的cookie配置
    let cookie = {
    maxAge: '', // cookie有效时长
    expires: '', // cookie失效时间
    path: '', // 写cookie所在的路径
    domain: '', // 写cookie所在的域名
    httpOnly: '', // 是否只用于http请求中获取
    overwrite: '', // 是否允许重写
    secure: '',
    sameSite: '',
    signed: '',

    }

    // 使用session中间件
    app.use(session({
    key: 'SESSION_ID',
    store: store,
    cookie: cookie
    }))

    app.use( async ( ctx ) => {

    // 设置session
    if ( ctx.url === '/set' ) {
    ctx.session = {
    user_id: Math.random().toString(36).substr(2),
    count: 0
    }
    ctx.body = ctx.session
    } else if ( ctx.url === '/' ) {

    // 读取session信息
    ctx.session.count = ctx.session.count + 1
    ctx.body = ctx.session
    }

    })

    app.listen(3000)
    console.log('[demo] session is starting at port 3000')

    运行例子

    执行命令
    1
    node index.js
    访问连接设置session

    http://localhost:3000/set
    session-result-01

    查看数据库session是否存储

    session-result-01

    查看cookie中是否种下了sessionId

    http://localhost:3000
    session-result-01

    模板引擎

    koa2加载模板引擎

    快速开始

    安装模块

    1
    2
    3
    4
    5
    ## 安装koa模板使用中间件
    npm install --save koa-views

    ## 安装ejs模板引擎
    npm install --save ejs

    使用模板引擎

    demo源码

    https://github.com/ChenShenhai/koa2-note/blob/master/demo/ejs/

    文件目录
    1
    2
    3
    4
    ├── package.json
    ├── index.js
    └── view
    └── index.ejs
    ./index.js文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    const Koa = require('koa')
    const views = require('koa-views')
    const path = require('path')
    const app = new Koa()

    // 加载模板引擎
    app.use(views(path.join(__dirname, './view'), {
    extension: 'ejs'
    }))

    app.use( async ( ctx ) => {
    let title = 'hello koa2'
    await ctx.render('index', {
    title,
    })
    })

    app.listen(3000)
    ./view/index.ejs 模板
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!DOCTYPE html>
    <html>
    <head>
    <title><%= title %></title>
    </head>
    <body>
    <h1><%= title %></h1>
    <p>EJS Welcome to <%= title %></p>
    </body>
    </html>

    ejs模板引擎

    具体查看ejs官方文档

    https://github.com/mde/ejs

    文件上传

    busboy模块

    快速开始

    安装

    1
    npm install --save busboy

    模块简介

    busboy 模块是用来解析POST请求,node原生req中的文件流。

    开始使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    const inspect = require('util').inspect 
    const path = require('path')
    const fs = require('fs')
    const Busboy = require('busboy')

    // req 为node原生请求
    const busboy = new Busboy({ headers: req.headers })

    // ...

    // 监听文件解析事件
    busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
    console.log(`File [${fieldname}]: filename: ${filename}`)


    // 文件保存到特定路径
    file.pipe(fs.createWriteStream('./upload'))

    // 开始解析文件流
    file.on('data', function(data) {
    console.log(`File [${fieldname}] got ${data.length} bytes`)
    })

    // 解析文件结束
    file.on('end', function() {
    console.log(`File [${fieldname}] Finished`)
    })
    })

    // 监听请求中的字段
    busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated) {
    console.log(`Field [${fieldname}]: value: ${inspect(val)}`)
    })

    // 监听结束事件
    busboy.on('finish', function() {
    console.log('Done parsing form!')
    res.writeHead(303, { Connection: 'close', Location: '/' })
    res.end()
    })
    req.pipe(busboy)

    更多模块信息

    更多详细API可以访问npm官方文档 https://www.npmjs.com/package/busboy

    上传文件简单实现

    依赖模块

    安装依赖

    1
    npm install --save busboy
    • busboy 是用来解析出请求中文件流

    例子源码

    demo源码

    https://github.com/ChenShenhai/koa2-note/blob/master/demo/upload/

    封装上传文件到写入服务的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    const inspect = require('util').inspect
    const path = require('path')
    const os = require('os')
    const fs = require('fs')
    const Busboy = require('busboy')

    /**
    * 同步创建文件目录
    * @param {string} dirname 目录绝对地址
    * @return {boolean} 创建目录结果
    */
    function mkdirsSync( dirname ) {
    if (fs.existsSync( dirname )) {
    return true
    } else {
    if (mkdirsSync( path.dirname(dirname)) ) {
    fs.mkdirSync( dirname )
    return true
    }
    }
    }

    /**
    * 获取上传文件的后缀名
    * @param {string} fileName 获取上传文件的后缀名
    * @return {string} 文件后缀名
    */
    function getSuffixName( fileName ) {
    let nameList = fileName.split('.')
    return nameList[nameList.length - 1]
    }

    /**
    * 上传文件
    * @param {object} ctx koa上下文
    * @param {object} options 文件上传参数 fileType文件类型, path文件存放路径
    * @return {promise}
    */
    function uploadFile( ctx, options) {
    let req = ctx.req
    let res = ctx.res
    let busboy = new Busboy({headers: req.headers})

    // 获取类型
    let fileType = options.fileType || 'common'
    let filePath = path.join( options.path, fileType)
    let mkdirResult = mkdirsSync( filePath )

    return new Promise((resolve, reject) => {
    console.log('文件上传中...')
    let result = {
    success: false,
    formData: {},
    }

    // 解析请求文件事件
    busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
    let fileName = Math.random().toString(16).substr(2) + '.' + getSuffixName(filename)
    let _uploadFilePath = path.join( filePath, fileName )
    let saveTo = path.join(_uploadFilePath)

    // 文件保存到制定路径
    file.pipe(fs.createWriteStream(saveTo))

    // 文件写入事件结束
    file.on('end', function() {
    result.success = true
    result.message = '文件上传成功'

    console.log('文件上传成功!')
    resolve(result)
    })
    })

    // 解析表单中其他字段信息
    busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) {
    console.log('表单字段数据 [' + fieldname + ']: value: ' + inspect(val));
    result.formData[fieldname] = inspect(val);
    });

    // 解析结束事件
    busboy.on('finish', function( ) {
    console.log('文件上结束')
    resolve(result)
    })

    // 解析错误事件
    busboy.on('error', function(err) {
    console.log('文件上出错')
    reject(result)
    })

    req.pipe(busboy)
    })

    }


    module.exports = {
    uploadFile
    }

    入口文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    const Koa = require('koa')
    const path = require('path')
    const app = new Koa()
    // const bodyParser = require('koa-bodyparser')

    const { uploadFile } = require('./util/upload')

    // app.use(bodyParser())

    app.use( async ( ctx ) => {

    if ( ctx.url === '/' && ctx.method === 'GET' ) {
    // 当GET请求时候返回表单页面
    let html = `
    <h1>koa2 upload demo</h1>
    <form method="POST" action="/upload.json" enctype="multipart/form-data">
    <p>file upload</p>
    <span>picName:</span><input name="picName" type="text" /><br/>
    <input name="file" type="file" /><br/><br/>
    <button type="submit">submit</button>
    </form>
    `
    ctx.body = html

    } else if ( ctx.url === '/upload.json' && ctx.method === 'POST' ) {
    // 上传文件请求处理
    let result = { success: false }
    let serverFilePath = path.join( __dirname, 'upload-files' )

    // 上传文件事件
    result = await uploadFile( ctx, {
    fileType: 'album', // common or album
    path: serverFilePath
    })

    ctx.body = result
    } else {
    // 其他请求显示404
    ctx.body = '<h1>404!!! o(╯□╰)o</h1>'
    }
    })

    app.listen(3000, () => {
    console.log('[demo] upload-simple is starting at port 3000')
    })

    运行结果

    upload-simple-result

    upload-simple-result

    upload-simple-result

    upload-simple-result

    异步上传图片实现

    快速上手

    demo 地址

    https://github.com/ChenShenhai/koa2-note/tree/master/demo/upload-async

    源码理解

    demo源码目录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    .
    ├── index.js ## 后端启动文件
    ├── node_modules
    ├── package.json
    ├── static ## 静态资源目录
    │   ├── image ## 异步上传图片存储目录
    │   └── js
    │   └── index.js ## 上传图片前端js操作
    ├── util
    │   └── upload.js ## 后端处理图片流操作
    └── view
    └── index.ejs ## ejs后端渲染模板

    后端代码

    入口文件 demo/upload-async/index.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    const Koa = require('koa')
    const views = require('koa-views')
    const path = require('path')
    const convert = require('koa-convert')
    const static = require('koa-static')
    const { uploadFile } = require('./util/upload')

    const app = new Koa()

    /**
    * 使用第三方中间件 start
    */
    app.use(views(path.join(__dirname, './view'), {
    extension: 'ejs'
    }))

    // 静态资源目录对于相对入口文件index.js的路径
    const staticPath = './static'
    // 由于koa-static目前不支持koa2
    // 所以只能用koa-convert封装一下
    app.use(convert(static(
    path.join( __dirname, staticPath)
    )))
    /**
    * 使用第三方中间件 end
    */

    app.use( async ( ctx ) => {
    if ( ctx.method === 'GET' ) {
    let title = 'upload pic async'
    await ctx.render('index', {
    title,
    })
    } else if ( ctx.url === '/api/picture/upload.json' && ctx.method === 'POST' ) {
    // 上传文件请求处理
    let result = { success: false }
    let serverFilePath = path.join( __dirname, 'static/image' )

    // 上传文件事件
    result = await uploadFile( ctx, {
    fileType: 'album',
    path: serverFilePath
    })
    ctx.body = result
    } else {
    // 其他请求显示404
    ctx.body = '<h1>404!!! o(╯□╰)o</h1>'
    }

    })

    app.listen(3000, () => {
    console.log('[demo] upload-pic-async is starting at port 3000')
    })

    后端上传图片流写操作
    入口文件 demo/upload-async/util/upload.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    const inspect = require('util').inspect
    const path = require('path')
    const os = require('os')
    const fs = require('fs')
    const Busboy = require('busboy')

    /**
    * 同步创建文件目录
    * @param {string} dirname 目录绝对地址
    * @return {boolean} 创建目录结果
    */
    function mkdirsSync( dirname ) {
    if (fs.existsSync( dirname )) {
    return true
    } else {
    if (mkdirsSync( path.dirname(dirname)) ) {
    fs.mkdirSync( dirname )
    return true
    }
    }
    }

    /**
    * 获取上传文件的后缀名
    * @param {string} fileName 获取上传文件的后缀名
    * @return {string} 文件后缀名
    */
    function getSuffixName( fileName ) {
    let nameList = fileName.split('.')
    return nameList[nameList.length - 1]
    }

    /**
    * 上传文件
    * @param {object} ctx koa上下文
    * @param {object} options 文件上传参数 fileType文件类型, path文件存放路径
    * @return {promise}
    */
    function uploadFile( ctx, options) {
    let req = ctx.req
    let res = ctx.res
    let busboy = new Busboy({headers: req.headers})

    // 获取类型
    let fileType = options.fileType || 'common'
    let filePath = path.join( options.path, fileType)
    let mkdirResult = mkdirsSync( filePath )

    return new Promise((resolve, reject) => {
    console.log('文件上传中...')
    let result = {
    success: false,
    message: '',
    data: null
    }

    // 解析请求文件事件
    busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
    let fileName = Math.random().toString(16).substr(2) + '.' + getSuffixName(filename)
    let _uploadFilePath = path.join( filePath, fileName )
    let saveTo = path.join(_uploadFilePath)

    // 文件保存到制定路径
    file.pipe(fs.createWriteStream(saveTo))

    // 文件写入事件结束
    file.on('end', function() {
    result.success = true
    result.message = '文件上传成功'
    result.data = {
    pictureUrl: `//${ctx.host}/image/${fileType}/${fileName}`
    }
    console.log('文件上传成功!')
    resolve(result)
    })
    })

    // 解析结束事件
    busboy.on('finish', function( ) {
    console.log('文件上结束')
    resolve(result)
    })

    // 解析错误事件
    busboy.on('error', function(err) {
    console.log('文件上出错')
    reject(result)
    })

    req.pipe(busboy)
    })

    }

    module.exports = {
    uploadFile
    }

    前端代码

    1
    2
    3
    4
    5
    6
    7
    <button class="btn" id="J_UploadPictureBtn">上传图片</button>
    <hr/>
    <p>上传进度<span id="J_UploadProgress">0</span>%</p>
    <p>上传结果图片</p>
    <div id="J_PicturePreview" class="preview-picture">
    </div>
    <script src="/js/index.js"></script>

    上传操作代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    (function(){

    let btn = document.getElementById('J_UploadPictureBtn')
    let progressElem = document.getElementById('J_UploadProgress')
    let previewElem = document.getElementById('J_PicturePreview')
    btn.addEventListener('click', function(){
    uploadAction({
    success: function( result ) {
    console.log( result )
    if ( result && result.success && result.data && result.data.pictureUrl ) {
    previewElem.innerHTML = '<img src="'+ result.data.pictureUrl +'" style="max-width: 100%">'
    }
    },
    progress: function( data ) {
    if ( data && data * 1 > 0 ) {
    progressElem.innerText = data
    }
    }
    })
    })


    /**
    * 类型判断
    * @type {Object}
    */
    let UtilType = {
    isPrototype: function( data ) {
    return Object.prototype.toString.call(data).toLowerCase();
    },

    isJSON: function( data ) {
    return this.isPrototype( data ) === '[object object]';
    },

    isFunction: function( data ) {
    return this.isPrototype( data ) === '[object function]';
    }
    }

    /**
    * form表单上传请求事件
    * @param {object} options 请求参数
    */
    function requestEvent( options ) {
    try {
    let formData = options.formData
    let xhr = new XMLHttpRequest()
    xhr.onreadystatechange = function() {

    if ( xhr.readyState === 4 && xhr.status === 200 ) {
    options.success(JSON.parse(xhr.responseText))
    }
    }

    xhr.upload.onprogress = function(evt) {
    let loaded = evt.loaded
    let tot = evt.total
    let per = Math.floor(100 * loaded / tot)
    options.progress(per)
    }
    xhr.open('post', '/api/picture/upload.json')
    xhr.send(formData)
    } catch ( err ) {
    options.fail(err)
    }
    }

    /**
    * 上传事件
    * @param {object} options 上传参数
    */
    function uploadEvent ( options ){
    let file
    let formData = new FormData()
    let input = document.createElement('input')
    input.setAttribute('type', 'file')
    input.setAttribute('name', 'files')

    input.click()
    input.onchange = function () {
    file = input.files[0]
    formData.append('files', file)

    requestEvent({
    formData,
    success: options.success,
    fail: options.fail,
    progress: options.progress
    })
    }

    }

    /**
    * 上传操作
    * @param {object} options 上传参数
    */
    function uploadAction( options ) {
    if ( !UtilType.isJSON( options ) ) {
    console.log( 'upload options is null' )
    return
    }
    let _options = {}
    _options.success = UtilType.isFunction(options.success) ? options.success : function() {}
    _options.fail = UtilType.isFunction(options.fail) ? options.fail : function() {}
    _options.progress = UtilType.isFunction(options.progress) ? options.progress : function() {}

    uploadEvent(_options)
    }


    })()

    运行效果

    images/upload-async-result

    数据库mysql

    mysql模块

    快速开始

    安装MySQL数据库

    https://www.mysql.com/downloads/

    安装 node.js的mysql模块

    1
    npm install --save mysql

    模块介绍

    mysql模块是node操作MySQL的引擎,可以在node.js环境下对MySQL数据库进行建表,增、删、改、查等操作。

    开始使用

    创建数据库会话
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const mysql      = require('mysql')
    const connection = mysql.createConnection({
    host : '127.0.0.1', // 数据库地址
    user : 'root', // 数据库用户
    password : '123456' // 数据库密码
    database : 'my_database' // 选中数据库
    })

    // 执行sql脚本对数据库进行读写
    connection.query('SELECT * FROM my_table', (error, results, fields) => {
    if (error) throw error
    // connected!

    // 结束会话
    connection.release()
    });

    注意:一个事件就有一个从开始到结束的过程,数据库会话操作执行完后,就需要关闭掉,以免占用连接资源。

    创建数据连接池

    一般情况下操作数据库是很复杂的读写过程,不只是一个会话,如果直接用会话操作,就需要每次会话都要配置连接参数。所以这时候就需要连接池管理会话。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    const mysql = require('mysql')

    // 创建数据池
    const pool = mysql.createPool({
    host : '127.0.0.1', // 数据库地址
    user : 'root', // 数据库用户
    password : '123456' // 数据库密码
    database : 'my_database' // 选中数据库
    })

    // 在数据池中进行会话操作
    pool.getConnection(function(err, connection) {

    connection.query('SELECT * FROM my_table', (error, results, fields) => {

    // 结束会话
    connection.release();

    // 如果有错误就抛出
    if (error) throw error;
    })
    })

    更多模块信息

    更多详细API可以访问npm官方文档 https://www.npmjs.com/package/mysql

    async/await封装使用mysql

    前言

    由于mysql模块的操作都是异步操作,每次操作的结果都是在回调函数中执行,现在有了async/await,就可以用同步的写法去操作数据库

    Promise封装mysql模块

    Promise封装 ./async-db
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    const mysql = require('mysql')
    const pool = mysql.createPool({
    host : '127.0.0.1',
    user : 'root',
    password : '123456',
    database : 'my_database'
    })

    let query = function( sql, values ) {
    return new Promise(( resolve, reject ) => {
    pool.getConnection(function(err, connection) {
    if (err) {
    reject( err )
    } else {
    connection.query(sql, values, ( err, rows) => {

    if ( err ) {
    reject( err )
    } else {
    resolve( rows )
    }
    connection.release()
    })
    }
    })
    })
    }

    module.exports = { query }
    async/await使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const { query } = require('./async-db')
    async function selectAllData( ) {
    let sql = 'SELECT * FROM my_table'
    let dataList = await query( sql )
    return dataList
    }

    async function getData() {
    let dataList = await selectAllData()
    console.log( dataList )
    }

    getData()

    建表初始化

    前言

    通常初始化数据库要建立很多表,特别在项目开发的时候表的格式可能会有些变动,这时候就需要封装对数据库建表初始化的方法,保留项目的sql脚本文件,然后每次需要重新建表,则执行建表初始化程序就行

    快速开始

    demo源码

    https://github.com/ChenShenhai/koa2-note/blob/master/demo/mysql/

    源码目录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ├── index.js ## 程序入口文件
    ├── node_modules/
    ├── package.json
    ├── sql ## sql脚本文件目录
    │   ├── data.sql
    │   └── user.sql
    └── util ## 工具代码
    ├── db.js ## 封装的mysql模块方法
    ├── get-sql-content-map.js ## 获取sql脚本文件内容
    ├── get-sql-map.js ## 获取所有sql脚本文件
    └── walk-file.js ## 遍历sql脚本文件

    具体流程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
           +---------------------------------------------------+
    | |
    | +-----------+ +-----------+ +-----------+ |
    | | | | | | | |
    | | | | | | | |
    | | | | | | | |
    | | | | | | | |
    +----------+ 遍历sql +---+ 解析所有sql +---+ 执行sql +------------>
    | | 目录下的 | | 文件脚本 | | 脚本 | |
    +----------+ sql文件 +---+ 内容 +---+ +------------>
    | | | | | | | |
    | | | | | | | |
    | | | | | | | |
    | | | | | | | |
    | +-----------+ +-----------+ +-----------+ |
    | |
    +---------------------------------------------------+

    源码详解

    数据库操作文件 ./util/db.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    const mysql = require('mysql')

    const pool = mysql.createPool({
    host : '127.0.0.1',
    user : 'root',
    password : 'abc123',
    database : 'koa_demo'
    })

    let query = function( sql, values ) {

    return new Promise(( resolve, reject ) => {
    pool.getConnection(function(err, connection) {
    if (err) {
    reject( err )
    } else {
    connection.query(sql, values, ( err, rows) => {

    if ( err ) {
    reject( err )
    } else {
    resolve( rows )
    }
    connection.release()
    })
    }
    })
    })

    }

    module.exports = {
    query
    }

    获取所有sql脚本内容 ./util/get-sql-content-map.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    const fs = require('fs')
    const getSqlMap = require('./get-sql-map')

    let sqlContentMap = {}

    /**
    * 读取sql文件内容
    * @param {string} fileName 文件名称
    * @param {string} path 文件所在的路径
    * @return {string} 脚本文件内容
    */
    function getSqlContent( fileName, path ) {
    let content = fs.readFileSync( path, 'binary' )
    sqlContentMap[ fileName ] = content
    }

    /**
    * 封装所有sql文件脚本内容
    * @return {object}
    */
    function getSqlContentMap () {
    let sqlMap = getSqlMap()
    for( let key in sqlMap ) {
    getSqlContent( key, sqlMap[key] )
    }

    return sqlContentMap
    }

    module.exports = getSqlContentMap

    获取sql目录详情 ./util/get-sql-map.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    const fs = require('fs')
    const walkFile = require('./walk-file')

    /**
    * 获取sql目录下的文件目录数据
    * @return {object}
    */
    function getSqlMap () {
    let basePath = __dirname
    basePath = basePath.replace(/\\/g, '\/')

    let pathArr = basePath.split('\/')
    pathArr = pathArr.splice( 0, pathArr.length - 1 )
    basePath = pathArr.join('/') + '/sql/'

    let fileList = walkFile( basePath, 'sql' )
    return fileList
    }

    module.exports = getSqlMap

    遍历目录操作 ./util/walk-file.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    const fs = require('fs')

    /**
    * 遍历目录下的文件目录
    * @param {string} pathResolve 需进行遍历的目录路径
    * @param {string} mime 遍历文件的后缀名
    * @return {object} 返回遍历后的目录结果
    */
    const walkFile = function( pathResolve , mime ){

    let files = fs.readdirSync( pathResolve )

    let fileList = {}

    for( let [ i, item] of files.entries() ) {
    let itemArr = item.split('\.')

    let itemMime = ( itemArr.length > 1 ) ? itemArr[ itemArr.length - 1 ] : 'undefined'
    let keyName = item + ''
    if( mime === itemMime ) {
    fileList[ item ] = pathResolve + item
    }
    }

    return fileList
    }

    module.exports = walkFile

    入口文件 ./index.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41

    const fs = require('fs');
    const getSqlContentMap = require('./util/get-sql-content-map');
    const { query } = require('./util/db');


    // 打印脚本执行日志
    const eventLog = function( err , sqlFile, index ) {
    if( err ) {
    console.log(`[ERROR] sql脚本文件: ${sqlFile}${index + 1}条脚本 执行失败 o(╯□╰)o !`)
    } else {
    console.log(`[SUCCESS] sql脚本文件: ${sqlFile}${index + 1}条脚本 执行成功 O(∩_∩)O !`)
    }
    }

    // 获取所有sql脚本内容
    let sqlContentMap = getSqlContentMap()

    // 执行建表sql脚本
    const createAllTables = async () => {
    for( let key in sqlContentMap ) {
    let sqlShell = sqlContentMap[key]
    let sqlShellList = sqlShell.split(';')

    for ( let [ i, shell ] of sqlShellList.entries() ) {
    if ( shell.trim() ) {
    let result = await query( shell )
    if ( result.serverStatus * 1 === 2 ) {
    eventLog( null, key, i)
    } else {
    eventLog( true, key, i)
    }
    }
    }
    }
    console.log('sql脚本执行结束!')
    console.log('请按 ctrl + c 键退出!')

    }

    createAllTables()

    sql脚本文件 ./sql/data.sql

    1
    2
    3
    4
    5
    6
    7
    8
    CREATE TABLE   IF NOT EXISTS  `data` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `data_info` json DEFAULT NULL,
    `create_time` varchar(20) DEFAULT NULL,
    `modified_time` varchar(20) DEFAULT NULL,
    `level` int(11) DEFAULT NULL,
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8

    sql脚本文件 ./sql/user.sql

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    CREATE TABLE   IF NOT EXISTS  `user` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `email` varchar(255) DEFAULT NULL,
    `password` varchar(255) DEFAULT NULL,
    `name` varchar(255) DEFAULT NULL,
    `nick` varchar(255) DEFAULT NULL,
    `detail_info` json DEFAULT NULL,
    `create_time` varchar(20) DEFAULT NULL,
    `modified_time` varchar(20) DEFAULT NULL,
    `level` int(11) DEFAULT NULL,
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

    INSERT INTO `user` set email='1@example.com', password='123456';
    INSERT INTO `user` set email='2@example.com', password='123456';
    INSERT INTO `user` set email='3@example.com', password='123456';

    效果

    执行脚本

    1
    node index.js

    执行结果

    mysql-init-result-01

    查看数据库写入数据

    mysql-init-result-01

    JSONP实现

    原生koa2实现jsonp

    前言

    在项目复杂的业务场景,有时候需要在前端跨域获取数据,这时候提供数据的服务就需要提供跨域请求的接口,通常是使用JSONP的方式提供跨域接口。

    实现JSONP

    demo地址

    https://github.com/ChenShenhai/koa2-note/blob/master/demo/jsonp/

    具体原理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    // 判断是否为JSONP的请求
    if ( ctx.method === 'GET' && ctx.url.split('?')[0] === '/getData.jsonp') {
    // 获取jsonp的callback
    let callbackName = ctx.query.callback || 'callback'
    let returnData = {
    success: true,
    data: {
    text: 'this is a jsonp api',
    time: new Date().getTime(),
    }
    }

    // jsonp的script字符串
    let jsonpStr = `;${callbackName}(${JSON.stringify(returnData)})`

    // 用text/javascript,让请求支持跨域获取
    ctx.type = 'text/javascript'

    // 输出jsonp字符串
    ctx.body = jsonpStr
    }

    解析原理

    • JSONP跨域输出的数据是可执行的JavaScript代码
      • ctx输出的类型应该是’text/javascript’
      • ctx输出的内容为可执行的返回数据JavaScript代码字符串
    • 需要有回调函数名callbackName,前端获取后会通过动态执行JavaScript代码字符,获取里面的数据

    效果截图

    同域访问JSON请求

    jsonp-result-01

    跨域访问JSON请求

    jsonp-result-02

    完整demo代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    const Koa = require('koa')
    const app = new Koa()

    app.use( async ( ctx ) => {


    // 如果jsonp 的请求为GET
    if ( ctx.method === 'GET' && ctx.url.split('?')[0] === '/getData.jsonp') {

    // 获取jsonp的callback
    let callbackName = ctx.query.callback || 'callback'
    let returnData = {
    success: true,
    data: {
    text: 'this is a jsonp api',
    time: new Date().getTime(),
    }
    }

    // jsonp的script字符串
    let jsonpStr = `;${callbackName}(${JSON.stringify(returnData)})`

    // 用text/javascript,让请求支持跨域获取
    ctx.type = 'text/javascript'

    // 输出jsonp字符串
    ctx.body = jsonpStr

    } else {

    ctx.body = 'hello jsonp'

    }
    })

    app.listen(3000, () => {
    console.log('[demo] jsonp is starting at port 3000')
    })

    ```## koa-jsonp中间件

    koa.js 官方wiki中也介绍了不少jsonp的中间件
    ![jsonp-wiki](./../images/jsonp-wiki.png)

    其中koa-jsonp是支持koa2的,使用方式也非常简单,koa-jsonp的官方demo也很容易理解


    ### 快速使用

    demo地址

    [https://github.com/ChenShenhai/koa2-note/blob/master/demo/jsonp-use-middleware/](https://github.com/ChenShenhai/koa2-note/blob/master/demo/jsonp-use-middleware/)


    #### 安装
    ```sh
    npm install --save koa-jsonp

    简单例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    const Koa = require('koa')
    const jsonp = require('koa-jsonp')
    const app = new Koa()

    // 使用中间件
    app.use(jsonp())

    app.use( async ( ctx ) => {

    let returnData = {
    success: true,
    data: {
    text: 'this is a jsonp api',
    time: new Date().getTime(),
    }
    }

    // 直接输出JSON
    ctx.body = returnData
    })

    app.listen(3000, () => {
    console.log('[demo] jsonp is starting at port 3000')
    })

    测试

    单元测试

    前言

    测试是一个项目周期里必不可少的环节,开发者在开发过程中也是无时无刻进行“人工测试”,如果每次修改一点代码,都要牵一发动全身都要手动测试关联接口,这样子是禁锢了生产力。为了解放大部分测试生产力,相关的测试框架应运而生,比较出名的有mocha,karma,jasmine等。虽然框架繁多,但是使用起来都是大同小异。

    准备工作

    安装测试相关框架

    1
    npm install --save-dev mocha chai supertest
    • mocha 模块是测试框架
    • chai 模块是用来进行测试结果断言库,比如一个判断 1 + 1 是否等于 2
    • supertest 模块是http请求测试库,用来请求API接口

    测试例子

    demo地址

    https://github.com/ChenShenhai/koa2-note/blob/master/demo/test-unit/

    例子目录

    1
    2
    3
    4
    5
    .
    ├── index.js ## api文件
    ├── package.json
    └── test ## 测试目录
    └── index.test.js ## 测试用例

    所需测试demo

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    const Koa = require('koa')
    const app = new Koa()

    const server = async ( ctx, next ) => {
    let result = {
    success: true,
    data: null
    }

    if ( ctx.method === 'GET' ) {
    if ( ctx.url === '/getString.json' ) {
    result.data = 'this is string data'
    } else if ( ctx.url === '/getNumber.json' ) {
    result.data = 123456
    } else {
    result.success = false
    }
    ctx.body = result
    next && next()
    } else if ( ctx.method === 'POST' ) {
    if ( ctx.url === '/postData.json' ) {
    result.data = 'ok'
    } else {
    result.success = false
    }
    ctx.body = result
    next && next()
    } else {
    ctx.body = 'hello world'
    next && next()
    }
    }

    app.use(server)

    module.exports = app

    app.listen(3000, () => {
    console.log('[demo] test-unit is starting at port 3000')
    })

    启动服务后访问接口会看到以下数据

    http://localhost:3000/getString.json

    test-unit-result-01

    开始写测试用例

    demo/test-unit/test/index.test.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    const supertest = require('supertest')
    const chai = require('chai')
    const app = require('./../index')

    const expect = chai.expect
    const request = supertest( app.listen() )

    // 测试套件/组
    describe( '开始测试demo的GET请求', ( ) => {

    // 测试用例
    it('测试/getString.json请求', ( done ) => {
    request
    .get('/getString.json')
    .expect(200)
    .end(( err, res ) => {
    // 断言判断结果是否为object类型
    expect(res.body).to.be.an('object')
    expect(res.body.success).to.be.an('boolean')
    expect(res.body.data).to.be.an('string')
    done()
    })
    })
    })

    执行测试用例

    1
    2
    3
    4
    5
    ## node.js <= 7.5.x
    ./node_modules/.bin/mocha --harmony

    ## node.js = 7.6.0
    ./node_modules/.bin/mocha

    注意:

    1. 如果是全局安装了mocha,可以直接在当前项目目录下执行 mocha –harmony 命令
    2. 如果当前node.js版本低于7.6,由于7.5.x以下还直接不支持async/awiar就需要加上–harmony

    会自动读取执行命令 ./test 目录下的测用例文件 inde.test.js,并执行。测试结果如下
    test-unit-result-03

    用例详解

    服务入口加载

    如果要对一个服务的API接口,进行单元测试,要用supertest加载服务的入口文件

    1
    2
    const supertest = require('supertest')
    const request = supertest( app.listen() )

    测试套件、用例
    • describe()描述的是一个测试套件
    • 嵌套在describe()的it()是对接口进行自动化测试的测试用例
    • 一个describe()可以包含多个it()

      1
      2
      3
      4
      5
      describe( '开始测试demo的GET请求', ( ) => {
      it('测试/getString.json请求', () => {
      // TODO ...
      })
      })
    • supertest封装服务request,是用来请求接口

    • chai.expect使用来判断测试结果是否与预期一样
      • chai 断言有很多种方法,这里只是用了数据类型断言

    debug

    开发debug

    快速开始

    环境

    • node环境 8.x +
    • chrome 60+

    启动脚本

    调试demo

    https://github.com/ChenShenhai/koa2-note/blob/master/demo/start-quick/

    1
    node --inspect index.js
    指令框显示

    指令框就会出现以下字样

    1
    2
    Debugger listening on ws://127.0.0.1:9229/4c23c723-5197-4d23-9b90-d473f1164abe
    For help see https://nodejs.org/en/docs/inspector

    debug-result

    访问chrome浏览器调试server

    debug-result

    打开浏览器调试窗口会看到一个node.js 的小logo

    debug-result

    打开chrome浏览器的node调试窗口

    debug-result

    debug-result

    注意打开了node的调试窗口后,原来绿色的node按钮会变灰色,同时调试框会显示debug状态

    debug-result

    debug-result

    可以自定义打断点调试了

    debug-result

    项目框架搭建

    项目demo

    快速启动

    demo地址

    https://github.com/ChenShenhai/koa2-note/blob/master/demo/project/

    环境准备

    初始化数据库

    • 安装MySQL5.6以上版本
    • 创建数据库koa_demo
    1
    create database koa_demo;
    • 配置项目config.js

    https://github.com/ChenShenhai/koa2-note/blob/master/demo/project/

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const config = {
    // 启动端口
    port: 3001,

    // 数据库配置
    database: {
    DATABASE: 'koa_demo',
    USERNAME: 'root',
    PASSWORD: 'abc123',
    PORT: '3306',
    HOST: 'localhost'
    }
    }

    module.exports = config

    启动脚本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    ## 安装淘宝镜像cnpm
    npm install -g cnpm --registry=https://registry.npm.taobao.org

    ## 安装依赖
    cnpm install

    ## 数据建库初始化
    npm run init_sql

    ## 编译react.js源码
    npm run start_static

    ## 启动服务
    npm run start_server

    访问项目demo

    http://localhost:3001/admin

    project-result

    框架设计

    实现概要

    • koa2 搭建服务
    • MySQL作为数据库
      • mysql 5.7 版本
      • 储存普通数据
      • 存储session登录态数据
    • 渲染
      • 服务端渲染:ejs作为服务端渲染的模板引擎
      • 前端渲染:用webpack4环境编译react.js动态渲染页面,使用ant-design框架

    文件目录设计

    demo源码

    https://github.com/ChenShenhai/koa2-note/blob/master/demo/project/

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    ├── init ## 数据库初始化目录
    │   ├── index.js ## 初始化入口文件
    │   ├── sql/ ## sql脚本文件目录
    │   └── util/ ## 工具操作目录
    ├── package.json
    ├── config.js ## 配置文件
    ├── server ## 后端代码目录
    │   ├── app.js ## 后端服务入口文件
    │   ├── codes/ ## 提示语代码目录
    │   ├── controllers/ ## 操作层目录
    │   ├── models/ ## 数据模型model层目录
    │   ├── routers/ ## 路由目录
    │   ├── services/ ## 业务层目录
    │   ├── utils/ ## 工具类目录
    │   └── views/ ## 模板目录
    └── static ## 前端静态代码目录
    ├── build/ ## webpack编译配置目录
    ├── output/ ## 编译后前端代码目录&静态资源前端访问目录
    └── src/ ## 前端源代码目录

    入口文件预览

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    const path = require('path')
    const Koa = require('koa')
    const convert = require('koa-convert')
    const views = require('koa-views')
    const koaStatic = require('koa-static')
    const bodyParser = require('koa-bodyparser')
    const koaLogger = require('koa-logger')
    const session = require('koa-session-minimal')
    const MysqlStore = require('koa-mysql-session')

    const config = require('./../config')
    const routers = require('./routers/index')

    const app = new Koa()

    // session存储配置
    const sessionMysqlConfig= {
    user: config.database.USERNAME,
    password: config.database.PASSWORD,
    database: config.database.DATABASE,
    host: config.database.HOST,
    }

    // 配置session中间件
    app.use(session({
    key: 'USER_SID',
    store: new MysqlStore(sessionMysqlConfig)
    }))

    // 配置控制台日志中间件
    app.use(convert(koaLogger()))

    // 配置ctx.body解析中间件
    app.use(bodyParser())

    // 配置静态资源加载中间件
    app.use(convert(koaStatic(
    path.join(__dirname , './../static')
    )))

    // 配置服务端模板渲染引擎中间件
    app.use(views(path.join(__dirname, './views'), {
    extension: 'ejs'
    }))

    // 初始化路由中间件
    app.use(routers.routes()).use(routers.allowedMethods())

    // 监听启动端口
    app.listen( config.port )
    console.log(`the server is start at port ${config.port}`)

    ```## 分层设计

    ### 后端代码目录
    ```sh
    └── server
       ├── controllers ## 操作层 执行服务端模板渲染,json接口返回数据,页面跳转
       │   ├── admin.js
       │   ├── index.js
       │   ├── user-info.js
       │   └── work.js
       ├── models ## 数据模型层 执行数据操作
       │   └── user-Info.js
       ├── routers ## 路由层 控制路由
       │   ├── admin.js
       │   ├── api.js
       │   ├── error.js
       │   ├── home.js
       │   ├── index.js
       │   └── work.js
       ├── services ## 业务层 实现数据层model到操作层controller的耦合封装
       │   └── user-info.js
       └── views ## 服务端模板代码
       ├── admin.ejs
       ├── error.ejs
       ├── index.ejs
       └── work.ejs

    数据库设计

    初始化数据库脚本

    脚本目录

    ./demos/project/init/sql/

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    CREATE TABLE   IF NOT EXISTS  `user_info` (
    `id` int(11) NOT NULL AUTO_INCREMENT, ## 用户ID
    `email` varchar(255) DEFAULT NULL, ## 邮箱地址
    `password` varchar(255) DEFAULT NULL, ## 密码
    `name` varchar(255) DEFAULT NULL, ## 用户名
    `nick` varchar(255) DEFAULT NULL, ## 用户昵称
    `detail_info` longtext DEFAULT NULL, ## 详细信息
    `create_time` varchar(20) DEFAULT NULL, ## 创建时间
    `modified_time` varchar(20) DEFAULT NULL, ## 修改时间
    `level` int(11) DEFAULT NULL, ## 权限级别
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

    ## 插入默认信息
    INSERT INTO `user_info` set name='admin001', email='admin001@example.com', password='123456';

    路由设计

    使用koa-router中间件

    路由目录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ## ...
    └── server ## 后端代码目录
    └── routers
       ├── admin.js ## /admin/* 子路由
          ├── api.js ## resetful /api/* 子路由
          ├── error.js ## /error/* 子路由
          ├── home.js ## 主页子路由
          ├── index.js ## 子路由汇总文件
          └── work.js ## /work/* 子路由
    ## ...

    子路由配置

    resetful API 子路由

    例如api子路由/user/getUserInfo.json,整合到主路由,加载到中间件后,请求的路径会是 http://www.example.com/api/user/getUserInfo.json

    ./demos/project/server/routers/api.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    * restful api 子路由
    */

    const router = require('koa-router')()
    const userInfoController = require('./../controllers/user-info')

    const routers = router
    .get('/user/getUserInfo.json', userInfoController.getLoginUserInfo)
    .post('/user/signIn.json', userInfoController.signIn)
    .post('/user/signUp.json', userInfoController.signUp)

    module.exports = routers

    子路由汇总

    ./demos/project/server/routers/index.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /**
    * 整合所有子路由
    */

    const router = require('koa-router')()

    const home = require('./home')
    const api = require('./api')
    const admin = require('./admin')
    const work = require('./work')
    const error = require('./error')

    router.use('/', home.routes(), home.allowedMethods())
    router.use('/api', api.routes(), api.allowedMethods())
    router.use('/admin', admin.routes(), admin.allowedMethods())
    router.use('/work', work.routes(), work.allowedMethods())
    router.use('/error', error.routes(), error.allowedMethods())
    module.exports = router

    app.js加载路由中间件

    ./demos/project/server/app.js

    1
    2
    3
    4
    const routers = require('./routers/index')

    // 初始化路由中间件
    app.use(routers.routes()).use(routers.allowedMethods())

    webpack4 环境搭建

    前言

    由于demos/project 前端渲染是通过react.js渲染的,这就需要webpack4 对react.js及其相关JSX,ES6/7代码进行编译和混淆压缩。

    webpack4

    安装和文档

    可访问网https://webpack.js.org/

    配置webpack4编译react.js + less + sass + antd 环境

    文件目录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    └── static ## 项目静态文件目录
    ├── build
    │   ├── webpack.base.config.js ## 基础编译脚本
    │   ├── webpack.dev.config.js ## 开发环境编译脚本
    │   └── webpack.prod.config.js ## 生产环境编译脚本
    ├── output ## 编译后输出目录
    │   ├── asset
    │   ├── dist
    │   └── upload
    └── src ## 待编译的ES6/7、JSX源代码
    ├── api
    ├── apps
    ├── components
    ├── pages
    ├── texts
    └── utils

    webpack4 编译基础配置

    babel@7 配置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    const babelConfig = {
    presets: [
    '@babel/env',
    // [
    // '@babel/env',
    // {
    // targets: {
    // edge: '17',
    // firefox: '60',
    // chrome: '67',
    // safari: '11.1'
    // },
    // useBuiltIns: 'usage'
    // }
    // ],
    '@babel/preset-react'
    ],
    'plugins': [
    [
    'import',
    { 'libraryName': 'antd', 'libraryDirectory': 'lib' },
    'ant'
    ],
    [
    'import',
    { 'libraryName': 'antd-mobile', 'libraryDirectory': 'lib' },
    'antd-mobile'
    ],
    '@babel/plugin-proposal-class-properties'
    ]
    };

    module.exports = babelConfig;
    webpack.base.config.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    const path = require('path');
    const webpack = require('webpack');
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    const babelConfig = require('./babel.config');

    // const prodMode = process.env.NODE_ENV === 'production';

    const srcResolve = function (file) {
    return path.join(__dirname, '..', 'src', file);
    };

    const distResolve = function (file) {
    return path.join(__dirname, '..', 'output', 'dist', file);
    };

    module.exports = {
    entry: {
    'index': srcResolve('js/index'),
    'admin' : srcResolve('pages/admin.js'),
    'work' : srcResolve('pages/work.js'),
    'index' : srcResolve('pages/index.js'),
    'error' : srcResolve('pages/error.js'),
    },
    output: {
    path: distResolve(''),
    filename: 'vendorjs/[name].js'
    },
    module: {
    rules: [
    {
    test: /\.(js|jsx)$/,
    use: {
    loader: 'babel-loader',
    options: babelConfig
    }
    },
    {
    test: /\.(css|less)$/,
    use: [
    // devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
    // 'style-loader',
    MiniCssExtractPlugin.loader,
    'css-loader',
    // 'postcss-loader',
    {
    loader: 'postcss-loader',
    options: {
    plugins: () => {
    return [];
    }
    }
    },
    'less-loader'
    ]
    }
    ]
    },
    plugins: [
    new MiniCssExtractPlugin({
    filename: 'css/[name].css'
    })
    ],
    optimization: {
    splitChunks: {
    cacheGroups: {
    commons: {
    test: /[\\/]node_modules[\\/]/,
    name: 'vendor',
    chunks: 'all'
    }
    }
    }
    }
    };

    配置开发&生产环境webpack4 编译设置

    为了方便编译基本配置代码统一管理,开发环境(wepack.dev.config.js)和生产环境(webpack.prod.config.js)的编译配置都是继承了基本配置(wepack.base.config.js)的代码

    开发环境配置 wepack.dev.config.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var merge = require('webpack-merge')
    var webpack = require('webpack')
    var baseWebpackConfig = require('./webpack.base.config');

    module.exports = merge(baseWebpackConfig, {

    devtool: 'source-map',
    plugins: [

    new webpack.DefinePlugin({
    'process.env': {
    NODE_ENV: JSON.stringify('development')
    }
    }),
    ]
    })
    编译环境配置 wepack.prod.config.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    process.env.NODE_ENV = 'production';

    const merge = require('webpack-merge');
    const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
    const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
    const config = require('./webpack.base.config');

    module.exports = merge(config, {
    mode: 'production',
    // plugins: [
    // new UglifyJsPlugin()
    // ]
    optimization: {
    minimizer: [
    new UglifyJsPlugin({
    cache: true,
    parallel: true
    }),
    new OptimizeCSSAssetsPlugin({})
    ]
    }
    });

    使用react.js

    react.js简介

    react.js 是作为前端渲染的js库(注意:不是框架)。react.js用JSX开发来描述DOM结构,通过编译成virtual dom的在浏览器中进行view渲染和动态交互处理。更多了解可查阅GitHubhttps://facebook.github.io/react/

    编译使用

    由于react.js开发过程用JSX编程,无法直接在浏览器中运行,需要编译成浏览器可识别运行的virtual dom。从JSX开发到运行,需要有一个编译的过程。目前最常用的方案是用webpack + babel进行编译打包。

    前端待编译源文件目录

    demos/project/static/

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    .
    ├── build ## 编译的webpack脚本
    │   ├── webpack.base.config.js
    │   ├── webpack.dev.config.js
    │   └── webpack.prod.config.js
    ├── output ## 输出文件
    │   ├── asset
    │   ├── dist ## react.js编译后的文件目录
    │   └── ...
    └── src
    ├── apps ## 页面react.js应用
    │   ├── admin.jsx
    │   ├── error.jsx
    │   ├── index.jsx
    │   └── work.jsx
    ├── components ## jsx 模块、组件
    │   ├── footer-common.jsx
    │   ├── form-group.jsx
    │   ├── header-nav.jsx
    │   ├── sign-in-form.jsx
    │   └── sign-up-form.jsx
    └── pages ## react.js 执行render文件目录
       ├── admin.js
       ├── error.js
       ├── index.js
       └── work.js
    ...

    react.js页面应用文件

    static/src/apps/index.jsx 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    import React from 'react'
    import ReactDOM from 'react-dom'
    import { Layout, Menu, Breadcrumb } from 'antd'
    import HeadeNav from './../components/header-nav.jsx'
    import FooterCommon from './../components/footer-common.jsx'
    import 'antd/lib/layout/style/css'

    const { Header, Content, Footer } = Layout

    class App extends React.Component {
    render() {
    return (
    <Layout className="layout">
    <HeadeNav/>
    <Content style={{ padding: '0 50px' }}>
    <Breadcrumb style={{ margin: '12px 0' }}>
    <Breadcrumb.Item>Home</Breadcrumb.Item>
    </Breadcrumb>
    <div style={{ background: '#fff', padding: 24, minHeight: 280 }}>
    <p>index</p>
    </div>
    </Content>
    <FooterCommon />
    </Layout>
    )
    }
    }
    export default App

    react.js执行render渲染

    static/src/pages/index.js 文件

    1
    2
    3
    4
    5
    6
    import React from 'react'
    import ReactDOM from 'react-dom'
    import App from './../apps/index.jsx'

    ReactDOM.render( <App />,
    document.getElementById("app"))

    静态页面引用react.js编译后文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!DOCTYPE html>
    <html>
    <head>
    <title><%= title %></title>
    <link rel="stylesheet" href="/output/dist/css/index.css">
    </head>
    <body>
    <div id="app"></div>
    <script src="/output/dist/js/vendor.js"></script>
    <script src="/output/dist/js/index.js"></script>
    </body>
    </html>

    页面渲染效果

    project-result-01.png

    登录注册功能实现

    用户模型dao操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    /**
    * 数据库创建用户
    * @param {object} model 用户数据模型
    * @return {object} mysql执行结果
    */
    async create ( model ) {
    let result = await dbUtils.insertData( 'user_info', model )
    return result
    },

    /**
    * 查找一个存在用户的数据
    * @param {obejct} options 查找条件参数
    * @return {object|null} 查找结果
    */
    async getExistOne(options ) {
    let _sql = `
    SELECT * from user_info
    where email="${options.email}" or name="${options.name}"
    limit 1`
    let result = await dbUtils.query( _sql )
    if ( Array.isArray(result) && result.length > 0 ) {
    result = result[0]
    } else {
    result = null
    }
    return result
    },

    /**
    * 根据用户名和密码查找用户
    * @param {object} options 用户名密码对象
    * @return {object|null} 查找结果
    */
    async getOneByUserNameAndPassword( options ) {
    let _sql = `
    SELECT * from user_info
    where password="${options.password}" and name="${options.name}"
    limit 1`
    let result = await dbUtils.query( _sql )
    if ( Array.isArray(result) && result.length > 0 ) {
    result = result[0]
    } else {
    result = null
    }
    return result
    },

    /**
    * 根据用户名查找用户信息
    * @param {string} userName 用户账号名称
    * @return {object|null} 查找结果
    */
    async getUserInfoByUserName( userName ) {

    let result = await dbUtils.select(
    'user_info',
    ['id', 'email', 'name', 'detail_info', 'create_time', 'modified_time', 'modified_time' ])
    if ( Array.isArray(result) && result.length > 0 ) {
    result = result[0]
    } else {
    result = null
    }
    return result
    },

    业务层操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    /**
    * 创建用户
    * @param {object} user 用户信息
    * @return {object} 创建结果
    */
    async create( user ) {
    let result = await userModel.create(user)
    return result
    },

    /**
    * 查找存在用户信息
    * @param {object} formData 查找的表单数据
    * @return {object|null} 查找结果
    */
    async getExistOne( formData ) {
    let resultData = await userModel.getExistOne({
    'email': formData.email,
    'name': formData.userName
    })
    return resultData
    },

    /**
    * 登录业务操作
    * @param {object} formData 登录表单信息
    * @return {object} 登录业务操作结果
    */
    async signIn( formData ) {
    let resultData = await userModel.getOneByUserNameAndPassword({
    'password': formData.password,
    'name': formData.userName})
    return resultData
    },


    /**
    * 根据用户名查找用户业务操作
    * @param {string} userName 用户名
    * @return {object|null} 查找结果
    */
    async getUserInfoByUserName( userName ) {

    let resultData = await userModel.getUserInfoByUserName( userName ) || {}
    let userInfo = {
    // id: resultData.id,
    email: resultData.email,
    userName: resultData.name,
    detailInfo: resultData.detail_info,
    createTime: resultData.create_time
    }
    return userInfo
    },


    /**
    * 检验用户注册数据
    * @param {object} userInfo 用户注册数据
    * @return {object} 校验结果
    */
    validatorSignUp( userInfo ) {
    let result = {
    success: false,
    message: '',
    }

    if ( /[a-z0-9\_\-]{6,16}/.test(userInfo.userName) === false ) {
    result.message = userCode.ERROR_USER_NAME
    return result
    }
    if ( !validator.isEmail( userInfo.email ) ) {
    result.message = userCode.ERROR_EMAIL
    return result
    }
    if ( !/[\w+]{6,16}/.test( userInfo.password ) ) {
    result.message = userCode.ERROR_PASSWORD
    return result
    }
    if ( userInfo.password !== userInfo.confirmPassword ) {
    result.message = userCode.ERROR_PASSWORD_CONFORM
    return result
    }

    result.success = true

    return result
    }

    controller 操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    /**
    * 登录操作
    * @param {obejct} ctx 上下文对象
    */
    async signIn( ctx ) {
    let formData = ctx.request.body
    let result = {
    success: false,
    message: '',
    data: null,
    code: ''
    }

    let userResult = await userInfoService.signIn( formData )

    if ( userResult ) {
    if ( formData.userName === userResult.name ) {
    result.success = true
    } else {
    result.message = userCode.FAIL_USER_NAME_OR_PASSWORD_ERROR
    result.code = 'FAIL_USER_NAME_OR_PASSWORD_ERROR'
    }
    } else {
    result.code = 'FAIL_USER_NO_EXIST',
    result.message = userCode.FAIL_USER_NO_EXIST
    }

    if ( formData.source === 'form' && result.success === true ) {
    let session = ctx.session
    session.isLogin = true
    session.userName = userResult.name
    session.userId = userResult.id

    ctx.redirect('/work')
    } else {
    ctx.body = result
    }
    },

    /**
    * 注册操作
    * @param {obejct} ctx 上下文对象
    */
    async signUp( ctx ) {
    let formData = ctx.request.body
    let result = {
    success: false,
    message: '',
    data: null
    }

    let validateResult = userInfoService.validatorSignUp( formData )

    if ( validateResult.success === false ) {
    result = validateResult
    ctx.body = result
    return
    }

    let existOne = await userInfoService.getExistOne(formData)
    console.log( existOne )

    if ( existOne ) {
    if ( existOne .name === formData.userName ) {
    result.message = userCode.FAIL_USER_NAME_IS_EXIST
    ctx.body = result
    return
    }
    if ( existOne .email === formData.email ) {
    result.message = userCode.FAIL_EMAIL_IS_EXIST
    ctx.body = result
    return
    }
    }


    let userResult = await userInfoService.create({
    email: formData.email,
    password: formData.password,
    name: formData.userName,
    create_time: new Date().getTime(),
    level: 1,
    })

    console.log( userResult )

    if ( userResult && userResult.insertId * 1 > 0) {
    result.success = true
    } else {
    result.message = userCode.ERROR_SYS
    }

    ctx.body = result
    },

    api路由操作

    1
    2
    3
    4
    5
    6
    7
    const router = require('koa-router')()
    const userInfoController = require('./../controllers/user-info')

    const routers = router
    .get('/user/getUserInfo.json', userInfoController.getLoginUserInfo)
    .post('/user/signIn.json', userInfoController.signIn)
    .post('/user/signUp.json', userInfoController.signUp)

    前端用react.js实现效果

    登录模式
    project-result-01
    注册模式
    project-result-01

    session登录态判断处理

    使用session中间件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // code ...
    const session = require('koa-session-minimal')
    const MysqlStore = require('koa-mysql-session')

    const config = require('./../config')

    // code ...

    const app = new Koa()

    // session存储配置
    const sessionMysqlConfig= {
    user: config.database.USERNAME,
    password: config.database.PASSWORD,
    database: config.database.DATABASE,
    host: config.database.HOST,
    }

    // 配置session中间件
    app.use(session({
    key: 'USER_SID',
    store: new MysqlStore(sessionMysqlConfig)
    }))
    // code ...

    登录成功后设置session到MySQL和设置sessionId到cookie

    1
    2
    3
    4
    let session = ctx.session
    session.isLogin = true
    session.userName = userResult.name
    session.userId = userResult.id

    需要判断登录态页面进行session判断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    async indexPage ( ctx ) {
    // 判断是否有session
    if ( ctx.session && ctx.session.isLogin && ctx.session.userName ) {
    const title = 'work页面'
    await ctx.render('work', {
    title,
    })
    } else {
    // 没有登录态则跳转到错误页面
    ctx.redirect('/error')
    }
    },

    其他进阶

    前言

    Node 9最激动人心的是提供了在flag模式下使用ECMAScript Modules,虽然现在还是Stability: 1 - Experimental阶段,但是可以让Noder抛掉babel等工具的束缚,直接在Node环境下愉快地去玩耍import/export

    如果觉得文字太多,看不下去,可以直接去玩玩demo,地址是https://github.com/chenshenhai/node-modules-demo

    Node 9下import/export使用简单须知

    • Node 环境必须在 9.0以上
    • 不加loader时候,使用import/export的文件后缀名必须为*.mjs(下面会讲利用Loader Hooks兼容*.js后缀文件)
    • 启动必须加上flag --experimental-modules
    • 文件的importexport必须严格按照ECMAScript Modules语法
    • ECMAScript Modulesrequire()的cache机制不一样

    使用简述

    Node 9.x官方文档 https://nodejs.org/dist/latest-v9.x/docs/api/esm.html

    与require()区别

    能力 描述 require() import
    NODE_PATH 从NODE_PATH加载依赖模块 Y N
    cache 缓存机制 可以通过require的API操作缓存 自己独立的缓存机制,目前不可访问
    path 引用路径 文件路径 URL格式文件路径,例如import A from './a?v=2017'
    extensions 扩展名机制 require.extensions Loader Hooks
    natives 原生模块引用 直接支持 直接支持
    npm npm模块引用 直接支持 需要Loader Hooks
    file 文件(引用) *.js,*.json等直接支持 默认只能是*.mjs,通过Loader Hooks可以自定义配置规则支持*.js,*.json等Node原有支持文件

    Loader Hooks模式使用

    由于历史原因,在ES6的Modules还没确定之前,JavaScript的模块化处理方案都是八仙过海,各显神通,例如前端的AMD、CMD模块方案,Node的CommonJS方案也在这个“乱世”诞生。
    当到了ES6规范确定后,Node的CommonJS方案已经是JavaScript中比较成熟的模块化方案,但ES6怎么说都是正统的规范,“法理”上是需要兼容的,所以*.mjs这个针对ECMAScript Modules规范的Node文件方案在一片讨论声中应运而生。

    当然如果import/export只能对*.mjs文件起作用,意味着Node原生模块和npm所有第三方模块都不能。所以这时候Node 9就提供了 Loader Hooks,开发者可自定义配置Resolve Hook规则去利用import/export加载使用Node原生模块,*.js文件,npm模块,C/C++的Node编译模块等Node生态圈的模块。

    Loader Hooks 使用步骤

    • 自定义loader规则
    • 启动的flag要加载loader规则文件
      • 例如:node --experimental-modules --loader ./custom-loader.mjs ./index.js

    Koa2 直接使用import/export

    看看demo4,https://github.com/chenshenhai/node-modules-demo/tree/master/demo4

    • 文件目录
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    ├── esm
    │   ├── README.md
    │   ├── custom-loader.mjs
    │   ├── index.js
    │   ├── lib
    │   │   ├── data.json
    │   │   ├── path.js
    │   │   └── render.js
    │   ├── package.json
    │   └── view
    │   ├── index.html
    │   ├── index.html
    │   └── todo.html

    代码片段太多,不一一贴出来,只显示主文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import Koa from 'koa';
    import { render } from './lib/render.js';
    import data from './lib/data.json';

    let app = new Koa();
    app.use((ctx, next) => {
    let view = ctx.url.substr(1);
    let content;
    if ( view === '' ) {
    content = render('index');
    } else if ( view === 'data' ) {
    content = data;
    } else {
    content = render(view);
    }
    ctx.body = content;
    })
    app.listen(3000, ()=>{
    console.log('the modules test server is starting');
    })

    自定义loader规则优化

    从上面官方提供的自定义loader例子看出,只是对*.js文件做import/export做loader兼容,然而我们在实际开发中需要对npm模块,*.json文件也使用import/export

    loader规则优化解析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    import url from 'url';
    import path from 'path';
    import process from 'process';
    import fs from 'fs';

    // 从package.json中
    // 的dependencies、devDependencies获取项目所需npm模块信息
    const ROOT_PATH = process.cwd();
    const PKG_JSON_PATH = path.join( ROOT_PATH, 'package.json' );
    const PKG_JSON_STR = fs.readFileSync(PKG_JSON_PATH, 'binary');
    const PKG_JSON = JSON.parse(PKG_JSON_STR);
    // 项目所需npm模块信息
    const allDependencies = {
    ...PKG_JSON.dependencies || {},
    ...PKG_JSON.devDependencies || {}
    }

    //Node原生模信息
    const builtins = new Set(
    Object.keys(process.binding('natives')).filter((str) =>
    /^(?!(?:internal|node|v8)\/)/.test(str))
    );

    // 文件引用兼容后缀名
    const JS_EXTENSIONS = new Set(['.js', '.mjs']);
    const JSON_EXTENSIONS = new Set(['.json']);

    export function resolve(specifier, parentModuleURL, defaultResolve) {
    // 判断是否为Node原生模块
    if (builtins.has(specifier)) {
    return {
    url: specifier,
    format: 'builtin'
    };
    }

    // 判断是否为npm模块
    if ( allDependencies && typeof allDependencies[specifier] === 'string' ) {
    return defaultResolve(specifier, parentModuleURL);
    }

    // 如果是文件引用,判断是否路径格式正确
    if (/^\.{0,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) {
    throw new Error(
    `imports must begin with '/', './', or '../'; '${specifier}' does not`);
    }

    // 判断是否为*.js、*.mjs、*.json文件
    const resolved = new url.URL(specifier, parentModuleURL);
    const ext = path.extname(resolved.pathname);
    if (!JS_EXTENSIONS.has(ext) && !JSON_EXTENSIONS.has(ext)) {
    throw new Error(
    `Cannot load file with non-JavaScript file extension ${ext}.`);
    }

    // 如果是*.js、*.mjs文件
    if (JS_EXTENSIONS.has(ext)) {
    return {
    url: resolved.href,
    format: 'esm'
    };
    }

    // 如果是*.json文件
    if (JSON_EXTENSIONS.has(ext)) {
    return {
    url: resolved.href,
    format: 'json'
    };
    }

    }

    规则总结

    在自定义loader中,export的resolve规则最核心的代码是

    1
    2
    3
    4
    return {
    url: '',
    format: ''
    }

    • url 是模块名称或者文件URL格式路径
    • format 是模块格式有esm, cjs, json, builtin, addon这四种模块/文件格式.

    注意:
    目前Node对import/export的支持现在还是Stability: 1 - Experimental阶段,后续的发展还有很多不确定因素,自己练手玩玩还可以,但是在还没去flag使用之前,尽量不要在生产环境中使用。Node 9.x 更详细import/export的使用,可参考 https://github.com/ChenShenhai/blog/issues/24