20190307のNode.jsに関する記事は9件です。

Reactのチュートリアル

最終目標

フロントエンドとバックエンドのDB排他を調べてみたい。

今回の目標

Tutorial: Intro to Reactを実施する
https://reactjs.org/tutorial/tutorial.html
https://mae.chab.in/archives/2943

環境

Ubuntu 18.04.2 LTS
node v8.10.0
npm 3.5.2
create-react-app v2.1.5

詳細は、https://qiita.com/y_ohr/items/8afd0852b24356059f2b 参照。

Tutorial: Intro to React

ローカルでやります。

react-app作成

$ create-react-app tutorial-react-app
$ cd tutorial-react-app/

src配下削除

$ cd src/
$ rm -f *
$ cd ..

src/index.css作成

src/index.css
body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

ol, ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: "";
  display: table;
}

.status {
  margin-bottom: 10px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square:focus {
  outline: none;
}

.kbd-navigation .square:focus {
  background: #ddd;
}

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

src/index.js作成

src/index.js
class Square extends React.Component {
    render() {
        return (
            <button className="square">
                {/* TODO */}
            </button>
        );
    }
}

class Board extends React.Component {
    renderSquare(i) {
        return <Square />;
    }

    render() {
        const status = 'Next player: X';

        return (
            <div>
                <div className="status">{status}</div>
                <div className="board-row">
                    {this.renderSquare(0)}
                    {this.renderSquare(1)}
                    {this.renderSquare(2)}
                </div>
                <div className="board-row">
                    {this.renderSquare(3)}
                    {this.renderSquare(4)}
                    {this.renderSquare(5)}
                </div>
                <div className="board-row">
                    {this.renderSquare(6)}
                    {this.renderSquare(7)}
                    {this.renderSquare(8)}
                </div>
            </div>
        );
    }
}

class Game extends React.Component {
    render() {
        return (
            <div className="game">
                <div className="game-board">
                    <Board />
                </div>
                <div className="game-info">
                    <div>{/* status */}</div>
                    <ol>{/* TODO */}</ol>
                </div>
            </div>
        );
    }
}

// ========================================

ReactDOM.render(
    <Game />,
    document.getElementById('root')
);

index.jsの先頭に3行追加。ここからはdiffにて。

src/index.js
diff --git a/src/index.js b/src/index.js
index 9a06c07..70f7c67 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,3 +1,7 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import './index.css';
+
 class Square extends React.Component {
     render() {
         return (

Passing Data Through Props

src/index.js
diff --git a/src/index.js b/src/index.js
index 70f7c67..43522c3 100644
--- a/src/index.js
+++ b/src/index.js
@@ -6,7 +6,7 @@ class Square extends React.Component {
     render() {
         return (
             <button className="square">
-                {/* TODO */}
+                {this.props.value}
             </button>
         );
     }
@@ -14,7 +14,7 @@ class Square extends React.Component {

 class Board extends React.Component {
     renderSquare(i) {
-        return <Square />;
+        return <Square value={i} />;
     }

     render() {

Making an Interactive Component

src/index.js
diff --git a/src/index.js b/src/index.js
index 43522c3..b908669 100644
--- a/src/index.js
+++ b/src/index.js
@@ -3,10 +3,19 @@ import ReactDOM from 'react-dom';
 import './index.css';

 class Square extends React.Component {
+    constructor(props) {
+        super(props);
+        this.state = {
+            value: null,
+        };
+    }
+
     render() {
         return (
-            <button className="square">
-                {this.props.value}
+            <button
+                className="square"
+                onClick={() => this.setState({ value: 'X' })}>
+                {this.state.value}
             </button>
         );
     }

Lifting State Up

src/index.js
diff --git a/src/index.js b/src/index.js
index b908669..01c54da 100644
--- a/src/index.js
+++ b/src/index.js
@@ -3,27 +3,38 @@ import ReactDOM from 'react-dom';
 import './index.css';

 class Square extends React.Component {
-    constructor(props) {
-        super(props);
-        this.state = {
-            value: null,
-        };
-    }
-
     render() {
         return (
             <button
                 className="square"
-                onClick={() => this.setState({ value: 'X' })}>
-                {this.state.value}
+                onClick={() => this.props.onClick()}>
+                {this.props.value}
             </button>
         );
     }
 }

 class Board extends React.Component {
+    constructor() {
+        super();
+        this.state = {
+            squares: Array(9).fill(null),
+        };
+    }
+
+    handleClick(i) {
+        const squares = this.state.squares.slice();
+        squares[i] = 'X';
+        this.setState({ squares: squares });
+    }
+
     renderSquare(i) {
-        return <Square value={i} />;
+        return (
+            <Square
+                value={this.state.squares[i]}
+                onClick={() => this.handleClick(i)}
+            />
+        );
     }

     render() {

Function Components

src/index.js
diff --git a/src/index.js b/src/index.js
index 01c54da..5f16f3d 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,16 +2,14 @@ import React from 'react';
 import ReactDOM from 'react-dom';
 import './index.css';

-class Square extends React.Component {
-    render() {
-        return (
-            <button
-                className="square"
-                onClick={() => this.props.onClick()}>
-                {this.props.value}
-            </button>
-        );
-    }
+function Square(props) {
+    return (
+        <button
+            className="square"
+            onClick={props.onClick}>
+            {props.value}
+        </button>
+    );
 }

 class Board extends React.Component {

Taking Turns

src/index.js
diff --git a/src/index.js b/src/index.js
index 5f16f3d..fa7e58a 100644
--- a/src/index.js
+++ b/src/index.js
@@ -17,13 +17,17 @@ class Board extends React.Component {
         super();
         this.state = {
             squares: Array(9).fill(null),
+            xIsNext: true,
         };
     }

     handleClick(i) {
         const squares = this.state.squares.slice();
-        squares[i] = 'X';
-        this.setState({ squares: squares });
+        squares[i] = this.state.xIsNext ? 'X' : 'O';
+        this.setState({
+            squares: squares,
+            xIsNext: !this.state.xIsNext,
+        });
     }

     renderSquare(i) {
@@ -36,7 +40,7 @@ class Board extends React.Component {
     }

     render() {
-        const status = 'Next player: X';
+        const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

         return (
             <div>

Declaring a Winner

src/index.js
diff --git a/src/index.js b/src/index.js
index fa7e58a..34c275d 100644
--- a/src/index.js
+++ b/src/index.js
@@ -23,6 +23,10 @@ class Board extends React.Component {

     handleClick(i) {
         const squares = this.state.squares.slice();
+        if(calculateWinner(squares) || squares[i]){
+            return;
+        }
+
         squares[i] = this.state.xIsNext ? 'X' : 'O';
         this.setState({
             squares: squares,
@@ -40,7 +44,13 @@ class Board extends React.Component {
     }

     render() {
-        const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
+        const winner = calculateWinner(this.state.squares);
+        let status;
+        if (winner) {
+            status = 'Winner: ' + winner;
+        } else {
+            status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
+        }

         return (
             <div>
@@ -87,3 +97,23 @@ ReactDOM.render(
     <Game />,
     document.getElementById('root')
 );
+
+function calculateWinner(squares) {
+    const lines = [
+        [0, 1, 2],
+        [3, 4, 5],
+        [6, 7, 8],
+        [0, 3, 6],
+        [1, 4, 7],
+        [2, 5, 8],
+        [0, 4, 8],
+        [2, 4, 6],
+    ];
+    for (let i = 0; i < lines.length; i++) {
+        const [a, b, c] = lines[i];
+        if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
+            return squares[a];
+        }
+    }
+    return null;
+}

Lifting State Up, Again

src/index.js
diff --git a/src/index.js b/src/index.js
index 34c275d..65a0010 100644
--- a/src/index.js
+++ b/src/index.js
@@ -13,48 +13,18 @@ function Square(props) {
 }

 class Board extends React.Component {
-    constructor() {
-        super();
-        this.state = {
-            squares: Array(9).fill(null),
-            xIsNext: true,
-        };
-    }
-
-    handleClick(i) {
-        const squares = this.state.squares.slice();
-        if(calculateWinner(squares) || squares[i]){
-            return;
-        }
-
-        squares[i] = this.state.xIsNext ? 'X' : 'O';
-        this.setState({
-            squares: squares,
-            xIsNext: !this.state.xIsNext,
-        });
-    }
-
     renderSquare(i) {
         return (
             <Square
-                value={this.state.squares[i]}
-                onClick={() => this.handleClick(i)}
+                value={this.props.squares[i]}
+                onClick={() => this.props.onClick(i)}
             />
         );
     }

     render() {
-        const winner = calculateWinner(this.state.squares);
-        let status;
-        if (winner) {
-            status = 'Winner: ' + winner;
-        } else {
-            status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
-        }
-
         return (
             <div>
-                <div className="status">{status}</div>
                 <div className="board-row">
                     {this.renderSquare(0)}
                     {this.renderSquare(1)}
@@ -76,14 +46,56 @@ class Board extends React.Component {
 }

 class Game extends React.Component {
+    constructor(props) {
+        super(props);
+        this.state = {
+            history: [{
+                squares: Array(9).fill(null),
+            }],
+            xIsNext: true,
+        };
+    }
+
+    handleClick(i) {
+        const history = this.state.history;
+        const current = history[history.length - 1];
+        const squares = current.squares.slice();
+        if (calculateWinner(squares) || squares[i]) {
+            return;
+        }
+
+        squares[i] = this.state.xIsNext ? 'X' : 'O';
+        this.setState({
+            history: history.concat([{
+                squares: squares,
+            }]),
+            xIsNext: !this.state.xIsNext,
+        });
+    }
+
     render() {
+        const history = this.state.history;
+        const current = history[history.length - 1];
+        const winner = calculateWinner(current.squares);
+
+        let status;
+        if (winner) {
+            status = 'Winner: ' + winner;
+        } else {
+            status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
+        }
+
         return (
             <div className="game">
                 <div className="game-board">
-                    <Board />
+                    <Board
+                        squares={current.squares}
+                        onClick={(i) => this.handleClick(i)}
+                    />
+
                 </div>
                 <div className="game-info">
-                    <div>{/* status */}</div>
+                    <div>{status}</div>
                     <ol>{/* TODO */}</ol>
                 </div>
             </div>

Showing the Past Moves

src/index.js
diff --git a/src/index.js b/src/index.js
index 65a0010..5afcc7d 100644
--- a/src/index.js
+++ b/src/index.js
@@ -78,6 +78,15 @@ class Game extends React.Component {
         const current = history[history.length - 1];
         const winner = calculateWinner(current.squares);

+        const moves = history.map((step, move) => {
+            const desc = move ? 'Go to move #' + move : 'Go to game start';
+            return (
+                <li>
+                    <button onClick={() => this.jumpTo(move)}>{desc}</button>
+                </li>
+            );
+        });
+
         let status;
         if (winner) {
             status = 'Winner: ' + winner;
@@ -96,7 +105,7 @@ class Game extends React.Component {
                 </div>
                 <div className="game-info">
                     <div>{status}</div>
-                    <ol>{/* TODO */}</ol>
+                    <ol>{moves}</ol>
                 </div>
             </div>
         );

Implementing Time Travel

src/index.js
diff --git a/src/index.js b/src/index.js
index 5afcc7d..0412658 100644
--- a/src/index.js
+++ b/src/index.js
@@ -52,12 +52,13 @@ class Game extends React.Component {
             history: [{
                 squares: Array(9).fill(null),
             }],
+            stepNumber: 0,
             xIsNext: true,
         };
     }

     handleClick(i) {
-        const history = this.state.history;
+        const history = this.state.history.slice(0, this.state.stepNumber + 1);
         const current = history[history.length - 1];
         const squares = current.squares.slice();
         if (calculateWinner(squares) || squares[i]) {
@@ -69,19 +70,27 @@ class Game extends React.Component {
             history: history.concat([{
                 squares: squares,
             }]),
+            stepNumber: history.length,
             xIsNext: !this.state.xIsNext,
         });
     }

+    jumpTo(step) {
+        this.setState({
+            stepNumber: step,
+            xIsNext: (step % 2) === 0,
+        });
+    }
+
     render() {
         const history = this.state.history;
-        const current = history[history.length - 1];
+        const current = history[this.state.stepNumber];
         const winner = calculateWinner(current.squares);

         const moves = history.map((step, move) => {
             const desc = move ? 'Go to move #' + move : 'Go to game start';
             return (
-                <li>
+                <li key={move}>
                     <button onClick={() => this.jumpTo(move)}>{desc}</button>
                 </li>
             );

感想

  • 写経
  • 思ったより時間がかかった=充実したチュートリアルだった。
  • inputで動的に仮想DOMが書き換わるようなプログラムを書いてみたい

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.jsで開発をする場合にまずやるべきこと

概要

僕がNuxt.jsで開発をする場合にまずやることを紹介したいと思います。

インストール

僕はcreate-nuxt-appでインストールします。

npx create-nuxt-app <my-project>

入れる拡張は
1. Use a custom server framework → express
2. Choose features to install → Linter / Formatter, Prettier, Axios
3. Use a custom UI framework → bootstrap
4. Use a custom test framework → jest
5. Choose rendering mode → Universal

が個人的に好きです。

次にとりあえず入れとけなパッケージもインストールします。
とりあえずsassを入れます。

npm i -D node-sass sass-loder

次にsass変数をグローバルに使うためのパッケージを入れます。

npm i @nuxtjs/style-resources

Font Awesome入れます。公式のgitにnuxt用の導入方法が載っています

npm i @fortawesome/fontawesome-svg-core @fortawesome/free-regular-svg-icon @fortawesome/free-solid-svg-icons @fortawesome/vue-fontawesome

適当に書き換える

package.jsonにnpm scriptを一つ追加します。lintfixは絶対追加したほうがいいです、エラー内容を人力で追っていくのは時間の無駄です。
herokuにデプロイする場合はそれも追記します。

package.json
{
    // lintfixを追加
    "lintfix": "eslint --fix --ext .js,.vue --ignore-path .gitignore .",
  // herokuデプロイ用
    "heroku-postbuild": "npm run build"
  },
}

.eslintrc.jsにルールを追加します。よく分かってないのですがconsole.logしたら時たまエラーになるので、それの回避です。

.eslintrc.js
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
  }

sass変数のあれやこれやの設定はこれこれをみたら良いですよ(宣伝)。

終わり

毎回苦労して初期設定してた気がするけど、そんな大した作業量でもなっかたです。
半年くらい前はeslintrcをゴニョゴニョ編集してた気がしますが進化してるっぽいですね。
あと最新のnuxt.js ver2.40以降はランダムにホットリロードが失敗するというちょっとムカつくバグがあるので気をつけてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[ES6] import でディレクトリー内すべてのファイルを読み込む

課題

export default しているファイルを読み込むときに以下の書き方をしてたけどhogeディレクトリ全体を一括で読み込みたいと思った。

import hoge1 from './hoge/hoge1.js'
import hoge2 from './hoge/hoge2.js'

hoge/index.js で import 書いてまとめるのもめんどくさい。

方法

これでいいのかわからないけど、以下の方法でやってみたら動いた。

import fs from "fs"
const hoges = {}
fs.readdirSync("./hoge").forEach(async file => {
  const module = await import(`./hoge/${file}`)
  hoges[file] = module.default
})
hoges.hoge1.hogeFunction()

ファイル形式を選択したいときはフィルタリングすればいけるし、サブディレクトリ読み込みたいときは再帰的に読めばいいと思う。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Express + passport(-twitter) + (PM2)で最速でTwitter認証機能付きWebAppを作る

いくつかTwitter認証を使うWebApplicationを作る過程でTwitterでの認証をテンプレート化させコピペで作れるようにしたのでそれをご紹介します。

この記事で出来るようになること

  • Twitter認証機能を持ったExpressAppをサクッと作れるようになる
  • WebAppとしても,ネイティブ使うAPIエンドポイントとしても使える
  • (後日出来るようになるかも) Swift4でのiOSネイティブからの当APIの使い方がサクッとわかる

前提

コピペで作るためには以下を満たしておく必要があります。

  • TwitterAppを作成できる

    • 以前にTwitterDeveloperの大規模な変更があって、TwitterAPPを作るためには作文を書かなきゃいけなかった気がします。
    • TwitterDeveloperにアクセスしてAppのリストとCreate an app ボタンが表示されればOK.
  • express-generatorが導入されている
    Expressプロジェクトをコマンド一つで生成してくれる便利な奴
    導入とか(公式)

  • PM2が導入されている
    ProcessManager(多分)
    nodeプロジェクトを手軽にデーモン化させられる奴(起動/ホットリロード/クラッシュ時の再起動/startup等々)
    導入とか(公式)

  • ドメインが用意されていてWebAppを公開できるようになっている
    長くなるので省きますが、https://xxx.your.domain/ とかにアクセスして生成したてのExpressを起動した際に

    Express
    Welcome to Express
    

    が表示されればOK.

今回はhttps://qiita.your.domain/というURLでAPIを公開してみたいと思います。

TwitterAppを作成

  1. twitterDeveloperからCreate an appを押し、App作成画面へ移ります。

  2. 必要事項を記述し、Allow this application to be used to sign in with Twitter項目のEnable Sign in with Twitterにチェックを入れます。

  3. Callback URLs項目がアクティブになるので、ここにhttps://qiita.your.domain/auth/twitter/callbackを入力します。
    ※ /auth/twitter/callbackの部分は本来任意ですが、これを変更する場合後述のルーティングのコードも変更する必要があります。

  4. 項目記述後、流れに沿ってAppを作成し最終的に自分のAppsDashboardに作成したAppが表示されていれば準備完了。

  5. AppsDashboardからAppを開き、keys and tokensのConsumer API Keysを確認できるようにしておきます。

実装

Express

まずはExpressを初期化します。

$ express -e qiita-twitter
$ cd qiita-twitter
$ npm i

次にpm2を初期化します

$ pm2 init
$ vi ecosystem.config.js

pm2

pm2の設定を変更します。

ecosystem.config.js
module.exports = {
    apps: [
        {
            name: 'qiita-twitter',
            script: 'npm',
            args: 'start',
            watch: true,
            env: {
                PORT: 3000,
                HOST_NAME: 'qiita.your.domain',
                USE_SSL: true,
                AUTH: {
                    twitter: {
                        active: true,
                        CONSUMER_KEY: 'Your CONSUMER_KEY',
                        CONSUMER_SECRET: 'Your CONSUMER_SECRET',
                    },
                }
            }
        }
    ]
};

nameはpm2上で管理するApp名です。
pm2 logs qiita-twitterとかするとログを見れたりします。

scriptとargsは起動時のコマンドです。
$ npm startで起動するので、このように記述します。

watchはホットリロード(ファイルを変更すると自動再起動してくれるやつ)を使うか否かです。
trueにすると起動ディレクトリ以下全てのファイルを、trueの代わりに['routes', 'models']みたいにするとroutesとmodelsディレクトリ以下を監視してくれるようです。

で、重要なのがenvです。

USE_SSLとかは後ほどコールバックURLを組み立てるときに使っていますが、そこでURLを直打ちしてしまえば必要ありません。

AUTH=>twitter=>activeみたいになってるのは、他にもlocal認証(よくあるメアドとPWの認証)やFaceBook認証とか使う時に分かるやすくなるメリットがあります。

この辺はコードと相談しながらお好みで変更しましょう。

gitで管理する場合ecosystem.config.jsはgitignoreに突っ込むなりしてgit監視下から外しましょう

個人的にはecosystem.config.js.sampleというenvの値を空白にしたものを用意しておき、これをgitに上げておくのがベターかなと思います。

休憩

一先ずここで起動確認をしておきましょう。

$ pm2 start ecosystem.config.js
$ pm2 logs qiita-twitter

適当なブラウザでhttps://qiita.your.domainにアクセスして

Express
Welcome to Experss

が表示されていればOKです。

Twitter認証

いよいよTwitter認証のroutingを実装します。

以下個人的に使っているディレクトリ構成となります。
お試す前にコードを読んで各自改変することをお勧めします。
(contorllers内でrouting設定しちゃってたりと健康被害を及ぼす可能性があります)

packageインストール

$ npm i -S express-session passport passport-twitter

ルーティング

$ mkdir controllers
$ touch controllers/authController.js
$ touch controllers/userController.js
controllers/authController.js
const passport = require('passport');
const TwitterStrategy = require('passport-twitter').Strategy;
const uc = require('./userController');

module.exports = { 
    initialize: function(app) {
        this.app = app;
        this.authSettings = JSON.parse(process.env.AUTH);

        app.use(passport.initialize());
        app.use(passport.session());
        passport.serializeUser(uc.userSerialize);
        passport.deserializeUser(uc.userDeserialize);

        this.twitterActivate();
    },
    twitterActivate: function() {
        if(!this.authSettings.twitter.active) {
            console.log('Twitter Auth is not activated');
            return;
        }
        let consumerKey = this.authSettings.twitter.CONSUMER_KEY;
        let consumerSecret = this.authSettings.twitter.CONSUMER_SECRET;
        let callbackURL = process.env.USE_SSL ? 'https' : 'http';
        callbackURL += '://';
        callbackURL += process.env.HOST_NAME;
        callbackURL += callbackURL.endsWith('/') ? '' : '/';
        callbackURL += 'auth/twitter/callback';
        passport.use(new TwitterStrategy({
            consumerKey: consumerKey,
            consumerSecret: consumerSecret,
            callbackURL: callbackURL
        }, uc.twitterAuth));

        this.app.get('/auth/twitter', passport.authenticate('twitter'));
        this.app.get('/auth/twitter/callback', passport.authenticate('twitter'), async function(req, res) {
            const user = req.session.passport.user;
            res.json(user);
        });
        console.log('Twitter Auth is activated');
    },
}
controllers/userController.js
const ac = require('./authController');

module.exports = {
    twitterAuth: async function(token, tokenSecret, profile, done) {
        //本来ここでユーザー検索して、存在したらそのObjectを返す
        //存在しなければ新規ユーザー作成
        //みたいなことをする。
        //tokenとtokenSecretそのままobjectに格納してログイン状態として扱うのは良くないです。
        const userObj = {
            twitterId: profile.id,
            screenName: profile.username,
            token: token,
            tokenSecret: tokenSecret
        };

        //ログ表示もいくない。
        console.log(userObj);
        done(null, userObj);
    },
    userSerialize: async function(user, done) {
        done(null, user);
    },
    userDeserialize: async function(id, done) {
        console.log('user deserializing');
        console.log(id);
        done(null, id);
    },
};

app.jsから読み込み/初期化

$ vi app.js

最後にapp.jsを編集し、読み込み・初期化を行います。

app.js
//最魚のrequire群でsessionとauthControllerを読み込み(6行目くらい)
const session = require('express-session');
const authController = require('./controllers/authController');

//requireしたモジュール系をuseしている部分でsessionの読み込みをしてからauthControllerをinitialize(順番大事)を追加(23行目くらい)
app.use(session({
    secret: 'kokoikeapi',
    resave: false,
    saveUninitialized: true
}));
authController.initialize(app);

動作確認

最後にコールバックしてきたら、ユーザーのTweet一覧を表示してみましょう.

さらに体に良くないですが、authController内でTwitterモジュールを読み込んでお試し表示してみましょう。

$ npm i -S twitter
controllers/authController.js
//4行目くらいでrequireしておきます。
const Twitter = require('twitter');

// 36行目くらいの/auth/twitter/callbackのルーティングの処理の中身を変更
 const user = req.session.passport.user;
 const consumer = JSON.parse(process.env.AUTH).twitter;
 const client = new Twitter({
     consumer_key: consumer.CONSUMER_KEY,
     consumer_secret: consumer.CONSUMER_SECRET,
     access_token_key: user.token,
     access_token_secret: user.tokenSecret
 });

const params = { screen_name: user.screenName};
client.get('statuses/user_timeline', params, function(err, tweets, response) {
    if(!err) {
        res.json(tweets);
    } else {
        res.json(err);
    }
});

//res.json(user);

これで認証が終わってcallbackしてくると自分のツイート一覧のjsonがresponseに流れます。

本来の使い方としては、userController時点でuserデータを作成し、doneにそのuserデータを渡しておきます。
すると、authControllerで取得するuserにはidが含まれているので、これを使ってユーザーのプロフィールページやトップページにリダイレクトするといいと思います。

controllers/authController.js
this.app.get('/auth/twitter/callback', passport.authenticate('twitter'), async function(req, res) {
    //トップへリダイレクトする場合
    //res.redirect('/');

   //user個別ページへリダイレクトする
   const user = req.session.passport.user;
   res.redirect('/users/' + user.id);

   //みたいな。
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScriptでSyntaxError: Unexpected token {

ちょいハマりした。

コイツ。

(node:93711) UnhandledPromiseRejectionWarning: /Users/username/project/src/entity/SomeEntity.ts:1
(function (exports, require, module, __filename, __dirname) { import {
                                                                     ^

SyntaxError: Unexpected token {
    at new Script (vm.js:79:7)
    at createScript (vm.js:251:10)
    at Object.runInThisContext (vm.js:303:10)
    at Module._compile (internal/modules/cjs/loader.js:657:28)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
    at Module.load (internal/modules/cjs/loader.js:599:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
    at Function.Module._load (internal/modules/cjs/loader.js:530:3)
    at Module.require (internal/modules/cjs/loader.js:637:17)
    at require (internal/modules/cjs/helpers.js:22:18)
(node:93711) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:93711) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

こういうエラーがいちばん嫌い。

20180727214556.png

環境

typescript ^3.3.3333
typeorm ^0.2.14

経緯

typeormセットアップして、
それ用のEntityを作って、
tscして、
node dist/index.js

したら上記エラーが出た。

エラー内容的にはNodeに対応してない文法が存在しているって感じだけど、そもそもtsからjsにコンパイル済みだし、意味不明。

typescriptのコンフィグにもちゃんと下記は入れてる。

tsconfig.json
"module": "commonjs",
"target": "es6",

意味わかんねーよバーカ!

結論

typeormのドキュメントが分かりづらい。

typeormを利用する際、下記のようなconfigがプロジェクトルートに必要になる。
(厳密に言うと必須ではない。コード内に書くこともできるので)

ormconfig.json
{
  "type": "mysql",
  "host": "localhost",
  "port": 3306,
  "username": "root",
  "password": "hogerahogera",
  "database": "mogeramogera",
  "synchronize": true,
  "logging": false,
  "entities": [
     "src/entity/**/*.ts"
  ],
  "migrations": [
     "src/migration/**/*.ts"
  ],
  "subscribers": [
     "src/subscriber/**/*.ts"
  ]
}

で、entitiesプロパティがプロジェクト内のEntity(他で言うmodel。要はテーブルを表す)の場所を指す。

のだが、僕のようにコンパイルしてからコンパイル後のファイルをnodeで動かしてる人の場合、上記の設定のままだとコンパイル後のjsファイルから、コンパイル前のtsファイルであるentityを読みに行ってしまう。

https://stackoverflow.com/questions/50361948/syntaxerror-unexpected-token-import-typeorm-entity

そんなん知るかよって感じ。

まあ徹頭徹尾ドキュメントを読んでない僕が悪いっちゃ悪いのだが、もうちょっと書きようはあると思う。

一応それっぽいことは、ドキュメント内のここに書いてあった。

修正後はこんな感じになります。

ormconfig.json
{
  "type": "mysql",
  "host": "localhost",
  "port": 3306,
  "username": "root",
  "password": "calamity1jane",
  "database": "i-sharing",
  "synchronize": true,
  "logging": false,
  "entities": [
     "dist/entity/**/*.js" <- ここが変わってる
  ],
  "migrations": [
     "src/migration/**/*.ts"
  ],
  "subscribers": [
     "src/subscriber/**/*.ts"
  ]
}

うまく動いた!

typescriptになれてる人ならハマらないのかな?

こういうconfig系で何度も躓くので、実践的なtsプロジェクトの作り方とか落ち着いたらまとめます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScriptとTypeORMの組み合わせでSyntaxError: Unexpected token {

ちょいハマりした。

コイツ。

(node:93711) UnhandledPromiseRejectionWarning: /Users/username/project/src/entity/SomeEntity.ts:1
(function (exports, require, module, __filename, __dirname) { import {
                                                                     ^

SyntaxError: Unexpected token {
    at new Script (vm.js:79:7)
    at createScript (vm.js:251:10)
    at Object.runInThisContext (vm.js:303:10)
    at Module._compile (internal/modules/cjs/loader.js:657:28)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
    at Module.load (internal/modules/cjs/loader.js:599:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
    at Function.Module._load (internal/modules/cjs/loader.js:530:3)
    at Module.require (internal/modules/cjs/loader.js:637:17)
    at require (internal/modules/cjs/helpers.js:22:18)
(node:93711) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:93711) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

こういうエラーがいちばん嫌い。

20180727214556.png

環境

typescript ^3.3.3333
typeorm ^0.2.14

経緯

typeormセットアップして、
それ用のEntityを作って、
tscして、
node dist/index.js

したら上記エラーが出た。

エラー内容的にはNodeに対応してない文法が存在しているって感じだけど、そもそもtsからjsにコンパイル済みだし、意味不明。

typescriptのコンフィグにもちゃんと下記は入れてる。

tsconfig.json
"module": "commonjs",
"target": "es6",

意味わかんねーよバーカ!

結論

typeormのドキュメントが分かりづらい。

typeormを利用する際、下記のようなconfigがプロジェクトルートに必要になる。
(厳密に言うと必須ではない。コード内に書くこともできるので)

ormconfig.json
{
  "type": "mysql",
  "host": "localhost",
  "port": 3306,
  "username": "root",
  "password": "hogerahogera",
  "database": "mogeramogera",
  "synchronize": true,
  "logging": false,
  "entities": [
     "src/entity/**/*.ts"
  ],
  "migrations": [
     "src/migration/**/*.ts"
  ],
  "subscribers": [
     "src/subscriber/**/*.ts"
  ]
}

で、entitiesプロパティがプロジェクト内のEntity(他で言うmodel。要はテーブルを表す)の場所を指す。

のだが、僕のようにコンパイルしてからコンパイル後のファイルをnodeで動かしてる人の場合、上記の設定のままだとコンパイル後のjsファイルから、コンパイル前のtsファイルであるentityを読みに行ってしまう。

https://stackoverflow.com/questions/50361948/syntaxerror-unexpected-token-import-typeorm-entity

そんなん知るかよって感じ。

まあ徹頭徹尾ドキュメントを読んでない僕が悪いっちゃ悪いのだが、もうちょっと書きようはあると思う。

一応それっぽいことは、ドキュメント内のここに書いてあった。

修正後はこんな感じになります。

ormconfig.json
{
  "type": "mysql",
  "host": "localhost",
  "port": 3306,
  "username": "root",
  "password": "calamity1jane",
  "database": "i-sharing",
  "synchronize": true,
  "logging": false,
  "entities": [
     "dist/entity/**/*.js" <- ここが変わってる
  ],
  "migrations": [
     "src/migration/**/*.ts"
  ],
  "subscribers": [
     "src/subscriber/**/*.ts"
  ]
}

うまく動いた!

typescriptになれてる人ならハマらないのかな?

こういうconfig系で何度も躓くので、実践的なtsプロジェクトの作り方とか落ち着いたらまとめます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS Lambda�(Node.js)でGoogle Cloud Natural Language APIを使用してエラー"Failed to load gRPC"が発生したときの対応方法

環境/ライブラリ

事象

AWS Lambda(Node.js)上の関数で、Google Cloud Natural Language APIを呼び出すとエラー"Failed to load gRPC"が発生する。

エラーログ例

Error: Failed to load gRPC binary module because it was not installed for the current system
Expected directory: node-v57-linux-x64-glibc
Found: [node-v57-darwin-x64-unknown]
This problem can often be fixed by running "npm rebuild" on the current system
Original error: Cannot find module '/opt/nodejs/node_modules/grpc/src/node/extension_binary/node-v57-linux-x64-glibc/grpc_node.node'
at Object.<anonymous> (/opt/nodejs/node_modules/grpc/src/grpc_extension.js:53:17)
at Module._compile (module.js:652:30)
at Object.Module._extensions..js (module.js:663:10)
at Module.load (module.js:565:32)
at tryModuleLoad (module.js:505:12)
at Function.Module._load (module.js:497:3)
at Module.require (module.js:596:17)
at require (internal/module.js:11:18)
at Object.<anonymous> (/opt/nodejs/node_modules/grpc/src/client_interceptors.js:144:12)
at Module._compile (module.js:652:30) code: 'MODULE_NOT_FOUND

対応方法

以下のコマンドでリビルドする。targetはNode.jsのバージョンに合わせる

npm rebuild grpc —target=8.1.0 --target_arch=x64 --target_platform=linux --target_libc=glibc

感想

こういうエラーが起きると、Pythonにすれば良かったと思う(逆もまた然り)

参照

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

node.jsでタイムゾーンの変換処理にdate-fns-timezoneを利用する

date-fns-timezone

node.jsで日付処理するにはdate-fnsを利用すると便利に実装できます。

参考: https://www.webprofessional.jp/date-fns-javascript-date-library/

ですがこのライブラリのみだとタイムゾーンの計算がうまくできず何か使えるものがないか調べてみたところ、date-fns-timezoneというライブラリが良さげだったので使ってみました。

今回やりたいこと

日付がUTCで格納されているデータソース(DynamoDB)に対して日本時間の日付範囲でクエリをかけたい

指定日に含まれるデータを取得するDynamoDBクエリを組み立てる
const targetDate = new Date(); // '2019-03-06T11:36:25.533Z' (ここはUTC)

// 日本時間の0時
const d1 = startOfDay(targetDate); // '2019-03-05T15:00:00.000Z'
// 日本時間の翌日0時
const d2 = endOfDay(targetDate); // '2019-03-06T15:00:00.000Z'

const param = {
  TableName : 'Comment',
  KeyConditionExpression: "#key= :key AND #date >= :d1 AND #date < :d2",
  ExpressionAttributeNames:{
    "#key": "key",
    "#date": "date"
  },
  ExpressionAttributeValues: {
    ":key": key,
    ":d1": d1,
    ":d2": d2,
  }
};

使ってみる

date-fns-timezoneのインストール

npmからインストールできます。

$ npm install date-fns-timezone

依存ライブラリとして date-fns もインストールされるのでこちらも利用します。

今回利用したのはライブラリバージョンは以下です。

  • date-fns-timezone : 0.1.4
  • date-fns : 1.30.1

日本時間での0時を計算する

// import
const { startOfDay, addDays } = require('date-fns');
const { convertToTimeZone } = require('date-fns-timezone');

// タイムゾーン定義
const timeZone = "Asia/Tokyo";

// 現在時刻(UTC)を取得
const targetDate = new Date();
// TimeZone付きDateに変換
const zonedTargetDate = convertToTimeZone(targetDate, { timeZone: timeZone });
// date-fnsでstartOfDayを計算
const startDate = startOfDay(zonedTargetDate);
const endDate = addDays(startDate, 1);

// 文字列化
const d1 = startDate.toISOString(); // => '2019-03-05T15:00:00.000Z'
const d2 = endDate.toISOString(); // => '2019-03-06T15:00:00.000Z'

convertToTimeZoneを利用するとTimeZone情報を持ったDateに拡張されるため、date-fnsのstartOfDay()などもそのまま使えるのが便利ですね。
指定のフォーマットでフォーマットするにはdate-fns-timezoneのformatToTimeZoneも使えます。

詳細ドキュメントはこちら。
https://www.npmjs.com/package/date-fns-timezone

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

サブディレクトリ URL で起動する Angular の Web アプリで API 通信を実装したらハマった話+解消手順

概要

Angular (+ express) + Node.js でバックエンドに API 通信をさせた時にエラーでハマった経緯と、その解消策の備忘録

先に結論から言うと

「Nginx の設定をミスったら、バックエンド通信に失敗するよ」と言う話。

経緯

Angular + express で MEAN スタックで作成した Web アプリ。
今までは普通に、開発 ➡️ サーバへデプロイ ➡️ 実機で動作確認、で問題なく動いていた。
ただし、ドメイン取得して、サーバのトップレベルで動作する Web サイトとして作っていた。

今回は、サブディレクトリでのみ動作する Web サイトとして開発。
つまり...

  • https://example.com ▶︎ 別の Web サイト(静的 HTML ページ)
  • https://example.com/my-app ▶︎ 今回の Angular サイト

としようとした。
実際に、Angular でアプリを開発して配置。

事象と解消手順

事象1: 画面表示でエラー発生

https://example.com/my-app にアクセスしたら、テキストだけ出るけど装飾系が描画されない。と言う中途半端な状態で画面表示。表示が壊れている状態。。。
もちろん、https://example.com は問題なく規定の Web ページが表示される。

▶︎ 解消: 「base href」を設定したら解消

index.html の <base href="/"><base href="/my-app/"> となると解消する。
しかし、ローカル環境では必要ない...。
なので、以下のとおり、package.json のビルドコマンドに --base-href オプションを指定することで、ビルド時に「base href」を変え分けられるようにした。

package.json
  :
"scripts": {
    // 起動コマンド
    "dev": "node ./dist/server/server.js",
    // ビルドコマンド (ng build に --base-href オプション追加)
    "build": "npm run build:client && npm run build:server",
    "build:client": "ng build --prod --base-href=/my-app/ && ng run my-app:server",
    "build:server": "tsc --project server --outDir dist --allowjs true",
    :
  },
  :

事象2: バックエンドへの通信時にエラー発生

「事象1」が解消し、画面表示はできるようになったが、
今度は、API でバックエンドにリスト取得する処理を実装したら以下のエラー事象が発生。

ローカルだと取得できてたのに、サーバだとリスト取得できない。。。(本来なら JSON のリストデータが返却される予定)
ブラウザコンソールを開いてみたら、以下のエラーログを吐いていた。

# 発生したエラー

error: SyntaxError: Unexpected token < in JSON at position 0 at JSON.parse...
headers: t {normalizedNames: Map(0), lazyUpdate: null, lazyInit: ƒ}
message: "Http failure during parsing for https://iexample.com/portal/api/my-app/api/heros"
name: "HttpErrorResponse"
ok: false
status: 200
statusText: "OK"
url: "https://example.com/my-app/api/heros"

バックエンド通信で、HTTP 結果コードは 200 が返っているが、返却された後に何かエラーが出ている。

error の部分を開いて詳しく見てみると

SyntaxError: Unexpected token < in JSON at position 0 at JSON.parse (<anonymous>) at XMLHttpRequest

さらに、すぐ下に

text: "<!DOCTYPE html><html lang="ja"><head>↵  <meta charset="utf-8">↵  <title>My App</title>↵ 

JSON データ、もしくは、 JSON のリストデータが返却されるべき箇所で、HTML を返しているために、JSON.parse() でエラーが発生したことが原因らしい。

長く悩んだが、よく考えたらそりゃそうだ。Web サービスで画面用のルーティングしか設定してなかった。

▶︎ 解消: Nginx のルーティングに API 専用のルートを追加したら解消

/etc/nginx/conf.d/default.conf
server {
    # http を https にリダイレクト
    listen 80;
    return 301 https://$host$request_uri;
}

server {
    listen       443 ssl;
    server_name  localhost;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Host $http_host;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # *** 追加: 以下の API 用ルーティングを追加 ↓↓↓ ******
    location /my-app/api/ {
        proxy_pass http://127.0.0.1:3000/api/;
        proxy_redirect default;
    }
    # *** 追加 ↑↑↑ *********************************

    location /my-app/ {
        proxy_pass http://127.0.0.1:3000/;
        proxy_redirect default;
    }

    location / {
        root   /var/www;
        index  index.php index.html index.htm;
    }

    # 404, 50x 等 エラー系のルートが以下にある
}

設定後、nginx -s reload で Nginx を再起動。
Angualar も再デプロイして起動し直し、ブラウザから再度アクセスしてみたら、
エラーは出なくなり、問題なく目的の JSON リストデータが返却されて、画面に表示された。

ハマりどころ

そもそも「Angular の設定方法やロジックに原因がある」として、ずーーーーっと、アプリ設定のネット記事を漁っていたが、そこが間違いだった。

原因の切り分けフロー

HTTP 結果は 200 で、エラーの吐き方が、JSON パースエラー: クラサバ通信はできている
→ リクエストとレスポンスの機構を改めて確認: SSR で実装しているので、リクエスト/レスポンスは HTML を返却するのと JSON データを返却する2種類がある
→ ローカル環境で、ブラウザに GET メソッドの API を叩いてみる: JSON データが返り、画面に表示されることを確認
→ 公開サーバで、同じくブラウザに GET メソッドの API を叩いてみる: トップページが再表示された = HTML が返却された
→ Web サーバのリクエスト URI に対するルーティング先を修正すべきことに気づく
→ Nginx の config を修正して動作確認
問題解消 !!!

こんな感じだった。

エラーの切り分けを丁寧に行い、対応していくことは大事だ。
以上、Web サービスの設定は、大した内容ではないが間違えると結構ハマる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む