馬上少年ブログ

無用の備忘録と雑談

Redux公式のIntroductionだけを読んで力尽きている

勢いに任せてRedux公式チュートリアルのIntroductionを読んでみたのでその覚書です。ほぼRedux公式の雑な翻訳要約です。

redux.js.org

Reduxとは

Reduxは、Reactのstateを管理をするためのフレームワークです。Fluxの概念を拡張して、より扱いやすく設計されています。Fluxとの違いは後述します。

基本的な考え方

ReduxでのStateは、大きな一つのJSONの塊で、Stateの中にsetterはない。

{
  todos: [{
    text: 'Eat food',
    completed: true
  }, {
    text: 'Exercise',
    completed: false
  }],
  visibilityFilter: 'SHOW_COMPLETED'
}

Stateに変更を加える場合は、ActionをDispatchする。Actionはこれまたオブジェクトの形で記述し、必ずtypeプロパティを持つ。typeはActionの内容を命名する。

{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }

StateとActionを結びつける役割を果たすのが、Reducer。Reducerは関数形式で書き、StateとActionを引数にとり、更新された新たなStateをreturnする。

function visibilityFilter(state = 'SHOW_ALL', action) {
  if (action.type === 'SET_VISIBILITY_FILTER') {
    return action.filter
  } else {
    return state
  }
}

function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([{ text: action.text, completed: false }])
    case 'TOGGLE_TODO':
      return state.map((todo, index) =>
        action.index === index
          ? { text: todo.text, completed: !todo.completed }
          : todo
      )
    default:
      return state
  }
}

そして最後につくった複数のReducerをひとまとめにするReducerを書く。

function todoApp(state = {}, action) {
  return {
    todos: todos(state.todos, action),
    visibilityFilter: visibilityFilter(state.visibilityFilter, action)
  }
}

以下、各登場人物についての詳細をまとめます。

登場人物

Action

何をどうする、といった処理の内容を記述したもので、オブジェクトの形で書きます。それぞれのActionには必ずtypeプロパティを持たせます。

{
  type: 'ADD_TODO',
  text: 'some text'
}

Actionは、ActionCreatorというメソッドにより生成します。

const addTodo = text => (
  return {
    type: 'ADD_TODO', 
    text: text
  }
}

Reducer

StateとActionを受け取り、新しいStateを返します。既存のStateの書き換えを行うのではなく、新しいStateオブジェクトを返します。Actionのtypeごとに処理を書き分けます。

const reducer = (state, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return {...state, text: action.text}
    default:
      return state
  }
}

Store

処理の種別に書いた複数のReducerをひとまとめにするReducerのことをStoreと呼びます。

Reduxでは、この巨大な一枚のStoreにすべてのState変更ロジックが記述されます。 Storeは全てのReducerをまとめるので、Storeを生成する際はstateを変更する処理を書いたreducerをすべて登録します。

const store = createStore(reducer)

Storeのインスタンスに対してdispatchメソッドで引数にactionを渡すことで、登録したreducerにactionとstateを渡して新しいstateを作成します。

store.dispatch(addTodo(text))

Reduxの三原則

Single source of truth

アプリケーションのすべてのStateは、ただ一つのオブジェクトツリーで管理する。

State is read-only

Stateは読み取り専用で、変更はActionによってのみ行う。

Changes are made with pure functions

純粋な関数ReducerによってStateが変更される。Reducerは直前のStateと、あるActionを受け取ることで、新しいStateツリーを返す。

Fluxとの違い

他にもありそうですがとりあえず気づいた点。

FluxのStoreがReduxのReducerのような感じ

Fluxでは、Stateの種類ごとにStoreを書き、各StoreでActionTypeごとに処理をかき分ける。Reduxではその役割をReducerが行なっている。ReduxのReducerを最後にひとまとめにするReducerのことを、ReduxではStoreと呼ぶ。Reduxでは1枚の巨大なStore、Fluxでは個別のStoreがある。

Dispatcherという概念がない

Fluxでは、ActionからDispatcherを呼び出し、Action内でStoreに対して自らをdispatchする必要がある。ReduxではActionはJSON、Reducerは純粋な関数で記述するため、Actionを送ってくれる役割のDispatcherは存在しない。

FluxはStateは唯一じゃなくていい

設計上は実現可能ですが、別に唯一じゃなくてもいいのがFlux。コンポーネントごとにStateツリーを管理できる。

ReduxではStateは変更されないものとされる

StoreでStateを更新するFluxと違い、Reduxでは新しいStateを丸ごとreturnするという。これについてはよく違いがわからないので、実装する過程で理解していきたいです。

まとめ

理解が足りないので間違いがあると思います。深まり次第随時修正していきます。

書かないとわからないことも多いです。

SVGのpath要素を使って図形を描画したのでメモ

最近使ったのでメモしておきます。

svgイラレから書き出して使ったことはありましたが、直に記述したことはなかったので楽しかったです。

path要素

path要素は、座標を指定して直線や曲線を描くsvg要素の一つ。以下のようにd属性に座標を指定して使います。

<svg width="1000" height="1000" viewBox="-2 -2 1000 1000">
  <path d="M 0 0 L 400 0 L 400 200 L 0 200 Z" fill="#0cc" stroke="#fc0" stroke-width="3"></path>
</svg>

上のコードでは下のように、400 * 200 の四角形を描くことができます。

fillとかstrokeは、塗りや線の色など、他のsvg要素でもよく使うstyleを指定する属性です。

今回注目するのはd属性です。

d属性とは

DrawのDです。コマンドと座標値を指定することで、図形を描画することができます。

コマンドのサンプル

M : pathの始点 , L : 直線

Mに続けて2つ値を指定することで、viewBox内でのpathの始点を定めることができます。最初は必ずMコマンドを使います。

Lコマンドでは、指定する座標に向かって直線を描きます。

<svg width="1000" height="200" viewBox="-2 -2 1000 200">
  <path d="M 0 0 L 100 22 L 100 200 L 140 100 L 230 140" fill="transparent" stroke="#fc0" stroke-width="3"></path>
</svg>

v/V : 垂直の直線 , h/H : 水平の直線

Lコマンドでどちらかを0にすれば垂直/水平な直線を描くことができますが、VやHを使えば0を省略できます。

<svg width="1000" height="150" viewBox="-2 -2 1000 150">
  <path d="M 0 0 V 100 H 100" fill="transparent" stroke="#fc0" stroke-width="3"></path>
</svg>

Z : pathをクローズする

Zコマンドを使うと、pathをクローズして、始点と終点を線で結ぶことができます。

一つ前の例で最後にZコマンドを使うと以下のようになります。

<svg width="1000" height="150" viewBox="-2 -2 1000 150">
  <path d="M 0 0 V 100 H 100 Z" fill="#0cc" stroke="#fc0" stroke-width="3"></path>
</svg>

小文字コマンド

L, V, Hなどの座標を指定するコマンドでは、大文字で絶対値を、小文字で相対値を指定することができます。

以下の2つのコードは同じ物を描画します。

<svg width="1000" height="150" viewBox="-2 -2 1000 150">
  <path d="M 0 0 L 50 0 V 60 L 20 100 H 100" fill="transparent" stroke="#fc0" stroke-width="3"></path>
</svg>

<svg width="1000" height="150" viewBox="-2 -2 1000 150">
  <path d="M 0 0 L 50 0 V 60 L 20 100 H 100" fill="transparent" stroke="#fc0" stroke-width="3"></path>
</svg>

曲線を描くコマンドたち

今回は割愛しますが曲線も描けます。暇なときに試します。

まとめ

サンプルが適当すぎてごめんなさい。

開発環境(webpack-dev-server)でaxiosによる非同期通信がクロスドメインではじかれる

直面しました。

本番環境ではAPIと、それを呼び出すファイルは同ドメインに置くので問題ないのですが、webpack-dev-serverでlocalhost:8080を立ち上げ、そこから本番ドメインAPIを呼ぶとクロスドメインで怒られます。

以下は実際にWP REST APIlocalhostから呼び出して怒られた例

localhost/:1 Access to XMLHttpRequest at 'https://xxx.com/wp/wp-json/wp/v2/posts' from origin 'http://localhost:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

非同期通信を行う際のクロスドメインへの対応として、プロキシとかJSONPとかがあります。

プロキシ

プロキシとは、ブラウザから直接データを要求できない時に、代理として経由して代わりに目的のサイトへアクセスしてくれる代理サーバのことらしいです。

同一ドメインのサーバにPHPなどのサーバーサイドのコードでプロキシを用意しておいて、クライアントサイドからはプロキシを経由してアクセスします。

でもサーバーサイド言語をわざわざ咬ますのも面倒だし、PHPとかよく知らんし、そもそも本番では同一ドメインなのでそこまでやりたくない。

JSONP

JSON with Padding

端的に言うと、<script>タグなら外部サーバからもスクリプトを読めることを利用してクロスドメイン通信を実現します。

イベント発生のタイミングで外部サーバのスクリプトをダウンロードする<script>タグを生成し、外部サーバは<script>タグを介してJSONデータを含んだ関数を返します。そして、その戻り値である関数をコールバック関数として処理します。

しかしセキュリティの問題とか、<script>タグ使いたくないよとか、axiosがデフォルトでJSONP対応してないよとか色々あって、そもそも本番では同一ドメインだからそこまでやりたくない。

結局webpack.config.jsに書く

どうにかしてAccess-Control-Allow-Origin: * をレスポンスヘッダに追加するやり方ももちろんありますが、webpack-dev-serverを使う場合はwebpackのproxy機能がありました。

webpack.config.js

devServer: {
    contentBase: __dirname + '/public',
    port: 8080,
    publicPath: '/js/',
    proxy: {
      '/wp/*': {
        target: 'https://xxx.com',
        changeOrigin: true
      }
    }
  },

targetで呼び出す側とAPIサーバのOriginが違う時だけ changeOrigin: true を追加すればいいようです。(localhostAPIサーバを用意する場合は不要)

/wp/以下へのリクエストをプロキシによってtargetで指定したURLに流してくれます。めちゃ便利。

Javascriptでアップロード画像のプレビューを表示する(jQueryなし)

意外と使う場面が多くなってきたのでメモ。

<div>
  <label><input id="file" type="file">画像を選択</label>
  <div id="preview"></div>
</div>

input[type=file]のchangeイベントに処理を書きます。

e.target.filesにアップロードしたFileオブジェクトの配列のようなシーケンスが渡されるので、1枚しかアップロードしない場合でも以下のようにして取り出します。

var file_input = document.getElementById('file');
var file_preview = document.getElementById('preview');

file_input.addEventListener('change', function(e){
  var file = e.target.files[0];

  console.log(file.name) // => ファイル名
  console.log(file.size) // => サイズ(バイト)
  console.log(file.type) // => ファイル種類
  
  var fileReader = new FileReader();  // 手順1
  
  fileReader.onload = function(){
    var img_src = this.result;  // 手順3
    
    if ( file_preview.childElementCount != 0 ) {
      file_preview.textContent = null;
    }

    var img = document.createElement('img');  // 手順4
    img.src = img_src;

    file_preview.appendChild(img);
  }

  fileReader.readAsDataURL(file);  // 手順2
}, false);

手順1

ローカルのファイルを非同期に読み込むことができる、FileReaderオブジェクトのインスタンスを作ります。onloadで画像のURIを受け取り(this.result)、画像を表示する処理を行っています。

手順2

ファイルを実際に読み込んでくれるreadAsDataURLメソッドを呼び出します。引数にはアップロードするファイルを渡しておきます。

手順3

readAsDataURLメソッドでファイルを読み込んだ後は、fileReader の result プロパティにファイルのデータを表す、base64 エンコーディングされた data: URL の文字列が格納されます。

手順4

あとはこれをimg要素のsrc属性に入れて表示するだけです。

上記コードでは、繰り返しファイルを選択したらプレビュー画像を入れ替えるようにしています。

ちなみに

changeイベントは、ファイルをアップロードした状態で、同じinputでもう一度選択しようとしてキャンセルした場合にも走ります。その場合はe.target.filesはundefinedとなるので、より厳格にするなら分岐しなければなりません。

参考

developer.mozilla.org

developer.mozilla.org

Wordpressで技術系ブログを書く時に使えそうなプラグイン・ツール・機能

はてなブログからWordpressへの移行を進めているところですが、直近railsに夢中でサボり気味です。

尻を叩く意味を込めて、Wordpressに関する覚書。

5系を入れてしまった

何も考えず最新版のWordpressをインストールしたら、昨年末に出たバージョン5系を入れてしまいました。今まで使ったことのある4系とはかなり勝手が違い、さらに対応しているプラグインもまだ少ない...。

しかしせっかく入れてしまったので、5系のまま慣れていきたいと思います。

React + WP REST API によるブログ構築

3月末を目標にReact + WP REST API によるブログ構築を進め、年度内に公開したいと考えています。

Wordpressのテーマは使わず、DBとやりとりする記事投稿・管理のみWordpressの力を借ります。

エディタ

はてなブログでの記事編集はMarkdown方式で行なっています。WordpressでもMarkdownで編集したいので、プラグインを入れます。

Markdown Editorというわかりやすい名前のプラグインがよく使われているようだったので、入れてみました。

ja.wordpress.org

ただ、これが5系だと思うような動きになりませんでした...。

最新版で検証済みのエディタの中から発見したのが、WP Editor.mdというプラグインです。使いやすく、挙動も期待通りなので、採用しました。

ja.wordpress.org

ソースコードの貼り付け

技術系をやっていく上で、<pre><code></code></pre>で囲んだ範囲を自動でシンタックスハイライトしてくれるツールが必要です。

Highlight.jsというライブラリを使いたいんですが、Wordpressにはこれを含んだPrismaticというプラグインがありました。

ja.wordpress.org

この中のHighlight.jsを適用すると、記事の中のコードがハイライトさせて表示することができます。

しかし、今回はテーマを使わずReactで構築するので、WordpressプラグインとしてHighlight.jsを入れるわけにはいきません。

npmパッケージにも同じものがあるので、そっちの環境でインストールすることにします。

WP REST API

Wordpressの投稿情報を返してくれるAPIです。プラグインが必要なのかと思っていましたが、すでに標準機能として使えるようで、

ドメイン(/wpディレクトリ)/wp-json/wp/v2/posts/

にアクセスするだけでOKでした。投稿だけでなくいろんな情報を返してくれるようです。必要に応じて調べていきます。

今後に向けて

現状Routingの設定をしたところなので、APIを組み込めばかなり形が見えてくると思います。

Wordpressの投稿まわりの整備もしなければ...

FluxのActionCreatorの中でaxiosを使ってAPIを叩く

Reactのfluxアーキテクチャでは、外部APIとのやりとりはActionCreatorのところで行われるとのことです。

今回は無料のOpenWeathermap API を使って、お天気を表示するだけのアプリを作成します。

axiosとは

PromiseベースでHTTP通信を簡単に行うことができるライブラリ。

以下のようにPromiseベースなので簡単にリクエストを生成でき、とてもみやすいです。

const url = 'https://xxx.com'
const params = { hoge: 'fuga' }

axios
  .get(url, { params })
  .then(data => {
    //成功時の処理
    console.log(data)
  })
  .catch(error => {
    //失敗時の処理
    console.log(error)
  })

以下のように設定をオブジェクトで渡すこともできます。パラメータがある時などこっちの方がわかりやすいですね。

const url = 'https://xxx.com'
const params = { hoge: 'fuga' }

axios({
  method : 'GET',
  url    : url,
  params : params
}).then(data => {
  //略
})

post通信の時はpostメソッドを使い、paramsではなくdataを使います。

const url = 'https://xxx.com'
const data = { hoge: 'fuga' }

axios({
  method : 'POST',
  url    : url,
  data   : data
}).then(response => {
  //略
})

環境構築

Nodo.jsをインストールして、プロジェクトディレクトリを作成してください。

まずは必要なパッケージをインストール。

$ npm install react react-dom
$ npm install --save-dev flux @babel/core @babel/preset-env @babel/preset-react babel-loader webpack webpack-cli webpack-dev-server

今回使用するaxiosもインストールしておきます。

$ npm install axios

package.jsonおよびbabel, webpackの設定ファイルは以下のとおり。

package.json

{
  "name": "otenki_app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "webpack-dev-server",
    "build": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "axios": "^0.18.0",
    "react": "^16.8.1",
    "react-dom": "^16.8.1"
  },
  "devDependencies": {
    "@babel/core": "^7.2.2",
    "@babel/preset-env": "^7.3.1",
    "@babel/preset-react": "^7.0.0",
    "babel-loader": "^8.0.5",
    "flux": "^3.1.3",
    "webpack": "^4.29.3",
    "webpack-cli": "^3.2.3",
    "webpack-dev-server": "^3.1.14"
  }
}

webpack.config.js

module.exports = {
  mode: 'development',
  entry: './src/index',
  output: {
    filename: 'app.js',
    path: __dirname + '/public/js'
  },
  devServer: {
    contentBase: __dirname + '/public',
    port: 8080,
    publicPath: '/js/'
  },
  devtool: 'eval-source-map',
  module: {
    rules: [{
      test: /\.js$/,
      exclude: /node_modules/,
      loader: 'babel-loader'
    }]
  }
};

.babelrc

{
  "presets": [
    [
      "@babel/preset-env", {
        "targets": {
          "node": "current"
        }
      }
    ], 
    "@babel/preset-react"
    ]
}

必要なディレクトリを作っておき、エントリーポイントのsrc/index.jsと、読み込み用のpublic/index.htmlを記述します。

$ mkdir public src src/containers src/data src/Views

src/index.js

import React from 'react'
import ReactDOM from 'react-dom'
import AppContainer from './containers/AppContainer';

ReactDOM.render(
  <AppContainer />,
  document.getElementById('root')
);

src/index.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <div id="root"></div>
  <script src="js/app.js"></script>
</body>
</html>

Fluxの各部門を実装する

Dispatcher

モジュールをimportしてインスタンス化するだけでOKです。ありがたい。

src/data/Dispatcher.js

import { Dispatcher } from 'flux';

export default new Dispatcher();

Container

ViewとStoreとStateを渡しておけばOK。ほんとうにありがたい。

src/containers/AppContainer.js

import AppView from '../Views/AppView';
import ActionCreator from '../data/ActionCreator'
import { Container } from 'flux/utils';
import OtenkiStore from '../data/OtenkiStore';

const getStores = () => {
  return [
    OtenkiStore,
  ];
};

const getState = () => {
  return {
    otenki: OtenkiStore.getState(),
    onGetOtenki: ActionCreator.getOtenki,
  };
}
export default Container.createFunctional( AppView, getStores, getState );

ActionCreator

今回の肝です。axiosで非同期通信しています。APIは都市名とAPIキーが必要です。APIキーは別途取得しておいてください。

qiita.com

返ってきたデータをActionTypeとともにDispatcherに送ります。ActionTypeは別ファイルで定義しています。

import ActionTypes from './ActionTypes';
import Dispatcher from './Dispatcher';
import axios from 'axios';

const Actions = {

    getOtenki(city) {
    const url = 'https://api.openweathermap.org/data/2.5/weather'
    const params = {
      q: city,
      units: 'metric',
      appid: 'YOUR_API_KEY'
    }

    axios({
      method : 'GET',
      url    : url,
      params : params
    })
    .then((data) => {
      Dispatcher.dispatch({
        type: ActionTypes.GET_OTENKI,
        data: data.data,
      })
    })
    .catch((error) => {
      console.log(`失敗しました:${error}`)
    })
  }

};

export default Actions;

src/data/ActionTypes.js

const ActionTypes = {
  GET_OTENKI: 'GET_OTENKI',
};

export default ActionTypes;

Store

データをそのまま返します。

src/data/OtenkiStore.js

import {ReduceStore} from 'flux/utils';
import ActionTypes from './ActionTypes';
import Dispatcher from './Dispatcher';

class OtenkiStore extends ReduceStore {
  constructor() {
    super(Dispatcher);
  }

  getInitialState() {
    return '';
  }

  reduce(state, action) {
    switch(action.type) {

      case ActionTypes.GET_OTENKI: {
        if(action.data === undefined) {
          return state;
        } else {
          return action.data;
        }
      }

      default: {
        return state;
      }

    }
  }
}

export default new OtenkiStore();

View

東京、大阪、岡山の3都市を用意しました。citiesをmapしてボタンを生成し、valueの都市名をコールバックの引数としてそのままActionに渡します。この都市名はAPIを叩くときに使用しています。

Views/AppView.js

import React from 'react'

const AppView = props => (
  <div>
    <OtenkiBtn {...props} />
    <OtenkiView {...props} />
  </div>
)

const cities = [ 'Tokyo', 'Osaka', 'Okayama' ]

const OtenkiBtn = props => {
  const onGetOtenki = (e) => props.onGetOtenki(e.target.value);
  return (
    <div>
      {cities.map(city => (
        <button key={city} onClick={onGetOtenki} value={city}>{city}</button>
      ))}
    </div>
  )
}

const OtenkiView = props => {
  if(props.otenki) {
    return (
      <div>
        <p>{props.otenki.name}の天気</p>
        <div>{props.otenki.weather.main}</div>
        <div>最高気温:{props.otenki.main.temp_max}&deg;C</div>
        <div>最低気温:{props.otenki.main.temp_min}&deg;C</div>
      </div>
    )
  } else {
    return (
      <div>
        <p>ここにお天気情報を表示します。</p>
      </div>
    )
  }
}

export default AppView

OtenkiViewでは、渡ってききたお天気データを表示しています。もっといろんな情報をもらえるのですが、今回は簡素にしてます。

実行

npm run dev

localhost:8080にwebpack-dev-serverが立ち上がり、ボタンの切り替えで都市ごとの天気を取得することができました。

Node.js開発環境でSassをコンパイルする

Sassをコンパイルするときにターミナル開いたりGUIツール開いたりすると思いますが、reactでサイトやアプリ作ってる時はNode.jsで一気にコンパイルしてくれたらありがたいですよね。

そんなとき便利なnode-sassを使ってみました。

インストール

global環境にinstallすればいいんでしょうが、なんとなくglobalいじりたくなくて、devにしときました。お好きな方にinstallしてください。

$ npm install --save-dev node-sass

scriptにnode-sassのコマンドを追加しておきます。css/style.scssを同名のcssコンパイルする設定です。

package.json

{
  "name": "sass_sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "node-sass css/style.scss css/style.css"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "node-sass": "^4.11.0"
  }
}

実行してみる

適当にscssを記述して、コンパイルを試します。

css/style.scss

$color-text: #3b3b39 !default;
$color-brand: #9aebe1 !default;
$max-width: 1080px !default;
$font-family: "Hiragino Kaku Gothic ProN", Meiryo, sans-serif !default;

$breakpoints: (
  'sp': '(max-width: 400px)',
  'tb': '(max-width: 768px)',
) !default;

@mixin mq($breakpoint: tb) {
  @media #{map-get($breakpoints, $breakpoint)} {
    @content;
  }
}

/*-----------------------------------------------------------*/

body {
  color: $color-text;
  font-family: $font-family;
}

.wrapper {
  background-color: $color-brand;
  max-width: $max-width;

  @include mq(tb) {
    max-width: auto;
    width: 100%;
  }
}

.logo {
  width: 50%;

  & .text {
    color: $color-brand;
  }
}

変数の定義、mixinによるbreacpoint管理、セレクタのネストなど書いてみました。コンパイルします。

npm run dev

css/style.cssコンパイル結果)

/*-----------------------------------------------------------*/
body {
  color: #3b3b39;
  font-family: "Hiragino Kaku Gothic ProN", Meiryo, sans-serif; }

.wrapper {
  background-color: #9aebe1;
  max-width: 1080px; }
  @media (max-width: 768px) {
    .wrapper {
      max-width: auto;
      width: 100%; } }

.logo {
  width: 50%; }
  .logo .text {
    color: #9aebe1; }

問題なくコンパイルできますね。しかしネストしたところにインデントが残っていて非常にキモいです。

これはnode-sassコマンドにオプションを加えることで解決しますが、これは記事の後半に掲載しています。

複数ファイルをまとめてコンパイル

モジュールごとに別ファイルでscssを書いて、最後に@importするのも同じやり方です。

css/style.scss

@import "modules/_header";
@import "modules/_footer";

css/modules/_header.scss

.header {
  width: 100%;
  &.logo {
    width: 50%;
  }
}

css/modules/_footer.scss

.footer {
  width: 100%;
  text-align: center;
  &.logo {
    width: 30%;
  }
}

結果

css/style.css

.header {
  width: 100%; }
  .header.logo {
    width: 50%; }

.footer {
  width: 100%;
  text-align: center; }
  .footer.logo {
    width: 30%; }

node-sassコマンドのオプション

node-sassコマンドにいくつかオプションを加えることができます。

全てのオプションは以下のページに記載されています。いくつか取り上げます。

www.npmjs.com

--output-style

先ほど非常にキモいインデントされたCSSコンパイルされていましたが、--output-styleオプションを加えることで、コンパイルのフォーマットを指定することができます。

  "scripts": {
    "build": "node-sass css/style.scss css/style.css --output-style expanded"
  },

--output-style expandedを設定してコンパイルした例がこちらです。

/*-----------------------------------------------------------*/
body {
  color: #3b3b39;
  font-family: "Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
}

.wrapper {
  background-color: #9aebe1;
  max-width: 1080px;
}

@media (max-width: 768px) {
  .wrapper {
    max-width: auto;
    width: 100%;
  }
}

.logo {
  width: 50%;
}

.logo .text {
  color: #9aebe1;
}

.header {
  width: 100%;
}

.header.logo {
  width: 50%;
}

.footer {
  width: 100%;
  text-align: center;
}

.footer.logo {
  width: 30%;
}

デフォルトがnestedで、他にも各行を改行せずに一行にしたcompactや、minify化してくれるcompressedがあります。

-w, --watch

ファイルの変更を監視してくれます。この例でいうと、style.scssを保存すると自動的にstyle.cssコンパイルされます。

  "scripts": {
    "build": "node-sass css/style.scss css/style.css -w"
  },

--source-map

通常だとブラウザで検証する際は、コンパイルされ実際に読み込まれたcssファイルのみ検証可能ですが、このオプションをつけることでscssファイルを使って検証を行うことができます。

まとめ

簡単なので即導入します。