Lavavel 不必过多介绍了, 作为全世界最流行的PHP框架,有着清晰的架构、完善的文档、丰富的工具等等,能够帮助开发者快速构建多页面web应用程序。
然而,随着技术的发展,web程序的另一面——客户端,正在变得越来越多元(PC,手机,平板,其他专用设备等)。所以需要一种统一的机制,方便服务器与不同的设备进行通信。Restful API 就是基于这个思想被提出来的。
阮一峰给出了对Restful架构的总结:
每一个URI代表一种资源;
客户端和服务器之间,传递这种资源的某种表现层;
客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"。
从行为上讲,就是服务器在约定好一套资源交互规则之后,依据该规则,通过统一的API接口与不同的前端设备进行交互。服务器只需要专注于数据的存储与分析,或曰业务逻辑的实现。在不同客户端上,其表现逻辑和交互逻辑与服务器端的业务逻辑实现了双重分离——逻辑分离与物理分离。
如果前后端只有资源(数据)的交互,那么页面路由自然当交给前端控制,相当于前端在首次加载页面后就不再进行全页面的刷新,所有的数据通过ajax从后端随取随用,所有的表单提交也是同样方法,这就是一个单页面应用(Single Page Application, SPA)。
Ember.js是一个模块化的前端框架,基于MVC理念,它提供了UI绑定、模板系统、路由系统等功能,非常适合SPA的快速开发。特别是Ember还提供了一个完整的命令行开发包——Ember Cli,不仅省去了繁琐的开发环境配置,还提供了丰富的开发与构建工具,例如,你立即就可以用CoffeeScript甚至ES6进行开发,相应的解释器已经随开发包安装妥当,在启动 ember server 的情况下,你也不必每次改动都在命令行键入 ember build,系统自动识别文本改动并进行解释与合并,将编译好的文件放在 dist/ 目录下。
然而,当我把 Laravel 和 Ember.js 分别配置妥当之后,发现我并不能马上撸起袖子写代码,因为他们并不是为彼此而生的。例如,此时我就面临着两个问题:
Laravel 与 Ember.js 有各自的路由系统,如何让 laravel 出让自己对 URL 的控制?
用户授权通常是由服务器管理和维护的,Laravel 提供完整的 authentication 方案,但在SPA 中后端不得不出让一部分权限控制给前端(主要是页面访问权限和为ajax授权),解决这个问题的最佳实践是怎样的?
我相信之前已经有很多人遇到类似问题,该问题可能有通用的原则指导,但在操作层面与具体的框架相关,限于篇幅,本文讨论第一个问题。第二的问题另作回答。
定义API接口
在laravel中,laravel/app/Http/routes.php 文件是所有 URL 的入口,所有的 URL 和相应的处理函数都应当在这里定义。
// laravel/app/Http/routes.php Route::group( array( 'prefix' => 'api/v1' ), function() { // USERS API ================================== Route::get('users/{id}', 'UserController@getById'); Route::delete('users/{id}', 'UserController@destroyById'); Route::put('users/{id}', 'UserController@updateById'); Route::post('users', 'UserController@storeNew'); // OTHER API ================================== // ...... });
我将所有API放入一个路由组,这个组约定API请求必须以 api /【版本号】作为前缀,例如需要服务器返回 id=5 的用户信息,应当向如下地址发出 GET 请求:
http://your_demain/api/v1/users/5
服务器收到该请求后将 request 对象传递给 UserController 的 getById 成员方法做处理。
我还在该文件中定义了用户授权的相关接口,并将它们分在一组:
// laravel/app/Http/routes.php Route::group( array ( 'prefix' => 'auth' ), function() { Route::post('login', 'Auth\AuthController@postLogin'); Route::get('logout', 'Auth\AuthController@getLogout'); Route::post('register', 'Auth\AuthController@postRegister'); });
三个接口分别提供登录、登出和注册功能。
好了,laravel 的路由只需要做这么多事情。
将其他URL的控制权交给前端
Ember页面启动时以 ember/dist/index.html 文件作为入口,dist 目录存放着所有构建好的文件,均为系统自动生成。在 index.html 文件中,从ember/dist/assets 目录加载了2个脚本文件和2个样式文件:
<!-- ember/dist/index.html --> <!DOCTYPE html> <html> <head> <!-- 其他head标签 --> <link rel="stylesheet" href="assets/vendor.css"> <link rel="stylesheet" href="assets/ember-app.css"> </head> <body> <script src="assets/vendor.js"></script> <script src="assets/ember-app.js"></script> </body> </html>
这4个文件包含了所有前端的逻辑和样式。而 laravel 以 laravel/public 作为项目根目录,该目录下保存了由 laravel 构建好的前端资源。所以我的处理方式如下:
同步 ember/dist/assets 与 laravel/public/assets 两个目录,后者是前者的镜像
在 laravel/resources/views 定义一个 view 命名为 app.php,它的内容是 ember/dist/index.html 的拷贝
laravel 拿到除 API 与 AUTH 之外的请求(之后统称非API请求),均返回 app.php
在正常使用时,前端只在首次加载时发出非API请求,一旦拿到 app.php 前端就获得了对应用表现层的控制,只要不刷新页面,之后用户与应用的所有交互都将由前端捕捉与控制。
具体操作如下:
由于我在 windows 下做开发,系统不提供直接同步两个本地目录的工具, 而且也没有找到实时自动同步的第三方桌面应用,最后选择了名为 InSync 的一款软件,每次同步都需要手动点击一下,是一个潜在的效率瓶颈。
在 laravel/resources/views 目录下创建 app.php 文件, 将 ember/dist/index.html 的内容拷贝过来。
在 laravel/app/Http/routes.php 中创建一个新的路由分组:
// laravel/app/Http/routes.php Route::get('{data?}', function() { return View::make('app'); })->where('data', '.*');
该分组捕捉所有非API请求并返回 app.php。
前端具体实现
在Ember中,每个路由都有与之相关联的一个模型(Model)。Model 负责数据的查询、更改和将更改保存回服务器,这一过程是通过模型适配器(Adapter)完成的。所以需要修改适配器让它匹配后端所定义的 API 前缀约定:
// ember/app/adapters/application.js export default DS.RESTAdapter.extend({ namespace: 'api/v1' });
然后就可以在 ember/app/routers.js 中定义前端路由了:
// ember/app/routers.js Router.map(function() { this.route('user', { path: '/user/:user_id' }); // Other routes ... });
这里有个不得不提的问题:
Ember 中每一个 Model 可以视为一种资源,而 Model 已经定义好了与这种资源的各种交互行为。例如当我定义好 userModel 之后,我要向服务器查询一条 user 记录可以使用如下代码,注释给出了它的网络请求(省略了前缀):
this.store.find('user', 5); // => GET '/users/5'
新建一个用户:
var user = this.store.createRecord('user', { email: '[email protected]', password: '123' }); user.save(); // => POST to '/users'
这一默认行为是不可配置的,所以后端提供的 API 必须配合该规则进行构建,这也是使用大型框架所带来的灵活性的的缺失。在需要大量定制化功能的应用中,轻量级的前端框架例如 backbone 更具有竞争力。
Ember意识到了这个问题,在最新的2.0版本中,可以通过自定义服务(Service)来解决。
总结
至此,确定了页面加载方案,打通了前后端的数据交互通道,前后端由各自为政变成了相互协作、各司其职,应用终于“活”了起来。