Node advanced — написанный от руки исходный код коа

Node.js исходный код JavaScript koa
Node advanced — написанный от руки исходный код коа

Koa — это фреймворк для веб-разработки, основанный на nodejs. Он отличается небольшим размером и сложностью. По сравнению с экспрессом, который является большим и всеобъемлющим, хотя оба они разработаны одной и той же командой, каждый из них имеет свои более подходящие сценарии применения: экспресс подходит для разработки более крупных приложений корпоративного уровня. , и koa стремится стать краеугольным камнем веб-разработки. Например, egg.js разработан на основе koa.

По поводу разницы и связи между двумя фреймворками позже напишу еще один экспресс-анализ исходного кода, который здесь повторяться не буду. Основная цель этой статьи заключается в следующем:

На официальном сайте Koa говорится: «koa предоставляет набор элегантных методов, которые помогут вам быстро и с удовольствием писать серверные приложения». Что это за элегантный подход? Как это достигается? Выясним и напишем исходный код вручную.

Я не знал солнца в прошлом, когда я жил зимой- Неруда

фиктивное использование

Можно сказать, что использование koa очень глупо, давайте быстро пройдемся по нему:

Первое что бросается в глаза это не рокарий, а привет мир

const Koa = require('koa');
const app = new Koa();

app.use((ctx, next) => {
  ctx.body = 'Hello World';
});

app.listen(3000); 

Написание без рамки

let http = require('http')

let server = http.createServer((req, res) => {
  res.end('hello world')
})

server.listen(4000)

Для сравнения обнаруживается, что относительно родной koa имеет еще два метода use и listen в экземпляре и два параметра ctx и next в обратном вызове use. Эти четыре отличия почти все являются коа, и именно эти четыре отличия делают коа таким могущественным.

listen

Простой! Синтаксический сахар http фактически использует http.createServer(), а затем прослушивает порт.

ctx

Полегче! использоватьконтекстМеханизм объединяет исходные объекты req и res в один и был значительно расширен, чтобы разработчики могли легко использовать больше атрибутов и методов, значительно сокращая время обработки строк и извлечения информации и устраняя множество вводных. сторонних пакетов. (например, ctx.query, ctx.path и т. д.)

use

Точка! Ядро коа ——промежуточное ПО. Решить проблему ада обратных вызовов в асинхронном программировании на основеPromise,использоватьлуковая модельИдея состоит в том, чтобы сделать вложенный, запутанный код понятным, явным, расширяемым, настраиваемым, а с помощью многих сторонних промежуточных программ упрощенный koa можно сделать более универсальным (например, koa-router, который реализует маршрутизацию). Принцип в основном очень тонкийcomposeфункция. При использовании используйтеnext()метод для перехода от предыдущего промежуточного программного обеспечения к следующему промежуточному программному обеспечению.

Примечание. Части, выделенные жирным шрифтом выше, подробно описаны ниже.

исходный код

Насколько легко коа? Это так же просто, как четыре файла, и с большим количеством пустых строк и комментариев, он составляет менее 1800 строк кода (всего несколько сотен строк полезности).

GitHub.com/Смотри, о, это/Смотри, о, он…

Итак, изучение исходного кода KOA не является болезненным процессом. Нет преувеличения, чтобы сказать, что после того, как вы получите эти четыре файла и напишите более 100 строк кода ниже, вы можете полностью понять KOA. Для того, чтобы предотвратить появление больших блоков, я пойду в отличную деталь.

Готов к работе

Для имитации официального создаем папку koa и создаем четыре файла: application.js, context.js, request.js, response.js. Взглянув на package.json, можно обнаружить, что application.js — это файл входа.

context.js связан с объектом контекста, request.js связан с объектом запроса, а response.js связан с объектом ответа.

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

  • Кроме того, параметр ctx функции обратного вызова расширяет многие функции.Этот ctx фактически генерируется нативными req и res после серии обработок.

  • На самом деле первое предложение неточное, использование может быть многократное, поэтому есть несколько callback-функций, второй параметр пользователя next() переходит к следующему, а callback-функции многократного использования выполняются в порядке правил .

  • Ну вроде очень просто, сложностей всего две: одна как переработать нативные req и res в ctx, а другая как реализовать middleware.

  • Во-первых, ctx на самом деле является объектом контекста. Два файла, запрос и ответ, используются для расширения атрибутов. Файл контекста реализует прокси, и мы напишем соответствующий исходный код.

  • Во-вторых, промежуточное ПО в исходном коде реализуется исполняющим модулем промежуточного ПО koa-compose, здесь мы его и напишем.

application.js

В сочетании с приведенным выше hello world становится ясно, что koa — это класс, и в экземпляре есть два основных метода: use и listen.

Как упоминалось выше, listen является синтаксическим сахаром для http, поэтому следует ввести модуль http.

Koa имеет набор механизмов обработки ошибок, которые должны отслеживать событие ошибки экземпляра. Итак, нам нужно ввести модуль событий, чтобы наследовать EventEmitter. Введите еще три пользовательских модуля.

let http = require('http')
let EventEmitter = require('events')
let context = require('./context')
let request = require('./request')
let response = require('./response')

class Koa extends EventEmitter {
  constructor () {
    super()
  }
  use () {

  }
  listen () {

  }
}

module.exports = Koa

Эти три модуля на самом деле являются объектами.Чтобы запустить код, вот простой экспорт.

context.js

let proto = {} // proto同源码定义的变量名
module.exports = proto

request.js

let request = {}
module.exports = request

response.js

let response = {}
module.exports = response

Коа начал писать класс внутри кода, сначала создайте функцию для реализации услуг: 1, прослушивайте метод http для создания службы и порта прослушивания. 2, используйте переданный метод обратного вызова.

class Koa extends EventEmitter {
  constructor () {
    super()
    this.fn
  }
  use (fn) {
    this.fn = fn // 用户使用use方法时,回调赋给this.fn
  }
  listen (...args) {
    let server = http.createServer(this.fn) // 放入回调
    server.listen(...args) // 因为listen方法可能有多参数,所以这里直接解构所有参数就可以了
  }
}

Это запустит службу и проверит ее:

let Koa = require('./application')
let app = new Koa()

app.use((req, res) => { // 还没写中间件,所以这里还不是ctx和next
  res.end('hello world')
})

app.listen(3000)

Давайте сначала решим ctx.ctx — это контекстный объект, который связывает множество запросов и связанных данных и методов, таких как ctx.path, ctx.query, ctx.body() и т. д., что значительно упрощает разработку.

Идея такова: когда пользователь вызывает метод use, сохраните обратный вызов fn, создайте функцию createContext для создания контекста, создайте функцию handleRequest для обработки запроса и поместите handleRequest в обратный вызов createServer, когда пользователь слушает, внутри функция вызывает fn и передает объект контекста, и пользователь получает ctx.

class Koa extends EventEmitter {
  constructor () {
    super()
    this.fn
    this.context = context // 将三个模块保存,全局的放到实例上
    this.request = request
    this.response = response
  }
  use (fn) {
    this.fn = fn
  }
  createContext(req, res){ // 这是核心,创建ctx
    // 使用Object.create方法是为了继承this.context但在增加属性时不影响原对象
    const ctx = Object.create(this.context)
    const request = ctx.request = Object.create(this.request)
    const response = ctx.response = Object.create(this.response)
    // 请仔细阅读以下眼花缭乱的操作,后面是有用的
    ctx.req = request.req = response.req = req
    ctx.res = request.res = response.res = res
    request.ctx = response.ctx = ctx
    request.response = response
    response.request = request
    return ctx
  }
  handleRequest(req,res){ // 创建一个处理请求的函数
    let ctx = this.createContext(req, res) // 创建ctx
    this.fn(ctx) // 调用用户给的回调,把ctx还给用户使用。
    res.end(ctx.body) // ctx.body用来输出到页面,后面会说如何绑定数据到ctx.body
  }
  listen (...args) {
    let server = http.createServer(this.handleRequest.bind(this))// 这里使用bind调用,以防this丢失
    server.listen(...args)
  }
}

Если вы не понимаете Object.create, вы можете увидеть этот пример:

let o1 = {a: 'hello'}
let o2 = Object.create(o1)
o2.b = 'world'
console.log('o1:', o1.b) // 创建出的对象不会影响原对象
console.log('o2:', o2.a) // 创建出的对象会继承原对象的属性

o1: undefined
o2: hello


После вышеперечисленных операций пользователь может использовать различные жесты для получения нужного значения ctx.

Например, url можно получить с помощью ctx.req.url, ctx.request.req.url, ctx.response.req.url.

app.use((ctx) => {
  console.log(ctx.req.url)
  console.log(ctx.request.req.url)
  console.log(ctx.response.req.url)
  console.log(ctx.request.url)
  console.log(ctx.request.path)
  console.log(ctx.url)
  console.log(ctx.path)
})

Посетите локальный хост: 3000/abc

/abc
/abc
/abc
/undefined
/undefined
/undefined
/undefined

Есть много поз, не обязательно крутых.Чтобы быть крутыми, мы надеемся достичь следующих двух пунктов:

  • Получайте значения из пользовательских запросов и расширяйте дополнительные атрибуты помимо собственных атрибутов, таких как путь запроса и т. д.
  • Недостаточно удобно иметь возможность получить значение напрямую через ctx.url.

1 Изменить запрос

request.js

let url = require('url')
let request = {
  get url() { // 这样就可以用ctx.request.url上取值了,不用通过原生的req
    return this.req.url
  },
  get path() {
    return url.parse(this.req.url).pathname
  },
  get query() {
    return url.parse(this.req.url).query
  }
  // 。。。。。。
}
module.exports = request

Очень просто, используйте метод доступа к объекту, чтобы вернуть обработанные данные, чтобы привязать данные к запросу. Проблема здесь в том, как получить данные. Из-за предыдущего шага ctx.request это ctx, затем это. родной req, и вы можете использовать некоторые сторонние модули для обработки req.Исходный код был сильно расширен.Вот только несколько примеров,просто поймите принцип.

Посетите локальный хост: 3000/abc?id=1

/abc?id=1
/abc?id=1
/abc?id=1
/abc?id=1
/abc
undefined
undefined

2 Далее нам нужно реализовать прямое значение CTX, которое достигается здесь через агента

context.js

let proto = {

}
function defineGetter(prop, name){ // 创建一个defineGetter函数,参数分别是要代理的对象和对象上的属性
    proto.__defineGetter__(name, function(){ // 每个对象都有一个__defineGetter__方法,可以用这个方法实现代理,下面详解
        return this[prop][name] // 这里的this是ctx(原因下面解释),所以ctx.url得到的就是this.request.url
    })
}
defineGetter('request', 'url')
defineGetter('request', 'path')
// .......
module.exports = proto

Посетите локальный хост: 3000/abc?id=1

/abc?id=1
/abc?id=1
/abc?id=1
/abc?id=1
/abc
/abc?id=1
/abc

Метод __defineGetter__ может привязать функцию к указанному свойству текущего объекта. Когда значение этого свойства будет считано, будет вызвана функция, которую вы привязываете. Первый параметр — это свойство, а второй — функция. , Поскольку ctx наследует proto, когда ctx.url срабатывает метод __defineGetter__, так что это ctx. Таким образом, при вызове метода defineGetter второй атрибут первого параметра может быть делегирован ctx.

Возникает вопрос, сколько раз нужно вызывать функцию defineGetter для такого количества свойств, которое вы хотите проксировать? Да, если хотите быть более элегантным, то можете сымитировать официальный исходный код и предложить модуль делегатов в качестве батч-агента (на самом деле он недостаточно элегантен), здесь для удобства отображения лучше понять это.

3 Измените ответ. Согласно API koa, выходные данные на страницу не res.end('xx') и не res.send('xx'), а ctx.body = 'xx'. Нам нужно реализовать настройку ctx.body, а также реализовать получение ctx.body.

response.js

let response = {
    get body(){
        return this._body // get时返回出去
    },
    set body(value){
        this.res.statusCode = 200 // 只要设置了body,就应该把状态码设置为200
        this._body = value // set时先保存下来
    }
}
module.exports = response

Таким образом вы получаете ctx.response.body, а не ctx.body Аналогично прокси через контекст

Изменить контекст

let proto = {

}
function defineGetter (prop, name) {
    proto.__defineGetter__(name, function(){
        return this[prop][name]
    })
}
function defineSetter (prop, name) {
    proto.__defineSetter__(name, function(val){ // 用__defineSetter__方法设置值
        this[prop][name] = val
    })
}
defineGetter('request', 'url')
defineGetter('request', 'path')
defineGetter('response', 'body') // 同样代理response的body属性
defineSetter('response', 'body') // 同理
module.exports = proto

пройти тест

app.use((ctx) => {
  ctx.body = 'hello world'
  console.log(ctx.body)
})

Посетите локальный хост: 3000

вывод консоли узла:

hello world

Отображение веб-страницы: привет, мир

Далее давайте решим проблему с телом.Как упоминалось выше, после установки значения для тела код состояния изменяется на 200, тогда значение должно быть 404, если значение не установлено. К тому же пользователь будет выводить не только строки, но и файлы, страницы, json и т.д., которые здесь необходимо обработать, поэтому измените функцию handleRequest:

let Stream = require('stream') // 引入stream
handleRequest(req,res){
    res.statusCode = 404 // 默认404
    let ctx = this.createContext(req, res)
    this.fn(ctx)
    if(typeof ctx.body == 'object'){ // 如果是个对象,按json形式输出
        res.setHeader('Content-Type', 'application/json;charset=utf8')
        res.end(JSON.stringify(ctx.body))
    } else if (ctx.body instanceof Stream){ // 如果是流
        ctx.body.pipe(res)
    }
    else if (typeof ctx.body === 'string' || Buffer.isBuffer(ctx.body)) { // 如果是字符串或buffer
        res.setHeader('Content-Type', 'text/htmlcharset=utf8')
        res.end(ctx.body)
    } else {
        res.end('Not found')
    }
}

Таким образом достигается контекстуальная релевантность, и следующий важный момент — сосредоточиться на промежуточном программном обеспечении.

Теперь мы можем использовать его только один раз, нам нужно реализовать использование несколько раз, и мы можем использовать следующий метод в функции обратного вызова использования, чтобы перейти к следующему промежуточному программному обеспечению.Перед этим давайте разберемся с концепцией: «луковая модель».

Когда мы используем use несколько раз

    app.use((crx, next) => {
        console.log(1)
        next()
        console.log(2)
    })
    app.use((crx, next) => {
        console.log(3)
        next()
        console.log(4)
    })
    app.use((crx, next) => {
        console.log(5)
        next()
        console.log(6)
    })

Порядок его выполнения следующий:

1
3
5
6
4
2

Метод next вызовет следующее использование, а приведенный ниже код next будет выполняться после выполнения следующего использования Мы можем представить приведенный выше код следующим образом:

app.use((ctx, next) => {
    console.log(1)
    // next()  被替换成下一个use里的代码
    console.log(3)
    // next()  又被替换成下一个use里的代码
    console.log(5)
    // next()  没有下一个use了,所以这个无效
    console.log(6)
    console.log(4)
    console.log(2)
})

В этом случае он должен вывести 135642

Это луковая модель, передающая выполнение следующему промежуточному ПО через следующее.

Таким образом, данные запроса в руках разработчика будут как почетный караул, послушно проходить каждый слой промежуточного ПО и, наконец, отвечать пользователю.

Он не только справляется со сложными операциями, но и позволяет избежать хаотичной вложенности.

Кроме того, промежуточное программное обеспечение KOA также поддерживает асинхронные, вы можете использовать Async / ждать

app.use(async (ctx, next) => {
    console.log(1)
    await next()
    console.log(2)
})
app.use(async (ctx, next) => {
    console.log(3)
    let p = new Promise((resolve, roject) => {
        setTimeout(() => {
            console.log('3.5')
            resolve()
        }, 1000)
    })
    await p.then()
    await next()
    console.log(4)
    ctx.body = 'hello world'
})

1
3
// через секунду
3.5
4
2

Функция async возвращает обещание.Когда ключевое слово await добавляется перед следующим использованием предыдущего использования, она будет ждать разрешения обратного вызова следующего использования, прежде чем продолжить выполнение кода.

Есть два шага ко всему, что делается сейчас:

Первый шаг — упорядочить обратные вызовы многократного использования в строку по порядку.

Здесь используются массивы и рекурсия.Каждое использование сохраняет текущую функцию в массиве и, наконец, выполняет ее по порядку. Для выполнения этого шага используйтеcomposeфункция, эта функция является наиболее важной.

constructor () {
    super()
    // this.fn  改成:
    this.middlewares = [] // 需要一个数组将每个中间件按顺序存放起来
    this.context = context
    this.request = request
    this.response = response
}
use (fn) {
    // this.fn = fn 改成:
    this.middlewares.push(fn) // 每次use,把当前回调函数存进数组
}
compose(middlewares, ctx){ // 简化版的compose,接收中间件数组、ctx对象作为参数
    function dispatch(index){ // 利用递归函数将各中间件串联起来依次调用
        if(index === middlewares.length) return // 最后一次next不能执行,不然会报错
        let middleware = middlewares[index] // 取当前应该被调用的函数
        middleware(ctx, () => dispatch(index + 1)) // 调用并传入ctx和下一个将被调用的函数,用户next()时执行该函数
    }
    dispatch(0)
}
handleRequest(req,res){
    res.statusCode = 404
    let ctx = this.createContext(req, res)
    // this.fn(ctx) 改成:
    this.compose(this.middlewares, ctx) // 调用compose,传入参数
    if(typeof ctx.body == 'object'){
        res.setHeader('Content-Type', 'application/json;charset=utf8')
        res.end(JSON.stringify(ctx.body))
    } else if (ctx.body instanceof Stream){
        ctx.body.pipe(res)
    }
    else if (typeof ctx.body === 'string' || Buffer.isBuffer(ctx.body)) {
        res.setHeader('Content-Type', 'text/htmlcharset=utf8')
        res.end(ctx.body)
    } else {
        res.end('Not found')
    }
}

Проверьте приведенный выше пример печати 123456 еще раз, вы можете правильно получить 135642

Второй шаг - обернуть каждый обратный вызов какPromiseдобиться асинхронности.

Последний шаг - использовать Promise.resolve для переноса каждого обратного вызова в Promise, и при вызове тогда я не понимаюPromiseВы можете перейти к другой моей статье[nuggets.capable/post/684490…]

compose(middlewares, ctx){
    function dispatch(index){
        if(index === middlewares.length) return Promise.resolve() // 若最后一个中间件,返回一个resolve的promise
        let middleware = middlewares[index]
        return Promise.resolve(middleware(ctx, () => dispatch(index + 1))) // 用Promise.resolve把中间件包起来
    }
    return dispatch(0)
}
handleRequest(req,res){
    res.statusCode = 404
    let ctx = this.createContext(req, res)
    let fn = this.compose(this.middlewares, ctx)
    fn.then(() => { // then了之后再进行判断
        if(typeof ctx.body == 'object'){
            res.setHeader('Content-Type', 'application/json;charset=utf8')
            res.end(JSON.stringify(ctx.body))
        } else if (ctx.body instanceof Stream){
            ctx.body.pipe(res)
        }
        else if (typeof ctx.body === 'string' || Buffer.isBuffer(ctx.body)) {
            res.setHeader('Content-Type', 'text/htmlcharset=utf8')
            res.end(ctx.body)
        } else {
            res.end('Not found')
        }
    }).catch(err => { // 监控错误发射error,用于app.on('error', (err) =>{})
        this.emit('error', err)
        res.statusCode = 500
        res.end('server error')
    })
}

Полный код приложения

let http = require('http')
let EventEmitter = require('events')
let context = require('./context')
let request = require('./request')
let response = require('./response')
let Stream = require('stream')
class Koa extends EventEmitter {
constructor () {
    super()
    this.middlewares = []
    this.context = context
    this.request = request
    this.response = response
}
use (fn) {
    this.middlewares.push(fn)
}
createContext(req, res){
    const ctx = Object.create(this.context)
    const request = ctx.request = Object.create(this.request)
    const response = ctx.response = Object.create(this.response)
    ctx.req = request.req = response.req = req
    ctx.res = request.res = response.res = res
    request.ctx = response.ctx = ctx
    request.response = response
    response.request = request
    return ctx
}
compose(middlewares, ctx){
    function dispatch (index) {
        if (index === middlewares.length) return Promise.resolve()
        let middleware = middlewares[index]
        return Promise.resolve(middleware(ctx, () => dispatch(index + 1)))
    }
    return dispatch(0)
}
handleRequest(req,res){
    res.statusCode = 404
    let ctx = this.createContext(req, res)
    let fn = this.compose(this.middlewares, ctx)
    fn.then(() => {
        if (typeof ctx.body == 'object') {
            res.setHeader('Content-Type', 'application/json;charset=utf8')
            res.end(JSON.stringify(ctx.body))
        } else if (ctx.body instanceof Stream) {
            ctx.body.pipe(res)
        } else if (typeof ctx.body === 'string' || Buffer.isBuffer(ctx.body)) {
            res.setHeader('Content-Type', 'text/htmlcharset=utf8')
            res.end(ctx.body)
        } else {
            res.end('Not found')
        }
    }).catch(err => {
        this.emit('error', err)
        res.statusCode = 500
        res.end('server error')
    })
}
listen (...args) {
    let server = http.createServer(this.handleRequest.bind(this))
        server.listen(...args)
    }
}

module.exports = Koa

Суммировать

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