The goal of this article is to build a basic Web MVC framework from the base modules of NodeJs.
Implement a simplified version of KOA from scratch
Before we start implementing a simple version of KOA, let’s take a look at how KOA is used.
const Koa = require('koa');
const app = new Koa();
// logger
app.use(async (ctx, next) => {
await next();
const rt = ctx.response.get('X-Response-Time');
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});
// x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time'.`${ms}ms`);
});
// response
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
Copy the code
From the above usage, we can see that there are several important elements, and then our task is to implement them
application
Define the Application class briefly, and then describe the interface
const http = require("http");
const EventEmitter = require("events");
const compose = require("./compose");
class Application extends EventEmitter {
constructor() {
super(a);this.middlewares = [];
}
// Create an HTTP server
listen(. args) {
const server = http.createServer(this.callback()); server.listen(... args); }// Middleware usage
use(fn) {
this.middlewares.push(fn);
}
/** * 1. HTTP server needs to encapsulate native REq, RES * 2. The loaded middleware needs to be executed in serial * 3. The message needs to be sent to the client after the completion of the middleware execution * 4. Error handling */
callback() {
return (req, res) = > {
const ctx = this.createContext(req, res);
const fn = compose(this.middlewares);
fn(ctx)
.then(() = > this.respond(ctx))
.catch((e) = > this.onError(e, ctx));
};
}
/** * packing native REq, res construct CTX *@param {*} req
* @param {*} res* /
createContext(req, res) {}
/** * Reply to client message *@param {*} ctx* /
respond(ctx) {}
/** * error handling *@param {Object} e
* @param {Object} ctx* /
onError(e, ctx){}}module.exports = Application;
Copy the code
Then encapsulate it based on native REQ and RES to create a context
createContext(req, res) {
const ctx = {};
ctx.request = {};
ctx.response = {};
ctx.req = ctx.request.req = req;
ctx.res = ctx.response.res = res;
return ctx;
}
Copy the code
Combined middleware, sequential execution
Koa-compose is implemented recursively, and the tail recursion can be slightly optimized here
module.exports = (middlewares) = > {
const len = middlewares.length;
let next = async function () {
return Promise.resolve({});
};
/** * Creates a next function * which is called to execute the next middleware of the current middleware */
function createNext(ctx, middleware, oldNext) {
return async() = > {await middleware(ctx, oldNext);
};
}
return async (ctx) => {
for (let i = len - 1; i >= 0; i--) {
next = createNext(ctx, middlewares[i], next);
}
await next();
};
};
Copy the code
Reply to client messages in a unified manner
/** * Reply to client message *@param {*} ctx* /
respond(ctx) {
const content = ctx.body;
if (typeof content === "string") {
ctx.res.end(content);
} else if (typeof content === "object") {
ctx.res.end(JSON.stringify(content)); }}Copy the code
Error handling
/** * error handling *@param {Object} e
* @param {Object} ctx* /
onError(e, ctx) {
if (e.code === "ENOENT") {
ctx.status = 404;
} else {
ctx.status = 500;
}
const msg = e.message || "Internal error";
ctx.res.end(msg);
// Raises an error event
this.emit("error", e);
}
Copy the code
At this point we have basically completed the basic functionality of a simple KOA, but you will notice that the ctx.body and ctx.status calls have not been defined yet, so we now need to extend context, Request, and Response
// application.js
class Application extends EventEmitter {
constructor() {
super(a);this.middlewares = [];
this.context = context;
this.request = request;
this.response = response;
}
/** * construct CTX *@param {*} req
* @param {*} res* /
createContext(req, res) {
// Extend context, request, and response respectively
const ctx = Object.create(this.context);
ctx.request = Object.create(this.request);
ctx.response = Object.create(this.response);
ctx.req = ctx.request.req = req;
ctx.res = ctx.response.res = res;
returnctx; }... }// response.js
module.exports = {
get body() {
return this._body;
},
set body(data) {
this._body = data;
},
get status() {
return this.res.statusCode;
},
set status(statusCode) {
if (typeofstatusCode ! = ="number") {
throw new Error("statusCode must be a number");
}
this.res.statusCode = statusCode; }};// request.js
const url = require("url");
module.exports = {
// Proxy to native REq
get query() {
return url.parse(this.req.url, true).query; }};// context.js
const { delegateGet, delegateSet } = require(".. /utils");
const proto = {};
// Define the setters and getters to proxy in the request
const requestSet = [];
const requestGet = ["query"];
// Define the setter and getter to proxy in response
const responseSet = ["body"."status"];
const responseGet = responseSet;
// Delegate attributes to request and response, respectively
requestSet.forEach((ele) = > {
delegateSet(proto, "request", ele);
});
requestGet.forEach((ele) = > {
delegateGet(proto, "request", ele);
});
responseSet.forEach((ele) = > {
delegateSet(proto, "response", ele);
});
responseGet.forEach((ele) = > {
delegateGet(proto, "response", ele);
});
module.exports = proto;
Copy the code
// Proxy method
function delegateGet(target, prop, ele) {
const descriptor = Object.getOwnPropertyDescriptor(target, ele);
Object.defineProperty(target, ele, { ... descriptor,configurable: true.enumerable: true.get() {
return this[prop][ele]; }}); }function delegateSet(target, prop, ele) {
const descriptor = Object.getOwnPropertyDescriptor(target, ele);
Object.defineProperty(target, ele, { ... descriptor,configurable: true.enumerable: true.set(data) {
this[prop][ele] = data; }}); }Copy the code
Implement a simple MVC framework based on KOA
MVC framework focuses on organizing code by separating business logic, data and interface display. A basic MVC organization form is as follows: directory structure
.Flag ─ app.js Flag ─ Controllers// Interact with the user, process input, send data to the model, process output├ ─ ─ middlewares ├ ─ ─ services// Complex business logic encapsulation├ ─ ─static // Static file directory└ ─ ─ views// View file, HTML
Copy the code
Controller
First we need to define the format of the Controller file
const homeController = async (ctx, next) => {
ctx.render("index.html", { title: "welcome" }); The render method is used to return a separate HTML file
};
// Route processing logic
const signController = async (ctx, next) => {
const email = ctx.request.body.email || "";
const password = ctx.request.body.password || "";
console.log(`signin with email: ${email}, password: ${password}`);
if (email === "[email protected]" && password === "12345") {
ctx.render("sign-ok.html", {
title: "sign in ok".name: "xiong"}); }else {
ctx.render("sign-fail.html", {
title: "fail with error"}); }};module.exports = {
"GET /": homeController, // Routing information
"POST /signin": signController,
};
Copy the code
A KOA middleware is then written to automatically scan the Controllers directory to register the route
const fs = require("fs");
const path = require("path");
const router = require("koa-router") ();// Register the route handler
function registerUrl(router, urlMap) {
for (const [key, value] of Object.entries(urlMap)) {
if (key.startsWith("GET")) {
const path = key.substring(4);
router.get(path, urlMap[key]);
console.log(`register URL mapping: GET ${path}`);
} else if (key.startsWith("POST")) {
const path = key.substring(5);
router.post(path, urlMap[key]);
} else {
console.log(`invalid URL: ${key}`); }}}function readControllerFile(router, dir) {
const files = fs.readdirSync(dir);
const jsFiles = files.filter((item) = > item.endsWith(".js"));
for (let file of jsFiles) {
console.log('start handling controller:${file}. `);
const mappping = require(path.join(dir, file)); registerUrl(router, mappping); }}module.exports = (dir) = > {
const real_dir = dir || path.resolve(__dirname, ".. /controllers");
readControllerFile(router, real_dir);
return router.routes();
};
Copy the code
templating
As you can see from the controller above, the ctx.render method is called. All this does is read the relevant template file (usually HTML) in the views directory and return it to the client. In the following implementation, Nunjucks is used as the template engine.
const nunjucks = require("nunjucks");
function createEnv(path, opts) {
const autoescape = opts.autoescape === undefined ? true : opts.autoescape;
const noCache = opts.noCache || false;
const watch = opts.watch || false;
const throwOnUndefined = opts.throwOnUndefined || false;
// Search the template file from path
const env = new nunjucks.Environment(
new nunjucks.FileSystemLoader(path, {
noCache,
watch,
}),
{
autoescape,
throwOnUndefined,
}
);
if (opts.filters) {
for (const [key, value] of Object.entries(opts.filters)) { env.addFilter(key, value); }}return env;
}
function templating(path, opts) {
const env = createEnv(path, opts);
return async (ctx, next) => {
ctx.render = function (view, model) {
ctx.response.body = env.render(
view,
Object.assign({}, ctx.state || {}, model || {})
);
ctx.response.type = "text/html";
};
await next();
};
}
module.exports = templating;
Copy the code
Working with static files
At some point, you need to return some front-end resource files such as CSS files and image files stored on the server side, so you need a middleware to handle static resources
const path = require("path");
const mime = require("mime");
const fs = require("mz/fs");
function staticFiles(url, dir) {
return async function (ctx, next) {
const rpath = ctx.request.path;
if (rpath.startsWith(url)) {
const fp = path.join(dir, rpath.substring(url.length));
const isExist = await fs.exists(fp);
if (isExist) {
ctx.response.type = mime.getType(rpath);
ctx.response.body = await fs.readFile(fp);
} else {
ctx.response.status = 404; }}else {
// Url that is not the specified prefix, proceed to the next middleware
awaitnext(); }}; }module.exports = staticFiles;
Copy the code
At this point, we have completed a basic MVC framework. In fact, all the expansion capabilities are based on koA middleware to achieve, if you need more advanced capabilities, can be packaged based on middleware logic.
Reference documentation
- Koa documentation: koajs.com/
- Koa – compose: github.com/koajs/compo…
- eggjs: github.com/eggjs/egg
- nunjucks: nunjucks.bootcss.com/
- Koa – the router: www.npmjs.com/package/koa…