🔍 搜尋結果:queryselector

🔍 搜尋結果:queryselector

99% 的時間都會用到的 22 個 JavaScript 函數 💯🔥

下面的函數對於 Web 開發和 JavaScript 程式設計來說是基礎,它們簡化了任務,例如*使用新舊 console.log()***進行偵錯**、**操作 DOM 以及處理 JSON 資料。** 了解和使用這些函數就像*建立該語言的基礎支柱*一樣。 掌握這些工具也肯定會簡化編碼流程,進而帶來良好的開發人員體驗。 --- 長話短說 ---- **控制台和除錯** -> 控制台.log(...args) **定時** -> setTimeout(回呼, 延遲) -> setInterval(回呼, 間隔) **DOM 操作和事件處理** -> 查詢選擇器全部(選擇器) -> addEventListener(事件,回呼) **JSON 處理** -> JSON.parse(jsonString) -> JSON.stringify(物件) **陣列運算(高階函數)** -> forEach(回呼) -> 地圖(回呼) -> 過濾器(回調) -> 減少(回調,初始值) **陣列運算** -> 切片(開始,結束) -> 拼接(開始,刪除計數,...專案) ->indexOf(元素) -> 包含(元素) -> 排序(比較函數) -> 反向() -> isArray(值) **字串操作** -> 分割(分隔符號) -> 連接(分隔符號) -> toLowerCase(), toUpperCase() -> 修剪() --- 控制台和除錯: ------- ### 0️⃣1️⃣ console.log(...args) ⇒ 將訊息或物件輸出到控制台以進行偵錯。 ``` // console.log(...args) console.log("Hello World!"); ``` --- 定時: --- ### 0️⃣2️⃣ setTimeout(回呼、延遲) ⇒ 在指定的延遲(以毫秒為單位)後執行函數。 ### 0️⃣3️⃣ setInterval(回調,間隔) ⇒ 以指定的時間間隔重複執行某個功能。 ``` // setTimeout(callback, delay) setTimeout(function() { console.log("This message will appear after 3 seconds."); }, 3000); // Runs the anonymous function after 3000 milliseconds (3 seconds) // setInterval(callback, interval) function printCounter() { let count = 0; setInterval(function() { console.log("Count:", count++); }, 1000); } printCounter(); // Prints count every second ``` --- DOM 操作與事件處理: ------------ ### 0️⃣4️⃣ querySelectorAll(選擇器) ⇒ 傳回一個 NodeList,其中包含與指定選擇器相符的所有元素。 ### 0️⃣5️⃣ addEventListener(事件,回呼) ⇒ 將事件處理函數附加到 HTML 元素。 ``` // querySelectorAll(selector) console.log("querySelectorAll(selector)"); const container = document.getElementById('container'); const links = container.querySelectorAll('a'); // Accessing the href attribute of each link // Iterate over the NodeList links.forEach(link => { console.log(link.href); }); // addEventListener(event, callback) console.log("addEventListener(event, callback)"); const button = document.getElementById('myButton'); button.addEventListener('click', function() { console.log('Button clicked!'); }); ``` --- JSON 處理: -------- ### 0️⃣6️⃣ JSON.parse(jsonString) ⇒ 解析 JSON 字串並傳回 JavaScript 物件。 ### 0️⃣7️⃣ JSON.stringify(物件) ⇒ 將 JavaScript 物件轉換為 JSON 字串。 ``` // JSON.parse(jsonString) console.log("JSON.parse(jsonString)"); const jsonString = '{"name": "John", "age": 30, "city": "New York"}'; const jsonObject = JSON.parse(jsonString); console.log(jsonObject.name); // Output: John console.log(jsonObject.age); // Output: 30 console.log(jsonObject.city); // Output: New York // JSON.stringify(object) console.log("JSON.stringify(object)"); const person = { name: 'John', age: 30, city: 'New York' }; const jsonString2 = JSON.stringify(person); console.log(jsonString2); // Output: {"name":"John","age":30,"city":"New York"} ``` --- 陣列運算(高階函數): ----------- ### 0️⃣8️⃣ forEach(回呼) ⇒ 對每個陣列元素執行一次提供的函數。 ### 0️⃣9️⃣地圖(回調) ⇒ 使用對每個元素呼叫提供的函數的結果來建立一個新陣列。 ### 1️⃣0️⃣過濾器(回呼) ⇒ 建立一個新陣列,其中的元素滿足給定的條件。 ### 1️⃣1️⃣減少(回調,初始值) ⇒ 透過對每個元素應用函數將陣列縮減為單一值。 ``` const numbers = [1, 2, 3, 4, 5]; // forEach(callback) console.log("forEach:"); numbers.forEach(num => { console.log(num); }); // Output: // 1 // 2 // 3 // 4 // 5 // map(callback) const doubledNumbers = numbers.map(num => num * 2); // Output: [2, 4, 6, 8, 10] // filter(callback) const evenNumbers = numbers.filter(num => num % 2 === 0); // [2, 4] // reduce(callback, initialValue) const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0); // 15 ``` --- 陣列操作: ----- ### 1️⃣2️⃣片(開始,結束) ⇒ 傳回指定起始索引和結束索引之間的陣列部分的淺表副本。 ### 1️⃣3️⃣ 拼接(開始,刪除計數,...專案) ⇒ 透過刪除/取代元素和/或新增元素來變更陣列內容。 ### 1️⃣4️⃣indexOf(元素) ⇒ 傳回在陣列中可以找到給定元素的第一個索引,如果不存在則傳回 -1。 ### 1️⃣5️⃣包含(元素) ⇒ 判斷陣列是否包含某個元素,傳回 true 或 false。 ### 1️⃣6️⃣排序(比較函數) ⇒ 根據提供的函數或預設排序順序對陣列元素進行排序。 ### 1️⃣7️⃣反向() ⇒ 反轉陣列中元素的順序。 ### 1️⃣8️⃣ isArray(值) ⇒ 檢查給定值是否為陣列,傳回 true 或 false。 ``` // slice(start, end) const array = [1, 2, 3, 4, 5]; const slicedArray = array.slice(1, 4); console.log("slice:", slicedArray); // Output: [2, 3, 4] // splice(start, deleteCount, ...items) const spliceArray = [1, 2, 3, 4, 5]; spliceArray.splice(2, 2, 'a', 'b'); console.log("splice:", spliceArray); // Output: [1, 2, "a", "b", 5] // indexOf(element) const indexOfArray = ['apple', 'banana', 'cherry']; const indexOfCherry = indexOfArray.indexOf('cherry'); console.log("indexOf:", indexOfCherry); // Output: 2 // includes(element) const includesArray = [1, 2, 3, 4, 5]; const includesValue = includesArray.includes(3); console.log("includes:", includesValue); // Output: true // sort(compareFunction) const sortArray = [3, 1, 4, 1, 5]; sortArray.sort((a, b) => a - b); console.log("sort:", sortArray); // Output: [1, 1, 3, 4, 5] // reverse() const reverseArray = ['a', 'b', 'c', 'd']; reverseArray.reverse(); console.log("reverse:", reverseArray); // Output: ['d', 'c', 'b', 'a'] // isArray(value) const isArrayValue = [1, 2, 3]; const isArray = Array.isArray(isArrayValue); console.log("isArray:", isArray); // Output: true ``` --- 字串操作: ----- ### 1️⃣9️⃣分割(分隔符號) ⇒ 根據指定的分隔符號將字串分割為子字串陣列。 ### 2️⃣0️⃣加入(分隔符號) ⇒ 將陣列的所有元素連接成一個字串,並以指定的分隔符號分隔。 ### 2️⃣1️⃣ toLowerCase(), toUpperCase() ⇒ 將字串轉換為小寫或大寫。 ### 2️⃣2️⃣修剪() ⇒ 刪除字串兩端的空格。 ``` // split(separator) const sentence = "Hello, world! How are you?"; const words = sentence.split(' '); console.log("split:", words); // Output: ["Hello,", "world!", "How", "are", "you?"] // join(separator) const fruits = ['Apple', 'Banana', 'Orange']; const fruitString = fruits.join(', '); console.log("join:", fruitString); // Output: "Apple, Banana, Orange" // toLowerCase(), toUpperCase() const mixedCase = "Hello WoRLd"; const lowerCase = mixedCase.toLowerCase(); const upperCase = mixedCase.toUpperCase(); console.log("toLowerCase:", lowerCase); // Output: "hello world" console.log("toUpperCase:", upperCase); // Output: "HELLO WORLD" // trim() const stringWithWhitespace = " Hello, world! "; const trimmedString = stringWithWhitespace.trim(); console.log("trim:", trimmedString); // Output: "Hello, world!" ``` 🙌 最後的想法 ------- 當我們結束對上述功能的討論時,我請您分享您的見解。 **您認為在 Web 開發或主要是 JavaScript 專案中還需要哪些其他功能?** 請隨時在下面的評論中分享您的想法和有用的功能! 我希望你喜歡這篇文章! ❤️ 與我聯絡: [Linktree。](https://linktr.ee/arjuncodess/) 快樂編碼! 🚀 感謝22097! 🤗 --- 原文出處:https://dev.to/arjuncodess/18-javascript-functions-youll-use-99-of-the-time-2bl4

用 24 行 CSS 建立自訂捲軸

您是否曾經遇到過一個帶有漂亮滾動條的網站,然後心想“哇,我希望我能建立這樣的東西。”我也是!經過一番調查後,我發現為您的部落格或個人作品集建立自訂捲軸並不需要大量的 CSS。 今天,我們將建立一個漸進式進度條,當您捲動時,它會從零不透明度淡入完全不透明度。本教學只需要基礎的 HTML、CSS 和 JavaScript 知識,不會使用任何 JavaScript 框架或函式庫。您可以在[CodePen](https://codepen.io/emmawedekind/pen/VwabMpX)上找到完整的程式碼! HTML。 ===== 首先讓我們建立我們的文件結構。在 HTML CodePen 編輯器內部新增兩個元素:進度條和進度容器。 進度條將指示使用者向下捲動頁面的距離。進度條容器將跨越頁面的整個高度並包含進度條。 我們還加入一個帶有文字“Welcome”的頁面標題和一些 lorem ipsem 段落。我加入了其中七個段落,但為了簡潔起見,本文僅包含一個。 ``` <h1>Welcome.</h1> <p> Lorem ipsum dolor, sit amet consectetur adipisicing elit. Aspernatur provident eveniet veritatis ipsa id consectetur ab tenetur dolores eaque. Temporibus laboriosam cum corporis amet doloremque animi aut ipsa ea a? </p> ``` 這是我們的網頁目前的樣子。 ![HTML 頁面](https://dev-to-uploads.s3.amazonaws.com/i/ywep5g9wskux9p1c6j0v.png) CSS。 ==== 現在我們準備好加入我們的樣式。首先,讓我們加入一些基本的頁面樣式,以使我們的網站看起來更好。 ``` body { background: #171414; font-family: "Courier New", Courier, monospace; color: #ffffff; padding: 15% 15% 5%; } p { font-size: 1.8rem; } p:first-of-type { margin-top: 100px; } h1 { font-size: 200px; position: absolute; top: -5%; left: 15%; opacity: 25%; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; } ``` ![CSS 1](https://dev-to-uploads.s3.amazonaws.com/i/yi71ifxijhhinx55mzn2.png) 接下來讓我們隱藏預設的瀏覽器捲軸。我們可以使用 webkit 供應商前綴來隱藏 Chrome 和 Safari 中的捲軸。 ``` ::-webkit-scrollbar { width: 0; background: transparent; } ``` 我們需要對 Firefox 進行特殊考慮,因為它不是 webkit 瀏覽器,並且不會回應 webkit 供應商前綴(如 Chrome 和 Safari)。 ``` html { scrollbar-width: none; } ``` 現在我們準備好設定自訂捲軸的樣式了。首先讓我們設定滾動條容器。我們希望滾動條容器固定在視窗的右側,因此我們將使用固定位置,並將頂部和右側值設為 0。我們將為滾動容器提供十個像素的寬度和非常淺的灰色背景。 ``` #progressBarContainer { position: fixed; top: 0; right: 0; width: 10px; height: 100%; background: rgba(255, 255, 255, 0.05); } ``` 現在您應該看到滾動容器出現在視窗的右側。 ![CSS 2](https://dev-to-uploads.s3.amazonaws.com/i/tjn42dlbk91wqqyqt3eq.png) 我們的進度條也將固定在視窗的右側,寬度為十個像素,但是,我們將給它一個從紅色到紫色的線性漸進,以使其脫穎而出。 ``` #progressBar { position: fixed; top: 0; right: 0; width: 10px; background: linear-gradient(to top, violet, red); height: 100%; opacity: 100%; } ``` ![CSS 3](https://dev-to-uploads.s3.amazonaws.com/i/pyhkn7346gjfwpqfwvig.png) 我們希望動態更新滾動進度條的高度和不透明度,因此讓我們將高度和不透明度設為零。 ``` #progressBar { position: fixed; top: 0; right: 0; width: 10px; background: linear-gradient(to top, violet, red); height: 0; opacity: 0; } ``` JavaScript。 =========== 要更新進度條的高度和不透明度,我們需要寫幾行 JavaScript。讓我們先取得進度條 DOM 節點。 ``` const progressBar = document.querySelector("#progressBar"); ``` 我們還需要計算網頁的總高度。我們可以透過整個文件主體滾動高度減去視窗內部高度來計算總高度。 ``` let totalPageHeight = document.body.scrollHeight - window.innerHeight; ``` 接下來,讓我們向視窗新增一個事件偵聽器。當我們滾動時,我們想要計算新的進度高度。我們可以透過先將視窗的頁面 Y 偏移量除以總頁面高度來計算小數。要在 CSS 程式碼中使用這個值,我們必須將該分數乘以 100(得到百分比)。 最後,我們可以將進度條的高度和不透明度設定為新的進度高度。 *不熟`${}`語法?它稱為模板字串或模板文字,它允許您將計算表達式(${} 之間的值)與純字串組合起來。* ``` window.onscroll = () => { let newProgressHeight = (window.pageYOffset / totalPageHeight) * 100; progressBar.style.height = `${newProgressHeight}%`; progressBar.style.opacity = `${newProgressHeight}%`; }; ``` 結論。 === 就是這樣!現在,只需幾行 CSS,您就擁有了一個漂亮的自訂捲軸。 --- 原文出處:https://dev.to/emmabostian/creating-a-custom-scroll-bar-in-24-lines-of-css-4gg0

你應該知道的 10 個 JavaScript 陣列方法

這篇文章最初發佈在[我的部落格](http://frugencefidel.com/blog/10-javascript-array-methods-you-should-know)上。 在這篇文章中,我將分享 10 個你應該知道的 JavaScript 陣列方法。 如果你對陣列一無所知,可以點選這裡查看[陣列介紹](http://frugencefidel.com/blog/understanding-basics-of-array-in-javascript)。 這裡有 10 個你至少應該知道的 javascript 陣列方法 *1. forEach()* 此方法可以幫助您循環陣列。 ``` const arr = [1, 2, 3, 4, 5, 6]; arr.forEach(item => { console.log(item); // output: 1 2 3 4 5 6 }); ``` *2. includes()* 此方法檢查陣列是否包含在方法中傳遞的專案。 ``` const arr = [1, 2, 3, 4, 5, 6]; arr.includes(2); // output: true arr.includes(7); // output: false ``` *3. filter()* 此方法會建立新陣列,其中僅包含在提供的函數內傳遞條件的元素。 ``` const arr = [1, 2, 3, 4, 5, 6]; // item(s) greater than 3 const filtered = arr.filter(num => num > 3); console.log(filtered); // output: [4, 5, 6] console.log(arr); // output: [1, 2, 3, 4, 5, 6] ``` *4. map()* 此方法透過在每個元素中呼叫提供的函數來建立新陣列。 ``` const arr = [1, 2, 3, 4, 5, 6]; // add one to every element const oneAdded = arr.map(num => num + 1); console.log(oneAdded); // output [2, 3, 4, 5, 6, 7] console.log(arr); // output: [1, 2, 3, 4, 5, 6] ``` *5. reduce()* > reduce() 方法對累加器和陣列中的每個元素(從左到右)套用函數,將其減少為單一值 - MDN ``` const arr = [1, 2, 3, 4, 5, 6]; const sum = arr.reduce((total, value) => total + value, 0); console.log(sum); // 21 ``` *6. some()* 此方法檢查陣列的至少一項是否符合條件。如果通過,則傳回“true”,否則傳回“false”。 ``` const arr = [1, 2, 3, 4, 5, 6]; // at least one element is greater than 4? const largeNum = arr.some(num => num > 4); console.log(largeNum); // output: true // at least one element is less than or equal to 0? const smallNum = arr.some(num => num <= 0); console.log(smallNum); // output: false ``` *7. every()* 此方法檢查所有陣列的專案是否都符合條件。如果通過,則傳回“true”,否則傳回“false”。 ``` const arr = [1, 2, 3, 4, 5, 6]; // all elements are greater than 4 const greaterFour = arr.every(num => num > 4); console.log(greaterFour); // output: false // all elements are less than 10 const lessTen = arr.every(num => num < 10); console.log(lessTen); // output: true ``` *8. sort()* 此方法用於按升序或降序排列/排序陣列的專案。 ``` const arr = [1, 2, 3, 4, 5, 6]; const alpha = ['e', 'a', 'c', 'u', 'y']; // sort in descending order descOrder = arr.sort((a, b) => a > b ? -1 : 1); console.log(descOrder); // output: [6, 5, 4, 3, 2, 1] // sort in ascending order ascOrder = alpha.sort((a, b) => a > b ? 1 : -1); console.log(ascOrder); // output: ['a', 'c', 'e', 'u', 'y'] ``` *9. Array.from()* 這會將所有類似陣列或可迭代的內容變更為真正的陣列,尤其是在使用 DOM 時,這樣您就可以使用其他陣列方法,如reduce、map、filter 等。 ``` const name = 'frugence'; const nameArray = Array.from(name); console.log(name); // output: frugence console.log(nameArray); // output: ['f', 'r', 'u', 'g', 'e', 'n', 'c', 'e'] ``` 使用 DOM ``` // I assume that you have created unorder list of items in our html file. const lis = document.querySelectorAll('li'); const lisArray = Array.from(document.querySelectorAll('li')); // is true array? console.log(Array.isArray(lis)); // output: false console.log(Array.isArray(lisArray)); // output: true ``` *10. Array.of()* 這會根據傳入的每個參數建立陣列。 ``` const nums = Array.of(1, 2, 3, 4, 5, 6); console.log(nums); // output: [1, 2, 3, 4, 5, 6] ``` --- 原文出處:https://dev.to/frugencefidel/10-javascript-array-methods-you-should-know-4lk3

30 個 JavaScript 奇妙小技巧

歡迎使用我們精選的 JavaScript 技巧集合,它將幫助您優化程式碼、使其更具可讀性並節省您的時間。 讓我們深入研究 JavaScript 的功能和超越傳統的技巧,並發現這種強大的程式語言的全部潛力。 ### 1. 使用!!轉換為布林值 使用雙重否定快速將任何值轉換為布林值。 ``` let truthyValue = !!1; // true let falsyValue = !!0; // false ``` ### 2. 預設功能參數 設定函數參數的預設值以避免未定義的錯誤。 ``` function greet(name = "Guest") { return `Hello, ${name}!`; } ``` ### 3. 短 if-else 的三元運算符 `if-else`語句的簡寫。 ``` let price = 100; let message = price > 50 ? "Expensive" : "Cheap"; ``` ### 4. 動態字串的範本文字 使用模板文字在字串中嵌入表達式。 ``` let item = "coffee"; let price = 15; console.log(`One ${item} costs $${price}.`); ``` ### 5. 解構賦值 輕鬆從物件或陣列中提取屬性。 ``` let [x, y] = [1, 2]; let {name, age} = {name: "Alice", age: 30}; ``` ### 6. 用於陣列和物件克隆的擴展運算符 克隆陣列或物件而不引用原始陣列或物件。 ``` let originalArray = [1, 2, 3]; let clonedArray = [...originalArray]; ``` ### 7. 短路評估 使用邏輯運算子進行條件執行。 ``` let isValid = true; isValid && console.log("Valid!"); ``` ### 8. 可選連結 (?.) 如果引用為`nullish`則可以安全地存取巢狀物件屬性,而不會出現錯誤。 ``` let user = {name: "John", address: {city: "New York"}}; console.log(user?.address?.city); // "New York" ``` ### 9. 空合併運算子 (??) 使用`??`為`null`或`undefined`提供預設值。 ``` let username = null; console.log(username ?? "Guest"); // "Guest" ``` [![monday.com 的互動式橫幅展示了在數位介面上組織工作流程任務的雙手,並帶有「告訴我如何做」號召性用語按鈕。](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xd7qdmxdd726kskelydk.png)](https://try.monday.com/9xf9ftaa4i2f) ### 10. 使用`map` 、 `filter`和`reduce`進行陣列操作 無需傳統循環即可處理陣列的優雅方法。 ``` // Map let numbers = [1, 2, 3, 4]; let doubled = numbers.map(x => x * 2); // Filter const evens = numbers.filter(x => x % 2 === 0); // Reduce const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0); ``` ### 11.標記模板文字 使用模板文字進行函數呼叫以進行自訂字串處理。 ``` function highlight(strings, ...values) { return strings.reduce((prev, current, i) => `${prev}${current}${values[i] || ''}`, ''); } let price = 10; console.log(highlight`The price is ${price} dollars.`); ``` ### 12.使用Object.entries()和Object.fromEntries() 將物件轉換為陣列並返回以方便操作。 ``` let person = {name: "Alice", age: 25}; let entries = Object.entries(person); let newPerson = Object.fromEntries(entries); ``` ### 13. 唯一元素的集合物件 使用 Set 儲存任何類型的唯一值。 ``` let numbers = [1, 1, 2, 3, 4, 4]; let uniqueNumbers = [...new Set(numbers)]; ``` ### 14. 物件中的動態屬性名稱 在物件文字表示法中使用方括號來建立動態屬性名稱。 ``` let dynamicKey = 'name'; let person = {[dynamicKey]: "Alice"}; ``` ### 15. 使用bind()進行函數柯里化 建立一個新函數,在呼叫時將其 this 關鍵字設定為提供的值。 ``` function multiply(a, b) { return a * b; } let double = multiply.bind(null, 2); console.log(double(5)); // 10 ``` ### 16. 使用 Array.from() 從類別陣列物件建立陣列 將類似陣列或可迭代的物件轉換為真正的陣列。 ``` let nodeList = document.querySelectorAll('div'); let nodeArray = Array.from(nodeList); ``` ### 17. 可迭代物件的 for...of 循環 直接迭代可迭代物件(包括陣列、映射、集合等)。 ``` for (let value of ['a', 'b', 'c']) { console.log(value); } ``` ### 18. 使用 Promise.all() 實作並發 Promise 同時執行多個 Promise 並等待所有的都解決。 ``` let promises = [fetch(url1), fetch(url2)]; Promise.all(promises) .then(responses => console.log('All done')); ``` ### 19. 函數參數的剩餘參數 將任意數量的參數捕獲到陣列中。 ``` function sum(...nums) { return nums.reduce((acc, current) => acc + current, 0); } ``` [![Coursera Plus 訂閱產品包括 AWS 基礎、Google IT 支援專業憑證和商業網路安全專業化。](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ys5gk2tfzcn4iniuq48s.png)](https://imp.i384100.net/c/3922519/1320997/14726) ### 20. 用於性能優化的記憶 儲存函數結果以避免冗餘處理。 ``` const memoize = (fn) => { const cache = {}; return (...args) => { let n = args[0]; // assuming single argument for simplicity if (n in cache) { console.log('Fetching from cache'); return cache[n]; } else { console.log('Calculating result'); let result = fn(n); cache[n] = result; return result; } }; }; ``` ### 21. 使用 ^ 交換值 使用 XOR 以位元運算子交換兩個變數的值,無需使用臨時變數。 ``` let a = 1, b = 2; a ^= b; b ^= a; a ^= b; // a = 2, b = 1 ``` ### 22. 使用 flat() 展平陣列 使用`flat()`方法輕鬆展平巢狀陣列,並將展平深度作為可選參數。 ``` let nestedArray = [1, [2, [3, [4]]]]; let flatArray = nestedArray.flat(Infinity); ``` ### 23. 用一元加法轉換為數字 使用一元加運算子快速將字串或其他值轉換為數字。 ``` let str = "123"; let num = +str; // 123 as a number ``` ### 24. HTML 片段的模板字串 使用模板字串建立 HTML 片段,使動態 HTML 生成更加清晰。 ``` let items = ['apple', 'orange', 'banana']; let html = `<ul>${items.map(item => `<li>${item}</li>`).join('')}</ul>`; ``` ### 25. 使用 Object.assign() 合併物件 將多個來源物件合併到一個目標物件中,有效地組合它們的屬性。 ``` let obj1 = { a: 1 }, obj2 = { b: 2 }; let merged = Object.assign({}, obj1, obj2); ``` [![符合人體工學的垂直滑鼠旨在減輕手腕壓力](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/56c0z4lzxtkthnnk8gsb.jpg)](https://amzn.to/3UXGIyx) 使用符合人體工學的滑鼠優化您的編程設置,專為舒適和長時間的編碼會話而定制。 ### 26. 預設值短路 處理潛在的未定義或空變數時,使用邏輯運算子指派預設值。 ``` let options = userOptions || defaultOptions; ``` ### 27. 使用括號表示法動態存取物件屬性 使用括號表示法動態存取物件的屬性,當屬性名稱儲存在變數中時非常有用。 ``` let property = "name"; let value = person[property]; // Equivalent to person.name ``` ### 28. 使用 Array.includes() 進行存在檢查 使用includes() 檢查陣列是否包含某個值,它是indexOf 的更清晰的替代方法。 ``` if (myArray.includes("value")) { // Do something } ``` ### 29. Function.prototype.bind() 的強大功能 將函數綁定到上下文(此值)並部分套用參數,以建立更多可重複使用和模組化的程式碼。 ``` const greet = function(greeting, punctuation) { return `${greeting}, ${this.name}${punctuation}`; }; const greetJohn = greet.bind({name: 'John'}, 'Hello'); console.log(greetJohn('!')); // "Hello, John!" ``` ### 30.防止物件修改 使用`Object.freeze()`防止物件進行修改,使其不可變。對於更深層的不變性,請考慮更徹底地強制不變性的函式庫。 ``` let obj = { name: "Immutable" }; Object.freeze(obj); obj.name = "Mutable"; // Fails silently in non-strict mode ``` 我希望這些 JavaScript 技巧為您提供如何進行[JavaScript 程式設計的](https://www.w3schools.com/js/)新視角。 從利用模板文字的簡潔功能到掌握`map` 、 `filter`和`reduce`的效率,這些 JavaScript 技巧將豐富您的開發工作流程並激發您的下一個專案。 讓這些 JavaScript 技巧不僅可以完善您目前的專案,還可以在您的[程式設計之旅](https://www.webdevstory.com/programming-roadmap/)中激發未來創新的靈感。 ***支持我們的技術見解*** [![請我喝杯咖啡](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lsm9uucbnw7x9iw0loxr.png)](https://www.buymeacoffee.com/mmainulhasan) [![透過 PayPal 按鈕捐贈](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ipnhbim2ba56kt32zhn3.png)](https://www.paypal.com/donate/?hosted_button_id=GDUQRAJZM3UR8) 注意:此頁面上的某些連結可能是附屬連結。如果您透過這些連結進行購買,我可能會賺取少量佣金,而無需您支付額外費用。感謝您的支持! --- 原文出處:https://dev.to/mmainulhasan/30-javascript-tricky-hacks-gfc

Next.js 14 使用瀏覽器爬蟲,進行即時資料抓取的預訂應用程式

目錄 == - [介紹](#introduction) - [技術堆疊](#tech-stack) - [特徵](#features) - [設定 Next.js 應用程式](#step-1-setting-up-the-nextjs-application) - [安裝所需的套件](#step-2-installing-required-packages) - [設定 Redis 連接](#step-3-setting-up-redis-connection) - [配置 BullMQ 佇列](#step-4-configuring-bullmq-queue) - [Next.js 儀器設置](#step-5-nextjs-instrumentation-setup) - [設定 Bright Data 的抓取瀏覽器](#step-6-setting-up-bright-datas-scraping-browser) - [Bright Data 的抓取瀏覽器是什麼?](#what-is-bright-datas-scraping-browser) - [設定 Bright Data 抓取瀏覽器的步驟](#steps-to-set-up-bright-datas-scraping-browser) - [使用 Puppeteer 實作抓取邏輯](#implementing-the-scraping-logic-with-puppeteer) - [航班搜尋功能](#flight-search-feature) - [顯示航班搜尋結果](#displaying-flight-search-results) - [探索完整的指南和程式碼庫](#discover-the-complete-guide-and-codebase) - [在 YouTube 上觀看詳細說明](#watch-the-detailed-explanation-on-youtube) - [在 GitHub 上探索完整程式碼](#explore-the-full-code-on-github) - [結論](#conclusion) 介紹 == 在不斷發展的 Web 開發領域,有效收集、處理和顯示外部來源資料的能力變得越來越有價值。無論是市場研究、競爭分析或客戶洞察,網路抓取在釋放網路資料的巨大潛力方面都發揮著至關重要的作用。 這篇部落格文章介紹了建立強大的 Next.js 應用程式的綜合指南,該應用程式旨在從領先的旅行搜尋引擎之一 Kayak 抓取航班資料。透過利用 Next.js 的強大功能以及 BullMQ、Redis 和 Puppeteer 等現代技術。 技術堆疊 ==== - [Next.js](https://nextjs.org/docs) - [順風CSS](https://tailwindcss.com/docs) - [下一個介面](https://nextui.org/docs) - [健康)狀況](https://zustand.surge.sh/) - [條紋](https://stripe.com/docs) - [Bright Data 的抓取瀏覽器](https://brdta.com/kishansheth21) - [打字稿](https://www.typescriptlang.org/docs) - [雷迪斯](https://redis.io/documentation) - [BullMQ](https://docs.bullmq.io/) - [傀儡師](https://pptr.dev/) - [智威湯遜](https://jwt.io/introduction) - [阿克西奧斯](https://axios-http.com/docs/intro) - [PostgreSQL](https://www.postgresql.org/docs) - [棱鏡](https://www.prisma.io/docs) 特徵 == - 🚀 帶有 Tailwind CSS 的 Next.js 14 應用程式目錄 - 體驗由最新 Next.js 14 提供支援的時尚現代的 UI,並使用 Tailwind CSS 進行設計,以實現完美的外觀和感覺。 - 🔗 API 路由和伺服器操作 - 深入研究與 Next.js 14 的 API 路由和伺服器操作的無縫後端集成,確保高效的資料處理和伺服器端邏輯執行。 - 🕷 使用 Puppeteer Redis 和 BullMQ 進行抓取 - 利用 Puppeteer 的強大功能進行進階 Web 抓取,並使用 Redis 和 BullMQ 管理佇列和作業以實現強大的後端操作。 - 🔑 用於身份驗證和授權的 JWT 令牌 - 使用 JWT 令牌保護您的應用程式,為整個平台提供可靠的身份驗證和授權方法。 - 💳 支付網關 Stripe - 整合 Stripe 進行無縫支付處理,為預訂旅行、航班和飯店提供安全、輕鬆的交易。 - ✈️ 使用 Stripe 支付網關預訂旅行、航班和飯店 - 使用我們的 Stripe 支援的支付系統,讓您的旅遊預訂體驗變得輕鬆。 - 📊 從多個網站抓取即時資料 - 從多個來源抓取即時資料,保持領先,讓您的應用程式更新最新資訊。 - 💾 使用 Prisma 將抓取的資料儲存在 PostgreSQL 中 - 利用 PostgreSQL 和 Prisma 高效儲存和管理抓取的資料,確保可靠性和速度。 - 🔄 用於狀態管理的 Zustand - 透過 Zustand 簡化狀態邏輯並增強效能,在您的應用程式中享受流暢且可管理的狀態管理。 - 😈 該應用程式的最佳功能 - 使用 Bright Data 的抓取瀏覽器抓取不可抓取的資料。 ![抓取瀏覽器迷因](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e0ytki1wpsbvluk1qkpo.jpg) Bright Data的抓取瀏覽器為我們提供了自動驗證碼解決功能,可以幫助我們抓取不可抓取的資料。 第 1 步:設定 Next.js 應用程式 --------------------- 1. **建立 Next.js 應用程式**:首先建立一個新的 Next.js 應用程式(如果您還沒有)。您可以透過在終端機中執行以下命令來完成此操作: ``` npx create-next-app@latest booking-app ``` 2. **導航到您的應用程式目錄**:變更為您新建立的應用程式目錄: ``` cd booking-app ``` 步驟2:安裝所需的軟體包 ------------ 您需要安裝多個軟體包,包括 Redis、BullMQ 和 Puppeteer Core。執行以下命令來安裝它們: ``` npm install ioredis bullmq puppeteer-core ``` - `ioredis`是 Node.js 的強大 Redis 用戶端,支援與 Redis 進行通訊。 - `bullmq`以 Redis 作為後端來管理作業和訊息佇列。 - `puppeteer-core`可讓您控制外部瀏覽器以進行抓取。 步驟3:設定Redis連接 ------------- 在適當的目錄(例如`lib/` )中建立一個檔案(例如`redis.js` )來配置 Redis 連線: ``` // lib/redis.js import Redis from 'ioredis'; // Use REDIS_URL from environment or fallback to localhost const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379'; const connection = new Redis(REDIS_URL); export { connection }; ``` 步驟4:配置BullMQ佇列 -------------- 透過在 Redis 配置所在的相同目錄中建立另一個檔案(例如, `queue.js` )來設定 BullMQ 佇列: ``` // lib/queue.js import { Queue } from 'bullmq'; import { connection } from './redis'; export const importQueue = new Queue('importQueue', { connection, defaultJobOptions: { attempts: 2, backoff: { type: 'exponential', delay: 5000, }, }, }); ``` 第 5 步:Next.js 儀器設置 ------------------ Next.js 允許偵測,可以在 Next.js 配置中啟用。您還需要建立一個用於作業處理的工作文件。 1.**在 Next.js 中啟用 Instrumentation** :將以下內容新增至`next.config.js`以啟用 Instrumentation: ``` // next.config.js module.exports = { experimental: { instrumentationHook: true, }, }; ``` 2.**建立用於作業處理的 Worker** :在您的應用程式中,建立一個檔案 ( `instrumentation.js` ) 來處理作業處理。該工作人員將使用 Puppeteer 來執行抓取任務: ``` // instrumentation.js export const register = async () => { if (process.env.NEXT_RUNTIME === 'nodejs') { const { Worker } = await import('bullmq'); const puppeteer = await import('puppeteer-core'); const { connection } = await import('./lib/redis'); const { importQueue } = await import('./lib/queue'); new Worker('importQueue', async (job) => { // Job processing logic with Puppeteer goes here }, { connection, concurrency: 10, removeOnComplete: { count: 1000 }, removeOnFail: { count: 5000 }, }); } }; ``` 第 6 步:設定 Bright Data 的抓取瀏覽器 --------------------------- 在設定 Bright 資料抓取瀏覽器之前,我們先來談談什麼是抓取瀏覽器。 ### Bright Data 的抓取瀏覽器是什麼? Bright Data 的抓取瀏覽器是一款用於自動網頁抓取的尖端工具,旨在與 Puppeteer、Playwright 和 Selenium 無縫整合。它提供了一套網站解鎖功能,包括代理輪換、驗證碼解決等,以提高抓取效率。它非常適合需要互動的複雜網頁抓取,透過在 Bright Data 基礎架構上託管無限的瀏覽器會話來實現可擴展性。如欲了解更多詳情,請造訪[光明資料](https://brdta.com/kishansheth21)。 ![明亮的資料抓取瀏覽器](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c70ochb1lgdrusgicwz4.png) <a id="steps-to-set-up-bright-datas-scraping-browser"></a> #### 第 1 步:導覽至 Bright Data 網站 首先造訪[Brightdata.com](https://brdta.com/kishansheth21) 。這是您存取 Bright Data 提供的豐富網頁抓取資源和工具的入口。 ![光明資料首頁](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/afgkpgytcytoqfuq0h8w.png) #### 第 2 步:建立帳戶 造訪 Bright Data 網站後,註冊並建立一個新帳戶。系統將提示您輸入基本資訊以啟動並執行您的帳戶。 ![登入/註冊 Bright Data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ffha75szs9tubh8kra9j.png) #### 第 3 步:選擇您的產品 在產品選擇頁面上,尋找代理商和抓取基礎設施產品。本產品專為滿足您的網路抓取需求而設計,提供強大的資料擷取工具和功能。 ![光明資料產品](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b058azlmjv30po6289fh.png) #### 第 4 步:新增代理 在「代理程式和抓取基礎設施」頁面中,您會找到一個「新增按鈕」。點擊此按鈕開始將新的抓取瀏覽器新增到您的工具包的過程。 ![新代理](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2jscxsyt9yess1nvzi4z.png) #### 第五步:選擇抓取瀏覽器 將出現一個下拉列表,您應該從中選擇抓取瀏覽器選項。這告訴 Bright Data 您打算設定一個新的抓取瀏覽器環境。 ![選擇抓取瀏覽器](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i483ipx7spwne65c6tep.png) #### 第 6 步:為您的抓取瀏覽器命名 為您的新抓取瀏覽器指定一個唯一的名稱。這有助於稍後辨識和管理它,特別是如果您計劃對不同的抓取專案使用多個瀏覽器。 ![抓取瀏覽器名稱](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n0qnitec7s7gki7t3826.png) #### 步驟7:新增瀏覽器 命名您的瀏覽器後,按一下「新增」按鈕。此操作完成了新的抓取瀏覽器的建立。 ![新增抓取瀏覽器](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ao6n1edyyx2no621nh01.png) #### 第 8 步:查看您的抓取瀏覽器詳細訊息 新增抓取瀏覽器後,您將被導向到一個頁面,您可以在其中查看新建立的抓取瀏覽器的所有詳細資訊。這些資訊對於整合和使用至關重要。 ![抓取瀏覽器詳細訊息](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ev33mbgznevn6h9p60g6.png) #### 第 9 步:存取程式碼和整合範例 尋找“查看程式碼和整合範例”按鈕。點擊此按鈕將為您提供如何跨多種程式語言和程式庫整合和使用抓取瀏覽器的全面視圖。對於希望自訂抓取設定的開發人員來說,此資源非常寶貴。 ![程式碼和整合範例按鈕](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/30araqzko585yzmhrumd.png) #### 第 10 步:整合您的抓取瀏覽器 最後,複製 SRS\_WS\_ENDPOINT 變數。這是一條關鍵訊息,您需要將其整合到原始程式碼中,以便您的應用程式能夠與您剛剛設定的抓取瀏覽器進行通訊。 ![抓取瀏覽器端點](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kqhpglz1lngobguk2nu4.png) 透過遵循這些詳細步驟,您已在 Bright Data 平台中成功建立了一個抓取瀏覽器,準備好處理您的網頁抓取任務。請記住,Bright Data 提供廣泛的文件和支持,幫助您最大限度地提高抓取專案的效率和效果。無論您是在收集市場情報、進行研究還是監控競爭格局,新設定的抓取瀏覽器都是資料收集庫中的強大工具。 ### 第 7 步:使用 Puppeteer 實作抓取邏輯 從我們上次設定用於抓取航班資料的 Next.js 應用程式的地方開始,下一個關鍵步驟是實現實際的抓取邏輯。此過程涉及利用 Puppeteer 連接到瀏覽器實例、導航到目標 URL(在我們的範例中為 Kayak)並抓取必要的飛行資料。提供的程式碼片段概述了實現此目標的複雜方法,與我們先前建立的 BullMQ 工作設定無縫整合。讓我們分解這個抓取邏輯的元件,並了解它如何適合我們的應用程式。 #### 建立與瀏覽器的連接 我們抓取過程的第一步是透過 Puppeteer 建立與瀏覽器的連線。這是透過利用`puppeteer.connect`方法來完成的,該方法使用 WebSocket 端點 ( `SBR_WS_ENDPOINT` ) 連接到現有的瀏覽器實例。此環境變數應設定為您正在使用的抓取瀏覽器服務的 WebSocket URL,例如 Bright Data: ``` const browser = await puppeteer.connect({ browserWSEndpoint: SBR_WS_ENDPOINT, }); ``` #### 開啟新頁面並導航到目標 URL 連線後,我們在瀏覽器中建立一個新頁面並導航到作業資料中指定的目標 URL。此 URL 是我們打算從中抓取航班資料的特定 Kayak 搜尋結果頁面: ``` const page = await browser.newPage(); await page.goto(job.data.url); ``` #### 抓取航班資料 我們邏輯的核心在於從頁面中抓取航班資料。我們透過使用`page.evaluate`來實現這一點,這是一種 Puppeteer 方法,允許我們在瀏覽器上下文中執行腳本。在此腳本中,我們等待必要的元素加載,然後繼續收集航班資訊: - **Flight Selector** :我們以`.nrc6-wrapper`類別為目標元素,其中包含航班詳細資訊。 - **資料擷取**:對於每個航班元素,我們提取詳細訊息,例如航空公司徽標、出發和到達時間、航班持續時間、航空公司名稱和價格。出發和到達時間經過清理,以刪除最後不必要的數值,確保我們準確地捕捉時間。 - **價格處理**:價格在刪除所有非數字字元後提取為整數,確保其可用於數值運算或比較。 擷取的資料被建構成飛行物件陣列,每個物件都包含上述詳細資訊: ``` const scrappedFlights = await page.evaluate(async () => { // Data extraction logic const flights = []; // Process each flight element // ... return flights; }); ``` #### 錯誤處理和清理 我們的抓取邏輯被包裝在一個 try-catch 區塊中,以在抓取過程中優雅地處理任何潛在的錯誤。無論結果如何,我們都會確保瀏覽器在finally區塊中正確關閉,從而保持資源效率並防止潛在的記憶體洩漏: ``` try { // Scraping logic } catch (error) { console.log({ error }); } finally { await browser.close(); console.log("Browser closed successfully."); } ``` #### 整個程式碼 ``` const SBR_WS_ENDPOINT = process.env.SBR_WS_ENDPOINT; export const register = async () => { if (process.env.NEXT_RUNTIME === "nodejs") { const { Worker } = await import("bullmq"); const puppeteer = await import("puppeteer"); const { connection } = await import("./lib/redis"); const { importQueue } = await import("./lib/queue"); new Worker( "importQueue", async (job) => { const browser = await puppeteer.connect({ browserWSEndpoint: SBR_WS_ENDPOINT, }); try { const page = await browser.newPage(); console.log("in flight scraping"); console.log("Connected! Navigating to " + job.data.url); await page.goto(job.data.url); console.log("Navigated! Scraping page content..."); const scrappedFlights = await page.evaluate(async () => { await new Promise((resolve) => setTimeout(resolve, 5000)); const flights = []; const flightSelectors = document.querySelectorAll(".nrc6-wrapper"); flightSelectors.forEach((flightElement) => { const airlineLogo = flightElement.querySelector("img")?.src || ""; const [rawDepartureTime, rawArrivalTime] = ( flightElement.querySelector(".vmXl")?.innerText || "" ).split(" – "); // Function to extract time and remove numeric values at the end const extractTime = (rawTime: string): string => { const timeWithoutNumbers = rawTime .replace(/[0-9+\s]+$/, "") .trim(); return timeWithoutNumbers; }; const departureTime = extractTime(rawDepartureTime); const arrivalTime = extractTime(rawArrivalTime); const flightDuration = ( flightElement.querySelector(".xdW8")?.children[0]?.innerText || "" ).trim(); const airlineName = ( flightElement.querySelector(".VY2U")?.children[1]?.innerText || "" ).trim(); // Extract price const price = parseInt( ( flightElement.querySelector(".f8F1-price-text")?.innerText || "" ) .replace(/[^\d]/g, "") .trim(), 10 ); flights.push({ airlineLogo, departureTime, arrivalTime, flightDuration, airlineName, price, }); }); return flights; }); } catch (error) { console.log({ error }); } finally { await browser.close(); console.log("Browser closed successfully."); } }, { connection, concurrency: 10, removeOnComplete: { count: 1000 }, removeOnFail: { count: 5000 }, } ); } }; ``` ### 步驟8:航班搜尋功能 基於我們的航班資料抓取功能,讓我們將全面的航班搜尋功能整合到我們的 Next.js 應用程式中。此功能將為使用者提供一個動態介面,透過指定出發地、目的地和日期來搜尋航班。利用強大的 Next.js 框架以及現代 UI 庫和狀態管理,我們建立了引人入勝且響應迅速的航班搜尋體驗。 #### 航班搜尋功能的關鍵組成部分 1. **動態城市選擇**:此功能包括來源和目的地輸入的自動完成功能,由預先定義的城市機場程式碼清單提供支援。當使用者輸入時,應用程式會過濾並顯示匹配的城市,透過更輕鬆地尋找和選擇機場來增強用戶體驗。 2. **日期選擇**:使用者可以透過日期輸入選擇預期的航班日期,為規劃旅行提供彈性。 3. **抓取狀態監控**:啟動抓取作業後,應用程式透過定期 API 呼叫來監控作業的狀態。這種非同步檢查允許應用程式使用抓取過程的狀態更新 UI,確保使用者了解進度和結果。 #### 航班搜尋元件的完整程式碼 ``` "use client"; import { useAppStore } from "@/store"; import { USER_API_ROUTES } from "@/utils/api-routes"; import { cityAirportCode } from "@/utils/city-airport-codes"; import { Button, Input, Listbox, ListboxItem } from "@nextui-org/react"; import axios from "axios"; import Image from "next/image"; import { useRouter } from "next/navigation"; import React, { useEffect, useRef, useState } from "react"; import { FaCalendarAlt, FaSearch } from "react-icons/fa"; const SearchFlights = () => { const router = useRouter(); const { setScraping, setScrapingType, setScrappedFlights } = useAppStore(); const [loadingJobId, setLoadingJobId] = useState<number | undefined>(undefined); const [source, setSource] = useState(""); const [sourceOptions, setSourceOptions] = useState< { city: string; code: string; }[] >([]); const [destination, setDestination] = useState(""); const [destinationOptions, setDestinationOptions] = useState< { city: string; code: string; }[] >([]); const [flightDate, setFlightDate] = useState(() => { const today = new Date(); return today.toISOString().split("T")[0]; }); const handleSourceChange = (query: string) => { const matchingCities = Object.entries(cityAirportCode) .filter(([, city]) => city.toLowerCase().includes(query.toLowerCase())) .map(([code, city]) => ({ code, city })) .splice(0, 5); setSourceOptions(matchingCities); }; const destinationChange = (query: string) => { const matchingCities = Object.entries(cityAirportCode) .filter(([, city]) => city.toLowerCase().includes(query.toLowerCase())) .map(([code, city]) => ({ code, city })) .splice(0, 5); setDestinationOptions(matchingCities); }; const startScraping = async () => { if (source && destination && flightDate) { const data = await axios.get(`${USER_API_ROUTES.FLIGHT_SCRAPE}?source=${source}&destination=${destination}&date=${flightDate}`); if (data.data.id) { setLoadingJobId(data.data.id); setScraping(true); setScrapingType("flight"); } } }; useEffect(() => { if (loadingJobId) { const checkIfJobCompleted = async () => { try { const response = await axios.get(`${USER_API_ROUTES.FLIGHT_SCRAPE_STATUS}?jobId=${loadingJobId}`); if (response.data.status) { set ScrappedFlights(response.data.flights); clearInterval(jobIntervalRef.current); setScraping(false); setScrapingType(undefined); router.push(`/flights?data=${flightDate}`); } } catch (error) { console.log(error); } }; jobIntervalRef.current = setInterval(checkIfJobCompleted, 3000); } return () => clearInterval(jobIntervalRef.current); }, [loadingJobId]); return ( <div className="h-[90vh] flex items-center justify-center"> <div className="absolute left-0 top-0 h-[100vh] w-[100vw] max-w-[100vw] overflow-hidden overflow-x-hidden"> <Image src="/flight-search.png" fill alt="Search" /> </div> <div className="absolute h-[50vh] w-[60vw] flex flex-col gap-5"> {/* UI and functionality for flight search */} </div> </div> ); }; export default SearchFlights; ``` ### 步驟9:航班搜尋頁面UI ![航班搜尋頁面](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/54bkhpea27fkzg4vlur1.png) ### 顯示航班搜尋結果 成功抓取飛行資料後,下一個關鍵步驟是以使用者友善的方式將這些結果呈現給使用者。 Next.js 應用程式中的 Flights 元件就是為此目的而設計的。 ``` "use client"; import { useAppStore } from "@/store"; import { USER_API_ROUTES } from "@/utils/api-routes"; import { Button } from "@nextui-org/react"; import axios from "axios"; import Image from "next/image"; import { useRouter, useSearchParams } from "next/navigation"; import React from "react"; import { FaChevronLeft } from "react-icons/fa"; import { MdOutlineFlight } from "react-icons/md"; const Flights = () => { const router = useRouter(); const searchParams = useSearchParams(); const date = searchParams.get("date"); const { scrappedFlights, userInfo } = useAppStore(); const getRandomNumber = () => Math.floor(Math.random() * 41); const bookFLight = async (flightId: number) => {}; return ( <div className="m-10 px-[20vw] min-h-[80vh]"> <Button className="my-5" variant="shadow" color="primary" size="lg" onClick={() => router.push("/search-flights")} > <FaChevronLeft /> Go Back </Button> <div className="flex-col flex gap-5"> {scrappedFlights.length === 0 && ( <div className="flex items-center justify-center py-5 px-10 mt-10 rounded-lg text-red-500 bg-red-100 font-medium"> No Flights found. </div> )} {scrappedFlights.map((flight: any) => { const seatsLeft = getRandomNumber(); return ( <div key={flight.id} className="grid grid-cols-12 border bg-gray-200 rounded-xl font-medium drop-shadow-md" > <div className="col-span-9 bg-white rounded-l-xl p-10 flex flex-col gap-5"> <div className="grid grid-cols-4 gap-4"> <div className="flex flex-col gap-3 font-medium"> <div> <div className="relative w-20 h-16"> <Image src={flight.logo} alt="airline name" fill /> </div> </div> <div>{flight.name}</div> </div> <div className="col-span-3 flex justify-between"> <div className="flex flex-col gap-2"> <div className="text-blue-600">From</div> <div> <span className="text-3xl"> <strong>{flight.departureTime}</strong> </span> </div> <div>{flight.from}</div> </div> <div className="flex flex-col items-center justify-center gap-2"> <div className="bg-violet-100 w-max p-3 text-4xl text-blue-600 rounded-full"> <MdOutlineFlight /> </div> <div> <span className="text-lg"> <strong>Non-stop</strong> </span> </div> <div>{flight.duration}</div> </div> <div className="flex flex-col gap-2"> <div className="text-blue-600">To</div> <div> <span className="text-3xl"> <strong>{flight.arrivalTime}</strong> </span> </div> <div>{flight.to}</div> </div> </div> </div> <div className="flex justify-center gap-10 bg-violet-100 p-3 rounded-lg"> <div className="flex"> <span>Airplane  </span> <span className="text-blue-600 font-semibold"> Boeing 787 </span> </div> <div className="flex"> <span>Travel Class:  </span> <span className="text-blue-600 font-semibold">Economy</span> </div> </div> <div className="flex justify-between font-medium"> <div> Refundable <span className="text-blue-600"> $5 ecash</span> </div> <div className={`${ seatsLeft > 20 ? "text-green-500" : "text-red-500" }`} > Only {seatsLeft} Seats Left </div> <div className="cursor-pointer">Flight Details</div> </div> </div> <div className="col-span-3 bg-violet-100 rounded-r-xl h-full flex flex-col items-center justify-center gap-5"> <div> <div> <span className="line-through font-light"> ${flight.price + 140} </span> </div> <div className="flex items-center gap-2"> <span className="text-5xl font-bold">${flight.price}</span> <span className="text-blue-600">20% OFF</span> </div> </div> <Button variant="ghost" radius="full" size="lg" color="primary" onClick={() => { if (userInfo) bookFLight(flight.id); }} > {userInfo ? "Book Now" : "Login to Book"} </Button> </div> </div> ); })} </div> </div> ); }; export default Flights; ``` #### 航班搜尋結果 ![航班搜尋結果](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cxufusoyrf6hgtrrcmsw.png) ### 探索完整的指南和程式碼庫 上面共享的部分和程式碼片段僅代表使用 Next.js 建立強大的航班資料抓取和搜尋應用程式所需的完整功能和程式碼的一小部分。為了掌握這個專案的全部內容,包括高級功能、優化和最佳實踐,我邀請您更深入地研究我的線上綜合資源。 #### 在 YouTube 上觀看詳細說明 有關引導您完成此應用程式的開發過程、編碼細微差別和功能的逐步影片指南,請觀看我的 YouTube 影片。本教程旨在讓您更深入地了解這些概念,讓您按照自己的步調進行操作並獲得對 Next.js 應用程式開發的寶貴見解。 https://www.youtube.com/watch?v=ZWVhk0fxHM0 #### 在 GitHub 上探索完整程式碼 如果您渴望探索完整的程式碼,請造訪我的 GitHub 儲存庫。在那裡,您將找到完整的程式碼庫,包括讓該應用程式在您自己的電腦上執行所需的所有元件、實用程式和設定說明。 https://github.com/koolkishan/nextjs-travel-planner ### 結論 使用 Next.js 建立飛行資料抓取和搜尋工具等綜合應用程式展示了現代 Web 開發工具和框架的強大功能和多功能性。無論您是希望提高技能的經驗豐富的開發人員,還是渴望深入 Web 開發的初學者,這些資源都是為您的旅程量身定制的。在 YouTube 上觀看詳細教程,在 GitHub 上探索完整程式碼,並加入對話以增強您的開發專業知識並為充滿活力的開發者社群做出貢獻。 --- 原文出處:https://dev.to/kishansheth/nextjs-14-booking-app-with-live-data-scraping-using-scraping-browser-610

70 個 JavaScript 面試問題

嗨大家好,新年快樂:煙火::煙火::煙火:! ---------------------- 這是一篇很長的文章,所以請耐心聽我一秒鐘或一個小時。每個問題的每個答案都有一個向上箭頭**↑**連結,可讓您返回到問題列表,這樣您就不會浪費時間上下滾動。 ### 問題 - [1. `undefined`和`null`有什麼差別?](#1-whats-the-difference-between-undefined-and-null) - [2. &amp;&amp; 運算子的作用是什麼?](#2-what-does-the-ampamp-operator-do) - [3. || 是什麼意思?運營商做什麼?](#3-what-does-the-operator-do) - [4. 使用 + 或一元加運算子是將字串轉換為數字的最快方法嗎?](#4-is-using-the-or-unary-plus-operator-the-fastest-way-in-converting-a-string-to-a-number) - [5.什麼是DOM?](#5-what-is-the-dom) - [6.什麼是事件傳播?](#6-what-is-event-propagation) - [7.什麼是事件冒泡?](#7-whats-event-bubbling) - [8. 什麼是事件擷取?](#8-whats-event-capturing) - [9. `event.preventDefault()`和`event.stopPropagation()`方法有什麼差別?](#9-whats-the-difference-between-eventpreventdefault-and-eventstoppropagation-methods) - [10. 如何知道元素中是否使用了`event.preventDefault()`方法?](#10-how-to-know-if-the-eventpreventdefault-method-was-used-in-an-element) - [11. 為什麼這段程式碼 obj.someprop.x 會拋出錯誤?](#11-why-does-this-code-objsomepropx-throw-an-error) - \[12.什麼是`event.target` ?\](#12-什麼是 eventtarget- ) - [13.什麼是`event.currentTarget` ?](#13-what-is-eventcurrenttarget) - [14. `==`和`===`有什麼差別?](#14-whats-the-difference-between-and-) - [15. 為什麼在 JavaScript 中比較兩個相似的物件時回傳 false?](#15-why-does-it-return-false-when-comparing-two-similar-objects-in-javascript) - [16. `!!`是什麼意思?運營商做什麼?](#16-what-does-the-operator-do) - [17. 如何計算一行中的多個表達式?](#17-how-to-evaluate-multiple-expressions-in-one-line) - [18.什麼是吊裝?](#18-what-is-hoisting) - [19.什麼是範圍?](#19-what-is-scope) - [20.什麼是閉包?](#20-what-are-closures) - [21. JavaScript 中的假值是什麼?](#21-what-are-the-falsy-values-in-javascript) - [22. 如何檢查一個值是否為假值?](#22-how-to-check-if-a-value-is-falsy) - [23. `"use strict"`有什麼作用?](#23-what-does-use-strict-do) - [24. JavaScript 中`this`的值是什麼?](#24-whats-the-value-of-this-in-javascript) - [25. 物件的`prototype`是什麼?](#25-what-is-the-prototype-of-an-object) - \[26.什麼是 IIFE,它有什麼用?\](#26-what-is-an-iife-what-is-the-use-of-it ) - [27. `Function.prototype.apply`方法有什麼用?](#27-what-is-the-use-functionprototypeapply-method) - [28. `Function.prototype.call`方法有什麼用?](#28-what-is-the-use-functionprototypecall-method) - [29. `Function.prototype.apply`和`Function.prototype.call`有什麼差別?](#29-whats-the-difference-between-functionprototypeapply-and-functionprototypecall) - [30. `Function.prototype.bind`的用法是什麼?](#30-what-is-the-usage-of-functionprototypebind) - \[31.什麼是函數式程式設計以及 JavaScript 的哪些特性使其成為函數式語言的候選者?\](#31-什麼是函數式程式設計和 javascript 的特性是什麼-使其成為函數式語言的候選者 ) - [32.什麼是高階函數?](#32-what-are-higher-order-functions) - [33.為什麼函數被稱為First-class Objects?](#33-why-are-functions-called-firstclass-objects) - \[34.手動實作`Array.prototype.map`方法。\](#34-手動實作 arrayprototypemap-method ) - [35. 手動實作`Array.prototype.filter`方法。](#35-implement-the-arrayprototypefilter-method-by-hand) - [36. 手動實作`Array.prototype.reduce`方法。](#36-implement-the-arrayprototypereduce-method-by-hand) - [37.什麼是`arguments`物件?](#37-what-is-the-arguments-object) - [38. 如何創造沒有**原型的**物件?](#38-how-to-create-an-object-without-a-prototype) - [39. 為什麼當你呼叫這個函數時,這段程式碼中的`b`會變成全域變數?](#39-why-does-b-in-this-code-become-a-global-variable-when-you-call-this-function) - [40.什麼是**ECMAScript** ?](#40-what-is-ecmascript) - [41. **ES6**或**ECMAScript 2015**有哪些新功能?](#41-what-are-the-new-features-in-es6-or-ecmascript-2015) - [42. `var` 、 `let`和`const`關鍵字有什麼差別?](#42-whats-the-difference-between-var-let-and-const-keywords) - [43. 什麼是**箭頭函數**?](#43-what-are-arrow-functions) - [44.什麼是**類別**?](#44-what-are-classes) - [45.什麼是**模板文字**?](#45-what-are-template-literals) - [46.什麼是**物件解構**?](#46-what-is-object-destructuring) - [47.什麼是`ES6 Modules` ?](#47-what-are-es6-modules) - [48.什麼是`Set`物件以及它如何運作?](#48-what-is-the-set-object-and-how-does-it-work) - [49. 什麼是回呼函數?](#49-what-is-a-callback-function) - [50. 什麼是**Promise** ?](#50-what-are-promises) - [51. 什麼是*async/await*以及它是如何運作的?](#51-what-is-asyncawait-and-how-does-it-work) - [52. **Spread 運算子**和**Rest 運算**子有什麼差別?](#52-whats-the-difference-between-spread-operator-and-rest-operator) - [53. 什麼是**預設參數**?](#53-what-are-default-parameters) - [54.什麼是**包裝物件**?](#54-what-are-wrapper-objects) - [55.**隱性強制**和**顯性**強制有什麼差別?](#55-what-is-the-difference-between-implicit-and-explicit-coercion) - [56. 什麼是`NaN` ?以及如何檢查值是否為`NaN` ?](#56-what-is-nan-and-how-to-check-if-a-value-is-nan) - [57. 如何檢查一個值是否為一個**陣列**?](#57-how-to-check-if-a-value-is-an-array) - [58. 如何在不使用`%`或模運算子的情況下檢查數字是否為偶數?](#58-how-to-check-if-a-number-is-even-without-using-the-or-modulo-operator) - [59. 如何檢查物件中是否存在某個屬性?](#59-how-to-check-if-a-certain-property-exists-in-an-object) - [60.什麼是**AJAX** ?](#60-what-is-ajax) - [61. JavaScript 中建立物件的方式有哪些?](#61-what-are-the-ways-of-making-objects-in-javascript) - [62. `Object.seal`和`Object.freeze`方法有什麼不同?](#62-whats-the-difference-between-objectseal-and-objectfreeze-methods) - [63. `in`運算子和物件中的`hasOwnProperty`方法有什麼差別?](#63-whats-the-difference-between-the-in-operator-and-the-hasownproperty-method-in-objects) - [64. JavaScript中處理**非同步程式碼的**方法有哪些?](#64-what-are-the-ways-to-deal-with-asynchronous-code-in-javasscript) - [65.**函數表達式**和**函數宣告**有什麼不同?](#65-whats-the-difference-between-a-function-expression-and-function-declaration) - \[66.一個函數有多少種*呼叫*方式?\]( 66-函數可以有多少種方式被呼叫) ================= - [67. 什麼是*記憶*,它有什麼用?](#67-what-is-memoization-and-whats-the-use-it) - [68. 實現記憶輔助功能。](#68-implement-a-memoization-helper-function) - [69. 為什麼`typeof null`回傳`object` ?如何檢查一個值是否為`null` ?](#69-why-does-typeof-null-return-object-how-to-check-if-a-value-is-null) - [`new`關鍵字有什麼作用?](#70-what-does-the-new-keyword-do) ### 1. `undefined`和`null`有什麼差別? [^](#the-questions "返回問題")在了解`undefined`和`null`之間的差異之前,我們必須先了解它們之間的相似之處。 - 它們屬於**JavaScript 的**7 種基本型別。 ``` let primitiveTypes = ['string','number','null','undefined','boolean','symbol', 'bigint']; ``` - 它們是**虛假的**價值觀。使用`Boolean(value)`或`!!value`將其轉換為布林值時計算結果為 false 的值。 ``` console.log(!!null); //logs false console.log(!!undefined); //logs false console.log(Boolean(null)); //logs false console.log(Boolean(undefined)); //logs false ``` 好吧,我們來談談差異。 - `undefined`是尚未指派特定值的變數的預設值。或一個沒有**明確**回傳值的函數。 `console.log(1)` 。或物件中不存在的屬性。 JavaScript 引擎為我們完成了**指派**`undefined`值的任務。 ``` let _thisIsUndefined; const doNothing = () => {}; const someObj = { a : "ay", b : "bee", c : "si" }; console.log(_thisIsUndefined); //logs undefined console.log(doNothing()); //logs undefined console.log(someObj["d"]); //logs undefined ``` - `null`是**「代表無值的值」** 。 `null`是已**明確**定義給變數的值。在此範例中,當`fs.readFile`方法未引發錯誤時,我們得到`null`值。 ``` fs.readFile('path/to/file', (e,data) => { console.log(e); //it logs null when no error occurred if(e){ console.log(e); } console.log(data); }); ``` 當比較`null`和`undefined`時,使用`==`時我們得到`true` ,使用`===`時得到`false` 。您可以[在此處](#14-whats-the-difference-between-and-)閱讀原因。 ``` console.log(null == undefined); // logs true console.log(null === undefined); // logs false ``` ### 2. `&&`運算子的作用是什麼? [^](#the-questions "返回問題") `&&`或**邏輯 AND**運算子在其運算元中尋找第一個*假*表達式並傳回它,如果沒有找到任何*假*表達式,則傳回最後一個表達式。它採用短路來防止不必要的工作。在我的一個專案中關閉資料庫連線時,我在`catch`區塊中使用了它。 ``` console.log(false && 1 && []); //logs false console.log(" " && true && 5); //logs 5 ``` 使用**if**語句。 ``` const router: Router = Router(); router.get('/endpoint', (req: Request, res: Response) => { let conMobile: PoolConnection; try { //do some db operations } catch (e) { if (conMobile) { conMobile.release(); } } }); ``` 使用**&amp;&amp;**運算子。 ``` const router: Router = Router(); router.get('/endpoint', (req: Request, res: Response) => { let conMobile: PoolConnection; try { //do some db operations } catch (e) { conMobile && conMobile.release() } }); ``` ### 3. `||`是什麼意思?運營商做什麼? [↑](#the-questions "返回問題") `||` or**邏輯 OR**運算子尋找其運算元中的第一個*真值*表達式並傳回它。這也採用短路來防止不必要的工作。在**ES6預設函數參數**被支援之前,它被用來初始化函數中的預設參數值。 ``` console.log(null || 1 || undefined); //logs 1 function logName(name) { var n = name || "Mark"; console.log(n); } logName(); //logs "Mark" ``` ### 4. 使用**+**或一元加運算子是將字串轉換為數字的最快方法嗎? [^](#the-questions "返回問題")根據[MDN 文件,](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Arithmetic_Operators#Unary_plus) `+`是將字串轉換為數字的最快方法,因為如果該值已經是數字,它不會對該值執行任何操作。 ### 5.什麼是**DOM** ? [^](#the-questions "返回問題") **DOM**代表**文件物件模型,**是 HTML 和 XML 文件的介面 ( **API** )。當瀏覽器第一次讀取(*解析*)我們的 HTML 文件時,它會建立一個大物件,一個基於 HTML 文件的非常大的物件,這就是**DOM** 。它是根據 HTML 文件建模的樹狀結構。 **DOM**用於互動和修改**DOM 結構**或特定元素或節點。 想像一下,如果我們有這樣的 HTML 結構。 ``` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document Object Model</title> </head> <body> <div> <p> <span></span> </p> <label></label> <input> </div> </body> </html> ``` 等效的**DOM**應該是這樣的。 ![DOM 等效項](https://thepracticaldev.s3.amazonaws.com/i/mbqphfbjfie45ynj0teo.png) **JavaScript**中的`document`物件代表**DOM** 。它為我們提供了許多方法,我們可以用來選擇元素來更新元素內容等等。 ### 6.什麼是**事件傳播**? [↑](#the-questions "返回問題")當某個**事件**發生在**DOM**元素上時,該**事件**並非完全發生在該元素上。在**冒泡階段**,**事件**向上冒泡,或到達其父級、祖父母、祖父母的父級,直到一直到達`window` ,而在**捕獲階段**,事件從`window`開始向下到達觸發的元素事件或`<a href="#12-what-is-eventtarget-">event.target</a>` 。 **事件傳播**分為**三個**階段。 1. [捕獲階段](#8-whats-event-capturing)-事件從`window`開始,然後向下到達每個元素,直到到達目標元素。 2. [目標階段](#12-what-is-eventtarget-)– 事件已到達目標元素。 3. [冒泡階段](#7-whats-event-bubbling)-事件從目標元素冒起,然後向上移動到每個元素,直到到達`window` 。 ![事件傳播](https://thepracticaldev.s3.amazonaws.com/i/hjayqa99iejfhbsujlqd.png) ### 7.什麼是**事件冒泡**? [↑](#the-questions "返回問題")當某個**事件**發生在**DOM**元素上時,該**事件**並非完全發生在該元素上。在**冒泡階段**,**事件**向上冒泡,或到達其父級、祖父母、祖父母的父級,直到一直到達`window` 。 如果我們有一個像這樣的範例標記。 ``` <div class="grandparent"> <div class="parent"> <div class="child">1</div> </div> </div> ``` 還有我們的js程式碼。 ``` function addEvent(el, event, callback, isCapture = false) { if (!el || !event || !callback || typeof callback !== 'function') return; if (typeof el === 'string') { el = document.querySelector(el); }; el.addEventListener(event, callback, isCapture); } addEvent(document, 'DOMContentLoaded', () => { const child = document.querySelector('.child'); const parent = document.querySelector('.parent'); const grandparent = document.querySelector('.grandparent'); addEvent(child, 'click', function (e) { console.log('child'); }); addEvent(parent, 'click', function (e) { console.log('parent'); }); addEvent(grandparent, 'click', function (e) { console.log('grandparent'); }); addEvent(document, 'click', function (e) { console.log('document'); }); addEvent('html', 'click', function (e) { console.log('html'); }) addEvent(window, 'click', function (e) { console.log('window'); }) }); ``` `addEventListener`方法有第三個可選參數**useCapture ,**預設值為`false`事件將在**冒泡階段**發生,如果為`true` ,事件將在**捕獲階段**發生。如果我們點擊`child`元素,它會分別在**控制台**上記錄`child` 、 `parent`元素、 `grandparent` 、 `html` 、 `document`和`window` 。這就是**事件冒泡**。 ### 8. 什麼是**事件擷取**? [↑](#the-questions "返回問題")當某個**事件**發生在**DOM**元素上時,該**事件**並非完全發生在該元素上。在**捕獲階段**,事件從`window`開始一直到觸發事件的元素。 如果我們有一個像這樣的範例標記。 ``` <div class="grandparent"> <div class="parent"> <div class="child">1</div> </div> </div> ``` 還有我們的js程式碼。 ``` function addEvent(el, event, callback, isCapture = false) { if (!el || !event || !callback || typeof callback !== 'function') return; if (typeof el === 'string') { el = document.querySelector(el); }; el.addEventListener(event, callback, isCapture); } addEvent(document, 'DOMContentLoaded', () => { const child = document.querySelector('.child'); const parent = document.querySelector('.parent'); const grandparent = document.querySelector('.grandparent'); addEvent(child, 'click', function (e) { console.log('child'); }, true); addEvent(parent, 'click', function (e) { console.log('parent'); }, true); addEvent(grandparent, 'click', function (e) { console.log('grandparent'); }, true); addEvent(document, 'click', function (e) { console.log('document'); }, true); addEvent('html', 'click', function (e) { console.log('html'); }, true) addEvent(window, 'click', function (e) { console.log('window'); }, true) }); ``` `addEventListener`方法有第三個可選參數**useCapture ,**預設值為`false`事件將在**冒泡階段**發生,如果為`true` ,事件將在**捕獲階段**發生。如果我們點擊`child`元素,它會分別在**控制台**上記錄`window` 、 `document` 、 `html` 、 `grandparent` 、 `parent`和`child` 。這就是**事件捕獲**。 ### 9. `event.preventDefault()`和`event.stopPropagation()`方法有什麼差別? [↑](#the-questions "返回問題") `event.preventDefault()`方法**阻止**元素的預設行為。如果在`form`元素中使用,它**會阻止**其提交。如果在`anchor`元素中使用,它**會阻止**其導航。如果在`contextmenu`中使用,它**會阻止**其顯示或顯示。而`event.stopPropagation()`方法會停止事件的傳播或停止事件在[冒泡](#7-whats-event-bubbling)或[捕獲](#8-whats-event-capturing)階段發生。 ### 10. 如何知道元素中是否使用了`event.preventDefault()`方法? [↑](#the-questions "返回問題")我們可以使用事件物件中的`event.defaultPrevented`屬性。它傳回一個`boolean` ,指示是否在特定元素中呼叫了`event.preventDefault()` 。 ### 11. 為什麼這段程式碼`obj.someprop.x`會拋出錯誤? ``` const obj = {}; console.log(obj.someprop.x); ``` [^](#the-questions "返回問題")顯然,由於我們嘗試存取 a 的原因,這會引發錯誤 `someprop`屬性中的`x`屬性具有`undefined`值。請記住,物件中的**屬性**本身並不存在,且其**原型**具有預設值`undefined`且`undefined`沒有屬性`x` 。 ### 12.什麼是**event.target** ? [↑](#the-questions "返回問題")最簡單來說, **event.target**是**發生**事件的元素或**觸發**事件的元素。 HTML 標記範例。 ``` <div onclick="clickFunc(event)" style="text-align: center;margin:15px; border:1px solid red;border-radius:3px;"> <div style="margin: 25px; border:1px solid royalblue;border-radius:3px;"> <div style="margin:25px;border:1px solid skyblue;border-radius:3px;"> <button style="margin:10px"> Button </button> </div> </div> </div> ``` JavaScript 範例。 ``` function clickFunc(event) { console.log(event.target); } ``` 如果您單擊按鈕,它會記錄**按鈕**標記,即使我們將事件附加在最外部的`div`上,它也會始終記錄**按鈕**,因此我們可以得出結論, **event.target**是觸發事件的元素。 ### 13.什麼是**event.currentTarget** ? [↑](#the-questions "返回問題") **event.currentTarget**是我們**明確**附加事件處理程序的元素。 複製**問題 12**中的標記。 HTML 標記範例。 ``` <div onclick="clickFunc(event)" style="text-align: center;margin:15px; border:1px solid red;border-radius:3px;"> <div style="margin: 25px; border:1px solid royalblue;border-radius:3px;"> <div style="margin:25px;border:1px solid skyblue;border-radius:3px;"> <button style="margin:10px"> Button </button> </div> </div> </div> ``` 並且稍微改變我們的**JS** 。 ``` function clickFunc(event) { console.log(event.currentTarget); } ``` 如果您按一下該按鈕,即使我們按一下該按鈕,它也會記錄最外層的**div**標記。在此範例中,我們可以得出結論, **event.currentTarget**是我們附加事件處理程序的元素。 ### 14. `==`和`===`有什麼差別? [^](#the-questions "返回問題") `==` \_\_(抽象相等)\_\_ 和`===` \_\_(嚴格相等)\_\_ 之間的區別在於`==`在*強制轉換*後按**值**進行比較,而`===`在不進行*強制轉換的*情況下按**值**和**類型**進行比較。 讓我們更深入地研究`==` 。那麼首先我們來談談*強制*。 *強制轉換*是將一個值轉換為另一種類型的過程。在本例中, `==`進行*隱式強制轉換*。在比較兩個值之前, `==`需要執行一些條件。 假設我們必須比較`x == y`值。 1. 如果`x`和`y`具有相同的類型。 然後將它們與`===`運算子進行比較。 2. 如果`x`為`null`且`y` `undefined` ,則傳回`true` 。 3. 如果`x` `undefined`且`y`為`null`則傳回`true` 。 4. 如果`x`是`number`類型, `y`是`string`類型 然後回傳`x == toNumber(y)` 。 5. 如果`x`是`string`類型, `y`是`number`類型 然後返回`toNumber(x) == y` 。 6. 如果`x`是`boolean`類型 然後返回`toNumber(x) == y` 。 7. 如果`y`是`boolean`類型 然後回傳`x == toNumber(y)` 。 8. 如果`x`是`string` 、 `symbol`或`number`且`y`是 type `object` 然後回傳`x == toPrimitive(y)` 。 9. 如果`x`是`object`且`x`是`string` 、 `symbol` 然後返回`toPrimitive(x) == y` 。 10. 返回`false` 。 **注意:** `toPrimitive`首先使用物件中的`valueOf`方法,然後使用`toString`方法來取得該物件的原始值。 讓我們舉個例子。 | `x` | `y` | `x == y` | | ------------- |:-------------:| ----------------: | | `5` | `5` | `true` | | `1` | `'1'` | `true` | | `null` | `undefined` | `true` | | `0` | `false` | `true` | | `'1,2'` | `[1,2]` | `true` | | `'[object Object]'` | `{}` | `true` | 這些範例都傳回`true` 。 **第一個範例**屬於**條件一**,因為`x`和`y`具有相同的類型和值。 **第二個範例**轉到**條件四,**在比較之前將`y`轉換為`number` 。 **第三個例子**涉及**條件二**。 **第四個範例**轉到**條件七,**因為`y`是`boolean` 。 **第五個範例**適用於**條件八**。使用`toString()`方法將陣列轉換為`string` ,該方法傳回`1,2` 。 **最後一個例子**適用於**條件十**。使用傳回`[object Object]`的`toString()`方法將該物件轉換為`string` 。 | `x` | `y` | `x === y` | | ------------- |:-------------:| ----------------: | | `5` | `5` | `true` | | `1` | `'1'` | `false` | | `null` | `undefined` | `false` | | `0` | `false` | `false` | | `'1,2'` | `[1,2]` | `false` | | `'[object Object]'` | `{}` | `false` | 如果我們使用`===`運算符,則除第一個範例之外的所有比較都將傳回`false` ,因為它們不具有相同的類型,而第一個範例將傳回`true` ,因為兩者俱有相同的類型和值。 ### 15. 為什麼在 JavaScript 中比較兩個相似的物件時回傳**false** ? [^](#the-questions "返回問題")假設我們有下面的例子。 ``` let a = { a: 1 }; let b = { a: 1 }; let c = a; console.log(a === b); // logs false even though they have the same property console.log(a === c); // logs true hmm ``` **JavaScript**以不同的方式比較*物件*和*基元*。在*基元*中,它透過**值**來比較它們,而在*物件*中,它透過**引用**或**儲存變數的記憶體位址**來比較它們。這就是為什麼第一個`console.log`語句回傳`false`而第二個`console.log`語句回傳`true`的原因。 `a`和`c`有相同的引用,而`a`和`b`則不同。 ### 16. **!!**是什麼意思?運營商做什麼? [↑](#the-questions "返回問題")**雙非**運算子或**!!**將右側的值強制轉換為布林值。基本上,這是一種將值轉換為布林值的奇特方法。 ``` console.log(!!null); //logs false console.log(!!undefined); //logs false console.log(!!''); //logs false console.log(!!0); //logs false console.log(!!NaN); //logs false console.log(!!' '); //logs true console.log(!!{}); //logs true console.log(!![]); //logs true console.log(!!1); //logs true console.log(!![].length); //logs false ``` ### 17. 如何計算一行中的多個表達式? [↑](#the-questions "返回問題")我們可以使用`,`或逗號運算子來計算一行中的多個表達式。它從左到右計算並傳回右側最後一項或最後一個操作數的值。 ``` let x = 5; x = (x++ , x = addFive(x), x *= 2, x -= 5, x += 10); function addFive(num) { return num + 5; } ``` 如果記錄`x`的值,它將是**27** 。首先,我們**增加**x 的值,它將是**6** ,然後我們呼叫函數`addFive(6)`並將 6 作為參數傳遞,並將結果分配給`x` , `x`的新值將是**11** 。之後,我們將`x`的當前值乘以**2**並將其分配給`x` , `x`的更新值將是**22** 。然後,我們將`x`的當前值減去 5 並將結果指派給`x` ,更新後的值將是**17** 。最後,我們將`x`的值增加 10 並將更新後的值指派給`x` ,現在`x`的值將是**27** 。 ### 18.什麼是**吊裝**? [^](#the-questions "返回問題")**提升**是一個術語,用於描述將*變數*和*函數*移動到其*(全域或函數)*作用域的頂部(即我們定義該變數或函數的位置)。 要理解**提升**,我必須解釋*執行上下文*。 **執行上下文**是目前正在執行的「程式碼環境」。**執行上下文**有兩個階段*:編譯*和*執行*。 **編譯**- 在此階段,它獲取所有*函數聲明*並將它們*提升*到作用域的頂部,以便我們稍後可以引用它們並獲取所有*變數聲明***(使用 var 關鍵字聲明)** ,並將它們*提升*並給它們一個默認值*未定義*的 . **執行**- 在此階段,它將值指派給先前*提升的*變數,並*執行*或*呼叫*函數**(物件中的方法)** 。 **注意:**只有使用*var*關鍵字宣告的**函數宣告**和變數才會*被提升*,而不是**函數表達式**或**箭頭函數**、 `let`和`const`關鍵字。 好吧,假設我們在下面的*全域範圍*內有一個範例程式碼。 ``` console.log(y); y = 1; console.log(y); console.log(greet("Mark")); function greet(name){ return 'Hello ' + name + '!'; } var y; ``` 此程式碼記錄`undefined` , `1` , `Hello Mark!`分別。 所以*編譯*階段看起來像這樣。 ``` function greet(name) { return 'Hello ' + name + '!'; } var y; //implicit "undefined" assignment //waiting for "compilation" phase to finish //then start "execution" phase /* console.log(y); y = 1; console.log(y); console.log(greet("Mark")); */ ``` 出於範例目的,我對變數和*函數呼叫*的*賦值*進行了評論。 *編譯*階段完成後,它開始*執行*階段,呼叫方法並向變數賦值。 ``` function greet(name) { return 'Hello ' + name + '!'; } var y; //start "execution" phase console.log(y); y = 1; console.log(y); console.log(greet("Mark")); ``` ### 19.什麼是**範圍**? [↑](#the-questions "返回問題") JavaScript 中的**作用域**是我們可以有效存取變數或函數的**區域**。 JavaScript 有三種類型的作用域。**全域作用域**、**函數作用域**和**區塊作用域(ES6)** 。 - **全域作用域**- 在全域命名空間中宣告的變數或函數位於全域作用域中,因此可以在程式碼中的任何位置存取。 ``` //global namespace var g = "global"; function globalFunc(){ function innerFunc(){ console.log(g); // can access "g" because "g" is a global variable } innerFunc(); } ``` - **函數作用域**- 函數內聲明的變數、函數和參數可以在該函數內部存取,但不能在函數外部存取。 ``` function myFavoriteFunc(a) { if (true) { var b = "Hello " + a; } return b; } myFavoriteFunc("World"); console.log(a); // Throws a ReferenceError "a" is not defined console.log(b); // does not continue here ``` - **區塊作用域**- 在區塊`{}`內宣告的變數**( `let` 、 `const` )**只能在區塊內存取。 ``` function testBlock(){ if(true){ let z = 5; } return z; } testBlock(); // Throws a ReferenceError "z" is not defined ``` **範圍**也是一組查找變數的規則。如果一個變數在**當前作用域中**不存在,它會在**外部作用域中查找**並蒐索該變數,如果不存在,它會再次**查找,**直到到達**全域作用域。**如果該變數存在,那麼我們可以使用它,如果不存在,我們可以使用它來拋出錯誤。它搜尋**最近的**變數,一旦找到它就停止**搜尋**或**尋找**。這稱為**作用域鏈**。 ``` /* Scope Chain Inside inner function perspective inner's scope -> outer's scope -> global's scope */ //Global Scope var variable1 = "Comrades"; var variable2 = "Sayonara"; function outer(){ //outer's scope var variable1 = "World"; function inner(){ //inner's scope var variable2 = "Hello"; console.log(variable2 + " " + variable1); } inner(); } outer(); // logs Hello World // because (variable2 = "Hello") and (variable1 = "World") are the nearest // variables inside inner's scope. ``` ![範圍](https://thepracticaldev.s3.amazonaws.com/i/l81b3nmdonimex0qsgyr.png) ### 20.什麼是**閉包**? [^](#the-questions "返回問題")這可能是所有這些問題中最難的問題,因為**閉包**是一個有爭議的話題。那我就從我的理解來解釋。 **閉包**只是函數在宣告時記住其當前作用域、其父函數作用域、其父函數的父函數作用域上的變數和參數的引用的能力,直到在**作用域鏈**的幫助下到達全域作用域。基本上它是聲明函數時建立的**作用域**。 例子是解釋閉包的好方法。 ``` //Global's Scope var globalVar = "abc"; function a(){ //testClosures's Scope console.log(globalVar); } a(); //logs "abc" /* Scope Chain Inside a function perspective a's scope -> global's scope */ ``` 在此範例中,當我們宣告`a`函數時**,全域**作用域是`a's`*閉包*的一部分。 ![a的閉包](https://thepracticaldev.s3.amazonaws.com/i/teatokuw4xvgtlzbzhn8.png) 變數`globalVar`在影像中沒有值的原因是該變數的值可以根據我們呼叫`a`**位置**和**時間**而改變。 但在上面的範例中, `globalVar`變數的值為**abc** 。 好吧,讓我們來看一個複雜的例子。 ``` var globalVar = "global"; var outerVar = "outer" function outerFunc(outerParam) { function innerFunc(innerParam) { console.log(globalVar, outerParam, innerParam); } return innerFunc; } const x = outerFunc(outerVar); outerVar = "outer-2"; globalVar = "guess" x("inner"); ``` ![複雜的](https://thepracticaldev.s3.amazonaws.com/i/e4hxm7zvz8eun2ppenwp.png) 這將列印“猜測外部內部”。對此的解釋是,當我們呼叫`outerFunc`函數並將`innerFunc`函數的回傳值指派給變數`x`時,即使我們將新值**outer-2**指派給`outerVar`變數, `outerParam`也會具有**outer**值,因為 重新分配發生在呼叫`outer`函數之後,當我們呼叫`outerFunc`函數時,它會在**作用域鏈**中尋找`outerVar`的值,而`outerVar`的值為**「outer」** 。現在,當我們呼叫引用了`innerFunc`的`x`變數時, `innerParam`的值為**inner,**因為這是我們在呼叫中傳遞的值,而`globalVar`變數的值為**猜測**,因為在呼叫`x`變數之前,我們為`globalVar`分配了一個新值,並且在呼叫`x`時**作用域鏈**中`globalVar`的值是**猜測**。 我們有一個例子來示範沒有正確理解閉包的問題。 ``` const arrFuncs = []; for(var i = 0; i < 5; i++){ arrFuncs.push(function (){ return i; }); } console.log(i); // i is 5 for (let i = 0; i < arrFuncs.length; i++) { console.log(arrFuncs[i]()); // all logs "5" } ``` 由於**Closures**的原因,此程式碼無法按我們的預期工作。 `var`關鍵字建立一個全域變數,當我們推送一個函數時 我們返回全域變數`i` 。因此,當我們在循環之後呼叫該陣列中的其中一個函數時,它會記錄`5` ,因為我們得到 `i`的目前值為`5` ,我們可以存取它,因為它是全域變數。因為**閉包**保留該變數的**引用,**而不是其建立時的**值**。我們可以使用**IIFES**或將`var`關鍵字變更為`let`來解決此問題,以實現區塊作用域。 ### 21. **JavaScript**中的**假**值是什麼? [↑](#the-questions "返回問題") ``` const falsyValues = ['', 0, null, undefined, NaN, false]; ``` **假**值是轉換為布林值時變成**false 的**值。 ### 22. 如何檢查一個值是否為**假值**? [↑](#the-questions "返回問題")使用**布林**函數或雙非運算符**[!!](#16-what-does-the-operator-do)** ### 23. `"use strict"`有什麼作用? [^](#the-questions "返回問題") `"use strict"`是**JavaScript**中的 ES5 功能,它使我們的程式碼在*函數*或*整個腳本*中處於**嚴格模式**。**嚴格模式**幫助我們避免程式碼早期出現**錯誤**並為其加入限制。 **嚴格模式**給我們的限制。 - 分配或存取未宣告的變數。 ``` function returnY(){ "use strict"; y = 123; return y; } ``` - 為唯讀或不可寫的全域變數賦值; ``` "use strict"; var NaN = NaN; var undefined = undefined; var Infinity = "and beyond"; ``` - 刪除不可刪除的屬性。 ``` "use strict"; const obj = {}; Object.defineProperty(obj, 'x', { value : '1' }); delete obj.x; ``` - 參數名稱重複。 ``` "use strict"; function someFunc(a, b, b, c){ } ``` - 使用**eval**函數建立變數。 ``` "use strict"; eval("var x = 1;"); console.log(x); //Throws a Reference Error x is not defined ``` - **該**值的預設值是`undefined` 。 ``` "use strict"; function showMeThis(){ return this; } showMeThis(); //returns undefined ``` **嚴格模式**的限制遠不止這些。 ### 24. JavaScript 中`this`的值是什麼? [↑](#the-questions "返回問題")基本上, `this`是指目前正在執行或呼叫函數的物件的值。我說**目前**是因為**它**的值會根據我們使用它的上下文和使用它的位置而改變。 ``` const carDetails = { name: "Ford Mustang", yearBought: 2005, getName(){ return this.name; }, isRegistered: true }; console.log(carDetails.getName()); // logs Ford Mustang ``` 這是我們通常所期望的,因為在**getName**方法中我們傳回`this.name` ,在此上下文中`this`指的是`carDetails`物件,該物件目前是正在執行的函數的「擁有者」物件。 好吧,讓我們加入一些程式碼讓它變得奇怪。在`console.log`語句下面加入這三行程式碼 ``` var name = "Ford Ranger"; var getCarName = carDetails.getName; console.log(getCarName()); // logs Ford Ranger ``` 第二個`console.log`語句印製了**「Ford Ranger」**一詞,這很奇怪,因為在我們的第一個`console.log`語句中它印了**「Ford Mustang」** 。原因是`getCarName`方法有一個不同的「擁有者」物件,即`window`物件。在全域作用域中使用`var`關鍵字聲明變數會在`window`物件中附加與變數同名的屬性。請記住,當未使用`"use strict"`時,全域範圍內的`this`指的是`window`物件。 ``` console.log(getCarName === window.getCarName); //logs true console.log(getCarName === this.getCarName); // logs true ``` 本例中的`this`和`window`指的是同一個物件。 解決此問題的一種方法是使用函數中的`<a href="#27-what-is-the-use-functionprototypeapply-method">apply</a>`和`<a href="#28-what-is-the-use-functionprototypecall-method">call</a>`方法。 ``` console.log(getCarName.apply(carDetails)); //logs Ford Mustang console.log(getCarName.call(carDetails)); //logs Ford Mustang ``` `apply`和`call`方法期望第一個參數是一個物件,該物件將是該函數內`this`的值。 **IIFE** (即**立即呼叫函數表達式)** 、在全域作用域中宣告的函數、物件內部方法中的**匿名函數**和內部函數都有一個指向**window**物件的預設**值**。 ``` (function (){ console.log(this); })(); //logs the "window" object function iHateThis(){ console.log(this); } iHateThis(); //logs the "window" object const myFavoriteObj = { guessThis(){ function getThis(){ console.log(this); } getThis(); }, name: 'Marko Polo', thisIsAnnoying(callback){ callback(); } }; myFavoriteObj.guessThis(); //logs the "window" object myFavoriteObj.thisIsAnnoying(function (){ console.log(this); //logs the "window" object }); ``` 如果我們想要取得`myFavoriteObj`物件中的`name`屬性**(Marko Polo)**的值,有兩種方法可以解決這個問題。 首先,我們將`this`的值保存在變數中。 ``` const myFavoriteObj = { guessThis(){ const self = this; //saves the this value to the "self" variable function getName(){ console.log(self.name); } getName(); }, name: 'Marko Polo', thisIsAnnoying(callback){ callback(); } }; ``` 在此圖像中,我們保存`this`的值,該值將是`myFavoriteObj`物件。所以我們可以在`getName`內部函數中存取它。 其次,我們使用**ES6[箭頭函數](#43-what-are-arrow-functions)**。 ``` const myFavoriteObj = { guessThis(){ const getName = () => { //copies the value of "this" outside of this arrow function console.log(this.name); } getName(); }, name: 'Marko Polo', thisIsAnnoying(callback){ callback(); } }; ``` [箭頭函數](#43-what-are-arrow-functions)沒有自己的`this` 。它複製封閉詞法範圍的`this`值,或複製`getName`內部函數外部的`this`值(即`myFavoriteObj`物件)。我們也可以根據[函數的呼叫方式](#66-how-many-ways-can-a-function-be-invoked)來決定`this`的值。 ### 25. 物件的`prototype`是什麼? [↑](#the-questions "返回問題")最簡單的`prototype`是一個物件的*藍圖*。如果目前物件中確實存在它,則將其用作**屬性**和**方法**的後備。這是在物件之間共享屬性和功能的方式。這是 JavaScript**原型繼承**的核心概念。 ``` const o = {}; console.log(o.toString()); // logs [object Object] ``` 即使`o.toString`方法不存在於`o`物件中,它也不會拋出錯誤,而是傳回字串`[object Object]` 。當物件中不存在屬性時,它會尋找其**原型**,如果仍然不存在,則會尋找**原型的原型**,依此類推,直到在**原型鏈**中找到具有相同屬性的屬性。**原型鏈**的末尾在**Object.prototype**之後為`null` 。 ``` console.log(o.toString === Object.prototype.toString); // logs true // which means we we're looking up the Prototype Chain and it reached // the Object.prototype and used the "toString" method. ``` ### 26. 什麼是**IIFE** ,它有什麼用? [^](#the-questions "返回問題") **IIFE**或**立即呼叫函數表達式**是在建立或宣告後將被呼叫或執行的函數。建立**IIFE**的語法是,我們將`function (){}`包裝在括號`()`或**分組運算**子內,以將函數視為表達式,然後用另一個括號`()`呼叫它。所以**IIFE**看起來像這樣`(function(){})()` 。 ``` (function () { }()); (function () { })(); (function named(params) { })(); (() => { })(); (function (global) { })(window); const utility = (function () { return { //utilities }; })(); ``` 這些範例都是有效的**IIFE** 。倒數第二個範例顯示我們可以將參數傳遞給**IIFE**函數。最後一個範例表明我們可以將**IIFE**的結果保存到變數中,以便稍後引用它。 **IIFE**的最佳用途是進行初始化設定功能,並避免與全域範圍內的其他變數**發生命名衝突**或污染全域名稱空間。讓我們舉個例子。 ``` <script src="https://cdnurl.com/somelibrary.js"></script> ``` 假設我們有一個指向庫`somelibrary.js`的連結,該庫公開了我們可以在程式碼中使用的一些全域函數,但該庫有兩個我們不使用`createGraph`和`drawGraph`方法,因為這些方法中有錯誤。我們想要實作我們自己的`createGraph`和`drawGraph`方法。 - 解決這個問題的一種方法是改變腳本的結構。 ``` <script src="https://cdnurl.com/somelibrary.js"></script> <script> function createGraph() { // createGraph logic here } function drawGraph() { // drawGraph logic here } </script> ``` 當我們使用這個解決方案時,我們將覆蓋庫為我們提供的這兩種方法。 - 解決這個問題的另一種方法是更改我們自己的輔助函數的名稱。 ``` <script src="https://cdnurl.com/somelibrary.js"></script> <script> function myCreateGraph() { // createGraph logic here } function myDrawGraph() { // drawGraph logic here } </script> ``` 當我們使用此解決方案時,我們還將這些函數呼叫更改為新函數名稱。 - 另一種方法是使用**IIFE** 。 ``` <script src="https://cdnurl.com/somelibrary.js"></script> <script> const graphUtility = (function () { function createGraph() { // createGraph logic here } function drawGraph() { // drawGraph logic here } return { createGraph, drawGraph } })(); </script> ``` 在此解決方案中,我們建立一個實用程式變數,它是**IIFE**的結果,它傳回一個包含`createGraph`和`drawGraph`兩個方法的物件。 **IIFE**解決的另一個問題就是這個例子。 ``` var li = document.querySelectorAll('.list-group > li'); for (var i = 0, len = li.length; i < len; i++) { li[i].addEventListener('click', function (e) { console.log(i); }) } ``` 假設我們有一個`ul`元素,其類別為**list-group** ,並且它有 5 個`li`子元素。當我們**點擊**單一`li`元素時,我們希望`console.log` `i`的值。 但我們想要的程式碼中的行為不起作用。相反,它會在對`li`元素的任何**點擊**中記錄`5` 。我們遇到的問題是由於**閉包的**工作方式造成的。**閉包**只是函數記住其當前作用域、其父函數作用域和全域作用域中的變數引用的能力。當我們在全域範圍內使用`var`關鍵字聲明變數時,顯然我們正在建立一個全域變數`i` 。因此,當我們單擊`li`元素時,它會記錄**5** ,因為這是我們稍後在回調函數中引用它時的`i`值。 - 解決這個問題的一種方法是**IIFE** 。 ``` var li = document.querySelectorAll('.list-group > li'); for (var i = 0, len = li.length; i < len; i++) { (function (currentIndex) { li[currentIndex].addEventListener('click', function (e) { console.log(currentIndex); }) })(i); } ``` 這個解決方案之所以有效,是因為**IIFE**為每次迭代建立一個新範圍,並且我們捕獲`i`的值並將其傳遞到`currentIndex`參數中,因此當我們呼叫**IIFE**時,每次迭代的`currentIndex`值都是不同的。 ### 27. `Function.prototype.apply`方法有什麼用? [^](#the-questions "返回問題") `apply`呼叫一個函數,在呼叫時指定`this`或該函數的「所有者」物件。 ``` const details = { message: 'Hello World!' }; function getMessage(){ return this.message; } getMessage.apply(details); // returns 'Hello World!' ``` 這個方法的工作方式類似於`<a href="#28-what-is-the-use-functionprototypecall-method">Function.prototype.call</a>`唯一的差異是我們傳遞參數的方式。在`apply`中,我們將參數作為陣列傳遞。 ``` const person = { name: "Marko Polo" }; function greeting(greetingMessage) { return `${greetingMessage} ${this.name}`; } greeting.apply(person, ['Hello']); // returns "Hello Marko Polo!" ``` ### 28. `Function.prototype.call`方法有什麼用? [^](#the-questions "返回問題")此`call`呼叫一個函數,指定呼叫時該函數的`this`或「擁有者」物件。 ``` const details = { message: 'Hello World!' }; function getMessage(){ return this.message; } getMessage.call(details); // returns 'Hello World!' ``` 這個方法的工作方式類似於`<a href="#27-what-is-the-use-functionprototypeapply-method">Function.prototype.apply</a>`唯一的差異是我們傳遞參數的方式。在`call`中,我們直接傳遞參數,對於每個參數`,`用逗號分隔它們。 ``` const person = { name: "Marko Polo" }; function greeting(greetingMessage) { return `${greetingMessage} ${this.name}`; } greeting.call(person, 'Hello'); // returns "Hello Marko Polo!" ``` ### 29. `Function.prototype.apply`和`Function.prototype.call`有什麼差別? [↑](#the-questions "返回問題") `apply`和`call`之間的唯一區別是我們如何在被呼叫的函數中傳遞**參數**。在`apply`中,我們將參數作為**陣列**傳遞,而在`call`中,我們直接在參數列表中傳遞參數。 ``` const obj1 = { result:0 }; const obj2 = { result:0 }; function reduceAdd(){ let result = 0; for(let i = 0, len = arguments.length; i < len; i++){ result += arguments[i]; } this.result = result; } reduceAdd.apply(obj1, [1, 2, 3, 4, 5]); // returns 15 reduceAdd.call(obj2, 1, 2, 3, 4, 5); // returns 15 ``` ### 30. `Function.prototype.bind`的用法是什麼? [↑](#the-questions "返回問題") `bind`方法傳回一個新*綁定的*函數 到特定的`this`值或“所有者”物件,因此我們可以稍後在程式碼中使用它。 `call` 、 `apply`方法立即呼叫函數,而不是像`bind`方法那樣傳回一個新函數。 ``` import React from 'react'; class MyComponent extends React.Component { constructor(props){ super(props); this.state = { value : "" } this.handleChange = this.handleChange.bind(this); // Binds the "handleChange" method to the "MyComponent" component } handleChange(e){ //do something amazing here } render(){ return ( <> <input type={this.props.type} value={this.state.value} onChange={this.handleChange} /> </> ) } } ``` ### 31.什麼是**函數式程式設計**? **JavaScript**的哪些特性使其成為**函數式語言**的候選者? [^](#the-questions "返回問題")**函數式程式設計**是一種**聲明式**程式設計範式或模式,它介紹如何**使用表達式來**計算值而不改變傳遞給它的參數的函數來建立應用程式。 JavaScript**陣列**具有**map** 、 **filter** 、 **reduce**方法,這些方法是函數式程式設計世界中最著名的函數,因為它們非常有用,而且它們不會改變或改變陣列,這使得這些函數變得**純粹**,並且JavaScript 支援**閉包**和**高階函數**,它們是**函數式程式語言**的一個特徵。 - **map**方法建立一個新陣列,其中包含對陣列中每個元素呼叫提供的回調函數的結果。 ``` const words = ["Functional", "Procedural", "Object-Oriented"]; const wordsLength = words.map(word => word.length); ``` - **filter**方法會建立一個新陣列,其中包含透過回調函數中測試的所有元素。 ``` const data = [ { name: 'Mark', isRegistered: true }, { name: 'Mary', isRegistered: false }, { name: 'Mae', isRegistered: true } ]; const registeredUsers = data.filter(user => user.isRegistered); ``` - **reduce**方法對累加器和陣列中的每個元素(從左到右)套用函數,將其減少為單一值。 ``` const strs = ["I", " ", "am", " ", "Iron", " ", "Man"]; const result = strs.reduce((acc, currentStr) => acc + currentStr, ""); ``` ### 32.什麼是**高階函數**? [^](#the-questions "返回問題")**高階函數**是可以傳回函數或接收具有函數值的一個或多個參數的函數。 ``` function higherOrderFunction(param,callback){ return callback(param); } ``` ### 33.為什麼函數被稱為**First-class Objects** ? [^](#the-questions "返回問題") JavaScript 中的 \_\_Functions\_\_ 是**一流物件**,因為它們被視為該語言中的任何其他值。它們可以分配給**變數**,可以是稱為**方法的物件的屬性**,可以是**陣列中的專案**,可以**作為參數傳遞給函數**,也可以**作為函數的值返回**。函數與**JavaScript**中任何其他值之間的唯一區別是**函數**可以被呼叫。 ### 34. 手動實作`Array.prototype.map`方法。 [↑](#the-questions "返回問題") ``` function map(arr, mapCallback) { // First, we check if the parameters passed are right. if (!Array.isArray(arr) || !arr.length || typeof mapCallback !== 'function') { return []; } else { let result = []; // We're making a results array every time we call this function // because we don't want to mutate the original array. for (let i = 0, len = arr.length; i < len; i++) { result.push(mapCallback(arr[i], i, arr)); // push the result of the mapCallback in the 'result' array } return result; // return the result array } } ``` 正如`Array.prototype.map`方法的MDN描述。 **map() 方法建立一個新陣列,其中包含對呼叫陣列中的每個元素呼叫所提供函數的結果。** ### 35. 手動實作`Array.prototype.filter`方法。 [↑](#the-questions "返回問題") ``` function filter(arr, filterCallback) { // First, we check if the parameters passed are right. if (!Array.isArray(arr) || !arr.length || typeof filterCallback !== 'function') { return []; } else { let result = []; // We're making a results array every time we call this function // because we don't want to mutate the original array. for (let i = 0, len = arr.length; i < len; i++) { // check if the return value of the filterCallback is true or "truthy" if (filterCallback(arr[i], i, arr)) { // push the current item in the 'result' array if the condition is true result.push(arr[i]); } } return result; // return the result array } } ``` 正如`Array.prototype.filter`方法的 MDN 描述。 **filter() 方法建立一個新陣列,其中包含透過所提供函數實現的測試的所有元素。** ### 36. 手動實作`Array.prototype.reduce`方法。 [↑](#the-questions "返回問題") ``js 函數reduce(arr,reduceCallback,initialValue){ // 首先,我們檢查傳遞的參數是否正確。 if (!Array.isArray(arr) || !arr.length || typeof reduceCallback !== 'function') { ``` return []; ``` } 別的 { ``` // If no initialValue has been passed to the function we're gonna use the ``` ``` let hasInitialValue = initialValue !== undefined; ``` ``` let value = hasInitialValue ? initialValue : arr[0]; ``` ``` // first array item as the initialValue ``` ``` // Then we're gonna start looping at index 1 if there is no ``` ``` // initialValue has been passed to the function else we start at 0 if ``` ``` // there is an initialValue. ``` ``` for (let i = hasInitialValue ? 0 : 1, len = arr.length; i < len; i++) { ``` ``` // Then for every iteration we assign the result of the ``` ``` // reduceCallback to the variable value. ``` ``` value = reduceCallback(value, arr[i], i, arr); ``` ``` } ``` ``` return value; ``` } } ``` As the MDN description of the <code>Array.prototype.reduce</code> method. __The reduce() method executes a reducer function (that you provide) on each element of the array, resulting in a single output value.__ ###37. What is the __arguments__ object? [&uarr;](#the-questions "Back To Questions") The __arguments__ object is a collection of parameter values pass in a function. It's an __Array-like__ object because it has a __length__ property and we can access individual values using array indexing notation <code>arguments[1]</code> but it does not have the built-in methods in an array <code>forEach</code>,<code>reduce</code>,<code>filter</code> and <code>map</code>. It helps us know the number of arguments pass in a function. We can convert the <code>arguments</code> object into an array using the <code>Array.prototype.slice</code>. ``` 函數一(){ 返回 Array.prototype.slice.call(參數); } ``` Note: __the <code>arguments</code> object does not work on ES6 arrow functions.__ ``` 函數一(){ 返回參數; } 常數二 = 函數 () { 返回參數; } 常量三 = 函數三() { 返回參數; } const 四 = () =&gt; 參數; 四(); // 拋出錯誤 - 參數未定義 ``` When we invoke the function <code>four</code> it throws a <code>ReferenceError: arguments is not defined</code> error. We can solve this problem if your enviroment supports the __rest syntax__. ``` const 四 = (...args) =&gt; args; ``` This puts all parameter values in an array automatically. ###38. How to create an object without a __prototype__? [&uarr;](#the-questions "Back To Questions") We can create an object without a _prototype_ using the <code>Object.create</code> method. ``` 常數 o1 = {}; console.log(o1.toString()); // Logs \[object Object\] 取得此方法到Object.prototype const o2 = Object.create(null); // 第一個參數是物件「o2」的原型,在此 // case 將為 null 指定我們不需要任何原型 console.log(o2.toString()); // 拋出錯誤 o2.toString 不是函數 ``` ###39. Why does <code>b</code> in this code become a global variable when you call this function? [&uarr;](#the-questions "Back To Questions") ``` 函數 myFunc() { 令a = b = 0; } myFunc(); ``` The reason for this is that __assignment operator__ or __=__ has right-to-left __associativity__ or __evaluation__. What this means is that when multiple assignment operators appear in a single expression they evaluated from right to left. So our code becomes likes this. ``` 函數 myFunc() { 令 a = (b = 0); } myFunc(); ``` First, the expression <code>b = 0</code> evaluated and in this example <code>b</code> is not declared. So, The JS Engine makes a global variable <code>b</code> outside this function after that the return value of the expression <code>b = 0</code> would be 0 and it's assigned to the new local variable <code>a</code> with a <code>let</code> keyword. We can solve this problem by declaring the variables first before assigning them with value. ``` 函數 myFunc() { 令 a,b; a = b = 0; } myFunc(); ``` ###40. <div id="ecmascript">What is __ECMAScript__</div>? [&uarr;](#the-questions "Back To Questions") __ECMAScript__ is a standard for making scripting languages which means that __JavaScript__ follows the specification changes in __ECMAScript__ standard because it is the __blueprint__ of __JavaScript__. ###41. What are the new features in __ES6__ or __ECMAScript 2015__? [&uarr;](#the-questions "Back To Questions") * [Arrow Functions](#43-what-are-arrow-functions) * [Classes](#44-what-are-classes) * [Template Strings](#45-what-are-template-literals) * __Enhanced Object literals__ * [Object Destructuring](#46-what-is-object-destructuring) * [Promises](#50-what-are-promises) * __Generators__ * [Modules](#47-what-are-es6-modules) * Symbol * __Proxies__ * [Sets](#48-what-is-the-set-object-and-how-does-it-work) * [Default Function parameters](#53-what-are-default-parameters) * [Rest and Spread](#52-whats-the-difference-between-spread-operator-and-rest-operator) * [Block Scoping with <code>let</code> and <code>const</code>](#42-whats-the-difference-between-var-let-and-const-keywords) ###42. What's the difference between <code>var</code>, <code>let</code> and <code>const</code> keywords? [&uarr;](#the-questions "Back To Questions") Variables declared with <code>var</code> keyword are _function scoped_. What this means that variables can be accessed across that function even if we declare that variable inside a block. ``` 函數給MeX(showX) { 如果(顯示X){ ``` var x = 5; ``` } 返回x; } console.log(giveMeX(false)); console.log(giveMeX(true)); ``` The first <code>console.log</code> statement logs <code>undefined</code> and the second <code>5</code>. We can access the <code>x</code> variable due to the reason that it gets _hoisted_ at the top of the function scope. So our function code is intepreted like this. ``` 函數給MeX(showX) { 變數 x; // 有一個預設值未定義 如果(顯示X){ ``` x = 5; ``` } 返回x; } ``` If you are wondering why it logs <code>undefined</code> in the first <code>console.log</code> statement remember variables declared without an initial value has a default value of <code>undefined</code>. Variables declared with <code>let</code> and <code>const</code> keyword are _block scoped_. What this means that variable can only be accessed on that block <code>{}</code> on where we declare it. ``` 函數給MeX(showX) { 如果(顯示X){ ``` let x = 5; ``` } 返回x; } 函數給MeY(顯示Y){ 如果(顯示Y){ ``` let y = 5; ``` } 返回y; } ``` If we call this functions with an argument of <code>false</code> it throws a <code>Reference Error</code> because we can't access the <code>x</code> and <code>y</code> variables outside that block and those variables are not _hoisted_. There is also a difference between <code>let</code> and <code>const</code> we can assign new values using <code>let</code> but we can't in <code>const</code> but <code>const</code> are mutable meaning. What this means is if the value that we assign to a <code>const</code> is an object we can change the values of those properties but can't reassign a new value to that variable. ###43. What are __Arrow functions__? [&uarr;](#the-questions "Back To Questions") __Arrow Functions__ are a new way of making functions in JavaScript. __Arrow Functions__ takes a little time in making functions and has a cleaner syntax than a __function expression__ because we omit the <code>function</code> keyword in making them. ``` //ES5版本 var getCurrentDate = 函數 (){ 返回新日期(); } //ES6版本 const getCurrentDate = () =&gt; new Date(); ``` In this example, in the ES5 Version have <code>function(){}</code> declaration and <code>return</code> keyword needed to make a function and return a value respectively. In the __Arrow Function__ version we only need the <code>()</code> parentheses and we don't need a <code>return</code> statement because __Arrow Functions__ have a implicit return if we have only one expression or value to return. ``` //ES5版本 函數問候(名稱){ return '你好' + 名字 + '!'; } //ES6版本 const 問候 = (name) =&gt; `Hello ${name}` ; constgreet2 = 名稱 =&gt; `Hello ${name}` ; ``` We can also parameters in __Arrow functions__ the same as the __function expressions__ and __function declarations__. If we have one parameter in an __Arrow Function__ we can omit the parentheses it is also valid. ``` const getArgs = () =&gt; 參數 const getArgs2 = (...休息) =&gt; 休息 ``` __Arrow functions__ don't have access to the <code>arguments</code> object. So calling the first <code>getArgs</code> func will throw an Error. Instead we can use the __rest parameters__ to get all the arguments passed in an arrow function. ``` 常量資料 = { 結果:0, 數字:\[1,2,3,4,5\], 計算結果() { ``` // "this" here refers to the "data" object ``` ``` const addAll = () => { ``` ``` // arrow functions "copies" the "this" value of ``` ``` // the lexical enclosing function ``` ``` return this.nums.reduce((total, cur) => total + cur, 0) ``` ``` }; ``` ``` this.result = addAll(); ``` } }; ``` __Arrow functions__ don't have their own <code>this</code> value. It captures or gets the <code>this</code> value of lexically enclosing function or in this example, the <code>addAll</code> function copies the <code>this</code> value of the <code>computeResult</code> method and if we declare an arrow function in the global scope the value of <code>this</code> would be the <code>window</code> object. ###44. What are __Classes__? [&uarr;](#the-questions "Back To Questions") __Classes__ is the new way of writing _constructor functions_ in __JavaScript__. It is _syntactic sugar_ for using _constructor functions_, it still uses __prototypes__ and __Prototype-Based Inheritance__ under the hood. ``` //ES5版本 函數人(名字,姓氏,年齡,地址){ ``` this.firstName = firstName; ``` ``` this.lastName = lastName; ``` ``` this.age = age; ``` ``` this.address = address; ``` } Person.self = 函數(){ ``` return this; ``` } Person.prototype.toString = function(){ ``` return "[object Person]"; ``` } Person.prototype.getFullName = function (){ ``` return this.firstName + " " + this.lastName; ``` } //ES6版本 類人{ ``` constructor(firstName, lastName, age, address){ ``` ``` this.lastName = lastName; ``` ``` this.firstName = firstName; ``` ``` this.age = age; ``` ``` this.address = address; ``` ``` } ``` ``` static self() { ``` ``` return this; ``` ``` } ``` ``` toString(){ ``` ``` return "[object Person]"; ``` ``` } ``` ``` getFullName(){ ``` ``` return `${this.firstName} ${this.lastName}`; ``` ``` } ``` } ``` __Overriding Methods__ and __Inheriting from another class__. ``` //ES5版本 Employee.prototype = Object.create(Person.prototype); 函數 Employee(名字, 姓氏, 年齡, 地址, 職位名稱, 開始年份) { Person.call(this, 名字, 姓氏, 年齡, 地址); this.jobTitle = jobTitle; this.yearStarted = YearStarted; } Employee.prototype.describe = function () { return `I am ${this.getFullName()} and I have a position of ${this.jobTitle} and I started at ${this.yearStarted}` ; } Employee.prototype.toString = function () { 返回“\[物件員工\]”; } //ES6版本 class Employee extends Person { //繼承自「Person」類 建構函數(名字,姓氏,年齡,地址,工作標題,開始年份){ ``` super(firstName, lastName, age, address); ``` ``` this.jobTitle = jobTitle; ``` ``` this.yearStarted = yearStarted; ``` } 描述() { ``` return `I am ${this.getFullName()} and I have a position of ${this.jobTitle} and I started at ${this.yearStarted}`; ``` } toString() { // 重寫「Person」的「toString」方法 ``` return "[object Employee]"; ``` } } ``` So how do we know that it uses _prototypes_ under the hood? ``` 類別東西{ } 函數 AnotherSomething(){ } const as = new AnotherSomething(); const s = new Something(); console.log(typeof Something); // 記錄“函數” console.log(AnotherSomething 類型); // 記錄“函數” console.log(as.toString()); // 記錄“\[物件物件\]” console.log(as.toString()); // 記錄“\[物件物件\]” console.log(as.toString === Object.prototype.toString); console.log(s.toString === Object.prototype.toString); // 兩個日誌都回傳 true 表示我們仍在使用 // 底層原型,因為 Object.prototype 是 // 原型鏈的最後一部分和“Something” // 和「AnotherSomething」都繼承自Object.prototype ``` ###45. What are __Template Literals__? [&uarr;](#the-questions "Back To Questions") __Template Literals__ are a new way of making __strings__ in JavaScript. We can make __Template Literal__ by using the backtick or back-quote symbol. ``` //ES5版本 vargreet = '嗨,我是馬克'; //ES6版本 讓問候 = `Hi I'm Mark` ; ``` In the ES5 version, we need to escape the <code>'</code> using the <code>\\</code> to _escape_ the normal functionality of that symbol which in this case is to finish that string value. In Template Literals, we don't need to do that. ``` //ES5版本 var 最後一個字 = '\\n' - ' 在' - '我\\n' - '鋼鐵人\\n'; //ES6版本 讓最後一個單字=` ``` I ``` ``` Am ``` 鋼鐵人 `; ``` In the ES5 version, we need to add this <code>\n</code> to have a new line in our string. In Template Literals, we don't need to do that. ``` //ES5版本 函數問候(名稱){ return '你好' + 名字 + '!'; } //ES6版本 const 問候 = 名稱 =&gt; { 返回`Hello ${name} !` ; } ``` In the ES5 version, If we need to add an expression or value in a string we need to use the <code>+</code> or string concatenation operator. In Template Literals, we can embed an expression using <code>${expr}</code> which makes it cleaner than the ES5 version. ###46. What is __Object Destructuring__? [&uarr;](#the-questions "Back To Questions") __Object Destructuring__ is a new and cleaner way of __getting__ or __extracting__ values from an object or an array. Suppose we have an object that looks like this. ``` 常量僱員 = { 名字:“馬可”, 姓氏:“波羅”, 職位:“軟體開發人員”, 聘用年份:2017 }; ``` The old way of getting properties from an object is we make a variable t

請問我應該如何調整自己的學習心態或是思維

利申 > 1.敝人的英文不是很好 > 2.目前嘗試產出專案中 > 3.目前仍在轉職學習的路上,也有報名課程等等 >4.需要一個抒發的地方,如不妥會刪文 >-----廢文----- >1.想問一下,假設我要在專案上用一個沒學過的技術,比如瀑布流排版,我有使用微軟的COPILOT來幫我提供資訊來源,觀看一些網路上各路大神的教學,可能看著看著就超出自己學的範圍了,假設有時間限制上,會直接拿來套用,還是會搞懂自己看不懂的地方後才套用在自己的專案,前者的想法我如果直接套用,我心裡會有一種這樣妥嗎,妥下去的作品,可以稱之為自己的作品嗎,後者的想法是我有時候真的理解不能,前面沒有宣告的東西,後面突然迸出一個詞來.eq(i),看不懂為什麼這個要這樣做,又或是程式碼高手們寫一寫就會開始簡寫,讓整段程式碼變得乾淨整潔,這讓我更摸不著頭緒 >2.常常碰到這個東西我會,所以我就能看到文章說的函數跟參數這種中文名稱看得懂,但只要是自己沒接觸過的,連函數參數中文名稱都很陌生... >3.想知道一下到底怎麼改變學習的方式或心態及思維,之前有跟著站長的自學嬰兒教材學,好比遇到選擇器document.getElementById或是document.querySelector的對象,我知道我要選誰但我不知道要怎麼填寫甚至到寫完了我不知道要console.log哪一句還是哪一個詞,然後整個越來越挫敗,連嬰兒教材都看不懂,想說看前面已經寫完的學長姐作業,各式各樣程式碼,連解讀都有問題 >-----廢文----- >所以我就跑去站長推薦看得印度阿北的頻道(?!?) >https://www.youtube.com/@sadhgurutraditionalchinese >結論大概是 >想知道一下到底怎麼改變學習的方式或心態及思維 >雖然每次看到站長的文章有被激勵到,但就像自學教材的舉例,我看懂了,實作也懂了,要套用到這次作業>上,耶我又看不懂了 >大家又是怎麼維持學習的熱情 >到底什麼樣的狀態是真的有融會貫通,當前學會的技巧等等之類的

🔥 NextJS 專案的 12 個頂級庫 🏆

![保存](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/szi0gw4l049yctxjeu1p.png) 在過去的十年裡,我一直是一名全端開發人員,建置了像[gitup](https://gitup.dev/) 這樣的較小專案和像[crosspublic](https://github.com/github-20k/) 這樣的更大專案跨公共)。 多年來,我測試了不同的工具: 1. 提高工作效率 2. bug 更少 3. 少寫程式碼 我整理了一系列庫來幫助您開發我每天使用的優秀 NextJS 東西,並解釋了您可以用它們做什麼。 **讓我們深入了解一下。** ![變得更好](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ap38q1ej3tqypjuebg3u.gif) --- # 1. [Trigger.dev](https://github.com/triggerdotdev/trigger.dev) 使用 NextJS,我總是需要幫助來處理與後台作業相關的所有事情。 它可以是在背景執行的 cron 作業,用於傳送電子郵件或處理系統中的新使用者管道。 這導致我執行另一台伺服器來處理這些作業,無論是外部 EC2 伺服器還是帶有事件橋的無伺服器功能。 這會導致我支付額外的服務費用(管理更多服務)並自行管理水平擴展(在某些時候)。 [Trigger.dev](http://Trigger.dev) 改變了這一點,在 NextJS(以及許多其他)之上提供後台作業。 他們也知道如何解決 NextJS 無伺服器逾時限制來處理長時間執行的作業。   ![TriggerDev](https://nevdav2.dreamhosters.com/wp-content/uploads/2023/12/triggertop.gif) --- ## 2. [Prisma](https://www.prisma.io) Prisma 不是 NextJS 特有的。它是一個與資料庫一起使用的 ORM。 ORM 是資料庫查詢的統一包裝器。 它保持良好的結構,並允許您在不同的資料庫提供者之間快速更改。 雖然您可以使用很多 ORM,但 Prisma 的獨特之處在於為您的查詢提供 Typescript 支持,使一切速度提高 100 倍。 NextJS 在預設配置中使用了 typescript,使其成為完美的匹配。   ![prisma.gif](https://nevdav2.dreamhosters.com/wp-content/uploads/2023/12/prisma.gif) --- ## 3. [NextAuth.js](https://next-auth.js.org) 假設您要實現任何服務提供者身份驗證,例如 Facebook / Google / GitHub (oAuth)。 在這種情況下,您必須為每個提供者建立實作或使用外部服務,例如 [Auth0](https://auth0.com/) 或 [Clerk](https://clerk.com/)。 如果您打算自行執行此操作,NextAuth 提供了豐富的實現,以便您只需提供正確的金鑰即可輕鬆新增它們。 一旦您登錄,他們也會處理授權。 *Next.JS auth 可以與 Prisma 開箱即用。*   ![authjs.gif](https://nevdav2.dreamhosters.com/wp-content/uploads/2023/12/authjs.gif) --- ## 4. [下一個網站地圖](https://github.com/iamvishnusankar/next-sitemap) 在伺服器上部署 NextJS 後,您需要協助 google 索引所有頁面。 如果您可以告訴 Google 您網站上的所有頁面,那就更好了。 為此,您可以建立一個列出所有頁面的 sitemap.xml 檔案。 您可以輕鬆地使用 Next-Sitemap 來實現這一點。   ![sitemap.gif](https://nevdav2.dreamhosters.com/wp-content/uploads/2023/12/sitemap.gif) --- ## 5. [下一步 SEO](https://github.com/garmeeh/next-seo) SEO 是透過向您的網站預覽提供關鍵字、描述和圖像,使您的網站出現在 Google Feed 上以進行不同查詢的過程。 如果您使用新的 NextJS 應用程式路由器,則可能不需要使用它。 您可以使用他們的“導出元資料”方法或“生成元資料”, 但如果您使用舊的應用程式路由器,這是為您的網站加入 SEO 的最佳方式。   ![seo.gif](https://nevdav2.dreamhosters.com/wp-content/uploads/2023/12/seo.gif) --- ## 6. [Zod](https://github.com/colinhacks/zod) Zod 是一個物件驗證器(伺服器和客戶端)。 您可以在物件上放置不同的規則並稍後對其進行驗證,例如使用者名稱和密碼,或更複雜的內容(例如陣列長度或其他鍵上的條件)。 *Zod 不是 NextJS 特定的。* 多年來,我看過很多物件驗證器,例如 [Yup](https://github.com/jquense/yup) 和 [class-validator](https://github.com/typestack/class-validator)。 是的,它看起來不像 Zod 那樣維護,並且在使用 NestJS 之類的東西時,類驗證器非常強大 - 所以你最好使用 Zod。   ![zod.gif](https://nevdav2.dreamhosters.com/wp-content/uploads/2023/12/zod.gif) --- ## 7. [React-hook-form](https://github.com/react-hook-form/react-hook-form) 雖然 Zod 可以驗證物件,但如果沒有自訂邏輯,它不會影響您的用戶端和後端。 React-hook-form 是優秀的用戶端驗證專案(顯示輸入錯誤、管理輸入狀態和提交)。 當然,您可以使用 Zod 作為 React-hook-form 的驗證器。   ![hookform.gif](https://nevdav2.dreamhosters.com/wp-content/uploads/2023/12/hookform.gif) --- ## 8. [tRPC](https://github.com/trpc/trpc) 我承認我以前從未使用過 tRPC,但今天它似乎吸引了許多人的目光。 它與 Prisma 有類似的概念;它們為您的請求和回應產生一個接口,因此當您使用前端呼叫時,您會獲得自動完成功能。 這很好,因為它減少了錯誤的機會 - 假設您修改了後端路由,您將無法編譯專案 - 客戶端將返回不存在的參數或回應鍵的錯誤。   ![trpc.gif](https://nevdav2.dreamhosters.com/wp-content/uploads/2023/12/trpc.gif) --- ## 9. [SWR](https://swr.vercel.app) 和 [React-Query](https://github.com/TanStack/query) 多年來我一直使用 Axios 和 fetch 作為發送請求的基礎庫。 SWR 和 React-Query 增強了這些函式庫並提供鉤子、快取、轉換等。 強烈推薦用於每個專案。請注意,這些庫適用於客戶端元件(“使用客戶端”),而不是伺服器元件。   ![query.gif](https://nevdav2.dreamhosters.com/wp-content/uploads/2023/12/query.gif) --- ## 10. [lodash](https://lodash.com) 這不是 NextJS 特定的函式庫。 它是一個用於改變資料的函式庫,雖然這些年來 JavaScript 憑藉像 flatMap 這樣優秀的原生函數取得了很大的進步,但仍然缺少一些東西,例如按鍵或分塊和陣列的唯一陣列。 我發現自己幾乎在所有專案中都使用 lodash。   ![lodash.gif](https://nevdav2.dreamhosters.com/wp-content/uploads/2023/12/lodash.gif) --- ## 11. [dayjs](https://day.js.org/) day.js 是一個包含與日期、格式、時區等相關的所有內容的函式庫。 我可能會因為那件事而被烤。我多年來一直在使用“moment.js”。 現在它不再維護了,dayjs 是一個不錯的選擇。 有些人喜歡新的 JS 函數來處理日期,但我仍然覺得 dayjs 選項和原生 JS 日期函數之間存在很大的差距。   ![scrolldown.gif](https://nevdav2.dreamhosters.com/wp-content/uploads/2023/12/scrolldown.gif) --- ## 12. [jsdom](https://github.com/jsdom/jsdom) 這不是必須的,但我最近在許多專案中都使用它作為 [cheerio](https://github.com/cheeriojs/cheerio) 的替代品。 您可以取得整個頁面內容(`<html><body>....</html>)` 並將其轉換為稍後可以使用「本機」javascript dom 函數`querySelector`、`innerHTML` 等來操作的物件… 非常適合需要一些刮擦的專案。   ![jsdomer.gif](https://nevdav2.dreamhosters.com/wp-content/uploads/2023/12/jsdomer.gif) --- 我們在 X 上連接嗎? :) [我在這裡](https://twitter.com/nevodavid) 您是否為 NextJS 使用其他一些很酷的程式庫? 請在評論中讓我了解它們:) --- 原文出處:https://dev.to/nevodavid/top-12-libraries-for-your-nextjs-project-1oob

✨ 用您的文件訓練 ChatGPT 🪄 ✨

# 簡介 ChatGPT 訓練至 2022 年。 但是,如果您希望它專門為您提供有關您網站的資訊怎麼辦?最有可能的是,這是不可能的,**但不再是了!** OpenAI 推出了他們的新功能 - [助手](https://platform.openai.com/docs/assistants/how-it-works)。 現在您可以輕鬆地為您的網站建立索引,然後向 ChatGPT 詢問有關該網站的問題。在本教程中,我們將建立一個系統來索引您的網站並讓您查詢它。我們將: - 抓取文件網站地圖。 - 從網站上的所有頁面中提取資訊。 - 使用新資訊建立新助理。 - 建立一個簡單的ChatGPT前端介面並查詢助手。 ![助手](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ekre38der95twom33tqb.gif) --- ## 你的後台工作平台🔌 [Trigger.dev](https://trigger.dev/) 是一個開源程式庫,可讓您使用 NextJS、Remix、Astro 等為您的應用程式建立和監控長時間執行的作業!   [![GiveUsStars](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bm9mrmovmn26izyik95z.gif)](https://github.com/triggerdotdev/trigger.dev) 請幫我們一顆星🥹。 這將幫助我們建立更多這樣的文章💖 --- ## 讓我們開始吧🔥 讓我們建立一個新的 NextJS 專案。 ``` npx create-next-app@latest ``` >💡 我們使用 NextJS 新的應用程式路由器。安裝專案之前請確保您的節點版本為 18+ 讓我們建立一個新的資料庫來保存助手和抓取的頁面。 對於我們的範例,我們將使用 [Prisma](https://www.prisma.io/) 和 SQLite。 安裝非常簡單,只需執行: ``` npm install prisma @prisma/client --save ``` 然後加入架構和資料庫 ``` npx prisma init --datasource-provider sqlite ``` 轉到“prisma/schema.prisma”並將其替換為以下架構: ``` // This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } model Docs { id Int @id @default(autoincrement()) content String url String @unique identifier String @@index([identifier]) } model Assistant { id Int @id @default(autoincrement()) aId String url String @unique } ``` 然後執行 ``` npx prisma db push ``` 這將建立一個新的 SQLite 資料庫(本機檔案),其中包含兩個主表:“Docs”和“Assistant” - 「Docs」包含所有抓取的頁面 - `Assistant` 包含文件的 URL 和內部 ChatGPT 助理 ID。 讓我們新增 Prisma 客戶端。 建立一個名為「helper」的新資料夾,並新增一個名為「prisma.ts」的新文件,並在其中新增以下程式碼: ``` import {PrismaClient} from '@prisma/client'; export const prisma = new PrismaClient(); ``` 我們稍後可以使用“prisma”變數來查詢我們的資料庫。 --- ![ScrapeAndIndex](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fc05wtlc4peosr62ydnx.png) ## 刮擦和索引 ### 建立 Trigger.dev 帳戶 抓取頁面並為其建立索引是一項長期執行的任務。 **我們需要:** - 抓取網站地圖的主網站元 URL。 - 擷取網站地圖內的所有頁面。 - 前往每個頁面並提取內容。 - 將所有內容儲存到 ChatGPT 助手中。 為此,我們使用 Trigger.dev! 註冊 [Trigger.dev 帳號](https://trigger.dev/)。 註冊後,建立一個組織並為您的工作選擇一個專案名稱。 ![pic1](https://res.cloudinary.com/practicaldev/image/fetch/s--B2jtIoA6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bdnxq8o7el7t4utvgf1u.jpeg) 選擇 Next.js 作為您的框架,並按照將 Trigger.dev 新增至現有 Next.js 專案的流程進行操作。 ![pic2](https://res.cloudinary.com/practicaldev/image/fetch/s--K4k6T6mi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e4kt7e5r1mwg60atqfka.jpeg) 否則,請點選專案儀表板側邊欄選單上的「環境和 API 金鑰」。 ![pic3](https://res.cloudinary.com/practicaldev/image/fetch/s--Ysm1Dd0r--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ser7a2j5qft9vw8rfk0m.png) 複製您的 DEV 伺服器 API 金鑰並執行下面的程式碼片段來安裝 Trigger.dev。 仔細按照說明進行操作。 ``` npx @trigger.dev/cli@latest init ``` 在另一個終端中執行以下程式碼片段,在 Trigger.dev 和您的 Next.js 專案之間建立隧道。 ``` npx @trigger.dev/cli@latest dev ``` ### 安裝 ChatGPT (OpenAI) 我們將使用OpenAI助手,因此我們必須將其安裝到我們的專案中。 [建立新的 OpenAI 帳戶](https://platform.openai.com/) 並產生 API 金鑰。 ![pic4](https://res.cloudinary.com/practicaldev/image/fetch/s--uV1LwOH---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ashau6i2sxcpd0qcxuwq.png) 點擊下拉清單中的「檢視 API 金鑰」以建立 API 金鑰。 ![pic5](https://res.cloudinary.com/practicaldev/image/fetch/s--Tp8aLqSa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4bzc6e7f7avemeuuaygr.png) 接下來,透過執行下面的程式碼片段來安裝 OpenAI 套件。 ``` npm install @trigger.dev/openai ``` 將您的 OpenAI API 金鑰新增至「.env.local」檔案。 ``` OPENAI_API_KEY=<your_api_key> ``` 建立一個新目錄“helper”並新增一個新檔案“open.ai.tsx”,其中包含以下內容: ``` import {OpenAI} from "@trigger.dev/openai"; export const openai = new OpenAI({ id: "openai", apiKey: process.env.OPENAI_API_KEY!, }); ``` 這是我們透過 Trigger.dev 整合封裝的 OpenAI 用戶端。 ### 建立後台作業 讓我們繼續建立一個新的後台作業! 前往“jobs”並建立一個名為“process.documentation.ts”的新檔案。 **新增以下程式碼:** ``` import { eventTrigger } from "@trigger.dev/sdk"; import { client } from "@openai-assistant/trigger"; import {object, string} from "zod"; import {JSDOM} from "jsdom"; import {openai} from "@openai-assistant/helper/open.ai"; client.defineJob({ // This is the unique identifier for your Job; it must be unique across all Jobs in your project. id: "process-documentation", name: "Process Documentation", version: "0.0.1", // This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction trigger: eventTrigger({ name: "process.documentation.event", schema: object({ url: string(), }) }), integrations: { openai }, run: async (payload, io, ctx) => { } }); ``` 我們定義了一個名為「process.documentation.event」的新作業,並新增了一個名為 URL 的必要參數 - 這是我們稍後要傳送的文件 URL。 正如您所看到的,該作業是空的,所以讓我們向其中加入第一個任務。 我們需要獲取網站網站地圖並將其返回。 抓取網站將返回我們需要解析的 HTML。 為此,我們需要安裝 JSDOM。 ``` npm install jsdom --save ``` 並將其導入到我們文件的頂部: ``` import {JSDOM} from "jsdom"; ``` 現在,我們可以新增第一個任務。 用「runTask」包裝我們的程式碼很重要,這可以讓 Trigger.dev 將其與其他任務分開。觸發特殊架構將任務拆分為不同的進程,因此 Vercel 無伺服器逾時不會影響它們。 **這是第一個任務的程式碼:** ``` const getSiteMap = await io.runTask("grab-sitemap", async () => { const data = await (await fetch(payload.url)).text(); const dom = new JSDOM(data); const sitemap = dom.window.document.querySelector('[rel="sitemap"]')?.getAttribute('href'); return new URL(sitemap!, payload.url).toString(); }); ``` - 我們透過 HTTP 請求從 URL 取得整個 HTML。 - 我們將其轉換為 JS 物件。 - 我們找到網站地圖 URL。 - 我們解析它並返回它。 接下來,我們需要抓取網站地圖,提取所有 URL 並返回它們。 讓我們安裝“Lodash”——陣列結構的特殊函數。 ``` npm install lodash @types/lodash --save ``` 這是任務的程式碼: ``` export const makeId = (length: number) => { let text = ''; const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; for (let i = 0; i < length; i += 1) { text += possible.charAt(Math.floor(Math.random() * possible.length)); } return text; }; const {identifier, list} = await io.runTask("load-and-parse-sitemap", async () => { const urls = /(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])/g; const identifier = makeId(5); const data = await (await fetch(getSiteMap)).text(); // @ts-ignore return {identifier, list: chunk(([...new Set(data.match(urls))] as string[]).filter(f => f.includes(payload.url)).map(p => ({identifier, url: p})), 25)}; }); ``` - 我們建立一個名為 makeId 的新函數來為所有頁面產生隨機辨識碼。 - 我們建立一個新任務並加入正規表示式來提取每個可能的 URL - 我們發送一個 HTTP 請求來載入網站地圖並提取其所有 URL。 - 我們將 URL「分塊」為 25 個元素的陣列(如果有 100 個元素,則會有四個 25 個元素的陣列) 接下來,讓我們建立一個新作業來處理每個 URL。 **這是完整的程式碼:** ``` function getElementsBetween(startElement: Element, endElement: Element) { let currentElement = startElement; const elements = []; // Traverse the DOM until the endElement is reached while (currentElement && currentElement !== endElement) { currentElement = currentElement.nextElementSibling!; // If there's no next sibling, go up a level and continue if (!currentElement) { // @ts-ignore currentElement = startElement.parentNode!; startElement = currentElement; if (currentElement === endElement) break; continue; } // Add the current element to the list if (currentElement && currentElement !== endElement) { elements.push(currentElement); } } return elements; } const processContent = client.defineJob({ // This is the unique identifier for your Job; it must be unique across all Jobs in your project. id: "process-content", name: "Process Content", version: "0.0.1", // This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction trigger: eventTrigger({ name: "process.content.event", schema: object({ url: string(), identifier: string(), }) }), run: async (payload, io, ctx) => { return io.runTask('grab-content', async () => { // We first grab a raw html of the content from the website const data = await (await fetch(payload.url)).text(); // We load it with JSDOM so we can manipulate it const dom = new JSDOM(data); // We remove all the scripts and styles from the page dom.window.document.querySelectorAll('script, style').forEach((el) => el.remove()); // We grab all the titles from the page const content = Array.from(dom.window.document.querySelectorAll('h1, h2, h3, h4, h5, h6')); // We grab the last element so we can get the content between the last element and the next element const lastElement = content[content.length - 1]?.parentElement?.nextElementSibling!; const elements = []; // We loop through all the elements and grab the content between each title for (let i = 0; i < content.length; i++) { const element = content[i]; const nextElement = content?.[i + 1] || lastElement; const elementsBetween = getElementsBetween(element, nextElement); elements.push({ title: element.textContent, content: elementsBetween.map((el) => el.textContent).join('\n') }); } // We create a raw text format of all the content const page = ` ---------------------------------- url: ${payload.url}\n ${elements.map((el) => `${el.title}\n${el.content}`).join('\n')} ---------------------------------- `; // We save it to our database await prisma.docs.upsert({ where: { url: payload.url }, update: { content: page, identifier: payload.identifier }, create: { url: payload.url, content: page, identifier: payload.identifier } }); }); }, }); ``` - 我們從 URL 中獲取內容(之前從網站地圖中提取) - 我們用`JSDOM`解析它 - 我們刪除頁面上存在的所有可能的“<script>”或“<style>”。 - 我們抓取頁面上的所有標題(`h1`、`h2`、`h3`、`h4`、`h5`、`h6`) - 我們迭代標題並獲取它們之間的內容。我們不想取得整個頁面內容,因為它可能包含不相關的內容。 - 我們建立頁面原始文字的版本並將其保存到我們的資料庫中。 現在,讓我們為每個網站地圖 URL 執行此任務。 觸發器引入了名為“batchInvokeAndWaitForCompletion”的東西。 它允許我們批量發送 25 個專案進行處理,並且它將同時處理所有這些專案。下面是接下來的幾行程式碼: ``` let i = 0; for (const item of list) { await processContent.batchInvokeAndWaitForCompletion( 'process-list-' + i, item.map( payload => ({ payload, }), 86_400), ); i++; } ``` 我們以 25 個為一組[手動觸發](https://trigger.dev/docs/documentation/concepts/triggers/invoke)之前建立的作業。 完成後,讓我們將保存到資料庫的所有內容並連接它: ``` const data = await io.runTask("get-extracted-data", async () => { return (await prisma.docs.findMany({ where: { identifier }, select: { content: true } })).map((d) => d.content).join('\n\n'); }); ``` 我們使用之前指定的標識符。 現在,讓我們在 ChatGPT 中使用新資料建立一個新檔案: ``` const file = await io.openai.files.createAndWaitForProcessing("upload-file", { purpose: "assistants", file: data }); ``` `createAndWaitForProcessing` 是 Trigger.dev 建立的任務,用於將檔案上傳到助手。如果您在沒有整合的情況下手動使用“openai”,則必須串流傳輸檔案。 現在讓我們建立或更新我們的助手: ``` const assistant = await io.openai.runTask("create-or-update-assistant", async (openai) => { const currentAssistant = await prisma.assistant.findFirst({ where: { url: payload.url } }); if (currentAssistant) { return openai.beta.assistants.update(currentAssistant.aId, { file_ids: [file.id] }); } return openai.beta.assistants.create({ name: identifier, description: 'Documentation', instructions: 'You are a documentation assistant, you have been loaded with documentation from ' + payload.url + ', return everything in an MD format.', model: 'gpt-4-1106-preview', tools: [{ type: "code_interpreter" }, {type: 'retrieval'}], file_ids: [file.id], }); }); ``` - 我們首先檢查是否有針對該特定 URL 的助手。 - 如果我們有的話,讓我們用新文件更新助手。 - 如果沒有,讓我們建立一個新的助手。 - 我們傳遞「你是文件助理」的指令,需要注意的是,我們希望最終輸出為「MD」格式,以便稍後更好地顯示。 對於拼圖的最後一塊,讓我們將新助手儲存到我們的資料庫中。 **這是程式碼:** ``` await io.runTask("save-assistant", async () => { await prisma.assistant.upsert({ where: { url: payload.url }, update: { aId: assistant.id, }, create: { aId: assistant.id, url: payload.url, } }); }); ``` 如果該 URL 已經存在,我們可以嘗試使用新的助手 ID 來更新它。 這是該頁面的完整程式碼: ``` import { eventTrigger } from "@trigger.dev/sdk"; import { client } from "@openai-assistant/trigger"; import {object, string} from "zod"; import {JSDOM} from "jsdom"; import {chunk} from "lodash"; import {prisma} from "@openai-assistant/helper/prisma.client"; import {openai} from "@openai-assistant/helper/open.ai"; const makeId = (length: number) => { let text = ''; const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; for (let i = 0; i < length; i += 1) { text += possible.charAt(Math.floor(Math.random() * possible.length)); } return text; }; client.defineJob({ // This is the unique identifier for your Job; it must be unique across all Jobs in your project. id: "process-documentation", name: "Process Documentation", version: "0.0.1", // This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction trigger: eventTrigger({ name: "process.documentation.event", schema: object({ url: string(), }) }), integrations: { openai }, run: async (payload, io, ctx) => { // The first task to get the sitemap URL from the website const getSiteMap = await io.runTask("grab-sitemap", async () => { const data = await (await fetch(payload.url)).text(); const dom = new JSDOM(data); const sitemap = dom.window.document.querySelector('[rel="sitemap"]')?.getAttribute('href'); return new URL(sitemap!, payload.url).toString(); }); // We parse the sitemap; instead of using some XML parser, we just use regex to get the URLs and we return it in chunks of 25 const {identifier, list} = await io.runTask("load-and-parse-sitemap", async () => { const urls = /(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])/g; const identifier = makeId(5); const data = await (await fetch(getSiteMap)).text(); // @ts-ignore return {identifier, list: chunk(([...new Set(data.match(urls))] as string[]).filter(f => f.includes(payload.url)).map(p => ({identifier, url: p})), 25)}; }); // We go into each page and grab the content; we do this in batches of 25 and save it to the DB let i = 0; for (const item of list) { await processContent.batchInvokeAndWaitForCompletion( 'process-list-' + i, item.map( payload => ({ payload, }), 86_400), ); i++; } // We get the data that we saved in batches from the DB const data = await io.runTask("get-extracted-data", async () => { return (await prisma.docs.findMany({ where: { identifier }, select: { content: true } })).map((d) => d.content).join('\n\n'); }); // We upload the data to OpenAI with all the content const file = await io.openai.files.createAndWaitForProcessing("upload-file", { purpose: "assistants", file: data }); // We create a new assistant or update the old one with the new file const assistant = await io.openai.runTask("create-or-update-assistant", async (openai) => { const currentAssistant = await prisma.assistant.findFirst({ where: { url: payload.url } }); if (currentAssistant) { return openai.beta.assistants.update(currentAssistant.aId, { file_ids: [file.id] }); } return openai.beta.assistants.create({ name: identifier, description: 'Documentation', instructions: 'You are a documentation assistant, you have been loaded with documentation from ' + payload.url + ', return everything in an MD format.', model: 'gpt-4-1106-preview', tools: [{ type: "code_interpreter" }, {type: 'retrieval'}], file_ids: [file.id], }); }); // We update our internal database with the assistant await io.runTask("save-assistant", async () => { await prisma.assistant.upsert({ where: { url: payload.url }, update: { aId: assistant.id, }, create: { aId: assistant.id, url: payload.url, } }); }); }, }); export function getElementsBetween(startElement: Element, endElement: Element) { let currentElement = startElement; const elements = []; // Traverse the DOM until the endElement is reached while (currentElement && currentElement !== endElement) { currentElement = currentElement.nextElementSibling!; // If there's no next sibling, go up a level and continue if (!currentElement) { // @ts-ignore currentElement = startElement.parentNode!; startElement = currentElement; if (currentElement === endElement) break; continue; } // Add the current element to the list if (currentElement && currentElement !== endElement) { elements.push(currentElement); } } return elements; } // This job will grab the content from the website const processContent = client.defineJob({ // This is the unique identifier for your Job; it must be unique across all Jobs in your project. id: "process-content", name: "Process Content", version: "0.0.1", // This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction trigger: eventTrigger({ name: "process.content.event", schema: object({ url: string(), identifier: string(), }) }), run: async (payload, io, ctx) => { return io.runTask('grab-content', async () => { try { // We first grab a raw HTML of the content from the website const data = await (await fetch(payload.url)).text(); // We load it with JSDOM so we can manipulate it const dom = new JSDOM(data); // We remove all the scripts and styles from the page dom.window.document.querySelectorAll('script, style').forEach((el) => el.remove()); // We grab all the titles from the page const content = Array.from(dom.window.document.querySelectorAll('h1, h2, h3, h4, h5, h6')); // We grab the last element so we can get the content between the last element and the next element const lastElement = content[content.length - 1]?.parentElement?.nextElementSibling!; const elements = []; // We loop through all the elements and grab the content between each title for (let i = 0; i < content.length; i++) { const element = content[i]; const nextElement = content?.[i + 1] || lastElement; const elementsBetween = getElementsBetween(element, nextElement); elements.push({ title: element.textContent, content: elementsBetween.map((el) => el.textContent).join('\n') }); } // We create a raw text format of all the content const page = ` ---------------------------------- url: ${payload.url}\n ${elements.map((el) => `${el.title}\n${el.content}`).join('\n')} ---------------------------------- `; // We save it to our database await prisma.docs.upsert({ where: { url: payload.url }, update: { content: page, identifier: payload.identifier }, create: { url: payload.url, content: page, identifier: payload.identifier } }); } catch (e) { console.log(e); } }); }, }); ``` 我們已經完成建立後台作業來抓取和索引文件🎉 ### 詢問助理 現在,讓我們建立一個任務來詢問我們的助手。 前往“jobs”並建立一個新檔案“question.assistant.ts”。 **新增以下程式碼:** ``` import {eventTrigger} from "@trigger.dev/sdk"; import {client} from "@openai-assistant/trigger"; import {object, string} from "zod"; import {openai} from "@openai-assistant/helper/open.ai"; client.defineJob({ // This is the unique identifier for your Job; it must be unique across all Jobs in your project. id: "question-assistant", name: "Question Assistant", version: "0.0.1", // This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction trigger: eventTrigger({ name: "question.assistant.event", schema: object({ content: string(), aId: string(), threadId: string().optional(), }) }), integrations: { openai }, run: async (payload, io, ctx) => { // Create or use an existing thread const thread = payload.threadId ? await io.openai.beta.threads.retrieve('get-thread', payload.threadId) : await io.openai.beta.threads.create('create-thread'); // Create a message in the thread await io.openai.beta.threads.messages.create('create-message', thread.id, { content: payload.content, role: 'user', }); // Run the thread const run = await io.openai.beta.threads.runs.createAndWaitForCompletion('run-thread', thread.id, { model: 'gpt-4-1106-preview', assistant_id: payload.aId, }); // Check the status of the thread if (run.status !== "completed") { console.log('not completed'); throw new Error(`Run finished with status ${run.status}: ${JSON.stringify(run.last_error)}`); } // Get the messages from the thread const messages = await io.openai.beta.threads.messages.list("list-messages", run.thread_id, { query: { limit: "1" } }); const content = messages[0].content[0]; if (content.type === 'text') { return {content: content.text.value, threadId: thread.id}; } } }); ``` - 該事件需要三個參數 - `content` - 我們想要傳送給助理的訊息。 - `aId` - 我們先前建立的助手的內部 ID。 - `threadId` - 對話的執行緒 ID。正如您所看到的,這是一個可選參數,因為在第一個訊息中,我們還沒有線程 ID。 - 然後,我們建立或取得前一個執行緒的執行緒。 - 我們在助理提出的問題的線索中加入一條新訊息。 - 我們執行線程並等待它完成。 - 我們取得訊息清單(並將其限制為 1),因為第一則訊息是對話中的最後一則訊息。 - 我們返回訊息內容和我們剛剛建立的線程ID。 ### 新增路由 我們需要為我們的應用程式建立 3 個 API 路由: 1、派新助理進行處理。 2. 透過URL獲取特定助手。 3. 新增訊息給助手。 在「app/api」中建立一個名為assistant的新資料夾,並在其中建立一個名為「route.ts」的新檔案。裡面加入如下程式碼: ``` import {client} from "@openai-assistant/trigger"; import {prisma} from "@openai-assistant/helper/prisma.client"; export async function POST(request: Request) { const body = await request.json(); if (!body.url) { return new Response(JSON.stringify({error: 'URL is required'}), {status: 400}); } // We send an event to the trigger to process the documentation const {id: eventId} = await client.sendEvent({ name: "process.documentation.event", payload: {url: body.url}, }); return new Response(JSON.stringify({eventId}), {status: 200}); } export async function GET(request: Request) { const url = new URL(request.url).searchParams.get('url'); if (!url) { return new Response(JSON.stringify({error: 'URL is required'}), {status: 400}); } const assistant = await prisma.assistant.findFirst({ where: { url: url } }); return new Response(JSON.stringify(assistant), {status: 200}); } ``` 第一個「POST」方法取得一個 URL,並使用用戶端傳送的 URL 觸發「process.documentation.event」作業。 第二個「GET」方法從我們的資料庫中透過客戶端發送的 URL 取得助手。 現在,讓我們建立向助手新增訊息的路由。 在「app/api」內部建立一個新資料夾「message」並新增一個名為「route.ts」的新文件,然後新增以下程式碼: ``` import {prisma} from "@openai-assistant/helper/prisma.client"; import {client} from "@openai-assistant/trigger"; export async function POST(request: Request) { const body = await request.json(); // Check that we have the assistant id and the message if (!body.id || !body.message) { return new Response(JSON.stringify({error: 'Id and Message are required'}), {status: 400}); } // get the assistant id in OpenAI from the id in the database const assistant = await prisma.assistant.findUnique({ where: { id: +body.id } }); // We send an event to the trigger to process the documentation const {id: eventId} = await client.sendEvent({ name: "question.assistant.event", payload: { content: body.message, aId: assistant?.aId, threadId: body.threadId }, }); return new Response(JSON.stringify({eventId}), {status: 200}); } ``` 這是一個非常基本的程式碼。我們從客戶端獲取訊息、助手 ID 和線程 ID,並將其發送到我們之前建立的「question.assistant.event」。 最後要做的事情是建立一個函數來獲取我們所有的助手。 在「helpers」內部建立一個名為「get.list.ts」的新函數並新增以下程式碼: ``` import {prisma} from "@openai-assistant/helper/prisma.client"; // Get the list of all the available assistants export const getList = () => { return prisma.assistant.findMany({ }); } ``` 非常簡單的程式碼即可獲得所有助手。 我們已經完成了後端🥳 讓我們轉到前面。 --- ![前端](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k3s5gks1j0ojoz11b93i.png) ## 建立前端 我們將建立一個基本介面來新增 URL 並顯示已新增 URL 的清單: ![ss1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ihvx4yn6uee6gritr9nh.png) ### 首頁 將 `app/page.tsx` 的內容替換為以下程式碼: ``` import {getList} from "@openai-assistant/helper/get.list"; import Main from "@openai-assistant/components/main"; export default async function Home() { const list = await getList(); return ( <Main list={list} /> ) } ``` 這是一個簡單的程式碼,它從資料庫中取得清單並將其傳遞給我們的 Main 元件。 接下來,讓我們建立“Main”元件。 在「app」內建立一個新資料夾「components」並新增一個名為「main.tsx」的新檔案。 **新增以下程式碼:** ``` "use client"; import {Assistant} from '@prisma/client'; import {useCallback, useState} from "react"; import {FieldValues, SubmitHandler, useForm} from "react-hook-form"; import {ChatgptComponent} from "@openai-assistant/components/chatgpt.component"; import {AssistantList} from "@openai-assistant/components/assistant.list"; import {TriggerProvider} from "@trigger.dev/react"; export interface ExtendedAssistant extends Assistant { pending?: boolean; eventId?: string; } export default function Main({list}: {list: ExtendedAssistant[]}) { const [assistantState, setAssistantState] = useState(list); const {register, handleSubmit} = useForm(); const submit: SubmitHandler<FieldValues> = useCallback(async (data) => { const assistantResponse = await (await fetch('/api/assistant', { body: JSON.stringify({url: data.url}), method: 'POST', headers: { 'Content-Type': 'application/json' } })).json(); setAssistantState([...assistantState, {...assistantResponse, url: data.url, pending: true}]); }, [assistantState]) const changeStatus = useCallback((val: ExtendedAssistant) => async () => { const assistantResponse = await (await fetch(`/api/assistant?url=${val.url}`, { method: 'GET', headers: { 'Content-Type': 'application/json' } })).json(); setAssistantState([...assistantState.filter((v) => v.id), assistantResponse]); }, [assistantState]) return ( <TriggerProvider publicApiKey={process.env.NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY!}> <div className="w-full max-w-2xl mx-auto p-6 flex flex-col gap-4"> <form className="flex items-center space-x-4" onSubmit={handleSubmit(submit)}> <input className="flex-grow p-3 border border-black/20 rounded-xl" placeholder="Add documentation link" type="text" {...register('url', {required: 'true'})} /> <button className="flex-shrink p-3 border border-black/20 rounded-xl" type="submit"> Add </button> </form> <div className="divide-y-2 divide-gray-300 flex gap-2 flex-wrap"> {assistantState.map(val => ( <AssistantList key={val.url} val={val} onFinish={changeStatus(val)} /> ))} </div> {assistantState.filter(f => !f.pending).length > 0 && <ChatgptComponent list={assistantState} />} </div> </TriggerProvider> ) } ``` 讓我們看看這裡發生了什麼: - 我們建立了一個名為「ExtendedAssistant」的新接口,其中包含兩個參數「pending」和「eventId」。當我們建立一個新的助理時,我們沒有最終的值,我們將只儲存`eventId`並監聽作業處理直到完成。 - 我們從伺服器元件取得清單並將其設定為新狀態(以便我們稍後可以修改它) - 我們新增了「TriggerProvider」來幫助我們監聽事件完成並用資料更新它。 - 我們使用「react-hook-form」建立一個新表單來新增助手。 - 我們新增了一個帶有一個輸入「URL」的表單來提交新的助理進行處理。 - 我們迭代並顯示所有現有的助手。 - 在提交表單時,我們將資訊傳送到先前建立的「路由」以新增助理。 - 事件完成後,我們觸發「changeStatus」以從資料庫載入助手。 - 最後,我們有了 ChatGPT 元件,只有在沒有等待處理的助手時才會顯示(`!f.pending`) 讓我們建立 `AssistantList` 元件。 在「components」內,建立一個新檔案「assistant.list.tsx」並在其中加入以下內容: ``` "use client"; import {FC, useEffect} from "react"; import {ExtendedAssistant} from "@openai-assistant/components/main"; import {useEventRunDetails} from "@trigger.dev/react"; export const Loading: FC<{eventId: string, onFinish: () => void}> = (props) => { const {eventId} = props; const { data, error } = useEventRunDetails(eventId); useEffect(() => { if (!data || error) { return ; } if (data.status === 'SUCCESS') { props.onFinish(); } }, [data]); return <div className="pointer bg-yellow-300 border-yellow-500 p-1 px-3 text-yellow-950 border rounded-2xl">Loading</div> }; export const AssistantList: FC<{val: ExtendedAssistant, onFinish: () => void}> = (props) => { const {val, onFinish} = props; if (val.pending) { return <Loading eventId={val.eventId!} onFinish={onFinish} /> } return ( <div key={val.url} className="pointer relative bg-green-300 border-green-500 p-1 px-3 text-green-950 border rounded-2xl hover:bg-red-300 hover:border-red-500 hover:text-red-950 before:content-[attr(data-content)]" data-content={val.url} /> ) } ``` 我們迭代我們建立的所有助手。如果助手已經建立,我們只顯示名稱。如果沒有,我們渲染`<Loading />`元件。 載入元件在螢幕上顯示“正在載入”,並長時間輪詢伺服器直到事件完成。 我們使用 Trigger.dev 建立的 useEventRunDetails 函數來了解事件何時完成。 事件完成後,它會觸發「onFinish」函數,用新建立的助手更新我們的客戶端。 ### 聊天介面 ![聊天介面](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0u7db3qwz03d6jkk965a.png) 現在,讓我們加入 ChatGPT 元件並向我們的助手提問! - 選擇我們想要使用的助手 - 顯示訊息列表 - 新增我們要傳送的訊息的輸入和提交按鈕。 在「components」內部新增一個名為「chatgpt.component.tsx」的新文件 讓我們繪製 ChatGPT 聊天框: ``` "use client"; import {FC, useCallback, useEffect, useRef, useState} from "react"; import {ExtendedAssistant} from "@openai-assistant/components/main"; import Markdown from 'react-markdown' import {useEventRunDetails} from "@trigger.dev/react"; interface Messages { message?: string eventId?: string } export const ChatgptComponent = ({list}: {list: ExtendedAssistant[]}) => { const url = useRef<HTMLSelectElement>(null); const [message, setMessage] = useState(''); const [messagesList, setMessagesList] = useState([] as Messages[]); const [threadId, setThreadId] = useState<string>('' as string); const submitForm = useCallback(async (e: any) => { e.preventDefault(); setMessagesList((messages) => [...messages, {message: `**[ME]** ${message}`}]); setMessage(''); const messageResponse = await (await fetch('/api/message', { method: 'POST', body: JSON.stringify({message, id: url.current?.value, threadId}), })).json(); if (!threadId) { setThreadId(messageResponse.threadId); } setMessagesList((messages) => [...messages, {eventId: messageResponse.eventId}]); }, [message, messagesList, url, threadId]); return ( <div className="border border-black/50 rounded-2xl flex flex-col"> <div className="border-b border-b-black/50 h-[60px] gap-3 px-3 flex items-center"> <div>Assistant:</div> <div> <select ref={url} className="border border-black/20 rounded-xl p-2"> {list.filter(f => !f.pending).map(val => ( <option key={val.id} value={val.id}>{val.url}</option> ))} </select> </div> </div> <div className="flex-1 flex flex-col gap-3 py-3 w-full min-h-[500px] max-h-[1000px] overflow-y-auto overflow-x-hidden messages-list"> {messagesList.map((val, index) => ( <div key={index} className={`flex border-b border-b-black/20 pb-3 px-3`}> <div className="w-full"> {val.message ? <Markdown>{val.message}</Markdown> : <MessageComponent eventId={val.eventId!} onFinish={setThreadId} />} </div> </div> ))} </div> <form onSubmit={submitForm}> <div className="border-t border-t-black/50 h-[60px] gap-3 px-3 flex items-center"> <div className="flex-1"> <input value={message} onChange={(e) => setMessage(e.target.value)} className="read-only:opacity-20 outline-none border border-black/20 rounded-xl p-2 w-full" placeholder="Type your message here" /> </div> <div> <button className="border border-black/20 rounded-xl p-2 disabled:opacity-20" disabled={message.length < 3}>Send</button> </div> </div> </form> </div> ) } export const MessageComponent: FC<{eventId: string, onFinish: (threadId: string) => void}> = (props) => { const {eventId} = props; const { data, error } = useEventRunDetails(eventId); useEffect(() => { if (!data || error) { return ; } if (data.status === 'SUCCESS') { props.onFinish(data.output.threadId); } }, [data]); if (!data || error || data.status !== 'SUCCESS') { return ( <div className="flex justify-end items-center pb-3 px-3"> <div className="animate-spin rounded-full h-3 w-3 border-t-2 border-b-2 border-blue-500" /> </div> } return <Markdown>{data.output.content}</Markdown>; }; ``` 這裡正在發生一些令人興奮的事情: - 當我們建立新訊息時,我們會自動將其呈現在螢幕上作為「我們的」訊息,但是當我們將其發送到伺服器時,我們需要推送事件 ID,因為我們還沒有訊息。這就是我們使用 `{val.message ? <Markdown>{val.message}</Markdown> : <MessageComponent eventId={val.eventId!} onFinish={setThreadId} />}` - 我們用「Markdown」元件包裝訊息。如果您還記得,我們在前面的步驟中告訴 ChatGPT 以 MD 格式輸出所有內容,以便我們可以正確渲染它。 - 事件處理完成後,我們會更新線程 ID,以便我們從以下訊息中獲得相同對話的上下文。 我們就完成了🎉 --- ![完成](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0half2g6r5zfn7asq084.png) ## 讓我們聯絡吧! 🔌 作為開源開發者,您可以加入我們的[社群](https://discord.gg/nkqV9xBYWy) 做出貢獻並與維護者互動。請隨時造訪我們的 [GitHub 儲存庫](https://github.com/triggerdotdev/trigger.dev),貢獻並建立與 Trigger.dev 相關的問題。 本教學的源程式碼可在此處取得: [https://github.com/triggerdotdev/blog/tree/main/openai-assistant](https://github.com/triggerdotdev/blog/tree/main/openai-assistant) 感謝您的閱讀! --- 原文出處:https://dev.to/triggerdotdev/train-chatgpt-on-your-documentation-1a9g

🧠 自己用 JavaScript 手寫:人工智慧/神經網路......! 😱 不使用任何套件!🤯

您是否嘗試過實際建立神經網路?不,我也沒有……直到今天! 在這篇文章中,我們將介紹我學到的一些東西,以及一些用 vanilla JS 編寫的非常簡單的神經網路的 2 個演示。 ## 介紹 今天早些時候,我正在閱讀 @supabase_io [“AI 內容風暴”](https://supabase.com/blog/supabase-ai-content-storm) 文章。 我突然意識到一件事。我得到了神經網路......但我實際上根本沒有得到它們! 就像,我得到了神經元的概念。但數學是如何運作的呢? 特別是如何使用「反向傳播」來訓練神經網路?偏差和權重如何發揮作用?什麼或誰是 sigmoid? ETC。 現在,明智的做法是閱讀大量文章,獲取一個庫並使用它。 **但我不懂事。** 因此,我閱讀了大量文章……然後決定建立我的第一個神經網路。 但這還不夠難,所以我決定用 JavaScript 來做(因為每個人似乎都使用 Python...)。哦,我決定在沒有任何庫的情況下完成它。哦,我也想在其中建立一個視覺化工具。 我有些不對勁……我似乎在痛苦中茁壯成長。 無論如何,我做到了,這就是我學到的東西。 ## 注意:這不是教學課程 聽著,我想澄清一下,這不是教學! 這只是我分享一些我在學習和我的**第一個**神經網路時發現有趣的事情。 請注意,這裡強調**第一**,所以請不要將其視為除了有趣的東西之外的任何東西。 我也盡力解釋每個部分及其作用,但與所有事情一樣,你對某些東西越熟練,你就越能更好地解釋它......所以我的一些解釋可能有點“偏離” ! 無論如何,既然所有這些都已經解決了,讓我們繼續吧! 如果您想直接跳到[最終演示](#the-demo),請繼續! ## 第一步 好的,首先,我可以建立的最基本的神經網路是什麼? 經過一番閱讀後,我發現神經網路可以像一些輸入神經元和一些輸出神經元一樣簡單。 每個輸入神經元都連接到一個輸出神經元,然後我們可以為每個連接加入權重。 考慮到這一點,我必須想出一個易於理解的問題來解決,但又足夠複雜以確保我的網路正常運作。 我決定建立一個神經網絡,它獲取圖表上某個點的 X 和 Y 座標,然後根據它們是正還是負為它們分配一個「團隊」(顏色)。 這樣我們就有了 2 個輸入(X 和 Y 位置),然後有 4 個輸出: 1. X > 0 且 Y > 0 2. X < 0 且 Y > 0 3. X > 0 且 Y < 0 4. X < 0 且 Y < 0 由於這裡的要求非常簡單,我們可以擺脫一些「隱藏」神經元(這是我稍後將介紹的內容)並讓事情變得超級簡單! 所以本質上我們必須建構一個看起來像這樣的神經網路: ![左側 2 個圓圈透過線連接到右側 4 個圓圈。深色背景上的霓虹燈風格。](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x9rh1tbxrndmjsz5b7oo.png) 左邊的圓圈是我們的輸入(X 和 Y 位置),右邊的圓圈是我們之前討論的輸出。 ### 我們的第一個神經元 好的,現在我們可以真正開始了。 現在我實際上並沒有先建置神經元。事實上,我實際上首先建立了一個視覺化工具,因為這是查看事情是否正常運作的最簡單方法,但我稍後會介紹這一點。 因此,讓我們建立一個神經元(或更具體地說,一些神經元及其連接)。 幸運的是,神經元其實非常簡單! (或者我應該說,它們*可以*非常簡單......它們在大型語言模型(LLM)等中變得更加複雜。) 簡單神經元具有偏差(將其視為內部權重,我們將加入到最終計算中以對每個神經元進行加權的數字),並透過每個連接之間的權重連接到其他神經元。 現在回想起來,單獨加入到每個神經元的連接可能是一個更好的主意,但我決定將每層神經元和每層連接作為單獨的專案加入,因為這樣更容易理解。 所以建構我的第一個神經網路的程式碼如下所示: ``` class NeuralNetwork { constructor(inputLen, outputLen) { this.inputLen = inputLen; this.outputLen = outputLen; this.weights = Array.from({ length: this.outputLen }, () => Array.from({ length: this.inputLen }, () => Math.random()) ); this.bias = Array(this.outputLen).fill(0); } } const neuralNetwork = new NeuralNetwork(2, 4); ``` 好的,我跳過了一些步驟,所以讓我們簡要介紹一下每個部分。 `this.inputLen = inputLen;` 和 `this.outputLen = outputLen;` 只是為了讓我們可以引用輸入和輸出的數量。 `this.weights = [...]` 是連線。現在看起來可能有點嚇人,但這就是我們正在做的事情: - 建立輸出神經元陣列(`outputLen`) - 將長度為“inputLen”的陣列加入到每個陣列條目,並用 0 到 1 之間的一些隨機值填充它以開始我們的工作。 該程式碼的輸出範例如下所示: ``` this.weights = [ [0.7583747881712366,0.4306037998314902], [0.40553698492617807,0.4419651593960727], [0.852978801662627,0.9762509253699836], [0.8701610553353811,0.5583309725764114] ] ``` 它們本質上代表以下內容: ``` [input 1 to output 1, input 2 to output 1], [input 1 to output 2, input 2 to output 2], [input 1 to output 3, input 2 to output 3], [input 1 to output 4, input 2 to output 4], ``` 然後我們還有「this.bias」。 這適用於輸出層中的每個神經元。我們稍後用它來加入輸出值,使一些神經元更強,一些神經元更弱。 它只是一個由 4 個 0 組成的陣列來讓我們開始,因為我們不想要初始偏差! 現在,雖然這是一個神經網絡,但它完全沒有用。 我們無法實際使用它......如果我們確實使用它,它產生的結果將是完全隨機的! 所以我們需要解決這些問題! ### 使用我們的網路! 我們需要做的第一件事是實際獲取一些輸入,透過我們的網路執行它們並收集輸出。 這是我想出來的: ``` propagate(inputs) { const output = new Array(this.outputLen); for (let i = 0; i < this.outputLen; i++) { output[i] = 0; for (let j = 0; j < this.inputLen; j++) { output[i] += this.weights[i][j] * inputs[j]; } output[i] += this.bias[i]; output[i] = this.sigmoid(output[i]); } return output; } sigmoid(x) { return 1 / (1 + Math.exp(-x)); } ``` 現在這裡有兩件有趣的事情。 ####乙狀結腸 首先,一件有趣的事情是我們的“sigmoid”函數。它所做的只是沿著「S 形」曲線將我們輸入的值(例如 12)轉換為 0 到 1 之間的值。 這是我們將價值觀從極端標準化為更統一的**且始終為正值的方式。** 進一步閱讀後,這裡還有其他關於如何將值更改為 0 到 1 之間的選項,但我還沒有完全探索它們(例如 [ReLU](https://en.wikipedia.org/wiki/Rectifier_(neural_networks)) 如果你想閱讀相關內容)。 我確信有一些很好的解釋為什麼需要這樣做,但在我的猴腦中,這只是將值保持在 0 和 1 之間的方法,以便乘法保持在一定範圍內並且值被「展平」。 這樣你就不會在過於強大的神經元之間出現「失控」的路徑。 例如,假設您有一個權重為 16 的連接和一個權重為 1 的連接,使用我們的 sigmoid 函數,我們可以將其從 16 倍的差異減少到大約 35% 的差異(「sigmoid(1)」為0.73 ,執行我們的函數後,`sigmoid(16)` 為0.99)。 這也意味著負值變成正值。 因此,透過 sigmoid 函數執行值意味著負數會轉換為 0 到 0.5 之間的值,0 的值恰好變成 0.5,大於 0 的值變成 0.5 到 1 之間的值。 ![描繪兩條虛線之間的 S 型曲線的圖形。中間有一條虛線表示0.5。深色背景上的霓虹燈發光造型。](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dr3460emdf5m77nc0rn2.png) 如果你考慮一下,這是有道理的,因為當我們開始將負數和正數相乘時,我們可以極大地改變我們的輸出。 例如,如果我們在 100 的路徑中有一個負神經元,而其餘的都是正神經元,這會將強值更改為弱值,並且可能會導致問題。 不管怎樣,隨著我閱讀更多和實驗更多,我相信我會更好地理解這一部分! #### 我需要偏見嗎? 第二個有趣的是「output[i] += this.bias[i];」。 好吧,在這個神經網路中,所有 4 個輸出都同樣重要,而且我們沒有隱藏神經元,所以我後來刪除了它以簡化程式碼! 但諷刺的是,在我們更複雜的神經網路上,由於網路反向傳播的工作方式,我需要重新加入輸出神經元的偏差。否則,一個輸出神經元會一直啟動。 我無法弄清楚這是否是必要的步驟,或者我的神經網路是否犯了一個錯誤,而這是為了彌補它。 再次提醒大家,我還在學習中,只是掌握了基礎知識,所以我不知道它是什麼! 🤣 #### 我們快到了 上面的其餘程式碼相當簡單。我們只是將每個輸入乘以與每個輸出相關的權重(並加入不必要的偏差!)。 事實上,我們現在就可以執行它,但結果會很糟糕!讓我們來解決這個問題! ### 訓練時間到了! 好吧,神經網路的最後一個重要部分是訓練它! 現在,隨著這篇文章越來越長,我將只介紹以下訓練程式碼的要點(順便說一下,我花了將近一個小時來編寫......我告訴過你我對此是個菜鳥! ) ``` train(inputs, target) { const output = this.propagate(inputs); const errors = new Array(this.outputLen); for (let i = 0; i < this.outputLen; i++) { errors[i] = target[i] - output[i]; for (let j = 0; j < this.inputLen; j++) { this.weights[i][j] += this.learningRate * errors[i] * output[i] * (1 - output[i]) * inputs[j]; } this.bias[i] += this.learningRate * errors[i]; } } ``` “為什麼花了這麼長時間?”我聽到你問了!好吧,它讓我思考了所有需要相乘才能更新每個權重的位元。 另外 `this.learningRate` 也需要一點時間來適應。這只是降低我們改變權重的速率,這樣我們就不會“超過”每個權重的目標值,但是將其調整到合理的值需要經驗......我沒有經驗並且設置得太低,所以我的程式碼看起來被破壞了! 經過一番擺弄後,我將數值定為 0.1(而不是 0.01 🤦🏼‍♂️),突然間事情開始變得更好了! 是的,所以我們有一個訓練函數。但請記住,此訓練功能僅進行一次訓練。 我們需要對我們的網路進行多次訓練,希望每次訓練都能使其更加準確。 我們將在一秒鐘內討論這一點,但我想分享一個快速的側面/我學到的東西。 #### 訓練資料調整 我知道我們甚至還沒有涵蓋最終的訓練資料,但這是我學到的一個有趣的觀點,適合這裡(因為它解釋了為什麼我花了這麼長時間來編寫這個訓練函數)。 最初我產生了數百個不同的訓練 X 和 Y 座標,全部都是隨機的。 但經過進一步閱讀後,我只產生 4 個靜態訓練點,得到了更好的結果: ``` const trainingData = [ { x: -0.5, y: -0.5, label: "blue" }, { x: 0.5, y: -0.5, label: "red" }, { x: -0.5, y: 0.5, label: "green" }, { x: 0.5, y: 0.5, label: "purple" } ]; ``` 一旦你明白了,就很有意義了! 我們希望「拉」值更接近目標,上述值是我們每個區域的確切「中心點」。 因此,對於給定的距離,我們的錯誤率將始終保持一致。 這意味著我們的神經網路學習得更快,因為我們的錯誤率更大,這取決於它們距離 X 更遠還是距離 Y 更遠。 我可以更好地解釋這一點,但這超出了本文的範圍。希望如果您仔細考慮一下,那麼它也會像對我一樣為您「點擊」! 諷刺的是,我回到了更大模型的更隨機的資料集,因為我想真正測試我對學習率、過度訓練等的理解。 ## 我們有一個有效且有用的神經網路! 實際上,這就是我們的整個神經網路。 不過,我們需要做一件事。 我們的訓練函數需要執行很多次! 因此,我們需要最後一個函數來做到這一點,它獲取我們的訓練資料並執行我們的訓練函數數百次: ``` function train() { for (let i = 0; i < 10000; i++) { const data = trainingData[Math.floor(Math.random() * trainingData.length)]; neuralNetwork.train([data.x, data.y], encode(data.label)); } console.log("Training complete"); } ``` ### 金髮女孩迭代 請注意,我們在「for」循環中訓練網路 10,000 次。 10,000 次迭代足以訓練這個特定的神經網路。但對於我們稍後將介紹的更複雜的問題,我需要更多的迭代(並降低學習率)。 這是機器學習有趣的部分之一,您需要足夠地訓練神經網路(這很難做到正確),但如果訓練太多,就會發生“過度擬合”,並且實際上開始得到更糟糕的結果。因此,為了獲得最佳結果,需要完美平衡! 不管怎樣,已經很多了,我們終於迎來了第一個示範! ## 簡單的普通 JS 神經網路演示 雖然有點亂,但我們的神經網路和所有訓練部分都在下面 CodePen 的前 67 行。 其餘程式碼行實際上執行我們的網路(“neuralNetwork.propagate([x, y]);”大約第 85 行),然後將點及其預測顏色輸出到“<canvas>”上。 「encode」和「decode」純粹是為了獲取我們的輸出神經元,找到哪個神經元具有最高的激活,然後將其映射到我們可視化的顏色。 這是最後要理解的事。我們的輸出神經元都會有一個值。神經網路不僅僅輸出 1, 0, 0, 0。 相反,它會輸出每個輸出神經元的「確定性」或猜測。因此我們將得到類似「0.92,0.76,0.55,0.87」的輸出。 這就是為什麼我們有“解碼”函數,它找到最高輸出的神經元並將其作為我們的最終猜測! ``` // this line finds the max value of all of our output neurons and then returns its index so we can use that to classify our X and Y coordinates. const maxIndex = output.indexOf(Math.max(...output)); ``` ### 用法和實際演示 要使用該範例,您有 3 個按鈕: - **訓練** - 在我們的神經網路開始未經訓練和隨機化時對其進行訓練。 - **分類點** - 這是為了執行我們的神經網路。它將在圖表上繪製點並為它們分配顏色。我建議在訓練之前和之後執行這個。 - **重設** - 這將建立一個未經訓練的新神經網路。非常適合在訓練前後測試點的分類。 另請注意,每個區域都根據該區域應顯示的顏色進行著色。它真的可以讓您看到隨機且未經訓練的神經網路距離成功還有多遠(重置然後對測試點進行分類)! 玩吧! https://codepen.io/GrahamTheDev/pen/abPxoqb ### 我們最基本的神經網路結束 這樣我們就有了最基本的神經網路! 它可以很好地滿足我們的需求,並且我們設法了解了一些關於反向傳播(我們在主類別中的“train”函數)以及權重和偏差的知識。 但這是非常有限的。如果我們將來想做更高級的事情,我們需要加入一些隱藏神經元! ## 版本 2 - 隱藏神經元 好的,那為什麼要隱藏神經元呢?他們的目的是什麼? 在更複雜的範例中,它們充當獲取輸入並為它們的分類方式加入更多維度的方式。 我們仍然使用 2 個輸入神經元和 4 個輸出神經元,但這次我們在中間加入了一個附加層(我們可以更改和調整其中神經元的數量)。 所以我們的神經網路看起來像這樣: ![左側 2 個圓圈透過線連接到中間的 6 個圓圈,然後透過線連接到右側的 4 個圓圈。深色背景上的霓虹燈風格。](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z0f1204y2d96wucq6fzv.png) 由於神經網路需要處理更多的輸入並進行更複雜的計算,隱藏層中的額外神經元使它們能夠更好地對輸入進行分類並提供更好的結果。 隱藏層也可以是不同的「深度」。 假設我們有 2 個輸入神經元。我們可以將它們連接到 6 個「隱藏」神經元,然後將它們連接到 4 個輸出神經元。 但我們也可以將第一層的 6 個神經元連結到第二層隱藏神經元。第二層可能有 8 個神經元,然後連接到我們的 4 個輸出神經元。 但要遵循的內容還有很多,這是為了讓我學習基礎知識,所以我選擇加入一個隱藏層。這也意味著我可以將每個連接層保留為單獨的陣列,這在現階段更容易理解! ### 那麼有什麼新內容呢? 沒有太大變化,只是我們有了更多的連結和更多的神經元! 您可以將其視為串聯加入 2 個原始神經網絡,只是第一個神經網路的輸出現在充當第二個神經網路的輸入。 雖然程式碼可能更加複雜,但我們的神經網路遵循相同的原則。 這是程式碼: ``` class NeuralNetwork { constructor(inputSize, hiddenSize, outputSize) { this.inputSize = inputSize; this.hiddenSize = hiddenSize; this.outputSize = outputSize; this.weightsInputToHidden = Array.from({ length: hiddenSize }, () => Array.from({ length: inputSize }, () => Math.random() * 2 - 1) ); this.biasHidden = Array(hiddenSize).fill(0); this.weightsHiddenToOutput = Array.from({ length: outputSize }, () => Array.from({ length: hiddenSize }, () => Math.random() * 2 - 1) ); this.biasOutput = Array(outputSize).fill(0); this.learningRate = document.querySelector('#learningRate').value; // Adjusted learning rate this.hiddenLayer = new Array(this.hiddenSize); } feedForward(inputs) { for (let i = 0; i < this.hiddenSize; i++) { this.hiddenLayer[i] = 0; for (let j = 0; j < this.inputSize; j++) { this.hiddenLayer[i] += this.weightsInputToHidden[i][j] * inputs[j]; } this.hiddenLayer[i] += this.biasHidden[i]; this.hiddenLayer[i] = sigmoid(this.hiddenLayer[i]); } const output = new Array(this.outputSize); for (let i = 0; i < this.outputSize; i++) { output[i] = 0; for (let j = 0; j < this.hiddenSize; j++) { output[i] += this.weightsHiddenToOutput[i][j] * this.hiddenLayer[j]; } output[i] += this.biasOutput[i]; output[i] = sigmoid(output[i]); } return output; } train(inputs, target) { for (let i = 0; i < this.hiddenSize; i++) { this.hiddenLayer[i] = 0; for (let j = 0; j < this.inputSize; j++) { this.hiddenLayer[i] += this.weightsInputToHidden[i][j] * inputs[j]; } this.hiddenLayer[i] += this.biasHidden[i]; this.hiddenLayer[i] = sigmoid(this.hiddenLayer[i]); } const output = new Array(this.outputSize); for (let i = 0; i < this.outputSize; i++) { output[i] = 0; for (let j = 0; j < this.hiddenSize; j++) { output[i] += this.weightsHiddenToOutput[i][j] * this.hiddenLayer[j]; } output[i] += this.biasOutput[i]; output[i] = sigmoid(output[i]); } const errorsOutput = new Array(this.outputSize); const errorsHidden = new Array(this.hiddenSize); for (let i = 0; i < this.outputSize; i++) { errorsOutput[i] = target[i] - output[i]; for (let j = 0; j < this.hiddenSize; j++) { this.weightsHiddenToOutput[i][j] += this.learningRate * errorsOutput[i] * output[i] * (1 - output[i]) * this.hiddenLayer[j]; } this.biasOutput[i] += this.learningRate * errorsOutput[i]; } for (let i = 0; i < this.hiddenSize; i++) { errorsHidden[i] = 0; for (let j = 0; j < this.outputSize; j++) { errorsHidden[i] += this.weightsHiddenToOutput[j][i] * errorsOutput[j]; } this.biasHidden[i] += this.learningRate * errorsHidden[i]; for (let j = 0; j < this.inputSize; j++) { this.weightsInputToHidden[i][j] += this.learningRate * errorsHidden[i] * this.hiddenLayer[i] * (1 - this.hiddenLayer[i]) * inputs[j]; } } } } ``` 現在,不要被嚇倒,我剛剛複製了幾個循環,其中要操作的目標資料集略有不同。 我們加入了一組額外的偏差(對於我們的隱藏層)和一組額外的連接:我們的輸入層到我們的隱藏層,然後我們的隱藏層現在連接到我們的輸出層。 最後,我們的「train」函數有一些額外的循環,只是為了透過每個步驟進行反向傳播。 唯一值得一提的其他變化是我們現在有第三個輸入參數(中間),用於隱藏神經元的數量。 ### 醜陋,但似乎有用 看,我想再說一遍,這是我一邊學習一邊學習,所以程式碼反映了這一點。 這裡有很多重複,可擴展性不太好。 然而,據我所知,它是有效的。 話雖如此,雖然它有效,但它的性能似乎比我們原來的、簡單得多的神經網路要差。 這要么意味著我犯了一個錯誤(可能),要么是我沒有「撥入」正確的訓練設定。 說到這裡... ### 加入一些變數來玩 由於這更複雜,我在一些快速設定中「迴避」了。 現在我們可以更新: - **訓練資料大小** - 我們產生的不同隨機點的數量 - **訓練迭代** - 我們從訓練集中選擇隨機資料點並將其輸入神經網路上的「訓練」函數的次數。 - **學習率** - 我們根據錯誤調整速度的乘數。 - **隱藏節點(超過2個!)** - 調整第二層有多少個隱藏節點(需要您再次初始化網絡,否則會損壞!) - **要分類的點** - 傳遞給我們訓練過的神經網路並繪製在圖表上的點數。 這意味著我們可以更快地處理這些值,看看它們對我們的神經網路及其準確性有什麼影響! ### 最後一件事 哦,我加入了一個按鈕來視覺化神經網路的樣子。 無論如何按“可視化神經元和權重”,但它還沒有完成。我也沒有立即完成它的打算,因為我想完全重新設計建構神經網路的方法,使其更具可擴展性。 不過,按鈕就在那裡,請隨意按下。更好的是,請隨時為我修復它! 🤣💗 ## 演示 控制項與以前相同,加上前面 2 個小節中提到的輸入。 試試一下,看看是否可以微調學習率、神經元數量和訓練設定以獲得真正準確的結果! https://codepen.io/GrahamTheDev/pen/qBLwBxP 請務必更新一些值,重新初始化神經網絡,嘗試使用不同數量的隱藏神經元等。 如果您像我一樣是初學者,希望您能開始理解一些事情! ## 結論 用 vanilla JS 建立神經網路真的很有趣。 我沒有見過很多人這樣做,所以我希望它對你或至少對某人有用! 我學到了很多關於偏差、反向傳播(神經網路的關鍵)等的知識。 顯然這個例子和這裡學到的東西只是機器學習的1%。但對於像我這樣的微小的、未優化的神經網路和巨大的數十億參數模型來說,核心原理是相同的。 這個例子就像機器學習 (ML) 和神經網路的「hello world」。 接下來,我真的想嘗試建立一個更大的、結構更好、更容易擴展的神經網絡,看看我們是否可以進行一些光學字元辨識(OCR)。您可以將其視為機器學習和神經網路的“待辦事項清單”! ### 發表評論。 您是神經網路專家嗎?告訴我我哪裡錯了! 你是像我一樣的初學者嗎?那麼請告訴我這是否有助於您理解,至少一點點!或者,如果這實際上讓事情變得更加混亂! 😱 最重要的是,如果這篇文章激發了您對我糟糕的編碼做鬼臉,或者想要建置您自己的神經網絡......那麼我很高興它對您產生了一些影響,並且很樂意聽到它! 💗 --- 原文出處:https://dev.to/grahamthedev/a-noob-learns-ai-my-first-neural-networkin-vanilla-jswith-no-libraries-1f92

JS教學-文章縮圖預覽,夢幻連動的腳本教學!

## 前情提要 常常寫文章的時候都會運用到圖文並茂, 如果可以設定文章的圖片封面就是一個很棒的設計。 今天的JS腳本教學已經夢幻連動! 原本IT幫的文章與codeLove像是平行宇宙,只有偶爾轉載,現在, 教學內容直接來針對codelove來寫一個小小有趣小功能XD ## 目標設計 今天以codelove這個論壇為例, 讓我們來手動創造一個小腳本:分析文章內容,以正則去匹配圖片網址, 然後透過create的API寫出div,並且監聽事件, 當點擊圖片的時候,把圖片網址放到封面圖片的input欄位。 ## 效果 會根據textArea change的時候來匹配出網址。 並且創建div與img元素去放入網頁。 ![](https://i.imgur.com/sHcMDU8.png) 點圖片後會把圖片網址丟入。 ![](https://i.imgur.com/MWhkOB7.png) ## 程式碼內容 ``` // 獲取textarea元素 var textarea = document.querySelector("body > div.container.px-0 > div > div.col-12.col-lg-9 > div > div > form > textarea"); var featuredImageInput = document.querySelector('input[name="featured_image"]'); textarea.addEventListener('change', function(event) { // 檢查是否存在imgBox元素 let imgBoxCheck = document.querySelector('.imgBox'); if (imgBoxCheck) { // 如果存在,則刪除imgBox元素 imgBoxCheck.parentNode.removeChild(imgBoxCheck); } // 獲取文本框的內容 var ContentText = textarea.value; // 創建一個空數組來存儲圖片鏈接 var imgArray = []; // 使用正則表達式查找圖片鏈接 var urlRegex = /https?:\/\/[^\s/$.?#].[^\s]*/gi; // 匹配以http或https開頭的鏈接 var matches = ContentText.match(urlRegex); if (matches) { // 疊代所有匹配項並篩選出以.png或.jpg結尾的鏈接 for (var i = 0; i < matches.length; i++) { if (matches[i].match(/\.(png|jpg)$/i)) { imgArray.push(matches[i]); } } } // 使用正則表達式匹配鏈接部分 var regex = /!\[\]\(([^)]+)\)/g; var match; while ((match = regex.exec(ContentText)) !== null) { imgArray.push(match[1]); } // 創建一個包含圖片的容器div(imgBox)並設置為display: flex let imgBox = document.createElement('div'); imgBox.className = 'imgBox'; // 添加類名 imgBox.style.display = 'flex'; // 平均分配圖片大小 var imageSize = 100 / imgArray.length; // 假設100%寬度,根據圖片數量計算每張圖片的寬度 // 將圖片鏈接轉換為img元素並插入到imgBox imgArray.forEach(function (imgUrl) { var imgDiv = document.createElement('div'); var img = document.createElement('img'); img.src = imgUrl; img.className = 'imgCover'; img.addEventListener('click', function () { // 設置name="featured_image"的輸入框的值為被點擊圖片的src屬性 featuredImageInput.value = img.src; }); // 設置img的寬度為100% img.style.width = '100%'; img.style.height = '100%'; imgDiv.appendChild(img); imgBox.appendChild(imgDiv); }); // 將imgBox插入到textarea後面 h5.insertAdjacentElement('afterend', imgBox); textarea.style.overflow = 'auto'; imgBox.style.border="5px solid gray" imgBox.style.marginTop="5px" }); // 創建一個包含CSS規則的<style>元素 var style = document.createElement('style'); style.type = 'text/css'; style.textContent = '.imgBox:hover .imgCover:not(:hover) { filter: grayscale(100%); opacity: 0.75; }'; // 將<style>元素添加到<head>中 document.head.appendChild(style); h5= document.createElement('h5') h5.innerText='請選擇你的文章縮圖:' h5.style.marginTop="50px" h5.style.fontSize="16px" textarea.insertAdjacentElement('afterend', h5); ``` ## 觀念筆記 這次的腳本內容其實沒有太多新的知識點,算是一個很好的複習。 querySelector addEventListener insertAdjacentElement forEach 以及正則表達式的使用。 ## 心得後記 網頁上的功能,其實就是這些基礎的運用配合起來!(°ཀ°) 所以不用擔心前端沒有東西練習,其實點子四處都有,練習也都很實用。 而不是每次都在重複輪迴相同的東西、感到疲倦! 這次的前端小試身手就帶來一個回鍋菜,下次敬請期待更有趣的變化。 喜歡記得追蹤或留言唷。 還沒挑戰過此系列其他內容的,可以試著去挑戰看看◑ω◐

不是共產黨,但是審查低質量雜訊很重要!自己寫一個JS腳本過濾!

本文轉自:https://ithelp.ithome.com.tw/articles/10338948 ## 前情提要 資訊大爆炸。 有時候我們瀏覽技術文章,不一定真的是想學深奧的高級技術。 然而劣質低端的文章充斥著,則會降低我們學習的效率、甚至變成噪音與雜訊,干擾我們的思緒。 因此針對某些洗文或是質量很低的作者,我們必須列為思想犯, 否則會降低閱讀質量,平白浪費自己的時間、平台的版面、上網的電力、看到垃圾資訊的副作用等等.... ## 構思來源 如果有個思想審查警衛可以:**去除那些垃圾低端,稱不上技術文章的雜訊。** 以確保未來瀏覽文章的時候,不會再被洗文打擾, 也可以針對不喜歡的主題去封鎖,讓時間與精神更能專注於自己想要學的資訊。 阻止一些垃圾就是喜歡把自己尚未整理的白痴內容一直丟上來, 什麼都還不懂,把技術文章當成個人日記簿,寫一堆自我囈語、無病呻吟, 每天大量狂發文章,昭告天下以為這就是努力,欺騙自己也浪費別人的人生。 ## 「思想審查警衛」出動! ## 功能 1. 把頭像屏蔽 2. 加上思想通緝犯、紅字與刪除線 3. 新增封殺按鈕 4. 版面通知封殺名單與文章數 5. 透過ajax確認某id的最新ID ## 效果截圖 ![](https://i.imgur.com/kgUYJbl.png) 此截圖僅是腳本示範,跟其使用者無關, 本人沒有任何覺得此使用者發的文章是差勁的意味,我認為非常上進、值得學習。 ![](https://i.imgur.com/G27Ea2F.png) 此圖也只是隨機挑user使用,純粹作為範例用途,不代表我個人意見。 ## 腳本下載 https://greasyfork.org/zh-TW/scripts/477283-%E6%80%9D%E6%83%B3%E7%8A%AF%E5%B0%81%E6%AE%BA ## 原理說明 鄭重聲明,這個示範真的毫無任何私人意味,此腳本也只是針對不同的主題去隱藏, 例如我想學python就不想看到JS的文章,因此使用此工具幫忙隱藏罷了。 取名做思想警察、比喻成清除垃圾等都只是文學上的趣味。 請勿當真Ꮚ・ꈊ・Ꮚ 這次的技術可說比較難,認真難很多!但是趣味性以及功能性是無比的超越! 可以說幾乎是我寫系列文以來,最頂、最派的一篇! 不過很多觀念已經出現過,在【前端小試身手】系列裡面,每次的腳本都是主打實用, 因此cookie或localstorage這種技術當然都會運用到。 ## JS原始碼 ``` // ==UserScript== // @license MIT // @name 👮思想犯封殺 // @namespace http://tampermonkey.net/ // @version 0.1 // @description 把廢文製造機轟出去 // @author You // @match https://ithelp.ithome.com.tw/articles* // @match https://ithelp.ithome.com.tw/users/* // @icon https://www.google.com/s2/favicons?sz=64&domain=ithome.com.tw // @grant none // @run-at document-end // ==/UserScript== let URL = window.location.href; let ArticleSite = "https://ithelp.ithome.com.tw/articles?tab=tech" let UserSite = "https://ithelp.ithome.com.tw/users/" // 判斷URL的開頭部分 if (URL.startsWith(ArticleSite)) { //文章頁執行清理垃圾程序 CleanGarbage(); } else if (URL.startsWith(UserSite)) { UserCheck(); } else { console.log("這邊不執行腳本"); } // 餅乾儲存的機制函數------------------------------------------------- function setListInCookie(list) { document.cookie = 'myList=' + JSON.stringify(list) + '; expires=Wed, 31 Dec 2099 23:59:59 GMT;'; } // 從 Cookie 中獲取 list function getListFromCookie() { var cookieValue = document.cookie.replace(/(?:(?:^|.*;\s*)myList\s*\=\s*([^;]*).*$)|^.*$/, "$1"); return JSON.parse(cookieValue) || []; } function CleanGarbage(){ // 從本地存儲中獲取數據 var myListData = localStorage.getItem("myList"); // 解析數據到變量 list var list = myListData ? JSON.parse(myListData) : []; //list = getListFromCookie()||[]; // 儲存要刪除的字符串名單 // 找到所有CLASS是"qa-list__info-link"的<a>元素 var linkElements = document.querySelectorAll('.qa-list__info-link'); var removedCount = 0; // 初始化已清除的垃圾數量 for (var j = 0; j < list.length; j++) { // 遍歷這些<a>元素,確保文本內容包含"伍貳捌",然後刪除其父元素 for (var i = 0; i < linkElements.length; i++) { if (linkElements[i].textContent.includes(list[j])) { // 開始向上查找父元素 var parentElement = linkElements[i].parentElement; while (parentElement) { // 如果找到具有"classname"為"qa-list"的<div>元素,則刪除它 if (parentElement.classList.contains('qa-list')) { parentElement.remove(); removedCount++; // 增加清除的數量 console.warn('抓到"'+list[j]+'"這位思想犯'); break; // 找到並刪除後,結束循環 } parentElement = parentElement.parentElement; } } } if (removedCount>0){ // 顯示已清除的垃圾數量 console.log('已清除他的 ' + removedCount + ' 篇垃圾');} removedCount=0; } } //------------------------------------------------------- // 為了防止五百八改名,我們針對他的ID去ajax得到他最新的名稱 function FindBitch() { // 使用 Fetch API 獲取指定 URL 的內容 return fetch("https://ithelp.ithome.com.tw/users/20163468") .then(response => response.text()) .then(data => { // 創建一個臨時 div 元素以容納頁面內容 var tempDiv = document.createElement("div"); tempDiv.innerHTML = data; // 查找 class 為 "profile-header__name" 的元素 var profileNameElement = tempDiv.querySelector(".profile-header__name"); if (profileNameElement) { // 刪除元素內的所有 <span> 元素 var spanElements = profileNameElement.querySelectorAll("span"); spanElements.forEach(function(span) { span.remove(); }); // 讀取元素的文本內容,去掉前導和尾隨空格 var text = profileNameElement.textContent.trim(); // 返回處理後的文本內容 return text; } else { return "未找到元素"; } }) .catch(error => { console.error("發生錯誤: " + error); return "發生錯誤"; }); } //------------------------------------------------------- function UserCheck(){ //轉換資料從餅乾到localstorage var currentCookieValue = getCookie("myList"); // 2. 存儲數據到本地存儲 if (currentCookieValue) { var list = JSON.parse(currentCookieValue); // 存儲到本地存儲 localStorage.setItem("myList", JSON.stringify(list)); }else{ FindBitch() .then(text => { let FirstKill = [text]; setListInCookie(FirstKill); }) .catch(error => { console.error("找不到五百八:", error); }); } // 刪除不需要的ID document.querySelector('.profile-header__account').remove(); //封殺按鈕------------------------------------------------- // 找到具有class為"profile-header__right"的元素 var profileRightElement = document.querySelector('.profile-header__right'); var pullRightElement = profileRightElement.querySelector('.pull-right'); // 創建一個新按鈕元素 var BlockBtn = document.createElement('button'); BlockBtn.textContent = '封殺'; // 添加樣式和類名到按鈕 BlockBtn.style.marginTop = '10px'; BlockBtn.style.width = '100%'; BlockBtn.className = 'btn btn-trace trace_btn_border BlockBtn'; // 將按鈕元素添加到"pull-right"元素內部 pullRightElement.appendChild(BlockBtn); // 通緝犯名單的cookie------------------------------------------------ // 從 Cookie 中加載 list(例如,頁面加載時) list = getListFromCookie()||[]; let UserBlock = document.querySelector('.profile-header__name'); let text = UserBlock.textContent.trim(); // 如果使用者已經在封殺名單內的判斷,已存在或不存在 if (list.includes(text)) { BlockStart(BlockBtn); } else { // 針對封殺按鈕進行監聽事件 BlockBtn.addEventListener('click', function() { list.push(text); setListInCookie(list); //先加入到名單內,然後再執行封殺事件 BlockStart(); // 輸出到控制台 console.log('黑名單新增:' + text); //本地儲存機制----------------------------- let currentCookieValue = getCookie("myList"); let list2 = JSON.parse(currentCookieValue); // 存儲到本地存儲 localStorage.setItem("myList", JSON.stringify(list2)); }); } } // ------------------------------------------------ //封殺事件的函數 function BlockStart(){ let BlockBtn = document.querySelector('.BlockBtn'); BlockBtn.textContent = '已封殺'; BlockBtn.disabled = true; BadText(); BadImg(); } //封殺事件函數裡面的細項函數 function BadText(){ // 標記這傢夥是垃圾------------------------------------------------- let UserBlock = document.querySelector('.profile-header__name'); let newHeading = document.createElement('h1'); newHeading.textContent = '思想通緝犯'; // 把思想通緝犯這幾個大字加上去 UserBlock.parentElement.insertBefore(newHeading, UserBlock); UserBlock.style.textDecoration = "line-through"; UserBlock.style.color = "red"; } function BadImg(){ //圖片進行網點作業XD------------------------------------------------- var originalImage = document.querySelector('.profile-header__avatar'); // 創建一個包含交叉紅線的覆蓋層 <div> 元素 var overlayDiv = document.createElement('div'); overlayDiv.style.position = 'absolute'; overlayDiv.style.width = '150px'; overlayDiv.style.height = '150px'; overlayDiv.style.background = 'linear-gradient(45deg, black 50%, transparent 50%), linear-gradient(-45deg, black 50%, transparent 50%)'; overlayDiv.style.backgroundSize = '5px 5px, 5px 5px'; overlayDiv.style.backgroundPosition = '0 0, 0 2px'; // 將覆蓋層疊加到圖片上 originalImage.parentNode.appendChild(overlayDiv); // 設置覆蓋層的位置,以與原始圖像對齊 overlayDiv.style.top = originalImage.offsetTop + 'px'; overlayDiv.style.left = originalImage.offsetLeft + 'px'; // 設置覆蓋層的z-index,以確保它在圖片上方 overlayDiv.style.zIndex = '2'; } ``` ## 觀念筆記 這個腳本開發足足花了我一整個晚上,將近八小時之久。 有趣的是,其中為了進行測試才選某些user當作隱藏對象,否則腳本執行上會出錯。 細心的人若觀察原始碼,也會發現裡面有個firstKill, 那是必須要的段落,先設置好cookie的首要內容物,才有辦法繼續操作下去( ิ◕㉨◕ ิ) 也就是初始化的概念XD 另一個有趣的點是,為了防止使用者改名導致腳本出錯,我甚至不惜再寫一段ajax, 去更新一下ID,這樣不管人家怎麼改,都逃不了, 要改成「別抓我」也沒用,這個腳本都還是可以run。 ## 心得後記 我只能說這篇是自從「備份IT幫發文、一眼全覽」最強的JS教學文章! 超派,真的不騙( メ∀・) 有些人喜歡看前端小試身手,有些人喜歡前端動手玩創意; 其實這兩個系列的本質都是JS的教學,是可以互相連接的,但也有很多不同的重心。 這個系列就是以腳本為主,重點在於創意與發想,打到使用者痛點。 【前端動手玩創意】則是以建構網站為起點, 任何元素與概念都會變成網頁上的一部分,算是比較基礎工的建立。 如果對JS的強大感興趣,那麼可以把這兩個系列交互看,反覆的閱讀、實際動手操作, 這樣一來的學習非常踏實,甚至比YT學習都來的高效率、實際。 尤其此系列都是原創發想的腳本,當然超強! 喜歡記得關注,未來還有更多超酷的前端內容可以玩,下課⧸⎩⎠⎞͏(・∀・)⎛͏⎝⎭⧹

前端可以這樣玩??JS腳本-IMG複製大師,懶人專用小腳本

## 轉載自 https://ithelp.ithome.com.tw/articles/10338052 ## 前情提要 有時候我們需要使用一些圖片,勢必需要圖片的網址,這時候, 也許你可以本機上傳,然後去把網址抓好, 又或者你可以找到網路上別人的圖片,右鍵開啟新分頁,再複製網址; 又或者直接打開檢查(F12)找到src的網址去複製。 光聽這個流程我就覺得,心累。 這時候前端大師,就會寫一套簡單的小腳本來完成這個白爛的工作。 ## 腳本下載 ![](https://i.imgur.com/D1s1kxi.png) https://greasyfork.org/zh-TW/scripts/477141-img%E8%A4%87%E8%A3%BD%E5%A4%A7%E5%B8%AB ## JS程式碼 ``` // ==UserScript== // @name IMG複製大師 // @namespace http://tampermonkey.net/ // @version 0.1 // @description 針對網頁上圖片,點擊就複製其網址 // @author You // @match *://*/* // @icon https://www.highcharts.com/demo/highcharts/spline-plot-bands // @grant none // ==/UserScript== let isEventActive = false; // 用於跟蹤事件的狀態 // 創建一個鏈接元素並設置其屬性 const toastrCssLink = document.createElement('link'); toastrCssLink.rel = 'stylesheet'; toastrCssLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/css/toastr.min.css'; // 創建一個腳本元素並設置其屬性 const toastrScript = document.createElement('script'); toastrScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js'; // 創建另一個腳本元素並設置其屬性 const toastrScript2 = document.createElement('script'); toastrScript2.src = 'https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/js/toastr.min.js'; // 將鏈接元素和腳本元素添加到文檔頭部 document.head.appendChild(toastrCssLink); document.head.appendChild(toastrScript); document.head.appendChild(toastrScript2); // 獲取頁面上的所有img元素 const images = document.querySelectorAll('img'); function removeClickHandler(image) { image.removeEventListener('click', clickHandler); } function toggleEvent() { if (isEventActive) { // 如果事件已經激活,則關閉事件 console.log('IMG複製大師關閉ꐦ°᷄д°᷅'); // 解除事件監聽 images.forEach(removeClickHandler); isEventActive = false; } else { // 如果事件尚未激活,則打開事件 console.log('IMG複製大師啟動ฅ^•ﻌ•^ฅ'); // 遍歷所有img元素 images.forEach((image) => { // 檢查圖像是否已加載 if (image.complete) { // 圖像已加載,直接添加點擊事件處理程序 addClickHandler(image); } else { // 圖像尚未加載,等待加載完成後再添加點擊事件處理程序 image.addEventListener('load', () => { addClickHandler(image); }); } }); isEventActive = true; } } document.addEventListener('keydown', (event) => { if ((event.key === 'q' && event.ctrlKey) || event.key === 'F8') { toggleEvent(); // 切換事件的狀態 } }); function transformImageUrl(url) { // 使用正則表達式匹配URL中的目標部分 const regex = /https:\/\/cache.ptt.cc\/c\/https\/i.imgur.com\/([^?]+)/; const match = url.match(regex); if (match) { // 如果匹配成功,構建新的URL const imgurId = match[1]; return `https://i.imgur.com/${imgurId}`; } else { // 如果沒有匹配到目標部分,返回原始URL return url; } } // 創建一個函數,用於添加點擊事件處理程序並處理圖像的src function addClickHandler(image) { image.addEventListener('click', clickHandler); } // 創建一個函數,用於處理點擊事件 function clickHandler() { let src = this.src; src = transformImageUrl(src); const textArea = document.createElement('textarea'); textArea.value = src; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); // 使用 toastr 進行通知 toastr.success('網址複製完成: ' + src); } ``` ## 觀念筆記 這個小腳本總共使用了大概三種觀念, 第一種就是toast的套件使用, 第二個是監聽鍵盤做開關, 第三個則是程式本身對於img的監聽,把src複製到剪貼簿。 ### 第一部分:套件 使用套件在於腳本,必須利用appendChild,先使用create的API製作出放置CDN的link元素, 再把它append到網頁上,則可以使用其套件。 ### 第二部分:監聽鍵盤 這腳本啟動與關閉是透過監聽鍵盤的,ctrl+q或F8這部分就是純粹監聽。 ``` document.addEventListener('keydown', (event) => { if ((event.key === 'q' && event.ctrlKey) || event.key === 'F8') { toggleEvent(); // 切換事件的狀態 } }); ``` 裡面寫了一個toggleEvent是因為想要設定可以開開關關。 ### 第三部分:程式本身 這邊有一點小技巧是複製到剪貼簿,其實以前文章也有寫過教學,是個常見實用的招數: ``` const textArea = document.createElement('textarea'); textArea.value = src; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); ``` 再來就是針對網頁上全部的img元素,去監聽點擊事件、移除事件這兩個。 ``` const images = document.querySelectorAll('img'); function addClickHandler(image) { image.addEventListener('click', clickHandler); } function removeClickHandler(image) { image.removeEventListener('click', clickHandler); } ``` 之後這兩個事件要寫在toggle裡面,當開啟的條件就執行新增事件;關閉就執行解除綁定 開啟也就是: ``` images.forEach((image) => { // 檢查圖像是否已加載 if (image.complete) { // 圖像已加載,直接添加點擊事件處理程序 addClickHandler(image); } else { // 圖像尚未加載,等待加載完成後再添加點擊事件處理程序 image.addEventListener('load', () => { addClickHandler(image); }); ``` 關閉則是: `images.forEach(removeClickHandler);` ## 心得 這次的小腳本功能非常簡單,寫起來邏輯也是很清晰透明,沒有什麼彎曲。 雖然簡單,但是寫起來功能非常實用! 這就是針對懶人專用的! 其實沒有人規定前端這種東西要寫得多大,多了不起/ᐠ。ꞈ。ᐟ\ 好像你很屌、很懂前端、框架,那個是一回事,能夠完成自己的需求也很美好。 很久沒更新了,這次的酷酷小腳本應該很爽⁽⁽٩(๑˃̶͈̀ ᗨ ˂̶͈́)۶⁾⁾ 好好的來玩前端吧,未來還有更大的宇宙!

[技術文章] 一篇文章馬上帶你學會JQuery!

## 前情提要 還在問說要不要學JQUERY?別問了,學就對了,沒人說學了一定要用好嗎。 以下讓我們先看一段純純的JS,就跟國中生的戀愛一樣, 牽牽小手,超純。 ## JS純 ``` // 純 JavaScript 選取元素清單 let list = document.querySelectorAll(".navigation li"); function activeLink(event) { // 使用 forEach 迴圈移除所有元素的 "hovered" 類別 list.forEach((item) => { item.classList.remove("hovered"); }); // 在滑鼠移過的元素上新增 "hovered" 類別 event.target.classList.add("hovered"); } // 使用 forEach 迴圈為每個元素加上滑鼠移過事件的監聽器 list.forEach((item) => { item.addEventListener("mouseover", activeLink); }); ``` ## 轉大人 接下來我們要轉大人了。 ### 選擇器 關於querySelectorAll這種querySelector很簡單,JQ都是用$這個字符來選取。 ## 修改class item.classList.remove("hovered"); event.target.classList.add("hovered"); 這兩個的話JQ有寫出兩隻API: removeClass addClass 所以很方便很好用 ## 事件監聽 addEventListener的話JQ則是用on來表達,這個看過幾次就能夠理解了唷! ## JQ的寫法寫出來 ``` // 使用 jQuery 選取元素清單 let list = $(".navigation li"); function activeLink() { // 移除所有元素的 "hovered" 類別 list.removeClass("hovered"); // 在滑鼠移過的元素上新增 "hovered" 類別 $(this).addClass("hovered"); } // 使用 jQuery 的 each 方法為每個元素加上滑鼠移過事件的監聽器 list.each(function () { $(this).on("mouseover", activeLink); }); ``` ## 心得 JQ到底有沒有被拋棄或是需不需要學習,其實都是假議題,一堆人嘴上用JQ,然後嘴砲JQ 我們身為學習者其實就是花個一小時 就能學起來的東西,沒必要故意閉著眼睛不去了解。 以上,一篇文章馬上帶你學會JQuery! 比較想要玩切版可以看這個: https://ithelp.ithome.com.tw/articles/10312922 [【前端動手玩創意】置頂按鈕,老梗經典|帶你的網頁搭電梯](https://ithelp.ithome.com.tw/articles/10312922)

用Github Page整理作品集

因為一直找不到工作(...汗)所以來整理作品 [成果展示](https://superyngo.github.io/to_do_list/) 基本上是參考[Let's Write](https://www.letswrite.tw/)做的[Demo](https://letswritetw.github.io/letswrite-food-check/) 在頁面下方有作品導覽以及Github連結 使用工具: 1.Github Page 2.[Google Tag Manager](https://tagmanager.google.com/#/home) 步驟: 1.申請GTM並取得GTM代碼 2.將GTM代碼插入所有專案的index.html中,並將所有專案上傳github,開啟page頁面 3.另外做一個myNavbat的github page,將所有專案連結放進去,可參考[我做的](https://github.com/superyngo/myNavbar) 4.用GTM的「自訂HTML」功能將myNavbar和github連結塞進所有專案中,參考代碼如下: ``` <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" /> <div class="myNav"></div> <div class="github"> <a class="githubHerf"> <i class="fa fa-github" aria-hidden="true"></i> </a> </div> <script> var githubHerf=document.querySelector(".githubHerf") githubHerf.setAttribute("href","https://github.com/superyngo"+{{Page Path}}) var myNav2 = document.querySelector(".myNav"); var iframe2 = document.createElement("iframe"); iframe2.setAttribute("src", "https://superyngo.github.io/myNavbar/"); iframe2.setAttribute("width", "100%"); iframe2.setAttribute("frameborder", "0"); myNav2.appendChild(iframe2) </script> <style> body{ background-color: #fff0df; } .github { position: fixed; left: 0; bottom: 0; display: flex; width: 100px; height: 100px; cursor: pointer; z-index: 2; :hover { position: absolute; left: 10px; bottom: 5px; font-size: 3rem; color: #fff; scale: 0.9; } } .github::before { content: ""; width: 0; height: 0; border-style: solid; border-width: 100px 0 0 100px; border-color: transparent transparent transparent #6e5494; } .github .fa { position: absolute; left: 10px; bottom: 5px; font-size: 3rem; color: #fff; } @media screen and (max-width: 600px) { :root { font-size: 12px; } .github { width: 70px; height: 70px; } .github::before { border-width: 70px 0 0 70px; } .github .fa { left: 8px; bottom: 4px; font-size: 2rem; } } </style> ``` 注意事項; 1.不知為何我GTM塞iframe一直不給過,所以用append的。 2.iframe內的超連結預設會在iframe內開啟,要避免只要在myNavbar的head裡加上<base target="_parent" />就可以了。 3當初做這些作業時完全沒考慮theme和RWD,花了一些時間但又不想花太多時間結果就勉勉強強用手機也都能開。 閒聊: 前陣子都在codeSignal上刷題,看別人解法真的很神,學很多... 有沒有北部公司缺人啊...投履歷都沒人回..中年轉行真的不行嗎...

用 JavaScript 輕鬆做出深色模式 vs 淺色模式的方法

我曾經不同意淺色和深色模式切換。 “切換開關是用戶系統偏好設置!”我會天真地感嘆,選擇讓 [prefers-color-scheme CSS 媒體查詢](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) 控制我個人網站上的主題。沒有切換。沒有選擇。 🫠 自從黑暗模式出現以來,我一直是它的用戶。但最近,我更喜歡在輕型模式下使用_**一些**_網站和工具 - 包括我的個人網站 - 同時將我的系統設置牢牢地保留在黑暗中。我需要一個開關。我需要一個選擇!其他人也是如此。 在這篇文章中,我將向您展示如何使用 JavaScript 為我的網站建置終極主題 Toggle™️: 1. 在本地瀏覽器存儲中存儲和檢索主題首選項, 1. 退回到用戶系統首選項, 1. 如果未檢測到上述情況,則回退到默認主題。 TL;DR:[這是 CodePen 上的程式碼](https://codepen.io/whitep4nth3r/pen/VwEqrQL)。 原文出處:https://dev.to/whitep4nth3r/the-best-lightdark-mode-theme-toggle-in-javascript-368f ## 將資料屬性加入到 HTML 標記中 在 HTML 標記上,加入一個資料屬性,例如“data-theme”,並為其指定默認值“淺色”或“深色”。過去我使用自定義屬性“color-mode”而不是資料屬性(例如“color-mode=”light“”)。雖然這可行,但它沒有被歸類為有效的 HTML,而且我找不到任何相關文件!對此的任何見解都非常感激。 😅 ``` <html lang="en" data-theme="light"> <!-- all other HTML --> </html> ``` ## 通過 CSS 自定義屬性配置主題 在 CSS 中,通過“data-theme”每個值下的 [CSS 自定義屬性](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties)(或變數)配置主題顏色` 屬性。請注意,您不一定需要將 `:root` 與 `data-theme` 結合使用,但它對於不隨主題變化的全局屬性很有用(如下例所示)。 [在 MDN 上了解有關 :root CSS 偽類的更多訊息。](https://developer.mozilla.org/en-US/docs/Web/CSS/:root) ``` :root { --grid-unit: 1rem; --border-radius-base: 0.5rem; } [data-theme="light"] { --color-bg: #ffffff; --color-fg: #000000; } [data-theme="dark"] { --color-bg: #000000; --color-fg: #ffffff; } /* example use of CSS custom properties */ body { background-color: var(--color-bg); color: var(--color-fg); } ``` 在 HTML 標籤上手動切換“data-theme”屬性,您就會看到主題已經發生變化(只要您使用這些 CSS 屬性來設置元素的樣式)! ## 在 HTML 中建置一個切換按鈕 將 HTML 按鈕加入到您的網站標題或任何需要主題切換的位置。加入一個 `data-theme-toggle` 屬性(稍後我們將使用它來定位 JavaScript 中的按鈕)和一個 [aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-label)如果您計劃在按鈕上使用圖標(例如太陽和月亮分別代表淺色和深色模式),以便螢幕閱讀器和輔助技術可以理解按鈕的用途互動按鈕。 ``` <button type="button" data-theme-toggle aria-label="Change to light theme" >Change to light theme (or icon here)</button> ``` ## 計算頁面加載時的主題設置 在這裡,我們將根據我所說的“_**偏好級聯**_”來計算主題設置。 ### 從本地存儲獲取主題首選項 我們可以使用 [JavaScript 中的 localStorage 屬性](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) 將用戶首選項保存在瀏覽器中,該首選項在會話之間持續存在(或直到它手動清除)。在 The Ultimate Theme Toggle™️ 中,存儲的用戶首選項是最重要的設置,因此我們將首先查找它。 頁面加載時,使用 localStorage.getItem("theme") 檢查之前存儲的首選項。在本文後面,我們將在每次按下切換按鈕時更新主題值。如果沒有本地存儲值,則該值為“null”。 ``` // get theme on page load localStorage.getItem("theme"); // set theme on button press localStorage.setItem("theme", newTheme); ``` ### 在 JavaScript 中檢測用戶系統設置 如果“localStorage”中沒有存儲的主題首選項,我們將使用 [window.matchMedia() 方法](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia)通過傳入媒體查詢字串。您只需計算一項設置即可實現首選項級聯,但下面的程式碼顯示瞭如何檢測淺色或深色系統設置。 ``` const systemSettingDark = window.matchMedia("(prefers-color-scheme: dark)"); // or const systemSettingLight = window.matchMedia("(prefers-color-scheme: light)"); ``` `window.matchMedia()` 返回一個 `MediaQueryList`,其中包含您請求的媒體查詢字串,以及它是否與用戶系統設置“匹配”(true/false)。 ``` { matches: true, media: "(prefers-color-scheme: dark)", onchange: null } ``` ### 回退到默認主題 現在您可以通過“window.matchMedia()”存取“localStorage”值和系統設置,您可以使用首選項級聯(本地存儲,然後系統設置)計算首選主題設置,並回退到默認主題您的選擇(這應該是您之前在 HTML 標記上指定的默認主題)。 我們將在頁面加載時執行此程式碼來計算當前的主題設置。 ``` function calculateSettingAsThemeString({ localStorageTheme, systemSettingDark }) { if (localStorageTheme !== null) { return localStorageTheme; } if (systemSettingDark.matches) { return "dark"; } return "light"; } const localStorageTheme = localStorage.getItem("theme"); const systemSettingDark = window.matchMedia("(prefers-color-scheme: dark)"); let currentThemeSetting = calculateSettingAsThemeString({ localStorageTheme, systemSettingDark }); ``` ## 加入事件監聽器到切換按鈕 接下來,我們將設置一個事件監聽器,以便在按下按鈕時切換主題。使用我們之前加入的資料屬性(`data-theme-toggle`)定位 DOM 中的按鈕,並加入一個[事件監聽器](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) 加入到單擊按鈕。 - 將新主題計算為字串 - 計算並更新按鈕文本(如果您在按鈕上使用圖標,則可以在此處進行切換) - 更新按鈕上的aria標籤 - 切換HTML標籤上的data-theme屬性 - 將新的主題首選項保存在本地存儲中 - 更新內存中的currentThemeSetting ``` // target the button using the data attribute we added earlier const button = document.querySelector("[data-theme-toggle]"); button.addEventListener("click", () => { const newTheme = currentThemeSetting === "dark" ? "light" : "dark"; // update the button text const newCta = newTheme === "dark" ? "Change to light theme" : "Change to dark theme"; button.innerText = newCta; // use an aria-label if you are omitting text on the button // and using sun/moon icons, for example button.setAttribute("aria-label", newCta); // update theme attribute on HTML to switch theme in CSS document.querySelector("html").setAttribute("data-theme", newTheme); // update in local storage localStorage.setItem("theme", newTheme); // update the currentThemeSetting in memory currentThemeSetting = newTheme; }); ``` 要確認“localStorage”正在更新,請打開開發工具,導航到“Application”選項卡,展開“Local Storage”並選擇您的站點。你會看到一個鍵:值列表;查找“主題”並單擊按鈕即可觀看其實時更新。重新加載您的頁面,您將看到保留的主題首選項! ![帶有開發工具的瀏覽器窗口在應用程式選項卡上打開。選擇whitepanther dot com上的本地存儲,顯示主題light瀏覽器中存儲的鍵值對。](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zosfts8gbby76vo1h40o.png) ## 把它們放在一起! 您現在可以通過以下方式建置您自己的終極主題切換™️: - 使用CSS自定義屬性來指定不同的主題顏色,通過HTML標籤上的資料屬性進行切換 - 使用 HTML 按鈕來啟動切換 - 使用首選項級聯計算頁面加載時的首選主題(本地存儲 > 系統設置 > 後備默認主題) - 單擊切換按鈕即可切換主題,並將用戶首選項存儲在瀏覽器中以供將來存取 這是完整的 CodePen,您可以在我的個人網站上查看工作版本。快樂切換! https://codepen.io/whitep4nth3r/pen/VwEqrQL

[好文分享] 太屌了吧!?用Class(類)製作Jquery的效果!

## 目錄 ##### [【前端動手玩創意】等待的轉圈圈效果 (1)](https://ithelp.ithome.com.tw/articles/10311621) ##### [【前端動手玩創意】google五星評分的星星(2)](https://ithelp.ithome.com.tw/articles/10311643) ##### [【前端動手玩創意】CSS-3D卡片翻轉效果(3) (今天難度頗高,想挑戰再進來!)](https://ithelp.ithome.com.tw/articles/10311672) ##### [【前端動手玩創意】一句CSS做出好看的hero section!(4)](https://ithelp.ithome.com.tw/articles/10311691) ##### [【前端動手玩創意】創造一個Skill bar(5)](https://ithelp.ithome.com.tw/articles/10311756) ##### [【前端動手玩創意】遮蔽廣告(D卡未登入)腳本、自定義新增名單(6)](https://ithelp.ithome.com.tw/articles/10311999) ##### [【前端動手玩創意】前端canvas截圖的招式!竟然有三招,可存成SVG或PNG (7)](https://ithelp.ithome.com.tw/articles/10312001) ##### [【前端動手玩創意】讓你的PDF檔案更難被抓取(8)](https://ithelp.ithome.com.tw/articles/10312046) ##### [【前端動手玩創意】哇操!你敢信?花式寫todo-list,body裡面一行都沒有也能搞?(9)](https://ithelp.ithome.com.tw/articles/10312056) ##### [【前端動手玩創意】卡片製作,才不是!是卡片製作器!(10)](https://ithelp.ithome.com.tw/articles/10312066) ## 前情提要 各位知道jquery,這個JS的函式庫提供我們非常多實用的方法,包裝好給我們用。 其中底層邏輯都還是跑不出js原生的概念, 今天就讓我們用原生JS的類來創造一些Jquery幫我們打包好的方法吧! 來掀開它的奧秘底牌。 ## 程式碼內容 假設我們要創造一個叫做Dquery的東西,該怎麼做呢: ``` class Dquery { constructor(selector) { this.elements = document.querySelectorAll(selector); //噴出DOM來 } css(property, value) { for (let i = 0; i < this.elements.length; i++) { this.elements[i].style[property] = value; } return this; //修改style } addClass(className) { for (let i = 0; i < this.elements.length; i++) { this.elements[i].classList.add(className); } return this; //增加class } removeClass(className) { for (let i = 0; i < this.elements.length; i++) { this.elements[i].classList.remove(className); } return this; } on(eventType, callback) { for (let i = 0; i < this.elements.length; i++) { this.elements[i].addEventListener(eventType, callback); } return this; //監聽事件的運作 } html(htmlString) { if (typeof htmlString === 'undefined') { return this.elements[0].innerHTML; } else { for (let i = 0; i < this.elements.length; i++) { this.elements[i].innerHTML = htmlString; } return this; } } } function $(selector) { return new Dquery(selector); } ``` ## 觀念筆記 其實概念就是先把底層邏輯刷過一遍 最後return this 就達成jquery的效果了唷。 ## 心得 Jquery曾經非常流行,儘管後來式微了,卻還是有一批死忠的粉絲鍾愛著它的簡便, 事實上它就是JS,所以討人喜歡也是正常的, 這次學會了用ES6類的概念,會讓我們在前端的功力大幅提升唷!!

HTML新標籤dialog簡化modal實作

看youtube看到的 新的<dialog></dialog>標籤內建開關api 範例: ``` const dialog = document.querySelector("dialog") dialog.show() // 打開dialog(原先位置) dialog.showModal() // 打開dialog(置中且屏蔽背景) dialog.close() // 關閉dialog ``` 幾個重點: 1.不用再用CSS display開關 2.用showModal()自動置中且屏蔽背景 3.modal設定css背景方式: ``` dialog::backdrop { background-color: hsl(250, 100%, 50%, 0.25); } ``` 4.可結合form使用,form提供新的method="dialog",或是form裡面的button設定formmethod="dialog"來關閉 ``` <dialog> <form method="dialog"> <input type="text" /> <button type="submit">Submit</button><!-- 不會submit--> <button formmethod="dialog" type="submit">Cancel</button><!-- 不會submit--> </form> </dialog> ``` 5.如果想要點擊dialog以外的地方自動關閉可用下列程式碼: ``` dialog.addEventListener("click", e => { const dialogDimensions = dialog.getBoundingClientRect() if ( e.clientX < dialogDimensions.left || e.clientX > dialogDimensions.right || e.clientY < dialogDimensions.top || e.clientY > dialogDimensions.bottom ) { dialog.close() } }) ``` 6.dialog.close()方法可以傳值到Dialog.returnValue ``` Dialog.close(valueToPass) const getValue=Dialog.returnValue ``` 參考資料: [Modals Will Never Be The Same - HTML dialog Element](https://blog.webdevsimplified.com/2023-04/html-dialog/) [影片 The New dialog HTML Element Changes Modals Forever ](https://www.youtube.com/watch?v=ywtkJkxJsdg&t=620s)

# JS油猴系列-遮蔽廣告(D卡未登入)腳本、自定義新增名單

## 前情提要 逛論壇或網站常常有惱人的蓋板廣告,要馬就是要你登入、註冊, 要馬就是搞一堆課程的販售廣告, 對重視使用者體驗的人來說,真的是一場噩夢! 為了解決這個問題,我模擬了AdBlock的其中一個功能,刪除頁面上的元素! 乾乾淨淨才是王道阿。 ## 效果 按下ctrl+Q啟動刪除元素模式: ![](https://i.imgur.com/411nh3Q.png) 被選取的會加上邊框色彩,讓人確認是否要刪除: ![](https://i.imgur.com/zPEKMhC.png) 點選確認後: ![](https://i.imgur.com/nnOY4Ji.png) 煩人的元素就此消失在這個永恆的宇宙裡。 ## 觀念筆記 ### 套件一:toastr ```javascript= //toast.js cdn匯入 const toastJS = ` <link href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css" rel="stylesheet" /> <script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js"></script>` $('body').append(toastJS); ``` ![](https://i.imgur.com/lNDch3V.png) [測試效果反應網站](https://codeseven.github.io/toastr/demo.html) ![](https://i.imgur.com/v95GUfO.png) [[十分鐘學習] toastr - 簡易提示訊息](https://ithelp.ithome.com.tw/articles/10197861) ### 套件二:SweetAlert2 ```javascript= //設定cdn使用Sweetalert套件 let Sweetalert2=`<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>` $('html').append(Sweetalert2) ``` ![](https://i.imgur.com/qUeTUlj.png) ![](https://i.imgur.com/FJH8EMX.png) 有提供很多API範例,還可以自己定義可愛的小動物XD [官方網站](https://sweetalert2.github.io/) ### 監聽鍵盤組合鍵 ``` document.addEventListener('keydown', function(e){ if(e.which==81 && e.ctrlKey==true){ //..... } ``` ### Object.assign 動態修改樣式 ![](https://i.imgur.com/AzZMRCc.png) [用JS動態修改CSS的三種寫法,花式JS玩弄樣式,嚇死你的同事!](https://codelove.tw/@JsLover0018/post/Ja64Kx) ## 油猴程式碼 ```javascript= // ==UserScript== // @name 刪除廣告元素 // @namespace http://tampermonkey.net/ // @version 0.1 // @description try to take over the world! // @author You // @match *://*/* // @require https://code.jquery.com/jquery-3.6.3.min.js // @icon https://www.hexschool.com/ // @grant none // ==/UserScript== (function() { //--------------------------------------- // 宣告變數 let set let Check = true; let List=[] //設定選取的border色彩效果 let styleNeon = `@keyframes neon-color { from { filter: hue-rotate(0deg); } to { filter: hue-rotate(360deg); } }` $('style:last').append(styleNeon) //設定cdn使用Sweetalert套件 let Sweetalert2=`<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>` $('html').append(Sweetalert2) //使用者點選某個元素 執行刪除元素function function SetDeleteEl(e){ e.preventDefault(); set=e.target.class console.log(set) const border={ border:"4px dotted red" } const Neon={ animation: "neon-color 2.5s linear infinite" } const NoBorder={ border:"none" } const StopNeon={ animation: "" } Object.assign(e.target.style,border) Object.assign(e.target.style,Neon) //e.target.remove() if (Check==true){ Swal.fire({ title: '確定要刪除這個元素?', showDenyButton: true, showCancelButton: false, confirmButtonText: '確認', denyButtonText: `取消`, }).then((result) => { /* Read more about isConfirmed, isDenied below */ if (result.isConfirmed) { e.target.remove() List.push(e.target.className) localStorage.setItem('List',List); Check=false; Swal.fire('成功刪除此元素!', '', 'success') } else if (result.isDenied) { Check=false; Swal.fire('動作已取消', '', 'info') Object.assign(e.target.style,NoBorder) Object.assign(e.target.style,StopNeon) }; }); } //選取流程結束後 解開選取流程的事件綁定 $('body').unbind('click',SetDeleteEl) } //toast.js cdn匯入 const toastJS = ` <link href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css" rel="stylesheet" /> <script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js"></script>` $('body').append(toastJS); // 當使用者按下Ctrl+Q 就執行主程序 document.addEventListener('keydown', function(e){ if(e.which==81 && e.ctrlKey==true){ RemoveListItem(); toastr.options = { // 參數設定[註1] "closeButton": false, // 顯示關閉按鈕 "debug": false, // 除錯 "newestOnTop": false, // 最新一筆顯示在最上面 "progressBar": false, // 顯示隱藏時間進度條 "positionClass": "toast-bottom-right", "preventDuplicates": false, // 隱藏重覆訊息 "onclick": null, // 當點選提示訊息時,則執行此函式 "showDuration": "300", // 顯示時間(單位: 毫秒) "hideDuration": "1000", // 隱藏時間(單位: 毫秒) "timeOut": "2000", // 當超過此設定時間時,則隱藏提示訊息(單位: 毫秒) "extendedTimeOut": "1000", // 當使用者觸碰到提示訊息時,離開後超過此設定時間則隱藏提示訊息(單位: 毫秒) "showEasing": "swing", // 顯示動畫時間曲線 "hideEasing": "linear", // 隱藏動畫時間曲線 "showMethod": "fadeIn", // 顯示動畫效果 "hideMethod": "fadeOut" // 隱藏動畫效果 } toastr.success( "已開啟刪除元素模式!" ); $('body').on('click',SetDeleteEl) Check=true }}) function RemoveListItem(){ if (localStorage.getItem('List')!=null){ item=localStorage.getItem('List').split(',') for (i=0;i<item.length;i++){ item[i]='.'+item[i] item[i]=item[i].replaceAll(' ','.') try{ document.querySelector(item[i]).remove()} catch{} } // item='.'+item // item=item.replaceAll(' ','.') // document.querySelector(item).remove() } } //-------------------------------------------------------- })(); ``` ## 心得 短短150行左右的程式碼,就寫得好辛苦!(汗 過去寫過很多次類似的東西,所以基本上沒有卡關的地方, 只是設計邏輯、嘗試的過程還是比想像的還要耗精神( ´•̥̥̥ω•̥̥̥ ) 但寫出來比過去快很多,也比過去好看超級多的。 給自己一個讚讚! 重新把localstorge複習一遍,Object.assign、套件使用等。 算是一個確確實實的實力鞏固過程。 ## 延伸 未來可以新增控制黑名單元素的UI。 可以新增、刪除,看有哪些元素被列入要刪除的黑名單。

在 React 使用 useWorker 來跑多執行緒,大幅改善 UX 效能

使用 useWorker 在單獨的執行緒中,處理昂貴且阻塞 UI 的任務。 眾所周知,Javascript 是一種單線程語言。所以,做任何昂貴的任務,它都會阻塞 UI 互動。用戶需要等到它完成,才能執行剩餘的其他操作,這會帶來糟糕的用戶體驗。 為了克服和執行這些任務,Javascript 有一個名為 [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) 的解決方案,它允許在 web 中執行消耗效能的任務時,瀏覽器不會阻塞用戶界面,使用戶體驗非常流暢。 本篇文章簡單介紹這個 hook。 原文出處:https://dev.to/nilanth/multi-threaded-react-app-using-useworker-gf8 --- ## Web Workers Web Worker 是一個在後台執行而不影響用戶界面的腳本,因為它在一個單獨的線程而不是主線程中執行。所以它不會對用戶交互造成任何阻塞。 Web Worker 主要用於在 Web 瀏覽器中執行昂貴的任務,例如對大量資料進行排序、CSV 導出、圖像處理等。 ![工作線程](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p2ikpn8va49460arsuum.png) 參考上圖,我們可以看到一個昂貴的任務是由一個工作線程並行執行的,而不會阻塞主線程。當我們在主線程中執行相同的操作時,會導致 UI 阻塞。 ## React 中的 Concurrent mode 呢? React [並發模式](https://beta.reactjs.org/reference/react/startTransition) 不會並行執行任務。它將非緊急任務轉移,接著立即執行緊急任務。它使用相同的主線程來處理。 正如我們在下圖中看到的,緊急任務是使用上下文切換來處理的。例如,如果一個表正在呈現一個大型資料集,並且用戶試圖搜尋某些內容,React 會將任務切換為用戶搜尋,並首先處理它,如下所示。 ![任務切換](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7nc8978zlpi8gmrcxlnd.png) 當為同一任務使用 worker 時,表格渲染在一個單獨的線程中並行執行。檢查下圖。 ![反應工作者](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4tnoy22tqchmq8cuavdp.png) ## useWorker [useWorker](https://github.com/alewin/useWorker) 是一個通過 React Hooks 在簡單配置中使用 Web Worker API 的函式庫。它支持在不阻塞 UI 的情況下執行昂貴任務,支援 promise 而不是事件監聽器,還有一些值得注意的功能: 1. 結束超時 worker 的選項 2. 遠程依賴 3. Transferable 4. Worker status > useWorker 是一個 3KB 的庫 ## 為什麼不用 JavaScript 內置 web worker? 在使用 javascript web worker 時,我們需要加入一些複雜的配置,來設置多行程式碼的 worker。使用 useWorker,我們可以簡化 worker 的設置。讓我們在下面的程式碼中,看看兩者之間的區別。 ## 在 React App 中使用 JS Web Worker 時 ``` // webworker.js self.addEventListener("message", function(event) { var numbers = [...event.data] postMessage(numbers.sort()) }); ``` ``` //index.js var webworker = new Worker("./webworker.js"); webworker.postMessage([3,2,1]); webworker.addEventListener("message", function(event) { console.log("Message from worker:", event.data); // [1,2,3] }); ``` ## 在 React App 中使用 useWorker 時 ``` // index.js const sortNumbers = numbers => ([...numbers].sort()) const [sortWorker] = useWorker(sortNumbers); const result = await sortWorker([1,2,3]) ``` 正如我之前所說,與普通的 javascript worker 相比,`useWorker()` 簡化了配置。 讓我們與 React App 整合並執行高 CPU 密集型任務,來看看 useWorker() 的實際執行情況。 ## Quick Start 要將 useWorker() 加入到 React 專案,請使用以下命令 `yarn add @koale/useworker` 安裝套件後,導入 useWorker()。 `import { useWorker, WORKER_STATUS } from "@koale/useworker";` 我們從函式庫中導入 useWorker 和 WORKER_STATUS。 **useWorker()** 鉤子返回 workerFn 和 controller。 1. `workerFn` 是允許使用 web worker 執行函數的函數。 2.controller 由 status 和 kill 參數組成。 status 參數返回 worker 的狀態和用於終止當前執行的 worker 的 kill 函數。 讓我們用一個例子來看看 **useWorker()**。 使用 **useWorker()** 和主線程對大型陣列進行排序 首先建立一個SortingArray元件,加入如下程式碼 ``` //Sorting.js import React from "react"; import { useWorker, WORKER_STATUS } from "@koale/useworker"; import { useToasts } from "react-toast-notifications"; import bubleSort from "./algorithms/bublesort"; const numbers = [...Array(50000)].map(() => Math.floor(Math.random() * 1000000) ); function SortingArray() { const { addToast } = useToasts(); const [sortStatus, setSortStatus] = React.useState(false); const [sortWorker, { status: sortWorkerStatus }] = useWorker(bubleSort); console.log("WORKER:", sortWorkerStatus); const onSortClick = () => { setSortStatus(true); const result = bubleSort(numbers); setSortStatus(false); addToast("Finished: Sort", { appearance: "success" }); console.log("Buble Sort", result); }; const onWorkerSortClick = () => { sortWorker(numbers).then((result) => { console.log("Buble Sort useWorker()", result); addToast("Finished: Sort using useWorker.", { appearance: "success" }); }); }; return ( <div> <section className="App-section"> <button type="button" disabled={sortStatus} className="App-button" onClick={() => onSortClick()} > {sortStatus ? `Loading...` : `Buble Sort`} </button> <button type="button" disabled={sortWorkerStatus === WORKER_STATUS.RUNNING} className="App-button" onClick={() => onWorkerSortClick()} > {sortWorkerStatus === WORKER_STATUS.RUNNING ? `Loading...` : `Buble Sort useWorker()`} </button> </section> <section className="App-section"> <span style={{ color: "white" }}> Open DevTools console to see the results. </span> </section> </div> ); } export default SortingArray; ``` 這裡我們配置了 useWorker 並傳遞了 bubleSort 函數來使用 worker 執行昂貴的排序。 接下來,將以下程式碼加入到 App.js 元件,並導入 SortingArray 元件。 ``` //App.js import React from "react"; import { ToastProvider } from "react-toast-notifications"; import SortingArray from "./pages/SortingArray"; import logo from "./react.png"; import "./style.css"; let turn = 0; function infiniteLoop() { const lgoo = document.querySelector(".App-logo"); turn += 8; lgoo.style.transform = `rotate(${turn % 360}deg)`; } export default function App() { React.useEffect(() => { const loopInterval = setInterval(infiniteLoop, 100); return () => clearInterval(loopInterval); }, []); return ( <ToastProvider> <div className="App"> <h1 className="App-Title">useWorker</h1> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <ul> <li>Sorting Demo</li> </ul> </header> <hr /> </div> <div> <SortingArray /> </div> </ToastProvider> ); } ``` 我們已將 React 徽標加入到 **App.js** 元件,該組件每 **100ms** 旋轉一次,以直觀地表示執行昂貴任務時的阻塞和非阻塞 UI。 執行上面的程式碼時,我們可以看到兩個按鈕 Normal Sort 和 Sort using **useWorker()**。 接下來,單擊 Normal Sort 按鈕在主線程中對陣列進行排序。我們可以看到 React 徽標停止旋轉幾秒鐘。由於排序任務阻塞了UI渲染,所以排序完成後logo又開始旋轉了。這是因為兩個任務都在主線程中處理。檢查以下gif ![正常排序](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k74iuo6qd36qbvpr9iaz.gif) 讓我們使用 [chrome 性能記錄](https://developer.chrome.com/docs/devtools/performance/) 檢查其性能分析。 ![性能正常](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8eclh8a6o7g9xzta03ic.png) 我們可以看到 Event: click 任務用了 **3.86 秒** 來完成這個過程,它阻塞了主線程 3.86 秒。 接下來,讓我們嘗試使用 **useWorker()** 選項進行排序。點擊它的時候我們可以看到 react logo 還在不間斷的旋轉。由於 useWorker 在不阻塞 UI 的情況下在後台執行排序。這使得用戶體驗非常流暢。檢查以下gif ![工人排序](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xe5ci0p3hy5nca4321cg.gif) 您還可以使用 `sortWorkerStatus` 在控制台中看到 worker 狀態為 **RUNNING**、**SUCCESS**。 讓我們看看這種方法的性能分析結果 ![主線程](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bxsl696fld6u79dqpwra.png) ![工作線程](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fcnwe2lczguyxgfqftrj.png) 正如我們所看到的,第一張圖片表示主線程中沒有長時間執行的進程。在第二張圖片中,我們可以看到排序任務由一個單獨的工作線程處理。所以主線程沒有阻塞任務。 您可以在以下沙箱中試用整個範例。 https://codesandbox.io/embed/useworker-sorting-example-041qhc?fontsize=14&hidenavigation=1&theme=dark ## 何時使用worker 1.圖像處理 2. 排序或處理大資料集。 3. 大資料CSV或Excel導出。 4. 畫布繪圖 5. 任何 CPU 密集型任務。 ## useWorker 的限制 1. web worker 無權存取 window 物件和 document。 2. 當 worker 正在執行時,我們不能再次呼叫它,直到它完成或被中止。為了解決這個問題,我們可以建立兩個或多個 useWorker() 鉤子實例。 3. Web Worker 無法返回函數,因為響應是序列化的。 4. Web Workers 受限於最終用戶機器的可用 CPU 內核和內存。 ## 結論 Web Worker 允許在 React 應用程式中使用多線程來執行昂貴的任務而不會阻塞 UI。而 useWorker 允許在 React 應用程式中以簡化的掛鉤方法使用 Web Worker API。 Worker 不應該被過度使用,我們應該只在必要時使用它,否則會增加管理 Worker 的複雜性。