การเริ่มต้นใช้งาน 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
ดาวน์โหลดเชิงมุม
- เพื่อให้การทดสอบของเราทำงานได้อย่างถูกต้องในภายหลัง เราจะต้องมีไฟล์ชื่อ
angular-mocks.js
. ฉันไม่คิดว่ามีการกล่าวถึงสิ่งนี้ในเอกสาร Angular ทุกที่ แต่จำเป็น - ในบทช่วยสอนของ 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-ป้อนรายละเอียดทั้งหมดให้คุณ เพื่อลดโอกาสที่คุณจะติดอยู่ในวัชพืช