blog
blog copied to clipboard
Components / HTML 组件开发
Components / HTML 组件开发
一个组件是一个独立的UI元素,被设计成可以在许多项目中重复使用。 它的目标是在做好一件事的同时,保持足够的抽象性以允许各种使用情况。 开发人员可以将它们作为构建块来构建新的用户体验。 每次使用这些组件时,都不必担心每个组件的核心设计和功能。 例如按钮、链接、表单、输入字段和模态。
前端组件演变
https://www.zhihu.com/question/267797409
无框架的年代
1.0 上古时期
我们需要开发这样的一个功能,点击+号按钮,上面的数字就加1
<div class="wrap">
<div class="text">0</div>
<button class='plus'>
+
</button>
</div>
//document.querySelector,其实就是DOM的api,选择一个元素
const wrap = document.querySelector(".wrap");
const plus = wrap.querySelector(".plus");
const text = wrap.querySelector(".text");
let number = 0;
//dom api,给元素添加一个监听器
plus.addEventListener(
"click",
function() {
number++;
text.innerHTML = number;
},
false
);
上古时期的开发们大约就是这样去书写代码,因为功能非常的简单,所以代码量也很少,思路很清晰。 随着页面的元素越来越多,你这个「点击加1按钮」的功能可能被复用, 那么你现在用一个最快的办法对其进行复用:复制HTML->复制JS代码->粘贴HTML和JS代码
这种方法是很挫的..... 于是为了简单化我们的流程,我们想出了一个聪明的办法去复用
1.1 看似聪明的办法
class Add {
render() {
return `
<div class="text">0</div>
<button class='plus'>
+
</button>`;
}
}
const wrap = document.querySelector(".wrap");
const add = new Add();
wrap.innerHTML = add.render();
const plus = wrap.querySelector(".plus");
const text = wrap.querySelector(".text");
let number = 0;
plus.addEventListener(
"click",
function() {
number++;
text.innerHTML = number;
},
false
);
代码稍微的一改,我们用一个class Add代替了HTML,当调用这个类的render函数的时候,就会返回一段字符串,这段字符串就是我们想要的组件。
由此,我们的组件复用方法变成了:复制JS代码->粘贴JS代码
这么一来啊,我们就简化了我们组件复用的流程。但是我们可以看到,我们手动操作DOM的次数太多了,而且逻辑非常的重复,因此,我们把这部分逻辑抽象一下,使得我们组件复用更加简单。
1.2 更加简单的组件复用
class Add {
createWrapper(string) {
//document.createElement, DOM api,根据标签名字创建DOM元素
const wrapper = document.createElement("div");
wrapper.innerHTML = string;
return wrapper;
}
render() {
const domString = `
<div class="text">0</div>
<button class='plus'>
+
</button>`;
//生成DOM元素
const wrapper = this.createWrapper(domString);
//DOM中查找元素
const plus = wrapper.querySelector(".plus");
const text = wrapper.querySelector(".text");
let number = 0;
plus.addEventListener(
"click",
function() {
number++;
text.innerHTML = number;
},
false
);
return wrapper;
}
}
const wrap = document.querySelector(".wrap");
const add = new Add();
wrap.appendChild(add.render());
现在我们的这个组件复用就非常的简单了, 我们只需要将class Add 通过export的方法导出以后, 你可以在任何地方使用几行代码,就可以使用他, 我们复用我们的组件就变成了:复制三行JS代码->粘贴三行JS代码
const wrap = document.querySelector(".wrap");
const add = new Add();
wrap.appendChild(add.render());
框架时代
2.0 数据驱动组件
什么是数据驱动呢?简单的来说,就是「数据是什么,我们就展示什么」。 回顾我们之前的组件内部
//生成DOM元素
const wrapper = this.createWrapper(domString);
//DOM中查找元素
const plus = wrapper.querySelector(".plus");
const text = wrapper.querySelector(".text");
let number = 0;
plus.addEventListener(
"click",
function() {
number++;
text.innerHTML = number;
},
false
);
我们在其中大量的使用了,querySelector这种api,使得我们需要各种手动的去操作DOM元素, 如果我们的组件变成
<div class="wrap">
<div class="text">0</div>
<div class="text_2">0</div>
<div class="text_3">0</div>
<button class='plus'>
+
</button>
</div>
然后我需要点一下按钮,三个标签都得变化, 那我们的代码很可能就需要对每一个text进行选择:
//生成DOM元素
const wrapper = this.createWrapper(domString);
//DOM中查找元素
const plus = wrapper.querySelector(".plus");
const text = wrapper.querySelector(".text");
const text_2 = wrapper.querySelector(".text_2");
const text_3 = wrapper.querySelector(".text_3");
let number = 0;
plus.addEventListener(
"click",
function() {
number++;
text.innerHTML = number;
text_2.innerHTML = number;
text_3.innerHTML = number;
},
false
);
这种做法,显然是不符合我们的预期的, 因为这非常影响我们的开发效率, 而我们也仅仅是简单的增添了一些新的标签以及展示,我们都非得增加一大堆的代码。
2.1 引入组件的状态
「数据是什么,我们就展示什么」,这是我们最终的想要达到的成果, 方法很多,但是最省代码的方式和最简单的方式就是:当我们数据发生改变的同时,我们根据数据的变化,重新渲染整个组件,那就简单多了。
话非常的绕,我们的代码这么去写:
class Add {
constructor() {
//构造函数,设置state
this.state = {
number: 0
};
}
//重写setState方法,使得每次调用setState,就会重新渲染,更新组件
setState(NewState) {
const oldElement = this.wrapper;
this.state = { ...NewState }; //ES6语法,展开操作符
this.wrapper = this.render(); //重新渲染
//拿到新的组件,更新原来的组件
if (this.update) this.update(oldElement, this.wrapper);
}
createWrapper(string) {
const wrapper = document.createElement("div");
wrapper.innerHTML = string;
return wrapper;
}
onClick() {
//事件调用
let NewState = this.state.number + 1;
this.setState({
number: NewState
});
}
render() {
const domString = `
<div class="text">${this.state.number}</div>
<div class="text_2">${this.state.number}</div>
<div class="text_3">${this.state.number}</div>
<button class='plus'>
+
</button>`;
this.wrapper = this.createWrapper(domString);
const plus = this.wrapper.querySelector(".plus");
plus.addEventListener("click", this.onClick.bind(this), false);
return this.wrapper;
}
}
const wrap = document.querySelector(".wrap");
const add = new Add();
wrap.appendChild(add.render());
add.update = (old, next) => {
//给组件定义一个方法
wrap.insertBefore(next, old); //插入新的组件
wrap.removeChild(old); //去掉旧的组件
};
代码稍微有些长了,但是其实很好理解。我们最终达到的目的就是「当我们数据发生改变的同时,我们根据数据的变化,重新渲染整个组件」
2.2 抽象:让所有组件都拥有自动更新的能力
我们现在已经自己开发出一个非常容易去复用的组件了, 但是当我们要开发另外一个组件的时候或许就要重新写一套这样的「复用」逻辑。 在我们的思想里,所有的组件都应该拥有「自动更新的能力」, 因此我们将组件根据状态,自动更新的能力抽取出来,那我们以后就非常方便了。
class ButtonComponent {
constructor() {}
createWrapper(string) {
//document.createElement, DOM api,根据标签名字创建DOM元素
const wrapper = document.createElement("div");
wrapper.innerHTML = string;
return wrapper;
}
setState(newState) {
const oldElement = this.wrapper;
this.state = { ...newState };
this.wrapper = this.renderElement(); //渲染出元素
if (this.update) this.update(oldElement, this.wrapper);
}
renderElement() {
this.wrapper = this.createWrapper(this.render());
const plus = this.wrapper.querySelector(".plus");
if (this.onClick) {
plus.addEventListener("click", this.onClick.bind(this), false);
}
return this.wrapper;
}
render() {} //玩家需要重写的函数
}
好了,逻辑抽象完成。 还记得我们的React有一个ReactDOM.render方法吗? 我们改一下名字直接叫做:
const renderToDOM = (component, DOMElement) => {
DOMElement.appendChild(component.renderElement());
component.update = (old, next) => {
//给组件定义一个方法
DOMElement.insertBefore(next, old); //插入新的组件
DOMElement.removeChild(old); //去掉旧的组件
};
};
最后呢,我们刚刚的组件,可以写成这样~
class Add extends ButtonComponent {
constructor() {
super();
this.state = {
number: 0
};
}
onClick() {
//事件调用
let NewState = this.state.number + 1;
this.setState({
number: NewState
});
}
render() {
return `
<div class="text">${this.state.number}</div>
<div class="text_2">${this.state.number}</div>
<div class="text_3">${this.state.number}</div>
<button class='plus'>
+
</button>`;
}
}
renderToDOM(new Add(), document.getElementById("wrap"));
自此,我们的组件化就完成了。 你可以看到,我们的组件其实外貌上已经非常像React,并且已经拥有了一个良好的组件复用能力。
最后:我们为什么需要React?
通过上述的一点一点分析,实现我们可以得知,实际上React的出现,已经完完全全的颠覆了传统的开发模式。
我们无须再去:
使用DOM API,一个一个的元素进行更新,操作 组件复用非常的简单,几乎每次复用之前写的组件,就是1-2行代码就搞定 组件内部状态,自己管理自己,跟外界完全无关
实际上,所有的框架出现都是为了解决「开发者困难」的问题。试想一下,一个10万行的项目,整天都是一个一个的元素进行更新,操作,那请10个程序员都维护不过来。有了这些框架的帮助,我们就能够更好的开发。
当我们站在这个角度去看前端框架的时候,我们就会发现,其实三大框架也不过就是:react牌锤子,vue牌锤子,ng牌锤子,他们都是锤子,以后就算有框架出来,也还是锤子....
组件分类
展示组件 Presentational components
基础样式组件 The base, styled components
只关心事情的样子。
通常用于排版和布局 typography and layout
经常只使用
props.children
[react]
示例:
h1
、section
、div
、span
、Icon
(可能包含className
和可访问性属性)
展示组件 Presentational components
关心事情的样子。
仅通过
props
接收数据和回调。
除了 UI 状态之外,很少有自己的状态
没有生命周期方法
示例:头像、信息、列表 Avatar, Info, List
容器组件 Container components
容器展示器组件 Container presenter components
关注事物与展示组件的外观如何相同。
未连接到 redux,或拥有自己的 UI 状态以外的状态
可以在没有状态或连接到 redux 的情况下进行测试
可以有生命周期方法
通常与容器位于同一文件中
示例:
UserPagePresenter
、UserListPresenter
[react]
容器组件 Container components
关心事情是如何运作的。
不使用样式
经常连接到 redux 或者有自己的状态
向展示组件或其他容器组件提供数据和行为。
示例:用户页面、用户列表 UserPage, UserList