爆速でフロントエンドのコーディングを始める開発環境(webpack4/jQuery)

フロントエンドの開発環境を構築するのが意外に大変ですよね。。そんな時間かけてられないし。
ということで、`Github`の`Repository`で管理したのでその内容を書きます。
その`Repository`はこちら「[macotok/frontend_develop_environment](https://github.com/macotok/frontend_develop_environment)」

想定する作業

- WordPressに落とし込む前のHTMLコーディング
- 依頼者がjQueryでの制作を求めてるとき
- VueやReactを使わない案件(LPとか)

## 作業したこと

- webpackでbundle
    - ES6(@babel/polyfill(async/await対応))
    - TypeScript
    - Sass
    - autoprefixerを自動生成
    - productionモードでcss/jsファイル圧縮
    - imageファイル圧縮
    - WebFont対応(woff/eot/ttf/svg)
    - media対応(mp4/webm/ogg)
    - jQueryに依存したライブラリに対応
- npm scriptsでタスクランナー
- jQuery
- font「NotoSansJP」設定
- fontawesomeのcssをnode_modulesで管理
- pugでHTMLコーディング
    - _include
    - index.pug
    - buildすると`_include`を削除
- cssファイル(smacss設計)
    - base
    - function
    - layout
    - mixin
    - module
    - responsive
    - state
    - variable
    - animation
    - font
- Lint
    - ESLint
    - StyleLint

## Tree

```
├── babel.config.js
├── package.json
├── src
│   ├── images
│   ├── js
│   │   ├── app.ts
│   ├── media
│   ├── pug
│   │   ├── _include
│   │   │   └── meta.pug
│   │   └── index.pug
│   ├── sass
│   │   ├── _animation.scss
│   │   ├── _base.scss
│   │   ├── _function.scss
│   │   ├── _layout.scss
│   │   ├── _mixin.scss
│   │   ├── _module.scss
│   │   ├── _responsive.scss
│   │   ├── _state.scss
│   │   ├── _variable.scss
│   │   ├── _webfont.scss
│   │   └── style.scss
│   └── webfonts
├── stylelint.config.js
├── webpack.config.js
└── yarn.lock
```

## package.json

``` json
"scripts": {
  "start": "concurrently \"webpack-dev-server\" \"yarn run watch:pug\" \"yarn run tsc -- -w\"",
  "dev": "webpack --mode development",
  "build": "concurrently \"rm -rf dist\" \"webpack\" \"yarn run pug\" --mode production",
  "pug": "pug src/pug/ --hierarchy -o dist/html -P",
  "watch:pug": "pug src/pug/ --hierarchy -o dist/html -P -w",
  "tsc": "tsc -p ./"
},
"dependencies": {
  "jquery": "^3.3.1",
  "yui": "^3.18.1"
},
"devDependencies": {
  "@babel/core": "^7.4.0",
  "@babel/polyfill": "^7.4.4",
  "@babel/preset-env": "^7.4.2",
  "@fortawesome/fontawesome-free": "^5.8.1",
  "autoprefixer": "^9.5.0",
  "babel-loader": "^8.0.5",
  "concurrently": "^4.1.0",
  "copy-webpack-plugin": "^5.0.2",
  "core-js": "^3.0.1",
  "css-loader": "^2.1.1",
  "eslint": "^5.16.0",
  "eslint-config-airbnb-base": "^13.1.0",
  "eslint-plugin-import": "^2.17.2",
  "file-loader": "^3.0.1",
  "imagemin-webpack-plugin": "^2.4.2",
  "mini-css-extract-plugin": "^0.5.0",
  "node-sass": "^4.11.0",
  "optimize-css-assets-webpack-plugin": "^5.0.1",
  "postcss-loader": "^3.0.0",
  "pug": "^2.0.3",
  "pug-cli": "github:pugjs/pug-cli#master",
  "pug-loader": "^2.4.0",
  "sass-loader": "^7.1.0",
  "style-loader": "^0.23.1",
  "stylelint": "^10.0.1",
  "stylelint-config-recommended": "^2.2.0",
  "stylelint-config-recommended-scss": "^3.3.0",
  "stylelint-scss": "^3.6.0",
  "ts-loader": "^6.1.0",
  "typescript": "^3.6.3",
  "uglifyjs-webpack-plugin": "^2.1.2",
  "url-loader": "^1.1.2",
  "webpack": "^4.29.6",
  "webpack-cli": "^3.3.0",
  "webpack-dev-server": "^3.2.1"
}
```

## webpack.config.js

``` javascript
const Autoprefixer = require('autoprefixer');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ImageminPlugin = require('imagemin-webpack-plugin').default;
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const path = require('path');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const webpack = require('webpack');

module.exports = {
  entry: {
    app: [
      './src/js/app.js',
      './src/sass/style.scss',
    ],
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].js',
    publicPath: '/',
  },
  devServer: {
    contentBase: './dist/html/',
    watchContentBase: true,
    port: 3000,
    open: true,
  },
  module: {
    rules: [
      {
        test: /\.js?$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
      },
      {
        test: /\.ts$/,
        use: 'ts-loader',
      },
      {
        test: /\.scss$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
          },
          {
            loader: 'css-loader',
            options: {
              url: false,
              sourceMap: true,
            },
          },
          {
            loader: 'postcss-loader',
            options: {
              plugins: [
                Autoprefixer(
                  {
                    browsers: ['last 2 versions', 'Android >= 4'],
                  },
                ),
              ],
            },
          },
          {
            loader: 'sass-loader',
            options: {
              sourceMap: true,
            },
          },
        ],
      },
      {
        test: /\.(jpe?g|png|gif|svg)(\?[a-z0-9=.]+)?$/,
        use: [
          {
            loader: 'url-loader',
          },
        ],
      },
      {
        test: /\.(woff|woff2|eot|ttf|svg)(\?[a-z0-9=.]+)?$/,
        use: [{
          loader: 'file-loader',
          options: {
            name: '[name].[ext]',
            outputPath: './webfonts',
            publicPath: '../webfonts',
          },
        }],
      },
      {
        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name].[ext]',
            },
          },
        ],
      },
    ],
  },
  optimization: {
    minimizer: [
      new OptimizeCSSAssetsPlugin(),
      new UglifyJsPlugin(),
    ],
  },
  resolve: {
    extensions: ['.js', '.ts'],
  },
  devtool: 'source-map',
  plugins: [
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, 'src/images/'),
        to: path.resolve(__dirname, 'dist/images/'),
      },
      {
        from: path.resolve(__dirname, 'src/webfonts/'),
        to: path.resolve(__dirname, 'dist/webfonts/'),
      },
      {
        from: path.resolve(__dirname, 'src/media/'),
        to: path.resolve(__dirname, 'dist/media/'),
      },
    ]),
    new ImageminPlugin({
      test: /\.(jpe?g|png|gif|svg)$/i,
      pngquant: {
        quality: '95-100',
      },
    }),
    new MiniCssExtractPlugin({
      filename: 'css/style.css',
    }),
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery',
      'window.jQuery': 'jquery',
    }),
  ],
};
```

## 説明

### webpack4でbabel 

webpackのversionが4なのでbabelは`@babel`の`core`と`preset-env`だけ。
~~そういえばこれだと非同期処理で`async await`が書けないので`Promise`を使った。いつか直す。~~ 使用可能

### polyfillでasync/await対応

非同期処理をasync/awaitで書きたかったのでpolyfillを設定して対応しました。
polyfillは今まではグローバルで読み込んでいたのを、`useBuiltIns: 'usage',`と書くと使用する箇所だけ適用する、かつグローバルを汚染させない。

``` json:package.json
"@babel/polyfill": "^7.4.4",
"core-js": "^3.0.1",
```

``` javascript:babel.config.js
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'usage',
        corejs: '3',
        targets: {
          browsers: ['last 2 versions', '> 3% in JP'],
        },
      },
    ],
  ],
};
```

#### 参考

- [Babel7.x時代のpolyfillの設定方法とuseBuiltInsの仕組み](https://aloerina01.github.io/blog/2018-11-29-1)

### ベンダープレフィックスを自動生成 
ベンダープレフィックスを書くのが面倒なのでpostcssで自動で生成するようにwebpackで行なう。これで作業するときはベンダープレフィックスを書かなくてOK。

### cssとjsをoptimize

cssとjsをminify化したいとき、いわゆるproductionモードにしたいときは`$ yarn build`のコマンドで`dist`に吐き出される。

### jQueryのライブラリ対応

`slick`とか`fancybox`のjQueryライブラリをnpmでinstallしたとき`node_modules`の中の`$`記述に対応させるためにwebpackのpluginで対応。
これ書かないと「$ is not function」でエラーになります。

``` javascript:weboack.config.js
plugins: [
  new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery',
    'window.jQuery': 'jquery',
  }),
]
```

### npmでタスクランナー

「gulpなんて今どき使うんですか？」
ってことで、「npm scripts」でタスクランナー書いてます。
そんな記述は多くないですが、`$ yarn dev`で`sourcemap`を吐き出します。
あと「pug/css/js」ファイルの変更を`watch`してます。

``` json:package.json
"scripts": {
    "start": "concurrently \"webpack-dev-server\" \"yarn run watch:pug\" --watch",
    "dev": "webpack --mode development",
    "build": "concurrently \"rm -rf dist\" \"webpack\" \"yarn run pug\" --mode production",
    "pug": "pug src/pug/ --hierarchy -o dist/html -P",
    "watch:pug": "pug src/pug/ --hierarchy -o dist/html -P -w"
  },
```

### NotoSansJPに対応

意外にfont-familyで`NotoSansJP`を使いたいというオーダーがあったので予め設定しておきました。
使わない場合はソースを削除すればOK。

``` css:sass/_base.scss
@font-face {
  font-family: 'Noto Sans JP';
  font-style: normal;
  font-weight: 400;
  src:
          url(//fonts.gstatic.com/ea/notosansjp/v5/NotoSansJP-Regular.woff2) format('woff2'),
          url(//fonts.gstatic.com/ea/notosansjp/v5/NotoSansJP-Regular.woff) format('woff'),
          url(//fonts.gstatic.com/ea/notosansjp/v5/NotoSansJP-Regular.otf) format('opentype');
}

body {
  font-family: 'Noto Sans JP', 'Hiragino Kaku Gothic ProN', 'メイリオ', sans-serif;
}
```

### fontawesomeのcssファイルをnode_modulesで管理

webfontはどのサイトでも使うと思うのでこれも初期設定しておきました。
有名な`fontawesome`を使うことを想定してcssファイルをnpmでinstallしてます。

``` commnad
$ yarn add -D @fortawesome/fontawesome-free
```

そして使うときは`font.scss`に書いてます

``` css:sass/_font.scss
@import "~@fortawesome/fontawesome-free/scss/brands.scss";
@import "~@fortawesome/fontawesome-free/scss/fontawesome.scss";
.instagram {
  &::before {
    content: '\f16d';
    font-style: normal;
    font-variant: normal;
    text-rendering: auto;
    -webkit-font-smoothing: antialiased;
    font-family: 'Font Awesome 5 Brands', sans-serif;
  }
}
```

#### 参考

- [Webpack4でFont-awesomeを利用する
](https://qiita.com/nttt/items/6f5f10fb761af6f0a14f)

### HTMLテンプレートエンジンにPugを使用

Vueのtemplateを書くときも`Pug`で書いてますよね。もはやHTMLを生で書く時代は終わりました。
`$ yarn build`したときに`_include`ディレクトリも出力されるのでその回避策として、下記のpug-cliをinstallするとのこと

``` command
$ yarn add github:pugjs/pug-cli#master
```

``` json:package.json
"pug-cli": "github:pugjs/pug-cli#master",
```

なんかあんまり美しくないですよね。
他にやり方があれば教えてください。。

#### 参考

- [pug-cliで_がついたファイル（_header.pug）やディレクトリ（_include）もコンパイルされてしまう問題を解決する
](https://qiita.com/soarflat/items/48cec8fb19252a3fc4ad)

### _function.scssにz-indexの順番を管理

いまどき`z-index`の指定で`9999`という数字をみると寒気がしませんか？
その値をfunction.scssで管理してます。

``` css:sass/_function.scss
$z-map: (
  main,
  nav,
  header,
  drawer
);

@function z($name) {
  @return index($z-map, $name);
}

// 使うとき
z-index: z(main);
```

#### 参考

- [Sassを使ったz-indexの管理](https://qiita.com/kojika17/items/b4694e3afe0da97e6335)

### _animation.scssに@keyframes記述

animationのkeyframeの指定がファイルのあちこちに散らばるのがあまり好きじゃないので一元管理させました。
とりあえずは`fadeIn`と`fadeOut`だけでいいかなと。

``` css:sass/_animation.scss
@keyframes fadeIn {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

@keyframes fadeOut {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}
```

### ESLint設定

lintチェックされてないファイルはもう見たくないですよね？規則性がないソースは目が拒絶します。
こんな感じで設定しました。`airbnb-base`を使用。

``` json:.eslintrc
{
  "extends": [
    "airbnb-base"
  ],
  "parserOptions": {},
  "globals": {
    "window": true,
    "document": true,
    "fetch": false
  }
}

```

## StyleLint設定

ESLintは結構前から使ってましたが、StyleLintは割と最近から使ってます。
これも設定するとコードに統一性が生まれて安眠できます。

``` javascript:stylelint.config.js
module.exports = {
  plugins: [
    'stylelint-scss',
  ],
  extends: [
    'stylelint-config-recommended',
    'stylelint-config-recommended-scss',
  ],
  rules: {
    'font-family-no-missing-generic-family-keyword': true,
    'declaration-block-no-shorthand-property-overrides': true,
    'declaration-block-trailing-semicolon': 'always',
    'selector-pseudo-element-colon-notation': 'double',
    'number-leading-zero': 'never',
    'block-closing-brace-empty-line-before': 'never',
    'selector-max-empty-lines': 0,
    'max-empty-lines': 0,
  },
};
```

ruleの設定に苦労したので、ruleの内容を事細かにまとめてくださったサイトを参考にしました。

#### 参考

- [buchiya4th/stylelint.config.js](https://gist.github.com/buchiya4th/f4ca1be2ab98ee5a8098fa68a93e752c)

### reset.cssはyuiを読み込む

reset.cssを外部から持ってくることが多く、且つ改変すると著作権に抵触する可能性があるので、予めpackageでinstallして読み込ませました。

``` css:sass/style.scss
@import "~yui/cssreset/cssreset-min";
```

## TypeScript設定

`tsconfig.json`にてTypeScriptの設定を書きました。
内容についてはこちらの記事を参照してください。「[TypeScript導入編](https://qiita.com/macotok/items/3fb60315da9206f9805c)」

## まとめ

これで急なコーディング依頼が来てもすぐに着手できますね。
まだ動的サイトを作るのにWordPressを使用する案件はありそうなので。。