<?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 pxn on Medium]]></title>
        <description><![CDATA[Stories by pxn on Medium]]></description>
        <link>https://medium.com/@nightspirit622?source=rss-c179cfb8173a------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*EASGUnL0x5iWytkKImfZgA.jpeg</url>
            <title>Stories by pxn on Medium</title>
            <link>https://medium.com/@nightspirit622?source=rss-c179cfb8173a------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 15 May 2026 18:36:42 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@nightspirit622/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[客觀評價 TailwindCSS]]></title>
            <link>https://medium.com/@nightspirit622/%E5%AE%A2%E8%A7%80%E8%A9%95%E5%83%B9-tailwindcss-af27581f6d9?source=rss-c179cfb8173a------2</link>
            <guid isPermaLink="false">https://medium.com/p/af27581f6d9</guid>
            <category><![CDATA[front-end-development]]></category>
            <category><![CDATA[tailwind-css]]></category>
            <dc:creator><![CDATA[pxn]]></dc:creator>
            <pubDate>Wed, 18 May 2022 16:40:26 GMT</pubDate>
            <atom:updated>2022-05-19T01:42:39.124Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mFNmwiH8FJ-xUN1RXj-o-w.jpeg" /></figure><blockquote>前端社群常見的宗教戰爭文： TailwindCSS根本邪魔歪道， Class根本不是這樣用的， 看了真他媽一肚子火－硬派本格 CSS/SCSS支持者</blockquote><p>會有這樣的言論，也許是你日常的工作流程中，不適合用這樣的框架，又或許是你沒有客觀的理解過 TailwindCSS 的優點所以體會不到它的魅力。</p><p>先說結論：如果你是一個團隊做 SAAS 產品，需要在統一的產品風格主題上面展開，並且使用 React 之類可以模塊化元件的前端框架，那麼 TailwindCSS 會是很值得導入的樣式解決方案。</p><h3>命名</h3><p>我發現對我來說，打斷心流狀態的往往是幫元件取名這件事，在傳統使用CSS/SCSS 上，我需要停下來花時間想一組元件還有其子元件的 class name 命名，檢查會不會跟已存在的元件衝突，多這一個步驟其實對開發效率上來講是拖累的。</p><p>誠然，你可以透過 Nested classes / BEM 之類的一些命名策略來讓這樣的步驟有一致性並減少命名碰撞，但在寫 JS 部分的元件時候已經要命名元件 / 命名變數，命名東命名西了，很多時候你 CSS 也只是把 JS 定義的名字改改文字格式複製貼上</p><p>例如這邊有個元件 AwesomeCard</p><pre>AwesomeCard -&gt; .awesomecard<br>AwesomeCardIcon -&gt; .awesomecard__icon<br>AwesomeCardBody -&gt; .awesomecard__body<br>AwesomeCardButton -&gt; .awesomecard__button</pre><p>說穿了其實是浪費生命的重複動作。</p><p>下面還有一種情境的命名我也常常“頓爹”打斷我思路</p><pre>&lt;div class=&quot;flex items-center justify-between px-3&quot;&gt;<br>  &lt;div class=&quot;flex items-center&quot;&gt;<br>    &lt;Icon/&gt;<br>    &lt;span class=&quot;ml-1&quot;&gt;Label&lt;/span&gt;<br>  &lt;/div&gt;<br>  &lt;Caret /&gt;<br>&lt;/div&gt;</pre><p>有時候你會需要一些額外的 div 搭配 flex 來做佈局，例如上面的程式碼中，我想要 Icon 跟 Label 兩者垂直置中，這一組元件要跟 Caret 垂直置中並分別對齊左右邊界，轉成 CSS 你可能就需要用上好幾個 classes</p><pre>&lt;div class=&quot;btn__container&quot;&gt;<br>  &lt;div class=&quot;btn__leftgroup&quot;&gt;<br>    &lt;div class=&quot;btn__icon&quot; /&gt;<br>    &lt;span class=&quot;btn__label&quot;&gt;Label&lt;/span&gt;<br>  &lt;/div&gt;<br>  &lt;div class=&quot;btn__caret&quot;/&gt;<br>&lt;/div&gt;</pre><p>要額外命名 btn__container btn__leftgroup 會讓我很煩躁，這些步驟如果能省起來個人是覺得能大幅提升開發效率。</p><h3>檔案之間的切換</h3><p>另外一個影響工作心流的是分開的 HTML/JS/CSS 檔案，雖然說 separation of concern 是軟體工作中很重要的一個概念，但前端實務上，三種檔案的耦合度極高，通常改一者就必須改另外兩者，頻繁的切換檔案其實很沒有效率。</p><p>以 React 框架來說，已經讓 Html 整合到 JSX 當中，當你習慣了這樣的工作模式，你會想更進一步的把樣式定義也納進來，這也是為什麼會有各種 css-in-js 的解決方案，TailwindCSS 在某種程度上也算是 css-in-js 的一種，各種元件狀態邏輯，例如說點選之後改變文字/背景顏色，可以透過 JSX 直接切換 className 來實現 (搭配 classnames 這樣的 npm module 更是如虎添翼)</p><h3>統一的風格樣式</h3><p>如果是 SAAS 產品，你會希望整個團隊有一致的調色盤， 而字體大小，間隔，常用寬高等維持有限度的選擇，讓你在元件佈局上能更好的對齊，TailwindCSS 這點已經幫你把最常見的 text/bg color、font-size、spacing 都提取出來，框架初始自帶的設定已經十分夠用，通常只需針對產品品牌色定義色盤，其他參數要客制修改擴充透過設定檔也十分方便。</p><p>你可能會說，SCSS裡面我也可以定義各種變數啊，的確，但變成你要自己設立一套參數規則或是參考某個框架範本 (MUI/AntD/Bootstrap) 來實作。</p><p>然後又回到一個懶人想省打字的問題上了，究竟是</p><pre>&lt;div class=&quot;bg-gray-100&quot;&gt;...&lt;/div&gt;</pre><p>還是</p><pre>&lt;div class=&quot;card&quot;&gt;&lt;/div&gt;</pre><pre>.card {<br>  background-color: $gray100<br>}</pre><p>對我來說，差別其實蠻明顯的。</p><h3>冗長的 class name</h3><pre>&lt;div class=&quot;w-96 bg-white shadow rounded flex items-center&quot;&gt;...&lt;/div&gt;</pre><p>通常反對 TailwindCSS 的正統硬派 CSS 使用者，最常攻擊的就是 Atomic utility classes 冗長的 class name，但這個透過 React 的元件封裝，其實根本不會是問題，通常你會把這些複雜度藏在可重複使用的元件中，實際上在開發的時候，程式碼往往是很清爽好讀的</p><p>例如底下一個 navigation 的元件，在 DOM tree 裡面長這樣</p><pre>&lt;ul class=&quot;flex&quot;&gt;<br>  &lt;li class=&quot;mr-6&quot;&gt;&lt;a class=&quot;text-blue-500 hover:text-blue-800&quot; href=&quot;#&quot;&gt;Link&lt;/a&gt;&lt;/li&gt;<br>  &lt;li class=&quot;mr-6&quot;&gt;&lt;a class=&quot;text-blue-500 hover:text-blue-800&quot; href=&quot;#&quot;&gt;Link&lt;/a&gt;&lt;/li&gt;<br>  &lt;li class=&quot;mr-6&quot;&gt;&lt;a class=&quot;text-blue-500 hover:text-blue-800&quot; href=&quot;#&quot;&gt;Link&lt;/a&gt;&lt;/li&gt;<br>&lt;/ul&gt;</pre><p>可以把 li 給抽出來寫成獨立的元件</p><pre>const NavItem = ({ href, children }) =&gt; &lt;li class=&quot;mr-6&quot;&gt;&lt;a className=&quot;text-blue-500 hover:text-blue-800&quot; href={href}&gt;{children}&lt;/a&gt;&lt;/li&gt;</pre><p>如此一來，你的JSX源碼就能整理成以下比較好讀的格式</p><pre>&lt;ul className=&quot;flex&quot;&gt;<br>  &lt;NavItem href=&quot;#&quot;&gt;Link&lt;/NavItem&gt;<br>  &lt;NavItem href=&quot;#&quot;&gt;Link&lt;/NavItem&gt;<br>  &lt;NavItem href=&quot;#&quot;&gt;Link&lt;/NavItem&gt;<br>&lt;/ul&gt;</pre><p>當然，前提是搭配 React 這樣可以輕易封裝組合元件的框架才能發揮最大功效，如果沒有用上類似框架，我也不能否認 atomic css 的 class 是真的很冗長難讀。</p><h3>檔案更小的 Stylesheet</h3><p>這點其實毋庸置疑，因為有很大部分的 Utility class 都是共用，沒有多餘命名，TailwindCSS 又自帶 Tree Shaking，不會產生沒用到的 class，整體的 CSS stylesheet 檔案可以壓到很小，瀏覽器載入超快速。</p><h3>結語</h3><p>老話一句，工具沒有好壞，只有適不適合，就我個人開發經驗，導入 TailwindCSS 在 Developer Experience 上是頗受團隊好評的，近年 TailwindCSS 的竄起，如果它沒有解決一些痛點，又何來這麼多人吹捧？</p><p>如果只是心理過不去，還抱持著寫傳統 CSS 方法的驕傲與矜持，沒有客觀的去理解新工具以及其適用的場景，我是覺得蠻可惜的，畢竟用得好的話，它真的能為你帶來更好的生產力以及效率，團隊協作上面也因為有共同的標準而能夠和諧的運作，何樂而不為？</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=af27581f6d9" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[零成本自帶CMS後台網站<二>：Blog]]></title>
            <link>https://medium.com/@nightspirit622/%E9%9B%B6%E6%88%90%E6%9C%AC%E8%87%AA%E5%B8%B6cms%E5%BE%8C%E5%8F%B0%E7%B6%B2%E7%AB%99-%E4%BA%8C-blog-8a39a235b785?source=rss-c179cfb8173a------2</link>
            <guid isPermaLink="false">https://medium.com/p/8a39a235b785</guid>
            <category><![CDATA[tailwind-css]]></category>
            <category><![CDATA[netlifycms]]></category>
            <category><![CDATA[netlify]]></category>
            <category><![CDATA[react]]></category>
            <dc:creator><![CDATA[pxn]]></dc:creator>
            <pubDate>Fri, 18 Feb 2022 22:59:09 GMT</pubDate>
            <atom:updated>2022-02-18T22:59:09.432Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lHZAWDTal6KqlwZcDNWJ-Q.png" /></figure><p>說好不富奸，打鐵趁熱把 Blog 也生出來，如果還沒看過之前 <a href="https://medium.com/@nightspirit622/%E9%9B%B6%E6%88%90%E6%9C%AC%E8%87%AA%E5%B8%B6cms%E5%BE%8C%E5%8F%B0%E7%B6%B2%E7%AB%99-react-tailwindcss-netlify-cms-4bd787fd63b5">零成本自帶CMS後台網站：React + Tailwindcss + Netlify CMS</a> 可以先回頭看看，這次 Blog 會在之前的基礎上繼續衍生下去</p><p>雖然說只是簡單弄弄，但 Blog 基本功能該有都還是都要有</p><ol><li>文章列表頁面</li><li>文章本體頁面</li><li>Markdown 文本</li><li>標籤</li><li>搜尋</li></ol><p>老樣子，本篇預設讀者為 <strong>React 中/高階開發者</strong>﹐請搭配下列 Repo 一起服用：</p><p><a href="https://github.com/nightspirit/cra-tailwindcss-netlifycms">https://github.com/nightspirit/cra-tailwindcss-netlifycms</a><br>(MIT 授權，歡迎大家進行各種魔改 🛠)</p><p>Live Demo：<a href="https://cra-tailwindcss-netlifycms.netlify.app/blog">https://cra-tailwindcss-netlifycms.netlify.app/blog</a></p><p><a href="https://app.netlify.com/start/deploy?repository=https://github.com/nightspirit/cra-tailwindcss-netlifycms">一鍵部署至Netlify</a> (如果想要玩看看CMS後台可點這個)</p><h4>現成免費模板</h4><p>與上次一樣，我找了一個免費共享的 Tailwind CSS base 極簡 Blog 單頁作為基礎模板</p><p><a href="https://github.com/tailwindtoolbox/Minimal-Blog">GitHub - tailwindtoolbox/Minimal-Blog: Tailwind CSS Starter Template - Minimal Blog</a></p><p>改法步驟跟上次差不多不贅述，模板裡面的閱讀進度條我覺得挺不賴因此有保留下來，通常這種原生 javascript 操作 DOM element / addEventListener 的，要轉移到 react app 當中就是用 useEffect 去包，元件 mounted 以後執行一次</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JSoXJwGVjVIMU3SOUdXyhw.png" /><figcaption>有訂閱 event 的話記得回傳 clean up callback 來 removeEventListener 避免 memory leak</figcaption></figure><p>另外文本我打算以 markdown 為主要編寫格式，在模板裡面文章區塊 p / a / li / pre / code 這些 tag 是有用上 tailwind 的 class，解法有兩種，一種是拉出來放到 css 檔，第二種是看用哪個函式庫轉檔，以 react-remark 為例，是可以在 rehypeReactOptions 定義這些 tag 怎麼 render ，藉此加入自定義的 class</p><p>不過我不想搞得那麼複雜，直接把樣式拉出來放到 index.css 裡簡單點， tailwind 其實蠻彈性的，除了直接在元件上用 utility class 之外，也可以利用 @apply 在 css 檔案裡面引用</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dWcW44kWEufLJaDH2Ay4Lg.png" /><figcaption>.post-body 裡的基礎 html tag 現在會套用上這些樣式</figcaption></figure><p>Markdown 轉 react component 則是用了現成的 react-remark 套件，它有提供元件或 hook 兩種方式來轉檔，這邊用元件去包 markdown 文本即可</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/686/1*jMfVDtiJmqt543LQeam6WQ.png" /></figure><h4>文章列表頁</h4><p>文章列表我沒有找別的模板，基本上是用了文章模板內的設計元素拼湊出來，每個 entry 會顯示 title 、 date 、 description 、 tags，另外我打算用同樣的模板來展示<strong>標籤文章列表</strong>、<strong>搜尋文章結果</strong>，就結果來說這些頁面呈現方式大同小異所以重複利用列表元件即可，差別只在 tag / search 要多額外秀一個 header 來告知使用者目前的使用情境</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/796/1*5KfUL9fWvINBPfkp37r3lw.png" /><figcaption>標籤文章列表</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/792/1*_c9Y9hHHdvgip2ypd1ZGjQ.png" /><figcaption>搜尋結果列表</figcaption></figure><h4><strong>定義 Blog Post 資料</strong></h4><p>與上次定義 Home page collection 一樣，這次我們要來定義 Post collection 好讓我們可以在 CMS 後台加入新 post entry</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fNjt0JJMjqivhbq9TGhm8w.png" /><figcaption>src/cms/post.js</figcaption></figure><p>與上次 home page collection 不同的點在於 home page 是 single file collection 而 post 是 folder collection，換言之，一個是 singleton，另一個則是會有 multiple entities，每次新增一個 post，cms 就會創建一個 json 檔放在 public/data/posts 底下，檔名會是 ${slug}.json</p><p>通常一篇 blog post 會有以下的 fields</p><ul><li>slug — 文章在 url 中的slug，也是文章 primary identifier 兼 json 檔名</li><li>status — 用來決定文章是 public / unlisted / archive，可以在後續做列表 index 的時候應用</li><li>title — 文章標題</li><li>date — 時間戳記</li><li>body — 文章文本</li><li>description — 文章短文本，用在列表與SEO</li><li>featureImage — 文章縮圖，可用在列表與SEO (只是預設但沒實際應用到)</li></ul><p>創建好之後我們把這個 collection 定義放進主 config 當中</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/830/1*b3Z3u1Mp-Zo6OKExRRRF_w.png" /><figcaption>src/cms/index.js</figcaption></figure><p>接著到後台創建兩三個 dummy post entries 方便我們後續串接動作</p><h4>資料串接</h4><p>現在我們有了 json 資料，與之前 Home page withData相同，我寫了一個類似的 withPost wrapper 來撈資料</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RnTTFWE8a5w08Q9pBMFGNw.png" /><figcaption>src/utils/withPost.js</figcaption></figure><p>由於 slug 是基於使用者進入的 url ，而實際上使用者可以在 url 裡面亂打網址，所以必須要針對撈不到 json 的情境做 error handling，這邊就簡單暴力的設成要是撈不到文章 json 就跳轉回 /blog 列表頁。此外，當 slug 變動的時候，也要重新觸發 fetch 的動作，所以要把 slug 納入 useFetch 第二個參數的 data dependency</p><p>路由方面我是這樣規劃</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gOU1IBs2YWdy9OoP2zL27A.png" /><figcaption>src/routes/Blog/index.js</figcaption></figure><ul><li>/blog 以及 /blog?t=tag /blog?q=searchterm render List 列表頁</li><li>/blog/:slug render Post 文章頁</li></ul><p>另外我把兩頁共用的 Header 拉出來放到 Layout 裡，並且在 Header 加了一個搜尋框，這樣切換路徑的時候只會重新 render 下面的內容部分</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/932/1*XG_KQJJbaRg9wzHgYqLWUQ.png" /></figure><p>回到 Post 的資料串接，由於 withPost post data 會以 data props 傳入 Post component，串接沒啥難度，日期格式我是靠 luxon 來 formating</p><p>值得提的是，我把原本的 Post 分拆了數個子元件：</p><ul><li>PostBody — 內容文本，會用在後台 CMS preview</li><li>Tags — 標籤列，會與文章列表共用</li><li>Footer — 包含上一篇文章與下一篇文章的導覽連結</li></ul><p>拆開原因除了元件共用性，最大的原因是為了 CMS preview，因為在 Preview mode 並沒有 react-router 的 BrowserProvider，所以 router 相關的元件或 hooks 會報錯（Link / useLocation / useParams / useNavigate …），因此最好的方式是把這個文本解離出來並且確定當中沒有任何 react-router 相依</p><p>既然提到 CMS preview 就順便把這邊也帶過，與上次相同，我利用 withEntry 這個 wrapper 來把 entry data 轉成同樣 data props 接口，但這次我只 import 子元件 PostBody 而非整個 Post component （PostBody 我有namespace export）</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1002/1*1saZJS0jHVk29it8VPKxgg.png" /></figure><p>在 CMS 後台看到的編輯頁如下， 可以看到 Header / Tags / Footer 這些就被去除了，但實際上不影響編輯工作</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MldO9Wsj4efhkEU5JmT-lw.png" /><figcaption>這邊其實還踩到另一個雷，留待到後面的篇章一併說明 😅</figcaption></figure><h4>文章摘要的資料串接</h4><p>列表頁不像文章頁有 Post json 的資料來源，那麼我們要如何顯示呢？一般來說網站如果有 backend， 文章是存在資料庫，通常會有 API query db 然後回傳 query 結果，但我們只是個靜態網站，而且我這個專案最高指導原則是能用最少資源就用最少資源，能當免費仔就絕不花錢</p><p>不用 backend / DB 或其他後端服務的前提下，一種可行的方式就是在 build time 的時候掃一遍所有文章 json 並且摘要輸出一份 index json，然後列表頁以這份 index json 為依歸，做各種切換標籤，搜尋等工作。如果文章量沒有破百破千，這個工作量是非常輕量的並不會有什麼效能問題 （如果文章數量級到一定規模其實就不會考慮用 Netlify CMS 老實說）</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rRnMfF4FGdVEOnzKCXJILA.png" /><figcaption>scripts/build-posts-index</figcaption></figure><p>我寫了一個簡單的 script 用 fs 讀 /public/data/posts 資料夾，然後走一遍JSON 檔把列表頁會用到的 fields 抓出來 map 到一個新 array，另外為了加速用標籤選定文章，我針對標籤另外產出一個 tagIndex （對應到每個 entry 在 entries array 的 index #），這樣 client side 過濾標籤文章會比較有效率</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VllTPqtlfVyMVTL_xkzoRA.png" /><figcaption>產出的摘要檔大概像這樣的資料結構</figcaption></figure><p>這份 index 用在四個地方</p><ul><li>文章列表 full list</li><li>當使用者選定標籤，透過 tabIndex 可以快速 map 出該標籤的文章列表</li><li>當使用者搜尋，我在 client side 用 <a href="https://fusejs.io/">fuse.js</a> 做 fuzzy search 並顯示結果</li><li>Post 中 Footer 上一篇/下一篇的文章連結</li></ul><p>載入 index 資料的時間點是在 mount/blog route 元件時 ，index 資料會向下傳進 List 與 Post 。這份資料基本上如果沒新增文章或改動是不會變的，而增添或修改文章在 Netlify CMS 的使用情境都會觸發 rebuild / redeploy，只要在 build process 有跑這個 script，就可以確保 index 永遠跟文章 json 同步</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-zbjF9c1VfjuztsuVx2Rvw.png" /></figure><p>在 package.json 裡面我新增了 build-posts-index 指令，並且用上了 concurrently 加速 build process，記得先前我們要幫 CMS 另外 build 一份 css 供 preview 使用嗎？這個步驟可以跟 build-posts-index 平行處理來加速。至於原本 CRA 的 build process 我放在後面跑是必須確定前兩者的 output 都進去 public folder 了，才能確保這些 output 會被帶到最後的 build folder，基於這個理由 CRA build process 是不能用 concurrently 一起跑的</p><h4>Netlify CMS 無法使用 react hooks 的雷</h4><p>在上一篇當中我沒有碰到這個雷是因為 Home page 實在太簡單了，我甚至連半個 hook 都沒用上，然而到了 Blog 以後我就吃了個鱉，一開始載入 Post preview 的時候整個右邊預覽列是報錯無法正常 render 的</p><p>本來想說是因為沒有 react-router 的 BrowserRouter ，但隨後發現其實問題不只那樣，主要是 netlify-cms 用的 react 是打包內建的，跟我們的 app 用的不是同一個 instance（我們的 react 來自於本地安裝的 node_modules ），所以當我們的元件傳進 netlify-cms 有 hooks 的時候是因為 react 不是同一個 instance 出錯</p><p><a href="https://www.netlifycms.org/blog/2019/07/netlify-cms-gatsby-plugin-4-0-0">React Hooks support in Netlify CMS (and the Gatsby plugin)</a></p><p>官方有解法，重新包了一份解離了 react 跟 react-dom 的 netlify-cms-app，用法可直接從 cdn 載入，或是透過 npm install 在 webpack bundle 的時候也可以被一併納入</p><p>最後我選擇了透過 cdn 的方式，我發現 netlify-cms-app 奇肥無比！如果納入 webpack build process，chuck 會高達1.8M，而且 build time 會將近三分半，對於只有 free quota 300 mins 的免費仔來說這可不是什麼好消息</p><p>為了達成從 cdn 加載，要做以下改動</p><p>首先是到 public/admin/index.html 把 react / react-dom / netlify-cms-app 透過 script tag 從 cdn 載入</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uSXv_zGHcaykEiZgCQ3NXA.png" /><figcaption>public/admin/index.html</figcaption></figure><p>再來是在 config-overrides.js 告訴 webpack 這些 module 會從外部導入</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/830/1*aiirxQFZsSh_qQxliv6WNw.png" /><figcaption>config-overrides.js</figcaption></figure><p>因為 react / react-dom 變成 external modules，所以原本前台的 index.html 我們也得加上 react /react-dom 的 script</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FsiIn_aLqnJ1Kso8vJ5QpQ.png" /><figcaption>public/index.html</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/525/1*vhF-Gz6A1yS2vJxiQ4-eMg.png" /></figure><p>如此一來，整個 bundle size 可以壓到非常小，並且可以在一分鐘內完成 build process，每月三百分鐘絕對夠用！</p><blockquote>阿這不就 wordpress，也沒啥好稀奇的啊？</blockquote><p>上篇分享後有網友給予了這樣的指教，就功能性來說的確是 wordpress，硬要講差異的話，wordpress 是建構在 php + mysql 之上，並非一個純前端的解決方案，我認為我提的解決方案對一個現代前端來說有下列優勢</p><ol><li>Hosting static site 有很多免費方案，但要 Hosting php server + mysql 免費的並沒有那麼多，deployment 大概也不會像 Netlify 如此無腦，純前端 project 啟動成本非常低</li><li>語言只用上前端熟習的 html/js/css ，並沒有任何的 backend 語言，你甚至不用懂 mysql，要魔改或加功能，對純前端來說掌握度比較高學習曲線比較低</li></ol><p>個人網頁跟 blog 其實只是最皮毛的功能，這樣做法核心精神是 Jamstack，你愛做甚麼都可以，反正啟動成本低 + 語言掌握度高，技術門檻與資源要求沒有以前 monolith app 成本那麼高，這是我想說明的</p><p>在實務上我前東家的 marketing site 也是用 Gatsby + Netlify cms 這樣搭起來的，行銷 team 的人可以直接用 Netlify CMS 後台改文本內容發新聞稿，而前端要修改或增添網站功能的時候可以直接動整個 repo (包含文本內容的部分都在一起，要改 collection definition 的時候就很方便，平常如果沒動到 data 那塊也不會有什麼 conflicts)，開發起來比起之前用 contentful cms 更直接，我們只付了 Level 1 的使用費就撐起整個 marketing site，把 CI/CD、hosting、scaling 這些問題都轉嫁外包出去，只把直接影響公司業務跟需要客製化的部分留下來 (CMS / front-end 外皮)</p><blockquote>多想兩分鐘，其實你真的不需要 wordpress 這樣的 monolith app，尤其是那些接外包做做地方小公司網站的，殺雞焉用牛刀？</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8a39a235b785" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[零成本自帶CMS後台網站：React + Tailwindcss + Netlify CMS]]></title>
            <link>https://medium.com/@nightspirit622/%E9%9B%B6%E6%88%90%E6%9C%AC%E8%87%AA%E5%B8%B6cms%E5%BE%8C%E5%8F%B0%E7%B6%B2%E7%AB%99-react-tailwindcss-netlify-cms-4bd787fd63b5?source=rss-c179cfb8173a------2</link>
            <guid isPermaLink="false">https://medium.com/p/4bd787fd63b5</guid>
            <category><![CDATA[react]]></category>
            <category><![CDATA[netlify]]></category>
            <category><![CDATA[tailwind-css]]></category>
            <category><![CDATA[netlifycms]]></category>
            <dc:creator><![CDATA[pxn]]></dc:creator>
            <pubDate>Tue, 08 Feb 2022 02:15:07 GMT</pubDate>
            <atom:updated>2022-02-20T08:07:06.441Z</atom:updated>
            <content:encoded><![CDATA[<p>零成本自帶CMS後台網站：React + Tailwindcss + Netlify CMS</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cZUp7DyqayF6m21xVij3LA.png" /></figure><blockquote>從零打造一個網站到上線有多難？ 其實只要你想全網路都會幫你</blockquote><h4>前言</h4><p>去年跟朋友聊天談到﹐現代的前端真的很幸福﹐要打造部署一個 static site 現成工具太多了﹐要起步根本飛快，而且流量不大的話<strong>基本免費。</strong>以 Netlify 為例﹐框架函式庫拼拼湊湊， Code 丟上 Github﹐只要在 Netlify 簡單設定一下﹐馬上就有現成的 CI/CD 環境﹐backend ? devops? 通通免！部署只要 git push﹐Netlify 就自動幫你 build / publish 甚至 Custom domain TLS 也幫你在 Let’s Encrypt 搞定﹐Scaling / CDN 通通不用煩惱幫你一條龍服務包到好</p><p><strong>特別適合</strong></p><ol><li>純前端 Proof of concept</li><li>個人網頁作品集</li><li>輕型網站 (大型網站也不是不行, 有錢什麼事都好辦)</li></ol><p>去年弄了一個潮潮 <strong>.dev</strong> 網域本來打算搞個人網頁+ blog﹐沒想到放在那邊一年就這樣過去了﹐實在有夠浪費錢 😨</p><p>2022 開春大吉﹐今天就來從零到有建構一個有基本 CMS 後台功能的個人網站﹐並部署到 Netlify，並展示一下之前說的：現代前端有多幸福</p><p>本篇預設讀者為 <strong>React 中/高階開發者</strong>﹐文章會比較多著墨在這個網站的構建步驟以及所遇上的地雷還有我的解法﹐文章請搭配下列 Repo 一起服用：</p><p><a href="https://github.com/nightspirit/cra-tailwindcss-netlifycms">https://github.com/nightspirit/cra-tailwindcss-netlifycms</a><br>(MIT 授權，歡迎大家進行各種魔改 🛠)</p><p>Live Demo：<a href="https://cra-tailwindcss-netlifycms.netlify.app/">https://cra-tailwindcss-netlifycms.netlify.app/</a></p><p><a href="https://app.netlify.com/start/deploy?repository=https://github.com/nightspirit/cra-tailwindcss-netlifycms">一鍵部署至Netlify</a> (如果想要玩看看CMS後台可點這個)</p><h4>React 的部分</h4><p>開局先用 create-react-app 創建一個專案﹐這邊選用 CRA 的理由是因為他最陽春﹐一來是我構思的網站其實並不複雜﹐殺雞用不到牛刀﹐二來是展示這個概念我並不想引用太複雜的框架跟 tooling 混淆視聽﹐但你要選用 next.js / gatsby 或任何框架都可以﹐甚至會跟 Netlify CMS 相性會更好﹐現成資源插件也更多一些﹐實戰上我後面兩個框架都有用過﹐概念都是成立的</p><p>CRA 陽春歸陽春﹐但在這個使用情境也是有好處﹐因為 Netlify free tier 一個月只給你 300 分鐘的 build time﹐用比較複雜的框架 build time 會比較長﹐我這個專案大概 build 一分鐘左右而已﹐免費扣打很難用完 (我就省 👌)</p><p>預設讀者大致熟習 create-react-app﹐這邊就不贅述 setup﹐ 請自行參閱官方文件 <a href="https://create-react-app.dev/docs/getting-started">https://create-react-app.dev/docs/getting-started</a></p><p>大致講講專案開了以後我做了哪些改動</p><ol><li>個人習慣 package.json 裡的 dependencies 與 production 無關者我通通先移到 devDependencies</li><li>加了 jsconfig.json 讓我可以用絕對路徑 import (<a href="https://dev.to/mr_frontend/absolute-imports-in-create-react-app-3ge8">https://dev.to/mr_frontend/absolute-imports-in-create-react-app-3ge8</a>)</li><li>因為不打算 eject CRA 但會動到一些 webpack plugins﹐所以使用 react-app-rewired 來 override﹐具體改動會在後面文章提及</li><li>把初始代碼內用不到的東西清一清 (App.js、App.test.js、logo.svg、index.css、Web Vitals 等)</li></ol><p>Routing 的解決方案﹐選擇了主流的 react-router﹐不過最新版 V6 跟 V5 的 API 有點差異﹐大家可能要注意一下。</p><p>我的專案檔案放置架構大致如下 (這邊只提 src 底下的檔案)</p><ul><li><strong>assets</strong>: 靜態資源﹐主要放 SVG icon</li><li><strong>cms</strong>: Netlify CMS 相關代碼 (後述)</li><li><strong>components</strong>: Shared react component</li><li><strong>routes</strong>: Route/Page component 還有主要路由﹐Page 相關的子元件如果沒共用的話基本上是也放在該 Route 資料夾底下 (e.g. Home 底下的子元件 Socials)</li><li><strong>utils</strong>: 公用函式﹐包含 hooks 或是一些 Wrapper component function</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1020/1*g2RkzTFxEpXkmhgUCD0-9g.png" /><figcaption>src/App.js</figcaption></figure><p>大家可以先從 App.js 開始讀起﹐起手是很常見的設定：包了兩個庫的 State Provider﹐ use-http 的 Provider 可以先跳過等等會後面會提到﹐然後是 react-router 的 BrowserRouter</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*acO__l2PBwY5jPzWp_xVLQ.png" /><figcaption>src/routes/index.js</figcaption></figure><p>接著我們看到 Routes元件部分﹐這邊是設定路由的主要區塊﹐我設計中網站其實只有一個 Home 主頁跟 Blog 部分所以並不複雜﹐我寫了一個簡單的 wrapper function lazy 與 withData 來包覆各個 Route 元件﹐lazy 就是官方利用 React.lazy + Suspense 的來做 lazy load/code splitting (<a href="https://reactjs.org/docs/code-splitting.html">https://reactjs.org/docs/code-splitting.html</a>) 衍生出來的 wrapper﹐而 withData 我在後面處理資料的部分一併說明</p><p>(P.S. Blog 部分可能要下集不然這篇會寫太長)</p><h4>Tailwind CSS + 現成免費模板</h4><p>TailwindCSS 的好，用過就回不去了！基本上我自己玩或是工作上條件許可的案子 Styling 肯定秒選 TailwindCSS 作為主要解決方案；而身為一個前端碼農，設計什麼我不太明白，術業有專攻，漂亮的門面還是交給設計師來</p><p>我找了一個免費共享的 Tailwind CSS base 極簡 Profile Card 單頁作為我的 Homepage</p><p><a href="https://github.com/tailwindtoolbox/Profile-Card">GitHub - tailwindtoolbox/Profile-Card: Tailwind CSS Starter Template - Profile Card (Single page website for your profile/links)</a></p><p>整合進案子裡也不難﹐Html 拉來改成 Home component 以後</p><ol><li>class -&gt; className﹐還有一些 attributes 要改成 React 習慣的 camelCase</li><li>SVG icon 的部分我獨立拉出來放到 <strong>assets</strong> 資料夾﹐自從 React 開始原生支持 SVG import 以後我就不太用第三方插件了﹐但我自己習慣寫一個 components/Icons 來統整所有 App 裡會用到的 SVG Icon 元件﹐另外我添加了 Medium/Linkedin 等原始模板沒有的 Icon</li><li>接著我把模板裡面的內容﹐例如背景圖、個人頭像、姓名、職業頭銜、所在地，內容文本等都 tokenize 設為變數﹐方便等等串接資料。溫馨提示：在還沒有完成端對端資料串接前﹐可以在 Home 裡寫一個 Fixture 餵給 props.data</li><li>本來 Social Links 的部分設計師 html template 裡面是複製貼上﹐這種代碼太濕了﹐身為工程師不重構一下渾身不對勁﹐所以包成子元件跑迴圈來 render</li><li>本來的模板有用 <a href="https://atomiks.github.io/tippyjs/">Tippy</a> 插件﹐這邊為求乾淨先拿掉了</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qPtvkCpu9PieuxggFF9Irw.png" /><figcaption>src/routes/Home/index.js</figcaption></figure><blockquote>所以說那個醬汁… 不﹐所以那個 tailwindcss 呢?</blockquote><p>做完上述步驟會發現模板怎麼都跑掉﹐Sorry 因為根本忘了裝 Tailwind 😆</p><p>CRA 5 以後很多內部 Tooling 都更新了也兼容 Tailwind，設置就照官方步驟來無難度 <a href="https://tailwindcss.com/docs/guides/create-react-app">https://tailwindcss.com/docs/guides/create-react-app</a> ，裝完之後顯示應該就正常了</p><h4>資料串接</h4><p>一般 Netlify CMS 會把 content data 存成 markdown，但我這邊打算用更簡單暴力的方式存成 JSON 並放在 public/data/* 底下，因為 CRA 的 Public 資料夾默認對外開放，在 Client 端我可以直接 fetch /data/* 來獲得資料，而 JSON 拉下來後可以直接使用，如此便省去了轉換 markdown 的額外步驟</p><p>如果你是使用 next.js 或是 gatsby 的話，框架內部倒是有幫你做 markdown 轉換或是有插件可使用，以 gatsby 為例，在 build time 它會幫你把 markdown 轉成 graphql api 讓你在元件間使用</p><p>而我第二個預先假設是所有 Page 都會有一個匹配的 CMS data entry，例如：</p><pre>/home &gt; /data/page/home<br>/blog/* &gt; /data/page/blog/*</pre><p>以這個邏輯寫了一個 withData 的 wrapper</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7an3rt6AlMMuiCPHN4jQTA.png" /><figcaption>src/utils/withData.js</figcaption></figure><p>這樣在切換路由的時候，Client 會根據頁面 pathname 去匹配 data 路徑撈 JSON，再加載該分頁元件並將 JSON data 透過 props.data 傳入</p><p>基於能坐我就不站著，有現成工具就不自己刻原則，這邊用上常見的 use-http 來做 <strong>ajax request + caching</strong>，前面 App.js 提到的 Provider 我有設置 url=&quot;/data&quot; 前綴，在後續 use-http 使用上 dataUrl 記得省去 /data</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*elPC_bx7SiJIpPOJftYjXA.png" /><figcaption>public/data/page/home.js</figcaption></figure><p>把 Home 裡剛剛設的 fixture data 轉換成 JSON 放到指定的資料路徑看看設置上有沒有錯誤，理論上進行到這邊不會有什麼大問題，有問題的話就仔細檢查看看路徑變數之類的有沒有打錯字</p><p>到這邊我們已經有了一個網站的初步構型，data &amp; presentation 切開，如果不導入 CMS 後台的話，純粹改改變數也堪用，如不喜歡後面我介紹的 Netlify CMS 你也可以考慮串其他 Headless CMS 方案或其他後端服務</p><h4>Netlify CMS</h4><p>為什麼選擇 Netlify CMS 作為我的 Headless CMS？</p><ol><li>因為我要把這個站部署在 Netlify，用本家工具比較直接</li><li>Netlify CMS 基本上後台更動內容是透過 git-gateway 發 commit 去 git repo，然後觸發 rebuild/deploy，簡單直白沒有其他後端資源需求，對於不是很頻繁需要更新內容的網站非常適合 (300分免費build time我就省)</li><li>因為 content data 也存在 repo 中，開發時候可以直接修改 content data 然後 Push commit，跟你在線上後台做等效，這樣你不用分開在本地還有線上服務改來改去，如果你偏好使用 GUI 修改內容的話，也可以在 Local 用 netlify-cms-proxy-server 來進入 CMS 後台</li><li>不用自己寫 CMS 後台，而且CMS系統有足夠空間讓我擴充功能，對小網站已堪用</li></ol><p>首先參照官方教學 (<a href="https://www.netlifycms.org/docs/add-to-your-site/">https://www.netlifycms.org/docs/add-to-your-site/</a>) 在 public 底下開一個 admin/index.html 作為 CMS 後台的 entry point</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LXntjlbBtMhgAgMQnGEWwQ.png" /><figcaption>public/admin/index.html</figcaption></figure><p>這邊我做了個小改動，因為我不喜歡官方利用 config.yml 的設置方式，我發現用 JS object 來導入 config 會比較方便後續做 collections 管理，所以我這邊把 auto init 給關了採用 manual init</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7uks0IcMBz4ALcjoWntvUg.png" /><figcaption>src/cms/index.js</figcaption></figure><p>在 src 下我開了 <strong>cms</strong> 資料夾專門拿來放跟 CMS 後台相關的代碼，主要是 collections definition 跟集成 config 並初始化，我之所以偏好 JS object 勝過 yml 的理由是可以把 collection definition 拆開成個別的檔案，上面程式碼你會看到 Home page collection 我是分開存在 ./pages/home 裡然後用 require import 進來的</p><p>Netlify CMS GUI 可以讓你上傳圖像，像我的 profile/bakckground picture 就放在 public/upload 的 資料夾內，要置換的話透過後台很方便</p><p><strong>media_folder</strong>：上傳圖像影音檔案的目的地資料夾<br><strong>public_folder</strong>：上傳圖像影音檔案的資料夾的公開路徑</p><p>題外話：上傳檔案如果多又大的話建議考慮使用 Netlify Large Media ，這樣 repo 比較不那麼臃腫<br>(<a href="https://docs.netlify.com/large-media/overview/">https://docs.netlify.com/large-media/overview/</a>)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Nqw4QGwfZg4VdV9n5gu7OQ.png" /><figcaption>src/cms/pages/home.js</figcaption></figure><p>Collection definition 可以參照官方說明來定義 (<a href="https://www.netlifycms.org/docs/collection-types/">https://www.netlifycms.org/docs/collection-types/</a>) 你可以把它想成是資料庫 schema 或是 typescript 的 type，定義了這個 collection 所含的變數 fields 以及所對應的 CMS widget (default 為 String)</p><p>fields 有 Object/List widget 支援巢狀資料結構或是重複資料列表，像 CTA 我就定義成 Object，而 SOCIALS 則是 List，這邊因為定義都是 JS object 所以可以很輕易地 code separate &amp; compose 讓代碼的可讀性更好 (大勝單一 config.yml)</p><p>現在我們有 create-react-app (/) 跟 Netlify CMS (/admin) 兩個 entry point，但 CRA 只會默認 public/index.html 和 src/index.js ，要怎麼在 build time 的時候也順便打包 src/cms 底下的代碼呢？ 這時候就要用到我前面說的 react-app-rewired 了</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DSJ8C0xSWxeFPR5unwKiWg.png" /><figcaption>react-app-rewired 默認 config-override.js 為覆寫檔</figcaption></figure><p>這邊用上了 react-app-rewire-multiple-entry 可以定義除了 index.html/index.js 之外的 app entry point ，但因為此插件版本還沒跟上最新 CRA 5 有 bug，所幸在 github issue 串裡找到別人分享的 hack solution 來讓舊 plugin 能兼容最新的 CRA 5 webpack.config</p><p>如此一來就可以透過 /admin 來訪問 CMS 後台了，這邊另一個要點是，如果還沒部署到 Netlify，只想單純在本地操作的話，你必須先在 config 裡設置local_backend: true，並且在本地跑個netlify-cms-proxy-server， 請參照 <a href="https://www.netlifycms.org/docs/beta-features/">https://www.netlifycms.org/docs/beta-features/</a> “<strong><em>Working with a Local Git Repository”</em></strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/699/1*94o315UNEfDyTSScrCfLpQ.png" /><figcaption>netlify-cms-proxy-server 有在 port:8081 啟動的話, localhost:3000/admin 登入頁面可按 Login直接進入</figcaption></figure><p>再來是 Preview 的設置，Netlify CMS 後台是支援 Live Preview 的，只要你註冊了該 entry 的 Preview Component，在你更動 Content 內容時會直接反映在元件上</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Eoz-4w8h2rMN1DBEgqGrHw.png" /><figcaption>Live Preview editing</figcaption></figure><p>理論上 Preview Component 等同於 Page Component ，以這邊為例，我們要把內容反映在 Home Component 上，但 Netlify CMS 的 preview data 是用 props.entry 來傳入，要得到實際的值還得跑 entry.get(&quot;data&quot;).toJS() ，為了統一資料接口所以我寫了一個類似 withData 的 wrapper withEntry</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nkq76x2Nqp6RgC8KqIhoFA.png" /><figcaption>src/utils/withEntry.js</figcaption></figure><p>這樣 Home 就能同樣透過 props.data 接資料來達到實時預覽功能</p><pre>// 載入 Home 元件<br>import Home from &quot;routes/Home&quot;;</pre><pre>// 包上 withEntry 可以吃到 entry 傳來的 data<br>const HomePreview = withEntry(Home);</pre><pre>// 註冊 HomePreview 元件到 CMS instance 來達到 live preview<br>CMS.registerPreviewTemplate(&quot;home&quot;, HomePreview);</pre><p>第二個會碰到的雷是，奇怪，怎麼預覽又跑版了？原因是我們的前台頁面是在 index.js 裡載入 import index.css 的，但預覽 iframe 裡面並沒有這個 tailwind 所產生的 css 檔案</p><p>「那簡單啊！我在 src/cms/index.js 底下也 import index.css 就好」，然而事情並不是憨人我想的那麽單純，因為預覽是 iframe，吃不到你 import 進 src/cms/index.js 的樣式，事實上那樣式只會影響到左半邊編輯器跟後台外觀，而 Netlify CMS 也不是基於 Tailwind CSS 開發的，所以根本毫無相干</p><p>預覽部分的 Netlify CMS 其實提供了一個 API 讓你載入 css stylesheet</p><pre>CMS.registerPreviewStyle(&quot;style url&quot;);</pre><p>不過這 Method 並不吃 inline style 只吃 remote url，當然高級點作法可能是從 build tool chain 裡面下手，在 build process 的時候看看可不可以 clone static/index.[hash].css 或是動態註冊這個 css url 給 CMS ，webpack dev server 部分也要另外想辦法串起來才能開發</p><p>左思右想，這實在太麻煩了，為了這小案子實在不想弄那麼大工程，所以我決定再次用簡單暴力的偷雞方式，另外跑 tailwindcss cli command 產出一個 index.css 然後放在 public/admin 底下給 CMS 專門註冊用</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*prbyW2V8XOrm5VKYhxRXwA.png" /><figcaption>package.json scripts 關於 build 的部分</figcaption></figure><p>開發時候可以跑 npm run build-css -- --watch 但其實也可以在處理後台之前視情況手動跑一次，基本上這 css 檔案只有在你動到元件 className / tailwind config / index.css 才有需要更新，如果你根本沒動到任何樣式的話，上次產生的 css 舊檔就夠了</p><p>而正式 build 的時候不管怎樣我都會 rebuild 這個 css 檔確保是最新版本，接著才執行一般的 build script</p><p>未來如果發現有更理想的方式會再更新這塊，各位看官大大們若發現有什麼好方法可以直接內建到 CRA build process 也歡迎留言告知</p><h4>Let’s go production</h4><p>到目前為止，我們建置了一個網站雛形內建CMS後台並可以擴充更多單頁，應該可以在本地跑這個APP不成問題，讓我們把這個網站部署到線上吧！</p><p>同樣的問題再來一遍，為什麼我選擇 Netlify</p><ol><li>超級無腦，一站式全包服務免煩惱</li><li>流量不大、內容不頻繁更動的小網站基本上免錢</li><li>基本 user access control 可用內建 identity (等下CMS就會用到)</li><li>要串其他後端服務有 Netlify functions 可用 (serverless computing)，特別適合作為 api proxy 。例如你有第三方服務的 api access key 不能暴露給 Client ，就可以寫 functions 橋接， 對 Node express 開發有經驗者應該對 functions 不會感到陌生，即使對一般純前端來說，還是熟悉的 JS 最對味</li></ol><p>這個部署流程中，Push code to master === build and deploy，這邊假設你 Github 已經有個 remote repo，Netlify 你已經申請好帳號了…</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/851/1*N66edhvlYmCY5q4Oa8hC_w.png" /></figure><p>登入 Netlify dashboard，選擇 Add new site -&gt; import an existing project</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/716/1*rfpfac2qAgIx0zk6ytRanw.png" /></figure><p>選擇你的 Git provider，這邊以 GitHub 為例，接著會跳出視窗要你登入Github 並給予權限，都做完以後應該會跳下一個步驟 (給過權限以後下次就不用再給了)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/770/1*ju5oyof3XipaIrWsZ0wxcA.png" /></figure><p>找到你的 remote repo 點選進下一步</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/727/1*3RY6SAB1F3y2sdIV6--CcA.png" /></figure><p>你如果在 repo 裡有透過 netlify.toml 來設定的 settings 的話，可以直接點選 Deploy site，不然也可以透過 GUI 來設置像 build command / env variables / publish directory 之類的參數</p><p>溫馨提示： 因爲 Netlify build env 會設 process.env.CI = true 導致 CRA build 不過，所以官方討論區給的折衷辦法是在 Build command 前面加 CI=</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uprlb2N6zZSJQlJjKomlCg.png" /></figure><p>第一次部署會久一點大概一分半，成功點選網站的連結你會看到你的網站已上線，Netlify 會隨機生成一個 domain 給你 https://random_site_name.netlify.app</p><p>接著可以到 Site settings 裡面 Change site name 改成你要的網址，記得這個 Site name 是先搶先贏不能重複，改了以後網址就會變成 https://your_site_name.netlify.app</p><p>Domain Settings 則可以設定你自己的 Custom domain (DNS 設定我這邊就不贅述，就是加 CNAME record 這樣) 如果是 Custom domain， Netlify 會貼心地幫你在 Let’s Encrypt 取得 TLS certificate</p><p>就降，超級無腦對吧？</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/482/1*_bC-joHJRbqFGic4zJKXFw.png" /></figure><p>前台是OK了，不過後台你會發現怎麼登不進去？要用哪個帳號登入？</p><p>由於我們 CMS 是設定使用 Git Gateway 方式所以我們要在 Netlify 開通 <strong>Identity</strong> 以及下轄的子服務 <strong>Git Gateway</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-SD3n-6mx4ULuNsjQQntxQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kohBKBl74IDepToW8gJQnQ.png" /></figure><p>別擔心，這些服務都有 Free tier ，對我們的案子來說夠用了不用額外付費</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/949/1*2sf_JFmcqTBF4eKQR4GYHw.png" /></figure><p>Registration preference 因為使用者是後台成員建議改成 Invite only 才不會有人隨便亂註冊，Free tier 是給五人的 Quota，對個人網頁/小網站來說應該相當充裕 (不夠怎麼辦？ 付錢升級就是囉 💰)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qCWDeOzCkazoLhvOmbe1sQ.png" /></figure><p>接著點選上方 nav tab 到 identity 邀請使用者，使用者創建方式跟一般邀請制網站差不多，Netlify 會寄邀請信到你提供的 email address，使用者點選 email 內連結回到你的網站首頁，然後…</p><p>就沒有然後了… 哭啊… 忘記講另一個在 code base 要設定的東西，因為你要接使用者這些 confirm sign up/reset password 之類的 request，所以你的首頁必須要載入 netlify-identity-widget 這個 npm package 來回應</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MDCJ2G_PeBTWcy9L_iCelQ.png" /><figcaption>npm install 完了以後在 src/index.js 裡加入 netlifyIdentity 相關的兩行code</figcaption></figure><p>如果不是 fork 我的 repo 而是一步一步手動做到現在的同學，麻煩加了這個套件以後 commit / push 再 deploy 一次吧</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*c2m9kLCIt63aCzMflx7tjw.png" /></figure><p>裝了 netlifyIdentity widget 的話，從 email 連結跳轉過來會觸發 confirm signup 這個 popup，填入密碼後點選 Sign Up 然後把 popup 關了</p><p>確認創建密碼之後，跳轉到 /admin 輸入 email / password 你應該就能順利進入 CMS 後台了</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*M1D14q43W7JKHwS3TNYaZA.png" /><figcaption>使用者管理包含邀請/重設密碼/刪除都可以在 Identity tab 處理</figcaption></figure><p><strong>Site Url</strong></p><p>Netlify CMS 設定內有個 Site Url 這樣的東西，如果不想手動，想同步實際部署之後的網址，有沒有什麼方式可以取得這個環境變數呢？ 答案是有的，Netlify 在 build time 其實有些 env variables 可以參考 (<a href="https://docs.netlify.com/configure-builds/environment-variables/">https://docs.netlify.com/configure-builds/environment-variables/</a>)</p><p>But ! CRA 會濾掉 build time 時的 env variables，僅留下 REACT_APP_ 前綴的環境變數，所以我們又要透過 react-app-rewired 來改寫了 webpack config</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*77aqCD-piZhSMyW7boBW5A.png" /><figcaption>cofig-override.js</figcaption></figure><p>處理環境變數的是 Webpack 的 DefinePlugin，基本上就是把 Netlify 這幾個環境變數給加進去</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*a1_GIwTPpgbKk1m1e2ZUtA.png" /><figcaption>src/cms/index.js</figcaption></figure><p>這樣你 CMS 的 config 就可以根據環境變數動態生成 site_url ，上面的例子我還考慮到了使用 Netlify deploy preview 的情境，沒有用上 deploy preview 的話你直接讀 process.env.URL 即可 (DefinePlugin 那邊也可以只加入 process.env.URL)</p><p><strong>Netlify.toml</strong></p><p>環境變數、站台設定、Build/deploy settings、redirects 幾乎都可以透過 netlify.toml 來做設定，建議如果有什麼客製需求可以先讀看看官方文件，也許你想要的需求都有現成參數可以設定</p><p><a href="https://docs.netlify.com/configure-builds/file-based-configuration/">File-based configuration</a></p><h4>結語</h4><p>回想十多年前的架站環境，前端框架還不是顯學，架個簡單網頁要搞個後端環境SSR，找主機商代管，ftp 上傳，現代的基礎網路服務都已經很完備，在這之上的服務商平台也百花齊放，今日的前端大可以把重心放在使用者體驗上而不用太擔心這些 backend/devops 工作，想想真的是很幸福呢</p><blockquote>不管需求是什麼，Static Site 我一率建議部署 Netlify</blockquote><p>這話當然是嘴砲唬爛，Netlify 不見得是最經濟的選擇 (流量大貴到哭)，規模大起來的話 AWS S3 + cloudfront 可能會是更好的選項，但 Netlify 的確有他無腦好用的可取之處，例如它的 Deploy preview 對團隊分工來說就超級好用，實際評估使用量之後，如果 tier 0 / 1 就可以搞定的話不失為一個好的部署選項，還是老話一句，工具沒有絕對的好壞，只有適不適合的使用情境</p><p>看完這篇，有沒有點想法來搞個什麼 side project 了呢？</p><p>下回預告：這網站的另一半是 Blog 系統，才是真正能發揮 CMS 價值的東西，先丟這篇試個水溫，讀者敲碗反應好的話我盡量不富奸，來個掌聲唄 👏</p><p>2022–02–19 Update：<a href="https://medium.com/@nightspirit622/%E9%9B%B6%E6%88%90%E6%9C%AC%E8%87%AA%E5%B8%B6cms%E5%BE%8C%E5%8F%B0%E7%B6%B2%E7%AB%99-%E4%BA%8C-blog-8a39a235b785">零成本自帶CMS後台網站&lt;二&gt;：Blog</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4bd787fd63b5" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[npm uninstall redux]]></title>
            <link>https://medium.com/@nightspirit622/npm-uninstall-redux-c00d86683b0d?source=rss-c179cfb8173a------2</link>
            <guid isPermaLink="false">https://medium.com/p/c00d86683b0d</guid>
            <category><![CDATA[react]]></category>
            <category><![CDATA[redux]]></category>
            <category><![CDATA[hooks]]></category>
            <dc:creator><![CDATA[pxn]]></dc:creator>
            <pubDate>Sat, 16 Feb 2019 10:06:14 GMT</pubDate>
            <atom:updated>2019-02-16T10:35:31.693Z</atom:updated>
            <content:encoded><![CDATA[<p>後hooks時代，我把redux從專案中移除了…</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-Ijet6kVJqGgul6adezDLQ.png" /></figure><p>早先16.3 推出了Context Api 時，我就有寫過一篇文章：</p><p><a href="https://medium.com/@nightspirit622/%E8%A9%B2%E4%B8%8D%E8%A9%B2%E7%94%A8context-api-%E4%BE%86%E5%8F%96%E4%BB%A3-redux-4d7395d5c8da">該不該用context api 來取代 redux?</a></p><p>說明 redux 的角色日漸式微，其實你的 react 專案不一定非得搭配 redux 不可，去年底 react team 宣布了 Hooks Api ，引進各種炫砲新工具，useReducer、useContext、useEffect，種種跡象顯示擺脫 redux 指日可待</p><p>「多想兩分鐘，其實你可以不用redux」，這兩天挑了一個小規模的專案重新改寫練練手，結果遠比想像中簡單呢…</p><h3>改寫之前</h3><p>先來個前情提要，我改寫的專案使用 redux 情境如下：</p><ul><li>專案使用 redux 但極少量使用來做 state management ，多數商業邏輯實踐在 Route level 的元件中</li><li>搭配使用 redux-persist 來處理 data persistence，狀態儲存在 localStorage 或 sessionStorage 中</li><li>搭配使用 redux-saga 來處理 ajax request</li><li>搭配使用 redux-responsive 來處理 responsive design</li></ul><p>如果要把 redux 從專案中抽掉，就必須要尋找上述需求的替代方案，我的作戰計畫如下：</p><ul><li>使用 useReducer 跟 useContext 來打造一個類似 redux 的輕量 State tree datastore，拿來存 App level 的共用資料</li><li>自刻一個 persist enhancer 搭配上述的 reducer 使用</li><li>在元件中直接使用 useState，useEffect 來處理 ajax request</li><li>自刻一個 useBreakpoints 的 Hooks 來取代 redux-responsive</li></ul><p>這邊先假設讀者對 Hooks 都有初步認識，所以接下來的內容會直指問題核心，如果對 Hooks 還不清楚的朋友要煩請先到官網把文件讀個幾遍喔</p><h3>useReducer + useContext = redux</h3><p>sort of…</p><p>撇開 middleware、enhancer 這些外掛不說，redux 不過是一個 State tree datastore，透過 react-redux 提供的 Provider 元件從最頂層包住整個 App，在需要連結的子（孫）元件使用 connect HOC ，state tree mutate的時候就可以透過 props 接到更新</p><p>這個部分 Context Api 完全可以取代</p><p>State tree mutation 則是透過 reducer 這樣的 pattern</p><pre>function reducer (state, action) {<br>  // do something<br>  return newState<br>}</pre><p>所謂的 reducer 簡單說，某個 action 透過 dispatch 發佈進到 reducer 裡，你拿 action payload 跟上一動的 state 攪和攪和以後產生一個新 state 然後回傳，這就是 reducer 的真面目</p><p>回頭看看 Hooks 官方文件的 useReducer ，我說這使用姿勢相似度87像</p><blockquote>redux：抗議！他抄我。<br>useReducer：這就跟跑步游泳一樣，大家 flux一家親，沒有誰抄誰辣</blockquote><p>所以技術上來說，要達成一個輕量化 redux ，使用 useContext + useReducer 是完全可行的，不過實作上有些眉眉角角要注意…</p><p>官網提供的 useReducer 範例只是單純使用單一 switch case reducer，而多數人使用 redux 習慣上是把 reducer 分拆成數個方便管理，再用 redux 提供的 combineReducers 合併成一個 rootReducer</p><p>我本來想沿用舊代碼，但發現這個 rootReducer 似乎跟 useReducer 接不太上，奇怪，記得明明是同樣的 signature ，有可能 redux 內部還有多做其他的事，後來轉念一想，最終目標是要丟掉 redux 相依，刻一個 combineReducers 也不難，就隨手寫了 helper</p><p>此外，早先寫 reducer 的時候我們有採用 <a href="https://github.com/infinitered/reduxsauce">reduxsauce</a> 這樣的 helper，用 handlers map 比 switch case 的寫法漂亮多了，於是我又另外刻了createReducer 這樣的 helper，至於 createTypes / createActions 我嫌囉嗦，打算 dispatch 的時候直接 hard code { type: ‘ACTION_TYPE’ } ，反正我們的dispatch 的 action 不多，實在無需 Action Creator 這樣脫褲子放屁…</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/2e4a17378afc532d4299f48388e65998/href">https://medium.com/media/2e4a17378afc532d4299f48388e65998/href</a></iframe><p>另外 immutable 之類的工具也不用了，反正記得 reducer 回傳的 state 得是一個新的 object 不然是不會動的，有需要搭配使用的同學就自由發揮吧</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/dc144767654d7770f5e1498773f66e48/href">https://medium.com/media/dc144767654d7770f5e1498773f66e48/href</a></iframe><p>Data persist 的部分參考了原先使用的 redux-persist 介面寫了自家用解法，這些小工具我也不好意思獻醜拉，如果有需要上述 helpers (combineReducers/createReducers/persistReducer)的同學可在底下留言，看需要的人多的話我再來發 gist</p><p>我們來看看這在元件中如何應用，首先跟使用 redux-react 一樣，在最外部我們要用一個 Context.Provider 包住</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/5fa5b54c1b6094a6db074f91ef89571c/href">https://medium.com/media/5fa5b54c1b6094a6db074f91ef89571c/href</a></iframe><p>然後在某個子元件如要使用的話，可以透過 Context.Consumer render props，不過既然我們都用了Hooks， 何不直接使用 useContext 更快，個人認為這樣的寫法比 Consumer render props 或 HOC 的解法都來的乾淨</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/26b897e12c9d7c5478babb420c26299a/href">https://medium.com/media/26b897e12c9d7c5478babb420c26299a/href</a></iframe><p>整個 import 步驟有點多，其實可以把分散的 Code 整理整理放到同一個檔案方便外部引入</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/67325082b2a635e74f0fa1c2f5460318/href">https://medium.com/media/67325082b2a635e74f0fa1c2f5460318/href</a></iframe><p>這邊我用了一個小伎倆，named export 是給 Provider，default export 則是給 Consumer，因為多半情境你會比較常 import 後者，前者只有在 app.js會需要設定一次</p><p>在 app.js 會變成這樣</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/d03f4df2e6181026efd6eaa813c102d8/href">https://medium.com/media/d03f4df2e6181026efd6eaa813c102d8/href</a></iframe><p>元件裡引用則變這樣</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f5a6f177b6a44d910cd921969b433842/href">https://medium.com/media/f5a6f177b6a44d910cd921969b433842/href</a></iframe><p>如此一來是不是就更簡潔了呢？ 好了我們這邊一口氣解決了兩個需求</p><p>2 down 2 to go :D</p><h3>useBreakpoints</h3><p>在講重頭戲 ajax request 之前先來個相較簡單的當作中場休息，這趴來說說如何自幹 Hooks 取代 redux-responsive<em>（迷之音：我褲子都脫惹結果你給我跳話題…）</em></p><p>redux-responsive 使用上你可以設定 breakpoints，然後它在 redux state tree 內提供了一整組 boolean flags 讓你可以做查照，在元件裡就可以做類似下面的事</p><pre>&lt;div className={browser.lessThan.medium ? &#39;col-1&#39; : &#39;col-3&#39;}&gt;<br>  ...<br>&lt;/div&gt;</pre><p>在 react-use 的專案中我看到了 <a href="https://github.com/streamich/react-use/blob/master/src/useMedia.ts">useMedia</a> 這樣的 Hooks，基於 Hooks 不過是函數，你可以利用原有的 Hooks 做 composition，於是借用了 useMedia 寫了如下的代碼</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/9e1572733d02312ed3ec7304c252a664/href">https://medium.com/media/9e1572733d02312ed3ec7304c252a664/href</a></iframe><p>useMedia 這樣的 hooks 如果直接使用在各個元件中，每次使用都會訂閱 matchMedia onChange event，這樣一來會做太多不必要的訂閱，其實只要在 App level 訂閱一次，其結果再用 Context 傳遞到子元件即可，如此一來就能省去重複的event listener，於是在之上我又多加了 Context，設置方法就跟上面的 useAppReducer 差不多</p><p>在子元件中使用的方法如下</p><pre>import useBreakpoints from &#39;Hooks/useBreakpoints&#39;</pre><pre>function Foo () {<br>  const breakpoints = useBreakpoints() <br>// breakpoints.lessThan.md is equivalent to browser.lessThan.medium</pre><p>我覺得這是體現 Hooks 概念強大的一個例子，Composition + Reusability ，有了 Hooks 很多邏輯都能抽出來重複運用，如果說 Component 是視圖的樂高積木，那 Hooks 就是商業邏輯的樂高積木了。</p><blockquote>什麼？可以在元件中直接呼叫 ajax request？這合法嗎？</blockquote><p>其實打從用 react 以來我一直沒有細究說為什麼不能從元件中直接呼叫 ajax request，大家開始學的時候透過 redux 去串 thunk/saga 來處理 ajax request 彷彿就是理所當然的事。</p><p>我能想到的大致上是怕說這些非同步的 response callback 如果執行時元件已經不在了會造成潛在的 memory leak，我猜是這樣，歡迎留言分享正解，感謝大大無私分享，施主好人一生平安</p><p>不過現在我們有了 useEffect ，你可以大大方方在元件內處理 side effect 了，以下用一個 Sign In form 來做為探討案例</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/1411cb9bc23eb9a97357ca937a3dc739/href">https://medium.com/media/1411cb9bc23eb9a97357ca937a3dc739/href</a></iframe><p>ajax request 的觸發是透過一個 pending 的 flag 來操作，我們可以用一個 if statement 確保在 componentDidMount 的階段不會發出 request ，只有當 pending 由 false -&gt; true 的時候會觸發</p><p>接下來元件中不管是 button onClick 或是 form onSubmit，要做的事就很簡單，只是單純 setPending(true) 這樣，記得 request succeed/failed 要把 flag set 回 false</p><p>網路上有些案例是利用 setUrl 來做觸發，其實概念上大同小異，大家可自由發揮，不過建議是集中在 Route level 來處理這些 Ajax side effect，你如果要把層級拉到 App level，包一層 Network Component，然後搭配使用前面說的 useReducer/useContext 也是可行，那樣就更像以前的 redux 寫法</p><h3>npm uninstall redux. Yes, you can.</h3><p>記得前不久在 <a href="https://www.facebook.com/groups/reactjs.tw/">ReactJS.tw</a> 社群，有人來叫板戰 vue vs react 優劣，結果裡面一半內容是在批評 redux 有多累贅難懂，我承認 redux 發展了這麼多年，生態系膨脹到有點讓人難以上手，但它的歷史定位是無庸置疑的</p><p>只是該走下神壇的總會走下神壇，react 生態系不是注定要跟 redux 鐵板一塊，之所以熱愛 react 最大原因就是看著它不斷進化，週邊工具可能殞落，但主角依舊屹立不搖</p><p>整個改寫過程下來並沒有太多的不適應，把 Class Component 改成 Function Component 再串上 Hooks 其實蠻簡單的，而且你也不用一次到位，有用到 Hooks 再改就好</p><p>這篇文章希望能幫助到還在觀望的人，我自己先當白老鼠把 redux 給拔了，效果拔擢，同事表示喜歡，我自己也覺得不賴…</p><p>所以說，你準備好 Hooks 了嗎？</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c00d86683b0d" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[專案重回 create-react-app v2 懷抱]]></title>
            <link>https://medium.com/@nightspirit622/%E5%B0%88%E6%A1%88%E9%87%8D%E5%9B%9E-create-react-app-v2-%E6%87%B7%E6%8A%B1-27f31261d458?source=rss-c179cfb8173a------2</link>
            <guid isPermaLink="false">https://medium.com/p/27f31261d458</guid>
            <category><![CDATA[react]]></category>
            <category><![CDATA[create-react-app]]></category>
            <dc:creator><![CDATA[pxn]]></dc:creator>
            <pubDate>Fri, 12 Oct 2018 08:48:46 GMT</pubDate>
            <atom:updated>2018-10-12T17:08:59.576Z</atom:updated>
            <content:encoded><![CDATA[<p>長久以來要搞定 react 的 tooling 一直不是一件容易的事，直到 facebook 正式推出了官方的 starter kit：<a href="https://github.com/facebook/create-react-app">create-react-app</a>（以下簡稱CRA），大幅簡化了專案啟動的難度，babel、webpack 這些部分都由fb官方幫你預先調校了，開發者只要專注在寫 App 即可。</p><p>聽起來美好然而實作就會發現 CRA 初版非常陽春， CRA 旨在讓使用者免設定，但這也意味著你沒辦法動到 babel 或 webpack 的 config</p><p>等於你沒辦法用上 preset、plugins、loaders</p><p>想用 sass/scss ，無理；想用 module-resolver 來簡化 import 路徑，不行；想用上graphql relay，唉，這要掛上 babel-plugin-relay 才能用…</p><blockquote>如果你不滿意或需要更多的工具組，你隨時都可以 npm run eject</blockquote><p>所以 CRA 提供了一個緊急逃生出口，執行 eject 之後熟悉的 webpack.config.js 現身了，babelrc 也可以用上了，掌控權再度的回歸到你手上，你想串什麼工具就串什麼，好不自由…</p><p>但自由的代價也就代表一切就只能靠自己了，eject 之後是沒辦法再回去的，這也表示之後 CRA 若更新修補了 bug，你要自己補洞；難搞的 tooling 問題也沒有大神代為操刀解決，不過當時為了用上別的工具，跳出 CRA 是必要的動作，只能說有一好沒兩好，好歹 CRA 幫你起了個頭。</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/362/1*4FauKEIx2Gf4IbsimQyh7A.gif" /><figcaption>我跳出去了，我又跳回來了，咬我啊你</figcaption></figure><p>CRA 1.x 版的幾次更新並沒有什麼大變動，所以跳脫之後也沒有碰上什麼問題，然而隨著時間演進，babel 轉眼從6到7， webpack從3到4，這些大版號更新意味著 breaking change ，周邊的plugins可能都有變動，現在不跟上以後可能又有技術債要還</p><p>此時 CRA 也堂堂邁入 2.0 ，洋洋灑灑列出許多<a href="https://reactjs.org/blog/2018/10/01/create-react-app-v2.html">進步</a>的地方</p><ul><li>可直接使用 sass 或 css-module</li><li>更新為 babel 7 與 webpack 4</li><li>因為導入 babel macro 的關係所以無需透過 babelrc 設定 plugin 即可使用 relay 等工具</li></ul><p>這讓我燃起了重回 CRA 保護傘的念頭，身為開發者我想花更多心力在撰寫產品，而非糾結在設定 tooling，所以這陣子找了個空檔打算把舊專案全部拉回原始未跳脫的 CRA 2.0 框架。</p><h4>回家的漫漫長路</h4><p>重回 CRA 原始框架得先釐清當初跳脫是為了什麼，現在 CRA 2.0 有什麼替代方案，以我手上案子為例，當初跳脫是為了幾個因素…</p><ul><li>CSS Preprocessing / Postprocessing － 樣式需要預處理或後處理，其實我們也沒有用上 sass，用postcss-cssnext跟postcss的一票plugins就可以達成類似 sass 預處理的需求，不過 cssnext 是個坑啊，就像前陣子發現 color-mod 被拿掉了，然後我們家設計師用很爽通通要砍掉重練 -_-|||</li><li>import 絕對路徑 － import SomeComponent from ‘../Components/Foo’ 的相對路徑簡化成 ‘Components/Foo’</li><li>Relay － 與我們家後端是用 graphql relay 做橋接的，需要 babel-plugin-relay 才能正常 compile，另外讀取 schema file 需要 webpack raw loader</li></ul><p>其他還有一些零星需求就比較枝微末節，並不是很重要，以下是我找到替代方案的一些筆記</p><h4>CSS Preprocessing / Postprocessing</h4><p>果斷使用 sass，反正我們設計師本來就很習慣用 sass 寫樣式， 要在CRA 2.0中使用 sass 非常簡單，只要 npm install node-sass 即可，CRA會自動在webpack幫你串上 sass-loader ，那麼在程式當中就可以直接 import</p><pre>import &#39;./style.scss&#39;</pre><p>至於 Postprocessing 就讓 CRA 去煩惱就好，反正主要用途是 postcss-autoprefixer 這個部分，如果有特定要支援的瀏覽器，你可以在 package.json 中找到 browserslist 這個項目做調整</p><p><strong>import 絕對路徑</strong></p><p>我本來是用 babel-plugin-module-resolver 來處理這件事，其實說穿了就是多加一個 src 路徑讓 node 可以順利解決路徑問題，如果不用 module-resolver 的話該怎麼解呢？感謝 react 社群的大神們提供了以下的奇淫技巧</p><p>在你的 .env 檔案中加入以下代碼</p><pre>NODE_PATH=src</pre><p>然後你就可以快樂的使用 import Foo from ‘Components/Foo’ 惹，可以想像說src變成你的根目錄，路徑可以從src出發…</p><p>嗯，你說你沒有 .env 這個檔案啊，孩子沒有你可以創一個，這個是放環境變數用的，附帶一提 port 可以改啟動專案時 localhost 的 port，然後 REACT_APP_ 前綴的環境變數，你可以在專案程式中透過 process.env.REACT_APP_XXX 去 invoke，這個在做一些 feature flag 或不同環境下的參數會特別有用。</p><h4>babel-plugin-macros</h4><p>像 CRA 這樣把 babel config 藏起來，很多 babel plugins 就用不上了，直到 babel-plugin-macros 這樣的工具出現，鼓勵把 plugin 變成巨集形式，開發者需要用到的時候在程式碼中引用，然後在 build phase 的時候直接運算 transform，有興趣了解詳情的可以看這篇 <a href="https://babeljs.io/blog/2017/09/11/zero-config-with-babel-macros">Zero-config code transformation with babel-plugin-macros</a></p><p>如果你沒耐心，我先講結論，像 relay 本來需要在 babelrc 設定 babel-plugin-relay，現在 babel-plugin-relay@1.7.0-rc.1有提供 macro 了</p><p>本來</p><pre>import { graphql } from &#39;react-relay&#39;</pre><p>改為</p><pre>import graphql from &#39;babel-plugin-relay/macro&#39;</pre><p>其他程式碼就照舊，這樣就能順利的轉換你的 graphql query，隨著越來越多babel plugin 提供替代的 macro，以後就算是沒有 babelrc 設定也不怕！</p><p>此外 macro 還可以處理一些使用情境</p><p>例如說假設環境變數可以設定選擇使用 production api 或是 mock api，這邊可以用上 <a href="https://github.com/kentcdodds/preval.macro">preval.macro</a> 來幫你轉換</p><pre>import preval from &#39;preval.macro&#39;</pre><pre>module.exports = require(preval`module.exports = process.env.REACT_APP_MOCK_API !== &#39;1&#39; ? &#39;./api.js&#39; : &#39;./api.mock.js&#39;`)</pre><p>這樣在 build time 的時候就可以根據環境變數匯出相對應的檔案（沒設定Flag的時候是 export ‘./api.js’，REACT_APP_MOCK_API=1的時候會 export ‘./api.mock.js’）</p><p>另一種情境是在 build time 需要讀取文檔的場合，可以用 <a href="https://github.com/pveyes/raw.macro">raw.macro</a></p><pre>import raw from &#39;raw.macro&#39;</pre><pre>const schemaText = raw(&#39;../../schema.graphql&#39;)</pre><p>這樣一來就可以取代掉 webpack raw-loader 的使用情境</p><h4>濕主，回頭是岸</h4><p>總而言之，我又重新回到 CRA 2.0 的保護傘下了，現代前端工程花在調教 Tooling 上往往比產品開發還累人，能夠把一件煩人的事交給大神們幫我們省省心，開發上會肯定能更愉快。</p><p>其實 2.0 加入了許多新功能能滿足大多數的開發情境了，如果你先前專案從 CRA 1.0 跳脫，不妨看看有沒有辦法跳回來吧。</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=27f31261d458" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[TestCafe 自動化端對端測試工具]]></title>
            <link>https://medium.com/@nightspirit622/testcafe-%E8%87%AA%E5%8B%95%E5%8C%96%E7%AB%AF%E5%B0%8D%E7%AB%AF%E6%B8%AC%E8%A9%A6%E5%B7%A5%E5%85%B7-bac1b843c6e5?source=rss-c179cfb8173a------2</link>
            <guid isPermaLink="false">https://medium.com/p/bac1b843c6e5</guid>
            <category><![CDATA[ui-testing]]></category>
            <category><![CDATA[testcafe]]></category>
            <category><![CDATA[qa-testing]]></category>
            <category><![CDATA[front-end-development]]></category>
            <dc:creator><![CDATA[pxn]]></dc:creator>
            <pubDate>Sun, 29 Apr 2018 20:29:03 GMT</pubDate>
            <atom:updated>2018-04-30T22:30:35.042Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/249/1*JyROLiWponCcrD4FssB_6A.png" /></figure><p>自動化 UI 測試大概是每次人家問我，我回的最心虛的一塊，以往的工作經驗，說到 QA 多半都是人力解，Code 寫好了就丟給 QA team 去人工測試，但往往百密一疏，費時費力又無法完全遍歷所有使用情境</p><p>以前公司因為領頭的沒打算弄這個，又有現成 QA team，所以我也睜隻眼閉隻眼；現在在 startup 就不同了，人力資源有限，若不導入自動化工具來做 QA，恐怕產品每次上線都是在博杯，身為前端頭頭，研究可行方案的重責大任就落到了我頭上</p><p>幾番搜尋之後，找到一篇含金量十足的文章：<a href="https://medium.com/welldone-software/an-overview-of-javascript-testing-in-2018-f68950900bc3">An Overview of JavaScript Testing in 2018</a>，光是掌聲有破 10k 我就先跪了，再看到後面一長串的引用文獻… 嗯~感謝大大無私地整理分享 orz</p><p>根據上面那篇文章，作者定義了三種常見的測試</p><ul><li>Unit Tests — 單元測試，通常用在純函數的部分，確保丟進去的 input 會正確的返還 output</li><li>Integration Tests — 整合測試，用來測試一段程序或是元件是否照設計的邏輯走，包含 side effect 的測試</li><li>UI Tests — 使用者介面(功能)測試，測試使用者實際會看到的 UI 和會用到的功能是否正確運行</li></ul><p>以前端來講，Unit Tests 幫助不大，頂多用在一些純 JS 函數的部分，整合測試雖可以測試商用邏輯部分，但很多時候往往不是那麼容易與 UI 切割，所以到頭來還是測 UI Tests 最為直接</p><h3>與 TestCafe 的初次見面</h3><p>以前有耳聞利用 PhantomJS 或 WebdriverIO 來做自動化腳本測試，但總覺得範例教學看起來很麻煩，所以一直沒認真玩過；上述文章中提到一個沒看過的新名字 — TestCafe，立馬引起我的注意，Google 了一下，诶~ 好像蠻簡單的，使用前不需要太多設定，Selector / Assertion 也都很淺顯易懂，那來玩一下好了</p><p>一個小時過後，我只恨我自己為什麼不早點知道這東西，真是相見恨晚阿！</p><p>廢話不多說直接看 Code ，底下是一個檢查 Login Form 的一段範例：</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/768a00b60a546dcecf0e20512c8f274a/href">https://medium.com/media/768a00b60a546dcecf0e20512c8f274a/href</a></iframe><p>首先你會發現，挖… 可以用 async / await ，沒錯，TestCafe 可以讓你用 ES2017 ，寫測試可以用上各式語法糖（葛萊芬多先加十分）；Selector 的概念跟 CSS / JQuery 幾乎一模一樣，所以只要你會 JQuery ，要上手 TestCafe毫無難度</p><p>以一個測試腳本來說基本上你會做的三件事為</p><ol><li>Selector — 選取你需要觸發或檢查的元件</li><li>Actions — 模仿使用者對指定元件們進行一系列操作</li><li>Assertion — 利用 expect 來檢查畫面上的變化情形</li></ol><p>要跑這段測試只要在 terminal / cmd 裡執行</p><pre>$ testcafe chrome testcafe-example.js</pre><p>你會看到 testcafe 彈出一個 Chrome window，然後滑鼠游標飛快的在你的網頁上面移動，打字，點擊，完成一系列動作之後檢查畫面是否與你期待相符</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*93Slzv1XMPK3FY3_TqH2qQ.gif" /></figure><p>從此再也不用辛苦手動測試，交給 TestCafe 一切自動搞定</p><h3>我覺得這樣很酷</h3><p>TestCafe 有幾點我覺得非常加分</p><ol><li>支援 ES2017 與 Typescript</li><li>零設定免安裝其他套件，npm install 完就可以用了</li><li>官方提供 React、Angular、Vue 等前端框架的專用 Selector ，也就是說你不見得只能靠 CSS Selector 來選元件，以 React 為例，你可以直接透過 Component Name 來做選取，並且可以讀 props、state 來做驗證</li><li>官方提供開發時專用的 testcafe-live ，類似 webpack-dev-server 之於webpack ，使用這個的話瀏覽器在測試跑完後不會關閉，你可以打開Chrome Console 查找元件，更新程式碼之後也會自動重新執行測試</li><li>測試中可以寫 code 來照螢幕截圖，這個在寫文件需要截圖時非常方便，可以全部交由 TestCafe 自動化處理；另外也可以在跑測試時聲明只有在錯誤發生的時候才拍照，方便你反查究竟是發生甚麼錯誤</li></ol><p>此外還有跟 CI 整合或是 Git Hooks 之類的那些，我還沒研究透徹就先不說了，光上面五點就有值回票價之感，我能想到需要解決的問題大體上TestCafe 都涵蓋了</p><h3>開發眉角</h3><p>這邊因為我也還在繼續摸索就不寫詳細的教學文誤人子弟，但可以聊一下我目前學到的一些眉眉角角</p><h4>Roles</h4><p>當測試頁面需要使用者登入的時候，你可以利用內建的 Role 來定義你的使用者身份，基本上這是告訴 TestCafe 你要怎麼登入：指定好登入頁面，然後用 Selector / Actions 組合達到自動化登入效果</p><p>打包好的Role 可以在執行測試的時候透過 useRole(role) 來執行，通常你會把這個動作掛在 fixture.beforeEach 或是 test.before</p><p>之所以需要這樣做的原因是每一個測試都是一個新沙盒， 所有的暫存資料如 Cookie Session、Local Storage 都會不見，變成得要重新登入 ，我目前還沒有找到可以留存暫時資料的方式，看官方討論區似乎也沒這樣的設計</p><p>現在能稍微緩解這個痛點的解法是</p><p>1. 簡化登入流程 — 可以另外開一個 endpoint 利用 URL Params 輸入使用者帳密或 AccessToken 然後 redirect 達到快速登入 （給機器用的登入口）</p><p>2. 需要同一暫存態的使用情境集中在同一個測試中做，一個測試裡其實可以在任意時刻呼叫 expect 做驗證，並不用死板板的硬是拆成好幾個 Tests<br>（4/30 更新: TestCafe 有 context 可用以儲存暫時資料，本文後面有更新）</p><h4>ClientFunction</h4><p>在測試中如果需要呼叫客戶端的 script，或是 access window 物件，可以利用這個，一個最常見的使用情境是檢查當前的 url location</p><pre>import { ClientFunction } from &#39;testcafe&#39;<br>const getWindowLocation = ClientFunction(() =&gt; window.location)</pre><p>在 Assertion 的時候就可以做類似這樣的檢查（以登入成功會跳轉離開 login page 為例）</p><pre>.expect(getWindowLocation()).notContains(&#39;login&#39;)</pre><h4>Page Model</h4><p>這是官方推薦的做法，為每個頁面寫一個 Page Model 把該頁面的 Selectors 集中起來，測試的時候再 import 使用，因為有時候測試是會涵蓋到好幾個頁面，這時候變成只要引入不同頁面的 Page Model 即可，無須重複定義；在原始程式改變了元件位置或名字的時候，你也可以透過更新 Page Model 重新選取正確 UI 元件，無須一個一個修改測試</p><p>另外我發現一個蠻好用的小技巧是把常用的一些動作組合，我稱之為<strong>巨集</strong>（macro）一併寫入 PageModel 裡</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/7191ecf49aafab9d60311bae0adcebcc/href">https://medium.com/media/7191ecf49aafab9d60311bae0adcebcc/href</a></iframe><p>這樣在實際測試的時候便可重複呼叫利用，省去每次都要寫同樣動作的流程</p><pre>test(&#39;signup a user&#39;, async t =&gt;{<br>  await signup.fillSignupForm(t)</pre><pre>  t.expect(...)<br>})</pre><h3>結語</h3><p>對前端來說自動化測試一直是很頭疼的問題，但 TestCafe 很好的處裡了大部分會用到的情境，其使用語法相當簡潔，幾乎是你會 JQuery 就會寫測試；官方提供的 React 專用 Selector 與開發時會用到的 testcafe-live 也讓人相當激賞，如果目前的工作流程中還沒有導入自動化 QA 這一塊，建議你不妨一試</p><h4>4/30 更新</h4><p>這兩天研究後發現 TestCafe 有提供 <a href="https://devexpress.github.io/testcafe/documentation/test-api/test-code-structure.html#sharing-variables-between-test-hooks-and-test-code">Context</a> ，並且有兩種不同的 scope 讓你應用，分別為 Test 本身的 t.ctx 和整個 Fixture 的 t.fixtureCtx，可用於暫存測試途中所產生的暫時資料，例如 signup 新產生的使用者 username，你可以帶到下一個測試中做驗證或作為另一個 text input</p><p>前面文中建議說把測試步驟集中在同一個測試，假若你的暫存態都是在客戶端為主，那麼還是建議將步驟分拆，數個測試間利用 fixtureCtx 來交換資料</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bac1b843c6e5" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[該不該用context api 來取代 redux?]]></title>
            <link>https://medium.com/@nightspirit622/%E8%A9%B2%E4%B8%8D%E8%A9%B2%E7%94%A8context-api-%E4%BE%86%E5%8F%96%E4%BB%A3-redux-4d7395d5c8da?source=rss-c179cfb8173a------2</link>
            <guid isPermaLink="false">https://medium.com/p/4d7395d5c8da</guid>
            <category><![CDATA[front-end-development]]></category>
            <category><![CDATA[react]]></category>
            <dc:creator><![CDATA[pxn]]></dc:creator>
            <pubDate>Mon, 16 Apr 2018 05:03:41 GMT</pubDate>
            <atom:updated>2018-04-16T06:49:22.208Z</atom:updated>
            <content:encoded><![CDATA[<p>React 16.3正式推出了 <a href="https://reactjs.org/docs/context.html">context api</a>﹐被戲稱“redux 末日”的官方share data api﹐究竟葫蘆裡賣的是什麼藥？又是不是真的能取代react社群長期以來依賴的redux套件（<a href="http://blog.isquaredsoftware.com/2018/03/redux-not-dead-yet/">redux維護者Mark Ericson表示沒這回事</a>！）實際玩了幾天來發表一下個人看法，順便提供我認為如何正確使用context api的姿勢</p><h4>redux 與 context api 解決了什麼問題？</h4><figure><img alt="Image result for react unidirectional data flow" src="https://cdn-images-1.medium.com/proxy/0*35JeHn5u_Kldva32.png" /><figcaption>“Props down Event up” Uni-direction data flow</figcaption></figure><p>React寫到一個程度﹐一定會知道一個重要概念：<strong>單向資料流</strong>﹐整個React App要遵行資料從上到下的單一方向性﹐如果子元件的狀態改變了會影響到父元件或共享元件﹐那麼也是透過event callback或是dispatch action去通知父元件更改狀態﹐父元件render之後再透過props把更新的值傳遞給子元件達成畫面更新。</p><p>當你的state變化會導致複數元件需要同時更新畫面的時候﹐該state就應該上浮到共同父元件暫存﹐如果只有父子間只有一兩層階層那麼state與callback透過props傳遞還算好處理﹐不過試想如果中間有許多的wrapper﹐之間差了五六層元件﹐你的props得一層一層傳下去﹐結果就是分不清楚自己是在寫程式還是演Inception﹐很容易造成不可預知的bug難以維護。</p><p>要解決這種被戲稱為“Props drilling”的問題﹐透過一個集中式的Data Store﹐需要共享/更新共同狀態的元件可以經由HOC來連結Data store﹐如此一來我們還是遵守資料流的單一向性﹐但同時又可以隔空抓藥﹐不需要透過層層傳遞才能獲取state。</p><p>這種應用架構被稱為“flux”﹐在當年與react一同推出﹐但FB並沒有出官方的套件只給了概念﹐一時間百家爭鳴：fluxxor、reflux、redux… 但最後redux殺出一條血路打趴眾多套件變成了社群間最推崇的flux實作。</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/0*Sc6hPcHvBR6woMxP." /></figure><blockquote>redux都一統江湖了那幹嘛用context api ?</blockquote><p>我不覺得react團隊出context api是劍指要把redux幹掉﹐充其量只能說補上了一塊長期缺漏的拼圖﹐讓React本身在不需要依賴第三方flux套件的情況可以避掉props drilling這個痛點﹐既然Component已經有了基礎的 state/setState 可以做state management並且跟 life cycle 接得很好﹐何必還要依賴第三方套件呢？</p><p>而且redux並不是沒有它自己的問題﹐雖然可以透過combineReducers把reducers分拆成複數檔案再集成﹐可以做到separation of concerns﹐但因為整個應用依舊共享單一Data Store﹐在取state/action名的時候都要格外小心naming collision（很多時候會讓人覺得state如果存在某個parent node就好了﹐實在沒有必要上浮到全域，尤其是某些UI上的狀態）此外額外的學習門檻，環境設定，也很難讓人馬上上手。</p><p>Context api可以說是簡化版的redux﹐它沒有redux那麼強大可以結合許多redux生態系的middleware﹐例如thunk或saga來做data fetching / 處理side effect﹐不過若只是單單想找地方存share data來避掉props drilling的問題﹐那麼context api不失為一個簡單的解法。</p><h4>正確的使用姿勢</h4><p>以下我會用一個簡單的Counter計數器範例來解釋如何使用Context</p><p>首先我們新開一個檔 context.js﹐用 React.createContext 實體化一個context物件﹐此物件有兩個Component：Provider、Consumer</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/ddac6ee5e82e37d763000eb799882f69/href">https://medium.com/media/ddac6ee5e82e37d763000eb799882f69/href</a></iframe><p>我發現最漂亮的做法是寫一個HOC﹐類比的話類似於redux的connect﹐又或是 react-router的 withRouter。以上面例子來說﹐用withCounter(Foo)包好包滿﹐在Foo元件內便可以透過 props.counter 提取counter object的 state/methods</p><p>使用HOC的好處是任何需要接上這個context的元件﹐只要import withCounter﹐包好後export即可﹐節省了很多複製貼上的重複動作…</p><p>HOC裡我們看到context的Consumer包住了元件﹐這Consumer有個Code signature 如下：</p><pre>&lt;Consumer&gt;<br>{value =&gt; &lt;Component counter={value} /&gt;}<br>&lt;/Consumer&gt;</pre><p>你可以想成類似redux裡的mapStateToProps動作﹐value 會被當作props傳入被包著的元件﹐key的部分可以隨意命名﹐這邊因為withCounter所以顧名思義用counter當作key﹐至於value從何而來﹐長甚麼樣？別急﹐底下為你分解</p><p>接著我們開一個新檔Counter.js﹐這個我們當作Counter最頂的父元件﹐所有Counter相關的state/methods會存在這個父元件的state中</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/5b559a5ebff8b7514d460d484b56b6d3/href">https://medium.com/media/5b559a5ebff8b7514d460d484b56b6d3/href</a></iframe><p>在上面範例當中﹐我存了一個counter object在state裡 ﹐包含了計數器目前的數字value﹐還有一個遞增(inc)與遞減(dec)的method</p><pre>this.state = { <br>  counter: { <br>    value: 0,<br>    inc: this.inc,<br>    dec: this.dec <br>  }<br>}</pre><p>遞增遞減的部分不外乎就是用setState去更新state.counter.value﹐至於render function我們會發現context Provider居然用在這</p><pre>render () {<br>  return (<br>    &lt;Provider value={this.state.counter}&gt;<br>      &lt;CounterCard/&gt;<br>    &lt;/Provider&gt;    <br>  )<br>}</pre><p>如果你稍微敏銳一點應該會發現﹐“疑剛剛Consumer接到的value該不會就是從Provider這邊來的吧”</p><p>Bingo! Provider跟Consumer就像是用蟲洞接上一樣﹐當value變動的時候﹐所有Provider底下源自同一個context的Consumer都會接到更新後的value。</p><p>如果你讀過redux原碼﹐作法其實大同小異（畢竟redux原作者Dan神都去react團隊了 XD）不過與redux不同的點在context api並沒有dispatch這樣的概念﹐所以我得將methods透過value一併傳給Consumer﹐這樣子元件才有辦法呼叫遞增/遞減更新狀態。</p><p>接著我們來處理細部元件﹐首先是按鈕﹐我們需要一個+按鈕跟一個－按鈕</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/2e3c8ea9b8ea859ce93c22a4023873fc/href">https://medium.com/media/2e3c8ea9b8ea859ce93c22a4023873fc/href</a></iframe><p>這邊我們先創建一個stateless component叫IncBtn﹐從props裡面取得counter之後﹐將button onClick綁上counter.inc method﹐接著從context.js裡面import withCounter這個HOC來包住IncBtn﹐如此IncBtn就會接收到counter這個props</p><p>遞減按鈕也是同樣步驟只不過onClick改綁counter.dec﹐接著來看下個元件</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/29c4a72c7f858348353fe4f083109615/href">https://medium.com/media/29c4a72c7f858348353fe4f083109615/href</a></iframe><p>我們再次創建一個Stateless component叫做Display﹐用withCounter包著確定他會接收到counter這個props﹐然後在h5裡面把counter.value給印出來</p><p>最後一個元件CounterCard只是單純定義Layout﹐把上面的按鈕與顯示元件一個蘿蔔一個坑的放好</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e8f78e9daf951fb0cb5ac1c999a8cabb/href">https://medium.com/media/e8f78e9daf951fb0cb5ac1c999a8cabb/href</a></iframe><p>整個元件階層從Counter -&gt; CounterCard -&gt; CounterDisplay &amp; CounterButton 算下來是兩層﹐但我們並不用大老遠地將props跟callback method一路傳下去﹐因為他們都已經透過withCounter HOC接好了。</p><p>底下是用jsFiddle寫的Live example﹐因為必須將所有元件放一起所以做了些許變動﹐不過相信聰明的讀者看得懂我在幹嘛。</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fjsfiddle.net%2Fnightspirit622%2Fhhtzyjj0%2Fembedded%2F&amp;url=https%3A%2F%2Fjsfiddle.net%2Fnightspirit622%2Fhhtzyjj0%2F2%2F&amp;image=https%3A%2F%2Fwww.gravatar.com%2Favatar%2Fede0178f1b65df81beb6218f77c81dc0%2F%3Fdefault%3D%26s%3D120&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=jsfiddle" width="600" height="400" frameborder="0" scrolling="no"><a href="https://medium.com/media/c0e766703b0b4aa5354aa124eca80521/href">https://medium.com/media/c0e766703b0b4aa5354aa124eca80521/href</a></iframe><p>看到這有沒有覺得context api其實就是一個簡化版redux？如果只是單純share state用的話其實差異不大。</p><h3>結語</h3><blockquote>所以﹐我該把全面改用Context嗎？</blockquote><p>不﹐當然不！如果你Production redux用得好好的﹐不用浪費時間去改成Context﹐千萬別自找麻煩﹐Context就是個替代方案﹐頂多在寫新元件如果不想要依賴redux的時候﹐可以試著用Context來解。</p><p>但如果你是開新專案做﹐那麼問題就變成…</p><blockquote>我有需要繼續用redux嗎？</blockquote><p>這個問題就比較開放式了﹐以筆者為例﹐我在公司新開的專案架構對於data fetching部分導入了 relay/graphql﹐redux在專案內的重要性相對降低﹐除了少數state配著redux-persist（local persistence）或reselect（computed state caching）使用﹐若計畫將redux完全拿掉﹐單純靠context api跟自己寫的lib/helper應該過得去。</p><p>另外根據Dan神 react 17的搶先看﹐async rendering 模式看不出非得仰賴redux來處理data fetching side effect的需求﹐所以如果是我當家說話﹐我可能會把redux從新專案架構中移除。</p><blockquote><a href="https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367">you might not need Redux</a></blockquote><p>是redux的維護者常掛嘴邊的一句話﹐老話一句﹐一百個案子有一百個不同的考量﹐選工具一向沒有甚麼正解不正解﹐能不能貼合團隊的需求最重要! 希望這篇文章能讓你對context api有初步的認知﹐幫助你做出合適的選擇。</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4d7395d5c8da" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[你可以沒有熱情，但不應該自我設限]]></title>
            <link>https://medium.com/@nightspirit622/%E4%BD%A0%E5%8F%AF%E4%BB%A5%E6%B2%92%E6%9C%89%E7%86%B1%E6%83%85-%E4%BD%86%E4%B8%8D%E6%87%89%E8%A9%B2%E8%87%AA%E6%88%91%E8%A8%AD%E9%99%90-ca623ed508cc?source=rss-c179cfb8173a------2</link>
            <guid isPermaLink="false">https://medium.com/p/ca623ed508cc</guid>
            <category><![CDATA[front-end-development]]></category>
            <category><![CDATA[careers]]></category>
            <dc:creator><![CDATA[pxn]]></dc:creator>
            <pubDate>Fri, 13 Apr 2018 08:35:08 GMT</pubDate>
            <atom:updated>2022-05-24T19:06:17.184Z</atom:updated>
            <content:encoded><![CDATA[<blockquote>大家都有熱情研究新技術嗎？<br>做軟體這一行，如果有熱情的話好像都過得很快樂。</blockquote><p><a href="https://www.facebook.com/groups/f2e.tw/permalink/1616468435057182/">原文</a>發表在<a href="https://www.facebook.com/groups/f2e.tw/">Front-End developer Taiwan臉書社團</a> ，為了回應團友<a href="https://www.facebook.com/groups/f2e.tw/permalink/1615779505126075/">Tai-Shun Huang的提問</a>，結果反應還不錯，所以決定排版潤稿補完缺漏後重新發佈在個人medium</p><p>自我進修是對於自身專業的尊重，這可以套用到所有行業，只是前端技術迭代的輪替非常快所以特別有感，當技能樹越廣越深, 待遇與職稱也會呈正相關向上</p><p>學習新技術的好處，具體來說我覺得有以下幾點</p><ul><li>人腦的知識是由神經元網路連結而成， 越多的連結會深化你對於這門知識的掌握程度，學習曲線到後期是越來越簡單，你會發現自己學新東西越來越快，處理問題上也能用更全面的角度去思考，提出更高效簡潔的解法</li><li>優化你的工作流程，我不曉得有多少人喜歡做重複浪費時間的事，至少我非常討厭，能自動化的東西就不會想手動，我不相信人類在做重複性質高的工作上會勝過電腦的效率與正確性，新技術很多時候其實是在優化你的工作流程，讓你能事半功倍 （換言之, 如果導入新技術是在拖慢你速度的話, 那不用也罷）</li><li>跟上業界潮流，這個就比較偏現實考量，當職缺開出來與自己的技能樹不相符，你機會就硬生生少人一截，身為碼農雇員技術力就是你的籌碼，籌碼少的時候談甚麼都顯得彆扭，程序員職涯最怕就是掉入死亡螺旋，想離職但沒人要，留在一間技術債太多的公司追著解不完的bug也提升不了自身實力，惡性循環的結局就是等到公司營運不佳被Layoff而面臨中年失業危機</li></ul><p>簡言之，學習新技術的目的是在於：</p><ul><li>深化對專業的掌握度</li><li>優化自身的工作效能</li><li>提升在職場上的籌碼</li></ul><p>如果你是認真看待這份工作，想靠這份工作吃一輩子的飯，你都不應該停止學習，It’s part of the job.</p><blockquote>新技術那麼多，怎麼追都追不完，哪來那麼多時間學？</blockquote><p>新技術有如過江之鯽，尤其前端，常常會讓人有種怎麼追也追不完的感覺，怎麼在這一波又一波的技術浪潮站在風尖浪口， 我自己會簡單的過濾一下:</p><ul><li>這個技術解決了甚麼痛點？</li><li>跟同類型技術相比，有甚麼優缺點？</li><li>能解決我目前會碰到的問題嗎？還是帶來更多技術債？</li></ul><p>通常這樣的過濾花不到你十分鐘，看個Demo、幾行程式碼、Benchmark，大概就可以略知一二了，要用沒有時間做為藉口實在說不過去</p><blockquote>時間就像乳溝, 擠一擠總是有的</blockquote><p>如果新技術正巧可以解決你現有的痛點，太棒了，一定要玩一下看怎麼整合到現有工作流程</p><p>如果新技術聽起來很炫砲但並不是直接能用上，那連結先存起來，有時間再來研究看看（通常會回頭看是當這技術變成Buzzword一直聽到，或是碰到難題突然想起來好像有這樣的解法）</p><p>如果技術聽起來就是個雷，果斷放棄吧，人生應該浪費在更美好的事物上</p><p>技術又有分</p><ul><li>Design Pattern / Coding Style：這種比較抽象高大上，不是很好理解但是念通了對自己程式設計功力會大幅提升</li><li>公用函式庫 / 框架：實戰的時候可以省掉很多手工，無聊可以看看原碼看人家怎麼實作的，萬一哪天想勤快一點自幹也知道該從何下手</li><li>流程自動化 / 優化：Webpack、Git、Unit testing、CI 這些都算，看團隊看個人看需求，當用則用，最高原則是你是用這些來幫你省事而不是自添麻煩的</li></ul><p>三者來說以第一種知識最為受用，因為內化成心法不管在哪都用得上；後兩者則是對你工作效能表現上會有助益，一個是內功一個是外功，兩者都應該與時並進不該偏廢一方</p><blockquote>學以致用，做中學方能找到熱情</blockquote><p>學技術是用來解決現實問題的，拿著牛刀沒牛好殺是件很無趣的事，如果你沒牛好殺，該思考的是是不是現在的工作環境限制住了你的發展，小心我前面提過的死亡螺旋！</p><p>熱情來自於設定目標達成之後大腦釋放內啡肽的正向循環，如果對學習沒有熱情，很可能是對於目標設定上的迷惘：</p><blockquote>我學這個到底可以幹嘛？</blockquote><p>這才是導致沒有學習動機的真正原因，你的大腦需要獎勵機制，你得先有一個痛點，通過學習而克服獲得獎勵，你才會食髓知味產生熱情</p><p>然而萬事起頭難，並不是誰一開始都知道有什麼問題好解，我在文章開頭說 “你可以沒有熱情，但不應該自我設限” 其實另一層意思是，你應該試著去培養對這份專業的熱情</p><p>不管是說提升自己工作效率，找題目解，甚至是換份新工作（通常會伴隨大量學習機會）這些都可以是動機，火車起步雖慢，但一旦跑起來後要停下來也很難</p><p>但如果火車真的啟動不了，恩～人生很長，每天做著沒有熱情的工作真的有意義嗎？</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2Fhr8jWDyb1jg&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Dhr8jWDyb1jg&amp;image=http%3A%2F%2Fi.ytimg.com%2Fvi%2Fhr8jWDyb1jg%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/4e7909a24395a4ce62f5f4d9024021f3/href">https://medium.com/media/4e7909a24395a4ce62f5f4d9024021f3/href</a></iframe><p>糊口飯吃不需要熱情，但有熱情在這條路上肯定能走得比較快樂也比較遠</p><p>又或許不是非得當碼農不可，換個跑道搞不好能見到另一片風光啊</p><blockquote>願每個人都能在自己的領域找到屬於自己的熱情</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ca623ed508cc" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[React 16.3 之後的Lifecycle hooks]]></title>
            <link>https://medium.com/@nightspirit622/react-16-3-%E4%B9%8B%E5%BE%8C%E7%9A%84lifecycle-hooks-311661f65859?source=rss-c179cfb8173a------2</link>
            <guid isPermaLink="false">https://medium.com/p/311661f65859</guid>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[front-end-development]]></category>
            <category><![CDATA[react]]></category>
            <dc:creator><![CDATA[pxn]]></dc:creator>
            <pubDate>Wed, 11 Apr 2018 10:05:56 GMT</pubDate>
            <atom:updated>2018-04-13T07:31:24.866Z</atom:updated>
            <content:encoded><![CDATA[<p>16.3最大的變革之一是有三個Lifecycle hook被列為<strong>不安全</strong>，分別為：</p><ol><li>componentWillMount</li><li>componentWillReceiveProps</li><li>componentWillUpdate</li></ol><p>目前16.3這些都還能正常觸發，另外為了慢慢地淘汰掉這些hook，此版開始這三個hook會有 <strong>UNSAFE_ </strong>前綴的別名，此外加入了新的hook來對應未來的改版，React團隊預計在17.0版淘汰掉這三個hook，僅留下<strong>UNSAFE_</strong>前綴作為備援，官方推薦盡量改用新的hook來喜迎17版</p><h3>在開幹之前…</h3><p>我知道聽到這個消息你可能第一時間會覺得錯愕，專案裡已經不知道用了多少(使用 x 誤用 o)想到要逐一改就頭大，不過在開幹之前可以先看看Dan神在JSConf Iceland 2018的Demo</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FnLF0n9SACd4%3Ffeature%3Doembed&amp;url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DnLF0n9SACd4&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FnLF0n9SACd4%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/4cb8b0a5fb5cee068e9fdb9c507b4ef5/href">https://medium.com/media/4cb8b0a5fb5cee068e9fdb9c507b4ef5/href</a></iframe><p>前半段演算效能的火力展示可能勾不起前端人太大興趣，不過後半段對於Async Rendering (suspense)的介紹讓我整個都燃起來了，透過新技術在實務上可以解決掉許多UI/UX痛點，讓我對17版抱持了非常大的期待</p><p>推出新的Lifecycle hook也是因為長久以來大家的錯誤使用Lifecycle，導致要實踐17版suspense這樣的新技術無法順利執行，所以為了炫砲的Async Rendering，該改的還是早點改一改別欠技術債吧</p><h3>過來人經驗談</h3><p>在官方部落格有提供詳細的升級說明 (原文: <a href="https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html">Update on Async Rendering</a>) 這兩天動手修了手上兩個project，其實改動程度並沒有想像中艱鉅，先說結論…</p><p>componentWillMount 改接 componentDidMount<br>componentWillUpdate 改接 compomentDidUpdate</p><p><strong>99%可以無痛改版</strong></p><blockquote>可是我需要在元件Mount前呼叫遠端資料阿…</blockquote><p>官方開釋這是誤用，這在Server side render跟未來的Async Rendering會有衝突，因為componentWillMount很可能被呼叫很多次，安全的做法是改接<br>componentDidMount</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e884cc339b59591697692d9dc5b22a7a/href">https://medium.com/media/e884cc339b59591697692d9dc5b22a7a/href</a></iframe><p>至於Subscribe event listener, setInterval 之類的動作我一直都習慣用componentDidMount不用改，如果以前誤植接到componentWillMount的，也請一併修正</p><p>componentWillUpdate官方敘述的使用情境是</p><blockquote>“在state mutated後將State傳到外部callback”</blockquote><p>要改成compomentDidUpdate來接，這其實蠻合理的，如果同時間有許多setState被觸發，串接compomentDidUpdate才能確保是安定的 state，不過這個使用情境我幾乎沒遇到</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/071a9efe2bd7ae68c03333194bbc3ba9/href">https://medium.com/media/071a9efe2bd7ae68c03333194bbc3ba9/href</a></iframe><h4>componentWillReceiveProps</h4><p>要改最多的應當是componentWillReceiveProps，在看到原文的時候就有這樣的預感，這個lifecycle hook 使用的機會很多，舉凡：</p><ul><li>根據props去更新內部state</li><li>props改變的時候呼叫外部function (包含fetch之類的async call)</li></ul><p>先說第一種案例, 16.3引進了一個新的Hook：<strong>getDerivedStateFromProp，</strong>大體上是來解決componentWillReceiveProps在案例一的情境，他的method signature長這樣</p><pre>static getDerivedStateFromProps(nextProps, prevState) {<br>  // ...<br>}</pre><p>要注意的是這個是 static method 所以是沒有this context的，this.props / this.state 這些都無法被引用 (並沒有綁到實體上)，可做為參考的只有當作args傳進來的 nextProps, prevState</p><p>getDerivedStateFromProp 回傳的會是更新過後的state object 或是 null 表示state不需要更改</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/50c114b2cffae66bb7760ae6b51839bb/href">https://medium.com/media/50c114b2cffae66bb7760ae6b51839bb/href</a></iframe><p>透過官方的範例可以得知，以往單靠比較 this.props (舊值) vs nextProps (新值)然後setState的作法已經行不通了，必須把要比較的值也存到state當中，才能在之後透過 nextProps.something vs prevState.something做比較</p><p>在有複數情況要透過props變化來update state，或是state本身複雜的時候，寫getDerivedStateFromProp要特別小心，注意回傳的state object並不會跟prevState merge，不像setState是會shallow merge的，我能給的小技巧建議就是善用es6的spread syntax (…) 做shallow merge 例如：</p><pre>return {<br>  ...prevState,<br>  isScrollDown: true<br>}</pre><p>然後複數conditions情況下，我會這樣寫 (state沒有髒掉就回傳null)</p><pre>let state = {...prevState}<br>let dirty = false</pre><pre>if(nextProps.foo !== prevState.foo){<br>  state.foo = nextProps.foo<br>  dirty = true<br>}<br>if(nextProps.bar!== prevState.bar){<br>  state.bar = nextProps.bar<br>  dirty = true<br>}</pre><pre>return dirty ? state : null </pre><p>這樣案例一算是解決了，來看看案例二的情況，這有可能發生在傳進來的新props碰到某個條件，你可能要dispatch action去redux, router要跳轉阿之類的</p><p>基本上只要不牽涉到state需要改變，其實這些動作可以接到compomentDidUpdate來執行</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e5500d59ed3af3b60f5f5b50df58602c/href">https://medium.com/media/e5500d59ed3af3b60f5f5b50df58602c/href</a></iframe><p>上面的例子是假定透過redux發了個ajax request等著props更新，reqPending變化並且reqPending === false的時候表示收到response了，然後response沒有錯誤的話就透過 react-router 的 history api跳到別的 route</p><p>在compomentDidUpdate是可以存取this context 還有 prevProps, prevState 所以可以做各種比較，大體上只要不涉及到setState的步驟都可以在這邊做…</p><p>疑? 你說如果我要改state怎麼辦，同學，那個你在上一動getDerivedStateFromProp就該做好了阿… (茶)</p><h3>Dan Abramov on Twitter</h3><p>I just made this diagram of modern React lifecycle methods. Hope you&#39;ll find it helpful!</p><p>Dan神有提供一張圖表來解釋各個Lifecycle hook的發生時序，看完應該會更明白整個生命週期</p><h3>結語</h3><p>除此之外還有一個新的 getSnapshotBeforeUpdate，理論上這個是來讀上一禎DOM的快照，主要拿來做一些高度調整樣式連動之類的改變，但這個我沒怎麼用到就不講了，總的來說變動不多啦，為了而後的17版強大功能，各位同學還是捏著____趕緊修一修改一改吧，Right On！</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=311661f65859" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>