ミルク色の記録

やったこと、やってみたこと

Niigata.js #1に参加して発表した話

(いろいろあって忘れてた)一ヶ月前の話だけど、記録のために書いておく。

3行でふりかえり

4月23日にNiigata.jsの第1回目で「Web Worker使って◯◯する」という話をしてきた。

話した内容は、Web Workerの基礎に見せかけたWeb Worker使ってFluxする話。

去年のYAPCの前夜祭で聴いた「質問があると嬉しいんですよ」と言う話がなんとなく解った。

Niigata.jsについて

新潟でJavaScriptにフォーカスした濃ーーいお話ができる場所が欲しかったので、すごく嬉しい。

ぜひ2回目もおねがいします...

Web WorkerでFlux

FluxとかReduxやっていて、ステートって単純にJSONとかでいいんだなーと思っていたら、以前、Backbone.js使っていた時に「ModelのレイヤーをWorkerの中に入れてプレゼンテーションレイヤーと分離してみたいなー、でもうまくできないなー」と思っていたやつが今度はできそうだと気づいたのがきっかけ。

資料は下。

www.slideshare.net

Universal(Isomorphic)な辺りを考えるとWorkerを直接使わずに、もうひとつ抽象化しないと行けないような気がするけど、どんなもんか。

発表してみて

他の参加者さんから質問とか反応をもらえて、なるほど。と思ったので、また、なにかネタができたら...。

2015年が終わるのでフロントエンド開発に使ってるツールまわりをざっと書き出してみた

題名の通り、ざっと書き出し。

このあたりのことを相談された時に、手元のリポジトリを漁って「こんな感じでやってます」とやってたけど、 とりあえずこのURL渡して参考にしてもらうみたいなことができれば...

ご注意

  • Xubuntu 14.04でやってるのでWindowsOSXは未確認。
  • こじんまりした規模の開発で使っている構成なので、規模によっては向いていないかも。
  • サーバとはJSONとかのAPIでお話するだけの、SPAなフロントエンド用(ルーティングはハッシュチェンジ系向け)。

おしながき

共通用

nodeの管理

とりあえずnodeがないと始まらないのでnodeから。

nodeの管理はnodebrewを使っている。 変なハマり方をしないように、v4.2.x系を使っている。

JavaScriptのLint

eslintを使っている。 Vimプラグインsyntasticを使って、ファイルの保存時にチェックしてる。

インストール

$ npm install -g eslint

次のようにVimに設定して有効にする。

let g:syntastic_javascript_checkers=['eslint']

プロジェクト用

プロジェクトディレクトリの下準備

npm initでpackage.jsonを作り、giboを使ってnode用の.gitignore設定を作っている。

$ mkdir project
$ cd project
$ npm init
$ gibo Node | tee .gitignore

スクランナー

スクランナーはgulpの3.9系を使っている。

gulpコマンドはプロジェクト配下にインストールしたものをnpmのコマンド経由で使う。 後々追加することになるgulpのプラグインはgulp-load-pluginsでまとめてロードできるようにしている。

HTMLやCSSJavaScriptなどのファイルはappディレクトリ配下に置いて行き、 各種ツールを通してビルド済みディレクトリ(dist)に配備されるようにする。

ビルド済みディレクトリはpackage.jsonに設定として書いておき、gulpfile.jsなどから参照して使っている。

ディレクトリ構成は次のような感じ。

.
├── app
├── dist
├── gulpfile.js
├── node_modules
└── package.json

インストール

$ npm install --save-dev gulp gulp-load-plugins del

開発時に使う共通的なgulpのタスクは次の通り。

タスク名 用途
clean ビルド済みディレクトリを削除する
dev 開発環境を起動する
default デフォルト(cleanしてdevを実行する)

gulpfile.js

var path = require('path');
var pkg = require('./package.json');
var distDir = path.join(__dirname, pkg.dist);
var gulp = require('gulp');
var $ = require('gulp-load-plugins')();
var del = require('del');

gulp.task('clean', del.bind(null, [distDir]));

gulp.task('dev', [], () => {

});

gulp.task('default', ['clean'], () => {
  gulp.start('dev');
});

package.jsonにgulpコマンドとビルド済みディレクトリの設定を次のように追加している。

package.json

-- 中略 --
  "dist": "dist",
  "scripts": {
    "gulp": "gulp"
  },
-- 中略 --

ビルド済みディレクトリはgitignore対象。

.gitignore

dist
-- 中略 --

これでgulpはこんな感じに使う。

$ npm run gulp

HTMLと開発用Webサーバ

開発時にブラウザで確認するためのWebサーバはconnectを使っている。

Webサーバはビルド済みディレクトリをドキュメントルートとして動作させる。 gulpのwatchでファイル監視して、変更があったら必要なビルドをしてLiveReloadでブラウザを更新させる。

BrowserSyncをしばらく使っていたけど、 connectで十分な機能しか使ってなかったのでconnectに戻した。

インストール

$ npm install --save-dev connect connect-livereload serve-static gulp-livereload

HTMLファイルをappディレクトリに追加する。

app/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Hello World!</title>
</head>
<body>
  <h1>Hello World!</h1>
</body>
</html>

HTMLと開発用Webサーバのためのgulpタスクは次の通り。

タスク名 用途
html HTMLファイルをビルド済みディレクトリへ配備する
serve 開発用Webサーバを起動する
dev 開発環境を起動し、HTMLファイルを監視する

gulpfile.js

-- 中略 --
var connect = require('connect');
var connectLivereload = require('connect-livereload');
var serveStatic = require('serve-static');
-- 中略 --
gulp.task('html', () => {
  return gulp.src('app/**/*.html')
  .pipe(gulp.dest(distDir))
  .pipe($.livereload());
});

gulp.task('serve', () => {
  $.livereload.listen();
  connect()
  .use(connectLivereload())
  .use(serveStatic(distDir))
  .listen(3100);
});

gulp.task('dev', ['html', 'serve'], () => {
  gulp.watch('app/**/*.html', ['html']);
});
-- 中略 --

これでgulpを起動して、開発用Webサーバを立ち上げてブラウザで確認できる。

$ npm run gulp

CSSや画像ファイルなどのスタイルまわり

CSSBootstrapをベースにlessで書いて、 gulp-lessでビルドしている。

インストール

$ npm install --save bootstrap
$ npm install --save-dev gulp-less

アプリ用にapp.lessファイルを作り、importでbootstrapのlessを読ませて必要なスタイルを書いていく。

Reactみたいなコンポーネント作って行く系のを使ってると、 コンポーネントごとにlessファイル作って管理とかしたほうがいいのかなーと思うけど、 Bootstrapに毛の生えた程度にしかスタイル追加してないのでapp.lessだけで今のところは済んでいる。

アイコン増やしたいみたいなときだけfont awesomeとか足してる。

app/styles/app.less

@import '../../node_modules/bootstrap/less/bootstrap.less';

body {
  background-color: #efffef;
}

HTMLにCSSの読み込みを追加。

app/index.html

-- 中略 --
<head>
  <meta charset="UTF-8">
  <title>Hello World!</title>
  <link rel="stylesheet" href="./styles/app.css" />
</head>
-- 中略 --

スタイル周りのgulpタスクは次の通り。

タスク名 用途
styles:dev 開発環境用スタイルビルド(less:dev、fonts、imagesを実行する)
less:dev lessファイルをビルドする
fonts フォントリソースをビルド済みディレクトリへ配備する
images 画像リソースをビルド済みディレクトリへ配備する
dev 開発環境を起動し、HTMLファイル、lessファイルを監視する

gulpfile.js

-- 中略 --
gulp.task('styles:dev', ['less:dev', 'fonts', 'images']);

gulp.task('less:dev', () => {
  return gulp.src('app/styles/**/*.less')
  .pipe($.less({
    paths: [
      'node_modules/bootstrap/less'
    ]
  }))
  .pipe(gulp.dest(path.join(distDir, 'styles')))
  .pipe($.livereload());
});

gulp.task('fonts', () => {
  return gulp.src([
    'node_modules/bootstrap/fonts/*'
  ])
  .pipe(gulp.dest(path.join(distDir, 'fonts')));
});

gulp.task('images', () => {
  return gulp.src([
    'app/images/**/*'
  ])
  .pipe(gulp.dest(path.join(distDir, 'images')));
});
-- 中略 --
gulp.task('dev', ['html', 'styles:dev', 'serve'], () => {
  gulp.watch('app/**/*.html', ['html']);
  gulp.watch('app/styles/**/*.less', ['less:dev']);
});
-- 中略 --

JavaScriptまわり(モジュールバンドラ)

Babelを使ってES6で書いたコードをES5にトランスパイルしている。 モジュールバンドラにはwebpackを使っている。

webpackは多機能だけど、あくまでJavaScriptのモジュールバンドラとして使っているので、 必要に応じてbrowserifyに差し替えたりしている。

インストール

$ npm install --save-dev webpack babel-loader babel-preset-es2015 gulp-util

Babelの設定は.babelrcに書く。

.babelrc

{
  "presets": ["es2015"]
}

開発環境用のwebpack設定を作る。

エントリポイントのapp.jsを読み込んで、Babelを通してソースマップ付きでビルド済みディレクトリに配備している。

webpack.config.js

var path = require('path');
var pkg = require('./package.json');
var distDir = path.join(__dirname, pkg.dist);

module.exports = {
  entry: {
    app: './app/scripts/app.js'
  },
  output: {
    path: path.join(distDir, 'scripts'),
    filename: '[name].js'
  },
  devtool: 'inline-source-map',
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel'
      }
    ]
  }
};

JavaScript用のgulpタスクは次の通り。

タスク名 用途
bundle:dev JavaScriptの開発用ビルドを行う
dev 開発環境を起動し、HTMLファイル、lessファイル、JavaScriptファイルを監視する

gulpfile.js

-- 中略 --
var webpack = require('webpack');
var bundler = webpack(require('./webpack.config.js'));
-- 中略 --
gulp.task('bundle:dev', cb => {
  bundler.run((err, stats) => {
    if (err) {
      throw new $.util.PluginError('webpack:build', err);
    }
    $.util.log('[webpack:build]', stats.toString({
      colors: true,
      chunkModules: false
    }));
    cb();
    $.livereload.reload();
  });
});
-- 中略 --
gulp.task('dev', ['html', 'styles:dev', 'bundle:dev', 'serve'], () => {
  gulp.watch('app/**/*.html', ['html']);
  gulp.watch('app/styles/**/*.less', ['less:dev']);
  gulp.watch('app/scripts/**/*.js', ['bundle:dev']);
});
-- 中略 --

JavaScriptソースコードはapp/scripts配下に置いて行く。

app/scripts/Hoge.js

export default class Hoge {

  constructor(name) {
    this._name = name;
  }

  greet() {
    return `Hello! ${this._name}`;
  }
}

app.jsをエントリポイントとして、HTMLファイルに読み込ませる。

app/scripts/app.js

import Hoge from './Hoge';

const hoge = new Hoge('test');

console.log(hoge.greet());

app/index.html

-- 中略 --
<body>
  <h1>Hello World!</h1>
  <script src="./scripts/app.js"></script>
</body>
</html>

プロダクション用のビルド

開発用のビルド設定とは別にプロダクション用ビルド設定(ソースマップなし、ミニファイ化)も用意している。 ビルド済みリソースはzipで固めて、直接フロントエンドの開発をしないメンバーに配れるようにする。

インストール

$ npm install --save-dev gulp-zip gulp-minify-css

gulpのプロダクション用ビルドタスクは次の通り。

タスク名 用途
styles:prod プロダクション環境用のstyleビルド(less:prod、fonts、imagesを実行する)
less:prod lessファイルのプロダクション用ビルドを行う
bundle:prod JavaScriptのプロダクション用ビルドを行う
prod プロダクションビルドを起動する
build プロダクションビルド(cleanしてprodする)

less:devとless:prodの共通部分をless関数として切り出して、それぞれのタスクに利用する。 webpackのプロダクションビルド用設定を追加してbundle:prodではそちらを使うようにする。

gulpfile.js

-- 中略 --
function less() {
  return gulp.src('app/styles/**/*.less')
  .pipe($.less({
    paths: [
      'node_modules/bootstrap/less'
    ]
  }));
}
-- 中略 --
gulp.task('styles:prod', ['less:prod', 'fonts', 'images']);

gulp.task('less:dev', () => {
  return less()
  .pipe(gulp.dest(path.join(distDir, 'styles')))
  .pipe($.livereload());
});

gulp.task('less:prod', () => {
  return less()
  .pipe($.minifyCss())
  .pipe(gulp.dest(path.join(distDir, 'styles')))
});
-- 中略 --
gulp.task('bundle:prod', cb => {
  webpack(require('./webpack.production.config.js'), cb);
});
-- 中略 --
gulp.task('prod', ['html', 'styles:prod', 'bundle:prod'], () => {
  return gulp.src(path.join(distDir, '**/*'))
  .pipe($.zip(pkg.name + '.zip'))
  .pipe(gulp.dest('build'));
});

gulp.task('build', ['clean'], () => {
  gulp.start('prod');
});
-- 中略 --

webpackの設定を開発用とプロダクション用で分離する。両方の共通部分をcommon設定ファイルとして取り出す。

webpack.common.js

var path = require('path');
var pkg = require('./package.json');
var distDir = path.join(__dirname, pkg.dist);

module.exports = function() {
  return {
    entry: {
      app: './app/scripts/app.js'
    },
    output: {
      path: path.join(distDir, 'scripts'),
      filename: '[name].js'
    },
    module: {
      loaders: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          loader: 'babel'
        }
      ]
    }
  };
};

開発用のwebpack設定は、共通設定を読み込んでソースマップを追加した設定にする。

webpack.config.js

var config = require('./webpack.common.js')();
config.devtool = 'inline-source-map';

module.exports = config;

プロダクション用のwebpack設定は、共通設定を読み込んでUglifyプラグインを追加した設定にする。

webpack.production.config.js

var config = require('./webpack.common.js')();
var webpack = require('webpack');
config.plugins = [
  new webpack.optimize.UglifyJsPlugin({
    compress: {
      warnings: false
    }
  })
];

module.exports = config;

buildディレクトリはgitignore対象。

.gitignore

dist
build
-- 中略 --

ビルド用コマンドをpackage.jsonに追加する。

package.json

-- 中略 --
  "scripts": {
    "gulp": "gulp",
    "build": "gulp build"
  },
-- 中略 --

プロダクション用ビルドはコマンドを叩いて行う。

$ npm run build

テスト環境

テストはmochapower-assertで書いている。 テストコードもES6で書きたいのでespower-babelを使っている。

インストール

$ npm install --save-dev mocha power-assert espower-babel

mochaの設定を入れておく。

test/mocha.opts

--compilers js:espower-babel/guess

テストコードはこんな感じ。

test/Hoge-test.js

import Hoge from '../app/scripts/Hoge';
import assert from 'power-assert';

describe('Hoge', () => {

  const name = 'Test';
  let hoge;
  beforeEach(() => {
    hoge = new Hoge(name);
  });

  describe('#greet', () => {
    it('returns greet message', () => {
      assert(hoge.greet() === `Hello! ${name}`);
    });
  });

});

テストを叩くためのnpmコマンドをpackage.jsonに追加する。

package.json

-- 中略 --
  "scripts": {
    "gulp": "gulp",
    "build": "gulp build",
    "mocha": "mocha",
    "test": "mocha test/*-test.js"
  },
-- 中略 --

テスト叩くときはこんな感じで一気に。Vimで編集のファイルとか個別に叩くときはnpm run mocha で。

$ npm test

APIサーバとつなぎながら開発

AjaxでバックエンドのAPIを使う場合、可能であればバックエンドのサーバを起動しておいて、 http-proxy-middlewareを使ってAPIへのリクエストだけリバースプロキシしている。

インストール

$ npm install --save-dev http-proxy-middleware

この場合、gulpのserveタスクに次のような修正を加える。 下の設定例だと/apiなパスへのリクエストはlocalhostの3000ポートの方へプロキシされる。

gulefile.js

-- 中略 --
var proxyMiddleware = require('http-proxy-middleware');
-- 中略 --
gulp.task('serve', () => {
  var port = process.env.API_PORT || 3000;

  $.livereload.listen();
  connect()
  .use(connectLivereload())
  .use(serveStatic(distDir))
  .use(proxyMiddleware([
    '/api'
  ], {
    target: 'http://localhost:' + port,
    changeOrigin: true
  }))
  .listen(3100);
});
-- 中略 --

fetchを使ってAPIを叩くサンプルが次のような感じ。

app/scripts/Hoge.js

export default class Hoge {

  constructor(name) {
    this._name = name;
  }

  greet() {
    return `Hello! ${this._name}`;
  }

  fetchGreet() {
    return fetch('/api/greeting')
    .then(res => res.json())
    .then(json => {
      return `${json.message} ${this._name}`;
    });
  }
}

app/scripts/app.js

import Hoge from './Hoge';

const hoge = new Hoge('test');

console.log(hoge.greet());

hoge.fetchGreet()
.then(message => {
  console.log(message);
});

余談:バックエンドサーバのAPIをモック

バックエンドサーバのAPI仕様がJSON Hyper-Schemaで定義されている場合 (下のfixture/api.jsonみたいな感じ)、雑にモックサーバを起動して代替にしている。

fixture/api.json

{
  "$schema": "http://interagent.github.io/interagent-hyper-schema",
  "type": [
    "object"
  ],
  "definitions": {
    "greeting": {
      "$schema": "http://json-schema.org/draft-04/hyper-schema",
      "title": "Greeting",
      "description": "ご挨拶API",
      "stability": "prototype",
      "strictProperties": true,
      "type": [
        "object"
      ],
      "definitions": {
        "message": {
          "description": "ご挨拶メッセージ",
          "example": "こんにちは",
          "type": [
            "string"
          ]
        }
      },
      "links": [
        {
          "description": "ご挨拶メッセージを取得する",
          "href": "/api/greeting",
          "method": "GET",
          "rel": "self",
          "title": "Info"
        }
      ],
      "properties": {
        "message": {
          "$ref": "#/definitions/greeting/definitions/message"
        }
      }
    }
  },
  "properties": {
    "greeting": {
      "$ref": "#/definitions/greeting"
    }
  }
}

インストール

$ npm install --save-dev json-schema-mockserve

この場合、gulpのserveタスクを次のように修正する。 環境変数MOCKに値が設定された場合、モックサーバを起動するようにしている。

gulpfile.js

-- 中略 --
var MockServe = require('json-schema-mockserve').MockServe;
-- 中略 --
gulp.task('serve', () => {
  var port = process.env.API_PORT || 3000;

  if (process.env.MOCK) {
    new MockServe({
      port: port,
      path: path.join(__dirname, 'fixture', 'api.json')
    }).start();
  }

  $.livereload.listen();
  connect()
  .use(connectLivereload())
  .use(serveStatic(distDir))
  .use(proxyMiddleware([
    '/api'
  ], {
    target: 'http://localhost:' + port,
    changeOrigin: true
  }))
  .listen(3100);
});
-- 中略 --

モックサーバで起動するコマンドをpackage.jsonに追加する。

package.json

-- 中略 --
  "scripts": {
    "gulp": "gulp",
    "gulp:mock": "MOCK=ON gulp",
    "build": "gulp build",
    "mocha": "mocha",
    "test": "mocha test/*-test.js"
  },
-- 中略 --

コマンドから起動する。

$ npm run gulp:mock

fetchをモックしてテスト

Ajaxなことは、Promiseベースのインターフェイスになっていることから、 XMLHttpRequestよりもfetchを使うようになった。 fetchを使うAPIをテストする場合、fetch-mockを使っている。

インストール

$ npm install --save-dev fetch-mock

テストコードは次のような感じに。 fetchMockのmockでモック設定して、afterEachでrestoreしてモック解除している。

test/Hoge-test.js

import Hoge from '../app/scripts/Hoge';
import assert from 'power-assert';
import fetchMock from 'fetch-mock';

describe('Hoge', () => {

  const name = 'Test';
  let hoge;
  beforeEach(() => {
    hoge = new Hoge(name);
  });
  afterEach(() => {
    fetchMock.restore();
  });

  describe('#greet', () => {
    it('returns greet message', () => {
      assert(hoge.greet() === `Hello! ${name}`);
    });
  });

  describe('#fetchGreet', () => {
    beforeEach(() => {
      fetchMock.mock('/api/greeting', 'GET', {
        status: 200,
        body: '{"message":"Hello Hello Hello..."}'
      });
    });
    it('returns greet message promise', () => {
      return hoge.fetchGreet()
      .then(message => {
        assert(message === `Hello Hello Hello... ${name}`);
      });
    });
  });

});

ブラウザでテスト

nodeの環境だけでなくブラウザ環境でもテストする場合はtestemを使っている。

webpackでJavaScriptコードをブラウザ用にビルドしてtestemに読ませてる。 このとき、power-assertとwebpackでjsonの読み込みを可能にしておかないと、 ビルド後のコードが動かないのでjson-loaderを追加しておく。

インストール

$ npm install --save-dev testem glob json-loader

テスト用のwebpack設定を追加する。 testディレクトリ配下のテスト用コードをglobでごそっと集めて変換する。

webpack.test.config.js

var path = require('path');
var glob = require('glob');

module.exports = {
  entry: {
    test: glob.sync(path.join(__dirname, 'test/**/*-test.js'))
  },
  output: {
    path: path.join(__dirname, '.powered-assert'),
    filename: '[name].js'
  },
  devtool: 'inline-source-map',
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel'
      },
      {
        test: /\.json$/,
        loader: 'json'
      }
    ]
  }
};

testemの設定をtestem.jsonに行う。 実行前にwebpackでテスト用ビルドを行い、実行後にテスト用ビルドディレクトリ(.powered-assert)ごと破棄する。

testem.json

{
  "framework": "mocha",
  "before_tests": "webpack --config webpack.test.config.js",
  "on_exit": "rm -rf .powered-assert/",
  "src_files": [
    ".powered-assert/**/*.js"
  ]
}

テスト用ビルドディレクトリもgitignore対象にする。

.gitignore

dist
build
.powered-assert/
-- 中略 --

testemの実行をコマンドとして追加しておく。

package.json

-- 中略 --
  "scripts": {
    "gulp": "gulp",
    "gulp:mock": "MOCK=ON gulp",
    "build": "gulp build",
    "mocha": "mocha",
    "testem": "testem",
    "test": "mocha test/*-test.js"
  },
-- 中略 --

testemを起動する場合は次のようにして行う。起動したらブラウザでアクセスする。

$ npm run testem

一気に行う場合はciオプションで実行する。

$ npm run testem ci

まとめ

一通りやったリポジトリこちら

必要なものを必要なときに必要なだけ取り入れつつ、全体的に薄めに取り替えが効くようにシンプルな構成で...というのが今のところの自分の方針。

勉強会に初めてLT参加した話

ちょっと時間開いてしまったけれど、記録のために書いておく。


NDS meetup #8nds-meetup.connpass.com
という勉強会で初めてLTやってみた。


自分は35歳の平凡なプログラマ
勉強会とかデベロッパー向けイベントに参加しても毎回聴くだけの側。


この夏YAPCに行った体験とか、日頃の尊敬するエンジニアさんたちのアドバイスとかでいろいろ思うことがあり、ミリ単位でいいから踏み出してみたいかな…とか思ったので、テーマが自分の好きなJavaScriptという事もあり初挑戦。


発表内容はECMAScript6で書けるようになったシンタックスについて。
普段コード書いていて「あ、コレってもっと簡単に書けるようになったよな…」みたいなのに絞った(クラスとかブロックスコープみたいな忘れにくいのはすっ飛ばした)。

www.slideshare.net

終わったあとに気づいたんだけど、ES6じゃないじゃんってのも混じってた...ごめんなさい。


今回の経験で劇的に何か変わったみたいなのはないけれど、またミリ単位で進んで行きたいかなーみたいな感じ。

YAPC::Asia Tokyo 2015に行ってきた

初参加。
めちゃくちゃ楽しかった。


以下、ふり返り。

きっかけ

去年のuzullaさんの『半端なPHPDisでPHPerに陰で笑われないためのPerl Monger向け最新PHP事情』を動画で見て、「なんなんだろう?この空間。会場でリアルタイムに見たかったな...」と思ったのがきっかけ。

で、チケットの購入をトークのスケジュールが決まるまで…と待機していたら最初見事に買い逃した...軽く放心状態になっていたところをneko_gata_sさんに救っていただいた。

当日の過ごし方

ひたすらトークを聞いて回る、完全にトーク参加中心で過ごして大満足。素人万歳!

  • トーク・質疑応答が終わったら速やかに移動する
  • 水分摂取は生命維持可能な最低限に抑える

と、徹底して動きまわり、前夜祭と当日のすべてのお目当てトークを聴いて回った。
徹底しすぎて無限コーヒー全く飲めなかった...(弁当は頂いた。ごちそうさまでした)

会場とか懇親会でmiyagawaさんをはじめ、注目している方々をたくさん見かけたけど、コミュ力低い方なのでその辺はお察し...ファンボーイとして柱の影からそっと見守った。

所感

前夜祭、待望の「PHP帝国の逆襲」を生で見れて胸が熱くなった。
hayajoさんのおかげでuzullaさんご本人にちょっとだけ挨拶できてステッカーもらえた!

参加したトークすべて面白くて、学びがあって、上手くて、スピーカーの方々の背景にコミュニティの文化みたいなのが感じられて、「自分は趣味+仕事でプログラム書いているだけの人間だったんだなー。やっぱ自分へっぽこだわー。」と思った。

運営スタッフの方々、スピーカーの方々、きっかけとなったuzullaさん、チケット譲っていただいたneko_gata_sさん、仕事扱いにしてくれた会社のボスと快く送り出してくれた家族に感謝しつつ、「楽しかったってだけで終わらせないようにしたいけど、どうしたらいいのやら...」とか考えながら、自分の最初で最後のYAPCが終わった。

WebSocketとcreateObjectURLとvideoで遊んでみた

(´・ω・`)。oO(WebSocketのバイナリデータ送信って、クライアントの方も対応したのかなぁ…)

と、ふと思ったので試してみた。

環境こんな感じ。

  1. OS : ubuntu 11.04
  2. ブラウザ : Google Chrome 17.0.963.56
  3. node.js v0.6.11, connect, websocket

nodeのwebsocketモジュールで

client.sendBytes(data);

みたいなメソッドがあったのを覚えていたので、画像ファイルを送ってブラウザ側で貰えているか、
messageイベントのデータを見てみると…届いてるっぽい。

せっかくなので、前々からちょっと試してみたかったことを試してみた。

なんちゃってストリーミング配信的な

適当に用意した動画ファイルをOpenShot Video Editorを使って、
webmでエンコードして10秒毎に分割。連番のファイル名にしてひとつのディレクトリに設置。
それをクライアントに次々送りつけて、createObjectURLで変換してvideoタグのsrcにセットして再生していく。

最初videoタグ一つでやってたら、継ぎ目がプチプチしてたので、
videoタグを2つ用意してダブルバッファリング的にやってみたら、
割といい感じになったっぽい。

カメラからとかで動的にwebmファイル作れるともっと面白いんだけど。。

書いたソースは下のとおり。

サーバ側

var connect = require('connect');
var WebSocketServer = require('websocket').server;
var util = require('util');
var fs = require('fs');
var EventEmitter = require('events').EventEmitter;

/**
 * ファイルリストを順番に処理するための簡易キュー
 */
function Queue(queue) {
    EventEmitter.call(this);
    this.queue_ = queue;
    this.next();
}
util.inherits(Queue, EventEmitter);
Queue.prototype.next = function() {
    var that = this;
    process.nextTick(function() {
        var q = that.queue_.shift();
        if (q != null) {
            that.emit('data', q);
        } else {
            that.emit('end');
        }
    });
};

/**
 * 簡易HTTPサーバ
 */
var httpServer = connect.createServer(
    connect.static(__dirname + '/public_html', { maxAge : 86400000 })
    , connect.cookieParser()
    , connect.bodyParser()
    , connect.session({ secret : 'hogefugapiyo' })
    , connect.router(function(route) {

    })
).listen(3000);

/**
 * WebSocketサーバ
 */
var wsServer = new WebSocketServer({
    httpServer : httpServer,
    autoAcceptConnections : true
});
wsServer.on('connect', function(client) {
    util.log('client connect');
    client.on('message', function(msg) {
        var param = msg.utf8Data;
        switch(param) {
            case 'play':
                // クライアントからplayメッセージを受け取ったらデータ送信
                sendData(client);
                break;
        }
    });
    client.on('close', function() {
        util.log('client disconnect');
    });
});
console.log('http://localhost:3000/');

/**
 * データをクライアントへ送信
 */
function sendData(client) {
    var path = __dirname + '/public_html/video';
    // ディレクトリ一覧読み込み
    fs.readdir(path, function(err, files) {
        if (err) {
            util.log(err);
            return;
        }
        // ファイル一覧を名前順にソート
        files.sort();
        var queue = new Queue(files);
        queue.on('data', function(file) {
            // ファイル読み込み
            fs.readFile(path + '/' + file, function(err, data) {
                if (!err) {
                    client.sendBytes(data);
                }
                // データを送信完了したら次のキューへ
                queue.next();
            });
        });
        queue.on('end', function() {
            // 全部送ったら終わり
            console.log('end');
        });
    });
}

クライアント側

HTMLこんな感じ。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <link rel="stylesheet" type="text/css" href="css/index.css" />   
        <title>Video Viewer</title>
    </head>
    <body>
        <div id="viewer">
            <div id="screen_cont">
                <video id="screen1" style="display:none;"></video>
                <video id="screen2" style="display:none;"></video>
            </div>
            <div id="controller">
                <input type="button" id="play" value="play" />
            </div>
        </div>
        <script type="text/javascript" src="js/jquery-1.6.4.min.js"></script>
        <script type="text/javascript" src="js/index.js"></script>
    </body>
</html>


クライアント側のソースはこんな感じ。

!function(global) {
    
    /**
     * サーバから受信したデータキュー
     */
    var datas_ = [];
    /**
     * 再生待ちのvideoタグキュー
     */
    var readyScreens_ = [];
    /**
     * データ設定待ちのvideoタグキュー
     */
    var waitScreens_ = [];
    /**
     * 最初のデータ受信判定フラグ
     */
    var firstData_ = true;
    
    // スクリーン1
    waitScreens_.push(document.getElementById('screen1'));
    waitScreens_[0].addEventListener('ended', function() {
        // 再生終了イベント
        // スクリーンを切り替え
        changeScreen(this);
        updateQueue();
    }, false);
    // スクリーン2
    waitScreens_.push(document.getElementById('screen2'));
    waitScreens_[1].addEventListener('ended', function() {
        changeScreen(this);
        updateQueue();
    }, false);
    
    
    /**
     * スクリーンを切り替える
     * @param {Element} oldScreen videoエレメント
     */
    function changeScreen(oldScreen) {
        // 再生済みのスクリーンを非表示にして設定待ちスクリーンキューに追加
        oldScreen.style.display = 'none';
        waitScreens_.push(oldScreen);
        // 再生待ちスクリーンを取得
        var screen = readyScreens_.shift();
        if (screen) {
            // 存在したら表示して再生
            screen.style.display = '';
            screen.play();
        }
    }
    
    /**
     * キューの更新
     */
    function updateQueue() {
        if (waitScreens_.length > 0 && datas_.length > 0) {
            // 受信データとデータ設定待ちのスクリーンが存在する場合それぞれ取り出し
            var screen = waitScreens_.shift();
            var blob = datas_.shift();
            // src属性に設定
            screen.src = blob;
            if (firstData_) {
                // 最初の受信データだった場合
                firstData_ = false;
                // データを再生
                screen.style.display = '';
                screen.play();
            } else {
                // 再生待ちのスクリーンキューに追加
                readyScreens_.push(screen); 
            }
        }
    }

    // WebSocketの接続を開く  
    var socket = new WebSocket('ws://' + location.host + '/');
    // messageイベント
    socket.addEventListener('message', function(evt) {
        // メッセージを受け取ったらcreateObjectURLで変換してデータキューに入れる
        datas_.push(window.webkitURL.createObjectURL(evt.data));
        // キュー更新
        updateQueue();
    }, false); 

    $('#play').click(function() {
        // 再生ボタンクリックしたら再生メッセージをサーバへ通知
        socket.send('play');
    });

}(this);