微信小程序的组件系统底层框架实现组件框架底层

  微信小程序的组件系统底层是通过 Exparser 组件框架实现,它内置在小程序的基础库中,小程序内的所有组件,包括内置组件和自定义组件都由 Exparser 组织管理。

  自定义组件和写页面一样包含以下几种文件:

  以编写一个 tab 组件为例: 编写自定义组件时需要在 json 文件中讲 component 字段设为 true:

  <pre class="brush:js;">{

"component": true
re>

  在 js 文件中,基础库提供有 Page 和 Component 两个构造器,Page 对应的页面为页面根组件,Component 则对应:

  <pre class="brush:js;">Component({

options: { // 组件配置
    addGlobalClass: true,
    // 指定所有 _ 开头的数据字段为纯数据字段
    // 纯数据字段是一些不用于界面渲染的 data 字段,可以用于提升页面更新性能
    pureDataPattern: /^_/, 
    multipleSlots: true // 在组件定义时的选项中启用多slot支持
},
properties: {
    vtabs: {type: Array, value: []},
},
data: {
    currentView: 0,
},
observers: { // 监测
    activeTab: function(activeTab) {
        this.scrollTabBar(activeTab);
    }
}, 
relations: {  // 关联的子/父组件
    '../vtabs-content/index': {
        type: 'child', // 关联的目标节点应为子节点
        linked: function(target) {
            this.calcVtabsCotentHeight(target);
        },
        unlinked: function(target) {
            delete this.data._contentHeight[target.data.tabIndex];
        }
    }
},
lifetimes: { // 组件声明周期
    created: function() {
        // 组件实例刚刚被创建好时
    },
    attached: function() {
        // 在组件实例进入页面节点树时执行
    },
    detached: function() {
        // 在组件实例被从页面节点树移除时执行
    },
},
methods: { // 组件方法
    calcVtabsCotentHeight(target) {}
} 
/pre>

  如果有了解过 Vue2 的小伙伴,会发现这个声明很熟悉。

  在小程序启动时,构造器会将开发者设置的properties、data、methods等定义段,

  写入Exparser的组件注册表中。这个组件在被其它组件引用时,就可以根据这些注册信息来创建自定义组件的实例。

  模版文件 wxml:

  <pre class="brush:xhtml;">


e>

  样式文件:

  <pre class="brush:css;">.vtabs {}</pre>

  外部页面组件使用,只需要在页面的 json 文件中引入

  <pre class="brush:js;">{
"navigationBarTitleText": "商品分类",
"usingComponents": {

"vtabs": "../../../components/vtabs",

}
}</pre>

  在初始化页面时,Exparser 会创建出页面根组件的一个实例,用到的其他组件也会响应创建组件实例(这是一个递归的过程):

  组件创建的过程大致有以下几个要点:

  组件通信

  由于业务的负责度,我们常常需要把一个大型页面拆分为多个组件,多个组件之间需要进行数据通信。

  对于跨代组件通信可以考虑全局状态管理,这里只讨论常见的父子组件通信:

  方法一 WXML 数据绑定

  用于父组件向子组件的指定属性设置数据。

  子声明 properties 属性

  <pre class="brush:js;">Component({

properties: {
    vtabs: {type: Array, value: []}, // 数据项格式为 `{title}`
}
pre>

  父组件调用:

  <pre class="brush:xhtml;"> item[1] || item[0] === 0) // 除了数字 0 外,其他非值都过滤

    .map(
        ([key, value]) => {
            if (typeof value === 'object') {
                // 对象转字符串
                value = JSON.stringify(value);
            }
            if (typeof value === 'string') {
                // 字符串 encode
                value = encodeURIComponent(value);
            }
            return `${key}=${value}`;
        }
    ).join('&');
if (queryString) { // 需要组装参数
    url = `${baseUrl}?${queryString}`;
}

const pageCount = wx.getCurrentPages().length;
if (jumpType === 'navigateTo' && pageCount < 5) {
    wx.navigateTo({ 
        url,
        fail: () => { 
            wx.switch({ url: baseUrl });
        }
    });
} else {
    wx.navigateTo({ 
        url,
        fail: () => { 
            wx.switch({ url: baseUrl });
        }
    });
} 
re>

  jumpTo 辅助函数:

  <pre class="brush:js;">export const resolveSearch = search => {

const queries = {};
cosnt paramList = search.split('&');
paramList.forEach(param => {
    const [key, value = ''] = param.split('=');
    queries[key] = value;
});
return queries;

};
export const resolveUrl = (url) => {

if (url.indexOf('?') === -1) {
    // 不带参数的 url
    return {
        queries: {},
        page: url
    }
}
const [page, search] = url.split('?');
const queries = resolveSearch(search);
return {
    page,
    queries
};
pre>

  在「下单页面A」传递数据:

  <pre class="brush:js;">jumpTo({

url: 'pages/consignment/index', 
{ 
    sender: { name: 'naluduo233' }
}
/pre>

  在「货物信息页面B」获得 URL 参数:

  <pre class="brush:js;">const sender = JSON.parse(getParam('sender') || '{}');</pre>

  url 参数获取辅助函数

  <pre class="brush:js;">// 返回当前页面
export function getCurrentPage() {

const pageStack = wx.getCurrentPages();
const lastIndex = pageStack.length - 1;
const currentPage = pageStack[lastIndex];
return currentPage;

}
// 获取页面 url 参数
export function getParams() {

const currentPage = getCurrentPage() || {};
const allParams = {};
const { route, options } = currentPage;
if (options) {
    const entries = objectEntries(options);
    entries.forEach(
        ([key, value]) => {
            allParams[key] = decodeURIComponent(value);
        }
    );
}
return allParams;

}
// 按字段返回值
export function getParam(name) {

const params = getParams() || {};
return params[name];
re>

  参数过长怎么办?路由 api 不支持携带参数呢?

  虽然微信小程序官方文档没有说明可以页面携带的参数有多长,但还是可能会有参数过长被截断的风险。

  我们可以使用全局数据记录参数值,同时解决 url 参数过长和路由 api 不支持携带参数的问题。

  <pre class="brush:js;">// global-data.js
// 由于 switchTab 不支持携带参数,所以需要考虑使用全局数据存储
// 这里不管是不是 switchTab,先把数据挂载上去
const queryMap = {

page: '',
queries: {}
pre>

  更新跳转函数

  <pre class="brush:js;">export function jumpTo(url, options) {

// ...
Object.assign(queryMap, {
    page: baseUrl,
    queries: options
});
// ...
if (jumpType === 'switchTab') {
    wx.switchTab({ url: baseUrl });
} else if (jumpType === 'navigateTo' && pageCount < 5) {
    wx.navigateTo({ 
        url,
        fail: () => { 
            wx.switch({ url: baseUrl });
        }
    });
} else {
    wx.navigateTo({ 
        url,
        fail: () => { 
            wx.switch({ url: baseUrl });
        }
    });
}
re>

  url 参数获取辅助函数

  <pre class="brush:js;">// 获取页面 url 参数
export function getParams() {

const currentPage = getCurrentPage() || {};
const allParams = {};
const { route, options } = currentPage;
if (options) {
    const entries = objectEntries(options);
    entries.forEach(
        ([key, value]) => {
            allParams[key] = decodeURIComponent(value);
        }
    );
  • if (isTabBar(route)) {
  • // 是 tab-bar 页面,使用挂载到全局的参数
  • const { page, queries } = queryMap;
  • if (page === ${route}) {
  • Object.assign(allParams, queries);
  • }
  • }
    }
    return allParams;
    }</pre>

  辅助函数

  <pre class="brush:js;">// 判断当前路径是否是 tabBar
const { tabBar} = appConfig;
export isTabBar = (route) => tabBar.list.some(({ pagePath })) => pagePath === route); </pre>

  按照这样的逻辑的话,是不是都不用区分是否是 isTabBar 页面了,全部页面都从 queryMap 中获取?这个问题目前后续探究再下结论,因为我目前还没试过从 页面示例的 options 中拿到的值是缺少的。所以可以先保留读取 getCurrentPages 的值。

  方法五 EventChannel 事件派发通信

  前面我谈到从「当前页面A」传递数据到被打开的「页面B」可以通过 url 参数。那么想获取被打开页面传送到当前页面的数据要如何做呢?是否也可以通过 url 参数呢?

  答案是可以的,前提是不需要保存「页面A」的状态。如果要保留「页面 A」的状态,就需要使用 navigateBack 返回上一页,而这个 api 是不支持携带 url 参数的。

  这样时候可以使用 页面间事件通信通道 EventChannel。

  pageA 页面

  <pre class="brush:js;">//
wx.navigateTo({

url: 'pageB?id=1',
events: {
    // 为指定事件添加一个监听器,获取被打开页面传送到当前页面的数据
    acceptDataFromOpenedPage: function(data) {
      console.log(data) 
    },
},
success: function(res) {
    // 通过eventChannel向被打开页面传送数据
    res.eventChannel.emit('acceptDataFromOpenerPage', { data: 'test' })
}
/pre>

  pageB 页面

  <pre class="brush:js;">Page({

onLoad: function(option){
    const eventChannel = this.getOpenerEventChannel()
    eventChannel.emit('acceptDataFromOpenedPage', {data: 'test'});

    // 监听acceptDataFromOpenerPage事件,获取上一页面通过eventChannel传送到当前页面的数据
    eventChannel.on('acceptDataFromOpenerPage', function(data) {
      console.log(data)
    })
  }
pre>

  会出现数据无法监听的情况吗?

  小程序的栈不超过 10 层,如果当前「页面A」不是第 10 层,那么可以使用 navigateTo 跳转保留当前页面,跳转到「页面B」js获取父页面url参数,这个时候「页面B」填写完毕后传递数据给「页面A」时,「页面A」是可以监听到数据的。

  如果当前「页面A」已经是第10个页面,只能使用 redirectTo 跳转「PageB」页面。结果是当前「页面A」出栈,新「页面B」入栈。这个时候将「页面B」传递数据给「页面A」,调用 navigateBack 是无法回到目标「页面A」的,因此数据是无法正常被监听到。

  不过我分析做过的小程序中,栈中很少有10层的情况,5 层的也很少。因为调用 wx.navigateBack 、wx.redirectTo 会关闭当前页面,调用 wx.switchTab 会关闭其他所有非 tabBar 页面。

  所以很少会出现这样无法回到上一页面以监听到数据的情况,如果真出现这种情况,首先要考虑的不是数据的监听问题了,而是要保证如何能够返回上一页面。

  比如在「PageA」页面中先调用 getCurrentPages 获取页面的数量,再把其他的页面删除,之后在跳转「PageB」页面,这样就避免「PageA」调用 wx.redirectTo导致关闭「PageA」。但是官方是不推荐开发者手动更改页面栈的,需要慎重。

  如果有读者遇到这种情况,并知道如何解决这种的话,麻烦告知下,感谢。

  使用自定义的事件中心 EventBus

  除了使用官方提供的 EventChannel 外,我们也可以自定义一个全局的 EventBus 事件中心。 因为这样更加灵活,不需要在调用 wx.navigateTo 等APi里传入参数,多平台的迁移性更强。

  <pre class="brush:js;">export default class EventBus {
private defineEvent = {};
// 注册事件
public register(event: string, cb): void {
if(!this.defineEvent[event]) {
(this.defineEvent[event] = [cb]);
}
else {
this.defineEvent[event].push(cb);
}
}
// 派遣事件
public dispatch(event: string, arg?: any): void {
if(this.defineEvent[event]) {{

        for(let i=0, len = this.defineEvent[event].length; i {
     cb && cb(arg); 
     this.off(event, onceCb); 
    }
    this.register(event, onceCb); 
}
// 清空所有事件
public clean(): void {
    this.defineEvent = {}; 
}

}
export connst eventBus = new EventBus();</pre>

  在 PageA 页面监听:

  <pre class="brush:js;">eventBus.on('update', (data) => console.log(data));</pre>

  在 PageB 页面派发

  <pre class="brush:js;">eventBus.dispatch('someEvent', { name: 'naluduo233'});</pre>

  小结

  本文主要讨论了微信小程序如何自定义组件,涉及两个方面:

  如果你使用的是 taro 的话,直接按照 react 的语法自定义组件就好。而其中的组件通信的话,因为 taro 最终也是会编译为微信小程序js获取父页面url参数,所以 url 和 eventbus 的页面组件通信方式是适用的。后续会分析 vant-ui weapp 的一些组件源码,看看有赞是如何实践的。

  附:组件和页面的区别

  1、点击一个文件夹右键——新建Page 、新建Component

  2、组件的js文件

  <pre class="brush:js;">Component({

  properties: {   //父组件传过来的data
    num: Number,
    flag:Boolean
  },

data: {
},
methods:{ //组件的方法要写在methods中

jia(e) {
  let a
  if (this.properties.flag) {
    a = -1;
  } else {
    a = 1;
  }
  this.setData({
    flag: !this.properties.flag,
    num: this.properties.num + a
  })
}

},
</pre>

  3、页面的js文件

  <pre class="brush:js;">Page({

  data: {
    flag:false,
    number:1,
    motto: 'Hello World',
  },
  //事件处理函数,直接作为参数
  bindViewTap: function() {
    wx.navigateTo({
      url: '../logs/logs'
    })
  },

})
</pre>

  总结

  到此这篇关于微信小程序自定义组件的文章就介绍到这了,更多相关微信小程序自定义组件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

文章由官网发布,如若转载,请注明出处:https://www.veimoz.com/1889
0 评论
581

发表评论

!