mip-project / mip

MIT License
11 stars 1 forks source link

mip-store 设计方案 #7

Open PengXing opened 6 years ago

PengXing commented 6 years ago

背景

有必要先阐述一下传统的 Vue 项目和 MIP2.0 的设计的不同之处。

Vue 的渲染模式

完全是 SPA 的,所有资源都在第一次加载初始化,特点有这些

  1. store 初始化在统一的地方书写
  2. 整个页面的生命周期 store 只会加载一次,页面切换不会导致 store 重新初始化或者变化
  3. namespace 也是在编写 store 的时候写好
// store/index.js
export default new Vuex.Store({
    modules: {
        appShell,
        user
    }
});

MIP2.0 的渲染模式

MIP2.0 的渲染模式和 Vue 有本质的区别,是将多个(或单个)通过 customElement 堆积而成的 HTML 页面通过 runtime/router 用 JS 在一个 webview 里渲染,每个 HTML 的页面也能直接访问,因此,每个单独的 HTML 页面都需要一个自身能够运行的所有资源的方案,包括 runtime/router, store, mip-elements.js 等等,这里我们不讨论怎么避免重复加载 runtime 或者 elements 的资源,集中在 store 上。

MIP 2.0 的 store 的需求和问题

综上所述,MIP2.0 的 store 有几个需求:

  1. 每个 HTML 页面都有自己完整的 store,因为需要能独立运行
  2. 多个页面 store 需要能共享数据,因为有多页面操作同一个对象的需求
  3. MIP2.0 的页面不会包含整站所有的 store 初始化逻辑,每个页面只需要自己的自定义组件关心的 store
  4. 每个页面如何区分自己要的那些 store
  5. store 的初始化哪里书写?
let state = {
    /**
     * 多个页面切换效果名称
     *
     * @type {string}
     */
    pageTransitionName: '',

    /**
     * 上个页面 scroll 的信息
     *
     * @type {Object}
     */
    historyPageScrollTop: {}
};

export default {
    state
};

MIP2.0 store 的设计

  1. 用户浏览的一次 Session 只有一个 store 对象
  2. 由于 MIP2.0 项目没有统一的地方书写该项目中用到的所有 store,那么,将 store 的初始化逻辑移到组件内,作为组件的逻辑,类似于下面的代码,只有组件第一次加载(非执行)才需要执行 initStore
<!-- mip-a.vue -->
<template></template>
<script>
export default {
    initStore() {
        return {
            namespace: 'appshell',
            state: {
                historyPageScrollTop: {}
            },
            mutations: {},
            modules: {}
        };
    }
};
</script>
  1. store 的数据还通过 mapState 的方式来使用,方式不变
  2. 同名 namespace 做第一级的 merge 操作
xiaoiver commented 6 years ago

我有一个问题:如果每个组件只需要用自己的数据,是不是就和 props 一样了。

huanghuiquan commented 6 years ago
  1. initStore 不就是 vue 组件的 data 吗?
  2. 突然想到不让开发者写 js 怎么让开发者处理 store 的逻辑啊
clark-t commented 6 years ago

@huanghuiquan 组件内部可以写 js

ccksfh commented 6 years ago

我理解这里的 initStore 不是为了构建组件自己的 data,而是类似于向页面的 store 去 register,从而构建页面那个 store?这样 store 感觉就是按照业务模块来组织起来的了,比如

<mip-content>
    <mip-a>
        {
            name: '',
            list: [],
            objNeedShare: {}
        }
    </mip-a>
    <mip-b>
<mip-content>

store: {
    state: {
        'mip-a': {
            name: '', ...
        },
        'mip-b': {}
    }
}

这里还有个问题:merge 操作的时候,页面间要共享的数据也未必是同级对应的吧?还是说这里需要一个规则,如果想要共享数据,在 state 树的位置就要对应?而且按前面的理解,store 是按照页面模块组织的,要共享数据的话,似乎也不太可能百分百要求页面之间的模块就存在对应关系,再找到模块下的 state 来 merge

我原来的想法是全局 store,然后因为我们实际上是 mpa,我们不能一下子获取所有页面的 store 信息,可是我们应该可以在用户的访问过程中逐步给全局 store 增加 module(这些 module 对应页面),可是问题又在于如果站内有很多页面,store 就很非常大,而且数据可能会存在冗余。不过这个方法倒是比较直接,也不会存在需要通知另一个页面我改变了什么数据的操作问题

PengXing commented 6 years ago

@huanghuiquan @ccksfh ck 理解的对,是向全局的 store 去 register 一个 namespace 的初始值

按照组件来行不通吧,一个页面包括多个同名的组件,怎么做 store 的设计?按照 Vue 的还应该是按照业务模块来

所以,store 并不是没有工作量的

PengXing commented 6 years ago

@xiaoiver

一个子组件需要获取数据,有几种方式

  1. 通过 mapState 直接从 store 中获取
  2. 通过父组件的 props 传过来,父组件传递过来的数据可以是自己的 data 里的,也可以是从 store 中拿来的
PengXing commented 6 years ago

根据4 月 27 日上午的会议决定,MIP2.0 的 store 沿用 vuex 和 vue 的项目,保持全局统一的地方书写 store 及初始化,考虑的原因主要有以下:

  1. 多是业务组件才会有用到 store 的需求
  2. 一个 MIP 页加载之后,跳到下个 MIP 页,还是在同一个上下文中,不需要重新初始化 store,用户也很难跳出 MIP 的流程
  3. components 管理 store 的方式有一个难以解决的问题,如果两个组件共用一个 namespace 下的数据,那两个 components 都要在自己的组件里写 state actions mutations,会有不少冗余,如果把这部分操作独立成 JS 文件,又和全局 store 差不多

综上所述,我们依然保留 vuex 的全局 store 的逻辑

用户在调用 mip2 publish 的时候,将带有 components, store, common, static, package.json 的项目提交到 MIP 端进行审核

OVER.

tayqassqan commented 6 years ago

mip 1.0页面是被iframe隔离渲染的,如果2.0保留此机制,全局的store共享需要考虑iframe数据传递。

PengXing commented 6 years ago

MIP2.0 暂定是在同一个上下文进行渲染,不需要 iframe 数据传递,后续如果同一上下文渲染有问题,再来考虑放在 iframe 进行渲染

tayqassqan commented 6 years ago

搜索侧也是同一个上下文渲染吗?

PengXing commented 6 years ago

同一个 host 同一个上下文

ccksfh commented 6 years ago

基于以上 mip-store 的基本方案制定以下规范:

  1. 开发者可以在提交项目的时候同时提交 store 文件夹,并在文件夹中创建单独的模块文件, mip 会将这些单独的模块组合起来,生成最终的 Vuex.Store 实例。mip 默认一个文件代表一个模块,以目录/文件名作为命名空间,可嵌套,默认 namespaced: true;若发现 index.js 模块,默认注册到全局;每个模块至少包含 state,而 state 必须是一个返回初始状态的方法。lavas/nuxt 的方式

    示例:

    
      // user.js
      export const state = () => {
          return {
              users: []
          };
      };
    
      export const mutations = {
          addUser(state, userInfo) {
              state.users.push(userInfo);
          }
      };

...


2. 对于开发者需要用到的全局初始数据,我们鼓励开发者使用 mip-store 组件设置数据源。要求 mip-store 不得被嵌套在其他标签下。使用 mip-store 主要有两种方式:

    1. **同步数据**
        在这里我们要求开发者必须把同步数据放在 **_model_** 层级下

            <mip-store>
                <script type="application/json">
                {
                    "model": {
                        "name": "张三",
                        "age": 25,
                        "job": {
                            "desc": "互联网从业者",
                            "location": "北京"
                        }
                    }
                 }
                </script>
            </mip-store>

    2. **异步数据**
        如果需要异步数据,则需指定 src 地址(注:src 需要是 https 或 // 协议开头,否则在 HTTPS 环境下会出现问题),如 

            <mip-store src="https://www.example.org/data"></mip-store>

    熟悉 mip1 的朋友应该比较了解,与 mip-data 类似

    mip-store 获取到数据后会把数据放到全局 store 的 **global** 命名空间下,开发者可在任意页面和任意组件使用其中的数据。

    mip-store 提供了 **global/setData** 的 mutation 方法,方便开发者更改 global 空间下的数据。该方法接受一个 _Object_ 作为参数,示例:
```javascript
state {
    detail: {
        info: {
            name: 'detail-1',
            content: '',
            desc: ''
        }
    }
}

...mip.Store.mapMutations('global', ['setData'])

this.setData({
    detail: {
        info: {
            name: 'detail-2'
        }
    }
})
  1. 考虑到某些特殊情况(需要提升成内置却又自带 store 的组件),我们提供 initStore 的生命周期钩子,开发者可以通过这个钩子在组件加载的时候动态向全局 store 注册 store module,要求书写方式如下:

    initStore() {
       return {
            // 指定命名空间,如果需要多级命名空间的就用 / 分割,比如 'appshell/appheader',模块默认 namespaced: true
            namespace: '',
            // 模块注册内容规范参照本规范第一条
            module: {
                state: () => {
                    return {};
                },
                getters: {},
                mutations: {},
                actions: {}
            }
        }
    }
  2. 我们提供全局的 mip.Store,开发者可以通过调用 mip.Store.mapState/mapGetters/mapMutations/mapActions 等方法,或者在组件中调用 this.$store.dispatch 等方法,就像使用 vuex 一样管理数据

  3. 目前 mip-store 组件并非内置组件,要求对这个组件的引用必须在 mip.js 之后,其他自定义组件之前。(是否应该内置还有待商榷) 已内置

欢迎讨论


基于 2018.5.14 的会议,store 设计方案改为类似单向数据流的设计,以上规范作废。移除 vuex 相关代码、移除 mip-store 内置组件、移除 initStore 生命周期钩子、组件提交不需要提交 store 文件夹。

easonyq commented 6 years ago

补充一句: 目前 mip-store 单独的例子在 examples/store/index.html 集成在 SPA 环境中的例子在 examples/page/data.html

均包含了同步和异步两种设置和获取数据的方式。提交 store 文件夹的方式在 page 的例子中暂不包括。待确定最终引入方式(可能 @clark-t @ccksfh )之后再行确定。

ccksfh commented 6 years ago

mip-store 单向数据流方案规范:

沿用 mip1 的 mip-data 和 mip-bind,在此基础上增加

  1. 支持开发者在 MIP 页面中编写 JS 操作 state,要求使用以下 type 作为标识:

    <script type="application/mip-script"></script>

    在 script 中仅允许操作 state,建议操作有

    1. 观察数据变化

      MIP.watch([数据字段,以 . 分割]: String | Array,  cb(newVal, oldVal))
    2. 修改全局数据

      MIP.setData()
  2. 支持多页共享 state:在需要全局共享的数据前添加 # 标识(仅检测数据第一层),如

    <mip-data>
        <script type="application/json">
            {
                "#globalState": {},
                "pageState": []
            }
        </script>
    </mip-data>

    提升为全局态的 state 将在每个页面都能读取到,如果页面自身有与全局 state 的数据冲突的字段,将优先读取页面数据,页面数据不存在将读取全局数据

    调用 MIP.setData 修改数据时,不需要给数据带上 # 标识

  3. 基于单向数据流的设计,我们给出的最佳实践如下:

    a.html

    <mip-a m-bind:globaldata="globalState"></mip-a>

    mip-a 组件内部

    template: `
        <div>
            <mip-b :globaldata="globaldata"></mip-b>
        </div>
    `,
    props: {
        globaldata: {
            type: Object
        }
    }

    组件内部 不允许 使用 m-bind: 语法来绑定全局数据

    组件内部可以通过调用 MIP.setData 来修改全局数据,重新渲染组件