Separation of Spring MCV REST and Angular.js Singl

2019-08-31 23:00发布

问题:

My Application is based on these technologies:

  • Spring MVC with REST Services
  • Angular.js.

The Application is working fine but I'm now at a point where I thinking I am using the wrong architecture.

What I want is to separate the backend and frontend functionalities at all. I started to build a menu in angular with different URL's.

My Index.html looks like this:

<html> //all html stuff 
...
<nav class="hidden-xs">
    <ul class="nav navbar-nav">
        <a href="#home" role ="button" class="navbar-brand">
            Home
        </a>
        <a href="#searchContract" role ="button" class="navbar-brand">
            Search Contract
        </a>
    </ul>
</nav>
... 
<div id="wrapper">
    <div ng-view></div>
</div>
...
//inport scripts
</html>

At the backend I created UI Controller which returns my html pages. The pages getting rendered in the Index.html. The Url is also changing.

IndexUIController.java

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/")
public class IndexUIController {

    @RequestMapping
    public String getIndexPage() {
        return "index";
    }
}

I created same Controller for home and searchContract. My HTML pages are in WEB-INF folder.

My app.js looks like this

var TestApp = {};
var App = angular.module('TestApp', ['ui.bootstrap','ngRoute','angular-table']);

App.config(['$routeProvider', function ($routeProvider) {
    $routeProvider.when('/searchContract', {
        templateUrl: 'searchContract/searchContractLayout',
        controller: contractController
    });
    $routeProvider.when('/home', {
        templateUrl: 'home/homeLayout',
    });
    $routeProvider.otherwise({redirectTo: '/home'});
}]);

I use Velocity as View Resolver:

part of web.xml

<!-- Handles Spring requests -->
<servlet>
    <servlet-name>TestApp</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>WEB-INF/spring/webmvc-config.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>TestApp</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

part of webmvc-config.xml

<mvc:resources mapping="/resources/**" location="/resources/" />

    <context:component-scan base-package="at.testApp" />

    <bean id="velocityConfig" class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
        <property name="resourceLoaderPath" value="/WEB-INF/html/"/>
    </bean>

    <bean id="viewResolver" class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
        <property name="cache" value="true"/>
        <property name="prefix" value=""/>
        <property name="suffix" value=".html"/>
        <property name="exposeSpringMacroHelpers" value="true"/>
    </bean>

I'm really unsure if this is the right architecture to build a single page application with angular and using functionalities for Spring and REST Services. Because I also thinking about to use the frontend application in an offline way (IndexedDb...). My approach is to use Spring only for the REST Service and Business logic. All the client side stuff should be done at client side.

How can I use Spring MVC as a REST Service provider instead of JSP/Velocity/Any other Template view resolver. And also in a clean architecture way.

Thanks in advance for all information.

回答1:

You need a single RequestMapping in all only one of your MVC controllers, like the one you have in IndexUIController.java, which returns the view upon request.

Your single page architecture is correct as you are using a client-side architecture for routing to different pages (ngRoute).

The only thing you should be considering is that in your client you should be requesting only data (preferably in JSON format) from the server whenever necessary. So, the rest of you MVC functions must all be tagged with @ResponseBody as the return type.

The only part I am seeing missed in the code you provided is a proper client-side controller. That is the heart of your client. Normally, in your html files you bind elements with some model variables. The it is the responsibility of the controller to send (Restful) requests to the server, retrieve and update the data. This is usually initiated by the view (html page) where an event results in invoking a controllers method which in turn will populate the corresponding model. Good thing about Angular is that once you have populated the model, the view will get updated via the framework automatically.

Of course in order to invoke restful requests you need a proper mechanism. The best way to do that in Angular is via ngResource. It is easy an intuitive. You can also use the lower level $http utility. Let us stop talking and go through some code.

Here is a simple html code in a partial page (e.g. the homeLayout in your example). This is a table where I show the list of users in the system.

<form role="form" action="#" class="form-horizontal">
    <div class="form-group">
        <label for="accountId" class="col-sm-1 control-label">G+ Id: </label>
        <input type="text" id="accountId" class="form-control" ng-model="user.id"></input>
        <label for="accountName" class="col-sm-2 control-label">Name: </label>
        <input type="text" id="accountName" class="form-control" ng-model="user.name"></input>
    </div>
    <button type="button" class="btn btn-primary" ng-click="saveUser()">Save</button>
</form>

A few things to notice in this html View:

  1. I have bound the input element (accountId) to user.id variable from the corresponding Angular controller. Similarly accountName is bound to user.name model.

  2. Clicking the button will invoke the saveUser method of the controller.

Now based on this we have the below controller:

app.controller('EditUserCtrl', [
        '$scope',
        '$routeParams',
        'UserFactory',
        '$location',
        function($scope, $routeParams, UserFactory,
                $location) {

            $scope.saveUser = function() {
                if (angular.isUndefinedOrNull($scope.user) || 
                    angular.isUndefinedOrNull($scope.user.id)) {

                    $scope.user = {};
                    UserFactory.create($scope.user, function() {
                        $location.path('/list-users');
                    });
                } else {
                    UserFactory.update($scope.user, function() {
                        $location.path('/list-users');
                    });

                }
            };

            if ($routeParams.userId !== undefined) {
                $scope.user = UserFactory.show({
                    userId : $routeParams.userId
                });
            }

    } ]);

This controller uses UserFactory service to store the user via Restful requests. Here is how a simple ngResource service defined:

services.factory('UserFactory', function($resource) {
    return $resource('/rest/user/:userId', {}, {
        show : {
            method : 'GET',
            isArray : false
        },
        update : {
            method : 'PUT',
            params : {
                userId : '@id'
            }
        },
        create : {
            method : 'POST'
        }
    });
});

So to summarize you will have to have these elements in your architecture:

  1. MVC controller in the backend to respond to Restful queries

  2. Front End Single Page View which uses ngRoute.

  3. A different client-side controller for each route.

  4. A client-side service factory to retrieve data from server via restful requests.

This way you can easily decouple your client from the server (by just switching the Service Factory).

Hope this long answer would help and let me know if there is anything ambiguous here.