今回作ったもの
フォームの内容から領収書発行する機能
ソース
https://github.com/sauzar18/nuxt-pdfkit-receipt
環境
- Nuxt.js v2.4.5
- Express.js v4.16.4
- backpack-core
- moment.js
- csurf
- xss
- cookie-parser
- PDFKit
各モジュールのインストール
Nuxtのプロジェクト下で
yarn add backpack-core moment csurf xss pdfkit cookie-parser
backpack-coreの設定は下記参照
https://qiita.com/sauzar18/items/32a8553587e2bdcf3689
モジュールの説明
moment.js
日付と時刻を解析、検証、操作、表示をする
csurf
csrf対策のためにtoken発行をしてくれるExpress用のモジュール
xss
クロスサイトスクリプティング対策用のモジュール
cookie-parser
cookie系の操作をするExpress用のモジュール
PDFKit
Nodeおよびブラウザ用のPDFドキュメント生成ライブラリ
Nuxt.jsのプロジェクト内にあるserverフォルダ内の設定をしていきます。
フォームを安全に送信するためにcsrf対策を設定
参考記事:Nuxt.jsでcsrfTokenを埋め込みセキュアな通信を行う
https://qiita.com/tkow/items/fd0a5ce8ff3d6d17a6c1
これを参考にして
import express from 'express'
import consola from 'consola'
import { Nuxt, Builder } from 'nuxt'
import bodyParser from 'body-parser'
import cookieParser from 'cookie-parser'
import csrf from 'csurf'
const app = express()
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({
extended: true
}))
app.use(cookieParser())
app.use(csrf({ cookie: true }))
// Import and Set Nuxt.js options
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export const state = () => ({
csrfToken: null
})
export const mutations = {
SET_CSRF_TOKEN(state, csrfToken) {
state.csrfToken = csrfToken
}
}
export const actions = {
nuxtServerInit({ commit }, { req }) {
if (req.cookies) {
commit('SET_CSRF_TOKEN', req.csrfToken())
}
}
}
今回はaxios使わないのでaxiosの設定はなし
これでvueファイルでstoreを介してcsrf tokenを取得できるようになります。
次にvueファイルを構築していきます。
クライアントサイドの構築
<template>
<div class="container">
<h1 class="title">
Nuxt.js + pdfkitでフォームの内容をPDF出力
</h1>
<form
class="st-form"
action="/api/pdf"
method="POST"
>
<input
:value="$store.state.csrfToken"
type="hidden"
name="_csrf"
>
<div class="st-datefield">
<label for="issue">発行日</label>
<input
id="issue"
type="date"
name="issue"
required
aria-required="true"
>
</div>
<div class="st-textfield">
<label for="client_name">会社名</label>
<input
id="client_name"
type="text"
name="client_name"
>
</div>
<div class="st-textfield">
<label for="charge">担当者名</label>
<input
id="charge"
type="text"
name="charge"
>
</div>
<div class="st-textfield">
<label for="content">但し書き</label>
<input
id="content"
type="text"
name="content"
>
</div>
<div class="st-textfield">
<label for="price">金額</label>
<input
id="price"
type="number"
name="price"
>
</div>
<div class="st-textfield">
<label for="bikou">備考</label>
<textarea
id="bikou"
name="bikou"
cols="30"
rows="10"
/>
</div>
<button type="submit">
発行
</button>
</form>
</div>
</template>
<style>
.container {
width: 80%;
margin: 60px auto;
}
.title {
font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
display: block;
font-weight: 300;
font-size: 24px;
color: #35495e;
letter-spacing: 1px;
margin-bottom: 20px;
text-align: center;
}
form {
width: 480px;
padding: 20px;
border: 1px solid #35495e;
margin: 0 auto;
}
.st-textfield {
margin-bottom: 20px;
display: flex;
flex-direction: column;
}
input,
textarea {
border: 1px solid #35495e;
}
.st-textfield input {
height: 36px;
padding: 0 4px;
}
button {
cursor: pointer;
background-color: #35495e;
color: #fff;
width: 120px;
height: 40px;
border: none;
border-radius: 2px;
box-shadow: 0 2px 6px rgba(0,0,0,0.16);
display: block;
margin: 0 auto;
}
button:hover,
button:focus {
background-color: #43607c;
}
button:active {
box-shadow: none;
}
.st-datefield {
border-bottom: 1px solid #43607c;
margin-bottom: 10px;
}
.st-datefield input {
border: none;
height: 36px;
}
</style>
やっていること
- formのactionでapiにむけてデータ送信
- csrfをstoreから取得
こんな感じでクライアントサイドの構築
PDFKitにデータを受け取るapiを構築
serverフォルダ下にapiフォルダを作成しapiを管理
/server
- index.js
- api
- index.js
- pdf.js
こんな感じでディレクトリをつくります。
1. apiを読み込み設定
import express from 'express'
import consola from 'consola'
import { Nuxt, Builder } from 'nuxt'
import bodyParser from 'body-parser'
import cookieParser from 'cookie-parser'
import csrf from 'csurf'
import api from './api' // 追加
const app = express()
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({
extended: true
}))
app.use(cookieParser())
app.use(csrf({ cookie: true }))
app.use('/api', api) // 追加
// Import and Set Nuxt.js options
const config = require('../nuxt.config.js')
config.dev = !(process.env.NODE_ENV === 'production')
async function start() {
// Init Nuxt.js
const nuxt = new Nuxt(config)
const {
host = process.env.HOST || '127.0.0.1',
port = process.env.PORT || 3000
} = nuxt.options.server
// Build only in dev mode
if (config.dev) {
const builder = new Builder(nuxt)
await builder.build()
} else {
await nuxt.ready()
}
// Give nuxt middleware to express
app.use(nuxt.render)
// Listen the server
app.listen(port, host)
consola.ready({
message: `Server listening on http://${host}:${port}`,
badge: true
})
}
start()
2. apiを管理する設定
import { Router } from 'express'
import pdfContent from './pdf'
const router = Router()
router.use(pdfContent)
export default router
pdfkitの処理を書いたファイルを読み込んでいる。
3. pdfkitの設定
import { Router } from 'express'
import PDFDocument from 'pdfkit'
import moment from 'moment'
import xss from 'xss'
const router = Router()
router.post('/pdf', (req, res) => {
const doc = new PDFDocument({ size: 'A4', margin: 50 })
const client = xss(req.body.client_name)
const charge = xss(req.body.charge)
const content = xss(req.body.content)
const price = xss(req.body.price)
const bikou = xss(req.body.bikou).replace(/\r?\n/g, '\n')
const planTotal = Number(price) * 1.08
const issue = moment(xss(req.body.issue)).format('YYYY年MM月DD日')
let filename = '領収書-' + client + '様'
filename = encodeURIComponent(filename) + '.pdf'
res.setHeader('Content-disposition', 'attachment; filename="' + filename + '"')
res.setHeader('Content-type', 'application/pdf')
doc.font('./static/fonts/meiryo.ttf')
doc.pipe(res)
doc.y = 300
doc.fontSize(10).text(issue, 389, 150, { align: 'right', width: 150 })
doc.fontSize(18).text('領 収 書', 0, 80, { width: 600, align: 'center' })
doc.fontSize(10)
doc.text(charge, 50, 175)
doc.fontSize(12)
doc.text('様', 304, 175)
doc.moveTo(50, 188).lineTo(300, 188).stroke()
doc.fontSize(10)
// left block
doc.fontSize(14).text('領収金額 ' + '¥' + Number(planTotal).toLocaleString() + '-', 0, 220, { width: 600, align: 'center' })
doc.moveTo(150, 248).lineTo(450, 248).stroke()
doc.moveDown(1)
doc.fontSize(10)
doc.text('但し、', 160, 264).stroke()
doc.text('として', 402, 264).stroke()
doc.moveDown(1)
doc.text(content, 0, 264, { align: 'center', width: 600 }).stroke()
doc.moveTo(200, 278).lineTo(400, 278).stroke()
doc.text('上記金額を正に受領いたしました。', 0, 298, { align: 'center', width: 600 })
// right block
doc.fontSize(10)
doc.text('〒000-000', 335, 430, { align: 'left', width: 170 })
doc.moveDown(0.1)
doc.text('東京都○○区')
doc.moveDown(0.1)
doc.text('TEL:00-0000-0000')
doc.moveDown(0.5)
doc.fontSize(12)
doc.text('株式会社○○')
doc.moveTo(75, 430).lineTo(150, 430).dash(1, { space: 1 }).stroke()
doc.moveTo(75, 430).lineTo(75, 505).stroke()
doc.moveTo(150, 430).lineTo(150, 505).stroke()
doc.moveTo(75, 505).lineTo(150, 505).stroke()
doc.fontSize(15)
doc.text('印紙', 75, 458, { align: 'center', width: 75 })
doc.fontSize(10)
doc.text('備考', 50, 564).stroke()
doc.moveDown(0.1)
doc.moveTo(50, 580).lineTo(540, 580).dash(0, { space: 0 }).stroke()
doc.moveTo(50, 580).lineTo(50, 700).stroke()
doc.moveTo(540, 580).lineTo(540, 700).stroke()
doc.moveTo(50, 700).lineTo(540, 700).stroke()
doc.text(bikou, 60, 590).stroke()
doc.fontSize(15)
doc.fillColor('black')
doc.end()
})
export default router
ここでやっていること
- bodyParserでフォームの内容を取得
- moment.jsでに日時を整形
- xssでフォームから受け取った値のクロスサイトスクリプティング対策
- PDFKitでPDF出力の形をつくる
これでバックエンドの設定は完了
これでPDFが取得されます。
PDFKitの使い方
PDFDocument
PDFDocumentインスタンスを作成する。ここでファイルのサイズなどを設定できる。
今回はA4サイズに設定
font
フォントの設置。
標準のフォントだと日本語が文字化けするので、今回はメイリオを読み込んでいる
pipe
Nodeストリームに送信するメソッドを呼び出す。
HTTP responseを今回は呼び出している
y
y軸からどの距離に描写するか宣言
x
x軸からどの距離に描写するか宣言
fontSize()
フォントサイズを設定。
文字を変更するごとに都度設置が必要
text
文字の設置をするための設定
doc.fontSize(18).text('領 収 書', 0, 80, { width: 600, align: 'center' })
フォントサイズ18の領収書という文字列をx軸0y軸80の位置に横幅600で中央揃えで配置
moveTo
ベクターグラフィックを描写するポイントを設定
lineTo
線を描写の宣言
stroke
図であることを指定
moveTo + lineTo + stoke
doc.moveTo(50, 188).lineTo(300, 188).stroke()
y軸50から300まで線をx軸188に描写
moveDown
文字を改行できる。値は改行のスペース(文字の大きさに対して)
image
画像を読み込める
doc.image('./static/images/ic_logo.png', 50, 50, { width: 130 })
こんな感じで読み込めます。
addPage()
ページを追加。
この後に設定をするとページが変わる
end()
PDFの描写を終了
ドキュメントに詳しく載っているので、ちゃんと理解したいならそちらをみながらやってみてください。
まとめ
PDFKitでPDFを出力できるのはいいけど、形を作るまでが鬼大変。
ある程度どういった形にするかデザインして、例えば、A4ならどの位置に文字、線、画像を設置するかを細かく設計しないとって感じです。
Node環境でないと使えないので注意