npm-scriptで開発環境を作ってみよう

最近、Gulpが4.xにバージョンアップしましたね。そのタイミングで開発環境でもGulpをアップデートしてエラーに困っている声を聞いたりします。

僕自身もGulpを使っている環境で4.xにアップデートして、それまで使っていたライブラリからエラーが出て四苦八苦しました。その時、ふと思ったんです。「Gulpに依存してるなー」と。そして、「だったらnpm-scriptの方がいいかも」と思い、npm-scriptで開発環境を作る流れを書いてみようと思います。

ソースは最下部です

今回作成したコードなどのソースはGitHubに掲載してあります。最下部にリンクが置いてあるので、お急ぎの方はそちらを参照してみてください。

タスクの流れ

  1. Sassの処理
    1. node-sassでコンパイル
    2. プリフィックス付与と圧縮処理
    3. ディレクトリを削除して新しくディレクトリを作る
    4. Sassの処理をまとめて一連のタスクを流して処理
  2. JavaScriptの処理
    1. バンドルする
    2. 圧縮する
    3. 一連のタスクを流して処理
  3. 画像の処理
    1. 画像圧縮の処理
    2. 画像圧縮の一連の流れのタスクを作る
  4. ファイルを監視する
    1. ブラウザシンクでサーバーを立ち上げる
    2. SCSS、JS、imagesを監視する
      1. HTMLの監視
      2. EJSの監視
    3. 常時監視用タスクを作る

このような流れで開発環境(タスクランナー)を作っていこうと思います。不要な部分は飛ばして読んでもらっても大丈夫だと思いますので、よしなに参照してください。

ディレクトリ構造

今回は静的ウェブサイトの構築を想定して開発環境を作ってみます。ディレクトリ構造は以下を想定して書いていますので、よしなに噛み砕いて活用してみてください。

package.json
-- dist
  index.html
  -- assets
    -- css
    -- js
    -- images
-- src
  -- assets
    -- sass
      style.scss
    -- js
      index.js
    -- images
      image.jpg
      image.gif
      image.png
      image.svg
    -- html
      index.html
postcss.config.js
imagemin.js
rollup.config.js

下準備

npm-scriptを使うためには下準備が少し必要です。node.jsは入れておいてくださいね。プロジェクトのルートにターミナルで移動して、

$ npm init

を実行してください。そして、必要な情報を入力してください。そうすると、package.jsonというファイルが自動的に作られると思うので、あとは以下を読み進めて色々と試して見てください。

Sassの処理

まずはSassの処理部分を作っていきましょう。

node-sassでscssファイルをコンパイル

scssファイルをコンパイルする処理を作ります。ここではnode-sassを使います。

$ npm install node-sass --save-dev

これでnode-sassのパッケージがインストールされます。

scriptsは以下のように書きます。

"css/sass": "node-sass src/assets/sass/style.scss -o dist/css --output-style expanded --source-map dist/assets/css"
-o出力するディレクトリを指定
–output-style書き出した後のスタイルを指定(nested | expanded | compact | compressed)
–source-mapSourceMapの書き出し先ディレクトリを指定

これで完了です。試しにsrc/sass内にstyle.scssを作ってから、

$ npm run css/sass

これを実行するとdist/css内にstyle.cssstyle.css.mapが作られていることが確認できるはずです。

以下、コマンドの打ち方(ここで言う $npm run css/sass )は省略します。適時読み替えてください。

ベンダープレフィックス付与と圧縮処理

コンパイルしたCSSに対して、ベンダープレフィックスを自動的に付けさせ、その後CSSを圧縮させます。

ベンダープレフィックスを付与するためにautoprefixerを、cssを圧縮するためにcssnanoを使います。そして、それらをPostCSSで処理するのでpostcss-cliも入れます。

$ npm install postcss-cli cssnano autoprefixer --dave-dev

scriptsには以下のように書きます。

"css/postcss": "postcss dist/assets/css/style.css -o dist/assets/css/style.min.css"

そして、このスクリプトを実行すると、

You did not set any plugins, parser, or stringifier. Right now, PostCSS does nothing. Pick plugins for your case on https://www.postcss.parts/ and use them in postcss.config.js.

このようにエラーが出ます。postcss.config.jsを用意しなさい、というようなことですね。ですので、ルートに用意します。中身はこんな感じです。

module.exports = {
  plugins: [
    require('autoprefixer')({
      browsers: ['last 2 versions'],
      cascade: false
    }),
    require('cssnano')({
      preset: 'default',
    })
  ]
}

ベンダープレフィックスの付与だけの場合

cssの圧縮を行わない(必要としない)場合もあると思いますので、こちらのケースだけの場合を書いておきますね。

postcss-clisutoprefixerをインストールします。

$ npm install postcss-cli autoprefixer --save-dev

scriptsにはこう書きます。

"css/postcss/autoprefix": "postcss dist/assets/css/style.css -o dist/assets/css/style.css -c=postcss.config.js"

そして、postcss.config.jsには、

module.exports = {
  plugins: [
    require('autoprefixer')({
      browsers: ['last 2 versions'],
      cascade: true
    })
  ]
}

というような感じで書いておきます。cascadeというのは、出力値を整列させるかどうか、という指定です。

body {
// cascade: false の場合
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;

// cascade: true の場合
-webkit-user-select: none;
    -moz-user-select: none;
      -ms-user-select: none;
             user-select: none;
}

先ほどの流れでcascade: true と記述していますが、デフォルトではcascade: trueなので、特に記述する必要はありません。

ディレクトリを削除して新しくディレクトリを作る

どんどん処理を重ねていくと、不要なファイルが存在するようになったりして、手動で対応しなくてはならなくなったりと面倒です。

ですので、毎回コンパイル〜ベンダープレフィックス付与〜圧縮を行う前にdist/cssディレクトリを削除して、新たにdist/cssディレクトリを作成するという処理を行います。

trashを使ってディレクトリを削除することもできるのですが、こちらはディレクトリが存在しない場合などにエラーで処理が止まってしまうので微妙な感じです。ですので、ここではrimrafを利用します。

こちらを使う際には特に何かしらインストールする必要はありません。

scriptsに以下の処理を加えます。

"clean/css": "rimraf dist/css && mkdir -p dist/css"

今後、他のディレクトリなどのcleanタスクを書くのでcssのディレクトリを削除・新規作成するタスクはこのように書きました。他にもjs、imagesなどでも同様の処理を行いますので覚えておきましょう。

スクリプトを実行すると、テストで書き出したdist/css内のデータが消えていることが確認できます。これは、い一度dist/cssを削除して再度dist/cssを作成しているので、見た目では中のデータが消えたというように見えるということです。

Sassの処理をまとめて一連のタスクを流して処理

ここまで作ったタスクをまとめて1つのスクリプトで実行できるようにします。この考え方はnpm-scriptを利用して開発環境を作る上ではベースとなる考え方・作り方なのでよく覚えておきましょう。

流れとしては、

  1. ディレクトリをクリーンにする
  2. scssファイルをコンパイル
  3. ベンダープレフィックスを付与
  4. 圧縮

となります。

タスクを流れで処理するためにnpm-run-allを利用します。

$ npm install npm-run-all --save-dev

スクリプトはこのように書きます。

"css": "npm run clean/css && npm-run-all -p css/*"

npm-run-allにはこのようなオプションがあります。

-p並行処理
-s順次処理

ここでは、コンパイルしてからプリフィックス付与、圧縮と順次処理してほしいのでこのように設定しています。順次というのは、package.jsonscriptに書いてある順に、という意味です。

JavaScriptの処理

ここからはJavaScriptの処理について書いていきます。といっても、特に難しいことをするつもりはなく(あまり僕の環境でそこまで必要じゃないので)、ただバンドルして圧縮するだけです。

バンドルする

まずは複数あるjsファイルを1つのファイルにまとめます。ここではrollupを利用します。

$ npm install rollup --save-dev

あと、付随するrollupのプラグインも入れておきましょう。

$ npm install rollup-plugin-node-resolve rollup-plugin-commonjs rollup-plugin-babel --save-dev

そして、大元の@babel/coreも入れないとエラーが出ます。あと、@babel/preset-envも。

$ npm install @babel/core @babel/preset-env --save-dev

そして、ルートにrollup.config.jsを用意して以下のように書いてみます。

import resolve  from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import babel    from 'rollup-plugin-babel';

export default {
    output: {
        format: 'iife',
        dir: 'dist/assets/js'
    },
    plugins: [
        resolve({ jsnext: true }),
        commonjs(),
        babel({
            presets: [
                [
                    "@babel/preset-env", {
                    "modules": false,
                    "targets": {
                        "browsers": ['last 2 versions']
                    }
                }
                ]
            ],
            babelrc: false
        })
    ],
    experimentalCodeSplitting: true
};

output:の部分でformatdirを設定しています。

format書き出すフォーマットを設定(amd, cjs, esm, iife, umd
dir書き出し先を設定

詳しくはこちらを見てみるとオプションとか書いてあるのでわかりやすいです。

圧縮する

バンドルしたJavaScriptを圧縮します。ここではuglify-jsを利用します。

$ npm install uglify-js --save-dev

スクリプトにはこのように記述しています。

"js/uglifyjs": "uglifyjs dir/assets/js/index.js -o dir/assets/js/index.min.js"

一連のタスクを流して処理

JavaScriptに関する一連のタスクを流して処理を作ります。詳しくは上記のCSS部分を参照してください。ここでは同じような処理なので詳しい説明は割愛します。

まず、CSSの時と同じようにdistディレクトリを綺麗にして作り直すタスクを作ります。

"clean/js": "rimraf dist/assets/js && mkdir -p dist/assets/js"

そして、JavaScriptのバンドル→圧縮の処理をディレクトリを綺麗にした後に動作させます。

"js": "npm run clean/js && npm-run-all -s js/*"

画像の処理

画像の処理といってもそれほどやることは多くありません。画像の容量を最適化してdistディレクトリにコピーするくらいです。

画像圧縮の処理

ここでがちょっと多めのパッケージを使います。

imagemin画像を圧縮するパッケージの親玉みたいもの
imagemin-keep-folderディレクトリ構造を保ったままにする
imagemin-mozjpegJPEGの圧縮用
imagemin-pngquantPNGの圧縮用
imagemin-gifsicleGIFの圧縮用
imagemin-svgoSVGの圧縮用

全て一緒にインストールしてあげましょう。

$ npm install imagemin imagemin-keep-folder imagemin-mozjpeg imagemin-pngquant imagemin-gifsicle imagemin-svgo --save-dev

そして、ルートにimagemin.jsという設定用ファイルを用意して、そちらをnodeで実行する形でタスクを実行します。imagemin.jsの記述はこのような感じです。

const imagemin         = require('imagemin-keep-folder');
const imageminMozjpeg  = require('imagemin-mozjpeg');
const imageminPngquant = require('imagemin-pngquant');
const imageminGifsicle = require('imagemin-gifsicle');
const imageminSvgo     = require('imagemin-svgo');

imagemin(['src/assets/images/**/*.{jpg,png,gif,svg}'], {
    plugins: [
        imageminMozjpeg({ quality: 80 }),
        imageminPngquant({ quality: '65-80' }),
        imageminGifsicle(),
        imageminSvgo()
    ],
    replaceOutputDir: output => {
        return output.replace(/images\//, '../../dist/assets/images/')
    }
}).then(() => {
    console.log('Images optimized');
});

スクリプトの方にはこのようなタスクを記述します。

"images/imagemin": "node imagemin.js"

これで圧縮させる部分の出来上がりです。

画像圧縮の一連の流れのタスクを作る

まずは恒例のディレクトリをクリーンにするタスクを作ります。

"clean/images": "rimraf dist/assets/images && midir -p dist/assets/images"

そして、画像圧縮のタスクをclean/imagesのあとで処理させます。

"images": "npm run clean/images && npm-run-all -s images/*"

これで画像の圧縮から一連の流れが完成です。

ファイルを監視する

ここからは実際の開発を進めていく上で、効率よくコードがかけるようにするためのチップスになります。ファイルを監視するというタイトルになっていますが、読んで字のごとく、ファイルを更新したかどうかを常に監視する部分になります。

ブラウザシンクでサーバーを立ち上げる

まずはbrowser-syncでサーバーを立ち上げます。まずはインストール。

$ npm install browser-sync --save-dev

そして、スクリプトには、

"watch/server": "browser-sync start -s dist -f 'src, **/*.html, !node_modules/**/*'"

ここでは、サーバーではdistのデータを表示するようにしています。詳しくはこちらを読むと全てわかります。

英語だけどきになるコマンドとかをページ内検索すればすぐに見つけられるよ

SCSS、JS、imagesを監視する

ではまずSCSSファイルの監視から。watchパッケージをインストールします。

$ npm install watch --save-dev

そして、SCSSファイルがあるsrc/assets/sass/を監視させます。スクリプトにはこんな感じで書きました。

"watch/css": "watch 'npm run css' ./src/assets/sass"

./src/assets/sassで変更があればnpm run cssを実行しろ、ということが書いてあります。


JavaScriptの監視もSCSSと同じ要領でOKです。

src/assets/js/を監視させるので、

"watch/js": "watch 'npm run js' ./src/assets/js"

画像の監視にはonchangeを利用します。

$ npm install onchange --save-dev

そしてスクリプトには、

"watch/images": "onchange 'src/assets/images' -e '**/*.DS_Store' -- npm run images"

これで画像に変更を加えると、npm run imagesが走るようになります。

HTMLの監視

おまけですが、テンプレートエンジンを使わずに直にHTMLファイルをsrcディレクトリ下で作成していく場合には、cpxというパッケージを使って、distディレクトリへコピーします。cpxの良いところはディレクトリ構造を保持したままコピーしてくれるところです。

$ npm install cpx --save-dev

そして、スクリプトでこんな感じに書いています。

"html/plain": "cpx 'src/assets/html/**/*.html' 'dist/'",
"html": "npm-run-all html/*",

監視させる記述に関してはこのように書いてみました。

"watch/html": "watch 'npm run html' ./src/assets/html",

EJSの監視

EJSなどのテンプレートエンジンを使うこともあるかと思います(Jadeとかもやり方は同じような感じのはず)。ここではEJSのコンパイルを取り上げてみます。まずは、ejs-cliパッケージをインストールします。

$ npm install ejs-cli --save-dev

スクリプトにはこのように書いてみました。

"html/ejscli": "ejs-cli --base-dir src/assets/ejs '**/*.ejs' --out dist"

これでsrc/assets/ejs内の*.ejsファイルをコンパイルしてくれます。階層なども保持したままdistに出力されます。

常時監視用タスクを作る

ここまで作ってきたタスクを1つにまとめて一括して実行できるようにすることでとても便利になります。

"watch": "npm-run-all -p watch/*"

こうすることで、開発を開始する際にはコマンドラインでnpm run watchを打つだけで、サーバーが立ち上がり、プロジェクトのファイルを監視開始してくれます。

まとめ

このような流れで開発環境を作ることができます。最初は黒い画面に慣れていない方は、ちょっと腰が重くなってしまうかもしれませんが、失敗したら消しちゃえば良いんで安心してください。

GulpやGruntなどといったタスクランナーはnpmの上で動いています。ですので、大元のnpm scriptsで開発環境を作ることで、外枠(GulpやGruntなどのバージョンアップなど)の影響を受けにくいものを手に入れることができます。

どんどん失敗して、最適な開発環境を構築してみてください。

今日作ったものはこちらに置いておきますね。

WordPress 6.5.x 対応版を出版しました

WordPress デフォルトテーマ Twenty Twenty-Four を使って、シンプルなブログやポートフォリオサイト、そしてコーポレートサイトを作りながら、ブロックテーマやサイトエディターの基本を理解することができます。