简单打通前后端,Vue+PHP+Swoole

前后端分离已经是共识,通过Restful API通信传递数据,前端使用React、Vue、Angular三大框架,后端各语言显神威,这篇文章简单你将了解到前后端之间是如何通力合作,Vue是如何构建单页应用,后端是如何通过Swoole提供API服务。

前置开发环境

MacOS 10.13.3
Docker version 18.03.1-ce, build 9ee9f40

1、后端服务EasySwoole

除非是追求创造新框架,一般来说开发都是追求简单快速、已经有的框架,为了照顾以后可能会出现的性能问题(几乎不可能)和兼顾开发速度,PHP+Swoole对我来说是比较不错的选择,API和Socket、队列任务都有现成的框架集成。经过不断考察,最终选择了EasySwoole,里面的代码风格不太认同,不过无偿大碍,而且速度和上手都很容易,加上我自己根据其他人写的验证库,API开发体验上是接近最佳实践。

当然是上Docker,安装swoole容器,将EasySwoole映射进去跑即可,连Nginx都可以节省了,但在同一个服务器也是需要Nginx将请求导向到正确的应用服务器。因为Swoole是常驻内存,这样就和平时的PHP开发非常不用,需要重启Swoole进程,其实这样和Java的开发反而比较类似,但比起Java的编译时间,启动Swoole的时间简直忽略不计。具体开发和注意事项请查看官方文档

EasySwoole官方文档

因为在Docker环境跑,重启Swoole进程需要在容器执行,为了方便重启写了个脚本

#!/bin/bash

CONTAINER=$1
ACTION=$2
COMMAND='php easyswoole'

if [ ! -n "$CONTAINER" ]; then
    echo "Usage ./dev.sh {container}"
    exit 1
fi

if [ ! -n "$ACTION" ]; then
    echo "Usage ./dev.sh {container} {start|stop}"
    exit 1
fi

PROJECT=${PWD##*/}
docker exec -it $CONTAINER sh -c "cd /var/www/html/$PROJECT && $COMMAND $ACTION"

当然项目的地址我也会放在Github上方便大家查看。

项目目录结构

bin代表脚本文件或者可执行文件
Configs代表配置文件,可分测试本地正式
HttpController路由入口文件,根据EasySwoole本身的特性定义
Models这个不多说,大家都懂
Modules业务写的地方
Utils工具类存放的位置
下面的Log、Temp则是EasySwoole本身自带的,dev.shdocker-compser.yml一站式开发文件,方便开发和部署。

swoole的docker镜像大家自行寻找,能用即可

首先创建我们的路由,在HttpController中添加文件夹v1,这个加不加看自己情况,可以不加,但加之后需要根据EasySwoole对路由规则的限定,URL映射也需要做相应的变更,在v1下添加Index.php,这个不同框架有不同的规定,写上简单的代码

namespace App\HttpController\v1;


use App\Modules\Thinks\repositories\ThinksRepository;
use EasySwoole\Core\Http\AbstractInterface\Controller;

class Index extends Controller
{
    public function index()
    {
    	return 'hello world';
    }
}

访问127.0.0.1/v1/index,即可看到hello world。这些有框架帮助我们做,我们只需要专注自己的想法即可。那么现在我们开始来个简单的Restful API接口。

1、配置数据库和各种参数

我们在Config文件夹中配好测试、正式、共用的参数,结构如下

Configs
  -- common 共用的参数
   -- params.php
  -- dev 开发环境参数,可分本地和线上测试
   -- params.php
  -- prod 正式环境参数
   -- params.php

dev下params.php文件为

$commonParams = require(__DIR__ . '/../common/params.php');

$devParams = [
    'mysql' => [
        'host'     => 'ip',
        'username' => 'username',
        'password' => 'password',
        'db'       => 'db',
        'port'     => 3306,
        'charset'  => 'utf8',
    ],
];

return array_merge($commonParams, $devParams);

这些都是配置MySQL链接的参数,在EasySwoole中需要引用参数非常简单

class Db
{
    private $db;

    public function __construct()
    {
        $config = Config::getInstance()->getConf('params')['mysql'];
        $this->db = new MysqliDb(
            $config['host'],
            $config['username'],
            $config['password'],
            $config['db'],
            $config['port'],
            $config['charset']
        );
    }

    public function dbConnector()
    {
        return $this->db;
    }

}

构造一个MysqliDb对象,EasySwoole官方推荐使用MysqliDb,因为这个库帮程序员做了自动重连机制,看这种结构,很明显知道出自国人手,轻为主。外国人做起来运用更多特性,封装更加深,看起来会更加重。

但有个问题是在哪里将这些参数文件引入呢,这得根据框架自身规则,EasySwoole则是推荐在事件初始化引入,在EasySwooleEvent.php中的frameInitialize初始化方法中,loadConf即可,同样EasySwoole提供了很多简便的扫描文件方法和全局配置写入

Class EasySwooleEvent implements EventInterface
{

    public static function frameInitialize(): void
    {
        // TODO: Implement frameInitialize() method.
        date_default_timezone_set('Asia/Shanghai');
        self::loadConf(EASYSWOOLE_ROOT . '/App/Configs/dev');
    }

    public static function loadConf($ConfPath)
    {
        $conf = Config::getInstance();
        $files = File::scanDir($ConfPath);
        foreach ($files as $file) {
            $data = require_once $file;
            $conf->setConf(strtolower(basename($file, '.php')), (array)$data);
        }
    }
}

接着在App/Models中,创建BaseModel类,继承上面的Db类,可以在BaseModel中做更多操作,我这里并没有做。然后创建Bean,提供基本的数据库操作。现在我们创建一张表

CREATE TABLE `hsucy_thinks` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `title` char(64) NOT NULL DEFAULT '' COMMENT '标题',
  `description` char(100) NOT NULL DEFAULT '' COMMENT '描述',
  `content` varchar(10000) NOT NULL DEFAULT '' COMMENT '详细内容',
  `author` char(30) NOT NULL DEFAULT '' COMMENT '作者',
  `status` tinyint(1) unsigned NOT NULL COMMENT '状态',
  `icon` varchar(255) NOT NULL DEFAULT '' COMMENT '图标',
  `created_at` int(11) unsigned NOT NULL COMMENT '创建时间',
  `updated_at` int(11) unsigned NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COMMENT='想法表';

创建Thinks文件夹,在下面创建这张表的Bean,ThinksBean,继承SplBean,方便我们将数据转化成方便setter和getter的模式,在PHP中Bean的作用大概如此,对数据的操作更加偏向Java

namespace App\Models\Thinks;

use EasySwoole\Core\Component\Spl\SplBean;

class ThinksBean extends SplBean
{
    protected $id;
    protected $title;
    protected $description;
    protected $content;
    protected $author;
    protected $status;
    protected $icon;
    protected $created_at;
    protected $updated_at;

    public function getId()
    {
        return $this->id;
    }

    public function setId($id) :void
    {
        $this->id = $id;
    }
    
    setter getter
}

有了Bean和Model,我们需要将数据操作整合到一块,也就是常说的repositories,对repositories进行处理则可以分到service中。
Modules/Thinks/repositories创建ThinksRepository.php

namespace App\Modules\Thinks\repositories;

use App\Models\BaseModel;

class ThinksRepository extends BaseModel
{
    protected $table = 'hsucy_thinks';

    public function select($numRows, $columns)
    {
        return $this->dbConnector()->where('status', 0)->get($this->table, $numRows, $columns);
    }

    public function selectOne($columns)
    {
        return $this->dbConnector()->getOne($this->table, $columns);
    }


}

这时在逻辑不深的话,直接写在controller即可,深的话再封装一层,这里自由发挥没有固定的套路,在HttpControllerIndex中应用刚才的类

namespace App\HttpController\v1;


use App\Modules\Thinks\repositories\ThinksRepository;
use EasySwoole\Core\Http\AbstractInterface\Controller;

class Index extends Controller
{
    public function index()
    {
        $data = (new ThinksRepository())->select(10, ['id', 'title', 'description', 'author']);
        $this->writeJson(200, [
            'list' => $data
        ], '获取成功');

    }
}

再访问http://127.0.0.1:9501/v1/index,一个简单的Restful API完成

这个是非常简单的后端API构成,很多配套都没有做,最重要是安全方面没有做,最简单都要加个token验证,其他诸如xss和csrf都要防范,性能方面,只要选好框架,数据库优化好即可。业务开发主要集中在module即可。

2、前端服务Vue

三大框架之一,因为公司使用Vue,有大量的项目可一参考,那么当然是拿来主义,vue三大库必要用,vue,vue-router,vuex,vuex其实比较重,官网也说得比较抽象,组件之间通信什么,对应后端服务本质上就是类似进程间通信使用Redis做过渡,vuex更聪明能够主动发现变化主动更改,话不多说马上开始。

使用vue-cli搭建基本框架,这款东西相信大家已经非常熟悉,在自己的实际使用中也有自己配置,目录设置如下

使用stylus作为样式文件,store保存api和做持久化,components组件的地方,views则是实际的页面文件,基本的文件结构介绍完毕了,一个SPA基本是这种配置。

现在修改package.json引入,需要用到的包

"dependencies": {
"ajv": "^6.5.3",
"axios": "^0.18.0",
"element-ui": "^2.2.0",
"es6-promise": "^4.2.4",
"vue": "^2.5.2",
"vue-router": "^3.0.1",
"vuex": "^3.0.1",
"web-storage-cache": "^1.0.3"
}

//补充
"devDependencies": {
"vue-style-loader": "^3.0.1",
"stylus": "^0.54.5",
"stylus-loader": "^3.0.2",
}

npm install安装需要的依赖,node什么都不多,就包多

为了偷懒,使用element的UI,反正能用就行,创建ElementUI.js,引用相关js文件

import Vue from 'vue'
import ElementUI, { Message } from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

Vue.use(ElementUI)

Vue.prototype.$message = Message

export default Vue

main.js中将相关组件和库引入并生效

import Vue from 'vue'
import App from './App'
import router from './router'
import './ElementUI'
import 'es6-promise/auto'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  ...App
})

类似后端开发,先定路由,在router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import home from '@/views/home/index';
import other from '@/views/other/index';
import list from '@/views/list/index';

Vue.use(Router)

const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'home',
      component: home
    },
    {
      path: '/other',
      name: 'other',
      component: other
    },
    {
      path: '/list',
      name: 'list',
      component: list
    }
  ]
})

export default router

这里我们设置三条路由,每个对应一个说明的要点,home看挂载,other看UI组件,list看api调用

定了路由,我们要创建相应的页面,在views中定义

//list/index.vue
<template>
    <div id="list">
        <list-view :list="list"></list-view>
    </div>
</template>

<script>
import listView from '@/components/listView/index'
import { getIndexInfo } from 'STORE/action';

export default {
    name: 'list',
    components: {
        listView
    },
    data () {
        return {
            list: []
        }
    },
    async created () {
        this.getIndexInfo()
    },
    methods: {
        async getIndexInfo () {
            const list = await getIndexInfo()
            this.list = list
        }
    }
}
</script>

//other.vue
<template>
  <div id="other">
      <Header></Header>
  </div>
    
</template>

<script>
import Header from '@/components/header/index'

export default {
    name: 'other',
    components: {
        Header
    }
}
</script>

其实这一步应该是在抽象出相应的组件先,在components中抽象相应的组件,然后在views中将组件组合挂载,这就是vue带来的前端开饭变化,一开始非常懵逼,但多接触后其实也不难理解了,很多原理都差不多。

//components/header/index.vue
<template>
    <div id="header">
        <el-menu
        :default-active="activeIndex2"
        class="el-menu-demo"
        mode="horizontal"
        @select="handleSelect"
        background-color="#545c64"
        text-color="#fff"
        active-text-color="#ffd04b">
        <el-menu-item index="1">处理中心</el-menu-item>
        <el-submenu index="2">
            <template slot="title">我的工作台</template>
            <el-menu-item index="2-1">选项1</el-menu-item>
            <el-menu-item index="2-2">选项2</el-menu-item>
            <el-menu-item index="2-3">选项3</el-menu-item>
            <el-submenu index="2-4">
            <template slot="title">选项4</template>
            <el-menu-item index="2-4-1">选项1</el-menu-item>
            <el-menu-item index="2-4-2">选项2</el-menu-item>
            <el-menu-item index="2-4-3">选项3</el-menu-item>
            </el-submenu>
        </el-submenu>
        <el-menu-item index="3" disabled>消息中心</el-menu-item>
        <el-menu-item index="4"><a href="https://www.ele.me" target="_blank">订单管理</a></el-menu-item>
        </el-menu>
    </div>
</template>

<script>
  export default {
    name: 'Header',
    data() {
      return {
        activeIndex: '1',
        activeIndex2: '1'
      };
    },
    methods: {
      handleSelect(key, keyPath) {
        console.log(key, keyPath);
      }
    }
  }
</script>

<style>
.el-menu-demo {
  margin: 0px;
  border: 0px;
  padding: 0px;
  height: 100%;
  width: 100%;
  top: 0;
  z-index: 10;
}
</style>

//components/listView/index.vue
<template>
  <div id="list-view">
    <ul class="list-ul">
      <li class="list-li" v-for="item in list" :key="item.id">
        <a :href="parseLink(item.id)" target="_blank" class="list-li-info">
          <h1 class="title-content">
            <span>{{item.title}}</span>
            &nbsp;<span class="title-time">[{{item.createdAt}}]</span>
            &nbsp;<span v-if="item.status" class="icon-top">有效</span>
          </h1>
          <div class="content-info">
            <span>{{item.author}}</span>
            <span>{{item.description}}</span>
          </div>
        </a>
      </li>
    </ul>
  </div>
</template>


<script>
export default {
    name: 'listView',
    props: {
        list: {
            type: Array
        }
    },
    methods: {
        parseLink (id) {
            return `${window.location.origin}/content/${id}`
        }
    }
}
</script>

<style>
#list-view .list-li {
    padding: 24px 0;
    border-bottom: 1px solid #f4f4f4;
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
}

#list-view .list-li-info {
    flex: 0 0 450px;
    cursor: pointer;
}

#list-view .title-content {
      display: flex;
      align-items: flex-end;
      font-size: 16px;
      color: #333333;
      height: 16px;
      line-height: 16px;
      font-weight: 400;
      cursor: pointer;
}

#list-view .title-content >>> span {
    max-width: 315px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

#list-view .title-time {
    display: inline-block;
    vertical-align: top;
    font-size: 14px;
    color: #a8a8a8;
}

#list-view .content-info {
    margin-top: 16px;
    color: #646464;
    font-size: 14px;
    height: 20px;
    line-height: 20px;
}

#list-view .info-money-wrap {
    font-size: 14px;
}

#list-view .info-money {
    color: #ff8a01;
    font-size: 16px;
}
# 
#list-view .is-top {
    display: inline-block;
    line-height: 18px - 2px;
    height: 18px;
    width: 32px;
    color: #9280ff;
    text-align: center;
    font-size: 12px;
    border: 1px solid #9280ff;
    border-radius: 8px;
    background: #efecff;
}
</style>

相信到这,已经明白vue文件的三板斧,template、script、style,本来以前这三者是要分开,现在在组件化浪潮中反而合并到一个组件中,你说为什么会有这种变化?目前业界是比较认同将页面中的元素一个个抽象成组件,在维护大型应用时候,清晰化的组件其实是比较有利于查找问题,复用倒是其次,复用没必要用上前端框架。

然后我们改看api调用了

//store/api.js
import axios from 'axios';

const testApi = 'http://localhost:9501/v1/';

axios.defaults.baseURL = testApi;
axios.defaults.headers.Accept = 'application/json';


export const getIndexInfo = data => {
    return axios.get('index', {params: data});
}

//store/action.js
import * as api from 'STORE/api'

export const getIndexInfo = async data => {
    const reponse = await api.getIndexInfo(data)
    return reponse.data.result.list
}

vuex实在太重,action可以用global的方式实现一个轻量级的类vuex功能的地方,这里就不在演示,不是专业的前端不花太多时间在这身上,相信代码都非常简单易懂,设置api地址,使用异步调用,这些都是基本操作,webpack的conf文件,注意配置stylus和alisa即可,这里我没用上stylus,简单样式

然后我们开始npm start,查看我们的成果

localhost:8080

localhost:8080/other

localhost:8080/list

路由、页面组件挂载、api使用都用上了。

当然前端还会有其他配套措施,比如创建资源使用qs过滤,token的处理,缓存等等,这些都是能说很多,但我们说明这个是简单打通,那就没必要费太多精力了,广不如精。集团化的分工合作才是主流,多面手易造成多面稀松,像这篇文章,样式是比较糟糕,调样式是个细活,需要耐心,这中其实很耗时间,需要大量积累。

Show Comments