4

谈一谈组件化

 2 years ago
source link: https://segmentfault.com/a/1190000041469213
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

今天前端生态里面,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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK