小程序基础

348 阅读18分钟

小程序机制介绍

什么是小程序

小程序本质上还是网页

  1. 技术栈与网页开发是一致的,都用到H5、CSS和JS
  2. 区别:不支持浏览器API,宿主环境为微信,只能用微信提供的API

外部代码通过小程序形式,在手机APP里运行:微信、支付宝,小程序可以视为只能用微信等APP作为载体打开和浏览网站。

微信小程序框架介绍

小程序的目录结构

工程目录中包含以下文件: image.png

核心 pages index.wxml 渲染层 index.json/log.json 子页面配置文件 app.js 逻辑层 app.json app的公用配置文件 app.wxss aap的公共样式表

子页面权重 > 公用

技术选型

渲染界面的技术方案:

  1. 纯客户端原生技术
  2. 纯web技术渲染
  3. 客户端原生技术与web技术结合的混合技术(简称Hybrid技术)渲染 Hybrid技术:微信小程序通过 js bridge 和客户端通信,调用相机

方案对比:

  1. 开发门槛:web 门槛低,Native 也有像 RN 这样的框架支持
  2. 体验:Native 要比 Web 好太多,Hybrid 在一定程度上比 Web 接近原生体验
  3. 版本更新:Web 支持在线更新,Native 则需要打包到微信和微信版本一起审核发布
  4. 管控和安全:Web 跳转或改变页面内容,存在一些不可控的因素和安全风险

方案确定:

  1. 小程序的诉诸环境是微信手机APP,用纯客户端原生技术来编写小程序,那么小程序代卖每次都需要与手机 App 代码一期发版 👎
  2. Web 支持一份副本资源包放在云端,通过下载到本地,动态执行后即可渲染出页面,但纯 Web 技术在一些复杂的交互上可能会面临一些性能问题 👎 a. 在 Web 技术中,UI 渲染跟脚本执行都在一个单线程中执行,这就容易导致一些逻辑任务抢占 UI 渲染的资源
  3. 两者结合起来的 Hybrid 来渲染小程序,用一种近似 Web 的方式来开发,并且可以实现在线更新代码 👍 a. 扩展 Web 的能力,比如想输入组件(input,textarea)有更好地控制键盘的能力 b. 体验更好,同时夜减轻 WebView 的渲染工作 c. 用客户端原生渲染内置一些复杂组件,可以提供更好的性能

双线程模型

⼩程序的渲染层和逻辑层分别由 2 个线程管理:

  1. 视图层 -> WebView 进⾏渲染;(数据从逻辑层获取)
  2. 逻辑层 -> JsCore 线程运⾏ JS脚本; image.png

image.png

设计⽬的:为了管控和安全等问题,阻⽌开发者使⽤⼀些,例如浏览器的window对象,跳转⻚⾯、操 作DOM、动态执⾏脚本的开放性接⼝;

使⽤沙箱环境提供纯 JavaScript 的解释执⾏环境

  1. 客户端系统:JavaScript 引擎;
  2. iOS : JavaScriptCore 框架;
  3. 安卓:腾讯 x5 内核提供的 JsCore ;

⼩程序双线程模型:

  • 逻辑层:创建⼀个单独的线程去执⾏ JavaScript,在这⾥执⾏的都是有关⼩程序业务逻辑的代码, 负责逻辑处理、数据请求、接⼝调⽤等;
  • 视图层:界⾯渲染相关的任务全都在 WebView 线程⾥执⾏,通过逻辑层代码去控制渲染哪些界 ⾯。⼀个⼩程序存在多个界⾯,所以视图层存在多个 WebView 线程;
  • JSBridge 起到架起上层开发与Native(系统层)的桥梁,使得⼩程序可通过API使⽤原⽣的功能, 且部分组件为原⽣组件实现,从⽽有良好体验;

数据驱动视图变化

DOM 的更新通过简单的数据通信来实现 逻辑层和视图层的通信会由 Native (微信客户端)做中转,逻辑层发送⽹络请求也经由 Native 转发。 JS 对象模拟 DOM 树 -> ⽐较两棵虚拟 DOM 树的差异 -> 把差异应⽤到真正的 DOM 树上。

image.png

  1. 在渲染层把 WXML 转化成对应的 JS 对象;
  2. 在逻辑层发⽣数据变更的时候,通过宿主环境提供的 setData ⽅法把数据从逻辑层传递到 Native,再 转发到渲染层;
  3. 经过对⽐前后差异,把差异应⽤在原来的 DOM 树上,更新界⾯;

事件的处理

视图层需要进⾏交互,这类反馈应该通知给开发者的逻辑层,需要将对应的处理状态呈现给⽤户。

视图层的功能只是进⾏渲染,因此对于事件的分发处理,微信进⾏了特殊的处理,将所有的事件拦截 后,丢到逻辑层交给JS处理。

image.png

事件的派发处理包括事件捕获和冒泡两种: 通过native传递给 JSCore,通过 JS 来响应响应的事件之后,对 Dom 进⾏修改,改动会体现在虚拟 Dom 上,然后再进⾏真实的渲染。

image.png

运行机制

⼩程序启动机制:

  1. 冷启动:⽤户⾸次打开或⼩程序被微信主动销毁后再次打开的情况,此时⼩程序需要重新加载启(完全重新启动) 动。
  2. 热启动:假如⽤户已经打开过某⼩程序,然后在⼀定时间内再次打开该⼩程序,此时⽆需重新启 动,只需将后台状态的⼩程序切换到前台;(放在后台)

注意: ⼩程序没有重启的概念; 当⼩程序进⼊后台,客户端会维持⼀段时间的运⾏状态,超过⼀定时间后(⽬前是5分钟)会被微信 主动销毁; 当短时间内(5s)连续收到两次以上收到系统内存告警,会进⾏⼩程序的销毁;

image.png

冷启动和热启动区别:是否从 CDN 上读取副本

小程序框架对比

原生语法

  1. ⽬前⼩程序⽣态⽀持开发者利⽤前端部分⽣态开发应⽤的;
  2. ⽬前⼩程序已经能够做到前端⼯程化,并且植⼊前端⽣态中已有的⼀些理念,例如状态管理、CLI ⼯程化等等,与早期 npm 能⼒的缺失、只能通过模板渲染实现组件化不可同⽇⽽语;
  3. 当业务的需求只有投放到微信或者⽀付宝⼩程序时,原⽣语法可以成为前端程序员们的⼀个选择; 前端能⼒基本都可以在⼩程序上复⽤(如状态管理库颗粒化管理组件状态、TS等);

增强型框架

指⼩程序引⼊ npm 之后,有了更加开放的能⼒所带来的收益;

以⼩程序原⽣语法为主,在逻辑层引⼊了增强语法来优化应⽤性能或者提供更便捷的使⽤⽅法;

Example:腾讯开源的 omix 框架为例:

// 逻辑层
create.Page(store, {
  // 声明依赖
  use: ['logs'],
  computed: {
    logsLength() {
      return this.logs.length
   }
 }, // 特点:在使用小程序的基础上进行了扩展和增强
  onLoad: function () {
    //响应式,⾃动更新视图
    this.store.data.logs = (wx.getStorageSync('logs') || []).map(log => {
      return util.formatTime(new Date(log))
   })
    setTimeout(() => {
      //响应式,⾃动更新视图
      this.store.data.logs[0] = 'Changed!'
   }, 1000)
 }
})
//视图层
<view class="container log-list">
  <block wx:for="{{logs}}" wx:for-item="log">
    <text class="log-item">{{index + 1}}. {{log}}</text>
  </block>
</view>
  1. 整体保留⼩程序已有的语法,但在此基础之上,对它进⾏了扩充和增强;
  2. ⽐如引⼊了 Vue 中⽐较有代表性的 computed,⽐如能够直接通过 this.store.data.logs[0] = 'Changed' 修改状态。可以说是在⼩程序原⽣半 Vue 半 React 的语法背景下,彻底将其 Vue 化的 ⼀种⽅案;

使⽤增强型框架优势:

  1. 可以在只引⼊极少依赖,并且保留对⼩程序认知的情况下,⽤更加舒爽的语法来写代码;
  2. 对于⽬标只投放到特定平台⼩程序的开发者或者⾮专业前端⽽⾔是⽐较好的选择之⼀;因为你只需 要关注很少的新增⽂档和⼩程序⾃身的⽂档就⾜够了,底层不需要考虑;

转换类框架 Rax

⽬的:让开发者⼏乎不⽤感受⼩程序原⽣语法,更⼤程度对接前端已有⽣态,并且可以实现「⼀码多 端」的业务诉求,只是最后的构建产物为⼩程序代码。

  1. 编译时 通过编译分析的⽅式,将开发者写的代码转换成⼩程序原⽣语法。 以 Rax 编译时和 Taro 2.0 为例,⾯向开发者的语法是类 React 语法,开发者通过写有⼀定语法限制的 React 代码,最后转换产物 1:1 转换成对应的⼩程序代码。 image.png

例子:

// rax
import { createElement, useEffect, useState } from 'rax';
import View from 'rax-view';
export default function Home() {
  const [name, setName] = useState('world');
  useEffect(() => {
    console.log('Here is effect.');
 }, [])
  return <View>Hello {name}</View>;
}
// 转为⼩程序后的代码
// 逻辑层
import { __create_component__, useEffect, useState } from 'jsx2mpruntime/dist/ali.esm.js'
function Home() {
  const [name, setName] = useState('world');
  useEffect(() => {
    console.log('Here is effect.');
 }, []);
  this._updateData({
    _d0: name
 });
}
Component(__create_component__(Home));
// 视图层
<block a:if="{{$ready}}">
  <view class="__rax-view">{{_d0}}</view>
</block>
  1. 开发者虽然写的是类 React 语法,但是转换后的代码和渐进增强型框架⾮常类似;
  2. 开发者可以⽐较清晰的看出编译前后代码的对应关系; 编译时⽅案会通过 AST 分析,将开发者写的 JSX 中 return 的模板部分构建到视图层,剩余部分代码保 留,然后通过运⾏时垫⽚模拟 React 接⼝的表现。

优势:

  1. 运⾏时性能损耗低;
  2. ⽬标代码明确,开发者所写即所得;
  3. 运⾏时、编译时优化:⽐如框架会给予开发者更多的语法⽀持以及默认的性能优化处理,⽐如避免 多次 setData,亦或是⻓列表优化等等;

劣势:

  1. 语法限制⾼:需要完全命中开发者在模板部分所⽤到的所有语法,语法受限,如由于是 1:1 编译转 换,开发者在开发的时候还是不得不去遵循⼩程序的开发规范,⽐如⼀个⽂件中定义只能定义⼀个 组件之类的;

  2. 运行时 相⽐于上⾯的编译时,最⼤的优势是可以⼏乎没有任何语法约束的去完成代码编写。

通过在逻辑层模拟 DOM/BOM API,将这些创建视图的⽅法转换为维护⼀棵 VDOM tree,再将其转换 成对应 setData 的数据,最后通过预置好的模板递归渲染出实际视图。

优势:没有语法限制; 劣势:以⼀定的性能损耗来换取更为全⾯的 Web 端特性⽀持;

微信小程序

基本内容

demo: github.com/wechat-mini…

  1. 基础 重点了解手册中:指南、框架、API、组件、工具部分

小程序代码组成:

  • WXML:(Weixin Markup Language)
  • WXSS: (WeiXin Style Sheets) WXS:(WeiXin Script)
  • JSON: 配置文件

⼩程序框架:

  1. 逻辑层 a. 官⽅⽂档:developers.weixin.qq.com/miniprogram… b. 使⽤ JavaScript 引擎为⼩程序提供开发者 JavaScript 代码的运⾏环境以及微信⼩程序的特有 功能;

c. 开发者写的所有代码最终将会打包成⼀份 JavaScript ⽂件,并在⼩程序启动的时候运⾏,直到⼩程序销毁。这⼀⾏为类似 ServiceWorker,所以逻辑层也称之为 App Service; d. 增加 App 和 Page ⽅法,进⾏程序注册和⻚⾯注册; e. 增加 getApp 和 getCurrentPages ⽅法,分别⽤来获取 App 实例和当前⻚⾯栈; f. 提供丰富的 API,如微信⽤户数据,扫⼀扫,⽀付等微信特有能⼒; g. 提供模块化能⼒,每个⻚⾯有独⽴的作⽤域; h. ⼩程序框架的逻辑层并⾮运⾏在浏览器中,因此 JavaScript 在 web 中⼀些能⼒都⽆法使⽤,如 window,document 等。--> 除非使用 多端框架磨平

  1. 视图层 a. 官⽅⽂档:developers.weixin.qq.com/miniprogram…

基础核⼼

  1. ⼩程序运⾏时 a. ⼩程序⽣命周期:热启动 == 后台切前台

image.png

b. 更新机制 ⅰ. 启动时同步更新 1. 定期检查⼩程序版本; 2. ⻓时间未使⽤⼩程序; ⅱ. 启动时异步更新 1. 打开发现有新版本,异步下载,下次冷启动时加载新版本; ⅲ. 开发者⼿动更新 1. wx.getUpdateManager 2. ⾃定义组件 3. 代码注⼊ a. 按需注⼊: "lazyCodeLoading": "requiredComponents";(app.json 配置)⼩程序仅注⼊当前⻚⾯需要的⾃定义组件和⻚⾯代码,在⻚⾯中必然不会⽤到的⾃定义组件不会被加载和初始化; b. ⽤时注⼊:在开启「按需注⼊」特性的前提下,指定⼀部分⾃定义组件不在⼩程序启动时注⼊,⽽是在真正渲染的时候才进⾏注⼊,使⽤占位组件在需要渲染但注⼊完成前展示; 4. 分包加载 a. 原则 ⅰ. 声明 subpackages 后,将按 subpackages 配置路径进⾏打包,subpackages 配置路径外的⽬录将被打包到 app(主包) 中; ⅱ. app(主包)也可以有⾃⼰的 pages(即最外层的 pages 字段); ⅲ. subpackage 的根⽬录不能是另外⼀个 subpackage 内的⼦⽬录; ⅳ. tabBar ⻚⾯必须在 app(主包)内 b. 独⽴分包 ⅰ. 开发者可以按需将某些具有⼀定功能独⽴性的⻚⾯配置到独⽴分包中。当⼩程序从普通的分包⻚⾯启动时,需要⾸先下载主包; ⅱ. 独⽴分包运⾏时,App 并不⼀定被注册,因此 getApp() 也不⼀定可以获得 App 对象;基础库 2.2.4 版本开始 getApp ⽀持 [allowDefault] 参数,在 App 未定义时返回⼀个默认实现。当主包加载,App 被注册时,默认实现中定义的属性会被覆盖合并到真正的 App 中;

具体代码分析:

根目录下
  1. app.json
  • pages:四个页面
"pages": [
  "page/component/index",
  "page/API/index",
  "page/cloud/index",
  "page/extend/index"
],
  • 子包 subpackages 分包 :四个子包具体内容;如果全部挂在pages下,大小超出2m了
  • 预加载规则 preloadRule: 定义了四个链接,四个根页面 --> 在pages 中,默认index.wxml index.js 对应逻辑层 index.json 展示样式

回到 preloadRule "network": "all"/"wifi" - 流量或无线都可以

"packages": [
      "packageComponent"
    ]

包的位置

  • window:根目录的配置
"navigationBarTextStyle": "@navigationBarTextStyle",

引入的变量在 themeLocation: "demo.theme.json"

  • tarBar: 最下面的栏 list 中配置最下方工具栏内容
  • networkTimeout:超时配置
  • permission: 默认获取用户地理位置信息
  • sitemapLocation:浏览器SEO加载权重,微信搜索
  • useExtendedLib: 使用微信weui 拓展包,用于微信 ui
  • requiredBackgroundModes: ["audio", "location"] 退出小程序是否继续播放
  • lazyCodeLoading:懒加载

当前页面一般配置样式

  1. app.wxss 样式配置
  2. config.js 可有可无,一般用来测试
  3. demo.theme.js on 配置主题
  4. package.json 加载包
  5. project.config.json 类似 webpack 配置文档,默认不需要配置
  6. app.js 小程序全局的入口文件 小程序生命周期:
  7. onLaunch 启动,能否使用 wx.canIUse API
  8. onShow 小程序显示:小程序打开
  9. onHide:小程序隐藏
  10. onThemeChange:主题切换(不是原生生命周期)

globalData:获取全局数据,类似 Vuex

组件内部
  1. index.js
  2. wx.reportAnalytics
  onShow() {
  wx.reportAnalytics('enter_home_programmatically', {})

小程序中可以汇报一些数据统计,类似于埋点 2. canIUse 判断API 是否可用 3. onShareAppMessage() 分享当前的 App

onShareAppMessage() {
  return {
    title: '小程序官方组件展示',
    path: 'page/component/index' // 分享哪个页面
  }
},
  1. onShareTimeline() 分享时监听数据
  2. data 数据
  3. kindToggle(e) 点击展开列表事件:如果 open 为 true就展开,否则就合起来

小程序调试

  1. vconsole
  2. sourceMap 源码映射。
  3. 实时日志:重写 log 方法
  4. errno:针对API的cb err进⾏状态码的判断,便于针对业务场景语义化展示;
  5. ⼩程序如何兼容版本?
  6. 版本号比较
  7. API 是否存在
  8. wx.canIUse
  9. 在后台设置最基础版本库

框架

  1. ⼩程序配置 a. 全局配置 ⅰ. 根⽬录下app.json; b. ⻚⾯配置 ⅰ. 在page⻚⾯中对应的json⽂件,权重最⾼; ⅱ. 原先在根⽬录下的app.json中window内属性,在⻚⾯json中⽆需添加window; c. sitemap 配置 ⅰ. 根⽬录下sitemap.json;
  2. 框架接⼝ a. ⼩程序App ⅰ. App:必须在 app.js 中调⽤,必须调⽤且只能调⽤⼀次
    1. onLaunch
    2. onShow
    3. onHide
    4. onError
    5. onPageNotFound
    6. onUnhandledRejection
    7. onThemeChange
    8. 其他:可以添加任意的函数或数据变量到 Object 参数中,app.js中⽤ this 可以访问; (Tips:⾮原⽣事件最好不要⽤on开头) ⅱ. getApp:外部访问App中数据的⽅式 b. ⻚⾯ ⅰ. Page:在⻚⾯级别中的"app.js"
    9. data
    10. ⽣命周期事件 a. onLoad:加载时触发 b. onReady:渲染完成触发 c. onShow d. onHide e. onUnload
    11. ⻚⾯事件处理事件 a. onPullDownRefresh b. onReachBottom c. onPageScroll:监听⻚⾯滚动 d. onAddToFavorites:添加到收藏并⾃定义收藏内容 e. onShareAppMessage:转发事件 f. onShareTimeline:转发朋友圈 g. onResize
      h. onTabItemTap i. onSaveExitState:⻚⾯销毁前 (页面退出前的数据会保存,下次登陆还会存在)
    12. 组件事件处理 a. wxml中绑定的⾃定义事件 b. Page.route c. Page.prototype.setData ⅰ. 注意:可以以数据路径来改变数组中的某⼀项或对象的某个属性,如 array[2].message,a.b.c.d,并且不需要在 this.data 中预先定义。 5. ⻚⾯间通信 使⽤ wx.navigateTo 打开,这两个⻚⾯间将建⽴⼀条数据通道: a. 被打开的⻚⾯可以通过 this.getOpenerEventChannel() ⽅法来获得⼀个 EventChannel 对象; b. wx.navigateTo 的 success 回调中也包含⼀个 EventChannel 对象; c. 这两个 EventChannel 对象间可以使⽤ emit 和 on ⽅法相互发送、监听事件; ⅱ. getCurrentPage 面试 1. 获取当前⻚⾯栈,数组中第⼀个元素为⾸⻚,最后⼀个元素为当前⻚⾯; 2. 场景:
//1. 进⼊⼩程序⾮默认⾸⻚时,需要执⾏对应操作
onShow() {
  let pages = getCurrentPages(); // 当前⻚⾯栈
  if (pages.length == 1) {
    //todo
 }
}
2. 跨⻚⾯赋值
let pages = getCurrentPages();// 当前⻚⾯栈
let prevPage = pages[pages.length - 2];// 上⼀⻚⾯
prevPage.setData({
  // 直接给上移⻚⾯赋值
});
3. ⻚⾯跳转后⾃动刷新
wx.switchTab({
  url: '../index/index',
  success: function (e) {
    const page = getCurrentPages().pop(); // 当前⻚⾯
    if (page == undefined || page == null) return;
    page.onLoad(); //或者其它操作
  }
})
4. 获取当前⻚⾯相关信息
let pages = getCurrentPages(); // 当前⻚⾯栈
// 当前⻚⾯为⻚⾯栈的最后⼀个元素
let prevPage = pages[pages.length - 1];// 当前⻚⾯
  
console.log( prevPage.route) //举例:输出为‘pages/index/index’

c. ⾃定义组件(参考上⽂) ⅰ. Component 类似于 组件 ⅱ. Behavior 类似于 mixin d. 模块化 ⅰ. require 1. 引⼊module.export或者 export暴露出的接⼝,需要引⼊模块⽂件相对于当前⽂件的相对路径,或npm模块名,或npm模块路径。不⽀持绝对路径 ⅱ. module:当前模块对象 ⅲ. export:module.export的引⽤ ⅳ. requirePlugin:引⽤插件 ⅴ. requireMiniProgram:引⽤当前⼩程序 e. 基础功能 ⅰ. console 1. console.debug 2. console.error 3. console.log 4. console.info 5. console.warn 6. console.group 7. console.groupEnd ⅱ. 定时器 1. setTimeout 2. clearTimeout 3. setInterval 4. clearInterval 3. WXML 注意: wx:for 默认数组的当前项的下标变量名默认为 index,数组当前项的变量名默认为 item

wx:if vs hidden wx:if 有更⾼的切换消耗⽽ hidden 有更⾼的初始渲染消耗。 因此,如果需要频繁切换的情景下,⽤ hidden 更好,如果在运⾏时条件不⼤可能改变则 wx:if 较好。

  1. WXS 模块 export 出来 a. 模块 ⅰ. 可以编写在 wxml ⽂件中的 标签内,或以 .wxs 为后缀名的⽂件内; ⅱ. wxs⽀持module、src标签,src为相对路径; ⅲ. 每个 wxs 模块均有⼀个内置的 module 对象; ⅳ. 在wxs中,可以引⼊新的wxs,或者使⽤require引⼊;
// /pages/tools.wxs
var foo = "'hello world' from tools.wxs";
var bar = function (d) {
  return d;
}
module.exports = {
  FOO: foo,
  bar: bar,
};
module.exports.msg = "some msg";
<!-- page/index/index.wxml -->
<wxs src="./../tools.wxs" module="tools" />
var tools = require("./tools.wxs");
  
<view> {{tools.msg}} </view>
<view> {{tools.bar(tools.FOO)}} </view>

b. 变量 ⅰ. WXS 中的变量均为值的引⽤; ⅱ. 没有声明的变量直接赋值使⽤,会被定义为全局变量; ⅲ. 如果只声明变量⽽不赋值,则默认值为 undefined; ⅳ. var表现与javascript⼀致,会有变量提升。 c. 注释 d. 运算符 ⅰ. 同JS⼀致; e. 语句 ⅰ. 同JS⼀致,⽀持if else if else、switch、for、while; f. 数据类型 ⅰ. number : 数值 ⅱ. string :字符串 ⅲ. boolean:布尔值 ⅳ. object:对象 ⅴ. function:函数 ⅵ. array : 数组 ⅶ. date:⽇期 ⅷ. regexp:正则

常见面试题

  1. 框架相关 a. 为什么要分包? ⅰ. ⽬前⼩程序分包⼤⼩有以下限制:
    1. 整个⼩程序所有分包⼤⼩不超过 20M;
    2. 单个分包/主包⼤⼩不能超过 2M; ⅱ. 对⼩程序进⾏分包,可以优化⼩程序⾸次启动的下载时间,以及在多团队共同开发时可以 更好的解耦协作; b. 如何提升⼩程序SEO? ⅰ. 官⽅⽂档: developers.weixin.qq.com/miniprogram… ⅱ. ⼩程序⾥跳转的⻚⾯ (url) 可被直接打开; ⅲ. ⻚⾯跳转优先采⽤navigator组件; ⅳ. 清晰简洁的⻚⾯参数; ⅴ. 配置⼩程序sitemap; (核心!!) ⅵ. 必要的时候才请求⽤户进⾏授权、登录、绑定⼿机号等; ⅶ. 我们不收录 web-view 中的任何内容; ⅷ. 设置⼀个清晰的标题和⻚⾯缩略图; c. 如何进⾏⻚⾯间通信? ⻚⾯间将建⽴⼀条数据通道:
    3. 被打开的⻚⾯可以通过 this.getOpenerEventChannel() ⽅法来获得⼀个EventChannel 对象;
    4. wx.navigateTo 的 success 回调中也包含⼀个 EventChannel 对象;
    5. 这两个 EventChannel 对象间可以使⽤ emit 和 on ⽅法相互发送、监听事件;
    具体看文档吧