Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
么是無頭瀏覽器(headless browser)?
無頭瀏覽器是指可以在圖形界面情況下運行的瀏覽器。我可以通過編程來控制無頭瀏覽器自動執行各種任務,比如做測試,給網頁截屏等。
“無頭”這個詞來源于最初的“無頭計算機(Headless computer)”。維基百科關于的“無頭計算機”詞條:
無頭系統(headless system)是指已配置為無須顯示器(即“頭”)、鍵盤和鼠標操作的計算機系統或設備。無頭系統通常通過網絡連接控制,但也有部分無頭系統的設備需要通過RS-232串行連接進行設備的管理。服務器通常采用無頭模式以降低運作成本。
除了之前提到的兩種無害的使用案例,無頭瀏覽器可以被用來自動執行惡意任務。最常見的形式是做網絡爬蟲,或偽裝訪問量,或探測網站漏洞。
一個非常流行的無頭瀏覽器是PhantomJS,因為它是基于 Qt 框架,所以跟我們常見的瀏覽器相比有很多不同的特征,因此有很多方法判斷出它。
但是,從chrome 59開始,谷歌發布了一款無頭谷歌瀏覽器。它跟PhantomJS不同,它是基于正統的谷歌瀏覽器開發出來的,不是基于其它的框架,這讓程序很難區分出它是正常瀏覽器還是無頭瀏覽器。
下面,我們將介紹幾種判斷程序是運行在普通瀏覽器還是無頭瀏覽器里的方法。
注意:這些方法只是在四種設備 (2 Linux, 2 Mac) 里測試過,也就是說, 肯定還有其他很多方法檢測無頭瀏覽器。
先介紹使用做最常見的一種判斷瀏覽器種類的方法,檢查User agent。在linux計算機里Chrome version 59無頭瀏覽器的User agent值是:
“Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/59.0.3071.115 Safari/537.36”
于是,我們可以這樣檢測是否是無頭Chrome瀏覽器:
if (/HeadlessChrome/.test(window.navigator.userAgent)) {
console.log("Chrome headless detected");
}
User agent 也可以從 HTTP headers 里獲取。然而,這兩種情況都很容易偽造。
navigator.plugins 會返回一個數組,里面是當前瀏覽器里的插件信息。通常,普通Chrome瀏覽器有一些缺省插件,比如 Chrome PDF viewer 或 Google Native Client。相反,在無頭模式里,沒有任何插件,返回的是個空數組。
if(navigator.plugins.length==0) {
console.log("It may be Chrome headless");
}
在谷歌瀏覽器里,有兩個JavaScript屬性可以獲取當前瀏覽器的語言設置: navigator.language 和 navigator.languages。頭一個是指瀏覽器界面的語言,后一個返回的是個數組,里面存儲的是瀏覽器用戶的所有次選語言。然而,在無頭模式里,navigator.languages 返回的是個空字符串。
if(navigator.languages=="") {
console.log("Chrome headless detected");
}
WebGL 提供了一組能在HTML canvas 里執行3D渲染的API。通過這些API,我們可以查詢出圖形驅動的 vendor 和 renderer 。
在linux上的普通谷歌瀏覽器里,我們獲得的 renderer 和 vendor 值為: “Google SwiftShader” 和 “Google Inc.”。
而在無頭模式里,我們獲得的一個是 “Mesa OffScreen”——它是沒有使用任何 window 系統的渲染技術的名稱,和 “Brian Paul” ——開源 Mesa 圖形庫的最初的程序。
var canvas=document.createElement('canvas');
var gl=canvas.getContext('webgl');
var debugInfo=gl.getExtension('WEBGL_debug_renderer_info');
var vendor=gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
var renderer=gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
if(vendor=="Brian Paul" && renderer=="Mesa OffScreen") {
console.log("Chrome headless detected");
}
并不是所有版本的無頭瀏覽器都有同樣的這兩個值。然而目前在無頭瀏覽器里是“Mesa Offscreen” 和 “Brian Paul” 這兩個值。
Modernizr 可以探測出當前瀏覽器對HTML和CSS各種特性的支持程度。我發現,普通Chrome和無頭Chrome里唯一的區別是,無頭模式下沒有 hairline 特征,它是用來檢測是否支持 hidpi/retina hairlines的
if(!Modernizr["hairline"]) {
console.log("It may be Chrome headless");
}
最后,我發現的最后一個方法,也是看起來最有效的方法,切入點是檢查瀏覽器里不能正常加載的圖片的高和寬。
在正常的Chrome里,未成功加載的圖片的大小跟瀏覽器的zoom有關,但肯定不是零。而在無頭Chrome瀏覽器里,這種圖片的寬和高都是0。
?
var body=document.getElementsByTagName("body")[0];
var image=document.createElement("img");
image.src="http://iloveponeydotcom32188.jg";
image.setAttribute("id", "fakeimage");
body.appendChild(image);
image.onerror=function(){
if(image.width==0 && image.height==0) {
console.log("Chrome headless detected");
}
}
這就是檢測無頭瀏覽器的詳細步驟
這里小編是一個有著10年工作經驗的前端高級工程師,關于web前端有許多的技術干貨,包括但不限于各大廠的最新面試題系列、前端項目、最新前端路線等。需要的伙伴可以私信我
發送【前端資料】
就可以獲取領取地址,免費送給大家。對于學習web前端有任何問題(學習方法,學習效率,如何就業)都可以問我。希望你也能憑自己的努力,成為下一個優秀的程序員
者 | 浪里行舟
責編 | 胡巍巍
在互聯網時代,數據安全與個人隱私受到了前所未有的挑戰,各種新奇的攻擊技術層出不窮。如何才能更好地保護我們的數據?本文主要側重于分析幾種常見的攻擊的類型以及防御的方法。
XSS
XSS (Cross-Site Scripting),跨站腳本攻擊,因為縮寫和 CSS重疊,所以只能叫 XSS。跨站腳本攻擊是指通過存在安全漏洞的Web網站注冊用戶的瀏覽器內運行非法的HTML標簽或JavaScript進行的一種攻擊。
跨站腳本攻擊有可能造成以下影響:
XSS 的原理是惡意攻擊者往 Web 頁面里插入惡意可執行網頁腳本代碼,當用戶瀏覽該頁之時,嵌入其中 Web 里面的腳本代碼會被執行,從而可以達到攻擊者盜取用戶信息或其他侵犯用戶安全隱私的目的。
XSS 的攻擊方式千變萬化,但還是可以大致細分為幾種類型。
1.非持久型 XSS(反射型 XSS )
非持久型 XSS 漏洞,一般是通過給別人發送帶有惡意腳本代碼參數的 URL,當 URL 地址被打開時,特有的惡意代碼參數被 HTML 解析、執行。
舉一個例子,比如頁面中包含有以下代碼:
1<select> 2 <script> 3 document.write('' 4 + '<option value=1>' 5 + location.href.substring(location.href.indexOf('default=') + 8) 6 + '</option>' 7 ); 8 document.write('<option value=2>English</option>'); 9 </script> 10</select>
攻擊者可直接通過URL (類似:https://xxx.com/xxx?default=<script>alert(document.cookie)</script>) 注入可執行的腳本代碼。不過一些瀏覽器如Chrome其內置了一些XSS過濾器,可以防止大部分反射型XSS攻擊。
非持久型 XSS 漏洞攻擊有以下幾點特征:
為了防止出現非持久型 XSS 漏洞,需要確保這么幾件事情:
2.持久型 XSS(存儲型 XSS)
持久型 XSS 漏洞,一般存在于 Form 表單提交等交互功能,如文章留言,提交文本信息等,黑客利用的 XSS 漏洞,將內容經正常功能提交進入數據庫持久保存,當前端頁面獲得后端從數據庫中讀出的注入代碼時,恰好將其渲染執行。
舉個例子,對于評論功能來說,就得防范持久型 XSS 攻擊,因為我可以在評論中輸入以下內容
主要注入頁面方式和非持久型 XSS 漏洞類似,只不過持久型的不是來源于 URL,referer,forms 等,而是來源于后端從數據庫中讀出來的數據 。持久型 XSS 攻擊不需要誘騙點擊,黑客只需要在提交表單的地方完成注入即可,但是這種 XSS 攻擊的成本相對還是很高。
攻擊成功需要同時滿足以下幾個條件:
持久型 XSS 有以下幾個特點:
3.如何防御
對于 XSS 攻擊來說,通常有兩種方式可以用來防御。
1) CSP
CSP 本質上就是建立白名單,開發者明確告訴瀏覽器哪些外部資源可以加載和執行。我們只需要配置規則,如何攔截是由瀏覽器自己實現的。我們可以通過這種方式來盡量減少 XSS 攻擊。
通常可以通過兩種方式來開啟 CSP:
這里以設置 HTTP Header 來舉例:
1Content-Security-Policy: default-src 'self'
1Content-Security-Policy: img-src https://*
1Content-Security-Policy: child-src 'none'
如需了解更多屬性,請查看Content-Security-Policy文檔
對于這種方式來說,只要開發者配置了正確的規則,那么即使網站存在漏洞,攻擊者也不能執行它的攻擊代碼,并且 CSP 的兼容性也不錯。
2) 轉義字符
用戶的輸入永遠不可信任的,最普遍的做法就是轉義輸入輸出的內容,對于引號、尖括號、斜杠進行轉義
1function escape(str) { 2 str=str.replace(/&/g, '&') 3 str=str.replace(/</g, '<') 4 str=str.replace(/>/g, '>') 5 str=str.replace(/"/g, '&quto;') 6 str=str.replace(/'/g, ''') 7 str=str.replace(/`/g, '`') 8 str=str.replace(/\//g, '/') 9 return str 10}
但是對于顯示富文本來說,顯然不能通過上面的辦法來轉義所有字符,因為這樣會把需要的格式也過濾掉。對于這種情況,通常采用白名單過濾的辦法,當然也可以通過黑名單過濾,但是考慮到需要過濾的標簽和標簽屬性實在太多,更加推薦使用白名單的方式。
1const xss=require('xss') 2let html=xss('<h1 id="title">XSS Demo</h1><script>alert("xss");</script>') 3// -> <h1>XSS Demo</h1><script>alert("xss");</script> 4console.log(html)
以上示例使用了 js-xss 來實現,可以看到在輸出中保留了 h1 標簽且過濾了 script 標簽。
3) HttpOnly Cookie。
這是預防XSS攻擊竊取用戶cookie最有效的防御手段。Web應用程序在設置cookie時,將其屬性設為HttpOnly,就可以避免該網頁的cookie被客戶端惡意JavaScript竊取,保護用戶cookie信息。
CSRF
CSRF(Cross Site Request Forgery),即跨站請求偽造,是一種常見的Web攻擊,它利用用戶已登錄的身份,在用戶毫不知情的情況下,以用戶的名義完成非法操作。
1.CSRF攻擊的原理
下面先介紹一下CSRF攻擊的原理:
完成 CSRF 攻擊必須要有三個條件:
我們來看一個例子: 當我們登入轉賬頁面后,突然眼前一亮驚現"XXX隱私照片,不看后悔一輩子"的鏈接,耐不住內心躁動,立馬點擊了該危險的網站(頁面代碼如下圖所示),但當這頁面一加載,便會執行submitForm這個方法來提交轉賬請求,從而將10塊轉給黑客。
2.如何防御
防范 CSRF 攻擊可以遵循以下幾種規則:
1) SameSite
可以對 Cookie 設置 SameSite 屬性。該屬性表示 Cookie 不隨著跨域請求發送,可以很大程度減少 CSRF 的攻擊,但是該屬性目前并不是所有瀏覽器都兼容。
2) Referer Check
HTTP Referer是header的一部分,當瀏覽器向web服務器發送請求時,一般會帶上Referer信息告訴服務器是從哪個頁面鏈接過來的,服務器籍此可以獲得一些信息用于處理。可以通過檢查請求的來源來防御CSRF攻擊。正常請求的referer具有一定規律,如在提交表單的referer必定是在該頁面發起的請求。所以通過檢查http包頭referer的值是不是這個頁面,來判斷是不是CSRF攻擊。
但在某些情況下如從https跳轉到http,瀏覽器處于安全考慮,不會發送referer,服務器就無法進行check了。若與該網站同域的其他網站有XSS漏洞,那么攻擊者可以在其他網站注入惡意腳本,受害者進入了此類同域的網址,也會遭受攻擊。出于以上原因,無法完全依賴Referer Check作為防御CSRF的主要手段。但是可以通過Referer Check來監控CSRF攻擊的發生。
3) Anti CSRF Token
目前比較完善的解決方案是加入Anti-CSRF-Token。即發送請求時在HTTP 請求中以參數的形式加入一個隨機產生的token,并在服務器建立一個攔截器來驗證這個token。服務器讀取瀏覽器當前域cookie中這個token值,會進行校驗該請求當中的token和cookie當中的token值是否都存在且相等,才認為這是合法的請求。否則認為這次請求是違法的,拒絕該次服務。
這種方法相比Referer檢查要安全很多,token可以在用戶登陸后產生并放于session或cookie中,然后在每次請求時服務器把token從session或cookie中拿出,與本次請求中的token 進行比對。由于token的存在,攻擊者無法再構造出一個完整的URL實施CSRF攻擊。但在處理多個頁面共存問題時,當某個頁面消耗掉token后,其他頁面的表單保存的還是被消耗掉的那個token,其他頁面的表單提交時會出現token錯誤。
4) 驗證碼
應用程序和用戶進行交互過程中,特別是賬戶交易這種核心步驟,強制用戶輸入驗證碼,才能完成最終請求。在通常情況下,驗證碼夠很好地遏制CSRF攻擊。但增加驗證碼降低了用戶的體驗,網站不能給所有的操作都加上驗證碼。所以只能將驗證碼作為一種輔助手段,在關鍵業務點設置驗證碼。
點擊劫持
點擊劫持是一種視覺欺騙的攻擊手段。攻擊者將需要攻擊的網站通過 iframe 嵌套的方式嵌入自己的網頁中,并將 iframe 設置為透明,在頁面中透出一個按鈕誘導用戶點擊。
1. 特點
2. 點擊劫持的原理
用戶在登陸 A 網站的系統后,被攻擊者誘惑打開第三方網站,而第三方網站通過 iframe 引入了 A 網站的頁面內容,用戶在第三方網站中點擊某個按鈕(被裝飾的按鈕),實際上是點擊了 A 網站的按鈕。
接下來我們舉個例子:我在優酷發布了很多視頻,想讓更多的人關注它,就可以通過點擊劫持來實現
1iframe { 2width: 1440px; 3height: 900px; 4position: absolute; 5top: -0px; 6left: -0px; 7z-index: 2; 8-moz-opacity: 0; 9opacity: 0; 10filter: alpha(opacity=0); 11} 12button { 13position: absolute; 14top: 270px; 15left: 1150px; 16z-index: 1; 17width: 90px; 18height:40px; 19} 20</style> 21...... 22<button>點擊脫衣</button> 23<img src="http://pic1.win4000.com/wallpaper/2018-03-19/5aaf2bf0122d2.jpg"> 24<iframe src="http://i.youku.com/u/UMjA0NTg4Njcy" scrolling="no"></iframe>
從上圖可知,攻擊者通過圖片作為頁面背景,隱藏了用戶操作的真實界面,當你按耐不住好奇點擊按鈕以后,真正的點擊的其實是隱藏的那個頁面的訂閱按鈕,然后就會在你不知情的情況下訂閱了。
3. 如何防御
1)X-FRAME-OPTIONS
X-FRAME-OPTIONS是一個 HTTP 響應頭,在現代瀏覽器有一個很好的支持。這個 HTTP 響應頭 就是為了防御用 iframe 嵌套的點擊劫持攻擊。
該響應頭有三個值可選,分別是
2)JavaScript 防御
對于某些遠古瀏覽器來說,并不能支持上面的這種方式,那我們只有通過 JS 的方式來防御點擊劫持了。
1<head> 2 <style id="click-jack"> 3 html { 4 display: none !important; 5 } 6 </style> 7</head> 8<body> 9 <script> 10 if (self==top) { 11 var style=document.getElementById('click-jack') 12 document.body.removeChild(style) 13 } else { 14 top.location=self.location 15 } 16 </script> 17</body> 以上代碼的作用就是當通過 iframe 的方式加載頁面時,攻擊者的網頁直接不顯示所有內容了。
URL跳轉漏洞
定義:借助未驗證的URL跳轉,將應用程序引導到不安全的第三方區域,從而導致的安全問題。
1.URL跳轉漏洞原理
黑客利用URL跳轉漏洞來誘導安全意識低的用戶點擊,導致用戶信息泄露或者資金的流失。其原理是黑客構建惡意鏈接(鏈接需要進行偽裝,盡可能迷惑),發在QQ群或者是瀏覽量多的貼吧/論壇中。
安全意識低的用戶點擊后,經過服務器或者瀏覽器解析后,跳到惡意的網站中。
惡意鏈接需要進行偽裝,經常的做法是熟悉的鏈接后面加上一個惡意的網址,這樣才迷惑用戶。
諸如偽裝成像如下的網址,你是否能夠識別出來是惡意網址呢?
1http://gate.baidu.com/index?act=go&url=http://t.cn/RVTatrd 2http://qt.qq.com/safecheck.html?flag=1&url=http://t.cn/RVTatrd 3http://tieba.baidu.com/f/user/passport?jumpUrl=http://t.cn/RVTatrd
2.實現方式:
這里我們舉個Header頭跳轉實現方式:
1<?php 2$url=$_GET['jumpto']; 3header("Location: $url"); 4?> 1http://www.wooyun.org/login.php?jumpto=http://www.evil.com
這里用戶會認為www.wooyun.org都是可信的,但是點擊上述鏈接將導致用戶最終訪問www.evil.com這個惡意網址。
3.如何防御
1)referer的限制
如果確定傳遞URL參數進入的來源,我們可以通過該方式實現安全限制,保證該URL的有效性,避免惡意用戶自己生成跳轉鏈接
2)加入有效性驗證Token
我們保證所有生成的鏈接都是來自于我們可信域的,通過在生成的鏈接里加入用戶不可控的Token對生成的鏈接進行校驗,可以避免用戶生成自己的惡意鏈接從而被利用,但是如果功能本身要求比較開放,可能導致有一定的限制。
SQL注入
SQL注入是一種常見的Web安全漏洞,攻擊者利用這個漏洞,可以訪問或修改數據,或者利用潛在的數據庫漏洞進行攻擊。
1.SQL注入的原理
我們先舉一個萬能鑰匙的例子來說明其原理:
1<form action="/login" method="POST"> 2 <p>Username: <input type="text" name="username" /></p> 3 <p>Password: <input type="password" name="password" /></p> 4 <p><input type="submit" value="登陸" /></p> 5</form>
后端的 SQL 語句可能是如下這樣的:
1let querySQL=` 2 SELECT * 3 FROM user 4 WHERE username='${username}' 5 AND psw='${password}' 6`; 7// 接下來就是執行 sql 語句... 8
這是我們經常見到的登錄頁面,但如果有一個惡意攻擊者輸入的用戶名是 admin' --,密碼隨意輸入,就可以直接登入系統了。why! ----這就是SQL注入。
我們之前預想的SQL 語句是:
1SELECT * FROM user WHERE username='admin' AND psw='password'
但是惡意攻擊者用奇怪用戶名將你的 SQL 語句變成了如下形式:
1SELECT * FROM user WHERE username='admin' --' AND psw='xxxx'
在 SQL 中,' --是閉合和注釋的意思,-- 是注釋后面的內容的意思,所以查詢語句就變成了:
1SELECT * FROM user WHERE username='admin'
所謂的萬能密碼,本質上就是SQL注入的一種利用方式。
一次SQL注入的過程包括以下幾個過程:
SQL注入的必備條件: 1.可以控制輸入的數據 2.服務器要執行的代碼拼接了控制的數據。
我們會發現SQL注入流程中與正常請求服務器類似,只是黑客控制了數據,構造了SQL查詢,而正常的請求不會SQL查詢這一步,SQL注入的本質:數據和代碼未分離,即數據當做了代碼來執行。
2.危害
3.如何防御
OS命令注入攻擊
OS命令注入和SQL注入差不多,只不過SQL注入是針對數據庫的,而OS命令注入是針對操作系統的。OS命令注入攻擊指通過Web應用,執行非法的操作系統命令達到攻擊的目的。只要在能調用Shell函數的地方就有存在被攻擊的風險。倘若調用Shell時存在疏漏,就可以執行插入的非法命令。
命令注入攻擊可以向Shell發送命令,讓Windows或Linux操作系統的命令行啟動程序。也就是說,通過命令注入攻擊可執行操作系統上安裝著的各種程序。
1.原理
黑客構造命令提交給web應用程序,web應用程序提取黑客構造的命令,拼接到被執行的命令中,因黑客注入的命令打破了原有命令結構,導致web應用執行了額外的命令,最后web應用程序將執行的結果輸出到響應頁面中。
我們通過一個例子來說明其原理,假如需要實現一個需求:用戶提交一些內容到服務器,然后在服務器執行一些系統命令去返回一個結果給用戶
1// 以 Node.js 為例,假如在接口中需要從 github 下載用戶指定的 repo 2const exec=require('mz/child_process').exec; 3let params={/* 用戶輸入的參數 */}; 4exec(`git clone ${params.repo} /some/path`); params.repo傳入的是 https://github.com/admin/admin.github.io.git 確實能從指定的 git repo 上下載到想要的代碼。
但是如果 params.repo 傳入的是 https://github.com/xx/xx.git && rm -rf /* && 恰好你的服務是用 root 權限起的就糟糕了。
2.如何防御
參考資料
編者按】本文來自A List Apart,重點講述了開發者如何應對琳瑯滿目的新技術。作者建議,開發者在接受新概念的同時,更要重視對原始瀏覽器的支持,并增強用戶體驗的開發。
將Web視為應用平臺的概念,正前所未有的流行著。但用來創建這些所謂“Web應用”的工具仍存在許多經常被我們忽視或誤解的陷阱。單頁面Web應用框架已得到極大關注,我們可以借助這些框架創建一些復雜的高性能應用,與傳統網站相比,這些應用更可靠且交互更加豐富。但所有的這些益處,以及隨之而來的思維模式和開發方式的轉變,是以犧牲瀏覽器的基本功能為代價的,Web開發者們有時卻將其視為理所當然。
JavaScript可能非常脆弱
隨著各家廠商不斷地炒作這股熱浪,我們可能誤以為當用戶的瀏覽器不能執行JavaScript時,并不需要為他們提供回退方案。用戶的瀏覽器不能執行JavaScript一定事出有因,他們手動選擇禁用JavaScript只是眾多原因之一。維護英國政府網站的團隊——政府數字服務(GDS)發現:每500位訪問GOV.UK的用戶中,有5人沒有請求JavaScript,其中只有1人主動禁用了JavaScript,其他4人沒有請求可能因為以下幾個原因:企業代理服務器限制過高;高延遲導致JavaScript請求超時;甚或是一個沒有被注意到的語法錯誤。
此外,CSS和HTML都可以優雅降級,而JavaScript卻做不到。這意味著,如果開發者使用一個單一的ES6語法特性,甚或是調用一個沒有經過驗證的標準庫函數,他們的JavaScript就很有可能在執行過程中終斷或者根本就不執行。如果你使用JavaScript來增強網站,上面提到的這些問題尚且可以忍受,畢竟訪問者仍然可以訪問鏈接,可以提交表單,可以使用Web能提供的最原始功能;但如果JavaScript是網站必不可少的一部分時,無論是誰使用稍微過時的瀏覽器都可能獲得一個空白頁面,自然也沒有人來解釋頁面為什么會變成空白。
語義結構仍然非常重要
自1993年Tim Berners-Lee設計HTML以來,HTML為相互關聯的文檔網定義了一個通用結構,也就是我們熟知的Web。滲透在這個通用結構中的語義含義為Web頁面中包含的信息提供了計算機可以處理的上下文。從實際的意義來說,這些額外的信息增強了用戶使用Web瀏覽器時的體驗。舉個例子,Web瀏覽器可以實現一個向用戶的日歷中添加使用time元素定義的事件的方法;屏幕閱讀器可以用不同的方式通讀一個列表或一段文字,對于人類來說,文檔中的列表與段落看起來明顯不一樣,HTML提供的通用框架讓計算機也能夠清晰分辨列表與段落。
HTML暗含的語義含義使Web與諸如Cocoa、WPF以及Qt這樣的原生應用環境有著不同的發展方向。結構化的信息對Web來說非常重要,因為我們需要通過多種方式訪問Web信息。而當我創建一個iPhone應用時,我可以穩妥地假設每一個人都會用相同的方式去使用它。我的App總會以相同的方式呈現信息,并且我能夠完全掌控信息在應用里的最終呈現。即使有些人通過VoiceOver(Apple為視障人群提供的輔助技術)與我的App進行交互,他們仍然可以與視力正常的用戶一樣:通過點擊屏幕進行操作。唯一的不同是他們需要聽文字而不是去閱讀。
而這種方法在Web上卻行不通。人們除了通過Web瀏覽器訪問網站,還會通過類似Pocket、Instapaper這樣的應用來消費網站內容,這些應用嘗試使用Web頁面的結構化信息來提取網站的相關內容。智能手表上的瀏覽器可能直接忽略你的布局,然后通過更適合一英寸屏幕的方式展現你的信息。未來的設備也許能夠直接將網站提供的信息轉化為人類大腦中的思維,這誰又會知道呢?回過頭看,VoiceOver的工作原理是按順序朗讀用戶指尖下排列的文字,然而Web屏幕閱讀器則通讀全部文檔,忽略布局,并且通過HTML標簽的標準化語義來推斷文檔含義。舉個例子,最近推出的main元素(譯者注:參考https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/main)用來定義文檔的主體部分,Web屏幕閱讀器可以讀取并識別這樣的標簽。對于一個視覺正常的用戶來說,通過Google Chrome訪問你的網站時,無論你使用<main>或者是<div id=”main”>基本沒有區別。但對于使用其它Web客戶端的人來說,例如使用屏幕閱讀器或Instapaper,main元素隱含的含義可以讓軟件更好地幫助他們瀏覽文檔。
所以,開發一款Web應用不像為原生平臺開發那么簡單。在五個主流瀏覽器中確保應用能按照我們的需求正常工作并及時發布,對于Web平臺來說還遠遠不夠,我們需要在屏幕閱讀器中測試我們的工作成果,需要重審我們的標記來確保應用能提供盡可能多的語義元數據——不僅需要協調已有的Web客戶端,也要為將來可能出現的一切設備做準備。
單頁面Web應用框架
當使用類似Angular和Ember這樣的“單頁面Web應用”框架時,流行的做法是把網站當成原生應用一樣對待,如此一來,開發者們就很少會考慮到Web平臺與眾不同的一面。他們為用戶作出的假設很容易徹底毀掉不滿足假設的用戶的真實體驗。這種思維模式會導致什么后果?我們來看一個示例,并認真思考我最近在Patreon網站上發現的一個登錄按鈕(后來有改動):
<span class="patreon-button sub-section navigation-active" data-ng-click="triggerChangeWindow(navigation.login_url)">Log In</span>
Patreon那個相當標準的登錄按鈕表現得像一個鏈接,這里不需要特殊的JavaScript。
這個鏈接在我的Safari中可以正常運行,但是在除主流瀏覽器外的其它環境中,這個按鈕完全不能使用。假設我們有一個稱為WatchBrowse的智能手表瀏覽器,很可能它需要為用戶顯示一系列的列表鏈接來實現站內導航,因為這個特殊的智能手表沒有光標用來與頁面進行交互。HTML定義了一個在Web頁面上創建鏈接的標準方式(a元素),WatchBrowse理論上可以在頁面上列出每一個a標簽和它的href屬性以及內容,除非出現一個類似Patreon的網站,并且該網站決定回避Web標準并且重新實現瀏覽器的基本功能。
如果Patreon使用一個a標簽而不是span標簽,WatchBrowse大概可以找到鏈接并將它顯示在列表中,你可以為鏈接模擬一個點擊事件,當用戶選中鏈接時進行跳轉。但是如果讓瀏覽器提前知曉鏈接將導向何處是否會更好?一個瀏覽器擴展可能通過頁面上標簽的href屬性來查找鏈接,如果你想快速找到某人Twitter賬戶的鏈接,那么提供一個可以溯源的href屬性就很實用。當鏈接的href屬性不再是靜態值,而是取決于任意的JavaScript點擊句柄,這些有用的功能就無法實現了。
Patreon的網站是基于Angular建立的,Angular本身沒有錯,將HTML當做視圖層并用這些框架去實現大概是導致Patreon糟糕決定的主要原因。
如果我們按照框架開發者在他們文檔中推薦的方法創建相同的鏈接會怎樣?一個更標準創建鏈接的方式看起來可能是這樣的:
<a ng-href="/login">Log In</a>
當通過客戶端JavaScript渲染到DOM中時,上面的代碼被轉換成這樣:
<a ng-href="/login" class="ng-binding" href="/login">Log In</a>
Ember以相同的方式處理這個問題。一個鏈接在Ember模板中被這樣定義:
{{#link-to sessions.new}}Log In{{/link-to}}
當它被渲染到DOM中時,它變成這樣:
<a id="ember-563" class="ember-view" href="/sessions/new">Log In</a>
Ember和Angular之后會攔截鏈接的點擊事件,這樣就可以不通過重載頁面來渲染新的內容。至關重要的是,如果點擊事件永遠不被觸發并且瀏覽器已經加載了href的值,那么對于用戶來說點擊鏈接只會帶來一次額外的頁面重載,看起來并不會有什么不同,因為Ember和Angular默認情況下不會嘗試依照URL定義他們自己的路由來重新制造輪子。
然而,在當前的形式下,Ember和Angular仍然需要加載JavaScript來渲染他們的模板并且在第一時間創建那些連接。每500個訪問使用Angular或Ember構建的網站中4人將會遭遇一次徹底白屏。
是否有一個解決方案?
如果在服務端渲染動態Web頁面的內容,那么渲染功能的代碼只需支持在服務器端運行。但Web頁面放在客戶端進行渲染時,相關代碼需要支持每一臺可能訪問網站的客戶端。開發者現在正逐步拋棄服務端渲染的做法,因為他們不能提供客戶端渲染所帶來的富應用體驗。但是我認為在客戶端應用的新世界中,服務端渲染尚有一席之地。
目前,開發者使用單頁面Web應用框架需要針對加載JavaScript作出一個權衡。但在我看來,這些正是框架應該去解決的問題。作為Web開發者,我們有幸使用有史以來最通用的編程語言之一為Web編寫應用代碼。如果框架開發者能夠夜以繼日(不可否認任務非常艱辛)地使應用像在瀏覽器中一樣地運行在Node中,服務器就可以完成初始頁面渲染的任務,隨后所有的任務由瀏覽器負責處理。當然,如果服務器可以將鏈接渲染成a標簽的形式,就像Ember目前在客戶端上實現的那樣,那么就可以允許沒有收到JavaScript的用戶(無論出于什么樣的原因)正常瀏覽網站。同樣也可以通過在服務器(而不是在客戶端)上運行所有的驗證和子任務邏輯,使表單正常工作。如果框架維護者一開始就朝著這個方向努力,那么每一個使用該框架的開發者都可以立即將一個只能工作在最新Web瀏覽器中的應用轉換為一種漸進增強的體驗,這樣做幾乎可以兼容任何Web客戶端——過去的、現在的、以及未來的。
漸進增強對于Web開發者來說早已是重要的一環,它使我們意識到對于Web體驗來說內容是至關重要的一部分,任何針對用戶體驗的額外改進不應當破壞任何一個客戶端訪問Web頁面所包含的內容。目前創建單頁面應用的方法傾向于放棄這條準則,然而漸進增強和單頁面應用從本質上來講其實可以相互兼容。
事實上,這個領域已經有了不小的進步,例如,一個Ember內部的團隊正在通過實現服務端渲染來改進Ember與搜索引擎的兼容性。但是由單頁面Web應用引發的問題的解決方案并不能只依賴純技術角度:人們看待Web的方式已成為一個日益嚴重的問題。將Web視為另一個應用平臺的做法已司空見慣,但是Web所能做的比這多得多。無論訪問者通過2000美元的iMac還是50美元的安卓平板,甚至在我們無法想象的未來,花費5美元就可以購買的Web客戶端來訪問,Web始終是一個通用信息平臺。事實上,不犧牲小部分用戶的體驗對我們來說非常重要,如此一來我們可以在這個過程中稍微改進一下其余正在破壞Web普適性的體驗。
作者:Ross Penman是一位來自蘇格蘭的web開發者和狂熱的技術專家。2014年度新型人才網絡獎決賽入圍選手。Ross經常慶祝他的工作來促進科技領域的年輕人。他的Twitter內容與web開發和口袋怪獸訓練有關。(譯者:劉振濤,審校:陳秋歌)
*請認真填寫需求信息,我們會在24小時內與您取得聯系。