読者です 読者をやめる 読者になる 読者になる

2noの日記

メモ用

ブログ移行

はてなブログ重いんで、Hugo でメモを書くことにした。公開場所は Github ページ

http://blog.2no.me/

golang+kocha を使ってみて

簡単な Web アプリケーションを作る機会があったので、前々から興味あった golang で作ってみた。

当初 revel で作ろうとしたが、マイグレーションが自前実装?だったりトランザクションをコントローラに実装する点が個人的に気持ちが悪くて止めた。
RoR っぽいし和製という単純な理由で kocha を採用。

やったこと・感じたことを書いていく。

環境

情報が少ない

golang の Web フレームワーク全体に言えることかも知れないが、とにかく少ない。
和製だからこそ日本語の情報が多いのかなと思ったが甘かった。ドキュメント自体も英語。

アプリケーション固有の設定

ドキュメントのデータベース設定に関する項で、「The Twelve-Factor App」にインスパイアされたと書かれており、環境変数を使って設定を行う。
参照: Model | Kocha web application framework for Go

当初、アプリケーション固有の設定をどこに記述して良いか分からず、設定ファイルを用意して読み込ませていた。
The Twelve-Factor App を知ってからは環境変数に記述し、kocha.Getenv で取得するようにした。

データベースのデフォルト文字コードを utf8(mb4) にしてはならない

MySQL+マイグレーションのお話。

デフォルトのストレージエンジンを InnoDB のまま、デフォルトの文字コードを utf8 などにするとマイグレーションが実施されない。

kocha のマイグレーションは、バージョンを管理する為に schema_migration テーブルを作成するのだが、 このテーブルの主キーは VARCHAR(255) であり、InnoDB+utf8 だと 767 byte を越えてインデックスを貼れずにエラーとなる。
ROW_FORMAT を DYNAMIC にして対応しようかと思ったが、SQL がハードコーディングされていたので無理そうだった。
参照: https://github.com/naoina/kocha/blob/37ac3fb2dcdc22508c647d39cd750da6a53fef1d/db.go#L126

ちなみに、schema_migration の作成を失敗しても kocha 上ではエラーならないので注意。

マイグレーションの書き方

ドキュメントを見ると、雛形の生成で話が終わっていてサンプルが無い。

テーブルを作成するのであれば、恐らく genmai で CreateTable を実行するのがベストだと思う。
参照: https://github.com/gurisugi/kocha-sample/blob/master/scripts/create_table.go#L23

今回は細かく設定したかったので SQL で書いた。

package migration

import "github.com/naoina/genmai"

func (m *Migration) Up_20150806072506_CreateUsersTable(tx *genmai.DB) {
    if _, err := tx.DB().Exec(`
      CREATE TABLE users (
          id INT UNSIGNED NOT NULL AUTO_INCREMENT,
          name VARCHAR(100) NOT NULL,
          created_at DATETIME,
          PRIMARY KEY (id)
      ) ENGINE = InnoDB DEFAULT CHARSET 'utf8mb4'
  `); err != nil {
        panic(err)
    }
}

func (m *Migration) Down_20150806072506_CreateUsersTable(tx *genmai.DB) {
    if _, err := tx.DB().Exec(`DROP TABLE users`); err != nil {
        panic(err)
    }
}

ORM で細かいことは出来ない

kocha では genmai という ORM が使われているが、細かい事をするなら内部で使用されている database/sql を取り出して使う必要がある。
詳しい理由は、次の参考サイトに分かり易く書かれている。

hachibeechan.hateblo.jp

もちろんgenmaiにもJOINをビルドするAPIはあるんだけど、ビルドされるフィールドの指定が暗黙的に"テーブル名".*みたいになるため、JOINで持ってきたテーブルのカラムを参照するようなことが出来ない。

まさしくその通り。
現状の ORM で開発は難しく、ほぼ database/sql を使って書いてた。

共通処理

例えば revel ではページを表示する際、以下のレスポンスヘッダを返す。

X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff

これを kocha でも実現するなら、どこに記述するのかが分からなかった。
色々と考えた結果、Middleware を使えばうまくいきそうだったので試したところうまくいった。

▼レスポンスヘッダ出力例

// config/app.go

// ...[略]

type RequestMiddleware struct{}

func (m *RequestMiddleware) Process(app *kocha.Application, c *kocha.Context, next func() error) (err error) {
    header := c.Response.Header()
    header.Set("X-Frame-Options", "SAMEORIGIN")
    header.Set("X-XSS-Protection", "1; mode=block")
    header.Set("X-Content-Type-Options", "nosniff")
    return next()
}

// ...[略]

var (
    AppConfig = &kocha.Config{

        // ...[略]

        // Middlewares.
        Middlewares: []kocha.Middleware{
            &kocha.RequestLoggingMiddleware{},
            &kocha.PanicRecoverMiddleware{},
            &kocha.FormMiddleware{},
            &kocha.SessionMiddleware{

                // ...[略]

            },
            &kocha.FlashMiddleware{},
            &RequestMiddleware{},   // --> 追加
            &kocha.DispatchMiddleware{},
        }
    }
)

同様に認証が必要な場合も Middleware を用意すると良い。
今回は Basic 認証を掛けたかったので、専用の Middleware を用意した。

ページネーションとかそういった類の物はない

kocha 側では用意されていない。
必要なら世に出回っている golang 製のページネーションを持ってくる必要がある。

github.com

github.com

今のうちに使い易いページネーションを作っておくと重宝されるかもしれない。

まとめにならないまとめ

作成した Web アプリケーションは元気よく OpenShift 上で動いている。

今回初めて golang で開発をしたが、全体的に Web に関して未成熟な印象を受けた。
これからどんどん発展していくのだろうから、追い続けたいところ。

kocha に関しては、開発を始める上でとても入り易かった。
願わくば日本語のドキュメントが充実するととても嬉しい。

Sass のディレクトリ構成に関する参考サイト

How to structure a Sass project

github.com

www.sitepoint.com

www.sitepoint.com

【gulp】gulp-webpack で複数 JS の書き出し Tips

webpack はエントリーポイントを指定することで複数の JS ファイルの書き出しが可能。
gulp-webpack も公式で以下の方法を紹介している。

var gulp = require('gulp');
var webpack = require('gulp-webpack');
var named = require('vinyl-named');
gulp.task('default', function() {
  return gulp.src(['src/app.js', 'src/test.js'])
    .pipe(named())
    .pipe(webpack())
    .pipe(gulp.dest('dist/'));
});

上記の場合、dist/app.jsdist/test.js が出来上がる。
一つ一つファイル名書くのは面倒なので、glob で省略するとこうなる。

var gulp = require('gulp');
var webpack = require('gulp-webpack');
var named = require('vinyl-named');
gulp.task('default', function() {
  return gulp.src('src/**/*.js')
    .pipe(named())
    .pipe(webpack())
    .pipe(gulp.dest('dist/'));
});

しかしこれ、ファイル名がかぶると想定外のことが起きる。

▼ファイル名がかぶる構成例

src
├─hoge
│  └── app.js
└─fuga
    └── app.js

この場合、出来上がるのは dist/app.js のみで、src/hoge/app.jssrc/fuga/app.js がマージされて出力される。

内部的にエントリーポイントはこの様に設定されている。

{
  entry: {
    app: [ '/path/to/src/hoge/app.js', '/path/to/src/fuga/app.js' ]
  }
}

本当に個別に書き出したいなら一工夫が必要。こんな感じ。

var gulp = require('gulp');
var webpack = require('gulp-webpack');
var named = require('vinyl-named');
gulp.task('default', function() {
  return gulp.src('src/**/*.js')
    .pipe(named(function(file) {
      return file.relative.replace(/\.[^\.]+$/, '');
    }))
    .pipe(webpack())
    .pipe(gulp.dest('dist/'));
});

vinyl-named はデフォルトだと拡張子なしのファイル名を返す様になっているので、ディレクトリも含む名前で返してあげるとマージは起きない。
file.relative の値は gulp.src の値に左右される点に注意。

内部的にエントリーポイントはこの様に設定されている。

{
  entry: {
    'hoge/app': [ '/path/to/src/hoge/app.js' ],
    'fuga/app': [ '/path/to/src/fuga/app.js' ]
  }
}

これにより、dist 以下には hoge/app.jsfuga/app.js が出来た。

【gulp】gulp-pleeease 1.2.0 と gulp-sourcemaps を併用すると sourceMappingURL が二つ追加される件

表題のとおり、gulp-pleeease 1.2.0 では sourcemap がおかしい。
例えば、以下のタスクを実行すると sourceMappingURL が二つ挿入される。

use strict';

var gulp = require('gulp');
var sass = require('gulp-sass');
var please = require('gulp-pleeease');
var sourcemaps = require('gulp-sourcemaps');

gulp.task('css', function () {
  gulp.src('./src/*.scss')
    .pipe(sourcemaps.init())
      .pipe(sass())
      .pipe(please())
    .pipe(sourcemaps.write())
    .pipe(gulp.dest('./dest'));
});

▼結果

body{font:100% Helvetica,sans-serif;color:#333}
/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW... */
/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW... */

この問題に関してプルリクは既に出ているが、まだマージされていない。

早くマージされてほしいところ。

【webpack】CommonJS に対応していない JS を exports-loader で華麗に対応する

スライドメニューの実装に sp-slidemenu を fixed 対応した以下のライブラリを使用した。

github.com

このライブラリは npm にも bower にも登録されていない。
bower.json があれば bower install も可能だろうが、無いので npm で github のパスを指定してインストールする。
git submodule はオペレーションの関係で今回使えない。

$ npm install --save-dev git://github.com/tokunagakazuya/sp-slidemenu#0.1.2-release

▼ インストールが成功すると、node_modules 直下に sp-slidemenu.js ディレクトリが出来る。

node_modules/sp-slidemenu.js/
├─Gruntfile.js
├── README.md
├─package.json
├─sample
│  ├─css
│  │  └── styles.css
│  ├─demo1.html
│  ├─demo2.html
│  ├─demo3.html
│  ├─demo4.html
│  ├─demo5.html
│  └─img
│      └── menu_button_back.png
├── sp-slidemenu-min.js
└── sp-slidemenu.js

CommonJS に対応していないので exports-loader を導入する。

$ npm install --save-dev exports-loader

次に webpack を設定する。

▼ webpack.config.js(必要なところだけ抜粋)

var path = require('path');
var pathToSpSlidemenu = path.join(__dirname, 'node_modules', 'sp-slidemenu.js', 'sp-slidemenu.js');

module.exports = {
  resolve: {
    alias: {
      SpSlidemenu: pathToSpSlidemenu,
    },
  },
  module: {
    loaders: [
      { test: pathToSpSlidemenu, loader: 'exports?SpSlidemenu' },
    ],
  },
};

後はソース内で呼び出すだけ。

var SpSlidemenu = require('SpSlidemenu');

設定で何をやっているかというと、resolve.aliasnode_modules/sp-slidemenu.js/sp-slidemenu.js までのパスにエイリアスをはり、require('SpSlidemenu') で呼び出せる様にしている。
※パスが通っていれば、別にエイリアスをはる必要は無いと思う。

また、CommonJS に対応する為、module.loaders にて require('SpSlidemenu') が実行されたら exports-loader を発動するように設定している。
exports?SpSlidemenu とすることで、require('SpSlidemenu') を実行した元ソースに sp-slidemenu.js の内容がマージされ、更に module.exports = SpSlidemenu; が自動付与される。

exports-loader の使用例がネット上にあまり無くて困った。

環境

  • webpack 1.5.3
  • exports-loader 0.6.2