整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          「JavaScript基礎」一份詳盡的 async/await 使用指南

          家好,前幾篇文章我們一起學習了「JavaScript基礎」Promise使用指南, 明白了ES6增加的新特性——Promise讓我們能夠更加優雅的書寫回調函數,清楚了Promise有哪些狀態,以及如何編寫Promise的相關代碼。本篇文章,小編將和大家一起學習異步編程的未來——async/await,它會打破你對上篇文章Promise的認知,竟然異步代碼還能這么寫! 但是別太得意,你需要深入理解Promise后,才能更好的的駕馭async/await,因為async/await是基于Promise的,沒有理解Promise,小編強烈建議各位再看看「JavaScript基礎」Promise使用指南。如果你一旦掌握了如何使用async/await,就沒必要使用Promise了(除非你需要將回調類型的API轉換為async/await,你需要使用到Promise)。

          關于async / await

          1. 用于編寫異步程序
          2. 代碼書寫方式和同步編碼十分相似,因此代碼十分簡潔易讀
          3. 基于Promise
          4. 您可以使用try和catch常規的方法捕獲異常
          5. ES8中引入了async/await,目前幾乎所有瀏覽器都已支持這個特性(除了IE和Opera不支持)
          6. 你可以輕松設置斷點,調試更容易。

          從async開始學起

          讓我們從async關鍵字開始吧,這個關鍵詞可以放在函數之前,如下所示:

          async function f() { 
           return 1; 
          }
          

          在函數之間加上async意味著:函數將返回一個Promise,雖然你的代碼里沒有顯示的生命返回一個Promise,但是編譯器會自動將其轉換成一個Promise中,不信你可以使用Promisethen語法試試:

          async function f() { 
           return 1; 
          } 
          f().then(alert); // 1
          

          …如果你不放心的話,你可以再代碼里明確返回一個Promise,輸出結果是相同的。

          async function f() { 
           return Promise.resolve(1); 
          } 
          f().then(alert); // 1
          

          很簡單吧,小編之所以說 async/await 是基于Promise是沒毛病的,async確保函數返回一個Promise,很簡單吧,不僅如此,還有一個關鍵字await,await只能在async中運行。

          等待——await

          await的基本語法:

          let value=await promise;
          

          該關鍵字的await的意思就是讓JS編譯器等待Promise并返回結果。接下來我們看一段簡單的示例:

          async function f() { 
           let promise = new Promise((resolve, reject) => { 
           setTimeout(() => resolve("done!"), 1000) 
           }); 
           let result = await promise; // wait till the promise resolves (*) 
           alert(result); // "done!" 
          } 
          f();
          

          函數執行將會在 let result = await promise 這一行暫停,直到Promise返回結果,因此上述代碼將會1秒后,在瀏覽器彈出“done”的提示框。

          小編在此強調下:

          • await的字面意思就是讓JavaScript等到Promise結束,然后輸出結果。這里并不會占用CPU資源,因為引擎可以同時執行其他任務:其他腳本或處理事件。
          • 不能單獨使用await,必須在async函數作用域下使用,否則將會報出異?!癊rror: await is only valid in async function”,比如以下代碼:
          function f() { 
           let promise = Promise.resolve(1); 
           let result = await promise; // Syntax error 
          }
          

          接下來,小編將和大家一起來親自動手實踐以下內容:

          • async與Promise.then的結合使用,依次處理多個執行結果
          • 使用await替代Promise.then,依次處理多個執行結果
          • 同時等待多個執行結果
          • 使用Promise.all收集多個結果
          • 使用try-catch捕獲異常
          • 如何處理Promise.all中拋出的錯誤
          • 使用finally確保函數執行

          一起動手之前,確保你安裝了Node,NPM相關工具,谷歌瀏覽器,為了預覽代碼效果,小編使用 npm install http-server -g 命令快速部署了web服務環境,方便我們運行代碼。接下來,我們寫一個火箭發射場景的小例子。

          async與Promise.then的結合使用,依次處理多個執行結果

          • 通過控制臺命令切換至工作區
          • 創建一個async-function-Promise-chain的文件夾
          • 在main.js中用創建第一個返回隨機函數的async函數getRandomNumber:
          async function getRandomNumber() { 
           console.log('Getting random number.'); 
           return Math.random(); 
          }
          
          • 在創建一個async函數determinReadyToLaunch:如果傳入參數大于0.5將返回True
          async function deteremineReadyToLaunch(percentage) { 
           console.log('Determining Ready to launch.'); 
           return percentage>0.5; 
          }
          
          • 創建第三個async函數reportResults,如果傳入參數為True將進入倒計時發射
          async function reportResults(isReadyToLaunch) { 
           if (isReadyToLaunch) { 
           console.log('Rocket ready to launch. Initiate countdown: '); 
           } else { 
           console.error('Rocket not ready. Abort mission: '); 
           } 
          }
          
          • 創建一個main函數,調用getRandomNumber函數,并且通過Promise.then方法相機調用determineReadyToLaunch和reportResults函數
          export function main() { 
           console.log('Before Promise created'); 
           getRandomNumber() 
           .then(deteremineReadyToLaunch) 
           .then(reportResults) 
           console.log('After Promise created'); 
          }
          
          • 新建一個html文件引入main.js
          <html> 
          <script type="module"> 
           import {main} from './main10.js'; 
           main(); 
          </script> 
          <body> 
          </body> 
          </html>
          
          • 在工作區域運行 http-server 命令,你將會看到如下輸出

          使用await替代Promise.then,依次處理多個執行結果

          上一節,我們使用Promise.then依次處理了多個執行結果,本小節,小編將使用await實現同樣的功能,具體操作如下:

          • 通過控制臺命令切換至工作區
          • 創建一個async-function-Promise-chain的文件夾
          • 在main.js中用創建第一個返回隨機函數的async函數getRandomNumber:
          async function getRandomNumber() { 
           console.log('Getting random number.'); 
           return Math.random(); 
          }
          
          • 在創建一個async函數determinReadyToLaunch:如果傳入參數大于0.5將返回True
          async function deteremineReadyToLaunch(percentage) { 
           console.log('Determining Ready to launch.'); 
           return percentage>0.5; 
          }
          
          • 創建第三個async函數reportResults,如果傳入參數為True將進入倒計時發射
          async function reportResults(isReadyToLaunch) { 
           if (isReadyToLaunch) { 
           console.log('Rocket ready to launch. Initiate countdown: '); 
           } else { 
           console.error('Rocket not ready. Abort mission: '); 
           } 
          }
          
          • 創建一個main函數,調用getRandomNumber函數,并且通過Promise.then方法相機調用determineReadyToLaunch和reportResults函數
          export async function main() { 
           const randomNumber = await getRandomNumber(); 
           const ready = await deteremineReadyToLaunch(randomNumber); 
           await reportResults(ready); 
          }
          
          • 在工作區域運行 http-server 命令,你將會看到如下輸出

          同時等待多個執行結果

          有時候我們需要同時啟動多個異步,無需依次等待結果消耗時間,接下來的例子可以使用await 同時啟動和等待多個結果。

          • 通過控制臺命令切換至工作區
          • 創建一個await-concurrently的文件夾
          • 創建三個函數功能checkEngines,checkFlightPlan,和checkNavigationSystem用來記錄信息時,這三個函數都返回一個Promise,示例代碼如下:
          function checkEngines() { 
           console.log('checking engine'); 
           return new Promise(function (resolve) { 
           setTimeout(function() { 
           console.log('engine check completed'); 
           resolve(Math.random() < 0.9) 
           }, 250) 
           }); 
          } 
          function checkFlightPlan() { 
           console.log('checking flight plan'); 
           return new Promise(function (resolve) { 
           setTimeout(function() { 
           console.log('flight plan check completed'); 
           resolve(Math.random() < 0.9) 
           }, 350) 
           }); 
          } 
          function checkNavigationSystem() { 
           console.log('checking navigation system'); 
           return new Promise(function (resolve) { 
           setTimeout(function() { 
           console.log('navigation system check completed'); 
           resolve(Math.random() < 0.9) 
           }, 450) 
           }); 
          }
          
          • 創建一個async 的main函數調用上一步創建函數。將每個返回的值分配給局部變量。然后等待Promise的結果,并輸出結果:
           export async function main() { 
           const enginePromise = checkEngines(); 
           const flighPlanPromise = checkFlightPlan(); 
           const navSystemPromise = checkNavigationSystem(); 
           const enginesOk = await enginePromise; 
           const flighPlanOk = await flighPlanPromise; 
           const navigationOk = await navSystemPromise; 
           if (enginesOk && flighPlanOk && navigationOk) { 
           console.log('All systems go, ready to launch: '); 
           } else { 
           console.error('Abort the launch: '); 
           if (!enginesOk) { 
           console.error('engines not ready'); 
           } 
           if (flighPlanOk) { 
           console.error('error found in flight plan'); 
           } 
           if (navigationOk) { 
           console.error('error found in navigation systems'); 
           } 
           } 
          }
          
          • 在工作區域運行 http-server 命令,你將會看到如下輸出

          使用Promise.all收集多個結果

          在上一小節中,我們一起學習了如何觸發多個異步并等待多個異步結果。上一節我們只使用了asyc/ await,本節小編和大家一起使用Promise.all來收集多個異步的結果,在某些情況下,盡量使用Promise相關的API,具體的代碼如下:

          • 通過控制臺命令切換至工作區
          • 創建一個Promise-all-collect-concurrently的文件夾

          創建三個函數功能checkEngines,checkFlightPlan,和checkNavigationSystem用來記錄信息時,這三個函數都返回一個Promise,示例代碼如下:

          function checkEngines() { 
           console.log('checking engine'); 
           return new Promise(function (resolve) { 
           setTimeout(function() { 
           console.log('engine check completed'); 
           resolve(Math.random() < 0.9) 
           }, 250) 
           }); 
          } 
          function checkFlightPlan() { 
           console.log('checking flight plan'); 
           return new Promise(function (resolve) { 
           setTimeout(function() { 
           console.log('flight plan check completed'); 
           resolve(Math.random() < 0.9) 
           }, 350) 
           }); 
          } 
          function checkNavigationSystem() { 
           console.log('checking navigation system'); 
           return new Promise(function (resolve) { 
           setTimeout(function() { 
           console.log('navigation system check completed'); 
           resolve(Math.random() < 0.9) 
           }, 450) 
           }); 
          }
          
          • 創建一個async 的main函數調用上一步創建函數。使用Promise.all收集多個結果,將結果返回給變量,代碼實現如下:
          export async function main() { 
           const prelaunchChecks = [ 
           checkEngines(), 
           checkFlightPlan(), 
           checkNavigationSystem() 
           ]; 
           const checkResults = await Promise.all(prelaunchChecks); 
           const readyToLaunch = checkResults.reduce((acc, curr) => acc && 
           curr); 
           if (readyToLaunch) { 
           console.log('All systems go, ready to launch: '); 
           } else { 
           console.error('Something went wrong, abort the launch: '); 
           } } 
          }
          
          • 在工作區域運行 http-server 命令,你將會看到如下輸出

          Promise.all接收多個promise的數組,并整體返回一個Promise,如果和上一小節的代碼進行比較,代碼量少了不少,但是也有個問題,不能返回是哪一步失敗。

          使用try-catch捕獲異常

          并非所有的async都能成功返回,我們需要能夠處理程序的異常,在本小節中,你將會看到如何使用try-catch捕獲async函數引發的錯誤,具體操作的流程如下:

          • 通過控制臺命令切換至工作區
          • 創建一個async-errors-try-catch的文件夾
          • 創建一個拋出錯誤的async函數addBoosters
          async function addBoosters() { 
           throw new Error('Unable to add Boosters'); 
          }
          
          • 創建一個async函數,performGuidanceDiagnostic它也會拋出一個錯誤:
          async function performGuidanceDiagnostic (rocket) { 
           throw new Error('Unable to finish guidance diagnostic')); 
          }
          
          • 創建一個async的main函數調用函數addBosters與performGuidanceDiagnostic ,使用try-catch處理錯誤:
           export async function main() { 
           console.log('Before Check'); 
           try { 
           await addBosters(); 
           await performGuidanceDiagnostic(); 
           } catch (e) { 
           console.error(e); 
           } 
          } 
          console.log('After Check');
          
          • 在工作區域運行 http-server 命令,你將會看到如下輸出

          從輸出看出,我們使用我們熟悉的try-catch捕獲到了異常,如果第一個發生異常,第二個就不會執行,同時將會記錄到發生的異常,并輸出到控制臺,在下一小節,我們一起將學習到如何使用try-catch捕獲同時運行多個異步操作的異常。

          如何處理Promise.all中拋出的錯誤

          在上面的小節中,我們使用了Promise.all來收集多個異步的執行結果。在收集錯誤狀態,Promise.all更有趣。通常,我們在處理多個錯誤時,同時顯示多個錯誤信息,我們必須編寫相關的業務邏輯。但是,在這小節,你將會使用Promise.all和try-catch捕獲異常,無需編寫復雜的布爾邏輯處理業務,具體如何實現示例如下:

          • 通過控制臺命令切換至工作區
          • 創建一個Promise-all-collect-concurrently的文件夾
          • 創建三個async功能checkEngines,checkFlightPlan以及checkNavigationSystem函數用來記錄信息時,返回Promise,一個成功的值的信息和一個失敗值的信息:
          function checkEngines() { 
           console.log('checking engine'); 
           return new Promise(function (resolve) { 
           setTimeout(function() { 
           console.log('engine check completed'); 
           resolve(Math.random() < 0.9) 
           }, 250) 
           }); 
          } 
          function checkFlightPlan() { 
           console.log('checking flight plan'); 
           return new Promise(function (resolve) { 
           setTimeout(function() { 
           console.log('flight plan check completed'); 
           resolve(Math.random() < 0.9) 
           }, 350) 
           }); 
          } 
          function checkNavigationSystem() { 
           console.log('checking navigation system'); 
           return new Promise(function (resolve) { 
           setTimeout(function() { 
           console.log('navigation system check completed'); 
           resolve(Math.random() < 0.9) 
           }, 450) 
           }); 
          }
          

          創建一個async的main函數調用每個在上一步中創建的功能函數。等待結果,捕獲并記錄引發的任何錯誤。如果沒有拋出錯誤,則記錄成功:

           export async function main() { 
           try { 
           const prelaunchChecks = [ 
           checkEngines, 
           checkFlightPlan, 
           checkNavigationSystem 
           ]; 
           await Promise.all(prelauchCheck.map((check) => check()); 
           console.log('All systems go, ready to launch: '); 
           } catch (e) { 
           console.error('Aborting launch: '); 
           console.error(e); 
           } 
           } 
          }
          
          • 在工作區域運行 http-server 命令,你將會看到如下輸出

          Promise.all返回一個Promise,當await在錯誤狀態下,會拋出異常。三個異步promise同時執行,如果其中一個或多個錯誤得到滿足,則會拋出一個或多個錯誤;

          你會發現只有一個錯誤會被記錄下來,與同步代碼一樣,我們的代碼可能會拋出多個異常,但只有一會被catch塊捕獲并記錄。

          使用finally確保函數執行

          錯誤處理可能會變得相當復雜。有些情況,其中您希望錯誤繼續冒泡調用堆棧以便執行其它更高級別處理。在這些情況下,您可能還需要執行一些清理任務。本小節,你將了解如何使用finally以確保執行某些代碼,而不管錯誤狀態如何,具體如何實現示例如下:

          • 通過控制臺命令切換至工作區
          • 創建一個Promise-all-collect-concurrently的文件夾
          • 創建三個async功能checkEngines,checkFlightPlan以及checkNavigationSystem函數用來記錄信息時,返回Promise,一個成功的值的信息和一個失敗值的信息:
          function checkEngines() { 
           console.log('checking engine'); 
           return new Promise(function (resolve, reject) { 
           setTimeout(function () { 
           if (Math.random() > 0.5) { 
           reject(new Error('Engine check failed')); 
           } else { 
           console.log('Engine check completed'); 
           resolve(); 
           } 
           }, 250) 
           }); 
          } 
          function checkFlightPlan() { 
           console.log('checking flight plan'); 
           return new Promise(function (resolve, reject) { 
           setTimeout(function () { 
           if (Math.random() > 0.5) { 
           reject(new Error('Flight plan check failed')); 
           } else { 
           console.log('Flight plan check completed'); 
           resolve(); 
           } 
           }, 350) 
           }); 
          } 
          function checkNavigationSystem() { 
           console.log('checking navigation system'); 
           return new Promise(function (resolve, reject) { 
           setTimeout(function () { 
           if (Math.random() > 0.5) { 
           reject(new Error('Navigation system check failed')); 
           } else { 
           console.log('Navigation system check completed'); 
           resolve(); 
           } 
           }, 450) 
           }); 
          }
          
          • 創建一個asyncperformCheck函數,調用上一步中創建的每個函數。等待結果,并用于finally記錄完整的消息:
          async function performChecks() { 
           console.log('Starting Pre-Launch Checks'); 
           try { 
           const prelaunchChecks = [ 
           checkEngines, 
           checkFlightPlan, 
           checkNavigationSystem 
           ]; 
           return Promise.all(prelauchCheck.map((check) => check()); 
           } finally { 
           console.log('Completed Pre-Launch Checks'); 
           } 
           }
          
          • 創建一個async的main函數調該函數performChecks。等待結果,捕獲并記錄引發的錯誤。
          export async function main() { 
           try { 
           await performChecks(); 
           console.log('All systems go, ready to launch: '); 
           } catch (e) { 
           console.error('Aborting launch: '); 
           console.error(e); 
           } 
          }
          
          • 在工作區域運行 http-server 命令,你將會看到如下輸出

          與上一小節一樣,錯誤在main函數中進行捕獲,由于finally的存在,讓我清楚的知道performChecks確保執行輸出已完成。你可以設想,處理錯誤是一個重要的任務,并且async/await允許我們使用try/catch的相同方式處理異步和同步代碼的錯誤,大大簡化了我們處理錯誤的工作量,讓代碼更加簡潔。

          用async/await改寫上篇文章Promise的例子

          上篇文章「JavaScript基礎」Promise使用指南的最后,我們使用Promise的方法改寫了基于回調的例子,本文的最后,我們將用今天學到的內容 async/await改寫這個例子, 如何實現呢,代碼如下:

          const fs = require('fs'); 
          const path = require('path'); 
          const postsUrl = path.join(__dirname, 'db/posts.json'); 
          const commentsUrl = path.join(__dirname, 'db/comments.json'); 
          //return the data from our file 
          function loadCollection(url) { 
           return new Promise(function(resolve, reject) { 
           fs.readFile(url, 'utf8', function(error, data) { 
           if (error) { 
           reject('error'); 
           } else { 
           resolve(JSON.parse(data)); 
           } 
           }); 
           }); 
          } 
          //return an object by id 
          function getRecord(collection, id) { 
           return new Promise(function(resolve, reject) { 
           const data = collection.find(function(element){ 
           return element.id == id; 
           }); 
           resolve(data); 
           }); 
          } 
          //return an array of comments for a post 
          function getCommentsByPost(comments, postId) { 
           return comments.filter(function(comment){ 
           return comment.postId == postId; 
           }); 
          } 
          async function getPost(){ 
           try { 
           const posts = await loadCollection(postsUrl); 
           const post = await getRecord(posts, "001"); 
           const comments = await loadCollection(commentsUrl); 
           const postComments = await getCommentsByPost(comments, post.id); 
           console.log(post); 
           console.log(postComments); 
           } catch (error) { 
           console.log(error); 
           } 
          } 
          getPost();
          

          和Promise的方式相比,async/await 的實現方式是不是更直觀更容易理解呢,讓我幾乎能用同步的方式編寫異步代碼。

          結束語

          本節內容就介紹到這里,我們學會了如何使用 async/await 的使用,并且學會了如何與Promise相關API進行結合,async/await 讓我們以同步的方式更容易的編寫異步代碼,大大降低了編寫異步函數的難度。

          更多精彩內容,請微信關注”前端達人”公眾號!

          一部分:HTML簡介

          什么是HTML?

          HTML代表超文本標記語言(Hypertext Markup Language)。它是一種用于構建網頁的標記語言。HTML文件包含一組標簽,這些標簽用于定義網頁的結構和內容。瀏覽器讀取HTML文件,并根據標記中的指示呈現網頁內容。

          HTML的主要作用是定義文本內容、圖像、鏈接和其他媒體的排列方式,并提供交互元素,例如表單和按鈕。

          HTML的基本結構

          每個HTML文檔都應該遵循以下基本結構:

          <!DOCTYPE html>
          <html>
          <head>
              <title>網頁標題</title>
          </head>
          <body>
              <!-- 內容在這里 -->
          </body>
          </html>
          

          讓我們逐步解釋這個結構:

          • <!DOCTYPE html>:這是文檔類型聲明,它告訴瀏覽器正在使用的HTML版本。<!DOCTYPE html>表示使用HTML5。
          • <html>:HTML文檔的根元素。所有其他元素都包含在<html>標簽內。
          • <head>:包含與文檔相關的元信息,如頁面標題、字符集聲明和外部樣式表鏈接。
          • <title>:定義網頁的標題,顯示在瀏覽器標簽頁上。
          • <body>:包含網頁的主要內容,如文本、圖像和其他媒體。

          HTML標簽和元素

          HTML標簽是由尖括號括起來的名稱,例如<p>表示段落,<img>表示圖像。標簽通常成對出現,有一個開始標簽和一個結束標簽。例如:

          <p>這是一個段落。</p>
          

          <p>是開始標簽,</p>是結束標簽,文本位于兩個標簽之間。標簽定義了元素的類型和結構。

          有些HTML標簽是自封閉的,不需要結束標簽,例如<img>用于插入圖像。

          HTML注釋

          在HTML中,你可以使用注釋來添加說明性文字,注釋不會在瀏覽器中顯示。HTML注釋使用<!--開頭和-->結尾,如下所示:

          <!-- 這是一個注釋 -->
          

          注釋通常用于添加文檔說明、調試代碼或標記未來的修改。

          第二部分:HTML基本元素

          文本

          HTML中的文本通常包含在段落、標題、列表等元素中。以下是一些常見的文本元素:

          • <p>:定義一個段落。
          • <h1><h6>:定義標題,<h1>是最高級別的標題,<h6>是最低級別的標題。
          • <strong>:定義強調文本,通常以粗體顯示。
          • <em>:定義強調文本,通常以斜體顯示。
          • <a>:定義超鏈接,允許用戶點擊跳轉到其他頁面。

          示例:

          <p>這是一個段落。</p>
          <h1>這是一個標題</h1>
          <p><strong>這是強調文本。</strong></p>
          <p><em>這是斜體文本。</em></p>
          <p>訪問<a href="https://www.example.com">示例網站</a></p>
          

          圖像

          要在網頁中插入圖像,可以使用<img>標簽。它是一個自封閉標簽,需要指定圖像的src屬性來指定圖像文件的路徑。

          示例:

          htmlCopy code
          <img src="image.jpg" alt="圖像描述">
          
          • src:指定圖像文件的路徑。
          • alt:提供圖像的替代文本,用于無法加載圖像時的文字描述。

          鏈接

          通過使用<a>標簽,可以在網頁中創建鏈接。鏈接通常包含在文本或圖像中,并使用href屬性指定目標URL。

          示例:

          <a href="https://www.example.com">訪問示例網站</a>
          
          • href:指定鏈接的目標URL。

          列表

          HTML支持有序列表(<ol>)、無序列表(<ul>)和定義列表(<dl>)。

          無序列表

          無序列表使用<ul>標簽定義,每個列表項使用<li>標簽。

          示例:

          <ul>
              <li>項目1</li>
              <li>項目2</li>
              <li>項目3</li>
          </ul>
          

          有序列表

          有序列表使用<ol>標簽定義,每個列表項使用<li>標簽。

          示例:

          <ol>
              <li>第一項</li>
              <li>第二項</li>
              <li>第三項</li>
          </ol>
          

          定義列表

          定義列表使用<dl>標簽定義,每個定義項目使用<dt>標簽定義術語,使用<dd>標簽定義描述。

          示例:

          <dl>
              <dt>術語1</dt>
              <dd>描述1</dd>
              <dt>術語2</dt>
              <dd>描述2</dd>
          </dl>
          

          第三部分:HTML表單

          HTML表單允許用戶與網頁進行交互,提交數據。以下是HTML表單的基本元素:

          <form>元素

          <form>元素用于創建表單,可以包含文本字段、復選框、單選按鈕、下拉列表等。

          示例:

          <form action="submit.php" method="post">
              <!-- 表單元素在這里 -->
          </form>
          
          • action:指定表單數據提交的目標URL。
          • method:指定提交方法,通常是"post"或"get"。

          輸入字段

          輸入字段用于接收用戶輸入的數據,常見的輸入字段類型包括文本框、密碼框、單選按鈕、復選框等。

          文本框

          文本框使用<input>標簽,type屬性設置為"text"。

          示例:

          <input type="text" name="username" placeholder="用戶名">
          
          • type:指定字段類型。
          • name:指定字段的名稱。
          • placeholder:設置文本框的占位符文本。

          密碼框

          密碼框使用<input>標簽,type屬性設置為"password"。

          示例:

          htmlCopy code
          <input type="password" name="password" placeholder="密碼">
          

          單選按鈕

          單選按鈕使用<input>標簽,type屬性設置為"radio"。

          示例:

          <input type="radio" name="gender" value="male">男
          <input type="radio" name="gender" value="female">女
          
          • name:指定單選按鈕組的名稱。
          • value:指定每個選項的值。

          復選框

          復選框使用<input>標簽,type屬性設置為"checkbox"。

          示例:

          <input type="checkbox" name="subscribe" value="yes">訂閱新聞
          

          下拉列表

          下拉列表使用<select><option>標簽創建。<select>定義下拉列表,而<option>定義選項。

          示例:

          <select name="country">
              <option value="us">美國</option>
              <option value="ca">加拿大</option>
              <option value="uk">英國</option>
          </select>
          
          • name:指定下拉列表的名稱。
          • 每個<option>標簽表示一個選項,使用value屬性定義選項的值。

          第四部分:HTML樣式和CSS

          HTML用于定義網頁的結構和內容,但要使網頁看起來更吸引人,需要使用CSS(層疊樣式表)。CSS允許你定義字體、顏色、布局等樣式。

          內聯樣式

          可以在HTML元素內部使用style屬性來定義內聯樣式。

          示例:

          <p style="color: blue; font-size: 16px;">這是一個藍色的段落。</p>
          

          外部樣式表

          外部樣式表將樣式規則保存在獨立的CSS文件中,并通過<link>標簽將其鏈接到HTML文檔。

          示例(style.css):

          /* style.css */
          p {
              color: blue;
              font-size: 16px;
          }
          

          在HTML中鏈接外部樣式表:

          <link rel="stylesheet" type="text/css" href="style.css">
          

          這使得可以在整個網站上共享相同的樣式。

          總結

          HTML是構建現代網頁的基礎。通過學習HTML的基本語法和元素,你可以創建吸引人且功能強大的網頁。無論是文本、圖像、鏈接還是表單,HTML提供了豐富的工具來呈現內容和實現用戶交互。

          這篇文章提供了HTML的基礎知識,但HTML是一個廣泛的主題,還有許多高級特性和技巧等待你探索。希望這篇文章對你入門HTML有所幫助,讓你能夠開始創建自己的網頁。繼續學習和實踐,你將成為一個熟練的網頁開發者。

          我們開發系統的時候,可能會接到這樣的需求:不要讓用戶復制頁面上的文字或者圖片,不要讓用戶調試我們的頁面,更甚至也不要讓用戶進行打印操作等等。

          聽起來是不是讓人很頭大,這咋實現啊?這有必要嗎?這能禁住么?

          如果你沒做過這些,或者沒接到過這樣的需求,那你也應該看到過某個網站做了一些這樣的措施。

          既然要做,我們就得想方案,先來看看禁止復制都有哪些方法。

          禁止復制

          假設我們有這樣一段代碼:

          <div style="padding-left: 56px;">
            <textarea rows="5" cols="33"></textarea>
          </div>
          <pre>
            海客談瀛洲,煙濤微茫信難求,
          
            越人語天姥,云霞明滅或可睹。
          
            天姥連天向天橫,勢拔五岳掩赤城。
          
            天臺四萬八千丈,對此欲倒東南傾。
          
            我欲因之夢吳越,一夜飛度鏡湖月。
          
            湖月照我影,送我至剡溪。
            </pre>

          接下來就通過這個例子來論述我們的方案:

          x效果

          1. 通過user-select:none

          這是一個css屬性,標識了元素及其子元素的文本不可被選中,因此設定之后,文本將不能夠被選中,因此也就不能復制:

          <pre style="user-select: none;">
            ??驼勫蓿瑹煗⒚P烹y求,
          
            越人語天姥,云霞明滅或可睹。
          
            天姥連天向天橫,勢拔五岳掩赤城。
          
            天臺四萬八千丈,對此欲倒東南傾。
          
            我欲因之夢吳越,一夜飛度鏡湖月。
          
            湖月照我影,送我至剡溪。
            </pre>

          我們在這段文本上,加上這個樣式。

          效果

          可以看到,文字壓根就不能選擇,從鼠標形狀也能看出來。

          1. 通過攔截copy操作

          由于用在進行復制操作的時候,會觸發copy事件,我們可以通過監聽它來做一些處理,使得復制的行為發生改變:

          <div>
            <div style="padding-left: 56px;">
              <textarea rows="5" cols="33"></textarea>
            </div>
            <pre id="content">
              海客談瀛洲,煙濤微茫信難求,
          
              越人語天姥,云霞明滅或可睹。
          
              天姥連天向天橫,勢拔五岳掩赤城。
          
              天臺四萬八千丈,對此欲倒東南傾。
          
              我欲因之夢吳越,一夜飛度鏡湖月。
          
              湖月照我影,送我至剡溪。
            </pre>
          </div>
          <script>
            let c = document.getElementById('content')
            c.removeEventListener("copy", copyFilter)
            c.addEventListener("copy", copyFilter)
            function copyFilter(e) {
              let cp = e.clipboardData || window.clipboardData
              if(!cp) {
                return
              }
              let text = window.getSelection().toString()
              if(text) {
                e.preventDefault()
                cp.setData("text/plain", "你復制了一段魔法")
              }
            }
          </script>

          先獲取到我們要禁止復制的元素,然后給它添加一個copy的事件監聽,在添加監聽之前,要先移除一下,這樣是為了避免局部刷新的時候重復添加,然后我們通過copyFilter函數來對這次操作進行處理。

          先獲取剪貼板對象,如果當前事件對象里不存在,那就從window里面取,然后我們通過getSelection再拿到選取的內容,因為我們對剪貼板對象進行修改,所以要阻止默認行為,然后把剪貼板的內容重新賦值,可以是示例中那樣的一段文字,也可以設置為空,甚至是任意其他內容,然后我們就可以看到產生的效果了:

          效果

          雖然能復制文本,但是由于我們攔截了復制操作,更改了它的行為,因此再粘貼的時候就變成了我們更改的樣子,也做到了禁止復制的功能。

          這種方式對于使用快捷鍵或者右鍵的方式都是有效的。

          1. 通過攔截cut操作

          這種情況主要是在可編輯區域,比如文本框、文本域、設置為contenteditable的元素等,用戶可以對文字進行剪切操作,雖然上面禁止了復制,但是剪切是另一個操作,不攔截的話還是相當于能復制出來。

          copy和cut只是觸發的事件不同而已,但是它們都是執行相同的邏輯處理:

          <div>
            <div style="padding-left: 56px;">
              <textarea rows="5" cols="33"></textarea>
            </div>
            <pre id="content" contenteditable>
              ??驼勫?,煙濤微茫信難求,
          
              越人語天姥,云霞明滅或可睹。
          
              天姥連天向天橫,勢拔五岳掩赤城。
          
              天臺四萬八千丈,對此欲倒東南傾。
          
              我欲因之夢吳越,一夜飛度鏡湖月。
          
              湖月照我影,送我至剡溪。
            </pre>
          </div>
          <script>
            let c = document.getElementById('content')
            c.removeEventListener("cut", copyFilter)
            c.addEventListener("cut", copyFilter)
            function copyFilter(e) {
              let cp = e.clipboardData || window.clipboardData
              if(!cp) {
                return
              }
              let text = window.getSelection().toString()
              if(text) {
                e.preventDefault()
                cp.setData("text/plain", "你復制了一段魔法")
              }
            }
          </script>

          這里我為了方便,給元素添加了contenteditable屬性,讓它變成可編輯的,copyFilter函數沒有變化,我們只是添加了一個剪切事件的監聽,然后它們的處理函數都是copyFilter??聪滦Ч?/p>

          效果

          可以看到,首先我們對文字進行剪切,沒有出現預期的效果,這時因為我們在代碼里面對剪切進行了攔截,并阻止了它的默認行為,然后我們在粘貼的時候,文字也改變成我們設置的了。

          1. 通過媒體查詢控制打印

          雖然我們可以通過上面的幾種方法禁止在頁面上復制,但是用戶也可能開啟打印預覽模式,在這種情況下,也是可以進行復制的,我們要想對打印頁面進行一些控制,那么就要用到媒體查詢,先看下打印的樣子:

          效果

          雖然我們做了限制,但是在打印頁面沒有生效,現在我們針對這個場景更改一下代碼:

          @media print {
            html {
              display: none;
            }
          }

          通過添加上面這個樣式規則,我們能夠使頁面在打印的時候,內容隱藏起來,這樣就無法進行復制了:

          效果

          能夠看到,點擊打印的時候,預覽頁面一片空白,,這樣就禁止了在打印頁面進行復制的操作。當然了,你其實也可以設置其他的樣式屬性來做些控制,但要記住寫在打印的媒體查詢里面,只有這樣才會在打印頁面生效。

          1. 通過偽元素覆蓋內容

          還有一種方式就是,通過設定一個偽元素,讓它全面覆蓋文本內容,這樣鼠標就不能選到實際的文本,改造一下代碼:

          .content {
            position: relative;
          }
          .content::before {
            content: '';
            position: absolute;
            left: 0px;
            top: 0px;
            width: 100%;
            height: 100%;
          }
          <pre id="content" class="content">
            海客談瀛洲,煙濤微茫信難求,
          
            越人語天姥,云霞明滅或可睹。
          
            天姥連天向天橫,勢拔五岳掩赤城。
          
            天臺四萬八千丈,對此欲倒東南傾。
          
            我欲因之夢吳越,一夜飛度鏡湖月。
          
            湖月照我影,送我至剡溪。
          </pre>

          現在就不能在元素上面選中文字了,不過用戶也可能還有一些操作,比如在內容區域外面ctrl+a全選,或者在外面拖動鼠標來全選,如果是這種情形,那么我們可以通過監聽鍵盤和鼠標事件來禁止全選等操作。

          1. 通過監聽鼠標和鍵盤事件

          由于用戶有很多種操作的方式,鍵盤全選、鼠標全選、鍵盤右鍵、鼠標右鍵等等,我們如果窮舉的話,情況太多了,因此我們只監聽鼠標按下和抬起事件,以及鍵盤的按下事件:

          document.removeEventListener("mousedown", haveSelect)
          document.addEventListener("mousedown", haveSelect)
          document.removeEventListener("mouseup", haveSelect)
          document.addEventListener("mouseup", haveSelect)
          document.removeEventListener("keydown", haveSelect)
          document.addEventListener("keydown", haveSelect)
          function haveSelect() {
            window.getSelection().removeAllRanges()
          }

          主要就是在removeAllRanges方法上面,能夠在觸發上面事件的時候,將所選區域清空,也就是不管你選沒選,咋選的,反正就是你只要進行了操作,那我就那可能選擇的區域給你清空,這樣你就啥也干不了了。

          效果

          好,這樣就可以啦,無論怎么選,即使出現了選區,但是只要你再按了鼠標或者鍵盤,那么選區就會直接消失,就能達到不能復制的效果,因為你發現啥都做不了。

          這里額外說明一下,對于事件的監聽,一定要用addEventListener來實現,因為它會將多個綁定的事件都添加上去,當觸發的時候就會按照綁定的順序進行執行,如果是用賦值的方式,那么后面的會覆蓋前面的,而且賦值的方式很容易被篡改,可以很輕松的讓你的綁定函數不能執行從而失效,而使用addEventListener就不會被人為覆蓋,只能通過綁定的函數句柄來手動移除,也就是說要移除的時候,必須使用跟綁定時使用同一個函數才行。

          通過CSS的方式禁止復制,可以很容易的被用戶解除,只能是設置的稍微復雜一點,增加難度。而通過JS的方式禁止復制,也可以通過禁用頁面JavaScript代碼來解除,因此我們可以將內容通過js來渲染,這樣如果頁面禁用了js,那么內容也不會渲染。

          禁止調試

          對于禁止調試,主要是指用戶打開控制臺,控制臺也就是開發者工具,我為了方便稱之為控制臺,想要對頁面進行調試時,我們做一些處理,阻止這種行為,最大可能的攔截通過控制臺對系統的調試。
          主要的方法有幾種,由于打開控制臺是瀏覽器提供的調試功能,因此我們沒法攔截打開操作,即使通過事件監聽不允許快捷鍵這樣做,但是也可以通過其他方式進行打開,因此我們的主要關注點就在于打開控制臺之后,我們能做哪些事情來限制用戶行為。

          1. 禁用快捷鍵

          打開控制臺的快捷鍵主要有F12和ctrl+shift+i,我們先把這倆給禁用了:

          document.removeEventListener("keydown", disableDevShortcut)
          document.addEventListener("keydown", disableDevShortcut)
          function disableDevShortcut(e) {
            console.log(e)
            if(e.keyCode === 123) {
              e.preventDefault()
            }else if(e.keyCode === 73 && e.ctrlKey && e.shiftKey) {
              e.preventDefault()
            }
          }

          這樣在使用這兩個快捷鍵的時候,頁面沒有任何反應,控制臺也不會喚起,因為我們阻止了它們的默認行為。

          1. 禁用右鍵的檢查功能

          除了通過快捷鍵,還可以使用右鍵的方式,并點擊檢查也會調出控制臺。

          效果

          這種情景,我們可以通過禁止在頁面上使用右鍵的方式,來阻止打開控制臺:

          document.removeEventListener("contextmenu", cancelContextmenu)
          document.addEventListener("contextmenu", cancelContextmenu)
          function cancelContextmenu(e) {
            e.preventDefault()
          }

          現在就不能通過右鍵打開控制臺了,但是相應的整個右鍵功能也都不能使用了。

          1. 設置無限斷點

          如果用戶最終打開了控制臺,比如通過在瀏覽器的更多功能中來打開的話,那么我就需要采取其他的措施,其中之一就是給代碼設置無限斷點,因為斷點只在控制臺打開的時候才會發生作用,從而不必擔心非調試模式下的程序正常運行。

          無限斷點的主要思路就是利用定時器等手段,頻繁的觸發斷點效果,使得不能輕松的調試程序,先看下代碼:

          ;(() => {
            function breakDebugger() {
              if(new checkDebugger().check) {
                breakDebugger()
              }
            }
            function checkDebugger() {
              const now = new Date();
              eval('(function () {debugger;false;})()')
              const dur = Date.now() - now
              if(dur < 5) {
                return {check: false}
              }else {
                return {check: true}
              }
            }
            setInterval(() => {
              eval('(function () {debugger;true;})()')
              breakDebugger()
            }, 500)
          })()

          我們利用一個立即執行的自執行函數,來使我們的代碼被封裝在一個固定塊內,不與其他部分有任何影響。

          這里主要做了兩步:

          第一步設置一個重復執行的定時器,其中包括了一個斷點和一個函數調用。

          第二步通過函數來遞歸調用斷點,主要使用了實例化對象的方式和時間差的判斷。

          這樣做的主要作用就是在設置無限斷點的同時,也能夠讓每次的斷點都是被重新生成的,看下效果,一目了然:

          效果

          發現沒有,我們通過這種方式,只要打開了控制臺,那么就會進入到無限斷點的循環中,使得不能做任何其他事情,而且每個斷點的生成都會開辟一個新的虛擬運行環境,這種情況下,只有關閉控制臺,才能結束斷點。

          即使使用右鍵選擇Never parse here,也毫無作用,雖然可以通過Deactive breakpoints按鈕來徹底禁用斷點,就是下面這個按鈕:

          按鈕

          但是,如果這樣做的話,那么用戶也就同時失去了調試其他代碼的能力。

          況且,我們接下來還會介紹其他的控制手段,可以配合著使用。

          1. 監測控制臺開啟

          我們如果能有一種手段,可以知道用戶開啟了控制臺,換句話說只要控制臺被打開,就通知我們或者被我們監測到,那么我們就可以執行一些控制手段,這種效果肯定是很理想的,遺憾的是還沒有這種api暴露給我們去讓我們能夠這樣做。

          不過我們可以通過其他的方式,利用既有的一些能力來實現這一點,這里我還是使用循環定時器,來不斷的去嗅探用戶是否開啟了控制臺,直接看代碼:

          ;(() => {
            setInterval(function() {
              let foo = document.createElement('a')
              let a1 = +new Date()
              console.table(foo)
              let a2 = +new Date()
              if(a2 - a1 > 1) {
                location.href = 'about:blank'
              }
              console.clear()
            }, 500)
          })()

          同樣,通過一個自執行函數,我們開啟了一個循環定時器,然后在回調方法里面,我們就去實現上面的目標,也是分為了兩步:

          第一步創建一個a元素,然后通過表格的形式將它打印出來,并記錄下消耗的時間。

          第二步判斷耗時的長短來控制是否跳轉到空白頁,然后清空控制臺。

          這種方式主要是利用了console.table的特性,它會將元素以表格的形式輸出到控制臺,大概就像下面的樣子:

          效果

          由于太多了,我就沒有全部截下來,如果沒有打開控制臺的話,使用console.table輸出我們創建的a標簽是很快的,有多快呢,就是js執行一條語句的速度,所以打印a1和a2的時間間隔非常短,幾乎為0,因為他們快到差不多是同時執行的,給大家打印看一下:

          效果

          我們先不打開控制臺,等輸出完畢再打開,很清楚的發現,我們沒打開控制臺的時候,輸出的a就是它標簽,而且時間間隔是0毫秒。

          現在我們打開控制臺的時候刷新一下頁面,看看控制臺的輸出:

          效果

          這次就變成了以table的形式輸出a元素,而且它的耗時明顯增多,不再是0毫秒,而是耗費了10毫秒,雖然打開控制臺的時候多次刷新頁面,每次輸出的毫秒數是不同的,但是跟關閉控制臺的時候輸出的耗時差距非常明顯,因此我們就可以在這個上面做文章。

          我在上面的代碼中假定了,只要是大于1毫秒的耗時,那就表示用戶打開了控制臺,然后我們就把頁面給跳轉到空白頁,當然了你也可以做一些任何你想做的操作,比如彈出一個提示,或者把body內容置空等等等等。

          回到我們上面的代碼,看一下它實際發生的作用和帶給我們的效果:

          效果

          哈哈,古德古德,平時瀏覽一切正常,只要剛一打開控制臺,瞬間頁面就被跳轉走了,什么都干不了。這樣我們就通過這種方式,達到了限制打開控制臺的目的,也就是在當前頁沒法調試,一打開就跳轉。

          這種辦法由于是繞路實現的,那么你可能會有疑問,它穩定嗎?會不會誤判,我可以對它絕對放心嗎?

          理論上來說,通過輸出的執行時間是不太能精確掌握的,但是我們可以再做一些其他的措施來逼近真相:

          ;(() => {
            setInterval(function() {
              let foo = document.createElement('a')
              let a1 = +new Date()
              console.table(foo)
              let a2 = +new Date()
              if(a2 - a1 > 1) {
                let time = 0
                for(let i = 0; i < 10; i++) {
                  let a1 = +new Date()
                  console.table(foo)
                  let a2 = +new Date()
                  time += a2 - a1
                }
                if(time > 20) {
                  location.href = 'about:blank'
                }
              }
              console.clear()
            }, 500)
          })()

          我又改造了一下判斷的邏輯,當發現輸出耗時為2毫秒甚至更多的時候,我立馬再進行一次真偽判斷,也就是說,萬一由于其他的影響,導致我第7行的代碼誤判了,那么我再同步執行一個循環,連續輸出10次,把他們的耗時總和計算出來,然后判斷是否大于20毫秒,如果還是耗時過高的話,那么就可以非常肯定的知道用戶是打開了控制臺,這個時候就可以放心的做一些處理了。

          最后

          其實限制用戶行為的方法有很多很多,上面列出了一些主要的,多種方法還是要結合著使用。你也可以自由發揮,多使用一些其他的手段,也會增加用戶復制或者調試的難度,比如防止用戶重寫console的方法,或者清除所有定時器等。

          甚至也可以將你的內容繪制到canvas上面來防止復制,多加一些js的處理工作,防止禁用js的時候,我們的代碼不生效,只有在js可用的時候再去渲染內容等。也可以在綜合考慮的情況下加上代碼混淆、代碼加密等措施。

          話說回來,大家都是同路人,何必相互為難,哈哈哈,不過提這個需求的人也著實會為難我們,既然提了那就盡力去做,能做到什么程度,只能說是盡量做到極致。

          希望上面的內容能夠幫助到你,也希望能夠對你有所啟發。

          謝謝


          主站蜘蛛池模板: 亚洲AⅤ无码一区二区三区在线 | 亚洲高清毛片一区二区| 久久精品亚洲一区二区三区浴池| 亚洲日韩中文字幕无码一区| 国产在线观看一区精品| 国产精品免费综合一区视频| 中文字幕无码不卡一区二区三区| 中文人妻无码一区二区三区| 亚洲国产一区视频| 日韩精品人妻av一区二区三区| 国产精品视频一区| 日韩免费无码一区二区视频| 无码中文字幕乱码一区| 亚洲影视一区二区| 韩国福利一区二区美女视频| 国产福利一区二区三区| 亚洲乱码一区二区三区国产精品 | 国产欧美一区二区精品仙草咪| 亚洲福利一区二区精品秒拍| 久久久久久综合一区中文字幕 | 国产精品亚洲产品一区二区三区 | 一区二区免费国产在线观看| 国产成人一区二区三区免费视频 | 日韩人妻精品一区二区三区视频 | 男人的天堂av亚洲一区2区 | 精品无码成人片一区二区| 国产亚洲福利一区二区免费看| 日韩在线一区视频| 日韩有码一区二区| 中文乱码精品一区二区三区| 在线观看午夜亚洲一区| 国产高清视频一区三区| 国产精品高清一区二区人妖| 亚洲熟女乱色一区二区三区| 韩国精品福利一区二区三区| 夜夜精品无码一区二区三区| 99久久国产精品免费一区二区| 久久久久人妻精品一区二区三区| 精品福利一区二区三区免费视频 | 一色一伦一区二区三区| 在线成人综合色一区|