本文介绍如何手动写一个简单的koa,通过这个流程就可以较好的掌握koa2中的基本概念。文章提供了项目的基本文件架构,以及每个文件的代码,但是代码的解释是使用英文来写的。
1. 搭建项目的文件结构
bash
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
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:
-
const response = {...}
:- This line declares a
response
object which will contain several properties and methods related to HTTP responses.
- This line declares a
-
set status (value) {...}
:- This is a setter method for
status
. When you assign a value toresponse.status
, it will set the HTTP status code of the response by assigning the provided value tothis.res.statusCode
. this.res
likely refers to the nativeresponse
object of Node.js's HTTP module, which is probably assigned elsewhere in your application.
- This is a setter method for
-
_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.
-
get body () {...}
andset body (value) {...}
:- These are getter and setter methods for
body
. The getter method returns the value ofthis._body
, allowing you to access the response body. - The setter method assigns a new value to
this._body
, letting you set the response body.
- These are getter and setter methods for
-
module.exports = response
:- This line exports the
response
object, making it available for import in other parts of your Node.js application.
- This line exports the
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:
javascript
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:
javascript
// 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
:
javascript
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
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.
-
const url = require('url')
:- This line imports Node.js's built-in
url
module, which provides utilities for URL resolution and parsing.
- This line imports Node.js's built-in
-
const request = {...}
:- This defines the
request
object, which contains methods to get various properties of the HTTP request.
- This defines the
-
get method () {...}
:- This getter method returns the HTTP method (like GET, POST, PUT, DELETE, etc.) of the request.
-
get header () {...}
:- This getter method returns the headers of the HTTP request as an object.
-
get url () {...}
:- This getter method returns the full URL of the request.
-
get path () {...}
:- This getter method uses the
url
module to parse the request's URL and returns only the pathname part.
- This getter method uses the
-
get query () {...}
:- This getter method also uses the
url
module to parse the request's URL and extract the query string as an object.
- This getter method also uses the
-
module.exports = request
:- This exports the
request
object so that it can be required and used in other modules of your Node.js application.
- This exports the
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:
javascript
console.log(request.method); // Outputs: 'GET', 'POST', etc.
Retrieving Request Headers
To retrieve all headers from the incoming request:
javascript
console.log(request.header); // Outputs: { 'content-type': 'application/json', ... }
Getting the Full URL
To get the full URL of the request:
javascript
console.log(request.url); // Outputs: '/path?name=value'
Extracting the Pathname
To extract just the pathname of the URL, without the query string:
javascript
console.log(request.path); // Outputs: '/path'
Parsing Query Parameters
To parse and retrieve the query parameters as an object:
javascript
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:
javascript
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
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.
-
const context = {...}
:- Initializes an empty
context
object. This object will later be augmented with properties likemethod
andurl
for therequest
, andbody
for theresponse
.
- Initializes an empty
-
defineProperty('request', 'method')
and other similar calls:- These function calls use
defineProperty
to add properties to thecontext
object. Each property is associated with either therequest
orresponse
objects.
- These function calls use
-
function defineProperty(target, name) {...}
:- Defines a function that takes a
target
(either 'request' or 'response') and aname
of the property to be defined on thecontext
object. - Within the function,
Object.defineProperty
is used to define a new property oncontext
. 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
orcontext.response
). - The setter updates the value of the property on the target object.
- Defines a function that takes a
-
module.exports = context
:- Exports the
context
object for use in other parts of the application.
- Exports the
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:
javascript
defineProperty('request', 'method');
defineProperty('request', 'url');
This will allow you to access the HTTP method and URL of the request through the context
object:
javascript
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:
javascript
defineProperty('response', 'body');
This lets you get or set the body of the response:
javascript
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:
javascript
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
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
-
Importing Modules:
- The necessary modules are imported, including
http
for creating the server and handling HTTP requests and responses. - Custom modules
context
,request
, andresponse
are also imported. These modules provide functionality to handle different aspects of the HTTP context.
- The necessary modules are imported, including
-
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.
- The
Application Methods
-
listen(...args):
- This method creates an HTTP server and listens on the given port. It uses the
callback()
method to handle requests.
- This method creates an HTTP server and listens on the given port. It uses the
-
use(fn):
- Adds a middleware function to the application's middleware stack.
-
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.
-
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.
-
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
- 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
- module.exports = Application :
- The
Application
class is exported, allowing it to be used in other files.
- The
How to Use This Framework
-
Creating an App Instance:
- You would create an instance of the
Application
class to start building your application.
- You would create an instance of the
-
Adding Middleware:
- Use the
app.use()
method to add middleware functions to your application.
- Use the
-
Starting the Server:
- Call the
app.listen()
method with the desired port to start your server.
- Call the
Sample Middleware Function
Here's how you might add a simple middleware function that logs the method and URL of each request:
javascript
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
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
-
Koa Framework:
- The custom Koa framework is imported and used to create a new application instance
app
.
- The custom Koa framework is imported and used to create a new application instance
-
File System (fs) Module:
- The
fs
module is imported to perform file system operations, such as reading files.
- The
-
Util Module:
- The
util
module is used to convertfs.readFile
(which uses callbacks) into a promise-based function for easier use with async/await syntax.
- The
-
Creating an App Instance:
- A new Koa application instance is created.
Middleware
- app.use :
- Middleware is added to the application using
app.use
. Middleware is a function that has access to thectx
(context) object.
- Middleware is added to the application using
- 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:
-
Start the Application:
- Run the application to start listening for requests on port 8888.
-
Add Middleware:
- Uncomment or add new middleware functions with
app.use()
to handle different types of requests and responses.
- Uncomment or add new middleware functions with
-
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.
- Choose the type of response you want to send back to the client by setting
-
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
json
{
"name": "koa",
"version": "1.0.0",
"description": "",
"main": "lib/application",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}