某社区项目
本项目技术栈较为陈旧,使用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 对应 page
var 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>