Posted at

Gulpのタスクをnpm-scriptsで書き換える


gulpをやめる経緯

は、、

色々なところで書いてあるので、ここでは割愛します。

ここでは、あくまで実コードベースで書き換えるように話を進めます。


ただ、npm-scriptsのみで書き換えるのは、無理っぽい。

処理の量的に、package.jsonに全て書くのは厳しかったです。

以下をご覧ください。

私が使っているgulpタスクの一部分です。

scssのタスクですが、がっつりgulpに依存しています。

読み込んでいるモジュール名も「gulp-●●」がふんだんに入っています。

■元ソース

https://github.com/underground0930/gulp_and_webpack/blob/master/gulp/gulpfile.babel.js


gulpfile.babel.js


import gulp from 'gulp';
import watch from 'gulp-watch'; // watchタスク
import plumber from 'gulp-plumber';// エラーが起きても止めない
import bs from 'browser-sync'; // ブラウザ更新
const browser = bs.create();
import sass from 'gulp-sass'; // sass
import autoprefixer from 'gulp-autoprefixer'; // ベンダープレフィックス自動付加
import gulpIf from 'gulp-if'; // 条件文を使えるようにする
import sassGlob from 'gulp-sass-glob'; // sassでglobを使用

const environment = process.env.NODE_ENV;
const paths = require('./conf.path'); // 変数が入ってます

////////////////////////////////////////
// SCSS
////////////////////////////////////////

export function styles() {
return gulp
.src(paths.assets + '/scss/**/!(_)*.scss') // 対象ファイル検索
.pipe(plumber()) //エラーで止めない
.pipe(sassGlob()) //sassでglobを使用
.pipe(
gulpIf( // 環境変数でminifyするか切り替え
environment !== 'dev',
sass({ outputStyle: 'compressed' }),
sass({ outputStyle: 'expanded' })
)
)
.on('error', sass.logError)
.pipe(
autoprefixer({ // ベンダープレフィックス自動付加
cascade: false
})
)
.pipe(gulp.dest(paths.assets2 + '/css'))
.pipe(browser.reload({ stream: true })); // ブラウザをリロード
}

//scssファイルの監視
watch([paths.assets + '/scss/**/*.scss'], () => {
styles();
});


処理を大まかに書くと


  • (1). scssファイルを検索

  • (2). scssファイル内でワイルドカードを使えるようにする

  • (3). minifyするかの選択、scssでコンパイル

  • (4). 生成されたcssにプレフィックスをつける

  • (5). ファイルを出力

  • (6). ブラウザのリロード

これを、package.jsonにワンライナーもしくは何行にもかけて書くと、

エラいことになりそうです。というか出来るのだろうか。

なので、今回は

それぞれのタスクを別のjsファイルに分けて、それをnpm scriptsから叩く

という方針で行こうと思います。

保守性も、可読性も良さそうです。


なので書き換えました

■サンプルコード

https://github.com/underground0930/node_tasks_sample


ファイル構成

今回の話とは関係ないものもあるのでそれは無視してください。


gulpfile.babel.js

// 関係あるファイルのみに絞ってます

.
├── README.md // タスクの内容は詳しくここに書く
├── package.json // 最小限のnpm scriptだけ書く
├── path.config.js // 全体で使用するパスはここで管理
├── src // 開発用ソース
├── htdocs // ビルドソース

├── tasks // ここにnodeのタスクを格納
│   ├── copy.js // コピータスク
│   ├── dele.js // 削除タスク
│   ├── html.js // ejsタスク
│   ├── index.js // このファイルに各タスクを読み込んで npm scriptsで叩く
│   ├── sass.js // sassタスク
│   └── watch.js // 監視タスク
└── webpack.config.js // webpackの設定(今回は関係ない)


開発環境全体で使うパスを管理

■元ソース

https://github.com/underground0930/node_tasks_sample/blob/master/path.config.js


path.config.js

const path = require('path')

// 環境変数を入れる
const NODE_ENV = process.env.NODE_ENV

// プロダクションビルドのディレクトリ
let buildRoot = 'prod'

// 開発中の場合はビルドされるディレクトリが変更される
if (NODE_ENV === 'dev') {
buildRoot = 'dev'
}
// ルートからのディレクトリを取得
const rootDir = process.cwd()

// os間のパスの違いを吸収
const pr = str => {
return path.resolve(str)
}

// それぞれのタスクで使うパスを設定しておく
const paths = {
src: {
root: pr(`${rootDir}/src`),
assets: pr(`${rootDir}/src/assets`),
js: pr(`${rootDir}/src/assets/js`),
css: pr(`${rootDir}/src/assets/scss`),
img: pr(`${rootDir}/src/assets/img`),
json: pr(`${rootDir}/src/assets/json`)
},
dist: {
root: pr(`${rootDir}/htdocs/${buildRoot}`),
assets: pr(`${rootDir}/htdocs/${buildRoot}/assets`),
js: pr(`${rootDir}/htdocs/${buildRoot}/assets/js`),
css: pr(`${rootDir}/htdocs/${buildRoot}/assets/css`),
img: pr(`${rootDir}/htdocs/${buildRoot}/assets/img`),
json: pr(`${rootDir}/htdocs/${buildRoot}/assets/json`)
},
node_env: NODE_ENV
}

module.exports = paths


注目して欲しいのはここで、

「gulpはwindowsとmacOSのパスの書き方の差異を吸収」

してくれてますが、

自前で書く場合は、どちらでも動くようにしておかなければなりません。

const path = require('path')

// os間のパスの違いを吸収
const pr = str => {
return path.resolve(str)
}


sassタスク

今回は書き換えの流れをscssタスクに絞って書きます

これが理解出来れば他のタスクも流れは似たようなものなので大丈夫かと思います

■元ソース

https://github.com/underground0930/node_tasks_sample/blob/master/tasks/sass.js

先ほどのgulpで書いたタスクを書き換えたものです

↓↓↓↓↓↓


tasks/sass.js

/**

* cssタスク
* @param {string} src - scssファイル群が入っているディレクトリのルート
* @param {string} dist - 出力されるcssファイルのディレクトリのルート
* @param {boolean} isDev - 開発フラグの有無
*/

const sass = require('node-sass') // node用 sass
const nodeSassGlobbing = require('node-sass-globbing') // sassファイル内でglobを使用する
const postcss = require('postcss') // autoprefixerに必要
const autoprefixer = require('autoprefixer') // cssにプレフィックスをつける
const fs = require('fs-extra') // ディレクトリを再帰的に作成
const glob = require('glob') // ファイル名のパターンマッチング

const css = (src, dist, isDev) => {
glob('/**/!(_)*.scss', { root: src }, (err, files) => {
// 対処となるファイルのパターンマッチング
if (err) {
console.log(err)
return
}
const resultArr = []
const length = files.length
let count = 0
files.forEach(file => {
sass.render(
{
importer: nodeSassGlobbing,
file,
outputStyle: isDev ? 'expanded' : 'compressed'
},
(error, resultSass) => {
if (error) {
console.log(error.message)
return
}
const f = file.split(src)
let filename = dist + f[1]
filename = filename.replace('.scss', '.css')
const dir = path.dirname(filename)
if (!fs.existsSync(dir)) {
// ディレクトリが無かったら
fs.mkdirsSync(dir) // ディレクトリを再帰的に作成
}
postcss([autoprefixer]) // postcssのプラグインのautoprefixerを設定
.process(resultSass.css, { from: undefined })
.then(resultPost => {
fs.writeFile(filename, resultPost.css, err => {
// ファイルに書き込む処理
if (err) throw err
resultArr.push(f[1])
count++
if (count === length) {
// ファイル数を数えてタスクが完了
console.log('css: [' + resultArr.join(', ') + ']')
console.log('====== css finished ======')
}
})
})
}
)
})
})
}

module.exports = css


先ほどのgulpファイルでやっていた、対象ファイルの選択と出力ですが、

gulpだとこんな簡単に出来ていますが、

  .src(paths.assets + '/scss/**/!(_)*.scss')   

.dest(paths.assets2 + '/css')

それを自前で実装するには少々コードをかかなくてはいけません

(gulpありがたいですね、、)。

ファイル名のパターンマッチング

ディレクトリを再帰的に作成する処理

ファイルにデータを書き込んで生成する処理

などは、実は色々なモジュールにお世話になっていた、というわけなんです。

また、それぞれのモジュールで、

たとえば、sassなら直接jsで叩けるようにAPIを用意してくれています。

なので、作者のリポジトリやドキュメントを読んで探してみましょう。

https://sass-lang.com/documentation/js-api

こんな感じで、他のタスクも書き換える事ができました。


全てのタスクを読み込むファイル

無駄に長くて説明とは関係ないものが多いですが、、

わかりやすくするために丁寧に書いてます。

ここは、各タスクをきちんと綺麗に書けていれば、

読み込んで実行するだけです!!

ソースを読めば、理解できると思います。

■元ソース

https://github.com/underground0930/node_tasks_sample/blob/master/tasks/index.js


tasks/index.js


const bs = require('browser-sync').create() // ローカルサーバー、ブラウザのリロード
const yaml = require('js-yaml') // yamlをjsに変換
const fs = require('fs') // ファイルシステム

/************************************************v
my task
************************************************/

const dele = require('./dele') // 自作の削除タスク
const copy = require('./copy') // 自作のコピータスク
const watch = require('./watch') // 自作のwatchタスク
const sass = require('./sass') // 自作のsassタスク
const html = require('./html') // 自作のhtmlタスク

/************************************************
paths
************************************************/

// 使いやすいようにそれぞれのパスを変数に入れ直す
const paths = require('../path.config')

// isDev
const isDev = paths.node_env === 'dev' ? true : false

// root path
const src = paths.src.root
const dist = paths.dist.root

// assets path
const assetsSrc = paths.src.assets
const assetsDist = paths.dist.assets

// img path
const imgSrc = paths.src.img
const imgDist = paths.dist.img

// json path
const jsonSrc = paths.src.json
const jsonDist = paths.dist.json

// js path
const jsSrc = paths.src.js
const jsDist = paths.dist.js

// css path
const cssSrc = paths.src.css
const cssDist = paths.dist.css

/************************************************
data
************************************************/

// ejsで使用するデータ
let data = {}
data = yaml.safeLoad(fs.readFileSync(assetsSrc + '/data/data.yaml', 'utf8'))

/************************************************
tasks
************************************************/

// 各タスク を関数化
const htmlTask = () => {
html(src, dist, data)
}
const cssTask = () => {
sass(cssSrc, cssDist, isDev)
}
const imgTask = () => {
copy(imgSrc, imgDist, '/**/*.{jpg,png,gif}')
}
const jsonTask = () => {
copy(jsonSrc, jsonDist, '/**/*.json')
}

// 監視して更新されたファイルに関するタスクを走らせる
const watchTasks = () => {
watch(src + '/**/*.{html,ejs}', f => {
htmlTask()
})
watch(cssSrc + '/**/*.scss', f => {
cssTask()
})
watch(imgSrc + '/**/*.{jpg,png,gif}', f => {
imgTask()
})
watch(jsonSrc + '/**/*.json', f => {
jsonTask()
})
}

// ローカルサーバーを立ち上げる、該当ファイルが更新されたらブラウザをリロード
const serverTask = () => {
bs.init({
open: 'external',
notify: false,
host: 'localhost',
ghostMode: false,
server: [dist],
https: false // or true
})

bs.watch(`${dist}/**/*.html`).on('change', bs.reload)
bs.watch(`${jsDist}/**/*.js`).on('change', bs.reload)
bs.watch(`${imgDist}/**/*.{png,jpg,gif}`).on('change', bs.reload)
bs.watch(`${jsonDist}/**/*.json`).on('change', bs.reload)
bs.watch(`${cssDist}/**/*.css`, (e, f) => {
if (e === 'change') {
bs.reload('*.css')
}
})
}

// 古いデータを削除後に各タスクを走らせる
dele(dist, () => {
// 各タスク
htmlTask()
cssTask()
imgTask()
jsonTask()

if (isDev) {
// 開発中ならwatchとサーバーも走らせる
watchTasks()
serverTask()
}
})



最後に、実行するnpm scriptsを書く

あとは、これらをNode.jsで実行します。

■元ソース

https://github.com/underground0930/node_tasks_sample/blob/master/package.json

作業者が使うであろうコマンドは以下の2つのみです。

https://github.com/underground0930/node_tasks_sample/blob/master/package.json#L7-L8

"build": "run-p webpack:prod tasks:prod" // 作業が完了して、本番環境にデプロイするデータを生成するコマンド

"dev": "run-p webpack:dev tasks:dev" // 開発をスタートするコマンド

run-p は、 webpackと先ほど作成したタスク群を

並列で実行させたいので入れている 「npm-run-all」 モジュールのコマンドです。

「cross-env」は、環境変数周りで頼らないと

Windowsでもちゃんと動かなかったので入れてます。

npm scriptsが何行も書いてあるサンプルが沢山あったのですが、

複数人数で作業する場合混乱しそうなので、

最小限にしておくのが良いかなと個人的には思います。


最後に

これらの書き換えをするには最低限のNode.jsの知識が必要になります。

私自身も今回の書き換えでわからないことが沢山あり、

Javascriptのとても良い勉強になりました。

このような書き換えをすることで、


  • 不具合の際にgulpレイヤーがないので、原因をつきとめやすい

  • 欲しい処理をわざわざgulpプラグインが出来るまで待つ必要がない

  • 自分で自由に処理を作成、変更しやすい(あんまり複雑なものはおすすめしない)

というメリットがあると思います。

なにはともあれ、

gulpは、ペーペーな自分に

この辺の処理も簡略化して使えるようにしていてくれていた

と、感謝せずにはいられませんでした。

gulpありがとう。