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吧。
作为(不会native的)前端,我们写的<code>.we</code>文件经过<code>webpack</code>的<code>weex-loader</code>编译以后,变成了native上js framework能够识别的js bundle,然后之后的生成<code>weex instance</code>和<code>virtual dom</code>就已经脱离我们的管辖范围了。也就是说,在前端<code>deploy</code>之前能做的就只有搞搞字符串的“把戏”。这一点很重要,在下面分析<code>vuex</code>的时候会说到。
记得之前在github上看到<code>vuex</code>的issue里有人问能不能把vuex用在别的框架里,作者的答复是
vuex is not coupled with what rendering platform you use.
于是我就天真的安装了<code>vuex</code>并且引入到了<code>weex</code>项目中,直到报错信息中提到没有找到<code>vue</code>实例才把我打回现实。看来是要改源码了。
在vuex源码中,进行了一个初始化vue实例的操作(说好的不依赖呢):
报错的来源就是这里,从注释得知,作者用了个<code>vue</code>实例来存储vuex的state tree,当用户直接访问<code>store.state.somestate</code>的时候返回这个<code>vue</code>实例中保存在<code>data</code>属性里的<code>state</code>,当用户直接访问<code>store.getters.somegetter</code>的时候返回这个<code>vue</code>实例中保存在<code>computed</code>属性里的<code>getter</code>,仅此而已。吗?
其实作者在这里用到<code>vue</code>实例是“别有居心”的,因为这样就能复用<code>vue</code>中的<code>watch</code>机制,当data改变,依赖该data的computed函数就会自动重新计算,因此,在store中的state改变之后,getters就会自动被计算了。
既然说要和<code>vue</code>解耦,那这个watch机制咋办?好在weex也在组件内部复用了<code>vue</code>的那套watch,所以理论上这个问题是可以解决的。但是正如之前提到的,weex的实例是在native端的js framework生成的,我们在前端是访问不到的。但访问不到weex实例不代表我们不能把state和getters引入到weex实例中,因为实例是会根据我们写的<code>.we</code>文件来生成的,所以思路很清晰,我们在<code>.we</code>文件里引入store不就能在组件内访问state和getters了。(vuex源码的改动不仅限于替换vue实例为普通对象)
简单一句<code>vue.use(vuex)</code>然后在根组件内部引入<code>store</code>属性就能将store引入到vue的子组件里,通过在vue组件内部调用<code>this.$store</code>就能访问到store,而不是每个子组件引入一次store。但是由于weex没有提供这样的接口,因此又只能自己想办法了。
在前端,我们编写的是<code>.we</code>文件,native的js framework拿到的是js bundle,那从<code>.we</code>到js bundle的过程中发生了什么?这就要看transformer做了什么事情了。先看看以下<code>.we</code>文件和它对应的js bundle是什么样子的:
可以看出,从<code>.we</code>文件转换到到js bundle的过程中,实际上就是对<code>.we</code>文件进行了编译,将他转换成js framework能够识别的格式。所以可以理解为,js bundle就是格式不同的<code>.we</code>文件,只要<code>.we</code>文件的格式是符合transformer要求的,那么transformer就可以输出符合js framework要求的js bundle。所以,只要保证在每次转换前的文件格式符合转换器的要求,我们对转换前的文件做怎样的修改都可以。
因此,为了解决需要在每个weex组件内部<code>require(xxx/store)</code>的问题,我们可以写一个loader,在transformer之前将<code>require</code>语句引入到每个<code>.we</code>文件中,这样就解决了这个问题。
另外,要用weex组件的watch机制,就需要在组件的data中引入<code>store.state</code>,所以loader也需要做这个事情。
首先在项目下安装weex-vuex-loader:
然后打开<code>webpack.config.js</code>,在weex-loader之后添加weex-vuex-loader:
后面<code>?</code>之后的<code>store</code>是用户能够自定义的,你可以替换成你想要的变量,这个变量就是在weex组件中通过<code>this</code>访问到的vuex的<code>store</code>。例如上面的例子中,<code>?</code>之后是<code>store</code>,那么在weex组件中就可以通过<code>this.store</code>访问vuex的<code>store</code>。
这样做的原因是,本来想像<code>vue</code>一样,通过<code>this.$store</code>来访问,但是js framework的规则不允许weex组件中的data里有<code>$</code>开头的变量,因为这些变量是事先定义好的weex组件的vm的属性。
所以,这里只能将默认的访问方式改为<code>this._store</code>,也就是在<code>weex-vuex-loader</code>后面不指定变量名的默认情况。考虑到不是人人都喜欢这样的访问方式,干脆就改成能够自定义了。
需要注意的是,这里getter的第一个参数是<code>store</code>,并不是vuex中原本的<code>state</code>。
完成了上面两个步骤之后,就能愉快地在.we文件中使用vuex了:
vue中的action是支持异步操作的,返回promise即可,weex也在native端支持了promise,看起来不应该有什么问题。但是我在使用过程中发现,在action中加入异步操作之后,在<code>then()</code>中调用了<code>commit()</code>来触发某个mutation之后,虽然mutation会执行(例如改变某个state),并且weex组件中调用了依赖这个state的getter的computed函数也会执行,但是视图是不会更新的,除非你再触发一个事件(比如click事件),之后视图才会更新。
说到stream模块,在目前的weex-v0.6.1中,在ios端传给回调函数的<code>response</code>的类型是<code>string</code>而不是<code>object</code>,所以在处理之前需要先<code>json.parse(response)</code>。这个bug将会在下个版本被修复。
这样就能如丝般顺滑地体验weex+vuex了。
下面gif中就是这几天用weex+vuex写的模仿“口袋记账app”的单页应用,跑在ios playground上