《前端之路》--- 重温 Koa2

1,475 阅读2分钟

[TOC]

一、简单介绍

1.1、快速开始 (这里省略了安装的过程)

const Koa = require('koa')
const app = new Koa()

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

app.listen(3000)

1.2、源码简单解析

源码文件主要包含了 application.js 、context.js 、request.js 、response.js

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

1.3、中间件的简单开发

这里主要介绍如何使用 async/await 在 koa2 中进行中间件的开发

middleware 在 koa2 中如何使用

const Koa = require('koa')
const logger = require('./middleware/logger-async')

const app = new Koa()

app.use(logger())

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

app.listen(3000)

如何编写一个简单的 middleware 中间件

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

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

// 对,就是这样,so easy

二、 路由

原生 JS 实现 koa 的 router

经过思考🤔, 实现路由的基本原理: 通过请求进来的 url 匹配到对应的页面文件,然后通过 fs 读取对应文件的内容,并返回给 ctx.body, 那下面我们就按照这个思路来实现一下路由。


function render(page) {
    return new Promise((resolve, reject) => {
        let viewUrl = `./view/${page}`;
        fs.readFile(viewUrl, 'utf8', (err, data) => {
            if (err) {
                reject(err);
            } else {
                resolve(data);
            }
        });
    });
}

async function route(url) {
    let view = '404.html';
    switch (url) {
        case '/':
            view = 'index.html';
            break;
        case '/index':
            view = 'index.html';
            break;
        case '/login':
            view = 'login.html';
            break;
        case '/404':
            view = '404.html';
            break;
        default:
            break;
    }
    let html = render(view);
    return html;
}

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

// 当然还有 koa-router 中间件 

三、请求数据

3.1、 GET 请求数据获取

GET 请求数据获取的方法有2中,如下

app.use(async ctx => {
    let url = ctx.request.url;
    let html = await route(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 = {
        ctx,
        request,
        url,
        req_query,
        req_queryString,
        ctx_query,
        ctx_queryString,
        html
    };
});

返回结果

url: "/index?page=1"
req_query: {page: "1"}
req_queryString: "page=1"
ctx_query: {page: "1"}
ctx_queryString: "page=1"

疑惑🤔的 点: 从上线文中获取的request对象和直接通过上线文获取的参数 有什么区别? 为什么要这么设计?

  • 从 Koa2 的框架设计层面 app.js 中封装了 ctx、request、response
  • 从 Koa2 的框架设计层面 ctx.js 中封装了 request、response 方法
  • 从上下文中获取和从 ctx.request 获取的参数是一样的,因为底层方法是一致的
  • 直接从上下文中获取的方式简单、快捷
  • 从上下文中的 request 对象中获取的话,会更加的明确该属性来源,不容易混淆。

注意:ctx.request是context经过封装的请求对象,ctx.req是context提供的node.js原生HTTP请求对象, 和这里的 ctx.query 和 ctx.request.query 是没有关系的。

3.2、 POST 请求数据获取

POST 请求的话,需要我们在页面mock一个表单,这样的话,可以更好的查看我们请求的数据。

        <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>
    if (ctx.method === 'GET') {
        ctx.body = html;
    } else if (ctx.url === '/' && ctx.method === 'POST') {
        ctx.body = html + `<script> alert('提交成功!') </script>`;
    } else {
        ctx.body = '<h1>404!!! o(╯□╰)o</h1>';
    }

3.3、 koa-bodyparser中间件

实际上是封装了一层 post 的数据处理方法,然后将其赋值给了 ctx.request 的 body 属性

const bodyParser = require('koa-bodyparser')

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

// 处理 method 为 POST 的方法
let postData = ctx.request.body
ctx.body = postData

四、 静态资源加载

4.1、静态资源加载源码解析

	// 核心代码
│   ├── content.js # 读取请求内容
│   ├── dir.js # 读取目录内容
│   ├── file.js # 读取文件内容
│   ├── mimes.js # 文件类型列表
│   └── walk.js # 遍历目录内容
└── index.js # 启动入口文件

4.1.1、index.js 入口文件(对于文本类型和图片类型返回请求数据的方式是不一样的)

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

4.1.2、content.js 为读取当前请求内容 (判断当前文件请求路径是是否存在且判断是 文件夹还是文件, 如果是文件夹则读取文件内容)

// 核心代码
    //判断访问地址是文件夹还是文件
    let stat = fs.statSync( reqPath )

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

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

4.1.3、dir.js 为读取目录内容

	// 核心部分代码
	// 遍历读取当前目录下的文件、子目录
	let contentList = walk( reqPath )

4.1.4、 file.js 读取文件内容

// 核心代码,读取对应文件的内容(此处读取出来的文件内行)
function file ( filePath ) {
	let content = fs.readFileSync(filePath[, options])
	return content
}

// 这里需要注释一下 fs.readFileSync(filePath[, options]) 中的 options 分别有 encoding 和 flag 二种选项,其中如果指定了 encoding 选项,则此函数返回字符串,否则返回 buffer。 就是说默认为 buffer

4.1.5、 mimes.js 文件类型列表

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'
}
// 其中除了我们常见的 text/xxx 的文本类型、还有 image/xxx 图片类型和等等等

4.1.6、 walk.js 文件类型列表

// 核心代码 通过遍历,得到当前文件夹内的文件夹名称、和最后的文件名称
let result = dirList.concat( fileList );
// 疑惑的点: 为什么需要把文件名称也加上呢? 大家也可以作为一个思考

五、 Koa2 使用 cookie/session

5.1、koa2 使用 cookie

简单粗暴的直接上代码吧, 里面有一些需要注意的问题点,都在注释点中了。关键点就在与 koa 本身提供了 cookie 的 set 和 get 方法,可以非常简单的获取到对应想要的,但是里面我们常见的一些设置的参数,简单看一眼,其实就非常不简单了,maxAge、expires、httpOnly、overwrite 等等,这些都是我们在使用 cookie 的时候需要注意的,安全问题,http 请求问题。每一点都值得仔细来讲讲。

app.use(async ctx => {
    if (ctx.url === '/index') {
        ctx.cookies.set('cid', 'hello world', {
            domain: '127.0.0.1',
            // 写cookie所在的域名, 需要注意的是如果访问的域名和这里的 domain 不一致的化,是无法成功写入的
            path: '/index', // 写cookie所在的路径
            maxAge: 10 * 60 * 1000, // cookie有效时长
            expires: new Date('2017-02-15'), // cookie失效时间
            httpOnly: false, // 是否只用于http请求中获取
            overwrite: false // 是否允许重写
        });
        ctx.body = 'cookies is ok';
    } else {
        ctx.body = 'hello koa2';
    }
});

5.2、koa2 使用 session

这里需要注意下,koa 本身没有提供 session 的方法,这里的例子是通过中间件来实现一些你需要的能力。这里的两种实现 session 能力的方案。这两个方案的区别就在于 存储信息的大小。

5.2.1、 通过 koa-session 直接将信息存储在 内存中

使用 koa-session 中间件的核心在于需要对于给出的 对应 config 配置的理解。

const Koa = require('koa');                               // 导入Koa
const Koa_Session = require('koa-session');   // 导入koa-session     
// 配置
const session_signed_key = ["some secret hurr"];  // 这个是配合signed属性的签名key
const session_config = {
    key: 'koa:sess', /**  cookie的key。 (默认是 koa:sess) */
    maxAge: 4000,   /**  session 过期时间,以毫秒ms为单位计算 。*/
    autoCommit: true, /** 自动提交到响应头。(默认是 true) */
    overwrite: true, /** 是否允许重写 。(默认是 true) */
    httpOnly: true, /** 是否设置HttpOnly,如果在Cookie中设置了"HttpOnly"属性,那么通过程序(JS脚本、Applet等)将无法读取到Cookie信息,这样能有效的防止XSS攻击。  (默认 true) */
    signed: true, /** 是否签名。(默认是 true) */
    rolling: true, /** 是否每次响应时刷新Session的有效期。(默认是 false) */
    renew: false, /** 是否在Session快过期时刷新Session的有效期。(默认是 false) */
};

// 然后通过 ctx.session.logged 来判断当前用户是否登陆成功、是否在有效期内等等
5.2.2、 通过 koa-mysql-session 和 koa-session-minimal 将信息存储在 mysql 中
// session 中间件
app.use(
    session({
        key: 'SESSION_ID',
        store: store,
        cookie: cookie
    })
);

// 数据库配置
let store = new MysqlSession({
    user: 'root',
    password: '123456',
    database: 'hellothinkjs',
    host: '127.0.0.1'
});

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

六、 koa2加载模板引擎

6.1、 koa2 加载模板引擎 (ejs)

这里直接展示使用的 demo

app.use(
    views(path.join(__dirname, './ejs'), {
        extension: 'ejs'
    })
);

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

另外,我们附上 ejs 官方文档

七、 koa2 中简单使用 mysql 数据库

这里写了一个简单的 demo 大致的介绍了下,koa 中 mysql 的使用方式

// 连接数据库
const connection = mysql.createConnection({
    host: '127.0.0.1', // 数据库地址
    user: 'root', // 数据库用户
    password: '123456', // 数据库密码
    database: 'hellothinkjs' // 选中数据库
});

let title = 'hello 404';
let users = [];

connection.connect();
connection.query('SELECT * FROM think_user', async (error, results, fields) => {
    if (error) throw error;
    // connected !
    console.log(results);
    users = results;
    app.use(async ctx => {
        await ctx.render('404', {
            title,
            users
        });
    });
});
connection.end();

这里需要注意一点的是: 因为网上之前找的文档中,很多关于 mysql modules 的使用方式比较古老了,不太适合新版本的 mysql 的链接和使用。 mysql 最新使用文档

八、 koa2 中使用单元检测

这里的单元测试主要是正对 node 提供的API 服务来进行测试,测试框架选择: mocha(测试框架)、chai(断言库,用来判断是否满足预期结果)、supertest(用来模拟 API 请求)当然这三个库,每一个看上去都会有更多的特性,这里只是简单的介绍了一些基础自动化测试的demo

// api.js    api server  
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();
    }
};
// index.test.js test server
describe('开始测试智商税了', () => {
    // 测试用例
    it('测试你的智商是不是二百五', done => {
        request
            .post('/postData.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();
            });
    });
});

// 这里发现,我们在测试我们的借口返回数据的类型、数值、错误码等类型的时候会有非常大的帮助的。以后如果需要用起来的话,推荐使用之。

九、 node 服务端开发过程中的 开发 debug 方式

9.1、vscode 进行debug

vscode 自带 debug 能力,这里需要花费一定时间去理解的地方是 debug 启动程序的时候,需要配置一个 launch.json 文件,这里给一个对应的 demo。

{
    // 使用 IntelliSense 了解相关属性。
    // 悬停以查看现有属性的描述。
    // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "启动程序",
            "skipFiles": ["<node_internals>/**"],
            "program": "${workspaceFolder}/api.js"
        }
    ]
}

修改对应的 program 的 value 的文件为执行的入口文件即可。(这里推荐使用)

9.2、chrome 浏览器进行 debug

通过 node --inspect index.js 启动服务,则可以在 chrome 浏览器控制台看到对应的 node 的小图标,点击然后就有一个对应的小弹框进行 debug 啦。试了下,也推荐吧,哈哈,看个人喜好了。

十、总结

看完整个koa2 的api,以及使用了一些特性之后,我们不难发现,koa2 相对于 express 真的要简洁很多,其核心也在于洋葱图和中间件的机制,那么能够编写中间件和从茫茫大海中找到高可用的中间件非常重要,这二点是大家未来需要注意的地方。过完年了,自己身为湖北人,因为这次肺炎没能回到老家过年,那就让自己多学习一些知识吧~ 同时也希望这次的疫情可以快速的被消灭掉~奥利给!!!

GitHub 地址:(欢迎 star 、欢迎推荐 : ) 《前端之路》 - 重温Koa2