kgshuo 2018-08-08
该框架是腾讯内部基于小程序的开发框架,设计思路基本参考VUE,开发模式和编码风 格上80%以上接近VUE
优势
组件化开发
小程序虽然有标签可以实现组件复用,但仅限于模板片段层面的复用,业务代码与交互事件 仍需在页面处理。无法实现组件化的松耦合与复用的效果。
wepy组件示例
// index.wpy <template> <view> <panel> <h1 slot="title"></h1> </panel> <counter1 :num="myNum"></counter1> <counter2 :num.sync="syncNum"></counter2> <list :item="items"></list> </view> </template> <script> import wepy from 'wepy'; import List from '../components/list'; import Panel from '../components/panel'; import Counter from '../components/counter'; export default class Index extends wepy.page { config = { "navigationBarTitleText": "test" }; components = { panel: Panel, counter1: Counter, counter2: Counter, list: List }; data = { myNum: 50, syncNum: 100, items: [1, 2, 3, 4] } } </script>
支持加载外部NPM包
小程序较大的缺陷是不支持NPM包,导致无法直接使用大量优秀的开源内容,wepy在编译过程当中,会递归 遍历代码中的require然后将对应依赖文件从node_modules当中拷贝出来,并且修改require为相对路径, 从而实现对外部NPM包的支持
单文件模式,使得目录结构更加清晰
小程序官方目录结构要求app必须有三个文件app.json,app.js,app.wxss,页面有4个文件 index.json,index.js,index.wxml,index.wxss。而且文 件必须同名。 所以使用wepy开发前后开发目录对比如下:
官方DEMO:
project ├── pages | ├── index | | ├── index.json index 页面配置 | | ├── index.js index 页面逻辑 | | ├── index.wxml index 页面结构 | | └── index.wxss index 页面样式表 | └── log | ├── log.json log 页面配置 | ├── log.wxml log 页面逻辑 | ├── log.js log 页面结构 | └── log.wxss log 页面样式表 ├── app.js 小程序逻辑 ├── app.json 小程序公共设置 └── app.wxss 小程序公共样式表
使用wepy框架后目录结构:
project └── src ├── pages | ├── index.wpy index 页面配置、结构、样式、逻辑 | └── log.wpy log 页面配置、结构、样式、逻辑 └──app.wpy 小程序配置项(全局样式配置、声明钩子等)
如何开发
快速起步
安装
npm install wepy-cli -g
创建项目
wepy new myproject
切换至项目目录
cd myproject
实时编译
wepy build Cwatch
目录结构 ├── dist 微信开发者工具指定的目录
├── node_modules ├── src 代码编写的目录 | ├── components 组件文件夹(非完整页面) | | ├── com_a.wpy 可复用组件 a | | └── com_b.wpy 可复用组件 b | ├── pages 页面文件夹(完整页面) | | ├── index.wpy 页面 index | | └── page.wpy 页面 page | └── app.wpy 小程序配置项(全局样式配置、声明钩子等) └── package.json package 配置
wepy和VUE主要区别
1.二者均支持props、data、computed、components、methods、watch(wepy中是watcher), 但wepy中的methods仅可用于页面事件绑定,其他自定义方法都要放在外层,而VUE中所有方法均放在 methods下
2.wepy中props传递需要加上.sync修饰符(类似VUE1.x)才能实现props动态更新,并且父组件再 变更传递给子组件props后要执行this.$apply()方法才能更新
3.wepy支持数据双向绑定,子组件在定义props时加上twoway:true属性值即可实现子组件修改父组 件数据
4.VUE2.x推荐使用eventBus方式进行组件通信,而在wepy中是通过broadcast,broadcast,emit,$invoke 三种方法实现通信
・ 首先事件监听需要写在events属性下: ``` bash import wepy from 'wepy'; export default class Com extends wepy.component { components = {}; data = {}; methods = {}; events = { 'some-event': (p1, p2, p3, $event) => { console.log(`${this.name} receive ${$event.name} from ${$event.source.name}`); } }; // Other properties } ``` ・ $broadcast:父组件触发所有子组件事件 ・ $emit:子组件触发父组件事件 ・ $invoke:子组件触发子组件事件
5.VUE的生命周期包括created、mounted等,wepy仅支持小程序的生命周期:onLoad、onReady等
6.wepy不支持过滤器、keep-alive、ref、transition、全局插件、路由管理、服务端渲染等VUE特性技术
进阶介绍
.wpy文件说明
一个.wpy文件可分为三大部分,各自对应于一个标签:
脚本部分,即标签中的内容,又可分为两个部分:
逻辑部分,除了config对象之外的部分,对应于原生的.js文件;
配置部分,即config对象,对应于原生的.json文件。
结构部分,即模板部分,对应于原生的.wxml文件。
样式部分,即样式部分,对应于原生的.wxss文件。
.wpy文件中的script、template、style这三个标签都支持lang和src属性,lang决定了其代码编译过程,src决定是否外联代码,存在src属性且有效时,会忽略内联代码。
标签 | lang默认值 | lang支持值 |
---|---|---|
style | css | css、less、sass、stylus |
template | wxml | wxml、xml、pug(原jade) |
script | babel | babel、TypeScript |
普通组件引用
当页面需要引入组件或组件需要引入子组件时,必须在.wpy文件的
<template> <!-- 以`<script>`脚本部分中所声明的组件ID为名命名自定义标签,从而在`<template>`模板部分中插入组件 --> <child></child> </template> <script> import wepy from 'wepy'; //引入组件文件 import Child from '../components/child'; export default class Index extends wepy.component { //声明组件,分配组件id为child components = { child: Child }; } </script>
需要注意的是,WePY中的组件都是静态组件,是以组件ID作为唯一标识的,每一个ID都对应一个组件实例,当页面引入两个相同ID的组件时,这两个组件共用同一个实例与数据,当其中一个组件数据变化时,另外一个也会一起变化。
如果需要避免这个问题,则需要分配多个组件ID和实例。
组件的循环渲染
1.4.6新增
当需要循环渲染WePY组件时(类似于通过wx:for循环渲染原生的wxml标签),必须使用WePY定义的辅助标签
<template> <!-- 注意,使用for属性,而不是使用wx:for属性 --> <repeat for="{{list}}" key="index" index="index" item="item"> <!-- 插入<script>脚本部分所声明的child组件,同时传入item --> <child :item="item"></child> </repeat> </template>
computed 计算属性
computed计算属性,是一个有返回值的函数,可直接被当作绑定数据来使用。因此类似于data属性,代码中可通过this.计算属性名来引用,模板中也可通过{{ 计算属性名 }}来绑定数据。
需要注意的是,只要是组件中有任何数据发生了改变,那么所有计算属性就都会被重新计算。
data = { a: 1 } // 计算属性aPlus,在脚本中可通过this.aPlus来引用,在模板中可通过{{ aPlus }}来插值 computed = { aPlus () { return this.a + 1 } }
watcher 监听器
通过监听器watcher能够监听到任何数值属性的数值更新。监听器在watch对象中声明,类型为函数,函数名与需要被监听的data对象中的数值属性同名,每当被监听的数值属性改变一次,监听器函数就会被自动调用执行一次。
监听器适用于当数值属性改变时需要进行某些额外处理的情形。
data = { num: 1 } // 监听器函数名必须跟需要被监听的data对象中的数值属性num同名, // 其参数中的newValue为数值属性改变后的新值,oldValue为改变前的旧值 watch = { num (newValue, oldValue) { console.log(`num value: ${oldValue} -> ${newValue}`) } } // 每当被监听的数值属性num改变一次,对应的同名监听器函数num()就被自动调用执行一次 onLoad () { setInterval(() => { this.num++; this.$apply(); }, 1000) }
props 传值
静态传值
静态传值为父组件向子组件传递常量数据,因此只能传递String字符串类型。
在父组件template模板部分的组件标签中,使用子组件props对象中所声明的属性名作为其属性名来接收父组件传递的值。
<child title="mytitle"></child> // child.wpy props = { title: String }; onLoad () { console.log(this.title); // mytitle }
动态传值
动态传值是指父组件向子组件传递动态数据内容,父子组件数据完全独立互不干扰。但可以通过使用.sync修饰符来达到父组件数据绑定至子组件的效果,也可以通过设置子组件props的twoWay: true来达到子组件数据绑定至父组件的效果。那如果即使用.sync修饰符,同时子组件props中添加的twoWay: true时,就可以实现数据的双向绑定了。
注意:下文示例中的twoWay为true时,表示子组件向父组件单向动态传值,而twoWay为false(默认值,可不写)时,则表示子组件不向父组件传值。这是与Vue不一致的地方,而这里之所以仍然使用twoWay,只是为了尽可能保持与Vue在标识符命名上的一致性。
在父组件template模板部分所插入的子组件标签中,使用:prop属性(等价于Vue中的v-bind:prop属性)来进行动态传值。
// parent.wpy <child :title="parentTitle" :syncTitle.sync="parentTitle" :twoWayTitle="parentTitle"></child> data = { parentTitle: 'p-title' }; // child.wpy props = { // 静态传值 title: String, // 父向子单向动态传值 syncTitle: { type: String, default: 'null' }, twoWayTitle: { type: Number, default: 50, twoWay: true } }; onLoad () { console.log(this.title); // p-title console.log(this.syncTitle); // p-title console.log(this.twoWayTitle); // 50 this.title = 'c-title'; console.log(this.$parent.parentTitle); // p-title. this.twoWayTitle = 60; this.$apply(); console.log(this.$parent.parentTitle); // 60. --- twoWay为true时,子组件props中的属性值改变时,会同时改变父组件对应的值 this.$parent.parentTitle = 'p-title-changed'; this.$parent.$apply(); console.log(this.title); // 'p-title'; console.log(this.syncTitle); // 'p-title-changed' --- 有.sync修饰符的props属性值,当在父组件中改变时,会同时改变子组件对应的值。 }
组件通信与交互
用于监听组件之间的通信与交互事件的事件处理函数需要写在组件和页面的events对象中
- broadcastbroadcastbroadcast事件是由父组件发起,所有子组件都会收到此广播事件,除非事件被手动取消。事件广播的顺序为广度优先搜索顺序
- emitemitemit与broadcast正好相反,事件发起组件的所有祖先组件会依次接收到broadcast正好相反,事件发起组件的所有祖先组件会依次接收到emit事件。
- invokeinvokeinvoke是一个页面或组件对另一个组件中的方法的直接调用,通过传入组件路径找到相应的组件,然后再调用其方法。
比如,想在页面Page_Index中调用组件ComA的某个方法:
this.$invoke('ComA', 'someMethod', 'someArgs');
如果想在组件ComA中调用组件ComG的某个方法:
this.$invoke('./../ComB/ComG', 'someMethod', 'someArgs');
组件自定义事件处理函数
可以通过使用.user修饰符为自定义组件绑定事件,如:@customEvent.user=”myFn”
其中,@表示事件修饰符,customEvent 表示事件名称,.user表示事件后缀。
目前总共有三种事件后缀:
.default: 绑定小程序冒泡型事件,如bindtap,.default后缀可省略不写;
.stop: 绑定小程序捕获型事,如catchtap;
.user: 绑定用户自定义组件事件,通过$emit触发。
// index.wpy <template> <child @childFn.user="parentFn"></child> </template> <script> import wepy from 'wepy' import Child from '../components/child' export default class Index extends wepy.page { components = { child: Child } methods = { parentFn (num, evt) { console.log('parent received emit event, number is: ' + num) } } } </script> // child.wpy <template> <view @tap="tap">Click me</view> </template> <script> import wepy from 'wepy' export default class Child extends wepy.component { methods = { tap () { console.log('child is clicked') this.$emit('childFn', 100) } } } </script>
slot 组件内容分发插槽
WePY中的slot插槽作为内容分发标签的空间占位标签,便于在父组件中通过对相当于扩展板卡的内容分发标签的“插拔”,更为灵活、方便地对子组件进行内容分发。
具体使用方法是,首先在子组件template模板部分中声明slot标签作为内容插槽,同时必须在其name属性中指定插槽名称,还可设置默认的标签内容;然后在引入了该带有插槽的子组件的父组件template模板部分中声明用于“插拔”的内容分发标签。
注意,这些父组件中的内容分发标签必须具有slot属性,并且其值为子组件中对应的插槽名称,这样父组件内容分发标签中的内容会覆盖掉子组件对应插槽中的默认内容。
在Panel组件中有以下模板:
<view class="panel"> <slot name="title">默认标题</slot> <slot name="content">默认内容</slot> </view>
在父组件中使用Pannel子组件时,可以这样使用:
<panel> <view slot="title">新的标题</view> <view slot="content"> <text>新的内容</text> </view> </panel>
混合
默认式混合
对于组件data数据,components组件,events事件以及其它自定义方法采用默认式混合,即如果组件未声明该数据,组件,事件,自定义方法等,那么将混合对象中的选项将注入组件这中。对于组件已声明的选项将不受影响。
// mixins/test.js import wepy from 'wepy'; export default class TestMixin extends wepy.mixin { data = { foo: 'foo defined by page', bar: 'bar defined by testMix' }; methods: { tap () { console.log('mix tap'); } } } // pages/index.wpy import wepy from 'wepy'; import TestMixin from './mixins/test'; export default class Index extends wepy.page { data = { foo: 'foo defined by index' }; mixins = [TestMixin ]; onShow() { console.log(this.foo); // foo defined by index. console.log(this.bar); // foo defined by testMix. } }
兼容式混合
对于组件methods响应事件,以及小程序页面事件将采用兼容式混合,即先响应组件本身响应事件,然后再响应混合对象中响应事件。
// mixins/test.js import wepy from 'wepy'; export default class TestMixin extends wepy.mixin { methods = { tap () { console.log('mix tap'); } }; onShow() { console.log('mix onshow'); } } // pages/index.wpy import wepy from 'wepy'; import TestMixin from './mixins/test'; export default class Index extends wepy.page { mixins = [TestMixin]; methods = { tap () { console.log('index tap'); } }; onShow() { console.log('index onshow'); } } // index onshow // mix onshow // ----- when tap // index tap // mix tap
拦截器
可以使用全域拦截器配置API的config、fail、success、complete方法,参考示例:
import wepy from 'wepy'; export default class extends wepy.app { constructor () { this.intercept('request', { config (p) { p.timestamp = +new Date(); return p; }, success (p) { console.log('request success'); return p; }, fail (p) { console.log('request error'); return p; } }); } }
WePY数据绑定方式
WePY使用脏数据检查对setData进行封装,在函数运行周期结束时执行脏数据检查,一来可以不用关心页面多次setData是否会有性能上的问题,二来可以更加简洁去修改数据实现绑定,不用重复去写setData方法。
this.title = 'this is title';
在函数运行周期之外的函数里去修改数据需要手动调用$apply方法
setTimeout(() => { this.title = 'this is title'; this.$apply(); }, 3000);
优化事件参数传递
// 官方 <view data-id="{{index}}" data-title="wepy" data-other="otherparams" bindtap="tapName"> Click me! </view> Page({ tapName: function(event) { console.log(event.currentTarget.dataset.id)// output: 1 console.log(event.currentTarget.dataset.title)// output: wepy console.log(event.currentTarget.dataset.other)// output: otherparams } }); // WePY 1.1.8以后的版本,只允许传string。 <view bindtap="tapName({{index}}, 'wepy', 'otherparams')"> Click me! </view> methods: { tapName (id, title, other, event) { console.log(id, title, other)// output: 1, wepy, otherparams } }
改变数据绑定方式
保留setData方法,但不建议使用setData执行绑定,修复传入undefined的bug,并且修改入参支持: this.setData(target, value) this.setData(object)
// 官方 <view> {{ message }} </view> onLoad: function () { this.setData({message: 'hello world'}); } // WePY <view> {{ message }} </view> onLoad () { this.message = 'hello world'; }
重要提醒
注意
WePY中的methods属性只能声明页面wxml标签的bind、catch事件,不能声明自定义方法