是时候使用Vue2和Yii2进行前后端分离开发啦

后台

前后端分离之后,后台语言的选择一下子多了,只要能处理请求,返回JSON就可以了。不过据说PHP是世界上最好的语言,所以本教程选的是PHP,当然,站在巨人的肩膀上,选个好用不浮夸的框架也是必要的,不然重复发明*会占用很多玩耍的时间,不利于身心健康。性能什么的,不要计较太多,如果幸福的烦恼来临,多花点钱,多搞几台机器的事而已。废话不多说,撸起袖子加油干!

1.安装Yii2

Yii2通过composer-asset-plugin整合bower和npm上的前端库到composer的安装上,如果之前安装过这个插件,就不用安装啦,直接


  1. composer create-project --prefer-dist yiisoft/yii2-app-basic basic 

如果没有安装过asset-plugin,就先安装插件,再创建一个新的Yii2程序:


  1. composer global require "fxp/composer-asset-plugin:^1.2.0"  
  2. composer create-project --prefer-dist yiisoft/yii2-app-basic basic 

稍等片刻,就能安装好啦。

配置下虚拟主机,以apache为例,打开apache目录下的httpd-vhosts.conf配置文件,添加配置:

是时候使用Vue2和Yii2进行前后端分离开发啦

DocumentRoot指向的是刚刚创建的Yii2程序的web目录,ServerName是本地域名,配置好apache配置后,需要再改一下hosts文件,以Win7为例,打开 C:\Windows\System32\drivers\etc\hosts 在文件最后添加一行


  1. 127.0.0.1 basic.backend.local 

保存,重启Apache,打开浏览器,打开地址 http://basic.backend.local/ 看到熟悉的 Congratulations!大字,说明安装配置完成啦!

2.配置Yii2支持API调用

2.1 添加API相关Controller的基础类,这里取名BaseAPIController:


  1. namespace app\controllers;  
  2. use yii\filters\ContentNegotiator; 
  3. use yii\rest\Controller; 
  4. use yii\web\Response; 
  5. use Yii;  
  6. class BaseAPIController extends Controller 
  7.     public function behaviors() 
  8.     { 
  9.         $behaviors = parent::behaviors(); 
  10.         unset($behaviors['authenticator']); 
  11.  
  12.         $behaviors['corsFilter'] = [ 
  13.             'class' => \yii\filters\Cors::className(), 
  14.             'cors' => [ 
  15.                 // restrict access to 
  16.                 'Access-Control-Request-Method' => ['*'], 
  17.                 // Allow only POST and PUT methods 
  18.                 'Access-Control-Request-Headers' => ['*'], 
  19.                 // Allow only headers 'X-Wsse' 
  20.                 'Access-Control-Allow-Credentials' => true
  21.                 // Allow OPTIONS caching 
  22.                 'Access-Control-Max-Age' => 3600, 
  23.                 // Allow the X-Pagination-Current-Page header to be exposed to the browser. 
  24.                 'Access-Control-Expose-Headers' => ['X-Pagination-Current-Page'], 
  25.             ], 
  26.         ]; 
  27.  
  28.         $behaviors['contentNegotiator'] = [ 
  29.             'class' => ContentNegotiator::className(), 
  30.             'formats' => [ 
  31.                 'application/json' => Response::FORMAT_JSON 
  32.             ] 
  33.         ]; 
  34.         return $behaviors; 
  35.     } 

主要解决两个问题:

  1. cors问题,就是跨域调用的问题,这个问题可大可小,展开了还能再写很多,这里不细说了。
  2. 返回的数据类型,本例所以请求都返回JSON格式数据。

2.1.2 修改接受的请求数据格式

传统的web应用,请求类型通常是x-www-form-urlencoded与multipart/form-data, 对应普通的表单提交,以及文件内容提交,默认Yii2不支持Json格式的请求,需要在config/web.php里修改request组件配置 :


  1. 'request' => [ 
  2.             'cookieValidationKey' => 'Dk7BQ6_hk9YRPMdkaaK6FFwIpa123456'
  3.             'parsers' => [ 
  4.                 'application/json' => 'yii\web\JsonParser'
  5.                 'text/json' => 'yii\web\JsonParser'
  6.             ], 
  7.         ], 

2.2 添加权限验证控制器


  1. namespace app\controllers; 
  2.  
  3. class AuthController extends BaseAPIController 
  4.     public function actionIndex() 
  5.     { 
  6.         $username = \Yii::$app->request->post('name'); 
  7.         $password = \Yii::$app->request->post('password'); 
  8.         if($username == "admin" && $password == "admin"
  9.         { 
  10.             return ['success'=>1,'msg'=>'100-token']; 
  11.         } 
  12.         return ['success'=>0,'msg'=>\Yii::t('erp','Username or password error')]; 
  13.     } 

简单起见,不涉及数据库操作,用户名密码如果都符合要求,就返回一个token,这个token的值是100-token,为什么是这个值,请看创建好的Yii2项目的Models目录下的User.php.

2.3 添加需要授权才能访问的控制器


  1. namespace app\controllers; 
  2. use Yii; 
  3. use yii\filters\auth\HttpBearerAuth; 
  4.  
  5. class ItemController extends BaseAPIController 
  6.     public function behaviors() 
  7.     { 
  8.         $behaviors = parent::behaviors(); 
  9.         if (Yii::$app->getRequest()->getMethod() !== 'OPTIONS') { 
  10.             $behaviors['authenticator'] = [ 
  11.                 'class' => HttpBearerAuth::className(), 
  12.             ]; 
  13.         } 
  14.         return $behaviors; 
  15.     } 
  16.     public function actionIndex() 
  17.     { 
  18.         return ['success'=>1,'msg'=>'hello']; 
  19.     } 

重点是,这个ItemController只有通过验证,才能访问到Index这个action,这里增加的验证器HttpBearerAuth就是用来验证请求的Header里是否带着刚刚分发的token, 之所以不对OPTIONS请求做验证,是因为OPTIONS请求不带别的信息啊,没法验证。浏览器非要在请求之前发个OPTIONS请求,也没办法。

2.4 测试

打开浏览器访问http://basic.backend.local/item 正常情况下,会返回为授权信息 :


  1. {"name":"Unauthorized","message":"Your request was made with invalid credentials.","code":0,"status":401,"type":"yii\\web\\UnauthorizedHttpException"

前台

前端采用Vue2这个单独好用的js框架,文档丰富,社区活跃,最难得的是作者很帅!

1.安装Node和vue-cli

现在前端没有Node简直寸步难行,所以Node是必须安装的,去官网下个安装包,安装好。然后再安装vue-cli工具。

2.创建Vue单页面程序

运行命令:


  1. vue init webpack-simple basic_front 

会让您输入必要的信息,如项目名称,描述,作者之类的。这里简单起见,一路默认回车就是啦。


  1. D:\devs\web>vue init webpack-simple basic_front 
  2.  
  3. > Project name basic 
  4. > Project description A Vue.js project 
  5. > Author elvis_lim <pclinwu@163.com> 
  6. > Use sass? No 
  7.  
  8.    vue-cli · Generated "basic_front"
  9.  
  10.    To get started: 
  11.  
  12.      cd basic_front 
  13.      npm install 
  14.      npm run dev. 

运行程序

如上面命令行输出的提示,vue-cli创建项目后,还需要npm install安装,然后npm run dev运行,看看能不能成功运行。

4.安装必要库

本例需要用到Vue-router这个官方推荐路由,以及axios这个官方推荐http库。至于官方为什么不推荐原来的vue-resource了,我也不知道,前端喜新厌旧,就是这样的。


  1. npm install axios bootstrap iview vue-router --save 

细心的朋友会发现多安装了bootstrap 和 iview这两个库,没别的目的,就是想让界面好看点,听说也比较火,安装了吧,反正闲着也是闲着。

稍等片刻,安装完毕,npm run dev看看效果吧。

5.开始添加前端逻辑

逻辑是这样的,这个前端页面,是登录之后才能用的,未登录用户,直接给跳转到登录页。登录成功后,跳回首页,然后调用Item/index接口,获取信息然后显示在页面上。

好了,计划完毕,开始写代码。在src目录下创建一个config目录,加一个setting.js, 内容如下:


  1. export default
  2.   remoteHost:'http://basic.backend.local'
  3.   userToken:'tk' 

基本的配置信息就在这里了,包含了后端服务地址,用户标志的key名称。

再在src目录下创建一个services目录,加一个auth.js,用来辅助判断用户登录状态:


  1. import setting from '../config/setting.js'  
  2. export default { 
  3.     login(data){ 
  4.         localStorage.setItem(setting.userToken,data)         
  5.     },     
  6.     // authentication status 
  7.     authenticated(){ 
  8.         var t = localStorage.getItem(setting.userToken); 
  9.         return t && t.length > 0; 
  10.     }, 
  11.     getToken(){ 
  12.         return localStorage.getItem(setting.userToken); 
  13.     }, 
  14.     logout(){ 
  15.         localStorage.setItem(setting.userToken,""); 
  16.     } 

就四个函数,功能不言而喻。

再在services目录下加一个http.js,用来包装一下axios:


  1. import axios from 'axios' 
  2. import setting from '../config/setting.js' 
  3. import router from '../main.js' 
  4. import Auth from './auth.js' 
  5.  
  6. // axios 配置 
  7. axios.defaults.timeout = 5000; 
  8. axios.defaults.baseURL = setting.remoteHost; 
  9.  
  10. // http request 拦截器 
  11. axios.interceptors.request.use( 
  12.     config => { 
  13.         if (Auth.authenticated()) { 
  14.           var token = Auth.getToken(); 
  15.           config.headers.common["Authorization"] = `Bearer ${token}`; 
  16.         } 
  17.         return config; 
  18.     }, 
  19.     err => { 
  20.         return Promise.reject(err); 
  21.     } 
  22. ); 
  23. axios.interceptors.response.use( 
  24.     response => { 
  25.         return response; 
  26.     }, 
  27.     error => { 
  28.         if (error.response) { 
  29.             switch (error.response.status) { 
  30.                 case 401: 
  31.                     // 401 清除token信息并跳转到登录页面 
  32.                     Auth.logout() 
  33.                     router.replace({ 
  34.                         path: 'login'
  35.                         query: {redirect: router.currentRoute.fullPath} 
  36.                     }) 
  37.             } 
  38.         } 
  39.         console.log(error);//console : Error: Request failed with status code 402 
  40.         return Promise.reject(error) 
  41. }); 
  42. export default axios; 

重点是在request和response两个拦截器。request里,查看用户是处于登录状态,如果是,那么就在请求的headers里加上 Bearer ${token}; 这样后端在接受到请求的时候,用HttpBearerAuth去校验这个token是不是符合要求,就能够进行用户权限验证啦。当然,不用拦截器也是可以的,但是每个请求都需要加Headers头,会不会很累?

response拦截器里,检查返回的状态,如果是401状态码,就是用户没有通过权限验证,那就去登录页面再登录啦。同样的,每个请求也是可以检查状态码的,但是太麻烦,偷懒是美德!

最后,在src目录下的main.js里把这些工具函数连接起来:


  1. import Vue from 'vue' 
  2. import App from './App.vue' 
  3. import VueRouter from 'vue-router' 
  4.  
  5. import http from './services/http.js'  
  6. import iView from 'iview'
  7. import 'iview/dist/styles/iview.css'
  8. import 'bootstrap/dist/css/bootstrap.css' 
  9.  
  10. import Login from './components/Login.vue' 
  11. import Home from './components/Home.vue'  
  12. Vue.prototype.$http = http  
  13. Vue.use(VueRouter) 
  14. Vue.use(iView)  
  15. const router = new VueRouter({ 
  16.   mode: 'history'
  17.   base: __dirname, 
  18.   routes: [ 
  19.     { 
  20.       path: '/'
  21.       component: Home, 
  22.       meta: { 
  23.         requireAuth: true 
  24.       } 
  25.     }, 
  26.     { 
  27.       path: '/login'
  28.       component: Login 
  29.     } 
  30.   ] 
  31. })  
  32. import Auth from './services/auth.js'
  33.  
  34. router.beforeEach((tofromnext) => { 
  35.     if(to.meta.requireAuth && !Auth.authenticated()) 
  36.     { 
  37.       next({ 
  38.           path: '/login'
  39.           query: { redirect: to.fullPath } 
  40.         }) 
  41.     } 
  42.     else { 
  43.       next() 
  44.     } 
  45. }) 
  46.  
  47. new Vue({ 
  48.   el: '#app'
  49.   router: router, 
  50.   render: h => h(App) 
  51. }); 
  52.  
  53. export default router 

写完这一大串后,发现命令行里报了一堆错,看了看,应该是引入了bootstrap,iview的css,而默认的webpack配置文件没有相应的loader, 于是先安装下这些loader:


  1. npm install css-loader style-loader --save-dev 

然后,修改webpack.config.js文件,给file-loader加上woff|woff2|ttf|eot,因为bootstrap用到了这些字体文件,然后再加上style-loader!css-loader, 修改如下:


  1.         test: /\.(png|jpg|gif|svg|woff|woff2|ttf|eot)$/, 
  2.         loader: 'file-loader'
  3.         options: { 
  4.           name'[name].[ext]?[hash]' 
  5.         } 
  6.       }, 
  7.       {  
  8.         test: /\.css$/,  
  9.         loader: 'style-loader!css-loader' 
  10.       } 

再次npm run dev后,熟悉的首页又回来啦。

6.添加路由根节点

打开App.vue,修改后的App.vue如下:


  1. <template> 
  2.   <div id="app"
  3.   <router-view 
  4.       class="view" 
  5.       keep-alive 
  6.       transition 
  7.       transition-mode="out-in"
  8.     </router-view
  9.   </div> 
  10. </template> 
  11.  
  12. <script> 
  13. export default { 
  14. </script> 
  15. </script> 

其实就是用App这个组件作为总的树根,然后Home和Login两个组件作为路由子组件加载进来。

7.添加components目录,再添加Login.vue和Home.vue


  1. <template> 
  2.   <div> 
  3.     <div class="row"
  4.       <div class="col-md-12 col-sm-12 col-xs-12 text-center" style="padding-top:10%"
  5.         <p class="login-title">Login</p> 
  6.       </div> 
  7.     </div> 
  8.     <Form ref="formInline" :model="formInline" :rules="ruleInline" inline> 
  9.       <div class="row"
  10.         <div class="col-md-2 col-sm-2 col-xs-2 text-center col-md-offset-5 col-sm-offset-5 col-xs-offset-5"
  11.             <Form-item prop="user"
  12.                 <Input type="text" v-model="formInline.user" placeholder="用户名"
  13.                     <i class="ivu-icon ivu-icon-ios-people-outline" slot="prepend"></i> 
  14.                 </Input> 
  15.             </Form-item> 
  16.         </div> 
  17.       </div> 
  18.       <div class="row"
  19.         <div class="col-md-2 col-sm-2 col-xs-2 text-center col-md-offset-5 col-sm-offset-5 col-xs-offset-5"
  20.             <Form-item prop="password"
  21.                 <Input type="password" v-model="formInline.password" placeholder="密码"
  22.                     <Icon type="ios-locked-outline" slot="prepend"></Icon> 
  23.                 </Input> 
  24.             </Form-item> 
  25.         </div> 
  26.       </div> 
  27.       <div class="row"
  28.         <div class="col-md-12 col-sm-12 col-xs-12 text-center"
  29.                 <Button type="primary" @click="handleSubmit('formInline')">登录</Button> 
  30.             </Form-item> 
  31.         </div> 
  32.       </div> 
  33.     </Form> 
  34.   </div> 
  35. </template> 
  36.  
  37. <style> 
  38. .login-title{ 
  39.   font-family: '黑体 Bold''黑体'
  40.   font-weight: 700; 
  41.   font-style: normal; 
  42.   font-size: 13px; 
  43.   color: #7B7B7B; 
  44. </style> 
  45. <script> 
  46. import Auth from '../services/auth.js' 
  47.  
  48. export default { 
  49.   data () { 
  50.     return { 
  51.         formInline: { 
  52.             user''
  53.             password'' 
  54.         }, 
  55.         ruleInline: { 
  56.             user: [ 
  57.                 { required: true, message: '请填写用户名'trigger'blur' } 
  58.             ], 
  59.             password: [ 
  60.                 { required: true, message: '请填写密码'trigger'blur' } 
  61.             ] 
  62.         } 
  63.     }   
  64.   },  
  65.   methods: { 
  66.     handleSubmit(name) { 
  67.       let obj = { 
  68.         name: this.formInline.user
  69.         password: this.formInline.password 
  70.       } 
  71.       if(this.formInline.user.length == 0 || this.formInline.password.length == 0){ 
  72.         this.$Message.error("用户名或密码不能为空"
  73.         return
  74.       } 
  75.       this.$http.post('/auth/index', obj) 
  76.         .then((res) => { 
  77.           console.log(res); 
  78.           if(res.data.success){ 
  79.             Auth.login(res.data.msg); 
  80.             this.$router.push({path:'/'}) 
  81.           }else
  82.             this.$Message.error(res.data.msg); // 登录失败,显示提示语 
  83.           } 
  84.         }, (err) => { 
  85.             this.$Message.error('请求错误!'
  86.         }) 
  87.     } 
  88.   } 
  89. </script> 

Home.vue


  1. <template> 
  2.     <div class="content"
  3.     <h1>Home Page</h1> 
  4.            {{msg}}    
  5.              <p v-if="msg.length > 0" @click="logout">logout</p> 
  6.  
  7.     </div> 
  8. </template> 
  9.  
  10. <script> 
  11.  
  12. import Auth from '../services/auth.js' 
  13.  
  14. export default { 
  15.   data(){ 
  16.     return { 
  17.       msg:"" 
  18.     } 
  19.   }, 
  20.   created(){ 
  21.  
  22.     this.$http.get('/item').then((res)=>{ 
  23.       this.msg = res.data.msg; 
  24.     }) 
  25.  
  26.   }, 
  27.   methods:{ 
  28.     logout(){ 
  29.       Auth.logout() 
  30.       this.$router.push({path:'/login'}) 
  31.     } 
  32.   } 
  33. </script> 

添加完毕后,npm run dev 就可以测试了。按照之前设计的逻辑,首先请求,http://localhost:8080, 由于/路由对应的是Home.vue,而Home组建创建的时候,调用了一个后端需要权限的API,于是返回401错误,然后被重定向到login去登录。

登录的时候,发现报错:


  1. XMLHttpRequest cannot load http://basic.backend.local/auth/indexNo 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8080' is therefore not allowed access. The response had HTTP status code 404 

意思是说跨域了,http://localhost:8080, 不被后端接受。跨域问题有很多解决方法,本人倾向于用应用服务器配置解决,这里以 Apache为例,在后端的web目录下,加上一.htaccess文件,内容如下:


  1. <IfModule mod_rewrite.c> 
  2.     Header always set Access-Control-Allow-Origin "*" 
  3.     Header always set Access-Control-Allow-Headers: "X-Requested-With, Content-Type, Origin, Authorization, Accept, Client-Security-Token, Accept-Encoding" 
  4.     Header always set Access-Control-Allow-Methods "POST, GET, OPTIONS" 
  5.     RewriteEngine On 
  6.     RewriteCond %{REQUEST_METHOD} OPTIONS 
  7.     RewriteRule ^(.*)$ $1 [R=200,L] 
  8.  
  9.     RewriteCond %{REQUEST_FILENAME} !-d 
  10.     RewriteCond %{REQUEST_FILENAME} !-f 
  11.     RewriteRule . index.php 
  12. </IfModule> 

告诉Apache,不要管跨域问题啦。Nginx的话,添加:


  1. location / { 
  2.               if ($request_method = OPTIONS ) { 
  3.                 add_header Access-Control-Allow-Origin "*"
  4.                 add_header Access-Control-Allow-Methods "*"
  5.                 add_header Access-Control-Allow-Headers "X-Requested-With, Content-Type, Origin, Authorization, Accept, Client-Security-Token, Accept-Encoding"
  6.                 add_header Access-Control-Allow-Credentials "true"
  7.                 add_header Content-Length 0; 
  8.                 add_header Content-Type text/plain; 
  9.                 return 200; 
  10.               } 
  11.               try_files $uri $uri/ /index.php?$args; 
  12.           } 

跨越问题其实挺复杂, 建议多看看相关资料,这里就不多说了。

加了.htaccess文件后,刷新下页面,发现来到了登录页面,这就对啦。

继续用admin,admin登录。相信就能正常啦。


作者:佚名

来源:51CTO

上一篇:使用FPM快速生成RPM包


下一篇:Silverlight 5 beta新特性探索系列:6.Silverlight 5新增低延迟声音效果类SoundEffect.支持wav音乐格式【附带源码实例】