1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Backbone.jsでSingle Page Application

Last updated at Posted at 2019-04-28

内容

今更感は満載ではありますが、以前に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/
    • dist/
    • template/
      • create.html
      • detail.html
      • index.html
    • index.html
    • .babelrc
    • gulpfile.js
    • package.json
    • webpack.config.js

環境設定

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のローダーを入れています。

サーバー側

users.js
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に作っています。

クライアント側

router.coffee
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で各ページのルーティングをおこなっています。

config.coffee
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ローダーを使っていますが、その過程での不要な文字を削除しています。

views/postLists.coffee
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()

一覧ページのリスト全体

views/postList.coffee
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 @;
});

一覧ページのリストアイテム

views/postDetail.coffee
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()

詳細ページ

postCreate.coffee
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あたりで作ってみたいと目論んでいます。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?