前端组件化
目录
前端组件化
Vue传参
高级组件
前端组件化
在了解模块化、组件化之前,最好先了解一下什么是高内聚,低耦合。它能更好的帮助你理解模块化、组件化。
高内聚,低耦合
高内聚,就是指一个函数尽量只做一件事。低耦合,就是两个模块之间的关联程度低。
仅看文字可能不太好理解,下面来看一个简单的示例。
坏代码
// test.vue
<template>
<div>
<button @click="clickBtn('add')">点击add</button>
<button @click="clickBtn('mul')">点击mul</button>
</div>
</template>
<script>
export default {
data() {
return {
a: 0,
b: 0
}
},
methods: {
add() {
return this.a + this.b
},
mul() {
const sum = this.add()
return sum * this.b
},
clickBtn(action) {
this.a = 1 // 模拟a取值
this.b = 2 // 模拟b取值
if(action === 'add') {
return this.add()
}
else if(action === 'mul') {
return this.mul()
}
}
}
}
</script>
好代码
// math.js
export function add(a, b) {
return a + b
}
export function mul(a, b) {
return a * b
}
// test.vue
<template>
<div>
<button @click="clickBtn('add')">点击add</button>
<button @click="clickBtn('mul')">点击mul</button>
</div>
</template>
<script>
import { add, mul } from 'math'
export default {
data() {
return {
a: 0,
b: 0
}
},
methods: {
clickBtn(action) {
this.a = 1 // 模拟a取值
this.b = 2 // 模拟b取值
if(action === 'add') {
return add(this.a, this.b)
}
else if(action === 'mul') {
const sum = add(this.a, this.b)
return mul(sum, this.b)
}
}
}
}
</script>
上面的 math.js
就是高内聚,低耦合的典型示例。add()
、mul()
一个函数只做一件事,它们之间也没有直接联系。如果要将这两个函数联系在一起,也只能通过传参和返回值来实现。
真实业务代码
【测算业务代码举例】
模块化、组件化
模块化
模块化,就是把一个个文件看成一个模块,它们之间作用域相互隔离,互不干扰。一个模块就是一个功能,它们可以被多次复用。另外,模块化的设计也体现了分治的思想。即把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
从前端方面来看,单独的 JavaScript 文件、CSS 文件都算是一个模块。
例如一个 math.js
文件,它就是一个数学模块,包含了和数学运算相关的函数:
// math.js
export function add(a, b) {
return a + b
}
export function mul(a, b) {
return a * b
}
export function abs() { ... }
...
一个 button.css
文件,包含了按钮相关的样式:
/* 按钮样式 */
button {
...
}
组件化
那什么是组件化呢?我们可以认为组件就是页面里的 UI 组件,一个页面可以由很多组件构成。例如一个后台管理系统页面,可能包含了 Header
、Sidebar
、Main
等各种组件。一个组件又包含了 template(html)
、script
、style
三部分,其中 script
、style
可以由一个或多个模块组成。
目前三大框架在构建工具(例如 webpack、vite...)的配合下都可以很好的实现组件化。例如 Vue,使用 *.vue
文件就可以把 template
、script
、style
写在一起,一个 *.vue
文件就是一个组件。
一个页面可以分解成一个个组件,每个组件又可以分解成一个个模块,充分体现了分治的思想。
最理想的情况就是一个页面元素全部由组件构成,这样前端只需要写一些交互逻辑代码。虽然这种情况很难完全实现,但我们要尽量往这个方向上去做,争取实现全面组件化。
问题:如果不使用框架和构建工具,还能实现组件化吗?
Web Components
组件化是前端未来的发展方向,Web Components 就是浏览器原生支持的组件化标准。使用 Web Components API,浏览器可以在不引入第三方代码的情况下实现组件化。
Vue传参
vue2.x父子组件通信可以用:
props
$emit / v-on
$attrs / $listeners
ref
.sync
v-model
$children / $parent
兄弟组件通信可以用:
EventBus
Vuex
$parent
跨层级组件通信可以用:
provide/inject
EventBus
Vuex
$attrs / $listeners
$root
一、props 传参
子组件定义 props 有三种方式:
// 第一种数组方式
props: [xxx, xxx, xxx]
// 第二种对象方式
props: { xxx: Number, xxx: String}
// 第三种对象嵌套对象方式
props: {
xxx: {
//类型不匹配会警告
type: Number,
default: 0,
required: true,
// 返回值不是 true,会警告
validator(val) { return val === 10}
}
}
第三种对象默认支持 4 种属性,并且都是非必填的。可以随意使用
父组件传参的俩种方式
第一种静态属性传参
注意:
在不定义 props 类型的情况下 props 接受到的均为 String。
当 props 属性指定为 Boolean 时,并且只有属性 key 没有值 value 时接受到的是 true
<!--props 接受到的均为 String -->
<children xxx="123"></children>
<!-- 有只有属性没有值, 这种情况 props 指定类型是 Boolean 则接收到的是 true -->
<children xxx></children>
第二种动态属性传参
注意:
需要区分非简写形式传入的值是对象,则会对应 props 中多个值
会保留传入值的类型
如果是表达式则获取到的是表达式的计算结果
<!-- prop 接收到 Number 类型的 123-->
<children :xxx="123"></children>
<!-- prop 接收到 Array 类型的 [1, 2, 3]-->
<children v-bind:xxx="[1, 2, 3]"></children>
<!-- prop 会接收到 xxx1 和 xxx2 俩个参数。这种不支持简写形式-->
<children v-bind="{xxx1: 1, xxx2: 2}"></children>
二、attrs和listeners
**$attrs **会获取到 props 中未定义的属性(class 和 style 属性除外),支持响应式。常用的场景有俩种:
组件嵌套组件时可以使用 $attrs 来支持过多的属性支持。比如 elementUI 的 table 组件。支持的属性十几个,而平常封装的时候用的最多的也就一俩个。
属性默认是添加在父组件上的,有时候想把多余的属性添加在子组件上(可以结合 inheritAttrs: false 属性,让父属性不接受多余的属性)
**$listeners **定义的事件都在子组件的根元素上,有时候想加到其他元素上。就可以使用 $listerners。它包含了父组件中的事件监听器(除了带有 .native 修饰符的监听器)
三、$emit 通知
Vue 默认有 $on $emit $once $off 几种方法来实现发布订阅模式,这也应用在了组件传参上。在组件上添加的特殊方法 @abc="methods" 就相当于使用了 $on 来监听这个方法。因此组件内可以使用 $emit 来进行通知。
问题**: for 循环的时候如何拿到子组件的传值和 for 中循环的值**
答案有俩种,一是 $event, 二是 闭包。只是需要注意 $event 只能获取到第一个值
<template v-for="item in [1, 2, 3]">
<children @abc="((val, val2) => getValue(val, item))"></children>
</template>
四、v-model
可以实现将父组件传给子组件的数据为双向绑定,子组件通过 $emit 修改父组件的数据
// Parent.vue
<template>
<child v-model="value"></child>
</template>
<script>
export default {
data(){
return {
value:1
}
}
}
// Child.vue
<template>
<input :value="value" @input="handlerChange">
</template>
export default {
props:["value"],
// 可以修改事件名,默认为 input
model:{
event:"updateValue"
},
methods:{
handlerChange(e){
this.$emit("input", e.target.value)
// 如果有上面的重命名就是这样
this.$emit("updateValue", e.target.value)
}
}
}
</script>
五、插槽
<template>
<div>
<!--默认插槽-->
<slot></slot>
<!--另一种默认插槽的写法-->
<slot name="default"></slot>
<!--具名插槽-->
<slot name="footer"></slot>
<!--传参插槽-->
<slot v-bind:user="user" name="header"></slot>
</div>
</template>
<!--使用-->
<children>
<!--跑到默认插槽中去-->
<div>123</div>
<template v-slot:default></template>
<!--跑到具名插槽 footer 中去-->
<template v-slot:footer></template>
<template #footer></template>
<!--获取子组件的值-->
<template v-slot:header="slot">{{slot.user}}</template>
<!--结构插槽值-->
<template v-slot:header="{user: person}">{{person}}</template>
<!--老式写法,可以写到具体的标签上面-->
<template slot="footer" slot-scope="scope"></template>
</children>
六、$refs, $root, $parent, $children
$root 获取根组件
$parent 获取父组件
$children 获取子组件(所有的子组件,不保证顺序)
$refs 组件获取组件实例,元素获取元素
七、provide / inject
注意:注入的值是非响应的
<!--父组件 提供-->
{
provide() {
return {
parent: this
}
}
}
<!--子组件 注入-->
{
// 写法一
inject: ['parent']
// 写法二
inject: { parent: 'parent' }
// 写法三
inject: {
parent: {
from: 'parent',
default: 222
}
}
}
八、Vuex
这个相当于单独维护的一组数据,就不过多的说了。
扩展内容
vue3组件通信方式
props
$emit
expose / ref
$attrs
v-model
provide / inject
Vuex
mitt
高级(阶)组件
例子
本文就以平常开发中最常见的需求,也就是异步数据的请求
为例,先来个普通玩家的写法:
<template>
<div v-if="error">failed to load</div>
<div v-else-if="loading">loading...</div>
<div v-else>hello {{result.name}}!</div>
</template>
<script>
export default {
data() {
return {
result: {
name: '',
},
loading: false,
error: false,
},
},
async created() {
try {
// 管理loading
this.loading = true
// 取数据
const data = await this.$axios('/api/user')
this.data = data
} catch (e) {
// 管理error
this.error = true
} finally {
// 管理loading
this.loading = false
}
},
}
</script>
一般我们都这样写,平常也没感觉有啥问题,但是其实我们每次在写异步请求的时候都要有 loading
、 error
状态,都需要有 取数据
的逻辑,并且要管理这些状态。
那么想个办法抽象它?React 社区在 Hook 流行之前,经常用 HOC
(high order component) 也就是高阶组件来处理这样的抽象。
高阶组件是什么?
说到这里,我们就要思考一下高阶组件到底是什么概念,其实说到底,高阶组件就是:
一个函数接受一个组件为参数,返回一个包装后的组件
。
在 React 中
在 React 里,组件是 Class
,所以高阶组件有时候会用 装饰器
语法来实现,因为 装饰器
的本质也是接受一个 Class
返回一个新的 Class
。在 React 的世界里,高阶组件就是 f(Class) -> 新的Class
。
在 Vue 中
在 Vue 的世界里,组件是一个对象,所以高阶组件就是一个函数接受一个对象,返回一个新的包装好的对象。
类比到 Vue 的世界里,高阶组件就是 f(object) -> 新的object
。
智能组件和木偶组件
这是 React 社区里一个很成熟的概念。木偶
组件: 就像一个牵线木偶一样,只根据外部传入的 props
去渲染相应的视图,而不管这个数据是从哪里来的。智能
组件: 一般包在 木偶
组件的外部,通过请求等方式获取到数据,传入给 木偶
组件,控制它的渲染。
一般来说,它们的结构关系是这样的:
<智能组件>
<木偶组件 />
</智能组件>
它们还有另一个别名,就是 容器组件
和 ui组件
实现
具体到上面这个例子中,我们的思路是这样的,
高阶组件接受
木偶组件
和请求的方法
作为参数在
mounted
生命周期中请求到数据把请求的数据通过
props
传递给木偶组件
。
接下来就实现这个思路,HOC
是个函数,本次我们的需求是实现请求管理的 HOC
,那么先定义它接受两个参数,我们把这个 HOC
叫做 withPromise
。并且 loading
、error
等状态,还有 加载中
、加载错误
等对应的视图,我们都要在 新返回的包装组件
,也就是下面的函数中 return 的那个新的对象
中定义好。
const withPromise = (component, request) => {
return {
name: "with-promise",
data() {
return {
loading: false,
error: false,
result: null,
};
},
async mounted() {
this.loading = true;
const result = await request().finally(() => {
this.loading = false;
});
this.result = result;
},
};
};
在参数中:
component
也就是需要被包裹的组件对象。request
也就是请求对应的函数,需要返回一个 Promise
看起来不错了,但是函数里我们好像不能像在 .vue
单文件里去书写 template
那样书写模板了,但是我们又知道模板最终还是被编译成组件对象上的 render
函数,那我们就直接写这个 render
函数。(注意,本例子是因为便于演示才使用的原始语法,脚手架创建的项目可以直接用 jsx
语法。)在这个 render
函数中,我们把传入的 component
也就是木偶组件给包裹起来。这样就形成了 智能组件获取数据
-> 木偶组件消费数据
,这样的数据流动了。
const withPromise = (component, request) => {
return {
data() {
return {
loading: false,
error: false,
result: null,
};
},
async mounted() {
this.loading = true;
const result = await request().finally(() => {
this.loading = false;
});
this.result = result;
},
render(h) {
return h(component, {
props: {
result: this.result,
loading: this.loading,
},
});
},
};
};
到了这一步,已经是一个勉强可用的雏形了,我们来声明一下 木偶
组件。这其实是 逻辑和视图分离
的一种思路(这里的view可以是任意 .vue
文件)
const view = {
template: `
<span>
<span>{{result?.name}}</span>
</span>
`,
props: ["result"],
};
封装高阶组件src/components/hoc/index.js:
import view from './demo.vue'
import { hoc } from './hoc'
// 假装这是一个 axios 请求函数
const request = (data) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(data)
// reject(new Error('请求出错了'))
}, 1000)
})
}
export default hoc(view, request)
然后在父组件中渲染它:
<div id="app">
<Hoc />
</div>
<script>
import Hoc from './components/hoc/index'
new Vue({
el: 'app',
components: {
Hoc
}
})
</script>
为了让交互更友好点,再加上 加载中
和 加载失败
视图。
const hoc = (component, request) => {
return {
data() { ... },
async mounted() { ... },
render(h) {
const args = {
props: {
result: this.result,
loading: this.loading,
},
};
const wrapper = h("div", [
h(component, args),
this.loading ? h("span", ["加载中……"]) : null,
this.error ? h("span", ["加载错误"]) : null,
]);
return wrapper;
},
};
};
完善
到此为止的高阶组件虽然可以演示,但是并不是完整的,它还缺少一些功能,比如
要拿到子组件上定义的参数,作为初始化发送请求的参数。
要监听子组件中请求参数的变化,并且重新发送请求。
外部组件传递给
hoc
组件的参数现在没有递传下去。
为了实现第一点,我们约定好 view
组件上需要挂载某个特定 key
的字段作为请求参数,比如这里我们约定它叫做 requestParams
。
为了实现第二点,子组件的请求参数发生变化时,父组件也要响应式
的重新发送请求,并且把新数据带给子组件。我们只要在渲染子组件的时候把 $attrs
、$listeners
、$scopedSlots
传递下去即可,以这个例子来说:
<my-input value="ssh" @change="onChange" />
组件内部就能拿到这样的结构:
{
$attrs: {
value: 'ssh'
},
$listeners: {
change: onChange
}
}
完整Demo
参考资料
https://www.cnblogs.com/liuyuweb/p/13820427.html