初心者のプログラミング日記

プログラミング初心者の日記

プログラミングに関することを書いていきます。

Expressで作ったアプリをHerokuで公開する

今回はsequelizeを使っているのでHerokuのDB構築からSeedの挿入までやりたいと思います。
Herokuはpostgresを使っているのでそれに合わせています。

ライブラリ バージョン
express 4.17.1
sequelize 6.3.5
sequelize-cli 6.2.0

Sequelizeのconfig.jsonの変更

Herokuはconfig.jsonのproductionを使うのでここを変更します。

"production": {
    "username": "postgres",
    "password": "postgres",
    "database": "databse_production",
    "host": "localhost",
    "dialect": "postgres",
    "use_env_variable": "DATABASE_URL", //変更
    "dialectOptions": { //追加
      "ssl": {
        "require": true,
        "rejectUnauthorized": false
      }
  }

まず「use_env_variable」の所を「DATABASE_URL」に変更します。「DATABASE_URL」は後ほど紹介しますが、HerokuのDBの環境変数になります。
「dialectOptions」にはssl接続の設定を書いておきます。

Herokuで新規アプリを作成

まずはHerokuから新規アプリを作成しましょう。
https://dashboard.heroku.com/apps
f:id:nasubiFX:20210204173102p:plain
そしたら上記のURLにアクセスし、左上の方にある「New」→「Crete new app」を選びます。
f:id:nasubiFX:20210204173300p:plain
次にアプリ名と場所を選んで、アプリを作成します。
f:id:nasubiFX:20210204173709p:plain
作成したらアプリのページに飛ぶので「Deploy」をクリックし、デプロイ方法を選びます。今回はGitHubリポジトリからデプロイします。GitHubリポジトリからデプロイした場合、pushが反映されるので便利です。
Githubを選んだら下の「Connect to GitHub」をクリックすると認証画面がでるのでOKします。
f:id:nasubiFX:20210204174205p:plain
そしたら、リポジトリ名を入れてSerachします。出てきたリポジトリと「Connetct」します。
f:id:nasubiFX:20210204174629p:plain
「Automatic deploys」で自動でデプロイするブランチを選択すれば、pushした際に自動でデプロイしてくれます。
まだHerokuにデプロイしていないので、「Manual deploy」で初回が手動でデプロイしてあげましょう。

次にDBを作成します。
f:id:nasubiFX:20210204180312p:plain
左上を①→②の順でクリックします。
f:id:nasubiFX:20210204180601p:plain
次に一番左の「Heroku Postgres」を選んで「Create one」をクリックします。
f:id:nasubiFX:20210204180902p:plain
次に、右上の方にある「Install Heroku Postgres」をクリックします。
f:id:nasubiFX:20210204181040p:plain
次にプランとインストールするアプリを選びます。アプリは先程作成してアプリを選択します。
選択が終わったら「Submit Order form」をクリックします。

これでDBの設定が終わりました。設定が終わったらデフォルトでHerokuの環境変数に「DATABASE_URL」が追加されるはずなのでその確認をします。
f:id:nasubiFX:20210205164640p:plain
「Settings」→「Config Vars」の「Reveal Config Vars」をクリックすると環境変数一覧がでてくるのでそこに「DATABASE_URL」があれば大丈夫です。

マイグレーションとシードの挿入を行う

次にHerokuのDBにマイグレーションとシードの挿入を行います。
以下の作業はコマンドプロンプトからでもできますが、今回はHeroku上で行います。
f:id:nasubiFX:20210205165218p:plain
右上の「More」→「Run console」をクリックします。
f:id:nasubiFX:20210205165451p:plain
そしたら「bash」と打ち、runをクリックします。
f:id:nasubiFX:20210205165828p:plain
次にローカルと同じようにSequelizeのconfigがあるフォルダに移動し「npx sequelize db:migrate」と打ちます。私はDBというフォルダに設定ファイルがあるので移動しています。
コマンドを打ったらローカルと同じようなログが出たらマイグレーション成功です。

続けてソードの挿入もやってしまいましょう。「npx sequelize db:seed:all」これでシードの挿入もできました。
一応sequelize-cliGitHubも載せておきます。
https://github.com/sequelize/cli

私の場合はこのままではエラーになりましたので、エラーになった人だけ以下のコマンドを実行して見てください。

まず、コマンドプロンプトに移動してコマンドを打って行きます。

heroku login -i 
heroku reset -a アプリ名
heroku logs --tail -a アプリ名

Herokuにログインして、アプリの再起動をしています。一応最後のコマンドでエラーが出た場合に確認することができます。

アプリが起動しない場合

以下のファイルを追加すらば治るかもしれません。

//Procfileというファイル名
web: node ./bin/www

またはapp.jsonを作成する必要があるかもです。

//app.jsonの内容
{
  "name": "アプリ名",
  "description": "アプリ",
  "logo": "",
  "keywords": ["node"],
  "image": "heroku/nodejs"
}

Express+Google OAuth 2.0を使ってログインする

まず、必要なモジュールをインストールします。
これで下準備は完了です。

yarn add passport
yarn add passport-google-oauth
yarn add express-session

Google OAuth 2.0を有効にする

まず、以下URLにアクセスします。
https://console.developers.google.com/

プロジェクトの作成

そしたら以下の画像の順番にクリックしてプロジェクトを作成します。
f:id:nasubiFX:20210108233014p:plain
あとはプロジェクト名と場所を入力して作成ボタンを押せばプロジェクトが作成されます。場所の所は特になければデフォルトのなしのままで大丈夫です。

OAuth 同意画面を作成

まず上記の画像の①の▼をクリックして今作成したプロジェクトを選択して開きます。
そしたら上記の画像の「OAuth 同意画面」の所をクリックして開きます。
まず、User Typeを内部か外部にするかですが今回は外部にして誰でも使用できるようにしますが、おのおの変えてください。
選んだら作成をクリックして次の画面に進みます。そしたら以下の画像のような画面が出てくるので入力してください。
f:id:nasubiFX:20210108235709p:plain
入力が終わったら保存して次へをクリックして次に画面に進みます。
今回のログインにはidとdisplayNameを使うので「スコープを追加または削除」をクリックしてスコープを追加します。右側に出てくるので真ん中を選びます。そして更新を押すと「非機密のスコープ」の所に今選んだAPIが表示されます。
f:id:nasubiFX:20210109002925p:plain
終わったら保存して次へをクリックして次に画面に進みます。テストユーザーはどちらでも構いません。追加する場合はメールアドレスを入れてください。
最後にこれまで入力した一覧が出てくるので、間違っていたら1番上のアプリ名の右の「アプリの編集」で変更してください。

認証情報の作成

以下の画像の順番で「認証情報」、「+ 認証情報を作成」、「OAuth クライアント ID の作成」をクリックします。
f:id:nasubiFX:20210109003322p:plain

そしたら以下の画面が出てくるので入力していきます。
f:id:nasubiFX:20210109004051p:plain
今回はウェブアプリケーションを選択します。
承認済みの JavaScript 生成元にはトップページのURLを入れとけば大丈夫だとおもいます(多分)。
承認済みのリダイレクト URIGoogle側で認証が完了した場合に呼ばれるURLです。好きなように設定してください。入力が終わったら作成します。
そしたら画面に「クライアント ID」と「クライアント シークレット」が出てくるのでコピーしておきます。忘れた場合でも「認証情報」から「OAuth 2.0 クライアント ID」に今作成したOAuth 2.0 クライアントがあるのでクリックすると「クライアント ID」と「クライアント シークレット」を確認できます。
これでgoogle側の設定は終わりなのでexpressに移ります。

ルーティング

すべてapp.jsに書いていきます。

//app.js(抜粋)
const passport = require('passport');
const session = require('express-session');
const GoogleStrategy = require('passport-google-oauth').OAuth2Strategy

//セッションに保存
passport.serializeUser(function (user, done) {
  done(null, { id: user.id, name: user.name });
});

//セッションから保存されたデータを呼び出し
passport.deserializeUser(function (user, done) {
  done(null, { id: user.id, name: user.name });
});

const clientID = '' //コピーしたクライアント ID
const clientSecret = '' //コピーしたクライアント シークレット
const callbackURL = '' //設定した承認済みのリダイレクト URI
passport.use(new GoogleStrategy({
  clientID, clientSecret, callbackURL
}, function (accessToken, refreshToken, profile, done) { //profileにスコープで設定したアカウント情報が入っている
  process.nextTick(function () {
    return done(null, { id:profile.id,name:profile.displayName });
  })
}
))

app.use( //絶対にpassport.initialize(),passport.session()よりも先に行う。そうしないとエラーになります
  session({
    secret: '', //自由に設定 例:require('crypto').randomBytes(8).toString('hex')で生成したランダムな文字列
    resave: false,
    saveUninitialized: false
  })
);
app.use(passport.initialize());
app.use(passport.session());

app.get('/auth/google', passport.authenticate('google', {
  scope: ['https://www.googleapis.com/auth/userinfo.profile'] //スコープ
}));

app.get('/auth/google/callback',
  passport.authenticate('google', {
    failureRedirect: '/enter', //失敗したときの遷移先
    session: true //セッションを使うためtrue
  }),
  function (req, res) { //成功したときの処理
    res.redirect('/')
  }
)

ほぼ書き方は固定なの好きなようにカスタマイズしてください。
セッションにuserで保存してあるので「req.user」で取り出せます。
scopについては公式のドキュメントを見てください。
https://developers.google.com/identity/protocols/oauth2/scopes#oauth2

/enterでは以下のようにルーティングしています。

//enter.pug
<a href="/auth/google">Sign In with Google</a>

このままではログインしているかわからないのでトップページを少し変更します

var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
  if(req.user){
    res.render('index',{user_name:req.user.name});
  }else{
    res.render('index',{user_name:null});
  }
});

module.exports = router;

これでログインしている時は「user_name」に名前が入るので、あとはテンプレートファイルで表示してあげるだけです。

ログインしている時のみ表示

まず、

//authentication.js
function Ensure(req, res, next){
  if (req.isAuthenticated()) {  // 認証済
      return next()
  }
  else {  // 認証されていない
      res.redirect('/enter')  // ログイン画面に遷移
  }
}

module.exports = 

このような関数を用意します。
あとはこれを ログインしている時のみ表示したいルーティングに組み込むだけです

var express = require('express');
var router = express.Router();
const authentication=require('./authentication.js')

/* GET user page. */
router.get('/', authentication,function(req, res, next) {
   //処理
});

module.exports = router;

参考記事
https://qiita.com/tinymouse/items/ab79a14173ebc7b75274#%E3%81%A9%E3%81%AE%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%A7%E3%82%82%E8%AA%8D%E8%A8%BC%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B
https://dackdive.hateblo.jp/entry/2017/08/28/100000
https://blog.spacemarket.com/code/passport_social_login/
http://www.passportjs.org/docs/google/

JSでブロック崩しを作りました

作り方は公式のサイトを参考にしました。
https://developer.mozilla.org/en-US/docs/Games/Tutorials/2D_Breakout_game_pure_JavaScript

わからなかった所を書いていきます。
まず、パドルの操作で一つ分からなかった所がるんですが、まずは以下のコードを見てください

canvas1.jsが公式のコードでcanvas2.jsが自分が書いたコードです。
パドルを操作すれば分るんですが、自分のコードではパドルがカクカクするんですよね。
どっちのコードでもキーを離したら止まって、またキーを押したら動くという過程は同じはずなんですけどね。この違いってなんなんですかね。

次にゲームオーバーの所です

const Draw = () => {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    DrowBall()
    DrowPaddle()

    if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
        dx = -dx;
    }

    if (y + dy < ballRadius) {
        dy = -dy
    }
    else if (y + dy > canvas.height - ballRadius) {
        if (x > paddleX && x < paddleX + paddleWidth) {
            dy = -dy
        }
        else {
            alert("GAME OVER");
            document.location.reload()
            return //ここを書かないとalertが出続け、リロードが終わらない
        }
    }

    if(rightPressed && paddleX < canvas.width-paddleWidth) {
        paddleX += 7
    }
    else if(leftPressed && paddleX > 0) {
        paddleX -= 7
    }

    x += dx
    y += dy
    requestAnimationFrame(Draw)
}
Draw()

公式のコードではreturnしなくてもリロード出来ていたんですが、自分の場合はreturnしないとalertが出続け、リロードが終わらない状態になりました。
setInterval()と requestAnimationFrame()の違いなんでしょうか?
ちなみに、returnを消してcancelAnimationFrame(Draw)を試しましたがダメでした。

しゃくとり法

しゃくとり法に関しては下記URLのスライド55ページ~を参考にどうぞ
https://www.slideshare.net/yuki2006_debel/21-35882983

ポイントとしては条件に一致するまでrightを動かしていき、処理が終わったらleftを動かす。この時rightの値はそのままにする。

問題は以下を使います
https://atcoder.jp/contests/abc130/tasks/abc130_d

入力まで

N,K=map(int,input().split())
a=list(map(int,input().split()))
right=0 #どこまで探索したか
ans=0 #答え
num=0 #left~rightの合計値

処理部分を書いていきます

for left in range(N):
    while num<K and right<N: #numがKを超えるかrightがNを超えたら終了
        num+=a[right] #値を足す
        right+=1 #rightを動かす

    if num>=K: #numがk以上
        ans+=N-right+1#left=rightが条件を満たしているため、rightに+1しても条件を満たす
        num-=a[left] #leftを動かすためnumからa[left]を引く

しゃくとり法を使うことによってO(n)で解くことができます。
while文の条件は今回の問題の場合は、連続部分列に含まれる全ての要素の値の和はK以上である必要があるので一つ目の条件はそれを満たすようにします。
もう1つの条件はrightは端までいったらループを終了します。
if文はleftを動かている途中でnumがK以下になる場合があるのでそれを弾いています。
ansの所は例えば例題2の場合[3,3,3]という配列が与えられ、left=0,right=2の場合[3,3],[3]に区間が分けられる。その場合左の区間はすでに問題の条件に達しており、右の区間をくっつけたとしても条件を満たすので右の区間がある場合に+1するようにしている。
numは例題1の場合[6,1,2,7]という配列が与えられている。[6,1,2,7]からleftを動かすと[1,2,7]になりa[left]の部分はいらなくなるのでその部分をnumから引いている。

コード全体

N,K=map(int,input().split())
a=list(map(int,input().split()))
right=0
ans=0
num=0
for left in range(N):
    while num<K and right<N:
        num+=a[right]
        right+=1

    if num>=K:
        ans+=N-right+1
        num-=a[left]
print(ans)

ちなみにK以下の個数を求める場合は以下のコードになります

N,K=map(int,input().split())
a=list(map(int,input().split()))
right=0
ans=0
num=0
for left in range(N):
    while right<N and num+a[right]<=K:
        num+=a[right]
        right+=1
    ans+=right-left
    if left==right:
        right+=1
    else:
        num-=a[left]
print(ans)

少しだけ解説をいれておきます。
参考記事の問題をぱくらせていただきます

6 12
5 3 8 6 1 4

これが与えられるとします。
最初のループでleft=0,right=2までいきます。その時左の区間が[5,3]になります。この区間の合計値はK以下なので、個々の値も条件を満たすことになります。[5],[3]も条件を満たす。
上記のことを踏まえると、ansにはその区間のlenghtを足すことになるのでright-leftとしている。

参考記事
https://qiita.com/drken/items/ecd1a472d3a0e7db8dce#%E5%95%8F%E9%A1%8C-2poj-3061-subsequence

ABC130(A~C)

A - Rounding

かかった時間 2分
実行時間 29ms

X,A=map(int,input().split())

if A>X:
    print(0)
else:
    print(10)

B - Bounding

かかった時間 3分
実行時間 22ms

N,X=map(int,input().split())
L=list(map(int,input().split()))
ans=1
num=0
for i in L:
    num+=i
    if num<=X:
        ans+=1
        
print(ans)

C - Rectangle Cutting

かかった時間 50分
実行時間 24ms

W,H,x,y=map(int,input().split())
cx,cy=W/2,H/2 #中心座標

if x==0 or y==0: #座標(x,y)のどちらかが0を通るなら面積は0
     print((W*H)/2,0)
elif  x==cx and y==cy: #中心座標と(x,y)が同じなら2分割する方法は他にもある
    print((W*H)//2,1)
else: #それ以外
    print((W*H)//2,0)

長方形の中心に座標がない場合は他に2分割する方法はない。
今回のも問題は面積の大きくない方の面積の最大値を出力せよなので、2分割する場合の面積の最大値は縦×横//2なのでそれを出力。
これに気付くまでに時間がかかった。

D問題は尺取り方というのを使うらしいので後日記事にします