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

简介: # 背景 weex刚开源不久,作为一名前端,当然是抑制不住自己的好奇心想要尝尝鲜。虽然weex的最大亮点在于对于电商类应用场景能够提供快速动态部署的功能,但是用js就能写跑在native端的页面更加吸引我。于是在空余时间就开始捣腾着weex,想做一个native app看看weex有什么“能耐”。 在开发过程中,在体会到weex周边工具带来的效率提升的同时,也发现了不少问题。除了wee

背景

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

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

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

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

现在前端比较火的框架都有配套的数据流工具,比如react的redux、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了。

下面gif中就是这几天用weex+vuex写的模仿“口袋记账APP”的单页应用,跑在IOS Playground上

目录
相关文章
|
移动开发 缓存 编解码
OTT端性能优化建设之Weex实践之路 | 《优酷OTT互联网大屏前端技术实践》第三章
追求极致的用户体验是个永恒的话题。无论在PC端、移动端,还是IOT端,大家都在尝试着各种技术方案,如提高秒开率,降低白屏时间等等。 在OTT端进行营销活动开发的我们,也面临这一挑战,尽管PC端和Mobile端都有成熟的技术方案,但是到了大屏端,由于终端的差异性,很多技术方案不能完全照搬照抄。 回顾优酷在OTT端的用户体验探索之路,经历了三个阶段:webview时期、自定义内核Blitz时期、weex阶段。
986 0
OTT端性能优化建设之Weex实践之路 | 《优酷OTT互联网大屏前端技术实践》第三章
|
移动开发 前端开发 JavaScript
Weex黑科技——提升用户体验实践
在2017年1月12日 Weex Conf 2017上,来自阿里的寒泉结合Weex的背景、动画和手势的技术分析,从Weex存在的实际问题入手,提出了新的构想和新方案expression binding。
8080 0
|
移动开发 前端开发 weex
Weex在千牛开放中的应用实践
在2017年1月12日 Weex Conf 2017上,阿里巴巴商家事业部无线千牛团队的无灵结合阿里巴巴无限商家端的实际业务分享了Weex在千牛开放中的应用实践,本文分享了面对业务的各种挑战,无线千牛团队是如何一步步转向Weex的,以及在实际过程中遇到挑战和所做的努力。
6156 0
|
移动开发 weex 双11
Weex动态化方案与双十一实践
在2017年1月12日 Weex Conf 2017上,来自手机淘宝移动平台Weex团队的凝砺结合淘宝实际业务分享了Weex动态化方案和双十一实践,本文先介绍了Weex的整体架构,接着重点分享了Weex在双十一会场上的实践,最后谈及了Weex的业务支撑,包括AliWeex等。
10184 0
|
移动开发 weex Android开发
手机淘宝 H5 和 Weex容器的构建实践
本文PPT来自阿里巴巴级前端开发专家徐凯(花名:鬼道)于10月16日在2016年杭州云栖大会上发表的《手机淘宝 H5 和 Weex容器的构建实践》。
6116 0