公司项目2017版管理后台改版前端总结(vue)

公司项目2017版管理后台改版前端总结:

这次平台后台改版的目标主要是实现前端工程化,实现前后端分离,让前端开发“自成体系”,脱离对后端的依赖。

一、为什么要做前后端分离

现状

java + velocity模式(mvc)

1.开发流程

1.UI设计 –> 2.前端切图(html,css,js) –> 3.后端开发业务层 –> 4.后端拿到前端的切图转换成velocity模板(套页面)–> 5.后端定义路由跳转 –> 6.后端将路由与页面一一对应 –> 7.测试 –> 8.上线

当后台开发人员辛辛苦苦的把页面都开发好之后,前端人员突然告诉你 xxx.html页面 上面有个样式要改一下…

后端:“什么鬼,html我都转换成velocity了,都不知道哪个是哪个了!”

前端:“我只更新样式,你把样式文件替换一下就可以了”

当替换了之后,

后端:“咦,怎么有些和之前的不一样,算了,两个都保留着吧!”

以至于现在项目中有很多冗余的css,js…

2.修改bug

当测试人员测试出来一系列bug之后,负责人把一些bug分给前端的时候…

后端:“这个bug你来改一下,就是ajax获取数据之后把值赋到那个div上面…”

前端:“这个值是怎么来的啊?${}这个又是什么语法,让我先学学<21天精通javaweb>”

后端:“要你何用!”

3.维护

某天,又出问题了,需要修改页面里面的一项数据…

1
2
3
4
// 放在model中的数据
model.addAttribute("agentInfoList", agentInfoList);
model.addAttribute("condition", condition);
model.addAttribute("area", area);
1
2
3
4
5
6
7
8
9
10
11
12
13
// velocity模板获取model中的数据
<div>
<a href="/wemall/stores/goods?b=${brand.brandID}&s=${stores.id}&typeId=${goodstype.id}&c=${clerkId}&isSearch=false" onclick="MtaH5.clickStat('wemall_main_classify')">
<p class="pimg">
#if(${goodstype.id} == 0)
<img class="aaa" src="${allTypePic}">
#else
<img class="aaa" src="${goodstype.goodstypepicThumbPath}">
#end
</p>
<p class="pfont">${goodstype.typename}</p>
</a>
</div>
1
2
3
4
5
6
7
8
9
10
// 不仅model中有数据,还需要异步请求数据
$.ajax({
type:'post',
url:'http://'+ '${stats_server_name}' +'/stats/stats_add_data',
data:{"action":action,"object_row":object_row,"object_id":object_id,"user_row":user_row,"src_storeid":storeid,"brand_id":brandid,"enter_type":enter_type,"source_tag":source_tag},
dataType:'json',
success:function(data){
window.location.href="/brandmanage/storesManage/storesList";
}
})

前端开心的打开代码:“怎么这么多数据,model里面,ajax请求,有点多让我先来捋一捋…这个是在controller里面塞的,这个是异步请求过来的…有一点混乱…”

4.问题

1.工作职责不清晰。

理想状态是controller层和view层都由前端人员开发维护,但成本也是挺大的,前端需要熟悉后端语言,后端架构。

如果是由后端开发controller层和view层,那么后端也需要熟悉前端的技术。

无论以上哪种开发方式,都是高度耦合的前后端分工,后端人员无法关注于开发业务逻辑,前端人员也无法关注于前端本身的技术。

2.开发效率问题

目前M+是基于MVC框架ssm,架构决定了前端只能依赖于后端。即使前端负责controller层和view层,也依然需要套页面,配置后端开发环境。

为了解决以上的问题,我们需要将关注点分离,职责分离,所以提出了一种修改尝试:后台提供数据,前端负责显示,也就是前后端分离。

二、怎么做前后端分离

什么是前后端分离

前后端分离和微服务一样,渐渐地影响了新的大型系统的架构。微服务和前后端分离要解决是类似的问题,解耦——可以解耦复杂的业务逻辑,解耦架构。可要是说相像吧,消息队伍和前后端便相似一些,通过传递数据的形式来解耦组件。

前后端分离意味着,前后端之间使用 JSON 来交流,两个开发团队之间使用 API 作为契约进行交互。从此,后台选用的技术栈不影响前台。当后台开发人员选择 Java 的时候,我可以不用 JSP 来编写前端页面,继续使用我的 React 又或者 Angular。而我使用 React 时,也不影响后台使用某一个框架。

初步的尝试

1.调研fis3和webpack

通过对fis3和webpack的调研比较,鉴于两者都能实现相同的功能,fis3虽然简单,但是社区不是很好,issue上重复未关闭的问题也没人解答;webpack的优势在于社区比fis3支持的更好,遇到问题通过百度/谷歌/github更容易搜索到相关内容,而且流行的前端框架vue,react等都有对webpack整合之后的脚手架,使开发更加顺畅。

2.修改后端接口为以json方式通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 使用spring4.0提供的 @RestController注解轻松实现返回json格式数据
@RestController
public class MplusManageAgentController {
private final static Logger logger = LoggerFactory.getLogger(MplusManageAgentController.class);

@RequestMapping(value = UrlDefine.MplusManage.AGENTLIST, method = RequestMethod.POST)
public DataRet getAgentList(HttpServletRequest request, HttpServletResponse response,
@RequestParam(value = "page", defaultValue = "1", required = false) Integer page,
@RequestParam(value = "size", defaultValue = "10", required = false) Integer size,
@RequestParam(value = "status", defaultValue = "", required = false) String status,
@RequestParam(value = "keywords", defaultValue = "", required = false) String keywords)
throws IOException {

DataRet ret = new DataRet();
...
List<AgentSettleInfo> agentInfoList = agentSettleService.getOnePageAgentInfo_OnCondition(items, status, keywords);
ret.set("agentList", agentInfoList);
return ret;
}

3.前端使用vue-cli+elementUI搭建

零基础开发参考vue2.0学习笔记01

3.1 实现组件化

引入elementUI中的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<el-col :span="24" class="toolbar" style="padding-bottom: 0px;">
<el-form :inline="true" :model="filters">
<el-select v-model="filters.statusValue" placeholder="审核状态" clearable>
<el-option v-for="item in status" :key="item.value" :label="item.label" :value="item.value">
</el-option>
</el-select>
<el-form-item>
<el-input v-model="filters.keyword" placeholder="关键字"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" v-on:click="handleSearch">查询</el-button>
</el-form-item>
</el-form>
</el-col>

3.2 实现模块化

按需引入模块

1
import { getAgentListPage } from '../../api/api';

导出一个模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export default {
formatDate: {
formate: function (date, fmt) {
var o = {
"M+": date.getMonth() + 1,//月份
"d+": date.getDate(),//日
"h+": date.getHours(),//小时
"m+": date.getMinutes(),//分
"s+": date.getSeconds(),//秒
"q+": Math.floor((date.getMonth() + 3) / 3), //季度
"S": date.getMilliseconds()//毫秒
};
if (/(y+)/.test(fmt))
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
for (var k in o)
if (new RegExp("(" + k + ")").test(fmt))
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
return fmt;
}
}
};

3.3 实现构建开发与发布

配置不同环境的打包

通过配置webpack实现在不同服务器的不同配置,例如接口的ROOT配置:

1
2
3
4
5
//a.env.js
module.exports = {
NODE_ENV: '"production"',
SERVER_ROOT: '"http://a.xxxxxx.com"'
}

在vuex中获取:

1
2
3
const state = {
SERVER_ROOT: process.env.SERVER_ROOT
}

在API模块中获取vuex中的SERVER_ROOT:

1
2
3
4
5
6
7
8
9
let base = store.getters.getServerRoot;
api.ajax = axios.create({
baseURL: base,
timeout: 30000,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
withCredentials: true
});
代码混淆

在webpack配置,实现代码混淆

1
2
3
4
5
6
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
sourceMap: true
})
代码整合

在webpack配置中配置entry,output,loader实现代码整合

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
38
39
40
41
42
module.exports = {
entry: {
app: './src/main.js'
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
}
}
图片资源转换

配置loader,可以实现转换为base64格式

1
2
3
4
5
6
7
8
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
}
路径转换

在webpack配置output

1
2
3
4
5
output: {
path: config.build.assetsRoot,
filename: '[name]-[hash:7].js',
publicPath: '/adminx/'
}
构建发布

只需要在命令行中输入

1
npm run build 

webpack会自动根据之前的配置生成打包后的项目,把项目直接拷贝到服务器即可。

4.mock数据

通过npm mockjs可以实现对数据的模拟。由于这次改版是我直接修改的后台接口,所以没有Mock数据。

5.开发过程中遇到的问题

5.1 跨域请求带cookie给后台

需要在创建axios中添加withCredentials:true

1
2
3
4
5
6
7
8
axios.create({
baseURL: ajaxUrl,
timeout: 30000,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
withCredentials:true
});

由于没有用token,所以这里带上现有的jsessionId给后台,当session过期之后会返回code为error的状态,前端根据返回的状态决定是否重新登陆

5.2 搜索条件的存储

之前的搜索条件都是存储在model里面的,现在是存储在localStorage里面,当页面初始化的时候从localStorage里面去取,离开页面时清除。

5.3 根据账号权限动态生成路由表

准备两套路由表,一套是admin权限,一套是finace权限,还有部分公共路由。

在路由跳转前生成路由表

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
38
39
40
41
42
43
44
45
46
47
48
49
router.beforeEach((to, from, next) => {
//登陆
//动态创建路由表
if (to.path == '/login') {
//清除vuex中的Role,并清除路由表(刷新)
if (store.getters.getRole != 0) {
console.log('reload...');
window.location.reload();
}
next();
} else {
if (store.getters.getRole === 0) {
console.warn('没有获取到用户Role,重新获取中...')
getUserInfo().then((data) => {
let userInfo = data.content.info;
if (data.code == 'error') {
router.push('/login');
} else {
store.dispatch('setUserInfo', userInfo).then((resolve) => {
store.dispatch('setUserRole', userInfo).then((resolve) => {
if (userInfo.role == 'finance') {
let routesmap = routes.common.concat(routes.finance);
store.dispatch('createRoutes', routesmap).then((resolve) => {
router.addRoutes(routesmap); //生成路由表
next(to.path); //必须添加参数,否则不会渲染
});
}
if (userInfo.role == 'admin') {
let routesmap = routes.common.concat(routes.admin);
store.dispatch('createRoutes', routesmap).then((resolve) => {
router.addRoutes(routesmap);
next(to.path);
});
}
}).catch((reject) => {
console.error(reject)
})
}).catch((reject) => {
console.error(reject);
})
}
})

} else {
next();
}
}

})

5.4 icon库的使用

由于elementUI提供的icon不是特别多,我们可以使用iconfont来扩充我们的icon。

首先添加icon到购物车,然后下载css放到相应文件夹下面,

在需要使用的页面中 import ‘../assets/icon/index/iconfont.css’。

5.5 在后台添加允许跨域访问并允许携带cookie

如果部署到不同端口则需要设置,如果是同一端口则不需要

1
2
3
4
5
6
7
8
response.setContentType("text/html;charset=UTF-8");
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "DNT,X-Mx-ReqToken,Keep-Alive,User-Agent," +
"X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,SessionToken");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("XDomainRequestAllowed","1");

三、后续的优化

性能优化

1. H5中的首屏优化,PC暂不考虑

安全优化

1.考虑使用jwt做token验证

测试优化

1.e2e测试,nightwatch

2.unit测试,karma

部署优化

1.考虑是否前端单独部署

异常处理优化

1.对于前端异常的上报

SEO优化

1.SSR服务端渲染

脚手架开发

1.开发符合公司产品的脚手架工具


参考:

前后端分离的思考与实践
你真的懂前后端分离吗?
前后端分离了,然后呢?
我们为什么要尝试前后端分离
系统架构:Web应用架构的新趋势—前端和后端分离的一点想法
图解基于node.js实现前后端分离

…[参考文献太多,不一一列举了]