父子组件的传递方式比较单一,在 Vue 2.0 以后,父子组件的关系可以总结为 props down, events up 。父组件通过 props 向下传递数据给子组件,子组件通过 events 给父组件发送消息。
父向子传递 举个例子:
Vue.component('child', {
// 声明 props
props: ['msg'],
// prop 可以用在模板内
// 可以用 `this.msg` 设置
template: '<span>{{ msg }}</span>'
})
<child msg="hello!"></child>复制代码 在 child 组件的 props 中声明了一个 msg 属性,在父组件中利用这个属性把值传给子组件。
这里有一点需要注意的是,在非字符串模板中, camelCased (驼峰式) 命名的 prop 需要转换为相对应的 kebab-case (短横线隔开式) 命名。
上面这个例子是静态的绑定,Vue 也支持动态绑定,这里也支持 v-bind 指令进行动态的绑定 props 。
父向子传递是一个单向数据流的过程,prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是不会反过来。这是为了防止子组件无意修改了父组件的状态——这会让应用的数据流难以理解。
另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop。Vue 建议子组件的 props 是 immutable 的。
这里就会牵涉到2类问题:
由于单向数据流的原因,会导致子组件的数据或者状态和父组件的不一致,为了同步,在子组件里面反数据流的去修改父组件的数据或者数据。
子组件接收到了 props 的值以后,有2种原因想要改变它,第一种原因是,prop 作为初始值传入后,子组件想把它当作局部数据来用;第二种原因是,prop 作为初始值传入,由子组件处理成其它数据输出。
这两类问题,开发者强行更改,也都是可以实现的,但是会导致不令人满意的 “后果” 。第一个问题强行手动修改父组件的数据或者状态以后,导致数据流混乱不堪。只看父组件,很难理解父组件的状态。因为它可能被任意子组件修改!理想情况下,只有组件自己能修改它的状态。第二个问题强行手动修改子组件的 props 以后,Vue 会在控制台给出警告。
如果优雅的解决这2种问题呢?一个个的来说:
(1)第一个问题,换成双向绑定就可以解决。 在 Vue 2.3.0+ 以后的版本,双向绑定有2种方式
第一种方式:
利用 .sync
修饰符,在 Vue 2.3.0+ 以后作为一个编译时的语法糖存在。它会被扩展为一个自动更新父组件属性的 v-on 侦听器。
// 声明一个双向绑定
<comp :foo.sync="bar"></comp>
// 上面一行代码会被会被扩展为下面这一行:
<comp :foo="bar" @update:foo="val => bar = val"></comp>
// 当子组件需要更新 foo 的值时,它会显式地触发一个更新事件:
this.$emit('update:foo', newValue)复制代码 第二种方式:
自定义事件可以用来创建自定义的表单输入组件,使用 v-model 来进行数据双向绑定。
<input :value="value" @input="updateValue($event.target.value)" >复制代码 在这种方式下进行的双向绑定必须满足2个条件:
接受一个 value 属性 在有新的值时触发 input 事件 官方推荐的2种双向绑定的方式就是上述2种方法。不过还有一些隐性的双向绑定,可能无意间就会造成bug的产生。
pros 是单向数据传递,父组件把数据传递给子组件,需要尤其注意的是,传递的数据如果是引用类型(比如数组和对象),那么默认就是双向数据绑定,子组件的更改都会影响到父组件里面。在这种情况下,如果人为不知情,就会出现一些莫名其妙的bug,所以需要注意引用类型的数据传递。
(2)第二个问题,有两种做法: 第一种做法是:定义一个局部变量,并用 prop 的值初始化它: props: ['initialCounter'],
data: function () {
return { counter: this.initialCounter }
}复制代码 第二种做法是:定义一个计算属性,处理 prop 的值并返回。 props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}复制代码 父向子传递还可以传递模板,使用 slot 分发内容。
slot 是 Vue 的一个内置的自定义元素指令。slot 在 bind 回调函数中,根据 name 获取将要替换插槽的元素,如果上下文环境中有所需替换的内容,则调用父元素的 replaceChild 方法,用替换元素讲 slot 元素替换;否则直接删除将要替换的元素。如果替换插槽元素中有一个顶级元素,且顶级元素的第一子节点为 DOM 元素,且该节点有 v-if 指令,且 slot 元素中有内容,则替换模板将增加 v-else 模板放入插槽中的内容。如果 v-if 指令为 false,则渲染 else 模板内容。
子向父传递 子组件要把数据传递回父组件,方式很单一,那利用自定义事件!
父组件使用 $on(eventName) 监听事件
子组件使用 $emit(eventName) 触发事件
举个简单的例子:
// 在子组件里面有一个 button
<button @click="emitMyEvent">emit</button>
emitMyEvent() {
this.$emit('my-event', this.hello);
}
// 在父组件里面监听子组件的自定义事件
<child @my-event="getMyEvent"></child>
getMyEvent() {
console.log(' i got child event ');
}复制代码 这里也可以通过父子之间的关系进行传递数据(直接修改数据),但是不推荐这种方法,例如 this.$parent 或者 this.$children 直接调用父或者子组件的方法,这里类比iOS里面的ViewControllers方法,在这个数组里面可以直接拿到所有 VC ,然后就可以调用他们暴露在.h里面的方法了。但是这种方式相互直接耦合性太大了。
2. Event Bus Event Bus 这个概念对移动端的同学来说也比较熟悉,因为在安卓开发中就有这个概念。在 iOS 开发中,可以类比消息总线。具体实现可以是通知 Notification 或者 ReactiveCocoa 中的信号传递。
Event Bus 的实现还是借助 Vue 的实例。新建一个新的 Vue,专门用来做消息总线。
var eventBus = new Vue()
// 在 A 组件中引入 eventBus
eventBus.$emit('myEvent', 1)
// 在要监听的组件中监听
eventBus.$on('id-selected', () => {
// ...
})复制代码 3. Vuex 单向数据流 由于本篇文章重点讨论组件化的问题,所以这里 Vuex 只是说明用法,至于原理的东西之后会单独开一篇文章来分析。
这一张图就描述了 Vuex 是什么。Vuex 专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
上图中箭头的指向就描述了数据的流向。数据的流向是单向的,从 Actions 流向 State,State 中的数据改变了从而影响到 View 展示数据的变化。
从简单的 Actions、State、View 三个角色,到现在增加了一个 Mutations。Mutations 现在变成了更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutations 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。
一般在组件中进行 commit 调用 Mutation 方法
this.$store.commit('increment', payload);复制代码 Actions 和 Mutations 的区别在于:
Action 提交的是 mutation,而不是直接变更状态。 Action 可以包含任意异步操作,而 Mutations 必须是同步函数。 一般在组件中进行 dispatch 调用 Actions 方法
this.$store.dispatch('increment');复制代码 Vuex 官方针对 Vuex 的最佳实践,给出了一个项目模板结构,希望大家都能按照这种模式去组织我们的项目。
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API请求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我们组装模块并导出 store 的地方
├── actions.js # 根级别的 action
├── mutations.js # 根级别的 mutation
└── modules
├── cart.js # 购物车模块
└── products.js # 产品模块复制代码 关于这个例子的详细代码在这里
七. 组件注册方式 组件的注册方式主要就分为2种:全局注册和局部注册
1. 全局注册 利用 Vue.component 指令进行全局注册
Vue.component('my-component', {
// 选项
})复制代码 注册完的组件就可以在父实例中以自定义元素 <my-component></my-component>
的形式使用。
// 注册
Vue.component('my-component', {
template: '<div>A custom component!</div>'
})
// 创建根实例
new Vue({
el: '#example'
})
<div id="example">
<my-component></my-component>
</div>复制代码 2. 局部注册 全局注册组件会拖慢一些页面的加载速度,有些组件只需要用的到时候再加载,所以不必在全局注册每个组件。于是就有了局部注册的方式。
var Child = {
template: '<div>A custom component!</div>'
}
new Vue({
// ...
components: {
// <my-component> 将只在父模板可用
'my-component': Child
}
})复制代码 iOS 篇 一. 组件化的需求 在 iOS Native app 前期开发的时候,如果参与的开发人员也不多,那么代码大多数都是写在一个工程里面的,这个时候业务发展也不是太快,所以很多时候也能保证开发效率。
但是一旦项目工程庞大以后,开发人员也会逐渐多起来,业务发展突飞猛进,这个时候单一的工程开发模式就会暴露出弊端了。
项目内代码文件耦合比较严重 容易出现冲突,大公司同时开发一个项目的人多,每次 pull 一下最新代码就会有很多冲突,有时候合并代码需要半个小时左右,这会耽误开发效率。 业务方的开发效率不够高,开发人员一多,每个人都只想关心自己的组件,但是却要编译整个项目,与其他不相干的代码糅合在一起。调试起来也不方便,即使开发一个很小的功能,都要去把整个项目都编译一遍,调试效率低。 为了解决这些问题,iOS 项目就出现了组件化的概念。所以 iOS 的组件化是为了解决上述这些问题的,这里与前端组件化解决的痛点不同。
iOS 组件化以后能带来如下的好处:
加快编译速度(不用编译主客那一大坨代码了,各个组件都是静态库) 自由选择开发姿势(MVC / MVVM / FRP) 方便 QA 有针对性地测试 提高业务开发效率 iOS 组件化的封装性只是其中的一小部分,更加关心的是如何拆分组件,如何解除耦合。前端的组件化可能会更加注重组件的封装性,高可复用性。
二. 如何封装组件 iOS 的组件化手段非常单一,就是利用 Cocoapods 封装成 pod 库,主工程分别引用这些 pod 即可。越来越多的第三方库也都在 Cocoapods 上发布自己的最新版本,大公司也在公司内部维护了公司私有的 Cocoapods 仓库。一个封装完美的 Pod 组件,主工程使用起来非常方便。
具体如果用 Cocoapods 打包一个静态库 .a 或者 framework ,网上教程很多,这里给一个链接 ,详细的操作方法就不再赘述了。
最终想要达到的理想目标就是主工程就是一个壳工程,其他所有代码都在组件 Pods 里面,主工程的工作就是初始化,加载这些组件的,没有其他任何代码了。
三. 如何划分组件 iOS 划分组件虽然没有一个很明确的标准,因为每个项目都不同,划分组件的粗粒度也不同,但是依旧有一个划分的原则。
App之间可以重用的 Util、Category、网络层和本地存储 storage 等等这些东西抽成了 Pod 库。还有些一些和业务相关的,也是在各个App之间重用的。
原则就是:要在App之间共享的代码就应该抽成 Pod 库,把它们作为一个个组件。不在 App 间共享的业务线,也应该抽成 Pod,解除它与工程其他的文件耦合性。
常见的划分方法都是从底层开始动手,网络库,路由,MVVM框架,数据库存储,加密解密,工具类,地图,基础SDK,APM,风控,埋点……从下往上,到了上层就是各个业务方的组件了,最常见的就类似于购物车,我的钱包,登录,注册等。
四. 组件化原理 iOS 的组件化原理是基于 Cocoapods 的。关于 Cocoapods 的具体工作原理,可以看这篇文章《CocoaPods 都做了什么?》 。
这里简单的分析一下 pod进来的库是什么加载到主工程的。
pod 会依据 Podfile 文件里面的依赖库,把这些库的源代码下载下来,并创建好 Pods workspace。当程序编译的时候,会预先执行2个 pod设置进来的脚本。
在上面这个脚本中,会把 Pods 里面的打包好的静态库合并到 libPods-XXX.a 这个静态库里面,这个库是主工程依赖的库。
上图就是给主项目加载 Pods 库的脚本。
Pods 另外一个脚本是加载资源的。见下图。
这里加载的资源是 Pods 库里面的一些图片资源,或者是 Boudle 里面的 xib ,storyboard,音乐资源等等。这些资源也会一起打到 libPods-XXX.a 这个静态库里面。
上图就是加载资源的脚本。
五. 组件分类 iOS 的组件主要分为2种形式:
静态库 动态库 静态库一般是以 .a 和 .framework 结尾的文件,动态库一般是以 .dylib 和 .framework 结尾的文件。
这里可以看到,一个 .framework 结尾的文件仅仅通过文件类型是无法判断出它是一个静态库还是一个动态库。
静态库和动态库的区别在于:
.a文件肯定是静态库,.dylib肯定是动态库,.framework可能是静态库也可能是动态库;
静态库在链接其他库的情况时,它会被完整的复制到可执行文件中,如果多个App都使用了同一个静态库,那么每个App都会拷贝一份,缺点是浪费内存。类似于定义一个基本变量,使用该基本变量是是新复制了一份数据,而不是原来定义的;静态库的好处很明显,编译完成之后,库文件实际上就没有作用了。目标程序没有外部依赖,直接就可以运行。当然其缺点也很明显,就是会使用目标程序的体积增大。
动态库不会被复制,只有一份,程序运行时动态加载到内存中,系统只会加载一次,多个程序共用一份,节约了内存。而且使用动态库,可以不重新编译连接可执行程序的前提下,更新动态库文件达到更新应用程序的目的。
六. 组件间的消息传递和状态管理 之前我们讨论过了,iOS 组件化十分关注解耦性,这算是组件化的一个重要目的。iOS 各个组件之间消息传递是用路由来实现的。关于路由,笔者曾经写过一篇比较详细的文章,感兴趣的可以来看这篇文章《iOS 组件化 —— 路由设计思路分析》 。
七. 组件注册方式 iOS 组件注册的方式主要有3种:
load方法注册 读取 plist 文件注册 Annotation注解方式注册 前两种方式都比较简单,容易理解。
第一种方式在 load 方法里面利用 Runtime 把组件名和组件实例的映射关系保存到一个全局的字典里,方便程序启动以后可以随时调用。
第二种方式是把组件名和组件实例的映射关系预先写在 plist 文件中。程序需要的时候直接去读取这个 plist 文件。plist 文件可以从服务器读取过来,这样 App 还能有一定的动态性。
第三种方式比较黑科技。利用的是 Mach-o 的数据结构,在程序编程链接成可执行文件的时候,就把相关注册信息直接写入到最终的可执行文件的 Data 数据段内。程序执行以后,直接去那个段内去读取想要的数据即可。
关于这三种做法的详细实现,可以看笔者之前的一篇文章《BeeHive —— 一个优雅但还在完善中的解耦框架》 ,在这篇文章里面详细的分析了上述3种注册过程的具体实现。
总结 经过上面的分析,我们可以看出 Vue 的组件化和 iOS 的组件化区别还是比较大的。
两者平台上开发方式存在差异 主要体现在单页应用和类多页应用的差异。
现在前端比较火的一种应用就是单页Web应用(single page web application,SPA),顾名思义,就是只有一张Web页面的应用,是加载单个HTML 页面并在用户与应用程序交互时动态更新该页面的Web应用程序。
浏览器从服务器加载初始页面,以及整个应用所需的脚本(框架、库、应用代码)和样式表。当用户定位到其他页面时,不会触发页面刷新。通过 HTML5 History API 更新页面的 URL 。浏览器通过 AJAX 请求检索新页面(通常以 JSON 格式)所需的新数据。然后, SPA 通过 JavaScript 动态更新已经在初始页面加载中已经下载好的新页面。这种模式类似于原生手机应用的工作原理。
但是 iOS 开发更像类 MPA (Multi-Page Application)。
长按二维码向我转账
受苹果公司新规定影响,微信 iOS 版的赞赏功能被关闭,可通过二维码转账支持公众号。
阅读
好看
已推荐到看一看
你的朋友可以在“发现”-“看一看”看到你认为好看的文章。
取消
分享想法到看一看
确定
最多200字,当前共 字
微信扫一扫 关注该公众号