Làm cách nào để Nút Quay lại hoạt động với máy trạng thái ui-router AngularJS?


87

Tôi đã triển khai một ứng dụng trang đơn anglejs bằng ui-router .

Ban đầu, tôi xác định mỗi trạng thái bằng cách sử dụng một url riêng biệt, tuy nhiên, điều này tạo ra các url đóng gói GUID không thân thiện.

Vì vậy, bây giờ tôi đã xác định trang web của mình là một máy trạng thái đơn giản hơn nhiều. Các trạng thái không được xác định bằng url mà chỉ được chuyển đổi thành theo yêu cầu, như sau:

Xác định các trạng thái lồng nhau

angular
.module 'app', ['ui.router']
.config ($stateProvider) ->
    $stateProvider
    .state 'main', 
        templateUrl: 'main.html'
        controller: 'mainCtrl'
        params: ['locationId']

    .state 'folder', 
        templateUrl: 'folder.html'
        parent: 'main'
        controller: 'folderCtrl'
        resolve:
            folder:(apiService) -> apiService.get '#base/folder/#locationId'

Chuyển sang trạng thái xác định

#The ui-sref attrib transitions to the 'folder' state

a(ui-sref="folder({locationId:'{{folder.Id}}'})")
    | {{ folder.Name }}

Hệ thống này hoạt động rất tốt và tôi thích cú pháp rõ ràng của nó. Tuy nhiên, vì tôi không sử dụng url nên nút quay lại không hoạt động.

Làm cách nào để giữ cho máy trạng thái ui-router gọn gàng nhưng vẫn bật chức năng nút quay lại?


1
"các trạng thái không được xác định bằng url" - và tôi nghi ngờ có vấn đề của bạn. Nút quay lại được bảo vệ khá tốt khỏi mã (nếu không mọi người sẽ ghi đè nó, gây ra sự cố). Tại sao không chỉ để góc cạnh tạo ra các url tốt hơn, giống như SO (OK, họ có thể không sử dụng góc cạnh, nhưng url schemse của họ là minh họa)?
jcollum

Ngoài ra, câu hỏi này có thể hữu ích: stackoverflow.com/questions/13499040/…
jcollum

Ngoài ra, vì bạn không sử dụng URL không có nghĩa là để đến trạng thái Z, mọi người sẽ phải nhấp qua trạng thái X và Y để đến được nó? Điều đó có thể gây khó chịu.
jcollum

nó sẽ đi với trạng thái với các tham số khác nhau? @jcollum
VijayVishnu 17/09/19

Tôi không có ý tưởng, đây là cách đây quá lâu
jcollum

Câu trả lời:


78

Ghi chú

Các câu trả lời đề xuất sử dụng các biến thể của $window.history.back()đều đã bỏ lỡ một phần quan trọng của câu hỏi: Làm thế nào để khôi phục trạng thái của ứng dụng về vị trí trạng thái chính xác khi lịch sử nhảy (lùi / chuyển tiếp / làm mới). Với ý nghĩ đó; làm ơn, hãy đọc tiếp.


Có, có thể để trình duyệt quay lại / chuyển tiếp (lịch sử) và làm mới trong khi chạy một ui-routermáy trạng thái thuần túy nhưng phải thực hiện một chút.

Bạn cần một số thành phần:

  • URL duy nhất . Trình duyệt chỉ bật nút quay lại / chuyển tiếp khi bạn thay đổi url, vì vậy bạn phải tạo một url duy nhất cho mỗi trạng thái đã truy cập. Mặc dù vậy, các url này không cần chứa bất kỳ thông tin trạng thái nào.

  • Một dịch vụ phiên . Mỗi url được tạo có tương quan với một trạng thái cụ thể, vì vậy bạn cần có cách lưu trữ các cặp trạng thái url của mình để bạn có thể truy xuất thông tin trạng thái sau khi ứng dụng góc của bạn được khởi động lại bằng các nhấp chuột lùi / chuyển tiếp hoặc làm mới.

  • A State History . Một từ điển đơn giản về các trạng thái ui-router được khóa bằng url duy nhất. Nếu bạn có thể dựa vào HTML5 thì bạn có thể sử dụng API lịch sử HTML5 , nhưng nếu, giống như tôi, bạn không thể thì bạn có thể tự triển khai nó trong một vài dòng mã (xem bên dưới).

  • Một Dịch vụ Vị trí . Cuối cùng, bạn cần có thể quản lý cả các thay đổi trạng thái ui-router, được kích hoạt nội bộ bởi mã của bạn và các thay đổi url trình duyệt thông thường thường được kích hoạt bởi người dùng nhấp vào các nút trình duyệt hoặc nhập nội dung vào thanh trình duyệt. Tất cả điều này có thể trở nên phức tạp một chút vì rất dễ bị nhầm lẫn về điều gì đã kích hoạt điều gì.

Đây là cách thực hiện của tôi đối với từng yêu cầu này. Tôi đã gói mọi thứ thành ba dịch vụ:

Dịch vụ phiên

class SessionService

    setStorage:(key, value) ->
        json =  if value is undefined then null else JSON.stringify value
        sessionStorage.setItem key, json

    getStorage:(key)->
        JSON.parse sessionStorage.getItem key

    clear: ->
        @setStorage(key, null) for key of sessionStorage

    stateHistory:(value=null) ->
        @accessor 'stateHistory', value

    # other properties goes here

    accessor:(name, value)->
        return @getStorage name unless value?
        @setStorage name, value

angular
.module 'app.Services'
.service 'sessionService', SessionService

Đây là một trình bao bọc cho sessionStorageđối tượng javascript . Tôi đã cắt nó xuống cho rõ ràng ở đây. Để có giải thích đầy đủ về điều này, vui lòng xem: Làm cách nào để xử lý việc làm mới trang bằng Ứng dụng Trang đơn AngularJS

Dịch vụ Lịch sử Tiểu bang

class StateHistoryService
    @$inject:['sessionService']
    constructor:(@sessionService) ->

    set:(key, state)->
        history = @sessionService.stateHistory() ? {}
        history[key] = state
        @sessionService.stateHistory history

    get:(key)->
        @sessionService.stateHistory()?[key]

angular
.module 'app.Services'
.service 'stateHistoryService', StateHistoryService

Việc StateHistoryServicetrông coi việc lưu trữ và truy xuất các trạng thái lịch sử được khóa bằng các url duy nhất, được tạo. Nó thực sự chỉ là một trình bao bọc tiện lợi cho một đối tượng kiểu từ điển.

Dịch vụ Vị trí Tiểu bang

class StateLocationService
    preventCall:[]
    @$inject:['$location','$state', 'stateHistoryService']
    constructor:(@location, @state, @stateHistoryService) ->

    locationChange: ->
        return if @preventCall.pop('locationChange')?
        entry = @stateHistoryService.get @location.url()
        return unless entry?
        @preventCall.push 'stateChange'
        @state.go entry.name, entry.params, {location:false}

    stateChange: ->
        return if @preventCall.pop('stateChange')?
        entry = {name: @state.current.name, params: @state.params}
        #generate your site specific, unique url here
        url = "/#{@state.params.subscriptionUrl}/#{Math.guid().substr(0,8)}"
        @stateHistoryService.set url, entry
        @preventCall.push 'locationChange'
        @location.url url

angular
.module 'app.Services'
.service 'stateLocationService', StateLocationService

Các StateLocationServicexử lý hai sự kiện:

  • locationChange . Điều này được gọi khi vị trí của trình duyệt bị thay đổi, thường là khi nhấn nút quay lại / chuyển tiếp / làm mới hoặc khi ứng dụng khởi động lần đầu tiên hoặc khi người dùng nhập url. Nếu trạng thái cho location.url hiện tại tồn tại trong StateHistoryServicethì nó được sử dụng để khôi phục trạng thái thông qua ui-router's $state.go.

  • stateChange . Điều này được gọi khi bạn di chuyển trạng thái trong nội bộ. Tên và thông số của trạng thái hiện tại được lưu trữ trong StateHistoryServicekhóa được tạo bởi một url được tạo. Url được tạo này có thể là bất kỳ thứ gì bạn muốn, nó có thể hoặc không thể xác định trạng thái theo cách con người có thể đọc được. Trong trường hợp của tôi, tôi đang sử dụng một tham số trạng thái cộng với một chuỗi chữ số được tạo ngẫu nhiên bắt nguồn từ một hướng dẫn (xem chân để biết đoạn mã trình tạo hướng dẫn). Url đã tạo được hiển thị trong thanh trình duyệt và quan trọng là được thêm vào ngăn xếp lịch sử nội bộ của trình duyệt bằng cách sử dụng @location.url url. Nó thêm url vào ngăn xếp lịch sử của trình duyệt cho phép các nút chuyển tiếp / quay lại.

Vấn đề lớn với kỹ thuật này là việc gọi @location.url urltrong stateChangephương thức sẽ kích hoạt $locationChangeSuccesssự kiện và do đó hãy gọi locationChangephương thức. Việc gọi @state.gotừ bằng nhau locationChangesẽ kích hoạt $stateChangeSuccesssự kiện và vì vậy stateChangephương thức. Điều này rất khó hiểu và làm rối tung lịch sử trình duyệt.

Giải pháp này rất đơn giản. Bạn có thể thấy preventCallmảng đang được sử dụng như một ngăn xếp ( poppush). Mỗi khi một trong các phương thức được gọi, nó sẽ ngăn phương thức kia được gọi là một lần duy nhất. Kỹ thuật này không can thiệp vào việc kích hoạt chính xác các sự kiện $ và giữ cho mọi thứ luôn ổn định.

Bây giờ tất cả những gì chúng ta cần làm là gọi các HistoryServicephương thức vào thời điểm thích hợp trong vòng đời chuyển đổi trạng thái. Điều này được thực hiện trong .runphương pháp AngularJS Apps , như sau:

Angular app.run

angular
.module 'app', ['ui.router']
.run ($rootScope, stateLocationService) ->

    $rootScope.$on '$stateChangeSuccess', (event, toState, toParams) ->
        stateLocationService.stateChange()

    $rootScope.$on '$locationChangeSuccess', ->
        stateLocationService.locationChange()

Tạo hướng dẫn

Math.guid = ->
    s4 = -> Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1)
    "#{s4()}#{s4()}-#{s4()}-#{s4()}-#{s4()}-#{s4()}#{s4()}#{s4()}"

Với tất cả những điều này, các nút tiến / lùi và nút làm mới đều hoạt động như mong đợi.


1
trong ví dụ về SessionService ở trên, tôi nghĩ rằng phương thức accessor: nên được sử dụng @setStorage@getStoragethay vì sử dụng `@ get / setSession '.
Peter Whitfield

3
Ngôn ngữ nào được sử dụng cho ví dụ này. Dường như không có góc cạnh mà tôi quen thuộc.
deepak

Ngôn ngữ là javascript, cú pháp là coffeescript.
biofractal

1
@jlguenego Bạn có một lịch sử trình duyệt chức năng / các nút tới lui và URL mà bạn có thể đánh dấu.
Torsten Barthel

3
@jlguenego - các câu trả lời đề xuất sử dụng các biến thể của $window.history.back()đã bỏ sót một phần quan trọng của câu hỏi. Mục đích là khôi phục trạng thái của ứng dụng về vị trí trạng thái chính xác khi lịch sử nhảy (quay lại / chuyển tiếp / làm mới). Điều này thường đạt được bằng cách cung cấp dữ liệu trạng thái qua URI. Câu hỏi hỏi làm thế nào để chuyển giữa các vị trí trạng thái mà không có dữ liệu trạng thái URI (rõ ràng). Với ràng buộc này, chỉ cần sao chép nút quay lại là không đủ vì điều này dựa vào dữ liệu trạng thái URI để thiết lập lại vị trí trạng thái.
biofractal,

46
app.run(['$window', '$rootScope', 
function ($window ,  $rootScope) {
  $rootScope.goBack = function(){
    $window.history.back();
  }
}]);

<a href="#" ng-click="goBack()">Back</a>

2
thích cái này! ... lol ... để rõ ràng $window.history.backlà không làm điều kỳ diệu $rootScope... vì vậy quay lại có thể bị ràng buộc với phạm vi chỉ thị thanh điều hướng của bạn nếu bạn muốn.
Benjamin Conant

@BenjaminConant Đối với những người không biết cách thực hiện điều này, bạn chỉ cần đặt $window.history.back();vào một hàm cần được gọi ng-click.
chakeda

đúng rootScope chỉ là làm cho chức năng truy cập vào bất kỳ mẫu
Guillaume Massé

Bữa tối gà.
Cody

23

Sau khi thử nghiệm các đề xuất khác nhau, tôi thấy rằng cách dễ nhất thường là cách tốt nhất.

Nếu bạn sử dụng ui-router góc cạnh và bạn cần một nút để quay lại thì tốt nhất là:

<button onclick="history.back()">Back</button>

hoặc là

<a onclick="history.back()>Back</a>

// Cảnh báo không đặt href hoặc đường dẫn sẽ bị hỏng.

Giải thích: Giả sử một ứng dụng quản lý tiêu chuẩn. Đối tượng tìm kiếm -> Xem đối tượng -> Chỉnh sửa đối tượng

Sử dụng các giải pháp góc Từ trạng thái này:

Tìm kiếm -> Xem -> Chỉnh sửa

Đến :

Tìm kiếm -> Xem

Đó là những gì chúng tôi muốn ngoại trừ nếu bây giờ bạn nhấp vào nút quay lại của trình duyệt, bạn sẽ lại ở đó:

Tìm kiếm -> Xem -> Chỉnh sửa

Và điều đó không logic

Tuy nhiên bằng cách sử dụng giải pháp đơn giản

<a onclick="history.back()"> Back </a>

từ :

Tìm kiếm -> Xem -> Chỉnh sửa

sau khi nhấp vào nút:

Tìm kiếm -> Xem

sau khi nhấp vào nút quay lại của trình duyệt:

Tìm kiếm

Tính nhất quán được tôn trọng. :-)


7

Nếu bạn đang tìm kiếm nút "quay lại" đơn giản nhất, thì bạn có thể thiết lập một lệnh như sau:

    .directive('back', function factory($window) {
      return {
        restrict   : 'E',
        replace    : true,
        transclude : true,
        templateUrl: 'wherever your template is located',
        link: function (scope, element, attrs) {
          scope.navBack = function() {
            $window.history.back();
          };
        }
      };
    });

Hãy nhớ rằng đây là một nút "quay lại" khá khó hiểu vì nó đang sử dụng lịch sử của trình duyệt. Nếu bạn đưa nó vào trang đích của mình, nó sẽ đưa người dùng quay lại bất kỳ url nào mà họ đến trước khi đến trang của bạn.


3

giải pháp nút quay lại / chuyển tiếp của trình duyệt
Tôi gặp phải vấn đề tương tự và tôi đã giải quyết nó bằng cách sử dụng popstate eventtừ đối tượng $ window và ui-router's $state object. Một sự kiện cửa sổ bật lên được gửi đến cửa sổ mỗi khi mục lịch sử hoạt động thay đổi.
Các$stateChangeSuccess$locationChangeSuccess kiện và không được kích hoạt khi nhấp vào nút của trình duyệt mặc dù thanh địa chỉ cho biết vị trí mới.
Vì vậy, giả sử bạn đã lèo lái từ các tiểu bang mainđể folderđến mainmột lần nữa, khi bạn nhấn backvào trình duyệt, bạn sẽ trở lại các foldertuyến đường. Đường dẫn được cập nhật nhưng chế độ xem thì không và vẫn hiển thị bất cứ thứ gì bạn có trên đó main. thử đi:

angular
.module 'app', ['ui.router']
.run($state, $window) {

     $window.onpopstate = function(event) {

        var stateName = $state.current.name,
            pathname = $window.location.pathname.split('/')[1],
            routeParams = {};  // i.e.- $state.params

        console.log($state.current.name, pathname); // 'main', 'folder'

        if ($state.current.name.indexOf(pathname) === -1) {
            // Optionally set option.notify to false if you don't want 
            // to retrigger another $stateChangeStart event
            $state.go(
              $state.current.name, 
              routeParams,
              {reload:true, notify: false}
            );
        }
    };
}

Các nút quay lại / chuyển tiếp sẽ hoạt động trơn tru sau đó.

lưu ý: kiểm tra khả năng tương thích của trình duyệt cho window.onpopstate () để chắc chắn


3

Có thể được giải quyết bằng cách sử dụng chỉ thị đơn giản "quay lại lịch sử", chỉ thị này cũng đang đóng cửa sổ trong trường hợp không có lịch sử trước đó.

Chỉ thị sử dụng

<a data-go-back-history>Previous State</a>

Khai báo chỉ thị Angular

.directive('goBackHistory', ['$window', function ($window) {
    return {
        restrict: 'A',
        link: function (scope, elm, attrs) {
            elm.on('click', function ($event) {
                $event.stopPropagation();
                if ($window.history.length) {
                    $window.history.back();
                } else {
                    $window.close();  
                }
            });
        }
    };
}])

Lưu ý: Làm việc bằng cách sử dụng ui-router hoặc không.


0

Nút Quay lại cũng không hoạt động với tôi, nhưng tôi phát hiện ra rằng vấn đề là tôi có htmlnội dung bên trong trang chính của mình, trong ui-viewphần tử.

I E

<div ui-view>
     <h1> Hey Kids! </h1>
     <!-- More content -->
</div>

Vì vậy, tôi đã chuyển nội dung sang một .html tệp và đánh dấu nó là một mẫu trong .jstệp với các tuyến.

I E

   .state("parent.mystuff", {
        url: "/mystuff",
        controller: 'myStuffCtrl',
        templateUrl: "myStuff.html"
    })

-1

history.back()và chuyển sang trạng thái trước đó thường cho hiệu quả không như ý muốn. Ví dụ: nếu bạn có biểu mẫu với các tab và mỗi tab có trạng thái riêng, điều này chỉ chuyển tab trước đó được chọn, không quay lại từ biểu mẫu. Trong trường hợp các trạng thái lồng nhau, bạn thường cần nghĩ về phù thủy của các trạng thái mẹ mà bạn muốn khôi phục.

Chỉ thị này giải quyết vấn đề

angular.module('app', ['ui-router-back'])

<span ui-back='defaultState'> Go back </span>

Nó trở lại trạng thái đã hoạt động trước khi nút hiển thị. Không bắt buộcdefaultState là tên trạng thái được sử dụng khi không có trạng thái trước đó trong bộ nhớ. Ngoài ra, nó khôi phục vị trí cuộn

class UiBackData {
    fromStateName: string;
    fromParams: any;
    fromStateScroll: number;
}

interface IRootScope1 extends ng.IScope {
    uiBackData: UiBackData;
}

class UiBackDirective implements ng.IDirective {
    uiBackDataSave: UiBackData;

    constructor(private $state: angular.ui.IStateService,
        private $rootScope: IRootScope1,
        private $timeout: ng.ITimeoutService) {
    }

    link: ng.IDirectiveLinkFn = (scope, element, attrs) => {
        this.uiBackDataSave = angular.copy(this.$rootScope.uiBackData);

        function parseStateRef(ref, current) {
            var preparsed = ref.match(/^\s*({[^}]*})\s*$/), parsed;
            if (preparsed) ref = current + '(' + preparsed[1] + ')';
            parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/);
            if (!parsed || parsed.length !== 4)
                throw new Error("Invalid state ref '" + ref + "'");
            let paramExpr = parsed[3] || null;
            let copy = angular.copy(scope.$eval(paramExpr));
            return { state: parsed[1], paramExpr: copy };
        }

        element.on('click', (e) => {
            e.preventDefault();

            if (this.uiBackDataSave.fromStateName)
                this.$state.go(this.uiBackDataSave.fromStateName, this.uiBackDataSave.fromParams)
                    .then(state => {
                        // Override ui-router autoscroll 
                        this.$timeout(() => {
                            $(window).scrollTop(this.uiBackDataSave.fromStateScroll);
                        }, 500, false);
                    });
            else {
                var r = parseStateRef((<any>attrs).uiBack, this.$state.current);
                this.$state.go(r.state, r.paramExpr);
            }
        });
    };

    public static factory(): ng.IDirectiveFactory {
        const directive = ($state, $rootScope, $timeout) =>
            new UiBackDirective($state, $rootScope, $timeout);
        directive.$inject = ['$state', '$rootScope', '$timeout'];
        return directive;
    }
}

angular.module('ui-router-back')
    .directive('uiBack', UiBackDirective.factory())
    .run(['$rootScope',
        ($rootScope: IRootScope1) => {

            $rootScope.$on('$stateChangeSuccess',
                (event, toState, toParams, fromState, fromParams) => {
                    if ($rootScope.uiBackData == null)
                        $rootScope.uiBackData = new UiBackData();
                    $rootScope.uiBackData.fromStateName = fromState.name;
                    $rootScope.uiBackData.fromStateScroll = $(window).scrollTop();
                    $rootScope.uiBackData.fromParams = fromParams;
                });
        }]);
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.