gulpコマンドだけでDjangoをrunserverしてscssも監視してコンパイルしてホットリロードもしたい。

# Djangoでモダンなコーディング環境を作りたい

### 最近、
Djangoを書く案件があり、pythonも楽しいしview, model等の操作性がいいので毎日楽しかったのですが、フロント開発の際にいつも通りpugやらscssやらモダンな言語使ってガンガン書こうとしたら壁に当たりました。てかdjango-htmlってpug使えるのか？そもそもgulpどうやって導入するんだ？

> え？素のhtmlとcssでコーディングするの...？

その日は絶望して寝ました。

### 翌日、
gulpをなんとかDjangoに導入してscssをコンパイルできるようになりました。
しかし、静的サイトでは問題なく動くgulpの`browser-sync`が、Djangoでは動きませんでした。
Djangoのプロジェクト起動コマンド`python3 manage.py runserver`を打ち、gulpを監視状態にしてscssを書き、保存したらブラウザを手動リロード。

> これのどこがモダンやねん

その日は絶望して寝ました。

### 翌日、
いろいろあり悟りを開きました。その結果、ターミナルで
```
gulp
```
と打つと

:::note
`python3 manage.py runserver`が実行されてDjangoプロジェクトが起動する。
さらに、gulpが動いてbrowser-syncで`localhost`にプロジェクトを表示。
同時に、scssを監視し始める。scssに変更があれば直ちにコンパイルして `localhost` をリロード。
:::

するようになりました。
ちなみにpug導入は成功しませんでした。うまくいったら続編を書きます。

正直なところ、かなり荒削りに書いているので詳細な仕様などわからないことが多いですが、私と同じ思いをしている人間が数多いることを信じて、完成したgulpfile.jsをここに遺します。

## この記事を理解するための前提条件目安
```
Django触ったことある
npm触ったことある
gulp触ったことある
```
くらいです。

# 環境
```zsh:ハードウェア情報
MacBookPro15,4
Quad-Core Intel Core i7
```
```zsh:django環境情報
Python 3.8.9
pip 22.1.2 
Django 2.2.27
```

# ディレクトリ構成

`+`がフォルダを表しています。

```zsh:通常のディレクトリ構成
+ mysite/
  + app/
    + static/
      + app/
        + css/
          + style.css
    + template/
      + app/
        + index.html
  + mysite/
    + settings.pyなど
  manage.py
```
:::note info
上記は、Djangoのプロジェクト名を`mysite`、アプリ名を`app`とした場合の構成です。
:::

:::note warn
Djangoにて`static`と`template`の設定が済んでいる前提です。
この記事を読んでいる人はほとんど済んでいるはずです...よね？
ちんぷんかんぷんという人は[この記事](https://qiita.com/t-iguchi/items/f658d607888449cfdf23)を読めば理解できるかも
:::

# npm導入

プロジェクトルートディレクトリ（mysite/）にてnpmを導入します
また、必要モジュールをインストールします。
```zsh:zsh
npm init y
npm i -D browser-sync gulp gulp-plumber gulp-sass sass
```

以下のようにpackage.jsonに追記されたと思います。
```js:package.json
  "devDependencies": {
    "browser-sync": "^2.27.10",
    "gulp": "^4.0.2",
    "gulp-plumber": "^1.2.1",
    "gulp-sass": "^5.1.0",
    "sass": "^1.53.0",
  },
```

次に、
`mysite/app/src/app/css/style.scss`となるようにフォルダを作成して空のscssファイルを用意します。
また、プロジェクトルートディレクトリに`gulpfile.js`を追加します。空で大丈夫です。

```zsh:gulp導入後のディレクトリ構成
+ mysite/
  + app/
    + src/
      + app/
        + scss/
          + style.scss // 追加した
    + static/
      + app/
        + css/
          + style.css
    + template/
      + app/
        + index.html
  + mysite/
    + settings.pyなど
  + node_modules/
  .gulpfile.js　//　追加した
  manage.py
  package-locl.json　//　増えた
  package.json //　増えた
```

# gulpfile.js書いていく
先ほどプロジェクトルートディレクトリに追加した`gulpfile.js`を書いていきます。
gulp4に対応したモダンな書き方になっているので見慣れたgulp記法と違う箇所があるかもしれません。

### まず、必要モジュールを読み込みます。

```js:gulpfile.js
const { src, dest, watch, parallel, series } = require('gulp')
const sass = require('gulp-sass')(require('sass'))
const plumber = require('gulp-plumber')
const browserSync = require('browser-sync').create()
const child = require('child_process')
```

### 続いて、django起動とホットリロードを有効にする関数を定義します。

この部分についてはほとんど[この記事](https://qiita.com/kameo0501/items/a472fcc312bd3faabfcc)を参考にしました。マジ感謝。

軽く解説すると、先ほど読み込んだ`child_process`から、`spawn`を使用してdjangoのrunserverを実現しています。`spawn`については[この記事](https://qiita.com/TsuyoshiUshio@github/items/cf4b28e7999403f7a04c)が参考になります。少し古いですが。

また、`browserSync`にて`proxy`、`port`オプションを使ってDjangoと紐づけています。ここに関しては正直よくわかっていません。たくさん試したらこれで成功したという程度です。むしろ正しい方法教えてください。

ついでに、reload関数も定義します。こちらは`browserSync`をリロードするだけの機能を持ちます。
のちほどwatch関数で呼び出すことで力を発揮します。

```js:gulpfile.js
// runserver
var server = null
const runserver = (cb) => {
  if (server) {
    server.kill()
    console.log("kill task done!")
  }
  server = child.spawn('python3', ['manage.py', 'runserver'], { 
    stdio: 'inherit'
  })
  server.on('close', (code) => {
    if (code !== 0) {
      cb(code)
      console.error('Django runserver exited with error code: ' + code)
    } else {
      cb()
      console.log('Django runserver exited normally.')
    }
  })
}
// browser
const browser = (cb) => {
  browserSync.init({
    ui: false,
    notify: false,
    proxy: {
      target: "http://127.0.0.1:8000",
    },
    port: 8000
  })
}
// reload
const reload = (cb) => {
  browserSync.reload()
  cb()
}
```

:::note warn
`python manage.py runserver`でdjango起動する人は`server = child.spawn('python' ~ `になります。
筆者は`python3 manage.py runserver`でdjango起動するので`server = child.spawn('python3' ~ `です。
:::

:::note info
browser-syncのオプションである`target`や`port`も環境によって変わってくるかと思います。
:::

### さらに、scssをコンパイルする関数を定義します。

`plumper`はエラーが出た時に後述のwatch関数を止めないためにあります。
また、`browserSync.stream()`を最後に渡すことでファイル保存時、browser-syncに変更差分のみ綺麗に伝えてくれます。

```js:gulpfile.js
const compileScss = () => {
  return src('./app/src/app/scss/**/*.scss')
    .pipe(plumber())
    .pipe(sass())
    .pipe(dest('./app/static/app/css/'))
    .pipe(browserSync.stream())
}
```

### watch関数を定義します。

watch関数にてscssを監視し、先ほど定義したコンパイル関数とリロード関数を渡します。
こうすると、監視対象に変更があった際に指定した関数を実行してくれます。

```js:gulpfile.js
const watchSass = () => {
  watch('./app/src/app/scss/**/*.scss', series(compileScss, reload))
}
```

### 最後に、関数を外部から呼び出せるようにして一つにまとめます。

gulp3から4への変更ではここが一番大きな変更かもしれませんね。

`exports.default`とすることでgulpのデフォルトコマンドに任意の関数を指定できます。
また、`parallel`は関数を並列に処理するためのものです。

```js:gulpfile.js
exports.runserver = runserver
exports.browser = browser
exports.watchSass = watchSass

exports.default = parallel(
  runserver,
  browser,
  watchSass,
)
```

# 実行

長かったですね。すいません。
ターミナルにてプロジェクトルートディレクトリに移動し、gulpを実行します。

```zsh:zsh
gulp
```

しばらく待つと、`localhost`にてプロジェクトが表示されるはずです。
その状態で、`mysite/app/src/app/css/style.scss`を編集して保存すれば、コンパイルされて`mysite/statoc/app/css/`に書き出されると思います。また、画面もリロードされるはずです。

# 完全版 gulpfile.js

これまで解説したgulpfile.jsをまとめたものを掲載します。

<details><summary>gulpfile.js</summary>

```js:gulpfile.js
const { src, dest, watch, parallel, series } = require('gulp')
const sass = require('gulp-sass')(require('sass'))
const plumber = require('gulp-plumber')
const browserSync = require('browser-sync').create()
const child = require('child_process')

/* -------------------------------------------
  RUNSERVER, BROWSER
------------------------------------------- */

// runserver
var server = null
const runserver = (cb) => {
  if (server) {
    server.kill()
    console.log("kill task done!")
  }
  server = child.spawn('python3', ['manage.py', 'runserver'], { 
    stdio: 'inherit'
  })
  server.on('close', (code) => {
    if (code !== 0) {
      cb(code)
      console.error('Django runserver exited with error code: ' + code)
    } else {
      cb()
      console.log('Django runserver exited normally.')
    }
  })
}
// browser
const browser = (cb) => {
  browserSync.init({
    ui: false,
    notify: false,
    proxy: {
      target: "http://127.0.0.1:8000",
    },
    port: 8000
  })
}
// reload
const reload = (cb) => {
  browserSync.reload()
  cb()
}

/* -------------------------------------------
  SCSS
------------------------------------------- */

const compileScss = () => {
  return src('./app/src/app/scss/**/*.scss')
    .pipe(plumber())
    .pipe(sass({outputStyle: 'expanded'}))
    .pipe(dest('./app/static/app/css/'))
    .pipe(browserSync.stream())
}

/* -------------------------------------------
  WATCH
------------------------------------------- */

const watchSass = () => {
  watch(paths.scss + '**/*.scss', series(compileScss, reload))
}

/* -------------------------------------------
  EXPORTS
------------------------------------------- */

exports.runserver = runserver
exports.browser = browser
exports.watchSass = watchSass

exports.default = parallel(
  runserver,
  browser,
  watchSass,
)
```
</details>

# 所感

自分の環境だと、実行後にしばらくエラーが出てから綺麗に表示されるまでのラグがあります。
そんな不安定なコードを紹介すんなよという感じですが、これを構築するだけで２日かかってますので、自分と同じ思いをしている人のためにもとりあえず書きました。**もっといい方法あるよという方がいたら是非教えていただきたいです。**

また、今回はscssをコンパイルするコードのみですが、django-htmlもモダンに記述できる方法がないか模索中です。ちなみに、pugを導入しようとして一度挫折しています。こちらについてもいい情報ありましたら是非教えてください。

以上です。