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

前言

项目环境

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

什么是前后端分离?

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

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

::: tip
前后端分离的本质:

  • 路由分离
  • 模板分离
    :::

前后端伪分离?

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

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

示例

新建控制器 TestController.php

<?php

namespace App\Controller;

class TestController
{
    public function indexAction()
    {
    }
}

新建模板 test/index.volt

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

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

新建前端控制器 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 },
});

新建 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>

前后端伪分离

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

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

根目录新建文件 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. 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];
    }
}

注册公共函数

<?php
// /public/index.php

$compiler->addFunction('get_version', function ($resolvedArgs, $exprArgs) {
    return 'App\Lib\WidgetLib::get_version(' . $resolvedArgs . ')';
});

使用公共函数

<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>

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()

入口

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

  • 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,
  },
})

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

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

后记

目前仍在不断地探索中

博客同步更新

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 4

这已经不算伪了.
针对伪不伪的问题.
对于这种方式已经使用了前端的大部分东西,个人理解还不如直接分开两个项目了
file
把组件封装在js文件里面.然后直接在html里面使用.
页面文件由php渲染. 页面内容由js生成. 这才是伪前后端分离吧..
楼主那样已经算是前端分离(只是代码放在同一个项目下而已)了.. 不算伪.. 只是构建工具借助了laravel-mix

4年前 评论
cnguu

@可乐加冰 路由是 PHP 的路由,模板也是 PHP 渲染的,并没有分离喔

4年前 评论

不使用到vue,只使用jquery和bootstrap有必要使用到laravel-mix进行打包吗?

4年前 评论
cnguu (楼主) 4年前
Taurus (作者) 4年前
cnguu (楼主) 4年前

请问一下 mix.setResourceRoot() 究竟是干嘛用的呢~翻了好多资料还是没明白。更改了测试发现没什么变化
Laravel

4年前 评论
cnguu (楼主) 4年前
wonderfate (作者) 4年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!