談一談組件化
作者:記得要微笑
來源:SegmentFault 思否社區
前言
今天前端生態裏面,React、Angular 和 Vue 三分天下。雖然這三個框架的定位各有不同,但是它們有一個核心的共同點,那就是提供了組件化的能力。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 將組件設計成一個函數容器,接受組件配置參數 options,append 到指定 DOM 上。這是一個完全使用 JS 來實現組件的體系,它定義了嚴格的繼承關係,以及初始化、渲染、銷燬的生命週期,這樣的方案很好地支撐了 ExtJS 的前端架構。
https://www.w3cschool.cn/extjs/extjs_overview.html
HTML Component
搞前端時間比較長的同學都會知道一個東西,那就是 HTC(HTML Components),這個東西名字很現在流行的 Web Components 很像,但卻是不同的兩個東西,它們的思路有很多相似點,但是前者已是昨日黃花,後者方興未艾,是什麼造成了它們的這種差距呢?
因爲主流瀏覽器裏面只有 IE 支持過 HTC,所以很多人潛意識都認爲它不標準,但其實它也是有標準文檔的,而且到現在還有鏈接,注意它的時間!
https://www.w3.org/TR/NOTE-HTMLComponents
在 MSDN online 對 HTC 的定義僅如下幾句:
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 高亮可以使用 onmouseover 和 onmouseout 事件動態更改 li 元素樣式:
<HEAD>
<STYLE>
.HILITE
{ color:red;letter-spacing:2; }
</STYLE>
</HEAD>
<BODY>
<UL>
<LI onmouseover="this.class
onmouseout ="this.class>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);用戶可以設置這兩種狀態下開關所顯示的文本(設置屬性 turnOffText 和 turnOnText);用戶點擊開關時,開關狀態被反置,並觸發一個事件(onStatusChanged)通知用戶,用戶可以自己寫代碼來響應這個事件;該 HTC 還定義了一個方法(reverseStatus),用來反置開關的狀態。下面是這個 HTC 的代碼:
<!—switch.htc定義 -->
<PUBLIC:COMPONENT TAG>
<!--屬性定義-->
<PUBLIC:PROPERTY />
<PUBLIC:PROPERTY />
<PUBLIC:PROPERTY />
<!--定義事件-->
<PUBLIC:EVENT />
<!--定義方法-->
<PUBLIC:METHOD />
<!--關聯客戶端事件-->
<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());
下面我們根據上面思想來設計一個輪播組件,能夠自動播放圖片
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta >
<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>
效果:
上面我們已經實現了一個簡單的輪播圖,接下來我們嘗試將其應用到 JSX 中
// index.js
const ele = <div id="root" >
<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 >
<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" >
<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"]();
/******/
/******/ })()
;
最後,實際的運行效果之前的一樣
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/f9jQCfdG_m5007B4UN1y1Q