tourjoin / node

2 stars 3 forks source link

MVVM计划 #3

Open tourjoin opened 8 years ago

tourjoin commented 8 years ago

说明

项目中使用到的变量我不做解释,大家可以查阅相关文档,我所有项目的地址也都是给出alpha的测试地址,不会出现正式地址,而部分地址也会根据项目进展而变更,如有疑问请及时联系我更新 找了几个地址提供大家初步了解mvvm模式 http://blog.csdn.net/pkxiuluo01/article/details/49383783 http://www.cnblogs.com/linzheng/archive/2011/03/12/1981960.html http://www.cnblogs.com/guwei4037/p/5591183.html

目录

  1. 目录结构

  2. 路由规则

3.通用scope

演示地址:

http://drivenode.alpha.zlvyun.com/4/v1/ http://drivenode.alpha.zlvyun.com/4/v1/#/card/index.html

tourjoin commented 8 years ago

目录结构

/model 后端模块,node相关的程序都放到这里
    │/apijs 与后端接口交互的模块
    └/commonjs 通用函数
/static 静态文件
    │/css 样式目录
    │ └/common 通用样式
    │/fonts 字体
    │/images 图片库
    │ │/common 通用图片
    │ └/weui 微信的ui图片
    └/js js脚本
        └/common 通用脚本
/view 模版
    │/应用目录
tourjoin commented 8 years ago

路由规则

静态文件

使用express组件实现路由规则

var express=require('express'); 

定义静态文件目录,node访问该目录内的文件不会经过其他操作直接调用输出

app.use('/static',express.static(__dirname+'/static'));

如:http://drivenode.alpha.zlvyun.com/static/images/common/logo.png

加载模块

用于node操作的比如,数据库、接口交互、缓存等模块,模块中的参数必须使用post进行传递

app.use("/model", function(request, response, next) {
    // 解析请求,包括文件名
    var urlParse=url.parse(request.url);
    var memberId=request.params[0];
    var fileName="/model"+urlParse.pathname;
    var ext=(urlParse.pathname).match(/(\.[^.]+|)$/)[0];
    if(fileName.charAt(fileName.length-1)!="/"&&ext==''){
        fileName+='/';
    }
    if(ext == ''){
        ext='.js';
        fileName+='index.js'; 
    }
    request.setEncoding('utf-8');
    var postData=""; //POST & GET : name=zzl&email=zzl@sina.com
    // 数据块接收中
    request.addListener("data", function (postDataChunk) {
        postData += postDataChunk;
    });
    // 数据接收完毕,执行回调函数
    request.addListener("end", function () {
        require(__dirname+fileName).run(centerUrl,apiUrl,postData,response);
    });
});

获取年卡列表的案例如:

$.post('/model/api.js',{
    'controller':'card',
    'action':'index',
    'memberId':'1',
    'option':'cardList'
},function(){
    //获取年卡后的回调
});

加载view模版

目前我只实现了三级目录,暂时够用了,稍后更新多级目录,定义v1的目录主要是与老项目区别开来,不会因为有新的规则之后导致老项目无法使用,v1代表了我们走向mvvm化的第一个版本,如果后面版本升级将会出现v1.1、v2等各种版本号,而不同的版本号也会对应到不同的viwe层,目前v1版本的试图层目录先命名为view

app.use(/^\/(\d+)\/v1(.*?)$/,function(request, response, next) {
    // 解析请求,包括文件名
    var urlParse=url.parse(request.url);
    var memberId=request.params[0];
    // var fileName="/view"+urlParse.pathname;
    var fileName="/view"+request.params[1];
    var ext=(request.params[1]).match(/(\.[^.]+|)$/)[0];
    if(fileName.charAt(fileName.length-1)!="/"&&ext==''){
        fileName+='/';
    }
    if(ext == ''){
        ext='.html';
        fileName+='index.html'; 
    }
    try{
        switch (ext) {
            case ".png":
            case ".jpg":
            case ".gif":
            case ".ico":
                fs.readFile(__dirname+fileName,'binary',function(err, data) {
                    if (err) {
                        console.log(err);
                        response.writeHead(404, {'Content-Type': 'text/html;charset=utf-8'});
                    }else{
                        if(fileName.match('.png')){
                            response.writeHead(200, {'Content-Type':'image/png'});
                        }else if(fileName.match('.gif')){
                            response.writeHead(200, {'Content-Type':'image/gif'});
                        }else if(fileName.match('.ico')){
                            response.writeHead(200, {'Content-Type':'image/x-icon'});
                        }else{
                            response.writeHead(200, {'Content-Type':'image/jpeg'});
                        }
                        response.write(data,'binary');
                        response.end();
                    }
                });
            break;
            default:
                fs.readFile(__dirname+fileName,'utf-8',function (err, data) {
                    if (err) {
                        console.log(__dirname+fileName+':'+err);
                        // HTTP 状态码: 404 : NOT FOUND
                        // Content Type: text/plain
                        response.writeHead(404, {'Content-Type': 'text/html;charset=utf-8'});
                    }else{
                        // HTTP 状态码: 200 : OK
                        // Content Type: text/plain
                        response.writeHead(200, {'Content-Type': 'text/html;charset=utf-8'});
                        var dataHtml=data.toString();
                        dataHtml=dataHtml.replace(new RegExp("{{apiUrl}}","g"),apiUrl);
                        dataHtml=dataHtml.replace(new RegExp("{{styleUrl}}","g"),styleUrl);
                        dataHtml=dataHtml.replace(new RegExp("{{memberId}}","g"),memberId);
                        // 响应文件内容
                        response.write(dataHtml);
                    }
                    //  发送响应数据
                    response.end();
                });
            break;
        }
    }catch(e){
        console.log('error..not:'+__dirname+fileName);
        response.end();
    }
});

防止模版文件中有些项目出现图片等静态文件,所以也进行了一次分析处理,其中apiUrl、styleUrl、memberId等变量进行了统一的替换,防止beta、alpha、rc、release等环境出现大量的开发地址 http://drivenode.alpha.zlvyun.com/4/v1/index.html 可以访问到viwe目录中的index.html文件

老项目兼容

老项目过去的就让他过去吧,不要纠结,IT的步伐是在前进的,所以我懒的实在不想做解释

app.use(/^\/(\d+)(.*?)$/,function(request, response, next) {
...
}
tourjoin commented 8 years ago

api模块

api模块主要位于model目录中(参考【目录结构】)主要作用就是负责与drive的接口通讯,但是从1.0版本开始我们升级了数据库交互的功能,但是为了安全考虑目前只开放了读接口并未开放写接口,所以写数据的权限还是要经过drive的接口去完成,下面我们就讲解一下这个模块怎么完成工作的 所有model中的模块都是依赖了路由规则(参考【路由规则】)

//路由规则中model的调用方法
require(__dirname+fileName).run(centerUrl,apiUrl,postData,response);

可以看出所有model中的模块都得用run来执行那么就定死了model的写法,定死的未必是坏的,但是我们的目标是什么?别墅、豪车、美女都要有,所以我认为这个定死的规则是可以慢慢改进的,但是现在先有自行车也是可以的 那么问题来了,我们都是知道express加载进来的文件最好是通过exports来定义,所以类我们又一次在玩弄美女之前先被强奸一次,但是我觉得这个代价还是值得,所以我索性享受了这次强奸

exports.run=function (centerUrl,apiUrl,postData,response) {
    //model具体要做的内容
}

回归api模块,crypto、querystring都是node默认有的,所以无需安装直接用,唯一要提的是common.js

var common=require(__dirname+'/common.js').common();

这个是我自己写的一部分我觉得需要用的函数稍后补文档 api的目的就是通过将获得的参数加密成接口识别的数据然后跟接口拿数据,不可能任何人跟我要数据我就给吧,当我什么人,那肯定要是同一个帮会的人我才能告诉你我的内幕消息呀,那要知道是你是跟我一个帮会的,那么我就要用个暗号sing来证明了,sing是各种参数按规则排序后窜连进行md5然后加上密钥后再次md5获得

var query=common.http_build_query(params);
var md5=crypto.createHash('md5');
md5.update(query);
query=md5.digest('hex');
md5=crypto.createHash('md5');
md5.update(query + appKey);
var sing=md5.digest('hex');

require('request').post({
    'url': apiUrl + '/api/?sign=' + sing,
    'form': params,
    'encoding': 'utf8'
}, function (error, rp, body) {
    if (rp.statusCode == 200) {
        response.writeHead(200, {'Content-Type': 'text/plain;charset=utf-8'});
        response.write(body);
        response.end();
    } else {
        console.log(rp.statusCode);
    }
});

验证了天王盖地虎,宝塔镇河妖以后数据就会通过接口回传过来 那么问题又来了,api去对接接口的时候有签名,但是我们之前路由规则里提过

$.post('/model/api.js',{
    'controller':'card',
    'action':'index',
    'memberId':'1',
    'option':'cardList'
},function(){
    //获取年卡后的回调
});

这里可没有安全措施,安全套也不是百分百不漏的,既然要灵活部分安全性的牺牲我们必须接受的,所以前端的重要责任就是怎么在一堆的get、post中确保与api的交互不被坏蜀黍利用,关于这点我会在v1.1的版本里升级一个新签名用于跟api交互,但是就算我加了这个新签名也是被暴露在js文件中的,坏蜀黍依然可以在长期猥亵后获得签名方案依然能通过post和get来勾引api,所以前端的同学是否感觉到你们的生活不止切图和特效那点苟且了? 好了再次回归正题,我说了提供了一组交互数据库的方案,程序猿都知道我们的数据库不是真的数据库,是通过了center的接口来交互数据的,那么既然前端安全隐患依然存在的今天,我们在js中裸奔就是危险的,所以我生产了一个AV环境,只让你看,不让你摸

$.post('/model/api.js',{
    'module':'api',
    'controller':'index',
    'action':'get',//one是一条数据、get是不限制读取数据(最多只能获得1000条)
    'tableId':'表名',
    'where':'查询条件',//这个需要开课题,在我开课之前先学着跟我们的程序猿哥哥们沟通,让他们教你们
    'page':'0',//limit中的当前页
    'limit':'10',//limit中的一页多少条
    'order':'',//排序规则
    'group',//要不要按组检索
},function(){
    //获取数据库后的回调
});

写完这些我感觉我心理非常慌张,虽然我做了很多安全防止,甚至在接口处禁止了join等sql方法,但是我们的前端同学说实话,黄色小本可能看的比较多(下次找个机会大家一起探讨一下),但是数据库的书籍看的还是比较少的,天知道你们会传什么!!! 我觉得我需要每天上班前先去灵隐烧柱香!

tourjoin commented 8 years ago

view的初衷

首先,安生跟我报怨过老版本的路由方式井号导致了很多奇怪的问题,我想他是七月待多了,我个人认为目前已经有不少应用都已经转过来用井号作为路由规则了,特别是我们当神一样的谷歌公司都在用ng来实现单页应用,我们为什么就就会有问题呢?只能说是我们的解决思路还是太死板,所以在新版本的view中我依然沿用了井号作为路由规则,但是我取消了mui等一些插件,确实我们对mui的了解还是不足,虽然mui是很好的开发插件,但是在设计、前端、开发全部都团队分离的情况下,还是很难用好,所以v1版本中我取消了mui,加入了angularjs,但是我暂时没有取消jquery,愿意很简单ng与dom交互的,jquery是操作dom的,我们的应用大量的用到了dom操作所以jquery的保留是非常必要的。 好的mvvm化之后其实能做到每个小功能模块都独立存在,用户页面需要用到什么模块就加载什么模块进入页面就好了

入口文件index.html

那么我们先来看看index.html的代码

<!DOCTYPE html>
<html class="no-js" lang="zh-cn" data-ng-app="mainApp" data-ng-controller="appCtrl">
<head>
    ...
    <title>{{window.title}}</title>
    <link rel="stylesheet" type="text/css" href="/static/css/common/bootstrap.min.css">
    <link rel="stylesheet" type="text/css" href="/static/css/common/weui.min.css">
    <script type="text/javascript" src="https://3gimg.qq.com/lightmap/components/geolocation/geolocation.min.js"></script>
</head>
<body>
<div ui-view='content' class="js_container">
    <section data-ng-view="" class="view-container"></section>
</div>
...
<footer data-ng-cloak class="ng-cloak" data-ng-show="window.footer">
    <p style="text-align:center;color:#ccc;">
        {{window.copy}}&copy;<span onclick="window.location='http://tujikeji.com/tj.html'">途记科技技术支持</span>
    </p>
</footer>
<script type="text/javascript" src="/static/js/common/jquery.min.js"></script>
<script type="text/javascript" src="/static/js/common/bootstrap.min.js"></script>
<script type="text/javascript" src="/static/js/common/angular.min.js"></script>
<script type="text/javascript" src="/static/js/common/angular-route.min.js"></script>
<script type="text/javascript" src="/static/js/common/ngTouch.js"></script>
<script type="text/javascript" src="/static/js/common/zly.min.js"></script>
<script type="text/javascript" src="/static/js/common/weixin.min.js"></script>
<script type="text/javascript">
var urlEncode = zly.urlencode((window.location.href).replace(window.location.hash, ""));
$.getScript("{{apiUrl}}/api/event/jssdk/?memberId={{memberId}}&url=" + urlEncode);
</script>
</body>
</html>

哇!典型的ng风格,只有两句js是直接写在页面上的了,作用我稍微解释一下,为了将来进入我们团队的同学做解释,其实这两句js是获取微信jssdk的,用来做什么我就不说了,不知道的请抄一遍微信wiki

//获得当前链接生成urledncode码
var urlEncode = zly.urlencode((window.location.href).replace(window.location.hash, ""));
//去拿微信的jssdk
$.getScript("{{apiUrl}}/api/event/jssdk/?memberId={{memberId}}&url=" + urlEncode);

这里有几个之前眼熟的东西登场了{{apiUrl}}drive接口的地址、{{memberId}}当前客户的编号

涉及到的资源

bootstrap我不打算讲,自己看文档 weui我也不打算讲,自己看微信网页开发样式库 jquery我倒是想讲,但是我觉得我讲不了:)自己看吧jquery官网 angular这个比较牛,我估计要请谷歌的同学来讲,所以等我有同学进谷歌以后我会邀请来的,在我同学进谷歌之前大家自己先看文档 angular-route这个是angular的一个路由组件,稍后我会涉及到,推酷有个人讲的还可以推酷 ngTouch这个还在犹豫是否使用,目前先加载进来了,但是实际没有使用到,这个到时候大家讨论是否要移除毕竟原生的写法也不难,而且我们还用了jq更是简单 zly我自己封装的ng配置,稍后单独讲 weixin我自己封装的微信配置,稍后单独讲

zly.min.js

前半部分有一点像常用函数,将来使用比较频繁的函数都整理到这里来,如果这个文件过大可以分成多个common.js的文件来存放函数。 先来讲讲基础变量

$document:$(document),//jq将页面的document赋值给zjy.$document
$window:$(window),//jq将页面的window赋值给zjy.$window
$title:$("title"),//jq将页面的title赋值给zjy.$title
$body:$('body'),//jq将页面的body赋值给zjy.$body
$footer:$('footer'),//jq将页面的footer赋值给zjy.$footer
ua:window.navigator.userAgent.toLowerCase(),//浏览器基础信息
appCtrl:{
    $scope:null,
    $location:null
},//解释在下面
scope:{},//解释在下面

appCtrl:是来源index.html中就已经定义了,未来的应用因为灵活多变,我不确定前端的同学是否光靠ng就能解决问题,所以在appCtrl创建后ng把$scope、$location赋值给了zly.appCtrl中,这样及时前端开发脱离了当前的appCtrl也能通过变量唤起appCtrl里的交互 scope:既然要mvvm作为目标,每个view和viewmodel都可能有属于自己的controller,但是模块是独立的所以无法避免A的controller要用B的controller进行交互,所以每一个appCtrl除外的controller创建后ng把$scope赋值给了zly.scope中,这样前端在任何模块都能通过zly.scope.xxxCtrl去唤起交互

urlencode:function(str){
    ...//url的encode编码
}
zly.urlencode('http://xxx.com/?a=参数');
//返回:http%3a%2f%2fxxx.com%2f%3fa%3d%e5%8f%82%e6%95%b0

urldecode:function(str,_space){
    ...//url的decode解码
}
zly.urldecode('http%3a%2f%2fxxx.com%2f%3fa%3d%e5%8f%82%e6%95%b0');
//返回:http://xxx.com/?a=参数

is_array:function (mixedVar) {
    ...//是否数组
}
zly.is_array(['a','b','c']);
//返回:true

empty:function (str) {
    ...//str是否为空或者是否存在
}
zly.empty('');
//返回:false

getUrlParam:function(name){
    ...//获取url中的参数
}
http://drivenode.alpha.zlvyun.com/1/v1/#/index.html?a=123
zly.getUrlParam('a');
//返回:123

getStorage:function(_name){
    ...//获取Storage
}
setStorage:function(_name,_value){
    ...//设置Storage
}
removeStorage:function(_name){
    ...//删除Storage
}
//window.localStorage的使用请自己查阅资料

oauth:function(_apiUrl,_memberId,_scope){
    ...//获取授权
}
zly.oauth('{{apiUrl}}','{{memberId}}','snsapi_userinfo');//显性授权,获得用户名和头像
zly.oauth('{{apiUrl}}','{{memberId}}');//隐性授权,不获得用户名和头像

有些函数不够用我觉得可以学习程序猿思维讲php函数转化为公用函数php转js

ng的部分稍后单独补

tourjoin commented 8 years ago

viewmodel的初衷

用年卡举例,在此前的版本我们是通过/1/#/card/xinyu/index.html来访问新余的年卡,宿迁则是#/card/suqian/index.html,这样的方案优点是view分离,新余和宿迁可以完全长的不一样,但是缺点也很明显,宿迁升级,新余就很尴尬!!!这个痛做年卡的同学知道。 那么接下来我们用mvvm的逻辑怎么解决这个问题呢? 我们drive的项目绝大部分都是能够在view框架上统一的,所以我先说第一种大框架统一的方案:

index.html

地址:#/card/index.html

<!-- 年卡首页也独立成为一个controller,这样可以提供任何地方调用,当然我明白首页是不会被任何地方调用了 -->
<div class="bd" data-ng-cloak data-ng-controller="cardIndexCtrl">
    <!-- 页面首次加载时的欢迎页 -->
    <div data-ng-show="indexIni.bg_style.opacity>0" data-ng-style="indexIni.bg_style"></div>
    <!-- 用户卡包为空时首页显示在售年卡 -->
    <div data-ng-if="indexIni.list=='card'">
        <div data-ng-include="'/{{memberId}}/v1/card/card_list.html'"></div>
    </div>
    <!-- 用户卡包不为空时首页显示开包列表 -->
    <div data-ng-if="indexIni.list=='package'">
        <div data-ng-include="'/{{memberId}}/v1/card/package_list.html'"></div>
    </div>
</div>
<!-- 加载年卡首页的module -->
<script type="text/javascript" src="/{{memberId}}/v1/card/module/index.js"></script>

所有年卡均使用统一的首页view,但是可以根据不同的情况include不同的模块

index.js

地址:#/card/module/index.js

//注册一个ng-controller的controller:cardIndexCtrl
angular.module("mainApp").register.controller("cardIndexCtrl", function ($scope,$document,$timeout,$http,$location) {
    //获取授权
    zly.oauth("{{apiUrl}}","{{memberId}}");
    //将$scope赋值给全局函数zly.scope
    zly.scope.cardIndexCtrl=$scope;
    //cardIndexCtrl中的model;
    $scope.indexIni={
        //index.html里的ng-style会自动加载indexIni.bg_style作为样式
        'bg_style':{
            'width':'100%',
            'height':zly.$window.height()+'px',
            'position':'absolute',
            'top':'0px',
            'left':'0px',
            'background':"url('/static/images/common/bg.jpeg')",
            'background-color':'#000',
            'background-repeat':'no-repeat',
            'background-attachment':'fixed',
            'background-position':'center',
            'background-size':'contain',
            'z-index':'9999',
            'filter':'alpha(opacity='+(zly.getStorage('cardBegHide') === false ? 100 : 0).toString()+')',
            '-moz-opacity':zly.getStorage('cardBegHide') === false ? 1 : 0,
            '-khtml-opacity':zly.getStorage('cardBegHide') === false ? 1 : 0,
            'opacity':zly.getStorage('cardBegHide') === false ? 1 : 0
        },
        //index.html里的ng-if会根据list决定最后显示在售年卡还是我的卡包
        'list':'card'
    };
    //通过api数据交互获得当前用户是否有卡包存在,接口参数详看对应文档
    $scope.indexStyle=function(){
        $scope.api({
            'controller':'card',
            'action':'user',
            'memberId':'{{memberId}}',
            'oauthToken': zly.getStorage("{{memberId}}_oauthToken"),
            'ajax':true,
            'option':'getList'
        },function(_data){
            $scope.alert("数据调用失败","可能是网络不稳定失去与服务器的连接",null);
        },function(_data){
            if(typeof(_data)!= 'undefined' && _data['code']==true){
                //接口回调
                var $data=_data['data'];
                //设置客户微信的logo作为年卡欢迎页的图片,稍后要通过其他接口重新获得对应图片
                $scope.indexIni.bg_style.background="url('"+$data['interface']['logo']+"')";
                //如用户年卡为空就显示在售年卡,如果不为空就显示用户卡包
                if($data['userList'].length > 0){
                    $scope.indexIni.list='package';
                }else{
                    $scope.indexIni.list='card';
                }
                //2秒后欢迎页淡出
                $timeout(function(){
                    $scope.indexBgHide();
                },2000);
            }
        });
    };
    $scope.indexBgHide=function(){
        if($scope.indexIni.bg_style.opacity > 0){
            $scope.indexIni.bg_style.filter='alpha(opacity='+($scope.indexIni.bg_style.opacity*10-10).toString()+')';
            $scope.indexIni['bg_style']['-moz-opacity']-=0.05;
            $scope.indexIni['bg_style']['-khtml-opacity']-=0.02;
            $scope.indexIni.bg_style.opacity-=0.05;
            $timeout(function(){
                $scope.indexBgHide();
            },100);
        }else{
            zly.setStorage('cardBegHide',true);
            $scope.indexIni.bg_style.filter='alpha(opacity=0)';
            $scope.indexIni['bg_style']['-moz-opacity']=0;
            $scope.indexIni['bg_style']['-khtml-opacity']=0;
            $scope.indexIni.bg_style.opacity=0;
        }
    };
    $scope.indexStyle();
});

这里面又有一个model的概念,这个属于ng的model与mvvm中的model还是有所不同,但是框架确实利用了ng的model来完成其中的一部分model,这个概念稍后再解释,先挖个坑,记得提醒我填坑。

card_list.html

地址:#/card/card_list.html

<!-- 年卡列表独立成为一个controller,这样可以提供任何地方调用,当然我明白首页是不会被任何地方调用了 -->
<div class="weui_panel_bd" data-ng-controller="cardListCtrl">
    <!-- ng部分我不解释了,请查阅ng手册 -->
    <div class="weui_cells_tips" data-ng-if="data.quantity == 0">暂无在售年卡</div>
    <div class="modal-dialog" data-ng-repeat="v in data.list" data-ng-show="data.quantity > 0">
        <div class="modal-content">
            <div class="modal-header" data-ng-if="v.card_display==''">
                <h4 class="modal-title">{{v.name}}</h4>
            </div>
            <div class="modal-body" data-ng-if="v.card_display!=''" style="padding:0 !important;">
                <div style="background:url({{v.card_display}}) no-repeat center;background-size:cover;width:100%;height:150px;border-top-left-radius:0.35em;border-top-right-radius:0.35em;"></div>
                <div style="opacity:0.6;filter:alpha(opacity=60);position:absolute;width:100%;left:0;bottom:0;background:#000;color:#FFF;"><h4>{{v.name}}</h4></div>
            </div>
            <div class="modal-body">
                <p>可用景区:</p>
                <p>有效期:1年</p>
                <p>价格:<span class="label label-success">&yen;{{v.money}}元</span>&nbsp;<small><del>&yen;{{v.free}}元</del></small></p>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-success" data-ng-click="gotoCardDetail(v.id)"><span class="badge badge-success">&yen;{{v.money}}元</span>&nbsp;购买年卡</button>
            </div>
        </div>
    </div>
</div>
<!-- 加载底部模块,这个就是viewmodel的魅力 -->
<div data-ng-include="'/{{memberId}}/v1/card/bottom.html'"></div>
<!-- 加载在售年卡列表的module -->
<script type="text/javascript" src="/{{memberId}}/v1/card/module/card_list.js"></script>

card_list.html的页面可以独立被访问也能被index.html包含,但是功能是独立的,这样一来就完成了一个独立的viewmodel,这样card_list.html即可自己独立被访问,也能被挂载到任何页面,同理页面中又一个ng-include,加载了bottom.html的底部模块,这个底部模块也是独立存在独立的viewmodel,可以被任何地方加载

card_list.js

地址:#/card/module/card_list.js

angular.module("mainApp").register.controller("cardListCtrl", function ($scope,$document,$timeout,$http,$location) {
    ...//这个也不用解释了,多看ng就能了解
});

只有一点是要注意的,card_list.html中的data-ng-controller="cardListCtrl"要与module中的cardListCtrl,其实我偷换了viewmodel的概念,但是为了能方便使用我将view中的html加上module里的js配合完成了一个viewmodel,就算当view部分需要定制只要功能不变都能不改写model形成一个行的viewmodel。 开发过程,特别是苦逼的客户为主的开发项目最苦逼的就是前端和程序,在我们实现前后端分离之前,最苦逼的是猿,才是前端,现在我们前后端已经分离了,该前端的同学来思考如何在被强奸时享受。 简单举例开始:假设新余的客户不希望通过用户是否有年卡来判断显示什么页面,他就希望显示他定制好的xinyu首页,设:新余首页地址:/card/xinyu/index.html 修改#/card/index.html

//增加一个include
<div data-ng-if="indexIni.list=='xinyu'">
    <div data-ng-include="'/{{memberId}}/v1/card/xinyu/card_list.html'"></div>
</div>

修改#/card/module/index.js

$scope.indexStyle=function(){
    $scope.api({
        'controller':'card',
        'action':'user',
        'memberId':'{{memberId}}',
        'oauthToken': zly.getStorage("{{memberId}}_oauthToken"),
        'ajax':true,
        'option':'getList'
    },function(_data){
        $scope.alert("数据调用失败","可能是网络不稳定失去与服务器的连接",null);
    },function(_data){
        if(typeof(_data)!= 'undefined' && _data['code']==true){
            var $data=_data['data'];
            $scope.indexIni.bg_style.background="url('"+$data['interface']['logo']+"')";
            if('{{memberId}}' == '新余编号'){
                $scope.indexIni.list='xinyu';
            }else{
                if($data['userList'].length > 0){
                    $scope.indexIni.list='package';
                }else{
                    $scope.indexIni.list='card';
                }
            }
            $timeout(function(){
                $scope.indexBgHide();
            },2000);
        }
    });
};

可以看出这个方案虽然解决了客户问题,但是多了if判断,这个方法不是非常好的,如果我们鞥跟程序猿好好沟通,通过接口返回一个客户模版(其实这个模版字段在后端程序真的有)$scope.indexIni.list=$data['interface']['templatename']; 那么新的问题又来了,如果客户要的首页就是个奇怪的页面里面包含了年卡列表也包括了卡包列表,举例: 修改#/card/xinyu/index.html

<div data-ng-include="'/{{memberId}}/v1/card/card_list.html'"></div>
<div data-ng-include="'/{{memberId}}/v1/card/package_list.html'"></div>

完成,很快,试想如果我们前端把所有所有的功能都变成一个一个一个一个又一个的小模块,最后是不是页面只是组装而已?当然这个是理想,但是前端为什么而存在?为了那个极致的理想,按现在的viewmodel来看我们不是做不到哦,只是要用心而已!

tourjoin commented 7 years ago

通用scope

通用的scope放在 /static/js/common/zly.min.js 文件中

$scope.window={
    'config':{ 
        ... 页面的基础配置
    },
    'toast':{
        ... 提示信息
    },
    'loading':{
        ... 加载信息
    },
    'alert':{
        ... alert弹出提示信息
    },
    'confirm':{
        ... confirm弹出提示信息
    }
};
$scope.api=function(_data,_error,_success){
    ... 唤醒api接口
    ... _data:提交的数据
    ... _error:接口错误回调
    ... _success:接口返回后的回调
};
$scope.get=function(_url,_data,_error,_success){
    ... get页面
    ... _url:要加载的页面
    ... _data:提交的数据
    ... _error:获取失败后的回调
    ... _success:获得页面后的回调
};
$scope.post=function(_url,_data,_error,_success){
    ... post页面
    ... _url:要加载的页面
    ... _data:提交的数据
    ... _error:获取失败后的回调
    ... _success:获得页面后的回调
};
$scope.setShare=function(_title,_content,_img,_timeLineLink,_callback){
    ... 分享信息
    ... _title:分享标题
    ... _content:分享内容
    ... _img:分享图片
    ... _timeLineLink:分享链接
    ... _callback:分享后的回调
};
$scope.toast=function(_text,_time){
    ... 提示
    ... _text:提示文字
    ... _time:提示显示的时间
};
$scope.loading=function(_text){
    ... 加载中
    ... _text:加载中的提示文字
};
$scope.goto=function(b,c){
    ... 跳转
    ... b:目标链接
    ... c:url中的参数
};
$scope.alert=function(_title,_content,_ok) {
    ... alert提示
    ... _title:标题
    ... _content:提示内容
    ... _ok:[确定键的名称,点击后的事件]
};
$scope.confirm=function(_title,_content,_ok,_cancel) {
    ... confirm提示
    ... _title:标题
    ... _content:提示内容
    ... _ok:[确定键的名称,点击后的事件]
    ... _cancel:[取消键的名称,点击后的事件]
};
$scope.getLocation=function(_success,_fail) {
    ... 获取当前坐标
    ... _success:成功后的回调
    ... _fail:失败后的回调
};
tourjoin commented 7 years ago

通用scope

通用的scope放在 /static/js/common/zly.min.js 文件中

$scope.window={
    'config':{ 
        ... 页面的基础配置
    },
    'toast':{
        ... 提示信息
    },
    'loading':{
        ... 加载信息
    },
    'alert':{
        ... alert弹出提示信息
    },
    'confirm':{
        ... confirm弹出提示信息
    }
};
$scope.api=function(_data,_error,_success){
    ... 唤醒api接口
    ... _data:提交的数据
    ... _error:接口错误回调
    ... _success:接口返回后的回调
};
$scope.get=function(_url,_data,_error,_success){
    ... get页面
    ... _url:要加载的页面
    ... _data:提交的数据
    ... _error:获取失败后的回调
    ... _success:获得页面后的回调
};
$scope.post=function(_url,_data,_error,_success){
    ... post页面
    ... _url:要加载的页面
    ... _data:提交的数据
    ... _error:获取失败后的回调
    ... _success:获得页面后的回调
};
$scope.setShare=function(_title,_content,_img,_timeLineLink,_callback){
    ... 分享信息
    ... _title:分享标题
    ... _content:分享内容
    ... _img:分享图片
    ... _timeLineLink:分享链接
    ... _callback:分享后的回调
};
$scope.toast=function(_text,_time){
    ... 提示
    ... _text:提示文字
    ... _time:提示显示的时间
};
$scope.loading=function(_text){
    ... 加载中
    ... _text:加载中的提示文字
};
$scope.goto=function(b,c){
    ... 跳转
    ... b:目标链接
    ... c:url中的参数
};
$scope.alert=function(_title,_content,_ok) {
    ... alert提示
    ... _title:标题
    ... _content:提示内容
    ... _ok:[确定键的名称,点击后的事件]
};
$scope.confirm=function(_title,_content,_ok,_cancel) {
    ... confirm提示
    ... _title:标题
    ... _content:提示内容
    ... _ok:[确定键的名称,点击后的事件]
    ... _cancel:[取消键的名称,点击后的事件]
};
$scope.getLocation=function(_success,_fail) {
    ... 获取当前坐标
    ... _success:成功后的回调
    ... _fail:失败后的回调
};
tourjoin commented 7 years ago

模块化尝试

从接口获取当前页面的配置信息

<link rel="stylesheet" type="text/css" data-ng-repeat="v in config.css" data-ng-href="{{v}}">
<div class="weui_panel_bd" data-ng-cloak data-ng-controller="testCtrl">
    <data-ng-include data-ng-repeat="v in includes" data-src="v.url" onload="onload(v.load)"></data-ng-include>
</div>
<script type="text/javascript">
angular.module("mainApp").register.controller("testCtrl",function ($scope,$document,$timeout,$http,$location) {
    zly.oauth("{{apiUrl}}","{{memberId}}");
    zly.scope.testCtrl=$scope;
    $scope.config.css.bootstrap='/static/css/common/bootstrap.min.css';
    $scope.includes={};
    $scope.init=function(){
        $scope.api({
            'controller':'index',
            'action':'test',
            'memberId':'{{memberId}}',
            'oauthToken': zly.getStorage("{{memberId}}_oauthToken"),
            'ajax':true,
            'option':'test'
        },function(_data){
            $scope.alert("数据调用失败","可能是网络不稳定失去与服务器的连接,刷新重试",['刷新',function(){
                window.location.reload();
            }]);
        },function(_data){
            if(typeof(_data)!= 'undefined' && _data['code']==true){
                ... 从接口获得当前页面的配置
            }
        });
    };
    $scope.onload=function(_code){
        ... 模块加载完成后的事件
    };
    $scope.init();
});
</script>

获得页面配置信息后分享处理

$data=_data.data;
$scope.setShare($data['title'],$data['content'],$data['img'],false,false);

获得页面配置信息后动态加载模块

$scope.includes=$data.includes;
//设
$data.includes={
    '0':{
        'load':'index_background',
        'url':'/{{memberId}}/v1/card/module/index_background.html'
    },
    '1':{
        'load':'card_apply',
        'url':'/{{memberId}}/v1/card/module/buyCard/apply.html'
    },
    '2':{
        'load':'bottom',
        'url':'/{{memberId}}/v1/card/module/public/footer.html'
    }
}
//页面会依次加载index_background.html、apply.html、footer.html模块作为当前页面的输出,并且每个模块加载完成后会调用一次$scope.onload();
//index_background.html加载完成后会调用$scope.onload('index_background');
//apply.html加载完成后会调用$scope.onload('card_apply');
//footer.html加载完成后会调用$scope.onload('bottom');