koa-view源码

190 阅读5分钟

用法

在看koa-view源码之前先看下koa-view用法:

在使用koa-view时,必须安装您想要使用的引擎如ejs

var views = require('@ladjs/koa-views');

const render = views(__dirname + '/views', {
  map: {
    html: 'ejs'
  }
})

// 注册中间件
app.use(render)
// 或者app.context.render = render()

app.use(async function (ctx) {
  ctx.state = {
    session: this.session,
    title: 'app'
  };

  await ctx.render('user', {
    user: 'John'
  });
});

实现原理是,views函数调用会产生一个中间件函数render,在render中间件中会向ctx中添加render方法,这个render方法的调用就会解析对应的模板得到html

API

views(root, opts)函数有两个参数

  • root: 必须是一个绝对路径,指定视图文件所在的目录。所有渲染的视图都是相对于这个路径的。
  • opts (可选): 一个包含配置选项的对象。

配置选项 (opts):

  • autoRender: 布尔值,决定是否使用 ctx.body 来接收渲染后的模板字符串。默认值为 true
// 设置成false 后需要return出去
const render = views(__dirname, { autoRender: false, extension: 'pug' });
app.use(render)

app.use(async function (ctx) {
  return await ctx.render('user.pug')
})
  • extension: 视图文件的默认扩展名。使用此选项,您可以省略文件扩展名。
const render = views(__dirname, { extension: 'pug' })
app.use(render)

app.use(async function (ctx) {
  await ctx.render('user') // 这里不需要写拓展名
})
  • map: 将文件扩展名映射到特定的模板引擎。例如,将 .html 文件映射到 nunjucks 引擎。
const render = views(__dirname, { map: { html: 'nunjucks' }});
app.use(render);
app.use(async function (ctx) {
  await ctx.render('user.html');
});
  • engineSource: 替换默认的 @ladjs/consolidate 引擎源。一般不用改,关于@ladjs/consolidate下一节会介绍
  • options: 这些选项将传递给视图引擎。模板引擎需要的options

koa-view的依赖包@ladjs/consolidate

@ladjs/consolidate 是一个 Node.js 的模板引擎集成库,它允许开发者使用多种模板引擎来渲染模板。@ladjs/consolidate 支持多种模板引擎,如 EJS。使用方法

const cons = require('@ladjs/consolidate');
// 使用ejs引擎,使用前请安装ejs
cons.ejs('views/page.html', { user: 'tobi' }, function(err, html) {
  if (err) throw err;
  console.log(html);
});

koa-view的依赖包koa-send

koa-send 它用于发送静态文件。这个库特别适用于当你需要在 Koa 应用中提供文件下载或服务静态文件时。

根路径 (root)

root 选项是必需的,它应该指定一个目录,从该目录提供文件服务。koa-send 会自动解析该路径,并去除前导的 /,以确保路径是相对的,并且不允许路径中包含 "..",以防止用户输入被串联。

示例用法

以下是一个简单的示例,展示了如何在 Koa 应用中使用 koa-send 来服务静态文件:

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

app.use(async (ctx) => {
  if ('/' == ctx.path) return ctx.body = 'Try GET /package.json';
  await send(ctx, ctx.path, { root: __dirname + '/public' });
});

app.listen(3000);
console.log('listening on port 3000');

在这个示例中,如果请求根路径 /,应用将返回一条消息提示尝试获取 package.json 文件。对于其他路径,koa-send 将尝试发送请求的文件。

源码注释


// 引入所需的npm包
const debug = require('debug')('koa-views'); // 调试模块,用于输出调试信息
const consolidate = require('@ladjs/consolidate'); // 模板引擎整合库
const send = require('koa-send'); // Koa文件发送中间件
const getPaths = require('get-paths'); // 用于获取模板文件路径的模块
const pretty = require('pretty'); // 美化HTML内容的模块
const resolve = require('resolve-path'); // 用于解析文件路径的模块

module.exports = viewsMiddleware; // 导出中间件

// 用于处理BigInt类型在JSON.stringify中的序列化问题
const bigIntReplacer = () => {
  const seen = new WeakSet(); // 使用WeakSet来记录已经遍历过的对象
  return (key, value) => {
    if (typeof value === 'bigint') return value.toString(); // 将BigInt转换为字符串

    if (typeof value === 'object' && value !== null) {
      if (seen.has(value)) {
        return; // 如果对象已经被遍历过,则跳过
      }
      seen.add(value); // 将对象添加到seen集合中
    }

    return value; // 返回原始值
  }
};

// 定义viewsMiddleware中间件函数,它接受模板路径和其他配置选项
function viewsMiddleware(
  path,
  {
    autoRender = true, // 是否自动渲染视图
    engineSource = consolidate, // 模板引擎的来源,默认为consolidate
    extension = 'html', // 默认的文件扩展名
    options = {}, // 传递给模板引擎的额外选项
    map // 用于指定不同文件扩展名对应的模板引擎
  } = {}
) {
  // 返回实际的Koa中间件函数
  return function views(ctx, next) {
    let extendsContext = false; // 标记是否扩展了应用上下文

    // 定义render函数,用于渲染模板
    function render(relPath, locals = {}) {
      // 根据上下文确定ctx变量
      if (extendsContext) {
        if (this.ctx && this.ctx.req === this.req) ctx = this.ctx;
        else ctx = this;
      }

      // 使用getPaths获取模板文件的完整路径
      return getPaths(path, relPath, extension).then(paths => {
        const suffix = paths.ext; // 获取文件扩展名
        const state = Object.assign({}, options, ctx.state || {}, locals); // 创建state对象,包含所有选项和上下文状态
        // 深度复制partials对象
        state.partials = Object.assign(Object.create(null), options.partials || {});
        // 如果debug开启,则输出调试信息
        if (debug.enabled) debug('render `%s` with %s', paths.rel, JSON.stringify(state, bigIntReplacer()));
        ctx.type = 'text/html'; // 设置响应类型为HTML

        // 检查文件扩展名是否为html,如果是且没有map指定引擎,则直接发送文件
        if (isHtml(suffix) && !map) {
          return send(ctx, paths.rel, {
            root: path
          });
        } else {
          // 根据文件扩展名或map配置获取对应的模板引擎名称
          const engineName = map && map[suffix] ? map[suffix] : suffix;
          const render = engineSource[engineName]; // 获取对应的渲染函数

          // 如果没有找到对应的引擎,则抛出错误
          if (!engineName || !render)
            return Promise.reject(
              new Error(`Engine not found for the ".${suffix}" file extension`)
            );

          // 使用模板引擎渲染模板
          return render(resolve(path, paths.rel), state).then(html => {
            // 如果设置了pretty选项,则使用pretty包美化HTML
            if (locals.pretty) {
              debug('using `pretty` package to beautify HTML');
              html = pretty(html);
            }

            // 如果设置了autoRender,则将渲染后的HTML设置为响应体
            if (autoRender) {
              ctx.body = html;
              return html;
            }

            // 如果没有设置autoRender,则返回Promise解析后的HTML
            return Promise.resolve(html);
          });
        }
      });
    }

    // 如果ctx未定义,则设置extendsContext为true,并返回render函数
    if (!ctx) {
      extendsContext = true;
      return render;
    }

    // 如果ctx已经定义了render函数,则直接执行下一个中间件
    if (ctx.render) return next();

    // 向ctx添加render方法,用于渲染模板
    ctx.response.render = ctx.render = render;

    // 执行下一个中间件
    return next();
  }
}

// 辅助函数,用于检查文件扩展名是否为html
function isHtml(ext) {
  return ext === 'html';
}

概括一下: koa-view返回一个函数,函数调用传入path和配置对象,返回值是view中间件函数,通过app.use注册中间件,此时在ctx中写入render函数:ctx.response.render = ctx.render = render;,当处理请求时,可以调用ctx.render处理模板,将处理后的模板返回会复制给body