某社区项目
本项目技术栈较为陈旧,使用framework7+template7+gulp+less+requireJS
。页面也存在很多迭代之后废弃的,故整理起来非常复杂,本文档将从几个方面试图对本项目进行梳理
为了使开发快速高效,使用了以下辅助工具:
- 样式预编译器:LESS
- JS 模块依赖管理:requirejs
- 自动化构建工具:gulpjs
- 应用框架:Framework7
建议阅读本文档之前,先学习
1.framework7官方文档
2.template7官方文档
1.目录结构
项目总览
该项目的静态资源全在app
文件下,如果需要全量出包,需要把整个app
文件进行替换。其中build
文件下有压缩的js和css
如果页面文件pages
和图片文件img
未做改动,那么只需要替换build
和index.html
即可
具体js文件
值得注意的是,本项目的会员权益相关的部分,是由林宇使用svelte
框架进行重构的,其他文件夹下的membership
即是相关的。同时也在gulp中使用了typeScript
进行转译
具体js对应具体页面
关于页面和JS的对应关系,详见index.controlller.js
如下方法:
//初始化事件function init() {$$(document).on("click", ".views .tip-box a:not(.other), .views .submit-btn", function() {//发送举报var report_type = $$(this).data("report_type");console.info("report_type:" + report_type);if ($$(this).hasClass("submit-btn")) {if (!$$("input[name=otherReason]").val().trim()) {f7.alert("请输入举报原因");return;}}var pageName = getCurPage().data("page");//即HTML文件里的data-page属性require(["controllers/" + pageName + ".controller"], function(controller) {controller.report(report_type);});cancleSelect();//举报完成,隐藏举报相关内容});
在一个HTML页面里,data-page
属性即为其对应js的名字
<div class="page tabbar-labels-fixed" id="AddAddressContent" data-page='addAddress'><div class='page-content page-add-address'>
注意,每个页面下对应的less
文件必须在app.less
里面注册,
@import 'pages/vip-rule';
@import 'pages/level-up-tips';
@import 'pages/mall-order';
@import 'pages/modify-address';
@import 'pages/earn-integral';
@import 'pages/gift-exchange';
至于与页面一一对应,只需要类名对应即可,格式如下.page-mall-order
不再过多阐述
2.关于项目注意事项,见readme.md
开发
要求 Node 在 4.x 的版本以上。
// 克隆至本地
git clone git@git.cairenhui.com:Community/ht-community-h5-s2.git
cd ht-community-h5-s2// 安装倚赖
npm install// 运行前必须先打一次包
gulp build
//启动服务器
npm run dev
// 注意:因为后端开发把index.html放入客户端,所以如果想在手机端看效果请在index.ejs配置: if (isDevMode) {App_token = '<%= token%>';App_accountId = '<%= accountId%>';
}
本项目目前是js文件和HTML文件一一对应的架构,如果需要书写es6标准js,请在es6文件下新建js,会自动转译成es5。命名规则参照html里的data-page属性并加上.controller
后缀
自适应各屏幕分辨率
如果index.ejs中没有定义meta name=viewport
,flexible
插件将动态根据各个屏幕的dpr动态改变html的class类名以及meta name=viewport的scale,而基准则是gulpfile中px2rem
中的remUnit的值,因为此项目一开始是以按除以二的方式计算距离,所以remUnit
的值为现在设计稿750的一半(37.5
)
3.接口文档相关
这项目之前的老旧文档都是Excel里的,无法提供一份全的文档,目前新接口以及改造过的老接口已经上线到eolinker地址
具体接口详细文档可以询问后端开发人员陈庆林,金明明。
下面详细介绍接口的定义规则,在api.ts
里定义,如下:
var API = {// 获取数据失败标志位failed: 'failed',// 用于签名校验appKey: 'cairenhuiweixin',//所有接口都传以下参数appParams: function () {return {appKey: this.appKey,timestamp: new Date().getTime(),appVersion: App_version,clientType: App_clientType};},// 赞support: function (data) {data = Utils.extend(data, this.appParams());data = Utils.serialize(data);return Http.post(App_data_domain + 'ajaxQueryPraise.json?' + data).then(handleSuccess, handleError);},
在具体页面里调用接口之前,需要先在API
对象里定义对应的成员方法,然后如下调用即可:
API.json2309({//加载文章内容articleId: articleId,accountId: App_accountId,source: source,token: App_token}).then(function(result){var data = result.resultMap;if(data.status === '2'){//状态2表示已下架,显示该文章已经删除$$('.empty-article-wrap').show();return;}
具体的ajax方法是在http.js
里定义的,通过promise
对象,如果返回的是reject
,则调用api.js里的handleError
//出错的时候调用自己的出错页面----如果有错误信息则弹窗提示function handleError(data) {f7.hideIndicator();if (data.error_no == '-9') {//跳转到提示升级页面即可mainView.loadPage(App_domain + 'page/level-up-tips.html');return} else if (data.error_no == '-10') {//返回-10,需要登出App_accountId = null;App_token = null;window.changeHref(window.hrefParam.function_exit);return}var msg = data.error_info;if (msg == 'goMaintainPage' || msg.indexOf('Bad Gateway') >= 0) {mainView.loadPage(App_domain + 'page/maintain-page2.html'); //跳转到‘系统维护升级中2’页面} else if (msg || msg != '') {f7.alert(msg);} else if (msg == '客户端版本过低,请升级后使用') {return}return API.failed;}
在这个方法里 可以对一些错误进行拦截和处理
4.客户端交互相关
这里的代码基本都是定义在js/controllers
下的index.controller.js
,这个文件会在项目初始化时直接调用。见app.js
Index.init();Router.init();
本项目作为H5与原生APP端交互的方法是这样定义的:
window.hrefParam = {//调用app原生接口的url'close_needRefresh': 'ht_square_function_close_needRefresh',//关闭页面--效果同下面那个'close': 'ht_square_function_close',//关闭页面'loginSquare': 'ht_square_function_loginSquare',//跳转到登录页面'function_params': 'ht_square_function_params',//相当于当前的webView重新加载一次社区'reloadViewList': 'ht_square_function_reloadViewList',//重新加载观点列表,用于观点列表页面'function_height': 'ht_square_function_@height',//ht_square_function_ 这个webView会拦截,并调用@后调用方法'function_exit': 'ht_square_function_exit',//退出社区,主页在用'function_checkFund': 'ht_square_function_checkFund',//no--没在用'function_opensjkh': 'ht_square_function_opensjkh',//no--没在用'backView': 'ht_square_function_backView',//返回上个页面,并调用对应的方法,传入对应参数,用于观点列表页面'checkFund_request': 'ht_square_function_checkFund_request',//彩虹主页打开绑定资金账号页面'function_share': 'ht_square_function_@share','function_errorPage': 'ht_square_function_@errorPage=true&@height','edit_nickname': 'ht_square_function_edit_nickname',//打开编辑昵称页面'edit_image': 'ht_square_function_edit_image',//打开编辑头像页面'infoDetail': 'ht_square_function_@infoDetail',//打开资讯页面'openLocalUrl': 'ht_square_function_openLocalUrl_request',//打开一个新的webview'openAdvertisement': 'ht_square_function_openAdvertisement_',//打开对应的广告页面'gotoUpdate':'ht_square_function_gotoUpdate',//提示版本升级页点击调用客户端方法跳转应用市场'openYlMarket': 'no_decode_ht_square_function_openAdvertisement_'//打开怡乐商城}
这是调用客户端的方法,如window.changeHref(window.hrefParam.loginSquare);
就是调用跳转到登录页面的方法
如果要客户端调用我们的方法,则如下定义即可:
window.htsecBack = leftBtnEvent;//暴露返回方法给全局调用
还有一点值得介绍的是,本项目把返回按钮的方法都改写了,可以根据历史记录长度和具体页面编号来进行拦截处理
//点击返回按钮时-----全部改成自己控制路由function leftBtnEvent(event){//如果是维护页面,按返回键直接关闭社区,退回到app里----if(ehtescObj.pageCode == 'maintain-page2'||ehtescObj.pageCode == 'level-up-tips'){window.changeHref(window.hrefParam.close);return;}var param;if(mainView.history.length <= 2){//如果小于2则证明是退出社区,或者是其他特别的处理if(ehtescObj.pageCode == 'home' || ehtescObj.pageCode == 'mall.index' || ehtescObj.pageCode == 'mall-game'|| ehtescObj.pageCode == 'scoreMine' || ehtescObj.pageCode == 'signIn') {window.changeHref(window.hrefParam.close_needRefresh,'=refreshData');}else if(ehtescObj.pageCode == 'send-comm' || ehtescObj.pageCode == 'reply' || ehtescObj.pageCode == 'sendComment') {window.changeHref(window.hrefParam.backView,'@loadNewData@' + window.ehtescObj.backRefreshFlag);}else if(ehtescObj.pageCode == 'comment-detail'){window.changeHref(window.hrefParam.backView,'@loadNewData@'+ viewRefreshFlag);}else if(ehtescObj.pageCode == 'personalcenter'){if (personalFlag == 0) {param = '@loadNewData@-2'} else {param = '@loadNewData@-1'}window.changeHref(window.hrefParam.backView,param);}else if(ehtescObj.pageCode == 'refresh-comlist'){if (publishViewFlag == 1) {param = '@loadNewData@-1'} else {param = '@loadNewData@-2'}window.changeHref(window.hrefParam.backView,param);}else if(ehtescObj.pageCode == 'acct-binding' && window.localStorage.getItem("backPointReloadPage") == 'yes'){window.changeHref('ht_square_function_finishAndInvokJS_reloadPointPage');}else if(ehtescObj.pageCode === 'modify'){//修改页面回退应该回退到主页mainView.history = [mainView.history[0],'page/personal-message.html',mainView.history[1]];mainView.router.back();}else{//默认就直接关闭页面window.changeHref(window.hrefParam.close);}}else{//如果大于2则直接使用f7框架的返回// window.changeHref('ht_square_function_go_back');if(ehtescObj.pageCode == 'kline'){if (modalFlag) {return;}modalFlag = true;f7.modal({text: '确定要退出K线英雄游戏?',buttons: [{text: '继续游戏',onClick: function() {modalFlag = false;return;}}, {text: '不想玩了',onClick: function() {API.kLineOver({token: App_token,accountId: App_accountId,endTime: window.ehtescObj.overTime,endPrice: window.ehtescObj.sellPriceLast}).then(function(data) {//退出操作,不做任何回调});modalFlag = false;mainView.router.back();return;}}]})return;
由于本项目是嵌在APP里的H5项目,故需要模拟登陆参数,目前是通过URL拼接的方法,如下:比如我要访问彩虹俱乐部界面,就可以在地址栏输入:
http://localhost:8000/?request=rainbow&accountId=1000185789&token=d49478f04b2f24a8ae30d2239e50e9fb0706138260d8db44b80369a50f859356ef806bc599af22b56f00a5b98cc2e86be9604d457f7195d0493db0cdd1443dadd9bc183708c8b0725460de719c853e11691741f91292ebcb89ef63e753e9a305ed0d708c7bee939031d4623d6cc93e0245c615a75dae6aa673e30e29fd885b3e&topicCode=TOP002
注意,所有页面都需要带上accountId
和token
,如果想要知道具体页面需要什么参数,可以看下面这个方法,在入口文件index.ejs里定义的:
function getUrlParam(name) {//var htsecUrl = decodeURIComponent(window.location.search);//var htsecUrl = window.location.search;//alert(decodeURIComponent(window.location.search));var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); //构造一个含有目标参数的正则表达式对象//var r = window.location.search.substr(1).match(reg); //匹配目标参数var r = htsecUrl.substr(1).match(reg);if (r != null) return r[2]; return undefined; //返回参数值}
```js
方法的主要作用就是,在url里获取到指定属性名的值,所以如果想要知道具体页面需要哪些传参,只要关注上面这个方法就行:
App_userId=App_userId||getUrlParam('user_id');
5.路由跳转相关
本项目内部路由使用的是framework7
的路由,内部跳转方式如下:
mainView.router.load({url: App_domain + 'page/community-personalcenter.html?user_id=' + to_user_id,animatePages: false})
或者(其实就是一个方法)
mainView.router.loadPage(App_domain + "page/my-community.html");
如果想要直接通过地址栏url进行跳转,则输request=
文件名即可,在router.js
里定义的方法如下:
// 前面的都没匹配的且有 request// 统一默认处理 request 对应 pagevar pageUrl = App_domain + 'page/' + request + '.html';request? mainView.router.load({url: pageUrl,animatePages: false}): mainView.router.load({url: firstPage + '?time=' + new Date().getTime(),animatePages: false});
如果想要内部路由传参,则需要拼接query
参数,如下:
url: App_domain + 'page/community-personalcenter.html?user_id=' + articleUserId,
user_id=' + articleUserId
这一块儿在下一个页面里的init
方法里通过query
可以取到,如下:
function init(query){pageNum = 0;loading = false;loadOver = false;//是否已加载完所有的数据hasMore = 1;lastViewId = undefined;template.bind(bindings);//绑定页面上的事件articleId = query.articleId || getUrlParam('articleId');
5.gulp相关
出包
gulp clean
gulp build
如果有第三方js引入需求
gulp.task('jsmin', () => {//压缩这几个导入的第三方包pump([gulp.src(['app/js/libs/flexible_css.debug.js','app/js/libs/flexible.debug.js','app/js/libs/clipboard.min.js','app/js/vendors/crypto-js.js','app/js/vendors/require.js','app/js/libs/vconsole.min.js']),uglify(),header(banner, {pkg: require('./package.json')}),gulp.dest('app/build/libs')]);
});
将路径添加进这里,并在index.ejs
里通过<script>
引用即可
<script src="build/libs/crypto-js.js"></script>