astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

複数サイトを運営する上でのフロントエンドのちょっとしたノウハウ

こんにちは。 デザイン部でフロントエンドエンジニアをしているkitoです。
弊社は、転職ナビというドメインの異なるサイトを300サイト以上運営しています。レアケースであるとは思いますが、運営上のノウハウの一部をご紹介したいと思います。
今回はスタイルシートを量産するフローを書かせて頂きます。

jsonファイルから複数のcssを作成する

転職ナビは数百サイトありますが、ひとつのアプリケーションでほぼ同じテンプレートが使われ、静的なアセットは原則として共有されています。
しかし、それぞれのサイトに固有のスタイルシートを適用しなければならない場面がどうしてもあります。 弊社では、サイト固有のスタイルシートをjsonファイルのデータから量産することでその問題に対処していますが、そのjsonの元ファイルはコミュニケーション上使い勝手のよいcsvファイルでデータが管理されています。 従って、まずcsvファイルをjsonに変換します。

下記のようなcsvファイル、prefecture.csvがあったとします。これをprefecture.jsonに変換します。
domainは文字通りサイトドメインで、R,G,BはそれぞれサイトカラーのRGBの値です。

domain R G B name
hokkaido 0 11 111 北海道
aomori 32 66 66 青森
iwate 4 15 43 岩手
miyagi 55 180 8 宮城
akita 10 20 30 秋田
yamagata 90 0 0 山形
fukushima 10 10 10 福島
ibaraki 40 30 12 茨城
tochigi 68 11 54 栃木
gunma 250 100 40 群馬

csvtojsonというモジュールを使いたいので下記コマンドでインストールしてください。

npm install --save csvtojson

以下作成していくディレクトリは以下のようになります。

├── package.json
├── converter.js
├── template.js
├── prefecture.json
├── Gruntfile.js
├── dev
│   └── unique
│        ├── akita
│        │   └── styles
│        │        ├── sp_unique.scss
│        │        └── unique.scss
│        ├── aomori
│        │   └── styles
│        │        ├── sp_unique.scss
│        │        └── unique.scss
│        以下略
├── tasks
│   └── prefecture_styles.js
├── unique
│   ├── akita
│   │   └── styles
│   │        ├── sp_unique.css
│   │        └── unique.css
│   ├── aomori
│   │   └── styles
│   │        ├── sp_unique.css
│   │        └── unique.css
│   以下略
└── node_modules

converter.jsを作成して下記のコードを実行してください。csvファイルをjsonに変換します。

var fs = require('fs');
var csv = require("csvtojson");
var converter = csv({});
var csvFile = './prefecture.csv';
var jsonFile = './prefecture.json';

fs.createReadStream(csvFile).pipe(converter);
converter.on("end_parsed", function (jsonArray) { 
  fs.writeFile( jsonFile, JSON.stringify( jsonArray, null, '    ' ),'utf8');
  console.log('csv to json is complete !');
});

下記のようなprefecture.jsonが作成されたと思います。

[
    {
        "domain": "hokkaido",
        "R": 0,
        "G": 11,
        "B": 111,
        "name": "北海道"
    },
    {
        "domain": "aomori",
        "R": 32,
        "G": 66,
        "B": 66,
        "name": "青森"
    },
    以下略
]

弊社はsassを導入しているのでprefecture.jsonからgruntを通じてsassを作成し、それを元にcssを作成するフローになります。
二度手間のように感じるかもしれませんが、他のsassをimportしたり、ユニークなカラーを変数に設定できたりと、使い勝手がよいからです。

sassを量産するprefecture_styles.jsというGruntプラグインを作成します。
tasksディレクトリに、prefecture_styles.jsを作成して下記コードを記入してください。prefecture.jsonと後で作成するテンプレートを元にsassを量産します。

module.exports = function(grunt) {
    var fs = require('fs');
    var json = grunt.config('prefecture_styles').json;
    var data = JSON.parse(fs.readFileSync(json, 'utf8'));

    grunt.registerTask('prefecture_styles', 'task', function() {

        var dir = grunt.config('prefecture_styles').dir;
        var stylesDir = grunt.config('prefecture_styles').stylesDir;
        var name = grunt.config('prefecture_styles').name;
        var template = grunt.config('prefecture_styles').template;
        var sasstmp = require(template);

        Object.keys(data).forEach(function(key, index) {
            var domain = data[key]["domain"]
            var R = data[key]["R"]
            var G = data[key]["G"]
            var B = data[key]["B"]
            var sassObj = new sasstmp(R,G,B);

            name.forEach(function(val, index) {
                grunt.file.write(dir + domain + stylesDir + name[0],sassObj.pc , function(err) {});
                grunt.file.write(dir + domain + stylesDir + name[1],sassObj.sp , function(err) {});
            });
        });
        console.log('generated sass!')
    });
};

grunt.config('prefecture_styles').〇〇で、Gruntfile.jsから設定を読み込んでいます。
jsonはスタイルシートの値が入ったjsonファイル(ここではprefecture.json)、dirはベースとなるディレクトリ、stylesDirはsassが入るディレクトリ名、nameはsassの名前、templateは量産するsassのテンプレートになります。
Object.keys(data).forEachでprefecture.jsonから値を取り出して、それぞれ変数に代入し、grunt.file.writeでpc用とsp用のsassを作成&記述しています。

元となるテンプレート、template.jsを作成します。

var sasstmp = function(R,G,B) {
  this.pc = '@charset "utf-8";\n' + '$uniqueSiteColor:rgb(' + R + ',' + G + ',' + B + ');\n' + '.siteColor{ background-color:rgb(' + R + ',' + G + ',' + B + ')};\n';
  this.sp = '@charset "utf-8";\n' + '$uniqueSiteColor:rgb(' + R + ',' + G + ',' + B + ');\n' + '.siteColor{ background-color:rgb(' + R + ',' + G + ',' + B + ')};\n';
}

module.exports = sasstmp;

次にGruntfile.jsの設定に移ります。
(gruntをインストールについては割愛します。)
下記gruntプラグインをインストールしてください。

npm install grunt-contrib-compass --save-dev
npm install grunt-contrib-clean --save-dev

grunt-contrib-compassはsassをcssにコンパイルするプラグイン、grunt-contrib-cleanはファイルを削除をするプラグインです。

Gruntfile.jsを下記のように記述してください。 実際に業務で使うときのGruntfile.jsは、sassの変更をwatchしたりwebpackを使ってjsをバンドルしたりと他のタスクを追加することになりますが、今回は必要最小限の構成にしてあります。

var grunt = require('grunt');

module.exports = function(grunt) {

    grunt.initConfig({

        pkg: grunt.file.readJSON('package.json'),

        clean: {
            build: {
                src: [
                    '.sass-cache',
                    './dev/build/unique/'
                ]
            },
            release: {
                src: [
                    '.sass-cache',
                    './unique/**/styles/*.css'
                ]
            }
        },

        prefecture_styles: {
            dir: './dev/unique/',
            json: './prefecture.json',
            template: '../template.js',
            stylesDir: '/styles/',
            name: [
                'unique.scss',
                'sp_unique.scss'
            ]
        },

        compass: {
            prod: {
                options: {
                    sassDir: './dev/unique',
                    cssDir: './unique',
                    environment: 'production'
                }
            }
        }
    });


    //ローカルにあるprefecture_styles.jsを読み込んでいる。
    grunt.task.loadTasks('tasks');

    grunt.loadNpmTasks('grunt-contrib-watch');
    grunt.loadNpmTasks('grunt-contrib-compass');
    grunt.loadNpmTasks('grunt-contrib-clean');

    grunt.registerTask('build', ['clean:release', ' prefecture_styles', 'compass:prod']);

};

grunt.initConfig({})の中に prefecture_styles.jsの設定を記述します。
compass:{}には prefecture_styles.js作成したsassをcssにコンパイルする設定を行っています。
grunt.task.loadTasks('tasks')は、コメントにあるように先程作成したprefecture_styles.jsをロードしています。
Nodeモジュールとして公開しておけば、grunt.loadNpmTasks(' prefecture_styles')で読み込めるでしょう。

下記でGruntを実行してください。

grunt build

uniqueディレクトリが作成され、それぞれのドメイン名以下に、unique.css、sp_unique.cssが作成されていれば成功です。
cssを量産するタスクを実施する際に、本稿を参考にして頂ければ幸いです。今回は以上になります。

アスタミューゼでは、エンジニア・デザイナーを募集中です。ご興味のある方は遠慮なく採用サイトからご応募ください。お待ちしています。

Blue Ocean on Jenkins

お久しぶりでございます。scalaでバックエンドを開発しているaxtstar(@axtstart)でございます。

前回はExcelにはVBAがある! の話をしましたが、今回は、、、、 やっとScalaの話…ではなく、Scalaは私よりも詳しい方にお任せして ご存じ、今年、新しくJenkinsに仲間入りしたJenkins Blue Oceanの話をしてみます。

Jenkinsあれ

弊社ではCI・CDツールとしてJenkinsを使用しています。

最近までは、GUIによる設定を行っていました。

↓よくあるこんな感じ↓

f:id:astamuse:20161219163520p:plain

最近、遅ればせながら、Jenkins PipelineとBlue Oceanを使いだしているので、その紹介と活用方法を書いてみたいと思います。

Jenkins Pipeline

jenkins.io

Jenkins Pipelineとは、かつて Workflow Pluginと呼ばれていた、Jenkins内でGroovyによって記述できるワークフロープラグインです。

このプラグインは、

  • Jenkins 2.x以上で動作
  • Code as Config
  • Groovy
  • Versatile
  • Durable

説明されています

通常のJobと異なり、Jobの定義に言語(Groovy)を使用することで、 今までGUIで設定していた部分をコードで記述することができます。 SCM内にJenkinsfileと呼ばれる設定ファイルを保存することが可能であり、レビューが(通常のJOBと比較して)容易です。 ファイルであるためコピーが可能で、GUIを用いた連携では全くできないような処理も記述できます。 *1

↓おなじみHello World

f:id:astamuse:20161220193451p:plain

node
{
  echo 'Hello World'
}

gitから取得する場合

f:id:astamuse:20161220193700p:plain

Jenkinsfileと呼ばれるファイルに処理を記述し、例えばgithubで管理できます。

例:

node {
  try { 
    //def array = ["a", "b", "c"] as String[]
    stage('prepare'){
      sh 'uname -a'
    }
    stage ('git'){
      checkout scm
    }
    stage ('build'){
      parallel 'sbt build':{
         sh 'sbt compile'
      }, 'node build':{
          sh 'npm -v'
      }
    }
    stage ('test'){
      sh 'sbt test'
    }
    stage ('staging deploy'){
      sh 'sbt publish'
    }
    stage ('test after staging deploy'){
      parallel 'chrome':{
         sh 'echo test'
      }, 'firefox':{
         sh 'echo test'
      }, 'edge':{
         sh 'echo test'
      }
    }
    stage ('puroduction deploy'){
      sh 'echo test'
    }
    stage ('test after production deploy'){
      parallel 'chrome':{
         sh 'echo test'
      }, 'firefox':{
         sh 'echo test'
      }, 'edge':{
         sh 'echo test'
      }
    }
    stage('Archive result') {
      sh 'touch dummy.xml'
      archiveArtifacts 'dummy.xml'
    }
  } catch(e){
    //slackSend (channel: '#general', color: '#FF0000', message: 'Error happened!')
    throw e
  }
}

こちらに置いておきました。

↓上記pipeline実行の様子

f:id:astamuse:20161220155148p:plain

stageと記載したブロックごとの状況と履歴が一目瞭然のビューになっています。

また、通常のJobと異なり、PipelineはJenkinsが落ちても処理を継続できるように設計されています。

担当プロジェクトではまず、Webからのトランザクションテストを別々のJOBとして、起動していたのですが、 プラグインで置き換えることで、ひとまとめの処理として閲覧できるようになりました。

Jenkinsの設定が比較的多かったプロジェクトなのでかなり効果を発揮しています。

ハマったところ

以下の2点は少しハマりました。

  • In-process Script Approval

RejectedAccessExceptionという例外が発生して落ちる。 Jenkinsの管理 -> In-process Script Approvalで許可する必要がある。

  • シリアル化エラー

NotSerializableExceptionという例外が発生して落ちる。 落ちる箇所をメソッドにして「@NonCPS」アノテーションを与える必要がある。

上記ともこちらに説明があります。

Blue Ocean

Jenkins Blue OceanはJenkinsのUIを変えるプラグインで、Jenkins Worldの資料 *2 を見ると、それまであまり変更の無かった、UX部分に手を加えることで、昨今のDevOpsChatOpsといった要請にこたえていくという方向性のようです。

デフォルトでは導入されていないので、Jenkinsの管理->プラグインの管理->利用可能 に行き、フィルターで「Blue Ocean」と入力してBlue Ocean betaをインストールすれば、 必要なプラグインを合わせてインストールしてくれます。

f:id:astamuse:20161220203856p:plain

インストールが成功すると、下記のようにBlue Ocean UXへのリンクができますので、それをクリックすれば、Blue OceanのUXに行けます。 元々のViewも無くなるわけではありません。

f:id:astamuse:20161220204128p:plain

Pipeline Pluginの結果をBlue OceanのViewで見ると、昨今のCIツールであるTravis CIなどが提供している画面に似た画面が表示されます。

f:id:astamuse:20161220172033p:plain

f:id:astamuse:20161220185603p:plain

このよう感じで今風の画面が出てきて、個々のビルド結果とその詳細が見れます。

f:id:astamuse:20161220171820p:plain

f:id:astamuse:20161220205321p:plain

stage内でパラレルで実行した処理に関しては、デフォルトのViewではわからないのですが、

Blue Oceanの場合はそういった部分もパラレルである様に表示されています。

また該当部分のエラーにも素早く移動することができます。

さていかがだったでしょうか?

最後に

今年最後の投稿になってしまいました。

寒い時節が続きますが、皆様もお体にお気をつけてよいお年をお迎えください。

Have a good new year!

*1:まだまだ発展途上で全プラグインがPipeline上で動くとかそういうレベルではないようです。

*2:Jenkins WorldでのBlue Oceanのプレゼンビデオyoutube

Play Framework 2.5で入力チェックしてみる

https://www.playframework.com/assets/images/logos/play_full_color.png

アスタミューゼ 開発部のYanagitaです。

Play Frameworkの記事も3回目になります。
連載的な形で続けているので過去分のリンクを貼っておきます。
Play Frameworkが初めての人や前回の見直しはそちらへ

lab.astamuse.co.jp

lab.astamuse.co.jp

今回は前回予告していた入力チェックについて書きます。

事前準備

今回も前回の延長で説明を行います。
事前準備がまだの方はこちらを参照にして下さい。

それから少しそれますが、Play Frameworkはリリースの周期が早く最新版はv2.5.10(2016/12/08時点)となっています。
なるべく最新のバージョンに合わせて記載したいので、v2.5.10から始める方向けにテンプレートプロジェクトの作成と本記事のための修正を記載しておきます。

※ 今回はGiter8を使用してテンプレートプロジェクトを作成します。

1. 作業環境にsbt(v0.13.13以降)をインストールします。

2. ターミナルで以下のコマンドを実行します。

sbt new playframework/play-scala-seed.g8

この後、プロジェクトの名前(本記事では「sample-app」)、組織名などを聞かれますが、デフォルトのままでも問題ありません。
ここからは本記事のための修正です。

3. Filtersライブラリのコメントアウト
sample-app/sample-app.sbt

libraryDependencies += filters ← 10行目あたり
↓
// libraryDependencies += filters

4. Filtersクラス(sample-app/app/Filters.scala)の削除

削除したのはCSRF(クロスサイトリクエストフォージェリ)対策のフィルターになります。
過去の記事で使用していないため今回無効化しました。

でわでわ、本題の入力チェックに入っていきます。

入力チェック

これまでの記事を試された方は既にお気づきかもしれませんが、 前回説明したマッピングクラスには既に入力チェック機能が含まれています。
今回はこのマッピングクラスの入力チェックをコントロールして、目的に合った入力チェックの実装方法を説明します。
まずは前回作成したフォームに下記の入力チェックをかけてみます。

  • 名前(name) ・・・ 入力必須、かつ、入力文字数を2文字以上
  • 性別(sex) ・・・入力必須のみ
  • 誕生日(birthday) ・・・ 未入力を許容する
  • 身長(height) ・・・ 入力必須、かつ、100以上
  • 血液型(bloodType) ・・・ 入力必須のみ

ソースコードだと以下の通り修正
/sample-app/app/controllers/SampleController.scala

// 入力チェック適用前
val form = Form(
   mapping(
      "name" -> text,
      "sex" -> text,
      "birthday" -> date,
      "height" -> number,
      "bloodType" -> text
   )(RequestForm.apply)(RequestForm.unapply)
  )

// 入力チェック適用後
val form = Form(
   mapping(
      "name" -> nonEmptyText(minLength = 2), // 入力必須、かつ、入力文字数を2文字以上
      "sex" -> nonEmptyText,                               // 入力必須のみ
      "birthday" -> optional(date),                        // 未入力を許容する
      "height" -> number(min = 100),                   // 入力必須、かつ、100以上
      "bloodType" -> nonEmptyText                    //  入力必須のみ
    )(RequestForm.apply)(RequestForm.unapply)

textクラス以外のマッピングはデフォルト入力必須のチェックが入ります。
textクラスは未入力/空文字を許容してしまうため、入力必須にする場合はnonEmptyTextクラスを使用します。(name, sex, bloodTypeの設定を参照)
逆に未入力を許容する場合は、optionalクラスを使って未入力を許容します。(birthdayを参照)
ここでoptionalを使用した場合、マッピング先の変数をOption型に変更が必要となります。

/sample-app/app/forms/RequestForm.scala

// 変更前
case class RequestForm(
                        name: String, // 名前
                        sex: String, // 性別
                        birthday: Date, // 誕生日
                        height: Int, // 身長
                        bloodType: String // 血液型
                      )

// 変更後
case class RequestForm(
                        name: String, // 名前
                        sex: String, // 性別
                        birthday: Option[Date], // 誕生日 ← Date型からOption[Date]型に変更
                        height: Int, // 身長
                        bloodType: String // 血液型
                      )

入力文字数や入力値の条件については各マッピングの引数に宣言します。 例えば、入力文字数を2文字以上とする場合は、引数に「minLength=2」を設定します。(nameを参照)
逆に文字数の上限を設定する場合は、引数に「maxLength = 上限値」を設定します。
入力値の制限についても変数名が変わるだけで設定方法は同じです。

上記の入力チェックであればマッピングの入力チェック機能で実現できます。

ここまで実装できたらエラーメッセージの表示実装に移るケースが多いと思いますが、
今回フォームの表示に使用しているForm template helpersは、エラーメッセージの表示機能も含まれているためテンプレートの修正は不要となります。
が、そのままではエラーメッセージの見分けがつかないので(黒文字表示となるため)input.htmlテンプレートにStyleを追加して、 エラーメッセージを赤色にして目立たせます。

/sample-app/app/views/sample/input.scala.html

    <!-- titleタグの下に追加 -->
    <style>
        dd.error { color: red; }
    </style>

では、早速動作を確認してみましょう。

初期表示
f:id:astamuse:20161209114417p:plain

入力エラー後
f:id:astamuse:20161209114416p:plain

エラーメッセージが確認できましたね。
基本的なチェックであればマッピングクラスの機能で十分ですが、実際開発をしてみるとそれだけでは足りないケースがあります。
例えば、メールアドレスや郵便番号など入力フォーマットが決まっている場合の入力チェックです。 この場合は、マッピングクラスのverifyingメソッドを使用します。 今回は名前の入力チェックに英字のみを許容するよう変更します。

/sample-app/app/controllers/SampleController.scala

// 名前(name)に英字のみチェックの適用前
val form = Form(
   mapping(
      "name" -> nonEmptyText(minLength = 2),
      "sex" -> nonEmptyText,
      "birthday" -> optional(date),
      "height" -> number(min = 100),
      "bloodType" -> nonEmptyText
    )(RequestForm.apply)(RequestForm.unapply)

// 名前(name)に英字のみチェックの適用後
val form = Form(
   mapping(
      "name" -> nonEmptyText(minLength = 2).verifying("Alphabet only", name => "^[a-zA-Z]+$".matches(name)), // ← 入力条件に一致する場合のみTRUEを返す
      "sex" -> nonEmptyText,
      "birthday" -> optional(date),
      "height" -> number(min = 100),
      "bloodType" -> nonEmptyText
    )(RequestForm.apply)(RequestForm.unapply)

では、もう一度動作確認
入力内容
f:id:astamuse:20161209121204p:plain

入力エラー後
f:id:astamuse:20161209121202p:plain

名前に数字が含まれているので入力エラーになりましたね。

という風に、基本的なところはマッピングクラスの入力チェック機能を使用し、特殊なチェックはverifyingメソッドを使用することで多少複雑なチェックもカバーできます。

が、しかし
たまに2つの項目を同時にチェックする相関チェックとよばれる入力チェックも存在します。
これについては次回説明します。 今日はここまで

エンジニア&デザイナーの皆様へ

アスタミューゼではまだまだエンジニア&デザイナーを募集中しています。
すこしでも気になった方は下のバナーから採用情報をご確認下さい!

でわでわ

Copyright © astamuse company, ltd. all rights reserved.