Эта статья была первоначально опубликована на jessenovotny.com .
Когда я начал программировать свое самое первое одностраничное приложение Angular (SPA), я заметил, что ресурсы для настройки и интеграции с Devise были тонкими или фрагментированными. Самое полезное руководство, которое я нашел, было на самом деле просто частью общего руководства по Angular с Rails. Были и другие ресурсы, которые были либо слишком сложными, либо продвинутыми, и они на самом деле не проходили первые шаги ребенка. Одна из самых сложных задач для нового программиста начинается с нуля. Я знаю, потому что я один из этих людей.
Большая часть того, что я узнал из моего онлайн-курса, была представлена в виде небольших, все более совершенных компонентов. Я открываю лабораторию, и основы уже заложены, так что нет никакой практики в настройке приложения с чистого листа. Ради времени завершения курса это имеет смысл. Кроме того, вам нужно всего лишь создать пару приложений с нуля, чтобы понять, как это делается. Если вы еще не добрались туда, это прохождение будет прямо по вашему переулку.
После того, как я наконец заработал все части и мой первый Angular проект был запущен и запущен, я почувствовал, что уместно отдать его сообществу. Поскольку в настоящее время у меня не хватает «очков репутации», чтобы отвечать на вопросы о переполнении стека, следующей лучшей вещью было бы сделать мое собственное пошаговое руководство по настройке Angular SPA на Rails с Devise и Bootstrap. Вот то, что я хотел бы найти в своем первоначальном исследовании по этой теме.
Конечно, огромная часть веб-разработки способна решать сложные проблемы, не передавая решение. Я чувствую, что иногда новому разработчику просто нужна рука помощи. Так и здесь.
Начиная
Это руководство предназначено для дайвинга для начала. Предполагается, что у вас уже есть базовое понимание Angular, Rails, Devise и Bootstrap. Я решил не изучать Active Record, но я касаюсь Active Model Serializer, так как это необходимо для отправки моделей в ваш интерфейс JavaScript. Существует гораздо больше, чтобы узнать об этом предмете, и это потребует отдельной серии руководств. Аналогично, я только начинаю установку Bootstrap до такой степени, что могу убедиться, что она работает.
Не стесняйтесь читать вместе с видео, которое я создал для этого урока:
Настройка
Для начала вы хотите открыть терминал и перейти к папке, в которой вы хотите создать новое приложение. В этой демонстрации я на рабочем столе.
В терминале вы запустите $ rails new YOUR-APP
которое инициализирует Rails, создает каталог со всей структурой и объединяет все выпеченные в гемах. (Если вы незнакомы, $
обозначает команду терминала.)
Откройте ваш Gemfile
, удалите gem 'turbolinks'
и добавьте следующее:
gem 'bower-rails' gem 'devise' gem 'angular-rails-templates' #=> allows us to place our html views in the assets/javascripts directory gem 'active_model_serializers' gem 'bootstrap-sass', '~> 3.3.6' #=> bootstrap also requires the 'sass-rails' gem, which should already be included in your gemfile
Хотя Бауэр не является необходимым для этого проекта, я решил использовать его по одной простой причине: опыт. Рано или поздно я, вероятно, обнаружу, что работаю над приложением, созданным с помощью Bower, так почему бы не начать играть с ним сейчас?
Что такое Бауэр? Вы можете узнать больше на их сайте, bower.io, но, насколько я могу судить, это по сути менеджер пакетов, такой как Ruby gems или npm. Вы можете установить его с помощью npm, но я решил включить гем bower-rails
для этого руководства.
Инициализация драгоценных камней, создание базы данных и добавление миграции
Теперь мы собираемся установить / инициализировать эти гемы, создать нашу базу данных, добавить миграцию, чтобы пользователи могли зарегистрироваться с именем пользователя, а затем применить эти миграции к нашей схеме с помощью следующих команд:
$ bundle install $ rake db:create #=> create database $ rails g bower_rails:initialize json #=> generates bower.json file for adding "dependencies" $ rails g devise:install #=> generates config/initializers/devise.rb, user resources, user model, and user migration with a TON of default configurations for authentication $ rails g migration AddUsernametoUsers username:string:uniq #=> generates, well, exactly what it says. $ rake db:migrate
К тому времени, когда у вас будет импульс для создания вашего приложения, у вас, вероятно, будет гораздо больше зависимостей или «пакетов», но вот что вам нужно для начала. Добавьте следующие зависимости от поставщиков в bower.json
:
... "vendor": { "name": "bower-rails generated vendor assets", "dependencies": { "angular": "v1.5.8", "angular-ui-router": "latest", "angular-devise": "latest" } }
После того как вы сохранили эти изменения в bower.json, вам нужно установить эти пакеты с помощью следующей команды, а затем сгенерировать ваш сериализатор пользователя из гема ‘active-model-serializer’, установленного ранее:
$ rake bower:install $ rails g serializer user
Найдите app / serializers / user_serializer.rb и добавьте , :username
сразу после attributes :id
чтобы, когда Devise запрашивал информацию о пользователе у Rails, вы могли отобразить выбранное имя пользователя. Это гораздо лучше, чем сказать «Добро пожаловать, [email protected]» или еще хуже: «Добро пожаловать, 5UPer $ 3CREtP4SSword». Шучу, но серьезно, не делай этого.
Добавьте следующее в config/application.rb
прямо под class Application < Rails::Application
:
config.to_prepare do DeviseController.respond_to :html, :json end
Поскольку Angular будет запрашивать информацию о пользователе, использующем .json
, мы должны убедиться, что DeviseController ответит соответствующим образом, что по умолчанию не выполняется.
Завершение внутренней настройки
Мы приближаемся к завершению нашего бэк-энда. Еще несколько настроек …
Откройте config/routes.rb
и добавьте следующую строку в devise_for :users
: root 'application#index'
. Затем замените содержимое app/controllers/application_controller.rb
следующим фрагментом:
class ApplicationController < ActionController::Base protect_from_forgery with: :exception before_action :configure_permitted_parameters, if: :devise_controller? skip_before_action :verify_authenticity_token respond_to :json def index render 'application/index' end protected def configure_permitted_parameters added_attrs = [:username, :email, :password, :password_confirmation, :remember_me] devise_parameter_sanitizer.permit :sign_up, keys: added_attrs devise_parameter_sanitizer.permit :account_update, keys: added_attrs end end
Мы сделали несколько вещей здесь. Во-первых, мы говорим Rails, что :json
— наш друг; наш единственный вид живет в views/application/index.html.erb
; не беспокойтесь о токенах аутентичности, когда вам звонит Devise; о, и наш пользователь будет иметь имя пользователя.
Затем откройте app/controllers/users_controller.rb
и убедитесь, что вы можете получить доступ к пользователю в формате JSON с помощью любого запроса /users/:id.json
:
class UsersController < ApplicationController def show user = User.find(params[:id]) render json: user end end
Не беспокойтесь о настройке ресурса :show
в routes.rb
Devise уже сделал это для нас!
По умолчанию Rails инициализируется с помощью views/layouts/application.html.erb
, но мы этого не хотим (точнее, я этого не хочу), поэтому сделайте следующее:
- Переместите этот файл в
app/views/application/
. - Переименуйте его в
index.html.erb
. - Замените
<%= yield %>
на<ui-view></ui-view>
(мы не будем отображать никаких ошибок, кроме тегов script / style в нашем заголовке). - Удалите все упоминания о «turoblinks» в тегах скрипта и erb таблицы стилей.
- Добавьте
ng-app="myApp"
в качестве атрибута<body>
. Когда мы запускаем наш сервер, Angular будет загружать и отчаянно искать в нашей DOM, прежде чем инициализировать наше приложение.
Последний шаг к настройке нашего бэк-энда — это разработка нашего конвейера активов. Бауэр уже установил для нас кучу вещей в vendor/assets/bower_components
. Аналогично, мы установили кучу сладких драгоценных камней ранее. Давайте удостоверимся, что наше приложение может найти эти сценарии и таблицы стилей:
Требуется следующее в app/assets/javascripts/application.js
:
//= require jquery //= require jquery_ujs //= require angular //= require angular-ui-router //= require angular-devise //= require angular-rails-templates //= require bootstrap-sprockets //= require_tree .
Примечание: не забудьте удалить require turbolinks
Наконец, мы должны переименовать app/assets/stylesheets/application.css
в application.scss
и добавить эти две строки @import
в конце нашей таблицы стилей:
* *= require_tree . *= require_self */ @import "bootstrap-sprockets"; @import "bootstrap";
Boom !! Теперь у нас все настроено, и мы можем начать работать над нашим интерфейсом.
Передний конец
Вот предварительный просмотр того, как будет выглядеть наше дерево приложений Angular. Поскольку мы установили гем ‘angular-templates’, мы можем хранить все наши HTML-файлы в каталоге assets/javascripts
вместе со всеми другими нашими Angular-файлами:
/javascripts/controllers/AuthCtrl.js /javascripts/controllers/HomeCtrl.js /javascripts/controllers/NavCtrl.js /javascripts/directives/NavDirective.js /javascripts/views/home.html /javascripts/views/login.html /javascripts/views/register.html /javascripts/views/nav.html /javascripts/app.js /javascripts/routes.js
Перво- app.js
: давайте объявим наше приложение в app.js
и app.js
необходимые зависимости:
(function(){ angular .module('myApp', ['ui.router', 'Devise', 'templates']) }())
Я использую IIFE здесь, по причинам, объясненным в этой цитате:
Обертывание ваших компонентов AngularJS в выражении немедленного вызова функции (IIFE). Это помогает не дать переменным и объявлениям функций жить дольше, чем ожидалось, в глобальной области, что также помогает избежать конфликтов переменных. Это становится еще более важным, когда ваш код минимизируется и объединяется в один файл для развертывания на рабочем сервере, предоставляя переменную область действия для каждого файла. — Codestyle.co AngularJS Руководство
Routes.js
Далее мы собираемся routes.js
наш файл routes.js
Отчасти это на шаг впереди того, где мы сейчас находимся, но я бы лучше убрал это с дороги сейчас, чем вернулся бы:
angular .module('myApp') .config(function($stateProvider, $urlRouterProvider){ $stateProvider .state('home', { url: '/home', templateUrl: 'views/home.html', controller: 'HomeCtrl' }) .state('login', { url: '/login', templateUrl: 'views/login.html', controller: 'AuthCtrl', onEnter: function(Auth, $state){ Auth.currentUser().then(function(){ $state.go('home') }) } }) .state('register', { url: '/register', templateUrl: 'views/register.html', controller: 'AuthCtrl', onEnter: function(Auth, $state){ Auth.currentUser().then(function(){ $state.go('home') }) } }) $urlRouterProvider.otherwise('/home') })
То, что мы только что сделали, называется нашим угловым приложением «myApp» и называется функцией config, передавая в качестве параметров $stateProvider
и $routerUrlProvider
. Сразу же мы можем вызвать $stateProvider
и начать цепочку .state()
, которые принимают два параметра: имя состояния (например, «home») и объект данных, который описывает состояние, например его URL, шаблон HTML и какой контроллер использовать. Мы также используем $urlRouterProvider
просто для того, чтобы убедиться, что пользователь не может перемещаться никуда, кроме как в наши заранее определенные состояния.
Несколько вещей, с которыми вы, возможно, еще не знакомы, это onEnter
, $state
и Auth
. Мы вернемся к этому позже.
Теперь давайте home.html
наши home.html
и HomeCtrl.js
:
<div class="col-lg-8 col-lg-offset-2"> <h1>{{hello}}</h1> <h3 ng-if="user">Welcome, {{user.username}}</h3> </div>
angular .module('myApp') .controller('HomeCtrl', function($scope, $rootScope, Auth){ $scope.hello = "Hello World" })
Вы можете прокомментировать состояния входа / регистрации и запустить $ rails s
чтобы убедиться, что все работает. Если это так, вы увидите большой красивый «Hello World». Если он направлен сверху вниз к середине, сделайте глубокий вздох облегчения, потому что Bootstrap начинает действовать, и этот col-lg
материал хорошо позиционирует его, а не застревает в верхнем левом углу.
Angular провел поиск в DOM, нашел атрибут ng-app
, инициализировал «myApp», по умолчанию переместился в /home
с нашего маршрутизатора, HomeCtrl
директиву <ui-view>
, HomeCtrl
наш HomeCtrl
, HomeCtrl
объект $scope
, добавил ключ hello
, присвоил ему значение "Hello World"
, а затем home.html
с этой информацией в элементе <ui-view>
. Оказавшись в представлении, Angular сканирует любые значимые команды, такие как привязки {{...}}
и директива ng-if
и при необходимости отображает информацию контроллера. Я признаю, что порядок этих операций может быть немного неточным, но вы понимаете, что происходит под капотом.
Создание файлов AuthCtrl.js и login.html / register.html
Поскольку у нас есть вся эта AuthCtrl.js
login.html
информация, давайте AuthCtrl.js
наши AuthCtrl.js
и login.html
/ register.html
:
# login.html <div class="col-lg-8 col-lg-offset-2"> <h1 class="centered-text">Log In</h1> <form ng-submit="login()"> <div class="form-group"> <input type="email" class="form-control" placeholder="Email" ng-model="user.email" autofocus> </div> <div class="form-group"> <input type="password" class="form-control" placeholder="Password" ng-model="user.password"> </div> <input type="submit" class="btn btn-info" value="Log In"> </form> </div>
# register.html <div class="col-lg-8 col-lg-offset-2"> <h1 class="centered-text">Register</h1> <form ng-submit="register()"> <div class="form-group"> <input type="email" class="form-control" placeholder="Email" ng-model="user.email" autofocus> </div> <div class="form-group"> <input type="username" class="form-control" placeholder="Username" ng-model="user.username" autofocus> </div> <div class="form-group"> <input type="password" class="form-control" placeholder="Password" ng-model="user.password"> </div> <input type="submit" class="btn btn-info" value="Log In"> </form> <br> <div class="panel-footer"> Already signed up? <a ui-sref="home.login">Log in here</a>. </div> </div>
Прежде чем я захлестну вас с помощью AuthCtrl
, я просто хочу отметить, что большая часть того, что вы видите, это классы начальной загрузки CSS, так что вы все очень впечатлены тем, насколько красиво это выглядит. Игнорируйте все атрибуты класса, и все остальное должно быть довольно знакомым, например, ng-submit
, ng-model
и ui-sref
, который занимает места нашего обычного атрибута тега привязки href
. Теперь для AuthCtrl … вы готовы?
angular .module('myApp') .controller('AuthCtrl', function($scope, $rootScope, Auth, $state){ var config = {headers: {'X-HTTP-Method-Override': 'POST'}} $scope.register = function(){ Auth.register($scope.user, config).then(function(user){ $rootScope.user = user alert("Thanks for signing up, " + user.username); $state.go('home'); }, function(response){ alert(response.data.error) }); }; $scope.login = function(){ Auth.login($scope.user, config).then(function(user){ $rootScope.user = user alert("You're all signed in, " + user.username); $state.go('home'); }, function(response){ alert(response.data.error) }); } })
Большая часть этого кода взята из документации Angular Devise , поэтому я не буду вдаваться в подробности. Теперь вам нужно знать, что Auth
— это сервис, созданный angular-device
, и он поставляется с некоторыми довольно удивительными функциями, такими как Auth.login(userParameters, config)
и Auth.register(userParameters, config)
. Они создают обещание, которое возвращает зарегистрированного пользователя после разрешения.
Я признаю, что немного обманул и назначил этого пользователя в $rootScope
. Однако более эффективный и более масштабируемый подход заключается в создании UserService, сохранении там пользователя и последующем внедрении UserService в любой из ваших контроллеров, которым нужен пользователь. Для краткости я также использовал простую функцию alert()
вместо интеграции ngMessages
или другого сервиса, такого как ngFlash
для объявления об ошибках или успешных событиях входа в систему.
Остальное должно быть довольно понятным. Формы ng-submit
присоединяются к этим функциям $scope
, $scope.user
информацию из ng-model
s на входах формы, а $state.go()
— изящная функция для перенаправления в другое состояние.
Если вы сейчас вернетесь к routes.js
, вся onEnter
логика onEnter
должна иметь намного больше смысла.
Объединяя все вместе
Я сохранил лучшее для последнего, поэтому давайте NavDirective.js
немного причудливых NavDirective.js
и nav.html
чтобы собрать все вместе:
angular .module('myApp') .directive('navBar', function NavBar(){ return { templateUrl: 'views/nav.html', controller: 'NavCtrl' } })
<div class="col-lg-8 col-lg-offset-2"> <ul class="nav navbar-nav" > <li><a ui-sref="home">Home</a></li> <li ng-hide="signedIn()"><a ui-sref="login">Login</a></li> <li ng-hide="signedIn()"><a ui-sref="register">Register</a></li> <li ng-show="signedIn()"><a ng-click="logout()">Log Out</a></li> </ul> </div>
И тем более надежный NavCtrl.js
:
angular .module('myApp') .controller('NavCtrl', function($scope, Auth, $rootScope){ $scope.signedIn = Auth.isAuthenticated; $scope.logout = Auth.logout; Auth.currentUser().then(function (user){ $rootScope.user = user }); $scope.$on('devise:new-registration', function (e, user){ $rootScope.user = user }); $scope.$on('devise:login', function (e, user){ $rootScope.user = user }); $scope.$on('devise:logout', function (e, user){ alert("You have been logged out.") $rootScope.user = undefined }); })
Все, что мы здесь делаем, — это настраиваем функции, используемые в ссылках навигации, такие как ng-hide="signedIn()"
и ng-click="logout()"
и добавляем слушатели в область $scope
чтобы мы могли запускать действия, когда определенные devise
конкретных событий происходят. Мы также вызываем Auth.currentuser()
чтобы при Auth.currentuser()
экземпляра этого контроллера мы могли дважды проверить наш объект $rootScope.user
и отобразить правильные навигационные ссылки.
Давайте снова найдем app/views/application/index.html
и добавим <nav-bar></nav-bar>
в строке выше <ui-view>
. Поскольку это не связано ни с одним из маршрутов, оно всегда будет отображаться выше нашего основного контента.
Иди и обнови свою страницу сейчас. Разве тебе не нравится, когда все работает? Надеюсь, у вас нет странных проблем с устаревшим пакетом, версией Ruby или чем-то в этом роде. Просто помните, Google — ваш лучший друг.
Anyhoo, я надеюсь, что это помогло! Пожалуйста, оставляйте любые вопросы, комментарии или предложения ниже!