Skip to content
This repository has been archived by the owner on Jun 6, 2019. It is now read-only.

weex中使用数据流工具Vuex实践 #62

Open
kmCha opened this issue Aug 17, 2016 · 13 comments
Open

weex中使用数据流工具Vuex实践 #62

kmCha opened this issue Aug 17, 2016 · 13 comments

Comments

@kmCha
Copy link

kmCha commented Aug 17, 2016

背景

weex刚开源不久,作为一名前端,当然是抑制不住自己的好奇心想要尝尝鲜。虽然weex的最大亮点在于对于电商类应用场景能够提供快速动态部署的功能,但是用js就能写跑在native端的页面更加吸引我。于是在空余时间就开始捣腾着weex,想做一个native app看看weex有什么“能耐”。

在开发过程中,在体会到weex周边工具带来的效率提升的同时,也发现了不少问题。除了weex本身刚开源肯定会存在各种问题之外,还有一些开发体验的问题。weex相关的问题都在GitHub上提了issue,而开发体验的问题只能自己来解决了。

本文主要记录的就是在用weex开发app过程中遇到的一个最大的问题——数据流管理问题。当然这个问题从某种程度上来说也是我“自找的”,毕竟现在weex大多数的应用场景(电商活动页面)的复杂度是不会有这个问题的。但是有想法就去试试也未尝不是一件好事,所以,接下来都是围绕着用weex来写单页app的情景来讨论的。

在写app的过程中,一旦复杂度稍微上升一点,管理应用状态就是个非常痛苦的事情。在没有引入数据流工具之前,在weex里只能通过组件间的通信和传props来控制数据的流动,页面和交互少一点还好,一旦应用的状态多起来,散落在各处的应用状态就是一团乱麻。

现在前端比较火的框架都有配套的数据流工具,比如redux、react的flux、vue的vuex、angular的自己……可见,现在开源社区有很多可用的数据流方案。于是我就琢磨着给weex引入一套数据流工具。由于weex和vue的渊源,在工具选型方面也没怎么纠结,就vue的亲儿子vuex吧。

vuex简介

VuexVue的作者尤小右开发的为Vue服务的数据流工具。其大致思想跟Redux相近,但在API调用和数据流动方式方面还是有一点区别。例如,vuex中,想要改变state的值需要调用store.dispatch('some action')来调用action,这个action跟Redux中的action概念差不多,可以进行异步操作,然后在action中来调用store.commit('some mutation')触发mutationmutation跟redux的reducer相似,对state直接进行操作,不能做异步操作。(从vuex-v2.0.0开始vuex外部也能调用store.commit()来调用mutation)。

引入过程

了解weex从*.we文件到native页面的过程

Weex相关文档里已经很清楚的解释了这一过程:

作为(不会native的)前端,我们写的.we文件经过webpackweex-loader编译以后,变成了native上JS Framework能够识别的JS Bundle,然后之后的生成weex instanceVirtual DOM就已经脱离我们的管辖范围了。也就是说,在前端deploy之前能做的就只有搞搞字符串的“把戏”。这一点很重要,在下面分析vuex的时候会说到。

分析主角vuex

记得之前在GitHub上看到vuex的issue里有人问能不能把vuex用在别的框架里,作者的答复是

Vuex is not coupled with what rendering platform you use.

于是我就天真的安装了vuex并且引入到了weex项目中,直到报错信息中提到没有找到Vue实例才把我打回现实。看来是要改源码了。

在vuex源码中,进行了一个初始化Vue实例的操作(说好的不依赖呢):

// ...
function initStoreState (store, state, getters) {
  // bind getters
  store.getters = {}
  const computed = {}
  Object.keys(getters).forEach(key => {
    const fn = getters[key]
    // use computed to leverage its lazy-caching mechanism
    computed[key] = () => fn(store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key]
    })
  })

  // use a Vue instance to store the state tree
  // suppress warnings just in case the user has added
  // some funky global mixins
  const silent = Vue.config.silent
  Vue.config.silent = true
  store._vm = new Vue({
    data: { state },
    computed
  })
  Vue.config.silent = silent
}
// ...

报错的来源就是这里,从注释得知,作者用了个Vue实例来存储vuex的state tree,当用户直接访问store.state.someState的时候返回这个Vue实例中保存在data属性里的state,当用户直接访问store.getters.someGetter的时候返回这个Vue实例中保存在computed属性里的getter,仅此而已。吗?

其实作者在这里用到Vue实例是“别有居心”的,因为这样就能复用Vue中的watch机制,当data改变,依赖该data的computed函数就会自动重新计算,因此,在store中的state改变之后,getters就会自动被计算了。

既然说要和Vue解耦,那这个watch机制咋办?好在weex也在组件内部复用了Vue的那套watch,所以理论上这个问题是可以解决的。但是正如之前提到的,weex的实例是在native端的JS Framework生成的,我们在前端是访问不到的。但访问不到weex实例不代表我们不能把state和getters引入到weex实例中,因为实例是会根据我们写的.we文件来生成的,所以思路很清晰,我们在.we文件里引入store不就能在组件内访问state和getters了。(vuex源码的改动不仅限于替换Vue实例为普通对象)

// vuex源码
// ...
  store._vm = {
    state,
    getters
  }
// ...
// component.we
<script>
  var store = require('xxx/vuex/store');
  module.exports = {
    data: {
      state: store.state
    },
    computed: {
      getter: function() {
        return store.getters.getter;
      }
    }
  }
</script>

// store.js
var Vuex = require('vuex');
var state = {
  getter: 'getter'
};
var getters = {
  getter: function(store) {
    return store.state.getter;
  }
};
module.exports = new Vuex.Store({
  state: state,
  getters: getters
})

我们将store.stategetter分别注入到了weex组件的datacomputed中,这样我们相当于就利用了weex内部的watch机制,跟之前用Vue实例效果是一样的。虽然能够正常工作,但是我们想要在weex组件中用到vuex的store,那每次我们都需要手动引入store.js文件来访问store。这样显然是不太优雅的,我们来看看Vue是怎么做的,以下代码来自Vuex官方文档

import Vue from 'vue'
import Vuex from 'vuex'

// 告诉 vue “使用” vuex
Vue.use(Vuex)

// 创建一个对象来保存应用启动时的初始状态
const state = {
  // TODO: 放置初始状态
}

// 创建一个对象存储一系列我们接下来要写的 mutation 函数
const mutations = {
  // TODO: 放置我们的状态变更函数
}

// 整合初始状态和变更函数,我们就得到了我们所需的 store
// 至此,这个 store 就可以连接到我们的应用中
export default new Vuex.Store({
  state,
  mutations
})

简单一句Vue.use(Vuex)然后在根组件内部引入store属性就能将store引入到vue的子组件里,通过在vue组件内部调用this.$store就能访问到store,而不是每个子组件引入一次store。但是由于weex没有提供这样的接口,因此又只能自己想办法了。

weex transformer分析

在前端,我们编写的是.we文件,native的JS Framework拿到的是JS Bundle,那从.we到JS Bundle的过程中发生了什么?这就要看transformer做了什么事情了。先看看以下.we文件和它对应的JS Bundle是什么样子的:

// main.we
<template>
  <div class="ct" style="height: {{ctHeight}}">
    <image class="img" style="width: 400px; height: 400px;" src="{{img}}" onclick="handleClick"></image>
  </div>
</template>

<style>
  .ct {
    width: 750;
    align-items: center;
    justify-content: center;
  }
  .img {
    margin-bottom: 20px;
  }
</style>

<script>
  module.exports = {
    data: {
      ctHeight: 800,
      img: '//gw.alicdn.com/tps/i2/TB1DpsmMpXXXXabaXXX20ySQVXX-512-512.png_400x400.jpg'
    },
    methods: {
      handleClick: function() {
        // do something
      }
    },
    created: function() {
      // do something
    },
    ready: function () {
      this.ctHeight = this.$getConfig().env.deviceHeight
    }
  }
</script>

// main.js
  //...
  __weex_module__.exports = {
        data: function () {return {
          ctHeight: 800,
          img: '//gw.alicdn.com/tps/i2/TB1DpsmMpXXXXabaXXX20ySQVXX-512-512.png_400x400.jpg'
        }},
        methods: {
          dispa: function() {
            // do something
          }
        },
        created: function() {
          // do something
        },
        ready: function () {
          this.ctHeight = this.$getConfig().env.deviceHeight
        }
      }

    ;__weex_module__.exports.template = __weex_module__.exports.template || {}
    ;Object.assign(__weex_module__.exports.template, {
      "type": "div",
      "classList": [
        "ct"
      ],
      "style": {
        "height": function () {return this.ctHeight}
      },
      "children": [
        {
          "type": "image",
          "classList": [
            "img"
          ],
          "style": {
            "width": 400,
            "height": 400
          },
          "attr": {
            "src": function () {return this.img}
          },
          "events": {
            "click": "dispa"
          }
        }
      ]
    })
    ;__weex_module__.exports.style = __weex_module__.exports.style || {}
    ;Object.assign(__weex_module__.exports.style, {
      "ct": {
        "width": 750,
        "alignItems": "center",
        "justifyContent": "center"
      },
      "img": {
        "marginBottom": 20
      }
    })
  // ...

可以看出,从.we文件转换到到JS Bundle的过程中,实际上就是对.we文件进行了编译,将他转换成JS Framework能够识别的格式。所以可以理解为,JS Bundle就是格式不同的.we文件,只要.we文件的格式是符合transformer要求的,那么transformer就可以输出符合JS Framework要求的JS Bundle。所以,只要保证在每次转换前的文件格式符合转换器的要求,我们对转换前的文件做怎样的修改都可以。

因此,为了解决需要在每个weex组件内部require(xxx/store)的问题,我们可以写一个loader,在transformer之前将require语句引入到每个.we文件中,这样就解决了这个问题。

另外,要用weex组件的watch机制,就需要在组件的data中引入store.state,所以loader也需要做这个事情。

weex+vuex使用方法

安装weex-vuex-loader

weex-vuex-loader是上面提到的在weex transformer之前处理.we文件的webpack loader。

首先在项目下安装weex-vuex-loader:

npm install --save-dev weex-vuex-loader

然后打开webpack.config.js,在weex-loader之后添加weex-vuex-loader:

  // ...
  module: {
    loaders: [
      {
        test: /\.we(\?[^?]+)?$/,
        loaders: ['weex-loader', 'weex-vuex-loader?store']
      }
    ]
  }
  // ...

后面?之后的store是用户能够自定义的,你可以替换成你想要的变量,这个变量就是在weex组件中通过this访问到的vuex的store。例如上面的例子中,?之后是store,那么在weex组件中就可以通过this.store访问vuex的store

这样做的原因是,本来想像Vue一样,通过this.$store来访问,但是JS Framework的规则不允许weex组件中的data里有$开头的变量,因为这些变量是事先定义好的weex组件的vm的属性。

所以,这里只能将默认的访问方式改为this._store,也就是在weex-vuex-loader后面不指定变量名的默认情况。考虑到不是人人都喜欢这样的访问方式,干脆就改成能够自定义了。

安装vuex-weex

npm install --save vuex-weex

vuex-weex是为了让Vuex适配Weex,更改了源码之后的Vuex。然后在项目根目录下src/vuex/中添加store.js文件,写法参考vuex-v2.0.0-rc.1。由于现在weex还不支持ES6语法,因此store.js中还是用ES5来写吧:

// src/vuex/store.js
var Vuex = require('vuex-weex');
var state = {your state object};
var mutations = {
  someMutation: function(state, payload) {
    // modify some state
  }
};
var actions = {
  someAction: function(store, payload) {
    var commit = store.commit;
    // async operations
    commit('someMutation', payload);
  }
};
var getters = {
  someComputedState: function(store) {
    return compute(store.state.someState);
  }
};
module.exports = new Vuex.Store({
  state: state,
  mutations: mutations,
  actions: actions,
  getters: getters
});

需要注意的是,这里getter的第一个参数是store,并不是Vuex中原本的state

编写.we文件

完成了上面两个步骤之后,就能愉快地在.we文件中使用vuex了:

// webpack.config.js
//...
module: {
  loaders: [
    {
      test: /\.we(\?[^?]+)?$/,
      loaders: ['weex-loader', 'weex-vuex-loader?store']
    }
  ]
}
//...

// store.js
var Vuex = require('vuex-weex');
var state = {
  count: 0
};
var mutations = {
  inc: function(state) {
    state.count++;
  }
};
var getters = {
  wrappedCount: function(store) {
    return store.state.count + '次';
  }
};
module.exports = new Vuex.Store({
  state: state,
  mutations: mutations,
  getters: getters
});

// component.we
<template>
  <text>{{count}}</text>
  <div onclick="inc">递增</div>
</template>

<script>
  module.exports = {
    computed: {
      count: function() {
        return this._store.getters.wrappedCount;
      }
    },
    methods: {
      inc: function() {
        this._store.commit('inc');
      }
    }
  }
<script>

还存在的问题

Vue中的action是支持异步操作的,返回Promise即可,weex也在native端支持了Promise,看起来不应该有什么问题。但是我在使用过程中发现,在action中加入异步操作之后,在then()中调用了commit()来触发某个mutation之后,虽然mutation会执行(例如改变某个state),并且weex组件中调用了依赖这个state的getter的computed函数也会执行,但是视图是不会更新的,除非你再触发一个事件(比如click事件),之后视图才会更新。

所以,为了避免这个问题,建议将异步操作放在weex组件内部,官方也提供了streamAPI来支持异步操作。

说到stream模块,在目前的weex-v0.6.1中,在ios端传给回调函数的response的类型是string而不是object,所以在处理之前需要先JSON.parse(response)。这个BUG将会在下个版本被修复。

这样就能如丝般顺滑地体验weex+vuex了。

@keelii
Copy link

keelii commented Aug 17, 2016

中英文之间没空格看着真难受 ( ಠ ಠ )

@sunfuze
Copy link

sunfuze commented Aug 17, 2016

其实我在开始用的weex的时候,我以为语法和vue是差不多的,后来发现,某些相同的用法在weex确是另一个语法,我很费解?看了这篇文章,既然weex-transformer就是将we生成你们js-framwork需要的js代码,那么为什么不直接做一个vue代码transform到你们这个js-framwork,这样不是更省力?还有为什么既然使用了transform为什么不一步到为,让we支持es6?(非质疑,只是困惑,想了解下里面的技术考量)

@agileago
Copy link

最好是和vue的语法是一致的,要不然其实这是学2样不同的东西

@kmCha
Copy link
Author

kmCha commented Aug 17, 2016

@sunfuze 其实这也是我在写这篇总结之前的疑惑,这个可能要等weex团队的同学来回答你了。。这个笨办法是在不改动JS Framework的情况下使用的

@kmCha
Copy link
Author

kmCha commented Aug 17, 2016

@agileago 如果官方把支持vuex的代码集成到了JSFramework中就不用这么费劲了。。这个是目前情况下的一个笨办法 只是想尝尝鲜

@Jinjiang
Copy link
Contributor

@agileago @kmCha 我们今天开搞!

@JimLiu
Copy link

JimLiu commented Aug 17, 2016

我只想问问图用啥软件画的呀?
image

@Jinjiang
Copy link
Contributor

已经有个样子了,Weex-X 这儿 https://github.com/Jinjiang/weex-x

@kmCha
Copy link
Author

kmCha commented Aug 18, 2016

@JimLiu OmniGraffle

@transtone
Copy link

@sunfuze 和 vue 相同的语法在这个分支:https://github.com/alibaba/weex/tree/jsfm-feature-vue

@aosp-11-lubancat
Copy link

aosp-11-lubancat commented Aug 30, 2016

this._store是undefined 项目是在哪里实例化?? 能否写一个demo?

@MeishanGuo
Copy link

@Jinjiang 请问现在weex支持vuex、vue-router了不?

现在可不可以实现vue开发项目(Vue + VueRouter + VueX + VueSource)轻松的在native跑?

@yundongbot
Copy link

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants