自己紹介
求職中のフロントエンドエンジニアです。
何を紹介していいかわからないので僕の開発環境について紹介します。
普段使う技術スタック
言語/ライブラリ
ReduxやMobXはあまり使わずに自前のclassでState管理を行うことが多いです。
TypeScriptが好みなので常用していますがFlowでも書けますし勿論生JavaScriptも。
ユニットテスト
- Karma.js (with Chrome Headless)
- Mocha.js
- Power Assert
- Sinon.js
- Enzyme
ビルド
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
- メールアドレス
- mail@gutchom.com
- ウェブサイト
- http://www.gutchom.com
- ブログ投稿数
- 4 記事
- ブログ投稿日数
- 3 日
- 読者