blog icon indicating copy to clipboard operation
blog copied to clipboard

Components / HTML 组件开发

Open WangShuXian6 opened this issue 5 years ago • 3 comments

Components / HTML 组件开发

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

WangShuXian6 avatar Jul 12 '19 08:07 WangShuXian6

前端组件演变

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 看似聪明的办法

 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); //去掉旧的组件
};

代码稍微有些长了,但是其实很好理解。我们最终达到的目的就是「当我们数据发生改变的同时,我们根据数据的变化,重新渲染整个组件」 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方法吗? 我们改一下名字直接叫做:

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 avatar Jul 12 '19 08:07 WangShuXian6

组件分类

展示组件 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 avatar Sep 14 '21 06:09 WangShuXian6