Gobble up pudding

プログラミングの記事がメインのブログです。

MENU

2018年のReact最小構成の構築(非SPA対応)

f:id:fa11enprince:20180815055023j:plain

今回作成する構成

  • React v16
  • Babel Core v6
  • Webpack v4

github.com

Reactの勉強を再開しようと思い、だいぶ前回から期間が開いてしまい、
最近のReact環境どうなんだってのがさっぱりわからず、
また、割とブラックボックスなcreate-react-appを使ってReact勉強しても初心者にあまりよくないのかも…
と思い、見よう見まねでReactの最小構成を作ってみました。
もちろんReduxは使ってません。
どうしてこういう構成にしたかというと、おそらくですが、まだフルでSPAを作るケースってそれほどないと思うのですよね。 ということでたぶん、既存のサーバサードでレンダリングされた各ページに動きをちょい足しするような使い方が多いと思いますので、 bundleも分割できるような作りにしました。
ひょっとするとこの辺りの用途はもうVue.js一択なのかもしれませんが、
そこそこ歴史があり、Reactには結構便利なコンポーネントが転がってるのでそこはVue.jsより優位性があるのではないのかと。

フロントエンド回りはだいぶ落ち着いてきた感があります。
Reactの勉強をするつもりが今回はほぼWebpack4の勉強になってしまいました…まぁいいか。
自分は最近Angularばかり触っていてReactにはかなり疎いので、
間違いやこうしたほうが良い等ありましたらご意見いただけると幸いです。
Angularはこのあたりほぼ何も考えなくてよいので楽ですね。

事前準備

開発環境を整えます。VS Codeがないとフロントエンドは始まりません。

VS Codeを入れる

これがないと始まりません
DLしてインストールしましょう
https://code.visualstudio.com/
Atomでもいいですが、AtomはVS Codeに完敗してしまった感があります

入れたほうが良いプラグイン

  • vscode-styled-jsx
  • Beautify
  • ESLint

Node.jsを入れる

DLしてインストール https://nodejs.org/ja/

webpackコマンドを叩けるようにする

$ npm install -g webpack

Reactプロジェクトを作成する

$ mkdir react-boilerplate

packageを追加する

$ cd $_
$ npm init -y
$ npm install --save react react-dom
$ npm install --save-dev webpack babel-loader babel-core babel-preset-react babel-preset-env babel-preset-stage-2 webpack-cli webpack-dev-serve

ソースコードを書く

ここを参考に階層を作成してください
若干フォルダ構成がSpring-Bootなのはご容赦ください。 下記にあるように src/main/client配下がReactのjsxを作成する領域
src/main/resources配下のpublicとstaticがwebサーバで公開する領域
として作成します。 今回は
index.htmlと
subpage/index.htmlのReactを用いたページを作成します。

src配下

src
└── main
    ├── client
    │   ├── components
    │   │   └── hello.jsx
    │   ├── index.jsx
    │   └── subpage
    │       ├── components
    │       │   └── world.jsx
    │       └── index.jsx
    └── resources
        ├── public
        │   ├── index.html
        │   └── subpage
        │       └── index.html
        └── static
            └── react
                この配下にトランスパイルされたjsファイルが生成される

 
src/main/resources/public/index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>React Test</title>
    </head>
    <body>
        <div id="app" />
        <!-- NOTE: webpack-dev-server root path is src/main/resources -->
        <script src="/static/react/index.js"></script>
    </body>
</html>

 
src/main/resources/public/subpage/index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>React Subpage Test</title>
    </head>
    <body>
        <div id="app" />
        <!-- NOTE: webpack-dev-server root path is src/main/resources -->
        <script src="/static/react/subpage/index.js"></script>
    </body>
</html>

 
src/main/client/components/hello.jsx

import React, { Component } from 'react';

export default class Hello extends Component {
    render() {
        return (
            <h1>Hello React World!</h1>
        );
    }
}

 
src/main/client/index.jsx

import React from 'react';
import { render } from 'react-dom';

import Hello from './components/hello';

render(
    <div>
        <Hello />
    </div>,
    document.getElementById('app')
);

 
src/main/client/subpage/components/world.jsx

import React, { Component } from 'react';

export default class World extends Component {
    render() {
        return (
            <h1>Subpage Hello React World!</h1>
        );
    }
}

 
src/main/client/subpage/index.jsx

import React from 'react';
import { render } from 'react-dom';

import World from './components/world';

render(
    <div>
        <World />
    </div>,
    document.getElementById('app')
);

 
注意
import { Hoge } from './components/hoge';

import Hoge from './components/hoge';
は意味が違うので注意
Angularのときここは意識していなかった・・・
defaultがついたクラスだと後者でimportしないと
Warning: React.createElement: type is invalid -- expected a string
という警告がでて、エラーになる。

package.jsonに追記する

$ vi package.json

...
  "description": "",
  "scripts": {
    "start": "webpack-dev-server --mode development --progress --colors --hot --open",
    "watch": "webpack --mode development --watch --progress --colors --hot",
    "build-dev": "webpack --mode development --progress --colors --hot",
    "build": "webpack --mode production --progress --colors"
  },
...
  "license": "MIT",
  "babel": {
    "presets": [
      "env",
      "react",
      "stage-2"
    ]
  },
  "dependencies": {
...

webpack.config.jsを書く

$ touch webpack.config.js
$ vi webpack.config.js
const path = require('path');

module.exports = {
    entry: {
        'index': path.resolve(__dirname, 'src/main/client/index.jsx'),
        'subpage/index': path.resolve(__dirname, 'src/main/client/subpage/index.jsx'),
    },
    output: {
        path: path.resolve(__dirname, 'src/main/resources/static/react'),
        filename: '[name].js'
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: 'babel-loader',
            }
        ]
    },
    resolve: {
        extensions: ['*', '.js', '.jsx'],
    },
    devServer: {
        contentBase: path.resolve(__dirname, 'src/main/resources'),
        port: 3000,
    },
}

※Macだと、バックスラッシュと円マークが違うので注意!
macだとbackslashと円マークは別物なので注意
Optionキー+円マーク
ちなみに間違えると・・・
"You may need an appropriate loader to handle this file type”
のエラーが出続けてハマった・・・
Macではバックスラッシュ()と円マーク(¥)を区別するのを忘れていて、
正規表現の部分を円マーク(¥)で書いていた・・・・・・

resolveのextentionsを書くことによりjsxでimport時に拡張子を省略できる

path.resolve, path.join, __dirnameを知らない人はググることをおすすめ(私もよく知らなかったのでググりました)。
ざっくりいうと__dirnameは現在動かしているカレントのディレクトリ的なものです。
resolve, joinは動きが違いますが、パスをconcatする的なものです。

Reactの動作確認をする

まずはビルドする

$ npm run watch

別ウィンドウで

$ npm run start

これでwebpack-dev-serverが立ち上がります。 http://localhost:3000/public/
http://localhost:3000/public/subpage/
をたたくと、作ったものが表示されます。

プロダクション用のビルドはnpm run buildで!
以上です!!

完成したものはここです。

参考リスト

minimai-react-webpack-babel-setup
React simple boilerplate

その他関連情報

webpack tutorial
react-webpack-babel tutorial
webpack-dev-serverの使い方
npm option
webpackでbundle分割

旧バージョンの情報

※情報が古いのでwebpackの記載方法が非推奨になっているものもあります。
Webpack + React + ES6の最小構成を考えてみる。
Reactを「webpack + babel-loader」でビルドする方法

Angular moment.js脱却メモ

f:id:fa11enprince:20180815053913j:plain

Angularを使っていてWebpackでのbundle.jsが肥大化したときにmoment.jsをやめたいときのメモ

可能な限りDateとimport { DatePipe } from '@angular/common';を使う

date -> string

moment

moment(date).format('YYYY-MM-DD HH:mm:ss.SSS');

TypeScript + DatePipe

this.datePipe.transform(date, 'yyyy-MM-dd HH:mm:ss.SSS');

※書式設定の仕方が違うので注意!

String -> toLocalString

moment

moment(dateString).toDate().toLocaleString();

TypeScript + DatePipe

this.datePipe.transform(dateString);

String -> Date

Dateを使う

Date.parse('2018-1-10 01:00:00.111');
Date.parse('2018-01-10T01:00:00.111');
new Date('2018-1-10 01:00:00.111');
new Date('2018-01-10T01:00:00.111');

時間差

moment

const diffFrame = moment(toDate).diff(moment(fromDate)) / 100;

Date

const diffFrame = (Date.parse(toDate) - Date.parse(fromDate)) / 100;

日付加算(ミリ秒)

moment

addedDate = moment(date).add(1000, 'milliseconds').toDate().toLocaleString();

Date

addedDate = this.datePipe.transform(Date.parse(date) + 1000);

ISOString

moment

moment(date).toISOString();

Date

new Date(date).toISOString()

経過時間

msから経過時間のhh:mm:ssを作るようなとき moment.durationだけはどうにもならない →自前実装するしかない

プログラムのインターフェースは必要か

ふと、いろんな記事を見ていて、インターフェースは必要かっていうのがあった。

Java インターフェース メリット わからない - 社内se × プログラマ × ビッグデータ

どういうものかは分かりますが、メリットについては何も分からないです。
処理を具体的に書かないと、何の役にも立たないのに、何でそんなことをするのか?という疑問が残ります。

というような記載を見つけました。 こういう風に思うのは正常だと思います。
いや、当然必要だけど、JavaとかJavaとかJavaとか過剰に使いすぎてるのでは?と思ったところがあったので…。RubyとかPythonってinterfaceないっすよね(そのかわり別にいろいろ後付けで実装差し替えなど動的言語ならではのことができますが…)。 なんかほら、StrutsでDIが積極的に取り入れたときに、あの汎用性重視のやりすぎ感。それに近いものを感じるわけです。結局歴史的には設定より規約(CoC)に流れていき、汎用性が高いけれども一方でとても面倒なXML地獄、いい加減、毎度おんなじテンプレみたいな呪文をひたすら書くということから解放されたのだ(しかも間違ったら意味不明なミスに遭遇するし、XMLなので静的チェックもかけにくい)…。

自分が思うにインターフェースにするメリットは

  • テストクラスを書くときに簡単に実装をダミーに差し替えれる
  • 大規模開発しているときにとりあえずこれをこういう形で呼べばいいというのを利用側に宣言できる

というのだけに尽きると思います。
ただ、フレームワークを作っている側だともっと恩恵があって、
これ以上にメリットがあってあとで丸っと実装を差し替えたりしても後方互換性がとりあえずインターフェースだけ変えなければ保たれるとかあると思うのですが…

Javaの話でいえば特に昔のSpring Frameworkとかの慣習だと何でもかんでも実装とインターフェースを分けて書きますが、正直アプリレベルの上位レイヤーでそれをやるとやりすぎ感があったりするような気がします。ぶっちゃけフレームワークの上位のアプリレベルだと別にインタフェース書かなくてもいいんじゃ?という気がします。テストフレームワーク(JUnitとかその他)でもMockでいろいろ差し替えようと思えば差し替えられるので。あれですね特にServiceクラスで見るFooServiceFooServiceImplとか。 正直、インターフェース書いてオーバーライドするのがだるい…アプリレベルならその手間があるならインターフェース書かなくてもいいんじゃ的な…。メリットよりだるさが上回ります。 小規模~中規模とかで自分が全部掌握しているようなプロジェクトだとインターフェースはほぼ意味をなさないんじゃないかと。
一方抽象クラスでポリモーフィックに作ろうとするとabstractメソッドは便利なんですがinterfaceってなんか中途半端というか。

といいつつ、Spring Bootでは慣習に倣いServiceとServieImplは分けています…。辞めようかなと思った今日この頃。なんでかって?テストクラスをpackage privateにすれば簡単に差し替えれるから。 そもそもSpringでDIする場合は@Autowiredでそのクラスを書き換えれば疎結合は維持されるので(ただし、呼び出し元の書き換えが発生しますが…)、余計メリットを感じないわけです。 そもそもクラスの名前がイケてなかった。あとで書き換えたい…ってときは結局インターフェースにしても意味ないですので…ということで、アプリなどの上位レイヤーばかり書いていると、メリット薄いかもしれないです…。 フレームワークはフレームワークでインターフェースないとしんどそうですが、処理を追うときに、実際にはどの実装使ってんのよ?っていうのが追うの辛いという…。

AngularとJQuery/JQuery UIを組み合わせる

f:id:fa11enprince:20180526233100j:plain AngularとJQuery/JQuery UIを組み合わせるのは何か間違ってる気がしますが、
Angularの部品が足りなくて、どうしても使いたいことが起きることがあるかと思います(たぶん…)。
ここは意識低い系の方法を紹介します。
(意識高い系の方法は知識不足でちょっとわからなかった)。一応補足に記載。
ここで紹介する方法のデメリットはTypeScriptを使ってるのにJQueryの型がなくなってしまうことです(てかそれで別に困らない)。 こういうシチュエーションがそれなりにあると思うのに、
jQuery排他主義の人が多いのか(自分もどちらかといえばそう) 情報が錯綜しているのでまとめました。

JQueryをインストールする

これは普通にnpmからインストールします

$ npm install --save-dev jquery

JQuery UIをインストールする

公式のは1.12.1で止まっていて、いつからかnpmで提供されるのは部品ごとに細かく分かれたらしく、 Angularから使うにはwebpackをいじらないといけない模様。 ただ、jquery-ui-distっていうのを作ってくれている人がいて、それを使えばnpmからイケるのだが、 ちょっと怪しいので、フツーに公式からzipを落として配置することにする。 https://jqueryui.com/download/
から最新版をデフォルトの設定で落として、 src/assets/vendor/jquery-ui とか作ってあげてそこに以下の解凍した中身を放り込む

external/
images/
AUTHORS.txt
...
jquery-ui.theme.min.css
LICENSE.txt
package.json

angular.jsonにCSSとJSを登録する

 "projects": {
    "client": {
      "root": "",
      "sourceRoot": "src",
      "projectType": "application",
      "architect": {
        "build": {
        ...
            "styles": [
              "src/assets/vendor/jquery-ui/jquery-ui.min.css",
              ...
            ],
            "scripts": [
              "node_modules/jquery/dist/jquery.min.js",
              "src/assets/vendor/jquery-ui/jquery-ui.min.js"
              ...
            ]
...

stylesscripts部分にJQueryとJQueryUIのCSSとJSを追加する。

使ってみる

あとはJQuery/JQuery UIを使っているところのTypeScriptで
declare var $: any;と書けばいいだけ。
この場合、import $ from 'jquery';はいりません。
ただし、型はない状態のままです。
これだけです。 どうしてもDOMを操作する場合は ngAfterViewInit()でやるとかありますが、基本はこれだけでOKです。

補足: 意識高い系の方法

おそらく、型を使う場合は、うまくいってませんが

$ npm install --save-dev @types/jquery
$ npm install --save-dev @types/jqueryui

とやってかつ

import $ from 'jquery';
import 'jqueryui';

とやればOKだと思われる...がこれだけではうまくいかない…(詳細不明)。 詳しい人教えてください。

ググると出てくる情報源

https://stackoverflow.com/questions/30623825/how-to-use-jquery-with-angular

Angular6に移行メモ

f:id:fa11enprince:20180628144634j:plain
Angular5.2からAngular6に移行したのでメモ

移行手順

https://update.angular.io/
で示されることをひたすらやっていく

  • Angular Version 5.2 -> 6.0

  • App Complexity Basic

  • ngUpgrade I use ngUpgrade no

  • Package Manager npm

Before Updating

httpモジュールからhttpClientモジュールへ

Switch from HttpModule and the Http service to HttpClientModule and the HttpClient service. HttpClient simplifies the default ergonomics (You don't need to map to json anymore) and now supports typed return values and interceptors. Read more on angular.io

→すでに実施済み

https://brianflove.com/2017/07/21/migrating-to-http-client/

                                                                                               
+import { HttpClient, HttpParams } from '@angular/common/http';                                                       
 import { Injectable } from '@angular/core';                                                                          
-import { Headers, Http, RequestOptions, Response, URLSearchParams } from '@angular/http';                            
+                                                                                                                     
 import { Observable } from 'rxjs/Observable';                                                                        
                                                                                                                      
import 'rxjs/add/observable/throw';                                                                                  
export class HogeService {                                                                                                       
                                                                                                                      
-    constructor(private http: Http) { }                                                                              
+    constructor(private httpClient: HttpClient) { }                                                                  
...                                                                                      
         locationCode: string,                                                                                        
         page: number,                                                                                                
         pageSize: number,                                                                                            
-        optionalParams: URLSearchParams,                                                                             
+        optionalParams: HttpParams,                                                                                  
     ): Observable<IPaginationPage<Hoge>> {                                                                    
-        const params = new URLSearchParams();                                                                                                                                 
-        params.set('size', `${pageSize}`);                                                                           
-        params.set('page', `${page}`);                                                                               
-        if (optionalParams != null && optionalParams.paramsMap.size !== 0) {                                         
-            params.appendAll(optionalParams);                                                                        
-        }                                                                                                            
-        const options = new RequestOptions({ search: params });                                                      
-        return this.http.get(this.hogeAllGetUrl, options).map(this.extractData)                               
+        let params: HttpParams = new HttpParams()                                                                                                                                  
+            .set('size', `${pageSize}`)                                                                              
+            .set('page', `${page}`);                                                                                 
+        optionalParams.keys().forEach((key) => {                                                                     
+            params = params.append(key, optionalParams.get(key));  // 注意:直観に反して戻り値を取らないと変わらない                        
+        });                                                                                                          
+        return this.httpClient.get<IPaginationPage<Hoge>>(this.hogeGetUrl, {params})                
+            .map(this.extractData)                                                                                   
             .catch((error) => Observable.throw(error.statusText));                                                   
     }                                                                                                                

これをやってなかったら多分これが一番大変。

Angular/CLIのアップデート

Update your Angular CLI globally and locally, and migrate the configuration to the new angular.json format by running the following:

npm install -g @angular/cli
npm install @angular/cli@latest
ng update @angular/cli

これで失敗する場合はたぶんNode.jsのバージョンを上げろと言ってきます。

Angularのcoreのアップデート

Update all of your Angular framework packages to v6, and the correct version of RxJS and TypeScript.

ng update @angular/core

Angular Materialのアップデート

After the update, TypeScript and RxJS will more accurately flow types across your application, which may expose existing errors in your application's typings

Update Angular Material to the latest version.

-> 使ってないのでやってない

After the Update

RxJSのlintを入れ替えてRxJSを新バージョンに

Remove deprecated RxJS 6 features using rxjs-tslint auto update rules. For most applications this will mean running the following two commands:

npm install -g rxjs-tslint
rxjs-5-to-6-migrate -p src/tsconfig.app.json # →失敗したが放置

ここからRxJSの直しにかかります。RxJSがバージョンアップしているので書き換えます。

 import { HttpClient, HttpParams } from '@angular/common/http';
 import { Injectable } from '@angular/core';

-import { Observable } from 'rxjs/Observable';
-
-import 'rxjs/add/observable/throw';
-import 'rxjs/add/operator/catch';
-import 'rxjs/add/operator/map';
+import { Observable, throwError } from 'rxjs';
+import { catchError, map } from 'rxjs/operators';
...
         return this.httpClient.get<IPaginationPage<Hoge>>(this.hogeGetUrl, {params})
-            .map(this.extractData)
-            .catch((error) => Observable.throw(error.statusText));
+            .pipe(
+                map(this.extractData),
+                catchError((error) => throwError(error.statusText)),
+            );
     }

Angular6へあげた効果

ビルド時間早くなった
200秒から60秒
バンドルサイズが減った
main.bundle.jsが1.1MBから800kB

以上で終わり。お疲れさまでした。

その後エラーになったことなど

AjaxでJSONをパラメータとしてPOSTする場合

stackoverflow.com stackoverflow.com これはAngular6の移行というよりhttpModuleからhttpClientModuleへ変えたときに問題になる話

npm installでnode-sassがエラーになる

node_modules\node-sass
> node scripts/install.js

module.js:549
    throw err;
    ^

Error: Cannot find module 'true-case-path'
    at Function.Module._resolveFilename (module.js:547:15)
    at Function.Module._load (module.js:474:25)
    at Module.require (module.js:596:17)
    at require (internal/module.js:11:18)

ngx-bootstrapがらみの問題

  • Accordion いつの間にかタイトル文字がクリック可能になってSubmit発火する→使うのやめる
  • Datepicker 下記の問題により、ヘッダの文字が崩れる

https://github.com/valor-software/ngx-bootstrap/issues/4443

// as of Angular 6 they set preserveWhitespaces to false from default.
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });
(window as any).global = window;

polyfills.ts

/***************************************************************************************************
 * APPLICATION IMPORTS
 */
import 'global-shim';  // workaround
/**
 * Date, currency, decimal and percent pipes.
 * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
 */
import 'intl';  // Run `npm install --save intl`.

その他気になったこと

全然関係ないが、Angularのコンポーネントって少ないなーと思ってたんですが、 探したらそこそこあるんですね。 github.com

中でもすごいのがコレ github.com

でもAngularって極力materialとかAngular Teamが提供しているもの以外依存しないほうがいろいろと幸せになれそうな気がする…

ちなみに僕はngx-bootstrapを使っています。 valor-software.com

おまけ

Angular5からAngular6に移行した小さなサンプルを置きました github.com