引入
前端编码中需要一种方式来封装已有功能,要求如下:
- 符合开闭原则,在开发新功能时不需要修改原有代码:包括html,css,js
- 良好的封装,不依赖上下文:不会有冲突如(
js重名,classname重名
)
本文认为组件化是一种符合上述要求的良好实践:
- 流行的框架(前端:
vue
,react
,angular
;动画:cocos-creator
)都采用了组件化
组件化
组件基本结构
- 内部状态: state,prop,attr
- 对外接口: setAttribute,addEventListener,mount(el)
组件树
组件树
- 实例化一个组件
- create 函数: h(Component,attr,...children) 返回组件实例
- 在内部创建Component的实例
- 设置组件attr
- 组合children 与 当前实例
- create 函数: h(Component,attr,...children) 返回组件实例
- 组件树的实例化
<Parent attr1={val1} attr2={val2}> <Child1 /> <Child2 /> </Parent>
-
先create末端节点,如Child1,Child2,再create 父节点
问:与
react
中子组件拿到父组件的数据矛盾:先创建子组件,此时父组件还没创建,应该拿不到父组件数据
答:不矛盾:
class CompA extends BaseComponent{ fn1(){ } render(){ return <CompNode1> <CompNode2 onClick={()=>this.fn1())}> </CompNode1> } }
- react 中 子组件拿到"父"组件,其中父指当前实例所在组件树上下文对应的组件,例如 CompNode2的父组件指其所在树 CompNode1>CompNode2 所在上下文对应的 CompA 组件
- 而不是说 组件树 CompNode1>CompNode2 中 CompNode2的父节点对应的组件 CompNode1
注意:区分父组件与组件树中组件的父节点
每个组件都可以是组件树的上下文
- 不是只有一个树
//入口文件中将jsx 树mount 到#app节点上 (<Comp1> <Comp2>Hello world</Comp2> </Comp1>).mount(document.getElementByElement('app'))
- 而是每个组件都可以包含一个组件树(例如react的render 函数返回一个jsx 组件树),例如
- Comp1
class Comp1 extends BaseComponent{ render(){ const {children} = this; return <div class="beautiful"> <span>~~~</span> {children} <span>~~~</span> </div> } }
- Comp2
class Comp2 extends BaseComponent{ render(){ const {children} = this; return <span class="goodFont"> {children} </span> } }
- 生成的html如下
<div class="beautiful"> <span>~~~</span> <span class="goodFont">Hello world</span> <span>~~~</span> </div>
-
组件树实现
- jsx,例如react
- 以jsx格式书写组件树:简单
- 配置babel转换jsx的 plugin:@babel/plugin-transform-react-jsx
- 实现 create 函数
- dsl ,例如vue
- 自定义格式:功能强大 可以定义表达式
- 实现自定义格式的parse
- 实例化组件并构成组件树
组件与组件通信
- 全局的eventHub,
- 基于dom的事件传递
- 子组件emit事件
- 父组件
组件封装
html的封装
- html标签能解析为组件实例,组件实例负责创建对应dom
//Wrapper组件负责代理一个 dom对象 class Wrapper extends BaseComponent { constuctor(tag){ ... this.tag = tag } created(){ this.el = document.createElement(this.tag); } setAttribute(name,val){ if(name === 'class'){ for(let aClassName of val.split(\/s+\).filter(s=>s!=='')) this.el.classList.add(aClassName); return; } this.el.setAttribute(name,val); } }
- 在create函数中创建Wrapper组件实例来产生实际的dom
function create(component,attribute,...children){ let compInstance; if(isHtmlTag(component)){ compInstance = new Wrapper(component); } ... }
- 注意:此方式会创建额外的组件对象,不适用于实际生产场景
css 的封装
-
全局:不封装
- 使用全局css, 组件中设置对应的css 类.
- 优点: 简单粗暴
- 缺点:
- css 没有封装到组件中。使用组件时也要引用对应的css
- 修改样式时,可能需要修改对应组件
- css 可能会与上下文冲突
- 使用全局css, 组件中设置对应的css 类.
-
将css 转成 键值对,例如react
//css .shop{border:1px solid #0f0;} //jsx <div class={shop}>...</div> //生成的代码 <div style="border:1px solid #0f0;">...</div>
- 优点: 封装性好~不会有样式冲突
- 缺点: 本质是内联样式,不能使用 伪类,伪元素
-
vue的
<style scoped>
方式- 每个组件生成唯一的id
- 利用组件id 防止样式冲突
//css .shop{border:1px solid #0f0;} //jsx <div class="shop">...</div> //生成的代码 //css .shop[comp-id='uniqId']{border:1px solid #0f0;} //dom <div class="shop" comp-id='uniqId'>...</div>
- 优点: 封装性好~不会有样式冲突
- 缺点: 暂无