Edited at

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

今時 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() を書いてハンドリングするべきなんですけどね🤮