2017年に向けてやってきたこと

去年の今頃ってなにしてたんだっけって思ったら、倒れて手術してたんだった。これを機に、人はいつでも死ぬのでなんかやるなら早めが良さそうという気持ちになったんだった。

この頃の僕には、自分のフロントエンドの専門性を高めたいという目標があった。とはいえ、専門学校に入ってからウェブサイトのコーディングとかするようになって2年目という程度の経験しかなくて、それでも実際にプロとしてやってる人たちに絶対に負けたくない気持ちがあった。そして、負けないためには短期間で効果的に成長する必要があった。そのために僕は、とにかく考える物事の範囲を狭めて、獲得した成長が必ず専門性を深める結果になるように意識した。短期的にはその試みは成功したと思ってる。

そうこうやってるうちに少し遅れて就職した。そこで感じたのは理想と現実の剥離だった。

僕が専門性を高めることを意識してきた理由は、自分が担当する役割の中においてベストな結果を実現させるためだ。けれど現実には、自分の担当範囲だと思っていた部分の外に見過ごせないだけのたくさんの問題があった。そんな状況のもとでは、仮に自分の手だけを正しく動かしていても、最終的な成果物を正解に近づけられない。現状を変えるためにもっといろんなことを考えないと良いものが作れないという意識に変わってきた。

入社してから今日までは、それらをいかにして改善していくかを考えてきた毎日だった。

僕がGitHubに公開してるReal World Website Boilerplateはそのひとつだ。他の人が作った開発環境が作業しづらくてかなりストレスを感じていたので、こうやれば快適に開発できるという枠組みを考えて提案するために公開している。「ぼくがかんがえたさいきょうのgulpfile.js」みたいなのは本質的でない部分に労力を割いているようで馬鹿にされがちだけど、その結果どうすれば開発しやすい形になるか考えることが軽視されて、関わりたくないつらいプロジェクトになっていく。あんまり開発環境にこだわりがない人に向けて「これ使ってたらおおよそ間違いないです」と薦めたい。

最近は会社で場を設けてもらって、ウェブはこうやって作っていくべきみたいな話をできるだけ具体的にしてる。それを叩き台にして全員で議論したり、加えて、これまでのやり方を改善して形にするための試験的なプロジェクトもやってみてる。

改善の手がかりが掴めたような気がして、変化の兆候が見え始めた気がしてる。

tween.jsでキャンセルできる直列なアニメーションを実装する

const TWEEN = require('tween.js')

const [tween] = [...document.querySelectorAll('.animate')]
  .map(el => {
    const state = {x: 0}
    return new TWEEN.Tween(state)
      .to({x: 300})
      .onUpdate(() => {
        el.style.transform = `translate(${state.x}px, 0)`
      })
  })
  .map((currentTween, i, tweens) => {
    const nextTween = tweens[i + 1]
    if (nextTween) currentTween.chain(nextTween)
    return currentTween
  })

tween.start()
document.querySelector('.stop').onclick = () => tween.stop()

requestAnimationFrame(function animate(time) {
  requestAnimationFrame(animate)
  TWEEN.update(time)
})

気の利かないライブラリだとsindresorhus/p-cancelableとかでcancelableなPromisesにする感じなのかなと思うけど、tween.jsは丁度いいAPIなので楽にできた。

Browserifyでbundleしたファイルをconcatする

Browserifyでrequireじゃなくて単にconcatしたいときがある。UMD対応してないライブラリを読み込むときとか、必ず最初に実行したい処理(ポリフィルとか)がある場合だ。

Webpackだと、以下のようにentryに配列を指定すればconcatできる。

module.exports = {
  entry: [
    'picturefill',
    'src/js/index.js',
  ],
  output: {
    filename: 'bundle.js',
    path: './dist',
  },
}

とはいえBrowserifyを使いたい。以下のようにしたら、Gulpでストリームとしていい感じに捌けた。

const gulp = require('gulp')
const gutil = require('gulp-util')
const sourcemaps = require('gulp-sourcemaps')
const concat = require('gulp-concat')
const uglify = require('gulp-uglify')
const mergeStream = require('merge-stream')
const browserify = require('browserify')
const source = require('vinyl-source-stream')
const buffer = require('vinyl-buffer')

export const js = () => {
  const ENTRY_FILES = [
    'node_modules/picturefill/dist/picturefill.js',
  ]

  const bundler = browserify('src/js/index.js', {
    debug: true,
  })

  const bundle = () => mergeStream(
    gulp.src(ENTRY_FILES),
    bundler
      .bundle()
      .on('error', err => gutil.log('Browserify Error', err))
      .pipe(source('index.js'))
      .pipe(buffer()),
  )
    .pipe(sourcemaps.init({loadMaps: true}))
    .pipe(concat('app.js'))
    .pipe(uglify({preserveComments: 'license'}))
    .pipe(sourcemaps.write('.'))
    .pipe(gulp.dest('dist/js'))

  return bundle()
}

watchも含めるとこんな感じになる。


追記:上記のやり方だとStreamの解決順にconcatされてしまうので絶対に安全では無い。Browserifyでbundleするよりもgulp.srcに時間がかかることはまずないけど、不安になるコードだった。gulp-headerとかでやったら良さそう。

const fs = require('fs')
const gulp = require('gulp')
const gutil = require('gulp-util')
const sourcemaps = require('gulp-sourcemaps')
const header = require('gulp-header')
const uglify = require('gulp-uglify')
const browserify = require('browserify')
const source = require('vinyl-source-stream')
const buffer = require('vinyl-buffer')

export const js = () => {
  const ENTRY_FILES = [
    'node_modules/picturefill/dist/picturefill.js',
  ]
  const concatedScripts = ENTRY_FILES.map(file => fs.readFileSync(file, 'utf8'))
    .concat('')
    .join('\n')

  const bundler = browserify('src/js/index.js', {
    debug: true,
  })

  const bundle = () => bundler
    .bundle()
    .on('error', err => gutil.log('Browserify Error', err))
    .pipe(source('app.js'))
    .pipe(buffer())
    .pipe(sourcemaps.init({loadMaps: true}))
    .pipe(header(concatedScripts))
    .pipe(uglify({preserveComments: 'license'}))
    .pipe(sourcemaps.write('.'))
    .pipe(gulp.dest('dist/js'))

  return bundle()
}

これだと、ENTRY_FILESのログがソースマップでうまいことできないけど、たぶんこっちのほうがよさげ。

Pugでrequireを利用する

Issue#2604にあった。割れ窓っぽい雑な手なので、使うのはその場凌ぎ的にやっていい場面だけに留める。

const fs = require('fs')
const pug = require('pug')

const result = pug.renderFile('test.pug', {
  require
})
fs.writeFileSync('test.html', result, 'utf8')
ul
  each n in require('lodash').range(3)
    li= n
<ul><li>0</li><li>1</li><li>2</li></ul>

BrowsersyncでSSIを利用する

ググったらmiddlewareで処理する方法しか見つからなかったけど、ドキュメントを見ていたらrewriteRulesというそれっぽいオプションがあった。レシピとしてそれっぽい方法で使うサンプルがあったけど、バージョンが古いし、SSIのシンタックスが違った。自分で以下のように書き直したら意図したとおりに動いた。

const browserSync = require('browser-sync').create()

browserSync.init({
  // 省略
  rewriteRules: [
    {
      match: /<!--#include virtual="(.+?)" -->/g,
      fn(req, res, match, filename) {
        const includeFilePath = path.join('path/to/includes', filename)

        if (fs.existsSync(includeFilePath)) {
          return fs.readFileSync(includeFilePath)
        } else {
          return `<span style="color: red">\`${includeFilePath}\` could not be found</span>`
        }
      }
    }
  ],
  // 省略
})

最近の僕のCSSとの向き合い方

業務内容に合わせたCSSの書き方が大方定まってきた感じがあるので書く。

ビルド

SassのSCSS記法で書いて、ポストプロセスとしてAutoprefixerとCSSOあたりを通すという風にしてる。

プリプロセッサーとしてPostCSSを使うのは難があって、利用できる機能を独自に追加していけるというのが難しいと思う。仕様にない独自の機能を無秩序に使ってしまって、プロジェクトの治安が悪くなったり、プロジェクトごとに利用できる機能が違うってなりそうでだるい。また、将来の仕様で書けるというプラグインでも、シンタックス上そのように書けるというだけで、機能が実装されてないブラウザでも動くようになるというわけではないというところで混乱を呼びそう。

例えばpostcss-custom-propertiesは、CSS Variablesのシンタックスで記述されていたコードを静的な値に変換するプラグインだけど、これだと仕様と意味が全然違う。変数がコンパイル前のコードにだけ存在するという前提で書いていると、将来的にブラウザ上でCSS Variablesを利用するときに想定してなかった問題にぶつかる可能性がある。これならPostCSSの独自機能として変数を定義できたほうがいい。仕様と関係のないプラグイン上の事情があることもあって、それならSassでいいやってなる。

とは言え、Sassにベッタリな書き方をしているといずれ捨てなければならないときに辛いので、Sassを使いつつ、将来の仕様で置き換えられないようなSassの独自機能にはできるだけ依存しないようにして書いている。

設計

すべての状況下で正しく機能する設計パターンは存在しなくて、CSS設計はサイト構造やデザインフローに依存する。

僕が業務パターンに合わせて利用しているCSS設計は、一般的ないわゆる設計っぽいものでなくて、いろんなことを思い切って諦めるというアプローチを強調したものになっている。

まず、CSSの再利用は可能な限りやめる。コーダーがカンプから読み取れる程度の抽象化なんて、だいたいデザイナーの認識とずれているので機能しない。ここで中途半端にがんばっても、デザインの変更のたびにリファクタリングが必要になるので諦める。CSS設計やコードの品質を上げるためだけに割ける時間はないし、そこにかけた労力を超えるメリットが享受できない。もちろんこれは業務による。

ディレクトリ構成

// core
@import "var";
@import "function";

// base
@import "../../node_modules/ress/ress";
@import "base";

// structure
@import "st/GlobalHeader";
@import "st/GlobalFooter";

// home
@import "home/Hero";
@import "home/GridList";

// about
@import "about/Hero";
@import "about/CompanyInfoTable";

ベースCSS

また、要素名に対してユーザーエージェントから与えられたスタイルはできるだけリセットする。要素名に対するスタイルは、まずデザインがセマンティックに基づいている前提で機能するものだが、すべてのデザインがセマンティックであるとは言えないからだ。マークアップは割とよく変わるので、要素名のスタイルに依存したCSSを書いていると死ぬ。

ressを読み込んだ上で、加えて以下のような感じのベースCSSを書いている。

:root {
  font-family: $font-stack-ui;
  line-height: 1.5;
}

h1,
h2,
h3,
h4,
h5,
h6 {
  font-size: 100%;
  font-weight: normal;
}

ol,
ul {
  list-style: none;
}

img,
video {
  max-width: 100%;
  height: auto;
}

セレクタルール

  • .namespace-Component_subComponent
  • .namespace-Component_subComponent.-modifier
  • .namespace-Component_subComponent.is-stateOfComponent

という風にする。

ネームスペースはページのパターンごとに分ける。

コンポーネントのクラス名は、JSとかのクラスの命名規則と似たようなイメージ。クラス名はパスカルケースで、そのメンバ変数はキャメルケースというのと同じ。言い換えると親のコンポーネント無しにサブコンポーネントを利用することはない。

liaなど、セマンティックな要素名ならセレクタに利用することも許容する。ただし2階層まで。子セレクタとして書く。

.home-GridList {
  > li {...}

  > li.is-active {...}

  > li > a {...}

  &_heading {...}

  &_heading.-wide {...}
}

ネストは出来る限り浅くする。

ECSSSUIT CSSrscssChainable BEM modifiersあたりに影響されてる。

気持ち

CSSは簡単なので設計するのが難しい。CSS設計をしようとすると、どうしてもデザインの方がそっちに寄せるしかない。

今回書いたやり方は、デザインの方が寄せなくて済むように多少非効率的にCSSを書いていくという感じになってる。

でも、ウェブデザインはCSS設計に限らず、ウェブの性質に寄り添った作りになっているべきで、ウェブデザイナーは可能な限り実装面も理解すべきだと思う。ウェブに合ってないデザインは非効率的で、もっと寄り添った作りになっていれば無駄なコストを使わずより良い結果にできるのにと、いつもそうやって悔しい思いをしている。

ちゃんとウェブデザインを作っていくためのワークフローを作りたいし、絶対にそれがないといいものは作れない。

やっていく。

ポケモンgoのメモ

ポケモンgoがリリースされ、翌日である昨日にフルコミットして得た知見をメモしておく。

※後日追記
以下の情報は無駄で大幅に死んだ可能性がある。

方針

序盤は効率よくレベル上げするついでに素材を集めつつ、軌道に乗ってきたら厳選してパーティを作ることを想定する。

トレーナーレベル10以下くらいまでやること

とにかくトレーナーレベルを上げることを考える。

大量のポケスポットを周り、大量にポケモンを乱獲する。

このときまだポケモンの強化や進化は意識する必要はない。

例外として、ポッポなどを大量に進化させると経験値を稼ぐのに効率がいいのでそれはやる。出現率が高く、進化のコストが少ないためポッポがよい。

ボックスがいっぱいになってきたら、重さと高さのいずれかまたは両方がXLでない種のみを博士に送る。理由は後述する。

レベル10以上から

この辺からジムでバトルすることを意識し始める。

プレイヤーレベルが上がると、cpが高い野生のポケモンが出現し始める。

このゲームにおけるポケモンの強さは、ほぼcpが全てであり、次いでそのポケモンの技や相性が重要となる。

cpの最大値はそれぞれの個体によって異なり、ポケモンの種類とそれぞれの個体の種族値によって決まる。

種類によるcpの最大値はここに一覧がある。

個体の種族値は、それぞれの重さと高さが大きいほど種族値も高くなる。

そのため、強いポケモンを作るには、cpの最大値が高い種類のポケモンでサイズが大きい個体を育てればいい。

そして、ポケモンを育てるためには素材を利用して強化すればよいが、この素材のコストは安くないので、できるだけ最初から高いcpのポケモンを探すと効率が良い。

また、技の組み合わせも重要である。全てのポケモンは、通常攻撃とゲージ攻撃をひとつずつ持っているが、それも個体によってことなるので考慮する。また、進化のタイミングで技の組み合わせは変化する。

加えて、cpが低い状態(maxでない状態? ここはまだ不明)で進化させないこと。このまま進化させると、せっかくの種族値が高い個体も進化後のcpの最大値が低くなってしまう。

しかし、まだこのレベルだとcpが高いところまで持っていくのは厳しい。また、トレーナーレベルによってcpの最大値に制限がかかる。そのため、種族値が高い個体は一旦置いておく。

そうするとバトルで勝つのが厳しいが、その場合は、捕獲時点からすでにcpの高かったポケモン種族値が低いものでも良い)を使うとよい。それにcp上げ途中のポケモンを加えると、そこそこで勝てるようになってくる。初期cpは高いが種族値の低いドードなどを雑に進化させたドードリオなども加えると良い。

そうしているうちに、厳選されたパーティーが出来上がっていくはず。WIP。

ハード的なこと

どこのフィールドでやるかというのがかなり重要で、自分の最寄り駅だけでずっとやってても同じようなポケモンしか手に入らない。出現するポケモンの種類は少しずつ変わっているような気はするが一気に大幅に入れ替わるということはなさそう。
平地、水辺、気温の高いところなど、条件に応じて発生するポケモンが変わるので、電車などで遠方にも出向いたほうが効率が良い。
土地のポケストップの場所はここで調べられる。

また、歩くことが死ぬほど重要になってくるゲームなので、サンダルなどを履いて遊んでいる場合ではない。
ランニングシューズなどの歩く、あるいは走れる靴を身につけておく。
自転車は交通事故の確率が高く、レアポケモンゲットの前に自分が死ぬことがあるのでやめる。

あと、個人的に心がけていることとして、充電は100%分だけ貯めて切れたら一旦家に帰るようにしている。
こうしないとやめどきがなくなって最悪死ぬ。