果你是一個JavaScript新手或僅僅最近才在你的開發工作中接觸它,你可能感到沮喪。所有的語言都有自己的怪癖(quirks)——但從基于強類型的服務器端語言轉移過來的開發人員可能會感到困惑。我就曾經這樣,幾年前,當我被推到了全職JavaScript開發者的時候,有很多事情我希望我一開始就知道。在這篇文章中,我將分享一些怪癖,希望我能分享給你一些曾經令我頭痛不已的經驗。這不是一個完整列表——僅僅是一部分——但希望它讓你看清這門語言的強大之處,可能曾經被你認為是障礙的東西。
我們將看下列技巧:
C#出身的我非常熟悉==比較運算符。值類型(或字符串)當有相同值是是相等的。引用類型相等需要有相同的引用。(我們假設你沒有重載==運算符,或實現你自己的等值運算和GetHashCode方法)我很驚訝為什么JavaScript有兩個等值運算符:==和===。最初我的大部分代碼都是用的==,所以我并不知道當我運行如下代碼的時候JavaScript為我做了什么:
var x=1;
if(x=="1") {
console.log("YAY! They're equal!");
}
這是怎么回事?整數1是如何和字符串”1”相等的?
在JavaScript中,有相等(==)和嚴格相等(===)之說。相等運算符將強制轉換兩邊的操作數為相同類型后執行嚴格相等比較。所以在上面的例子中,字符串”1”會被轉換為整數1,這個過程在幕后進行,然后與變量x進行比較。
嚴格相等不進行類型轉換。如果操作數類型不同(如整數和字符串),那么他們不全等(嚴格相等)。
var x=1;
// 嚴格平等,類型必須相同
if(x==="1") {
console.log("Sadly, I'll never write this to the console");
}
if(x===1) {
console.log("YES! Strict Equality FTW.")
}
你可能正在考慮可能發生強制類型轉換而引起的各種恐怖問題——假設你的引用中發生了這種轉換,可能導致你非常困難找到問題出在哪里。這并不奇怪,這也是為什么經驗豐富的JavaScript開發者總是建議使用嚴格相等。
這取決于你來自其他什么語言,你可能見過或沒見過這種方式(這就是廢話)。
// 獲取person對象的firstName值
var name=person.firstName;
// 獲取數組的第三個元素
var theOneWeWant=myArray[2]; // remember, 0-based index不要忘了第一個元素的索引是0
然而,你知道它也可以使用括號引用對象的成員嗎?比如說:
var name=person["firstName"];
為什么會這樣有用嗎?而你會用點符號的大部分時間,有幾個實例的括號使某些方法可能無法這樣做。例如,我會經常重構大開關語句到一個調度表,所以這樣的事情:
為什么可以這樣用?你以前可能對使用點更熟悉,有幾個特例只能用括號表示法。例如,我經常會將switch語句重構為查找表(速度更快),其實就像這樣:
var doSomething=function(doWhat) {
switch(doWhat) {
case "doThisThing":
// more code...
break;
case "doThatThing":
// more code...
break;
case "doThisOtherThing":
// more code....
break;
// additional cases here, etc.
default:
// default behavior
break;
}
}
可以轉化為像下面這樣:
var thingsWeCanDo={
doThisThing : function() { /* behavior */ },
doThatThing : function() { /* behavior */ },
doThisOtherThing : function() { /* behavior */ },
default : function() { /* behavior */ }
};
var doSomething=function(doWhat) {
var thingToDo=thingsWeCanDo.hasOwnProperty(doWhat) ? doWhat : "default"
thingsWeCanDo[thingToDo]();
}
使用switch并沒有錯誤(并且在許多情況下,如果被迭代多次并且非常關注性能,switch可能比查找表表現更好)。然而查找表提供了一個很好的方法來組織和擴展代碼,并且括號允許你的屬性延時求值。
已經有一些偉大的博客發表了文章,正確理解了JavaScript中的this上下文(在文章的結尾我會給出一些不錯的鏈接),但它確實應該加到“我希望我知道”的列表。它真的困難看懂代碼并且自信的知道在任何位置this的值——你僅需要學習一組規則。不幸的是,我早起讀到的許多解釋只是增加了我的困惑。因此我試圖簡明扼要的做出解釋。
默認情況下,直到某些原因改變了執行上下文,否則this的值都指向全局對象。在瀏覽器中,那將會是window對象(或在node.js中為global)。
當你有一個對象,其有一個函數成員,沖父對象調用這方法,this的值將指向父對象。例如:
var marty={
firstName: "Marty",
lastName: "McFly",
timeTravel: function(year) {
console.log(this.firstName + " " + this.lastName + " is time traveling to " + year);
}
}
marty.timeTravel(1955);
// Marty McFly is time traveling to 1955
你可能已經知道你能引用marty對象的timeTravel方法并且創建一個其他對象的新引用。這實際上是JavaScript非常強大的特色——使我們能夠在不同的實例上引用行為(調用函數)。
var doc={
firstName: "Emmett",
lastName: "Brown",
}
doc.timeTravel=marty.timeTravel;
所以——如果我們調用doc.timeTravel(1885)將會發生什么?
doc.timeTravel(1885);
// Emmett Brown is time traveling to 1885
再次——上演黑暗魔法。嗯,并不是真的。記住,當你調用一個方法的時候,this上下文是被調用函數父的父對象。
當我們保存marty.TimeTravel方法的引用然后調用我們保存的引用時發生了什么?讓我們看看:
var getBackInTime=marty.timeTravel;
getBackInTime(2014);
// undefined undefined is time traveling to 2014
為什么是“undefined undefined”?!而不是“Matry McFly”?
讓我們問一個關鍵的問題:當我們調用我們的getBackInTime函數時父對象/容器對象是什么?當getBackIntTime函數存在于window中時,我們調用它作為一個函數,而不是一個對象的方法。當我們像這樣調用一個函數——沒有容器對象——this上下文將是全局對象。David Shariff有一個偉大的描述關于這:
無論何時調用一個函數,我們必須立刻查看括號的左邊。如果在括號的左邊存在一個引用,那么被傳遞個調用函數的this值確定為引用所屬的對象,否則是全絕對象。
由于getBackInTime的this上下文是window——沒有firstName和lastName屬性——這解釋了為什么我們看見“undefined undefined”。
因此我們知道直接調用一個函數——沒有容器對象——this上下文的結果是全局對象。然而我也說我早就知道我們的getBackInTime函數存在于window上。我是如何知道的?好的,不像上面我包裹getBackInTime在不同的上下文(我們探討立即執行函數表達式的時候),我聲明的任何變量都被添加的window。來自Chrome控制臺的驗證:
是時候討論下this的主要用武之地之一了:訂閱事件處理。
所以,讓我們假裝我們想調用我們的marty.timeTravel方法當有人點擊一個按鈕時:
var flux=document.getElementById("flux-capacitor");
flux.addEventListener("click", marty.timeTravel);
在上面的代碼中,當用戶點擊按鈕是,我們會看見“undefined undefined is time traveling to [object MouseEvent]”。什么?好——首先,非常明顯的問題是我們沒有給我們的timeTravel方法提供year參數。反而,我們直接訂閱這方法作為事件處理程序,并且MouseEvent參數被作為第一個參數傳遞個事件處理程序。這是很容易修復的,但真正的問題是我們再次見到“undefined undefined”。不要無望——你已經知道為什么會發生這種情況(即使你還沒意識到)。讓我們修改我們的timeTravel函數,輸出this,從而幫助我們搞清事實:
marty.timeTravel=function(year) {
console.log(this.firstName + " " + this.lastName + " is time traveling to " + year);
console.log(this);
};
現在——當我們點擊這按鈕,我們將類似下面的輸出 在你的瀏覽器控制臺:
當方法被調用時,第二個console.log輸出出this上下文——它實際上是我們訂閱事件的按鈕元素。你感到吃驚嗎?就像之前——當我們將marty.timeTravel賦值給getBackInTime變量時——對marty.timeTravel的引用被保存到事件處理程序,并被調用,但容器對象不再是marty對象。在這種情況下,它將在按鈕實例的點擊事件中異步調用。
所以——有可能將this設置為我們想要的結果嗎?絕對可以!在這個例子里,解決方法非常簡單。不在事件處理程序中直接訂閱marty.timeTravel,而是使用匿名函數作為事件處理程序,并在匿名函數中調用marty.timeTravel。這也能修復year參數丟失的問題。
flux.addEventListener("click", function(e) {
marty.timeTravel(someYearValue);
});
點擊按鈕將會在控制臺輸出類似下面的信息:
成功了!但為什么這樣可以?思考我們是如何調用timeTravel方法的。在我們按鈕點擊的第一個例子中,我們在事件處理程序中訂閱方法自身的引用,所以它沒有從父對象marty上調用。在第二個例子中,通過this為按鈕元素的匿名函數,并且當我們調用marty.timeTravel時,我們從其父對象marty上調用,所以this為marty。
當你用構造函數創建對象實例時,函數內部的this值就是新創建的對象。例如:
var TimeTraveler=function(fName, lName) {
this.firstName=fName;
this.lastName=lName;
// Constructor functions return the
// newly created object for us unless
// we specifically return something else
};
var marty=new TimeTraveler("Marty", "McFly");
console.log(marty.firstName + " " + marty.lastName);
// Marty McFly
你可能開始疑惑,上面的例子中,沒有語言級別的特性允許我們在運行時指定調用函數的this值嗎?你是對的。存在于函數原型上的call和apply方法允許我們調用函數并傳遞this值。
call方法的第一個參數是this,后面是被調用函數的參數序列:
someFn.call(this, arg1, arg2, arg3);
apply的第一個參數也是this,后面是其余參數組成的數組:
someFn.apply(this, [arg1, arg2, arg3]);
我們的doc和marty實例他們自己能時間旅行,但einstein(愛因斯坦)需要他們的幫助才能完成時間旅行。所以讓我們給我們的doc實例添加一個方法,以至于doc能幫助einstein完成時間旅行。
doc.timeTravelFor=function(instance, year) {
this.timeTravel.call(instance, year);
// 如果你使用apply使用下面的語法
// this.timeTravel.apply(instance, [year]);
};
現在它可以傳送Einstein 了:
var einstein={
firstName: "Einstein",
lastName: "(the dog)"
};
doc.timeTravelFor(einstein, 1985);
// Einstein (the dog) is time traveling to 1985
我知道這個例子有些牽強,但它足以讓你看到應用函數到其他對象的強大之處。
這種方法還有我們沒有發現的另一種用處。讓我們給我們的marty實例添加一個goHome方法,作為this.timeTravel(1985)的快捷方式。
marty.goHome=function() {
this.timeTravel(1985);
}
然而,我們知道如果我們訂閱marty.goHome作為按鈕的點擊事件處理程序,this的值將是按鈕——并且不幸的是按鈕沒有timeTravel方法。我們能用上面的方法解決——用個一匿名函數作為事件處理程序,并在其內部調用上述方法——但我們有另一個選擇——bind函數:
flux.addEventListener("click", marty.goHome.bind(marty));
bind函數實際上會返回一個新函數,新函數的this值根據你提供的參數設置。如果你需要支持低版本瀏覽器(例如:ie9以下版本),你可能需要bind函數的shim(或者,如果你使用jQuery你可以用$.proxy代替,underscore和lodash都提供_.bind方法)。
記住重要一點,如果你直接使用原型上的bind方法,它將創建一個實例方法,這將繞過原型方法的優點。這不是錯誤,做到心里清楚就行了。我寫了關于這個問題得更多信息在這里。
函數聲明不需要var關鍵字。事實上,如Angus Croll所說:“把他們想象成變量聲明的兄弟有助于理解”。例如:
function timeTravel(year) {
console.log(this.firstName + " " + this.lastName + " is time traveling to " + year);
} 上面例子里的函數名字timeTravel不僅在它聲明的在作用域可見,同時在函數本身內部也是可見的(這對遞歸函數調用非常有用)。函數聲明,本質上說其實就是命名函數。換句話說,上面函數的名稱屬性是timeTravel。
函數表達式定義一個函數并指派給一個變量。典型應用如下:
var someFn=function() {
console.log("I like to express myself...");
}; 也可以對函數表達式命名——然而,不像函數聲明,命名函數表達式的名字僅在它自身函數體內可訪問:
var someFn=function iHazName() {
console.log("I like to express myself...");
if(needsMoreExpressing) {
iHazName(); // 函數的名字在這里可以訪問
}
};
// 你可以在這里調用someFn(),但不能調用iHazName()
someFn();
討論函數表達式和函數聲明不能不提“hoisting(提升)”——函數和變量聲明被編譯器移到作用域的頂部。在這里我們無法詳細解釋hoisting,但你可以讀Ben Cherry和Angus Croll兩個人的偉大解釋。
基于我們剛才的討論,你可能一進猜到“匿名”函數其實就是一個沒有名字的函數。大多數JavaScript開發者能迅速識別瞎買年第一個參數為匿名函數:
someElement.addEventListener("click", function(e) {
// I'm anonymous!
});
然而,同樣的我們的marty.timeTravvel方法也是一個匿名函數:
var marty={
firstName: "Marty",
lastName: "McFly",
timeTravel: function(year) {
console.log(this.firstName + " " + this.lastName + " is time traveling to " + year);
}
}
因為函數聲明必須有一個唯一的名字,只有函數表達式可以沒有名字。
因為我們正在談論函數表達式,有一個東西我希望我早知道:立即執行函數表達式(IIFE)。有很多關于IIFE的好文章(我將在文章結尾出列出),但用一句話來形容,函數表達式不是通過將函數表達式賦值給一個標量,稍后再執行,而是理解執行。可以在瀏覽器控制臺看這一過程。
首先——讓我們先敲入一個函數表達式——但不給它指派變量——看看會發什么:
語法錯誤——這被認為是函數聲明,缺少函數名字。然而,為了使其變為表達式,我們僅需將其包裹在括號內:
讓其變為表達式后控制臺返回給我們一個匿名函數(記住,我們沒有為其指派值,但表達式會有返回值)。所以——我們知道“函數表達式”是“立即調用函數表達式”的一部分。為了等到“立即執行”的特性,我們通過在表達式后面添加另一個括號來調用返回的表達式(就像我們調用其他函數一樣):
“但是等一下,Jim!(指作者)我想我以前見過這種調用方式”。 事實上你可能見過——這是合法的語法(眾所周知的是Douglas Crockford的首選語法)
這兩種方法都起作用,但是我強烈建議你讀一讀這里。
OK,非常棒——現在我們已經知道了IIFE是什么——以及為什么要用它?
它幫助我們控制作用域——任何JavaScript教程中非常重要的部分!前面我們看到的許多實例都創建在全局作用域。這意味著window(假設環境是瀏覽器)對象將有很多屬性。如果我們全部按照這種方式寫我們的JavaScript代碼,我們會迅速在全局作用域積累一噸(夸張)變量聲明,window代碼會被污染。即使在最好的情況下,在全局變量暴漏許多細節是糟糕的建議,但當變量的名字和已經存在的window屬性名字相同時會發生什么呢?window屬性會被重寫!
例如,如果你最喜歡的“Amelia Earhart”網站在全局作用域聲明了一個navigator變量,下面是設置之前和之后的結果:
哎呀!
顯而易見——全局變量被污染是糟糕的。JavaScript使用函數作用域(而不是塊作用域,如果你來自C#或Java,這點非常重要!),所以保持我們的代碼和全局作用域分離的辦法是創建一個新作用域,我們可以使用IIFE來實現,因為它的內容在它自己的函數作用域內。在下面的例子中,我將在控制臺向你顯示window.navigator的值,然后我常見一個IIFE(立即執行函數表達式)去包裹Amelia Earhart的行為和數據。IIFE結束后返回一個作為我們的“程序命名空間”的對象。我在IIFE內聲明的navigator變量將不會重寫window.navigator的值。
作為額外好處,我們上面創建的IIFE是JavaScript中模塊模式的啟蒙。我將在結尾處包括一些我瀏覽的模塊模式的鏈接。
最終,可能發現在某些情況下,你需要檢查傳遞給函數參數的類型,或其他類似的東西。typeof運算符會是顯而易見的選擇,但是,這并不是萬能的。例如,當我們對一個對象,數組,字符串或正則表達式,調用typeof運算符時會發生什么?
還好——至少我們可以將字符串和對象,數組,正則表達式區分開,對嗎?幸運的是,我們可以得到更準確的類型信息,我們有其他不同的方法。我們將使用Object.prototype.toString方法,并且應用我們前面提到的call方法:
為什么我們要使用Object.prototype上的toString方法?因為第三方庫或你自己的代碼可能重寫實例的toString方法。通過Object.prototype,我們可以強制實現實例原來的toString行為。
如果你知道typeof將會返回什么那么你不需要進行多余的檢查(例如,你僅需要知道是或不是一個字符串),此時用typeof非常好。然而,如果你需要區分數組和對象,正則表達式和對象,等等,那么使用Object.prototype.toString吧。
說得簡單點,在線HTML編輯器就是在網上發帖子、寫博客的那個帶編輯功能的框框,可以進行圖文排版等操作。
當年本菜鳥做網站的時候,曾經自己用javascript編寫過一個比較簡單的在線HTML編輯器,用于文本內容的排版。但是因為水平有限,很多功能都無法實現。后來有了eWebEditor,功能確實強大,但這個編輯器是個收費的軟件,并且正因為功能強大,也就顯得過重,一些輕量的場合不是太適用。那有沒有既免費、又功能強大、還能適應輕量場合的在線HTML編輯器呢?
答案肯定是有的。這首先要感謝這些年來有一批奉獻精神的程序猿們不斷推進共享軟件的開發,讓我們得以享受他們的成果。
UEditor是由百度web前端研發部開發所見即所得富文本web編輯器,具有輕量,可定制,注重用戶體驗等特點,開源基于MIT協議,允許自由使用和修改代碼。特別要說的是,頭條號后臺發布文章的編輯器就是用的UEditor!
百度UEditor
UEditor還有一個輕量版的,叫做UMeditor,簡稱UM。UM是為滿足廣大門戶網站對于簡單發帖框,或者回復框需求所定制的在線HTML編輯器。 主要特點是容量和加載速度上的改變,主文件的代碼量為139k,而且放棄了使用傳統的iframe模式,采用了div的加載方式, 以達到更快的加載速度和零加載失敗率。UM的第一個使用者是百度貼吧,以經受貼吧每天幾億的pv的考驗,功能設計應當是最優化的了。 當然隨著代碼的減少,UM的功能對于UE來說還是有所減少,但也有增加,比如拖拽圖片上傳,chrome的圖片拖動改變大小等。
百度UEditor界面
xhEditor是一個基于jQuery開發的簡單迷你并且高效的可視化HTML編輯器,基于網絡訪問并且兼容IE 6.0+,Firefox 3.0+,Opera 9.6+,Chrome 1.0+,Safari 3.22+。
xhEditor完全基于Javascript開發,可以應用在任何的服務端語言環境下,例如:PHP、ASP、ASP.NET、JAVA等。可以在CMS、博客、論壇、商城等互聯網平臺上完美的嵌入運行,能夠非常靈活簡單的和您的系統實現完美的無縫銜接。
主要特點:
精簡迷你:初始加載4個文件,包括:1個js(50k)+2個css(10k)+1個圖片(5k),總共65k。若js和css文件進行gzip壓縮傳輸,可以進一步縮減為24k左右。
使用簡單:簡單的調用方式,加一個class屬性就能將textarea變成一個功能豐富的可視化編輯器。
無障礙訪問:提供WAI-ARIA全面支持,全鍵盤精細操作,全程語音向導,提供完美無障礙訪問體驗,充分滿足殘疾人的上網需求。
內置Ajax上傳:內置強大的Ajax上傳,包括HTML4和HTML5上傳支持(多文件上傳、真實上傳進度及文件拖放上傳),剪切板上傳及遠程抓取上傳。
Word自動清理:實現Word代碼自動檢測并清理,生成代碼最優化精簡,卻不丟失細節效果。
UBB可視化編輯:支持UBB可視化編輯,在獲得安全高效代碼存儲的同時,又能享受可視化編輯的便捷。
KindEditor 也是一個開源的在線HTML編輯器, 使用 JavaScript 編寫,可以無縫地與 Java、.NET、PHP、ASP 等程序集成,比較適合在 CMS、商城、論壇、博客、Wiki、電子郵件等互聯網應用上使用。本菜鳥感覺這個編輯器上手比較容易,功能也很強大,界面比較友好,很適合菜鳥使用。可惜的是最近好像停止更新了,官網好象也能不正常訪問。
KindEditor界面
主要特點:
快速:體積小,加載速度快
開源:開放源代碼,高水平,高品質
底層:內置自定義 DOM 類庫,精確操作 DOM
擴展:基于插件的設計,所有功能都是插件,可根據需求增減功能
風格:修改編輯器風格非常容易,只需修改一個 CSS 文件
兼容:支持大部分主流瀏覽器,比如 IE、Firefox、Safari、Chrome、Opera
嚴格來說,KISSY不僅僅是一個在線HTML編輯器,而是由阿里集團前端工程師們發起創建的一個開源 JS 框架,具有跨終端、模塊化、使用簡單的特點。里面帶有HTML編輯器這個模塊。
阿里的KISSY
正因為KISSY采取模塊化設計,因此具有高擴展性、組件齊全,接口一致、自主開發、適合多種應用場景等優點。KISSY 除了完備的工具集合諸如 DOM、Event、Ajax、Anim 等,KISSY 還面向團隊協作做了獨特設計,提供了經典的面向對象、動態加載、性能優化解決方案。作為一款全終端支持的 JavaScript 框架,KISSY還 為移動終端做了大量適配和優化,搞移動web開發的可以好好研究一下KISSY的運用。
關于菜鳥手記:
菜鳥最怕就是看大神的攻略,全是術語看頭就頭大!本人作為一名對啥都感興趣的資深菜鳥,潛心研究各類技術二十余年,做網站、寫程序、搞美工、練書法、學畫畫、作文章、抓管理、裝逼格,屬于搞IT里面最懂美工的,搞HR里面最懂畫畫的,搞文字里面最懂程序的,最終一事無成,博而不精,徒留一堆手記。從菜鳥角度寫手記,同樣的”白”更易懂,你值得擁有!感興趣的,請別忘點右角關注菜鳥手記。
文地址:https://markodenic.com/html-tips/
Marko 2020年4月12日
在這篇文章中,我很興奮地與您分享一些非常有用的HTML小技巧。
*文章會定期更新,假如有新的小技巧的話。
超文本標記語言(HTML)是設計用來在瀏覽器中顯示文檔的標準標記語言。它能夠獲得諸如級聯樣式表(CSS)、JavaScript等腳本語言技術的支持。
性能小技巧,我們能夠通過使用loading=lazy屬性,去推遲圖片的加載,直到用戶滾動看到它們。
<img src='image.jpg' loading='lazy' alt='Alternative Text'>
<a href="mailto:{email}?subject={subject}&body={content}">
Send us an email
</a>
<a href="tel:{phone}">
Call us
</a>
<a href="sms:{phone}?body={content}">
Send us a message
</a>
通過使用"start"屬性去改變有序列表的開始點。
您可以使用<meter>元素來顯示數量,無需JavaScript和CSS。
您可以使用fieldset元素,在一個web表單中對幾個控件和標簽(<label>)進行分組。
用target="_blank"打開的新頁面,新頁面中可以訪問原始的來源window,通過window.opener。這個可能會有隱含的安全或者性能問題。使用 rel="noopener" 或者 rel="noreferrer"去阻止這種行為。
<a href="https://markodenic.com/" target="_blank" rel="noopener">
Marko's website
</a>
如果您想讓您文檔中的所有鏈接都在新的tab頁打開,您可以使用base標簽:
_self: 載入結果到當前瀏覽上下文中。(該值是元素的默認值)。
_blank: 載入結果到一個新的未命名的瀏覽上下文。
_parent: 載入結果到父級瀏覽上下文(如果當前頁是內聯框)。如果沒有父級結構,該選項的行為和_self一樣。
_top: 載入結果到頂級瀏覽上下文(該瀏覽上下文是當前上下文的最頂級上下文)。如果沒有父級,該選項的行為和_self一樣
為了能夠刷新你網站的favicon,你可以強制瀏覽器去下載新文件,通過給文件鏈接后面添加?v=2。
這個在生產環境特別有用,確保你的用戶使用的是最新的版本。
<link rel="icon" href="/favicon.ico?v=2" />
通過spellcheck屬性去定義元素是否應該檢查拼寫錯誤。
您可以通過使用<input type="range">來創建一個滑塊
您可以使用details元素去創建原生的HTML手風琴
您可以使用<mark>標記去高亮文本。
您可以在鏈接上使用download屬性,去下載您的文件,而不是打開鏈接。
<a href='path/to/file' download>
Download
</a>
使用圖片大小更小的.webp格式,同時提升網站性能。
<picture>
<!-- load .webp image if supported -->
<source srcset="logo.webp" type="image/webp">
<!--
Fallback if `.webp` images or <picture> tag
not supported by the browser.
-->
<img src="logo.png" alt="logo">
</picture>
使用poster屬性指定一張在視頻下載時顯示的圖片,或者一直到用戶點擊播放按鈕。
<video poster="path/to/image">
對于你的搜索輸入框可以使用type="search",然后你將會免費獲得一個清除的按鈕。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。