本文实现了独立前端通过angularJS的controller向后端发送数据和从后端获取数据
本文前端调用SpringBoot+Swagger-UI构建API及其文档的后端API
搭建基础前端架构
用NodeJS搭建服务器,基础教程看这里
1. 项目架构
Project Structure- node_modules:项目依赖的NodeJS libraries,通过npm下载,package.json和package-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)中出现
<link rel="stylesheet" type="text/css" href="css/dashboard.css"></link>
服务器会自动查找向当前文件夹下的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内
<!DOCTYPE html>
<html lang="en">
<head>
<meta HTTP-EQUIV="CONTENT-TYPE" CONTENT="text/html; charset=utf-8"></meta>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Application Form</title>
<script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://cdn.staticfile.org/angular.js/1.4.6/angular.min.js"></script>
</head>
<body>
<div ng-app="myApp" class="container" style="width:300px;">
<div class="row " style="width: 100%; vertical-align:middle; margin: 10px 10px 10px 10px; padding: 10px 10px 10px 10px;">
<h2>Application Form</h2>
<form novalidate ng-submit="submitForm()" ng-controller="formCtrl" role="form">
<div class="form-group">
<label for="first_name">First Name</label>
<input type="text" class="form-control" ng-model="user.firstName">
</div>
<div class="form-group">
<label for="last_name">Last Name</label>
<input type="text" class="form-control" ng-model="user.lastName">
</div>
<div class="form-group">
<label for="birth_date">Date of Birth</label>
<input type="date" class="form-control" ng-model="user.birthDate" ng-value="transform(user.birthDate)">
</div>
<div class="form-group">
<button type="submit" class="btn btn-default">submit</button>
</div>
<p>{{result}}</p>
</form>
</div>
</div>
<script>
var app = angular.module('myApp', []);
app.controller('formCtrl', function($scope) {
$scope.submitForm = function () {
console.log('enter submitForm');
$scope.result = angular.copy($scope.user);
};
$scope.transform = function (transTime) {
var date = new Date(transTime);
var year = date.getFullYear();
var month = date.getMonth() + 1;
month = month < 10 ? '0' + month : month;
var d = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
return year + '-' + month + '-' + d;
};
});
</script>
</body>
</html>
运行NodeJS,浏览器输入http://0.0.0.0:6060/applicationForm
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信息,果然如此。
注意Content Type和Request 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<Object[]> countByBirthYear();
SpringBoot Controller里
@ResponseBody
@RequestMapping(value="/countByBirthYear", method=RequestMethod.POST)
@ApiOperation("Return the number of users born in each year")
public Map<String, Integer> countByBirthYear(){
List<Object[]> summary = userRepository.countByBirthYear();
Map<String, Integer> res = new HashMap<>();
for(int i = 0; i<summary.size(); i++) {
Object[] ans = summary.get(i);
String year = ans[0].toString();
int count = Integer.parseInt(ans[1].toString());
res.put(year, count);
}
return res;
}
可以通过swagger-ui检测运行。
Swagger-UI countByBirthYear
2. 创建页面
templates里新建一个显示图表的页面dashboard.html。
先设定基本的HTML架构,canvas是用来存放图表的容器。其下方的两行列表是AngularJS module中稍后会设置的两个变量,用来检验我们输出的数据是否正确。
<!DOCTYPE html>
<html lang="en">
<head>
<meta HTTP-EQUIV="CONTENT-TYPE" CONTENT="text/html; charset=utf-8"></meta>
<title>DashBoard</title>
<link rel="stylesheet" type="text/css" href="css/dashboard.css"></link>
<script src=“https://cdn.jsdelivr.net/npm/chart.js@2.9.3/dist/Chart.min.js”></script>
<script src="https://cdn.staticfile.org/angular.js/1.4.6/angular.min.js"></script>
</head>
<body>
<div ng-app="myApp">
<h2>Show my data summary</h2>
<div id="Dashboard" ng-controller="graphCtrl">
<canvas id="year_count">{{drawChart()}}</canvas>
<ul>
<li>{{labels}}</li>
<li>{{counts}}</li>
</ul>
</div>
</div>
</body>
</html>
在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。
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,才能在其中创造服务并注入控制器。
<body>
<div ng-app="myApp">
............
</div>
<script type="text/javascript" src="js/app/app.js"></script>
<script type="text/javascript" src="js/app/services.js"></script>
<script type="text/javascript" src="js/app/controllers.js"></script>
</body>
运行NodeJS,浏览器输入[http://0.0.0.0:6060/dashboard]
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
网友评论