前后端分离探索——MVC 项目升级的一个过渡方案

# 前言

# 项目环境

  • 后端框架:Phalcon
  • 前端框架:Bootstrap + jQuery

# 什么是前后端分离?

传统项目大多数是 MVC 架构,直接使用 PHP 等后端语言渲染 HTML 模板,返回给浏览器

现在,前后端分离不需要后端渲染模板,而是交由浏览器 Javascript 渲染,后端只需要返回前端渲染所需要的数据即可

前后端分离的本质:

  • 路由分离
  • 模板分离

# 前后端伪分离?

传统 MVC 项目直接升级到前后端分离需要大量的时间与人力,在业务多变的阶段并不适合,所以便有了本文的过渡方案探索

  1. 路由先不分离,仍然采用 PHP 提供的路由
  2. 模板部分分离,在原 PHP 模板中,引入 Vue 编译后的模板,为此需要约定

# 示例

新建控制器 TestController.php

<?php

namespace App\Controller;

class TestController
{
    public function indexAction()
    {
    }
}
1
2
3
4
5
6
7
8
9
10

新建模板 test/index.volt

<div id="app">
    <!-- 约定 一个页面对应一个 Vue 组件 -->
    <index-view></index-view>
</div>
<!-- 约定 一个页面对应一个前端控制器 -->
<script src="/mix/dist/js/test/index.js?v={{ time() }}"></script>
1
2
3
4
5
6

暂时找不到很好解决缓存的方案,所以统一不缓存

新建前端控制器 public/mix/resources/js/test/index.js

import Vue from 'vue';
import ElementUI from 'element-ui';
import IndexView from '@views/test/index.vue';
import Mixin from '@utils/mixin';

Vue.use(ElementUI);
Vue.use(Mixin); // 全局组件、方法、计算属性等

new Vue({
    el: '#app',
    components: { IndexView },
});
1
2
3
4
5
6
7
8
9
10
11
12

新建 Vue 组件 public/mix/resources/views/test/index.vue

<template>
    <div>
        Hello Vue!
    </div>
</template>
<script>
    export default {
        components: {},
        props: {},
        data() {
            return {};
        },
        beforeCreate() {
        },
        created() {
            console.log('Created');
        },
        beforeMount() {
        },
        mounted() {
        },
        beforeUpdate() {
        },
        updated() {
        },
        beforeDestroy() {
        },
        destroyed() {
        },
        watch: {},
        computed: {},
        methods: {},
    };
</script>
<style lang="scss" scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 前后端伪分离

  • 后端框架:Phalcon + Hyperf
  • 前端框架:Bootstrap + jQuery + Vue

前端编译使用 Laravel Mix (opens new window) 工具,这会节省大量前端配置时间

根目录新建文件 webpack.mix.js

const fs = require('fs');
const mix = require('laravel-mix');

const rs_root = 'public/mix/resources';  // 资源 源目录
const rs_output = 'public/mix/dist';     // 资源 打包目录
const js_entry = `${ rs_root }/js`;      // js 源目录
const js_output = `${ rs_output }/js`;   // js 打包目录
const css_entry = `${ rs_root }/css`;    // css 源目录
const css_output = `${ rs_output }/css`; // css 打包目录

mix.webpackConfig({
    resolve: {
        alias: {
            '@': path.resolve(__dirname, rs_root),
            '@api': path.resolve(__dirname, `${ rs_root }/api`),
            '@components': path.resolve(__dirname, `${ rs_root }/components`),
            '@utils': path.resolve(__dirname, `${ rs_root }/utils`),
            '@views': path.resolve(__dirname, `${ rs_root }/views`),
        },
    },
});

// 按照约定,编译对应的资源
fs.readdirSync(path.resolve(__dirname, js_entry)).forEach(dir => {
    fs.readdirSync(path.resolve(__dirname, `${ js_entry }/${ dir }`)).forEach(file => {
        mix.js(`${ js_entry }/${ dir }/${ file }`, `${ js_output }/${ dir }/${ file }`);
    });
});

mix.sass(`${ css_entry }/app.scss`, `${ css_output }/app.css`); // 公共 CSS
mix.setPublicPath(rs_output);
mix.setResourceRoot('/mix/dist/');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 流程

  1. 按照示例配置一个页面
  2. Yarn 安装前端依赖
  3. Yarn 前端编译,此时,PHP 模板中已正确引入 Vue
  4. 访问路由,PHP 渲染模板,返回给浏览器
  5. 浏览器加载 Vue,交由 Vue 渲染页面

# 局限

  • 不能做到全局自动加载组件
  • 编译后的文件大小可能会很大

# 优势

  • 可以更好地编写复杂的页面
  • 更好的维护性

# 权限交互

前后端分离探索——MVC项目升级的一个过渡方案

# 更新 2020/03/13

随着页面重构,文件越来越多,导致编译后总文件大小足足 150 M,而且 Git 合并困难,大大降低了开发效率和前端性能,这明显不合预期;

分析原因:每个页面都引入了公共模块,接下来只要把公共模块分开一个文件即可,并且要做缓存控制

# 缓存控制

添加公共函数

<?php
// /app/lib/WidgetLib.php

namespace App\Lib;

class WidgetLib
{
    public static function get_version($file)
    {
        return json_decode(file_get_contents(BASE_PATH . '/public/mix/dist/mix-manifest.json'), true)[$file];
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

注册公共函数

<?php
// /public/index.php

$compiler->addFunction('get_version', function ($resolvedArgs, $exprArgs) {
	return 'App\Lib\WidgetLib::get_version(' . $resolvedArgs . ')';
});
1
2
3
4
5
6

使用公共函数

<link rel="stylesheet" href="/mix/dist{{ get_version('/css/app.css') }}">

{% if app is not defined %}
  {% set app = 'search' %}
{% endif %}
<div id="app">
  <{{ router.getControllerName() }}-{{ router.getActionName() }}/>
</div>
<script src="/mix/dist{{ get_version('/js/manifest.js') }}"></script>
<script src="/mix/dist{{ get_version('/js/vendor.js') }}"></script>
<script src="/mix/dist{{ get_version('/js/'~app~'.js') }}"></script>
1
2
3
4
5
6
7
8
9
10
11

# laravel-mix 配置

const path = require('path')
const mix = require('laravel-mix')

const rs_root = 'public/mix/resources' // 资源 源目录
const rs_output = 'public/mix/dist' // 资源 打包目录
const js_output = `${rs_output}/js` // js 打包目录
const css_entry = `${rs_root}/css` // css 源目录
const css_output = `${rs_output}/css` // css 打包目录

mix.webpackConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, rs_root),
      '@api': path.resolve(__dirname, `${rs_root}/api`),
      '@components': path.resolve(__dirname, `${rs_root}/components`),
      '@utils': path.resolve(__dirname, `${rs_root}/utils`),
      '@views': path.resolve(__dirname, `${rs_root}/views`),
    },
  },
})
  .disableNotifications()
  .setPublicPath(`${rs_output}`)
  .setResourceRoot('/mix/dist/')
  .js(`${rs_root}/search.js`, js_output)
  .js(`${rs_root}/new.js`, js_output)
  .js(`${rs_root}/edit.js`, js_output)
  .js(`${rs_root}/other.js`, js_output)
  .sass(`${css_entry}/app.scss`, css_output)
  .extract()
  .version()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 入口

按照页面性值,分为四个入口文件:

  • search.js
  • edit.js
  • new.js
  • other.js
// /public/mix/resources/new.js

import Vue from 'vue'
import Router from 'vue-router'
import Mixin from '@utils/mixin'
import ElementUI from 'element-ui'
import * as COMMONAPI from '@api/common'

// 一个页面
import gameDemandsNew from '@views/game-demands/new'

Vue.use(Router)
Vue.use(Mixin)
Vue.use(ElementUI)

Object.entries(COMMONAPI).forEach(item => {
  Vue.prototype[item[0]] = item[1]
})

Vue.config.productionTip = false

// eslint-disable-next-line no-new
new Vue({
  el: '#app',
  router: new Router({
    mode: 'history',
    scrollBehavior: () => ({ y: 0 }),
    routes: [
      // 页面路由
      { path: '/game-demands/new', component: gameDemandsNew },
    ],
  }),
  components: {
    // 页面组件
    gameDemandsNew,
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

/public/mix/resources/js 文件夹可以删掉了,编译后的总文件大小约 2.5 M

至此,优化完成,完美解决了开发流程的痛点

# 后记

目前仍在不断地探索中