Weex开发之路(二):完成一个ToDoList项目

一、需求分析

在本文实例中,我们将要完成一个ToDoList项目,将会管理里一天的生活安排。拥有以下几个功能。

  1. 新增当天行程
  2. 删除当前行程
  3. 给当天行程打标(设置为已完成)
  4. 数据本地缓存,避免记录的行程被销毁。

最终实现效果如下图所示:


二、基础知识预备

2.1:如何获取屏幕宽高

获取屏幕的高度,你就需要了解$getConfig()方法,他可以获取当前全局环境变量和配置信息。

var config = this.$getConfig();
console.log(config);

输出信息如下:



你可以从中得到bundle的Url;屏幕的宽高,device的宽度总是等于750px,屏幕的高度是基于屏幕比例得到的一个高度值。如果你要针对于iOS,做一些特殊的设置,你也可以通过platform字段进行判断,因为我是在浏览器中进行调试,所以这里的platform为Web。

2.2:Weex的生命周期

<script>
  module.exports = {
    data: {},
    methods: {},

    init: function () {
      console.log('在初始化内部变量,并且添加了事件功能后被触发');
    },
    created: function () {
      console.log('完成数据绑定之后,模板编译之前被触发');
    },
    ready: function () {
      console.log('模板已经编译并且生成了 Virtual DOM 之后被触发');
    },
    destroyed: function () {
      console.log('在页面被销毁时调用');
    }
  }
</script>
  • init内一般用于初始化一些内部变量,绑定一些自定义事件,这时还没有数据绑定,没有创建vdom,所以不能通过this获取到data和methods,也不能获取vdom的节点
  • created 完成了数据绑定 ,但还未开始编译模板,可以通过this获取data和methods,但不能获取vdom的节点
  • ready表示渲染完成 ,从子组件往上触发
  • destroyed 组件销毁,比如页面跳转,从子组件开始往上触发

2.3:组件之间通信

参考官方文档:组件之间通信

2.4:本地缓存组件的使用

参考官方文档:storage

三、开发过程

3.1:Weex开发的组件骨架

<template>
</template>

<style>
</style>

<script>
    module.exports = {
        data: {
        },
        init: function () {
        },
        created: function () {
        },
        ready: function () {
        },
        methods:{
        },
        computed:{
        }
    }
</script>
  • template中主要是组件的引用,大体页面布局。类似于HTML。
  • style中主要是组件的CSS样式引用
  • script中主要就是js的调用。data属于绑定的数据,init、created、ready是Weex的生命周期方法。
  • methods中存放用户定义的一些js事件。
  • computed中主要是对绑定数据data部分进行预处理。

3.2:项目目录结构

  • todolist.we是目录的入口。
  • add_dialog.we是新增事件的弹出框组件。
  • icon.js是存放的一些js中使用的图片常量,主要包括选中和未选中的图片。
  • todolist_bottom.we是底部操作栏,包括新增、完成和操作事件的操作。
  • todolist_cell.we是list中每一个cell组件。

3.2.1:add_dialog.we程序清单和解释

<template>
    <div class="container" style="height: {{screenHeight}};background-color:rgba(0, 0, 0,{{opacity}})" >
        <div class="dialog_container">
            <text class="title">新增事件</text>
            <input class="title-input" placeholder="请输入标题" type="text"
                   oninput="oninputForTitle"></input>
            <input class="time-input" placeholder="请输入时间" type="text"
                   oninput="oninputForTime"></input>
            <div class="bottom">
                <text class="cancel" onclick="cancel">取消</text>
                <text class="ok" onclick="ok">确定</text>
            </div>
        </div>
    </div>
</template>

<style>
    .container{
        background-color:grey;
        position: fixed;
        width: 750px;
        top:0;
        left: 0;
        align-items: center;
        justify-content: center;
    }
    .dialog_container{
        background-color: white;
        width: 600px;
        height: 310px;
        align-items: center;
    }
    .title{
        margin-top: 20px;
        font-size:40px;
    }
    .title-input{
        margin-top: 20px;
        height: 60px;
        width: 500px;
        font-size: 25px;
        border-width: 1px;
    }
    .time-input{
        margin-top: 20px;
        height: 60px;
        width: 500px;
        font-size: 25px;
        border-width: 1px;
    }
    .bottom{
        margin-top: 30px;
        flex-direction: row;
        width: 400;
    }
    .cancel{
        text-align: center;
        flex: 1;
    }
    .ok{
        text-align: center;
        flex: 1;
    }

</style>

<script>
    module.exports = {
        data: {
            screenHeight:0,
            opacity:0.5,
            title: '',
            time: ''
        },
        created: function () {
            var config = this.$getConfig();
            var env = config.env;
            var screenHeight = env.deviceHeight;
            this.screenHeight = screenHeight;
        },
        methods:{
            cancel: function () {
                this.$dispatch('dialog-cancel');
            },
            ok: function () {
                this.$dispatch('dialog-ok',{
                    title : this.title,
                    time : this.time
                });
            },
            oninputForTitle: function(event) {
                this.title = event.value;
            },
            oninputForTime : function (event) {
                this.time = event.value;
            }
        }
    }

</script>
  • 弹出层的背景是一个半透明的阴影部分,高度要和屏幕高度一致,所以我们需要通过this.$getConfig().env.deviceHeight获取高度之后,设置给最外层的style属性。
<div class="container" style="height: {{screenHeight}};background-color:rgba(0, 0, 0,{{opacity}})" >
</div>
  • input组件中如果我们要获取字体改变的事件,并且获取当前的text内容的话,我们需要监听oninput事件,然后在该事件中进行数据绑定。
oninputForTitle: function(event) {
   this.title = event.value;
},
oninputForTime : function (event) {
   this.time = event.value;
}
  • 点击确定和取消的时间需要通过This.$dispatch()方法传递给父组件
cancel: function () {
    this.$dispatch('dialog-cancel');
},
ok: function () {
    this.$dispatch('dialog-ok',{
        title : this.title,
        time : this.time
     });
},

3.2.2:icon.js程序清单和解释

module.exports = {
     check: "http://p1.bpimg.com/567571/1a7a707567b04e4e.png",
    nocheck : "http://p1.bpimg.com/567571/b3e63a0c308955f0.png"
}

其他文件中可以通过以下方式进行引用

const icon = require('./icon');
icon.check
icon.nocheck

3.2.3:todolist_bottom.we程序清单和解释

<template>
   <div class="container">
      <text class="text" onclick="add">新增</text>
      <text class="text" onclick="finish">完成</text>
      <text class="text" onclick="delete">删除</text>
   </div>
</template>

<style>
    .container{
       background-color: aquamarine;
       height: 200px;
       flex-direction: row;
       align-items: center;
    }
   .text{
      flex: 1;
      text-align: center;
      padding: 40px;
   }
</style>

<script>
   module.exports = {
      data : {

      },
      methods : {
         add : function () {
            this.$dispatch('bottom-add');
         },
         finish : function () {
            this.$dispatch('bottom-finish');
         },
         delete : function () {
            this.$dispatch('bottom-delete');
         }
      }
   }

</script>
  • 添加、完成和删除方法都通过this.$dispatch方法把事件传递给父组件进行了调用。

3.2.4:todolist_cell.we程序清单和解释

<template>
    <div class="container">
        <image src="{{picUrl}}" class="pic" onclick="check"></image>
        <text class="title" style="text-decoration:{{textDecoration}}">{{item.title}}</text>
        <text class="time">{{item.time}}</text>
    </div>
</template>

<style>
    .container{
        background-color:gainsboro;
        height: 80px;
        margin: 10px;
        flex-direction: row;
        align-items: center;
    }
    .pic{
        width: 40px;
        height: 40px;
        margin-left: 20px;
    }
    .title{
        margin-left: 20px;
        flex: 1;
    }
    .time{
        margin-right: 20px;
    }
</style>

<script>
    const icon = require('./icon');

    module.exports = {
        data : {
            item:[]
        },
        methods : {
            check : function () {
                this.item.check = !this.item.check;
            }
        },
        computed: {
            picUrl: {
                get :  function () {
                    if(this.item){
                        var check = this.item.check;
                        if(check){
                            return icon.check;
                        }else{
                            return icon.nocheck;
                        }
                    }
                }
            },
            textDecoration: {
                get : function () {
                    if(this.item){
                        var finish = this.item.finish;
                        if(finish){
                            return 'line-through';
                        }else{
                            return 'none';
                        }
                    }
                }
            }

        }
    }

</script>
  • 父组件中通过以下方式切入子组件
<todolist_cell item="{{item}}"></todolist_cell>

需要在子组件的data区域中也要定义一个item属性,我们才能接收到父组件传给子组件的值。

  • 对显示的数据进行预处理。因为每一cell种图片是否选中item中只有一个check字段与之对应,而check字段只是bool,并不是一个check图片的picUrl。picUrl不在data区域,而是根据data区域的值的逻辑获得,所以我们可以在computed区域中对其的get方法进行操作。
        computed: {
            picUrl: {
                get :  function () {
                    if(this.item){
                        var check = this.item.check;
                        if(check){
                            return icon.check;
                        }else{
                            return icon.nocheck;
                        }
                    }
                }
            },
            textDecoration: {
                get : function () {
                    if(this.item){
                        var finish = this.item.finish;
                        if(finish){
                            return 'line-through';
                        }else{
                            return 'none';
                        }
                    }
                }
            }

        }

3.2.5:todolist.we程序清单和解释

<template>
    <div style="height: {{screenHeight}}">
        <div class="list-container">
            <list>
                <cell repeat="{{item in items}}">
                    <todolist_cell item="{{item}}"></todolist_cell>
                </cell>
            </list>
        </div>
        <todolist_bottom></todolist_bottom>
        <add_dialog if="{{showDialog}}"></add_dialog>
    </div>
</template>

<style>
    .list-container{
        background-color: bisque;
        flex: 1;
    }
</style>

<script>
    //require('weex-components');
    require('./component/todolist_bottom.we');
    require('./component/todolist_cell.we');
    require('./component/add_dialog.we');
    var modal = require('@weex-module/modal');
    var storage = require('@weex-module/storage');
    
    module.exports = {
        data: {
            platform: 'Web',
            screenHeight : 0,
            showDialog: false,
            items : []
        },
        init: function () {

        },
        created: function () {
            var config = this.$getConfig();
            console.log(JSON.stringify(config));
            var env = config.env;
            var screenHeight = env.deviceHeight;
            this.screenHeight = screenHeight;
            this.platform = env.platform;
            if (this.platform && this.platform === 'Web'){

            }else{
                this.screenHeight = screenHeight - 120;
            }

            var self = this;
            storage.getItem('todolist_cache', function(e) {
                var result = e.result;
                if (result === 'success') {
                    var data = e.data;
                    self.items = JSON.parse(data);
                } else {
                }
            });

            var self = this;
            this.$on('bottom-add', function () {
                self.showDialog = true;
            });
            this.$on('bottom-finish', function () {
                for(var index in this.items){
                    var item = this.items[index];
                    var check = item.check;
                    if(check){
                        item.finish = true;
                        item.check = false;
                    }
                }
                storage.setItem('todolist_cache', JSON.stringify(this.items), function(e) {
                });
            });
            this.$on('bottom-delete', function () {
                var tempArray = [];
                for(var index in this.items){
                    var item = this.items[index];
                    var check = item.check;
                    if(!check){
                        tempArray.push(item);
                    }
                }
                this.items = tempArray;

                storage.setItem('todolist_cache', JSON.stringify(this.items), function(e) {
                });
            });
            this.$on('dialog-cancel', function () {
                this.showDialog = false;
            });
            this.$on('dialog-ok',function (e) {
                this.showDialog = false;
                var detail = e.detail;
                var title = detail.title;
                var time = detail.time;
                this.items.push({
                    title:title,
                    time:time,
                    check:false,
                    finish:false
                });
                storage.setItem('todolist_cache', JSON.stringify(this.items), function(e) {
                });
            });



        },
        ready: function () {

        },
        methods:{
            
        },
        
    }
</script>
  • list组件的使用
<div class="list-container">
    <list>
         <cell repeat="{{item in items}}">
             <todolist_cell item="{{item}}"></todolist_cell>
         </cell>
    </list>
</div>
  • 引用自定义组件的系统自带组件
    require('./component/todolist_bottom.we');
    require('./component/todolist_cell.we');
    require('./component/add_dialog.we');
    var modal = require('@weex-module/modal');//系统自带toast组件
    var storage = require('@weex-module/storage');//系统自带本地数据缓存组件
  • 数据缓存
    获取数据,注意这里需要保存this对象给self,应该在回调方法中,this对象已经不是该类自己了。
var self = this;
storage.getItem('todolist_cache', function(e) {
      var result = e.result;
      if (result === 'success') {
          var data = e.data;
          self.items = JSON.parse(data);
      } else {
      }
});

保存数据

storage.setItem('todolist_cache', JSON.stringify(this.items), function(e) {
});
  • 父组件中注册监听子组件中传递过来的事件
this.$on('bottom-add', function () {
});
this.$on('bottom-finish', function () {
});
this.$on('bottom-delete', function () {
});
this.$on('dialog-cancel', function () {
});
this.$on('dialog-ok',function (e) {
});

四、总结

本文实现一个简单的todolist实例,大家对于Weex的基本使用应该有了更深的了解。而且这个Demo虽然简单,但是也包括了不少内容,包括父子组件的通信、weex中本地缓存的使用、如何自定义组件、commonJS的封装等等。有时候在使用weex的时候发现,flex布局没有效果,这时候你试着给该组件设置height就会解决问题。
如果你在weex todolist.we 或者 weex todolist.we --qr报错的话

yixiangdeMacBook-Pro:ToDoList yixiang$ weex todolist.we 
ERR! ModuleNotFoundError: Module not found: Error: Cannot resolve module 'babel-runtime/core-js/json/stringify' in /Users/yixiang/Weex/Demo/ToDoList 
info Please try to enter directory where your we file saved, and run command 'npm install babel-runtime' 
yixiangdeMacBook-Pro:ToDoList yixiang$ weex todolist.we --qr
ERR! ModuleNotFoundError: Module not found: Error: Cannot resolve module 'babel-runtime/core-js/json/stringify' in /Users/yixiang/Weex/Demo/ToDoList 
info Please try to enter directory where your we file saved, and run command 'npm install babel-runtime' 

尝试根据提示npm install babel-runtime,应该就会解决问题。

五、参考资料

Weex Github仓库
Weex 英文文档(比较全)
Weex 中文文档(非官方提供,不全)
Weex学习与实践

六、联系方式

新浪微博
github
简书首页

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

推荐阅读更多精彩内容