ぐっちょむの開発にっき

プログラミングの話をするブログ

自己紹介

求職中のフロントエンドエンジニアです。

何を紹介していいかわからないので僕の開発環境について紹介します。

普段使う技術スタック

言語/ライブラリ

ReduxやMobXはあまり使わずに自前のclassでState管理を行うことが多いです。
TypeScriptが好みなので常用していますがFlowでも書けますし勿論生JavaScriptも。

ユニットテスト

JestAVAなども使えます。

ビルド

TSCとbabelの2段構えでトランスパイルしています。

開発支援ツール

gulpなどのタスクランナーは使わずnpm-scriptsで完結させる派です。
Express.jsで書いたモックサーバーをnodemonで走らせてBrowsersyncからオートリロードさせることが多いです。

プロジェクト構成例

observe-2ch-client
├── README.md
├── karma.conf.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── tsconfig.json
├── tslint.json
├── webpack.config.js
├── server/
├── public
│   ├── css
│   │   ├── style.css
│   │   └── page.css
│   └── js
│       └── page.js
└── src
    ├── 
    ├── styles
    │   ├── commons
    │   │   ├── variables.css
    │   │   └── reset.css
    │   ├── modules
    │   │   └── modal.css
    │   └── pages
    │       ├── style.css
    │       └── page.css
    └── scripts
        ├── app
        │   ├── entries
        │   │   └── page.ts
        │   ├── lib
        │   │   └── deep-equal.ts
        │   ├── stores
        │   │   └── Logger.ts
        │   └── components
        │       ├── Foo.tsx
        │       └── Bar # You can have directory for grouping co-components
        │           ├── index.tsx
        │           └── Baz.tsx
        ├── test
        │   ├── lib
        │   │   └── deep-equal.test.ts
        │   ├── stores
        │   │   └── Logger.test.ts
        │   └── components
        │       ├── Foo.test.tsx
        │       └── Bar
        │           ├── index.test.tsx
        │           └── Baz.test.tsx
        └── types
            └── lib
                └── deep-equal.d.ts

ディレクトリの詳細

  • src/: 開発用のメインディレクトリ
  • server/: モックサーバー
  • public/: 出力先ディレクトリ

scripts

  • app/: プロダクトに関するコードは全てここ  - entries/: transpileのentryとなるファイル群  - components/: Reactコンポーネント  - stores/: State管理用のロジック  - lib/: Stateを持たないユーティリティ関数など
  • test/: ユニットテストをapp/と同じディレクトリ構造で書いていく
  • types/: 型定義ファイルをapp/と同じディレクトリ構造で書いていく

styles

  • commons/: 共通で使われる一般的なファイル (例: variables.css, reset.css)
  • modules/: 特定のコンポーネント用のStyle (例: modal.css, header.css)
  • pages/: transpileのentryとなるファイル群

server

  • index.js: Server script.
  • data/: JSON files as storage of API.
  • views/ # Template files for server.

設定ファイル

こちらのGistにもまとめておきます。

postcss.config.js

あんまりdigれてないので薄味の設定です。

const isProduction = (process.env.NODE_ENV === 'production')
const isDevelopment = (process.env.NODE_ENV === 'development')

module.exports = (context) => ({
  parser: context.options.parser,
  map: isDevelopment ? { inline: true } : false,
  plugins: {
    'postcss-import': {
      path: ['src/styles'],
    },
    'postcss-cssnext': {},
    'postcss-reporter': {},
    'postcss-browser-reporter': {},
    'cssnano': isProduction && {
      preset: 'default',
    },
  }
})

tsconfig.json

トランスパイルは後段にbabelを噛ませるのでtargetはesnextです。pathsが肝です。後述するwebpack.config.jsと組み合わせてfrom '../../../../components/foo'みたいなパス指定とさよならできます。

{
  "compilerOptions": {
    "jsx": "react",
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noUnusedLocals": true,
    "removeComments": true,
    "importHelpers": true,
    "baseUrl": "./src/scripts",
    "typeRoots" : [
      "types",
      "./node_modules/@types"
    ],
    "paths": {
      "*": ["*"],
      "app": ["./src/scripts/app"]
    }
  },
  "include": [
    "./src/scripts/**/*"
  ],
  "exclude": [
    "./node_modules"
  ],
  "compileOnSave": false,
  "atom": {
    "rewriteTsConfig": false
  }
}

tslint.json

.eslintのpresetで有名なAirBnBのルールとは結構かけ離れています。でも僕のコードは読みにくくないですよ。

{
  "defaultSeverity": "error",
  "rulesDirectory": [],
  "rules": {
    "class-name": true,
    "comment-format": [true, "check-space"],
    "curly": true,
    "indent": [true, "spaces"],
    "max-line-length": [true, 150],
    "no-duplicate-imports": true,
    "no-duplicate-variable": true,
    "no-eval": true,
    "no-internal-module": true,
    "no-trailing-whitespace": true,
    "no-unsafe-finally": true,
    "no-unused-variable": [true],
    "no-var-keyword": true,
    "object-literal-shorthand": true,
    "one-line": [
      true,
      "check-catch",
      "check-finally",
      "check-else",
      "check-open-brace",
      "check-whitespace"
    ],
    "prefer-const": true,
    "prefer-object-spread": true,
    "quotemark": [
      true,
      "single",
      "jsx-double"
    ],
    "restrict-plus-operands": [true],
    "semicolon": [true, "never"],
    "space-within-parens": 1,
    "triple-equals": true,
    "trailing-comma": [
      true,
      {
        "multiline": {
          "objects": "always",
          "arrays": "always",
          "functions": "never",
          "typeLiterals": "ignore"
        },
        "singleline": "never"
      }
    ],
    "typedef-whitespace": [
      true,
      {
        "call-signature": "nospace",
        "index-signature": "nospace",
        "parameter": "nospace",
        "property-declaration": "nospace",
        "variable-declaration": "nospace"
      }
    ],
    "variable-name": [
      true,
      "ban-keywords"
    ],
    "whitespace": [
      true,
      "check-branch",
      "check-decl",
      "check-operator",
      "check-separator",
      "check-type"
    ]
  }
}

webpack.config.js

環境ごとの設定の分けかたが正直キモいです。

const path = require('path')
const glob = require('glob')
const webpack = require('webpack')
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin')
const isDevelopment = (process.env.NODE_ENV === 'development')
const isTesting = (process.env.NODE_ENV === 'test')

// configオブジェクトを結合する関数
const combine = base => addition => (
  Object.keys(addition).reduce((merged, key) => (
    Object.assign(merged, {
      [key]: typeof merged[key] !== 'object'
        ? addition[key]
        : merged[key] instanceof Array
          ? merged[key].concat(addition[key])
          : Object.assign(merged[key], addition[key])
    })
  ), base)
)

// 基本設定
const base = {
  context: path.resolve(__dirname, 'src/scripts'),
  plugins: [
    new webpack.EnvironmentPlugin(['NODE_ENV']),
    new webpack.NoEmitOnErrorsPlugin(),
    new CaseSensitivePathsPlugin(),
  ],
  resolve: {
    modules: ['node_modules'],
    extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
    alias: {
      app: path.resolve(__dirname, 'src/scripts/app'), // 先述した`from '../../../../components/foo'`みたいなパス指定とさよならするためのオプション
    },
  },
}

// テスト環境用の設定
const test = combine(base)({
  devtool: 'cheap-module-eval-source-map',
  externals: {
    'react/lib/ExecutionEnvironment': 'react',
    'react/lib/ReactContext': 'react',
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      mangle: false,
      compress: true,
      parallel: {
        cache: true,
        workers: 4,
      },
      output: {
        comments: true,
        beautify: true,
      },
    }),
  ],
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        exclude: ['node_modules'],
        use: [
          { loader: 'babel-loader' },
          { loader: 'ts-loader' },
        ],
      },
    ],
  },
})

// development/production環境で共用の設定
const entriesDir = __dirname + '/src/scripts/app/entries/'
const common = combine(base)({
  entry: glob
    .sync('**/*.tsx', { cwd: entriesDir, root: entriesDir })
    .reduce((entries, path) => (
      Object.assign(entries, { [path.split('.')[0]]: entriesDir + path })
    ), {
      vendor: [
        'axios',
        'react',
        'react-dom',
      ]
    }),
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'public/js'),
    jsonpFunction: 'vendor',
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: Infinity,
    }),
  ],
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        exclude: ['node_modules', /\.test\.tsx?$/],
        use: [
          { loader: 'babel-loader' },
          { loader: 'ts-loader' },
        ],
      },
    ],
  },
})

// development環境での設定
const development = combine(common)({
  devtool: 'cheap-module-eval-source-map',
  output: {
    pathinfo: true,
  },
  stats: {
    errorDetails: true,
    colors: true,
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      mangle: false,
      compress: true,
      parallel: {
        cache: true,
        workers: 4,
      },
      output: {
        comments: true,
        beautify: true,
      },
    }),
  ],
})

// production環境での設定
const production = combine(common)({
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      mangle: true,
      compress: true,
      parallel: {
        cache: true,
        workers: 4,
      },
      output: {
        comments: false,
        beautify: false,
      },
    }),
  ],
})

// 環境による設定の出し分け
module.exports = isTesting ? test : isDevelopment ? development : production

karma.conf.js

普通です。

const webpackConfig = require('./webpack.config')

module.exports = config => {
  config.set({
    basePath: './src/scripts',
    frameworks: ['power-assert', 'mocha'],
    browsers: ['ChromeHeadless'],
    mime: {
      'text/x-typescript': ['ts','tsx']
    },
    files: [
      'test/**/*.test.+(ts|tsx)',
    ],
    preprocessors: {
      'app/**/*.+(ts|tsx)': ['webpack', 'sourcemap'],
      'test/**/*.test.+(ts|tsx)': ['webpack', 'sourcemap'],
    },
    plugins: [
      'karma-coverage',
      'karma-mocha',
      'karma-mocha-reporter',
      'karma-power-assert',
      'karma-chrome-launcher',
      'karma-sourcemap-writer',
      'karma-sourcemap-loader',
      'karma-webpack',
    ],
    port: 9876,
    concurrency: Infinity,
    singleRun: true,
    reporters: ['mocha'],
    mochaReporter: {
      colors: true,
    },
    browserConsoleLogOptions: {
      level: '',
      terminal: true,
    },
    loggers: [
      { type: 'console'},
    ],
    logLevel: config.LOG_INFO,
    webpack: webpackConfig,
    webpackMiddleware: {
      stats: 'errors-only',
    },
  })
}

package.json

フロントエンドでの基本構成です。ここに必要なものを足していきます。

{
  "name": "observe-2ch-client",
  "version": "0.1.0",
  "description": "UI for Observe 2ch Web",
  "author": "gutchom",
  "license": "MIT",
  "homepage": "https://observe-2ch.gutchom.com",
  "repository": {
    "type": "git",
    "url": "https://github.com/gutchom/observe-2ch-client"
  },
  "bugs": {
    "url": "https://github.com/gutchom/observe-2ch-client/issues"
  },
  "engines": {
    "node": "^8.*",
    "npm": "^5.*"
  },
  "scripts": {
    "start": "cross-env NODE_ENV=development run-p -s count server:** 'build:** -- -w'",
    "test": "cross-env NODE_ENV=test karma start",
    "testing": "npm t -- --auto-watch --no-single-run",
    "build": "cross-env NODE_ENV=production run-s lint:** test clean build:** count",
    "build:image": "cpx ./src/images/**/* ./public/img",
    "build:style": "postcss ./src/styles/pages/**/*.css -b ./src/styles/pages -d ./public/css",
    "build:script": "webpack --progress",
    "lint": "run-p lint:**",
    "lint:style": "stylelint --fix ./src/styles/**/*.css",
    "lint:script": "tslint -p tsconfig.json --fix ./src/**/*.ts*",
    "server:mock": "cross-env PORT=${OBSERVE_2CH_PORT-3000} nodemon -w ./server ./server",
    "server:sync": "browser-sync start -b 'google chrome' -f ./public -p localhost:${OBSERVE_2CH_PORT-3000} --port ${OBSERVE_2CH_SYNC-3333}",
    "clean": "rimraf ./public && mkdirp ./public/js ./public/css ./public/img",
    "count": "TOTAL=$(find ./src -name '*.ts*' -o -name '*.css' | xargs wc -l | tail -1); printf '\\e[36m\n  This project has\\e[35m %s lines\\e[36m of source code!\n\n\\e[m' ${TOTAL%total}"
  },
  "dependencies": {
    "@tweenjs/tween.js": "^17.1.1",
    "axios": "^0.17.0",
    "eventemitter3": "^2.0.3",
    "normalize.css": "^7.0.0",
    "react": "^16.0.0",
    "react-dom": "^16.0.0",
    "url-search-params-polyfill": "^2.0.1"
  },
  "devDependencies": {
    "@types/enzyme": "^3.1.1",
    "@types/eventemitter3": "^2.0.2",
    "@types/mocha": "^2.2.43",
    "@types/node": "^8.0.47",
    "@types/power-assert": "^1.4.29",
    "@types/react": "^16.0.18",
    "@types/react-dom": "^16.0.2",
    "@types/sinon": "^2.3.7",
    "@types/tween.js": "16.9.0",
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-plugin-transform-class-properties": "^6.24.1",
    "babel-plugin-transform-decorators-legacy": "^1.3.4",
    "babel-plugin-transform-object-rest-spread": "^6.26.0",
    "babel-preset-airbnb": "^2.4.0",
    "babel-preset-env": "^1.6.1",
    "babel-preset-power-assert": "^1.0.0",
    "babel-preset-react": "^6.24.1",
    "babel-preset-stage-2": "^6.24.1",
    "body-parser": "^1.18.2",
    "browser-sync": "^2.18.13",
    "caniuse-lite": "^1.0.30000751",
    "case-sensitive-paths-webpack-plugin": "^2.1.1",
    "colors": "^1.1.2",
    "cpx": "^1.5.0",
    "cross-env": "^5.1.0",
    "cssnano": "^3.10.0",
    "enzyme": "^3.1.0",
    "enzyme-adapter-react-16": "^1.0.2",
    "eslint": "^4.9.0",
    "express": "^4.16.2",
    "git-pre-push": "0.0.5",
    "glob": "^7.1.2",
    "karma": "^1.7.1",
    "karma-chrome-launcher": "^2.2.0",
    "karma-coverage": "^1.1.1",
    "karma-mocha": "^1.3.0",
    "karma-mocha-reporter": "^2.2.5",
    "karma-power-assert": "^1.0.0",
    "karma-sourcemap-loader": "^0.3.7",
    "karma-sourcemap-writer": "^0.1.2",
    "karma-webpack": "^2.0.5",
    "mkdirp": "^0.5.1",
    "mocha": "^4.0.1",
    "nodemon": "^1.12.1",
    "npm-run-all": "^4.1.1",
    "postcss": "^6.0.13",
    "postcss-browser-reporter": "^0.5.0",
    "postcss-cli": "^4.1.1",
    "postcss-cssnext": "^3.0.2",
    "postcss-import": "^11.0.0",
    "postcss-reporter": "^5.0.0",
    "power-assert": "^1.4.4",
    "pre-commit": "^1.2.2",
    "pug": "^2.0.0-rc.4",
    "react-test-renderer": "^16.0.0",
    "rimraf": "^2.6.2",
    "sinon": "^4.0.2",
    "stylelint": "^8.2.0",
    "stylelint-config-standard": "^17.0.0",
    "stylelint-order": "^0.7.0",
    "ts-loader": "^3.1.0",
    "tslint": "5.8.0",
    "typescript": "^2.5.3",
    "uglify-js": "^3.1.5",
    "webpack": "^3.8.1"
  },
  "pre-commit": [
    "lint"
  ],
  "pre-push": [
    "test"
  ],
  "browserslist": [
    "> 5%",
    "last 2 versions"
  ],
  "stylelint": {
    "extends": "stylelint-config-standard",
    "plugins": [
      "stylelint-order"
    ],
    "rules": {
      "order/properties-alphabetical-order": true,
      "order/order": [
        "custom-properties",
        "declarations"
      ]
    }
  },
  "babel": {
    "presets": [
      "env",
      "react",
      "stage-2"
    ],
    "plugins": [
      "transform-decorators-legacy",
      "transform-class-properties",
      "syntax-object-rest-spread"
    ],
    "env": {
      "test": {
        "presets": [
          "airbnb",
          "power-assert"
        ]
      }
    }
  }
}

以上、自己紹介でした。

プロフィール
id:gutcho
リンク
ブログ投稿数
4 記事
ブログ日数
3 日
継続期間
2 ヶ月
読者
2 人 Diplozoon Hiro_Matsuno