شروع کار با AngularJS (بخش دوم)

چهارشنبه 12 اسفند 1394

در این مقاله قصد داریم درباره مسیریابی ها در AngularJS توسط Angular UI Router ، Viewهای تودرتو در AngularJS و ایجاد آدرس ها با استفاده از AngularJS HTML5 صحبت کنیم.

شروع کار با  AngularJS (بخش دوم)

در مقاله قبل برنامه ساده ای را با استفاده از  AngularJS پیاده سازی کردیم که شامل کدهای  boilerplate برای ایجاد برنامه های تک صفحه ای ، بعضی مسیریابی ها که پارامتر و احراز هویت را در برداشتند و فرم های ورود و ثبت نام بود. در اینجا از همان برنامه ایجاد شده استفاده کرده و بعضی مشکلات آن را برطرف خواهیم کرد.

برخی از مشکلات برنامه را به شرح زیر می باشند :

آدرس های نامناسب – هنگام پیمایش بین  Viewها در آدرس خود علامت  #  را مشاهده میکردیم. در این قسمت با استفاده از  HTML5 این مشکل بر طرف خواهد شد.

Viewهای نامناسب – هیچ استایل و سبکی به  Viewهای خود ندادیم، در اینجا با استفاده از  Bootstrap این مشکل برطرف خواهد شد.

مسیریابی های بسیار ابتدایی – در مقاله قبل از  ngRoute که برای مسیریابی های پایه ای مناسب است استفاده شد، اما ما نیازهای پیشرفته تری خواهیم داشت. در این بخش آن را با  Angular UI Router عوض خواهیم کرد.

هم اکنون آدرس به صورت زیر نمایش داده می شود.

هر چیزی بعد از علامت  #  توسط  web Server  نادیده گرفته خواهد شد. ngRoute که در مقاله قبل اضافه شد این بخش را در آدرس قرار می دهد.

می توانیم آدرس زیباتری با استفاده از حالت  AngularJS HTML5 داشته باشیم. باعث می شود که  Angular از  HTML5 history API برای مدیریت پیچیدگیها توسط دو خط کد استفاده کند.

تابع  ConfigFunction را به صورتی اصلاح میکنیم که شامل ماژول  $LocationProvider به عنوان یک وابستگی شود و  hashPrefixو  Html5mode را در آن فراخوانی میکنیم.

var configFunction = function ($routeProvider, $httpProvider, $locationProvider) {

    $locationProvider.hashPrefix('!').html5Mode(true);

    $routeProvider.
        when('/routeOne', {
            templateUrl: 'routesDemo/one'
        })
        .when('/routeTwo/:donuts', {
            templateUrl: function (params) { return '/routesDemo/two?donuts=' + params.donuts; }
        })
        .when('/routeThree', {
            templateUrl: 'routesDemo/three'
        })
        .when('/login', {
            templateUrl: '/Account/Login',
            controller: LoginController
        })
        .when('/register', {
            templateUrl: '/Account/Register',
            controller: RegisterController
        });

    $httpProvider.interceptors.push('AuthHttpResponseInterceptor');
}
configFunction.$inject = ['$routeProvider', '$httpProvider', '$locationProvider'];

لینکها در index  نیز به صورت زیر تغییر میکنند.

<ul>
    <li><a href="/routeOne">مسیر 1</a></li>
    <li><a href="/routeTwo/6">مسیر 2</a></li>
    <li><a href="/routeThree">مسیر 3</a></li>
</ul>

<ul>
    <li><a href="/login">ورود</a></li>
    <li><a href="/register">ثبت نام</a></li>
</ul>

برنامه را یک بار اجرا کرده و به یکی از مسیرها می رویم.

آدرس نمای بهتری گرفته است اما با refresh صفحه به صورت زیر نمایش داده می شود.

حالت  HTML5 کار کرده است . یک refresh صفحه باعث فرستادن آدرس کامل به سرور می شود. این مسئله را با پیکربندی دوباره  RouteCollection در  MVC برطرف خواهیم کرد.  مسیرها برای  Viewها را باید به روشنی مشخص کنیم و سپس یک catch-all که همه آدرس های دیگر را به صفحه landing برای دستکاری توسط  angular می فرستد را اضافه کنیم.

متد RegisterRoutes را درون  App_Start -> RouteConfig.cs به صورت زیر آپدیت میکنیم.

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
        name: "routeOne",
        url: "routesDemo/One",
        defaults: new { controller = "RoutesDemo", action = "One" });

    routes.MapRoute(
        name: "routeTwo",
        url: "routesDemo/Two/{donuts}",
        defaults: new { controller = "RoutesDemo", action = "Two", donuts = UrlParameter.Optional });

    routes.MapRoute(
        name: "routeThree",
        url: "routesDemo/Three",
        defaults: new { controller = "RoutesDemo", action = "Three" });

    routes.MapRoute(
        name: "login",
        url: "Account/Login",
        defaults: new { controller = "Account", action = "Login" });

    routes.MapRoute(
        name: "register",
        url: "Account/Register",
        defaults: new { controller = "Account", action = "Register" });

    routes.MapRoute(
        name: "Default",
        url: "{*url}",
        defaults: new { controller = "Home", action = "Index" });
}

دوباره سایت را debug کرده و دکمه های  Refresh  و  back را امتحان میکنیم. همه چیز باید درست کار کند.

برای استایل دادن به  Viewها از  Bootstrap استفاده میکنیم که آن را در بخش <head>  صفحه landing قرار داده  و  Angular UI Directives برای  Bootstrap را قبل از بسته شدن تگ  <body> قرار می دهیم.

با اضافه کردن کلاس ها و بعضی عناصر می توانیم ظاهر برنامه خود را تغییر دهیم.  Index  به صورت زیر تغییر خواهد کرد.

<!DOCTYPE html>
<html ng-app="AwesomeAngularMVCApp" ng-controller="LandingPageController">
<head>
    <title ng-bind="models.helloAngular"></title>
    <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.2.0/css/bootstrap.min.css">
    @Styles.Render("~/Content/css")
</head>
    <body>
        <div class="navbar navbar-default navbar-fixed-top" role="navigation">
            <div class="container">
                <div class="navbar-header">
                    <button type="button" class="navbar-toggle" ng-click="navbarProperties.isCollapsed = !navbarProperties.isCollapsed">
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                    </button>
                    <a class="navbar-brand" href="#">Awesome Angular MVC APP</a>
                </div>
                <div class="navbar-collapse collapse" collapse="navbarProperties.isCollapsed">
                    <ul class="nav navbar-nav">
                        <li><a href="/routeOne">مسیر 1</a></li>
                        <li><a href="/routeTwo/6">مسیر 2</a></li>
                        <li><a href="/routeThree">مسیر 3</a></li>
                    </ul>
                    <ul class="nav navbar-nav navbar-right">
                        <li><a href="/login">ورود</a></li>
                        <li><a href="/register">ثبت نام </a></li>
                    </ul>
                </div>
            </div>
        </div>
        
        <div class="container mainContent">
            <div ng-view></div>
        </div>

        <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular.min.js"></script>
        <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular-route.min.js"></script>
        <script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.10.0/ui-bootstrap-tpls.min.js"></script>
        @Scripts.Render("~/bundles/AwesomeAngularMVCApp")
    </body>
</html>

کد زیر را در  Content->Site.css  وارد میکنیم.

.mainContent {
    margin-top: 60px;
}

حالا باید Angular UI Directives  برای  Bootstrap را به برنامه Angular معرفی کنیم ، این کار را با استفاده از AwesomeAngularMVCApp.js انجام دهیم.

var AwesomeAngularMVCApp = angular.module('AwesomeAngularMVCApp', ['ngRoute', 'ui.bootstrap']);

در آخر کنترلر LandingPage  را باید با وضعیت پیش فرض برای پیمایش منو موبایل  آپدیت کنیم.

var LandingPageController = function($scope) {

    ...

    $scope.navbarProperties = {
        isCollapsed: true
    };
}

در حال حاضر از ماژول ngRoute خود  Angular  استفاده کردیم که برای مسیر یابی های ساده مناسب است. اما اگر به مسیریابی های پیشرفته تری نیاز باشد این ماژول مناسب نیست.

آن را با  Angular UI Router عوض میکنیم.  فریم ورک مسیریابی کاملی است که مسیرهای تو در تو ، چندگانه و نام  Viewرا نیر برای ما فراهم میکند.

Landing Page  خود را آپدیت کرده و تگ جاوا اسکریپتی که ngRoute را اضافه میکند با تگی که Angular UI Router  را اضافه میکند عوض میکنیم.


<script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.10/angular-ui-router.min.js"></script

سپس باید آن را در ماژول angular ثبت و ngRoute  را حذف کنیم.

var AwesomeAngularMVCApp = angular.module('AwesomeAngularMVCApp', ['ui.router', 'ui.bootstrap']);

Angular UI Router  بر پایه حالت است. بر اساس محتوای ریاضی Finit State Machine است و  Web application شما را به چیز مشابهی تبدیل میکند. بجای تبدیل پیمایش از  URL  به URL ، از حالتی به حالتی دیگر تبدیل خواهد شد و یک مسیر برای ارائه هر حالت که برنامه شما می تواند در آن قرار گیرد تنظیم میکند.

با استفاده از  UI Router می توانیم چندین  Container views بر روی صفحه landing داشته باشیم، در صورتی که با ngRoute فقط می توانیم یکی داشته باشیم. اکنون می خواهیم دو  View  را به صفحه خود اضافه کنیم و برنامه خود را به نحوی تنظیم کنیم که چهار حالت داشته باشد.

زمانی که برنامه بر روی حالت 1 است ، مسیر 1 درون اولین div  و مسیر 2 در دومین نمایش داده می شوند.

زمانی که برنامه بر روی حالت 2 است، مسیر 1 درون اولین div  و مسیر 3 در دومین نمایش داده می شوند.

زمانی که برنامه بر روی حالت 3 است، مسیر 2 درون اولین div  و مسیر 3 در دومین نمایش داده می شوند.

زمانی که برنامه در حالت LoginRegister است، فرم ورود در اولین div  و فرم ثبت نام در دومین نمایش داده می شوند.

صفحه خود را به صورت زیر که دارای دو Container باشد آپدیت میکنیم.

<div class="container mainContent">
    <div class="row">
        <div class="col-md-6">
            <div ui-view="containerOne"></div>
        </div>
        <div class="col-md-6">
            <div ui-view="containerTwo"></div>
        </div>
    </div>
</div>

برای معرفی شدن به انگولار که کدام  View در کجا و چه حالتی قرار میگیرد ، AwesomeAngularMVCApp.js را به صورت زیر آپدیت میکینم. دیگر به وابستگیهای سرویس $routeProvider احتیاجی نیست و بجای آن از وابستگی بر روی  $stateProviderاستفاده میکنیم.

var configFunction = function ($stateProvider, $httpProvider, $locationProvider) {

    $locationProvider.hashPrefix('!').html5Mode(true);

    $stateProvider
        .state('stateOne', {
            url: '/stateOne?donuts',
            views: {
                "containerOne": {
                    templateUrl: '/routesDemo/one'
                },
                "containerTwo": {
                    templateUrl: function (params) { return '/routesDemo/two?donuts=' + params.donuts; }
                }
            }
        })
        .state('stateTwo', {
            url: '/stateTwo',
            views: {
                "containerOne": {
                    templateUrl: '/routesDemo/one'
                },
                "containerTwo": {
                    templateUrl: '/routesDemo/three'
                }
            }
        })
        .state('stateThree', {
            url: '/stateThree?donuts',
            views: {
                "containerOne": {
                    templateUrl: function (params) { return '/routesDemo/two?donuts=' + params.donuts; }
                },
                "containerTwo": {
                    templateUrl: '/routesDemo/three'
                }
            }
        })
        .state('loginRegister', {
            url: '/loginRegister?returnUrl',
            views: {
                "containerOne": {
                    templateUrl: '/Account/Login',
                    controller: LoginController
                },
                "containerTwo": {
                    templateUrl: '/Account/Register',
                    controller: RegisterController
                }
            }
        });

    $httpProvider.interceptors.push('AuthHttpResponseInterceptor');
}
configFunction.$inject = ['$stateProvider', '$httpProvider', '$locationProvider'

 همچنین باید AuthHttpResponseInterceptor را برای رفتن به حالت loginRegister هنگامی که پاسخ 401 از سرور دریافت می شود آپدیت کنیم. برای رسیدن به این هدف باید سرویس  $state را تزریق کنیم. به علت اشکال در این کتابخانه ، نمی توانیم آن را به صورت مستقیم تزریق کنیم. ابتدا سرویس $injector از  angularJS را تزریق کرده و از آن برای بر طرف کردن مشکل یک نمونه از  $state استفاده می شود.

var AuthHttpResponseInterceptor = function($q, $location, $injector) {
    return {
        response: function (response) {
            if (response.status === 401) {
                console.log("Response 401");
            }
            return response || $q.when(response);
        },
        responseError: function (rejection) {
            if (rejection.status === 401) {
                $injector.get('$state').go('loginRegister', { returnUrl: $location.path() });
            }
            return $q.reject(rejection);
        }
    }
}

AuthHttpResponseInterceptor.$inject = ['$q', '$location', '$injector'];

کنترلر  LoginController نیز باید آپدیت شود که  URL بازگشتی را از شیء  $stateParam دریافت کند .

var LoginController = function ($scope, $stateParams, $location, LoginFactory) {
    $scope.loginForm = {
        ...etc
        returnUrl: $stateParams.returnUrl,
        ...etc
    };

    ...etc
}

LoginController.$inject = ['$scope', '$stateParams', '$location', 'LoginFactory'];

در آخر باید لینکهای خود را  آپدیت کنیم. جالب نیست که هایپرلینکهای ما دیگر در  URL قرار نداشته باشند؟ در اینجا از حالتها در آدرس استفاده می شود و هر پارامتری برای آن حالت با استفاده از  JSON فراهم می شود. لینکهای صفحه خود را به صورت زیر آپدیت میکنیم .

<div class="navbar-collapse collapse" collapse="navbarProperties.isCollapsed">
    <ul class="nav navbar-nav">
        <li><a ui-sref="stateOne({ donuts: 12 })">حالت 1</a></li>
        <li><a ui-sref="stateTwo">حالت 2</a></li>
        <li><a ui-sref="stateThree({ donuts: 4 })">حالت 3</a></li>
    </ul>
    <ul class="nav navbar-nav navbar-right">
        <li><a ui-sref="loginRegister">ورود / ثبت نام</a></li>
    </ul>
</div>

برنامه را debug  کرده و به حالت 1 میرویم که باید مسیر 1 و 2 را کنار هم مشاهده کنیم.

اگر بخواهیم به حالت 2 یا 3 برویم ، به حالت loginRegister فرستاده می شویم. اگر بتوانیم وارد شویم می توانیم مسیر 2 یا 3 را هم ببینیم.

در این قسمت میخواهیم درباره  nested Viewها یا Viewهای تو در تو صحبت کنیم.  ابتدا یک متد  C# دیگر در کنترلر  routesDemo با نام Four ایجاد میکنیم و برای آن View را می سازیم. محتوایی برای آن در نظر می گیریم. همچنین صفت Authorize را از متد  Route Three حذف میکنیم. میخواهیم مسیر 4 را درون مسیر 1  قرار دهیم. پس View متعلق به مسیر 1 را به صورت زیر آپدیت میکنیم.

مسیر 1

<div ui-view="nestedView"></div>

برای منعکس شدن آن در  Angular ، تنظیمات مسیر یابی را آپدیت میکینم. هنگامی که یک  nested View  را به یک حالت اضافه میکنیم، از تنظیمات نامگذاری viewName@stateName استفاده میکنیم. پس برای تنظیم nestedView برای StateOne ، نام آن را nestedView@stateOne قرار می دهیم.

$stateProvider
        .state('stateOne', {
            url: '/stateOne?donuts',
            views: {
                "containerOne": {
                    templateUrl: '/routesDemo/one'
                },
                "containerTwo": {
                    templateUrl: function (params) { return '/routesDemo/two?donuts=' + params.donuts; }
                },
                "nestedView@stateOne": {
                    templateUrl: '/routesDemo/four'
                }
            }
        })
        .state('stateTwo', 
        ...etc

 یک View جدید اضافه کردیم . پس باید  RouteConfig.cs را برای منعکس شدن آن آپدیت کنیم.

routes.MapRoute(
    name: "routeFour",
    url: "routesDemo/Four",
    defaults: new { controller = "RoutesDemo", action = "Four" });

خروجی به صورت زیر خواهد بود.

آموزش angular

فایل های ضمیمه

برنامه نویسان

نویسنده 3355 مقاله در برنامه نویسان

کاربرانی که از نویسنده این مقاله تشکر کرده اند

در صورتی که در رابطه با این مقاله سوالی دارید، در تاپیک های انجمن مطرح کنید