著者:しょっさん
ソフトウエアを正しく作るために、エンジニアたちはどんなことを知らなければならないでしょうか。実際のコードを使って、より良くしていくためのステップを考えてみましょう。第7回は、JavaScriptフレームワーク「Vue.js」でアプリを作成し、テストとリリースの方法を紹介します。
シェルスクリプトマガジン Vol.67は以下のリンク先でご購入できます。![]()
![]()
図2 Vue.js および各ライブラリを利用するための定義(simple.htmlから抜粋)
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script src="https://unpkg.com/vue-router@3.3.2/dist/vue-router.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@0.19.2/dist/axios.min.js"></script>
<script src="./jwt-decode.min.js"></script>
図3 Vue RouterのHTMLファイル側の定義
<div id="app">
<h1>経費精算アプリケーション(Vue.js)</h1>
<router-link to="/expense">経費登録</router-link>
<router-link to="/payment">経費精算</router-link>
<router-link to="/login">ログイン</router-link>
<router-link to="/logout">ログアウト</router-link>
<router-view />
</div>
図4 Vue RouterのJavaScript側の定義
const router = new VueRouter({
routes: [
// 精算処理
{
path: "/expense",
},
// 支払処理
{
path: "/payment",
},
// 認証 - ログイン
{
path: "/login",
},
// 認証 - ログアウト
{
path: "/logout",
},
// どのURLにも該当しなかった場合
{
path: "*",
redirect: "/expense",
},
],
});
図5 認証部分のVueコンポーネントとVue Router定義
const Login = {
template: "#login",
data: function () {
return {
user: localStorage.user || "",
password: localStorage.password || "",
remember: localStorage.remember || false,
error: false,
};
},
methods: {
login: function () {
axios
.post(${baseUrl}/api/auth, {
user: this.user,
password: this.password,
})
.then((response) => {
if (response.status === 200) {
// ログインが成功した場合は、ローカルストレージにトークンを保管する(ログインが成功した状態とする)
this.error = false;
localStorage.token = response.data.token;
// "remember me" チェックボックスが付いていたら、各々の入力項目を保管する
if (this.remember) {
localStorage.user = this.user;
localStorage.password = this.password;
localStorage.remember = true;
} else {
// 逆にオフであれば入力項目の内容を破棄する
delete localStorage.user;
delete localStorage.password;
delete localStorage.remember;
}
// ログイン成功したら /expense へ切り替える
this.$router.replace("/expense");
} else {
this.error = true;
}
})
.catch((response) => {
this.error = true;
this.remember = false;
console.log(response);
});
},
},
};
const router = new VueRouter({
routes: [
(略)
// ログイン画面
{
path: "/login",
component: Login,
},
// ログアウト処理
{
path: "/logout",
beforeEnter: (to, from, next) => {
delete localStorage.token;
next("/login");
},
},
],
});
図6 認証部分のHTMLテンプレート
<script type="text/x-template" id="login">
<form @submit.prevent="login">
<input v-model="user" type="email" placeholder="Your Email" autofocus="" />
<input v-model="password" type="password" placeholder="Your Password" />
<button type="submit">ログイン</button>
</form>
<div v-show="error">
<p>ログイン失敗</p>
</div>
</script>
図7 請求処理ののVueコンポーネントとVue Router定義
const Expense = {
template: "#expense",
data: function () {
let decoded = {};
if (localStorage.token) {
// トークン内のユーザー情報を基に変数へ配置
decoded = jwt_decode(localStorage.token);
}
return {
user: decoded.email || "",
id: decoded.id || "",
user_name: decoded.user_name || "",
date: "",
type: "",
amount: "",
description: "",
error: false,
};
},
// 経費を登録するメソッド
methods: {
expense: function () {
axios
.post(
${baseUrl}/api/expense,
{
user: this.user,
user_id: this.id,
user_name: this.user_name,
date: this.date,
type: this.type,
amount: this.amount,
description: this.description,
},
{
headers: {
Authorization: Bearer ${localStorage.token},
},
}
)
.then((response) => {
if (response.status === 200) {
// 正常に登録できた場合は、変更が伴うフィールドをクリアーして、再度入力可能な状態にする
this.error = false;
console.log(response);
this.date = "";
this.type = "";
this.amount = "";
this.description = "";
}
})
.catch((response) => {
this.error = true;
console.log(response);
});
},
},
};
const router = new VueRouter({
routes: [
// 経費登録
{
path: "/expense",
component: Expense,
beforeEnter: (to, from, next) => {
// 認証前の場合は /login ページへ遷移する
if (!localStorage.token) {
next({
path: "/login",
query: { redirect: to.fullPath },
});
} else {
next();
}
},
},
(略)
});
図8 請求処理のHTMLテンプレート
<script type="text/x-template" id="expense">
<div>
<form @submit.prevent="expense">
<input v-model="user_name" type="text" placeholder="Your Name" />
<input v-model="user" type="email" placeholder="Your Email" />
<input v-model="id" type="hidden" placeholder="Your User ID" />
<input v-model="date" type="datetime-local" placeholder="経費利用日" autofocus="" />
<input v-model="type" type="text" placeholder="費目" />
<input v-model="amount" type="number" placeholder="金額" />
<input v-model="description" type="text" placeholder="費用詳細" />
<button type="submit">経費申請</button>
</form>
<p v-if="error" class="error">経費登録失敗</p>
</div>
</script>
図9 支払処理のVueコンポーネントとVue Router定義
// 経費リストを axios を利用して取得する関数
const getPayments = function (callback) {
axios
.get(${baseUrl}/api/payment, {
// JWTの認可ヘッダー
headers: {
Authorization: Bearer ${localStorage.token},
},
})
.then((response) => {
if (response.status === 200) {
// "response.data" 配下に経費リストが含まれる
callback(null, response.data);
} else {
callback(true, response);
}
})
.catch((response) => {
callback(true, response);
});
};
// 経費リスト用の Vueコンポーネント
const Payment = {
template: "#payment",
data: function () {
return {
loading: false,
error: false,
payments: function () {
return [];
},
};
},
// 初期化されたときにデータを取得する
created: function () {
this.fetchData();
},
// ルーティングが変更されてきたときに再度データを取得する
watch: {
$route: "fetchData",
},
// 経費データを取得するメソッドのメイン部分
methods: {
fetchData: function () {
this.loading = true;
getPayments(
function (err, payments) {
this.loading = false;
if (!err) this.payments = payments;
else this.error = true;
}.bind(this)
);
},
},
};
const router = new VueRouter({
routes: [
(略)
{
path: "/payment",
component: Payment,
beforeEnter: (to, from, next) => {
// 認証前の場合は /login ページへ遷移する
if (!localStorage.token) {
next({
path: "/login",
query: { redirect: to.fullPath },
});
} else {
next();
}
},
},
(略)
],
});
図10 支払処理のHTMLテンプレート
<script type="text/x-template" id="payment">
<div>
<table>
<tr>
<th>ユーザー名</th>
<th>発生日</th>
<th>費目</th>
<th>経費</th>
<th>詳細</th>
</tr>
<tr v-for="payment in payments">
<td>{{payment.user_name}}</td>
<td>{{payment.date}}</td>
<td>{{payment.type}}</td>
<td>{{payment.amount}}</td>
<td>{{payment.description}}</td>
</tr>
</table>
<div class="loading" v-if="loading">アクセス中...</div>
<p v-if="error" class="error">経費取得失敗</p>
</div>
</script>
図14 controllers/auth/authentication.tsファイルの追加・改修部分
import jwt from "jsonwebtoken";
export class Authentication {
(略)
static verifyLocal(username: string, password: string, done: any) {
User.findOne({
where: {
email: username,
deleted_at: null,
},
})
.then((user) => {
if (user && bcrypt.compareSync(password, user.hash)) {
const opts = {
issuer: process.env.ISSUER,
audience: process.env.AUDIENCE,
expiresIn: process.env.EXPIRES,
};
const secret: string = process.env.SECRET || "secret";
const token: string = jwt.sign(
{
email: user.email,
id: user.id,
user_name: user.last_name + " " + user.first_name,
},
secret,
opts
);
return done(null, token);
}
return done(true, "authentication error");
})
.catch((err) => done(true, err));
}
(略)
}
図15 controllers/auth/authorization.tsファイル
import { Request, Response, NextFunction } from "express";
import { User } from "../../models/user";
import passport from "passport";
import { Strategy as JWTStrategy, ExtractJwt } from "passport-jwt";
export class Authorization {
// JWTトークンで該当するユーザーの有無をチェック
static verifyJWT(req: Request, jwt_payload: any, done: any) {
User.findOne({
where: {
email: jwt_payload.email,
deleted_at: null,
},
}).then((user) => {
if (!user) return done(null, false);
return done(null, user.get());
});
}
// JWT Strategyの定義
static setJWTStrategy() {
const field = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
issuer: process.env.ISSUER,
audience: process.env.AUDIENCE,
secretOrKey: process.env.SECRET || "secret",
passReqToCallback: true,
};
passport.use(new JWTStrategy(field, this.verifyJWT));
}
// 認可チェック
static isAuthorized(req: Request, res: Response, next: NextFunction) {
passport.authenticate("jwt", { session: false }, (err, user) => {
if (err) {
res.status(401).json({ status: "10001" });
}
if (!user) {
res.status(401).json({ status: "10002" });
} else {
return next();
}
})(req, res, next);
}
}
図16 API用のモジュールとルーティング定義
// API用ルーティング先のモジュール
import auth from "./api/auth";
import payment from "./api/payment";
import expense from "./api/expense";
// APIルーティング
app.use("/api/auth", auth);
app.use("/api/expense", Authorization.isAuthorized, expense);
app.use("/api/payment", Authorization.isAuthorized, payment);
図18 請求処理(/api/expense.ts)
import { Request, Response, NextFunction } from "express";
import Express from "express";
import { Expense } from "../models/expense";
const router = Express.Router();
// POST 経費の入力
router.post("/", (req: Request, res: Response, next: NextFunction) => {
Expense.create(req.body)
.then((result) => {
res.status(200).json(result);
})
.catch((err) => {
console.log(err);
res.status(400).json({ id: 20002, message: err });
});
});
export default router;
図19 支払処理(/api/payment.ts)
import { Request, Response, NextFunction } from "express";
import Express from "express";
import { Expense } from "../models/expense";
const router = Express.Router();
// POST / ユーザーの認証処理
router.get("/", (req: Request, res: Response, next: NextFunction) => {
Expense.findAll()
.then((results) => {
res.status(200).json(results);
})
.catch((err) => {
res.status(400).json({ id: 20011, message: err });
});
});
export default router;
図35 修正した/src/index.tsファイル
import { Request, Response, NextFunction } from "express";
import Express from "express";
const app = Express();
import logger from "morgan";
import { Authentication } from "./controllers/auth/authentication";
import { Authorization } from "./controllers/auth/authorization";
app.use(logger("dev"));
app.use(Express.json());
app.use(Express.urlencoded({ extended: true }));
Authentication.initialize(app);
Authentication.setLocalStrategy();
Authorization.setJWTStrategy();
app.use(Express.static("htdocs"));
// API用ルーティング
import auth from "./api/auth";
import payment from "./api/payment";
import expense from "./api/expense";
// API
app.use("/api/auth", auth);
app.use("/api/expense", Authorization.isAuthorized, expense);
app.use("/api/payment", Authorization.isAuthorized, payment);
app.use((req: Request, res: Response, next: NextFunction) => {
var err: any = new Error("Not Found");
err.status = 404;
next(err);
});
// error handler
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
res.locals.message = err.message;
res.locals.error = req.app.get("env") === "development" ? err : {};
res.status(err.status || 500);
res.json(err);
});
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(http://localhost:${port});
});
export default app;