前置き
タイトルは釣りです。リモートワーク関連に期待された方はすみませんorz
内容は一年前に作った動的っぽい挙動をする静的サイトをwebアプリにしたという内容です。ご了承ください。
↓前回
【客先常駐SES向け時給シミュレーターを作る(やだ・・・あたしの年収低すぎ・・・?)+転職についての雑記】
https://qiita.com/lunalice/items/c788b82ba52ad940ac9c
最近また転職について考える事があって、前回と違って今回はそれなりにエンジニア職っぽい事を経験してやれる事は増えてるはずなんですが、プロジェクト内のコミットは持ってこれないし、何かしら作れるという事をアッピルするために何かちょいちょいコミットしておきたいなぁと思って今回2回目の作成物です、1回目はまた別記事にする予定。
本題
先に作成物から。
¥Time-Is-Money$
https://re-time-is-money.herokuapp.com/
31才さんのコスパは...
— やまP (@yamashitaP21) August 5, 2020
時給:2634円
日給:21073円
通勤時間(年):174時間
年間消費金額:513316円
実収入:4986684円
https://t.co/wE38fSzDaT #コスパシミュ
ソースコードはこちらにございます。
https://github.com/lunalice/time-is-money
いつも業務で使ってるのはRuby on Railsなんですが、前回転職の際pythonかrubyか迷って求人数からrubyを選択した経緯があって、でも自分の好きなように作るならpythonにしました。で、フルスタックなフレームワークよりミニマムでいいかなと思ってFlaskを選択。フロントはちょっと調べてvue-cliが楽そうなのでそうしました。業務でもvue使ってますが設定関係が凄く面倒臭い・・・
デザインセンスがないので本当どうにかしたいお気持ち表明。
deploy先は全てにおいて無料を貫くつもりだったのでherokuにしました、楽なので。
作ろうと思って本当に合間合間でしたが(休日はほぼゲームしてた)二週間くらいで形にはなってくれました。
簡単に環境をまとめると以下となります。同環境で作成する方の参考になればと思います。
- @vue/cli 4.4.6
- python-3.7.8
- heroku
- pipenv
- その他ライブラリいろいろ
- 鯖代無料で作る!
前回について補足
前回作成した環境は記事を見て頂いてもろた方がいいんですが、こんな環境で作成してました。
- 客先常駐
- 外部インターネットが使えない。
- Windows(旧版)
- 作業自体は楽だったり待ちの時間が多い環境
- 開発環境が無い
そんな中出来上がったソースコードはこれ
長すぎるので見なくてもよきソースコード
| <!DOCTYPE html> |
|:--|
| <html lang="ja"> |
| <meta charset="Shift_JIS"> |
| |
| <head> |
| <title>時給シミュレーター</title> |
| <link rel="stylesheet" type="text/css" href="./css/main.css"> |
| </head> |
| |
| <body> |
| <!-- タイトル --> |
| <h1><center>時給シミュレーター</center></h1> |
| <center> |
| <table> |
| <form id="inputbox"> |
| <tr> |
| <th>年齢</th> |
| <td><input type="text" required placeholder="何歳?" id="input_nen"/></td> |
| <td class="soe">歳</td> |
| </tr> |
| <tr> |
| <th>年収</th> |
| <td><input type="text" required placeholder="5000兆円?" id="input_money"/></td> |
| <td class="soe">万円</td> |
| </tr> |
| <tr> |
| <th>休日(年)</th> |
| <td><input type="text" required placeholder="何日?" id="input_rest"/></td> |
| <td class="soe">日</td> |
| </tr> |
| <tr> |
| <th>勤務時間(日)</th> |
| <td><input type="text" required placeholder="何時間?" id="input_dotime"/></td> |
| <td class="soe">時間</td> |
| </tr> |
| <tr> |
| <th>残業時間(年)</th> |
| <td><input type="text" required placeholder="何時間?" id="input_overtime"/></td> |
| <td class="soe">時間</td> |
| </tr> |
| <tr> |
| <th>通勤時間(片)</th> |
| <td><input type="text" required placeholder="何分?" id="input_rostime"/></td> |
| <td class="soe">分</td> |
| </tr> |
| <tr> |
| <th>家賃(月)</th> |
| <td><input type="text" required placeholder="何円?" id="input_rosmoney"/></td> |
| <td class="soe">円</td> |
| </tr> |
| </form> |
| </table> |
| <input type="submit" value="計算" id="zikko" onclick="simulate();return false;"/> |
| </center> |
| <!-- 結果を挿入するようにする --> |
| <center><div id="result"><div></center> |
| <center><table id="data_list"></table></center> |
| |
| <script type="text/javascript"> |
| // 位置取得用定数 |
| var str_path = location.pathname; |
| var str_array = str_path.split("/"); |
| var input_list_path = str_path.replace(str_array.pop(),"").substring(1).replace("/","\/") + "data\/inputdata.txt"; // スクリプト実行フォルダの取得 |
| |
| // オープン時、画面を描写する:完 |
| window.onload=function(){ |
| readRecord(); |
| } |
| |
| // 時給計算したり損失金額を考えたり:完 |
| function simulate(){ |
| var input_nen = document.getElementById("input_nen").value; // 年齢 |
| var input_money = document.getElementById("input_money").value; // 年収 |
| var input_rest = document.getElementById("input_rest").value; // 休日 |
| var input_dotime = document.getElementById("input_dotime").value; // 勤務時間 |
| var input_overtime = document.getElementById("input_overtime").value; // 残業時間 |
| var input_rostime = document.getElementById("input_rostime").value; // 通勤時間 |
| var input_rosmoney = document.getElementById("input_rosmoney").value; // 家賃 |
| simulate_output(input_nen,input_money,input_rest,input_rest,input_dotime,input_overtime,input_rostime,input_rosmoney); |
| insertRecord(); //挿入処理 |
| } |
| |
| // 出力:完 |
| function simulate_output(input_nen,input_money,input_rest,input_rest,input_dotime,input_overtime,input_rostime,input_rosmoney){ |
| var str_edit = ""; |
| var result = exRound(keisan(input_money,input_rest,input_dotime,input_overtime),1); // 時給 |
| str_edit = " " + input_nen + "歳さんの時給は <font color=\"red\">" + result + "</font> 円です。</br>"; |
| str_edit = str_edit + "日給は <font color=\"red\">" + exRound((input_money * 10000) / activeDays(input_rest),1) + "</font> 円(稼働 <font color=\"red\">" + exRound((Number(input_dotime) + Number(input_overtime / activeDays(input_rest))),2) + "</font> 時間)です。</br>"; |
| var result_ros = exRound(ros_keisan(result,input_rostime,input_rest),1); // 年間損失金額 |
| str_edit = str_edit + "通勤時間は年間 <font color=\"red\">" + exRound(rosTime(input_rostime,input_rest),1); |
| str_edit = str_edit + "</font> 時間で <font color=\"red\">" + result_ros + "</font> 円消費しています。</br>"; |
| var yachin = input_rosmoney * 12; //家賃 |
| str_edit = str_edit + "家賃(<font color=\"red\">" + yachin + "</font>円)合わせ年間 <font color=\"red\">" + exRound(Number(result_ros) + yachin,1) + "</font> 円消費しています。</br>"; |
| str_edit = str_edit + "年間収入は (<font color=\"red\">" + input_money*10000 + "</font>円 - <font color=\"red\">" + exRound(Number(result_ros) + yachin,1) + "</font>円) = <font color=\"red\">" + (input_money*10000 - exRound(Number(result_ros) + yachin,1)) + "</font>(残業分<font color=\"red\">" + (input_overtime * result) + "</font>)円です!</br></br>"; |
| str_edit = str_edit + "節約ヒント:家賃を上げて通勤時間を下げてみよう!</br>"; |
| str_edit = str_edit + "※時間のみ想定している為、光熱費・食費等は考慮しておりません。</br>"; |
| document.getElementById("result").innerHTML = str_edit; |
| } |
| |
| // 指定した桁数まで四捨五入する:完 |
| function exRound(input_number,input_keta){ |
| return Math.round(input_number*Math.pow(10,input_keta))/Math.pow(10,input_keta); |
| } |
| |
| // 時給計算:完 |
| function keisan(input_money,input_rest,input_dotime,input_overtime){ |
| var activeday = activeDays(input_rest); // 勤務日数 |
| var day_money = (input_money * 10000) / activeday; // 日給 |
| var day_time = Number(input_dotime) + (input_overtime / activeday); // 1日の稼働 |
| return ( day_money / day_time ); // 時給 |
| } |
| |
| // 勤務日数:完 |
| function activeDays(rest){ |
| return (365-rest); |
| } |
| |
| // 年間損失金額計算:完 |
| function ros_keisan(result,input_rostime,input_rest){ |
| return (result * rosTime(input_rostime,input_rest)); |
| } |
| |
| // 年間通勤時間:完 |
| function rosTime(input_rostime,input_rest){ |
| return (input_rostime * 2 / 60) * (365 - input_rest); |
| } |
| |
| // linkの挿入機能:完 |
| function insertRecord(){ |
| // テキストに追記する。 |
| var fs = new ActiveXObject("Scripting.FileSystemObject"); |
| // 1:読み込み |
| var file_path = fs.openTextFile(input_list_path,1,false,0); |
| var string_array = file_path.AtEndOfStream ? "" : file_path.ReadAll(); // 全て読み込み |
| string_array = string_array.split(","); // 配列格納 |
| file_path.close(); |
| var createID = string_array.length != 0 ? 1 + (string_array.length - 1) / 8 : 1; // ユニークIDを振る |
| var file_path = fs.openTextFile(input_list_path,8,false,0); // 8は追記。 |
| var str_record = createID + "," + document.getElementById("input_nen").value + "," + document.getElementById("input_money").value + "," + document.getElementById("input_rest").value + ","; |
| str_record = str_record + document.getElementById("input_dotime").value + "," + document.getElementById("input_overtime").value + "," + document.getElementById("input_rostime").value + ","; |
| str_record = str_record + document.getElementById("input_rosmoney").value + ","; // 家賃 |
| file_path.write(str_record); |
| file_path.close(); |
| readRecord() // 画面更新 |
| } |
| |
| // linkの削除機能:完 |
| function deleteRecord(){ |
| // テキストを最初に取得しておく。 |
| var fs = new ActiveXObject("Scripting.FileSystemObject"); |
| // 1:読み込み |
| var file_path = fs.openTextFile(input_list_path,1,false,0); |
| var string_array = file_path.ReadAll(); // 全て読み込み |
| file_path.close(); |
| var array_buff = string_array.split(","); |
| // data_listから要素を取得する。 |
| var data_list = document.getElementById("data_list"); |
| for (var i = 1;i<data_list.rows.length;i++){ |
| // チェックボックス付けたものを削除する。 |
| if (data_list.rows(i).getElementsByTagName("input")[0].checked == true){ |
| data_list.rows(i).getElementsByTagName("input")[0].checked = false; |
| // 横ループ |
| var str_buff = ""; |
| for(var j = 0;j<data_list.rows[i].cells.length;j++){ |
| str_buff = str_buff + data_list.rows[i].cells[j].innerText + ","; |
| } |
| alert(str_buff + "を削除します。"); |
| string_array = string_array.replace(str_buff, ""); // 削除処理 |
| i = i - 1; // 位置調整 |
| } |
| } |
| // テキスト書き込み |
| file_path = fs.openTextFile(input_list_path,2,false,0); |
| file_path.write(string_array); |
| file_path.close(); |
| // 画面描写更新 |
| readRecord(); |
| } |
| |
| // linkの読み込み機能:完 |
| function readRecord(){ |
| // 描写リセット |
| document.getElementById("data_list").innerHTML=""; |
| var fs = new ActiveXObject("Scripting.FileSystemObject"); |
| // 1:読み込み |
| var file_path = fs.openTextFile(input_list_path,1,false,0); |
| var string_array = file_path.AtEndOfStream ? "" : file_path.ReadAll(); // 全て読み込み |
| var string_array = string_array.split(","); // 配列格納 |
| // 項目作成 |
| var tr = document.createElement("tr"); |
| tr.innerHTML = "<tr><th></th><th>ID</th><th>年齢</th><th>年収</th><th>休日</th><th>勤務時間</th><th>残業時間</th><th>通勤時間</th><th>家賃</th></tr>"; // ヘッダー |
| document.getElementById("data_list").appendChild(tr); |
| // html描写処理 |
| for (var i=0;i<string_array.length-1;i=i+8){ |
| var tr = document.createElement("tr"); |
| tr.innerHTML = "<input name=\"selectTarget\" type=\"radio\" onChange=\"checkAdd();\"/>"; |
| // テーブル結合 |
| for (var j=0;j<8;j++){ |
| tr.innerHTML = tr.innerHTML + "<td>" + string_array[i+j] + "</td>"; |
| } |
| tr.innerHTML = tr.innerHTML + "<input type=\"submit\" value=\"削除\" onclick=\"deleteRecord();\"/>"; |
| document.getElementById("data_list").appendChild(tr); |
| } |
| file_path.close(); |
| } |
| |
| // checkbox処理:完 |
| function checkAdd(){ |
| // data_listから要素を取得する。 |
| var data_list = document.getElementById("data_list"); |
| for (var i = 1;i<data_list.rows.length;i++){ |
| // チェックボックス付けたものをキャッチする。 |
| if (data_list.rows(i).getElementsByTagName("input")[0].checked == true){ |
| // 横ループ |
| for(var j = 0;j<data_list.rows[i].cells.length;j++){ |
| switch(j){ |
| case 1: |
| var input_nen = data_list.rows[i].cells[j].innerHTML; // 年齢 |
| break; |
| case 2: |
| var input_money = data_list.rows[i].cells[j].innerHTML; // 年収 |
| break; |
| case 3: |
| var input_rest = data_list.rows[i].cells[j].innerHTML; // 休日 |
| break; |
| case 4: |
| var input_dotime = data_list.rows[i].cells[j].innerHTML; // 勤務時間 |
| break; |
| case 5: |
| var input_overtime = data_list.rows[i].cells[j].innerHTML; // 残業時間 |
| break; |
| case 6: |
| var input_rostime = data_list.rows[i].cells[j].innerHTML; // 通勤時間 |
| break; |
| case 7: |
| var input_rosmoney = data_list.rows[i].cells[j].innerHTML; // 家賃 |
| break; |
| } |
| } |
| } |
| } |
| simulate_output(input_nen,input_money,input_rest,input_rest,input_dotime,input_overtime,input_rostime,input_rosmoney); //表示 |
| } |
| |
| </script> |
| <footer> |
| <center><p>Copyright (C) 2018 時給シミュレーター 辛い人</p></center> |
| </footer> |
| </body> |
| </html> |
当時の自分に言いたい、きったないし色々間違ってる!
まぁ調べ物も出来ないし確かメモ帳かサクラエディタで書いてたのでフォーマッターとかない状況でよく書いたと思います。ライブラリも何もない状態でhtmlだけでDBのCRAD再現してるの頭おかしいと思います。
これをwebアプリにreplaceする感じで作成していきます。
で、一から説明も長くなるし先人が大量に情報を残してある状態なので、参考リンクを張りつつ、詰まった部分やポイントと思ったとこ、大変だった所だけ記載していこうかと思います。
【参考】
FlaskとVue.jsでSPA Webアプリ開発
https://qiita.com/y-tsutsu/items/67f71fc8430a199a3efd
Vue.js + FlaskでWebアプリケーション制作 - herokuにデプロイするまで -
https://qiita.com/Nonta0605/items/5d8fa9a8eda9b3b7bc33
Vue.js(vue-cli)とFlaskを使って簡易アプリを作成する【前半 - フロントエンド編】
https://qiita.com/mitch0807/items/2a93d93adbf6b5fc445c
詰まった所
まず先に結論から申し上げると以下を使えばおこらなかった可能性が高いので、もし同環境で作りたい方は使用をおすすめ致します。
https://github.com/gtalarico/flask-vuejs-template
旧仕様から現仕様の置き換えが大変だった
前回作ったものをベースにしたのですが、javascriptに見えてWScriptというものだと思います、確か。
普通にvueにぺたっと貼り付けても使えないのとそもそもファイルの読み書きでCRADを表現してましたが全然いらない処理ですし、今回は文明の力、フォーマッター入れててめっちゃエラーはいてくるし実はアイデアだけ持ってきて1から記載しました。今回から得られた教訓は__現行踏襲って大変だよね。__
Vue-cliは最新のものを使う
凄く初歩的な話ですが、参考記事そのままの環境ではダメです。
このアプリを作り直す際、気軽な気分で始めたのもあるんですが、frontendは進化の早い分野ですので、バージョンの違いが挙動を著しくバグらせます。
自分がやらかしたのはvue-cli 3.0.0(現在4👆)で作り始めて、vue-cli触った事なかったのもあって知らずに、やたらとwebpackがエラーふくなぁこんなもんかなぁとpackage.jsonのupdateを始めてしまいました(業務でやったので・・・苦行やん・・・)いやいや、開発楽にする為のvue-cliなのにおかしいやろと現実に帰ってバージョンあげたら一発で通りました、気をつけます。
エラーの理由としてはクリティカルのものとしてvue-cli3の依存関係にあるライブラリがセキュリティ的にアウトで使えなくてバージョンあげないと行けなくて芋づる式にいろいろバージョンをあげないと行けなくなっていました、苦行。
vue.config.jsonをしっかり見る
vue-cliの大元と言ってもいいんですが、ここの記載によりアウトプットがガラッと変わります。
自分が困ったのはFlaskと連携するindex.htmlがFaviconを認識してくれない所が始まりでした。
この関連で困ったのはservice-worker・favicon・manifestだったと思います。
module.exports = {
outputDir: "dist",
assetsDir: "static",
pages: {
index: {
entry: "src/main.js",
title: "TimeIsMoney",
},
},
pwa: {
name: "time-is-money",
manifestPath: "static/manifest.json",
manifestOptions: {
icons: [
{
src: "img/icons/android-chrome-192x192.png",
sizes: "192x192",
type: "image/png",
},
{
src: "img/icons/android-chrome-512x512.png",
sizes: "512x512",
type: "image/png",
},
],
},
outputDir: "static",
iconPaths: {
favicon: "static/favicon.ico",
favicon32: "static/favicon.ico",
favicon16: "static/favicon.ico",
appleTouchIcon: "static/img/icons/apple-touch-icon-152x152.png",
maskIcon: "static/img/icons/safari-pinned-tab.svg",
msTileImage: "static/img/icons/msapplication-icon-144x144.png",
},
workboxPluginMode: "GenerateSW",
workboxOptions: {
swDest: "static/service-worker.js",
// ...other Workbox options...
importsDirectory: "static",
},
},
};
当初はほぼ何も書かずに生成しようとしていました。浅はかなり・・・
まず勘違いしやすいポイントですがiconPathsとmanifestOptions.iconsは別ものだという事が一つ。
一番やられたのが自分の設定だとFlaskがdist/staticの中身しか参照しないのですが、初期設定だとdist内にservice-worker/manifest/faviconもろもろが生成されるので、Flask立ち上げからのlocalhostで確認してもずっとfavicon変わらないし変なエラーおきてるしvue-cliもFlaskも経験値なさすぎで全然わからんわ・・・ってつまりました。冷静になって生成物を確認するとurlとかなんかいろいろおかしいなってなり気付きました。無記載だとデフォルト設定されるものが多いと思うのでその辺りの確認が必要ですね
SQLAlchemyが変なエラーを吐く
これは自分がpythonを使い慣れてないせいもあるんですが、readしただけなのにやたらとエラー吐くなぁと思いpython使い的には常識なのかしら・・・以下記事を参考にとりあえずエラーは減少しました。
[Python]SQLAlchemyのエラー回避備忘録
https://qiita.com/yukiB/items/67336716b242df3be350
herokuにdeployしたらエラー起きる問題
まずherokuの使用としてpackage.jsonがローカルにある想定で動きます。
ので今回のディレクトリ構成だと以下の感じで動くわけもなく・・・
- application
- frontend
- package.json
- .gitignore
- backend
- flask_application
- .gitignore
- .env
- frontend
いろいろ記事を参考にしてpackage.jsonをローカルに移し.gitignoreからdistを消してdeployとか個人的にちょっとうーん・・・と思う内容が多くて、流石にherokuさん毎回生成物コミットしてpushしないといけないとかherokuとvue-cli素人の自分でも頭悪いとおm(自主規制)
解決策としては二つやる事があり、まずはシンボリックリンク
ln -s 「シンボリックリンク元のパス」 「シンボリックリンクを作成する場所のパス」
注意としてはこれはデプロイするので相対パスで作成する事。
deploy先で/user/hogehoge/application/frontend/package.json
とか存在しませんからね!これで少しやらかしました。恥ずかしい。
次にheroku特有の設定。
"heroku-postbuild": "cd frontend; npm install; npm run build"
heroku-postbuild
をpackage.jsonのscriptに記入すると優先して読んでくれます。でとりあえずの解決策として無理やりfrontendに移動してbuildしてます。スマートじゃないと思うので詳しい方は助言ください。
スマートじゃないと思っているのはなぜかというと、多分したみたいなフォルダ構成になっちゃってる。いや確認してないけど多分きっとそう。
- application
- node_module
- frontend
- dist
- node_module
- package.json
- .gitignore
- backend
- flask_application
- .gitignore
- package.json
- .env
herokuの環境変数どうやって設定する??
散々フォルダ構成を提示してきましたが、センシティブなデータは.envに記載してdeployしないように。frontもbackendもライブラリを使って読み込むはずです。しかしこのフォルダ構成。vueの環境変数はfrontendの中に存在するし、一定の行動しかしないherokuにどうやって読み込ませたらいい??問題発生、flaskは普通にos.getenvで読めるのにどうして・・・って思いましたが、結局herokuの設定で解決。
heroku buildpacks:add heroku/nodejs
heroku config:set VUE_APP_SENSITIVE=yarn
まずherokuにbuildpacksを追加します。その後に__VUE_APP___をつけて環境変数を設定。
vue-cli3くらいからの仕様のようで環境変数にVUE_APPが必要になってます。あとはbuildpackがよしなに環境変数を読み取ってくれますえらい。
herokuをスリープしないようにする
heroku無料枠だと30分アクセスがない状態だとスリープモードに入ってユーザー的に遅くて残念な感じになってしまいます。それを回避する為に定期的にアクセスするようにしないといけません。
- sendgridを使う
- uptimerobotを使う
- 自身にアクセスするタスクを組む
- etc
調べると色々ある感じだったので自分は無料でいけて手軽なuptimerobotさんを使いました。
その他抱えてる問題
herokuのclearDBが無料枠なので5MBしか使えません。MAX10000レコードだったかな?5mbに本当に入るんかいな・・・って感じなんですが、もしアプリが動かなかったら容量バーストしてると思うので許してください、、
あとがき
いろいろ細かい事を書けばあるんですが、大きく困ったのは記載した通り(忘れてるのもあります)。まだちっこうバグいっぱいあるんですが、形になったのでよし。結局触った事ないpython、vue-cli、herokuに振り回されただけだった気もしますが、楽しめました。
今度はVPSかAWSと迷って実務よりなAWSかつRailsでちゃんとしたプロダクト作ろうかなぁと思ってる最中です。簡単なアプリで合間合間で二週間とかかるとガチものプロダクトだと数ヶ月かかりそう(しかもアイデアはあるけどソースにはない)状態なのでやっぱり時間が全てだなぁと思ったり。自分は休日はずっとゲームしたい派なので回らない・・・
最初に書きましたが、じわじわ転職を考えてるので拾ってくれる企業様はtwitterとかでDM頂けたらなと
https://twitter.com/yamashitaP21