最近の僕の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%分だけ貯めて切れたら一旦家に帰るようにしている。
こうしないとやめどきがなくなって最悪死ぬ。

入社してた

4月になってしばらくしてから新卒として内定が決まって、5月中旬頃から今の会社で働いている。
就職が決まった時期としてはかなり遅い。ギリのタイミングまで自分のレベル上げをしたかったというのと、最悪就職できないはずもないだろうと社会をなめていたことが原因だった。
でも結果オーライというか、あのときだらだら悩んでたおかげで今のいい感じの環境で働けるようになってラッキーだったなという感想。ついでに言うと、他人の言葉を無視して、自分だけを信じろという考え方が功を奏した感じはある。

入社した会社は、広告のサイトとか作ってる、いわゆる制作会社。10人に満たないくらいの規模でやってる。
加えて、Web制作以外にもイベントの設営とかアプリ開発とかやってたり。
なにより、おもろそうだなー、こいつら、おもろそうだなーと思ったのでここに決めた。

人間関係は良好で、自分の発言も尊重されるし、かなり自由に働ける。
フロントエンドな人は僕だけで、その辺の道は自分で切り開くしかないんだけど、そんでもやってやんぜという心意気はある。
この世界でやっていくために申し分ない土俵に上がったという感触があって、これからはこの場所から世界を変えていく予定です。

BrowserSyncでサーバーのパスのbasenameを指定する

BrowserSyncでサーバーを立ち上げると、http://localhost:3000/とかルートのURLで指定したディレクトリが開くようになっている。
でもたまにパスの指定の関係で、ルートでなくて下層のディレクトリでファイルを開きたいというときがある。http://localhost:3000/dir-name/みたいに。
jekyllとかルーティングライブラリとかだと、basenameみたいな感じのオプションで設定できるので、BrowserSyncでもできるのかなーと思ってたらそういうのはなさそうだった。
ただ、別のオプションで同じようなことが実現できた。

module.exports = {
  files: 'dist',
  server: {
    baseDir: './',
    routes: {
      '/dir-name': 'dist'
    }
  },
  startPath: '/dir-name/'
};

こんな感じの設定ファイルを作って、以下のようなコマンドで実行できる。

$ browser-sync start --config "bs-config.js"

普通のウェブサイトのためのJSボイラープレートを作った

JSバリバリじゃない、いわゆる普通のウェブサイトでもモダンなJSを書くためのボイラープレートを作った。

最近のJSの文脈的には、browserifyでモジュール分割してes2015で書きたい。

でも、browserifyは全部のモジュールをひとつのファイルにまとめるというのが基本的な使い方で、モジュールごとにファイルを分割して、ページによって読み込むファイルを変えるみたいなのは結構やりにくい。

これを解決するために、ページごとに1ファイルにバンドルするという手があるけど、それだと、ページ遷移時に共通部分をもつファイルを丸々ダウンロードしないといけないのが無駄になる。
es6 modulesがブラウザに実装されればこの問題はなくなるけど、かなり先の未来だと思う。

ところが、全部のファイルを1ファイルにまとめるという方法を採ると、ページで実行する機能ごとの切り分けが難しくなる。

なので、URLごとに実行する処理を分けるURLディスパッチャーを作った。
yuheiy/simple-url-dispatcher

この記事から着想を得た。
そこそこ規模が大きくても何とかなるjavascriptの設計(URL dispatcherの薦め)

jQueryプラグインやCreateJSとかのbrowserifyしにくいファイルは、バンドルするファイルの前に普通にscriptタグで読み込めば使える。

モジュールシステムとes2015、そしてURLディスパッチャーを採用しつつ、jadeとscssも加えて、そこそこ現実的なフロントエンドのスタックができたと思う。
yuheiy/js-boilerplate-for-normal-websites

PhantomJSでサムネイルの取得を自動化する

前に作ったウェブサイトギャラリーで使ってるサムネイル画像の取得を自動化した。
それまでは、URLを渡すとサムネイル画像を提供してくれる外部のAPIを使ってたけど、Node.jsでフォルダ内の全HTMLをキャプチャし画像化という記事を見て、おもしろそうだったのでやってみた。
実際にはこの記事のサンプルより少し複雑になってしまった。

PhantomJSコマンドラインから実行できるブラウザで、これを利用してサイトのスクリーンショットを取得した。
そのスクリーンショットを、EasyImageというモジュールを使って縮小してる。
最初からスクリーンショットのサイズを指定して生成することもできるけど、大きめの画面でスクリーンショットを撮影して縮小したほうがきれいに撮れる。

PhantomJSの操作を非同期で行ってるんだけど、並行タスクの数が増えてくるとかなりマシンの負荷が増えてくるので、とりあえずタスクを10ずつに分割して、その10のタスクが終了したら次の10のタスクを実行するようにした。
本当は並行タスクの数を10までに制限するというような形にするべきだけど、あんまりきれいな実装が思いつかなかったのでひとまずこれにした。
bluebirdにそんなAPIがあるらしいので後で調べる。

既存のものを使えばだいたいなんでもできるんだなあと少し感動した。

Velocityのイージングを拡張する

Velocityっていうアニメーション用のライブラリがある。
最初からいろんなイージングが使えて、jQuery UIのイージングの一部が入ってるんだけど、Back系、Elastic系、Bounce系のやつは入ってない。
Velocityは独自にイージングを拡張することもできるので、jQuery UIで使えるイージングは全部使えるようにしてみた。

'use strict';
import Velocity from 'velocity-animate';

let baseEasings = {};

baseEasings.Elastic = p => {
  return p === 0 || p === 1
    ? p
    : -Math.pow(2, 8 * (p - 1)) *
        Math.sin (((p - 1) * 80 - 7.5) * Math.PI / 15);
};

baseEasings.Back = p => p * p * (3 * p - 2);

baseEasings.Bounce = p => {
  let pow2;
  let bounce = 4;
  while (p < ((pow2 = Math.pow(2, --bounce)) - 1) / 11) {}

  return 1 / Math.pow(4, 3 - bounce) - 7.5625 *
    Math.pow((pow2 * 3 - 2) / 22 - p, 2);
};

Object.keys(baseEasings).forEach(name => {
  Velocity.Easings[`easeIn${name}`] = baseEasings[name];

  Velocity.Easings[`easeOut${name}`] = p => 1 - baseEasings[name](1 - p);

  Velocity.Easings[`easeInOut${name}`] = p => {
    return p < 0.5
      ? baseEasings[name](p * 2) / 2
      : 1 - baseEasings[name](p * -2 + 2) / 2;
  }
});

export default Velocity;

以上のコードで、

  • easeInElastic
  • easeOutElastic
  • easeInOutElastic
  • easeInBack
  • easeOutBack
  • easeInOutBack
  • easeInBounce
  • easeOutBounce
  • easeInOutBounce

を拡張できた。
これをimportしたらイージングを拡張したVelocityが使える。

2016/8/15 追記

ソースはこの辺参考
あと、イージングは一度拡張すれば、ファイルを分けてrequireする必要はない。
つまり以下のようにして利用できる。

main.js

const Velocity = require('velocity-animate');

// イージングを拡張
Velocity.Easings.anyEasing = () => {};

require('./animation');

animation.js

const Velocity = require('velocity-animate');

Velocity(document.getElementById('el'), {opacity: 0}, 'anyEasing');