概要
PDFをサーバサイドで作成し、ダウンロードするAPIを作成した。
ローカルで動いていたものがデプロイ時に動かないなどのトラブルがあったが、
最終的には成功できた。
ローカル環境
- node:13.12.0
- Now CLI 17.1.1
フォルダ構成
- api
- pdf.ts
- fonts
- ipaexg.ttf
- ipaexm.ttf
- src
- pages
- index.tsx
- PdfArea.tsx
- .babelrc.js
- next-env.d.ts
- next.config.js
- now.json
- package.json
- tsconfig.json
ハマった点
APIをsrc/pages/api/pdf.ts
に書いていたとき、ローカルで動かすときには成功し、デプロイしたときにエラーとなる現象が発生した。
デプロイ時にはfonts
フォルダが見えなくてファイルが開けない問題が発生していた。
apiはソース通りのフォルダ構成ではなく、ラムダ環境にコンパイル済で配置されていたことが原因と考えられる。
apiファイルの場所を、src/pages/api
から直下のapi
に移し、now.jsonにincluceFiles
を追加することで解決した。
now.json
{
"version": 2,
"functions": {
"api/pdf.ts": {
"includeFiles": "fonts/**"
}
}
}
incluceFiles
を使うときは、nextの仕組みのpages/api
は使えない模様。
以下では、コンパイルは通るが、フォルダがアップロードされなかった。
now.jsonのうまくいかなかった例
{
"version": 2,
"functions": {
"src/pages/api/pdf.ts": {
"includeFiles": "fonts/**"
}
}
}
ファイル
src/pages/index.tsx
import Head from 'next/head'
import * as React from 'react'
import { NextPage } from 'next'
import PdfArea from './PdfArea'
const Home: NextPage = () => {
return (
<div className="container">
<main>
<PdfArea />
</main>
</div>
)
}
export default Home
src/pages/PdfArea.tsx
import React, { useState, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import scenarioModule, {
useScenario,
usePdf
} from '../store/modules/scenarioModule'
const makePdf = async (scenario, dispatch) => {
const pdf = await (
await fetch('/api/pdf', {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=utf-8' },
body: JSON.stringify(scenario)
})
).text()
dispatch(scenarioModule.actions.setPdf(pdf))
}
const PdfArea: React.FC = () => {
const scenario = useScenario()
const pdf = usePdf()
const dispatch = useDispatch()
if (!scenario) {
return <div>読込失敗</div>
}
return (
<>
<button onClick={(e) => makePdf(scenario, dispatch)}>PDFを作る</button>
{pdf !== '' && (
<a href={pdf} download="scenario.pdf">
作成したPDFをダウンロード
</a>
)}
</>
)
}
export default PdfArea
api/pdf.ts
import { NextApiRequest, NextApiResponse } from 'next'
import path from 'path'
import PdfPrinter from 'pdfmake'
import { Scenario } from '../src/store/modules/scenarioModule'
function createPdfBinary(pdfDoc, callback) {
const baseDir = 'fonts/'
const gPath = path.resolve(baseDir + 'ipaexg.ttf')
const mPath = path.resolve(baseDir + 'ipaexm.ttf')
const fontDescriptors = {
IPASerif: {
normal: mPath,
bold: mPath,
italics: mPath,
bolditalics: mPath
},
IPAGothic: {
normal: gPath,
bold: gPath,
italics: gPath,
bolditalics: gPath
}
}
const printer = new PdfPrinter(fontDescriptors)
const doc = printer.createPdfKitDocument(pdfDoc)
const chunks = []
doc.on('data', function(chunk) {
chunks.push(chunk)
})
doc.on('end', function() {
const result = Buffer.concat(chunks)
callback('data:application/pdf;base64,' + result.toString('base64'))
})
doc.end()
}
export default (req: NextApiRequest, res: NextApiResponse) => {
const scenario: Scenario = req.body
const docDefinition = {
content: [
{ text: scenario.title, fontSize: 55, font: 'IPASerif' },
],
defaultStyle: {
font: 'IPAGothic',
alignment: 'center'
}
}
createPdfBinary(docDefinition, (binary) => {
res.setHeader('Content-Type', 'application/json')
res.status(200).send(binary)
})
}
tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/*": [
"./src/*"
],
},
"target": "es6",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"exclude": [
"node_modules"
],
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
]
}
next.config.js
const { resolve } = require('path')
const withOffline = require('next-offline')
const nextConfig = {
webpack: (config) => {
// src ディレクトリをエイリアスのルートに設定
config.resolve.alias['~'] = resolve(__dirname, 'src')
return config
},
// manifest設定
target: 'serverless',
transformManifest: (manifest) => ['/'].concat(manifest),
generateInDevMode: true,
workboxOpts: {
swDest: 'static/service-worker.js',
runtimeCaching: [
{
urlPattern: /^https?.*/,
handler: 'NetworkFirst',
options: {
cacheName: 'https-calls',
networkTimeoutSeconds: 15,
expiration: {
maxEntries: 150,
maxAgeSeconds: 30 * 24 * 60 * 60 // 1 month
},
cacheableResponse: {
statuses: [0, 200]
}
}
}
]
}
}
// PWA に対応
module.exports = withOffline(nextConfig)
.babelrc.js
// ローカルの開発サーバー側の SSR 時と クライアント側のCSR 時に styled-components が付与するクラス名に差が生まれるエラーの対応
module.exports = {
presets: ['next/babel'],
plugins: [
['styled-components', { ssr: true, displayName: true, preprocess: false }]
]
}
参考
including-additional-files
pdfmake
IPAex フォント ダウンロードページ
IPAex フォント ダウンロードページ
Zeit Now の具体的な Tips 集
Javascript で PDF を作成する