前言:
"宁肯像种子一样等待 也不愿像疲惫的陀螺 旋转得那样勉强"
这是前几天在查资料无意间看到的一位园友的签名,看完后又读了两遍,觉得很有味道。后来一寻根究底才知这是出资大诗人汪国真之口,出处《她》。且抛开上下文,单从这短短几句,正恰如其分的折射出有一群人,他们穿着不那么fashion,言辞不那么犀利,但是内心某一块地方像是躁动的火山,拥有无尽的动力和激情,矢志不渝种子般投身到技术研究和心得分享当中。
或许每一次的生长都是那么悄无声息,但是无数次的坚持只是为了破土那日让别人看到坚持的自己。
正文:
上篇我们主要做了如下工作:
创建了两个文件:
statistic.html:提供Statistic这个功能的界面
StatisticController.js:作为statistic.html的御用控制器,负责为statistic.html提供相应的功能和数据
更新了两个文件:
Angello.js:为页面跳转添加接口
boot.js:注册新建的js文件,以便新建的js文件投入使用
同时遇到了一些坑比如:
controllerAs参数的使用,以及它的利与弊
Statistic的功能分为两块:
第一是数据统计,通过上篇的StatisticController控制器就能实现传值并配合data.html显示,如上篇中看到效果图页面的上半部分;
第二是数据展示,这就是今天以及后面所要做的工作。今天会讲到如何使用指令,为什么要用指令以及在编码过程中遇到的一些各色问题。
项目的代码我已经托管在Github上:angelloExtend
一、使用D3.js
以前做可视化的时候,研究过Gephi和Prefuse,但是D3.js的大名如雷贯耳。当时只知道D3都是js的代码,与项目使用的场景不合,现在来看,正好派上用场。
D3本身就是负责直观显示的视觉类产品,所以首先需要跑出一个效果出来。于是找到了一段代码
代码语言:javascript复制<html>
<head>
<meta charset="utf-8">
<title>做一个简单的图表</title>
</head>
<body>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script>
var width = 300; //画布的宽度
var height = 300; //画布的高度
var svg = d3.select("body") //选择文档中的body元素
.append("svg") //添加一个svg元素
.attr("width", width) //设定宽度
.attr("height", height); //设定高度
var dataset = [ 250 , 210 , 170 , 130 , 90 ];
var rectHeight = 25; //每个矩形所占的像素高度(包括空白)
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x",20)
.attr("y",function(d,i){
return i * rectHeight;
})
.attr("width",function(d){
return d;
})
.attr("height",rectHeight-2)
.attr("fill","steelblue");
</script>
</body>
</html>
将其直接替换statistic.html页面,刷新页面发现并没有出现预期的图形,而是在控制台界面中报错说d3没定义。根据这个线索开始怀疑d3的js文件并没有引入成功,有怀疑过是否被墙,但是后来证实不是这个原因。最终发现还是应了上篇的那个小坑,js文件在适用前都需要注册,于是在boot.js中加入代码行:
代码语言:javascript复制{ file: 'http://d3js.org/d3.v3.min.js'},
刷新界面后显示正常。
二、开辟显示Statistic结果的地盘
前面我们一直借statistic.html这个页面来实现测试,但事实是statistic.html是用于显示用户列表,当点击某个用户下面的statistic功能的时候,一切的statistic结果需要显示在另外一张页面。
这个逻辑可以借用Angello项目中的User模块。所以我们需要新建一个存放statistic数据的data.html页面并在Angello.js中添加data.html页面跳转的路由:
Angello.js
代码语言:javascript复制.when('/statistic/:userId', {
templateUrl: 'src/angello/statistic/tmpl/data.html',
controller: 'DataCtrl',
controllerAs: 'myUser',
requiresLogin: true,
resolve: {
user: function ($route, $routeParams, UsersModel) {
var userId = $route.current.params['userId']
? $route.current.params['userId']
: $routeParams['userId'];
return UsersModel.fetch(userId);
},
stories: function ($rootScope, StoriesModel) {
return StoriesModel.all();
}
}
})
data.html
就是上面放到statistic.html的中代码
与此同时我们需要一个支持这个data.html的controller——DataController.js
代码语言:javascript复制angular.module('Angello.Statistic')
.controller('DataCtrl',
function ($routeParams, user, stories, $log) {
var myUser = this;
myUser.userId = $routeParams['userId'];
myUser.user = user.data;
myUser.getAssignedStories = function (userId, stories) {
var assignedStories = {};
Object.keys(stories, function(key, value) {
if (value.assignee == userId) assignedStories[key] = stories[key];
});
return assignedStories;
};
myUser.stories = myUser.getAssignedStories(myUser.userId, stories);
var len=0,toDo=0,inProgress=0,codeReview=0,qaReview=0,verified=0;
function getJsonObjLength(jsonObj) {
for (var item in jsonObj) {
switch (jsonObj[item].status){
case "To Do":
toDo ;
break;
case "In Progress":
inProgress ;
break;
case "Code Review":
codeReview ;
break;
case "QA Review":
qaReview ;
break;
case "Verified":
verified ;
break;
}
len ;
}
return len;
}
myUser.num = getJsonObjLength(myUser.stories);
myUser.toDo = toDo;
myUser.inProgress = inProgress;
myUser.codeReview = codeReview;
myUser.qaReview = qaReview;
myUser.verified = verified;
myUser.statusArr = [["To Do", toDo], ["In Progress", inProgress], ["Code Review", codeReview], ["QA Review", qaReview], ["Verified", verified]];
});
三、引入指令
到目前为止已经有了路由和页面,能够正常显示,但是这里有一个问题:
当前的data.html包含了javascript代码和要显示的页面元素,这不符合MVC分离架构。我们需要将负责显示d3的业务逻辑放到它该存在的地方。
当时我想到了指令。在页面中通过Attribute、Element、Class等任意一种形式定义一个指令,然后在指令完成需要的代码逻辑。
首先是在data.html中声明一个指令,我们命名为d3chart:
代码语言:javascript复制<div style="margin-top:100px">
<div style="color:white" align="center">
<h2>Stories Assigned to {{myUser.user.name}}</h2>
<h2>Total Stories Num {{myUser.num}}</h2>
<h2>To Do:{{myUser.toDo}}</h2>
<h2>In Progress:{{myUser.inProgress}}</h2>
<h2>Code Review:{{myUser.codeReview}}</h2>
<h2>QA Review:{{myUser.qaReview}}</h2>
<h2>Verified:{{myUser.verified}}</h2>
</div>
<div style="color:white" align="center" status-arr="myUser.statusArr" stories="myUser.stories" d3chart></div>
</div>
其中status-arr="myUser.statusArr"是将保存在DataController.js中的数据传到这里的status-arr变量上,然后在D3Chart.js中注入这个变量以便directive能够使用这个传过来的变量值。
备注:这里有一个命名坑
如果你在这里的data.html页面中想通过一个属性名为statusArr的来接收这个myUser.statusArr,你会发现在D3Chart.js中根本接收不到这个statusArr。
原因很简单,html不区分大小写,所以在这里你可以使用status-arr来代替你想要的statusArr,这也是一种规范,希望大家不要在这个小问题上栽跟头。
下面我们就来实现这个d3chart指令,其中业务很简单,只是将原来放在data.hmtl中的javascript代码移到这里的指令里面
D3Chart.js
代码语言:javascript复制angular.module("Angello.Statistic")
.directive("d3chart",function(){
return{
restrict:'EA',
link: function($scope, $element, $attrs){//link or controller
var width = 400;
var height = 400;
var dataset = $scope.myUser.statusArr;
var svg = d3.select("body")
.attr("align","center")// align svg to center
.append("svg")
.attr("width", width)
.attr("height", height);
var pie = d3.layout.pie();
var piedata = pie(dataset);
var outerRadius = 150; //外半径
var innerRadius = 0; //内半径,为0则中间没有空白
var arc = d3.svg.arc() //弧生成器
.innerRadius(innerRadius) //设置内半径
.outerRadius(outerRadius); //设置外半径
var color = d3.scale.category10();
var arcs = svg.selectAll("g")
.data(piedata)
.enter()
.append("g")
.attr("transform","translate(" (width/2) "," (width/2) ")");
arcs.append("path")
.attr("fill",function(d,i){
return color(i);
})
.attr("d",function(d){
return arc(d);
});
arcs.append("text")
.attr("transform",function(d){
return "translate(" arc.centroid(d) ")";
})
.attr("text-anchor","middle")
.text(function(d){
return d.data;
});
console.log(dataset);
console.log(piedata);
}
}
});
OK,到此为止,我们就能够看到一个显示了statistic结果的D3饼状图了
文章还没有结束,下面补充多讲一点,有关controller和directive之间的scope问题和通信问题。
四、controller和directive纠缠不清?
只有一个controller存在的时候,我们思路很清晰,需要传的值,需要实现的逻辑,统统在这里。可是有了directive之后,从初学者来说,真的不是1 1=2这样看的见的难度。
我们需要明确这两个家伙怎么联系,联系的方式有几种,又各有什么不同。
主要的理论情景其实我早在《Angularjs入门新的1——directive和controller如何通信》就有介绍:
1. 共享 scope :directive 中不设置 scope 属性
2. 独立 scope :directive 中设置 scope : true
3. 隔离 scope :directive 中设置 scope : { }
之所以会牵扯到这个问题,是在注入statusArr时联想到的。
当在directive中不添加scope声明的时候,默认是directive和controller共用scope,这会降低指令的重用性,也有可能会"弄脏"scope。此时这个statusArr是不需要注入的,只要在使用前加上$scope即可,代码如下:
代码语言:javascript复制angular.module("Angello.Statistic")
.directive("d3chart",function(){
return{
restrict:'EA',
link: function($scope, $element, $attrs){//link or controller
var width = 400;
var height = 400;
var dataset = $scope.myUser.statusArr;
var svg = d3.select("body")
.attr("align","center")// align svg to center
.append("svg")
.attr("width", width)
.attr("height", height);
var pie = d3.layout.pie();
var piedata = pie(dataset);
var outerRadius = 150; //外半径
var innerRadius = 0; //内半径,为0则中间没有空白
var arc = d3.svg.arc() //弧生成器
.innerRadius(innerRadius) //设置内半径
.outerRadius(outerRadius); //设置外半径
var color = d3.scale.category10();
var arcs = svg.selectAll("g")
.data(piedata)
.enter()
.append("g")
.attr("transform","translate(" (width/2) "," (width/2) ")");
arcs.append("path")
.attr("fill",function(d,i){
return color(i);
})
.attr("d",function(d){
return arc(d);
});
arcs.append("text")
.attr("transform",function(d){
return "translate(" arc.centroid(d) ")";
})
.attr("text-anchor","middle")
.text(function(d){
return d.data;
});
console.log(dataset);
console.log(piedata);
}
}
});
其中确实没有声明scope属性,所以这里在使用statusArr时只需要$scope.myUser.statusArr即可。
另外一种是创建属于directive自己的scope,这时就没有了共享controller中scope的福利,但是也提高了自己的独立性,低耦合。代码如下:
代码语言:javascript复制angular.module("Angello.Statistic")
.directive("d3chart",function(){
return{
restrict:'EA',
scope: {
statusArr: "="
},
link: function($scope, $element, $attrs){//link or controller
var width = 400;
var height = 400;
var dataset = $scope.statusArr;;
var svg = d3.select("body")
.attr("align","center")// align svg to center
.append("svg")
.attr("width", width)
.attr("height", height);
var pie = d3.layout.pie();
var piedata = pie(dataset);
var outerRadius = 150; //外半径
var innerRadius = 0; //内半径,为0则中间没有空白
var arc = d3.svg.arc() //弧生成器
.innerRadius(innerRadius) //设置内半径
.outerRadius(outerRadius); //设置外半径
var color = d3.scale.category10();
var arcs = svg.selectAll("g")
.data(piedata)
.enter()
.append("g")
.attr("transform","translate(" (width/2) "," (width/2) ")");
arcs.append("path")
.attr("fill",function(d,i){
return color(i);
})
.attr("d",function(d){
return arc(d);
});
arcs.append("text")
.attr("transform",function(d){
return "translate(" arc.centroid(d) ")";
})
.attr("text-anchor","middle")
.text(function(d){
return d.data;
});
console.log(dataset);
console.log(piedata);
}
}
});
这时候在scope将statusArr双向绑定到页面中的statusArr,所以这里要使用可以直接写成$scope.statusArr即可。
通过这个问题的解决,更加深刻的理解了不同scope的使用场景。directive和controller之间scope的关系。
今天主要介绍的内容有:
- 添加一个新的页面用于存放statistic出来的数据信息和图形信息;
- 如何引入D3引擎;
- 为什么要使用指令;
- 我的代码逻辑中如何使用指令;
- html的命名规范坑;
- directive和controller如何通信以及它们的scope之间的关系
下篇预告:bug hunting篇
如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!如果您想持续关注我的文章,请扫描二维码,关注JackieZheng的微信公众号,我会将我的文章推送给您,并和您一起分享我日常阅读过的优质文章。