在开发小程序的时候,我们总是期望用以往的技术规范和语法特点来书写当前的小程序,所以才会有各色的小程序框架,例如 mpvue、taro 等这些编译型框架。当然这些框架本身对于新开发的项目是有所帮助。而对于老项目,我们又想要利用 vue 的语法特性进行维护,又该如何呢?
在此我研究了一下youzan的 vant-weapp。而发现该项目中的是如此编写的。
import { vantcomponent } from '../common/component';
vantcomponent({
mixins: [],
props: {
name: string,
size: string
},
// 可以使用 watch 来监控 props 变化
// 其实就是把properties中的observer提取出来
watch: {
name(newval) {
...
},
// 可以直接使用字符串 代替函数调用
size: 'changesize'
},
// 使用计算属性 来 获取数据,可以在 wxml直接使用
computed: {
bigsize() {
return this.data.size + 100
}
},
data: {
size: 0
},
methods: {
onclick() {
this.$emit('click');
},
changesize(size) {
// 使用set
this.set(size)
}
},
// 对应小程序组件 created 周期
beforecreate() {},
// 对应小程序组件 attached 周期
created() {},
// 对应小程序组件 ready 周期
mounted() {},
// 对应小程序组件 detached 周期
destroyed: {}
});
居然发现该组件写法整体上类似于 vue 语法。而本身却没有任何编译。看来问题是出在了导入的 vantcomponet 这个方法上。下面我们开始详细介绍一下如何利用 vantcomponet 来对老项目进行维护。
tldr (不多废话,先说结论)
小程序组件写法这里就不再介绍。这里我们给出利用 vantcomponent 写 page 的代码风格。
import { vantcomponent } from '../common/component';
vantcomponent({
mixins: [],
props: {
a: string,
b: number
},
// 在页面这里 watch 基本上是没有作用了,因为只做了props 变化的watch,page不会出现 props 变化
// 后面会详细说明为何
watch: {},
// 计算属性仍旧可用
computed: {
d() {
return c++
}
},
methods: {
onload() {}
},
created() {},
// 其他组件生命周期
})
这里你可能感到疑惑,vantcomponet 不是对组件 component 生效的吗?怎么会对页面 page 生效呢。事实上,我们是可以使用组件来构造小程序页面的。
在官方文档中,我们可以看到 使用 component 构造器构造页面
事实上,小程序的页面也可以视为自定义组件。因而,页面也可以使用 component 构造器构造,拥有与普通组件一样的定义段与实例方法。代码编写如下:
component({
// 可以使用组件的 behaviors 机制,虽然 react 觉得 mixins 并不是一个很好的方案
// 但是在某种程度该方案的确可以复用相同的逻辑代码
behaviors: [mybehavior],
// 对应于page的options,与此本身是有类型的,而从options 取得数据均为 string类型
// 访问 页面 /pages/index/index?parama=123¶mb=xyz
// 如果声明有属性 parama 或 paramb ,则它们会被赋值为 123 或 xyz,而不是 string类型
properties: {
parama: number,
paramb: string,
},
methods: {
// onload 不需要 option
// 但是页面级别的生命周期却只能写道 methods中来
onload() {
this.data.parama // 页面参数 parama 的值 123
this.data.paramb // 页面参数 paramb 的值 ’xyz’
}
}
})
那么组件的生命周期和页面的生命周期又是怎么对应的呢。经过一番测试,得出结果为: (为了简便。只会列出 重要的的生命周期)
// 组件实例被创建 到 组件实例进入页面节点树 component created -> component attched -> // 页面页面加载 到 组件在视图层布局完成 page onload -> component ready -> // 页面卸载 到 组件实例被从页面节点树移除 page onunload -> component detached
当然 我们重点不是在 onload 和 onunload 中间的状态,因为中间状态的时候,我们可以在页面中使用页面生命周期来操作更好。
某些时候我们的一些初始化代码不应该放在 onload 里面,我们可以考虑放在 component create 进行操作,甚至可以利用 behaviors 来复用初始化代码。
某种方面来说,如果不需要 vue 风格,我们在老项目中直接利用 component 代替 page 也不失为一个不错的维护方案。毕竟官方标准,不用担心其他一系列后续问题。
vantcomponent 解析
vantcomponent
此时,我们对 vantcomponent 开始进行解析
// 赋值,根据 map 的 key 和 value 来进行操作
function mapkeys(source: object, target: object, map: object) {
object.keys(map).foreach(key => {
if (source[key]) {
// 目标对象 的 map[key] 对应 源数据对象的 key
target[map[key]] = source[key];
}
});
}
// ts代码,也就是 泛型
function vantcomponent<data, props, watch, methods, computed>(
vantoptions: vantcomponentoptions<
data,
props,
watch,
methods,
computed,
combinedcomponentinstance<data, props, watch, methods, computed>
> = {}
): void {
const options: any = {};
// 用function 来拷贝 新的数据,也就是我们可以用的 vue 风格
mapkeys(vantoptions, options, {
data: 'data',
props: 'properties',
mixins: 'behaviors',
methods: 'methods',
beforecreate: 'created',
created: 'attached',
mounted: 'ready',
relations: 'relations',
destroyed: 'detached',
classes: 'externalclasses'
});
// 对组件间关系进行编辑,但是page不需要,可以删除
const { relation } = vantoptions;
if (relation) {
options.relations = object.assign(options.relations || {}, {
[`../${relation.name}/index`]: relation
});
}
// 对组件默认添加 externalclasses,但是page不需要,可以删除
// add default externalclasses
options.externalclasses = options.externalclasses || [];
options.externalclasses.push('custom-class');
// 对组件默认添加 basic,封装了 $emit 和小程序节点查询方法,可以删除
// add default behaviors
options.behaviors = options.behaviors || [];
options.behaviors.push(basic);
// map field to form-field behavior
// 默认添加 内置 behavior wx://form-field
// 它使得这个自定义组件有类似于表单控件的行为。
// 可以研究下文给出的 内置behaviors
if (vantoptions.field) {
options.behaviors.push('wx://form-field');
}
// add default options
// 添加组件默认配置,多slot
options.options = {
multipleslots: true,// 在组件定义时的选项中启用多slot支持
// 如果这个 component 构造器用于构造页面 ,则默认值为 shared
// 组件的apply-shared,可以研究下文给出的 组件样式隔离
addglobalclass: true
};
// 监控 vantoptions
observe(vantoptions, options);
// 把当前重新配置的options 放入component
component(options);
}
内置behaviors
组件样式隔离
basic behaviors
刚刚我们谈到 basic behaviors,代码如下所示
export const basic = behavior({
methods: {
// 调用 $emit组件 实际上是使用了 triggerevent
$emit() {
this.triggerevent.apply(this, arguments);
},
// 封装 程序节点查询
getrect(selector: string, all: boolean) {
return new promise(resolve => {
wx.createselectorquery()
.in(this)[all ? 'selectall' : 'select'](selector)
.boundingclientrect(rect => {
if (all && array.isarray(rect) && rect.length) {
resolve(rect);
}
if (!all && rect) {
resolve(rect);
}
})
.exec();
});
}
}
});
observe
小程序 watch 和 computed的 代码解析
export function observe(vantoptions, options) {
// 从传入的 option中得到 watch computed
const { watch, computed } = vantoptions;
// 添加 behavior
options.behaviors.push(behavior);
/// 如果有 watch 对象
if (watch) {
const props = options.properties || {};
// 例如:
// props: {
// a: string
// },
// watch: {
// a(val) {
// // 每次val变化时候打印
// consol.log(val)
// }
}
object.keys(watch).foreach(key => {
// watch只会对prop中的数据进行 监视
if (key in props) {
let prop = props[key];
if (prop === null || !('type' in prop)) {
prop = { type: prop };
}
// prop的observer被watch赋值,也就是小程序组件本身的功能。
prop.observer = watch[key];
// 把当前的key 放入prop
props[key] = prop;
}
});
// 经过此方法
// props: {
// a: {
// type: string,
// observer: (val) {
// console.log(val)
// }
// }
// }
options.properties = props;
}
// 对计算属性进行封装
if (computed) {
options.methods = options.methods || {};
options.methods.$options = () => vantoptions;
if (options.properties) {
// 监视props,如果props发生改变,计算属性本身也要变
observeprops(options.properties);
}
}
}
observeprops
现在剩下的也就是 observeprops 以及 behavior 两个文件了,这两个都是为了计算属性而生成的,这里我们先解释 observeprops 代码
export function observeprops(props) {
if (!props) {
return;
}
object.keys(props).foreach(key => {
let prop = props[key];
if (prop === null || !('type' in prop)) {
prop = { type: prop };
}
// 保存之前的 observer,也就是上一个代码生成的prop
let { observer } = prop;
prop.observer = function() {
if (observer) {
if (typeof observer === 'string') {
observer = this[observer];
}
// 调用之前保存的 observer
observer.apply(this, arguments);
}
// 在发生改变的时候调用一次 set 来重置计算属性
this.set();
};
// 把修改的props 赋值回去
props[key] = prop;
});
}
behavior
最终 behavior,也就算 computed 实现机制
// 异步调用 setdata
function setasync(context: weapp.component, data: object) {
return new promise(resolve => {
context.setdata(data, resolve);
});
};
export const behavior = behavior({
created() {
if (!this.$options) {
return;
}
// 缓存
const cache = {};
const { computed } = this.$options();
const keys = object.keys(computed);
this.calccomputed = () => {
// 需要更新的数据
const needupdate = {};
keys.foreach(key => {
const value = computed[key].call(this);
// 缓存数据不等当前计算数值
if (cache[key] !== value) {
cache[key] = needupdate[key] = value;
}
});
// 返回需要的更新的 computed
return needupdate;
};
},
attached() {
// 在 attached 周期 调用一次,算出当前的computed数值
this.set();
},
methods: {
// set data and set computed data
// set可以使用callback 和 then
set(data: object, callback: function) {
const stack = [];
// set时候放入数据
if (data) {
stack.push(setasync(this, data));
}
if (this.calccomputed) {
// 有计算属性,同样也放入 stack中,但是每次set都会调用一次,props改变也会调用
stack.push(setasync(this, this.calccomputed()));
}
return promise.all(stack).then(res => {
// 所有 data以及计算属性都完成后调用callback
if (callback && typeof callback === 'function') {
callback.call(this);
}
return res;
});
}
}
});
数学老师指着黑板问约不约