JavaScript
Node.js
Express
React
VSCode

WindowsのVSCodeでSPAのローカル開発環境を作る

作りたいもの

  • クライアントサイドはReact、サーバサイドはnode.js でExpress。
  • SVGでグラフ表示できる
  • サーバサイドでグラフ付のPDFを作れる
  • クライアントサイドもサーバサイドもVisual Studio Codeで開発
  • 管理者用のページを別のエントリポイントで作る

必要なソフトウェアのインストール

以下のアプリケーションをインストールしておきます。
バージョンが指定されていないものは最新のバージョンでOK。

VSCodeの拡張機能で「Debugger for Chrome」と「ESLint」をインストールしておきます。

npmで ESLint と nodemon をグローバルにインストールします。

>npm install -g eslint
>npm install -g nodemon

※すでにESLintを使っていて、Windowsのユーザフォルダ(c:\Users\xxxx\)配下にESLinstの設定ファイルがあると、VSCodeでうまく動作しない場合があるので注意。

プロジェクトを最初から作る

プロジェクトルートとして適当なフォルダを作ります。
手順をすすめていくと、プロジェクトルートの下にclientおよびserverフォルダがある状態になります。

プロジェクトルート
    |- client
    |- server

サーバ側

npmで express-generator をグローバルにインストールします。

>npm install -g express-generator

プロジェクトルートで次のコマンドを実行し、サーバ側のExpressのひな形をserverフォルダに作ります。

>express server

serverフォルダに移動し、Expressをインストールします。

>cd server
>npm install

その他、必要なモジュールをインストールします。

>npm install express wkhtmltopdf express-react-views react react-dom recharts

クライアント側

npmで create-react-app をグローバルにインストールします。

>npm install -g create-react-app

プロジェクトルートで次のコマンドを実行し、クライアント側の資源をclientフォルダに作ります。

>create-react-app client

インストール中に以下のエラーが出て失敗することがあります。

npm ERR! code EPERM
npm ERR! errno -4048
npm ERR! syscall scandir
npm ERR! Error: EPERM: operation not permitted, scandir

いずれかの方法で解消されるかもしれません。

  • npm cache verify を実行。
  • 管理者権限でcmdを起動してコマンド実行してみる。
  • npm自身のアップデートを試す。npm install -g npm
  • ウィルス対策ソフトが動作しているなら停止してみる。
  • 他にnode.jsのプロセスが動作しているなら停止してみる。

clientフォルダに資源が作成されたら、clientフォルダに移動し、必要なモジュールをインストールします。

>cd client
>npm install superagent recharts

Gitリポジトリの作成

プロジェクトルートフォルダを右クリックし、TortoiseGitの「Git Create repository hear ...」でGitリポジトリを作成します。
serverフォルダ直下に.gitignoreファイルを作成し、node_modulesフォルダを無視するように編集します。

/node_modules

VSCode でプロジェクトを開く

VSCodeで、clientとserverそれぞれをルートとして別々のウィンドウで開きます。
2つのVSCodeのウィンドウが起動した状態となります。
以降それぞれ「VSCode(clinet)」「VSCode(server)」とします。

クライアント側のデバッグ設定

VSCode(clinet)の拡張機能ウィンドウを開き、「Debugger for Chrome」と「ESLint」を有効にします。他の拡張機能は無効にしておきます。
デバッグウィンドウを開き、歯車アイコンをクリックします。
環境の選択プルダウンで、「Chrome」を選択。
launch.jsonを変更します。※名前は「Launch Chrome・・・」になっているはず。

変更前
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
変更後
"url": "http://localhost:3000",
"webRoot": "${workspaceRoot}/src",
"sourceMapPathOverrides": {
    "webpack:///src/*": "${webRoot}/*"
}

ターミナルエリアを開き、npm start を実行します。
自動的にブラウザが開いて Welcome to React のページが表示されますが、一旦ブラウザを閉じます。
デバックウィンドウを開き、「Launch Chrome・・・・」が選択されていることを確認して、デバッグ開始ボタンを押します。
再度、自動的にブラウザが開くので、VSCodeで適当な箇所にブレークポイントを張って、ブラウザのページを再読込みし、デバッグできることを確認します。

サーバ側のデバッグ設定

VSCode(server)の拡張機能ウィンドウを開き、「ESLint」を有効にする。他の拡張機能は無効にしておきます。
デバッグウィンドウを開き、歯車アイコンをクリックします。
環境の選択プルダウンが開くので「Node.js」を選択。
launch.jsonを変更します。configurationsに次の要素を追加。

{
    "type": "node",
    "request": "attach",
    "name": "nodemon",
    "processId": "${command:PickProcess}",
    "restart": true,
    "protocol": "inspector",
}

bin/www を変更して、ポート番号を3001にします。

変更前
var port = normalizePort(process.env.PORT || '3000');
変更後
var port = normalizePort(process.env.PORT || '3001');

package.json を変更して、nodemonを使うようにします。

変更前
"scripts": {
    "start": "node ./bin/www"
},
変更後
"scripts": {
    "start": "nodemon --inspect ./bin/www"
},

ターミナルエリアを開き、npm start を実行します。
ブラウザから http://localhost:3001/ にアクセスすると、Expressのデフォルトページが表示されます。
デバッグウィンドウを開いて、「nodemon」を選択し、デバッグを開始します。
いくつかnodeのプロセスが表示されますが、「node --inspect ./bin/www」のものを選択します。
routes/index.js の適当な箇所にブレークポイントを設定し、ブラウザで http://localhost:3001/ を再読込みして、デバッグできることを確認します。

サーバ/クライアント間の連携

クライアント側にインストールした SuperAgent を使って、Ajaxでサーバ側からデータを取得します。

クライアント側でAjax

VSCode(client)を開きます。
コンポーネントでsuperagentをインポートします。

import request from 'superagent';

onClickなどのイベントで、Ajaxの処理を書きます。
ここでは、取得したJSONデータをコンソールに出力します。

request
  .get('http://localhost:3001/users')
  .set('Content-Type', 'application/json')
  .end((err, res) => {
    if (err) {
      console.log('error');
    }else{
      console.log(res.body);
    }
  });

たとえば、src/App.js 全体を次のようにします。

import React, { Component } from 'react';
import request from 'superagent';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
        <button onClick={()=>this._testAjax()}>TEST Ajax</button>
      </div>
    );
  }

  _testAjax() {
    request
      .get('http://localhost:3001/users')
      .set('Content-Type', 'application/json')
      .end((err, res) => {
        if (err) {
          console.log('error');
        }else{
          console.log(res.body);
        }
      });
  }
}

export default App;

サーバ側でJSONデータを返す

VSCode(server)を開きます。
/usersにアクセスがあったらJSONデータを返すようにします。
app.js を変更し、クロスドメインを許可します。

変更前
app.use('/', index);
app.use('/users', users);
変更後
app.use(function(req, res, next) {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
  next();
});

app.use('/users', users);
app.use('/', index);

JSONデータを返すように、routes/uses.js を変更します。

変更前
res.send('respond with a resource');
変更後
res.contentType('application/json');
res.end(JSON.stringify({'test': 'hoge'}));

連携の確認

VSCode(server)、VSCode(client)のそれぞれのターミナルで npm start します。
http://localhost:3000/ にアクセスし、「TEST Ajax」ボタンをクリックすると、ブラウザのデベロッパーツールのコンソールに次のように出力されることを確認します。

Object {test: "hoge"}

VSCode(client)でデバッグ中の場合は、VSCodeのデバッグコンソールに出力されます。

PDFの出力

/chartpdf にアクセスしたらSVGのグラフを含むPDFをダウンロードするような処理を書きます。
VSCode(server)の変更のみです。

app.js を編集して、ExpressのビューエンジンにJSXを使うようにします。

変更前
app.set('view engine', 'jade');
変更後
app.set('view engine', 'jsx');
app.engine('jsx', require('express-react-views').createEngine());

routes/index.js を変更します。
wkhtmltopdf をインポートします。

var wkhtmltopdf = require('wkhtmltopdf');

chartコンポーネントのhtmlを埋め込んでPDF出力します。

router.get('/chartpdf', function(req, res, next) {
  res.render('chart', { name: 'John' }, function(err, html){
    wkhtmltopdf.command = 'C:/Program Files/wkhtmltopdf/bin/wkhtmltopdf.exe';
    wkhtmltopdf('<html><head><meta charset="UTF-8"></head><div><h1>あいうえお</h1>' + html + '</div></html>')
      .pipe(res);
  });
});

views/chart.jsx を作成し、折れ線グラフを表示するchartコンポーネントを作ります。
グラフの描画は、rechartsモジュールを使います。

var React = require('react');
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, Brush } from 'recharts';

const data = [
  {name: 'Page A', uv: 4000, pv: 2400, amt: 2400},
  {name: 'Page B', uv: 3000, pv: 1398, amt: 2210},
  {name: 'Page C', uv: 2000, pv: 9800, amt: 2290},
  {name: 'Page D', uv: 2780, pv: 3908, amt: 2000},
];

class HelloMessage extends React.Component {
  render() {
    return (
      <LineChart width={600} height={300} data={data}
        margin={{top: 25, right: 30, left: 20, bottom: 105}}>
        <XAxis dataKey="name"/>
        <YAxis/>
        <CartesianGrid strokeDasharray="3 3"/>
        <Tooltip/>
        <Legend />
        <Brush height={20} width={0} />
        <Line type="monotone" dataKey="pv" stroke="#8884d8" activeDot={{r: 8}}/>
        <Line type="monotone" dataKey="uv" stroke="#82ca9d" />
      </LineChart>
    );
  }
}

module.exports = HelloMessage;

ブラウザで http://localhost:3001/chartpdf にアクセスし、グラフ付のPDFが表示されることを確認します。

create-react-app で複数エントリポイントを作る

この作業をする前に、gitの未コミットのファイルがあったらコミットしておきます。
ここでは、/admin.html にアクセスしたとき別のエントリポイントが読み込まれるようにします。

  1. ターミナルで次のコマンドを実行し、プロジェクト設定を取り出します。

    >npm run eject
    
  2. config/webpack.config.dev.js を編集します。
    entry および output の箇所を次のようにします。

    entry: {
      index: [
        require.resolve('react-dev-utils/webpackHotDevClient'),
        require.resolve('./polyfills'),
        require.resolve('react-error-overlay'),
        paths.appIndexJs,
      ],
      admin:[
        require.resolve('react-dev-utils/webpackHotDevClient'),
        require.resolve('./polyfills'),
        require.resolve('react-error-overlay'),
        paths.appSrc + '/admin.js',
      ]
    },
    output: {
      pathinfo: true,
      filename: 'static/js/[name].bundle.js',
      chunkFilename: 'static/js/[name].chunk.js',
      publicPath: publicPath,
      devtoolModuleFilenameTemplate: info =>
        path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'),
    },
    

    HtmlWebpackPlugin の箇所を変更します。

    変更前
    new HtmlWebpackPlugin({
      inject: true,
      template: paths.appHtml,
    }),
    
    変更後
    new HtmlWebpackPlugin({
      inject: true,
      chunks: ['index'],
      template: paths.appHtml,
    }),
    new HtmlWebpackPlugin({
      inject: true,
      chunks: ['admin'],
      template: paths.appHtml,
      filename: 'admin.html',
    }),
    
  3. config/webpackDevServer.config.js を変更します。

    変更前
    historyApiFallback: {
      // Paths with dots should still use the history fallback.
      // See https://github.com/facebookincubator/create-react-app/issues/387.
      disableDotRule: true,
    },
    
    変更後
    historyApiFallback: {
      // Paths with dots should still use the history fallback.
      // See https://github.com/facebookincubator/create-react-app/issues/387.
      disableDotRule: true,
      rewrites: [
        { from: /^\/admin.html/, to: '/build/admin.html' },
      ],
    },
    
  4. src/index.js をコピーして src/admin.js を作ります。
    Appコンポーネントではなく、AppAdminコンポーネントを読み込むように変更します。

    import React from 'react';
    import ReactDOM from 'react-dom';
    import './index.css';
    import AppAdmin from './AppAdmin';
    import registerServiceWorker from './registerServiceWorker';
    
    ReactDOM.render(<AppAdmin />, document.getElementById('root'));
    registerServiceWorker();
    
  5. src/App.jsをコピーして AddAdminコンポーネント(src/AppAdmin.js)を作ります。
    AppAdmin.js 全体は次のようにします。

    import React, { Component } from 'react';
    import logo from './logo.svg';
    import './App.css';
    
    class AppAdmin extends Component {
      render() {
        return (
          <div className="App">
            <header className="App-header">
              <img src={logo} className="App-logo" alt="logo" />
              <h1 className="App-title">Welcome to React</h1>
            </header>
            <p className="App-intro">
              To get started, edit <code>src/App.js</code> and save to reload.
            </p>
          </div>
        );
      }
    }
    
    export default AppAdmin;
    
  6. public/index.html をコピーして public/admin.html を作ります。
    title のところだけ <title>React App Admin</title> のように変更しておきます。

  7. VSCode(client)で確認します。
    npm startし、http://localhost:3000/ と、http://localhost:3000/admin.html を表示したときで、ボタンの有無に違いがあることを確認します。

  8. config/webpack.config.prod.js を編集します。
    entry の箇所を次のようにします。

    entry: {
      index: [
        require.resolve('./polyfills'),
        paths.appIndexJs,
      ],
      admin:[
        require.resolve('./polyfills'),
        paths.appSrc + '/admin.js',
      ]
    },
    

    HtmlWebpackPlugin の箇所を次のようにします。

    変更前
    new HtmlWebpackPlugin({
      inject: true,
      template: paths.appHtml,
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeRedundantAttributes: true,
        useShortDoctype: true,
        removeEmptyAttributes: true,
        removeStyleLinkTypeAttributes: true,
        keepClosingSlash: true,
        minifyJS: true,
        minifyCSS: true,
        minifyURLs: true,
      },
    }),
    
    変更後
    new HtmlWebpackPlugin({
      inject: true,
      chunks: ['index'],
      template: paths.appHtml,
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeRedundantAttributes: true,
        useShortDoctype: true,
        removeEmptyAttributes: true,
        removeStyleLinkTypeAttributes: true,
        keepClosingSlash: true,
        minifyJS: true,
        minifyCSS: true,
        minifyURLs: true,
      },
    }),
    new HtmlWebpackPlugin({
      inject: true,
      chunks: ['admin'],
      template: paths.appHtml,
      filename: 'admin.html',
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeRedundantAttributes: true,
        useShortDoctype: true,
        removeEmptyAttributes: true,
        removeStyleLinkTypeAttributes: true,
        keepClosingSlash: true,
        minifyJS: true,
        minifyCSS: true,
        minifyURLs: true,
      },
    }),
    
  9. ビルドします。

    >npm run build
    

    buildフォルダにコンパイルされた資源が作成されるのでデプロイします。

参考
https://github.com/facebookincubator/create-react-app/issues/1084

ESLintを設定する

client、serverそれぞれでルールを設定します。
ESLintはグローバルにインストール済みです。

クライアント側

clientフォルダで次のコマンドを実行し、ESLintの初期設定ファイルを作ります。

>eslint --init

いくつか初期設定の内容を質問されますが、ここでは次のようにします。

? How would you like to configure ESLint? Answer questions about your style
? Are you using ECMAScript 6 features? Yes
? Are you using ES6 modules? Yes
? Where will your code run? Browser, Node
? Do you use CommonJS? No
? Do you use JSX? Yes
? Do you use React? Yes
? What style of indentation do you use? Spaces
? What quotes do you use for strings? Single
? What line endings do you use? Unix
? Do you require semicolons? Yes
? What format do you want your config file to be in? JavaScript

.eslintrc.jsが作られます。
インデントをスペース2つの設定にします。

"rules": {
    "indent": [
        "error",
        2
    ],

次の設定を rules に追加します。

"no-console": "warn",
"no-unused-vars": "warn",

サーバ側

サーバ側も同様に初期設定ファイルを作ります。
各質問はサーバ用のものを選択します。

? How would you like to configure ESLint? Answer questions about your style
? Are you using ECMAScript 6 features? Yes
? Are you using ES6 modules? Yes
? Where will your code run? Node
? Do you use JSX? Yes
? Do you use React? Yes
? What style of indentation do you use? Spaces
? What quotes do you use for strings? Single
? What line endings do you use? Unix
? Do you require semicolons? Yes
? What format do you want your config file to be in? JavaScript