构建移动网站与APP:ionic移动开发入门与实战 (跨平台移动开发丛书)
上QQ阅读APP看书,第一时间看更新

3.6 一个简单的AngularJS项目:实时自选股行情页

本章前面各节都引入了相当厚重而抽象的概念,为了帮助初学AngularJS的读者理解,本节将开发一个简单但基本完整覆盖本章所介绍的概念和组件的AngularJS项目来演示说明各概念与组件的定义使用方法和严密的配合工作机制。

示例的初始页面如图3.5所示,主要的功能点有两个:

● 定时刷新显示自选股的行情信息,根据涨跌设置行项目颜色(涨红跌绿)

● 提供用户维护(增加/删除)自选股列表的功能

图3.5 使用AngularJS开发的实时股票行情页

后台行情数据取自网上找到的新浪行情接口,HTTP访问网址和返回结果如图3.6所示。

图3.6 新浪行情接口和返回结果

【示例3-10】使用AngularJS框架开发的实时股票行情页。

    视图模板文件/index.html的代码
    <! DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <title>AngularJS演示项目:实时股票行情页</title>
        <! -- 引入Bootstrap -->
        <link href="css/bootstrap.css" rel="stylesheet">
        <! -- 引入自定义CSS -->
        <link href="css/stock.css" rel="stylesheet">
        <! -- 引入angularjs-->
        <script src="js/lib/angular.js"></script>
        <! -- 引入外部lodash库 -->
        <script src="js/lib/lodash-4.13.1.js"></script>
        <! -- 引入应用的js -->
        <script src="js/app.js"></script>
        <script src="js/controllers.js"></script>
        <script src="js/services.js"></script>
        <script src="js/directives.js"></script>
      </head>
      <body ng-app="stockAPP">
        <div class="container" ng-controller="rootController">
          <! -- 沪深A股行情显示区 -->
          <div ng-controller="stockListController" class="row">
          <h1 class="text-center">股票行情显示区</h1>
          <table class="table table-striped col-md-12">
            <thead>
                  <tr>
                    <th>代码</th><th>名称</th><th>涨跌幅</th><th>涨跌额</th>
                    <th>现价</th><th>开盘价</th><th>昨收</th><th>操作</th>
                  </tr>
                </thead>
                <tbody>
                  <tr ng-repeat="item in stockItems"
                    ng-class="{inc:                item.changePercent>0.0,                dec:item.changePercent<0.0}">
                    <td><stock-code info="item.code"></stock-code></td>
                    <td>{{item.name}}</td>
                    <td>{{item.changePercent|number:2}}%</td>
                    <td>{{item.changeAmount|number:2}}</td>
                    <td>{{item.currentPrice}}</td>
                    <td>{{item.openPrice}}</td>
                    <td>{{item.closePrice}}</td>
                    <td>
                      <button class="btn btn-danger btn-sm"
                        ng-click="removeStock(item.code)">
                      删除</button>
                    </td>
                  </tr>
                </tbody>
              </table>
            </div>
            <! -- 增加股票代码操作区 -->
            <div ng-controller="addStockController" class="row">
              <div class="col-sm-3" style="padding-left: 0px; padding-right: 0px">
                <input ng-model="newStockCode" ng-keypress="keypress($event)"
                class="form-control" placeholder="输入股票代码,如600028">
              </div>
              <div class="col-md-1">
              <button ng-click="addStock(newStockCode)"
                style="margin-left: -10px; " class="btn btn-primary">
                增加
             </button>
             </div>
           </div>
         </div>
       </body>
     </html>

【代码解析】index.html文件是应用的入口文件,在HEAD标签引入了使用的BootStrap样式库、自定义的样式库文件stock.css、AngularJS的库文件、用于集合操作的外部lodash库和依据本章前面各节介绍的AngularJS主应用、控制器、服务和指令模块定义文件。注意自定义的AngularJS模块文件需要在AngularJS的库文件之后引入。页面视图分为2个区域:

● 上方的沪深A股行情显示区使用了stockListController控制器,整个行情表都将使用这个控制器提供的作用域对象。表格的TBODY区使用了ng-repeat指令,这是用于数组型作用域变量循环生成HTML子元素的常用组件。TR标签的ng-class指令用于根据作用域对象的表达式动态设置CSS样式类,这里的实际逻辑是涨幅为正则附加.inc样式类,为负则附加.dec样式类。股票代码栏使用了自定义的指令stock-code,并将股票代码item.code设置到了该指令的info属性上。关于涨跌幅的两个数字输出都使用了number过滤器用于四舍五入只显示小数位后2位数字。最后的删除按钮上附加了ng-click事件,点击后将调用作用域对象的removeStock方法,传入参数为当前项的股票代码。

● 下方的增加股票代码操作区使用了addStockController控制器,用于输入股票代码的INPUT文本控件使用ng-model与作用域对象的newStockCode属性绑定。这样当用户按下“增加”按钮或是在文本控件里按回车键时,作用域对象的newStockCode属性会同步为输入框内的值,用于后续事件处理的参数。

两个区域组合成一个整体,该整体用一个DIV标签包容,该标签的DOM对象对应于rootController控制器。该控制器是为了两个区域的控制器通过事件通信而存在的。

提示

代码里为了方便演示,使用的是目前流行的外部CSS样式库BootStrap。然而Ionic建立了自己的CSS样式库,与BootStrap并不适合一起使用。因此笔者不对使用的BootStrap样式类进行深入的解释了,读者可以在本书的第5章了解到Ionic内置CSS样式库的使用方法。

主应用模块文件/js/app.js的代码:

        angular.module("stockAPP", ['stockAPP.controllers', 'stockAPP.services', 'stockAPP.directives'])
        .config(['stockListProvider', function(stockListProvider) {
          //为了演示Provider型组件的效果,随意选择几个股票代码配置

    stockListProvider.setCodeList(['sz000858', 'sh600048', 'sh601857', 'sz002594', 'sh601965']);
        }])
        ;

【代码解析】主应用文件的代码很简单,主要是用于引用载入应用涉及的控制器、服务和指令的模块。此外为了演示Provider型组件的效果,对stockList服务在设置代码块设置了初始的自选股列表,在图3.5中显示的列表就是这里设置指定的。

控制器模块文件/js/controllers.js的代码

        angular.module('stockAPP.controllers', [])
        //自选股行情列表控制器
        .controller('stockListController',
        ['$scope', '$interval', 'stockInfoService', 'stockList',
        function($scope, $interval, stockInfoService, stockList){
          $scope.stockItems=[];
      //刷新自选股行情列表方法
          $scope.refresh = function () {
            stockInfoService.syncInfoList([]).success(function () {
              $scope.stockItems = stockInfoService.getInfoList();
            }).error(function () {
              $scope.stockItems = stockInfoService.getInfoList();
            });
          };
      //接收到刷新自选股行情列表消息,执行刷新
          $scope.$on('Refresh_Table', function(){
            $scope.refresh();
          });
          //初始时刷新一次
          $scope.$broadcast('Refresh_Table');
          //设置每隔一段时间刷新自选股行情列表
          $interval(()=>$scope.$broadcast('Refresh_Table'), 5000);

          //删除某自选股方法
          $scope.removeStock= function(stockCode){
            stockList.removeCode(stockCode);
            $scope.$broadcast('Refresh_Table');
          };
        }])
        //增加自选股面板控制器
        .controller('addStockController',
        ['$scope', 'stockList', function($scope, stockList){
          //在自选股中增加股票的方法
          $scope.addStock = function(stockCode){
            stockList.addCode(stockCode);
            $scope.newStockCode="";
            $scope.$emit('StockList_Changed');
          };
          //处理股票代码输入框中按回车键自动增加的方法
          $scope.keypress = function(evt){

            if(evt.which===13){
              $scope.addStock($scope.newStockCode);
            }
          }
        }])
        //最外层容器控制器
        .controller('rootController',
        ['$scope', function($scope){
          /自选股列表变化后通知广播刷新自选股行情列表
          $scope.$on('StockList_Changed', function(){ /
            $scope.$broadcast('Refresh_Table');
          });
        }])
        ;

【代码解析】控制器模块文件分别定义了rootController、stockListController和addStockController这3个控制器,它们分别对应外容器与页面视图DOM中的上下两个区域。值得注意的是刷新自选股行情列表的refresh()方法是使用stockListController定义在其内部作用域的。这是因为在下方区域执行完增加自选股后也需要马上刷新自选股行情列表。因此下方区域将使用$emit()方法往上发射一个名为StockList_Changed的事件。rootController通过$on()函数侦听到这个事件后,判断需要让关心此事件的下层控制器来刷新自选股行情列表。因此使用$broadcast()方法进行了广播,注意事件已改名为Refresh_Table。正在侦听Refresh_Table事件的stockListController调用作用域对象的refresh()方法,从而刷新了自选股行情列表。此外$interval服务也是才接触到的,它可以理解为全局函数setInterval()的封装,可用于执行周期性任务。不过使用它能激发AngularJS执行作用域的更新通知机制,因此更适合目前的场景。

提示

代码的控制器里使用了本章3.3.2节介绍的作用域事件上传与广播机制,而不是把refresh()方法直接定义在根作用域上来使其他作用域能够激发自选股行情列表的刷新。这种松散耦合的做法是复杂前端页面常见的组件间解耦模式,而使用jQuery开发往往是把组件间的调用设计成强耦合的方法调用模式。笔者不做过多评判,读者可以思考一下两种方案的利弊和分别适用的场景。

服务模块文件/js/services.js的代码:

        angular.module('stockAPP.services', [])
        .factory('stockInfoService', ['$http', 'stockList', function  ($http, stockList)
    {
          return {
            //与行情提供方同步数据
            syncInfoList: function(){
              var codeList = _.join(stockList.getCodeList());
          //使用JSONP的方式调用新浪提供的股票实时行情API
          return $http.jsonp("http://hq.sinajs.cn/list=" + codeList);
    },
    //解析并返回通过JSONP方式拿到的行情方的数据
        getInfoList: function(){
          return _.map(stockList.getCodeList(), stockCode=>{
            var stockInfoArray = _.split(window['hq_str_' + stockCode], ", ");
                closePrice = parseFloat(stockInfoArray[2]),
                currentPrice = parseFloat(stockInfoArray[3]);
            return {
              code: stockCode,
              name: stockInfoArray[0],
              changePercent: 100*(currentPrice - closePrice) / closePrice,
              changeAmount: currentPrice - closePrice,
              currentPrice: parseFloat(stockInfoArray[3]),
              openPrice: parseFloat(stockInfoArray[1]),
              closePrice: parseFloat(stockInfoArray[2]),
            }
          });
        },
      };
    }])
    //维护视图中显示的股票代码列表,带市场前缀形式
    .provider('stockList', [function stockListProvider(){
      this.$get = [function(){
        var currentCodeList = this.codeList;
    return {
      //获取自选股列表
          getCodeList: function(){
            return currentCodeList;
          },
          //增加自选股
          addCode: function(code){
            var fullCode = code[0]=='6' ? 'sh'+code : 'sz'+code;
            if(-1 == _.findIndex(currentCodeList, n => n==fullCode)){
              currentCodeList.push(fullCode);
            }
          },
          //删除自选股
          removeCode: function(code){
            _.remove(currentCodeList, n => n==code);
          }
        };
          }];
          //初始化自选股,随机选择两个
          this.codeList = ['sh600030', 'sz002650'];
          this.setCodeList = function(initCodeList){
            this.codeList = initCodeList || this.codeList;
          };
        }])
        ;

【代码解析】服务模块文件分别使用factory()和provider ()方法定义了两个服务类组件。stockInfoService组件用于从后端行情提供方获取并解析数据。由于后端行情提供方未在服务器上设置好CORS,这样直接使用$http服务通过get方法拿数据会因为浏览器的默认安全设置而阻止AJAX调用。因此代码里的syncInfoList方法改用$http服务的jsonp帮助方法,获的数据后AngularJS将自动执行返回的全局变量定义脚本,这样就把自选股的行情列表原始数据存放到了多个全局变量里,随后getInfoList方法可以被调用来解析数据。两个函数里都使用了lodash库(通过全局变量_)进行数组和字符串的处理,相当方便。stockList组件用于维护视图中显示的自选股代码列表,因此提供了CRUD中除了修改(因为没有意义)的函数。值得注意的是这里为了演示的目的,提供了自选股代码列表在应用初始启动时的配置接口函数setCodeList,在主应用模块文件/js/app.js中的config方法块里对它进行了调用。

指令模块文件/js/directives.js的代码:

        angular.module('stockAPP.directives', [])
        .directive('stockCode', [function(){
        //演示用显示股票代码指令组件,去掉了股票市场前缀
          return {
            restrict: 'E',
            //stockCode的形式为'sh600030',这里去掉市场前缀,取后六个字符显示
            template: `{{stockCode| limitTo:-6}}`,
            scope: {
              stockCode: '=info' //股票代码取自stock-code标签的info属性
            }
          };
        }])
        ;

【代码解析】指令模块文件出于演示的目的,创建了一个显示股票代码指令的小指令组件。该组件里调用了AngularJS框架内置的limitTo筛选器用于去掉了股票代码里的股票市场前缀,并且限制其只能用于HTML元素的形式。

自定义的样式库文件/css/stock.css的代码:

        /*上涨的股票红色文本强调*/
        tr.inc{
          color: red;
          font-weight: bold;
        }
        /*下跌的股票用绿色文本显示*/
        tr.dec{
          color: green;
        }

【代码解析】自定义的样式库文件定义了视图模板文件/index.html里ng-class指令可能加入的.inc和.dec样式类,这样能通过颜色和字体动态区分股票的涨跌类型。

提示

本示例的重点是介绍AngularJS,并未针对移动应用进行优化,需要学习后面章节的知识来增强。

经过增加和删除模拟操作测试使用后的页面如图3.7所示。

图3.7 AngularJS开发的实时股票行情页操作测试后效果

提示

这里的测试是笔者手工测试的结果,事实上使用AngularJS框架开发的另一个强大之处是单元测试、集成测试和模拟用户手工测试的便利性。本书这里由于篇幅的关系不再介绍编写测试用例和相关工具的使用了,有兴趣的读者可以自己在网上搜索到相关的资料学习。

相信看完本节示例项目代码的读者会发现,相对于使用jQuery来编写同样的功能页来说,AngularJS的项目文件结构感很强,模块之间的职责划分很明显。而视图页也不会充斥太多的JavaScript逻辑,使前端程序员东拼西凑完成功能的行为有所收敛。这些都是AngularJS框架哲学带来的效果。对于团队型的项目开发来说,AngularJS带来的严谨要求和便利性是值得花时间学习尝试的。