koa
- 官网:https://koajs.com/;
- koa解析:
https://juejin.cn/post/6892952604163342344;
https://juejin.cn/post/6903350655474204680;
koa compose
js
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}koa-static
js
'use strict'
/**
* Module dependencies.
*/
const debug = require('debug')('koa-static')
const path = require('path')
const assert = require('assert')
const send = require('koa-send')
/**
* Expose `serve()`.
*/
module.exports = serve
/**
* Serve static files from `root`.
*
* @param {String} root
* @param {Object} [opts]
* @return {Function}
* @api public
*/
function serve (root, opts = {}) {
assert(root, 'root directory is required to serve files')
debug('static "%s" %j', root, opts)
opts.root = path.resolve(root)
opts.index = opts.index ?? 'index.html'
if (!opts.defer) {
return async function serve (ctx, next) {
let done = false
if (ctx.method === 'HEAD' || ctx.method === 'GET') {
try {
done = await send(ctx, ctx.path, opts)
} catch (err) {
if (err.status !== 404) {
throw err
}
}
}
if (!done) {
await next()
}
}
}
return async function serve (ctx, next) {
await next()
if (ctx.method !== 'HEAD' && ctx.method !== 'GET') return
// response is already handled
if (ctx.body != null || ctx.status !== 404) return // eslint-disable-line
try {
await send(ctx, ctx.path, opts)
} catch (err) {
if (err.status !== 404) {
throw err
}
}
}
}js
/**
* Module dependencies.
*/
import fs from 'node:fs';
import asyncFs from 'node:fs/promises';
import path from 'node:path';
import safeResolvePath from 'resolve-path';
import createError from 'http-errors';
import type {SendOptions, ParameterizedContext} from './send.types';
import {isPathExists, isPathHidden, getFileType} from './send.utils';
/**
* Send file at `path` with the
* given `options` to the koa `ctx`.
*
* @param {Context} ctx
* @param {String} filePath
* @param {Object} [opts]
* @return {Promise}
* @api public
*/
export async function send(
ctx: ParameterizedContext,
filePath: string,
opts: SendOptions = {},
): Promise<string | undefined> {
if (!ctx) throw new Error('koa context required');
if (!filePath) throw new Error('file pathname required');
// options
const root = opts.root ? path.resolve(opts.root) : '';
const trailingSlash = filePath.at(-1) === '/';
filePath = filePath.slice(path.parse(filePath).root.length);
const {index} = opts;
const maxage = opts.maxage || opts.maxAge || 0;
const immutable = opts.immutable || false;
const hidden = opts.hidden || false;
const format = opts.format !== false;
const extensions = Array.isArray(opts.extensions) ? opts.extensions : false;
const brotli = opts.brotli !== false;
const gzip = opts.gzip !== false;
const {setHeaders} = opts;
if (setHeaders && typeof setHeaders !== 'function')
throw new TypeError('option setHeaders must be function');
// normalize and decode path
try {
filePath = decodeURIComponent(filePath);
} catch {
return ctx.throw(400, 'failed to decode');
}
// index file support
if (index && trailingSlash) filePath += index;
filePath = safeResolvePath(root, filePath);
// hidden file support, ignore
if (!hidden && isPathHidden(root, filePath)) return;
// serve brotli file when possible otherwise gzipped file when possible
let encodingExt = '';
if (
ctx.acceptsEncodings('br', 'identity') === 'br' &&
brotli &&
(await isPathExists(filePath + '.br'))
) {
filePath += '.br';
ctx.set('Content-Encoding', 'br');
ctx.res.removeHeader('Content-Length');
encodingExt = '.br';
} else if (
ctx.acceptsEncodings('gzip', 'identity') === 'gzip' &&
gzip &&
(await isPathExists(filePath + '.gz'))
) {
filePath += '.gz';
ctx.set('Content-Encoding', 'gzip');
ctx.res.removeHeader('Content-Length');
encodingExt = '.gz';
}
if (extensions && !path.basename(filePath).includes('.')) {
for (let ext of extensions) {
if (typeof ext !== 'string')
throw new TypeError(
'option extensions must be array of strings or false',
);
if (!ext.startsWith('.')) ext = `.${ext}`;
if (await isPathExists(`${filePath}${ext}`)) {
filePath = `${filePath}${ext}`;
break;
}
}
}
// stat
let stats;
try {
stats = await asyncFs.stat(filePath);
// Format the path to serve static file servers
// and not require a trailing slash for directories,
// so that you can do both `/directory` and `/directory/`
if (stats.isDirectory()) {
if (!format || !index) return;
filePath += `/${index}`;
stats = await asyncFs.stat(filePath);
}
} catch (err) {
const notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR'];
if (notfound.includes(err.code)) throw createError(404, err);
err.status = 500;
throw err;
}
// inject headers
setHeaders?.(ctx.res, filePath, stats);
// stream
ctx.set('Content-Length', stats.size.toString());
if (!ctx.response.get('Last-Modified'))
ctx.set('Last-Modified', stats.mtime.toUTCString());
if (!ctx.response.get('Cache-Control')) {
const directives = [`max-age=${(maxage / 1000) | 0}`];
if (immutable) directives.push('immutable');
ctx.set('Cache-Control', directives.join(','));
}
if (!ctx.type) ctx.type = getFileType(filePath, encodingExt);
ctx.body = fs.createReadStream(filePath);
return filePath;
}