在说说WePY框架之前,先小结一下自己对前端工作的基本认识。对普通的前端开发者来讲,做到心中有数,方能得心应手。在工作中遇到问题时,能快速地定位到是环境层面的问题?还是开发层面的问题?还是性能优化层面的问题?这很重要,因为快速地定位问题是找到问题根源的前提,进而做到宠辱不惊。我通常把日常工作拆解为如下三大模块的任务:
任务1:基于(nodejs/npm/webpack/eslint/editorconfig/git)等工具搭建工程开发环境,以保证我们可以在开发环境中使用ES6、TypeScript、Less等更加便捷的进阶技术,可以使用npm对依赖库进行管理,可以使用eslint进行代码规范等。所谓“工欲善其事,必先利其器”,小到代码编辑器的选择、插件的选择、代码高亮等;大到构建出合理的工程结构目录以便于代码管理,并实现开发环境下热更新、代理服务,前端自动化等。
任务2:借助于前端框架(Vue/React/Angular/Wepy)等实现MVVM组件化开发,灵活使用这些框架所提供的特色功能,如组件的设计与复用、组件实例的生命周期与钩子函数、组件之间的数据传递与事件通信、Slot内容分发插槽,Mixin混合、数据动态绑定、事件绑定、列表循环渲染、表单输入与取值、条件渲染、动态样式等。在这个任务下,要深刻理解当前所选用框架的核心特点并灵活使用。特别是组件的设计与封装,需要花工夫精心设计,这不仅需要开发者对框架特色深入理解,还需要开发者对产品、UI和数据接口的综合考虑;可扩展性、灵活性,是组件设计的核心。
任务3:最终使用webpack打包工具进行打包,代码优化,性能优化、部署上线等。但凡涉及到这方面的话题,就比较复杂,大多数情况下我们使用框架配套的命令行工具可以构建出标准模板的项目目录以及打包配置,比如Vue的 vue-cli,Wepy的 wepy-cli等。打包优化常常使用的就是Webpack这个神奇而牛逼的工具了。
前端工作乱如麻,希望一点小结能帮助自己再次梳理工作中的主要内容,对知识进行对比性学习,不盲目崇拜任何一项技术,不跟风;而应该结合项目的实际需求灵活地选择框架和技术。大家都说前端框架大同小异,我也希望有时间和机会能深入地研究其中之一的源码,通过源码分析帮助自己更加深入地理解前端开发工作。好了,就扯淡这么多了,接下来就分享一下“关于WePY框架入门使用”所需要掌握的一些基本知识点。
1、安装Wepy脚手架工具
在 nodejs / npm 环境下安装 wepy-cli :
npm install wepy-cli -g
使用 wepy-cli 创建项目标准模板:
wepy init standard project_name
cd project_name
npm install
npm run dev // 运行开发环境
项目打包:
npm run build
2、认识 Wepy项目的目录结构
其中,/src/components/ 目录用于放置我们自定义的组件,/src/pages/ 目录用于放置程序中的页面。app.wpy 是程序的入口文件,放置着各种配置项。WePY框架对原生小程序开发进行了二次封装,将更加贴近于MVVM架构模式。
注意(重要):应在WePY项目的根目录添加 project.config.json文件,它的作用如下图示:
3、代码编辑器和调试环境
建议使用 atom 编辑器进行开发,配置代码高亮的方法如下:
atom调试环境直接使用“微信开发者工具”,调试前得先申请一个小程序账号,需要 appid才能打开WePY小程序项目。
4、WePY代码规范
1)变量、方法名使用驼峰命名法,并且要避免使用 开头的。
2)程序入口文件、组件文件、页面文件,都以 .wpy 为后缀。其它外链文件不受限制。
3)要使用 ES6 语法进行开发,整个WePY框架就是在 ES6 下开发的。
4)WePY 支持 Promise,可以直接在程序中使用 async / await 等新特性。
5)小程序原生开发中的事件绑定,如 bindtap="click",在 WePY框架下要替换成 @tap="click";原生的 catchtap="click",在WePY框架下要替换成 @tap.stop="click"。更多关于事件绑定的变化,请参见WePY官网之“组件自定义事件”。
6)小程序原生开发中的事件传参 bindtap="click" data-item={{item}} ,在WePY框架下优化成了 @tap="click({{item}})",更加简洁了。
7)自定义组件的命名,应该避免与微信原生组件名称相同。
5、组件化开发模式
WePY支持组件化开发模式,文件模板如下:
// demo.wpy
<template>
<view>HTML部分</view>
</template>
<script>
import wepy from 'wepy';
export default class Demo extends wepy.page {
}
</script>
<style lang="less">
</style>
从上述文件模板看,其代码组织与 Vue 组件化开发模式几乎一模一样。把小程序原生开发中的 demo.wxml、demo.wxss、demo.json、demo.js 四个文件合并成了一个 demo.wpy 文件。
.wpy文件的编译过程三种标签分别支持哪些语法?如下图示:
三种标签所支持的语法
6、小程序入口文件 app.wpy 与 小程序实例
<script>
import wepy from 'wepy'
import 'wepy-async-function'
export default class extends wepy.app {
config = {
pages: [
'pages/demo',
],
window: {
navigationStyle: 'custom'
}
}
globalData = {
userInfo: null,
}
constructor () {
super()
this.use('requestfix')
console.log('app实例', this)
}
onLaunch() {
// 测试异步
this.testAsync()
wepy.login({
success (res) {
console.log('登录', res)
}
})
}
// 获取用户信息
getUserInfo(cb) {
const that = this
if (this.globalData.userInfo) {
return this.globalData.userInfo
}
wepy.getUserInfo({
success (res) {
that.globalData.userInfo = res.userInfo
cb && cb(res.userInfo)
}
})
}
// 测试异步
async testAsync () {
const data = await this.sleep(3)
console.log('async', data)
}
sleep (s) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('promise resolved')
}, s * 1000)
})
}
}
</script>
入口文件app.wpy中所声明的小程序实例继承自wepy.app类,包含一个config属性和其它全局属性、方法、事件。其中config属性对应原生的app.json文件,build编译时会根据config属性自动生成app.json文件,如果需要修改config中的内容,请使用微信提供的相关API。
在页面实例中,可以通过 this.$parent
来访问 App 实例;在子组件中可以使用 this.$parent
访问父组件实例。
7、页面实例 与 组件实例
<template> </ template>
<script>
import wepy from 'wepy';
export default class MyPage extends wepy.page { // 定义页面
export default class MyComponent extends wepy.component { // 定义组件
customData = {} // 自定义数据
customFunction () {} //自定义方法
onLoad () {} // 在Page和Component共用的生命周期函数
onShow () {} // 只在Page中存在的页面生命周期函数
config = {}; // 只在Page实例中存在的配置数据,对应于原生的page.json文件
data = {}; // 页面所需数据均需在这里声明,可用于模板数据绑定
components = {}; // 声明页面中所引用的组件,或声明组件中所引用的子组件
mixins = []; // 声明页面所引用的Mixin实例
computed = {}; // 声明计算属性(详见后文介绍)
watch = {}; // 声明数据watcher(详见后文介绍)
methods = {}; // 声明页面wxml中标签的事件处理函数。注意,此处只用于声明页面wxml中标签的bind、catch事件,自定义方法需以自定义方法的方式声明
events = {}; // 声明组件之间的事件处理函数
}
<script />
<style lang="less"></ style>
属性解读
这里需要特别强调一下:WePY中的methods属性只能声明页面 template 标签中的@ 事件所需要的绑定方法,不能声明自定义方法,切记。
在 WePY 中,小程序被分为三个实例:小程序实例App、页面实例Page、组件实例Component。其中Page实例继承自Component。它们各自的声明方式如下:
import wepy from 'wepy';
// 声明一个App小程序实例
export default class MyAPP extends wepy.app {
}
// 声明一个Page页面实例
export default class IndexPage extends wepy.page {
}
// 声明一个Component组件实例
export default class MyComponent extends wepy.component {
}
8、computed 计算属性
computed计算属性,是一个有返回值的函数,可直接被当作绑定数据来使用。因此类似于data属性,代码中可通过this.计算属性名来引用,模板中也可通过{{ 计算属性名 }}来绑定数据。当组件中有任何数据发生了改变,那么所有计算属性就都会被重新计算。示例代码如下:
<template>
<view>{{aPlus}}</view>
</template>
<script>
import wepy from 'wepy';
export default class Demo extends wepy.page {
data = {
a: 1
}
// 计算属性aPlus,在脚本中可通过this.aPlus来引用,在模板中可通过{{ aPlus }}来插值
computed = {
aPlus () {
return this.a + 1
}
}
}
</script>
9、watcher 监听器
监听器watcher能够监听到任何属性的更新。监听器在watch属性中声明,类型为函数,函数名与需要被监听的data对象中的属性同名,每当被监听的属性改变一次,监听器函数就会被自动调用执行一次。监听器适用于当属性改变时需要进行某些额外处理的情景。示例代码如下:
<script>
import wepy from 'wepy';
export default class Demo extends wepy.page {
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)
}
}
</script>
注意:监听器函数名必须跟需要被监听的data对象中的属性num同名。
10、模块化与作用域
在WePY中实现了小程序的组件化、模块化开发,组件的所有业务与功能在组件本身内部实现,组件与组件之间是彼此隔离的。比如在两个不同组件中定义了两个同名的事件方法,基于模块化的设计,那么这两个同名的事件方法事实上是没有任何关联的,它们互不影响。WePY编译组件的过程如下:
组件编译过程
11、列表循环渲染
在WePY中,使用了 <repeat> 标签替代了原生小程序中的 wx:for 循环渲染。示例代码如下:
<template>
<!-- 注意,使用for属性,而不是使用wx:for属性 -->
<repeat for="{{list}}" key="index" index="index" item="item">
<!-- 插入<script>脚本部分所声明的child组件,同时传入item -->
<child :item="item"></child>
</repeat>
</template>
12、在组件或页面中引用其它组件
<template>
<view>
<!-- 在 -->
<child1></child1>
<child2></child2>
<child3></child3>
</view>
</template>
<script>
import wepy from 'wepy';
//引入组件文件
import Child from '../components/child';
export default class Index extends wepy.component {
//声明组件,分配组件id为child
components = {
//为三个相同组件的不同实例分配不同的组件ID,从而避免数据同步变化的问题
child1: Child,
child2: Child,
child3: Child
};
}
</script>
当组件或页面中需要引入其它组件时,必须在.wpy文件的<script>脚本部分先import组件文件,然后在components属性中给组件声明唯一的组件ID,接着在<template>模板部分中添加以components对象中所声明的组件ID进行命名的自定义标签以插入组件。
需要注意的是,WePY中的组件都是静态组件,是以组件ID作为唯一标识的,每一个ID都对应一个组件实例,当页面引入两个相同ID的组件时,这两个组件共用同一个实例与数据,当其中一个组件数据变化时,另外一个也会一起变化。为了避免这个问题,则需要分配多个组件ID和实例,如上述代码中同一个组件的三个ID —— child1、child2、child3。
页面可以引入组件,而组件还可以引入子组件。一个页面引入若干组件后,组件结构如下图(组件树):
组件树
13、父子组件之间的 props 传值
props传值在WePY中属于父子组件之间传值的一种机制,包括静态传值与动态传值。
1)静态传值
静态传值只能从父组件向子组件传递常量数据,且为String字符串类型。在子组件使用props对象属性来接收从父组件传递过来的值。示例代码如下:
在父组件中:
<template>
<child name="geekxia"></child>
</template>
<script>
import wepy from 'wepy';
import Child from '../components/child';
export default class Parent extends wepy.page {
components = {
child: Child
}
}
</script>
在子组件中使用 props 对象属性接收父组件的传值:
<template>
<view>{{name}}</view>
</template>
<script>
import wepy from 'wepy';
export default class Child exntends wepy.component {
props = {
name: String, // 接收传值
}
}
</script>
2)动态传值
动态传值是指父组件向子组件传递动态数据,父子组件数据完全独立且互不干扰。所谓动态传值,即传给子组件的数据不是常量,而是变量,且在父子组件内部分别发生变化时,不会影响到另一方的变化。
当在父组件中使用 .sync修饰符的props传值,在父组件中改变这个props传值时,会同步改变子组件中这个对应的值。(即由父组件向子组件中流动)
当在子组件的 props对象属性中使用 twoWay: true 修饰所要传递的值,在子组件中改变这个props传值时,会同步改变父组件中这个对应的传值。(即由子组件向父组件中流动)
当在父组件中使用 .sync修饰,并同时在子组件中使用 twoWay:true 修饰时,可以实现父子组件之间的数据双向绑定,即这个要传递的值,在父子组件任意一方的变化都会同步更新到另一方。如下示例
父组件代码如下:
<template>
<view>
<child :name1.sync="name1" :name2="nam2" :name3.sync="name3"></child>
</view>
</template>
<script>
import wepy from 'wepy';
import Child from '../components/child';
export default class Parent extends wepy.page {
components = {
child: Child
}
data = {
name1: '1',
name2: '2',
nmae3: '3'
}
}
</script>
子组件代码如下:
<template>
<view>
<view>{{name1}}</view>
<view>{{name2}}</view>
<view>{{name3}}</view>
</view>
</template>
<script>
import wepy from 'wepy';
export default class Child exntends wepy.component {
props = {
// 父组件向子组件 单向传值
name1: {
type: String,
default: "geekxia",
twoWay: false, // 默认是 false
},
// 子组件向父组件 单向传值
name2: {
type: String,
default: "geekxia",
twoWay: true
},
// 数据双向绑定
name3: {
type: String,
default: "geekxia",
twoWay: true
}
}
}
</script>
如上示例,当在父组件中改变 name1 时,子组件中的name1将会同步改变;当在子组件中改变 name2时,父组件中的 name2 将会同步改变。当父子组件其中任意一方改变 name3时,另一方中的 name3 将会同步改变。
14、组件之间的事件通信与交互
WePY 提供了 $broadcast、$emit、$invoke
三个方法用于组件之间的通信和交互。注意:用于组件之间事件通信与交互的事件处理函数要写在组件和页面的events对象属性中去。
1)$broadcast
事件机制
$broadcast
事件是由父组件发起,所有子组件都会收到此广播事件,除非事件被手动取消。事件广播的顺序为广度优先搜索顺序,如上图,如果页面Page_Index发起一个$broadcast
事件,那么按先后顺序依次接收到该事件的组件为:ComA、ComB、ComC、ComD、ComE、ComF、ComG、ComH。如下图:
2)$emit
事件机制
$emit
与$broadcast
正好相反,事件发起组件的所有祖先组件会依次接收到$emit
事件。如果组件ComE发起一个$emit
事件,那么接收到事件的先后顺序为:组件ComA、页面Page_Index。如下图:
3)$invoke
直接调用另一个组件中 event 事件
$invoke
是一个页面或组件对另一个组件中的方法的直接调用,通过传入组件路径找到相应的组件,然后再调用其方法。如,在组件ComA中直接调用组件ComG的某个方法:
this.$invoke('./../ComB/ComG', 'someMethod', 'someArgs');
15、三种事件修饰符与自定义事件
1).default 绑定小程序冒泡型事件,即原生中的 bindXXX,.default 默认可以省略不写。
2).stop 绑定小程序捕获型事件,即原生中的 catchXXX。
3).user 绑定自定义事件,并且自定义事件只能在子组件中通过 $emit
来触发。
使用以上三个修饰符所修饰的事件,都必须写在 methods 对象属性中。如果写在了 event 对象属性中,将不会被触发。示例如下
父组件代码如下:
<template>
<view>
<!-- 绑定冒泡事件 -->
<view @tap.default="bindEvent"></view>
<!-- 绑定捕获事件 -->
<view @tap.stop="bindEvent"></view>
<!-- 给子组件绑定 自定义事件 -->
<child @customFn.user="customEvent"></child>
</view>
</template>
<script>
import wepy from 'wepy';
import Child from '../components/child';
export default class Parent extends wepy.page {
components = {
child: Child
}
methods = {
bindEvent() {
console.log('冒泡事件');
},
catchEvent() {
console.log('捕获事件');
},
customEvent(arg1, arg2) {
console.log('自定义事件,且只能在子组件中使用 $emit 来触发');
console.log(arg1, arg2);
}
}
}
</script>
子组件代码如下:
<template>
<view>
<view @tap="click"></view>
</view>
</template>
<script>
import wepy from 'wepy';
export default class Child exntends wepy.component {
methods = {
click() {
console.log('这里是子组件中的事件');
// 触发父组件中的自定义事件
$emit("customFn", "arg1", "arg2");
}
}
}
</script>
从上述代码可以看出,.default用于控制冒泡事件,.stop用于控制捕获事件,.user用于定义自定义事件(自定义事件用于在子组件中触发)。事件修饰符非常有用,要理解它们的基本使用。
16、slot 内容分发插槽
WePY中的slot插槽作为内容分发标签的空间占位标签,便于在父组件中通过对相当于扩展板卡的内容分发标签的“插拔”,更为灵活、方便地对子组件进行内容分发。示例如下
定义Modal弹框子组件,并使用 slot 插槽分发弹框的头部、内容体和按钮组:
<template>
<!-- 弹框下面的遮罩层 -->
<view class="layer">
<!-- 弹框 -->
<view class="modal" style="height:{{height}}rpx;margin-top:{{-0.5*height}}rpx;">
<image class="modal-close" src="../assets/image/icon/close.png" @tap="closeModal"></image>
<view class="modal-title">
<!-- 弹框的头部插槽 -->
<slot name="title"></slot>
</view>
<view class="modal-content">
<!-- 弹框的内容体插槽 -->
<slot name="content"></slot>
</view>
<view class="modal-btns">
<!-- 弹框的按钮组插槽 -->
<slot name="btns"></slot>
</view>
</view>
</view>
</template>
<script>
import wepy from 'wepy';
export default class Modal extends wepy.component {
props = {
height: String
}
methods = {
// 关闭Modal
closeModal() {
this.$emit('closeModal');
},
}
}
</script>
在父组中使用这个Modal 弹框子组件:
<template>
<view>
<modal hidden="{{modalHide}}" height="492" @closeModal.user="closeModal">
<view slot="title">
<text>更换手机号</text>
</view>
<view slot="content">
<view>
<input class="full" type="text" placeholder="请输入手机号" />
</view>
<view>
<input class="half" type="text" placeholder="短信验证码" />
<text>获取验证码</text>
</view>
</view>
<view slot="btns">
<text class="one-btn">确定</text>
</view>
</modal>
</view>
</template>
<script>
import wepy from 'wepy';
import Modal from '../components/modal';
export default class Parent extends wepy.page {
components = {
modal: Modal
}
data = {
modalHide: true,
}
methods = {
closeModal() {
this.modalHide = true;
}
}
}
</script>
从上述代码可见,使用 slot 内容插槽可以灵活方便地封装 UI 组件。如 Modal 弹框中的内容体可以因实际情景而不同,slot 就派上了大用场。
17、Mixin 混合
Mixin 混合可以把组件之间的可复用部分抽离出来,从而在组件中使用Mixin 混合时,可以将混合的数据、事件以及方法注入到组件之中。Mixin 混合又为两种模式,分别是默认式混合和兼容式混合。代码如下
定义一个 mixin ,代码如下:
import wepy from 'wepy'
export default class testMixin extends wepy.mixin {
data = {
foo: 'foo from mixin',
bar: 'bar from mixin'
}
methods = {
click () {
console.log('tap from mixin');
}
}
onShow() {
console.log('mixin -> onShow')
}
}
在组件或页面中使用 Mixin ,代码如下:
<template>
<view @tap="click"></view>
</template>
<script>
import wepy from 'wepy';
import testMixin from '../mixin/test.js';
export default class Demo extends wepy.page {
mixins = [testMixin]
data = {
foo: 'foo from index'
}
methods = {
click () {
console.log('tap from index');
}
}
onShow () {
console.log(this.foo); // foo from index
console.log(this.bar); // bar from mixin
}
}
</script>
对于组件的 data、components、events以及methods中的.user自定义事件,都将采用默认式的Mixin混合,即组件中优先使用自身定义的数据。如果组件中未定义这些数据时,将使用Mixin混合中的数据。
对于组件的 methods 中非自定义事件(即小程序页面中的响应事件),则采用兼容式的 Mixin混合,即先响应组件自身事件,再接着响应从Mixin中混合而来的事件。另需说明,当组件中引入了Mixin混合时,同样是组件的 onShow / onLoad 函数先执行,Mixin 的 onShow / onLoad 后执行;这个顺序与页面事件的执行顺序是一致的,即兼容式Mixin混合。
18、数据绑定的变化
在原生小程序中,使用 this.setData() 来绑定数据,如下代码:
this.setData({title: 'this is title'});
WePY使用脏数据检查对setData进行封装,在函数运行周期结束时执行脏数据检查,一来可以不用关心页面多次setData是否会有性能上的问题,二来可以更加简洁去修改数据实现绑定,不用重复去写setData方法。数据绑定像如下做:
this.title = 'this is title';
需注意的是,在异步函数中更新数据的时候,必须手动调用$apply方法,才会触发脏数据检查流程的运行。示例如下:
setTimeout(() => {
this.title = 'this is title';
this.$apply();
}, 3000);
脏数据检查的流程,如下示意图:
脏检查流程
19、事件传参的变化
原生小程序中的事件传参,通常做法如下:
<view data-id="{{index}}" data-title="wepy" data-other="otherparams" bindtap="click"></view>
Page({
click: function (e) {
console.log(e.currentTarget.dataset.id);
console.log(e.currentTarget.dataset.title);
console.log(e.currentTarget.dataset.other);
}
});
在WePY优化之后可以更加方便地为事件传递参数了,示例如下:
<template>
<view @tap="click({{index}},{{item}},"1")"></view>
</template>
<script>
import wepy from 'wepy';
export default class Demo extends wepy.page {
methods = {
click (index, item, arg, e) {
console.log(index);
console.log(item);
console.log(arg);
console.log(e);
}
}
}
</script>
20、Intercept 拦截器
在WePY中提供了全局的 intercept 拦截器,以对原生API的请求进行拦截。具体做法是配置 API 的config、fail、success、complete回调函数。示例代码如下:
import wepy from 'wepy';
export default class extends wepy.app {
constructor () {
// this is not allowed before super()
super();
// 拦截 request 请求
this.intercept('request', {
// 发出请求时的回调函数
config (p) {
// 对所有request请求中的OBJECT参数对象统一附加时间戳属性
p.timestamp = +new Date();
console.log('config request: ', p);
// 必须返回OBJECT参数对象,否则无法发送请求到服务端
return p;
},
// 请求成功后的回调函数
success (p) {
// 可以在这里对收到的响应数据对象进行加工处理
console.log('request success: ', p);
// 必须返回响应数据对象,否则后续无法对响应数据进行处理
return p;
},
//请求失败后的回调函数
fail (p) {
console.log('request fail: ', p);
// 必须返回响应数据对象,否则后续无法对响应数据进行处理
return p;
},
// 请求完成时的回调函数(请求成功或失败都会被执行)
complete (p) {
console.log('request complete: ', p);
}
});
}
}
上述代码,我们对小程序原生 API —— request() 进行拦截处理。在 request 的前、中、后分别做了相应的逻辑处理,可以理解成把一个任务拆解成多个阶段,并在不同的阶段中去实现我们想做的特定任务。
至此关于 WePY的基础使用,就整理到这里。这些基本的知识点,在各大MVVM框架中或多或小都有使用到。学会使用一个框架,其它框架可谓大同小异。把每一个技术点产生的原因,为了解决什么问题搞明白了,这将有助于我们举一反三,灵活地使用各个技术点,在项目开发过程中做到宠辱不惊,以不变应千变。
END 2018年12月1日
网友评论