# 视图跳转（二）

## 关联事件

View\.js 在执行视图跳转时，将按顺序执行如下动作：

1. 同步触发 `View` 的 `beforechange` 事件
2. 异步触发 当前活动视图 的 `leave` 事件
3. 同步触发 目标视图 的 `beforeenter` 事件
4. 隐藏当前活动视图，展现目标视图
5. 同步触发 目标视图 的布局动作
6. 同步触发 `View` 的 `change` 事件
7. 同步触发 目标视图 的 `ready` 事件
8. 同步触发 目标视图 的 `enter` 事件
9. 同步触发 目标视图 的 `afterenter` 事件
10. 更新视图容器上标记的活动视图信息
11. 异步触发 `View` 的 `afterchange` 事件

> 附加在 `window` 上的全局变量 `View`，以及每个 `View` 的实例，均是事件驱动的，均支持API：\
> 1\. 添加事件监听：`on(evtName: string, handle: Function)` \
> 2\. 移除事件监听：`off(evtName: string, handle: Function)`\
> 3\. 发起事件：`fire(evtName: string, evtData?: any)`

假定我们当前处于视图 A ，对于浏览路径：A → B 及如下事件监听：

{% tabs %}
{% tab title="init.js" %}

```javascript
var viewA = View.ofId("A"),
    viewB = View.ofId("B");

var log = function(msg){
    return function(){
        console.log(msg);
    };
};

View.on("beforechange", log("View: beforechange"));
View.on("change", log("View: change"));
View.on("afterchange", log("View: afterchange"));

viewA.on("leave", log("A: leave"));
viewB.on("beforeenter", log("B: beforeenter"));
viewB.on("ready", log("B: ready"));/* ready 事件仅在视图第一次进入时触发 */
viewB.on("enter", log("B: enter"));
viewB.on("afterenter", log("B: afterenter"));
```

{% endtab %}
{% endtabs %}

在切换至视图 B 后，我们将会得到下面的结果

![事件触发顺序](https://img-blog.csdnimg.cn/20190809204048147.jpg)

View\.js 预置的这些事件，是为了提供干预入口，使得开发者能够尽可能地找到贴切、恰当的时机执行程序任务。

通常来讲：

1. 实例的 `ready` 事件，由于只在视图第一次进入时触发，多用于实现不需要反复进行的数据加载
2. 实例的 `enter` 事件，多用于实现 “每次进入视图均重新加载、渲染数据”
3. 实例的 `leave`, `beforeenter` 和 `afterenter`，多用于 “重置页面内容，或准备页面环境”
4. `View` 的 `beforechange`, `change` 和 `afterchange`，多用于 “实现视图跳转动画，或执行其它宏观处理”

## 跳转目标

View\.js 当前支持如下几种跳转目标：

1. 视图ID
2. 视图名称
3. 视图实例（`1.7.0+` 可用）
4. 伪视图
5. 外部链接地址

&#x20;例如：

{% code title="action.js" %}

```javascript
/**
 * 以“压入堆栈”方式跳转至 default 命名空间下，ID 为 targetView 的视图
 * 等同于 View.navTo("targetView", "default");
 *
 * 跳转目标：视图 
 */
View.navTo("targetView");

/**
 * 以“压入堆栈”方式跳转至 my-namespace 命名空间下，ID 为 targetView 的视图
 * 跳转目标：视图
 *
 * 不同命名空间下可以声明相同ID的视图
 */
View.navTo("targetView", "my-namespace");

/**
 * 以“压入堆栈”方式跳转至 my-namespace 命名空间下，ID 为 targetView 的视图
 * 跳转目标：视图
 *
 * 不同命名空间下可以声明相同ID的视图
 */
View.navTo(View.ofId("targetView", "my-namespace"));

/**
 * 以“切换栈顶”方式跳转至 default 命名空间下，ID 为 targetView 的视图，
 * 并使用 params 关键字和 options 关键字传递参数
 *
 * 跳转目标：视图
 */
View.changeTo("targetView", {
    /**
     * params 为预留关键字，代表 视图参数。
     * 视图参数可以传递任意类型的数据，但在视图刷新后丢失。
     */
    params: {
        "attr1": 1,
        "obj": document.body,
        "callback": function(data){
            //do something with data
        }
    },
    
    /**
     * options 为预留关键字，代表 视图选项。
     * 视图选项只能传递字符串类型的数据，页面刷新后参数仍然存在。
     */
    options: {
        "token": "token123"
    }
});

/**
 * 跳转目标：伪视图
 *
 * 支持的伪视图：
 * 1) :default-view - 默认视图
 * 2) :back - 上一个视图
 * 3) :forward - 下一个视图
 *
 * View.changeTo() 仅支持伪视图：":default-view"
 */
View.navTo(":default-view");

/**
 * 跳转目标：视图名称
 *
 * 符号：“~” 用于告诉 View.js 后边跟随的，是视图的名称
 *
 * View.changeTo() 同样支持按相同的语法进行跳转
 */
View.navTo("~viewName");

/**
 * 跳转目标：外部链接
 *
 * 符号：“@” 用于告诉 View.js 后边跟随的，是外部链接地址
 * 当跳转目标以 http，https 或 ftp 开头时，将自动识别为外部链接，不需要 "@" 符号
 *
 * View.changeTo() 同样支持按相同的语法跳转至外部链接
 */
View.navTo("@http://view-js.com");

/**
 * 跳转目标：外部链接
 *
 * 如果跳转前的 URL 为 "http://domain/path/to/html/main.html"，
 * 跳转后的 URL 将为 "http://domain/path/to/html/sub/another.html"
 */
View.changeTo("@sub/another.html");
```

{% endcode %}

## 跳转拦截

从 `1.7.0` 开始，View\.js 支持开发者通过添加拦截器的方式，在宏观层面拦截视图跳转动作。例如：

```javascript
/**
 * 添加视图跳转拦截器
 * 拦截器通过返回 true 或 false 告知 View.js 是否继续执行视图跳转动作
 */
View.addSwitchInterceptor(function(meta){
    if(user.isLogined())
        return true;
    
    toast("请登录");
    //...
    
    return false;
});
```

{% hint style="info" %}
如果添加了多个拦截器，View\.js 将按添加顺序顺序执行拦截器。如果上一个拦截器返回 `false`，下一个拦截器不会继续执行。
{% endhint %}

## 跳转效果优化

由于视图跳转完成的，只是活动视图DOM骨架的切换。而开发者一般在视图进入时才开始加载数据，所以会呈现出 “页面结构先变化，而后才填充数据” 的非原子性展示效果。

为了缓解这一不友好的体验，从 `1.7.0` 开始，View\.js 允许开发者在进入视图前预先执行异步请求，在得到数据后再执行视图跳转动作。

例如：

{% tabs %}
{% tab title="商品详情" %}

```javascript
var view = View.ofId("goods-detail");

/**
 * 商品详情 视图使用该API描述自己所需要的数据的获取方式。
 * 其它视图则使用该视图中由 View.js 创建的标准 fetchData() 
 * 方法执行数据加载动作。
 * 
 * resolve：由 View.js 提供，供开发者执行的，用于告知 View.js 数据加载完成的方法；
 * reject：由 View.js 提供，供开发者执行的，用于告知 Viewjs 数据加载失败的方法。
 */
view.setDataFetchAction(function(resolve, reject){
    var goodsId = view.seekParameter("goods-id");

    /* 加载商品详情 */
    var promise1 = new Promise(function(_resolve, _reject){
        callApi("get-goods-detail", {
            id: "GOODS1",
            
            onsuccess: function(data){
                _resolve(data);
            },
            onerror: function(err){
                _reject(err);
            }
        })
    });
    
    /* 加载商品评论 */
    var promise2 = new Promise(function(_resolve, _reject){
        callApi("get-goods-comments", {
            id: "GOODS1",
            
            onsuccess: function(data){
                _resolve(data);
            },
            onerror: function(err){
                _reject(err);
            }
        })
    });
    
    /* 数据加载成功或失败后通知 View.js */
    Promise.all([promise1, promise2]).then(resolve, reject);
});


/* 视图进入后，根据传入的数据渲染界面，不再执行网络加载，从而带来完整性体验 */
view.on("enter", function(){
    /* 参数由 商品列表 界面传入 */
    var datas = view.getParameter("preloadedDatas");

    var goodsDetal = datas[0],
        goodsComments = dats[1];
    
    /* 呈现商品详情 */
    showGoodsDetail(goosdDetail);
    
    /* 呈现商品评论 */
    showGoodsComments(goodsComments);
});
```

{% endtab %}

{% tab title="商品列表" %}

```javascript
var view = View.ofId("goods-list");

/* 商品详情的入口 */
var goods1Obj = view.find(".list .detail[data-id=GOODS1]");

/**
 * 进入 商品详情 视图。
 * 进入前，预先加载 商品详情 所需要的数据。
 */
goods1Obj.addEventListener("click", function(){
    var targetView = View.ofId("goods-detail");
    
    /**
     * 加载数据（数据的加载方法由目标视图自行描述）
     */
    loading.show();
    var thenable = targetView.fetchData();
    thenable.then(function(datas){/* 数据加载成功 */
        loading.hide();
        
        /* 将数据传入 商品详情 视图 */
        View.navTo(targetView, {
            preloadedDatas: datas
        });
    }, function(err){/* 数据加载失败 */
        loading.hide();
        
        /* 提示错误后停留在 商品列表 视图 */
        toast(err);
    });
});
```

{% endtab %}
{% endtabs %}

## 开发调测

当进行视图切换时，View\.js 将在 web 控制台中实时输出跳转信息，包括：

1. 跳转方式（压入堆栈、替换堆栈，还是弹出栈顶）
2. 跳转传参
3. 视图的当前浏览位置

例如：

![开发调测1](https://img-blog.csdnimg.cn/20190814230725909.gif)

![开发调测2](https://img-blog.csdnimg.cn/20190814233159676.gif)

此外，View\.js 提供了API：`View.ifCanGoBack()` 来检索当前是否处于栈顶，例如：

![开发调测3](https://img-blog.csdnimg.cn/20190814231252164.gif)

![开发调测4](https://img-blog.csdnimg.cn/20190814233937526.gif)

当浏览位置处于栈顶时，`View.back()` 以及 `data-view-rel=':back'` 在执行时将没有任何反应，开发者可以通过API： `View.setNoViewToNavBackAction(action: Function)` 设定此时的表现，亦即： “没有更多页面可回退时将要执行的动作” 。例如：

{% code title="init.js" %}

```javascript
/**
 * 当执行 View.back() 或 点击 data-view-rel=':back' 元素时，
 * 如果没有更早的浏览信息，浏览器将弹窗提示 “2”。
 */
View.setNoViewToNavBackAction(function(){
    alert(2);
});
```

{% endcode %}

## 其它能力

视图跳转在完成活动视图切换的同时，还具有如下功能：

1. 设置视图跳转动画
2. 在视图之间传递参数
3. 动态设置浏览器标题&#x20;

我们将在后边的章节中分别介绍。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://doc.view-js.com/main.-shi-tu-tiao-zhuan-2.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
