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

Node.jsをバックエンドに使うべきユースケース

$
0
0

こちらの記事は、Max Savonin 氏により2020年6月に公開された『 When To Use Node.js as a Back-End 』の和訳です。
本記事は原著者から許可を得た上で記事を公開しています。

Node.jsをバックエンドに使うべきユースケース

node.jsバックエンド

JavaScriptは、世界で最も人気のあるプログラミング言語の1つだ。 現在の状況ではライバルになれるのはPythonとJavaだけであり、コンピューティング市場で約8%のシェアを保持している。

Webに関しては、JavaScriptの地位はさらに一つ突き抜けている。 JavaScriptは、事実上、Chrome(またはその派生物)またはFirefoxブラウザーで使用可能な大部分のWebアプリケーションを強化している。 結局のところ、これらのブラウザはWebアプリケーション開発に特化して開発されてきた。 JavaScriptは私たちの生活のいたるところに存在しているのだ。

このプログラミング言語がなければ、あなたは仕事を見つけたり、お気に入りの映画を見たりすることはできなかっただろう。 JavaScriptを使用して開発された最も人気のあるWebサイトは、Netflix、Facebook、LinkedInなどだ。

ただし、JavaScriptの制限された特性は、開発者にとっての多くの問題を生み出した。 そのために、JavaScriptを使用したWebサイトのサーバー側アプリケーションは、別のプログラミング言語で作成する必要しなければならなかった。 これは、JavaScriptを利用する企業にいくつかの重大な問題をもたらした。

1つ目の問題として、バックエンドとフロントエンドの2つのプログラミングチームを雇うコストを負担する必要が生じたということだ。 コストが高いだけでなく、このような企業の部門細分化は、別々のチーム間の衝突の可能性を生み出してしまった。

2つ目の問題として、バックエンドのJavaScriptソリューションがないことで、フロントエンドとサーバーサイド間で互換性の問題が発生する可能性が高くなってしまったということだ。 それらの大部分は簡単に解決できたものの、それらが徐々に蓄積されていくと、特定のWebアプリケーションでは容易に遅延が生じ、バグをはらむ可能性がある。

2009年のNode.jsの登場によって状況は一変した。 なんといっても、このプラットフォームはJavaScript用の本格的なランタイム環境を実現してくれるということだ。これはJava仮想マシンやPythonの統合開発・学習環境に劣らないものだ。

かくして、Node.jsにより開発者は無限の機会を手に入れた。 初めて、JavaScriptはいかなる場所でも使用できるようになった。 残る質問は1つだけだ。「Webベースのプロダクトのバックエンド開発をするためにNode.jsをいつ使用すべきか?」 これが私たちがこの記事で取り上げることだ。Node.jsがバックエンド開発に最適なテクノロジーになると考えられる3つの主要なケースを紹介する。

バックエンドとしてのNode.js:キーとなるユースケース

Node.jsが提供するすべての利点を考慮してみても、Node.jsは万能薬でも、万能ソリューションでもない。 Node.jsがバックエンド開発に最適な場合と、別のプログラミング言語を選択した方が良い場合がある。 そこでここでは、Node.jsがファーストチョイスとなる3つのユースケースに焦点を当ててみよう。

ユースケース1: バグのない、セキュアでメンテナンスが容易なWebベースのソリューションのためにJavaScriptでのフルスタック開発が必要となる場合

Node.jsは、バックエンド開発を念頭に置いて作られた。 基本的に、Node.jsの作者らはNode.jsを純粋なバックエンドソリューションにしようと考えていた。 したがって、市場に出回っている無数の既存のJavaScriptツールと全く問題なく一緒に使うことができる。 あなたのWebアプリケーションがすでにJavaScriptをベースとしていれば、Node.jsを使ったJavaScriptサーバーへの移行は理にかなっている。

第一に、ソフトウェアをアップデートする際の一般的な問題がなくなる。 フルスタックJavaScript開発を使用すると、開発者がWebアプリケーションの更新をプッシュするために費やす時間は大幅に減少するだろう。

第二に、構成の統一は、より古典的なサーバーとクライアント間の調整に必要なインタラクションにおいて発生する可能性がある重大なバグとセキュリティ欠陥の数を減らす可能性がある。 このことから、あなたのWebアプリが機密性の高いユーザー情報を処理する場合、全面的なJavaScriptフロントエンドがすでに存在する場合、JavaScriptバックエンドへの移行は、真に画期的なものになる可能性があるのだ。

同様の論理が、フロントエンド開発にJavaScriptを使用する可能性の高い新規のWebアプリ開発に当てはまる。 Node.jsバックエンドは将来のコードの複雑さを大幅に減らすことができ、一貫性を向上させてくれる。結果として有望なプロダクトのファーストバージョンの完成に一役買うことだろう。

ユースケース2: Webベースのソリューションの開発コストを大幅に削減したい

node.jsバックエンド

JavaScriptの大きな強みの1つは、そのシンプルさだ。 C++、Python、さらにはJavaよりも学習が簡単だと言うのが私の意見だ。 その結果、フルスタック開発およびさまざまなツールを使用したフロントエンド開発を断念した場合でも、作業コストを大幅に削減できるだろう。

JavaScriptバックエンドチームはおそらくPythonやC#のチームよりも規模が小さく、より経験の少ないプログラマーでも回すことができるかもしれない。 実際にフルスタックに移行する場合は、バックエンドチームとフロントエンドチームという分割を捨て去って、真に統一された開発環境を作ることも可能だ。

Node.jsランタイム環境にはフロントエンド開発をサポートするツールの拡張セットがあり、Node.jsの開発者とコミュニティのおかげでこのような開発体制を選ぶことは容易になっている。 たとえば、Pugというパッケージは、サーバー側のバックエンドデータを、クライアント側PC上で簡単にレンダリングできるHTML文字列に変換できるNode.jsテンプレートエンジンを提供している。

リソースに関して制約がある場合(たとえば、会社が小規模なスタートアップであったり開発者があなた1人だけだったりする場合)、

Node開発</ 0>が唯一の現実的な解決策かもしれない。 Node.jsでの開発はシンプルで習得が容易であり、バックエンド開発でもかなり控えめな規模の開発チームで十分だ。

Node.jsのランタイム環境はWebの外部での開発を可能にするほど十分に堅牢だ。例えば、ロボティクスを含む最もユニークな使用例のいくつかが知られている。 これらのことから、Node.jsは、従来の開発よりのソリューションが持つ効率やセキュリティを失うことなく、費用を節約する完璧な方法となってくれる。

ユースケース3: 多数のユーザーからのリクエストに対応するプログラムを作成しようとしている場合

Node.jsの主な利点の1つは、シングルスレッド処理をサポートしているという点だ。 Node.jsはこれを最大限に利用し、現代の非同期シングルスレッドコンピューティング</ 0>の事実上の「王」だ。

Node.js / JavaScriptバックエンドは、計算上の大きなニーズを持たない大量のユーザーの流入に遭遇する可能性が高いサーバーに最適なソリューションだ。 結果的に、1つのスレッド内ですべてのクライアントのリクエストを処理することができるので、リクエストすべてに対して別々のインスタンスを作成するという手段に訴えることはない。

現実的には、これはクライアント側とサーバー側の両方のCPU機能のより効率的な使用につながっている。 Node.jsをバックエンドに採用することによって、これまで述べてきたユースケースで他の一般的なバックエンドソリューションよりもサーバーへの負担が少なくできる。 たとえば、Webチャットを開発することを選択した場合、バックエンドは数千とまでいかなくても数百のリクエストを処理しなければならないだろう。

従来のマルチスレッドではスレッドの数はかなり制限される傾向があるため、パフォーマンスを妨げる可能性がある。 Node.jsはユーザーリクエストの処理能力を有意に向上させる。というのも、Node.jsでは異なるクライアント要求がランタイム環境内の一部の変数を共有することさえできてしまうからだ。

バックエンドに関しては、典型的な計算負荷はチャットやさらには単純化されたWebゲームでさえ、通常かなり制限されている。 結果的に、Node.jsは多数のユーザー向けのスケーラブルなソリューションを構築するための大きな力となってくれる。 今日、Node.jsのユースケースは枚挙にいとまがない。 バックエンドNode.jsソリューションは、今では多くの分野で積極的に使用されている。

  • Webチャット開発
  • シンプルなWebベースのゲームの開発
  • ソーシャルメディアサイト(Facebookなど)
  • Netflixといった動画ストリーミングWebサイト
  • オンラインの文書編集アプリケーション
  • ユーザーに少量の情報を提供することを目的とした多様なデータベースアプリケーション(証券取引ブローカー向けのWebアプリケーションは、Node.jsを使用して財務革新を推進している)

さらに、フロントエンドソリューションとしてJavaScriptを使用しないことを選択した場合でも、シングルスレッドコンピューティングの利点は残る。 つまり、JavaScriptの重要な利点をスマートフォンのアプリやさらにはデスクトップアプリケーションの開発にまで簡単に拡張できてしまう。

バックエンド開発でNode.jsの使用を避けるべきケース

すでに述べたように、Node.jsの主な長所は、シングルスレッドの性質だ。 純然に計算能力の話となると、リクエストの数が限られた数百、数千、または数十万のユーザーがいる場合に、Node.jsはさまざまなリクエストの処理の複雑さを大幅に軽減してくれる。

しかし、少数のクライアント側リクエストがありながら、かなりの計算能力を必要とする場合、話は変わってくる。 かなり複雑な数学的計算のための複雑なWebアプリケーションを構築している状況を想像してみよう。 特定のシナリオでは、2つの同時問い合わせしかなくても、サーバーのCPUがすぐに過負荷となる可能性がある。

マルチスレッディングだけがこの問題に対する堅牢なソリューションとなりうる。これは、複雑なプロセスにより多くの「火力」を割り当てることで、CPUの複数のコアで計算を迅速に初期化するためだ。

ただし、この状況はかなり仮説的だ。 というのも、重い計算を伴うプログラムの大部分は常にデスクトップベースだからだ。 多くの場合、計算は非常に複雑になるので、マルチスレッドサーバーのバックエンドでさえ、比較的少数のユーザーからのリクエストに対応することができないことがある。

すべての計算をユーザーのPCにオフロードする方が簡単だ。これはビデオレンダリングや数学計算などの最も複雑なタスクのいくつかのためにオフラインのプログラムを作成するということだ。 以上のことから、あらゆる種類の複雑な計算にWebプラットフォームを使用することはお勧めしない。 しかし、この状況は先10年で変わるかもしれない。ただし、将来のソリューションには、いくつかの画期的なイノベーションが関わっている可能性が高いだろう。 このケースでは、Node.jsは明らかに選択肢ではない。

Node.jsは、普通のPCユーザーの日常のニーズ向けに作られた適材適所なプロダクトだ。このセーフゾーンの外から出るならば、その本質上、自分の首を絞めることになるだろう。 複雑な計算は、古典的な開発ソリューションに全てお任せしよう。

Node.js:夢のプラットフォーム

node.jsバックエンド

Node.jsは間違いなく、現代の最も堅牢で興味深いバックエンド開発ソリューションの1つの代表だ。 このプラットフォームは、IT関連のマネジメントと開発業務に従事している多くの個人にとって興味深いものであるはずだ。

  • 起業家であれば、Node.jsを使ったWeb開発サービスを使用すことで、Web開発のコストを削減する絶好の機会が得られる。
  • Web開発者であれば、Node.jsはフルスタックJavaScript開発への完璧な道となってくれる。

開発の世界でこのような堅牢性を提供できるプラットフォームはほとんど存在しない。 そのため、私はすべてのタイプのWeb開発にNode.jsを本当にお勧めしている。

Node.jsは、セキュアでスケーラブルなバックエンドを作成しようとする開発者にとって夢のプラットフォームだ。 さらに重要なことに、シンプルなフロントエンド開発を可能にするための十分なツールも備えている。

JavaScriptを学習して、専門分野に関係なくNode.jsを使ってみるべきだ。 JavaScriptとNode.js、どちらのソリューションもシンプルであるから、あなたが決断すれば自身の開発者ポートフォリオに簡単に付け加えることができるはずだ。

翻訳協力

Original Author: Max Savonin
Original Article: When To Use Node.js as a Back-End
Thank you for letting us share your knowledge!

この記事は以下の方々のご協力により公開する事が出来ました。
改めて感謝致します。
選定担当: @_masa_u
翻訳担当: @_masa_u
監査担当: @r_pg10
公開担当: @_masa_u

ご意見・ご感想をお待ちしております

今回の記事は、いかがだったでしょうか?
・こうしたら良かった、もっとこうして欲しい、こうした方が良いのではないか
・こういったところが良かった
などなど、率直なご意見を募集しております。
いただいたお声は、今後の記事の質向上に役立たせていただきますので、お気軽にコメント欄にてご投稿ください。Twitterでもご意見を受け付けております。
みなさまのメッセージをお待ちしております。


OpenCC で中国語変換だけしたい!

$
0
0

OpenCC とは?

npm の漢字変換もづーる。

https://github.com/BYVoid/OpenCC

インストール

パッケージに入れたいわけじゃない、そもそもパッケージがない、リソースに追加したいだけなので、グローバルインストール。
npm i opencc -g

そして、 node.js でさっさとオブジェクトをいい具合にしたいだけなので、 require に必要なフルパスを探す。

find $HOME/.nvm/ -name opencc

以下、
$userdir$HOME
$versionnode -vの結果
で読み替えてください。

OpenCC-sample.js
constOpenCC=require('/home/$userdir/.nvm/versions/node/$version/lib/node_modules/opencc');constt2s=newOpenCC('t2s.json');/*
 * 英語,日本語,繁体字中国語 でリソース組んだけど OpenCC を入れ込むの
 * 重たいし、リソースの後ろにくっつけたいです。っていう感じのオブジェクト。
 */constl={displayLanguage:['Language','表示言語','顯示語言'],english:['English','英語','英文'],japanese:['Japanese','日本語','日文'],tradChinese:['Traditional Chinese','繁体字中国語','繁體中文'],simpleChinese:['Simplified Chinese','簡体字中国語','簡體中文'],}// キーごとの繁体字中国語を簡体字中国語に変換して、配列のケツにぶっこむ。// foreach 書き方忘れたからこれでいいやObject.keys(l).map(key=>{letterms=l[key],sc=t2s.convertSync(terms[2]);terms.push(sc);});#>l{displayLanguage:['Language','表示言語','顯示語言','显示语言'],english:['English','英語','英文','英文'],japanese:['Japanese','日本語','日文','日文'],tradChinese:['Traditional Chinese','繁体字中国語','繁體中文','繁体中文'],simpleChinese:['Simplified Chinese','簡体字中国語','簡體中文','简体中文'],}// あとはコレをコピペしてリソースにしちゃえばオッケーオッケー。

ExpressでCRUDアプリ(メモアプリ)を作る

$
0
0

Expressで基本的なCRUDアプリを作成する際の手順をまとめました。
DBにはMySQLを使います。

対象読者

初めてExpressを触る方

使用したバージョン

macOS Catalina 10.15.4
node:v12.14.0
MySQL:v8.0.19
Express:v4.17.1

MySQLを使う準備

自前のMacにMySQLが入っていない場合、MySQLをhomebrew経由でインストールする。
brew install mysql

// 起動
mysql.server start

// 停止
mysql.server stop

//ステータス確認
mysql.server status

// バージョン確認
mysqld --version

*MySQLのバージョンが8以上の場合、node.jsからMySQLへの接続時エラーが出るため以下のページを参考にすると良いです。
Express.js(node.js)からMySQLへの接続とCRUD操作 | アールエフェクト

プロジェクト作成〜Hello Worldまで

適当なディレクトリを作ってnpm init -yでプロジェクトを作成。
npm install expressでexpressインストール
npm install mysqlでmysqlインストール
npm install ejsでejsインストール

DB作成

create-database.jsを作成し、以下を記述します。

create-database.js
constmysql=require('mysql');constconnection=mysql.createConnection({host:'localhost',user:'root',//mysqlと合わせるpassword:'',//mysqlと合わせる});//mysqlに接続してデータベース作成(成功するとconsoleに'database created'と出る)connection.connect(function(err){if(err)throwerr;console.log('Connected');connection.query('CREATE DATABASE express_db',function(err,result){if(err)throwerr;console.log('database created');});});

※mysqlはmysql.server startのコマンドで立ち上げておきます。
node create-database.jsを実行するとDBが作成されます。

テーブル作成

create-table.jsを作成し、以下を記述します。

create-table.js
constmysql=require('mysql');constconnection=mysql.createConnection({host:'localhost',user:'root',//mysqlと合わせるpassword:'',//mysqlと合わせるdatabase:'express_db'//先ほど作成したDBの名前});//テーブルの作成connection.connect(function(err){if(err)throwerr;console.log('Connected');constsql='CREATE TABLE items (id INT NOT NULL PRIMARY KEY AUTO_INCREMENT, title VARCHAR(255) NOT NULL, text VARCHAR(255) NOT NULL)';connection.query(sql,function(err,result){if(err)throwerr;console.log('table created');});});

node create-table.jsを実行するとitemsテーブルが作成されます。

ローカルサーバーの設定

app.jsを作成し、以下を記述します。

app.js
constexpress=require('express');constmysql=require('mysql');constapp=express();app.use(express.static('public'));//静的配信のフォルダを指定app.use(express.urlencoded({extended:false}));constconnection=mysql.createConnection({host:'localhost',user:'root',//mysqlと合わせるpassword:'',//mysqlと合わせるdatabase:'express_db'//先ほど作成したDBの名前});//viewsディレクトリ以下のejsファイル認識させるapp.set('views','./views');app.set('view engine','ejs');//hello worldapp.get("/",(req,res)=>{res.send("Hello World");});app.listen(3000);//3000ポートでローカルサーバーたつ

node app.jsを実行するとローカルでサーバが立ちます。
localhost:3000にアクセスすると'Hello World'と表示されます。

CRUD

app.jsを編集し、基本的なCRUD機能を実装していきます。
※変更を加えた際は、ブラウザで確認する前に一度ctrl+Cでサーバーを停止させてから再度node app.jsを実行してください。

作成(create)

app.js
constexpress=require('express');constmysql=require('mysql');constapp=express();app.use(express.static('public'));app.use(express.urlencoded({extended:false}));constconnection=mysql.createConnection({host:'localhost',user:'root',password:'',database:'express_db'});//viewsディレクトリ以下のejsファイル認識させるapp.set('views','./views');app.set('view engine','ejs');//hello-worldapp.get("/",(req,res)=>{res.send("Hello World");});/* ============ここから追加============ *///新規作成画面のルーティングapp.get('/new',(req,res)=>{res.render('new.ejs');});//新規作成アクションapp.post('/create',(req,res)=>{connection.query('INSERT INTO items (title, text) VALUES (?, ?)',[req.body.itemTitle,req.body.itemText],(error,results)=>{res.redirect('/')//methodがpostのときはredirectを使う});});/* ============ここまで追加============ */app.listen(3000);
views/new.ejs
<formaction="/create"method="post"><inputtype="text"name="itemTitle"><inputtype="text"name="itemText"><inputtype="submit"value="作成する"></form>

localhost:3000/newにアクセスすると、フォームが表示されるようになります。
「作成する」をクリックすると、DBにデータが保存されます。

一覧(read)

Hello Worldと表示させていた部分を投稿したデータが一覧表示されるようにします。app.jsを修正し、index.ejsを新規作成します。

app.js
constexpress=require('express');constmysql=require('mysql');constapp=express();app.use(express.static('public'));app.use(express.urlencoded({extended:false}));constconnection=mysql.createConnection({host:'localhost',user:'root',password:'',database:'express_db'});//viewsディレクトリ以下のejsファイル認識させるapp.set('views','./views');app.set('view engine','ejs');/* ============ここから変更============ *///一覧画面のルーティングapp.get('/',(req,res)=>{connection.query('SELECT * FROM items',(error,results)=>{res.render('index.ejs',{items:results});//itemというキーでviewにデータベースの中身を渡せる});});/* ============ここまで変更============ *///新規作成画面のルーティングapp.get('/new',(req,res)=>{res.render('new.ejs');});//新規作成アクションapp.post('/create',(req,res)=>{connection.query('INSERT INTO items (title, text) VALUES (?, ?)',[req.body.itemTitle,req.body.itemText],(error,results)=>{res.redirect('/')});});app.listen(3000);
views/index.ejs
<%items.forEach((item) => {%><li><div><span><%=item.title%></span><span><%=item.text%></span></div></li><%})%>

編集(update)

一覧画面から、それぞれの記事の編集画面に飛べるようにapp.jsとindex.ejsを修正し、edit.ejsを新規作成します。

app.js
/* ~~省略~~ *///新規作成画面のルーティングapp.get('/new',(req,res)=>{res.render('new.ejs');});//新規作成アクションapp.post('/create',(req,res)=>{connection.query('INSERT INTO items (title, text) VALUES (?, ?)',[req.body.itemTitle,req.body.itemText],(error,results)=>{res.redirect('/')});});/* ============ここから追加============ *///編集画面へのルーティングapp.get('/edit/:id',(req,res)=>{connection.query('SELECT * FROM items WHERE id = ?',[req.params.id],(error,results)=>{res.render('edit.ejs',{item:results[0]});});});//更新アクションapp.post('/update/:id',(req,res)=>{connection.query('UPDATE items SET title = ?, text = ? WHERE id = ?',[req.body.itemTitle,req.body.itemText,req.params.id],(error,results)=>{res.redirect('/');})});/* ============ここまで追加============ */app.listen(3000);
views/index.ejs
<%items.forEach((item) => {%><li><div><span><%=item.title%></span><span><%=item.text%></span></div></li><!-- ============ここから追加============ --><ahref="/edit/<%= item.id %>">編集</a><!-- ============ここまで追加============ --><%})%>
views/edit.ejs
<formaction="/update/<%= item.id %>"method="post"><inputtype="text"name="itemTitle"value="<%= item.title %>"><inputtype="text"name="itemText"value="<%= item.text %>"><inputtype="submit"value="更新する"></form>

削除(delete)

一覧画面から、それぞれの記事を削除できるようにapp.jsとindex.ejsを修正します。

app.js
/* ~~省略~~ *///編集画面へのルーティングapp.get('/edit/:id',(req,res)=>{connection.query('SELECT * FROM items WHERE id = ?',[req.params.id],(error,results)=>{res.render('edit.ejs',{item:results[0]});});});//更新アクションapp.post('/update/:id',(req,res)=>{connection.query('UPDATE items SET title = ?, text = ? WHERE id = ?',[req.body.itemTitle,req.body.itemText,req.params.id],(error,results)=>{res.redirect('/');})});/* ============ここから追加============ *///削除app.post('/delete/:id',(req,res)=>{connection.query('DELETE FROM items WHERE id = ?',[req.params.id],(error,results)=>{res.redirect('/');});});/* ============ここまで追加============ */app.listen(3000);
views/index.ejs
<%items.forEach((item) => {%><li><div><span><%=item.title%></span><span><%=item.text%></span></div></li><ahref="/edit/<%= item.id %>">編集</a><!-- ============ここから追加============ --><formaction="/delete/<%= item.id %>"method="post"><inputtype="submit"value="削除"></form><!-- ============ここまで追加============ --><%})%>

以上でCRUD機能の実装は完了です!

完成形

GitHubに完成形のコードを上げてます。ご参考まで。

参考

//mysqlのインストール(記事の前半部分)
https://reffect.co.jp/laravel/mysql-laravel-in-mac

//データベースの作成、テーブルの作成など
https://reffect.co.jp/node-js/express-js-connect-mysql

V8 の Inline Caches と Hidden Class について

$
0
0

動的型付けの仕組み

JavaScript のコンパイラーである V8 は C++ で書かれていますが、JSILというOSSの Inline Caches の説明を参照したため仕組みの部分については、C# での説明になってしまいました。仕組みとしては同じはずですのでご容赦ください。

JavaScript は動的型付けの言語であるため、動的に型を決めることができるように内部的に Variant Generic Interface を実装しています。これは共変性という性質を持つもので、string型object型を継承しており、同じ性質を持つため、Generic に string型を持つ変数に対して、Generic に object型を持つ値を代入することができる性質です。C#のドキュメントがわかりやすかったです。

Variant Generic Interface の曖昧さ

上記で説明した Variant Generic Interface は実装によっては曖昧さを含んでしまいます。

例えば以下のように実装した場合、どの型が正しいのか判別できずに意図しない結果になる可能性があります。

// Simple class hierarchy.classAnimal{}classCat:Animal{}classDog:Animal{}// This class introduces ambiguity// because IEnumerable<out T> is covariant.classPets:IEnumerable<Cat>,IEnumerable<Dog>{IEnumerator<Cat>IEnumerable<Cat>.GetEnumerator(){Console.WriteLine("Cat");// Some code.returnnull;}IEnumeratorIEnumerable.GetEnumerator(){// Some code.returnnull;}IEnumerator<Dog>IEnumerable<Dog>.GetEnumerator(){Console.WriteLine("Dog");// Some code.returnnull;}}classProgram{publicstaticvoidTest(){IEnumerable<Animal>pets=newPets();pets.GetEnumerator();}}

C#ドキュメントからコードを引用しました。

このコードを読むと、GetEnumeratorメソッドを持つ Interface があり、それぞれが Generic によって型を決定しています。さらにここでは Variant Generic Interface によって、多少曖昧な型でも、コンパイルエラーは起こりません。そのため、実行時にどの Interface のメソッドが実行されるのか分からない状態になってしまっています。

JavaScript(V8) への影響

JavaScriptは動的型付け言語であるため、この曖昧さに対して実行時に対処しなければなりません。JavaScriptはこの問題に対して、全ての候補を調べるという方法でどのプロパティを参照するのかを決定しています。この方法では、実行がかなり遅くなってしまいます。そのため、JavaScriptでは Inline Caches(ICs) という方法を使って、一度決定した型を次に使い回すという方法で高速化を測っています。こちらのページに C# で実行した場合、ICs が有効の場合、無効の場合のベンチマークの計測結果が載っています。

Hidden Class とは

Hidden Class はオブジェクトを宣言した時に必ず作られるもので、メモリ上のプロパティのオフセット(メモリ上の位置)を動的に決めるための方法です。静的型付け言語であればコンパイル時に事前にオフセットの位置を決めて、そこから値を参照することができるのですが、動的型付け言語である JavaScript は事前にコンパイルすることができないので Hidden Class を使って動的にメモリ上のオフセットの位置を更新しています。

例えば以下のようなコードがあった時に、p1p2は異なる Hidden Class を持ちます。Hidden Class は 値が代入される度に変わり、アップデートされます。アップデートされた時はメモリ上のオフセットを動かして、新しいオフセットへと更新します。そのため、p1.aの位置 => p1.bの位置へと更新されていきます。この時にp2ではp2.b => p2.aの順番で値が代入されてしまっているため、同じルートでオフセットの位置を辿ることができません。そのため、今回のケースではp1p2で代入の順番が異なっているため、異なる Hidden Class が生成されてしまいます。

functionPoint(x,y){this.x=x;this.y=y;}varp1=newPoint(1,2);p1.a=5;p1.b=6;varp2=newPoint(3,4);p2.b=7;p2.a=8;

Hidden Class についての詳しい説明はこちらの記事がわかりやすかったです。

Inline Caches とは

Inline Caches について具体的な例を示します。

getX関数があったとして、引数oはプロパティxを持ちます。この場合、引数oは呼び出し時に Hidden Class を作り、オブジェクトをメモリ上にキャッシュします。

functiongetX(o){returno.x;}
// 型を決めて、Hidden Class を生成し、メモリ上にキャッシュされるgetX({x:'test1'});

最初の呼び出し時に引数oはキャッシュされているので2回目の呼び出しからはキャッシュされた値が使われます。

// 2回目の呼び出しではキャッシュを使うので、型の処理が不要になるgetX({x:'test2'});

しかしここで、異なる Hidden Class を生成してしまうとキャッシュが利用できなくなる

// プロパティyはメモリ上に存在しないのでキャッシュを利用できないgetX({y:'abc'});

そのため、出来るだけ Hidden Class を再利用するように実装することで runtime の速度を少しでも改善することができます。

JavaScript でどのように動くのか

上記のようにキャッシュされた関数呼び出しの情報は、オブジェクトへのアクセスを容易にします。最初に述べたとおり、JavaScript は動的型付け言語であるため、渡された Object が何の型なのか最初から分からないため、プロパティを調査します。しかし、キャッシュすることによって、調査の必要がなくなり、スムーズにプロパティにアクセスできるようになります。

つまり、オブジェクトへのルックアップを減らし、パフォーマンスを向上させるためには、オブジェクトのプロパティが変更されないことが重要になります。

オブジェクトが変更されていないことを保証するために Hidden Class を使って値が変更されていないことを保証します。この Hidden Class のプロパティが変更されていない時のみ、プロパティを調査する処理を省き、プロパティへのアクセスを高速にすることができます。

JavaScriptの最適化

ここまでの説明をまとめると、V8 では Object が作成される度に、Hidden Class が作成され、型情報をキャッシュしていることがわかります。そしてこの型情報を元にプロパティの呼び出しを効率的に行っており、Inline Caches も Hidden Class の型に基づいて関数を効率的に呼び出すようにキャッシュしています。

このことを踏まえると以下のことを意識してコードを書く必要がありそうです。

  • Hidden Class が異なってしまい、最適化されたコードが共有されなくなってしまうため、同じ順番でインスタンス化する
  • 動的に値を追加することは Hidden Class を強制的に変更し、処理を遅くするため、出来るだけ constructor で初期化するようにする
  • Inline Caches によって最適化が施されるため、繰り返し使うようなメソッドは、1つのメソッドを使うようにする
  • このような性質のため、Reduxのような頻繁にオブジェクトを変更するような作業は少し重い作業になります。そのため、Reduxではオブジェクトを浅くすることを推奨しています。浅い方がコピーのスピードが早く、ポインタを効率的に動かすことができるからです。

まとめ

JavaScript(V8) がどのように処理しているのかを知ることで、コンパイラの気持ちを考えてコードが書くことができるようになったと思います。しかし、まだまだ知らないことがありそうなので今後も少しずつ調べてまとめていたらと思います。

また JavaScript は動的型付け言語であるために、記事にあるようなことを意識するのは少し難しいですが、最初から型で固定してしまえば、意識しなくてもある程度パフォーマンスの良いコードを書けるようになるのかなと思いました。つまり TypeScript がパフォーマンスの面でも重要なのかなと感じました。

最後まで読んでいただきありがとうございました。

何か間違っているところがあれば、教えていただけると嬉しいです。

参考

Nodeのバージョン管理をnodebrewからnに切り替える

$
0
0

Nodeのバージョン管理には「nodebrew」を使用していたのですが、なぜか調子がよくないのでこの際に気になっていた「n」に切り替えてみます。
私自身の備忘録になりますので、
作業手順や状況などは細かいのですがご了承ください。

環境

・OS: Mac OS X
・Version: 10.14.6
・bash

まずは既存のnodebrewを削除

私の場合、「homebrew」でインストールした記憶があったので brewコマンドで削除します。

brew uninstall nodebrew

その後、しっかり消えているか確認します。

nodebrew -v

あれ?なぜかたくさん表示されました。。。
その中には nodebrew バージョン番号の文字がありました。

これは削除されていないということで探します。

which nodebrew

whichで検索しました。
そうすると

/Users/admin/.nodebrew/current/bin/nodebrew

しっかりそれっぽいのがいらっしゃいました。

とりあえずごっそり消してみます。

rm -R /Users/admin/.nodebrew

もう一度消えたか確認してみると

nodebrew -v
/Users/admin/.nodebrew/current/bin/nodebrew: No such file or directory

とりあえずPATHを確認
私の場合、~./bashrc に色々記載しているのでそこを確認

export PATH=$HOME/.nodebrew/current/bin:$PATH

こんなのを発見したので削除。

nodebrew -v
nodebrew: command not found

よっしゃ。

nをhomebrewでインストール

次に「n」をhomebrewでインストールします。

brew install n

インストール後、恒例のバージョン確認。

n -V
6.7.0

インストールは無事完了。

nodeのインストール

次にnodeをインストールします。
最新版をインストールする際にはlatest
LTS(Long Term Support)版をインストールする際にはlts
安定バージョンをインストールする際にはstable
というラベルを使用します。
今回は、最新版をインストールしてみます。

n latest

  installing : node-v14.9.0
  mkdir : /Users/admin/.n/n/versions/node/14.9.0

  Error: sudo required (or change ownership, or define N_PREFIX)

おや?N_PREFIXで設定したらいいのか?
調べてみたらnodeのインストール先を変更してあげれば
sudoも不要でいい模様
とりあえず環境変数をbashrcに設定してみる。

~/.bashrc
export N_PREFIX=$HOME/.n
export PATH=$N_PREFIX/bin:$PATH

記載後にsource ~./bashrcをしてあげる。

その後もう一度nodeのインストール

n latest

  installing : node-v14.9.0
       mkdir : /Users/admin/.n/n/versions/node/14.9.0
       fetch : https://nodejs.org/dist/v14.9.0/node-v14.9.0-darwin-x64.tar.xz
   installed : v14.9.0 (with npm 6.14.8)

今度はうまくいった模様。
安定のバージョン確認

node -v
v14.9.0

npm -v
6.14.8

無事完了!

色々な処理

せっかくなのでちょっとやってみる

インストール済みのバージョンの一覧

n list
node/12.18.3
node/14.9.0

これでインストール済みのバージョンは表示される

バージョン切り替え

    node/12.18.3
  ο node/14.9.0

Use up/down arrow keys to select a version, return key to install, d to delete, q to quit

これだけで切り替え可能。便利。

バージョン削除

n rm バージョン番号

削除したいバージョンを指定して削除します。

n prune

現在使用中のバージョン以外の全てのバージョンを削除する。

参考にさせていただいた記事

nodeのバージョンをnで管理する

【Node.js】LINE BOT グループにプッシュ通知を送る

$
0
0

はじめに

LINE APIを使ってグループにpush通知を送る個人的メモ

モジュールインストール

axiosを使うと非常にシンプルにPOSTリクエストが送れるので採用

$ npm i axios

コード

index.js
constaxios=require('axios')constlinePost=async(msg)=>{console.log('linePost...')consttoken='API TOKENを入力してください'constreq_url='https://api.line.me/v2/bot/message/push'try{constres=awaitaxios.post(req_url,{//グループID"to":"グループのIDを設定してください","messages":[{"type":"text","text":msg}]},{headers:{Authorization:`Bearer ${token}`,'Content-Type':'application/json; charset=UTF-8'},})console.log(JSON.stringify(res))}catch(e){console.log(e.response.data)}}(async()=>{awaitlinePost('テストメッセージ')})()

実行方法

$ node index.js

Node.js環境構築

$
0
0
  • Node.js npmインストール

  • アプリフォルダ作成

ターミナル.
任意の場所で
mikdir list-app
  • フォルダへ移動

  • パッケージのインストール

ターミナル.
npm init --yes

package.jsonを生成するコマンド
package.json
=> npmパッケージ設定情報などが記載されている。

  • mpmパッケージのインストール(express ejs)
ターミナル.
npm install express ejs
  • 必要なファイルの作成  
ターミナル.
(list.appのフォルダで)
mkdir views
touch app.js
cd views
touch hello.ejs
  • コードの記載
ターミナル.
vi  app.js

//以下をコピー

const express = require('express');

const app = express();

app.get('/', (req, res) => {
  res.render('hello.ejs');
});

app.listen(3000);

const express = require('express');
const app = express();

=> expressを読み込む際の定型分

app.get('/', (req, res) => {
res.render('hello.ejs');

=> ルートにアクセスした際に、hello.ejsを開く

  • サンプル文を記載
ターミナル.
vi views/hello.ejs

//以下をコピペ
<h1>Hello World</h1>

* サーバ起動

ターミナル.
node app.js

* ブラウザで localhost:3000 を打ち込み確認

ターミナル.
npm install nodemon

ファイルが変更されると自動でサーバーを再起動するパッケージ

* nodemon を使ってサーバーを起動

ターミナル.
./node_modules/.bin/nodemon app.js
  • ↑ 毎回これを打つのは面倒なので、簡単に起動できるようにする。
ターミナル.
vi package.json

//以下を修正

"scripts":{
  "test": ~
//↑  削除

"scripts":{
  "start": "./node_modules/.bin/nodemon app.js"
},
//を削除した部分に追記

これで"start"というタスク"./node_modules/.bin/nodemon app.js"を登録
ターミナルで

npm run start

を実行すれば、簡単にnodemonを起動できる。(ファイル更新時にサーバーが自動で再起動)

Node.jsでモジュールが認識されなかった話

$
0
0

Windows環境でNode.jsとwsを用いたシグナリングサーバを運用したくて使い始めてみたのですが、wsが見つからないという旨のメッセージが出る。
node.jsを入れなおそうが、wsをグローバルインストールしようが改善しなかったのでいろいろ調べてみると、どうにもパスのせいらしい。
どこかで自動的にパスを通してくれると見ましたが、それはnodeそのもののパスだけでモジュールまでのパスは自分で通さないといけないようで......

結果としてはシステムの詳細設定からプロパティに飛び、環境変数のユーザー環境変数にNODE_PATHという名前で、C:\Users\ユーザー名\AppData\Roaming\npm\node_modulesを追加するだけで動作した。
node上で見れるモジュールの参照先には既に指定されていたのでパスの問題ではないだろうと思い、package.jsonとかいろいろ見ていたのですがもっと基本的な部分でハマっていましたとさ。


LambdaでRDS Proxy経由でAurora(MySQL)に接続する(IAM認証)

$
0
0

待ち望んでいたRDS Proxyが東京リージョンでGAされました。
https://aws.amazon.com/jp/blogs/aws/amazon-rds-proxy-now-generally-available/

早速、Lambdaから利用する機会があったのでメモしておきます。
今回は、セキュアに接続するため、TLS+IAM認証で接続します。
上記で接続すると、Lambdaに認証情報(具体的にはDBのパスワード)の取得コードを記載しなくて良いのがかなり良い感じです。

前提

Aurora(MySQL)は存在していること

手順

  1. Secrets Manager設定
  2. RDS Proxy作成
  3. Lambdaから接続

Secrets Manager設定

マネジメントコンソールからSecrets Managerに移動して、認証情報を入力し、シークレットを作成します。

スクリーンショット 2020-08-29 10.39.57.png

スクリーンショット 2020-08-29 9.52.20.png

スクリーンショット 2020-08-29 9.53.08.png

RDS Proxy作成

マネジメントコンソールからRDSに移動しメニューからProxies ->「プロキシを作成」ボタンクリックでRDS Proxyを作成していきます。

スクリーンショット 2020-08-29 9.56.06.png

TLSを有効にし、

スクリーンショット 2020-08-29 10.43.49.png

IAM認証を必須に設定します。

スクリーンショット 2020-08-29 9.57.45.png

Proxyのエンドポイントは後で利用するので、メモっておきます。

スクリーンショット 2020-08-29 10.46.54.png

Lambdaから接続

Node.jsでデフォルト設定で作成していきます。

スクリーンショット 2020-08-29 10.02.37.png

LambdaVPCリソースのAuroraにアクセスするため、VPCのアタッチが必要になります。
なので、まずLambdaにロールを付与する必要があります。
ここでは、EC2FullAccessを付与しています。

スクリーンショット 2020-08-29 10.04.42.png

次に接続したいAuroraが存在するVPCLambdaにアタッチします。

スクリーンショット 2020-08-29 10.14.15.png

次に、Lambdaに作成したRDS ProxyLambdaに追加します。

スクリーンショット 2020-08-29 10.44.49.png

プログラム

上記で全ての設定が完了したので、プログラムを記載していきます。

※ サンプルコード全量
https://github.com/Thirosue/aws-lambda-sample/blob/master/rds_proxy/index.js

MySQL clientが必要なので、npmパッケージを含めて、zipでアップロードしてください。
手っ取り早く試したい方は以下コマンドでzipを作成し、アップロードしてください。 ※ yarnかnpmがインストールされている前提です。

$ git clone git@github.com:Thirosue/aws-lambda-sample.git # サンプルコードをclone$ cd aws-lambda-sample/rds_proxy/ # 対象ディレクトリに移動$ yarn # もしくは npm install$ zip -r src.zip index.js node_modules/ # アップロード用のzip作成
node.js
letAWS=require('aws-sdk');constmysql2=require('mysql2/promise');letconnection;constusername='admin';// 接続ユーザを設定してください。exports.handler=async(event)=>{console.log("Starting query ...\n");console.log("Running iam auth ...\n");constsigner=newAWS.RDS.Signer({region:'ap-northeast-1',hostname:process.env['proxy_endpoint'],// メモしておいたプロキシエンドポイントを設定します。ここでは環境変数から取得しています。port:3306,username});consttoken=signer.getAuthToken({username});console.log(token);console.log("IAM Token obtained\n");constconnectionConfig={host:process.env['proxy_endpoint'],// メモしておいたプロキシエンドポイントを設定します。user:username,database:'mysql',// データベース名を設定してください。ssl:'Amazon RDS',password:token,authPlugins:{mysql_clear_password:()=>()=>signer.getAuthToken()}};connection=awaitmysql2.createConnection(connectionConfig);const[rows,fields]=awaitconnection.query("SELECT 1");console.log(rows);};

アップロード後、テストイベントを作成して、コンソールからテストすると正常に接続できていることが確認できます。

スクリーンショット 2020-08-29 11.30.55.png

参考リンク

80mm フィルム (58mm フィルム) を発明してみた

$
0
0

レシート用のサーマルロール紙 (幅 80 mm or 58 mm) を映画のフィルムにしちゃいます!

01.jpg

receiptline シリーズ最終回では、インスタントカメラを作って、最後にレシートプリンターを自撮り連写しました。
この写真を眺めていたら、動画もできるんじゃないか?と思ったのです。

02.jpg

自動用紙カットを解除

receiptlineは、印刷が終わると用紙を自動でカットしてくれます。
映画のフィルムにするには、この自動用紙カットを解除しなくてはなりません。
変換ライブラリ lib/receiptline.jsのソースコードを調べます。

lib/receiptline.js
//// ESC/POS//const_escpos={// start printing: ESC @ GS a n ESC M n FS ( A pL pH fn m ESC SP n FS S n1 n2 ESC 3 n ESC { nopen:printer=>'\x1b@\x1da\x00\x1bM0\x1c(A'+$(2,0,48,0)+'\x1b \x00\x1cS\x00\x00\x1b3\x00\x1b{'+$(printer.upsideDown),// finish printing: GS r nclose:function(){returnthis.cut()+'\x1dr1';},

見つけました。close メソッドの this.cut() +を削除します。

lib/receiptline.js
//// ESC/POS//const_escpos={// start printing: ESC @ GS a n ESC M n FS ( A pL pH fn m ESC SP n FS S n1 n2 ESC 3 n ESC { nopen:printer=>'\x1b@\x1da\x00\x1bM0\x1c(A'+$(2,0,48,0)+'\x1b \x00\x1cS\x00\x00\x1b3\x00\x1b{'+$(printer.upsideDown),// finish printing: GS r nclose:function(){// return this.cut() + '\x1dr1';return'\x1dr1';},

インスタントカメラを改造

インスタントカメラのコードと実行環境を流用します。
映像クリックで撮影開始/終了するように変更しました。

wwwroot/index.html
<!DOCTYPE html><html><head><metacharset="utf-8"><title>80mm Film Camera</title><script type="text/javascript">asyncfunctioninitialize(){constvideo=document.querySelector('video');constcanvas=document.querySelector('canvas');lettimer=0;// videovideo.srcObject=awaitnavigator.mediaDevices.getUserMedia({audio:false,video:{width:1280,height:720}});video.onclick=event=>{if(timer>0){// stopclearInterval(timer);timer=0;}else{// starttimer=setInterval(()=>{// imagecanvas.getContext('2d').drawImage(video,0,0,canvas.width,canvas.height);letdata=`{i:${canvas.toDataURL('image/png').slice(22)}}\n`;// barcodeconstnow=newDate();constiso=newDate(now.getTime()-now.getTimezoneOffset()*60000).toISOString();data+=`{c:${iso.replace(/\D/g,'').slice(2,14)};o:ean,24}|`;// send dataconstxhr=newXMLHttpRequest();xhr.open('POST','printer');xhr.setRequestHeader('Content-Type','text/plain; charset=utf-8');xhr.send(data);},2000);}};}</script></head><bodyonload="initialize()"><videoautoplaystyle="width: 100%;"></video><canvaswidth="512"height="288"style="display: none;"></canvas></body></html>
  • レシートプリンター
    • TM-T88V (80mm)
  • 記録メディア
    • 80 ミリフィルム (80mm 幅のサーマルロール紙)
  • アスペクト比
    • 16 : 9
  • 画素数
    • 512 x 288 ピクセル
  • フレームレート
    • 0.5 fps (2 秒間隔で印刷)
  • 音声
    • なし

ストリームデータ処理にすれば、もっとフレームレートを上げられると思います。

80 ミリカメラ

80 ミリカメラのハードウェアです。家にあるものを組み合わせました。

03.jpg

  • ノートパソコン (カメラデバイス)
  • レシートプリンター
  • AC アダプタ
  • LAN ケーブル
  • ポータブル電源
  • ベビーキャリア

レシートプリンターが据え置き型なので重装備になってしまいました。
撮影しているとフィルムがはみ出してくるので、巻き取り装置がほしいです。

80 ミリ映写機

80 ミリ映写機は、まだこれから。そこで・・・

撮影済みのフィルムをイメージスキャナで取り込んで GIF アニメーションを作りました。
倍速でお届けします!

04.gif

05.gif

06.gif

いかがでしょうか?
また何か作ったら投稿します。ではまた!

IMI住所変換コンポーネントを改造してリバースジオコーディングに対応してみた

$
0
0

先日、経産省が公開する「住所変換コンポーネント」についての性能を調べた記事を投稿しました。

IMI住所変換コンポーネントでいろんな住所を正規化してみた
https://qiita.com/uedayou/items/4c9d30fc031a9bf6762e

「IMI住所変換コンポーネント」は、表記にゆれがある住所表記を正規化してくれるとても便利なツールです。

たとえば

霞が関2 -> 東京都千代田区霞が関二丁目

のようになります。

このツールのもう一つの大きな特徴として、その住所に対する位置情報(緯度経度)も取得することができる点があります。

霞が関2 -> 東京都千代田区霞が関二丁目 -> {"緯度": "35.675551", "経度": "139.750413"}

所謂「ジオコーディング機能」があります。
これは、ツール内にあらかじめ住所に対する位置情報データが含まれることを意味し、作り方によってはこの逆、位置情報(緯度経度)から住所にも変換することができるのではないか?と思いました。

{"緯度": "35.675551", "経度": "139.750413"} -> 東京都千代田区霞が関二丁目

これを一般的にリバースジオコーディング(逆ジオコーディング)と言いますが、本家コードを改造してこのリバースジオコーディング機能をつけてみました。

使い方

ツールを使うにはまず、Node.jsとリバースジオコーディング対応住所変換コンポーネントモジュールのインストールが必要です。経産省が公開するものとは違うものをインストールします。
ファイルは

https://github.com/uedayou/imi-enrichment-address-plus/releases/download/v1.0.0/imi-enrichment-address-plus-1.0.0.tgz

にあります。このツールには3つの使い方があります。

  • (1) コマンドラインインターフェイス
  • (2) Web API
  • (3) Node.js

(1) は、npm でグローバルにインストール

npm install -g https://github.com/uedayou/imi-enrichment-address-plus/releases/download/v1.0.0/imi-enrichment-address-plus-1.0.0.tgz

(2), (3) はローカルインストールしてください。

npm install https://github.com/uedayou/imi-enrichment-address-plus/releases/download/v1.0.0/imi-enrichment-address-plus-1.0.0.tgz

(1) コマンドラインインターフェイス

リバースジオコーディングをコマンドラインで利用する場合は

$ imi-enrichment-address-plus --lat [緯度] --lng [経度]
$ imi-enrichment-address-plus --lat 35.675551 --lng 139.750413
出力
{"@context":"https://imi.go.jp/ns/core/context.jsonld","@type":"場所型","住所":{"@type":"住所型","表記":"東京都千代田区霞が関二丁目","都道府県":"東京都","都道府県コード":"http://data.e-stat.go.jp/lod/sac/C13000","市区町村":"千代田区","市区町村コード":"http://data.e-stat.go.jp/lod/sac/C13101","町名":"霞が関","丁目":"2"},"地理座標":{"@type":"座標型","緯度":"35.675551","経度":"139.750413"}}

(2) Web API

ツール内にWebサーバ機能があり、サーバ経由でWeb APIとして利用することができます。

$ node node_modules/imi-enrichment-address-plus/bin/server.js 8080

上記を実行して http://localhost:8080/をブラウザで開くと以下のようなWebページが開きます。

Webページ

ここの緯度経度の部分の変換ボタンを押すと、実行結果には以下のように表示されます。

実行結果

プログラム上から使う場合は、以下のJSONをPOSTしてください。

{"@type":"座標型","緯度":"[緯度の値]","経度":"[経度の値]"}
cURL
$ curl -X POST -H'Content-Type: application/json'-d'{"@type": "座標型","緯度": "35.170915","経度": "136.881537"}' localhost:8080
出力
{"@context":"https://imi.go.jp/ns/core/context.jsonld","@type":"場所型","住所":{"@type":"住所型","表記":"愛知県名古屋市中村区名駅一丁目","都道府県":"愛知県","都道府県コード":"http://data.e-stat.go.jp/lod/sac/C23000","市区町村":"名古屋市","区":"中村区","市区町村コード":"http://data.e-stat.go.jp/lod/sac/C23105","町名":"名駅","丁目":"1"},"地理座標":{"@type":"座標型","緯度":"35.170778","経度":"136.882494"}}

(3) Node.js

ツールはNode.jsのモジュールなので、Node.js のコード上からも利用可能です。
リバースジオコーディングは以下のように利用できます。

constconvert=require('imi-enrichment-address-plus');constpoint={lat:35.675551,lng:139.750413}convert(point).then(json=>{console.log(json);});

利用についての注意

本ツールのリバースジオコーディングは、半径1km内から最も近い代表点を持つ住所データを1件返します。与える緯度経度によっては必ずしもその位置の住所を表さない可能性がありますので、くれぐれも正確なデータではなくある程度あいまいなデータであることを留意して使用してください。

ただ、間違っていたとしてもかなり近い住所であることがほとんどだと思います。ざっくりとした住所でも構わない場合も結構あると思いますが、そういう場合はかなり便利だと思います。

ソースコード

ソースコードは以下のリポジトリで公開しています。
詳しい使い方は、README.mdを参照してください。

https://github.com/uedayou/imi-enrichment-address-plus

httpとwebsocketの兼用(統合)サーバーを作る

$
0
0

一例です。

前準備

前提・環境

  • node.js: v10.16.0(async/awaitをサポートしている程度のバージョン)
  • npm: v6.14.2(上のnode.jsとマッチするバージョンであればOK)
  • windows 10 64bit

※ async/awaitは簡易化のために使用してるだけなので、必須ではないです。

ディレクトリ構造

[ルートフォルダ]
|- public
  |- index.html
|- utils
  |- getLocalIp.js
|- server.js
|- …その他package.json等

依存モジュールのインストール

npm i node-static ws

サーバー側

/server.js
const{createServer}=require("http");const{Server:FileServer}=require("node-static");const{Server:WebSocketServer}=require("ws");constgetLocalIp=require("./utils/getLocalIp");constPORT_NUM=8080;(async()=>{// ローカルipアドレス取得consthost=awaitgetLocalIp().catch((err)=>{throwerr;});// httpサーバー立ち上げconstfileServer=newFileServer(`${__dirname}/public`);constserver=createServer((request,response)=>{request.addListener("end",function(){fileServer.serve(request,response);}).resume();});// websocketサーバー立ち上げ(兼用)constwsServer=newWebSocketServer({server:server,});// クライアントとの接続確立後wsServer.on("connection",(ws)=>{ws.on("message",(message)=>{console.log("Received: "+message);if(message==="hello"){ws.send("hello from server!");}});});// サーバーリッスンserver.listen(PORT_NUM,host);server.on("listening",()=>{// 準備完了でコンソールにURL表示constaddressInfo=server.address();console.log(`http://${addressInfo.address}:${addressInfo.port}`);});})();
/utils/getLocalIp.js
/**
 * ローカルipアドレスを取得。非同期
 * @see https://stackoverflow.com/a/9542157
 */module.exports=function(){returnnewPromise((resolve,reject)=>{require("dns").lookup(require("os").hostname(),(error,address)=>{if(error){reject(error);return;}resolve(address);});});};

クライアント側

/public/index.html
<!DOCTYPE html><htmllang="ja"><head><metacharset="UTF-8"/><metaname="viewport"content="width=device-width, initial-scale=1.0"/><title>Websocket sample</title></head><h1>Websocket sample</h1><body><script>// URLがそのままwsとのコネクション用アドレスになるconst{host,pathname,protocol}=window.location;constwsProtocol=protocol==="http:"?"ws://":"wss://";constaddress=wsProtocol+host+pathname;constsocket=newwindow.WebSocket(address);// サーバーからのデータsocket.onmessage=(res)=>{if(typeofres.data==='string'){alert(`Message from server: ${res.data}`)}};// コネクション確立後socket.onopen=()=>{// メッセージ送ってみるsocket.send('hello')}</script></body></html>

サーバー起動

node server.jsで起動して、ターミナルに表示されるURLにアクセスするとalertでメッセージが表示される…はず。

参考

anyenv+nodenvでnode.jsの複数バージョンを管理する(MacOS用)

$
0
0

anyenv+nodenvでnode.jsの複数バージョンを管理する(MacOS用)

概略

MacOS用のバージョン管理方法です。

node.jsのバージョン管理マネージャー「nodenv」を通して、.node-versionでnode.jsのバージョン情報を共有しつつ、自動で切り替わるように設定します。

xcode→Homebrew→anyenv→nodenvの順番でインストールの手順で実施します。

自動切り替え最高の体験。

リリース情報確認URL

動作確認済みバージョン

  • Homebrew(v 2.4.14)
  • anyenv(v1.1.1)
  • nodenv(v1.4.0)
  • macOS Catalina(v10.15.6)
  • terminal(zsh)

更新履歴

  • 2020/8/31 初稿

xcodeのインストール

インストール済みかどうか、ターミナルから確認します。

zsh: terminal
% xcode-select --version

xcode-select version xxxx.

もし、インストールをしていない場合は、Xcodeのダウンロードページからインストールしてください。(時間かかります。)

すでにnode.jsがインストール済みの場合は削除する

はじめに、node -vを事項して、node.jsがインストール済みか確認します。昨日の自分は赤の他人です。

terminal
% node -v↓
v12.xx.x

バージョンが表示されてしまったら、node.jsを削除してください。

node.jsがインストールされている場合は削除

こちらの記事を参考にnode.jsを削除します。

zsh: terminal
// クイックリファレンス
% which node // node.jsの実行時のフルパス取得
% sudo rm -rf /usr/local/bin/node // 実際の削除
% node -v // 削除されているか確認
% sudo rm -rf ~/.npm // npm削除
% npm -v // 削除されているか確認

Homebrewをインストールする

Homebrewがインストール済みか確認します。昨日の自分は赤の他人です。

terminal
// インストール済みか確認のために、Homebrewのバージョンを表示する
% brew -v↓
Homebrew 2.x.xx
Homebrew/homebrew-core (git revision ...)

Homebrewのバージョンが表示された方は、インストール済みです。anyenvのインストールの項へ移動してください。

バージョンが表示されなかった方は、公式ドキュメントを参考にHomebrewをインストールします。
途中インストールユーザのパスワード入力を求められるので記載してEnterキーを押します。

terminal
% /bin/bash -c"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"↓
% Password:鍵マーク
↓
==> Next steps:
- Run `brew help` to get started
- Further documentation: 
    https://docs.brew.sh

インストール完了時に出てくるNext stepsに従って、Homebrewの動作を確認します。

terminal
% brew help↓
Example usage:
  brew search [TEXT|/REGEX/]
  brew info [FORMULA...]
  brew install FORMULA...
...

色々と出てきたら、Homebrewのインストール完了です。

anyenvのインストール

公式ドキュメントに沿って、Homebrewでインストールします。

terminal
% brew install anyenv

シェルにanyenvを設定しつつ、シェルの再起動をします。

terminal
% anyenv init
% exec$SHELL-l

anyenv -vでバージョン情報が表示されたらインストール完了です。

terminal
% anyenv -v↓
anyenv 1.x.x

anyenvのアップデートプラグインを導入

anyenv-updateという、anyenv経由でインストールしたすべての**envをアップデートするプラグインをインストールします。公式ドキュメントに則ってインストールするだけです。

Tips: 最新バージョンのnode.jsなどを使用したい場合は、anyenv updateする必要があります。

terminal
% kdir -p ~/.anyenv/plugins
% git clone https://github.com/znz/anyenv-update.git ~/.anyenv/plugins/anyenv-update

準備OK。

nodenvのインストール

nodenvのインストール前に、anyenvでインストールできる**envの種類を確認してみます。

terminal
% anyenv install-l↓
  Renv
  crenv
  denv
  erlenv
  exenv
  goenv
  hsenv
  jenv
  jlenv
  luaenv
  nodenv
  phpenv
  plenv
  pyenv
  rbenv
  sbtenv
  scalaenv
  swiftenv
  tfenv

いろんな種類の言語が選べます。いいね。
今回は、node.jsのバージョン管理が目的なので、nodenvをインストールします。

terminal
% anyenv install nodenv

インストールが完了したら、シェルを再起動します。

terminal
% exec$SHELL-l

動作確認のために、nodenv -vでバージョン情報を確認します。

terminal
% nodenv -v↓
nodenv 1.x.x+x.xxxxxx

nodenvのインストール完了しました。

node.jsをインストールしてみる

公式ドキュメントを掻い摘みつつ、node.jsの最新版をインストールしてみます。

インストールできるnode.jsのバージョンを出力する。

terminal
% nodenv install-list↓
...
14.7.0
14.8.0
14.9.0
chakracore-dev
chakracore-nightly
chakracore-8.1.2
chakracore-8.1.4
...

いっぱい出てくるけど焦らない。執筆時点では14.9.0が最新版となります。

最新版をインストールして、グローバルに指定する

installコマンドで、最新版(執筆時点14.9.0)をインストールします。

terminal
% nodenv install 14.9.0

globalコマンドで、先ほどインストールした最新版をグローバルに指定し、反映を確認します。

terminal
% nodenv global 14.9.0
↓
% nodenv versions
  system
* 14.9.0 (set by /Users/xxxxxx/.anyenv/envs/nodenv/version)

動作確認のために、node -vでバージョン情報を確認します。

terminal
% node -v↓
v14.9.0

OK。やっとnode.jsが入りました。

プロジェクト毎に別バージョンのnode.jsを反映する

まずは、installコマンドで使いたいバージョンをインストールする。

terminal
% nodenv install 14.8.0

プロジェクトルートへ移動します。

terminal
% cd<プロジェクトルート>

localコマンドで、使いたいバージョンを指定します。

terminal
% nodenv local 14.8.0
↓
% nodenv versions
  system
* 14.8.0 (set by /Users/xxxxxx/.anyenv/envs/nodenv/version) 14.9.0

念のため確認します。

terminal
% node -v↓
v14.8.0

できました!

.node-versionファイルでnode.jsバージョンを共有する

node.jsのバージョンを指定したいプロジェクトルートに、バージョンを書いた.node-versionファイルを置き、インストールされていないnode.jsのバージョンを記載します。

.node-version
// .node-versionファイル
14.7.0

node -vでバージョンを確認をしようとして、インストールされてないよ!と怒られてみます。

terminal
% node -v↓
nodenv: version `14.7.0' is not installed (set by /Users/xxx/.node-version)

OK。怒られました。
次は、怒られたバージョンをインストールして、確認してみます。

terminal
% nodenv install 14.7.0
% node -v↓
v14.7.0

OK。指定バージョンのnode.jsが表示されました。

これで、node.js生活もシンプルに。やったね。

TypeScriptのみ(Express/Nuxt)でWebアプリを作ったので個人の感想をまとめておく

$
0
0

APIサーバ、フロントエンドともにTypeScriptで、とあるベンチャーのCTO*として1人でWebアプリを作りました。忘れないうちに感想をまとめておきたいと思います。(*記事を書いている現在はフリーランスとして活動しています。)

要約すると

全体について

完璧とはいえないまでも、基本的にはとても良かったです。

フロント

ある程度良かったです。今回はNuxt(Vue2)を使ったのですが、Vue(正確にはtemplate部分にjsxを利用しないフレームワーク?)とTypeScriptとの相性があまりよくないのかなと思いました。その意味で次はReactを試したいです。また、Vuexやスキーマから自動生成(OpenAPI Generator)されたクライアントコードとTypeScriptは相性が良かったと思います。

バックエンド

全体的にとても良かったです。(あえて不満を述べるなら、後述するように今回はスキーマ定義としてOpenAPIを利用しましたが、これはバックエンドのAPIのinterface相当のものを出力できなくてその部分は不満でした。結果的にはTypeScriptの型関数を駆使して 7割くらいのエンドポイントに関しては(typescript-axiosテンプレートから出力されたTSコードを利用して)ある程度型を当てることができました。)

スキーマ

OpenAPIおよびOpenAPI Generator(typescript-axiosテンプレート)を利用しました。基本的には良かったですが出力されるコードで肝心の型情報が失われていることがたまにあったので、次はgRPCWebを試したいです。

補足

タイトルから特にAPIスキーマについて単一のTypeScriptの型情報をフロント、バックエンドどちらかにおき、両方のソースコードから参照する方法を想定された方もいらっしゃるかも知れませんが、それは行いませんでした。(行ってもいいと思いますが、今回はバックエンドとフロントエンドをより疎結合にする(それにより、たとえばバックエンドのみ別言語にリプレイスしやすくなる)ため、スキーマに関してはなるべくプログラミング言語非依存にしようと考えました。)

技術スタック

全体的なソースコードは公開できませんが、主な技術スタックは次の通りです。

APIサーバ

  • TypeScript(3.9.7)
  • Node.js(14.4)
  • server: Express
  • ORM: sequelize
  • API設計: REST
  • ソースコードアーキテクチャ: オニオンアーキテクチャ(なるべく)
  • テスト: jest

フロントエンド

スキーマ定義

インフラ

  • Heroku
  • DB: MySQL(JawsDB)

CI/CD

  • Heroku Pipelines
  • GitHub Actions + GitHub Packages(openapi-generatorで出力したファイルをGitHub Actionsでプライベートにホスティング)

その他

僕の開発環境
  • code-server
  • spot instanceのEC2でホスティング(terraformで管理)
読んで良かった本
TypeScriptの好きな機能
Gitレポジトリ構成
  • xxx-api
  • xxx-front
  • xxx-openapi-schema

各分野に関して感想

バックエンド

Express

前提として僕はRuby on Rails出身で、Expressで本格的なアプリは初めて作った形になります。
Expressは非常にミニマムで、
・nodeのhttpサーバモジュールの薄いラッパー
・HTTPリクエストを解析してルーティングを行ってくれる
・Railsと同じように抜き差し可能なミドルウェアの連なりを持つ(簡単に外部の機能をミドルウェアとして挟める)
ような特徴をもったものだと考えています。
余談ですがExpressの本体はTypeScriptでは書かれておらず、TypeScriptの型はDefinitelyTypedから持ってくる必要があります。個人的にはソースコードが追いやすいのでExpress自体がTypeScriptで書き直されて欲しいです。

とはいえ通常利用する分には全く問題なく、DefinitelyTypedから型定義を持ってきて利用すると例えばExpressのRequstやReponseクラスに型定義が付くおかげで、これらに対してどんなメソッドが呼べるのかすぐ分かるなどのメリットはちゃんとありました。

TypeScriptに移行してExpressに関して大きく変化した部分は、RequestHandlerというジェネリクス型です。このRequestHandlerジェネリクスはExpressのミドルウェア型を表すもので、(型引数を渡さなくても利用できますが)そのエンドポイントのリクエストやレスポンスの型を型引数として渡せばそのミドルウェアをより詳細に型付けできます。具体的にはreqやresに型がつき、req.bodyから存在しないキーで取得したり、res.jsonに誤った引数を渡す(型レベルで誤ったAPIレスポンスを返す)ことがなくなります。

今回はOpenAPIという規格で(個人的には初めての試みでしたが)、API仕様を定義しました。できればこのスキーマ情報から型付きのRequestHandlerを自動生成できたら良かったのですが、OpenAPIからのサーバサイドの型生成はできませんでした(おそらく基本的にserver stubが限界だと思います)。なので、苦肉の策ですがクライアントのコード生成をtypescript-axiosテンプレートで行い、そこから型関数を利用してなんとかRequestHandlerへAPIの型を渡していました。とはいえエンドポイントのmethodやpathまではOpenAPIからは生成することができず、中途半端になってしまいました。最終的にこれは無理やりすぎてあまり良かった方法でもないかなと思うので、宿題です。

(参考)typescript-axiosテンプレートで生成されたクライアントコードからAPI定義だけ引き抜くコード
// openapi-schemaレポジトリ// tsディレクトリ: generate済みのtypescript-axiosコードimport{DefaultApi}from'./ts'import{AxiosResponse}from'axios'typeThenArg<T>=TextendsPromiseLike<inferU>?U:TtypeAxiosArg<T>=TextendsAxiosResponse<inferU>?U:TexporttypeExtractResponseType<TextendskeyofDefaultApi>=AxiosArg<ThenArg<ReturnType<DefaultApi[T]>>>exporttypeExtractRequestType<TextendskeyofDefaultApi>=Parameters<DefaultApi[T]>[0]


ロジックなど

Webアプリケーションはいろいろ複雑な処理やオブジェクトを書く必要があり、そういったコードはしばしば(書くときはまだしも)読み解くのが難しくなると思います。こういったコードを読み解く際、やはり型があると型だけ見ればなんとなくやりたいことが分かるのでソースコードの理解が早くなるかな、という印象はありました。

アーキテクチャ

やはり型があると、(静的型付け言語をある程度前提とするような)アーキテクチャ論を勉強した際にその恩恵を実感しやすかったです。たとえばDIを行う場合、rubyやJavaScriptだとその依存注入が妥当かどうか型レベルで検査できませんが、TypeScriptだとできます。

まとめ

良かったこと

  • TypeScriptの型機能を存分に利用できる
  • フロントと同じ言語なので頭の切り替えが楽
  • 場合によっては、フロントからバックへ、あるいはバックからフロントへコピペ+多少の手直しで済ませられることがある(本当は共通化しても良いのかも知れない)
  • npmライブラリを眺める時、universalという言葉があるとテンションが上がる

悪かったこと

  • 型を付けないといけない
  • 型にこだわりすぎると進まない
  • (TypeScript全般と思われるが)トランスパイルする際に変数名が変わる場合があるので、debuggerで止めて変数の中身を確認するとき、debugコンソールに存在するはずの変数名を入力しても取得できない場合がある(この辺(VSCode)とかこの辺(TypeScript)とか)
  • HerokuでSentry(エラー監視ツール)にソースマップを送るのが大変だった
  • (ORMライブラリのsequelizeが苦手)

まとめ

  • 個人的な体験としてはNuxtのフロントよりTypeScriptの恩恵は大きかった。ただ、debug時に変数が変わってしまっていてそれがdebugコンソールにて補正されていないのが気になった。
  • 個人的な興味として選べるなら次はgRPCWebとGoでやってみたい

フロントエンド

Vue(Nuxt)

前提として僕はVue経験あり、React経験なしでした。このアプリは最初はJavaScriptで書き始めて途中からTypeScriptに置き換えたのですが、他でも言われている通りTypeScriptの型システムを隅々まで活用したい場合、VueよりReactの方が相性がいいのかなと思いました。
Vue+TypeScriptで個人的に特に気になったのは、

  • (VeturというVSCodeのextentionで一応対応はしていましたが)templateの中で利用する変数が存在してなくてもコンパイルエラーにならない。また、誤った型のpropsを渡してもコンパイルエラーにならない。
  • SFC(single file component)を少なくともVSCode上でauto importできない(Reactのコンポーネントや普通のTypeScriptのクラス、関数はできた)

あたりでした。

Vuex(Nuxt)

TypeScriptを導入したことにより、Vuexを型付けできること自体は良かったです。例えばアクションを呼びたい場合、通常はstore.dispatch('user/fetch', { id: 3 })というようにアクション名をstringでdispatchを呼ばないといけないですが、これをuserModule.fetch({ id: 3 })というようにメソッドで呼べるので良かったです。
特にAPIを呼ぶ部分がOpenAPI Generatorで生成したクライアントコードで書けたので、レスポンスにも型がついていて良かったです。
ただ、vuex-module-decoratorsが特にNuxtのSSRと組み合わさった場合に個人的に扱いづらく、安定的にSSRできるまで難儀しました。

まとめ

良かったこと

  • 型を使える
  • 特にAPI部分にもスキーマから型をつけられると良い

悪かったこと

  • Vue(Nuxt)よりReactの方がTypeScriptとの相性が良いと思われる
  • NuxtのVuex(SSR)+vuex-module-decoratorsが大変だった

まとめ

  • 次はReact+gRPCWebでやってみたい。

スキーマ定義

Spotlight Studio

こちらを、OpenAPIの定義ファイル(OAS)編集ツールとして利用しました。
ずっと無料で使えて個人的にはとても使いやすかったです。

OpenAPI Generator

期待値が高すぎましたが、使って良かったと思います。ただ、oneOfやnullがしばしば使えなかったり、ステータスコードごと(200,404,500など)にレスポンスのスキーマを定義しても出力されたTypeScriptコードでは1種類にまとめられていたりなど、OpenAPIの理想にコード生成機能が追いついていない印象は受けました。

まとめ

  • 大体良かったが、次はgRPC Webを試してみたい

終わりに

TypeScriptでアプリを作る際のなにかしら参考になれば幸いです。誤りや修正点を見つけた際は指摘いただければ幸いです。ここまでお読みいただきありがとうございました。

ここまでお読みいただきありがとうございました。

Nuxt.jsのサーバフレームワークをKoa.jsに変更する

$
0
0

選択できなくなっていたサーバフレームワーク

Nuxt.jsインストール時にはExpressやKoa.jsなどのサーバサイドフレームワークを選択できていたと思うんだけど、最近のバージョンだとそれができなくなっていたので、フレームワークをKoa.jsに変更する方法を記載する。Expressでも同じかと。

デフォルトでは "connect"

https://ja.nuxtjs.org/api/configuration-servermiddleware
で次のように記載されているように、デフォルトではconnectが適用されるらしい。

Nuxt は内部で connect のインスタンスを作ります。
それはミドルウェアをスタックに登録したり、 外部サーバーを
必要とせず に API などのルートを増やす事を可能にしてくれます。
connect 自体はミドルウェアで、登録されたミドルウェアは
nuxt start と express-template のようなプログラム的な
使用法を持つミドルウェアとして使用されます。

connectのままでもいいんだろうけども、自分的にはKoa.jsの方が使い慣れてるし、今はまだKoa.jsの方が広く受け入れられてると思う。

手順

1. Nuxt.js、Koa.jsインストール

npx create-nuxt-app app
cd app
npm i koa

2. server/index.js 作成

アプリフォルダ直下にserverフォルダを作成し、その配下にindex.jsを作成する。

index.jsはKoa.jsのページにある"Hello World"のサンプルにexport defaultを追加。
あとは煮るなり焼くなり。

index.js
constKoa=require('koa')constapp=newKoa()app.use((ctx)=>{ctx.body='Hello World'})exportdefault{path:'/api',handler:app.callback()}

3. nuxt.config.js 編集

serverMiddleware: ['~~/server/'] を追加する。

nuxt.config.js
importcolorsfrom'vuetify/es5/util/colors'exportdefault{serverMiddleware:['~~/server/'],

以上で完了。/apiにアクセスするとKoa.jsが動く。
screen02.png


eject後のwebpack.config.jsを1から読み解いてみる②

$
0
0

はじめに

この記事は前回の記事の続きです。前回の記事を見ていない方はこちらからご覧ください。

Ln274-Ln320

resolve:{// This allows you to set a fallback for where webpack should look for modules.// We placed these paths second because we want `node_modules` to "win"// if there are any conflicts. This matches Node resolution mechanism.// https://github.com/facebook/create-react-app/issues/253modules:['node_modules',paths.appNodeModules].concat(modules.additionalModulePaths||[]),// These are the reasonable defaults supported by the Node ecosystem.// We also include JSX as a common component filename extension to support// some tools, although we do not recommend using it, see:// https://github.com/facebook/create-react-app/issues/290// `web` extension prefixes have been added for better support// for React Native Web.extensions:paths.moduleFileExtensions.map(ext=>`.${ext}`).filter(ext=>useTypeScript||!ext.includes('ts')),alias:{// Support React Native Web// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/'react-native':'react-native-web',// Allows for better profiling with ReactDevTools...(isEnvProductionProfile&&{'react-dom$':'react-dom/profiling','scheduler/tracing':'scheduler/tracing-profiling',}),...(modules.webpackAliases||{}),},plugins:[// Adds support for installing with Plug'n'Play, leading to faster installs and adding// guards against forgotten dependencies and such.PnpWebpackPlugin,// Prevents users from importing files from outside of src/ (or node_modules/).// This often causes confusion because we only process files within src/ with babel.// To fix this, we prevent you from importing files out of src/ -- if you'd like to,// please link the files into your node_modules/ and let module-resolution kick in.// Make sure your source files are compiled, as they will not be processed in any way.newModuleScopePlugin(paths.appSrc,[paths.appPackageJson]),],},

resolveではmoduleの取り扱いに関する設定を行います。
・modules
 どこのディレクトリにあるmoduleを利用するかを指定する。

・extensions
 moduleの読み込みのときに補完できる拡張子を設定する。
 .jsが含まれていたらrequire('a')としたときa.jsを読み込むことができるようになる。

・alias
 moduleを呼び出したときに参照するパスの設定を行う。
 基本は'a': 'b'require('a')としたときbの直下を読む。

・plugins
 moduleの解決時に追加で用いるpluginの設定。
 PnpWebpackPlugin: 詳しい説明なかったけどnode_modulesの解決を簡略化するらしい。
 ModuleScopePlugin: srcより外からのインポートを禁止する。(node_modulesは許可)

resolveLoader:{plugins:[// Also related to Plug'n'Play, but this time it tells webpack to load its loaders// from the current package.PnpWebpackPlugin.moduleLoader(module),],},

loaderの取り扱いについての設定
内容はresolveと同様

Ln321-Ln509

module:{strictExportPresence:true,rules:[// Disable require.ensure as it's not a standard language feature.{parser:{requireEnsure:false}},// First, run the linter.// It's important to do this before Babel processes the JS.{test:/\.(js|mjs|jsx|ts|tsx)$/,enforce:'pre',use:[{options:{cache:true,formatter:require.resolve('react-dev-utils/eslintFormatter'),eslintPath:require.resolve('eslint'),resolvePluginsRelativeTo:__dirname,},loader:require.resolve('eslint-loader'),},],include:paths.appSrc,},{// "oneOf" will traverse all following loaders until one will// match the requirements. When no loader matches it will fall// back to the "file" loader at the end of the loader list.oneOf:[// "url" loader works like "file" loader except that it embeds assets// smaller than specified limit in bytes as data URLs to avoid requests.// A missing `test` is equivalent to a match.{test:[/\.bmp$/,/\.gif$/,/\.jpe?g$/,/\.png$/],loader:require.resolve('url-loader'),options:{limit:imageInlineSizeLimit,name:'static/media/[name].[hash:8].[ext]',},},// Process application JS with Babel.// The preset includes JSX, Flow, TypeScript, and some ESnext features.{test:/\.(js|mjs|jsx|ts|tsx)$/,include:paths.appSrc,loader:require.resolve('babel-loader'),options:{customize:require.resolve('babel-preset-react-app/webpack-overrides'),plugins:[[require.resolve('babel-plugin-named-asset-import'),{loaderMap:{svg:{ReactComponent:'@svgr/webpack?-svgo,+titleProp,+ref![path]',},},},],],// This is a feature of `babel-loader` for webpack (not Babel itself).// It enables caching results in ./node_modules/.cache/babel-loader/// directory for faster rebuilds.cacheDirectory:true,// See #6846 for context on why cacheCompression is disabledcacheCompression:false,compact:isEnvProduction,},},// Process any JS outside of the app with Babel.// Unlike the application JS, we only compile the standard ES features.{test:/\.(js|mjs)$/,exclude:/@babel(?:\/|\\{1,2})runtime/,loader:require.resolve('babel-loader'),options:{babelrc:false,configFile:false,compact:false,presets:[[require.resolve('babel-preset-react-app/dependencies'),{helpers:true},],],cacheDirectory:true,// See #6846 for context on why cacheCompression is disabledcacheCompression:false,// Babel sourcemaps are needed for debugging into node_modules// code.  Without the options below, debuggers like VSCode// show incorrect code and set breakpoints on the wrong lines.sourceMaps:shouldUseSourceMap,inputSourceMap:shouldUseSourceMap,},},// "postcss" loader applies autoprefixer to our CSS.// "css" loader resolves paths in CSS and adds assets as dependencies.// "style" loader turns CSS into JS modules that inject <style> tags.// In production, we use MiniCSSExtractPlugin to extract that CSS// to a file, but in development "style" loader enables hot editing// of CSS.// By default we support CSS Modules with the extension .module.css{test:cssRegex,exclude:cssModuleRegex,use:getStyleLoaders({importLoaders:1,sourceMap:isEnvProduction&&shouldUseSourceMap,}),// Don't consider CSS imports dead code even if the// containing package claims to have no side effects.// Remove this when webpack adds a warning or an error for this.// See https://github.com/webpack/webpack/issues/6571sideEffects:true,},// Adds support for CSS Modules (https://github.com/css-modules/css-modules)// using the extension .module.css{test:cssModuleRegex,use:getStyleLoaders({importLoaders:1,sourceMap:isEnvProduction&&shouldUseSourceMap,modules:{getLocalIdent:getCSSModuleLocalIdent,},}),},// Opt-in support for SASS (using .scss or .sass extensions).// By default we support SASS Modules with the// extensions .module.scss or .module.sass{test:sassRegex,exclude:sassModuleRegex,use:getStyleLoaders({importLoaders:3,sourceMap:isEnvProduction&&shouldUseSourceMap,},'sass-loader'),// Don't consider CSS imports dead code even if the// containing package claims to have no side effects.// Remove this when webpack adds a warning or an error for this.// See https://github.com/webpack/webpack/issues/6571sideEffects:true,},// Adds support for CSS Modules, but using SASS// using the extension .module.scss or .module.sass{test:sassModuleRegex,use:getStyleLoaders({importLoaders:3,sourceMap:isEnvProduction&&shouldUseSourceMap,modules:{getLocalIdent:getCSSModuleLocalIdent,},},'sass-loader'),},// "file" loader makes sure those assets get served by WebpackDevServer.// When you `import` an asset, you get its (virtual) filename.// In production, they would get copied to the `build` folder.// This loader doesn't use a "test" so it will catch all modules// that fall through the other loaders.{loader:require.resolve('file-loader'),// Exclude `js` files to keep "css" loader working as it injects// its runtime that would otherwise be processed through "file" loader.// Also exclude `html` and `json` extensions so they get processed// by webpacks internal loaders.exclude:[/\.(js|mjs|jsx|ts|tsx)$/,/\.html$/,/\.json$/],options:{name:'static/media/[name].[hash:8].[ext]',},},// ** STOP ** Are you adding a new loader?// Make sure to add the new loader(s) before the "file" loader.],},],},

長いですが内容はほぼ同じなので一気にいきます。
moduleはファイルの取り扱い(loaderを使うか)を決めます。

・strictExportPresence
 ファイルのエクスポートがないときに呼ばれてるかに関わらずエラーにする。

・rules
 どのloaderをどのファイルに当てるかの設定。
 parser: 標準対応されていないrequire.ensureを無効化する。
 (bundleファイルの分け方に関する設定らしい)
 test: 設定を適用するファイルの正規表現。
 use: どのloaderを使うか。useでなくloaderを使って直接書くこともできるが、useを使うことでoptionなども指定できる。
 enforce: loaderの分類。
 include: 条件に合うものも設定を適用する
 exclude: 条件に合うものには設定を適用しない
 oneof: 複数のruleの中で最初に条件に合ったもののみ適用する

ここで扱われてるloader
・eslint-loader
oneof [
 ・url-loader
 ・bable-loader
 ・getStyleLoaders(Ln71~Ln129で指定されていた内容)
 ・file-loader
]

個々のloaderについては前回触れたものも多いので省略します。もし必要そうだったら別で記事を書くかもしれないです。

Ln510-Ln652

plugins:[// Generates an `index.html` file with the <script> injected.newHtmlWebpackPlugin(Object.assign({},{inject:true,template:paths.appHtml,},isEnvProduction?{minify:{removeComments:true,collapseWhitespace:true,removeRedundantAttributes:true,useShortDoctype:true,removeEmptyAttributes:true,removeStyleLinkTypeAttributes:true,keepClosingSlash:true,minifyJS:true,minifyCSS:true,minifyURLs:true,},}:undefined)),// Inlines the webpack runtime script. This script is too small to warrant// a network request.// https://github.com/facebook/create-react-app/issues/5358isEnvProduction&&shouldInlineRuntimeChunk&&newInlineChunkHtmlPlugin(HtmlWebpackPlugin,[/runtime-.+[.]js/]),// Makes some environment variables available in index.html.// The public URL is available as %PUBLIC_URL% in index.html, e.g.:// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">// It will be an empty string unless you specify "homepage"// in `package.json`, in which case it will be the pathname of that URL.newInterpolateHtmlPlugin(HtmlWebpackPlugin,env.raw),// This gives some necessary context to module not found errors, such as// the requesting resource.newModuleNotFoundPlugin(paths.appPath),// Makes some environment variables available to the JS code, for example:// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.// It is absolutely essential that NODE_ENV is set to production// during a production build.// Otherwise React will be compiled in the very slow development mode.newwebpack.DefinePlugin(env.stringified),// This is necessary to emit hot updates (currently CSS only):isEnvDevelopment&&newwebpack.HotModuleReplacementPlugin(),// Watcher doesn't work well if you mistype casing in a path so we use// a plugin that prints an error when you attempt to do this.// See https://github.com/facebook/create-react-app/issues/240isEnvDevelopment&&newCaseSensitivePathsPlugin(),// If you require a missing module and then `npm install` it, you still have// to restart the development server for webpack to discover it. This plugin// makes the discovery automatic so you don't have to restart.// See https://github.com/facebook/create-react-app/issues/186isEnvDevelopment&&newWatchMissingNodeModulesPlugin(paths.appNodeModules),isEnvProduction&&newMiniCssExtractPlugin({// Options similar to the same options in webpackOptions.output// both options are optionalfilename:'static/css/[name].[contenthash:8].css',chunkFilename:'static/css/[name].[contenthash:8].chunk.css',}),// Generate an asset manifest file with the following content:// - "files" key: Mapping of all asset filenames to their corresponding//   output file so that tools can pick it up without having to parse//   `index.html`// - "entrypoints" key: Array of files which are included in `index.html`,//   can be used to reconstruct the HTML if necessarynewManifestPlugin({fileName:'asset-manifest.json',publicPath:paths.publicUrlOrPath,generate:(seed,files,entrypoints)=>{constmanifestFiles=files.reduce((manifest,file)=>{manifest[file.name]=file.path;returnmanifest;},seed);constentrypointFiles=entrypoints.main.filter(fileName=>!fileName.endsWith('.map'));return{files:manifestFiles,entrypoints:entrypointFiles,};},}),// Moment.js is an extremely popular library that bundles large locale files// by default due to how webpack interprets its code. This is a practical// solution that requires the user to opt into importing specific locales.// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack// You can remove this if you don't use Moment.js:newwebpack.IgnorePlugin(/^\.\/locale$/,/moment$/),// Generate a service worker script that will precache, and keep up to date,// the HTML & assets that are part of the webpack build.isEnvProduction&&newWorkboxWebpackPlugin.GenerateSW({clientsClaim:true,exclude:[/\.map$/,/asset-manifest\.json$/],importWorkboxFrom:'cdn',navigateFallback:paths.publicUrlOrPath+'index.html',navigateFallbackBlacklist:[// Exclude URLs starting with /_, as they're likely an API callnewRegExp('^/_'),// Exclude any URLs whose last part seems to be a file extension// as they're likely a resource and not a SPA route.// URLs containing a "?" character won't be blacklisted as they're likely// a route with query params (e.g. auth callbacks).newRegExp('/[^/?]+\\.[^/]+$'),],}),// TypeScript type checkinguseTypeScript&&newForkTsCheckerWebpackPlugin({typescript:resolve.sync('typescript',{basedir:paths.appNodeModules,}),async:isEnvDevelopment,useTypescriptIncrementalApi:true,checkSyntacticErrors:true,resolveModuleNameModule:process.versions.pnp?`${__dirname}/pnpTs.js`:undefined,resolveTypeReferenceDirectiveModule:process.versions.pnp?`${__dirname}/pnpTs.js`:undefined,tsconfig:paths.appTsConfig,reportFiles:['**','!**/__tests__/**','!**/?(*.)(spec|test).*','!**/src/setupProxy.*','!**/src/setupTests.*',],silent:true,// The formatter is invoked directly in WebpackDevServerUtils during developmentformatter:isEnvProduction?typescriptFormatter:undefined,}),].filter(Boolean),

拡張プラグインの設定。
設定されているプラグインは以下

常に使用

・HtmlWebpackPlugin
 htmlファイルをひとまとめにする。

・InterpolateHtmlPlugin
 bundleするhtmlファイルで変数を使えるようにする。

・ModuleNotFoundPlugin
 インポートするファイルが見つからなかったときのエラーを設定する。

・webpack.DefinePlugin
 グローバル変数を設定する。

・ManifestPlugin
 manifest.jsonを生成し、ライブラリの参照先を指定する。
 
・webpack.IgnorePlugin
 特定のファイルをインポートしないようにする。

開発環境のみで使用

・webpack.HotModuleReplacementPlugin
 hot module replacement(起動中にファイルを切り替えることで処理を高速化する)を利用できるようにする。

・CaseSensitivePathsPlugin
 インポートファイルのパスの大文字小文字を区別する。

・WatchMissingNodeModulesPlugin
 依存しているライブラリがインストールされていないときにインストールする

本番環境のみで使用

・InlineChunkHtmlPlugin (shouldInlineRuntimeChunk(Ln36)がtrueのとき)
 共通の設定をChunkの中に埋め込み、http通信の数を減らすことができる。

・MiniCssExtractPlugin
 CSSを別ファイルに分離してまとめる。

・WorkboxWebpackPlugin
 ページのキャッシングを行う。

TypeScript使用時(useTypeScript(Ln45)がtrueのとき)のみ使用

・ForkTsCheckerWebpackPlugin
 Typescriptの型判定を高速化する。

Ln653-Ln669

// Some libraries import Node modules but don't use them in the browser.// Tell webpack to provide empty mocks for them so importing them works.node:{module:'empty',dgram:'empty',dns:'mock',fs:'empty',http2:'empty',net:'empty',tls:'empty',child_process:'empty',},// Turn off performance processing because we utilize// our own hints via the FileSizeReporterperformance:false,

node
nodeの設定。呼ばれるが実際使われないものについて空のオブジェクトを返す。

performance
bundleしたファイルサイズが指定を超えた場合にアラートを出すかの設定。

終わりに

後半大分駆け足でしたがなんとかwebpackの設定を読み切ることができました!
eject時には設定されていない内容もあるし、出てきたものも細かい使い方を理解できたわけではありませんがこの記事を書いたことでwebpackでどういったことができるのかがなんとなくつかめたかなと思います。

今後新しく気づきがあればこの記事も追記させていただきます。いろいろ調べながら書いたものの理解できていない部分もあるので間違っている部分などがあれば指摘していただけるとありがたいです

参考ページ

全体
https://webpack.js.org/
http://js.studio-kingdom.com/webpack/api/configuration
https://qiita.com/soarflat/items/28bf799f7e0335b68186
https://site-builder.wiki/posts/9654

module
https://yuhodev.hatenablog.com/entry/2019/07/13/165518
https://qiita.com/s4kr4/items/c6f0e8278844117502cc#modulestrictexportpresence
https://www.slideshare.net/ssuserc9c8d8/reactscriptswebpack-130687608

HtmlWebpackPlugin
https://ema-hiro.hatenablog.com/entry/2017/10/12/015748

InterpolateHtmlPlugin
https://spectrum.chat/create-react-app/general/how-to-use-interpolatehtmlplugin-on-multiple-html-pages~d4fdc579-86cd-4d46-9503-33088c037164

ManifestPlugin
https://www.npmjs.com/package/webpack-manifest-plugin

WorkboxWebpackPlugin
https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin

ForkTsCheckerWebpackPlugin
https://www.npmjs.com/package/fork-ts-checker-webpack-plugin

Lambda(node.js)におけるaws.docClient.get(...).promise() のsinon.stubのつくりかた

$
0
0

やりたいこと

以下のlambdaのテスト対象コード(main.js)をMochaでtestするときに
docClient.get(...).promise() 部分のsinon.stubをつくりたい

main.js
constaws=require("aws-sdk");constdocClient=newaws.DynamoDB.DocumentClient(...);asyncfunctionmain(event){awaitdocClient.get(...).primise();}

結論のテストコード
(***で囲まれているところがスタブのつくり方、それ意外はモジュールの設定)

main_test.js
constassert=require('assert');constsinon=require("sinon");constaws=require("aws-sdk");constdocClient=newaws.DynamoDB.DocumentClient(...);describe("main_test",()=>{it("test1",async()=>{// ******ここスタブのつくりかたでハマった******letstub_DB=sinon.stub(docClient,'get').callsFake(()=>{return{promise:function(){returnPromise.resolve({data:111})}};});//stub_DBはdocClient.get(...).promies()が呼ばれると{date:111}をreturnするスタブ//***********************************});});

参考

https://stackoverflow.com/questions/47135483/how-to-stub-promise-method-of-node-aws-sdk

ECS(Node.js + TypeScript)からAWS DynamoDBから最新のデータを1件のみ取得するまで

$
0
0

バッチ処理をNode.js + TypeScriptで作成しており、そのバッチをECS on Fargateで起動しAWS DynamoDBから最新のデータを1件のみ取得する方法を紹介します。

その中でハマったポイントがいくつもあったため、同じ轍を踏むことがないようポイントをまとめておきます。

TL;DR

import*asAWSfrom'aws-sdk';import{DocumentClient}from'aws-sdk/clients/dynamodb';AWS.config.credentials=newAWS.ECSCredentials({httpOptions:{timeout:5000},// 5 second timeoutmaxRetries:10,// retry 10 times});constdynamodb=newAWS.DynamoDB();(async()=>{constparams:DocumentClient.QueryInput={TableName:"Hoge",KeyConditionExpression:"Code = :number",ExpressionAttributeValues:{":number":{"N":"1111"}},ScanIndexForward:false,Limit:1};constdata=awaitdynamodb.query(params,(err,data)=>{if(err){console.log("Error",err);}else{console.log("Success",data);}}).promise();console.log(data.Items[0].hoge.S);})();

ECSからDynamoDBへアクセスする方法

事前準備

事前にECSに付与するためのIAMロールを作成し、DyanamoDBへの権限を有したポリシーをアタッチしておきます。

次にECSのタスク定義、もしくはタスク実行時にタスクロールに先程作成したIAMロールを付与します。

プログラム

AWS.config.credentials=newAWS.ECSCredentials({httpOptions:{timeout:5000},// 5 second timeoutmaxRetries:10,// retry 10 times});

最新の1件を取得するQuery

始めはDocumentClientを利用していましたが、どうやっても上手くいかず最終的にはDocumentClientを使わない方法に落ち着きました。

※ schemaの不一致とエラーになる

(node:1) UnhandledPromiseRejectionWarning: ValidationException: One or more parameter values were invalid: Condition parameter type does not match schema type

※未だに原因がわかっていないので、コメント頂けると嬉しいです

パラメータの型にはDocumentClient.QueryInputを指定します。

この辺はググっても全然情報が出なかったので、sdkのGitHubから引っ張ってようやくわかりましたが、どこかに載ってるのかな??🤔
https://github.com/aws/aws-sdk-js/blob/master/lib/dynamodb/document_client.d.ts

最後に最新の1件のみを取得する方法ですが、Sort Keyが設定されている状態でScanIndexForward: falseを指定すると降順でデータを取得できます。

さらにLimit: 1を指定して、1件のみ取得することが可能です。

constparams:DocumentClient.QueryInput={TableName:"Hoge",KeyConditionExpression:"Code = :number",ExpressionAttributeValues:{":number":{"N":"1111"}},ScanIndexForward:false,// 降順(新しい順)Limit:1// 1件のみ};

ちなみにDynamoDBのNumber型を指定しているカラムも文字列としないとエラーになりましたので、数値が与えられる場合はString型にキャストしたほうが良いみたいです。

Expected params.Item['Code'].N to be a string
ExpressionAttributeValues:{":number":{"N":"1111"}},

おわりに

今回紹介した方法は調べながらトライアンドエラーで解決まで持っていったので、もっと良い方法があるかも知れません。

見識の広い方はコメント頂けると助かります🙏

LINE Blockchain を Node.js からたたきはじめてみた

$
0
0

どこかに nodejs SDK があるかもしれませんが、たぶん api exprorer のサンプルが python だけっぽいけどすぐに node.js も追加されそうな気がしますが、ちょっと生でたたきはじめてみました。

Tutorial Step 1. Create your service / 4b. Check the result with API

$ npm install axios
node.js
constcrypto=require('crypto');constaxios=require('axios').default;constendpoint='https://test-api.blockchain.line.me';constapikey='apikey'constsecret='secret';constmethod='GET';axios.defaults.baseURL=endpoint;asyncfunctioncallAPI(url){constnonce=crypto.randomBytes(16).toString('base64').slice(4,12);consttimestamp=Date.now();constsignature=crypto.createHmac('sha512',secret).update(`${nonce}${timestamp}${method}${url}`).digest('base64');varheaders={'service-api-key':apikey,'nonce':nonce,'timestamp':timestamp,'signature':signature};varoptions={method:method,url:`${endpoint}${url}`,headers:headers};try{letresult=awaitaxios.request(options);console.log(result.data.responseData)}catch(err){console.error(err.response.status,err.response.status.text);}}(async()=>{// 4b-a) Retrieve service wallet transaction history// $ curl -v -X GET https://test-api.blockchain.line.me/v1/wallets/{walletAddress}/transactions?type=token/MsgMint \//     -H 'service-api-key: {API Key}' \//     -H 'nonce: {nonce}' \//     -H 'timestamp: {timestamp}' \//     -H 'signature: {signature}'constwalletAddress='walletAddress';leturl=`/v1/wallets/${walletAddress}/transactions?type=token/MsgMint`awaitcallAPI(url);// b) Retrieve a transaction with txHash// curl -v -X GET https://test-api.blockchain.line.me/v1/transactions/{transaction_hash} \//     -H 'service-api-key: {API Key}' \//     -H 'nonce: {nonce}' \//     -H 'timestamp: {timestamp}' \//     -H 'signature: {signature}'consttransactionHash='transactionHash';// tx hashurl=`/v1/transactions/${transactionHash}`;awaitcallAPI(url);})();

Nuxt on Dockerでpuppeteer使ってスクレイピングする。

$
0
0

はじめに

あーあ、スクレイピングしたいな。

ということで Nuxt on Docker でスクレイピングします。
node系だとpuppeteerというライブラリがスクレイピングするのにおすすめっぽかったので、NuxtのserverMiddlewareからサクッとスクレイピングします。

あまり人に迷惑をかけてはいけないと言われて育ったので、スクレイピングは自分のサイトにします。(ログイン不要だよ。ぜひ使ってみてね♪)
toribure | ひとりでもチームでも使えるシンプルイズベストなブレストツール
image.png

やや宣伝ですね。トップページにかわいい鳥(いらすとや)の画像があります。今回はこれをスクレイピングで取ってきて表示させようと思います。

Nuxt on Dockerの準備

このあたりは他にも記事がいっぱいあると思うのでさらーっと。
ちなみに環境は

$ docker -v
  Docker version 19.03.13-beta2, build ff3fbc9d55
$ docker-compose -v
  docker-compose version 1.26.2, build eefe0d31

でした。

Nuxtアプリを作る

$ docker run --rm -it -w /app -v `pwd`:/app node yarn create nuxt-app scraping
? Project name: scraping
? Programming language: JavaScript
? Package manager: Yarn
? UI framework: None
? Nuxt.js modules: Axios
? Linting tools: 
? Testing framework: None
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Server (Node.js hosting)
? Development tools: 

axiosだけ後で使うので意識的に入れておきます。

ちなみに記事作成時点でnode:latestイメージのバージョンは14.9.0、create-nuxt-appのバージョンはv3.2.0で、nuxtは2.14.0が入りました。

Dockerfile, docker-compose.ymlを準備する

ここからは今できたばかりのscraping/ディレクトリが作業ディレクトリです。

$ cd scraping

Dockerfile
FROM nodeENV HOME=/app     \
    LANG=C.UTF-8  \
    TZ=Asia/Tokyo \
    HOST=0.0.0.0

WORKDIR ${HOME}RUN apt-get update \
&& apt-get install-y wget gnupg \
&& wget -q-O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& sh -c'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'\
&& apt-get update \
&& apt-get install-y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
--no-install-recommends\
&&rm-rf /var/lib/apt/lists/*COPY package.json ${HOME}COPY yarn.lock ${HOME}RUN yarn installCOPY . ${HOME}EXPOSE 3000CMD ["yarn", "run", "dev"]

RUN apt-get ...あたりはpuppeteerのトラブルシューティングに詳細があります。ブラウザやフォントがコンテナ内に用意されていないとエラーになるってことです。

docker-compose.yml
version:"3"services:nuxt:build:.volumes:-.:/appports:-3000:3000

ここまで終わったら一度コンテナをビルドしておきます。

$ docker-compose build

スクレイピングアプリを作るぞ

puppeteer導入

yarnでいれちゃいます。

$ docker-compose run --rm nuxt yarn add puppeteer

APIを作る

serverMiddlewareを使っていきます。公式でも紹介されているexpress-templateを参考にserverMiddlewareをAPIとして通してスクレイピングしていきます。

nuxt.config.js
exportdefault{...,serverMiddleware:{'/api':'~/api'}}

これで/apiのアクセスを~/api/index.jsに流します。のでファイル作ります。

$ mkdir api
$ touch api/index.js api/scraping.js

ファイルを2つ作りましたがindex.jsが受け口になっていて実処理はscraping.jsにやらせようと思います。

api/index.js
constapp=require('express')()constscraping=require('./scraping')app.get('/get_image',async(req,res)=>{constimage=awaitscraping.getImage()res.send(image)})module.exports={path:'/api',handler:app}

こちらは/api/get_imageにアクセスがあったらscraping.jsget_image()メソッドを呼び出すようにしてます。

api/scraping.js
constpuppeteer=require('puppeteer')asyncfunctiongetImage(){constbrowser=awaitpuppeteer.launch({args:['--no-sandbox','--disable-dev-shm-usage']})constpage=awaitbrowser.newPage()awaitpage.goto("https://toribure.herokuapp.com/")constimage=awaitpage.evaluate(()=>{returndocument.getElementsByTagName("main")[0].getElementsByTagName("img")[0].src})returnimage}module.exports={getImage}

ほぼpuppeteer公式のREADMEに則ってます。
page.evaluateを使うことで要素を取得したり操作したりできるんですね。
今回のスクレイピング先(https://toribure.herokuapp.com/)のHTML構造をDeveloper toolなどで見ると分かる通り、全体で1つしかないmain要素配下に1つしかないimg要素がターゲットとしてる鳥さんの画像です。(汚い構造してますがご愛嬌です)
そこまでわかれば通常のjsと同じように要素を取得するだけです。

ここまででAPIサイドはコーディング終了です。

フロントはさっくりと

もう疲れてきたのでフロントはボタン押したら画像表示されるくらいで。

pages/index.vue
<template><div><button@click="showBird">Scraping!!</button><br><imgv-if="src":src="src"></div></template><script>exportdefault{data(){return{src:""}},methods:{asyncshowBird(){this.src=awaitthis.$axios.$get("/api/get_image")}}}</script>

完成!!

動作確認

test.gif

かわいい鳥が出てきました♪

さいごに

ここまでできれば、あとはDOM操作の世界なので、対象のページの構造を理解してjsを書けば何でもスクレイピングできますね。
スクレイピングは禁止しているサイトもあるのでその点は注意しながらいろんなアイデアを実現できればいいですね!

参考

Viewing all 8825 articles
Browse latest View live