WangShuXian6 / blog

FE-BLOG
https://wangshuxian6.github.io/blog/
MIT License
46 stars 10 forks source link

Components / HTML 组件开发 #86

Open WangShuXian6 opened 5 years ago

WangShuXian6 commented 5 years ago

Components / HTML 组件开发

一个组件是一个独立的UI元素,被设计成可以在许多项目中重复使用。 它的目标是在做好一件事的同时,保持足够的抽象性以允许各种使用情况。 开发人员可以将它们作为构建块来构建新的用户体验。 每次使用这些组件时,都不必担心每个组件的核心设计和功能。 例如按钮、链接、表单、输入字段和模态。

WangShuXian6 commented 5 years ago

前端组件演变

https://www.zhihu.com/question/267797409


无框架的年代

1.0 上古时期

image

我们需要开发这样的一个功能,点击+号按钮,上面的数字就加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 看似聪明的办法
```ts
 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 = `

0
    <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代码
```ts
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元素,
>如果我们的组件变成
```html
<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 引入组件的状态
>「数据是什么,我们就展示什么」,这是我们最终的想要达到的成果,
>方法很多,但是最省代码的方式和最简单的方式就是:当我们数据发生改变的同时,我们根据数据的变化,重新渲染整个组件,那就简单多了。

>话非常的绕,我们的代码这么去写:
```ts
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); //去掉旧的组件
};

代码稍微有些长了,但是其实很好理解。我们最终达到的目的就是「当我们数据发生改变的同时,我们根据数据的变化,重新渲染整个组件」 image


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方法吗?
>我们改一下名字直接叫做:
```ts
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牌锤子,他们都是锤子,以后就算有框架出来,也还是锤子....
WangShuXian6 commented 3 years ago

组件分类

展示组件 Presentational components

基础样式组件 The base, styled components

只关心事情的样子。

通常用于排版和布局 typography and layout

经常只使用 props.children [react]

示例:h1sectiondivspanIcon(可能包含 className 和可访问性属性)

展示组件 Presentational components

关心事情的样子。

仅通过 props 接收数据和回调。

除了 UI 状态之外,很少有自己的状态

没有生命周期方法

示例:头像、信息、列表 Avatar, Info, List


容器组件 Container components

容器展示器组件 Container presenter components

关注事物与展示组件的外观如何相同。

未连接到 redux,或拥有自己的 UI 状态以外的状态

可以在没有状态或连接到 redux 的情况下进行测试

可以有生命周期方法

通常与容器位于同一文件中

示例:UserPagePresenterUserListPresenter [react]

容器组件 Container components

关心事情是如何运作的。

不使用样式

经常连接到 redux 或者有自己的状态

向展示组件或其他容器组件提供数据和行为。

示例:用户页面、用户列表 UserPage, UserList

WangShuXian6 commented 3 months ago

管理大量状态

创建自定义钩子可以帮助你抽离和复用组件逻辑。

例如,如果多个组件需要进行相同的数据获取和更新逻辑,可以将这部分逻辑封装在自定义钩子中。


import { useState, useEffect } from 'react';

function useUserData(userId) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`).then(response => response.json()).then(setUser);
  }, [userId]);

  return user;
}

function UserProfile({ userId }) {
  const user = useUserData(userId);
  return <div>{user ? user.name : 'Loading...'}</div>;
}