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

シンクロ・フード エンジニアブログ

飲食店.COMを運営する、株式会社シンクロ・フードの技術ブログです

レガシーなフレームワークでcssの圧縮を自動化する

シンクロ・フードでフロントエンドの開発を担当している四之宮です。
前回投稿した「レガシーなフレームワークでcss/javascriptのキャッシュクリアを自動化する」が思っていた以上にアクセスがありましたので、今回はその続編となります。
本記事からでも理解できるようにしたつもりですが、上記の記事から読んでいただくとより理解していただけるかなと思います。

ちなみに、前回の記事を投稿してから約2か月ですが、この間にReactを使ったサービスの開発を行いリリースしています。
弊社のReact導入の手順に関しては、下記の通りになります。
ご興味があれば是非ご確認ください。
既に稼働しているWebサービスに対してgulp+webpackでReactをビルドする

さて、いつもレガシーばかりではないこともアピールできたので、本題に入りたいと思います。

CSSの圧縮を行う方法

弊社ではgulpによるscssのコンパイルを行っているため、gulp-cssminというnpmを使用しました。

gulp-cssmin

下記コマンドでインストールを行います。

npm install --save-dev gulp-cssmin  

実際の使用例は下記の通りです。

var cssmin = require('gulp-cssmin');  
  
gulp.task('cssmin', ['sassのbuildなど'], function () {  
    return gulp.src('dest/webapp/**/*.css')  
        .pipe(cssmin(  
            {  
                'processImport': false,  
                'advanced': false  
            }  
        ))  
        .pipe(gulp.dest('dest/webapp/'));  
});  

圧縮する際のオプションは以下のものが設定できます。
https://github.com/jakubpawlowicz/clean-css

今回使用しているオプションは2つあります。
どちらのオプションも圧縮前のcssの記述方法によっては、致命的なバグを生んでしまうので注意が必要です。
また、圧縮前のcssは可能な限り綺麗な方が圧縮による思わぬバグを防げるため、lintを通してから行うのもいいかと思います。

※弊社では、この2つのオプションで運用できていますが、導入する環境に応じて適切なオプションを指定してください

processImport

css内の@importを圧縮の際に残すためのオプションです。
@importを使うことはないかと思いますが、弊社のcssは本当に古いものがありこれの対応をする必要がありました。
@importを使っている際は注意が必要ですが、使っていないのであればこの指定は不要になります。

advanced

font-sizefontborder-colorborderといったような、親子関係のようなプロパティが指定されているとき、これらをまとめあげるかを決めるオプションになります。
'advanced': falseを指定しなかった場合の例は下記の通りです。

圧縮前

.test {  
    border-color: #aaa;  
    border: solid 1px;  
}  

圧縮後

.test {  
    border: solid 1px;  
}  

'advanced': falseの指定が無いと、border-colorの指定が無くなってしまいます。


以上でcssの圧縮処理は完了です。
しかし、弊社では圧縮前のファイルと、圧縮後のファイルを別名で保存する対応を行っています。
ここからはそれについて説明していきたいと思います。

圧縮後のCSSの名前を変更する

gulp-renameというnpmを使用します。

gulp-rename

下記コマンドでインストールを行います。

npm install --save-dev gulp-rename  

使用例は下記の通りです。
suffixでリネイム後のファイル名を指定します。
ここでは、○○.min.cssにするためsuffix: '.min'を指定しています。
これで、元のファイル名.min.cssというファイル名でファイルがコピーされます。

var rename = require('gulp-rename');  
  
gulp.task('rename', function () {  
    return gulp.src('dest/webapp/**/*.css')  
        .pipe(rename({suffix: '.min'}))  
        .pipe(gulp.dest('dest/webapp/'));  
});  

これを先ほどの圧縮と合わせます。

var cssmin = require('gulp-cssmin');  
var rename = require('gulp-rename');  
  
gulp.task('cssmin', function () {  
    return gulp.src('dest/webapp/**/*.css')  
        .pipe(cssmin(  
            {  
                'processImport': false,  
                'advanced': false  
            }  
        ))  
        .pipe(rename({suffix: '.min'}))  
        .pipe(gulp.dest('dest/webapp/'));  
});  

これで、圧縮後のファイルが、○○.min.cssという名前で生成することができます。

HTMLからの読み込むcssを圧縮後のものに変更する

圧縮後のファイル名を変更しましたが、hrefで指定されているcssは圧縮されていないので、読み込むcssを変更する必要があります。

そこで、gulp-replaceというnpmを使用します。

gulp-replace

下記コマンドでインストールを行います。

npm install --save-dev gulp-replace  

使用例は下記の通りです。
書き換えの対象のcssの取得と、書き換え後のcssのファイル名を正規表現で指定する必要があります。
replaceメソッドの第1引数で対象を取得して、第2引数で書き換え指定しています。

var replace = require('gulp-replace');  
  
gulp.task('htmlreplace', function() {  
    return gulp.src('dest/webapp/WEB-INF/view/**/*.jsp')  
        .pipe(replace(/<link(.*)href=(["`]\/[^/].*)\.css/g, '<link$1href=$2.min.css'))  
        .pipe(replace(/<link(.*)href=(["`]\/[^/].*)\.min\.min\.css/g, '<link$1href=$2.min.css'))  
        .pipe(gulp.dest('dest/webapp/WEB-INF/view/'));  
});  

2つ目のreplace処理についてですが、ライブラリなどの圧縮済のmin.cssの対応のための記述になります。
元々、○○.min.cssとなっている箇所を、1つ目のreplace処理によって、○○.min.min.cssにしてしまうため、これを元に戻しています。
1つ目のreplace処理の正規表現でmin.cssを除外できれば、この処理は不要になります。
ファイル名のminと圧縮を表すminを区別することができなったので、このような形になっています。

圧縮からhtmlの書き換えまでをまとめる

ここまで行ってきた処理を全てまとめたものが下記になります。
※ライブラリ用などの圧縮済のmin.cssを除外する処理を追加しています

var cssmin = require('gulp-cssmin');  
var rename = require('gulp-rename');  
var replace = require('gulp-replace');  
  
// 各cssファイルの圧縮  
gulp.task('cssmin',  function () {  
    return gulp.src(['dest/webapp/**/*.css', '!dest/webapp/**/*.min.css'])  
        .pipe(cssmin(  
            {  
                'processImport': false,  
                'advanced': false  
            }  
        ))  
        .pipe(rename({suffix: '.min'}))  
        .pipe(gulp.dest('dest/webapp/'));  
});  
  
// cssの読み込みを圧縮したものに書き換える  
gulp.task('htmlreplace', ['cssmin'], function() {  
    return gulp.src('dest/webapp/WEB-INF/view/**/*.jsp')  
        .pipe(replace(/<link(.*)href=(["`]\/[^/].*)\.css/g, '<link$1href=$2.min.css'))  
        .pipe(replace(/<link(.*)href=(["`]\/[^/].*)\.min\.min\.css/g, '<link$1href=$2.min.css'))  
        .pipe(gulp.dest('dest/webapp/WEB-INF/view/'));  
});  

前回のキャッシュクリア自動化対応にを組み込む

更に、前回のキャッシュクリア自動化対応を組み込むと下記のようになります。
圧縮したファイルを元に、キャッシュクリアを行っています。

var CacheBuster = require('gulp-cachebust');  
var cssmin = require('gulp-cssmin');  
var rename = require('gulp-rename');  
var replace = require('gulp-replace');  
  
// css圧縮タスク  
gulp.task('cssmin', ['sassのbuildなど'], function () {  
    return gulp.src(['dest/webapp/**/*.css', '!dest/webapp/**/*.min.css'])  
        .pipe(cssmin(  
            {  
                'processImport': false,  
                'advanced': false  
            }  
        ))  
        .pipe(rename({suffix: '.min'}))  
        .pipe(gulp.dest('dest/webapp/'));  
});  
  
// キャッシュクリア準備  
var cachebust = new CacheBuster();  
  
gulp.task('cachebustResources-css', ['cssmin'], function () {  
    return gulp.src('dest/webapp/**/*.css')  
        .pipe(cachebust.resources())  
        .pipe(gulp.dest('dest/webapp/'));  
});  
  
gulp.task('cachebustResources-javascript', function () {  
    return gulp.src('dest/webapp/**/*.js')  
        .pipe(cachebust.resources())  
        .pipe(gulp.dest('dest/webapp/'));  
});  
  
// 圧縮したcssを読み込むようにして、キャッシュクリア対応  
gulp.task('convert-jsp', ['cachebustResources-css', 'cachebustResources-javascript'], function () {  
    return gulp.src('dest/webapp/WEB-INF/view/**/*.jsp')  
        .pipe(replace(/<link(.*)href=(["`]\/[^/].*)\.css/g, '<link$1href=$2.min.css'))  
        .pipe(replace(/<link(.*)href=(["`]\/[^/].*)\.min\.min\.css/g, '<link$1href=$2.min.css'))  
        .pipe(cachebust.references())  
        .pipe(gulp.dest('dest/webapp/WEB-INF/view/'));  
});  

これで、圧縮からキャッシュクリアまでを自動に行うことが可能となります。

また、弊社では、元のcss、圧縮後のcss、圧縮後のキャッシュクリア用のcssの3つが同時に存在した状態になっています。
特に問題もないですし、何かあった際の調査も全てが揃っていた方がやりやすいということからそうしています。
最終形のみでよいということであれば、不要ファイルを削除してしまってもいいと思います。

最後に

以上が、レガシーなフレームワークでcssの圧縮を自動化する方法になります。

javascriptに関しても、専用のnpmを使用し同じように組み込むことはできると思いますが、AngularでDIしている部分のコードが一部動かなくなることがわかっているため、今はストップしています。
こちらはAngularでのDIの書き方を直せば問題なく圧縮できるので、しばらくしたら導入しようと思っています。

尚、シンクロ・フードではエンジニアを大募集しています!
少しでもご興味があれば、気軽にお話しする場を設けますので、以下よりご連絡ください!

シンクロ・フード採用サイト

既に稼働しているWebサービスに対してgulp+webpackでReactをビルドする

こんにちは。エンジニアの大久保です。年の瀬ギリギリにブログを書いてみました。

シンクロ・フードでは、半年くらい前からReactの導入を少しずつやっているのですが、今回はgulpが動作している非SPAサイトに対するReactビルド環境を紹介したいと思います。

「Reactを使ってみたいけれど、ゼロからプロジェクトを作る機会は少ない。だから稼働中のWebサイトに対してReactを使いたい。でもどのような環境にしているのだろう」という方向けに、一つの事例として参考にしていただければと思います。
結論から言うと、gulp+webpack+babelの組み合わせでReactのJSXをビルドし、JavaScriptは複数のエンドポイントに出力する、という方法を取っています。
尚、RailsでReactを使う場合は違う方法をとっているため、今回はあくまでもgulpでのビルドプロセスがある環境、且つSPAではないサイトにReactを導入する場合の事例です。

gulp+webpackでのビルド

弊社では、すでにgulpでのSassビルドを行なっていたため、gulp経由でReactをビルドしたいと思っていました。
gulp経由でReactをビルドする構成としては、以下の2つが候補に挙がるのではないでしょうか。

  • gulp+browserify+babelify
  • gulp+webpack+babel

弊社では両方とも設定してみた結果、設定としてスッキリする、という理由からwebpackを使ったほうに決めました。当初は、gulp→webpack、という、両方とも似たような役割のツールを重ねることが気持ち悪かったのですが、結果としては気にならなくなりました。
ただし、これは既にgulpを使った環境があるからこのような構成にしたのであって、gulpなどのタスクランナーを使っていないならば、webpackを直接使ってビルドすることをおすすめします。

具体的な導入手順

ディレクトリ構成

ディレクトリ構成は以下のような感じ。わかりやすいように単純化しています。

project  
├── src  
│   └── main  
│       └── webapp  
│           ├── es2015-src  
│           ├── es2015-dest  
│           └── 省略...  
└── gulpfile.js  

なんとなくJavaでのWebサイトを作った場合によくあるディレクトリ構成にしてみました。
上記例では、es2015-srcディレクトリにおいたjsファイルが、es2015-destに出力されることになります。
では、手順を紹介してみます。

パッケージのインストール

まずgulp-webpackのインストール。

npm install --save-dev gulp-webpack  

引き続き、babel関連のインストール。

npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react  

更に、vinyl-namedを入れます。これは後ほど説明する、複数エンドポイントにjsを出力するために必要なパッケージです。

npm install --save-dev vinyl-named  

これでビルドに必要なパッケージのインストールは完了です。

gulpfile.js

続いてgulpfile.jsを記載します。

var gulp = require('gulp');  
var webpack = require('gulp-webpack');  
var named = require('vinyl-named');  
  
gulp.task('webpack', function () {  
    return gulp.src('src/main/webapp/es2015-src/**/index.js')  
        .pipe(named(function(file) {  
            return file.relative.replace(/\.[^\.]+$/, '');  
        }))  
        .pipe(webpack({  
            module: {  
                loaders: [  
                    {  
                        test: /\.js?$/,  
                        exclude: /(node_modules)/,  
                        loader: 'babel-loader',  
                        query: {  
                            presets: ['es2015', 'react']  
                        }  
                    }  
                ]  
            }  
        }))  
        .pipe(gulp.dest('src/main/webapp/es2015-dest/'));  
});  

// watchタスク  
gulp.task('watch', function () {  
    gulp.watch('src/main/webapp/es2015-src/**/*.js', ['webpack']);  
});
  
gulp.task('default', ['webpack', 'watch']);  

webpackという関数の中で渡しているオブジェクトがwebpackのオプションです。ここでbabelによるビルドと、presetsを指定することで、JSXをビルドすることができます。
次の項目で説明しますが、上記設定はindex.js、というファイル名のjsファイルだけをビルドするようにしています。それと、複数jsを書き出すための設定が加わっています。

以上で、Reactをビルドするための環境構築は完了です。

gulpfileに関する捕捉

複数jsの書き出しについて

SPAサイトでない場合、ページ毎に読み込むJSが違う、ということはよくあると思います。
弊社もそういったWebサイトを運営しており、webpackでもsrcに入れたjs毎に別々にビルドしたい、というニーズがありました。
jsファイルを追加するたびに、webpackのオプションを修正していけば個別出力は可能なのですが、毎回gulpfileをいじくるのも面倒だったので、src側においたjsの構造をそのままdest側に出力させる、ということをやっています。
そのために、vinyl-namedというパッケージを使って、gulpfileを都度修正することなく、複数jsを出力するようにしています。

ここについては、以下の方のエントリーが非常に参考になりました。
http://2no.hatenablog.com/entry/2015/06/08/170511

なぜindex.jsのみをビルドするか

Reactではimport/exportsを使ったJSを書くことが普通だと思うのですが、*.jsでビルド対象のjsファイルを指定すると、本来ビルド不要な、importされる側のjsも個別にビルドしてしまうため、src側でindex.jsのみをビルドするようにしています。
index.jsの場合はビルド対象、それ以外のファイル名はビルド除外、というルールでやっています。
これは運用でカバーという状態なので、改善が必要だと思っています。

まとめ

以上が弊社のgulpビルドをしている非SPAサイト向けReactビルド環境のご紹介でした。

シンクロ・フードではこのようなフロントエンドに対して興味を持つエンジニアを募集しています(フロントだけではありませんが!)。
興味を持った方がいらっしゃれば、お気軽にご応募ください!それでは!
http://www.synchro-food.co.jp/recruit/

レガシーなフレームワークでcss/javascriptのキャッシュクリアを自動化する

シンクロ・フードでフロントエンドの開発を担当している四之宮です。
今回は、「cssとjavascriptのキャッシュクリアの自動化に対応していないフレームワークでのキャッシュクリア自動化」についてについてお話したいと思います。
ここでいうキャッシュクリアとは、ハッシュ値をファイルに付与することで、強制的にブラウザキャッシュを無効化するキャッシュクリアのことです。

お話する前に、なぜこのような記事を書こうと思ったのかを弊社のシステム構成を交え、ご説明したいと思います。

システム構成

弊社のシステム構成に関しては以前にご紹介した通り、新サービスの開発ではRuby on Rails、既存サービスの開発はSeasar2となっています。

シンクロ・フードのサービスとシステム構成

もちろん既存サービスに関しても、少しずつRuby on Railsへの載せ替えを行っていますが、まだSeasar2で開発しているサービスも多々ある状態です。

キャッシュクリアの対応について

Ruby on Railsで開発していればcssとjavascriptのキャッシュクリアは自動で行ってくれます。しかし、Seasar2で開発したものに関しては、キャッシュクリアを自動では行ってくれません。 そのため、Seasar2で開発しているサービスに関しては、以前までは手動で対応していました。よくあるファイル名の後ろにキャッシュクリア用のパラメータを付与するという手法です。
しかし、この対応を忘れ、キャッシュが効いてしまい変更後のcssが反映されないことがありました。

このように、「本当は新しいフレームワークに載せ替えたいけどできていない。そのため手動でキャッシュクリアの対応をしている。」という方もまだいるのではないでしょうか?
本記事はそんな方向けの内容となっています。

キャッシュクリア自動化の方法

弊社ではgulpによるscssのコンパイルを行っているため、gulp-cachebustというnpmを使用しました。

gulp-cachebust

下記コマンドでインストールを行います。

npm install --save-dev gulp-cachebust  

そして、テスト~本番用のTaskにgulp-cachebustを組み込む形となります。
開発用に組み込まなかったのは、ファイル数が多くなってくると時間がかかるためです。

gulp-cachebustについて

gulp-cachebustには大きく分けてresourcesとreferencesという2つのメソッドがあります。
resourcesは、対象ファイルにハッシュ値を付与する処理を、referencesは対象ファイルを参照しているファイル(cssやjsを読み込んでいるhtmlが記述されたファイル)の中身を変える処理を行います。
この2つのメソッドを使用することで、簡単にキャッシュクリアをしてくれます。

使用例

実際のコードを元に説明したいと思います。

var CacheBuster = require('gulp-cachebust');  
  
var cachebust = new CacheBuster();  
  
gulp.task('cachebustResources-css', ['事前Taskがあれば指定(scssのコンパイルTaskなど)'], function () {  
    return gulp.src('src/main/webapp/**/*.css')  
        .pipe(cachebust.resources())  
        .pipe(gulp.dest('src/main/webapp/'));  
});  
  
gulp.task('cachebustReferences-jsp', ['cachebustResources-css'], function () {  
    return gulp.src('src/main/webapp/WEB-INF/view/**/*.jsp')  
        .pipe(cachebust.references())  
        .pipe(gulp.dest('src/main/webapp/WEB-INF/view/'));  
});  

cachebustResources-css Task

まず、srcにcssが格納されたディレクトリをします。
そこにpipeでつなげてcachebust.references()します。

これだけでファイルの中身から作成された文字列が付与されたファイルが生成されます。
test.cssとtest.de4e3ce6.cssのような状態です。
このファイルの中身は全く同じ内容ですので、二重に出来上がってしまいます。
これが嫌な場合は、destで指定するディレクトリを別にするなどして元ファイルを削除する必要があります。

cachebustReferences-jsp Task

cachebust.resources()の後で実行する必要があります。
上記の例では、cssに対するresources実行後に指定しています。
こちらでは、cssを読み込んでいるファイルをsrcに指定し、pipeでつなげてcachebust.references()します。
これで、例えばtest.cssを読み込んでいる箇所が、test.de4e3ce6.cssと書き換わった状態になります。

使用例ではcssのみですが、JavaScriptファイルについても同様の処理を行うことでキャッシュクリアできます。

注意点

srcに指定したディレクトリ直下(上記の場合src/main/webapp/)に.cssファイルがないようにする必要があります。
これは、直下に存在するcssファイルの名前と同名のcssファイルが別ディレクトリにあると問題が発生するためです。
具体的には、jsp内のcss読み込みの書き換えの際に追加されるハッシュ値が、指定したディレクトリ直下にあるものになってしまうというものです。
その結果、存在しないcssの読み込みをすることになり、cssが効かない状態になってしまいます。

例えば、以下のような状態の場合にこの問題が発生します。

ディレクトリ構成

webapp/  
 ├ stylesheet/  
 │ └ test.css  
 └ test.css  

キャッシュクリア前

  • html1
<link href="/test.css" rel="stylesheet" type="text/css">  
  • html2
<link href="/stylesheet/test.css" rel="stylesheet" type="text/css">  

キャッシュクリア後

  • html1
<link href="/test.de4e3ce6.css" rel="stylesheet" type="text/css">  
  • html2
<!-- 本当ならこちらになって欲しい -->  
<link href="/stylesheet/test.se5teg42.css" rel="stylesheet" type="text/css">  
  
<!-- でもこちらになってしまう。html1で読み込んでいるtest.cssのハッシュ値が付与。 -->  
<link href="/stylesheet/test.de4e3ce6.css" rel="stylesheet" type="text/css">  

最後に

以上が、cssとjavascriptのキャッシュクリアの自動化に対応していないフレームワークでのキャッシュクリア自動化になります。
これだけの実装で、cssとjsのキャッシュクリア漏れによるバグがなくなったので、とてもよかったと思います。
この記事によって、キャッシュクリア機能がないフレームワークを使用していて同じ悩みを持っていた方のお役に立てればと思います。

尚、シンクロ・フードではエンジニアを大募集しています!
最近では、下記の記事ようにフロントエンド側にも力を入れており、ReactやAngular2によるサービス開発もスタートしています。

IonicとAngularJSを使ってSPAでサービスを作った話

少しでもご興味があれば、気軽にお話しする場を設けますので、以下よりご連絡ください!

シンクロ・フード採用サイト

IonicとAngularJSを使ってSPAでサービスを作った話

こんにちは、最近CourseraのMachine Learningコースで機械学習を勉強中の安藤です。
今回は食材発注ツール「PlaceOrders」 の開発についてお話したいと思います。
PlaceOrdersの開発ではIonicを使って初めてSingle Page Application(SPA)を構築したので、その感想などを書いていこうと思います。尚、バックエンドはRubyOnRailsです。

f:id:synchro-food:20160908101609p:plain:w300

Ionicとは?

IonicはAngularJSをベースにしたモバイル向けのCSS/JavaScriptフレームワークです。
Apache Cordova(PhoneGap)を利用してネイティブアプリへビルドすることが基本的な使い方ですが、今回はネイティブアプリへのビルドは考えずにスマートフォン/タブレット向けのWEBサイトの開発に使用しました。
http://ionicframework.com/

なぜIonicを採用したか?

当初はPlaceOrdersをネイティブアプリとして開発することも検討していましたが、SPAであればネイティブアプリのようなユーザー体験を提供しながら、Web開発の延長として効率的に開発を進めることができます。
SPAを開発するにあたって検討したフレームワークはIonicとOnsenUIでしたが、Ionicは公式ドキュメントがしっかりしていて、公式フォーラムなどのコミュニティも活発なので情報を探しやすいため初めてSPAを作成する上で安心感がありました。
パフォーマンスでは少し見劣りするといった情報もありましたが、PlaceOrdersでは特別パフォーマンスを重視する必要はなかったため、その点は問題ないと判断しました。

使ってみて良かったところ

ネイティブアプリのような動作が簡単に実現できた

Ionicのコンポーネントを利用するだけでネイティブアプリのように動作を簡単に実現できたことが、今回Ionicを使ってみて一番良かったところです。
画面遷移のアニメーションやサイドメニュー、モーダル、ポップアップなどのコンポーネントを様々な端末、ブラウザに対応して作ろうとするとかなりの労力になりますが、Ionicのコンポーネントはほとんど問題が発生することもなく非常に使いやすかったです。
例えば、ポップアップのコンポーネントでは以下のようにタイトルやテキスト、ボタン名、各ボタンに紐付ける処理などを決まった形で書くだけで利用できます。

var myPopup = $ionicPopup.confirm({
  title: 'myPopup',
  templateUrl: 'myPopup.html',
  buttons: [
    {
      text: 'キャンセル',
      type: 'button-stable',
      onTap: function(e) {
        // キャンセル時の処理
      }
    },
    {
      text: 'OK',
      type: 'button-positive',
      onTap: function(e) {
        // OK時の処理
      }
    }
  ]
});
// ポップアップの表示
myPopup.show();

f:id:synchro-food:20160908101433p:plain:w300

AngularJSベースなので開発しやすかった

IonicはAnglarJSをベースにしているので、通常のAngularJSアプリと同様に開発を進めることができました。
ルーティングには定番モジュールのUI Routerが使用されているため、Nested Viewsでコントローラーやビューの共通化したり、resolveで画面遷移を制御するといった柔軟なルーティングが可能でした。

苦労したところ

大きくハマってしまうことはなかったですが、細かいところで気になるポイントがいくつかありました。

コンテンツサイズ変更時のリサイズ

リストの伸び縮みなどでコンテンツサイズが変化した時に、画面の端までスクロールできなくなることがありました。
サイズが変わるような処理の度に$ionicScrollDelegate.resize()が呼び出されるようにして、強制的にサイズを再計算させることで解決しました。

日本語入力時の挙動

日本語入力時に入力を確定するまでモデルに値が反映されなかったり、確定前に他のテキストボックスにフォーカスを移すと、入力中の文字がフォーカスしたテキストボックスにコピーされてしまうといった現象が一部端末、ブラウザで発生しました。
現在もまだ一部問題が残っていますが、compositionstartというイベントでモデルの更新を止めてしまっている処理を無効化するようなディレクティブを作成したり、data-tap-disabledを付けることで対応しました。
前者の問題については以下の記事が詳しいです。
http://pirosikick.hateblo.jp/entry/2014/10/16/232409

touchstart/touchendイベント

Ionic、AngularJSのどちらにもtouchstart/touchendイベントに対応したディレクティブが実装されていなかっため、独自にディレクティブを作成する必要がありました。
特に難しいものではないですが、なぜ用意されていないのか不思議でした。

最後に

以上、IonicによるSPA開発の所感でした。
初めてのSPA開発でも順調に開発を進めることができたので結果的にはIonicを採用して良かったと思います。

最近ではIonicはAngular2をベースにしたIonic2に移行しつつあるので、機会があればIonic2にも挑戦してみたいと思っています。

尚、シンクロ・フードでは、こういったフロントエンド開発に興味のあるエンジニアを大募集しています!
少しでもご興味があれば、気軽にお話しする場を設けますので、以下よりご連絡ください!

キャリア採用 | シンクロ・フード採用サイト

Tomcatのクラスタリング環境でのセッションレプリケーションについて

シンクロ・フードの越森です。
今回は、Tomcatのクラスタリング環境でのセッションレプリケーションについてお話したいと思います。

弊社ではAWS移行したことをきっかけにTomcatのセッションレプリケーションを見直すことになったのですが、中々これといったやり方を決めることができず試行錯誤したため、皆さんの参考になればと思い公開します。
※現段階でも、運用でカバーしている問題は残っていますが...。

何故セッションレプリケーションが必要なのか?

セッションレプリケーションとはTomcatなどのアプリケーションサーバーを複数台並列で稼働させるクラスタリング環境において、各アプリケーションサーバー上のセッションを共有する方法です。

セッションを共有することで、アプリケーションサーバーが1台落ちた場合でもユーザーには影響を与えず、別のアプリケーションサーバーでサイトを運用することができます。

Tomcatのセッションレプリケーションの種類

Tomcatは3種類のセッションレプリケーションの手段を提供しています。

  • DeltaManager
  • BackupManager
  • PersistenceManager

DeltaManager

基本的にマルチキャストで構築されたクラスタリング環境において、セッションレプリケーションするために利用するSessionManagerです。
Tomcatの公式ドキュメントのはじめに説明されている代表的なSessionManagerです。
クラスタリング環境内の全てのサーバーでセッション情報をほぼリアルタイムで共有します。
クラスタリング環境内の全サーバーでセッション情報を共有するため、セッション情報の変更を反映させるための負荷が高いこと、1台で全台数分のセッション情報を持つことによりメモリ使用量が大きくなることから、小規模なクラスタリング環境向けのセッションレプリケーション方法です。

BackupManager

DeletaManagerと同様、マルチキャストで構築されたクラスタリング環境において、セッションレプリケーションするために利用するSessionManagerです。
DeltaManagerとは異なり、リクエストを受け付けているサーバー(Primary)と、クラスタリング環境内にある別の1台のサーバー(Backup)との2台でしかセッション情報を格納しないため、大規模なクラスタリング環境でも利用することができるセッションレプリケーション方法です。

PersistenceManager

Tomcatが持つセッション情報をファイルやデータベースに格納して利用するSessionManagerです。
各Tomcatのサーバーにセッション情報の格納先を設定することでクラスタリング環境を構築することができます。(マルチキャストでグルーピングする必要はありません)
PersistenceManagerではTomcatのメモリ上にセッション情報があれば優先的に利用し、メモリ上にない場合にセッション情報の格納先から取得します。
そのため、Tomcatの前に配置するロードバランサーではスティッキーセッションを付加する必要があります。

注意する点としては、セッション情報の格納先へ反映するタイミングと反映するデータ量です。
反映するタイミングについては、設定で変更はできますが一番短いタイミングでも格納先に反映するのは1秒ごとになるという点です。
反映するデータ量については、反映するタイミングでTomcatが持っている全てのセッション情報を格納先に反映することになりますので、セッション情報に大きな情報を入れているとネットワーク負荷、CPU負荷、I/O負荷が大きくなってしまいます。

オンプレミス時代のセッションレプリケーション

ここからは、シンクロ・フードで利用しているセッションレプリケーションについてお話しします。
初めてセッションレプリケーションを導入したのはオンプレミス時代で、この時はTomcatの公式ドキュメントで紹介されている最初の方法を採用しました。
クラスタリングはマルチキャストで構成し、セッションレプリケーションについては、DeltaManagerによりすべてのノードのセッションをリアルタイムに共有するように設定しました。
設定方法についても公式ドキュメントの設定通りに行うことでセッションレプリケーションを実現できました。

[Apache Tomcat 6.0 Clustering/Session Replication HOW-TO]
https://tomcat.apache.org/tomcat-6.0-doc/cluster-howto.html

AWS移行直後のセッションレプリケーション

2014年からAWSへの移行を検討し始めましたが、検討の過程でAWS環境ではマルチキャストが使えないということが分かり、クラスタリング/セッションレプリケーションの方法についても見直しを行いました。
このタイミングで他の方法に変更することも検討しましたが、簡単には実現することができず調査に時間がかかりそうなことから、他作業との優先度を考えて、マルチキャスト設定部分をユニキャストで1台ずつ設定することで、オンプレミス時代と同じセッションレプリケーション方法で対応しました。

具体的にはマルチキャスト設定を行っているMembershipの記述を削除して、以下の設定を追加しました。

<Interceptor className="org.apache.catalina.tribes.group.interceptors.StaticMembershipInterceptor">  
<Member className="org.apache.catalina.tribes.membership.StaticMember"  
port="5000"  
securePort="-1"  
host="172.16.1.100"  
domain="product-cluster"  
uniqueId="{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}"  
/>  
</Interceptor>  

[Apache Tomcat 7.0 Clustering/Session Replication HOW-TO]
https://tomcat.apache.org/tomcat-7.0-doc/config/cluster-interceptor.html

自動デプロイ対応後のセッションレプリケーション

AWS移行が終わりインフラの次に取り組むテーマを検討した結果、AWSに移行したことで自動化周りの施策が色々できるようになったので、自動デプロイに取り組むことになりました。
自動化という方向性で進めるとした場合に現在のユニキャストによるサーバー1台ごとの設定は、今後障害になるということでセッションレプリケーションの方法についてもあわせて変更することにしました。
実はそれ以外にもデプロイで1台ずつサーバーを落とすタイミングでセッションが維持できない場合が発生していたというのも理由の1つであったりします。

色々調査したところ、Amazonが公開しているライブラリを使ったDynamoDBによるセッションレプリケーションやRedisを使ったセッションレプリケーションなどがありましたが、やはりTomcatに含まれている点を重視してPersistenceManagerを採用することにしました。

シンクロ・フードのPersistenceManagerによるセッションレプリケーション

具体的にはEC2インスタンス上のMySQLにセッション格納用のテーブルを作成し、Tomcatからそのテーブルに対して読み書きするように設定しています。
セッション格納用のテーブルについては読み書きの頻度が高くI/Oの負荷を考慮して、MEMORYストレージエンジンを利用して、メモリ上にセッション情報を格納しています。
以下はMySQLとTomcatの設定になります。

  • MySQLのテーブル定義
CREATE DATABASE tomcat_session DEFAULT CHARACTER SET utf8;  
  
USE tomcat_session;  
  
CREATE TABLE sessions (  
  session_id varchar(100) NOT NULL,  
  valid_session char(1) NOT NULL,  
  max_inactive int(11) NOT NULL,  
  last_access bigint(20) NOT NULL,  
  app_name varchar(255) DEFAULT NULL,  
  session_data varbinary(42000),  
  PRIMARY KEY (session_id),  
  KEY kapp_name (app_name)  
) ENGINE=MEMORY DEFAULT CHARSET=utf8;  
  • Tomcatの設定(context.xml)
<Manager className="org.apache.catalina.session.PersistentManager"  
     saveOnRestart="true"  
     processExpiresFrequency="1"  
     maxIdleBackup="0">  
     <Store className="org.apache.catalina.session.JDBCStore"  
         connectionURL="jdbc:mysql://[IPアドレス]:3306/tomcat_session?useUnicode=true&amp;characterEncoding=UTF-8&amp;user=[ユーザー名]&amp;password=[パスワード]"  
         driverName="com.mysql.jdbc.Driver"  
         sessionAppCol="app_name"  
         sessionDataCol="session_data"  
         sessionIdCol="session_id"  
         sessionLastAccessedCol="last_access"  
         sessionMaxInactiveCol="max_inactive"  
         sessionTable="sessions"  
         sessionValidCol="valid_session" />  
</Manager>  

現在はPersistenceManagerを利用した方法で運用はできていますが、1点だけ問題が発生するケースがあるため、その問題について説明します。

PersistenceMangerの運用上の問題

Tomcatのフェイルオーバーやロードバランサーの振り分け変更などにより、同じユーザーのセッションが複数台のTomcatのメモリ上に存在する場合に、Tomcatの再起動やセッションのタイムアウトなどでメモリのセッション情報がクリアされずに、フェイルオーバーなどでもう1度別のTomcatのサーバーにアクセスすることになった場合に、セッション情報が過去の情報に戻ってしまうという問題が発生します。

本問題については、谷本さんの以下の記事が詳しいのでご確認ください。
[DynamoDBでTomcatのセッション共有をするとハマるかも]
http://d.hatena.ne.jp/cero-t/20151019/1445262219

実際に問題が発生する具体的なオペレーションのケースとしては、以下の2つが考えられます。
ここでは、Tomcat1, Tomcat2の2台構成で運用していて、その前にロードバランサー(LB)があることを前提とします。

  1. LBでTomcat1へのアクセス遮断、復旧、遮断
  2. Tomcat2ダウンによるフェイルオーバーが2回連続で発生

弊社ではどちらのケースにも対応できるように別サーバーへのアクセスが発生するような状況になった場合には、全てのサーバーのTomcatを再起動してメモリ上のセッション情報をクリアするようにしています。

最後に

以上で簡単な説明になりますが、弊社でのTomcatのクラスタリング環境でのセッションレプリケーションの設定をご紹介しました。
Tomcatのセッションレプリケーションについて少しでも参考になれば幸いです。

尚、シンクロ・フードではエンジニアを大募集しています!
こうしたインフラ周りの改善以外にも色々なことをやっていますので、少しでもご興味があれば、気軽にお話しする場を設けますので、以下よりご連絡ください!

キャリア採用 | シンクロ・フード採用サイト

シンクロ・フードのブランチ運用フロー

シンクロ・フードの五十嵐です。
今回は、私たちの開発フローについてお話したいと思います。
特にGitのブランチ運用については皆さん頭を悩ませる部分だと思いますので、弊社の紆余曲折した経緯も踏まえて公開します。
Gitへの移行を検討している方などの参考になれば嬉しいです。

リリース頻度

お話の前提として、弊社のリリース頻度を紹介します。
平日は、ほぼ毎日リリースをしています。一日数回リリースする日もあります。
月間で捌くチケット数(≒featureブランチ数)の平均は約170です。
それなりの規模になってきたため、開発フローの安定化や効率化は全体の生産性に直結すると考えており、現在も改良し続けています。

GitHub Enterprise の利用

バージョン管理はGitで行っており、GitHub Enterpriseを利用し、プルリクベースで開発を行っています。
全てのプルリクは必ずコードレビューを受けてからマージされます。レビュアーは基本はランダムに決めています。
また、毎日の朝会でレビューの状況を可視化しており、レビューが依頼されたまま放置されることを防いだり、レビューの負担が偏った場合に調整して分散できるようにしています。

Enterpriseにした理由としては、弊社はISMSを取得していて、情報セキュリティマネジメントの観点から、機密性を重視しました。
可用性という面でも、今はEnterpriseのほうが安定しているかもしれません。導入後1年以上が経ちますが、発生した障害はゼロです。

ブランチ運用

本題のブランチ運用についてです。
現在はGitHub Flowを参考にしつつも、独自にアレンジした運用をしています。

f:id:synchro-food:20160318183348p:plain

  • masterブランチは常にデプロイ可能、本番環境と同一の状態
  • developブランチはmaster+テスト中のブランチが取り込まれた状態
  • featureブランチはチケットごとにmasterブランチから作成される
  • featureブランチからdevelopにプルリクエストが作成され、コードレビューを受ける
  • コードレビューがOKならdevelopにマージされる

しかし、この状態に落ち着くまでには、紆余曲折がありました。

Git導入以前

f:id:synchro-food:20160318183611p:plain

多くの企業が同様だと思うのですが、弊社はGit導入以前はSubversion(以下SVN)を利用してバージョン管理を行っていました。
SVN時代は、release - trunk のブランチ構成で、開発者はtrunkにガンガンcommitしていました。
ステージング環境および本番環境にデプロイする際には、releaseにチェリーピックで反映し、releaseブランチをそのままデプロイしていました。

これはこれで数年に及ぶ運用実績のあるフローでしたが、下記のような問題が出てきて、2014年の7月頃から、真剣にGitへの移行を検討することになりました。

  1. 開発者が増えてきて開発環境での競合が増えてきたこと(チケットごとにブランチを分けたい)
  2. コードレビューの工数が増えてきたこと(プルリクを活用してレビューを楽にしたい)
  3. 中途採用の面接時などにバージョン管理ソフトを聞かれるようになったこと(採用戦略としてGit使ってますと言いたい)

Git導入に向けての検討

上記の課題解決のためにGit導入を決意しました。
SVN -> Git へのリポジトリ変換も当時すでに複数の事例や手順の紹介があり、大きな問題にはなりませんでした。
デプロイにはJenkinsを利用していましたが、こちらもGit用のプラグインが存在したため、問題ありません。

弊社が頭を悩ませたのはやはりブランチ運用でした。
まず、弊社の開発フローでは、環境が4つに分かれています。
本番環境、本番デプロイ前の検証を行うステージング環境、企画責任者(非エンジニア)が受け入れテストを行うためのテスト環境、そして各開発者の開発環境です。
SVN時代は本番環境とステージング環境がreleaseブランチ、テスト環境と開発環境がtrunkです。
これを、release -> masterブランチ、trunk -> developブランチにそれぞれ変更すれば、特に大きな変更点なく移行ができると考えていました。
masterブランチからチケットごとにfeatureブランチを作成し、developにマージし、受け入れテストがOKになればmasterにマージするというフローです。

しかし致命的なことに、SVN時代のmasterとtrunkの二大ブランチは根が全く異なっていました。つまり、親子関係がなかったのです。
実際にGit移行のリハーサルでマージを試みると大量の競合が発生してしまい、とても解決しきれない程でした。
これでは、featureブランチをmasterから切った場合にdevelopブランチへ自動マージするのが困難…というよりも無理だということが分かりました。

考えられる手段としては、移行コストと割り切ってmaster(旧release)とdevelop(旧trunk)のマージ作業をしてしまうことですが、弊社は当時8サイトの運営をしており、ライブラリなども含めて移行対象となるリポジトリは10を超えていました。
多数のリポジトリで正しく競合を解決してマージした上に、全機能の動作確認を行うとなると、非常に大きな工数がかかることが予想されます。しかも、その動作確認中にも新しい機能開発はどんどん進んでいきます。

そこで、なんとかmasterとdevelopが断絶したまま運用できないかを考えました(今思えば、この発想が間違いだと思われるのですが…)。
まず当初の想定通りにmasterからfeatureブランチを作成した場合、masterへのマージ(≒本番デプロイ)は簡単に行えますが、developへのマージ(≒受け入れテストの開始)が難しくなります。
また、コードレビューは受け入れテスト前、つまりdevelopブランチへのマージ時に行いたいです。当然のことながら、developに向けたプルリクエストの差分は、そのブランチに関係のある変更だけが出てこないと困ります。
以上のことから、featureブランチはdevelopブランチから作成することが妥当と考えられました。

次なる課題は、developブランチから作成されたfeatureブランチから、どのようにmasterブランチに反映するかというものです。
調査を進めると、Gitにもcherry-pickコマンドが存在することが分かりました。
cherry-pickコマンドを利用すれば、履歴の断絶しているmasterへの反映も行えそうです。実質SVN時代とほぼ差のない開発フローが作れそうです。実際に試してみたところ、上手く運用ができそうでした。

以上の経緯から、移行直後の開発フローは以下のように固まりました。

  1. developブランチからfeatureブランチを作成する
  2. featureブランチからdevelopブランチにプルリクエストを作成する
  3. コードレビュー後にプルリクエストをマージする
  4. 受け入れテスト後にmasterにfeatureブランチのコミットをcherry-pickする
  5. ステージング環境でテストする
  6. 本番環境にデプロイする

折衷案のようなブランチ運用ですが、Git移行の目的であった以下3点は満たすことができていました。

  1. チケットごとにブランチを分けたい
  2. プルリクを活用してレビューを楽にしたい
  3. 採用戦略としてGit使ってますと言いたい

cherry-pick運用の廃止

運用が2~3ヵ月過ぎたころ、次第に無視できないトラブルが発生するようになってきました。
それは、masterへのcherry-pick時の競合解決の失敗や、cherry-pickすべきコミットが多い場合に漏れが出てしまう、更にはdevelopには存在するがmasterには存在しないクラスへの依存に気付けない等のトラブルです。
上記のようなトラブルは、当然ながらエラーが発生したり、バグの温床になったりと、サイトに致命的なダメージを与えてしまいます。

こうしたトラブルには都度、解決策を考えて実施してきました。
例えば1番目の問題については競合解決用のブランチをmasterから作成してレビューを行うようにしたり、2番目の問題についてはdevelopへのマージコミットをcherry-pickすることで漏れを無くす、等の対応を行いました。

しかしながら上記対応は対症療法でしかなく、やはり根本的な問題になっているのは、featureブランチからmasterブランチに直接マージできないことです。
本来なら緊急時に使うべきであろうcherry-pickコマンドを日々の運用で利用してしまっているという状態の気持ち悪さもあり、masterブランチとdevelopブランチの履歴を統一する必要があるという判断に至りました。

そこで弊社は、現在のdevelopブランチを破棄し、新しくmasterブランチからnew-developブランチを作成することで履歴を統一することにしました。
Git移行時にこの判断を見送った経緯としては、masterとdevelopのマージ作業は現実的ではないことが理由でしたが、それはこの時も変わりませんでした。
しかしGit移行後の現在、開発中の内容は全て各featureブランチに存在しています。
移行前はmasterには存在しないがdevelopにのみ存在する開発中の内容を救う手段がマージ作業以外に思い浮かびませんでしたが、各featureブランチが存在する現在は、その内容を拾うのは、それこそcherry-pickコマンドで容易になっています。

そこで、新しくmasterブランチから作成されたnew-developブランチに、開発中のfeatureブランチの内容をcherry-pickで反映していくという作業を行って、新しいブランチ運用を開始しました。旧developブランチは直後に削除され、new-developブランチがdevelopブランチへとリネームされています。
その後、masterブランチへのfeatureブランチのマージもプルリクエスト上で行うことにより、マージ漏れや競合の解消ミスなどもレビューで気付けるようになりました。

現在の開発フロー

上記のような紆余曲折を経て、現在の開発フローに落ち着きました。

  1. 開発ブランチで各開発者がローカルで開発を行う
  2. ローカルでの開発が完了したらdevelopブランチへのプルリクエストを出し、レビュアーがレビューを行う
  3. レビューが完了したらdevelopブランチにマージされ、受け入れテストが行われる
  4. テストが完了したら開発ブランチがmasterブランチにマージされ、ステージング環境でテストが行われる
  5. 問題なければmasterブランチを本番環境へデプロイする
  6. masterブランチからdevelopブランチにマージが行われる(Jenkinsで自動化)

GitHub Flowとの差異としては、masterに取り込む前にdevelopブランチを挟んでいます。
すでに軽く触れていますが、企画責任者(非エンジニア)が受け入れテストを行うための環境です。
非エンジニアなので手元に開発ブランチを落としてきて動作確認するということができないため、テスト用の環境を用意しています。
また、開発ブランチ同士の競合などがdevelopブランチで検知できるという利点もあります。
そのほか、今回は詳しく触れませんが、developブランチが反映されるテスト環境では、Seleniumを用いたE2Eテストが定期実行されており、サイトの品質を担保してくれています。

また、最後のmasterブランチからdevelopブランチへのマージは、現在も正解なのかどうか自信がない部分です…。
取り入れた理由としては、developブランチへのプルリクエストで余計な差分が発生するのを防ぐためです。
masterブランチからdevelopブランチへのマージを行わないと、masterブランチには存在する本番反映のためにマージコミットが、developブランチには存在しないという理由で、関係ないファイルが差分に挙がってきてしまうという現象が発生していました。
Git Flowでもreleaseブランチからdevelopブランチにマージする手順があるので、大きくは間違っていないと思うのですが、どうでしょうか。

なお、releaseブランチを作成しない理由としては、弊社のリリース頻度が多いため、リリース作業の負荷を大きくしたくないためです。
この辺りは各種ツールやデプロイ方法の工夫で乗り切れる部分かもしれません。

最後に

以上、弊社のGitブランチ運用の経緯と現在の開発フローをご紹介しました。
色々なトラブルは発生しましたし紆余曲折もありましたが、現在はGit/GitHubの恩恵を受けながら、かなり快適に開発やコードレビューが行えるようになっています。

尚、シンクロ・フードではエンジニアを大募集しています!
こうした開発フロー改善以外にも色々なことをやっていますので、少しでもご興味があれば、気軽にお話しする場を設けますので、以下よりご連絡ください!

キャリア採用 | シンクロ・フード採用サイト

Webサイトの文字コードをShift_JISからUTF-8に変更するときに実施したこと

こんにちは、シンクロ・フードの大久保です。
10年以上運用しているWebサービスだと、文字コードがShift_JIS、という状況は多いのではないでしょうか。
弊社もそうだったのですが、昨年、自社で運用するWebサイトすべての文字コードをShift_JISからUTF-8に変更しました。
今回はこの対応について実施したことをご紹介したいと思います。

前提条件

WebサーバはApache、WebアプリケーションサーバはTomcat、DBサーバはMySQL5.6を利用しています。
Webサイトは全部で6サイト、すべてJavaで書かれたWebアプリケーションで、その中のメインである飲食店.COMのJavaファイル数は約2000ファイル、cssやjs、htmlファイルなどを含めると約10000ファイルくらいです。Webアプリケーションですとそこそこ大きい部類だと思います。

なぜUTF-8にするのか

Shift_JISのままで良いのでは?という意見があるのかもしれませんので、念のため弊社がUTF-8化した代表的な理由を挙げておきます。

  1. 入力できる文字の増加(㈱、㈲、①など)
  2. UTF-8で書かれたJavaScriptとの連動がスムーズになる
  3. nodejs系フロントエンドツールがShift_JISに対応していないことがある

1は昔からある問題なので、今回変更した理由の動機としては薄いです。
2はJavaScriptのMV◯◯系フレームワーク(AngularJSやKnockoutJS等)を使う度に悩まされている問題で、JavaScriptを使ったリッチな動きのサービスを作る機会が増えたため、避けて通れない問題となっていました。
3は、GruntやGulpで実行するようなプラグインが、Shift_JISだと動かないことがあるという問題で、そもそもそんな制約に縛られるのが嫌だ、と開発者のストレスが溜まっていました。

一言で言うと、モダンなフロントエンド開発を進めていくたびに、「UTF-8だったら楽だなあ…」と思うことが増えてきたため、対応に踏み切ったというわけです。

実施したこと

文字コードを変更するにあたって実施したことを以下に挙げます。
実際にはこれ以外にも細かい対応がありましたが、大きくは以下の対応です。

プログラムファイルそのものの文字コードを変更する

ファイルの文字コード変更です。エディタ等で一気に変えることができるので、簡単です。
弊社では、Linuxのnkfコマンドを使って一気に変換をしました。

プログラムファイル中に記載されている文字コードを変更する

ファイル内に登場する、Shift_JISやsjisなどといった文字コード指定を、UTF-8に修正します。
こちらもエディタ等で一気に置換をすることができます。
弊社では、Linuxのsedコマンドを使って一気に置換しました。

メールの文字コードをiso-2022-jpからutf8に変更する

サイトをUTF-8にするので、こちらも一緒に対応しました。
弊社のサービスは年配の方やWebに疎い方が使うことが多いため、古いメールクライアントを使っている可能性があり、少し不安なポイントでした。結果的には、文字化けする、という問い合わせは数件のみでした。

DBデータの文字コードを変更する

alter文を使って、DBのデータを変換します。
弊社ではMySQLを使っているため、alter文はinformation_schemaから一気に自動生成するスクリプトを書いて生成しました。
実際に流してみると、変換に2時間くらい必要で、ここはメンテナンスとしてサイトを停止することで対応しました。
データ量が大きいサービスを運営している方は、ここが一番のネックになるかもしれません。

各種サーバ設定の文字コードを変更する

ここは普通に変更するだけです。特に問題はありませんでした。

特別な対応をしたこと

上記の対応とは別に、特別な対応をしたことが2点あります。

  • 検索エンジンにインデックスされている、Shift_JISでURLエンコードされたパラメータをどうするか
  • 開発中コードとの衝突問題

1つずつ説明いたします。

検索エンジンにインデックスされている、Shift_JISでURLエンコードされたパラメータをどうするか

フリーワード検索の結果などで、日本語の文字列を含むパラメータが、検索エンジンにインデックスされていることがあると思うのですが、その文字列はShift_JISでURLエンコードされています。Shift_JISでサイトを運営していれば当然だと思います。
その状態で、WebサイトをUTF-8に変更すると、検索エンジンからはShift_JISでURLエンコードされたパラメータが飛んでくるにも関わらず、UTF-8でURLデコードすることになってしまい、文字化けが発生してしまいます。
リンク元である検索エンジンにインデックスされているURLを変更できれば良いのですが、それはできません。

対応策

まず思いつく対応策は、URLエンコードされた文字列から、文字コードを逆引きする、という方法です。
これができれば、文字コードに応じてURLデコードするだけなので簡単です。
ですが、URLエンコードされた文字列から文字コードを判定するは困難である、と結論付け、この方向はやめました。
今思うと、この方向性を進めることもアリだったのでは?と思っています…。
[参考] http://clikington-saito.com/UrlEncode/UrlEncode.html

そこで僕たちが取った手法は、判定ができないのであれば、パラメータ名を分ければいい!という、かなり強引な手法です。
まず日本語がやりとりされるであろうパラメータを抽出し、パラメータ名を別名に変えます。具体的にはパラメータ名に_uを付与しました。
そして、旧名(_uが付かないパラメータ)の場合はShift_JISでURLデコードし、_uを付けて301リダイレクトを実施。
新名(_uが付いているパラメータ)の場合はUTF-8でURLデコードしました。

これで問題は解決するのですが、この別名変換処理は、UTF-8への変更を反映すると同時に有効にしなければならないため、事前にコード内に処理を埋め込みつつ、設定値によって無効化しておき、UTF-8への変更と同時に有効化する、という方法も行なっています。

文字コード変更全体の中で、工数を一番かけたのがココです。文字化けを受容する選択肢もあったのですが、やはり検索エンジンからのアクセスは少しでも無駄にしたくなかったため、丁寧に実施しました。概ね成功したと思います(一部文字化けは発生してしまいましたが…)。

開発中コードとの衝突問題

弊社は、バージョン管理にgitを使っており、常時10名以上のエンジニアがブランチを切って開発を繰り返しています。
もし、文字コードを変えるのであれば、以下のような手順になります。

  1. gitのmasterブランチから作業用ブランチを切り
  2. 文字コード変更処理を加え
  3. テストを実施
  4. masterへの取り込み

この手順の中で、時間がかかるのは3番目のテストです。テストをしている間に、他の開発者がmasterからブランチを切って開発を実施し、先にmasterに取り込まれてしまうと、文字コード変更のための修正がmasterに取り込まれる時点で確実に競合が発生してしまいます。

対応策

これを解決する方法としては、リリース直前まで、gitのブランチを切らない、ということで対応することにしました。

まず、gitのmasterからブランチを切ることと、文字コード変更処理をすること、この2つを一気に実施するシェルスクリプトを書きます。
テストをする際は、毎回このスクリプトを使うことで、現時点の最新状態をUTF-8にしてからテストを行い、毎回破棄していました。
そしてデプロイ前日のみ、開発者にはブランチを切ることを一時停止してもらい、上記スクリプトを実行、masterへマージ、本番デプロイ、という作業を行ないました。

尚、この状態だと、開発者の手元にShift_JISで開発中のブランチが残ってしまうのですが、こちらについては、手元のブランチをUTF-8化するスクリプトを用意し、こちらを個別に実行してもらうことにしました。

インターンを含めると10名以上が同時に開発しているリポジトリですが、この方法を用いることで、最小限の工数でプログラムの文字コードを変更することができました。

文字コード変更後の感想、反省など

その他、実際にやってみて起こった出来事などを書いておきます。

MySQLの文字コードは、utf8mb4を使うべきだった

はじめはMySQLの文字コードはutf8にしていたのですが、サービスを運用している中で、登録できない文字がある…ということに気づきました(𠮟、など)。
これは4バイトの文字で、こういった文字を格納するには、utf8mb4という文字コードにしなければならない、ということを後で知りました。
ですので、この変換のためにもう一度深夜にメンテナンスを実施しています…。

MySQLの設定値、group_concat_max_lenを伸ばす必要があった

Shift_JIS->utf8mb4ということで、MySQL内で格納されるバイト数が2バイトから最大4バイトまで増えています。
この変更で見落としていたのが、group_concat_max_lenという設定値…。
場所は少ないのですが、group_concat関数を使っている部分があり、この結合結果文字数が、デフォルトは1024バイトまでしか保持できず、文字コード変更後はこれを超えてしまい、エラーになってしまいました(厳密にいうと、group_concatの結果が切断されます)。
こちらは、group_concat_max_lenを1024から2048に変更することで対応しました。

[mysqld]
group_concat_max_len=1024 // これを2048に変更する

最後に

以上、かなり大雑把な説明ですが、弊社が行なったWebサイトをUTF-8にする際に実施したことをご紹介しました。
工数はかかりましたが、移行後は文字コード関連のトラブルはほぼ発生しなくなり、かなり快適に開発することができています。
対応自体は一部かなりの力技になってしまいスマートとは言えませんが、Shift_JISのWebサイトをUTF-8化する際の参考になれば幸いです。

尚、シンクロ・フードではエンジニアを大募集しています!
こういったレガシー環境をモダン化する対応以外にも色々なことをやっていますので、少しでもご興味があれば、気軽にお話しする場を設けますので、以下よりご連絡ください!

キャリア採用 | シンクロ・フード採用サイト