在我开始为自己的首个 Angular 单页面应用程序 (SPA) 编写代码时,我意识到用于设置和集成 Devise 的资源都比较零碎。我找到的堪称最实用的指南,也只是照常将 Angular 同 Rails 的运用过一遍了,其内容只有一小节。其它的一些资源不是太复杂就是太高深,而且步骤描述也不够详尽。对于一个新手而言,入门成了一大问题。
我在线了解的课程多是让我们积累高级组件,课程实验也已经准备好了基础框架,我们没法自己从零开始演练。而在我看来,学习的关键就是要自己动手从领开始构建几个应用。
所有工作都完成后,我的第一个 Angular 项目就正式启动并运行了。我为在 Rails 上构建 Angular 单页应用并集成 Devise 和 Bootstrap 制作了一份指导手册。 以下内容是我在相关主题上做的初步研究。
不可否认,Web 开发中的很大一部分功能都能直接解决复杂问题而不需要额外的解决方案。我希望这篇文章能给新手程序员带来帮助。
该指南只做入门介绍。适合对 Angular, Rails, Devise 和 Bootstrap 有基本了解的用户群体。本文不深入探究 Active Record,但会提及 Active Model Serializer,因其是发送模型到 JavaScript 前端的必要途径。为深化主题,我会安装 Bootstrap,并做相应的验证。
你可通过以下视频进行辅助学习:
视频: https://youtu.be/CtsC0iRxrAk
打开一个终端窗口,进入你想要创建项目的工作目录,就可以开始开发工作了。在这个演示示例里,我使用的是 Desktop (桌面)目录。
在终端窗口中,执行 `$ rails new YOUR-APP` 即可初始化 Rails 项目,初始化程序创建了一个目录,目录将会包含了整个框架,以及相关依赖包,目前这些依赖关系都被记录在 gems 配置文件 Gemfile 里面。(PS, 文中 $ 符合表示终端提示符)
接着,打开 Gemfile ,删除 `gem turbolinks`,然后添加以下内容:
gem 'bower-rails' gem 'devise' gem 'angular-rails-templates' #=> allows us to place our html views in the assets/javascript directory gem 'active-model-serializer' gem 'bootstrap-sass', '~> 3.3.6' #=> bootstrap also requires the 'sass-rails' gem, which should already be included in your gemfile
尽管 Bower (一个前端包管理工具) 非本项目必须,但是我还是选择使用 Bower ,原因很简单:增加经验,为之后可能用到 Bower 的情况做准备。
Bower 是和 Ruby gems 或者 Node npm 类似的包管理工具,更多信息请参阅: https://bower.io 。你可以通过 npm (npm install -g bower) 将其安装,但是在这份指南中,我使用 bower-rails 来安装 bower。
现在我们准备进行这些 gem 资源库的安装与初始化,然后创建数据库,并添加一个迁移文件以便用户可以使用一个用户名进行注册操作,然后用如下命令将这些迁移应用到我们方案中去:
$ 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”gem 库中生成用户串号器:
$ rake bower:install $ rails g serializer user
找到 app/serializers/user_serializer.rb 文件,然后在当 Devise 从 Rails 请求用户信息的时候直接在属性 :idso 后面添加 :username。这样要比提示说 “Welcome, jesse@email.com” 要好看许多,不过如果提示成了 “Welcome, 5UPer$3CREtP4SSword”,那也蛮糟糕的。这只是开个玩笑,不过说真的,不要这样做。
在 config/application.rb 文件中直接在 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 获取一次调用时,不要担心有关于认证令牌的事情;第四,用户将会有一个用户名称(username)。
接下来要打开 app/controllers/users_controller.rb 文件,然后确定你可以使用任何 /users/:id.json 这样 JSON 形式的请求访问用户:
class UsersController < ApplicationController def show user = User.find(params[:id]) render json: user end end
不要担心 routes.rb 文件中的 :show 的设置问题。Devise 已为我们处理好!
Rails 会默认使用 views/layouts/application.html.erb 进行初始化,但这不是我们所需要的,所以我们需要做进一步操作:
将这个文件挪到 app/views/application/ 目录下。
将它重新命名为 index.html.erb。
使用<ui-view></ui-view>替换<%= yield %> (除了头部的 scritp/style 标记,我们不会对其它任何的 erb 进行渲染)。
移除脚本和样式表的 erb 标记中所有的 “turoblinks” 提示信息。
向 <body>标签添加一个 ng-app="myApp" 的属性。当我们启动服务器的时候,Angular 将会进行加载操作,并会为此在初始化应用之前加紧对 DOM 的查找。
配置好后端的最后一个步骤就是放置好资源管道。Bower 已经在 vendor/assets/bower_components 中为我们安装好系列东西。同时我们也早将较好的 gem 资源库安装好。现在,我们要确保应用可以找到下列脚本和样式表:
在 app/assets/javascript/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 的 turbolink。
最后,我们必须将 app/assets/stylesheets/application.css 重新命名为 application.scss,然后在样式的最后加上这两行 @import 代码:
* *= require_tree . *= require_self */ @import "bootstrap-sprockets"; @import "bootstrap";
Duang!! 现在,我们已经把所有设置都搞定,可以着手弄前端了。
这里可以预览一下 Angular 应用程序树的样子。因为我们已经安装了 ‘angular-templates’ 的 gem 库,所以可以将所有的 HTML 文件保持在 assets/javascript 目录中,里面有我们所有其它的 Angular 文件:
/javascript/controllers/AuthCtrl.js /javascript/controllers/HomeCtrl.js /javascript/controllers/NavCtrl.js /javascript/directives/NavDirective.js /javascript/views/home.html /javascript/views/login.html /javascript/views/register.html /javascript/views/nav.html /javascript/app.js /javascript/routes.js
第一件要事就是 : 我们要在 app.js 中对应用程序进行声明,并注入必要的依赖:
(function(){ angular .module('myApp', ['ui.router', 'Devise', 'templates']) }())
这里我用到了一个 IIFE,理由引用了下面这段话:
要将 AngularJS 组件封装到一个立即执行函数表达式(IIFE)里面。这样做可以帮助你防止变量和函数声明在全局范围内存活时间过长而超出你的预期, 也可以帮助你避免变量冲突。这个为你想要把自己写的代码最小化,然后打包到一个单独的文件部署到生产服务器上提供了方便,以为每一个文件提供一个变量作用域。 — Codestyle.co 上的 AngularJS 指南
接下来我们将对 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 和 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" })
你可以尝试注释掉 login/register 状态,然后运行 $ rails 来看看是否一切正常。如果正常的话,就可以看到一个好看的 “Hello World”大字。如果它就在界面的中上部,那就屏息静待,因为 Bootstrap 正在介入,然后 col-lg 会将它放到一个漂亮的位置,而不是死死的杵在左上角。
Angular 所做的就是搜寻 DOM,找到属性 ng-app,初始化 “myApp”, 从路由程序默认导航到 /hom, 定位到 <ui-view> 指令,实例化 HomeCtrl,注入 $scope 对象,加入一个 hello 的键,给它分配一个值 "Hello World",然后用 <ui-view> 元素中的信息渲染出 home.html。一旦到了视图中,Angular 就可以扫描到任何有意义的诸如 {{...}} 绑定以及 ng-if 指令这样的命令,然后按照需要渲染出控制器的信息。这些是接下来的操作的要点,但操作顺序会有不同。
因为我们已经有了所有这些在幕后的细节信息,让我们再附加上 AuthCtrl.js 和 login.html/register.html 文件:
# login.js <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.js <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,我只想指出,因为你看到的 大多数 是 Bootstrap 的 CSS 类, 所以会对其渲染留下不错的印象。如果忽略所有类的属性,其他内容都很类似,例如:asng-submit,ng-model,andui-sref,通常它们都有 href 锚的 tag 属性。现在,为了 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 创建的,并且有一些非常棒的功能,例如:sAuth.login(userParameters, config) 和 Auth.register(userParameters, config),它们创建了一个 promise,并由登陆用户一次性处理。