欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

从VantComponent谈小程序维护-随笔-SegmentFault思否

程序员文章站 2022-06-07 19:18:33
在开发小程序的时候,我们总是期望用以往的技术规范和语法特点来书写当前的小程序,所以才会有各色的小程序框架,例如 mpvue、taro 等这些编译型框架。当然这些框架本身对于新开发的...

在开发小程序的时候,我们总是期望用以往的技术规范和语法特点来书写当前的小程序,所以才会有各色的小程序框架,例如 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&paramb=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;
      });
    }
  }
});

写在后面

js 是一门灵活的语言(手动滑稽) 本身 小程序 component 在 小程序 page 之后,就要比page 更加成熟好用,有时候新的方案往往藏在文档之中,每次多看几遍文档绝不是没有意义的。 小程序版本 版本2.6.1 component 目前已经实现了 observers,可以监听 props data 数据监听器,目前 vantcomponent没有实现,当然本身而言,page 不需要对 prop 进行监听,因为进入页面压根不会变,而data变化本身就无需监听,直接调用函数即可,所以对page而言,observers 可有可无。 该方案也只是对 js 代码上有vue的风格,并没在 template 以及 style 做其他文章。 该方案性能一定是有所缺失的,因为computed是每次set都会进行计算,而并非根据set 的 data 来进行操作,在删减之后我认为本身是可以接受。如果本身对于vue的语法特性需求不高,可以直接利用 component 来编写 page,选择不同的解决方案实质上是需要权衡各种利弊。如果本身是有其他要求或者新的项目,仍旧推荐使用新技术,如果本身是已有项目并且需要维护的,同时又想拥有 vue 特性。可以使用该方案,因为代码本身较少,而且本身也可以基于自身需求修改。 同时,vant-weapp是一个非常不错的项目,推荐各位可以去查看以及star。