Анализ принципа промежуточного программного обеспечения Koa2 - чтение и запись

внешний интерфейс Promise koa NPM


читать оригинал


предисловие

Koa 2.xversion — самый популярный фреймворк NodeJS на данный момент,Koa 2.0Исходный код особенно оптимизирован, в отличие отExpressИнкапсулировано так много функций, что большинство функцийKoaкоманда разработчиков (сExpressявляется продуктом) и участники сообщества дляKoaЭто обеспечивается промежуточным программным обеспечением, реализованным функцией инкапсуляции NodeJS.Использование очень простое, то есть вводится промежуточное программное обеспечение, а вызовKoaизuseМетод используется в соответствующем положении, так что его можно использовать внутренне с помощьюctxЧтобы реализовать некоторые функции, мы обсудим принципы реализации общего промежуточного программного обеспечения и то, как нам следует разработатьKoaПромежуточное программное обеспечение используется самостоятельно и другими.


Введение в луковую модель Коа

На этот раз мы не будем слишком подробно анализировать принцип реализации луковой модели, а в основном проанализируем, как работает промежуточное ПО в зависимости от использования API и луковой модели.

// 洋葱模型特点
// 引入 Koa
const Koa = require("koa");

// 创建服务
const app = new Koa();

app.use(async (ctx, next) => {
    console.log(1);
    await next();
    console.log(2);
});

app.use(async (ctx, next) => {
    console.log(3);
    await next();
    console.log(4);
});

app.use(async (ctx, next) => {
    console.log(5);
    await next();
    console.log(6);
});

// 监听服务
app.listen(3000);

// 1
// 3
// 5
// 6
// 4
// 2

мы знаемKoaизuseМетод поддерживает асинхронность, поэтому для обеспечения нормального выполнения кода согласно порядку выполнения onion-модели необходимо вызватьnextКогда код ждет, дождитесь асинхронного завершения, прежде чем продолжить выполнение, так что мы находимся вKoaРекомендуется использоватьasync/awaitДа, представленное промежуточное ПО все вuseметод, с помощью которого мы можем анализировать каждыйKoaВсе промежуточное ПО возвращаетasyncфункциональный.


Моделирование промежуточного программного обеспечения koa-bodyparser

хочу проанализироватьkoa-bodyparserПринцип первой необходимости знать использование и функции,koa-bodyparserпромежуточное ПО - этоpostСтроки запроса запроса и отправки формы преобразуются в объекты и связываются вctx.request.bodyНам удобно получать значения из другого промежуточного программного обеспечения или интерфейсов, и его нужно установить заранее перед использованием.

npm install koa koa-bodyparser

Конкретное использование koa-bodyparser выглядит следующим образом:

// koa-bodyparser 的用法
const Koa = require("koa");
const bodyParser = require("koa-bodyparser");

const app = new Koa();

// 使用中间件
app.use(bodyParser());

app.use(async (ctx, next) => {
    if (ctx.path === "/" && ctx.method === "POST") {
        // 使用中间件后 ctx.request.body 属性自动加上了 post 请求的数据
        console.log(ctx.request.body);
    }
});

app.listen(3000);

Из использования мы видим, чтоkoa-bodyparserПромежуточное программное обеспечение на самом деле является функцией, мы помещаем его вuseреализуется, согласноKoaхарактеристики, делаем вывод, чтоkoa-bodyparserФункция должна вернуть намasyncНиже приведен код нашей реализации моделирования.

// 文件:my-koa-bodyparser.js
const querystring = require("querystring");

module.exports = function bodyParser() {
    return async (ctx, next) => {
        await new Promise((resolve, reject) => {
            // 存储数据的数组
            let dataArr = [];

            // 接收数据
            ctx.req.on("data", data => dataArr.push(data));

            // 整合数据并使用 Promise 成功
            ctx.req.on("end", () => {
                // 获取请求数据的类型 json 或表单
                let contentType = ctx.get("Content-Type");

                // 获取数据 Buffer 格式
                let data = Buffer.concat(dataArr).toString();

                if (contentType === "application/x-www-form-urlencoded") {
                    // 如果是表单提交,则将查询字符串转换成对象赋值给 ctx.request.body
                    ctx.request.body = querystring.parse(data);
                } else if (contentType === "applaction/json") {
                    // 如果是 json,则将字符串格式的对象转换成对象赋值给 ctx.request.body
                    ctx.request.body = JSON.parse(data);
                }

                // 执行成功的回调
                resolve();
            });
        });

        // 继续向下执行
        await next();
    };
};

В приведенном выше коде нам нужно обратить внимание на несколько моментов, а именноnextвызова и зачем получать данные через потоки, обрабатывать данные и вешать данные наctx.request.bodyСделать это в Обещании.

прежде всегоnextЗвоните, знаемKoaизnextВыполнение фактически выполняет функцию следующего промежуточного программного обеспечения, то есть следующегоuseсерединаasyncфункция, чтобы убедиться, что следующий асинхронный код выполняется, прежде чем продолжить выполнение текущего кода, поэтому нам нужно использоватьawaitОжидание, за которым следуют данные от получения до зависанияctx.request.bodyВсе выполняются в промисе, т.к. операция получения данных асинхронная, и весь процесс обработки данных нужно дождаться асинхронного завершения, а потом вешать данные вctx.request.bodyon, гарантируется, что мы находимся в следующемuseизasyncфункция может быть вctx.request.bodyчтобы получить данные, поэтому мы используемawaitПодождите, пока обещание будет выполнено, прежде чем выполнятьnext.


моделирование промежуточного программного обеспечения koa-better-body

koa-bodyparserОн все еще немного слаб в обработке отправки форм, потому что загрузка файлов не поддерживается, иkoa-better-bodyвосполняет этот недостаток, ноkoa-better-bodyдляKoa 1.xверсия промежуточного ПО,Koa 1.xПромежуточное ПО используетсяGeneratorфункция, нам нужно использоватьkoa-convertБудуkoa-better-bodyКонвертировано вKoa 2.xпромежуточное ПО.

npm install koa koa-better-body koa-convert path uuid

Конкретное использование koa-better-body выглядит следующим образом:

// koa-better-body 的用法
const Koa = require("koa");
const betterBody = require("koa-better-body");
const convert = require("koa-convert"); // 将  koa 1.0 中间转化成 koa 2.0 中间件
const path = require("path");
const fs = require("fs");
const uuid = require("uuid/v1"); // 生成随机串

const app = new Koa();

// 将 koa-better-body 中间件从 koa 1.0 转化成 koa 2.0,并使用中间件
app.use(convert(betterBody({
    uploadDir: path.resolve(__dirname, "upload")
})));

app.use(async (ctx, next) => {
    if (ctx.path === "/" && ctx.method === "POST") {
        // 使用中间件后 ctx.request.fields 属性自动加上了 post 请求的文件数据
        console.log(ctx.request.fields);

        // 将文件重命名
        let imgPath = ctx.request.fields.avatar[0].path;
        let newPath = path.resolve(__dirname, uuid());
        fs.rename(imgPath, newPath);
    }
});

app.listen(3000);

в приведенном выше кодеkoa-better-bodyОсновная функция состоит в том, чтобы сохранить файл, загруженный формой, в локальную указанную папку и повесить объект файлового потока вctx.request.fieldsсвойств, мы будем моделировать следующийkoa-better-bodyФункциональная реализация версии на основеKoa 2.xПромежуточное ПО для обработки загрузки файлов.

// 文件:my-koa-better-body.js
const fs = require("fs");
const uuid = require("uuid/v1");
const path = require("path");

// 给 Buffer 扩展 split 方法预备后面使用
Buffer.prototype.split = function (sep) {
    let len = Buffer.from(sep).length; // 分隔符所占的字节数
    let result = []; // 返回的数组
    let start = 0; // 查找 Buffer 的起始位置
    let offset = 0; // 偏移量

    // 循环查找分隔符
    while ((offset = this.indexOf(sep, start)) !== -1) {
        // 将分隔符之前的部分截取出来存入
        result.push(this.slice(start, offset));
        start = offset + len;
    }

    // 处理剩下的部分
    result.push(this.slice(start));

    // 返回结果
    return result;
}

module.exports = function (options) {
    return async (ctx, next) => {
        await new Promise((resolve, reject) => {
            let dataArr = []; // 存储读取的数据

            // 读取数据
            ctx.req.on("data", data => dataArr.push(data));

            ctx.req.on("end", () => {
                // 取到请求体每段的分割线字符串
                let bondery = `--${ctx.get("content-Type").split("=")[1]}`;

                // 获取不同系统的换行符
                let lineBreak = process.platform === "win32" ? "\r\n" : "\n";

                // 非文件类型数据的最终返回结果
                let fields = {};

                // 分隔的 buffer 去掉没用的头和尾即开头的 '' 和末尾的 '--'
                dataArr = dataArr.split(bondery).slice(1, -1);

                // 循环处理 dataArr 中每一段 Buffer 的内容
                dataArr.forEach(lines => {
                    // 对于普通值,信息由包含键名的行 + 两个换行 + 数据值 + 换行组成
                    // 对于文件,信息由包含 filename 的行 + 两个换行 + 文件内容 + 换行组成
                    let [head, tail] = lines.split(`${lineBreak}${lineBreak}`);

                    // 判断是否是文件,如果是文件则创建文件并写入,如果是普通值则存入 fields 对象中
                    if (head.includes("filename")) {
                        // 防止文件内容含有换行而被分割,应重新截取内容并去掉最后的换行
                        let tail = lines.slice(head.length + 2 * lineBreak.length, -lineBreak.length);

                        // 创建可写流并指定写入的路径:绝对路径 + 指定文件夹 + 随机文件名,最后写入文件
                        fs.createWriteStream(path.join(__dirname, options.uploadDir, uuid())).end(tail);
                    } else {
                        // 是普通值取出键名
                        let key = head.match(/name="(\w+)"/)[1];

                        // 将 key 设置给 fields tail 去掉末尾换行后的内容
                        fields[key] = tail.toString("utf8").slice(0, -lineBreak.length);
                    }
                });

                // 将处理好的 fields 对象挂在 ctx.request.fields 上,并完成 Promise
                ctx.request.fields = fields;
                resolve();
            });
        });

        // 向下执行
        await next();
    }
}

Приведенную выше логику содержимого можно понять с помощью комментариев к коду, то есть симуляции.koa-better-bodyФункциональная логика , наша главная забота заключается в том, как реализовано промежуточное программное обеспечение.Асинхронная операция, реализованная вышеуказанной функцией, все еще читает данные.Для того, чтобы дождаться окончания обработки данных, она все еще выполняется в промисе, и с использованиемawaitПодождите, обещание успешно выполненоnext.


моделирование промежуточного программного обеспечения koa-views

Шаблон узла — это инструмент, который мы часто используем для отображения страниц на стороне сервера.koa-viewПромежуточное программное обеспечение, помогите нам быть совместимыми с этими шаблонами, сначала установите зависимые модули.

npm install koa koa-views ejs

Ниже приведен файл шаблона ejs:

<!-- 文件:index.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>ejs</title>
</head>
<body>
    <%=name%>
    <%=age%>

    <%if (name=="panda") {%>
        panda
    <%} else {%>
        shen
    <%}%>

    <%arr.forEach(item => {%>
        <li><%=item%></li>
    <%})%>
</body>
</html>

Конкретное использование koa-views выглядит следующим образом:

// koa-views 的用法
const Koa = require("koa");
const views = require("koa-views");
const path = require("path");

const app = new Koa();

// 使用中间件
app.use(views(path.resolve(__dirname, "views"), {
    extension: "ejs"
}));

app.use(async (ctx, next) => {
    await ctx.render("index", { name: "panda", age: 20, arr: [1, 2, 3] });
});

app.listen(3000);

Видно, что мы использовалиkoa-viewsПосле промежуточного программного обеспечения пустьctxболееrenderМетод помогает нам отображать шаблон и реагировать на страницу, просто используйте его напрямую.ejsавтономныйrenderМетод тот же, и это видно из использованияrenderМетод выполняется асинхронно, поэтому вам нужно использоватьawaitПодождите, давайте смоделируем и реализуем простую версиюkoa-viewsпромежуточное ПО.

// 文件:my-koa-views.js
const fs = require("fs");
const path = require("path");
const { promisify } = require("util");

// 将读取文件方法转换成 Promise
const readFile = promisify(fs.radFile);

// 到处中间件
module.exports = function (dir, options) {
    return async (ctx, next) => {
        // 动态引入模板依赖模块
        const view = require(options.extension);

        ctx.render = async (filename, data) => {
            // 异步读取文件内容
            let tmpl = await readFile(path.join(dir, `${filename}.${options.extension}`), "utf8");

            // 将模板渲染并返回页面字符串
            let pageStr = view.render(tmpl, data);

            // 设置响应类型并响应页面
            ctx.set("Content-Type", "text/html;charset=utf8");
            ctx.body = pageStr;
        }

        // 继续向下执行
        await next();
    }
}

висит наctxВверхrenderПричина, по которой метод выполняется асинхронно, заключается в том, что внутренний файл шаблона чтения выполняется асинхронно и должен ждать, поэтомуrenderметодasyncфункция, которая динамически вводит шаблон, который мы используем внутри промежуточного программного обеспечения, напримерejs, И вctx.renderиспользуйте соответствующийrenderспособ получить строку страницы после замены данных и начать сhtmlтип ответа.


Коа-статическое моделирование промежуточного программного обеспечения

Нижеkoa-staticИспользование промежуточного программного обеспечения и зависимости кода следующие, которые необходимо установить перед использованием.

npm install koa koa-static mime

Конкретное использование koa-static выглядит следующим образом:

// koa-static 的用法
const Koa = require("koa");
const static = require("koa-static");
const path = require("path");

const app = new Koa();

app.use(static(path.resolve(__dirname, "public")));

app.use(async (ctx, next) => {
    ctx.body = "hello world";
});

app.listen(3000);

Благодаря использованию и анализу мы знаемkoa-staticРоль промежуточного программного обеспечения заключается в том, чтобы помочь нам обрабатывать статические файлы, когда сервер получает запрос. Если мы напрямую обращаемся к имени файла, он будет искать файл и отвечать напрямую. Если такого пути к файлу нет, он будет рассматриваться как папка и папка будет найдена.index.html, если он существует, он ответит напрямую, а если он не существует, он будет передан другому промежуточному программному обеспечению для обработки.

// 文件:my-koa-static.js
const fs = require("fs");
const path = require("path");
const mime = require("mime");
const { promisify } = require("util");

// 将 stat 和 access 转换成 Promise
const stat = promisify(fs.stat);
const access = promisify(fs.access)

module.exports = function (dir) {
    return async (ctx, next) => {
        // 将访问的路由处理成绝对路径,这里要使用 join 因为有可能是 /
        let realPath = path.join(dir, ctx.path);

        try {
            // 获取 stat 对象
            let statObj = await stat(realPath);

            // 如果是文件,则设置文件类型并直接响应内容,否则当作文件夹寻找 index.html
            if (statObj.isFile()) {
                ctx.set("Content-Type", `${mime.getType()};charset=utf8`);
                ctx.body = fs.createReadStream(realPath);
            } else {
                let filename = path.join(realPath, "index.html");

                // 如果不存在该文件则执行 catch 中的 next 交给其他中间件处理
                await access(filename);

                // 存在设置文件类型并响应内容
                ctx.set("Content-Type", "text/html;charset=utf8");
                ctx.body = fs.createReadStream(filename);
            }
        } catch (e) {
            await next();
        }
    }
}

Приведенная выше логика должна определить, существует ли путь, потому что все экспортируемые функцииasyncфункция, поэтому мы будемstatа такжеaccessПреобразовано в обещание и использованоtry...catchЗахватить, позвонить, когда путь недействителенnextПередать другому промежуточному ПО для обработки.


моделирование промежуточного программного обеспечения koa-router

существуетExpressВо фреймворке маршрутизация встроена в фреймворк, иKoaне встроен, это использоватьkoa-routerОн реализуется промежуточным программным обеспечением и должен быть установлен перед использованием.

npm install koa koa-router

koa-routerФункция очень мощная Ниже мы просто используем ее и моделируем ее в соответствии с используемой функцией.

// koa-router 的简单用法
const Koa = require("Koa");
const Router = require("koa-router");

const app = new Koa();
const router = new Router();

router.get("/panda", (ctx, next) => {
    ctx.body = "panda";
});

router.get("/panda", (ctx, next) => {
    ctx.body = "pandashen";
});

router.get("/shen", (ctx, next) => {
    ctx.body = "shen";
})

// 调用路由中间件
app.use(router.routes());

app.listen(3000);

видно сверхуkoa-routerТо, что экспортируется, является классом, вам нужно создать экземпляр при его использовании и вызвать экземплярroutesметод возвращаетasyncфункция для подключения, но при сопоставлении маршрута он будет основан на маршрутеgetПуть в методе сопоставляется, и внутренняя функция обратного вызова выполняется последовательно.Когда все функции обратного вызова будут выполнены, будет выполнена вся функция обратного вызова.Koaсериалnext, принцип такой же, как и у других промежуточных программ, я легко реализую функции, использованные выше.

// 文件:my-koa-router.js
// 控制每一个路由层的类
class Layer {
    constructor(path, cb) {
        this.path = path;
        this.cb = cb;
    }
    match(path) {
        // 地址的路由和当前配置路由相等返回 true,否则返回 false
        return path === this.path;
    }
}

// 路由的类
class Router {
    constructor() {
        // 存放每个路由对象的数组,{ path: /xxx, fn: cb }
        this.layers = [];
    }
    get(path, cb) {
        // 将路由对象存入数组中
        this.layers.push(new Layer(path, cb));
    }
    compose(ctx, next, handlers) {
        // 将匹配的路由函数串联执行
        function dispatch(index) {
            // 如果当前 index 个数大于了存储路由对象的长度,则执行 Koa 的 next 方法
            if(index >= handlers.length) return next();

            // 否则调用取出的路由对象的回调执行,并传入一个函数,在传入的函数中递归 dispatch(index + 1)
            // 目的是为了执行下一个路由对象上的回调函数
            handlers[index].cb(ctx, () => dispatch(index + 1));
        }

        // 第一次执行路由对象的回调函数
        dispatch(0);
    }
    routes() {
        return async (ctx, next) { // 当前 next 是 Koa 自己的 next,即 Koa 其他的中间件
            // 筛选出路径相同的路由
            let handlers = this.layers.filter(layer => layer.match(ctx.path));
            this.compose(ctx, next, handlers);
        }
    }
}

Выше мы создалиRouterкласс, который определяетgetметод и, конечно же,postподождите, мы просто реализуемgetЯ имею в виду,getЛогика внутри заключается в вызовеgetПараметрическая функция метода и строка маршрутизации объединяются в объект и сохраняются в массиве.layers, поэтому мы создаем классы, которые специально создают объекты маршрутизацииLayer, что удобно для расширения.При совпадении маршрута мы можемctx.pathПолучите строку маршрута и используйте маршрут для фильтрации объектов маршрута в массиве вызовов, которые не соответствуют маршруту, и вызовитеcomposeМетод принимает отфильтрованный массив в качестве параметраhandlersПередайте, последовательно выполните функцию обратного вызова на объекте маршрута.

composeИдея реализации этого метода очень важна, т.к.KoaИспользуется в исходном коде для объединения промежуточного ПО, вReactисходный код для конкатенацииreduxизpromise,thunkа такжеloggerи другие модули, наша реализация это упрощенная версия, не совместимая с асинхронностью, основная идея рекурсияdispatchФункция, каждый раз, когда функция обратного вызова следующего объекта маршрутизации в массиве вынимается и выполняется, пока не будут выполнены функции обратного вызова всех соответствующих маршрутов, выполнитьKoaследующее промежуточное ПОnext, обратите внимание на здесьnextОтличие от параметров функции обратного вызова в массивеnext, функция обратного вызова объекта маршрута в массивеnextПредставляет обратный вызов для следующего соответствующего маршрута.


Суммировать

Выше мы разобрали и смоделировали некоторое middleware, собственно разберемсяKoaа такжеExpressСравнительным преимуществом является то, что он не такой тяжелый, его легко разрабатывать и использовать, а все необходимые функции могут быть реализованы соответствующим промежуточным программным обеспечением.Использование промежуточного программного обеспечения может принести нам некоторые преимущества, такие как возможность монтировать наши обработанные данные и новые методы существуютctxвверх, удобно для спиныuseЕго также можно использовать в функции входящего обратного вызова, а также он может помочь нам справиться с некоторой общей логикой, так что он не будет использоваться во всех случаях.useВсе коллбеки обрабатываются, что сильно сокращает избыточный код, кажется, что на самом деле даетKoaПроцесс использования промежуточного программного обеспечения представляет собой типичный паттерн «декоратора», и после приведенного выше анализа я думаю, что все его понимают.Koa«Луковая модель» и асинхронные функции позволяют разрабатывать собственное промежуточное ПО.