<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Zong-Rong Huang on Medium]]></title>
        <description><![CDATA[Stories by Zong-Rong Huang on Medium]]></description>
        <link>https://medium.com/@zongrong.h886?source=rss-e582e959bd07------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*BJDh7p5fbGoiYSJD.jpg</url>
            <title>Stories by Zong-Rong Huang on Medium</title>
            <link>https://medium.com/@zongrong.h886?source=rss-e582e959bd07------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 00:14:53 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@zongrong.h886/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Know your JavaScript find-* array methods]]></title>
            <link>https://medium.com/@zongrong.h886/know-your-javascript-find-array-methods-55e1678b92bf?source=rss-e582e959bd07------2</link>
            <guid isPermaLink="false">https://medium.com/p/55e1678b92bf</guid>
            <category><![CDATA[arrays]]></category>
            <category><![CDATA[typescript]]></category>
            <category><![CDATA[front-end-development]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[javascript]]></category>
            <dc:creator><![CDATA[Zong-Rong Huang]]></dc:creator>
            <pubDate>Thu, 20 Jul 2023 16:11:20 GMT</pubDate>
            <atom:updated>2023-07-20T17:10:20.767Z</atom:updated>
            <content:encoded><![CDATA[<h4>And other array methods with similar names and functionalities</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*jnHELPrEbFBk4w-_" /><figcaption>Photo by <a href="https://unsplash.com/@laughayette?utm_source=medium&amp;utm_medium=referral">Marten Newhall</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><h3>Overview</h3><p>JavaScript offers powerful array methods for various operations, some of which have similar names and functionalities.</p><p>In this article, I focus on the array methods prefixed with find- and explore their unique characteristics, comparing them with each other and also with other related array methods, to help JavaScript developers better distinguish and wield them.</p><h3>Know your find-* array methods</h3><p>JavaScript offers four array methods prefixed with find-:</p><ul><li>find()</li><li>findLast()</li><li>findIndex()</li><li>findLastIndex()</li></ul><p>These find-* array methods share three characteristics in syntax and return value.</p><p><strong>They require a callback function as argument</strong></p><p>For the find-* methods, the callback serves as a matching condition to check array elements with. When evaluated as truthy by the callback, the element is a match; when evaluated as falsy, it is a no-match.</p><p><strong>They return a matched array element or its index when there is one</strong></p><p>Let’s start with find() and findLast() first.</p><p>The two both return the first matched element no matter how many matches there are in the array.</p><p>They differ only in traversal direction: find() checks array elements from left to right while findLast() does so from right to left.</p><p>The example shows this directional difference. With the same array, find() returns a match at its beginning, but findLast() returns one that’s situated at the end.</p><pre>const list = [{name: &#39;A&#39;, age: 10}, <br>              {name: &#39;B&#39;, age: 20}, <br>              {name: &#39;C&#39;, age: 10}]<br><br>const cb = member =&gt; member.age === 10<br>const firstMatch = list.find(cb) // {name: &#39;A&#39;, age: 10}<br>const lastMatch = list.findLast(cb) // {name: &#39;C&#39;, age: 10}</pre><p>We should beware of the type of array elements being checked. When an element is of a primitive type–be it a string, number, or boolean–find() and findLast() return a distinct copy of the match. Changing the copy doesn’t affect the source array at all.</p><pre>const numbers = [1, 100, 200]<br><br>let match = numbers.find(number =&gt; number &lt; 100)<br>match++<br>console.log(match) // 2 <br>console.log(numbers) // [1, 100, 200]</pre><p>However, when an element is of a complex type, like an array or object, find() and findLast() return the match’s reference in memory. In the light of “copy by reference” of JavaScript, whatever changes made to the returned match are preserved in the source array.</p><pre>const list = [{name: &#39;A&#39;, age: 10}, <br>              {name: &#39;B&#39;, age: 20}, <br>              {name: &#39;C&#39;, age: 10}]<br><br>const match = list.find(obj =&gt; obj.age === 20)<br>match.age++<br><br>console.log(match) //  {name: &#39;B&#39;, age: 21}<br>console.log(list)  // [{name: &#39;A&#39;, age: 10}, <br>                   // {name: &#39;B&#39;, age: 21}, <br>                   // {name: &#39;C&#39;, age: 10}]<br>console.log(match === list[1]) // true</pre><p>findIndex() and findLastIndex() return the index of the first match in the array, but they also differ in traversal direction: findIndex() runs from left to right whereas findLastIndex() runs from right to left.</p><p>We should note that, in whichever traversal direction the methods run, we always get an index number in the conventional left-to-right ordering.</p><pre>const list = [{name: &#39;A&#39;, age: 10}, <br>              {name: &#39;B&#39;, age: 20}, <br>              {name: &#39;C&#39;, age: 10}]<br><br>const cb = member =&gt; member.age === 10<br>const firstMatchIndex = list.findIndex(cb) // 0<br>const lastMatchIndex = list.findLastIndex(cb) // 2</pre><p><strong>They return </strong><strong>undefined or </strong><strong>-1 when no match is found in the array</strong></p><p>If there’s not any array element matched, find() and findLast() return undefined, indicating that no such an element in the array meets the matching condition.</p><pre>const list = [{name: &#39;A&#39;, age: 10}, <br>              {name: &#39;B&#39;, age: 20}, <br>              {name: &#39;C&#39;, age: 10}]<br><br>const cb = member =&gt; member.age &lt; 0<br><br>const firstMatch = list.find(cb) // undefined<br>const lastMatch = list.findLast(cb) // undefined</pre><p>On the other hand, findIndex() and findLastIndex() return -1, an impossible index number that falls out of the zero-based ordering for array elements. -1 is a conventional return value for array and string methods–for example, indexOf() and lastIndexOf()–when no match is found.</p><pre>const list = [{name: &#39;A&#39;, age: 10}, <br>              {name: &#39;B&#39;, age: 20}, <br>              {name: &#39;C&#39;, age: 10}]<br><br>const cb = member =&gt; member.age &lt; 0<br>const firstMatchIndex = list.findIndex(cb) // -1<br>const lastMatchIndex = list.findLastIndex(cb) // -1</pre><h3>find-* methods and their characteristics</h3><p>In brief, find-* array methods rely on the callback to evaluate every element in the array. They either return the first matched array element or its index. If there’s no match found, they either return undefined or -1 to indicate non-existence of such matches.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/642/1*dHnDn0igvPVY_QObUN4hsQ.png" /></figure><h3>Other similar array methods</h3><p>These find-* methods are not the only methods available in JavaScript arrays to find a matched element or its index. Let’s check out some other methods with similar functionality.</p><ul><li>find() and findLast() vs filter()</li></ul><p>These three all require a callback function as the argument, and evaluate all the elements inside an array with it.</p><p>The major difference lies in their return values.</p><p>When a match is found in the array, find() and findLast() return only the first matched element, depending on the traversal direction taken. filter() returns a new array containing all matched elements.</p><pre>const numbers = [-10, -1, 0, 1, 10]<br><br>const cb = number =&gt; number &gt; 0<br><br>const firstMatch = numbers.find(cb)   // 1<br>const lastMatch = numbers.findLast(cb) // 10<br>const allMatches = numbers.filter(cb)  // [1, 10]</pre><p>When no match is found, find() and findLast() return undefined, while filter() returns an empty array.</p><pre>const numbers = [-10, -1, 0, 1, 10]<br><br>const cb = number =&gt; number &gt; 100<br><br>const firstMatch = numbers.find(cb)   // undefined<br>const lastMatch = numbers.findLast(cb) // undefined<br>const allMatches = numbers.filter(cb)  // []</pre><ul><li>findIndex() vs indexOf()</li></ul><p>These two are close in their names and both return an index number. However, they take different types of argument to fulfill their job.</p><p>findIndex() requires a callback function but indexOf() requires just a value. By passing in a value to indexOf(), we are asking if there is any element in the array that is identical to the value (in the sense of strict equality).</p><p>If there is such a match, indexOf() returns the match’s index, that is, its position in the array. If not, they both return -1.</p><pre>const numbers = [-10, -1, 0, 1, 10]<br><br>const matchId1 = numbers.indexOf(0) // 2<br>const matchId2 = numbers.findIndex(number =&gt; number === 0) // 2<br><br>const noMatchId1 = numbers.indexOf(100) // -1<br>const noMatchId2 = numbers.findIndex(number =&gt; number === 100) // -1</pre><ul><li>findLastIndex() vs lastIndexOf()</li></ul><p>They are the exact twin of the findIndex() and indexOf() methods, despite the fact that they traverse over an array in the opposite direction. findLastIndex() requires a callback argument whereas lastIndexOf() requires a value argument. They either return the index of the first match or -1 when there’s none.</p><pre>const numbers = [-10, -1, 0, 1, 10]<br><br>const matchId1 = numbers.lastIndexOf(0) // 2<br>const matchId2 = numbers.findLastIndex(number =&gt; number === 0) // 2<br><br>const noMatchId1 = numbers.lastIndexOf(100) // -1<br>const noMatchId2 = numbers.findLastIndex(number =&gt; number === 100) // -1</pre><h3>Summary</h3><p>The find-* array methods all require a callback to evaluate every element in an array, allowing us to search through an array flexibly. When a match is found, they return the first matched array element or its index, or undefined or -1 when there’s no match.</p><p>We’ve also checked out some other similar array methods. filter() requires a callback to evaluate array elements too, but it returns an new array with all the elements that match.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/577/1*xdwUoztKLG9x_o7nt2w7oQ.png" /></figure><p>indexOf() and lastIndexOf() require a value, not a callback, and return the index of the first match that’s strictly equal to the value.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/667/1*9T6Ir9sDq_0yNZiWaaBObw.png" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=55e1678b92bf" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[認識 JavaScript find-* 陣列方法]]></title>
            <link>https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-javascript-find-%E9%99%A3%E5%88%97%E6%96%B9%E6%B3%95-135bca441567?source=rss-e582e959bd07------2</link>
            <guid isPermaLink="false">https://medium.com/p/135bca441567</guid>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[arrays]]></category>
            <category><![CDATA[前端]]></category>
            <category><![CDATA[教學]]></category>
            <category><![CDATA[陣列]]></category>
            <dc:creator><![CDATA[Zong-Rong Huang]]></dc:creator>
            <pubDate>Sat, 01 Jul 2023 05:30:41 GMT</pubDate>
            <atom:updated>2023-07-17T10:31:58.830Z</atom:updated>
            <content:encoded><![CDATA[<h4>和 filter()、indexOf() 和 lastIndexOf() 的差異</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*b8dC7zQr_UDoCkj5" /><figcaption>Photo by <a href="https://unsplash.com/@pawel_czerwinski?utm_source=medium&amp;utm_medium=referral">Pawel Czerwinski</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><h3>概述</h3><p>JavaScript 歷經多年演進，內建相當多的語法可供使用。然而，其中許多語法不僅名稱類似，甚至連使用方式也極為雷同。對於新手或有經驗的開發者來說，要分辨它們的差異並在適當時機使用並不是一件簡單的事。</p><p>本文聚焦在 JavaScript 的 find-* 陣列方法，接著再與其他名稱/使用方式相近的陣列方法相互比較，更能清楚判斷應該使用哪個語法。</p><h3>認識 find-* 的陣列方法</h3><p>JavaScript 有以下 find 開頭的陣列方法：</p><ul><li>find()</li><li>findLast()</li><li>findIndex()</li><li>findLastIndex()</li></ul><p>這些 find-* 方法的特性如下：</p><ul><li><strong>傳入 callback 函式作為 argument</strong></li></ul><p><strong>callback 函式負責判斷陣列元素是否符合條件。</strong>如果陣列元素放入 callback 的執行結果為 truthy 值，代表符合條件。如果執行結果為 falsy 值，代表不符合條件。</p><ul><li><strong>發現符合條件的陣列元素，則回傳元素或元素的 index</strong></li></ul><p>當有 n 個陣列元素符合條件時 (n &gt;= 1)，find() 和 findLast() 會回傳第一個符合條件的陣列元素。</p><p>差別在於 find() 從陣列開頭 (最左側) 開始遍歷陣列元素，而 findLast() 則是從陣列尾端 (最右側) 開頭遍歷元素：</p><pre>const list = [{name: &#39;A&#39;, age: 10}, <br>              {name: &#39;B&#39;, age: 20}, <br>              {name: &#39;C&#39;, age: 10}]<br><br>const cb = member =&gt; member.age === 10<br><br>const firstMatch = list.find(cb) // {name: &#39;A&#39;, age: 10}<br>const lastMatch = list.findLast(cb) // {name: &#39;C&#39;, age: 10}</pre><p>當元素是字串、數字、布林值等原始型別的值，find() 和 findLast() 會回傳元素的複本 copy。修改複本並不會影響到原來的陣列：</p><pre>const numbers = [1, 100, 200]<br><br>let match = numbers.find(number =&gt; number &lt; 100)<br><br>match++<br><br>console.log(match) // 2 <br>console.log(numbers) // [1, 100, 200]</pre><p><strong>當元素是陣列或物件之類的複雜型別，則是回傳元素的 reference (記憶體位址)</strong>；要注意的是，一旦修改了這類元素的內容，也會同時影響到原來的陣列：</p><pre>const list = [{name: &#39;A&#39;, age: 10}, <br>              {name: &#39;B&#39;, age: 20}, <br>              {name: &#39;C&#39;, age: 10}]<br><br>const match = list.find(obj =&gt; obj.age === 20)<br><br>match.age++<br><br>console.log(match) //  {name: &#39;B&#39;, age: 21}<br><br>console.log(list)  // [{name: &#39;A&#39;, age: 10}, <br>                   // {name: &#39;B&#39;, age: 21}, <br>                   // {name: &#39;C&#39;, age: 10}]<br><br>console.log(match === list[1]) // true</pre><p>與 find() 和 findLast() 類似，findIndex() 和 findLastIndex() 則會分別從陣列開頭或尾端開始遍歷陣列，只是它們的回傳值是第一個符合條件的元素的 index。雖然遍歷陣列的方式不同，但回傳的 index 和一般認知的 index 一樣，都是由左至右、從 0 開始編號：</p><pre>const list = [{name: &#39;A&#39;, age: 10}, <br>              {name: &#39;B&#39;, age: 20}, <br>              {name: &#39;C&#39;, age: 10}]<br><br>const cb = member =&gt; member.age === 10<br><br>const firstMatchIndex = list.findIndex(cb) // 0<br>const lastMatchIndex = list.findLastIndex(cb) // 2</pre><ul><li><strong>找不到符合條件的陣列元素，則回傳 </strong><strong>undefined 或 </strong><strong>-1</strong><br>如果所有陣列元素皆不符合 callback 的指定條件 (皆為 falsy 值)，find() 和 findLast() 方法則會回傳 undefined，代表找不到符合的元素：</li></ul><pre>const list = [{name: &#39;A&#39;, age: 10}, <br>              {name: &#39;B&#39;, age: 20}, <br>              {name: &#39;C&#39;, age: 10}]<br><br>const cb = member =&gt; member.age &lt; 0<br><br>const firstMatch = list.find(cb) // undefined<br>const lastMatch = list.findLast(cb) // undefined</pre><p>findIndex() 和 findLastIndex() 則回傳 -1。由於找不到符合的元素，自然也沒有對應的 index，就以 -1 代表不可能出現的 index：</p><pre>const list = [{name: &#39;A&#39;, age: 10}, <br>              {name: &#39;B&#39;, age: 20}, <br>              {name: &#39;C&#39;, age: 10}]<br><br>const cb = member =&gt; member.age &lt; 0<br><br>const firstMatchIndex = list.findIndex(cb) // -1<br>const lastMatchIndex = list.findLastIndex(cb) // -1</pre><h4>小整理：</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/487/1*pV69YMq74AZ1pJcmX_cy4Q.png" /></figure><p>簡言之，find-* 陣列方法都是為了「找到符合判斷條件的元素」或「找到符合判斷條件的元素的 index」，需要傳入 callback 函式作為判斷條件。<br>find-* 方法翻譯成自然語言的大致意義如下：</p><blockquote><em>請依據條件 (callback 函式)，判斷陣列內是否有符合條件的元素。如果有的話，請回傳給我符合的第一個元素或是這個元素的 index。如果找不到符合的元素，請給我 </em><em>undefined 或 </em><em>-1。</em></blockquote><h3>其他相似的陣列方法</h3><p>find-* 陣列方法能在陣列中找到符合條件的元素，但還有其他方法也有類似的作用。一起來認識之間的差異！</p><ul><li>find() 和 findLast() vs filter()</li></ul><p>這三個陣列方法都需要傳入 callback 函式來判斷元素是否符合條件。主要差異在於它們的回傳值。</p><p>當存在符合條件的元素時，find() 和 findLast() 會直接回傳第一個符合的元素 (兩者遍歷陣列的方向不同)。filter() 則是回傳一個新陣列，內含所有符合條件的元素：</p><pre>const numbers = [-10, -1, 0, 1, 10]<br><br>const cb = number =&gt; number &gt; 0<br><br>const firstMatch = numbers.find(cb)   // 1<br>const lastMatch = numbers.findLast(cb) // 10<br>const allMatches = numbers.filter(cb)  // [1, 10]</pre><p>當陣列中沒有符合條件的元素時，find() 和 findLast() 皆回傳 undefined，而 filter() 則回傳空陣列：</p><pre>const numbers = [-10, -1, 0, 1, 10]<br><br>const cb = number =&gt; number &gt; 100<br><br>const firstMatch = numbers.find(cb)   // undefined<br>const lastMatch = numbers.findLast(cb) // undefined<br>const allMatches = numbers.filter(cb)  // []</pre><ul><li>findIndex() vs indexOf()</li></ul><p>這兩個方法名稱非常相似，有如雙胞胎，而且都會回傳元素的 index。差別在於需要傳入的 argument 類型不同。findIndex() 需要傳入 callback 作為判斷條件，再回傳符合的元素 index。</p><p>indexOf() 的意義是在查詢「某個值在陣列中的 index 位置」。不須傳入任何條件，只需要傳入要找的值，就會確認陣列內有無嚴格意義上相等 (===) 的元素，接著回傳該元素的 index (在陣列內的位置)。</p><p>當找不到符合的元素時，這兩個方法都會回傳 -1。</p><pre>const numbers = [-10, -1, 0, 1, 10]<br><br>const matchId1 = numbers.indexOf(0) // 2<br>const matchId2 = numbers.findIndex(number =&gt; number === 0) // 2<br><br>const noMatchId1 = numbers.indexOf(100) // -1<br>const noMatchId2 = numbers.findIndex(number =&gt; number === 100) // -1</pre><ul><li>findLastIndex() vs lastIndexOf()</li></ul><p>這是另一組名稱相似的雙胞胎方法，同樣都會回傳符合元素的 index；找不到符合的元素時，都會回傳 -1。</p><p>差異也和上一組雙胞胎方法一樣：findLastIndex() 需要 callback 函式作為判斷條件，而 lastIndex() 只需傳入值。</p><pre>const numbers = [-10, -1, 0, 1, 10]<br><br>const matchId1 = numbers.lastIndexOf(0) // 2<br>const matchId2 = numbers.findLastIndex(number =&gt; number === 0) // 2<br><br>const noMatchId1 = numbers.lastIndexOf(100) // -1<br>const noMatchId2 = numbers.findLastIndex(number =&gt; number === 100) // -1</pre><h3>結論</h3><p>本文以 find-* 方法為主，介紹其特色：</p><ul><li>find-* 方法皆需要傳入 callback 函式作為判斷條件，接著回傳第一個符合的元素 (或元素 index)。</li></ul><p>透過 callback 函式，也方便我們自訂各種條件找到符合的元素。</p><p>另外，也與名稱、作用雷同的其他陣列語法一齊比較。透過比較掌握彼此間的相似及相異處：</p><ul><li>filter() 同樣需要傳入 callback 函式作為判斷條件，但回傳值是一個新的陣列，陣列內會有所有符合條件的元素。</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/508/1*KB9MIHdVigvHKQsPc1n1XQ.png" /></figure><ul><li>indexOf() 和 lastIndexOf() 則是需要傳入一個值，確認陣列中是否有嚴格相等的值，接著回傳相等值的所在位置 (index)。</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/670/1*uSUt_yrMKrXvd5_lIdY9Tw.png" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=135bca441567" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[認識 Gulp.js — 拆分 Gulp 任務]]></title>
            <link>https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E6%8B%86%E5%88%86-gulp-%E4%BB%BB%E5%8B%99-2911d31048a0?source=rss-e582e959bd07------2</link>
            <guid isPermaLink="false">https://medium.com/p/2911d31048a0</guid>
            <category><![CDATA[前端]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[gulp]]></category>
            <category><![CDATA[nodejs]]></category>
            <category><![CDATA[學習]]></category>
            <dc:creator><![CDATA[Zong-Rong Huang]]></dc:creator>
            <pubDate>Fri, 20 Jan 2023 07:30:11 GMT</pubDate>
            <atom:updated>2023-01-31T17:44:54.674Z</atom:updated>
            <content:encoded><![CDATA[<h3>認識 Gulp.js — 拆分 Gulp 任務</h3><h4>拆分 Gulp 任務到不同檔案，管理更方便！</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/512/1*fO3Npx1KaLxTYj-MyS_8Jg.png" /><figcaption><a href="https://github.com/gulpjs/artwork">https://github.com/gulpjs/artwork</a></figcaption></figure><h3>概述</h3><p>當專案架構變複雜，gulpfile.js 裡的公開及私有任務數量也更加龐大，在維護和執行指令時更加不便。</p><p>可以將 gulpfile.js 依照功能、專案或其他分類拆分成多個檔案，達到關注點分離 (separation of concerns) 的目的。日後維護和執行會更加輕鬆。</p><p>本文會介紹如何拆分 gulpfile.js 檔案及如何執行任務：</p><ul><li>依功能拆分為多個 gulpfile.js 檔案</li><li>依專案拆分 gulpfile.js 檔案</li></ul><h3>依功能拆分為多個 gulpfile.js 檔案</h3><p>當 Gulp 任務數量眾多，可以將任務拆分成多個模組檔案進行管理，例如將 JavaScript、CSS 和 HTML 的處理任務放在不同模組檔案中。</p><p>如此一來，不僅方便新增、修改和搜尋任務。要使用任務時，再將模組檔案的任務集中匯入到相同的 index.js 檔案即可。</p><p>接下來，會為範例專案中的 JavaScript、CSS 和 HTML 檔案各自建立壓縮內容的任務。</p><p>範例架構如下：</p><pre>|-- 專案/<br>    |-- index.html<br>    |-- css/<br>        |-- index.css<br>        |-- mobile.css<br>    |-- js/<br>        |-- index.js</pre><ol><li>安裝要使用的套件及外掛程式：</li></ol><pre>npm install --save-dev gulp gulp-cssnano gulp-htmlmin gulp-uglify</pre><p>2. 在專案根目錄處建立名為 gulpfile.js 資料夾，在裡面新增 index.js 檔案及其他存放任務的模組檔案。新的專案結構如下：</p><pre>|-- 專案/<br>    |-- index.html<br>    |-- css/<br>        |-- index.css<br>        |-- mobile.css<br>    |-- js/<br>        |-- index.js<br>    |-- gulpfile.js/<br>        |-- index.js<br>        |-- cssTasks.js<br>        |-- jsTasks.js<br>        |-- htmlTasks.js</pre><p><strong>對於 Gulp 來說，gulpfile.js 資料夾內的 index.js 檔案等於平常使用的 gulpfile.js 檔案，都是 Gulp 存取任務函式的唯一進入點。</strong></p><p>3. 在個別模組檔案內建立壓縮任務。</p><pre>// cssTasks.js<br>const {src, dest} = require(&#39;gulp&#39;)<br>const cssnano = require(&#39;gulp-cssnano&#39;)<br><br>exports.minifyCSS = function(){<br>    return src(&#39;css/*.css&#39;)<br>        .pipe(cssnano())<br>        .pipe(dest(&#39;dist/css&#39;))<br>}</pre><pre>// jsTasks.js<br>const {src, dest} = require(&#39;gulp&#39;)<br>const uglify = require(&#39;gulp-uglify&#39;)<br><br>exports.uglifyJS = function(){<br>    return src(&#39;js/*.js&#39;)<br>        .pipe(uglify())<br>        .pipe(dest(&#39;dist/js&#39;))<br>}</pre><pre>// htmlTasks.js<br>const {src, dest} = require(&#39;gulp&#39;)<br>const htmlmin = require(&#39;gulp-htmlmin&#39;)<br><br>exports.minifyHTML = function(){<br>  return src(&quot;*.html&quot;)<br>    .pipe(htmlmin({ collapseWhitespace: true }))<br>    .pipe(dest(&quot;dist/&quot;));<br>}</pre><p>要注意的是，<strong>在模組檔案內指定輸入和輸出的檔案路徑時，必須以 gulpfile.js 資料夾為基準點，而不是以模組檔案為基準點。</strong></p><p>換句話說，要將 gulpfile.js 資料夾當成是一個單純的檔案，如同未拆分時的 gulpfile.js 檔案。所有的路徑皆從這個資料夾為基準點。</p><p>4. 將模組檔案的任務集中匯入至 index.js。在 index.js 中可以直接使用匯入的任務，或是重新進行組裝建立新的任務。</p><pre>// index.js<br><br>const { src, dest, parallel, watch } = require(&quot;gulp&quot;);<br>const { minifyCSS } = require(&quot;./cssTasks&quot;);<br>const { minifyHTML } = require(&quot;./htmlTasks&quot;);<br>const { uglifyJS } = require(&quot;./jsTasks&quot;);<br><br>exports.default = function () {<br>  watch(<br>    [&quot;*.html&quot;, &quot;js/*.js&quot;, &quot;css/*.css&quot;],<br>    { ignoreInitial: false },<br>    parallel(minifyCSS, minifyHTML, uglifyJS)<br>  );<br>};</pre><p>此處利用匯入的任務建立新的監聽任務，再匯出任務以供執行。</p><p>5. 在終端機輸入 Gulp 指令就能執行任務：</p><pre>gulp</pre><h3>依專案拆分 gulpfile.js 檔案</h3><p>另一種可能的情況是大專案資料夾內有多個小專案資料夾。小專案各自有自己的 Gulp 任務可以使用。所有小專案的Gulp 任務皆集中在大專案根目錄的 gulpfile.js 檔案內。</p><pre>|-- 大專案/<br>    |-- package.json<br>    |-- gulpfile.js<br>    |-- 小專案1/<br>        |-- index.html<br>        |-- styles.css<br>        |-- index.js<br>        |-- ...<br>    |-- 小專案2/<br>    |-- 小專案3/</pre><p>要為某個小專案新增、修改任務時，很可能要連帶調整其他小專案的 Gulp 任務，處理起來可能不方便。當小專案數量變多時，更加不輕鬆且容易出錯。</p><p>這時候可以將大專案的 gulpfile.js 拆分至各個小專案內。讓小專案有獨立的 gulpfile.js 檔案。</p><p>執行小專案時，只需要用到小專案的 gulpfile.js 檔案，不會和其他小專案共用任務。</p><pre>|-- 大專案/<br>    |-- package.json<br>    |-- 小專案1/<br>        |-- gulpfile.js<br>        |-- index.html<br>        |-- styles.css<br>        |-- index.js<br>        |-- ...<br>    |-- 小專案2/<br>        |-- gulpfile.js<br>        |-- ...<br>    |-- 小專案3/<br>        |-- gulpfile.js<br>        |-- ...</pre><p>在小專案 gulpfile.js 檔案內撰寫任務的方式，完全與為單一專案撰寫 Gulp 任務的方式相同。</p><p>差別在於執行 Gulp 任務的方式。如果要執行某個小專案的 Gulp 任務，有兩種方式：</p><ul><li>在小專案根目錄執行 Gulp 指令</li><li>在大專案根目錄執行 Gulp 指令</li></ul><p>在小專案根目錄執行 Gulp 任務頗符合直覺。然而，如果同時有多個小專案要執行 Gulp 指令，就得反覆切換資料夾，不是那麼方便。</p><p>在大專案根目錄執行 Gulp 指令會是更方便的方式！</p><p>Gulp CLI 內建 --gulpfile [檔案路徑] 指令，能夠選擇特定路徑的 gulpfile.js 執行：</p><p>查看小專案 1 的 Gulp 任務清單：</p><pre>gulp --tasks --gulpfile ./小專案1/gulpfile.js</pre><p>執行小專案 1 的預設任務：</p><pre># OK<br>gulp --gulpfile ./小專案1/gulpfile.js</pre><p>或</p><pre># OK<br>gulp default --gulpfile ./小專案1/gulpfile.js</pre><p>執行小專案 1 的 minifyCSS 任務：</p><pre>gulp minifyCSS --gulpfile ./小專案1/gulpfile.js</pre><p>此時 Gulp 指令已經變長不少。建議在大專案根目錄的 package.json 中建立對應的 npm 指令，不僅可以集中管理，執行時更方便。</p><p>例如為小專案 1 及所有專案建立 build 任務：</p><pre>// package.json<br><br>&quot;scripts&quot;: {<br>    &quot;build_p1&quot;: &quot;gulp build --gulpfile ./小專案1/gulpfile.js&quot;,<br>    &quot;build_all&quot;: &quot;gulp build --gulpfile ./小專案1/gulpfile.js &amp; gulp build --gulpfile ./小專案2/gulpfile.js &amp; gulp build --gulpfile ./小專案3/gulpfile.js&quot;<br>}</pre><p>只需要在大專案根目錄執行 npm run build_p1 和 npm run build_all 就好，非常精簡！</p><h3>結論</h3><p>當 gulpfile.js 檔案的任務數量變多，維護難度增加時，可以依據不同功能或專案將任務拆分至不同檔案。</p><p>有兩種建議作法：</p><ul><li>在 gulpfile.js 資料夾中，將任務拆分為不同的模組檔案，再將模組檔案匯入到相同的 index.js 檔案。</li><li>將任務拆分至多個 gulpfile.js 檔案，執行任務時指令加上 --gulpfile 和對應的檔案路徑。</li></ul><p>這樣一來管理 Gulp 任務時會更加方便，也減少出錯的可能。</p><h3>參考資料</h3><ul><li><a href="https://stackoverflow.com/questions/68981941/trouble-with-splitting-gulpfile-js-into-multiple-files-in-gulp-4">Trouble with splitting gulpfile.js into multiple files in Gulp 4</a></li><li><a href="https://gulpjs.com/docs/en/getting-started/javascript-and-gulpfiles">JavaScript and Gulpfiles | gulp.js</a></li><li><a href="https://namenotrepeated.medium.com/gulpv4-splitting-up-gulpfile-aded4d08f57b">Gulpv4 Splitting Up Gulpfile</a></li><li><a href="https://www.npmjs.com/package/gulp-cli">gulp-cli</a></li></ul><h3>認識 Gulp.js 系列</h3><p>想要再多瀏覽 Gulp.js 4 相關教學，別錯過下列文章！</p><ol><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E4%BD%A0%E7%9A%84%E5%89%8D%E7%AB%AF%E9%96%8B%E7%99%BC%E8%90%AC%E7%94%A8%E5%88%80-f4b440bfa8bf">認識 Gulp.js — 你的前端開發萬用刀</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E4%BD%A0%E7%9A%84%E5%89%8D%E7%AB%AF%E9%96%8B%E7%99%BC%E8%90%AC%E7%94%A8%E5%88%80-7d36ff9ef5df">認識 Gulp.js — 建立 Gulp 任務</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-gulp-%E4%BB%BB%E5%8B%99%E7%9A%84%E9%9D%9E%E5%90%8C%E6%AD%A5%E7%89%B9%E6%80%A7-c38bc5374173">認識 Gulp.js — Gulp 任務的非同步特性</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E5%8C%AF%E5%87%BA%E5%8F%8A%E5%9F%B7%E8%A1%8C-gulp-%E4%BB%BB%E5%8B%99-f4d6d1b764f9">認識 Gulp.js — 匯出及執行 Gulp 任務</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E5%BB%BA%E7%AB%8B%E7%9B%A3%E8%81%BD%E4%BB%BB%E5%8B%99-b0a9a203a280">認識 Gulp.js — 建立監聽任務</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E6%8B%86%E5%88%86-gulp-%E4%BB%BB%E5%8B%99-2911d31048a0">認識 Gulp.js — 拆分 Gulp 任務</a></li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2911d31048a0" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[認識 Gulp.js — 建立監聽任務]]></title>
            <link>https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E5%BB%BA%E7%AB%8B%E7%9B%A3%E8%81%BD%E4%BB%BB%E5%8B%99-b0a9a203a280?source=rss-e582e959bd07------2</link>
            <guid isPermaLink="false">https://medium.com/p/b0a9a203a280</guid>
            <category><![CDATA[教學]]></category>
            <category><![CDATA[前端]]></category>
            <category><![CDATA[gulp]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[nodejs]]></category>
            <dc:creator><![CDATA[Zong-Rong Huang]]></dc:creator>
            <pubDate>Thu, 19 Jan 2023 15:57:55 GMT</pubDate>
            <atom:updated>2023-02-07T13:03:38.993Z</atom:updated>
            <content:encoded><![CDATA[<h3>認識 Gulp.js — 建立監聽任務</h3><h4>監聽檔案變動，自動執行 Gulp 流程</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/512/1*fO3Npx1KaLxTYj-MyS_8Jg.png" /><figcaption><a href="https://github.com/gulpjs/artwork">https://github.com/gulpjs/artwork</a></figcaption></figure><h3>概述</h3><p>若要執行 Gulp 任務，通常要在終端機上手動輸入指令觸發。然而，開發時可能遇上需要反覆執行任務的情境，若是只能透過手動觸發，效率極低。</p><p>為了解決上述問題，Gulp 提供非常方便的監聽功能：Gulp 會監聽指定檔案，當檔案內容發生變動時，就會自動執行對應任務。</p><p>本文內容如下：</p><p>．認識監聽任務</p><p>．更多任務設定</p><p>．建立更複雜的監聽任務</p><p>．匯出和執行監聽任務</p><p>．用 chokidar 實例建立監聽任務 (不推薦)</p><p>．動手練習</p><h3>認識監聽任務</h3><p>Gulp 內建 watch 方法，用來建立監聽任務。當 Gulp 監聽到檔案發生指定的事件，就會立即執行對應任務。</p><p>watch 方法能夠監聽下列事件：</p><ul><li>add：新增檔案</li><li>change：修改檔案</li><li>unlink：移除檔案</li><li>addDir：新增資料夾</li><li>unlinkDir：移除資料夾</li><li>error：發生錯誤</li><li>ready：完成檔案／資料夾初次掃瞄，能夠開始監聽 change 事件</li><li>raw：所有事件 (能夠取得所有事件的名稱、路徑及細節)</li><li>all：上述所有事件 (不含 ready、raw 和 error 事件)</li></ul><p>各個事件可提供不同的詳細資料，請參考：<a href="https://www.npmjs.com/package/chokidar">https://www.npmjs.com/package/chokidar</a></p><p><strong>撰寫監聽任務時，要提供檔案路徑、要監聽的事件類型和任務函式。</strong>如果自行指定監聽事件，透過 {events: ...} 就能帶入。</p><p>任務函式內要傳入 callback 函式，負責通知 Gulp 這個監聽任務已完成。callback 函式是由 Gulp 自行產生，開發者不需另行宣告。</p><pre>// gulpfile.js<br><br>const {src, dest, watch} = require(&#39;gulp&#39;)<br><br>function watchStyles () {<br>   watch(&#39;./src/css/**&#39;, {events: &#39;change&#39;}, function(cb) {<br>       src(...)<br>       .pipe(...)<br>       .pipe(dest(...))<br>             <br>       cb()<br>   }) <br>}</pre><p>若要監聽多個事件，透過 {events: []} 以陣列帶入所有要監聽的事件：</p><pre>// gulpfile.js<br><br>const {src, dest, watch} = require(&#39;gulp&#39;)<br><br>function watchStyles () {<br>   watch(&#39;./src/css/**&#39;, {events: [&#39;change&#39;, &#39;add&#39;]}, function(cb) {<br>       src(...)<br>       .pipe(...)<br>       .pipe(dest(...))<br>             <br>       cb()<br>   }) <br>}</pre><p>如果任務未指定要監聽的事件，預設會監聽 add、change 和 unlink 事件。</p><pre>// gulpfile.js<br><br>const {src, dest, watch} = require(&#39;gulp&#39;)<br><br>// 監聽 add、change 和 unlink 事件<br>function watchStyles () {<br>   watch(&#39;./src/css/**&#39;, function(cb) {<br>       src(...)<br>       .pipe(...)<br>       .pipe(dest(...))<br>             <br>       cb()<br>   }) <br>}</pre><h3>更多任務設定</h3><p>監聽任務函式中的第二個 argument {...} 不僅能夠帶入要監聽的事件，還能帶入更多設定，讓監聽任務更加符合需求。</p><p>以下介紹其中三種設定：ignoreInitial、queue 和 delay。</p><ul><li>ignoreInitial</li></ul><p>執行監聽任務時，預設會在檔案出現變動後才會觸發任務執行。</p><p>如果希望在已經開始監聽檔案、但檔案尚未出現變動的時候就執行任務，可以傳入 {ignoreInitial: false} 設定。當監聽任務一開始執行、還未修改檔案時，就會對已有的 CSS 檔案作處理。</p><pre>// gulpfile.js<br><br>const {src, dest, watch} = require(&#39;gulp&#39;)<br><br>function watchStyles () {<br>   watch(&#39;./src/css/*.css&#39;, {ignoreInitial: false}, function(cb) {<br>       src(...)<br>       .pipe(...)<br>       .pipe(dest(...))<br>             <br>       cb()<br>   }) <br>}</pre><ul><li>queue</li></ul><p>由於監聽的事件可能在短時間內觸發多次 (例如快速修改檔案並存檔)，Gulp 會讓每一次觸發的任務依時間順序執行。</p><p>如果有特殊考量，希望能讓多個觸發的任務同時執行，而非依序執行，可以傳入 {queue: false} 關閉預設行為。</p><pre>// gulpfile.js<br><br>const {src, dest, watch} = require(&#39;gulp&#39;)<br><br>function watchStyles () {<br>   watch(&#39;./src/css/**&#39;, {queue: false}, function(cb) {<br>       src(...)<br>       .pipe(...)<br>       .pipe(dest(...))<br>             <br>       cb()<br>   }) <br>}</pre><ul><li>delay</li></ul><p>監聽函式預設會在檔案修改後 200 毫秒才執行任務，避免在一次性大量修改檔案時就執行任務，導致錯誤發生。</p><p>在特定情況下，可能需要更長的延遲時間才執行監聽任務。可以傳入 {delay: ...} 來調整延遲時間。時間單位為毫秒。</p><pre>// gulpfile.js<br><br>const {src, dest, watch} = require(&#39;gulp&#39;)<br><br>// 等待 500 毫秒後再執行<br>function watchStyles () {<br>   watch(&#39;./src/css/**&#39;, {delay: 500}, function(cb) {<br>       src(...)<br>       .pipe(...)<br>       .pipe(dest(...))<br>             <br>       cb()<br>   }) <br>}</pre><p>如果想要了解更多監聽任務設定，參考： <a href="https://gulpjs.com/docs/en/api/watch">https://gulpjs.com/docs/en/api/watch</a></p><h3>建立更複雜的監聽任務</h3><p>先前所介紹的監聽任務都相當單純，只監聽單一檔案路徑內是否有變更；如果有變更的話，則執行一項指定任務。</p><p>然而，實際開發時所需的監聽任務可能更為複雜：必須監聽多個檔案路徑或觸發多個指定任務。</p><p>接下來會介紹這些可能的複雜情境以及如何建立合適的監聽任務。</p><h4>監聽多個檔案路徑 + 執行單一任務</h4><p>若要同時監聽多個檔案，在陣列內列出要監聽的所有檔案即可。</p><pre>// gulpfile.js<br>const {watch} = require(&#39;gulp&#39;)<br><br>function informMe(cb){<br>    console.log(&#39;Changes detected!&#39;)<br>    cb()<br>}<br><br>// 用陣列列出所有要監聽的檔案<br>function watcher () {<br>  watch([&#39;./src/js/**&#39;, &#39;./src/css/**&#39;], informMe)    <br>}</pre><p>在上面的例子中，只要 Gulp 監聽到這些資料夾內的檔案有任何變更，就會執行任務印出訊息。</p><h4>監聽單一檔案路徑 + 執行多個任務</h4><p>當 Gulp 監聽到單一檔案路徑內有變更，即可觸發多個任務。只需用 series 或 parallel 包住要觸發的任務。不需為個別任務撰寫對應的監聽任務，寫起來簡潔不少。</p><pre>// gulpfile.js<br><br>const {src, dest, watch, series} = require(&#39;gulp&#39;)<br><br>function convertToJS () {...}<br>function uglifyJS () {...}<br>function concatJS () {...}<br><br>function watcher () {<br>  watch(&#39;./src/ts/**&#39;, series(convertToJS, uglifyJS, concatJS))    <br>}</pre><p>在上面的例子中，當 TypeScript 檔案內發生變更，Gulp 會依序將所有的 .ts 檔案轉成 .js 檔案、進行壓縮，最後再將所有產出的 .js 檔案合併為單一 .js 檔案。</p><h4>監聽多個檔案路徑 + 執行不同任務</h4><p>如果必須同時監聽多個不同的檔案路徑，而且各個檔案路徑要執行的任務不相同時，還是得要為各個檔案路徑撰寫對應的監聽任務。</p><p>只是，當專案內有多個監聽任務分散四處時，在管理和執行時，可能有所遺漏，增加處理負擔。</p><p>很棒的是，可以只用一個任務函式整合多個監聽任務。如此一來，只須匯出、執行外層函式，就能同時執行多個監聽任務，維護監聽函式也更加輕鬆：</p><pre>// gulpfile.js<br>const {watch} = require(&#39;gulp&#39;)<br><br>function watchAll () {<br>  watch(&#39;./src/js/**&#39;, function() {...})  <br>    <br>  watch(&#39;./src/scss/**&#39;, function() {...})<br>  <br>  watch(&#39;./src/index.html&#39;, function() {...})<br>}</pre><h3>匯出和執行監聽任務</h3><p>監聽任務和一般的 Gulp 任務一樣，都必須匯出為公開任務才能執行。可以匯出為預設任務或一般任務：</p><pre>// gulpfile.js<br><br>const {src, dest, watch} = require(&#39;gulp&#39;)<br><br>function watchStyles () {<br>   watch(&#39;./src/css/**&#39;, function(cb) {...}) <br>}<br><br>function watchHTML () {<br>    watch(&#39;./src/index.html&#39;, function(cb) {...})<br>}<br><br>// 匯出為預設任務<br>exports.default = watchStyles<br>          <br>// 匯出為一般任務<br>exports.html = watchHTML</pre><p>匯出監聽任務後，在終端機輸入 Gulp 指令即開始執行監聽任務。例如，輸入 gulp 或 gulp default 會執行 watchStyles 任務，輸入 gulp html 則會執行 watchHTML 任務。</p><h3>用 chokidar 實例建立監聽任務 (不推薦)</h3><p>以上介紹的是用 Gulp 內建的 watch 建立監聽任務的方式，也是最安全的方式。</p><p>然而，你也有機會看到類似 JavaScript 事件監聽器的其他寫法。這種寫法是利用 Gulp 內部的 chokidar 套件，直接建立 chokidar 實例來執行監聽任務：</p><pre>// gulpfile.js<br><br>const {watch} = require(&#39;gulp&#39;)<br><br>// 傳入要監聽的檔案路徑，建立 chokidar 實例<br>const watcher = watch([&#39;styles/sass/*.scss&#39;])<br><br>// 註冊監聽器<br>// path = 受到修改的檔案的路徑<br>// stats = 檔案狀態 (即檔案的 metadata)<br>watcher.on(&#39;change&#39;, function(path, stats) {<br>    console.log({path, stats})<br>})<br><br>watcher.on(&#39;add&#39;, function (path, stats) {<br>    // ...<br>})</pre><p>依據官方文件說明，建立 chokidar 實例作為監聽器會失去 Gulp 的系統優勢：</p><blockquote><em>When using the chokidar instance directly, you will not have access to the task system integrations, including async completion, queueing, and delay.</em></blockquote><p>容易導致錯誤發生，使得處理結果不如預期或造成效能負擔。因此已不建議使用此種寫法。</p><h3>動手練習</h3><p>Gulp 監聽任務最經典的使用情境是將 SCSS 即時編譯為 CSS。SCSS 語法比 CSS 更加彈性，是不少人的開發首選。但是瀏覽器無法直接讀取 SCSS，必須先執行指令將 SCSS 編譯成 CSS，瀏覽器才能讀取到樣式。</p><p>Gulp 能夠監聽 SCSS 檔案變化，就能立即產生 CSS 檔案，不需反覆手動下指令編譯，非常方便！</p><p>不過，瀏覽器並不會因為 CSS 檔案有變化而自動重新載入。在這個練習中會搭配 Browsersync 套件，依賴它來建立開發伺服器。當 HTML 或 SCSS 檔案變動時，Gulp 會通知 Browsersync 重新整理頁面，重新渲染畫面。</p><p>練習的專案資料夾結構如下：</p><pre>|-- 專案資料夾/<br>    |-- styles/<br>        |-- sass/<br>            |-- index.scss<br>    |-- index.html</pre><ol><li>依上述結構建立你的練習用專案。</li><li>在終端機輸入下列指令進行專案初始化：</li></ol><pre>npm init -y</pre><p>3. 全域安裝 gulp-cli</p><pre>npm install --global gulp-cli</pre><p>4. 安裝 ‘gulp’、‘sass’、‘gulp-sass’ 和 ‘browser-sync’ 套件到專案資料夾中：</p><pre>npm install gulp sass gulp-sass browser-sync --save-dev</pre><p>5. 在根目錄處建立 gulpfile.js 檔案。</p><p>6. 開啟 gulpfile.js 來建立任務。首先引入需要的外掛程式和套件：</p><pre>// gulpfile.js<br><br>// 取出 Gulp 提供的方法<br>const { src, dest, parallel, watch } = require(&quot;gulp&quot;);<br><br>// 建立 browserSync 實例 (開發伺服器)<br>const browserSync = require(&quot;browser-sync&quot;).create();<br><br>// 引入 SASS<br>const sass = require(&quot;gulp-sass&quot;)(require(&quot;sass&quot;));</pre><p>7. 建立將 SCSS 轉換為 CSS 的 Gulp 任務：</p><pre>// gulpfile.js<br><br>function convertSCSSToCSS() {<br>  return src(&quot;styles/scss/*.scss&quot;)<br>    .pipe(sass())<br>    .pipe(dest(&quot;app/styles/css&quot;))<br>    .pipe(browserSync.stream());<br>}</pre><p>這個任務將會 styles/scss 裡所有的 SCSS 檔案轉換為 CSS 檔案，並將產生的檔案放置到 app/styles/css 資料夾中。app 資料夾內也會存放所有給開發伺服器使用的資源。</p><p>最後透過 browserSync.stream() 方法回傳 stream，通知瀏覽器 CSS 檔案有變化。</p><p>8. 建立將 HTML 檔案複製到 app 資料夾的 Gulp 任務：</p><pre>// gulpfile.js<br><br>function copyHTML(){<br>    return src(&#39;*.html&#39;)<br>    .pipe(dest(&#39;app&#39;))<br>    .pipe(browserSync.stream())<br>}</pre><p>一樣在最後加上 browserSync.stream() 方法回傳 stream，讓瀏覽器得知 HTML 檔案有變化。</p><p>9. 在 index.html 檔案裡要指定 CSS 檔案來源，瀏覽器才能讀取到樣式：</p><pre>&lt;!-- index.html --&gt;<br><br>&lt;head&gt;<br>    ...<br>    &lt;link rel=&quot;stylesheet&quot; href=&quot;styles/css/index.css&quot; /&gt;<br>&lt;/head&gt;</pre><p>10. 建立要匯出執行的任務：</p><pre>// gulpfile.js<br><br>function serve() {<br>  browserSync.init({<br>    server: &quot;./app&quot;,<br>  });<br><br>  watch(<br>    [&quot;styles/scss/*.scss&quot;, &quot;*.html&quot;],<br>    { ignoreInitial: false },<br>    parallel(convertSCSSToCSS, copyHTML)<br>  );<br><br>  watch([&quot;app/**&quot;], browserSync.reload);<br>}</pre><p>在這個任務中，先對 Browsersync 實例 (也就是開發用的伺服器) 進行初始化。開發伺服器的進入點設定為 ./app 資料夾。</p><p>裡面含有兩個監聽任務。第一個任務負責監聽 SCSS 檔案和 HTML 檔案是否有任何變動。如果有變動的話，依序將 SCSS 檔案轉成 CSS 檔案，再複製 HTML 檔案。產生的檔案都會放到 app 資料夾內，讓瀏覽器讀取。</p><p>監聽任務中的 {ignoreInitial: false} 設定。即使還沒修改 SCSS 檔案和 HTML 檔案，Gulp 會先產生對應的檔案到 app 資料夾。這樣一來，開啟開發網址時，就有檔案讓瀏覽器讀取。</p><p>第二個任務監聽 app 資料夾內是否有任何變動 (例如新增檔案或檔案內容有變化)。一旦發現變動，瀏覽器就會重新整理頁面，依據最新的CSS 和 HTML 內容渲染畫面。</p><p>11. 將 serve 任務匯出為公開任務，之後才能使用 Gulp 指令執行。</p><pre>// gulpfile.js<br><br>function serve () {...}<br><br>exports.default = serve</pre><p>12. 在終端機輸入 Gulp 指令來執行任務：</p><pre>gulp</pre><p>瀏覽器會自動開啟頁面 (或是點選終端機上的開發網址前往頁面)。接著就能進行開發！只要 HTML 和 SCSS 檔案有任何變化，監聽任務都會立刻執行，即時更新瀏覽器畫面。即使新增 SCSS 檔案或 HTML 檔案，Gulp 也會進行處理，讓開發伺服器和瀏覽器都能讀取到。</p><p>完整內容如下：</p><pre>// gulpfile.js<br><br>const { src, dest, parallel, watch } = require(&quot;gulp&quot;);<br>const browserSync = require(&quot;browser-sync&quot;).create();<br>const sass = require(&quot;gulp-sass&quot;)(require(&quot;sass&quot;));<br><br>function convertSCSSToCSS() {<br>  return src(&quot;styles/scss/*.scss&quot;)<br>    .pipe(sass())<br>    .pipe(dest(&quot;app/styles/css&quot;))<br>    .pipe(browserSync.stream());<br>}<br><br>function copyHTML() {<br>  return src(&quot;*.html&quot;).pipe(dest(&quot;app&quot;)).pipe(browserSync.stream());<br>}<br><br>function serve() {<br>  browserSync.init({<br>    server: &quot;./app&quot;,<br>  });<br><br>  watch(<br>    [&quot;styles/scss/*.scss&quot;, &quot;*.html&quot;],<br>    { ignoreInitial: false },<br>    parallel(convertSCSSToCSS, copyHTML)<br>  );<br><br>  watch([&quot;app/**&quot;], browserSync.reload);<br>}<br><br>exports.default = serve;</pre><h3>結論</h3><p>Gulp 的 watch 方法能用來建立監聽任務，自動進行處理。建立監聽任務時，除了指定監聽的檔案路徑及要執行的任務，還可以加上更多設定，讓監聽行為更加符合需求。</p><p>Gulp 監聽任務可以應付多個檔案路徑和多個執行任務，方便維護、執行監聽任務。</p><h3>參考資料</h3><ul><li><a href="https://gulpjs.com/docs/en/getting-started/watching-files">Watching Files | gulp.js</a></li><li><a href="https://gulpjs.com/docs/en/api/watch">watch() | gulp.js</a></li><li><a href="https://github.com/paulmillr/chokidar#events">GitHub - paulmillr/chokidar: Minimal and efficient cross-platform file watching library</a></li><li><a href="https://www.freecodecamp.org/news/gulp-4-walk-through-with-example-code-c3c018eab306/">Using Gulp 4 in your workflow for Sass and JS files</a></li><li><a href="https://ithelp.ithome.com.tw/articles/10228593">[試著把切版專案升級到 gulp4.0 吧] Day31 邊開發邊看畫面變動：談 gulp.watch - iT 邦幫忙::一起幫忙解決難題，拯救 IT 人的一天</a></li><li><a href="https://browsersync.io/docs/gulp">Browsersync + Gulp.js</a></li><li><a href="https://stackoverflow.com/questions/46105461/browsersync-reload-and-browsersync-stream-what-are-the-difference">browserSync.reload and browserSync.stream()) - what are the difference?</a></li></ul><h3>認識 Gulp.js 系列</h3><p>想要再多瀏覽 Gulp.js 4 相關教學，別錯過下列文章！</p><ol><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E4%BD%A0%E7%9A%84%E5%89%8D%E7%AB%AF%E9%96%8B%E7%99%BC%E8%90%AC%E7%94%A8%E5%88%80-f4b440bfa8bf">認識 Gulp.js — 你的前端開發萬用刀</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E4%BD%A0%E7%9A%84%E5%89%8D%E7%AB%AF%E9%96%8B%E7%99%BC%E8%90%AC%E7%94%A8%E5%88%80-7d36ff9ef5df">認識 Gulp.js — 建立 Gulp 任務</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-gulp-%E4%BB%BB%E5%8B%99%E7%9A%84%E9%9D%9E%E5%90%8C%E6%AD%A5%E7%89%B9%E6%80%A7-c38bc5374173">認識 Gulp.js — Gulp 任務的非同步特性</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E5%8C%AF%E5%87%BA%E5%8F%8A%E5%9F%B7%E8%A1%8C-gulp-%E4%BB%BB%E5%8B%99-f4d6d1b764f9">認識 Gulp.js — 匯出及執行 Gulp 任務</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E5%BB%BA%E7%AB%8B%E7%9B%A3%E8%81%BD%E4%BB%BB%E5%8B%99-b0a9a203a280">認識 Gulp.js — 建立監聽任務</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E6%8B%86%E5%88%86-gulp-%E4%BB%BB%E5%8B%99-2911d31048a0">認識 Gulp.js — 拆分 Gulp 任務</a></li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b0a9a203a280" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[認識 Gulp.js — 匯出及執行 Gulp 任務]]></title>
            <link>https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E5%8C%AF%E5%87%BA%E5%8F%8A%E5%9F%B7%E8%A1%8C-gulp-%E4%BB%BB%E5%8B%99-f4d6d1b764f9?source=rss-e582e959bd07------2</link>
            <guid isPermaLink="false">https://medium.com/p/f4d6d1b764f9</guid>
            <category><![CDATA[gulp]]></category>
            <category><![CDATA[前端]]></category>
            <category><![CDATA[nodejs]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[技術筆記]]></category>
            <dc:creator><![CDATA[Zong-Rong Huang]]></dc:creator>
            <pubDate>Tue, 17 Jan 2023 14:17:30 GMT</pubDate>
            <atom:updated>2023-02-07T13:06:51.916Z</atom:updated>
            <content:encoded><![CDATA[<h3>認識 Gulp.js — 匯出及執行 Gulp 任務</h3><p>一個指令，立即執行你的 Gulp 任務！</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/512/1*fO3Npx1KaLxTYj-MyS_8Jg.png" /><figcaption><a href="https://github.com/gulpjs/artwork">https://github.com/gulpjs/artwork</a></figcaption></figure><h3>概述</h3><p>本文介紹如何匯出 Gulp 任務函式以及如何利用 Gulp/npm 指令執行任務。</p><p>本文內容如下：</p><p>．認識公開任務 vs 私有任務</p><p>．匯出預設任務 vs 匯出一般任務</p><p>．匯出基本任務 vs 匯出複合任務</p><p>．執行 Gulp 任務</p><h3>認識公開任務 vs 私有任務</h3><p>Gulp 任務函式皆屬於<strong>私有任務</strong> (private task)，無法透過Gulp CLI 工具直接呼叫執行。</p><p>必須先將任務函式匯出，函式匯出後即為<strong>公開任務 </strong>(public task)，能夠進一步呼叫、執行。</p><p>未匯出的私有任務雖然無法直接呼叫執行，但是可以作為公開任務的子任務。換句話說，當公開任務裡面有私有任務，在執行公開任務時，自然就會執行內部的私有任務。</p><pre>// gulpfile.js<br><br>const {src, dest, series} = require(&#39;gulp&#39;)<br><br>// 未匯出的私有任務<br>// 無法直接呼叫執行<br>function task1 () {...}<br>function privateTask () {<br>    return src(...)<br>        .pipe(...)<br>        .pipe(dest(...))<br>}<br><br>// 已匯出的公開任務<br>// 執行公開任務時，裡面的私有任務也會執行<br>exports.public = series(task1, privateTask)</pre><h3>匯出預設任務 vs 匯出一般任務</h3><p>任務可以匯出為預設任務和一般任務，差別在於採用什麼關鍵字命名：</p><ul><li><strong>預設任務</strong>：匯出時使用 default 關鍵字作為任務名稱。在同一份 gulpfile.js 內，最多只能匯出一個預設任務。</li><li><strong>一般任務</strong>：匯出時用其他名稱命名的任務。在同一份 gulpfile.js 內，可以匯出多個一般任務。</li></ul><pre>// gulpfile.js<br><br>const {src, dest, series, parallel} = require(&#39;gulp&#39;)<br><br>// 未匯出，為 private task，在 public task 內部使用<br>function uglifyJS () {...}<br>function minifyCSS () {...}<br>function minifyHTML () {...}<br>function compressPNG () {...}<br><br>// 匯出為預設任務<br>exports.default = parallel(uglifyJS, minifyCSS, minifyHTML, compressPNG)<br>                                 <br>// 匯出為一般任務<br>exports.minify = parallel(uglifyJS, minifyCSS, minifyHTML, compressPNG)</pre><h3>匯出基本任務 vs 匯出複合任務</h3><p>在先前文章提到，src、pipe 和 desc 會回傳 stream，但 series 和 parallel 只是一般的函式，並不會回傳 stream。這樣的差異導致基本任務和複合任務的匯出方式略有不同。</p><p>基本任務可以包裝成命名函式匯出：</p><pre>// gulpfile.js<br><br>// 命名函式<br>function myTask () {<br>    return src(...)<br>    .pipe(...)<br>    .pipe(dest(...))<br>}<br>exports.default = myTask</pre><p>也可以用匿名函式匯出基本任務：</p><pre>// gulpfile.js<br><br>// 匿名函式<br>exports.default = function () {<br>  return src(...)<br>    .pipe(...)<br>    .pipe(dest(...))<br>}</pre><p>由 series 或 parallel 包住的複合任務，直接匯出即可，不用包在函式內再匯出：</p><pre>// gulpfile.js<br><br>exports.default = parallel(task1, task2, ..., taskN)</pre><pre>// gulpfile.js<br><br>exports.default = series(task1, task2, ..., taskN)</pre><p>若要以函式的型式匯出複合任務，寫法會稍微不同：函式內不需要加上 return 關鍵字，要另外加上 () 執行。</p><pre>// gulpfile.js<br><br>function comboTask () {<br>    series(task1, task2, ..., taskN)()<br>}<br><br>exports.default = comboTaskS</pre><h3>執行 Gulp 任務</h3><p>從 gulpfile.js 檔案中匯出的公開任務都能透過指令執行。</p><p>在 gulpfile.js 檔案中，可能會匯出多個任務。除了打開檔案確認有哪些公開任務之外，Gulp CLI 工具也提供指令，直接列出所有公開任務：</p><pre>gulp --tasks</pre><p>在同一份 gulpfile.js 裡的所有任務會以樹狀圖列出。在樹狀圖中，公開任務列在第一層，而內部使用的子任務/私有任務列在第二層。由此可以對任務結構有清楚掌握。</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XTPS5c_EpGdWGuIlRbOsmA.png" /><figcaption><a href="https://gulpjs.com/img/docs-gulp-tasks-command.png">https://gulpjs.com/img/docs-gulp-tasks-command.png</a></figcaption></figure><p>公開任務會以藍色顯示，私有任務則以白色顯示。在公開任務中，預設任務以 default 為名，其他公開任務則顯示對應的名稱。</p><p>若要執行預設任務，只須在終端機輸入 gulp 或 gulp default，就會執行該任務。</p><pre># 執行預設任務<br>gulp</pre><p>或</p><pre># 執行預設任務<br>gulp default</pre><p>若要執行特定名稱的任務，在終端機輸入 gulp [任務名稱]，就會執行對應的任務。例如要執行名稱為 minify 的任務：</p><pre># 執行一般任務<br>gulp minify</pre><p>經常執行的 Gulp 指令可以寫在 npm package.json 的 scripts 內，作為 npm 指令使用。當 Gulp 指令變複雜時，只需輸入簡短的 npm 指令就能順利執行、避免出錯。</p><p>例如在 package.json 裡建立了兩個 npm 指令：</p><pre>// package.json<br><br>&quot;scripts&quot;: {<br>    &quot;build&quot;: &quot;gulp&quot;,<br>    &quot;minify&quot;: &quot;gulp minify&quot;<br>}</pre><p>在終端機輸入 npm run build 和 npm run minify 就會執行對應的 Gulp 任務。</p><h3>結論</h3><p>gulpfile.js 內的任務函式皆是私有任務；私有任務必須匯出成為公開任務後，才能透過 Gulp/npm 指令執行。</p><h3>參考資料</h3><p><a href="https://gulpjs.com/docs/en/getting-started/creating-tasks/">Creating Tasks | gulp.js</a></p><h3>認識 Gulp.js 系列</h3><p>想要再多瀏覽 Gulp.js 4 相關教學，別錯過下列文章！</p><ol><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E4%BD%A0%E7%9A%84%E5%89%8D%E7%AB%AF%E9%96%8B%E7%99%BC%E8%90%AC%E7%94%A8%E5%88%80-f4b440bfa8bf">認識 Gulp.js — 你的前端開發萬用刀</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E4%BD%A0%E7%9A%84%E5%89%8D%E7%AB%AF%E9%96%8B%E7%99%BC%E8%90%AC%E7%94%A8%E5%88%80-7d36ff9ef5df">認識 Gulp.js — 建立 Gulp 任務</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-gulp-%E4%BB%BB%E5%8B%99%E7%9A%84%E9%9D%9E%E5%90%8C%E6%AD%A5%E7%89%B9%E6%80%A7-c38bc5374173">認識 Gulp.js — Gulp 任務的非同步特性</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E5%8C%AF%E5%87%BA%E5%8F%8A%E5%9F%B7%E8%A1%8C-gulp-%E4%BB%BB%E5%8B%99-f4d6d1b764f9">認識 Gulp.js — 匯出及執行 Gulp 任務</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E5%BB%BA%E7%AB%8B%E7%9B%A3%E8%81%BD%E4%BB%BB%E5%8B%99-b0a9a203a280">認識 Gulp.js — 建立監聽任務</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E6%8B%86%E5%88%86-gulp-%E4%BB%BB%E5%8B%99-2911d31048a0">認識 Gulp.js — 拆分 Gulp 任務</a></li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f4d6d1b764f9" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[認識 Gulp.js — Gulp 任務的非同步特性]]></title>
            <link>https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-gulp-%E4%BB%BB%E5%8B%99%E7%9A%84%E9%9D%9E%E5%90%8C%E6%AD%A5%E7%89%B9%E6%80%A7-c38bc5374173?source=rss-e582e959bd07------2</link>
            <guid isPermaLink="false">https://medium.com/p/c38bc5374173</guid>
            <category><![CDATA[async]]></category>
            <category><![CDATA[gulp]]></category>
            <category><![CDATA[筆記]]></category>
            <category><![CDATA[非同步]]></category>
            <category><![CDATA[前端]]></category>
            <dc:creator><![CDATA[Zong-Rong Huang]]></dc:creator>
            <pubDate>Sun, 25 Dec 2022 06:55:16 GMT</pubDate>
            <atom:updated>2023-02-07T13:08:44.138Z</atom:updated>
            <content:encoded><![CDATA[<h4>如何標示非同步任務已完成 (async completion)</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/512/1*fO3Npx1KaLxTYj-MyS_8Jg.png" /><figcaption><a href="https://github.com/gulpjs/artwork">https://github.com/gulpjs/artwork</a></figcaption></figure><h3>概述</h3><p>本文會介紹為何需要在 Gulp 任務中加入非同步完成的標示以及加入標示的方法。</p><p>同時透過實際經驗推敲出，基本任務和複合任務在加入非同步完成標示時有些許不同。</p><p>本文內容如下：</p><p>．什麼是非同步完成 (async completion)</p><p>．如何標示非同步任務已完成</p><p>．To return a stream or not to?</p><h3>什麼是非同步完成 (async completion)？</h3><p><strong>在 Gulp 4 中，所有的 Gulp 任務皆是非同步任務。</strong>由於非同步特性，我們無法確切掌握某個任務是否已經執行完畢、是否能夠接著執行下一個相依任務。這樣一來，一連串任務執行下來，過程中可能發生錯誤或結果不如預期。</p><p>因此，Gulp 4 另外提供多種方式用來標示非同步的任務已完成 (async completion)：當 Gulp 接收到標示後，就會安心進行下一個非同步任務。</p><p>如果忘了在任務中加上非同步完成的標示，Gulp 一樣會執行任務，只是執行結果變得無法掌握。</p><p>Gulp 也會在 terminal 中拋出警告訊息 “Did you forget to signal async completion?”，提醒開發者應加上非同步完成的標示。</p><h3>如何標示非同步任務已完成</h3><p>根據官方文件，有 7 種方式可以標示任務已完成：</p><ul><li>回傳 Node.js stream、event emitter 或 child process</li><li>回傳 Rx.js 的 observable</li><li>回傳 Promise</li><li>傳入錯誤優先處理函式 (error-first callback)</li></ul><p>以下介紹常見的方式：回傳 Node.js stream (推薦)、回傳 Promise 和傳入錯誤優先處理函式。</p><h4><strong>回傳 stream</strong></h4><p>在開頭加上 return 即可。</p><pre>// gulpfile.js<br><br>const { dest, src } = require(&quot;gulp&quot;);<br><br>function task () {<br>    return src(...)<br>        .pipe(...)<br>        .pipe(dest(...))<br>}</pre><p>從 src 開始到 dest 結束，檔案資料都以 stream 格式傳遞和接受處理。因此，只要加上 return 就能回傳 stream。</p><p>Gulp 用到的外掛程式通常是對 stream 做處理，處理完後也會回傳 stream。因此，在任務函式中回傳 stream 來標示非同步完成，基本上都不會有問題。</p><h4><strong>回傳 Promise</strong></h4><pre>// gulpfile.js<br><br>const { dest, src } = require(&quot;gulp&quot;);<br><br>function task () {<br>    return Promise.resolve(<br>      src(...)<br>      .pipe(...)<br>      .pipe(desc(...))<br>    )<br>}</pre><h4><strong>使用錯誤優先處理函式 (error-first callback)</strong></h4><p>如果不回傳上述類型的資料，必須提供錯誤優先處理函式作為任務函式的 argument。</p><p>錯誤優先處理函式通常以 cb 代表 (即 callback 縮寫)。cb() 內可不傳入任何值，也可以放入 console.log 印出提示。</p><pre>// gulpfile.js<br><br>function task (cb) {<br>    src(...)<br>     .pipe(...)<br>     .pipe(dest(...))<br>         <br>    // 通知 Gulp 非同步任務已完成     <br>    cb() <br>    <br>    // 通知 Gulp 非同步任務已完成 + 印出 console 提示<br>    cb(console.log(&quot;Task done&quot;))<br>}</pre><p>不過，開發者不需要自行宣告錯誤優先函式。根據<a href="https://stackoverflow.com/questions/29298244/gulp-where-is-the-gulp-task-callback-function-defined#:~:text=The%20callback%20function%20comes%20from,your%20task%20is%20%22done%22">這一篇 StackOverflow</a>，這個 callback 是由 Gulp 4 引用的 undertaker 套件提供：</p><blockquote><em>The callback function comes from Orchestrator (or the new one — undertaker — in Gulp 4) and is actually nothing more than a call to tell the task system that your task is “done”.</em></blockquote><p>什麼時候適合用到錯誤優先處理函式呢？根據<a href="http://根據這一篇 StackOverflow">同一篇 StackOverflow</a> 的回答，如果該任務用到的外掛程式不會回傳 stream，適合傳入 callback。</p><blockquote><em>By returning a stream, the task system is able to plan the execution of those streams. But sometimes, especially when you’re in callback hell or calling some streamless plugin, you aren’t able to return a stream. That’s what the callback is for. To let the task system know that you’re finished and to move on to the next call in the execution chain.</em></blockquote><h3>To return a stream or not to?</h3><p>雖然知道如何在 Gulp 中標示非同步任務完成，也儘量使用回傳 stream 的方式，但在實際使用上還是不免碰到不少疑問：</p><ul><li>以上方式都可以任意使用嗎?</li><li>某些任務只適合某些方式嗎?</li><li>為什麼回傳了 stream，還是出現 “Did you forget to signal async completion?” 警告訊息或任務沒有成功執行?</li></ul><p>自己摸索一番後推敲出基本任務和複合任務的非同步完成標示方式不太相同。</p><p>關鍵在於 series 和 parallel 和 src 還有 dest 都是函式，但有不同的回傳值。</p><h4>基本任務</h4><p>src 和 dest (包含 pipe) 都和 Node.js stream 有關，負責回傳 stream。用 console.log可以看到 stream 物件有極複雜的內容。</p><pre>// gulpfile.js<br><br>const {src, dest} = require(&#39;gulp&#39;)<br><br>// 印出複雜的 stream 物件<br>console.log(src(...))<br>console.log(src(...).pipe(...))<br>console.log(src(...).pipe(...).pipe(dest(...)))</pre><p>因此，要標示非同步的基本任務完成，可以回傳 stream，也可以用其他的方式：</p><pre>// gulpfile.js<br><br>// OK<br>function task () {<br>    return src(...)<br>    .pipe(...)<br>    .pipe(dest(...))<br>}<br><br>// OK<br>function task () {<br>    return Promise.resolve(<br>      src(...)<br>      .pipe(...)<br>      .pipe(dest(...))<br>    )<br>}<br>        <br>// OK<br>function task (cb) {<br>  src(...)<br>  .pipe(...)<br>  .pipe(dest(...))<br>          <br>  cb()<br>}</pre><h4>複合任務</h4><p>series 和 parallel 是負責為任務建立排程的函式，執行後回傳 undefined 而非 stream。透過 console.log 印出函式則更容易了解：</p><pre>// gulpfile.js<br><br>const {src, dest, series, parallel} = require(&#39;gulp&#39;)<br><br>function task1 () {...}<br>function task2 () {...}<br><br>// [Function: series] [Function: parallel]<br>// 函式已宣告但未執行<br>console.log(series(task1, task2), parallel(task1, task2))<br>                   <br>// undefined undefined<br>// 函式已執行<br>console.log(series(task1, task2)(), parallel(task1, task2)())</pre><p>從印出的資料中可以看出，series 和 parallel 行為就像是一般沒有回傳值的 JavaScript 函式。</p><p>由於複合任務執行後不會回傳 stream，用回傳 stream 的方式標示則會出現警告訊息。用錯誤優先函式和 Promise 處理則會更適合。</p><pre>// gulpfile.js<br><br>// BAD <br>// 任務沒有執行，出現警告訊息；只是回傳函式，但沒有執行<br>function task() {<br>    return series(...)<br>}<br><br>// KINDA BAD <br>// 任務成功執行，出現警告訊息<br>function task () {<br>    return series(...)()<br>}               <br><br>// OK<br>function task() {<br>    return Promise.resolve(<br>      series(...)()<br>    )<br>}<br><br>// OK<br>function task (cb) {<br>    series(...)()<br>    cb()<br>}</pre><p>要注意的是，必須在 series 和 parallel 函式後加上 ()，才會執行裡面的基本任務。</p><h3>結論</h3><p>總結來說，基本任務可以用各種方式通知 Gulp 非同步任務已完成。</p><p>複合任務則無法使用回傳 stream 的方式。因為複合任務只是一般的函式，不會回傳 stream，因此不適合用回傳 stream 的方式標示。</p><h3>參考資料</h3><ul><li><a href="https://gulpjs.com/docs/en/getting-started/async-completion">Async Completion | gulp.js</a></li><li><a href="https://livebook.manning.com/book/front-end-tooling-with-gulp-bower-and-yeoman/chapter-2/1">Chapter 2. Getting started with Gulp · Front-End Tooling with Gulp, Bower, and Yeoman</a></li></ul><p><a href="https://stackoverflow.com/questions/29298244/gulp-where-is-the-gulp-task-callback-function-defined#:~:text=The%20callback%20function%20comes%20from,your%20task%20is%20%22done%22">https://stackoverflow.com/questions/29298244/gulp-where-is-the-gulp-task-callback-function-defined#:~:text=The callback function comes from,your task is “done”</a></p><h3>認識 Gulp.js 系列</h3><p>想要再多瀏覽 Gulp.js 4 相關教學，別錯過下列文章！</p><ol><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E4%BD%A0%E7%9A%84%E5%89%8D%E7%AB%AF%E9%96%8B%E7%99%BC%E8%90%AC%E7%94%A8%E5%88%80-f4b440bfa8bf">認識 Gulp.js — 你的前端開發萬用刀</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E4%BD%A0%E7%9A%84%E5%89%8D%E7%AB%AF%E9%96%8B%E7%99%BC%E8%90%AC%E7%94%A8%E5%88%80-7d36ff9ef5df">認識 Gulp.js — 建立 Gulp 任務</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-gulp-%E4%BB%BB%E5%8B%99%E7%9A%84%E9%9D%9E%E5%90%8C%E6%AD%A5%E7%89%B9%E6%80%A7-c38bc5374173">認識 Gulp.js — Gulp 任務的非同步特性</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E5%8C%AF%E5%87%BA%E5%8F%8A%E5%9F%B7%E8%A1%8C-gulp-%E4%BB%BB%E5%8B%99-f4d6d1b764f9">認識 Gulp.js — 匯出及執行 Gulp 任務</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E5%BB%BA%E7%AB%8B%E7%9B%A3%E8%81%BD%E4%BB%BB%E5%8B%99-b0a9a203a280">認識 Gulp.js — 建立監聽任務</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E6%8B%86%E5%88%86-gulp-%E4%BB%BB%E5%8B%99-2911d31048a0">認識 Gulp.js — 拆分 Gulp 任務</a></li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c38bc5374173" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[認識 Gulp.js — 你的前端開發萬用刀]]></title>
            <link>https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E4%BD%A0%E7%9A%84%E5%89%8D%E7%AB%AF%E9%96%8B%E7%99%BC%E8%90%AC%E7%94%A8%E5%88%80-7d36ff9ef5df?source=rss-e582e959bd07------2</link>
            <guid isPermaLink="false">https://medium.com/p/7d36ff9ef5df</guid>
            <category><![CDATA[網頁開發]]></category>
            <category><![CDATA[工具]]></category>
            <category><![CDATA[gulp]]></category>
            <category><![CDATA[前端]]></category>
            <category><![CDATA[自動化]]></category>
            <dc:creator><![CDATA[Zong-Rong Huang]]></dc:creator>
            <pubDate>Sat, 10 Dec 2022 13:43:53 GMT</pubDate>
            <atom:updated>2023-02-07T13:10:43.529Z</atom:updated>
            <content:encoded><![CDATA[<h3>認識 Gulp.js — 建立 Gulp 任務</h3><h4>認識 src、pipe、dest、series 和 parallel 方法</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/512/1*fO3Npx1KaLxTYj-MyS_8Jg.png" /><figcaption><a href="https://github.com/gulpjs/artwork">https://github.com/gulpjs/artwork</a></figcaption></figure><h3>概述</h3><p>本文會深入介紹 Gulp 任務中的重要方法：src、pipe、dest、series、parallel。</p><p>這些方法可以用來建立 Gulp 基本任務，或是將基本任務串連起來組成複合任務。</p><p>Gulp 官方文件並沒有區別基本任務和複合任務。由於兩者的寫法和一些特性差異，在本文中會介紹這兩種任務：</p><ul><li>基本任務：由 src、pipe 和 dest 組成的任務，對應到一則 JavaScript 函式。</li><li>複合任務：由 series 或 parallel 組裝多個基本任務而成的複雜任務。</li></ul><h3>認識基本任務</h3><p>Gulp 基本任務是由 src、pipe 和 dest 組成的任務，對應到一則 JavaScript 函式。</p><p>了解如何使用 src、pipe 和 dest 可以讓我們更清楚如何撰寫 Gulp 任務。</p><h4>src()：用 glob 格式指定檔案來源</h4><p>src() 用來指定要處理的檔案或資料夾路徑。</p><p>指定路徑時，要使用 glob 格式。glob 是一種用來表示檔案路徑的格式，可以寫出明確的檔案路徑或搭配特殊符號一次指定多個檔案。</p><p>如果來源為明確的單個檔案，在 src() 裡提供檔案路徑字串；如果是好幾個明確的檔案，用陣列標明所有檔案的路徑。</p><pre>// gulpfile.js<br><br>// 來源為單一檔案<br>function singleFileTask () {<br> return src(&#39;css/style.css&#39;)<br> .pipe(…)<br> .pipe(dest(…))<br>}<br><br>// 來源為多個檔案<br>function multiFilesTask () {<br> return src([&#39;css/style.css&#39;, &#39;css/mobile.css&#39;])<br> .pipe(…)<br> .pipe(dest(…))<br>}</pre><p>要注意的是，要提供相對於 gulpfile.js 的檔案路徑。</p><p>glob 格式也可以搭配特殊字元使用，能夠一次指定多個符合條件的檔案。</p><p>常見的 glob 特殊字元：</p><ul><li>*：0 到多個任意字元</li><li>**：0 到多層的資料夾</li><li>?：1 個任意字元，不區分大小寫</li><li>!：否定字元，排除指定的檔案和資料夾</li></ul><p>一起來了解這些特殊字元的用法：</p><pre>// gulpfile.js<br><br>// &#39;styles&#39; 資料夾內 (第一層) 所有 CSS 檔案<br>src(&#39;styles/*.css&#39;)<br><br>// &#39;js&#39; 資料夾內 (不論內部有多少層資料夾) 的所有 index.js 檔案<br>// (&#39;js/index.js&#39;、&#39;js/core/index.js&#39;、&#39;js/core/apis/index.js&#39; 等等)<br>src(&#39;js/**/index.js&#39;)<br><br>// &#39;img&#39; 資料夾內 (不論內部有多少層資料夾) 的所有 PNG 檔案<br>// (&#39;img/brand.png&#39;、&#39;img/icon/add.png&#39;、&#39;img/pages/banners/sale.png&#39; 等等)<br>src(&#39;img/**/*.png&#39;)<br><br>// &#39;img&#39; 資料夾內 (第一層), 所有名稱為任何字元 + ig 的 PNG 檔案 <br>// (例如：big.png, dig.png, Pig.png, Fig.png)<br>src(&#39;img/?ig.png&#39;)<br><br>// &#39;app&#39; 資料夾裡所有的 js 檔案，但是不包含 &#39;node_modules&#39; 資料夾<br>// 換句話說，&#39;node_modules&#39; 內任何 js 檔案皆略過不處理<br>// 必須先列出要處理的項目，最後才列出不要的項目 (也就是帶有 ! 的項目)<br>src([&#39;app/**/*.js&#39;, &#39;!node_modules/**&#39;])</pre><p>要注意的是，使用 ! 時，src() 中的檔案路徑要用陣列標示：先舉出要處理的檔案路徑，最後才舉出要排除的檔案路徑 (也就是加上 ! 的路徑)。</p><p>除了上述介紹的特殊字元外，glob 另有其他的特殊字元可以搭配使用。然而，對於一般專案來說，只要檔案放置得宜，* 和 ** 已可滿足大多數需求。</p><p>src() 找到指定檔案後，會將這些檔案轉換為 Node.js stream，再轉給接下來的 pipe() 處理。</p><h4>pipe()：結合外掛程式進行處理</h4><p>pipe 是 Node.js stream 的內建方法。一個 pipe() 會搭配一個外掛程式，相等於一個處理步驟，也是 Gulp 任務中最重要的部份。</p><p>取得 src() 提供的 Node.js stream 後，pipe() 會依照外掛程式的功能進行處理。使用外掛程式時，可能要額外提供一些設定值 (參考外掛程式說明文件)。</p><p>pipe() 處理完後，會再回傳 stream 給下一個 pipe() 接力處理。串接多個 pipe() 即可將多個步驟串連成一組工作流程。</p><h4>dest()：輸出檔案到指定目的地</h4><p>dest() 負責將上一個 pipe() 處理完的資料輸出到目的地。</p><p>dest() 要放在 pipe() 中使用，負責將上一個 pipe() 傳來的 stream 以檔案格式輸出到指定的位置。輸出路徑用字串標示。</p><h4>試寫第一個基本任務</h4><p>有了前面介紹的 src、pipe 和 dest 概念後，就可以來模擬撰寫第一個基本任務。</p><p>任務需求：</p><ul><li>壓縮所有 HTML 檔案，同時去除裡面不必要的空格。</li><li>壓縮後的 HTML 檔案輸出到 <strong>dist</strong> 資料夾。</li></ul><p>能夠壓縮 HTML 檔案的外掛程式眾多，在這裡以 <strong>gulp-htmlmin</strong> 作為外掛程式。</p><pre>// gulpfile.js<br><br>// 匯入 Gulp 方法和外掛程式<br>const { dest, src } = require(&quot;gulp&quot;);<br>const htmlmin = require(&quot;gulp-htmlmin&quot;);<br><br>// 建立任務函式<br>function minifyHTML() {<br> // 來源指定為專案資料夾內所有 HTML 檔案<br> return src(&quot;./**/*.html&quot;)<br> // 使用外掛程式進行壓縮，加上壓縮空白字元功能<br> .pipe(htmlmin({collapseWhitespace: true }))<br> // 輸出檔案至 &#39;dist&#39; 資料夾<br> .pipe(dest(&quot;dist&quot;));<br>}</pre><h3>認識複合任務</h3><p>了解如何建立基本任務後，一起來了解如何將多個基本任務組裝成複合任務。</p><p>Gulp 提供 series 和 parallel 方法，可將多個任務組合成複合任務，控制基本任務的執行順序：</p><ul><li>series(task1, task2, ..., taskN)：依序從 task1 執行到 taskN。適合用在基本任務有相依性/特定執行順序時使用，確保處理結果正確。</li><li>parallel(task1, task2, ..., taskN)：同時執行所有任務。適合當基本任務彼此獨立時使用，能夠縮短整體處理時間。</li></ul><p>用 series 和 parallel 組合基本任務就能建立複合任務。series() 和 parallel() 也可以作為彼此的子任務：</p><pre>// gulpfile.js<br><br>// 匯入 Gulp 方法和外掛程式<br>const { dest, src, series, parallel } = require(&quot;gulp&quot;);<br>const plugin1 = require(&quot;plugin1&quot;);<br>const plugin2 = require(&quot;plugin2&quot;);<br>const plugin3 = require(&quot;plugin3&quot;);<br>const plugin4 = require(&quot;plugin4&quot;);<br><br>// 建立基本任務<br>function task1 () {…}<br>function task2 () {…}<br>function task3 () {…}<br>function task4 () {…}<br><br>// 複合任務：依序從 task1 執行到 task4<br>series(task1, task2, task3, task4)<br> <br>// 複合任務：同時執行四個任務 <br>parallel(task1, task2, task3, task4)<br> <br>// 複合任務：執行 task1，接著執行 parallel(task2, task3), 最後執行 task4<br>series(task1, parallel(task2, task3), task4)<br> <br>// 複合任務：同時執行 task1、series(task2, task3) 和 task4<br>parallel(task1, series(task2, task3),task4)<br><br>// 其他可能任務組合…</pre><h4>不要重複執行任務</h4><p>使用 series 和 parallel 組裝基本任務時，記得避免重複執行特定任務，以免發生錯誤或拉長處理時間。</p><p>設計不良的複合任務 (取自 Gulp 官網範例)：</p><pre>// gulpfile.js<br><br>const { series, parallel } = require(&#39;gulp&#39;);<br><br>const clean = function(cb) {<br> // body omitted<br> cb();<br>};<br><br>const css = series(clean, function(cb) {<br> // body omitted<br> cb();<br>});<br><br>const javascript = series(clean, function(cb) {<br> // body omitted<br> cb();<br>});<br><br>exports.build = parallel(css, javascript);</pre><p>在上述範例中，css 和 javascript 複合任務都會執行clean 基本任務。也就是，clean 任務會執行兩次。</p><p>由於 clean 任務執行一次後就會清除目標檔案，再執行一次 clean 任務並沒有意義。另外，執行第二次 clean 任務時，指定的目標檔案已不存在，找不到要處理的檔案可能導致錯誤發生。</p><p>設計較佳的任務 (取自 Gulp 官網範例)：</p><pre>// gulpfile.js<br><br>const { series, parallel } = require(&#39;gulp&#39;);<br><br>function clean(cb) {<br> // body omitted<br> cb();<br>}<br><br>function css(cb) {<br> // body omitted<br> cb();<br>}<br><br>function javascript(cb) {<br> // body omitted<br> cb();<br>}<br><br>exports.build = series(clean, parallel(css, javascript));</pre><p>比較好的作法是將重覆用到的 clean 任務抽取出來。執行一次 clean 任務後，再同時執行 css 和 javascript 任務。如此一來，執行結果能夠滿足需求，不會重覆執行導致錯誤發生。</p><h4>試寫第一個複合任務</h4><p>有了前面介紹的 series 和 parallel 概念後，就可以來模擬撰寫第一個複合任務。</p><p>任務需求：</p><ul><li>將 Markdown 檔案轉換成 HTML 檔案。</li><li>壓縮 HTML 檔案，同時去除裡面不必要的空格。</li><li>壓縮後的 HTML 檔案輸出到 <strong>dist</strong> 資料夾。</li></ul><p>在這裡會使用的外掛程式：</p><ul><li><strong>gulp-remarkable</strong>：將 Markdown 轉換成 HTML。</li><li><strong>gulp-htmlmin</strong>：壓縮 HTML 內容。</li></ul><pre>// gulpfile.js<br><br>// 匯入 Gulp 方法和外掛程式<br>const { dest, src, series } = require(&quot;gulp&quot;);<br>const markdown = require(&quot;gulp-remarkable&quot;);<br>const htmlmin = require(&quot;gulp-htmlmin&quot;);<br><br>function convertMarkdownToHTML() {<br> return src(&#39;readme.md&#39;)<br> .pipe(markdown())<br> .pipe(dest(&#39;dist&#39;))<br>}<br><br>function minifyHTML() {<br> return src(&quot;dist/readme.html&quot;)<br> .pipe(htmlmin({collapseWhitespace: true }))<br> .pipe(dest(&quot;dist&quot;));<br>}<br><br>exports.build = series(convertMarkdownToHTML, minifyHTML)</pre><p>從流程順序思考，必須先將 Markdown 轉換成 HTML 後才能進行壓縮，代表任務之間有相依性，因此使用 series 方法安排任務。</p><p>convertMarkdownToHTML 任務會將 Markdown 轉換成 HTML，輸出 HTML (<strong>readme.html</strong>) 至 <strong>dist</strong> 資料夾。minifyHTML 任務接著處理 <strong>readme.html</strong>，再將結果輸出至 <strong>dist</strong> 資料夾。由於輸出檔案與原有檔案同名，會自動覆蓋之前的檔案。</p><h3>結論</h3><p>Gulp 基本任務由 src、pipe 和 dest 組成，對應到一則 JavaScript 函式。在 src 內要用 glob 格式指定來源檔案；在 pipe 內指定外掛程式；在 dest 內指定檔案輸出位置。</p><p>Gulp 複合任務由 series 或 parallel 組裝多個基本任務而成。series 能讓任務依序執行，parallel 能讓任務同時執行。要注意是否有任務重覆執行，以免拉長處理時間或發生錯誤。</p><h3>參考資料</h3><p><a href="https://www.malikbrowne.com/blog/a-beginners-guide-glob-patterns">https://www.malikbrowne.com/blog/a-beginners-guide-glob-patterns</a></p><ul><li><a href="https://amobiz.github.io/2015/11/14/gulp-glob/">Gulp 學習筆記 - Glob 篇</a></li><li><a href="https://blog.logrocket.com/understanding-using-globs-node-js/">Understanding the glob pattern in Node.js - LogRocket Blog</a></li><li><a href="https://gulpjs.com/docs/en/getting-started/explaining-globs">Explaining Globs | gulp.js</a></li><li><a href="https://gulpjs.com/docs/en/getting-started/creating-tasks">Creating Tasks | gulp.js</a></li></ul><h3>認識 Gulp.js 系列</h3><p>想要再多瀏覽 Gulp.js 4 相關教學，別錯過下列文章！</p><ol><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E4%BD%A0%E7%9A%84%E5%89%8D%E7%AB%AF%E9%96%8B%E7%99%BC%E8%90%AC%E7%94%A8%E5%88%80-f4b440bfa8bf">認識 Gulp.js — 你的前端開發萬用刀</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E4%BD%A0%E7%9A%84%E5%89%8D%E7%AB%AF%E9%96%8B%E7%99%BC%E8%90%AC%E7%94%A8%E5%88%80-7d36ff9ef5df">認識 Gulp.js — 建立 Gulp 任務</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-gulp-%E4%BB%BB%E5%8B%99%E7%9A%84%E9%9D%9E%E5%90%8C%E6%AD%A5%E7%89%B9%E6%80%A7-c38bc5374173">認識 Gulp.js — Gulp 任務的非同步特性</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E5%8C%AF%E5%87%BA%E5%8F%8A%E5%9F%B7%E8%A1%8C-gulp-%E4%BB%BB%E5%8B%99-f4d6d1b764f9">認識 Gulp.js — 匯出及執行 Gulp 任務</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E5%BB%BA%E7%AB%8B%E7%9B%A3%E8%81%BD%E4%BB%BB%E5%8B%99-b0a9a203a280">認識 Gulp.js — 建立監聽任務</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E6%8B%86%E5%88%86-gulp-%E4%BB%BB%E5%8B%99-2911d31048a0">認識 Gulp.js — 拆分 Gulp 任務</a></li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7d36ff9ef5df" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[認識 Gulp.js — 你的前端開發萬用刀]]></title>
            <link>https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E4%BD%A0%E7%9A%84%E5%89%8D%E7%AB%AF%E9%96%8B%E7%99%BC%E8%90%AC%E7%94%A8%E5%88%80-f4b440bfa8bf?source=rss-e582e959bd07------2</link>
            <guid isPermaLink="false">https://medium.com/p/f4b440bfa8bf</guid>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[前端]]></category>
            <category><![CDATA[工具]]></category>
            <category><![CDATA[網頁開發]]></category>
            <category><![CDATA[gulp]]></category>
            <dc:creator><![CDATA[Zong-Rong Huang]]></dc:creator>
            <pubDate>Sat, 10 Dec 2022 13:10:07 GMT</pubDate>
            <atom:updated>2023-02-07T13:15:00.271Z</atom:updated>
            <content:encoded><![CDATA[<h3>認識 Gulp.js — 你的前端開發萬用刀</h3><h4>認識 Gulp.js 及整體處理流程</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/512/1*fO3Npx1KaLxTYj-MyS_8Jg.png" /><figcaption><a href="https://github.com/gulpjs/artwork">https://github.com/gulpjs/artwork</a></figcaption></figure><h3>概述</h3><p>本文會介紹 Gulp.js 的用途及大致運作流程。</p><p>本文內容如下：</p><p>．什麼是 Gulp.js?</p><p>．Gulp.js 核心 — 外掛程式 (plugin)</p><p>．Gulp 任務</p><p>．用 JavaScript 實現 Gulp 任務</p><h3>什麼是 Gulp.js？</h3><p>Gulp 是自動化流程處理工具，可與各種外掛程式結合，執行不同任務。開發者透過 JavaScript 就能撰寫處理任務，組合成一套符合自己需求的工作流程。</p><p>透過 CLI 指令即可自動執行一連串繁瑣、重複性高的工作流程，大幅提升開發效率。</p><h3>Gulp.js 核心 — 外掛程式 (plugin)</h3><p>Gulp 運作基礎在於豐富多樣的外掛程式 (plugin) 生態系：每個外掛程式各自負責單一功能，能夠讀取、修改、寫入檔案。</p><p>常見的外掛程式功能有修改檔名、壓縮檔案內容及合併檔案內容等等。</p><p>開發者除了能依需求下載適合的外掛程式來使用，還可以串連多個外掛程式組裝成不同的處理流程，滿足各種開發情境需求。</p><p>搜尋 Gulp 外掛程式的網站：</p><ul><li>官方網站：<a href="https://gulpjs.com/plugins">https://gulpjs.com/plugins</a></li><li>npm Registry：<a href="https://www.npmjs.com/">https://www.npmjs.com</a> (加上 「gulp」 關鍵字)</li></ul><h3>Gulp 任務</h3><p>Gulp 任務是工作流程的核心元素，由三個基礎方法構成：</p><ul><li>src()：指定輸入的來源檔案／資料夾。</li><li>pipe()：指定要使用的外掛程式。一個 pipe() 等於一個處理步驟，每個處理步驟由外掛程式負責。</li><li>dest()：指定資料輸出的目的地。dest() 要放在 pipe() 內使用。</li></ul><p><strong>Gulp 從 </strong><strong>src() 輸入要處理的檔案，經過各個 </strong><strong>pipe() 及外掛程式的處理，再將最終成果輸出到 </strong><strong>dest() 指定的位置</strong>：</p><pre>src(來源) <br>&gt; pipe(外掛1) <br>&gt; …<br>&gt; pipe(外掛N) <br>&gt; pipe(dest(目的地))</pre><p>假設有一個任務要將 SCSS 轉換為 CSS 檔案，CSS 檔案內容也要壓縮：</p><pre>src(style.scss) <br>&gt; pipe(轉換為 CSS) <br>&gt; pipe(壓縮 CSS) <br>&gt; pipe(dest(style.css))</pre><p>不論需求如何改變，Gulp 任務都有類似的架構。</p><h3>用 JavaScript 實現 Gulp 任務</h3><p>在 Gulp 中實現上述流程的方式是用 JavaScript 撰寫任務。一個任務對應到一則 JavaScript 函式。</p><p>在任務內可以任意串連多個 pipe()，讓不同外掛程式接力處理資料。</p><p>要注意的是，Gulp 任務函式必須寫在 <strong>gulpfile.js</strong> 檔案內，Gulp 才能讀取到裡面的函式來執行任務。</p><p>任務的基本寫法：<br>1. 引入要使用的 Gulp 方法和外掛程式。<br>2. 建立任務函式。函式內含 src()、pipe() 和 dest()。</p><pre>// gulpfile.js<br><br>// 引入 Gulp 方法及外掛程式<br>const {src, dest} = require(&#39;gulp&#39;)<br>const plugin1 = require(&#39;plugin1&#39;)<br>const plugin2 = require(&#39;plugin2&#39;)<br><br>// 函式即任務<br>function 任務 () {<br>  return gulp.src(&#39;來源路徑&#39;)<br>   .pipe(plugin1())<br>   .pipe(plugin2())<br>   .pipe(dest(&#39;目的地路徑&#39;))<br>}</pre><h3>結論</h3><p>Gulp.js 能建立符合自動化處理流程。整體運作流程以任務為核心。透過 JavaScript 即可自行撰寫任務，在任務內指定檔案來源、用到的外掛程式和資料輸出位置，輕鬆建立符合需求的處理流程。</p><p>下一篇文章會深入 src()、pipe() 和 dest() 的用法。</p><h3>參考資料</h3><ul><li><a href="https://gulpjs.com/docs/en/getting-started/quick-start">Quick Start | gulp.js</a></li><li><a href="https://livebook.manning.com/book/front-end-tooling-with-gulp-bower-and-yeoman/chapter-2/1">Chapter 2. Getting started with Gulp · Front-End Tooling with Gulp, Bower, and Yeoman</a></li></ul><h3>認識 Gulp.js 系列</h3><p>想要再多瀏覽 Gulp.js 4 相關教學，別錯過下列文章！</p><ol><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E4%BD%A0%E7%9A%84%E5%89%8D%E7%AB%AF%E9%96%8B%E7%99%BC%E8%90%AC%E7%94%A8%E5%88%80-f4b440bfa8bf">認識 Gulp.js — 你的前端開發萬用刀</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E4%BD%A0%E7%9A%84%E5%89%8D%E7%AB%AF%E9%96%8B%E7%99%BC%E8%90%AC%E7%94%A8%E5%88%80-7d36ff9ef5df">認識 Gulp.js — 建立 Gulp 任務</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-gulp-%E4%BB%BB%E5%8B%99%E7%9A%84%E9%9D%9E%E5%90%8C%E6%AD%A5%E7%89%B9%E6%80%A7-c38bc5374173">認識 Gulp.js — Gulp 任務的非同步特性</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E5%8C%AF%E5%87%BA%E5%8F%8A%E5%9F%B7%E8%A1%8C-gulp-%E4%BB%BB%E5%8B%99-f4d6d1b764f9">認識 Gulp.js — 匯出及執行 Gulp 任務</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E5%BB%BA%E7%AB%8B%E7%9B%A3%E8%81%BD%E4%BB%BB%E5%8B%99-b0a9a203a280">認識 Gulp.js — 建立監聽任務</a></li><li><a href="https://medium.com/@zongrong.h886/%E8%AA%8D%E8%AD%98-gulp-js-%E6%8B%86%E5%88%86-gulp-%E4%BB%BB%E5%8B%99-2911d31048a0">認識 Gulp.js — 拆分 Gulp 任務</a></li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f4b440bfa8bf" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[用 CSS 自訂表單元件的外觀樣式]]></title>
            <link>https://medium.com/@zongrong.h886/%E7%94%A8-css-%E8%87%AA%E8%A8%82%E8%A1%A8%E5%96%AE%E5%85%83%E4%BB%B6%E7%9A%84%E5%A4%96%E8%A7%80%E6%A8%A3%E5%BC%8F-b828d14da0cb?source=rss-e582e959bd07------2</link>
            <guid isPermaLink="false">https://medium.com/p/b828d14da0cb</guid>
            <category><![CDATA[表單]]></category>
            <category><![CDATA[css]]></category>
            <category><![CDATA[html]]></category>
            <category><![CDATA[欄位]]></category>
            <category><![CDATA[按鈕]]></category>
            <dc:creator><![CDATA[Zong-Rong Huang]]></dc:creator>
            <pubDate>Sat, 10 Sep 2022 04:51:27 GMT</pubDate>
            <atom:updated>2022-09-10T10:59:00.148Z</atom:updated>
            <content:encoded><![CDATA[<h4>用特殊屬性和偽元素解開表單元件的 CSS 封印</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ivZHtOnp120AO6p7" /><figcaption>Photo by <a href="https://unsplash.com/@polarmermaid?utm_source=medium&amp;utm_medium=referral">Anne Nygård</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><h3>概述</h3><p>輸入欄位、進度條、核取方塊 (checkbox)、單選按鈕 (radio button) 等表單控制項目 (form controls) 樣式由瀏覽器控制，無法直接用一般的 CSS 選擇器進行處理。如果自行手刻表單元件，需要處理 HTML、CSS 和 JavaScript 三者互動，會耗費不少時間。</p><p>幸運的是，CSS 內有一些屬性和偽元素能夠解開瀏覽器的封印，讓我們調整元件的樣式。一起來認識這些屬性和選擇器吧！</p><p>對照 <a href="https://codepen.io/Zong-Rong/pen/MWGyzEp">CodePen</a> 看會更有感覺 (建議使用 Chrome 最新版開啟)。</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fcodepen.io%2FZong-Rong%2Fembed%2Fpreview%2FMWGyzEp%3Fdefault-tabs%3Dcss%252Cresult%26height%3D600%26host%3Dhttps%253A%252F%252Fcodepen.io%26slug-hash%3DMWGyzEp&amp;display_name=CodePen&amp;url=https%3A%2F%2Fcodepen.io%2FZong-Rong%2Fpen%2FMWGyzEp&amp;image=https%3A%2F%2Fshots.codepen.io%2FZong-Rong%2Fpen%2FMWGyzEp-512.jpg%3Fversion%3D1662784600&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=codepen" width="800" height="600" frameborder="0" scrolling="no"><a href="https://medium.com/media/da23fef1eb25f6f25624ee6dbc350d5f/href">https://medium.com/media/da23fef1eb25f6f25624ee6dbc350d5f/href</a></iframe><p>本文內容如下：</p><ul><li>用 accent-color 調整重點顏色</li><li>用 appearance: none 和偽元素 (pseudo-element) 大幅度重設樣式</li><li>核取方塊 (&lt;input type=&quot;checkbox&quot;&gt;)</li><li>單選按鈕 (&lt;input type=&quot;radio&quot;/&gt;)</li><li>檔案上傳按鈕 (&lt;input type=&quot;file&quot;&gt;)</li><li>滑桿 (&lt;input type=&quot;range&quot;&gt;)</li><li>進度條 (&lt;progress&gt;)</li><li>計量條 (&lt;meter&gt;)</li><li>一般輸入框 (&lt;input&gt;)</li><li>數字輸入框 (&lt;input type=&quot;number&quot;&gt;)</li><li>搜尋輸入框 (&lt;input type=&quot;search&quot;&gt;)</li></ul><h3>用 accent-color 調整重點顏色</h3><p>強調色 (accent color) 是指頁面主要顏色 (primary color) 的對比色，在頁面上的視覺效果突出，能夠吸引引使用者的注意。</p><p>在各家瀏覽器中，表單項目的預設強調色為藍色。如果要改用其他顏色，用 accent-color 就能進行設定。</p><pre>&lt;label&gt;&lt;input type=&quot;checkbox&quot;&gt;Checkbox&lt;/label&gt;<br>&lt;label&gt;&lt;input type=&quot;radio&quot;&gt;Radio button&lt;/label&gt;<br>&lt;label&gt;&lt;input type=&quot;range&quot;&gt;Range slider&lt;/label&gt;<br>&lt;progress value=&quot;0.5&quot;&gt;&lt;/progress&gt;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/337/1*UT7uHRHjj7RGQpLm_DX0Ig.png" /></figure><p>接著再勾選所有選項，就會發現套用新的顏色了。</p><p>同時，也會發現進度條中不僅代表值的部份改變顏色，連空白部份的顏色也改變了。這是瀏覽器自動計算的結果，確保進度條的值和空白有足夠的視覺對比。</p><p>目前 accent-color 只能用在下列 HTML 元素：</p><ul><li>&lt;input type=&quot;checkbox&quot;&gt;</li><li>&lt;input type=&quot;radio&quot;&gt;</li><li>&lt;input type=&quot;range&quot;&gt;</li><li>&lt;progress&gt;</li></ul><h3>用 appearance: none 和偽元素 (pseudo-element) 大幅度重設樣式</h3><p>appearance: none 會取消瀏覽器對表單項目的樣式設定，允許我們自行設計表單項目的外觀。在這裡會搭配 -webkit 偽元素或一般偽元素來處理細部樣式。</p><p>appearance: none 可相容大部份的新版瀏覽器。若要相容較舊的瀏覽器，可加入帶有 vendor prefix 的屬性：</p><ul><li>Safari、Chrome、iOS 和 Android：-webkit-appearance: none</li><li>Firefox：-moz-appearance: none</li></ul><p>使用 vendor prefix 屬性時，記得要放在標準屬性之前。</p><p>如果瀏覽器只能理解 vendor prefix 屬性，會自動忽略不支援的標準屬性。</p><p>如果瀏覽器皆能理解這些屬性，則會依順序採用標準屬性，避開行為不穩定的 vendor prefix 屬性，因為各家瀏覽器推出的 vendor prefix 屬性實作效果可能不一致。</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/380/1*uVC7LR1jslyzT_RCyTmCag.png" /></figure><p>順帶一提，appearance: none 會取消原有樣式，之前介紹的 accent-color 設定會連帶失效。</p><p>以下會示範如何調整核取方塊、單選按鈕、數字輸入框、滑桿和進度條的樣式。</p><h3>核取方塊 (&lt;input type=&quot;checkbox&quot;&gt;)</h3><p>核取方塊不需要選取特別的偽元素來修改細部樣式。</p><p>只有在修改勾選符號樣式時，才會用到 ::before 或 ::after 這些常見的偽元素。這些偽元素裡會用到 content 屬性。</p><p>content 屬性可以接受不同的值：</p><ul><li>字串 (一般文字或 CSS code)</li><li>圖片路徑</li><li>線性漸層 (linear gradient)、放射漸層 (radial gradient)、錐狀漸層 (conic-gradient) 等各種漸層</li></ul><pre>&lt;input type=&quot;checkbox&quot;&gt;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/607/1*07SrNOj73hhDocjEmOesCw.png" /></figure><p>偽元素裡的 content 屬性可直接寫入要呈現的字串或特殊符號的 CSS code。</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/472/1*_q1Cfaqgn_SCpdkVI5Tzvw.png" /></figure><p>content 屬性可以接受圖片路徑，但無法控制圖片大小。可以搭配 transform 屬性調整圖片大小和位置。</p><p>另一種比較推薦的作法是：content 不設定任何值，只設定偽元素的寬、高。用 background-image 將圖片置入偽元素中，再利用 background-size 控制背景圖片大小。</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/658/1*DuRNLodKSjVODOnMJyBmbg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/590/1*5pfkeQ8MV9Gi_3mCqilUJA.png" /></figure><p>content 也可以設定漸層色。設定好偽元素的寬、高後，就可以呈現漸層色塊。</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/582/1*UY5L5Qr2avvVMhwolC7tyQ.png" /></figure><h3>單選按鈕 (&lt;input type=&quot;radio&quot;/&gt;)</h3><p>單選按鈕也是利用 ::before 或 ::after 偽元素，搭配 content 屬性處理。如同在處理核取方塊時一樣，利用 content 屬性可呈現圖片、漸層色，能讓勾選符號看起來更有創意。</p><pre>&lt;input type=&quot;radio&quot;/&gt;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/447/1*_GpD304RQtF8EqjR69y1VQ.png" /></figure><h3>檔案上傳按鈕 (&lt;input type=&quot;file&quot;&gt;)</h3><p>檔案上傳按鈕的樣式可由 ::file-selector-button 偽元素進行調整。文字內容由瀏覽器決定，僅能修改文字樣式。</p><pre>&lt;input type=&quot;file&quot;&gt;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/531/1*QWQI_FuRy0fpmL4JYs5ifw.png" /></figure><h3>滑桿 (&lt;input type=&quot;range&quot;&gt;)</h3><p>::-webkit-slider-thumb 和 ::-webkit-slider-runnable-track 可以分別選取到滑桿把手和滑桿軌道。</p><p>要注意的是，修改滑桿軌道的樣式不須加入 appearance:none，但修改滑桿把手時必須加入 appearance:none。</p><pre>&lt;input type=&quot;range&quot;&gt;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/649/1*TtaS2aYjUPhhNiK7qxbEBg.png" /></figure><h3>進度條 (&lt;progress&gt;)</h3><p>透過::-webkit-progress-value 能夠修改進度值的樣式，::-webkit-progress-bar 可修改整體進度條的樣式。</p><pre>&lt;progress value=&quot;0.5&quot;&gt;50%&lt;/progress&gt;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/632/1*O1XjB7mTLCzPtp5JllTxZw.png" /></figure><h3>計量條 (&lt;meter&gt;)</h3><p>::-webkit-meter-inner-element 可以修改計量條外框的樣式，::-webkit-meter-bar 可以修改計量條整體樣式。</p><pre>&lt;meter min=&quot;0&quot; low=&quot;30&quot; optimum=&quot;70&quot; high=&quot;80&quot; max=&quot;100&quot; value=&quot;&quot;&gt;&lt;/meter&gt;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/439/1*3krbStBk5UQQnm0KoRdPZQ.png" /></figure><p>在一般情況下，無法修改內部累計值條的樣式。只有當累計值達到特定範圍時，才可顯示對應的樣式。</p><p>::-webkit-meter-optimum-value：當 value 與 optimum 值一致時，則會顯示指定樣式。</p><p>::-webkit-meter-suboptimum-value：當 value 值比 low 值低或比 high 值高，會顯示指定樣式。用來表示值落在非預期的範圍外。</p><p>::-webkit-meter-even-less-good-value：當 value 和 optimum 值皆不位於 low 至 high 中間範圍，而且分別落在兩端。用來差示目前數值與理想值不僅落在預期範圍外，而且兩者差距過大，為相當不理想的狀況。屬性值符合下列狀況時，會顯示指定樣式：</p><ul><li>value &lt; low &lt; high &lt; optimum</li><li>optimum &lt; low &lt; high &lt; value</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/557/1*cZ_NsLyeFea8lns8-Oz4NQ.png" /></figure><h3>一般輸入框 (&lt;input&gt;)</h3><p>輸入框的 placeholder 屬性用來提示使用者應輸入什麼資料或資料格式。預設為淺灰色。透過 ::placeholder 偽元素可修改文字樣式。</p><pre>&lt;input placeholder=&quot;Enter name&quot;&gt;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/363/1*QFyoqJ-QZo2AiYsl-KazIA.png" /></figure><h3>數字輸入框 (&lt;input type=&quot;number&quot;&gt;)</h3><p>::-webkit-inner-spin-button 偽元素選取到數字輸入框內預設的上下箭頭按鈕 (用來增加或減少框內的數字)。這個偽元素比較特別，不用 appearance: none，就可以修改部份樣式。</p><p>例如，可以用 display: none 移除預設箭頭按鈕。輸入框能夠輸入更多位數的數字。即使移除箭頭按鈕，仍然可以利用鍵盤上下箭頭鍵來調整數值。</p><pre>&lt;input type=&quot;number&quot;&gt;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/548/1*Wy-9-n9NHPXBMr8U7MZ2RA.png" /></figure><p>大部份的 CSS 屬性無法調整箭頭按鈕的樣式，transform 是少數有作用的屬性：</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/599/1*7gpvaedTcQKGcLBz1q35jw.png" /></figure><p>如果加上 appearance: none，可以修改多一點的樣式，但箭號會消失不見。也無法更換箭號或設定其他實用的樣式。</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/548/1*TzK0niGyEUP37m0rmY4TBQ.png" /></figure><h3>搜尋輸入框 (&lt;input type=&quot;search&quot;&gt;)</h3><p>在搜尋輸入框內輸入內容時，會出現瀏覽器預設的清除輸入內容按鈕，讓使用者可以一次清除輸入的內容。透過 ::-webkit-search-cancel-button 可以移除這個按鈕，避免使用者手誤清除已輸入的內容。</p><pre>&lt;input type=&quot;search&quot;&gt;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/557/1*XFQV6CJR4M4BTGKZbDGGhw.png" /></figure><h3>結論</h3><p>表單控制項的樣式大多受到瀏覽器控制，無法直接修改。透過 accent-color、appearance: none 和偽元素，開發者能有更大自由修改樣式。特別是 -webkit- 開頭的偽元素，能夠讓我們進入元件內部，直接修改細部樣式。</p><p>透過上面介紹的方法，開發者不需特別費心自製符合需求的表單元件。只需稍微修改樣式，不需重寫原有功能，開發效率更好。也不需引入任何 UI 庫或函式庫，能夠減少專案程式碼大小。</p><p>如果想要修改其他沒有介紹到的表單元件，但發現無法透過一般 CSS 語法直接調整，建議先使用 appearance: none 試著解開瀏覽器的樣式封印，再查查看有無對應的偽元素能夠達成需求。</p><p>若無法達成需求，再考慮自製需要的元件或引入合適的 UI 庫/函式庫來處理。</p><h3>參考資料</h3><ul><li><a href="https://css-tricks.com/styling-cross-browser-compatible-range-inputs-css/">Styling Cross-Browser Compatible Range Inputs with CSS</a></li><li><a href="https://moderncss.dev/pure-css-custom-styled-radio-buttons/">Pure CSS Custom Styled Radio Buttons</a></li><li><a href="https://moderncss.dev/pure-css-custom-checkbox-style/">Pure CSS Custom Checkbox Style</a></li><li><a href="https://css-tricks.com/almanac/properties/a/accent-color/">accent-color</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/WebKit_Extensions">WebKit CSS extensions</a></li><li><a href="https://www.sitepoint.com/12-css3-vendor-prefix-crisis-myths/">12 CSS3 Vendor Prefix Crisis Myths</a></li><li><a href="https://www.thoughtco.com/css-vendor-prefixes-3466867">CSS Vendor Prefixes</a></li><li><a href="https://ithelp.ithome.com.tw/articles/10203733?sc=iThelpR">網頁設計色彩配置概念</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b828d14da0cb" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[啟動 GitLab 雙因素驗證 (2FA)]]></title>
            <link>https://medium.com/@zongrong.h886/%E5%95%9F%E5%8B%95-gitlab-%E9%9B%99%E5%9B%A0%E7%B4%A0%E9%A9%97%E8%AD%89-bff89c6065c5?source=rss-e582e959bd07------2</link>
            <guid isPermaLink="false">https://medium.com/p/bff89c6065c5</guid>
            <category><![CDATA[驗證]]></category>
            <category><![CDATA[gitlab]]></category>
            <category><![CDATA[權限管理]]></category>
            <category><![CDATA[pat]]></category>
            <category><![CDATA[安全]]></category>
            <dc:creator><![CDATA[Zong-Rong Huang]]></dc:creator>
            <pubDate>Wed, 31 Aug 2022 16:48:59 GMT</pubDate>
            <atom:updated>2022-08-31T16:58:27.690Z</atom:updated>
            <content:encoded><![CDATA[<h4>不需手動輸入帳號密碼，立即安全登入 GitLab</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*y6xhGx7gX7Az94Qy" /><figcaption>Photo by <a href="https://unsplash.com/@pankajpatel?utm_source=medium&amp;utm_medium=referral">Pankaj Patel</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><h3>概述</h3><p>GitLab 採用<strong>雙因素驗證</strong> (two-factor authentication，2FA)，與傳統的單一因素密碼驗證相比，能夠提高資料安全性。</p><p>GitLab 雙因素驗證需要搭配 personal access token (PAT)。啟動雙因素驗證後，需使用 PAT 作為個人專屬的驗證碼。</p><p>將 PAT 儲存在電腦上後，不需要再次輸入帳號密碼，就能直接存取 GitLab 儲存庫，相當方便。</p><p>本文會引導 GitLab 使用者完成下列設定：</p><ol><li>在 GitLab 啟動雙因素驗證</li><li>建立 GitLab PAT</li><li>將 PAT 紀錄到電腦上</li></ol><h3>啟動雙因素驗證</h3><p>GitLab 雙因素驗證要求使用者提供兩種驗證因素進行驗證：</p><ul><li>Personal Access Token (PAT，作用類似密碼)</li><li>行動裝置</li></ul><p>與以往只需密碼即可登入的流程相比，雙因素驗證更能提高資料安全性。</p><ol><li>先在行動裝置上安裝用來產生一次性密碼的驗證程式。GitLab 推薦使用 <a href="https://authy.com/">Authy</a>、<a href="https://duo.com/product/multi-factor-authentication-mfa/duo-mobile-app">Duo Mobile</a>、<a href="https://support.google.com/accounts/answer/1066447?hl=en">Google Authenticator</a> 和 <a href="https://www.microsoft.com/en-us/security/mobile-authenticator-app">Microsoft Authenticator</a>。此處以 Google Authenticator 為例。</li><li>使用個人帳號、密碼登入 GitLab。</li><li>按一下左方側欄上的 <strong>Account</strong> 類別。</li><li>按一下 <strong>Enable Two-factor Authentication</strong>。畫面中會出現 QR code 及相關資料。</li><li>開啟行動裝置上的驗證程式，採用任一方式來產生一次性密碼：</li></ol><ul><li>掃瞄 QR code (推薦)</li><li>依照驗證程式說明，手動輸入 GitLab QR code 旁列出的 <strong>Account</strong> 及 <strong>Key</strong> 字串</li></ul><p>6. 成功後，驗證程式中會出現六碼數字的一次性密碼。將一次性密碼輸入到 GitLab 頁面 QR code 下方的 <strong>Pin code</strong> 欄位。</p><p>7. 按一下 <strong>Submit</strong>。</p><p>8. 成功完成二因素驗證後，GitLab 會提供一份 recovery codes，內有多個一次性密碼。按一下 <strong>Download codes</strong> 下載 recovery codes (檔名為 <strong>gitlab-recovery-codes.txt</strong>)，再將檔案保存在安全的地方。如果日後遺失行動裝置，可以使用檔案內的一次性密碼登入 GitLab。</p><h3>建立 PAT</h3><p>成功啟動雙因素驗證後，接著建立 PAT。</p><ol><li>在 GitLab 頁面上，按一下右上方的大頭貼。</li><li>按一下選單中的 <strong>Edit profile</strong>。</li><li>按一下左側導覽列的 <strong>Access Tokens</strong>。</li><li>前往 <strong>Personal Access Tokens</strong> 區塊，填入所需資料：</li></ol><ul><li><strong>Toke name</strong>：用來辨識該 PAT 的名稱。建議用 PAT 的用途來命名。</li><li><strong>Expiration date</strong>：指定該 PAT 到期日。</li><li><strong>Select scopes</strong>：選取該 PAT 適用的權限層級。建議全部勾選。</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SBmDignZyST42bKfhFguiw.png" /></figure><p>5. 按一下 <strong>Create personal access token</strong> 來建立 PAT。複製 PAT 後，再存放在安全的地方。</p><p>6. 建立好的 PAT 會出現在 <strong>Active personal access tokens</strong> 區塊內。如果要取消已建立的 PAT，按一下 <strong>Revoke</strong>，該 PAT 立即失效。</p><h3>再次登入</h3><p>建立 PAT 後，原有的密碼即失效，因此要重新登入 GitLab。使用 PAT 作為新的密碼來重新驗證。</p><ol><li>登出您的 GitLab 帳號，回到登入頁面。</li><li>填入您的帳號，密碼則填入剛才複製的 PAT。即可進入 GitLab 主畫面。<br>由於先前已啟動二因素驗證，之前手動設定的密碼會失效，只可使用 PAT 作為密碼登入。</li></ol><h3>在本機電腦儲存 PAT</h3><p>將 PAT 儲存在常用的工作電腦中，就不用重覆手動輸入。之後進入 GitLab 專案，電腦就會自動取用 PAT 登入，不用再手動輸入密碼。</p><p>這裡會以 macOS 的鑰匙圈存取 (Keychain Access) 程式做示範。鑰匙圈存取是 macOS 專門用來管理使用者眾多服務帳號密碼的應用程式。</p><ol><li>開啟 macOS 的 鑰匙圈存取應用程式。</li><li>在上方工作列中按一下 檔案 &gt; 新增密碼項目。</li><li>填入下列資料：</li></ol><ul><li><strong>鑰匙圈項目名稱</strong>：填入 GitLab 專案網址 (例如https://example.git.com)。</li><li><strong>帳號名稱</strong>：填入你的 GitLab 登入帳號。</li><li><strong>密碼</strong>：填入你的 PAT。</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/836/1*zK3tmUAZh1KBv0fQb9iaaA.png" /></figure><p>4. 按一下 <strong>加入</strong> 即完成。</p><p>在此臺電腦上即可自動登入 GitLab 專案，不用再手動輸入帳號密碼。只有在 PAT 到期時，才需要再建立新的 PAT，並在鑰匙圈存取應用程式中更新密碼。</p><h3>結論</h3><p>和傳統常見的密碼驗證流程相比，雙因素驗證步驟較為繁瑣，但提供了更安全的登入機制，減少惡意使用者取得重要資料的可能性。</p><p>搭配 PAT 使用，還能讓登入流程更加簡化，不再需要手動輸入帳號密碼，減少帳密流出的風險。</p><h3>參考資料</h3><ul><li><a href="https://docs.gitlab.com/ee/security/two_factor_authentication.html">Enforce two-factor authentication</a></li><li><a href="https://docs.gitlab.com/ee/user/profile/account/two_factor_authentication.html">Two-factor authentication</a></li><li><a href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#personal-access-tokens">Personal access tokens</a></li><li><a href="https://experience.dropbox.com/zh-tw/resources/what-is-2fa">什麼是雙因素驗證 (2FA)？</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bff89c6065c5" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>