前後端分離探索——MVC 專案升級的一個過渡方案

cnguu發表於2019-11-12

前言

專案環境

  • 後端框架: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 協議》,轉載必須註明作者和本文連結

相關文章