Kooa 2中間部品の原理解析――見たらすぐ書きます.


原文を読む
前言Koa 2.xバージョンは今最も流行っているNodeJSフレームワークで、Koa 2.0のソースコードは特別に簡略化されています.Expressパッケージの機能ほど多くないです.ですから、ほとんどの機能はKoa開発チーム(Expressと同じ製品です.)とコミュニティ貢献者がKoaのNodeJSのパッケージ特性に対して実現した中間部品に提供しています.中間部品を導入し、対応する位置にKoause方法を呼び出し、これによって内部動作ctxを通じていくつかの機能を実現することができます.次に、一般的な中間部品の実現原理と自分と他の人のためにどのように開発すべきかを議論します.
KKAのタマネギの模型は紹介します.
今回はタマネギモデルの実現原理に対してあまりにも多くの解剖を行いません.主に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
KoaKoa方法は非同期をサポートするものであることを知っています.したがって、通常の玉ねぎモデルの実行順序に従ってコードを実行するためには、useを呼び出したときにコードを待つ必要があり、非同期の終了を待ってからまた下方に実行する必要があります.nextではKoaを使用することを提案しています.導入されたミドルウェアはいずれもasync/await法で呼び出されており、useの各々のミドルウェアはKoa関数に戻るものであると分析できる.
koa-bodyparserミドルウェアシミュレーションasyncの原理を分析するには、まず使い方と役割を知る必要があります.koa-bodyparserのミドルウェアは私たちのkoa-bodyparser要求とフォームに提出されたクエリ文字列をオブジェクトに変換し、postに掛けます.他のミドルウエアやインターフェースで値を取って、使う前にインストールする必要があります.
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);
使用法によれば、ctx.request.bodyの中間部品が導入されたのは実は関数であることが分かります.koa-bodyparserにおいて実行され、useの特徴からKoaの関数が実行されたと推測した後、koa-bodyparserの関数を返してくれました.以下はシミュレーションで実現したコードです.
//   :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();
    };
};
上記のコードでは、asyncの呼び出しと、なぜストリームを介してデータを受信し、処理データをnextに接続し、Promiseで行われるべきかについて、いくつかの点で注意が必要です.
まず、ctx.request.bodyの呼び出しです.nextKoaが実行することを知っています.つまり、次の中間部品の関数、つまり次のnextuseasync関数を実行することです.後の非同期コードが実行された後、現在のコードを実行するためには、awaitを使って待つ必要があります.第二に、ctx.request.bodyを受信したデータがPromiseで実行されるのは、データを受信する動作が非同期であるため、データを処理する過程全体が非同期の完了を待ってからctx.request.bodyにデータを掛ける必要があり、次のuseasync関数でctx.request.bodyにデータを取得できることを保証することができる.ですから、私たちはawaitを使って、一つのPromiseが成功したらnextを実行します.
koa-better-bodyミドルウェアシミュレーションkoa-bodyparserは、処理フォーム提出時にはまだ弱いように見えます.ファイルアップロードはサポートされていないので、koa-better-bodyはこの不足を補いましたが、koa-better-bodyKoa 1.xバージョンのミドルウェアで、Koa 1.xのミドルウェアはGenerator関数を使用して実装されています.koa-convertを使用してkoa-better-bodyKoa 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
" : "
"; // 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の機能論理であり、我々の主な関心は、中間部品が実現されるように、上記の機能によって実現される非同期動作は依然としてデータの読み取りであり、データ処理が終了するのを待つためにまだPromiseで実行され、awaitを使用して待機している.
koa-view中間部品シミュレーション
Nodeテンプレートは、私たちがよく使うツールです.ビジネスの最中にページをレンダリングしてくれます.テンプレートの種類が多いので、nextミドルウェアが現れました.これらのテンプレートと互換して、先に依存モジュールをインストールしてください.
npm install koa koa-view ejs
以下はejsのテンプレートファイルです.




    
    ejs


    
    

    
        panda
    
        shen
    

     {%>
        
  • 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-viewミドルウェアを使用した後、koa-viewsctx方法を追加してください.テンプレートに対するレンダリングと応答ページを実現するためには、renderが持参したejs方法と同様に、用法からrender方法が非同期であることが分かります.次に、簡単なrenderミドルウェアを実現するためのシミュレーションを行います.
    //   :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();
        }
    }
    awaitに掛けられたkoa-views方法が非同期的に実行されたのは、内部読取テンプレートファイルが非同期的に実行されるため、待ち時間が必要であるため、ctx方法はrender関数であり、中間部品内部に私たちが使用するテンプレートを動的に導入している.render内部で、対応するasync方法を用いて代替データ後のページ文字列を取得し、ejsのタイプで応答する.
    koa-static中間部品シミュレーション
    以下はctx.renderミドルウェアの使い方です.コード使用の依存性は以下の通りです.使用前にインストールする必要があります.
    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);
    使用と分析を通じて、renderミドルウェアの役割はサーバが要求を受けた時に、静的ファイルを処理してくれることが分かりました.ファイル名に直接アクセスすると、このファイルを探して直接応答します.もしこのファイルパスがなかったら、作文フォルダの下の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();
            }
        }
    }
    上記のロジックでは、経路の有無を検出する必要があります.私たちが導出した関数は全部koa-static関数ですので、私たちはkoa-staticindex.htmlをPromiseに変換し、asyncを用いて捕獲し、経路が不正な場合はstatを呼び出して他のミドルウェア処理に渡します.
    koa-router中間部品シミュレーションaccessのフレームワークでは、ルーティングはフレーム内に内蔵されており、try...catchに内蔵されていない.nextのミドルウェアを使用して実現され、使用前にインストールが必要である.
    npm install koa koa-routerExpressは機能が非常に強いです.以下は簡単に使うだけで、使用する機能によってシミュレーションします.
    // 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から導出されたのはクラスであり、使用時には一例を作成する必要があり、この方法を呼び出したkoa-routerの関数を接続するが、ルーティングに適合すると、ルーティングkoa-routerの方法における経路に従ってマッチングし、シリアルで内部のコールバック関数を実行する.すべてのコールバック関数が実行されるとkoa-routerシリアルのroutes全体が実行されます.原理は他のミドルウェアと同じです.私は以下に上で使用した機能について簡単に実現します.
    //   :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);
            }
        }
    }
    上記のasyncクラスを作成しました.get方法を定義しました.もちろんKoaなどもあります.nextの意味だけを実現します.Router内はgetメソッドを呼び出すパラメータ関数とルート文字列を一緒に構成してオブジェクトを構築して配列postに預けました.したがって、ルーティングオブジェクトを専門に構成するクラスgetを作成し、ルーティングマッチング時にgetに従ってルーティング文字列を取得し、ルーティングフィルタリング配列中のルーティングと一致しないルーティングオブジェクトを介してget方法を呼び出してフィルタリングされた配列をパラメータlayersとして入力することができる.シリアルは、ルートオブジェクトのコールバック関数を実行します.Layerこの方法の実現思想は非常に重要で、ctx.pathのソースコードの中で直列に接続するために使用され、composeのソースコードの中でhandlerscomposeKoaReactなどのモジュールに使用されています.すべての整合されたルーティングのコールバック関数が実行されるまで、配列内の次の中間reduxが実行される.promiseは、配列内のコールバック関数のパラメータthunkとは異なるloggerであり、配列内のルーティング対象コールバック関数のdispatchは、次のマッチングルートのコールバックを表す.
    締め括りをつける
    上記のいくつかの中間部品を分析してシミュレーションしましたが、実はKoanextとの比較の利点はそんなに重くないことを理解しています.開発と使用が便利で、必要な機能はすべて対応の中間部品で実現できます.中間部品を使うと、私達が処理したデータと新しい方法をnextにマウントするなど、良いところを持ってきます.後のnextから入ってきたコールバック関数で使用してもいいし、いくつかの共通論理を処理してくれてもいいです.nextの各コールバックで処理するほどではなく、冗長コードが大幅に減少しました.これにより、Koaにミドルウェアを使用する過程は、典型の「装飾器」モードになります.上記の分析を通して、Expressの「タマネギモデル」と非同期の特徴も分かりました.どうやって自分のミドルウェアを開発するべきか分かりました.