現在 Google App Engine で Golang + React での開発を行っているのですが、学習曲線を高めるために同じ構成で Blog を作ってみました。
いろいろとはまるポイントがあって学ぶポイントが結構あったので折を見て共有したいと思います。とりあえずはてなブログで書きなぐった記事を少しずつ POST して少しずつ成長させたいと思います。メリークリスマス。
[2019/04/02] 上記サイトは Netlify + VuePress で動いています。
現在 Google App Engine で Golang + React での開発を行っているのですが、学習曲線を高めるために同じ構成で Blog を作ってみました。
いろいろとはまるポイントがあって学ぶポイントが結構あったので折を見て共有したいと思います。とりあえずはてなブログで書きなぐった記事を少しずつ POST して少しずつ成長させたいと思います。メリークリスマス。
[2019/04/02] 上記サイトは Netlify + VuePress で動いています。
追記: c.Bind とかする時に type error が起きたりするので Unmarshal も追加
MySQL で nullable なカラムレコードを入れるために struct の定義をこうしたらレコードには null が入らずに空文字が入ってしまいます。 Something.Code には Unique 制約をつけたいので空文字ではなく Null を挿入したい。
type Something struct { ID int `json:"id"` Code string `json:"code"` Name string `json:"name"` }
この問題自体は以下のように sql.NullString を使えば解決します。
type Something struct { ID int `json:"id"` Code sql.NullString `json:"code"` Name string `json:"name"` } s := Something{ID: 1, Code: sql.NullString{"", false}, Name: "something"}
なのですがこれを JSON にすると以下のようになるけどそうじゃないんだという場合にどうすればいいのかというお話です。
{ "id" : 1, "code" : { "String" : "", "Valid" : false }, "name" : "somethen" }
型を定義しました。
type NullString struct { sql.NullString } func (s *NullString) MarshalJSON() ([]byte, error) { if s.Valid { return json.Marshal(s.String) } else { return json.Marshal(nil) } } func (s *NullString) UnmarshalJSON(data []byte) error { var str string json.Unmarshal(data, &str) s.String = str s.Valid = str != "" return nil } func NewNullString(s string) NullString { return NullString{sql.NullString{s, s != ""}} } type Something struct { ID int `json:"id"` Code NullString `json:"code"` Name string `json:"name"` } s := Something{ID: 1, Code: NewNullString(""), Name: "something"}
めでたし。
{ "id" : 1, "code" : null, "name" : "something" }
grunt, gulp, webpack ツールは違えど Javascript を bundle する事前準備で疲弊していることに変わりがない今日この頃ですが、elm-lang は何も考えなくても bundle してくれるので意外とアリかもしれないぞと思ったので練習します。
いい感じの動画があったのでそちらで練習しました。 もしそちらの動画を参考にする場合は使用されている Syntax が 0.18 では削除されていて動かない箇所があるので読み替える必要があります。
[1..3]
-> range 1 3
Html.App.beginnerProgram
-> Html.beginnerProgram
type'
-> type_
% brew install elm % elm --version 0.18.0
% mkdir practice-elm && cd $_ % elm package install -y % elm reactor
create exercise.elm
module Main exposing (..) import Html main = Html.text "Hello World"
確認
% open http://localhost:8000/exercise.elm
create increment.elm
elm 0.18 から Html.App.beginnerProgram
ではなく Html.beginnerProgram
に変更されています
import Html exposing (..) import Html.Events exposing (..) main = Html.beginnerProgram { view = view, model = model, update = update } --model type alias Model = Int model : Model model = 0 --update type Msg = Inc | Dec update : Msg -> Model -> Model update msg model = case msg of Inc -> model + 1 Dec -> model - 1 -- view view : Model -> Html Msg view model = div [] [ button [ onClick Dec ] [ text "-" ] , div [] [ text (toString model) ] , button [ onClick Inc ] [ text "+" ] ]
近年のフロント側の技術の進歩は目覚ましく、いろいろ覚えることがたくさんあるので elm-lang
に対して最初は良い印象はありませんでした。
virtual-dom を採用しているし、 redux の思想と共通するので一度 redux が絡むアプリを触っておくと違和感がないと思います。
So The Elm Architecture is easy in Elm, but it is useful in any front-end project. In fact, projects like Redux have been inspired by The Elm Architecture, so you may have already seen derivatives of this pattern.
あと webpack
不要なのが個人的には嬉しいです。とある週末に「ひさしぶりに家でプログラミング書くか」「以前書いた webpack.config.js をコピーして...」「あれーなんかいろいろ変わってる」「やっと直った」「もう寝ないと:0」みたいな経験するぐらいなら新しい言語触りたい。
exercise.elm: https://www.youtube.com/watch?v=Rf2CkojtxFw
increment.elm: https://www.youtube.com/watch?v=vb7ZdjSblok
以下のワードに興味が無い方はたぶん読まなくても良い記事です。
json-server
と jwt
browsersync
と http-proxy-middleware
npm-run-all --parallel
% mkdir practice-mock-api && cd $_ % mkdir -p mock src/components www % npm init -y
json-server は express がベースとなっているので少し手を加えて jwt
が関わる処理を追加します。
db.json は jsonplaceholder のデータを流用します。
% npm install --save-dev json-server body-parser jsonwebtoken express-jwt % curl http://jsonplaceholder.typicode.com/db -o ~/practice-mock-api/mockServer/db.json
create mockServer.js:
const jsonServer = require('json-server') const server = jsonServer.create() const router = jsonServer.router('./mock/db.json') const jwt = require('jsonwebtoken') const expressJwt = require('express-jwt') const bodyParser = require('body-parser') server.use(bodyParser.urlencoded({ extended: false })) server.use(bodyParser.json()) const SERCRET = "shhhhhhared-secret" function createToken() { return jwt.sign({ exp: Math.floor(Date.now() / 1000) + (60 * 60), // 1 hour data: { username: "okamuuu", roles: ["operator", "admin"], } }, SERCRET) } const token = createToken() console.log("=== create new token ===") console.log(token) jwt.verify(token, SERCRET, (err, decoded) => { console.log("=== decoded ===") console.log(decoded) }) server.post('/auth/login', (req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({token: createToken()})) }) server.post('/auth/refresh', expressJwt({secret: SERCRET}), (req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({token: createToken()})) }) server.use('/api', expressJwt({secret: SERCRET}), router) server.listen(3000, function () { console.log("=== Mock API Server is running ===") console.log("=== USAGE ===") console.log(`curl -H \"Authorization: Bearer ${token}\" http://localhost:3000/api/posts`) })
起動するとこんな感じです。
% node mockServer.js === create new token === eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0ODAzOTAxNTksImRhdGEiOnsidXNlcm5hbWUiOiJva2FtdXV1Iiwicm9sZXMiOlsib3BlcmF0b3IiLCJhZG1pbiJdfSwiaWF0IjoxNDgwMzg2NTU5fQ.s5f1tRvFCwIZeJant3LLyyqWcVFlny4oQRv6KJ3PCRM === decoded === { exp: 1480390159, data: { username: 'okamuuu', roles: [ 'operator', 'admin' ] }, iat: 1480386559 } === Mock API Server is running === === USAGE === curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0ODAzOTAxNTksImRhdGEiOnsidXNlcm5hbWUiOiJva2FtdXV1Iiwicm9sZXMiOlsib3BlcmF0b3IiLCJhZG1pbiJdfSwiaWF0IjoxNDgwMzg2NTU5fQ.s5f1tRvFCwIZeJant3LLyyqWcVFlny4oQRv6KJ3PCRM" http://localhost:3000/api/posts
動作確認をします。無事 JWT で認証する必要がある WEB API の mock ができました。
% TOKEN=`curl -s -X "POST" http://localhost:3000/auth/login | jq .token -r` % curl -s -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/posts | jq length 100
前回の記事で紹介したように BroserSync と webpack を使って Hot Module Replacement
するためのサーバーを作成します。
前回の記事との違いは port の指定と起動する度にブラウザが open しないようにオプションを追加していて、http-proxy-middleware
を使って /api
と /auth
へのリクエストを mockServer へ渡すようにしています。
% npm install --save react react-dom % npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-stage-0 babel-preset-react % npm install --save-dev browser-sync webpack webpack-dev-middleware webpack-hot-middleware http-proxy-middleware
create webpack.config.js
const path = require('path') const webpack = require('webpack') module.exports = { debug: true, devtool: 'inline-source-map', entry: [ 'webpack/hot/dev-server', 'webpack-hot-middleware/client', './src/index.js' ], output: { publicPath: "/", path: path.resolve('www'), filename: "bundle.js", }, plugins: [ new webpack.optimize.OccurenceOrderPlugin(), new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin() ], resolve: { extensions: ['', '.js'], }, module: { loaders:[ { test: /\.jsx?$/, exclude: /node_modules/, loaders: ['babel'] } ] } }
create devServer.js
var webpack = require('webpack') var proxy = require('http-proxy-middleware'); var browserSync = require('browser-sync') var webpackDevMiddleware = require('webpack-dev-middleware') var webpackHotMiddleware = require('webpack-hot-middleware') var config = require('./webpack.config') var bundler = webpack(config) var apiProxy = proxy('/api', { target: 'http://localhost:3000', logLevel: 'debug' }); var authProxy = proxy('/auth', { target: 'http://localhost:3000', logLevel: 'debug' }); browserSync({ port: 8080, open: false, server: { baseDir: config.output.path, middleware: [ apiProxy, authProxy, webpackDevMiddleware(bundler, { publicPath: config.output.publicPath, stats: { colors: true } }), webpackHotMiddleware(bundler) ] }, files: [ 'www/*.html' ] })
create .babelrc
{ "presets": ["es2015", "stage-0", "react"] }
create www/index.html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>title</title> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> </head> <body> <div id="root"></div> <script src="/bundle.js"></script> </body> </html>
create src/index.js
import React from 'react' import { render } from 'react-dom' import App from './components/App' render( <App />, document.getElementById('root') ) if (module.hot) { module.hot.accept('./components/App.js', function() { const NextApp = require('./components/App').default; render(<NextApp/>, document.getElementById('root')); }); }
create scr/components/App.js
import React from 'react' export default (props) => ( <h1>HMR test</h1> )
起動
% node devServer.js
最後に2つのフォアグランドプロセスをまとめて実行、停止できるようにします。
% npm install --save-dev npm-run-all
edit package.json
"scripts": { "mock-server": "node mockServer.js", "dev-server": "node devServer.js", "serve": "npm-run-all --parallel mock-server dev-server" },
rpm run serve
と Ctrl-C
を使って起動、停止ができます。
https://github.com/chimurai/http-proxy-middleware/blob/master/examples/browser-sync/index.js
Browsersync と webpack でやってみます。
少しわかりづらいのですが Live Reload を React Component 単位で行います。
mkdir practice-react-hmr && cd $_ mkdir -p src/components www npm init -y npm install --save-dev browser-sync webpack webpack-dev-middleware webpack-hot-middleware npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-stage-0 babel-preset-react react-hot-loader npm install --save react react-dom
create webpack.config.js
const path = require('path') const webpack = require('webpack') module.exports = { debug: true, devtool: 'inline-source-map', entry: [ 'webpack/hot/dev-server', 'webpack-hot-middleware/client', './src/index.js' ], output: { publicPath: "/", path: path.resolve('www'), filename: "bundle.js", }, plugins: [ new webpack.optimize.OccurenceOrderPlugin(), new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin() ], resolve: { extensions: ['', '.js'], }, module: { loaders:[ { test: /\.jsx?$/, exclude: /node_modules/, loaders: ['babel'] } ] } }
create devServer.js
var webpack = require('webpack') var browserSync = require('browser-sync') var webpackDevMiddleware = require('webpack-dev-middleware') var webpackHotMiddleware = require('webpack-hot-middleware') var config = require('./webpack.config') var bundler = webpack(config) browserSync({ server: { baseDir: config.output.path, middleware: [ webpackDevMiddleware(bundler, { publicPath: config.output.publicPath, stats: { colors: true } }), webpackHotMiddleware(bundler) ] }, files: [ 'www/*.html' ] })
create .babelrc
{ "presets": ["es2015", "stage-0", "react"], "plugins": ["react-hot-loader/babel"] }
create www/index.html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>title</title> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> </head> <body> <div id="root"></div> <script src="/bundle.js"></script> </body> </html>
create src/index.js
import React from 'react' import { render } from 'react-dom' import App from './components/App' render( <App />, document.getElementById('root') ) if (module.hot) { module.hot.accept('./components/App.js', function() { const NextApp = require('./components/App').default; render(<NextApp/>, document.getElementById('root')); }); }
create scr/components/App.js
import React from 'react' export default (props) => ( <h1>HMR test</h1> )
run server
node devServer.js
ブラウザを確認しながら scr/components/App.js
を編集するとブラウザを reoload せずに component だけ refresh することができます。
ユーザー画面と管理画面を SPA で作りたいんだけど component とか再利用したい時のお話です
mkdir practice-webpack-output && cd $_ mkdir -p src/{app,admin} www/{app,admin} npm init -y npm install --save-dev webpack babel-core babel-loader babel-preset-es2015
create .babelrc
echo '{"presets": ["es2015"]}' > .babelrc
create webpack.config.js
const path = require('path') const webpack = require('webpack') module.exports = { entry: { "app/app": "./src/app/index.js", "admin/admin": "./src/admin/index.js" }, output: { path: "./www", filename: "[name].bundle.js", }, devtool: 'inline-source-map', resolve: { extensions: ['', '.js'], }, module: { loaders:[ { test: /\.js$/, exclude: /node_modules/, loader: 'babel' }, ] } }
共通処理を記述
echo 'console.log("common")' > src/common.js
ユーザー画面用の処理を記述
echo 'import "../common"' > src/app/index.js echo 'console.log("app")' >> src/app/index.js
管理画面の処理を記述
echo 'import "../common"' > src/admin/index.js echo 'console.log("admin")' >> src/admin/index.js
$(npm bin)/webpack
確認
% node www/app/app.bundle.js common app % node www/admin/admin.bundle.js common admin
http://webdesign-dackel.com/2015/09/10/webpack-multiple-output/
User.birthday や Diary.ymd みたいな date 型を指定したい場合は struct を以下のようにすれば良いらしい。 Update するときは Query に hms つけた状態でも ymd だけ更新されます。 Find するときは ymd 形式に変えてあげればいいと思います。
package main import ( "fmt" "time" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" ) type Diary struct { ID int Title string Ymd time.Time `sql:"not null;type:date"` } func main() { db, err := gorm.Open("mysql", "root:@/go_tutorial?charset=utf8&parseTime=True&loc=Local") if err != nil { panic("failed to connect database") } db.LogMode(true) db.AutoMigrate(&Diary{}) db.Delete(&Diary{}) ymd := time.Date(2014, time.December, 31, 0, 0, 0, 0, time.UTC) db.Create(&Diary{Title: "titile", Ymd: ymd}) var diary Diary err = db.Where("ymd = ?", ymd.Format("2006-01-02")).First(&diary).Error if err != nil { panic(err) } newYmd := time.Date(2015, time.December, 31, 0, 0, 0, 0, time.UTC) diary.Ymd = newYmd db.Save(diary) err = db.Where("ymd = ?", newYmd.Format("2006-01-02")).First(&diary).Error if err != nil { panic(err) } fmt.Println(diary) }
実行結果
[2016-11-16 16:37:13] [0.36ms] DELETE FROM `diaries` [2016-11-16 16:37:13] [0.44ms] INSERT INTO `diaries` (`title`,`ymd`) VALUES ('titile','2014-12-31T00:00:00Z') [2016-11-16 16:37:13] [0.50ms] SELECT * FROM `diaries` WHERE (ymd = '2014-12-31') ORDER BY `diaries`.`id` ASC LIMIT 1 [2016-11-16 16:37:13] [0.66ms] UPDATE `diaries` SET `title` = 'titile', `ymd` = '2015-12-31T00:00:00Z' WHERE `diaries`.`id` = '13' [2016-11-16 16:37:13] [0.59ms] SELECT * FROM `diaries` WHERE `diaries`.`id` = '13' AND ((ymd = '2015-12-31')) ORDER BY `diaries`.`id` ASC LIMIT 1 {13 titile 2015-12-31 00:00:00 +0900 JST}