Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8913

【Node.js + Lambda】ノンエンジニア向けにツールでみんな幸せになる方法

$
0
0

:calendar_spiral:i-plug Advent Calendar 2019の【17日目】の記事です:santa::tada:

私事ですが、2019/12/17本日はエンジニアとして職につき1周年になります。
それの記念っぽく、長めの記事を投下します。

あるスプリントで...

弊社とある架空の動物部門の業務フローで毎日決まった:cat:キャットフード:cat:をgoogle詳細検索してきてその結果をpdf出力してファイリングするといったものがありました。これを効率化してほしいと依頼がありました。(フェイクです。)

つまり、手作業でやってる工程を
キーワードぽちーだけでpdfにできるノンエンジニア向けツールを作るということです。

できるようにしたいこと

:one::cat:キャットフードを詳細検索 + pdf出力 を(できる限り)自動でできること

ノンエンジニア向けであるということは

まず思いついたのはコマンドラインツールでした。
Node.js + Seleniumでスクリプトを書きnodeコマンドで実行すれば操作なしで自動でpdfを取ってくれると考えました。

しかし!

問題がありました。:joy:

カインドネスに作らないとみんな辛い

操作が複雑、環境構築の必要は:ng:

使うのは開発部門:robot:ではなく、他部門のノンエンジニアの方です。
しかもwin端末。

よって
できる限り複雑な操作を必要としないものがいいのです。
(私達開発者が思っている以上に簡単明快でシンプルでなければならない)

さらに
使用するにあたって環境構築が不要であるほどいいということです。
言わずもがな、エンジニアでも時には環境構築につまずくわけで、win端末でわざわざ動作環境を作り上げるのは骨が折れるし、作業担当者が変わってPCが変わったなどで発生するメンテナンスのコストが高いのです。

そして
コマンドラインツールだと、ノンエンジニアにとってUXは最悪
並列に際して、tarminalを複数開いて叩いてもらうわけですが、そもそもスペックとかの問題できるのかすら怪しく、一番最初の複雑な操作を必要としないから反します。

まとめると次の通りです。

つまりポイントは3つ?

:one:シンプルかつ簡単操作であること(ワンクリックレベル)
:two: GUIであること(コマンドラインはきつい)
:three:環境構築不要であること(OS違い・メンテナンスを気にしなくていいように)

この要件を目指してノンエンジニア向けの社内ツールを作ればきっと使い易く役に立つはずです:tada:

逆にこれらを気をつけないと、
開発者:robot:は作り損とメンテナンスコストで疲弊しガス欠:skull:に、
ノンエンジニアの方は逆に使いづらくて効率化されてるのかわからないし、むしろストレスフル:skull:になったりしそうです。

いざ実装〜!

みんなうれしい、カインドフルな構成とは?

環境構築が不要でOS・環境依存がしないことを目指すと簡単な社内Webアプリにすることにしました。
さらに、小さなアプリなので手をかけたくないのでサーバーレスアーキテクチャを採用。
そして、クライアントサイドはデプロイせずに必要な人にHTMLファイルの配布することにしました。これはセキュリティーやdomain取得、デプロイの手間を省く為です。
これらを踏まえて次の通りの構成になりました。

:one:インフラはAWS Lambdaを使う
:two: AWS Lambdaの構築はServerless Frameworkで実行
:three: API内部の処理は Node.js + puppeteer で作成
:four: GUIは HTML + CSS + JavaScript で作成

Serverless Framework

AWS Lambda を使用するには AWS Gatewayの設定などが必要でAPIとして使うにはそれ単体では使えません。
この構築を一元で行うものがServerless Frameworkです。

参考にさせていただきました記事 : Serverless Frameworkの使い方まとめ

ただ、自分の知見の関係でAWS Lambdaを選択しましたが
今だと AWS Amplify や Firebase なども検討しそうです。
(料金の関係はわからないけど小規模ならどれも変わらない気がする。)

APIを作成 Node.js + puppeteer

ポイントは2つ:nerd:

:one: fontを設定する
:two:バイナリを返す処理にする

handler.js
'use strict'constpuppeteer=require('puppeteer-core')constchromium=require('chrome-aws-lambda')module.exports.google=asyncevent=>{constexecutablePath=event.isOffline?"< local-chromium の path >":awaitchromium.executablePathconstquery=event.queryStringParameters.queryif(query===undefined){// エラーハンドリング}constbrowser=awaitpuppeteer.launch({args:chromium.args,executablePath})constpage=awaitbrowser.newPage()awaitpage.goto('https://www.google.com/advanced_search')awaitpage.type('#CwYCWc',query)awaitpage.type('#mSoczb','安い おいしい 無添加 健康')awaitpage.click('body > div.bottom-wrapper > div.Mza7yc > form > div:nth-child(5) > div:nth-child(9) > div.jYcx0e > input')awaitpage.waitForNavigation()consturl=awaitpage.url()awaitpage.goto(url+'&num=100',{waitUntil:'domcontentloaded'})awaitpage.evaluate(()=>{varstyle=document.createElement('style')style.textContent=`
        @import url('//fonts.googleapis.com/css?family=Source+Code+Pro');
        @import url('//fonts.googleapis.com/earlyaccess/notosansjp.css');
        div, input, a{ font-family: 'Noto Sans JP', sans-serif !important; };`document.head.appendChild(style)})constpdfStream=awaitpage.pdf()return{statusCode:200,isBase64Encoded:true,headers:{"Content-type":"application/pdf; charset=UTF-8","Access-Control-Allow-Origin":"*","Access-Control-Allow-Credentials":"true"},body:pdfStream.toString('base64')}}

フォントを設定する

Node.js + puppeteer をローカルで実行する場合はローカルで設定されている日本語フォントが適応されます。
しかし、AWS Lambdaの内部で動いているのはまっさらなLinuxのコンテナです。
日本語フォントはもちろん入っていません

なので
以下の記述でヘッドレスブラウザに日本語フォントを設定してあげる必要があります。

handler.js
awaitpage.evaluate(()=>{varstyle=document.createElement('style')style.textContent=`
        @import url('//fonts.googleapis.com/css?family=Source+Code+Pro');
        @import url('//fonts.googleapis.com/earlyaccess/notosansjp.css');
        div, input, a{ font-family: 'Noto Sans JP', sans-serif !important; };`document.head.appendChild(style)})

バイナリを返す処理

await page.pdf('hoge.pdf')のようにfilepathを指定するとコンテナ内部にpdfがダウンロードされてしまいます。よってpdfそのままをクライアントに返すことができません。

以下のようにfilepathを未指定のまま、返り値を受けるとバイナリが受け取ることができます。
これをクライアントへレスポンスとして返してあげるとクライアントでpdfを生成することができるようになります!:nerd:

handler.js
constpdfStream=awaitpage.pdf()return{statusCode:200,isBase64Encoded:true,headers:{"Content-type":"application/pdf; charset=UTF-8","Access-Control-Allow-Origin":"< クロスドメインの設定 >","Access-Control-Allow-Credentials":"true"},body:pdfStream.toString('base64')}

クライアントは簡単なHTMLファイルだけに

ダウンロードとか大層なことはさせずにZIPファイルでの配布だけで終わるようにするため
HTML + CSS + JavaScript をすべて1つのファイルにまとめました。
これで配布は超簡単:tada:
配布されたフォルダの中にファイルがいくつも存在していたら、きっと見づらいのでちょっとした配慮ですよ。:nerd:

ポイントは1つ
:one:バイナリーデータを受け取ってJavaScriptでファイルを生成 / 自動ダウンロード化

index.html
<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="content-language"content="ja"><metaname="viewport"content="width=device-width, initial-scale=1.0"><metahttp-equiv="X-UA-Compatible"content="ie=edge"><script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/encoding-japanese/1.0.30/encoding.min.js"></script><title>Document</title></head><body><style type="text/css">html{background-color:#f0f8ff;width:100%;}.main-container{width:100%;display:flex;}/* 直接CSSを埋め込みます */</style><divclass="main-container"><divclass="cat-container"><divclass="cat-form-section"><h2>キャットフードのPDFを取得します〜</h2><span>キャットフード名:</span><inputclass="cat-container-form"type="text"id="cat-form"onkeypress="eventEnter(getAPI)"></div><divclass="loading-section displaynone"><p>Googleからpdfを取得中です。しばらくお待ちください。</p><divclass="loader"></div></div></div></div></body><script>// JavaScriptも埋め込んでしまう //const_base64ToArrayBuffer=base64=>{constbinary_string=window.atob(base64)constlen=binary_string.lengthconstbytes=newUint8Array(len)for(vari=0;i<len;i++){bytes[i]=binary_string.charCodeAt(i)}returnbytes.buffer}// いろんな処理を中略... //// eventEnter: func -> func | voidconsteventEnter=(func)=>{if(window.event.keyCode==13){func()}}constgetAPI=()=>{letquery=document.getElementById("cat-form").valueshowGoogleLoading()consttargetUrl='< ここにAWS Lambdaで作ったAPIのエンドポイント >'$.ajax({type:'GET',url:targetUrl,data:{query:query}}).done(function(res){letdecodedPdf=_base64ToArrayBuffer(res);letblob=newBlob([decodedPdf],{'type':'application/pdf'});letblobURL=window.URL.createObjectURL(blob)leta=document.createElement('a')a.href=blobURLa.download='google'+query+'.pdf'document.body.appendChild(a)a.click()a.parentNode.removeChild(a)document.getElementById("cat-form").value=""hideGoogleLoading()}).fail(function(e){alert('pdf取得に失敗しました、数分後再度取得を試みてください。');hideGoogleLoading()})}</script></html>

バイナリーデータを受け取ってJavaScriptでファイルを生成 / 自動ダウンロード化

デコード

さきほど作成したAPIからのレスポンスはbase64エンコーディングが施されています。

index.html
const_base64ToArrayBuffer=base64=>{constbinary_string=window.atob(base64)constlen=binary_string.lengthconstbytes=newUint8Array(len)for(vari=0;i<len;i++){bytes[i]=binary_string.charCodeAt(i)}returnbytes.buffer}constgetAPI=()=>{// 中略 ....done(function(res){letdecodedPdf=_base64ToArrayBuffer(res);// 中略 ...})}

この処理でデコードを実施しています。:nerd:

pdfファイル生成 / 自動ダウンロード処理

index.html
constgetAPI=()=>{// 中略 ....done(function(res){letdecodedPdf=_base64ToArrayBuffer(res);letblob=newBlob([decodedPdf],{'type':'application/pdf'});letblobURL=window.URL.createObjectURL(blob)leta=document.createElement('a')a.href=blobURLa.download='google'+query+'.pdf'document.body.appendChild(a)a.click()a.parentNode.removeChild(a)// 中略 ...})}

デコードしたバイナリからpdfファイルを生成しています。:nerd:

letdecodedPdf=_base64ToArrayBuffer(res);letblob=newBlob([decodedPdf],{'type':'application/pdf'});

これからブラウザ上に表示させるfilepathを作成して、filepathのurlを持ったaタグを生成します。

letblobURL=window.URL.createObjectURL(blob)leta=document.createElement('a')a.href=blobURLa.download='google'+query+'.pdf'document.body.appendChild(a)

そして作成したaタグをクリックする処理をすればダウンロードが自動で行われます。:nerd:

a.click()

小ネタ

index.html
consteventEnter=(func)=>{if(window.event.keyCode==13){func()}}

エンターキーを押したときだけ指定した関数が発火する為に
関数を受ける関数を作成しました。

無事に完成して配布したら〜

やったで完成したったわ :nerd:
ZIPに圧縮して〜これをちょいちょいのちょい〜っと〜!! :nerd:
「完成しましたさかい、確認よろしゅうお願いしまっさ〜」っとぽち〜!配布も終わりや:nerd:


~~ :cat:数日後:dog: ~~

「すごく使いやすい!」
「助かりました!」

やったやで〜:nerd:

どれぐらい効果あったのか

業務としてだいたい30分~1時間ほど圧縮できたそうです。
すこしは効率化できて良かったですね。


Viewing all articles
Browse latest Browse all 8913

Trending Articles