AngularJS通过HTTP与后台API进行数据交互

本文实现了独立前端通过angularJS的controller向后端发送数据和从后端获取数据

本文前端调用SpringBoot+Swagger-UI构建API及其文档的后端API

搭建基础前端架构

用NodeJS搭建服务器,基础教程看这里

1. 项目架构

Project Structure
  • node_modules:项目依赖的NodeJS libraries,通过npm下载,package.jsonpackage-lock.json是其附属产物
  • pde_server.js:前端服务器
  • project_name/templates:页面源码
  • project_name/static:静态资源,JavaScript,CSS等

2. 添加依赖

如果没有,则在当前目录添加express library

$ cnpm install express --save

3. 修改之前的server.js成pde_server.js

1. 增加到不同文件夹的快捷路径

app.use('/js', express.static('Pennsylvania_Education/static/js'));
app.use('/css', express.static('Pennsylvania_Education/static/css'));

app.use可以用来重设静态资源的地址,当视图(html)中出现


服务器会自动查找向当前文件夹下的Pennsylvania_Education/static/css/dashboard.css

除了可以少打写字,这个功能还可以方便我们在不同的服务器下反复使用已经编写好的前端代码,比如现在这个project还可以被python的flask和SpringBoot的Thymeleaf无缝兼容,因为他们都默认进入templates文件夹查找视图,进入static文件夹查找静态资源

2. 增加到获得数据和展示数据的前台页面的路径

app.get('/applicationForm', function (req, res) {
        res.sendFile( __dirname + "/Pennsylvania_Education/templates/" + "applicationForm.html" );
    });

app.get('/dashBoard', function (req, res) {
        res.sendFile( __dirname + "/Pennsylvania_Education/templates/" + "dashboard.html" );
    });

4. 完整的pde_server.js放一份

var express = require('express');

function startServer() {
    var app = express();
    // use 'http://0.0.0.0:8081/static' points to local '/frontend/static'
    // first parameter the same as the one include in html
    app.use('/js', express.static('Pennsylvania_Education/static/js'));
    app.use('/css', express.static('Pennsylvania_Education/static/css'));
    app.set('view engine', 'html');
    
    app.get('/index', function (req, res) {
        res.sendFile( __dirname + "/Pennsylvania_Education/templates/" + "index.html" );
    });

    app.get('/applicationForm', function (req, res) {
        res.sendFile( __dirname + "/Pennsylvania_Education/templates/" + "applicationForm.html" );
    });

    app.get('/dashBoard', function (req, res) {
        res.sendFile( __dirname + "/Pennsylvania_Education/templates/" + "dashboard.html" );
    });

    var server = app.listen(6060, '0.0.0.0', function () {
        var host = server.address().address
        var port = server.address().port
        console.log("visit http://%s:%s", host, port)
    });
};

startServer();

向后台传输数据

1. 创建页面

templates里新建一个输入数据的表单页面applicationForm.html
AngularJS的controller中目前实现了两个方法:

  • scope.submitForm目前将我们输入的值返回显示在页面上,之后我们将改变这个方法使其将值传入后端
  • scope.transform帮助我们将选择的日期返回显示在选择框内,单纯的时间戳并没有办法显示在input内


  
    
    
    Application Form
    
      
    
    
  

  
    
    

Application Form

{{result}}

运行NodeJS,浏览器输入http://0.0.0.0:6060/applicationForm

applicationForm.html

2. 使用angularJS向后端发送HTTP request

修改$scope.submitForm方法

$scope.submitForm = function () {
                console.log('enter submitForm');
                $http({
                    method:'post',
                    url:'http://localhost:6050/user/add',
                    data:$.param($scope.user),
                }).then(function(resp){
                    console.log('post success');
                    console.log(resp);
                },function(resp){
                    console.log('post error');
                    console.log(resp);
                });
                console.log('exit submitForm');
            };

后台SpringBoot controller里的接收方法

@RequestMapping(value="/add", method=RequestMethod.POST)
    @ApiOperation("Add User")
    public void add(User u){
        System.out.println(u);
        userRepository.save(u);
    }

运行后报如下错误


applicationForm.html

说明请求的接口地址和本身的服务器不属于一个域内,需要设置跨区域访问。
按照报错提示给angularJS的HTTP Request添加header

$http({
    method:'post',
    url:'http://localhost:6050/user/add',
    data:$.param($scope.user),
    headers: {
        'Access-Control-Allow-Origin': '*'
    }
  }).then......

重启后发现还是没有解决问题。调查了一下发现原来是后端Controller也要加上@CrossOrigin注释。(PS:后来测试其实只要后端有注释,前端没有Access-Control-Allow-Origin也是可以的。)

@CrossOrigin
@RestController
@Api(tags = "User internface")
@RequestMapping(value="/user")
public class UserController {
        ............
}

再重启之后,提交一个表单,从前端来看是成功的


applicationForm.html

然而后端接收到的数据却没有值

2020-05-22 13:54:29.016  INFO 72808 --- [nio-6050-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2020-05-22 13:54:29.017  INFO 72808 --- [nio-6050-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2020-05-22 13:54:29.036  INFO 72808 --- [nio-6050-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 19 ms
User [ Id = 0, firstName = , lastName = , birthDate = 2020-05-22 ]
Hibernate: insert into testu (birth_date, first_name, last_name) values (?, ?, ?)

原因是默认情况下,jQuery传输数据使用Content-Type: x-www-form-urlencodedand和类似于"name=zhangsan&age=18"的序列,然而AngularJS,传输数据使用Content-Type: application/json和{ "name": "zhangsan", "age": "18" }这样的json序列。
查看Request信息,果然如此。

Request Information

注意Content TypeRequest Payload
我们此时需要改变Request header中的Content Type

$scope.submitForm = function () {
                console.log('enter submitForm');
                $http({
                    method:'post',
                    url:'http://localhost:6050/user/add',
                    data:$.param($scope.user),
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded',
                        'Access-Control-Allow-Origin': '*'
                    }
                }).then(function(resp){
                    console.log('post success');
                    console.log(resp);
                },function(resp){
                    console.log('post error');
                    console.log(resp);
                });
                console.log('exit submitForm');
            };

重新提交表单,得到正确结果。

2020-05-23 00:39:03.237  INFO 72808 --- [nio-6050-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2020-05-23 00:39:03.237  INFO 72808 --- [nio-6050-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2020-05-23 00:39:03.241  INFO 72808 --- [nio-6050-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 4 ms
User [ Id = 0, firstName = Hermione, lastName = Granger, birthDate = 2014-02-26 ]
Hibernate: insert into testu (birth_date, first_name, last_name) values (?, ?, ?)

id=0说明未给id赋值,存入MySQL时将会被自动修正为自增值。

此时的Request信息。


Request Information

从后台读取数据

1. 检查后台方法

JPA Repository里

@Query(value = "SELECT YEAR(birth_date) AS year, COUNT(id) as num FROM testu GROUP BY YEAR(birth_date)", nativeQuery = true)
List countByBirthYear();

SpringBoot Controller里

    @ResponseBody
    @RequestMapping(value="/countByBirthYear", method=RequestMethod.POST)
    @ApiOperation("Return the number of users born in each year")
    public Map countByBirthYear(){
        List summary = userRepository.countByBirthYear();
        Map res = new HashMap<>();
        for(int i = 0; i

可以通过swagger-ui检测运行。


Swagger-UI countByBirthYear

2. 创建页面

templates里新建一个显示图表的页面dashboard.html。
先设定基本的HTML架构,canvas是用来存放图表的容器。其下方的两行列表是AngularJS module中稍后会设置的两个变量,用来检验我们输出的数据是否正确。



  
    
    DashBoard
    
    
    
  

  
    

Show my data summary

{{drawChart()}}
  • {{labels}}
  • {{counts}}

在static/css里创建一个dashboard.css(也可以不加并删除dashboard.html里的css依赖)

h2{
  font-size:3em;
  text-align:center;
  font-family: Monospace;
  color: #F08080;
}

#Dashboard{
  width: 90%;
  height: 100%;
  vertical-align: middle;
  margin-left: 30px;
}

#year_count{
  margin: 10px 10px 10px 10px;
}

为了保证项目的结构性,每个AngularJS module都要有自己独立的区域。我们在js目录里建立当前管理当前页面的AngularJS module的app文件夹,在文件夹里创建三个js文件。

  • app.js: 导入当前AngularJS module。
  • services.js: 当前AngularJS module的controllers里注入的services。目前我们有两个方法。一个用来从后端获取数据,一个用来将数据展示到图表里。
  • controllers.js: 当前AngularJS module的controllers。
js目录中的结构

app.js里就一行code。

//variable can only be defined once
var app = angular.module('myApp', []);

services.js里先创建一个用于获取数据的service,目前只有静态数据。存储数据的变量是私有的,需要公有的方法使controller可以获取其中数据。如果使变量公有,则每次在service内部使用都要用this.variableName的形式去调用,不如直接封装方便。

app.service('dataGenerator', function ($http, $q) {
        var labels = ["supermarket", "clothes", "electronic", "kitchen", "baby", "book"];
        var counts = [40, 100, 20, 60, 0, 30];

        //private variables and functions are decorated by var
        //public variables and functions are decorated by this.variableName
        this.getLabels = function(){
          return labels;
        }

        this.getCounts = function(){
          return counts;
        }
});

services.js里再创建一个画图的service,用获取的数据作为公有方法的input。

app.service('drawer', function () {
        this.drawChart = function(labels, counts){
          console.log("Enter draw chart");
          var chart = document.getElementById('year_count').getContext('2d');
          myChart = new Chart(chart, {
              type: 'bar',
              data: {
                  labels: labels,
                  datasets: [{
                      label: 'times of visit',
                      data: counts,
                      backgroundColor: [
                          'rgba(255, 99, 132, 0.2)',
                          'rgba(54, 162, 235, 0.2)',
                          'rgba(255, 206, 86, 0.2)',
                          'rgba(75, 192, 192, 0.2)',
                          'rgba(153, 102, 255, 0.2)',
                          'rgba(255, 159, 64, 0.2)'
                      ],
                      barPercentage: 0.5
                  }]
              }
            });
          console.log("Exit draw chart");
        }
});

controllers.js:里调用两个services。

app.controller('graphCtrl', function($scope, dataGenerator, drawer) {

        $scope.labels = dataGenerator.getLabels();
        $scope.counts = dataGenerator.getCounts();
        drawer.drawChart($scope.labels, $scope.counts);
});

dashboard.html的body最下方导入三个js文件。注意顺序,比如要先创造了module,才能在其中创造服务并注入控制器。


    
............

运行NodeJS,浏览器输入[http://0.0.0.0:6060/dashboard]

dashboard.html

3. 使用HTTP从后台获取数据

services.js里的dataGenerator里添加一个getData()获取数据的方法。从后端获取的数据会存储到私有变量中,并且在回调函数中返回。如果请求发生错误,错误信息一样会被回调函数返回。
$http调用了官方的http请求函数,$q调用了官方的Promise回调库。

this.getData = function(){
          console.log('Enter getData');
          var defer = $q.defer();
          //from angularJS 1.6 on, use "then" instead of "success" to get a promise manner
          $http({
            method:'post',
            url:'http://localhost:6050/user/countByBirthYear',
            headers: {
             'Content-Type': 'application/x-www-form-urlencoded'
            },
          }).then(function(resp){
            console.log('get count data successfully');
            // console.log(resp);
            labels = [];
            counts = [];
            angular.forEach(resp.data, function (v, k) {
              labels.push(k);
              counts.push(v);
            });
            defer.resolve({"labels":labels, "counts":counts});
            // defer.resolve(); 如果不想返回任何结果也可以为空
          },function(resp){
            console.log('get count data failed');
            defer.reject(resp);
          });
          console.log('Exit getData');
          return defer.promise;
        }

controller.js调用getData(),并在成功后画图。

app.controller('graphCtrl', function($scope, dataGenerator, drawer) {
        dataGenerator.getData().then(function(data){
            $scope.labels = dataGenerator.getLabels();
            $scope.counts = dataGenerator.getCounts();
            // 使用以下是一样的效果
            // $scope.labels = data.labels;
            // $scope.counts = data.counts;
            drawer.drawChart($scope.labels, $scope.counts);
          },function(err){
            console.log(err);
          })
});

刷新页面测试一下。


dashboard.html

是图表坐标的问题,在图表设置中加上y轴范围的限制。

myChart = new Chart(chart, {
              type: 'bar',
              data: {
                  labels: labels,
                  datasets: [{
                      label: 'times of visit',
                      data: counts,
                      backgroundColor: [
                          'rgba(255, 99, 132, 0.2)',
                          'rgba(54, 162, 235, 0.2)',
                          'rgba(255, 206, 86, 0.2)',
                          'rgba(75, 192, 192, 0.2)',
                          'rgba(153, 102, 255, 0.2)',
                          'rgba(255, 159, 64, 0.2)'
                      ],
                      barPercentage: 0.5
                  }]
              },
              options: {
                scales: {
                  yAxes: [{
                    ticks: {
                      min: 0,
                      max: 5
                    }
                  }]
                }
              }
            });

再来刷新一下,就正确了。


dashboard.html

参考文献

angularjs日期格式化
AngularJS表单提交
AngularJs中$http发送post或者get请求,SpringMVC后台接收不到参数值的解决办法
怎么把服务和控制器单独写在某个文件,单独进行调用?
AngularJS中的Provider们:Service和Factory等的区别

这一次还引用了很多网络上的前端Library,如果想要下载到本地使用,请移步官网
Chart.js Official Website
Bootstrap Official Website
AngularJS中文网
JQuery Official Website

你可能感兴趣的:(AngularJS通过HTTP与后台API进行数据交互)