千里の道も1commitから

関西在住のWebエンジニアです。長らくブログをサボっていたので、1日1記事、1commitを目標にゆるりと頑張ります。積み重ねが研鑽となると信じて

JavaScriptのネストした関数のエラーハンドリングを理解する

JavaScriptではエラーが発生した時、強制的にプログラムが終了してしまいます。
具体的に何が起こるかと言うと後続の処理は実行されません。また、ユーザーはどのエラーが発生したかわからなくなります。Node.jsのサーバーではHTTPサーバーが止まる事象にもなってしまいます。

const parent = () => {
  child()
  console.log('後続処理');
}

parent();

以下のエラーが出て処理が強制終了します。

  child()
  ^
ReferenceError: child is not defined
    at parent (/Users/xxx/Desktop/MyProject/study/study-javascript/try-catch/none_try_catch.js:2:3)

適切に例外処理をすることで、エラーを補足し対処することが可能です。try .. catch 通常のエラーハンドリングはすぐにわかりますが、try ... catchはどの単位で書くべきでしょうか? ネストした関数は??

通常のエラーハンドリング

const parent = () => {
  try {
    throw new Error('call nomarl!!') // ここでスローした例外は
  } catch (ex) {  // ここでcatchされる
    console.log(ex.message);
  }
}

parent();

ネストした関数のエラーハンドリング

const parent = () => {
  try {
    child()
  } catch (ex) {  // ここでキャッチする
    console.log(`catch parent ${ex}`);
  }
}

const child = () => {
  // 何かの処理
  if (error) {
    throw new Error('call child!!')  // ここでスローした例外は
  }
}

parentTryCatch();

ネストした関数内であっても、親のtry ... catchで補足できることがわかります。

ネストした関数でエラーハンドリング + エラーハンドリング

const parent = () => {
  try {
    child()
  } catch (ex) {
    console.log(`cath parent ${ex}`);
  }
}

const child = () => {
  try {
    gChild()
  } catch (ex) { // ここでキャッチする。しかし、上へ伝播しないので、処理はここで終了する
    console.log(`cath child ${ex}`);
  }
}

const gChild = () => {
  ggChild() // ここで例外発生
  // throw new Error('call gChild')
}

parent();

上記だと、gChildでエラーが発生します。
親childで補足することはできますが、握り潰されてるので、parentに伝播することはありません。 ルーティング処理をしている場合等はトップレベル関数で補足したいでしょう。 その場合は、childでthrowして例外を伝播させると良いです。

良いエラーハンドリングとは

適切な箇所で、多すぎないエラーハンドリングが行われてると良いです。
エラーハンドリングができてなかったり、必要以上に try .. catchが書かれてるケースを見ます。

また、非同期のエラーハンドリングは async/awaitで囲む劇的に直感的になります。 JavaScriptのエラーついて知っておくべきこと - Qiita

参考

制御フローとエラー処理 - JavaScript | MDN JavaScriptのエラーついて知っておくべきこと - Qiita async関数においてtry/catchではなくawait/catchパターンを活用する - Qiita

JavaScript 再入門 配列コピー

JavaScriptの配列・オブジェクトのコピーはちょっとややこしいです。
業務のコードでも間違った(もしくは考慮されていない)使い方をしているのを見かけるし、
自分も脳筋 ... or _.cloneDeep は良くないので今一度思考を整理したいと思います。

コピーの種類

JavaScriptのコピーでは第一レベルまでコピーする浅いコピー(Shallow Copy)とネストしたオブジェクトまでコピーする2つのコピーがあります。
この第一階層までってのが肝で、 concatはダメでspreadは大丈夫。みたいな主張をたまに聞く結果となります。第一階層とは以下のようなネストしてない配列となります。

let family = ['わい', '弟', '妹', 'おとん', 'おかん', ['嫁', '義妹']];
let newFamily = [];

// 配列処理
newFamily = [...family];
newFamily[0] = 'ぼく';
newFamily[5][0] = 'ぼく嫁';
console.log(family);
console.log(newFamily);

// 結果
[ 'わい', '弟', '妹', 'おとん', 'おかん', [ 'ぼく嫁', '義妹' ] ]
[ 'ぼく', '弟', '妹', 'おとん', 'おかん', [ 'ぼく嫁', '義妹' ] ] // ネストした配列はメモリ参照となる

それでいてJavaScriptではコピーできる方法が多いので、それが理解の妨げに拍車をかけてる気がします。

浅すぎるコピー

同じ参照を持つ。コピー用途で使ってたらしばくやつです。

  let family = ['わい', '弟', '妹', 'おとん', 'おかん', ['嫁', '義妹']];
  let newFamily = family;

浅いコピー

第一階層までは完全に別の

Array.from(['a', 'b', 'c']);

配列風オブジェクトや反復可能オブジェクトから、新しい、浅いコピーの Array インスタンスを生成します。 ここでは省きますが、配列風というのが特徴

  let family = ['わい', '弟', '妹', 'おとん', 'おかん', ['嫁', '義妹']];
  // 配列を複製
  let newFamily = Array.from(family);

Array.from() - JavaScript | MDN

['a', 'b', 'c'].slice(1, 2)

begin から end まで選択された配列の一部をシャローコピーして、新しい配列オブジェクトを返します。
名前の通り切り出し箇所をbeginとendで指定できるのがポイント

  let family = ['わい', '弟', '妹', 'おとん', 'おかん', ['嫁', '義妹']];
  // 配列を複製
  let newFamily = arr.slice(2,4);

Array.prototype.slice() - JavaScript | MDN

spread構文

ES6から入ったあれ
スプレッド構文を使うと、配列式や文字列などの反復可能オブジェクトを、 0 個以上の引数 (関数呼び出しの場合) や要素 (配列リテラルの場合) を期待された場所で展開したり、オブジェクト式を、 0 個以上のキーと値のペア (オブジェクトリテラルの場合) を期待された場所で展開したりすることができます。

airbも配列コピーに推奨している。確かに配列のみならスプレッドは確かにわかりやすいか〜

  let family = ['わい', '弟', '妹', 'おとん', 'おかん', ['嫁', '義妹']];
  // 配列を複製
  let newFamily = [...family];

spread構文はこの使い方が最強。

  const numbers = [1, 2, 3];
  const str = "abc";
  console.log([...numbers, ...str]));

  // 6

深いコピー

ネストした階層まで完全に別オブジェクトを作りだす深いコピーです。 シャローコピーよりはコストがかかるのでネストをしないなら不要。

JSON.parse(JSON.stringify([1, 2, 3]))

昔からあるTips。Date型が上手く変換できなかったかも。完全にコピーするなら _.cloneDeepが良いかなぁ。

  let family = ['わい', '弟', '妹', 'おとん', 'おかん', ['嫁', '義妹']];
  let newFamily = JSON.parse(JSON.stringify(family));

_.cloneDeep

現在最強ディープコピー。lodashの一関数遅いと聞くので、もっと早いものがあれば是非...

  const _ = require('lodash');
  let family = ['わい', '弟', '妹', 'おとん', 'おかん', ['嫁', '義妹']];
  let newFamily = _.cloneDeep(family);

lodash.com

TODO: _.cloneDeepの詳細を追う

所感

シャローコピーならスプレッド、ディープコピーなら _.cloneDeep
各種メソッドの整理になってよかったです。良いJavaScriptライフを

1日1コミット

JS_Study/array_copy.js at b820b5d77376ed70c005b86363c0b0ff4c45b9d8 · gonta616/JS_Study · GitHub

参考

yukimonkey.com stackoverflow.com medium.com

Next.jsでFormを作成する

表題の通りで何のひねりもないがw
Formの導入というより、submitの際のデータ取得方法を工夫するととてもすっきりします。

雑だが。コード全文

import React, { useState } from 'react'
import Layout from '../components/layout'
import {
  FormControl,
  TextField,
  RadioGroup,
  FormLabel,
  FormControlLabel,
  Radio,
  Checkbox
} from '@material-ui/core'
import axios from 'axios'

const Form = () => {
  const [title, setTitle] = useState('')
  const [radio, setRadio] = useState('male')
  const [check, setCheck] = useState(true)
  const [date, setDate] = useState(Date.now())
  const [select, setSelect] = useState(0)
  const [aSwitch, setSwitch] = useState(0)

  const [banner, setBanner] = useState('')

  const handleSubmit = async (e) => {
    e.preventDefault()
    const result = await axios.post('/api/demo', { title, radio, check, date, select, aSwitch })
    setBanner(result.data.msg)
  }

  const handleChange = (e) => {
    const fn = {
      title: { fn: setTitle, value: e.target.value },
      radio: { fn: setRadio, value: e.target.value },
      check: { fn: setCheck, value: e.target.checked },
      date: { fn: setDate, value: e.target.checked },
      select: { fn: setSelect, value: e.target.value },
      aSwitch: { fn: setSwitch, value: e.target.value }
    }

    e.preventDefault()
    fn[e.target.name].fn(fn[e.target.name].value)
    console.log(title, radio, check, date, select, aSwitch)
  }

  return (
    <Layout>
      {banner && <div>{banner}</div>}
      <form onSubmit={handleSubmit} role="form">
        <FormControl>
          <TextField name="title" id="title" type="string" onChange={handleChange} />
          <FormControl>
            <FormLabel component="legend">Gender</FormLabel>
            <RadioGroup aria-label="gender" name="radio" value={radio} onChange={handleChange}>
              <FormControlLabel value="female" control={<Radio />} label="Female" />
              <FormControlLabel value="male" control={<Radio />} label="Male" />
              <FormControlLabel value="other" control={<Radio />} label="Other" />
              <FormControlLabel value="disabled" disabled control={<Radio />} label="(Disabled option)" />
            </RadioGroup>
          </FormControl>
          <FormControlLabel
            control={<Checkbox checked={check} onChange={handleChange} name="check" />}
            label="Primary"
          />
          <button type="submit">Submit</button>
        </FormControl>
      </form>
    </Layout>
  )
}

export default Form

jQuery時代の場合は、エレメントを指定して直接値をとっていましたが、Reactでやる場合は、onChengeイベント毎にstateを更新してあげると良い。 Hooksを使用すると関数ライクにかけるよ。

1日1コミット

github.com

Next.jsでExpressを導入する(カスタムサーバー)

Next.jsではカスタムサーバーで、サーバーサイドをNode.jsで実装することが可能です。 これはかなり優秀で、pagesディレクトリ以下はNext.jsのルーティング、apiディレクトリ以下はNode.jsのルーティングというように直感的に分けることが可能です。 nextjs.org

Next.jsはhttpの例ですが、 express でも導入することが可能です。

カスタムサーバーでExpressを導入する

GitHub - gonta616/nextjs-study at 069e9924a9314a8418f4c930abcaf446675802e7

の続きです。

npm i express express-session body-parser cross-env 

server.jsを作ります。以下のようにNext.jsはexpressのルーティングをラップしているだけで、非常に短いコードでカスタムサーバーを構築することが可能です。
expressのエコシステムも問題なく使うことができます。

const express = require('express')
const bodyParser = require('body-parser')
const session = require('express-session')
const next = require('next')

const port = parseInt(process.env.PORT, 10) || 3000
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare().then(() => {
  const server = express()
  server.use(bodyParser.json())
  server.use(session({
    secret: 'zZAai8301bSnuA8sabUwabxe3',
    resave: false,
    saveUninitialized: false,
    cookie: { maxAge: 60000 }
  }))

  server.all('*', (req, res) => {
    return handle(req, res)
  })

  server.listen(port, err => {
    if (err) throw err
    console.log(`> Ready on http://localhost:${port}`)
  })
})

続いて、package.jsonを書き換えます。

  "scripts": {
    "dev": "node server.js",
    "build": "next build",
    "start": "cross-env NODE_ENV=production node server.js",

サーバーを起動すると、変わらずpages以下にアクセスできることがわかります。

npm run dev

肝はこの部分。getRequestHandler でhandlerを作成し、serverのミドルウェアでreq, resを喰わせています。handleはNext.js内でURLを解釈しroutingします。

const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare().then(() => {
  const server = express()

  server.all('*', (req, res) => {
    return handle(req, res)
  })

デモ用のrouterを作成します。

const express = require('express')
const router = express.Router()

router.get('/', (req, res, next) => {
  res.json(`GET OK! query is ${req.query}`)
})

router.get('/:id', (req, res, next) => {
  res.json(`GET OK! params is ${req.params.id}`)
})

router.post('/', (req, res, next) => {
  res.json(`POST OK! params is ${req.body}`)
})

module.exports = router

server.jsでrouterをミドルウェアに登録します。

  server.use('/demo', demoRouter)
  server.all('*', (req, res) => {
    return handle(req, res)
  })

サーバーを再起動すると /demo 以下にアクセスできるようになります。
フロントはpages以下がになってるので、内部APIをカスタムサーバーのroutingに登録すると。綺麗に役割分担できそうです。

所感

  • Next.jsではExpressのカスタムサーバーを簡易に構築することができる
  • Expressのエコシステムが利用可能
  • Viewはpages、内部APIカスタムサーバーのapiと役割づけると良い

1日1コミット

Setup Custom server using express · gonta616/nextjs-study@7c7f8ef · GitHub

Vercelでアプリをデプロイする

Next.jsのアプリケーションをデプロイします。

Next.jsはNode.js v10が動くことが要件なので、様々なプラットフォームで動作します。

今回は、Vercel(元nowという名称)を試してみます。(Next.jsの開発元) Deployまでの道筋が示してあるのはポイントだし、安心感が高いです。

How to Deploy for Next.js Project

https://nextjs.org/ から、右上のDeploy...にアクセスします。

f:id:gonta616:20200516084856p:plain

 Projectのインポート画面が表示されます。(初回の場合はAuth画面)

f:id:gonta616:20200516085133p:plain

From Git Repositoryをクリックし、GitHubのページをクリックすると、Repositoryにあるプロジェクトが表示されます。

f:id:gonta616:20200516085707p:plain

f:id:gonta616:20200516085728p:plain

該当のプロジェクトを選択して次に進みます。
ビルドコマンドや、デプロイコマンドをオプショナルで変更できそう。中身で判断したのか、Next.jsを自動で選択されていました。

f:id:gonta616:20200516090422p:plain

おーー、すごい!ドメインが生えてきた。(ここまでコードは1行もなしw)

f:id:gonta616:20200516090718p:plain

https://nextjs-study-weld.now.sh/

 

中身をみます。おお。。。/pages/list.jsをコミットするのを忘れてました。

f:id:gonta616:20200516090025p:plain

コミットしてmasterにpushします。

f:id:gonta616:20200516091206p:plain

何と自動更新された模様。GitHubのページと連携して自動的に更新されるようです。
masterにマージすればデプロイは難しいこと考えなくて良いですね!!

GitHubフローだとmaster = プロダクションなので、PRでブロック出来ればそれで良さそう

f:id:gonta616:20200516091427p:plain

Next

プロジェクトが何の資料もなく、一発でデプロイできました。(ほんと凄い!)

本番運用を考えると以下が気になります。引き続き調べていこうっと

  • カスタムサーバー(expressはどうか)
  • ブランチを指定できる?
  • ドメインはどう指定する?
  • データベースやストレージなどの連携は?
  • キャッシュはどうする?
  • 負荷対策やレイテンシは(スケールアウト、スケールインは)?
  • 料金...?
  • その他、Vercelの機能(プレビューモードなど)

 

Next.jsにMaterial-UIを入れる

React開発でよく使用していた、Material-UIをNext.jsにも入れたいと思います。 Materil-UIはReactのコンポーネント集で、特にアプリケーションの開発が捗ります。 material-ui.com

Hwo to Setup

導入は想像以上に簡単でした。(Reactの時と一緒)
まずはモジュールをインストール

npm i @material-ui/core @material-ui/icons

各Componentでimportしたら使える。-> importだけで使えるのは楽
/components/header/index.js

import React from 'react'
import Lint from 'next/link'
import AppBar from '@material-ui/core/AppBar'
import Toolbar from '@material-ui/core/Toolbar'
import Typography from '@material-ui/core/Typography'
import IconButton from '@material-ui/core/IconButton'
import MenuIcon from '@material-ui/icons/Menu'
import Button from '@material-ui/core/Button'

const Header = () => {
  return (
    <AppBar position="static">
      <Toolbar>
        <IconButton edge="start" color="inherit" aria-label="menu">
          <MenuIcon />
        </IconButton>
        <Typography variant="h6" >
          Demo
        </Typography>
        <Lint href="/"><a><Button color="inherit">Top</Button></a></Lint>
        <Lint href="/list"><a><Button color="inherit">List</Button></a></Lint>
      </Toolbar>
    </AppBar>
  )
}

export default Header

ComponentとしてLayoutでimportする。
下記ののようにchildrenが子Elementで食わせることで共通部品化可能
/components/layout.js

import React from 'react'
import { Container } from '@material-ui/core'
import Header from './header'
import PropTypes from 'prop-types'

const Layout = ({ children }) => {
  return (
    <>
      <Header></Header>
      <Container maxWidth="lg">
        {children}
      </Container>
    </>
  )
}

Layout.propTypes = {
  children: PropTypes.object
}

export default Layout

最後、これを読み込む。
Next.jsはpages以下を自動的にルーティングしてくれる。 /pages/index.js

import React from 'react'
import Layout from '../components/layout'

export default function Home () {
  return (
    <Layout>
      <p>Hello Next.js</p>
    </Layout>
  )
}

所感

Next.jsでMaterial-UIを使おうと思ったら想像以上に楽だった件。
ReactのSPAを意識しなくても良いので、思考はもっと単純かも。
ちなみに以下の記述はクライアントでの遷移となる

<Lint href="/"><a><Button color="inherit">Top</Button></a></Lint>
<Lint href="/list"><a><Button color="inherit">List</Button></a></Lint>

1日1コミット

[https://github.com/gonta616/nextjs-study:title]

参考

Node.jsからPythonを呼び出す

Node.jsを実装している時に他言語との連携をしたいケースがありました。 APIになってるケースはあるが、さくっと内部から呼び出したい。 プロセスからシェルスクリプトとして呼ぶことで、呼び出すことができます。 スクリプト言語だとコレでいけそう -> bashperlpythonなどなど。

const spawn = require("child_process").spawn;
const pythonProcess = spawn('python',["path/to/script.py", arg1, arg2, ...]);

うん。良い感じ。 終了時のステータス、Webから呼び出すケースを考慮した時はどうでしょうか??

How to Create

expressから呼び出してみます

const spawn = require('child_process').spawn

/**
 * NodeからCLIを呼び出す
 * @param {*} cb コールバック
 */
const callCLI = (cb) => {
  const pythonProcess = spawn('python', [`${process.cwd()}/controllers/script.py`, 'https://www.google.com/?hl=ja'])
  pythonProcess.stdout.on('data', (data) => {
    console.log(`stdout: ${data}`)
  })

  pythonProcess.stderr.on('data', (data) => {
    console.error(`stderr: ${data}`)
  })

  pythonProcess.on('close', (code) => {
    console.log(`child process exited with code ${code}`)
    cb(null, code)
  })
}

module.exports = {
  callCLI
}
#!/usr/bin/env python3
import requests
import sys

if __name__ == '__main__':
  args = sys.argv
  result = requests.get(args[1])
  print( result.text, result.status_code )

肝はここ。Node.jsはシングルスレッドとして動くが、非同期型のイベントドリブンです。 I/O待ちにならないよう、コマンドを呼び出して抜けている。

キャッチするためにコールバック関数で返してあげる

  pythonProcess.on('close', (code) => {
    console.log(`child process exited with code ${code}`)
    cb(null, code)
  })

懸念

ローカルでは問題ないがデプロイ時は、多言語の実行環境は整えてあげる必要がありますね。

1日1コミット

nodecore-study/process.js at master · gonta616/nodecore-study · GitHub

参考