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