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

【Nuxt/Node】API_URLが便利

$
0
0

baseURLを動的に切り替える

baseURLを動的に切り替える方法が色々ありますが、下記の API_URLを設定する実装が楽なのでは?と思ったので、そのメモをしていきたいと思います。

baseURLの決まり方

基本は、defaultPortdefaultHostと順番に決まっていき、 optionsの中で baseURLが定義されています。
ただ例外として、 API_URLが定義されていると baseURLが全て上書きされることがわかります。
これを使っていきたいと思います。
axios-module/module.js at 932abc071b1e1bb64e8d8fc1fdd6e6f9ceb99b5a · nuxt-community/axios-module · GitHub

// module.jsで定義されている実装if(process.env.API_URL){options.baseURL=process.env.API_URL}

Nuxt.js 2.6.2 / axios 5.0.0 で、axios の deafaults.baseUrl を設定するハナシ - the industrial
サブディレクトリ動かすNuxt.jsでもルートディレクトリの.envを読み込む - Qiita

実装

ローカル環境

ローカル環境では、dotenvを使いました。

dotenvインストール

下記でdotenvをインストールします。
yarn add @nuxtjs/dotenv

.env作成

.envを作成し、そこに API_URLを書いておきます。
下記のようなイメージです。

API_URL=http://localhost:3000

baseUrl

ステージング環境、プロダクション環境

Kubernetesを使っている想定で、 ConfigMapAPI_URLを設定しするだけで終わりです。


Node.js Expressフレームワークを使用する(新規ページ作成)

$
0
0

はじめに

前回の投稿でExpressの雛形を生成しました。
今回は新規ページ(Hello World)を作成します。

環境

OS:Windows 10 Pro 64bit
node.js:v12.16.1
npm:v6.13.4
Express:v4.16.1

jsファイル作成

routesフォルダにhello.jsを作成します。

hello.js
varexpress=require('express');varrouter=express.Router();router.get('/',function(req,res,next){res.render('hello',{msg:'Hello World'});});module.exports=router;

ejsファイル作成

viewsフォルダにhello.ejsを作成します。

hello.ejs
<!DOCTYPE html><html><head><metacharset="utf-8"><title>Hello World</title></head><body><%=msg%></body></html>

app.jsの修正

ルート直下にあるapp.jsに次の2行を追加します。

var hello = require('./routes/hello');
app.use('/hello', hello);
app.js
varcreateError=require('http-errors');varexpress=require('express');varpath=require('path');varcookieParser=require('cookie-parser');varlogger=require('morgan');varindexRouter=require('./routes/index');varusersRouter=require('./routes/users');varhello=require('./routes/hello');// ←追加varapp=express();// view engine setupapp.set('views',path.join(__dirname,'views'));app.set('view engine','ejs');app.use(logger('dev'));app.use(express.json());app.use(express.urlencoded({extended:false}));app.use(cookieParser());app.use(express.static(path.join(__dirname,'public')));app.use('/',indexRouter);app.use('/users',usersRouter);app.use('/hello',hello);// ←追加// catch 404 and forward to error handlerapp.use(function(req,res,next){next(createError(404));});// error handlerapp.use(function(err,req,res,next){// set locals, only providing error in developmentres.locals.message=err.message;res.locals.error=req.app.get('env')==='development'?err:{};// render the error pageres.status(err.status||500);res.render('error');});module.exports=app;

以上で追加作業は終わりです。サーバーを実行して動作確認をします。

動作確認

コマンドプロンプトで次のコマンドを実行します。

npm start

ブラウザで「http://localhost:3000/hello」にアクセスします。
次のキャプチャが表示されればOKです。

express03.jpg

まとめ

このような流れでページ追加をすることが出来ます。

Angular + Expressの開発環境サンプル

$
0
0

はじめに

Angular + Expressの開発環境サンプルを作ったので公開。

Angular + Express Example

DBはPostgreSQLを使用しています。
最低限の構成で、簡単なCRUD機能を実現しています。
デモ用などに簡単なWEBアプリを作りたい時とかに、ベースにできればと。

動作環境

動作確認したときの環境は、以下の通り。

・node.js 12.16.1
・npm 6.13.4
・PostgreSql 12.1

機能

サンプル機能として、入退室時間とユーザを登録・表示する画面を作成しています。

ログイン画面

Angular+Express Example01.png

入退室管理画面

Angular+Express Example02.png

ユーザ管理画面

Angular+Express Example03.png

登録ダイアログ画面

Angular+Express Example04.png

プロジェクト構成

フロントエンドとバックエンドをひとまとめにしたようなプロジェクト構成にしています。

・ルートフォルダ ─┬─ front(angular)
          └─ server(express)

という構成です。
起動するのはサーバ側のnodeだけで、画面表示のリクエストもサーバ側(express)で受け付けて転送しています。

angular側も普通にng serveして別サーバにすることも可能ですが、小規模なものであればフロントエンドとバックエンドを一人で実装することも多いでしょうし、このほうが軽そうなので。

実行

実行方法は、git の ReadMe に記載の通りです。
PostgreSQLのデータベースを準備し、テーブル・初期ユーザを登録した後、ルートフォルダからnpmコマンド(npm installnpm run watchなど)を叩くだけです。

事前にインストールなどが必要なのは、
・PostgreSQL
・node(npm)
くらいのはずです。
(必要があればgitも。)

開発時の起動

ルートにてnpm run watchを実行すると、ソースの変更を監視して起動してくれます。
ブラウザにて、http://localhost:3000/にアクセスするとログイン画面が表示されるはずですので、

・ユーザID:1
・パスワード:123456

でログインしてください。

front/serverともに、ソースを修正・保存すると、自動的にビルドが走ります。
ブラウザは自動更新されませんので、必要に応じて手動でブラウザ更新してください。

デバッグ

サーバ側でデバッグしたいときは、VSCodeなら「Attach Node」でnodeのプロセスを選択するとブレークポイントを使用してデバッグが可能になります。
debug-01.png

クライアント側は・・・何も用意してないので、ブラウザのデバッグ機能を使ってください。
debug-02.png

実装についての補足

以下、細かい機能とその実装について補足します。

画面デザイン

画面デザインについては、プロジェクトによってそれぞれ見直しすることになると思いますので、かなり適当です。
いちおう、「Bootstrap」と「Angular Material」を使用してある程度は調整していますが、必要に応じて見直ししてもらえればと。

ルーティング

「server/src/app.ts」にて、リクエストのルーティングをしています。

app.ts
// Expressのルーティング(認証不要のもの)app.use('/api/',indexRouter);app.use('/api/common/auth',authRouter);// トークンの正常チェックapp.use('/api/*',async(req:any,res:any,next:any)=>{consttoken=req.headers['access-token'];if(!token){res.status(401);returnres.json({message:'No token provided'});}try{constdecoded=awaitjwt.verify(token,tokenConf.accessTokenSecretKey);req.decoded=decoded;next();}catch(e){res.status(401);returnres.json({message:e.message});}});// Expressのルーティング(認証が必要なもの)app.use('/api/common/db',dbRouter);// Angularのルーティングapp.use(express.static(path.join(__dirname,'../../front/dist')));app.use('/*',express.static(path.join(__dirname,'../../front/dist/index.html')));

まずはAPI側(URIが/api/・・・のもの)の処理として、認証されていなくても実行できるAPI(ログイン用のAPIなど)を受け付けます。
その次に認証チェックを行い、エラーの場合は401を返して終了します。
認証チェックOKの場合、その次で認証前提のAPIの処理をしています。

その次で、Angular側へのルーティングをしています。

ログイン

JWT形式のトークンで管理しています。
いちおう、アクセストークンとリフレッシュトークンを使用するようにはしていますが、管理方法はいまいちです。
・リフレッシュトークンをアクセストークンと同じようにLocalStrageで保存している。
・トークンをDBなどに管理しておらず、ログアウト時に破棄などもしていない。
・暗号化も適当。
実際に使えるようにするには、上記あたりも見直しが必要だと思います。
とりあえずはトークン生成、受け渡しのサンプルとして。

トークンのリフレッシュ

front/src/app/common/services/http.ts
で、アクセストークンの期限が切れていたら、リフレッシュトークンによる更新を試みるようにしています。
リフレッシュトークンの期限はデフォルトで180日にしてるので、一度ログインすると180日間ログイン状態を維持する動きになります。

interceptでやるのが一般的?かもしれませんが、どうにもキレイに実装できない(Callback地獄になってしまう・・・)ので、諦めて原始的な手法(HttpClient をラップする関数、callAPI を作成)で実装しました。

サーバ側のデータアクセス

「api/common/db」にて、とりあえずserver/src/config/sqlに定義したSQLを実行して返却する、というだけの共通的なAPIを用意しています。
実際には、それぞれ個別の処理を行うAPIを追加していくことになると思いますが、データアクセスのサンプルということで。

ダイアログ表示

client/src/app/view/common/dialogs/simpleDialog.componentにて、簡単なダイアログを共通的に表示できるようにしています。
これも、実際には個別にダイアログを増やしていくことになるかと思いますが、ダイアログ表示のサンプルとして。

列挙型、的な定義について

「1:男性, 2:女性」みたいな定義を汎用的に使用したいので、
「client/src/app/common/defines/enums.ts」
でまとめて定義しています。
表示変換用のpipeとかも同ファイルに定義していて、ちょっとゴチャっとしていますが・・・まあ、細かいファイル構成はご自由にして頂ければということで。

多言語化

ngx-translateを使用して多言語化対応しています。
よく考えたら、いうほど多言語化対応が必要になることもないので、サンプルとしては不要だったかも。
英文は、google先生まかせなので、怪しいです。

DBについて

DBはPostgreSQLを使用していますが、RDBであれば他のものに簡単に置き換えできると思います。(試してませんが。)
「server/src/utility/postgres.ts」
で一般的なコマンドをラップしてますので、そのあたりを書き換えればいけるかと。

その他、課題・雑感など。

・せっかくTypescriptにしてるのに、型定義をサボっているところが多いです・・・。
 特にサーバ側は、とりあえず Typescript にしたってだけの状態です。
 今だと、Nest.js みたいなフレームワークを導入したほうが幸せになれるかも?
・最低限のシンプルな構成にしたかったので、一般的なアプリケーション構成(サービス層とかコントローラ層とか)もバッサリ省略しています。
 必要に応じて分離して頂ければ。
・日付と時刻が同時に選択できるカレンダーが欲しくて、flatpickr を使ってみましたが・・・時刻入力の挙動がいまいちなような・・・。
 これなら、Angular Material の Picker を使ったほうがよかったかも。
・スマホでもそれなりに表示できるようにしましたが、グリッド表示などはいまいち・・・。
・Callbackな書き方が好きじゃないので、なるべくAsync-Awaitを使うようにしてます。
・Angular9が出たので、アップデートしてみました。
 レンダリングエンジンが変わったらしいですが、いまいちわかってません。
 ビルド時のメッセージがなんか変わったかなーくらいしか。。。
 それより、Typescript3.7対応になったので、Null条件演算子(hoge?.name みたいな書き方)が使えるようになったのが嬉しい。。。
・Heroku での実行手順を以下に投稿しました。
 Angular + Express + PostgreSQLのWEBアプリをHerokuで実行する手順

Node.js Expressフレームワークを使用してSQL Serverに接続する(準備作業)

$
0
0

はじめに

Node.js Expressフレームワークを使用して、SQL Serverのレコードを表示するページを作りたいと思います。
その前に、SQL Serverにテーブルを作成し、サンプルデータを挿入します。

データベース作成

「Training01」というデータベースを作成します。
T-SQLによるデータベース作成の詳しい説明はこちらを参考にして下さい。

CREATEDATABASETraining01;

テーブル作成

このようなテーブルを作成します。
sql01.jpg

テーブル作成
CREATETABLE[dbo].[ProductsMaster]([ProductsCode][char](8)NOTNULL,[ProductsName][nvarchar](50)NOTNULL,[UnitPrice][int]NOTNULL,CONSTRAINT[PK_ProductsMaster]PRIMARYKEYCLUSTERED([ProductsCode]ASC))ON[PRIMARY];

サンプルデータの挿入

次のSQL文でサンプルデータを挿入します。

INSERTINTOProductsMasterVALUES('A0000001','牛肉',200);INSERTINTOProductsMasterVALUES('A0000002','豚肉',150);INSERTINTOProductsMasterVALUES('A0000003','鶏肉',100);INSERTINTOProductsMasterVALUES('B0000001','玉子',200);INSERTINTOProductsMasterVALUES('C0000001','タマネギ',100);

確認

SELECT文を発行して確認します。

SELECT*FROMProductsMaster;

まとめ

以上で準備作業は終了です。

参考/出展

データベースの作成
https://docs.microsoft.com/ja-jp/sql/relational-databases/databases/create-a-database?view=sql-server-ver15

Cent OS 8にSQL Server 2019をインストール
https://qiita.com/t_skri/items/54e9a47706ba634a5558

Node.js Expressフレームワークを使用してSQL Serverに接続する(ページ作成)

$
0
0

はじめに

過去の投稿を参考にSQL Serverの準備をして下さい。

環境

OS:Windows 10 Pro 64bit
DB:SQL Server 2019(Cent OS 8 on Hyper-V)
node.js:v12.16.1
npm:v6.13.4
Express:v4.16.1
Editor:Visual Studio Code

Expressフレームワークの雛形作成

express --view=ejs
npm install

詳しい事はこちらを参考にして下さい。

SQL Serverへの接続ドライバ(tedious)インストール

npm install tedious --save
D:\Node\ExpressTest01>npm install tedious --save
npm WARN deprecated request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142
+ tedious@8.0.1
added 79 packages from 184 contributors and audited 263 packages in 16.976s
found 0 vulnerabilities

WARNと表示されますが、無視して大丈夫です。
今回はv8.0.1がインストールされました。

jsファイル作成

routesフォルダにsqlSample.jsを作成します。

sqlSample.js
varexpress=require('express');varrouter=express.Router();// Connectionを定義するvarConnection=require('tedious').Connection;// SQLServerの接続定義を記載する。varconfig={server:'xxx.xxx.xxx.xxx',// IPアドレスかサーバー名を指定する。authentication:{type:'default',options:{userName:'xxx',// 接続ユーザー名を指定する。password:'xxx'// 接続ユーザーのパスワードを指定する。}},options:{encrypt:true,database:'Training01'// データベース名を指定する。}};/* GET users listing. */router.get('/',function(req,res,next){varconnection=newConnection(config);varcontent=[];// DBからselectした結果を格納する変数// DB接続した際のイベントハンドラconnection.on('connect',function(err){if(err){// ERROR - SQL Serer connect error.console.log('SQL Serer connect error.('+err+')');// 終了process.exit();}console.log("connected");executeStatement();});// DB接続を終了した際のイベントハンドラ// DB接続を切断した後に画面を描写するconnection.on('end',function(){console.log("disconnected");res.render('sqlSample',{title:'製品一覧',content:content});});varRequest=require('tedious').Request;// SQLを発行する関数functionexecuteStatement(){// 発行するSQLを記載するrequest=newRequest("SELECT * FROM ProductsMaster with (NOLOCK)",function(err){if(err){console.log(err);}});varresult={};// SQLの結果を行ごとにオブジェクトに格納する。// SQLの行ごとに実行するイベントハンドラrequest.on('row',function(columns){columns.forEach(function(column){if(column.value===null){console.log('NULL');}else{result[column.metadata.colName]=column.value;}});content.push(result);result={};});// SQLのリクエスト完了時のイベントハンドラ。// コネクションをクローズしないとDBにいらないプロセスが残るので、コネクションをクローズする。request.on('requestCompleted',function(){console.log('requestCompleted');connection.close();});// DBへSQLを発行する。connection.execSql(request);}});module.exports=router;

ejsファイル作成

viewsフォルダにsqlSample.ejsを作成します。

sqlSample.ejs
<!DOCTYPE html><htmllang="ja"><head><metahttp-equiv="content-type"content="text/html; charset=UTF-8"><title><%=title%></title><linkrel='stylesheet'href='/stylesheets/style.css'/></head><body><h1><%=title%></h1><divrole="main"><table><tr><th>製品コード</th><th>製品名</th><th>単価</th></tr><%content.forEach(function(value,key){%><tr><td><%=value.ProductsCode%></td><td><%=value.ProductsName%></td><td><%=value.UnitPrice%></td></tr><%});%></table></div></body></html>

app.jsの修正

ルート直下にあるapp.jsに次の2行を追加します。

var sqlSample= require('./routes/sqlSample');
app.use('/sqlSample', sqlSample);

動作確認

コマンドプロンプトで次のコマンドを実行します。

npm start

ブラウザで「http://localhost:3000/sqlSample」にアクセスします。
次のキャプチャが表示されればOKです。

sql02.jpg

参考/出展

手順 3:Node.js を使用した SQL への接続を概念実証する
https://docs.microsoft.com/ja-jp/sql/connect/node-js/step-3-proof-of-concept-connecting-to-sql-using-node-js?view=sql-server-ver15

Node.jsでSQLServer2017に接続してSELECT結果を画面に表示するサンプル
http://hiyo-ac.hatenablog.com/entry/2018/01/28/141831

Docker コンテナを使って Node.js 開発を始める

$
0
0

logo-light.svg

この記事について

本記事は、Docker を使って Node.js 開発を始めるための方法について記載しています。

対象読者

  • Node.js を使って開発を始めたい方
  • Docker コンテナ上で Node.js アプリケーションを動かしたい方
  • ホスト OS を綺麗なまま Node.js の開発を行いたい方

はじめに

以前、npm パッケージ n を使って Node.js のバージョン管理を行う方法を Qiitaで投稿しましたが、正直なところ、開発は全部 Docker コンテナ上で行いたいのが理想でした。
そのため、今回は Docker Compose で定義したコンテナで Node.js を実行しようと思います。

環境構築

Docker インストール

OS が Windows または macOS の場合は、Docker Desktopをインストールします。
OS が Linux の場合は、以下の記事を参考にしてください。

Docker Compose 定義

Docker および Docker Compose をインストールしたら、Node.js を動かすコンテナを docker-compose.yamlに定義します。
任意のフォルダ/ディレクトリに移動し、docker-compose.yaml を作成します。

docker-compose.yaml
version:'3'services:app:image:node:lts# バージョン指定も可能 ex. node:12.16.1container_name:<任意のコンテナ名>tty:truevolumes:-./src:/srcworking_dir:"/src"

コンテナ起動

docker-compose.yaml ファイルを作成したら、コンテナを起動します。

docker-compose up -d

Docker Compose のログを確認します。以下のような実行結果になれば OK です。

docker-compose logs -f
実行結果
Attaching to <任意のコンテナ名>
<任意のコンテナ名> | Welcome to Node.js <バージョン情報>.
<任意のコンテナ名> | Type ".help" for more information.

Node.js バージョン確認

Docker コンテナ上で動いている Node.js のバージョンを確認します。

docker-compose run app node -v
実行結果(例)
v12.16.1

Node.js 開発

docker-compose.yaml ファイルと同じ階層に src フォルダ/ディレクトリが作成されていることを確認します。
src 以下に、新しく sample.js ファイルを作成します。

sample.js
console.log('Hello World!')

以下のコマンドを実行し、コンテナ上で Node.js を起動します。
Hello World!が結果として帰ってくれば OK です。

なお、node sample.jsの部分がコンテナ内で実行される内容です。単純な node だけでなく、npm installlsなどの Linux コマンドも実行可能です。

docker-compose run app node sample.js
実行結果
Hello World!
docker-compose run app ls-al
実行結果
total 8
drwxr-xr-x 3 root root   96 Mar  7 09:46 .
drwxr-xr-x 1 root root 4096 Mar  7 10:02 ..
-rw-r--r-- 1 root root   27 Mar  7 09:45 sample.js

関連リンク

参考情報

Qiita

nodejsのpathモジュールの使い方

$
0
0

pathモジュールの使い方をまとめました。

前提条件

  • npmがインストールされていること

使い方

設定

touchコマンドでファイルを作成します。

$ touch test.js
test.js
constpath=require('path')console.log('basename:',path.basename('./dir/test.txt'))console.log('dirname:',path.dirname('./dir/test.txt'))console.log('extname:',path.extname('./dir/test.txt'))console.log('parse:',path.parse('./dir/test.txt'))console.log('join:',path.join('dir','dir2','test.txt'))console.log('relative:',path.relative('./dir','./dir2/test.txt'))

実行

nodeコマンドを実行します。

$ node test

出力結果

basename: test.txt
dirname: ./dir
extname: .txt
parse: { root: '',
  dir: './dir',
  base: 'test.txt',
  ext: '.txt',
  name: 'test' }
join: dir/dir2/test.txt
relative: ../dir2/test.txt

参考文献

この記事は以下の情報を参考にして執筆しました。

nodejsのfsモジュールの使い方

$
0
0

fsモジュールの使い方をまとめました。

前提条件

  • npmがインストールされていること

同期と非同期

同期処理と非同期処理の2つのファイル処理が出来ます。
Syncと付けると同期処理で、付けないと非同期で処理されます。

同期非同期
statSyncstat
readFileSyncreadFile
copyFileSynccopyFile
unlinkSyncunlink
  • 同期は処理を完了するまで後続の処理を止める
  • 非同期は処理の完了を待たずに後続の処理を行う

設定

mkdirコマンドとtouchコマンドでファイルを作成します。

$ mkdir dir&&touch test.js dir/test.txt
test.js
constfs=require('fs')try{fs.statSync('dir/test.txt')console.log('同期処理')}catch(err){console.log(err)}fs.stat('dir/test.txt',(err)=>{if(err)throwerrconsole.log('非同期処理')})console.log('後続の処理')

実行

nodeコマンドを実行します。

$ node test

出力結果

同期処理
後続の処理
非同期処理

使い方

ファイルの存在確認

ファイルが存在していればオブジェクトを取得出来ます。
isDirectoryメソッドでディレクトリかどうかも確認が出来ます。

constfs=require('fs')try{constdir=fs.statSync('dir')constfile=fs.statSync('dir/test.txt')console.log(dir.isDirectory())console.log(file.isDirectory())}catch(err){console.log(err)}

実行結果

true
false

ファイル一覧を取得

constfs=require('fs')try{console.log(fs.readdirSync('dir'))}catch(err){console.log(err)}

実行結果

[ 'test.txt' ]

ファイルの読み込み

constfs=require('fs')try{console.log(fs.readFileSync('dir/test.txt','utf8'))}catch(err){console.log(err)}
dir/test.txt
Hello world!

実行結果

Hello world!

ファイルの書き込み

ファイルが存在しなければ新規作成して、存在する場合は上書きします。

constfs=require('fs')try{fs.writeFileSync('dir/test2.txt','Hello world!!','utf8')}catch(err){console.log(err)}

実行結果

dir/test2.txt
Hello world!!

ファイルの追記

constfs=require('fs')try{fs.appendFileSync('dir/test.txt','Hello overwrite!','utf8')}catch(err){console.log(err)}
dir/test.txt
Hello world!!

実行結果

dir/test.txt
Hello world!!
Hello overwrite!

ファイルの複製

constfs=require('fs')try{fs.copyFileSync('dir/test.txt','dir/test2.txt')}catch(err){console.log(err)}

ファイルの削除

constfs=require('fs')try{fs.unlinkSync('dir/test.txt')}catch(err){console.log(err)}

参考文献

この記事は以下の情報を参考にして執筆しました。


Angularの開発・実行環境について

$
0
0

前提

  • ローカルに node.js の環境が整備されていること。
  • docker 関連のコマンドがローカル環境で実行できること。
  • 実装はローカルの端末で行うが、実行は Docker のコンテナ上で行う。
    • 実装者の使い慣れた開発環境を使いながら、バージョン不整合による不具合等を事前に極力防止するため。

確認方法

node.js

$ node --version
v12.14.1
# npm --version
6.14.0

docker

$ docker --version
Docker version 17.12.1-ce, build 7390fc6
$ docker-compose --version
docker-compose version 1.25.0, build 0a186604
$ service docker status
 * Docker is running

環境

  • Windows 10 Home
  • WSL2 で Ubuntu を構築し、作業を行っていますが Docker 環境が作成済みであれば OS は問わない筈です。

Angular の開発環境を整備する

最新の lts 版をグローバルインストール

$ npm install -g @angular/cli

確認

$ ng version

     _                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/


Angular CLI: 9.0.4
Node: 12.14.1
OS: linux x64

Angular:
...
Ivy Workspace:

Package                      Version
------------------------------------------------------
@angular-devkit/architect    0.900.4
@angular-devkit/core         9.0.4
@angular-devkit/schematics   9.0.4
@schematics/angular          9.0.4
@schematics/update           0.900.4
rxjs                         6.5.3

Angular の実行環境を整備する

実現すること

  • 開発者全員が共通の Angular のバージョンを用いてビルドを行う。
  • ビルドした資産は、Nginx (Web サーバー)にデプロイして動作を確認する。
  • 大規模なプロジェクトでは、各開発者のローカル環境に同一の実行環境を整備することは困難なため、実行環境は Docker を用いて配布する。

手順

アプリのひな型を作成する

※サンプル用にプロジェクトを作成します。既存の Angular 資産が存在する場合は、本手順は省略してください。

$ ng new <アプリケーションの名前>

完成したアプリケーションは、以下のような構成となっていると思います。

<アプリケーションの名前>
|- e2e
|- node_modules
|- src
|- ... 各種設定ファイル
|- package-lock.json
|- package.json
|- ... 省略

Dockerfile をプロジェクトルートに配置する

<アプリケーションの名前>
|- e2e
|- node_modules
|- src
|- .dockerignore // <- 追加
|- ... 各種設定ファイル
|- Dockerfile // <- 追加
|- package-lock.json
|- package.json
|- ... 省略

Dockerfile

# ----------------------------------------# Angular build env.# ----------------------------------------FROM node:lts AS build_stageRUN npm install-g @angular/cli
WORKDIR /usr/src/appCOPY package.json ./RUN npm installCOPY . .RUN ng build --prod# ----------------------------------------# Nginx# ----------------------------------------FROM nginx:stableCOPY --from=build_stage /usr/src/app/dist/<アプリケーションの名前> /usr/share/nginx/html

DockerHub に公式の Angular イメージがなかったため、自作した。

  1. ベースイメージの node.js の実行環境に@angular/cliをグローバルインストールする。
  2. ローカルのpackage.jsonをコンテナ環境にコピー
  3. 依存ライブラリをコンテナ内でインストールさせる。(ローカルの node_modules をマウントする手もあるが、Windows 環境だとうまくいかないらしいのでこの方法を採用)
  4. angular cli コマンドを用いてビルド
  5. Nginx のドキュメントルートにビルドした資産を配置する。

.dockerignore

node_modules
e2e

テストコードや、node_modules は、コピーの対象外とする。

Docker Image のビルド

$ cd <アプリケーションの名前>
$ docker build -t angular:lts .

※warn メッセージが出てくるがいったん無視する。

動作確認

以下の構成になるようにする。

.
|- <アプリケーションの名前>
|   |- src
|   |- ...
|   |- Dockerfile
|- docker-compose.yml
version:'3.3'services:front-app:image:angular:ltsbuild:.ports:-'80:80'environment:TZ:'Asia/Tokyo'

docker-compose.yml のディレクトリで以下のコマンドを実行する。

$ docker-compose up -d

http://localhostにアクセスし Angular のトップページが確認できれば終了。

おわりに

開発環境は、自分の使い慣れた環境を使いたい!という思いで、折衷案を考えましたが、
実行環境のみを配布してもバージョン不整合などを完璧に防ぐことは難しいと考えています。Angular 本体の組み込み Web サーバーで動作確認してコミットなどされたら防ぐことはできないので。。やはり、「コミット前に確認をすること!」等のルールで縛るのではなく、仕組みで縛れる様にしたほうがいいのではないか?と書きながら思いました。

参考

Square APIを使ってみる(在庫数の取得)

$
0
0

Square APIを使って、在庫数の取得まで出来たのでまとめておきます。

アクセストークンの取得

Application Dashboardで、新しいアプリケーションを作成すると、アプリケーションIDとアクセストークンが作られます。

実際の課金など発生しないSandboxモードと、本番環境のProductionモードがあり、それぞれアプリケーションIDとアクセストークンが違うので、まずはSandboxモードを使いましょう。

Image from Gyazo

curlでAPIを叩いてみる

curl -X POST \
-H 'Accept: application/json' \
-H 'Authorization: Bearer {{ACCESS_TOKEN}}' \
-H 'Cache-Control: no-cache' \
-H 'Content-Type: application/json' \
-d '{
    "idempotency_key": "TESTKEY123",
    "autocomplete": true,
    "amount_money": {
      "amount": 100,
      "currency": "JPY"
    },
    "source_id": "cnon:card-nonce-ok"
    }
}' \
'https://connect.squareupsandbox.com/v2/payments'

{{ACCESS_TOKEN}}を、先ほど取得したSnadboxのアクセストークンに置き換えます。

ドキュメントでは、curencyが、USDでしたが、JPYにしろ!ってERRORが表示されたので、JPYにしたところレスポンスが返ってきました。

Square Connect Node SDKを使ってAPIアクセス

Node.jsのSDK「Square Connect Node SDK」を使ってアクセスしてみます。

Square Connect Node SDKをインストール

$ yarn add square-connect

サンプルコードを動かしてみます。

varSquareConnect=require('square-connect');vardefaultClient=SquareConnect.ApiClient.instance;// Set sandbox urldefaultClient.basePath='https://connect.squareupsandbox.com';// Configure OAuth2 access token for authorization: oauth2varoauth2=defaultClient.authentications['oauth2'];// Set sandbox access tokenoauth2.accessToken="YOUR SANDBOX ACCESS TOKEN";// Pass client to APIvarapi=newSquareConnect.LocationsApi();api.listLocations().then(function(data){console.log('API called successfully. Returned data: '+JSON.stringify(data,0,1));},function(error){console.error(error);});

実行すると、以下のように表示されました。

API called successfully. Returned data: {
 "locations": [
  {
   "id": "M513Y0SCF175A",
   "name": "Default Test Account",
   "address": {
    "address_line_1": "1",
    "locality": "Chiyoda City",
    "administrative_district_level_1": "13",
    "postal_code": "100-0014",
    "country": "JP"
   },
   "timezone": "Etc/UTC",
   "capabilities": [
    "CREDIT_CARD_PROCESSING"
   ],
   "status": "ACTIVE",
   "created_at": "2020-03-07T10:20:33Z",
   "merchant_id": "VFZJMXEYNJVYN",
   "country": "JP",
   "language_code": "ja-JP",
   "currency": "JPY",
   "business_name": "Default Test Account",
   "type": "PHYSICAL",
   "business_hours": {},
   "mcc": "5944"
  }
 ]
}

Sandboxに入っている店舗情報が返ってきました。

商品リストを取得してみる

CatalogApiのlistCatalogを使って、商品のリストを取得してみます。

どうもSandboxは、商品リストが空っぽのようで、何も結果が返ってこなかったので、本番のアクセストークンを使ってみました。

varSquareConnect=require('square-connect');vardefaultClient=SquareConnect.ApiClient.instance;// Configure OAuth2 access token for authorization: oauth2varoauth2=defaultClient.authentications['oauth2'];oauth2.accessToken='YOUR ACCESS TOKEN';varapiInstance=newSquareConnect.CatalogApi();varopts={'types':'ITEM'};apiInstance.listCatalog(opts).then(function(data){console.log('API called successfully. Returned data: ');console.log(JSON.stringify(data,null,2));},function(error){console.error(error);});

以下のような出力になりました。

{"objects":[{"type":"ITEM","id":"E5OD3ACUQ6GOTA2KFGTVDT5L","updated_at":"2020-01-12T11:06:49.742Z","version":1578827209742,"is_deleted":false,"catalog_v1_ids":[{"catalog_v1_id":"9B78CDF8-8D3D-4D23-8F16-3E7D9534B9E8","location_id":"454ACGR0V7GPA"}],"present_at_all_locations":false,"present_at_location_ids":["454ACGR0V7GPA"],"item_data":{"name":"鯉口 虹","label_color":"593C00","available_online":false,"available_for_pickup":false,"available_electronically":false,"category_id":"B42OYZA5ENOYOVVYIDT6F5T2","tax_ids":["OIB76Q3LPWOXXKKRVJLFACKB"],"variations":[{"type":"ITEM_VARIATION","id":"5B3FDDMKODDFS5WP2NIU5ZTO","updated_at":"2020-01-12T11:06:49.742Z","version":1578827209742,"is_deleted":false,"catalog_v1_ids":[{"catalog_v1_id":"993447A1-EB87-4BAA-AA98-1124D2A853DE","location_id":"454ACGR0V7GPA"}],"present_at_all_locations":false,"present_at_location_ids":["454ACGR0V7GPA"],"item_variation_data":{"item_id":"E5OD3ACUQ6GOTA2KFGTVDT5L","name":"M","sku":"","ordinal":0,"pricing_type":"VARIABLE_PRICING","location_overrides":[{"location_id":"454ACGR0V7GPA","track_inventory":true}],"service_duration":0,"available_for_booking":false,"transition_time":0}},{"type":"ITEM_VARIATION","id":"MWZZO64LINWIJVNIA7FDQQVR","updated_at":"2020-01-12T11:06:49.742Z","version":1578827209742,"is_deleted":false,"catalog_v1_ids":[{"catalog_v1_id":"89215CEA-730E-4E3E-8C5E-88F88CAE5BC0","location_id":"454ACGR0V7GPA"}],"present_at_all_locations":false,"present_at_location_ids":["454ACGR0V7GPA"],"item_variation_data":{"item_id":"E5OD3ACUQ6GOTA2KFGTVDT5L","name":"S","sku":"","ordinal":1,"pricing_type":"VARIABLE_PRICING","location_overrides":[{"location_id":"454ACGR0V7GPA","track_inventory":true}],"service_duration":0,"available_for_booking":false,"transition_time":0}}],"product_type":"REGULAR","skip_modifier_screen":false}},{"type":"ITEM","id":"6CZOC256NZGH7R3Z5IRADPX5","updated_at":"2019-03-08T00:44:30.092Z","version":1552005870092,"is_deleted":false,"catalog_v1_ids":[{"catalog_v1_id":"64B15EC3-36FC-4511-8CD2-96F7A150B751","location_id":"454ACGR0V7GPA"}],"present_at_all_locations":false,"present_at_location_ids":["454ACGR0V7GPA"],"item_data":{"name":"帽子","label_color":"13B1BF","available_online":false,"available_for_pickup":false,"available_electronically":false,"category_id":"LE6QTI6LQXHOOKH6GEHTLCUU","tax_ids":["OIB76Q3LPWOXXKKRVJLFACKB"], : :

在庫数を取得してみる

在庫数の取得は、InventoryApiのretrieveInventoryCountを使います。

varSquareConnect=require('square-connect');vardefaultClient=SquareConnect.ApiClient.instance;// Configure OAuth2 access token for authorization: oauth2varoauth2=defaultClient.authentications['oauth2'];oauth2.accessToken='YOUR ACCESS TOKEN';varapiInstance=newSquareConnect.InventoryApi();varcatalogObjectId="catalogObjectId_example";// String | ID of the `CatalogObject` to retrieve.varopts={//  'locationIds': "locationIds_example", // String | The `Location` IDs to look up as a comma-separated list. An empty list queries all locations.//  'cursor': "cursor_example" // String | A pagination cursor returned by a previous call to this endpoint. Provide this to retrieve the next set of results for the original query.  See the [Pagination](https://developer.squareup.com/docs/docs/working-with-apis/pagination) guide for more information.};apiInstance.retrieveInventoryCount(catalogObjectId,opts).then(function(data){console.log('API called successfully. Returned data: ');console.log(JSON.stringify(data,null,2));},function(error){console.error(error);});

catalogObjectIdには、listCatalogで取得したアイテムのID
を入れます。

item_idという値もありますが、ここで使うのはidの値です。

「鯉口 虹」のバリエーション「S」のIDは、MWZZO64LINWIJVNIA7FDQQVRなので、この値をcatalogObjectIdに入れて在庫数を取得します。

結果は以下のようになりました。

{"counts":[{"catalog_object_id":"MWZZO64LINWIJVNIA7FDQQVR","catalog_object_type":"ITEM_VARIATION","state":"IN_STOCK","location_id":"454ACGR0V7GPA","quantity":"1","calculated_at":"2020-01-12T11:06:41.11211Z"}]}

在庫数は1です。

リクルートスピードハッカソンに参加したのでその感想や勉強したことまとめ

$
0
0

リクルートFrontend スピードハッカソン

やったこと

参加者の中で3〜4人の間でチームを組みgitで共有されたホットペッパービューティーの1ページのパフォーマンス性能をできるだけ高めるというものでした。

ちなみに僕のチームの結果はこんな感じです

スタート時

スクリーンショット 2020-03-07 20.27.48.png

終了時

スクリーンショット 2020-03-07 20.35.57.png

約5時間の調整でここまでの成果が、、、
チーム内にすでにがっつり実務でフロントエンドやっている方がいたのでその方のおかげもありパフォーマンス性能を60以上あげることができました。
ただ、今回参加した全8チーム中僕たちのチームは5位でした。。。
全体的にとてもレベルが高く、周りのレベルの高さに驚きですね😅

パフォーマンス効率改善方法

今回のイベントでのパフォーマンスの改善方法をざっくりと説明していきます。

※基本的にはパフォーマンスチューニング童貞でも大枠くらいはわかるようにするために、できるだけ簡単な言葉でざっくりと説明しています。もし、ちゃんと正確な事知りたい!という人は他の記事を読むことをお勧めします笑

まずは測定!、そして改善へ!

  1. chromeで対象の計測するサイトを開く
  2. 検証ツールを開く(macだと「⌘ + ⌥ + i」で開きます)
  3. 検証ツールの上のタブからAuditsを選択
    スクリーンショット 2020-03-07 23.03.43.png
  4. 「Generate report」を押す
    スクリーンショット 2020-03-07 23.09.04.png
  5. するとこんな感じのレポートを出してくれます!(もちろんlocalhostでも検証できます!) スクリーンショット 2020-03-07 23.12.13.png
  6. スクロールするとOpportunitiesという項目があるのでその項目を愚直に潰していく スクリーンショット 2020-03-07 21.00.49.png

基本的にはOpportunitiesに書かれていることを潰していくだけです。
僕たちのチームはほぼこれを潰しただけでしたが、60以上もスコアが改善されました。

今回対処したOpportunitiesとその方法

Enable text compression(テキスト圧縮して送ってよw)

スクリーンショット 2020-03-07 23.55.55.png
なんか色々書いていますが、結局何が言いたいかと言えば テキストは圧縮して送れよ!これだけです笑
なので今回はnode.jsの圧縮ライブラリであるcompressionを用いてテキストをgzip形式で圧縮して送信しました

※ちなみに最適解はbrotliという形式での圧縮をすることだったそうです。

Properly size images(画像サイズ大きすぎね?)

スクリーンショット 2020-03-07 23.34.27.png
めちゃ簡単にいうと、画像サイズ不適切だから適切なファイルサイズに圧縮しろよな!っていう警告です
色々とやり方はあると思うのですが、今回は

Squoosh

という画像圧縮webアプリを使いました。(使い方は下記サイトを見てください)
https://blog.marswee.com/entry/squoosh-how-to

ついでに今回は画像関連で

Serve images in next-gen formats(古いJPEGとかPNGとか使うなよ〜)

っていうエラーも出ていたのでwebpという形式で画像を変換、圧縮しました。
webpの難しいことについてはそれぞれ調べていただくとしてざっくり特徴を書くと、、、

  • 結構軽いよ〜!
  • でも対応してないブラウザあるから気をつけてね!

こんな感じです。
おそらく今回のハッカソンではchromeでしか検証を行っていないのでwebp形式に変換してhtml側の拡張子も.webpにすればよかったと思うのですが、本来ならば <picture>タグというタグを使ってユーザーの閲覧環境に合わせて画像を出し分ける処理を書く必要があります。(全部対応しようとすると結構大変です笑)

Eliminate render-blocking resources(読み込むファイルが多いんじゃ!)

スクリーンショット 2020-03-08 0.27.29.png
こいつはページ表示するまでに読み込むもの多すぎてページ表示させるの遅れてるよ!ってやつです。
そもそもwebサイトの表示はざっくり

1. ブラウザーがHTMLファイルをダウンロードする
2. HTMLファイルの読み込みと、CSSファイル、JavaScriptファイル、画像ファイルの確認
3. 画像ファイルをダウンロード
4. ページを表示するためにCSSファイルとJavaScriptファイルが必要になる
5. CSSファイルをダウンロードして読み込む ←ファイル読み込んでるせいでページ表示できない
6. JavaScriptファイルをダウンロードして読み込む ←ファイル読み込んでるせいでページ表示できない
7. Webページを表示する

という順番で表示されています(下記のURL参考)
https://goworkship.com/magazine/rendering-block/

これの一番簡単な方法としては
Minifyしてhtmlにインラインで書き込むという方法です

※ Minifyってのはcssとかの不要なスペースや改行を全部無くしてファイルサイズを小さくする方法です!(jsファイルが無駄に長い1行のファイルになってることあるでしょ?あれですw)
↓みたいなサイトを使って簡単にMinifyをすることができます
https://csscompressor.net/ja/

※ インラインで書き込む、、、
つまり本来↓のような形でcssファイルを読み込むのですが、、、

<linkrel="stylesheet"type="text/css"href="example.css">

↓みたいにMinifyしたcssを直接html内に書き込むということです。

<style>
  body,dd,div,dl,dt,h1,img,li,ol,p,table,td,ul{margin:0;paddi......
</style>

Defer offscreen images(画像はあとで読み込んだら?)

スクリーンショット 2020-03-08 0.27.29.png
これも上記と同様に読み込む系のやつなのですが、画像ファイルはどうしても外部から読み込む必要があるのでスクロールしていって必要になったら画像を読み込むようにします(下記のURL参照)
https://webtan-tsushin.com/seo-lazy-image-load

Remove unused CSS(使ってないcssあるんだけどw)

これはその名の通り使ってないcssがあるから消してねっていうやつです
今回は↓あたりを参考に使ってないcssを削除しました
https://www.monotalk.xyz/blog/Use-UnCSS-with-gulp/

Preconnect to required origins(どうせ使うなら先に接続したら?)

スクリーンショット 2020-03-08 1.48.52.png
そもそも外部のサーバーとの接続は

  • DNS look up
  • リダイレクト

などなど、たくさんの処理があり、それらの処理を低速ネットワーク下でやるとかなり時間がかかってしまいます。(とりあえず、結構いろんな処理走っててネット環境悪いと遅くなっちゃうってことです。)なので、これらの処理を事前にやっておくことで処理を早くすることができます。
対応方法は簡単でheadタグ内に

<linkrel="preconnect"href="https://example.com">

とを入れるだけです

※ ちなみに写真にあるt.tgknt.comはソース内に存在しなかったので何なのかとちょっと調べてみたら、広告配信するときに使うバナーを生成するドメインみたいです。(間違ってたら誰か教えてください汗)

以上が
Opportunitiesのざっくりとした対応です

まとめ

正直僕が今まで参加して来たインターンの中では一二を争うレベルでめちゃめちゃ為になるイベントだと思います!
イベントの結果発表後には改善のポイントや解説はもちろんのこと、リクルート社員さんのLT会もありそれぞれのLT全てがとても為になるものでした。
このイベントは去年からやっているイベントらしく、おそらく来年も開催されるのではないかと思うのでぜひ参加してみるのをオススメします!

Electronで1からデスクトップアプリを作り、electron-builderを使ってビルド・リリースするまで

$
0
0

この記事について

この記事では、Electronを使ってデスクトップアプリを作成し、それを配布可能な状態にビルドするまでの過程を紹介します。
また、Electronアプリを作る際に、知っておくと便利な知識・ライブラリもあわせて紹介します。

使用する環境・バージョン

  • OS : MacOS Mojave ver 10.14.5
  • node.js v12.13.0
  • npm 6.13.4
  • electron 8.0.1
  • electron-builder 22.3.2

前提条件

  • node.jsとnpmは既にインストール済みで使用可能な状態とします。

読者に要求する前提知識

  • 基本的なunixコマンドの意味がわかり、ターミナルで実行できること。
  • Javascriptの基本的な文法がわかること。

Electronとは?

Githubによって開発された、クロスプラットフォームデスクトップアプリのフレームワークです。
クロスプラットフォームなので、Electronで作成したアプリは、MacでもWindowsでも動きます。
また、アプリの画面を作るにあたってhtmlとcss, jsといったWebフロントエンドの技術を使うので、Web系の知識がある人にとっては敷居が低いツールです。
参考:Electron公式ドキュメント

Electronのアプリケーションアーキテクチャ

Electronの仕組みは以下のようになっています。
electron-security-2.png
引用:DeNA Engineers' Blog 「Electronのセキュリティは難しい?」

メインプロセス

アプリの画面ウィンドウを生成して、起動・終了などのアプリ本体の制御を行います。1つのアプリに対してメインプロセスは1つだけです。
アプリのウィンドウ生成をBrowserWindowインスタンスの作成で行います。
Node.jsで動いています。つまり、npmモジュールや、ファイルの読み書きやネットワークなどのOS機能をAPI経由で使うことができます。

レンダラープロセス

メインプロセスで作成されたアプリ画面をChromiumでレンダリングして表示します。1つのアプリに対して複数個(=画面の数だけ)用意することができます。
アプリの画面レイアウト・装飾をhtml/CSS・JSで行います。
レンダラープロセスで使える機能は基本的にブラウザ上で動くJavascript(+α)です。

プロセス間通信(IPC通信)

メインプロセスとレンダラープロセス間でやりとりをする&レンダラープロセスから機能を呼び出すためには、IPC通信というものを使います。
IPC通信をするためには、メインプロセス側ではipcMainモジュールを、レンダラープロセス側ではipcRendererモジュールをインポートする必要があります。

レンダラープロセス→メインプロセス

レンダラープロセス側からデータを送る場合は、ipcRendererモジュールのAPIを呼び出す形になります。

(レンダラープロセス)index.js
//ipcRendererモジュールをインポートconst{ipcRenderer}=require("electron");//メインプロセスのipcMain.on("test-send")に変数dataを送るipcRenderer.send("test-send",data);

メインプロセス側では、ipcMainモジュールのAPIでそれを受け取ります。

(メインプロセス)main.js
//ipcMainモジュールをインポートconst{ipcMain}=require("electron");//レンダラープロセスから送られたdataの内容がargに格納されているipcMain.on("test-send",(event,arg)=>{//処理});

メインプロセス→レンダラープロセス

メインプロセス側からデータを送る場合は、基本的にレンダラープロセス側からのイベントに返信という形になります。
レンダラープロセスから送られたtest-sendイベントに、test-replyというチャネル名でdataを送ります。

(メインプロセス)main.js
const{ipcMain}=require("electron");ipcMain.on('test-send',(event,arg)=>{event.reply('test-reply',data)})

レンダラープロセス側では、ipcRendererモジュールのAPIでそれを受け取ります。

(レンダラープロセス)index.js
const{ipcRenderer}=require("electron");//メインプロセスから送られたdataの内容がargに格納されているipcRenderer.on('test-reply',(event,arg)=>{//処理})

参考:Electron アプリケーションアーキテクチャ
参考:ようこそ!Electron入門
参考:今日から始める Electron

ipcMain/Rendererの関数

ipc通信をするためのAPIはipcMain/Rendererモジュール内に他にも存在します。
詳しくは公式ドキュメントを参照してください。

アプリの作成

0.ディレクトリ・ファイル構造

一覧

アプリのソースを入れるフォルダを一つ作成してください(ここではappフォルダとします)。
今後ここがアプリのルートディレクトリになります。

$ mkdir app

このappフォルダの中に、最終的に以下のような構造になるようにファイルを配置します。

/app #アプリのルートディレクトリ
  ├─assets #アプリのアイコンを格納
  │  ├─mac
  │  │  └─icon_mac.icns #Mac用のアプリアイコン
  │  └─win
  │     └─icon_win.ico #windows用のアプリアイコン
  ├─dist #ビルドされたアプリの格納場所
  ├─node_modules
  ├─package.json
  ├─package-lock.json
  └─src
     ├─main.js    #メインプロセス
     ├─preload.js
     ├─index.html #レンダラープロセス
     ├─index.css  #レンダラープロセス
     └─index.js   #レンダラープロセス

git管理に含めるファイル・含めないファイル

開発にあたり、ソースコードをgit管理したいという人もいるでしょう。
基本的には問題ないのですが、それをGithubにpushしたいと考えると、一部のファイルは管理対象外にした方が無難です。
というのも、Githubは大容量ファイルのpushを拒否するようになっているからです。(50MB超で警告、100MB超で拒否)
参考:Github公式ドキュメント 大容量ファイルの制限

そのため、以下のディレクトリは.gitignoreに追加しておきましょう。

  • dist
    ビルドされたアプリ(数10MBになります)がこのディレクトリに入ります。当然重いです。
  • node_modules
    npmモジュールのソースがそのままここに入るので当然重くなります。

1.プロジェクトの作成

アプリルートディレクトリの中に、node.jsのプロジェクトを作成します。

$ cd app
$ npm init -y

このコマンドを実行すると、ルートディレクトリ中にpackage.jsonファイルが作成されます。
そのpackage.jsonを以下のように編集します。

package.json
{"name":"your-app-name","version":"1.0.0","description":"your app's description","main":"src/main.js","scripts":{"test":"echo \"Error: no test specified\"&& exit 1"},"keywords":[],"author":"your-name","license":"ISC"}

jsonの項目の意味は以下の通りです。

  • name : アプリの名前。デフォルトだとnpm initをしたディレクトリの名前
  • version : アプリのバージョン。デフォルトは1.0.0
  • description : アプリの説明。
  • main : メインプロセスの相対パス。
    デフォルトはindex.jsだが、Electronアプリではメインプロセスのファイル名はmain.jsとするのが一般的。
  • scripts : 後述(アプリ起動の項で解説)。
  • keywords : 今回はさして重要ではないので放置。(本来はnpm searchされたときの検索キーワード)
  • author : アプリの作者名。
  • license : 配布時のライセンス。デフォルトはISC

参考:npm公式ドキュメント npm-package.json
参考:package.jsonの内容をまとめてみました

2.Electronのインストール

ルートディレクトリ直下で以下のコマンドを実行して、Electronをインストールします。

$ npm install-D electron

注意:インストールに時間がかかる場合がありますが、気長に待ちましょう。
注意:上記のようにdevDependenciesとして追加しないと、ビルド時にエラーが出ます。

この時点で、package-lock.jsonと、node_modulesが作成されます。

3.メインプロセスの作成

スクリプト作成

srcフォルダの中に、メインプロセスのコードを記述するmain.jsを作成します。

$ mkdir src
$ cd src
$ touch main.js

そうして作成したmain.jsに以下のように書き込みます。

src/main.js
'use strict';//モジュールを使えるようにするconst{app,BrowserWindow}=require("electron");// メインウィンドウはGCされないようにグローバル宣言letmainWindow;//アプリの画面を作成functioncreateWindow(){mainWindow=newBrowserWindow({width:800,height:600,webPreferences:{nodeIntegration:false,contextIsolation:false,preload:__dirname+'/preload.js'}});mainWindow.loadURL('file://'+__dirname+'/index.html');}// Electronの初期化完了後に実行app.on('ready',function(){createWindow();});//アプリの画面が閉じられたら実行app.on('window-all-closed',()=>{// macOSでは、ユーザが Cmd + Q で明示的に終了するまで、// アプリケーションとそのメニューバーは有効なままにするのが一般的です。if(process.platform!=='darwin'){mainWindow=null;app.quit()}});app.on('activate',()=>{// macOSでは、ユーザがドックアイコンをクリックしたとき、// そのアプリのウインドウが無かったら再作成するのが一般的です。if(BrowserWindow.getAllWindows().length===0){createWindow()}});

このコード内では主に二つのモジュールを使用しています。

  • app : アプリの起動・終了の制御を行う
  • BrowserWindow : アプリ画面の制御を行う

参考:Electron公式ドキュメント 3分でわかるElectronアプリ開発
参考:30分で出来る、JavaScript (Electron) でデスクトップアプリを作って配布するまで

mainWindowの設定

new BrowserWindowの作成の際に指定したオプション"webPreferences"の項目について解説します。

  • nodeIntegration : レンダラープロセスでNode.jsの機能を使えるようにするか。false推奨
  • contextIsolation : それぞれのプロセスを別々のJSコンテキストで実行するかどうか(詳細はpreload.jsの項目で解説)。
  • preload : レンダラープロセス実行前に読み込まれるスクリプトを指定。

nodeIntegration: falseの重要性

先ほども述べたとおり、レンダラープロセスで使えるのは基本的にはブラウザ上で使えるJavascriptです。
しかし、ブラウザ上のJSにはセキュリティ上の理由で制限されている機能・実現不可能なことがあります。
例えば、<input type="file"/>で設置されたフォームから入力されたファイルについて、JS側でvalue値を取得しようとするとファイル名のみが取得され、ローカルマシン上でのフルパスが入手できないようになっています。
参考:javascript - C:\ fakepathを解決する方法は?

そのため、例えば「ユーザーのローカルPC上にあるファイルを選択・中身を表示させるようなアプリを作るために、選択したファイルのパスをレンダラープロセスで取得したい」という場合は、Node APIを使う必要があります(Node.jsはサーバーサイドの環境なので、OSの機能を使うことができます)。

しかし、レンダラープロセス(=ユーザーが触れるアプリ画面(ブラウザ画面))でNode.jsの機能を使うことを認めてしまうと、クロスサイトスクリプティング(XSS)が発生することがあり危険です。

クロスサイトスクリプティングは、Webサイト閲覧者側がWebページを制作することのできる動的サイト(例:TwitterなどのSNSや掲示板等)に対して、自身が制作した不正なスクリプトを挿入することにより起こすサイバー攻撃です。
出所:クロスサイトスクリプティングとは?仕組みと事例から考える対策

つまり、アプリの画面からNode APIを使ってOSの機能を呼び出して、

  • ファイルデータの改ざん・消去
  • ローカルマシンの情報を取得・外部に送信

ということが可能になってしまいます。

実際に、ElectronのアプリでXSSを発生させ、ローカルマシンのデータを全消去させたという実験記事があるので挙げておきます。
ElectronアプリのXSSでrm -fr /を実行する

そのため、nodeIntegrationの値をfalseにしてレンダラープロセスでOSの機能にアクセスさせないことが推奨されているのです。

ここで、Electronアプリ開発の際に極めて重要な心構えが公式ドキュメントに記載されていたので、引用しておきます。

Electron で開発する時、Electron はブラウザではないということを意識することが重要です。 使い慣れたウェブ技術を使用して、機能あふれるデスクトップアプリケーションを構築できますが、あなたのコードの方がはるかに大きな力を発揮します。 JavaScript はファイルシステム、ユーザシェルなどにアクセスできます。 これはつまり、質の高いネイティブアプリケーションを作成することができる反面、あなたの書くコードに与えられた権限に応じて固有のセキュリティリスクが増加するということです。

それを念頭に置いて、信頼できないソースからの任意のコンテンツを表示するということは、Electron が扱うことを意図しない重大なセキュリティリスクを引き起こすということに注意してください。 実際、人気のある Electron アプリ (Atom、Slack、Visual Studio Code、等) は、主にローカル (あるいは信頼されており、なおかつ Node integration を使用しないリモート) のコンテンツを取り扱います。もしあなたのアプリケーションがオンライン上のリソースからコードを実行する場合、あなたの責任の下でそのコードが悪意のあるものではないことを確認する必要があります。

Electron公式 セキュリティ、ネイティブ機能、あなたの責任

しかし、このままではNode.jsで提供されている多くの便利なnpmモジュールやElectron APIがレンダラープロセスで利用不可になってしまいます。
そのため、Node.jsのモジュールの中でレンダラープロセスで利用したいものを選び、そのモジュールだけを使用できるようにするという方法をとります。

4.preload.jsの作成

preload.jsの機能

nodeIntegrationの値をfalseのまま、レンダラープロセスでNode.jsのモジュールを利用できるようにする方法の一つにpreload.jsの作成があります。

preload.jsは、nodeIntegrationの値に関わらず、require('モジュール名')でNode APIにアクセスすることができます。
そのため、「preload.jsで読み込んだモジュールをグローバルに共有→それをレンダラープロセスで使用」という形で、レンダラープロセスでのNode APIの利用を可能にできるのです。
(このモジュール共有を行うために、mainWindowの設定でcontextIsolationをfalseにする必要があります)

ファイル作成

srcフォルダの中に、preload.jsを作成し、例えば以下のように書き込みます。

$ mkdir src
$ cd src
$ touch preload.js
preload.js
constelectron=require('electron');process.once('loaded',()=>{global.ipcRenderer=electron.ipcRenderer;global.app=electron.remote.app;});

このようにすることで、レンダラープロセスで以下のようにすることでipcRendererappモジュールが使えるようになります。

(レンダラープロセス)index.js
//nodeIntegrationをfalseにしたことで、以下は使えなくなった//const {ipcRenderer, app} = require('electron');constipcRenderer=window.ipcRenderer;constapp=window.app;

参考:Electron で nodeIntegration: false にする方法
参考:Electron IPC通信を行う方法まとめ
参考:Electron Webviewのセキュリティで注意すべきこと

5.レンダラープロセスの作成

先ほどmain.jsの中で指定したレンダラープロセスindex.htmlを、srcフォルダの中に作成します。

$ touch index.html

作成したindex.htmlの中に、アプリの画面をhtmlで書いていきます。
ここでは動作確認をするために、hello,world!だけ記述します。

src/index.html
<h1>hello,world!</h1>

今後画面を装飾・機能を追加ということをしたい場合は、index.cssindex.jsを読み込んでいけば実現可能です。

6.アプリの起動

起動コマンドを打つ

今の状態でアプリがどうなっているのかを、実際に起動して確かめてみましょう。
ルートディレクトリ直下のnode_modules/.binの中にelectronコマンドが入っているので、それを実行します。

$ cd app
$ node_modules/.bin/electron .

すると、以下のような画面が立ち上がるはずです。
Electron.png
先ほどhtmlに書いたhello,worldが表示されていますので成功です。

起動ショートカットを設定する

アプリを起動するたびに、先ほどのような長いパスを打つのは面倒です。
そのため、簡単なショートカットで同様に起動できるようにpackage.jsonに設定を追加しましょう。

package.json
{...(略)..."scripts":{"test":"echo \"Error: no test specified\"&& exit 1","start":"electron ."},...(略)...}

すると、以下のコマンドでアプリの起動が行えるようになります。

$ npm start

package.jsonのscriptsの詳しい仕様については、以下の記事を参考にしてください。
参考:npm公式ドキュメント npm-scripts
参考:package.json の scripts

アプリ開発に使える便利機能

hello,worldができたら、自分の思うがままにアプリの機能をどんどん豊富にしていく段階です。
ここでは、Electronアプリ開発で使える便利な機能・ライブラリを紹介します。

デベロッパーツールの表示

アプリ画面のデバッグ等に使えるデベロッパーツールは、メインプロセス内に以下のコードを追記することで表示させることができます。

main.js
app.on('ready',function(){ ...()...mainWindow.webContents.openDevTools() ...()...});

追記することで、アプリ起動時にChromeのデベロッパーツールが表示されます。
devtool.png
注意:アプリをビルドして完成させ、配布するというときには該当箇所をコメントアウト・デベロッパーツールを非表示にさせることを忘れないでください。

よく使えるプロセスモジュール

プロセスモジュールを見れば、Electronでどんなことができるのかが大体わかります。
ここでは、公式ドキュメントに掲載してあるモジュールをざっくり紹介します。

モジュール名説明
autoUpdaterアプリの自動アップデート機能の追加(Mac,Winのみ)
BrowserViewBrowserWindowに追加でウェブコンテンツを埋め込む子ウィンドウの制御
contentTracingChromiumからのトレースデータを収集
dialogローカルファイルの選択・新規作成・保存
globalShortcutショートカットキーの登録・操作の管理
inAppPurchaseMac App Store のアプリ内購入機能の提供
netHTTP/HTTPS リクエストの発行
netLogネットワークイベントのロギング
NotificationOSのデスクトップ通知の作成
powerMonitorPCの電源の状態を取得
powerSaveBlockerシステムの省電力モードの制御
protocolカスタムプロトコルの登録
screen画面サイズ、ディスプレイ、カーソルの位置等の情報取得
sessionブラウザーセッション、クッキー、キャッシュ、プロキシの設定管理
systemPreferencesシステム環境設定の取得
TouchBarタッチバーレイアウトの作成
Trayシステムの通知領域にアイコンやコンテキストメニューを追加
webContentsウェブページの描画・制御
desktopCapturerデクストップのスクリーンショットやビデオキャプチャの制御
webFrameウェブページの描画のカスタマイズ
clipboardシステムのクリップボードを利用したコピー・ペーストの操作提供
crashReporterクラッシュレポートをリモートサーバーに送信
nativeImagetrayやDockやアプリケーションのアイコン画像ファイル作成
shellデフォルトのアプリケーションを使用してのファイル・URL管理

アプリケーションメニューをつける

ウィンドウ上部に設定されるアプリケーションメニューを作るためには、MenuMenuItemモジュールを使用します。

設置のためには、以下のコードをメインプロセス側に記述します。

main.js
//アプリケーションメニューconstMenu=electron.Menu//メニューバー内容lettemplate=[{label:'Your-App',submenu:[{label:'アプリを終了',accelerator:'Cmd+Q',click:function(){app.quit();}}]},{label:'Window',submenu:[{label:'最小化',accelerator:'Cmd+M',click:function(){mainWindow.minimize();}},{label:'最大化',accelerator:'Cmd+Ctrl+F',click:function(){mainWindow.maximize();}},{type:'separator'},{label:'リロード',accelerator:'Cmd+R',click:function(){BrowserWindow.getFocusedWindow().reload();}}]}]// Electronの初期化完了後に実行app.on('ready',function(){//メニューバー設置constmenu=Menu.buildFromTemplate(template);Menu.setApplicationMenu(menu);...()...});

すると、以下のようなアプリケーションメニューが表示されます。
(写真ではElectronとなっているところは、アプリをパッケージしたらYour-Appになります)
menu.png
参考:JavaScript (Electron) を使ってアプリの見栄えを整える

組み込み式DBの導入(NeDB)

セットアップ

NeDBは、Javascriptで使える組込データベースです。APIはMongoDBのサブセットなので、Mongoを使ったことがある人にとっては扱いやすいかと思います。
インストールにはnpmを使います。

$ npm install--save nedb

実際に使うためには、preload.jsとレンダラープロセス側に以下を記述します。

preload.js
constelectron=require('electron');process.once('loaded',()=>{global.app=electron.remote.app;global.Datastore=require('nedb');});
(レンダラープロセス)index.js
constapp=window.app;constDatastore=window.Datastore;constdb=newDatastore({filename:app.getPath('userData')+'/member.db',autoload:true});

注意:永続的なDBにするため指定するdbファイルのパスをNode APIのapp.getPath('userData')でアプリケーションデータディレクトリを指定しないと、パッケージした後に動かなくなります。
参考:electron-vueのproductionビルドで気をつけるところ

基本操作

ここまで準備ができると、insertやfindなどの普通のDB操作が可能になります。
例えば、insertは以下のように行います。

vardoc={//examplefirst_name:Smith,last_name:Sam};db.insert(doc,function(err,newDoc){//処理});

他の操作については参考文献に譲ります。
参考:NeDB を使ってみた
参考:NeDBの基本

アプリのビルド

1.electron-builderのインストール

ルートディレクトリ直下で以下のコマンドを実行して、electron-builderをインストールします。

$ npm install-D electron-builder

注意:上記のようにdevDependenciesとして追加しないと、ビルド時にエラーが出ます。

2.アイコン画像の作成

アプリのアイコンを作成します。外部ツールを使って好きなようにデザインしてください。
ファイルの形式については以下の通りです。

  • Mac用: icnsファイル
  • Windows用: .icoファイル

参考:Electronの各Platform向けアプリアイコンを作成する
今回は、mac用のアイコンをassets/macに、Windows用のアプリをassets/winに置きました。

3.ビルド設定を記述

package.jsonにビルド用の設定を追加します。
この時、buildキーは一番上の階層(=nameやversionと同じ階層)に設置してください。

package.json
{...(略)... "build":{"appId":"com.electron.yourapp","directories":{"output":"dist"},"files":["assets","src","package.json","package-lock.json"],"mac":{"icon":"assets/mac/icon_mac.icns","target":["dmg"]},"win":{"icon":"assets/win/icon_win.ico","target":"nsis"},"nsis":{"oneClick":false,"allowToChangeInstallationDirectory":true}},...(略)...}

項目の意味は以下の通りです。

  • appId : アプリのBundle ID。
  • directories
    • output : ビルドしたアプリの格納先
  • files : ビルドに含めるファイル
  • mac : Mac用にビルドするときの設定
    • icon : アイコンファイルの相対パス
    • target : パッケージ後のファイル形式
  • win : Windows用にビルドするときの設定
    • icon : アイコンファイルの相対パス
    • target : パッケージ後のファイル形式
  • nsis : インストーラ生成ツールNSISの設定
    • oneClick : インストールから実行まで一気に行うかどうか
    • allowToChangeInstallationDirectory : インストール先の変更を許可するかどうか

参考:electron-builder公式ドキュメント Common Configuration
参考:electron-builderでwindows用インストーラーを作る時の設定

4.ビルドコマンドを実行

ルートディレクトリ直下のnode_modules/.binの中にelectron-builderコマンドが入っているので、それを実行します。

$ node_modules/.bin/electron-builder --mac--x64# Mac用のインストーラー(.dmg)が作成される$ node_modules/.bin/electron-builder --win--x64# Windows用のインストーラー(.exe)が作成される

コマンド実行後に、distディレクトリ内にアプリのインストーラーが作成されていればビルド成功です。

  • Mac用: your-app-name-1.0.0.dmg
  • Windows用: your-app-name Setup 1.0.0.exe

このインストーラーを起動すれば、アプリがPCにインストールされて動き出します。お疲れ様でした。

参考:electronでリリース用パッケージを作る
参考:electron-builderを使ってdmgファイルを生成する

amplify cliはnode.js version 10以上が必要

$
0
0

現象

amplify cliをインストール(npm install -g @aws-amplify/cli
)して、いざamplifyを使おうとしたら

amplify configure
/home/****/.nvm/versions/node/v8.16.2/lib/node_modules/@aws-amplify/cli/lib/plugin-manager.js:47
    catch {
          ^

SyntaxError: Unexpected token {
    at Generator.next (<anonymous>)
    at Object.Module._extensions..js (module.js:664:10)

とエラーメッセージが出て利用できなかった。
amplify initなど他のすべてのコマンドで失敗した。

解決策

単純な話で、AWS amplify cliのgithubに書いてあるようにnode.jsのバージョンが10以上でないとamplify cliは使えない。エラーメッセージを見ると利用しているバージョンはv8.16.2だとわかる。10以上にアップデートしたら使えた。

教訓

対応バージョンやインストール時のエラーメッセージはしっかり読まないといけない。

mongo + express + nodejs + ejs + node-dev で 新規PJを立ち上げる

$
0
0

menスタック構築手順

mongo + express + nodejs + ejs を構築したので備忘録として残したい。

  • 一般的にはMEANスタックだが、Angularを除いて構築することにした。
  • jadeではなくejsとした理由は、汎用的に使えるhtml*1 でモックをつくるため
  • angularはあとで必用になったら考えることにした。

前提条件

1.nodejs インストール
2.express インストール
3.mongodb インストール

構築

1.モック作成

画面イメージhtml*1 を作成する。

2.フォルダ作成

2-1.ディレクトリ移動
2-2.フォルダ作成

3.プロジェクト作成

3-1.npm install
3-2.ディレクトリ構成確認
root/
 ├ bin/
 │ └ www
 ├ node_modules/
 ├ public/
 │ └ images/
 │ └ javascripts/
 │ └ stylesheets/
 ├ routes/
 │ └ index.js/
 │ └ users.js/
 ├ views/
 │ └ index.jade/
 ├ app.js/
 ├ package-lock.json/
 └ package.json/

4.アプリ起動

4-1.npm start
4-2.http://localhost:3000/にアクセスし、デフォルト表示の確認
4-3.ctrl + c でアプリ終了

5.ejs インストール

npminstallejs

6.モック追加

6-1.viewsフォルダ内に index.ejsとして追加
6-2.public/stylesheets内にcssファイル追加
6-3.public/javascripts内にjsファイル追加
6-4.public/images内に画像ファイル追加

7.ejsロジック追加

7-1.index.js修正

router.get('/',function(req,res,next){res.render("./index.ejs",{hoge:"hoge"});});

7-2.app.js修正

app.set("view engine","ejs");

8.ejsファイルにデータを渡す設定をする

8-1.バインドしたい場所に<%= hoge %>を記述
8-2.index.jsのrouter.getのres.renderの第二引数にobject型で設定する
 {hoge:"hoge"}

9.サーバー再起動ロジックの設定

  • ソースを修正するたびに再起動は手間なので、自動化する。
npminstall-gnode-dev
  • 再起動ロジックは、開発版でしか使わないので、グローバルにしておき、package.jsonには記述しない

10.アプリ起動

10-1.

node-dev./bin/www

10-2.http://localhost:3000/にアクセスすると、index.ejsに値がバインドされて、styleやjsや画像が読み込まれたものが表示される。
 

番外.コマンドプロンプトでmongodb crud操作

mongoshowdbsuse"dbName"showcollectionsdb."collectionName".find()
MySQLMongoDB
CREATE TABLE products (...);db.createCollection("products")
SHOW TABLES;show collections
DROP TABLE products;db.products.drop()
TRUNCATE TABLE products;db.products.remove()
SELECT * FROM products;db.products.find()
SELECT COUNT(*) FROM products;db.products.count()
SELECT COUNT(*) FROM products WHERE name LIKE "%camera%" OR category='C';db.products.find({ $or : [{ name : /camera/ },{ category : 'C' }] }).count()
SELECT COUNT(stock) FROM products;db.products.find({ stock : { $exists : true } }).count()

Square APIを使って在庫数リストを作成

$
0
0

Square APIを使って、在庫数の取得ができるようになったので、在庫数の一覧を作ってみます。

Square APIを使ってみる(在庫数の取得) - Qiita

商品リストをシンプルにする

CatalogApiのlistCatalogで取得できるカタログオブジェクトから、在庫表を作るのに必要そうなデータだけを抜き出して、解析しやすくします。

// カタログオブジェクトをシンプルにするfunctionsimplifyCatalogObjects(catalogObjects){letresultObjects=[];catalogObjects.forEach(obj=>{constresultObj={};if(!obj.is_deleted){resultObj.id=obj.id;resultObj.item_data={"name":obj.item_data.name,};//バリエーションif(obj.item_data.variations){letvariations=[];obj.item_data.variations.forEach(variation=>{if(!variation.is_deleted){variations.push({"id":variation.id,"item_variation_data":{"name":variation.item_variation_data.name,"sku":variation.item_variation_data.sku,},});}});resultObj.item_data.variations=variations;}resultObjects.push(resultObj);}});returnresultObjects;}// Square APIへアクセスconstSquareConnect=require('square-connect');constdefaultClient=SquareConnect.ApiClient.instance;constoauth2=defaultClient.authentications['oauth2'];oauth2.accessToken='YOUR ACCESS TOKEN';// カタログリスト取得constapiInstance=newSquareConnect.CatalogApi();constopts={'types':'ITEM'// String | An optional case-insensitive, comma-separated list of object types to retrieve, for example `ITEM,ITEM_VARIATION,CATEGORY,IMAGE`.  The legal values are taken from the CatalogObjectType enum: `ITEM`, `ITEM_VARIATION`, `CATEGORY`, `DISCOUNT`, `TAX`, `MODIFIER`, `MODIFIER_LIST`, or `IMAGE`.};apiInstance.listCatalog(opts).then(function(data){console.log('API called successfully. Returned data: ');constlistObj=simplifyCatalogObjects(data.objects);console.log(JSON.stringify(listObj,null,2));},function(error){console.error(error);});

以下のようなオブジェクトになります。

{"id":"5PH23FJXUXRA762XBNMIWXOP","item_data":{"name":"龍童子","variations":[{"id":"YFUAKFPQFKZGLDVDAC7BAEZM","item_variation_data":{"name":"亀","sku":""}},{"id":"GKG5AJSJGPGWBKKCPDXVTYYL","item_variation_data":{"name":"龍","sku":""}}]}},{"id":"GHUUZGRZR4RLKU5CORONQGCE","item_data":{"name":"獅子毛毬","variations":[{"id":"XLYCP3R5HKYGBZGXXYMR4FRG","item_variation_data":{"name":"カラフル","sku":""}}]}},{"id":"SGT6ELC5BRESKTIDJ4OV2AF5","item_data":{"name":"たまねぎ屋","variations":[{"id":"6I4VBILOEURJCZHDA5KXF3YS","item_variation_data":{"name":"定価","sku":""}}]}},

シンプルにしてみて気づいたのですが、Squareの商品管理画面では、バリエーションがないように見える商品も必ず「定価」とか「販売価格」といったバリエーションができるようです。

在庫数を問い合わせる

どの商品にも必ずバリエーションが1つはあることがわかったので、全てのバリエーションのidで在庫数を問い合わせれば良さそうです。

id一つ一つ問い合わせればいいかと思ったのですが、一度に取得出来そうです。

複数の商品の在庫数を問い合わせるには、InventoryApiのbatchRetrieveInventoryCountsを使います。

問い合わせるidを配列に入れて指定することもできるようですが、今回は全ての在庫数を取得してみます。

varSquareConnect=require('square-connect');vardefaultClient=SquareConnect.ApiClient.instance;// Configure OAuth2 access token for authorization: oauth2varoauth2=defaultClient.authentications['oauth2'];oauth2.accessToken='YOUR ACCESS TOKEN';varapiInstance=newSquareConnect.InventoryApi();varbody=newSquareConnect.BatchRetrieveInventoryCountsRequest();// BatchRetrieveInventoryCountsRequest | An object containing the fields to POST for the request.  See the corresponding object definition for field details.apiInstance.batchRetrieveInventoryCounts(body).then(function(data){console.log('API called successfully. Returned data: ');console.log(JSON.stringify(data,null,2));},function(error){console.error(error);});

在庫データが以下のような感じで取得できます。

{"counts":[{"catalog_object_id":"CQZIEPDLFE64WVKMCQDLW7D3","catalog_object_type":"ITEM_VARIATION","state":"IN_STOCK","location_id":"454ACGR0V7GPA","quantity":"1","calculated_at":"2020-01-25T03:15:12.1253Z"},{"catalog_object_id":"Q4QA5OQ6EY5ZQR6LC7K22RT6","catalog_object_type":"ITEM_VARIATION","state":"IN_STOCK","location_id":"454ACGR0V7GPA","quantity":"0","calculated_at":"2020-02-03T04:27:25.234Z"},{"catalog_object_id":"52EALZY6VCSEZEHAJPGFJB5R","catalog_object_type":"ITEM_VARIATION","state":"IN_STOCK","location_id":"454ACGR0V7GPA","quantity":"0","calculated_at":"2019-11-03T09:50:46.1139Z"},{"catalog_object_id":"LEGPUO52IVTP7RLT2A5NRD6T","catalog_object_type":"ITEM_VARIATION","state":"IN_STOCK","location_id":"454ACGR0V7GPA","quantity":"2","calculated_at":"2019-11-03T09:50:16.1139Z"},

※後でわかったのですが、1度に取得できる件数が100アイテム分のようで、返ってきたオブジェクトにcursorプロパティがあった場合は、このカーソルプロパティの値をパラメーターに使って、追加でbatchRetrieveInventoryCountsを呼び出す必要があります。

在庫データを連想配列にする

使いやすいように、idをキーにした連想配列に変換します。

// 全在庫数データを連想配列にするfunctionarrayInventoryCounts(inventoryCounts){letresultArray=[];inventoryCounts.forEach(obj=>{resultArray[obj.catalog_object_id]=obj.quantity;});returnresultArray;}

以下のような感じの連想配列になります。

[CQZIEPDLFE64WVKMCQDLW7D3:'1',Q4QA5OQ6EY5ZQR6LC7K22RT6:'0','52EALZY6VCSEZEHAJPGFJB5R':'0',LEGPUO52IVTP7RLT2A5NRD6T:'2',JE3HZYQEKDXDEPQJYYDGMWDV:'0',X7EGVDUYAVVWKWHHZPMDS53E:'0',THTWJQPEEAXU4S75DKUBSF6V:'0',Y7KFYSA33DQ22JY3QJVPBTRG:'0',K46BVOXXYN7IESVTNTKOQNLY:'0',ZZTLDYJAIXNCLWS3MV3NQTRX:'0','3TUHUDMXKKS3RDLLGAJGTMPM':'0',F3Z6XGJPU7XKXCVLKQ5BBTDB:'0',

※こちらも後で分かったのですが、stateプロパティがIN_STOCK以外のものも入るようなので、stateプロパティがIN_STOCKのものだけを使うようにする必要があります。

在庫数の一覧を作る

batchRetrieveInventoryCountsのページネーションや、在庫データの種類の問題を解決して結局以下のようなプログラムで在庫一覧のデータを作成できました。

// 在庫数リスト取得サンプルconstSquareConnect=require('square-connect');// 全在庫数データを連想配列にするfunctionarrayInventoryCounts(inventoryCounts){letresultArray=[];inventoryCounts.forEach(obj=>{if(obj.state=="IN_STOCK"){if(resultArray[obj.catalog_object_id]){// console.log(obj.catalog_object_id);}resultArray[obj.catalog_object_id]=obj.quantity;}});returnresultArray;}// 全在庫数リストを作成functionlistInventoryCount(catalogObjects,inventoryCounts){letresultObjects=[];constarrayCounts=arrayInventoryCounts(inventoryCounts);catalogObjects.forEach(obj=>{constresultObj={};if(!obj.is_deleted){resultObj.id=obj.id;resultObj.item_data={"name":obj.item_data.name,};//バリエーションif(obj.item_data.variations){letvariations=[];obj.item_data.variations.forEach(variation=>{if(!variation.is_deleted){variations.push({"id":variation.id,"item_variation_data":{"name":variation.item_variation_data.name,"sku":variation.item_variation_data.sku,"stock":arrayCounts[variation.id]},});}});resultObj.item_data.variations=variations;}resultObjects.push(resultObj);}});returnresultObjects;}//商品カタログ取得asyncfunctionlistCatalog(){constapiInstance=newSquareConnect.CatalogApi();constopts={'types':'ITEM'};constdata=awaitapiInstance.listCatalog(opts);returndata;}// 全在庫数データ取得asyncfunctionbatchRetrieveInventoryCounts(){constapiInstance=newSquareConnect.InventoryApi();letbody={"cursor":""};letdata={"counts":[]};do{constdt=awaitapiInstance.batchRetrieveInventoryCounts(body);data.counts=data.counts.concat(dt.counts);body.cursor=dt.cursor||null;}while(body.cursor);returndata;}// Square APIへアクセスconstdefaultClient=SquareConnect.ApiClient.instance;constoauth2=defaultClient.authentications['oauth2'];oauth2.accessToken='YOUR ACCESS TOKEN';(async()=>{try{//カタログデータと在庫データを並列で問い合わせconstlistCatalogPromise=listCatalog();console.log("listCatalog()");constinventoryCountsPromise=batchRetrieveInventoryCounts();console.log("batchRetrieveInventoryCounts()");//問い合わせ処理の終了待ちcatalogObjs=awaitlistCatalogPromise;inventoryCounts=awaitinventoryCountsPromise;console.log('問い合わせ終了');//在庫リスト作成constdata=listInventoryCount(catalogObjs.objects,inventoryCounts.counts);console.log(JSON.stringify(data,null,2));}catch(error){console.error(error);}})();

取得できるデータは以下のよう感じです。

{"id":"KEGDS5AB55HZXNKDVU2KMR54","item_data":{"name":"T ベージュ","variations":[{"id":"L3AMBR4VKPEKACZM42W5XSSR","item_variation_data":{"name":"XS","sku":"","stock":"1"}},{"id":"H6SZNXOHR5AODKVYXSF5LQUJ","item_variation_data":{"name":"S","sku":"","stock":"0"}},{"id":"IEU5P2IX25NYGMTTNP6MJHZE","item_variation_data":{"name":"M","sku":"","stock":"0"}},{"id":"OV2UHORNF7NMC6KJHUXQTOPR","item_variation_data":{"name":"L","sku":"","stock":"2"}},{"id":"L22LMUV37MNPRZMA2SX3G3AI","item_variation_data":{"name":"Kids","sku":""}}]}},{"id":"YKWBCEXWXMQHJ3OJNSUTZ532","item_data":{"name":"T オレンジ","variations":[{"id":"ZXAY4RQ56TCXJQXWGJDKSBRP","item_variation_data":{"name":"XS","sku":"","stock":"2"}},{"id":"MY7KQ7RUBDQG3LCWARXPZZCN","item_variation_data":{"name":"S","sku":"","stock":"0"}},{"id":"RO4SSMLWPQ7IQ75G6HVFMXNH","item_variation_data":{"name":"M","sku":"","stock":"0"}},{"id":"QLSRDXB52BKDFXLZ3XRAEQO4","item_variation_data":{"name":"KIDS","sku":"","stock":"-1"}}]}},

stockが在庫数になります。

stockプロパティがないものがありますが、これは在庫データを全く入力していない状況だとデータがないので、このようになります。

在庫数=0とは別の状況です。

stockがマイナスのものは、在庫補充時に在庫データを変更しなかったので、売れた分マイナスになっていっている状況です。


不規則にエラーを返すWebAPIを使って、マイクロサービス間のリトライを実装しよう。

$
0
0

retry.gif

マイクロサービスアーキテクチャでは、サービス間の通信に失敗することがあります。ネットワークを介したリモートコールである以上、なんらかの異常が発生することは考慮に入れた上で設計をする必要があります。

本記事では REST API を使用した場合の API のリトライ方法について、いくつかのライブラリを使用して解説します。

リトライする条件

サービス間の通信に失敗しても、全てリトライするというわけにはいけません。何度リトライをしても必ず失敗するエラーに対しては無駄にリトライをしないようにしましょう。
RESTful な API では、4xx 系のエラーはリトライ不要です。4xx 系エラーは主にバリデーションエラーや認証エラーなど、クライアント側に問題があるリクエストであるため何度リトライしてもエラーが返却されます。

以下に代表的な HTTP ステータスコードを挙げます。REST API の考え方やステータスコードについては、REST API Tutorialが参考になります。

ステータスコード説明
400 (Bad Request)不正な形式のリクエスト、バリデーションエラーなど
401 (Unauthorized)認証エラー、認証されずにリソースにアクセスした
403 (Forbidden)認可エラー、指定したリソースに対する権限がない
404 (Not Found)対象のリソース、パスが見つからない
500 (Internal Server Error)システムエラー
503 (Service Unavailable)サービスが一時的に利用できない
504 (Gateway Timeout)タイムアウト、処理時間がかかりすぎている

一方、5xx 系のエラーを返却したサービスはリトライすることで復旧できる場合があります。レスポンスの HTTP ステータスコードを条件にして、リトライする可否を判断しましょう。

レスポンスコード以外にも、ネットワークの一時的な障害によりサービスに到達できなかった場合のエラーを考慮しましょう。ネットワーク障害の場合はリクエスト先のサービスからレスポンスコードが返却されないため、接続に失敗した旨の例外をキャッチしてリトライを実行することになります。

5xx Error を返す API

このようなマイクロサービスのエラーをハンドリングするコードを書くために、頻繁に障害が発生する API を作りました。 以下からアクセスしてください。

https://instability.now.sh/

リクエストを送るとランダムに 5xx 系エラーを返します。

$ curl https://instability.now.sh
{"status":504,"message":"Gateway Timeout"}$ curl https://instability.now.sh
{"status":200,"message":"OK"}$ curl https://instability.now.sh
{"status":200,"message":"OK"}$ curl https://instability.now.sh
{"status":503,"message":"Service Unavailable"}$ curl https://instability.now.sh
{"status":504,"message":"Gateway Timeout"}

errorRateをクエリパラメータに指定することで障害発生率を調整できます。

$ curl https://instability.now.sh?errorRate=99   # 99% の確率でエラー{"status":500,"message":"Internal Server Error"}$ curl https://instability.now.sh?errorRate=2    #  2% の確率でエラー{"status":200,"message":"OK"}

POST リクエストを送信することも可能です。POST の場合はリクエストボディに errorRateを設定します。

$ curl -X POST -d'{ "errorRate": "20" }' https://instability.now.sh
{"status":500,"message":"Internal Server Error"}

詳しい API ドキュメントはこちらを参照してください。

リトライする方法

リトライでは以下の2つを考慮する必要があります。

  • リトライの間隔
  • リトライを何回繰り返すのか

リトライの間隔については、Exponential Backoff が良いでしょう。リトライするたびに指数関数的にその間隔を長くしていく方法です。再試行する度に、1 秒後、2 秒後、4 秒後と指数関数的に待ち時間を加えていきます。等間隔のリトライの場合、障害がおきているサービスに無駄なリクエストを発生させることになり、余計な負荷をかけてしまいます。Exponential Backoff のテクニックを使用すれば、リトライを繰り返すたびにその間隔が広がっていくのでこの問題を緩和できます。
この方法はクラウドやマイクロサービスの文脈では基本的なお作法です。AWS Solutions Architect ブログでも紹介されています。Exponential Backoff の方法にばらつき(Jitter)を加えた方法を紹介しています。

AWS Solutions Architect ブログ: Exponential Backoff And Jitter

リトライを何回繰り返すのかは難しい課題です。障害が発生したマイクロサービスの復旧時間に依存するところがあり、まずは 5 回などに設定しておき、運用を進めるにしたがって調整していくのが良いでしょう。

さて、今回はこの2つの考慮事項を node-fetch, request, got の各種ライブラリを使用して実装してみましょう。

node-fetch での実装例

node-fetchの fetch メソッドは Promise を返すため比較的シンプルに実装ができます。
ネットワークエラーの場合は待ち時間なしで即座にリトライをかけ、5xx 系エラーの場合は Exponential Backoff を行います。

importfetchfrom"node-fetch";exportdefaultasync()=>{consturl="https://instability.now.sh";constinit={method:"GET"};constoption={retry:{limit:5}};constresult=awaitretryFetch(url,init,option);returnresult;};constsleep=msec=>newPromise(resolve=>setTimeout(resolve,msec));constretryFetch=async(url,init,option)=>{const{retry}=option;for(leti=0;i<=retry.limit;i++){letres;try{res=awaitfetch(url,init);}catch(error){// ネットワークエラーの場合は即座にリトライconsole.log(error);continue;}if(res.status<500){// 5xx 系エラー以外の場合はレスポンスデータを返すreturnres;}// 5xx 系エラーの場合は数秒待ってからリトライ(Exponential Backoff)constsleepTime=2**i;awaitsleep(sleepTime*1000);}};

request での実装例

requestの request メソッドは Promise を返さないので取り扱いやすいように、薄くラップしましょう。
あとの手続きは node-fetchと同様です。

import*asrequestfrom"request";exportdefault()=>{constparam={url:"https://instability.now.sh",json:true};constoption={retry:{limit:5}};returnretryRequest(param,option);};constsleep=msec=>newPromise(resolve=>setTimeout(resolve,msec));constretryRequest=async(param,option)=>{const{retry}=option;for(leti=0;i<=retry.limit;i++){letres:{status:number;message:string};try{res=awaitrequestPromise(param);}catch(error){// ネットワークエラーの場合は即座にリトライconsole.log(error);continue;}if(res.status<500){// 5xx 系エラー以外の場合はレスポンスデータを返すreturnres;}// 5xx 系エラーの場合は数秒待ってからリトライ(Exponential Backoff)constsleepTime=2**i;awaitsleep(sleepTime*1000);}};// Promise を返すように薄いラッパーを作るfunctionrequestPromise(param):any{returnnewPromise((resolve,reject)=>{request.get(param,(err,req,body)=>{if(err){reject(err);}else{resolve(body);}});});}

got での実装例

gotは非常に軽量でリトライの仕組みも標準的に取り揃えているシンプルな HTTP クライアントライブラリです。Promise と StreamAPI にも対応しており、現代の API クライアントライブラリとしてはかなり優秀です。getClientメソッドでクライアントオブジェクトを生成し、あとは client.get(path)の形でリクエストを送ります。

importgotfrom"got";exportdefaultasync()=>{try{constprefixUrl="https://instability.now.sh/";constclient=getClient(prefixUrl);returnawaitclient.get("").json();}catch(error){console.log(error.response.body);}};constgetClient=(url:string)=>{constclient=got.extend({prefixUrl:urlretry:{limit:5,calculateDelay:delay=>{console.log(delay);// リトライ処理が発生した場合だけログを出力return1;}}});returnclient;};

リトライの間隔は 1 秒、2 秒、4 秒、8 秒と増えていくようで、Exponential Backoff の方法を取っているようです。got なかなか使い心地いいんじゃないでしょうか。

さいごに

マイクロサービス間のエラーハンドリングはAPIのリトライだけを考慮すれば良いわけではありません。
リトライをする上で、各サービスは冪等な処理が行われるようにしておかなければなりませんし、必要に応じてキャッシュやサービスブローカーを導入して耐障害性をあげるテクニックもあります。
今回はその中でも初歩の初歩であるAPIのリトライについて、実装を交えながら説明しました。これで少しでも初学者の助けになりますように。

Reactのテストはスナップショットじゃなくてスクリーンショットで

$
0
0

title

Reactのテスト書いてますか?
スナップショットテストが一般的ですが、GitHubのPullRequestではどうも差分が分かりづらい。もしスクリーンショットを自動的に撮って差分を画像ファイルとして比較できたらなぁと思っていたところ良さそうなパッケージを見つけました。react-screenshot-testというパッケージなのですが、以下のように差分を画像ファイルとして確認できる素晴らしいツールです。

image.png

早速試してみます。まずはインストールから

$ yarn add -D react-screenshot-test

次に設定ファイルを作成します。

jest.screenshot.config.js
module.exports={testEnvironment:"node",globalSetup:"react-screenshot-test/global-setup",globalTeardown:"react-screenshot-test/global-teardown",testMatch:["**/?(*.)+(screenshot).[jt]s?(x)"],transform:{"^.+\\.[t|j]sx?$":"babel-jest",// or ts-jest"^.+\\.css$":"react-screenshot-test/css-transform","^.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":"react-screenshot-test/asset-transform"}};

babelを使うようなので以下ファイルも用意しておきます。ちなみに今回作成したサンプルアプリケーションは create-react-appを使用しています。

babel.config.js
module.exports={presets:["babel-preset-react-app"]};

スナップショットを作成するコードはこんな感じになるようです。デスクトップとiPhoneXのそれぞれの場合でスクリーンショットを撮ります。

App.screenshot.jsx
importReactfrom"react";import{ReactScreenshotTest}from"react-screenshot-test";import"./index.css";importAppfrom"./App";ReactScreenshotTest.create("App").viewport("Desktop",{width:1024,height:768}).viewport("iPhone X",{width:375,height:812,deviceScaleFactor:3,isMobile:true,hasTouch:true,isLandscape:false}).shoot("app",<App/>).run();

あとは以下のコマンドを実行して作成するようです。npmスクリプトに登録しておきましょう。

$ jest -c jest.screenshot.config.js -u

実行すると以下のエラーが発生しました。どうやら必要なdockerイメージがあるようです。

$ yarn test:screenshot
yarn run v1.21.1
$ jest -c jest.screenshot.config.js -u
Error: It looks like you're missing the Docker image required to render screenshots.

Please run the following command:

$ docker pull fwouts/chrome-screenshot:1.0.0


    at ensureDockerImagePresent (/Users/daisuke/work/react/screenshot.lesson/node_modules/react-screenshot-test/dist/lib/screenshot-server/DockerizedScreenshotServer.js:50:15)
    at processTicksAndRejections (internal/process/task_queues.js:97:5)
    at async DockerizedScreenshotServer.start (/Users/daisuke/work/react/screenshot.lesson/node_modules/react-screenshot-test/dist/lib/screenshot-server/DockerizedScreenshotServer.js:28:9)
    at async setUpScreenshotServer (/Users/daisuke/work/react/screenshot.lesson/node_modules/react-screenshot-test/dist/lib/global-setup.js:26:9)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

dockerイメージをpullしてから再チャレンジします。

$ yarn test:screenshot
yarn run v1.21.1
$ jest -c jest.screenshot.config.js -u
 PASS  src/App.screenshot.jsx (7.325s)
  App
    Desktop
      ✓ app (643ms)
    iPhone X
      ✓ app (680ms)› 2 snapshots written.
Snapshot Summary
 › 2 snapshots written from 1 test suite.

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   2 written, 2 total
Time:        7.384s
Ran all test suites.
✨  Done in 9.85s.

できました。きちんとデスクトップとiPhoneXの2種類のスクリーンショットが取得できていますね。

image.png

なかなか深掘りできそうな良いツールを見つけてしまった。まずは取り急ぎ書き留める。

Node.js を完全にアンインストールする

$
0
0

過去に Mac nodebrew で Node.js をインストールする手順の記事を書きました。

現在の私の環境ではnodebrewは削除して、Node.jsnodenvで管理し、nodenvはanyenvで管理してます。

$ brew uninstall nodebrew
$ curl -o uninstall-node.sh https://gist.githubusercontent.com/nicerobot/2697848/raw/uninstall-node.sh
$ chmod u+x uninstall-node.sh 
$ ./uninstall-node.sh 
  空Enter でアンインストール処理を進める
$ rm uninstall-node.sh

この辺のディレクトリもあったら消す

$ sudo rm -rf /usr/local/include/node
$ sudo rm -rf /usr/local/lib/dtrace
$ rm -rf ~/.node-gyp
$ rm -rf ~/.npm
$ rm -rf ~/.sourcemint 
$ which node
/usr/local/lib/node

$ rm -rf /usr/local/lib/node
# 診断コマンドを実行
$ brew doctor

# 必要に応じて...
$ brew prune
$ brew cleanup

anyenv のインストール方法

別記事にまとめています。
Mac を買ったら必ずやっておきたい初期設定

package.jsonに"engines"を設定すると「このバージョンのNode.jsでしか動かない」を表明できる

$
0
0

特定のバージョンのNode.jsでしか動かしてほしくないパッケージがある場合、package.jsonのenginesフィールドに、Node.jsのバージョンを明記しておくと、yarn installnpm installしたときに警告を表示できるようになる。

  • 実行環境のバージョンを固定したいときに便利。

例: Node.js 12だけに限定したい場合

例えば、Node.js 12で実行してほしい場合、次のようにenginesフィールドをpackage.jsonに追加する:

package.json
{"name":"my-module","version":"1.0.0","main":"index.js","license":"MIT","engines":{"node":"12.x"}}

この設定で、Node.js 13環境下でyarn installすると、エラーを起こすことができる:

Hyper.png

NPMの場合は、--engine-strictオプションが必要

npm installで同様の警告を起こすためには、--engine-strictオプションをもたせる必要がある。

Hyper.png

毎回指定するのは面倒なので、.npmrcに設定しておくといい:

~/.npmrc
engine-strict=true

Ubuntuで使う言語のインストール方法とか環境構築とか

$
0
0

最近はバックエンド言語毎にVMで環境用意して勉強したりしてて、その環境構築方法の管理を最近はGistでしてるのですが、何となくQittaに。※※但し、Gistは英語で書いてるので。

environment

  • host OS: Windows
  • VM: Virtual Box with Vagrant
    • Ubuntu 18.0

CUIまたはGUIの仮想環境をUbuntuを使って構築するのはこっち。

Ruby on Rails

Install latest version

terminal
# install in one timesudo apt install autoconf bison build-essential libssl-dev libreadline-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm-dev

# install rbenv# rbenv is tool to manage a few of ruby versions and enable to change ruby ver. project by project.
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
echo'export PATH="$HOME/.rbenv/bin:$PATH"'>> ~/.bashrc
echo'eval "$(rbenv init -)"'>> ~/.bashrc
source ~/.bashrc
git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build

# Install ruby
rbenv install--list
rbenv install 2.〇.〇
rbenv global 2.〇.〇

# Instal yarn# Rails6 needs webpacker, and Webpacker needs yarn to install
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo"deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt update
sudo apt install yarn

# Install Rails
gem install rails --no-document# install webpacker# inner App
rails webpacker:install

Install RubyonRails by "apt install"

terminal
sudo apt install-y ruby ruby-dev build-essential
sudo apt install yarn

sudo gem install rails
  • "-y" means "All Yes"
  • build-essential contain information about package to build Debian pack.
    • If do not build Debian, build~ is not needed
    • Reference

Nodejs

rails6 uses webpacker, which needs nodejs

terminal
# first, install nodejs and npmsudo apt install-y nodejs npm

# install n-packagesudo npm install n -g# by n-package, install nodesudo n stable

# uninstal old nodejs and npm, and re-loginsudo apt purge -y nodejs npm
exec$SHELL-l# confirm
node -v

Rust

when discord changed golang to Rust, I just tried this and coded a little.

terminal
sudo apt install build-essential

# install rust
curl https://sh.rustup.rs -sSf | sh

# add the passsource$HOME/.cargo/env

Java

terminal
sudo apt update
sudo apt install git
sudo apt install openjdk-11-jdk

# confirmation
java --version

PHP

Python

Viewing all 8812 articles
Browse latest View live