Bắt đầu với AngularJS không khó. Tài liệu là một trong những tài liệu tốt nhất hiện có và các hướng dẫn của nó đủ đơn giản.
Nhưng mọi thứ trở nên phức tạp khi bạn bắt đầu kết hợp các công nghệ.
Nếu bạn đang sử dụng CoffeeScript thay vì JavaScript thông thường, bạn biết rằng cần tính đến các mối quan tâm về tiền xử lý - cũng như sự khác biệt rõ ràng về cú pháp. Đây chỉ là những vấn đề nhỏ, nhưng nếu bạn ném Ruby on Rails, Jasmine và Karma vào hỗn hợp thì sao? Nó phức tạp hơn một cách đáng ngạc nhiên.
Đây chính xác là ngăn xếp mà chúng tôi sẽ sử dụng trong hướng dẫn này. Không phải vì chúng tôi là những kẻ tham ăn để trừng phạt, mà là bởi vì đây là kiểu sắp đặt mà bạn sẽ thấy trong thế giới thực.
Hướng dẫn này giả định rằng bạn cảm thấy thoải mái với Rails, nhưng không nhất thiết phải có AngularJS.
Tạo ứng dụng Rails cơ bản
Vì có rất nhiều lớp công nghệ liên quan, tôi sẽ xây dựng một ứng dụng đơn giản mà hầu như không làm được gì cả. Chúng tôi sẽ thiết lập chức năng CRUD cho các nhà hàng - trên thực tế, chỉ là phần CR. -UD được để lại như một bài tập cho người đọc.;-)
Chúng tôi sẽ gọi ứng dụng là Restauranteur .
Tôi đang sử dụng PostgreSQL và RSpec ở đây, nhưng DBMS và khung kiểm tra phía máy chủ không đáng kể. Bạn có thể sử dụng bất cứ thứ gì bạn muốn.
Thiết lập ban đầu
Đầu tiên tạo dự án:
$ rails new restauranteur --database=postgresql --skip-test-unit
Nếu bạn đang sử dụng Pow, hãy thêm dự án của bạn vào Pow:
$ ln -s /Users/jasonswett/projects/restauranteur ~/.pow/restauranteur
Tạo người dùng cơ sở dữ liệu PostgreSQL:
$ createuser -P -s -e restauranteur
Thêm RSpec vào Gemfile của bạn:
# Gemfile
gem "rspec-rails", "~> 2.14.0"
Cài đặt RSpec:
$ bundle install
$ rails g rspec:install
Tạo cơ sở dữ liệu:
$ rake db:create
Tạo mô hình nhà hàng
Bây giờ chúng ta đã tạo dự án và cơ sở dữ liệu, hãy tạo tài nguyên đầu tiên của chúng ta. Tài nguyên Nhà hàng sẽ chỉ có một thuộc tính: tên , là một chuỗi.
$ rails generate scaffold restaurant name:string
Bây giờ, chỉ để trở thành OCD về nó, chúng tôi sẽ đảm bảo tên nhà hàng là duy nhất.
# 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
Chạy quá trình di chuyển:
$ rake db:migrate
Hãy thêm một số thông số kỹ thuật để xác minh rằng chúng tôi không thể tạo nhà hàng không hợp lệ. Lưu ý rằng lỗi duy nhất gây ra lỗi thô.
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
Việc thêm các trình xác thực này sẽ làm cho các thông số kỹ thuật vượt qua:
class Restaurant < ActiveRecord::Base
validates :name, presence: true, uniqueness: { case_sensitive: false }
end
Bây giờ chúng ta đã sẵn sàng để tiếp tục.
Đưa AngularJS vào hỗn hợp
Thay vì đổ mọi thứ lên bạn cùng một lúc, trước tiên tôi muốn giới thiệu phiên bản "Hello, world" đơn giản nhất của ứng dụng AngularJS-Rails và sau đó xây dựng chức năng CRUD nhà hàng của chúng tôi vào đó.
Không có lý do gì mà trang "Xin chào, thế giới" của chúng tôi phải hoặc nên được gắn với bất kỳ tài nguyên Rails cụ thể nào. Vì lý do này, chúng tôi sẽ tạo một StaticPagesController
để phục vụ trang chủ AngularJS của chúng tôi.
Tạo bộ điều khiển
$ rails generate controller static_pages index
Tuyến gốc của chúng tôi ngay bây giờ chỉ là trang "Chào mừng đến với Rails". Hãy đặt nó thành index
hành động của StaticPagesController
mới của chúng tôi :
# config/routes.rb
Restauranteur::Application.routes.draw do
# Add the following line
root 'static_pages#index'
end
Tải xuống Angular
- Để các thử nghiệm của chúng tôi hoạt động bình thường sau này, chúng tôi sẽ cần một tệp có tên là
angular-mocks.js
. Tôi không nghĩ rằng có bất kỳ đề cập nào về vấn đề này trong tài liệu Angular ở bất kỳ đâu, nhưng điều đó là cần thiết. - Trong hướng dẫn AngularJS, tài liệu liệt kê phiên bản mới nhất, nhưng nếu tôi nhớ lại chính xác, tôi đã gặp sự cố với khả năng tương thích giữa
angular.js
vàangular-mocks.js
cho phiên bản mới nhất. Tôi biết rằng các phiên bản 1.1.5 đã hoạt động cùng nhau, vì vậy mặc dù đó không phải là phiên bản ổn định mới nhất, nhưng đó là phiên bản tôi đang liệt kê ở đây. Tất nhiên, theo thời gian, tình hình tương thích có thể sẽ được cải thiện.
Tải xuống angular.js
và angular-mocks.js
từ code.angularjs.org và di chuyển các tệp vào 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
Thêm nó vào quy trình nội dung
Bây giờ chúng tôi muốn yêu cầu ứng dụng của mình yêu cầu tệp AngularJS và chúng tôi muốn đảm bảo rằng nó được tải trước các tệp khác phụ thuộc vào nó. (Chúng tôi có thể sử dụng một cái gì đó như RequiJS để quản lý những phụ thuộc này và đó có thể là những gì tôi sẽ làm trên một sản phẩm sản xuất, nhưng với mục đích của hướng dẫn này, tôi muốn giữ cho công nghệ mỏng nhất có thể.)
Lưu ý: Angular và Turbolinks có thể xung đột với nhau, vì vậy chúng tôi vô hiệu hóa chúng ở đây
// 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 .
Thiết lập bố cục
Chúng tôi sẽ thêm ng-app và ng-view, báo hiệu rằng chúng tôi có một ứng dụng Angular trong trang của mình. Cũng lưu ý rằng các đề cập về Turbolinks đã bị xóa.
<%= yield %>
Tạo bộ điều khiển Angular
Đầu tiên, hãy tạo một thư mục cho bộ điều khiển của chúng tôi. Bạn có thể đặt tên cho nó bất cứ thứ gì bạn muốn.
$ mkdir -p app/assets/javascripts/angular/controllers
Bây giờ chúng ta hãy tạo tệp điều khiển chính nó. Tôi gọi bộ điều khiển này là "bộ điều khiển gia đình" và quy ước trong Angular là nối tên tệp bộ điều khiển của bạn bằng Ctrl
. Do đó, tên tệp của chúng ta sẽ là 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
]
Thêm tuyến Angular
Bây giờ chúng tôi sẽ thêm một chỉ thị định tuyến để tạo HomeCtrl
của chúng tôi là "trang mặc định" của chúng tôi. Ở đây tôi đang xác định định tuyến của mình trong app/assets/javascripts/main.js.coffee
, nhưng một lần nữa tôi không nghĩ tên tệp quan trọng.
# 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'
})
])
Thêm mẫu Angular
Chúng tôi cũng sẽ muốn có một nơi để lưu giữ các mẫu Angular của chúng tôi. Tôi quyết định đặt của tôi vào public/templates
. Một lần nữa, bạn có thể đặt chúng ở bất cứ đâu bạn muốn.
mkdir public/templates
Nếu chúng tôi tạo một tệp public/templates/home.html
với một số nội dung tùy ý, chúng tôi sẽ có thể xem nội dung đó trong trình duyệt.
This is the home page.
Bây giờ, nếu bạn truy cập https://restauranteur.dev/
(hoặc https://localhost:3000/
nếu bạn không sử dụng Pow) và bạn sẽ thấy nội dung của home.html
.
Một ví dụ về liên kết dữ liệu
Đó là loại thú vị, nhưng có lẽ không ấn tượng lắm. Hãy thực sự gửi một cái gì đó qua dây. Chỉnh sửa app/assets/angular/controllers/HomeCtrl.js.coffee
của bạn như thế này:
# app/assets/angular/controllers/HomeCtrl.js.coffee
@restauranteur.controller 'HomeCtrl', ['$scope', ($scope) ->
$scope.foo = 'bar'
]
Điều này hơi ghê khi nói @foo = "bar"
trong bộ điều khiển Rails. Chúng ta có thể cắm foo
vào mẫu bằng cách sử dụng cú pháp dấu ngoặc kép như sau:
Value of "foo": {{foo}}
Làm điều đó trong thời gian thực
Chúng tôi đã xây dựng một ứng dụng hello world đơn giản. Tạo một ứng dụng CRUD hoàn chỉnh không khó hơn nhiều.
Tạo cơ sở dữ liệu
Làm việc với CRUD nhà hàng của chúng tôi sẽ có ý nghĩa hơn một chút nếu chúng tôi bắt đầu với một số nhà hàng trong cơ sở dữ liệu. Đây là tệp hạt giống mà bạn có thể sử dụng.
# db/seeds.rb
Restaurant.create([
{ name: "The French Laundry" },
{ name: "Chez Panisse" },
{ name: "Bouchon" },
{ name: "Noma" },
{ name: "Taco Bell" },
])
rake db:seed
Tạo trang chỉ mục nhà hàng
Đầu tiên, hãy tạo một thư mục mẫu cho nhà hàng:
mkdir public/templates/restaurants
Mẫu đầu tiên chúng tôi sẽ tạo là trang chỉ mục:
[index](/#)
* {{ restaurant.name }}
Tôi sẽ giải thích ngay những điều này có nghĩa là gì. Đầu tiên, hãy tạo bộ điều khiển:
# 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
)
]
Cuối cùng, chúng tôi sẽ điều chỉnh cấu hình định tuyến của mình:
# 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'
})
])
Bây giờ, cuối cùng, chúng ta có thể đi đến URI /#/restaurants
và chúng tôi sẽ có thể xem danh sách các nhà hàng của chúng tôi. Trước khi chúng ta tiếp tục, hãy thêm một bài kiểm tra.
Thêm thử nghiệm đầu tiên của chúng tôi
Thêm thư mục thử nghiệm JS:
mkdir spec/javascripts
Viết thử nghiệm:
# 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
)
Thêm cấu hình:
// 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'
]
});
};
Cài đặt Karma và khởi động máy chủ:
sudo npm install -g karma
sudo npm install -g karma-ng-scenario
karma start spec/javascripts/restauranteur.conf.js
Nếu bạn truy cập https://localhost:9876/
, thử nghiệm của chúng tôi sẽ chạy và thành công. Nếu bạn muốn kiểm tra không thành công, hãy thay đổi expect(scope.restaurants.length).toBe 0
thành expect(scope.restaurants.length).toBe 1
và chạy lại bài kiểm tra.
Ý nghĩa của thử nghiệm này mà chúng tôi vừa thêm vào rõ ràng là đáng nghi ngờ, nhưng mục đích của tôi ở đây là giúp bạn tiết kiệm công việc tìm ra cách đưa mã Angular của bạn vào khai thác thử nghiệm. Có những thứ nhất định, như bộ tiền xử lý CoffeeScript và angular-mocks.js
sự bao gồm hoàn toàn không rõ ràng và tôi đã mất vài giờ vò đầu bứt tai để đi đúng hướng.
Xây dựng trang nhà hàng
Bây giờ, hãy thực hiện điều chỉnh tạm thời đối với mẫu chỉ mục nhà hàng của chúng tôi:
* {{restaurant.name}} ({{restaurant.id}})
Nếu bây giờ bạn truy cập lại /#/restaurants
, bạn sẽ nhận thấy rằng không có nhà hàng nào có ID của họ. Tại sao chúng trống?
Khi bạn tạo giàn giáo trong Rails 4, nó cung cấp cho bạn một số .jbuilder
tệp:
$ ls -1 app/views/restaurants/*.jbuilder
app/views/restaurants/index.json.jbuilder
app/views/restaurants/show.json.jbuilder
Nếu bạn mở app/views/restaurants/index.json.jbuilder
, bạn sẽ thấy điều này:
# app/views/restaurants/index.json.jbuilder
json.array!(@restaurants) do |restaurant|
json.extract! restaurant, :name
json.url restaurant_url(restaurant, format: :json)
end
Như bạn có thể thấy, nó bao gồm :name
nhưng không phải :id
. Hãy thêm nó:
# app/views/restaurants/index.json.jbuilder
json.array!(@restaurants) do |restaurant|
json.extract! restaurant, :id, :name
json.url restaurant_url(restaurant, format: :json)
end
Nếu bạn lưu tệp và làm mới /#/restaurants
, bạn sẽ thấy các ID xuất hiện.
Bây giờ hãy thay đổi mẫu trở lại như ban đầu:
[index](/#)
* {{ restaurant.name }}
Có thể bạn đã nhận thấy ở một số điểm rằng chúng tôi đang hướng những thứ này vào một thứ gọi là viewRestaurant()
nhưng chúng tôi chưa bao giờ thực sự định nghĩa bất kỳ thứ gì được gọi là viewRestaurant()
. Hãy làm điều đó ngay bây giờ:
# 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}"
]
Quy ước trong Rails là resource_name/:id
ánh xạ tới trang "hiển thị" và đó là những gì chúng tôi sẽ làm ở đây. Hãy tạo một mẫu chương trình, tuyến đường và bộ điều khiển.
# {{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
)
]
Bây giờ nếu bạn làm mới /#/restaurants
và nhấp vào một nhà hàng, bạn sẽ tìm thấy chính mình tại trang hiển thị của nhà hàng đó. Yay!
Đó là tất cả bây giờ
Có thể chúng tôi chưa thấy kết quả đặc biệt ấn tượng, nhưng tôi hy vọng tôi đã giúp bạn tiết kiệm thời gian khi cắm AngularJS vào Rails 4. Tiếp theo, tôi có thể khuyên bạn nên xem intongResource, điều này có thể giúp làm cho chức năng CRUD KHÔ hơn.
Bạn muốn tìm hiểu thêm?
Hãy xem bài đăng tuyệt vời của Adam Anderson, người có Bootstrapping một ứng dụng AngularJS trong loạt Rails 4.0 đã giúp tôi bắt đầu với AngularJS và Rails. Bạn cũng có thể muốn xem qua hướng dẫn của anh ấy, nhưng hướng dẫn này khác ở chỗ tôi cố gắng _really_spoon cung cấp cho bạn tất cả các chi tiết, giảm thiểu khả năng bạn bị mắc kẹt trong đám cỏ dại.