内容
今更感は満載ではありますが、以前にBackbone.jsを練習する為にSPAでCRUDなアプリケーションを作成したので、書き残しておきたいと思います。
サーバー側はnode.js、クライアント側はBackbone.js、タスクランナーはgulpでバンドルはwebpackを使用しています。
Githubのソースコードはこちら。
練習用で制作したため、所々に拙い部分はあるかと思いますが、ご容赦ください。
開発環境
- html
- css
- sass
- materialize.css
- backbone.js
- underscore.js
- jquery
- node.js (APIサーバー)
- gulp
- webpack
- coffeescript
- babel
- html-loader
ファイル構成
- app/
- api-server/
- assets/
- coffee/
- collections/
- models/
- views/
- config.coffee
- router.coffee
- sass/
- coffee/
- dist/
- template/
- create.html
- detail.html
- index.html
- index.html
- .babelrc
- gulpfile.js
- package.json
- webpack.config.js
環境設定
var webpack = require('gulp-webpack').webpack
var path = require('path')
var current = process.cwd()
module.exports = {
entry:{
main: './app/assets/coffee/router.coffee'
},
output:{
filename: "[name].bundle.js"
},
//出力するファイル名
resolve: {
extensions: ['', '.js', '.coffee'],
root: path.join(current, '/app/'),
},
module:{
loaders: [
{test: /\.coffee$/, loader: 'babel-loader!coffee-loader'},
{test: /\.html$/, loader: 'html?minimize'}
],
options: {
presets: ['es2015']
}
},
}
htmlローダー、BabelやCoffeescriptのローダーを入れています。
サーバー側
const express = require('express');
const router = express.Router();
const test = require('../public/javascripts/data');
// 一覧
router.get('/', (req, res, next) => {
res.json(test.datas);
});
// データ詳細
router.get('/:id', (req, res, next) => {
// GETリクエストされたパラメータと合致するデータを抽出する。
const resData = test.datas.filter((data) => {
return req.params.id == data.id
})
res.json(resData[0]);
});
// データ作成
router.post('/', (req, res, next) => {
// postされたデータをマージする。
let data = Object.assign(
{id: null, title: null,body: null},req.body)
if(data.id === null){
// POSTされたデータの中にIDがない場合にデータのIDを準備しておく。
data = test.createId(data);
} else {
const isExist = test.chkExistId(data);
if(isExist != null){
res.send('id exist!')
return
}
}
// データを追加する。
test.datas.push(data)
res.json(test.datas)
})
router.put('/:id',(req, res, next) => {
// PUTリクエストされたパラメータと合致するデータのインデックスを抽出する。
const index = test.datas.map((data, i) => {
return data.id + ''
}).indexOf(req.params.id + '')
if(index == -1){
res.send('data no exist!');
return
}
// データを更新する。
test.datas[index] = {
id: req.params.id,
title:req.body.title,
body:req.body.body
}
res.json(test.datas)
})
router.delete('/:id',(req, res, next) => {
// DELETEリクエストされたパラメータと合致するデータのインデックスを抽出する。
const index = test.datas.map((data, i) => {
return data.id + ''
}).indexOf(req.params.id + '')
if(index == -1){
res.send('data no exist!');
return
}
// データを削除する。
test.datas.splice(index, 1)
res.json(test.datas)
})
module.exports = router;
APIサーバー側はnode.jsで作成しています。
初期表示するデータは仮に用意して、Backbone.jsで使い易いようにRESTfulに作っています。
クライアント側
config = require './config'
postListsView = require './views/postLists'
postDetailView = require './views/postDetail'
postCreateView = require './views/postCreate'
Router = Backbone.Router.extend({
initialize: ->
@rootElm = $('#app')
routes :
'create' : 'create',
'detail(/:id)' : 'detail',
'' : 'index',
_tmplRender: (view) ->
@rootElm.empty()
@rootElm.html $(config.tmpl[view]).html()
index : ->
@_tmplRender 'index'
postListsView.init()
create : ->
@_tmplRender 'create'
postCreateView.init()
detail : (id) ->
@_tmplRender 'detail'
postDetailView.init(id)
})
router = new Router()
$( () ->
Backbone.history.start();
)
Backbone.Routerで各ページのルーティングをおこなっています。
tmplLists = {}
tmplLists.index = require 'html!template/index.html'
tmplLists.detail = require 'html!template/detail.html'
tmplLists.create = require 'html!template/create.html'
テンプレートの不要な文字を削除する。
_.each tmplLists, (tmpl, key) ->
tmpl = tmpl.slice 18, tmpl.length-2
tmpl = tmpl.replace /\n/g, ''
.replace /\\r/g, ''
.replace /\\t/g, ''
.replace /\\n/g, ''
.replace /\\/g, ''
.replace /> </g, '><'
tmplLists[key] = tmpl
module.exports =
tmpl:
index: tmplLists.index,
detail: tmplLists.detail,
create: tmplLists.create
SPAで切り替えるコンテンツの中身をJSで扱えるようHTMLローダーを使っていますが、その過程での不要な文字を削除しています。
listView = require './postList'
postCollection = require '../collections/postCollection'
UserListsView = Backbone.View.extend({
collection: postCollection
init: ->
postCollection.fetch(
success: (collection, res, opt) =>
@$el = $('#userList')
@render()
)
render: ->
if _.isEmpty postCollection.models
@$el.addClass 'data-none'
.text 'post nothing...'
return
_.each postCollection.models, (user) =>
list = new listView(model:user.attributes)
@.$el.append list.render().el
return @
})
module.exports = new UserListsView()
一覧ページのリスト全体
module.exports = Backbone.View.extend({
tagName: 'tr'
template: _.template '<td><a href="#detail/<%= id %>"><%= title %></a></td><td><%= body %></td>'
render: ->
@.$el.append(@template(@model))
return @;
});
一覧ページのリストアイテム
postModel = require '../models/post'
PostDetailView = Backbone.View.extend({
init: (id) ->
@id = id || null
@url = 'http://localhost:3010/users/' + @id
@$el = $('#detail')
@$elm =
update: @$el.find('#update').children()
inputTitle: @$el.find('#title')
inputBody: @$el.find('#body')
@setModel()
# イベントを設定する。
@delegateEvents(
'click #delete':'del'
'click #update':'update'
)
@post.on 'locationChange', @locationChange, @
@post.on 'invalid', @valiMessage, @
# リクエストのIDに応じたモデルを取得して初期値を設定する。
setModel: ->
@post = new postModel()
@post = _.extend @post,
url: @url
@post.fetch(
success: =>
@$elm.inputTitle.val @post.attributes.title
@$elm.inputBody.val @post.attributes.body
)
# 表示されている投稿を削除する。
del: ->
res = window.confirm('削除してよろしいですか?')
if res
@post.destroy(
success: =>
@post.trigger 'locationChange'
error: =>
location.reload()
)
# 表示されている投稿を更新する
update:->
data =
id: @id
title: @$elm.inputTitle.val()
body: @$elm.inputBody.val()
@post.save(data,
success: =>
@post.trigger 'locationChange'
)
# バリデートエラーメッセージを表示する。
valiMessage: (errors) ->
$('.vali-error-txt').empty()
$([@$elm.inputTitle[0], @$elm.inputBody[0]]).removeClass 'vali-error'
_.each errors.validationError, (error) =>
$('.error' + error.name).html error.message
$('#' + error.name).addClass 'vali-error'
locationChange: ->
location.replace '#'
})
module.exports = new PostDetailView()
詳細ページ
postModel = require '../models/post'
PostCreateView = Backbone.View.extend({
init: ->
@$el = $('#create')
@$elm =
inputTitle: @$el.find('#title')
inputBody: @$el.find('#body')
@post = new postModel()
@post = _.extend @post,
url: 'http://localhost:3010/users/'
# イベントを設定する。
@delegateEvents(
"click #create-btn":'saveModel'
)
@post.on 'locationChange', @locationChange, @
@post.on 'invalid', @valiMessage, @
# 投稿を作成する
saveModel:->
data =
title: @$elm.inputTitle.val()
body: @$elm.inputBody.val()
@post.save(data,
success: =>
@post.trigger 'locationChange'
)
# バリデートエラーメッセージを表示する。
valiMessage: (errors) ->
$('.vali-error-txt').empty()
$([@$elm.inputTitle[0], @$elm.inputBody[0]]).removeClass 'vali-error'
_.each errors.validationError, (error) =>
$('.error' + error.name).html error.message
$('#' + error.name).addClass 'vali-error'
locationChange: ->
location.replace '#'
})
module.exports = new PostCreateView()
作成ページ
まとめ
作成したのは結構前ですが、初めてSinglePageApplicationを作成したので、仕組みなどが理解できて非常に勉強になりました。
次はSPA+SSRなアプリケーションをnuxtあたりで作ってみたいと目論んでいます。