Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

เริ่มต้นใช้งาน AngularJS และ Rails 4

การเริ่มต้นใช้งาน AngularJS ไม่ใช่เรื่องยาก เอกสารประกอบเป็นเอกสารที่ดีที่สุดบางส่วนและบทช่วยสอนก็ง่ายพอ

แต่สิ่งต่าง ๆ จะยุ่งยากเมื่อคุณเริ่มผสมผสานเทคโนโลยี

หากคุณกำลังใช้ CoffeeScript แทน JavaScript แบบตรง คุณทราบดีว่ามีข้อกังวลในการประมวลผลล่วงหน้าที่ต้องคำนึงถึง - เช่นเดียวกับความแตกต่างทางไวยากรณ์ที่ชัดเจน สิ่งเหล่านี้เป็นปัญหาเล็กน้อยโดยตัวมันเอง แต่ถ้าคุณโยน Ruby บน Rails, Jasmine และ Karma มารวมกันล่ะ? มันยากขึ้นอย่างน่าประหลาดใจ

นี่คือสแต็กที่เราจะใช้ในบทช่วยสอนนี้ ไม่ใช่เพราะเราเป็นคนตะกละในการลงโทษ แต่ เพราะนี่คือการตั้งค่าแบบที่คุณจะได้เห็นในโลกแห่งความเป็นจริง

บทแนะนำนี้ถือว่าคุณพอใจกับ Rails แต่ไม่จำเป็นต้องเป็น AngularJS

การสร้างแอป Rails พื้นฐาน

เนื่องจากมีหลายชั้นของเทคโนโลยีที่เกี่ยวข้อง ฉันจะสร้างแอปพลิเคชันง่ายๆ ที่แทบไม่ทำอะไรเลย เราจะตั้งค่าฟังก์ชัน CRUD สำหรับร้านอาหาร - อันที่จริง เป็นเพียงส่วน CR -UD เหลือไว้เป็นแบบฝึกหัดสำหรับผู้อ่าน;-)

เราจะเรียกแอปพลิเคชันว่า ร้านอาหาร .

ฉันใช้ PostgreSQL และ RSpec ที่นี่ แต่เฟรมเวิร์กการทดสอบ DBMS และฝั่งเซิร์ฟเวอร์ไม่สำคัญ คุณสามารถใช้อะไรก็ได้ที่คุณต้องการ

การตั้งค่าเริ่มต้น

สร้างโครงการก่อน:

$ rails new restauranteur --database=postgresql --skip-test-unit

หากคุณกำลังใช้ Pow ให้เพิ่มโครงการของคุณไปที่ Pow:

$ ln -s /Users/jasonswett/projects/restauranteur ~/.pow/restauranteur

สร้างผู้ใช้ฐานข้อมูล PostgreSQL:

$ createuser -P -s -e restauranteur

เพิ่ม RSpec ให้กับ Gemfile ของคุณ:

# Gemfile
gem "rspec-rails", "~> 2.14.0"

ติดตั้ง RSpec:

$ bundle install
$ rails g rspec:install

สร้างฐานข้อมูล:

$ rake db:create

การสร้างแบบจำลองร้านอาหาร

ตอนนี้เราได้สร้างโครงการและฐานข้อมูลแล้ว มาสร้างทรัพยากรแรกของเรากัน ทรัพยากรร้านอาหารจะมีแอตทริบิวต์เดียวเท่านั้น:ชื่อ ซึ่งเป็นสตริง

$ rails generate scaffold restaurant name:string

ตอนนี้ เพื่อให้เป็น OCD เราจะทำให้แน่ใจว่าชื่อร้านอาหารมีเอกลักษณ์เฉพาะตัว

# db/migrate/[timestamp]_create_restaurants.rb

class CreateRestaurants < ActiveRecord::Migration
  def change
    create_table :restaurants do |t|
      t.string :name

      t.timestamps
    end

    # Add the following line
    add_index :restaurants, :name, unique: true
  end
end

เรียกใช้การย้ายข้อมูล:

$ rake db:migrate

มาเพิ่มข้อกำหนดบางอย่างเพื่อยืนยันว่าเราไม่สามารถสร้างร้านอาหารที่ไม่ถูกต้องได้ ขอให้สังเกตว่าความล้มเหลวที่ไม่ซ้ำกันทำให้เกิดข้อผิดพลาดดิบ

require 'spec_helper'

describe Restaurant do
  before do
    @restaurant = Restaurant.new(name: "Momofuku")
  end

  subject { @restaurant }

  it { should respond_to(:name) }
  it { should be_valid }

  describe "when name is not present" do
    before { @restaurant.name = " " }
    it { should_not be_valid }
  end

  describe "when name is already taken" do
    before do
      restaurant_with_same_name = @restaurant.dup
      restaurant_with_same_name.name = @restaurant.name.upcase
      restaurant_with_same_name.save
    end

    it { should_not be_valid }
  end
end

การเพิ่มเครื่องมือตรวจสอบเหล่านี้จะทำให้ข้อกำหนดผ่าน:

class Restaurant < ActiveRecord::Base 
  validates :name, presence: true, uniqueness: { case_sensitive: false }
end

ตอนนี้เราพร้อมแล้วที่จะไปต่อ

นำ AngularJS มาผสมผสาน

แทนที่จะทิ้งทุกอย่างให้คุณในครั้งเดียว อันดับแรก ฉันต้องการสาธิตแอปพลิเคชัน AngularJS-Rails เวอร์ชัน "สวัสดี โลก" ที่ง่ายที่สุด แล้วจึงสร้างฟังก์ชัน CRUD ของร้านอาหารของเราลงไป

ไม่มีเหตุผลใดที่หน้า "สวัสดี ชาวโลก" ของเราจะต้องหรือควรเชื่อมโยงกับทรัพยากร Rails ใดโดยเฉพาะ ด้วยเหตุนี้ เราจะสร้าง StaticPagesController เพื่อแสดงหน้าแรกของ AngularJS

สร้างตัวควบคุม

$ rails generate controller static_pages index

เส้นทางรูทของเราตอนนี้เป็นเพียงหน้า "ยินดีต้อนรับสู่ Rails" มาตั้งค่าเป็น index การทำงานของ StaticPagesController . ใหม่ของเรา :

# config/routes.rb

Restauranteur::Application.routes.draw do
  # Add the following line
  root 'static_pages#index'
end

ดาวน์โหลดเชิงมุม

  1. เพื่อให้การทดสอบของเราทำงานได้อย่างถูกต้องในภายหลัง เราจะต้องมีไฟล์ชื่อangular-mocks.js . ฉันไม่คิดว่ามีการกล่าวถึงสิ่งนี้ในเอกสาร Angular ทุกที่ แต่จำเป็น
  2. ในบทช่วยสอนของ AngularJS เอกสารแสดงรายการเวอร์ชันล่าสุดที่รั่วไหล แต่ถ้าฉันจำได้ถูกต้อง ฉันมีปัญหาเรื่องความเข้ากันได้ระหว่างangular.js และ angular-mocks.js สำหรับเวอร์ชันล่าสุด ฉันรู้ว่าเวอร์ชัน 1.1.5 ทำงานร่วมกันได้ ดังนั้นถึงแม้จะไม่ใช่เวอร์ชันเสถียรล่าสุด แต่นั่นเป็นเวอร์ชันที่ฉันกำลังแสดงอยู่ที่นี่ แน่นอนว่าเมื่อเวลาผ่านไป สถานการณ์ความเข้ากันได้อาจจะดีขึ้น

ดาวน์โหลด angular.js และ angular-mocks.js จาก code.angularjs.org และย้ายไฟล์ไปที่ app/assets/javascripts .

$ wget https://code.angularjs.org/1.1.5/angular.js \
https://code.angularjs.org/1.1.5/angular-mocks.js
$ mv angular* app/assets/javascripts

เพิ่มไปยังไปป์ไลน์สินทรัพย์

ตอนนี้ เราต้องการบอกแอปพลิเคชันของเราว่าต้องการไฟล์ AngularJS และเราต้องการให้แน่ใจว่าไฟล์ถูกโหลดก่อนไฟล์อื่นๆ ที่ขึ้นอยู่กับไฟล์นั้น (เราสามารถใช้บางอย่างเช่น RequireJS เพื่อจัดการการพึ่งพาเหล่านี้ และนั่นอาจเป็นสิ่งที่ฉันจะทำกับผลิตภัณฑ์ที่ใช้งานจริง แต่สำหรับจุดประสงค์ของบทช่วยสอนนี้ ฉันต้องการทำให้กองเทคโนโลยีมีความบางที่สุดเท่าที่จะเป็นไปได้)

หมายเหตุ: Angular และ Turbolinks สามารถขัดแย้งกันได้ ดังนั้นเราจึงปิดการใช้งานที่นี่

// app/assets/javascripts/application.js

// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs

// Add the following two lines
//= require angular
//= require main

//= require_tree .

ตั้งค่าเลย์เอาต์

เราจะเพิ่ม ng-app และ ng-view ซึ่งเป็นสัญญาณว่าเรามีแอป Angular ในหน้าของเรา นอกจากนี้ โปรดสังเกตว่ามีการกล่าวถึง Turbolinks ออกแล้ว

  <%= yield %>

การสร้างตัวควบคุมเชิงมุม

ขั้นแรก ให้สร้างไดเร็กทอรีสำหรับคอนโทรลเลอร์ของเรา ตั้งชื่ออะไรก็ได้ตามใจชอบ

$ mkdir -p app/assets/javascripts/angular/controllers

ตอนนี้เรามาสร้างไฟล์คอนโทรลเลอร์กัน ฉันกำลังเรียกตัวควบคุมนี้ว่า "ตัวควบคุมภายในบ้าน" และแบบแผนใน Angular คือการต่อท้ายชื่อไฟล์ตัวควบคุมของคุณด้วยCtrl . ดังนั้นชื่อไฟล์ของเราจะเป็นapp/assets/javascripts/angular/controllers/HomeCtrl.js.coffee :

# app/assets/javascripts/angular/controllers/HomeCtrl.js.coffee

@restauranteur.controller 'HomeCtrl', ['$scope', ($scope) ->
  # Notice how this controller body is empty
]

เพิ่มเส้นทางเชิงมุม

ตอนนี้เราจะเพิ่มคำสั่งการกำหนดเส้นทางเพื่อสร้าง HomeCtrl . ของเรา เป็น "หน้าเริ่มต้น" ของเรา ฉันกำลังกำหนดเส้นทางของฉันใน app/assets/javascripts/main.js.coffee แต่อีกครั้ง ฉันไม่คิดว่าชื่อไฟล์นั้นสำคัญ

# app/assets/javascripts/main.js.coffee

# This line is related to our Angular app, not to our
# HomeCtrl specifically. This is basically how we tell
# Angular about the existence of our application.
@restauranteur = angular.module('restauranteur', [])

# This routing directive tells Angular about the default
# route for our application. The term "otherwise" here
# might seem somewhat awkward, but it will make more
# sense as we add more routes to our application.
@restauranteur.config(['$routeProvider', ($routeProvider) ->
  $routeProvider.
    otherwise({
      templateUrl: '../templates/home.html',
      controller: 'HomeCtrl'
    }) 
])

เพิ่มเทมเพลตเชิงมุม

เราต้องการสถานที่สำหรับเก็บเทมเพลต Angular ของเราไว้ด้วย ฉันตัดสินใจใส่ของฉันในpublic/templates . ย้ำอีกครั้งว่าวางได้ทุกที่ที่ต้องการ

mkdir public/templates

ถ้าเราสร้างไฟล์ public/templates/home.html ด้วยเนื้อหาตามอำเภอใจ เราควรจะสามารถเห็นได้ในเบราว์เซอร์


This is the home page.

ตอนนี้ ถ้าคุณไปที่ https://restauranteur.dev/ (หรือ https://localhost:3000/ หากคุณไม่ได้ใช้ Pow) และคุณควรเห็นเนื้อหาของ home.html .

ตัวอย่างการเชื่อมโยงข้อมูล

นั่นคือ แบบ น่าสนใจ แต่อาจจะไม่น่าประทับใจมาก มาส่งอะไรบางอย่างข้ามสายกัน แก้ไขapp/assets/angular/controllers/HomeCtrl.js.coffeeของคุณ แบบนี้:

# app/assets/angular/controllers/HomeCtrl.js.coffee 

@restauranteur.controller 'HomeCtrl', ['$scope', ($scope) -> 
  $scope.foo = 'bar'
]

คล้ายกับการพูดว่า @foo = "bar" ในตัวควบคุม Rails เราสามารถเสียบfoo ลงในเทมเพลตโดยใช้ไวยากรณ์วงเล็บปีกกาดังนี้:

Value of "foo": {{foo}}  

ลงมือจริงครั้งนี้

เราได้สร้างแอพ Hello World ที่เรียบง่ายแล้ว การสร้างแอปพลิเคชัน CRUD แบบสมบูรณ์นั้นไม่ยากนัก

ตั้งฐานข้อมูล

การทำงานกับร้านอาหาร CRUD ของเราจะมีความหมายมากขึ้นเล็กน้อยหากเราเริ่มต้นด้วยร้านอาหารในฐานข้อมูล นี่คือไฟล์เมล็ดพันธุ์ที่คุณสามารถใช้ได้

# db/seeds.rb

Restaurant.create([
  { name: "The French Laundry" },
  { name: "Chez Panisse" },
  { name: "Bouchon" },
  { name: "Noma" },
  { name: "Taco Bell" },
])
rake db:seed

การสร้างหน้าดัชนีร้านอาหาร

ขั้นแรก ให้สร้างโฟลเดอร์เทมเพลตสำหรับร้านอาหาร:

mkdir public/templates/restaurants

เทมเพลตแรกที่เราจะสร้างคือหน้าดัชนี:

[index](/#)

  * {{ restaurant.name }}

ฉันจะอธิบายในอีกสักครู่ว่าสิ่งเหล่านี้หมายถึงอะไร ขั้นแรก มาสร้างคอนโทรลเลอร์กัน:

# app/assets/javascripts/angular/controllers/RestaurantIndexCtrl.js.coffee

@restauranteur.controller 'RestaurantIndexCtrl', ['$scope', '$location', '$http', ($scope, $location, $http) ->
  $scope.restaurants = []
  $http.get('./restaurants.json').success((data) ->
    $scope.restaurants = data
  )
]

สุดท้าย เราจะปรับการกำหนดค่าการกำหนดเส้นทางของเรา:

# app/assets/javascripts/main.js.coffee

@restauranteur = angular.module('restauranteur', [])

@restauranteur.config(['$routeProvider', ($routeProvider) ->
  $routeProvider.
    when('/restaurants', {
      templateUrl: '../templates/restaurants/index.html',
      controller: 'RestaurantIndexCtrl'
    }).
    otherwise({
      templateUrl: '../templates/home.html',
      controller: 'HomeCtrl'
    })
])

สุดท้ายนี้ เราก็สามารถไปที่ URI /#/restaurants และเราควรจะสามารถดูรายชื่อร้านอาหารของเราได้ ก่อนที่เราจะไปต่อ เรามาเพิ่มการทดสอบกันก่อน

เพิ่มการทดสอบครั้งแรกของเรา

เพิ่มโฟลเดอร์ทดสอบ JS:

mkdir spec/javascripts

เขียนแบบทดสอบ:

# spec/javascripts/controllers_spec.js.coffee

describe "Restauranteur controllers", ->
  beforeEach module("restauranteur")

  describe "RestaurantIndexCtrl", ->
    it "should set restaurants to an empty array", inject(($controller) ->
      scope = {}
      ctrl = $controller("RestaurantIndexCtrl",
        $scope: scope
      )
      expect(scope.restaurants.length).toBe 0
    )

เพิ่มการกำหนดค่า:

// spec/javascripts/restauranteur.conf.js

module.exports = function(config) {
  config.set({
    basePath: '../..',

    frameworks: ['jasmine'],

    autoWatch: true,

    preprocessors: {
      '**/*.coffee': 'coffee'
    }, 

    files: [
      'app/assets/javascripts/angular.js',
      'app/assets/javascripts/angular-mocks.js',
      'app/assets/javascripts/main.js.coffee',
      'app/assets/javascripts/angular/controllers/RestaurantIndexCtrl.js.coffee',
      'app/assets/javascripts/angular/*',
      'spec/javascripts/*_spec.js.coffee'
    ]  
  });
};

ติดตั้ง Karma และเริ่มเซิร์ฟเวอร์:

sudo npm install -g karma
sudo npm install -g karma-ng-scenario
karma start spec/javascripts/restauranteur.conf.js

หากคุณไปที่ https://localhost:9876/ การทดสอบของเราจะดำเนินการและประสบความสำเร็จ หากคุณต้องการเห็นการทดสอบล้มเหลว ให้เปลี่ยน expect(scope.restaurants.length).toBe 0 ถึงexpect(scope.restaurants.length).toBe 1 แล้วทำการทดสอบอีกครั้ง

ความหมายของการทดสอบที่เราเพิ่งเพิ่มเข้าไปนั้นเป็นที่น่าสงสัยอย่างชัดเจน แต่ความตั้งใจของฉันที่นี่คือการช่วยคุณในการหาวิธีนำโค้ด Angular ของคุณไปเป็นสายรัดทดสอบ มีบางอย่าง เช่น ตัวประมวลผลล่วงหน้าของ CoffeeScript และangular-mocks.js การรวมที่ไม่ชัดเจนโดยสิ้นเชิงและต้องใช้เวลาหลายชั่วโมงในการเกาหัวเพื่อให้ถูกต้อง

การสร้างหน้าร้านอาหาร

มาทำการปรับเปลี่ยนเทมเพลตดัชนีร้านอาหารของเราชั่วคราว:

  * {{restaurant.name}} ({{restaurant.id}})

หากคุณกลับมาที่ /#/restaurants . อีกครั้ง คุณจะสังเกตเห็นว่าไม่มีร้านอาหารใดมีรหัสประจำตัว ทำไมมันว่างเปล่า?

เมื่อคุณสร้างนั่งร้านใน Rails 4 มันจะให้ .jbuilder ไฟล์:

$ ls -1 app/views/restaurants/*.jbuilder
app/views/restaurants/index.json.jbuilder
app/views/restaurants/show.json.jbuilder

หากคุณเปิด app/views/restaurants/index.json.jbuilder คุณจะเห็นสิ่งนี้:

# app/views/restaurants/index.json.jbuilder

json.array!(@restaurants) do |restaurant|
  json.extract! restaurant, :name
  json.url restaurant_url(restaurant, format: :json)
end

อย่างที่คุณเห็น มันรวมถึง :name แต่ไม่ใช่ :id . มาเพิ่มกันเถอะ:

# app/views/restaurants/index.json.jbuilder

json.array!(@restaurants) do |restaurant|
  json.extract! restaurant, :id, :name
  json.url restaurant_url(restaurant, format: :json)
end

หากคุณบันทึกไฟล์และรีเฟรช /#/restaurants คุณควรเห็นรหัสปรากฏขึ้น

ตอนนี้เรามาเปลี่ยนเทมเพลตกลับเป็นเหมือนเดิมกันเถอะ:

[index](/#)

  * {{ restaurant.name }}

คุณอาจสังเกตเห็นในบางจุดว่าเรากำลังชี้สิ่งเหล่านี้ไปยังสิ่งที่เรียกว่า viewRestaurant() แต่เราไม่เคยกำหนดสิ่งที่เรียกว่าviewRestaurant() . มาทำกันตอนนี้เลย:

# app/assets/javascripts/angular/controllers/RestaurantIndexCtrl.js.coffee

@restauranteur.controller 'RestaurantIndexCtrl', ['$scope', '$location', '$http', ($scope, $location, $http) ->
  $scope.restaurants = []
  $http.get('./restaurants.json').success((data) ->
    $scope.restaurants = data
  )

  # Add the following lines
  $scope.viewRestaurant = (id) ->
    $location.url "/restaurants/#{id}"
]

ข้อตกลงใน Rails คือ resource_name/:id แมปไปยังหน้า "แสดง" และนั่นคือสิ่งที่เราจะทำที่นี่ มาสร้างเทมเพลตการแสดง เส้นทาง และตัวควบคุมกันเถอะ

# {{restaurant.name}}
# app/assets/javascripts/main.js.coffee

@restauranteur = angular.module('restauranteur', [])

@restauranteur.config(['$routeProvider', ($routeProvider) ->
  $routeProvider.
    when('/restaurants', {
      templateUrl: '../templates/restaurants/index.html',
      controller: 'RestaurantIndexCtrl'
    }).
    when('/restaurants/:id', {
      templateUrl: '../templates/restaurants/show.html',
      controller: 'RestaurantShowCtrl'
    }).
    otherwise({
      templateUrl: '../templates/home.html',
      controller: 'HomeCtrl'
    })
])
# app/assets/javascripts/angular/controllers/RestaurantShowCtrl.js.coffee

@restauranteur.controller 'RestaurantShowCtrl', ['$scope', '$http', '$routeParams', ($scope, $http, $routeParams) ->
  $http.get("./restaurants/#{$routeParams.id}.json").success((data) ->
    $scope.restaurant = data
  )
]

ตอนนี้ถ้าคุณรีเฟรช /#/restaurants และคลิกที่ร้านอาหาร คุณควรพบว่าตัวเองอยู่ที่หน้าแสดงของร้านอาหารนั้น เย้!

แค่นี้ก่อน

เราอาจไม่เห็นผลลัพธ์ที่น่าประทับใจเป็นพิเศษ แต่ฉันหวังว่าจะช่วยคุณประหยัดเวลาในการเชื่อมต่อ AngularJS กับ Rails 4 ต่อไป ฉันอาจแนะนำให้มองหา ngResource ซึ่งจะช่วยให้ฟังก์ชัน CRUD แห้งมากขึ้น

สนใจเรียนรู้เพิ่มเติมไหม

ตรวจสอบโพสต์ที่ยอดเยี่ยมโดย Adam Anderson ซึ่ง Bootstrapping แอป AngularJS ใน Rails 4.0 series ช่วยให้ฉันเริ่มต้นใช้งาน AngularJS และ Rails คุณอาจต้องการอ่านบทช่วยสอนของเขาเช่นกัน แต่บทช่วยสอนนี้แตกต่างในแง่ที่ฉันพยายาม _really_spoon-ป้อนรายละเอียดทั้งหมดให้คุณ เพื่อลดโอกาสที่คุณจะติดอยู่ในวัชพืช