浅谈使用Vue开发Weex——以智能硬件项目为例

介绍

weex是阿里出品的一个类似RN的框架,可以使用前端技术来开发移动应用,实现一份代码支持H5,iOS和Android。最新版本的weex已默认将Vue.js作为前端框架,而wisdom-medical-hardware则是我第一次使用Weex和Vue开发的原生应用,在项目中使用了vue-router等官方组件。下面我们以该项目代码为例,具体了解如何使用weex技术栈进行开发,实现同一份代码在 iOS、Android下都能完整地工作。

这是我Weex开发初体验,其中难免出现常识性的纰漏,还请各位指正~

效果

WechatIMG18.jpeg
WechatIMG18.jpeg

代码分析

将项目里的src导入到IDE里,可以看到代码结构如下:


WechatIMG19.jpeg
WechatIMG19.jpeg

简单说明

  • components —— vue组件
  • views —— 视图
  • mixins —— 扩展
  • filters —— vue.js 的filter
  • App.vue —— 主UI界面
  • entry.js —— 入口程序
  • router.js —— vue-router

入口程序

import App from './App.vue'
import router from './router'
import * as filters from './filters'
import mixins from './mixins'

// register global utility filters.
Object.keys(filters).forEach(key => {
  Vue.filter(key, filters[key])
})

// register global mixins.
Vue.mixin(mixins)

new Vue(Vue.util.extend({ el: '#root', router }, App))

router.push('/')

该段代码主要实现将各个组件和扩展导入,执行各种初始化工作,包括view、router等核心功能。暂且说这么多,后面再详说。

vue-router (https://github.com/vuejs/vue-router) 是vue.js生态里重要的一环,是vue.js官方router它与Vue.js核心深度集成,使得使用Vue.js构建单页面应用程序变得轻而易举,包含如下特性:

  • 嵌套路由/视图映射
  • 基于组件的路由器配置
  • 路由参数,查询,通配符
  • 集成Vue.js页面过渡效果
  • 导航控制
  • 历史记录:HTML5 history mode 或者 hash mode

我们本项目项目来看如何使用vue-router:

// import Vue from 'vue'
import Router from 'vue-router'

import MineDeviceView from './views/MineDeviceView.vue'
import AddDeviceView from './views/AddDeviceView.vue'
import BloodSugarResultView from './views/BloodSugarResultView.vue'
import BloodSugarRunView from './views/BloodSugarRunView.vue'
import BlueToothPreView from './views/BlueToothPreView.vue'
import BlueToothRunView from './views/BlueToothRunView.vue'
import BlueToothErrorView from './views/BlueToothErrorView.vue'
import BloodPressureResultView from './views/BloodPressureResultView.vue'
import BloodPressureRunView from './views/BloodPressureRunView.vue'

Vue.use(Router)

export default new Router({
  // mode: 'abstract',
  routes: [
    { path: '/device-mine', component: MineDeviceView },
    { path: '/device-add', component: AddDeviceView },
    { path: '/blood-sugar-result', component: BloodSugarResultView },
    { path: '/blood-sugar-run', component: BloodSugarRunView },
    { path: '/bluetooth-pre', component: BlueToothPreView },
    { path: '/bluetooth-run', component: BlueToothRunView },
    { path: '/bluetooth-error', component: BlueToothErrorView },
    { path: '/blood-press-result', component: BloodPressureResultView },
    { path: '/blood-press-run',component: BloodPressureRunView },
    { path: '/', redirect: '/device-mine' } 
  ]
})
  • 首先,需要import Router from 'vue-router',导入Router,然后Vue.use(Router)
  • rourter是基于组件的路由配置,所以还需要导入各种View
  • 最重要的,router需要返回Router的实例对象,关键是配置routes,如代码所示,routes是一个json-array,里面的每一个json-object包含了path和component
  • path支持字符串、通配符
  • component返回一个View
  • 看到这里大概就理解了router的原理,通过path去匹配,然后返回匹配的View,比如访问主页,route里配置的是redirect:'/device-mine', 则会跳转到device-mine
  • 项目里,top,new,show等都是StoriesView,只是类型不同,所以createStoriesView函数用于实例化不同类型的StoriesView
  • 路由跳转
  • 跳转:包含两种方式,声明和编程。
<router-link :to="...">
router.push(...)
router.push({ path: 'home' })
router.push('home')
router.push({ name: 'user', params: { userId: 123 }})
声明式 编程式
<router-link :to="..."> router.push(...)
  • mixins,在入口代码里有
Vue.mixin(mixins)
export default {
  methods: {
    jump (to) {
      if (this.$router) {
        this.$router.push(to)
      }
    }
  }
}

  • Vue.mixin 混合是一种灵活的分布式复用 Vue 组件的方式,所有混合对象的选项将被混入该组件本身的选项,因此上述代码实现为Vue组件增加jump方法,而jump的核心就是路由的跳转。mixin后,可以在vue组件里使用jump方法。例如:<div class="link" @click="jump('/top')">

这是抄袭自weex官方demo

filters过滤器

filter是vue.js的一个特性,过滤器是一个通过输入数据,能够及时对数据进行处理并返回一个数据结果的简单函数。Vue有很多很便利的过滤器,可以参考官方文档, http://cn.vuejs.org/api/#过滤器 ,过滤器通常会使用管道标志 " | ", 比如:

{{ msg | capitalize }}

// 'abc' => 'ABC'

项目里的定义如下:

export function timeAgo (time) {
  const between = Date.now() / 1000 - Number(time)
  if (between < 3600) {
    return pluralize(~~(between / 60), ' minute')
  } else if (between < 86400) {
    return pluralize(~~(between / 3600), ' hour')
  } else {
    return pluralize(~~(between / 86400), ' day')
  }
}

格式化时间的filter

使用过滤器

<text>{{comment.time|timeAgo}} ago</text>

App.vue

<template>
  <div @androidback="back" id="app" class="app">
    <transition name="fade">
      <router-view style="flex:1"></router-view>
    </transition>
  </div>
</template>

<script>
export default {
  methods: {
    back: function() {
      this.$router.back()
    }
  }
}
</script>

<style>
.app {
    background-color: #F7F7F7;
}
</style>

定义了一个div作为body容器,transition 作为过渡动画容器,router-view作为component容器

这里存在一个问题是似乎transition过渡动画的存在是个然并暖

  • @androidback="back"处理了Android返回按钮点击事件,点击返回按钮时,router执行back回退。
  • 通过Vue.util.extend,将router注入到所有子组件,这样就可以使用this.$router来访问路由。
new Vue(Vue.util.extend({ el: '#root', router }, App))
router.push('/')

代码最后一行,router.push('/') , 跳转到'/', 根据上篇的内容,会跳转到/device-mine

export default new Router({
  // mode: 'abstract',
  routes: [
    { path: '/device-mine', component: MineDeviceView },
    { path: '/device-add', component: AddDeviceView },
    { path: '/blood-sugar-result', component: BloodSugarResultView },
    { path: '/blood-sugar-run', component: BloodSugarRunView },
    { path: '/bluetooth-pre', component: BlueToothPreView },
    { path: '/bluetooth-run', component: BlueToothRunView },
    { path: '/bluetooth-error', component: BlueToothErrorView },
    { path: '/blood-press-result', component: BloodPressureResultView },
    { path: '/blood-press-run',component: BloodPressureRunView },
    { path: '/', redirect: '/device-mine' } 
  ]
})

MineDeviceView.vue(我的设备)

<template>
    <div>
        <HeaderNor title="我的设备"></HeaderNor>
        <div v-if="showTag === true">
            <text class="tag1">目前尚未使用任何设备</text>
            <text class="tag2">请点击屏幕右上角添加设备</text>
        </div>
        <div>
            <list style="width: 750px;height: 1294px;top: 2px;">
                <cell class="device-list" v-for="item in items" @click="jumpTo(item.query)">
                    <image class="device-pic" v-bind:src="item.img"></image>
                    <text class="device-title">{{ item.title }}</text>
                    <text class="device-used-time">{{ item.time }}使用</text>
                    <text v-if="item.showValueTitle === true" class="device-subtitle">上次测量结果</text>
                    <text class="device-value">{{ item.lastValue }}</text>
                </cell>
            </list>
        </div>
    </div>
</template>
<script>
    import HeaderNor from '../components/HeaderNor.vue'

    const storage = weex.requireModule('storage')
    const modal = weex.requireModule('modal')

    export default {
        mounted () {
            this.getItem()
        },
        data() {
            return {
                items: [],
                showTag: true
            }
        },
        methods: {
            jumpTo (query) {
                this.jump('/' + query)
//                this.$router.push({path: 'blood-press-result', query: {result: '115,56'}})
            },
            getItem () {
                var vm = this
                var isCatch = false
                storage.getItem('bloodPress', event => {
                    console.log('get value:', event)
                    if (event.data != "undefined") {
                        var jsonobj = JSON.parse(event.data)
                        jsonobj.time = vm.countTimeDifference(jsonobj.time)
                        vm.items.push(jsonobj)
                        isCatch = true
                        if (isCatch) {
                            vm.showTag = false
                        }
                    }
                })
            },
            catchShowTag () {
                console.log(this.items)
                console.log(this.items.length)
                if (this.items.length > 0) {
                    this.showTag = false
                }
            },
            countTimeDifference (dateString) {
                var timeDifference = Date.parse(new Date()) - Date.parse(dateString)
                var timeDifferenceSeconds = parseInt(timeDifference / 1000)
                if (timeDifferenceSeconds > 0 && timeDifferenceSeconds < 3600) {
                    let minute = Math.round(timeDifferenceSeconds / 60)
                    if (minute === 0) {
                        return '1分钟前'
                    } else {
                        return Math.round(timeDifferenceSeconds / 60) + '分钟前'
                    }
                } else if (timeDifferenceSeconds >= 3600 && timeDifferenceSeconds < 86400) {
                    return Math.round(timeDifferenceSeconds / 3600) + '小时前'
                } else if (timeDifferenceSeconds >= 86400) {
                    return Math.round(timeDifferenceSeconds / 86400) + '天前'
                }
            }
        },
        components: {
            HeaderNor
        }
    }
</script>
<style scoped>
    .text {
        font-size: 200px;
    }

    .tag1 {
        position: absolute;
        top: 575px;
        left: 238px;
        color: #838383;
        font-size: 28px;
    }

    .tag2 {
        position: absolute;
        top: 621px;
        left: 208px;
        color: #838383;
        font-size: 28px;
    }

    .device-list {
        height: 160px;
        border-bottom-color: #B4B4B4;
        border-bottom-style: solid;
        border-bottom-width: 1px;
        background-color: #FFFFFF;
    }

    .device-pic {
        height: 100px;
        width: 78px;
        position: relative;
        left: 46px;
        top: 30px;
        bottom: 30px;
    }

    .device-title {
        font-size: 31px;
        position: absolute;
        top: 46px;
        left: 158px;
    }

    .device-used-time {
        position: absolute;
        font-size: 19px;
        top: 95px;
        left: 159px;
        color: #838383;
    }

    .device-subtitle {
        position: absolute;
        top: 57px;
        right: 49px;
        font-size: 19px;
        color: #333333;
    }

    .device-value {
        position: absolute;
        top: 88px;
        right: 49px;
        font-size: 19px;
        color: #333333;
    }
</style>

组件通过props属性传递数据

MineDeviceView

<HeaderNor title="我的设备"></HeaderNor>

HeaderNor组件

Vue.js可以使用 props 把数据传给子组件,prop 是父组件用来传递数据的一个自定义属性,子组件需要显式地用 props 选项声明 "prop":

<template>
    <div>
        <div class="header">
            <div class="in-header">
                <image class="return" src="https://hardware.baichengyiliao.com/static/return-button.jpg"
                       @click="goBack()"></image>
                <image class="add" src="https://hardware.baichengyiliao.com/static/add-button.jpg"
                       @click="jump('/device-add')"></image>
                <text class="title">{{ title }}</text>
            </div>
        </div>
        <div style="height: 88px">
        </div>
    </div>
</template>

<script>
    const native = weex.requireModule('event');
    export default {
        methods: {
            goBack () {
                native.backToMain()
            }
        },
        props: ['title']
    }
</script>

子组件

在MineDeviceView中,包含子组件HeaderNor,使用时需要import
import HeaderNor from '../components/HeaderNor.vue'

<template>
    <div>
        <div class="header">
            <div class="in-header">
                <image class="return" src="https://hardware.baichengyiliao.com/static/return-button.jpg"
                       @click="goBack()"></image>
                <image class="add" src="https://hardware.baichengyiliao.com/static/add-button.jpg"
                       @click="jump('/device-add')"></image>
                <text class="title">{{ title }}</text>
            </div>
        </div>
        <div style="height: 88px">
        </div>
    </div>
</template>

<script>
    const native = weex.requireModule('event');
    export default {
        methods: {
            goBack () {
                native.backToMain()
            }
        },
        props: ['title']
    }
</script>

两个关键点

  • @click 处理点击事件,jump是在入口程序里mixin的router跳转函数
  • weex里,text用于显示文本元素,可以理解为html里的span

谈一谈使用Vue.js开发Weex的一些体验

Weex 可以让前端开发人员开发native app程序,值得点赞

缺点是目前坑还比较多,开源社区资源也较少

在开发过程中遇到一些情况

  1. Weex 盒模型基于 CSS 盒模型,每个 Weex 元素都可视作一个盒子。本项目中绝大多数布局,都基于「盒模型」概念。但是,Weex目前对盒模型的支持并不完全。
    • Weex 对于长度值目前只支持像素值,不支持相对单位(em、rem)
    • 设定边框,border 目前不支持类似这样 border: 1 solid #ff0000; 的组合写法
    .button-choice {
        border-radius: 110px;
        height: 221px;
        width: 221px;
        border-color: #B4B4B4;
        border-width: 1px;
        border-style: solid;
        background-color: #5A33BE;
    }

写出来的CSS很多时候都是这个样子的,在web可以一行带带吗搞定的问题,在这个可能需要3行或者4行,并且,weex是不支持全局样式的,所在在style标签中scoped是必须的。

  1. style选择器只支持class

这个在上面的MineDeviceView代码中有所体现,写到最后会发现满屏幕的class。。。

  1. 令人困惑的官方文档
    以下代码来自于官方手册
    https://weex.incubator.apache.org/cn/v-0.10/guide/syntax/style-n-class.html
<template>
  <div>
    <text style="font-size: {{fontSize}};">Alibaba</text>
    <text class="large {{textClass}}">Weex Team</text>
  </div>
</template>
<style>
  .large {font-size: 32;}
  .highlight {color: #ff0000;}
</style>
<script>
  module.exports = {
    data: {
      fontSize: 32,
      textClass: 'highlight'
    }
  }
</script>

class="large {{textClass}}"
style="font-size: {{fontSize}};"
这样的写法我相信写过vue的人都知道是不可以的,但是,weex手册中出现了,那么在weex中支持这样的写法吗?答案是不支持。反正我被文档骗了一整天。
正确的写法是使用v-bind指令:v-bind:style="...",那么v-bind:class="..."应该也是支持的吧?结果是不支持

weex vue
生命周期 ready: function() {} mounted: function() {}

……
实际情况当然是mounted,被骗一天+1

  • 对动画的支持
    weex提供了animation模块,我们可以很方便的在页面中加入动画,利用相关的API,可以设置动画对象、动画类型、持续时间、回调函数,但是我们无法手动结束动画

  • 本地存储
    相比较动画,weex的storage做的还是不错的。
    storage 是一个在前端比较常用的模块,可以对本地数据进行存储、修改、删除,并且该数据是永久保存的,除非手动清除或者代码清除。但是,storage 模块有一个限制就是浏览器端(H5)只能存储小于5M的数据,因为在 H5/Web 端的实现是采用 HTML5 LocalStorage API。而 Android 和 iOS 这块是没什么限制的。

storage.setItem('bloodPress', vm.data.jsonString, event => {
    console.log('set success')
})

很容易就可以保存相关的数据,和使用h5的localStorage很相似。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容