公司项目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" > <img class="aaa" src="${allTypePic} " > <img class="aaa" src="${goodstype.goodstypepicThumbPath} " > </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搭建 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/' }
构建发布 只需要在命令行中输入
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(); } } })
由于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.开发符合公司产品的脚手架工具
参考: …[参考文献太多,不一一列举了]