SPA预研
简介
演 讲 者 :
10年西南大学本科毕业,后从事2年java开发, 12年专职前端开发,13年进入腾讯,
现职于FIT 金融市场部前端开发工程师。
http://www.lowinwu.com
lowinwu(吴祥 )
博 客 :
个人简介 :
SPA预研
大纲
1、何为单页面
3、构成
2、业界方案
4、自研框架
6、优缺点
7、疑问解答
5、案例
SPA预研
开发区别
体验区别
1、页面前进,
资源重复下载、执行
2、页面回退,
资源重复下载、执行
1、配置环境
2、模板片段引入
标题,路由执行方法...
js&html结合
3、减少cgi数量
模板页面,微信授权信息获取
<?cs include:PARSE_PATH("/hybrid/mch/inc/js/dust.html") ?>
<?cs include:PARSE_PATH("/hybrid/mch/inc/js/hkwallet_config.html") ?>
<script type="text/javascript">
var USER_FLAG = "<?cs var:user_flag ?>";
var PAY_PARAM = {
bind_type : "<?cs var:bind_type ?>" || 1,
user_state : "<?cs var:user_state ?>" || 1,
ticket_flag : "<?cs var:ticket_flag ?>",
ticket : "<?cs var:ticket ?>"
}
var G_NOT_NEED_BASE = 1;
dust.use(["/res/scripts/app/hkwallet/ewallet_global.js","/res/scripts/app/hkwallet/ewallet_add_cardnum.js"],function(app,index){
app.init(function(){
index.init();
});
})
</script>
</body>
</html>SPA预研
业界方案
SPA预研
业界方案(1)-pjax
ajax+pushstate
http://userbag.co.uk/demo/pjax/
https://github.com/welefen/pjax
SPA预研
业界方案(1)-pjax
ajax+pushstate

SPA预研
业界方案(1)-pjax
ajax+pushstate
1、不支持刷新功能,需接合cgi
缺点
2、依赖jquery
3、不支持hash方式,兼容性差
SPA预研
业界方案(2)-angular
requirejs + angular + angular-route
http://beletsky.net/2013/11/using-angular-dot-js-with-require-dot-js.html
define(['angular', 'require', 'angular-route'], function (angular, require) {
var app = angular.module('webapp', [
'ngRoute'
]);
app.config(['$routeProvider', '$controllerProvider',
function($routeProvider, $controllerProvider) {
$routeProvider.
when('/module1', {
templateUrl: 'module1/tpl.html',
controller: 'module1Controller',
resolve: {
keyName: function ($q) {
var deferred = $q.defer();
require(['module1/module1.js'], function (controller) {
$controllerProvider.register('module1Controller', controller); //由于是动态加载的controller,所以要先注册,再使用
deferred.resolve();
});
return deferred.promise;
}
}
}).
otherwise({
redirectTo: '/module1'
});
}]);
return app;
});define(['angular'], function (angular) {
//angular会自动根据controller函数的参数名,导入相应的服务returnfunction($scope, $http, $interval){
$scope.info = 'kenko'; //向view/模版注入数据//模拟请求cgi获取数据,数据返回后,自动修改界面,不需要啰嗦的$('#xxx').html(xxx)
$http.get('module2/tpl.html').success(function(data) {
$scope.info = 'vivi';
});
};
});
SPA预研
业界方案(2)-angular
requirejs + angular + angular-route

SPA预研
业界方案(2)-angular
requirejs + angular + angular-route
1、压缩包78K
缺点
2、开发逻辑复杂
3、语法复杂
SPA预研
业界方案(3)-backbone
http://coenraets.org/blog/2013/06/building-modular-web-applications-with-backbone-js-and-requirejs-sample-app/
Modularized Model
define(function (require) {
"use strict";
var $ = require('jquery'),
Backbone = require('backbone'),
Employee = Backbone.Model.extend({
urlRoot: "http://localhost:3000/employees",
initialize: function () {
this.reports = new EmployeeCollection();
this.reports.url = this.urlRoot + "/" + this.id + "/reports";
}
}),
EmployeeCollection = Backbone.Collection.extend({
model: Employee,
url: "http://localhost:3000/employees"
});
return {
Employee: Employee,
EmployeeCollection: EmployeeCollection
};
});Modularized Router
define(function (require) {
var $ = require('jquery'),
Backbone = require('backbone'),
$content = $("#content");
return Backbone.Router.extend({
routes: {
"": "home",
"employees/:id": "employee"
},
home: function () {
require(["app/views/Home"], function (HomeView) {
var view = new HomeView({el: $content});
view.render();
});
},
employee: function (id) {
require(["app/views/Employee", "app/models"], function (EmployeeView, models) {
var employee = new models.Employee({id: id});
employee.fetch({
success: function (data) {
var view = new EmployeeView({model: data, el: $content});
view.render();
}
});
});
}
});
});Modularized View
define(function (require) {
"use strict";
var $ = require('jquery'),
_ = require('underscore'),
Backbone = require('backbone'),
tpl = require('text!tpl/Employee.html'),
template = _.template(tpl);
return Backbone.View.extend({
render: function () {
this.$el.html(template());
return this;
}
});
});SPA预研
业界方案(3)-backbone

SPA预研
业界方案(3)-backbone
1、MVC死板
缺点
2、开发流程复杂
SPA预研
构 成
路由管理
代码组织
注册路由,查找路由,启动路由
视图注入,title更换,主入口
SPA预研
自研框架
SPA预研
面临问题
URL监听
缓存策略
首屏保障
代码隔离
切屏方案
......
穿透事件
跨域方案
性能优化
SPA预研
措 施
1、路由注册与监控
2、功能模块化加载与管理
SPA预研
路由监控
1、注册路由
2、ajax与tap/click事件
3、监控路由变化
刷新&第三方跳转
重新包装&写路由
onhashchange
SPA预研
多模块加载&管理
描述:复杂交互中,
按需加载,代码隔离,模块注入...
?如何解决?
SPA预研
方案1:html与js全注入首页
消耗首屏,无效加载,js污染
方案2:动态加载js--require("xx.js")
减少js消耗首屏时间&无效加载
<div class="container">
<?cs include:PARSE_PATH("/hybrid/mch/weixin/transfer_hk/mod/guide.shtml") ?>
<!--表单[[-->
<div class="main hide js-send"></div>
<!--表单]]-->
<?cs include:PARSE_PATH("/hybrid/mch/weixin/transfer_hk/mod/input_confirm.shtml") ?>
</div>
dust.use(["/res/scripts/app/hkwallet/ewallet_global.js","/res/scripts/app/hkwallet/ewallet_add_cardnum.js"],function(app,index){
app.init(function(){
index.init();
});
})regRouters:function(){//注册路由
$.SPA.r({
id: 'main_content',//挂载点
type:'index',//设置为首页
animate: '', //入场动画效果
script:'/res/scripts/app/transfer_hk/record.js'
});
},
init:fucntion(){
this.regRouter();//注册路由
$.SPA.startRun();//启动监听
}多模块管理
SPA预研
方案3:业务js中嵌入html
代码强偶合,不利于维护
<div class="container">
<?cs include:PARSE_PATH("/hybrid/mch/weixin/transfer_hk/mod/guide.shtml") ?>
<!--表单[[-->
<div class="main hide js-send"></div>
<!--表单]]-->
<?cs include:PARSE_PATH("/hybrid/mch/weixin/transfer_hk/mod/input_confirm.shtml") ?>
</div>define(function(require,exports,module){
var body = [
'<div>',
'<ul class="list"...........
].join("");
var Detail = {
title: '订单详情',
body : body,
init : function($view,options){
this.render();
$view.addClass("detail").removeClass("hide");
$.hideLoading();
},
....
}
}多模块管理
SPA预研
方案4:业务js,require对应业务html
<div class="container">
<div id="index"></div>
<!--表单[[-->
<div class="main hide js-send"></div>
<!--表单]]-->
<div id="input-confirm"></div>
</div>define(function(require,exports,module){
var tpl = require("text!/hybrid/mch/weixin/transfer_hk/mod/detail.shtml");
var Detail = {
title: '订单详情',
body : tpl ,
init : function($view,options){
this.render();
$view.addClass("detail").removeClass("hide");
$.hideLoading();
},
....
}
}多模块管理
SPA预研
方案4:实现原理
var load = function () {
......
emit("resolve",emitData);
emit("request",emitData);
if (!emitData.requested && url) {//非模板文件
_loadScript(url, function () {// normal text
register({
name: "text",
ext: [".tpl", ".html",".shtml"],
exec: function(uri, content) {
globalEval('define("' + uri + '", [], "' + jsEscape(content) + '")')
}
})
function xhr(url, callback) {
var r = global.XMLHttpRequest ?
new global.XMLHttpRequest() :
new global.ActiveXObject("Microsoft.XMLHTTP")
r.open("GET", url, true)ajax请求,增加define逻辑
多模块管理
SPA预研
发布方案
编译时,
交由工具js中注入html

多模块管理
SPA预研
模块配置文件
var defaultUIOptions = {
id: '', //路由名,注入点
script:'', //模块名
init: function() {}, //初始化回调函数
beforeinit: function() {}, //打开前回调
afterinit: function() {}, //打开后回调
beforeclose: function() {}, //关闭前回调
afterclose: function() {} //关闭后回调
} $.fn.template = require("/res/scripts/mod/global/template.js");
var tpl = require("text!/hkwallet/zh_hk/balance/mod/detail.shtml");
var Detail = {
title: '零錢详情',
body : tpl,
bodyClass:'rec-record-detail',
//更新body
beforeinit:function(options){//处理数据
this.initModel(options);
this.title = $.Lang.get("detailTitle");
},
init : function($view,options,rr){
this.render(options);
},多模块管理
SPA预研
3、文件加载
2、页面结构
<body class="rec-money">
<!--首页面]] -->
<div class="container" id="js-index">
</div>
<!--总结构-->
<div class="view-container" style="height: 100%;">
</div>
dust.use([uiModule['script']],function(module){ var Router = {
regRouter : function(){
//首页面
$.SPA.r({
path: 'js-index',//挂载点
animate: '', //入场动画效果
bodyClass:"rec-money",
type : 'index',
script:'',
beforeinit:function(){
//國際化title
this.title = $.Lang.get("indexTitle");
$("#balance").text(cur_type + $.Amount.fen2Yuan(balance||0));
$("#go-record").unbind($.Env.CLICK_EVENT).bind($.Env.CLICK_EVENT,function(){
$.SPA.boot('record');
});
$(".pop-loading-txt").text($.Lang.get("loading"));
//事件處理
$.Env.goWalletIndex();
$.Env.goDrawIndex();
},
afterinit:function(){
$.hideLoading();
}
});
//注册输入路由:
$.SPA.r({
path: 'detail',//挂载点
animate: '', //入场动画效果
script:'/res/scripts/app/hkwallet/balance/detail.js'
});
//注册新的路由:
$.SPA.r({
path: 'record',//挂载点
animate: '', //入场动画效果
classname: '', //自定义样式名
script:'/res/scripts/app/hkwallet/balance/recordmain.js'
});
},
init : function(tmplate){
this.regRouter();
$.SPA.startRun();
}
}
1、路由注册
多模块管理
SPA预研
6、数据传递
//value from session
if(arg && arg.length == 0){
arg = SPA.Store.getItem(qs+"_"+uiModule['script'])
}
module.init.call(module,$("#"+id),arg);5、body更新
//默认在view-container上,统一在一个节点上,利于开发时,在首页加节点
var viewContainer = $("#"+id);
var view = viewContainer.length == 0 ? $(".view-container") : viewContainer;
//清事件
this.clearData(this.getAllNodes(view));
//注入
view.html(html);4、title更新
// hack在微信等webview中无法修改document.title的情况
var $iframe = $('<iframe style="display:none;" src="/favicon.ico"></iframe>').on('load', function() {
setTimeout(function() {
$iframe.off('load').remove() }, 0)
}).appendTo($body);多模块管理
SPA预研
1、资源加载
JS,html,css資源文件如何加載?
2、线上动态合并加载
1、全部打包一次性加载
性能优化
SPA预研
动态线上合并加载

动态合并 mgp_file_merge.cgi
/res/scripts/app/transfer_hk/send/sendmain.js<itemSplit>define(function(require,exports,module){
require("/res/scripts/mod/global/spa.js");
var hkwexin = require("/res/scripts/mod/global/hkweixin.js");
var SendMain = {
regRouter : function(){
//首页面
$.SPA.r({
id: 'js-guide',//挂载点
animate: '', //入场动画效果
type : 'index',
script:'',
beforeinit:function(){
$(".js-index").removeAttr("class").addClass("js-index hide-mod-pop-loading guide");
$("#start_use").unbind($.Env.CLICK_EVENT).bind($.Env.CLICK_EVENT,function(){
hkwexin.selectSingleContact({success:function(data){
var openid = data["info"]["openid"];
$.SPA.boot('input_page',{openid:openid});
}});
});
},
afterinit:function(){
$.hideLoading();
}
});
//注册输入路由:
$.SPA.r({
beforeclose:function(){alert(3);},
id: 'input_page',//挂载点
animate: 'fadeIn', //入场动画效果
script:'/res/scripts/app/transfer_hk/send/inputpage.js'
});
//注册新的路由:
$.SPA.r({
id: 'ok_page',//挂载点
animate: '', //入场动画效果
classname: '', //自定义样式名
script:'/res/scripts/app/transfer_hk/send/okpage.js'
});
},
init : function(tmplate){
this.initWX();
this.regRouter();
$.SPA.startRun();
},
initWX:function(){
hkwexin.initWX(para);
hkwexin.hideShare({});
}
}
return SendMain;
});<resSplit>/res/scripts/app/transfer_hk/send/inputpage.js<itemSplit>/**
* 输入界面
*/
define(function(require,exports,module){
var tpl = require("text!/hybrid/mch/weixin/transfer_hk/mod/input_page.shtml");
var InputExtend = require("/res/scripts/mod/global/inputextend.js");
var datavalid = require("/res/scripts/mod/global/datavalid.js");
$.Amount = require("/res/scripts/mod/global/amount.js");
var PATH = $.Env.CGI_HOST+"cgi-bin/app/transfer/";
var CGI = {//
create_list : PATH + 'ia_transfers_create_list.cgi'//
};
var InputPage = {
title: '轉賬',
body : tpl,
openid:"",
//更新body
beforeinit:function(options){//处理数据
options = options || [{}]
$(".js-index").removeAttr("class").addClass("js-index hide-mod-pop-loading input");
this.openid = options[0]["openid"];
},
init : function($view,options){
this.render(options);
$.hideLoading();
this.bindEvent();
},
showTips: function(msg){
$.Tips.showTips(msg, 2, 1, {top:"15px"});
},
render : function(options){
$("#headImg").attr("src",HEAD_IMG_URL);
$("#money").attr("placeholder","每筆最高限額為HK$"+$.Amount.fen2Yuan(PAY_MAX_AMOUNT));
$("#receiver").val();
new InputExtend('del',["money","receiver"]);
new InputExtend('format',["money"]);
},
onValidFail:function(msg){
InputPage.showTips(msg);
},
checkFormRules:function(){
//配置
return {
"money":function(v){
if(!v){
return $.Lang.get("limitMoneyEmpty",'zh_cn');
}
return true;
},
"receiver":function(v){
if(!v){
return "姓名不能為空";
}
return true;
}
}
},
getNameRules:function(type){
var data = {
"money":"transfer_amount",
"receiver":"receiver_name"
};
if(type){
data = {
"transfer_amount":"money",
"receiver_name":"receiver"
};
}
return data;
},
validate:function(sucfn){
datavalid.valid("form-translate",sucfn,this.onValidFail,this.checkFormRules(),null,this.getNameRules());
},
getParams:function(){
var amount = $.trim($("#money").val());
var name = $.trim($("#receiver").val());
var params = {
"curtype":'HKD',
"transfer_amount":$.Amount.yuan2Fen(amount),
"receiver_name":name
};
return params;
},
createListResult:function(data){
$.SPA.boot('ok_page',data);
},
bindEvent:function(){
$("#createlist").bind("tap",function(e){
InputPage.validate(function(){
$.ajaxEx({
url : CGI["create_list"],
data : InputPage.getParams(),
onSuccess :InputPage.createListResult.bind(this)
});
});
});
}
}
return InputPage;
})性能优化
SPA预研

//去掉缓存的事件以及上面缓存数据
clearData:function(elems){
//去除事件
for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
$.event&&$.event.remove&&$.event.remove(elem,ZeptEvenNames);
}
}2、DOM事件 ?
性能优化
SPA预研
性能优化
3、DOM属性 ?

SPA预研
整体框架

SPA预研
开发流程

SPA预研
模块生命周期


SPA预研
案例-香港零錢包



SPA预研
案例-代码
define(function(require,exports,module){
require("/res/scripts/mod/global/spa2.0.js");
var Router = {
regRouter : function(){
//首页面
$.SPA.r({
path: 'js-index',//挂载点
animate: '', //入场动画效果
bodyClass:"rec-money",
type : 'index',
script:'',
beforeinit:function(){
//國際化title
this.title = $.Lang.get("indexTitle");
$("#balance").text(cur_type + $.Amount.fen2Yuan(balance||0));
$("#go-record").unbind($.Env.CLICK_EVENT).bind($.Env.CLICK_EVENT,function(){
$.SPA.boot('record');
});
$(".pop-loading-txt").text($.Lang.get("loading"));
//事件處理
$.Env.goWalletIndex();
$.Env.goDrawIndex();
},
afterinit:function(){
$.hideLoading();
}
});
//注册输入路由:
$.SPA.r({
path: 'detail',//挂载点
animate: '', //入场动画效果
script:'/res/scripts/app/hkwallet/balance/detail.js'
});
//注册新的路由:
$.SPA.r({
path: 'record',//挂载点
animate: '', //入场动画效果
classname: '', //自定义样式名
script:'/res/scripts/app/hkwallet/balance/recordmain.js'
});
},
init : function(tmplate){
this.regRouter();
$.SPA.startRun();
}
}
return Router;
});/**
* 列表页面
*/
define(function(require,exports,module){
$.fn.template = require("/res/scripts/mod/global/template.js");
var tpl = require("text!/hkwallet/zh_hk/balance/mod/record.shtml");
var Record = require("/res/scripts/app/hkwallet/balance/record.js");
var RecordMain = {
title: '零錢記錄',
body : tpl,
bodyClass:"rec-record",
//更新body
beforeinit:function(options){//处理数据
this.title = $.Lang.get("recordTitle");
},
getModel:function(options){
},
init : function($view,options,rr){
$("#norecord").text($.Lang.get("norecord"));
$("#busy").text($.Lang.get("busy"));
this.render(options);
},
render : function(options){
Record.init(function(){
$.hideLoading();
});
}
}
return RecordMain;
})
define(function(require, exports, module){
require("/res/scripts/mod/global/iscroll-lite.js");
$.Amount = require("/res/scripts/mod/global/amount.js");
$.Date = require("/res/scripts/mod/global/date.js");
var Flip = require("/res/scripts/mod/global/flipiscroll.js");
$.request = require("/res/scripts/mod/hkwallet/balance/ajax.js");
var _flip = null;
var _iscrollObj = null;
var Record = {
pagesize : 9, //一页数据多少
offset : 0, //查询偏移量
totalNum : 0, //总记录数
pageindex : 0, //第一页
iScrollNode : $("#iScroll"),
init : function(fn){
//初始化下拉加载
this.flipMoreRecord();
this.getRecordList(fn);
this.bindEvent();
// 发送测速统计
//$.SpeedLog.add();
//$.SpeedLog.send(5, 9);
},
//获取记录
getRecordList : function(fn){
var url = $.Env.CGI_PRE + 'ewallet_querybalance_list.cgi';
var data = {
offset : Record.offset,
limit : Record.pagesize //多查一条用于翻页
}
//取缴费记录成功处理
var onSuc = function(data){
var array = data.list || [];
Record.totalNum = array.length;
array = array.slice(0, Record.pagesize);
if(Record.totalNum < 1){
$("#record .no-record").removeClass("hide");
Record.iScrollNode.addClass('hide');
return false;
}
Record.showRecordList(array);
}
var beforeSend = function(){
// 上锁
_flip.setLock(true);
}
var complete = function(data){
_flip.setLock(false);
fn&&fn();
}
var onFailure = function(data){
$("#record .busy").removeClass("hide");
return false;//不显示提示框
}
//请求配置信息
var cfg = {
url : url,
data : data,
onSuccess : onSuc,
beforeSend : beforeSend,
complete : complete,
onFailure:onFailure
}
$.request.ajax(cfg);
},
//展示记录列表
showRecordList : function(data){
var dataArr = [];
//更新數據
$.each(data,function(k,v){
v.type_class = v.type == 1 ? "income":"";
dataArr.push(v);
})
var tpl = $("#tpl_list").html();
var html = $.format(tpl,dataArr,null,{
memo:function(value,key,index,recodes){
return $.filterScript(value);
},
trade_time:function(value,key,index,recodes){
return value;
},
paynum:function(value,key,index,recodes){
return $.Amount.fen2Yuan(value||0);
},
cur_type:function(value,key,index,recodes){
return $.filterScript(cur_type);
}
});
$("#record-list").append(html);
Record.offset += Record.pagesize;
if (Record.onFlipCheck()) {
_flip.showUpState();
} else {
_flip.setVisible();
}
_iscrollObj.refresh();
},
//绑定事件
bindEvent : function(){
$("#record-list").off("tap").on("tap","li",function(e){
var target = $(e.currentTarget);
$.SPA.boot('detail',{key:target.attr("key")});
});
},
//滑动加载更多初始化
flipMoreRecord : function(){
_iscrollObj = $.browser.ie ? null : new iScroll("iscroll");
_flip = new Flip("iscroll", _iscrollObj, {
"check":Record.onFlipCheck,
"end": Record.onFlipEnd
});
},
//滑动加载,判断是否还有记录可拉取
onFlipCheck : function(){
return Record.totalNum > Record.offset;
},
//滑动结束,加载数据
onFlipEnd : function(){
Record.getRecordList();
}
}
return Record;
});/**
* 付款页面逻辑
*/
define(function(require,exports,module){
$.fn.template = require("/res/scripts/mod/global/template.js");
var tpl = require("text!/hkwallet/zh_hk/balance/mod/detail.shtml");
var CGI = {//
query : $.Env.CGI_PRE + 'ewallet_querybalance_detail.cgi'//
};
var Detail = {
title: '零錢详情',
body : tpl,
bodyClass:'rec-record-detail',
//更新body
beforeinit:function(options){//处理数据
this.initModel(options);
this.title = $.Lang.get("detailTitle");
},
initModel:function(options){
this.model = options&&options[0];
},
init : function($view,options,rr){
this.render(options);
},
render : function(options){
this.queryDetail();
},
queryDetail:function(){
var data = {
bkid:this.model["key"]
};
$.ajaxEx({
url : CGI["query"],
data : data,
onSuccess :Detail.queryDetailResult.bind(this)
});
},
//增加多语言配置
fixData:function(data){
var type = $.Env.STATUS[data.type],
sincoming = $.Lang.get(type);
data = $.extend(data,{
balance : $.Amount.fen2Yuan(data.balance),
paynum : $.Amount.fen2Yuan(data.paynum),
sincoming : sincoming + $.Lang.get("money"),
stype : $.Lang.get("type"),
type : sincoming,
stime : $.Lang.get("time"),
stransaction : $.Lang.get("transaction"),
snote : $.Lang.get("note"),
paynumClass : data.type == 1 ? "income" : ""
})
return data;
},
queryDetailResult:function(data){
var tmpl = $("#tmp-detail").template();
var html = tmpl(this.fixData(data));
$(".js-detail").html(html);
$.hideLoading();
}
}
return Detail;
})SPA预研
优缺点
优点:
5、功能配置化
2、模块级存储
1、体验流畅化
6、减少cgi模板化
4、维护轻量化
3、资源增量化
SPA预研
优缺点
缺点:
1、基于dust.js
2、无模块切换动画
3、无css片段化处理
4、基于配置化非规则化
5、CDN解决不了,依赖的依赖
SPA预研
规避问题
1、window上注册变量
2、基于seajs,保存在window中,每次须初始化变量
SPA预研
疑问解答

SPA预研
By lowinwu
SPA预研
spa单页面方案
- 1,350