5

前言

今天前端生态里面,ReactAngularVue三分天下。虽然这三个框架的定位各有不同,但是它们有一个核心的共同点,那就是提供了组件化的能力。W3C也有Web Component的相关草案,也是为了提供组件化能力。今天我们就来聊聊组件化是什么,以及它为什么这么重要。

正文

其实组件化思想是一种前端技术非常自然的延伸,如果你使用过HTML,相信你一定有过“我要是能定义一个标签就好了”这样的想法。HTML虽然提供了一百多个标签,但是它们都只能实现一些非常初级的功能。

HTML本身的目标,是标准化的语义,既然是标准化,跟自定义标签名就有一定的冲突。所以从前端最早出现的2005年,到现在2022年,我们一直没有等到自定义标签这个功能,至今仍然是Draft状态。

但是,前端组件化的需求一直都存在,历史长流中工程师们提出过很多组件化的解决方案。

ExtJS

Ext JS是一个流行的JavaScript框架,它为使用跨浏览器功能构建Web应用程序提供了丰富的UI。我们来看看它的组件定义:

MainPanel = function() {
  this.preview = new Ext.Panel({
    id: "preview",
    region: "south"
    // ...
  });
  MainPanel.superclass.constructor.call(this, {
    id: "main-tabs",
    activeTab: 0,
    region: "center"
    // ...
  });

  this.gsm = this.grid.getSelectionModel();

  this.gsm.on(
    "rowselect", function(sm, index, record) {
      // ...
    }, this, { buffer: 250 }
  );

  this.grid.store.on("beforeload", this.preview.clear, this.preview);
  this.grid.store.on("load", this.gsm.selectFirstRow, this.gsm);

  this.grid.on("rowdbclick", this.openTab, this);
};

Ext.extend(MainPanel, Ext.TabPanel, {
  loadFeed: function(feed) {
    // ...
  },
  // ...
  movePreview: function(m, pressed) {
    // ...
  }
});

你可以看到ExtJS将组件设计成一个函数容器,接受组件配置参数optionsappend到指定DOM上。这是一个完全使用JS来实现组件的体系,它定义了严格的继承关系,以及初始化、渲染、销毁的生命周期,这样的方案很好地支撑了ExtJS的前端架构。

https://www.w3cschool.cn/extj...

HTML Component

搞前端时间比较长的同学都会知道一个东西,那就是HTCHTML Components),这个东西名字很现在流行的Web Components很像,但却是不同的两个东西,它们的思路有很多相似点,但是前者已是昨日黄花,后者方兴未艾,是什么造成了它们的这种差距呢?

因为主流浏览器里面只有IE支持过HTC,所以很多人潜意识都认为它不标准,但其实它也是有标准文档的,而且到现在还有链接,注意它的时间!

http://www.w3.org/TR/NOTE-HTM...

MSDN onlineHTC的定义仅如下几句:

HTML Components (HTCs) provide a mechanism to implement components in script as Dynamic HTML (DHTML) behaviors. Saved with an .htc extension, an HTC is an HTML file that contains script and a set of HTC-specific elements that define the component.
(HTC是由HTML标记、特殊标记和脚本组成的定义了DHTML特性的组件.)

作为组件,它也有属性、方法、事件,下面简要说明其定义方式:

  • <PUBLIC:COMPONENT></PUBLIC:COMPONENT>:定义HTC,这个标签是其他定义的父元素。
  • <PUBLIC:PROPERTY NAME=”pName” GET=”getMethod” PUT=”putMethod” />: 定义HTC的属性,里面三个定义分别代表属性名、读取属性、设置属性时HTC所调用的方法。
  • <PUBLIC:METHOD NAME=”mName” />:定义HTC的方法,NAME定义了方法名。
  • <PUBLIC:EVENT NAME=”eName” ID=”eId” />:定义了HTC的事件,NAME定义了事件名,ID是个可选属性,在HTC中唯一标识这个事件。
  • <PUBLID:ATTACH EVENT=”sEvent” ONEVENT=”doEvent” />:定义了浏览器传给HTC事件的相应方法,其中EVENT是浏览器传入的事件,ONEVENT是处理事件的方法。

我们来看看它主要能做什么呢?

它可以以两种方式被引入到HTML页面中,一种是作为“行为”被附加到元素,使用CSS引入,一种是作为“组件”,扩展HTML的标签体系

行为为脚本封装和代码重用提供了一种手段

通过行为,可以轻松地将交互效果添加为可跨多个页面重用的封装组件。例如,考虑在 Internet Explorer 4.0 中实现onmouseover highlight的效果,通过使用 CSS 规则,以及动态更改样式的能力,很容易在页面上实现这种效果。在 Internet Explorer 4.0 中,实现在列表元素li上实现 onmouseover 高亮可以使用onmouseoveronmouseout事件动态更改li元素样式:

<HEAD>
<STYLE>
.HILITE
{ color:red;letter-spacing:2; }
</STYLE>
</HEAD>

<BODY>
<UL>
<LI onmouseover="this.className='HILITE'"
    onmouseout ="this.className=''">HTML Authoring</LI>
</UL>
</BODY>

Internet Explorer 5 开始,可以通过 DHTML 行为来实现此效果。当将DHTML行为应用于li元素时,此行为扩展了列表项的默认行为,在用户将鼠标移到其上时更改其颜色。

下面的示例以 HTML 组件 (HTC) 文件的形式实现一个行为,该文件包含在hilite.htc文件中,以实现鼠标悬停高亮效果。使用 CSS 行为属性将行为应用到元素li 上。上述代码在 Internet Explorer 5 及更高版本中可能如下所示:

// hilite.htc
<HTML xmlns:PUBLIC="urn:HTMLComponent">
// <ATTACH> 元素定义了浏览器传给HTC事件的相应方法,其中EVENT是浏览器传入的事件,ONEVENT是处理事件的方法
<PUBLIC:ATTACH EVENT="onmouseover" ONEVENT="Hilite()" />
<PUBLIC:ATTACH EVENT="onmouseout"  ONEVENT="Restore()" />
<SCRIPT LANGUAGE="JScript">
var normalColor;

function Hilite()
{
   if (event.srcElement == element)
   {
     normalColor = style.color;
     runtimeStyle.color  = "red";
     runtimeStyle.cursor = "hand";
   }
}

function Restore()
{
   if (event.srcElement == element)
   {
      runtimeStyle.color  = normalColor;
      runtimeStyle.cursor = "";
   }
}
</SCRIPT>

通过CSS behavior属性将DHTML行为附加到页面上的元素

<HEAD>
<STYLE>
   LI {behavior:url(hilite.htc)}
</STYLE>
</HEAD>

<BODY>
<UL>
  <LI>HTML Authoring</LI>
</UL>
</BODY>

HTC自定义标记

我们经常看到某些网页上有这样的效果:用户点击一个按钮,文本显示,再次点击这个按钮,文本消失,但浏览器并不刷新。下面我就用HTC来实现这个简单效果。编程思路是这样的:用HTC模拟一个开关,它有”on””off”两种状态(可读/写属性status);用户可以设置这两种状态下开关所显示的文本(设置属性 turnOffTextturnOnText);用户点击开关时,开关状态被反置,并触发一个事件(onStatusChanged)通知用户,用户可以自己写代码来响应这个事件;该HTC还定义了一个方法(reverseStatus),用来反置开关的状态。下面是这个HTC的代码:

<!—switch.htc定义 -->  
<PUBLIC:COMPONENT TAGNAME="Switch">  
    <!--属性定义-->  
    <PUBLIC:PROPERTY NAME="turnOnText" PUT="setTurnOnText" VALUE="Turn on" />  
    <PUBLIC:PROPERTY NAME="turnOffText" PUT="setTurnOffText" VALUE="Turn off" />  
    <PUBLIC:PROPERTY NAME="status" GET="getStatus" PUT="setStatus" VALUE="on" />  
  
    <!--定义事件-->  
    <PUBLIC:EVENT NAME="onStatusChanged" ID="changedEvent" />  
  
    <!--定义方法-->  
    <PUBLIC:METHOD NAME="reverseStatus" />  
  
    <!--关联客户端事件-->  
    <PUBLIC:ATTACH EVENT="oncontentready" ONEVENT="initialize()"/>  
    <PUBLIC:ATTACH EVENT="onclick" ONEVENT="expandCollapse()"/>  
  
</PUBLIC:COMPONENT>  
  
<!-- htc脚本 -->  
<script language="javascript">  
    var sTurnOnText;    //关闭状态所显示的文本  
    var sTurnOffText;   //开启状态所显示的文本  
    var sStatus;    //开关状态  
    var innerHTML   //使用开关时包含在开关中的HTML     
  
    //设置开关关闭状态所显示的文本  
    function setTurnOnText(value)  
    {  
        sTurnOnText = value;  
    }   
  
    //设置开关开启状态所显示的文本  
    function setTurnOffText(value)  
    {  
        sTurnOffText = value;  
    }     
  
    //设置开关状态  
    function setStatus(value)  
    {  
        sStatus = value;  
    }   
  
     //读取开关状态  
    function getStatus()  
    {  
        return sStatus;  
    }     
  
    //反向开关的状态  
    function reverseStatus()  
    {  
        sStatus = (sStatus == "on") ? "off" : "on";  
    }  
  
    //获取htc控制界面html文本  
    function getTitle()  
    {  
        var text = (sStatus == "on") ? sTurnOffText : sTurnOnText;  
        text = "<div id='innerDiv'>" + text + "</div>";  
        return text;  
  
    }  
  
    //htc初始化代码  
    function initialize()  
    {  
        //back up innerHTML  
        innerHTML = element.innerHTML;  
        element.innerHTML = (sStatus == "on") ? getTitle() + innerHTML : getTitle();  
    }   
  
    //响应用户鼠标事件的方法  
    function expandCollapse()  
    {  
         reverseStatus();  
         //触发事件  
         var oEvent = createEventObject();  
         changedEvent.fire(oEvent);    
         var srcElem = element.document.parentWindow.event.srcElement;  
         if(srcElem.id == "innerDiv")  
         {  
              element.innerHTML = (sStatus == "on") ? getTitle() + innerHTML : getTitle();  
         }  
    }  
</script>  

html页面引入自定义标记

<!--learnhtc.html-->  
<html xmlns:frogone><!--定义一个新的命名空间-->  
<head>  
    <!--告诉浏览器命名空间是由哪个HTC实现的-->  
    <?IMPORT namespace="frogone" implementation="switch.htc">  
</head>  
<body>  
   <!--设置开关的各个属性及内部包含的内容-->  
   <frogone:Switch id="mySwitch"  
                    TurnOffText="off"  
                    TurnOnText="on"  
                    status="off"  
                    onStatusChanged="confirmChange()">  
        <div id="dBody">文本内容...... </div>  
    </frogone:Switch>  
</body>  
<script language="javascript">  
    //相应开关事件  
    function confirmChange()  
    {  
        if(!confirm("是否改变开关状态?"))  
            mySwitch.reverseStatus();
    }  
</script>  
</html>

这项技术提供了事件绑定和属性、方法定义,以及一些生命周期相关的事件,应该说已经是一个比较完整的组件化方案了。但是我们可以看到后来的结果,它没有能够进入标准,默默地消失了。用我们今天的角度来看,它可以说是生不逢时。

如何定义一个组件

ExtJS基于面向对象的思想,将组件设计成函数容器,拥有严格的继承关系和组件生命周期钩子。HTC利用IE浏览器内置的一种脚本封装机制,将行为从文档结构中分离,通过类似样式或者自定义标识的方式为HTML页面引入高级的自定义行为(behavior)。从历史上组件化的尝试来看,我们应该如何来定义一个组件呢?

首先应该清楚组件化的设想为了解决什么问题?不言而喻,组件化最直接的目的就是复用,提高开发效率,作为一个组件应该满足下面几个条件:

  • 封装:组件屏蔽了内部的细节,组件的使用者可以只关心组件的属性、事件和方法。
  • 解耦:组件本身隔离了变化,组件开发者和业务开发者可以根据组件的约定各自独立开发和测试。
  • 复用:组件将会作为一种复用单元,被用在多处。
  • 抽象:组件通过属性和事件、方法等基础设施,提供了一种描述UI的统一模式,降低了使用者学习的心智成本。

接下来我们深入具体的技术细节,看看组件化的基本思路。首先,最基础的语义化标签就能看作成一个个组件,通过DOM API可以直接挂载到对应的元素上:

var element = document.createElement('div')
document.getElementById('container').appendChild(element)

但是实际上我们的组件不可能这么简单,涵盖场景比较多的组件都比较复杂,工程师就想到将组件定义为原生JS中的Function或者Class容器(其实Object也是一种思路,比如Vue),因为在JavaScript语法中它们天生就是提供了一个闭关的空间,形如:

function MyComponent(){
    this.prop1;
    this.method1;
    ……
}

不过,要想挂载又成了难题,普通的JS对象没法被用于appendChild,所以前端工程师就有了两种思路,第一种是反过来,设计一个appendTo方法,让组件把自己挂到DOM树上去。

function MyComponent(){
    this.root = document.createElement("div");
    this.appendTo = function(node){
        node.appendChild(this._root)
    }
}

第二种比较有意思,是让组件直接返回一个DOM元素,把方法和自定义属性挂到这个元素上:

function MyComponent(){
    var _root = document.createElement("div");
    root.prop1 // = ...
    root.method1 = function(){
    /*....*/
    }
    return root;
}

document.getElementById("container").appendChild(new MyComponent());

下面我们根据上面思想来设计一个轮播组件,能够自动播放图片

Picture1.gif

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .carousel, .carousel > img {
      width: 500px;
      height: 300px;
    }

    .carousel {
      display: flex;
      overflow: hidden;
    }

    .carousel > img {
      transition: transform ease 0.5s;
    }
  </style>
</head>
<body>
  <script>
    let d = [
      {
          img: "https://static001.geekbang.org/resource/image/bb/21/bb38fb7c1073eaee1755f81131f11d21.jpg",
          url: "https://time.geekbang.org",
          title: "蓝猫"
      },
      {
          img: "https://static001.geekbang.org/resource/image/1b/21/1b809d9a2bdf3ecc481322d7c9223c21.jpg",
          url: "https://time.geekbang.org",
          title: "橘猫"
      },
      {
          img: "https://static001.geekbang.org/resource/image/b6/4f/b6d65b2f12646a9fd6b8cb2b020d754f.jpg",
          url: "https://time.geekbang.org",
          title: "橘猫加白"
      },
      {
          img: "https://static001.geekbang.org/resource/image/73/e4/730ea9c393def7975deceb48b3eb6fe4.jpg",
          url: "https://time.geekbang.org",
          title: "猫"
      }
    ];

    class Carousel {
      constructor(data) {
        this._root = document.createElement('div');
        this._root.classList = ['carousel']
        this.children = [];
        for (const d of data) {
          const img = document.createElement('img');
          img.src = d.img;
          this._root.appendChild(img);
          this.children.push(img);
        }

        let i = 0;
        let current = i
        setInterval(() => {
          for (const child of this.children) {
            child.style.zIndex = '0';
          }
          // 计算下一张图片的下标
          let next = (i + 1) % this.children.length;

          const currentElement = this.children[current];
          const nextElement = this.children[next];

          // 下一张图片的zIndex应该大于当前图片的zIndex
          currentElement.style.zIndex = '1';
          nextElement.style.zIndex = '2';
          // 禁止添加的动画过渡样式
          currentElement.style.transition = 'none';
          nextElement.style.transition = 'none';
          console.log('current', current, next)
          // 每次初始化当前图片和下一张图片的位置
          currentElement.style.transform = `translate3d(${-100 * current}%, 0 , 0)`;
          nextElement.style.transform = `translate3d(${100 - 100 * next}%, 0 , 0)`;

          // 浏览器刷新频率是每秒60帧,所以这里需要延迟到浏览器下次重绘更新下一帧动画
          setTimeout(() => {
            // 启动添加的动画过渡样式
            currentElement.style.transition = '';
            nextElement.style.transition = '';
            // 当前图片退出,下一张图片进来
            currentElement.style.transform = `translate3d(${-100 -100 * current}% 0 , 0)`;
            nextElement.style.transform = `translate3d(${-100 * next}%, 0 , 0)`;
          }, 1000 / 60);
            
          // 或者使用window.requestAnimationFrame,当然这个比较难理解,容易出错,使用setTimeout也是可以的
          // window.requestAnimationFrame(() => {
          //   window.requestAnimationFrame(() => {
          //   // 启动添加的动画过渡样式
          //   currentElement.style.transition = '';
          //   nextElement.style.transition = '';
          //   // 当前图片退出,下一张图片进来
          //   currentElement.style.transform = `translate3d(${-100 -100 * current}% 0 , 0)`;
          //   nextElement.style.transform = `translate3d(${-100 * next}%, 0 , 0)`;
          //   })
          // })

          current = next;
          i++;
        }, 3000);

        // 追加 
        this.appendTo = function(node){
          node.appendChild(this._root)
        }
      }
    }

    new Carousel(d).appendTo(document.body);
  </script>
</body>
</html>

效果:

amimation.gif

上面我们已经实现了一个简单的轮播图,接下来我们尝试将其应用到JSX

// index.js
const ele = <div id="root" name="container">
  <Carousel data={d}></Carousel>
  <span>a</span>
  <span>b</span>
  <span>c</span>
</div>

document.body.appendChild(ele);
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .carousel, .carousel > img {
      width: 500px;
      height: 300px;
    }

    .carousel {
      display: flex;
      overflow: hidden;
    }

    .carousel > img {
      transition: transform ease 0.5s;
    }
  </style>
</head>
<body>
  <script src="./index.js"></script>
</body>
</html>

但是直接照上面这样往html中引入含有JSX语法的js脚本运行会报错,因为浏览器并不支持JSX,那我们怎么办呢?

我们需要先将JSX编译成js,然后再引入到html中,此时Babel就派上用场了,@babel/plugin-transform-react-jsx插件可以帮助我们将JSX编译js

// 编译前
const ele = <div id="root" name="container">
  <Carousel data={d}></Carousel>
  <span>a</span>
  <span>b</span>
  <span>c</span>
</div>

// 编译后
var ele = React.createElement("div", {id: "root", name: "container"}, 
    React.createElement(Carousel, {data: d}), 
    React.createElement("span", null, "a"), 
    React.createElement("span", null, "b"), 
    React.createElement("span", null, "c")
);

编译后的元素将默认采用React.createElement创建,createElement方法除了支持基本的html标签外,还支持自定义的函数组件和类组件,但问题是我们的Carousel组件并不是React中的函数组件和类组件,正好@babel/plugin-transform-react-jsx默认配置参数pragma使用React.createElement替换编译JSX表达式时使用的函数,也允许我们自定义函数去做React.createElement函数类似的事情,下面我们来实现一下:

function createElement<P extends {}>(
    type: FunctionComponent<P>,
    props?: Attributes & P | null,
    ...children: ReactNode[]): FunctionComponentElement<P>;
function createElement<P extends {}>(
    type: ClassType<P, ClassicComponent<P, ComponentState>, ClassicComponentClass<P>>,
    props?: ClassAttributes<ClassicComponent<P, ComponentState>> & P | null,
    ...children: ReactNode[]): CElement<P, ClassicComponent<P, ComponentState>>;
function createElement<P extends {}, T extends Component<P, ComponentState>, C extends ComponentClass<P>>(
    type: ClassType<P, T, C>,
    props?: ClassAttributes<T> & P | null,
    ...children: ReactNode[]): CElement<P, T>;
function createElement<P extends {}>(
    type: FunctionComponent<P> | ComponentClass<P> | string,
    props?: Attributes & P | null,
    ...children: ReactNode[]): ReactElement<P>;

首先,我们先来改造一下Carousel组件,使其可以接收注入的data属性,这里我们采用setAttribute和属性描述符set存值函数来实现

// index.js
class Carousel {
  constructor(data) {
    this._root = document.createElement('div');
    this._root.classList = ['carousel'];
    this.children = [];
  }

  set data(data) {
    this._root.innerHTML = '';

    for (const d of data) {
      const img = document.createElement('img');
      img.src = d.img;
      this._root.appendChild(img);
      this.children.push(img);
    }

    let i = 0;
    let current = i
    setInterval(() => {
      for (const child of this.children) {
        child.style.zIndex = '0';
      }
      let next = (i + 1) % this.children.length;

      const currentElement = this.children[current];
      const nextElement = this.children[next];

      currentElement.style.zIndex = '1';
      nextElement.style.zIndex = '2';
      currentElement.style.transition = 'none';
      nextElement.style.transition = 'none';
      currentElement.style.transform = `translate3d(${-100 * current}%, 0 , 0)`;
      nextElement.style.transform = `translate3d(${100 - 100 * next}%, 0 , 0)`;

      setTimeout(() => {
        currentElement.style.transition = '';
        nextElement.style.transition = '';
        currentElement.style.transform = `translate3d(${-100 -100 * current}% 0 , 0)`;
        nextElement.style.transform = `translate3d(${-100 * next}%, 0 , 0)`;
      }, 1000 / 60);

      current = next;
      i++;

    }, 3000);
  }

  setAttribute(name, value) {
    this[name] = value; // 这里统一attribute和properties,vue使用的是attribute
  }

  // 追加 
  appendTo = function(node){
    node.appendChild(this._root);
  }
}

当往Carousel组件注入data时,我们触发组件的setAttribute方法将data挂载到组件实例上,并且触发set存值函数,初始化轮播图组件。那么如何在注入data时触发组件的setAttribute方法呢?这就是我们自定义转化函数要做的事了

// index.js
const create = (Class, properity, ...children) => {
  let element;
  if (typeof Class === 'string') {
    // 基本标签直接创建
    element = document.createElement(Class);
  } else {
    // 自定义组件实例化
    element = new Class;
  }

  // 注入到基本标签上的属性直接追加到元素的Attribute属性中,而注入到自定义组件的属性调用组件的setAttribute方法
  for (const p in properity) {
    element.setAttribute(p, properity[p]);  
  }

  // 处理子节点
  for(let child of children) {
    if (typeof child === 'string') {
      // 如果子节点是字符串,那就创建文本节点
      child = document.createTextNode(child);
    }
    // 如果子节点含有appendTo方法,则是我们自定义的Carousel组件,将子节点追加上当前节点上
    if (child.appendTo) {
      child.appendTo(element);
    } else {
      // html标签,也追加到当前节点上
      element.appendChild(child);
    }
  }
  return element;
}

最后我们将index.js构建后在引入到html中,附上webpack配置:

// webpack.config.js
const path = require('path')

module.exports = {
  entry: "./index.js",
  output: {
    path: path.resolve(__dirname, 'dist')
  },
  module: {
      rules: [
          {
              test:/\.js$/,
              use:{
                  loader: "babel-loader",
                  options: {
                      presets:["@babel/preset-env"],
                      plugins: [["@babel/plugin-transform-react-jsx", {pragma: "create"}]]
                  }
              }
          }
      ]
  },
  mode: "development"
}

我们来看下构建后的脚本

/*
 * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
 * This devtool is neither made for production nor for readable output files.
 * It uses "eval()" calls to create a separate source file in the browser devtools.
 * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
 * or disable the default devtool with "devtool: false".
 * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
 */
/******/ (() => { // webpackBootstrap
/******/     var __webpack_modules__ = ({

/***/ "./index.js":
/*!******************!*\
  !*** ./index.js ***!
  \******************/
/***/ (() => {

eval("function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== \"undefined\" && o[Symbol.iterator] || o[\"@@iterator\"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === \"number\") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError(\"Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\"); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it[\"return\"] != null) it[\"return\"](); } finally { if (didErr) throw err; } } }; }\n\nfunction _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === \"string\") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === \"Object\" && o.constructor) n = o.constructor.name; if (n === \"Map\" || n === \"Set\") return Array.from(o); if (n === \"Arguments\" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }\n\nfunction _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }\n\nvar create = function create(Class, properity) {\n  var element;\n\n  if (typeof Class === 'string') {\n    element = document.createElement(Class);\n  } else {\n    element = new Class();\n  }\n\n  for (var p in properity) {\n    element.setAttribute(p, properity[p]);\n  }\n\n  for (var _len = arguments.length, children = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {\n    children[_key - 2] = arguments[_key];\n  }\n\n  for (var _i = 0, _children = children; _i < _children.length; _i++) {\n    var child = _children[_i];\n\n    if (typeof child === 'string') {\n      // 文本节点\n      child = document.createTextNode(child);\n    }\n\n    if (child.appendTo) {\n      // Carousel组件\n      child.appendTo(element);\n    } else {\n      // html标签\n      element.appendChild(child);\n    }\n  }\n\n  return element;\n};\n\nvar d = [{\n  img: \"https://static001.geekbang.org/resource/image/bb/21/bb38fb7c1073eaee1755f81131f11d21.jpg\",\n  url: \"https://time.geekbang.org\",\n  title: \"蓝猫\"\n}, {\n  img: \"https://static001.geekbang.org/resource/image/1b/21/1b809d9a2bdf3ecc481322d7c9223c21.jpg\",\n  url: \"https://time.geekbang.org\",\n  title: \"橘猫\"\n}, {\n  img: \"https://static001.geekbang.org/resource/image/b6/4f/b6d65b2f12646a9fd6b8cb2b020d754f.jpg\",\n  url: \"https://time.geekbang.org\",\n  title: \"橘猫加白\"\n}, {\n  img: \"https://static001.geekbang.org/resource/image/73/e4/730ea9c393def7975deceb48b3eb6fe4.jpg\",\n  url: \"https://time.geekbang.org\",\n  title: \"猫\"\n}];\n\nvar Carousel = /*#__PURE__*/function () {\n  function Carousel(data) {\n    _classCallCheck(this, Carousel);\n\n    _defineProperty(this, \"appendTo\", function (node) {\n      node.appendChild(this._root);\n    });\n\n    this._root = document.createElement('div');\n    this._root.classList = ['carousel'];\n    this.children = [];\n  }\n\n  _createClass(Carousel, [{\n    key: \"data\",\n    set: function set(data) {\n      var _this = this;\n\n      this._root.innerHTML = '';\n\n      var _iterator = _createForOfIteratorHelper(data),\n          _step;\n\n      try {\n        for (_iterator.s(); !(_step = _iterator.n()).done;) {\n          var _d = _step.value;\n          var img = document.createElement('img');\n          img.src = _d.img;\n\n          this._root.appendChild(img);\n\n          this.children.push(img);\n        }\n      } catch (err) {\n        _iterator.e(err);\n      } finally {\n        _iterator.f();\n      }\n\n      var i = 0;\n      var current = i;\n      setInterval(function () {\n        var _iterator2 = _createForOfIteratorHelper(_this.children),\n            _step2;\n\n        try {\n          for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {\n            var child = _step2.value;\n            child.style.zIndex = '0';\n          }\n        } catch (err) {\n          _iterator2.e(err);\n        } finally {\n          _iterator2.f();\n        }\n\n        var next = (i + 1) % _this.children.length;\n        var currentElement = _this.children[current];\n        var nextElement = _this.children[next];\n        currentElement.style.zIndex = '1';\n        nextElement.style.zIndex = '2';\n        currentElement.style.transition = 'none';\n        nextElement.style.transition = 'none';\n        currentElement.style.transform = \"translate3d(\".concat(-100 * current, \"%, 0 , 0)\");\n        nextElement.style.transform = \"translate3d(\".concat(100 - 100 * next, \"%, 0 , 0)\");\n        setTimeout(function () {\n          currentElement.style.transition = '';\n          nextElement.style.transition = '';\n          currentElement.style.transform = \"translate3d(\".concat(-100 - 100 * current, \"% 0 , 0)\");\n          nextElement.style.transform = \"translate3d(\".concat(-100 * next, \"%, 0 , 0)\");\n        }, 1000 / 60);\n        current = next;\n        i++;\n      }, 3000);\n    }\n  }, {\n    key: \"setAttribute\",\n    value: function setAttribute(name, value) {\n      this[name] = value; // 这里统一attribute和properties,vue使用的是attribute\n    } // 追加 \n\n  }]);\n\n  return Carousel;\n}();\n\nvar ele = create(\"div\", {\n  id: \"root\",\n  name: \"container\"\n}, create(Carousel, {\n  data: d\n}), create(\"span\", null, \"a\"), create(\"span\", null, \"b\"), create(\"span\", null, \"c\"));\ndocument.body.appendChild(ele);\n\n//# sourceURL=webpack://webpack-jsx/./index.js?");

/***/ })

/******/     });
/************************************************************************/
/******/     
/******/     // startup
/******/     // Load entry module and return exports
/******/     // This entry module can't be inlined because the eval devtool is used.
/******/     var __webpack_exports__ = {};
/******/     __webpack_modules__["./index.js"]();
/******/     
/******/ })()
;

最后,实际的运行效果之前的一样

未完待续!!!

参考:

HTC(HTML Component) 入门

HTML Component


记得要微笑
1.9k 声望4.5k 粉丝

知不足而奋进,望远山而前行,卯足劲,不减热爱。