前后端分离探索——MVC 项目升级的一个过渡方案
# 前言
# 项目环境
- 后端框架:Phalcon
- 前端框架:Bootstrap + jQuery
# 什么是前后端分离?
传统项目大多数是 MVC 架构,直接使用 PHP 等后端语言渲染 HTML 模板,返回给浏览器
现在,前后端分离不需要后端渲染模板,而是交由浏览器 Javascript 渲染,后端只需要返回前端渲染所需要的数据即可
前后端分离的本质:
- 路由分离
- 模板分离
# 前后端伪分离?
传统 MVC 项目直接升级到前后端分离需要大量的时间与人力,在业务多变的阶段并不适合,所以便有了本文的过渡方案探索
- 路由先不分离,仍然采用 PHP 提供的路由
- 模板部分分离,在原 PHP 模板中,引入 Vue 编译后的模板,为此需要约定
# 示例
新建控制器 TestController.php
<?php
namespace App\Controller;
class TestController
{
public function indexAction()
{
}
}
1
2
3
4
5
6
7
8
9
10
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
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
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
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
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
# 流程
- 按照示例配置一个页面
- Yarn 安装前端依赖
- Yarn 前端编译,此时,PHP 模板中已正确引入 Vue
- 访问路由,PHP 渲染模板,返回给浏览器
- 浏览器加载 Vue,交由 Vue 渲染页面
# 局限
- 不能做到全局自动加载组件
- 编译后的文件大小可能会很大
# 优势
- 可以更好地编写复杂的页面
- 更好的维护性
# 权限交互
# 更新 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
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
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
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
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
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
至此,优化完成,完美解决了开发流程的痛点
# 后记
目前仍在不断地探索中