今後の勉強について
プログラミング入門Webアプリが一通り終わったので今後は以前考えたアプリケーションを構築したい。
あとプログラミング入門の2018年度版が始まる?っぽいので動画の授業とペースを合わせて追いかけることで復習したい。
N予備校のプログラミング入門メモ18
はじめに
中途半端なところで中断してしまった。
4章 実践サーバーサイドプログラミング
20.コメントの表示の更新
コメントを取得する部分を実装する。予定にアクセスした際に予定に関するコメントを全件取得する。
コメントのRouterオブジェクトを実装。Applicationオブジェクトに登録する。
テストを書く。スタブを使ってログインしてスケジュールを作ってからパスにコメントのオブジェクトを渡してコメントを作成、レスポンスのステータスの確認とDBの確認を書いた。
テンプレートの方にコメントの表示とボタンを書いて、jQuery(app/entry.js
)でコメントをPOSTするクリックイベントハンドラを実装。
ここまででwebpackしてからサーバを再起動して確認したところ、テンプレートのインデントを間違えて候補ごとにコメントが出てしまってちょっと笑った。
Promiseのチェインについて。そういえば以前の職場ではthenがすごい入れ子になってて直さないのかなぁと気になっていた。講座ではネストが深い関数についてチェインでリファクタリングした。
21.予定の編集と削除
編集機能を作る。まず編集画面のテンプレートを作成。ちなみに予定作成画面と要素はほぼ同じなので、作成モードと編集モードみたいなパラメータで同じテンプレートを使うようにも作れそうな気がする。どっちがいいんだろうか。
Routerオブジェクトにパスのハンドラを登録。アクセスして表示されるか確認する。
POSTメソッドの処理を実装。対象のスケジュールを取得して自分のIDとの一致を確認、update
関数でスケジュールを更新したら、候補を追加する。この際同じ処理を切り出して関数化したりした。
削除機能を作る。編集画面に削除ボタンを追加し、テストで作成した削除関数を持ってきて削除機能は完成。
これで要件はすべて満たせた。
22.デザインの改善
Bootstrapを使ってかっちょいい感じにした。
グローバルオブジェクトを取得するには以下のようにする。
const global = Function('return this;')();
23.セキュリティ対策と公開
脆弱性一覧を見て対策が必要なものを考える。
csurf
を使って対策する。
const csrfProtection = csrf({ cookie: true }); router.get('/new', authenticationEnsurer, csrfProtection, (req, res, next) => { res.render('new', { user: req.user, csrfToken: req.csrfToken() }); }); router.post('/', authenticationEnsurer, csrfProtection, (req, res, next) => { // 中略
みたいにするとトークンを発行・確認する。あとは
input(type="hidden" name="_csrf" value!="#{csrfToken}")
のようにテンプレートでトークンをPOSTするようにして対策完了。csurf
の使い方は README.mdを参照。
認証後にアクセスした画面にリダイレクトされるように実装を変更する。
認証する関数でログイン画面にアクセスする際、元のURLをクエリに含めるようにして、ログインのRouterオブジェクト側でクエリの内容をCookieに保存。OAuthのコールバック関数内でCookieから元のURLを取得してそこにリダイレクトするように実装した。
Herokuへの公開の準備をする。
app.json
でアプリケーション情報を記述、package.json
にNode.jsのバージョンを追記、DBのURLもHerokuのものを使用するに変更。ここまでをコミットしておく。
Herokuにログインしてサーバーを作成、URLを控えておく。
以下のようにPostgreSQLのDBを作成。
heroku addons:create heroku-postgresql:hobby-dev
一旦Herokuにpush。ここまででトップページが表示できる。
GitHub認証のためにアプリケーションを登録する。Client IDとClient Secretを控える。
Herokuの環境変数に登録。
heroku config:set HEROKU_URL='https://xxx-xxxxxxx-XXXXXX.herokuapp.com/' heroku config:set GITHUB_CLIENT_ID='000000000000000000' heroku config:set GITHUB_CLIENT_SECRET='000000000000000000000000000000000'
アプリケーション側でIDと秘密鍵、コールバックするURLを上記を使うように変更する。
以上をコミットしてHerokuにpushするとアプリ開発は完了。
ちなみにトップページを表示する段階でHerokuのエラー画面が出たのでheroku logs
して確認して修正してデプロイしなおすなどした。
練習問題は、何かネタを考えてみよう。ひとまず4章は完了。
N予備校のプログラミング入門メモ17
はじめに
妹とデートしてきた。
4章 実践サーバーサイドプログラミング
18.予定の一覧の表示
講座用のプロジェクトをcloneしてnpm start
したところ、DBのエラーが出た。
Unhandled rejection SequelizeDatabaseError: column "scheduleId" does not exist at Query.formatError (/home/vagrant/workspace/schedule-arranger-4018/node_modules/sequelize/lib/dialects/postgres/query.js:357:14) at Query.<anonymous> (/home/vagrant/workspace/schedule-arranger-4018/node_modules/sequelize/lib/dialects/postgres/query.js:88:19) at emitOne (events.js:96:13) at Query.emit (events.js:188:7) at Query.handleError (/home/vagrant/workspace/schedule-arranger-4018/node_modules/pg/lib/query.js:108:8) at Connection.<anonymous> (/home/vagrant/workspace/schedule-arranger-4018/node_modules/pg/lib/client.js:171:26) at emitOne (events.js:96:13) at Connection.emit (events.js:188:7) at Socket.<anonymous> (/home/vagrant/workspace/schedule-arranger-4018/node_modules/pg/lib/connection.js:109:12) at emitOne (events.js:96:13) at Socket.emit (events.js:188:7) at readableAddChunk (_stream_readable.js:176:18) at Socket.Readable.push (_stream_readable.js:134:10) at TCP.onread (net.js:547:20)
まだ何もいじっていないがscheduleId
が存在しないという旨のエラーのようなので一応コードを確認するも特に問題がない。ということでDBの方が不整合になっているのかと思ってpsql
でdrop database
してcreate database
したところ、今度はちゃんと起動した。前回の写経で変な風にtypoしたくせにつじつまが合って起動できてしまったといったところか。
index.jade
にログイン/ログアウトボタンを追加してRouterオブジェクトのindex.js
でテンプレートにユーザ情報のオブジェクトを渡すように修正。
新規予定作成画面のテンプレート、そこからPOSTを受け取るRouterオブジェクトを実装する。認証を確認するハンドラ関数も実装しておき、Routerのget
関数などの第二引数にしていすることで呼び出し時に認証が確認されるようにできる。
'use strict'; function ensure(req, res, next) { if (req.isAuthenticated()) { return next(); } res.redirect('/login'); } module.exports = ensure;
const authenticationEnsurer = require('./authentication-ensurer'); router.get('/new', authenticationEnsurer, (req, res, next) => { res.render('new', { user: req.user }); });
上記のようにするとログインしていない状態で予定作成画面/new
にアクセスするとログイン画面にリダイレクトされる。
予定作成に使うUUIDを実装する。
yarn add node-uuid@1.4.7
インストールしたらv4
という関数を呼び出すとUUID文字列を取得できる。
予定と候補を保存する機能を実装する。sequelizeオブジェクトのbulkCreate
関数で複数のデータをDBに保存できる。
Promiseについて。そういえば解説されてなかった。非同期でよく見るやつ、というイメージだったけど解説もだいたいそんな感じだった(失礼)。
index.jade
に自分で作った予定の一覧を表示するように修正。each in
を使ってテーブルの行をループで表示する。Routerオブジェクトからテンプレートに予定の一覧を渡すのを忘れずにやる。以前もやったfindAll
関数を使って自分が作った予定一覧を取得する。
/schedule/:scheduleId
を実装する。:scheduleId
はUUIDの文字列。
RouterオブジェクトのfindOne
関数は1行を取得する。
ここでinclude
というのが急に出てきた。(忘れてるだけかもしれない)
Schedule.findOne({ include: [ { model: User, attributes: ['userId', 'username'] }],
事前に Schedule.belongsTo(User, {foreignKey: 'createdBy'});
と従属の設定をしているのでこうやってユーザー情報が取得できる、というもののようだ。たぶん。user
プロパティにユーザー情報が設定される。
練習問題で予定が表示されることのテストを書いたが、そこでexpect
関数でテストしたあとにend
関数内でテストで作成したデータを削除する処理が書かれている。
it('予定が作成でき、表示される', (done) => { User.upsert({ userId: 0, username: 'testuser' }).then(() => { request(app) .post('/schedules') .send({ scheduleName: 'テスト予定1', memo: 'テストメモ1\r\nテストメモ2', candidates: 'テスト候補1\r\nテスト候補2\r\nテスト候補3' }) .expect('Location', /schedules/) .expect(302) .end((err, res) => { let createdSchedulePath = res.headers.location; request(app) .get(createdSchedulePath) .expect(/テスト予定1/) .expect(/テストメモ1/) .expect(/テストメモ2/) .expect(/テスト候補1/) .expect(/テスト候補2/) .expect(/テスト候補3/) .expect(200) .end((err, res) => { // テストで作成したデータを削除 let scheduleId = createdSchedulePath.split('/schedules/')[1]; Candidate.findAll({ where: { scheduleId: scheduleId } }).then((candidates) => { candidates.forEach((c) => { c.destroy(); }); Schedule.findById(scheduleId).then((s) => { s.destroy(); }); }); if (err) return done(err); done(); }); }); }); });
19.出欠の表示と更新
2次元のデータ構造についての話。特別新しい話はなかった気がする。ApplicationオブジェクトにRouterオブジェクトを設定する際、同じパスに対して複数のRouterオブジェクトを設定しているのはこういうのもあるのかという感じ。
20.コメントの表示と更新
出欠の更新、コメントの更新APIを実装して、jQueryとwebpackを入れてAJAXでそれぞれが更新できるようにした。
一時中断してたけど日をまたいだので一旦投稿。
N予備校のプログラミング入門メモ16
はじめに
男子スキークロス見てたら開設の人のニーデラー推しにハマってしまい予定より遅い開始になりました。
4章 実践サーバーサイドプログラミング
10.RDBMS と SQL
PostgreSQL って「ポストグレ」だったのか「ポストグル」って読んでた。
sudo su - postgres
でpostgres
ユーザを開始してpsql
。以前やったのと同じ。
CREATE DATABASE diary_1;
でdiary_1
というDBを作成し、\c diary_1
で接続。
CREATE TABLE pages (write_date DATE PRIMARY KEY, body TEXT);
でpages
というテーブルを作成。ここでは列名、型、主キーを設定している。
この辺は以前の講座でsequelize? だったかっていうモジュールを使ってJSで定義を書いたのと同じことを直接やっている。
\dt
でテーブル一覧を表示する。何の略だろ。すこーしだけググったけど出てこなかった。
INSERT INTO pages (write_date, body) VALUES ('2016-02-22', '最初の日記');
でデータを挿入する。
SELECT body FROM pages;
でpages
から取得してbody
を表示する。SQL Serverなら触ったことあるので簡単なSELECT文ならたぶん分かる。細かい文法の差とかはあるんだろうけど。
複数のデータを入れたり主キーがダブるように入れたりして挙動を見た。
N予備校のプログラミング入門を始めるにあたって実はUS配列のキーボードも一緒に始めたのだけど、シングルクォーテーション(')の入力がEnterの隣にあっていつか誤ってEnter押しちゃうなーって思ってたらSQLのDELETE文で押しちゃってヒヤッとした。最後にセミコロン(;)を入力するまで実行されないという動きもこれで初めて確認できた。間違ってEnterした時の取り消し(セミコロンを入力してない状態を解除する)ってどうすればいいんだろうか。とりあえず明らかに誤った文法を入力してエラーにして回避した。
DELETE FROM pages WHERE write_date='2016-02-29';
してデータを消したりした。
SELECT * FROM pages WHERE body LIKE '%寝た%';
さっき簡単なSELECT文ならたぶん分かるって書いたけどLIKE句初めて見た(小声)。
DROP TABLE ${テーブル名};
でテーブル自体を削除する。
\q
でPostgreSQLのフロントエンドを終了。そのままexit
でpostgres
ユーザも終了する。
RDBのデータの定義を扱うものをDDL(Data Define Language)、テーブルのデータ自体を扱うものをDML(Data Manipulation Language)、DBをコントロールするものをDCL(Data Control Language)という。試験問題みたいだあ。
'UPDATE pages SET body='三月になった' WHERE write_date='2016-03-01';'で本文を更新した。
11.データモデリング
データモデリングのお話。非正規形であるテーブルを正規形に直したりする。今度ゆっくり読む。
psql diary2
でdiary2
に接続した状態でフロントエンドを開く。
CREATE TABLE users (userid SERIAL PRIMARY KEY, name VARCHAR(16), gender CHAR(1));
IDを1,2,3...とするためにuserid
をSERIAL
型で定義している。
CHAR
とVARCHAR
の違いは、CHAR
はスペースパディングすること。CHAR(5)
にa
を入れたら、a____
みたいにになる(アンダーバーはスペース)。
ALTER TABLE ${テーブル名} ADD ${追加する列の名前} ${データ型};
で既存のテーブルに列を追加できる。
12.テーブルの結合
SELECT * FROM diaries JOIN users ON diaries.userid = users.userid;
JOIN
句でテーブルを結合。
内部結合と外部結合がある。内部結合はどちらのテーブルにも存在したものを結果に含める。AND条件みたいな感じ。
INNER JOIN
と書くと、動作は同じだが内部結合であることが読み取りやすくなる。
外部結合は以下がある。どのテーブルを残すかの違い。 - 左外部結合(LEFT JOIN) - 右外部結合(RIGHT JOIN) - 完全外部結合(FULL JOIN)
SELECT id, body, comment FROM diaries LEFT JOIN comments ON diaries.id = comments.diary_id;
でdiaries
が残るような結果になる。さらにORDER BY
句を使ってソートする。
SELECT id, body, comment FROM diaries LEFT JOIN comments ON diaries.id = comments.diary_id ORDER BY id;
13.インデックス
RDBのパフォーマンスについて。
EXPLAIN ANALYZE SELECT * FROM scores WHERE score = 100;
のようにEXPLAIN ANALYZE
をつけると実行時間が計れる。
CREATE INDEX ON scores (score);
とするとscores
のscore
列にインデックスが作成されてscore
列に対する処理が早くなる。ほかの列も早くしたい場合は別途インデックスを作成する必要がある。
インデックスがない場合はシーケンシャルスキャン(1行ずつなめるスキャン)をしていたが、インデックスを作成するとすべてを調べる必要がなくなるので早くなる。
B木、二分探索木に似てるなと思って調べたら二分探索木→AVL木→B木のように改良されていった感じだった。
14.集計とソート
AVG``COUNT``MIN``MAX``SUM
等が使える。
GROUP BY
句でグループ化できる。以下のようにすると名前ごとに集計したデータが表示される。
SELECT name, MAX(stage), AVG(score), COUNT(score) FROM scores INNER JOIN users ON scores.user_id = users.user_id GROUP BY name;
数が10000未満のユーザだけ取得したい、という場合にWHERE
句にCOUNT(score) < 10000
というようには記述できない。WHRER
句は行についての条件を書く句であるため。
グループについての条件を書くHAVING
句を使う。
SELECT name, MAX(stage), AVG(score), COUNT(score) FROM scores INNER JOIN users ON scores.user_id = users.user_id GROUP BY name HAVING COUNT(score) < 10000;
15.「予定調整くん」の設計
- 要件を出す
- 用語を定義する
- 英語表記も用意するとコード書くときに便利
- URL設計
- ページ構成
- Web APIのURL設計
- モジュール設計
自分でアプリケーションを作る時にも参考になりそう。
最後にGitHubでアプリケーションを登録した。
16.認証の実装とテスト
Expressでひな形を準備。
express <project-name>
ディレクトリを移動して一連の作業。
echo "node_modules/" > .gitignore git init git add . git commit -am "first commit"
helmetをインストール。
yarn add helmet@1.1.0
app.js
にhelmetを読み込んでapp.use(helmet());
することで有効化する。
./routes
にRouterモジュールのファイル群を作成する(login.js
とかlogout.js
とか)
認証に必要なモジュールをインストール
yarn add passport@0.3.2 yarn add passport-github2@0.1.9 yarn add express-session@1.13.0
GitHub認証の実装は以前とほぼ同じ。
Routerオブジェクトをテストする。
yarn add mocha@3.2.0 --dev yarn add supertest@1.2.0 --dev yarn add passport-stub@1.1.1 --dev
supertest
はRouterオブジェクトをテストするモジュール、passport-stub
は認証のスタブ。
supertest
と作っているアプリ../app
を読み込んでテストを記述する。
'use strict'; const request = require('supertest'); const app = require('../app'); describe('/login', () => { it('ログインのためのリンクが含まれる', (done) => { request(app) .get('/login') .expect('Content-Type', 'text/html; charset=utf-8') .expect(/<a href="\/auth\/github"/) .expect(200, done); }); });
上記ではヘッダが含まれているか(文字列を2つ渡す)、GitHubへのリンクが含まれているか(正規表現を渡す)をテストして、最後にステータスコードとdone
関数を渡す。
認証に関するテストを書く。
const passportStub = require('passport-stub'); describe('/login', () => { before(() => { passportStub.install(app); passportStub.login({ username: 'testuser' }); }); after(() => { passportStub.logout(); passportStub.uninstall(app); }); // 中略 it('ログイン時はユーザー名が表示される', (done) => { request(app) .get('/login') .expect(/testuser/) .expect(200, done); }); });
before``after
関数はそれぞれdescribe
の前後に処理される。ここではスタブにアプリをインストールしてテストユーザでログイン、終了後にログアウトしてアプリをアンインストールしている。
17.ユーザーの保存
sequelizeモジュールを利用してDBを扱う。
yarn add sequelize@3.25.0 yarn add pg@6.1.0 yarn add pg-hstore@2.3.2
それからschedule_arranger
というDBを作成。
複数のモデルを別ファイルに記述したいのでsequelize読み込み用のmodels/sequelize-loader.js
を用意する。
'use strict'; const Sequelize = require('sequelize'); const sequelize = new Sequelize( 'postgres://postgres:postgres@localhost/schedule_arranger', { logging: true }); module.exports = { database: sequelize, Sequelize: Sequelize };
あとはひたすらモデルを作成するけどモデルの写経がなかなかしんどい。どういうときにインデックスを作成して、こんな場合にはインデックスを作成しなくてもよいみたいな話学びがある。
sequelizeはエンティティ同士の関係を定義しておくとテーブルを結合して取得ができるらしい。
// モデルの読み込み var User = require('./models/user'); var Schedule = require('./models/schedule'); var Availability = require('./models/availability'); var Candidate = require('./models/candidate'); var comment = require('./models/comment'); User.sync().then(() => { Schedule.belongsTo(User, {foreignKey: 'createdBy'}); Schedule.sync(); Comment.belongsTo(User, {foreignKey: 'userId'}); Comment.sync(); Availability.belongsTo(User, {foreignKey: 'userId'}); Candidate.sync().then(() => { Availability.belongsTo(Candidate, {foreignKey: 'candidateId'}); Availability.sync(); }); });
各モデルを読み込み、sync
関数でモデルに合わせてDBのテーブルを作成する。作成が終わった後に実行したい処理をthen
でつないでいる。なのでUserのテーブルを作成→外部キーを設定してテーブルを作成→外部キーを設定して……みたいな流れで各テーブルを作成している。
認証が実行されたときに呼び出される処理のところに以下を記述
process.nextTick(function () { User.upsert({ userId: profile.id, username: profile.username }).then(() => { done(null, profile); }); });
upsert
関数でUPDATE(無ければINSERT)を行う。これでユーザーを永続化できた。
ちょっと疲れてきたので休憩。
N予備校のプログラミング入門メモ15
はじめに
もう少しだけやります。
4章 実践サーバーサイドプログラミング
09.WebSocket
Socket.IOというアプリを利用する。
npm install socket.io@1.4.5 socket.io-client@1.4.5 --save
yarnの場合は特に--save
をつけなくてもpackage.json
に反映される。npm install --save-dev
のときはyarn add --dev
にする必要がある。
bin/www
を編集する。socket.io
モジュールを読み込んでsocket.emit
関数でWebSocketの接続に対してイベントを発行するようにemitServerStatus
関数を実装して、socket.io
モジュールのconnection
イベントが発火した際に実装したemitServerStatus
関数を定期的に実行する感じのコードを書いた。
function emitServerStatus(socket) { socket.emit('server-status', { loadavg: os.loadavg() }); console.log('server-status event emitted.'); } io.on('connection', function (socket) { setInterval(emitServerStatus, 10, socket); });
setInterval
で関数を10ミリ秒ごとに呼び出しててマジかってなるけどWebSocketなら大丈夫ってことなんだろうきっと。
ここまで実装してPORT=8000 npm start
するとエラーが出て起動しない。追記した部分をコメントアウトして再度サーバを起動したが同じエラーが出た。
node_modules
を削除して再度yarn
した後、試しに
yarn add socket.io@1.4.5 socket.io-client@1.4.5
してサーバを起動したらうまくいった。yarnでインストールしても一緒だと思うのでtypoしてたんじゃないかと思っている。
ここまでのコードではまだSocket.IOの恩恵を受けていない。
クライアント側で以下のようにする。
const socket = require('socket.io-client')('http://localhost:8000'); socket.on('server-status', (data) => { loadavg.text(data.loadavg.toString()); });
localhost:8000
にアクセスすると怒涛の勢いでserver-status event emitted.
の文字が表示された。
webpack
するのをよく忘れるので忘れないこと。
yarn run webpack
で./node_modules/.bin/
の中のwebpack
を実行してくれる。
gitにコミットするとき、コメントを待ちがえたままコミットしてしまった。git commit --amend -m "コメント"
とすると直前のコメントを変更できた。
途中で電池が切れて落ちたんだけど、復帰したらここまで書いたところが生きてて感動した。新しいPCありがとう、学習用に買ってよかった。
イラストのトレース練習サイト(仮)メモ
はじめに
以前読んだ「【これは納得】 #0から絵師になる方法論 #ダラダラと絵師になる方法 【従来の労力を軽減】」という記事、何となくイラストを練習したいけど続かないしそもそもどうやって練習すればいいのか分からないという自分にとても合ってる練習方法なのでは? と思って早2年。やらない。知ってたけどやらないんだよなあ。うん。
ということでトレースの練習に特化したアプリでもあれば継続して練習するのでは、しかも自分で作ったアプリならなおさらなのでは!? という感じのモチベーションでアプリを作ってみたいと思います。今はN予備校をやってるけど、終わってからがいいか並行するかは未定。
(たぶんアプリを作ってもイラストの練習はやら)ないです。
必要な機能を書きなぐる
- イラストをトレースする
- イラストの上から線を描く
- 線の太さを変える
- 線の色を変える
- イラストの上に書いた線を消す
- 消しゴムのように消す
- すべて消す
- UNDO
- 書いた線を非表示にする
- イラストの上から線を描く
- イラストを表示する
- イラストを半透明にする
- イラストを非表示にする
- イラストを横に表示する
- イラストを左右反転する
- イラストを上下反転する
- 書いた線を保存する
- ローカルに
- Web上に
- 書いた線を共有する
- トレースするイラストを用意する
- アップロードする?
- ローカルの画像を表示するだけとかできないかな
- Web上の画像をもってくる
- アプリ側であらかじめ用意する?
- アップロードする?
色々書いたけど、とりあえずパスはログインとお絵かき(トレース)画面くらいがあればいいのかな。基本的な部分以外は作りながら考えよう。
path | 機能 |
---|---|
/ | トップ画面 |
/login | ログイン画面 |
/logout | ログアウト画面 |
/main | トレースする画面 |
まず最小構成で作るためにお絵かき画面だけでもいいかもしれない。そもそもログインが必要かどうかも怪しい。
お絵かき機能はcanvasで実現できそう。簡単な線が描ける程度のものを想定していて、「canvas お絵かき」とかでググるとそれっぽいコードがたくさん出てくるのでパク参考になるだろう。
画像を表示する部分も、画像をどこから持ってくるのかはさておき、canvasでレイヤーを実現できるかは調べてみるがそもそも要素を2つ重ねるとかすればできそうである。半透明・非表示も並べて表示もなんならCSSで実現できる。
まずはそこまで作ってみよう。