mip-project / mip

MIT License
11 stars 1 forks source link

mip-router 设计方案 #2

Open easonyq opened 6 years ago

easonyq commented 6 years ago

mip-router 设计方案

mip-router 用来处理多个 MIP 页面间的跳转,主要功能包括:

mip-router 其实是一段 JS。鉴于 MIP 2.0 必然依赖 mip-router 和 mip,因此 mip-router 的代码就直接包含在 mip.js 内部,不再额外独立成单独的文件了

SPA 融合方案

mip-router 的核心之一是将多个单独的 MIP 页面融合为 SPA。实现方案如下:

准备工作

mip-router 要求页面以一定的结构使用两个标签 <mip-content><mip-shell>

<mip-content> 一般位于 <body> 的一个子节点,用以标识哪一段是页面的主体内容,区别于其他内容(如 AppShell,script 引用等等)。

<mip-shell> 和 AppShell 的实现有关,将在后面详述。

举例来说,一个可能的页面结构是:

<html>
    <head>
        <!-- meta & link & title & mip-custom style -->
    </head>
    <body>
        <mip-shell id="title">{{ titleText }}</mip-shell>
        <mip-content>
            <mip-product-list :data="list"></mip-product-list>
            <mip-img :src="logoSrc"></mip-img>
        </mip-content>
        <script src="https://c.mipcdn.com/static/v2/mip.js"></script>
        <script src="https://c.mipcdn.com/static/v2/mip-product-list/mip-product-list.js"></script>
    </body>
</html>

<mip-content> 只能存在一个<mip-shell> 可以存在多个。如果页面不包含 <mip-content>,则 <body> 下的所有除 <mip-shell><script> 之外的节点全部认为属于 <mip-content> 下。

首次打开时

  1. mip-router 创建一个空数组,并把当前 URL 记录到其中
  2. 页面头部的 <style mip-custom> 和尾部的几个 <script> 标签保留,维持原状。
  3. mip-router 获取当前页面 <mip-shell><mip-content> 里的内容,传递给 Mip 构造函数(由 mip-core 提供)
  4. mip-core 将这段内容解析成组件树 注意:根据目前的架构,这段内容可能是站长拼接的原始模板,也可能是由 spider 执行过模板替换和 syncData 方法之后的模板,我们称之为中间模板
  5. mip-router 调用实例的 mount 方法进行挂载。挂载点可以设置在原 <mip-content>,则将内部的原始模板清空;也可以在其平级创建一个新的挂载点,同时将 <mip-content> 节点删除。

后续跳转页面

如果跳转到站外,则使用 <a> 标签,和普通站点一样跳转。

如果跳转到站内,则使用 <router-link> 标签,mip-router 的操作步骤如下:

  1. mip-router 把目标页面的 URL 记录到数组中
  2. mip-router 获取目标页面头部的 <style mip-custom> 的内容,替换当前页面的同名标签
  3. mip-router 获取目标页面 <body> 底部的 <script> 标签,和当前页面的同名标签进行比较:相同的保留;不同的替换。
  4. mip-router 获取目标页面 <mip-shell><mip-content> 里的内容,传递给 compile 函数(由 mip-core 提供)
  5. mip-core 将这段内容解析成组件树(和首次打开相同)
  6. mip-router 调用实例的 mount 方法进行挂载。这里为了过场动画(默认右向左滑动),需要在右侧重新建立一个容器作为新的挂载点
  7. 记录原有页面的滚动位置,存在内存中(为了保留滚动位置)
  8. 设置并执行过场动画
  9. 可以将原有页面从 dom 树中去掉,只在内存中保留节点,直到超越存储数量限制才销毁。

如果是前进/后退,基本过程类似,从数组中找到上一个页面的 URL,并执行跳转站内的逻辑。略有不同的点在于:

  1. 后退时滑动方向相反
  2. 如果目标页面被记录过滚动位置,则尝试滚动到该位置再进行过场动画。但考虑到类似 scroll loading 之类的组件存在,可能这个滚动需要尝试多次,甚至有失败的可能性。

AppShell 的支持

AppShell 不是 mip-router 的一部分,但是它的支持方案和 mip-router 息息相关。

第一版

考虑到排期压力,第一版规定每个页面都有一个固定的 AppShell,即头部标题栏

因此在每个页面的 <body> 中,都应该拥有这样一个标签:

<mip-shell id="title">{{ titleText }}</mip-shell>

其中 id 属性是必填项,用以区分 AppShell。{{ titleText }} 用以将标题内容绑定到一个变量上,仅为示例,以实际实现为准。

不论是首次打开还是切换页面,mip-router 会获取当前/目标页面的 <mip-shell> 的内容并传递给 mip-core 进行解析。挂载之后再将其移动到挂载点外的平级。(这里应该有更好的做法,既渲染 shell 内容,又不至于让 shell 一开始被挂载到 content 的内部)

由于 <mip-shell> 和挂载点平级,因此可以脱离过场动画之外。

完整方案(草案)

  1. AppShell 是脱离在过场动画之外的,但前提是原页面和目标页面的 <mip-shell>id 属性是相同的。如果不同,那么 AppShell 一样将被退场。
  2. 考虑到 AppShell 位置的不确定性(可能头部,尾部,侧边栏等等),退场动画建议使用 fade。或者如果有办法确定位置,那么使用滑动也更好,如底部导航栏,可以由上往下退场。而确定位置的办法就可以添加一个属性,如 <mip-shell position="bottom">
  3. 可以考虑提供几个预设的 AppShell,毕竟作为站长真正有强烈的动力去开发自定义的 AppShell 的比例应该并不高
  4. 开发者如果要实现在直接打开时和搜索时两种情况出不同的 AppShell,可以采用 use 属性进行标识。如下:

    <mip-shell id="title" use="search">{{ titleText }}</mip-shell>
    <mip-shell id="title" use="standalone">{{ anotherTitleText }}</mip-shell>

    通过使用相同的 id 和不用的 use 来实现两者的差别和替换。如果开发者在直接打开时不需要这个 Shell,也可以不写第二句。默认情况下不使用 use 属性的即认为两种情况都有且内容相同。

<mip-shell> 目前拥有的属性集合:

和 mip-core 的交互

  1. mip-core 能够解析字符串格式的两种模板:原始模板和中间模板
  2. mip-core 提供构造函数,用以首次打开时创建 mip 实例
  3. mip-core 提供后续的编译函数 compile,用以后续跳转链接时传入新的模板。注意这里并不创建新的实例,而是全局只有一个 mip 实例
  4. mip-core 提供一些方法让 mip-router 有办法给每个页面/组件提供 router 对象并实时更新,使它们能够获取当前 URL 信息

存疑

  1. mip-router 对 shell 的处理应该有更好的做法,既渲染 shell 内容,又不至于让 shell 挂在到 content 的内部。因为 Vue 就可以做到渲染 <router-view> 平级的 shell
PengXing commented 6 years ago
  1. mip-content 这个标签不一定存在,如果兼容 MIP 1.0,这个标签就不存在,可以在 body 下将 mip-shell 标签去掉之后,剩下的就是 mip-content
  2. mip-shell 应该只存在一个,如果存在多个,如何将站点自己的 Shell 和搜索里的 Shell 映射上,或者如果能达到搜索端和独立站点同样的控制粒度,也可以多个 mip-shell?
  3. 考虑存储多个 Page,在切换到另一个页面之后,可以将前面的页面从 dom 树中去掉,只在内存中保留节点,否则确实会页面滚动滑动等动画的平滑
  4. shell 由谁来渲染,由谁来控制生命周期?
easonyq commented 6 years ago

对于4个问题

  1. 如果页面不包含 <mip-content>,则 <body> 下的所有除 <script> 之外的节点全部认为属于 <mip-content> 下。

    我想表达的意思和你是相同的,但漏写了除 <mip-shell>,已经补充。

  2. 我当时的考虑是不论在哪个环境,都出相同的 AppShell。但现在看来直接访问用户站点,可以根据用户的情况出不同的 Shell是一种功能,那补充一下设计:

    开发者如果要实现在直接打开时和搜索时两种情况出不同的 AppShell,可以采用 use 属性进行标识。如下:

    <mip-shell id="title" use="search">{{ titleText }}</mip-shell>
    <mip-shell id="title" use="standalone">{{ anotherTitleText }}</mip-shell>

    通过使用相同的 id 和不用的 use 来实现两者的差别和替换。如果开发者在直接打开时不需要这个 Shell,也可以不写第二句。默认情况下不使用 use 属性的即认为两种情况都有且内容相同。

    回到问题 2,这样的补充设计我想应该也是支持多个 Shell 的。支持多个的原因主要在于 position 属性,因为根据不同的 position 在退场时采取不同的方向,比起简单的 fade 看上去更加好看和合理一些。

  3. 这个提议非常好。至于在内存中存储多少个 Page,我也没有经验。先暂定一个数字,以观后效。

  4. 这个问题就是我最后列出的存疑点。我的想法如下:

    1. 因为 Shell 和 Content 挂载点不同,因此萌生了多个 Mip 实例的想法。但开发者编写时是写在一起,并且 data 只有一份,且 vue-router 是只有一个实例的,因此这个方案应该不现实。

    2. 既然仍然是一个实例,那么挂载点只能在 <mip-content>,那就会导致 AppShell 也一起被挂到内容里面,还需要额外用 js 把它取出来,这个做法太 LOW 且不确定是否有闪动的可能性。我就比较好奇 Vue 是如何完成既渲染 <router-view> 的节点,又能渲染 <router-view> 平级的 Shell 节点,且都在同一个实例中。这个我会继续研究。

clark-t commented 6 years ago

组件能不能访问 this.$router this.$routes 等对象?

easonyq commented 6 years ago

@clark-t 能,功能设计第二点

为每个页面提供当前路由信息,如当前 URL、参数、域名、hash 等等

就是这个。我会把 route 信息放到 Mip.prototype 上就可以使用了

tayqassqan commented 6 years ago

App shell设计和搜索侧的是否能兼容,否则后续在搜索侧以及站点单独打开时技术能力表现会不一致。

PengXing commented 6 years ago

App Shell 和之前讨论的设计方式一致,搜索侧用另外的 App Shell 组件替代渲染

tayqassqan commented 6 years ago

当前站点希望流畅切换到下一个站点,这里是不是得考虑Iframe方案?或者说我们体验上就是要跳转。

另外,如果是集成熊掌号的登录和支付相关的能力,也不能是当前上下文渲染吧?

PengXing commented 6 years ago

切换到不同站点会直接跳出,不会用 iframe 包裹起来

tayqassqan commented 6 years ago

这个很有可能是需求,能满足吗?如果不能,我理解可能需要重新考虑设计。 @PengXing @windtalkers

windtalkers commented 6 years ago

我不建议把mip-shell作为core的一部分,集成到mip的核心代码里面,本质上,mip-shell还是一个组件,router是实现组件用的一个库,我们这里需要首先讨论清楚并达成一致的是:

  1. 我们的设计是一个shell需要考虑哪些东西?

  2. 这些东西在shell里面怎么DSL抽象?

  3. 用户怎么使用这些DSL抽象进行定制shell功能?

easonyq commented 6 years ago

@windtalkers 我们也同意 mip-shell 是一个组件。但目前它是 mip-router 会用到的组件,虽然和内置组件(mip-img 等等)实现上略有不同,但最终也是打包到 mip.js 里面。我想我们分歧的点应该在于 mip-shell 的代码应不应该打包到 mip.js 里面 对吗?

关于设计思路方面,我们最终方案是让用户去选择预先提供的几种 Shell 的一种,然后提供需要的内容(例如通过 slot 之类的机制提供模板,并提供初始数据)。MIP 把它当成是个普通组件进行渲染,进行进场退场动画的注册和进行。通过 store 进行数据的管理,Shell 的显示由 store 里面某些数据的变化而变化。

windtalkers commented 6 years ago

 设计思路我觉得没有问题,我关注的点是mip-shell这个组件的dsl设计,我们的重点是怎么设计mip-shell。那么我这里为什么坚持shell是一个组件的重要原因就是,shell的dsl是规范固定的,那么,基于这个DSL,搜索结果页和全站mip的情况,就可以做不通的处理。