使用RailsAPI和Angular来构建一个酷炫的Web应用程序【环境设置版】
前几天在公司内举办了一个非常快速掌握Rails的研修营。由于之前只稍微接触过Rails,所以我勇敢地尝试了一些我之前未曾见过的API模式,并记录下来了备忘。
环境
Ruby:2.2.4
Rails:5.0.0.1
Angular:1.5.8(因为Angular2一直是rc版本)
Webpack:1.13.2
AngularMaterial:1.1.1
我也放了源代码,供参考。
我们将以Rails的环境已经准备完毕为前提,继续推进对话。
1. Rails应用程序接口 (RailsAPI)
【参考】使用Rails建立的仅用于API的应用程序
因为所有重要的事情都已经在RailsGuides(用日语)详细介绍了,所以在这里我只会简要概述。
为了创建一个专用于API的应用程序,只需在常规的rails new命令后面加上–api选项。
如果要使用数据库,为了避免后续调整设置麻烦,我们也应该加上-d选项。
$rails new rails-api-angular --api
出现的目录结构是这样的
与常规的Rails应用程序相比
-
- app/helpers
- app/frontend
减少了
- app/jobs
数量在增加。
应用控制器(application_controller.rb)从::Base变为::API。
class ApplicationController < ActionController::API
end
Gemfile看起来整洁
source 'https://rubygems.org'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 5.0.0', '>= 5.0.0.1'
# Use mysql as the database for Active Record
gem 'mysql2', '>= 0.3.18', '< 0.5'
# Use Puma as the app server
gem 'puma', '~> 3.0'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
# gem 'jbuilder', '~> 2.5'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 3.0'
# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'
# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development
# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
# gem 'rack-cors'
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platform: :mri
end
group :development do
gem 'listen', '~> 3.0.5'
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
gem 'spring'
gem 'spring-watcher-listen', '~> 2.0.0'
end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
2. Angular (角度)
这次我们在创建的应用程序下面还创建了Angular的环境,并采用了将其打包到公共目录下使用webpack的方式。
使用WebPack将JavaScript轻松分离并与Rails良好地结合
在Rails 4.2中放弃Sprockets,转而使用webpack进行迁移
安装Angular和webpack
首先准备 package.json(不用担心它可能包含了一些不必要的东西,因为它是从一个现有的项目中带来的)。
{
"name": "rails-api-angular",
"version": "1.0.0",
"description": "rails-api-angular",
"scripts": {
"dev": "webpack-dev-server --hot --inline --port 3500 --progress --profile --colors",
"build": "NODE_ENV=production webpack -p --progress --profile --colors "
},
"dependencies": {
"angular": "1.5.8",
"angular-animate": "1.5.8",
"angular-aria": "1.5.8",
"angular-cookies": "1.5.8",
"angular-material": "1.1.1",
"angular-material-icons": "0.7.1",
"angular-messages": "1.5.8",
"angular-resource": "1.5.8",
"angular-route": "1.5.8",
"angular-sanitize": "1.5.8",
"clean-webpack-plugin": "^0.1.9",
"css-loader": "^0.23.1",
"es6-promise": "~3.2.1",
"eslint-plugin-jsx-a11y": "^1.5.5",
"eslint-plugin-react": "^5.2.2",
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.8.5",
"font-awesome": "^4.6.3",
"font-awesome-sass-loader": "^1.0.1",
"imports-loader": "^0.6.5",
"jquery": "^2.2.4",
"json-loader": "^0.5.4",
"node-sass": "^3.7.0",
"sass-loader": "^3.2.0",
"style-loader": "^0.13.1",
"ts-loader": "^0.8.1",
"url-loader": "^0.5.7",
"webpack": "^1.13.2",
"webpack-manifest-plugin": "^1.0.1"
},
"devDependencies": {
"es5-shim": "^4.5.7",
"eslint": "^2.9.0",
"eslint-config-airbnb": "^9.0.1",
"eslint-import-resolver-webpack": "^0.2.4",
"eslint-plugin-import": "^1.7.0",
"typescript": "^1.8.10",
"webpack-dev-middleware": "^1.6.1",
"webpack-dev-server": "^1.14.1",
"webpack-hot-middleware": "^2.10.0"
}
}
安装
$npm install
2-2. 创建Angular项目
我們將建立一個必要最低限的 Angular 專案。
$mkdir app/angular
$cd app/angular/
$mkdir javascripts
$touch javascripts/application.ts
$mkdir stylesheets
$touch stylesheets/application.sass
我会准备Typescript的类型定义。
这次我们会继续使用我们经常使用的dtsm,但是无论使用哪个类型定义管理工具,大体上都差不多,所以你可以使用你喜欢的工具。
$dtsm init
我要修改已经创建好的dtsm.json文件。
{
"repos": [
{
"url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git",
"ref": "master"
}
],
"path": "typings",
"bundle": "typings/bundle.d.ts",
"link": {
"npm": {
"include": true
}
},
"dependencies": {
"node/node.d.ts": {
"ref": "17795ae18fc214be862fe578ad48c28fecfef8a6"
},
"angularjs/angular.d.ts": {
"ref": "17795ae18fc214be862fe578ad48c28fecfef8a6"
},
"angular-material/angular-material.d.ts": {
"ref": "17795ae18fc214be862fe578ad48c28fecfef8a6"
},
"angular-cookie/angular-cookie.d.ts": {
"ref": "17795ae18fc214be862fe578ad48c28fecfef8a6"
},
"angularjs/angular-animate.d.ts": {
"ref": "0c5c7a2d2bd0ce7dcab963a8402a9042749ca2da"
}
}
}
安装
$dtsm install
在javascripts/application.ts中调用angular和其他一些内容,并使用stylesheets/application.sass。
/// <reference path="./typings/bundle.d.ts"/>
// angular
import angular = require('angular')
require('angular-material')
require('angular-cookies')
require('angular-resource')
require('angular-sanitize')
require('angular-route')
require('angular-animate')
require('angular-material-icons')
require('es5-shim')
let app = angular.module('App', [
'ngMaterial',
'ngCookies',
'ngResource',
'ngSanitize',
'ngRoute',
'ngMdIcons',
'ngAnimate'
]);
//stylesheets
require('../stylesheets/application')
//angular-materialのTheme
app.config(($mdThemingProvider) => {
$mdThemingProvider.theme('default')
.primaryPalette('grey', {
'default': '100'
})
.accentPalette('pink', {
'default': '700'
});
});
//Controllerも一緒に定義しちゃいました
export default class AppCtrl {
private title: string = 'Hello RailsAPI × Angular!'
static $inject = ['$rootScope', '$scope', '$cookies', '$window', '$timeout', '$location']
constructor(
private rootScope: ng.IRootScopeService,
private scope: ng.IScope,
private cookies: any,
private window: ng.IWindowService,
private timeout: ng.ITimeoutService,
private location: ng.ILocationService
) {
}
}
app.controller('AppCtrl', AppCtrl)
在stylesheets/application.sass中导入angular-material。
@import '~angular-material/angular-material.css'
body
background-color: whitesmoke
.header
md-toolbar
color: #D81B60
button
height: 100%
margin: 0px
// angularの評価式が一瞬見えちゃう問題対応
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak
display: none !important
2-3. webpack配置
在项目的根目录下创建webpack.config.js文件。
const DEBUG = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === undefined;
const webpack = require('webpack');
const path = require('path');
/**
* Require webpack plugins
*/
const ManifestPlugin = require('webpack-manifest-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
/**
* Environment settings
*/
const devtool = DEBUG ? '#inline-source-map' : '#eval';
const fileName = DEBUG ? '[name]' : '[name]-[hash]';
const publicPath = DEBUG ? 'http://localhost:3500/assets/' : '/assets/';
/**
* Entries
*/
const entries = {
application: ['./app/angular/javascripts/application.ts']
}
/**
* Add plugins
*/
const plugins = [
new ExtractTextPlugin(fileName + '.css')
]
if (DEBUG) {
plugins.push(new webpack.NoErrorsPlugin());
} else {
plugins.push(new ManifestPlugin({fileName: 'webpack-manifest.json'}));
plugins.push(new webpack.optimize.UglifyJsPlugin({compress: {warnings: false}}));
plugins.push(new CleanWebpackPlugin(['assets'], {
root: __dirname + '/public',
verbose: true,
dry: false
}));
}
module.exports = {
entry: entries,
output: {
path: __dirname + '/public/assets',
filename: fileName + '.js',
publicPath: publicPath
},
devtool: devtool,
plugins: plugins,
module: {
loaders: [
{
test: /\.ts$/,
loader: 'ts',
exclude: [/node_modules/]
},
{
test: /\.css$/,
loader: ExtractTextPlugin.extract('style-loader', 'css-loader?minimize')
},
{
test: /\.scss$/,
loader: ExtractTextPlugin.extract('style-loader', 'css-loader!sass-loader?minimize')
},
{
test: /\.sass$/,
loader: ExtractTextPlugin.extract('style-loader', 'css-loader!sass-loader?minimize')
},
{
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
loader: 'url-loader?mimetype=image/svg+xml'
},
{
test: /\.woff(\d+)?(\?v=\d+\.\d+\.\d+)?$/,
loader: 'url-loader?mimetype=application/font-woff'
},
{
test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
loader: 'url-loader?mimetype=application/font-woff'
},
{
test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
loader: 'url-loader?mimetype=application/font-woff'
},
{
test: /\.(jpg|png|gif)$/,
loader: DEBUG ? 'file-loader?name=[name].[ext]' : 'file-loader?name=[name]-[hash].[ext]'
}
]
},
resolve: {
root: path.resolve(__dirname, 'app', 'angular'),
extensions: ['', '.js', '.ts', '.css', '.scss', '.sass'],
},
devServer: {
headers: {
"Access-Control-Allow-Origin": "http://localhost:3000",
"Access-Control-Allow-Credentials": "true"
}
}
}
用这个方法,在开发过程中可以启动服务器并进行监控,非常方便。
尝试运行之前在之前的package.json文件中偷偷定义的命令。
$npm run dev
然后就出现了这个错误。
ERROR in ./app/angular/javascripts/application.ts
Module build failed: TypeError: Path must be a string. Received undefined
at assertPath (path.js:7:11)
at Object.dirname (path.js:1324:5)
at ensureTypeScriptInstance (/Users/nozakishohei/WorkSpace/repository/sample/rails-api-angular/node_modules/ts-loader/index.js:156:103)
at Object.loader (/Users/nozakishohei/WorkSpace/repository/sample/rails-api-angular/node_modules/ts-loader/index.js:403:14)
@ multi application
原来如此。ts-loader要求必须有tsconfig.json文件!
所以我会为你做这个
$touch app/angular/javascripts/tsconfig.json
{
"compilerOptions": {
"target": "es5",
"sourceMap": true
},
"exclude": [
"node_modules"
]
}
只需再次运行npm run dev,就可以成功。然后只需要从屏幕上调用即可。
创建图像
我一直在标题中使用“RailsAPI”这个词,但现在我要打破规定(设置根目录,并创建一个页面)
只设置了3-1根目录(只有一个界面,请原谅)。
在routes.rb文件中设置根路由
我已经找不到别的想法,所以将其命名为root#index,但你可以给它取一个你喜欢的名字。
Rails.application.routes.draw do
root 'root#index'
end
制造控制器
class RootController < ActionController::Base
def index
end
end
制作一个看起来像那样的顶部界面
<html ng-app="App">
<head>
<meta charset="UTF-8">
<meta name="viewport", content="width=device-width, initial-scale=1, maximum-scale=1">
<title>rails-api-angular</title>
</head>
<body ng-controller="AppCtrl as appCtrl" ng-cloak>
<header class="header md-whiteframe-3dp">
<md-toolbar md-scroll-shrink>
<div class="md-toolbar-tools">
<h3>
<span>rails-api-angular</span>
</h3>
<span flex></span>
<md-button class="md-accent">Sign up</md-button>
<md-button class="md-accent">Log in</md-button>
</div>
</md-toolbar>
</header>
<div layout="row" layout-align="center center">
<h1>{{appCtrl.title}}</h1>
</div>
<%= stylesheet_link_tag webpack_asset_path('application.css') %>
<%= javascript_include_tag webpack_asset_path('application.js')%>
</body>
</html>
添加一个辅助函数 webpack_asset_path,用于区分读取 webpack 资源。
module RootHelper
def webpack_asset_path(path)
if Rails.env.development?
return "http://localhost:3500/assets/#{path}"
end
host = Rails.application.config.action_controller.asset_host
manifest = Rails.application.config.assets.webpack_manifest
path = manifest[path] if manifest && manifest[path].present?
"#{host}/assets/#{path}"
end
end
我终于启动了Rails,并尝试访问localhost:3000。
$rails s
来了!