Help us understand the problem. What is going on with this article?

ejs のテンプレート内で await する

More than 1 year has passed since last update.

今時 ejs !?という声はさておき…
ひさしぶりに ejs をいじっていたところ、テンプレートの中からリモートサーバの API にアクセスし、ダウンロードしたデータを HTML に加工したい欲求がわきました。
こういった時は ejs.render() にデータを渡して使うのがセオリーですが、 .ejs ファイルが多数ある場合、ファイルが要求するデータを逐一選定して渡すというのは現実的ではありません(私の場合)。

そこで、 include() が URL に対応していればなぁ、と思いつつ色々試していると、こんなエラーメッセージが。

If the above error is not helpful, you may want to try EJS-Lint:
https://github.com/RyanZim/EJS-Lint
Or, if you meant to create an async function, pass async: true as an option.

どうも ejs.render()async: true を渡すとテンプレート内で await できるようになる模様。
そんなオプションあったっけ?( ウェブサイト を見る)ありますね。あれぇ?

axios を間接的に呼び出す例

ejs の呼び出し元で axios を使う関数を定義し、それをテンプレートに渡します。

const axios = require('axios')
const ejs = require('ejs')

const request = async config => {
  const response = await axios(config)
  return response.data
}

// async 関数内に記述する想定
const html = await ejs.render(text, { request }, { async: true })

テンプレートでは request() 関数に await を付けて呼び出すだけ。

<% const data = await request({
  method: 'get',
  url: 'https://api.mymemory.translated.net/get',
  params: {
    q: 'こんにちは最近は暑いですね。',
    langpair: 'ja|en'
  }
}) %>
<% if (data) { %>
  <!-- 煮るなり焼くなり好きにする -->
<% } %>

任意の JavaScript ファイルを実行する例

かなり荒っぽいですが、こういうこともできます。

const ejs = require('ejs')
const path = require('path')

const execute = async relativePath => {
  const absolutePath = path.resolve(__dirname, `./methods/${relativePath}`)
  const method = require(absolutePath)
  return await method()
}

// async 関数内に記述する想定
const html = await ejs.render(text, { execute }, { async: true })

使い方は同じ。

<% const data = await execute('hoge/fuga.js') %>
<% if (data) { %>
  <!-- 煮るなり焼くなり好きにする -->
<% } %>

hoge/fuga.js では module.exports に関数を設定する前提です。

ところがぎっちょん

後で気付いたんですが、どうも { async: true } を設定すると include() が Promise を返すようになってしまうようです。 await してもエラーになります。つまり実質的に include() が使えません。
色々試した結果、あまり気は進みませんが、擬似的な include() を作ることにしました。

// アロー関数ではない点に注意
const includeSync = function (relativePath, option) {
  const srcDir = path.dirname(this.srcPath)
  const absolutePath = path.resolve(srcDir, relativePath)
  const text = fs.readFileSync(absolutePath, 'utf8')
  const nextOption = Object.assign({ srcPath: absolutePath, includeSync }, option)
  const html = ejs.render(text, nextOption, { filename: absolutePath })
  return html
}

ただし、 ejs.render() のオプションに srcPath として .ejs ファイルの絶対パスを付与する必要があります。

const html = await ejs.render(text, { srcPath, includeSync, execute, request }, { async: true, filename: srcPath })

使い方はほぼ同じです。

<%- includeSync('./ejs/_component.ejs', { hoge }) %>

includeSync() の読込先でも includeSync() は使えますが、 execute()request() といった非同期関数は使えません。もうこれはどうしようもないかと…。

おまけ

Promise エラーが発生しやすくなるので、処理を止めたくなければ下記コードを記述しておくと良いかと。

process.on('uncaughtException', console.error)
process.on('unhandledRejection', console.error)

本当は個別に catch() を書いてハンドリングするべきなんですけどね🤮

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away