手写一个简单的koa

本文介绍如何手动写一个简单的koa,通过这个流程就可以较好的掌握koa2中的基本概念。文章提供了项目的基本文件架构,以及每个文件的代码,但是代码的解释是使用英文来写的。

1. 搭建项目的文件结构

mkdir generate-koa
cd generate-koa
npm init -y
mkdir -p koa/lib
touch koa/lib/application.js # 自定义koa库入口
touch koa/lib/context.js
touch koa/lib/request.js
touch koa/lib/response.js
touch app.js # 测试主程序
touch koa/package.json

2. 各个文件内容及详解

2.1 response.js

const response = {
  set status (value) {
    this.res.statusCode = value
  },

  _body: '', // 真正用来存数据的

  get body () {
    return this._body
  },

  set body (value) {
    this._body = value
  }
}

module.exports = response

descriptions:
The code snippet above is a JavaScript object that is meant to be used as a part of a Node.js module, specifically for handling HTTP response objects in server-side programming, possibly with a framework like Koa, which is known for using middleware functions to handle requests and responses.

Here’s a breakdown of what each part of the code does:

  1. const response = {...}:

    • This line declares a response object which will contain several properties and methods related to HTTP responses.
  2. set status (value) {...}:

    • This is a setter method for status. When you assign a value to response.status, it will set the HTTP status code of the response by assigning the provided value to this.res.statusCode.
    • this.res likely refers to the native response object of Node.js’s HTTP module, which is probably assigned elsewhere in your application.
  3. _body: '':

    • This property is prefixed with an underscore, which by convention indicates that it is a private property. It is meant to hold the body of the HTTP response as a string.
  4. get body () {...} and set body (value) {...}:

    • These are getter and setter methods for body. The getter method returns the value of this._body, allowing you to access the response body.
    • The setter method assigns a new value to this._body, letting you set the response body.
  5. module.exports = response:

    • This line exports the response object, making it available for import in other parts of your Node.js application.

Now, for a tutorial-like explanation:


Handling HTTP Response with a Custom response Module in Node.js

In Node.js, managing HTTP responses effectively is crucial for building web applications. The provided response module is a custom implementation that allows you to manipulate HTTP response objects with ease.

Setting HTTP Status Codes

To set the status code of a response, use the status setter method:

response.status = 404;

When you assign a value to response.status, it delegates this value to the underlying Node.js response object’s statusCode property.

Managing Response Body

To set or retrieve the body of the response, use the body getter and setter methods:

// Set the response body
response.body = 'Hello, World!';

// Get the response body
console.log(response.body); // Outputs: Hello, World!

The body property uses a private variable _body to store the response data. The getter method allows you to access this data, while the setter method lets you update it.

Exporting the Module

Finally, the module is made available for use in other parts of the application using module.exports:

const customResponse = require('./response');

By importing the response module, you can control your HTTP responses with the designed interface, ensuring consistency and potentially adding more functionality as your application grows.

2.2 request.js

const url = require('url')

const request = {
  get method () {
    return this.req.method
  },

  get header () {
    return this.req.headers
  },

  get url () {
    return this.req.url
  },

  get path () {
    return url.parse(this.req.url).pathname
  },

  get query () {
    return url.parse(this.req.url, true).query
  }
}

module.exports = request
    

descriptions:

The code above defines a JavaScript object that represents a request in a Node.js application, with several getter methods that extract specific parts of the incoming HTTP request. It’s structured in a way that’s common in Node.js frameworks like Express or Koa, where middleware functions are used to process HTTP requests and responses.

  1. const url = require('url'):

    • This line imports Node.js’s built-in url module, which provides utilities for URL resolution and parsing.
  2. const request = {...}:

    • This defines the request object, which contains methods to get various properties of the HTTP request.
  3. get method () {...}:

    • This getter method returns the HTTP method (like GET, POST, PUT, DELETE, etc.) of the request.
  4. get header () {...}:

    • This getter method returns the headers of the HTTP request as an object.
  5. get url () {...}:

    • This getter method returns the full URL of the request.
  6. get path () {...}:

    • This getter method uses the url module to parse the request’s URL and returns only the pathname part.
  7. get query () {...}:

    • This getter method also uses the url module to parse the request’s URL and extract the query string as an object.
  8. module.exports = request:

    • This exports the request object so that it can be required and used in other modules of your Node.js application.

Now let’s convert this explanation into a tutorial-like format.


Understanding the request Module in Node.js

The request module provides an abstraction over the Node.js HTTP request object, allowing you to easily access various components of the incoming request. Here’s how to use each part of the module:

Accessing the HTTP Method

To get the HTTP method of the incoming request:

console.log(request.method); // Outputs: 'GET', 'POST', etc.
Retrieving Request Headers

To retrieve all headers from the incoming request:

console.log(request.header); // Outputs: { 'content-type': 'application/json', ... }
Getting the Full URL

To get the full URL of the request:

console.log(request.url); // Outputs: '/path?name=value'
Extracting the Pathname

To extract just the pathname of the URL, without the query string:

console.log(request.path); // Outputs: '/path'
Parsing Query Parameters

To parse and retrieve the query parameters as an object:

console.log(request.query); // Outputs: { name: 'value' }
Utilizing the Module

To use this module in other parts of your application, you need to import it:

const request = require('./request');

By using the request module, you can handle different aspects of the incoming HTTP request without directly interacting with the Node.js request object and its more verbose API.

2.3 context.js

const context = {
  // get method () {
  //   return this.request.method
  // },

  // get url () {
  //   return this.request.url
  // }
}

defineProperty('request', 'method')
defineProperty('request', 'url')
defineProperty('response', 'body')

function defineProperty (target, name) {
  // context.__defineGetter__(name, function () {
  //   return this[target][name]
  // })
  Object.defineProperty(context, name, {
    get () {
      return this[target][name]
    },

    set (value) {
      this[target][name] = value
    }
  })
}

module.exports = context

descriptions:
The code snippet defines a context object which acts as a container for a request and response in a Node.js server-side application, possibly in a framework like Koa. It also includes a function defineProperty which dynamically adds properties to the context object with getter and setter methods.

  1. const context = {...}:

    • Initializes an empty context object. This object will later be augmented with properties like method and url for the request, and body for the response.
  2. defineProperty('request', 'method') and other similar calls:

    • These function calls use defineProperty to add properties to the context object. Each property is associated with either the request or response objects.
  3. function defineProperty(target, name) {...}:

    • Defines a function that takes a target (either ‘request’ or ‘response’) and a name of the property to be defined on the context object.
    • Within the function, Object.defineProperty is used to define a new property on context. This property is accessorized with a getter and a setter.
    • The getter returns the value of the property from the target object (either context.request or context.response).
    • The setter updates the value of the property on the target object.
  4. module.exports = context:

    • Exports the context object for use in other parts of the application.

Here’s how the code functions in a tutorial format:


Custom Context Object in Node.js

When building web applications in Node.js, it’s common to have a context object that represents the current request and response. This custom context module will help you manage and access the request and response data easily.

Dynamic Property Definition

Instead of manually defining each property, you can dynamically add properties to the context object using the defineProperty function. This function uses JavaScript’s Object.defineProperty to add getters and setters to the context object for each property.

Adding Request and Response Properties

For instance, you can define properties related to the HTTP request:

defineProperty('request', 'method');
defineProperty('request', 'url');

This will allow you to access the HTTP method and URL of the request through the context object:

console.log(context.method); // Outputs the HTTP method
console.log(context.url); // Outputs the URL of the request
Managing Response Body

Similarly, you can define a property for the response body:

defineProperty('response', 'body');

This lets you get or set the body of the response:

context.body = 'Hello World'; // Sets the response body
console.log(context.body); // Retrieves the response body
Exporting the Context

To use this context object in other parts of your application, export it at the end of your module:

module.exports = context;

By importing this context module in your application, you streamline the way you handle request and response data across your codebase.

2.4 application.js

const http = require('http')
const { Stream } = require('stream')
const context = require('./context')
const request = require('./request')
const { body } = require('./response')
const response = require('./response')
class Application {
constructor() {
this.middleware = [] // 保存用户添加的中间件函数
this.context = Object.create(context)
this.request = Object.create(request)
this.response = Object.create(response)
}
listen(...args) {
const server = http.createServer(this.callback())
server.listen(...args)
}
use(fn) {
this.middleware.push(fn)
}
// 异步递归遍历调用中间件处理函数
compose(middleware) {
return function (context) {
const dispatch = index => {
if (index >= middleware.length) return Promise.resolve()
const fn = middleware[index]
return Promise.resolve(
// TODO: 上下文对象
fn(context, () => dispatch(index + 1)) // 这是 next 函数
)
}
// 返回第 1 个中间件处理函数
return dispatch(0)
}
}
// 构造上下文对象
createContext(req, res) {
// 一个实例会处理多个请求,而不同的请求应该拥有不同的上下文对象,为了避免请求期间的数据交叉污染,所以这里又对这个数据做了一份儿新的拷贝
const context = Object.create(this.context)
const request = (context.request = Object.create(this.request))
const response = (context.response = Object.create(this.response))
context.app = request.app = response.app = this
context.req = request.req = response.req = req
context.res = request.res = response.res = res
request.ctx = response.ctx = context
request.response = response
response.request = request
context.originalUrl = request.originalUrl = req.url
context.state = {}
return context
}
callback() {
console.log('http://localhost:8888/')
const fnMiddleware = this.compose(this.middleware)
const handleRequest = (req, res) => {
// 每个请求都会创建一个独立的 Context 上下文对象,它们之间不会互相污染
const context = this.createContext(req, res)
fnMiddleware(context)
.then(() => {
respond(context)
// res.end(context.body)
// res.end('My Koa')
})
.catch(err => {
res.end(err.message)
})
}
return handleRequest
}
}
function respond (ctx) {
const body = ctx.body
const res = ctx.res
if (body === null) {
res.statusCode = 204
return res.end()
}
if (typeof body === 'string') return res.end(body)
if (Buffer.isBuffer(body)) return res.end(body)
if (body instanceof Stream) return body.pipe(ctx.res)
if (typeof body === 'number') return res.end(body + '')
if (typeof body === 'object') {
const jsonStr = JSON.stringify(body)
return res.end(jsonStr)
}
}
module.exports = Application

descriptions:

The code above defines a Node.js application framework similar to Koa. It’s a custom implementation of a web application server that can handle HTTP requests, run middleware functions, and send responses.

Here’s a step-by-step tutorial on what the code does:

Setting Up the Application Class
  1. Importing Modules:

    • The necessary modules are imported, including http for creating the server and handling HTTP requests and responses.
    • Custom modules context, request, and response are also imported. These modules provide functionality to handle different aspects of the HTTP context.
  2. Creating the Application Class:

    • The Application class is created with properties for middleware, request, response, and context.
    • The middleware array will store functions that can modify the request/response or perform operations in between receiving a request and sending a response.
Application Methods
  1. listen(…args):

    • This method creates an HTTP server and listens on the given port. It uses the callback() method to handle requests.
  2. use(fn):

    • Adds a middleware function to the application’s middleware stack.
  3. compose(middleware):

    • Takes an array of middleware functions and composes them into a single function that can be executed sequentially.
    • It handles the asynchronous execution of middleware and ensures that each middleware can call the next one in the stack.
  4. createContext(req, res):

    • Creates and returns a context object for each request, avoiding cross-request contamination.
    • It links the context with the request and response objects and sets up references between them.
  5. callback():

    • Logs a message indicating the server is running.
    • Wraps the middleware composition in a handleRequest function that is used to handle incoming HTTP requests.
Handling Responses
  1. respond(ctx):
    • Takes a context object and sends the appropriate response to the client.
    • It handles different types of response bodies, including null, string, Buffer, Stream, and JSON objects.

Exporting the Application

  1. module.exports = Application:
    • The Application class is exported, allowing it to be used in other files.
How to Use This Framework
  1. Creating an App Instance:

    • You would create an instance of the Application class to start building your application.
  2. Adding Middleware:

    • Use the app.use() method to add middleware functions to your application.
  3. Starting the Server:

    • Call the app.listen() method with the desired port to start your server.
Sample Middleware Function

Here’s how you might add a simple middleware function that logs the method and URL of each request:

const app = new Application();
app.use(async (ctx, next) => {
console.log(`${ctx.request.method} ${ctx.request.url}`);
await next();
});
app.listen(8888);

This framework provides a solid foundation for creating complex applications by allowing middleware functions to be chained and executed in order, giving you control over the request and response lifecycle.

2.5 app.js

const Koa = require('./koa')
const fs = require('fs')
const app = new Koa()
const util = require('util')
const readFile = util.promisify(fs.readFile)
app.use(async ctx => {
// console.log('ctx:', ctx)
// ctx.body = 'string'
ctx.body = 123
// const data = await readFile('./package.json')
// ctx.body = data
// ctx.body = fs.createReadStream('./package.json')
// ctx.body = { foo: 'bar' }
// ctx.body = [1, 2, 3]
// ctx.body = null
})
// app.use(async (ctx, next) => {
//   ctx.body = 'Hello Koa'
//   // ctx.response.body = 'Hello Koa'
//   next()
//   ctx.body = 'Hello Koa 3'
// })
// app.use(async (ctx, next) => {
//   console.log(ctx.response.body, ctx.body)
//   ctx.body = 'Hello Koa 2'
// })
app.listen(8888)

descriptions:

The code above is a simple Node.js application using a custom Koa framework (presumably similar to the popular Koa framework, but this seems to be a custom implementation located in the local ‘./koa’ directory). It demonstrates how to create a web server that listens on port 8888 and how to use middleware to process requests.

Here’s how the code works, step by step:

Importing Modules and Setting Up

  1. Koa Framework:

    • The custom Koa framework is imported and used to create a new application instance app.
  2. File System (fs) Module:

    • The fs module is imported to perform file system operations, such as reading files.
  3. Util Module:

    • The util module is used to convert fs.readFile (which uses callbacks) into a promise-based function for easier use with async/await syntax.
  4. Creating an App Instance:

    • A new Koa application instance is created.

Middleware

  1. app.use:
    • Middleware is added to the application using app.use. Middleware is a function that has access to the ctx (context) object.
  2. Asynchronous Middleware Function:
    • An async middleware function is defined, which will process incoming requests.
    • Various types of responses are shown, all commented out except ctx.body = 123, which will send a numeric response to the client.

Handling Different Response Types

The middleware showcases how to handle different types of responses:

  • Sending a simple string: ctx.body = 'string';
  • Sending a number: ctx.body = 123;
  • Sending file contents read asynchronously: ctx.body = await readFile('./package.json');
  • Streaming file contents: ctx.body = fs.createReadStream('./package.json');
  • Sending a JSON object: ctx.body = { foo: 'bar' };
  • Sending an array: ctx.body = [1, 2, 3];
  • Sending a null, which would set the status to 204 No Content: ctx.body = null;

Additional Middleware Examples

Additional middleware examples are commented out, showing how ctx.body can be reassigned in subsequent middleware functions, and how the next() function is used to move to the next middleware in the stack.

Starting the Server

Finally, app.listen(8888) starts the server on port 8888.

How to Use This Application

Here’s a high-level guide on using this code:

  1. Start the Application:

    • Run the application to start listening for requests on port 8888.
  2. Add Middleware:

    • Uncomment or add new middleware functions with app.use() to handle different types of requests and responses.
  3. Customize Response:

    • Choose the type of response you want to send back to the client by setting ctx.body to a string, number, stream, or other types.
  4. Additional Processing:

    • Add additional middleware to perform further processing or override previous response values.

By running this application, you’ll have a simple web server capable of responding with different types of content based on the incoming HTTP requests.

2.6 package.json

{
"name": "koa",
"version": "1.0.0",
"description": "",
"main": "lib/application",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

原文链接:https://juejin.cn/post/7324867096605065250 作者:慕仲卿

(0)
上一篇 2024年1月18日 上午10:48
下一篇 2024年1月18日 上午10:58

相关推荐

发表回复

登录后才能评论