CSSにおける和欧混植のベストプラクティス

欧文はウェブフォント、和文はユーザーエージェントのデフォルトにするというのがベスト。

html {
  font-family: Lato, sans-serif;
}

というのも、ローカルの欧文フォントを指定するためにはプラットフォームごとにインストールされているフォントを把握しておく必要があるからだ。現状のシェアを占めているものだけに絞ればまだしも、クライアント環境が多様化し続けている昨今ではそのアプローチは不十分に思える。

ちなみに、主要なプラットフォームにインストールされているフォントの一覧は以下。

ウェブフォントを利用した指定にしておけば、プラットフォームに関わらず和欧混植にできる。読み込みに失敗するというケースも考えられるが、その場合の見栄えは妥協できるだろう。

和文の指定に関しては、まずウェブフォントはコストが高いので避けたい。加えて、前述したような理由でフォントファミリー名を指定するのも微妙。そこでsans-serifなどのジェネリックな指定にしておけば、ユーザーエージェントが最適なフォントを選択してくれる可能性が高い。

とはいえ、タイポグラフィとしては和文フォントと欧文フォントの組み合わせがまずいものにならないようにはしたい。そういう場合は、それぞれのフォントが利用できる場合のみ特定の処理を行うというアプローチも取れる。

参考:

Sassのmixinでユニークなキーフレーム名を宣言する

スプライトアニメーションをコンテンツの背景にフィットさせる - ライデンの新人ブログ

こういうやつを複数回やりたかったのでmixinにした。普通にやるとキーフレーム名がかぶるので、mixinを呼び出すたびにIDをインクリメントさせるようにしたらいけた。

$_sprite-id: -1;

@mixin sprite($frame-count, $seconds-per-frame) {
  $_sprite-id: $_sprite-id + 1 !global;

  background-size: 100% ($frame-count * 100%);
  animation: sprite-#{$_sprite-id} #{$frame-count * $seconds-per-frame}s steps($frame-count) infinite;

  @keyframes sprite-#{$_sprite-id} {
    from {
      background-position: left top;
    }
    to {
      background-position: left (100% * $frame-count / ($frame-count - 1));
    }
  }
}

.sprite1 {
  @include sprite(3, .1);

  background-image: url("sprite1.png");
}

.sprite2 {
  @include sprite(12, .2);

  background-image: url("sprite2.png");
}

keyframesの内容が同じでも複数回宣言するので雑だと思うけど、ちゃんとやるのめんどくさそうなのでやめた。Mapで$frame-countをキーにしてIDを保存しとくとかすればいけそう。

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>`
        }
      }
    }
  ],
  // 省略
})