ErrorBaker 技術共筆部落格
ErrorBaker 技術共筆部落格
2023-12-30T00:00:00Z
https://blog.errorbaker.tw
ErrorBaker 技術共筆部落格
用 k6 為 api 做 Load Testing
2021-08-07T00:00:00Z
https://blog.errorbaker.tw/posts/ruofan/k6-load-testing/
<!-- summary -->
<p>Hi,大家好!前陣子在研究可以為 api 做 Load Testing 的工具。<br />
如果你對於這個工具感到好奇,可以看看這篇。</p>
<!-- summary -->
<!-- more -->
<h2 id="%E4%BB%80%E9%BA%BC%E6%98%AF-k6-%3F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/k6-load-testing/#%E4%BB%80%E9%BA%BC%E6%98%AF-k6-%3F">#</a> 什麼是 k6 ?</h2>
<p>以下為 k6 官方文件上對自己的介紹:</p>
<blockquote>
<p>k6 is a developer-centric, free and open-source load testing tool built for making performance testing a productive and enjoyable experience.</p>
</blockquote>
<blockquote>
<p>Using k6, you'll be able to catch performance regression and problems earlier, allowing you to build resilient systems and robust applications.</p>
</blockquote>
<p>簡單來說,k6 是一個開源的 api 負載測試工具,下方圖片中區分出不同類型的api 測試,出自於 k6 官方文件。</p>
<p>如果想認識不同類型的 api 測試之間的差異推薦去看<a href="https://k6.io/docs/">官方文件</a>,這次會實作的部分是 (For performance) Load testing。</p>
<p><img src="https://k6.io/docs/static/e45e3f092ab0445aa3da987a69ddad85/d9937/test-types.png" alt="" /></p>
<p>特別值得注意的是,因為 k6 是用 Go 撰寫的,如果需要引用其他 node module 搭配 k6 做測試的話,會需要使用 Webpack 打包工具以及 Babel 編譯。</p>
<h2 id="%E9%96%8B%E5%A7%8B%E5%AF%A6%E4%BD%9C%E5%90%A7%EF%BC%81"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/k6-load-testing/#%E9%96%8B%E5%A7%8B%E5%AF%A6%E4%BD%9C%E5%90%A7%EF%BC%81">#</a> 開始實作吧!</h2>
<p>首先會需要安裝 k6,這邊會以 mac 透過 Homebrew 安裝為例:</p>
<pre class="language-bash"><code class="language-bash"><span class="token variable">$brew</span> <span class="token function">install</span> k6</code></pre>
<p>這邊會用 fake.js 套件產生假資料協助測試,因此也會實作打包與編譯設定檔的部分。</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">install</span><br /> @babel/core <span class="token punctuation">\</span><br /> @babel/preset-env <span class="token punctuation">\</span><br /> babel-loader <span class="token punctuation">\</span><br /> core-js <span class="token punctuation">\</span><br /> webpack <span class="token punctuation">\</span><br /> webpack-cli</code></pre>
<p>這邊先分享一下接下來實作的檔案結構配置。</p>
<pre class="language-bash"><code class="language-bash">├── dist<br />│ ├── test.index.js<br />│ └── test.index.js.map<br />│<br />├── utils<br />│ ├── faker.js<br />│ ├── generateData.js<br />│ └── testing.js<br />│ <br />├── webpack.config.js<br />├── .babelrc<br />└── package.json</code></pre>
<p>接著設定 webpack config 檔案<br />
讀者如果想要試著實作的話,這邊的設定檔只需要更改 entry file 的路徑。<br />
打包完的檔案會出現在 dist 的資料夾中,最後在跑 script 的時候只需要跑 dist 中產生出來的檔案。</p>
<h6 id="webpack.config.js"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/k6-load-testing/#webpack.config.js">#</a> <strong>webpack.config.js</strong></h6>
<pre class="language-json"><code class="language-json">const path = require(<span class="token string">"path"</span>)<br /><br />module.exports = <span class="token punctuation">{</span><br /> mode<span class="token operator">:</span> <span class="token string">"development"</span><span class="token punctuation">,</span><br /> entry<span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"index"</span><span class="token operator">:</span> <span class="token string">"./utils/testing"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> output<span class="token operator">:</span> <span class="token punctuation">{</span><br /> path<span class="token operator">:</span> path.resolve(__dirname<span class="token punctuation">,</span> <span class="token string">"dist"</span>)<span class="token punctuation">,</span><br /> filename<span class="token operator">:</span> <span class="token string">"test.[name].js"</span><span class="token punctuation">,</span><br /> libraryTarget<span class="token operator">:</span> <span class="token string">"commonjs"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> module<span class="token operator">:</span> <span class="token punctuation">{</span><br /> rules<span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">{</span> test<span class="token operator">:</span> /\.js$/<span class="token punctuation">,</span> use<span class="token operator">:</span> <span class="token string">"babel-loader"</span><span class="token punctuation">,</span> exclude<span class="token operator">:</span> /node_modules/ <span class="token punctuation">}</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> stats<span class="token operator">:</span> <span class="token punctuation">{</span><br /> colors<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> warnings<span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> target<span class="token operator">:</span> <span class="token string">"node"</span><span class="token punctuation">,</span><br /> externals<span class="token operator">:</span> <span class="token punctuation">[</span>/k6(\/.*)?/<span class="token punctuation">]</span><span class="token punctuation">,</span><br /> devtool<span class="token operator">:</span> <span class="token string">"source-map"</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span></code></pre>
<p>還有 .babelrc 檔案</p>
<h6 id=".babelrc"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/k6-load-testing/#.babelrc">#</a> <strong>.babelrc</strong></h6>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br /> <span class="token property">"presets"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">[</span><br /> <span class="token string">"@babel/preset-env"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"useBuiltIns"</span><span class="token operator">:</span> <span class="token string">"usage"</span><span class="token punctuation">,</span><br /> <span class="token property">"corejs"</span><span class="token operator">:</span> <span class="token number">3</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">]</span><br /> <span class="token punctuation">]</span><span class="token punctuation">,</span><br /><br /><span class="token punctuation">}</span></code></pre>
<p>稍微調整一下 package.json 中的 script<br />
這邊在 script 中使用的 pretest ,其中的 pre 能夠讓每次在跑 npm run test 這個 script 的時候都會先重新打包。<br />
關於 script 的其他用法可以參考<a href="https://docs.npmjs.com/cli/v7/using-npm/scripts">npm doc</a>。</p>
<h6 id="package.json"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/k6-load-testing/#package.json">#</a> <strong>package.json</strong></h6>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br /> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"functions"</span><span class="token punctuation">,</span><br /> <span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"pretest"</span><span class="token operator">:</span> <span class="token string">"webpack"</span><span class="token punctuation">,</span><br /> <span class="token property">"test"</span><span class="token operator">:</span> <span class="token string">"k6 run ./dist/test.index.js"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /><br /><span class="token punctuation">}</span></code></pre>
<p>設定好打包與編譯的工具後就可以安裝 faker.js 了!</p>
<pre class="language-bash"><code class="language-bash"><span class="token variable">$npm</span> <span class="token function">install</span> faker</code></pre>
<p>這邊先和大家分享一下,在使用 faker.js 套件產生假資料時,筆者踩過的雷 💀<br />
就是,這個套件產生的假資料是有蠻高機會重複的!但是當你產生的資料需要是唯一的時候,該怎麼辦呢 ?!<br />
目前我的解決方式是:</p>
<h6 id="faker.js"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/k6-load-testing/#faker.js">#</a> <strong>faker.js</strong></h6>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> faker <span class="token keyword">from</span> <span class="token string">"faker/locale/en_US"</span><br /><br />faker<span class="token punctuation">.</span><span class="token function">seed</span><span class="token punctuation">(</span>Math<span class="token punctuation">.</span><span class="token function">floor</span><span class="token punctuation">(</span><span class="token number">100000000000000000</span> <span class="token operator">*</span> Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br /><br /><span class="token keyword">export</span> <span class="token punctuation">{</span> faker <span class="token punctuation">}</span></code></pre>
<p>如果大家有更好的解決方式,歡迎分享讓我知道!<br />
接著,設定產生假資料的檔案</p>
<h6 id="generatedata.js"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/k6-load-testing/#generatedata.js">#</a> <strong>generateData.js</strong></h6>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> <span class="token punctuation">{</span>faker<span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"./faker"</span><br /><span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token function-variable function">generateStores</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token literal-property property">contactPerson</span><span class="token operator">:</span> faker<span class="token punctuation">.</span>name<span class="token punctuation">.</span><span class="token function">lastName</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">storeName</span><span class="token operator">:</span> faker<span class="token punctuation">.</span>company<span class="token punctuation">.</span><span class="token function">bsBuzz</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">storeSlug</span><span class="token operator">:</span> faker<span class="token punctuation">.</span>random<span class="token punctuation">.</span><span class="token function">word</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">address</span><span class="token operator">:</span> faker<span class="token punctuation">.</span>address<span class="token punctuation">.</span><span class="token function">country</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">customerServicePhone</span><span class="token operator">:</span> faker<span class="token punctuation">.</span>phone<span class="token punctuation">.</span><span class="token function">phoneNumber</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">account</span><span class="token operator">:</span> faker<span class="token punctuation">.</span>internet<span class="token punctuation">.</span><span class="token function">email</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">password</span><span class="token operator">:</span> faker<span class="token punctuation">.</span>internet<span class="token punctuation">.</span><span class="token function">password</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">companyName</span><span class="token operator">:</span> faker<span class="token punctuation">.</span>company<span class="token punctuation">.</span><span class="token function">bsBuzz</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">liffid</span><span class="token operator">:</span> faker<span class="token punctuation">.</span>datatype<span class="token punctuation">.</span><span class="token function">uuid</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<p>處理完假資料後,就可以開始實作測試檔案!<br />
options 可以讓我們設定如何在 k6 執行測試,以下方範例測試檔為例:</p>
<p>scenarios 是測試的情境<br />
這邊使用了 constant-arrival-rate 來執行,也就是固定一個數量的虛擬用戶在設定的一段時間內執行測試api。</p>
<p>preAllocatedVU 是指在跑測試之前預先配置的虛擬用戶數量,這個設定能夠讓測試執行得更有效率。<br />
其他的設定就會比較好理解了,大意上是指在一分鐘內每隔 15 秒就會去打一次 api。</p>
<p>此外,這邊特邊需要注意的是在k6使用環境變數的方式<br />
以下方的測試檔來說,我使用了 LOCAL_HOSTNAME 這個環境變數,在跑 script 的時候就會需要帶上這個環境變數的值。</p>
<h6 id="package.json-2"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/k6-load-testing/#package.json-2">#</a> <strong>package.json</strong></h6>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br /> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"functions"</span><span class="token punctuation">,</span><br /> <span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"pretest"</span><span class="token operator">:</span> <span class="token string">"webpack"</span><span class="token punctuation">,</span><br /> <span class="token property">"test"</span><span class="token operator">:</span> <span class="token string">"k6 run -e LOCAL_HOSTNAME=test/api ./dist/test.index.js"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /><br /><span class="token punctuation">}</span></code></pre>
<h6 id="testing.js"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/k6-load-testing/#testing.js">#</a> <strong>testing.js</strong></h6>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> http <span class="token keyword">from</span> <span class="token string">"k6/http"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> check<span class="token punctuation">,</span> sleep <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"k6"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> generateStores <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./generateData'</span><span class="token punctuation">;</span><br /><br /><span class="token comment">// define url</span><br /><span class="token keyword">const</span> baseUrl <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">http://</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>__ENV<span class="token punctuation">.</span><span class="token constant">LOCAL_HOSTNAME</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> urls <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">createStore</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>baseUrl<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/store</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">let</span> options <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">scenarios</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">constant_request_rate</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">executor</span><span class="token operator">:</span> <span class="token string">"constant-arrival-rate"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">rate</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">timeUnit</span><span class="token operator">:</span> <span class="token string">"15s"</span><span class="token punctuation">,</span> <span class="token comment">// 1 iterations per 15 second</span><br /> <span class="token literal-property property">duration</span><span class="token operator">:</span> <span class="token string">"1m"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">preAllocatedVUs</span><span class="token operator">:</span> <span class="token number">5</span><span class="token punctuation">,</span> <span class="token comment">// how large the initial pool of VUs would be</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> <span class="token function-variable function">createStoreApi</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> store <span class="token operator">=</span> <span class="token function">generateStores</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> payload <span class="token operator">=</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>store<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">const</span> params <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token string-property property">"Content-Type"</span><span class="token operator">:</span> <span class="token string">"application/json"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">tags</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">k6test</span><span class="token operator">:</span> <span class="token string">"🔥"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> createStoreResult <span class="token operator">=</span> http<span class="token punctuation">.</span><span class="token function">post</span><span class="token punctuation">(</span>urls<span class="token punctuation">.</span>createStore<span class="token punctuation">,</span> payload<span class="token punctuation">,</span> params<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token function">check</span><span class="token punctuation">(</span>createStoreResult<span class="token punctuation">,</span> <span class="token punctuation">{</span><br /> <span class="token string-property property">"status is 200"</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">res</span><span class="token punctuation">)</span> <span class="token operator">=></span> res<span class="token punctuation">.</span>status <span class="token operator">===</span> <span class="token number">200</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token function">createStoreApi</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token function">sleep</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<h2 id="%E5%9F%B7%E8%A1%8C%E7%B5%90%E6%9E%9C"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/k6-load-testing/#%E5%9F%B7%E8%A1%8C%E7%B5%90%E6%9E%9C">#</a> 執行結果</h2>
<p>跑完測試後可以在終端機或是 k6 cloud 看到的資料,透過分析後的數據可以評估 api 反應時間是否能夠滿足需求定義時間,找出可以優化的方向。<br />
下方圖片是終端機跑完測試後印出來的資料。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/k6_result_iterms.png" alt="" /></p>
<p>下方圖片是 k6 cloud 上把資料視覺化後的圖表。<br />
如果想把測試顯示在 k6 cloud 上的話,免費的使用額度是有限制的。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/k6_cloud.png" alt="" /></p>
<h2 id="%E7%B5%90%E8%AA%9E"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/k6-load-testing/#%E7%B5%90%E8%AA%9E">#</a> 結語</h2>
<p>在還沒有流量但又需要測試 api 的效能或是負載時,k6 是一個很不錯的工具!<br />
並且可以根據不同情境做不同測試,想研究其他更多設定的話,可以參考 k6 的文件跟官網:<a href="https://k6.io/docs/">https://k6.io/docs/</a></p>
<p>在閱讀文章時如果有遇到什麼問題,或是有什麼建議,都歡迎留言告訴我,謝謝。😃</p>
初探用 Nest.js 做出一個 CRUD 服務
2021-08-08T00:00:00Z
https://blog.errorbaker.tw/posts/minw/nest-js-intro/
<!-- summary -->
<p>繼 Express 之後已經很久沒有接觸新的後端框架,作為前端工程師這次因為公司專案上要過渡到 Nest 的可能性,而選擇了 Nest.js 作為這次的研究主題,希望可以透過 Nest.js 了解一些常常提到的設計實踐跟成熟工具,並從常見的 CRUD 帶大家走過 Nest 的基本操作。</p>
<!-- summary -->
<!-- more -->
<h2 id="nest-%E7%B0%A1%E4%BB%8B"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/nest-js-intro/#nest-%E7%B0%A1%E4%BB%8B">#</a> Nest 簡介</h2>
<p>參考自官方文件的介紹,我們可以知道 Nest 是一個支援 TypeScript 開發,並結合了 OOP, FP 與 FRP 等開發元素,在底層 Express HTTP Server 框架之上,進行快速開發的 node.js 後端框架。但使用的體感上,我會說相比於 Express, Nest 是一個相對完整、嚴謹遵循著模組化 MVC 架構的後端框架。</p>
<p>在 Nest 之中,從程式架構的角度大致分成三個區塊:</p>
<ul>
<li>Controller: 負責 Request、Response 的處理。</li>
<li>Provicer: 負責商業邏輯的各式 Class e.g. Service、Repository、Factory,其中 Provider 會透過依賴注入的方式來使用。</li>
<li>Module: 彙整 Controller, Provider 等元件的模組 Class。</li>
</ul>
<p>在把架構拉得更遠來看,Nest 基本上是由 Module 層層堆疊組成,最後透過 root module 彙整:</p>
<p><img src="https://docs.nestjs.com/assets/Modules_1.png" alt="" /></p>
<p>粗略瀏覽過常見 Class,接下來我們會先透過簡單的 CRUD 來示範一個 user module 以及使用對應的 typeORM 作為 repository provider 的示範,並在最後補充一些接觸的過程中覺得可以研究的概念。</p>
<h2 id="nest-cli"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/nest-js-intro/#nest-cli">#</a> Nest CLI</h2>
<p>同樣的 Nest 官方提供了 CLI 工具讓我們可以快速開始一個 Nest 專案,而這邊我們會使用 Nest + TypeORM 的配置來進行開發。</p>
<pre class="language-shell"><code class="language-shell"><span class="token function">npm</span> i <span class="token parameter variable">-g</span> @nestjs/cli <br />nest new login-project // 其中會提供 package manager 選擇看要選擇 npm, <span class="token function">yarn</span> 還是其他</code></pre>
<p>從上面的 CLI 就可以簡單得到一個下列專案架構的 Nest 專案:</p>
<pre class="language-shell"><code class="language-shell"><span class="token builtin class-name">.</span><br />├── README.md<br />├── nest-cli.json<br />├── package.json<br />├── src<br />│ ├── app.controller.spec.ts<br />│ ├── app.controller.ts<br />│ ├── app.module.ts<br />│ ├── app.service.ts<br />│ └── main.ts<br />├── <span class="token builtin class-name">test</span><br />│ ├── app.e2e-spec.ts<br />│ └── jest-e2e.json<br />├── tsconfig.build.json<br />├── tsconfig.json<br />└── yarn.lock</code></pre>
<p>其中 src 裡的 <code>app.controller.ts</code>, <code>app.service.ts</code> 與 <code>app.module.ts</code> 就是最一開始的核心模組,在這個時候我們 CLI 輸入 <code>yarn start</code>,到對應頁面會看到預設的 Hello World,這邊運作的方式如下:</p>
<p>在 controller 的架構如下</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Controller<span class="token punctuation">,</span> Get <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> AppService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./app.service'</span><span class="token punctuation">;</span><br /><br /><span class="token decorator"><span class="token at operator">@</span><span class="token function">Controller</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token comment">// 這邊可以輸入路徑相關參數</span><br /><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">AppController</span> <span class="token punctuation">{</span><br /> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token keyword">private</span> <span class="token keyword">readonly</span> appService<span class="token operator">:</span> AppService<span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><br /><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">Get</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token comment">// get method</span><br /> <span class="token function">getHello</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">string</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>appService<span class="token punctuation">.</span><span class="token function">getHello</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 調用 appService 的 getHello() method </span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>接著在 service 裏面撰寫相關的商業邏輯:</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Injectable <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span><br /><br /><span class="token decorator"><span class="token at operator">@</span><span class="token function">Injectable</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token comment">// 由於需要告知 module 這個是依賴注入的 class,所以透過 injectable() 裝飾器裝飾。</span><br /><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">AppService</span> <span class="token punctuation">{</span><br /> <span class="token function">getHello</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">string</span> <span class="token punctuation">{</span> <span class="token comment">// 開發對應的商業邏輯</span><br /> <span class="token keyword">return</span> <span class="token string">'Hello World!'</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>最後在 Module 中引入 service 跟 controller:</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Module <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> AppController <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./app.controller'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> AppService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./app.service'</span><span class="token punctuation">;</span><br /><br /><span class="token decorator"><span class="token at operator">@</span><span class="token function">Module</span></span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> imports<span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> controllers<span class="token operator">:</span> <span class="token punctuation">[</span>AppController<span class="token punctuation">]</span><span class="token punctuation">,</span><br /> providers<span class="token operator">:</span> <span class="token punctuation">[</span>AppService<span class="token punctuation">]</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><br /><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">AppModule</span> <span class="token punctuation">{</span><span class="token punctuation">}</span></code></pre>
<p>這樣就是一個對 root route 發 get request 並回傳 Hello World API 完成了。</p>
<h2 id="%E4%BD%BF%E7%94%A8-typeorm"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/nest-js-intro/#%E4%BD%BF%E7%94%A8-typeorm">#</a> 使用 TypeORM</h2>
<p>目前有一個簡單的 API,但我們還沒有任何的 DB 資料,所以這邊我們先透過 Nest 官方支援的 TypeORM 來建立 DB。</p>
<p>首先安裝 TypeORM 相關的內容,並使用 typeorm init 來快速建立 typeorm 專案 :</p>
<pre class="language-shell"><code class="language-shell"><span class="token function">yarn</span> <span class="token function">add</span> @nestjs/typeorm typeorm mysql2<br />npx typeorm init</code></pre>
<p>這時候會發現我們的檔案結構有一些改變,多了 entity, migration 與 ormconfig.json。</p>
<pre class="language-shell"><code class="language-shell"><span class="token builtin class-name">.</span><br />├── ormconfig.json<br />├── src<br />│ ├── <span class="token punctuation">..</span>.<br />│ ├── entity<br />│ │ └── User.ts<br />│ └── migration<br />├─ <span class="token punctuation">..</span>.</code></pre>
<p>這邊 entity 是用來定義 Table 的結構,而 migration 則是 DB 更動的版本紀錄,所以簡潔一點我們可以直接接著使用 typeorm 提供的 ormconfig.json 內的設定,在 module 中引入。</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Module <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> TypeOrmModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/typeorm'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> AppController <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./app.controller'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> AppService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./app.service'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> Connection <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'typeorm'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> getConnectionOptions <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'typeorm'</span><span class="token punctuation">;</span><br /><br /><span class="token decorator"><span class="token at operator">@</span><span class="token function">Module</span></span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> imports<span class="token operator">:</span> <span class="token punctuation">[</span><br /> TypeOrmModule<span class="token punctuation">.</span><span class="token function">forRootAsync</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token function-variable function">useFactory</span><span class="token operator">:</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span><br /> Object<span class="token punctuation">.</span><span class="token function">assign</span><span class="token punctuation">(</span><span class="token keyword">await</span> <span class="token function">getConnectionOptions</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br /> autoLoadEntities<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token punctuation">]</span><span class="token punctuation">,</span><br /> controllers<span class="token operator">:</span> <span class="token punctuation">[</span>AppController<span class="token punctuation">]</span><span class="token punctuation">,</span><br /> providers<span class="token operator">:</span> <span class="token punctuation">[</span>AppService<span class="token punctuation">]</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><br /><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">AppModule</span> <span class="token punctuation">{</span><br /> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token keyword">private</span> connection<span class="token operator">:</span> Connection<span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>要注意的是,由於這邊我們是使用 ormconfig 在處理時間上,我們 entity 實際使用的是 ts 編譯後在 dist 中的 entity 檔案,所以在 ormconfig 中我們可以略作修改,將原先的 src 位置修改為 dist,如果今天有 migration 或 subscriber 也同理 [^1]。</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br /> <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"mysql"</span><span class="token punctuation">,</span><br /> ...<br /> <span class="token property">"synchronize"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> <span class="token property">"logging"</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> <span class="token property">"entities"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token string">"dist/**/*.entity.js"</span><br /> <span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token property">"migrations"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token string">"src/migration/**/*.ts"</span><br /> <span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token property">"subscribers"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token string">"src/subscriber/**/*.ts"</span><br /> <span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token property">"cli"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"entitiesDir"</span><span class="token operator">:</span> <span class="token string">"src/entity"</span><span class="token punctuation">,</span><br /> <span class="token property">"migrationsDir"</span><span class="token operator">:</span> <span class="token string">"src/migration"</span><span class="token punctuation">,</span><br /> <span class="token property">"subscribersDir"</span><span class="token operator">:</span> <span class="token string">"src/subscriber"</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /></code></pre>
<h2 id="crud"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/nest-js-intro/#crud">#</a> CRUD</h2>
<p>基礎內容設定完後,我們可以自行起一個 mysql server 來放資料,也可以透過 migration 來自行產生 table,不過這邊我們先從簡起一個 mysql Container,並在裡面創建我們想要的 DB 跟資料,接著回到 Nest 之中,我們可以沿用原先 init 出來的 user entity 來初步介紹 Entity 的內容:</p>
<p>這邊 Entity 我們可以透過不同的裝飾器來定義資料的特性,其中裝飾器都可以帶入諸如 <code>nullable</code> 這樣的參數去設定 Column 的內容。</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Entity<span class="token punctuation">,</span> PrimaryGeneratedColumn<span class="token punctuation">,</span> Column <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'typeorm'</span><span class="token punctuation">;</span><br /><br /><span class="token decorator"><span class="token at operator">@</span><span class="token function">Entity</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token punctuation">{</span><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">PrimaryGeneratedColumn</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token comment">// 自動產生的 primary key</span><br /> id<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">;</span> <span class="token comment">// 資料型態</span><br /><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">Column</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> firstName<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">Column</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> lastName<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">Column</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> age<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>接著我們可以簡單的建立一個 User 的 CRUD Service,其中 Repository 是 TypeORM 提供的 Provider,我們可以透過 nest 官方提供的 InjectRepository 來引入對應的 User 資料對應,並且透過 Nest 規範的依賴注入的方式。</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Injectable<span class="token punctuation">,</span> HttpException<span class="token punctuation">,</span> HttpStatus <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> InjectRepository <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/typeorm'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> Repository <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'typeorm'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> User <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../entity/User.entity'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> UpdateUserDTO <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./dto/update-student.dto'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> CreateUserDTO <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./dto/create-user.dto'</span><span class="token punctuation">;</span><br /><br /><span class="token decorator"><span class="token at operator">@</span><span class="token function">Injectable</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">UserService</span> <span class="token punctuation">{</span><br /> <span class="token function">constructor</span><span class="token punctuation">(</span><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">InjectRepository</span></span><span class="token punctuation">(</span>User<span class="token punctuation">)</span> <span class="token comment">// nest 官方提供的 InjectRepository 來注入 TypeORM repository</span><br /> <span class="token keyword">private</span> usersRepository<span class="token operator">:</span> Repository<span class="token operator"><</span>User<span class="token operator">></span><span class="token punctuation">,</span> <span class="token comment">// 宣告依賴</span><br /> <span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><br /><br /> <span class="token keyword">async</span> <span class="token function">createUser</span><span class="token punctuation">(</span>createUserDTO<span class="token operator">:</span> CreateUserDTO<span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator"><</span>User<span class="token operator">></span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>usersRepository<span class="token punctuation">.</span><span class="token function">save</span><span class="token punctuation">(</span>createUserDTO<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">async</span> <span class="token function">findUserById</span><span class="token punctuation">(</span>id<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator"><</span>User <span class="token operator">|</span> <span class="token keyword">undefined</span><span class="token operator">></span> <span class="token punctuation">{</span><br /> <span class="token keyword">let</span> user<span class="token operator">:</span> User<span class="token punctuation">;</span><br /> <span class="token keyword">try</span> <span class="token punctuation">{</span><br /> user <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>usersRepository<span class="token punctuation">.</span><span class="token function">findOne</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>user<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">HttpException</span><span class="token punctuation">(</span><span class="token string">'user not found'</span><span class="token punctuation">,</span> HttpStatus<span class="token punctuation">.</span><span class="token constant">BAD_REQUEST</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>err<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">HttpException</span><span class="token punctuation">(</span>err<span class="token punctuation">,</span> HttpStatus<span class="token punctuation">.</span><span class="token constant">INTERNAL_SERVER_ERROR</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> user<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">async</span> <span class="token function">updateUser</span><span class="token punctuation">(</span>id<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">,</span> updateUserDTO<span class="token operator">:</span> UpdateUserDTO<span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator"><</span><span class="token builtin">boolean</span><span class="token operator">></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> foundUser <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>usersRepository<span class="token punctuation">.</span><span class="token function">findOne</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>foundUser<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">HttpException</span><span class="token punctuation">(</span><span class="token string">'user not found'</span><span class="token punctuation">,</span> <span class="token number">404</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>usersRepository<span class="token punctuation">.</span><span class="token function">save</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token operator">...</span>foundUser<span class="token punctuation">,</span><br /> <span class="token operator">...</span>updateUserDTO<span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br /> <span class="token operator">?</span> <span class="token boolean">true</span><br /> <span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">async</span> <span class="token function">deleteUser</span><span class="token punctuation">(</span>id<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator"><</span><span class="token builtin">boolean</span><span class="token operator">></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> foundUser <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>usersRepository<span class="token punctuation">.</span><span class="token function">findOne</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>foundUser<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">HttpException</span><span class="token punctuation">(</span><span class="token string">'User not found.'</span><span class="token punctuation">,</span> <span class="token number">404</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>usersRepository<span class="token punctuation">.</span><span class="token function">delete</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">?</span> <span class="token boolean">true</span> <span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /></code></pre>
<p>這個過程中我們可以透過 DTO 去要求更新的資料格式,以 create User DTO 為例,而 DTO 的錯誤判斷也可以透過前面提到資料流中提供的不同 Class 去做錯誤處理。</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> IsString<span class="token punctuation">,</span> IsOptional<span class="token punctuation">,</span> IsNotEmpty <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'class-validator'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">CreateUserDTO</span> <span class="token punctuation">{</span><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">IsString</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">IsNotEmpty</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">readonly</span> firstName<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">IsNotEmpty</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">IsString</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">readonly</span> lastname<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">IsOptional</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">readonly</span> age<span class="token operator">:</span> numbder<span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /></code></pre>
<p>最後我們在 user controller 中宣告 userService 依賴,並且利用 Nest 提供的 Express 方法,來對應不同的 Method 調用 userService。</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span><br /> Controller<span class="token punctuation">,</span><br /> Response<span class="token punctuation">,</span><br /> Get<span class="token punctuation">,</span><br /> HttpStatus<span class="token punctuation">,</span><br /> Param<span class="token punctuation">,</span><br /> Patch<span class="token punctuation">,</span><br /> Body<span class="token punctuation">,</span><br /> Delete<span class="token punctuation">,</span><br /> Post<span class="token punctuation">,</span><br /><span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> UserService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./user.service'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> UpdateUserDTO <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./dto/update-student.dto'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> CreateUserDTO <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./dto/create-user.dto'</span><span class="token punctuation">;</span><br /><br /><span class="token decorator"><span class="token at operator">@</span><span class="token function">Controller</span></span><span class="token punctuation">(</span><span class="token string">'users'</span><span class="token punctuation">)</span> <span class="token comment">// 設定 route e.g. localhost:3000/users</span><br /><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">UserController</span> <span class="token punctuation">{</span><br /> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token keyword">private</span> userService<span class="token operator">:</span> UserService<span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><br /><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">Post</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token comment">// 設定 method</span><br /> <span class="token keyword">async</span> <span class="token function">createUser</span><span class="token punctuation">(</span><span class="token decorator"><span class="token at operator">@</span><span class="token function">Body</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span> createUserDTO<span class="token operator">:</span> CreateUserDTO<span class="token punctuation">,</span> <span class="token decorator"><span class="token at operator">@</span><span class="token function">Response</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span> res<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">try</span> <span class="token punctuation">{</span><br /> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>userService<span class="token punctuation">.</span><span class="token function">createUser</span><span class="token punctuation">(</span>createUserDTO<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> res<span class="token punctuation">.</span><span class="token function">status</span><span class="token punctuation">(</span>HttpStatus<span class="token punctuation">.</span><span class="token constant">OK</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">{</span> status<span class="token operator">:</span> <span class="token string">'success'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span>error<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> res<span class="token punctuation">.</span><span class="token function">status</span><span class="token punctuation">(</span>HttpStatus<span class="token punctuation">.</span><span class="token constant">INTERNAL_SERVER_ERROR</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">Get</span></span><span class="token punctuation">(</span><span class="token string">'/:id'</span><span class="token punctuation">)</span><br /> <span class="token keyword">async</span> <span class="token function">getUser</span><span class="token punctuation">(</span><span class="token decorator"><span class="token at operator">@</span><span class="token function">Response</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span> res<span class="token punctuation">,</span> <span class="token decorator"><span class="token at operator">@</span><span class="token function">Param</span></span><span class="token punctuation">(</span><span class="token string">'id'</span><span class="token punctuation">)</span> id<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">try</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> user <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>userService<span class="token punctuation">.</span><span class="token function">findUserById</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> res<span class="token punctuation">.</span><span class="token function">status</span><span class="token punctuation">(</span>HttpStatus<span class="token punctuation">.</span><span class="token constant">OK</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span>user<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span>error<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> res<span class="token punctuation">.</span><span class="token function">status</span><span class="token punctuation">(</span>HttpStatus<span class="token punctuation">.</span><span class="token constant">INTERNAL_SERVER_ERROR</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">Patch</span></span><span class="token punctuation">(</span><span class="token string">'/:id'</span><span class="token punctuation">)</span><br /> <span class="token keyword">async</span> <span class="token function">updateUser</span><span class="token punctuation">(</span><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">Response</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span> res<span class="token punctuation">,</span><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">Param</span></span><span class="token punctuation">(</span><span class="token string">'id'</span><span class="token punctuation">)</span> id<span class="token punctuation">,</span><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">Body</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span> updateUserDTO<span class="token operator">:</span> UpdateUserDTO<span class="token punctuation">,</span><br /> <span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">try</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> isUpdate <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>userService<span class="token punctuation">.</span><span class="token function">updateUser</span><span class="token punctuation">(</span>id<span class="token punctuation">,</span> updateUserDTO<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> res<span class="token punctuation">.</span><span class="token function">status</span><span class="token punctuation">(</span>HttpStatus<span class="token punctuation">.</span><span class="token constant">OK</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">{</span> status<span class="token operator">:</span> isUpdate <span class="token operator">?</span> <span class="token string">'success'</span> <span class="token operator">:</span> <span class="token string">'fail'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> res<span class="token punctuation">.</span><span class="token function">status</span><span class="token punctuation">(</span>HttpStatus<span class="token punctuation">.</span><span class="token constant">INTERNAL_SERVER_ERROR</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">Delete</span></span><span class="token punctuation">(</span><span class="token string">'/:id'</span><span class="token punctuation">)</span><br /> <span class="token keyword">async</span> <span class="token function">deleteUser</span><span class="token punctuation">(</span><span class="token decorator"><span class="token at operator">@</span><span class="token function">Response</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span> res<span class="token punctuation">,</span> <span class="token decorator"><span class="token at operator">@</span><span class="token function">Param</span></span><span class="token punctuation">(</span><span class="token string">'id'</span><span class="token punctuation">)</span> id<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">try</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> isDelete <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>userService<span class="token punctuation">.</span><span class="token function">deleteUser</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> res<span class="token punctuation">.</span><span class="token function">status</span><span class="token punctuation">(</span>HttpStatus<span class="token punctuation">.</span><span class="token constant">OK</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">{</span> status<span class="token operator">:</span> isDelete <span class="token operator">?</span> <span class="token string">'success'</span> <span class="token operator">:</span> <span class="token string">'fail'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> res<span class="token punctuation">.</span><span class="token function">status</span><span class="token punctuation">(</span>HttpStatus<span class="token punctuation">.</span><span class="token constant">INTERNAL_SERVER_ERROR</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>最後透過 user.module 去做彙整,要注意的是 TypeOrmModule 需要另外透過 forFeature 來引入 User Model。</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Module <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> TypeOrmModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/typeorm'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> UserController <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./user.controller'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> UserService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./user.service'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> User <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./entities/user.entity'</span><span class="token punctuation">;</span><br /><br /><span class="token decorator"><span class="token at operator">@</span><span class="token function">Module</span></span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> imports<span class="token operator">:</span> <span class="token punctuation">[</span>TypeOrmModule<span class="token punctuation">.</span><span class="token function">forFeature</span><span class="token punctuation">(</span><span class="token punctuation">[</span>User<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> controllers<span class="token operator">:</span> <span class="token punctuation">[</span>UserController<span class="token punctuation">]</span><span class="token punctuation">,</span><br /> providers<span class="token operator">:</span> <span class="token punctuation">[</span>UserService<span class="token punctuation">]</span><span class="token punctuation">,</span><br /> exports<span class="token operator">:</span> <span class="token punctuation">[</span>UserService<span class="token punctuation">]</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><br /><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">UserModule</span> <span class="token punctuation">{</span><span class="token punctuation">}</span></code></pre>
<p>最終在 app.module 加入我們的 user.module 即大功告成:</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Module <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> TypeOrmModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/typeorm'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> AppController <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./app.controller'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> AppService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./app.service'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> UserModule <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./modules/user.module'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> Connection <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'typeorm'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> getConnectionOptions <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'typeorm'</span><span class="token punctuation">;</span><br /><br /><span class="token decorator"><span class="token at operator">@</span><span class="token function">Module</span></span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> imports<span class="token operator">:</span> <span class="token punctuation">[</span><br /> TypeOrmModule<span class="token punctuation">.</span><span class="token function">forRootAsync</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token function-variable function">useFactory</span><span class="token operator">:</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span><br /> Object<span class="token punctuation">.</span><span class="token function">assign</span><span class="token punctuation">(</span><span class="token keyword">await</span> <span class="token function">getConnectionOptions</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br /> autoLoadEntities<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> UserModule<br /> <span class="token punctuation">]</span><span class="token punctuation">,</span><br /> controllers<span class="token operator">:</span> <span class="token punctuation">[</span>AppController<span class="token punctuation">]</span><span class="token punctuation">,</span><br /> providers<span class="token operator">:</span> <span class="token punctuation">[</span>AppService<span class="token punctuation">]</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><br /><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">AppModule</span> <span class="token punctuation">{</span><br /> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token keyword">private</span> connection<span class="token operator">:</span> Connection<span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span></code></pre>
<p>至此我們 src 檔案結構如下:</p>
<pre class="language-ts"><code class="language-ts"><span class="token punctuation">.</span><br />├── src<br />│ ├── app<span class="token punctuation">.</span>controller<span class="token punctuation">.</span>spec<span class="token punctuation">.</span>ts<br />│ ├── app<span class="token punctuation">.</span>controller<span class="token punctuation">.</span>ts<br />│ ├── app<span class="token punctuation">.</span>module<span class="token punctuation">.</span>ts<br />│ ├── app<span class="token punctuation">.</span>service<span class="token punctuation">.</span>ts<br />│ ├── entity<br />│ │ └── User<span class="token punctuation">.</span>ts<br />│ ├── index<span class="token punctuation">.</span>ts<br />│ ├── main<span class="token punctuation">.</span>ts<br />│ ├── migration<br />│ └── modules<br />│ └── user<br />│ ├── user<span class="token punctuation">.</span>controller<span class="token punctuation">.</span>ts<br />│ ├── user<span class="token punctuation">.</span>module<span class="token punctuation">.</span>ts<br />│ └── user<span class="token punctuation">.</span>service<span class="token punctuation">.</span>ts</code></pre>
<p>以上就是一個最簡單的 CRUD Server,這個過程我們也可以透過 Nest CLI e.g. <code>nest g controller user</code> 來幫我們建立各個 Class 基本模板,但這邊還是透過一個個檔案走過來了解 Nest 的架構。</p>
<h2 id="nest-%E4%B8%AD%E9%87%8D%E8%A6%81%E8%A7%80%E5%BF%B5"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/nest-js-intro/#nest-%E4%B8%AD%E9%87%8D%E8%A6%81%E8%A7%80%E5%BF%B5">#</a> Nest 中重要觀念</h2>
<p>其中 Nest 在接觸時有一些過去比較沒有接觸到的觀念,在下方一一去做研究跟補充:</p>
<h3 id="%E4%BE%9D%E8%B3%B4%E6%B3%A8%E5%85%A5"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/nest-js-intro/#%E4%BE%9D%E8%B3%B4%E6%B3%A8%E5%85%A5">#</a> 依賴注入</h3>
<p>在進入說明資料流之前,這邊想要了解一下依賴注入 (DI) 的概念 :</p>
<p>今天 A, B 兩 Class,A Class 內有直接產生 B Class 的實例,所以 A 依賴 B,如果今天 B 有修改、A 也要跟著改。<br />
所以如果今天想要解決這個問題,做到所謂的「控制反轉」(IoC) 其中一種做法透過一個注入容器 C Class 來負責建立 B 的內容,讓 A 使用 C 來呼叫 B。這樣就不用因為 B 的修改影響到 A 的內容,因為 C 這個中間容器會去處理要依賴對象的調度。</p>
<p>用一個比喻來說,今天 A 跟 B 是情侶,A 是黏黏怪所以依賴 B,但如果今天 A、B 是一個合情合意的開放式關係,B 讓 A 使用交友軟體,那今天 A 只要跟交友軟體輸入自己的偏好,然後找到喜歡的人就可以了。此時的「交友軟體」就是注入容器,負責接收 A 發出的需求,找到像 B 一樣的人。</p>
<p>而用 Nest 的狀況來說今天過去我們要在 Controller 中直接創建 Service 的實例,但 Nest 透過 Module 這個中間 IoC 容器幫我們處理了 Service 的宣告,我們只要讓 Controller 引入對應的 Interface 並宣告 [^2]。</p>
<p>首先在 user.service.ts 建立 user 相關的操作邏輯,例如:回傳所有 users 的 <code>findAll()</code>。這邊由於 usersService 透過 @Injectable 修飾器修飾過後,宣告 usersService 是可以被 Module 做 IoC 處理的 Container。</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Injectable <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> User <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./interfaces/user.interface'</span><span class="token punctuation">;</span><br /><br /><span class="token decorator"><span class="token at operator">@</span><span class="token function">Injectable</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">usersService</span> <span class="token punctuation">{</span><br /> <span class="token keyword">private</span> <span class="token keyword">readonly</span> users<span class="token operator">:</span> User<span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br /><br /> <span class="token function">findAll</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> User<span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>users<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>接著在 user.controller.ts 中提供 service Class 在 constructor 中宣告依賴。</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Controller<span class="token punctuation">,</span> Get <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> UsersService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./users.service'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> User <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./interfaces/user.interface'</span><span class="token punctuation">;</span><br /><br /><span class="token decorator"><span class="token at operator">@</span><span class="token function">Controller</span></span><span class="token punctuation">(</span><span class="token string">'users'</span><span class="token punctuation">)</span><br /><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">UsersController</span> <span class="token punctuation">{</span><br /> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token keyword">private</span> usersService<span class="token operator">:</span> UsersService<span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><br /><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">Get</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">async</span> <span class="token function">findAll</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator"><</span>User<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token operator">></span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>usersService<span class="token punctuation">.</span><span class="token function">findAll</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>最後在 user.module.ts 這個 IoC 容器中引入會使用到的 Provider - usersService 來做依賴注入。當 Nest 要創建 UsersController 的實例的時候,他會先確認有沒有任何依賴的內容在 constructor 中,若有,他會搜尋有沒有 UsersService 的內容在 IoC 中,若有,才會去創建 UsersService 的實例來回傳。</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Module <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/common'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> UsersController <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./users/users.controller'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> UsersService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./users/users.service'</span><span class="token punctuation">;</span><br /><br /><span class="token decorator"><span class="token at operator">@</span><span class="token function">Module</span></span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> controllers<span class="token operator">:</span> <span class="token punctuation">[</span>UsersController<span class="token punctuation">]</span><span class="token punctuation">,</span><br /> providers<span class="token operator">:</span> <span class="token punctuation">[</span>UsersService<span class="token punctuation">]</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><br /><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">AppModule</span> <span class="token punctuation">{</span><span class="token punctuation">}</span></code></pre>
<p>這樣我們就可以隨時修改或增添相關的 Provider 並透過中間的 module 去做管理,上面的原始碼會再整理到<a href="https://github.com/ishin4554/playground/tree/js-node_nest">這邊</a> 供大家做參考。</p>
<h3 id="entity-vs-dto"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/nest-js-intro/#entity-vs-dto">#</a> Entity vs DTO</h3>
<p>在定義回傳或傳入的型別時,一直很猶豫要使用 Entity 還是 DTO? 這兩者又有什麼不同?根據 Stack Overflow 的討論 [^3]</p>
<blockquote>
<p>Entities may be part of a business domain. Thus, they can implement behavior and be applied to different use cases within the domain. DTOs are used only to transfer data from one process or context to another. As such, they are without behavior - except for very basic and usually standardised storage and retrieval functions.</p>
</blockquote>
<p>我的理解是,最能代表 DB 邏輯的型別是 Entity,而用於資料流一路通過不同階段的型態,則用 DTO。而於開發應用情境上來說,最理想的應該是 request 從 controller 一路到 service 都使用 DTO 去定義規範,但要送進 DB 或剛從 DB 撈出來的時候應維持 Entity 的型別。</p>
<p>以上是個人的理解,如果有一些疏漏再請大家多多補充。</p>
<h2 id="%E5%B0%8F%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/nest-js-intro/#%E5%B0%8F%E7%B5%90">#</a> 小結</h2>
<p>以上是 Nest CRUD 的初探索,而其實 Nest 在換一個角度,根據資料流還有提供許多我們可以參考這張圖來看一趟資料流會經過哪些階段:</p>
<p><img src="https://i.stack.imgur.com/2lFhd.jpg" alt="" /></p>
<p>下一篇文章希望可以跟著資料流,接著探索 Nest 提供的其他 Class,並基於 User Service 延伸出一個簡單的 Auth 服務。</p>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/nest-js-intro/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li><a href="https://docs.nestjs.com/">Documentation | NestJS - A progressive Node.js framework</a></li>
<li>[^1]: <a href="https://stackoverflow.com/questions/59435293/typeorm-entity-in-nestjs-cannot-use-import-statement-outside-a-module">TypeORM Entity in NESTJS - Cannot use import statement outside a module - Stack Overflow</a></li>
<li>[^2]: <a href="https://www.jianshu.com/p/8c1cb18d219c">3、Nest.js 中的依赖注入与提供者 - 简书</a></li>
<li>[^3]: <a href="https://stackoverflow.com/questions/39397147/difference-between-entity-and-dto">java - Difference between Entity and DTO - Stack Overflow</a></li>
<li>[^4]: <a href="https://stackoverflow.com/questions/63244163/nestjs-do-i-need-dtos-along-with-entities">node.js - NestJS Do I need DTO's along with entities? - Stack Overflow</a></li>
</ul>
一段的 React-markdown 研究之旅
2021-08-08T00:00:00Z
https://blog.errorbaker.tw/posts/sixwings/survey-about-react-markdown/
<!-- summary -->
<p>因為在學習系統的進度報告出現了奇怪的排版而困惑不已的我,踏上了研究 React-markdown 的旅途。</p>
<!-- summary -->
<!-- more -->
<h1 id="%E5%89%8D%E6%83%85%E6%8F%90%E8%A6%81"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/survey-about-react-markdown/#%E5%89%8D%E6%83%85%E6%8F%90%E8%A6%81">#</a> 前情提要</h1>
<p>事情是這樣子的,參加 Lidemy <a href="https://bootcamp.lidemy.com/">程式導師計畫</a>的學生需要定期在進度報告中發表學習進度、心得,白話一點就是可以用 markdown 語法的留言板這樣。我平常是先在 Obsidian 筆記軟體內把當天的進度報告寫好再轉貼上去,事件的起因是我使用了以下的語法:</p>
<pre class="language-md"><code class="language-md"><span class="token list punctuation">*</span> test<br />blahblah...</code></pre>
<p>原本預期出現的結果:<br />
<img src="https://blog.errorbaker.tw/img/posts/sixwings/hackmd.png" alt="" /></p>
<p>但實際結果卻是:<br />
<img src="https://blog.errorbaker.tw/img/posts/sixwings/learing_sys.png" alt="" /></p>
<p>十分醜陋的段落岔了出來,之前寫進度報告的時候也有發現這個狀況,但當天就特別好奇這背後的原因是什麼?為此我把上面那段語法丟到 hackMD 上測試,結果為前者,那時候以為是系統 BUG 還是什麼問題所以拿去問老師。後來老師說學習系統的 markdown 語法是 <a href="https://github.github.com/gfm/">GitHub Flavored Markdown (GFM)</a> 然後說<a href="https://github.com/Lidemy/lidemy-learning-frontend">學習系統有開源程式碼</a>可以自己研究看看唷。於是我就展開了今天的不歸路</p>
<h1 id="%E6%8E%A2%E7%A9%B6"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/survey-about-react-markdown/#%E6%8E%A2%E7%A9%B6">#</a> 探究</h1>
<p>經過一番做功課時間後。得知學習系統採用的框架是 react,然後是透過 react-markdown 套件處理 markdown 格式的資料。react-markdown 遵循的 markdown 標準為 <a href="https://spec.commonmark.org/current/">CommonMark</a> ,然後額外支援 <a href="https://github.github.com/gfm/">GitHub Flavored Markdown</a> (GFM) 語法,新增了刪除線、表格、TODO list、直接URL等功能。</p>
<p>react-markdown 經過了<a href="https://github.com/remarkjs/react-markdown#architecture">好幾道步驟</a>把 markdown 語法轉換成 html 語言。然後也很好心提供了<a href="https://remarkjs.github.io/react-markdown/">線上語法測試</a></p>
<h1 id="%E9%87%8D%E7%8F%BE"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/survey-about-react-markdown/#%E9%87%8D%E7%8F%BE">#</a> 重現</h1>
<p>測試字串還是最前面的那段,一開始在線上測試語法的時候遇到的問題是它沒有顯示換行,為了搞清楚換行問題還去翻 <a href="https://spec.commonmark.org/0.29/">CommonMark</a> 規格書看它是怎麼處理換行的。學習系統是套用樣式 <code>white-space: pre-wrap</code> 所以可以看到換行效果。</p>
<p>然後看到線上測試結果的時候我一脸懵逼,奇怪是正常的阿?後來不信邪,花了一些時間在電腦上實際建立 react 環境去跑,結果也是正常的,我整個滿頭問號。</p>
<p>推測原因可能是 react-markdown 版本不對,或是 remark-gfm 插件造成 parser 的行為改變。後來實測證實是 remark-gfm 造成的問題,同時也在 remark-gfm 套件庫的 <a href="https://github.com/remarkjs/remark-gfm#important">README.md</a> 獲得證實。研究了好幾天的東西終於可以落幕了,那時候如果可以先看一下說明文件或許就可以早點發現問題吧。</p>
<p>事後我寫作的方式是使用有縮排的寫法,這在其他地方的 markdown 顯示就都是正常的了</p>
<pre class="language-md"><code class="language-md"><span class="token list punctuation">*</span> test<br /> blahblah...</code></pre>
<h1 id="%E5%9B%9E%E9%A1%A7"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/survey-about-react-markdown/#%E5%9B%9E%E9%A1%A7">#</a> 回顧</h1>
<p>這段過程除了「以後在 list 下面一行要記得補縮排」,其實還學習到蠻多東西的。</p>
<p>從 CommonMark 規範的誕生原因,可以知道有一份 Markdown 規格對大家來說還是蠻重要的。也從規格書中感受到 CommonMark 確實想要維持「肉眼看到看到什麼就應該長那個樣子」的精神,但仍然要克服克種奇奇怪怪的特殊情況。從規格書中提供的 example 可以感受到,處理格式真的是不容易的事情。</p>
<p>雖然現在各家 markdown 軟體都說 <strong>100% 完全相容 CommonMark</strong>,但或多或少都加入了一些便利功能,或是懶人語法之類的東西。這點在不同 markdown 平台切換的時候可能會發生問題,造成最後顯示出來的結果不一樣。建議使用 markdown 語法的時候可以先確認一下自己是用 CommonMark、GFM 或是 markdown 軟體自己提供的原生功能。</p>
<p>我是 sixwings,追尋技術的程式人,我們下次見 :)</p>
從本地到雲端,淺談部署和各種部署姿勢
2021-08-12T00:00:00Z
https://blog.errorbaker.tw/posts/lavi/deploy-and-cloud/
<h2 id="deploy-%E6%98%AF%E4%BB%80%E9%BA%BC"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/deploy-and-cloud/#deploy-%E6%98%AF%E4%BB%80%E9%BA%BC">#</a> Deploy 是什麼</h2>
<!-- summary -->
<p>如果拿 Server 來舉例(本篇主要講的內容也是指 server), server 基本上就是一個程式,而這個程式運行之後其他人就可以透過網路連線到 Server 來拿資料。而 Deploy,也就是部署,可以視為將已經寫好的程式使其運行的過程。</p>
<!-- summary -->
<!-- more -->
<p>可以這樣想,你今天在工廠製造了一台飲料販賣機,把這台販賣機搬到它應該被使用的位置,可能是籃球場旁邊,或者是河濱公園。但你的任務並不只是把它搬過去而已,所以你可能還要找電源幫它插電,甚至第一次還會需要幫它補充飲料,做一些設定等等。這些流程都在部署的範圍。</p>
<p>但是在軟體上面的部署不用插電,而是需要設定我們使用的電腦,像是安裝作業意系統等,並且在電腦上處理好執行軟體需要的一些設定、或者是執行環境,最後在電腦上執行自己開發的程式。</p>
<h2 id="%E9%83%A8%E7%BD%B2%E6%96%B9%E5%BC%8F%E7%9A%84%E5%BD%B1%E9%9F%BF"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/deploy-and-cloud/#%E9%83%A8%E7%BD%B2%E6%96%B9%E5%BC%8F%E7%9A%84%E5%BD%B1%E9%9F%BF">#</a> 部署方式的影響</h2>
<p>延續剛剛的比喻,你的飲料販賣機可能以不同的方式供電,<code>220V</code> 或者是 <code>110V</code>,甚至也可能可以吃直流電(比喻),但這些都不影響販賣機的功能還有提供的服務。不過不同的供電方式可能會帶來不同的優缺點像是攜帶性、經濟性等等。部署也一樣,不同的方式帶來的差異並不會影響 Server 的功能,但會影響到的會是:</p>
<ul>
<li>你的荷包或者說你老闆的荷包</li>
<li>可擴充性</li>
<li>部署的複雜度</li>
<li>穩定度</li>
<li>還有很多很多...</li>
</ul>
<p>而這些就是在選擇部署方式時需要去做的 Trade-off。那下面會簡單的介紹不同部署方式的一些特性,以及提一下有哪些產品屬於這些方式。</p>
<h2 id="%E5%90%84%E7%A8%AE%E4%B8%8D%E5%90%8C%E7%9A%84%E9%83%A8%E7%BD%B2%E6%96%B9%E5%BC%8F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/deploy-and-cloud/#%E5%90%84%E7%A8%AE%E4%B8%8D%E5%90%8C%E7%9A%84%E9%83%A8%E7%BD%B2%E6%96%B9%E5%BC%8F">#</a> 各種不同的部署方式</h2>
<h3 id="%E6%9C%AC%E5%9C%B0"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/deploy-and-cloud/#%E6%9C%AC%E5%9C%B0">#</a> 本地</h3>
<p>簡單說就是部署在你自己的電腦,一台真正你看得到、摸的到,還要幫它插電插網路線的電腦。當然現在已經比較少人這麼幹,大家都丟在各種雲端服務上面,但這麼 old school 的方式還是有他永遠無法取代的特點。</p>
<blockquote>
<p>你擁有自己的程式,而不是掌控在別人的手上</p>
</blockquote>
<p>把自己的東西緊握在手中,難道不是一種浪漫嗎?</p>
<p>當然,浪漫要付出的代價可不小,得處理很多問題:像最初需要面臨的就是固定 IP、找個不會被踢到的地方放你的電腦、穩定的供電來源等等。其實如果能解決,部署一些小型的服務像 Blog 等並不是太差的選擇,而且還可以真正的去學習如何去設定 Linux、安裝環境等。</p>
<p>但如果你只是想要簡單弄一個部落格,甚至是有商業上的需求,這已經不是推薦的作法了。畢竟雲端部署已經方便的太多。</p>
<h3 id="%E9%9B%B2%E7%AB%AF%E4%B8%BB%E6%A9%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/deploy-and-cloud/#%E9%9B%B2%E7%AB%AF%E4%B8%BB%E6%A9%9F">#</a> 雲端主機</h3>
<p>這就是很常聽到的 IaaS (Infrastructure as a Service,基礎建設即服務),基本上就是租電腦。你不用真正買一台電腦,選好自己需要的規格,像是 CPU 的核心數、記憶體的容量、硬碟的容量等,就能夠建立 instance(instance 就是我們租的電腦)。就像自己的電腦一樣,可以讓 server、資料庫或者是任何的程式在上面運行,雲端有很多好處:</p>
<ol>
<li>方便,你不用插電。辦個會員填個信用卡資料就好</li>
<li>可靠性,你不會踢到插頭或者是機櫃</li>
<li>便宜,同樣規格的主機你自己買起來至少也要破千</li>
<li>彈性,想關就關、想擴充就擴充</li>
<li>幫你弄好固定 IP,甚至還有 https</li>
</ol>
<p>而雲端主機的服務有:</p>
<ul>
<li>AWS 的 EC2(Amazon Elastic Compute Cloud)</li>
<li>Google 的 Compute engine</li>
<li>Azure Virtual Machined</li>
<li>DigitalOcean Droplets</li>
<li>Linode</li>
</ul>
<p>當然還有很多很多,族繁不及備載。</p>
<p>雲端主機就像自己的電腦一樣。大部份的雲端主機都是會安裝好 OS 的,使用者可以透過 ssh 連線到 instance,然後使用 CLI 介面操作,來設定對應的執行環境,例如 node 等等。如果是當作網站的 Server 使用的話,還會需要設定像是防火牆,網路連線等等的東西才能夠讓外部網路連線雲端主機。然而這些設定並不容易,尤其是對於 Linux 生態不熟悉的使用者來說。</p>
<p>除此之外,擴展性也是個問題,畢竟這就是一台電腦,就像如果你想要幫你的電腦換 CPU 或者是更大的 RAM,你就必須把主機關機,而雲端主機同樣的也需要主動的 terminate instance(終止實例,就是電腦關機),而關機就代表著無法提供 Server 的服務。(當然,可以透過多台電腦維持服務不中斷,不過不是這裡討論的範疇。)</p>
<p>IaaS 雲端主機可以說是其他部署服務的基礎。在 IaaS 的基礎上,服務幫你多做一些事情,而想要建立下一個 Facebook 的你就可以少做一些事情,還能夠有更高的擴展性、更方便的佈署方式。這就是接下來的其他服務。</p>
<p><img src="https://azurecomcdn.azureedge.net/cvt-665f0680393306b6d0a912671748e5cca6b061d55ecee48d4f985eaa8e15e6bf/images/page/overview/what-is-iaas/iaas-paas-saas.png" alt="Azure 的雲端服務類型" /></p>
<h3 id="paas"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/deploy-and-cloud/#paas">#</a> PaaS</h3>
<p>PaaS,Platform as a Service,平台即服務。設定環境真的很麻煩,我們都懂。有沒有一個地方能夠只讓我丟上去程式碼就可以執行,減少設定環境方面等等的事情?</p>
<p>在 PaaS 服務裡面你不會需要親自去下載那些環境(例如 node),透過描述檔的方式來設定,而 PaaS 的服務就會依照你的描述檔幫你安裝環境,處理前面提到的網路連線等等的問題,並自動部署運行在前面提到的雲端主機上。</p>
<p>環境的描述檔就像是這樣:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token comment"># [START django_app]</span><br /><span class="token key atrule">runtime</span><span class="token punctuation">:</span> python37<br /><br /><span class="token key atrule">handlers</span><span class="token punctuation">:</span><br /><span class="token comment"># This configures Google App Engine to serve the files in the app's static</span><br /><span class="token comment"># directory.</span><br /><span class="token punctuation">-</span> <span class="token key atrule">url</span><span class="token punctuation">:</span> /static<br /> <span class="token key atrule">static_dir</span><span class="token punctuation">:</span> static/<br /><br /><span class="token comment"># This handler routes all requests not caught above to your main app. It is</span><br /><span class="token comment"># required when static routes are defined, but can be omitted (along with</span><br /><span class="token comment"># the entire handlers section) when there are no static files defined.</span><br /><span class="token punctuation">-</span> <span class="token key atrule">url</span><span class="token punctuation">:</span> /.*<br /> <span class="token key atrule">script</span><span class="token punctuation">:</span> auto<br /><span class="token comment"># [END django_app]</span></code></pre>
<p>此外因為服務會自動幫你執行,所以能夠作到自動擴展。在你的 Server 需要的記憶體不夠時,能夠從 2G 自動升級 4G,在過剩時又能夠降回 2G。可以作到以時間,甚至流量為分割粒度來設定需求。</p>
<p>總而言之,你只是把你的程式「部署」(某方面來說,甚至可以看做是簡單的「上傳」)到服務上,並寫好環境的描述檔,服務就會幫你處理好一切,Magic~</p>
<p>而這類型的服務有:</p>
<ul>
<li>GCP App engine</li>
<li>AWS Elastic Beanstalk</li>
<li>Azure App Service</li>
<li>Heroku</li>
<li>Vercel</li>
<li>還有很多很多...</li>
</ul>
<p>但這種很方便的東西一定是有 trade-off 的,一個是這樣的服務能提供的環境類型有限,如果有一位大神用 C++ 寫 Server,那這樣冷門的環境服務就大部分沒辦法提供,再來環境類型有限也意味著控制程度有限,沒辦法作到更詳細的設定。</p>
<p>面對這樣的問題,容器化技術(Containerization)正好可以解決這樣的問題,而目前最知名的就是大名鼎鼎的 Docker。那什麼是 Docker 呢?</p>
<h4 id="%E5%AE%B9%E5%99%A8%E5%8C%96%E6%8A%80%E8%A1%93%E8%88%87-docker"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/deploy-and-cloud/#%E5%AE%B9%E5%99%A8%E5%8C%96%E6%8A%80%E8%A1%93%E8%88%87-docker">#</a> 容器化技術與 Docker</h4>
<p>在過去,想要開一家餐廳就必須租一間店面、牽水電、弄灶台流理台櫃台、放置座椅等等。而如果我們的店要搬遷絕對是非常麻煩的事情,光設備就很有可能一台卡車搬不完了,更何況到新的店面還是需要牽水電、佈置電燈之類的。</p>
<p>但如果只是要開一家小店,可能不用那麼累?我們都看過在路邊賣著漢堡而且讓你流口水的美式餐車,餐車可以使用發電機、攜帶水箱,還有使用攜帶式的爐灶。重點是,這些東西都被放在一台可愛的小卡車上,想去哪裡開店,就去哪裡開店。</p>
<p>Docker 也是一樣的道理。我們可以把部署所需要的程式碼以及對應的環境,包含作業系統(ubuntu、Debian、Arch ...)、語言的執行環境(Node...)等等。通通包在一起丟到一個容器裡面,就像餐車把需要的設備通通放在卡車上一樣。有了這個容器,我們就不需要再設定環境了,想去哪台 server ,直接執行包好的容器就 OK 。</p>
<p>這個概念特別適合 PaaS 的服務,原本 PaaS 只能提供特定環境,但有了 Docker,就可以脫離服務的限制,隨心所欲的將自己需要的環境包進 Docker,就能作到想幹嘛就幹嘛。</p>
<p>大部分 PaaS 服務也除了內建的環境以外,都會提供使用 Docker 來作到更彈性的設定,像 GCP App engine 就可以使用下面的設定來使用 Docker。</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">runtime</span><span class="token punctuation">:</span> flex</code></pre>
<p>而 AWS 的 Elastic Beanstalk 也能夠設定<a href="https://docs.aws.amazon.com/zh_tw/elasticbeanstalk/latest/dg/create_deploy_docker.html">使用 Docker Image 來部署</a>,如果想要更精準的控制容器像是停止、啟動等等,則可以使用 <a href="https://aws.amazon.com/tw/ecs/faqs/">Elastic Container Service</a>。</p>
<h4 id="kubernetes"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/deploy-and-cloud/#kubernetes">#</a> Kubernetes</h4>
<p>Kubernetes,唸作哭ㄅ捏題絲,簡稱 K8s。講到 Docker 就會有人提到 K8s,然後就會有人說:</p>
<blockquote>
<p>You don't F**kin need Kubernetes</p>
</blockquote>
<p>沒有很了解說具體 K8s 能作到哪些,但簡單可以想像成管理多個 Docker 的工具。各大廠也提供了服務,能夠執行 K8s 的 CaaS(Container as a Service)服務。像是:</p>
<ul>
<li>Google Kubernetes Engine</li>
<li>Amazon Elastic Kubernetes Service</li>
<li>Azure Kubernetes</li>
</ul>
<h3 id="faas"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/deploy-and-cloud/#faas">#</a> FaaS</h3>
<p>PaaS 還是有他的問題在。過去我們使用 Server 會讓 Server 運行,等待 Request 後再回傳 Response。但在沒有接收到 Request 時,就 Server 只是待命而已,並沒有實際的使用。但主機的錢還是照樣計算,要知道在雲端上,每一秒都是錢阿!</p>
<p>於是就有了 FaaS (Function as a Service)的出現。這樣的服務同樣幫你做好環境了,也同樣只需要上傳程式碼。但不同的是,Function 並不是常時運行的。Function 能夠依照特定的事件(像是定時、或者是 Request 等)來去執行你設定好的程式。</p>
<p>這樣的模式已經離和我們過去的方式大不相同了。過去我們有點像我們開一台機器持續的運轉,已經開好了,等待需要來就立即運作並提供需要的服務。而 FaaS 的方式就像每次需要的時候再開啟機器。這樣還是沒概念的話,平常使用的飲水機先把熱水燒好並且持續保溫,有需要就按下按鍵自動出水。但有一種瞬熱式飲水機,在有需要用熱水的時候才會瞬間加熱,並沒有常時的在保溫。</p>
<p>這樣的模式會帶來什麼樣好處?</p>
<ul>
<li>相對便宜,畢竟有需要才會使用,執行的時間減少了。</li>
</ul>
<p>不過相對的也會帶來問題。每次都重新執行</p>
<ul>
<li>不適用於需要持久性連結的協議,例如 websocket</li>
<li>每次的 function 之間是無狀態的,執行時的狀態(或者說資料)若不另外儲存,無法讓下一次的執行使用。</li>
<li>Cold start:服務會有很高的延遲,因為每次執行時都是從頭開始執行,重新執行程式碼。</li>
<li>畢竟是服務幫你設置環境的,所以控制權較低...?等等!</li>
</ul>
<p>環境控制權的問題有點既視感,沒錯,在 PaaS 也有同樣的問題,但這樣的問題也一樣能夠透過 Docker 來解決。</p>
<p>這類型的服務有</p>
<ul>
<li>GCP cloud functions/ cloud run</li>
<li>AWS Lambda / AWS Fargate</li>
<li>Azure Functions Serverless Compute</li>
</ul>
<h2 id="%E7%B5%90%E8%AA%9E"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/deploy-and-cloud/#%E7%B5%90%E8%AA%9E">#</a> 結語</h2>
<p>這些不同的部署方式絕對不是越後面就越潮,什麼東西都用最潮 FaaS 服務來做絕對會非常痛苦(甚至根本做不出來)。在技術上的選用永遠是老話一句:選擇自己需求來決定說要使用哪些技術。</p>
<p>消毒一下,這篇文章的解釋非常粗淺,要是希望用比較好懂的方式來解釋各種不同的部署方式還有雲端服務。還有舉例部份,並不代表完全列出該類型的服務,像是 FaaS 可能在 AWS 上不只有 Lambda 和 Fargate 這兩個服務,畢竟每家公司的產品五花八門,自己也沒有全盤的了解,沒辦法全部列出。</p>
<p>如果解釋上有任何的問題,或者是舉例的錯誤也歡迎指出,會盡速做修改!感謝打給的收看~</p>
<p>參考資料:</p>
<ul>
<li><a href="https://www.youtube.com/watch?v=uEVmD6n8Il0&t=1s">7 Ways to Deploy a Node.js App</a>:簡單易懂解釋不同的 Deploy 方式</li>
<li><a href="https://cloud.google.com/free/docs/aws-azure-gcp-service-comparison">Compare AWS and Azure services to Google Cloud</a></li>
<li><a href="https://aws.amazon.com/cn/blogs/china/lambda-serverless/">带您玩转Lambda,轻松构建Serverless后台!</a></li>
<li><a href="https://cynthiachuang.github.io/Difference-between-IaaS-PaaS-SaaS-and-FaaS/">雲端計算 IaaS、PaaS、SaaS 與 FaaS</a></li>
<li><a href="https://azure.microsoft.com/zh-tw/overview/what-is-paas/">何謂 PaaS?</a></li>
</ul>
結合 Line bot 與 twitter 爬蟲的研究紀錄 實戰篇
2021-08-15T00:00:00Z
https://blog.errorbaker.tw/posts/umer/line-bot/
<!-- summary -->
<!-- 因為懶,我想要讓 Line 定期更新推特推文給我看 -->
<!-- summary -->
<!-- more -->
<h2 id="%E8%B5%B7%E6%BA%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/line-bot/#%E8%B5%B7%E6%BA%90">#</a> 起源</h2>
<p>智慧手機已經成為大多數人日常生活中不可或缺的一部分,每天定期滑 FB、IG、twitter、line 之類的社交軟體更是不可少的日常任務( •̀ ω •́ )✧。<br />
為了能夠定期、及時取得最新資訊,但又不想每個社群 app 都慢慢滑,只想透過 line 就可以取得其他社群 app 的通知,這篇系列文章就是想解決這個問題,白話來講就是想以更懶的方式來滑手機。<br />
本文以 twitter 爬蟲為範例,示範如何寫出一個 <strong>「每天晚上 12 點定期去爬某位推特的最新三篇推文,並且通過 line 來回傳資料」</strong> 的 line bot,以及紀錄這次開發遇到的各種阻礙的 debug 方法(下篇)。</p>
<p>總結下來的完整運作流程會是:申請使用 line bot > twitter 爬蟲 > 寫 line bot 程式碼(用 javascript) > 部署到虛擬主機 > 用 crontab 排程,定期去爬推特的最新推文</p>
<h2 id="%E7%94%B3%E8%AB%8B%E4%BD%BF%E7%94%A8-line-bot"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/line-bot/#%E7%94%B3%E8%AB%8B%E4%BD%BF%E7%94%A8-line-bot">#</a> 申請使用 line bot</h2>
<p>line 官方其實有提供免費的 line bot 服務方案,因此首先來到 <a href="https://developers.line.biz/zh-hant/">LINE Developers</a>,選擇使用 Messaging API 來做開發,接著做帳號登入,因為沒有商用帳號,所以選擇 line 帳號登入。</p>
<h3 id="%E5%A1%AB%E5%85%A5-channel-%E8%B3%87%E8%A8%8A"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/line-bot/#%E5%A1%AB%E5%85%A5-channel-%E8%B3%87%E8%A8%8A">#</a> 填入 channel 資訊</h3>
<p>之後要創一個新的 channel 來做發送 line 給使用者,如下。<br />
<img src="https://i.imgur.com/21EPUoN.png" alt="" /></p>
<p><img src="https://i.imgur.com/dr72AAB.png" alt="" /></p>
<p><img src="https://i.imgur.com/SRZ6jex.png" alt="" /></p>
<h3 id="console-home"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/line-bot/#console-home">#</a> console home</h3>
<p>完成之後就可以在 console home 看到剛剛創的 channel 資訊(如下圖),裡面有提供 QR code 來加入這個 channel,前面填寫的 channel 資訊之後也都可以在 conosle home 做編輯修改。</p>
<p>channel 裡面需要特別記住的資料有:</p>
<ol>
<li><code>CHANNEL_ID</code></li>
<li><code>CHANNEL_SECRET</code></li>
<li><code>CHANNEL_ACCESS_TOKEN</code></li>
</ol>
<p>它們是用來執行 channel 的資訊,小心不要外洩,後續會透過環境變數的方式來輸入這些資料。</p>
<ul>
<li><code>CHANNEL_ID</code>、<code>CHANNEL_SECRET</code> 可以在 Basic settings 裡面查看。</li>
<li><code>CHANNEL_ACCESS_TOKEN</code> 則是在<br />
Messaging API 裡面查看,初次使用的 token 要按 issue 來產生。</li>
</ul>
<p>最後,在 Messaging API 裡面可以看到有一個 <strong>Webhook URL</strong>,我們需要提供一個具有 SSL 的 URL 來設定傳送的訊息。</p>
<p><img src="https://i.imgur.com/31sQS8g.png" alt="" /></p>
<p>Webhook UR 的運作方式大概如下圖,為了要提供 webhook URL,下一步就是寫出 line bot 以及 twitter 爬蟲,最後找個能夠部署 server 的地方把程式放上去。</p>
<blockquote>
<p>如果想要有免費的地方來做部署,可以考慮 Heroku(免費版的 Heroku 有自動休眠的機制)、 AWS(EC2 主機有一年免費服務),或是其他地方。</p>
</blockquote>
<p><img src="https://i.imgur.com/242PByM.png" alt="" /></p>
<h3 id="line-official-account-manager"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/line-bot/#line-official-account-manager">#</a> LINE Official Account Manager</h3>
<p>除了 console home 之外,更詳細的修改、帳單(如果申請商用版或是用量超過限制)或是統計資料,可以去 Basic settings 裡面的 LINE Official Account Manager 查看。</p>
<p><img src="https://i.imgur.com/DnZ1YWl.png" alt="" /></p>
<h2 id="%E9%96%8B%E5%A7%8B%E5%AF%AB-line-bot-%E7%A8%8B%E5%BC%8F%E7%A2%BC"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/line-bot/#%E9%96%8B%E5%A7%8B%E5%AF%AB-line-bot-%E7%A8%8B%E5%BC%8F%E7%A2%BC">#</a> 開始寫 line bot 程式碼</h2>
<p>這次用 Javascript 來寫,首先去下載別人寫好的套件,<code>npm i linebot</code>,或是也可以使用 line 官方提供的套件<code>npm i @line/bot-sdk</code>,使用起來差別不大,只是 linbot 套件有現成的範例可以滿足這次的目標所以才選擇它。</p>
<p>安裝 express,<code>npm i express</code>,作為這次的 server。</p>
<p>寫程式碼,如下,先跟著 linebot 文件的範例來測試,寫一個會復述使用者傳送的訊息的 line bot。</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// example.js</span><br /><span class="token keyword">const</span> express <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'express'</span><span class="token punctuation">)</span><br /><span class="token keyword">const</span> linebot <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'linebot'</span><span class="token punctuation">)</span><br /><span class="token keyword">const</span> app <span class="token operator">=</span> <span class="token function">express</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><span class="token keyword">const</span> port <span class="token operator">=</span> <span class="token number">3000</span><br /><br /><span class="token keyword">const</span> bot <span class="token operator">=</span> <span class="token function">linebot</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token literal-property property">channelId</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">CHANNEL_ID</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">channelSecret</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">CHANNEL_SECRET</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">channelAccessToken</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">CHANNEL_ACCESS_TOKEN</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">verify</span><span class="token operator">:</span> <span class="token boolean">true</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><br /><span class="token keyword">const</span> linebotParser <span class="token operator">=</span> bot<span class="token punctuation">.</span><span class="token function">parser</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br />app<span class="token punctuation">.</span><span class="token function">post</span><span class="token punctuation">(</span><span class="token string">'/linewebhook'</span><span class="token punctuation">,</span> linebotParser<span class="token punctuation">)</span><br /><br />bot<span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">'message'</span><span class="token punctuation">,</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> event<span class="token punctuation">.</span><span class="token function">reply</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">did you say: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>event<span class="token punctuation">.</span>message<span class="token punctuation">.</span>text<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">resp</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'resp'</span><span class="token punctuation">,</span> resp<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">catch</span><span class="token punctuation">(</span><span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">error</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'we have err'</span><span class="token punctuation">,</span> error<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><br /><br />app<span class="token punctuation">.</span><span class="token function">listen</span><span class="token punctuation">(</span>port<span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">listening now...</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<p>第 8-10 行程式碼就是用環境變數來傳送 channel 的</p>
<ol>
<li><code>CHANNEL_ID</code></li>
<li><code>CHANNEL_SECRET</code></li>
<li><code>CHANNEL_ACCESS_TOKEN</code></li>
</ol>
<p>最後把這份專案推到 github 上面,方便後續做部署。</p>
<h2 id="%E9%83%A8%E7%BD%B2%E5%88%B0%E8%99%9B%E6%93%AC%E4%B8%BB%E6%A9%9F-ec2"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/line-bot/#%E9%83%A8%E7%BD%B2%E5%88%B0%E8%99%9B%E6%93%AC%E4%B8%BB%E6%A9%9F-ec2">#</a> 部署到虛擬主機 EC2</h2>
<p>接著來做部署,目的是先測試前面寫好的 linebot 會不會正常運作。</p>
<p>因為這次實戰主要是想記錄開發 line bot 和 twitter 爬蟲的部分,部署的部分會寫的比較簡單,使用的部署平台是 AWS 的虛擬主機 EC2,部署方式是用 git,另外還有使用到 Nginx, pm2。</p>
<blockquote>
<p>參考<a href="https://dev.to/shadid12/how-to-deploy-your-node-js-app-on-aws-with-nginx-and-ssl-3p5l">這裡的步驟</a></p>
</blockquote>
<p>安裝 JS 的執行環境 Nodejs。<br />
<code>curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - sudo apt install nodejs</code></p>
<p>把 github 的專案複製一份下來,用環境變數輸入 channel 的<code>CHANNEL_ID</code>、<code>CHANNEL_SECRET</code>、<code>CHANNEL_ACCESS_TOKEN</code>並執行剛剛寫的程式。</p>
<pre class="language-c"><code class="language-c">git clone <span class="token operator"><</span>project_repo<span class="token operator">></span><br />cd yourproject<br />npm install<br />CHANNEL_ID<span class="token operator">=</span><span class="token char">'123'</span> CHANNEL_SECRET<span class="token operator">=</span><span class="token char">'123'</span> CHANNEL_ACCESS_TOKEN<span class="token operator">=</span><span class="token char">'123'</span> node index<span class="token punctuation">.</span>js node example<span class="token punctuation">.</span>js</code></pre>
<p>之後去,<code>http://IP:3000/linewebhook</code> 確認能夠正常連線。<br />
然後再用 line 傳送訊息,確認能夠正常回傳。</p>
<p><img src="https://i.imgur.com/SL9GRP1.png" alt="" /></p>
<h3 id="%E5%AE%89%E8%A3%9D-pm2"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/line-bot/#%E5%AE%89%E8%A3%9D-pm2">#</a> 安裝 pm2</h3>
<p>接著安裝 pm2,目的是讓 line-bot 可以背景執行,</p>
<pre class="language-c"><code class="language-c">sudo npm i pm2 <span class="token operator">-</span>g<br />CHANNEL_ID<span class="token operator">=</span><span class="token char">'123'</span> CHANNEL_SECRET<span class="token operator">=</span><span class="token char">'123'</span> CHANNEL_ACCESS_TOKEN<span class="token operator">=</span><span class="token char">'123'</span> node index<span class="token punctuation">.</span>js node example<span class="token punctuation">.</span>js pm2 start example<span class="token punctuation">.</span>js</code></pre>
<h3 id="%E5%AE%89%E8%A3%9D-nginx"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/line-bot/#%E5%AE%89%E8%A3%9D-nginx">#</a> 安裝 nginx</h3>
<p>使用 nginx 的目的是做反向代理,不要讓使用者直接看到這個 linebot 是使用虛擬主機的 port 3000 在提供服務,概念上來說如圖片這樣。<br />
<img src="https://i.imgur.com/0W0xWdR.png" alt="" /></p>
<p>安裝 nginx,<code>udo apt install Nginx</code>,然後去做代理的設定,新建一個設定檔<code>sudo nano /etc/nginx/sites-available/line</code> 並寫入</p>
<pre class="language-c"><code class="language-c">server_name yourdomain<span class="token punctuation">.</span>com www<span class="token punctuation">.</span>yourdomain<span class="token punctuation">.</span>com<span class="token punctuation">;</span><br /><br /> location <span class="token operator">/</span> <span class="token punctuation">{</span><br /> proxy_pass http<span class="token operator">:</span><span class="token comment">//localhost:3000; #app 使用的 port</span><br /> proxy_http_version <span class="token number">1.1</span><span class="token punctuation">;</span><br /> proxy_set_header Upgrade $http_upgrade<span class="token punctuation">;</span><br /> proxy_set_header Connection <span class="token char">'upgrade'</span><span class="token punctuation">;</span><br /> proxy_set_header Host $host<span class="token punctuation">;</span><br /> proxy_cache_bypass $http_upgrade<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span></code></pre>
<p>最後對這個檔案做 link,<code>sudo ln -s /etc/nginx/sites-available/your_config /etc/nginx/sites-enabled/</code>。</p>
<h2 id="%E7%A0%94%E7%A9%B6%E5%A6%82%E4%BD%95%E5%8F%96%E5%BE%97-twitter-%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/line-bot/#%E7%A0%94%E7%A9%B6%E5%A6%82%E4%BD%95%E5%8F%96%E5%BE%97-twitter-%E8%B3%87%E6%96%99">#</a> 研究如何取得 twitter 資料</h2>
<p>現成的方法有兩類,或是也可以自己嘗試從零到有打造一個爬資料的程式,</p>
<ol>
<li>申請 developer account 來使用官方的 API,<a href="https://developer.twitter.com/en/docs/developer-portal/overview">申請頁面</a><br />
這需要一些審核時間,審核也滿嚴格的,而且需要填寫各種使用原因,這次想取得的資料比較簡單,先 PASS 這個方法。</li>
<li>使用 twint,這次採用的方法</li>
</ol>
<h3 id="twint-%E4%BB%8B%E7%B4%B9"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/line-bot/#twint-%E4%BB%8B%E7%B4%B9">#</a> Twint 介紹</h3>
<blockquote>
<p>Twint is an advanced Twitter scraping tool written in Python that allows for scraping Tweets from Twitter profiles without using Twitter's API.</p>
</blockquote>
<p>Twint 是一個使用 Python 來寫的 scraping tool,使用的好處是 setup 容易、取得資料不會受限於最新的 3200 篇推文、不需要 token 驗證、匿名。</p>
<p>實作的時候發現因為 twitter 在 2020 年的時候把 Legacy Twitter version 關掉了,如果有使用到這部份的 twint,會看到類似這樣的錯誤訊息 <code>CRITICAL:root:twint.feed:Follow:IndexError</code>,<a href="https://github.com/twintproject/twint/issues/409">issue</a></p>
<h3 id="%E9%96%8B%E5%A7%8B%E4%BD%BF%E7%94%A8-twint"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/line-bot/#%E9%96%8B%E5%A7%8B%E4%BD%BF%E7%94%A8-twint">#</a> 開始使用 twint</h3>
<p>安裝 twint,<code>pip3 install --user --upgrade git+https://github.com/twintproject/twint.git@origin/master#egg=twint</code></p>
<pre class="language-python"><code class="language-python"><span class="token comment"># parse.py</span><br /><span class="token keyword">import</span> twint<br /><span class="token comment"># Configure</span><br />c <span class="token operator">=</span> twint<span class="token punctuation">.</span>Config<span class="token punctuation">(</span><span class="token punctuation">)</span><br />c<span class="token punctuation">.</span>Username <span class="token operator">=</span> <span class="token string">"123"</span> <span class="token comment"># 你要搜尋推文的對象</span><br />c<span class="token punctuation">.</span>Limit <span class="token operator">=</span> <span class="token number">0</span><br />c<span class="token punctuation">.</span>Media <span class="token operator">=</span> <span class="token boolean">True</span><br />c<span class="token punctuation">.</span>Images <span class="token operator">=</span> <span class="token boolean">True</span><br />c<span class="token punctuation">.</span>Custom<span class="token punctuation">[</span><span class="token string">"tweet"</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">"created_at"</span><span class="token punctuation">,</span> <span class="token string">"username"</span><span class="token punctuation">,</span> <span class="token string">"name"</span><span class="token punctuation">,</span> <span class="token string">"tweet"</span><span class="token punctuation">,</span> <span class="token string">"thumbnail"</span><span class="token punctuation">]</span><br />c<span class="token punctuation">.</span>Store_json <span class="token operator">=</span> <span class="token boolean">True</span><br />c<span class="token punctuation">.</span>Output <span class="token operator">=</span> <span class="token string">"Tweets.txt"</span><br /><span class="token comment"># Run</span><br />twint<span class="token punctuation">.</span>run<span class="token punctuation">.</span>Search<span class="token punctuation">(</span>c<span class="token punctuation">)</span></code></pre>
<p>執行之後會輸出一個 txt 檔案,裡面會有推文的相關資料,接著要讓 example.js (line-bot) 來讀取這份資料。</p>
<h3 id="%E8%AE%93-line-bot-%E5%8F%96%E5%BE%97%E6%8E%A8%E7%89%B9%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/line-bot/#%E8%AE%93-line-bot-%E5%8F%96%E5%BE%97%E6%8E%A8%E7%89%B9%E8%B3%87%E6%96%99">#</a> 讓 Line-bot 取得推特資料</h3>
<p>在前面段落 <a href="https://hackmd.io/syL5QgITQZO7OWazmaZfCQ?both#%E9%96%8B%E5%A7%8B%E5%AF%AB-line-bot-%E7%A8%8B%E5%BC%8F%E7%A2%BC">開始寫 line bot 程式碼</a> 示範了如何讓 lie-bot 復述使用者傳來的訊息,在 <a href="https://hackmd.io/syL5QgITQZO7OWazmaZfCQ?both#%E9%96%8B%E5%A7%8B%E4%BD%BF%E7%94%A8-twint">開始使用 twint</a> 則示範了如何取得推特資料。現在我們要讓 line-bot 在使用者傳來任何訊息時,都回覆特定推特的最新三篇推文,如下</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> express <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'express'</span><span class="token punctuation">)</span><br /><span class="token keyword">const</span> linebot <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'linebot'</span><span class="token punctuation">)</span><br /><span class="token keyword">const</span> utils <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'./utils'</span><span class="token punctuation">)</span><br /><br /><span class="token keyword">const</span> app <span class="token operator">=</span> <span class="token function">express</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><span class="token keyword">const</span> port <span class="token operator">=</span> <span class="token number">3000</span><br /><br /><span class="token keyword">const</span> bot <span class="token operator">=</span> <span class="token function">linebot</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token literal-property property">channelId</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">CHANNEL_ID</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">channelSecret</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">CHANNEL_SECRET</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">channelAccessToken</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">CHANNEL_ACCESS_TOKEN</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">verify</span><span class="token operator">:</span> <span class="token boolean">true</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><br /><span class="token keyword">const</span> linebotParser <span class="token operator">=</span> bot<span class="token punctuation">.</span><span class="token function">parser</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br />app<span class="token punctuation">.</span><span class="token function">post</span><span class="token punctuation">(</span><span class="token string">'/linewebhook'</span><span class="token punctuation">,</span> linebotParser<span class="token punctuation">)</span><br /><br />bot<span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">'message'</span><span class="token punctuation">,</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'we got a message'</span><span class="token punctuation">,</span> event<span class="token punctuation">)</span><br /> <span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token keyword">await</span> utils<span class="token punctuation">.</span><span class="token function">getTweets</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> event<span class="token punctuation">.</span><span class="token function">reply</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">這是最新的三篇推特:</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>data<span class="token punctuation">.</span>tweets0<span class="token punctuation">.</span>tweet<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">, </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>data<span class="token punctuation">.</span>tweets0<span class="token punctuation">.</span>thumbnail<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">, </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>data<span class="token punctuation">.</span>tweets1<span class="token punctuation">.</span>tweet<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">, </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>data<span class="token punctuation">.</span>tweets1<span class="token punctuation">.</span>thumbnail<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">, </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>data<span class="token punctuation">.</span>tweets2<span class="token punctuation">.</span>tweet<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">, </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>data<span class="token punctuation">.</span>tweets2<span class="token punctuation">.</span>thumbnail<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">resp</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'resp'</span><span class="token punctuation">,</span> resp<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">catch</span><span class="token punctuation">(</span><span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">error</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'we have err'</span><span class="token punctuation">,</span> error<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><br /><br />app<span class="token punctuation">.</span><span class="token function">listen</span><span class="token punctuation">(</span>port<span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">listening now...</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<p>最後再次用 git 去做部署,執行前面寫的 twint,重新啟動 pm2 執行 example.js(line-bot),用 line 傳個訊息測試剛剛的程式碼是否正常,正常的話就會看到以下結果,</p>
<p><img src="https://i.imgur.com/3LGzyIx.png" alt="" /></p>
<h2 id="%E4%BD%BF%E7%94%A8-shell-script-%E4%BE%86%E5%AE%9A%E6%9C%9F%E5%9F%B7%E8%A1%8C%E7%88%AC%E8%9F%B2"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/line-bot/#%E4%BD%BF%E7%94%A8-shell-script-%E4%BE%86%E5%AE%9A%E6%9C%9F%E5%9F%B7%E8%A1%8C%E7%88%AC%E8%9F%B2">#</a> 使用 shell script 來定期執行爬蟲</h2>
<p>最後的最後,來寫一個 crontab 來執行 shell script,目的是想要在每天晚上 00:00 都執行一次 twint,</p>
<h3 id="shell-script"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/line-bot/#shell-script">#</a> shell script</h3>
<p><code>delOld.sh</code> ></p>
<pre class="language-c"><code class="language-c">#<span class="token operator">!</span><span class="token operator">/</span>bin<span class="token operator">/</span>bash<br />rm <span class="token operator">/</span>home<span class="token operator">/</span>ubuntu<span class="token operator">/</span>my_project<span class="token operator">/</span>line<span class="token operator">-</span>bot<span class="token operator">/</span>Tweets<span class="token punctuation">.</span>txt</code></pre>
<p><code>twitterParse.sh</code> ></p>
<pre class="language-c"><code class="language-c">#<span class="token operator">!</span><span class="token operator">/</span>bin<span class="token operator">/</span>bash<br />PATH<span class="token operator">=</span><span class="token operator">/</span>usr<span class="token operator">/</span>bin<span class="token operator">:</span><span class="token operator">/</span>bin<br />python3<span class="token punctuation">.</span><span class="token number">8</span> parse<span class="token punctuation">.</span>py</code></pre>
<h3 id="crontab"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/line-bot/#crontab">#</a> crontab</h3>
<pre class="language-c"><code class="language-c"><span class="token number">0</span> <span class="token number">16</span> <span class="token operator">*</span> <span class="token operator">*</span> <span class="token operator">*</span> <span class="token operator">/</span>your path to file<span class="token operator">/</span>delOld<span class="token punctuation">.</span>sh<br /><span class="token number">5</span> <span class="token number">16</span> <span class="token operator">*</span> <span class="token operator">*</span> <span class="token operator">*</span> <span class="token operator">/</span>your path to file<span class="token operator">/</span>twitterParse<span class="token punctuation">.</span>sh <span class="token operator">>></span> <span class="token operator">/</span>your path to file<span class="token operator">/</span>out<span class="token punctuation">.</span>txt <span class="token number">2</span><span class="token operator">></span><span class="token operator">&</span><span class="token number">1</span></code></pre>
用 Web Component 製作客製化表格元件
2021-08-15T00:00:00Z
https://blog.errorbaker.tw/posts/xiang/build-webcomponent-to-table/
<!-- summary -->
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-to-table/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<p>目前團隊在規劃建立一個元件庫,希望未來能在各個專案上快速套用自己所寫的元件。由於各項專案使用的框架可能有所不同,所以元件庫必須支援不同框架的使用情境。例如說 VueFormGenerator 當中的 custom field 是可以自由客製化表單中的元件,我們就能引用這個元件庫的元件,將其使用於 Vue 的表單之中。</p>
<p>大部分元件在實作上沒有太大的困難,像是按鈕這個元件就很單純,開幾個欄位提供使用者改改顏色或改改樣式基本上就沒太大問題了。<br />
不過有幾項元件就不是那麼好實作了,例如本篇文章想要探討的 table 這個元件,它不光是改樣式這麼簡單而已,像是每個欄位需要如何抓取資料,各自的行為是什麼?還有表格所衍生出來的各項功能如何去實作?以及如何讓使用者自由客製化...等等。這些都需要提前設計好,才不會在使用的時候感到不方便。</p>
<p>以往在 HTML 使用到 table 這個標籤時,每個頁面的表格都會是分開來做的,A 頁面的表格直接寫在 A 頁面的檔案底下,B 頁面的表格也直接寫在 B 頁面的檔案底下,所以每個頁面的表格需要幾個 column 都可以分別定義好的,有幾個功能也可以分別放上去。</p>
<p>但是現在要達成的目標變成是,做出一個 table 元件,不管是 A 頁面還是 B 頁面,都可以直接使用這個元件,這也是第一次研究如何可以做出讓不同頁面一起使用的 table 元件。</p>
<p>由於這邊的 table 元件是透過 Web Component 來實作的,所以上半部的篇幅會先提到關於 Web Component 最簡單的概念(已經了解的大大們可以跳過這部分),下半部才會是分享 table 元件的實作。</p>
<!-- summary -->
<!-- more -->
<h2 id="%E7%9B%AE%E6%A8%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-to-table/#%E7%9B%AE%E6%A8%99">#</a> 目標</h2>
<p>第一,要能讓使用者在使用當下,透過設定的方式來帶入它需要幾個 column,且分別需要顯示什麼資料。</p>
<p>第二,要能讓使用者自由定義要綁上去的功能有哪些</p>
<h2 id="%E5%88%9D%E6%8E%A2-web-component"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-to-table/#%E5%88%9D%E6%8E%A2-web-component">#</a> 初探 Web Component</h2>
<p>考量到希望引入元件是獨立的,不會與專案當中其他的程式碼相互干擾而發生悲劇,所以參考了利用 web component 的方法來實作元件。它可以讓我們自定義元件,包含 HTML 結構、CSS 樣式、JavaScript,並且取一個自己喜歡的標籤名稱(例如:<code><wc-table></wc-table</code>),插入到頁面上就能得到一個封裝好的組件。</p>
<p>由於 Web Component 是利用 Browser 原生支援的 Custom Elements 來渲染共用元件的,所以能共用在任何前端框架上,非常符合我們對於元件庫的需求。它的概念其實跟 React 的 class component 有幾分類似,都能讓我們自定義一個元件的架構跟樣式,並使用在其他地方,只是它還有一些需要另外去熟悉的特性,例如 shadow DOM 的概念。</p>
<p>shadow DOM 允許我們創建一些完全獨立於其他元素的 sub-DOM trees,什麼意思呢?有點像我們組裝模型一樣,有一個可以組裝用的接口,讓我們裝上別的手臂或武器之類的,而且裝上去的部位跟模型本身不會相互影響。</p>
<p>可以參考下圖:我們可以利用 shadow-host 這個節點,裝上一個 shadow-tree</p>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/shadow-tree.png" alt="" /></p>
<p>(圖片來源: <a href="https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_shadow_DOM">https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_shadow_DOM</a>)</p>
<p>Shadow DOM 的操作方式跟一般我們常操作的 DOM 是相同的,可以新增屬性、增加 child node...等等,但是我們沒辦法直接透過外部來修改 Shadow DOM 底下的元件。</p>
<p>比如說下面這個 HTML 的架構:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>.wrapper<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>wc-element</span><span class="token punctuation">></span></span><br /> #shadow-root<br /> <span class="token comment"><!----></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>wc-btn<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>...<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span><br /> <span class="token comment"><!----></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>wc-element</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>如果我們想要透過外部的 <code>.wrapper</code> 來調整底下 <code>.wc-btn</code> 的顏色是無法調整的:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">.wrapper .wc-btn</span> <span class="token punctuation">{</span><br /> <span class="token property">color</span><span class="token punctuation">:</span> red<span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>相反的 Shadow DOM 内部的元素也不會影響到外部。</p>
<p>由於這個特性,使得我們可以封裝一個具有獨立功能的 <code><wc-table></code> 元件,並且可以保證不會引用到專案的同時影響到其它 DOM 元素。shadow DOM 和標準的 DOM 一樣,可以設置它的樣式,也可以用 JavaScript 操作它的行為。DOM 和 shadow DOM 創建的獨立組件之間的互不干擾,有利於組件在各個專案的復用。</p>
<h2 id="%E5%88%9D%E6%8E%A2-slot"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-to-table/#%E5%88%9D%E6%8E%A2-slot">#</a> 初探 Slot</h2>
<p>上面提到了可以透過 Web component 來封裝組件,提供別的專案來使用,不過還有一個問題是,我只是提供一個固定樣式的組件,這樣使用者怎麼客製化?總得要讓使用者可以做一些調整,不然每次要調整都要回去 Web Component 調,這樣也不是很好。</p>
<p><strong>第一個辦法:開一些 attribute 的欄位讓使用者帶入</strong></p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>wc-button</span> <span class="token attr-name">btn-color</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>primary<span class="token punctuation">"</span></span> <span class="token attr-name">hidden</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>true<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> 按鈕 <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>wc-button</span><span class="token punctuation">></span></span></code></pre>
<p>我們只要在 Web Component 預先定義好去接收這個 attribute,並且決定當使用者帶入的 btn-color 為某些值的時候,要做出什麼樣的反應,就可以讓使用者去挑選自己想要的顏色。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">get</span> <span class="token function">color</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'btn-color'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>不過這樣有一個困擾,就是使用者一定只能使用我開好的欄位,且只能局限於我所提供的設定。重點是使用者沒辦法自由的客製化底下的標籤。比如說我的按鈕需要加上 icon</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>wc-button</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>i</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>pen<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>i</span><span class="token punctuation">></span></span><br /> 按鈕<br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>wc-button</span><span class="token punctuation">></span></span></code></pre>
<p>像上面需要加上客製化的標籤 <code><i class="pen"></i></code>,這就沒辦法透過 attribute 的欄位來設定了,於是我們可以透過第二種辦法。</p>
<p><strong>第二個辦法: Slot</strong><br />
Slot 可以想像為 Web Component 的 placeholder,當外部想要傳 Value 或者 HTML 的標籤進來時,可以透過 Slot 來達成:</p>
<p>先在 Web Component 底下定義好 slot 跟它的 name</p>
<pre class="language-html"><code class="language-html"><span class="token comment"><!-- 我們定義的 Web Component --></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>slot</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>avatar<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>slot</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>使用者可以將資料或標籤帶入,使用時透過一個 slot 的 attribute 傳入上面要對應的 name</p>
<pre class="language-html"><code class="language-html"><span class="token comment"><!-- 使用者在引入使用元件的時候 --></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>user-data</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span> <span class="token attr-name">slot</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>avatar<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Value<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>user-data</span><span class="token punctuation">></span></span></code></pre>
<p>瀏覽器會將我們的標籤根據對應到的 name 把它置入於 template 之中</p>
<pre class="language-html"><code class="language-html"><span class="token comment"><!-- 瀏覽器最終渲染出來的樣子 --></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>slot</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>avatar<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span> <span class="token attr-name">slot</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>avatar<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Value<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>slot</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>如此一來,除了可以傳入想要的顏色或 attribute 之外,如果需要客製化標籤,也可以達成了。<br />
下一步要開始來設計我們要製作的元件 table。</p>
<h2 id="%E8%87%AA%E8%A8%82-table"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-to-table/#%E8%87%AA%E8%A8%82-table">#</a> 自訂 table</h2>
<p>製作 table 元件以前最重要的就是先把資料給定義清楚,資料確定了以後,後面就只差如何去顯示而已。<br />
因為表格會是有很多組資料組合而成的,所以我們可以想像資料會長的像下面這個樣子</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> tableData <span class="token operator">=</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">"第一筆資料"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token number">2</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">"第二筆資料"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token number">3</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">"第三筆資料"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /><span class="token punctuation">]</span><span class="token punctuation">;</span></code></pre>
<p>由於表格資料它不含有 HTML 的標籤,所以我們可以想像讓使用者直接利用 attribute 的方式來傳入 Web Component</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>wc-table</span> <span class="token attr-name">data</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{tableData}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>wc-table</span><span class="token punctuation">></span></span></code></pre>
<p>接下來就要來思考如何去讓使用者定義 column 要對應到哪些資料</p>
<h2 id="%E8%87%AA%E8%A8%82-column"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-to-table/#%E8%87%AA%E8%A8%82-column">#</a> 自訂 column</h2>
<p>在討論 Web Component 當中的 column 要怎麼實做之前,我們先來決定要怎麼讓使用者來傳入 column,可能有幾種方式:</p>
<p><strong>☞ 第一種方法:由 table 定義好規格,使用者透過 attribute 傳入設定值</strong><br />
只要在我們的 <code><wc-table></code> 先定義好 table 的規格,再給使用者自己傳入相關的設定,使用上可能會像下面這個樣子:</p>
<ul>
<li>data:是整個表個的資料</li>
<li>columnCount:是表格會有幾個 column</li>
<li>config:可以傳入每個 column 要抓哪筆資料,或者其他設定</li>
</ul>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>wc-table</span> <span class="token attr-name">data</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{tableData}<span class="token punctuation">"</span></span> <span class="token attr-name">columnCount</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{n}<span class="token punctuation">"</span></span> <span class="token attr-name">config</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{fitData}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>wc-table</span><span class="token punctuation">></span></span></code></pre>
<p>這個方法的好處是,我很好管控規格,使用者只能完全按照規格來走,<br />
缺點也很明顯,使用上很不直觀,也缺少客製化的空間。</p>
<p><strong>☞ 第二種方法:直接讓使用者帶入標籤</strong><br />
我們直接透過傳入 slot 的方式,讓使用者在使用時直接塞入所有標籤,使用上會像下面這個樣子:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>wc-table</span> <span class="token attr-name">data</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{tableData}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>thead</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>tr</span><span class="token punctuation">></span></span><br /> ...<br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>tr</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>thead</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>tbody</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>tr</span><span class="token punctuation">></span></span><br /> ...<br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>tr</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>tr</span><span class="token punctuation">></span></span><br /> ...<br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>tr</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>tbody</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>wc-table</span><span class="token punctuation">></span></span></code></pre>
<p>使用者直接在 <code><wc-table></code> 底下傳入它想要客製化的所有標籤進來,web component 再把整個 slot 渲染出來。優點是具備非常強大的客製化空間,缺點是完全沒辦法控制使用者傳入什麼進來,也沒辦法去把規格給定義出來。而且這樣子的寫法跟直接使用一般的 <code><table></code> 實在差不了多少,缺少了把它模組化的價值。</p>
<p>上述兩種方式其實都可以達成我們想要的目的,但是有沒有能夠擷取各自優點得折衷方法,既能夠有一定的客製化空間,又能夠限制住規格呢?<br />
我們能不能再定義出一個特殊的元件,它不是用來渲染的,而是用來讓使用者帶入資料?然後再去辨別這個元件並且把它的資料擷取出來?既然不想要使用者傳入這麼繁雜的 HTML 標籤,那就定義一種規格的標籤讓使用者使用吧!</p>
<p><strong>☞ 第三種方法:定義新的標籤來使用</strong><br />
我們來自定義出一個新的標籤 <code><wc-table-column></code>,它是用來讓使用者設定 column 用的,使用上會像下面這個樣子</p>
<ul>
<li>path:每個 column 個別要抓取的資料</li>
<li>headName:每個 column 標題的資料</li>
</ul>
<pre class="language-html"><code class="language-html"><span class="token comment"><!-- 使用者在引入使用元件的時候 --></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>wc-table</span> <span class="token attr-name">data</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{tableData}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>wc-table-column</span> <span class="token attr-name">path</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{id}<span class="token punctuation">"</span></span> <span class="token attr-name">headName</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{name}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>wc-table-column</span> <span class="token attr-name">path</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{value}<span class="token punctuation">"</span></span> <span class="token attr-name">headName</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{name}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>wc-table</span><span class="token punctuation">></span></span></code></pre>
<p>然後我們在 Web Component 底下特別去把 slot 當中的 <code><wc-table-column></code> 挑選出來:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// Web Component 擷取元件的時候</span><br /><span class="token keyword">function</span> <span class="token function">findColumnNodes</span><span class="token punctuation">(</span><span class="token parameter">slot</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> nodes <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br /> <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> element <span class="token keyword">of</span> slot<span class="token punctuation">.</span><span class="token function">assignedNodes</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">flatten</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>element<span class="token punctuation">.</span><span class="token function">matches</span><span class="token punctuation">(</span>element<span class="token punctuation">,</span> <span class="token string">"wc-table-column"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> nodes<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>element<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> nodes<span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>我們將 slot 底下的 <code><wc-table-column></code> 整理成一個新的陣列 nodes,這樣一來我們就會有一組 column 的資料,印出來看的話它會是兩個 HTMLElement</p>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/console-wc-table-column.png" alt="" /></p>
<p>到目前為止,Web Component 有 tableData 的資料,以及使用者 column 的設定,我們只要把它們拼接起來,就可以繪製出我們所想要的表格出來了。</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// Web Component 繪製 HTML 的時候</span><br /><span class="token keyword">const</span> tableHTML <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> <table><br /> <thead><br /> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token keyword">this</span><span class="token punctuation">.</span>nodes<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><br /> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> headerName <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span><br /> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> <th><br /> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>headerName<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"><br /> </th><br /> </span><span class="token template-punctuation string">`</span></span><br /> <span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"><br /> </thead><br /> <tbody><br /> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token keyword">this</span><span class="token punctuation">.</span>tableData<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><br /> <span class="token punctuation">(</span><span class="token parameter">item<span class="token punctuation">,</span> i</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> <tr><br /> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token keyword">this</span><span class="token punctuation">.</span>nodes<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><br /> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> path <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span><br /> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> <td><br /> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">getDataFromPath</span><span class="token punctuation">(</span>item<span class="token punctuation">,</span> path<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"><br /> </td><br /> </span><span class="token template-punctuation string">`</span></span><br /> <span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"><br /> </tr><br /> </span><span class="token template-punctuation string">`</span></span><br /> <span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"><br /> </tbody><br /> </table><br /></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span></code></pre>
<p>最後將這組 HTML render 出來,就完成了 table 的繪製了,第三個方法既達到讓使用者客製化 column 的內容,同時也管控到元件該有的規格。最後剩下的就是 action 的客製化。</p>
<h2 id="%E8%87%AA%E8%A8%82-action"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-to-table/#%E8%87%AA%E8%A8%82-action">#</a> 自訂 action</h2>
<p>先提一下 action 是什麼,我這邊指的 action 代表提供給使用者,可以對每個 row 進行哪些操作,比如說:想要複製、刪除 row,或者想要點開來閱讀詳細資料。這些都是對於 row 操作的行為。</p>
<p>由於這些行為在不同的頁面的表格會有所不同,所以一定也是需要客製化的。比如說 A 頁面需要有編輯跟刪除功能,B頁面可能需要有複製、切換狀態、分享...等等更多功能。也就是說,action 該怎麼設定,是需要讓使用者自行選擇的。</p>
<p>想到這邊會發現,其實實作的方式可以利用跟 column 類似的方法,我們可以一樣可以建立一個特別的元件 <code><wc-table-action></code>,並且一樣透過 slot 擷取傳進來的 action,並整理成一個陣列。</p>
<p>不過有一個地方需要注意到的是,操作的按鈕也是要讓使用者客製化的,比如說 A 頁面的編輯按鈕用的是「編輯」這個文字,但 B頁面的編輯按鈕要用像鉛筆一樣的 icon 來表示,所以需要在 <code><wc-table-action></code> 底下去接使用者傳進來的 slot,才能讓使用者去客製化這個按鈕的樣式:</p>
<ul>
<li>action:傳入一個 callback function 當這個 action 觸發時要發生什麼行為</li>
<li>slot:傳入這個 action 想要客製化的樣式(HTML 標籤)</li>
</ul>
<pre class="language-html"><code class="language-html"><span class="token comment"><!-- 使用者在引入使用元件的時候 --></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>wc-table</span> <span class="token attr-name">data</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{tableData}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>wc-table-column</span> <span class="token attr-name">path</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{id}<span class="token punctuation">"</span></span> <span class="token attr-name">headName</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{name}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>wc-table-column</span> <span class="token attr-name">path</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{value}<span class="token punctuation">"</span></span> <span class="token attr-name">headName</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{name}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>wc-table-action</span> <span class="token attr-name">action</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>handleUpdate()<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>i</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>pen<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>編輯<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>i</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>wc-table-action</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>wc-table-action</span> <span class="token attr-name">action</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>handleDelete()<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>i</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>trash-can<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>刪除<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>i</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>wc-table-action</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>wc-table</span><span class="token punctuation">></span></span></code></pre>
<p>由於 <code><wc-table-action></code> 底下的內容,我們是沒有要讓他渲染的,所以整個 <code><wc-table-action></code> 元件要設定 <code>display: none;</code> 來讓它不會顯示,我們要做的是擷取這些內容,把它們組裝起來之後,動態產生在對應的位置。<br />
所以我們先在 table 底下挖好一個位置,最後組合起來的 actions 標籤就會把它放在這裡:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// Web Component 繪製 HTML 的時候</span><br /><span class="token keyword">const</span> tableHTML <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> <table><br /> <thead>...</thead><br /> <tbody><br /> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token keyword">this</span><span class="token punctuation">.</span>tableData<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><br /> <span class="token punctuation">(</span><span class="token parameter">item<span class="token punctuation">,</span> i</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> <tr><br /> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>actionNodes<br /> <span class="token operator">?</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> <td class="tooltip"><br /> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">setActions</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"><br /> </td><br /> </span><span class="token template-punctuation string">`</span></span><br /> <span class="token operator">:</span> <span class="token keyword">null</span><br /> <span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"><br /> </tr><br /> </span><span class="token template-punctuation string">`</span></span><br /> <span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"><br /> </tbody><br /> </table><br /></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span></code></pre>
<p>這邊的 <code>setActions()</code> 是待會才要定義的 function,我們要利用它來回傳組合好的 HTML 標籤。<br />
我們先在 Web Component 利用相同的方式來擷取 slot 底下的 <code><wc-table-action></code></p>
<pre class="language-js"><code class="language-js"><span class="token comment">// Web Component 擷取元件的時候</span><br /><span class="token keyword">function</span> <span class="token function">findColumnNodes</span><span class="token punctuation">(</span><span class="token parameter">slot</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> actionNodes <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br /> <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> element <span class="token keyword">of</span> slot<span class="token punctuation">.</span><span class="token function">assignedNodes</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">flatten</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>element<span class="token punctuation">.</span><span class="token function">matches</span><span class="token punctuation">(</span>element<span class="token punctuation">,</span> <span class="token string">"wc-table-action"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> nodes<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>element<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> actionNodes<span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>有了這組 actionNodes 的資料以後,我們還需要去取出每個 action node 底下的 slot,這些 slot 就是使用者傳進來的 action 樣式:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">getActions</span><span class="token punctuation">(</span><span class="token parameter">actionNodes</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> actions <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br /> actionNodes<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">item</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> slot <span class="token operator">=</span> item<span class="token punctuation">.</span>shadowRoot<span class="token operator">?.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"slot"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> childNodes <span class="token operator">=</span> slot<span class="token punctuation">.</span><span class="token function">assignedNodes</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">flatten</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> action<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>childNodes<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> actions<span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>我們將 action 樣式整理完成之後,就要開始定義稍早講到的 <code>setActions()</code> 這個函式了,它會將每個 action 的樣式組合起來並回傳完整的 HTML 標籤出去。而這邊這個動作除了會繪製 action 的標籤以外,還會呼叫 _handleClickAction 將 action 的 click 事件綁上去。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">private</span> <span class="token function">setActions</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> actionsHTML <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">getActions</span><span class="token punctuation">(</span>actionNodes<span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><br /> <span class="token punctuation">(</span><span class="token parameter">item<span class="token punctuation">,</span> i</span><span class="token punctuation">)</span> <span class="token operator">=></span><br /> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> <div click-event-index="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>i<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"><br /> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>item<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token operator">?</span> item<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>outerHTML <span class="token operator">:</span> <span class="token string">""</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"><br /> </div><br /> </span><span class="token template-punctuation string">`</span></span><br /> <span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">""</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"><br /> </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> actionsElement <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>shadowRoot<span class="token operator">?.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">"tr .action"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> actionsElement<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">element</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> element<span class="token punctuation">.</span>innerHTML <span class="token operator">=</span> <span class="token string">""</span><span class="token punctuation">;</span><br /> element<span class="token punctuation">.</span><span class="token function">insertAdjacentHTML</span><span class="token punctuation">(</span><span class="token string">"beforeend"</span><span class="token punctuation">,</span> actionHTML<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">const</span> actionItems <span class="token operator">=</span> element<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">"div[click-event-index]"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> actionItems<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">_<span class="token punctuation">,</span> i</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> actionItems<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"click"</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>_handleClickAction<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">private</span> <span class="token function-variable function">_handleClickAction</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> currentTargetIndex <span class="token operator">=</span> e<span class="token punctuation">.</span>currentTarget<span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'click-event-index'</span><span class="token punctuation">)</span><br /> <span class="token keyword">const</span> rowIndex <span class="token operator">=</span> e<span class="token punctuation">.</span>currentTarget<span class="token punctuation">.</span>parentNode<span class="token punctuation">.</span>parentNode<span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'aria-rowindex'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> currentData <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>data<span class="token punctuation">[</span><span class="token function">Number</span><span class="token punctuation">(</span>rowIndex<span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>actionNodes<span class="token punctuation">[</span><span class="token function">Number</span><span class="token punctuation">(</span>currentTargetIndex<span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">_click</span><span class="token punctuation">(</span>currentData<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>如此一來,只要 call 了 <code>setActions()</code> 這個 function 就可以拿到整組的 actions 標籤,而且已經綁定了對應的 click 事件。<br />
所以 table 就透過了 <code><wc-table-column></code> 及 <code><wc-table-action></code> 兩個特殊標籤,來達成了讓使用者自訂表個的目的了。</p>
<h2 id="%E7%B5%90%E8%AA%9E"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-to-table/#%E7%B5%90%E8%AA%9E">#</a> 結語</h2>
<p>以上的程式碼示範主要要傳達實作的概念,以往在切版時都是按照設計稿,定義標籤,定義樣式。這次因為接觸到了元件庫的建立,才有機會從「複用」的角度來思考怎麼設計出一個可以讓使用者客製化的元件。雖然目前這個表格非常的陽春只有顯示資料跟簡單的操作功能,沒辦法支援很多厲害的操作或設定。不過也讓我理解到原來透過 Web Component 自訂義標籤可以有這些變化性的玩法。</p>
<p>之後會再把整個表格的功能變得更加完善,目前也在討論規格當中。在這邊提供一個不錯的套件 <a href="https://vaadin.com/components/vaadin-grid/html-examples/grid-basic-demos">vaadin</a>,它也是能夠用類似的方式建立客製化表格,而且功能相對於完善許多,如果有人有類似的需求,希望 vaadin 可以幫助到你們。</p>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-to-table/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li><a href="https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_shadow_DOM">https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_shadow_DOM</a></li>
<li><a href="https://the-allstars.com/blog/website-information/what-is-web-components-why-is-it-so-important.html">https://the-allstars.com/blog/website-information/what-is-web-components-why-is-it-so-important.html</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_templates_and_slots">https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_templates_and_slots</a></li>
<li><a href="https://vaadin.com/components/vaadin-grid/html-examples/grid-basic-demos">https://vaadin.com/components/vaadin-grid/html-examples/grid-basic-demos</a></li>
</ul>
人人都可以寫文章 - 從筆記到文章
2021-08-22T00:00:00Z
https://blog.errorbaker.tw/posts/benben/01-article/
<!-- summary -->
<!-- 網路上有很多寫文章教學,也有不少寫文章的好處,或許你就曾想過要寫文章?... -->
<!-- summary -->
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/01-article/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<p>耶 ~ 紀念第一篇文章,請容我稍為介紹一下自己XD</p>
<p>大家好,我是 Benben ,喜歡理性的冷知識、感性的人事物。</p>
<p>根據統計學的鐘形常態分配,約有 5% 的人會把 「感性」看成「性感」</p>
<p>沒有,我騙你的!什麼?一開始介紹就騙人,這個作者是想被 ban 嗎?<br />
等等... 先別離開!</p>
<p>我還是有點根據的,是這樣的,統計學中有個名詞叫標準差 ( 這裡沒有要細講XD ),我把看錯一個字當作一個標準差,那看錯兩個字就是兩個標準差,兩個標準差就是 95% 的信賴區間 ( 這也沒有要細講XD ),所以約有 5% 的人會看錯。</p>
<p>那講了這麼多廢話,我是在供 three 小?</p>
<p>你問我常態分配會在哪裡用的到?生活上還真的用不到。</p>
<p>但這就是我要的冷知識 (?) 。對,我就喜歡跟別人不一樣。</p>
<p>那我多喜歡冷知識呢?請看我其他的冷知識 ( 技能 ) :</p>
<ul>
<li>學了 Bass ( 樂團中最冷門的樂器,幫 QQ )</li>
<li>學了 vim 來寫程式 ( 應該是編輯中最難入門的 )</li>
<li>學了嘸蝦米輸入法 ( 應該也是超級少人使用 )</li>
</ul>
<p>大概是這樣,是沒有到頂尖,但也不差囉,歡迎交流但勿戰 XD</p>
<p>那感性的部分呢?</p>
<ul>
<li>我會做手寫的卡片、手寫的書籤 ( 用鋼筆的寫的那種 )</li>
<li>偶爾寫一些的日記等等 ( 現在比較少就是了 )</li>
<li>聽歌或是看電影到哭 ( 天啊!太害羞了 )</li>
</ul>
<p>各位廣大的男性們,知道怎麼讓女生心動了嗎 (對喔,就是男人的眼淚?)</p>
<p>其實我也在思考要不要寫這一段,有點矯情又有點搞笑 (?) ,但是天知地知你知我知,「自我介紹」還真的是很重要的技能,就跟書到用時方恨少一樣,比起那種:「我是 XXX,OO 大學畢業,今年 18 歲,會 HTML/CSS 跟一點 JavaScript。」一個好的自我介紹,能夠讓面試官瞇起來眼睛稍微打開一點,因為現在大家會的技能都一樣,HTML/CSS/JavaScript 這些 ( 以前端為例 ),我會、你會、連隔壁老王都會。</p>
<p>當然,之後求職的自我介紹不可能像我這樣寫啦,這邊用了比較輕鬆的方式介紹自己,如果你正苦腦自我介紹/自傳不知道怎麼寫,希望對你有個啟發的作用,至少可以像我一樣寫個驕情版、吹噓版,之後再來修也個方法 XD</p>
<blockquote>
<p>以上是我的簡單介紹,我們下回見!以下進入正題</p>
</blockquote>
<h2 id="%E6%96%87%E7%AB%A0%E6%98%AF%E4%BB%80%E9%BA%BC"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/01-article/#%E6%96%87%E7%AB%A0%E6%98%AF%E4%BB%80%E9%BA%BC">#</a> 文章是什麼</h2>
<p>首先,來聊聊文章是什麼呢?</p>
<p>大家小時候應該有寫過作文,會有一個題目,約 500 ~ 600 字的文字,我以前的基測 6 分 ( 滿分 12 分 ),對,我很不擅長寫文章,所以才會來教大家寫文章 (?),總之,會有一個主題、結構、文體,重點是有很多文字,如果不是跟我一樣有一種「看超過十行字就會死的病」,應該是不會覺得 600 字太多,所以很多文字就可以算文章嗎?我覺得因人而異,像很多的官方文件落落長,算文章嗎?這感覺又是可以戰的議題,等等,先別戰!</p>
<p>我認為廣義來說,官方文件、技術文章、農場文章、筆記、廢文都可以算文章,我要大家思考的是 : 「他們之間的區別是什麼?」</p>
<p>所以是什麼呢?我也不知道,這個真的很因人而異,知識含量、用詞深淺、文章結構都不一樣,而每個人的感覺也都不一樣,為了讓大家比較有畫面,很主觀的整理了一個表格給大家參考 :</p>
<table>
<thead>
<tr>
<th></th>
<th>官方文件</th>
<th>技術文章</th>
<th>農場文章</th>
<th>筆記</th>
<th>廢文</th>
</tr>
</thead>
<tbody>
<tr>
<td>知識含量</td>
<td>5</td>
<td>4</td>
<td>2</td>
<td>3</td>
<td>1</td>
</tr>
<tr>
<td>用詞深淺</td>
<td>5</td>
<td>4</td>
<td>2</td>
<td>3</td>
<td>1</td>
</tr>
<tr>
<td>有趣程度</td>
<td>1</td>
<td>2</td>
<td>4</td>
<td>3</td>
<td>5</td>
</tr>
<tr>
<td>文字多寡</td>
<td>5</td>
<td>4</td>
<td>3</td>
<td>2</td>
<td>?</td>
</tr>
<tr>
<td>文章結構</td>
<td>2</td>
<td>5</td>
<td>4</td>
<td>3</td>
<td>1</td>
</tr>
</tbody>
</table>
<p>對,文章可以很複雜,例如 : 文章的結構、內容、用字、受眾 ... 等,當然網路上有很多寫文章教學,也有不少寫文章的好處,我就不多說明,或許你就曾想過要寫文章、寫部落格,甚至也看過很多大神的寫文章教學,很多道理你也懂,但 ... 就是寫不出來啊!是的,這是很多人的心聲,當然,我也是。</p>
<blockquote>
<p>延伸閱讀 : <a href="https://hulitw.medium.com/how-do-i-write-965328ae91fe">我是如何完成一篇文章的? | by Huli</a></p>
</blockquote>
<p>對於沒有寫東西習慣的人來說,要完成一篇完整的文章,真的太難太難了...</p>
<p>但是!誰不是這樣來的,大神們也都是從糞扣、廢文開始的,只是他們開始的比較早。如果你開始了,你會很利害嗎?我不知道,但如果你想很利害,你一定得要開始!</p>
<p>所以我想,我的目標會先放在 : 讓沒寫東西習慣的人也能有寫東西的習慣,甚至可以寫出簡單的文章。</p>
<p>我不會講太深,那太複雜了,我不一定說的清楚,你不一定聽的明白,所以讓我們從更簡單的地方切入吧。</p>
<p>「看到一團文字,你覺得最重要的是什麼?」</p>
<p>先不論內容,如果沒使用標點符號,一定不用看,直接掰。其實呢,標點符號就是一種結構,是需要謹慎使用的。</p>
<blockquote>
<p>延伸閱讀 : <a href="https://github.com/sparanoid/chinese-copywriting-guidelines">中文文案排版指北</a></p>
</blockquote>
<p>好,有了標點符號的結構後,文字就會變成句子,那我們再往上看一層,文章的段落。文章一定也要分段,還記得作文中的<strong>起、承、轉、合</strong>嗎?沒錯,其他的不說,我們就把握這個原則就好!</p>
<p>上面講了這麼多,但我的重點只有一個,對!就是「結構」</p>
<h2 id="%E7%AD%86%E8%A8%98"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/01-article/#%E7%AD%86%E8%A8%98">#</a> 筆記</h2>
<p>: 「我是個 junior 工程師,我需要筆記嗎?」</p>
<p>「當然要呀!」</p>
<p>除非你是個練武神才,擁有過目不忘的能力,那你可以回上一頁了 XD</p>
<p>如果你沒有寫東西的習慣,最好的入門就是筆記,可以簡單寫點什麼,又可以幫助記憶,可謂一舉兩得啊。</p>
<p>但我也不太會寫筆記怎麼辨?別擔心,先試著在筆記加入結構的概念,簡單的一個 list ( 或是講 <code>li 元素</code> 會比較好懂 ) 就很好用了,可以參考子彈筆記一書,或是對 HTML 熟悉的人也可以將 DOM 模型的結構套入筆記中使用,其實也不是什麼很厲害的技術,大家都懂,但事實是 : 「不少人的筆記仍然雜亂無章」,也許可以試著將「結構」加入看看,也許會好很多了。</p>
<blockquote>
<p>推薦書藉 : <a href="https://www.books.com.tw/products/0010803003">子彈筆記術</a></p>
</blockquote>
<p>最最最基本的筆記結構,如下</p>
<pre class="language-markdown"><code class="language-markdown"><span class="token title important"><span class="token punctuation">###</span> 我的前端技能</span><br /><span class="token list punctuation">-</span> HTML<br /><span class="token list punctuation">-</span> CSS<br /><span class="token list punctuation">-</span> JavaScript</code></pre>
<p>對,你可以真的開一個記事本這樣寫,但是用記事本開起來就不是很好看,但是如果你知道 <code>Markdown</code> 這個語法,就會知道我在幹嘛,筆者是很推薦可以從 <code>HackMD</code> 練習寫筆記跟 <code>Markdown</code> 語法,如果你沒用過,不如就趁現在開始用用看吧,號稱可以 10 分鐘快速入門。</p>
<blockquote>
<p>HackMD : <a href="https://hackmd.io/c/tutorials-tw/%2Fs%2Fquick-start-tw">HackMD 使用教學</a></p>
</blockquote>
<p>好,簡單的筆記先到這邊,回到文章路部分。</p>
<p>筆者最近在參加一個程式導師計劃,算是轉職工程師的一個課程,因為是線上的,其中有一個制度是一周要交 5 篇進度報告,雖然「報告」聽起來很嚇人,但其實寫什麼都可以,用意是要讓老師知道你還有在呼吸、還活著這樣,這不是重點。筆者有一個朋友,覺得進度報告很難寫,於是某一天就寫下了 :</p>
<p>「今天好熱喔 ~~~」</p>
<p>嗯,就 5 個字,在座的各位先不要笑,要<strong>每天</strong>寫點東西,還真的不容易,如果你有試過應該就知道其中的困難。</p>
<p>但你知道還有更廢的文嗎?</p>
<p>「讚!」</p>
<p>沒錯,就是 Facebook 的「讚」,是一個使用的越多越會喪失思考的能力的按鈕,你可能會想有這麼嚴重嗎,有!</p>
<p>首先,小小的一個讚的 icon 只透露了使用者的名字跟「讚」,對於發文者來說沒什麼實質的幫助,只有空虛的讚數,顯得好像獲得很多關注,但是,一互獨處時,又會覺得那一刻出奇平淡,我相信很多人有這樣的感受。</p>
<p>再來是不少人為了這個讚的數量,而做了很多喪心病狂的事,族繁不及備載,如果把時間倒轉到 2008 年,有誰想得到,十幾年後的 Facebook 影響我們這麼多,多一個「讚」數,好像就覺得贏別了一點了,如果你也想擺脫這個「讚數上癮」,下面這本書也許有幫助。</p>
<blockquote>
<p>推薦書藉 : <a href="https://www.books.com.tw/products/0010843512">深度數位大掃除</a></p>
</blockquote>
<p>有看過最爛的評論家嗎?</p>
<p>我覺得好吃、我覺得好看、我覺得很棒 ...等,有沒有這種似曾相識的感覺,覺得對方是在敷衍,有的話,我覺得很棒,因為你已經開始意識到這是一個問題,要解決這個問題,沒有捷徑,只能多思考多表達。</p>
<p>我們,什麼時候失去了表達能力了,這樣講好像有點言之過重了,但確實大多數的人,在成長的過程中,沒有特別去培養獨立思考的能力,而通常文章寫的好的人,思考能力都比好,因為要獨立思考才能表達出自己的想法,甚至表達給大家聽的懂。</p>
<p>讓我們再回到上面的 : 「今天好熱喔 ~~~」</p>
<p>我們來試試看,如果要把它變成短文的話可以怎麼做呢?</p>
<ol>
<li>補知識點</li>
<li>加入標題</li>
<li>加入自身的故事</li>
</ol>
<pre class="language-markdown"><code class="language-markdown"><span class="token title important"><span class="token punctuation">###</span> 淺談天氣</span><br /><br />遇見妳的那天,是七月悶熱的天氣,如今又到七月。<br />今天也好熱喔,不確定是不是新高溫,但是今天 (2021/07/13) 台灣的用電來到了新高<br />可以參考 : https://money.udn.com/money/story/5648/5598949<br /></code></pre>
<p>這樣看起是不是有點樣子了,只內容有點廢 XD</p>
<p>但文章最重要的還是內容,一篇文章要表達的是什麼?</p>
<p>還記得上面的筆記嗎?沒錯,內容是要靠你平常累積的,可以用筆記記錄,其實什麼東西都可以筆記,心得感受、程式筆記、吃飯遊記 ...,什麼都可以筆記下來,之後都是你自己的資本,靈光乍現的時候,一篇文章就這麼不小心出來了。</p>
<p>文章最後,最難想的是往往是標題,來看一下常見的文章的標題,想不到的話,就挑一個來用吧 XD</p>
<p>常見的文章標題 : 淺談 ... 、初探 ... 、深入淺出的 ... 、從 ... 開始的 ... 、你所不知道的 ... 、我該學 ... 嗎、... 入門、...等</p>
<p>但是文章千萬不要這樣做 :</p>
<ol>
<li>超過 20% 的內容複製貼上別人文章,這樣找 5 篇都各複製 20% 不就完成文章啦,咦 ( ? )</li>
<li>沒附上參考資料,這問題也是蠻嚴重的,確實有不少人沒有注意,但對於原作者應該只是基本的尊重</li>
</ol>
<p>以上,文章可能就這樣不小心生出來了,如果還想看更多寫文章的技術,非常推薦去看下面的參考資料。</p>
<h2 id="%E6%9C%80%E5%BE%8C"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/01-article/#%E6%9C%80%E5%BE%8C">#</a> 最後</h2>
<p>不確定這篇算不算技術文章,但寫文章確實是一門技術,那我們來看一下今天學了什麼 :</p>
<p>文章簡單的架構 ( 起、承、轉、合 ) 有了、標題的模版也有了、文章的小提醒也有了,剩下的就是你的思考表達能力,這部分是要慢慢培養的,以目前台灣的教育來說是很少注重這一塊,所以下次當你按讚前,不要只是按讚,不妨先停下來多想一下,你當下的感受是什麼?為什麼會覺得這個讚?試著用自己的話表達看看。</p>
<p>看到這邊,應該大家對「寫文章」這件事,應該會有不同的想法,例如 : 好像有一點點覺得寫文章不是這麼難,也許沒有?好吧,我不知道,每個人的背景都不太一樣,但我自己確實是這樣慢慢一點一點養成的寫東西的習慣,感謝大家看到這裡!</p>
<p>也許,你現在就可以寫點東西,或是寫一篇簡單的廢文心得 ( ? )</p>
<h2 id="ref"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/01-article/#ref">#</a> Ref</h2>
<ul>
<li><a href="https://hulitw.medium.com/%E5%BB%A2%E6%96%87%E5%B7%A5%E4%BD%9C%E8%80%85%E7%9A%84%E9%A4%8A%E6%88%90-d05a5b7e539">廢文工作者的養成 | by Huli</a></li>
<li><a href="https://hulitw.medium.com/how-do-i-write-965328ae91fe">我是如何完成一篇文章的? | by Huli</a></li>
<li><a href="https://hulitw.medium.com/blog-e7a23a74ae2b">我為什麼寫部落格,以及部落格帶給我的影響 | by Huli</a></li>
<li><a href="https://medium.com/hulis-blog/why-blogging-ab77fd8c6ffa">每一篇心得都有價值——為什麼初學者才更應該要寫心得筆記| by Huli</a></li>
<li><a href="https://www.books.com.tw/products/0010803003">子彈筆記術</a></li>
<li><a href="https://www.books.com.tw/products/0010843512">深度數位大掃除</a></li>
</ul>
<blockquote>
<p>免責聲名</p>
</blockquote>
<p>以上均為筆者自身經驗,難免小有主觀意見,供讀者們參考,也歡迎分享經驗交流。<br />
如果有錯誤的地方還請大大們指正,筆者會立刻修改,再次感謝大家!</p>
<p><a href="https://creativecommons.org/licenses/by/4.0/"><img src="https://i.creativecommons.org/l/by/4.0/88x31.png" alt="Creative Commons License" /></a></p>
<p>本著作係採用 <a href="https://creativecommons.org/licenses/by/4.0/">創用 CC 姓名標示 4.0 國際授權條款</a> 授權。您可以在 <a href="https://benben.me/">benben.me</a> 找到我。</p>
<p>This work is licensed under a <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>. You can find me at <a href="https://benben.me/">benben.me</a></p>
優化舊有專案(一):加入 dotenv, nodemon, babel 優化開發
2021-08-22T00:00:00Z
https://blog.errorbaker.tw/posts/cwc329/dev-tools/
<!-- summary -->
<p>新手上路的工程師,在使用公司專案的時候發覺專案中使用很多開發上很好用的套件,這是我原本在學習路上沒有見過或者有看過但沒用過的東西,於是乎我就開始研究這些東西要怎麼使用,而我覺得最好的方法,就是在我自己原本的 side project 中導入這些,讓我看看到底要怎麼使用。</p>
<!-- summary -->
<h1 id="%E4%BD%BF%E7%94%A8%E7%9A%84%E5%B0%88%E6%A1%88---podcastify"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/dev-tools/#%E4%BD%BF%E7%94%A8%E7%9A%84%E5%B0%88%E6%A1%88---podcastify">#</a> 使用的專案 - Podcastify</h1>
<p>我使用的是我在 Lidemy 的期末專案,這是我與 <a href="https://github.com/Yu040419">Kuan Yu</a> 以及 <a href="https://github.com/sophiebetough"><br />
Sophie Chang</a> 一同合作開發。</p>
<p><a href="https://github.com/cwc329/mtr04-final-project-Podcastify">舊版專案原始碼</a></p>
<p>新版專案原始碼:<a href="https://github.com/cwc329/mtr04-final-project-Podcastify-ui">前端</a>、<a href="https://github.com/cwc329/mtr04-final-project-Podcastify-api">後端</a></p>
<p>這邊文章會以後端 api 專案的重構為主。</p>
<h2 id="dotenv"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/dev-tools/#dotenv">#</a> dotenv</h2>
<p>我開始工作之後才之後 dotenv 有多好用。<br />
簡單來說,dotenv 是一個可以幫開發者把 <code>.env</code> 檔案中的環境變數丟到 <code>process.env</code>中,這樣就可以在一個檔案中管理所有的環境變數。這樣我就可以把資料庫連線、第三方 API token、session secret 等東西都放在 <code>.env</code> 中,在部署的指令前就不需要宣告這些環境變數。</p>
<p>這個大概是導入最簡單的東西了,照著<a href="https://github.com/motdotla/dotenv">官方文件</a>走,只要先</p>
<pre class="language-shell"><code class="language-shell"><span class="token function">npm</span> <span class="token function">install</span> dotenv</code></pre>
<p>接著在專案的根目錄建立 <code>.env</code>,並且記得把 <code>.env</code> 加入 <code>.gitignore</code>,接下來在想要使用環境變數的檔案中使用 <code>dotenv</code> 即可。</p>
<p>在我這個專案中,影響最大的是 Sequelize 的 config 檔案,這是記錄資料庫連線帳號密碼的檔案,以往都是直接寫死,但是現在可以用 <code>.env</code> 控制之外,我甚至可以先去檢查這些變數是不是沒有給,並且拋出錯誤,讓我比較好 debug。</p>
<p><strong>config.js</strong></p>
<pre class="language-js"><code class="language-js"><span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"dotenv"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">config</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> <span class="token punctuation">{</span> <span class="token constant">DB_NAME</span><span class="token punctuation">,</span> <span class="token constant">DB_USER</span><span class="token punctuation">,</span> <span class="token constant">DB_PASSWORD</span><span class="token punctuation">,</span> <span class="token constant">DB_HOST</span><span class="token punctuation">,</span> <span class="token constant">DB_DIALECT</span> <span class="token punctuation">}</span> <span class="token operator">=</span> process<span class="token punctuation">.</span>env<span class="token punctuation">;</span><br /><br /><span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token constant">DB_NAME</span> <span class="token operator">||</span> <span class="token operator">!</span><span class="token constant">DB_USER</span> <span class="token operator">||</span> <span class="token operator">!</span><span class="token constant">DB_PASSWORD</span> <span class="token operator">||</span> <span class="token operator">!</span><span class="token constant">DB_HOST</span> <span class="token operator">||</span> <span class="token operator">!</span><span class="token constant">DB_DIALECT</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">"missing DB env"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">const</span> config <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">username</span><span class="token operator">:</span> <span class="token constant">DB_USER</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">password</span><span class="token operator">:</span> <span class="token constant">DB_PASSWORD</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">database</span><span class="token operator">:</span> <span class="token constant">DB_NAME</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">host</span><span class="token operator">:</span> <span class="token constant">DB_HOST</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">dialect</span><span class="token operator">:</span> <span class="token constant">DB_DIALECT</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br />module<span class="token punctuation">.</span>exports <span class="token operator">=</span> config<span class="token punctuation">;</span></code></pre>
<p>而且 docker 在執行 image 的時候,也可以注入 env file,將環境變數改用 env file 管理,也可以讓我之後部署,甚至是使用 ci/cd 的時候更加順利。</p>
<h2 id="nodemon"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/dev-tools/#nodemon">#</a> nodemon</h2>
<p>根據<a href="https://github.com/remy/nodemon#nodemon">官方文件</a>的描述</p>
<blockquote>
<p>nodemon is a tool that helps develop node.js based applications by automatically restarting the node application when file changes in the directory are detected.</p>
</blockquote>
<p>nodemon 是個開發上很好用的工具,我之前在開發 api 的時候,如果有 code 改變,我就要關掉目前的 app 並且重新啟動,這樣做有個缺點,就是當我忘記重啟的時候,就會因為執行的 code 與 IDE 的 code 不同,導致我會去解一些不存在的 bug。</p>
<p>而 nodemon 就是一款可以自動監聽工作資料夾,當這個資料夾的檔案有變動的時候,nodemon 就會自動幫重啟 app,這樣就可以保證執行的 code 是跟當前一樣的版本。</p>
<p>至於怎麼使用,也很簡單。依照官方文件有兩種方法可以安裝,global 以及 local,我個人比較喜歡 local 的安裝。</p>
<pre class="language-shell"><code class="language-shell"><span class="token function">npm</span> <span class="token function">install</span> nodemon --save-dev</code></pre>
<p>之後只要跑</p>
<pre><code>npx nodemon <path/to/app>
</code></pre>
<p>就可以了。</p>
<p>不過懶人如我就在 <code>package.json</code> 中定義了 npm script 去執行 nodemon<br />
<strong>package.json</strong></p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br /> <span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"test"</span><span class="token operator">:</span> <span class="token string">"echo \"Error: no test specified\" && exit 1"</span><span class="token punctuation">,</span><br /> <span class="token property">"dev"</span><span class="token operator">:</span> <span class="token string">"nodemon ./src/app.js"</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>這樣我只要下</p>
<pre><code>npm run dev
</code></pre>
<p>就可以執行 nodemon 幫我監控資料夾並且自動重啟 app,快速又方便。</p>
<h2 id="babel-node"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/dev-tools/#babel-node">#</a> babel-node</h2>
<p>使用 babel 其實是因為我比較想用 ES6 的語法,尤其是 <code>import</code> 與 <code>export</code> 開發,不過這樣在直接執行的時候會噴錯</p>
<pre><code>SyntaxError: Cannot use import statement outside a module
</code></pre>
<p>而使用 babel 先將語法作轉換,是一種避免這種錯誤的方法,同時也能讓開發更順利。</p>
<p>不過在導入 babel 就比較複雜一些,因為同時我也想要使用一些 import alias 讓我開發上能稍微省一些力氣,所以除了基本的 babel 之外,我還安裝了 import-resolver 這個插件。這個可以讓我在 <code>.babelrc</code> 裡面先設定好我想要的 import alias 並且可以在開發的時候使用,當 babel 要編譯的時候,就會依照預先設定好的 import alias 幫我轉換。</p>
<p>一樣點進<a href="https://babeljs.io/docs/en/usage">官方文件</a>依照步驟安裝</p>
<pre><code>npm install --save-dev @babel/core @babel/cli @babel/preset-env
</code></pre>
<p>並且創建 <code>.babelrc</code> 然後把下面的檔案內容複製過去,不過這邊有點不同,因為後端是使用 node.js 運行,所以不需要設定瀏覽器支援度,不過依然需要 preset-env 才能將新版的語法轉換,所以我的 <code>.babelrc</code> 長這樣</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br /> <span class="token property">"presets"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"@babel/preset-env"</span><span class="token punctuation">]</span><br /><span class="token punctuation">}</span></code></pre>
<p>不過這還沒完,剛開始說我想要使用 import alias,所以我又安裝了 <a href="https://www.npmjs.com/package/babel-plugin-module-resolver">import-resolver</a></p>
<pre><code>npm install --save-dev babel-plugin-module-resolver
</code></pre>
<p>依照官方文件的教學,我又在我的 <code>.babelrc</code> 中加入設定,最後長這樣</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br /> <span class="token property">"presets"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"@babel/preset-env"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token property">"plugins"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">[</span><br /> <span class="token string">"module-resolver"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"root"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"./"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token property">"alias"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"Middleware"</span><span class="token operator">:</span> <span class="token string">"./src/middleware"</span><span class="token punctuation">,</span><br /> <span class="token property">"Model"</span><span class="token operator">:</span> <span class="token string">"./src/models"</span><span class="token punctuation">,</span><br /> <span class="token property">"Util"</span><span class="token operator">:</span> <span class="token string">"./src/utils"</span><span class="token punctuation">,</span><br /> <span class="token property">"Constant"</span><span class="token operator">:</span> <span class="token string">"./src/constants"</span><span class="token punctuation">,</span><br /> <span class="token property">"Controller"</span><span class="token operator">:</span> <span class="token string">"./src/controllers"</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">]</span><br /> <span class="token punctuation">]</span><br /><span class="token punctuation">}</span></code></pre>
<p>我將 model, controller 等比較上層的 folder 都設定了 import alias,這樣之後我就不用去寫一堆相對路徑,我只要使用 alias 就可以輕鬆地引入我想要使用的東西。</p>
<p>最後的最後,還有一個重要的地方,原本的 nodemon 是直接使用 node 去執行的,所以使用 babel + ES6 開發會讓 nodemon 噴錯。這個時候就是 <a href="https://babeljs.io/docs/en/babel-node">@babel/node</a> 派上用場的時候了。這個是 babel 的一個 CLI,比 node.js 多了一項功能,就是可以先將程式碼做轉換。</p>
<p>一樣先安裝</p>
<pre><code>npm install --save-dev @babel/node
</code></pre>
<p>然後我再將 npm run dev 的指令稍做修改,讓它使用 @babel/node 執行</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br /> <span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"test"</span><span class="token operator">:</span> <span class="token string">"echo \"Error: no test specified\" && exit 1"</span><span class="token punctuation">,</span><br /> <span class="token property">"dev"</span><span class="token operator">:</span> <span class="token string">"nodemon --exec babel-node ./src/app.js"</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>這樣我的 nodemon 就可以順利執行了!</p>
<h1 id="%E5%B0%8F%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/dev-tools/#%E5%B0%8F%E7%B5%90">#</a> 小結</h1>
<p>到這邊,我使用一些簡單的工具,沒有很複雜的設定就大大的降低我開發的麻煩。這樣我就可以用比較習慣的語法,以及用比較簡潔的路徑來開發 api。不過這只是我把新的工具應用到就專案的第一步。</p>
<p>如果有第二集,我會來寫我是如何用 docker 封裝我的 app,以及如何使用 docker hub 提供的功能在我 push code 的時候自動幫我 build image。還有最後我是怎麼用 circleci 達成自動 build image 並且 publish。</p>
使用 Golang 建立一個 gRPC 架構
2021-08-28T00:00:00Z
https://blog.errorbaker.tw/posts/cian/grpc-unary/
<!-- summary -->
<!-- gRPC(Remote Procedure Calls)是 Google 發起的一個開源的 RPC 系統。目標是讓我們可以更輕鬆地創建分佈式應用程序和服務。這篇文章簡單介紹了 gRPC 和初步練習用 Go 寫出一個 gRPC 前後端。 -->
<!-- summary -->
<p>嗨,我是 Cian。<br />
最近意外成為了一個前後端各半的半半工程師,這篇文章和大家分享最近研究 gRPC 的成果。</p>
<h2 id="%E9%A6%96%E5%85%88%EF%BC%8C-rpc-%E6%98%AF%E4%BB%80%E9%BA%BC%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-unary/#%E9%A6%96%E5%85%88%EF%BC%8C-rpc-%E6%98%AF%E4%BB%80%E9%BA%BC%EF%BC%9F">#</a> 首先, RPC 是什麼?</h2>
<p>RPC 是 Remote Procedure Calls (遠端程序呼叫)的簡稱。<br />
簡單來說,RPC 是一種調用遠端程序的模式。它讓我們在使用遠端的 Procedure 的時候,可以像使用 local 調用一樣方便。<br />
Client 和 Server 都會有一個 Stub(樁),這個 Stub 會把底下的處理抽象化,因此對於 Client 端來說,他是和 Stub 互動,並不會在意他使用的這個函數實際上寫在哪。<br />
因為可以像是調用本地函式一樣調用遠端函式,這個模式解决了分布式系統中 Server 間的調用問題。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/cian/grpc-unary/rpc.png" alt="" /></p>
<p>這張圖簡單描述了整個 RPC 的過程:</p>
<ol>
<li>Client 的 Application 是這個 Procedure 的調用,他會調用 Client Stub 中的 Method</li>
<li>Client Stub 是一個把調用過程包裝起來的代理對象,他其實並不擁有 Client Application 想要調用的 Method,所以他會向外進行 RPC 的調用。</li>
<li>Client Run-time Library 是一個實現 RPC 調用的工具包</li>
<li>最後我們會透過底層網路實現 data 的傳輸。</li>
</ol>
<h2 id="grpc"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-unary/#grpc">#</a> gRPC</h2>
<p>接著,來看一下 gRPC 是什麼。<br />
gRPC(Remote Procedure Calls)是 Google 發起的一個開源的 RPC 系統。目標是讓我們可以更輕鬆地創建分佈式應用程序和服務。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/cian/grpc-unary/grpc-structure.png" alt="" /></p>
<p>gRPC 基於 HTTP/2 協定傳輸,並且使用 Protocol Buffers 作為介面描述語言(IDL)。<br />
使用 gRPC 的流程如下:</p>
<ol>
<li>在一個 <code>.proto</code> 的 Protocol Buffers 檔案中定義想要的數據類型和方法</li>
<li>使用 gRPC 的 CLI 指令進行編譯</li>
<li>在編譯之後,會根據這個文件生成 Stub 的 Interface,以及一些的 Accessors(訪問器)</li>
<li>透過實現這些 Interface 和 Accessors,我們可以輕鬆地用各種語言在各種數據流中讀寫結構化數據</li>
</ol>
<p>gRPC Client 和 Server 可以在各種環境中運行及進行通信,而且可以自由選用支援 gRPC 的語言進行開發。舉上圖的例子來說,我們可以在 Server 端,以 C++ 開發 gRPC Server,並且在 Client 端同時選用 Ruby 和 Android Java 來開發。</p>
<p>另外由於他有嚴格的 API 規範,因此也十分適合團隊使用。<br />
在這中間,因為只是想調用一下遠端的 Procedure,所以 RPC 其實常常選用傳輸效率更高的二進制傳輸,過程中會有序列化和反序列化的部分。</p>
<h2 id="protocol-buffers"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-unary/#protocol-buffers">#</a> Protocol Buffers</h2>
<p>gRPC 預設的 IDL 是 Protocol Buffers,雖然其實也可以使用像是 JSON 等其他數據格式,但當然最推薦使用的就是這個 IDL。它是一種序列化結構化數據。</p>
<p>Protocol Buffers 的<a href="https://developers.google.com/protocol-buffers">官網</a>上是這樣介紹自己的:</p>
<blockquote>
<p>Protocol buffers are Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler.</p>
</blockquote>
<p>簡單翻譯是「Protocol Buffers 是 Google 的語言原生、平台原生的可擴張機制。用於序列化結構化的 data。有點像是 XML,但更小、更快也更簡單。」</p>
<p>Protocol Buffers 適合用在高性能,對響應速度有要求的數據傳輸場景。因為它以二進制傳輸,所以數據本身不具有可讀性。我們會需要透過反序列化之後得到真正可讀的數據。</p>
<h2 id="grpc-%E7%9A%84%E5%9B%9B%E7%A8%AE%E7%94%9F%E5%91%BD%E9%80%B1%E6%9C%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-unary/#grpc-%E7%9A%84%E5%9B%9B%E7%A8%AE%E7%94%9F%E5%91%BD%E9%80%B1%E6%9C%9F">#</a> gRPC 的四種生命週期</h2>
<p>受惠於它基於 HTTP2 的技術,gRPC 支援四種生命週期,可供不同的使用場景選用:</p>
<pre class="language-plaintext"><code class="language-plaintext">Unary:一對一<br /> 一個 request 對上一個 response<br /><br />Client-side streaming: 多對一<br /> Client 送很多個請求(Streaming),結束之後 server 只回一個 response<br /><br />Server-side streaming:一對多<br /> Client 只上傳一個請求,但 Server 回一堆 response(Streaming)<br /><br />Bidirectional streaming:多對多<br /> Client 和 Server 都以 Streaming 的形式交互</code></pre>
<h2 id="%E5%AF%A6%E4%BD%9C-unary"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-unary/#%E5%AF%A6%E4%BD%9C-unary">#</a> 實作 Unary</h2>
<p>作為練習,我們實作一個 Unary 的 gRPC 系統。目標是 Client 給定一個座標,Server 會返回對應的景點名。</p>
<h2 id="install"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-unary/#install">#</a> install</h2>
<p>首先,今天要嘗試寫的是 Go 的 gRPC。<br />
需要,先到 <code>~/go/src</code> 底下建立一個這次專案的資料夾,這裡叫它 <code>grpc/</code> 。</p>
<p>接著安裝 grpc。</p>
<pre class="language-shell"><code class="language-shell">$ go <span class="token function">install</span> google.golang.org/protobuf/cmd/protoc-gen-go@v1.26<br />$ go <span class="token function">install</span> google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1</code></pre>
<p>和更新 PATH</p>
<pre class="language-shell"><code class="language-shell">$ <span class="token builtin class-name">export</span> <span class="token assign-left variable"><span class="token environment constant">PATH</span></span><span class="token operator">=</span><span class="token string">"<span class="token environment constant">$PATH</span>:<span class="token variable"><span class="token variable">$(</span>go <span class="token function">env</span> GOPATH<span class="token variable">)</span></span>/bin"</span></code></pre>
<ul>
<li>這一段請以官網內容優先:<a href="https://grpc.io/docs/languages/go/quickstart/">Quick start | Go | gRPC</a></li>
</ul>
<h2 id="%E5%AE%9A%E7%BE%A9-protocol-buffers-%E6%AA%94%E6%A1%88"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-unary/#%E5%AE%9A%E7%BE%A9-protocol-buffers-%E6%AA%94%E6%A1%88">#</a> 定義 Protocol Buffers 檔案</h2>
<p>在 <code>~/go/src/</code> 底下新增一個 <code>grpc/</code> 資料夾作為我們這次的專案資料夾。<br />
新增一個 <code>route.proto</code> 檔案。<br />
首先,定義要回傳的 message 類型,我們想回傳的是一個位置情報,會有這個地點的名字和他的座標。</p>
<pre class="language-text"><code class="language-text">// route.proto<br />/* 指定使用的是 proto3 的語法 */<br />syntax = 'proto3';<br /><br />option go_package = ".;route";<br /><br />/* option 不會改變聲明的整體含義,但可能會影響它在特定上下文中的處理方式 */<br /><br />package route;<br /><br />/* 定義要回傳的 message 類型,我們想回傳的是一個位置情報 */<br />/* 座標訊息 */<br />message Point {<br /> int32 latitude = 1; // 把一個數字作為key使用,可以壓縮長度。要從 1 開始。<br /> int32 longitude = 2;<br />}<br />/* 相關訊息 */<br />message Feature {<br /> string name = 1;<br /> Point location = 2;<br />}</code></pre>
<p>接著,定義要進行回傳的方法 <code>GetFeature</code>,他會接收一個座標情報,並且回傳這個座標上的景點訊息。</p>
<pre class="language-text"><code class="language-text">// route.proto<br /><br />service RouteGuide {<br /> // Unary<br /> rpc GetFeature(Point) returns (Feature) {}<br />}</code></pre>
<h3 id="go.mod"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-unary/#go.mod">#</a> go.mod</h3>
<p>我們會需要一個 <code>go.mod</code> 檔案來描述我們的專案將使用的 modules,不然後期可能會遇到引入問題。</p>
<pre class="language-go"><code class="language-go">module route<br /><br /><span class="token keyword">go</span> <span class="token number">1.16</span><br /><br />require <span class="token punctuation">(</span><br /> google<span class="token punctuation">.</span>golang<span class="token punctuation">.</span>org<span class="token operator">/</span>grpc v1<span class="token punctuation">.</span><span class="token number">38.0</span><br /> google<span class="token punctuation">.</span>golang<span class="token punctuation">.</span>org<span class="token operator">/</span>protobuf v1<span class="token punctuation">.</span><span class="token number">26.0</span><br /><span class="token punctuation">)</span></code></pre>
<h3 id="%E7%94%9F%E6%88%90-stub-code"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-unary/#%E7%94%9F%E6%88%90-stub-code">#</a> 生成 stub code</h3>
<p>接著,我們使用<a href="https://www.grpc.io/docs/languages/go/basics/#generating-client-and-server-code">官方提供的指令</a>產生 route.pb 文件和 route_grpc.pb 兩個文件。</p>
<p>我們這裡使用 go。</p>
<pre class="language-shell"><code class="language-shell">$ protoc <span class="token parameter variable">--go_out</span><span class="token operator">=</span>. <span class="token parameter variable">--go_opt</span><span class="token operator">=</span>paths<span class="token operator">=</span>source_relative --go-grpc_out<span class="token operator">=</span>. --go-grpc_opt<span class="token operator">=</span>paths<span class="token operator">=</span>source_relative route.proto</code></pre>
<ul>
<li>可能會遇到「Please specify a program using absolute path or make sure the program is available in your PATH system variable --go_out: protoc-gen-go: Plugin failed with status code 1.」error,可以嘗試 <code>go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest</code> 指令,詳情參考 <a href="https://stackoverflow.com/questions/60578892/protoc-gen-go-grpc-program-not-found-or-is-not-executable">這篇文章</a> ,對我有效。</li>
</ul>
<p>在剛剛的指令完成之後,我們在 <code>route.pb.go</code> 檔案中會找到我們定義的 message 的 struct:</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">type</span> Point <span class="token keyword">struct</span> <span class="token punctuation">{</span><br /> <span class="token comment">// ...</span><br /> Latitude <span class="token builtin">int32</span> <span class="token string">`protobuf:"varint,1,opt,name=latitude,proto3" json:"latitude,omitempty"`</span> <span class="token comment">// 把一個數字作為key使用,可以壓縮長度。要從 1 開始。</span><br /> Longitude <span class="token builtin">int32</span> <span class="token string">`protobuf:"varint,2,opt,name=longitude,proto3" json:"longitude,omitempty"`</span><br /><span class="token punctuation">}</span><br /></code></pre>
<p>和</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">type</span> Feature <span class="token keyword">struct</span> <span class="token punctuation">{</span><br /> <span class="token comment">//...</span><br /> Name <span class="token builtin">string</span> <span class="token string">`protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`</span><br /> Location <span class="token operator">*</span>Point <span class="token string">`protobuf:"bytes,2,opt,name=location,proto3" json:"location,omitempty"`</span><br /><span class="token punctuation">}</span><br /></code></pre>
<p>接著我們也可以在 <code>route_grpc.pb</code> 文件中找到一些 gRPC 生成的 Interface</p>
<p>Server</p>
<pre class="language-go"><code class="language-go"><span class="token comment">// RouteGuideServer is the server API for RouteGuide service.</span><br /><span class="token comment">// ...</span><br /><span class="token keyword">type</span> RouteGuideServer <span class="token keyword">interface</span> <span class="token punctuation">{</span><br /> <span class="token comment">// Unary</span><br /> <span class="token function">GetFeature</span><span class="token punctuation">(</span>context<span class="token punctuation">.</span>Context<span class="token punctuation">,</span> <span class="token operator">*</span>Point<span class="token punctuation">)</span> <span class="token punctuation">(</span><span class="token operator">*</span>Feature<span class="token punctuation">,</span> <span class="token builtin">error</span><span class="token punctuation">)</span><br /> <span class="token function">mustEmbedUnimplementedRouteGuideServer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<p>以及 Client</p>
<pre class="language-go"><code class="language-go"><span class="token comment">// RouteGuideClient is the client API for RouteGuide service.</span><br /><span class="token comment">// ...</span><br /><span class="token keyword">type</span> RouteGuideClient <span class="token keyword">interface</span> <span class="token punctuation">{</span><br /> <span class="token comment">// Unary</span><br /> <span class="token function">GetFeature</span><span class="token punctuation">(</span>ctx context<span class="token punctuation">.</span>Context<span class="token punctuation">,</span> in <span class="token operator">*</span>Point<span class="token punctuation">,</span> opts <span class="token operator">...</span>grpc<span class="token punctuation">.</span>CallOption<span class="token punctuation">)</span> <span class="token punctuation">(</span><span class="token operator">*</span>Feature<span class="token punctuation">,</span> <span class="token builtin">error</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<p>接下來,我們要分別在 Client 和 Server 把其中的方法實踐出來。</p>
<h2 id="client"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-unary/#client">#</a> Client</h2>
<p>這裡我們先寫 Client。<br />
Client 的目標是發送一個 Point type 的值並且取得該座標的景點情報。</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">package</span> main<br /><br /><span class="token keyword">import</span> <span class="token punctuation">(</span><br /> <span class="token string">"context"</span><br /> <span class="token string">"fmt"</span><br /> <span class="token string">"google.golang.org/grpc"</span><br /> <span class="token string">"log"</span><br /> pb <span class="token string">"route/route"</span><br /><span class="token punctuation">)</span><br /><br /><span class="token keyword">func</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> conn<span class="token punctuation">,</span>err <span class="token operator">:=</span> grpc<span class="token punctuation">.</span><span class="token function">Dial</span><span class="token punctuation">(</span><span class="token string">"localhost:5000"</span><span class="token punctuation">,</span>grpc<span class="token punctuation">.</span><span class="token function">WithInsecure</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> grpc<span class="token punctuation">.</span><span class="token function">WithBlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br /> <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> log<span class="token punctuation">.</span><span class="token function">Fatalln</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">defer</span> conn<span class="token punctuation">.</span><span class="token function">Close</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /> client <span class="token operator">:=</span> pb<span class="token punctuation">.</span><span class="token function">NewRouteGuideClient</span><span class="token punctuation">(</span>conn<span class="token punctuation">)</span><br /> feature<span class="token punctuation">,</span>err <span class="token operator">:=</span> client<span class="token punctuation">.</span><span class="token function">GetFeature</span><span class="token punctuation">(</span><br /> context<span class="token punctuation">.</span><span class="token function">Background</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token operator">&</span>pb<span class="token punctuation">.</span>Point<span class="token punctuation">{</span><br /> Latitude<span class="token punctuation">:</span> <span class="token number">353931000</span><span class="token punctuation">,</span><br /> Longitude<span class="token punctuation">:</span> <span class="token number">139444400</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">)</span><br /> <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> log<span class="token punctuation">.</span><span class="token function">Fatalln</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /><br /> fmt<span class="token punctuation">.</span><span class="token function">Println</span><span class="token punctuation">(</span>feature<span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<p>Dial 是 grpc 提供的一個方法,會是一個 dail 請求,第一個參數會說他要播向哪裡,之後是一些 option。<br />
<code>grpc.WithInsecure()</code>:因為現在 server 端沒有提供驗證,所以使用 Insecure 來跳過驗證<br />
<code>grpc.WithBlock()</code>:如果沒有成功就不讓他往下走的一個選項</p>
<p>NewRouteGuideClient 是 grpc 自動生成在 Client stub 產生的一個方法,可以在 <code>route_grpc.go</code> 找到它的定義,不過總之他會接收一個連接的 Interface,並且回傳一個 RouteGuideClient 類型的東西。</p>
<p>在製作時,會有像這樣的 suggest,調用 Server 的 GetFeature 就像是調用 local 的 method 一樣方便。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/cian/grpc-unary/get-feature-suggest.png" alt="" /></p>
<p>RouteGuideClient 是我們定義的方法的 Interface,我們可以調用 Interface 中指定好的 GetFeature 。</p>
<p>這時真正在做這個處理的是 Server 端,所以我們開始實踐 Server 端吧!</p>
<h3 id="server-side"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-unary/#server-side">#</a> Server Side</h3>
<p>這部分要做的事情有:</p>
<ol>
<li>製作資料庫(optional)</li>
<li>資料處理</li>
<li>開一個 Listener 監聽 request</li>
</ol>
<h2 id="%E8%B3%87%E6%96%99%E5%BA%AB"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-unary/#%E8%B3%87%E6%96%99%E5%BA%AB">#</a> 資料庫</h2>
<p>這裡我們做一個開發用的簡易 DB。</p>
<p>在 <code>route/</code> 資料夾旁邊生成一個 <code>route-server/</code> 資料夾,並新增一個 <code>server.go</code> 檔案,我們要在這裡實現 Server stub 的 interface。<br />
首先,定義 routeGuideServer 的 type</p>
<pre class="language-go"><code class="language-go"><span class="token comment">// server.go</span><br /><span class="token keyword">package</span> main<br /><br /><span class="token keyword">import</span> <span class="token punctuation">(</span><br /> pb <span class="token string">"route/route"</span> <span class="token comment">// 透過 `proto` 生成的 Server Stub</span><br /><span class="token punctuation">)</span><br /><br /><span class="token keyword">type</span> routeGuideServer <span class="token keyword">struct</span> <span class="token punctuation">{</span><br /> pb<span class="token punctuation">.</span>UnimplementedRouteGuideServer<br /><span class="token punctuation">}</span></code></pre>
<p>在這裡,因為在這個 <code>routeGuideServer</code> interface 中有一個 <code>mustEmbedUnimplementedRouteGuideServer()</code>,所以必須要有這個東西,是用來實現向上兼容的。</p>
<p>接著我們先做 Server 的假 DB 部分。<br />
在這個 <code>routeGuideServer</code> 中,加上這一行 DB 的 type 定義。</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">type</span> routeGuideServer <span class="token keyword">struct</span> <span class="token punctuation">{</span><br /> pb<span class="token punctuation">.</span>UnimplementedRouteGuideServer<br /> features <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token operator">*</span>pb<span class="token punctuation">.</span>Feature<br /><span class="token punctuation">}</span></code></pre>
<p>我們把這個 DB 叫做 <code>features</code>,裡面是有個一些 feature 的 Slice。</p>
<pre class="language-go"><code class="language-go"><span class="token comment">// server.go</span><br /><span class="token keyword">func</span> <span class="token function">dbServer</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">*</span>routeGuideServer <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token operator">&</span>routeGuideServer<span class="token punctuation">{</span><br /> features<span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token operator">*</span>pb<span class="token punctuation">.</span>Feature<span class="token punctuation">{</span><br /> <span class="token punctuation">{</span><br /> Name<span class="token punctuation">:</span> <span class="token string">"東京鐵塔"</span><span class="token punctuation">,</span><br /> Location<span class="token punctuation">:</span> <span class="token operator">&</span>pb<span class="token punctuation">.</span>Point <span class="token punctuation">{</span><br /> Latitude<span class="token punctuation">:</span> <span class="token number">353931000</span><span class="token punctuation">,</span><br /> Longitude<span class="token punctuation">:</span> <span class="token number">139444400</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> Name<span class="token punctuation">:</span> <span class="token string">"淺草寺"</span><span class="token punctuation">,</span><br /> Location<span class="token punctuation">:</span> <span class="token operator">&</span>pb<span class="token punctuation">.</span>Point <span class="token punctuation">{</span><br /> Latitude<span class="token punctuation">:</span> <span class="token number">357147651</span><span class="token punctuation">,</span><br /> Longitude<span class="token punctuation">:</span> <span class="token number">139794466</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> Name<span class="token punctuation">:</span> <span class="token string">"晴空塔"</span><span class="token punctuation">,</span><br /> Location<span class="token punctuation">:</span> <span class="token operator">&</span>pb<span class="token punctuation">.</span>Point <span class="token punctuation">{</span><br /> Latitude<span class="token punctuation">:</span> <span class="token number">357100670</span><span class="token punctuation">,</span><br /> Longitude<span class="token punctuation">:</span> <span class="token number">139808511</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<h3 id="%E8%B3%87%E6%96%99%E8%99%95%E7%90%86"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-unary/#%E8%B3%87%E6%96%99%E8%99%95%E7%90%86">#</a> 資料處理</h3>
<p>有了資料就可以開始處理了。<br />
這個 Server 的目標是在收到一個 request 時回傳回相對應的景點資訊,在這裡我們先簡單使用 for loop 遍歷進行比對,並回傳合適的資料。</p>
<p>也就是作出 API 中的 <code>GetFeature(context.Context, *Point) (*Feature, error)</code> 的實體:</p>
<pre class="language-go"><code class="language-go"><span class="token comment">// server.go</span><br /><span class="token comment">// import 要加上這兩個 Library</span><br /><span class="token keyword">import</span> <span class="token punctuation">(</span><br /> pb <span class="token string">"route/route"</span><br /> <span class="token string">"context"</span> <span class="token comment">// 新增</span><br /> <span class="token string">"google.golang.org/protobuf/proto"</span> <span class="token comment">// 新增</span><br /><span class="token punctuation">)</span><br /><br /><span class="token keyword">func</span> <span class="token punctuation">(</span>s <span class="token operator">*</span>routeGuideServer<span class="token punctuation">)</span> <span class="token function">GetFeature</span><span class="token punctuation">(</span>cxt context<span class="token punctuation">.</span>Context<span class="token punctuation">,</span> point <span class="token operator">*</span>pb<span class="token punctuation">.</span>Point<span class="token punctuation">)</span> <span class="token punctuation">(</span><span class="token operator">*</span>pb<span class="token punctuation">.</span>Feature<span class="token punctuation">,</span> <span class="token builtin">error</span><span class="token punctuation">)</span><span class="token punctuation">{</span><br /> <span class="token keyword">for</span> <span class="token boolean">_</span><span class="token punctuation">,</span>feature <span class="token operator">:=</span> <span class="token keyword">range</span> s<span class="token punctuation">.</span>features<span class="token punctuation">{</span><br /> <span class="token keyword">if</span> proto<span class="token punctuation">.</span><span class="token function">Equal</span><span class="token punctuation">(</span>feature<span class="token punctuation">.</span>Location<span class="token punctuation">,</span> point<span class="token punctuation">)</span><span class="token punctuation">{</span><br /> <span class="token keyword">return</span> feature<span class="token punctuation">,</span> <span class="token boolean">nil</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> <span class="token boolean">nil</span><span class="token punctuation">,</span> <span class="token boolean">nil</span><br /><span class="token punctuation">}</span></code></pre>
<h3 id="listener"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-unary/#listener">#</a> Listener</h3>
<p>最後是在 main 中開一個 Listener 監聽,如果沒有問題,就使用 gRPC 內建的 Server 來進行處理。</p>
<p>到這裡的 import 區</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">import</span> <span class="token punctuation">(</span><br /> <span class="token string">"log"</span> <span class="token comment">// 新增</span><br /> <span class="token string">"net"</span> <span class="token comment">// 新增</span><br /> <span class="token string">"context"</span><br /><br /> pb <span class="token string">"route/route"</span><br /> <span class="token string">"google.golang.org/protobuf/proto"</span><br /> <span class="token string">"google.golang.org/grpc"</span> <span class="token comment">// 新增 grpc library</span><br /><span class="token punctuation">)</span></code></pre>
<p>接著寫 main()</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">func</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// 生成一個listener</span><br /> lis<span class="token punctuation">,</span> err <span class="token operator">:=</span> net<span class="token punctuation">.</span><span class="token function">Listen</span><span class="token punctuation">(</span><span class="token string">"tcp"</span><span class="token punctuation">,</span> <span class="token string">"localhost:5000"</span><span class="token punctuation">)</span><br /> <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> log<span class="token punctuation">.</span><span class="token function">Fatalln</span><span class="token punctuation">(</span><span class="token string">"cannot create a listener a the address"</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token comment">// server</span><br /> grpcServer <span class="token operator">:=</span> grpc<span class="token punctuation">.</span><span class="token function">NewServer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> pb<span class="token punctuation">.</span><span class="token function">RegisterRouteGuideServer</span><span class="token punctuation">(</span>grpcServer<span class="token punctuation">,</span> <span class="token function">dbServer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br /> log<span class="token punctuation">.</span><span class="token function">Fatalln</span><span class="token punctuation">(</span>grpcServer<span class="token punctuation">.</span><span class="token function">Serve</span><span class="token punctuation">(</span>lis<span class="token punctuation">)</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<p>使用 <code>net.Listen</code> 生成一個 listener 之後,使用 <code>grpc.NewServer()</code> 產生一個 <code>grpcServer</code>,接著向 grpc 把這個 Server 登記在案。</p>
<p>之後就使用 <code>grpcServer.Serve(lis)</code> 來取得 Listener 拿到的資訊,就可以了。</p>
<p>兩邊都跑跑看,結果就會像這樣。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/cian/grpc-unary/grpc-server-running.png" alt="" /></p>
<p><img src="https://blog.errorbaker.tw/img/posts/cian/grpc-unary/result.png" alt="" /></p>
<p>到這裡就做到了簡單的 Unary,完成可以連接的前後端。</p>
<h3 id="%E9%87%8D%E6%A7%8B"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-unary/#%E9%87%8D%E6%A7%8B">#</a> 重構</h3>
<p>因為之後需要再開其他 api ,所以這裡先把 Unary 的相關處理放到函式 <code>getFeat</code> 裡面。</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">package</span> main<br /><br /><span class="token keyword">import</span> <span class="token punctuation">(</span><br /> <span class="token string">"context"</span><br /> <span class="token string">"fmt"</span><br /> pb <span class="token string">"github.com/keronscribe/learn-go/grpc-unary/route"</span><br /> <span class="token string">"google.golang.org/grpc"</span><br /> <span class="token string">"log"</span><br /><span class="token punctuation">)</span><br /><br /><span class="token keyword">func</span> getFeat <span class="token punctuation">(</span>client pb<span class="token punctuation">.</span>RouteGuideClient<span class="token punctuation">)</span><span class="token punctuation">{</span><br /> feature<span class="token punctuation">,</span>err <span class="token operator">:=</span> client<span class="token punctuation">.</span><span class="token function">GetFeature</span><span class="token punctuation">(</span>context<span class="token punctuation">.</span><span class="token function">Background</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token operator">&</span>pb<span class="token punctuation">.</span>Point<span class="token punctuation">{</span><br /> Latitude<span class="token punctuation">:</span> <span class="token number">353931000</span><span class="token punctuation">,</span><br /> Longitude<span class="token punctuation">:</span> <span class="token number">139444400</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span><span class="token punctuation">{</span><br /> log<span class="token punctuation">.</span><span class="token function">Fatalln</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> fmt<span class="token punctuation">.</span><span class="token function">Println</span><span class="token punctuation">(</span>feature<span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">func</span> main <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> conn<span class="token punctuation">,</span> err <span class="token operator">:=</span> grpc<span class="token punctuation">.</span><span class="token function">Dial</span><span class="token punctuation">(</span><span class="token string">"localhost:5000"</span><span class="token punctuation">,</span> grpc<span class="token punctuation">.</span><span class="token function">WithInsecure</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> grpc<span class="token punctuation">.</span><span class="token function">WithBlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br /><br /> <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span><span class="token punctuation">{</span><br /> log<span class="token punctuation">.</span><span class="token function">Fatalln</span><span class="token punctuation">(</span><span class="token string">"Client cannot dail grpc server"</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">defer</span> conn<span class="token punctuation">.</span><span class="token function">Close</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /> client <span class="token operator">:=</span> pb<span class="token punctuation">.</span><span class="token function">NewRouteGuideClient</span><span class="token punctuation">(</span>conn<span class="token punctuation">)</span><br /> <span class="token function">getFeat</span><span class="token punctuation">(</span>client<span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /></code></pre>
<p>今天就到這裡,下一篇文章會繼續嘗試剩下三種生命週期。</p>
<hr />
<h3 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-unary/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h3>
<p><a href="https://grpc.io/docs/what-is-grpc/introduction/">Introduction to gRPC | gRPC</a><br />
<a href="https://developers.google.com/protocol-buffers/docs/proto3?hl=en#simple">Language Guide (proto3) | Protocol Buffers | Google Developers</a><br />
<a href="https://www.grpc.io/docs/languages/go/basics/#generating-client-and-server-code">Basics tutorial | Go | gRPC</a><br />
<a href="https://ithelp.ithome.com.tw/articles/10207937">Day20-Go modules - iT 邦幫忙</a><br />
<a href="https://www.bilibili.com/video/BV1DV411s7ij">神奇代码在哪里Go实战#9:gRPC</a><br />
<a href="https://www.youtube.com/watch?v=hlyNZoaXvqU">「 gRPC Web 」で gRPC 実践! Go と gRPC で WebAPI を作ってみよう!!</a></p>
<hr />
<p>同步刊載於個人部落格 <a href="https://keronscribe.tw/golang-grpc-unary">使用 Golang 建立一個 gRPC 架構</a></p>
探討幾種常見的 CSS 命名慣例
2021-08-29T00:00:00Z
https://blog.errorbaker.tw/posts/simon198/css-naming-conventions/
<!-- summary -->
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/css-naming-conventions/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<p>工作一陣子之後發現其實大部分的工作時間都不是從 0 開始寫程式,而是在維護或整理之前的 code。這個時候遇到的困難常常不是這個 bug 該怎麼解,而花費很多的心力在看前人留下的 code。這時候就會發現其實每個人對 class 的命名都有些不一樣,常常有種雖然好像看的懂但卻沒有一個規則的感覺,所以今天就來探討一些 CSS 的命名慣例來讓我們的 class 更加淺顯易懂卻又不失實用性吧!</p>
<!-- summary -->
<!-- more -->
<h3 id="%E5%85%88%E8%AA%AA%E8%AA%AA%E4%B8%80%E4%BA%9B%E5%A4%A7%E9%83%A8%E5%88%86%E6%85%A3%E4%BE%8B%E9%83%BD%E5%85%B1%E9%80%9A%E7%9A%84%E5%9C%B0%E6%96%B9"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/css-naming-conventions/#%E5%85%88%E8%AA%AA%E8%AA%AA%E4%B8%80%E4%BA%9B%E5%A4%A7%E9%83%A8%E5%88%86%E6%85%A3%E4%BE%8B%E9%83%BD%E5%85%B1%E9%80%9A%E7%9A%84%E5%9C%B0%E6%96%B9">#</a> 先說說一些大部分慣例都共通的地方</h3>
<h5 id="%E5%91%BD%E5%90%8D%E5%BF%85%E9%A0%88%E6%9C%89%E8%AA%9E%E6%84%8F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/css-naming-conventions/#%E5%91%BD%E5%90%8D%E5%BF%85%E9%A0%88%E6%9C%89%E8%AA%9E%E6%84%8F">#</a> 命名必須有語意</h5>
<p>首先呢!跟其他程式語言的變數一樣的是給自己的 class 一個有語意的名字,而且不要隨意進行縮寫,如果真的要縮寫的話要注意那個縮寫會是大家都看得懂的,如果不確定的話就不要用。例如他是一個按鈕那就叫他 <code>button</code> ,頂多縮寫成約定俗成的 <code>btn</code>,不要縮寫成沒人知道是什麼東西的 <code>b</code> 之類的。</p>
<h5 id="%E6%B3%A8%E6%84%8F-case-%E7%9A%84%E5%95%8F%E9%A1%8C"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/css-naming-conventions/#%E6%B3%A8%E6%84%8F-case-%E7%9A%84%E5%95%8F%E9%A1%8C">#</a> 注意 case 的問題</h5>
<p>就是 case 的問題,在前端另外一個常用的語言 JavaScript 裡面,他的命名慣例會用 <code>camelCase</code>,但這不適用於 CSS。在 CSS 裡面比較常用 <code>-</code> 或是 <code>_</code> 來串連兩個字,至於到底要用 - 還是 _ 我等等會在多討論一些。</p>
<h5 id="%E6%8A%BD%E8%B1%A1%E5%8C%96%E5%91%BD%E5%90%8D"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/css-naming-conventions/#%E6%8A%BD%E8%B1%A1%E5%8C%96%E5%91%BD%E5%90%8D">#</a> 抽象化命名</h5>
<p>有的時候我們會因為它是一個會紅色的的 button 所以想要把他的 class 取名叫 <code>button-red</code> ,這個命名就很直觀可以看出來,但試想看看如果有一天專案想要把這個紅色的 button 改成藍色,那是不是就會發生不只要改 style,連 class 的命名也都要改變了!</p>
<p>所以這個時候其實可以把整個專案最常用的顏色設為 primary color,這樣那個 button 的命名就可以叫 <code>button-primary</code>,之後在改顏色的時候就只需要改 style 就好,不需要更動到 class 的名字。</p>
<p>另外一個範例是 layout 相關的,有的時候我們會因為他在 navbar 的左邊所以就把他取名字叫做 <code>nav-left</code>。但問題來了!當我們遇到 rwd 的時候他可能就會跑到下面去了!那這個時候 <code>nav-left</code> 的命名不夠精確,所以我們就可以用它的功能來命名,例如他是 logo 的話就可以叫 <code>nav-logo</code> 或直接叫 <code>logo</code> ,這樣不管 rwd 怎麼改都不會有不精確的情況發生了!</p>
<h3 id="%E5%B8%B8%E8%A6%8B%E7%9A%84%E5%91%BD%E5%90%8D%E6%85%A3%E4%BE%8B"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/css-naming-conventions/#%E5%B8%B8%E8%A6%8B%E7%9A%84%E5%91%BD%E5%90%8D%E6%85%A3%E4%BE%8B">#</a> 常見的命名慣例</h3>
<h3 id="oocss"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/css-naming-conventions/#oocss">#</a> OOCSS</h3>
<p>全名是 Object Orientied CSS,是在 2008 年的時候由網頁工程師 Nicole Sullivan 提出的!他的核心概念有兩個</p>
<ol>
<li>The separation of “structure” from “skin”</li>
<li>The separation of “container” from “content”</li>
</ol>
<p>翻成中文就是</p>
<ol>
<li>結構與樣式分離</li>
<li>內容與容器分離</li>
</ol>
<p>這樣的好處就是可以大大的增加 <code>CSS 的可覆用性</code> 跟 <code>CSS 的可擴展性</code>,就可以讓原本動輒幾百行的 CSS 變得更加精簡又好讀。</p>
<h5 id="%E7%B5%90%E6%A7%8B%E8%88%87%E6%A8%A3%E5%BC%8F%E5%88%86%E9%9B%A2"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/css-naming-conventions/#%E7%B5%90%E6%A7%8B%E8%88%87%E6%A8%A3%E5%BC%8F%E5%88%86%E9%9B%A2">#</a> 結構與樣式分離</h5>
<p>一般來說在寫 CSS 的時候我們會把樣式跟形狀寫在一起,但在 OOCSS 裡面可以把看得到的歸類成樣式,看不到的分類成結構,一樣用 button 來舉例:</p>
<p><img src="https://blog.errorbaker.tw/img/posts/simon198/buttons.png" alt="" /></p>
<p>這三個按鈕如果要做成結構與樣式分離的話就可以這樣寫:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">.btn</span> <span class="token punctuation">{</span><br /> <span class="token property">border-radius</span><span class="token punctuation">:</span> 8px<span class="token punctuation">;</span><br /> <span class="token property">border</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br /> <span class="token property">font-size</span><span class="token punctuation">:</span> 24px<span class="token punctuation">;</span><br /> <span class="token property">margin</span><span class="token punctuation">:</span> 12px<span class="token punctuation">;</span><br /> <span class="token property">padding</span><span class="token punctuation">:</span> 12px 24px<span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><span class="token selector">.btn-success</span> <span class="token punctuation">{</span><br /> <span class="token property">background-color</span><span class="token punctuation">:</span> green<span class="token punctuation">;</span><br /> <span class="token property">color</span><span class="token punctuation">:</span> white<span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><span class="token selector">.btn-warning</span> <span class="token punctuation">{</span><br /> <span class="token property">background-color</span><span class="token punctuation">:</span> orange<span class="token punctuation">;</span><br /> <span class="token property">color</span><span class="token punctuation">:</span> white<span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><span class="token selector">.btn-danger</span> <span class="token punctuation">{</span><br /> <span class="token property">background-color</span><span class="token punctuation">:</span> red<span class="token punctuation">;</span><br /> <span class="token property">color</span><span class="token punctuation">:</span> white<span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>我們用 btn 這一個 class 來管理這個 button 的結構,包含 padding,border 等等,其他會因為不同使用情境而有不同外觀的,我們就再各自用一個 class 來寫。這就是所謂的把結構和樣式分離。</p>
<h5 id="%E6%8A%8A%E5%AE%B9%E5%99%A8%E5%92%8C%E5%85%A7%E5%AE%B9%E5%88%86%E9%9B%A2"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/css-naming-conventions/#%E6%8A%8A%E5%AE%B9%E5%99%A8%E5%92%8C%E5%85%A7%E5%AE%B9%E5%88%86%E9%9B%A2">#</a> 把容器和內容分離</h5>
<p>另一個概念我們用一個跟 coding 比較不相關的車子來舉例好了,大家印象中的車子大概就是有四顆了輪子然後用引擎或馬達當動力在路上跑的。這個既定的印象其實就像是車子的容器,在這個容器裡面的內容卻會因為每一台車的需求不同會有所不一樣。</p>
<p>舉例來說一般的車子講求的是好開省油,所以裝的會是馬力比較小的引擎 ;但如果今天是賽車的話,就會需要馬力大的引擎來讓車子更快。這就是在一樣的容器內裝入不一樣的內容而產生出不一樣的產出。</p>
<h3 id="bem"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/css-naming-conventions/#bem">#</a> BEM</h3>
<p>BEM 是由 Yandex 公司所推出的一套命名規則,他是 Block, Element, Modifier 這三個單字的字首所組成的。他的優點同樣是增加 class 的可重用性和擴充性,但和 OOCSS 相比缺點就使他的 class 名字容易顯得比較冗長。至於到底是不是利大於弊就等大家看完我的介紹之後再自己判斷啦!</p>
<h5 id="%E5%9F%BA%E6%9C%AC%E6%9E%B6%E6%A7%8B"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/css-naming-conventions/#%E5%9F%BA%E6%9C%AC%E6%9E%B6%E6%A7%8B">#</a> 基本架構</h5>
<p>BEM 的基礎 CSS 架構大概是長這個樣子:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">.block</span> <span class="token punctuation">{</span><br /><span class="token punctuation">}</span><br /><span class="token selector">.block__element</span> <span class="token punctuation">{</span><br /><span class="token punctuation">}</span><br /><span class="token selector">.block__element--modifier</span> <span class="token punctuation">{</span><br /><span class="token punctuation">}</span></code></pre>
<h5 id="b---block"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/css-naming-conventions/#b---block">#</a> B - Block</h5>
<p>可以想成網頁是由一個又一個的 Block 所組成的,而在 BEM 中的的 block 會有以下的規範來幫助他達到可以重複使用且可以互相嵌入的特性。</p>
<ol>
<li>不能使用 CSS 標籤選擇器和 ID 選擇器。</li>
<li>Block 名稱需能清晰的表達出,其用途、功能或意義,且具有唯一性。</li>
<li>每個塊在邏輯上和功能上都相互獨立,在頁面上不能相依其他 Blocks 或元素。</li>
</ol>
<h5 id="e---element"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/css-naming-conventions/#e---element">#</a> E - Element</h5>
<p>Element 是 Block 的字元素,可以看作是整個 Block 的內容們,命名原則為 <code>__</code>(兩個下底線)之接 Element 的名稱,他有著以下的特性:</p>
<ol>
<li>沒有辦法獨立於 block 之外生存,換句話說 block 可以沒有 element,但 element 不可以沒有 block</li>
<li>表達的是目的,而不是狀態</li>
</ol>
<h5 id="m---modifier"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/css-naming-conventions/#m---modifier">#</a> M - Modifier</h5>
<p>Modifier 則是作為表達 block 或是 element 的屬性或是狀態使用,命名原則為在 block 或是 element 之後加上 <code>--</code> 之後再加上 modifier 的名稱。他的特性如下:</p>
<ol>
<li>不能脱離 Block 或 Element 使用。</li>
<li>應該改變的是實體的外觀,行為或狀態,而不是替換它。</li>
<li>不能同時使用兩個相同屬性卻不同值的 modifier</li>
</ol>
<h4 id="%E5%AF%A6%E4%BE%8B"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/css-naming-conventions/#%E5%AF%A6%E4%BE%8B">#</a> 實例</h4>
<p>那就讓我們用下面這張 card 來舉例 BEM 的實作</p>
<p><img src="https://blog.errorbaker.tw/img/posts/simon198/card.png" alt="" /></p>
<p>首先這張卡片的 html 是長這樣子的:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>card<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h2</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>card__title<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>我是標題<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h2</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>card__content<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> 很多內容很多內容....<br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>card__content--important<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> 重要的內容 <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span><br /> 還是很多內容很多內容....<br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>這邊可以看到在 <code>card</code> 這一個 block 裡面存在著 <code>card__title</code> 和 <code>card__content</code> 這兩個 element 而在 card**content 裡面又有一個 <code>card**content--important</code> 這一個 modifier。</p>
<h5 id="block"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/css-naming-conventions/#block">#</a> block</h5>
<pre class="language-css"><code class="language-css"><span class="token selector">.card</span> <span class="token punctuation">{</span><br /> <span class="token property">border</span><span class="token punctuation">:</span> 1px solid black<span class="token punctuation">;</span><br /> <span class="token property">border-radius</span><span class="token punctuation">:</span> 8px<span class="token punctuation">;</span><br /> <span class="token property">box-shadow</span><span class="token punctuation">:</span> 2px 2px <span class="token function">rgba</span><span class="token punctuation">(</span>0<span class="token punctuation">,</span> 0<span class="token punctuation">,</span> 0<span class="token punctuation">,</span> 0.6<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token property">margin</span><span class="token punctuation">:</span> 24px<span class="token punctuation">;</span><br /> <span class="token property">height</span><span class="token punctuation">:</span> 120px<span class="token punctuation">;</span><br /> <span class="token property">width</span><span class="token punctuation">:</span> 250px<span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>在這張卡片裡面 block 決定的整個區塊的大小和形狀,並且不影響外部的 style,使他可以很容易地與其他的 block 並且不會互相干擾。</p>
<h5 id="element"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/css-naming-conventions/#element">#</a> Element</h5>
<pre class="language-css"><code class="language-css"><span class="token selector">.card__title</span> <span class="token punctuation">{</span><br /> <span class="token property">font-size</span><span class="token punctuation">:</span> 24px<span class="token punctuation">;</span><br /> <span class="token property">padding</span><span class="token punctuation">:</span> 12px<span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><span class="token selector">.card__content</span> <span class="token punctuation">{</span><br /> <span class="token property">font-size</span><span class="token punctuation">:</span> 14px<span class="token punctuation">;</span><br /> <span class="token property">padding</span><span class="token punctuation">:</span> 4px 12px<span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>在這裡因為我們希望這個張卡片裡面有一個標題跟一段內容,所以就加入了兩個 element。</p>
<p>這兩個 element 只有定義一下他基本的 style 而沒有詳細的屬性,並且如果把他單獨放到外面的話就只是兩行文字,就失去了 title 跟 content 的作用。</p>
<h5 id="modifiter"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/css-naming-conventions/#modifiter">#</a> modifiter</h5>
<pre class="language-css"><code class="language-css"><span class="token selector">.card__content--important</span> <span class="token punctuation">{</span><br /> <span class="token property">background-color</span><span class="token punctuation">:</span> red<span class="token punctuation">;</span><br /> <span class="token property">color</span><span class="token punctuation">:</span> white<span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>這邊我們加了一個 modifier 來讓重要的文字反紅,如果想要在 block 或是 element 上面加上屬性或是狀態的話就利用 modifier 來加!</p>
<h2 id="%E7%B8%BD%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/css-naming-conventions/#%E7%B8%BD%E7%B5%90">#</a> 總結</h2>
<p>其實要要把 CSS 寫得好,不外乎是想要增加 class 可重複利用性以及可擴充性,這兩套命名慣例也都可以很好的達到這兩個目的。所以如果開發前先規劃遵循的命名規則就可以有效的增加自己的效率,也可以讓之後在閱讀的人可以更有效率的上手哦!</p>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/css-naming-conventions/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li><a href="https://hackernoon.com/best-practice-in-css-organisation-and-naming-conventions-4d103ujy">Best Practice in CSS: Organisation and Naming Conventions</a></li>
<li><a href="https://wcc723.github.io/css/2016/12/02/oocss-one/">鐵人賽 2 - OOCSS 結構與樣式、容器與內容</a></li>
<li><a href="http://getbem.com/">BEM</a></li>
</ul>
你可能不需要 React Memo
2021-08-29T00:00:00Z
https://blog.errorbaker.tw/posts/tian/you-might-not-need-react-memo/
<h2 id="%E5%85%88%E8%AA%AA%E8%AA%AA%E6%88%91%E7%9A%84%E7%B5%90%E8%AB%96"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/you-might-not-need-react-memo/#%E5%85%88%E8%AA%AA%E8%AA%AA%E6%88%91%E7%9A%84%E7%B5%90%E8%AB%96">#</a> 先說說我的結論</h2>
<!-- summary -->
<p>對 React 的 Functional Component 做渲染效能優化,不一定要使用 <code>memo</code> 來達成,也可以透過元件的重組或 <code>useMemo</code> 來達成。</p>
<!-- summary -->
<p>值得注意的是,每一種優化都會帶來相應的成本(犧牲可讀性以及開發效率),先釐清需求之後再來使用,才能取得綜效。</p>
<!-- more -->
<h2 id="%E8%AA%B0%E6%AF%94%E8%BC%83%E9%81%A9%E5%90%88%E9%96%B1%E8%AE%80%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/you-might-not-need-react-memo/#%E8%AA%B0%E6%AF%94%E8%BC%83%E9%81%A9%E5%90%88%E9%96%B1%E8%AE%80%EF%BC%9F">#</a> 誰比較適合閱讀?</h2>
<ul>
<li>使用 React v16.6+ 的開發者</li>
<li>你知道 <code>memo</code> 的存在,但不確定應該在哪些情境下使用它們。</li>
</ul>
<p>如果你不是上述對象,也沒有上述問題,你可以考慮改去讀讀其他夥伴們的<a href="https://blog.errorbaker.tw/">優秀作品</a> ~</p>
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/you-might-not-need-react-memo/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<p>直接寫出效能與可讀性兼具的 React 程式碼,我認為這是在各種前端優化方式中[1],最容易順手做到的一種,你一定聽過 <code>memo</code> ,但你知道怎麼使用它寫出可讀性和效能兼具的程式碼嗎?至少在寫這篇文章前,我沒辦法很肯定的說出我知道...</p>
<h3 id="%E9%80%99%E7%AF%87%E6%96%87%E7%AB%A0%E4%B9%8B%E6%89%80%E4%BB%A5%E5%AD%98%E5%9C%A8%EF%BC%8C%E6%98%AF%E5%9B%A0%E7%82%BA%E6%88%91%E6%83%B3%E8%A6%81%E8%A7%A3%E6%B1%BA%E7%9A%84%E8%87%AA%E5%B7%B1%E7%9A%84%E5%B9%BE%E5%80%8B%E5%95%8F%E9%A1%8C"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/you-might-not-need-react-memo/#%E9%80%99%E7%AF%87%E6%96%87%E7%AB%A0%E4%B9%8B%E6%89%80%E4%BB%A5%E5%AD%98%E5%9C%A8%EF%BC%8C%E6%98%AF%E5%9B%A0%E7%82%BA%E6%88%91%E6%83%B3%E8%A6%81%E8%A7%A3%E6%B1%BA%E7%9A%84%E8%87%AA%E5%B7%B1%E7%9A%84%E5%B9%BE%E5%80%8B%E5%95%8F%E9%A1%8C">#</a> 這篇文章之所以存在,是因為我想要解決的自己的幾個問題</h3>
<ul>
<li>為什麼需要 <code>memo</code>?在什麼情境下需要 <code>memo</code>?</li>
<li><code>memo</code> 背後的 shallow compare 是怎麼運作的 ?</li>
<li>使用 <code>memo</code> 要付出什麼樣的成本?</li>
<li>有了 <code>useMemo</code> 之後,還有使用 <code>memo</code> 的場景嗎?</li>
</ul>
<h3 id="%E5%BE%9E%E6%83%85%E5%A2%83%E9%96%8B%E5%A7%8B"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/you-might-not-need-react-memo/#%E5%BE%9E%E6%83%85%E5%A2%83%E9%96%8B%E5%A7%8B">#</a> 從情境開始</h3>
<p>我相信每個技術,都是解決特定場景下的特定問題,所以要能理解一個技術的使用,我覺得一定要從問題場景開始說起,</p>
<p>如果你也使用 React 開發,你會知道 Component 每次的 state 的改變都會讓下層所有 Component re-render,元件層數少的時候可能還感覺不出來,一旦元件層數和數量增加,每一次的 re-render 都是成倍數的增加,你前端應用程式效能也會一起跟著變慢。有沒有一種元件,是只有 props 改變時,元件才會重新渲染呢?有,那就是 Pure Component。</p>
<h3 id="%E5%B9%AB%E8%87%AA%E5%B7%B1%E5%AF%AB%E5%80%8B-user-story"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/you-might-not-need-react-memo/#%E5%B9%AB%E8%87%AA%E5%B7%B1%E5%AF%AB%E5%80%8B-user-story">#</a> 幫自己寫個 User Story</h3>
<p>我希望只有當 Component 的 props 和內部的 state 改變的時候,元件才會 re-render,這樣就可以避免不必要的渲染,增加前端應用程式的效能。</p>
<h2 id="%E4%BB%80%E9%BA%BC%E6%98%AF-pure-component-%3F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/you-might-not-need-react-memo/#%E4%BB%80%E9%BA%BC%E6%98%AF-pure-component-%3F">#</a> 什麼是 Pure Component ?</h2>
<p>借用 Pure Function 這個概念,可能會比較好理解,Pure Function 指的是</p>
<blockquote>
<p>一個 function return 的 value,只受 function 的 parameter 決定。</p>
</blockquote>
<p>同理,你可以想像 Functional Component 也是一個 function</p>
<blockquote>
<p>一個 Component 當下渲染的結果,只受 props 的影響,只要 props 相同,re-render 的結果就會相同</p>
</blockquote>
<p>我們可以利用這個特性,將 Component 上一次渲染的結果記憶起來,當上層 Component 的狀態改變,沒有影響到 props 的時候就不需要 re-render,這樣就可以避免不必要的渲染,那我們就可以稱這樣的 Component 為 Pure Component。</p>
<p>與原來一般的 Component 最大的差別是 Pure Component 只有在上層傳給 Component 的 props 改變的時候,才會 re-render。</p>
<h3 id="%E5%A6%82%E4%BD%95%E5%B0%87%E4%B8%80%E5%80%8B-functional-component-%E6%94%B9%E9%80%A0%E6%88%90-pure-component-%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/you-might-not-need-react-memo/#%E5%A6%82%E4%BD%95%E5%B0%87%E4%B8%80%E5%80%8B-functional-component-%E6%94%B9%E9%80%A0%E6%88%90-pure-component-%EF%BC%9F">#</a> 如何將一個 Functional Component 改造成 Pure Component ?</h3>
<p>React 提供了一個 HOC —— <code>memo</code> [2],只要將 <code>memo</code> 包在 Functional Component 外面,就可以將 Component 改造成 Pure Component</p>
<pre class="language-tsx"><code class="language-tsx"><span class="token keyword">function</span> <span class="token function">FunctionalComponent</span><span class="token punctuation">(</span>props<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">/* render using props */</span><br /><span class="token punctuation">}</span><br /><span class="token keyword">const</span> PureComponent <span class="token operator">=</span> React<span class="token punctuation">.</span><span class="token function">memo</span><span class="token punctuation">(</span>FunctionalComponent<span class="token punctuation">)</span> <span class="token comment">// default shallow compare</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> PureComponent</code></pre>
<pre class="language-tsx"><code class="language-tsx"><span class="token keyword">function</span> <span class="token function">FunctionalComponent</span><span class="token punctuation">(</span>props<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">/* render using props */</span><br /><span class="token punctuation">}</span><br /><span class="token keyword">function</span> <span class="token function">shouldPreventReRendering</span><span class="token punctuation">(</span>prevProps<span class="token punctuation">,</span> nextProps<span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><br /><br /><span class="token keyword">const</span> PureComponentWithCustomCompareFunction <span class="token operator">=</span> React<span class="token punctuation">.</span><span class="token function">memo</span><span class="token punctuation">(</span><br /> FunctionalComponent<span class="token punctuation">,</span> <br /> shouldPreventReRendering <span class="token comment">// custom compare function</span><br /><span class="token punctuation">)</span><br /><span class="token keyword">export</span> <span class="token keyword">default</span> PureComponentWithCustomCompareFunction</code></pre>
<h2 id="react.memo-%E5%B9%AB%E6%88%91%E5%80%91%E5%81%9A%E4%BA%86%E4%BB%80%E9%BA%BC-%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/you-might-not-need-react-memo/#react.memo-%E5%B9%AB%E6%88%91%E5%80%91%E5%81%9A%E4%BA%86%E4%BB%80%E9%BA%BC-%EF%BC%9F">#</a> React.memo 幫我們做了什麼 ?</h2>
<p>要實現 Pure Component 需要做到兩件事情,記憶和對照</p>
<ol>
<li>
<p>記憶:記憶這次渲染的 props 是什麼</p>
</li>
<li>
<p>對照:每次上層元件 re-render 的看看記憶中的 <code>prevProps</code> 和當下的 <code>nextProps</code> 有沒有差別,如果沒有客製化 <code>compare</code> function,預設是使用 <code>shallowEqual</code> [3] 做對照。</p>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">const</span> compare <span class="token operator">=</span><br /> shouldPreventReRendering <span class="token comment">// custom compare function</span><br /> <span class="token operator">||</span> shallowEqual <span class="token comment">// default compare function</span><br /><span class="token function">compare</span><span class="token punctuation">(</span>prevProps<span class="token punctuation">,</span> nextProps<span class="token punctuation">)</span> <span class="token comment">// if true prevent rerender</span></code></pre>
<pre class="language-jsx"><code class="language-jsx"><span class="token comment">/*<br /> shallowly compare the component props<br /> if passing nextProps to render would return the same result as passing prevProps to render,<br /> return `true`<br /> otherwise, return false<br />*/</span><br /><span class="token keyword">function</span> <span class="token function">shallowEqual</span><span class="token punctuation">(</span><span class="token parameter"><span class="token literal-property property">objA</span><span class="token operator">:</span> mixed<span class="token punctuation">,</span> <span class="token literal-property property">objB</span><span class="token operator">:</span> mixed</span><span class="token punctuation">)</span><span class="token operator">:</span> boolean <span class="token punctuation">{</span><br /> <span class="token comment">// prevProps 和 nextProps 完全相同</span><br /> <span class="token comment">// P.S. 不太清楚什麼情況下 prevProps 和 nextProps 會完全相同,如果你知道的話可以告訴我嗎?</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>Object<span class="token punctuation">.</span><span class="token function">is</span><span class="token punctuation">(</span>objA<span class="token punctuation">,</span> objB<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token comment">// not render</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><br /> <span class="token keyword">typeof</span> objA <span class="token operator">!==</span> <span class="token string">'object'</span> <span class="token operator">||</span><br /> objA <span class="token operator">===</span> <span class="token keyword">null</span> <span class="token operator">||</span> <span class="token comment">// typeof null === 'object'</span><br /> <span class="token keyword">typeof</span> objB <span class="token operator">!==</span> <span class="token string">'object'</span> <span class="token operator">||</span><br /> objB <span class="token operator">===</span> <span class="token keyword">null</span><br /> <span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token comment">// pervProps 和 nextProps 都是 object</span><br /> <span class="token comment">// props 改變:prop 數量增減</span><br /> <span class="token keyword">const</span> keysA <span class="token operator">=</span> Object<span class="token punctuation">.</span><span class="token function">keys</span><span class="token punctuation">(</span>objA<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> keysB <span class="token operator">=</span> Object<span class="token punctuation">.</span><span class="token function">keys</span><span class="token punctuation">(</span>objB<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>keysA<span class="token punctuation">.</span>length <span class="token operator">!==</span> keysB<span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span> <span class="token comment">// re-render</span><br /> <span class="token punctuation">}</span><br /> <br /> <span class="token comment">// Test for A's keys different from B.</span><br /> <span class="token comment">// props 改變: prop 不相同</span><br /> <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator"><</span> keysA<span class="token punctuation">.</span>length<span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><br /> <span class="token comment">// nextProps 的 key 和 prevProps 上的 key 不完全相同</span><br /> <span class="token operator">!</span>Object<span class="token punctuation">.</span><span class="token function">hasOwnProperty</span><span class="token punctuation">.</span><span class="token function">call</span><span class="token punctuation">(</span>objB<span class="token punctuation">,</span> keysA<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token operator">||</span><br /> <span class="token comment">// nextProps 上 key 的 value 和 prevProps 上 key 的 value 不相同</span><br /> <span class="token operator">!</span>Object<span class="token punctuation">.</span><span class="token function">is</span><span class="token punctuation">(</span>objA<span class="token punctuation">[</span>keysA<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">,</span> objB<span class="token punctuation">[</span>keysA<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">)</span><br /> <span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span> <span class="token comment">// re-render</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /> <br /> <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token comment">// not render</span><br /><span class="token punctuation">}</span></code></pre>
</li>
</ol>
<h2 id="trade-off-%E5%9C%A8%E4%BD%BF%E7%94%A8-memo-%E4%B9%8B%E5%89%8D-%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/you-might-not-need-react-memo/#trade-off-%E5%9C%A8%E4%BD%BF%E7%94%A8-memo-%E4%B9%8B%E5%89%8D-%EF%BC%9F">#</a> Trade-Off 在使用 memo 之前 ?</h2>
<blockquote>
<p>任何的優化都有成本,只有在 Z > B 的時候才使用 memo [4]</p>
</blockquote>
<p>總得來說,我想要盡量的找到可讀性和效能兼具的模式,並且熟練的運用這些模式</p>
<h3 id="you-might-not-need-memo-%E2%80%94%E2%80%94-%E9%80%8F%E9%81%8E%E7%B5%84%E5%90%88%E7%9A%84%E6%96%B9%E5%BC%8F%EF%BC%8C%E5%B0%87-state-%E5%92%8C%E4%B8%8D%E6%83%B3%E8%A2%AB-re-render-%E7%9A%84%E5%8D%80%E5%A1%8A%E6%8B%86%E9%96%8B%5B5%5D"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/you-might-not-need-react-memo/#you-might-not-need-memo-%E2%80%94%E2%80%94-%E9%80%8F%E9%81%8E%E7%B5%84%E5%90%88%E7%9A%84%E6%96%B9%E5%BC%8F%EF%BC%8C%E5%B0%87-state-%E5%92%8C%E4%B8%8D%E6%83%B3%E8%A2%AB-re-render-%E7%9A%84%E5%8D%80%E5%A1%8A%E6%8B%86%E9%96%8B%5B5%5D">#</a> You might not need <code>memo</code> —— 透過組合的方式,將 state 和不想被 re-render 的區塊拆開[5]</h3>
<p>每次 setText 都會讓 <code><ExpensiveCalculation /></code> 重新渲染,為了避免不必要的 re-render,我們可以用一下兩種重組元件的方式來避免。</p>
<pre class="language-tsx"><code class="language-tsx"><span class="token keyword">const</span> <span class="token function-variable function">App</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>text<span class="token punctuation">,</span> setText<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token string">''</span><span class="token punctuation">)</span><br /> <span class="token keyword">return</span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>input</span> <br /> <span class="token attr-name">value</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>text<span class="token punctuation">}</span></span><br /> <span class="token attr-name">onClick</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>e <span class="token operator">=></span> <span class="token function">setText</span><span class="token punctuation">(</span>e<span class="token punctuation">.</span>target<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">}</span></span><br /> <span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">ExpensiveCalculation</span></span> <span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span></span><span class="token punctuation">></span></span><br /><span class="token punctuation">}</span></code></pre>
<ol>
<li>
<p>將元件拆小,減少 state 的影響範圍,來避免 re-render</p>
<pre class="language-tsx"><code class="language-tsx"><span class="token keyword">const</span> <span class="token function-variable function">App</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">TextInput</span></span> <span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">ExpensiveCalculation</span></span> <span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span></span><span class="token punctuation">></span></span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">const</span> <span class="token function-variable function">TextInput</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>text<span class="token punctuation">,</span> setText<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token string">''</span><span class="token punctuation">)</span><br /> <span class="token keyword">return</span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>input</span> <br /> <span class="token attr-name">value</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>text<span class="token punctuation">}</span></span><br /> <span class="token attr-name">onClick</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>e <span class="token operator">=></span> <span class="token function">setText</span><span class="token punctuation">(</span>e<span class="token punctuation">.</span>target<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">}</span></span><br /> <span class="token punctuation">/></span></span><br /><span class="token punctuation">}</span></code></pre>
</li>
<li>
<p>透過 children,來避免 re-render</p>
<pre class="language-tsx"><code class="language-tsx"><span class="token keyword">const</span> <span class="token function-variable function">App</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">TextInput</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">ExpensiveCalculation</span></span> <span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span><span class="token class-name">TextInput</span></span><span class="token punctuation">></span></span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">const</span> <span class="token function-variable function">TextInput</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">{</span> children <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>text<span class="token punctuation">,</span> setText<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token string">''</span><span class="token punctuation">)</span><br /> <span class="token keyword">return</span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>input</span> <br /> <span class="token attr-name">value</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>text<span class="token punctuation">}</span></span><br /> <span class="token attr-name">onClick</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>e <span class="token operator">=></span> <span class="token function">setText</span><span class="token punctuation">(</span>e<span class="token punctuation">.</span>target<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">}</span></span><br /> <span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token punctuation">{</span>children<span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span></span><span class="token punctuation">></span></span><br /><span class="token punctuation">}</span></code></pre>
</li>
</ol>
<h2 id="%E6%97%A2%E7%84%B6%E9%9D%A0%E9%87%8D%E7%B5%84%E5%B0%B1%E5%8F%AF%E4%BB%A5%E7%B0%A1%E5%96%AE%E8%A7%A3%E6%B1%BA%E6%95%88%E8%83%BD%E5%95%8F%E9%A1%8C%EF%BC%8C%E9%82%A3%E6%9C%89%E6%B2%92%E6%9C%89%E4%BB%80%E9%BA%BC%E9%9D%9E%E7%94%A8-memo-%E4%B8%8D%E5%8F%AF%E7%9A%84%E6%83%85%E6%B3%81-%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/you-might-not-need-react-memo/#%E6%97%A2%E7%84%B6%E9%9D%A0%E9%87%8D%E7%B5%84%E5%B0%B1%E5%8F%AF%E4%BB%A5%E7%B0%A1%E5%96%AE%E8%A7%A3%E6%B1%BA%E6%95%88%E8%83%BD%E5%95%8F%E9%A1%8C%EF%BC%8C%E9%82%A3%E6%9C%89%E6%B2%92%E6%9C%89%E4%BB%80%E9%BA%BC%E9%9D%9E%E7%94%A8-memo-%E4%B8%8D%E5%8F%AF%E7%9A%84%E6%83%85%E6%B3%81-%EF%BC%9F">#</a> 既然靠重組就可以簡單解決效能問題,那有沒有什麼非用 memo 不可的情況 ?</h2>
<p>確實單靠重組就可以解決大部分的 re-render 問題,但每多拆一個元件,就需要多一個元件命名,將元件拆的太細,額外帶來的是元件命名和程式閱讀上的負擔。</p>
<p>如果非使用 <code>memo</code> 不可,我會建議使用這種顯式的作法來增加可讀性</p>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">const</span> <span class="token function-variable function">App</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>text<span class="token punctuation">,</span> setText<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token string">''</span><span class="token punctuation">)</span><br /> <span class="token keyword">const</span> PureExpensiveCalculation <span class="token operator">=</span> <span class="token function">memo</span><span class="token punctuation">(</span>ExpensiveCalculation<span class="token punctuation">)</span><br /> <span class="token keyword">return</span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>input</span> <br /> <span class="token attr-name">value</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>text<span class="token punctuation">}</span></span><br /> <span class="token attr-name">onClick</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token parameter">e</span> <span class="token operator">=></span> <span class="token function">setText</span><span class="token punctuation">(</span>e<span class="token punctuation">.</span>target<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">}</span></span><br /> <span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">PureExpensiveCalculation</span></span> <span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span></span><span class="token punctuation">></span></span><br /><span class="token punctuation">}</span></code></pre>
<h3 id="%E5%A6%82%E4%BD%95%E5%88%A4%E6%96%B7%E8%A6%81%E4%B8%8D%E8%A6%81%E4%BD%BF%E7%94%A8-memo%EF%BC%9F%E6%88%91%E6%89%80%E8%83%BD%E8%80%83%E6%85%AE%E5%88%B0%E7%9A%84-3-%E5%80%8B%E9%9D%A2%E5%90%91"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/you-might-not-need-react-memo/#%E5%A6%82%E4%BD%95%E5%88%A4%E6%96%B7%E8%A6%81%E4%B8%8D%E8%A6%81%E4%BD%BF%E7%94%A8-memo%EF%BC%9F%E6%88%91%E6%89%80%E8%83%BD%E8%80%83%E6%85%AE%E5%88%B0%E7%9A%84-3-%E5%80%8B%E9%9D%A2%E5%90%91">#</a> 如何判斷要不要使用 memo?我所能考慮到的 3 個面向</h3>
<p>如果今天這份 Code 只是一個短期的個人 Side Project,其實用或不用都不會造成太大的影響。</p>
<p>那我們實際在使用 <code>memo</code> 前究竟要考慮什麼呢?</p>
<p><code>memo</code> 只能解決程式效能的問題,但會延伸出降低開發效率和增加溝通成本的問題,下面是我所能想到的 3 個面向</p>
<ul>
<li>
<p>開發效率:</p>
<ul>
<li>加上 memo 是否會增加開發的時程?</li>
<li>重組的過程中,是否會增加元件命名上的困難?</li>
</ul>
</li>
<li>
<p>溝通(程式碼可讀性):</p>
<p>如何讓其他人或未來的自己,很快的就能得知我所使用的元件是一個 Pure Component?</p>
</li>
<li>
<p>程式效能:</p>
<ul>
<li>這部分的程式碼是否影響足夠多的使用者?</li>
<li>使用重組元件的方式優化渲染之後,是否仍然有使用相同的 props 來重複渲染的 Component</li>
</ul>
</li>
</ul>
<h3 id="%E5%A6%82%E4%BD%95%E5%88%A4%E6%96%B7%E4%B8%80%E5%80%8B%E4%BD%BF%E7%94%A8%E7%9B%B8%E5%90%8C-props-%E7%9A%84-component-%E7%B6%93%E5%B8%B8%E6%B8%B2%E6%9F%93-%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/you-might-not-need-react-memo/#%E5%A6%82%E4%BD%95%E5%88%A4%E6%96%B7%E4%B8%80%E5%80%8B%E4%BD%BF%E7%94%A8%E7%9B%B8%E5%90%8C-props-%E7%9A%84-component-%E7%B6%93%E5%B8%B8%E6%B8%B2%E6%9F%93-%EF%BC%9F">#</a> 如何判斷一個使用相同 <code>props</code> 的 Component 經常渲染 ?</h3>
<p>可以使用 <code>Profiler API</code> [6] 來測量,再來決定要不要優化渲染。</p>
<h2 id="react-%E7%B9%BC-memo-%E4%B9%8B%E5%BE%8C%EF%BC%8C%E5%87%BA%E7%8F%BE%E4%BA%86%E4%B8%80%E5%80%8B-usememo-hook%EF%BC%8C%E5%AE%83%E6%98%AF%E5%81%9A%E4%BB%80%E9%BA%BC%E7%94%A8%E7%9A%84%EF%BC%8C%E6%9C%89%E4%BA%86-usememo-%E4%B9%8B%E5%BE%8C%EF%BC%8C%E6%88%91%E5%80%91%E9%82%84%E6%9C%89%E4%BD%BF%E7%94%A8-memo-%E7%9A%84%E5%BF%85%E8%A6%81%E5%97%8E%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/you-might-not-need-react-memo/#react-%E7%B9%BC-memo-%E4%B9%8B%E5%BE%8C%EF%BC%8C%E5%87%BA%E7%8F%BE%E4%BA%86%E4%B8%80%E5%80%8B-usememo-hook%EF%BC%8C%E5%AE%83%E6%98%AF%E5%81%9A%E4%BB%80%E9%BA%BC%E7%94%A8%E7%9A%84%EF%BC%8C%E6%9C%89%E4%BA%86-usememo-%E4%B9%8B%E5%BE%8C%EF%BC%8C%E6%88%91%E5%80%91%E9%82%84%E6%9C%89%E4%BD%BF%E7%94%A8-memo-%E7%9A%84%E5%BF%85%E8%A6%81%E5%97%8E%EF%BC%9F">#</a> React 繼 <code>memo</code> 之後,出現了一個 <code>useMemo</code> hook,它是做什麼用的,有了 <code>useMemo</code> 之後,我們還有使用 <code>memo</code> 的必要嗎?</h2>
<p><code>useMemo</code> [7] 是一個很猛的 Hook!不僅可以做自己 memorize value,還可以當 <code>useCallback</code> 和 <code>memo</code> 使用,學 1 個會 3 個,3 個願望一次滿足。</p>
<p>memorize value:</p>
<pre class="language-tsx"><code class="language-tsx"><span class="token keyword">const</span> <span class="token function-variable function">computeExpensiveValue</span> <span class="token operator">=</span> <span class="token punctuation">(</span>parameter<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><span class="token punctuation">}</span><br /><span class="token keyword">const</span> memorizedValue <span class="token operator">=</span> <span class="token function">useMemo</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">computeExpensiveValue</span><span class="token punctuation">(</span>argument<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>argument<span class="token punctuation">]</span><span class="token punctuation">)</span></code></pre>
<p>memorize function:</p>
<pre class="language-tsx"><code class="language-tsx"><span class="token comment">// useCallback(fn, []) = usemMemo(() => fn, [])</span><br /><span class="token keyword">const</span> memorizedFn <span class="token operator">=</span> <span class="token function">useMemo</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> state<span class="token punctuation">,</span> <span class="token punctuation">[</span>state<span class="token punctuation">]</span><span class="token punctuation">)</span><br /><span class="token keyword">const</span> memorizedFn <span class="token operator">=</span> <span class="token function">useCallback</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> state<span class="token punctuation">,</span> <span class="token punctuation">[</span>state<span class="token punctuation">]</span><span class="token punctuation">)</span><br /><br /><span class="token function">memorizedFn</span><span class="token punctuation">(</span><span class="token punctuation">)</span></code></pre>
<p>memorize component:</p>
<pre class="language-tsx"><code class="language-tsx"><span class="token keyword">const</span> <span class="token function-variable function">Component</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">{</span> prop <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><span class="token punctuation">}</span><br /><span class="token operator">--</span><span class="token operator">--</span><br /><span class="token keyword">const</span> <span class="token function-variable function">App</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">{</span> state <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> element <span class="token operator">=</span> <span class="token function">useMemo</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Component</span></span> <span class="token attr-name">prop</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>state<span class="token punctuation">}</span></span> <span class="token punctuation">/></span></span><span class="token punctuation">,</span> <span class="token punctuation">[</span>state<span class="token punctuation">]</span><span class="token punctuation">)</span><br /> <span class="token keyword">return</span> element<br /><span class="token punctuation">}</span><br /><span class="token operator">--</span><span class="token operator">--</span><br /><span class="token keyword">const</span> PureFunctionalComponent <span class="token operator">=</span> <span class="token function">memo</span><span class="token punctuation">(</span>Component<span class="token punctuation">)</span><br /><span class="token keyword">const</span> <span class="token function-variable function">App</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">{</span> state <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">PureFunctionalComponent</span></span> <span class="token attr-name">prop</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>state<span class="token punctuation">}</span></span> <span class="token punctuation">/></span></span></code></pre>
<p>從技術上來說 <code>useMemo</code> 能應用於所有 React 渲染優化的場景,但由於 hook 天生的侷限,沒辦法在渲染的時候使用,一般除非特殊理由,我們不會默認一個 Functional Component 是 Pure Component,就只是一般元件。所以如果要凸顯這部分的 Code 是做效能優化的 Code,我更傾向用 memo 處理。</p>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">const</span> <span class="token function-variable function">App</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> list <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> list<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">itemName<span class="token punctuation">,</span> i</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">FunctionalComponent</span></span> <span class="token attr-name">key</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>i<span class="token punctuation">}</span></span> <span class="token attr-name">itemName</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>itemName<span class="token punctuation">}</span></span> <span class="token punctuation">/></span></span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">const</span> <span class="token function-variable function">FunctionalComponent</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> itemName <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> element <span class="token operator">=</span> <span class="token function">useMemo</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span><span class="token punctuation">></span></span><span class="token punctuation">{</span>itemName<span class="token punctuation">}</span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span><span class="token punctuation">,</span> <span class="token punctuation">[</span>itemName<span class="token punctuation">]</span><span class="token punctuation">)</span><br /> <span class="token keyword">return</span> element<br /><span class="token punctuation">}</span></code></pre>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">const</span> <span class="token function-variable function">App</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> list <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> list<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">itemName<span class="token punctuation">,</span> i</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">PureFunctionalComponent</span></span> <span class="token attr-name">key</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>i<span class="token punctuation">}</span></span> <span class="token attr-name">itemName</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>itemName<span class="token punctuation">}</span></span> <span class="token punctuation">/></span></span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">const</span> PureFunctionalComponent <span class="token operator">=</span> <span class="token function">memo</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> itemName <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span><span class="token punctuation">></span></span><span class="token punctuation">{</span>itemName<span class="token punctuation">}</span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">const</span> <span class="token function-variable function">App</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> list <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> list<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">itemName<span class="token punctuation">,</span> i</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> PureFunctionalComponent <span class="token operator">=</span> <span class="token function">memo</span><span class="token punctuation">(</span>FunctionalComponent<span class="token punctuation">)</span> <span class="token comment">// 顯式使用</span><br /><br /> <span class="token keyword">return</span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">PureFunctionalComponent</span></span> <span class="token attr-name">key</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>i<span class="token punctuation">}</span></span> <span class="token attr-name">itemName</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>itemName<span class="token punctuation">}</span></span> <span class="token punctuation">/></span></span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<h2 id="%E7%B5%90%E8%AB%96"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/you-might-not-need-react-memo/#%E7%B5%90%E8%AB%96">#</a> 結論</h2>
<p>對 React 的 Functional Component 做渲染效能優化,不一定要使用 <code>memo</code> 來達成,也可以透過元件的重組或 <code>useMemo</code> 來達成,值得注意的是,每一種優化都會帶來相應的成本(犧牲可讀性或開發效率),先釐清需求之後再來使用,才能取得綜效。</p>
<h2 id="%E6%84%9F%E8%AC%9D"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/you-might-not-need-react-memo/#%E6%84%9F%E8%AC%9D">#</a> 感謝</h2>
<p>天下文章一大抄,感謝巨人們的肩膀。</p>
<p>[1] <a href="https://medium.com/starbugs/%E4%BB%8A%E6%99%9A-%E6%88%91%E6%83%B3%E4%BE%86%E9%BB%9E-web-%E5%89%8D%E7%AB%AF%E6%95%88%E8%83%BD%E5%84%AA%E5%8C%96%E5%A4%A7%E8%A3%9C%E5%B8%96-e1a5805c1ca2">今晚,我想來點 Web 前端效能優化大補帖!</a><br />
[2] <a href="https://reactjs.org/docs/react-api.html#reactmemo">React.memo</a><br />
[3] <a href="https://github.com/facebook/react/blob/main/packages/shared/shallowEqual.js"><code>shallowEqual.js</code></a><br />
[4] <a href="https://kentcdodds.com/blog/usememo-and-usecallback">When to useMemo and useCallback</a><br />
[5] <a href="https://overreacted.io/before-you-memo/">Before You memo()</a><br />
[6] <a href="https://reactjs.org/docs/profiler.html#gatsby-focus-wrapper">Profiler API</a><br />
[7] <a href="https://reactjs.org/docs/hooks-reference.html#usememo">React.useMemo</a></p>
<p>你有遇過什麼非使用 memo 不可的情境嗎?歡迎留言與我討論 ~</p>
next-seo 初體驗
2021-08-30T00:00:00Z
https://blog.errorbaker.tw/posts/clay/next-seo/
<!-- summary -->
<!-- 想要為 next.js 開發的網站做 SEO 嗎?或許你可以先考慮 next-seo -->
<!-- summary -->
<h3 id="%E7%B7%A3%E8%B5%B7"><a class="direct-link" href="https://blog.errorbaker.tw/posts/clay/next-seo/#%E7%B7%A3%E8%B5%B7">#</a> 緣起</h3>
<p>前陣子曾經在處理公司網站上的 SEO,原本只是要修復一個小小的 bug,但因為過去沒有接觸過太多有關網站 SEO 的經驗,就決定從頭開始查詢研究</p>
<p>由於網站是用 next.js 框架建構的,最初的處理方式,是將我們使用到一些會影響 SEO 的 <code><meta></code> 或是 <code><link></code> 加進去 next.js 提供的 <code><Head></code> Component,大概就是像以下這種形式:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token function-variable function">Page</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span><span class="token operator">></span><br /> <span class="token operator"><</span>Head<span class="token operator">></span><br /> <span class="token operator"><</span>title<span class="token operator">></span>Error Baker<span class="token operator"><</span><span class="token operator">/</span>title<span class="token operator">></span><br /> <span class="token operator"><</span>meta property<span class="token operator">=</span><span class="token string">"og:title"</span> content<span class="token operator">=</span><span class="token string">"Error Baker"</span> <span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>meta property<span class="token operator">=</span><span class="token string">"og:url"</span> content<span class="token operator">=</span><span class="token string">"https://blog.errorbaker.tw/"</span> <span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>meta property<span class="token operator">=</span><span class="token string">"og:image"</span> content<span class="token operator">=</span><span class="token string">"https://blog.errorbaker.tw/official-smo.jpg"</span> <span class="token operator">/</span><span class="token operator">></span> <br /> <span class="token operator"><</span>meta property<span class="token operator">=</span><span class="token string">"og:description"</span> content<span class="token operator">=</span><span class="token string">"Welcome to Error Baker"</span> <span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>link rel<span class="token operator">=</span><span class="token string">"canonical"</span> href<span class="token operator">=</span><span class="token string">"https://blog.errorbaker.tw/"</span> <span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>link href<span class="token operator">=</span><span class="token string">"https://blog.errorbaker.tw/media/favicon.png"</span> rel<span class="token operator">=</span><span class="token string">"icon"</span> <span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>Head<span class="token operator">></span><br /> <span class="token operator"><</span>Component<span class="token operator">></span><br /> <span class="token operator">...</span><br /> <span class="token operator"><</span><span class="token operator">/</span>Component<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span><span class="token operator">></span><br /><span class="token punctuation">)</span><br /></code></pre>
<p>上面是單單一頁的寫法,看似沒什麼問題,然而我們之所以會選用 next.js 框架,就代表我們絕對會有各式各樣的 page</p>
<p>如果這些 SEO 相關 tag 都由同一位工程師負責,那倒也沒什麼太大問題,但是當各個頁面都要做自己的 SEO 相關 meta,或者當一個專案中的各種 page 都各自由不同工程師開發時,就可能會有開發模式紊亂的狀況。</p>
<p>或許你會想問,單單就幾個 <code>meta</code> 與 <code>link</code> tag,即使放置錯誤也不會影響到功能,似乎日後再行標準化也沒什麼關係,是嗎?</p>
<p>從功能面上來看,的確是不會影響到網站本身的功能,我原本也是這樣以為的,直到我有一日瞥見了 <em>next-seo</em> 這個套件,才發現事情似乎沒有我想的這麼簡單。</p>
<h3 id="next-seo-%E7%9A%84-%3Cnextseo%3E-component"><a class="direct-link" href="https://blog.errorbaker.tw/posts/clay/next-seo/#next-seo-%E7%9A%84-%3Cnextseo%3E-component">#</a> next-seo 的 <code><NextSeo></code> Component</h3>
<p><em>next-seo</em>是一套專門處理 next.js 網站的 SEO 套件工具,若要簡單描述它的使用方式,就是使用套件為我們準備好的 Component,將一些我們需要的 props 帶入其中,它就會幫我們 render 出我們需要的 HTML tag。</p>
<p>如果將上面包在 <code><Head></code> 內的 tag 改為使用 next-seo 處理,就會如下方呈現:</p>
<p><img src="https://blog.errorbaker.tw/img/posts/clay/next-seo/12.jpg" alt="" /></p>
<p><code><NextSeo></code> 會幫我們 render 出與 props 相對應的 HTML tag,並且我們不用再將 <code><NextSeo></code> 包在 <code><Head></code> 裡面,next-seo 已經幫我們處理好了這件事。</p>
<p>這麼做有一個好處,就是如果在各頁面我們單純只用 <code><Head></code> 來處理上述等 SEO 相關 tag 的話,我們就可以不用它了。<code><Head></code> 在 next.js 中的使用會需要特別留意,這裡未來有機會再寫一篇文章來講述 <code><Head></code> 的正確使用方式。</p>
<p><code><NextSeo></code> 還有另外一個好用的地方,就是當我們會需要自定義一些 <code><NextSeo></code> 沒有提供的方法時,可以使用 <code>additionalLinkTags</code> 與 <code>additionalMetaTags</code> 來加上 <code><link></code> 與 <code><meta></code>:</p>
<p><img src="https://blog.errorbaker.tw/img/posts/clay/next-seo/13.jpg" alt="" /></p>
<p>可以用簡單的對應方法來比對,在 <code><NextSeo /></code> 中,<code>openGraph</code> 這個 props 內的 <code>url</code>、<code>title</code>、<code>description</code> 與 <code>images</code>,分別對應了以下四個 HTML tag:</p>
<pre class="language-js"><code class="language-js"> <span class="token operator"><</span>meta property<span class="token operator">=</span><span class="token string">"og:title"</span> content<span class="token operator">=</span><span class="token string">"Error Baker"</span> <span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>meta property<span class="token operator">=</span><span class="token string">"og:url"</span> content<span class="token operator">=</span><span class="token string">"https://blog.errorbaker.tw/"</span> <span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>meta property<span class="token operator">=</span><span class="token string">"og:image"</span> content<span class="token operator">=</span><span class="token string">"https://blog.errorbaker.tw/official-smo.jpg"</span> <span class="token operator">/</span><span class="token operator">></span> <br /> <span class="token operator"><</span>meta property<span class="token operator">=</span><span class="token string">"og:description"</span> content<span class="token operator">=</span><span class="token string">"Welcome to Error Baker"</span> <span class="token operator">/</span><span class="token operator">></span></code></pre>
<p>如果接著來看官方的範例,可以看到更多 props,如果你是像我一樣剛接觸 SEO 的新手,應該會對 <code><NextSeo></code> 內還有 <code>twitter</code> 這個 props 滿驚喜的,我也是看到文件的描述才知道原來我們也可以做一些友善其他社群平台的 SEO 標籤:</p>
<p><img src="https://blog.errorbaker.tw/img/posts/clay/next-seo/1.jpg" alt="" /></p>
<p><code><NextSeo></code> 這個 Component 幫我們處理掉了一些基本 SEO 所需要的 tag,也包括了網頁標題與 icon 等等</p>
<p>另外,我覺得更棒的一點是由於 next-seo 支持 TypeScript 型別檢查,所以當你沒有按照規範輸入 props 或是少寫了某些必要的 props,就會跳出 TS Error:</p>
<p><img src="https://blog.errorbaker.tw/img/posts/clay/next-seo/2.jpg" alt="" /></p>
<p>如此一來可以更有效的標準化不同開發者的開發內容,若是搭配 ESLint 或是 husky 等檢查工具,更能使開發過程不易出錯。</p>
<h3 id="%E5%88%9D%E6%8E%A2-json-ld"><a class="direct-link" href="https://blog.errorbaker.tw/posts/clay/next-seo/#%E5%88%9D%E6%8E%A2-json-ld">#</a> 初探 JSON-LD</h3>
<p>在繼續介紹 next-seo 之前,我們先來簡單描述什麼是 JSON-LD</p>
<p>JSON-LD 全名為 JSON for Link Data,它是結構化資料的一種,的目的是讓我們網站上一些關鍵字,有了這些關鍵字,就能更好的被搜尋引擎爬到資料,增加網站的曝光率,並能讓搜尋引擎更好地判斷我們網站中的內容,</p>
<p>當然結構化資料有很多,有微資料 (micro data) 與 RDFa 等等,加上要好好講解 JSON-LD 又可以另外再寫一篇了 XD,這裡就留個小伏筆,先來看範例</p>
<p>我這裡隨便找了一個網站中的<a href="https://drop.com/buy/drop-sennheiser-hd-8xx-headphones">商品</a>,並在網站中開啟 devtool,搜尋 <code>ld+json</code>,來看一下它的 JSON-LD Script:</p>
<p><img src="https://blog.errorbaker.tw/img/posts/clay/next-seo/03.jpg" alt="" /></p>
<p>將其展開看看:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>application/ld+json<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br /><span class="token punctuation">{</span><br /> <span class="token string-property property">"@context"</span><span class="token operator">:</span><span class="token string">"http://schema.org/"</span><span class="token punctuation">,</span><br /> <span class="token string-property property">"@type"</span><span class="token operator">:</span><span class="token string">"Product"</span><span class="token punctuation">,</span><br /> <span class="token string-property property">"name"</span><span class="token operator">:</span><span class="token string">"Drop + Sennheiser HD 8XX Headphones"</span><span class="token punctuation">,</span><br /> <span class="token string-property property">"image"</span><span class="token operator">:</span><span class="token string">"https://massdrop-s3.imgix.net/product-images/drop-sennheiser-hd-8xx-headphones/FP/CFpa6gMaTgoQryuKz9CQ_PC.png?bg=f0f0f0"</span><span class="token punctuation">,</span><br /> <span class="token string-property property">"description"</span><span class="token operator">:</span><span class="token string">"The HD 8XX is based on the HD 800S; a flagship audiophile headphone produced by our partners at Sennheiser. Like the HD 800S, the HD 8XX is made in Germany at Sennheiser’s HQ factory. Based on community requests, we worked with Sennheiser to tune the housing resonance to add low end extension..."</span><span class="token punctuation">,</span><br /> <span class="token string-property property">"sku"</span><span class="token operator">:</span><span class="token string">"drop-sennheiser-hd-8xx-headphones"</span><span class="token punctuation">,</span><br /> <span class="token string-property property">"gtin12"</span><span class="token operator">:</span><span class="token string">"810027786159"</span><span class="token punctuation">,</span><br /> <span class="token string-property property">"brand"</span><span class="token operator">:</span><span class="token punctuation">{</span><br /> <span class="token string-property property">"@type"</span><span class="token operator">:</span><span class="token string">"Brand"</span><span class="token punctuation">,</span><br /> <span class="token string-property property">"name"</span><span class="token operator">:</span><span class="token string">"Drop + Sennheiser"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token string-property property">"offers"</span><span class="token operator">:</span><span class="token punctuation">[</span><span class="token punctuation">{</span><br /> <span class="token string-property property">"sku"</span><span class="token operator">:</span><span class="token string">"MDX-35505-1"</span><span class="token punctuation">,</span><br /> <span class="token string-property property">"name"</span><span class="token operator">:</span><span class="token string">"Drop + Sennheiser HD 8XX Headphones"</span><span class="token punctuation">,</span><br /> <span class="token string-property property">"price"</span><span class="token operator">:</span><span class="token number">1100</span><span class="token punctuation">,</span><span class="token string-property property">"availability"</span><span class="token operator">:</span><span class="token string">"http://schema.org/InStock"</span><span class="token punctuation">,</span><br /> <span class="token string-property property">"gtin12"</span><span class="token operator">:</span><span class="token string">"810027786159"</span><span class="token punctuation">,</span><br /> <span class="token string-property property">"@type"</span><span class="token operator">:</span><span class="token string">"Offer"</span><span class="token punctuation">,</span><br /> <span class="token string-property property">"url"</span><span class="token operator">:</span><span class="token string">"https://drop.com/buy/drop-sennheiser-hd-8xx-headphones"</span><span class="token punctuation">,</span><br /> <span class="token string-property property">"priceCurrency"</span><span class="token operator">:</span><span class="token string">"USD"</span><span class="token punctuation">,</span><br /> <span class="token string-property property">"itemCondition"</span><span class="token operator">:</span><span class="token string">"http://schema.org/NewCondition"</span><span class="token punctuation">,</span><br /> <span class="token string-property property">"priceValidUntil"</span><span class="token operator">:</span><span class="token string">"2021-11-25T07:59:00Z"</span><span class="token punctuation">,</span><br /> <span class="token string-property property">"validThrough"</span><span class="token operator">:</span><span class="token string">"2021-11-25T07:59:00Z"</span><span class="token punctuation">,</span><br /> <span class="token string-property property">"validFrom"</span><span class="token operator">:</span><span class="token string">"2021-02-18T17:00:00Z"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">]</span><br /><span class="token punctuation">}</span><br /></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span></code></pre>
<p>哇,看起來密密麻麻的,好像很複雜的樣子,不過如果仔細看內容,會發現好像也沒有到很難理解,比如說 <code>name</code> 就是對應到商品的名字,<code>description</code> 就是產品描述,<code>gtin12</code> 就是商品序號,以此類推</p>
<p>這些 JSON 資料被放在 <code><script></code> 裡面,讓搜尋引擎可以更好的辨識我們的產品,要實作的話也不難,只要在網站中寫一個 function 來生成 JSON 檔案就好:</p>
<pre class="language-js"><code class="language-js"><br /><span class="token keyword">const</span> <span class="token function-variable function">convertProductToJsonLd</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">product</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> ldJSON <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> product<span class="token punctuation">.</span>name<span class="token punctuation">,</span><br /> <span class="token string-property property">'@context'</span><span class="token operator">:</span> <span class="token string">'https://schema.org/'</span><span class="token punctuation">,</span><br /> <span class="token string-property property">'@type'</span><span class="token operator">:</span> <span class="token string">'Product'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">image</span><span class="token operator">:</span> product<span class="token punctuation">.</span>image<span class="token punctuation">,</span><br /> <span class="token literal-property property">description</span><span class="token operator">:</span> product<span class="token punctuation">.</span>description<span class="token punctuation">,</span><br /> <span class="token literal-property property">brand</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token string-property property">'@type'</span><span class="token operator">:</span> <span class="token string">'Brand'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'Drop + Sennheiser'</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">gtin12</span><span class="token operator">:</span> product<span class="token punctuation">.</span>id<span class="token punctuation">,</span><br /> <span class="token literal-property property">offers</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token string-property property">'@type'</span><span class="token operator">:</span> <span class="token string">'Offer'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">url</span><span class="token operator">:</span> product<span class="token punctuation">.</span>url<span class="token punctuation">,</span><br /> <span class="token literal-property property">priceCurrency</span><span class="token operator">:</span> <span class="token string">'USD'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">price</span><span class="token operator">:</span> product<span class="token punctuation">.</span>price<span class="token punctuation">,</span><br /> <span class="token literal-property property">priceValidUntil</span><span class="token operator">:</span> <span class="token string">'2021-12-31'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">itemCondition</span><span class="token operator">:</span> <span class="token string">'https://schema.org/NewCondition'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">availability</span><span class="token operator">:</span> <span class="token string">'https://schema.org/InStock'</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">try</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>ldJSON<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span> <span class="token keyword">catch</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>生成之後,將其內容放入 <code><script></code> tag 中,這樣就算是完成了,概念上並不難理解,</p>
<p>但除此之外,這些資料的內容其實是有規範的,我們可以在 <a href="https://schema.org/">https://schema.org/</a> 中閱讀相關文件,至於 <a href="http://schema.org/">Schema.org</a> 的核心概念,就如同網站首頁中所敘述的一樣,重點是在以下這句:</p>
<blockquote>
<p>Founded by Google, Microsoft, Yahoo and Yandex, <a href="http://schema.org/">Schema.org</a> vocabularies are developed by an open community process, using the <a href="mailto:public-schemaorg@w3.org">public-schemaorg@w3.org</a> mailing list and through GitHub.</p>
</blockquote>
<p><a href="http://schema.org/">Schema.org</a> 中的文件規範是由 Google、Microsoft 與 Yahoo 等知名公司共同制定的一套規範,如果再深入探討,可以看到內中有不少給開發者的開發建議,撰寫更有意義的語義化標籤等等,這份文件目前也有<a href="https://schema.org/">中文版</a> (雖然是簡體),有興趣的讀者可以好好研究裡面的內容,</p>
<p>而 JSON-LD 的一些建議欄位在 <a href="http://schema.org/">schema.org</a> 中其實也有跡可循,但本篇就先不帶大家看文件了,我們換個方式,先來看看 next-seo 是如何處理 JSON-LD。</p>
<h2 id="%E4%BD%BF%E7%94%A8-next-seo-%E6%8F%90%E4%BE%9B%E7%9A%84-json-ld-component%EF%BC%9A"><a class="direct-link" href="https://blog.errorbaker.tw/posts/clay/next-seo/#%E4%BD%BF%E7%94%A8-next-seo-%E6%8F%90%E4%BE%9B%E7%9A%84-json-ld-component%EF%BC%9A">#</a> 使用 next-seo 提供的 JSON-LD Component:</h2>
<p>JSON-LD 是 next-seo 另外一個主打的重點項目,如果查閱文件,你會看到其中有不少範例:</p>
<p><img src="https://blog.errorbaker.tw/img/posts/clay/next-seo/4.png" alt="" /></p>
<p>當初第一次研讀文件時,我本來以為這些 Component 都只是官方舉出的範例,但實際比對過之後發現並非如此。</p>
<p>如同之前提到的,JSON-LD 的內容是已經經過標準化的內容,以目前最大的搜尋引擎 Google 來說,就定義了如下圖紅框處各種情境的 JSON-LD 格式:</p>
<p><img src="https://blog.errorbaker.tw/img/posts/clay/next-seo/5.png" alt="" /></p>
<p>我們挑選 <a href="https://developers.google.com/search/docs/advanced/structured-data/product">Product</a> 這個結構化資料,並查閱其內容,可以看到 Google 這裡已經建議我們可以如何生成與商品相關的 JSON-LD:</p>
<p><img src="https://blog.errorbaker.tw/img/posts/clay/next-seo/6.jpg" alt="" /></p>
<p>再回頭來比對 next-seo 提供的 <a href="https://github.com/garmeeh/next-seo#product"><code><ProductJsonLd ></code></a> Component,你會發現一個驚人的巧合:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> <span class="token punctuation">{</span> ProductJsonLd <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'next-seo'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> <span class="token function-variable function">Page</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span><span class="token operator">></span><br /> <span class="token operator"><</span>h1<span class="token operator">></span>Product <span class="token constant">JSON</span><span class="token operator">-</span><span class="token constant">LD</span><span class="token operator"><</span><span class="token operator">/</span>h1<span class="token operator">></span><br /> <span class="token operator"><</span>ProductJsonLd<br /> productName<span class="token operator">=</span><span class="token string">"Executive Anvil"</span><br /> images<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">[</span><br /> <span class="token string">'https://example.com/photos/1x1/photo.jpg'</span><span class="token punctuation">,</span><br /> <span class="token string">'https://example.com/photos/4x3/photo.jpg'</span><span class="token punctuation">,</span><br /> <span class="token string">'https://example.com/photos/16x9/photo.jpg'</span><span class="token punctuation">,</span><br /> <span class="token punctuation">]</span><span class="token punctuation">}</span><br /> description<span class="token operator">=</span><span class="token string">"Sleeker than ACME's Classic Anvil, the Executive Anvil is perfect for the business traveler looking for something to drop from a height."</span><br /> brand<span class="token operator">=</span><span class="token string">"ACME"</span><br /> color<span class="token operator">=</span><span class="token string">"blue"</span><br /> manufacturerName<span class="token operator">=</span><span class="token string">"Gary Meehan"</span><br /> manufacturerLogo<span class="token operator">=</span><span class="token string">"https://www.example.com/photos/logo.jpg"</span><br /> material<span class="token operator">=</span><span class="token string">"steel"</span><br /> slogan<span class="token operator">=</span><span class="token string">"For the business traveller looking for something to drop from a height."</span><br /> disambiguatingDescription<span class="token operator">=</span><span class="token string">"Executive Anvil, perfect for the business traveller."</span><br /> releaseDate<span class="token operator">=</span><span class="token string">"2014-02-05T08:00:00+08:00"</span><br /> productionDate<span class="token operator">=</span><span class="token string">"2015-02-05T08:00:00+08:00"</span><br /> purchaseDate<span class="token operator">=</span><span class="token string">"2015-02-06T08:00:00+08:00"</span><br /> award<span class="token operator">=</span><span class="token string">"Best Executive Anvil Award."</span><br /> reviews<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">author</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Person'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'Jim'</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">datePublished</span><span class="token operator">:</span> <span class="token string">'2017-01-06T03:37:40Z'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">reviewBody</span><span class="token operator">:</span><br /> <span class="token string">'This is my favorite product yet! Thanks Nate for the example products and reviews.'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'So awesome!!!'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">reviewRating</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">bestRating</span><span class="token operator">:</span> <span class="token string">'5'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">ratingValue</span><span class="token operator">:</span> <span class="token string">'5'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">worstRating</span><span class="token operator">:</span> <span class="token string">'1'</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">publisher</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Organization'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'TwoVit'</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">]</span><span class="token punctuation">}</span><br /> aggregateRating<span class="token operator">=</span><span class="token punctuation">{</span><br /> <span class="token literal-property property">ratingValue</span><span class="token operator">:</span> <span class="token string">'4.4'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">reviewCount</span><span class="token operator">:</span> <span class="token string">'89'</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><br /> offers<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">price</span><span class="token operator">:</span> <span class="token string">'119.99'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">priceCurrency</span><span class="token operator">:</span> <span class="token string">'USD'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">priceValidUntil</span><span class="token operator">:</span> <span class="token string">'2020-11-05'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">itemCondition</span><span class="token operator">:</span> <span class="token string">'https://schema.org/UsedCondition'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">availability</span><span class="token operator">:</span> <span class="token string">'https://schema.org/InStock'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">url</span><span class="token operator">:</span> <span class="token string">'https://www.example.com/executive-anvil'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">seller</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'Executive Objects'</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">price</span><span class="token operator">:</span> <span class="token string">'139.99'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">priceCurrency</span><span class="token operator">:</span> <span class="token string">'CAD'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">priceValidUntil</span><span class="token operator">:</span> <span class="token string">'2020-09-05'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">itemCondition</span><span class="token operator">:</span> <span class="token string">'https://schema.org/UsedCondition'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">availability</span><span class="token operator">:</span> <span class="token string">'https://schema.org/InStock'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">url</span><span class="token operator">:</span> <span class="token string">'https://www.example.ca/executive-anvil'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">seller</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'Executive Objects'</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">]</span><span class="token punctuation">}</span><br /> mpn<span class="token operator">=</span><span class="token string">"925872"</span><br /> <span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span><span class="token operator">></span><br /><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> Page<span class="token punctuation">;</span></code></pre>
<p>將上述程式碼與 Google 官方文件的提供的格式一比對,你會發現兩者近乎相同,這就代表在大多數情況下,next-seo 已經幫我們都規範好了相關內容,為什麼這樣說呢?</p>
<p>還記得我們稍早有提到的 TS Error 嗎?在 <code><NextSeo></code> 中看似還不太有用,但是在使用 LD-JSON Component 時幫助可就大了,當我們輸入了一些不符合的 LD-JSON 欄位或是少加一些必要的資料,TypeScript 就會報錯:</p>
<p><img src="https://blog.errorbaker.tw/img/posts/clay/next-seo/7.jpg" alt="" /></p>
<p>看看是哪裡錯誤:</p>
<p><img src="https://blog.errorbaker.tw/img/posts/clay/next-seo/8.jpg" alt="" /></p>
<p>另外,我們可以從這一點推斷,next-seo 應該也會隨著 Google or Schema 的規範變動而跟著更新,</p>
<p>舉個例子,之前在使用 <code>next-seo 4.25.0</code> 版本的時候,我在 <code><ProductJsonLd /></code> 中的 <code>aggregateOffer</code> props 中增加 <code>offers</code>,卻一直噴錯:</p>
<p><img src="https://blog.errorbaker.tw/img/posts/clay/next-seo/9.jpg" alt="" /></p>
<p>但這裡百思不得其解,因為去追 next-seo 上的原始碼,是有定義 <code>offers</code> 的,直到再看仔細一點,才發現這個 <code>offers</code> 其實是在兩個月前剛加上去的:</p>
<p><img src="https://blog.errorbaker.tw/img/posts/clay/next-seo/10.png" alt="" /></p>
<p>查找相關 issue,以 <code>aggregateOffer</code> 為關鍵字可以查到<a href="https://github.com/garmeeh/next-seo/issues/766">這一則</a>,我猜應該就是這篇 issue 促使了這次改動,這裡我在更新到了 <code>4.26.0</code> 版本獲得了解決</p>
<p>附帶一提,該 issue 提到 <code>offers</code> 應該是可以被包含在 <code>aggregateOffer</code> 之中,而這個規範我們可以在 <a href="http://schema.org/">schema.org</a> 中可以找得到:</p>
<p><img src="https://blog.errorbaker.tw/img/posts/clay/next-seo/11.jpg" alt="" /></p>
<p>綜上所述,有了 next-seo 內建的型別判斷,可以讓我們標準化不同開發人員撰寫 SEO 程式碼的風格,也可以藉由 next-seo 提供的各種 Template Component,防止我們寫出錯誤的資料格式,也幫助我們更方便地 render 出 LD-JSON,而且一樣不需要自己撰寫 <code><script></code> 並將其包在 <code><Head></code> 之中,使整體程式碼與架構更加純粹。</p>
<h2 id="%E7%B5%90%E8%AA%9E"><a class="direct-link" href="https://blog.errorbaker.tw/posts/clay/next-seo/#%E7%B5%90%E8%AA%9E">#</a> 結語</h2>
<p>開頭有說到,開始研究 next-seo 之後,才發現事情並沒有自己想像得這麼簡單,主要的原因是看完文件的使用之後,才發現自己原本認知的網站 SEO 知識實在是太微不足道了。</p>
<p>試想,在競爭激烈的市場中,如果我們今天我們開發的是購物網站或是形象網站,那麼關於 SEO 的優化就會非常看重,沒有好好處理這一塊,輕則客戶沒辦法很好搜尋到你的網站,重則可能會被 Google 搜尋引擎列為拒絕往來戶 (比如說一些不當的操作或是給予錯誤的結構化資料)。</p>
<p>而處理 SEO 麻煩的地方在於,他很難光靠本機的開發測試就能去得到一個百分之百確定的結果,我只能說我的 SEO 可以相對變好,但很難去保證絕對會被客戶搜尋到,這也是一個很深的坑。</p>
<p>另外,關於 next-seo,還有一些細節沒有提到,比如一些特殊的 props 與 <code>DefaultSeo</code> 等等,但基本的入門應該已經足夠了,藉由此次練習 next-seo 的使用,讓我有以下的收穫:</p>
<ol>
<li>了解 next-seo 套件可以更好的標準化我們處理網站中 SEO</li>
<li>學習如何使用 next-seo (局部)</li>
<li>了解 Google 已經為結構化資料有建議使用的 schema,可以照其規範實作</li>
</ol>
<p>另外,自己也曾與同事討論過,當 <a href="http://schema.org/">schema.org</a> 更新規範內容時,next-seo 是否可以跟上規範並持續更新?</p>
<p>後來想想,只要掌握 next-seo 的概念,其實我們也可以自己寫一些 library 或是函式來處理,一些套件可以解決的,我們使用套件,套件解決不了的,就自己動手即可</p>
<p>最後,不論各位是選擇自己建構 LD-JSON 或是使用 next-seo,都可以使用以下兩個工具來檢測生成的資料格式是否符合搜尋引擎的需求:</p>
<ol>
<li>Google Rich Result Test: <a href="https://search.google.com/test/rich-results?hl=zh-tw">https://search.google.com/test/rich-results?hl=zh-tw</a></li>
<li><a href="https://validator.schema.org/">https://validator.schema.org/</a></li>
</ol>
<p>感謝您閱讀完上述的文章,如果有描述錯誤或資訊不齊的部分,請再麻煩留言給我,十分感謝你光臨 Error Baker 🙏</p>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/clay/next-seo/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li><a href="https://developers.google.com/search/docs/advanced/structured-data/product">https://developers.google.com/search/docs/advanced/structured-data/product</a></li>
<li><a href="https://schema.org/">https://schema.org/</a></li>
<li><a href="https://github.com/garmeeh/next-seo">https://github.com/garmeeh/next-seo</a></li>
<li><a href="https://ithelp.ithome.com.tw/articles/10186398">JSON-LD, 決定未來 SEO 的 25 項標準與通訊協定系列 II</a></li>
</ul>
svelte 結合 firebase 實作登入
2021-09-04T00:00:00Z
https://blog.errorbaker.tw/posts/ruofan/svelte/
<!-- summary -->
<p>Hi,大家好!最近初次嘗試使用 svelte 開發一個新的專案,這篇文章會分享使用 firebase google login 以及搭配 xstate 的實作。</p>
<!-- summary -->
<!-- more -->
<h2 id="svelte-%E7%9A%84%E7%89%B9%E8%89%B2%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/svelte/#svelte-%E7%9A%84%E7%89%B9%E8%89%B2%EF%BC%9F">#</a> svelte 的特色?</h2>
<p>svelte 和其他前端框架的不同,在 svelte 的官網上是這樣介紹的:</p>
<blockquote>
<p>Traditional frameworks allow you to write declarative state-driven code, but there's a penalty: the browser must do extra work to convert those declarative structures into DOM operations, using techniques like that eat into your frame budget and tax the garbage collector.</p>
</blockquote>
<blockquote>
<p>Instead, Svelte runs at build time, converting your components into highly efficient imperative code that surgically updates the DOM. As a result, you're able to write ambitious applications with excellent performance characteristics.</p>
</blockquote>
<p>簡單來說,svelte 會在 build time 的時後編譯元件,performance 的表現上是不錯的。<br />
想了解更多的話,推薦看 svelte 的 <a href="https://svelte.dev/blog/svelte-3-rethinking-reactivity">官方文件</a>。</p>
<h2 id="quickstart"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/svelte/#quickstart">#</a> QuickStart</h2>
<p>透過下方的指令可以快速的開啟一個新的專案。</p>
<pre class="language-bash"><code class="language-bash"><span class="token variable">$npx</span> degit sveltejs/template svelte-app</code></pre>
<h2 id="setup"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/svelte/#setup">#</a> SetUp</h2>
<p>首先,需要先到 firebase console 新增一個新的專案,再進入 project setting 內設定 App nickname 後就會生成 firebaseConfig。<br />
這邊的 firebaseConfig 需要記下來,接下來專案中會需要用到。<br />
<img src="https://blog.errorbaker.tw/img/posts/ruofan/firebase-project-setting.png" alt="" /></p>
<p>接著到 Authentication 設定 provider。<br />
<img src="https://blog.errorbaker.tw/img/posts/ruofan/firebase-auth.png" alt="" /></p>
<h2 id="%E9%96%8B%E5%A7%8B%E5%AF%A6%E4%BD%9C%E5%90%A7%EF%BC%81"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/svelte/#%E9%96%8B%E5%A7%8B%E5%AF%A6%E4%BD%9C%E5%90%A7%EF%BC%81">#</a> 開始實作吧!</h2>
<p>這邊先從 xstate 的 machine 開始設定,筆者使用的是 firebase 9 的版本,用法會跟 firebase 8 有些許差異,詳細資訊可以看 firebase 的 <a href="https://firebase.google.com/docs/auth/web/google-signin">官方文件</a>。</p>
<p>下方程式碼 services 中 checkLogin 內的 onAuthStateChanged 可以用來檢查 user 是否 sign in。</p>
<p>login 內的 setCustomParameters 有些參數可以設定,像是 hd(hosted domain) 可以設定你期望登入的使用者帳號。更多參數設定可以看 google identity 的 <a href="https://developers.google.com/identity/protocols/oauth2/openid-connect#authenticationuriparameters">文件</a>。</p>
<p>接著用 <a href="https://stately.ai/viz/920210a6-93b5-41e6-af00-efbba67e299b">visualizer</a> 來看一下登入的流程,logout 的部分因為還沒有想到好的寫法因此先沒有放在 machine 裡面,讀者如果有更好的寫法,歡迎留言分享!</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/xstate.gif" alt="" /></p>
<h6 id="authmachine.js"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/svelte/#authmachine.js">#</a> <strong>authMachine.js</strong></h6>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> <span class="token punctuation">{</span> createMachine<span class="token punctuation">,</span> assign <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"xstate"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> initializeApp <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"firebase/app"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span><br /> getAuth<span class="token punctuation">,</span><br /> signInWithRedirect<span class="token punctuation">,</span><br /> signOut<span class="token punctuation">,</span><br /> onAuthStateChanged<span class="token punctuation">,</span><br /> GoogleAuthProvider<span class="token punctuation">,</span><br /><span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"firebase/auth"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> firebaseConfig <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">apiKey</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token keyword">import</span><span class="token punctuation">.</span>meta<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">VITE_FIREBASE_KEY</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br /> <span class="token literal-property property">authDomain</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token keyword">import</span><span class="token punctuation">.</span>meta<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">VITE_FIREBASE_DOMAIN</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br /> <span class="token literal-property property">projectId</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token keyword">import</span><span class="token punctuation">.</span>meta<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">VITE_FIREBASE_PROJECT</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br /> <span class="token literal-property property">storageBucket</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token keyword">import</span><span class="token punctuation">.</span>meta<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">VITE_FIREBASE_STORAGE</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br /> <span class="token literal-property property">messagingSenderId</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token keyword">import</span><span class="token punctuation">.</span>meta<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">VITE_FIREBASE_SENDERID</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br /> <span class="token literal-property property">appId</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token keyword">import</span><span class="token punctuation">.</span>meta<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">VITE_FIREBASE_APPID</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br /><span class="token function">initializeApp</span><span class="token punctuation">(</span>firebaseConfig<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> authMachine <span class="token operator">=</span> <span class="token function">createMachine</span><span class="token punctuation">(</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token string">"auth"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">initial</span><span class="token operator">:</span> <span class="token string">"checkAuth"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">context</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">auth</span><span class="token operator">:</span> <span class="token keyword">null</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">error</span><span class="token operator">:</span> <span class="token keyword">null</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">states</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">checkAuth</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">invoke</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token string">"authChecker"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">src</span><span class="token operator">:</span> <span class="token string">"checkLogin"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">onDone</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">target</span><span class="token operator">:</span> <span class="token string">"signedIn"</span><span class="token punctuation">,</span> <span class="token literal-property property">actions</span><span class="token operator">:</span> <span class="token string">"setAuth"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">onError</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">actions</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"setError"</span><span class="token punctuation">,</span> <span class="token string">"clearAuth"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">on</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token constant">LOGIN</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">target</span><span class="token operator">:</span> <span class="token string">"signingIn"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">signedIn</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">signingIn</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">invoke</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token string">"authenticator"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">src</span><span class="token operator">:</span> <span class="token string">"login"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">onDone</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">target</span><span class="token operator">:</span> <span class="token string">"checkAuth"</span><span class="token punctuation">,</span><br /> <span class="token comment">// clear error if successful login</span><br /> <span class="token literal-property property">actions</span><span class="token operator">:</span> <span class="token string">"clearError"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">onError</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token comment">// set an error</span><br /> <span class="token literal-property property">actions</span><span class="token operator">:</span> <span class="token string">"setError"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">actions</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">clearAuth</span><span class="token operator">:</span> <span class="token function">assign</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">user</span><span class="token operator">:</span> <span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token literal-property property">auth</span><span class="token operator">:</span> <span class="token keyword">null</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">clearError</span><span class="token operator">:</span> <span class="token function">assign</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">error</span><span class="token operator">:</span> <span class="token keyword">null</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">setAuth</span><span class="token operator">:</span> <span class="token function">assign</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token function-variable function">auth</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">_<span class="token punctuation">,</span> event</span><span class="token punctuation">)</span> <span class="token operator">=></span> event<span class="token punctuation">.</span>data <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">setError</span><span class="token operator">:</span> <span class="token function">assign</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token function-variable function">error</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">_<span class="token punctuation">,</span> event</span><span class="token punctuation">)</span> <span class="token operator">=></span> event<span class="token punctuation">.</span>data<span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">services</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token function-variable function">checkLogin</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Promise</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">resolve<span class="token punctuation">,</span> reject</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> auth <span class="token operator">=</span> <span class="token function">getAuth</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> unsubscribe <span class="token operator">=</span> <span class="token function">onAuthStateChanged</span><span class="token punctuation">(</span>auth<span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">user</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token function">unsubscribe</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">return</span> user <span class="token operator">?</span> <span class="token function">resolve</span><span class="token punctuation">(</span>user<span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token function">reject</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token function-variable function">login</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">_<span class="token punctuation">,</span> event</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>event<span class="token punctuation">.</span>provider <span class="token operator">===</span> <span class="token string">"google"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> provider <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">GoogleAuthProvider</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> auth <span class="token operator">=</span> <span class="token function">getAuth</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> provider<span class="token punctuation">.</span><span class="token function">setCustomParameters</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token literal-property property">hd</span><span class="token operator">:</span> <span class="token string">"errorBaker.com.tw"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">prompt</span><span class="token operator">:</span> <span class="token string">"select_account"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">return</span> <span class="token function">signInWithRedirect</span><span class="token punctuation">(</span>auth<span class="token punctuation">,</span> provider<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token function-variable function">logout</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> auth <span class="token operator">=</span> <span class="token function">getAuth</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">await</span> <span class="token function">signOut</span><span class="token punctuation">(</span>auth<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> authMachine<span class="token punctuation">;</span></code></pre>
<p>把上方的 machine 包在 svelte 的 store 中,onTransition 可以監聽每一次 state 的轉變。</p>
<h6 id="usemachine.js"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/svelte/#usemachine.js">#</a> <strong>useMachine.js</strong></h6>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> <span class="token punctuation">{</span> readable <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"svelte/store"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> interpret <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"xstate"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token function-variable function">useMachine</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">machine<span class="token punctuation">,</span> options</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> service <span class="token operator">=</span> <span class="token function">interpret</span><span class="token punctuation">(</span>machine<span class="token punctuation">,</span> options<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token comment">// wrap machine in a svelte readable store with</span><br /> <span class="token keyword">const</span> store <span class="token operator">=</span> <span class="token function">readable</span><span class="token punctuation">(</span>service<span class="token punctuation">.</span>initialState<span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token keyword">set</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token comment">// every time change state onTransition</span><br /> <span class="token comment">// hook is triggered</span><br /> service<span class="token punctuation">.</span><span class="token function">onTransition</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">state</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token function">set</span><span class="token punctuation">(</span>state<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token comment">// start the machine service</span><br /> service<span class="token punctuation">.</span><span class="token function">start</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> service<span class="token punctuation">.</span><span class="token function">stop</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token comment">// return a custom Svelte store</span><br /> <span class="token keyword">return</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">state</span><span class="token operator">:</span> store<span class="token punctuation">,</span><br /> <span class="token literal-property property">send</span><span class="token operator">:</span> service<span class="token punctuation">.</span>send<span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>machine 設定完成後,我們來實作 App.svelte 。<br />
透過 是否是 signedIn 的狀態來限制使用者進入頁面。</p>
<h6 id="app.svelte"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/svelte/#app.svelte">#</a> <strong>App.svelte</strong></h6>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br /> <span class="token keyword">import</span> <span class="token punctuation">{</span> Router<span class="token punctuation">,</span> Link<span class="token punctuation">,</span> Route<span class="token punctuation">,</span> navigate <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"svelte-routing"</span><span class="token punctuation">;</span><br /> <span class="token keyword">import</span> Login <span class="token keyword">from</span> <span class="token string">"./pages/Login.svelte"</span><span class="token punctuation">;</span><br /> <span class="token keyword">import</span> NotFound <span class="token keyword">from</span> <span class="token string">"./pages/NotFound.svelte"</span><span class="token punctuation">;</span><br /> <span class="token keyword">import</span> Loading <span class="token keyword">from</span> <span class="token string">"./components/Loading.svelte"</span><span class="token punctuation">;</span><br /><br /> <span class="token comment">// Admin Layout</span><br /> <span class="token keyword">import</span> Layout <span class="token keyword">from</span> <span class="token string">"./layout/Layout.svelte"</span><span class="token punctuation">;</span><br /> <span class="token keyword">import</span> Report <span class="token keyword">from</span> <span class="token string">"./layout/Report.svelte"</span><span class="token punctuation">;</span><br /> <span class="token keyword">import</span> <span class="token punctuation">{</span> beforeUpdate <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"svelte"</span><span class="token punctuation">;</span><br /> <span class="token keyword">import</span> <span class="token punctuation">{</span> currentUser <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"./utils/stores"</span><span class="token punctuation">;</span><br /> <span class="token keyword">import</span> authMachine <span class="token keyword">from</span> <span class="token string">"./utils/lib/authMachine"</span><span class="token punctuation">;</span><br /> <span class="token keyword">import</span> <span class="token punctuation">{</span> useMachine <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"./utils/lib/useMachine"</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> <span class="token punctuation">{</span> state <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">useMachine</span><span class="token punctuation">(</span>authMachine<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token comment">// use custom auth machine store</span><br /> <span class="token function">beforeUpdate</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>$state<span class="token punctuation">.</span><span class="token function">matches</span><span class="token punctuation">(</span><span class="token string">"signedIn"</span><span class="token punctuation">)</span> <span class="token operator">&&</span> <span class="token operator">!</span>$state<span class="token punctuation">.</span><span class="token function">matches</span><span class="token punctuation">(</span><span class="token string">"checkAuth"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token function">navigate</span><span class="token punctuation">(</span><span class="token string">"/login"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">export</span> <span class="token keyword">let</span> url <span class="token operator">=</span> <span class="token string">""</span><span class="token punctuation">;</span><br /></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span><br /><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>Router</span> <span class="token attr-name">url</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{url}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> {#if $state.matches('signedIn')}<br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>Route</span> <span class="token attr-name">path</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/<span class="token punctuation">"</span></span> <span class="token attr-name">component</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{Report}<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>Route</span> <span class="token attr-name">path</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>admin/*<span class="token punctuation">"</span></span> <span class="token attr-name">component</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{Layout}<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br /> {:else if $state.matches('checkAuth')}<br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>Loading</span> <span class="token punctuation">/></span></span><br /> {/if}<br /><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>Route</span> <span class="token attr-name">path</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/login<span class="token punctuation">"</span></span> <span class="token attr-name">component</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{Login}<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br /><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>Route</span> <span class="token attr-name">path</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>*<span class="token punctuation">"</span></span> <span class="token attr-name">component</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{NotFound}<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>Router</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>style</span> <span class="token attr-name">global</span> <span class="token attr-name">lang</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>postcss<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token style"><span class="token language-css"><br /> <span class="token atrule"><span class="token rule">@tailwind</span> base<span class="token punctuation">;</span></span><br /> <span class="token atrule"><span class="token rule">@tailwind</span> components<span class="token punctuation">;</span></span><br /> <span class="token atrule"><span class="token rule">@tailwind</span> utilities<span class="token punctuation">;</span></span><br /></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>style</span><span class="token punctuation">></span></span></code></pre>
<p>接著實作 Login.svelte !<br />
這邊只需要使用 <code>send({type:'LOGIN', provider: 'google'})</code> 並且偵測是否是在 signedIn 的狀態。<br />
可以特別注意導頁的時間點,以及出現 loading 的時間,讓使用體驗更流暢。</p>
<h6 id="login.svelte"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/svelte/#login.svelte">#</a> <strong>Login.svelte</strong></h6>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>style</span> <span class="token attr-name">lang</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>postcss<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token style"><span class="token language-css"><br /> <span class="token selector">.title</span> <span class="token punctuation">{</span><br /> <span class="token property">font-family</span><span class="token punctuation">:</span> <span class="token string">"Ribeye Marrow"</span><span class="token punctuation">,</span> cursive<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>style</span><span class="token punctuation">></span></span><br /><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br /> <span class="token keyword">import</span> <span class="token punctuation">{</span> fade <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"svelte/transition"</span><span class="token punctuation">;</span><br /> <span class="token keyword">import</span> Google <span class="token keyword">from</span> <span class="token string">"../components/Google.svelte"</span><span class="token punctuation">;</span><br /> <span class="token keyword">import</span> <span class="token punctuation">{</span> navigate <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"svelte-routing"</span><span class="token punctuation">;</span><br /> <span class="token keyword">import</span> <span class="token punctuation">{</span> currentUser <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"../utils/stores"</span><span class="token punctuation">;</span><br /> <span class="token keyword">import</span> authMachine <span class="token keyword">from</span> <span class="token string">"../utils/lib/authMachine"</span><span class="token punctuation">;</span><br /> <span class="token keyword">import</span> <span class="token punctuation">{</span> useMachine <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"../utils/lib/useMachine"</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> <span class="token punctuation">{</span> state<span class="token punctuation">,</span> send <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">useMachine</span><span class="token punctuation">(</span>authMachine<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">loginProcess</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token function">send</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">"LOGIN"</span><span class="token punctuation">,</span> <span class="token literal-property property">provider</span><span class="token operator">:</span> <span class="token string">"google"</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> state<span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">state</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>state<span class="token punctuation">.</span>value <span class="token operator">===</span> <span class="token string">"signedIn"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token function">navigate</span><span class="token punctuation">(</span><span class="token string">"/"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> currentUser<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token literal-property property">username</span><span class="token operator">:</span> state<span class="token punctuation">.</span>context<span class="token punctuation">.</span>auth<span class="token punctuation">.</span>displayName<span class="token punctuation">,</span><br /> <span class="token literal-property property">email</span><span class="token operator">:</span> state<span class="token punctuation">.</span>context<span class="token punctuation">.</span>auth<span class="token punctuation">.</span>email<span class="token punctuation">,</span><br /> <span class="token literal-property property">picture</span><span class="token operator">:</span> state<span class="token punctuation">.</span>context<span class="token punctuation">.</span>auth<span class="token punctuation">.</span>photoURL<span class="token punctuation">,</span><br /> <span class="token literal-property property">accessToken</span><span class="token operator">:</span> state<span class="token punctuation">.</span>context<span class="token punctuation">.</span>auth<span class="token punctuation">.</span>accessToken<span class="token punctuation">,</span><br /> <span class="token literal-property property">isLogin</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span><br /><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name"><span class="token namespace">in:</span>fade</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Login<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>h-screen flex items-center justify-center<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token comment"><!-- card --></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>section</span><br /> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>max-w-2xl mx-auto p-6 overflow-hidden bg-white rounded-lg dark:bg-gray-800<span class="token punctuation">"</span></span><br /> <span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><br /> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>title md:text-4xl text-3xl flex items-center justify-center flex-wrap <span class="token punctuation">"</span></span><br /> <span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>mr-2 whitespace-nowrap<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Login with<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>Google</span> <span class="token attr-name">className</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>inline-block<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>img</span><br /> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>object-cover w-3/4 md:w-3/4 sm:w-3/4 lg:w-2/3 m-auto mb-20<span class="token punctuation">"</span></span><br /> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>../images/login.png<span class="token punctuation">"</span></span><br /> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>login<span class="token punctuation">"</span></span><br /> <span class="token punctuation">/></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>flex items-center justify-center<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> {#if $state.matches('checkAuth')}<br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span><br /> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>button<span class="token punctuation">"</span></span><br /> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span> w-full flex items-center cursor-not-allowed justify-center capitalize transition-colors tracking-wide py-2 bg-yellow px-4 focus:outline-none text-white text-base font-semibold rounded-lg transition shadow-md ease-in-out duration-500<span class="token punctuation">"</span></span><br /> <span class="token attr-name">disabled</span><br /> <span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>svg</span><br /> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>animate-spin h-5 w-5 mr-3 <span class="token punctuation">"</span></span><br /> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>24<span class="token punctuation">"</span></span><br /> <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>24<span class="token punctuation">"</span></span><br /> <span class="token attr-name">viewBox</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0 0 24 24<span class="token punctuation">"</span></span><br /> <span class="token attr-name">fill</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>none<span class="token punctuation">"</span></span><br /> <span class="token attr-name">xmlns</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>http://www.w3.org/2000/svg<span class="token punctuation">"</span></span><br /> <span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>path</span><br /> <span class="token attr-name">opacity</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0.2<span class="token punctuation">"</span></span><br /> <span class="token attr-name">fill-rule</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>evenodd<span class="token punctuation">"</span></span><br /> <span class="token attr-name">clip-rule</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>evenodd<span class="token punctuation">"</span></span><br /> <span class="token attr-name">d</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>M12 19C15.866 19 19 15.866 19 12C19 8.13401 15.866 5 12 5C8.13401 5 5 8.13401 5 12C5 15.866 8.13401 19 12 19ZM12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z<span class="token punctuation">"</span></span><br /> <span class="token attr-name">fill</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#FBF6EB<span class="token punctuation">"</span></span><br /> <span class="token punctuation">/></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>path</span><br /> <span class="token attr-name">d</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>M2 12C2 6.47715 6.47715 2 12 2V5C8.13401 5 5 8.13401 5 12H2Z<span class="token punctuation">"</span></span><br /> <span class="token attr-name">fill</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#FBF6EB<span class="token punctuation">"</span></span><br /> <span class="token punctuation">/></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>svg</span><span class="token punctuation">></span></span><br /><br /> Processing<br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span><br /> {:else}<br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span><br /> <span class="token attr-name"><span class="token namespace">on:</span>click</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{loginProcess}<span class="token punctuation">"</span></span><br /> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><br /> w-2/3<br /> text-base<br /> font-semibold<br /> py-2<br /> px-4<br /> text-white<br /> bg-yellow<br /> hover:bg-yellow<br /> rounded-lg<br /> shadow-md<br /> focus:outline-none<br /> transition<br /> duration-500<br /> ease-in-out<br /> <span class="token punctuation">"</span></span><br /> <span class="token punctuation">></span></span><br /> Login<br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span><br /> {/if}<br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>section</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>最後來看一下實際完成的流程吧!<br />
<img src="https://blog.errorbaker.tw/img/posts/ruofan/svelte.gif" alt="" /></p>
<h2 id="%E5%9B%9E%E9%A1%A7"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/svelte/#%E5%9B%9E%E9%A1%A7">#</a> 回顧</h2>
<p>一開始在 firebase 開啟的 google login 設定,在使用者登入後我們可以在 firebase 看得到使用者被新增的時間點跟最後一次登入的時間點。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/firebase-auth-user.png" alt="" /></p>
<h2 id="%E5%B0%8F%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/svelte/#%E5%B0%8F%E7%B5%90">#</a> 小結</h2>
<p>在 google login 成功後可以拿到 idToken 來使用,但須特別注意的是 token 有效時限是 一個小時,因此還需要實作 refresh token 的部分。<br />
整體使用 svelte 實作的過程,渡過熟悉規則上的使用後,覺得還蠻不錯的,推薦給大家!</p>
<p>在閱讀文章時如果有遇到什麼問題,或是有什麼建議,都歡迎留言告訴我,謝謝。😃</p>
<ul>
<li><a href="https://github.com/ruofanwei/svelte-xstate-app">Github | Repo: Svelte</a></li>
</ul>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/svelte/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li><a href="https://svelte.dev/blog/svelte-3-rethinking-reactivity">Documentation | Svelte 3: Rethinking reactivity</a></li>
</ul>
實作 Nest.js 中的 Migration 與 Seeder
2021-09-05T00:00:00Z
https://blog.errorbaker.tw/posts/minw/nest-js-migration/
<!-- summary -->
<p>這一篇文章是上一篇 <a href="https://minw.blog/node-nest-intro">Nest.js 初探 CRUD</a> 的補充,由於 Typeorm + Nestjs 實在有太多坑可以踩,這邊紀錄在實作 Migration 跟 Seeder 的過程。</p>
<!-- summary -->
<!-- more -->
<h2 id="migration-%26-seeder"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/nest-js-migration/#migration-%26-seeder">#</a> Migration & Seeder</h2>
<p>Migration 的用途是用來紀錄 Table 的變動,通常結構上會分為 up 跟 down 分別在裡面撰寫進行更動的程式與需要復原更動的程式,而 Seeder 也是類似 Migration 的概念,差別在 Seeder 專指替 Table 塞入假資料的程式。</p>
<p>而在 Typeorm 中,有原生提供的 Migration 並且可以根據 Entity 自動產生,然而 Seeder 並不在原生方案之中,在探索可以怎麼實作 Seeder 時,也有看到 typeorm-seeding [^1] 但有鑒於上一次更新時間已是 2020 年中,加上也想要藉此更了解 TypeORM 的運作,所以沒有使用,以下則會分享用原生 Migration 實作 Seeder 的過程,相關的專案設置可以參考 <a href="https://minw.blog/node-nest-intro">上一篇 CRUD</a> 的設置。</p>
<h2 id="typeorm-%26-entity"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/nest-js-migration/#typeorm-%26-entity">#</a> TypeORM & Entity</h2>
<p>首先針對 TypeORM 的 <code>ormconfig.json</code> 我們將預設的 src 改為 build,這是因為實際執行 migration 檔案的時候是執行 js 檔,而 Nest 編譯後才會將 js 丟在 build 之中(預設有有可能是 dist) 之中,所以將設置的參考位置改如下:</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br /> ...<br /> <span class="token property">"entities"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token string">"build/**/*.{ts,js}"</span><br /> <span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token property">"migrations"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token string">"build/db/migration/**/*.{ts,js}"</span><br /> <span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token property">"subscribers"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token string">"build/db/subscriber/**/*.{ts,js}"</span><br /> <span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token property">"cli"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"entitiesDir"</span><span class="token operator">:</span> <span class="token string">"build/modules"</span><span class="token punctuation">,</span><br /> <span class="token property">"migrationsDir"</span><span class="token operator">:</span> <span class="token string">"build/db/migration"</span><span class="token punctuation">,</span><br /> <span class="token property">"subscribersDir"</span><span class="token operator">:</span> <span class="token string">"build/db/subscriber"</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>接著我們開始設定對應資料的 Entity,在 TypeORM 中 Entity 是可以代表 Table 的 Class,將定義資料類型的過程拉了出來,這讓我們操作 Table 的內容的時候,是基於這個中間 Class 產生出的 Instance,而不是直接的資料。以常見的 User 為例,這是一個有關聯到 Role 的 User Table,我們先建立好 User 的 Entity:</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span><br /> Entity<span class="token punctuation">,</span><br /> PrimaryGeneratedColumn<span class="token punctuation">,</span><br /> Column<span class="token punctuation">,</span><br /> ManyToOne<span class="token punctuation">,</span><br /> JoinColumn<span class="token punctuation">,</span><br /> CreateDateColumn<span class="token punctuation">,</span><br /><span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'typeorm'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> RoleEntity <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./role.entity'</span><span class="token punctuation">;</span><br /><br /><span class="token decorator"><span class="token at operator">@</span><span class="token function">Entity</span></span><span class="token punctuation">(</span><span class="token punctuation">{</span> name<span class="token operator">:</span> <span class="token string">'user'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">UserEntity</span> <span class="token punctuation">{</span><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">PrimaryGeneratedColumn</span></span><span class="token punctuation">(</span><span class="token string">'increment'</span><span class="token punctuation">)</span><br /> id<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">;</span><br /><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">Column</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> email<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">Column</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> password<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">Column</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> roleId<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">;</span><br /><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">ManyToOne</span></span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> RoleEntity<span class="token punctuation">,</span> <span class="token punctuation">{</span> onDelete<span class="token operator">:</span> <span class="token string">'CASCADE'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">JoinColumn</span></span><span class="token punctuation">(</span><span class="token punctuation">{</span> name<span class="token operator">:</span> <span class="token string">'roleId'</span><span class="token punctuation">,</span> referencedColumnName<span class="token operator">:</span> <span class="token string">'id'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /> role<span class="token operator">:</span> RoleEntity<span class="token punctuation">;</span><br /><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">CreateDateColumn</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> createdAt<span class="token operator">:</span> Date<span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>而 User 有關聯的 Role Entity 也是。</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> Entity<span class="token punctuation">,</span> PrimaryGeneratedColumn<span class="token punctuation">,</span> Column <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'typeorm'</span><span class="token punctuation">;</span><br /><br /><span class="token decorator"><span class="token at operator">@</span><span class="token function">Entity</span></span><span class="token punctuation">(</span><span class="token punctuation">{</span> name<span class="token operator">:</span> <span class="token string">'role'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">RoleEntity</span> <span class="token punctuation">{</span><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">PrimaryGeneratedColumn</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> id<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">;</span><br /><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">Column</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> name<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>接著我們執行 Build,確定 Build 中已有相關的 Entity js 檔案,再來就進到我們的重頭戲:Migration。</p>
<h2 id="migration"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/nest-js-migration/#migration">#</a> Migration</h2>
<p>在執行 migration 指令之前,我們先安裝 <code>ts-node</code> 確保在 node 環境下可以順利使用我們的 ts 檔案:</p>
<pre class="language-shell"><code class="language-shell"><span class="token function">yarn</span> <span class="token function">add</span> <span class="token parameter variable">-D</span> ts-node</code></pre>
<p>接著在 package.json 中,加入 TypeORM cli 的指令,讓我們之後可以居於 <code>ts-node</code> 去使用 TypeORM [^3]。</p>
<pre class="language-json"><code class="language-json"><span class="token property">"script"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> ...<br /> <span class="token property">"typeorm"</span><span class="token operator">:</span> <span class="token string">"ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js"</span><br /> ...<br /><span class="token punctuation">}</span></code></pre>
<p>接下來,我們可以如常的透過 TypeORM cli 指令來自動產生根據 Entity 的 Migration,這是最理想的狀態,我們不需要自己去重新寫一次 Table 的定義,舉例來說:</p>
<pre class="language-shell"><code class="language-shell"><span class="token function">yarn</span> typeorm migration:create <span class="token parameter variable">-n</span> CreateUser</code></pre>
<p>然而實際產生出來會是像這樣的 Migration:</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span>MigrationInterface<span class="token punctuation">,</span> QueryRunner<span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"typeorm"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">CreateUser1630222102818</span> <span class="token keyword">implements</span> <span class="token class-name">MigrationInterface</span> <span class="token punctuation">{</span><br /> name <span class="token operator">=</span> <span class="token string">'CreateUser1630222102818'</span><br /><br /> <span class="token keyword">public</span> <span class="token keyword">async</span> <span class="token function">up</span><span class="token punctuation">(</span>queryRunner<span class="token operator">:</span> QueryRunner<span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator"><</span><span class="token keyword">void</span><span class="token operator">></span> <span class="token punctuation">{</span><br /> <span class="token keyword">await</span> queryRunner<span class="token punctuation">.</span><span class="token function">query</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">CREATE TABLE \\</span><span class="token template-punctuation string">`</span></span>learning<span class="token operator">-</span>casino\\<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">.\\</span><span class="token template-punctuation string">`</span></span>role\\<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> (\\</span><span class="token template-punctuation string">`</span></span>id\\<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> int NOT NULL AUTO_INCREMENT, \\</span><span class="token template-punctuation string">`</span></span>name\\<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> varchar(255) NOT NULL, PRIMARY KEY (\\</span><span class="token template-punctuation string">`</span></span>id\\<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">)) ENGINE=InnoDB</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">await</span> queryRunner<span class="token punctuation">.</span><span class="token function">query</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">CREATE TABLE \\</span><span class="token template-punctuation string">`</span></span>learning<span class="token operator">-</span>casino\\<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">.\\</span><span class="token template-punctuation string">`</span></span>user\\<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> (\\</span><span class="token template-punctuation string">`</span></span>id\\<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> int NOT NULL AUTO_INCREMENT, \\</span><span class="token template-punctuation string">`</span></span>nickname\\<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> varchar(255) NOT NULL, \\</span><span class="token template-punctuation string">`</span></span>email\\<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> varchar(255) NOT NULL, \\</span><span class="token template-punctuation string">`</span></span>password\\<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> varchar(255) NOT NULL, \\</span><span class="token template-punctuation string">`</span></span>points\\<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> int NOT NULL, \\</span><span class="token template-punctuation string">`</span></span>roleId\\<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> int NOT NULL, \\</span><span class="token template-punctuation string">`</span></span>createdAt\\<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (\\</span><span class="token template-punctuation string">`</span></span>id\\<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">)) ENGINE=InnoDB</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">public</span> <span class="token keyword">async</span> <span class="token function">down</span><span class="token punctuation">(</span>queryRunner<span class="token operator">:</span> QueryRunner<span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator"><</span><span class="token keyword">void</span><span class="token operator">></span> <span class="token punctuation">{</span><br /> <span class="token keyword">await</span> queryRunner<span class="token punctuation">.</span><span class="token function">query</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">ALTER TABLE \\</span><span class="token template-punctuation string">`</span></span>learning<span class="token operator">-</span>casino\\<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">.\\</span><span class="token template-punctuation string">`</span></span>user\\<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> DROP FOREIGN KEY \\</span><span class="token template-punctuation string">`</span></span>FK_c28e52f758e7bbc53828db92194\\<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">await</span> queryRunner<span class="token punctuation">.</span><span class="token function">query</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">DROP TABLE \\</span><span class="token template-punctuation string">`</span></span>learning<span class="token operator">-</span>casino\\<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">.\\</span><span class="token template-punctuation string">`</span></span>user\\<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">await</span> queryRunner<span class="token punctuation">.</span><span class="token function">query</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">DROP TABLE \\</span><span class="token template-punctuation string">`</span></span>learning<span class="token operator">-</span>casino\\<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">.\\</span><span class="token template-punctuation string">`</span></span>role\\<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /><span class="token punctuation">}</span></code></pre>
<p>在使用上產生出的結果不是很直覺,尤其要 debug Foreign Key 跟加入 seeder 的時候也不太好維護,於是這邊做了一個取捨,另外再維護一份 Migration 的 table 設定,改為使用比較直覺的語法去建立 Table,然這樣的缺點是,要同步確認 Entity 的改動有沒有同步到 Migration 之中,但考量到如果原先的 Migration 也是由 Entity 自動產生而來,後續 Entity 修改也需要重新 Revert 並執行產生的指令,這樣的缺點還可以接受。</p>
<p>於是在 Migration Up 的部分,我們透過 createTable 去新增 Role, User Table 並設定他的參數,並透過 createForeignKey 來設定關聯的 Foreign Key。其中,這邊 <code>createTable</code> 與 <code>createForeignKey</code> 都是 Query 層級,可以理解成這邊的指令都是直接對 DB 進行操作。</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">public</span> <span class="token keyword">async</span> <span class="token function">up</span><span class="token punctuation">(</span>queryRunner<span class="token operator">:</span> QueryRunner<span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator"><</span><span class="token keyword">void</span><span class="token operator">></span> <span class="token punctuation">{</span><br /> <span class="token keyword">await</span> queryRunner<span class="token punctuation">.</span><span class="token function">createTable</span><span class="token punctuation">(</span><br /> <span class="token keyword">new</span> <span class="token class-name">Table</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> name<span class="token operator">:</span> <span class="token string">'role'</span><span class="token punctuation">,</span><br /> columns<span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> name<span class="token operator">:</span> <span class="token string">'id'</span><span class="token punctuation">,</span><br /> type<span class="token operator">:</span> <span class="token string">'int'</span><span class="token punctuation">,</span><br /> isPrimary<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> isGenerated<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> generationStrategy<span class="token operator">:</span> <span class="token string">'increment'</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> name<span class="token operator">:</span> <span class="token string">'name'</span><span class="token punctuation">,</span><br /> type<span class="token operator">:</span> <span class="token string">'varchar'</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">await</span> queryRunner<span class="token punctuation">.</span><span class="token function">createTable</span><span class="token punctuation">(</span><br /> <span class="token keyword">new</span> <span class="token class-name">Table</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> name<span class="token operator">:</span> <span class="token string">'user'</span><span class="token punctuation">,</span><br /> columns<span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> name<span class="token operator">:</span> <span class="token string">'id'</span><span class="token punctuation">,</span><br /> type<span class="token operator">:</span> <span class="token string">'int'</span><span class="token punctuation">,</span><br /> isPrimary<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> isGenerated<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> generationStrategy<span class="token operator">:</span> <span class="token string">'increment'</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> name<span class="token operator">:</span> <span class="token string">'nickname'</span><span class="token punctuation">,</span><br /> type<span class="token operator">:</span> <span class="token string">'varchar'</span><span class="token punctuation">,</span><br /> <span class="token keyword">default</span><span class="token operator">:</span> <span class="token keyword">null</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> name<span class="token operator">:</span> <span class="token string">'email'</span><span class="token punctuation">,</span><br /> type<span class="token operator">:</span> <span class="token string">'varchar'</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> name<span class="token operator">:</span> <span class="token string">'password'</span><span class="token punctuation">,</span><br /> type<span class="token operator">:</span> <span class="token string">'varchar'</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> name<span class="token operator">:</span> <span class="token string">'points'</span><span class="token punctuation">,</span><br /> type<span class="token operator">:</span> <span class="token string">'integer'</span><span class="token punctuation">,</span><br /> <span class="token keyword">default</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> name<span class="token operator">:</span> <span class="token string">'roleId'</span><span class="token punctuation">,</span><br /> type<span class="token operator">:</span> <span class="token string">'integer'</span><span class="token punctuation">,</span><br /> <span class="token keyword">default</span><span class="token operator">:</span> <span class="token number">4</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> name<span class="token operator">:</span> <span class="token string">'createdAt'</span><span class="token punctuation">,</span><br /> type<span class="token operator">:</span> <span class="token string">'timestamp'</span><span class="token punctuation">,</span><br /> <span class="token keyword">default</span><span class="token operator">:</span> <span class="token string">'now()'</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">await</span> queryRunner<span class="token punctuation">.</span><span class="token function">createForeignKey</span><span class="token punctuation">(</span><br /> <span class="token string">'user'</span><span class="token punctuation">,</span><br /> <span class="token keyword">new</span> <span class="token class-name">TableForeignKey</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> columnNames<span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'roleId'</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> referencedColumnNames<span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'id'</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> referencedTableName<span class="token operator">:</span> <span class="token string">'role'</span><span class="token punctuation">,</span><br /> onDelete<span class="token operator">:</span> <span class="token string">'CASCADE'</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>接著在 Down 的部分,我們要刪除原先建立的 Foreign Key 並且刪除 Table:</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">public</span> <span class="token keyword">async</span> <span class="token function">down</span><span class="token punctuation">(</span>queryRunner<span class="token operator">:</span> QueryRunner<span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator"><</span><span class="token keyword">void</span><span class="token operator">></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> table <span class="token operator">=</span> <span class="token keyword">await</span> queryRunner<span class="token punctuation">.</span><span class="token function">getTable</span><span class="token punctuation">(</span><span class="token string">'user'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> foreignKey <span class="token operator">=</span> table<span class="token punctuation">.</span>foreignKeys<span class="token punctuation">.</span><span class="token function">find</span><span class="token punctuation">(</span><br /> <span class="token punctuation">(</span>fk<span class="token punctuation">)</span> <span class="token operator">=></span> fk<span class="token punctuation">.</span>columnNames<span class="token punctuation">.</span><span class="token function">indexOf</span><span class="token punctuation">(</span><span class="token string">'roleId'</span><span class="token punctuation">)</span> <span class="token operator">!==</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">,</span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">await</span> queryRunner<span class="token punctuation">.</span><span class="token function">dropForeignKey</span><span class="token punctuation">(</span><span class="token string">'user'</span><span class="token punctuation">,</span> foreignKey<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">await</span> queryRunner<span class="token punctuation">.</span><span class="token function">dropColumn</span><span class="token punctuation">(</span><span class="token string">'user'</span><span class="token punctuation">,</span> <span class="token string">'roleId'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">await</span> queryRunner<span class="token punctuation">.</span><span class="token function">dropTable</span><span class="token punctuation">(</span><span class="token string">'user'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">await</span> queryRunner<span class="token punctuation">.</span><span class="token function">dropTable</span><span class="token punctuation">(</span><span class="token string">'role'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>那當我們設置好 Migration 後,就可以 Build 確定 Migration 編譯成 js 進到 Build 檔案夾,並且執行 <code>yarn typeorm migration:run</code>,執行 migration run 的時候,TypeORM 會幫我們在 DB 裏面開一個新的 Migration Table 紀錄我們使用的 Migration,並且執行我們 Migration 中的 Up Function,如果今天需要退回到上一個 Migration,也可以透過 <code>yarn typeorm migration:revert</code> 就可以回到上一個版本的 Migration,而同樣的 TypeORM 也會執行我們 Migration 中的 Down Function 來復原。</p>
<p>這邊要注意的是,由於 TypeORM 預設執行 Migration 的時候吃的 Config 檔是 ormconfig.json,如果有另外的 config 被包成物件在 src 中,這樣的方式 TypeORM CLI 可能吃不到設定檔,會需要另外讀取 config 的位置,或將物件的 config 內容取自於 ormconfig.json 中。</p>
<h2 id="seeder"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/nest-js-migration/#seeder">#</a> Seeder</h2>
<p>現在有 Table 了但我們還沒有相關的資料,於是我們可以開一個 Role 的 Seed 檔案:</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">export</span> <span class="token keyword">const</span> RoleSeed <span class="token operator">=</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> id<span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span><br /> name<span class="token operator">:</span> <span class="token string">'admin'</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> id<span class="token operator">:</span> <span class="token number">2</span><span class="token punctuation">,</span><br /> name<span class="token operator">:</span> <span class="token string">'user'</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">]</span></code></pre>
<p>由於 TypeORM 不像 Sequelize 有原生提供 seeding 的功能,這邊我們可以使用 Migration 來實作,非常簡單的,只要在 createTable 之後,透過 TypeORM Repository 來對 DB 進行操作就可以了 [^2]:</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">const</span> roleRepository <span class="token operator">=</span> <span class="token function">getRepository</span><span class="token punctuation">(</span><span class="token string">'role'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token keyword">await</span> <span class="token builtin">Promise</span><span class="token punctuation">.</span><span class="token function">all</span><span class="token punctuation">(</span>RoleSeed<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span>role<span class="token punctuation">)</span> <span class="token operator">=></span> roleRepository<span class="token punctuation">.</span><span class="token function">save</span><span class="token punctuation">(</span>role<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>這邊由於開始使用 Repository,Typeorm 如果要關聯到對應的 Table 建立 Repository 時,會需要我們建立好的 Entity Class,所以這邊我們在 getRepositoty 中帶入前面 Entity name。</p>
<p>以上步驟都完成後,在一次 Build 執行 <code>yarn typeorm migration:run</code> 就可以了。</p>
<h2 id="typeorm-%E8%A7%80%E5%BF%B5"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/nest-js-migration/#typeorm-%E8%A7%80%E5%BF%B5">#</a> TypeORM 觀念</h2>
<h3 id="repository-pattern"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/nest-js-migration/#repository-pattern">#</a> Repository Pattern</h3>
<p>一開始摸 Entity 的時候覺得不太好對應到其他使用過的 ORM e.g. Sequelize 去想像,這次重新閱讀了 Typeorm 跟 Sequelize 的文件,在 Sequelize 中,Model 事實上是將 Entity + 取資料的邏輯結合在一起,舉例來說,下方是一段 sequelize init 時會產生出來的 model index.js 以及我們自己撰寫的 model.js [^5]:</p>
<pre class="language-js"><code class="language-js"><span class="token string">'use strict'</span><span class="token punctuation">;</span><br />module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">sequelize<span class="token punctuation">,</span> DataTypes</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> User <span class="token operator">=</span> sequelize<span class="token punctuation">.</span><span class="token function">define</span><span class="token punctuation">(</span><span class="token string">'User'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">nickname</span><span class="token operator">:</span> DataTypes<span class="token punctuation">.</span><span class="token constant">STRING</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">email</span><span class="token operator">:</span> DataTypes<span class="token punctuation">.</span><span class="token constant">STRING</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">status</span><span class="token operator">:</span> DataTypes<span class="token punctuation">.</span><span class="token constant">ENUM</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token string">'active'</span><span class="token punctuation">,</span> <span class="token string">'inactive'</span><span class="token punctuation">]</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> User<span class="token punctuation">.</span><span class="token function-variable function">associate</span> <span class="token operator">=</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">models</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> User<span class="token punctuation">.</span><span class="token function">hasMany</span><span class="token punctuation">(</span>models<span class="token punctuation">.</span>Role<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> User<span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>接著可以看到 Sequelize 將我們的 Model 設定檔 import 之後,直接放入 DB Object 中,在往後的操作中我們直接由 Model 進行操作:</p>
<pre class="language-js"><code class="language-js"><span class="token string">'use strict'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> fs <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'fs'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> path <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'path'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> Sequelize <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'sequelize'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> basename <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">basename</span><span class="token punctuation">(</span>__filename<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> env <span class="token operator">=</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">NODE_ENV</span> <span class="token operator">||</span> <span class="token string">'development'</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> config <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span>__dirname <span class="token operator">+</span> <span class="token string">'/../config/config.json'</span><span class="token punctuation">)</span><span class="token punctuation">[</span>env<span class="token punctuation">]</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> db <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">let</span> sequelize<span class="token punctuation">;</span><br /><span class="token keyword">if</span> <span class="token punctuation">(</span>config<span class="token punctuation">.</span>use_env_variable<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> sequelize <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Sequelize</span><span class="token punctuation">(</span>process<span class="token punctuation">.</span>env<span class="token punctuation">[</span>config<span class="token punctuation">.</span>use_env_variable<span class="token punctuation">]</span><span class="token punctuation">,</span> config<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br /> sequelize <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Sequelize</span><span class="token punctuation">(</span>config<span class="token punctuation">.</span>database<span class="token punctuation">,</span> config<span class="token punctuation">.</span>username<span class="token punctuation">,</span> config<span class="token punctuation">.</span>password<span class="token punctuation">,</span> config<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><br />fs<br /> <span class="token punctuation">.</span><span class="token function">readdirSync</span><span class="token punctuation">(</span>__dirname<span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span><span class="token parameter">file</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span>file<span class="token punctuation">.</span><span class="token function">indexOf</span><span class="token punctuation">(</span><span class="token string">'.'</span><span class="token punctuation">)</span> <span class="token operator">!==</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token operator">&&</span> <span class="token punctuation">(</span>file <span class="token operator">!==</span> basename<span class="token punctuation">)</span> <span class="token operator">&&</span> <span class="token punctuation">(</span>file<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token operator">-</span><span class="token number">3</span><span class="token punctuation">)</span> <span class="token operator">===</span> <span class="token string">'.js'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token parameter">file</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> model <span class="token operator">=</span> sequelize<span class="token punctuation">[</span><span class="token string">'import'</span><span class="token punctuation">]</span><span class="token punctuation">(</span>path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span>__dirname<span class="token punctuation">,</span> file<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> db<span class="token punctuation">[</span>model<span class="token punctuation">.</span>name<span class="token punctuation">]</span> <span class="token operator">=</span> model<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br />Object<span class="token punctuation">.</span><span class="token function">keys</span><span class="token punctuation">(</span>db<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token parameter">modelName</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>db<span class="token punctuation">[</span>modelName<span class="token punctuation">]</span><span class="token punctuation">.</span>associate<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> db<span class="token punctuation">[</span>modelName<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">associate</span><span class="token punctuation">(</span>db<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br />db<span class="token punctuation">.</span>sequelize <span class="token operator">=</span> sequelize<span class="token punctuation">;</span><br />db<span class="token punctuation">.</span>Sequelize <span class="token operator">=</span> Sequelize<span class="token punctuation">;</span><br /><br />module<span class="token punctuation">.</span>exports <span class="token operator">=</span> db<span class="token punctuation">;</span></code></pre>
<p>而我們 Model 定義檔就類似於 Entity 的效果,但實際效果上,TypeORM 的 Entity 更為純粹,並透過 Repository Pattern 將取資料的部分分開,需要做資料操作的時候透過 Repository:</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">UserService</span> <span class="token punctuation">{</span><br /> <span class="token function">constructor</span><span class="token punctuation">(</span><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">InjectRepository</span></span><span class="token punctuation">(</span>UserEntity<span class="token punctuation">)</span><br /> <span class="token keyword">private</span> usersRepository<span class="token operator">:</span> Repository<span class="token operator"><</span>UserEntity<span class="token operator">></span><span class="token punctuation">,</span><br /> <span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><br /> <span class="token operator">...</span><br /><span class="token punctuation">}</span></code></pre>
<p>而也可以將 Entity 作為資料的 Instance,舉例來說:repository 回傳的資料 class 會是 UserEntity:</p>
<pre class="language-ts"><code class="language-ts"> <span class="token keyword">async</span> <span class="token function">getUsers</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator"><</span>UserEntity<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token operator">></span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>usersRepository<span class="token punctuation">.</span><span class="token function">find</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span></code></pre>
<p>相較於原本 Model 將 Entity 與執行綁在一起的做法,Entity 的彈性更大。</p>
<h3 id="foreign-key-%E9%81%8B%E4%BD%9C"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/nest-js-migration/#foreign-key-%E9%81%8B%E4%BD%9C">#</a> Foreign Key 運作</h3>
<p>由於在開發 revert 的時候踩到了很多 ForeignKey 衝突的雷,這邊花了一些時間去了解 MySQL Foreign Key 的運作方式。</p>
<p>Foreign Key 是為了實現 Model Relation 的一個機制,而在講 Model Relation 之前就要先介紹三種 Relation [^7]「一對一」、「一對多」跟「多對多」,以一個社群軟體為例:</p>
<p>一對一:每個使用者只有一個大頭貼。</p>
<pre class="language-text"><code class="language-text">// user has one avatar<br />User {<br /> id<br /> nickname<br /> avatar_id<br />}<br /><br />// avatar belongs to user<br />Avatar {<br /> id<br /> img_url<br />}</code></pre>
<p>一對多:一個使用者有很多貼文。</p>
<pre class="language-text"><code class="language-text">// user has many post<br />User {<br /> id<br /> nickname<br /> post_id<br />}<br /><br />// post belongs to user<br />Post {<br /> id<br /> content<br />}</code></pre>
<p>多對多:一個使用者可以有很多粉專、而粉專也可以被很多使用者擁有。</p>
<pre class="language-text"><code class="language-text">// user has many userpage ~ has many page<br />User {<br /> id<br /> nickname<br />}<br /><br />// page has many userpage ~ has many user<br />Page {<br /> id<br /> url<br />}<br /><br />// userpage belongs to user & page<br />UserPage {<br /> id<br /> page_id<br /> user_id<br />}</code></pre>
<p>而 Foreign Key (FK) 就是指那些對應到其他 Table 的 Key e.g. page_id, user_id, avatar_id ...,而為什麼要特別指定 Foreign Key 而不直接 JOIN 就好?</p>
<p>事實上 MySQL 預設引擎中也沒有提供 FK 的功能,而是需要使用 InnoDB 儲存引擎才有。之所以有 FK 是因為於 DB 的角度並不會知道這個欄位的特殊意義,反之在 Table 指定 FK 可以讓 DB 知道資料的連動性,並警示能不能新增跟刪除,甚至設定更新跟刪除的連動動作:<code>CASCADE</code>, <code>SET NULL</code>, <code>NO ACTION</code> 與 <code>RESTRICT</code>。</p>
<p>但既然有額外去設定為 FK,MySQL 也有另外做一些處理,這些關聯性的資料會被存在 MySQL 的系統 DB information_schema 中的 <code>KEY_CONLUMN_USAGE</code> table 中,會定義所有 key 的名稱、類型跟連結方式。所以 revert 的 drop FK 不單單只是刪除而已,也包含在這個 table 中將關聯的資訊刪除 [^8]。</p>
<h2 id="%E5%B0%8F%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/nest-js-migration/#%E5%B0%8F%E7%B5%90">#</a> 小結</h2>
<p>踩坑的過程覺得幾個是 debug 的關鍵點:</p>
<ul>
<li>Migration 的連線設定吃的是哪一段?要吃的檔案到底在哪裡?有更新到嗎?</li>
<li>階段執行的層級是什麼? 是 Query 還是 Repository?</li>
<li>Revert 有完全抹消 Run 的執行嗎?除了資料之外 Foreign Key 有清除嗎?</li>
</ul>
<p>以上是這次踩坑一個機會了解 ORM 跟 DB 之間的互動方式。</p>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/nest-js-migration/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li><a href="https://minw.blog/node-nest-intro">初探用 Nest.js 做出一個 CRUD 服務 | min</a></li>
<li><a href="https://github.com/typeorm/typeorm/tree/master/docs">typeorm/docs at master · typeorm/typeorm</a></li>
<li>[^1]: <a href="https://www.npmjs.com/package/typeorm-seeding">typeorm-seeding - npm</a></li>
<li>[^2]: <a href="https://stackoverflow.com/questions/51198817/typeorm-how-to-seed-database">TypeORM how to seed database - Stack Overflow</a></li>
<li>[^3]: <a href="https://stackoverflow.com/questions/66604876/typeorm-commands-return-nothing">node.js - TypeORM commands return nothing - Stack Overflow</a></li>
<li>[^5]: <a href="https://sequelize.org/master/manual/model-basics.html">Manual | Sequelize</a></li>
<li>[^6]: <a href="https://stackoverflow.com/questions/3433975/why-use-foreign-key-constraints-in-mysql">sql - Why use Foreign Key constraints in MySQL? - Stack Overflow</a></li>
<li>[^7]: <a href="https://railsbook.tw/chapters/18-model-relationship.html">Model 關連性 為你自己學 Ruby on Rails | 高見龍</a></li>
<li>[^8]: <a href="https://stackoverflow.com/questions/201621/how-do-i-see-all-foreign-keys-to-a-table-or-column">mysql - How do I see all foreign keys to a table or column? - Stack Overflow</a></li>
</ul>
如何從 GitHub Template 更新 repo?
2021-09-05T00:00:00Z
https://blog.errorbaker.tw/posts/sixwings/update-github-repo-from-template/
<!-- summary -->
<p>從 Github Template 建立的 repo,當參照的 Github Template 更新的時候,要如何同步版本呢?</p>
<!-- summary -->
<!-- more -->
<h1 id="%E5%89%8D%E6%83%85%E6%8F%90%E8%A6%81"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/update-github-repo-from-template/#%E5%89%8D%E6%83%85%E6%8F%90%E8%A6%81">#</a> 前情提要</h1>
<p>事情是這樣子的,身為<a href="https://bootcamp.lidemy.com/">程式導師實驗計畫</a> 的學生,最近老師默默地更新了<a href="https://github.com/Lidemy/mentor-program-5th/">課程的 Template</a> ,於是想說跟著一起更新,也順便記錄這段過程。</p>
<h1 id="%E5%88%9D%E6%AD%A5%E8%AA%AA%E6%98%8E"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/update-github-repo-from-template/#%E5%88%9D%E6%AD%A5%E8%AA%AA%E6%98%8E">#</a> 初步說明</h1>
<p><img src="https://blog.errorbaker.tw/img/posts/sixwings/figue-1.png" alt="" /></p>
<p>template 是這次需要更新的遠端 remote,後面三個紅色方塊為本次需要更新的 commit。<br />
而下方的 My repo 則是目前的狀態,綠色方塊是從 template 那邊初始化的 commit,而後面黃色方塊則是後續更新作業時提交的 commit。另外後面尚有一段還沒 merge 進去的 homework 作業分支。</p>
<h1 id="%E5%90%88%E4%BD%B5%E5%89%8D%EF%BC%8C%E8%AB%8B%E5%8B%99%E5%BF%85%E5%85%88%E5%81%9A%E7%9A%84%E5%8B%95%E4%BD%9C"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/update-github-repo-from-template/#%E5%90%88%E4%BD%B5%E5%89%8D%EF%BC%8C%E8%AB%8B%E5%8B%99%E5%BF%85%E5%85%88%E5%81%9A%E7%9A%84%E5%8B%95%E4%BD%9C">#</a> 合併前,請務必先做的動作</h1>
<p>請先把 repo 根目錄下的 <strong>package.json, package-lock.json</strong> 檔案複製到 repo 以外的資料夾做備份,因為接下來的衝突會需要使用到它。</p>
<h1 id="%E5%88%86%E6%94%AF%E5%90%88%E4%BD%B5-%26-%E8%A1%9D%E7%AA%81%E7%99%BC%E7%94%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/update-github-repo-from-template/#%E5%88%86%E6%94%AF%E5%90%88%E4%BD%B5-%26-%E8%A1%9D%E7%AA%81%E7%99%BC%E7%94%9F">#</a> 分支合併 & 衝突發生</h1>
<p><img src="https://blog.errorbaker.tw/img/posts/sixwings/figue-2.png" alt="" /></p>
<p>首先需要先在本地端建立遠端主機連線,然後再將 template 上的 main/master 分支合併到本地端的 main/master 分支,然後會需要處理合併時發生的衝突問題。</p>
<p>新增遠端主機,使用 access token 連線請選擇第 1 個選項,SSH keys 連線請選擇第 2 個選項</p>
<ol>
<li><code>git remote set-url origin https://Lidemy:<access-token>@github.com/Lidemy/mentor-program-5th.git</code></li>
<li><code>git remote add template git@github.com:Lidemy/mentor-program-5th.git</code></li>
</ol>
<p>從遠端 template 抓資料下來<br />
<code>git fetch template</code></p>
<p>切換到主分支,根據自己的主分支命名選擇 main 或 master<br />
<code>git checkout master</code></p>
<p>把 template/master 分支的東西合併到 master 主分支,<br />
<code>git merge template/master</code></p>
<p>沒意外的話,這時候會看到錯誤訊息 <strong>fatal: refusing to merge unrelated histories</strong><br />
這是因為 git 從 2.9.0 開始預設不允許合併沒有共同祖先的分支,要解決這個問題需要加上 <code>--allow-unrelated-histories</code> 參數。<br />
<code>git merge template/master --allow-unrelated-histories</code></p>
<p>沒意外的話,這時候 git 會跳出 conflict 衝突警告,請你先解決好衝突再發一個 commit</p>
<pre class="language-txt"><code class="language-txt">error: Merging is not possible because you have unmerged files.<br />hint: Fix them up in the work tree, and then use 'git add/rm <file>'<br />hint: as appropriate to mark resolution and make a commit.<br />fatal: Exiting because of an unresolved conflict.</code></pre>
<h1 id="%E8%A7%A3%E6%B1%BA%E8%A1%9D%E7%AA%81"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/update-github-repo-from-template/#%E8%A7%A3%E6%B1%BA%E8%A1%9D%E7%AA%81">#</a> 解決衝突</h1>
<p>先確認一下衝突檔案有哪些?<br />
<code>git status</code></p>
<p><img src="https://blog.errorbaker.tw/img/posts/sixwings/conflict-files.png" alt="" /></p>
<h2 id="%E6%89%8B%E5%8B%95%E8%A7%A3%E8%A1%9D%E7%AA%81"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/update-github-repo-from-template/#%E6%89%8B%E5%8B%95%E8%A7%A3%E8%A1%9D%E7%AA%81">#</a> 手動解衝突</h2>
<p>衝突檔案打開會看到大概像這樣的東西</p>
<pre class="language-txt"><code class="language-txt"><<<<<<< HEAD<br />...<br />... # 這邊會是本地端 repo 自己目前的內容<br />...<br />=======<br />...<br />... # 這邊會是從課程 template 拉過來的內容<br />...<br />>>>>>>> template/master</code></pre>
<p>這時候解決衝突的辦法是選擇保留 HEAD 或是 template/master 的內容</p>
<p>衝突檔案大概會有兩種:</p>
<ol>
<li>老師的 <a href="http://readme.md/">README.md</a> 檔案</li>
<li>homework 資料夾底下的</li>
</ol>
<p>因為這次合併主要就是為了把老師更新的內容放到自己的 repo 上,所以當然就會是選擇保留 template/master 的內容。而作業檔案的部分則是選擇保留 HEAD 的部分。</p>
<p>記得後面要把下面的內容也一起刪掉唷</p>
<ul>
<li><code><<<<<<< HEAD</code></li>
<li><code>=======</code></li>
<li><code>>>>>>>> template/master</code></li>
</ul>
<p>再來可能會遇到很棘手的衝突檔 <strong>package.json</strong> <strong>package-lock.json</strong> 這兩個衝突檔基本上用手動修改的話會非常非常痛苦,所以後來想了一個比較簡便的方式,就是先把原本的檔案備份下來直接蓋過去。在正式專案這樣做應該是不好的,但目前也找不到一個比較好的解決方式。</p>
<p>有興趣可以參照這篇文章的說明:<a href="https://jishuin.proginn.com/p/763bfbd570e3">很多人上来就删除的package-lock.json,还有这么多你不知道的!-技术圈</a><br />
看起來大家對這兩個檔案也蠻頭痛的</p>
<h2 id="%E7%94%A8-sourcetree-%E8%A7%A3%E8%A1%9D%E7%AA%81"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/update-github-repo-from-template/#%E7%94%A8-sourcetree-%E8%A7%A3%E8%A1%9D%E7%AA%81">#</a> 用 SourceTree 解衝突</h2>
<p>這個解法需要安裝 <a href="https://www.sourcetreeapp.com/">Sourcetree</a> ,然後 Ctrl+O 打開本地端的 repo 做後續的操作。<br />
這時候應該會看到一些有驚嘆號的衝突檔案,解衝突的方式是在檔案上面點右鍵,然後選擇「解決衝突/使用 我的版本 解決衝突」</p>
<p><img src="https://blog.errorbaker.tw/img/posts/sixwings/sourcetree-resolve.png" alt="" /></p>
<p>目前找到的教學幾乎都是手動修改檔案解衝突,要不然就是像 SourceTree 這種 GUI 介面的軟體才有提供指定保留什麼版本的便利功能。</p>
<p>好不容易修改上面的衝突檔之後,再來就是把這些衝突檔 add 加入 commit<br />
這時候這個艱辛的 merge 就終於完成了。</p>
<h1 id="%E5%90%8C%E6%AD%A5%E6%9B%B4%E6%96%B0%EF%BC%8C%E5%90%88%E4%BD%B5%E5%88%B0%E5%85%B6%E4%BB%96%E7%9A%84%E4%BD%9C%E6%A5%AD%E5%88%86%E6%94%AF%E4%B8%8A"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/update-github-repo-from-template/#%E5%90%8C%E6%AD%A5%E6%9B%B4%E6%96%B0%EF%BC%8C%E5%90%88%E4%BD%B5%E5%88%B0%E5%85%B6%E4%BB%96%E7%9A%84%E4%BD%9C%E6%A5%AD%E5%88%86%E6%94%AF%E4%B8%8A">#</a> 同步更新,合併到其他的作業分支上</h1>
<p><img src="https://blog.errorbaker.tw/img/posts/sixwings/figue-3.png" alt="" /></p>
<p>完成以上動作後,本地端主分支就會是最新的版本,若這時候還有其他正在撰寫中的作業分支,請先一併更新,避免後面主分支和作業分支發生版本落差。</p>
<p>切換到作業分支<br />
<code>git checkout weekN</code></p>
<p>把 master 分支的內容合併到目前 (weekN) 分支<br />
<code>git merge master</code></p>
<p>其他份作業依此類推<br />
<code>git checkout weekXX</code><br />
<code>git merge master</code></p>
<p>最後,如果不需要這個遠端的話,就可以把它移除了<br />
<code>git remote rm template</code></p>
<h1 id="%E5%9B%9E%E9%A1%A7%E7%9F%A5%E8%AD%98%E9%BB%9E"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/update-github-repo-from-template/#%E5%9B%9E%E9%A1%A7%E7%9F%A5%E8%AD%98%E9%BB%9E">#</a> 回顧知識點</h1>
<ul>
<li>學習如何新增遠端主機並且拉取新版本合併</li>
<li>學習如何處理合併時發生的衝突</li>
</ul>
<p>我是 sixwings,追尋技術的程式人,我們下次見 :)</p>
初探 Google extension
2021-09-09T00:00:00Z
https://blog.errorbaker.tw/posts/umer/google-extension/
<!-- summary -->
<!-- 初探 Google extension,寫出簡單的小作品 -->
<!-- summary -->
<!-- more -->
<h2 id="%E4%BB%8B%E7%B4%B9-extension"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/google-extension/#%E4%BB%8B%E7%B4%B9-extension">#</a> 介紹 extension</h2>
<p>使用常見的網頁技術(HTML, JavaScript, CSS)所寫出的程式,運行在 Chrome 瀏覽器的沙盒執行環境中,透過 UI 介面以及 Extensions APIs 來客製化 Chrome 瀏覽器的使用體驗。</p>
<h2 id="extesion-%E7%9A%84%E5%9F%BA%E6%9C%AC%E7%B5%84%E6%88%90%E6%9C%83%E5%85%B7%E6%9C%89%E4%BB%A5%E4%B8%8B%E7%9A%84%E6%AA%94%E6%A1%88"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/google-extension/#extesion-%E7%9A%84%E5%9F%BA%E6%9C%AC%E7%B5%84%E6%88%90%E6%9C%83%E5%85%B7%E6%9C%89%E4%BB%A5%E4%B8%8B%E7%9A%84%E6%AA%94%E6%A1%88">#</a> extesion 的基本組成會具有以下的檔案</h2>
<ul>
<li>manifest.json</li>
<li>popup.js</li>
<li>background.js</li>
<li>content-script.js</li>
</ul>
<h2 id="%E5%AF%A6%E4%BD%9C%E4%B8%80%E5%80%8B%E7%B0%A1%E5%96%AE%E7%9A%84-extension"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/google-extension/#%E5%AF%A6%E4%BD%9C%E4%B8%80%E5%80%8B%E7%B0%A1%E5%96%AE%E7%9A%84-extension">#</a> 實作一個簡單的 extension</h2>
<p>最終的結果會有以下簡單功能,<br />
extension 會有一個 popup(彈出視窗),裡面可以輸入一段訊息,按下 submit 之後,訊息會傳送到圖片右邊的 extension 後台,再把這段訊息 prepend 到當前使用中的分頁。<br />
<img src="https://i.imgur.com/jOR2TZD.png" alt="" /></p>
<h3 id="manifest.json"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/google-extension/#manifest.json">#</a> manifest.json</h3>
<p>每個 extension 都需要有的文件,用來配置 extension 所使用到的檔案的依賴性(dependency)以及基本資訊。</p>
<ul>
<li>background: extension 的後台腳本,在 manifest V3 使用到瀏覽器的 service worker 來運作後台腳本。</li>
</ul>
<blockquote>
<p>a "service worker is a script that your browser runs in the background, separate from a web page, opening the door to features that don't need a web page or user interaction."</p>
</blockquote>
<ul>
<li>permissions: 在執行 extension 的時候(run time)申請 extension APIs 的權限,這次實作是要在點擊 extension 當下的分頁做操作,所以申請 "active tab" 權限。</li>
<li>action: 設定 extension UI 相關資料。</li>
<li>content_scripts: 網頁、content script 分別是獨立運行的,透過設定 <a href="https://developer.chrome.com/docs/extensions/mv3/match_patterns/">match patterns</a>就可以把 content script 放到網頁的 DOM 裡面執行,並且可以取得頁面資訊、對頁面的 DOM 做操作、回傳頁面資訊給 extension 等。</li>
</ul>
<pre class="language-c"><code class="language-c"><span class="token punctuation">{</span><br /> <span class="token string">"name"</span><span class="token operator">:</span> <span class="token string">"Getting Started Example"</span><span class="token punctuation">,</span><br /> <span class="token string">"description"</span><span class="token operator">:</span> <span class="token string">"Build an Extension!"</span><span class="token punctuation">,</span><br /> <span class="token string">"version"</span><span class="token operator">:</span> <span class="token string">"1.0"</span><span class="token punctuation">,</span><br /> <span class="token string">"manifest_version"</span><span class="token operator">:</span> <span class="token number">3</span><span class="token punctuation">,</span><br /> <span class="token string">"background"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token string">"service_worker"</span><span class="token operator">:</span> <span class="token string">"background.js"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token string">"permissions"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token string">"activeTab"</span><br /> <span class="token punctuation">]</span><span class="token punctuation">,</span><br /><br /> <span class="token string">"action"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token string">"default_popup"</span><span class="token operator">:</span> <span class="token string">"popup.html"</span><span class="token punctuation">,</span><br /> <span class="token string">"default_icon"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token string">"16"</span><span class="token operator">:</span> <span class="token string">"/images/get_started16.png"</span><span class="token punctuation">,</span><br /> <span class="token string">"32"</span><span class="token operator">:</span> <span class="token string">"/images/get_started32.png"</span><span class="token punctuation">,</span><br /> <span class="token string">"48"</span><span class="token operator">:</span> <span class="token string">"/images/get_started48.png"</span><span class="token punctuation">,</span><br /> <span class="token string">"128"</span><span class="token operator">:</span> <span class="token string">"/images/get_started128.png"</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token string">"icons"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token string">"16"</span><span class="token operator">:</span> <span class="token string">"/images/get_started16.png"</span><span class="token punctuation">,</span><br /> <span class="token string">"32"</span><span class="token operator">:</span> <span class="token string">"/images/get_started32.png"</span><span class="token punctuation">,</span><br /> <span class="token string">"48"</span><span class="token operator">:</span> <span class="token string">"/images/get_started48.png"</span><span class="token punctuation">,</span><br /> <span class="token string">"128"</span><span class="token operator">:</span> <span class="token string">"/images/get_started128.png"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token string">"content_scripts"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token string">"matches"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"<all_urls>"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token string">"js"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"content-script.js"</span><span class="token punctuation">]</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">]</span><br /><span class="token punctuation">}</span></code></pre>
<h3 id="popup"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/google-extension/#popup">#</a> popup</h3>
<p>popup、background、分頁彼此之間都是 single thread,為了要讓彼此可以傳遞訊息,需要使用<code>chrome.runtime</code> API 的 connect 方法。<br />
這次實作會在 popup.js 建立一個監聽器,監聽是否有 response 傳過來並用 callback 印出, 以及監聽 button 的 click 事件,點擊 button 之後傳送訊息 <code>這是 popup 發送的訊息: ${inputElement.value}</code> 給 port。<br />
(popup.js)</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> inputElement <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"input"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> buttonElement <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"button"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> port <span class="token operator">=</span> chrome<span class="token punctuation">.</span>runtime<span class="token punctuation">.</span><span class="token function">connect</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">"connection"</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br />port<span class="token punctuation">.</span>onMessage<span class="token punctuation">.</span><span class="token function">addListener</span><span class="token punctuation">(</span><span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">response</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>response<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br />buttonElement<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'click'</span><span class="token punctuation">,</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> port<span class="token punctuation">.</span><span class="token function">postMessage</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">msg</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">這是 popup 發送的訊息: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>inputElement<span class="token punctuation">.</span>value<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /></code></pre>
<p>popup 的介面跟一般網頁一樣透過 HTML、CSS 來做出。<br />
(popup.html)</p>
<pre class="language-html"><code class="language-html"><span class="token doctype"><span class="token punctuation"><!</span><span class="token doctype-tag">DOCTYPE</span> <span class="token name">html</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>html</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>head</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>popup.css<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">charset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>UTF-8<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>head</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>body</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>input</span> <span class="token attr-name">placeholder</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>filter this word<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>submit-btn<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Submit<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>popup.js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>body</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>html</span><span class="token punctuation">></span></span></code></pre>
<h3 id="background.js"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/google-extension/#background.js">#</a> background.js</h3>
<ul>
<li>extension 的後台腳本,生命週期是 extension 裡面最長的,在瀏覽器打開時運作,瀏覽器關閉時停止。</li>
<li>background.js 透過載入 HTML 時註冊一個 service worker 來運行。</li>
<li>service worker 運行在 single thread (單執行緒、單線程),常用來監聽事件,可以在 DevTools 查看 service worker。</li>
</ul>
<p>前面有提到 service worker 本身是 single thread,沒辦法直接影響網頁的 DOM,因此需要使用<code>chrome.runtime</code> API 來做訊息的傳遞(Message passing)。<br />
這次實作會在 background.js 建立一個監聽器,在 popup.js 和 background.js 建立連結之後,再繼續監聽是否有 response 傳過來,接收到 response 之後執行 callback,發送訊息 '這是 background 發送的訊息' 給 popup,以及發送訊息(response)給當前使用的分頁。</p>
<pre class="language-js"><code class="language-js">chrome<span class="token punctuation">.</span>runtime<span class="token punctuation">.</span>onConnect<span class="token punctuation">.</span><span class="token function">addListener</span><span class="token punctuation">(</span><span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">port</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>port<span class="token punctuation">.</span>name<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> port<span class="token punctuation">.</span>onMessage<span class="token punctuation">.</span><span class="token function">addListener</span><span class="token punctuation">(</span><span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">response</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>response<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> port<span class="token punctuation">.</span><span class="token function">postMessage</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">msg</span><span class="token operator">:</span> <span class="token string">'這是 background 發送的訊息'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /><br /> chrome<span class="token punctuation">.</span>tabs<span class="token punctuation">.</span><span class="token function">query</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">active</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> <span class="token literal-property property">currentWindow</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">tabs</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>tabs<span class="token punctuation">)</span><br /> chrome<span class="token punctuation">.</span>tabs<span class="token punctuation">.</span><span class="token function">sendMessage</span><span class="token punctuation">(</span>tabs<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>id<span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">data</span><span class="token operator">:</span> response <span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<h3 id="content-script.js"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/google-extension/#content-script.js">#</a> content-script.js</h3>
<p>會注入到頁面 DOM 執行的腳本</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> bodyElement <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'body'</span><span class="token punctuation">)</span><br /><br />chrome<span class="token punctuation">.</span>runtime<span class="token punctuation">.</span>onMessage<span class="token punctuation">.</span><span class="token function">addListener</span><span class="token punctuation">(</span>handleMessage<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token keyword">function</span> <span class="token function">handleMessage</span><span class="token punctuation">(</span><span class="token parameter">request</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>request<span class="token punctuation">.</span>data<span class="token punctuation">.</span>msg<span class="token punctuation">)</span><br /> bodyElement<span class="token punctuation">.</span><span class="token function">prepend</span><span class="token punctuation">(</span>request<span class="token punctuation">.</span>data<span class="token punctuation">.</span>msg<span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<h3 id="%E7%B5%90%E6%9E%9C%E4%BB%A5%E5%8F%8A%E6%9C%89%E8%B6%A3%E7%9A%84%E7%99%BC%E7%8F%BE"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/google-extension/#%E7%B5%90%E6%9E%9C%E4%BB%A5%E5%8F%8A%E6%9C%89%E8%B6%A3%E7%9A%84%E7%99%BC%E7%8F%BE">#</a> 結果以及有趣的發現</h3>
<p>最後會做出文章前面提到的功能,如圖片。<br />
<img src="https://i.imgur.com/B3UgQ1x.png" alt="" /></p>
<p>需要注意的是 extension 使用到的 popup、background、content-script 彼此之前如果要進行訊息的傳遞,需要先建立 connect(或是透過 one-time requests 達成),以及 content-script 需要先在 manifest 設定 match patterns 來把腳本放到頁面的 DOM 中執行。</p>
<p>這次實作寫出一個簡單的 google extension 之後才發現背後的原理一點也不簡單,特別是發現到原來瀏覽器有 service worker 的存在,也算是稍微回答了一些我在實作這次 extension 的時候遇到的以下狀況(圖片顯示 extension 後台):<br />
<img src="https://i.imgur.com/r3D1fGw.png" alt="" /></p>
<p>在接收到訊息之後,background.js 印出的訊息會是亂碼,popup.js 印出的訊息卻是正常的編碼結果(前提:確定 background 跟 popup 這兩個檔案程式碼都跟上面一樣)。</p>
<p>明明都是在瀏覽器裡面跑為什麼結果會不一樣?之後才發現到原來在載入 popup 的 HTML 之後會註冊一個 service worker,兩者都是 single thread,但是我沒有在 popup 的 HTML 加入<code><meta charset="UTF-8"></code>這串文字所導致亂碼結果。</p>
進階版 To do list
2021-09-12T00:00:00Z
https://blog.errorbaker.tw/posts/xiang/advanced-todo-list/
<!-- summary -->
<!-- # 你想過自己改造出新的進階版 To do list 嗎? -->
<!-- summary -->
<!-- more -->
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/advanced-todo-list/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<p>To do list 一直都是大家剛開始熟悉一項新技術時,會用來練習的主題。擁有基本的 CRUD 四大要素,讓我們對於新的工具該如何去操作資料,有最直接的體悟。</p>
<p>但是 To do list 真的只能用來練習這些東西而已嗎?我能不能再將功能變得更加完善呢?剛好內心曾萌生出這樣子的想法,所以想來嘗試看看,製作出新的進階版 To do list。</p>
<p>此篇文章只會拋出一個概念,跟提到一些規劃的思考方向,不會示範完整的程式碼,所以大家也可以把這篇作為一個練習題目,自己實作出這些進階的功能出來。</p>
<h2 id="%E7%9B%AE%E6%A8%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/advanced-todo-list/#%E7%9B%AE%E6%A8%99">#</a> 目標</h2>
<p>一般我們常看到的 To do list 它是一行行的新增下去,每一項事項它可以選擇完成或未完成,或者去編輯裡面的內容,但是不同的事項之間不會有任何的關聯。</p>
<p>生活中有很多的代辦事項,常常是一組一組的,比如說工作代辦事項、採買代辦事項、甚至是閱讀清單、學習清單...等等,他們會有所謂類別的概念。在原有的 To do list 我們沒辦法把它歸類。</p>
<p>所以我想來嘗試做一個能加入群組概念的 To do list,這樣工作相關的 to do,就會在工作群組底下,採買相關的 to do 就會建立在採買的群組底下,能方便知道各個群組剩下哪些事項,也更符合日常生活中面對到的情況。</p>
<p>所以新的 To do list 的規格如下:</p>
<ul>
<li>第一,新增 To do 時要能選擇是要 <code>新增一個 To do</code>,或者 <code>新增一個群組</code></li>
<li>第二,每個群組我要能夠給它一個名稱</li>
<li>第三,要滿足基本的 To do list 的需求( 要能編輯內容、選擇完成 / 未完成、刪除 to do )</li>
</ul>
<p>備註:此次的 To do list 主要是想實現群組的概念,所以先不把 Filter 放入功能當中,如果有想實作的朋友,也可以將這項功能給完善化。</p>
<h2 id="user-flow"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/advanced-todo-list/#user-flow">#</a> User flow</h2>
<p>直接用 wireframe 來說明大家會更好明白,所以直接上圖!</p>
<ul>
<li>
<p>一開始未設定 To do 時的起始頁面,可以把它當作是一個 Home page,未來優化的方向會希望這邊會依照日期來呈現每天的 To do。點選個別的日期之後可以看到當天的所有 To do list(但這個功能不列入此次的範疇),當點選了 <code>新增 To do</code> 按鈕以後,會進到 To do list 的編輯頁<br />
<img src="https://blog.errorbaker.tw/img/posts/xiang/todo-homepage.png" alt="" /></p>
</li>
<li>
<p>To do list 的編輯頁,畫面上會預設會先給一個 To do,左側會是輸入框,可以自由編輯 To do 的內容,右側會是下拉式選單,可以選取該項 To do 當天是否有完成。最右邊垃圾桶可以將該筆 To do 刪除。然後 To do 的下方會有兩個按鈕 <code>新增 To do</code> 跟 <code>新增群組</code>,顧名思義我們可以選擇要在底下加入一個新的 To do 或者加入一個新的群組<br />
<img src="https://blog.errorbaker.tw/img/posts/xiang/todo-01.png" alt="" /></p>
</li>
<li>
<p>當我們加入一個新的群組,就會看到底下的畫面,新的群組上方會有新的群組名稱,群組底下也會有一個 <code>新增 To do</code> 按鈕,讓可以在此群組底下繼續新增 To do<br />
<img src="https://blog.errorbaker.tw/img/posts/xiang/todo-02.png" alt="" /></p>
</li>
<li>
<p>群組底下的 To do 就可以一直長下去<br />
<img src="https://blog.errorbaker.tw/img/posts/xiang/todo-03.png" alt="" /></p>
</li>
<li>
<p>如果有兩個不同的群組,就可以各別在底下新增多個 To do<br />
<img src="https://blog.errorbaker.tw/img/posts/xiang/todo-04.png" alt="" /></p>
</li>
</ul>
<p>所以這次要達成的功能,就像上面提到的那樣,有興趣直接挑戰的朋友,可以直接把它當作一份題目,自己用來練習,或者再把這些功能延伸的更完整,改造成自己的一份 side project。</p>
<p>從這邊開始,底下就會針對這一份需求,來做基本的資料規劃,還有實作方向的討論。</p>
<h2 id="%E8%B3%87%E6%96%99%E8%A6%8F%E5%8A%83"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/advanced-todo-list/#%E8%B3%87%E6%96%99%E8%A6%8F%E5%8A%83">#</a> 資料規劃</h2>
<p>先回憶一下一般的 To do list 資料規劃方式,我們會用一個陣列包著多個 To do 物件,像下面這樣:<br />
當新增 to do 時,就會在陣列底下多一個物件,刪除 to do 時,就會將對應的物件移除。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> todos <span class="token operator">=</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">"todo01"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">isDone</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token number">2</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">"todo02"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">isDone</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /><span class="token punctuation">]</span><span class="token punctuation">;</span></code></pre>
<p>但我們現在多了群組的概念,資料內容勢必會再複雜一些。<br />
至於會需要新增哪些參數進到我們的資料結構,可以先從能想到的一個一個加進去。</p>
<p>最先想到的呢,就是界定好使用者要新增的會是一個 to do,還是一個群組,我們可以用一個簡單的 <code>isGroup</code> 變數來代表,</p>
<p>一旦我們能區分它是一個 to do 還是一個群組了,就可以來分別將群組需要的資料欄位加上去,像是群組名稱、群組 id、以及群組內容:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// 一個 to do 的資料結構</span><br /><br /><span class="token punctuation">{</span><br /> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">"todo01"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">isDone</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><br /><br /><br /><span class="token comment">// 一個群組的資料結構</span><br /><br /><span class="token punctuation">{</span><br /> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token number">2</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">isGroup</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'第一個群組'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">data</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><br /><span class="token punctuation">}</span><br /></code></pre>
<p>群組底下的功能,就會跟一般 to do list 的功能一模一樣,所以群組底下的 <code>data</code> 資料格式跟一般的 to do list 相同就可以囉</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// 一個群組的資料結構</span><br /><br /><span class="token punctuation">{</span><br /> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token number">2</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">isGroup</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'第一個群組'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">data</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">"todo01"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">isDone</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">"todo02"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">isDone</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">]</span><br /><span class="token punctuation">}</span><br /></code></pre>
<p>這邊我把 data 底下 to do 資料的 id 拿掉了,因為我覺得可以透過外面群組的 id 加上 data 的 index 來取得各個 to do,所以就算我不定義每個 to do 的 id,還是可以準確的取得我要的那筆資料。</p>
<p>所以一個完整的資料格式就會像下面這樣:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> todos <span class="token operator">=</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">"todo-01"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">isDone</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token number">2</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">isGroup</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">"第一個群組"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">data</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">"group-todo-01"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">isDone</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">"group-todo-02"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">isDone</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token number">3</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">isGroup</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">"第二個群組"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">data</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">"group-todo-03"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">isDone</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">"group-todo-04"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">isDone</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /><span class="token punctuation">]</span><span class="token punctuation">;</span></code></pre>
<h2 id="%E7%95%AB%E9%9D%A2%E6%B8%B2%E6%9F%93"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/advanced-todo-list/#%E7%95%AB%E9%9D%A2%E6%B8%B2%E6%9F%93">#</a> 畫面渲染</h2>
<p>資料結構出來了以後,接下來的重點就是如何把資料渲染到畫面上,以及如何達成我們要的操作功能:</p>
<p>渲染的動作,應該會跟一般 to do list 概念類似,遍歷整個 <code>todos</code> 陣列,利用 map 的方式回傳 HTML 的 template。<br />
不過由於現在多了群組的資料格式,所以 HTML 的 template 也會變成兩種:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// 單一個 to do</span><br /><span class="token operator"><</span>todo<br /> id<span class="token operator">=</span><span class="token punctuation">{</span>id<span class="token punctuation">}</span><br /> value<span class="token operator">=</span><span class="token punctuation">{</span>value<span class="token punctuation">}</span><br /><span class="token operator">></span><span class="token operator"><</span><span class="token operator">/</span>todo<span class="token operator">></span><br /><br /><span class="token comment">// 單一個群組</span><br /><span class="token operator"><</span>todo<span class="token operator">-</span>group<br /> id<span class="token operator">=</span><span class="token punctuation">{</span>id<span class="token punctuation">}</span><br /> name<span class="token operator">=</span><span class="token punctuation">{</span>name<span class="token punctuation">}</span><br /> data<span class="token operator">=</span><span class="token punctuation">{</span>data<span class="token punctuation">}</span><br /><span class="token operator">></span><span class="token operator"><</span><span class="token operator">/</span>todo<span class="token operator">-</span>group<span class="token operator">></span></code></pre>
<pre class="language-js"><code class="language-js">todos<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">item</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>item<span class="token punctuation">.</span>isGroup<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token operator"><</span>todo id<span class="token operator">=</span><span class="token punctuation">{</span>item<span class="token punctuation">.</span>id<span class="token punctuation">}</span> value<span class="token operator">=</span><span class="token punctuation">{</span>item<span class="token punctuation">.</span>value<span class="token punctuation">}</span><span class="token operator">></span><span class="token operator"><</span><span class="token operator">/</span>todo<span class="token operator">></span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span>todo<span class="token operator">-</span>group id<span class="token operator">=</span><span class="token punctuation">{</span>item<span class="token punctuation">.</span>id<span class="token punctuation">}</span> name<span class="token operator">=</span><span class="token punctuation">{</span>item<span class="token punctuation">.</span>name<span class="token punctuation">}</span> data<span class="token operator">=</span><span class="token punctuation">{</span>item<span class="token punctuation">.</span>data<span class="token punctuation">}</span><span class="token operator">></span><span class="token operator"><</span><span class="token operator">/</span>todo<span class="token operator">-</span>group<span class="token operator">></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>由於渲染的部分跟一般的 to do list 差異不大,就是分成兩種不同情況的 template,剩下的就是一些切版的功夫,所以容我快速帶過,直接進入操作功能的實作吧。</p>
<h2 id="%E6%93%8D%E4%BD%9C%E5%8A%9F%E8%83%BD"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/advanced-todo-list/#%E6%93%8D%E4%BD%9C%E5%8A%9F%E8%83%BD">#</a> 操作功能</h2>
<p>重點操作項目如下:</p>
<ul>
<li>新增 to do</li>
<li>新增群組</li>
<li>群組下新增 to do</li>
<li>刪除 to do</li>
<li>群組下刪除 to do</li>
</ul>
<p>☞ 新增 to do 跟新增群組,雖然分成不同的按鈕,但是我們僅需要定義一個 function 來達成這兩個功能:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">handleCreate</span><span class="token punctuation">(</span><span class="token parameter">isGroup</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token function">setTodos</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>isGroup<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token punctuation">[</span><br /> <span class="token operator">...</span>todos<span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">id</span><span class="token operator">:</span> id<span class="token operator">++</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">""</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">isDone</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">]</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> <span class="token punctuation">[</span><br /> <span class="token operator">...</span>todos<span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">id</span><span class="token operator">:</span> id<span class="token operator">++</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">isGroup</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">""</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">data</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">""</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">isDone</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">]</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>☞ 群組下新增 to do,我們需要接到群組的 id,並且更新資料:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">handleCreateGroupTodo</span><span class="token punctuation">(</span><span class="token parameter">id</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token function">setTodos</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> todos<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">item</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>item<span class="token punctuation">.</span>id <span class="token operator">!==</span> id<span class="token punctuation">)</span> <span class="token keyword">return</span> item<span class="token punctuation">;</span><br /> <span class="token keyword">return</span> <span class="token punctuation">{</span><br /> <span class="token operator">...</span>item<span class="token punctuation">,</span><br /> <span class="token literal-property property">data</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token operator">...</span>item<span class="token punctuation">.</span>data<span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">""</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">isDone</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>☞ 刪除 to do 跟群組下刪除 to do 我選擇一起寫,它會比較複雜一點,需要先判斷現在要刪除的 to do 它是不是在群組底下,如果是,需要再判斷這個群組底下的 data 是否為空,如果是的話要直接把群組刪除:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">handleDelete</span><span class="token punctuation">(</span><span class="token parameter">id<span class="token punctuation">,</span> index</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token function">setTodos</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token comment">// 如果刪除的不是群組下的 to do,index 為 undefined</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>index <span class="token operator">===</span> <span class="token keyword">undefined</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> todos<span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">item</span><span class="token punctuation">)</span> <span class="token operator">=></span> item<span class="token punctuation">.</span>id <span class="token operator">!==</span> id<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token comment">// 如果刪除的是群組下的 to do,先利用 id 找到該群組,再去將底下對應 index 的 to do 移除</span><br /> <span class="token keyword">return</span> todos<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">item</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>item<span class="token punctuation">.</span>id <span class="token operator">!==</span> id<span class="token punctuation">)</span> <span class="token keyword">return</span> item<span class="token punctuation">;</span><br /> <span class="token keyword">return</span> <span class="token punctuation">{</span><br /> <span class="token operator">...</span>item<span class="token punctuation">,</span><br /> <span class="token literal-property property">data</span><span class="token operator">:</span> item<span class="token punctuation">.</span>data<span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">_<span class="token punctuation">,</span> i</span><span class="token punctuation">)</span> <span class="token operator">=></span> i <span class="token operator">!==</span> index<span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>如此一來新增、刪除都有了,編輯的話直接利用 input 的方式來處理,所以可以不用特別寫一個 function。<br />
所以 CRUD 的四項功能就都完成啦!</p>
<h2 id="%E7%B5%90%E8%AA%9E"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/advanced-todo-list/#%E7%B5%90%E8%AA%9E">#</a> 結語</h2>
<p>這次的進階版 To do list,我個人認為非常適合在第一次學習框架的時候作為一項 <code>挑戰題</code>。它除了基本的功能以外,資料更為複雜,功能也需要考慮到更多細節,而且更趨近於工作上會遇到的情形。</p>
<p>工作上往往是以前開發了某項專案,未來會針對這項專案開發更複雜的功能。所以這次的練習,也可以把它當作是將原先寫好的 To do list 嘗試做一次改造,讓大家體會將自己的舊專案翻新的感覺。</p>
原始碼探索
2021-09-18T00:00:00Z
https://blog.errorbaker.tw/posts/cwc329/dig-in-source-code/
<!-- summary -->
<p>在第一次用 sequelize 的時候,除了被開發的方便性震驚,同時也讓我更注意看 app 運作時的 logs。這也讓我想要去看看它的原始碼是怎麼產生與我要求的物件格式相符之資料。</p>
<!-- summary -->
<h1 id="sequelize-%E8%88%87%E6%88%91%E7%9A%84%E5%A5%BD%E5%A5%87"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/dig-in-source-code/#sequelize-%E8%88%87%E6%88%91%E7%9A%84%E5%A5%BD%E5%A5%87">#</a> Sequelize 與我的好奇</h1>
<p>Sequelize 是一個 Node.js 的 ORM 套件,支援 MySQL, Postgres, MariaDB, SQLite 與 Microsoft SQL Server。這是一套方便開發的套件,使用 ORM 除了開發上方便可以不需要再寫 SQL 之外,依照套件的支援程度也可以無痛地轉移到各種 database engine。</p>
<p>初學 Sequelize 時,最令我驚豔的是 eager loading 的功能,只要我先將每個 model 之間的關係定義好,然後就可以使用物件的寫法,傳入我想要得到的資料結構,而且他可以做到 nested,也就是我可以寫很多層的關聯,Sequelize 就可以依照這些層層疊疊的關係把我想要的資料撈出來。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> me <span class="token operator">=</span> <span class="token keyword">await</span> Users<span class="token punctuation">.</span><span class="token function">findOne</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token literal-property property">where</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">username</span><span class="token operator">:</span> req<span class="token punctuation">.</span>jwtData<span class="token punctuation">.</span>username<span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">attributes</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"username"</span><span class="token punctuation">,</span> <span class="token string">"email"</span><span class="token punctuation">,</span> <span class="token string">"isAdmin"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">include</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">model</span><span class="token operator">:</span> Podcasts<span class="token punctuation">,</span><br /> <span class="token keyword">as</span><span class="token operator">:</span> <span class="token string">"subscriptions"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">attributes</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"id"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">through</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">attributes</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">model</span><span class="token operator">:</span> Playlists<span class="token punctuation">,</span><br /> <span class="token keyword">as</span><span class="token operator">:</span> <span class="token string">"playlists"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">attributes</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"id"</span><span class="token punctuation">,</span> <span class="token string">"name"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">include</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">model</span><span class="token operator">:</span> Episodes<span class="token punctuation">,</span><br /> <span class="token literal-property property">attributes</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"id"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">through</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">attributes</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">model</span><span class="token operator">:</span> Records<span class="token punctuation">,</span><br /> <span class="token keyword">as</span><span class="token operator">:</span> <span class="token string">"playedRecords"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">attributes</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"episodeId"</span><span class="token punctuation">,</span> <span class="token string">"progress"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">order</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token punctuation">{</span> <span class="token literal-property property">model</span><span class="token operator">:</span> Records<span class="token punctuation">,</span> <span class="token keyword">as</span><span class="token operator">:</span> <span class="token string">"playedRecords"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token string">"updatedAt"</span><span class="token punctuation">,</span> <span class="token string">"DESC"</span><span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>這個時候我就很好奇 Sequelize 到底是怎麼做到的,我曾經使用 app log 中它使用的 SQL 直接執行,不過結果並不是巢狀結構,而是個一維的 array of objects。這就讓我好奇它到底是怎麼將這些東西 parse 出如此複雜的結構。於是乎我就去找了原始碼來看。想說把我 trace code 的過程記錄下來,雖然有點流水帳,不過也算是一次不錯的經驗。</p>
<h2 id="%E4%BD%BF%E7%94%A8%E5%B7%A5%E5%85%B7%E6%8A%80%E5%B7%A7%E8%88%87%E5%85%88%E5%82%99%E7%9F%A5%E8%AD%98"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/dig-in-source-code/#%E4%BD%BF%E7%94%A8%E5%B7%A5%E5%85%B7%E6%8A%80%E5%B7%A7%E8%88%87%E5%85%88%E5%82%99%E7%9F%A5%E8%AD%98">#</a> 使用工具技巧與先備知識</h2>
<p>基本上我就是從 github 將 Sequelize 的原始碼 git clone,然後使用我的 VS Code 編輯器看,平常工作時我也習慣使用 VS Code 去 trace code。在這邊我稍微介紹一下在 trace code 時好用的兩個快捷鍵與一個功能。快捷鍵分別是 <code>F12</code> 與 上一頁 (<code>Ctrl</code> + <code>-</code>)。前者可以直接帶你到這個變數被定義的地方,而後者會直接帶回到上一頁,游標上一個位置,基本上要看 code 這兩個鍵就足夠了。而功能就是全域搜尋,VS Code 支援在你所在的 Workspace 痊癒搜尋你所想要找的關鍵字。知道這三個技巧,就可以來看 code。</p>
<p>除了工具,這篇文章為了節省一些篇幅,預設讀者知道最基本的物件導向知識,都準備好就可以開始了。</p>
<h2 id="%E7%AC%AC%E4%B8%80%E6%AC%A1%E6%89%BE%EF%BC%8C%E5%BE%88%E9%9A%A8%E4%BE%BF"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/dig-in-source-code/#%E7%AC%AC%E4%B8%80%E6%AC%A1%E6%89%BE%EF%BC%8C%E5%BE%88%E9%9A%A8%E4%BE%BF">#</a> 第一次找,很隨便</h2>
<p>根據上面的 code 我想要找的是 <code>findOne</code> 這個 function 到底做了什麼,<br />
一開始我很直覺地全域搜尋 <code>findOne</code>,在眾多結果中,我找到一行 <code>static async findOne</code> 開頭的程式碼,我想這應該就是我要找的東西了。</p>
<pre class="language-js"><code class="language-js"> <span class="token keyword">static</span> <span class="token keyword">async</span> <span class="token function">findOne</span><span class="token punctuation">(</span><span class="token parameter">options</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>options <span class="token operator">!==</span> <span class="token keyword">undefined</span> <span class="token operator">&&</span> <span class="token operator">!</span>_<span class="token punctuation">.</span><span class="token function">isPlainObject</span><span class="token punctuation">(</span>options<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">'The argument passed to findOne must be an options object, use findByPk if you wish to pass a single primary key value'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> options <span class="token operator">=</span> Utils<span class="token punctuation">.</span><span class="token function">cloneDeep</span><span class="token punctuation">(</span>options<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>options<span class="token punctuation">.</span>limit <span class="token operator">===</span> <span class="token keyword">undefined</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> uniqueSingleColumns <span class="token operator">=</span> _<span class="token punctuation">.</span><span class="token function">chain</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>uniqueKeys<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">values</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span><span class="token parameter">c</span> <span class="token operator">=></span> c<span class="token punctuation">.</span>fields<span class="token punctuation">.</span>length <span class="token operator">===</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token string">'column'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">value</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token comment">// Don't add limit if querying directly on the pk or a unique column</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>options<span class="token punctuation">.</span>where <span class="token operator">||</span> <span class="token operator">!</span>_<span class="token punctuation">.</span><span class="token function">some</span><span class="token punctuation">(</span>options<span class="token punctuation">.</span>where<span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">value<span class="token punctuation">,</span> key</span><span class="token punctuation">)</span> <span class="token operator">=></span><br /> <span class="token punctuation">(</span>key <span class="token operator">===</span> <span class="token keyword">this</span><span class="token punctuation">.</span>primaryKeyAttribute <span class="token operator">||</span> uniqueSingleColumns<span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span>key<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">&&</span><br /> <span class="token punctuation">(</span>Utils<span class="token punctuation">.</span><span class="token function">isPrimitive</span><span class="token punctuation">(</span>value<span class="token punctuation">)</span> <span class="token operator">||</span> Buffer<span class="token punctuation">.</span><span class="token function">isBuffer</span><span class="token punctuation">(</span>value<span class="token punctuation">)</span><span class="token punctuation">)</span><br /> <span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> options<span class="token punctuation">.</span>limit <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token comment">// Bypass a possible overloaded findAll.</span><br /> <span class="token keyword">return</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">findAll</span><span class="token punctuation">(</span>_<span class="token punctuation">.</span><span class="token function">defaults</span><span class="token punctuation">(</span>options<span class="token punctuation">,</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">plain</span><span class="token operator">:</span> <span class="token boolean">true</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span></code></pre>
<p>看著這段程式碼,如果我只專注在我想要找的東西,也就是如何 parse 資料,其實是一段滿好看懂的程式碼。首先是這些變數與函式的命名非常的簡潔明瞭,還有就是適時出現的註解,非常有效的減少我去理解這段 code 的用意。如果以空白行區分可以將這段 code 分成三份,第一份主要是在 function 一開始先做錯誤處理,不讓有不符規範的 option 執行。第二段則是去處理如果沒有 limit 選項時的狀況。而第三段是我在這邊的重點,也就是其實 <code>findOne</code> 是使用預先處理過的 option 去執行 <code>findAll</code>。所以我應該要再去找 <code>findAll</code>,把游標移到 <code>this.findAll</code> 然後按下 F12 就會帶我過去了。</p>
<pre class="language-js"><code class="language-js"> <span class="token keyword">static</span> <span class="token keyword">async</span> <span class="token function">findAll</span><span class="token punctuation">(</span><span class="token parameter">options</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>options <span class="token operator">!==</span> <span class="token keyword">undefined</span> <span class="token operator">&&</span> <span class="token operator">!</span>_<span class="token punctuation">.</span><span class="token function">isPlainObject</span><span class="token punctuation">(</span>options<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">sequelizeErrors<span class="token punctuation">.</span>QueryError</span><span class="token punctuation">(</span><span class="token string">'The argument passed to findAll must be an options object, use findByPk if you wish to pass a single primary key value'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>options <span class="token operator">!==</span> <span class="token keyword">undefined</span> <span class="token operator">&&</span> options<span class="token punctuation">.</span>attributes<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>Array<span class="token punctuation">.</span><span class="token function">isArray</span><span class="token punctuation">(</span>options<span class="token punctuation">.</span>attributes<span class="token punctuation">)</span> <span class="token operator">&&</span> <span class="token operator">!</span>_<span class="token punctuation">.</span><span class="token function">isPlainObject</span><span class="token punctuation">(</span>options<span class="token punctuation">.</span>attributes<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">sequelizeErrors<span class="token punctuation">.</span>QueryError</span><span class="token punctuation">(</span><span class="token string">'The attributes option must be an array of column names or an object'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">warnOnInvalidOptions</span><span class="token punctuation">(</span>options<span class="token punctuation">,</span> Object<span class="token punctuation">.</span><span class="token function">keys</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>rawAttributes<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">const</span> tableNames <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br /> tableNames<span class="token punctuation">[</span><span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getTableName</span><span class="token punctuation">(</span>options<span class="token punctuation">)</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span><br /> options <span class="token operator">=</span> Utils<span class="token punctuation">.</span><span class="token function">cloneDeep</span><span class="token punctuation">(</span>options<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> _<span class="token punctuation">.</span><span class="token function">defaults</span><span class="token punctuation">(</span>options<span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">hooks</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token comment">// set rejectOnEmpty option, defaults to model options</span><br /> options<span class="token punctuation">.</span>rejectOnEmpty <span class="token operator">=</span> <span class="token class-name">Object</span><span class="token punctuation">.</span>prototype<span class="token punctuation">.</span><span class="token function">hasOwnProperty</span><span class="token punctuation">.</span><span class="token function">call</span><span class="token punctuation">(</span>options<span class="token punctuation">,</span> <span class="token string">'rejectOnEmpty'</span><span class="token punctuation">)</span><br /> <span class="token operator">?</span> options<span class="token punctuation">.</span>rejectOnEmpty<br /> <span class="token operator">:</span> <span class="token keyword">this</span><span class="token punctuation">.</span>options<span class="token punctuation">.</span>rejectOnEmpty<span class="token punctuation">;</span><br /><br /> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">_injectScope</span><span class="token punctuation">(</span>options<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>options<span class="token punctuation">.</span>hooks<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">runHooks</span><span class="token punctuation">(</span><span class="token string">'beforeFind'</span><span class="token punctuation">,</span> options<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">_conformIncludes</span><span class="token punctuation">(</span>options<span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">_expandAttributes</span><span class="token punctuation">(</span>options<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">_expandIncludeAll</span><span class="token punctuation">(</span>options<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>options<span class="token punctuation">.</span>hooks<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">runHooks</span><span class="token punctuation">(</span><span class="token string">'beforeFindAfterExpandIncludeAll'</span><span class="token punctuation">,</span> options<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> options<span class="token punctuation">.</span>originalAttributes <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">_injectDependentVirtualAttributes</span><span class="token punctuation">(</span>options<span class="token punctuation">.</span>attributes<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>options<span class="token punctuation">.</span>include<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> options<span class="token punctuation">.</span>hasJoin <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">_validateIncludedElements</span><span class="token punctuation">(</span>options<span class="token punctuation">,</span> tableNames<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token comment">// If we're not raw, we have to make sure we include the primary key for de-duplication</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><br /> options<span class="token punctuation">.</span>attributes<br /> <span class="token operator">&&</span> <span class="token operator">!</span>options<span class="token punctuation">.</span>raw<br /> <span class="token operator">&&</span> <span class="token keyword">this</span><span class="token punctuation">.</span>primaryKeyAttribute<br /> <span class="token operator">&&</span> <span class="token operator">!</span>options<span class="token punctuation">.</span>attributes<span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>primaryKeyAttribute<span class="token punctuation">)</span><br /> <span class="token operator">&&</span> <span class="token punctuation">(</span><span class="token operator">!</span>options<span class="token punctuation">.</span>group <span class="token operator">||</span> <span class="token operator">!</span>options<span class="token punctuation">.</span>hasSingleAssociation <span class="token operator">||</span> options<span class="token punctuation">.</span>hasMultiAssociation<span class="token punctuation">)</span><br /> <span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> options<span class="token punctuation">.</span>attributes <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token keyword">this</span><span class="token punctuation">.</span>primaryKeyAttribute<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">concat</span><span class="token punctuation">(</span>options<span class="token punctuation">.</span>attributes<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>options<span class="token punctuation">.</span>attributes<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> options<span class="token punctuation">.</span>attributes <span class="token operator">=</span> Object<span class="token punctuation">.</span><span class="token function">keys</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>rawAttributes<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> options<span class="token punctuation">.</span>originalAttributes <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">_injectDependentVirtualAttributes</span><span class="token punctuation">(</span>options<span class="token punctuation">.</span>attributes<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token comment">// whereCollection is used for non-primary key updates</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>options<span class="token punctuation">.</span>whereCollection <span class="token operator">=</span> options<span class="token punctuation">.</span>where <span class="token operator">||</span> <span class="token keyword">null</span><span class="token punctuation">;</span><br /><br /> Utils<span class="token punctuation">.</span><span class="token function">mapFinderOptions</span><span class="token punctuation">(</span>options<span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> options <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">_paranoidClause</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">,</span> options<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>options<span class="token punctuation">.</span>hooks<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">runHooks</span><span class="token punctuation">(</span><span class="token string">'beforeFindAfterOptions'</span><span class="token punctuation">,</span> options<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">const</span> selectOptions <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token operator">...</span>options<span class="token punctuation">,</span> <span class="token literal-property property">tableNames</span><span class="token operator">:</span> Object<span class="token punctuation">.</span><span class="token function">keys</span><span class="token punctuation">(</span>tableNames<span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> results <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>queryInterface<span class="token punctuation">.</span><span class="token function">select</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getTableName</span><span class="token punctuation">(</span>selectOptions<span class="token punctuation">)</span><span class="token punctuation">,</span> selectOptions<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>options<span class="token punctuation">.</span>hooks<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">runHooks</span><span class="token punctuation">(</span><span class="token string">'afterFind'</span><span class="token punctuation">,</span> results<span class="token punctuation">,</span> options<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token comment">//rejectOnEmpty mode</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>_<span class="token punctuation">.</span><span class="token function">isEmpty</span><span class="token punctuation">(</span>results<span class="token punctuation">)</span> <span class="token operator">&&</span> options<span class="token punctuation">.</span>rejectOnEmpty<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">typeof</span> options<span class="token punctuation">.</span>rejectOnEmpty <span class="token operator">===</span> <span class="token string">'function'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">options<span class="token punctuation">.</span>rejectOnEmpty</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">typeof</span> options<span class="token punctuation">.</span>rejectOnEmpty <span class="token operator">===</span> <span class="token string">'object'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">throw</span> options<span class="token punctuation">.</span>rejectOnEmpty<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">sequelizeErrors<span class="token punctuation">.</span>EmptyResultError</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">return</span> <span class="token keyword">await</span> Model<span class="token punctuation">.</span><span class="token function">_findSeparate</span><span class="token punctuation">(</span>results<span class="token punctuation">,</span> options<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span></code></pre>
<p><code>findAll</code> 是個更複雜的 function,洋洋灑灑將近 100 行,不過有些地方不是我關心的,像是在真的下 SQL 前所要跑的 hooks 與對 option 做預處理都不是這次的重點,第一個重點出現在</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> results <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>queryInterface<span class="token punctuation">.</span><span class="token function">select</span><span class="token punctuation">(</span><br /> <span class="token keyword">this</span><span class="token punctuation">,</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getTableName</span><span class="token punctuation">(</span>selectOptions<span class="token punctuation">)</span><span class="token punctuation">,</span><br /> selectOptions<br /><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>這邊是他真的去拿資料的 function,我去看一下 parse 是不是在這裏面做的。不過這邊想要直接 F12 過去就沒這個簡單了,因為 <code>this.queryInterface.select</code> 的 <code>select</code> 找不到 definition,這個時候就要先從 <code>queryInterface</code> 下手。這邊可以看出這個 <code>queryInterface</code> 是這個物件的一個 attribute,在這邊 F12 就可以看到是怎麼產生的了。</p>
<pre class="language-js"><code class="language-js"> <span class="token keyword">static</span> <span class="token keyword">get</span> <span class="token function">queryInterface</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>sequelize<span class="token punctuation">.</span><span class="token function">getQueryInterface</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span></code></pre>
<p>看起來這是從同個物件的 <code>sequelize</code> 這個 attribute 得到的,那這個 <code>sequelize</code> 又是怎麼來的?</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">static</span> <span class="token function">init</span><span class="token punctuation">(</span><span class="token parameter">attributes<span class="token punctuation">,</span> options <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>options<span class="token punctuation">.</span>sequelize<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">'No Sequelize instance passed'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>sequelize <span class="token operator">=</span> options<span class="token punctuation">.</span>sequelize<span class="token punctuation">;</span><br /> <span class="token comment">// a lot of code</span><br /><span class="token punctuation">}</span></code></pre>
<p>是這個 static method 的 option 決定的,那從上方 js doc 可以看到 <code>option.sequelize</code> 是什麼。</p>
<pre class="language-js"><code class="language-js"><span class="token comment">/**<br /> * @param {object} options These options are merged with the default define options provided to the Sequelize constructor<br /> * @param {object} options.sequelize Define the sequelize instance to attach to the new Model. Throw error if none is provided.<br /> */</span></code></pre>
<p>所以這個會是一個 sequelize instance,那要去找 sequelize instance 到底是什麼。所謂 instance 就是把 class 具體化之後的實體,也就是說當 new 一個 class 時就會產生一個 instance,那在這邊我要找的其實就是 class sequelize。先全域搜尋碰碰運氣,還真的讓我找到了 sequelize.js 這個檔案定義了 class Sequelize。在這個檔案我搜尋 <code>getQueryInterface</code> 發現了這個 method。</p>
<pre class="language-js"><code class="language-js"> <span class="token function">getQueryInterface</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>queryInterface<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span></code></pre>
<p>那我們再去看一下在 class Sequelize 中的 queryInterface 是麼產生的。</p>
<pre class="language-js"><code class="language-js"><span class="token function">constructor</span><span class="token punctuation">(</span><span class="token parameter">database<span class="token punctuation">,</span> username<span class="token punctuation">,</span> password<span class="token punctuation">,</span> options</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// a lot of code</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>queryInterface <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>dialect<span class="token punctuation">.</span>queryInterface<span class="token punctuation">;</span><br /> <span class="token comment">// a lot of code</span><br /><span class="token punctuation">}</span></code></pre>
<p>使用 F12 大法可以看到這個 <code>this.dialect</code> 是怎麼決定的。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">let</span> Dialect<span class="token punctuation">;</span><br /><span class="token comment">// Requiring the dialect in a switch-case to keep the</span><br /><span class="token comment">// require calls static. (Browserify fix)</span><br /><span class="token keyword">switch</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getDialect</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">case</span> <span class="token string">"mariadb"</span><span class="token operator">:</span><br /> Dialect <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"./dialects/mariadb"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">break</span><span class="token punctuation">;</span><br /> <span class="token keyword">case</span> <span class="token string">"mssql"</span><span class="token operator">:</span><br /> Dialect <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"./dialects/mssql"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">break</span><span class="token punctuation">;</span><br /> <span class="token keyword">case</span> <span class="token string">"mysql"</span><span class="token operator">:</span><br /> Dialect <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"./dialects/mysql"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">break</span><span class="token punctuation">;</span><br /> <span class="token keyword">case</span> <span class="token string">"postgres"</span><span class="token operator">:</span><br /> Dialect <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"./dialects/postgres"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">break</span><span class="token punctuation">;</span><br /> <span class="token keyword">case</span> <span class="token string">"sqlite"</span><span class="token operator">:</span><br /> Dialect <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"./dialects/sqlite"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">break</span><span class="token punctuation">;</span><br /> <span class="token keyword">default</span><span class="token operator">:</span><br /> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><br /> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">The dialect </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getDialect</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> is not supported. Supported dialects: mssql, mariadb, mysql, postgres, and sqlite.</span><span class="token template-punctuation string">`</span></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><span class="token keyword">this</span><span class="token punctuation">.</span>dialect <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Dialect</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>也就是說這邊是依照給進來的 dialect 不同去決定要使用哪個 class 去 new instance。因為我原本是用 MySQL,所以我就先去看一下 "./dialects/mysql"。</p>
<p>到這個檔案裡面,在 <code>class MysqlDialect</code> 的 <code>constructor</code> 中就可以找到</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">class</span> <span class="token class-name">MysqlDialect</span> <span class="token keyword">extends</span> <span class="token class-name">AbstractDialect</span> <span class="token punctuation">{</span><br /> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token parameter">sequelize</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">super</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>sequelize <span class="token operator">=</span> sequelize<span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>connectionManager <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ConnectionManager</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">,</span> sequelize<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>queryGenerator <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">QueryGenerator</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token literal-property property">_dialect</span><span class="token operator">:</span> <span class="token keyword">this</span><span class="token punctuation">,</span><br /> sequelize<span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>queryInterface <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">MySQLQueryInterface</span><span class="token punctuation">(</span><br /> sequelize<span class="token punctuation">,</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>queryGenerator<br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>這邊是 new 一個 <code>MySQLQueryInterface</code>,接著看這個 class 裡只有三個 method,</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">class</span> <span class="token class-name">MySQLQueryInterface</span> <span class="token keyword">extends</span> <span class="token class-name">QueryInterface</span> <span class="token punctuation">{</span><br /> <span class="token keyword">async</span> <span class="token function">removeColumn</span><span class="token punctuation">(</span><span class="token parameter">tableName<span class="token punctuation">,</span> columnName<span class="token punctuation">,</span> options</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// a lot of code</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">async</span> <span class="token function">upsert</span><span class="token punctuation">(</span><span class="token parameter">tableName<span class="token punctuation">,</span> insertValues<span class="token punctuation">,</span> updateValues<span class="token punctuation">,</span> where<span class="token punctuation">,</span> options</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// a lot of code</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">async</span> <span class="token function">removeConstraint</span><span class="token punctuation">(</span><span class="token parameter">tableName<span class="token punctuation">,</span> constraintName<span class="token punctuation">,</span> options</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// a lot of code</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>所以如果想要找 <code>queryInterface.select()</code> 要從 <code>QueryInterface</code> 下手。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">class</span> <span class="token class-name">QueryInterface</span> <span class="token punctuation">{</span><br /> <span class="token comment">// a lot of code</span><br /> <span class="token keyword">async</span> <span class="token function">select</span><span class="token punctuation">(</span><span class="token parameter">model<span class="token punctuation">,</span> tableName<span class="token punctuation">,</span> optionsArg</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> options <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token operator">...</span>optionsArg<span class="token punctuation">,</span> <span class="token literal-property property">type</span><span class="token operator">:</span> QueryTypes<span class="token punctuation">.</span><span class="token constant">SELECT</span><span class="token punctuation">,</span> model <span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">return</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>sequelize<span class="token punctuation">.</span><span class="token function">query</span><span class="token punctuation">(</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>queryGenerator<span class="token punctuation">.</span><span class="token function">selectQuery</span><span class="token punctuation">(</span>tableName<span class="token punctuation">,</span> options<span class="token punctuation">,</span> model<span class="token punctuation">)</span><span class="token punctuation">,</span><br /> options<br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>那我們就再去看一下 <code>query</code> 這個 method。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">async</span> <span class="token function">query</span><span class="token punctuation">(</span><span class="token parameter">sql<span class="token punctuation">,</span> options</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// a lot of code</span><br /> <span class="token keyword">return</span> <span class="token function">retry</span><span class="token punctuation">(</span><span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>options<span class="token punctuation">.</span>transaction <span class="token operator">===</span> <span class="token keyword">undefined</span> <span class="token operator">&&</span> Sequelize<span class="token punctuation">.</span>_cls<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> options<span class="token punctuation">.</span>transaction <span class="token operator">=</span> Sequelize<span class="token punctuation">.</span>_cls<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">'transaction'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token function">checkTransaction</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">const</span> connection <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token punctuation">(</span>options<span class="token punctuation">.</span>transaction <span class="token operator">?</span> options<span class="token punctuation">.</span>transaction<span class="token punctuation">.</span>connection <span class="token operator">:</span> <span class="token keyword">this</span><span class="token punctuation">.</span>connectionManager<span class="token punctuation">.</span><span class="token function">getConnection</span><span class="token punctuation">(</span>options<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> query <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">this<span class="token punctuation">.</span>dialect<span class="token punctuation">.</span>Query</span><span class="token punctuation">(</span>connection<span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">,</span> options<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">try</span> <span class="token punctuation">{</span><br /> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">runHooks</span><span class="token punctuation">(</span><span class="token string">'beforeQuery'</span><span class="token punctuation">,</span> options<span class="token punctuation">,</span> query<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token function">checkTransaction</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> <span class="token keyword">await</span> query<span class="token punctuation">.</span><span class="token function">run</span><span class="token punctuation">(</span>sql<span class="token punctuation">,</span> bindParameters<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span> <span class="token keyword">finally</span> <span class="token punctuation">{</span><br /> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">runHooks</span><span class="token punctuation">(</span><span class="token string">'afterQuery'</span><span class="token punctuation">,</span> options<span class="token punctuation">,</span> query<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>options<span class="token punctuation">.</span>transaction<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>connectionManager<span class="token punctuation">.</span><span class="token function">releaseConnection</span><span class="token punctuation">(</span>connection<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span> retryOptions<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>前面都是對 sql 與 options 做處理,這邊重點在 <code>retry</code> 的 callback 裡面的 <code>query.run</code>。</p>
<pre class="language-js"><code class="language-js"> <span class="token keyword">async</span> <span class="token function">run</span><span class="token punctuation">(</span><span class="token parameter">sql<span class="token punctuation">,</span> parameters</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// a lot of code</span><br /> <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">formatResults</span><span class="token punctuation">(</span>results<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span></code></pre>
<p>這邊代表他在 run return 的是已經 format 過的結果,那大概表示 parse 是在這邊做的。</p>
<pre class="language-js"><code class="language-js"><span class="token function">formatResults</span><span class="token punctuation">(</span><span class="token parameter">data</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">let</span> result <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>instance<span class="token punctuation">;</span><br /> <span class="token comment">// a lot of code</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">isSelectQuery</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">handleSelectQuery</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token comment">// a lot of code</span><br /><span class="token punctuation">}</span></code></pre>
<pre class="language-js"><code class="language-js"><span class="token function">handleSelectQuery</span><span class="token punctuation">(</span><span class="token parameter">results</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">let</span> result <span class="token operator">=</span> <span class="token keyword">null</span><span class="token punctuation">;</span><br /> <span class="token comment">// Map raw fields to names if a mapping is provided</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>options<span class="token punctuation">.</span>fieldMap<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /><br /> <span class="token comment">// Queries with include</span><br /> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>options<span class="token punctuation">.</span>hasJoin <span class="token operator">===</span> <span class="token boolean">true</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> results <span class="token operator">=</span> AbstractQuery<span class="token punctuation">.</span><span class="token function">_groupJoinData</span><span class="token punctuation">(</span>results<span class="token punctuation">,</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">model</span><span class="token operator">:</span> <span class="token keyword">this</span><span class="token punctuation">.</span>model<span class="token punctuation">,</span><br /> <span class="token literal-property property">includeMap</span><span class="token operator">:</span> <span class="token keyword">this</span><span class="token punctuation">.</span>options<span class="token punctuation">.</span>includeMap<span class="token punctuation">,</span><br /> <span class="token literal-property property">includeNames</span><span class="token operator">:</span> <span class="token keyword">this</span><span class="token punctuation">.</span>options<span class="token punctuation">.</span>includeNames<br /> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">checkExisting</span><span class="token operator">:</span> <span class="token keyword">this</span><span class="token punctuation">.</span>options<span class="token punctuation">.</span>hasMultiAssociation<br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> result <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>model<span class="token punctuation">.</span><span class="token function">bulkBuild</span><span class="token punctuation">(</span>results<span class="token punctuation">,</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">isNewRecord</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">include</span><span class="token operator">:</span> <span class="token keyword">this</span><span class="token punctuation">.</span>options<span class="token punctuation">.</span>include<span class="token punctuation">,</span><br /> <span class="token literal-property property">includeNames</span><span class="token operator">:</span> <span class="token keyword">this</span><span class="token punctuation">.</span>options<span class="token punctuation">.</span>includeNames<span class="token punctuation">,</span><br /> <span class="token literal-property property">includeMap</span><span class="token operator">:</span> <span class="token keyword">this</span><span class="token punctuation">.</span>options<span class="token punctuation">.</span>includeMap<span class="token punctuation">,</span><br /> <span class="token literal-property property">includeValidated</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">attributes</span><span class="token operator">:</span> <span class="token keyword">this</span><span class="token punctuation">.</span>options<span class="token punctuation">.</span>originalAttributes <span class="token operator">||</span> <span class="token keyword">this</span><span class="token punctuation">.</span>options<span class="token punctuation">.</span>attributes<span class="token punctuation">,</span><br /> <span class="token literal-property property">raw</span><span class="token operator">:</span> <span class="token boolean">true</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token comment">// Regular queries</span><br /> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br /><br /> <span class="token punctuation">}</span><br /><br /> <span class="token comment">// return the first real model instance if options.plain is set (e.g. Model.find)</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>options<span class="token punctuation">.</span>plain<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> result <span class="token operator">=</span> result<span class="token punctuation">.</span>length <span class="token operator">===</span> <span class="token number">0</span> <span class="token operator">?</span> <span class="token keyword">null</span> <span class="token operator">:</span> result<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> result<span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>看到這邊我猜想我應該快到終點了,</p>
<pre class="language-js"><code class="language-js"> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>options<span class="token punctuation">.</span>hasJoin <span class="token operator">===</span> <span class="token boolean">true</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> results <span class="token operator">=</span> AbstractQuery<span class="token punctuation">.</span><span class="token function">_groupJoinData</span><span class="token punctuation">(</span>results<span class="token punctuation">,</span> <span class="token punctuation">{</span><span class="token operator">...</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span></code></pre>
<p>關鍵應該就在 <code>_groupJoinData</code>,只要看完這個 function 我應該就能知道他是怎麼從原始的結果 parse 出巢狀結構的資料。</p>
<pre class="language-js"><code class="language-js"> <span class="token keyword">static</span> <span class="token function">_groupJoinData</span><span class="token punctuation">(</span><span class="token parameter">rows<span class="token punctuation">,</span> includeOptions<span class="token punctuation">,</span> options</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /><br /> <span class="token comment">/*<br /> * Author (MH) comment: This code is an unreadable mess, but it's performant.<br /> * groupJoinData is a performance critical function so we prioritize perf over readability.<br /> */</span><br /> <span class="token punctuation">}</span></code></pre>
<p>原本我興高采烈終於可以知道原理了,不過看到這兩行我心就涼了,再往下看,那些 code 真的精美到我不知其所以然。<br />
看來我這次 trace source code 就只能先到這邊了。</p>
<h2 id="%E7%B5%90%E8%AA%9E"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/dig-in-source-code/#%E7%B5%90%E8%AA%9E">#</a> 結語</h2>
<p>其實這次雖然不能真的了解 Sequelize 是怎麼 parse 資料。不過在 trace 的過程中,我對於怎樣是好的 code 有更多理解。首先是資料夾的結構,他們每個資料夾與檔案是依照這個專案的架構去區分,檔案中幾乎都只有一個對應名稱的 class,除此之外就沒有其他東西。還有就是命名,class 中有一些 attribute 是其他 class,這時這些 attribute 就會和 class 一樣的名稱,如此一來在 trace code 的時候看到這些名稱就可以知道要去哪裡找對應的 class 看。</p>
<p>而當我第二次 trace 的時候,我覺得應該用一個更好的方式來做。當我們使用 Sequelize 時,如果把連線到資料庫這部分先不管,整體的流程應該像是:</p>
<blockquote>
<ol>
<li>依照 code 建立 model instance。</li>
<li>對這個 instance 下我們想要的指令。</li>
<li>instance 會依照指令 build SQL,然後透過連線向資料庫 query,接著 parse 資料再回傳。</li>
</ol>
</blockquote>
<p>所以如果真的想要理解整個 Sequelize 運作,一開始就全域搜尋可能是個不太好的行為,這樣會讓 trace code 時失去整體脈絡。比較好的作法可能會是從建立 model 相關的 code 開始,因為我第一次 trace 的時候,就在 <code>queryInterface</code> 卡了很久,當無頭蒼蠅瞎找好久之後才找到要從哪裡下手。所幸 Sequelize 的原始碼大部分的命名與撰寫方式還是很好閱讀的,雖然我第一次看這麼龐大的物件導向原始碼,但是還是滿快就進入狀況。</p>
<p>原本我上一期就想要寫 Sequelize 怎麼 parse 資料,但是當我 trace code 到那兩行 comment 讓我直接放棄,不過經過 huli 的提醒發現怎麼去看 code 好像也是個值得寫的題材,於是乎就把我怎麼找 code 的過程記錄下來。</p>
<p>感謝 huli,感謝 error baker。</p>
為什麼要使用框架 - 你聽過最好的答案是什麼?
2021-09-19T00:00:00Z
https://blog.errorbaker.tw/posts/benben/02-framework/
<!-- summary -->
<!-- 框架百百種,但有想過為什麼要用框架嗎? -->
<!-- summary -->
<p><img src="https://www.saaspegasus.com/static/images/web/modern-javascript/2008-vs-2021.png" alt="modern-javascript" /></p>
<blockquote>
<p>圖片來源:<a href="https://www.saaspegasus.com/guides/">https://www.saaspegasus.com/guides/</a></p>
</blockquote>
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/02-framework/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<p>大家安安!2021 的 Web 前端也是一如往常的五花八門!當然,我們也是學的落花流水的。</p>
<p>最近也是筆者程式導師計劃第五期的尾聲了,不知道大家的學習路徑是如何,也許有大前門輩是一路從原生 JavaScript 、到 jQuery 、再到三分框架的時代,而當中框架也經歷不少更新(2016 - 2021),但很多的 <code>前端快速上手班</code> 的學習過程,可能剛學完 JavaScript 就直接學 React hooks、Vue 3 ...等,我敢說有人(可能還不少)不知道 JavaScript 的作者是誰,甚至也沒聽過網景,所以更別說框架了,為什麼要學 JavaScript ?可能都沒辨法回答的很好。</p>
<p>像這種 <code>為什麼</code> 類型的問題,筆者喜歡去找歷史,因為歷史會告訴你前人的時空背景,通常在理解並稍加分析後,就會推導出自己答案了,這種答案比起那種塘塞給面試管的答案,更能說服自己。</p>
<p>筆者這邊會以前端的框架的角度,去思考為什麼我要使用框架。但有時候或許可以不用這麼糾結,有些可能是哲學問題,例如:人是為了什麼而活?然後就開始疑懷人生。</p>
<h2 id="javascript-%E7%B0%A1%E5%8F%B2"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/02-framework/#javascript-%E7%B0%A1%E5%8F%B2">#</a> JavaScript 簡史</h2>
<p><strong>1995 年,Brendan Eich 大大發明了 JavaScript</strong> ,今年(2021)的 7 月 4 號也是他的六十歲大壽,我們懷念他,不是!是祝福他感謝他,感謝他發明了 JavaScript 讓我們有工作可以做、而且還有很多東西很難懂,但如果可以搞懂了大部分人不懂的東西,就可以跟別人區隔開來,老實說這樣鑑別度蠻好的,所以面試考這些,好像就很合理了。</p>
<p><img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZDtqrBOj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://github.com/damiancipolat/js_vs_memes/blob/master/doc/js_thanks.png%3Fraw%3Dtrue" alt="Thanks for JS - meme" /></p>
<blockquote>
<p>圖片來源:<a href="https://dev.to/damxipo/javascript-versus-memes-explaining-various-funny-memes-2o8c">https://dev.to/damxipo/javascript-versus-memes-explaining-various-funny-memes-2o8c</a></p>
</blockquote>
<p>謝了!Brendan Eich 大大發明 JavaScript!對了,如果你不知道,他就是圖中的辣個男人。</p>
<p>當時的時空背景是:<strong>需要一個可以在瀏覽器中執行的程式</strong>,當時的 <strong>網景</strong> ( Netscape ) 指派給 Brendan Eich 這個任務,正好 Java 正火紅,他就把這個程式語言取名為 <code>JavaScript</code> 想蹭一點知名度,其實跟 Java 完全沒有關係(沒有那種會了其中一個,另一個就差不多會了的事),但又不是完全沒有關係,確實他發明的時候也 "參考" 了 Java ,但後來各個語言也是互相 "參考" 來的,很多寫法確實有一曲同工之妙,這裡讓大家自行體會了。</p>
<p>後來 Brendan Eich 引起了一些爭議又是另外一個故事了。</p>
<p>這時的 JavaScript 還很純很純,好!JavaScript 的簡史先到這。</p>
<h2 id="jquery-%E9%9C%B8%E4%B8%BB%E8%88%87%E7%99%BE%E5%AE%B6%E7%88%AD%E5%97%9A%E7%9A%84%E7%80%8F%E8%A6%BD%E5%99%A8"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/02-framework/#jquery-%E9%9C%B8%E4%B8%BB%E8%88%87%E7%99%BE%E5%AE%B6%E7%88%AD%E5%97%9A%E7%9A%84%E7%80%8F%E8%A6%BD%E5%99%A8">#</a> jQuery 霸主與百家爭嗚的瀏覽器</h2>
<p>JavaScript 發明完後,網景的市佔率也順勢起飛,還曾經一度獨站鱉頭,後來其他群雄當然也眼紅了,因為當時用網路的人還不多,不用我說,小孩子都知道這絕對是一片大藍海啊!不用多久,隨便就生出了一堆瀏覽器:Internet Explorer, Chrome, Firefox, Safari, Opera, ...等,於是瀏覽器混戰就開始了!小孩子才做選擇,我都用 IE !</p>
<p>但是好景不常,工程師面臨的問題是:<strong>要如何解決跨瀏覽器</strong> 的問題,最一開始是,每一種寫法都寫好寫滿,當然這很累人,但又創造了不少工作機會,是吧?有需求就有機會。</p>
<p><strong>2006 年 jQuery 推出了</strong>,曾經全球前 10,000 個存取最高的網站中,有 65% 使用了 jQuery ,他完美解決了跨瀏覽器問題,一種寫法就可以轉成各瀏覽器都能用的程式碼,好 library,不用嗎?那時候可能也還沒有 library, framework 的概念,反正問題解決了,客戶開心、老闆開心,當然你也發大財,所以你也開心。</p>
<p>簡單 jQuery 寫法如下:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token comment">// jQuery</span><br /><span class="token function">$</span><span class="token punctuation">(</span><span class="token string">"#hello"</span><span class="token punctuation">)</span><br /><br /><span class="token comment">// 原生 JavaScript</span><br />document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span>hello<span class="token punctuation">)</span></code></pre>
<p>為什麼要用 jQuery ,當然也就不明而喻了,除了可以少打很多字以外,最重要的一點就是可以解決上述的跨瀏覽器問題,但其實你不需要 jQuery ,除了現在的瀏覽器支援度都很好以外,另人垢病的就是效能問題,以前的專案還不大所以沒感覺,但當你一件簡單的事都想用 jQuery ,這就不對了,例如:原生 <code>document.getElementById</code> 遠大於 jQuery,當你專案越來越大,也就越來越慢。</p>
<blockquote>
<p>延伸閱讀:<a href="https://youmightnotneedjquery.com/">You Might Not Need jQuery</a></p>
</blockquote>
<h2 id="framework-%26-library"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/02-framework/#framework-%26-library">#</a> Framework & Library</h2>
<p>隨著時間推進,2013 年 Facebook 推出了 React ,在當時是非常新的技術,當然在台灣要有一定的開發者使用,少說也要 3 年,筆者那時候也還在唸書,當然學校怎麼可能會教這麼潮的東西,但是面試的門檻就要你會框架(<s>阿我就沒學過啊,學校也沒教啊</s>),也確實在那個時候(2016),許多的 <code>前端速成班</code> 如雨後春筍般出現,打著無經驗轉職的招牌,阿貓阿狗都收,但實際上的狀況是:我們無從得知那些所有的阿貓阿狗後來怎麼了,反正人進來發大財。</p>
<p>這裡不是說要限制什麼樣的人就不能當工程師,而是要說這條路可能沒你表面看到的容易,那些轉職成功的人都是付出相當大的時間精力,都是你沒看到的,如果你意志還堅定,那麼可以試試看,了不起也就浪費 3 ~ 6 個月。</p>
<p>在回來看到 React 框架,許多人看到 <a href="https://reactjs.org/">React 的官網</a> 中寫道:<code>A JavaScript library for building user interfaces</code>,喔!原來 React 不是框架啊,是一個 "Library",是整個生態系合成才是一個框架。嗯嗯,這樣你就懂了吧!但是!新手如我,看到這還是充滿一堆問號啊,好了,我的問題又來了:那什麼是 Library?</p>
<p>Library,其實可以源自於西元前 2600 ,由蘇美人的楔形石板打造而成,喔!不是,那是圖書館(library),其實是我找不到合理的解釋,因為這個詞已經很抽像了又有很多意思,也可以看一下 <a href="https://zh.wikipedia.org/wiki/%E5%87%BD%E5%BC%8F%E5%BA%AB">Wiki 的中文頁面</a> 有多麼少的內容,Library 又分為 Computing Library 跟 Digital Library,但這邊要講的是 Digital Library 裡的放程式碼的 Library,等等!你搞得我好亂啊!但我想你懂我意思。</p>
<p>再換一個角度,如果你是一位 Web 工程師,我敢說你一定用過 <code>npm</code> ,好吧,如果你說沒有,你都手刻來著,那我也認了你是大神。npm 是於 2010 年誕生的,library 一詞,估且說是那個時間點才廣為流傳的,好像也不為過,但大部分的新手對 Library 的認識,不就是用 <code>npm install XXX</code> 下載你要使用的套件,對,我就是這樣認識的 library 一詞的,對於這個熟悉又陌生的一詞僅此而已。</p>
<p>其實會寫 function 就會寫 Library 了喔,例如:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">add</span><span class="token punctuation">(</span><span class="token parameter">a<span class="token punctuation">,</span> b</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> a <span class="token operator">+</span> b<br /><span class="token punctuation">}</span><br /><br /><span class="token comment">// 或是已經沒這麼潮的寫法</span><br /><span class="token keyword">const</span> <span class="token function-variable function">add</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">a<span class="token punctuation">,</span> b</span><span class="token punctuation">)</span> <span class="token operator">=></span> a <span class="token operator">+</span> b</code></pre>
<p>恭喜你,你已經寫了一個加法的 Library 了,對這一詞的概念就是這麼簡單。</p>
<p>Library 跟 Framework 簡單說都是可以使用別人寫好的程式碼,但差別在於 <code>自由度</code>,這個概念有點抽像的概念,拿餐廳舉例來說:</p>
<ol>
<li>Library 的寫法:像是可以自由的選擇你要的吃的食物,也可以只喝飲料。</li>
<li>Framework 的寫法:則像是配好的套餐,可以選擇某些主食,但甜點不能選擇。</li>
</ol>
<p>這樣應該有比較懂兩者的差別,但其實自由度一詞也很主觀,像是:如果說 React 的自由度高於 Angular ,大家應該都可以接受,但是他們都是還是框架(準確來說是 React 生態系跟 Angular)</p>
<blockquote>
<p>延伸閱讀:<a href="https://www.freecodecamp.org/news/the-difference-between-a-framework-and-a-library-bd133054023f/">The Difference Between a Framework and a Library</a></p>
</blockquote>
<p>既然 React 不是一個框架(準確來說),那麼可以思考一下:<strong>React 裡還要再加哪些東西才算一個框架?</strong></p>
<p>如果你用過 <code>create-react-app</code> ,而且你也有把 <code>package.json</code> 來看,會看到 <code>react</code>, <code>react-dom</code> 還有一些測試套件等,看起來好像也沒有很多東西啊,但是如果你跟我一樣好奇,可以打開 <code>node_modules</code> 看看裡面有多少套件,會看到一些知名的如:<code>webpack</code>, <code>babel</code>, <code>jest</code> ...等,也有一些好用的小套件:<code>uuid</code>, <code>dotenv</code>, <code>fast</code> ... 等。</p>
<p>是因為有了 webpack, babel, jest 這些大的 Library 才算框架嗎?可能是也可能不是,我不知道。</p>
<blockquote>
<p>延伸閱讀:<a href="https://www.freecodecamp.org/news/is-react-a-library-or-a-framework/">Is React a Library or a Framework? Here's Why it Matters</a></p>
</blockquote>
<h3 id="%E4%BD%BF%E7%94%A8%E6%A1%86%E6%9E%B6%E7%9A%84%E5%A5%BD%E8%99%95"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/02-framework/#%E4%BD%BF%E7%94%A8%E6%A1%86%E6%9E%B6%E7%9A%84%E5%A5%BD%E8%99%95">#</a> 使用框架的好處</h3>
<p>常見的答案有:</p>
<ul>
<li>同樣的功能可以重複利用,易擴充也易維護</li>
<li>Component & 模組化</li>
<li>畫面與資料的分離</li>
<li>React 可以被用來寫 SPA</li>
<li>React 的生態系已經很成熟,網路上的資源也非常的多</li>
<li>保持程式的單一入口點</li>
<li>因為社群比較多人用,所以資源也會比較多</li>
<li>... 等</li>
</ul>
<p>但仔細想想上面的答案,可能都只是 <strong>結果</strong> 並不是原因,這需要把前因後果搞清楚,邏輯才會清晰,例如:最上面的兩點,跟框架一點關係都沒有,這兩點只要是寫程式都要都應該要注意的。</p>
<p>如之前所說的,應該要關注的點是框架解決了什麼問題,先來看看這時候什麼問題出現了吧!</p>
<p>當時的時空背景是:<strong>2010 iPhone 4 上市,使用者大量的轉移到移動裝置</strong>,這時候的 web 除了要適應裝置的大小(RWD)之外,還要面對更貼近的商業市場,這代表著 UI/UX 越來越重要,因為使用者越來越多,必須有更好的體驗才能留住使用者,前後端分離的概念也隨之萌芽,確實這個神奇的時間點(2010)開始,大量的工具推出,還記得最上面那張圖嗎,你點發現時間軸是一致的。</p>
<p>這時的分工可能還沒有前後端分離,為了要因應這個巨大的轉變,框架的雛形就誕生了,但是分離的做法當然不只一種,常見的有:MVC、MVP、MVVM 模型,所以框架也是有一些小區別的,但都是為了解決 <strong>前後端分離</strong> 這件事。</p>
<p>到這邊答案就漸漸浮現了,為什麼會需要框架?因為要需前後端分離。為什麼要前後端分離?因為專案規模大。</p>
<p>現行的大型專案幾乎都會用到框架,所以公司要求你至少學一個框架,好像也是挺合理的,因為公司等級的專案通常都很大。既然現在的框架都可以解決前後端分離的問題,再來就是挑選框架了,可以依你喜歡的模型、效能、檔案大小、語法 ...等,去使用任何你喜歡的框架,當然最重要的還是看公司需求 XD</p>
<h2 id="%E7%B8%BD%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/02-framework/#%E7%B8%BD%E7%B5%90">#</a> 總結</h2>
<p>現在前端新的東西一直出,舊的東西也一直更新,都快學不完啦!</p>
<p>回顧前端技術的發展,不難發現新技術和新工具總是圍繞著問題而生。但其實近幾年有趨緩的趨勢,其實也如同手機一般的飽合,代表著前端已經逐漸成形了,未來可能不會有太大的改動,可以趁現在把握機會,能掌握技術的時候,就不要讓他溜走!</p>
<p>最後希望大家可以一同進步!如果有說錯或是講得不夠清楚的地方歡迎指正,感謝您的閱讀。</p>
<h2 id="ref"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/02-framework/#ref">#</a> Ref</h2>
<ul>
<li><a href="https://youmightnotneedjquery.com/">You Might Not Need jQuery</a></li>
<li><a href="https://www.freecodecamp.org/news/the-difference-between-a-framework-and-a-library-bd133054023f/">The Difference Between a Framework and a Library</a></li>
<li><a href="https://www.freecodecamp.org/news/is-react-a-library-or-a-framework/">Is React a Library or a Framework? Here's Why it Matters</a></li>
</ul>
<blockquote>
<p>免責聲名</p>
</blockquote>
<p>以上均為筆者自身經驗,難免小有主觀意見,供讀者們參考,也歡迎分享經驗交流。<br />
如果有錯誤的地方還請大大們指正,筆者會立刻修改,再次感謝大家!</p>
<p><a href="https://creativecommons.org/licenses/by/4.0/"><img src="https://i.creativecommons.org/l/by/4.0/88x31.png" alt="Creative Commons License" /></a></p>
<p>本著作係採用 <a href="https://creativecommons.org/licenses/by/4.0/">創用 CC 姓名標示 4.0 國際授權條款</a> 授權。您可以在 <a href="https://benben.me/">benben.me</a> 找到我。</p>
<p>This work is licensed under a <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>. You can find me at <a href="https://benben.me/">benben.me</a></p>
React Ref 的一點研究
2021-09-27T00:00:00Z
https://blog.errorbaker.tw/posts/lavi/react-ref-reference/
<!-- summary -->
<!-- 除了拿 DOM element 還有存變數外,更全面的了解 Ref 一點點 -->
<!-- summary -->
<!-- more -->
<h1 id="react-ref-%E7%9A%84%E4%B8%80%E9%BB%9E%E7%A0%94%E7%A9%B6"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/react-ref-reference/#react-ref-%E7%9A%84%E4%B8%80%E9%BB%9E%E7%A0%94%E7%A9%B6">#</a> React Ref 的一點研究</h1>
<blockquote>
<p>A JavaScript library for building user interfaces</p>
</blockquote>
<p>React 是狀態和 UI 的 Library 我們都知道,使用了 React 可以這樣思考:每一個狀態都會產生出對應的 UI。使用了 React 之後,就很少使用像是 DOM 的原生 API 來操作元素了,但還是會有需要直接從 DOM 元素取得資料或者是操作的情境,這時候就是使用 ref 的時候。</p>
<p>React ref 就是一個可以直接操作 DOM 的出口,透過 <code>createRef</code> / <code>useRef</code>,以及將 ref 作為 props 放入 DOM element ,能透過 ref 直接操作 DOM。就像下面 React <a href="https://reactjs.org/docs/hooks-reference.html#useref">官方文件</a> 中 hooks 的範例:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">TextInputWithFocusButton</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> inputEl <span class="token operator">=</span> <span class="token function">useRef</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> <span class="token function-variable function">onButtonClick</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token comment">// `current` points to the mounted text input element</span><br /> inputEl<span class="token punctuation">.</span>current<span class="token punctuation">.</span><span class="token function">focus</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span><span class="token operator">></span><br /> <span class="token operator"><</span>input ref<span class="token operator">=</span><span class="token punctuation">{</span>inputEl<span class="token punctuation">}</span> type<span class="token operator">=</span><span class="token string">"text"</span> <span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span>onButtonClick<span class="token punctuation">}</span><span class="token operator">></span>Focus the input<span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span><span class="token operator">></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>我們直接透過 <code>input.current</code>,來使用 DOM 的 method。雖然上面是 Hooks 的範例,但其實在 Class component 也沒什麼不同,只是從在 function 中宣告變數變成 class 的內部屬性而已。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">class</span> <span class="token class-name">TextInputWithFocusButton</span> <span class="token keyword">extends</span> <span class="token class-name">React<span class="token punctuation">.</span>Component</span> <span class="token punctuation">{</span><br /> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token parameter">props</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">super</span><span class="token punctuation">(</span>props<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>inutEl <span class="token operator">=</span> React<span class="token punctuation">.</span><span class="token function">createRef</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <br /> <span class="token keyword">this</span><span class="token punctuation">.</span>handleClick <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">handleClick</span><span class="token punctuation">.</span><span class="token function">bind</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <br /> <span class="token function">onButtonClick</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>inputEl<span class="token punctuation">.</span>current<span class="token punctuation">.</span><span class="token function">focus</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <br /> <span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token operator"><</span><span class="token operator">></span><br /> <span class="token operator"><</span>input ref<span class="token operator">=</span><span class="token punctuation">{</span><span class="token keyword">this</span><span class="token punctuation">.</span>inputEl<span class="token punctuation">}</span> type<span class="token operator">=</span><span class="token string">"text"</span> <span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span>onButtonClick<span class="token punctuation">}</span><span class="token operator">></span>Focus the input<span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span><span class="token operator">></span> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>另外一個會用到的 ref 的地方是,當你希望儲存一個不會影響 ui 的狀態的時候。在 React 中,每次 state 的改變都會造成 Rerender,進而改變 UI,但並不是每次都會想要這樣。這時候就能夠把值儲存在 ref 裡面:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">notRefreshCounter</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> counterRef <span class="token operator">=</span> <span class="token function">useRef</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <br /> <span class="token keyword">const</span> <span class="token function-variable function">onClick</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> counterRef<span class="token punctuation">.</span>current<span class="token operator">++</span> <br /> <span class="token punctuation">}</span> <br /> <br /> <span class="token keyword">const</span> <span class="token function-variable function">onPrint</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> <span class="token operator"><</span>div<span class="token operator">></span><br /> <span class="token operator"><</span>button <span class="token parameter">onClick</span><span class="token operator">=></span>add <span class="token number">1</span><span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><br /><span class="token punctuation">}</span></code></pre>
<p>好的,比較基本的用法大概就是這樣了,那可以來談談一些比較有趣的用法。</p>
<h2 id="callback-ref"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/react-ref-reference/#callback-ref">#</a> Callback Ref</h2>
<p>剛剛我們在使用 ref 的時候可以分為兩種用法</p>
<ol>
<li>透過將 ref 放入 react element(jsx 語法建立的)的 props,可以操作 element 上面的方法或者讀取屬性。</li>
<li>儲存不影響 UI 的值。</li>
</ol>
<p>雖然講的是 ref,但其實第一種用法我們是透過兩個東西來做到的:</p>
<ul>
<li>create Ref 的 API,不論是 <code>React.createRef</code> 還是 <code>React.useRef</code></li>
<li>React element 上面的 ref 屬性</li>
</ul>
<p>然而 React element 的 ref 屬性除了接受 <code>createRef</code> / <code>useRef</code> 以外,還可以接受function 的形式, 並能夠帶來更大的彈性。</p>
<pre class="language-js"><code class="language-js"><br /><span class="token keyword">function</span> <span class="token function">AutoSelectInput</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>_<span class="token punctuation">,</span> refresh<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /> <span class="token keyword">const</span> <span class="token function-variable function">autoFocus</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">element</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>element<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> element<span class="token punctuation">.</span><span class="token function">focus</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span>div className<span class="token operator">=</span><span class="token string">"App"</span><span class="token operator">></span><br /> <span class="token operator"><</span>input ref<span class="token operator">=</span><span class="token punctuation">{</span>autoFocus<span class="token punctuation">}</span><span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span>refresh<span class="token punctuation">}</span><span class="token operator">></span>refresh<span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /></code></pre>
<p>上面是個應用 callback ref 的小小範例,改寫自 react 官方文件,除了 component 第一次被 render 時會 auto focus 之外。當按下 refresh 時( <code>useState</code> 這樣的用法可以在 <a href="https://zh-hant.reactjs.org/docs/hooks-faq.html#is-there-something-like-forceupdate">React FAQ</a> 中看到),整個 component 重新 render 也會自動 focus 在 input 上面。</p>
<p>callback ref 是什麼?從他的名字上面就可以知道他其實是一個 callback function,那麼會在什麼時候被執行?</p>
<blockquote>
<p>React will call the <code>ref</code> callback with the DOM element when the component mounts, and call it with <code>null</code> when it unmounts. Refs are guaranteed to be up-to-date before <code>componentDidMount</code> or <code>componentDidUpdate</code> fires.</p>
</blockquote>
<p><a href="https://reactjs.org/docs/refs-and-the-dom.html#callback-refs">文件</a>中裡面提到了, callback ref 會在兩種情況被呼叫。</p>
<ul>
<li>DOM element 被 mount 上 component 的時候:執行 <code>callbackRef(element)</code></li>
<li>DOM element 被 unmount 的時候:執行 <code>callbackRef(null)</code></li>
</ul>
<p>在時機上,其實有點像 useEffect 的 callback 中 return 的 cleanup function,或者說是 componentDidMount 還有 componentDidUnmount 會更準確。</p>
<p>不過在<a href="https://reactjs.org/docs/refs-and-the-dom.html#caveats-with-callback-refs">文件</a>中有使用 callback ref 的注意事項:</p>
<blockquote>
<p>If the <code>ref</code> callback is defined as an inline function, it will get called twice during updates, first with <code>null</code> and then again with the DOM element. This is because a new instance of the function is created with each render, so React needs to clear the old ref and set up the new one. You can avoid this by defining the <code>ref</code> callback as a bound method on the class, but note that it shouldn’t matter in most cases.</p>
</blockquote>
<p>文件中提到如果使用 inline function 作為 callback ref 時,每次 rerender 都會呼叫兩次,一次是 callbackRef(null),一次是 callbackRef(element)。原因是因為每次都會建立新的 function,所以要清理舊的 function 然後設定新的。</p>
<p>雖然我自己沒有很清楚為什麼清理舊的 function 就必須執行 callback。但這個原因讓我們在使用 callback ref 的時候需要注意這種情形:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">InlineAutoSelectInput</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>_<span class="token punctuation">,</span> refresh<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span>div className<span class="token operator">=</span><span class="token string">"App"</span><span class="token operator">></span><br /> <span class="token operator"><</span>input ref<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span><span class="token parameter">element</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> element<span class="token punctuation">.</span><span class="token function">focus</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span>refresh<span class="token punctuation">}</span><span class="token operator">></span>refresh<span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <br /><span class="token punctuation">}</span><br /></code></pre>
<p>如果像上面那樣是會報錯的,因為在 rerender 時第一次時 element 為 null,當然沒有 focus 給你用。</p>
<p>隨後有提到一種解決方法,換成 class component,然後把 方法 bind 在 class 上面,<s>但沒有人會想要再回去寫 Class component</s>。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">class</span> <span class="token class-name">ClassComp</span> <span class="token keyword">extends</span> <span class="token class-name">Component</span> <span class="token punctuation">{</span><br /> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token parameter">props</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">super</span><span class="token punctuation">(</span>props<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>rerender <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">rerender</span><span class="token punctuation">.</span><span class="token function">bind</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token function">rerender</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">forceUpdate</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <br /> <span class="token punctuation">}</span><br /><br /> <span class="token function">focusRef</span><span class="token punctuation">(</span><span class="token parameter">ele</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> ele<span class="token punctuation">.</span><span class="token function">focus</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span> <br /><br /> <span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span>div<span class="token operator">></span><br /> <span class="token operator"><</span>input ref<span class="token operator">=</span><span class="token punctuation">{</span><span class="token keyword">this</span><span class="token punctuation">.</span>focusRef<span class="token punctuation">}</span><span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span><span class="token keyword">this</span><span class="token punctuation">.</span>rerender<span class="token punctuation">}</span><span class="token operator">></span>refresh<span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>為什麼這樣就可以了呢?因為 callback ref 永遠指向同一個 function,也就是 <code>ClassComp</code> 產生的 instance 中的 <code>focusRef</code> 這個 function,而沒有建立新的,因此就不會發生 cleanup 的狀況。</p>
<p>但即使是這樣,React element 被 unmount 時還是會呼叫 <code>callbackRef(null)</code> ,這樣的狀況還是會找不到 <code>ele.focus</code> 而報錯。</p>
<p>所以比較簡單,也比較保險的方式是這樣,加個 if 就好,這樣是 null 就會自動忽略:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">InlineAutoSelectInput</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>_<span class="token punctuation">,</span> refresh<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span>div<span class="token operator">></span><br /> <span class="token operator"><</span>input ref<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span><span class="token parameter">element</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>element<span class="token punctuation">)</span><span class="token punctuation">{</span><br /> element<span class="token punctuation">.</span><span class="token function">focus</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span>refresh<span class="token punctuation">}</span><span class="token operator">></span>refresh<span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <br /><span class="token punctuation">}</span></code></pre>
<p>值得一提的是,雖然文件上只提到 inline 的 callback function 會有這個問題,但其實在 function component 中,即使使用變數也會遇到一樣的問題,我們拿前面提到的例子。</p>
<pre class="language-js"><code class="language-js"><br /><span class="token keyword">function</span> <span class="token function">AutoSelectInput</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>_<span class="token punctuation">,</span> refresh<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /> <span class="token keyword">const</span> <span class="token function-variable function">autoFocus</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">element</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>element<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> element<span class="token punctuation">.</span><span class="token function">focus</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span>div<span class="token operator">></span><br /> <span class="token operator"><</span>input ref<span class="token operator">=</span><span class="token punctuation">{</span>autoFocus<span class="token punctuation">}</span><span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span>refresh<span class="token punctuation">}</span><span class="token operator">></span>refresh<span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /></code></pre>
<p>因為 callback ref 一樣是在 function component 被 render 的時候才被建立的,每次被帶進去 input 時一樣是不同的 function。</p>
<p>那想要在 function component 中也想要做到 class component 的效果要怎麼做?既然要保持相同,最直覺的作法就是 <code>useCallback</code> 了</p>
<pre class="language-js"><code class="language-js"><br /><span class="token keyword">function</span> <span class="token function">AutoSelectInput</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>_<span class="token punctuation">,</span> refresh<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /> <span class="token keyword">const</span> autoFocus <span class="token operator">=</span> <span class="token function">useCallback</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">element</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>element<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> element<span class="token punctuation">.</span><span class="token function">focus</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><br /><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span>div<span class="token operator">></span><br /> <span class="token operator"><</span>input ref<span class="token operator">=</span><span class="token punctuation">{</span>autoFocus<span class="token punctuation">}</span><span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span>refresh<span class="token punctuation">}</span><span class="token operator">></span>refresh<span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /></code></pre>
<p>除此之外,因為沒有用到 component 內部的屬性, 所以這樣處理也是可行的,把 callback ref 提到外層。提到外層後就永遠指向同一個 function 了,能夠避免在沒有 mount / unmount 也呼叫的問題。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token function-variable function">autoFocus</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">element</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /><span class="token keyword">if</span> <span class="token punctuation">(</span>element<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> element<span class="token punctuation">.</span><span class="token function">focus</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">function</span> <span class="token function">AutoSelectInput</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>_<span class="token punctuation">,</span> refresh<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span>div<span class="token operator">></span><br /> <span class="token operator"><</span>input ref<span class="token operator">=</span><span class="token punctuation">{</span>autoFocus<span class="token punctuation">}</span><span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span>refresh<span class="token punctuation">}</span><span class="token operator">></span>refresh<span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<h3 id="callback-ref-%E7%94%A8%E5%9C%A8%E5%93%AA%E8%A3%A1%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/react-ref-reference/#callback-ref-%E7%94%A8%E5%9C%A8%E5%93%AA%E8%A3%A1%EF%BC%9F">#</a> Callback Ref 用在哪裡?</h3>
<p>在剛剛的範例中,我們使用 callback ref 來讓 input 元素自動 focus,而除了這樣的用法之外,在 React 的<a href="https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node">文件</a>中也有提到可以用來拿取 DOM element 的元素資訊。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">MeasureExample</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>height<span class="token punctuation">,</span> setHeight<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">const</span> measuredRef <span class="token operator">=</span> <span class="token function">useCallback</span><span class="token punctuation">(</span><span class="token parameter">node</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>node <span class="token operator">!==</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <br /> <span class="token function">setHeight</span><span class="token punctuation">(</span>node<span class="token punctuation">.</span><span class="token function">getBoundingClientRect</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>height<span class="token punctuation">)</span><span class="token punctuation">;</span> <br /> <span class="token punctuation">}</span> <br /> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span><span class="token operator">></span><br /> <span class="token operator"><</span>h1 ref<span class="token operator">=</span><span class="token punctuation">{</span>measuredRef<span class="token punctuation">}</span><span class="token operator">></span>Hello<span class="token punctuation">,</span> world<span class="token operator"><</span><span class="token operator">/</span>h1<span class="token operator">></span> <br /> <span class="token operator"><</span>h2<span class="token operator">></span>The above header is <span class="token punctuation">{</span>Math<span class="token punctuation">.</span><span class="token function">round</span><span class="token punctuation">(</span>height<span class="token punctuation">)</span><span class="token punctuation">}</span>px tall<span class="token operator"><</span><span class="token operator">/</span>h2<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span><span class="token operator">></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>自己對於 callback Ref 的理解是,父層能夠用 callback Ref 的方式,能夠在 DOM element 還是 React element,設定 mount / unmount 時的 callback function。在下面的範例中:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">App</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>_<span class="token punctuation">,</span> refresh<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"rerender"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token function">useEffect</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"effect"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token function">useLayoutEffect</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"layout effect"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span>div ref<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span><span class="token parameter">ele</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"cb1"</span><span class="token punctuation">,</span> ele<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">}</span> <span class="token operator">></span><br /> <span class="token operator"><</span>h1<span class="token operator">></span>Hello Ref<span class="token operator"><</span><span class="token operator">/</span>h1<span class="token operator">></span><br /> <span class="token operator"><</span>input<br /> ref<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span><span class="token parameter">ele</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"cb2"</span><span class="token punctuation">,</span> ele<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">}</span><br /> <span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>div<br /> ref<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span><span class="token parameter">ele</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"cb3"</span><span class="token punctuation">,</span> ele<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">}</span><br /> <span class="token operator">></span><span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span>refresh<span class="token punctuation">}</span><span class="token operator">></span>click<span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span>PassRef<br /> ref<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span><span class="token parameter">ele</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"cb4"</span><span class="token punctuation">,</span> ele<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">}</span><br /> <span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>ClassComp<br /> ref<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span><span class="token parameter">ins</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"cb5"</span><span class="token punctuation">,</span> ins<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">}</span><br /> <span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<h2 id="component-%E7%9A%84-ref"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/react-ref-reference/#component-%E7%9A%84-ref">#</a> Component 的 ref</h2>
<p>剛剛提到的兩種用法是</p>
<ol>
<li>ref 綁定在 DOM element 上,可以使用
<ol>
<li>object</li>
<li>callback</li>
</ol>
</li>
<li>ref 作為不會 rerender 的 mutable object</li>
</ol>
<p>那 ref 可以綁定在 Component Element 上嗎?可以的。</p>
<pre class="language-js"><code class="language-js"><br /><span class="token keyword">class</span> <span class="token class-name">Child</span> <span class="token keyword">extends</span> <span class="token class-name">Component</span> <span class="token punctuation">{</span><br /> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token parameter">props</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">super</span><span class="token punctuation">(</span>props<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>addCount <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">addCount</span><span class="token punctuation">.</span><span class="token function">bind</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>state <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token literal-property property">count</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">}</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <br /> <span class="token function">addCount</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">setState</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span>count<span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token literal-property property">count</span><span class="token operator">:</span> count <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><span class="token operator"><</span>div<span class="token operator">></span><br /> <span class="token operator"><</span>div<span class="token operator">></span>counter<span class="token operator">:</span> <span class="token punctuation">{</span><span class="token keyword">this</span><span class="token punctuation">.</span>state<span class="token punctuation">.</span>count<span class="token punctuation">}</span><span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span> <br /> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> ref<span class="token operator">=</span> <span class="token function">useRef</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <br /> <span class="token keyword">const</span> <span class="token function-variable function">addConntFromParent</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>ref<span class="token punctuation">.</span>current<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> ref<span class="token punctuation">.</span>current<span class="token punctuation">.</span><span class="token function">addCount</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /> <br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span>div<span class="token operator">></span><br /> <span class="token operator"><</span>Child ref<span class="token operator">=</span><span class="token punctuation">{</span>ref<span class="token punctuation">}</span><span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span>addConntFromParent<span class="token punctuation">}</span><span class="token operator">></span>add count<span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>透過 ref,我們可以拿到 <code>ClassComp</code> 的 instance 本身,而且還可以透過 instance 來操作 <code>ClassComp</code>,換句話說,我們可以在 Parent 操作 child component 內部的狀態。</p>
<p>function component 也可以做到這點,但是要透過 <code>useImperativeHandle</code> 這個 API。如果用上面那個範例但是改成 function component 的話:</p>
<pre class="language-js"><code class="language-js"><br /><span class="token keyword">const</span> <span class="token function-variable function">Child</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span>passRef<span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>state<span class="token punctuation">,</span> setState<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <br /> <span class="token function">useImperativeHandle</span><span class="token punctuation">(</span>passRef<span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token function-variable function">addCount</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><span class="token function">setState</span><span class="token punctuation">(</span><span class="token parameter">state</span> <span class="token operator">=></span> state <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><br /> <br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><span class="token operator"><</span>div<span class="token operator">></span><br /> <span class="token operator"><</span>div<span class="token operator">></span>counter<span class="token operator">:</span> <span class="token punctuation">{</span>state<span class="token punctuation">}</span><span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span> <br /> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> ref<span class="token operator">=</span> <span class="token function">useRef</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <br /> <span class="token keyword">const</span> <span class="token function-variable function">addCountFromParent</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>ref<span class="token punctuation">.</span>current<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> ref<span class="token punctuation">.</span>current<span class="token punctuation">.</span><span class="token function">addCount</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /> <br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span>div<span class="token operator">></span><br /> <span class="token operator"><</span>Child passRef<span class="token operator">=</span><span class="token punctuation">{</span>ref<span class="token punctuation">}</span><span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span>addCountFromParent<span class="token punctuation">}</span><span class="token operator">></span>add count<span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>因為 function component 本身不像 class component 一樣能夠產生 instance 儲存狀態,我在我們需要透過 useImperativeHandle,並透過第二個參數的 function 建立一個 instance 給 parent 的 ref。</p>
<p>從這點就可以看到,我們可以自由決定需要公開的介面給 parent,這點比起 class component 完全公開 instance 內部的 property / method 來的更加安全,也帶有一點物件導向的味道。</p>
<p>但是在上面的範例可以看到我們的 <code>Child</code> 接收的是 passRef 這個 props 而不是 ref。原因是如果直接使用 ref 的話在預設狀況是會如 class component 的行為一樣綁定 instance 到 ref 上面。但是 function component 和 class component 處理方式不同,在這個部分會報錯。</p>
<pre><code>Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
</code></pre>
<p>所以上面的範例才會使用 <code>passRef</code> 而不能直接使用 ref 來傳送進去 component。因此我們會需要 React .forwardRef 讓 function component 也能夠送 ref 進去。</p>
<pre class="language-js"><code class="language-js"><br /><span class="token keyword">const</span> Child <span class="token operator">=</span> React<span class="token punctuation">.</span><span class="token function">forwardRef</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">props<span class="token punctuation">,</span> ref</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>state<span class="token punctuation">,</span> setState<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <br /> <span class="token function">useImperativeHandle</span><span class="token punctuation">(</span>ref<span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token function-variable function">addCount</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><span class="token function">setState</span><span class="token punctuation">(</span><span class="token parameter">state</span> <span class="token operator">=></span> state <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><br /> <br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><span class="token operator"><</span>div<span class="token operator">></span><br /> <span class="token operator"><</span>div<span class="token operator">></span>counter<span class="token operator">:</span> <span class="token punctuation">{</span>state<span class="token punctuation">}</span><span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span> <br /> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> ref<span class="token operator">=</span> <span class="token function">useRef</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <br /> <span class="token keyword">const</span> <span class="token function-variable function">addCountFromParent</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>ref<span class="token punctuation">.</span>current<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> ref<span class="token punctuation">.</span>current<span class="token punctuation">.</span><span class="token function">addCount</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /> <br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span>div<span class="token operator">></span><br /> <span class="token operator"><</span>Child ref<span class="token operator">=</span><span class="token punctuation">{</span>ref<span class="token punctuation">}</span><span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span>addCountFromParent<span class="token punctuation">}</span><span class="token operator">></span>add count<span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>使用了 forwardRef 這個 HOC 包住 function component 之後,就能用第二個參數來接收 ref,這樣我們可以把 ref 傳進去給 useImperativeHandle 使用了。</p>
<h2 id="%E7%B8%BD%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/react-ref-reference/#%E7%B8%BD%E7%B5%90">#</a> 總結</h2>
<p>說實在這篇文章的內容幾乎都是文件上面有的東西,只是和 Ref 有關的 API 不少,再加上相關的資訊散佈再放在文件的各處,就對 Ref 比較難有全面的了解。</p>
<p>這篇看起來落落長,不過對於 Ref 這裡這樣理解:</p>
<ul>
<li>是建立一個 mutable 的 object
<ul>
<li><code>React.createRef</code>:使用在 class component</li>
<li><code>useRef</code>:使用在 function component</li>
</ul>
</li>
<li>React element(包含 DOM、Function component 和 Class component)上的 ref 屬性
<ul>
<li>可以說是 component 在 mount / unmount 的 callback
<ul>
<li>在帶入 function時,會在 mount 時執行 <code>callbackRef(node)</code>,而在 unmount 時執行 <code>callbackRef(null)</code>。
<ul>
<li>如果 rerender 前後是不同的 callback ref function,會進行 cleanup 執行 callback function。</li>
<li>DOM element 的 node 會是 element 本身</li>
<li>Function component 的 node 會是 useImparativeHandle 第二個參數的 return 值</li>
</ul>
</li>
<li>在帶入 object 時,會自動執行 <code>(ele) => {object.current = ele}</code>。</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="%E4%B8%80%E4%BA%9B%E5%A5%87%E6%80%AA%E7%9A%84%E7%99%BC%E7%8F%BE%E5%92%8C%E7%8C%9C%E6%83%B3"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/react-ref-reference/#%E4%B8%80%E4%BA%9B%E5%A5%87%E6%80%AA%E7%9A%84%E7%99%BC%E7%8F%BE%E5%92%8C%E7%8C%9C%E6%83%B3">#</a> 一些奇怪的發現和猜想</h2>
<p>在研究 ref 的時候做了些實驗,不過以目前對 react 的理解也還沒辦法解釋,或者有些東西目前只能做猜想沒辦法做證實(要證實只能看 source code 了,但目前看不太懂),所以就放在這邊,如果讀者們能夠解釋或者有想法的話歡迎提點或者是討論。</p>
<h3 id="%E9%97%9C%E6%96%BC-callback-ref-%E5%92%8C-component-%E6%9C%AC%E8%BA%AB-lifecycle-%E7%9A%84%E9%A0%86%E5%BA%8F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/react-ref-reference/#%E9%97%9C%E6%96%BC-callback-ref-%E5%92%8C-component-%E6%9C%AC%E8%BA%AB-lifecycle-%E7%9A%84%E9%A0%86%E5%BA%8F">#</a> 關於 callback ref 和 component 本身 lifecycle 的順序</h3>
<p>我們把 給予 ref 的 Component 稱作 Parent,而接受 ref 的稱作 Child。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">Parent</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span>div<span class="token operator">></span><br /> <span class="token operator"><</span>FuncChild<span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>ClassChild<span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><br /> <span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /></code></pre>
<p>我們都知道不管是 function component 還是 class component 本身都有所謂的 lifecycle / effect,會在 component 本身 render 結束之後被執行。自己蠻好奇說這些 lifecycle 和 callback ref 之間的執行順序是怎麼樣的,所以寫了下面這個實驗:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> FuncChild <span class="token operator">=</span> React<span class="token punctuation">.</span><span class="token function">fowardRef</span><span class="token punctuation">(</span><span class="token punctuation">(</span>_<span class="token punctuation">,</span> ref<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token function">useEffect</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"func effect"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token function">useLayoutEffect</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"func layoutEffect"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /><br /> <span class="token function">useImperativeHandle</span><span class="token punctuation">(</span>ref<span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'func useImperativeHandle'</span><span class="token punctuation">)</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">"name"</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span> <span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">return</span> <span class="token operator"><</span>h1<span class="token operator">></span>FuncComp<span class="token operator"><</span><span class="token operator">/</span>h1<span class="token operator">></span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">class</span> <span class="token class-name">ClassChild</span> <span class="token keyword">extends</span> <span class="token class-name">Component</span> <span class="token punctuation">{</span><br /> <span class="token function">componentDidMount</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"class componentDidMount"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token operator"><</span>h1<span class="token operator">></span>ClassComp<span class="token operator"><</span><span class="token operator">/</span>h1<span class="token operator">></span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span>div ref<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"div ref"</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token operator">></span><br /> <span class="token operator"><</span>ClassChild<br /> ref<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"class ref"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">}</span><br /> <span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>FuncChild<br /> ref<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"func ref"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">}</span><br /> <span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<pre><code>class componentDidMount
class ref
func layout effect
func useImpaertive
func ref
div ref
func effect
</code></pre>
<p>首先比較可以理解的是各個 element 的 ref 被 print 出來的順序:</p>
<ol>
<li>class component</li>
<li>function comonent</li>
<li>div</li>
</ol>
<p>class component 在 function component 前,所以這個順序理所當然。而因為 div 在外層,所以會等 children 被 render 完之後才會被 mount 上去,在最後很合理。</p>
<p>再來是 class component 的 ref 和 lifecycle 的順序。對於 class component 太不熟了,而且也蠻懶得去找資料,但可以理解的是會先執行完 component 內部的 lifecycle,然後才被 mount 上去 Parent component,所以才會是這樣的執行順序。</p>
<p>最後是 function component。第一點是 useLayoutEffect 的執行會在 ref 前面,layoutEffect 會在 DOM paint 後執行,這個部分覺得蠻合理的,而且文件也有提到 useLayoutEffect 觸發的時間點和 componentDidMount 相同。</p>
<p>最疑惑的是這點,useEffect 的觸發點在 callback ref 後面是可以預期的,會在 paint 之後。但這個結果代表說不只是在 component paint 之後,而是在整個 app 被重新 paint 之後(在 div ref 後面)。</p>
<p>這個研究大概就到這邊了,關於 useEffect 的執行時機點,Child / Parent 被 render 還有 paint 的順序,甚至還有 hook 是在什麼時間被執行的,這些概念目前似乎都很模糊,還需要更多理解。</p>
<h3 id="%E7%B4%94%E6%89%8B%E5%B7%A5-ref"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/react-ref-reference/#%E7%B4%94%E6%89%8B%E5%B7%A5-ref">#</a> 純手工 ref</h3>
<p>我們真的需要 createRef / useRef?</p>
<pre class="language-js"><code class="language-js"><br /><span class="token keyword">class</span> <span class="token class-name">ClassComp</span> <span class="token keyword">extends</span> <span class="token class-name">Component</span> <span class="token punctuation">{</span><br /> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token parameter">props</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">super</span><span class="token punctuation">(</span>props<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>ref <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">current</span><span class="token operator">:</span> <span class="token keyword">null</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>state <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token literal-property property">show</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>printRef <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">printRef</span><span class="token punctuation">.</span><span class="token function">bind</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>toggle <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">toggle</span><span class="token punctuation">.</span><span class="token function">bind</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token function">printRef</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>ref<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token function">toggle</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">setState</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> show <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">show</span><span class="token operator">:</span> <span class="token operator">!</span>show <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span>div<span class="token operator">></span><br /> <span class="token punctuation">{</span><span class="token keyword">this</span><span class="token punctuation">.</span>state<span class="token punctuation">.</span>show <span class="token operator">&&</span> <span class="token operator"><</span>h1 ref<span class="token operator">=</span><span class="token punctuation">{</span><span class="token keyword">this</span><span class="token punctuation">.</span>ref<span class="token punctuation">}</span><span class="token operator">></span><span class="token number">123</span><span class="token operator"><</span><span class="token operator">/</span>h1<span class="token operator">></span><span class="token punctuation">}</span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span><span class="token keyword">this</span><span class="token punctuation">.</span>printRef<span class="token punctuation">}</span><span class="token operator">></span>print<span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span><span class="token keyword">this</span><span class="token punctuation">.</span>toggle<span class="token punctuation">}</span><span class="token operator">></span>toggle<span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">const</span> ref <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">current</span><span class="token operator">:</span> <span class="token keyword">null</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">function</span> <span class="token function">FuncComp</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>show<span class="token punctuation">,</span> setShow<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token boolean">true</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> <span class="token function-variable function">toggle</span><span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token function">setShow</span><span class="token punctuation">(</span><span class="token parameter">state</span> <span class="token operator">=></span> <span class="token operator">!</span>state<span class="token punctuation">)</span> <span class="token punctuation">}</span><br /> <span class="token keyword">const</span> <span class="token function-variable function">printRef</span><span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>ref<span class="token punctuation">)</span> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span>div<span class="token operator">></span><br /> <span class="token punctuation">{</span>show <span class="token operator">&&</span> <span class="token operator"><</span>h1 ref<span class="token operator">=</span><span class="token punctuation">{</span>ref<span class="token punctuation">}</span><span class="token operator">></span>h1<span class="token operator"><</span><span class="token operator">/</span>h1<span class="token operator">></span><span class="token punctuation">}</span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span>printRef<span class="token punctuation">}</span><span class="token operator">></span>print ref<span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span>toggle<span class="token punctuation">}</span><span class="token operator">></span>toggle<span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>這兩個範例都是沒問題的,不論是在 <code>h1</code> 有沒有 show 的狀態都可以正常的表現 ref 為 element / null。那為什麼會需要這兩個 api?useRef 或者是 React.createRef 是不是有做一些其他的事情?可能需要再研究了。</p>
<h3 id="%E4%B8%8D%E4%BD%BF%E7%94%A8-useinperativehandle-%E4%BE%86%E5%81%9A%E7%B6%81%E5%AE%9A%E7%9A%84%E8%A9%B1%E6%9C%83%E6%80%8E%E9%BA%BC%E6%A8%A3%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/react-ref-reference/#%E4%B8%8D%E4%BD%BF%E7%94%A8-useinperativehandle-%E4%BE%86%E5%81%9A%E7%B6%81%E5%AE%9A%E7%9A%84%E8%A9%B1%E6%9C%83%E6%80%8E%E9%BA%BC%E6%A8%A3%EF%BC%9F">#</a> 不使用 useInperativeHandle 來做綁定的話會怎麼樣?</h3>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> Child <span class="token operator">=</span> <span class="token function">forwardRef</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">_<span class="token punctuation">,</span> ref</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>state<span class="token punctuation">,</span> setState<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token boolean">false</span><span class="token punctuation">)</span><br /> <span class="token keyword">const</span> <span class="token function-variable function">toggle</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token function">setState</span><span class="token punctuation">(</span><span class="token parameter">state</span> <span class="token operator">=></span> <span class="token operator">!</span>state<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> ref<span class="token punctuation">.</span>current <span class="token operator">=</span> toggle<span class="token punctuation">;</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"func"</span><span class="token punctuation">,</span> ref<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> <span class="token operator"><</span>p<span class="token operator">></span><span class="token punctuation">{</span>state <span class="token operator">+</span> <span class="token string">''</span><span class="token punctuation">}</span><span class="token operator"><</span><span class="token operator">/</span>p<span class="token operator">></span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">function</span> <span class="token function">Parent</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>_<span class="token punctuation">,</span> refresh<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> ref <span class="token operator">=</span> <span class="token function">useRef</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> <span class="token function-variable function">click</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>ref <span class="token punctuation">)</span> <span class="token punctuation">{</span> ref<span class="token punctuation">.</span><span class="token function">current</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br /> <span class="token function">useEffect</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>ref<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span>div<span class="token operator">></span><br /> <span class="token operator"><</span>Child ref<span class="token operator">=</span><span class="token punctuation">{</span>ref<span class="token punctuation">}</span> <span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span>refresh<span class="token punctuation">}</span><span class="token operator">></span>rerender<span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span>click<span class="token punctuation">}</span><span class="token operator">></span>toggle<span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p><s>到底是為什麼要一直不照文件的做</s>。用上面的作法,完全不使用 useImperativeHandle 這個 api,自己把 component 內的 setState 綁定到 <code>ref.current</code> 上面,這樣的用法也是沒問題的,不過也不清楚原因就是了。</p>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/react-ref-reference/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<p>其實也不過都是文件而已</p>
<ul>
<li><a href="https://reactjs.org/docs/hooks-reference.html">Hooks API Reference</a></li>
<li><a href="https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node">How can I measure a DOM node?</a></li>
<li><a href="https://reactjs.org/docs/react-api.html#reactforwardref">React.fowardRef</a></li>
<li><a href="https://reactjs.org/docs/refs-and-the-dom.html">Refs and the DOM</a></li>
<li><a href="https://reactjs.org/docs/forwarding-refs.html">Forwarding Refs</a></li>
</ul>
如何準確的 Google 到想要的內容
2021-09-28T00:00:00Z
https://blog.errorbaker.tw/posts/simon198/google-searching-tips/
<!-- summary -->
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/google-searching-tips/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<p>其實在之前轉職的路上就有聽說過工程師只是一群非常會 google 的猴子而已,當初也就只是笑笑的聽過去沒想太多。但後來發現不論是學習上或者工作上真的花了非常大量的時間在 google 上面搜尋各式各樣的東西。話雖如此,但其實我發現自己在搜尋東西的時候花了非常多的時間在看他吐給我但其實沒有那麼相關的搜尋結果,所以為了要從一隻猴子進化成那隻非常會 google 的猴子,決定來跟大家一起學習一下如何增進自己的 google 技巧。</p>
<!-- summary -->
<!-- more -->
<h3 id="%E5%AE%8C%E5%85%A8%E6%AF%94%E5%B0%8D"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/google-searching-tips/#%E5%AE%8C%E5%85%A8%E6%AF%94%E5%B0%8D">#</a> 完全比對</h3>
<blockquote>
<p>"搜尋內容"</p>
</blockquote>
<p>Google 為了盡可能地幫我們搜尋到我們想要的東西,所以在關鍵字搜尋的時候會用模糊比對的方式把結果吐給我們,但當我們搜尋某個錯誤訊息或是某個 bug 的解法的時候,大量的結果反兒會讓我們事倍功半,常常跑出來的是其他相似錯誤訊息的解法,這個時候如果用 <code>""</code> 來把想要搜尋的關鍵字包住的話就會變成完全比對,而不是像原本預設的模糊比對,可以在關鍵字明確的情況下提前過濾掉一些結果。</p>
<h3 id="%E7%89%B9%E5%AE%9A%E7%B6%B2%E7%AB%99%E6%90%9C%E5%B0%8B"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/google-searching-tips/#%E7%89%B9%E5%AE%9A%E7%B6%B2%E7%AB%99%E6%90%9C%E5%B0%8B">#</a> 特定網站搜尋</h3>
<blockquote>
<p>搜尋內容 site:在哪個網站搜尋</p>
</blockquote>
<p>這個比較常用在我們想要在某些網站中搜尋(例如 stack overflow, Github)或是一些搜尋功能做的不是很好的官方文件當中都蠻好用的。只要在我們搜尋的最後加上一個空白鍵之後打下 <code>site:想在哪個網站搜尋</code> ,這個時候搜尋結果就只會是那個網站裡面的內容了!</p>
<h3 id="%E6%8E%92%E9%99%A4%E7%89%B9%E5%AE%9A%E9%97%9C%E9%8D%B5%E5%AD%97"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/google-searching-tips/#%E6%8E%92%E9%99%A4%E7%89%B9%E5%AE%9A%E9%97%9C%E9%8D%B5%E5%AD%97">#</a> 排除特定關鍵字</h3>
<blockquote>
<p>想要搜尋的內容 -想要排除的關鍵字</p>
</blockquote>
<p>有的時候在搜尋不主流的套件或者是框架的時候,常常會有主流套件的一些內容來攪局,在看的時候都要花很多心力在排除那些主流的東西,費時又費力。這個時候其實只要在搜尋結果後面打一下 <code>-想排除的關鍵字</code> 就可以直接過濾掉含有那些關鍵字的搜尋結果啦!</p>
<h3 id="%E5%8D%80%E9%96%93"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/google-searching-tips/#%E5%8D%80%E9%96%93">#</a> 區間</h3>
<blockquote>
<p>想要搜尋的內容 after:開始數字<br />
想要搜尋的內容 before:結束數字<br />
想要搜尋的內容 區間起點..區間終點</p>
</blockquote>
<p>這個也滿實用的,因為常常我們在搜尋資料會出現很久以前資料,但這種資料又大部分都是過時的,所以如果在後面加上 <code>after:時間</code>,就可以有效的避免搜尋到太久以前的資料。反之,如果我們就是想要找很久以前的資料,就可以用 <code>before:時間</code>。最後是區間 <code>區間起點..區間終點</code>,這個區間其實不只可以幫我們搜尋時間,只要是兩個數字中間他都可以幫我們做搜尋(e.g. 價格)。</p>
<h3 id="*"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/google-searching-tips/#*">#</a> *</h3>
<blockquote>
<p>用 react 打造一個 *</p>
</blockquote>
<p>這個 <code>*</code> 其實就代表全部,上面那個例子搜尋的話,只要是 <code>用 react 打造 xxx</code> 裡面的 xxx 代表的是任何東西。這個其實也蠻好用的,因為有時候我們並沒有很詳細地知道自己想要搜尋什麼,這個時候加上一個 * 就可以讓我們從一個大方向開始看,慢慢的找到一些線索之後在縮小搜尋範圍,進而找到最終的答案。</p>
<h3 id="tags-%26-hashtags"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/google-searching-tips/#tags-%26-hashtags">#</a> tags & hashtags</h3>
<blockquote>
<p>搜尋內容 @social media<br />
搜尋內容 #hashtags</p>
</blockquote>
<p>這個好像比較不常用在寫程式的時候,但看起來其實蠻有趣的!不過在我認真搜尋過所有我認識的社群網站之後發現好像只有 <code>twitter</code> 跟 <code>instagram</code> 比較有效果,其他社群網站都會混雜著其他的網頁,所以算是一個補充,詳細的用法如果大家知道的話就幫忙補充一下 XD。</p>
<h3 id="%E7%B8%BD%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/google-searching-tips/#%E7%B8%BD%E7%B5%90">#</a> 總結</h3>
<p>說實在的這些功能全部都不用,只要多花一點時間還是可以找到自己想要的資料。但這些小功能不論是在寫程式的時候或是在其他搜尋的時候其實都可以變得更加有效率。</p>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/simon198/google-searching-tips/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li><a href="https://support.google.com/websearch/answer/35890">Google Search Help</a></li>
<li><a href="https://www.youtube.com/watch?v=cEBkvm0-rg0">How to "Google it" like a Senior Software Engineer</a></li>
</ul>
gRPC 和 HTTP/2 Streaming
2021-10-03T00:00:00Z
https://blog.errorbaker.tw/posts/cian/grpc-streaming-n-http2/
<!-- summary -->
<!-- gRPC(Remote Procedure Calls)是 Google 發起的一個開源的 RPC 系統。這篇文章簡單介紹 HTTP2 和 gRPC中的 streaming。 -->
<!-- summary -->
<p>嗨,我是 Cian。<br />
上個月初意外成為了一個前後端各半的半半工程師,過了一個月之後深深覺得這樣的生活不是我想要的,所以之後會回去專注在前端方面。雖然可以專心研究喜歡的前端很開心,但既然起了頭,這篇文章還是想做個收尾。另外,在上一篇文章之後,我才知道原來這 route 專案是官方的<a href="https://grpc.io/docs/languages/go/basics/">教學專案</a>,因此,我決定來看一下補充的內容,以及延續上一篇的內容繼續往剩下的三種方向看,以及提供一些相關的資源給有興趣的同學參考。</p>
<h2 id="http%2F2-%E5%92%8C-streaming-%E5%82%B3%E8%BC%B8"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-streaming-n-http2/#http%2F2-%E5%92%8C-streaming-%E5%82%B3%E8%BC%B8">#</a> HTTP/2 和 Streaming 傳輸</h2>
<p>在上一篇文章中有提到 gRPC 可以擁有 Unary、Client-side Streaming、Server-side Streaming 和 Bidirectional Streaming 四種生命週期,是受惠於 HTTP/2,這是因為在 HTTP/2 中有 Streaming 的技術,另外使用 bytecode 傳輸也是 HTTP/2 的特點。</p>
<h3 id="http%2F2-%E4%B9%8B%E5%89%8D%E2%80%94%E2%80%94spdy"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-streaming-n-http2/#http%2F2-%E4%B9%8B%E5%89%8D%E2%80%94%E2%80%94spdy">#</a> HTTP/2 之前——SPDY</h3>
<p>在 2009 年, google 發布了一個稱為 SPDY 的實驗性協議。他的主要目標是希望可以解決一些 HTTP/1.1 中的性能限制來縮短網頁載入時間。這個協議的其中一個目標是將頁面加載時間(PLT)減少 50%。<br />
想要讓頁面加載時間(PLT)減少 50%,就得知道什麼對頁面加載時間能造成巨大的影響,其中,SPDY 的創作者之一 Mike Belshe 的<a href="https://hpbn.co/primer-on-web-performance/#latency-as-a-performance-bottleneck">一個實驗結果</a>提供了一個方向。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/cian/grpc-streaming-n-http2/page-load-time-vs-bandwidth-and-latency.png" alt="" /></p>
<p>他研究了不同帶寬與延遲對頁面加載時間的影響。在這個實驗中,他發現當流量從 1Mbp 變成 2Mbps 的時候,頁面加載時間確實會減半,但隨後帶寬對加載時間的影響就會急速降低,當可用帶寬超過 5 Mbps 時,這個影響就會變得非常小。從 5 Mbps 升級到 10 Mbps 時,只讓頁面的加載時間縮短了 5%。</p>
<p>相對於帶寬對頁面加載時間的效率遞減狀況,延遲對頁面加載時間的影響影響就十分穩定,是線性的影響。也就是說,如果要對頁面加載時間進行改善,從改善延遲來思考可能會是比較有效率的途徑。</p>
<p>造成延遲的原因很多,像是 TCP 協議每次開始就必須進行的三次握手、丟失表頭會造成的 data packet 阻塞等等。這些都是底層協議的影響,也就是說,如果可以更有效率地利用底層協議,就可以達成這個優化。為了達成這件事,SPDY 引入了一個新的二進制的分流幀層,用來實現 request 和 response 複用、優先級以及壓縮標頭。這使得 TCP 的利用更有效率,從而實現更短的頁面加載時間。</p>
<h3 id="http%2F2"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-streaming-n-http2/#http%2F2">#</a> HTTP/2</h3>
<p>在 SPDY 發佈之後,這個協議逐漸被人接受,到了 2012 到了 2012 年,各個大瀏覽器都開始支援這樣技術,在這樣的潮流之下,HTTP 工作組 (HTTP-WG) 決定從中學習,並且提供正式的 HTTP/2 標準。經過許多討論之後,SPDY 規範被採納為新 HTTP/2 協議的起點。</p>
<p>2015 年初,IESG 審查並批准了新的 HTTP/2 標準發布。和 HTTP/1 相比,HTTP/2 可以用更少的 TCP 連結,進行更長時間的傳輸,這網路容量的利用率變高。由於過程中 SPDY 和 HTTP/2 的共同進化使服務器、瀏覽器和站點開發人員可以在新協議開發過程中獲得實際經驗。因此,HTTP/2 標準是在正式發布前經過最廣泛測試的標準之一,幾週之後就在真實世界有了不少實踐。</p>
<blockquote>
<p>關於實驗的參考資料在<a href="https://hpbn.co/primer-on-web-performance/#latency-as-a-performance-bottleneck">這裡</a>,關於優化 TCP 的資料在<a href="https://hpbn.co/building-blocks-of-tcp/#optimizing-for-tcp">這裡</a>,有興趣的朋友可以進一步閱讀。</p>
</blockquote>
<h3 id="stream-%E5%82%B3%E8%BC%B8"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-streaming-n-http2/#stream-%E5%82%B3%E8%BC%B8">#</a> Stream 傳輸</h3>
<p>HTTP/2 的所有通信都會在一個 TCP 連接上完成,這個連接可以承載任意數量的雙向 byte stream。每一個 byte stream 都會有一個唯一標示符,以及可選的優先級信息。<br />
在這個雙向 byte stream 中,又可以承載複數條 message,每一個 message 都是一個 HTTP message,像是 request 或 response。<br />
在這些 message 中,會包含一系列的 frames。frame 是最小的通信單位,裡面會有特定類型的數據、 HTTP header 以及 payload 等等。<br />
在一個 TCP 通訊中,由於可以同時可以有複數條 streams,所以 frame 中也會帶有 stream 的標示符,屬於不同 stream 的 frames 可以交互傳輸,之後再依據這些標示符重新組裝。</p>
<p>基於這樣新的傳輸結構,在 HTTP/2 上就達成了減少 TCP 連結和可以進行 Streaming 的功能。同時,因為這些傳輸使用 byte stream,gRPC 的開發中也就會需要在發送或接收請求時,進行編碼和解碼。</p>
<h2 id="streaming-rpc-%E5%8F%AF%E8%83%BD%E6%9C%83%E7%94%A8%E4%B8%8A%E7%9A%84%E4%B8%80%E4%BA%9B%E9%A3%9F%E8%AD%9C"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-streaming-n-http2/#streaming-rpc-%E5%8F%AF%E8%83%BD%E6%9C%83%E7%94%A8%E4%B8%8A%E7%9A%84%E4%B8%80%E4%BA%9B%E9%A3%9F%E8%AD%9C">#</a> Streaming RPC 可能會用上的一些食譜</h2>
<h3 id="%E5%9C%A8-server-%E7%99%BC%E9%80%81-stream"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-streaming-n-http2/#%E5%9C%A8-server-%E7%99%BC%E9%80%81-stream">#</a> 在 Server 發送 stream</h3>
<p>Server-side Streaming 是指 Server 收到一個 Client 的 request 時,會開啟一個 Stream 傳輸,並且回傳一個或多個 message response。<br />
在使用 Streaming 之前,可能的做法是讓 Server 去搜尋資料,接著把在範圍內的資料放到一個 response 中回傳給 User,這在數量少的資料中也許還好,但當資料的數量變多,Server 所需要的搜尋時間就會變長,而 Client 需要等待的時間也就越久。<br />
而其中,假設搜尋到第一筆資料只花了三秒,但全部完成搜尋花了一分鐘,所有先搜尋到的結果就必須一起等到搜尋完成,十分浪費時間。<br />
Server-side Streaming 的做法則是每找到一筆資料就進行回傳,可以降低中間乾等的時間,讓 Client 可以更快拿到資料並進行處理。</p>
<p>可以在一個 for loop 中使用 <code>stream.Send(item)</code> 在一個 stream 中回傳多個東西。</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">func</span> <span class="token punctuation">(</span>s <span class="token operator">*</span>routeGuideServer<span class="token punctuation">)</span> <span class="token function">ListFeatures</span><span class="token punctuation">(</span>rectangle <span class="token operator">*</span>pb<span class="token punctuation">.</span>Rectangle<span class="token punctuation">,</span> stream pb<span class="token punctuation">.</span>RouteGuide_ListFeaturesServer<span class="token punctuation">)</span> <span class="token builtin">error</span> <span class="token punctuation">{</span><br /> <span class="token keyword">for</span> <span class="token boolean">_</span><span class="token punctuation">,</span> feature <span class="token operator">:=</span> <span class="token keyword">range</span> s<span class="token punctuation">.</span>features <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token function">inRange</span><span class="token punctuation">(</span>feature<span class="token punctuation">.</span>Location<span class="token punctuation">,</span> rectangle<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// 使用 stream.Send(feature) 回傳 feature</span><br /> <span class="token keyword">if</span> err <span class="token operator">:=</span> stream<span class="token punctuation">.</span><span class="token function">Send</span><span class="token punctuation">(</span>feature<span class="token punctuation">)</span><span class="token punctuation">;</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> err<br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> <span class="token boolean">nil</span><br /><span class="token punctuation">}</span></code></pre>
<h3 id="client-%E6%83%B3%E7%9F%A5%E9%81%93-stream-%E7%B5%90%E6%9D%9F%E7%9A%84%E6%99%82%E5%80%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-streaming-n-http2/#client-%E6%83%B3%E7%9F%A5%E9%81%93-stream-%E7%B5%90%E6%9D%9F%E7%9A%84%E6%99%82%E5%80%99">#</a> Client 想知道 stream 結束的時候</h3>
<p>接收 stream 的時候,需要知道什麼時候 stream 結束了。<br />
這時可以使用 <code>io.EOF</code> 來處理。</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">for</span> <span class="token punctuation">{</span> <br /> <span class="token comment">// 這個 serverStream 是 Client 發 request 時接收到的 response,我們放到 serverStream 裡面</span><br /> feature<span class="token punctuation">,</span> err <span class="token operator">:=</span> serverStream<span class="token punctuation">.</span><span class="token function">Recv</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span><br /> <span class="token keyword">if</span> err <span class="token operator">=</span> io<span class="token punctuation">.</span>EOF <span class="token punctuation">{</span><br /> <span class="token keyword">break</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> log<span class="token punctuation">.</span><span class="token function">Fatalln</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<h3 id="client-%E7%99%BC%E9%80%81-stream"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-streaming-n-http2/#client-%E7%99%BC%E9%80%81-stream">#</a> Client 發送 stream</h3>
<p>Client Side Streaming 是指 Client 不斷回傳 message,而 Server 則在整個 Stream 結束之後,回傳一個 message response。<br />
在範例專案中,使用者將他所經過的點透過 Stream 不斷回傳。而 Server 則在整個 stream 結束之後,回傳一個路徑的總結。</p>
<p>Client 和 Server 端一樣是在 for loop 裡面用 clientStream 發送。</p>
<pre class="language-go"><code class="language-go">clientStream<span class="token punctuation">,</span> err <span class="token operator">:=</span> client<span class="token punctuation">.</span><span class="token function">RecordRoute</span><span class="token punctuation">(</span>context<span class="token punctuation">.</span><span class="token function">Background</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br /><br /><span class="token comment">// 這裡有一些 error handling...</span><br /><br /><span class="token keyword">for</span> <span class="token boolean">_</span><span class="token punctuation">,</span> point <span class="token operator">:=</span> <span class="token keyword">range</span> points <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> err <span class="token operator">:=</span> clientStream<span class="token punctuation">.</span><span class="token function">Send</span><span class="token punctuation">(</span>point<span class="token punctuation">)</span><span class="token punctuation">;</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> log<span class="token punctuation">.</span><span class="token function">Fatalln</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<h3 id="client-%E7%B5%90%E6%9D%9F%E7%99%BC%E9%80%81-stream"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-streaming-n-http2/#client-%E7%B5%90%E6%9D%9F%E7%99%BC%E9%80%81-stream">#</a> Client 結束發送 stream</h3>
<p>當 Client 端發送 Stream 的時候我們需要調用 <code>CloseAndRecv()</code> 來關閉這個 stream 以讓 gRPC 知道我們已完成寫入,並且接受來自 Server 的 response。</p>
<pre class="language-go"><code class="language-go">clientStream<span class="token punctuation">,</span> err <span class="token operator">:=</span> client<span class="token punctuation">.</span><span class="token function">RecordRoute</span><span class="token punctuation">(</span>context<span class="token punctuation">.</span><span class="token function">Background</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br /><br /><span class="token comment">// 這裡有一些 Client 發送 Streaming 的 code</span><br /><span class="token comment">// 還有一些 error handling...</span><br /><br /><span class="token comment">// Streaming 的內容寫完了,要來關掉這個 stream</span><br /><span class="token comment">// 並且我們把 server 最後的回傳值放進 summary </span><br />summary<span class="token punctuation">,</span> err <span class="token operator">:=</span> clientStream<span class="token punctuation">.</span><span class="token function">CloseAndRecv</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> log<span class="token punctuation">.</span><span class="token function">Fatalln</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br />fmt<span class="token punctuation">.</span><span class="token function">Println</span><span class="token punctuation">(</span>summary<span class="token punctuation">)</span><br /> </code></pre>
<h3 id="server-%E6%8E%A5%E6%94%B6%E5%8F%8A%E7%B5%90%E6%9D%9F%E6%8E%A5%E6%94%B6-stream"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-streaming-n-http2/#server-%E6%8E%A5%E6%94%B6%E5%8F%8A%E7%B5%90%E6%9D%9F%E6%8E%A5%E6%94%B6-stream">#</a> Server 接收及結束接收 stream</h3>
<p>在 Server 端接收 stream 的寫法基本上和 Client 是差不多的。只是當收到來自 Client 的完成訊息時,Server 端會需要發送一個結束的 response 回去給 Client。這時我們需要使用 <code>SendAndClose()</code> 來完成這件事。</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">for</span> <span class="token punctuation">{</span><br /> point<span class="token punctuation">,</span> err <span class="token operator">:=</span> stream<span class="token punctuation">.</span><span class="token function">Recv</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">if</span> err <span class="token operator">==</span> io<span class="token punctuation">.</span>EOF <span class="token punctuation">{</span><br /> <span class="token comment">// 這裡知道 gRPC 結束了,所以要回傳一個 summary</span><br /> endTime <span class="token operator">:=</span> time<span class="token punctuation">.</span><span class="token function">Now</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">return</span> stream<span class="token punctuation">.</span><span class="token function">SendAndClose</span><span class="token punctuation">(</span><span class="token operator">&</span>pb<span class="token punctuation">.</span>RouteSummary<span class="token punctuation">{</span><br /> <span class="token comment">// 這裡是最後的內容們</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> err<br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<h3 id="%E8%A8%88%E7%AE%97%E5%9C%B0%E7%90%83%E4%B8%8A%E5%85%A9%E5%80%8B%E9%BB%9E%E7%9A%84%E8%B7%9D%E9%9B%A2"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-streaming-n-http2/#%E8%A8%88%E7%AE%97%E5%9C%B0%E7%90%83%E4%B8%8A%E5%85%A9%E5%80%8B%E9%BB%9E%E7%9A%84%E8%B7%9D%E9%9B%A2">#</a> 計算地球上兩個點的距離</h3>
<p>這是一個根據<a href="https://zh.wikipedia.org/zh-tw/%E5%8D%8A%E6%AD%A3%E7%9F%A2%E5%85%AC%E5%BC%8F">半正矢公式 (Haversine formula)</a> 寫成的函式。<br />
順帶一提,我看到這個函式時,他的來源說這使用了一個開源的內容,但根據內文連結並沒有真正找到那個源。</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">func</span> <span class="token function">toRadians</span><span class="token punctuation">(</span>num <span class="token builtin">float64</span><span class="token punctuation">)</span> <span class="token builtin">float64</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> num <span class="token operator">*</span> math<span class="token punctuation">.</span>Pi <span class="token operator">/</span> <span class="token function">float64</span><span class="token punctuation">(</span><span class="token number">180</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><br /><span class="token comment">// calcDistance calculates the distance between two points using the "haversine" formula.</span><br /><span class="token comment">// The formula is based on http://mathforum.org/library/drmath/view/51879.html.</span><br /><span class="token keyword">func</span> <span class="token function">calcDistance</span><span class="token punctuation">(</span>p1 <span class="token operator">*</span>pb<span class="token punctuation">.</span>Point<span class="token punctuation">,</span> p2 <span class="token operator">*</span>pb<span class="token punctuation">.</span>Point<span class="token punctuation">)</span> <span class="token builtin">int32</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> CordFactor <span class="token builtin">float64</span> <span class="token operator">=</span> <span class="token number">1e7</span><br /> <span class="token keyword">const</span> R <span class="token operator">=</span> <span class="token function">float64</span><span class="token punctuation">(</span><span class="token number">6371000</span><span class="token punctuation">)</span> <span class="token comment">// earth radius in metres</span><br /> lat1 <span class="token operator">:=</span> <span class="token function">toRadians</span><span class="token punctuation">(</span><span class="token function">float64</span><span class="token punctuation">(</span>p1<span class="token punctuation">.</span>Latitude<span class="token punctuation">)</span> <span class="token operator">/</span> CordFactor<span class="token punctuation">)</span><br /> lat2 <span class="token operator">:=</span> <span class="token function">toRadians</span><span class="token punctuation">(</span><span class="token function">float64</span><span class="token punctuation">(</span>p2<span class="token punctuation">.</span>Latitude<span class="token punctuation">)</span> <span class="token operator">/</span> CordFactor<span class="token punctuation">)</span><br /> lng1 <span class="token operator">:=</span> <span class="token function">toRadians</span><span class="token punctuation">(</span><span class="token function">float64</span><span class="token punctuation">(</span>p1<span class="token punctuation">.</span>Longitude<span class="token punctuation">)</span> <span class="token operator">/</span> CordFactor<span class="token punctuation">)</span><br /> lng2 <span class="token operator">:=</span> <span class="token function">toRadians</span><span class="token punctuation">(</span><span class="token function">float64</span><span class="token punctuation">(</span>p2<span class="token punctuation">.</span>Longitude<span class="token punctuation">)</span> <span class="token operator">/</span> CordFactor<span class="token punctuation">)</span><br /> dlat <span class="token operator">:=</span> lat2 <span class="token operator">-</span> lat1<br /> dlng <span class="token operator">:=</span> lng2 <span class="token operator">-</span> lng1<br /><br /> a <span class="token operator">:=</span> math<span class="token punctuation">.</span><span class="token function">Sin</span><span class="token punctuation">(</span>dlat<span class="token operator">/</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token operator">*</span>math<span class="token punctuation">.</span><span class="token function">Sin</span><span class="token punctuation">(</span>dlat<span class="token operator">/</span><span class="token number">2</span><span class="token punctuation">)</span> <span class="token operator">+</span><br /> math<span class="token punctuation">.</span><span class="token function">Cos</span><span class="token punctuation">(</span>lat1<span class="token punctuation">)</span><span class="token operator">*</span>math<span class="token punctuation">.</span><span class="token function">Cos</span><span class="token punctuation">(</span>lat2<span class="token punctuation">)</span><span class="token operator">*</span><br /> math<span class="token punctuation">.</span><span class="token function">Sin</span><span class="token punctuation">(</span>dlng<span class="token operator">/</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token operator">*</span>math<span class="token punctuation">.</span><span class="token function">Sin</span><span class="token punctuation">(</span>dlng<span class="token operator">/</span><span class="token number">2</span><span class="token punctuation">)</span><br /> c <span class="token operator">:=</span> <span class="token number">2</span> <span class="token operator">*</span> math<span class="token punctuation">.</span><span class="token function">Atan2</span><span class="token punctuation">(</span>math<span class="token punctuation">.</span><span class="token function">Sqrt</span><span class="token punctuation">(</span>a<span class="token punctuation">)</span><span class="token punctuation">,</span> math<span class="token punctuation">.</span><span class="token function">Sqrt</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token operator">-</span>a<span class="token punctuation">)</span><span class="token punctuation">)</span><br /><br /> distance <span class="token operator">:=</span> R <span class="token operator">*</span> c<br /> <span class="token keyword">return</span> <span class="token function">int32</span><span class="token punctuation">(</span>distance<span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<hr />
<h4 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/grpc-streaming-n-http2/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h4>
<p><a href="https://grpc.io/docs/what-is-grpc/introduction/">Introduction to gRPC | gRPC</a><br />
<a href="https://www.grpc.io/docs/languages/go/basics/#generating-client-and-server-code">Basics tutorial | Go | gRPC</a><br />
<a href="https://www.bilibili.com/video/BV1DV411s7ij">神奇代码在哪里Go实战#9:gRPC</a><br />
<a href="https://developers.google.com/web/fundamentals/performance/http2#%E6%95%B0%E6%8D%AE%E6%B5%81%E4%BC%98%E5%85%88%E7%BA%A7">HTTP/2 简介 | Web Fundamentals | Google Developers</a><br />
<a href="https://hpbn.co/building-blocks-of-tcp/#optimizing-for-tcp">Networking 101: Building Blocks of TCP - High Performance Browser Networking (O'Reilly)</a><br />
<a href="https://hpbn.co/primer-on-web-performance/#latency-as-a-performance-bottleneck">HTTP: Primer on Web Performance - High Performance Browser Networking (O'Reilly)</a></p>
<hr />
<p>這篇文章在我工作轉專案、換部署以及第一次在日本搬家的三重夾擊中寫成,內容恐多有不備,但遺憾短期內恐怕也不會有時間來補足。<br />
如果不幸文章有一些錯誤,請不吝指正,我會盡可能將修正意見補進內文中。<br />
非常感謝各位的閱讀,之後會主要寫前端相關內容,也請多多支持!感謝~</p>
<p>全文同步刊載於個人部落格 <a href="https://keronscribe.tw/grpc-he-http2-streaming">gRPC 和 HTTP2 Streaming</a></p>
再看 D3.js
2021-10-03T00:00:00Z
https://blog.errorbaker.tw/posts/minw/d3-intro/
<!-- summary -->
<p>從以前就很喜歡資料視覺化,經過半年比較沒有接觸到數據相關的服務,終於在近期一口氣做好做滿,趁著機會整理一下之前寫過對於資料視覺化的想法,把關於開發的部分補上,作為 D3 使用的紀錄。</p>
<!-- summary -->
<!-- more -->
<h2 id="%E9%97%9C%E6%96%BC%E8%B3%87%E6%96%99%E8%A6%96%E8%A6%BA%E5%8C%96"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/d3-intro/#%E9%97%9C%E6%96%BC%E8%B3%87%E6%96%99%E8%A6%96%E8%A6%BA%E5%8C%96">#</a> 關於資料視覺化</h2>
<p>生活中充斥著資料,但是看看這些資料原始的樣貌:看到了一堆數字,但沒有任何的「感覺」,甚至覺得頭很大,很難在一秒之內找到最大的數字、最小的數字、也不知道變化多大、甚至要從裡面看看這個資料是對是錯都很困難。</p>
<p>為了讓這些資料不要躺著生灰塵、可以讓人們可以更方便的討論、理解它們,有人先從花時間用表格替這些資料對齊、分類變得更好閱讀,到後來大家找到更好的方法:把這些資料變成有助於人們看懂的圖。</p>
<p>資料視覺化就是在把這些「象徵」轉換成「圖像」的過程,這件事是在任何媒體上都可以發生的事情。</p>
<p>從目的上,這件事可以沒有特別的功能性,以藝術表現為主,像是 Generative Art,也可以為了傳遞資訊來設計,像是狹義的資料視覺化,產生各種圖表。甚至這兩者本身的界線就不是這麼明確,例如:<a href="https://variable.io/">Variable 工作室</a> 的 Data Art 作品。或我不要視覺化,我要「物體化」!沒問題,當然可以在實體物體上發生,像是:Data Physicalization [^5]。</p>
<p>但一般印象裡,圖像是連續的、一體的,資料是離散的、分開的,這兩件事要怎麼轉換在一起?</p>
<p>除了思考資料與圖像的轉換外,還需要的是透過程式把資料轉換成網站可以懂的語言。剎那間,有各式各樣的工具百花齊放,例如:Highchart.js, Chart.js ... 它們都是從更高層的 function 來打包各種圖表,意思類似於:你呼叫一個 function 設定一些參數、就跑出基於網頁元素的一張圖。</p>
<p>這樣很方便沒有錯,但這樣距離原本轉換的概念太遙遠了。如果要客製化、還要學習新套件的標準而且也沒有效率,而 D3 的美學在於,我們可以基於我們對 DOM 的理解、直接控制 DOM 元素,直接對應了一般對資料視覺化的想像。</p>
<h2 id="%E8%B3%87%E6%96%99%E8%B7%9F%E5%9C%96%E5%83%8F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/d3-intro/#%E8%B3%87%E6%96%99%E8%B7%9F%E5%9C%96%E5%83%8F">#</a> 資料跟圖像</h2>
<p>既然圖像是完整的那就分解它,既然資料是分散的那就聚合它,前人替我們將視覺分析成一系列的元素,並替資料們分了幾大類,於是乎這兩件事就可以對應在一起了 [^3]。</p>
<h3 id="visual-encoding"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/d3-intro/#visual-encoding">#</a> Visual Encoding</h3>
<p><img src="https://www.oreilly.com/library/view/designing-data-visualizations/9781449314774/httpatomoreillycomsourceoreillyimages898026.png" alt="" /></p>
<p>Visual Encoding 將圖像分解,拆解可以由點到面,分別可以有尺寸、形狀、色相、飽和度、亮度及方向上的變化,更進階的我們可以讓視覺元素重複,便呈現出了樣式、或調整視覺元素與週遭的關係。</p>
<p>但當將圖像拆解到基本組成時,會發現各種圖像都有其特質,例如:線條可以有粗細、方向性、樣式,適合呈現關聯性、粗細可以表達等級,但不適合用來呈現類別,這創造了無限多種適合與不適合的元素去對應數據。</p>
<h3 id="data-type"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/d3-intro/#data-type">#</a> Data Type</h3>
<p>同樣的,分散的資料可以聚合成最基本幾種分類 [^4]:</p>
<ul>
<li>類別 (nominal):這種資料之間沒有數值關係、也沒有順序關係,只是項目之間有不同,例如:性別、宗教。</li>
<li>等級 (ordinal):這類型的資料之間有順序關係,但不是基於數值,也不能運算,例如:不同意、同意、非常同意。</li>
<li>等距 (interval):這類型的資料之間有順序、也有實質數據上的意義,但不能代表真實世界的意義,只是一種指標,例如:溫度(0 度不等於沒有溫度)。</li>
<li>等比 (ratio):這類型的資料不但有順序、有數據上的意義、也有真實世界的意義,例如:人數、收入、年齡。</li>
</ul>
<p>資料看起來只有上述四種,卻已經足夠複雜,因為我們可以將許多數據疊加在同一個圖表上,</p>
<p>而資料視覺化就是在挑戰資料與視覺之間的可能性的一個領域,了解什麼樣的轉換最能達成不同的目的?並隨著有越來越多元的資料被揭露出來,從無意義的資料、到有意義的、數據分析、預測...,搭配更多的呈現手法,例如:動畫、互動、甚至走向沈浸 VR... 資料視覺化依舊是一個欣欣向榮的領域。</p>
<h2 id="d3-%E6%80%8E%E9%BA%BC%E5%81%9A%E5%88%B0%E7%9A%84"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/d3-intro/#d3-%E6%80%8E%E9%BA%BC%E5%81%9A%E5%88%B0%E7%9A%84">#</a> D3 怎麼做到的</h2>
<p>講了許許多多的資料視覺化,回歸到我們焦點 - 網站怎麼實現這檔事?</p>
<p>除了思考資料與圖像的轉換外,還需要的是透過程式把資料轉換成網站可以懂的語言。剎那間,有各式各樣的工具百花齊放,例如:Highchart, Chart ... 它們都是從更高層的 function 來打包各種圖表,意思類似於:你呼叫一個 function 設定一些參數、就跑出基於網頁元素的一張圖:</p>
<p>這樣很方便沒有錯,但這樣距離原本轉換的概念太遙遠了。如果要客製化、還要學習新套件的標準而且也沒有效率,而 D3 的美學在於,我們可以基於我們對 DOM 的理解、直接控制 DOM 元素,直接對應了一般對資料視覺化的想像:</p>
<p>D3 透過四個系列工具來處理資料的轉換:</p>
<ul>
<li>Data:將資料轉換成 D3 Modules 需要的形式。</li>
<li>Modules:將資料轉成 SVG 需要的內容。</li>
<li>Selections:將轉換後的資料塞進 DOM 元素之中。</li>
<li>Interaction and Animation:透過時間跟互動來更新資料的變化。</li>
</ul>
<p>而這是 2011 年時 D3 提出時的架構 [^1],回到 2021 的現代,前端框架已經流行了好幾年,去年很幸運看到 Shirley Xu 在 React + D3 的分享 [^2],從此打破了我過去看到大多數教學推薦的 D3 使用方法,也讓我開始更容易了解 D3 在做什麼。</p>
<h2 id="%E5%9C%A8%E9%96%8B%E5%A7%8B-d3-%E4%B9%8B%E5%89%8D"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/d3-intro/#%E5%9C%A8%E9%96%8B%E5%A7%8B-d3-%E4%B9%8B%E5%89%8D">#</a> 在開始 D3 之前</h2>
<p>約略知道了 D3 的轉換過程,會發現這個過程中對 SVG 的了解很重要,因為最終轉換會轉成 SVG Element,所以要了解我們轉換的結果要如何對應到 SVG 之中,不過 SVG 的說明很瑣碎,所以接下來的說明不會再贅述關於 svg, rect, path, line 等等的不同跟使用方法,所幸 SVG 的使用也很直覺,可以直接參考 MDN 的介紹。</p>
<h3 id="d3-original-%3A-data-in%2C-data-out"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/d3-intro/#d3-original-%3A-data-in%2C-data-out">#</a> D3 original : Data In, Data Out</h3>
<p>接下來就先從最常見的,利用 D3 內建的 function 來看轉換的過程是怎麼成立的,以下是一個很簡單的數據:</p>
<pre class="language-csv"><code class="language-csv"><span class="token value">category</span><span class="token punctuation">,</span><span class="token value">value</span><br /><span class="token value">1</span><span class="token punctuation">,</span><span class="token value">12394</span><br /><span class="token value">2</span><span class="token punctuation">,</span><span class="token value">6148</span><br /><span class="token value">3</span><span class="token punctuation">,</span><span class="token value">1653</span><br /><span class="token value">4</span><span class="token punctuation">,</span><span class="token value">2162</span><br /><span class="token value">5</span><span class="token punctuation">,</span><span class="token value">1214</span></code></pre>
<p>這是一個有五筆的 category 對應 value 的數據,category 屬於類別資料,value 可能屬於等比或等距資料,視資料含義而定。 接著我們要使用上面的數據建立一個最簡單的 Bar Chart,在建立轉換之前,我們要做的是範圍對範圍的轉換,簡言之,定義數據跟圖之間的縮放關係,讓我們知道數字的值會讓圖像的座標、長寬轉換成對應畫布上的多少:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> margin <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token literal-property property">top</span><span class="token operator">:</span> <span class="token number">30</span><span class="token punctuation">,</span> <span class="token literal-property property">right</span><span class="token operator">:</span> <span class="token number">30</span><span class="token punctuation">,</span> <span class="token literal-property property">bottom</span><span class="token operator">:</span> <span class="token number">70</span><span class="token punctuation">,</span> <span class="token literal-property property">left</span><span class="token operator">:</span> <span class="token number">60</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> height <span class="token operator">=</span> <span class="token number">300</span><br /><span class="token keyword">const</span> width <span class="token operator">=</span> <span class="token number">600</span><br /><span class="token keyword">const</span> xRange <span class="token operator">=</span> <span class="token punctuation">[</span>margin<span class="token punctuation">.</span>left<span class="token punctuation">,</span> width <span class="token operator">-</span> margin<span class="token punctuation">.</span>right<span class="token punctuation">]</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> yRange <span class="token operator">=</span> <span class="token punctuation">[</span>height <span class="token operator">-</span> margin<span class="token punctuation">.</span>bottom<span class="token punctuation">,</span> margin<span class="token punctuation">.</span>top<span class="token punctuation">]</span><span class="token punctuation">;</span></code></pre>
<p>首先先定義最後要轉換出來的畫布的邊界大小。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> xExtent <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">,</span> data<span class="token punctuation">.</span>length<span class="token punctuation">]</span><br /><span class="token keyword">const</span> yExtent <span class="token operator">=</span> d3<span class="token punctuation">.</span><span class="token function">extent</span><span class="token punctuation">(</span>data<span class="token punctuation">,</span> <span class="token parameter">d</span> <span class="token operator">=></span> <span class="token function">Number</span><span class="token punctuation">(</span>d<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">)</span></code></pre>
<p>接著定義數據的邊界大小,通常是數據的最大最小值。這兩個步驟是為了讓我們做到這件事情:</p>
<p><img src="https://i.imgur.com/T40fZnA.png" alt="" /></p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> x <span class="token operator">=</span> d3<span class="token punctuation">.</span><span class="token function">scaleBand</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">domain</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">,</span> data<span class="token punctuation">.</span>length<span class="token punctuation">]</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">rangeRound</span><span class="token punctuation">(</span>xRange<span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">padding</span><span class="token punctuation">(</span><span class="token number">0.1</span><span class="token punctuation">)</span><br /> <br /><span class="token keyword">const</span> y <span class="token operator">=</span> d3<span class="token punctuation">.</span><span class="token function">scaleLinear</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">domain</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">,</span> yExtent<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">range</span><span class="token punctuation">(</span>yRange<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>接著透過 D3 提供的 scale 代入兩者的邊界轉換關係,D3 有提供不同的 Scale 類型,剛好處理的就是我們提到的數據種類的議題,舉例來說:分類資料的縮放就會是離散的、一個個的、有邊界的,而線性資料就會是連續的對應,而這邊的這個轉換方程式就可以用於將數據對應到圖像資訊:</p>
<p><img src="https://i.imgur.com/6vqRAAb.png" alt="" /></p>
<p>當我們把數據轉換器設定好後,接下來透過 D3 提供的 data method 來將數據狀態對應到剛剛的 svg 元素中,並執行轉換:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> svg <span class="token operator">=</span> d3<span class="token punctuation">.</span><span class="token function">select</span><span class="token punctuation">(</span><span class="token string">"svg"</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">attr</span><span class="token punctuation">(</span><span class="token string">"viewBox"</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> width<span class="token punctuation">,</span> height<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br />svg<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span><span class="token string">"g"</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">attr</span><span class="token punctuation">(</span><span class="token string">"fill"</span><span class="token punctuation">,</span> <span class="token string">"steelblue"</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">selectAll</span><span class="token punctuation">(</span><span class="token string">"rect"</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">data</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">"rect"</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">attr</span><span class="token punctuation">(</span><span class="token string">"x"</span><span class="token punctuation">,</span> <span class="token parameter">d</span> <span class="token operator">=></span> <span class="token function">x</span><span class="token punctuation">(</span>d<span class="token punctuation">.</span>category<span class="token punctuation">)</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">attr</span><span class="token punctuation">(</span><span class="token string">"y"</span><span class="token punctuation">,</span> <span class="token parameter">d</span> <span class="token operator">=></span> <span class="token function">y</span><span class="token punctuation">(</span>d<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">attr</span><span class="token punctuation">(</span><span class="token string">"height"</span><span class="token punctuation">,</span> <span class="token parameter">d</span> <span class="token operator">=></span> <span class="token function">y</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token function">y</span><span class="token punctuation">(</span>d<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">attr</span><span class="token punctuation">(</span><span class="token string">"width"</span><span class="token punctuation">,</span> x<span class="token punctuation">.</span><span class="token function">bandwidth</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>使用 D3 選取 svg 後,因為是柱狀圖,我們先設定選取 rect 這類型 svg 元素,接著 <code>data(data)</code> 塞入資料,可以理解成把數據狀態丟進去作為原料,接著使用 <code>join</code> 指示這筆資料最終要與 rect 作轉換,但怎麼轉換呢,在 rect 會有的 attribute x, y, height, width 中丟進轉換公式跟對應的資料,這樣就能產生出 Bar Chart 中的一個個 Bar 了。</p>
<p>這邊 join 的功用除了指示外,join 其實幫我們去比對目前的資料與 svg element 之間的狀況,若有多會新增更多 element,若有少則減去 element,這邊可以看下列這個實驗。</p>
<p>最後在並非一對一數據對應而是呈現數據概況的 axis 部分,D3 提供了方便的 function 來產生出完整的圖樣:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token function-variable function">yAxis</span> <span class="token operator">=</span> <span class="token parameter">g</span> <span class="token operator">=></span> g<br /> <span class="token punctuation">.</span><span class="token function">attr</span><span class="token punctuation">(</span><span class="token string">"transform"</span><span class="token punctuation">,</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">translate(</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>margin<span class="token punctuation">.</span>left<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">,0)</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">call</span><span class="token punctuation">(</span>d3<span class="token punctuation">.</span><span class="token function">axisLeft</span><span class="token punctuation">(</span>y<span class="token punctuation">)</span><span class="token punctuation">)</span><br /> <br /><span class="token keyword">const</span> <span class="token function-variable function">xAxis</span> <span class="token operator">=</span> <span class="token parameter">g</span> <span class="token operator">=></span> g<br /> <span class="token punctuation">.</span><span class="token function">attr</span><span class="token punctuation">(</span><span class="token string">"transform"</span><span class="token punctuation">,</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">translate(0,</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>height <span class="token operator">-</span> margin<span class="token punctuation">.</span>bottom<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">)</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">call</span><span class="token punctuation">(</span>d3<span class="token punctuation">.</span><span class="token function">axisBottom</span><span class="token punctuation">(</span>x<span class="token punctuation">)</span><span class="token punctuation">)</span><br /> <br />svg<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span><span class="token string">"g"</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">call</span><span class="token punctuation">(</span>xAxis<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br />svg<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span><span class="token string">"g"</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">call</span><span class="token punctuation">(</span>yAxis<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>我們可以透過:d3.axisLeft, d3.axisBottom 產生出想要的 svg element 最後直接 append 進 svg 就有熱騰騰的 axis 可以使用。透過以上我們大概了解了最簡單的 d3 chart 流程,但可以發現有很多動作其實是前端中早有工具在解決的問題,舉例一直反覆用到的 append, select ... 這些不是 jQuery 甚至可以由原生 JS 取代的嗎?</p>
<h3 id="d3-dom-%E6%93%8D%E4%BD%9C"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/d3-intro/#d3-dom-%E6%93%8D%E4%BD%9C">#</a> D3 DOM 操作</h3>
<p>還記得大家的好朋友 jQuery 嗎?今天 DOM 處理並非 D3 獨有,甚至 D3 data join (聽起來超像什麼獨立樂團 XD) 的概念與現今 state change,我們現在來逐漸剝奪 D3 在 DOM 處理的環節,由原生 JS 取代看看差異。以上的轉換器不變,今天如果要處理塞入資料以及選取元素,我們可以直接透過熟悉的 DOM 處理來替代 D3 提供的 method 會更直覺。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> svg <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'svg'</span><span class="token punctuation">)</span><br />svg<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">"viewBox"</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> width<span class="token punctuation">,</span> height<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> chartStr <span class="token operator">=</span> data<span class="token punctuation">.</span><span class="token function">reduce</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">str<span class="token punctuation">,</span> d</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> str <span class="token operator">+=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> <rect <br /> x="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">x</span><span class="token punctuation">(</span>d<span class="token punctuation">.</span>category<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">" <br /> y="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">y</span><span class="token punctuation">(</span>d<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"<br /> height="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">y</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token function">y</span><span class="token punctuation">(</span>d<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"<br /> width="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>x<span class="token punctuation">.</span><span class="token function">bandwidth</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"<br /> ></rect><br /> </span><span class="token template-punctuation string">`</span></span><br /> <span class="token keyword">return</span> str<span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">,</span><span class="token string">""</span><span class="token punctuation">)</span><br /><br />svg<span class="token punctuation">.</span>innerHTML <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> <g fill="steelblue"></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>chartStr<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"></g><br /></span><span class="token template-punctuation string">`</span></span></code></pre>
<p>透過將數據利用轉換器轉為一系列的 HTML str 塞進 svg 之中,現在轉換的流程更直覺了,D3 負責數據轉換器,而 DOM 操作被拆到原生 JS 來處理,每當數據改變的時候就重新產生新的 HTML String,再丟進 SVG 中就可以了。</p>
<h3 id="d3-in-framework-e.g.-react-%26-vue"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/d3-intro/#d3-in-framework-e.g.-react-%26-vue">#</a> D3 in Framework e.g. React & Vue</h3>
<p>數據改變、重繪,聽到這兩個關鍵字啊哈,發現 Framework 結合的地方了,今天我們讓 DOM 處理跟著狀態走,D3 持續在 state 跟 DOM 元素之間負責轉換,於是無論 react 還是 vue 我們都可以很直覺的使用 D3,而不是透過常見的 ref 處理方式,讓 D3 從轉換到 DOM 操作一條龍的處理。</p>
<p>以 React 為例:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token function-variable function">Chart</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>data<span class="token punctuation">,</span> setData<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> xExtent <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">,</span> data<span class="token punctuation">.</span>length<span class="token punctuation">]</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> x <span class="token operator">=</span> d3<span class="token punctuation">.</span><span class="token function">scaleBand</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">domain</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">,</span> data<span class="token punctuation">.</span>length<span class="token punctuation">]</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">rangeRound</span><span class="token punctuation">(</span>xRange<span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">padding</span><span class="token punctuation">(</span><span class="token number">0.1</span><span class="token punctuation">)</span><br /> <span class="token operator">...</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span>svg<span class="token operator">></span><br /> <span class="token operator"><</span>g fill<span class="token operator">=</span><span class="token string">"steelblue"</span><span class="token operator">></span><br /> <span class="token punctuation">{</span> data<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">d<span class="token punctuation">,</span> idx</span><span class="token punctuation">)</span> <span class="token operator">=></span> <br /> <span class="token operator"><</span>rect <br /> key<span class="token operator">=</span><span class="token punctuation">{</span>idx<span class="token punctuation">}</span><br /> x<span class="token operator">=</span><span class="token punctuation">{</span><span class="token function">x</span><span class="token punctuation">(</span>d<span class="token punctuation">.</span>category<span class="token punctuation">)</span><span class="token punctuation">}</span> <br /> y<span class="token operator">=</span><span class="token punctuation">{</span><span class="token function">y</span><span class="token punctuation">(</span>d<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">}</span> <br /> width<span class="token operator">=</span><span class="token punctuation">{</span>x<span class="token punctuation">.</span><span class="token function">bandwidth</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">}</span> <br /> height<span class="token operator">=</span><span class="token punctuation">{</span><span class="token function">y</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token function">y</span><span class="token punctuation">(</span>d<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">}</span><br /> <span class="token operator">></span><span class="token operator"><</span><span class="token operator">/</span>rect<span class="token operator">></span><br /> <span class="token punctuation">)</span><span class="token punctuation">}</span><br /> <span class="token operator"><</span><span class="token operator">/</span>g<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>svg<span class="token operator">></span><br /> <span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<p>我們把 rect 的 DOM 操作直接透過 data map 去做對應,而今天 data 這個 state 有改變,下面的圖表區塊也會重新渲染,element 的比對也會在 virtual DOM 的階段被優化,同樣的以 Vue 為例也是類似的邏輯:</p>
<p>p.s. 由於 elevently 使用的 highlight 沒有支援 vue,使用 js 來顯示:</p>
<pre class="language-js"><code class="language-js"><span class="token operator"><</span>template<span class="token operator">></span><br /> <span class="token operator"><</span>svg<span class="token operator">></span><br /> <span class="token operator"><</span>g fill<span class="token operator">=</span><span class="token string">"steelblue"</span><span class="token operator">></span><br /> <span class="token operator"><</span>rect<br /> v<span class="token operator">-</span><span class="token keyword">for</span><span class="token operator">=</span><span class="token string">"(d, idx) in chartData"</span><br /> <span class="token operator">:</span>key<span class="token operator">=</span><span class="token string">"idx"</span><br /> <span class="token operator">:</span>x<span class="token operator">=</span><span class="token string">"d.x"</span><br /> <span class="token operator">:</span>y<span class="token operator">=</span><span class="token string">"d.y"</span><br /> <span class="token operator">:</span>height<span class="token operator">=</span><span class="token string">"d.height"</span><br /> <span class="token operator">:</span>width<span class="token operator">=</span><span class="token string">"d.x"</span><br /> <span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>g<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>svg<span class="token operator">></span><br /><span class="token operator"><</span><span class="token operator">/</span>template<span class="token operator">></span><br /><span class="token operator"><</span>script<span class="token operator">></span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> ref <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'vue'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> d3 <span class="token keyword">from</span> <span class="token string">'d3'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token punctuation">{</span><br /> <span class="token function">setup</span><span class="token punctuation">(</span><span class="token parameter">props</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> chartData <span class="token operator">=</span> <span class="token function">ref</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> xExtent <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">,</span> exampleData<span class="token punctuation">]</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> x <span class="token operator">=</span> d3<span class="token punctuation">.</span><span class="token function">scaleBand</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">domain</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">,</span> exampleData<span class="token punctuation">.</span>length<span class="token punctuation">]</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">rangeRound</span><span class="token punctuation">(</span>xRange<span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">padding</span><span class="token punctuation">(</span><span class="token number">0.1</span><span class="token punctuation">)</span><br /> <span class="token operator">...</span><br /> chartData<span class="token punctuation">.</span>value <span class="token operator">=</span> exampleData<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">d<span class="token punctuation">,</span> idx</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token literal-property property">x</span><span class="token operator">:</span> <span class="token function">x</span><span class="token punctuation">(</span>idx<span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">y</span><span class="token operator">:</span> <span class="token function">y</span><span class="token punctuation">(</span>d<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">height</span><span class="token operator">:</span> height <span class="token operator">-</span> <span class="token function">y</span><span class="token punctuation">(</span>d<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">width</span><span class="token operator">:</span> x<span class="token punctuation">.</span><span class="token function">bandWidth</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> <span class="token punctuation">{</span><br /> chartData<br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><span class="token operator"><</span><span class="token operator">/</span>script<span class="token operator">></span></code></pre>
<p>等等,那 Axis 這種元素也要這樣處理嗎?沒錯,並非說能用 state 就不用 ref 處理,我們還是可以使用 ref,尤其在 Element 複雜且互動變化少的 Axis 適合。以 React 為例:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token function-variable function">Chart</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>data<span class="token punctuation">,</span> setData<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> svg <span class="token operator">=</span> <span class="token function">useRef</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> <span class="token function-variable function">xAxisGenerator</span> <span class="token operator">=</span> <span class="token parameter">g</span> <span class="token operator">=></span> g<br /> <span class="token punctuation">.</span><span class="token function">attr</span><span class="token punctuation">(</span><span class="token string">"transform"</span><span class="token punctuation">,</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">translate(0,</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>height <span class="token operator">-</span> margin<span class="token punctuation">.</span>bottom<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">)</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">call</span><span class="token punctuation">(</span>d3<span class="token punctuation">.</span><span class="token function">axisBottom</span><span class="token punctuation">(</span>x<span class="token punctuation">)</span><span class="token punctuation">)</span><br /> <br /> <span class="token operator">...</span><br /> <span class="token function">useEffect</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> svg<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span><span class="token string">"g"</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">call</span><span class="token punctuation">(</span>xAxisGenerator<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>data<span class="token punctuation">]</span><span class="token punctuation">)</span><br /> <br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span>svg ref<span class="token operator">=</span><span class="token punctuation">{</span>svg<span class="token punctuation">}</span><span class="token operator">></span><br /> <span class="token operator"><</span>g fill<span class="token operator">=</span><span class="token string">"steelblue"</span><span class="token operator">></span><br /> <span class="token punctuation">{</span> data<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">d<span class="token punctuation">,</span> idx</span><span class="token punctuation">)</span> <span class="token operator">=></span> <br /> <span class="token operator"><</span>rect <br /> key<span class="token operator">=</span><span class="token punctuation">{</span>idx<span class="token punctuation">}</span><br /> x<span class="token operator">=</span><span class="token punctuation">{</span><span class="token function">x</span><span class="token punctuation">(</span>d<span class="token punctuation">.</span>category<span class="token punctuation">)</span><span class="token punctuation">}</span> <br /> y<span class="token operator">=</span><span class="token punctuation">{</span><span class="token function">y</span><span class="token punctuation">(</span>d<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">}</span> <br /> width<span class="token operator">=</span><span class="token punctuation">{</span>x<span class="token punctuation">.</span><span class="token function">bandwidth</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">}</span> <br /> height<span class="token operator">=</span><span class="token punctuation">{</span><span class="token function">y</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token function">y</span><span class="token punctuation">(</span>d<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">}</span><br /> <span class="token operator">></span><span class="token operator"><</span><span class="token operator">/</span>rect<span class="token operator">></span><br /> <span class="token punctuation">)</span><span class="token punctuation">}</span><br /> <span class="token operator"><</span><span class="token operator">/</span>g<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>svg<span class="token operator">></span><br /> <span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<p>這樣就可以快速處理 SVG 複雜的 Axis 圖形了。</p>
<h2 id="%E5%B0%8F%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/d3-intro/#%E5%B0%8F%E7%B5%90">#</a> 小結</h2>
<p>每每接觸 D3 Layout 有一種看一個是一個的感覺嗎?現在重新從轉換的角度看,要接觸 D3 Layout 應該要先知道怎麼轉:</p>
<ul>
<li>了解每一個 Layout Method 需要輸入的資料型態與輸出的對應 SVG</li>
<li>知道哪一些圖表元素適合直接 DOM 操作、哪些不。</li>
</ul>
<p>舉個例子,假設今天突然經歷到比較少見的甜甜圈圖,我會先去 <a href="https://www.d3-graph-gallery.com/">Graph Gallery</a> 或 <a href="https://observablehq.com/">Observable</a> 比較直覺地看對應到 D3 的哪一種 Layout。</p>
<p>接著釐清這個 Layout 最終對應的 SVG 為何,需要的資料型態為何,以甜甜圈圖為例,最終產出的資料區塊會是 path,而 path 需要的 input 資料是 path 的路徑,</p>
<p><img src="https://i.imgur.com/iFGqS7d.png" alt="" /></p>
<p>另外對應在 D3 的 Layout 是 arc,arc 會產出 path 對應的路徑資料,更前置 arc 需要的 input 是 d3.pie data,pie data 則需要 Array 形式的 key value object ... etc,一路轉換逆推就可以形成這樣的架構。</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// example data should be [{ key: ..., value: ... }, ...]</span><br /><br /><span class="token comment">// pie data transform </span><br /><span class="token keyword">const</span> arcData <span class="token operator">=</span> d3<span class="token punctuation">.</span><span class="token function">pie</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">value</span><span class="token punctuation">(</span><span class="token parameter">d</span> <span class="token operator">=></span> d<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token comment">// pir to arc path transform function</span><br /><span class="token keyword">const</span> arc <span class="token operator">=</span> d3<span class="token punctuation">.</span><span class="token function">arc</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <br /><span class="token keyword">const</span> radius <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">min</span><span class="token punctuation">(</span>width<span class="token punctuation">,</span> height<span class="token punctuation">)</span> <span class="token operator">/</span> <span class="token number">4</span> <span class="token punctuation">;</span><br /> chartData <span class="token operator">=</span> <span class="token function">arcData</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token parameter">d</span> <span class="token operator">=></span> <span class="token function">arc</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token literal-property property">innerRadius</span><span class="token operator">:</span> radius <span class="token operator">+</span> <span class="token number">30</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">outerRadius</span><span class="token operator">:</span> radius<span class="token punctuation">,</span><br /> <span class="token operator">...</span>d<span class="token punctuation">,</span> <span class="token comment">// path input </span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>綜合以上拆解就能有系統的上手各式各樣的圖表。</p>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/d3-intro/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li><a href="https://www.d3-graph-gallery.com/">Graph Gallery</a></li>
<li><a href="https://observablehq.com/">Observable</a></li>
<li>[^1] : <a href="http://vis.stanford.edu/files/2011-D3-InfoVis.pdf">2011-D3-InfoVis.pdf</a></li>
<li>[^2] : <a href="https://www.youtube.com/watch?v=w493jXg5D8o&ab_channel=ReactNext">Shirley Wu: D3 and React, Together — ReactNext 2017 - YouTube</a></li>
<li>[^3] : <a href="https://courses.cs.washington.edu/courses/cse512/21sp/lectures/CSE512-DataAndImageModels.pdf">CSE512-DataAndImageModels</a></li>
<li>[^4] : <a href="https://variable.io/">Variable - new ways of experiencing data</a></li>
<li>[^5] : <a href="https://www.youtube.com/watch?v=-ITadxbL8Wk&ab_channel=AssociationforComputingMachinery%28ACM%29">Opportunities and Challenges for Data Physicalization - YouTube</a></li>
</ul>
mongoose 中的 aggregate
2021-10-03T00:00:00Z
https://blog.errorbaker.tw/posts/ruofan/mongoose-aggregate/
<!-- summary -->
<p>Hi,大家好!前陣子在研究專案上從 MySQL migrate 到 MongoDB 的可能性,選擇了 Mongoose 作為這次的主題,這篇文章會帶著大家認識 aggregate 的基本操作。</p>
<!-- summary -->
<h2 id="%E4%BB%80%E9%BA%BC%E6%98%AF-mongoose%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/mongoose-aggregate/#%E4%BB%80%E9%BA%BC%E6%98%AF-mongoose%EF%BC%9F">#</a> 什麼是 Mongoose?</h2>
<p>以下為 Mongoose 官方文件上對自己的介紹:</p>
<blockquote>
<p>elegant mongodb object modeling for node.js.</p>
</blockquote>
<blockquote>
<p>writing MongoDB validation, casting and business logic boilerplate is a drag. That's why we wrote Mongoose.</p>
</blockquote>
<p>簡單來說,Mongoose 是基於 node.js 的 Object Data Modeling (ODM),讓我們可以優雅的操作 MongoDB 中的資料。</p>
<h2 id="aggregate"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/mongoose-aggregate/#aggregate">#</a> Aggregate</h2>
<p>在進入介紹前,先和大家分享一下在 Mongoose v6.0.7 版本之前使用 Aggregate 會出現下方錯誤資訊。</p>
<pre class="language-js"><code class="language-js"><span class="token literal-property property">Error</span><span class="token operator">:</span> Method <span class="token string">"collection.aggregate()"</span> accepts at most two arguments<br /><br /><span class="token literal-property property">MongoInvalidArgumentError</span><span class="token operator">:</span> Method <span class="token string">"collection.aggregate()"</span> accepts at most two arguments<br /> at Collection<span class="token punctuation">.</span><span class="token function">aggregate</span> <span class="token punctuation">(</span><span class="token operator">/</span>home<span class="token operator">/</span>dave<span class="token operator">/</span>projects<span class="token operator">/</span>asd<span class="token operator">/</span>node_modules<span class="token operator">/</span>mongodb<span class="token operator">/</span>lib<span class="token operator">/</span>collection<span class="token punctuation">.</span>js<span class="token operator">:</span><span class="token number">367</span><span class="token operator">:</span><span class="token number">19</span><span class="token punctuation">)</span><br /> at NativeCollection<span class="token punctuation">.</span><span class="token operator"><</span>computed<span class="token operator">></span> <span class="token punctuation">[</span><span class="token keyword">as</span> aggregate<span class="token punctuation">]</span> <span class="token punctuation">(</span><span class="token operator">/</span>home<span class="token operator">/</span>dave<span class="token operator">/</span>projects<span class="token operator">/</span>asd<span class="token operator">/</span>node_modules<span class="token operator">/</span>mongoose<span class="token operator">/</span>lib<span class="token operator">/</span>drivers<span class="token operator">/</span>node<span class="token operator">-</span>mongodb<span class="token operator">-</span>native<span class="token operator">/</span>collection<span class="token punctuation">.</span>js<span class="token operator">:</span><span class="token number">200</span><span class="token operator">:</span><span class="token number">33</span><span class="token punctuation">)</span><br /> at NativeCollection<span class="token punctuation">.</span>Collection<span class="token punctuation">.</span><span class="token function">doQueue</span> <span class="token punctuation">(</span><span class="token operator">/</span>home<span class="token operator">/</span>dave<span class="token operator">/</span>projects<span class="token operator">/</span>asd<span class="token operator">/</span>node_modules<span class="token operator">/</span>mongoose<span class="token operator">/</span>lib<span class="token operator">/</span>collection<span class="token punctuation">.</span>js<span class="token operator">:</span><span class="token number">135</span><span class="token operator">:</span><span class="token number">23</span><span class="token punctuation">)</span><br /> at <span class="token operator">/</span>home<span class="token operator">/</span>dave<span class="token operator">/</span>projects<span class="token operator">/</span>asd<span class="token operator">/</span>node_modules<span class="token operator">/</span>mongoose<span class="token operator">/</span>lib<span class="token operator">/</span>collection<span class="token punctuation">.</span>js<span class="token operator">:</span><span class="token number">82</span><span class="token operator">:</span><span class="token number">24</span><br /> at <span class="token function">processTicksAndRejections</span> <span class="token punctuation">(</span>internal<span class="token operator">/</span>process<span class="token operator">/</span>task_queues<span class="token punctuation">.</span>js<span class="token operator">:</span><span class="token number">77</span><span class="token operator">:</span><span class="token number">11</span><span class="token punctuation">)</span></code></pre>
<p>這個 <a href="https://github.com/Automattic/mongoose/issues/10722">issue</a> 前幾天被修正了,在實作時可以注意一下 Mongoose 的版本。</p>
<h2 id="%24match-%26-%24project"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/mongoose-aggregate/#%24match-%26-%24project">#</a> $match & $project</h2>
<p>$match 可以讓我們篩選資料,$project 可以組合出最後 output 的資料。<br />
來看看下面的範例。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getStoreInfo</span><span class="token punctuation">(</span><span class="token parameter">storeUuid</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> ObjectId <span class="token operator">=</span> mongoose<span class="token punctuation">.</span>Types<span class="token punctuation">.</span>ObjectId<br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>result<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token keyword">await</span> Store<span class="token punctuation">.</span><span class="token function">aggregate</span><span class="token punctuation">(</span><span class="token punctuation">[</span><br /> <span class="token punctuation">{</span> <span class="token literal-property property">$match</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">uuid</span><span class="token operator">:</span> <span class="token function">ObjectId</span><span class="token punctuation">(</span>storeUuid<span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">$project</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">_id</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">uuid</span><span class="token operator">:</span> <span class="token string">"$uuid"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">"$info.name"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">servicePhone</span><span class="token operator">:</span> <span class="token string">"$info.servicePhone"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">address</span><span class="token operator">:</span> <span class="token string">"$info.address"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">]</span><span class="token punctuation">)</span><br /> <span class="token keyword">return</span> result<br /><span class="token punctuation">}</span></code></pre>
<p>如果印出上方的 result 會得到下方資料。</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br /> <span class="token property">"uuid"</span><span class="token operator">:</span> <span class="token string">"6123456e1787b96a05123456"</span><span class="token punctuation">,</span><br /> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Error Baker"</span><span class="token punctuation">,</span><br /> <span class="token property">"servicePhone"</span><span class="token operator">:</span> <span class="token string">"0287878787"</span><span class="token punctuation">,</span><br /> <span class="token property">"address"</span><span class="token operator">:</span> <span class="token string">"台北市中正區羅斯福路一段2號"</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span></code></pre>
<h2 id="%24lookup-%26-%24unwind"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/mongoose-aggregate/#%24lookup-%26-%24unwind">#</a> $lookup & $unwind</h2>
<p>$lookup 可以讓我們使用 JOIN 從其他 collection 拿資料,$unwind 可以拆分資料。<br />
來看看下面的範例。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getRoleWithUserByStoreId</span><span class="token punctuation">(</span><span class="token parameter">storeId</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> ObjectId <span class="token operator">=</span> mongoose<span class="token punctuation">.</span>Types<span class="token punctuation">.</span>ObjectId<span class="token punctuation">;</span><br /> <span class="token keyword">const</span> result <span class="token operator">=</span> <span class="token keyword">await</span> Role<span class="token punctuation">.</span><span class="token function">aggregate</span><span class="token punctuation">(</span><span class="token punctuation">[</span><br /> <span class="token punctuation">{</span> <span class="token literal-property property">$match</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">store</span><span class="token operator">:</span> <span class="token function">ObjectId</span><span class="token punctuation">(</span>storeId<span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">$lookup</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">from</span><span class="token operator">:</span> <span class="token string">"storeuserroles"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">pipeline</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">$lookup</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">from</span><span class="token operator">:</span> <span class="token string">"users"</span><span class="token punctuation">,</span> <span class="token comment">// Collection name</span><br /> <span class="token literal-property property">localField</span><span class="token operator">:</span> <span class="token string">"user"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">foreignField</span><span class="token operator">:</span> <span class="token string">"_id"</span><span class="token punctuation">,</span><br /> <span class="token keyword">as</span><span class="token operator">:</span> <span class="token string">"user"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">$unwind</span><span class="token operator">:</span> <span class="token string">"$user"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">$project</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">_id</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">$toString</span><span class="token operator">:</span> <span class="token string">"$user._id"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">uuid</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">$toString</span><span class="token operator">:</span> <span class="token string">"$user.uuid"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">"$user.name"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">email</span><span class="token operator">:</span> <span class="token string">"$user.email"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">phone</span><span class="token operator">:</span> <span class="token string">"$user.phone"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">photoUrl</span><span class="token operator">:</span> <span class="token string">"$user.photoUrl"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">intro</span><span class="token operator">:</span> <span class="token string">"$user.intro"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token keyword">as</span><span class="token operator">:</span> <span class="token string">"users"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">$project</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">_id</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">$toString</span><span class="token operator">:</span> <span class="token string">"$_id"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">"$name"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">users</span><span class="token operator">:</span> <span class="token string">"$users"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">return</span> result<span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /></code></pre>
<p>如果印出上方的 result 會得到下方資料。</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"6123456d87bee84123456887"</span><span class="token punctuation">,</span><br /> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"errorBaker筆者"</span><span class="token punctuation">,</span><br /> <span class="token property">"users"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"6151234567bee84077654321"</span><span class="token punctuation">,</span><br /> <span class="token property">"uuid"</span><span class="token operator">:</span> <span class="token string">"615528412345674072b40887"</span><span class="token punctuation">,</span><br /> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"ruofan"</span><span class="token punctuation">,</span><br /> <span class="token property">"email"</span><span class="token operator">:</span> <span class="token string">"errorBaker@gmail.com"</span><span class="token punctuation">,</span><br /> <span class="token property">"phone"</span><span class="token operator">:</span> <span class="token string">"0987878787"</span><span class="token punctuation">,</span><br /> <span class="token property">"photoUrl"</span><span class="token operator">:</span> <span class="token null keyword">null</span><span class="token punctuation">,</span><br /> <span class="token property">"intro"</span><span class="token operator">:</span> <span class="token null keyword">null</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"6123450058bee87072012345"</span><span class="token punctuation">,</span><br /> <span class="token property">"uuid"</span><span class="token operator">:</span> <span class="token string">"61552123450ee84072b40870"</span><span class="token punctuation">,</span><br /> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"xiang"</span><span class="token punctuation">,</span><br /> <span class="token property">"email"</span><span class="token operator">:</span> <span class="token string">"errorBaker@gmail.com"</span><span class="token punctuation">,</span><br /> <span class="token property">"phone"</span><span class="token operator">:</span> <span class="token string">"0978787878"</span><span class="token punctuation">,</span><br /> <span class="token property">"photoUrl"</span><span class="token operator">:</span> <span class="token null keyword">null</span><span class="token punctuation">,</span><br /> <span class="token property">"intro"</span><span class="token operator">:</span> <span class="token null keyword">null</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">]</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"6123456d87bee80073456801"</span><span class="token punctuation">,</span><br /> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"errorBaker管理員"</span><span class="token punctuation">,</span><br /> <span class="token property">"users"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"6151234567bee84077654302"</span><span class="token punctuation">,</span><br /> <span class="token property">"uuid"</span><span class="token operator">:</span> <span class="token string">"615528412345674070020887"</span><span class="token punctuation">,</span><br /> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"huli"</span><span class="token punctuation">,</span><br /> <span class="token property">"email"</span><span class="token operator">:</span> <span class="token string">"errorBaker@gmail.com"</span><span class="token punctuation">,</span><br /> <span class="token property">"phone"</span><span class="token operator">:</span> <span class="token string">"0987878787"</span><span class="token punctuation">,</span><br /> <span class="token property">"photoUrl"</span><span class="token operator">:</span> <span class="token null keyword">null</span><span class="token punctuation">,</span><br /> <span class="token property">"intro"</span><span class="token operator">:</span> <span class="token null keyword">null</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"6123450058bee87072012303"</span><span class="token punctuation">,</span><br /> <span class="token property">"uuid"</span><span class="token operator">:</span> <span class="token string">"61552123450ee84000340870"</span><span class="token punctuation">,</span><br /> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"clay"</span><span class="token punctuation">,</span><br /> <span class="token property">"email"</span><span class="token operator">:</span> <span class="token string">"errorBaker@gmail.com"</span><span class="token punctuation">,</span><br /> <span class="token property">"phone"</span><span class="token operator">:</span> <span class="token string">"0978787878"</span><span class="token punctuation">,</span><br /> <span class="token property">"photoUrl"</span><span class="token operator">:</span> <span class="token null keyword">null</span><span class="token punctuation">,</span><br /> <span class="token property">"intro"</span><span class="token operator">:</span> <span class="token null keyword">null</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">]</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">]</span><br /></code></pre>
<h2 id="%24cond"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/mongoose-aggregate/#%24cond">#</a> $cond</h2>
<p>$cond 可以讓我們在 $project 中加上條件判斷。<br />
來看看下面的範例。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getUserByType</span><span class="token punctuation">(</span><span class="token parameter">storeId<span class="token punctuation">,</span> userType <span class="token operator">=</span> <span class="token keyword">null</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> ObjectId <span class="token operator">=</span> mongoose<span class="token punctuation">.</span>Types<span class="token punctuation">.</span>ObjectId<span class="token punctuation">;</span><br /> <span class="token keyword">const</span> result <span class="token operator">=</span> <span class="token keyword">await</span> StoreUserRole<span class="token punctuation">.</span><span class="token function">aggregate</span><span class="token punctuation">(</span><span class="token punctuation">[</span><br /> <span class="token punctuation">{</span> <span class="token literal-property property">$match</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">store</span><span class="token operator">:</span> <span class="token function">ObjectId</span><span class="token punctuation">(</span>storeId<span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">$lookup</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">from</span><span class="token operator">:</span> <span class="token string">"users"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">localField</span><span class="token operator">:</span> <span class="token string">"user"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">foreignField</span><span class="token operator">:</span> <span class="token string">"_id"</span><span class="token punctuation">,</span><br /> <span class="token keyword">as</span><span class="token operator">:</span> <span class="token string">"user"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span> <span class="token literal-property property">$unwind</span><span class="token operator">:</span> <span class="token string">"$user"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">$lookup</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">from</span><span class="token operator">:</span> <span class="token string">"roles"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">localField</span><span class="token operator">:</span> <span class="token string">"role"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">foreignField</span><span class="token operator">:</span> <span class="token string">"_id"</span><span class="token punctuation">,</span><br /> <span class="token keyword">as</span><span class="token operator">:</span> <span class="token string">"role"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span> <span class="token literal-property property">$unwind</span><span class="token operator">:</span> <span class="token string">"$role"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">$project</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">_id</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">uuid</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">$toString</span><span class="token operator">:</span> <span class="token string">"$user.uuid"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">"$user.name"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">email</span><span class="token operator">:</span> <span class="token string">"$user.email"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">phone</span><span class="token operator">:</span> <span class="token string">"$user.phone"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">photoUrl</span><span class="token operator">:</span> <span class="token string">"$user.photoUrl"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">intro</span><span class="token operator">:</span> <span class="token string">"$user.intro"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">role</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">$toString</span><span class="token operator">:</span> <span class="token string">"$role._id"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">"$role.name"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">isSuperAdmin</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">$cond</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span> <span class="token literal-property property">$eq</span><span class="token operator">:</span> <span class="token punctuation">[</span>userType<span class="token punctuation">,</span> <span class="token keyword">null</span><span class="token punctuation">]</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token string">"$role.isSuperAdmin"</span><span class="token punctuation">,</span><br /> <span class="token string">"$$REMOVE"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> result<span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>如果沒有傳入 userType 印出上方的 result 會得到下方資料。</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"role"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"6878784d58bee12345640801"</span><span class="token punctuation">,</span><br /> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"errorBaker筆者"</span><span class="token punctuation">,</span><br /> <span class="token property">"isSuperAdmin"</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token property">"uuid"</span><span class="token operator">:</span> <span class="token string">"6878784d58bee12345640887"</span><span class="token punctuation">,</span><br /> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"ruofan"</span><span class="token punctuation">,</span><br /> <span class="token property">"email"</span><span class="token operator">:</span> <span class="token string">"errorBaker@gmail.com"</span><span class="token punctuation">,</span><br /> <span class="token property">"phone"</span><span class="token operator">:</span> <span class="token string">"0987878787"</span><span class="token punctuation">,</span><br /> <span class="token property">"photoUrl"</span><span class="token operator">:</span> <span class="token null keyword">null</span><span class="token punctuation">,</span><br /> <span class="token property">"intro"</span><span class="token operator">:</span> <span class="token null keyword">null</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"role"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"6878784d58bee12345640801"</span><span class="token punctuation">,</span><br /> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"errorBaker管理員"</span><span class="token punctuation">,</span><br /> <span class="token property">"isSuperAdmin"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token property">"uuid"</span><span class="token operator">:</span> <span class="token string">"6878784d58bee12345640887"</span><span class="token punctuation">,</span><br /> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"huli"</span><span class="token punctuation">,</span><br /> <span class="token property">"email"</span><span class="token operator">:</span> <span class="token string">"errorBaker@gmail.com"</span><span class="token punctuation">,</span><br /> <span class="token property">"phone"</span><span class="token operator">:</span> <span class="token string">"0988878787"</span><span class="token punctuation">,</span><br /> <span class="token property">"photoUrl"</span><span class="token operator">:</span> <span class="token null keyword">null</span><span class="token punctuation">,</span><br /> <span class="token property">"intro"</span><span class="token operator">:</span> <span class="token null keyword">null</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">]</span><span class="token punctuation">,</span></code></pre>
<h2 id="%E5%B0%8F%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/mongoose-aggregate/#%E5%B0%8F%E7%B5%90">#</a> 小結</h2>
<p>Aggregate 中有非常多方法可以讓我們整理與操作資料,在研究的過程中即便踩了一些坑也還是覺得挺有趣的!推薦給大家!</p>
<p>在閱讀文章時如果有遇到什麼問題,或是有什麼建議,都歡迎留言告訴我,謝謝。😃</p>
<ul>
<li><a href="https://github.com/ruofanwei/express-mongoose">Github | Repo: express-mongoose</a></li>
</ul>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/mongoose-aggregate/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li><a href="https://mongoing.com/docs/reference/operator/aggregation.html">Documentation | mongoDB: Aggregation Pipeline Operators</a></li>
<li><a href="https://mongoosejs.com/">Documentation | mongoose</a></li>
</ul>
現代前端開發 - 那些我使用過的 Pattern 和 工具
2021-10-04T00:00:00Z
https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/
<!-- summary -->
<p>以 React Hook Form + TS 完成一個 Nested Todo List 來舉例</p>
<!-- summary -->
<!-- more -->
<p>不多說先看結果 <a href="https://futianshen.github.io/nested-todo-list/">Nested Todo List - GitHub Pages</a><br />
如果說你比較習慣看 Code,可以看這裡 <a href="https://github.com/futianshen/nested-todo-list">Source Code</a></p>
<h2 id="%E8%AA%B0%E5%8F%AF%E8%83%BD%E6%AF%94%E8%BC%83%E9%81%A9%E5%90%88%E9%96%B1%E8%AE%80%E9%80%99%E7%AF%87%E6%96%87%E7%AB%A0%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#%E8%AA%B0%E5%8F%AF%E8%83%BD%E6%AF%94%E8%BC%83%E9%81%A9%E5%90%88%E9%96%B1%E8%AE%80%E9%80%99%E7%AF%87%E6%96%87%E7%AB%A0%EF%BC%9F">#</a> 誰可能比較適合閱讀這篇文章?</h2>
<ul>
<li>使用 React v16.6+ 的開發者</li>
<li>想看看我是如何學習一個自己從來沒碰過的工具</li>
<li>想嘗試使用 React + TS 開發前端</li>
<li>想知道基本的 React Hook Form 如何使用?</li>
<li>想了解一個 React 專案中有那些常用的 Pattern,以及使用這些 Pattern 背後的原因</li>
</ul>
<p>如果你不是上述對象,也沒有上述問題,你可以考慮改去讀讀其他夥伴們的 <a href="https://blog.errorbaker.tw/">優秀作品</a> ~</p>
<h2 id="%E5%A5%91%E6%A9%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#%E5%A5%91%E6%A9%9F">#</a> 契機</h2>
<p>最近工作需要將專案中的表單 Migrate 到 <a href="https://react-hook-form.com/">React Hook Form</a>,原來是想要看懂它的 <a href="https://github.com/react-hook-form/react-hook-form/tree/master/src">Source Code</a> 是怎麼實作,來寫一篇分享文,但因為看不懂(明明都是最熟悉的 React + TS 但我就是看不懂),加上拖延症爆發,於是去看了隔壁的 <a href="https://blog.errorbaker.tw/posts/xiang/advanced-todo-list/">Xiang 寫的這篇《進階版 To do list》</a>,警覺自己還沒學會走就想飛,連 React Hook Form 都還沒學會使用,竟然就妄想看懂 Source Code(?,於是便想用 React Hook Form 加上原來就已經在使用的 React + TS,做做看這個 Side Project,順便熟悉一下 React Hook Form 的使用,於是就有了這篇文章。</p>
<h2 id="%E5%BE%9E-user-story-%E9%96%8B%E5%A7%8B"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#%E5%BE%9E-user-story-%E9%96%8B%E5%A7%8B">#</a> 從 User Story 開始</h2>
<ul>
<li>我希望有一個 Todo List 可以紀錄我日常生活中所有需要做的事</li>
<li>我希望這個 Todo List 的每一個 Todo 項目都可增、改、勾、刪,這樣我就可以調配我手上要做的事情。</li>
<li>我希望每一個 Todo 項目下面,還可以再新增額外的多個子項目,這樣我就可以將一個大項目,拆成更細的幾個步驟。</li>
<li>我希望當我勾選 Todo 的時候,每個子 Todo,也會一起被勾選,代表我完成了這整個大項裡面的所有細項。</li>
<li>我希望當子 Todo 有任何一個沒有被勾選的時候,上層的 Todo 也要取消勾選,這樣我才知道這個大項還沒有完成。</li>
<li>我希望這個 Todo List 能夠被儲存和匯入,這樣我每次進入這個頁面的時候,都能取得這個 Todo List 的資料。</li>
<li>我可以一次性的清空所有 Todo,這樣我拖延症爆發的時候,才可以一次忘記所有要做的事情,讓真正重要的事情自己來找我。</li>
</ul>
<p>成品 <a href="https://futianshen.github.io/nested-todo-list/">Nested Todo List</a></p>
<h2 id="%E6%88%91%E6%98%AF%E5%A6%82%E4%BD%95%E9%96%8B%E5%A7%8B%E5%AD%B8%E7%BF%92-react-hook-form-%E7%9A%84%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#%E6%88%91%E6%98%AF%E5%A6%82%E4%BD%95%E9%96%8B%E5%A7%8B%E5%AD%B8%E7%BF%92-react-hook-form-%E7%9A%84%EF%BC%9F">#</a> 我是如何開始學習 React Hook Form 的?</h2>
<p>雖然已經先用 React + TS 試著實作過 <a href="https://github.com/futianshen/ts-react-functional-component-nested-todo-list">Nested Todo List</a><br />
但用的都是 React 內建的 useState,並不熟悉 React Hook Form 的 API,於是我先掃描了 <a href="https://react-hook-form.com/api">官方文件的 API</a>,然後看了一下 <a href="https://github.com/react-hook-form/react-hook-form/tree/master/examples">官方的 Example</a>,知道 Nested List 是可以做到的(知道一個東西已經做的到,比不確定做不做的到,在實作上的把握和信心程度會差很多),再知道可以用 useForm 和 useFieldArray 這 2 個 Custom Hook 來做之後,就開始邊看 Example 和 API 文件邊實作邊整理自己的程式碼。</p>
<p>大致的步驟是</p>
<ol>
<li>完成功能</li>
<li>反覆優化(抽象、樣式)</li>
<li>改完收工</li>
</ol>
<p>有興趣可以看看我那些笨拙可愛的過程[1]</p>
<h2 id="%E5%BE%9E%E9%80%99%E5%80%8B-over-engineering-%E7%9A%84%E5%B0%88%E6%A1%88%EF%BC%8C%E7%9C%8B%E5%B8%B8%E8%A6%8B%E7%9A%84-react-pattern-%E5%92%8C%E5%B7%A5%E5%85%B7%E4%BD%BF%E7%94%A8"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#%E5%BE%9E%E9%80%99%E5%80%8B-over-engineering-%E7%9A%84%E5%B0%88%E6%A1%88%EF%BC%8C%E7%9C%8B%E5%B8%B8%E8%A6%8B%E7%9A%84-react-pattern-%E5%92%8C%E5%B7%A5%E5%85%B7%E4%BD%BF%E7%94%A8">#</a> 從這個 Over Engineering 的專案,看常見的 React Pattern 和工具使用</h2>
<p>以我對前端工程認知,其實就是在開發的過程,不斷的將重複的部分不斷抽象、再組合.不同組合和抽象方式,解決的是不同場景下的問題,值得注意的是,每一次的抽象都會帶來認知上的負擔,良好的架構、Pattern 和命名會讓我們對功能產生正確的預期,這也是他們之所以重要的原因。</p>
<p>從一個專案中,會持續重組大致就是這幾大類</p>
<ul>
<li>Type (如果你使用的是 TypeScript)</li>
<li>Component (Functional Component)</li>
<li>Logic (React Hook)</li>
<li>Style (CSS in JS / Utility-First CSS)</li>
</ul>
<p>對應到整個專案的結構就會是</p>
<ul>
<li><code>/src</code>
<ul>
<li><code>/types</code></li>
<li><code>/components</code></li>
<li><code>/hooks</code></li>
<li><code>/style.tsx</code></li>
</ul>
</li>
</ul>
<h3 id="type-%E7%9A%84%E7%B5%84%E5%90%88"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#type-%E7%9A%84%E7%B5%84%E5%90%88">#</a> Type 的組合</h3>
<h4 id="%E8%87%AA%E5%AE%9A%E7%BE%A9-type"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#%E8%87%AA%E5%AE%9A%E7%BE%A9-type">#</a> 自定義 Type</h4>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">type</span> <span class="token class-name">FormValues</span> <span class="token operator">=</span> <span class="token punctuation">{</span><br /> nestedList<span class="token operator">:</span> <span class="token punctuation">{</span><br /> value<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /> isDone<span class="token operator">:</span> <span class="token builtin">boolean</span><span class="token punctuation">;</span><br /> list<span class="token operator">?</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> value<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /> isDone<span class="token operator">:</span> <span class="token builtin">boolean</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">type</span> <span class="token class-name">FormValues</span> <span class="token operator">=</span> <span class="token punctuation">{</span><br /> nestedList<span class="token operator">:</span> NestedList<span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">type</span> <span class="token class-name">NestedList</span> <span class="token operator">=</span> <span class="token punctuation">(</span>Todo <span class="token operator">&</span> <span class="token punctuation">{</span><br /> list<span class="token operator">?</span><span class="token operator">:</span> List<span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">type</span> <span class="token class-name">List</span> <span class="token operator">=</span> Todo<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">type</span> <span class="token class-name">Todo</span> <span class="token operator">=</span> <span class="token punctuation">{</span><br /> value<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /> isDone<span class="token operator">:</span> <span class="token builtin">boolean</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">const</span> initialList<span class="token operator">:</span> NestedList <span class="token operator">=</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> value<span class="token operator">:</span> <span class="token string">"todo group 2"</span><span class="token punctuation">,</span><br /> isDone<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> value<span class="token operator">:</span> <span class="token string">"todo group 1"</span><span class="token punctuation">,</span><br /> isDone<span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> list<span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> value<span class="token operator">:</span> <span class="token string">"group todo 2"</span><span class="token punctuation">,</span><br /> isDone<span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> value<span class="token operator">:</span> <span class="token string">"group todo 1"</span><span class="token punctuation">,</span><br /> isDone<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /><span class="token punctuation">]</span><span class="token punctuation">;</span></code></pre>
<h4 id="library-%E5%AE%9A%E7%BE%A9%E7%9A%84-type"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#library-%E5%AE%9A%E7%BE%A9%E7%9A%84-type">#</a> Library 定義的 Type</h4>
<pre class="language-tsx"><code class="language-tsx"><span class="token keyword">import</span> React<span class="token punctuation">,</span> <span class="token punctuation">{</span> ReactElement<span class="token punctuation">,</span> <span class="token constant">FC</span> <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"react"</span><span class="token punctuation">;</span> <span class="token comment">// from library type definition</span><br /><br /><span class="token keyword">const</span> <span class="token function-variable function">TodoForm</span><span class="token operator">:</span> <span class="token punctuation">(</span>props<span class="token operator">:</span> <span class="token punctuation">{</span>children<span class="token operator">:</span> ReactElement<span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function-variable function">ReactElement</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>form</span> <span class="token punctuation">/></span></span><br /><span class="token keyword">const</span> TodoList<span class="token operator">:</span> React<span class="token punctuation">.</span><span class="token function-variable function">FC</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">{</span>children<span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>ol</span><span class="token punctuation">></span></span><span class="token punctuation">{</span>children<span class="token punctuation">}</span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>ol</span><span class="token punctuation">></span></span><br /><span class="token keyword">const</span> Todo<span class="token operator">:</span> <span class="token function-variable function">FC</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">{</span>children<span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span><span class="token punctuation">{</span>children<span class="token punctuation">}</span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span><br /><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">TodoForm</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">TodoList</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Todo</span></span><span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Todo</span></span><span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Todo</span></span><span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Todo</span></span><span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span><span class="token class-name">TodoList</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span><span class="token class-name">TodoForm</span></span><span class="token punctuation">></span></span></code></pre>
<h4 id="%E7%B6%9C%E5%90%88%E4%BD%BF%E7%94%A8"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#%E7%B6%9C%E5%90%88%E4%BD%BF%E7%94%A8">#</a> 綜合使用</h4>
<pre class="language-tsx"><code class="language-tsx"><span class="token comment">// library type definition</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> <span class="token constant">FC</span><span class="token punctuation">,</span> ReactElement<span class="token punctuation">,</span> ChangeEventHandler <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"react"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> UseFormRegisterReturn <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"react-hook-form/dist/types/form"</span><span class="token punctuation">;</span><br /><br /><span class="token comment">// self type definition</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> NestedList <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"../types/todo"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> TodoForm<span class="token operator">:</span> <span class="token function-variable function">FC</span> <span class="token operator">=</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">{</span><span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token generic-function"><span class="token function">useForm</span><span class="token generic class-name"><span class="token operator"><</span><span class="token punctuation">{</span> nestedList<span class="token operator">:</span> NestedList <span class="token punctuation">}</span><span class="token operator">></span></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">return</span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>form</span> <span class="token punctuation">/></span></span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> Todo<span class="token operator">:</span> <span class="token punctuation">(</span>props<span class="token operator">:</span> <span class="token punctuation">{</span><br /> onRegister<span class="token operator">:</span> <span class="token punctuation">(</span>name<span class="token operator">:</span> <span class="token string">"isDone"</span> <span class="token operator">|</span> <span class="token string">"value"</span><span class="token punctuation">)</span> <span class="token operator">=></span> UseFormRegisterReturn<span class="token punctuation">;</span><br /> children<span class="token operator">?</span><span class="token operator">:</span> ReactElement<span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function-variable function">ReactElement</span> <span class="token operator">=</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">{</span> onRegister<span class="token punctuation">,</span> children <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> handleChange<span class="token operator">:</span> ChangeEventHandler<span class="token operator"><</span>HTMLInputElement<span class="token operator">></span> <span class="token operator">=</span> <span class="token punctuation">(</span>e<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> onRegister<span class="token operator">?.</span><span class="token punctuation">(</span><span class="token string">"isDone"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">onChange</span><span class="token punctuation">(</span>e<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> onCheck<span class="token operator">?.</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">StyledCheckbox</span></span> <span class="token attr-name">onChange</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>handleChange<span class="token punctuation">}</span></span> <span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">StyledTextInput</span></span> <span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token punctuation">{</span>children<span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<h3 id="component-%E7%9A%84%E7%B5%84%E5%90%88%E8%88%87%E8%A4%87%E7%94%A8"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#component-%E7%9A%84%E7%B5%84%E5%90%88%E8%88%87%E8%A4%87%E7%94%A8">#</a> Component 的組合與複用</h3>
<h4 id="composition-component"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#composition-component">#</a> Composition Component</h4>
<pre class="language-tsx"><code class="language-tsx"><span class="token keyword">const</span> TodoList<span class="token operator">:</span> <span class="token function-variable function">FC</span> <span class="token operator">=</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">{</span> children <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>ol</span><span class="token punctuation">></span></span><span class="token punctuation">{</span>children<span class="token punctuation">}</span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>ol</span><span class="token punctuation">></span></span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> Todo<span class="token operator">:</span> <span class="token constant">FC</span> <span class="token operator">&</span> <span class="token punctuation">{</span> TodoList<span class="token operator">:</span> <span class="token keyword">typeof</span> TodoList <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span><span class="token punctuation">{</span>children<span class="token punctuation">}</span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br />Todo<span class="token punctuation">.</span>TodoList <span class="token operator">=</span> TodoList<span class="token punctuation">;</span></code></pre>
<pre class="language-tsx"><code class="language-tsx"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">TodoList</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Todo</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Todo.TodoList</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Todo</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Todo.TodoList</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span><span class="token class-name">Todo</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span><span class="token class-name">TodoList</span></span><span class="token punctuation">></span></span><span class="token plain-text"></span></code></pre>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>ol</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>ol</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>ol</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>ol</span><span class="token punctuation">></span></span></code></pre>
<h4 id="props"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#props">#</a> Props</h4>
<ul>
<li>
<p>value</p>
<pre class="language-tsx"><code class="language-tsx"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">TodoList</span></span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>nestedList<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></code></pre>
</li>
<li>
<p>callback(<code>onVerb</code>)</p>
<pre class="language-tsx"><code class="language-tsx"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Todo</span></span><br /> <span class="token attr-name">onRegister</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span>name<span class="token punctuation">)</span> <span class="token operator">=></span> name<span class="token punctuation">}</span></span><br /> <span class="token attr-name">onCheck</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">}</span></span><br /> <span class="token attr-name">onRemove</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">}</span></span><br /><span class="token punctuation">></span></span><span class="token plain-text"></span></code></pre>
</li>
<li>
<p>children</p>
<pre class="language-tsx"><code class="language-tsx"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Todo</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Todo.TodoList</span></span> <span class="token punctuation">/></span></span><span class="token plain-text"><br /></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span><span class="token class-name">Todo</span></span><span class="token punctuation">></span></span></code></pre>
</li>
<li>
<p>render props (<code>renderNoun</code>)</p>
<pre class="language-tsx"><code class="language-tsx"><span class="token operator"><</span>TodoList<br /> renderAddButton<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span>prepend<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Button</span></span><br /> <span class="token attr-name">onClick</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token function">prepend</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> value<span class="token operator">:</span> <span class="token string">""</span><span class="token punctuation">,</span><br /> isDone<span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">}</span></span><br /> <span class="token punctuation">/></span></span><br /> <span class="token punctuation">)</span><span class="token punctuation">}</span><br /><span class="token operator">/</span><span class="token operator">></span></code></pre>
</li>
</ul>
<h3 id="style-%E7%9A%84%E7%B5%84%E5%90%88%E8%88%87%E8%A4%87%E7%94%A8"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#style-%E7%9A%84%E7%B5%84%E5%90%88%E8%88%87%E8%A4%87%E7%94%A8">#</a> Style 的組合與複用</h3>
<h4 id="utility-first-css-(for-layout)"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#utility-first-css-(for-layout)">#</a> Utility First CSS (for layout)</h4>
<blockquote>
<p>Tailwind, Bootstrap, <a href="https://github.com/search?q=utility+first+css">etc</a></p>
</blockquote>
<pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@tailwind</span> base<span class="token punctuation">;</span></span><br /><span class="token atrule"><span class="token rule">@tailwind</span> utilities<span class="token punctuation">;</span></span></code></pre>
<pre class="language-tsx"><code class="language-tsx"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">className</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>flex items-center gap-3 mb-3<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>input</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>checkbox<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>input</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span> <span class="token punctuation">/></span></span><span class="token plain-text"><br /></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<h4 id="css-in-js-(for-customize)"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#css-in-js-(for-customize)">#</a> CSS in JS (for customize)</h4>
<blockquote>
<p>Styled Component, Emotion, Linaria, <a href="https://github.com/topics/css-in-js">etc</a>.</p>
</blockquote>
<pre class="language-tsx"><code class="language-tsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> Button <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"@geist-ui/react"</span><span class="token punctuation">;</span> <span class="token comment">// any ui library</span><br /><span class="token keyword">import</span> styled <span class="token keyword">from</span> <span class="token string">"styled-components"</span><span class="token punctuation">;</span> <span class="token comment">// any css in js solution</span><br /><br /><span class="token keyword">const</span> StyledAddButton <span class="token operator">=</span> <span class="token function">styled</span><span class="token punctuation">(</span>Button<span class="token punctuation">)</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> &&& {<br /> display: flex;<br /> align-items: center;<br /> justify-content: center;<br /> box-sizing: border-box;<br /> padding: 0;<br /> width: 33px;<br /> height: 33px;<br /> }<br /></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span></code></pre>
<pre class="language-tsx"><code class="language-tsx"><span class="token keyword">import</span> styled<span class="token punctuation">,</span> <span class="token punctuation">{</span> css <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"styled-components"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> ContainerMixin <span class="token operator">=</span> css<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> margin: 0 auto;<br /> width: 500px;<br /></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> StyledSection <span class="token operator">=</span> styled<span class="token punctuation">.</span>section<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>ContainerMixin<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"><br /></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span></code></pre>
<h3 id="logic-%E7%9A%84%E8%A4%87%E7%94%A8%EF%BC%88react-hooks)"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#logic-%E7%9A%84%E8%A4%87%E7%94%A8%EF%BC%88react-hooks)">#</a> Logic 的複用(React Hooks)</h3>
<p>在這個專案中沒有直接用到任何 React 的 Hook(useState, useEffect, useRef...),因為這些 Hook 都藏在 React Hook Form 提供給我們 Custom Hook 裡面,我們可以直接使用,這些 Hook 提供給我們重複出現於表單處理的邏輯做開發。</p>
<pre class="language-tsx"><code class="language-tsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> useForm<span class="token punctuation">,</span> useFieldArray <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"react-hook-form"</span><span class="token punctuation">;</span> <span class="token comment">// any react hook library or custom hook</span><br /><br /><span class="token keyword">const</span> <span class="token punctuation">{</span> control<span class="token punctuation">,</span> register<span class="token punctuation">,</span> getValues<span class="token punctuation">,</span> setValue<span class="token punctuation">,</span> handleSubmit <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">useForm</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 經常用於表單資料獲取和提交</span><br /><span class="token keyword">const</span> <span class="token punctuation">{</span> fields<span class="token punctuation">,</span> prepend<span class="token punctuation">,</span> remove <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">useFieldArray</span><span class="token punctuation">(</span><span class="token punctuation">{</span> name<span class="token punctuation">,</span> control <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 經常用於動態新增表單項目</span></code></pre>
<h3 id="misc"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#misc">#</a> MISC</h3>
<h4 id="module"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#module">#</a> Module</h4>
<ul>
<li>
<p>Before</p>
<p><code>/todo/TodoList.tsx</code></p>
<pre class="language-tsx"><code class="language-tsx"><span class="token keyword">export</span> <span class="token keyword">default</span> TodoList<span class="token punctuation">;</span></code></pre>
<p><code>/todo/Todo.tsx</code></p>
<pre class="language-tsx"><code class="language-tsx"><span class="token keyword">export</span> <span class="token keyword">default</span> Todo<span class="token punctuation">;</span></code></pre>
<pre class="language-tsx"><code class="language-tsx"><span class="token keyword">import</span> Todo <span class="token keyword">from</span> <span class="token string">"../components/todo/Todo"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> TodoList <span class="token keyword">from</span> <span class="token string">"../components/todo/TodoList"</span><span class="token punctuation">;</span></code></pre>
</li>
<li>
<p>After</p>
<p><code>/todo/index.tsx</code></p>
<pre class="language-tsx"><code class="language-tsx"><span class="token keyword">export</span> <span class="token punctuation">{</span> <span class="token keyword">default</span> <span class="token keyword">as</span> TodoList <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"../todo/TodoList"</span><span class="token punctuation">;</span><br /><span class="token keyword">export</span> <span class="token punctuation">{</span> <span class="token keyword">default</span> <span class="token keyword">as</span> Todo <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"../todo/Todo"</span><span class="token punctuation">;</span></code></pre>
<pre class="language-tsx"><code class="language-tsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> Todo<span class="token punctuation">,</span> TodoList <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"../components/todo"</span><span class="token punctuation">;</span></code></pre>
</li>
</ul>
<h2 id="%E6%9C%AA%E4%BE%86%E5%B1%95%E6%9C%9B"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#%E6%9C%AA%E4%BE%86%E5%B1%95%E6%9C%9B">#</a> 未來展望</h2>
<blockquote>
<p>那些我還做不到、還沒做、也許未來也不會做的事</p>
</blockquote>
<ul>
<li>搜尋功能</li>
<li>標籤功能</li>
<li>拖拉 Todo 項目功能</li>
<li>完成/未完成篩選器</li>
<li>清空所有完成的 Todo 項目</li>
<li>看懂 React Hook Form</li>
<li>更新這篇文章,詳細描述各種 Pattern 的使用場景和理由。</li>
</ul>
<h2 id="%E6%84%9F%E8%AC%9D"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#%E6%84%9F%E8%AC%9D">#</a> 感謝</h2>
<p>天下文章一大抄,感謝巨人們的肩膀。</p>
<h3 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h3>
<ul>
<li><a href="https://blog.errorbaker.tw/posts/xiang/advanced-todo-list/">進階版 To do list</a></li>
<li><a href="https://github.com/react-hook-form/react-hook-form/tree/master/examples">React Hook Form Example</a></li>
<li><a href="https://codesandbox.io/s/react-hook-form-usefieldarray-nested-arrays-m8w6j">React Hook Form Example - useFieldArray</a></li>
<li><a href="https://react-hook-form.com/api/useform/">React Hook Form Api Document</a></li>
<li><a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#template-literal-types">Template Literal Types</a></li>
<li><a href="https://tailwindcss.com/">Tailwind</a></li>
<li><a href="https://react.geist-ui.dev/en-us">Geist UI</a></li>
<li><a href="https://styled-components.com/">Styled Component</a></li>
<li><a href="https://javascript.info/import-export#re-export">Export and Import</a></li>
<li><a href="https://create-react-app.dev/docs/adding-typescript/">Create React App</a></li>
<li><a href="https://vitejs.dev/guide/#scaffolding-your-first-vite-project">Vite</a></li>
</ul>
<h3 id="%E6%8E%A8%E8%96%A6%E9%96%B1%E8%AE%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#%E6%8E%A8%E8%96%A6%E9%96%B1%E8%AE%80">#</a> 推薦閱讀</h3>
<p><a href="https://kimtoday.medium.com/%E6%88%91%E6%98%AF%E5%A6%82%E4%BD%95%E9%96%8B%E5%A7%8B%E5%81%9A-%E4%B8%8D%E6%9C%83-%E7%9A%84%E4%BA%8B-8fe1c92cbe65">《我是如何開始做不會的事》</a></p>
<h2 id="%E5%82%99%E8%A8%BB"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/ts-react-pattern-nested-todo-list/#%E5%82%99%E8%A8%BB">#</a> 備註</h2>
<p>[1]紀錄那些我笨拙的時刻<br />
我第一個用 React 做的 Todo List(轉職前)<a href="https://github.com/futianshen/js-react-class-component-todo-list">https://github.com/futianshen/js-react-class-component-todo-list</a><br />
完成功能 <a href="https://github.com/futianshen/react-hook-form">https://github.com/futianshen/react-hook-form</a><br />
不斷優化 <a href="https://github.com/futianshen/ts-react-hook-form-nested-todo-list">https://github.com/futianshen/ts-react-hook-form-nested-todo-list</a><br />
改完收工 <a href="https://github.com/futianshen/nested-todo-list">https://github.com/futianshen/nested-todo-list</a></p>
<p>你有任何處理複雜表單,或學習新工具、新知識的經驗嗎?希望你能留言與我分享~</p>
在 AWS EB 上部署 Nodejs 網站的入門指南
2021-10-05T00:00:00Z
https://blog.errorbaker.tw/posts/sixwings/deploy-nodejs-to-AWS-EB-beginner's-guide/
<!-- summary -->
<!-- 在 Heroku 上部署網站是一個愉快的體驗,但在 AWS 上面要怎樣使用類似的服務呢? -->
<!-- summary -->
<!-- more -->
<h1 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/deploy-nodejs-to-AWS-EB-beginner&#%E5%89%8D%E8%A8%80">#</a> 前言</h1>
<p>這篇文章希望能夠以新手視角,大概說明怎麼在 AWS EB 上部署 Node.js 網站。AWS 包山包海的服務對於一開始接觸的人來說真的太多太複雜了,當初看 AWS 官方文件也覺得有段距離,是在自己實作、啃文件的過程慢慢理解到底這些東西在做什麼,所以想把這過程整理成一份入門指南。</p>
<h1 id="%E5%85%88%E7%9B%B4%E6%8E%A5%E4%BE%86"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/deploy-nodejs-to-AWS-EB-beginner&#%E5%85%88%E7%9B%B4%E6%8E%A5%E4%BE%86">#</a> 先直接來</h1>
<p>假設你已經註冊好 AWS 帳號,點下 <a href="https://ap-southeast-1.console.aws.amazon.com/elasticbeanstalk/home?region=ap-southeast-1#/welcome">Elastic Beanstalk 管理主控台</a> ,右上角可以選擇你想要把主機放在哪個區域,因為主機建立之後是不可以改區域的,先確認好自己想要的區域然後點選新增應用程式</p>
<p><img src="https://blog.errorbaker.tw/img/posts/sixwings/aws-eb-setup/f1.png" alt="選擇主機所在的區域" title="選擇主機所在的區域" /></p>
<p><img src="https://blog.errorbaker.tw/img/posts/sixwings/aws-eb-setup/f2.png" alt="建立 Web 應用程式" title="建立 Web 應用程式" /></p>
<p><strong>應用程式標籤</strong> 暫時用不到可以先忽略,然後程式碼也可以先用範例程式碼,要用自己的專案程式碼需要先壓縮打包才能上傳。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/sixwings/aws-eb-setup/f3.png" alt="建立應用程式" title="建立應用程式" /></p>
<p>等幾分鐘,環境建立好後應該會看到下面這個畫面</p>
<p><img src="https://blog.errorbaker.tw/img/posts/sixwings/aws-eb-setup/f4.png" alt="自動建立環境" title="自動建立環境" /></p>
<p><img src="https://blog.errorbaker.tw/img/posts/sixwings/aws-eb-setup/f5.png" alt="應用程式環境" title="應用程式環境" /></p>
<p>這時候就可以把你的 Nodejs 程式專案打包上傳部署<br />
如果你是用 git 做版本控管的話,有一個方便的打包指令可以用</p>
<p><code>git archive -o path-to-file/app.zip HEAD</code></p>
<p>上傳檔案後,會進入部署過程,等待幾分鐘之後迎接你的會是這個畫面</p>
<p><img src="https://blog.errorbaker.tw/img/posts/sixwings/aws-eb-setup/f6.png" alt="部署失敗" title="部署失敗" /></p>
<p>這時候就需要檢查一下發生什麼問題了</p>
<p><img src="https://blog.errorbaker.tw/img/posts/sixwings/aws-eb-setup/f7.png" alt="查詢日誌" title="查詢日誌" /></p>
<p>一般點選右邊的「請求日誌▼」,點「下 100 行」,過一陣子會顯示可以下載的日誌檔案,然後就可以看看發生什麼錯誤。一開始對設定還不熟常常需要在這邊尋找線索。</p>
<h1 id="%E8%AA%BF%E6%95%B4%E7%B4%B0%E9%83%A8%E8%A8%AD%E5%AE%9A"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/deploy-nodejs-to-AWS-EB-beginner&#%E8%AA%BF%E6%95%B4%E7%B4%B0%E9%83%A8%E8%A8%AD%E5%AE%9A">#</a> 調整細部設定</h1>
<p>基本上這邊的過程就是遇到什麼錯誤就調整什麼,雖然方法很笨,但過程中也會比較知道到底整個部署過程經歷了哪些環節?下面列出我在部署時候遇到的幾個錯誤:</p>
<h2 id="start-script-missing-error-when-running-npm-start"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/deploy-nodejs-to-AWS-EB-beginner&#start-script-missing-error-when-running-npm-start">#</a> Start script missing error when running npm start</h2>
<p>這個錯誤是說你沒有在 package.json 的 scripts 裡面設定 start 對應的指令,因為部署完之後程式會主動呼叫 <code>npm start</code> 讓伺服器開始跑起來。</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br /> ...<br /> <span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"test"</span><span class="token operator">:</span> <span class="token string">"echo \"Error: no test specified\" && exit 1"</span><span class="token punctuation">,</span><br /> <span class="token property">"start"</span><span class="token operator">:</span> <span class="token string">"node index.js"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> ...<br /><span class="token punctuation">}</span></code></pre>
<h2 id="error%3A-connect-econnrefused-127.0.0.1%3A3306"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/deploy-nodejs-to-AWS-EB-beginner&#error%3A-connect-econnrefused-127.0.0.1%3A3306">#</a> ERROR: connect ECONNREFUSED 127.0.0.1:3306</h2>
<p>沒有設定好資料庫,這時候要到左側的環境點選組態,然後設定資料庫。捲到畫面最底下會看到資料庫,點選 <strong>編輯</strong>。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/sixwings/aws-eb-setup/f8.png" alt="環境>組態" title="環境>組態" /></p>
<p>設定好自己的資料庫平台,按下<strong>套用</strong>後是一段漫長的等待,建置資料庫通常都蠻久的,大概 10 ~ 15 分鐘,建議先仔細檢查設定有沒有寫錯的地方,不然要更改的話又要再等很久。</p>
<p>參考文件:</p>
<ul>
<li><a href="https://docs.aws.amazon.com/zh_tw/elasticbeanstalk/latest/dg/using-features.managing.db.html">將資料庫新增至您的 Elastic Beanstalk 環境 - AWS Elastic Beanstalk</a></li>
<li><a href="https://docs.aws.amazon.com/zh_tw/elasticbeanstalk/latest/dg/create-deploy-nodejs.rds.html#nodejs-rds-connect">將 Amazon RDS 資料庫執行個體新增到您的 Node.js 應用程式環境 - AWS Elastic Beanstalk</a></li>
</ul>
<h2 id="%E7%84%A1%E6%B3%95%E9%80%8F%E9%81%8E-aws-eb-cli-%E4%B8%8B%E6%8C%87%E4%BB%A4-sequelize-db%3Amigrate"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/deploy-nodejs-to-AWS-EB-beginner&#%E7%84%A1%E6%B3%95%E9%80%8F%E9%81%8E-aws-eb-cli-%E4%B8%8B%E6%8C%87%E4%BB%A4-sequelize-db%3Amigrate">#</a> 無法透過 AWS EB cli 下指令 sequelize db:migrate</h2>
<p>使用 heroku cli 的時候,我們可以透過 <code>heroku run <command></code> 的方式去執行已經定義在 package.json scripts 裡面的指令。但 AWS EB cli 還沒有這種功能,也有可能我還沒找到之類的。但這個動作還蠻重要的,後來查了一下 npm script 的<a href="https://docs.npmjs.com/cli/v7/using-npm/scripts" title="scripts | npm Docs">說明文件</a></p>
<p>之後改成這樣</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br /> ...<br /> <span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"test"</span><span class="token operator">:</span> <span class="token string">"echo \"Error: no test specified\" && exit 1"</span><span class="token punctuation">,</span><br /> <span class="token property">"prestart"</span><span class="token operator">:</span> <span class="token string">"npx sequelize-cli db:migrate && npx sequelize-cli db:seed:undo:all && npx sequelize-cli db:seed:all"</span><span class="token punctuation">,</span><br /> <span class="token property">"start"</span><span class="token operator">:</span> <span class="token string">"node index.js"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> ...<br /><span class="token punctuation">}</span></code></pre>
<p>在 npm run start 之前,先跑 db:migrate 的指令,問題才算解決。<br />
另外 db:seed:all 的指令沒辦法連續執行,所以折衷的作法是每次開始前清掉全部的 seed,然後再執行 db:seed:all。</p>
<h2 id="%E8%AE%93-sequelize-%E7%9A%84%E8%A8%AD%E5%AE%9A%E5%8F%AF%E4%BB%A5%E4%BD%BF%E7%94%A8%E7%92%B0%E5%A2%83%E8%AE%8A%E6%95%B8"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/deploy-nodejs-to-AWS-EB-beginner&#%E8%AE%93-sequelize-%E7%9A%84%E8%A8%AD%E5%AE%9A%E5%8F%AF%E4%BB%A5%E4%BD%BF%E7%94%A8%E7%92%B0%E5%A2%83%E8%AE%8A%E6%95%B8">#</a> 讓 sequelize 的設定可以使用環境變數</h2>
<p>參考官方文件的 <a href="https://sequelize.org/v5/manual/migrations.html#using-environment-variables">Dynamic Configuration</a> 和 <a href="https://sequelize.org/v5/manual/migrations.html#using-environment-variables">Using Environment Variables</a> 這兩個章節的說明完成。</p>
<p>AWS EB 主控台設定環境變數的地方放在一個不是很好找的地方,看了<br />
<a href="https://docs.aws.amazon.com/zh_tw/elasticbeanstalk/latest/dg/environments-cfg-softwaresettings.html#environments-cfg-softwaresettings-console" title="環境屬性與其他軟體設定 - AWS Elastic Beanstalk">官方文件</a> 才知道是在組態中的<strong>軟體</strong>類別最底下才找到。</p>
<p>如果想要透過 AWS EB cli 設定的話,指令是 <code>eb setenv <ENV_VAR_NAME>=<VALUE> <ENV_VAR_NAME>=<VALUE> <ENV_VAR_NAME>=<VALUE> ...</code>,詳細用法可參考<a href="https://docs.aws.amazon.com/zh_tw/elasticbeanstalk/latest/dg/eb3-setenv.html" title="eb setenv - AWS Elastic Beanstalk">官方文件</a></p>
<h1 id="%E5%85%B6%E4%BB%96%E8%AD%B0%E9%A1%8C"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/deploy-nodejs-to-AWS-EB-beginner&#%E5%85%B6%E4%BB%96%E8%AD%B0%E9%A1%8C">#</a> 其他議題</h1>
<h2 id="aws-eb-cli"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/deploy-nodejs-to-AWS-EB-beginner&#aws-eb-cli">#</a> AWS EB cli</h2>
<p>對整個 AWS EB 的操作大概熟悉之後,這邊就可以開始學習用 <a href="https://docs.aws.amazon.com/zh_tw/elasticbeanstalk/latest/dg/eb-cli3-install.html">AWS EB cli</a> 了,主要是之後靠打指令部署程式碼還是比較便利。第一次用 cli 的時候 EB 會提示你要先<a href="https://docs.aws.amazon.com/zh_tw/general/latest/gr/aws-sec-cred-types.html#access-keys-and-secret-access-keys">建立一個帳戶金鑰</a>,之後這筆金鑰會存在 <code>C:\Users\USERNAME\.aws\config</code> 底下,以後下指令的時候就是依靠這裏面的金鑰做身分識別。</p>
<p>常用指令:<br />
<code>eb creat</code> 建立應用程式環境<br />
<code>eb init</code> 初始化相關設定<br />
<code>eb deploy</code> 把目前的專案部署到 AWS EB<br />
<code>eb open</code> 打開網站<br />
<code>eb logs</code> 檢查日誌</p>
<h2 id="%E8%A6%81%E5%A6%82%E4%BD%95%E9%80%8F%E9%81%8E-ssh-%E9%80%A3%E7%B7%9A%E5%88%B0-aws-eb-%E7%9A%84-ec2-%E5%9F%B7%E8%A1%8C%E5%AF%A6%E9%AB%94%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/deploy-nodejs-to-AWS-EB-beginner&#%E8%A6%81%E5%A6%82%E4%BD%95%E9%80%8F%E9%81%8E-ssh-%E9%80%A3%E7%B7%9A%E5%88%B0-aws-eb-%E7%9A%84-ec2-%E5%9F%B7%E8%A1%8C%E5%AF%A6%E9%AB%94%EF%BC%9F">#</a> 要如何透過 SSH 連線到 AWS EB 的 EC2 執行實體?</h2>
<p>可以先參考這兩篇官方文件教學進行設定</p>
<ul>
<li><a href="https://docs.aws.amazon.com/zh_tw/AWSEC2/latest/UserGuide/AccessingInstancesLinux.html">使用 SSH 連線至您的 Linux 執行個體 - Amazon Elastic Compute Cloud</a></li>
<li><a href="https://docs.aws.amazon.com/zh_tw/elasticbeanstalk/latest/dg/eb3-ssh.html">eb ssh - AWS Elastic Beanstalk</a></li>
</ul>
<p>AWS EB 會幫我們建立一個 EC2 的執行實體,我們需要有 EC2 金鑰對才可以透過 SSH 連線到執行實體。但是一開始在主控台建置應用程式的時候並沒有提示我們設定這個部分,事後要修改的話,一樣也是要透過修改組態的方式,但是修改組態之後又要等待一段漫長的時間。</p>
<p>但問題是用 AWS EB cli <code>eb inti --interactive</code> 的方式建置應用程式環境的時候,又會遇到 win10 跳錯誤提示說 SSH 沒安裝之類的問題,也如同 <a href="https://docs.aws.amazon.com/zh_tw/elasticbeanstalk/latest/dg/eb3-init.html">eb init</a> 說明的那樣,確認過自己電腦可以執行 <code>ssh-keygen</code> 但仍然一直鬼打牆無法順利執行 <code>eb init</code>。假如真的還是不行的話,可以用預先建立在 <code>C:\Users\USERNAME\.ssh\</code> 底下的金鑰對,或是在<a href="https://ap-southeast-1.console.aws.amazon.com/ec2/v2/home?region=ap-southeast-1#KeyPairs:">主控台</a> 產生金鑰對,然後再去修改組態更新。</p>
<p>確認<a href="https://docs.aws.amazon.com/zh_tw/AWSEC2/latest/UserGuide/ec2-key-pairs.html#verify-key-pair-fingerprints">自己的金鑰是不是跟上面的一致</a>?</p>
<p><img src="https://blog.errorbaker.tw/img/posts/sixwings/aws-eb-setup/f9.png" alt="組態>安全" title="組態>安全" /></p>
<p><code>eb ssh <app-env-name></code></p>
<h2 id="%E8%A6%81%E5%A6%82%E4%BD%95%E9%80%8F%E9%81%8E-workbench-%E9%80%A3%E7%B7%9A%E5%88%B0-aws-eb-%E7%9A%84-rds-%E8%B3%87%E6%96%99%E5%BA%AB%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/deploy-nodejs-to-AWS-EB-beginner&#%E8%A6%81%E5%A6%82%E4%BD%95%E9%80%8F%E9%81%8E-workbench-%E9%80%A3%E7%B7%9A%E5%88%B0-aws-eb-%E7%9A%84-rds-%E8%B3%87%E6%96%99%E5%BA%AB%EF%BC%9F">#</a> 要如何透過 Workbench 連線到 AWS EB 的 RDS 資料庫?</h2>
<p>最關鍵的點是需要幫 RDS 的執行實體增加一個新的安全群組,然後編輯傳入規則。<br />
<img src="https://blog.errorbaker.tw/img/posts/sixwings/aws-eb-setup/f10.png" alt="編輯傳入規則" title="編輯傳入規則" /></p>
<p><strong>連線與安全性</strong> 裡面的 <strong>可公開存取</strong> 要設定「是」<br />
<img src="https://blog.errorbaker.tw/img/posts/sixwings/aws-eb-setup/f11.png" alt="確認可否公開存取" title="確認可否公開存取" /></p>
<p>如果無法連線請參考以下兩篇文件排除問題</p>
<ul>
<li><a href="https://aws.amazon.com/tw/premiumsupport/knowledge-center/rds-cannot-connect/">解決連線至 Amazon RDS 資料庫執行個體時發生的問題</a></li>
<li><a href="https://docs.aws.amazon.com/zh_tw/AmazonRDS/latest/UserGuide/CHAP_Troubleshooting.html#CHAP_Troubleshooting.Connecting">Amazon RDS 故障診斷 - Amazon Relational Database Service</a></li>
</ul>
<h2 id="%E8%A8%AD%E5%AE%9A-https"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/deploy-nodejs-to-AWS-EB-beginner&#%E8%A8%AD%E5%AE%9A-https">#</a> 設定 https</h2>
<p>參考文件</p>
<ul>
<li><a href="https://docs.aws.amazon.com/zh_tw/elasticbeanstalk/latest/dg/configuring-https.html">為您的 Elastic Beanstalk 環境設定 HTTPS - AWS Elastic Beanstalk</a></li>
<li><a href="https://docs.aws.amazon.com/zh_tw/elasticbeanstalk/latest/dg/configuring-https-elb.html">設定您 Elastic Beanstalk 環境的負載平衡器來實現 HTTPS - AWS Elastic Beanstalk</a><br />
官方說這是最容易實現的,但有些應用程式環境沒有提供負載平衡器,需要<a href="https://docs.aws.amazon.com/zh_tw/elasticbeanstalk/latest/dg/using-features-managing-env-types.html#using-features.managing.changetype">重新設定環境</a>。</li>
<li><a href="https://docs.aws.amazon.com/zh_tw/elasticbeanstalk/latest/dg/https-singleinstance-nodejs.html">在執行 Node.js 的 EC2 執行個體上實現 HTTPS - AWS Elastic Beanstalk</a></li>
</ul>
<p>建議一起設定</p>
<ul>
<li><a href="https://docs.aws.amazon.com/zh_tw/elasticbeanstalk/latest/dg/configuring-https-httpredirect.html">將 HTTP 設定為 HTTPS 重新導向 - AWS Elastic Beanstalk</a><br />
這個請注意自己的環境上有沒有 Application Load Balancer 再使用裡面的 <a href="https://github.com/awsdocs/elastic-beanstalk-samples/tree/master/configuration-files/aws-provided/security-configuration/https-redirect">https-redirect</a> ,然後用錯可能整個環境會爆炸而且修不回來。 <strong>強烈建議: 對 nginx 的設定熟悉才使用裡面的檔案</strong></li>
<li><a href="https://docs.aws.amazon.com/zh_tw/elasticbeanstalk/latest/dg/https-storingprivatekeys.html">將私有金鑰安全地儲存於 Amazon S3 中 - AWS Elastic Beanstalk</a><br />
注意 S3 要關閉所有公開存取,不然還是會有問題</li>
</ul>
<p><img src="https://blog.errorbaker.tw/img/posts/sixwings/aws-eb-setup/f12.png" alt="封鎖公有存取權" title="封鎖公有存取權" /></p>
<p>以上這邊主要是用 nginx 設定,如果是採用 apache 的話,可以再查其他文件教學</p>
<h1 id="%E7%B5%90%E8%AA%9E"><a class="direct-link" href="https://blog.errorbaker.tw/posts/sixwings/deploy-nodejs-to-AWS-EB-beginner&#%E7%B5%90%E8%AA%9E">#</a> 結語</h1>
<p>因為用了 AWS EB 的關係,對整個 AWS 服務的生態系終於有比較了解的感覺。 EC2 是虛擬主機、RDS 是虛擬資料庫,S3 是永久儲存服務。中間因為搞不太懂 VPC 的概念,也順便惡補了一下相關知識。經過這次 EB 部署經驗,有比較了解系統是怎麼運作和維護的,也感受到一個龐大系統如果沒依靠一些自動化工具輔助管理,真的會維護的很辛苦。</p>
<p>我是 sixwings,善於挖坑的程式人,我們下次見!</p>
介紹 Cookie 的新屬性 SameParty
2021-10-05T00:00:00Z
https://blog.errorbaker.tw/posts/umer/cookie-sameparty/
<!-- summary -->
<!-- 介紹 Cookie 的新屬性 SameParty -->
<!-- summary -->
<!-- more -->
<h1 id="%E4%BB%8B%E7%B4%B9-cookie-%E7%9A%84%E6%96%B0%E5%B1%AC%E6%80%A7-sameparty"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/cookie-sameparty/#%E4%BB%8B%E7%B4%B9-cookie-%E7%9A%84%E6%96%B0%E5%B1%AC%E6%80%A7-sameparty">#</a> 介紹 Cookie 的新屬性 SameParty</h1>
<p><img src="https://i.imgur.com/jYgKebp.png" alt="" /></p>
<p>最近研究 Cookie 的時候發現有一個之前沒看過的屬性,SameParty 屬性,一查之後發現是個挺新的屬性,查到的資料普遍在最近兩年而且還在開發中(但是 <a href="https://www.chromestatus.com/feature/5280634094223360">chrome</a> 已經在支援了),並且幾乎都是英文資料,所以這篇文章就來介紹我目前對這個新屬性的理解以及它想解決的問題。</p>
<h2 id="%E7%AC%AC%E4%B8%80%E6%96%B9-cookie-%E8%88%87%E7%AC%AC%E4%B8%89%E6%96%B9-cookie"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/cookie-sameparty/#%E7%AC%AC%E4%B8%80%E6%96%B9-cookie-%E8%88%87%E7%AC%AC%E4%B8%89%E6%96%B9-cookie">#</a> 第一方 Cookie 與第三方 Cookie</h2>
<p>Cookie 本質上沒有第一方、第三方之分,這兩種分法是一種相對的概念,取決於 Cookie 的來源以及傳送 Cookie 的目的地,例如我在瀏覽露天拍賣(<a href="http://sale.com/">sale.com</a>)的時候,把購物車裡面的商品內容存在 Cart Cookie,當我在瀏覽 PChome (<a href="http://pchome.com/">pchome.com</a>)的時候,由於兩者的域名不同,因此對於 PChome 而言, Cart Cookie 就是一個第三方 Cookie,對於露天而言則是第一方 Cookie。</p>
<h2 id="samesite-%E7%9A%84%E4%B8%89%E7%A8%AE%E5%80%BC-strict%2C-lax%2C-none"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/cookie-sameparty/#samesite-%E7%9A%84%E4%B8%89%E7%A8%AE%E5%80%BC-strict%2C-lax%2C-none">#</a> SameSite 的三種值 Strict, Lax, None</h2>
<p>那麼,該怎麼限制 Cookie 是否要傳送(限制原因比如 Cookie 有存放重要資訊,或是避免 CSRF)?方法之一是透過設定 Cookie 的 SameSite 屬性。<br />
這個屬性有三種值。</p>
<p><code>Strict</code>,只允許第一方 Cookie 傳送,不會傳送給任何不同域名(不會有 cross-site requests),可以避免一定程度的 CSRF</p>
<p><code>Lax</code>,允許第一方 Cookie 傳送,和某些條件下的第三方 Cookie 傳送,例如</p>
<ul>
<li>輸入網址</li>
<li><code><a href="..."></code></li>
<li><code><form method="GET"></code></li>
<li><code><link rel="prerender" href="..."></code></li>
<li><code><form method="POST"></code></li>
</ul>
<p><code>None</code>,允許第三方 Cookie 傳送</p>
<p><a href="https://ithelp.ithome.com.tw/articles/10251288">https://ithelp.ithome.com.tw/articles/10251288</a></p>
<h2 id="samesite-%E7%9A%84%E7%BC%BA%E9%BB%9E"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/cookie-sameparty/#samesite-%E7%9A%84%E7%BC%BA%E9%BB%9E">#</a> SameSite 的缺點</h2>
<p>目前隱私意識崛起,主流瀏覽器也打算逐步禁用第三方 Cookie,雖然有<code>SameSite=Lax</code>這個屬性能在有限程度上傳送第三方 Cookie,但是在實作上通常一家大廠都不會只有單一一個域名,而是會有許多域名來提供各種服務,這些域名的 Cookie 彼此之間都是第三方 Cookie,但都是為在同一個廠商底下管理,彼此之間要共享 Cookie 實在不容易,因為對瀏覽器而言它們都是第三方 Cookie。</p>
<p>以前面的 PChome 例子來說,PChome 其實有很多子公司,並且露天也是它的合資企業之一,更不用說一家大廠也可能委託其他廠商來提供服務,如果 PChome 和露天或是其他合作廠商想要共享某些 Cookie,那麼在 SameSite 屬性底下其實很難做到。</p>
<h2 id="chromium-projects-%E7%9A%84-first-party-sets"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/cookie-sameparty/#chromium-projects-%E7%9A%84-first-party-sets">#</a> Chromium projects 的 First-Party Sets</h2>
<p>針對同一廠商底下會有多個域名想共享 Cookie 的情況,Google 使用 First-Party Sets 來實作這個功能。<br />
在想要共享 Cookie 的 Server 底下的路徑添加一個 JSON 檔案 <code><owner site>/.well-known/first-party-set</code>,檔案裡面需要紀錄共享域名們的相關資訊,↓</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br />owner<span class="token operator">:</span><span class="token string">"https://fps-owner.example"</span><span class="token punctuation">,</span><br />members<span class="token operator">:</span><span class="token punctuation">[</span><br /><span class="token string">"https://fps-member1.example"</span><span class="token punctuation">,</span><br /><span class="token string">"https://fps-member2.example"</span><br /><span class="token punctuation">]</span><br /><span class="token punctuation">}</span></code></pre>
<p>以及 membersite 底下也要有相對應的配置↓</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br /> owner<span class="token operator">:</span> <span class="token string">"https://fps-owner.example"</span><br /><span class="token punctuation">}</span></code></pre>
<p>更詳細的配置和測試方式可以參考<a href="https://www.chromium.org/updates/first-party-sets">Chromium projects</a></p>
<h2 id="cookie-%E7%9A%84%E6%96%B0%E5%B1%AC%E6%80%A7-sameparty"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/cookie-sameparty/#cookie-%E7%9A%84%E6%96%B0%E5%B1%AC%E6%80%A7-sameparty">#</a> Cookie 的新屬性 SameParty</h2>
<p>使用了 First-Party Sets 的域名要共享 Cookie 之前,需要設定 Cookie 的 SameParty 屬性<br />
<code>Set-Cookie: name=tasty; Secure; SameSite=Lax; SameParty</code>(SameParty 在比較新的 Chrome 瀏覽器才有,Google 建議加上 SameSite 來支援較舊的瀏覽器版本)。<br />
如此一來,不同域名之間(<a href="http://example.com/">example.com</a>, <a href="http://example.rs/">example.rs</a>, <a href="http://example.co.uk/">example.co.uk</a>)就可以共享 Cookie 了。</p>
<h2 id="%E7%B8%BD%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/cookie-sameparty/#%E7%B8%BD%E7%B5%90">#</a> 總結</h2>
<p>SameParty 屬性在我看來類似於 SameSite=Strict 的延伸版,也有對 CSRF 做處理,具體的 Cookie 傳送規則可以參考這張圖片。</p>
<p><code>owner.example</code> 擁有這兩個網域 <code>member1.example</code>, <code>member2.example</code>,<code>member1.example</code>,在設定了 SameParty 的情況下,<code>member1.example</code> 的 Cookie 不會傳送到非 SameParty 的網域。<br />
<img src="https://raw.githubusercontent.com/cfredric/sameparty/main/images/same_party_table.png" alt="SameParty Rule" /></p>
<p>同時,<code>member1.example</code> 也不會把 Cookie 傳給 <code>member2.example</code><br />
<img src="https://github.com/cfredric/sameparty/raw/main/images/same_party_sop.png" alt="SameParty Rule" /></p>
<p>在查資料的途中,發現到 W3C 其實不贊同 SameParty 的功能(<a href="https://www.theregister.com/2021/04/08/w3c_google_multple_domains/">來源</a>),即使如此 Chrome 已經在 Chrome 89 以上實作這個功能 ( <a href="https://github.com/cfredric/sameparty">Google</a> )瀏覽器了,使用了 Chromium 的 Edge 瀏覽器也有這個功能,兩方勢均力敵。</p>
<h2 id="%E7%9B%B8%E9%97%9C%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/cookie-sameparty/#%E7%9B%B8%E9%97%9C%E8%B3%87%E6%96%99">#</a> 相關資料</h2>
<p><a href="https://segmentfault.com/a/1190000040161207">SameSite 那些事</a><br />
<a href="https://juejin.cn/post/7002011181221167118#heading-5">详解 Cookie 新增的 SameParty 属性</a><br />
<a href="https://github.com/privacycg/first-party-sets7">SFirst-Party Sets</a><br />
<a href="https://github.com/cfredric/sameparty">SameParty cookie attribute explainer<br />
</a></p>
聊聊 JavaScript 中的深拷貝 (上)
2021-10-08T00:00:00Z
https://blog.errorbaker.tw/posts/clay/copy/
<!-- summary -->
<!-- 簡單來理解一下做深拷貝時會遇到的幾個問題 -->
<!-- summary -->
<h3 id="%E7%B7%A3%E8%B5%B7"><a class="direct-link" href="https://blog.errorbaker.tw/posts/clay/copy/#%E7%B7%A3%E8%B5%B7">#</a> 緣起</h3>
<p>記得過去剛學習 JavaScript 的時候,最讓我感興趣的,就是在變數為物件型別的賦值上,實際是 call by reference 而不是 call by value,由於 JavaScript 是我第一個學習的程式語言,當時花了不少時間才理解這究竟是怎麼一回事。</p>
<p>在工作一段時間之後,仍然也能在各種不同大小的專案裡面看到物件型別拷貝的幾種用法,自己過去雖然明白這些方法各有其利弊,但一直沒有好好的做個整理與探究背後的原理,所以這一文章的上篇,就是先來看目前我們已知的深拷貝處理上,可能會存在著哪些問題。</p>
<blockquote>
<p>基礎雖然簡單,卻也最容易藏有陷阱,因此將過程記錄下來,無非是一個最好的方式。</p>
</blockquote>
<h3 id="%E5%9F%BA%E7%A4%8E"><a class="direct-link" href="https://blog.errorbaker.tw/posts/clay/copy/#%E5%9F%BA%E7%A4%8E">#</a> 基礎</h3>
<p>最基本的問題,就是假設我們有一個物件 <code>obj</code>,我們現在希望能有另外一個與 <code>obj</code> 一模ㄧ樣的 <code>copyObj</code>,我們能怎麼做?</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> obj <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'Clay'</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">const</span> copyObj <span class="token operator">=</span> obj</code></pre>
<p>你知道這樣做是有問題的,因為當我接著修改 <code>copyObj</code> 時,<code>obj</code> 也會被修改:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> obj <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'Clay'</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">const</span> copyObj <span class="token operator">=</span> obj<br /><br />copyObj<span class="token punctuation">.</span>age <span class="token operator">=</span> <span class="token number">18</span><br /><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>obj<span class="token punctuation">)</span> <span class="token comment">// { name: 'Clay', age: 18 }</span></code></pre>
<p>聰明的你這時候會想到一個還不錯的方法,就是利用 ES6 的展開運算子幫我們做處理:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> obj <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'Clay'</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">const</span> copyObj <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token operator">...</span>obj <span class="token punctuation">}</span><br /><br />copyObj<span class="token punctuation">.</span>age <span class="token operator">=</span> <span class="token number">18</span><br /><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>obj<span class="token punctuation">)</span> <span class="token comment">// { name: 'Clay' }</span></code></pre>
<p>簡單形容一下上面的做法,上述的做法是給予 <code>copyObj</code> 一個新的物件,所以 <code>copyObj</code> 會指向一個新的記憶體位置,與 <code>obj</code> 不同。展開運算子幫我們將 <code>obj</code> 的 key/value 放到 <code>copyObj</code> 裏面,所以 <code>obj</code> 的記憶體位置與 <code>copyObj</code> 的記憶體位置毫不相關,修改上彼此也不會互相影響。</p>
<p>同樣的概念放在 Array 中來實作,也是一樣的結果:</p>
<ul>
<li>
<p>A 方法</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> arr <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">,</span><span class="token number">2</span><span class="token punctuation">,</span><span class="token number">3</span><span class="token punctuation">]</span><br /><br /><span class="token keyword">const</span> copyArr <span class="token operator">=</span> arr<br /><br />copyArr<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token number">4</span><span class="token punctuation">)</span><br /><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>arr<span class="token punctuation">)</span> <span class="token comment">// [1, 2, 3, 4]</span></code></pre>
</li>
<li>
<p>B 方法</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> arr <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">,</span><span class="token number">2</span><span class="token punctuation">,</span><span class="token number">3</span><span class="token punctuation">]</span><br /><br /><span class="token keyword">const</span> copyArr <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token operator">...</span>arr<span class="token punctuation">]</span><br /><br />copyArr<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token number">4</span><span class="token punctuation">)</span><br /><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>arr<span class="token punctuation">)</span> <span class="token comment">// [1, 2, 3]</span></code></pre>
</li>
</ul>
<blockquote>
<p>上述的 A 方法,由於 copy 時還是有 copied by reference,所以這樣的行為我們將其稱之為<strong>淺拷貝 (Shallow Copy)</strong></p>
</blockquote>
<blockquote>
<p>而 B 方法所複製的物件型別 (<code>copyObj</code> 或 <code>copyArr</code>),由於已經與原本的 <code>obj</code>或 <code>arr</code> 沒有共同參考的記憶體位置,此種完整的拷貝,我們稱之為<strong>深拷貝 (Deep Copy)</strong></p>
</blockquote>
<p>但要達成深拷貝,不是只有使用展開運算子才可以做到。事實上,還是有展開運算子無法處理的深拷貝需求,我們可以接著來看。</p>
<blockquote>
<p>我這裡沒有另外提到 <code>Object.assign()</code> 這個方法,因為在這個範例中,它與展開運算子達到的效果會是一樣的 (可能也更適合),所以就沒有另外做舉例了,讀者可以自己尋找最適合自己的方式。</p>
</blockquote>
<h3 id="%E8%B6%85%E9%81%8E%E4%B8%80%E5%B1%A4%E4%BB%A5%E4%B8%8A%E7%9A%84%E7%89%A9%E4%BB%B6"><a class="direct-link" href="https://blog.errorbaker.tw/posts/clay/copy/#%E8%B6%85%E9%81%8E%E4%B8%80%E5%B1%A4%E4%BB%A5%E4%B8%8A%E7%9A%84%E7%89%A9%E4%BB%B6">#</a> 超過一層以上的物件</h3>
<p>一樣,我們現在來舉別的例子,我有一個 <code>obj</code>,這次的它是一個兩層的物件,我們來看對其使用展開運算子來做深拷貝,會發生什麼樣的事:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> obj <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">old</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'Clay'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">age</span><span class="token operator">:</span> <span class="token number">30</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">young</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'Xen'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">age</span><span class="token operator">:</span> <span class="token number">18</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">const</span> copyObj <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token operator">...</span>obj <span class="token punctuation">}</span><br /><br />copyObj<span class="token punctuation">.</span>young<span class="token punctuation">.</span>age <span class="token operator">=</span> <span class="token number">21</span><br /><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>obj<span class="token punctuation">)</span> <span class="token comment">// { old: { name: 'Clay', age: 30 }, young: { name: 'Xen', age: 21 } }</span></code></pre>
<p>我們使用展開運算子方式,期許可以達到與前面一樣的成果,然而,當我修改 <code>copyObj.young.age</code> 的內容時,<code>obj.young.age</code> 也被修改了。</p>
<p>但有個狀況很有趣,如果你修改的是 <code>copyObj.young</code>,<code>obj.young</code> 則不會受到影響:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> obj <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">old</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'Clay'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">age</span><span class="token operator">:</span> <span class="token number">30</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">young</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'Xen'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">age</span><span class="token operator">:</span> <span class="token number">18</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">const</span> copyObj <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token operator">...</span>obj <span class="token punctuation">}</span><br /><br />copyObj<span class="token punctuation">.</span>young <span class="token operator">=</span> <span class="token number">123456</span><br /><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>obj<span class="token punctuation">)</span> <span class="token comment">// { old: { name: 'Clay', age: 30 }, young: { name: 'Xen', age: 18 } }</span><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>obj <span class="token operator">===</span> copyObj<span class="token punctuation">)</span> <span class="token comment">// false</span><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>obj<span class="token punctuation">.</span>young <span class="token operator">===</span> copyObj<span class="token punctuation">.</span>young<span class="token punctuation">)</span> <span class="token comment">// false</span><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>obj<span class="token punctuation">.</span>young<span class="token punctuation">.</span>age <span class="token operator">===</span> copyObj<span class="token punctuation">.</span>young<span class="token punctuation">.</span>age<span class="token punctuation">)</span> <span class="token comment">// true</span></code></pre>
<p>即使我們都知道 ES6 的展開運算子無法處理一層以上的深拷貝,但上述的狀況才是讓我覺得最危險的,因為假設我不知道 <code>copyObj</code> 是怎麼來的,當我修改 <code>copyObj.young</code> 時,我都還可能會以為它是一個被<strong>深拷貝</strong>的物件,但實際上,它仍應算是<strong>淺拷貝</strong>,而直到你找到是哪一層原來是 copied by reference 之前,你很可能都不會知道這件事 (或者你去追源頭,看它是怎麼被拷貝的)。</p>
<p>不過,在大部分情況下,因為淺拷貝所產生出來的 Bug 可能都早過你發現之前先發生,這不是我們所希望的狀況。</p>
<h2 id="%E7%94%A8-json.parse(json.stringify(obj))-%E4%BE%86%E5%81%9A%E6%B7%B1%E6%8B%B7%E8%B2%9D-%3F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/clay/copy/#%E7%94%A8-json.parse(json.stringify(obj))-%E4%BE%86%E5%81%9A%E6%B7%B1%E6%8B%B7%E8%B2%9D-%3F">#</a> 用 <code>JSON.parse(JSON.stringify(obj))</code> 來做深拷貝 ?</h2>
<p><code>JSON.stringify()</code> 可以幫我們把物件或是陣列做<a href="https://zh.wikipedia.org/wiki/%E5%BA%8F%E5%88%97%E5%8C%96">序列化處理</a>,<code>JSON.parse()</code> 則可以幫我們做到反序列化。</p>
<p>使用 <code>JSON.parse(JSON.stringify(obj))</code> 的組合技,先將物件型別轉為字串,再將字串轉為物件,就可以實現深拷貝:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> obj <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">old</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">member</span> <span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'Clay'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">age</span><span class="token operator">:</span> <span class="token number">30</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">const</span> copyObj <span class="token operator">=</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span><span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>obj<span class="token punctuation">)</span><span class="token punctuation">)</span><br /><br />copyObj<span class="token punctuation">.</span>old<span class="token punctuation">.</span>member<span class="token punctuation">.</span>name <span class="token operator">=</span> <span class="token number">123456</span><br /><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>obj<span class="token punctuation">)</span> <span class="token comment">// { old: { member: { name: 'Clay', age: 30 } } }</span><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>obj <span class="token operator">===</span> copyObj<span class="token punctuation">)</span> <span class="token comment">// false</span><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>obj<span class="token punctuation">.</span>young <span class="token operator">===</span> copyObj<span class="token punctuation">.</span>young<span class="token punctuation">)</span> <span class="token comment">// false</span><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>obj<span class="token punctuation">.</span>old<span class="token punctuation">.</span>member<span class="token punctuation">.</span>name <span class="token operator">===</span> copyObj<span class="token punctuation">.</span>old<span class="token punctuation">.</span>member<span class="token punctuation">.</span>name<span class="token punctuation">)</span> <span class="token comment">// false</span></code></pre>
<p>但這樣做還是會有問題,而且問題還不小,原因是因為當你物件中有 <code>function</code>,<code>NaN</code>,或是 <code>new Date()</code> 等類型的值時,會無法按照原樣進行拷貝,不論資料結構的深淺都一樣:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> obj <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">num</span><span class="token operator">:</span> <span class="token number">123</span><span class="token punctuation">,</span><br /> <span class="token function-variable function">func</span><span class="token operator">:</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'hello ~'</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">const</span> copyObj <span class="token operator">=</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span><span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>obj<span class="token punctuation">)</span><span class="token punctuation">)</span><br /><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>copyObj<span class="token punctuation">)</span> <span class="token comment">// { bar: 123 }</span><br />copyObj<span class="token punctuation">.</span><span class="token function">func</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token comment">// copyObj.func is not a function</span></code></pre>
<p>如上案例,<code>copyObj</code> 並沒有拷貝到 <code>obj.func</code>,實際上,<code>JSON.stringify(obj)</code> 執行之後的結果就已經是 <code>{"num":123}</code> 了,看不到 <code>func</code> 的蹤影。</p>
<p>這與 <code>JSON.stringify()</code> 的特性有關,對於無法序列化的值,比如說 <code>Date</code>,<code>undefined</code> 或是 <code>Function</code> 等,會在序列化時被忽略或是轉為 <code>null</code>,或是在經由 <code>JSON.parse()</code> 轉換時無法呈現原本的形式。</p>
<blockquote>
<p>整理 <code>JSON.parse(JSON.stringify())</code> 時會遇到狀況的幾種類型:</p>
</blockquote>
<ol>
<li><code>undefined</code></li>
<li><code>null</code></li>
<li><code>Function</code></li>
<li><code>Date</code></li>
<li><code>RegExp</code></li>
<li><code>Symbol</code></li>
<li><code>Error</code></li>
<li><code>NaN</code></li>
<li><code>Infinity</code></li>
<li><code>-Infinity</code></li>
</ol>
<p>因為上述的特性,所以當我們使用 <code>JSON.parse(JSON.stringify())</code> 來做深拷貝時,就會多出一些無法掌控,或是你必須先行了解的風險,但 <code>JSON.parse(JSON.stringify())</code> 所帶來的問題,可能不止於此。</p>
<p>最近有不少文章在探討 <code>JSON.parse(JSON.stringify())</code> 帶來的效能問題,它在處理龐大或深層的資料時,運行速度並不是這麼理想,當我們在一個專案中大量的使用這樣的深拷貝方式,可能會產生一些潛在的效能問題,詳情可以參考<a href="https://medium.com/ft-product-technology/this-one-line-of-javascript-made-ft-com-10-times-slower-5afb02bfd93f">This one line of Javascript made FT.com 10 times slower</a> 這篇文章,內中鉅細靡遺地描述了他們所遇到的問題,從發現,測試,到找出原因是由於專案中大量使用 <code>JSON.parse(JSON.stringify())</code> 造成的效能拖累,非常值得一看。</p>
<p><code>JSON.parse(JSON.stringify())</code> 還有一個問題,由於 <code>JSON.parse()</code> 中的對象單純是一個字串,對 <code>JSON.parse()</code> 來說,他不會知道內中 <code>JSON.stringify(obj)</code> 的前世是什麼樣子,所以如果 <code>obj</code> 本身有一些的內容有一些引用,那麼由 <code>JSON.parse()</code> 反序列化所產生的物件則完全不會知道這件事。</p>
<p>舉個例子,在 <code>arr = [obj, obj]</code> 的狀況下,兩個 <code>obj</code> 都是同一個記憶體位置,但是經由深拷貝之後就完全不是這麼回事:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> obj <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">a</span><span class="token operator">:</span> <span class="token number">1</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">const</span> arr <span class="token operator">=</span> <span class="token punctuation">[</span>obj<span class="token punctuation">,</span> obj<span class="token punctuation">]</span><br /><br /><span class="token keyword">const</span> copyArr <span class="token operator">=</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span><span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>arr<span class="token punctuation">)</span><span class="token punctuation">)</span><br /><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>arr<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token operator">===</span> arr<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token comment">// true</span><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>copyArr<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token operator">===</span> copyArr<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token comment">// false</span></code></pre>
<p><code>copyArr[0] === copyArr[1]</code> 為 <code>false</code>,這是因為 <code>JSON.parse()</code> 在轉換的過程中只看得見字串,這是一個很重要的原因使我們<strong>不能</strong>一看到 <code>JSON.parse(JSON.stringify())</code> 就去將其與深拷貝掛在一起做聯想,因為原本的引用關係並沒有連帶被拷貝過來。</p>
<p>只要拆分 <code>JSON.stringify(obj)</code> 與 <code>JSON.parse()</code> 的執行步驟,就可以發現不少深拷貝相關的問題,以我自己來說,也不建議使用 <code>JSON.parse(JSON.stringify(obj))</code> 來做深拷貝,除非我可以很確保我可以預見所有的 side effect。</p>
<blockquote>
<p>補充:關於 <code>JSON.stringify()</code> 的效能優化,現行也有一些相關的開源套件來做取代,比如說 <a href="https://github.com/fastify/fast-json-stringify">fast-json-stringify</a> 或是 <a href="https://github.com/lucagez/slow-json-stringify">slow-json-stringify</a>,未來有機會的話也來研究這一塊。</p>
</blockquote>
<h2 id="%E5%9B%9E%E9%A1%A7%E8%88%87%E5%BE%8C%E7%BA%8C"><a class="direct-link" href="https://blog.errorbaker.tw/posts/clay/copy/#%E5%9B%9E%E9%A1%A7%E8%88%87%E5%BE%8C%E7%BA%8C">#</a> 回顧與後續</h2>
<p>上述遇到 <code>JSON.parse(JSON.stringify())</code> 的相關問題,比如說物件型別中有 <code>undefined</code>,<code>new RegExp</code> 或是 <code>new Date()</code> 時的拷貝錯誤,如果使用展開運算子或是 <code>Object.assign()</code> 來處理,就可以順利解決,仔細想想真的滿有趣的,有種魚與熊掌難以兼得的感覺,這也讓人不太意外,畢竟 JavaScript 在處理拷貝的這件事情上面,天生就不屬於一件直覺且直接的事情。</p>
<p>在實務上,如果是比較小的專案,當物件或是陣列等資料結構較單純時,我會使用展開運算子或是 <code>Object.assign()</code> 來實現<br />
一層的深拷貝,但大多數狀況下我覺得最保險的是使用 <code>lodash</code> 的 <a href="https://lodash.com/docs/#cloneDeep"><code>cloneDeep</code></a>,儘管在效能上可能沒有原生方法來得好 (可以參考這篇 StackOverflow 上的這篇<a href="https://stackoverflow.com/questions/728360/how-do-i-correctly-clone-a-javascript-object/61523278#61523278">討論</a>),但的確是相對保險與經得起驗證的做法。</p>
<p>這篇文章其實可以延伸討論的點滿多的,包括如果我們知道上述的深拷貝部分都有自己的問題,那該如何做最好 (儘管我們知道可以使用 lodash 的 cloneDeep,但它是如何做到的?),如果自己實現深拷貝,我們應該要注意什麼部分 (原型鏈與 <code>hasOwnProperty</code> 的使用),以及如果 <code>JSON.stringify()</code> 的序列化效能若不盡理想,那又是為什麼?</p>
<p>這也是我想把這篇文章拆成上下兩個篇章的原因,先闡述可能遇到的問題,再往我們可以怎麼去解決它,並從中學習到什麼內容的方向邁進。</p>
<p>感謝您的閱讀,如有描述錯誤,請不吝指教。</p>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/clay/copy/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li><a href="https://stackoverflow.com/questions/728360/how-do-i-correctly-clone-a-javascript-object/61523278#61523278">How do I correctly clone a JavaScript object?</a></li>
<li><a href="https://segmentfault.com/a/1190000019400854">如何提升JSON.stringify()的性能?</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/67374716">你不知道的JSON.parse()和JSON.stringify()</a></li>
</ul>
Debug 系列 01
2021-10-10T00:00:00Z
https://blog.errorbaker.tw/posts/xiang/debug-case-01/
<!-- summary -->
<!-- # 這個系列文要來分享工作上 debug 的案例 -->
<!-- summary -->
<!-- more -->
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/debug-case-01/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<p>工作上時不時會遇到一些有趣的 bug,它可能技術含量不見得那麼高,沒辦法獨立寫成一篇文章,但卻又值得拿來跟大家分享。<br />
於是我決定打算把這些小 bug 們累積起來,一次多分享幾個,讓內容可以更貼近工作上遇到的實際狀況,也可以當作自己處理完問題後留下來的一次紀錄。</p>
<h2 id="%E6%9C%AC%E7%AF%87%E5%85%A7%E5%AE%B9"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/debug-case-01/#%E6%9C%AC%E7%AF%87%E5%85%A7%E5%AE%B9">#</a> 本篇內容</h2>
<ul>
<li>dispatchEvent 的烏龍事件</li>
<li>tab 導致的頁面跑版</li>
<li>font-weight 無法控制的粗細問題</li>
<li>表單 tab 無法跳至下一題</li>
</ul>
<h4 id="%E2%98%9E-%E6%A1%88%E4%BE%8B%E4%B8%80%EF%BC%9A-dispatchevent-%E7%9A%84%E7%83%8F%E9%BE%8D%E4%BA%8B%E4%BB%B6"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/debug-case-01/#%E2%98%9E-%E6%A1%88%E4%BE%8B%E4%B8%80%EF%BC%9A-dispatchevent-%E7%9A%84%E7%83%8F%E9%BE%8D%E4%BA%8B%E4%BB%B6">#</a> ☞ 案例一: dispatchEvent 的烏龍事件</h4>
<p>這個案例是在實作一個 <strong>拖曳式檔案上傳</strong>(Dropzone) 元件遇到的,當我們將第一個檔案拖曳進去上傳時一切都很正常,但是再將另一個檔案拖曳進來後,會使第一次上傳的檔案又被重複上傳,明明只拖曳兩次,卻上傳了三個檔案。由於是在測試階段發現的問題,所以回頭去看程式碼有沒有什麼問題。</p>
<p>當下的程式碼是這樣寫的:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"drop"</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>_onFileDrop<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">private</span> <span class="token function-variable function">_onFileDrop</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token literal-property property">e</span><span class="token operator">:</span> DragEvent <span class="token operator">&</span> <span class="token punctuation">{</span> dataTransfer<span class="token operator">?</span><span class="token operator">:</span> any <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> e<span class="token punctuation">.</span><span class="token function">stopPropagation</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> e<span class="token punctuation">.</span><span class="token function">preventDefault</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>_value <span class="token operator">=</span> e<span class="token punctuation">.</span>dataTransfer<span class="token punctuation">.</span>files<span class="token punctuation">;</span><br /><br /> <span class="token comment">// 因為這個元件要給其他專案引入後使用,所以會打一個事件出去給外面接收</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">dispatchEvent</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Event</span><span class="token punctuation">(</span><span class="token string">"drop"</span><span class="token punctuation">,</span> e<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>只有一個事件監聽,而且也做了 <code>stopPropagation</code> 跟 <code>preventDefault</code>,那為何檔案上傳會跟預期的不一樣?<br />
我們在呼叫 <code>_onFileDrop</code> 的時候把 <code>e</code> 印出來看看:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// 這邊是 devtool 底下的 console: 我只有進一次拖曳檔案的動作,但是 _onFileDrop 被呼叫了兩次,而且出現了兩個不同的 event</span><br /><br />event DragEvent <span class="token punctuation">{</span><span class="token literal-property property">isTrusted</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> _constructor<span class="token operator">-</span>name_<span class="token operator">:</span> <span class="token string">'DragEvent'</span><span class="token punctuation">,</span> <span class="token literal-property property">dataTransfer</span><span class="token operator">:</span> DataTransfer<span class="token punctuation">,</span> <span class="token literal-property property">screenX</span><span class="token operator">:</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">,</span> <span class="token literal-property property">screenY</span><span class="token operator">:</span> <span class="token operator">-</span><span class="token number">605</span><span class="token punctuation">,</span> …<span class="token punctuation">}</span><br /><br />event Event <span class="token punctuation">{</span><span class="token literal-property property">isTrusted</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span> _constructor<span class="token operator">-</span>name_<span class="token operator">:</span> <span class="token string">'Event'</span><span class="token punctuation">,</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'drop'</span><span class="token punctuation">,</span> <span class="token literal-property property">target</span><span class="token operator">:</span> wc<span class="token operator">-</span>upload<span class="token operator">-</span>dropzone<span class="token punctuation">,</span> <span class="token literal-property property">currentTarget</span><span class="token operator">:</span> wc<span class="token operator">-</span>upload<span class="token operator">-</span>dropzone<span class="token punctuation">,</span> …<span class="token punctuation">}</span></code></pre>
<p>原來最下面的 <code>this.dispatchEvent(new Event("drop", e));</code> 導致 drop event 又被觸發了一次,也就是說我們打出了一個 new Event,而這個 event 又被自己的 addEventListener 給接收到了,才使這個 function 被重複呼叫了。</p>
<p>所以第一次的 <code>DragEvent</code> 是瀏覽器觸發的,第二次的 <code>Event</code> 是我們自己讓元件觸發的,才導致這次重複上傳的烏龍。<br />
這件事情就是在告訴我,撰寫程式時,事件的監聽,以及事件的觸發,要小心不要讓彼此互相干擾了。</p>
<h4 id="%E2%98%9E-%E6%A1%88%E4%BE%8B%E4%BA%8C%EF%BC%9A-tab-%E5%B0%8E%E8%87%B4%E7%9A%84%E9%A0%81%E9%9D%A2%E8%B7%91%E7%89%88"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/debug-case-01/#%E2%98%9E-%E6%A1%88%E4%BE%8B%E4%BA%8C%EF%BC%9A-tab-%E5%B0%8E%E8%87%B4%E7%9A%84%E9%A0%81%E9%9D%A2%E8%B7%91%E7%89%88">#</a> ☞ 案例二: tab 導致的頁面跑版</h4>
<p>這個案例很有趣,也很感謝辛苦的 PM 詳細的測試抓出了這次的錯誤。<br />
這個頁面本身是一個外部無法滾動,但中間元件可以滾動的頁面(就像 gmail 一樣),</p>
<p>上方的 Navbar 是 <code>fix</code> 的,所以會固定在螢幕上方,中間的表單是 <code>overflow: scroll</code> 的,所以可以滾動。那外層的背景呢,它的高度其實有超過 view height,但是我不想讓它滾動,所以我設置了 <code>overflow: hidden</code> 來防止滾動,依照我們正常的理解,這樣子的設定可以讓整個頁面只有中間的表單是可以滾的,其他都不會動。</p>
<p>當 PM 在測試中間的表單時,按下了鍵盤上的 tab 希望能快速跳至下一題,結果背景的位置竟然滑動了,向上偏移了一段距離後它就卡在 Navbar 底下了,因為外層沒辦法滾動,所以它就卡死在那裡,也沒辦法滾動,反正就是出不來。</p>
<p>(影片當中因為是 gif,所以看似它有脫困,但那是因為 gif 在重播的關係,實際上它真的就是給它卡死了,完全出不來也動不了)</p>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/debug-case-01a.gif" alt="" /></p>
<p>回頭去查了一下 tab 為何可以讓一個不能滾動的元件產生位移,找到了 <a href="https://stackoverflow.com/questions/18520956/tabbing-causes-overflow-content-to-shift-up">這篇討論</a> 提到當我們按下 tab,可能會導致 <code>overflow: hidden</code> 的元件位置跑掉。原因就是該元件本身的高度其實是大過螢幕高度的,只是我們透過 CSS 來把多餘的部分隱藏了,不代表它高度有變小。所以雖然使用者無法上下滾動,但是按下 tab 後經過了瀏覽器的調整,元件還是有機會滑動的,而且一但滑動後就回不來了,所以卡在那邊。</p>
<p>處理方式是我將外部元件的高度經過計算,讓它可以維持在一個小於螢幕高度的狀態,然後移除 <code>overflow: hidden</code> 的設定,也就是讓它不可能再有滑動的機會。</p>
<h4 id="%E2%98%9E-%E6%A1%88%E4%BE%8B%E4%B8%89%EF%BC%9A-font-weight-%E7%84%A1%E6%B3%95%E6%8E%A7%E5%88%B6%E7%9A%84%E7%B2%97%E7%B4%B0%E5%95%8F%E9%A1%8C"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/debug-case-01/#%E2%98%9E-%E6%A1%88%E4%BE%8B%E4%B8%89%EF%BC%9A-font-weight-%E7%84%A1%E6%B3%95%E6%8E%A7%E5%88%B6%E7%9A%84%E7%B2%97%E7%B4%B0%E5%95%8F%E9%A1%8C">#</a> ☞ 案例三: font-weight 無法控制的粗細問題</h4>
<p>這是一次測試時 PM 說我表單的 textarea 文字設定錯了,placeholder 變成粗體,讓我改成跟 input 的 placeholder 相同是細的字體。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/debug-case-01b.png" alt="" /></p>
<p>我看看這個簡單,直接在 textarea 按右鍵檢查看看它 font-weight 的設定,看看是不是真的設定錯誤。</p>
<pre class="language-css"><code class="language-css"><span class="token selector">:host > textarea</span> <span class="token punctuation">{</span><br /> <span class="token property">display</span><span class="token punctuation">:</span> flex<span class="token punctuation">;</span><br /> <span class="token property">padding</span><span class="token punctuation">:</span> 9px 12px<span class="token punctuation">;</span><br /> <span class="token property">border-width</span><span class="token punctuation">:</span> 1px<span class="token punctuation">;</span><br /> <span class="token property">border-style</span><span class="token punctuation">:</span> solid<span class="token punctuation">;</span><br /> <span class="token property">border-color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--textarea-border_color<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token property">border-radius</span><span class="token punctuation">:</span> 4px<span class="token punctuation">;</span><br /> <span class="token property">background-color</span><span class="token punctuation">:</span> white<span class="token punctuation">;</span><br /> <span class="token property">font-size</span><span class="token punctuation">:</span> 14px<span class="token punctuation">;</span><br /> <span class="token property">font-weight</span><span class="token punctuation">:</span> normal<span class="token punctuation">;</span><br /> <span class="token property">font-stretch</span><span class="token punctuation">:</span> normal<span class="token punctuation">;</span><br /> <span class="token property">font-style</span><span class="token punctuation">:</span> normal<span class="token punctuation">;</span><br /> <span class="token property">line-height</span><span class="token punctuation">:</span> 24px<span class="token punctuation">;</span><br /> <span class="token property">letter-spacing</span><span class="token punctuation">:</span> normal<span class="token punctuation">;</span><br /> <span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--textarea-color<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token property">outline</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span><br /> <span class="token property">width</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span><br /> <span class="token property">font-family</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--font-family<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token property">box-sizing</span><span class="token punctuation">:</span> inherit<span class="token punctuation">;</span></code></pre>
<p>看了一下 <code>font-weight: normal;</code> 咦?設定沒有錯誤啊!<br />
還是我把它改成 <code>font-weight: 100;</code> 看看</p>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/debug-case-01c.gif" alt="" /></p>
<p>怎麼改都一樣粗?我眼睛業障重?<br />
所以現在已經是最細的字體囉?那要怎麼讓它更細?變得跟 input 一樣細?<br />
人家 input 的設定也是 <code>font-weight: normal;</code> 啊!所以我的 <code>font-weight</code> 壞掉了嗎?</p>
<blockquote>
<p>愛因斯坦說過:<br />
Insanity: doing the same thing over and over again and expecting different results.<br />
(不負責任翻譯: 做一樣的事卻期待能有不同的結果,是智障!)</p>
</blockquote>
<p>不然,我來試試看改別的地方好了~</p>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/debug-case-01d.gif" alt="" /></p>
<p>啊哈!果真被我找到了,原來是字體的問題導致粗細上面的差異。<br />
前陣子調整過幾個元件的字體,但是 textarea 沒有吃到調整過後的字體,才導致了粗細跟 input 不同。<br />
一開始認為是 <code>font-weight</code> 的問題,才會找不出原因,所以其實 <code>font-family</code> 是會影響字的粗細的,下次檢查粗細時要特別提醒自己一下。</p>
<h4 id="%E2%98%9E-%E6%A1%88%E4%BE%8B%E5%9B%9B%EF%BC%9A-%E8%A1%A8%E5%96%AE-tab-%E7%84%A1%E6%B3%95%E8%B7%B3%E8%87%B3%E4%B8%8B%E4%B8%80%E9%A1%8C"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/debug-case-01/#%E2%98%9E-%E6%A1%88%E4%BE%8B%E5%9B%9B%EF%BC%9A-%E8%A1%A8%E5%96%AE-tab-%E7%84%A1%E6%B3%95%E8%B7%B3%E8%87%B3%E4%B8%8B%E4%B8%80%E9%A1%8C">#</a> ☞ 案例四: 表單 tab 無法跳至下一題</h4>
<p>沒錯,又是 tab 的坑!<br />
我們一般都知道,我們在填表單的時候,可以透過鍵盤的 tab 鍵快速跳至下一個輸入欄位。<br />
可是 PM 今天說更改密碼頁面的 tab 失靈了,沒辦法正常跳至下一題。</p>
<p>issue 描述內容如下:<br />
輸入舊密碼後,點選 Tab,但沒有成功跳掉下一個輸入欄。<br />
再次點選 Tab 才進入下一個輸入欄。(目前都要點兩次才會進入下一格)</p>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/debug-case-01e.png" alt="" /></p>
<p>怪了,這元件在其他頁面也有用,怎麼其他頁面就沒這個問題?<br />
去餵 google 看看有沒有人遇到同樣的問題,不曉得 tab 鍵是不是有什麼神奇的設定,不然我怎麼一直踩 tab 的坑?<br />
結果查不太到類似的情況,但是看過了 <a href="https://developers.google.com/web/fundamentals/accessibility/focus/using-tabindex?hl=zh-tw">這邊</a> 的說明後,我察覺到一件事情,我們一般認為 tab 會移至下一個輸入欄,但是如果輸入欄跟輸入欄之間有 button 存在,按下第一次 tab 就會先 focus 在 button 上面,按了第二次,才會 focus 在第二個輸入欄!</p>
<p>所以因為我的 password 輸入欄,最右邊會有一個 button 可以點選,用來讓使用者切換顯示的密碼。<br />
由於這個 button 存在於 tab 的排序之中的,所以按了 tab 後其實會先 focus 在 button,點第二次才能正確跳至下一題。</p>
<p>知道原因就好辦了,讓這個 button 脫離 tab 的順序就可以了,透過一個 <code>tabindex="-1"</code> 的設定 。(這下可以擺脫 tab 的糾纏了吧 ><)</p>
<h2 id="%E7%B8%BD%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/debug-case-01/#%E7%B8%BD%E7%B5%90">#</a> 總結</h2>
<p>今天分享了四個工作上遇到的 debug 案例,雖說提到的內容很淺,但有時候小小的問題,也有可能像擱淺的船隻一樣卡死在那裡,重點在於提醒自己換一個角度去思考,debug 有時候也可以很好玩,甚至有些時候很敬佩測試者能夠測出很多意料之外的問題,有這些專業的測試在後頭協助把關,也讓開發者可以更放膽去揮灑(笑<br />
歡迎大家針對這次分享的案例提出自己的解決方法,或者分享自己工作上遇到過類似的問題。<br />
之後時不時會整理更多工作的 debug 案例,持續在 ErrorBaker 上面分享。</p>
淺談 Base64 與應用實例分享
2021-10-21T00:00:00Z
https://blog.errorbaker.tw/posts/cian/base64-qrcode/
<!-- summary -->
<!-- 最近拿到了一個顯示 QRCode 的任務,才知道現在的專案是使用 Base64 顯示 QRCode 的圖片。這篇文章分享這個做法背後牽涉到的一部分原理:Base64、Data URIs,以及簡單的實例分享。 -->
<!-- summary -->
<p>成為前端工程師的第二個月,我拿到了一個顯示 QRCode 的任務,在這個任務之前我從來沒有想過這些 QRCode 是怎麼來的,我擅自認為大概就是有一串網址,然後一個 QRCode 轉換的套件,給這個套件網址列,就可以拿到生成的 QRcode 這樣吧。</p>
<p>但是當我看到後端傳回來的東西的時候,我發現代誌不是戇人想的那麼簡單。 在 <code>'qrcode'</code> 的 key 底下,回傳了一串很像亂碼的很長的字串<br />
大概是長這個樣子:</p>
<pre class="language-text"><code class="language-text">iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAAAXNSR0IArs4c6QAADEpJREFUeF7tneF287gKRSfv/9C9K+1Mb5raltgGrDR7/lpIcDgHhNL1ze3j4+PjH/8TARHYROCmQGSGCOwjoEBkhwgcIKBApIcIKBA5IAIMATsIw02rN0FAgbxJog2TIaBAGG5avQkCCuRNEm2YDAEFwnDT6k0QUCBvkmjDZAgoEIabVm+CQFggt9vt5aE5+vOzo/hWsqNJqIiP+tJtR/7sUIE8ZamCQBXCouSqiI/60m2nQCYRryBsBfFIQkcQVPg5OnOV7wRPO4gd5BsBWjhWEcDIDwUyQujf75QIK9lNhvprmR0khpwdxA5iBznQjAJRIAqkSyDkjhdrePOr6VXi6IRXeeL+C1fB+Uz/XJmd99QOokBoWnPtFMg2noSfCiTATTtIHvECsIeW2kEm4coG6n6sAlEgQ/pVEG94KFhQ4acCUSBDKlYQb3goWFDhpwJRIEMqVhBveChYUOGnAlEgQypS4lWQq/u1ZgjOzgLyejKaebr3/Ot538tt2yuWAonLi5KS/pbzFwpONmYK5IlN3UKuIHPFnpR43XhSP+0gDwjQShnvAV8W3dchBZI3K9lB7CDfCNDCQe0qCo4dJKETrJTQimpfsSclnlesBMKuVEm6E1pB5oo9FcgkS/86UN0C6T5vMs2/lv31vDukv2Cno2SmjwLdnaciPipkBaJAKB8/7Sjxujsk9VOBKBAFAv53nD7zXvzM211hqUpoZe6Oj/ppB7GDUG14xYogRxX6KpVkJT8jeXlc65C+jRzBxStW4IpFAB4NuEciWOkHzb9eGL1iJVyxFEi8Mq/UkUn+7CB2kG8EaMeidhVXSNrp7CB2kCEfKdGp3dChnQX0PDvIA6C0klC7o2TTawYlwkrEo7FXzGYKRIFQbXzaUUFSO+osPU+BKBDKOQWyg1zbkH4qc8C44qr0KteoigrbjSdI+adJtp8KhGYi8PpVcZ+u2LNCWEnwTm+jQCahygZqdCwdRikpFcg2Atl5t4OMmD/5XYFMAlW8TIFMApwN1OhYBTJCqOd7dt7tIEl5UyBJQJ7cRoFMApgN1OhYBTJCqOd7dt5TO0gPBOdPoYOxduexv3KHy38ovDL4yNkSfRstiksE+yvXKpBJ9CkRtJsEeNFlCmQyMRLdDjJJlX+cQZ6QokPeO9vNku3qdXaQyQzYQewgk1SxgzwD9c6dgBaOWbJdva6lg1wd5KrndwvrCAfqy6rYXulXeAa50tmVz6akpHYKpIcNCiQJZ0p0aqdAkhI32EaBJOFMiU7tFEhS4hRIE5C32+5BdPglQ+XdiQrR9aC43il2kKScUFJSOztIUuLsIE1A2kF6gG4+JdxB6J9107i6rxm0olfgQmOnWNPYaTejfnZeWRXIU5YoSRTINt27caH52xOrAlEg3whkk2v0YGAHoQg82NFrBk12hR2FgcZOz6Oxe8V6QKCiZR4BTElCk11hRwlLY6fn0dgViAL5RIAOh5SwCmQbOZoHgqcziDOIM8hBBQsLhFZDr2a5rzykGo6G5pX2pNc2GkPaK5YCiROdXglWms1WIWyVyBXIpLLpoFphp0Byi9EkBX4s84rVMIPYQQg1921oMSJeKBAF8o0Avb93EtYrFpH5hk13silJ6ONFd3zOIEnE3B12Dv7ateLobgIpkO0s0jysIsjwFWulikeFVTETUF+oHSUeFfIqhKV4UTsFEphBKMgVdgqkAtXfeyoQBfJyQ3qPNL5OUSAKRIEcKE6BKBAFokB+IuCQnvviVDH4d16jDh8gPoLTnq9Ya6QumLZvpyvIXLHnGignzyDdSaOJqbA7SuhKuFDirdR1KZ4k9tQZhDpeQViaUGqnQAj9mA3lGTlNgTyhpkDi8wm9dhPC3m0UyANylLDddnYQSve4nQJRIF8/UhX8a41xOn5Z0IJDz6soOMQXr1hesaZ4o0CmYOJV7fCtuaBS0oRSu4qKZweJz0OTNJ5eFu4glOjdBHqV81YacKkvtKhQu2l2B24He3sqkADa3QQKuDa9tIKUdE9qNx2sAtn/n9bYQeLXk+4CoEAmpU6f7SoS2n2FpDFMQvtrWQUp6Z7UriJ2r1gPCKwkSAWyTc0KXEjenUEC5YgmrbtS0utld3zduCiQSbIToO5bdxNoMpzQsgpS0j2pXSjgkzeHtg5CSUlngorzaGIqhEVx6e4u9LwKrAknFAjNRMBOgcRf1ALw/lhKf1y9fEgn6h2BlA3G6Dz6XYEokE8EugnbfZ4CoQgoEAVywB07iAJRIAok3F5e4drtkB5Oa9zADmIHuaSD0KfO7spFnzq7hRWX/tiCzomrxP7SHUSB9FXmsRS2VyiQB1woGBR8BaJAnhHIvh3YQZLUWXElqNgzKdzpbWjRXCV2BTKd6uOFFQmt2DMp3OltFIhXrOEDhUN6/Cq4SnGwg0zXQjsIgertOkh3wCQpd5vsYe2+J61qtIPQ2LvtKC7dOSLnhTuIAsmlH0largfnd1MgCXMGBZGmr4J4FTFU+Ekxo3YUl4rYaQHfi90OEmAFJYJXrPiQHkjLj6UKZBK57uo06davZRV+Ul+oHS0cFbErkMksdoM/6ZYCeUCgO0fkPK9YAWbTSukV642uWAE+pSz966Sk8ZFqWPVUnZLohk0IZuEO0hDH9NBFfSFA0bNGdgpkhFDed5J3BZKHP9pJgSDYkJECmYSNADW5dXiZAglDhg1I3u0gGO4cQwWSg+PMLgpkBqWiv9OaPPrXMgVCkYvbKZBJzAhQk1uHlymQMGTYgOR9+SsWRgMadhP2yE3qCwy9/S+g6T9eXWG3h5kCeUKGkpJUpxGRqS+jffe+d8dQQfTL/9SEgv8qdpSU3eSqwLM7BgVSkcXiPRVILsC0onfbecWazLsCmQRqclk30el5CiQhoUdbdF9PJsMJLeuOwStWKD1rLLaD5OaBVvRuu7QOQgmUC/u53Soq5TmPtq0p1itV5gqiU6xJ3sPPvDRpNKgKOwJUhR+jPSnWCmQbWZJ3BTJi6YXfFUgu+ApkEk8C1OTWqcsUSCqc6C8F7CC5OUjdTYGkwqlAZuG0g8Tv6BXDNp2VZvP8vI7k3Q5C0W6ws4Pkgny5QIgDuRD8fzda8Y78oXtSO4pNhbAqfKngSzbWqR2kIuCVEkPBp3YVsR/tWZG/lWIn8SmQAAtpsqldwLUfS+0g8RlrD2sFEmAhJTq1C7imQP5FIBtrBRJgIQWf2gVcUyAKJEaXClLSPaldLOK5BwpnkBiqdpAAXpTo1C7gmh3k1TsIHRxpxVuJlOT1hIpjZFeRh9GZ5Dv9ETEb67YOUpGYVUC8E6BbkIR0Iz/pnhV2q+RWgSRlV4EkAfnvNgokAc9VQBxV5uy2fwa6ik5+xp8921VyawdJyq4dJAlIO0gekKtUGTtIXk7/22mV3NpBknJrB0kC0g6SB2RFlaF39Io5g4qOxkAzQ/NAz6N2JEd2kCe0KbkI+KNEK5ARQrHvJEcKRIHEWLax2g7yAMpKVY0mhtodMYlUpxEzV8Kaxk478ggb8p3kyA5iByFc+2FTUXBOOxXsdHvnKRAFcpqLCsQr1pBEpH2PNvWKNUIo9p3kyA4S6CC0UpLE3N366wKheNJ5KCanr9UKRIEQ3qTMIApk8mpGM0QBXsnuKHY7SJwZtFs7pD8goEDixKPXmm6RK5BFiU6JYAfZRoD+fqJAFMgnArQL5vaOr92oL9SOdjMSu0O6QzrhjUP67tByu+0Cmt3ezmSOXnkqWjvdk8ZP80D9pJ2gwo5idvmQnu34aD8FMkLo93cFsoHJR7DcUOLF03XOgvrZTZJzUW5bB1P6vUl37HaQiuxP7qlAJoF6WKZA7CCnXl3o6wklXpzi41eloz2pnxWdgBY4ipkzyAMCNKEKJH6lo0SndgpkEgEKcHcVnQwntMwZJATX4eLU30Hy3KrdiXYQakejoUSn53UXFeontSN4KpAntFciCUkoJc/dbqXYz8SxZ0vwVCAK5BsBBVL8ilWh+oo96VWJ2tEYSMWjZ9lBtpGzg9hB7CAHVUWBKBAFokB+IkCvStSOXnu8YlHktu0InuEOkuuyu4nA2ggokLXzo3cXI6BALk6Ax6+NgAJZOz96dzECCuTiBHj82ggokLXzo3cXI6BALk6Ax6+NgAJZOz96dzECCuTiBHj82ggokLXzo3cXI6BALk6Ax6+NgAJZOz96dzECCuTiBHj82gj8DyPtU5nKjNV6AAAAAElFTkSuQmCC</code></pre>
<p>這個東西其實是 QRCode 圖片在 Base64 encode 之後產生的字串。透過這種方式顯示 QRcode,可以達成圖片優化的效果,這篇文章就想來看一下這個做法和背後牽涉到的原理。<br />
這篇文章想來看看三個東西:Base64 是什麼、Data URIs 是什麼以及 HTML 應用。</p>
<h2 id="base64"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/base64-qrcode/#base64">#</a> Base64</h2>
<p>Base64 是一種基於 64 個可列印字元來表示二進位資料的表示方法。這 64 個可列印字元包括 A-Z、a-z、數字 0-9,這樣共有 62 個字元,另外會再加上兩個可列印符號,這兩個符號在不同的系統會不同,不過最常見的是 <code>/</code> 和 <code>+</code>。<br />
我們詳細看一下 Base64 的轉換過程</p>
<h3 id="encode-%E6%96%B9%E5%BC%8F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/base64-qrcode/#encode-%E6%96%B9%E5%BC%8F">#</a> encode 方式</h3>
<p>首先,我們想要使用的內容是 64 個可列印字元,這些字元在 ASCII 的轉換表上是長這樣的:</p>
<p><img src="https://blog.errorbaker.tw/img/posts/cian/base64-qrcode/ascii-table-0-63.png" alt="" /></p>
<p>接著,基於 2 的 6 次方等於 64,我們可以以每 6 個 bit 為單位,創造一個限制在 64 內的小組,這些小組轉回來的樣子,就會是在我們想要的 64 個可列印字元之內。</p>
<p>維基百科提供了這樣的一個例子:<br />
我們將「Man」進行 Base64 的編碼,首先,M A N 分別會對應到 ASCII 的 77、97 和 110。<br />
將 77、97 和 110 分別用一個 byte ( 8 bit )表示,接著將這一共 24 個 bit 再進行 6 個一組,得到一組新的數字19、22、5 和 46,這一組數字就是 base64 的索引,用這些索引在 ASCII 中找到對應的文字 T、W、F、u 就是 Base64 編碼的結果。<br />
<img src="https://blog.errorbaker.tw/img/posts/cian/base64-qrcode/base64-encode-progress.png" alt="" /></p>
<p>另外,如果要編碼的 byte 數不能被 3 整除(結果無法為 4 個一組),那麼就會在最後補上一個或兩個 byte 的 0,並在生成的 Base64 字串最後補上一或兩個 <code>=</code> 表示。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/cian/base64-qrcode/add.png" alt="" /></p>
<h3 id="%E7%82%BA%E4%BB%80%E9%BA%BC%E9%9C%80%E8%A6%81%E4%BD%BF%E7%94%A8-base64"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/base64-qrcode/#%E7%82%BA%E4%BB%80%E9%BA%BC%E9%9C%80%E8%A6%81%E4%BD%BF%E7%94%A8-base64">#</a> 為什麼需要使用 base64</h3>
<p>雖然我沒有查到準確的原因,但 base64 很常被使用的一個場景,是在以 SMTP(Simple Mail Transfer Protocol) 為基礎的電子郵件通訊上。因為原本的電子郵件規範是基於文字創造的,所以這個規範在訂定時規定使用 7 個 bit,這使得它在處理二進制檔案傳輸時遇到一些困難。為了解決這個困難,當時會把附加檔案和圖片等等以 base64 轉換成可列印的字串,這樣就可以很容易的使用 SMTP 進行傳輸。<br />
現在大多數 SMTP 伺服器已經都都援 8 位元 MIME 擴充,就沒有必要使用這個方式傳輸。另外,在 2018 年也有基於這種傳輸方式的<a href="https://exim.org/static/doc/security/CVE-2018-6789.txt">漏洞</a>被發現,這個漏洞讓攻擊者可以遠端執行程式,不過提供服務的 Exim 表示這個漏洞很難被利用,並且已經被修正。<br />
有些像是垃圾郵件依然會使用 Base64 encode 信件內容,因為經過轉換的內容比較容易通過垃圾郵件的審查機制。</p>
<h2 id="data-uris"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/base64-qrcode/#data-uris">#</a> Data URIs</h2>
<h3 id="data-uris-%E7%B0%A1%E4%BB%8B"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/base64-qrcode/#data-uris-%E7%B0%A1%E4%BB%8B">#</a> Data URIs 簡介</h3>
<p>接著要介紹的是 Data URIs。<br />
data URIs 是在 August 1998 新增的 RFC標準,<a href="https://datatracker.ietf.org/doc/html/rfc2397">rfc2397</a>。</p>
<blockquote>
<p>A new URL scheme, "data", is defined. It allows inclusion of small<br />
data items as "immediate" data, as if it had been included<br />
externally.</p>
</blockquote>
<p>在這個標準中,定義了一個新的 URL 格式。這個新的格式允許直接傳輸小型 data 的內容。</p>
<p>一般來說, URI(Uniform Resource Identifier) 通常是代表一個檔案位置的標示,我們需要在取得 URI 之後另外取得內容,然而,在這個新的 Scheme 出現之後,再取得 URI 的同時,我們就已經取得經過 base64 編碼的檔案內容了。</p>
<h3 id="%E8%AA%9E%E6%B3%95%E5%92%8C%E7%AF%84%E4%BE%8B"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/base64-qrcode/#%E8%AA%9E%E6%B3%95%E5%92%8C%E7%AF%84%E4%BE%8B">#</a> 語法和範例</h3>
<p>這個結構的語法長這樣<br />
<code>data:[<mediatype>][;base64],<data></code></p>
<p>在 <code><mediatype></code> 中會填入媒體類型,接著是 <code>;base64</code> 表明他的 encode 方式,最後則是 encode 後的結果,這會是前面說到的由可以印出來的 64 個字元組成的內容。</p>
<p>在標準中提供的範例長這樣:</p>
<pre class="language-text"><code class="language-text">data:image/gif;base64,R0lGODdhMAAwAPAAAAAAAP///ywAAAAAMAAw<br /> AAAC8IyPqcvt3wCcDkiLc7C0qwyGHhSWpjQu5yqmCYsapyuvUUlvONmOZtfzgFz<br /> ByTB10QgxOR0TqBQejhRNzOfkVJ+5YiUqrXF5Y5lKh/DeuNcP5yLWGsEbtLiOSp<br /> a/TPg7JpJHxyendzWTBfX0cxOnKPjgBzi4diinWGdkF8kjdfnycQZXZeYGejmJl<br /> ZeGl9i2icVqaNVailT6F5iJ90m6mvuTS4OK05M0vDk0Q4XUtwvKOzrcd3iq9uis<br /> F81M1OIcR7lEewwcLp7tuNNkM3uNna3F2JQFo97Vriy/Xl4/f1cf5VWzXyym7PH<br /> hhx4dbgYKAAA7</code></pre>
<p><code><mediatype></code> 是 <code>image/gif</code>,接著是編碼模式和結果。<br />
直接把這串 URI 放到網址列,就可以看到這張圖片的內容。這時看 response 會發現並沒有拿到 response body,但圖片卻正確顯示,這是因為這張圖片已經被帶在 URI 中了。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/cian/base64-qrcode/no-response.png" alt="" /></p>
<p>另外,除了圖片之外 Data URIs 也可以用來傳輸其他檔案類型像是 HTML 或者 JavaScript。其相應的編碼格式也不一定都是 base64,不過最常見的應用還是結合 Data URIs 和 Base64 用來進行圖片效能優化。</p>
<h3 id="%E5%92%8C-html-%E7%B5%90%E5%90%88"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/base64-qrcode/#%E5%92%8C-html-%E7%B5%90%E5%90%88">#</a> 和 HTML 結合</h3>
<p>Data URI 其實可以用在任何需要指定 URL 的地方,包括 <code><script></code> 標籤和 <code><a></code> 標籤,但它最常被用來進行圖片的效能優化,所以經常被放在 HTML 的 <code><img></code>、<code><iframe></code> 等等標籤中。直接在 <code>src</code> 中放上內容,就可以正確顯示圖片。另外在 CSS 的 <code>background: url()</code> 中也可以填入這種格式的 URI 來顯示圖片。</p>
<p>不過使用 Data URI 也是有一些缺點:</p>
<ul>
<li>由於 base64 的特性,在 encode 之後整體檔案會變大,所以需要傳輸量也會變大</li>
<li>可讀性很差</li>
<li>因為瀏覽器不會認知 Data URI 是一張圖片,所以不會進行快取。</li>
<li>如果選擇放在 CSS 中,會增加 CSS tree 解析的時間</li>
</ul>
<p>在考慮使用時可以作為參考。</p>
<h2 id="%E7%94%A8-base64-%E9%A1%AF%E7%A4%BA-qrcode-%E5%AF%A6%E4%BE%8B"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cian/base64-qrcode/#%E7%94%A8-base64-%E9%A1%AF%E7%A4%BA-qrcode-%E5%AF%A6%E4%BE%8B">#</a> 用 Base64 顯示 QRCode 實例</h2>
<p>知道了編碼方式和 DATA URI 格式,就可以理解這次使用 base64 顯示QRcode 的過程都發生了些什麼事。</p>
<p>首先,根據需要的網址轉換出 QRCode 之後,我們直接在後端對它進行 Base64 encode。<br />
在前端發 request 時,直接回傳這串 encode 後的字串。<br />
前端在拿到這一串字串之後,加上 <code>data:<mediatype></code> 和 <code>;base64</code> 的辨識符,放進 <code><img></code> 中,就可以顯示這張圖片。<br />
這樣就完成整個過程了。</p>
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAAAXNSR0IArs4c6QAADEpJREFUeF7tneF287gKRSfv/9C9K+1Mb5raltgGrDR7/lpIcDgHhNL1ze3j4+PjH/8TARHYROCmQGSGCOwjoEBkhwgcIKBApIcIKBA5IAIMATsIw02rN0FAgbxJog2TIaBAGG5avQkCCuRNEm2YDAEFwnDT6k0QUCBvkmjDZAgoEIabVm+CQFggt9vt5aE5+vOzo/hWsqNJqIiP+tJtR/7sUIE8ZamCQBXCouSqiI/60m2nQCYRryBsBfFIQkcQVPg5OnOV7wRPO4gd5BsBWjhWEcDIDwUyQujf75QIK9lNhvprmR0khpwdxA5iBznQjAJRIAqkSyDkjhdrePOr6VXi6IRXeeL+C1fB+Uz/XJmd99QOokBoWnPtFMg2noSfCiTATTtIHvECsIeW2kEm4coG6n6sAlEgQ/pVEG94KFhQ4acCUSBDKlYQb3goWFDhpwJRIEMqVhBveChYUOGnAlEgQypS4lWQq/u1ZgjOzgLyejKaebr3/Ot538tt2yuWAonLi5KS/pbzFwpONmYK5IlN3UKuIHPFnpR43XhSP+0gDwjQShnvAV8W3dchBZI3K9lB7CDfCNDCQe0qCo4dJKETrJTQimpfsSclnlesBMKuVEm6E1pB5oo9FcgkS/86UN0C6T5vMs2/lv31vDukv2Cno2SmjwLdnaciPipkBaJAKB8/7Sjxujsk9VOBKBAFAv53nD7zXvzM211hqUpoZe6Oj/ppB7GDUG14xYogRxX6KpVkJT8jeXlc65C+jRzBxStW4IpFAB4NuEciWOkHzb9eGL1iJVyxFEi8Mq/UkUn+7CB2kG8EaMeidhVXSNrp7CB2kCEfKdGp3dChnQX0PDvIA6C0klC7o2TTawYlwkrEo7FXzGYKRIFQbXzaUUFSO+osPU+BKBDKOQWyg1zbkH4qc8C44qr0KteoigrbjSdI+adJtp8KhGYi8PpVcZ+u2LNCWEnwTm+jQCahygZqdCwdRikpFcg2Atl5t4OMmD/5XYFMAlW8TIFMApwN1OhYBTJCqOd7dt7tIEl5UyBJQJ7cRoFMApgN1OhYBTJCqOd7dt5TO0gPBOdPoYOxduexv3KHy38ovDL4yNkSfRstiksE+yvXKpBJ9CkRtJsEeNFlCmQyMRLdDjJJlX+cQZ6QokPeO9vNku3qdXaQyQzYQewgk1SxgzwD9c6dgBaOWbJdva6lg1wd5KrndwvrCAfqy6rYXulXeAa50tmVz6akpHYKpIcNCiQJZ0p0aqdAkhI32EaBJOFMiU7tFEhS4hRIE5C32+5BdPglQ+XdiQrR9aC43il2kKScUFJSOztIUuLsIE1A2kF6gG4+JdxB6J9107i6rxm0olfgQmOnWNPYaTejfnZeWRXIU5YoSRTINt27caH52xOrAlEg3whkk2v0YGAHoQg82NFrBk12hR2FgcZOz6Oxe8V6QKCiZR4BTElCk11hRwlLY6fn0dgViAL5RIAOh5SwCmQbOZoHgqcziDOIM8hBBQsLhFZDr2a5rzykGo6G5pX2pNc2GkPaK5YCiROdXglWms1WIWyVyBXIpLLpoFphp0Byi9EkBX4s84rVMIPYQQg1921oMSJeKBAF8o0Avb93EtYrFpH5hk13silJ6ONFd3zOIEnE3B12Dv7ateLobgIpkO0s0jysIsjwFWulikeFVTETUF+oHSUeFfIqhKV4UTsFEphBKMgVdgqkAtXfeyoQBfJyQ3qPNL5OUSAKRIEcKE6BKBAFokB+IuCQnvviVDH4d16jDh8gPoLTnq9Ya6QumLZvpyvIXLHnGignzyDdSaOJqbA7SuhKuFDirdR1KZ4k9tQZhDpeQViaUGqnQAj9mA3lGTlNgTyhpkDi8wm9dhPC3m0UyANylLDddnYQSve4nQJRIF8/UhX8a41xOn5Z0IJDz6soOMQXr1hesaZ4o0CmYOJV7fCtuaBS0oRSu4qKZweJz0OTNJ5eFu4glOjdBHqV81YacKkvtKhQu2l2B24He3sqkADa3QQKuDa9tIKUdE9qNx2sAtn/n9bYQeLXk+4CoEAmpU6f7SoS2n2FpDFMQvtrWQUp6Z7UriJ2r1gPCKwkSAWyTc0KXEjenUEC5YgmrbtS0utld3zduCiQSbIToO5bdxNoMpzQsgpS0j2pXSjgkzeHtg5CSUlngorzaGIqhEVx6e4u9LwKrAknFAjNRMBOgcRf1ALw/lhKf1y9fEgn6h2BlA3G6Dz6XYEokE8EugnbfZ4CoQgoEAVywB07iAJRIAok3F5e4drtkB5Oa9zADmIHuaSD0KfO7spFnzq7hRWX/tiCzomrxP7SHUSB9FXmsRS2VyiQB1woGBR8BaJAnhHIvh3YQZLUWXElqNgzKdzpbWjRXCV2BTKd6uOFFQmt2DMp3OltFIhXrOEDhUN6/Cq4SnGwg0zXQjsIgertOkh3wCQpd5vsYe2+J61qtIPQ2LvtKC7dOSLnhTuIAsmlH0largfnd1MgCXMGBZGmr4J4FTFU+Ekxo3YUl4rYaQHfi90OEmAFJYJXrPiQHkjLj6UKZBK57uo06davZRV+Ul+oHS0cFbErkMksdoM/6ZYCeUCgO0fkPK9YAWbTSukV642uWAE+pSz966Sk8ZFqWPVUnZLohk0IZuEO0hDH9NBFfSFA0bNGdgpkhFDed5J3BZKHP9pJgSDYkJECmYSNADW5dXiZAglDhg1I3u0gGO4cQwWSg+PMLgpkBqWiv9OaPPrXMgVCkYvbKZBJzAhQk1uHlymQMGTYgOR9+SsWRgMadhP2yE3qCwy9/S+g6T9eXWG3h5kCeUKGkpJUpxGRqS+jffe+d8dQQfTL/9SEgv8qdpSU3eSqwLM7BgVSkcXiPRVILsC0onfbecWazLsCmQRqclk30el5CiQhoUdbdF9PJsMJLeuOwStWKD1rLLaD5OaBVvRuu7QOQgmUC/u53Soq5TmPtq0p1itV5gqiU6xJ3sPPvDRpNKgKOwJUhR+jPSnWCmQbWZJ3BTJi6YXfFUgu+ApkEk8C1OTWqcsUSCqc6C8F7CC5OUjdTYGkwqlAZuG0g8Tv6BXDNp2VZvP8vI7k3Q5C0W6ws4Pkgny5QIgDuRD8fzda8Y78oXtSO4pNhbAqfKngSzbWqR2kIuCVEkPBp3YVsR/tWZG/lWIn8SmQAAtpsqldwLUfS+0g8RlrD2sFEmAhJTq1C7imQP5FIBtrBRJgIQWf2gVcUyAKJEaXClLSPaldLOK5BwpnkBiqdpAAXpTo1C7gmh3k1TsIHRxpxVuJlOT1hIpjZFeRh9GZ5Dv9ETEb67YOUpGYVUC8E6BbkIR0Iz/pnhV2q+RWgSRlV4EkAfnvNgokAc9VQBxV5uy2fwa6ik5+xp8921VyawdJyq4dJAlIO0gekKtUGTtIXk7/22mV3NpBknJrB0kC0g6SB2RFlaF39Io5g4qOxkAzQ/NAz6N2JEd2kCe0KbkI+KNEK5ARQrHvJEcKRIHEWLax2g7yAMpKVY0mhtodMYlUpxEzV8Kaxk478ggb8p3kyA5iByFc+2FTUXBOOxXsdHvnKRAFcpqLCsQr1pBEpH2PNvWKNUIo9p3kyA4S6CC0UpLE3N366wKheNJ5KCanr9UKRIEQ3qTMIApk8mpGM0QBXsnuKHY7SJwZtFs7pD8goEDixKPXmm6RK5BFiU6JYAfZRoD+fqJAFMgnArQL5vaOr92oL9SOdjMSu0O6QzrhjUP67tByu+0Cmt3ezmSOXnkqWjvdk8ZP80D9pJ2gwo5idvmQnu34aD8FMkLo93cFsoHJR7DcUOLF03XOgvrZTZJzUW5bB1P6vUl37HaQiuxP7qlAJoF6WKZA7CCnXl3o6wklXpzi41eloz2pnxWdgBY4ipkzyAMCNKEKJH6lo0SndgpkEgEKcHcVnQwntMwZJATX4eLU30Hy3KrdiXYQakejoUSn53UXFeontSN4KpAntFciCUkoJc/dbqXYz8SxZ0vwVCAK5BsBBVL8ilWh+oo96VWJ2tEYSMWjZ9lBtpGzg9hB7CAHVUWBKBAFokB+IkCvStSOXnu8YlHktu0InuEOkuuyu4nA2ggokLXzo3cXI6BALk6Ax6+NgAJZOz96dzECCuTiBHj82ggokLXzo3cXI6BALk6Ax6+NgAJZOz96dzECCuTiBHj82ggokLXzo3cXI6BALk6Ax6+NgAJZOz96dzECCuTiBHj82gj8DyPtU5nKjNV6AAAAAElFTkSuQmCC" />
<p>順帶一提,我一開始在最上面提供的超長字串,是我的部落格的 QRCode encode 後的字串,有興趣的朋友可以自己加上 DATA URI 的語法試試能不能正常顯示。<br />
我使用的是<br />
<a href="https://www.the-qrcode-generator.com/">QR Code Generator</a><br />
<a href="https://www.base64-image.de/">Base64 Image Encoder</a><br />
這兩個簡單的轉換網站,有興趣的話也可以做做看自己的圖片來玩。</p>
<p>這篇文章就到這邊結束,我們下篇文章見拉!</p>
<hr />
<p>參考資料:<br />
<a href="https://zh.wikipedia.org/wiki/%E7%AE%80%E5%8D%95%E9%82%AE%E4%BB%B6%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE">簡單郵件傳輸協定 - 維基百科,自由的百科全書</a><br />
<a href="https://en.wikipedia.org/wiki/Base64">Base64 - Wikipedia(英文)</a><br />
<a href="https://ja.wikipedia.org/wiki/Base64">Base64 - Wikipedia(日文)</a><br />
<a href="https://zh.wikipedia.org/wiki/Base64">Base64 - 維基百科,自由的百科全書</a><br />
<a href="https://exim.org/static/doc/security/CVE-2018-6789.txt">CVE-2018-6789</a><br />
<a href="https://developer.mozilla.org/en-US/docs/Glossary/Base64">Base64 - MDN Web Docs Glossary: Definitions of Web-related terms | MDN</a><br />
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types">MIME types (IANA media types) - HTTP | MDN</a></p>
<p>全文同步刊載於個人部落格 <a href="https://keronscribe.tw/base64-qrcode">淺談 Base64 與應用實例分享</a></p>
初探 swagger-ui
2021-10-24T00:00:00Z
https://blog.errorbaker.tw/posts/cwc329/swagger-ui/
<!-- summary -->
<p>swagger-ui 是我最近接觸到的一項工具,使用者可以依照 OpenAPI 規範,將自己的 API 撰寫成 YAML 或者 JSON 檔案,再藉由 swagger-ui 轉換成一個 API 文件網頁。這個網頁可以部署在主機,或者放在專案中,需要的時候在 local 架起來檢視。</p>
<!-- summary -->
<h1 id="swagger-ui"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/swagger-ui/#swagger-ui">#</a> Swagger-ui</h1>
<p>swagger-ui 我們團隊最近引入的一項工具,想要把原本四散各處而且不完整的 API 文件整理,並且用一個對於工程師而言比較容易取得的方式管理。</p>
<p>剛接觸的時候因為不太熟悉 YAML 撰寫方式以及 <a href="https://github.com/OAI/OpenAPI-Specification">OpenAPI</a> 規範稍微麻煩一些,<br />
不過上手難度不高,搭配 <a href="https://www.npmjs.com/package/swagger-ui-watcher">swagger-ui-watcher</a> 或者其他類似套件可以做到更好的分層管理,讓 API doc 的撰寫可以更模組化更快速。</p>
<h2 id="swagger-ui-online-demo"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/swagger-ui/#swagger-ui-online-demo">#</a> Swagger-ui Online Demo</h2>
<p>Swagger 官方有網頁的線上 ui demo 與 editor demo,我們可以先從這兩個網站一窺 swagger 到底是什麼樣子,以及要怎麼使用。</p>
<p>先看 <a href="https://petstore.swagger.io/?_ga=2.19666047.679151482.1635056385-1464799535.1632669410">ui</a> 的頁面<br />
<img src="https://blog.errorbaker.tw/img/posts/cwc329/swagger-ui-demo/1.png" alt="" /><br />
<img src="https://blog.errorbaker.tw/img/posts/cwc329/swagger-ui-demo/2.png" alt="" /><br />
<img src="https://blog.errorbaker.tw/img/posts/cwc329/swagger-ui-demo/3.png" alt="" /></p>
<p>頁面很清楚地列出 API 的 method 以及 path,點開之後可以看到更多相關資訊,像是接受什麼 parameter,以及會有什麼 response。</p>
<p>而且 <code>try it out</code> 的 button 點下去可以實際操作 API,讓使用者可以直接使用 API,不需要再切換到 postman 等工具,達成在同時看到 API 的文件與測試 API。</p>
<p>而 swagger-ui 產生這個頁面的方式是藉由預先撰寫好的 YAML 或者 JSON 檔案,讀取檔案內容之後將其渲染。這點可以從 <a href="https://editor.swagger.io/">editor demo</a> 看到。<br />
<img src="https://blog.errorbaker.tw/img/posts/cwc329/swagger-ui-demo/4.png" alt="" /><br />
畫面左方就是 YAML 檔案,而右方則是依照左方的 YAML 檔案渲染出來的 ui 頁面。</p>
<p>這份 YAML 檔案首先要標明所使用的是哪一個版本的規範,先前提過的 OpenAPI 規範,其前身就是 swagger specification,是在 swagger 底下的,後來才獨立出來變成 OpenAPI 規範。YAML 與 JSON 文件只要依照這個規範撰寫,就可以在 swagger 底下自動產生出一份包含 methods, parameters 與 models 的 API 文件。</p>
<p>實際上的撰寫在下一部份會比較詳細的說明。</p>
<h2 id="swagger-ui-watcher"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/swagger-ui/#swagger-ui-watcher">#</a> Swagger-ui-watcher</h2>
<blockquote>
<p>以下的示範程式碼都可以在<a href="https://github.com/cwc329/swagger-ui-demo">這裡</a>找到。<br />
這是一份可以在 local 跑起來的小小 demo,有興趣可以 clone 下來玩玩看。</p>
</blockquote>
<p>剛剛的 editor 頁面其實就可以示範怎麼撰寫 YAML 檔案,同時也能及時顯示出這份檔案在哪邊不符合規範。不過很麻煩的是那個頁面只支援單一 YAML 檔案渲染,也就是需要把整個 API 文件都寫在同一個檔案中才能在這邊渲染,這在實際撰寫的時候非常麻煩。</p>
<p>這邊我要介紹一個我們公司專案使用的工具,swagger-ui-watcher。這是一個 npm 上的套件,開發上主要的功能是可以在本地端快速的架設一個 swagger-ui 的 server,並且監聽特定的檔案,當檔案有變動時可以即時的重新渲染畫面,而且當檔案格式有錯時也會有錯誤訊息。最重要的是,這個套件可以把多個檔案自動彙整為一個完整的檔案,所以在寫文件的時候可以使用模組化的方式,能夠讓文件的原始碼更容易撰寫與維護。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/cwc329/swagger-ui-demo/5.png" alt="" /></p>
<p>像是範例程式碼中,我簡單地區分資料夾,讓不同的 endpoint 可以分開,同時也有一個 schema 的資料夾去存放各種常用的資料結構。資料夾可以依照每個專案有不同的設計,這點非常有彈性。</p>
<p>這邊可以先打開 <a href="https://github.com/cwc329/swagger-ui-demo/blob/main/docs/index.yaml">index.yaml</a> 看看。</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">openapi</span><span class="token punctuation">:</span> 3.0.3<br /><br /><span class="token key atrule">info</span><span class="token punctuation">:</span><br /> <span class="token key atrule">title</span><span class="token punctuation">:</span> PIMQ workshop API<br /> <span class="token key atrule">description</span><span class="token punctuation">:</span> PIMQ workshop api document<br /> <span class="token key atrule">version</span><span class="token punctuation">:</span> 4.0.0<br /><br /><span class="token key atrule">servers</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">url</span><span class="token punctuation">:</span> http<span class="token punctuation">:</span>//localhost<span class="token punctuation">:</span><span class="token number">5566</span><br /> <span class="token key atrule">description</span><span class="token punctuation">:</span> for local demo<br /><br /><span class="token key atrule">tags</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> <span class="token string">"posts"</span><br /> <span class="token key atrule">description</span><span class="token punctuation">:</span> <span class="token string">"Posts of the site"</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> <span class="token string">"comments"</span><br /> <span class="token key atrule">description</span><span class="token punctuation">:</span> <span class="token string">"Comments of posts"</span></code></pre>
<p>可以看到我一開始定義我要使用的是規範版本是 3.0.3。info 是這個 API 的一些資訊。servers 是這個 API 有哪些 server,這邊寫出來的 server 都可以在 ui 的頁面選擇並且從頁面發送 request。tags 可以把我的 API 分類,在頁面上可以看到我的 API 依照不同的 tag 被放在不同的 block。<br />
<img src="https://blog.errorbaker.tw/img/posts/cwc329/swagger-ui-demo/6.png" alt="" /></p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">paths</span><span class="token punctuation">:</span><br /> <span class="token key atrule">/posts</span><span class="token punctuation">:</span><br /> <span class="token key atrule">get</span><span class="token punctuation">:</span><br /> <span class="token key atrule">tags</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> posts<br /> <span class="token key atrule">summary</span><span class="token punctuation">:</span> get a list of posts<br /> <span class="token key atrule">responses</span><span class="token punctuation">:</span><br /> <span class="token key atrule">"200"</span><span class="token punctuation">:</span><br /> <span class="token key atrule">description</span><span class="token punctuation">:</span> ok<br /> <span class="token key atrule">content</span><span class="token punctuation">:</span><br /> <span class="token key atrule">application/json</span><span class="token punctuation">:</span><br /> <span class="token key atrule">schema</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> array<br /> <span class="token key atrule">items</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> object<br /> <span class="token key atrule">properties</span><span class="token punctuation">:</span><br /> <span class="token key atrule">id</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> integer<br /> <span class="token key atrule">title</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> string<br /> <span class="token key atrule">cotent</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> string<br /> <span class="token key atrule">authorId</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> integer<br /> <span class="token key atrule">post</span><span class="token punctuation">:</span><br /> <span class="token key atrule">tags</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> posts<br /> <span class="token key atrule">summary</span><span class="token punctuation">:</span> add a new post<br /> <span class="token key atrule">requestBody</span><span class="token punctuation">:</span><br /> <span class="token key atrule">content</span><span class="token punctuation">:</span><br /> <span class="token key atrule">application/json</span><span class="token punctuation">:</span><br /> <span class="token key atrule">schema</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> object<br /> <span class="token key atrule">properties</span><span class="token punctuation">:</span><br /> <span class="token key atrule">title</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> string<br /> <span class="token key atrule">content</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> string<br /> <span class="token key atrule">authorId</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> integer<br /> <span class="token key atrule">responses</span><span class="token punctuation">:</span><br /> <span class="token key atrule">"200"</span><span class="token punctuation">:</span><br /> <span class="token key atrule">description</span><span class="token punctuation">:</span> ok<br /> <span class="token key atrule">content</span><span class="token punctuation">:</span><br /> <span class="token key atrule">application/json</span><span class="token punctuation">:</span><br /> <span class="token key atrule">schema</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> object<br /> <span class="token key atrule">properties</span><span class="token punctuation">:</span><br /> <span class="token key atrule">id</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> integer<br /> <span class="token key atrule">content</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> string<br /> <span class="token key atrule">title</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> string<br /> <span class="token key atrule">authorId</span><span class="token punctuation">:</span><br /> <span class="token key atrule">type</span><span class="token punctuation">:</span> integer</code></pre>
<p>paths 就是 API 的路徑,依照 OpenAPI 的規範,paths 這個 object 下面的 field 是有<br />
格式規範的,像是必須要以 <code>/</code> 開頭才行。路徑下面可以列出這個路徑所接受的 http request method。method 下面則包含這個 method 屬於哪個 tag,這就是在前面所寫好的 tags,同時也可以用 summary 以及 description 來簡述與詳述這支 API 的功能。parameter 與 requestBody 則是描述這個 API 可以接受怎樣的參數以及怎樣的 request body,包括參數的名稱、資料型態、規範等。responses 則是這個 request 會有什麼樣的 response,這些 response 會有什麼樣的 http status code,這些回應有什麼意義,又會回傳什麼資料。</p>
<p>雖然寫了滿長一段的,不過如果熟悉 YAML 檔案應該很容易就可以看出其結構,也能發現這個結構其實很清楚。</p>
<p>不過如這章節一開始所說,如果把所有的東西都寫在一起會非常難管理。<br />
於是接下來的幾個 path 就使用了 <code>$ref</code> 這個關鍵字,讓我們可以用 reference 的方式引入其他 YAML 檔案,方便我們管理與撰寫原始碼。</p>
<pre class="language-yaml"><code class="language-yaml">/posts/<span class="token punctuation">{</span>postId<span class="token punctuation">}</span><span class="token punctuation">:</span><br /> <span class="token key atrule">$ref</span><span class="token punctuation">:</span> posts/index.yaml<span class="token comment">#/post-collections</span><br /><br /><span class="token key atrule">/comments</span><span class="token punctuation">:</span><br /> <span class="token key atrule">$ref</span><span class="token punctuation">:</span> comments/index.yaml<span class="token comment">#/comments-collections</span><br />/comments/<span class="token punctuation">{</span>commentId<span class="token punctuation">}</span><span class="token punctuation">:</span><br /> <span class="token key atrule">$ref</span><span class="token punctuation">:</span> comments/index.yaml<span class="token comment">#/comment-collections</span></code></pre>
<p>根據 OpenAPI <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.1.md#reference-object">reference object</a> 可以使用相對路徑引入檔案,而如果要使用檔案中的某個 field,在檔案後用 <code>#</code> 加入 hash router 就可以了。是非常方便的方法。所以在這邊我就可以把 comments 與 posts 分成不同的資料夾管理,並且把資料夾中的東西放在 index.yaml 中,方便取用。</p>
<p>以 GET <code>/posts/{postId}</code> 為例,其 ref 是 <a href="https://github.com/cwc329/swagger-ui-demo/blob/main/docs/posts/index.yaml">posts/index.yaml#/post-collections</a>。</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">post-collections</span><span class="token punctuation">:</span><br /> <span class="token key atrule">get</span><span class="token punctuation">:</span><br /> <span class="token key atrule">$ref</span><span class="token punctuation">:</span> getPost.yaml<br /> <span class="token key atrule">put</span><span class="token punctuation">:</span><br /> <span class="token key atrule">$ref</span><span class="token punctuation">:</span> updatePost.yaml<br /> <span class="token key atrule">delete</span><span class="token punctuation">:</span><br /> <span class="token key atrule">$ref</span><span class="token punctuation">:</span> deletePost.yaml</code></pre>
<p>想要看 GET <code>/posts/{postID}</code> 就要去 <a href="https://github.com/cwc329/swagger-ui-demo/blob/main/docs/posts/getPost.yaml">getPost.yaml</a> 看。</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">tags</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> posts<br /><span class="token key atrule">summary</span><span class="token punctuation">:</span> get a post<br /><span class="token key atrule">parameters</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">$ref</span><span class="token punctuation">:</span> ../schemas/index.yaml<span class="token comment">#/parameters/postId</span><br /><span class="token key atrule">responses</span><span class="token punctuation">:</span><br /> <span class="token key atrule">"200"</span><span class="token punctuation">:</span><br /> <span class="token key atrule">description</span><span class="token punctuation">:</span> ok<br /> <span class="token key atrule">content</span><span class="token punctuation">:</span><br /> <span class="token key atrule">application/json</span><span class="token punctuation">:</span><br /> <span class="token key atrule">schema</span><span class="token punctuation">:</span><br /> <span class="token key atrule">$ref</span><span class="token punctuation">:</span> ../schemas/index.yaml<span class="token comment">#/schemas/objects/post</span></code></pre>
<p>可以看到我這邊 parameter 與 response 的 schema 都是使用 ref 的方式,這樣使用的好處是因為 postId 以及 post 的回覆其實很多地方都會使用到,如果不用 ref 的方式而是在每個地方都寫,這樣維護上很麻煩,假設今天 postId 從原本的流水號變成 uuid,又或者 post 要增加一個屬性,在維護文件上會很麻煩。但是使用 ref 並且事先寫好,就可以避免這種問題。</p>
<p>我習慣把 component 的定義統一放在 schema 的資料夾中,由於現在使用資料結構不多,我先偷懶用一個 <a href="https://github.com/cwc329/swagger-ui-demo/blob/main/docs/schemas/index.yaml">index.yaml</a> 管理,簡單的分成 parameters, requestBodies, responses 與 schemas,這些命名都是依照 <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.1.md#components-object">components object</a> 的規範去命名。把這些</p>
<p>應用 component 以及 reference 就可以建立有效率、可以重複使用的各種小元件,讓撰寫 API 文件變得迅速。</p>
<p>文章到此其實基本的撰寫已經差不多了,如果讀者想要自行練習的話我有兩個小習題可以做。</p>
<ol>
<li>
<p>我在 swagger-ui-demo 的 repo 中已經有建立兩個 parameter: order 與 sort,這是讓 API 把回傳的東西排序。請把這兩個 parameter 加到 GET <code>/posts</code> 中並且試著用加上這兩個 parameter 去發送 request。</p>
</li>
<li>
<p>在 demo 的 API server 中其實還有一個 API path 我沒有寫在文件中,大家可以到 <a href="http://localhost:5566/">http://localhost:5566</a> 首頁去看一下 json-server 預設會有哪些 path, request bodies 以及 response,並且試著加在 swagger-ui 上。</p>
</li>
</ol>
<p>第 1 題會比較簡單。第 2 題就比較困難,因為基本上是要自己從 0 開始。<br />
那今天的介紹就大概到這邊。<br />
感謝!</p>
共筆中的共筆 - 以 JavaScript30 為例
2021-10-26T00:00:00Z
https://blog.errorbaker.tw/posts/benben/03-collaborative-writing/
<!-- summary -->
<!-- 有寫過共筆嗎?如果沒有就自己發起吧! -->
<!-- summary -->
<p><img src="https://i.imgur.com/ZmEEvep.png" alt="imgur picture" /></p>
<blockquote>
<p>圖片來源:imgur</p>
</blockquote>
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/03-collaborative-writing/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<p>嗨嗨!筆者的第五期 Lidemy 導師計畫剛結束了,大家過得還好嗎?整個過程也是有血有淚、可歌可泣的啊 ><,希望大家也都順利畢業!</p>
<p>原本以為畢業之後應該會順利一點,但之後還要面對求職大魔王啊,看來我想得真是太美好了,想說之後應該比較有時間可以寫文章的說 XD</p>
<p>這個計畫的六個月說長不長說短不短,有些人也在期間做了很多事、研究了不一樣的東西,大家也都成長不少吧! 這篇文章算是筆者在學習時期「發起共筆讀書會」的一個小回顧。之前聽到學長姐的公司有讀書會,覺得蠻不錯的,不確定大家的公司有沒有讀書會,如果有的話,那很棒,如果沒有就自己發起吧!</p>
<h2 id="%E7%B7%A3%E8%B5%B7"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/03-collaborative-writing/#%E7%B7%A3%E8%B5%B7">#</a> 緣起</h2>
<p>為什麼會有這個共筆讀書會活動呢?緣起是這樣的,因為筆者還在 Lidemy 學習時,一直都會做筆記,相信大家都跟筆者一樣認真的。有一天大家在聊天時(在 gather.town 上面,可以想成是比較自由版本的 google meet,可以自由去別的空間聊天、開會等等),我推薦大家 <strong>JavaScript30</strong> 這個免費的學習資源,但我其實也才看了 5 篇左右,但是真的覺得這個資源很不錯,於是突發奇想「還是我們來辨一個讀書會」主題就是:JavaScript30 ,剛好這也是適合新手的學習資源,非常適合還是新手的我們,找了幾個人一起輪流分享,順便練習自己的解說能力,然後再分享給其他人聽,聽的人可以不只我們自己人,其他 gather.town 上的人也都可以聽。</p>
<blockquote>
<p>延伸學習:<a href="https://javascript30.com/">JavaScript</a></p>
</blockquote>
<p>JavaScript30 一直是我覺得不錯的免費資源,只是要先註冊一下才可以看這樣,分享給大家的時候,大家也都覺得不錯,也有人之前就註冊過了,但跟我一樣註冊完一直都沒看(笑),畢竟是免費的嘛,只要是免費的就會覺得:「啊!反正之後還有很多時間可以看」然後就會一直放著了,對!我就是這樣,但如果要分享給別人就不一樣了。</p>
<p>因為有要分享給別人的動力,會逼迫你一定要去看、要去了解,或是至少要看懂要分享的部分,所以動力也會比較足夠,比起「免費的先領起來長灰塵」來說,這個動力絕對多很多,甚至比起你自己看的了解程度也會多很多,這是我自己參加完的心得,但應該很多人也有這樣的感覺。</p>
<h2 id="%E9%81%B8%E6%93%87%E4%BD%A0%E7%9A%84%E5%B7%A5%E5%85%B7%EF%BC%9Ahackmd-%2F-codepen"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/03-collaborative-writing/#%E9%81%B8%E6%93%87%E4%BD%A0%E7%9A%84%E5%B7%A5%E5%85%B7%EF%BC%9Ahackmd-%2F-codepen">#</a> 選擇你的工具:HackMD / CodePen</h2>
<p>首先,先來介紹線上筆記軟體 - <strong>HackMD</strong> 吧,因為要可以共筆嘛,所以「線上的功能」是必備的,當然也可以用 Notion 之類的,總之要大家都可以接受並願意使用的線上工具,HackMD 是一個線上的 <strong>markdown</strong> 編輯器,markdown 簡單來說就是 github 裡 <code>README.md</code> 的 <code>.md</code> 檔,可以讓人快速寫出文件的格式的寫法,有點像是 <strong>HTML</strong> 的簡化版,簡單明瞭的寫法,當然要客製化比較難一點。</p>
<p>再來是線上 IDE - <strong>CodePen</strong> ,因為我們有寫扣的需求,又需要一個即時可以編寫、執行的環境,所以一個輕巧的線上平台是我覺得很棒的工具,其他像是 CodeSandbox 等可能也不錯,但因為我們的 JavaScript30 只會用到原生的 HTML, CSS, JavaScript,顯然 CodePen 比較適合我們的需求,於是就選擇了這個平台做為 demo 的平台。</p>
<p>有了這兩個核心的平台,就差不多可以準備共筆了,可以簡單打一下介紹,如:<code>README.md</code> 的檔案那樣,簡單的描述一下要怎麼開始讀書會、時程之類的,更簡單來說就是:「人、事、時、地、物」這些都說清楚,才不會讓人不知道要幹嘛。</p>
<h2 id="%E9%96%8B%E5%A7%8B%E5%85%B1%E7%AD%86%E5%90%A7"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/03-collaborative-writing/#%E9%96%8B%E5%A7%8B%E5%85%B1%E7%AD%86%E5%90%A7">#</a> 開始共筆吧</h2>
<p>即然工具都有了,就準備開工吧,當然也都可以選擇自己喜歡的工具啦,但要記得工具只是輔助並非一定。</p>
<p>因為是筆者發起的,所以就先幫大家準備簡單的模版:</p>
<pre class="language-markdown"><code class="language-markdown"><span class="token title important"><span class="token punctuation">#</span> Title</span><br /><br /><span class="token title important"><span class="token punctuation">##</span> HTML</span><br /><br /><span class="token title important"><span class="token punctuation">##</span> CSS</span><br /><br /><span class="token title important"><span class="token punctuation">##</span> JavaScript</span><br /><br />// 其他補充 ...<br /></code></pre>
<p>大概是醬,因為都是原生的 HTML, CSS, JavaScript ,由這三個面向去解說一定不會錯,雖然主要是 JavaScript ,但也可以看看 HTML 怎麼規劃的,有時候 CSS 也會有特別的玩意,只要是你學到的東西都可以分享。</p>
<p>如果你還不太會 markdown 語法也沒關係,可以參考 <strong>HackMD 使用教學</strong> ,號稱 10 分鍾就可以上手,但筆者認為對非工程師的朋友,還是有點不友善,推坑給非工程師的朋友還是一直沒成功 XD</p>
<blockquote>
<p>延伸學習:<a href="https://hackmd.io/c/tutorials-tw/%2Fs%2Fquick-start-tw">HackMD 使用教學</a></p>
</blockquote>
<p>另外 HackMD 也有投影片模式(side mode)喔,只是要先算好內容的多寡,才不會被截斷的問題,分享時真的很方便,也推薦試試看。</p>
<p>CodePen 的話,就真的比較工程師一點了,要先具備 HTML, CSS, JavaScript 的基礎,才可以比較懂在做什麼,如果讀書會的內容不需要這些的話,也不一定要用這個工具,可以用其他平台。</p>
<p>CodePen 介面看起來像這樣:</p>
<p><img src="https://i.imgur.com/T0viLyL.png" alt="CodePen interface" /></p>
<p>CodePen 使用起來像這樣:</p>
<p><img src="https://i.imgur.com/D7lmpRR.png" alt="CodePen usage" /></p>
<p>簡單說分為 3 個部分:HTML, CSS, JavaScript ,右邊可以即時顯視出當前的樣子,有什麼新的想法也可以試試看、玩玩看,當作一個 PlayGround 的概念,也可以分享給別人。</p>
<p>這個共筆讀書會的建置就差不多到這邊,之後就是最難的一部分:「持續下去」</p>
<p>在整個 JavaScript 30 中學習到的東西很多、很全面,雖然有些東西真的沒有這麼長用到,但是絕對可以打開你的眼界,甚至會讓你說:「竟然還有這種東西!」,而且都是原生的,看完不禁讓我懷疑:「我真的會 JavaScript 嗎?」</p>
<p>在 JavaScript 30 中一些有趣的東西:</p>
<ul>
<li><code>console.log</code>, <code>console.error</code>, <code>console.count</code> 等等。</li>
<li>canvas 繪圖</li>
<li>array 的各種練習</li>
<li>base64 像素的操作</li>
<li>其他有趣的小特效</li>
</ul>
<p>最後的簡單成果(感謝參與的同學們): <a href="https://hackmd.io/@benben6515/javascript-30">JS30 我要成為 JavaScript 大師</a></p>
<h2 id="%E7%B8%BD%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/03-collaborative-writing/#%E7%B8%BD%E7%B5%90">#</a> 總結</h2>
<p>這一篇又是稍為沒什麼技術內容的一篇(汗),但是一定有一些人不太了解這些工具,可能剛好就有人很有想法,但是沒有工具、不知道如何開始,那麼這一篇文章就是為你所寫的。</p>
<p>其實這整個 ErrorBaker 部落格也是一個共筆部落格,所以你想要的話,也可以一起發起一個共筆部落格,或著如果你也覺得共筆讀書會不錯的話,也可以發起共筆讀書會,那麼現在就開始規劃你的共筆吧!</p>
<p>最後希望大家可以一同進步、一同成長,感謝您的閱讀。</p>
<h3 id="ref"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/03-collaborative-writing/#ref">#</a> Ref</h3>
<ul>
<li><a href="https://javascript30.com/">JavaScript 30</a></li>
<li><a href="https://hackmd.io/">HackMD</a></li>
<li><a href="https://codepen.io/">CodePen</a></li>
<li><a href="https://medium.com/starrocket/hackmd-product-story-1e332f83d343">為工程師文件而生的協作平台:HackMD 開發故事 | Medium</a></li>
<li><a href="https://hackmd.io/@benben6515/javascript-30">JS30 我要成為 JavaScript 大師 | 筆者 HackMD 範例</a></li>
</ul>
<blockquote>
<p>免責聲名</p>
</blockquote>
<p>以上均為筆者自身經驗,難免小有主觀意見,供讀者們參考,也歡迎分享經驗交流。<br />
如果有錯誤的地方還請大大們指正,筆者會立刻修改,再次感謝大家!</p>
<p><a href="https://creativecommons.org/licenses/by/4.0/"><img src="https://i.creativecommons.org/l/by/4.0/88x31.png" alt="Creative Commons License" /></a></p>
<p>本著作係採用 <a href="https://creativecommons.org/licenses/by/4.0/">創用 CC 姓名標示 4.0 國際授權條款</a> 授權。您可以在 <a href="https://benben.me/">benben.me</a> 找到我。</p>
<p>This work is licensed under a <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>. You can find me at <a href="https://benben.me/">benben.me</a></p>
我所理解的 GitFlow
2021-10-28T00:00:00Z
https://blog.errorbaker.tw/posts/tian/git-flow/
<!-- summary -->
<p>公司開始 Run Git Flow 也已經超過 3 個月了,記錄一下,隨著團隊擴大,需求和用戶增加,版本控制流程的演變過程。</p>
<!-- summary -->
<p>這樣的流程有助於我們穩定地、持續地進行交付。</p>
<!-- more -->
<h3 id="%E4%B8%80%E6%A2%9D%E4%B8%BB%E5%88%86%E6%94%AF"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/git-flow/#%E4%B8%80%E6%A2%9D%E4%B8%BB%E5%88%86%E6%94%AF">#</a> 一條主分支</h3>
<blockquote>
<ul>
<li>master</li>
</ul>
</blockquote>
<ol>
<li>Team Lead 的個人專案,也沒什麼用戶在使用, <code>master</code> 就是我的遊樂場,我想怎麼改怎麼改,反正改壞趁沒人發現的時候再改回來就好。</li>
</ol>
<h3 id="%E5%85%A9%E6%A2%9D%E4%B8%BB%E5%88%86%E6%94%AF"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/git-flow/#%E5%85%A9%E6%A2%9D%E4%B8%BB%E5%88%86%E6%94%AF">#</a> 兩條主分支</h3>
<blockquote>
<ul>
<li>master</li>
<li>develop</li>
</ul>
</blockquote>
<ol start="2">
<li>業務需求增加,加入初期團隊成員。開始分 branch 分別是 <code>develop</code> 和 <code>master</code>,團隊成員直接在 <code>develop</code> 上做開發,<code>develop</code> 和 <code>master</code> 分別綁上測試和正式環境的 CI/CD,<code>develop</code>用於測試,<code>master</code>用於交付,每次開發週期將 <code>develop</code> 合併到 <code>master</code> 一次做為新版本發佈。</li>
</ol>
<h3 id="%E5%85%A9%E6%A2%9D%E4%B8%BB%E5%88%86%E6%94%AF%E3%80%81%E4%B8%80%E6%A2%9D%E5%89%AF%E5%88%86%E6%94%AF"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/git-flow/#%E5%85%A9%E6%A2%9D%E4%B8%BB%E5%88%86%E6%94%AF%E3%80%81%E4%B8%80%E6%A2%9D%E5%89%AF%E5%88%86%E6%94%AF">#</a> 兩條主分支、一條副分支</h3>
<blockquote>
<ul>
<li>master(主)</li>
<li>develop(主)</li>
<li>feature(副)</li>
</ul>
</blockquote>
<ol start="3">
<li>為確保程式碼品質與,每個人在實作需求的時候都要從 <code>develop</code> 開啟 <code>feature</code> 分支,每次合併回 <code>develop</code> 都要發 Pull Request,由 Team Lead Review 合併。</li>
<li>為了讓自己的 Code 容易 Review,如果開發的過程需要重構,另外開一個用於重構的 <code>feature</code> branch 來重構程式碼,新的功能則基於這個重構的 Branch 繼續做開發,一個功能可以分別發好幾個 PR,盡量減少每個 PR 改動檔案的數量,</li>
<li>需求做不完,團隊人員再度增加,為增進團隊交流,保持同步,隨著專案擴大,每個人在發 PR 的時候都可以 Assign 給相關成員(曾經接觸過這裡的程式碼,或將要接觸這部分程式碼的人),團隊的中心分散,每個人都可以對彼此的 Code 發表評論與 Approved。</li>
<li>同步 Coding Style,每隔固定週期,對這這個週期發送的 PR 做 Review,將 Coding Style 寫成文件,方便未來入職的人能快速的理解專案,並融入團隊。</li>
</ol>
<h3 id="%E5%85%A9%E6%A2%9D%E4%B8%BB%E5%88%86%E6%94%AF%E3%80%81%E5%85%A9%E6%A2%9D%E5%89%AF%E5%88%86%E6%94%AF"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/git-flow/#%E5%85%A9%E6%A2%9D%E4%B8%BB%E5%88%86%E6%94%AF%E3%80%81%E5%85%A9%E6%A2%9D%E5%89%AF%E5%88%86%E6%94%AF">#</a> 兩條主分支、兩條副分支</h3>
<blockquote>
<ul>
<li>master(主)</li>
<li>release (副)</li>
<li>develop(主)</li>
<li>feature(副)</li>
</ul>
</blockquote>
<ol start="7">
<li>以 <code>develop</code> 作為測試環境,當多人協作,不小心有沒寫好的程式碼,被合到 <code>develop</code> 的時候,這時候就會影響測試人員 QA 做測試,我們新增了 <code>release</code> branch,來代替 <code>develop</code> 測試的功能,將原來的測試環境的 CI/CD 改從 <code>develop</code> 綁到 <code>release</code> 上面,這樣一來 PR 合併到 <code>develop</code> 的時候就不會部署,不僅減少不必要 build time,當 <code>release</code> branch 出現問題的時候,可以即時將 <code>release</code> rebase 到 <code>develop</code> 上面沒有問題的 commit 讓 QA 的測試不會被影響,當測試遇到問題也可以即時的在 <code>release</code> 上面做修正,只不過最後要記得再合併回主要的 <code>develop</code> 分支。</li>
</ol>
<h3 id="%E5%85%A9%E6%A2%9D%E4%B8%BB%E5%88%86%E6%94%AF%E3%80%81%E4%B8%89%E6%A2%9D%E5%89%AF%E5%88%86%E6%94%AF%EF%BC%88gitflow%EF%BC%89"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/git-flow/#%E5%85%A9%E6%A2%9D%E4%B8%BB%E5%88%86%E6%94%AF%E3%80%81%E4%B8%89%E6%A2%9D%E5%89%AF%E5%88%86%E6%94%AF%EF%BC%88gitflow%EF%BC%89">#</a> 兩條主分支、三條副分支(GitFlow)</h3>
<blockquote>
<ul>
<li>master(主)</li>
<li>hotfix(副)</li>
<li>release (副)</li>
<li>develop(主)</li>
<li>feature(副)</li>
</ul>
</blockquote>
<ol start="8">
<li>即便走過這個流程,還是會有 Bug 被部署到正式環境的情況發生,這時候就需要從 <code>master</code> 開出一個 <code>hotfix</code> branch 做搶修,確認沒問題之後,也是要合併回 <code>master</code> 和 <code>develop</code> 兩個大分支,如果沒有將 <code>hotfix</code> 合併到 <code>develop</code>,在下個週期部署的時候,很可能會將修好的 hotfix 蓋掉。</li>
</ol>
<p>你有什麼不使用 Git Flow 的理由嗎?,歡迎下面留言與我分享?</p>
用 11ty 寫部落格的心得
2021-10-31T00:00:00Z
https://blog.errorbaker.tw/posts/lavi/blog-log/
<h2 id="td%3Blr"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/blog-log/#td%3Blr">#</a> TD;LR</h2>
<!-- summary -->
<p>從規劃、設計、開發到部署,簡單談談用 eleventy 架設部落的心得。</p>
<!-- summary -->
<hr />
<p>最近一個月左右的時間都在架設部落格,利用這篇回顧一下過程,也記錄一下心得。</p>
<p>在正式開始前先提一下為什麼決定自己架部落格好了。</p>
<p>自己過去有在 Medium 還有 CoderBridge 放一些內容,在 Medium 平台上寫技術文章的問題,在網路上有已經有非常多的討論,可以左轉 Huli 的<a href="https://hulitw.medium.com/do-you-really-want-to-write-tech-blog-on-medium-3dd25640f26c">在 Medium 上寫技術文章,你確定嗎?</a>或其他文章。</p>
<p>不過對於自己而言問題 Medium 主要體現在兩個部分:寫文章體驗、沒有文章列表和分類。</p>
<p>寫文章體驗部分,如果單純分享純文字內容可能沒有那麼不便,但對於工程師而言想要寫技術文章內容很難避開程式碼,在 Medium 上面放程式碼絕對是體驗很糟的選擇,還有自己習慣使使用 Markdown 來寫文章然後再上傳 Medium,即使有了轉換器也還是很麻煩。</p>
<p>此外可能出於商業考量, Medium 的文章列表非常難用,整體並沒有一個能夠一目了然看到自己 PO 了多少篇文章的功能,還有如果不是 Publication 的話,目前似乎還沒有分類的功能。</p>
<p>Medium 這個平台的推力對於自己主要就是這兩項,其他比較常提到的 SEO、搬家麻煩等等的問題其實對我而言也沒有那麼嚴重。自己本來文章就不多,搬家的過程其實並沒有想像中的那麼困難(後面會提到如何搬家),SEO 部分沒什麼流量所以還不需要擔心。</p>
<p>再來是 CoderBridge ,CoderBridge 的確是蠻方便的平台,功能上也很完整,沒甚麼好挑剔的,蠻推薦初期想要寫自己的技術文章但是還沒有能力或懶得建立自己部落格的人,自己大多的技術文章也都放在上面。</p>
<p>不過做為工程師到了一個程度之後就會開始想要架設自己的部落格,一方面可以累積一些內容,還有之前在面試的時候就有被問到為什麼沒有自己架設,這樣的問題不管怎麼回答都是滿滿的尷尬。</p>
<p>另外一點是,總覺得自己的部落格算是一個工程師技術能力上的證明,在處理部落格的過程中,從前端、<s>後端</s>(不,根本沒有)、部署都能夠接觸到,也能夠踩到一些坑還有處理問題,也算是不錯的經歷。也有個平台可以累積內容。</p>
<p>好了,建立部落格的原因大概講到這裡,正文開始。</p>
<h2 id="%E5%B7%A5%E5%85%B7%E6%A3%A7"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/blog-log/#%E5%B7%A5%E5%85%B7%E6%A3%A7">#</a> 工具棧</h2>
<p>整個部落格大概有五個步驟:規劃、設計、開發、搬家、部署,這篇文章也大概會依照這樣的脈絡下去敘述。</p>
<p>規劃部分,一開始初步思考一下部落格的定位,可以找找常看的部落格,參考有哪些頁面和功能是想要放進部落格的。思考完定位後,最重要的是列出資訊架構,包含說每個頁面中需要放入哪些內容,需要哪些功能。</p>
<p>這裡簡單列出一些自己列出的項目:</p>
<ul>
<li>主頁:文章列表,列表項目需要
<ul>
<li>標題</li>
<li>概要</li>
<li>發文時間</li>
<li>閱讀時長</li>
<li>標籤</li>
</ul>
</li>
<li>archive 文章列表</li>
<li>post 文章頁面</li>
</ul>
<p>另外,除了頁面之後,也需要列出每一頁共同區塊。像是最基本的 Footer 和 Header 的資訊架構。列出資訊架構後,在後面的設計與開發,會依照這些頁面和區塊作為基本單位去執行。</p>
<h2 id="%E8%A8%AD%E8%A8%88"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/blog-log/#%E8%A8%AD%E8%A8%88">#</a> 設計</h2>
<p>在軟體上是用 Figma。至於設計的流程部分,自己不是什麼正統的設計師,並沒有非常嚴謹清楚的方法論,只是分享下個人經驗。</p>
<p>自己在設計上的順序是這樣:</p>
<ol>
<li>顏色和字體:自己認為會決定整個網頁的調性</li>
<li>文章中的出現的元素</li>
<li>將元素微調,組合成各個頁面需要的功能</li>
</ol>
<h3 id="%E8%89%B2%E5%BD%A9%E5%92%8C%E5%AD%97%E9%AB%94"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/blog-log/#%E8%89%B2%E5%BD%A9%E5%92%8C%E5%AD%97%E9%AB%94">#</a> 色彩和字體</h3>
<p>起初是希望走比較乾淨的風格,但又不要太嚴肅(希望有做到)。所以在字體上就先決定用無襯線體作為主要的文字,在比較細節的部分則用襯線體做為點綴。</p>
<p>色彩部分,定義了這些顏色:</p>
<ul>
<li>Primary:主色</li>
<li>Secondary:輔色</li>
<li>Text:主要字體顏色</li>
<li>Text-secondary:輔助字體顏色</li>
<li>Text-highlight:強調字體顏色</li>
<li>background:背景色</li>
<li>background-secondary:次背景色</li>
</ul>
<p>因為還有 Dark-mode,所以需要兩套配色。當然,每一個部落格設計都不同,很有可能在顏色上有增減,但基本一定需要定義的是字體和背景色(不然要怎麼看內容...),下面是自己這個階段定義出來的色盤:</p>
<p><img src="https://blog.errorbaker.tw/img/posts/lavi/blog-log/Pasted%20image%2020211026222438.png" alt="" /></p>
<p>在這個階段會先做一個非非常簡單的文章 Mockup,能夠比較好想像顏色組合的結果,也可以先在這個階段初步的避免掉易讀性的問題。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/lavi/blog-log/Pasted%20image%2020211026222708.png" alt="" /></p>
<p>在這個階段只是抓一個方向,在最後開發的時候還會在明度、彩度上根據易讀性作更細節的調整。</p>
<h3 id="%E5%85%83%E7%B4%A0"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/blog-log/#%E5%85%83%E7%B4%A0">#</a> 元素</h3>
<p>設計過程比較特別部分在於,自己是先從文章內容中的元素開始設計,文章中有哪些元素呢?這並不是太難的問題,基本上 Markdown 裡面有哪些元素,文章中就有哪些,可以找一些 markdown-demo 來確定有哪些元素。這邊簡單列舉:</p>
<p>行內元素樣式:<strong>粗體</strong>、<em>斜體</em>、<s>橫線</s>、<a href="https://blog.errorbaker.tw/posts/lavi/blog-log/">連結</a>、<code>code</code>等</p>
<p>區塊元素樣式:</p>
<ul>
<li>內文段落 paragraph</li>
<li>標題:h1~h6</li>
<li>order list, unordered list</li>
<li>引用區塊 quoteblock</li>
<li>codeblock</li>
<li>分隔線</li>
<li>Table</li>
</ul>
<p>在設計區塊樣式時,除了要滿足單一元素的使用情境,而凸顯元素使用情境的方式在於和其他元素之間做出區別。</p>
<p>在設計的時候可以先把最簡單的內文段落(paragraph)樣式設定出來(包含字距、行距等),然後把內文作為最基礎的樣式來做變化。</p>
<p>像是清單(order list/ unordered list) 主要的目的是要能夠清楚的列出項目,那在行距上就需要再大一點,來和內文做出區別。</p>
<p>還有像引用區塊的文字,本身視覺上是不是真的帶來「引用」的感覺(通常的設計在左邊會有直線,或者是放在方框裡面),並和普通的段落有所區別。</p>
<p>除了個別的樣式外,在區塊會有兩不同類型的互動,一種是相鄰,另一種是嵌套。</p>
<p>在兩相鄰區塊的設計上,就必須要注意區塊間距的拿捏,需要在比行距還要大、但又不能過大導致太分離難以閱讀。而在嵌套上,像是 list 和 quoteblock 能夠再包住其他區塊像是其他 list,這時如何同時內外兩層元素樣式的融合是需要思考的部分。</p>
<p>在使用 Figma 設計時有一個訣竅,可以上述每個元素都做成 Figma 的 Component,並在旁邊直接互相組合排列,模擬真實的文章,來檢視自己的設計。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/lavi/blog-log/Pasted%20image%2020211026231304.png" alt="" /></p>
<h3 id="%E9%A0%81%E9%9D%A2%E5%92%8C%E5%8D%80%E5%A1%8A"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/blog-log/#%E9%A0%81%E9%9D%A2%E5%92%8C%E5%8D%80%E5%A1%8A">#</a> 頁面和區塊</h3>
<p>當個別元素完成後,就能夠以頁面為單位來進行設計了。在設計每個頁面時,會以先前的元素作為基礎,進行調整及組合後,來構成頁面上面的元件。舉例來說,這是主頁面的文章列表,基本上就是由多個文章項目排列而成</p>
<p><img src="https://blog.errorbaker.tw/img/posts/lavi/blog-log/Pasted%20image%2020211027220350.png" alt="" /></p>
<p>而在每個文章項目都是由原本文章中的元素(標題、內文段落等等)做樣式上的調整後再進行組合。</p>
<ul>
<li>在文章標題上,就直接採用 h1 標題的樣式</li>
<li>標籤則是連結的樣式,所以帶有 <code>hever:underline</code> 的效果</li>
<li>時間上,基本上是內文段落的樣式(顏色、字距)但使用襯線體的字型做出區別</li>
<li>概要也是內文段落的樣式,但運用顏色做出區別</li>
<li>分隔線就直接採用文章的分隔線</li>
</ul>
<p>除此之外不同頁面之間的元件,如果資訊架構上相似的話,也是可以共用的。像主頁文章列表和標籤文章列表的樣式基本上就沒有太大差異,只是缺少了上方的 tag 而已。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/lavi/blog-log/Pasted%20image%2020211027232241.png" alt="標籤頁文章列表" /></p>
<p>將元素重組的好處一來是省時間,二來是整體的樣式會比較一致,當然缺點就是看起來會比較呆板無變化。相信好的設計還是能夠在樣式和一致性上取得平衡,但自己能力有限,也希望讀者把注意放在文章上,樣式的變化就比較次之。</p>
<h2 id="%E9%96%8B%E7%99%BC"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/blog-log/#%E9%96%8B%E7%99%BC">#</a> 開發</h2>
<p>再來是開發,這裡先提一下使用到的開發上使用的工具。自己在開發上是用 eleventy 這個 static site generator(後面簡稱 11ty)。一開始其實沒想過這個選擇,身為 React 仔,一開始的方案其實是朝 Gatsby 或者是 Next,覺得還可以順便寫熟悉的 React 根本是一舉兩得,不過拖延症卻讓自己遲遲還沒開始動工。</p>
<p>後來因為在 <a href="https://blog.errorbaker.tw/">Errorbaker</a> 上參與共筆部落格,而部落格本身也是用 11ty 寫的。了解後發現是個輕量的選擇,想想與其要花時間在學習新的框架,那不如就先採用個簡單的方案,看看能不能在短時間內就把部落格架起來(寫完之後,自己的答案是不太行🥲)。</p>
<p>那在這部分也會提一下自己使用 11ty 和 <a href="https://github.com/google/eleventy-high-performance-blog"><br />
eleventy-high-performance-blog</a> 這個 template 的心得,以及自己額外做的處理。</p>
<h3 id="eleventy"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/blog-log/#eleventy">#</a> Eleventy</h3>
<p>消毒一下,這裡是非常個人的心得,好不好用本身和每個人的道行很有關係。</p>
<p>如果想要多了解 11ty 可以先參考前輩的這兩篇</p>
<ul>
<li><a href="https://blog.huli.tw/2021/08/22/eleventy-over-hexo/">除了 hexo,也可以考慮用 eleventy 來寫技術部落格</a></li>
<li><a href="https://jason-memo.dev/posts/why-i-leave-medium-and-build-blog-with-eleventy/">為什麼我離開 Medium 用 eleventy 做一個 blog</a></li>
</ul>
<p>11ty 基本的用法需要搭配 template language 來 compile 出 html,標配是 nunjucks,不過也可以用自己喜歡的 template language 像是 Pug, ejs, Mustache 等等,或者官方也有提到可以直接使用 JS 來 compile。</p>
<p>那除了利用 template language 外,寫文章的部分</p>
<p>在 build 的過程中,11ty 可以自由選要用什麼樣的 parser 來解析語法,在<a href="https://github.com/11ty/eleventy-base-blog/blob/master/.eleventy.js#L68">官方的 base-blog 中</a>使用 <a href="https://github.com/markdown-it/markdown-it">markdown-it</a> 做為 markdown parser(hight-preformance 也是),所以如果想要調整 markdown 解析出來的 html 或者新增功能,就只需要更改 config 或者新增 plugin 上去即可。</p>
<h3 id="nunjucks"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/blog-log/#nunjucks">#</a> Nunjucks</h3>
<p>Nunjucks 本身語法不算複雜,如果有接觸過模板語言或者只是作一些內容的調整非常容易。但畢竟還是有學習曲線,再一些複雜的調整還是需要更深入的理解語法,而且語法本身寫起來稍微比較像早期的 PHP,比起平常常接觸的 JSX 起來並沒有那麼自然。但這可能不是好的比較,也才寫過幾天的 PHP。</p>
<p>Nunjucks 本身也有提供一些非常基本的操作,用這些操作來組合成需要的頁面功能如果比較複雜的話自己覺得頗有難度。在 11ty 中如果要操作模板引擎很方便,有提供 API 來統一操作。這樣一來就能夠在 <code>eleventy.js</code> 這個檔案中使用 JS 寫出需要的 filter ,再拿到 Nunjucks 裡面做使用。</p>
<p>這樣一來比起單純使用 11ty 和 Nunjucks 的語法組合功能會方便非常多。像是利用正則就可以做到像是下方的 summary (參考自 <a href="https://github.com/Lidemy/error-baker-blog/blob/main/_11ty/summary.js">Errorbaker source code</a> )</p>
<pre class="language-text"><code class="language-text"><!-- summary --><br />這邊是摘要 <br /><!-- summary --><br /><br />這邊開始才是本文</code></pre>
<p>在撰寫 Nunjucks 就能時使用下面的 custom filter 來做到渲染 tag 內部的內容:</p>
<pre class="language-text"><code class="language-text"><p>{{post | summary }}</p></code></pre>
<p>產生出的 HTML:</p>
<pre class="language-text"><code class="language-text"><p>這邊是摘要</p></code></pre>
<p>不過在寫 custom filter 的時候需要知道 11ty 本身給的資料結構,自己在文件裡面一直找不到一份齊全的資料,只能在 custom filter 內部 print 出來後輸出到 log 裡面看,導致花了蠻多的時間的。</p>
<p>此外 Nunjucks 可能太少使用者,有一點蠻影響開發體驗的是 VScode 似乎沒有對應的 Formatter(有 extension 但無法使用),這個時代還要要手動 Prettier 真的非常之惱人,還有 syntax highlight 常常也有問題。</p>
<h3 id="eleventy-high-performance-blog"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/blog-log/#eleventy-high-performance-blog">#</a> eleventy-high-performance-blog</h3>
<p>自己是基於 <a href="https://github.com/google/eleventy-high-performance-blog">eleventy-high-performance-blog</a> 這個 template 來改造搭建的。既然都叫 high-performance 了,那當然做好做滿的就是性能優化,先附上 lighthouse。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/lavi/blog-log/Pasted%20image%2020211028230922.png" alt="圖片來源:https://github.com/google/eleventy-high-performance-blog" /><br />
<img src="https://blog.errorbaker.tw/img/posts/lavi/blog-log/Pasted%20image%2020211028230907.png" alt="自己部落格的 lighthouse 評分" /></p>
<p>可以看到滿滿的綠燈,就算自己改過不少內容,Performance 還是達到 99 分。可以看到 github 上面的介紹:</p>
<blockquote>
<ul>
<li>Perfect score in applicable lighthouse audits (including accessibility).</li>
<li>Single HTTP request to <a href="https://web.dev/first-contentful-paint/">First Contentful Paint</a>.</li>
<li>Very optimized <a href="https://web.dev/lcp/">Largest Contentful Paint</a> (score depends on image usage, but images are optimized).</li>
<li>0 <a href="https://web.dev/cls/">Cumulative Layout Shift</a>.</li>
<li>~0 <a href="https://web.dev/fid/">First Input Delay</a>.</li>
</ul>
</blockquote>
<p>果然是 Google 內部開發的 template,優化真的是完全做好做滿。在這個 template 覺得最大的優點在圖片方面處理的非常之好,對於 Blog 這種本身沒有 SPA 也不用另外 fetch 資料的網站來說,處理好圖片幾乎就解決效能上 90% 的問題了。這邊簡單提一下自己使用這個 template 的心得。</p>
<h4 id="%E5%9C%96%E7%89%87%E5%84%AA%E5%8C%96"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/blog-log/#%E5%9C%96%E7%89%87%E5%84%AA%E5%8C%96">#</a> 圖片優化</h4>
<p>會把 markdown 中以網址插入的圖片 fetch 下來 local,還會做響應式圖片,以及載入的優化,像是:</p>
<ol>
<li>多寬度圖片大小</li>
<li>優化</li>
<li>async decode, lazy loading 以及 <code>content-visibility: auto</code></li>
<li>轉檔(webp, avif)</li>
<li>loading 時的 blurred placeholder</li>
</ol>
<p>尤其是第四點自己還是第一次看到把 jpg 檔案再轉換成模糊的 svg 的做法,做出來的 placeholder 一來體積小,還可以利用 baseUrl 直接放進 HTML,二來又能增進 Web vital 的 <a href="https://support.google.com/webmasters/answer/9205520?hl=en#cls_description"><strong>CLS</strong></a> 分數,而且因為是模糊的圖片,體驗上會比單純的 image placeholder 優雅許多。</p>
<p>但也是有對應的 tradeoff 就是,這個 template 最大的問題就是在 server 上 building 時,可能因為規格較弱,所以常常在轉換圖片卡住導致 building error,對應的處理會在後面部署的部分提到。</p>
<h4 id="csp"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/blog-log/#csp">#</a> CSP</h4>
<p>template 本身有透過 <code><meta></code> 來做 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP">CSP</a>,可以限制網頁上各種資源的來源,來避免 XSS 等攻擊。</p>
<p>如果要引用外部資源,像透過 uri 來存取像是 CSS, script 或者是字型時,在 CSP 就要另外做設定。行內 inline script 也要記得加上指定的 attribute 來建立 hash。</p>
<h4 id="css"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/blog-log/#css">#</a> CSS</h4>
<p>在 CSS 部份的優化方式上,是使用 <a href="https://kimeiga.github.io/bahunya/">Bahunya CSS</a>. 這個 classless framework,Bahunya CSS 本身沒有用到 class,只使用 element selector 來減少 CSSOM 解析負擔。</p>
<p>除此之外 CSS 你可以看到 template 裡面並沒有用 link 來載入 url 檔案。所有的 style 會先經過 purge CSS 來做 tree shake,去掉頁面不需要的 css 之後,再利用 style 插入到 <code><head></code> 裡面來減少 request 的次數。</p>
<p>不過這樣也造成一點問題就是,一個問題是基於 Bahunya CSS 上再做 customize 不太方便,最後幾乎原本的 CSS 都刪光光了...,還有預設的 purge CSS 會清除掉冗餘的 css variable 還有 font-face,不過自己在使用時有點問題,如果是多層 reference 的 variable 使用似乎就不會被解析而被刪掉。</p>
<p>另外這個時代還要寫純 CSS 真的 非 常 痛 苦,後來加入了 postcss 使用 import 還有 nesting 的功能。個人看法在 CSS 上只要有這兩樣功能在開發體驗上就會好非常非常多,沒有必要一定得用 Scss。</p>
<hr />
<p>關於這個 template 不知不覺提了很多,自己也還沒有研究的非常完全,像 SEO 的部分自己還是不太熟悉。如果你是剛好再用 11ty,還是很推薦說可以看看這個 template 做了哪些事情,用了那些套件去做到的。</p>
<h2 id="%E6%90%AC%E5%AE%B6"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/blog-log/#%E6%90%AC%E5%AE%B6">#</a> 搬家</h2>
<p>開發完之後就可以搬家囉</p>
<p>我的原本的發佈平台主要有兩個: CoderBridge 以及 Medium。Coderbridge 本身有提供輸出 markdown 的功能,非常的方便,但是 Medium 竟然只能下載整包的 HTML 😕。</p>
<p>自己使用 <a href="https://www.npmjs.com/package/medium-to-markdown">Medium to markdown</a> 這個小工具來做轉換,先到 Medium 上面把自己的備份下載下來,然後再轉換就可以。但轉換後的 Markdown 也依然要做整理,沒辦法做到無腦轉換。幸好自己的文章內容不算多,比起常常在 PO 文的作者整理起來還不算太複雜。</p>
<h2 id="%E9%83%A8%E7%BD%B2"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/blog-log/#%E9%83%A8%E7%BD%B2">#</a> 部署</h2>
<p>最後是部署,自己是參考<a href="https://jason-memo.dev/posts/why-i-leave-medium-and-build-blog-with-eleventy/">文中</a> 的方式,利用 <a href="https://vercel.com/dashboard">vercel</a> 來做部署。</p>
<p>第一次使用 vercel 的體驗蠻好的,介面很清楚使用起來也蠻簡單。但是就像前面提到的,在圖片轉換部分一直 build失敗,後來的做法是直接把整個網頁 build 出來並加入 git,而且並不使用內建的 11ty setting,而是當作 static site 來部署。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/lavi/blog-log/Pasted%20image%2020211031021955.png" alt="" /></p>
<p>這樣一來就暫時避免掉 build 的問題,雖然解法似乎不是很優雅就是了。</p>
<h2 id="%E6%9C%AA%E4%BE%86"><a class="direct-link" href="https://blog.errorbaker.tw/posts/lavi/blog-log/#%E6%9C%AA%E4%BE%86">#</a> 未來</h2>
<p>在完成這個部落格的時其實算是抱著完成 MVP 的心態去做的,這邊列下自己在這次的部落格中沒有做到,或者是未來可能希望再重新的部分:</p>
<ul>
<li>CSS 原本想用 tailwind 來處理,</li>
<li>SEO 部分,目前還不太了解怎麼實作</li>
<li>這次 Logo 模仿<s>抄襲</s> vercel 暫時先弄個三角形,之後有想法再補上</li>
<li>認真做個 og image</li>
</ul>
<p>除此之外也還有一些樣式上的 bug 要處理就是。不過總算是有一個自己的平台可以開始累積內容 🙂。</p>
<p>Big guy is john,感謝大家的觀看。</p>
從開發到產品的心得
2021-11-07T00:00:00Z
https://blog.errorbaker.tw/posts/minw/product-interest/
<!-- summary -->
<p>最近接觸到很多產品實務上的議題,趁著記憶猶新統整,覺得從開發者的角度開始參與產品管理十分有趣,也能結合自己過去所學,來分享一些作為開發者協助執行產品管理跟上手產品的零碎紀錄。</p>
<!-- summary -->
<!-- more -->
<h2 id="%E4%B8%8A%E6%89%8B%E5%B0%88%E6%A1%88"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/product-interest/#%E4%B8%8A%E6%89%8B%E5%B0%88%E6%A1%88">#</a> 上手專案</h2>
<p>身為開發者對於專案通常是最了解的人,但也僅限於自己有開發過的部分,要走向產品,就必須在自己開發以外的部分也非常熟悉,要怎麼在不寫程式碼就成為最了解產品的人,我自己分為產品、產品使用者跟利害關係人三個部分需要了解,並以一個開發兼產品的視角來進行闡述。</p>
<h3 id="%E7%94%A2%E5%93%81"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/product-interest/#%E7%94%A2%E5%93%81">#</a> 產品</h3>
<p>由於是開發者,先確保把軟體架構、DB Schema [^1]、跟實際使用 Client 端的服務並紀錄一些問題把這幾個釐清,針對核心邏輯也可以去閱讀程式碼,甚至畫出流程圖 [^2] 來作為未來 FAQ 的素材,雖然這些是規格書通常不會寫到的部分,但卻是常常會被問到的環節。</p>
<p>接下來,我嘗試的是「看過所有規格書並進行完整的測試」,以自己遇到的狀況來說,當時的規格書有兩個問題:</p>
<ul>
<li>並非所有功能都有與規格書同步</li>
<li>規格書的文件系統非常難用</li>
</ul>
<p>所以趁著這一批次將規格書移植到 Notion 外 [^3],有規格書的地方可以比對規格書並且記錄一些 Issue 跟提問跟找卡,沒規格書的地方可以補進規格書中。當補完規格的部分,接下來了解目前開發進展的狀況。</p>
<p>相較於規格書,由於團隊不大加上本身就是開發團隊,哪些卡在 code review 哪些這次會 release 相應比起了解其他部分要來的容易,總之嘗試要盡可能做到的是,當今天一個其他部門的同事過來問你說:</p>
<blockquote>
<p>欸、這邊是先 XXX 然後再 YYY 嗎?但為什麼會 ZZZ 這樣不是很怪嗎?</p>
</blockquote>
<p>能迅速的反應說「喔、你說的是 XXX 這邊嗎(拿出產品 demo)這有可能是 YYY 的原因,之前 ZZZ 已經開發到 JJJ 但因為 KKK 而 blocked ... 」,能做到這樣這樣就是初步對於產品有一定程度的了解了。</p>
<h3 id="%E4%BD%BF%E7%94%A8%E8%80%85"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/product-interest/#%E4%BD%BF%E7%94%A8%E8%80%85">#</a> 使用者</h3>
<p>當產品的商業邏輯掌握後,接下來要了解產品的使用者,才得以規劃可能的解決方案跟重新審視功能的必要性,以我遇到的狀況是,我們屬於 B2C 公司,產品分為對內使用的後台與對外使用者使用的 app,所以會有內外使用者需要了解。</p>
<p>外部使用者若原先就有相關的 Persona 資料可以先去閱讀一些使用者樣貌資料,不過這次專案使用者族群與自己相去不遠,相應的就比較不急迫去補齊質化的資料,而是需要量化的行為數據來輔助決策,這時候開發者的背景可以幫上不少忙,可以實際去載資料跑簡單的統計 [^4],看看使用者 5W1H 各種面向怎麼使用我們的產品。</p>
<p>另外內部使用者這一次則是安排了一輪使用者研究,分別是觀察跟訪談,透過跟設計師一同執行一次研究跟參與 User Story Mapping Workshop [^6],了解其他部門的工作流程、限制跟需求。</p>
<p>有別於外部使用者,內部使用者的產品可以預期使用者有比較長時間學習且屬於專家使用者,所以相比於了解行為數據,更需要的是透過產品來協助優化工作流程、增加公司內部的執行效率,比較接近服務設計的層級。</p>
<p>以了解使用者來說,盡可能做到的是今天有人問你說:</p>
<blockquote>
<p>欸、XXX 每週平日的時候都會在哪裡怎麼使用我們的產品?為什麼?</p>
</blockquote>
<p>能迅速的反應說「喔、平日的話因為 XXX 所以他們都會用我們的 YYY 因為 ZZZ,但如果遇到 JJJ 可能會 KKK ...」這樣就是初步對於產品使用者有一定程度的了解了。</p>
<h3 id="%E5%88%A9%E5%AE%B3%E9%97%9C%E4%BF%82%E4%BA%BA"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/product-interest/#%E5%88%A9%E5%AE%B3%E9%97%9C%E4%BF%82%E4%BA%BA">#</a> 利害關係人</h3>
<p>當產品跟使用者都有初步的了解後,要了解產品周遭的利害關係人(包含外部、即將接觸與內外部使用者們),心裡要抓著這些人之間的關係跟重要性,才得以決策功能的順位與準備需要的資料讓老闆說服他們。</p>
<p>以我自身的狀況,尚不需要對外部利害關係人去報告產品現況,所以對於外部利害關係人,可以盡量多從老闆口中了解他們喜歡什麼、底線是什麼之外,也可以同時了解目前業務上開發的需求,協助提出現行功能下的解決方案或安排新的功能開發。</p>
<p>而內部來說,則盡可能以透明公開的方式,善用內部公告 [^5] 來進行對話,讓大家了解產品開發進展,並盡量跟大家溝通透過回報的方式來進行 Issue 的收集,以上以了解利害關係人來說,盡可能做到的是老闆問你說:</p>
<blockquote>
<p>欸、今天因為 XXX 需要 YYY 可能要做 ZZZ</p>
</blockquote>
<p>能迅速的反應說「喔、不過 JJJ 也會需要 KKK 這樣考慮到 HHH 覺得是不是以 KKK 優先而 XXX 可以先用 GGG 來處理 ...」這樣就是初步對於產品利害關係人有一定程度的了解了。</p>
<p>上述的各種狀況說的都是初步的了解,我自己期待要能更深刻的了解,可能會是能從現況到預測可能的發展:</p>
<ul>
<li>以產品來說就是為未來先預先開一些打底的需求</li>
<li>使用者來說就是提供潛在可能開發的使用者數據</li>
<li>利害關係人可能是了解募資需求等等。</li>
</ul>
<p>而以上這些都會需要對現況相關的競品、族群跟市場都有更多經驗才得以積累,領域知識的路非常漫長。</p>
<h2 id="%E6%96%87%E4%BB%B6%E3%80%81%E9%96%8B%E5%8D%A1%E8%88%87%E5%B0%8F%E5%B7%A5%E5%85%B7"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/product-interest/#%E6%96%87%E4%BB%B6%E3%80%81%E9%96%8B%E5%8D%A1%E8%88%87%E5%B0%8F%E5%B7%A5%E5%85%B7">#</a> 文件、開卡與小工具</h2>
<p>初步的對於圍繞產品的一切有一些了解,但需要一些工具來輔助自己執行實際的工作,以下是過程中在日常執行 Roadmap 規劃、開卡、寫文件與討論等會使用到的工具。</p>
<h3 id="%E6%96%87%E4%BB%B6"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/product-interest/#%E6%96%87%E4%BB%B6">#</a> 文件</h3>
<p>覺得很適合拿來作為開始文件體系的敲門磚是 Onboard 文件 [^12],想像一個新人今天要了解公司,必須要知道哪些事情才能快速上手,透過這樣情境的視角慢慢補上公司各式各樣的大小文件,一路上補完的順序如下:</p>
<ul>
<li>Onboard & Onboard Checklist</li>
<li>工作流程</li>
<li>規格書</li>
<li>Issue Board 與 Roadmap</li>
<li>服務架構</li>
<li>對內公告</li>
<li>會議紀錄</li>
<li>FAQ</li>
</ul>
<p>對於小團隊來說,其實文件並非第一順位重要的事情,因為轉個身就可以進行討論,其實也不失效率。但保持產品團隊可以透過文件溝通的透明公開與溝通效率,這些文件系統是非常非常重要基石。</p>
<p>寫文件跟寫 code 一樣,需要發揮 Git 的精神,盡可能保持版控,而這裡的版控不一定要像是使用 Confluence 一樣有實質的版本紀錄,而是對於文件版本的更動需要討論跟 memo。</p>
<p>有了版控後,我發現文件中一件重要的事情是不要有「堅持」,舉例來說:寫文件一定會參考很多別人的範例,開了一堆欄位發現後續很難維護,這時候就大膽的移除,並在流程反省會議中說明為什麼,讓文件成為輔助。</p>
<h3 id="%E9%96%8B%E5%8D%A1"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/product-interest/#%E9%96%8B%E5%8D%A1">#</a> 開卡</h3>
<p>卡該怎麼寫有一大堆寫法,秉持著開發者的背景,設身處地的想,如果今天是我要開發,我到底要怎麼寫才能保持一些自己決定就好的彈性但又不會不知道該怎麼開發才好。</p>
<p>關於開卡諮詢了很多人的建議,最終暫時保持了這樣的架構 [^7]:</p>
<ul>
<li>Issue Overview: 這次的需求是什麼?為什麼?</li>
<li>Technique Overview: 可能需要的技術或發生的問題點</li>
<li>Priority: 需求的重要性、為什麼?</li>
<li>Solution: 解決方案,包含: user story, UI flow</li>
</ul>
<p>與朋友最多討論的是第二環節 Technique Overview,到底產品規劃需不需要看 code,我自己最終還是保留這個區塊,如果真的來不及就不強求,但時間允許,除了基於自己同時兼顧開發,可以更了解專案的好處外,最重要的還是:這樣決策比較快。</p>
<p>很多時候一個 Issue 進來,如果可以初步排錯到實作的層級,可以先針對排錯的結果提出不同的 Solution 來進行討論,一來可以刺激自己思考不同解決方案的技術限制,二來可以不用一直一來一往的釐清方案,節約工程師的時間。</p>
<h3 id="%E5%B0%8F%E5%B7%A5%E5%85%B7"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/product-interest/#%E5%B0%8F%E5%B7%A5%E5%85%B7">#</a> 小工具</h3>
<p>專案圍繞著 Notion + Google Drive + Trello 展開,在工具選擇上,選擇大家最能接受的、最容易流通在不同部門的就是最好的工具。而個人工作上,所有可以輔助表達的工具都是好夥伴:</p>
<ul>
<li>流程圖 FigJam</li>
<li>截圖 Snippaste</li>
<li>螢幕錄影 giphy / 內建</li>
<li>線上即時繪圖 whiteboardfox</li>
</ul>
<p>另外就是大量的「模板」,舉凡各種:紀錄、回報、公告、開卡、規格 ... etc 等文字全部都模板處理,雖然有想過要不要裝 espanso 這類的服務,但因為實在太多可以客製化的地方,所以最後沒有採用只是複製貼上,簡言之能自動化的地方自動化,並開發一些腳本跟工具輔助,可以節約大量的時間。</p>
<h2 id="%E6%B5%81%E7%A8%8B"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/product-interest/#%E6%B5%81%E7%A8%8B">#</a> 流程</h2>
<p>有了了解有了工具接下來就來想流程,說到流程真的非常憧憬極限編程等等的高端技巧,但是現實總是骨感,知道流程容易導入難。目前不甚成功的案例就有:</p>
<ul>
<li>嘗試從 Trello 轉到 Jira 串 CI/CD 自動化,因為 Jira + Confluence 太複雜適應不良。</li>
<li>嘗試照著 Scrum Guide 跑過所有的 Scrum 會議實際上不敷需求。</li>
<li>嘗試照 Qase 寫測試案例希望可以聘雇工讀生執行 QA,結果反而更難測。</li>
<li>討論了 Trunk Based Flow 結果超級不適合小團隊,改用 Git Flow [^8]。<br />
... etc</li>
</ul>
<p>看起來踩爆所有的雷,但以上的迭代可能都不超過一週,有時可能還沒有發展到工程團隊只在自己身上嘗試後,就在產品會議時討論後移除了,取而代之的是帥帥的做法裡面部分好用的地方與現在的系統去銜接。</p>
<p>當然聽到別人說到自家流程很完整時還是有點羨慕,但流程還是以自己團隊狀況為重,最終以現在最適合的狀態:</p>
<ul>
<li>一次產品會議 [^13]:開始下一輪的 Sprint,會討論 RoadMap、成效、這週接收到的 Issue 與預計的解決方向,並進行這輪流程的檢討。</li>
<li>一次是工程會議:會討論解決方案可行性、預計可以排入這輪 Sprint 的功能,預計的 Test & Official Release 時間。</li>
</ul>
<p>最後就是每日的 Stand Up Meeting。過程中沒有 Review 取而代之的是產品更新公告與公開的 Issue Board 來與其他部門同步產品部門的現況。</p>
<p>透過上述的流程來進行開發。</p>
<h2 id="%E5%9C%98%E9%9A%8A"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/product-interest/#%E5%9C%98%E9%9A%8A">#</a> 團隊</h2>
<p>有了產品的了解、有小工具輔助自己逐步建立了工作流程,接下來就是需要建立團隊、打造文化,簡言之找人進來、讓人願意留下。但文化真的很抽象,關於文化我還無法組織這段過程的改變來敘述。</p>
<p>基本上現在團隊還小相應不複雜,只要能兼顧現在團隊的樣子下、去打造出大家都認同的更好的團隊樣貌,我想就是一個好的文化,這其中也包含大量的個人偏好,我自己很喜歡像是財報狗、Pikabu 這種精英團隊,尤其想要打造出「沒有我也可以自我成長的團隊」[^9] 希望自己可以建立這樣的文化。</p>
<p>於是尋找了大量類似這樣的案例跟找前主管或有主管經驗的人聊聊,也閱讀了一些關於團隊、mentor、coach、scrum 的書,並實際運用在流程中,適合就保留、不適合就移除。</p>
<p>以目前有參與到的了解部門預算、基於預算去規劃團隊組成並實際去寫徵才文、發文 [^10],但怎麼樣才能讓人覺得想要留下可以還待驗證,過程中參考了 GitLab Onboard 設計、尤其參考了前主管大推的經理人之道 [^11],One On One 的習慣、Demo Day、設計或工程的技術交流、溝通的透明公開、基於數據的討論習慣 ... etc。</p>
<p>零零總總的我想文化就是一種習慣,想要大家一起養成的習慣跟默契,繼續朝著這個方向努力。</p>
<h2 id="%E5%B0%8F%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/product-interest/#%E5%B0%8F%E7%B5%90">#</a> 小結</h2>
<p>除此之外還有什麼坑要補,太多了 T皿T</p>
<p>除了上述提到的各種知識內容,還有財會的概念希望可以補足,產品就像是情報商,要讓自己盡可能瘋狂的被各面向的資訊洗臉、並站穩自己心中的錨又維持著彈性做流程優化跟決策。</p>
<p>意外的產品依舊在軌道上運行使用狀況也有了起色,有時在路上看到使用者使用自己的產品,也會覺得「啊、是我們的產品!」看到 FB、客服進線抱怨我們的產品會覺得「嗚嗚嗚、好辣 QQQ」不禁覺得兼顧開發跟產品兩件事是一個不錯而且有趣的嘗試。</p>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/minw/product-interest/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li>[^1]: 原先使用 <a href="https://dbdiagram.io/home">dbdiagram.io</a> 最近改為同一個團隊製作的 <a href="https://dbdocs.io/">dbdocs</a>,可以沿用原本的語法,也可以用他們提供的 CLI 工具從 db 逆輸出 dml 檔案來建立文件,實在好用,只要再補 relation 就可以了。另外 <a href="http://draw.io/">draw.io</a> 有架構圖元件可以使用</li>
<li>[^2]: 為了 UX 文件管理所以使用 <a href="https://www.figma.com/figjam/">FigJAM</a>,原先比較習慣使用 <a href="https://whimsical.com/">Whimsical</a>,但文件就會分散在不同的系統裡所以作罷。</li>
<li>[^3]: 原先在 Google Docs 中,但要將 Issue 跟文件 comment 拆分才有辦法管理,改用 Notion 這個上手比 Confluence 方便很多的工具,方案選擇上可以參考 <a href="https://zhuanlan.zhihu.com/p/65780461">這篇文章</a>,目前使用 Personal 付費版。</li>
<li>[^4]: 指標先是 PO 就有一些用 excel 實作好的指標但需要自動化,這邊提到的數據工作主要是用 python 將資料清理跟運算自動跑腳本,存到資料庫,並實作一些比較複雜的運算。</li>
<li>[^5]: 內部公告的概念是來自於均一 <a href="https://official.junyiacademy.org/blog/po-interview/">這篇文章</a> 的 Product Release Note,可以節約跟其他部門溝通的時間,也能作為 app 更新文案的參考。</li>
<li>[^6]: <a href="https://www.books.com.tw/products/0010712916">博客來-使用者故事對照:User Story Mapping</a></li>
<li>[^7]: Issue 回報的模板有很多,在同樣均一的 <a href="https://official.junyiacademy.org/blog/po-interview/">這篇文章</a> 也有提到,但目前對內外有 PO 作為窗口在轉述,故沒有採用這個複雜的模板供需求方填寫。</li>
<li>[^8]: 當時是因為遇到需要 rollback 的狀況而有了這次討論,討論的決議點在於,我們並沒有能專門顧 Release 的 Release Developer,若遇到 Hotfix 的狀況,可能 Release Developer 會 Cherry Pick 到生氣 XDDD 所以維持經典的 Git Flow,Hotfix Branch 則壓在只有 Official Release 有問題才可以使用。</li>
<li>[^9]: <a href="https://mz026.medium.com/%E7%95%B6%E5%B7%A5%E7%A8%8B%E5%B8%AB%E5%8D%81%E5%B9%B4%E7%9A%84%E5%BF%83%E5%BE%97%E6%84%9F%E6%83%B3-30c1ec688f9f">當工程師十年的心得感想. 從 2010… | by Yang Hsing Lin | Medium</a></li>
<li>[^10]: <a href="https://hulitw.medium.com/my-ideal-job-description-d72ec9cf5d0e">我心目中的理想徵才文. 四年前是我人生中第一次求職,抱持著「之後應該不會海投了」的心態,一口氣面試了快要… | by Huli | Medium</a></li>
<li>[^11]: <a href="https://www.books.com.tw/products/0010890612">博客來-經理人之道 技術領袖航向成長與改變的參考指南</a></li>
<li>[^12]: <a href="https://blog.starrocket.io/posts/how-to-create-smooth-employee-onboarding-process/">你在試用新人,新人也在試用你:五個科技公司提高新人留存率的 onboarding 方法 | Star Rocket Blog</a></li>
<li>[^13]: 小故事:原本保持 Scrum 的說法叫做 Refinement + Retrospective,但講這個面試的時候面試者會一臉不知道你在說什麼,於是這個名字就直接改叫產品會議了,同樣的工程會議原本也叫做 Sprint Planning。</li>
</ul>
render quill delta 的方式
2021-11-07T00:00:00Z
https://blog.errorbaker.tw/posts/ruofan/react-quill/
<!-- summary -->
<p>Hi,大家好! 前陣子專案上接到一個需求,從後台編輯的 text editor,如何在 client 端一致的渲染出後台在 text editor 送出的資料!這篇文章會帶著大家認識如何透過 Quill 實作。</p>
<!-- summary -->
<!-- more -->
<h2 id="%E4%BB%80%E9%BA%BC%E6%98%AF-quill%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/react-quill/#%E4%BB%80%E9%BA%BC%E6%98%AF-quill%EF%BC%9F">#</a> 什麼是 Quill?</h2>
<p>以下為 Quill <a href="https://quilljs.com/">官方文件</a> 上對自己的介紹:</p>
<blockquote>
<p>Quill is a modern rich text editor built for compatibility and extensibility.</p>
</blockquote>
<p>簡單來說,Quill 是一個現代化的文字編輯器。想了解更多的話,推薦看 Quill 的 <a href="https://quilljs.com/">官方文件</a>。</p>
<h2 id="%E7%94%A8-json-%E8%B3%87%E6%96%99%E4%B9%9F%E5%8F%AF%E4%BB%A5%E6%88%90%E5%8A%9F%E6%B8%B2%E6%9F%93%E5%97%8E%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/react-quill/#%E7%94%A8-json-%E8%B3%87%E6%96%99%E4%B9%9F%E5%8F%AF%E4%BB%A5%E6%88%90%E5%8A%9F%E6%B8%B2%E6%9F%93%E5%97%8E%EF%BC%9F">#</a> 用 JSON 資料也可以成功渲染嗎?</h2>
<p>之所以提出這個問題,是因為用 Quill 編輯器產生的內容,拿到的會是 Delta。</p>
<h3 id="%E4%BB%80%E9%BA%BC%E6%98%AF-delta%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/react-quill/#%E4%BB%80%E9%BA%BC%E6%98%AF-delta%EF%BC%9F">#</a> 什麼是 Delta?</h3>
<blockquote>
<p>Deltas are a simple, yet expressive format that can be used to describe Quill’s contents and changes. The format is a strict subset of JSON, is human readable, and easily parsible by machines. Deltas can describe any Quill document, includes all text and formatting information, without the ambiguity and complexity of HTML.</p>
</blockquote>
<p>簡單來說,Delta 將所有描述 html 上的資料簡化成了 JSON 格式,簡化了可讀性。想了解更多的話,推薦看 Quill 的 <a href="https://quilljs.com/docs/delta/">官方文件</a>。</p>
<h2 id="%E9%96%8B%E5%A7%8B%E5%AF%A6%E4%BD%9C%E5%90%A7%EF%BC%81"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/react-quill/#%E9%96%8B%E5%A7%8B%E5%AF%A6%E4%BD%9C%E5%90%A7%EF%BC%81">#</a> 開始實作吧!</h2>
<p>先帶大家看一下第一種實作方法。</p>
<p>透過下方的指令可以快速的安裝 react-quill。</p>
<pre class="language-bash"><code class="language-bash"><span class="token variable">$yarn</span> <span class="token function">add</span> react-quill@beta</code></pre>
<p>接著引用 theme,bubble 這個 style 不會顯示 編輯器的編輯欄,只會顯示內容。</p>
<pre class="language-tsx"><code class="language-tsx"><span class="token keyword">import</span> NextDocument<span class="token punctuation">,</span> <span class="token punctuation">{</span> Html<span class="token punctuation">,</span> Head<span class="token punctuation">,</span> Main<span class="token punctuation">,</span> NextScript <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"next/document"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> ColorModeScript <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"@chakra-ui/react"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">class</span> <span class="token class-name">Document</span> <span class="token keyword">extends</span> <span class="token class-name">NextDocument</span> <span class="token punctuation">{</span><br /> <span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Html</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Head</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span><br /> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span><br /> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://unpkg.com/react-quill@1.3.3/dist/quill.bubble.css<span class="token punctuation">"</span></span><br /> <span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span><span class="token class-name">Head</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>body</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token punctuation">{</span><span class="token comment">/* Make Color mode to persists when you refresh the page. */</span><span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">ColorModeScript</span></span> <span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Main</span></span> <span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">NextScript</span></span> <span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>body</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span><span class="token class-name">Html</span></span><span class="token punctuation">></span></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>接著使用 <a href="https://quilljs.com/">編輯器</a> 拿到 delta 資料。</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br /> <span class="token property">"ops"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"attributes"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"background"</span><span class="token operator">:</span> <span class="token string">"#ffffff"</span><span class="token punctuation">,</span><br /> <span class="token property">"color"</span><span class="token operator">:</span> <span class="token string">"#333333"</span><span class="token punctuation">,</span><br /> <span class="token property">"bold"</span><span class="token operator">:</span> <span class="token boolean">true</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token property">"insert"</span><span class="token operator">:</span> <span class="token string">"拉吉波拉拉村免裝備露營"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"attributes"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"background"</span><span class="token operator">:</span> <span class="token string">"#ffffff"</span><span class="token punctuation">,</span><br /> <span class="token property">"color"</span><span class="token operator">:</span> <span class="token string">"#333333"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token property">"insert"</span><span class="token operator">:</span> <span class="token string">":園區內的星月花園及小溪樂園提供廣闊的草坪及人造溪流。"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"insert"</span><span class="token operator">:</span> <span class="token string">"\n\n"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"insert"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"image"</span><span class="token operator">:</span> <span class="token string">"https://i.ibb.co/6y66vVT/pars-sahin-V7u-P-Xzq-X18-unsplash.jpg"</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"insert"</span><span class="token operator">:</span> <span class="token string">"\n"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"attributes"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"color"</span><span class="token operator">:</span> <span class="token string">"#333333"</span><span class="token punctuation">,</span><br /> <span class="token property">"background"</span><span class="token operator">:</span> <span class="token string">"#ffffff"</span><span class="token punctuation">,</span><br /> <span class="token property">"italic"</span><span class="token operator">:</span> <span class="token boolean">true</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token property">"insert"</span><span class="token operator">:</span> <span class="token string">"(圖片來源:"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"attributes"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"italic"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> <span class="token property">"link"</span><span class="token operator">:</span> <span class="token string">"https://unsplash.com/"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token property">"insert"</span><span class="token operator">:</span> <span class="token string">"https://unsplash.com"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"attributes"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"italic"</span><span class="token operator">:</span> <span class="token boolean">true</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token property">"insert"</span><span class="token operator">:</span> <span class="token string">")"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"insert"</span><span class="token operator">:</span> <span class="token string">"\n\n"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"attributes"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"background"</span><span class="token operator">:</span> <span class="token string">"#ffffff"</span><span class="token punctuation">,</span><br /> <span class="token property">"color"</span><span class="token operator">:</span> <span class="token string">"#333333"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token property">"insert"</span><span class="token operator">:</span> <span class="token string">"離城市不遠,卻能保有山林的寧靜。絕佳地理位置可眺望"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"attributes"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"background"</span><span class="token operator">:</span> <span class="token string">"#ffffcc"</span><span class="token punctuation">,</span><br /> <span class="token property">"color"</span><span class="token operator">:</span> <span class="token string">"#333333"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token property">"insert"</span><span class="token operator">:</span> <span class="token string">"日落黃昏、星空夜景"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"attributes"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"background"</span><span class="token operator">:</span> <span class="token string">"#ffffff"</span><span class="token punctuation">,</span><br /> <span class="token property">"color"</span><span class="token operator">:</span> <span class="token string">"#333333"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token property">"insert"</span><span class="token operator">:</span> <span class="token string">"!"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"insert"</span><span class="token operator">:</span> <span class="token string">"\n\n"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"insert"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"image"</span><span class="token operator">:</span> <span class="token string">"https://i.ibb.co/JjpTqyh/christopher-jolly-gc-Cc-Iy6-Fc-M-unsplash.jpg"</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"insert"</span><span class="token operator">:</span> <span class="token string">"\n"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"attributes"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"color"</span><span class="token operator">:</span> <span class="token string">"#333333"</span><span class="token punctuation">,</span><br /> <span class="token property">"background"</span><span class="token operator">:</span> <span class="token string">"#ffffff"</span><span class="token punctuation">,</span><br /> <span class="token property">"italic"</span><span class="token operator">:</span> <span class="token boolean">true</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token property">"insert"</span><span class="token operator">:</span> <span class="token string">"(圖片來源:"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"attributes"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"italic"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> <span class="token property">"link"</span><span class="token operator">:</span> <span class="token string">"https://unsplash.com/"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token property">"insert"</span><span class="token operator">:</span> <span class="token string">"https://unsplash.com"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"attributes"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"italic"</span><span class="token operator">:</span> <span class="token boolean">true</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token property">"insert"</span><span class="token operator">:</span> <span class="token string">")"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"insert"</span><span class="token operator">:</span> <span class="token string">"\n\n"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"attributes"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"background"</span><span class="token operator">:</span> <span class="token string">"#ffffff"</span><span class="token punctuation">,</span><br /> <span class="token property">"color"</span><span class="token operator">:</span> <span class="token string">"#333333"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token property">"insert"</span><span class="token operator">:</span> <span class="token string">"座落於壯闊的山林裡,享受旅居自然的慢時光 🏕"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"insert"</span><span class="token operator">:</span> <span class="token string">"\n"</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">]</span><br /><span class="token punctuation">}</span></code></pre>
<p>下一步,把內容顯示出來。<br />
在 NextJs 透過 dynamic 的方式引用,把 delta 轉成 html 後,搭配 readOnly 以及 bubble 這個 style 順利的渲染出資料。</p>
<pre class="language-tsx"><code class="language-tsx"><span class="token keyword">import</span> React <span class="token keyword">from</span> <span class="token string">"react"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> dynamic <span class="token keyword">from</span> <span class="token string">"next/dynamic"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> Container <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"@chakra-ui/react"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> QuillDeltaToHtmlConverter <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"quill-delta-to-html"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span>contents<span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./contents'</span><br /><br /><span class="token keyword">const</span> ReactQuill <span class="token operator">=</span> <span class="token function">dynamic</span><span class="token punctuation">(</span><span class="token keyword">import</span><span class="token punctuation">(</span><span class="token string">"react-quill"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br /> ssr<span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br /> <span class="token function-variable function">loading</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span><span class="token plain-text">Loading ...</span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">Product</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> justHtmlContent <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> converter <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">QuillDeltaToHtmlConverter</span><span class="token punctuation">(</span><br /> contents<span class="token punctuation">.</span>ops<span class="token punctuation">,</span><br /> justHtmlContent<br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> html <span class="token operator">=</span> converter<span class="token punctuation">.</span><span class="token function">convert</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Container</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">ReactQuill</span></span> <span class="token attr-name">readOnly</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token boolean">true</span><span class="token punctuation">}</span></span> <span class="token attr-name">theme</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>bubble<span class="token punctuation">"</span></span> <span class="token attr-name">value</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>html<span class="token punctuation">}</span></span> <span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span><span class="token class-name">Container</span></span><span class="token punctuation">></span></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/quill-1.png" alt="" /></p>
<p>從上方的第一種方法,我們可以把拿到的 delta 資料一致的顯示出來,從上方圖片中的 html 我們可以看到 Quill 的 className 也成功地被置入。</p>
<blockquote>
<p>但是... 真的有需要引用整包 react-quill 嗎?</p>
</blockquote>
<p>筆者從這個 <a href="https://github.com/quilljs/quill/issues/993">issue</a> 的對話串中被啟發了!</p>
<p>是的,接下來為大家介紹第二種實作方法。<br />
我們拿到 html 後事實上只需要特別注意 css 如何被正確的渲染。<br />
從剛剛第一個方法,我們可以知道 Quill 透過特別的 className 讓引入的 theme 能夠成功渲染。<br />
這邊我們手動加上 <code>ql-bubble</code> 以及 <code>ql-editor</code> 兩個 className。</p>
<pre class="language-tsx"><code class="language-tsx"><span class="token keyword">import</span> React <span class="token keyword">from</span> <span class="token string">'react'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> Container<span class="token punctuation">,</span> Box <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@chakra-ui/react'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> QuillDeltaToHtmlConverter <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'quill-delta-to-html'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span>contents<span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./contents'</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">Product</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> justHtmlContent <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> converter <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">QuillDeltaToHtmlConverter</span><span class="token punctuation">(</span>contents<span class="token punctuation">.</span>ops<span class="token punctuation">,</span> justHtmlContent<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> html <span class="token operator">=</span> converter<span class="token punctuation">.</span><span class="token function">convert</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Container</span></span> <span class="token attr-name">className</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>ql-bubble<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Box</span></span> <span class="token attr-name">dangerouslySetInnerHTML</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span>__html:</span> <span class="token attr-name">html</span> <span class="token attr-name">className</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>ql-editor<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span><span class="token class-name">Box</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span><span class="token class-name">Container</span></span><span class="token punctuation">></span></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /></code></pre>
<p>一樣可以把拿到的 delta 資料一致的顯示出來,並且不需要引用整包 react-quill,只需要引用 Quill 提供的 theme。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/quill-1.png" alt="" /></p>
<h2 id="%E5%B0%8F%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/react-quill/#%E5%B0%8F%E7%B5%90">#</a> 小結</h2>
<p>在實作過程中反覆審視是否是目前的最佳作法,整體來說蠻有趣的!<br />
在閱讀文章時如果有遇到什麼問題,或是有什麼建議,都歡迎留言告訴我,謝謝。😃</p>
<ul>
<li><a href="https://github.com/ruofanwei/next-app/tree/feature/quill-delta">Github | Repo: Next-app</a></li>
</ul>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/react-quill/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li><a href="https://quilljs.com/">Documentation | Quill</a></li>
<li><a href="https://github.com/quilljs/quill/issues/993">Issue | Render quill delta without instantiating an editor</a></li>
<li><a href="https://stackoverflow.com/questions/69726804/how-to-import-css-from-react-quill">StackOverflow | How to import css from React Quill?</a></li>
</ul>
如何使用 Web Component 技術來製作元件
2021-11-07T00:00:00Z
https://blog.errorbaker.tw/posts/xiang/build-webcomponent-element/
<!-- summary -->
<p>本文要來介紹 LitElement 這個套件</p>
<!-- summary -->
<!-- more -->
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-element/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<p>先前有寫過一篇關於「<a href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-to-table/">用 Web Component 製作客製化表格元件</a>」的文章,當中有介紹了很多 web component 的基本概念,以及製作表格的方向。<br />
這篇文章要來沿用相同的基礎,把其他各式各樣常見的元件,利用相同的技術實作出來。</p>
<h2 id="%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A0%85"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-element/#%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A0%85">#</a> 注意事項</h2>
<p>Design System 使用 <a href="https://lit-element.polymer-project.org/guide">LitElement</a> 套件來實作元件,由於此套件利用 ES6 語法撰寫,要注意引入專案的執行的環境,以及打包流程要能夠支援 ES6 語法。</p>
<h2 id="web-component-%E7%B0%A1%E4%BB%8B"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-element/#web-component-%E7%B0%A1%E4%BB%8B">#</a> Web Component 簡介</h2>
<p>web component 可以讓我們自定義元件,包含 HTML 結構、CSS 樣式、JavaScript,並且取一個自己喜歡的標籤名稱(例如:<code><wc-button></wc-button</code>),插入到頁面上就能得到一個封裝好的組件。</p>
<p>由於 Web Component 是利用 Browser 原生支援的 Custom Elements 來渲染共用元件的,所以能共用在任何前端框架上,非常符合我們對於元件庫的需求。它的概念其實跟 React 的 class component 有幾分類似,都能讓我們自定義一個元件的架構跟樣式,並使用在其他地方,只是它還有一些需要另外去熟悉的特性,例如 shadow DOM 的概念。</p>
<p>shadow DOM 允許我們創建一些完全獨立於其他元素的 sub-DOM trees,什麼意思呢?有點像我們組裝模型一樣,有一個可以組裝用的接口,讓我們裝上別的手臂或武器之類的,而且裝上去的部位跟模型本身不會相互影響。</p>
<p>可以參考下圖:我們可以利用 shadow-host 這個節點,裝上一個 shadow-tree</p>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/shadow-tree.png" alt="" /></p>
<p>(圖片來源: <a href="https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_shadow_DOM">https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_shadow_DOM</a>)</p>
<p>Shadow DOM 的操作方式跟一般我們常操作的 DOM 是相同的,可以新增屬性、增加 child node...等等,但是我們沒辦法直接透過外部來修改 Shadow DOM 底下的元件。</p>
<p>比如說下面這個 HTML 的架構:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>.wrapper<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>wc-element</span><span class="token punctuation">></span></span><br /> #shadow-root<br /> <span class="token comment"><!----></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>wc-btn<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>...<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span><br /> <span class="token comment"><!----></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>wc-element</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>如果我們想要透過外部的 <code>.wrapper</code> 來調整底下 <code>.wc-btn</code> 的顏色是無法調整的:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">.wrapper .wc-btn</span> <span class="token punctuation">{</span><br /> <span class="token property">color</span><span class="token punctuation">:</span> red<span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>相反的 Shadow DOM 内部的元素也不會影響到外部。</p>
<p>由於這個特性,使得我們可以封裝一個具有獨立功能的 <code><wc-table></code> 元件,並且可以保證不會引用到專案的同時影響到其它 DOM 元素。shadow DOM 和標準的 DOM 一樣,可以設置它的樣式,也可以用 JavaScript 操作它的行為。DOM 和 shadow DOM 創建的獨立組件之間的互不干擾,有利於組件在各個專案的復用。</p>
<h2 id="%E5%85%83%E4%BB%B6%E7%9A%84%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-element/#%E5%85%83%E4%BB%B6%E7%9A%84%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5">#</a> 元件的基本概念</h2>
<p>要來實作元件以前,首先要先了解元件的基礎要素有哪些:</p>
<ul>
<li>Templates:定義 html 的架構</li>
<li>Styles:定義 css 的內容</li>
<li>Properties:定義外部傳進來的參數</li>
<li>Events:定義元件的行為事件</li>
<li>Lifecycle:定義元件的生命週期</li>
</ul>
<h2 id="templates"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-element/#templates">#</a> Templates</h2>
<p>定義 template,只需要透過簡單的 render function,在裡頭 return 一個 html 模板,如下面這段程式碼可以讓我們定義好一個 button 元件。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> <span class="token punctuation">{</span> LitElement<span class="token punctuation">,</span> html <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'lit-element'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">class</span> <span class="token class-name">MyButton</span> <span class="token keyword">extends</span> <span class="token class-name">LitElement</span> <span class="token punctuation">{</span><br /> <span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><button>按鈕</button></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>我們可以利用 <code>customElements.define</code> 來定義這個元件的標籤名稱,<br />
例如說我們如果希望這個元件叫做 <code>wc-button</code>:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// 我們可以透過兩種方式來定義:</span><br /><span class="token number">1.</span> customElements<span class="token punctuation">.</span><span class="token function">define</span><span class="token punctuation">(</span><span class="token string">'wc-element'</span><span class="token punctuation">,</span> MyButton<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token number">2.</span> @<span class="token function">customElement</span><span class="token punctuation">(</span><span class="token string">'wc-button'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>我們也可以預先定義好變數,在 template 中帶入:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> <span class="token punctuation">{</span> LitElement<span class="token punctuation">,</span> html <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'lit-element'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">class</span> <span class="token class-name">MyButton</span> <span class="token keyword">extends</span> <span class="token class-name">LitElement</span> <span class="token punctuation">{</span><br /> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">super</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>content <span class="token operator">=</span> <span class="token string">'按鈕'</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><button></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token keyword">this</span><span class="token punctuation">.</span>content<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"></button></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<h4 id="%E7%89%B9%E6%AE%8A%E6%83%85%E5%A2%83"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-element/#%E7%89%B9%E6%AE%8A%E6%83%85%E5%A2%83">#</a> 特殊情境</h4>
<p>☞ 條件渲染<br />
如果遇到需要依照條件來決定 html 結構的情況,我們可以利用「三元運算子」來實作:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">class</span> <span class="token class-name">MyButton</span> <span class="token keyword">extends</span> <span class="token class-name">LitElement</span> <span class="token punctuation">{</span><br /> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">super</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>type <span class="token operator">=</span> <span class="token string">'primary'</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> <div><br /> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token keyword">this</span><span class="token punctuation">.</span>type <span class="token operator">===</span> <span class="token string">'primary'</span> <span class="token operator">?</span><br /> html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><button>primary</button></span><span class="token template-punctuation string">`</span></span> <span class="token operator">:</span><br /> html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><button>secondary</button></span><span class="token template-punctuation string">`</span></span><br /> <span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"><br /> </div><br /> </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>☞ 迴圈渲染<br />
如果遇到多筆資料我們可以透過 <code>map</code> 來實作迴圈渲染:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">class</span> <span class="token class-name">MyButton</span> <span class="token keyword">extends</span> <span class="token class-name">LitElement</span> <span class="token punctuation">{</span><br /> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">super</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>myArray <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">'primary'</span><span class="token punctuation">,</span> <span class="token string">'secondary'</span><span class="token punctuation">,</span> <span class="token string">'active'</span><span class="token punctuation">,</span> <span class="token string">'error'</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> <div><br /> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span> <span class="token keyword">this</span><span class="token punctuation">.</span>myArray<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token parameter">text</span> <span class="token operator">=></span> html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><button></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>text<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"></button></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span> <span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"><br /> </div><br /> </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<h2 id="styles"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-element/#styles">#</a> Styles</h2>
<p>style 是我們用來定義元件 css 的地方,我們可以透過 <code>get styles()</code> 的方式,在裡頭 return 一個 css 模板:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> <span class="token punctuation">{</span> LitElement<span class="token punctuation">,</span> css<span class="token punctuation">,</span> html <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'lit-element'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">class</span> <span class="token class-name">MyElement</span> <span class="token keyword">extends</span> <span class="token class-name">LitElement</span> <span class="token punctuation">{</span><br /> <span class="token keyword">static</span> <span class="token keyword">get</span> <span class="token function">styles</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> css<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> div { color: red; }<br /> </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> <div>I'm styled!</div><br /> </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>我們如果有多組的 css 設定,也可以利用陣列將多個 css 模板給包起來:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">static</span> <span class="token keyword">get</span> <span class="token function">styles</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token punctuation">[</span> css<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">...</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> css<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">...</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">]</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>如果需要事先定義變數,在 <code>get styles()</code> 裡面帶入,有兩種方式可以實作:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// 第一種方式:</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> LitElement<span class="token punctuation">,</span> html<span class="token punctuation">,</span> css <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'lit-element'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> mainColor <span class="token operator">=</span> css<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">red</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /><br /><span class="token keyword">class</span> <span class="token class-name">MyElement</span> <span class="token keyword">extends</span> <span class="token class-name">LitElement</span> <span class="token punctuation">{</span><br /> <span class="token keyword">static</span> <span class="token keyword">get</span> <span class="token function">styles</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> css<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> div { color: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>mainColor<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> }<br /> </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><div>Some content in a div</div></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><br /><span class="token comment">// 第二種方式:</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> LitElement<span class="token punctuation">,</span> html<span class="token punctuation">,</span> css<span class="token punctuation">,</span> unsafeCSS <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'lit-element'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">class</span> <span class="token class-name">MyElement</span> <span class="token keyword">extends</span> <span class="token class-name">LitElement</span> <span class="token punctuation">{</span><br /> <span class="token keyword">static</span> <span class="token keyword">get</span> <span class="token function">styles</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> mainColor <span class="token operator">=</span> <span class="token string">'red'</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">return</span> css<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> div { color: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">unsafeCSS</span><span class="token punctuation">(</span>mainColor<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> }<br /> </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><div>Some content in a div</div></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><br />customElements<span class="token punctuation">.</span><span class="token function">define</span><span class="token punctuation">(</span><span class="token string">'my-element'</span><span class="token punctuation">,</span> MyElement<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>如果我們要針對當下這個元件的樣式做設定,我們需要透過 <code>host:</code> 來定義</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">static</span> <span class="token keyword">get</span> <span class="token function">styles</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> css<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> /* Selects the host element */<br /> :host {<br /> display: block;<br /> }<br /><br /> /* Selects the host element if it is hidden */<br /> :host([hidden]) {<br /> display: none;<br /> }<br /> </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>如果要針對當下這個元件的子元素做定義,我們可以透過 <code>:slotted</code> 來定義</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> <span class="token punctuation">{</span> LitElement<span class="token punctuation">,</span> html<span class="token punctuation">,</span> css <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'lit-element'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">class</span> <span class="token class-name">MyElement</span> <span class="token keyword">extends</span> <span class="token class-name">LitElement</span> <span class="token punctuation">{</span><br /> <span class="token keyword">static</span> <span class="token keyword">get</span> <span class="token function">styles</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> css<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> ::slotted(*) { font-family: Roboto; }<br /> ::slotted(p) { color: blue; }<br /> div ::slotted(*) { color: red; }<br /> </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> <slot></slot><br /> <div><slot name="hi"></slot></div><br /> </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /></code></pre>
<h4 id="%E7%89%B9%E6%AE%8A%E6%83%85%E5%A2%83-2"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-element/#%E7%89%B9%E6%AE%8A%E6%83%85%E5%A2%83-2">#</a> 特殊情境</h4>
<p>☞ 主題色設定<br />
像 Design-system 會需要 follow 設計稿的主題色,所以我們可以透過定義主題色的方式,方便我們在 css 引入對應的色號</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> color <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">lightPrimary</span><span class="token operator">:</span> <span class="token string">'#4066ff'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">lightPrimaryDarkerPhase1</span><span class="token operator">:</span> <span class="token string">'#395be5'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">lightPrimaryDarkerPhase2</span><span class="token operator">:</span> <span class="token string">'#3351cc'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">lightPrimaryLighterPhase1</span><span class="token operator">:</span> <span class="token string">'#7994ff'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">lightPrimaryLighterPhase2</span><span class="token operator">:</span> <span class="token string">'#d8e0ff'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">lightPrimaryLighterPhase3</span><span class="token operator">:</span> <span class="token string">'#ebefff'</span><span class="token punctuation">,</span><br /> <span class="token punctuation">.</span><br /> <span class="token punctuation">.</span><br /> <span class="token punctuation">.</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">static</span> <span class="token keyword">get</span> <span class="token function">styles</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token punctuation">[</span><br /> css<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> :host {<br /> --light-primary: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">unsafeCSS</span><span class="token punctuation">(</span>color<span class="token punctuation">.</span>lightPrimary<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">;<br /> --light-primary-darker-phase-1: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">unsafeCSS</span><span class="token punctuation">(</span>color<span class="token punctuation">.</span>lightPrimaryDarkerPhase1<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">;<br /> --light-primary-darker-phase-2: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">unsafeCSS</span><span class="token punctuation">(</span>color<span class="token punctuation">.</span>lightPrimaryDarkerPhase2<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">;<br /> --light-primary-lighter-phase-1: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">unsafeCSS</span><span class="token punctuation">(</span>color<span class="token punctuation">.</span>lightPrimaryLighterPhase1<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">;<br /> --light-primary-lighter-phase-2: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">unsafeCSS</span><span class="token punctuation">(</span>color<span class="token punctuation">.</span>lightPrimaryLighterPhase2<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">;<br /> --light-primary-lighter-phase-3: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">unsafeCSS</span><span class="token punctuation">(</span>color<span class="token punctuation">.</span>lightPrimaryLighterPhase3<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">;<br /> .<br /> .<br /> .<br /> }<br /> </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br /> css<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> :host {<br /> --button-background_color: var(--light-primary);<br /> --button-background_color-disable: var(<br /> --light-primary-lighter-phase-1<br /> );<br /> --button-background_color-hover: var(--light-primary-darker-phase-1);<br /> --button-background_color-active: var(--light-primary-darker-phase-2);<br /> --button-text_color: var(--white);<br /> --button-text_color-disable: var(--white);<br /> --button-text_color-hover: var(--white);<br /> --button-text_color-active: var(--white);<br /> --button-border_color: var(--light-primary);<br /> --button-border_color-disable: var(<br /> --light-primary-lighter-phase-1<br /> );<br /> --button-border_color-hover: var(--light-primary-darker-phase-1);<br /> --button-border_color-active: var(--light-primary-darker-phase-2);<br /> }<br /> </span><span class="token template-punctuation string">`</span></span><br /> <span class="token punctuation">]</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /></code></pre>
<h2 id="properties"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-element/#properties">#</a> Properties</h2>
<p>要定義元件的 properties,我們可以透過這個寫法:</p>
<pre class="language-js"><code class="language-js">@<span class="token function">property</span><span class="token punctuation">(</span>options<span class="token punctuation">)</span><br />propertyName<span class="token punctuation">;</span></code></pre>
<p>options 當中可以帶入以下幾種參數:</p>
<ul>
<li>type:定義此 property 的型態</li>
<li>reflect:定義屬性是否會反映回關聯的屬性,預設為 <code>false</code>(當我們需要利用 attributeChangedCallback 來做監控,我們就可以將其設定為 <code>true</code>)</li>
<li>hasChanged:可以自定義屬性更新的條件,預設為 <code>(newValue !== oldValue)</code></li>
</ul>
<pre class="language-js"><code class="language-js">@<span class="token function">property</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> String<span class="token punctuation">,</span> <span class="token literal-property property">reflect</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /><span class="token keyword">public</span> value <span class="token operator">=</span> <span class="token string">''</span><span class="token punctuation">;</span></code></pre>
<p>當 property 更新時,觸發的更新機制:</p>
<ol>
<li>property setter 會被呼叫</li>
<li>setter 會去呼叫 hasChanged 函式判斷目前的變更是否滿足更新的條件,如果回傳的 true 則進入下一步</li>
<li>setter 會去將 requestUpdate 列入排程</li>
<li>元件的 update 會被呼叫,並將 template re-render</li>
</ol>
<p>詳細資訊可參考 <a href="https://lit-element.polymer-project.org/guide/lifecycle">文件</a> 描述</p>
<p>我們也可以自定義 getter 跟 setter:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">private</span> _disabled <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">get</span> <span class="token function">disabled</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>_disabled<span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><br /><span class="token comment">/**<br /> * This attribute prevents the user from interacting with the input: it cannot be pressed or focused.<br /> * @type {Boolean}<br /> * @default false<br /> */</span><br />@<span class="token function">property</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> Boolean<span class="token punctuation">,</span> <span class="token literal-property property">reflect</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /><span class="token keyword">set</span> <span class="token function">disabled</span><span class="token punctuation">(</span><span class="token parameter"><span class="token literal-property property">disabled</span><span class="token operator">:</span> boolean</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> oldValue <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>_disabled<span class="token punctuation">;</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token operator">!</span>disabled <span class="token operator">===</span> <span class="token operator">!</span><span class="token operator">!</span>oldValue<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>_disabled <span class="token operator">=</span> disabled<span class="token punctuation">;</span><br /><br /> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">requestUpdate</span><span class="token punctuation">(</span><span class="token string">'disabled'</span><span class="token punctuation">,</span> oldValue<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">dispatchEvent</span><span class="token punctuation">(</span><br /> <span class="token keyword">new</span> <span class="token class-name">Event</span><span class="token punctuation">(</span><span class="token string">'disabled'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">bubbles</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> <span class="token literal-property property">composed</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /></code></pre>
<h2 id="events"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-element/#events">#</a> Events</h2>
<p>加上事件監聽的幾個方式:</p>
<p>☞ 加在 element 上</p>
<pre class="language-js"><code class="language-js"><span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><button @click="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token keyword">this</span><span class="token punctuation">.</span>_handleClick<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>☞ 加在 constructor</p>
<pre class="language-js"><code class="language-js"><span class="token function">constructor</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">super</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'focus'</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>_handleFocus<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>☞ 加在 connectedCallback</p>
<pre class="language-js"><code class="language-js"><span class="token function">connectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">connectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> window<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'resize'</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>_handleResize<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><span class="token function">disconnectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> window<span class="token punctuation">.</span><span class="token function">removeEventListener</span><span class="token punctuation">(</span><span class="token string">'resize'</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>_handleResize<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">disconnectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>☞ 加在 firstUpdated</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">async</span> <span class="token function">firstUpdated</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// Give the browser a chance to paint</span><br /> <span class="token keyword">await</span> <span class="token keyword">new</span> <span class="token class-name">Promise</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">r</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">setTimeout</span><span class="token punctuation">(</span>r<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'click'</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>_handleClick<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>建立 callback function(這邊以建立 handleResize 為例)</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// 第一種方式:直接宣告在 class 當中</span><br /><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">MyElement</span> <span class="token keyword">extends</span> <span class="token class-name">LitElement</span> <span class="token punctuation">{</span><br /> <span class="token keyword">private</span> <span class="token function-variable function">_handleResize</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token comment">/* handle the event */</span> <span class="token punctuation">}</span><br /><br /> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> window<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'resize'</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>_handleResize<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><br /><span class="token comment">// 第二種方式:透過 eventOption</span><br /><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">MyElement</span> <span class="token keyword">extends</span> <span class="token class-name">LitElement</span> <span class="token punctuation">{</span><br /> @<span class="token function">eventOptions</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token literal-property property">passive</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token keyword">private</span> <span class="token function">_handleTouchStart</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token operator">...</span> <span class="token punctuation">}</span><br /><br /> <span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> <div @touchstart=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token keyword">this</span><span class="token punctuation">.</span>_handleTouchStart<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">><div><br /> </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<h2 id="lifecycle"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-element/#lifecycle">#</a> Lifecycle</h2>
<p>生命週期的幾個 hook:</p>
<p>Web Component 內建:</p>
<ul>
<li>connectedCallback: 當元件被建立於 DOM tree 的時候呼叫</li>
<li>disconnectedCallback: 當元件從 DOM tree 移除時呼叫</li>
<li>adoptedCallback: 當元件被移至別的 document 時被呼叫</li>
<li>attributeChangedCallback: 當元件的 attribute 有更新時呼叫</li>
</ul>
<p>LitElement 提供:</p>
<ul>
<li>someProperty.hasChanged</li>
<li>requestUpdate</li>
<li>shouldUpdate</li>
<li>update</li>
<li>firstUpdated</li>
<li>updated</li>
<li>updateComplete</li>
</ul>
<p>☞ someProperty.hasChanged<br />
所有 property 都有自己的 hasChanged,用來定義數值改變時是否要呼叫 update</p>
<p>☞ requestUpdate<br />
當 hasChanged 回傳 true 的時候,requestUpdate 會被觸發,我們也可以手動自行呼叫 requestUpdate 來使元件重新渲染</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">set</span> <span class="token function">prop</span><span class="token punctuation">(</span><span class="token parameter">val</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">let</span> oldVal <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>_prop<span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>_prop <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">floor</span><span class="token punctuation">(</span>val<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">requestUpdate</span><span class="token punctuation">(</span><span class="token string">'prop'</span><span class="token punctuation">,</span> oldVal<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /></code></pre>
<p>☞ shouldUpdate<br />
在 requestUpdate 觸發以後,控制是否要繼續執行更新,預設會回傳 true,有特殊條件需要判斷時我們可以使用</p>
<pre class="language-js"><code class="language-js"><span class="token function">shouldUpdate</span><span class="token punctuation">(</span><span class="token parameter">changedProperties</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> changedProperties<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">oldValue<span class="token punctuation">,</span> propName</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>propName<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> changed. oldValue: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>oldValue<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> changedProperties<span class="token punctuation">.</span><span class="token function">has</span><span class="token punctuation">(</span><span class="token string">'prop1'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>☞ update<br />
用於將屬性值反映到屬性,以及呼叫 render,一般來說我們不需要自定義一個新的 update 來覆蓋掉預設的。</p>
<p>☞ firstUpdated<br />
於元件 mounted 時觸發,可以用來做元件初次建立時的初始化設定,也可以在這邊加上事件監聽。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">public</span> <span class="token function">firstUpdated</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// if (!this.hasAttribute('role')) {</span><br /> <span class="token comment">// this.setAttribute('role', 'textbox');</span><br /> <span class="token comment">// }</span><br /> <span class="token comment">// if (!this.inputEl.hasAttribute('tabindex')) {</span><br /> <span class="token comment">// this.inputEl.setAttribute('tabindex', '0');</span><br /> <span class="token comment">// }</span><br /><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>inputEl<span class="token punctuation">.</span>style<span class="token punctuation">.</span>minHeight <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>minHeight <span class="token operator">+</span> <span class="token string">'px'</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>_initHeight <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>inputEl<span class="token punctuation">.</span>style<span class="token punctuation">.</span>height<span class="token punctuation">;</span><br /><br /> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'click'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">this</span><span class="token punctuation">.</span>inputEl<span class="token punctuation">.</span><span class="token function">focus</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>☞ updated<br />
元件更新完成後觸發,當我們需要在元件更新完成後做事情可以使用</p>
<pre class="language-js"><code class="language-js"><span class="token function">updated</span><span class="token punctuation">(</span><span class="token parameter">changedProperties</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> changedProperties<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">oldValue<span class="token punctuation">,</span> propName</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>propName<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> changed. oldValue: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>oldValue<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">let</span> b <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>shadowRoot<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">'b'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> b<span class="token punctuation">.</span><span class="token function">focus</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>☞ updateComplete<br />
會回傳 true / false 來表示 update 的流程是否通通結束。</p>
<h2 id="%E5%AF%A6%E9%9A%9B%E7%AF%84%E4%BE%8B"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-element/#%E5%AF%A6%E9%9A%9B%E7%AF%84%E4%BE%8B">#</a> 實際範例</h2>
<p>我們以一個 Button 作為範例:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> <span class="token punctuation">{</span> LitElement<span class="token punctuation">,</span> html<span class="token punctuation">,</span> customElement<span class="token punctuation">,</span> property<span class="token punctuation">,</span> css <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'lit-element'</span><span class="token punctuation">;</span><br /><br />@<span class="token function">customElement</span><span class="token punctuation">(</span><span class="token string">'wc-button'</span><span class="token punctuation">)</span><br /><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">Button</span> <span class="token keyword">extends</span> <span class="token class-name">LitElement</span> <span class="token punctuation">{</span><br /><br /> @<span class="token function">property</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> Boolean <span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token keyword">public</span> disabled <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span><br /><br /> @<span class="token function">property</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> String <span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token keyword">public</span> color <span class="token operator">=</span> <span class="token string">'primary'</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">static</span> <span class="token keyword">get</span> <span class="token function">styles</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token punctuation">[</span><br /> baseCss<span class="token punctuation">,</span><br /> css<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> :host {<br /> --button-background_color: var(--light-primary);<br /> --button-background_color-disable: var(<br /> --light-primary-lighter-phase-1<br /> }<br /> .<br /> .<br /> .<br /> </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br /> <span class="token punctuation">]</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">public</span> <span class="token function-variable function">handleClick</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token literal-property property">event</span><span class="token operator">:</span> Event</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>disabled<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">const</span> form <span class="token operator">=</span> <span class="token keyword">this</span><span class="token operator">?.</span><span class="token function">closest</span><span class="token punctuation">(</span><span class="token string">'form'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>form<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> event<span class="token punctuation">.</span><span class="token function">preventDefault</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> fakeSubmit <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'button'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> fakeSubmit<span class="token punctuation">.</span>type <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>type<span class="token punctuation">;</span><br /> fakeSubmit<span class="token punctuation">.</span>style<span class="token punctuation">.</span>display <span class="token operator">=</span> <span class="token string">'none'</span><span class="token punctuation">;</span><br /> form<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>fakeSubmit<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> fakeSubmit<span class="token punctuation">.</span><span class="token function">click</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> fakeSubmit<span class="token punctuation">.</span><span class="token function">remove</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">public</span> <span class="token function">connectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">connectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'click'</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>handleClick<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">public</span> <span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br /> <slot></slot><br /> </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /></code></pre>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/build-webcomponent-element/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li><a href="https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_shadow_DOM">https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_shadow_DOM</a></li>
<li><a href="https://the-allstars.com/blog/website-information/what-is-web-components-why-is-it-so-important.html">https://the-allstars.com/blog/website-information/what-is-web-components-why-is-it-so-important.html</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_templates_and_slots">https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_templates_and_slots</a></li>
<li><a href="https://vaadin.com/components/vaadin-grid/html-examples/grid-basic-demos">https://vaadin.com/components/vaadin-grid/html-examples/grid-basic-demos</a></li>
<li><a href="https://lit-element.polymer-project.org/guide">https://lit-element.polymer-project.org/guide</a></li>
</ul>
介紹 WeakMap
2021-11-12T00:00:00Z
https://blog.errorbaker.tw/posts/umer/weak-map/
<!-- summary -->
<!-- 本文介紹ES6 新增的物件-- WeakMap object -->
<!-- summary -->
<!-- more -->
<h2 id="%E4%BB%80%E9%BA%BC%E6%98%AF-weakmap-object"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/weak-map/#%E4%BB%80%E9%BA%BC%E6%98%AF-weakmap-object">#</a> 什麼是 WeakMap object</h2>
<p>WeakMap object 是一個 ES6 新增的物件,是一個由 key/value 組成的物件,其中 key 屬於<strong>弱引用(weak reference)</strong>,並且必須是物件形式(object)。</p>
<h2 id="%E5%BC%B1%E5%BC%95%E7%94%A8"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/weak-map/#%E5%BC%B1%E5%BC%95%E7%94%A8">#</a> 弱引用</h2>
<p>弱引用在程式設計中是用來做垃圾回收機制(釋放記憶體空間)用的,一個對象若只被弱引用所引用,則被認為是不可訪問(或弱可訪問)的,並因此可能在任何時刻被回收。</p>
<p>JavaScript 本身具有自動的垃圾回收機制,但在某些情況下還是會發生記憶體洩漏(memory leak)的問題。</p>
<p>當程式不再使用到記憶體,卻沒有將該記憶體清空,就會發生記憶體洩漏。當不被用到的記憶體越來越多,就會出現程式變慢、卡頓、高延遲之類的問題,嚴重的話程式就會崩潰。</p>
<p>一些記憶體洩漏的情況有以下:</p>
<ol>
<li>
<p>使用全域變數,但在用完後沒有設置為 null</p>
<p>JavaScript 的全域變數是由根節點(根據執行環境不同,可能是 window 或 global)引用的,因此它們在應用程式的整個生命週期中都不會被垃圾回收,除非有再設置為 null。在建立太多全域變數或是用全域變數來儲存大量資料......等情況,就會發生記憶體洩漏。</p>
</li>
<li>
<p>閉包<br />
在函式中回傳另一個函式,</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">parent</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span><br /> <span class="token keyword">var</span> count <span class="token operator">=</span> <span class="token number">0</span><br /> <span class="token keyword">return</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'count'</span><span class="token operator">+</span>count<span class="token punctuation">)</span><span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>在執行 <code>parent()</code> 之後,因為還有另一個函式在使用<code>count</code>,所以記憶體不會被清空。</p>
</li>
<li>
<p>Event listeners<br />
使用了 Event listener 但沒有把它移除掉,使用到的 callback 不會被回收記憶體</p>
</li>
<li>
<p>其他情況</p>
<p>使用到 <code>setTimeout</code>, <code>setInterval</code>,或是 Cache 沒有定期清理,DOM 元素被移除,多處引用......等,也有可能發生記憶體洩漏。</p>
<p>具體例子可以參考這篇文章,<a href="https://www.ditdot.hr/en/causes-of-memory-leaks-in-javascript-and-how-to-avoid-them#timers">Causes of Memory Leaks in JavaScript and How to Avoid Them</a>。</p>
</li>
</ol>
<h2 id="weakmap-%E7%9A%84%E7%94%A8%E6%B3%95"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/weak-map/#weakmap-%E7%9A%84%E7%94%A8%E6%B3%95">#</a> WeakMap 的用法</h2>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> wm <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">WeakMap</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br />wm<span class="token punctuation">.</span><span class="token function">delete</span><span class="token punctuation">(</span>key<span class="token punctuation">)</span><br /><span class="token comment">// 移除 key 的關聯對象(value)</span><br /><br />wm<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span>key<span class="token punctuation">)</span><br /><span class="token comment">// 取得 key 的關聯對象(value)</span><br /><br />wm<span class="token punctuation">.</span><span class="token function">has</span><span class="token punctuation">(</span>key<span class="token punctuation">)</span><br /><span class="token comment">// 返回 key 是否有關聯對象(value)的 boolean</span><br /><br />wm<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span>key<span class="token punctuation">,</span> value<span class="token punctuation">)</span><br /><span class="token comment">// 設置 key 的關聯對象(value)並返回 key/value</span><br /></code></pre>
<h2 id="map-vs-weakmap"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/weak-map/#map-vs-weakmap">#</a> Map VS WeakMap</h2>
<p>Map 是 ES6 新加入的資料結構,大體來說跟 Object 很像,但多了一些內建功能。Map 對於物件的引用是<strong>強引用</strong>,即使物件設定了 null 並且沒有其他引用存在時,也不會被JavaScript 的垃圾回收機制做記憶體回收,造成記憶體洩漏。</p>
<p>相反的,如果是使用 WeakMap,因為 WeakMap 的引用是<strong>弱引用</strong>,被它引用的物件在設定了 null 並且沒有其他引用存在時,就可以在某一時刻被JavaScript 的垃圾回收機制做記憶體回收。</p>
<p>簡單的重現兩者的差別,這邊直接用開發者工具示範,更嚴謹的方法可以使用 nodeJS 的 <code>node --expose-gc</code> 參數來偵測記憶體的使用狀況。</p>
<p><img src="https://i.imgur.com/xr6ZXzR.png" alt="" /></p>
<p>從圖片結果中可以看到,<code>Map()</code> 引用的物件在設定為 null 之後,也沒有被回收記憶體, <code>WeakMap()</code> 引用的物件在設定為 null 之後,則有被回收記憶體。</p>
<h2 id="%E6%9C%89%E5%92%8C%E6%B2%92%E6%9C%89-weakmap-%E7%9A%84%E5%B7%AE%E5%88%A5"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/weak-map/#%E6%9C%89%E5%92%8C%E6%B2%92%E6%9C%89-weakmap-%E7%9A%84%E5%B7%AE%E5%88%A5">#</a> 有和沒有 WeakMap 的差別</h2>
<p>如同前面所說,WeakMap 方便用來減少記憶體的洩漏,或者是不想每次都手動(設定為 null)回收記憶體的時候,就可以透過 <code>WeakMap.prototype.delete(key)</code>或是把被它引用的物件設定為 null 並且沒有其他引用存在時,讓該物件被自動回收。</p>
<p><a href="https://262.ecma-international.org/6.0/#sec-weakmap-objects">節錄 ECMAScript6 的說明</a></p>
<blockquote>
<p>WeakMap and WeakSets are intended to provide mechanisms for dynamically associating state with an object in a manner that does not “leak” memory resources...</p>
</blockquote>
<p>使用場合之一,</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">let</span> wm <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">WeakMap</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br />wm<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">".title"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">click</span><span class="token operator">:</span> <span class="token number">0</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br />document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">".title"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><br /> <span class="token string">"click"</span><span class="token punctuation">,</span><br /> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">let</span> data <span class="token operator">=</span> myWeakmap<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">".title"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> data<span class="token punctuation">.</span>click<span class="token operator">++</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br />document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">"logo"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">remove</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>上面例子的 DOM 節點作為 WeakMap 的 key,在這個節點被移除的時候,所對應的 value (<code>{ click: 0 }</code>)也會被清除。</p>
<p>如果上面的例子沒有使用 WeakMap 的話,並且忘記手動做<code>logoData = null</code> 的動作的話,記憶體就不會被回收,如下。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">let</span> data <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token literal-property property">click</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br />document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">".title"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><br /> <span class="token string">"click"</span><span class="token punctuation">,</span><br /> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> data<span class="token punctuation">.</span>click<span class="token operator">++</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br />document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">".title"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">remove</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<h2 id="%E7%B8%BD%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/weak-map/#%E7%B8%BD%E7%B5%90">#</a> 總結</h2>
<p>WeakMap 適合在映射(mapping) key(object) 和 value 並且這個 object 會在未來某一時間消失的時候使用,讓被使用到的記憶體可以自動回收而不會洩漏。</p>
<p>需要注意的是,WeakMap 弱引用的只是 key(object),而不是 value,value 依然是正常引用。<br />
<img src="https://i.imgur.com/GJvGLm9.png" alt="" /></p>
<h2 id="%E7%9B%B8%E9%97%9C%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/umer/weak-map/#%E7%9B%B8%E9%97%9C%E8%B3%87%E6%96%99">#</a> 相關資料</h2>
<p><a href="https://juejin.cn/post/6844904169417998349#heading-17">你不知道的 WeakMap</a><br />
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap">Mdn WeakMap</a><br />
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map">Mdn Map</a><br />
<a href="https://www.bookstack.cn/read/es6-3rd/spilt.4.docs-set-map.md#8pz2kf">阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版</a><br />
<a href="https://juejin.cn/post/6854573215549751310">从JS中的内存管理说起 —— JS中的弱引用</a><br />
<a href="https://www.gushiciku.cn/pl/pncE/zh-tw">JavaScript常見的記憶體洩漏</a></p>
來做一個 Gatsby Generic Plugin 吧
2021-11-15T00:00:00Z
https://blog.errorbaker.tw/posts/YongChen/gatsby-generic-plugin/
<!-- summary -->
<p>之前朋友有個 side project 想做一個功能,也想將該製作成一個 Gatsby Plugin 並上傳到 <a href="https://www.gatsbyjs.com/plugins">Gatsby Plugin Library</a> 未來提供大家使用,而我對該功能也有點興趣,就來研究與 Gatsby 相關的議題啦。簡介 Gatsby 以及 Gatsby Plugin 的生態系,詳細描述 Gatsby Plugin 的組成與類型,最後透過 Gatsby APIs 製作一個最簡單的 Gatsby Generic Plugin。</p>
<!-- summary -->
<!-- more -->
<h2 id="%E4%BB%80%E9%BA%BC%E6%98%AF-gatsby%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/YongChen/gatsby-generic-plugin/#%E4%BB%80%E9%BA%BC%E6%98%AF-gatsby%EF%BC%9F">#</a> 什麼是 Gatsby?</h2>
<p><strong>不僅是靜態網頁產生器</strong><br />
Gatsby 一直以來是著名的靜態網頁產生器 (Static Site Generator, SSG),其特點是在 build time,便將應用程式編譯成一個個的 HTML 檔案,接著再部署到 web,在使用者向 server 第一次發送 request 之後,server 回傳的便是完整的 HTML 檔案,在使用者切換頁面的時候,瀏覽器處理網頁路徑、JS 更換網頁內容,因此在使用者切換其他頁面無須再向 server 拿資料,所以頁面瀏覽速度迅速,操作起來就像是單頁式應用 (Single Page Application, SPA),但 SSG 回傳的是預渲染好的靜態網頁內容,不同於 SPA 回傳的是空白的 HTML 檔案,因此有利於 SEO。</p>
<p>然而 Gatsby 不僅能產生靜態網頁也能產生動態網頁,或者是讓靜態網頁中部分內容是動態產生的,可看到官網的介紹,除了相當挺吸引人標語,也描述剛所提及的部分特點。</p>
<p><img src="https://i.imgur.com/S6b9RLF.png" alt="" /></p>
<p>在最新版本的 Gatsby 4 中,新增並支援 2 種渲染方式:</p>
<ul>
<li>延遲靜態生成 (Deferred Static Generation, DSG):在 build time 的時候預先標記特定頁面,直到 run-time 的時候再將標記過的頁面做 building,該方法可延遲特定頁面的生成。</li>
<li>伺服器渲染 (Server-Side Rendering, SSR):透過伺服器作渲染。<br />
<br /></li>
</ul>
<p><strong>Gatsby 數據層與 UI 層分離</strong><br />
若簡單解釋 Gatsby 的架構,Gatsby 建構於 React 以及 GraphQL 之上,React component 作為「UI 層」,GraphQL 則作為「數據層」。</p>
<p>我想有寫過 React 的人想必對 React component 的概念不陌生,在此不敘述,至於 Gatsby 的「數據層」,能整合不同的數據來源,無論在從 CMSs (ex: WordPress, Drupal)、JSON、Markdown、電子表單、其他第三方系統 APIs,都能將資料整合在一起,這是因為 Gatsby 採用 GraphQL 獲取資料,在 build time 的時候會建立一個將所有資料整合的 GraphQL server,所以在 UI 層的 React component 中的所有數據,都是在 <code>build time</code> 從相同的地方取得的,這樣的特性將 UI 層與數據層分離。</p>
<p><img src="https://i.imgur.com/UFEUMX7.png" alt="" /></p>
<h2 id="%E4%BB%80%E9%BA%BC%E6%98%AF-gatsby-plugin%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/YongChen/gatsby-generic-plugin/#%E4%BB%80%E9%BA%BC%E6%98%AF-gatsby-plugin%EF%BC%9F">#</a> 什麼是 Gatsby Plugin?</h2>
<p>Gatsby plugins 是 <code>Node.js packages</code>,可以快速地為網站新增模組化、客製化的功能,而不用重頭開始打造。Gatsby 擁有豐富的 plugin 生態系,可參考本文開頭所提及的 <a href="https://www.gatsbyjs.com/plugins">Gatsby Plugin Library</a></p>
<p>可先將 Gatsby plugin 的核心概念做 3 點小結:</p>
<ol>
<li>每個 Gatsby plugin 皆可被製作成對外發布的 npm 或是 yarn package。另外也可作為內部的 <a href="https://www.gatsbyjs.com/docs/creating-a-local-plugin/">local plugin</a>。</li>
<li>Gatsby plugin 必須有 <code>package.json</code> 檔案。</li>
<li>Gatsby plugin 使用 Gatsby APIs 來實作功能。</li>
</ol>
<p><strong>Gatsby Plugin 的檔案組成</strong></p>
<ul>
<li><code>package.json</code> (若作為 local plugin 可為空物件 <code>{}</code>)
<ul>
<li><code>name</code>: plugin 的名稱。</li>
<li><code>main</code>: 若需要其他應用程式,將其檔案名稱寫在這,若不需要,官方建議填寫 <code>index.js</code>,並且在與<code>package.json</code> 相同層級的地方,建立一個空的 index.js。</li>
<li><code>version</code>:版本紀錄,當版號改變時會清除 <code>cache</code>。</li>
<li><code>keyword</code>:plugin 發布到 npm 上,須有 <code>gatsby</code>、<code>gatsby-plugin</code> 的關鍵字,也才能發被到 Gatsby Plugin Library。</li>
</ul>
</li>
<li><code>gatsby-node.js</code>:該檔案中撰寫 <a href="https://www.gatsbyjs.com/docs/reference/config-files/gatsby-node/">Gatsby Node APIs</a> 相關的 code,當 build 的時候會 run 過該檔案的 code 一次。</li>
<li><code>gatsby-config.js</code>:定義網站的 metadata、plugin、其他基本設定。</li>
<li><code>gatsby-browser.js</code>:該檔案中可實作<a href="https://www.gatsbyjs.com/docs/reference/config-files/gatsby-browser/">Gatsby's Browser APIs</a></li>
<li><code>gatsby-ssr.js</code>:該檔案中可實作<a href="https://www.gatsbyjs.com/docs/reference/config-files/gatsby-ssr/">Gatsby's SSR APIs</a><br />
<br /></li>
</ul>
<p><strong>Gatsby Plugin 的類型</strong><br />
根據 plugin 功能,區分成不同類型的 plugin,由於有一定的<a href="https://www.gatsbyjs.com/docs/how-to/plugins-and-themes/naming-a-plugin/">命名規範</a>,故從名稱便可略知一二該 plugin 的功能。</p>
<ul>
<li><code>gatsby-source-*</code>:從給定的資料來源 (e.g. WordPress, MongoDB, the file system),載入資料,使用這類型的 plugin,可連接對應來源的資料到 Gatsby 網站中。</li>
<li><code>gatsby-transformer-*</code>:將資料格式 (e.g. CSV, YAML) 轉換資料成 Gatsby 通用的 JS 物件型式的資料格式。</li>
<li><code>gatsby-[plugin-name]-*</code>:若該 plugin 是另一個 plugin 的 plugin 😅 (不是我要繞口令,官方文件就是這麼說明的,連表情符號也是。),相依 plugin 的名稱,則會作為前綴加在新 plugin 的名稱之前,<br />
例如,新增 emoji 到 <code>gatsby-transformer-remark</code> 的輸出,新的 plugin 名稱就會變成 <code>gatsby-remark-add-emoji</code>。</li>
<li><code>gatsby-theme-*</code>:代表該 plugin 會有 UI component,可能會有 section、page、page 的一部分。</li>
<li><code>gatsby-plugin-*</code>:這是最為常見的 plugin 類型,也被稱作 Generic Plugin。若 plugin 的功能不完全符合上述的 plugin 功能,即可這樣命名。</li>
</ul>
<h2 id="%E8%A3%BD%E4%BD%9C%E4%B8%80%E5%80%8B-gatsby-generic-plugin"><a class="direct-link" href="https://blog.errorbaker.tw/posts/YongChen/gatsby-generic-plugin/#%E8%A3%BD%E4%BD%9C%E4%B8%80%E5%80%8B-gatsby-generic-plugin">#</a> 製作一個 Gatsby Generic Plugin</h2>
<p><strong>資料的最小單位 Node</strong><br />
在 Gatsby 中,一個 <a href="https://www.gatsbyjs.com/docs/node-creation/">Node</a> ,是資料的最小單位,我們能透過 <a href="https://www.gatsbyjs.com/docs/reference/config-files/actions/#createNode">createNode</a>,來製作一個 Node,而前面有提到的 <code>gatsby-node.js</code>,該檔案中所寫的 code 是用來操作 Gastsby Node APIs,例如 <code>createPage</code>、<code>createResolvers</code>、<code>sourceNodes</code>,也就是用來操作 Node(s) 節點。</p>
<p><strong>Generic plugin 向 API 請求資料的流程</strong><br />
在 <code>gatsby-node.js</code> 中可以使用 Gatsby APIs 執行下列功能:</p>
<ol>
<li>載入 API keys。</li>
<li>發送請求到 APIs。</li>
<li>利用 API 回傳的結果製作 Gatsby-nodes。</li>
<li>依照 Nodes 的製作頁面。</li>
</ol>
<p><strong>Generic plugin 的範例</strong><br />
<code>sourceNodes</code>,是用來製作 Nodes 的 API,功能實作如下:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// gatsby-node.js</span><br />exports<span class="token punctuation">.</span><span class="token function-variable function">sourceNodes</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> actions<span class="token punctuation">,</span> createNodeId<span class="token punctuation">,</span> createContentDigest <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> nodeData <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">title</span><span class="token operator">:</span> <span class="token string">"Test Node"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">description</span><span class="token operator">:</span> <span class="token string">"Testing the node "</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">const</span> newNode <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token operator">...</span>nodeData<span class="token punctuation">,</span><br /> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token function">createNodeId</span><span class="token punctuation">(</span><span class="token string">"TestNode-testid"</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">internal</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">"TestNode"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">contentDigest</span><span class="token operator">:</span> <span class="token function">createContentDigest</span><span class="token punctuation">(</span>nodeData<span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><br /> actions<span class="token punctuation">.</span><span class="token function">createNode</span><span class="token punctuation">(</span>newNode<span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<p>上述的程式碼,會製造一個叫做 <code>TEST Node</code> 的 Node,只要在 Gatsby 專案中使用 <code>gatsby develop</code>,指令重啟 graphQL server,便能在預設的 <code>http://localhost:8000/___graphql</code> 頁面中,取得 <code>allTestNode</code> 的 query 結果。</p>
<p><img src="https://i.imgur.com/JcpOi02.png" alt="" /></p>
<h2 id="%E7%B8%BD%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/YongChen/gatsby-generic-plugin/#%E7%B8%BD%E7%B5%90">#</a> 總結</h2>
<p>這篇文章介紹:</p>
<ul>
<li>SSG 的運作原理以及 Gatsby 但它不僅僅是 SSG,也能動態產生內容。</li>
<li>透過 GraphQL 將所以數據整合在一起,也因為 Gatsby 建立在 React 以及 GraphQL 之上,能清楚拆分成 UI 層與數據層。</li>
<li>Gatsby Plugin 的核心概念以及其組成需要哪些檔案。</li>
<li>Gatsby 擁有豐富的 Plugin 生態系,可迅速為網站新增功能,Plugin 依照功能區分類型。</li>
<li>如何製作一個 Gatsby Generic Plugin。</li>
</ul>
<p>因為省略 Gatsby 專案創建以及 GraphQL 的操作方式,本文的最後一小節:「製作一個 Gatsby Generic Plugin」,可能對完全沒接觸過 Gatsby 以及 GraphQL 的讀者會比較不清楚,可參閱這兩篇文件:<a href="https://www.gatsbyjs.com/docs/tutorial/part-0/">環境設定</a>、<a href="https://www.gatsbyjs.com/docs/tutorial/part-1/">製作並部署你的第一個 Gatsby 網站</a>。</p>
<p>原本想直接寫完「如何製作 Gatsby Theme Plugin」的,只是發現這樣難以說明清楚 Gatsby Plugin 是什麼,下一篇再詳細介紹 Gatsby Theme Plugin。</p>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%BA%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/YongChen/gatsby-generic-plugin/#%E5%8F%83%E8%80%83%E8%B3%87%E6%BA%90">#</a> 參考資源</h2>
<ul>
<li><a href="https://www.gatsbyjs.com/docs/creating-plugins/">Create Gatsby Plugin</a></li>
<li><a href="https://www.smashingmagazine.com/2020/07/understanding-plugin-development-gatsby/">Understanding Plugin Development In Gatsby</a></li>
</ul>
async/await 到底在等什麼?
2021-11-22T00:00:00Z
https://blog.errorbaker.tw/posts/cwc329/async-await-tips/
<h1 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/async-await-tips/#%E5%89%8D%E8%A8%80">#</a> 前言</h1>
<p>最近在工作上解了幾個 issue,都是跟 async/await 有關係的,用這個機會紀錄一下這段時間學到的一些東西。</p>
<h2 id="%E7%B8%BD%E4%B9%8B%E5%8A%A0%E7%88%86"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/async-await-tips/#%E7%B8%BD%E4%B9%8B%E5%8A%A0%E7%88%86">#</a> 總之加爆</h2>
<p>身為一個後端工程師,我寫的 code 有一半以上都是要跟其他服務做互動,比如說對資料庫下 query 以及打其他 micro service 的 API。</p>
<p>這些非同步的行為,身為一個 junior 理所當然 async/await 加爆,不管 function 裡面有沒有非同步行爲我都先加上 async,不管 function 是不是 async function 我都加上 await。現在回頭看,那個時候真的都在寫一些在地上爬的義大利麵。</p>
<p>要脫離這種加爆的寫法,首先就要先粗淺了解一下 async/await 到底代表什麼意思。</p>
<p>我當初只知道,async/await 可以解決 callback hell,讓很多相互依賴的非同步程式可以用人類更好閱讀的方式呈現,而不是縮排縮排再縮排。</p>
<p>這種寫法可以讓人清楚明瞭,並且用類似同步執行的寫法去寫出非同步的步驟。</p>
<p>但是我不知道的是,這其實是 Promise。</p>
<p>用最簡單最不精確的話來說,async function 就是一個會回傳 Promise 的 function,而 await 則表示我要等這個 Promise 結束之後我才會執行下面的動作。理解這個的話,就可以來看我第一個犯的錯,這也是 eslint 的其中一個 rule: no-return-await。</p>
<p>先看一下以下的程式碼</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token function-variable function">queryPromise</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">sql<span class="token punctuation">,</span> params</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Promise</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">resolve<span class="token punctuation">,</span> reject</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token comment">// use Promise to wrap db query function</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> <span class="token function-variable function">selectPosts</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token keyword">await</span> <span class="token function">queryPromise</span><span class="token punctuation">(</span>sql<span class="token punctuation">,</span> params<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>上面的這段 code,在 <code>selectPosts</code> 中 return 後面是不需要 await 的。<br />
我當初的疑問是,我回傳的是執行 query 之後的結果,這個結果不是要用 await 拿到之後再回傳嗎?這樣我加上 await 是哪裡有錯呢?</p>
<p>但是在仔細想想,async function 回傳的是什麼?是一個 Promise。</p>
<p>而 queryPromise 原本就是回傳一個 Promise,就算我用 await 先等待 resolve or reject,然後再回傳,因為 async function 的特性,最後還是會變成一個 Promise。</p>
<p>那麼我何苦這麼麻煩?直接把原本的 Promise 回傳就可以了啊!</p>
<p>所以上面的程式碼,修改之後會變成這樣</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token function-variable function">selectPosts</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token function">queryPromise</span><span class="token punctuation">(</span>sql<span class="token punctuation">,</span> params<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>既然我在這邊不需要等待 Promise 的動作,那麼就可以省略 await 的關鍵字,讓 <code>selectPosts</code> 被呼叫的時候再 resolve or reject 即可。</p>
<h2 id="async%2Fawait-%E4%B8%8D%E7%AD%89%E4%BA%BA"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/async-await-tips/#async%2Fawait-%E4%B8%8D%E7%AD%89%E4%BA%BA">#</a> async/await 不等人</h2>
<p>在解決 async/await 加爆的問題之後,我認為自己對於 async/await 的理解已經前進一大步了,畢竟我之前只會無腦亂加。</p>
<p>這個時候遇到第二個問題,有一段舊的程式使用 request 套件執行 API request 更新 cache,並且在 request 結束之後要用 mqtt 發布訊息數則訊息。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token function-variable function">updatePostCache</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token function">request</span><span class="token punctuation">(</span>option<span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">err<span class="token punctuation">,</span> res<span class="token punctuation">,</span> body</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token comment">//cb here</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> <span class="token function-variable function">publishMqtt</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token function">mqttClient</span><span class="token punctuation">(</span>option<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> <span class="token function-variable function">updatePost</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">await</span> <span class="token function">queryPromise</span><span class="token punctuation">(</span>sql<span class="token punctuation">,</span> param<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token function">updatePostCache</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">await</span> <span class="token function">publishMqtt</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>但是因為 request 套件的回應是要在 callback 裡面處理,導致在 API request 尚未被執行完成,後面的 mqtt 就已經在動作了,這會造成資料更新與發出 mqtt 會有 race condition,這是不應該有的狀況。</p>
<p>這邊的解法,我很直覺想,那我就把 <code>updatePostCache</code> 變成 async function 就好了,於是乎第一版的解法長這樣</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token function-variable function">updatePostCache</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token function">request</span><span class="token punctuation">(</span>option<span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">err<span class="token punctuation">,</span> res<span class="token punctuation">,</span> body</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token comment">//cb here</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> <span class="token function-variable function">publishMqtt</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token function">mqttClient</span><span class="token punctuation">(</span>option<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> <span class="token function-variable function">updatePost</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">await</span> <span class="token function">queryPromise</span><span class="token punctuation">(</span>sql<span class="token punctuation">,</span> param<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">await</span> <span class="token function">updatePostCache</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">await</span> <span class="token function">publishMqtt</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>本來以為打完收工,沒想到實際測試下去,race condition 還是存在,搞得我都想再回去打波動拳了。後來去看了 MDN 之後我才恍然大悟,原來要理解 async/await 還是要從 Promise 下手才行。</p>
<p>這邊就簡單的說一下 Promise,它是 JavaScript 原生提供的非同步運算物件,其建構子可以傳入兩個 callback function,分別是成功與失敗的處理,一般來說前者會被叫做 resolve,後者是 reject。</p>
<p>根據官方文件,原本使用 callback 的 API 或者 function 也可以用 Promise 包起來,並且在 callback 裡面使用 resolve 與 reject,這樣就可以用 Promise chaining 或者 async/await 融入比較新的寫法。</p>
<p>所以我依照 MDN 的方法重新把 <code>updatePostCache</code> 包起來後變成這樣</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token function-variable function">updatePostCache</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Promise</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">resolve<span class="token punctuation">,</span> reject</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token function">request</span><span class="token punctuation">(</span>option<span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">err<span class="token punctuation">,</span> res<span class="token punctuation">,</span> body</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>err<span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token function">reject</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> <span class="token function">resolve</span><span class="token punctuation">(</span>body<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> <span class="token function-variable function">publishMqtt</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token function">mqttClient</span><span class="token punctuation">(</span>option<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> <span class="token function-variable function">updatePost</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">await</span> <span class="token function">queryPromise</span><span class="token punctuation">(</span>sql<span class="token punctuation">,</span> param<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">await</span> <span class="token function">updatePostCache</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">await</span> <span class="token function">publishMqtt</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>改好之後我就看到我的 mqtt 會在 API request 收到 response 之後才執行,總算避免掉 race condition。不過 cache 更新沒有優化導致 call API 其間畫面一直轉圈圈就是另一個故事了。</p>
<h2 id="promise.all-vs-for-loop-await"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/async-await-tips/#promise.all-vs-for-loop-await">#</a> Promise.all vs for loop await</h2>
<p>最後一個我想要說的小坑是關於 Promise.all。</p>
<p>Promise.all 在我們公司通常被用在兩個地方,一個是一次呼叫很多不同的 async function,另一種是搭配 <code>Array.prototype.map()</code> 使用,對一個 array of objects 做同樣的處理,例如再把資料一筆一筆塞入資料庫中。</p>
<p>這次的問題就出在後者,因為希望不要有重複的資料出現,所以在塞入之前會先檢查一下是不是已經有同樣的資料存在。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token function-variable function">addPosts</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">posts</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> categoryHashTable <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">getCategoryHashTable</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> <span class="token function-variable function">addPost</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">post</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">{</span> category <span class="token punctuation">}</span> <span class="token operator">=</span> post<span class="token punctuation">;</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>categoryHashTable<span class="token punctuation">[</span>category<span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token keyword">await</span> <span class="token function">insertNewCategory</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> result <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">insertPost</span><span class="token punctuation">(</span>post<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> result<span class="token punctuation">.</span>insertId<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> Promise<span class="token punctuation">.</span><span class="token function">all</span><span class="token punctuation">(</span>Posts<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span>addPost<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>這邊我想先檢查分類是否已經存在,如果不存在就新增分類。但是這段程式碼有個問題,如果我新增五篇文章都用同一個新的分類,我本來預期只會新增一個分類,沒想到最後新增五個一樣的分類。</p>
<p>後來問了大神同事才知道,原來這是滿基本的 Promise 觀念。Promise.all 並不會保證從 array 的第一個依序開始執行,反而所有的元素一起執行,彼此是有 race condition 的。</p>
<p>所以如果要如我想像的執行,有兩種比較可行的方法。第一是在 runtime 預處理資料,是先整理好我要 insert 什麼,然後統一執行。而如果要沿用我剛剛的程式邏輯,那可以用 for...of loop 處理,在每個 loop 中依序執行 async function,這樣就可以保證我的文章是依照在 array 的順序被處理。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token function-variable function">addPosts</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">posts</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> categoryHashTable <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">getCategoryHashTable</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> <span class="token function-variable function">addPost</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">post</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">{</span> category <span class="token punctuation">}</span> <span class="token operator">=</span> post<span class="token punctuation">;</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>categoryHashTable<span class="token punctuation">[</span>category<span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token keyword">await</span> <span class="token function">insertNewCategory</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> result <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">insertPost</span><span class="token punctuation">(</span>post<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> result<span class="token punctuation">.</span>insertId<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /> <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> post <span class="token keyword">of</span> posts<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">await</span> <span class="token function">addPost</span><span class="token punctuation">(</span>post<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<h1 id="%E7%B5%90%E8%AA%9E"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/async-await-tips/#%E7%B5%90%E8%AA%9E">#</a> 結語</h1>
<p>以上是我最近踩到的一些小坑,跟大家分享一下,也希望透過寫文章讓自己可以記得更清楚。</p>
用 NextAuth 實作第三方登入
2021-12-04T00:00:00Z
https://blog.errorbaker.tw/posts/ruofan/next-auth/
<!-- summary -->
<p>Hi,大家好! 前陣子在專案上使用了 NextAuth 實作第三方登入,發現 NextAuth 整合了相當多的第三方登入!這篇文章除了會帶著大家認識如何實作,也會介紹在 Line, Facebook, Google 的環境設定。</p>
<!-- summary -->
<!-- more -->
<h2 id="%E4%BB%80%E9%BA%BC%E6%98%AF-nextauth%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/next-auth/#%E4%BB%80%E9%BA%BC%E6%98%AF-nextauth%EF%BC%9F">#</a> 什麼是 NextAuth?</h2>
<p>以下為 NextAuth <a href="https://next-auth.js.org/getting-started/introduction">官方文件</a> 上對自己的介紹:</p>
<blockquote>
<p>NextAuth.js is a complete open source authentication solution for Next.js applications.</p>
</blockquote>
<p>簡單來說,NextAuth 讓我們可以輕鬆地搭配 Next.js 實作第三方登入。想了解更多的話,推薦看 NextAuth 的 <a href="https://next-auth.js.org/getting-started/introduction">官方文件</a>。</p>
<h2 id="line-%E7%9A%84%E7%92%B0%E5%A2%83%E8%A8%AD%E5%AE%9A"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/next-auth/#line-%E7%9A%84%E7%92%B0%E5%A2%83%E8%A8%AD%E5%AE%9A">#</a> LINE 的環境設定</h2>
<p>進入 <a href="https://developers.line.biz/en/">Line developer console</a>,接著新增一個 provider。在新的 provider 中新增 LINE login 的 channel。在剛剛新增好的 login channel,我們可以拿到 Channel ID 以及 Channel secret,這兩個號碼可以先記著,待會會用到。<br />
接著在 LINE Login settings 中,設定 <code>Callback URL = http://localhost:{port}/api/auth/callback/line</code>。這邊視相對應的 port 填入。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/line-login.png" alt="" /></p>
<h2 id="google-%E7%9A%84%E7%92%B0%E5%A2%83%E8%A8%AD%E5%AE%9A"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/next-auth/#google-%E7%9A%84%E7%92%B0%E5%A2%83%E8%A8%AD%E5%AE%9A">#</a> Google 的環境設定</h2>
<p>進入 <a href="https://cloud.google.com/">Google cloud console</a>,新增一個專案。在新的專案中進入 api & services 的 Credentials,新增一個 OAuth client ID,設定 <code>Authorized JavaScript origins URIs = http://localhost:{port}</code>; <code>Authorized redirect URIs = http://localhost:{port}/api/auth/callback/google</code>,新增成功後也記著這邊拿到的 Client ID 以及 Client secret。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/google-login.png" alt="" /></p>
<h2 id="facebook-%E7%9A%84%E7%92%B0%E5%A2%83%E8%A8%AD%E5%AE%9A"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/next-auth/#facebook-%E7%9A%84%E7%92%B0%E5%A2%83%E8%A8%AD%E5%AE%9A">#</a> Facebook 的環境設定</h2>
<p>進入 <a href="https://developers.facebook.com/">Facebook developer</a>,新增一個 app,在 basic setting 的 domain 新增 <code>http://localhost:{port}</code>。接著在右上角按下 create test app ,這是剛剛新增的 app 的測試版。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/facebook-login(1).png" alt="" /></p>
<p>接著我們使用測試版的環境,進入測試版後在 facebook login settings 的 Valid OAuth Redirect URIs 設定 <code>https://{domain}/api/auth/callback/facebook</code>。<br />
這邊特別值得注意的是,如果你在 Valid OAuth Redirect URIs 想新增 <code>http://localhost:{port}/api/auth/signin/facebook</code> 會得到下方資訊:</p>
<blockquote>
<p><a href="http://localhost/">http://localhost</a> redirects are automatically allowed while in development mode only and do not need to be added here.</p>
</blockquote>
<p>簡單來說,在 development mode 下不需要在這新增 redirect url。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/facebook-login(2).png" alt="" /></p>
<p>接著回到 basic setting 中把測試版的 App ID 以及 App Secret 記下來。<br />
設定完以上環境後,可以開始進入實作啦!</p>
<h2 id="%E5%AF%A6%E4%BD%9C%E7%AC%AC%E4%B8%89%E6%96%B9%E7%99%BB%E5%85%A5"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/next-auth/#%E5%AF%A6%E4%BD%9C%E7%AC%AC%E4%B8%89%E6%96%B9%E7%99%BB%E5%85%A5">#</a> 實作第三方登入</h2>
<p>建立一個 NextJS 專案後,先安裝 next-auth</p>
<pre class="language-bash"><code class="language-bash"><span class="token variable">$yarn</span> <span class="token function">add</span> next-auth</code></pre>
<p>接著把剛剛拿到的 Client ID 以及 Client secret 寫入環境變數。<br />
這邊可以看到環境變數中還有一個 SECRET,這個環境變數主要會被用來像是 hash tokens。<br />
我們可以使用官方文件推薦的方法,使用 openssl 產生。</p>
<pre class="language-json"><code class="language-json">SECRET='xxxxxx'<br /><br />FACEBOOK_ID='xxxxxx'<br />FACEBOOK_SECRET='xxxxxx'<br /><br />GOOGLE_ID='xxxxxx'<br />GOOGLE_SECRET='xxxxxx'<br /><br />LINE_CHANNEL_ID='xxxxxx'<br />LINE_CHANNEL_SECRET='xxxxxx'</code></pre>
<p>接著在 pages/api/auth 建立一個 [...nextauth].ts 檔。<br />
因為 signIn, callback, signOut 等 request 都會在 <code>/api/auth/*</code> 被 NextAuth.js 處理。<br />
此外,在 callbacks 中我們可以彈性的拿到一些資料。</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">import</span> NextAuth <span class="token keyword">from</span> <span class="token string">"next-auth"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token keyword">type</span> <span class="token punctuation">{</span> NextApiRequest<span class="token punctuation">,</span> NextApiResponse <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"next"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> FacebookProvider <span class="token keyword">from</span> <span class="token string">"next-auth/providers/facebook"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> GoogleProvider <span class="token keyword">from</span> <span class="token string">"next-auth/providers/google"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> LineProvider <span class="token keyword">from</span> <span class="token string">"next-auth/providers/line"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">auth</span><span class="token punctuation">(</span>req<span class="token operator">:</span> NextApiRequest<span class="token punctuation">,</span> res<span class="token operator">:</span> NextApiResponse<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> providers <span class="token operator">=</span> <span class="token punctuation">[</span><br /> <span class="token function">FacebookProvider</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> clientId<span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">FACEBOOK_ID</span><span class="token punctuation">,</span><br /> clientSecret<span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">FACEBOOK_SECRET</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token function">GoogleProvider</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> clientId<span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">GOOGLE_ID</span><span class="token punctuation">,</span><br /> clientSecret<span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">GOOGLE_SECRET</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token function">LineProvider</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> clientId<span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">LINE_CHANNEL_ID</span><span class="token punctuation">,</span><br /> clientSecret<span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">LINE_CHANNEL_SECRET</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token punctuation">]</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">return</span> <span class="token keyword">await</span> <span class="token function">NextAuth</span><span class="token punctuation">(</span>req<span class="token punctuation">,</span> res<span class="token punctuation">,</span> <span class="token punctuation">{</span><br /> providers<span class="token punctuation">,</span><br /> secret<span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">SECRET</span><span class="token punctuation">,</span><br /> jwt<span class="token operator">:</span> <span class="token punctuation">{</span><br /> secret<span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">SECRET</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> session<span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token comment">// This is the default. The session is saved in a cookie and never persisted anywhere.</span><br /> strategy<span class="token operator">:</span> <span class="token string">"jwt"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token comment">// Enable debug messages in the console if you are having problems</span><br /> debug<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /><br /> pages<span class="token operator">:</span> <span class="token punctuation">{</span><br /> signIn<span class="token operator">:</span> <span class="token string">"/auth/signin"</span><span class="token punctuation">,</span><br /> error<span class="token operator">:</span> <span class="token string">"/auth/signin"</span><span class="token punctuation">,</span><br /> newUser<span class="token operator">:</span> <span class="token string">"/auth/new-user"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /><br /> callbacks<span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token keyword">async</span> <span class="token function">session</span><span class="token punctuation">(</span><span class="token punctuation">{</span> session<span class="token punctuation">,</span> token <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// Send properties to the client, like an access_token from a provider.</span><br /> session<span class="token punctuation">.</span>accessToken <span class="token operator">=</span> token<span class="token punctuation">.</span>accessToken<span class="token punctuation">;</span><br /> session<span class="token punctuation">.</span>refreshToken <span class="token operator">=</span> token<span class="token punctuation">.</span>refreshToken<span class="token punctuation">;</span><br /> session<span class="token punctuation">.</span>idToken <span class="token operator">=</span> token<span class="token punctuation">.</span>idToken<span class="token punctuation">;</span><br /> session<span class="token punctuation">.</span>provider <span class="token operator">=</span> token<span class="token punctuation">.</span>provider<span class="token punctuation">;</span><br /> session<span class="token punctuation">.</span>id <span class="token operator">=</span> token<span class="token punctuation">.</span>id<span class="token punctuation">;</span><br /> <span class="token keyword">return</span> session<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token keyword">async</span> <span class="token function">jwt</span><span class="token punctuation">(</span><span class="token punctuation">{</span> token<span class="token punctuation">,</span> user<span class="token punctuation">,</span> account <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// Persist the OAuth access_token to the token right after signin</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>account<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> token<span class="token punctuation">.</span>accessToken <span class="token operator">=</span> account<span class="token punctuation">.</span>access_token<span class="token punctuation">;</span><br /> token<span class="token punctuation">.</span>refreshToken <span class="token operator">=</span> account<span class="token punctuation">.</span>refresh_token<span class="token punctuation">;</span><br /> token<span class="token punctuation">.</span>idToken <span class="token operator">=</span> account<span class="token punctuation">.</span>id_token<span class="token punctuation">;</span><br /> token<span class="token punctuation">.</span>provider <span class="token operator">=</span> account<span class="token punctuation">.</span>provider<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>user<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> token<span class="token punctuation">.</span>id <span class="token operator">=</span> user<span class="token punctuation">.</span>id<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> token<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>接著在 types 建立一個 next-auth.d.ts 檔。</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">import</span> NextAuth <span class="token keyword">from</span> <span class="token string">'next-auth'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> <span class="token constant">JWT</span> <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'next-auth/jwt'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> Session <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'next-auth'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> Providers <span class="token keyword">from</span> <span class="token string">'next-auth/providers'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">declare</span> <span class="token keyword">module</span> <span class="token string">'next-auth'</span> <span class="token punctuation">{</span><br /> <span class="token comment">/**<br /> * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context<br /> */</span><br /> <span class="token keyword">interface</span> <span class="token class-name">Profile</span> <span class="token punctuation">{</span><br /> email_verified<span class="token operator">:</span> <span class="token builtin">boolean</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">interface</span> <span class="token class-name">Session</span> <span class="token punctuation">{</span><br /> user<span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token comment">/** The user's postal address. */</span><br /> id<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">;</span><br /> email<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /> name<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /> image<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">declare</span> <span class="token keyword">module</span> <span class="token string">'next-auth/jwt'</span> <span class="token punctuation">{</span><br /> <span class="token comment">/** Returned by the `jwt` callback and `getToken`, when using JWT sessions */</span><br /> <span class="token keyword">interface</span> <span class="token class-name"><span class="token constant">JWT</span></span> <span class="token punctuation">{</span><br /> <span class="token comment">/** OpenID ID Token */</span><br /> idToken<span class="token operator">?</span><span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /> accessToken<span class="token operator">?</span><span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /> providerId<span class="token operator">?</span><span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">declare</span> <span class="token keyword">module</span> <span class="token string">'next-auth/providers'</span> <span class="token punctuation">{</span><br /> <span class="token keyword">interface</span> <span class="token class-name">providers</span> <span class="token punctuation">{</span><br /> provider<span class="token operator">:</span> provider<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">interface</span> <span class="token class-name">provider</span> <span class="token punctuation">{</span><br /> id<span class="token operator">?</span><span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /> name<span class="token operator">?</span><span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /></code></pre>
<p>下一步,在 _app.tsx 加上 SessionProvider。</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> SessionProvider <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'next-auth/react'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> ChakraProvider <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@chakra-ui/react'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> theme <span class="token keyword">from</span> <span class="token string">'../chakra/theme'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token keyword">type</span> <span class="token punctuation">{</span> AppProps <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'next/app'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">function</span> <span class="token function">MyApp</span><span class="token punctuation">(</span><span class="token punctuation">{</span> Component<span class="token punctuation">,</span> pageProps<span class="token operator">:</span> <span class="token punctuation">{</span> session<span class="token punctuation">,</span> <span class="token operator">...</span>pageProps <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token operator">:</span> AppProps<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span>SessionProvider session<span class="token operator">=</span><span class="token punctuation">{</span>session<span class="token punctuation">}</span><span class="token operator">></span><br /> <span class="token operator"><</span>ChakraProvider resetCSS theme<span class="token operator">=</span><span class="token punctuation">{</span>theme<span class="token punctuation">}</span><span class="token operator">></span><br /> <span class="token operator"><</span>Component <span class="token punctuation">{</span><span class="token operator">...</span>pageProps<span class="token punctuation">}</span> <span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>ChakraProvider<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>SessionProvider<span class="token operator">></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> MyApp<span class="token punctuation">;</span></code></pre>
<p>最後一步,在 header component 中設定拿到 session 跟沒有拿到 session 的狀態。</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">import</span> <span class="token punctuation">{</span> useSession<span class="token punctuation">,</span> signIn<span class="token punctuation">,</span> signOut <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'next-auth/react'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> React <span class="token keyword">from</span> <span class="token string">'react'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token function-variable function">Header</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">{</span> data<span class="token operator">:</span> session<span class="token punctuation">,</span> status <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">useSession</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>session<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span><span class="token operator">></span><br /> Signed <span class="token keyword">in</span> <span class="token keyword">as</span> <span class="token punctuation">{</span>session<span class="token punctuation">.</span>user<span class="token punctuation">.</span>email<span class="token punctuation">}</span> <span class="token operator"><</span>br <span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">signOut</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token operator">></span>Sign out<span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span><span class="token operator">></span><br /> <span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span><span class="token operator">></span><br /> Not signed <span class="token keyword">in</span> <span class="token operator"><</span>br <span class="token operator">/</span><span class="token operator">></span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">signIn</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token operator">></span>Sign <span class="token keyword">in</span><span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span><span class="token operator">></span><br /> <span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<p>來看一下整個 login 流程吧!<br />
<img src="https://blog.errorbaker.tw/img/posts/ruofan/next-login.gif" alt="" /></p>
<h2 id="%E5%B0%8F%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/next-auth/#%E5%B0%8F%E7%B5%90">#</a> 小結</h2>
<p>在實作過程中,環境變數以及環境設定會特別需要注意,初次使用可能會踩坑XD<br />
整體來說,設計上蠻有彈性的!推薦給大家。</p>
<p>在閱讀文章時如果有遇到什麼問題,或是有什麼建議,都歡迎留言告訴我,謝謝。😃</p>
<ul>
<li><a href="https://github.com/ruofanwei/next-app">Github | Repo: Next-app</a></li>
</ul>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/next-auth/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li><a href="https://next-auth.js.org/">Documentation | NextAuth</a></li>
<li><a href="https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/?locale=en_US">Documentation | Manually Build a Login Flow</a></li>
<li><a href="https://developers.google.com/identity/protocols/oauth2">Documentation | Using OAuth 2.0 to Access Google APIs </a></li>
<li><a href="https://developers.line.biz/en/docs/line-login/integrate-line-login/">Documentation | Integrating LINE Login with your web app </a></li>
</ul>
好玩 && 好用的 console log 技巧
2021-12-12T00:00:00Z
https://blog.errorbaker.tw/posts/xiang/console-log/
<!-- summary -->
<p>你知道 console.log 還有其他好玩的功用嗎?</p>
<!-- summary -->
<!-- more -->
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/console-log/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<p>在我們 JS 的開發歷程當中,最熟悉不過的語法 <code>console.log</code>,伴隨著我們每天的 debug 的生活。<br />
從還在學習程式的第一天起,導師 Huli 就傳授給我們前端開發最強大的技能:「 console.log 加爆 」!<br />
好用程度之高,從學習 JS 的第一天起,一直到現在工作上面的開發,無時無刻都可以來一段 console.log。</p>
<p>原本以為 console.log 已經無敵好用的我,無意之間認識到了,原來 console 不是只有 <code>log</code> 這個厲害的用法而已。下面就來介紹一些不同情境下能夠幫助我們更好 debug 的 console 技巧。</p>
<h2 id="%E5%8D%B0%E5%87%BA%E8%B3%87%E6%96%99%E7%9A%84%E5%90%84%E7%A8%AE%E6%8A%80%E5%B7%A7"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/console-log/#%E5%8D%B0%E5%87%BA%E8%B3%87%E6%96%99%E7%9A%84%E5%90%84%E7%A8%AE%E6%8A%80%E5%B7%A7">#</a> 印出資料的各種技巧</h2>
<p>我們最簡易印出資料的方式,就是直接在 console.log 裡面放入我們想印出的變數:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">let</span> a <span class="token operator">=</span> <span class="token string">"apple"</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>a<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token comment">// apple</span></code></pre>
<p>但是當我們一份檔案裡面有很多 console.log 時,可能不容易判斷印出來的資料是哪裡的資料,所以我們可能會利用搭配字串的方式來標注目前正在呈現哪一個變數:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">var</span> a <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span><br /><span class="token keyword">function</span> <span class="token function">fn</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"第一個 a:"</span><span class="token punctuation">,</span> a<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">var</span> a <span class="token operator">=</span> <span class="token number">5</span><span class="token punctuation">;</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"第二個 a:"</span><span class="token punctuation">,</span> a<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> a<span class="token operator">++</span><span class="token punctuation">;</span><br /> <span class="token keyword">var</span> a<span class="token punctuation">;</span><br /> <span class="token function">fn2</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"第三個 a:"</span><span class="token punctuation">,</span> a<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">function</span> <span class="token function">fn2</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"第四個 a:"</span><span class="token punctuation">,</span> a<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> a <span class="token operator">=</span> <span class="token number">20</span><span class="token punctuation">;</span><br /> b <span class="token operator">=</span> <span class="token number">100</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><span class="token function">fn</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"第五個 a:"</span><span class="token punctuation">,</span> a<span class="token punctuation">)</span><span class="token punctuation">;</span><br />a <span class="token operator">=</span> <span class="token number">10</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"第六個 a:"</span><span class="token punctuation">,</span> a<span class="token punctuation">)</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"第一個 b:"</span><span class="token punctuation">,</span> b<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token comment">// 第一個 a: undefined</span><br /><span class="token comment">// 第二個 a: 5</span><br /><span class="token comment">// 第四個 a: 6</span><br /><span class="token comment">// 第三個 a: 20</span><br /><span class="token comment">// 第五個 a: 1</span><br /><span class="token comment">// 第六個 a: 10</span><br /><span class="token comment">// 第一個 b: 100</span></code></pre>
<p>如果我們想印的樣子是 <code>變數: 值</code> 的形式,可以用類似 ES6 解構的語法:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">let</span> a <span class="token operator">=</span> <span class="token string">"apple"</span><span class="token punctuation">;</span><br /><span class="token keyword">let</span> b <span class="token operator">=</span> <span class="token string">"banana"</span><span class="token punctuation">;</span><br /><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token punctuation">{</span> a <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token punctuation">{</span> b <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token comment">// {a: 'apple'}</span><br /><span class="token comment">// {b: 'banana'}</span></code></pre>
<p>好,現在特別的要來了!</p>
<h2 id="console-%E7%89%B9%E6%AE%8A%E7%94%A8%E6%B3%95"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/console-log/#console-%E7%89%B9%E6%AE%8A%E7%94%A8%E6%B3%95">#</a> console 特殊用法</h2>
<p>☞ 當我們可能想要判斷某種狀況下才要印出資料,可以使用 console.assert:</p>
<ul>
<li>傳入的第一個參數為 false 時,會印出傳入的第二個參數</li>
</ul>
<pre class="language-js"><code class="language-js"><span class="token comment">// 判斷當 number 不為偶數時,印出錯誤訊息</span><br /><br /><span class="token keyword">let</span> errMsg <span class="token operator">=</span> <span class="token string">"the number is not even"</span><span class="token punctuation">;</span><br /><span class="token keyword">let</span> number <span class="token operator">=</span> <span class="token number">3</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">assert</span><span class="token punctuation">(</span>number <span class="token operator">%</span> <span class="token number">2</span> <span class="token operator">===</span> <span class="token number">0</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> number<span class="token punctuation">,</span> errMsg <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token comment">// Assertion failed: {number: 3, errMsg: "the number is not even"}</span></code></pre>
<p>☞ 當我們想給 console 加上計數功能時,可以透過 console.count():</p>
<ul>
<li>只要參數的數值不變,每被呼叫一次,就會 count++</li>
<li>當參數的數值有改變,count 就會重新計算</li>
</ul>
<pre class="language-js"><code class="language-js"><span class="token keyword">let</span> a<span class="token punctuation">;</span><br /><span class="token keyword">function</span> <span class="token function">doConsole</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">count</span><span class="token punctuation">(</span>a<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><br />a <span class="token operator">=</span> <span class="token string">"apple"</span><span class="token punctuation">;</span><br /><span class="token function">doConsole</span><span class="token punctuation">(</span>a<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token function">doConsole</span><span class="token punctuation">(</span>a<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token function">doConsole</span><span class="token punctuation">(</span>a<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token comment">// a 的值改變後</span><br />a <span class="token operator">=</span> <span class="token string">"banana"</span><span class="token punctuation">;</span><br /><span class="token function">doConsole</span><span class="token punctuation">(</span>a<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token function">doConsole</span><span class="token punctuation">(</span>a<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token comment">// apple: 1</span><br /><span class="token comment">// apple: 2</span><br /><span class="token comment">// apple: 3</span><br /><span class="token comment">// banana: 1</span><br /><span class="token comment">// banana: 2</span></code></pre>
<p>除了改變數值以外,也可以透過 console.countReset 將數值歸零:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">let</span> a <span class="token operator">=</span> <span class="token string">"apple"</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">count</span><span class="token punctuation">(</span>a<span class="token punctuation">)</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">count</span><span class="token punctuation">(</span>a<span class="token punctuation">)</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">countReset</span><span class="token punctuation">(</span>a<span class="token punctuation">)</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">count</span><span class="token punctuation">(</span>a<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token comment">// apple: 1</span><br /><span class="token comment">// apple: 2</span><br /><span class="token comment">// apple: 1</span></code></pre>
<p>☞ 當我們要印出的東西是有層級關係的,我們可以使用 console.group:</p>
<pre class="language-js"><code class="language-js">console<span class="token punctuation">.</span><span class="token function">group</span><span class="token punctuation">(</span><span class="token string">"Lidemy 鋰學院"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"Lidemy 鋰學院的老師 Huli"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">group</span><span class="token punctuation">(</span><span class="token string">"Lidemy 鋰學院的第一期課程"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"第一期課程的第一個 學員"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"第一期課程的第二個 學員"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">groupEnd</span><span class="token punctuation">(</span><span class="token string">"Lidemy 鋰學院的第一期課程"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">group</span><span class="token punctuation">(</span><span class="token string">"Lidemy 鋰學院的第二期課程"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"第二期課程的第一個 學員"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"第二期課程的第二個 學員"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">groupEnd</span><span class="token punctuation">(</span><span class="token string">"Lidemy 鋰學院的第二期課程"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">groupEnd</span><span class="token punctuation">(</span><span class="token string">"Lidemy 鋰學院"</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p><img src="http://blog.errorbaker.tw/img/posts/xiang/console-log-1.png" alt="" /></p>
<p>☞ 當我們要印出折疊隱藏的內容時,可以使用 console.groupCollapsed:</p>
<pre class="language-js"><code class="language-js">console<span class="token punctuation">.</span><span class="token function">groupCollapsed</span><span class="token punctuation">(</span><span class="token string">"折疊隱藏的彩蛋"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"為什麼一個功能改了又改"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"當我許願池膩!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p><img src="http://blog.errorbaker.tw/img/posts/xiang/console-log-2.png" alt="" /></p>
<p>展開以後,可以看到藏在底下的彩蛋!<br />
<img src="http://blog.errorbaker.tw/img/posts/xiang/console-log-3.png" alt="" /></p>
<p>☞ 當我們要印出物件型態的資料時,可以使用 console.table:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">let</span> obj <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">食物</span><span class="token operator">:</span> <span class="token string">"西瓜"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">工具</span><span class="token operator">:</span> <span class="token string">"牙刷"</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br />console<span class="token punctuation">.</span><span class="token function">table</span><span class="token punctuation">(</span>obj<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p><img src="http://blog.errorbaker.tw/img/posts/xiang/console-log-4.png" alt="" /></p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">Person</span><span class="token punctuation">(</span><span class="token parameter">firstName<span class="token punctuation">,</span> lastName</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>firstName <span class="token operator">=</span> firstName<span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>lastName <span class="token operator">=</span> lastName<span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">var</span> family <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br />family<span class="token punctuation">.</span>mother <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Person</span><span class="token punctuation">(</span><span class="token string">"Jane"</span><span class="token punctuation">,</span> <span class="token string">"Smith"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br />family<span class="token punctuation">.</span>father <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Person</span><span class="token punctuation">(</span><span class="token string">"John"</span><span class="token punctuation">,</span> <span class="token string">"Smith"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br />family<span class="token punctuation">.</span>daughter <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Person</span><span class="token punctuation">(</span><span class="token string">"Emily"</span><span class="token punctuation">,</span> <span class="token string">"Smith"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br />console<span class="token punctuation">.</span><span class="token function">table</span><span class="token punctuation">(</span>family<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p><img src="http://blog.errorbaker.tw/img/posts/xiang/console-log-5.png" alt="" /></p>
<p>☞ 上面提到過 console 能幫我們計數,但其實他還可以幫我們計時,我們可以使用 console.time:</p>
<pre class="language-js"><code class="language-js">console<span class="token punctuation">.</span><span class="token function">time</span><span class="token punctuation">(</span><span class="token string">"timer"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator"><</span> <span class="token number">100</span><span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// do something</span><br /><span class="token punctuation">}</span><br /><br />console<span class="token punctuation">.</span><span class="token function">timeEnd</span><span class="token punctuation">(</span><span class="token string">"timer"</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>瀏覽器會幫我們紀錄從 console.time 執行到 console.timeEnd 所經過的時間</p>
<p><img src="http://blog.errorbaker.tw/img/posts/xiang/console-log-6.png" alt="" /></p>
<p>☞ 如果我們想看看 function 被呼叫的順序,或者誰執行了這個 function,可以使用 console.trace:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">a</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">trace</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><span class="token keyword">function</span> <span class="token function">b</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token function">a</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><span class="token keyword">function</span> <span class="token function">c</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token function">b</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><span class="token function">b</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token function">c</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p><img src="http://blog.errorbaker.tw/img/posts/xiang/console-log-7.png" alt="" /></p>
<p>☞ 當我們想要更清楚分類印出的資訊時,可以使用 info、warn、error:</p>
<pre class="language-js"><code class="language-js">console<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"我是 info"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">warn</span><span class="token punctuation">(</span><span class="token string">"我是 warn"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">"我是 error"</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p><img src="http://blog.errorbaker.tw/img/posts/xiang/console-log-8.png" alt="" /></p>
<p>☞ 當我們想要把印出的文字加上 style 時,可以這麼做:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">let</span> a <span class="token operator">=</span> <span class="token string">"apple"</span><span class="token punctuation">;</span><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><br /> <span class="token string">"%c a = %s"</span><span class="token punctuation">,</span><br /> <span class="token string">"color:red; font-size: 35px; background-color: yellow"</span><span class="token punctuation">,</span><br /> a<br /><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>可以搭配的設定:</p>
<ul>
<li>%s: for string</li>
<li>%i or %d: for integers</li>
<li>%f: for float numbers</li>
<li>%o: for DOM element</li>
<li>%O: for JS object</li>
<li>%c : for adding styles to your log</li>
</ul>
<p><img src="http://blog.errorbaker.tw/img/posts/xiang/console-log-9.png" alt="" /></p>
<h2 id="%E7%B5%90%E8%AA%9E"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/console-log/#%E7%B5%90%E8%AA%9E">#</a> 結語</h2>
<p>文章中提到了一些 console 的特殊用法,有時候能幫助我們在 debug 時更清楚的呈現出資料,或者在 devtool console 隱藏一些有趣的彩蛋。<br />
以後開發的時候,除了「console.log 加爆!」以外,還有更多 console 組合技可以一起搭配使用囉。</p>
用 Fireorm interact with Cloud Firestore
2021-12-17T00:00:00Z
https://blog.errorbaker.tw/posts/ruofan/express-firestore/
<!-- summary -->
<p>Hi,大家好! 前陣子在研究專案上從 MySQL migrate 到 Cloud Firestore 的可能性,選擇了 Fireorm 作為這次的主題,這篇文章會帶著大家認識 Fireorm 的基本操作。</p>
<!-- summary -->
<!-- more -->
<h2 id="%E4%BB%80%E9%BA%BC%E6%98%AF-cloud-firestore-%3F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/express-firestore/#%E4%BB%80%E9%BA%BC%E6%98%AF-cloud-firestore-%3F">#</a> 什麼是 Cloud Firestore ?</h2>
<p>以下為 Cloud Firestore <a href="https://firebase.google.com/docs/firestore">官方文件</a> 上對自己的介紹:</p>
<blockquote>
<p>Cloud Firestore is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud.</p>
</blockquote>
<p>簡單來說,Cloud Firestore 是一個有彈性與擴展性的 NoSQL cloud database。推薦看 Cloud Firestore <a href="https://firebase.google.com/docs/firestore">官方文件</a> 瞭解更多。</p>
<h2 id="%E4%BB%80%E9%BA%BC%E6%98%AF-fireorm-%3F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/express-firestore/#%E4%BB%80%E9%BA%BC%E6%98%AF-fireorm-%3F">#</a> 什麼是 Fireorm ?</h2>
<p>以下為 Fireorm <a href="https://fireorm.js.org/#/">官方文件</a> 上對自己的介紹:</p>
<blockquote>
<p>Fireorm is a tiny wrapper on top of firebase-admin that makes life easier when dealing with a Firestore database.</p>
</blockquote>
<blockquote>
<p>Fireorm is heavily inspired by other orms such as TypeORM and Entity Framework. The idea is that we:<br />
1- define our model as a simple JavaScript class,<br />
2- decorate our model with fireorm’s Collection decorator to represent a Firestore collection<br />
3- use our model’s repository to do CRUD operations on your Firestore database.</p>
</blockquote>
<h4 id="%E5%8F%AF%E4%BB%A5%E4%B8%8D%E4%BD%BF%E7%94%A8-fireorm-%E5%97%8E%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/express-firestore/#%E5%8F%AF%E4%BB%A5%E4%B8%8D%E4%BD%BF%E7%94%A8-fireorm-%E5%97%8E%EF%BC%9F">#</a> 可以不使用 Fireorm 嗎?</h4>
<p>筆者認為,Fireorm 不一定要用。這邊使用的原因單純是因為 fireorm 可以簡便的定義 schema。<br />
值得注意的是,在 Fireorm 的 <a href="https://github.com/wovalle/fireorm/issues/1">Roadmap</a> 還有很多 功能 pending 中。</p>
<h2 id="firebase-%E7%92%B0%E5%A2%83%E8%A8%AD%E5%AE%9A"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/express-firestore/#firebase-%E7%92%B0%E5%A2%83%E8%A8%AD%E5%AE%9A">#</a> Firebase 環境設定</h2>
<p>首先,先進入 firebase console 開啟一個新的專案,接著在 firestore database 按下開啟使用。最後到專案設定中 把 project 名稱跟 service account key 的 json 先記下來,待會會使用到。<br />
<img src="https://blog.errorbaker.tw/img/posts/ruofan/firestore-1.png" alt="" /></p>
<h2 id="%E9%96%8B%E5%A7%8B%E5%AF%A6%E4%BD%9C%EF%BC%81"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/express-firestore/#%E9%96%8B%E5%A7%8B%E5%AF%A6%E4%BD%9C%EF%BC%81">#</a> 開始實作!</h2>
<p>先來看一下如何 initialize firebase。</p>
<blockquote>
<p>這邊特別值得注意的是 筆者使用的 firebase-admin 版本是 v10,引用的方法跟 v9 有些許的不同,推薦看 <a href="https://firebase.google.com/docs/admin/migrate-node-v10">官方文件</a> 瞭解更多。</p>
</blockquote>
<p>剛剛記下來的 project 名稱跟 service account key 這邊使用在 initialize firebase。</p>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> functions <span class="token keyword">from</span> <span class="token string">"firebase-functions"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> initializeApp<span class="token punctuation">,</span> cert <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"firebase-admin/app"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> getFirestore <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"firebase-admin/firestore"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> fireorm <span class="token keyword">from</span> <span class="token string">"fireorm"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> Request<span class="token punctuation">,</span> Response<span class="token punctuation">,</span> NextFunction <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"express"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> express <span class="token operator">=</span> <span class="token keyword">require</span><span class="token punctuation">(</span><span class="token string">"express"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> routes <span class="token keyword">from</span> <span class="token string">"./routes"</span><span class="token punctuation">;</span><br /><br /><br /><span class="token keyword">const</span> serviceAccount <span class="token operator">=</span> <span class="token keyword">require</span><span class="token punctuation">(</span><span class="token string">"../firestore.creds.json"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> app <span class="token operator">=</span> <span class="token function">express</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token function">initializeApp</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> credential<span class="token operator">:</span> <span class="token function">cert</span><span class="token punctuation">(</span>serviceAccount<span class="token punctuation">)</span><span class="token punctuation">,</span><br /> databaseURL<span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">API_PROJECT</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">.firebaseio.com</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> firestore <span class="token operator">=</span> <span class="token function">getFirestore</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br />firestore<span class="token punctuation">.</span><span class="token function">settings</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> timestampsInSnapshots<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> ignoreUndefinedProperties<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br />fireorm<span class="token punctuation">.</span><span class="token function">initialize</span><span class="token punctuation">(</span>firestore<span class="token punctuation">,</span> <span class="token punctuation">{</span><br /> validateModels<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br />app<span class="token punctuation">.</span><span class="token function">use</span><span class="token punctuation">(</span><span class="token string">"/fire/"</span><span class="token punctuation">,</span> routes<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token comment">// error handling middleware</span><br />app<span class="token punctuation">.</span><span class="token function">use</span><span class="token punctuation">(</span><span class="token keyword">function</span> <span class="token punctuation">(</span>err<span class="token operator">:</span> Error<span class="token punctuation">,</span> req<span class="token operator">:</span> Request<span class="token punctuation">,</span> res<span class="token operator">:</span> Response<span class="token punctuation">,</span> next<span class="token operator">:</span> NextFunction<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">//console.log(err);</span><br /> res<span class="token punctuation">.</span><span class="token function">status</span><span class="token punctuation">(</span><span class="token number">422</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">send</span><span class="token punctuation">(</span><span class="token punctuation">{</span> error<span class="token operator">:</span> err<span class="token punctuation">.</span>message <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token comment">// listen for requests</span><br />app<span class="token punctuation">.</span><span class="token function">listen</span><span class="token punctuation">(</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span>port <span class="token operator">||</span> <span class="token number">4000</span><span class="token punctuation">,</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"Ready to Go!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /></code></pre>
<h4 id="%E5%AF%A6%E4%BD%9C-schema"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/express-firestore/#%E5%AF%A6%E4%BD%9C-schema">#</a> 實作 schema</h4>
<p>這邊把 每個 document 都會有的 Timestamp 整理做一個統整。</p>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token punctuation">{</span> Timestamp <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'firebase-admin/firestore'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">abstract</span> <span class="token keyword">class</span> <span class="token class-name">AbstractSchema</span> <span class="token punctuation">{</span><br /> createdAt<span class="token operator">:</span> Timestamp<span class="token punctuation">;</span><br /> updatedAt<span class="token operator">:</span> Timestamp<span class="token punctuation">;</span><br /> deletedAt<span class="token operator">:</span> Timestamp <span class="token operator">|</span> <span class="token keyword">null</span><span class="token punctuation">;</span><br /><br /> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>createdAt <span class="token operator">=</span> Timestamp<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>updatedAt <span class="token operator">=</span> Timestamp<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>deletedAt <span class="token operator">=</span> <span class="token keyword">null</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">public</span> <span class="token keyword">async</span> <span class="token function">paranoid</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator"><</span><span class="token keyword">void</span><span class="token operator">></span> <span class="token punctuation">{</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>deletedAt <span class="token operator">=</span> Timestamp<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /></code></pre>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token punctuation">{</span> Collection <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'fireorm'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> AbstractSchema <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./abstractSchema'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> ICompanySchema <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../interfaces/company.interface'</span><span class="token punctuation">;</span><br /><span class="token decorator"><span class="token at operator">@</span><span class="token function">Collection</span></span><span class="token punctuation">(</span><span class="token string">'companys'</span><span class="token punctuation">)</span><br /><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">class</span> <span class="token class-name">CompanySchema</span><br /> <span class="token keyword">extends</span> <span class="token class-name">AbstractSchema</span><br /> <span class="token keyword">implements</span> <span class="token class-name">ICompanySchema</span><br /><span class="token punctuation">{</span><br /> id<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /> name<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /><br /> stores<span class="token operator">:</span> <span class="token builtin">Array</span><span class="token operator"><</span><span class="token builtin">string</span><span class="token operator">></span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /></code></pre>
<h5 id="%E5%BE%9E%E4%B8%8A%E9%9D%A2%E9%80%99%E6%AE%B5%E7%A8%8B%E5%BC%8F%E7%A2%BC%EF%BC%8C%E7%9C%8B%E4%B8%80%E4%B8%8B%E6%88%91%E5%80%91%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8-fireorm-%E7%B0%A1%E4%BE%BF%E7%9A%84%E5%AE%9A%E7%BE%A9-schema"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/express-firestore/#%E5%BE%9E%E4%B8%8A%E9%9D%A2%E9%80%99%E6%AE%B5%E7%A8%8B%E5%BC%8F%E7%A2%BC%EF%BC%8C%E7%9C%8B%E4%B8%80%E4%B8%8B%E6%88%91%E5%80%91%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8-fireorm-%E7%B0%A1%E4%BE%BF%E7%9A%84%E5%AE%9A%E7%BE%A9-schema">#</a> 從上面這段程式碼,看一下我們如何使用 fireorm 簡便的定義 schema</h5>
<ul>
<li>
<p>在範例中 companys collection 中預計會存放 name, stores 等資料,但是為什麼會有一個 type 是 string 的 id 呢? 因為這是 Firestore 會用 id 來辨識 document! 推薦看 Firebase <a href="https://firebase.google.com/docs/firestore/manage-data/add-data">官方文件</a> 瞭解更多。</p>
</li>
<li>
<p>在 Firestore 中我們儲存資料在 document,而 documents 組成 Collection! 這邊 fireorm 使用了 decorate 的方式宣告 Collection。 fireorm 在背後做了哪些事呢? 他會讓我們宣告的 Collection 中的 instance 變成 Firestore 的 Document!推薦看 Fireorm <a href="https://fireorm.js.org/#/globals?id=collection">官方文件</a> 瞭解更多。</p>
</li>
</ul>
<h4 id="%E5%AF%A6%E4%BD%9C-interface"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/express-firestore/#%E5%AF%A6%E4%BD%9C-interface">#</a> 實作 interface</h4>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">export</span> <span class="token keyword">interface</span> <span class="token class-name">ICompanySchema</span> <span class="token punctuation">{</span><br /> id<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /> uuid<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /> name<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /><br /> stores<span class="token operator">:</span> <span class="token builtin">Array</span><span class="token operator"><</span><span class="token builtin">string</span><span class="token operator">></span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">interface</span> <span class="token class-name">createCompanyReq</span><br /> <span class="token keyword">extends</span> <span class="token class-name">Omit<span class="token operator"><</span><br /> ICompanySchema<span class="token punctuation">,</span><br /> <span class="token operator">|</span> <span class="token string">'createdAt'</span><br /> <span class="token operator">|</span> <span class="token string">'updatedAt'</span><br /> <span class="token operator">|</span> <span class="token string">'deletedAt'</span><br /> <span class="token operator">|</span> <span class="token string">'paranoid'</span><br /> <span class="token operator">|</span> <span class="token string">'id'</span><br /> <span class="token operator">></span></span> <span class="token punctuation">{</span><br /> isMainStore<span class="token operator">:</span> <span class="token builtin">boolean</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">interface</span> <span class="token class-name">createCompanyRes</span><br /> <span class="token keyword">extends</span> <span class="token class-name">Omit<span class="token operator"><</span><br /> ICompanySchema<span class="token punctuation">,</span><br /> <span class="token string">'createdAt'</span> <span class="token operator">|</span> <span class="token string">'updatedAt'</span> <span class="token operator">|</span> <span class="token string">'deletedAt'</span> <span class="token operator">|</span> <span class="token string">'paranoid'</span><br /> <span class="token operator">></span></span> <span class="token punctuation">{</span><span class="token punctuation">}</span></code></pre>
<h4 id="%E5%AF%A6%E4%BD%9C-repository"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/express-firestore/#%E5%AF%A6%E4%BD%9C-repository">#</a> 實作 repository</h4>
<h5 id="%E5%9C%A8%E9%96%8B%E5%A7%8B%E4%B9%8B%E5%89%8D%E5%85%88%E4%BE%86%E7%9C%8B%E4%B8%80%E4%B8%8B%EF%BC%8C%E4%BB%80%E9%BA%BC%E6%98%AF-inversifyjs-%3F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/express-firestore/#%E5%9C%A8%E9%96%8B%E5%A7%8B%E4%B9%8B%E5%89%8D%E5%85%88%E4%BE%86%E7%9C%8B%E4%B8%80%E4%B8%8B%EF%BC%8C%E4%BB%80%E9%BA%BC%E6%98%AF-inversifyjs-%3F">#</a> 在開始之前先來看一下,什麼是 InversifyJS ?</h5>
<p>以下為 InversifyJS <a href="https://inversify.io/">官方文件</a> 上對自己的介紹:</p>
<blockquote>
<p>InversifyJS is a lightweight (4KB) inversion of control (IoC) container for TypeScript and JavaScript apps. A IoC container uses a class constructor to identify and inject its dependencies.</p>
</blockquote>
<p>簡單來說,InversifyJS 讓我們可以簡便的實作 Dependency injection。 推薦看 InversifyJS <a href="https://inversify.io/">官方文件</a> 瞭解更多。</p>
<h5 id="%E4%BB%80%E9%BA%BC%E6%98%AF-dependency-injection-%3F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/express-firestore/#%E4%BB%80%E9%BA%BC%E6%98%AF-dependency-injection-%3F">#</a> 什麼是 Dependency injection ?</h5>
<blockquote>
<p>Dependency injection (DI) is a very simple concept that aims to decouple components of your software and ease their integration and testing.</p>
</blockquote>
<p>簡單來說,Dependency injection 的概念是讓我們在設計架構上可以提高可測試性與重用性 。 推薦看 <a href="https://hackernoon.com/introduction-to-design-patterns-and-dependency-injection">Introduction to Design Patterns and Dependency Injection</a> 瞭解更多。</p>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token punctuation">{</span><br /> getRepository<span class="token punctuation">,</span><br /> BaseFirestoreRepository<span class="token punctuation">,</span><br /> runTransaction<span class="token punctuation">,</span><br /><span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'fireorm'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> injectable <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'inversify'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> CompanySchema <span class="token keyword">from</span> <span class="token string">'../schemas/company'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span><br /> ICompanySchema<br /> createCompanyRes<span class="token punctuation">,</span><br /><span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../interfaces/company.interface'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">interface</span> <span class="token class-name">ICompanyRepository</span> <span class="token punctuation">{</span><br /> <span class="token function">create</span><span class="token punctuation">(</span>name<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator"><</span>createCompanyRes<span class="token operator">></span><span class="token punctuation">;</span><br /> <span class="token function">findOne</span><span class="token punctuation">(</span>name<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator"><</span>ICompanySchema<span class="token operator">></span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><span class="token decorator"><span class="token at operator">@</span><span class="token function">injectable</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">CompanyRepository</span> <span class="token keyword">implements</span> <span class="token class-name">ICompanyRepository</span> <span class="token punctuation">{</span><br /> <span class="token keyword">private</span> companyCollection<span class="token operator">:</span> BaseFirestoreRepository<span class="token operator"><</span>CompanySchema<span class="token operator">></span><span class="token punctuation">;</span><br /><br /> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>companyCollection <span class="token operator">=</span> <span class="token function">getRepository</span><span class="token punctuation">(</span>CompanySchema<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">public</span> <span class="token keyword">async</span> <span class="token function">create</span><span class="token punctuation">(</span>name<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator"><</span>createCompanyRes<span class="token operator">></span> <span class="token punctuation">{</span><br /> <span class="token keyword">let</span> company <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">CompanySchema</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> <span class="token keyword">await</span> <span class="token function">runTransaction</span><span class="token punctuation">(</span><span class="token keyword">async</span> <span class="token punctuation">(</span>tran<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> companyTranRepository <span class="token operator">=</span> tran<span class="token punctuation">.</span><span class="token function">getRepository</span><span class="token punctuation">(</span>CompanySchema<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> company<span class="token punctuation">.</span>name <span class="token operator">=</span> name<span class="token punctuation">;</span><br /> <span class="token keyword">return</span> <span class="token keyword">await</span> companyTranRepository<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span>company<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">public</span> <span class="token keyword">async</span> <span class="token function">findOne</span><span class="token punctuation">(</span>id<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator"><</span>ICompanySchema<span class="token operator">></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> company <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>companyCollection<span class="token punctuation">.</span><span class="token function">findById</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> company<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /></code></pre>
<h6 id="%E5%BE%9E%E4%B8%8A%E9%9D%A2%E9%80%99%E6%AE%B5%E7%A8%8B%E5%BC%8F%E7%A2%BC%EF%BC%8C%E7%9C%8B%E4%B8%80%E4%B8%8B%E6%88%91%E5%80%91%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8-fireorm-%E5%9C%A8-getrepository-%E4%B8%AD%E6%8F%90%E4%BE%9B%E7%9A%84-crud-%E6%96%B9%E6%B3%95"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/express-firestore/#%E5%BE%9E%E4%B8%8A%E9%9D%A2%E9%80%99%E6%AE%B5%E7%A8%8B%E5%BC%8F%E7%A2%BC%EF%BC%8C%E7%9C%8B%E4%B8%80%E4%B8%8B%E6%88%91%E5%80%91%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8-fireorm-%E5%9C%A8-getrepository-%E4%B8%AD%E6%8F%90%E4%BE%9B%E7%9A%84-crud-%E6%96%B9%E6%B3%95">#</a> 從上面這段程式碼,看一下我們如何使用 fireorm 在 getRepository 中提供的 CRUD 方法</h6>
<ul>
<li>
<p>在 Repository 方面 fireorm 應用了 <a href="https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design">Repository Pattern</a>,因此上方的 companyTranRepository 可以直接使用 create 方法在 Firestore 的 collection 中新增 documents。</p>
</li>
<li>
<p>使用 <code>@injectable</code> & <code>@inject</code> decorators 宣告 dependencies。</p>
</li>
</ul>
<h4 id="%E5%AF%A6%E4%BD%9C-service"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/express-firestore/#%E5%AF%A6%E4%BD%9C-service">#</a> 實作 service</h4>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token punctuation">{</span> CompanyRepository <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../repositories/companyRepository'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> injectable<span class="token punctuation">,</span> inject <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'inversify'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">import</span> <span class="token punctuation">{</span><br /> ICompanySchema<span class="token punctuation">,</span><br /> createCompanyReq<span class="token punctuation">,</span><br /> createCompanyRes<span class="token punctuation">,</span><br /><span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../interfaces/company.interface'</span><span class="token punctuation">;</span><br /><br /><span class="token decorator"><span class="token at operator">@</span><span class="token function">injectable</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">CompanyService</span> <span class="token keyword">implements</span> <span class="token class-name">ICompanyService</span> <span class="token punctuation">{</span><br /> <span class="token function">constructor</span><span class="token punctuation">(</span><br /> <span class="token decorator"><span class="token at operator">@</span><span class="token function">inject</span></span><span class="token punctuation">(</span><span class="token string">'CompanyRepository'</span><span class="token punctuation">)</span> <span class="token keyword">private</span> companyRepository<span class="token operator">:</span> CompanyRepository<br /> <span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>companyRepository <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">CompanyRepository</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">async</span> <span class="token function">firnOrCreateCompany</span><span class="token punctuation">(</span><br /> companyUuid<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">,</span><br /> companyName<span class="token operator">:</span> <span class="token builtin">string</span><br /> <span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">try</span> <span class="token punctuation">{</span><br /> <span class="token keyword">let</span> company<span class="token punctuation">;</span><br /> <span class="token keyword">let</span> isMainStore<span class="token operator">:</span> <span class="token builtin">boolean</span> <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span><br /> <span class="token comment">/* find or create company by uuid */</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>companyUuid<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> company <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>companyRepository<span class="token punctuation">.</span><span class="token function">findOne</span><span class="token punctuation">(</span>companyName<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br /> company <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span>companyRepository<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span>companyName<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> isMainStore <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">return</span> <span class="token punctuation">{</span> company<span class="token punctuation">,</span> isMainStore <span class="token punctuation">}</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">throw</span> error<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /></code></pre>
<h4 id="%E5%AF%A6%E4%BD%9C-inversify-config"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/express-firestore/#%E5%AF%A6%E4%BD%9C-inversify-config">#</a> 實作 inversify config</h4>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token string">'reflect-metadata'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> Container <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'inversify'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> CompanyService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../services/companyService'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> CompanyRepository <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../repositories/companyRepository'</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> container <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Container</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token comment">// Services</span><br />container<span class="token punctuation">.</span><span class="token generic-function"><span class="token function">bind</span><span class="token generic class-name"><span class="token operator"><</span>CompanyService<span class="token operator">></span></span></span><span class="token punctuation">(</span><span class="token string">'CompanyService'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">to</span><span class="token punctuation">(</span>CompanyService<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token comment">// Repositories</span><br />container<span class="token punctuation">.</span><span class="token generic-function"><span class="token function">bind</span><span class="token generic class-name"><span class="token operator"><</span>CompanyRepository<span class="token operator">></span></span></span><span class="token punctuation">(</span><span class="token string">'CompanyRepository'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">to</span><span class="token punctuation">(</span>CompanyRepository<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> container<span class="token punctuation">;</span><br /></code></pre>
<p>從上面這段程式碼可以看到,在 inversify.config.ts 中,我們新增和定義了 Container。</p>
<h4 id="%E5%AF%A6%E4%BD%9C-controller"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/express-firestore/#%E5%AF%A6%E4%BD%9C-controller">#</a> 實作 controller</h4>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token punctuation">{</span> NextFunction<span class="token punctuation">,</span> Request<span class="token punctuation">,</span> Response <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'express'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> container <span class="token keyword">from</span> <span class="token string">'../config/inversify.config'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> CompanyService <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../services/companyService'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">StoreController</span> <span class="token punctuation">{</span><br /><br /> <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">async</span> <span class="token function">buildStore</span><span class="token punctuation">(</span><br /> req<span class="token operator">:</span> Request <span class="token operator">&</span> Express<span class="token punctuation">.</span>CustomRequest<span class="token punctuation">,</span><br /> res<span class="token operator">:</span> Response<span class="token punctuation">,</span><br /> next<span class="token operator">:</span> NextFunction<br /> <span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">try</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> companyName<span class="token operator">:</span> <span class="token builtin">string</span> <span class="token operator">=</span> req<span class="token punctuation">.</span>body<span class="token punctuation">.</span>companyName<span class="token punctuation">;</span> <span class="token comment">// option</span><br /> <span class="token comment">/* clarify the type for companyUuid while assign value */</span><br /> <span class="token keyword">let</span> companyUuid<span class="token operator">:</span> <span class="token builtin">string</span> <span class="token operator">=</span> req<span class="token punctuation">.</span>headers<span class="token punctuation">.</span>companyuuid<span class="token operator">!</span><span class="token punctuation">;</span> <span class="token comment">// option</span><br /><br /> <span class="token keyword">const</span> companyService <span class="token operator">=</span> <span class="token keyword">await</span> container<span class="token punctuation">.</span><span class="token generic-function"><span class="token function">get</span><span class="token generic class-name"><span class="token operator"><</span>CompanyService<span class="token operator">></span></span></span><span class="token punctuation">(</span><br /> <span class="token string">'CompanyService'</span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token comment">/* find or create company */</span><br /> <span class="token keyword">const</span> <span class="token punctuation">{</span>company<span class="token punctuation">,</span> isMainStore<span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> companyService<span class="token punctuation">.</span><span class="token function">firnOrCreateCompany</span><span class="token punctuation">(</span><br /> companyUuid<span class="token punctuation">,</span><br /> companyName<br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">return</span> res<span class="token punctuation">.</span><span class="token function">status</span><span class="token punctuation">(</span><span class="token number">200</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> code<span class="token operator">:</span> <span class="token number">200</span><span class="token punctuation">,</span><br /> message<span class="token operator">:</span> <span class="token string">'執行成功'</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>e<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>e<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> <span class="token function">next</span><span class="token punctuation">(</span>e<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><span class="token keyword">export</span> <span class="token keyword">default</span> StoreController<span class="token punctuation">;</span><br /></code></pre>
<p>從上面這段程式碼可以看到,controller 中我們將 container 使用 <code>get<T></code> 方法,在這邊 resolve 了 dependency。</p>
<h4 id="%E5%AF%A6%E4%BD%9C-route"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/express-firestore/#%E5%AF%A6%E4%BD%9C-route">#</a> 實作 route</h4>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token punctuation">{</span> Router <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'express'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> StoreController <span class="token keyword">from</span> <span class="token string">'../controller/storeController'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> router<span class="token operator">:</span> Router <span class="token operator">=</span> <span class="token function">Router</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token keyword">require</span><span class="token punctuation">(</span><span class="token string">'dotenv'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">config</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token comment">// create store</span><br />router<span class="token punctuation">.</span><span class="token function">post</span><span class="token punctuation">(</span><br /> <span class="token string">'/store'</span><span class="token punctuation">,</span><br /> StoreController<span class="token punctuation">.</span>buildStore<br /><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> router<span class="token punctuation">;</span><br /></code></pre>
<h2 id="firestore-database"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/express-firestore/#firestore-database">#</a> firestore database</h2>
<p>成功新增完資料後,進到 Firestore database 看一下吧!<br />
<img src="https://blog.errorbaker.tw/img/posts/ruofan/firestore-2.png" alt="" /></p>
<h2 id="%E5%B0%8F%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/express-firestore/#%E5%B0%8F%E7%B5%90">#</a> 小結</h2>
<p>這次實作的過程認識了不同的架構設計概念,整體來說蠻有趣的!<br />
在閱讀文章時如果有遇到什麼問題,或是有什麼建議,都歡迎留言告訴我,謝謝。😃</p>
<ul>
<li><a href="https://github.com/ruofanwei/express-firestore">Github | Repo: express-firestore</a></li>
</ul>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/express-firestore/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li><a href="https://firebase.google.com/docs/firestore">Documentation | firestore</a></li>
<li><a href="https://fireorm.js.org/#/">Documentation | fireorm</a></li>
<li><a href="https://inversify.io/">Documentation | inversify</a></li>
<li><a href="https://hackernoon.com/introduction-to-design-patterns-and-dependency-injection">Hackernoon | design-patterns</a></li>
</ul>
monorepo 之我見
2021-12-26T00:00:00Z
https://blog.errorbaker.tw/posts/cwc329/monorepo/
<!-- summary -->
<p>公司這半年陸續將專案架構由 polyrepo 轉為 monorepo,這項改變對於開發以及維運都有影響,這篇文章想要解釋 polyrepo 與 monorepo 這兩個詞,並且寫出在工作上筆者感受到的差異。</p>
<!-- summary -->
<h2 id="monorepo-%26-polyrepo"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/monorepo/#monorepo-%26-polyrepo">#</a> Monorepo & Polyrepo</h2>
<p>根據 wiki,monorepo 與 polyrepo 是不同的軟體開發策略。</p>
<p>mono 的意思是 single,repo 是 repository 的縮寫,這個意思就是在軟體開發過程中,使用一個 repository 開發多個模組、專案的方式;而 polyrepo 則是將不同的專案分為不同的 repository 管理。</p>
<p>以前後端分離的 web 開發為例,monorepo 就是把前端與後端的原始碼都放在同個 repository;而 polyrepo 則是把前端與後端分為兩個不同的 repository。</p>
<p><strong><em>monorepo</em></strong></p>
<pre class="language-ts"><code class="language-ts">├── docs # Documents<br />│ ├── api # <span class="token constant">API</span> spec<br />│ ├── archived # Archived documents<br />│ └── <span class="token operator">...</span><br />├── <span class="token constant">README</span><span class="token punctuation">.</span>md<br />├── services<br />│ ├── ui<br />│ ├── api<br />│ └── <span class="token operator">...</span><br />├── packages<br />│ ├── util<br />│ ├── core<br />│ └── <span class="token operator">...</span><br />└── <span class="token keyword">package</span><span class="token punctuation">.</span>json<br /></code></pre>
<p><strong><em>polyrepo</em></strong></p>
<pre class="language-ts"><code class="language-ts">ui<br />├── docs # Documents<br />│ └── <span class="token operator">...</span><br />├── <span class="token constant">README</span><span class="token punctuation">.</span>md<br />├── src<br />│ ├── util<br />│ ├── components<br />│ ├── containers<br />│ └── <span class="token operator">...</span><br />└── <span class="token keyword">package</span><span class="token punctuation">.</span>json<br /><br />api<br />├── docs # Documents<br />│ └── <span class="token operator">...</span><br />├── <span class="token constant">README</span><span class="token punctuation">.</span>md<br />├── src<br />│ ├── util<br />│ ├── router<br />│ ├── model<br />│ └── <span class="token operator">...</span><br />└── <span class="token keyword">package</span><span class="token punctuation">.</span>json</code></pre>
<p>上面兩個 file structure 應該可以看出 monorepo 以及 polyrepo 的基本不同。</p>
<h2 id="%E9%96%8B%E7%99%BC%E4%B8%8A%E7%9A%84%E5%B7%AE%E7%95%B0"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/monorepo/#%E9%96%8B%E7%99%BC%E4%B8%8A%E7%9A%84%E5%B7%AE%E7%95%B0">#</a> 開發上的差異</h2>
<p>入職的前半年,公司是使用 polyrepo,產品分為十幾個不同的 repo,前端、後端、mqtt、cron job 以及 devOps 等不論是產品、開發以及維運等原始碼都拆分為不同的 repository。今年八月開始將部份 repository 導入 monorepo,如今公司所有軟體相關的專案已經都整合進 monorepo。</p>
<p>筆者在公司的職位是後端工程師,除了寫後端 API 之外也負責部分 devOps 的工作,先簡述在兩個環境下開發的情境,之後再比較異同。</p>
<h3 id="polyrepo"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/monorepo/#polyrepo">#</a> polyrepo</h3>
<p>開發上,首先是開發環境的建立,polyrepo 的架構下,如果只是單純的前後端開發,不牽扯到 mqtt 或者 cron job 的話,只要把前後端的 repo clone 下來即可,開發上不需要用到的 repository 是可以不用拉到 local 的。<br />
而進入到功能開發階段,前端與後端基本上是各寫各的,各自用各自 team 慣用的 branch naming 與 commit convention,當要整合測試的時候,就把數個專案切換到對應的 branch 對接。</p>
<p>公司有自己的 library,讓多個專案使用,在 polyrepo 下做法是包成 private npm package,需要 library 的專案就用 npm install 的方式將 library 當作 npm package 引入。而 package 的專案設有自動發佈到 npm 的 CI/CD 流程。</p>
<p>至於 operating 方面,公司有 dev, staging 以及 prod 三個環境,這三個環境是以 branch 作為區分。如果需要更新 staging 的版本,就要在十幾個 repository 將 dev 的 code 進到 staging,重複十幾次的動作。<br />
在設定 CI/CD 時也是,如果今天 CI/CD 流程有所變動,也需要更改所有的 repository 的 CI/CD 設定檔。</p>
<h3 id="monorepo"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/monorepo/#monorepo">#</a> monorepo</h3>
<p>開發上,monorepo 只要把一個 repository clone 到 local 就好了,這個 repository 就會包含所有的專案,是一包非常龐大、檔案架構複雜且非常多層的原始碼。<br />
前後端開發雖然也是各寫各的,不過功能開發都會在同一個 feature branch 下共同開發。branch naming 會一致,不過 commit convention 差異較大。feature 在整合測試的時候直接使用 feature branch 即可。<br />
當 feature 開發完成,需要將所有專案的 feature branch 都進到主線,整個功能才會正常上線。</p>
<p>有一些比較底層的 util function 或者公司自己的 library 則是放在 monorepo 的 package 中作為 shared package 讓所有專案調用。而透過一些 monorepo 的工具,在 build image 的時候就會把用到的 util function 以及 library 打包進去,就不需要再透過 npm package 的方式引入。</p>
<p>operating 方面,更新版本就是把一個 repository 的 dev 進到 staging,這一個動作就會包含這次所有的變動。<br />
CI/CD 設定則可以透過模組化的方式,讓需要類似流程的專案使用同樣的一個 workflow,如果需要更改某個流程只要更改那個流程的設定,這樣就會直接套用到那些專案上面。</p>
<h3 id="%E5%B7%AE%E7%95%B0"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/monorepo/#%E5%B7%AE%E7%95%B0">#</a> 差異</h3>
<p>開發環境上,polyrepo 建立上較不花資源,比如說今天只要開發 web 前後端,就只需要兩個 repository 就可以了,其餘不相關的專案就不需要一起 clone。但是在 monorepo 下,不論需要一個還是多個專案,都要把所有的原始碼都 clone 下來。如果在 git hook 有跑一些全域檢查,並且條件設定沒有做好的話,可能每一次 commit 都會是漫長的等待。</p>
<p>在功能開發上的差異,筆者認為比較顯著是在後面的整合上。polyrepo 的架構,會需要將不同的專案切換到對應的 branch 才行,如果 team 之間沒有溝通好 branch naming,那麼就會需要去記專案、功能與 branch name 的對應關係,這在 local 測試或者 pre-dev site 架設上都多一份工,多一次出錯的機會。有的時候一些 bug 只是因為某個專案並不是在正確的 branch 上。<br />
而 monorepo 因為所有團隊都是在同一個 branch 上開發,就不要再費心力去讓所有的專案都在對的 branch 上,local 測試或者 pre-dev 只要確認是在 feature branch 上即可。功能要上線時,只要把 feature branch 進入主線即可。</p>
<p>在原始碼維護上,polyrepo 可能會出現相同或者類似的程式碼在多個專案都出現,這從 util function 到與資料庫對接的程式碼都有。這些程式碼可能連命名都一樣,但是在不同專案可能會相同或者不同,這在開發遇到跨專案時,可能會造成問題。筆者就曾經遇到名稱相同的 function 在不同專案接收 argument 的方式不同而有 bug,還需要 trace 到最底層才發現是 argument 的問題。<br />
不過在 monorepo 的架構下,可以把 util function 以及比較底層的程式碼都放在 shared package,這樣除了可以減少重複的程式碼,也可以讓這些底層 function 的使用方式一致,在開發上就比較不容易出現因為這種命名相同但是使用方式不同的狀況。</p>
<p>而 operating 方面,最顯著的就是花在更新版本的人工時間大幅減少。polyrepo 下筆者需要到所有的 repo 檢查並且進版本;而在 monorepo 下僅需在一個 repository 裡進版本即可,不過這是建立在 monorepo 建立時較為複雜的前期設置,需要用到比較多的開發工具,以及較為複雜的 workflow condition,不過這些前期投資筆者認為是值得的。</p>
<p>雖然這樣看起來,筆者好像認為 monorepo 與 polyrepo 相較是前者較優,不過這也是有各種考量的。筆者認為首先 monorepo 的一些好處是建立在 monorepo 建立時較為繁瑣的前置設定,筆者公司在導入之時,除了評估,也是由資深的工程師測試各項設定,統整不同專案的設定,找出各個專案可以共通的基本設定,前前後後花好長一段時間才完成初期設定並且開始慢慢將舊專案導入。<br />
以及目前公司的結構還算扁平,每個開發人員的權限相近,不會有哪些專案只允許特定開發人員的狀況,如果有專案權限管理著需求,monorepo 就不會是個好選擇,因為只要在 monorepo 裡的專案,基本上能看到 monorepo 的人都可以去檢視並且修改。</p>
<h2 id="%E7%B5%90%E8%AA%9E"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/monorepo/#%E7%B5%90%E8%AA%9E">#</a> 結語</h2>
<p>不知道各位的公司或者組織是使用 monorepo 還是 polyrepo,還是兩者都有。<br />
筆者認為自己能夠在入職第一年就體驗到這兩種不同的管理方式是滿新奇的體驗,希望這篇簡介以及個人意見能讓各位對於 monorepo 以及 polyrepo 有一些理解,也歡迎在留言指正以及分享。感謝!</p>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/monorepo/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<p><a href="https://en.wikipedia.org/wiki/Monorepo">wiki</a></p>
a simple RESTful api with Go
2022-01-01T00:00:00Z
https://blog.errorbaker.tw/posts/ruofan/go-RESTful-api/
<!-- summary -->
<p>迎接嶄新的一年,一起透過實作簡單的 restful api 來認識 Golang 吧!</p>
<!-- summary -->
<p>什麼是 Golang ? 以下是 Golang 官方文件 上的說明。</p>
<blockquote>
<p>Go is an open source programming language supported by Google</p>
</blockquote>
<blockquote>
<p>Go is expressive, concise, clean, and efficient. Its concurrency mechanisms make it easy to write programs that get the most out of multicore and networked machines, while its novel type system enables flexible and modular program construction. Go compiles quickly to machine code yet has the convenience of garbage collection and the power of run-time reflection. It's a fast, statically typed, compiled language that feels like a dynamically typed, interpreted language.</p>
</blockquote>
<h4 id="install"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/go-RESTful-api/#install">#</a> install</h4>
<p>在 <a href="https://go.dev/dl/">官方網站</a> 安裝完成後,可以透過 <code>$ go version</code> 來確認是否安裝完成以及安裝的版本。</p>
<pre class="language-bash"><code class="language-bash"><span class="token variable">$go</span> version<br />go version go1.17.5 darwin/amd64</code></pre>
<h4 id="init-project"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/go-RESTful-api/#init-project">#</a> init project</h4>
<p>新增一個檔案夾, example: <code>go-project</code> ,接著在新增的檔案夾 <code>go mod init go-project</code><br />
這邊完成了初始化 project,並且會產生一個 <code>go.mod</code> 的檔案。</p>
<pre class="language-bash"><code class="language-bash"><span class="token variable">$go</span> mod init go-project<br />go: creating new go.mod: module go-project</code></pre>
<p><code>go.mod</code> 檔案會紀錄 所有的 package,舉例來說:安裝了 <code>gorilla/mux</code> 這個 package。</p>
<pre class="language-bash"><code class="language-bash"><span class="token variable">$go</span> get <span class="token parameter variable">-u</span> github.com/gorilla/mux<br />go get: added github.com/gorilla/mux v1.8.0</code></pre>
<p>這時進入 <code>go.mod</code> 檔案會看到這邊紀錄了 <code>gorilla/mux</code> 最新的版本。</p>
<pre class="language-go"><code class="language-go">module <span class="token keyword">go</span><span class="token operator">-</span>project<br /><br /><span class="token keyword">go</span> <span class="token number">1.17</span><br /><br />require github<span class="token punctuation">.</span>com<span class="token operator">/</span>gorilla<span class="token operator">/</span>mux v1<span class="token punctuation">.</span><span class="token number">8.0</span> <span class="token comment">// indirect</span></code></pre>
<h4 id="packages-in-go"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/go-RESTful-api/#packages-in-go">#</a> packages in Go</h4>
<p>新增一個 <code>main.go</code> 的檔案來實作。<br />
這邊第一行可以看到使用了 <code>package main</code> ,在不同的檔案中都會定義 <code>package packagename</code> ,而這邊的 <code>main</code> 是執行 <code>go application</code> 的入口點。</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">package</span> main</code></pre>
<p>接著引入 <code>fmt package</code> 試著在 <code>main function</code> 印出資料。</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">package</span> main<br /><br /><span class="token keyword">import</span> <span class="token punctuation">(</span><br /> <span class="token string">"fmt"</span><br /><span class="token punctuation">)</span><br /><br /><span class="token keyword">func</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> fmt<span class="token punctuation">.</span><span class="token function">Println</span><span class="token punctuation">(</span><span class="token string">"Hello, Modules!"</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<p>在 <code>command line</code> 輸入 <code>go run main.go</code> 就會在 terminal 印出 <code>Hello, Modules!</code><br />
下一步,先來定義這次會用到的 data 結構。<br />
從下方程式碼中,可以看到我們定義了 <code>Movie struct</code> 包含了 3 個 fields 以及一個 <code>Director struct</code>。</p>
<ol>
<li>這邊透過 <code>Struct Tags</code> 來定義,讓輸出的資料 id, isbn, title, director 為 Json 中的 key。</li>
<li><code>*Director</code> 透過宣告一個 pointer 變數讓他的 type 是 <code>Director struct</code>。</li>
</ol>
<pre class="language-go"><code class="language-go"><span class="token keyword">package</span> main<br /><br /><span class="token keyword">import</span> <span class="token punctuation">(</span><br /> <span class="token string">"encoding/json"</span><br /><span class="token punctuation">)</span><br /><br /><span class="token keyword">type</span> Movie <span class="token keyword">struct</span> <span class="token punctuation">{</span><br /> ID <span class="token builtin">string</span> <span class="token string">`json:"id"`</span><br /> Isbn <span class="token builtin">string</span> <span class="token string">`json:"isbn"`</span><br /> Title <span class="token builtin">string</span> <span class="token string">`json:"title"`</span><br /> Director <span class="token operator">*</span>Director <span class="token string">`json:"director"`</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">type</span> Director <span class="token keyword">struct</span> <span class="token punctuation">{</span><br /> Firstname <span class="token builtin">string</span> <span class="token string">`json:"firstname"`</span><br /> Lastname <span class="token builtin">string</span> <span class="token string">`json:"lastname"`</span><br /><span class="token punctuation">}</span></code></pre>
<h4 id="%E5%AF%A6%E4%BD%9C-main-func"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/go-RESTful-api/#%E5%AF%A6%E4%BD%9C-main-func">#</a> 實作 main func</h4>
<ol>
<li>透過 <code>append function</code> 來新增兩筆假資料進入 <code>movies array</code>。</li>
<li>透過 <code>net/http package</code> 中的 <code>ListenAndServe function starts an HTTP server</code>
<ul>
<li><code>func ListenAndServe(addr string, handler Handler) error</code> 第一個參數接收一個 type 是 <code>string</code> 的 addr, 第二個參數接收一個 type 是 <code>Handler</code> 的 handler 處理進來的 request。</li>
</ul>
</li>
<li><code>gorilla/mux package</code> 中 <code>mux.Router</code>會讓進來的 request 對應到正確的 router,並且會去 call 對應到的 route 的 handler。
<ul>
<li><code>r := mux.NewRouter()</code> 在這邊可以看到 <code>:=</code> 代表同時做了宣告和賦值兩件事。</li>
</ul>
</li>
<li><code>log.Fatal(http.ListenAndServe(":8000", r))</code> 如果有非預期的錯誤出現,在這邊會透過 <code>log.Fatal()</code> 印出來。</li>
<li>透過 <code>r.HandleFunc()</code> 來實作 restful api。</li>
</ol>
<pre class="language-go"><code class="language-go"><span class="token keyword">import</span> <span class="token punctuation">(</span><br /> <span class="token string">"encoding/json"</span><br /> <span class="token string">"fmt"</span><br /> <span class="token string">"log"</span><br /> <span class="token string">"net/http"</span><br /><br /> <span class="token string">"github.com/gorilla/mux"</span><br /><span class="token punctuation">)</span><br /><br /><span class="token keyword">type</span> Movie <span class="token keyword">struct</span> <span class="token punctuation">{</span><br /> ID <span class="token builtin">string</span> <span class="token string">`json:"id"`</span><br /> Isbn <span class="token builtin">string</span> <span class="token string">`json:"isbn"`</span><br /> Title <span class="token builtin">string</span> <span class="token string">`json:"title"`</span><br /> Director <span class="token operator">*</span>Director <span class="token string">`json:"director"`</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">type</span> Director <span class="token keyword">struct</span> <span class="token punctuation">{</span><br /> Firstname <span class="token builtin">string</span> <span class="token string">`json:"firstname"`</span><br /> Lastname <span class="token builtin">string</span> <span class="token string">`json:"lastname"`</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">var</span> movies <span class="token punctuation">[</span><span class="token punctuation">]</span>Movie<br /><br /><span class="token keyword">func</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> r <span class="token operator">:=</span> mux<span class="token punctuation">.</span><span class="token function">NewRouter</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /> movies <span class="token operator">=</span> <span class="token function">append</span><span class="token punctuation">(</span>movies<span class="token punctuation">,</span> Movie<span class="token punctuation">{</span>ID<span class="token punctuation">:</span> <span class="token string">"1"</span><span class="token punctuation">,</span> Isbn<span class="token punctuation">:</span> <span class="token string">"438227"</span><span class="token punctuation">,</span> Title<span class="token punctuation">:</span> <span class="token string">"Emily in Paris"</span><span class="token punctuation">,</span> Director<span class="token punctuation">:</span> <span class="token operator">&</span>Director<span class="token punctuation">{</span>Firstname<span class="token punctuation">:</span> <span class="token string">"Michael"</span><span class="token punctuation">,</span> Lastname<span class="token punctuation">:</span> <span class="token string">"Amodio"</span><span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">)</span><br /> movies <span class="token operator">=</span> <span class="token function">append</span><span class="token punctuation">(</span>movies<span class="token punctuation">,</span> Movie<span class="token punctuation">{</span>ID<span class="token punctuation">:</span> <span class="token string">"2"</span><span class="token punctuation">,</span> Isbn<span class="token punctuation">:</span> <span class="token string">"438228"</span><span class="token punctuation">,</span> Title<span class="token punctuation">:</span> <span class="token string">"The Silent Sea"</span><span class="token punctuation">,</span> Director<span class="token punctuation">:</span> <span class="token operator">&</span>Director<span class="token punctuation">{</span>Firstname<span class="token punctuation">:</span> <span class="token string">"Hang-yong"</span><span class="token punctuation">,</span> Lastname<span class="token punctuation">:</span> <span class="token string">"Choi"</span><span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">)</span><br /><br /> r<span class="token punctuation">.</span><span class="token function">HandleFunc</span><span class="token punctuation">(</span><span class="token string">"/movies"</span><span class="token punctuation">,</span> getMovies<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Methods</span><span class="token punctuation">(</span><span class="token string">"GET"</span><span class="token punctuation">)</span><br /> r<span class="token punctuation">.</span><span class="token function">HandleFunc</span><span class="token punctuation">(</span><span class="token string">"/movies/{id}"</span><span class="token punctuation">,</span> getMovie<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Methods</span><span class="token punctuation">(</span><span class="token string">"GET"</span><span class="token punctuation">)</span><br /> r<span class="token punctuation">.</span><span class="token function">HandleFunc</span><span class="token punctuation">(</span><span class="token string">"/movies"</span><span class="token punctuation">,</span> createMovie<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Methods</span><span class="token punctuation">(</span><span class="token string">"POST"</span><span class="token punctuation">)</span><br /> r<span class="token punctuation">.</span><span class="token function">HandleFunc</span><span class="token punctuation">(</span><span class="token string">"/movies/{id}"</span><span class="token punctuation">,</span> updateMovie<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Methods</span><span class="token punctuation">(</span><span class="token string">"PUT"</span><span class="token punctuation">)</span><br /> r<span class="token punctuation">.</span><span class="token function">HandleFunc</span><span class="token punctuation">(</span><span class="token string">"/movies/{id}"</span><span class="token punctuation">,</span> deleteMovie<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Methods</span><span class="token punctuation">(</span><span class="token string">"DELETE"</span><span class="token punctuation">)</span><br /><br /> fmt<span class="token punctuation">.</span><span class="token function">Printf</span><span class="token punctuation">(</span><span class="token string">"Starting server at port 8000\n"</span><span class="token punctuation">)</span><br /> log<span class="token punctuation">.</span><span class="token function">Fatal</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span><span class="token function">ListenAndServe</span><span class="token punctuation">(</span><span class="token string">":8000"</span><span class="token punctuation">,</span> r<span class="token punctuation">)</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<h4 id="%E5%AF%A6%E4%BD%9C-handler-func"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/go-RESTful-api/#%E5%AF%A6%E4%BD%9C-handler-func">#</a> 實作 handler func</h4>
<ol>
<li>
<p><code>getMovies func</code> 的 type 是 <code>http.HandlerFunc</code> ,在 <code>http package</code> 文件上對 <code>type HandlerFunc</code> 的介紹是 :</p>
<blockquote>
<p>an adapter to allow the use of ordinary functions as HTTP handlers</p>
</blockquote>
<p>因此在這邊可以使用 <code>ServeHTTP()</code> 的方法,從相關的 <a href="https://github.com/golang/go/blob/f8a8a73096a4d36ce7d35e9643db89e669bbee1f/src/net/http/server.go#L2076">source code</a> 可以看到這邊實作的方式相當簡潔。</p>
<ul>
<li>透過 <code>encoding/json package</code> 的 <code>func NewEncoder(w io.Writer) *Encoder</code> 方法讓 struct 解析成 Json。</li>
</ul>
</li>
<li>
<p><code>deleteMovie func</code> 中可以看到使用了 <code>gorilla/mux package</code> 的 <code>mux.Vars()</code> 方法取得我們在 route 設定的 id。</p>
<ul>
<li>在迴圈中透過 <code>range array</code>,我們可以拿到 array 中的 index 跟 value。</li>
<li>這邊的 <code>movies[:index]</code> 透過 <code>slices</code> 方法將目標 index 移除。</li>
</ul>
</li>
<li>
<p><code>createMovie func</code> 中 <code>var movie Movie</code> 這邊宣告了一個 type 是 <code>Movie struct</code> 的變數 movie。</p>
<ul>
<li>透過 <code>strconv package</code> 中的 <code>strconv.Itoa()</code> 方法將 integer 轉成 string,搭配 <code>math/rand package</code> 的 <code>rand.Intn()</code> 方法產生亂數來製造出 id。</li>
</ul>
</li>
</ol>
<pre class="language-go"><code class="language-go"><span class="token keyword">package</span> main<br /><br /><span class="token keyword">import</span> <span class="token punctuation">(</span><br /> <span class="token string">"encoding/json"</span><br /> <span class="token string">"fmt"</span><br /> <span class="token string">"log"</span><br /> <span class="token string">"math/rand"</span><br /> <span class="token string">"net/http"</span><br /> <span class="token string">"strconv"</span><br /> <span class="token string">"github.com/gorilla/mux"</span><br /><span class="token punctuation">)</span><br /><br /><span class="token keyword">type</span> Movie <span class="token keyword">struct</span> <span class="token punctuation">{</span><br /> ID <span class="token builtin">string</span> <span class="token string">`json:"id"`</span><br /> Isbn <span class="token builtin">string</span> <span class="token string">`json:"isbn"`</span><br /> Title <span class="token builtin">string</span> <span class="token string">`json:"title"`</span><br /> Director <span class="token operator">*</span>Director <span class="token string">`json:"director"`</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">type</span> Director <span class="token keyword">struct</span> <span class="token punctuation">{</span><br /> Firstname <span class="token builtin">string</span> <span class="token string">`json:"firstname"`</span><br /> Lastname <span class="token builtin">string</span> <span class="token string">`json:"lastname"`</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">var</span> movies <span class="token punctuation">[</span><span class="token punctuation">]</span>Movie<br /><br /><br /><span class="token keyword">func</span> <span class="token function">getMovies</span><span class="token punctuation">(</span>w http<span class="token punctuation">.</span>ResponseWriter<span class="token punctuation">,</span> r <span class="token operator">*</span>http<span class="token punctuation">.</span>Request<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> w<span class="token punctuation">.</span><span class="token function">Header</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Set</span><span class="token punctuation">(</span><span class="token string">"Content-Type"</span><span class="token punctuation">,</span> <span class="token string">"application/json"</span><span class="token punctuation">)</span><br /> json<span class="token punctuation">.</span><span class="token function">NewEncoder</span><span class="token punctuation">(</span>w<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Encode</span><span class="token punctuation">(</span>movies<span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">func</span> <span class="token function">deleteMovie</span><span class="token punctuation">(</span>w http<span class="token punctuation">.</span>ResponseWriter<span class="token punctuation">,</span> r <span class="token operator">*</span>http<span class="token punctuation">.</span>Request<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> w<span class="token punctuation">.</span><span class="token function">Header</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Set</span><span class="token punctuation">(</span><span class="token string">"Content-Type"</span><span class="token punctuation">,</span> <span class="token string">"application/json"</span><span class="token punctuation">)</span><br /> params <span class="token operator">:=</span> mux<span class="token punctuation">.</span><span class="token function">Vars</span><span class="token punctuation">(</span>r<span class="token punctuation">)</span><br /> <span class="token keyword">for</span> index<span class="token punctuation">,</span> item <span class="token operator">:=</span> <span class="token keyword">range</span> movies <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> item<span class="token punctuation">.</span>ID <span class="token operator">==</span> params<span class="token punctuation">[</span><span class="token string">"id"</span><span class="token punctuation">]</span> <span class="token punctuation">{</span><br /> movies <span class="token operator">=</span> <span class="token function">append</span><span class="token punctuation">(</span>movies<span class="token punctuation">[</span><span class="token punctuation">:</span>index<span class="token punctuation">]</span><span class="token punctuation">,</span> movies<span class="token punctuation">[</span>index<span class="token operator">+</span><span class="token number">1</span><span class="token punctuation">:</span><span class="token punctuation">]</span><span class="token operator">...</span><span class="token punctuation">)</span><br /> <span class="token keyword">break</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /> json<span class="token punctuation">.</span><span class="token function">NewEncoder</span><span class="token punctuation">(</span>w<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Encode</span><span class="token punctuation">(</span><span class="token punctuation">(</span>movies<span class="token punctuation">)</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">func</span> <span class="token function">getMovie</span><span class="token punctuation">(</span>w http<span class="token punctuation">.</span>ResponseWriter<span class="token punctuation">,</span> r <span class="token operator">*</span>http<span class="token punctuation">.</span>Request<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> w<span class="token punctuation">.</span><span class="token function">Header</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Set</span><span class="token punctuation">(</span><span class="token string">"Content-Type"</span><span class="token punctuation">,</span> <span class="token string">"application/json"</span><span class="token punctuation">)</span><br /> params <span class="token operator">:=</span> mux<span class="token punctuation">.</span><span class="token function">Vars</span><span class="token punctuation">(</span>r<span class="token punctuation">)</span><br /> <span class="token keyword">for</span> <span class="token boolean">_</span><span class="token punctuation">,</span> item <span class="token operator">:=</span> <span class="token keyword">range</span> movies <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> item<span class="token punctuation">.</span>ID <span class="token operator">==</span> params<span class="token punctuation">[</span><span class="token string">"id"</span><span class="token punctuation">]</span> <span class="token punctuation">{</span><br /> json<span class="token punctuation">.</span><span class="token function">NewEncoder</span><span class="token punctuation">(</span>w<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Encode</span><span class="token punctuation">(</span>item<span class="token punctuation">)</span><br /> <span class="token keyword">return</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">func</span> <span class="token function">createMovie</span><span class="token punctuation">(</span>w http<span class="token punctuation">.</span>ResponseWriter<span class="token punctuation">,</span> r <span class="token operator">*</span>http<span class="token punctuation">.</span>Request<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> w<span class="token punctuation">.</span><span class="token function">Header</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Set</span><span class="token punctuation">(</span><span class="token string">"Content-Type"</span><span class="token punctuation">,</span> <span class="token string">"application/json"</span><span class="token punctuation">)</span><br /> <span class="token keyword">var</span> movie Movie<br /> <span class="token boolean">_</span> <span class="token operator">=</span> json<span class="token punctuation">.</span><span class="token function">NewDecoder</span><span class="token punctuation">(</span>r<span class="token punctuation">.</span>Body<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Decode</span><span class="token punctuation">(</span><span class="token operator">&</span>movie<span class="token punctuation">)</span><br /> movie<span class="token punctuation">.</span>ID <span class="token operator">=</span> strconv<span class="token punctuation">.</span><span class="token function">Itoa</span><span class="token punctuation">(</span>rand<span class="token punctuation">.</span><span class="token function">Intn</span><span class="token punctuation">(</span><span class="token number">1000000</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br /> movies <span class="token operator">=</span> <span class="token function">append</span><span class="token punctuation">(</span>movies<span class="token punctuation">,</span> movie<span class="token punctuation">)</span><br /> json<span class="token punctuation">.</span><span class="token function">NewEncoder</span><span class="token punctuation">(</span>w<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Encode</span><span class="token punctuation">(</span>movie<span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">func</span> <span class="token function">updateMovie</span><span class="token punctuation">(</span>w http<span class="token punctuation">.</span>ResponseWriter<span class="token punctuation">,</span> r <span class="token operator">*</span>http<span class="token punctuation">.</span>Request<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> w<span class="token punctuation">.</span><span class="token function">Header</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Set</span><span class="token punctuation">(</span><span class="token string">"Content-Type"</span><span class="token punctuation">,</span> <span class="token string">"application/json"</span><span class="token punctuation">)</span><br /> params <span class="token operator">:=</span> mux<span class="token punctuation">.</span><span class="token function">Vars</span><span class="token punctuation">(</span>r<span class="token punctuation">)</span><br /> <span class="token keyword">for</span> index<span class="token punctuation">,</span> item <span class="token operator">:=</span> <span class="token keyword">range</span> movies <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> item<span class="token punctuation">.</span>ID <span class="token operator">==</span> params<span class="token punctuation">[</span><span class="token string">"id"</span><span class="token punctuation">]</span> <span class="token punctuation">{</span><br /> movies <span class="token operator">=</span> <span class="token function">append</span><span class="token punctuation">(</span>movies<span class="token punctuation">[</span><span class="token punctuation">:</span>index<span class="token punctuation">]</span><span class="token punctuation">,</span> movies<span class="token punctuation">[</span>index<span class="token operator">+</span><span class="token number">1</span><span class="token punctuation">:</span><span class="token punctuation">]</span><span class="token operator">...</span><span class="token punctuation">)</span><br /> <span class="token keyword">var</span> movie Movie<br /> <span class="token boolean">_</span> <span class="token operator">=</span> json<span class="token punctuation">.</span><span class="token function">NewDecoder</span><span class="token punctuation">(</span>r<span class="token punctuation">.</span>Body<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Decode</span><span class="token punctuation">(</span><span class="token operator">&</span>movie<span class="token punctuation">)</span><br /> movie<span class="token punctuation">.</span>ID <span class="token operator">=</span> params<span class="token punctuation">[</span><span class="token string">"id"</span><span class="token punctuation">]</span><br /> movies <span class="token operator">=</span> <span class="token function">append</span><span class="token punctuation">(</span>movies<span class="token punctuation">,</span> movie<span class="token punctuation">)</span><br /> json<span class="token punctuation">.</span><span class="token function">NewEncoder</span><span class="token punctuation">(</span>w<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Encode</span><span class="token punctuation">(</span>movie<span class="token punctuation">)</span><br /> <span class="token keyword">return</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">func</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> r <span class="token operator">:=</span> mux<span class="token punctuation">.</span><span class="token function">NewRouter</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /> movies <span class="token operator">=</span> <span class="token function">append</span><span class="token punctuation">(</span>movies<span class="token punctuation">,</span> Movie<span class="token punctuation">{</span>ID<span class="token punctuation">:</span> <span class="token string">"1"</span><span class="token punctuation">,</span> Isbn<span class="token punctuation">:</span> <span class="token string">"438227"</span><span class="token punctuation">,</span> Title<span class="token punctuation">:</span> <span class="token string">"Emily in Paris"</span><span class="token punctuation">,</span> Director<span class="token punctuation">:</span> <span class="token operator">&</span>Director<span class="token punctuation">{</span>Firstname<span class="token punctuation">:</span> <span class="token string">"Michael"</span><span class="token punctuation">,</span> Lastname<span class="token punctuation">:</span> <span class="token string">"Amodio"</span><span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">)</span><br /> movies <span class="token operator">=</span> <span class="token function">append</span><span class="token punctuation">(</span>movies<span class="token punctuation">,</span> Movie<span class="token punctuation">{</span>ID<span class="token punctuation">:</span> <span class="token string">"2"</span><span class="token punctuation">,</span> Isbn<span class="token punctuation">:</span> <span class="token string">"438228"</span><span class="token punctuation">,</span> Title<span class="token punctuation">:</span> <span class="token string">"The Silent Sea"</span><span class="token punctuation">,</span> Director<span class="token punctuation">:</span> <span class="token operator">&</span>Director<span class="token punctuation">{</span>Firstname<span class="token punctuation">:</span> <span class="token string">"Hang-yong"</span><span class="token punctuation">,</span> Lastname<span class="token punctuation">:</span> <span class="token string">"Choi"</span><span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">)</span><br /><br /> r<span class="token punctuation">.</span><span class="token function">HandleFunc</span><span class="token punctuation">(</span><span class="token string">"/movies"</span><span class="token punctuation">,</span> getMovies<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Methods</span><span class="token punctuation">(</span><span class="token string">"GET"</span><span class="token punctuation">)</span><br /> r<span class="token punctuation">.</span><span class="token function">HandleFunc</span><span class="token punctuation">(</span><span class="token string">"/movies/{id}"</span><span class="token punctuation">,</span> getMovie<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Methods</span><span class="token punctuation">(</span><span class="token string">"GET"</span><span class="token punctuation">)</span><br /> r<span class="token punctuation">.</span><span class="token function">HandleFunc</span><span class="token punctuation">(</span><span class="token string">"/movies"</span><span class="token punctuation">,</span> createMovie<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Methods</span><span class="token punctuation">(</span><span class="token string">"POST"</span><span class="token punctuation">)</span><br /> r<span class="token punctuation">.</span><span class="token function">HandleFunc</span><span class="token punctuation">(</span><span class="token string">"/movies/{id}"</span><span class="token punctuation">,</span> updateMovie<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Methods</span><span class="token punctuation">(</span><span class="token string">"PUT"</span><span class="token punctuation">)</span><br /> r<span class="token punctuation">.</span><span class="token function">HandleFunc</span><span class="token punctuation">(</span><span class="token string">"/movies/{id}"</span><span class="token punctuation">,</span> deleteMovie<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Methods</span><span class="token punctuation">(</span><span class="token string">"DELETE"</span><span class="token punctuation">)</span><br /><br /> fmt<span class="token punctuation">.</span><span class="token function">Printf</span><span class="token punctuation">(</span><span class="token string">"Starting server at port 8000\n"</span><span class="token punctuation">)</span><br /> log<span class="token punctuation">.</span><span class="token function">Fatal</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span><span class="token function">ListenAndServe</span><span class="token punctuation">(</span><span class="token string">":8000"</span><span class="token punctuation">,</span> r<span class="token punctuation">)</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /></code></pre>
<p>打開 Insomnia call api 看一下吧!<br />
<img src="https://blog.errorbaker.tw/img/posts/ruofan/go-2.png" alt="" /></p>
<h2 id="%E5%B0%8F%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/go-RESTful-api/#%E5%B0%8F%E7%B5%90">#</a> 小結</h2>
<p>在實作過程中發現有相當多可以延伸與深入學習的地方,整體來說相當有趣!<br />
在閱讀文章時如果有遇到什麼問題,或是有什麼建議,都歡迎留言告訴我,謝謝。😃</p>
<ul>
<li><a href="https://github.com/ruofanwei/go-rest-api">Github | Repo: go-rest-api</a></li>
</ul>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/go-RESTful-api/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li><a href="https://golang.google.cn/doc/articles/wiki/">Document | Writing Web Applications</a></li>
<li><a href="https://pkg.go.dev/net/http#Handler">Document | http</a></li>
<li><a href="https://go.dev/ref/spec#Short_variable_declarations">Document | Short variable declarations</a></li>
<li><a href="https://golangbot.com/arrays-and-slices/">golangbot | Arrays and Slices</a></li>
<li><a href="https://www.digitalocean.com/community/tutorials/how-to-use-struct-tags-in-go">digitalocean | How To Use Struct Tags in Go</a></li>
<li><a href="https://www.educative.io/edpresso/what-is-go-lang-func-newencoderw-iowriter--starencoder">Blog | What is Go lang func NewEncoder(w io.Writer) *Encoder?</a></li>
<li><a href="https://www.alexedwards.net/blog/an-introduction-to-handlers-and-servemuxes-in-go">Blog | An Introduction to Handlers and Servemuxes in Go</a></li>
<li><a href="https://eager.io/blog/go-and-json/">Blog | Go and JSON</a></li>
</ul>
等等我還想學 CLI - 微進階的 CLI
2022-01-02T00:00:00Z
https://blog.errorbaker.tw/posts/benben/04-cli/
<!-- summary -->
<!-- 師父領進門,修行在個人,你的 CLI 是否還有在修行? -->
<!-- summary -->
<p><img src="https://i.imgur.com/PfIQ0Lq.png" alt="window terminal" /></p>
<blockquote>
<p>圖片來源:我的 window terminal (imgur)</p>
</blockquote>
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/04-cli/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<p>(因為工作太忙,拖稿中)</p>
<p>大家安安,時間好快啊!已經 11 月底了,再一個月,今年又要過完了!</p>
<p>Lidemy 第五期的求職期到 12/12 ,也要來到最後的句點了,不少人應該也在工作了吧</p>
<p>筆者的新工作也剛上線,前身是設計公司,主要產品是 3D/VR 的,之前的案子確實是外包,最近在招自己的前端團隊,應該也算是創新(?)</p>
<p>所以整個前端的發展都還沒有一個流程,主管前端技術其實也懂的不深(在幾次的問答中發現),但他也知道(因為他以前都是做 3D 建模的),所以說有問題都可以問同事,有一位前端的同事很厲害,所以目前有問題都會跟他討論,這邊的人都還不錯,非常熱心。</p>
<p>累的地方當然也是有,因為前端的部門剛開始,所以資源比較少,也沒什麼舊的案子可以看,案子都是近期的,框架一下 react 、一下 vue 的,裡面亂到不行,例如 styles 裡面同時有 <code>.css</code>, <code>.scss</code>, <code>.sass</code> 檔(一般是不會這樣寫吧?),但之後確定是用 vue / pug / sass 跟 babylon.js ( WebGL 3D 框架) ,後端是 php 搭配 laravel 框架。</p>
<p>因為是接案公司,所以也要快比較快上手,他只給我一個禮拜的適應期,之後就丟一個專案來了,這一個禮拜我要學公司的流程、Vue、babylon.js,還好我之前有先看一下 Vue,但還是有自主加班多留下來學一點,因為同事也都有留下,有一點責任制的概念 XD</p>
<p>這樣好嗎?我覺得是好的,身為 junior 能多學就多學,不要太在意薪水(但也不要太誇張就好),你能解決的問題越多、價值也就越高,等你待個一、兩年,能力有了,自然會有人找上門挖角,這時候你就知道是誰吃虧了(X),所以眼光要放長遠的啦 XDD</p>
<p>不說了,我假日還在看公司的文件,要繼續去顛覆公司了(?)</p>
<blockquote>
<p>以下正題</p>
</blockquote>
<h2 id="%E5%B8%AB%E7%88%B6%E9%A0%98%E9%80%B2%E9%96%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/04-cli/#%E5%B8%AB%E7%88%B6%E9%A0%98%E9%80%B2%E9%96%80">#</a> 師父領進門</h2>
<p>一開始,會接觸 CLI (Command-Line interface)應該是在 Lidemy 的中,最初的 week1,當中老師簡單的介紹 CLI 的操作,原來只用鍵盤就可以做這邊多事!我對此深深著迷,因為離 <strong>心中想像中的駭客</strong> 又更接近了,你說有點中二(?)我敢說你小時候一定也想像過,但有時候就是這種微不足道的小動力反而一直推著你前進。</p>
<p>但是沒有師父領進門,你也不會自己去研究這些東西,沒有國文老師還會自己去學數學、沒有數學老師還會自己去學公民,隔行如隔山啊,如果你想要惹毛學校的老師,照我上面的去問看看,一定會被老師打。</p>
<p>我很幸運,剛好也喜歡 CLI 操作又有師父帶領,所以稍為理解 CLI 的一些指令,下面也會介紹一下我認識的 CLI,當然如果有懂比較多的朋友,也可以交流一下,我會很開心的:D</p>
<p>一開始的接觸的就是下面這門課,其實看試看的就很夠了。</p>
<blockquote>
<p>延伸學習:<a href="https://www.lidemy.com/p/cmd101-command-line">Command Line 超新手入門 | Lidemy</a></p>
</blockquote>
<h2 id="%E5%85%88%E6%B1%82%E8%83%BD%E7%94%A8%E5%B0%B1%E5%A5%BD"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/04-cli/#%E5%85%88%E6%B1%82%E8%83%BD%E7%94%A8%E5%B0%B1%E5%A5%BD">#</a> 先求能用就好</h2>
<p>工作上其實用滑鼠就可以做到很多事了,但有時候非 CLI 不可的時候,你就會覺得「書到用時方恨少」,例如:需要遠端操作主機的時候(好吧,純前端工程師可能真的不太用到)。</p>
<p>總之先來看看基礎的指令吧!</p>
<ul>
<li>cd<br />
切換資料夾路徑</li>
<li>ls<br />
查看當前路徑資料夾有什麼檔案或資料夾</li>
<li>pwd<br />
查看當前路徑位置</li>
<li>touch(window 可能不適用,可以考慮先使用 git bash)<br />
新增檔案</li>
</ul>
<p>這幾個指令會非常非常常用到,所以一定要記下來,但依筆者的學習過程,我認為:先掌握基本的指令就好,然後再慢慢增加指令,是最有效率的學習法!</p>
<p>例如:一次教你一堆指令,不用一個禮拜,兩天後你就全忘光了,那不如就先記幾個常用的吧,我覺得這也是老師故意的安排,只帶我們認識幾個常用的,而且在第一周就交了,為的就是讓我們提早熱悉 CLI ,因為之後會大量用到,越早熱悉越好!</p>
<p>最最最常用的應該就是 <code>ls</code> 了,剛學習時,一進來資料夾就先 ls 看一下有什麼內容,cd 後再 ls 一下,沒事就 ls 一下,漸漸的就記下來了!</p>
<p>還有一個不錯用的指令:<code>whoami</code> ,在自己電腦上使用可能無感,但如果是公司的電腦、有很多使用者的話,這個指令可以查看當前的使用者是誰,簡單說就是讓你的電腦知道「他爸是誰」,一不開心就問電腦 <code>whoami</code> ,他永遠不會反抗你的!</p>
<p>很多指令都是有意議的,看完上面的 <code>whoami</code> ,是不是不用特別記,你下次就記得了,是吧?</p>
<h2 id="%E4%BF%AE%E8%A1%8C%E5%9C%A8%E5%80%8B%E4%BA%BA"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/04-cli/#%E4%BF%AE%E8%A1%8C%E5%9C%A8%E5%80%8B%E4%BA%BA">#</a> 修行在個人</h2>
<p>再來之後會用到 CLI 的地方大概就是專案了吧,很多文件也都會寫一點基本的用法。</p>
<p>例如:</p>
<pre class="language-bash"><code class="language-bash"><span class="token comment"># 開啟專案</span><br /><span class="token function">npm</span> run start<br /><br /><span class="token comment"># 開發專案</span><br /><span class="token function">npm</span> run <span class="token function">watch</span><br /><br /><span class="token punctuation">..</span>.</code></pre>
<p>其實這些都算 CLI 的指令,甚至各個工具、框架也都有自己的 CLI ,如:<code>vue-cli</code>, <code>git-bash</code>, <code>docker-cli</code>, <code>mysql-cli</code> ... 等</p>
<p>你說這麼多都要學也太累了吧!好在除了特別的功能不一樣,大多通用的功能指令都是一樣的,如上面的:ls、cd、pwd、touch 等等。</p>
<p>另外這幾個也是開發中非常好用的指令:</p>
<ul>
<li>mv<br />
移動檔案</li>
<li>copy<br />
複製檔案</li>
<li>rm<br />
刪除檔案</li>
</ul>
<h2 id="%E5%BE%AE%E9%80%B2%E9%9A%8E%E7%9A%84-cli"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/04-cli/#%E5%BE%AE%E9%80%B2%E9%9A%8E%E7%9A%84-cli">#</a> 微進階的 CLI</h2>
<p>上以的基本的部分都熟悉了,就可以尋找自己喜歡的指令,然後慢慢擴充他,很像玩遊戲練技能的感覺,哇今天又多學了一個指令,感覺像是多學了一個技能,這樣才能一直往前,不會固定在這邊。</p>
<p>再來我要分享幾個好用(但還是因人而異啦 XD)指令,也是我邊東看看西看看學來的</p>
<ul>
<li>open/start (Mac/Windows)<br />
開啟檔案</li>
<li>code .<br />
開啟 VScode</li>
<li>shutdown<br />
關機(shutdown -s -t 10)、登出(shutdown -l)</li>
<li>exit<br />
關閉 terminal (你說我點個 X 不就好了,信不信我打完 exit 比你使用滑鼠精準地點到那個小小 X 還快,不信?好吧,我以前也不信,但學起來你就會信了)</li>
</ul>
<p>另外下面這兩個都可以拆好多好多好多好多文章講了,要深可以深到馬里亞納海溝了 ...</p>
<ul>
<li>git</li>
<li>vim</li>
</ul>
<p>但筆者也非常喜歡這兩個指令呢!他們底下分別又多了很多的指令,目前也是使用 VScode 搭配 vim 插件在開發,水真的深,但使用熟悉了,長期下來開發效率會提升很多的(而且還很潮?)</p>
<p>如果也有興趣的朋友也可以 google 一下這兩個關鍵字,應該也是很多資源,之是都很摧眠就是了,例如:git 只會 pull/push 的話真的很可惜,還有很多強大的功能、vim 的話,可能只有會用的人才知道他的好</p>
<blockquote>
<p>延伸學習:<a href="https://git-scm.com/doc">Git - document</a><br />
延伸學習:<a href="https://www.vim.org/docs.php">Vim - document</a></p>
</blockquote>
<h2 id="%E7%B8%BD%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/04-cli/#%E7%B8%BD%E7%B5%90">#</a> 總結</h2>
<p>以上稍為簡單地介紹了一些 CLI ,當然這水還很深,等著你我去探索,雖然我也還只是個小小的 junior ,但我認為熟悉 CLI 是前往 senior 的門票之一,除了效率之外,CLI 也是跨入後端的必備技能,所以當然不能只停留在這!</p>
<p>但這邊多指令又該先學哪個呢?我覺得開心就好!</p>
<p>為什麼要學這個?因為我開心、我喜歡這個,比起盲目的去學一堆,我認為無壓力去學一個 <code>whoami</code> 指令開心多了,你說是吧?</p>
<h2 id="ref"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/04-cli/#ref">#</a> Ref</h2>
<ul>
<li><a href="http://linux.vbird.org/">鳥哥私房菜</a></li>
<li><a href="https://git-scm.com/doc">Git - document</a></li>
<li><a href="https://www.vim.org/docs.php">Vim - document</a></li>
</ul>
<blockquote>
<p>免責聲名</p>
</blockquote>
<p>以上均為筆者自身經驗,難免小有主觀意見,供讀者們參考,也歡迎分享經驗交流。<br />
如果有錯誤的地方還請大大們指正,筆者會立刻修改,再次感謝大家!</p>
<p><a href="https://creativecommons.org/licenses/by/4.0/"><img src="https://i.creativecommons.org/l/by/4.0/88x31.png" alt="Creative Commons License" /></a></p>
<p>本著作係採用 <a href="https://creativecommons.org/licenses/by/4.0/">創用 CC 姓名標示 4.0 國際授權條款</a> 授權。您可以在 <a href="https://benben.me/">benben.me</a> 找到我。</p>
<p>This work is licensed under a <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>. You can find me at <a href="https://benben.me/">benben.me</a></p>
從與工程師共事,到與工程師共識(ㄧ)
2022-01-09T00:00:00Z
https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer/
<!-- summary -->
<p>寫給正準備踏入軟體相關產業,第一次準備與工程師共事,卻又對程式一頭霧水的你</p>
<!-- summary -->
<!-- more -->
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<p>這篇文章,主要不是寫給工程師看的,而是寫給其他需要與工程師合作的朋友們。<br />
工程師一向難搞?愛用技術名詞來欺壓?每次遇到問題都甩鍋或說做不出來?不然就是永遠都客戶的問題?<br />
跟工程師合作怎麼好像自己都被欺負?怎麼樣理解這些人在想什麼?</p>
<p>這篇文章,帶你搞懂最基本的技術名詞,理解網頁運作邏輯,<s>防止被工程師欺負</s><br />
並且告訴你如何與工程師有效率的溝通,使團隊運作更加順暢。</p>
<p>這篇主要以跟「網頁工程師」合作為例,若是其他領域的工程師(像是 App 工程師、區塊鏈工程師),需要熟悉的領域會有不同,但核心的理念其實是差不多的。</p>
<h2 id="%E7%AC%AC%E9%9B%B6%E6%AD%A5%EF%BC%9A%E5%BF%83%E6%85%8B%E5%BB%BA%E7%AB%8B"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer/#%E7%AC%AC%E9%9B%B6%E6%AD%A5%EF%BC%9A%E5%BF%83%E6%85%8B%E5%BB%BA%E7%AB%8B">#</a> 第零步:心態建立</h2>
<p>初次碰到程式,腦海中浮現很多問題是正常的,不論是 PM 或工程師。<br />
尤其是大量的技術名詞,在溝通的時候往往可能造成很大的障礙。明明在討論這個登入功能怎麼做,工程師嘴裡卻冒出個:「這個 token 怎樣....,那個 token 怎樣....」。蛤?什麼是 token?心裡不斷 OS 到底要不要開口問?不問的話我整個會議基本上都聽不懂,問了又耽誤大家時間,要是前輩解釋完我一樣聽不懂怎麼辦?算了我先記下來,待會私底下去查好了。</p>
<p>這樣的情況不曉得大家熟不熟悉?其實新的技術名詞只會更多不會更少,我們沒有辦法確保讓自己一定能知道所有的名詞是什麼意思,就算今天我都知道了,明天可能又有新的技術出現,又有不同的名詞準備要去理解。我們會害怕聽不懂的東西,我們不希望造成別人困擾,但是總是得讓自己有效的與其他人溝通,今天聽不懂沒關係,如何能讓自己下次能聽懂?</p>
<p>其實不論何種技術,它總得運行在類似的環境上面,就算有 100 種寫網頁的方式,最後一樣得運行在瀏覽器上面,所以理解瀏覽器勢必能理解 80% 會出現在瀏覽器上的問題。<br />
所以琳琅满目的技術名詞,它都有它的歸類,只要我們清楚它屬於哪個範疇的問題,就能運用既有的理解,將其容納並吸收進我們的腦海當中。</p>
<p>下面我會針對初次接觸網頁應用程式,會需要具備的基礎知識,做簡易的說明,並提供我認為不錯的參考資源,讓大家能以比較輕鬆的方式,理解工程師口中常常提到的技術名詞,它是在做什麼?</p>
<p>透過生活中類似的概念做比喻,你會發現其實存在很多相似之處,而且我們都理解它,當我們可以把這些技術名詞與我們既有的觀念做連結後,下次再遇到它,就不會再害怕了。<br />
我們反而會很有自信的運用著這些技術名詞,來與工程師做溝通。</p>
<h2 id="%E7%AC%AC%E4%B8%80%E6%AD%A5%EF%BC%9A%E5%BB%BA%E7%AB%8B%E5%B0%8D%E7%B6%B2%E9%A0%81%E7%9A%84%E7%86%9F%E6%82%89%E5%BA%A6"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer/#%E7%AC%AC%E4%B8%80%E6%AD%A5%EF%BC%9A%E5%BB%BA%E7%AB%8B%E5%B0%8D%E7%B6%B2%E9%A0%81%E7%9A%84%E7%86%9F%E6%82%89%E5%BA%A6">#</a> 第一步:建立對網頁的熟悉度</h2>
<blockquote>
<p>常聽到的技術名詞:<br />
☞ 瀏覽器、devtool、切版</p>
</blockquote>
<p>瀏覽器其實就是一個可以執行程式的軟體,就好像電視可以接收訊號並播放出來一樣。<br />
差別在於瀏覽器可以接收程式碼,並且依照程式碼撰寫的內容,將畫面印出來給你看,也可以透過程式的設定,讓使用者與畫面上的元件做互動。</p>
<p>我們在轉電視的時後,切換第幾台第幾台,可以看到對應頻道的節目。<br />
在瀏覽器上,則是透過網址,讓瀏覽器幫我們發送請求到對應的網址,取得對應伺服器所回傳過來的程式碼內容,瀏覽器會幫我們把程式碼運行起來,我們就能在自己的瀏覽器上看到完整的網頁內容。</p>
<p>瀏覽器不是什麼程式語言都能看懂,它可以看懂的程式碼有三種 <code>HTML</code>、<code>CSS</code>、<code>JavaScript</code>,分別掌管著網頁的 <code>元件</code>、<code>樣式</code>、<code>互動</code>,我們在網站上能看到的內容,有文字、圖片、按鈕、輸入框,任何看得到的元件,都由 <code>HTML</code> 建立的,而這些元件長的什麼樣子,是圓的扁的?黑的白的?有框沒框?只要是樣式,都是由 <code>CSS</code> 決定。最後就是瀏覽器與使用者的互動,我們打字或點選按鈕的反應,都是由 <code>JavaScript</code> 來處理。</p>
<p>以上講歸講,我帶大家直接操作瀏覽器,用最實際的例子去理解,上面講的內容實際上跑起來會是什麼樣子(請大家以 Chrome 瀏覽器來操作,才會看到跟我相同的畫面唷~):</p>
<p><strong><font color="#00FFFF">👉 請大家對著我點滑鼠右鍵 👈</font></strong>,並且選擇「檢查」(大家放心,電腦一定不會壞掉)</p>
<p>大家會看到下面這個很大的框框,這個就是工程師常說的 <code>DevTool</code> 開發者工具。<br />
<img src="https://blog.errorbaker.tw/img/posts/xiang/cooperate-with-engineer-01.png" alt="" /></p>
<p>圖中左邊紅框框的部分,就是 <code>HTML</code>、右半邊藍框框的部分,就是 <code>CSS</code>,這些就是瀏覽器所取得的程式碼,HTML 決定了畫面上的元件,所以我們網頁上看到的「請大家對著我點滑鼠右鍵」這串文字,會跟紅框框裡的那段中文文字一模一樣,而看到的 <font color="#00FFFF">青藍色</font> 這個顏色,就會跟右邊 CSS 設定的一樣了。</p>
<p>所以網頁上的內容,是透過接收到的 <code>HTML</code> 及 <code>CSS</code>,由瀏覽器幫我們畫出來的。<br />
所以工程師口中說的 <code>切版</code>,指的就是將設計稿上的畫面透過程式碼切出來。</p>
<p>只要是網頁上的元素都可以透過滑鼠右鍵檢查,來查看 Devtool 的內容,<br />
大家不妨嘗試看看到各個自己常用的網站,在任意元件點滑鼠右鍵,按下檢查,看看那些網站的畫面都是透過哪些 <code>HTML</code>、<code>CSS</code> 所產生的。</p>
<p>關於瀏覽器的部分,僅需要知道網頁的畫面是透過 <code>HTML</code> 及 <code>CSS</code> 呈現即可,不需要深入到瞭解如何去撰寫程式碼的語法,知道上面這些內容以後,該如何用來與工程師溝通,我在下面 「<strong>第三步:如何制伏工程師</strong>」 的地方會提到。</p>
<p>OK!聰明的你此時可能會想問,那 <code>JavaScript</code> 呢?怎麼沒有提到怎麼看 <code>JavaScript</code> 的程式碼?<br />
好的,大家不要著急,不是因為從 Devtool 看不到 <code>JavaScript</code>,而是因為沒有必要去看。因為 <code>HTML</code> 及 <code>CSS</code> 掌管了畫面上的元素跟樣式,身為與工程師共事的朋友,可能是 PM 或者設計師,有很高的機率會需要提供畫面上發現的錯誤,與工程師進行溝通或修正,但是通常不會遇到一種情況,是需要提供 <code>JavaScript</code> 的截圖,告訴工程師說哪裡寫得有問題。因為 <code>JavaScript</code> 定義的會是一些跟使用者的互動,或者網頁的一些反應,我們只需要在操作的時候紀錄發現的錯誤就好,找出 <code>JavaScript</code> 的錯誤,屬於工程師自己的責任,就讓他們自己去煩惱就好囉~</p>
<p>這邊提供一些相關文章,大家可以簡單對一些技術名詞有個粗淺的印象就好,不用理解太深,之後有遇到再回頭來查就可以了:</p>
<ul>
<li>更加瞭解 HTML: <a href="https://developer.mozilla.org/zh-TW/docs/Learn/Getting_started_with_the_web/HTML_basics">https://developer.mozilla.org/zh-TW/docs/Learn/Getting_started_with_the_web/HTML_basics</a></li>
<li>更加瞭解 CSS: <a href="https://developer.mozilla.org/zh-TW/docs/Learn/Getting_started_with_the_web/CSS_basics">https://developer.mozilla.org/zh-TW/docs/Learn/Getting_started_with_the_web/CSS_basics</a></li>
<li>瀏覽器與網頁架構: <a href="https://noootown.com/fontend-engineer-should-know-network-structure/">https://noootown.com/fontend-engineer-should-know-network-structure/</a></li>
<li>開發者工具: <a href="https://ithelp.ithome.com.tw/articles/10207782">https://ithelp.ithome.com.tw/articles/10207782</a><br />
這篇收錄了更多 Devtool 可以使用或查看的細節,若只是想與工程師溝通,對於 Devtool 的瞭解度即時不用那麼深,底下會提到更多這個部分的內容,然後告訴大家哪些是重要的哪些不是。</li>
</ul>
<h2 id="%E7%AC%AC%E4%BA%8C%E6%AD%A5%EF%BC%9A%E5%BB%BA%E7%AB%8B%E5%B0%8D%E7%A8%8B%E5%BC%8F%E7%9A%84%E7%86%9F%E6%82%89%E5%BA%A6"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer/#%E7%AC%AC%E4%BA%8C%E6%AD%A5%EF%BC%9A%E5%BB%BA%E7%AB%8B%E5%B0%8D%E7%A8%8B%E5%BC%8F%E7%9A%84%E7%86%9F%E6%82%89%E5%BA%A6">#</a> 第二步:建立對程式的熟悉度</h2>
<blockquote>
<p>常聽到的技術名詞:<br />
☞ 前端、後端、框架、套件、測試機、正式機、api、Cache、CI/CD</p>
</blockquote>
<p>這邊會是程式初學者最常遇到問題的地方,最多的技術名詞也出現於此,我在撰寫文章的同時,也不斷在思考該如何呈現這個段落,才能讓大家在尋找答案或累積知識的同時,更加方便快速。</p>
<p>下面我會將技術名詞列點,若需要快速針對特定的技術名詞找尋答案,可以直接跳著看。若是沒有時間壓力,建議直接按照排定的順序由上到下一一認識這些名詞。</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer/">☞ 程式語言</a></strong><br />
程式語言只是機器看得懂的代碼,就好像我們人跟人用語言溝通一樣,程式語言就是我們與機器溝通的語言。<br />
不同的機器能看懂的代碼不一樣,就好像跟日本人說話得講日文,跟英國人說話得用英文。我可以用日文跟服務生說:「請給我一杯水」,如果對方是日本人,我就能得到我要的那杯水,因為對方聽得懂。那我如果是對著英國人說呢?那可能很高機率什麼都不會發生,因為我用了錯誤的語言來溝通。同理,機器也是一樣的,我在瀏覽器就得使用 <code>JavaScript</code> 這種程式語言,不然它看不懂。</p>
<p>程式語言還有哪些:<a href="http://ms2.ctjh.ntpc.edu.tw/~luti/108-2-7grade-week005.htm">http://ms2.ctjh.ntpc.edu.tw/~luti/108-2-7grade-week005.htm</a></p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer/">☞ 機器、部署</a></strong><br />
而程式碼寫好了我們要放哪裡呢?<br />
放在任何能夠讓瀏覽器取得的地方即可。也就是說,只要這個地方,能夠接收瀏覽器發送的請求,並且在收到請求的時候,回傳寫好程式碼的檔案給瀏覽器,這個地方就可以放置程式碼。通常就是我們常講的 <code>機器</code>。而把程式碼放上這部 <code>機器</code> 的這個動作,我們叫做 <code>部署(deploy)</code>。</p>
<p>一旦程式碼部署上機器以後,假設我們忽略這台機器可能做的任何限制,那任何一台電腦,都可以透過瀏覽器,向這台機器發送取得程式碼檔案的請求。<br />
也就是說,我們兩個人,使用同樣的網址,都會向同一台機器發送請求,並且取得一模一樣的程式碼檔案(或資料)。而這份檔案,會分別運行在我們兩個人的瀏覽器上。<br />
也就是說,如果我在我的電腦上發現某一個功能壞掉了,那你使用跟我相同的步驟操作,應該也可以在你的電腦上看到有相同的情況,因為是同樣的程式碼,所以會存在相同的錯誤。</p>
<p>瀏覽器如何發送請求:<a href="https://yakimhsu.com/project/project_w4_Network_http.html">https://yakimhsu.com/project/project_w4_Network_http.html</a></p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer/">☞ 更版、測試機、正式機</a></strong><br />
上面提到了,機器上的程式碼有錯誤,那任何人都會取得有錯誤的程式碼,所以工程師就得維修,把維修之後正確的程式碼,建立新的版本,部署上機器。<br />
這個動作就叫做 <code>更版</code>。更版完成之後,機器上有了正確的程式碼,在這之後所有人透過瀏覽器向機器發送請求,取得的程式碼就會是新的版本正確的程式碼。</p>
<p>那我們不一定只有在維修的時候會 <code>更版</code>,有時候像是有新功能要發佈,我們也會做 <code>更版</code> 的動作。<br />
那新的功能我們總得讓測試人員先測試過一遍,確認沒有問題以後再發佈給民眾使用,所以我們得先把程式碼部署到一台只給測試人員使用的機器,我們把它叫做 <code>測試機</code>。<br />
這台測試機可以提供測試人員對於即將發佈的版本進行測試,等測試與修復動作都完成了以後,再把最後完成的版本,部署上給民眾使用的那台機器,也就是 <code>正式機</code>。</p>
<p>這邊要特別提一下測試的重要性,不論是由 PM 進行測試,或者由專職的測試人員進行測試。測試這個動作都可以有效避免正式機上的功能發生錯誤。而且網站的瀏覽人次越多,這個錯誤影響的層面就越嚴重。如果一個網站 1 小時只會有 1~2 個人造訪,那可能有錯誤也不太會被發現,但是如果一個網站 1 分鐘就會有 10 萬人同時造訪,那一丁點錯誤都可能導致大量使用者產生不滿,整個團隊就要緊急號招趕緊來救火了。</p>
<p>在此感謝所有測試人員,你們真的很重要!</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer/">☞ 前端、後端</a></strong><br />
這個 bug 到底是前端的?還是後端?<br />
這個內心掙扎天天都在上演,我何時才能搞懂前後端的差異,自行判斷 issue 該找誰處理?這個問題呢...我最後再來回答。</p>
<p>在講前端後端如何區分以前,先給大家一個情境:<br />
我們走進一間餐廳,坐下來準備點餐,服務員走了過來,詢問我們要點些什麼。你指了指菜單中的濃湯,說你想要一份濃湯,此時服務員說好,並且走向廚房,向廚房內部傳達了需要一份濃湯的消息。過了一會兒,廚房那遞出了一份濃湯,服務員便將濃湯端到你的面前。類似的場景相信大家再熟悉不過,而這個簡單的情境,正好跟網頁的前後端運作,有很相似的關聯。</p>
<p>我們跟服務生溝通,就猶如我們在操作瀏覽器一般,我們看得到服務生,<s>我們摸得到服務生</s>(咦...不可以色色),我們可以清楚看見這個正在服務我們的對象,這就是 <code>前端</code>,它有幾個非常非常明顯的特徵:</p>
<ul>
<li>你一定看得到它</li>
<li>每個人看到的一定是一樣的</li>
<li>它能運行在每個人電腦的瀏覽器</li>
<li>負責與使用者溝通</li>
<li>你只能透過它幫你傳遞你所需要的東西</li>
</ul>
<p>所以瀏覽器看到的任何元件、按鈕、介面,這些通通都屬於 <code>前端</code>,你一定摸得到它,每個人看到的介面會相同,你也只能透過它幫你發送請求。<br />
餐廳的服務生也是,我們可以看到很多個穿著相同制服的服務生,穿梭在客人之前,他們會向「同一個廚房」溝通。<br />
那廚房裡頭到底長什麼樣子,它如何幫你製作餐點,這些都是你看不到的,你也無法直接跟廚房溝通,這就是 <code>後端</code>,他也有幾個非常明顯的特徵:</p>
<ul>
<li>你一定看不到它</li>
<li>每個人得到的東西不一定一樣</li>
<li>它只運行在它自己部署的唯一的那一台機器</li>
<li>負責存放資料</li>
<li>正常情況,你無法直接向它傳遞你的需求</li>
</ul>
<p>我們操作網站,網站會幫我們跟 <code>後端</code> 部署的那一台機器溝通,向 <code>後端</code> 發送、取得資料,並且將資料透過 <code>前端</code> 的處理,呈現在畫面上。所以這些資料到了 <code>後端</code> 以後發生了什麼?<code>後端</code> 如何處理資料並回傳給 <code>前端</code>,我們都看不到。正常情況下,我們也無法跳過 <code>前端</code>,直接與 <code>後端</code> 進行溝通。</p>
<p>不知道大家有沒有吃過「永和豆漿」?有時候我們點完餐之後,會聽到類似的吶喊聲:「ㄧ套、兩燒蛋、三溫漿」,這是什麼意思?「一份燒餅油條 + 兩份燒餅夾蛋 + 三杯溫豆漿」。但這段訊息是員工之間彼此溝通的一種方式,只是我們一般人聽不懂就是了。<br />
所以我們操作 <code>前端</code> 的介面,<code>前端</code> 也會用類似的方式,將我們想傳達給 <code>後端</code> 的請求,透過 <code>後端</code> 聽得懂的方式,傳達給 <code>後端</code>。意思是假設我們知道「ㄧ套、兩燒蛋、三溫漿」廚房聽得懂,下一次我們可不可以直接向廚房大喊「ㄧ套、兩燒蛋、三溫漿」?可以呀!<s>可是廚房不一定會理你</s>。</p>
<p>所以,<code>前端</code> 只是使用者與 <code>後端</code> 溝通的一個媒介,<code>後端</code> 幫我們存放資料,我們希望取得資料的時候,點選 <code>前端</code> 頁面上的元件,<code>前端</code> 就會幫我們向 <code>後端</code> 發送取得資料的請求。</p>
<p>好,事情總不可能永遠順利,程式也不可能永遠沒有 bug。<br />
假設遇到問題的時候,究竟如何判斷是前端的錯誤還是後端的錯誤?</p>
<p>我們回到餐廳裡,再一次向服務員點餐,這次遇到了幾個不同的情境:</p>
<ol>
<li>你指了指菜單中的濃湯,說你想要一份濃湯,此時服務員說好,但是!他沒幫你把這個消息傳達給廚房,你沒喝到你想要的濃湯。這是服務員的錯?還是廚房的錯?</li>
<li>你指了指菜單中的濃湯,說你想要一份濃湯,此時服務員說好,服務員也幫你跟廚房說了,但是!廚房沒有幫你做!這是服務員的錯?還是廚房的錯?</li>
</ol>
<p>以上兩個情境,你做了一樣的事情,得到了同樣的結果,向服務員點餐,但湯沒有來。<br />
一個是服務員有幫你向廚房傳遞消息,另一個沒有。<br />
很明顯第一種情況一定是服務員的錯,他不講廚房怎麼可能知道。但是如果他講了,但廚房不做,那肯定是廚房的問題了。</p>
<blockquote>
<p>所以判斷問題到底是「服務生」還是「廚房」的關鍵,就在於判斷:</p>
<ol>
<li>「服務生」有沒有準確幫你向「廚房」傳遞需求</li>
<li>「廚房」有沒有準確拿給「服務生」正確的餐點</li>
<li>「服務生」取得餐點後是否有正確將餐點端至客人面前</li>
</ol>
</blockquote>
<blockquote>
<p>所以判斷問題到底是 <code>前端</code> 還是 <code>後端</code> 的關鍵,就在於判斷:</p>
<ol>
<li><code>前端</code> 有沒有準確幫你向 <code>後端</code> 發送請求</li>
<li><code>後端</code> 有沒有準確向 <code>前端</code> 回傳正確的資料</li>
<li><code>前端</code> 取得資料後是否有正確將資料呈現於畫面</li>
</ol>
</blockquote>
<p>只要我們能弄清楚,這個功能 <code>前端</code> 負責了什麼?<code>後端</code> 負責了什麼?並且能在發生錯誤的時候,判斷出是 <code>前端</code> 沒有發送正確的請求?還是 <code>後端</code> 沒有回傳正確的資料?前端取得資料後沒有正常呈現於畫面上?那我們就能確認這個問題究竟該請誰來做處理了。</p>
<p>實際舉個例子:<br />
假設你現在在測試一個網站的編輯頁面,你發現編輯完成之後按了「儲存」,資料卻沒有更新。現在要來釐清是 <code>前端</code> 的問題還是 <code>後端</code> 的問題?</p>
<p>第一步:<code>前端</code> 有沒有準確幫你向 <code>後端</code> 發送請求?<br />
先確認 <code>前端</code> 是否有將編輯過後,正確的資料發送到後端?如果沒有發送,或者發送的資料錯誤,那就可以確認是 <code>前端</code> 的問題。如果到這邊為止都是正常的,那再進入下一步。</p>
<p>第二步:<code>後端</code> 有沒有準確向 <code>前端</code> 回傳正確的資料?<br />
確認 <code>後端</code> 有沒有回傳正確的資料回來?如果沒有回傳?或回傳的資料錯誤,那就可以確認是 <code>後端</code> 的問題。一樣到這邊為止都沒問題的話,再進入下一步。</p>
<p>第三步:<code>前端</code> 取得資料後是否有正確將資料呈現於畫面?<br />
確認 <code>後端</code> 有回傳正確的資料,<code>前端</code> 卻沒確實顯示在畫面,不論是畫面沒動,畫面壞掉,頁面錯誤,元件跑版,最後這些畫面上的問題,都會是 <code>前端</code> 的問題。</p>
<p>概念上有沒有很簡單?釐清問題屬於前端還是後端,就是透過這種方式而已。<br />
大家可以先從很簡單的功能應證看看,熟悉這個思考的流程以後,下次再釐清問題會更加快速。</p>
<p>上面講的都是大方向,確實我們幾乎能依照上面那套步驟來找出 80% 的問題是出自於 <code>前端</code> 還是 <code>後端</code>。<br />
但凡事總有例外,實務上碰到的情況往往比概念複雜許多。原因如下:</p>
<ul>
<li>你怎麼能判定 <code>前端</code> 發送的請求一定是對的?它的格式對不對只有 <code>後端</code> 會知道。</li>
<li>你怎麼知道 <code>後端</code> 回傳的資料一定沒有遺漏?有些欄位遺漏以後 <code>前端</code> 根本無法完成接下去的任務。</li>
<li>你怎麼知道這一個功能 <code>前端</code> 一定只會發送一次請求?有時候一個按鈕點選下去,發送請求的次數不會只有一次。</li>
</ul>
<p>上面這些問題,都是實務上會遇到讓人難以釐清問題的主因,我想要告訴大家一個觀念,</p>
<blockquote>
<p>實務上的程式,只有開發者自己知道它會如何運作。</p>
</blockquote>
<p>意思是,如果你拿了一個不是我維護的專案來問我這個是 <code>前端</code> 的錯誤?還是 <code>後端</code> 的錯誤?我會回答你:「我不知道!」<br />
因為在我看完它的程式碼,理解它運作的每個步驟以前,我也沒辦法釐清究竟是 <code>前端</code> 發送的請求錯誤?還是 <code>後端</code> 回傳的資料錯誤?<br />
如果我貿然回答你:「啊!這個就是前端的啊,因為畫面上出現亂碼,畫面上的問題都前端的。」結果最後卻發現,是 <code>後端</code> 回傳的資料少給一個欄位,導致前端無法拿到正確的資料來顯示,這樣不是白白誤會了那位前端工程師嗎(笑)。</p>
<p>講到這邊,我告訴了大家「判斷問題的方式」,也告訴了大家「只有開發者能準確判斷」,我想給大家一個認知:</p>
<blockquote>
<p>我們需要知道「前端」與「後端」的差異,與協作上的關係,但是我們不需要熟悉程式運作的每個細節</p>
</blockquote>
<p>如果今天碰到的問題,你已經能清楚它是前端的問題?還是後端的問題?那恭喜你,你學會了判斷的方法。<br />
但如果今天遇到的問題,你不曉得該給前端還後端,就直接開口問開發者!他有義務要告訴你,因為程式會如何運作只有開發者知道。</p>
<p>回答了最開頭的那個問題:「我何時才能搞懂前後端的差異,自行判斷 issue 該找誰處理?」<br />
答案是:<br />
如果是已經遇過了的問題,已經知道 <code>前端</code> 會傳發送什麼格式,<code>後端</code> 會回傳什麼資料,也知道 <code>前端</code> 該如何將資料呈現於畫面,那再遇到的時候,可以嘗試自己再判斷一次,是 <code>前端</code> 還是 <code>後端</code> 的問題。<br />
如果這問題是第一次碰到,我們一樣可以嘗試依照上面提到的步驟,釐清看看是 <code>前端</code> 還是 <code>後端</code> 的問題,但是記得要跟開發者確認,看看自己這次的判斷是不是正確的。<br />
如果長久下來發現自己的判斷有 90% 以上是準確的,那代表你已對這份專案有一定程度的熟悉了,可以自行判斷與區分前後端各自負責的工作內容。</p>
<p>補充:本章一直提到的「看發送的請求與回傳的資料是否正確」,究竟要怎麼看?等到了 API 這個技術名詞的介紹時會提到。</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer/">☞ 框架</a></strong><br />
很常聽到工程師口中提到 <code>框架</code>,<code>框架</code> 是什麼?跟蓋房子的鋼筋一樣嗎?(我一開始也是這樣以為的)<br />
但我後來發現其實 <code>框架(framework)</code> 這個東西比較像是「門派」。<br />
就好像功夫有分很多「門派」,街舞有分很多「種類」,程式也分很多「框架」。</p>
<p>很多人會問:「你街舞都跳什麼種類?Breaking 啊、Locking 啊」,就好像「你 JavaScript 都用什麼框架?React?Vue?」是一樣的感覺。<br />
之所以會有 <code>框架</code> 的出現,是因為網頁越來越複雜了,相較以往只能閱讀的網頁,現在不但可以跟使用者互動,可以有很多多媒體娛樂,有很多不同種類的功能可以實作。<br />
每位開發者都可以有自己的寫法,都能自由的發揮,那程式碼每個人寫出來的都會不一樣,就會變得很雜亂。就好像如果少林寺裡每個人打的拳法都不一樣,想怎麼打就怎麼打,想怎麼翻怎麼跳都沒有人管,那就不會有「少林十八銅人陣」這種多人配合的陣法可以使用了。</p>
<p>所以為了讓程式的多人協作變得順暢,讓開發者按照相同的模式去設計或開發程式,這套規範,或者說這套方法,就是所謂的 <code>框架</code>。當我們使用了相同的 <code>框架</code> 開發時,就會規範某個類型的檔案該放在哪裡,撰寫的時候應該遵從什麼樣的規則等等...。一旦新的專案來了,雖然程式碼我都沒看過,但至少我能知道,如果我想要找什麼類型的檔案,我可以去哪裡找。因為大家使用相同的 <code>框架</code>,會遵守一樣的開發準則,合作就會變得更容易。</p>
<p>HR 在招聘新人也是一樣的,公司想要招聘會 php 這個程式語言的新人,開發部門說,團隊主要使用的是 Laravel 這套 php 的 <code>框架</code>。那 HR 就能在篩選的時候,特別找出會 Laravel 這個 <code>框架</code> 的求職者,這樣招聘進來的新人也能在更短的時間內上手團隊當中的任務。</p>
<p>所以每種程式語言,都有屬於自己的 <code>框架(門派)</code>,<code>框架</code> 本身沒有好壞,看每個團隊自己的選擇與習慣。下次再聽到 <code>框架</code> 這個詞的時候,你就會知道現在談論的就是開發所使用的技術,是屬於什麼門派了。</p>
<p>小重點:</p>
<ul>
<li>一個 <code>框架</code> 一定只會對應一種程式語言</li>
<li>一種程式語言可以有很多個 <code>框架</code></li>
<li>同一份專案通常只使用一種 <code>框架</code>
<ul>
<li>例如:前端專案使用一種前端 <code>框架</code>,後端專案使用一種後端 <code>框架</code><br />
(當然會有例外,前端不用框架,或者同個專案存在兩個框架之類的)</li>
</ul>
</li>
</ul>
<p>框架介紹:<a href="https://ithelp.ithome.com.tw/articles/10199730">https://ithelp.ithome.com.tw/articles/10199730</a></p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer/#">☞ 套件</a></strong><br />
如果有一種東西,是別人提供的,可以直接使用的工具,可以減少開發時間,或幫你處理掉麻煩的手續以及步驟,你說好不好?<br />
就像是「氣炸鍋」、「氣炸烤箱」,它讓我們不必親自將食物下鍋烹煮,不用翻、不用炒、不用煮,你只需要把食物關進去之後按開始,等它 叮!就煮好了,非常快速方便。</p>
<p><code>套件 Library</code> 就是這麼樣的一個東西,別人提供寫好的一段程式,可能是一段功能,或者一個元件,只要是別人提供,可以讓使用者引用進自己的專案裡頭使用的,都可以稱作 <code>套件</code>,這樣使用者就可以少寫一段功能,或少製作一個元件,減少開發時程。</p>
<p><code>套件</code> 這個東西怎麼聽起來這麼好?偷懶神器啊!<br />
全部都拿別人製作好的來用不就好了?我幹嘛還自己做?</p>
<p>確實日常開發會用到許許多多的 <code>套件</code> 來協助,但不是什麼都可以直接拿 <code>套件</code> 來處理,原因有幾個:</p>
<ul>
<li>這個 <code>套件</code> 既然是別人提供的,那如果哪天壞了,連我自己都沒辦法修</li>
<li>這個 <code>套件</code> 不一定能滿足我想要的所有需求,比如說我覺得它顏色太醜,但我可能也沒辦法改</li>
</ul>
<p>所以方便的背後,也隱藏著風險。<br />
像前陣子最大的資安風暴「<a href="https://tech-blog.cymetrics.io/posts/huli/what-is-log4j-and-log4shell/">Log4Shell 漏洞</a>」,就是由於 Java 的一個套件,出現安全漏洞,導致全世界所有使用這個套件的程式,通通陷入資安風險當中。堪稱近 10 年最大資安威脅,美國資安主管機關出面要求立即修正。</p>
<p>當然不是所有 <code>套件</code> 都是不好的,只是我們在選擇使用的時候,必須要謹慎考量到它對於我們專案的安全性,是否有人監督?發生問題是否會及時修正?是否能滿足我們使用的需求,這些都要經過謹慎的評估以及考量。</p>
<p>原則上能自己製作的東西,還是自己做最好,就好比「氣炸烤箱」沒有這麼萬能,否則米其林主廚早就每個人都在使用了。之所以這些大廚還是會親自料理,就是因為這項工具沒辦法完美呈現出他希望達到的效果。</p>
<p>程式開發也是相同的,剛開始為了快速達成某項需求,可能先以某些 <code>套件</code> 作為工具下去使用,久而久之,功能越來越強大了,原先 <code>套件</code> 再也滿足不了新的需求,就得把它捨棄掉,尋找新的 <code>套件</code>,或者自己親自開發製作。</p>
<p>這樣大家應該就可以了解,下次如果有聽到開發團隊在討論要使用或者捨棄某項 <code>套件</code>,代表著什麼意思了。</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer/">☞ API、request、response</a></strong><br />
如果你有看完上面全部的介紹,你會發現我為 <code>API</code> 這個章節埋下了很多的伏筆。<br />
這單元如此重要的原因是,如果把 <code>API</code> 搞清楚,會非常大幅度的貼近開發者的思維,你會看懂開發上很多的邏輯,也能釐清許多開發上遇到的問題。大大提升與開發者溝通的效率,也能增進自己對程式理解的自信。</p>
<p>那 <code>API(Application Programming Interface)</code> 是什麼?簡單來說:</p>
<blockquote>
<p><code>API</code> 就是個拿來交換資料的東西</p>
</blockquote>
<p>前端跟後端交換資料,透過 <code>API</code>; 網頁要串接 Facebook 或 Google 登入的功能,透過 <code>API</code>。<br />
所以傳遞、交換資料的這個東西,我們就把它叫做 <code>API</code>。</p>
<p>不曉得大家有沒有注意到「Interface 介面」這個字?<br />
它就好比是「窗口」,或者是「使用介面」的概念。</p>
<p>而這個 <code>API</code> 由誰來提供?由「擁有資料」的那個人提供。<br />
像餐廳的廚房,可能會跟服務生說,「湯都會由 A 窗口遞出」,「菜都會由 B 窗口遞出」。<br />
這個提供餐點的廚房,開了兩個窗口給服務生使用。</p>
<p>所以:</p>
<ul>
<li>後端要提供資料給前端,後端要建立 <code>API</code></li>
<li>Google 要提供串接服務,Google 要建立 <code>API</code></li>
</ul>
<p>假設後端想要提供「取得資料」以及「儲存資料」兩種功能給前端,<br />
它就要分別建立「取得資料」以及「儲存資料」用的兩支 <code>API</code>。<br />
所以前端就透過後端建立的 <code>API</code> 來發送請求,而後端也透過 <code>API</code> 來回傳資料。</p>
<p>大家就能明白,<code>API</code> 的構成一定包含著兩個東西:「請求 與 回覆」、「<code>request</code> & <code>response</code>」<br />
前端發送一個 <code>request</code> 給後端,後端回傳一個 <code>response</code> 給前端,這就是一支 api 所做的事情。<br />
它的特徵如下:</p>
<ul>
<li><code>API</code> 一定會包含 <code>request</code>,跟 <code>response</code></li>
<li>request 一定採用固定的格式</li>
<li>response 只會回傳資料,沒辦法回傳畫面</li>
</ul>
<p>所謂的固定格式,就好像廚房會跟服務生說,我只接受固定的點餐格式,寫得不一樣我一概不做:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// 廚房說只看得懂標準格式</span><br />燒鴨 x <span class="token number">1</span><br />叉雞 x <span class="token number">2</span><br /><br /><span class="token comment">// 其他格式廚房一率看不懂</span><br />燒肉烤鴨飯一個<br />叉燒雞肉飯兩份</code></pre>
<p>所以後端也會訂定 <code>request</code> 的標準格式,前端必須要發送符合格式的 <code>request</code> 給後端,後端也要回傳符合格式的 <code>response</code> 給前端。<br />
若前端發送的 <code>request</code> 格式錯誤,後端就無法準確處理這份請求,若後端回傳的 <code>response</code> 格式錯誤,前端就無法將資料呈現於畫面。<br />
至於格式會如何訂定?可以是後端自己決定,也可以讓給前端來決定,或更多的時候,會交由前後端雙方一起去協調決定格式。<br />
但不論決定格式的方式為何?最終一定會將協調的結果,紀錄在 <code>API 文件</code> 當中,作為日後 <code>串接 API</code> 時的參考依據。</p>
<p>像 Google 登入功能,它也有提供公開的 API 串接文件:<a href="https://developers.google.com/identity/sign-in/web/sign-in">https://developers.google.com/identity/sign-in/web/sign-in</a></p>
<p>那我們知道 <code>API</code> 運作的概念以後,我們該如何去看 <code>API</code> 有沒有問題?判斷 <code>request</code> 的格式對不對?判斷 <code>response</code> 有沒有回傳正確的資料?<br />
一樣打開我們的「<font color="#00FFFF">Devtool</font>」(網頁任意位置點選滑鼠右鍵,選擇檢查)。</p>
<p>一樣看到這個開發者工具的白色框框,找到上方橫條有一個選項「Network」給它點下去。<br />
<img src="https://blog.errorbaker.tw/img/posts/xiang/cooperate-with-engineer-02.png" alt="" /></p>
<p>此時你會看到像下面這樣的一個畫面,請你開著這個畫面並再將網頁重新整理一次<br />
<img src="https://blog.errorbaker.tw/img/posts/xiang/cooperate-with-engineer-03.png" alt="" /></p>
<p>重新整理過後,底下會出現很多這個頁面載入的資源,我們點選如下圖中一個「Fetch/XHR」的按鈕,它會幫我們篩選出屬於 <code>API</code> 的部分,顯示於下圖的紅框處,每一條項目都代表著瀏覽器發送出去的 <code>API</code>。<br />
<img src="https://blog.errorbaker.tw/img/posts/xiang/cooperate-with-engineer-04.png" alt="" /></p>
<p>我們點選最下方的那一支 API 來參考,可以看到下圖右半邊有兩個選項可以點選,分別是「Payload」以及「Response」,我們點選其中一個選項後,會在下圖籃框處出現對應的內容:</p>
<ul>
<li>Payload 紀錄著 request 夾帶給後端的資料</li>
<li>Response 紀錄著後端回傳給前端的資料<br />
<img src="https://blog.errorbaker.tw/img/posts/xiang/cooperate-with-engineer-05.png" alt="" /></li>
</ul>
<p>如此一來我們就能夠看到瀏覽器發送出去的這支 API,它帶著什麼資料?格式是什麼?以及後端回傳了什麼資料?格式是什麼?</p>
<p>再舉一個例子:</p>
<p>假設現在有一個編輯表單的功能,表單的欄位如下圖所示,在我填答完成以後,想測試「儲存資料」這個功能是否正常:<br />
<img src="https://blog.errorbaker.tw/img/posts/xiang/cooperate-with-engineer-06.png" alt="" /></p>
<p>當我按下了儲存按鈕以後,發現瀏覽器幫我打了一支「儲存資料」的 <code>API</code> 出去,我馬上來查看打出去的 <code>request</code> 及 <code>response</code> 的內容:<br />
<img src="https://blog.errorbaker.tw/img/posts/xiang/cooperate-with-engineer-07.png" alt="" /></p>
<p>我可以看到我填答好的資料被分別轉換成了一個固定的 <code>request</code> 格式(如上圖紅色框框處):</p>
<pre class="language-js"><code class="language-js"><span class="token punctuation">{</span><br /> <span class="token literal-property property">svid</span><span class="token operator">:</span> <span class="token string">"ILoveBsx"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">survey_domain</span><span class="token operator">:</span> <span class="token string">"www.surveycake"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">api_version</span><span class="token operator">:</span> <span class="token string">"v0"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">hash_key</span><span class="token operator">:</span> <span class="token string">"ILove25sprout"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">iv_key</span><span class="token operator">:</span> <span class="token string">"ILoveBsxTeam"</span><br /><span class="token punctuation">}</span><br /><br /><span class="token comment">// 對照上面表單填寫的答案,會發現訊息內容是相同的</span></code></pre>
<p>後端也在這支 <code>API</code> 回傳了 <code>response</code> 給瀏覽器(如上圖藍色框框處):</p>
<pre class="language-js"><code class="language-js"><span class="token punctuation">{</span><br /> <span class="token literal-property property">statue</span><span class="token operator">:</span> <span class="token number">200</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">message</span><span class="token operator">:</span> <span class="token string">"successful update"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">data</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">data</span><span class="token operator">:</span> <span class="token keyword">null</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>上面的格式究竟是如何訂定的,我怎麼知道格式有沒有寫對,就要去對照 <code>API 文件</code> 記載的內容了。<br />
不過至少從以上的資料當中,我們已經可以判讀到兩個重要的資訊:</p>
<ol>
<li><code>前端</code> 有將正確的資料發送到 <code>後端</code></li>
<li><code>後端</code> 有回傳帶有成功字樣的訊息</li>
</ol>
<p>這樣就能代表這個編輯功能的 <code>API</code> 是成功的。<br />
如果這個流程你懂了,你可以在操作網頁的過程中去觀察,點選什麼按鈕,瀏覽器會幫我們發送什麼 <code>API</code>,前端會打什麼 <code>request</code> 出去?後端又會回傳什麼 <code>response</code> 回來?<br />
如果每一個功能都用這種方式去理解,整個網頁運作的原理就會在腦海中建立起來。<br />
哪天一個明明該發送 <code>API</code> 的功能卻沒有發送?或者該回傳的資料沒有回傳?就可以快速釐清大概是哪邊出了問題,可以找誰處理。</p>
<p><code>API</code> 的細節其實還有很多,發送的方式有哪些類別?發送的位置要如何定義?發送的資料該如何夾帶?發送的驗證該如何處理?<br />
這其中包含著不少實務上需要注意的細節,不過我們不用理解到這麼深。</p>
<p>我們只需要知道 <code>API</code> 是什麼?什麼情況下會用到?稍微能猜到一點不同資料欄位所代表的意義,這樣就夠了。遇到問題的時候,可以先試著看一看 Devtool 的資訊,然後跟開發者做個確認,驗證自己的理解對不對。<br />
<code>API</code> 就是個交換資料的過程,就好像在跟客戶發 Email 一樣,客戶發送給我一段訊息,我回覆客戶一段訊息。Email 也有 Email 的格式,我們也需要確認夾帶的資料是否準確。換成 <code>API</code> 以後,差別只在於我們怎麼透過手上的工具來查看這些內容罷了。</p>
<p>相信我,如果你指著 Devtool 詢問開發者關於 <code>API</code> 的問題,開發者一定會覺得「WOW ~ 你怎麼連這個都懂!」</p>
<p>從拉麵店的販賣機理解什麼是 API:<a href="https://hulitw.medium.com/ramen-and-api-6238437dc544">https://hulitw.medium.com/ramen-and-api-6238437dc544</a></p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer/">☞ Cache、Cookies、Session、token</a></strong><br />
<code>Cache</code> 是瀏覽器為了加快使用者使用網頁的速度,在第一次載入的時候,幫我們將一部分的資料拉出來透過特定的方式暫存起來,這樣下次再造訪同的網站的時候,瀏覽器就不必再將所有資源都重新載入一遍,載入速度就會比第一次快上許多。不過也因為這樣,常常出現一種狀況是:明明開發者說已經將程式碼更新了,自己電腦上看到的仍舊是舊的版本,原因就出在 <code>Cache</code> 幫我們把原先的資源暫存住了,所以更新上來的程式碼,沒有被載入進來。解決方法就是「清除快取」,或者使用無痕模式來操作網站。</p>
<p><code>Cookies</code> 跟 <code>Session</code> 大家可以簡單理解成,後端為了辨識使用者,所在瀏覽器上加附的一個狀態。讓每一次瀏覽器發送 request 的時候,帶上這個使用者的狀態,後端就能夠從這個狀態中辨認發送 request 的這個人的身份,以及它擁有什麼樣的權限。關於 <code>Cookies</code> 跟 <code>Session</code>,我推薦大家直接看這篇文章「<a href="https://hulitw.medium.com/session-and-cookie-15e47ed838bc">白話 Session 與 Cookie:從經營雜貨店開始</a>」。</p>
<p><code>token</code> 可以想像成「通行證」,為什麼要有通行證呢?假如有個功能叫做「刪除使用者」,那如果人人都可以刪,豈不是很危險?通行證的概念,就是給擁有這個權限的對象,一張屬於這個權限的通行證。這個通行證是由後端發給前端的,前端會將拿到的通行證記錄起來。後端可以決定,哪一些 API 需要經過 <code>token</code> 的驗證。例如說後端如果定義刪除使用者需要驗證 <code>token</code>,在前端沒有出示 <code>token</code>,或者 <code>token</code> 錯誤的情況下,後端都不會真的將使用者刪除,以確保資料的安全性。</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer/">☞ CI/CD</a></strong><br />
<code>CI/CD (Continuous Integration/Continuous Deployment)</code>,持續性整合/持續性部署,指的就是「自動化測試/自動化部屬」,開發者會透過一個類似「腳本」的檔案,來定義每一次更新的時候,自動化所要做的事情。就像我們寫劇本一樣,從開場到結束,每一位演員要做什麼,每一幕畫面會有哪些劇情發生。當程式碼要做更新的時候,一樣可以去定義它要執行哪些測試,要將檔案做哪些處理。</p>
<p>這些事情可不可以人工處理?當然可以!不過有自動化的好處,就是能夠有效降低人力成本,如果這些事情,每次都得做,做的事情也都一樣,那就交給機器去做吧!好的自動化測試,能夠省去很多人工測試的成本,好的自動化部屬,也能夠有效提升開發效率與品質。</p>
<hr />
<p>以上是一些剛接觸程式領域的朋友,常常會一頭霧水的名詞,或許還有很多常見的詞是我沒講到的,也或許未來還會出現更多各式各樣的新名詞。<br />
大家只要記得,同樣是網頁,同樣運作在瀏覽器,就不會脫離類似的概念,「操作介面、交換資料、顯示畫面」,當有新的名詞出現時,我們只要知道它是在哪個階段,有著什麼樣的作用,對應到原先所對累積的概念,就能快速瞭解新的技術名詞代表的意義。</p>
<h2 id="%E7%AC%AC%E4%B8%89%E6%AD%A5%EF%BC%9A%E5%A6%82%E4%BD%95%E5%88%B6%E4%BC%8F%E5%B7%A5%E7%A8%8B%E5%B8%AB%EF%BC%8C%E8%AE%93%E5%B7%A5%E7%A8%8B%E5%B8%AB%E9%96%89%E5%98%B4"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer/#%E7%AC%AC%E4%B8%89%E6%AD%A5%EF%BC%9A%E5%A6%82%E4%BD%95%E5%88%B6%E4%BC%8F%E5%B7%A5%E7%A8%8B%E5%B8%AB%EF%BC%8C%E8%AE%93%E5%B7%A5%E7%A8%8B%E5%B8%AB%E9%96%89%E5%98%B4">#</a> 第三步:如何制伏工程師,讓工程師閉嘴</h2>
<p>這裡原本會是最精彩的章節,但礙於上面累積下來的篇幅實在是太長了,我相信第一次看到這些技術名詞的你,需要花不少時間來吸收。<br />
所以決定把最精彩的章節留到了下一次再講。</p>
<p>不過,我可以先預告一下下個章節會提到的內容:</p>
<ul>
<li>工程師的思維是什麼?</li>
<li>工程師到底整天都在想什麼?</li>
<li>如何能簡單制伏工程師,讓工程師閉嘴?</li>
<li>如何從與工程師共事,到與工程師共識?</li>
</ul>
<p>除了讓大家期待一下下一篇文章,我也在這邊提醒大家,「第三步:如何制伏工程師,讓工程師閉嘴」會運用到本文所介紹到的很多技術名詞與方法。所以如果跳過這篇文章直接去看下一篇,是一定看不懂的唷~<br />
大家就乖乖把這篇文章整個看完吧(笑)。</p>
<p><a href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer-part2/">下集:傳送門</a></p>
淺談工作上遇到的 Circular Dependency
2022-01-30T00:00:00Z
https://blog.errorbaker.tw/posts/cwc329/circular-dependency/
<!-- summary -->
<p>公司專案除了導入 monorepo,同時也導入更多的 eslint rules,其中有一條 no-cycle,一加入就全部紅通通。<br />
除了 eslint,團隊現在逐步導入 unit test,也因為 circular dependency 而有問題。<br />
用這一篇文章簡單紀錄一下。</p>
<!-- summary -->
<h1 id="circular-dependency"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/circular-dependency/#circular-dependency">#</a> Circular Dependency</h1>
<h2 id="%E4%BB%80%E9%BA%BC%E6%98%AF-circular-dependency%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/circular-dependency/#%E4%BB%80%E9%BA%BC%E6%98%AF-circular-dependency%EF%BC%9F">#</a> 什麼是 Circular Dependency?</h2>
<p>circular dependency 簡單說就是在程式中,有兩個以賞的模組相互引用,導致依賴鍊變成環狀。<br />
舉個例子來說,假設有 A、B 兩個模組,兩者相互依賴就會變成下面這樣的圖。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/cwc329/circular-dependency/1.png" alt="" /></p>
<p>又或者是多個模組形成一個依賴圈。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/cwc329/circular-dependency/2.png" alt="" /></p>
<p>這樣就會形成 circular dependency,知道定義之後還需要知道這會造成什麼問題呢?<br />
這邊是個簡單的例子,可以暫時忽略為何會引入沒有使用的函式,<br />
現在有四個檔案,內容分別為:<br />
constant.js</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> h <span class="token operator">=</span> <span class="token string">'hello'</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> w <span class="token operator">=</span> <span class="token string">'world'</span><span class="token punctuation">;</span><br />module<span class="token punctuation">.</span>exports <span class="token operator">=</span> <span class="token punctuation">{</span><br /> h<span class="token punctuation">,</span> w<br /><span class="token punctuation">}</span></code></pre>
<p>hello.js</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token punctuation">{</span> h <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'./constant'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> <span class="token punctuation">{</span> world <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'./world'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">function</span> <span class="token function">hello</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>h<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><span class="token function">hello</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br />module<span class="token punctuation">.</span>exports <span class="token operator">=</span> <span class="token punctuation">{</span> hello <span class="token punctuation">}</span></code></pre>
<p>world.js</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token punctuation">{</span> w <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'./constant'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> <span class="token punctuation">{</span> hello <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'./hello'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">function</span> <span class="token function">world</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>w<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><span class="token function">world</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br />module<span class="token punctuation">.</span>exports <span class="token operator">=</span> <span class="token punctuation">{</span> world <span class="token punctuation">}</span></code></pre>
<p>main.js</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token punctuation">{</span> world <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'./world'</span><span class="token punctuation">)</span><br /><br /><span class="token function">world</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>四個檔案的依賴關係會像這樣<br />
<img src="https://blog.errorbaker.tw/img/posts/cwc329/circular-dependency/3.png" alt="" /></p>
<p>在 main.js 裡面我只想要用到 <code>world</code>,而我也預期應該不會出現 world 以外的字出現,</p>
<pre class="language-js"><code class="language-js">world<br />world</code></pre>
<p>但是當我執行 main.js 的時候會發現輸出如下</p>
<pre class="language-js"><code class="language-js"><br />hello<br />world<br />world<br /></code></pre>
<p>會多一個 hello 的輸出,這是因為在 world.js 裡面引用了 hello.js,所以會多一個輸出。<br />
而如果我只想要使用 <code>hello</code>,但是因為 circular dependency,我還是會輸出預期外的 world。<br />
這是 circular dependency 最主要的問題,會造成非預期的 side effects。</p>
<h2 id="circular-dependency-%E7%9A%84%E5%BD%B1%E9%9F%BF"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/circular-dependency/#circular-dependency-%E7%9A%84%E5%BD%B1%E9%9F%BF">#</a> Circular Dependency 的影響</h2>
<p>像是公司專案中 redis 模組和 mysql 模組就有 circular dependency 的問題,即便只需要從 mysql 拿資料,還是會啟動 redis 連線。<br />
這在 app 中會造成無謂或者預期外的行為,使得之後要除錯會有困難。</p>
<p>另外比較大的影響是在 unit test。<br />
unit test 時會依照需求 mock 一些 function call,例如在測試 controller 的時候,如果有呼叫到 model 都會 mock,因為 unit test 時不需要(也無法)真的連到資料庫拿資料。<br />
但是因為專案有 circular dependency,會有即便 mock 了依舊會執行到連線的 code,甚至連 redis 連線也開啟,導致在 CI/CD pipeline 中無法順利讓虛擬機關閉而阻塞流程。</p>
<h2 id="circular-dependency-%E7%9A%84%E8%A7%A3%E6%B1%BA%E6%96%B9%E6%B3%95"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/circular-dependency/#circular-dependency-%E7%9A%84%E8%A7%A3%E6%B1%BA%E6%96%B9%E6%B3%95">#</a> Circular Dependency 的解決方法</h2>
<p>在我們的專案中,這些 circular dependency 幾乎都是因為當初檔案位置沒有妥善規劃。<br />
我們首先 trace code,先釐清原先模組切分以及產生 circular dependency 的依賴鍊在哪裡。<br />
接著我們先依照模組切分改寫 import 的寫法,在同一個模組內的檔案,會直接指到要 import 的檔案。<br />
每個模組都建立 index.ts 檔案作為 export 的出口,其他模組要引用都是指到 index.ts 而不是實際所在的檔案。</p>
<p>而在釐清模組的同時,我們同時也將常出現的各個模組的一些底層實作分拆出去另外變成一個模組,也對整個 code base 做一次大重構。</p>
<p>重構之後,接著就是訂定相關的 coding 規範,讓大家遵守不要再犯同樣的錯誤。</p>
如何使用 react-intl + babel plugin 自動化產出多國語系檔?
2022-01-31T00:00:00Z
https://blog.errorbaker.tw/posts/tian/react-intl-automation/
<h3 id="%E8%AA%B0%E5%8F%AF%E8%83%BD%E9%81%A9%E5%90%88%E9%96%B1%E8%AE%80%E9%80%99%E7%AF%87%E6%96%87%E7%AB%A0%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/react-intl-automation/#%E8%AA%B0%E5%8F%AF%E8%83%BD%E9%81%A9%E5%90%88%E9%96%B1%E8%AE%80%E9%80%99%E7%AF%87%E6%96%87%E7%AB%A0%EF%BC%9F">#</a> 誰可能適合閱讀這篇文章?</h3>
<ul>
<li>有使用 React 實作多國語系的需求</li>
<li>想瞭解 Babel 除了將 ES6+ 轉成 ES5 還能幫助我們做些什麼?</li>
<li>如何結合 react-intl 和 babel 自動化產出語系檔</li>
</ul>
<p>如果你沒有上述需求,你可以考慮去閱讀其他夥伴們的 <a href="https://blog.errorbaker.tw/">優秀作品</a></p>
<h3 id="react-intl-%E7%B0%A1%E4%BB%8B"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/react-intl-automation/#react-intl-%E7%B0%A1%E4%BB%8B">#</a> react-intl 簡介</h3>
<p>從<a href="https://formatjs.io/">官方文件</a>可以得知, react-intl 是一套可以協助我們做多國語系的 Library</p>
<h3 id="react-intl-%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/react-intl-automation/#react-intl-%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C">#</a> react-intl 基本操作</h3>
<p>在 React 專案中執行指令 <code>yarn react-intl</code> or <code>npm install react-intl</code> 可以按照下面的程式碼做最基本的配置</p>
<ol>
<li>
<p>在元件的最外層包一個 react-intl 內建的 Context:IntlProvider,這樣被包裹的元件才能透過 useIntl 這個 custom hook 取得 formatMessage</p>
</li>
<li>
<p>formatMessage 內放的是一個 object 包含 id 和 defaultMessage,當 IntlProvider messages 中的 key 和 formateMessage 中 object 的 id 對應時,就會使用對應的詞作為翻譯。</p>
</li>
</ol>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> IntlProvider<span class="token punctuation">,</span> useIntl <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"react-intl"</span><br /><br /><span class="token keyword">function</span> <span class="token function">App</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span>IntlProvider <span class="token parameter">messages</span><span class="token operator">=></span><br /> <span class="token punctuation">{</span><span class="token comment">/* props: messages 的 test 對應到 formatMessage({ id: "test" }) */</span><span class="token punctuation">}</span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">TranslationBlock</span></span> <span class="token punctuation">/></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span><span class="token class-name">IntlProvider</span></span><span class="token punctuation">></span></span><br /> <span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><span class="token keyword">function</span> <span class="token function">TranslationBlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">{</span> formatMessage <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">useIntl</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token punctuation">{</span><span class="token function">formatMessage</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token string">"test"</span><span class="token punctuation">,</span> <span class="token literal-property property">defaultMessage</span><span class="token operator">:</span> <span class="token string">"預設訊息"</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token punctuation">{</span><span class="token comment">/* 顯示為 測試 */</span><span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /> <span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> IntlProvider<span class="token punctuation">,</span> useIntl <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"react-intl"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">function</span> <span class="token function">App</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">IntlProvider</span></span> <span class="token attr-name">messages</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token punctuation">}</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token punctuation">{</span><span class="token comment">/* messages 沒有翻譯 */</span><span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">TranslationBlock</span></span> <span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span><span class="token class-name">IntlProvider</span></span><span class="token punctuation">></span></span><br /> <span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><span class="token keyword">function</span> <span class="token function">TranslationBlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">{</span> formatMessage <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">useIntl</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token punctuation">{</span><span class="token function">formatMessage</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token string">"test"</span><span class="token punctuation">,</span> <span class="token literal-property property">defaultMessage</span><span class="token operator">:</span> <span class="token string">"預設訊息"</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token punctuation">{</span><span class="token comment">/* messages 沒有翻譯 顯示 defaultMessage 為 預設訊息 */</span><span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /> <span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<p>隨著翻譯字詞數量的增加我們會額外獨立出一個翻譯檔,再引入到 IntlProvider 中,方便集中管理</p>
<p><code>locale/zh.json</code></p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br /> <span class="token property">"test1"</span><span class="token operator">:</span> <span class="token string">"測試1"</span><span class="token punctuation">,</span><br /> <span class="token property">"test2"</span><span class="token operator">:</span> <span class="token string">"測試2"</span><span class="token punctuation">,</span><br /> <span class="token property">"test3"</span><span class="token operator">:</span> <span class="token string">"測試3"</span><span class="token punctuation">,</span><br /> <span class="token property">"test4"</span><span class="token operator">:</span> <span class="token string">"測試4"</span><br /><span class="token punctuation">}</span></code></pre>
<p><code>App.jsx</code></p>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> IntlProvider<span class="token punctuation">,</span> useIntl <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"react-intl"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> zh <span class="token keyword">from</span> <span class="token string">"../locale/zh"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> jp <span class="token keyword">from</span> <span class="token string">"../locale/jp"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> en <span class="token keyword">from</span> <span class="token string">"../locale/en"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">function</span> <span class="token function">App</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">IntlProvider</span></span> <span class="token attr-name">messages</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span>zh<span class="token punctuation">}</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">TranslationBlock</span></span> <span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span><span class="token class-name">IntlProvider</span></span><span class="token punctuation">></span></span><br /> <span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">function</span> <span class="token function">TranslationBlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">{</span> formatMessage <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">useIntl</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span><span class="token punctuation">></span></span><span class="token punctuation">{</span><span class="token function">formatMessage</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token string">"test1"</span><span class="token punctuation">,</span> <span class="token literal-property property">defaultMessage</span><span class="token operator">:</span> <span class="token string">"預設訊息1"</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span><span class="token punctuation">{</span><span class="token comment">/* 測試1 */</span><span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span><span class="token punctuation">></span></span><span class="token punctuation">{</span><span class="token function">formatMessage</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token string">"test2"</span><span class="token punctuation">,</span> <span class="token literal-property property">defaultMessage</span><span class="token operator">:</span> <span class="token string">"預設訊息2"</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span><span class="token punctuation">{</span><span class="token comment">/* 測試2 */</span><span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span><span class="token punctuation">></span></span><span class="token punctuation">{</span><span class="token function">formatMessage</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token string">"test3"</span><span class="token punctuation">,</span> <span class="token literal-property property">defaultMessage</span><span class="token operator">:</span> <span class="token string">"預設訊息3"</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span><span class="token punctuation">{</span><span class="token comment">/* 測試3 */</span><span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span><span class="token punctuation">></span></span><span class="token punctuation">{</span><span class="token function">formatMessage</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token string">"test4"</span><span class="token punctuation">,</span> <span class="token literal-property property">defaultMessage</span><span class="token operator">:</span> <span class="token string">"預設訊息4"</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span><span class="token punctuation">{</span><span class="token comment">/* 測試4 */</span><span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /> <span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<p>但很快的,隨著翻譯的語系(zh, en, jp...)和字詞的增加,就會開始遇到問題了,每一次新增翻譯和語系的時候,都必須 <strong>手動</strong> 的將語系的 <code>translation key</code> 複製出來貼到語系檔裡面才能進行翻譯,這樣不僅麻煩也很容易漏,<br />
所以我希望能採用某種自動化的機制,讓我只需要在 formatMessage 中定義好 id 和 defaultMessage,就來自動的產出語系檔,每個不同語系檔都包含對應 id 的 translation key,這樣我就只需要將注意力放在定 id 和翻譯。</p>
<p>在查了很多資料之後發現,使用 react-intl 中的 defineMessage + babel plugin 可以幫我們做到這件事情。</p>
<p>再加上 defineMessage 之後,翻譯不會並不會發生什麼變化,但使用 defineMessage 可以讓我們講使用螢光筆一樣,將待會需要 extract 的翻譯字詞標記起來。</p>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> defineMessage<span class="token punctuation">,</span> useIntl <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"react-intl"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">function</span> <span class="token function">TranslationBlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">{</span> formatMessage <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">useIntl</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token punctuation">{</span><span class="token function">formatMessage</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token string">"test1"</span><span class="token punctuation">,</span> <span class="token literal-property property">defaultMessage</span><span class="token operator">:</span> <span class="token string">"預設訊息 1"</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span><span class="token punctuation">{</span><span class="token string">" "</span><span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token punctuation">{</span><span class="token comment">/* 預設訊息 1 */</span><span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token punctuation">{</span><span class="token comment">/* defineMessage 回傳的值等同於 { id: "test2", defaultMessage: "預設訊息 2" } */</span><span class="token punctuation">}</span><span class="token plain-text"><br /> /</span><span class="token punctuation">{</span><span class="token function">formatMessage</span><span class="token punctuation">(</span><br /> <span class="token function">defineMessage</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token string">"test2"</span><span class="token punctuation">,</span> <span class="token literal-property property">defaultMessage</span><span class="token operator">:</span> <span class="token string">"預設訊息 2"</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token plain-text"> </span><span class="token punctuation">{</span><span class="token comment">/* 預設訊息 2 */</span><span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> App<span class="token punctuation">;</span></code></pre>
<p>隨著翻譯字詞的增多,不想寫那麼多次 defineMessage ,我們還可以用有加 s 的 defineMessage<em>s</em> 進行標注</p>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">const</span> messages <span class="token operator">=</span> <span class="token function">defineMessages</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token literal-property property">test3</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token string">"test3"</span><span class="token punctuation">,</span> <span class="token literal-property property">defaultMessage</span><span class="token operator">:</span> <span class="token string">"預設訊息 3"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">test4</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token string">"test4"</span><span class="token punctuation">,</span> <span class="token literal-property property">defaultMessage</span><span class="token operator">:</span> <span class="token string">"預設訊息 4"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><br /><br /><span class="token comment">// defineMessages 回傳的 messages 等同於</span><br /><span class="token comment">// const messages = {</span><br /><span class="token comment">// test3: { id: "test3", defaultMessage: "預設訊息 3" },</span><br /><span class="token comment">// test4: { id: "test4", defaultMessage: "預設訊息 4" },</span><br /><span class="token comment">// }</span><br /><br /><span class="token keyword">function</span> <span class="token function">TranslationBlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">{</span> formatMessage <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">useIntl</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token punctuation">{</span><span class="token function">formatMessage</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token string">"test1"</span><span class="token punctuation">,</span> <span class="token literal-property property">defaultMessage</span><span class="token operator">:</span> <span class="token string">"預設訊息 1"</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token plain-text"> </span><span class="token punctuation">{</span><span class="token comment">/* 預設訊息 1 */</span><span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token punctuation">{</span><span class="token function">formatMessage</span><span class="token punctuation">(</span><br /> <span class="token function">defineMessage</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token string">"test2"</span><span class="token punctuation">,</span> <span class="token literal-property property">defaultMessage</span><span class="token operator">:</span> <span class="token string">"預設訊息 2"</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token comment">/* 預設訊息 2 */</span><span class="token punctuation">}</span><br /> <span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span><span class="token punctuation">></span></span><span class="token punctuation">{</span><span class="token function">formatMessage</span><span class="token punctuation">(</span>messages<span class="token punctuation">.</span>test4<span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span><span class="token plain-text"> </span><span class="token punctuation">{</span><span class="token comment">/* 預設訊息 3 */</span><span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span><span class="token punctuation">></span></span><span class="token punctuation">{</span><span class="token function">formatMessage</span><span class="token punctuation">(</span>messages<span class="token punctuation">.</span>test4<span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span><span class="token plain-text"> </span><span class="token punctuation">{</span><span class="token comment">/* 預設訊息 4 */</span><span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /> <span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<h3 id="babel-%E7%B0%A1%E4%BB%8B"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/react-intl-automation/#babel-%E7%B0%A1%E4%BB%8B">#</a> Babel 簡介</h3>
<p>Babel 是一個 JavaScript 的轉譯器,通常拿來將瀏覽器的尚未支援的 ES6+ 語法轉成 ES5,這樣就可以讓開發者用最新的語法來寫 Code。</p>
<p>Babel 背後的原理是抽象語法樹 Abstract Syntax Tree,簡稱 AST,對 AST 有興趣的話,推薦看<a href="https://chihyang41.github.io/2021/06/28/AST-and-ESLint-Introduction-part-1/">這篇</a></p>
<h3 id="babel-%E8%A8%AD%E7%BD%AE"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/react-intl-automation/#babel-%E8%A8%AD%E7%BD%AE">#</a> Babel 設置</h3>
<p>可以參考<a href="https://babeljs.io/setup#installation">官方文件</a>安裝,還需要加上 Plugin <a href="https://github.com/formatjs/babel-plugin-react-intl">babel-plugin-react-intl</a></p>
<p><code>babel.config.json</code></p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br /> <span class="token property">"presets"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"@babel/preset-env"</span><span class="token punctuation">,</span> <span class="token string">"@babel/preset-react"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token property">"plugins"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">[</span><br /> <span class="token comment">// 這個 Plugin 會將定義好的 id 和 defaultMessage extract 出來</span><br /> <span class="token string">"react-intl"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">"messagesDir"</span><span class="token operator">:</span> <span class="token string">"./src/translations/extract"</span> <span class="token comment">// extract 出來的檔案要放到那個資料夾下面</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">]</span><br /> <span class="token punctuation">]</span><br /><span class="token punctuation">}</span></code></pre>
<p>在專案中執行 <code>babel ./src</code> 就會再目標資料夾看到 extract 出來的 json 檔,資料格式會像是這樣</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">[</span><br /> <span class="token punctuation">{</span> <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"test1"</span><span class="token punctuation">,</span> <span class="token property">"defaultMessage"</span><span class="token operator">:</span> <span class="token string">"預設訊息 1"</span> <span class="token punctuation">}</span><br /> <span class="token punctuation">{</span> <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"test2"</span><span class="token punctuation">,</span> <span class="token property">"defaultMessage"</span><span class="token operator">:</span> <span class="token string">"預設訊息 2"</span> <span class="token punctuation">}</span><br /> <span class="token punctuation">{</span> <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"test3"</span><span class="token punctuation">,</span> <span class="token property">"defaultMessage"</span><span class="token operator">:</span> <span class="token string">"預設訊息 3"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span> <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"test4"</span><span class="token punctuation">,</span> <span class="token property">"defaultMessage"</span><span class="token operator">:</span> <span class="token string">"預設訊息 4"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /><span class="token punctuation">]</span></code></pre>
<p>但這不是我們要的格式,我希望的語系檔格式會像是這樣</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br /> <span class="token property">"test1"</span><span class="token operator">:</span> <span class="token string">"預設訊息 1"</span><span class="token punctuation">,</span><br /> <span class="token property">"test2"</span><span class="token operator">:</span> <span class="token string">"預設訊息 2"</span><span class="token punctuation">,</span><br /> <span class="token property">"test3"</span><span class="token operator">:</span> <span class="token string">"預設訊息 3"</span><span class="token punctuation">,</span> <br /> <span class="token property">"test4"</span><span class="token operator">:</span> <span class="token string">"預設訊息 4"</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span></code></pre>
<p>聰明如你,一定想到了可以自己寫一個腳本做轉換,但正所謂不要重複造輪子,社群上的大大們已經把工具都做好,在這裡我們可以直接使用就好了,</p>
<p>我們要使用的是這個 <a href="https://github.com/GertjanReynaert/react-intl-translations-manager">react-intl-translations-manager</a></p>
<p>使用這個工具甚至可以直接幫我們把 Extract 的文字轉換成各種不同的語系檔</p>
<p><code>manageTranslation.js</code></p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> manageTranslations <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"react-intl-translations-manager"</span><span class="token punctuation">)</span><span class="token punctuation">.</span>default<br /><br /><span class="token function">manageTranslations</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token literal-property property">messagesDirectory</span><span class="token operator">:</span> <span class="token string">"src/translations/extract"</span><span class="token punctuation">,</span> <span class="token comment">// 剛剛 extract 出來的檔案</span><br /> <span class="token literal-property property">translationsDirectory</span><span class="token operator">:</span> <span class="token string">"src/translations/locales/"</span><span class="token punctuation">,</span> <span class="token comment">// 輸出語系檔的資料夾</span><br /> <span class="token literal-property property">languages</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"en"</span><span class="token punctuation">,</span> <span class="token string">"zh"</span><span class="token punctuation">,</span> <span class="token string">"jp"</span><span class="token punctuation">,</span> <span class="token string">'alien'</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token comment">// any language you need</span><br /> <span class="token literal-property property">whitelistsDirectory</span><span class="token operator">:</span> <span class="token string">"src/translations/locales/whitelists"</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<p>在所有的設置都安排好之後我們要做的事就是</p>
<ol>
<li>利用 react-intl 的 defineMessage 將需要 extract 的字詞定義好</li>
<li>使用 babel-plugin-react-intl 將 id 和 defaultMessage extract 出來</li>
<li>使用 react-intl-translations-manager 將 extract 出來的翻譯轉換成語系檔</li>
<li>最後將 extract 出來,已經轉換成語系檔的檔案案刪除</li>
</ol>
<p>寫在 package.json 的 script 會像這樣</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br /> <span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"trans"</span><span class="token operator">:</span> <span class="token string">"yarn trans:extract && yarn trans:manage && yarn trans:clean"</span><span class="token punctuation">,</span><br /> <span class="token property">"trans:extract"</span><span class="token operator">:</span> <span class="token string">"babel ./src"</span><span class="token punctuation">,</span><br /> <span class="token property">"trans:manage"</span><span class="token operator">:</span> <span class="token string">"node manageTranslation.js"</span><span class="token punctuation">,</span><br /> <span class="token property">"trans:clean"</span><span class="token operator">:</span> <span class="token string">"rm -rf ./src/translations/extract"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span></code></pre>
<p>執行 <code>yarn trans</code> 之後你就會看到你需要的語法出 locale 資料夾下面,你就可以在 IntlProvider 的 messages 切換不同的語系檔來轉換語系</p>
<p>可參考 <a href="https://github.com/futianshen/react-js-intl">repo</a></p>
<h3 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/tian/react-intl-automation/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h3>
<p><a href="https://medium.com/@yehiasaleh/internationalization-localization-using-react-intl-typescript-1e7cfccd34d7">Internationalization & Localization Using React-Intl & TypeScript</a><br />
<a href="https://formatjs.io/docs/getting-started/installation/">formatjs</a><br />
<a href="https://babeljs.io/setup">babel</a><br />
<a href="https://github.com/formatjs/babel-plugin-react-intl">babel-plugin-react-intl</a><br />
<a href="https://github.com/GertjanReynaert/react-intl-translations-manager">react-intl-translation-manager</a><br />
<a href="https://chihyang41.github.io/2021/06/28/AST-and-ESLint-Introduction-part-1/">前端癢癢 der - 淺談 AST 及 ESlint Rule:AST 是殺毀?(上)</a></p>
用 Vue 實作 Infinite Scroll
2022-02-06T00:00:00Z
https://blog.errorbaker.tw/posts/ruofan/vue-infinite-scroll/
<!-- summary -->
<p>Hi 大家好,過完美好的春節連假,扎實的充飽了電! 實作 Infinite Scroll 有幾種方法,這篇文章會帶著大家透過 Vue 認識兩個實作 Infinite Scroll 的方法。</p>
<!-- summary -->
<!-- more -->
<h4 id="%E6%96%B9%E6%B3%95%E4%B8%80-%EF%BC%9A-%E9%80%8F%E9%81%8E%E7%9B%A3%E8%81%BD-scroll-%E6%90%AD%E9%85%8D-throttling-%EF%BC%8C%E5%81%B5%E6%B8%AC%E6%BB%91%E5%8B%95%E5%88%B0%E5%BA%95%E9%83%A8%E6%99%82%E6%8B%BF%E5%8F%96%E6%9B%B4%E5%A4%9A%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/vue-infinite-scroll/#%E6%96%B9%E6%B3%95%E4%B8%80-%EF%BC%9A-%E9%80%8F%E9%81%8E%E7%9B%A3%E8%81%BD-scroll-%E6%90%AD%E9%85%8D-throttling-%EF%BC%8C%E5%81%B5%E6%B8%AC%E6%BB%91%E5%8B%95%E5%88%B0%E5%BA%95%E9%83%A8%E6%99%82%E6%8B%BF%E5%8F%96%E6%9B%B4%E5%A4%9A%E8%B3%87%E6%96%99">#</a> 方法一 : 透過監聽 scroll 搭配 throttling ,偵測滑動到底部時拿取更多資料</h4>
<p>監聽 scroll 這邊使用到的是 window <a href="https://html.spec.whatwg.org/multipage/webappapis.html#event-handlers-on-elements,-document-objects,-and-window-objects">event handler</a> 中的 onscroll 方法。從 <a href="https://html.spec.whatwg.org/multipage/webappapis.html#event-handlers">html specification</a> 中可以認識到更詳細的說明。 讓我們一起來從下方的程式碼來看一下實作方法吧!</p>
<ol>
<li>
<p>先來看一下這邊如何偵測滑動到底部<br />
a. 透過 <code>document.documentElement.offsetHeight</code> 可以拿到 document 中 root element 本身的高度。<br />
b. <code>window.innerHeight</code> 是瀏覽器視窗的高度。<br />
c. 搭配 <code>Math.max()</code> 方法讓我們拿到 <code>window.pageYOffset</code>, <code>document.documentElement.scrollTop</code>, <code>document.body.scrollTop</code> 中捲軸的捲動高度最大值。</p>
<blockquote>
<p>如果滾動區域只有在畫面中一小區塊呢?</p>
</blockquote>
<p>可以從下方的圖片觀察出計算方法。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/scroll-1.png" alt="" /><br />
圖片來源: <a href="https://javascript.info/size-and-scroll">javascript.info</a></p>
</li>
<li>
<p><code>throttling</code> 的概念是在時間區間內,只會發出一次請求。<br />
a. 這邊透過 <code>Promise</code> 搭配 <code>setTimeout()</code> 方法來實作像是 <a href="https://apidock.com/ruby/Kernel/sleep"><code>sleep</code></a> 的方法。</p>
</li>
</ol>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>template</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span> bg-gray-100 h-screen<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>nav</span><br /> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><br /> bg-neutral-200<br /> shadow<br /> font-mono<br /> p-4<br /> font-bold<br /> text-amber-900<br /> w-screen<br /> fixed<br /> top-0<br /> left-0<br /> right-0<br /> <span class="token punctuation">"</span></span><br /> <span class="token punctuation">></span></span><br /> Infinite Scroll<br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>nav</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>p-14<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">ref</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>list<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>overflow-scroll<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><br /> <span class="token attr-name">v-for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>product in products<span class="token punctuation">"</span></span><br /> <span class="token attr-name">:key</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>product.id<span class="token punctuation">"</span></span><br /> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>p-5 font-mono text-primary leading-10<span class="token punctuation">"</span></span><br /> <span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span><br /> { product.title }<br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span>$ { product.price }<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text-gray-400<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>{ product.description }<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>flex justify-center p-10<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>Loading</span> <span class="token attr-name">v-show</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>loading<span class="token punctuation">"</span></span><span class="token punctuation">/></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>template</span><span class="token punctuation">></span></span><br /><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">lang</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>ts<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> computed<span class="token punctuation">,</span> defineComponent<span class="token punctuation">,</span> onMounted<span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'vue'</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> useStore <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'vuex'</span><br /><span class="token keyword">import</span> Loading <span class="token keyword">from</span> <span class="token string">'@/components/Loading.vue'</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token function">defineComponent</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'App'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">components</span><span class="token operator">:</span><span class="token punctuation">{</span><br /> Loading<br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token function-variable function">setup</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /><br /> <span class="token keyword">const</span> store <span class="token operator">=</span> <span class="token function">useStore</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">const</span> loading <span class="token operator">=</span> <span class="token function">computed</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> store<span class="token punctuation">.</span>getters<span class="token punctuation">[</span><span class="token string">'loading'</span><span class="token punctuation">]</span><span class="token punctuation">)</span><br /> <span class="token keyword">const</span> products <span class="token operator">=</span> <span class="token function">computed</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> store<span class="token punctuation">.</span>getters<span class="token punctuation">[</span><span class="token string">'products'</span><span class="token punctuation">]</span><span class="token punctuation">)</span><br /><br /> <span class="token function">onMounted</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> store<span class="token punctuation">.</span><span class="token function">dispatch</span><span class="token punctuation">(</span><span class="token string">'GET_PRODUCT'</span><span class="token punctuation">)</span><br /> <span class="token function">intersectionObserver</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token keyword">function</span> <span class="token function">intersectionObserver</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> window<span class="token punctuation">.</span><span class="token function-variable function">onscroll</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> bottomOfWrapper <span class="token operator">=</span><br /> Math<span class="token punctuation">.</span><span class="token function">max</span><span class="token punctuation">(</span><br /> window<span class="token punctuation">.</span>pageYOffset<span class="token punctuation">,</span><br /> document<span class="token punctuation">.</span>documentElement<span class="token punctuation">.</span>scrollTop<span class="token punctuation">,</span><br /> document<span class="token punctuation">.</span>body<span class="token punctuation">.</span>scrollTop<br /> <span class="token punctuation">)</span> <span class="token operator">+</span><br /> window<span class="token punctuation">.</span>innerHeight <span class="token operator">===</span><br /> document<span class="token punctuation">.</span>documentElement<span class="token punctuation">.</span>offsetHeight<br /><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>bottomOfWrapper<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> store<span class="token punctuation">.</span><span class="token function">commit</span><span class="token punctuation">(</span><span class="token string">'nextPage'</span><span class="token punctuation">)</span><br /> <span class="token function">setTimeout</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> store<span class="token punctuation">.</span><span class="token function">dispatch</span><span class="token punctuation">(</span><span class="token string">'LOAD_MORE_PRODUCT'</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">1000</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> <span class="token punctuation">{</span><br /> products<span class="token punctuation">,</span><br /> loading<br /><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><br /></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span><br /></code></pre>
<p>完整程式碼可以透過這個 <a href="https://github.com/ruofanwei/vue-infinite-scroll/tree/Method1/event-listeners">Repository (event-listeners)</a> 觀看。</p>
<h4 id="%E6%96%B9%E6%B3%95%E4%BA%8C-%EF%BC%9A-%E9%80%8F%E9%81%8E-intersection-observer-api-%E5%81%B5%E6%B8%AC-element-%E9%80%B2%E5%85%A5-viewport-%E6%99%82%E6%8B%BF%E5%8F%96%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/vue-infinite-scroll/#%E6%96%B9%E6%B3%95%E4%BA%8C-%EF%BC%9A-%E9%80%8F%E9%81%8E-intersection-observer-api-%E5%81%B5%E6%B8%AC-element-%E9%80%B2%E5%85%A5-viewport-%E6%99%82%E6%8B%BF%E5%8F%96%E8%B3%87%E6%96%99">#</a> 方法二 : 透過 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API"><code>Intersection Observer API</code></a> 偵測 element 進入 viewport 時拿取資料</h4>
<p>以下是 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API">Intersection Observer API</a> 在 MDN 上的說明:</p>
<blockquote>
<p>provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.</p>
</blockquote>
<p>讓我們一起來從下方的程式碼來看一下實作方法吧!</p>
<p><em><strong>Observer.vue</strong></em></p>
<ol>
<li>這邊把 <code>Observe</code> 獨立成一個元件,因為在專案中可能會遇到有多個地方會需要使用同樣的方法。</li>
<li>當 <code>entry.isIntersecting</code> 成立時透過 <code>context.emit('intersect')</code> 方法傳遞到外層元件,讓外層元件進行拿取資料的動作。</li>
</ol>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>template</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">ref</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>target<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>target<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>template</span><span class="token punctuation">></span></span><br /><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">lang</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>ts<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> defineComponent<span class="token punctuation">,</span> onMounted<span class="token punctuation">,</span> ref<span class="token punctuation">,</span> onBeforeUnmount <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'vue'</span><br /><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token function">defineComponent</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'Observer'</span><span class="token punctuation">,</span><br /> <span class="token function-variable function">setup</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">props<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">let</span> <span class="token literal-property property">observer</span><span class="token operator">:</span> IntersectionObserver<br /> <span class="token keyword">const</span> target <span class="token operator">=</span> <span class="token function">ref</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token function">onMounted</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> observer <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">IntersectionObserver</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">[</span>entry<span class="token punctuation">]</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>entry<span class="token punctuation">.</span>isIntersecting<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> context<span class="token punctuation">.</span><span class="token function">emit</span><span class="token punctuation">(</span><span class="token string">'intersect'</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /> observer<span class="token punctuation">.</span><span class="token function">observe</span><span class="token punctuation">(</span>target<span class="token punctuation">.</span>value<span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token function">onBeforeUnmount</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> observer<span class="token punctuation">.</span><span class="token function">disconnect</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token keyword">return</span> <span class="token punctuation">{</span><br /> target<span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><br /></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span></code></pre>
<p><em><strong>App.vue</strong></em></p>
<ol>
<li>透過 <code>store.dispatch('GET_PRODUCT', limit.value)</code> 來實作分頁拿取資料。</li>
</ol>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>template</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span> bg-gray-100 h-screen<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>nav</span><br /> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><br /> bg-neutral-200<br /> shadow<br /> font-mono<br /> p-4<br /> font-bold<br /> text-amber-900<br /> w-screen<br /> fixed<br /> top-0<br /> left-0<br /> right-0<br /> <span class="token punctuation">"</span></span><br /> <span class="token punctuation">></span></span><br /> Infinite Scroll<br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>nav</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>p-14 h-screen<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><br /> <span class="token attr-name">v-for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>product in products<span class="token punctuation">"</span></span><br /> <span class="token attr-name">:key</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>product.id<span class="token punctuation">"</span></span><br /> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>pb-5 pt-5 font-mono text-primary leading-10<span class="token punctuation">"</span></span><br /> <span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span><br /> { product.title }<br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span>$ { product.price }<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text-gray-400<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>{ product.description }<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>flex justify-center p-10 h-32<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>Loading</span> <span class="token attr-name">v-show</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>loading<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>Observer</span> <span class="token attr-name">@intersect</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>intersected<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>template</span><span class="token punctuation">></span></span><br /><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">lang</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>ts<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> computed<span class="token punctuation">,</span> defineComponent<span class="token punctuation">,</span> ref <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'vue'</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> useStore <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'vuex'</span><br /><span class="token keyword">import</span> Loading <span class="token keyword">from</span> <span class="token string">'@/components/Loading.vue'</span><br /><span class="token keyword">import</span> Observer <span class="token keyword">from</span> <span class="token string">'@/components/Observer.vue'</span><br /><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token function">defineComponent</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'App'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">components</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> Loading<span class="token punctuation">,</span><br /> Observer<span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token function-variable function">setup</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> store <span class="token operator">=</span> <span class="token function">useStore</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">const</span> limit <span class="token operator">=</span> <span class="token function">ref</span><span class="token punctuation">(</span><span class="token number">5</span><span class="token punctuation">)</span><br /> <span class="token keyword">const</span> loading <span class="token operator">=</span> <span class="token function">computed</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> store<span class="token punctuation">.</span>getters<span class="token punctuation">[</span><span class="token string">'loading'</span><span class="token punctuation">]</span><span class="token punctuation">)</span><br /> <span class="token keyword">const</span> products <span class="token operator">=</span> <span class="token function">computed</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> store<span class="token punctuation">.</span>getters<span class="token punctuation">[</span><span class="token string">'products'</span><span class="token punctuation">]</span><span class="token punctuation">)</span><br /> <span class="token keyword">const</span> total <span class="token operator">=</span> <span class="token function">computed</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> store<span class="token punctuation">.</span>getters<span class="token punctuation">[</span><span class="token string">'total'</span><span class="token punctuation">]</span><span class="token punctuation">)</span><br /> <span class="token keyword">function</span> <span class="token function">intersected</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>products<span class="token punctuation">.</span>value<span class="token punctuation">.</span>length <span class="token operator">>=</span> total<span class="token punctuation">.</span>value<span class="token punctuation">)</span> <span class="token keyword">return</span><br /> store<span class="token punctuation">.</span><span class="token function">dispatch</span><span class="token punctuation">(</span><span class="token string">'GET_PRODUCT'</span><span class="token punctuation">,</span> limit<span class="token punctuation">.</span>value<span class="token punctuation">)</span><br /> limit<span class="token punctuation">.</span>value <span class="token operator">+=</span> <span class="token number">5</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> <span class="token punctuation">{</span><br /> products<span class="token punctuation">,</span><br /> loading<span class="token punctuation">,</span><br /> intersected<span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><br /></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span><br /></code></pre>
<p>完整程式碼可以透過這個 <a href="https://github.com/ruofanwei/vue-infinite-scroll/tree/Method2/Intersection-Observer/src">Repository (Intersection-Observer)</a> 觀看。</p>
<h4 id="%E5%B0%8F%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/vue-infinite-scroll/#%E5%B0%8F%E7%B5%90">#</a> 小結</h4>
<p>實作完兩個方法後,會很驚喜的發現 <code>Intersection Observer API</code> 的美好,以下是筆者的觀察:</p>
<ol>
<li>
<p>不需要去計算出 <code>scroll</code> 到底部的部分,在不同的頁面上可以共用同一個 <code>observe</code> 元件。</p>
</li>
<li>
<p>因為沒有監聽的行為,拿取更多資料的時候不需要實作 <code>throttling</code>,在使用者體驗上不會有延遲感,但是如果想特別設計等待的延遲感,使用 <code>Intersection Observer API</code> 方法一樣可以彈性的透過 <code>setTimeout()</code> 來實現。</p>
<p>下方是 <code>event-listeners</code> 的實作 demo</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/event.gif" alt="" /></p>
<p>下方是 <code>Intersection Observer API</code> 的實作 demo</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/observe.gif" alt="" /></p>
</li>
</ol>
<p>在閱讀文章時如果有遇到什麼問題,或是有什麼建議,都歡迎留言告訴我,謝謝。😃</p>
<ul>
<li><a href="https://github.com/ruofanwei/vue-infinite-scroll">Github | Repo: vue-infinite-scroll</a></li>
</ul>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/vue-infinite-scroll/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li><a href="https://javascript.info/size-and-scroll">Documentation | javascript.info</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API">Documentation | Intersection Observer API</a></li>
<li><a href="https://vuex.vuejs.org/">Documentation | Vuex</a></li>
<li><a href="https://v3.vuejs.org/">Documentation | Vuejs</a></li>
</ul>
從與工程師共事,到與工程師共識(二)
2022-02-06T00:00:00Z
https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer-part2/
<!-- summary -->
<p>寫給正準備踏入軟體相關產業,第一次準備與工程師共事,卻又對程式一頭霧水的你(下集)</p>
<!-- summary -->
<!-- more -->
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer-part2/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<p>接續前一篇 <a href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer/">從與工程師共事,到與工程師共識(一)</a> 提到的內容,我們已經搞懂最基本的技術名詞,理解網頁運作邏輯。</p>
<p>☞ 第零步:心態建立<br />
讓大家了解程式會遇到的問題,很多都是有關聯性的,當我們遇到了新的問題時,只要我們清楚它屬於哪個範疇的問題,就能運用既有的理解,將其容納並吸收進我們的腦海當中。</p>
<p>☞ 第ㄧ步 ~ 第二部:建立對網頁 / 程式的熟悉度<br />
透過生活中的情境做舉例,帶大家走進餐廳,跟服務生點菜。從點餐到上菜的過程中,認識程式領域常常遇到的技術名詞(例如:前端、後端、API),並且介紹了當我們在工作中遇到問題的時候,可以如何去釐清問題,找到可以協助處理的工程師。</p>
<p>接下來就要開始第三部「如何制伏工程師,讓工程師閉嘴」,告訴大家工程師在想什麼?如何能簡單制伏工程師?以及如何從與工程師共事,到與工程師共識?<br />
本篇的內容會延續到上一篇的基本觀念,為了讓大家閱讀起來不會太吃力,我會盡量舉一些例子讓大家參考。</p>
<blockquote>
<p>本篇文章是以我個人的角度作為出發點,不能代表所有工程師的想法。所以大家僅能夠把這篇文章當作是一個參考,實際工作上還是需要與協作的工程師多溝通,才能找出最適合你們的協作模式。</p>
</blockquote>
<h2 id="3-1%EF%BC%9A%E5%B7%A5%E7%A8%8B%E5%B8%AB%E6%95%B4%E5%A4%A9%E9%83%BD%E5%9C%A8%E6%83%B3%E4%BB%80%E9%BA%BC%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer-part2/#3-1%EF%BC%9A%E5%B7%A5%E7%A8%8B%E5%B8%AB%E6%95%B4%E5%A4%A9%E9%83%BD%E5%9C%A8%E6%83%B3%E4%BB%80%E9%BA%BC%EF%BC%9F">#</a> 3-1:工程師整天都在想什麼?</h2>
<p>各行各業都會有不同的煩惱,從旅遊到餐飲,再從金融到資訊,每個職位每天充斥著各式各樣不同的問題。<br />
那工程師每天面對的又是什麼樣的問題呢?常常聽到工程師都在哀嚎些什麼呢?</p>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/cooperate-with-engineer-part2-01.png" alt="" /><br />
(圖片來源:<a href="https://www.facebook.com/factsaboutprogrammers/">https://www.facebook.com/factsaboutprogrammers/</a>)</p>
<p>coding 就是這麼有趣,永遠都有解不完的問題、開發不完的項目、優化不完的程式碼,當然這些都是煩惱的部分。那有沒有開心的部分?</p>
<p>有~~~<br />
遇到解出難題的瞬間,或者是將原本長得很像垃圾場的程式碼優化成一項藝術品,這些都是會讓工程師爽到升天的時刻。把難題解出來會很開心這個大家應該可以理解,但是把程式碼寫得很美這究竟是什麼樣一種感覺,我舉個例子來讓大家體會一下:</p>
<p>假設我們現在是一間漢堡店的廚師,因為我們還很菜什麼都不會做,所以只好把漢堡的作法寫在紙上像這樣:</p>
<pre class="language-md"><code class="language-md"><span class="token title important"><span class="token punctuation">###</span> 第一版</span><br /><br /><span class="token list punctuation">1.</span> 看一下菜單,客人點的是潛艇堡還是貝果堡<br /><span class="token list punctuation">2.</span> 如果是潛艇堡就去籃子裡拿長麵包,如果是貝果堡就去籃子裡拿圓麵包<br /><span class="token list punctuation">3.</span> 看一下菜單,客人是點牛肉還是雞肉<br /><span class="token list punctuation">4.</span> 如果是牛肉就去冰箱拿牛肉,如果是雞肉就去冰箱拿雞肉<br /><span class="token list punctuation">5.</span> 將拿來的肉放上煎台開始煎,如果是牛肉就計時 3 分鐘後翻面,如果是雞肉就計時 5 分鐘後翻面<br /><span class="token list punctuation">6.</span> 看一下菜單,客人的蛋要全熟還是半熟<br /><span class="token list punctuation">7.</span> 去冰箱拿雞蛋,把蛋打上煎台,如果要全熟就煎 5 分鐘,如果要半熟就煎 3 分鐘<br /><span class="token list punctuation">8.</span> 看一下菜單,客人選的是塔塔醬還是芥末醬<br /><span class="token list punctuation">9.</span> 如果是塔塔醬就先拿塔塔醬預備,如果是芥末醬就拿芥末醬預備<br /><span class="token list punctuation">10.</span> 看一下菜單,客人有沒有不想要加的菜<br /><span class="token list punctuation">11.</span> 如果沒有,就把洋蔥、番茄、生菜放到麵包上,如果有不想加的菜,就跳過不要加<br /><span class="token list punctuation">12.</span> 擠上剛剛準備的醬<br /><span class="token list punctuation">13.</span> 放上煎好的肉跟蛋<br /><span class="token list punctuation">14.</span> 完成</code></pre>
<p>上面的步驟又臭又長,流程上看起來沒什麼問題,至少我可以確定只要按照流程來做,都能做出客人要的漢堡。但是冗文贅字很多,有一些重複的事情如果能處理一次就處理好,感覺會方便許多。比如說:流程中看了 5 次菜單,能不能只看一次就好?還有去冰箱拿完肉回來後沒多久又要去冰箱拿蛋,我如果一次拿完感覺會方便很多。</p>
<p>於是我們把流程做一下改造:</p>
<pre class="language-md"><code class="language-md"><span class="token title important"><span class="token punctuation">###</span> 第二版</span><br /><br /><span class="token list punctuation">1.</span> 把菜單從頭看到尾,並且紀錄下代號:ex.客人點的是潛艇堡+牛肉+全熟蛋+芥末醬+全加,就記下 「潛牛熟芥全」<br /><span class="token list punctuation">2.</span> 依照記下來的代號,去拿好所需要的材料:麵包、肉、蛋、醬、菜<br /><span class="token list punctuation">3.</span> 將肉跟蛋放上煎台開始煎,如果是牛肉就計時 3 分鐘後翻面,如果是雞肉就計時 5 分鐘後翻面,蛋如果要全熟就煎 5 分鐘,如果要半熟就煎 3 分鐘<br /><span class="token list punctuation">4.</span> 將準備好的材料依照 麵包、菜、醬、肉、蛋、麵包 的順序疊起來<br /><span class="token list punctuation">5.</span> 完成</code></pre>
<p>有沒有發現,重複的事情如果能一次完成,步驟瞬間就剩下一半不到了?!<br />
寫程式其實就是在做類似的事情,我們要把 PM 開給我們的需求,轉換成一個一個簡單的小步驟,並且透過指令來告訴電腦怎麼做,差別只在於工程師是用程式語言在寫這些步驟而已。<br />
步驟可以寫得又臭又長,也可以寫得精簡美妙。</p>
<p>但是,問題來囉~<br />
不是寫的精簡就一定最好,需求是有可能改變的喔!</p>
<p>假設今天漢堡店決定將漢堡加入新口味,這次會有不同的麵包不同的肉以及不同的醬,你需要將新的口味製作方式加進流程圖當中,你覺得是舊的流程比較好改?還是新的流程比較好改?</p>
<p>(建議各位在這邊暫停,先思考看看...)</p>
<p>我個人是覺得原先又臭又長的流程比較好改,因為簡化後的流程還有使用代號來做紀錄,加入新的口味還要處理新的代號,如果未來新口味一大堆,代號一大堆,到時候也記不起來,反而弄得一團亂。</p>
<p>但是如果代號管理能夠做好,新代號加入時,不會亂掉,而且可以利用代號來管理新的口味,那可能新的流程會更好處理,比如說未來新口味出現時,只要在代號管理中加入新的代號,流程圖的部分完全不用改動,像下面這樣:</p>
<pre class="language-md"><code class="language-md"><span class="token title important"><span class="token punctuation">###</span> 第三版:</span><br /><br />代號表:<br /><br /><span class="token list punctuation">-</span> 麵包:<br /> <span class="token list punctuation">-</span> 潛艇堡(潛)<br /> <span class="token list punctuation">-</span> 貝果堡(貝)<br /><span class="token list punctuation">-</span> 肉:<br /> <span class="token list punctuation">-</span> 牛肉(牛):煎 3 分鐘翻面<br /> <span class="token list punctuation">-</span> 雞肉(雞):煎 5 分鐘翻面<br /> .<br /> .<br /> .(省略)<br /><span class="token list punctuation">-</span> 蛋:<br /> .<br /><span class="token list punctuation">-</span> 醬:<br /> .<br /><span class="token list punctuation">-</span> 菜:<br /> .<br /> .<br /> .(省略)<br /><br />流程:<br /><br /><span class="token list punctuation">1.</span> 把菜單從頭看到尾,並且紀錄下代號:ex.客人點的是潛艇堡+牛肉+全熟蛋+芥末醬+全加,就記下 「潛牛熟芥全」<br /><span class="token list punctuation">2.</span> 依照記下來的代號,去拿好所需要的材料:麵包、肉、蛋、醬、菜<br /><span class="token list punctuation">3.</span> 將肉跟蛋放上煎台開始煎,參考代號表確認要煎幾分鐘<br /><span class="token list punctuation">4.</span> 將準備好的材料依照 麵包、菜、醬、肉、蛋、麵包 的順序疊起來<br /><span class="token list punctuation">5.</span> 完成<br /><br />加新口味:只要加代號就好,流程不需要改</code></pre>
<p>以上演化的流程,從一開始 <code>第一版:只是單純完成需求</code>,到 <code>第二版:開始簡化流程</code>,最後 <code>第三版:優化調整彈性</code> 在未來改動需求時,可以有最小的修改成本。對應工程師們每天都在想的事情,如何讓程式碼更好看懂?如何在未來需求改動時方便調整?所有的煩惱,都在程式碼上面,所有的煩惱,都在這一步步細小的流程上面。</p>
<p>舉個更貼近程式開發的例子:<br />
比如說要做驗證密碼這個功能,我們想驗證 6 ~ 10 位英文加數字,英文要有大小寫。這樣子的功能現在很常見了,如果把它拆解成一步一步的流程,可能就會像:</p>
<pre class="language-md"><code class="language-md"><span class="token list punctuation">1.</span> 計算密碼的長度,如果小於 6 或者大於 10,就出現錯誤訊息,否則就進入下一步<br /><span class="token list punctuation">2.</span> 從第一個字母看到最後一個字母,有沒有大寫的英文字母,如果沒有就出現錯誤訊息,否則就進入下一步<br /><span class="token list punctuation">3.</span> 從第一個字母看到最後一個字母,有沒有小寫的英文字母,如果沒有就出現錯誤訊息,否則就進入下一步<br /><span class="token list punctuation">4.</span> 從第一個字母看到最後一個字母,有沒有數字,如果沒有就出現錯誤訊息,否則就進入下一步<br /><span class="token list punctuation">5.</span> 通過驗證</code></pre>
<p>一樣我們找找有沒有冗文贅字,從第一個字母到最後一個字母一共看了三次,我們嘗試把它優化看看:</p>
<pre class="language-md"><code class="language-md">驗證表:<br /><br /><span class="token list punctuation">-</span> 大寫<br /><span class="token list punctuation">-</span> 小寫<br /><span class="token list punctuation">-</span> 數字<br /><br />步驟:<br /><br /><span class="token list punctuation">1.</span> 計算密碼的長度,如果小於 6 或者大於 10,就出現錯誤訊息,否則就進入下一步<br /><span class="token list punctuation">2.</span> 從第一個字母看到最後一個字母,如果遇到大寫就把驗證表的大寫打勾,如果遇到小寫就把驗證表的小寫字母打勾,如果遇到數字就把驗正表的數字打勾。完成後進入下一步<br /><span class="token list punctuation">3.</span> 看看最後是不是都有打勾,如果有任何項目沒勾就出現錯誤訊息,否則就進入下一步<br /><span class="token list punctuation">4.</span> 通過驗證</code></pre>
<p>同樣一個需求,每個人能想到的流程可能都不一樣,從做漢堡的流程,到密碼驗證的流程,不同的廚師會有自己的習慣,不同的工程師可能也有不同的風格跟喜好。<br />
從網頁上操作可能感受不到,但從程式碼的字裡行間,就會交織著每位工程師日常的喜怒哀樂。</p>
<p>做漢堡可能只是一個小功能的規模,大家來想像一下,一個平台案可能有幾千幾萬個像這樣子的小功能?那可能是一場幾百人婚宴,好幾十桌桌菜的規模,到了那個規模之後,你再嘗試看看,像做漢堡的例子一樣把一整場婚宴每一道菜的製作流程一步一步細細的撰寫下來,從備料到清洗,從烹煮到擺盤,從出餐到上菜,通通要寫的一清二楚,你就會感受到工程師的痛苦在哪裡了(笑!</p>
<p>寫完以後還沒有結束,通常需求只會不斷產生,不同季節又有不同的菜色,不同活動又會有不同的功能要做,所以在這樣功能又做不完時程趕得要死的情況下,流程只會越寫越亂,加上去的步驟只會越來越複雜,長時間下來就會是一筆可觀的債務。<br />
所以如果哪天工程師說他非常非常想重構專案,可能就是專案的程式碼變得越來越像垃圾場,指令流程寫得亂七八糟,改一個錯誤可能出現另外三個錯誤,整天心情很差。<br />
我良心的祈求與建議,如果時程允許的話,就讓他重構一下吧,讓他把專案變成他喜歡的形狀,人們都是會善待藝術品的,尤其還是自己親手製作的東西。</p>
<p>扯得有點遠了,上面舉的一大堆例子,都是想讓大家體會工程師每天思考程式碼是什麼感覺。想讓大家知道,需求出現的那一刻起,到需求完成以後,這幾行已經撰寫的程式碼,在工程師眼中,會不斷被看到,不斷被嫌棄,再不斷被修改,直到它呈現出在工程師眼中最完美的一面。</p>
<p>這就是整天面對著程式碼的工程師每天都在想的事情,上面的例子可能只是片面,實際工作上還會有許多千奇百怪的事情會發生,但回到問題的根本,就是如何去將功能及需求,透過一行行的指令,把它實作出來。</p>
<h2 id="3-2%EF%BC%9A%E5%B7%A5%E7%A8%8B%E5%B8%AB%E7%9A%84%E6%80%9D%E7%B6%AD%E6%98%AF%E4%BB%80%E9%BA%BC%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer-part2/#3-2%EF%BC%9A%E5%B7%A5%E7%A8%8B%E5%B8%AB%E7%9A%84%E6%80%9D%E7%B6%AD%E6%98%AF%E4%BB%80%E9%BA%BC%EF%BC%9F">#</a> 3-2:工程師的思維是什麼?</h2>
<p>那工程師的思維是什麼呢?就是把完整的需求透過上面提到的一個個小步驟,一行一行的把指令給打下來。<br />
不曉得大家有沒有注意到重點:「完整的需求」!</p>
<p>怎樣叫完整的需求?一樣我們以做漢堡為例子。假設今天有客人反應半熟蛋要再生一點,然後櫃檯的人員告訴你說蛋再生一點,你的腦海中第一個想法是什麼?<br />
生一點是多生?是煎 30 秒還是 20 秒?<s>還是直接把生蛋打在麵包上</s>?總要讓我知道他希望要多生我才有辦法做啊,不然做出來做錯了還得要重做一次。</p>
<p>回到程式的情境下,客戶說標題想改大一點,如果直接告訴工程師說希望把標題改大一點,工程師通常都會反問,大一點要改多大?<br />
可能有人會覺得,工程師難道不會變通嗎?顏色淺一點、字體大一點,這些工程師先抓個大概不行嗎?頂多客戶看了覺得不滿意那就再修正一次就好啦。</p>
<p>也許從樣式的角度感受不出修改上的麻煩,我們換個功能來舉例。假設今天要做一個註冊功能,需求上僅僅提到要用帳號密碼註冊,然後工程師寫出了下面這個流程:</p>
<pre class="language-md"><code class="language-md"><span class="token list punctuation">1.</span> 取得使用者輸入的帳號<br /><span class="token list punctuation">2.</span> 取得使用者輸入的密碼<br /><span class="token list punctuation">3.</span> 將帳號及密碼存放至資料庫</code></pre>
<p>結果測試之後發現,就算不要輸入密碼,或者只輸入一個字母,竟然都可以註冊成功,想想覺得哪裡怪怪的?於是再調整需求,希望密碼能夠加上限制(限制長度,限制要英文加數字),然後再請工程師回去改程式的流程:</p>
<pre class="language-md"><code class="language-md"><span class="token list punctuation">1.</span> 取得使用者輸入的帳號<br /><span class="token list punctuation">2.</span> 取得使用者輸入的密碼<br /><span class="token list punctuation">3.</span> 檢查使用者輸入的密碼是否符合長度?(啊~長度限制是多少?)<br /> .<br /> .<br /> .</code></pre>
<p>工程師又會回來問,長度的限制是多少,歐~忘記給限制的內容了,好我們把長度定為 6 ~ 10 碼,接下來再請工程師回去改程式流程:</p>
<pre class="language-md"><code class="language-md"><span class="token list punctuation">1.</span> 取得使用者輸入的帳號<br /><span class="token list punctuation">2.</span> 取得使用者輸入的密碼<br /><span class="token list punctuation">3.</span> 檢查使用者輸入的密碼是否大於等於 6 且小於等於 10,如果不符合就.....(不符合要幹嘛?)<br /> .<br /> .<br /> .</code></pre>
<p>工程師又回來問,長度不符合要做什麼處理?是要顯示錯誤訊息嗎?那錯誤訊息的內容是什麼?<br />
大家能懂為什麼需求要完整嗎?因為需求不完整,這流程會寫不出來。</p>
<p>上面這個還僅僅是簡單的流程而已,有時候如果複雜的流程要改起來,就不是加上去這麼簡單而已,還要重新從步驟一開始檢查,檢查到步驟 n,看看修改後每一個步驟執行的結果有沒有問題,流程是否能夠滿足新的需求,又確保原有的功能沒有壞掉。</p>
<blockquote>
<p>所以工程師的思維很單純,沒有什麼想像力,不是給個主題工程師就能自由發揮的。我們沒辦法期望只給工程師一個製作後台的任務,他就能在腦海裡就出現一個精美別緻的畫面。</p>
</blockquote>
<p>工程師需要的,是最後要呈現的樣子,越仔細越好,每一個操作會有什麼反應,每一種情境要呈現什麼內容。然後工程師會依據這些規格,規劃出最適合的步驟,轉換成程式碼一行一行的打下來。因為流程清楚,所以工程師不需要來回確認,因為需求清楚,所以一開始規劃的方向不會錯誤。依照這個思維來推敲之後,我要來告訴大家,要搞定工程師其實很簡單。</p>
<h2 id="3-3%EF%BC%9A%E5%A6%82%E4%BD%95%E6%90%9E%E5%AE%9A%E5%B7%A5%E7%A8%8B%E5%B8%AB%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer-part2/#3-3%EF%BC%9A%E5%A6%82%E4%BD%95%E6%90%9E%E5%AE%9A%E5%B7%A5%E7%A8%8B%E5%B8%AB%EF%BC%9F">#</a> 3-3:如何搞定工程師?</h2>
<p>上面提到了兩個主題:<code>工程師每天都在想什麼?</code>、<code>工程師的思維是什麼?</code>,如果我的闡述能讓大家聽懂,大家就會知道,工程師每天都在煩惱如何把明確的規格,規劃成一行行的步驟。</p>
<p>他不會來跟你吵說這顏色設太淡了他不喜歡,他也不會來跟你吵說這個按鈕想放在右上角不想放在左上角,因為這些他都不在乎!他只在乎這幾行程式碼寫得藝不藝術,看起來順不順眼,還有,這用心規劃寫出來的步驟,會不會才剛寫完客戶就突然說這功能不需要了(<s>還要把它刪掉,多心痛啊</s>)。<br />
他在乎這個需求明不明確,合不合理,做不做的出來,寫進去之後會不會玷污他的藝術品。明白了這些以後,接下來就可以開始來制定搞定工程師的招式了。</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer-part2/">☞ 第一招:拿證據給他看</a></strong><br />
假設專案出現了一些 bug 需要修改,第一個想到的,一定是叫工程師去修。但是有沒有聽過工程師抱怨的經驗:「這是使用者自己的問題」,或是 「這個是對方版本不對」,「這個是...」反正通通都是別人的問題!這工程師真難搞,回報問題給他,他還自認為沒問題?偏偏使用者看到的真的是壞的啊!</p>
<p>對付這種工程師,就要拿證據給他看,證明真的是程式的問題,讓他知道不能甩鍋不能推卸給別人,給他看證據,他就會閉嘴了!<br />
那證據去哪裡找呢?給大家舉一些例子:</p>
<p>☞ 樣式相關的證據:<br />
我們可以在元件上點選右鍵,選擇檢查,會出現開發者工具。<br />
然後找到如下圖紅色箭頭指向的那個按鈕給它按下去。<br />
<img src="https://blog.errorbaker.tw/img/posts/xiang/cooperate-with-engineer-part2-02.png" alt="" /></p>
<p>接下來你就會發現,只要滑鼠移動到的位置,網頁都會幫你顯示元件的樣式設定<br />
<img src="https://blog.errorbaker.tw/img/posts/xiang/cooperate-with-engineer-part2-03.png" alt="" /></p>
<p>這就是網頁執行程式碼之後所讀取到的設定,像上圖右上角顯示的 <code>207.34 x 22</code> 就代表這個元件實際的 <code>寬 x 高</code>。<br />
假如設計稿要求的高度是 24px 但是我們看到網頁顯示給我們的設定是 22px,我們就可以把這個畫面截圖下來,作為一個樣式設定錯誤的證據。<br />
其他像是 <code>顏色(color)</code>、<code>字體的大小字型(font)</code> ...等等,都可以直接透過這個方式找到。</p>
<p>☞ 操作相關的證據:<br />
操作證據最簡單也最方便取得的方式,就是直接錄影。使用者提到的問題,我們在自己的電腦上實際操作一次,並且錄影下來。<br />
錄影的好處是,操作的順序很清楚,也很方便重現問題。</p>
<p>除了錄影之外,開發者工具還有一個很方便紀錄操作流程的工具 <code>Recorder</code>,我們一樣在任意位置點滑數右鍵後選擇檢查,<br />
點選如下圖紅色箭頭指向的 Recorder 按鈕,之後會看到下方出現 <code>Start new recording</code> 按鈕,給他點下去<br />
<img src="https://blog.errorbaker.tw/img/posts/xiang/cooperate-with-engineer-part2-04.png" alt="" /></p>
<p>建立自己想要的名稱,再點 <code>Start a new recording</code> 按鈕,就可以開始採集操作流程的細節了。<br />
<img src="https://blog.errorbaker.tw/img/posts/xiang/cooperate-with-engineer-part2-05.png" alt="" /></p>
<p>這東西很猛的就是,它除了會紀錄操作的流程以外,還會把操作的細節採集下來,連滑鼠滾輪滾了多少範圍它都會詳細記錄。<br />
只要把操作的項目點開,就可以看到每個操作底下的所有內容。(甚至可以驗證效能,不過這不在我們這篇文章的探討範圍之內)<br />
<img src="https://blog.errorbaker.tw/img/posts/xiang/cooperate-with-engineer-part2-06.png" alt="" /></p>
<p>總之,操作相關的證據就包含,做了什麼?順序是什麼?結果是什麼?只要能詳細囊括這些內容,就可以作為一個好的證據。</p>
<p>☞ 資料相關的證據:<br />
資料相關證據,相較於樣式、流程,會複雜很多,有的資料是從後端傳過來的,有的資料是前端自己會處理的,有的是直接可以使用,有的又會要經過轉換。<br />
當然這些程式運作的流程,我們很難每一步都去掌握,那難道沒機會找到資料相關的證據了?</p>
<p>其實我們可以先從兩個關鍵去下手:<code>使用者輸入的資料</code>、<code>畫面上最後看到的資料</code><br />
這兩個資料分別是:最原始的資料(完全沒經過處理),以及最終呈現的資料(不會再經過處理)。</p>
<p>舉個例子:<br />
活動報名系統,一定會讓使用者填寫報名資訊,並且在後台呈現所有的報名者資料。<br />
假設我填寫的資料跟最後後台看到的資料不一致,這就是一個很明顯的資料證據。<br />
<img src="https://blog.errorbaker.tw/img/posts/xiang/cooperate-with-engineer-part2-07.png" alt="" /></p>
<p>為什麼要從 <code>最原始的資料</code>、<code>最終呈現的資料</code> 去下手?因為 <code>使用者輸入的資料</code> 一定沒有做過任何處理,所以他會是一個很純的資料(不是系統轉換的資料),而 <code>畫面上最後看到的資料</code> 它不會再經過處理,所以它現在不正常它就不會再正常了。工程師沒有理由跟你說:「歐~這資料是暫時的啦,它會再經過什麼什麼轉換,到時候就會正常了。」</p>
<p>擷取最原始以及最終出現的資料,就可以說服工程師資料處理的過程是否有問題。就好像一個數字相加的程式,如果輸入 1 跟 1 它偏偏出現 3,那一定就是邏輯有寫錯:</p>
<pre class="language-md"><code class="language-md">數字相加程式:輸入任意數量的數字,程式會回傳所有數字相加的結果。<br />輸入:1, 1<br />結果:3<br /><br />// 怎麼看都不對,一定是有問題</code></pre>
<p>我們不需要知道中間邏輯錯在哪,我們只需要拿著輸入的資料,以及最後的資料,告訴工程師兩者之間哪裡不一致,讓工程師自己去檢查程式的問題。<br />
以上就是最最最基本提出資料證據的方式。接下來介紹一個進階一點的:<br />
從 <code>API 交換的資料</code> 下手!</p>
<p>不知道大家記不記得上一篇文章提到的 <code>API</code> 是什麼,它是個拿來交換資料的東西,也是前後端交換資料的方式。我們從瀏覽器的開發者工具,可以觀察到每一支發送出去的 <code>API</code> 帶著什麼資料?格式是什麼?以及後端回傳的東西是什麼?並且透過這些資訊,還可以判斷是後端的資料錯誤,還是前端的顯示錯誤。</p>
<p>舉個例子:<br />
一樣看到報名系統,這次遇到的問題是,報名者填寫了姓名、性別、email,但是後台的表格中卻少了性別這個欄位<br />
<img src="https://blog.errorbaker.tw/img/posts/xiang/cooperate-with-engineer-part2-08.png" alt="" /></p>
<p>我們發現了最初資料以及最終資料不一致,接下來我們來檢查取得這筆資料的 <code>API</code>。(如果不知道怎麼查看 API,可參考<a href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer/">上一篇</a> API 的介紹)</p>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/cooperate-with-engineer-part2-09.png" alt="" /></p>
<p>從上面截圖的內容,我們可以看到這是後端回傳給前端的資料,其中 columns 欄位的部分,底下有姓名(name)、信箱(email)、狀態(status),但是沒有看到性別這個欄位。也就是說後端回傳的資料當中就缺少了性別這筆資料,如果需要補上這筆資料,就要請後端工程師協助處理,我們可以拿著明確的證據,告訴後端工程師是哪一支 API 缺少了什麼欄位,導致最終顯示的畫面上與使用者輸入的內容不一致。</p>
<p>證據有了、錯誤的地方也找出來了,這時候工程師除了回覆:「好的,我馬上調整。」之外,應該找不出任何藉口來推託了!<s>又是 PM 的一場勝利</s>。<br />
所以當我們提出明確的證據來證明哪個環節出了問題的時候,可以有效避免被甩鍋或被推託責任的情形。<code>第一招:拿證據給他看</code> 你學廢了嗎?</p>
<blockquote>
<p>補充:資料相關的證據還有一個方式是從資料庫取得,不過考慮到如果是剛入行 PM 不一定會有資料庫的帳密,所以我就先跳過不介紹這個方式了</p>
</blockquote>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer-part2/">☞ 第二招:表明預期結果</a></strong><br />
還記得上面做漢堡的例子嗎?客人說半熟蛋想做生一點。依照工程師的思維,我們必須給出 <code>完整的需求</code>,否則工程師做不出來,他就會跟你『歡』(台語)。<br />
所以第二招制服工程師的技巧,就是在每次提出需求時,表明清楚想要什麼樣的預期結果。</p>
<p>給大家三個關鍵指標:<code>樣式</code>、<code>流程</code>、<code>反應</code><br />
<code>樣式</code> 指的就是,畫面上看得到的所有細節,每個元件的大小、顏色、形狀、陰影的數據...等等。<br />
<code>流程</code> 指的就是,這個頁面的每個操作流程,或者資料的驗證流程,要有明確的流程圖,順序、步驟、結果...等等。<br />
<code>反應</code> 指的就是,每個流程最終會呈現什麼樣的結果,成功的結果、失敗的結果、不同情境呈現的提示訊息...等等。</p>
<p>我們拿會員註冊來做舉例:<br />
我們先跳過樣式的部分,來討論下面這張註冊流程圖,大家覺得還有哪裡需要再更完整?<br />
<img src="https://i.imgur.com/rIwntXP.png" alt="" /></p>
<p>註冊之後經過驗證,驗證通過後成功導至登入頁,流程的架構本身是完整的。但細節沒講清楚,註冊要填寫哪些資訊?驗證是要驗證什麼?驗證沒通過的話要給什麼樣的錯誤訊息?或許這些內容不用全部囊括進去流程圖當中,可能會使得流程圖本身變得很亂很難閱讀,但是一定要把這些規格另外寫下來。</p>
<pre class="language-md"><code class="language-md">註冊:<br /><br /><span class="token list punctuation">-</span> 帳號:<br /> <span class="token list punctuation">-</span> 必填<br /> <span class="token list punctuation">-</span> 格式: email 格式<br /> <span class="token list punctuation">-</span> placeholder:請利用 email 進行註冊<br /> <span class="token list punctuation">-</span> error state:<br /> <span class="token list punctuation">-</span> 情境:格式不符<br /> <span class="token list punctuation">-</span> 驗證錯誤訊息:請輸入 email 格式的資料<br /><span class="token list punctuation">-</span> 密碼:<br /> <span class="token list punctuation">-</span> 必填<br /> <span class="token list punctuation">-</span> 格式:6 <span class="token strike"><span class="token punctuation">~</span><span class="token content"> 10 碼,含有大小寫英文字母加數字<br /> - placeholder:請輸入密碼<br /> - help text: 密碼需含有 6 </span><span class="token punctuation">~</span></span> 10 位英文數字及大寫英文字母<br /> <span class="token list punctuation">-</span> error state:<br /> <span class="token list punctuation">-</span> 情境:格式不符<br /> <span class="token list punctuation">-</span> 驗證錯誤訊息:請輸入含有 6 ~ 10 位英文數字及大寫英文字母<br /><br />驗證:<br /><br /><span class="token list punctuation">1.</span> 驗證欄位是否都有填寫<br /><br /><span class="token list punctuation">-</span> error message: 此欄位為必填<br /><br /><span class="token list punctuation">2.</span> 驗證欄位是否格式正確<br /><br /><span class="token list punctuation">-</span> error message: 如上<br /><br /><span class="token list punctuation">3.</span> 驗證帳號是否已被註冊<br /><br /><span class="token list punctuation">-</span> error message: 此帳號已註冊,請直接登入或以其他 email 進行註冊</code></pre>
<p>上面的規格,補充了流程沒有提到的細節,包括每個環節要做哪些事情,不同的錯誤情境,出現的 <code>反應</code> 是什麼。只要再搭配設計稿給的樣式做參考,任何人都可以在腦海中清楚呈現出它最終呈現的樣子。這就是一個 <code>完整的需求</code>。既然需求寫明確了,工程師就沒有辦法來『歡』說哪裡寫的不清楚,或者哪裡不知道要怎麼做了。</p>
<p>上面我把第一招跟第二招講完了,不如就加碼釋出一個 BONUS:組合技給大家參考一下</p>
<p>☞ 組合技能:把第一招跟第二招一起使用<br />
以 bug issue 來舉例,一個標準的 bug issue 含有三個要件:<code>測試步驟</code>、<code>調整項目</code>、<code>預期結果</code><br />
<code>測試步驟</code>:使用者怎麼連到這個頁面、點了什麼、輸入的資料是什麼<br />
<code>調整項目</code>:待調整的內容是什麼(配合第一招:拿證據給他看)<br />
<code>預期結果</code>:期望看到的結果是什麼(配合第二招:表明預期結果)</p>
<p>以實際的案例來看,我現在開一個 bug issue 給工程師:<br />
<img src="https://blog.errorbaker.tw/img/posts/xiang/cooperate-with-engineer-part2-10.png" alt="" /></p>
<p>上面的組合技當中,我們達到了哪些效果:</p>
<ul>
<li>工程師不能說找不到問題在哪裡,因為我們把明確的測試步驟提供給他了</li>
<li>工程師不能推卸責任,因為我們連 API 傳送的資料都截圖給他了</li>
<li>工程師不能說不知道要怎麼改,因為我們很明確的表達了我們的預期結果</li>
</ul>
<p>這邊只是舉例一個簡單的例子,給大家體會這些招式的效果,實際還是要靠各位多多練習,隨著專案的不同,需求的詳細程度就會不同。越是複雜的需求,越需要詳細的說明跟規範。雖然撰寫規格會變得很花時間,但是有效的傳達,一定勝過於兩個人不斷來回的溝通修正。</p>
<p>上面兩招,如果能夠融會貫通,已經足以制服大部分的工程師了。<br />
下面介紹的第三招,就算不學也已經很強了,如果能夠學起來,你絕對戰無不勝!</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer-part2/">☞ 第三招:明確的紀錄</a></strong><br />
我們都知道,凡走過必留下痕跡,<s>凡打過必留下血跡</s>(誤)。</p>
<p>記錄好每一次討論的結果,記錄下每一次評估的過程。<br />
如果哪一天工程師說這個地方做不出來,趕快回去翻當初的會議紀錄,奇怪當初跟大家說可以這樣做的不就是工程師你自己嗎?(讓他自打嘴巴,啪啪!)</p>
<p>上面當然是開玩笑的,工程師也會有評估失誤的時候,我們要用寬容的心來包容他。<br />
那紀錄究竟是要記錄什麼呢?</p>
<p>我們要把每個需求跟目標做關聯,也就是說,需求不單單是需求,它會跟整個專案發展的走向有關。比如說,我現在需要開一個新的需求把匯出的功能拿掉,這個需求如果明確紀錄著,它是根據哪一次會議的討論結果中的哪一個項目決定的,我們就可以在閱讀這個需求的時候,清楚知道為什麼會需要做這個改動,以及做這個改動需要注意什麼。</p>
<p>這件事為什麼很重要?因為如果開發者從需求中理解到的意思是有差異的,很可能做出來的東西,跟原先以為的是不一樣的。</p>
<p>舉個例子:<br />
我寫了一個移除匯出功能的需求:</p>
<pre class="language-md"><code class="language-md"><span class="token title important"><span class="token punctuation">###</span> 移除 XX 頁 匯出功能</span><br /><br /><span class="token title important"><span class="token punctuation">####</span> 調整項目</span><br /><br />移除 XX 頁 匯出功能<br /><br /><span class="token title important"><span class="token punctuation">####</span> 預期結果</span><br /><br />XX 頁 僅剩下搜尋功能</code></pre>
<p>接著我再寫一個加上會議紀錄版本的需求:</p>
<pre class="language-md"><code class="language-md"><span class="token title important"><span class="token punctuation">###</span> 移除 XX 頁 匯出功能</span><br /><br />關聯至 2022/02/06 會議紀錄:<br /><br /><span class="token list punctuation">-</span> 整併 xx 頁 及 oo 頁:先移除 xx 頁 匯出功能,於下個版本整併至 oo 頁 當中<br /><br /><span class="token title important"><span class="token punctuation">####</span> 調整項目</span><br /><br />移除 XX 頁 匯出功能<br /><br /><span class="token title important"><span class="token punctuation">####</span> 預期結果</span><br /><br />XX 頁 僅剩下搜尋功能</code></pre>
<p>大家閱讀完上面兩個版本的需求後有沒有感受到差異?<br />
同樣要移除匯出功能,但是加上了會議紀錄的關聯以後,理解到的意思是完全不同的!</p>
<p>如果開發者僅僅獲取了第一版本的資訊,等他做完了以後,是不是下個版號的時候又會需要再重新把功能建立起來,因為獲取的訊息不同,有一定的風險會導致規劃的方向產生偏誤。一旦產生偏誤後,需要再花時間重新溝通,花時間重新修正,花時間重新測試。</p>
<p>當然專案發展走向這種事情,開發者如果有參加會議自己應該就要知道。不過假如我們把這個紀錄做好,並且明確與需求做關聯,工程師就沒有理由再『歡』說:「歐~我當初沒聽到啊」、「歐~需求沒寫清楚啊」。</p>
<p>那除了把會議紀錄關聯至需求以外,還可以把需求彼此做關聯,比如說新的專案模組,它可以由一個大需求(大方向),切分成無數個小需求(issue),每個小需求上面,可以紀錄這個小需求是由哪個大需求切分而來,而大需求也可以紀錄說底下分了幾個小需求(Link issue)出去。(hen 饒舌~)</p>
<p>因為需求跟需求之間有了關聯,所以開發者能夠從這些小需求間找出最合適的開發順序,或者有些時候不同的小需求其實是可以一起做完的,節省開發時間,也降低維護成本。</p>
<p>接下來,討論過程也可以做關聯,<br />
比如說某一個新的規劃方向,它是由當初的某一個討論的結果所衍生出來的。而這個當初討論的內容,就可以與這次的需求做關聯。</p>
<p>舉個例子:</p>
<pre class="language-md"><code class="language-md"><span class="token title important"><span class="token punctuation">###</span> 專案 node 版本升級至 v16</span><br /><br />根據 <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>某某討論連結</span><span class="token punctuation">></span></span> 紀錄,前端需要更新套件,由於新的套件無法支援現在的 node 版本,故需要升級 node 版本至 v16<br />.<br />.<br />.</code></pre>
<p>可能來年某一天,需要回頭翻找當初版本更新的紀錄,我們不但可以找到版本更新的時間,還可以從中得知當初進行版本升級的原因以及討論紀錄。</p>
<p>這些一點一滴的紀錄,在現在這個當下真的很難看出它具有什麼用處。但是一但時間拉長以後,會發現以往做過的每一次完整的紀錄,它都會讓你在參考或者查找資料的時候,變得非常方便。</p>
<p>而且因為有留下討論細節,所以每一次的留言都可以成為一個依據。只要我們有把需求寫好,並且把紀錄給做好(開需求都是根據某某會議的某某討論而來,不是自己亂開的),工程師也都有事先評估好可行性,那最後如果系統運行上出現了問題,我們都可以證明自己的清白(<s>我們是無辜的</s>)。</p>
<h2 id="3-4%EF%BC%9A%E5%BE%9E%E8%88%87%E5%B7%A5%E7%A8%8B%E5%B8%AB%E5%85%B1%E4%BA%8B%EF%BC%8C%E5%88%B0%E8%88%87%E5%B7%A5%E7%A8%8B%E5%B8%AB%E5%85%B1%E8%AD%98"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/cooperate-with-engineer-part2/#3-4%EF%BC%9A%E5%BE%9E%E8%88%87%E5%B7%A5%E7%A8%8B%E5%B8%AB%E5%85%B1%E4%BA%8B%EF%BC%8C%E5%88%B0%E8%88%87%E5%B7%A5%E7%A8%8B%E5%B8%AB%E5%85%B1%E8%AD%98">#</a> 3-4:從與工程師共事,到與工程師共識</h2>
<p>我們統整一下前面提到過的內容,包含 <code>工程師在想什麼?</code>、<code>工程師的思維是什麼?</code>以及 <code>如何制服工程師?</code><br />
如果上面提到的所有內容你都能夠理解的話,你已經開始能與工程師共「識」。</p>
<p>為什麼這麼說呢?<br />
以前我們可能都會認為工程師很難搞、愛用技術名詞來欺壓、每次遇到問題都甩鍋或說做不出來、跟工程師合作好像自己都被欺負、沒辦法理解這些人在想什麼。<br />
你有沒有發現,當你越讀完這兩篇文章以後,這些問題全部都解決了。</p>
<p>因為你了解了技術名詞,不再害怕聽不懂工程師在講什麼,因為你理解了工程師的思維,知道工程師整天都在想什麼,因為你明白如何與工程師有效溝通,你獲得了可以幫助工程師的能力,所以你不再害怕被工程師欺負,你開始與工程師共識。</p>
<p>剛開始入行,可能有許多的不理解或者不適應,如果能藉由簡單的說明,讓大家獲得一些些的幫助,這就是這篇文章最主要的目的。不過我希望大家不要因此與工程師更遙遠,我們有了程式基礎以後,的確不再需常常詢問工程師技術名詞,我們把需求寫完整以後,的確工程師不再需要來向我們詢問需求問題。</p>
<p>也許感覺沒什麼問題是需要再與工程師溝通了,但其實細想之後你會發現,從此刻開始才真的是能夠與工程師有效的溝通。因為我們能聊得更深入了,我們開始能詢問工程師 API 是怎麼串的,資料是怎麼處理的。正因為我們有了一些些程式基礎,所以我們能夠把內容聊的更深。當這些話題已經成為工作中的習慣後,整個團隊的運作就會開始順暢。</p>
<p>其實不光是工程師,各行各業的合作都是如此,我們先能夠了解對方,才知道怎麼幫助對方。<br />
一開始可能沒什麼默契,要靠著不斷的溝通不斷的協調,經過一次次的優化與改進,久而久之,可能簡單的一個暗示,大家開始能理解彼此的想法是什麼。</p>
<p>希望大家都能在工作中順順利利!</p>
<p>以上就是 <code>從與工程師共事,到與工程師共識</code> 系列文全部的內容,<br />
希望這兩篇文章,能夠讓大家離原本距離遙遠的同事更近一點,啊如果你的工程師同事真的是太難搞,要拿這篇文章的招數去對付他,我也是沒有意見(笑</p>
優化舊有專案(二):加入 CI/CD with github actions
2022-02-27T00:00:00Z
https://blog.errorbaker.tw/posts/cwc329/dev-tools-2/
<!-- summary -->
<p>擺在我心中好久的計畫總算付諸行動,以往這個專案的部署都是我手動部署。<br />
這次是要將舊的專案加上 CI/CD,從而減少我的工作量,達成我的懶人願望。</p>
<!-- summary -->
<h1 id="%E4%BD%BF%E7%94%A8%E7%9A%84%E5%B0%88%E6%A1%88---podcastify"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/dev-tools-2/#%E4%BD%BF%E7%94%A8%E7%9A%84%E5%B0%88%E6%A1%88---podcastify">#</a> 使用的專案 - Podcastify</h1>
<p>我使用的是我在 Lidemy 的期末專案,這是我與 <a href="https://github.com/Yu040419">Kuan Yu</a> 以及 <a href="https://github.com/sophiebetough">Sophie Chang</a> 一同合作開發。</p>
<p><a href="https://github.com/Podcastify/Podcastify">專案原始碼</a></p>
<p>這篇文章會以建構簡單的 CI/CD 為主,<br />
前端會使用 github page,而後端則是部署在 heroku 上。</p>
<h2 id="%E5%B8%B3%E6%88%B6%E6%90%AC%E9%81%B7%E4%BB%A5%E5%8F%8A%E9%83%A8%E7%BD%B2%E7%92%B0%E5%A2%83%E8%AE%8A%E6%9B%B4"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/dev-tools-2/#%E5%B8%B3%E6%88%B6%E6%90%AC%E9%81%B7%E4%BB%A5%E5%8F%8A%E9%83%A8%E7%BD%B2%E7%92%B0%E5%A2%83%E8%AE%8A%E6%9B%B4">#</a> 帳戶搬遷以及部署環境變更</h2>
<h3 id="github"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/dev-tools-2/#github">#</a> Github</h3>
<p>原本這個專案的原始碼都是放在我的 github 帳戶下面,不過這就有個問題了。</p>
<p>因為這個 github 帳戶是我自己的東西,當初因為是期末專案,而且最初後端以及環境設定幾乎都是我在處理,所以想說先暫時放在我底下。然而隨著時間過去,目前我們又想要重啟這個專案,我覺得繼續放在我的 github 帳戶下面不是辦法。</p>
<p>首先,因為這個帳戶是我的,也就是我其實擁有這個專案的生殺大權,而其他組員則無。如果今天我想要把整個專案刪掉,其餘組員也無從阻止。我們三個是這個專案的共同創作及擁有者,不應該只有我有這個專案的最高權限。</p>
<p>再者,假設這個專案需要與其他的服務作串接(比如我這次要使用 heroku),因為很多服務都有免費的額度,假設我自己也想要使用,但是卻因為這個專案吃掉我的額度,這對我以及專案都事件麻煩的事情。</p>
<p>基於以上兩點,我們討論後決定創一個專案的帳號,日常的開發及維護都是使用自己的帳號作為協作者。</p>
<h3 id="deploy"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/dev-tools-2/#deploy">#</a> Deploy</h3>
<p>這方面有兩個問題:domain 以及部署方案。</p>
<p>這個專案原本是部署在我的 AWS EC2 上,然而我的免費額度已經用完了,我目前也沒有打算要使用。 如果使用我自己的信用卡或者帳戶,分擔費用這點對我們來說有點麻煩。還有原本的網域名稱因為忘記續約已經被搶佔了。所以新的方案我們想要一次解決這兩個問題。</p>
<p>最後我們決定把前端頁面部署在 github pages,而後端的 API 則部署在 heroku 上。原本會這樣決定是因為我覺得 heroku 好像網址都會是亂數,而 github pages 則會是我們的專案名稱。如果把前端架在 heroku 上會無法有個有辨識度的網址 [^1],這樣對於一個公開網頁來說不是好事。還有,我在上網查如何部署 heroku 時,發現要在一個 repo 下設定部署多個 app 有點複雜,因為有時程關係,這邊首要目的就是在短時間內把整個服務部署到網路上,所以我最後才提案這樣的配置。</p>
<h2 id="ci%2Fcd"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/dev-tools-2/#ci%2Fcd">#</a> CI/CD</h2>
<p>這邊先簡單說明一下 CI/CD。從 <a href="https://en.wikipedia.org/wiki/CI/CD">wiki</a> 的定義,CI/CD 是指 continuous integration 以及 continuous delivery/deploy,這是連結開發以及維運部門的橋樑。軟體開發上流程大致上為:</p>
<pre><code>開發 → 測試 → 封裝 → 部署 → 監控 → 開發 → ...
</code></pre>
<p>這樣的循環,CI/CD 重要的是 continuous 這一點,因為這些流程基本上都是不斷重複的,要如何能做到這整個流程非常地順暢,不被延誤。如果可以做到這點,就能在部署前提早發現問題,或者可以在產品上線之後盡快發現問題,將會對產品有重大的幫助。</p>
<p>自動化佈署其實只是完成 CI/CD 其中一項工作,之後還有監控、回報問題等動作要處理。不過這些動作基本上如果有用其它現成的方案都是包含在裡面的,像是 github actions 以及 heroku 都有自己的 CI/CD 服務,裡面也都會有各種設置可以讓使用者查看、搜尋 log 或者設定錯誤回報等等。</p>
<p><em><strong>以下是我個人的意見</strong></em>:因為這些服務基本上只要把部署整合進去之後,就可以輕鬆的設定測試、監控等其他服務,所以現在大部分的 CI/CD 教學或者心得大部分都會聚焦在如何把自動化部署完成,因為只要能夠完成自動化部署的設定,剩下的只要知道如何使用服務即可。</p>
<h3 id="github-actions-%26-github-pages"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/dev-tools-2/#github-actions-%26-github-pages">#</a> Github Actions & Github Pages</h3>
<p>github actions 是 github 推出的 CI/CD 工具,透過 workflows 可以在 github repo 有異動的時候觸發特定的工作,例如自動測試、上標籤、打包映像檔以及部署等等。</p>
<p>使用者可以用 yaml 檔案去定義一個 workflow,包括這個 workflow 的名字、什麼條件會被執行、執行時的環境變數等。</p>
<p>而一個 workflow 中可以執行多的 actions,這些 actions 可以定義在同一個 repo 中,也可以使用自己其他 repo 的 actions,更可以使用其他人或者 github 官方打包好的 actions。</p>
<p>這次的前端部署我使用 github action [^2],其實很簡單,我在裡面用了兩個 actions,一個是 github 的 <a href="https://github.com/actions/checkout">checkout</a>,這個 actions 可以讓使用者在 workflow 中使用 github 上的專案原始碼。因為 github actions 是 launch 一台虛擬機執行程序,但是這台虛擬機不會有專案的原始碼,所以要使用這個去拿到原始碼。</p>
<p>我使用的第二個 actions 是 <a href="https://github.com/peaceiris/actions-gh-pages">peaceiris/actions-gh-pages@v3</a>,這是一個第三方提供的 github pages 部署 actions,只要使用 github token,並且指定好 source branch 以及要部署到哪個 branch,就可以執行。</p>
<p>因為專案資料夾結構的關係,我在 yaml 中定義只有在 <code>ui/</code> 下面的 <code>*.js</code> 檔案有變化的時候才會執行部署 github pages。</p>
<h3 id="heroku"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/dev-tools-2/#heroku">#</a> Heroku</h3>
<p>後端我選擇部署在 heroku 上,這部分的設定就比較簡單了。</p>
<p>首先我參考 Lidemy 的 <a href="https://lidemy.com/courses/390625/lectures/24510403">heroku 教學</a> 以及 <a href="https://blog.softup.co/how-to-deploy-a-monorepo-to-multiple-heroku-apps-using-github-actions/">How to deploy a monorepo to multiple Heroku apps using GitHub Actions</a>,將我的 app 設定為部署 <code>api/</code> 底下的檔案。接著參照 Lidemy 的 <a href="https://lidemy.com/courses/390625/lectures/24510404">clearDB 教學</a> 設定好我的 heroku app 環境變數,成功連結到資料庫。</p>
<p>不過最後我遇到一些小問題,我的 app 一直遇到</p>
<pre><code>Error: listen EACCES 0.0.0.0:80
</code></pre>
<p>我後來 google 到<a href="https://stackoverflow.com/questions/52992258/nodejs-express-on-heroku-app-eacces-0-0-0-080">這篇</a>,原來是因為 heroku 在部署環境動態指定某個 port,所以如果自己指定 port 的話會有問題。於是我修改了一下原始碼就沒有問題了。</p>
<p>現在我只要更新 master 的 code,就會自動觸發部署 heroku。不過這邊還是有可以優化的空間,因為如果我的改動是 ui 的話,因為 master 上還是有變動,所以依舊會觸發 heroku 的自動部署,這邊應該要改成只有 api 有變動的時候再自動部署會比較好。</p>
<h2 id="%E7%B5%90%E8%AB%96"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/dev-tools-2/#%E7%B5%90%E8%AB%96">#</a> 結論</h2>
<p>在這一篇文章中,除了講解技術我也嘗試把為何選擇這些技術的原因寫出來。我自己工作一年的心得,有些大家公認的優良開發習慣或者規範,像是 TDD、clean code 等,又或者一些很厲害的技術,像是我自己這一整年都在學的 k8s,確實這些技術與規範可以為公司帶來成效,不過這也取決於公司目前狀況。假設公司目前只是要部署一個靜態的形象網站,上線之後基本上不會有大流量,即便斷線也不會造成鉅額損失,那個引入 k8s 感覺就有點多餘。同樣的,假設今天公司需要的是一個 MVP (minimum viable product),可能要趕在一季甚至一個月內上線,而且不保證之後這個產品會持續發展,那麼堅持 clean code 而讓開發時程變長一兩倍可能就不是個好想法。這邊推薦最近 Dan 大的文章 <a href="https://overreacted.io/goodbye-clean-code/">Goodbye, Clean Code</a>,我也沒有說這些技術或者規範都不用遵守,不過凡事都有其代價,要如何拿捏 trade off 我覺得是我這一年工作下來的一點小體悟,在這邊跟大家分享。</p>
<h2 id="%E8%A8%BB%E8%85%B3"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/dev-tools-2/#%E8%A8%BB%E8%85%B3">#</a> 註腳</h2>
<p>[^1]: 其實 heroku 是可以自訂 <a href="http://herokuapp.com/">herokuapp.com</a> 的 subdomain,也可以綁定其他 domain 在 app 上。</p>
<p>[^2]: <a href="https://github.com/Podcastify/Podcastify/blob/master/.github/workflows/deploy-ui.yaml">我的 deploy-ui workflow yaml 檔</a></p>
帶你入門區塊鏈(ㄧ)
2022-03-06T00:00:00Z
https://blog.errorbaker.tw/posts/xiang/block-chain-01/
<!-- summary -->
<p>寫給有想要了解區塊鏈,對於專有名詞不太熟悉,希望有人能透過白話文說明協助導覽的你</p>
<!-- summary -->
<!-- more -->
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/block-chain-01/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<p>這篇文章,想要給有興趣了解區塊鏈運作原理,卻對於程式不太熟悉的人,有一個踏入這個領域的入門點。<br />
內容與虛擬貨幣沒有什麼的關係,也不會針對商業內容有任何闡述,純粹就是透過一般人都可以理解的角度,分享區塊鏈的概念是什麼,以及它有些什麼樣的特性。</p>
<p>閱讀這篇文章以前,不需要對於區塊鏈有任何了解,也不需要任何的程式基礎。<br />
我會以日常生活的情境去帶入,針對一般人會好奇的點,或者大部分人不容易理解的內容來做分享。</p>
<p>從區塊鏈特性開始,帶你搞懂基本的專有名詞,理解基本的運作架構,讓你可以建立簡單的基礎。<br />
未來在了解更深入的應用時,也會更加快速,更好理解。</p>
<blockquote>
<p>本篇文章不會提到加密的概念,由於加密已經算是虛擬貨幣上的應用了,與單純的區塊鏈概念不同,所以這篇文章不會對應用情境有所討論。</p>
</blockquote>
<h2 id="%E5%8D%80%E5%A1%8A%E9%8F%88%E6%98%AF%E4%BB%80%E9%BA%BC%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/block-chain-01/#%E5%8D%80%E5%A1%8A%E9%8F%88%E6%98%AF%E4%BB%80%E9%BA%BC%EF%BC%9F">#</a> 區塊鏈是什麼?</h2>
<p>回朔到我第一次聽到 <code>區塊鏈</code> 這個名詞時,我腦海中浮現的第一個疑問就是:「區塊在哪裡?鏈指的是什麼?是不是每個人的電腦都是一個區塊,然後把每台電腦透過網路連結就是所謂的區塊鏈?」<code>挖礦</code> 是什麼?有一個有資源上限的礦場,大家拼命去把裡面的資源挖出來嗎?</p>
<p>其實光以 <code>區塊鏈</code> 這個名詞,真的沒辦法馬上理解到完整的概念。就算去網路上搜尋,只會得到更多新的專有名詞:<code>複式記帳法</code>、<code>去中心化</code>、<code>加密貨幣</code>...。</p>
<p>大部分網路上的資源提到的都是 <code>區塊鏈</code> 的使用情境,或者生活上應用方式。如果我單純想了解 <code>區塊鏈</code> 本身是什麼東西,很難從這些應用中得到答案。就算去看維基百科的定義,看完也是整個人萌萌噠,完全不懂到底在解釋什麼。</p>
<p>我自己第一次感覺到理解 <code>區塊鏈</code> 是什麼,是等到我看完了 <code>區塊鏈</code> 的程式運作邏輯以後才漸漸曉得的。所以要了解 <code>區塊鏈</code> 是什麼,我認為還是得回到它的程式架構上面來討論。即便我們很難能單純用短短的一句話完美的詮釋 <code>區塊鏈</code> 的定義,但是我相信等我用完整篇文章的篇幅來說明後,大家就能很清楚的在腦海中建立起 <code>區塊鏈</code> 的架構了。</p>
<h2 id="%E5%8D%80%E5%A1%8A%E9%8F%88%E7%9A%84%E7%89%B9%E6%80%A7"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/block-chain-01/#%E5%8D%80%E5%A1%8A%E9%8F%88%E7%9A%84%E7%89%B9%E6%80%A7">#</a> 區塊鏈的特性</h2>
<p>在談到程式的架構以前,我想先提一下區塊鏈所擁有的 <code>特性</code>,讓大家在腦海中有一些可以關注的方向,這樣待會提到程式架構時,會比較好理解為什麼程式會這樣去設計。</p>
<p>☞ 1. 去中心化<br />
所謂的去中心化,大家可以想像成,不透過中介機構去管理或運行。像我們日常生活中的大小事務,很多都是中心化的:例如我們將錢匯給別人需要透過銀行,或者傳遞訊息需要透過通訊軟體,這些都是中心化,因為有一個中介機構負責管理與執行。<br />
而 <code>去中心化</code> 就是不再透過這些中介機構,大家自己就是自己的銀行,自己管理好自己的錢,當有交易進行的時候,誰給了誰多少錢,會讓所有人都知道,所有人都會把所有交易紀錄寫進自己的帳本當中,這些資訊會分散在每個人手上,不再只是存在一個銀行系統。</p>
<p>☞ 2. 匿名性<br />
在中心化的系統中,我們需要在銀行開戶,留下個人資料。每一次進行交易時,都是以個人名義進行,銀行會知道是誰將錢匯款給了誰,政府也可以去查每個人手上擁有多少財產。<br />
而 <code>去中心化</code> 系統中,每個人只需要擁有一組密碼,不需要任何個人資料就可以註冊,交易也是以該密碼的名義去進行,不會有人知道這些密碼的背後代表的人究竟是誰,所有的交易都是匿名且隱蔽的。</p>
<p>☞ 3. 加密<br />
加密其實是算虛擬貨幣上的應用,<code>區塊鏈</code> 本身的運作不需要透過加密就可以達成(底下的原理說明也不會談到加密),只是因為現今 <code>區塊鏈</code> 的應用都以虛擬貨幣為主,虛擬貨幣又需要有完善的加密系統,所以很多人會把加密這個特性跟 <code>區塊鏈</code> 一起討論。<br />
加密的定義很簡單,當有某些訊息只希望讓特定的第三方看到,我們就可以透過加密來達成。</p>
<p>☞ 4. 不可竄改<br />
這個特性非常重要!區塊鏈上的資料 <code>不可竄改</code>,意思是每一筆被記錄在區塊鏈上的資料,基本上一定是安全的。至少以現今的電腦或者硬體設備,都無法達成竄改資料的可能。(底下的原理說明,很大的篇幅都在讓大家理解區塊鏈是如何達成 <code>不可竄改</code> 的。)</p>
<p>☞ 5. 共識機制<br />
為了確保區塊鏈上的資料安全,以及達成 <code>不可竄改</code> 的目的,區塊鏈必須具備特定的 <code>共識機制</code>,來定義清楚,如何可以將資料寫進區塊鏈,誰有權力把資料寫進去,如何證明這個人有這個權利,這套證明機制就是所謂的 <code>共識機制</code>。</p>
<p>講完上面這 5 項區塊鏈的 <code>特性</code> 以後,相信大家一定對區塊鏈更加的陌生了!(這很正常,<s>專有名詞本來就是為了讓人更混淆的</s>)<br />
其實這些特性的背後,本質都是希望資料能夠安全,如果有一個中心化的機構來把持所有人的資源,那這個機構一但倒閉,或者有駭客竊取資料,甚至極端一點的情況是這個機構背叛了,那所有人的資料通通都會暴露在危險之中。所以區塊鏈的出現,本質就是希望能有一套系統,它不需要依賴中介機構,它是絕對安全的,而且可以匿名保護好自己的隱私。<br />
而這套系統衍伸出來最合適的情境,就是 <code>交易</code> 以及 <code>貨幣</code>,所以現今區塊鏈最主要的應用情境,就是大家常常會聽到的 <code>虛擬貨幣</code>。</p>
<p>以上我們知道了區塊鏈具備的 <code>特性</code>,以及這套系統發明背後所希望達成的 <code>目的</code> 以後,我們就可以開始來討論區塊鏈是如何運作,以及它透過什麼方式來確保資料的安全。</p>
<h2 id="%E5%8D%80%E5%A1%8A%E9%8F%88%E7%9A%84%E5%8E%9F%E7%90%86"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/block-chain-01/#%E5%8D%80%E5%A1%8A%E9%8F%88%E7%9A%84%E5%8E%9F%E7%90%86">#</a> 區塊鏈的原理</h2>
<p>在準備進入區塊鏈的世界之前,先給大家一個情境:<br />
現在假設我們回到了學生的時代,重拾鉛筆與橡皮擦,坐在書桌前,開始撰寫眼前的考券。</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/block-chain-01/">☞ 出題</a></strong><br />
現在我們手上有一張空白的考卷,我們要在考卷上面出題,出的題目只能是 <code>單選題</code>,而選項從 0 ~ 9 (共 10 個) 、 A ~ Z(共 24 個)、ㄅ ~ ㄦ(共 37 個),全部加起來共要有 71 個選項,每一題都是如此。<br />
意思是不管題目的難度是簡單或者困難,就算考題的內容是「1 加 1 等於多少?」這麼簡單的題目,一樣要給滿 71 個選項。所以每一題要猜對的機率就固定為 1 / 71。<br />
除了上面的條件以外,我們再給出題規則加上一個限制:「只要題目被改變,答案就一定會變」。</p>
<p>所以我們 <code>出題</code> 就會有以下幾個特性:</p>
<ul>
<li>每一題只對應一個答案</li>
<li>不管題目的長度多長,答案的長度都是固定的(1 個字母 or 1 個數字 or 1 個注音)</li>
<li>答案一定不可能回推題目的內容(我不可能因為知道答案是 A,就回推出題目的內容)</li>
<li>只要題目有改變,答案一定會變</li>
</ul>
<p>上面幾點是出題的特性,我們假設所有的題目都一定會滿足這幾條規定,而且不會有例外。</p>
<blockquote>
<p>而出題者,是你我在座每一個人,我們每一個人都有資格出題目,並且把我們手上的題目交由解題者進行解題。</p>
</blockquote>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/block-chain-01/">☞ 解題</a></strong><br />
我們開始來說明 <code>解題</code> 的規則:</p>
<ul>
<li>不限時間</li>
<li>只能考剛好 85 分</li>
<li>只採納最快的人的答案</li>
</ul>
<p>解題開始時,會有無數位解題者同時進行解題,不論題目有多少,也不管是誰來解題。所有解題者在解自己的考卷時,都必須要剛好考 85 分才算通過(不能多也不能少)。<br />
由於會有很多解題者同時解題,所以我們只採納最快考出 85 分的人的答案,這個最快的人,會得到獎勵。<br />
只要一有人把考卷解完,其他人就要幫他檢查是不是剛好 85 分,檢查沒問題以後,這張考卷就是公認的考卷,並且被列印出來,每一個人手上都會發一張。而其他解題者全部停止作答。</p>
<p>接下來給每一位解題者發新的一張考卷,再次開始解題,一樣取最快考出 85 分的人,檢查是否通過,把考卷列印出來,發給每一個人,其他人停止作答,週而復始。</p>
<blockquote>
<p>不是每個人都可以擔任解題者,解題者需要有解題的能力,而且如果無法成為最快考出 85 分的人,就永遠拿不到獎勵。</p>
</blockquote>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/block-chain-01/">☞ 考卷</a></strong><br />
上面提到了出題以及解題的遊戲規則,在這邊我想補充一下關於考卷的細節。<br />
雖然出題者是在座的每一個人,而且一張考卷的題目可能來自無數不同的出題者,但有一個 <code>考卷</code> 限制在於:</p>
<ul>
<li>每一張考卷的第一題一定是問:上一張考卷的答案是多少?</li>
<li>這一題是規定是不可以答錯的</li>
</ul>
<p>舉個例子:如果剛剛有一個最快的解題者將考卷考出剛好 85 分,而且經過其他人確認沒有問題,那張考卷的答案是 <code>Cㄅ7DGㄨEㄠ3Eㄚㄎ</code>,下一張新的考卷,第一題的答案就一定要選擇答案是 <code>Cㄅ7DGㄨEㄠ3Eㄚㄎ</code> 的那個選項。</p>
<p>所以每一張考卷的第一題,都會記錄著前一張考卷的那組考出 85 分的答案,一張接著一張串連起來,形成一條 <code>考卷鏈</code>。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/block-chain-01-01.png" alt="" /></p>
<p><code>考卷鏈</code> 的架構就會像上圖一樣,每一張都是通過大家驗證,滿足條件的考卷。<br />
由於每一次有考卷被解出來時,都會列印出來給每個人,大家手上都會發一張,所以每個人的手上都會有一模一樣的 <code>考卷鏈</code>,大家拿到的考卷內容跟順序都是一樣的。</p>
<p>當我們想要驗證這條 <code>考卷鏈</code> 有沒有問題的時候,只需要透過幾個簡單的方式去檢查:</p>
<ul>
<li>考卷是不是每一張都是 85 分?</li>
<li>考卷的第一題是不是都有對應前一張考卷的答案?</li>
<li>考卷的內容是不是跟別人手上的一樣?</li>
</ul>
<p>只要我們能夠確定這幾個條件都有滿足,這條 <code>考卷鏈</code> 就一定是對的。<br />
為什麼我敢保證滿足上面的條件,<code>考卷鏈</code> 一定是對的,我們下面直接來嘗試竄改看看,就能知道為什麼了。</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/block-chain-01/">☞ 竄改</a></strong><br />
假設我現在想嘗試 <code>竄改</code> 考卷上的資料,我們來看看竄改後會發生什麼事情。<br />
我選擇了下面這張考卷的第三題,我想嘗試將原本的題目做竄改:</p>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/block-chain-01-02.png" alt="" /></p>
<p>一但我試圖將第三題題目做竄改,由於 <code>出題規則</code> 有提到,<code>只要題目有改變,答案一定會變</code>,所以第三題的答案不再是 <code>7</code> 了,原本選了 <code>7</code> 的答案現在對應同一題題目,就會從對的答案變成了錯的答案,也就是說,這張考卷將不再是 <code>85 分</code> 了!</p>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/block-chain-01-03.png" alt="" /></p>
<p>既然我手上這張考卷它不在是 85 分,就代表它絕對不會是對的考卷。為了重新讓它滿足 85 分,我只好把考卷 <code>重解</code>!<br />
利用 <code>重解</code> 的方式,讓這張考卷再次滿足 85 分。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/block-chain-01-04.png" alt="" /></p>
<p>這張考卷滿足 85 分的條件以後,手上的考卷練就會變成下面這樣:</p>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/block-chain-01-05.png" alt="" /></p>
<p>由於剛剛那張考卷被 <code>重解</code> 過,對於下一張考卷而言,第一題就不再是正確的,所以分數不再是 85 分!<br />
大家有沒有發現,如果我想讓下一張考卷再次滿足 85 分,會導致它的再下一張考卷第一題答錯,不再滿足 85 分。</p>
<blockquote>
<p>如果我試圖竄改考卷,就必須將該份考卷以及之後的每一張考卷通通 <code>重解</code>,才有辦法再次讓整條 <code>考卷鏈</code> 滿足 85 分的規則。</p>
</blockquote>
<p>而且 <code>重解</code> 的速度,還要快過後面新的考卷產生的速度,否則大家會直接採納最快解出考卷的人的答案,不會去等到你重解完整條考卷鏈。<br />
而且經過重解過的 <code>考卷練</code>,最後一張考卷的內容一定會跟別人長的不一樣。每個人手上都會有一條 <code>考卷鏈</code>,當所有人之中只有你一個人的 <code>考卷鏈</code> 長的跟別人不一樣,一看就知道這條考卷鏈有問題。</p>
<p>如此一來就能夠使 <code>竄改</code> 考卷變得很困難!尤其是越前面的考卷,被竄改的可能性就會越低。<br />
由於 <code>去中心化</code> 的特性,我們不用擔心資料只存放在一個地方會不會遺失,會不會被攻擊。就算自己手上的資料不見了,我隨時複製別人手上的資料過來就好。</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/block-chain-01/">☞ 考卷鏈系統</a></strong></p>
<p>我們回顧一下整個 <code>考卷鏈</code> 系統,所有人都是 <code>出題者</code>,出的題目都是單選題,而且滿足一但題目改變答案一定會改變的特性。<br />
並且有無數個擁有解題能力的 <code>解題者</code>,他們負責在拿到考卷時負責解題,需要滿足剛好 85 分的條件,由大家檢查通過以後,得到獎勵,該份考卷影印下來發給每個人。其他解題者停止作答,準備進入下一輪解題。而隨著考卷一張一張被解出來,每個人的手上就會有一條一模一樣的 <code>考卷鏈</code>。且這條 <code>考卷鏈</code> 具備不容易被竄改的特性。</p>
<h2 id="%E5%8D%80%E5%A1%8A%E9%8F%88%E7%B3%BB%E7%B5%B1"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/block-chain-01/#%E5%8D%80%E5%A1%8A%E9%8F%88%E7%B3%BB%E7%B5%B1">#</a> 區塊鏈系統</h2>
<p>如果上面的 <code>考卷鏈</code> 流程,能讓你在腦海中浮現一條 <code>考卷鏈</code> 形成的畫面,那就成功幫你建立起 <code>區塊鏈</code> 形成的基礎了。接下來我們從學生時期,重新回到區塊鏈的討論上面。藉由下面的區塊鏈系統說明,幫助大家補齊剩餘的重要觀念。<br />
前面提到的 <code>考卷</code>,概念就跟 <code>區塊</code> 很像</p>
<p>每一張 <code>考卷</code> 會紀錄著:</p>
<ul>
<li>題目</li>
<li>答案</li>
<li>分數</li>
<li>上一張考卷的答案</li>
</ul>
<p>而每一個 <code>區塊</code> 會紀錄著:</p>
<ul>
<li>交易內容</li>
<li>亂數值 (Nonce)</li>
<li>雜湊值 (Hash)</li>
<li>上一個區塊的雜湊值 (Previous Hash)</li>
</ul>
<blockquote>
<p>因為現在區塊鏈主要的應用情境為虛擬貨幣,所以紀錄的資料就會以 <code>交易內容</code> 為主</p>
</blockquote>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/block-chain-01/">☞ 雜湊</a></strong><br />
雜湊是透過演算法,將一段任意長度的資料,轉換為固定長度的結果。就如同 <code>出題規則</code> 中任意長度的題目,轉換為固定長度的答案。<br />
<code>雜湊</code> 的特性如下:</p>
<ul>
<li>任意長度的輸入,會轉換為固定長度的輸出</li>
<li>同樣的輸入,會產生一樣的輸出結果</li>
<li>輸出的結果一定不可能回推出輸入值</li>
<li>輸入值有任何改變,輸出值一定會改變</li>
</ul>
<p>所以當我們輸入值為: 西瓜、葡萄、2022、上班<br />
它如果輸出的結果為: 4kjsdlfj3i2ojli</p>
<p>那我們把輸入值做一點點調整: 西瓜、葡萄、202、上班<br />
輸出結果就會改變: dgfjkklj23f3245</p>
<p>你會發現,任何輸入值有一點點的改變都會使產出的 <code>雜湊值</code> 長的完全不同,所以我們可以透過這個特性對 <code>雜湊值</code> 定義一些規則。(例如:我希望最後雜湊值的結果,要滿足前四碼為 0000 開頭,像是 0000i3jfkn3l1if,就是滿足規則的雜湊值。)而這個規則待會我會提到他的用處!</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/block-chain-01/">☞ 容量</a></strong><br />
考卷的容量有限,當題目已經寫滿後,這張考卷就必須交給解題者進行解題。而區塊的容量也是有限制的,新的區塊會被新的交易紀錄一筆一筆填滿,一但區塊填滿,它就會準備進入挖礦的程序。所以不會是一筆交易就是一個區塊,每一個區塊只要容量還足夠,就可以繼續塞入交易資料。</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/block-chain-01/">☞ 挖礦</a></strong><br />
<code>挖礦</code> 就像 <code>解題</code> 一樣,當交易一筆一筆產生時,新的區塊會被填滿,一但區塊滿了,就會交由 <code>礦工</code> 進行 <code>挖礦</code>,<code>挖礦</code> 的過程就是,將這個區塊當中的 <code>交易紀錄</code>、<code>上一個區塊的雜湊值</code>、<code>亂數值</code> 這些資料一起進行 <code>雜湊</code>。</p>
<p>並且看看取得的雜湊值是否滿足規定,如果產生的雜湊值不符合規定(假如規定是前四碼為 0000,那我產生的雜湊直如果為 4kjsdlfj3i2ojli,就沒有符合規定),就改變 <code>亂數值</code> 在做一次 <code>雜湊</code>,再看看是否符合規定,如果還是不符合,就再改變 <code>亂數值</code>,一直試一直試,直到找出可以滿足規定的 <code>亂數值</code> 。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/block-chain-01-06.png" alt="" /></p>
<p>例如:當我將 <code>亂數值</code> 改成 542343,產生的 <code>雜湊值</code> 為 0000d9c8e0j3l4j 符合規定,就代表我成功 <code>出塊</code>(挖出區塊),其他人就會來驗證是否滿足 0000 開頭,驗證通過之後,我就可以將我挖出的區塊向所有人進行 <code>廣播</code>。</p>
<p>所以挖礦,就是一群礦工在比誰可以最快找出滿足規定的 <code>亂數值</code>,最快找到的人,就可以獲得獎勵。而挖到的區塊,就會 <code>上鏈</code>(上到公認的主鏈),成為公認的區塊。當區塊一個一個被挖出來,就會一塊接著一塊,形成所謂的 <code>區塊鏈</code>。</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/block-chain-01/">☞ 區塊鏈</a></strong><br />
由於每一個區塊會紀錄著前一個區塊的 <code>雜湊值</code>,且每一個區塊的 <code>雜湊值</code> 都會滿足 0000 開頭這個條件,所以要驗證區塊是否符合規定,只需要去確認每個區塊的 <code>雜湊值</code> 是否都是為 0000 開頭,就可以確認區塊鏈是否是對的。</p>
<p>考卷只要有任何題目被改變,就會使該張考卷不再滿足 85 分。同樣的,區塊當中任何內容被竄改,不論是 <code>交易紀錄</code>、<code>亂數值</code>、<code>上個區塊的雜湊值</code>,只要任何個數值有改變,會使這個區塊的 <code>雜湊值</code> 不再滿足 0000 開頭,這個區塊就不符合規定了。</p>
<p>考卷不滿足 85 分,就得重解該張考卷的題目。同樣的,區塊鏈的 <code>雜湊值</code> 不符合規定,就得重新挖礦找出 <code>亂數值</code>,就算重算過後這個區塊的 <code>雜湊值</code> 對了,也會導致下一個區塊的 <code>雜湊值</code> 改變(因為對於下個區塊而言 <code>前個區塊的雜湊值</code> 改變了)。</p>
<p>你會發現,當區塊透過 <code>雜湊值(Hash)</code> 以及 <code>亂數值(Nonce)</code> 鏈起來以後,任何一個區塊要竄改,都需要把該區塊之後的所有區塊都重挖一次,而且計算速度還需要比其他所有礦工都快。這個機制能確保 <code>區塊鏈</code> 上資料不容易被竄改。</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/block-chain-01/">☞ 區塊鏈系統</a></strong><br />
理解了 <code>區塊鏈</code> 的原理以後,我們可以把 <code>區塊鏈</code> 的運作,用一個簡單的循環來進行描述:</p>
<ol>
<li>用戶與用戶間進行交易,交易紀錄會先暫存在一個空間,等待被放入區塊</li>
<li>礦工會把暫存的交易移入自己的區塊,當區塊滿了以後,開始進行挖礦</li>
<li>當區塊被挖出來後,其他人會檢查是否符合規則,只要合格,該區塊就會上到公鏈,該名礦工就可以獲得獎勵</li>
<li>重複以上步驟,持續新的交易、持續挖礦</li>
</ol>
<p>最基本的 <code>區塊鏈架構</code>,就如同上面的循環,不論何種應用情境,都離不開這一個基礎。<br />
如果在區塊上存放交易資訊,它可能就會是 <code>虛擬貨幣</code> 性質的區塊鏈。如果存放是別種資料,它可能就是別種用途的區塊鏈。</p>
<h2 id="%E7%B8%BD%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/block-chain-01/#%E7%B8%BD%E7%B5%90">#</a> 總結</h2>
<p>這次的入門主題,透過考卷的出題、解題,讓大家了解 <code>區塊</code> 以及 <code>鏈</code> 的概念,實際模擬運作的流程,是如何從一筆一筆的資料,進行挖礦,再到出塊最後成功上鏈。</p>
<p>我們了解到用戶與區塊鏈之間的關係、礦工與區塊鏈之間的關係,只要交易不斷進行,就會有新的區塊不停的產生,只要上到區塊鏈的資料,就具備不可竄改的特性。</p>
<p>還有很多區塊鏈的細節,在這篇文章裡面沒有提到,可能你心中還存在許多疑問:<br />
誰有資格進行挖礦?區塊的容量上限是多少?交易的手續費怎麼計算?加密的原理是什麼?雜湊值的規定會不會調整?多久時間能產生新的區塊?</p>
<p>或許這些疑問沒能在這篇文章獲得解答,但我相信閱讀完這篇文章,理解了基本概念以後,要再去搜尋這些問題的答案,其實就不再是這麼困難的事情了!</p>
<p>希望這篇文章,有成功帶給大家一些幫助,讓我們一起入門區塊鏈。</p>
<p><a href="https://blog.errorbaker.tw/posts/xiang/block-chain-02/">下一篇文章</a>,會以程式的角度為大家說明,區塊鏈程式的運作具備哪一些要件?它如何運行與實作?</p>
web Push Notifications
2022-03-13T00:00:00Z
https://blog.errorbaker.tw/posts/ruofan/fcm/
<!-- summary -->
<p>Hi,大家好! 前陣子在研究使用 Firebase Cloud Messaging 傳送通知到 web。 這篇文章會和大家分享如何搭配 service worker 實作。</p>
<!-- summary -->
<!-- more -->
<h2 id="%E4%BB%80%E9%BA%BC%E6%98%AF-firebase-cloud-messaging%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/fcm/#%E4%BB%80%E9%BA%BC%E6%98%AF-firebase-cloud-messaging%EF%BC%9F">#</a> 什麼是 Firebase Cloud Messaging?</h2>
<p>以下是 <a href="https://firebase.google.com/docs/cloud-messaging">Firebase 官方文件</a> 上的說明。</p>
<blockquote>
<p>Firebase Cloud Messaging (FCM) is a cross-platform messaging solution that lets you reliably send messages at no cost.</p>
</blockquote>
<p>透過 FCM 讓我們可以以低成本的方式傳送訊息到 web 或是 app。 這邊以 web 為例,使用者的瀏覽器以及通知設定在開啟的前提下,是可以收到通知的。</p>
<blockquote>
<p>這個功能如此低成本看似相當方便,但是還是有許多額外需要注意的地方。 iPadOS and iOS 15.4 web Push Notifications 還在實驗階段,目前預設是 disabled,因此這部分的實作不適用在 iphone 以及 ipod。 詳細資訊可以看 <a href="https://developer.apple.com/documentation/safari-release-notes/safari-15_4-release-notes">safari Release Notes</a></p>
</blockquote>
<h2 id="push-notifications-%E5%9C%A8-web-%E4%B8%8A%E6%98%AF%E5%A6%82%E4%BD%95%E9%81%8B%E4%BD%9C%E7%9A%84%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/fcm/#push-notifications-%E5%9C%A8-web-%E4%B8%8A%E6%98%AF%E5%A6%82%E4%BD%95%E9%81%8B%E4%BD%9C%E7%9A%84%EF%BC%9F">#</a> push Notifications 在 web 上是如何運作的?</h2>
<p>透過瀏覽器背後實作的 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Push_API">push service</a>,搭配 service worker 收取與產生通知給使用者。<br />
從下方的圖片上可以觀察到各家瀏覽器對於 notification 的支援度。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/notification.png" alt="" /><br />
圖片來源: <a href="https://caniuse.com/?search=notification">caniuse</a></p>
<h2 id="%E4%BB%80%E9%BA%BC%E6%98%AF-service-worker%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/fcm/#%E4%BB%80%E9%BA%BC%E6%98%AF-service-worker%EF%BC%9F">#</a> 什麼是 Service worker?</h2>
<p>service worker 是 web worker API 中的其中一種 worker。</p>
<p>以下是 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API#web_worker_interfaces">mdn web docs</a> 上的說明。</p>
<blockquote>
<p>Web Workers makes it possible to run a script operation in a background thread separate from the main execution thread of a web application.</p>
</blockquote>
<blockquote>
<p>Service Workers offering offline capabilities, including handling notifications, performing heavy calculations on a separate thread, etc. Service workers are quite powerful as they can take control over network requests, modify them, serve custom responses retrieved from the cache, or synthesize responses completely.</p>
</blockquote>
<p>當 web 收到訊息後需要啟用 service worker 來收取通知,我們可以透過 service worker API 中提供的 <code>register</code> 與 <code>skipWaiting</code> 方法來下載與啟用 service worker。 產生通知給使用者則是透過 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API">Notifications API</a>。</p>
<pre class="language-javascript"><code class="language-javascript"> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token string">"serviceWorker"</span> <span class="token keyword">in</span> navigator<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> window<span class="token punctuation">.</span>navigator<span class="token punctuation">.</span>serviceWorker<br /> <span class="token punctuation">.</span><span class="token function">register</span><span class="token punctuation">(</span><span class="token string">"/firebase-messaging-sw.js"</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><br /> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">registration</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> title <span class="token operator">=</span> <span class="token string">'SEND NOTIFICATION FROM CLICK EVENT'</span><br /> <span class="token keyword">const</span> body <span class="token operator">=</span> <span class="token string">'send background message from click event'</span><br /> registration<span class="token punctuation">.</span><span class="token function">showNotification</span><span class="token punctuation">(</span>title<span class="token punctuation">,</span> <span class="token punctuation">{</span>body<span class="token punctuation">}</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token comment">/*catch*/</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">error</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"Service worker registration failed:"</span><span class="token punctuation">,</span> error<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"Service workers are not supported."</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span></code></pre>
<pre class="language-javascript"><code class="language-javascript">self<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"message"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>event<span class="token punctuation">.</span>data <span class="token operator">&&</span> event<span class="token punctuation">.</span>data<span class="token punctuation">.</span>type <span class="token operator">===</span> <span class="token string">"SKIP_WAITING"</span><span class="token punctuation">)</span> self<span class="token punctuation">.</span><span class="token function">skipWaiting</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<h2 id="%E9%96%8B%E5%A7%8B%E5%AF%A6%E4%BD%9C%E5%90%A7%EF%BC%81"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/fcm/#%E9%96%8B%E5%A7%8B%E5%AF%A6%E4%BD%9C%E5%90%A7%EF%BC%81">#</a> 開始實作吧!</h2>
<p>在 firebase console 中的網路設定取得網路推播憑證的金鑰。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/fcm-firebase.png" alt="" /></p>
<p>透過金鑰讓我們可以在 firebase 上傳送測試訊息。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/firebase-test-notification.png" alt="" /></p>
<p>在使用者啟用通知設定的情境下,可以透過 firebase 提供的 <code>getToken</code> 方法取得 token。</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> <span class="token punctuation">{</span> getMessaging<span class="token punctuation">,</span> getToken <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"firebase/messaging"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> messaging <span class="token operator">=</span> <span class="token function">getMessaging</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token function">getToken</span><span class="token punctuation">(</span>messaging<span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">vapidKey</span><span class="token operator">:</span> <span class="token string">'<VAPID_KEY>'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">currentToken</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>currentToken<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// Send the token to server</span><br /> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'No registration token available. Request permission to generate one.'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">catch</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">err</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'An error occurred while retrieving token. '</span><span class="token punctuation">,</span> err<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>取得 token 後就可以使用 firebase 提供的 <code>onMessage</code> 以及 <code>onBackgroundMessage</code> 方法收取與產生通知給使用者。</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> <span class="token punctuation">{</span> getMessaging<span class="token punctuation">,</span> onMessage <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"firebase/messaging"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> messaging <span class="token operator">=</span> <span class="token function">getMessaging</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token function">onMessage</span><span class="token punctuation">(</span>messaging<span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">payload</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'Message received. '</span><span class="token punctuation">,</span> payload<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> <span class="token punctuation">{</span> getMessaging<span class="token punctuation">,</span> onBackgroundMessage <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"firebase/messaging"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> messaging <span class="token operator">=</span> <span class="token function">getMessaging</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token function">onBackgroundMessage</span><span class="token punctuation">(</span>messaging<span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">payload</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'[firebase-messaging-sw.js] Received background message '</span><span class="token punctuation">,</span> payload<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token comment">// Customize notification here</span><br /> <span class="token keyword">const</span> notificationTitle <span class="token operator">=</span> <span class="token string">'Background Message Title'</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> notificationOptions <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token string">'Background Message body.'</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">icon</span><span class="token operator">:</span> <span class="token string">'/firebase-logo.png'</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br /> self<span class="token punctuation">.</span>registration<span class="token punctuation">.</span><span class="token function">showNotification</span><span class="token punctuation">(</span>notificationTitle<span class="token punctuation">,</span><br /> notificationOptions<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /></code></pre>
<p>這邊在 <code>vite config.json</code> 搭配 <code>vite plugin pwa</code> 來 build service worker。<br />
<code>vite plugin pwa</code> 背後是透過 Workbox 實作 build service worker,詳細資訊可以到 <a href="https://vite-plugin-pwa.netlify.app/">vite plugin pwa 官方文件</a> 觀看。</p>
<pre class="language-json"><code class="language-json">import <span class="token punctuation">{</span> defineConfig <span class="token punctuation">}</span> from 'vite'<br />import vue from '@vitejs/plugin-vue'<br />import <span class="token punctuation">{</span> resolve <span class="token punctuation">}</span> from 'path'<br />import <span class="token punctuation">{</span> VitePWA <span class="token punctuation">}</span> from <span class="token string">"vite-plugin-pwa"</span>;<br /><br />const pwaOptions = <span class="token punctuation">{</span><br /> <span class="token comment">// mode: "development",</span><br /> base<span class="token operator">:</span> <span class="token string">"/"</span><span class="token punctuation">,</span><br /> includeAssets<span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"/favicon.png"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> manifest<span class="token operator">:</span> <span class="token punctuation">{</span><br /> name<span class="token operator">:</span> <span class="token string">"notification"</span><span class="token punctuation">,</span><br /> short_name<span class="token operator">:</span> <span class="token string">"notification"</span><span class="token punctuation">,</span><br /> description<span class="token operator">:</span> <span class="token string">"Get notifications"</span><span class="token punctuation">,</span><br /> theme_color<span class="token operator">:</span> <span class="token string">"#ced4da"</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> srcDir<span class="token operator">:</span> <span class="token string">"src/service"</span><span class="token punctuation">,</span><br /> strategies<span class="token operator">:</span> <span class="token string">"injectManifest"</span><span class="token punctuation">,</span><br /> filename<span class="token operator">:</span> <span class="token string">"firebase-messaging-sw.js"</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span>;<br /><br /><br />export default defineConfig(<span class="token punctuation">{</span><br /> plugins<span class="token operator">:</span> <span class="token punctuation">[</span>vue()<span class="token punctuation">,</span> VitePWA(pwaOptions)<span class="token punctuation">]</span><span class="token punctuation">,</span><br /> resolve<span class="token operator">:</span> <span class="token punctuation">{</span><br /> alias<span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"@"</span><span class="token operator">:</span> resolve(__dirname<span class="token punctuation">,</span> <span class="token string">"src"</span>)<span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> server<span class="token operator">:</span> <span class="token punctuation">{</span><br /> open<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span>);<br /></code></pre>
<h2 id="%E5%B0%8F%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/fcm/#%E5%B0%8F%E7%B5%90">#</a> 小結</h2>
<p>閱讀文獻的過程中花了許多時間在理解 pwa 以及 service worker,整體來說蠻有趣的!<br />
在閱讀文章時如果有遇到什麼問題,或是有什麼建議,都歡迎留言告訴我,謝謝。😃</p>
<p>完整程式碼可以透過這個 <a href="https://github.com/ruofanwei/vue-fcm">Github | Repository: vue-fcm</a> 觀看。</p>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/fcm/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li><a href="https://developers.google.com/web/updates/2018/05/beyond-spa?hl=en">Document | architectures for your PWA</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker">Document | serviceWorker</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API">Document | web Workers API</a></li>
<li><a href="https://developers.google.com/web/ilt/pwa/introduction-to-push-notifications">Document | push Notifications</a></li>
<li><a href="https://firebase.google.com/docs/cloud-messaging">Document | firebase Cloud Messaging</a></li>
<li><a href="https://developer.apple.com/documentation/safari-release-notes/safari-15_4-release-notes">Document | safari 15.4 Beta Release Notes</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Re-engageable_Notifications_Push">Document | how to make PWAs re-engageable using Notifications and Push</a></li>
<li><a href="https://blog.mozilla.org/services/2016/08/23/sending-vapid-identified-webpush-notifications-via-mozillas-push-service/">Blog | sending VAPID identified WebPush Notifications via Mozilla’s Push Service</a></li>
</ul>
TS, JS 漫談 - 編譯與直譯
2022-03-27T00:00:00Z
https://blog.errorbaker.tw/posts/cwc329/2022-03-27/
<!-- summary -->
<p>TS 與 JS 誰好誰壞先不論,TS 新手如我很多事情也是在開發的時候慢慢了解的,這次想跟大家從 TS 與 JS 來談談編譯語言與直譯語言。</p>
<!-- summary -->
<h2 id="typescript-vs-javascript"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/2022-03-27/#typescript-vs-javascript">#</a> TypeScript VS JavaScript</h2>
<p>TypeScript(TS) 是微軟推出並且維護的一套基於 JavaScript(JS) 的程式語言,其特色在於實作型別、介面等。詳情可以看他們的<a href="https://www.typescriptlang.org/">官網</a>。</p>
<p>在使用 TS 與 JS 有滿多不同的,但是其實他們最底層都還是執行 JS;換句話說,TS 如果真的要執行的話,要先轉換成 JS 然後才執行,這也就是為何 TS 與 JS 可以在同一個專案並存的原因,因為他們在骨子裡其實都是一樣的。</p>
<p>這個一樣的到底是怎麼回事?這個我們可以先看一下官網的教學<br />
<img src="https://blog.errorbaker.tw/img/posts/cwc329/ts-and-js/1.png" alt="" /><br />
這邊可以發現教學說在執行 TS 檔案之前要先執行 TS 的編譯器</p>
<pre class="language-shell"><code class="language-shell">npx tsc</code></pre>
<p>這個步驟有什麼用呢?這是因為一般常用的 JS 執行環境,也就是瀏覽器與 Node.js,其實無法辨別與執行 <code>.ts</code> 檔案,因為裡面有很多不是 JS 的語法,而 <code>tsc</code> 這個指令就是把這些 <code>.ts</code> 檔案編譯成 <code>.js</code> 檔,好讓 JS 的執行環境可以順利執行。</p>
<h3 id="%E7%B7%A8%E8%AD%AF%E8%88%87%E7%9B%B4%E8%AD%AF"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/2022-03-27/#%E7%B7%A8%E8%AD%AF%E8%88%87%E7%9B%B4%E8%AD%AF">#</a> 編譯與直譯</h3>
<p>在初學 TS 的時候,我的 lead 說引入 TS 可以讓很多錯誤在編譯的時候就被找到,而不是在執行的時候才拋出錯誤。一開始聽到這段話我其實一知半解,因為 lead 在導入 TS 的時候已經把很多底層的指令都封裝打包好,讓大家在開發的時候只要依照以前下的指令即可,完全沒有自己有使用到 <code>tsc</code> 的感覺。直到我自己從頭開始建立 TS 專案,我才了解意思。</p>
<p>用以下的 code 來示範:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">arrayNumberMap</span><span class="token punctuation">(</span><span class="token parameter">arr</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> arr<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span>Number<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">function</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// some awsome code</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>someCondition<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token function">arrayNumberMap</span><span class="token punctuation">(</span><span class="token string">'123'</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token comment">// some awsome code</span><br /><span class="token punctuation">}</span></code></pre>
<p>對 JS 有基礎的人大概都會知道上面這段 code 有問題,當呼叫 <code>arrayNumberMap</code> 但是傳入一個 string,這會噴出錯誤:</p>
<pre><code>Uncaught TypeError: arr.map is not a function
</code></pre>
<p>因為 string 並沒有這個 method,所以無法呼叫而出現錯誤。在 JS 這個錯誤要一直等到這段程式碼被執行才會噴出,也就是要呼叫到這個 function 並且傳入錯誤的參數,才會發現這個錯誤,所以如果沒有滿足條件而沒有呼叫到 function,有可能要等到上線時才發現這個錯誤。</p>
<p>但是類似的 code 在 TS 編譯的過程中可能就會被察覺。</p>
<pre class="language-typescript"><code class="language-typescript"><span class="token keyword">function</span> <span class="token function">arrayNumberMap</span><span class="token punctuation">(</span>arr<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> arr<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span>Number<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><span class="token keyword">function</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token keyword">void</span> <span class="token punctuation">{</span><br /> <span class="token comment">// some awsome code:</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>someCondition<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token function">arrayNumberMap</span><span class="token punctuation">(</span><span class="token string">'123'</span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token comment">// some awsome code</span><br /><span class="token punctuation">}</span></code></pre>
<p>因為在 TS 中需要指定傳入參數的 type,所以類似的 code 在 TS 可以這樣改寫。而當我們使用 <code>tsc</code> 編譯的時候,TS 的編譯器會去偵測 code 然後發現有個地方使用 <code>arrayNumberMap</code> 的方法不正確而拋出錯誤,</p>
<pre><code>Argument of type 'string' is not assignable to parameter of type 'string[]'.
</code></pre>
<p>這個時候整個程式碼其實尚未被執行,也就是說這個錯誤會在比較早的階段就被發現,不會到真正執行然後被觸發的時候才發現。</p>
<p>在這篇漫談會想要說這點,是因為前陣子看到的一個有趣<a href="https://twitter.com/LauviahF/status/1505060464312545280">討論</a>進而去仔細看 TS 文件以及自己實驗才有更多理解。從<a href="https://millenniummeetonce.blogspot.com/2018/04/blog-post_5.html">動態語言與靜態?直譯與編譯?強型別與弱型別?</a>與<a href="https://totoroliu.medium.com/%E7%B7%A8%E8%AD%AF%E8%AA%9E%E8%A8%80-vs-%E7%9B%B4%E8%AD%AF%E8%AA%9E%E8%A8%80-5f34e6bae051">編譯語言 VS 直譯語言</a>可以知道所謂編譯與直譯的差別,編譯是我們所寫的程式碼還需要先經過一層轉換,變成更底層的語言才可以執行,例如 C 語言;而直譯就是程式碼在執行時一邊編譯一邊執行,例如 python。編譯語言的型別錯誤會在編譯的時候就被發現,而直譯語言則是因為編譯與執行是連續的,所以要到執行的時候才會發現型別錯誤。</p>
<p>這樣來看 TS 與 JS 的話,JS 是直譯語言,在電腦上只要使用 Node.js 直接執行 <code>.js</code> 檔案就可以,不會要先編譯產生一個編譯好的檔案之後再執行編譯好的檔案;TS 則是要先經過 <code>tsc</code> 編譯,轉換成 <code>.js</code> 檔案之後才能被執行。所以 TS 在設計上會在編譯的時候就發現程式碼有地方發生型別錯誤,因此在轉換成 JS 時候就可以發現錯誤,不會等到轉換成 JS 並且執行的時候才發現。</p>
實作專案套件化
2022-04-03T00:00:00Z
https://blog.errorbaker.tw/posts/xiang/package-vue-project/
<!-- summary -->
<p>看到這個標題,不曉得讀者心中是否會產生一些疑問?專案套件化是什麼意思?為什麼會有這個想法?做這個會有什麼用處?<br />
本篇文章將會分享為何我會想製作這個主題,以及我在實作時是如何去構思及修正的。</p>
<!-- summary -->
<!-- more -->
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/package-vue-project/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<p>作為工程師,我們很習慣會去把有重複使用到的東西獨立出來管理。不論是函式、元件、樣式,甚至是頁面,當我們把重複用到的東西獨立出來以後,就能夠避免在維護的時候,明明是相同的東西,卻要去改好幾個地方。</p>
<p>獨立出來以後,又能夠依據使用情境,來細分管理方式,比如說一個 sidebar 元件,只有在這個專案會用到,那我其實在這個專案底下開一個資料夾來存放就可以了。但如果是類似 button 的元件,我希望未來新的專案都能夠使用,那我們就可以把這個元件獨立成一個套件。只要在各個專案中引入就可以直接使用。</p>
<p>類似的機制其實在座各位都並不陌生,像 <a href="https://fontawesome.com/">Font Awesome</a>、<a href="https://chakra-ui.com/">Chakra</a> 就是在提供類似的服務,幫我們把很常重複使用的元件給封裝起來,我們只需要在專案中引入即可直接使用。</p>
<p>除了引用別人做好的套件以外,我們還可以製作自己的套件,好處就是我們可以完全自由的決定我們的元件要有什麼功能,要長什麼樣子。還有最重要的是,別人的東西我們不敢保證哪一天會不會停止維護,我們自己做的東西我們能夠自己來維護。</p>
<p>將元件變成套件,其實網路上已經有非常多很棒的資源了,本篇文章會分享的,是如何能將整份專案變成套件,並且可以自由的決定,要開放哪些功能讓使用者能夠客製化,來達成整份專案的重複使用。</p>
<blockquote>
<p>本篇文章的範例會以 Vue2 為主,但任何前端框架都能透過相同的概念,實作專案套件化。</p>
</blockquote>
<h2 id="%E7%9B%AE%E6%A8%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/package-vue-project/#%E7%9B%AE%E6%A8%99">#</a> 目標</h2>
<p>在這邊先提一下整份專案套件化的目的是什麼,首要目標當然就是讓我們可以在各個不同的專案當中引入使用。但是既然各個專案都能夠使用,代表這個套件底下的功能並不齊全,它必須要涵蓋大部分專案都會使用到的架構,但不必去擁有專案底下需要具備的所有細節。</p>
<p>也就是說這份套件化的專案,它其實是專案的 <code>核心</code>,而這個 <code>核心</code> 會在真正使用於專案時,再把其他附帶的功能給組裝上去。<br />
舉例來說,假設我每一個專案,它都會有登入頁面,那我就可以把登入功能納入核心的功能當中,如果我今天要建立一個購物網站,我可以去引用我的核心,並將購物車功能組裝上去,這樣我就只需要做出購物車系統就能夠產生新的購物網站,省去了製作登入功能的時間。</p>
<p>或者我將管理系統變成一個 <code>核心</code>,未來我想實作員工管理系統,或者商品管理系統、活動管理系統、XX管理系統,我都能直接使用這個 <code>核心</code>,再加上一些客製化的頁面上去,快速產生出新的專案。因為管理系統的架構是不變的,差別只在最後使用的資料內容是什麼,所以只要能夠將 <code>核心</code> 給完善,在使用時基本上只要套入設定就能夠完成了,大幅減少新專案的開發時間。</p>
<blockquote>
<p>大家可以把這個所謂的 <code>核心</code>,想像成功能更完善的 <code>create-react-app</code> 或者 <code>vue-cli</code>。</p>
</blockquote>
<p>那這個所謂的 <code>核心</code>,需要具備什麼樣的功能及條件呢?</p>
<ol>
<li>隨裝隨用</li>
<li>方便更版</li>
<li>支援客製化</li>
</ol>
<p><code>隨裝隨用</code>、<code>方便更版</code>,這兩點大家一定都能理解,而 <code>支援客製化</code> 其實是最難的,也是最重要的部分。因為每個專案會有每個專案自己的細節,我們需要開放使用者自行客製化(不論是功能或頁面),如果這個核心未來都只有自己會使用,當然直接開放所有功能的客製化即可。但是如果我們會把核心開放給外界使用,就得去定義哪些地方要開放客製化哪些地方不要。</p>
<p>但為了讓文章的主題不要太過發散,我把開放客製化的目標定為下面幾項就好:</p>
<ul>
<li>router - 使用者可以自行加入新的 route</li>
<li>component - 使用者可以自行加入新的 component</li>
</ul>
<h2 id="%E8%A6%8F%E5%8A%83"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/package-vue-project/#%E8%A6%8F%E5%8A%83">#</a> 規劃</h2>
<p>確認目標以後,就要來規劃實作的步驟了:</p>
<ol>
<li>嘗試把一個簡單的元件部署上 npm</li>
<li>思考客製化的邏輯及方法</li>
<li>嘗試將專案套件化</li>
<li>延伸思考</li>
</ol>
<p>先從簡單的任務開始,理解基本的部署流程(先部署元件就好),再從使用情境去探討該如何規劃套件架構,前面的規劃都確定好最後才是實作出我們的套件。</p>
<!-- - 先談談單純將 component 套件化,變成可複用的元件
- 思考專案套件化後的使用邏輯(舊的 slot 概念 -> 新的 register 概念)
- 接著變成將專案套件化
- 需要解決的問題
- 套件部署的位置(公開?非公開?)
- babel 轉譯的時機 -->
<h2 id="%E5%AF%A6%E4%BD%9C"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/package-vue-project/#%E5%AF%A6%E4%BD%9C">#</a> 實作</h2>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/package-vue-project/">☞ 將元件套件化</a></strong><br />
由於專案套件化比較複雜,我們先嘗試將 <code>元件</code> 做成套件就好,等初步了解部署 npm 的流程以後,再來思考如何將專案製做成套件。<br />
假設我現在有一個 hello world 元件,我想要把它變成套件打包到 npm。</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// src/components/HelloWorld.vue</span><br /><br /><span class="token operator"><</span>template<span class="token operator">></span><br /> <span class="token operator"><</span>div<span class="token operator">></span>Hello World<span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><br /><span class="token operator"><</span><span class="token operator">/</span>template<span class="token operator">></span><br /><span class="token operator"><</span>script<span class="token operator">></span><br /><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">"hello world"</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><span class="token operator"><</span><span class="token operator">/</span>script<span class="token operator">></span></code></pre>
<p>我要做的就是使用 vue 官方提供的方法,定義 <code>install</code> 函式。先建立一個 js 檔,把我的 hello world 元件引入:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// src/components/install.js</span><br /><br /><span class="token keyword">import</span> HelloWorld <span class="token keyword">from</span> <span class="token string">"./HelloWorld.vue"</span><span class="token punctuation">;</span><br /><br /><span class="token comment">// 定義 install 函式</span><br />HelloWorld<span class="token punctuation">.</span><span class="token function-variable function">install</span> <span class="token operator">=</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">Vue</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> Vue<span class="token punctuation">.</span><span class="token function">component</span><span class="token punctuation">(</span><span class="token string">"hello-world"</span><span class="token punctuation">,</span> HelloWorld<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br /><span class="token comment">// export 出去</span><br /><span class="token keyword">export</span> <span class="token keyword">default</span> HelloWorld<span class="token punctuation">;</span></code></pre>
<p>這個 <code>install</code> 的 function 就是讓使用者可以透過 <code>Vue.use()</code> 來呼叫並且引用我們打包好的元件。</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// 使用時</span><br /><span class="token keyword">import</span> HelloWorld <span class="token keyword">from</span> <span class="token string">"<hello-world> 套件"</span><span class="token punctuation">;</span><br /><br />Vue<span class="token punctuation">.</span><span class="token function">use</span><span class="token punctuation">(</span>HelloWorld<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>基本的 install 函式定義好以後,我們就可以來把元件打包了,打包的方式也很容易,使用 Vue 官方提供的 Library 建置指令就好了!</p>
<ul>
<li><code>vue-cli-service build --target lib —-name <打包後的檔名> <install.js 檔的路徑></code></li>
</ul>
<pre class="language-json"><code class="language-json"><span class="token comment">// package.json</span><br /><br />scripts<span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"build"</span><span class="token operator">:</span> <span class="token string">"vue-cli-service build --target lib —-name hello-world ./src/components/install.js"</span><br /><span class="token punctuation">}</span><br /></code></pre>
<p>同時我們必須要在 package.json 定義幾項資訊:</p>
<ul>
<li>name:套件名稱</li>
<li>version:套件版號</li>
<li>main:主要入口點</li>
</ul>
<pre class="language-json"><code class="language-json"><span class="token comment">// package.json</span><br /><br /><span class="token punctuation">{</span><br /> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"hello-world"</span><span class="token punctuation">,</span><br /> <span class="token property">"version"</span><span class="token operator">:</span> <span class="token string">"1.0.0"</span><span class="token punctuation">,</span><br /> <span class="token property">"main"</span><span class="token operator">:</span> <span class="token string">"dist/hello-world.common.js"</span><span class="token punctuation">,</span><br /> <span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token property">"build"</span><span class="token operator">:</span> <span class="token string">"vue-cli-service build --target lib —-name hello-world ./src/components/install.js"</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>上述兩件事情完成以後,就可以來準備發佈了。發佈之前因為需要有 npm 的帳號,所以需要先<a href="https://www.npmjs.com/">註冊</a>一個,驗證 Email 並且在本地端登入:</p>
<pre class="language-js"><code class="language-js">npm adduser <span class="token comment">// 新增使用者</span><br />npm login <span class="token comment">// 登入</span></code></pre>
<p>登入完成後,<code>發射鈕</code> 給它按下去!</p>
<pre class="language-js"><code class="language-js">npm publish</code></pre>
<p>這樣就可以完整的將元件打包上 npm 了!使用時就像使用一般套件一樣引入就行啦。</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>template</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>hello-world</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>hello-world</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>template</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br /> <span class="token keyword">import</span> helloWorld <span class="token keyword">from</span> <span class="token string">"<套件>"</span><span class="token punctuation">;</span><br /> Vue<span class="token punctuation">.</span><span class="token function">use</span><span class="token punctuation">(</span>helloWorld<span class="token punctuation">)</span><span class="token punctuation">;</span><br /></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span></code></pre>
<p>因為只是單純的 Hello world 元件,所以引入時看到的內容就是這一行字:</p>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/package-vue-project-01.png" alt="" /></p>
<blockquote>
<p>上面這段講得很快,主要只是帶大家稍微認識一下部署套件的流程,有興趣了解細節的朋友可以查看 <a href="https://v2.vuejs.org/v2/cookbook/packaging-sfc-for-npm.html">官方文件</a>,或 google 搜尋更多資源。</p>
</blockquote>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/package-vue-project/">☞ 思考專案套件化後的使用情境</a></strong></p>
<p>簡單了解部署套件的流程以後,接著我們來初步構思一下,若是我們想將 <code>專案</code> 套件化,使用的時候應該要如何使用?先定義好使用方式,我們才更容易思考如何建構專案。</p>
<p>首先,因為我們的專案也要能支援客製化,所以我們得要能接收使用者給我們的資料才行。回想過去我們在使用元件的時候,通常會如何讓元件接收外部傳送進來的資料?我們最常使用的方式,就是透過 props,或者是 slot 來傳遞。</p>
<pre class="language-html"><code class="language-html">// 在父層元件引入 child component 使用,並且傳入 props1、props2、slot 等資料<br /><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>template</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>child-component</span> <span class="token attr-name">:props1</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>aaa<span class="token punctuation">"</span></span> <span class="token attr-name">:props2</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>bbb<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> slot content <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>child-component</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>template</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br /> <span class="token keyword">import</span> childComponent <span class="token keyword">from</span> <span class="token string">"./childComponent"</span><span class="token punctuation">;</span><br /> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">components</span><span class="token operator">:</span> <span class="token punctuation">{</span><br /> <span class="token string-property property">"child-component"</span><span class="token operator">:</span> childComponent<span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span></code></pre>
<p>子層的 child component 就能對接收到的 props1、props2、slot 來做事情:</p>
<pre class="language-html"><code class="language-html">// 在子層接收 props 等資料<br /><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>template</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">:class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>props1 === 'aaa' ? 'active' : ''<span class="token punctuation">"</span></span> <span class="token attr-name">:type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>props2<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>template</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br /> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">props</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"props1"</span><span class="token punctuation">,</span> <span class="token string">"props2"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span></code></pre>
<p>那如果我把整個 App 作為一個元件讓外部做使用,例如在 App 上面挖很多個 props (ex. config, schema, model),這樣使用者就可以透過這些 props 來傳入設定。</p>
<pre class="language-html"><code class="language-html">// 引入整個 App 元件<br /><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>template</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>App</span> <span class="token attr-name">:config</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>config<span class="token punctuation">"</span></span> <span class="token attr-name">:schema</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>schema<span class="token punctuation">"</span></span> <span class="token attr-name">:model</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>model<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>App</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>template</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br /> <span class="token keyword">import</span> App <span class="token keyword">from</span> <span class="token string">"<套件>"</span><span class="token punctuation">;</span><br /> Vue<span class="token punctuation">.</span><span class="token function">use</span><span class="token punctuation">(</span>App<span class="token punctuation">)</span><span class="token punctuation">;</span><br /></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span></code></pre>
<p>上面這樣子的使用方式,就可以把專案最外層的 component (App),作為一個很大的元件,提供給外部做引用。使用者就可以直接像操作 component 一樣的方式來做使用。我們可以把這種套件化的方式稱作 <code>Base on component</code>,透過 component 的概念來將專案套件化。</p>
<p><code>Base on component</code> 是最直覺,最好理解的一種套件化方式之一。但是它會存在一些不便,因為我們的資料需要透過 props、slot 做傳遞,所以如果是這兩種方式沒辦法傳遞的資料,就得依賴別種管道來做處理了。</p>
<p>這樣會對專案的實際應用產生限制,例如我們不能夠利用 props 來新增 routes,也就是說使用者將會無法新增新的頁面到專案當中,這絕對是不行的。所以得另外找出可以用來新增 routes 的方式。</p>
<p>為了解決這個問題,我重新回到官方文件去找靈感。看看如何能設計出一種簡單就能新增 routes 的方式。後來發現,當我們在使用框架的時候,其實都會有一個將 router 註冊上 App 的動作。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">new</span> <span class="token class-name">Vue</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token literal-property property">router</span><span class="token operator">:</span> router<br /> <span class="token function-variable function">render</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">h</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">h</span><span class="token punctuation">(</span>App<span class="token punctuation">)</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">$mount</span><span class="token punctuation">(</span><span class="token string">'#app'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /></code></pre>
<p>也就是說,如果我能夠把 routes 先新增好,再把 router 註冊上 App,其實就能達成新增 routes 的需求了。以這個改念下去延伸,我只需要接收到 routes 的設定,再將它加入原本的 router 即可。</p>
<p>流程是這樣子:</p>
<ol>
<li>接收 routes 的設定</li>
<li>將 routes 新增進 router</li>
<li>把 router 註冊到 App</li>
</ol>
<p>例如:我原本的 <code>核心</code> 只有 login 頁面,我在購物網站這個專案底下想要新增商品頁面,只需要傳入 routes 的設定並註冊上 App 就可以了。</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// 核心的 routes 只有 login 頁面</span><br /><span class="token keyword">const</span> routes <span class="token operator">=</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">path</span><span class="token operator">:</span> <span class="token string">"/login"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">components</span><span class="token operator">:</span> loginPage<span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /><span class="token punctuation">]</span><span class="token punctuation">;</span><br /><br /><span class="token comment">// 我設計一個 function 只要呼叫就可以把 route 的設定新增進 routes</span><br /><span class="token keyword">const</span> <span class="token function-variable function">registerRoutes</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">newRoute</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> routes<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>newRoute<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<pre class="language-js"><code class="language-js"><span class="token comment">// 當我呼叫新增 routes 的 function</span><br /><span class="token function">registerRoutes</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">path</span><span class="token operator">:</span> <span class="token string">"/products"</span><span class="token punctuation">,</span> <span class="token literal-property property">components</span><span class="token operator">:</span> ProductsPage <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token comment">// products 就會被新增進 routes 當中</span><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>routes<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token comment">/*<br />[<br /> {<br /> path: '/login',<br /> components: loginPage,<br /> },<br /> {<br /> path: '/products',<br /> components: ProductsPage<br /> }<br />]<br />*/</span></code></pre>
<pre class="language-js"><code class="language-js"><span class="token comment">// 最後再把新增完成的 router 註冊上 App</span><br /><br /><span class="token keyword">import</span> routes <span class="token keyword">from</span> <span class="token string">'./routes'</span><span class="token punctuation">;</span><br /><br />Vue<span class="token punctuation">.</span><span class="token function">use</span><span class="token punctuation">(</span>VueRouter<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> router <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">VueRouter</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token literal-property property">mode</span><span class="token operator">:</span> <span class="token string">'history'</span><span class="token punctuation">,</span><br /> routes<span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">new</span> <span class="token class-name">Vue</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token literal-property property">router</span><span class="token operator">:</span> router<br /> <span class="token function-variable function">render</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">h</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">h</span><span class="token punctuation">(</span>App<span class="token punctuation">)</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">$mount</span><span class="token punctuation">(</span><span class="token string">'#app'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /></code></pre>
<p>如此一來只要使用者想新增 routes,只需要呼叫 <code>registerRoutes</code> 並傳入設定即可。</p>
<p>既然我們可以提供新增 routes 的 function,那我們就可以提供新增 component 的 function。讓使用者自行呼叫 function 就可以新增新的頁面或元件。使用時期望可以單純透過 call function 的動作,就完成所有客製化頁面、元件的註冊:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// 使用時</span><br /><br /><span class="token keyword">import</span> Core <span class="token keyword">from</span> <span class="token string">"<套件>"</span><span class="token punctuation">;</span><br /><br />Core<span class="token punctuation">.</span><span class="token function">registerRoutes</span><span class="token punctuation">(</span>customRoute<span class="token punctuation">)</span><span class="token punctuation">;</span><br />Core<span class="token punctuation">.</span><span class="token function">registerComponents</span><span class="token punctuation">(</span>customComponent<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>我們把上面這種使用方式稱作 <code>Base on register</code>,透過註冊的方式來實作專案套件化。<br />
有了這個註冊的機制以後,我們就可以自由定義要讓哪些東西被註冊,routes、store、i18n...等等都可以開放讓使用者進行註冊。</p>
<blockquote>
<p>有一點值得注意的問題是,我們必須要等到所有該註冊的東西都註冊完了,才可以執行 <code>new Vue</code> 的動作,否則一但 <code>new Vue</code> 執行完了,我再加新的 routes 進去也不會成功註冊到 App 上面。</p>
</blockquote>
<p>代表 <code>new Vue</code> 這個行為是要讓使用者可以自行呼叫的。當使用者註冊完所有東西以後,自行呼叫 <code>new Vue</code>。<br />
所以我們可以把使用情境改成下面這樣:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// main.js 套件使用情境</span><br /><br /><span class="token comment">// 使用時先註冊完所有東西,再執行 new Vue</span><br /><span class="token keyword">import</span> Core <span class="token keyword">from</span> <span class="token string">"<套件>"</span><span class="token punctuation">;</span><br /><br />Core<span class="token punctuation">.</span><span class="token function">registerRoutes</span><span class="token punctuation">(</span>customRoute<span class="token punctuation">)</span><span class="token punctuation">;</span><br />Core<span class="token punctuation">.</span><span class="token function">registerComponents</span><span class="token punctuation">(</span>customComponent<span class="token punctuation">)</span><span class="token punctuation">;</span><br />Core<span class="token punctuation">.</span><span class="token function">registerRun</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 這個 function 用來執行 new Vue 這個動作</span></code></pre>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/package-vue-project/">☞ 將專案套件化</a></strong></p>
<p>現在使用的情境已經有了,我們只需要針對這個使用情境,來製作這個 <code>Core</code> 的功能就行了。<br />
我們把我們的注意力,拉回到套件的製作上面。</p>
<p>因為我們是將整份專案製作成套件,所以我們會需要用到這個專案原本的設定:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> Vue <span class="token keyword">from</span> <span class="token string">"vue"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> App <span class="token keyword">from</span> <span class="token string">"./App.vue"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> router <span class="token keyword">from</span> <span class="token string">"./router"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> store <span class="token keyword">from</span> <span class="token string">"./store"</span><span class="token punctuation">;</span></code></pre>
<p>在沒有要註冊任何東西的情況下,其實只需要把 <code>核心</code> 專案底下的所有東西註冊上 App 並回傳出去即可</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// core.js 套件開發</span><br /><br /><span class="token keyword">import</span> Vue <span class="token keyword">from</span> <span class="token string">"vue"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> App <span class="token keyword">from</span> <span class="token string">"./App.vue"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> router <span class="token keyword">from</span> <span class="token string">"./router"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> store <span class="token keyword">from</span> <span class="token string">"./store"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> <span class="token function-variable function">registerRun</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Vue</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token literal-property property">router</span><span class="token operator">:</span> router<span class="token punctuation">,</span><br /> store<span class="token punctuation">,</span><br /> <span class="token function-variable function">render</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">h</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">h</span><span class="token punctuation">(</span>App<span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">$mount</span><span class="token punctuation">(</span><span class="token string">"#app"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> registerRun<span class="token punctuation">;</span></code></pre>
<p>其實上面的步驟就只是平常我們在使用框架時,多把 <code>new Vue</code> 包成一個 function,並且主動去呼叫它而已。<br />
當我們把這支 core.js 作為套件的入口點,就可以在引入套件時直接使用到 core.js 匯出的 registerRun 這個 function。</p>
<pre class="language-json"><code class="language-json"><span class="token comment">// 套件的 package.json</span><br /><span class="token punctuation">{</span><br /> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"core"</span><span class="token punctuation">,</span><br /> <span class="token property">"version"</span><span class="token operator">:</span> <span class="token string">"1.0.0"</span><span class="token punctuation">,</span><br /> <span class="token property">"main"</span><span class="token operator">:</span> <span class="token string">"src/core.js"</span><span class="token punctuation">,</span> <span class="token comment">// 把 core.js 作為整個套件的入口檔</span><br /> .<br /> .<br /> .<br /><span class="token punctuation">}</span><br /><br /></code></pre>
<p>專案在使用套件時,就可以直接用 core.js 提供的 function:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// main.js 套件使用情境</span><br /><span class="token keyword">import</span> Core <span class="token keyword">from</span> <span class="token string">"<套件>"</span><span class="token punctuation">;</span><br />Core<span class="token punctuation">.</span><span class="token function">registerRun</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>接下來做的事情,就是定義 registerRoutes、registerComponents 兩個 function,並把它們跟 registerRun 一起包進一個 Object 並且 export 出去:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// core.js 套件開發</span><br /><span class="token keyword">import</span> Vue <span class="token keyword">from</span> <span class="token string">"vue"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> App <span class="token keyword">from</span> <span class="token string">"./App.vue"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> routes <span class="token keyword">from</span> <span class="token string">"./router/routes"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> router <span class="token keyword">from</span> <span class="token string">"./router"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> store <span class="token keyword">from</span> <span class="token string">"./store"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token punctuation">{</span><br /> <span class="token function-variable function">registerRoutes</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">route</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token operator">...</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token function-variable function">registerComponents</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">component</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token operator">...</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token function-variable function">registerRun</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Vue</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token literal-property property">router</span><span class="token operator">:</span> router<span class="token punctuation">,</span><br /> store<span class="token punctuation">,</span><br /> <span class="token function-variable function">render</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">h</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">h</span><span class="token punctuation">(</span>App<span class="token punctuation">)</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">$mount</span><span class="token punctuation">(</span><span class="token string">"#app"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /></code></pre>
<p>專案在使用套件時,就可以直接用 core.js 匯出的整個 Object:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// main.js 套件使用情境</span><br /><span class="token keyword">import</span> Core <span class="token keyword">from</span> <span class="token string">"<套件>"</span><span class="token punctuation">;</span><br /><br />Core<span class="token punctuation">.</span><span class="token function">registerRoutes</span><span class="token punctuation">(</span>customRoute<span class="token punctuation">)</span><span class="token punctuation">;</span><br />Core<span class="token punctuation">.</span><span class="token function">registerComponents</span><span class="token punctuation">(</span>customComponent<span class="token punctuation">)</span><span class="token punctuation">;</span><br />Core<span class="token punctuation">.</span><span class="token function">registerRun</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>由於 main.js 會作為專案的 js 入口點,所以執行 <code>Core.registerRun()</code> 就等於透過 <code>new Vue</code> 將整個 Vue 專案綁到 <code><div id="app"></div></code> 這個 tag 上面。這個操作就能夠將核心的專案透過套件引入進來,並且註冊好客製化的頁面及元件,最後綁到 HTML 上面,完成整個專案套件的引入。</p>
<p>日後我有新的功能想要加進 <code>核心</code> 套件時,只需要把功能完成後建立新的版號,就可以在新的專案當中引入新版本的 <code>核心</code> 套件進行開發。而這個 <code>核心</code> 套件,就能在每一次建立新專案的時候一直被重複使用。</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/package-vue-project/">☞ 延伸思考</a></strong></p>
<p>專案套件化的基本概念,就如同上面提供的方法。不過還是有很多值得思考的問題:</p>
<ul>
<li>如何避免使用者註冊的客製化 component 跟核心的 component 發生衝突?</li>
<li>如何避免註冊 routes 之後可能造成核心功能壞掉?</li>
<li>套件化以後的專案,在引用時如何解決 babel 不會轉譯 node_module 檔案的問題?</li>
<li>上面介紹的 <code>registerRoutes</code>、<code>registerComponents</code> 都只有提到概念而已,細節該如何去規劃?</li>
<li>如何開放其他功能讓使用者註冊?</li>
</ul>
<h2 id="%E7%B8%BD%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/package-vue-project/#%E7%B8%BD%E7%B5%90">#</a> 總結</h2>
<p>比起網頁開發,開發套件其實更像是軟體開發。當有不同的使用情境出現時,就會有不同的需求需要滿足。雖然不是每個開發者都會遇到需要開發大量專案的情境,也不見得大家會有需要將專案製作成套件的需求,不過每項工具的實作,相信都有它值得參考的價值。因為一但有人拋了新的概念出來時,或許相同的主題底下,每個人都有機會想出不同的解決方法。</p>
<p>對了~延伸思考的問題並沒有標準答案,需要視使用情境而定。所以如果看完這篇文章的你也有實作專案套件化的需求,都歡迎跟我交流心得唷。</p>
儲存資料到 firestore 前,需要注意哪些事?
2022-04-16T00:00:00Z
https://blog.errorbaker.tw/posts/ruofan/firestore/
<!-- summary -->
<p>Hi 大家好。 前陣子在開發微服務的時候,對於儲存的資料結構有一些小小的心得。這篇文章主要會帶大家看 firestore 使用上的限制,在開發上有哪些解決方法給大家參考。</p>
<!-- summary -->
<!-- more -->
<p>首先帶大家來看在 <a href="https://firebase.google.com/docs/firestore/quotas">官方文件上</a> 關於寫入資料的使用限制。</p>
<blockquote>
<p>Maximum number of writes that can be passed to a Commit operation or performed in a transaction<br />
☞ 500</p>
</blockquote>
<p>這個限制是指在寫入的時候 每一次的 transaction 最多只能有 500 筆。</p>
<h4 id="%E4%BB%80%E9%BA%BC%E6%A8%A3%E7%9A%84%E6%83%85%E5%A2%83%E6%9C%83%E7%94%A2%E7%94%9F%E4%B8%80%E6%AC%A1%E5%AF%AB%E5%85%A5%E7%9A%84-document-%E6%9C%83%E8%B6%85%E9%81%8E-500-%E7%AD%86%E5%91%A2%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/firestore/#%E4%BB%80%E9%BA%BC%E6%A8%A3%E7%9A%84%E6%83%85%E5%A2%83%E6%9C%83%E7%94%A2%E7%94%9F%E4%B8%80%E6%AC%A1%E5%AF%AB%E5%85%A5%E7%9A%84-document-%E6%9C%83%E8%B6%85%E9%81%8E-500-%E7%AD%86%E5%91%A2%EF%BC%9F">#</a> 什麼樣的情境會產生一次寫入的 document 會超過 500 筆呢?</h4>
<p>範例情境: 在設計上把每一天每五分鐘的資料存成一個 document。</p>
<p>這樣的資料結構設計存在一個很大的問題,那就是如果讀取一個月的資料量的讀取成本會爆高 ... 。</p>
<blockquote>
<p>假設先不論成本的情境下,要如何解決一次寫入超過 500 筆的問題呢?</p>
</blockquote>
<p>可以使用批次寫入,設定一個批次寫入的數量完成後先 commit ,再接續寫入後續的資料。</p>
<p>下方是範例程式碼:</p>
<pre class="language-javascript"><code class="language-javascript"> <span class="token keyword">static</span> <span class="token keyword">async</span> <span class="token function">waitForPromiseChunkToBeResolved</span><span class="token punctuation">(</span><span class="token parameter">promiseChunk</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> Promise<span class="token punctuation">.</span><span class="token function">all</span><span class="token punctuation">(</span>promiseChunk<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><br /> <span class="token punctuation">(</span><span class="token parameter">resolvedChunkResults</span><span class="token punctuation">)</span> <span class="token operator">=></span> resolvedChunkResults<br /> <span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">static</span> <span class="token keyword">async</span> <span class="token function">promiseAllInBatches</span><span class="token punctuation">(</span><span class="token parameter">promises</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> promisesBatches <span class="token operator">=</span> <span class="token function">chunk</span><span class="token punctuation">(</span>promises<span class="token punctuation">,</span> <span class="token constant">CHUNK_INTERVAL</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">let</span> result<span class="token punctuation">;</span><br /> <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> promises <span class="token keyword">of</span> promisesBatches<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> result <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">waitForPromiseChunkToBeResolved</span><span class="token punctuation">(</span>promises<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> result<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span></code></pre>
<p>讀取成本爆高當然是不 ok 的,假設直接不使用 sub collection 把全部資料都放在同一個 document ! 這樣讀取成本總該大幅降低了吧 ... ?</p>
<p>但這樣的設計很可能會面臨另一個問題,接著帶大家看一下 <a href="https://firebase.google.com/docs/firestore/quotas">官方文件上</a> 上關於 document 儲存容量上的使用限制。</p>
<blockquote>
<p>Maximum size for a document<br />
☞ 1 MiB (1,048,576 bytes)</p>
</blockquote>
<p>這個限制是指,如果想要讓資料在擴充時是有彈性的,就不應該把所有資料都只放在一個 document !<br />
在儲存資料結構的設計上,可以適當的搭配 sub collection 以及 nest document 權衡成本與資料擴充的彈性。</p>
<p>以下是範例資料結構:</p>
<pre class="language-javascript"><code class="language-javascript">collection<br /> document<br /> sub collection<br /> document<br /> <span class="token function">data</span> <span class="token punctuation">(</span>data type <span class="token operator">-</span> map<span class="token punctuation">)</span></code></pre>
<h2 id="%E5%B0%8F%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/firestore/#%E5%B0%8F%E7%B5%90">#</a> 小結</h2>
<p>這次的開發體驗著實地體會到了在設計資料結構之前了解搭配使用的資料庫系統上限制的重要。<br />
在閱讀文章時如果有遇到什麼問題,或是有什麼建議,都歡迎留言告訴我,謝謝。😃</p>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/firestore/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li><a href="https://firebase.google.com/docs/firestore/quotas">Document | Usage and limits for Firestore</a></li>
<li><a href="https://dev.to/moga/decrease-read-costs-of-firestore-using-firestore-data-bundles-30e9">Blog | Decrease read costs of Firestore using Firestore Data Bundles</a></li>
</ul>
multipart/form-data 初探
2022-04-30T00:00:00Z
https://blog.errorbaker.tw/posts/cwc329/multipart-form-data/
<!-- summary -->
<p>筆者忝為後端工程師,一直到最近因為要直接處理上傳檔案,才碰到要接收 application/json 以外的 request content-type。<br />
記錄一下為了開發功能去翻閱 RFC 有關於 multipart/form-data 的一些心得。</p>
<!-- summary -->
<h1 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/multipart-form-data/#%E5%89%8D%E8%A8%80">#</a> 前言</h1>
<p>雖然這樣說有點不可思議,但是寫後端一年多了,我其實沒有碰過 json 以外的資料傳輸格式,即便公司產品已經有上傳 xlsx, csv 檔案並且將檔案內容如存到 DB,但是底層處理已經是前人包好,我只需要處理最後產出的 json 格式資料而已。所以我完全沒有碰過如何處理 http request body 是 json 以外的 body。</p>
<p>最近開發新功能,除了要將資料儲存到 DB,也要把上傳的檔案儲存到 AWS S3 備份。<br />
這是過往沒有過的先例,於是我只好自己生出規格,並且依照規格實作接收檔案並且上傳到 S3 的功能。依照我們公司現在參照的 OpenAPI,如果要在一個 request 同時傳輸檔案與資料,要使用 <code>Content-type: multipart/data-form</code>。<br />
基本上,現代開發使用的 npm 套件都已經處理好很多東西了,但是還是需要知道一個 request 到底長怎樣,以及需要帶什麼樣的 header 與參數,這篇文章希望能用簡單的方式分享一下我在這次研究的一些心得。</p>
<h1 id="openapi-spec"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/multipart-form-data/#openapi-spec">#</a> OpenAPI Spec</h1>
<p>一開始撰寫 API doc,為了因應需要同時傳遞資料以及檔案,根據 <a href="https://swagger.io/docs/specification/describing-request-body/multipart-requests/">OpenAPI spec</a> 需要使用 multipart request。我先依照網站上面的範例把 API doc 生出來給前端之後,在參照著網頁的敘述開始研究 multipart/form-data。</p>
<h2 id="openapi-multipart-request"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/multipart-form-data/#openapi-multipart-request">#</a> OpenAPI Multipart Request</h2>
<p>在網頁中很清楚的寫道:</p>
<blockquote>
<p>You typically use these requests for file uploads and for transferring data of several types in a single request (for example, a file along with a JSON object)</p>
</blockquote>
<p>這完全符合我的使用情境,需要同時傳輸 json 格式的資料以及一個檔案。OpenAPI 的 yaml 範例這邊先略過不談,主要是要看網頁中的 http request example。</p>
<pre class="language-http"><code class="language-http"><span class="token request-line"><span class="token method property">POST</span> <span class="token request-target url">/upload</span> <span class="token http-version property">HTTP/1.1</span></span><br /><span class="token header"><span class="token header-name keyword">Content-Length</span><span class="token punctuation">:</span> <span class="token header-value">428</span></span><br /><span class="token header"><span class="token header-name keyword">Content-Type</span><span class="token punctuation">:</span> <span class="token header-value">multipart/form-data; boundary=abcde12345</span></span><br />--abcde12345<br /><span class="token header"><span class="token header-name keyword">Content-Disposition</span><span class="token punctuation">:</span> <span class="token header-value">form-data; name="id"</span></span><br /><span class="token header"><span class="token header-name keyword">Content-Type</span><span class="token punctuation">:</span> <span class="token header-value">text/plain</span></span><br />123e4567-e89b-12d3-a456-426655440000<br />--abcde12345<br /><span class="token header"><span class="token header-name keyword">Content-Disposition</span><span class="token punctuation">:</span> <span class="token header-value">form-data; name="address"</span></span><br /><span class="token header"><span class="token header-name keyword">Content-Type</span><span class="token punctuation">:</span> <span class="token header-value">application/json</span></span><br /><span class="token application-json"><span class="token punctuation">{</span><br /> <span class="token property">"street"</span><span class="token operator">:</span> <span class="token string">"3, Garden St"</span><span class="token punctuation">,</span><br /> <span class="token property">"city"</span><span class="token operator">:</span> <span class="token string">"Hillsbery, UT"</span><br /><span class="token punctuation">}</span><br />--abcde12345<br />Content-Disposition<span class="token operator">:</span> form-data; name=<span class="token string">"profileImage "</span>; filename=<span class="token string">"image1.png"</span><br />Content-Type<span class="token operator">:</span> application/octet-stream<br /><span class="token punctuation">{</span>…file content…<span class="token punctuation">}</span><br />--abcde12345--</span></code></pre>
<p>這是一份 http request 的內容,從第一行開始分別是 http request method 以及 發送到哪裡,<br />
第二、三行分別是這個 request 的 headers,最後則是 request body。</p>
<p>這樣就可以看出一個 multipart/form-data request 到底是怎麼組成的,不過詳細到底是怎樣,需要什麼 header 以及 request body 要怎麼寫,這就要去看 RCF 了。</p>
<h1 id="rcf-7578"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/multipart-form-data/#rcf-7578">#</a> RCF 7578</h1>
<p>關於 multipart/form-data 的規格,記錄在 <a href="https://datatracker.ietf.org/doc/html/rfc7578">RCF 7578</a>。這份文件詳細的紀錄 multipart/form-data 的用途、規格以及如果開發者想要傳送或者處理 multipart/form-data request 時需要注意的事情。筆者在這邊文章只會關心網頁中的 section 4,也就是其定義。</p>
<h2 id="boundary"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/multipart-form-data/#boundary">#</a> Boundary</h2>
<p>從上方的 http request 範例可以看到,Content-Type 除了表示這個 request 所傳送的 body 是 multipart/form-data 之外,還有一個參數 boundary,並且標明這個參數的值。而在 request body 裡面可以看到參數的值會跟著 <code>--</code> 一起出現,並且分隔 form 的不同部分。</p>
<p>依據 <a href="https://datatracker.ietf.org/doc/html/rfc7578#section-4.1">section 4.1</a>,boundary 是 multipart 這個 media type 所需要的參數,並且會以單獨一行、<code>--</code> 為開頭再加上 boundary 組成。根據 <a href="https://datatracker.ietf.org/doc/html/rfc2046#section-5.1">RFC 2046 section 5.1</a> boundary line 會出現在 request body 的開頭、結尾以及所有 part 的之間。</p>
<h2 id="headers-of-each-part"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/multipart-form-data/#headers-of-each-part">#</a> Headers of Each Part</h2>
<p>依照 <a href="https://datatracker.ietf.org/doc/html/rfc7578#section-4.2">section 4.2</a> multipart 的每個 part 都必須要有 Content-Disposition 的 header,標明這個 part 的 disposition type,很明顯的 disposition type 就是 form-data。除此之外,也必須要一個 name 的參數,這個參數代表這個 part 在這份 request 中的名稱。<br />
而如果這個 part 所夾帶的資料是個檔案,那還要再多提供一個參數 filename,代表這個檔案的名字。</p>
<p><a href="https://datatracker.ietf.org/doc/html/rfc7578#section-4.2">section 4.4</a> 與 <a href="https://datatracker.ietf.org/doc/html/rfc7578#section-4.2">section 4.8</a> 除了必須的 header 之外,每個 part 都可以再帶 Content-Type 表明這個 part 所帶的資料是什麼格式。如果沒有指定,預設就會是 text/plain。除了 Content-Disposition 以及 Content-Type 還有 Content-Transfer-Encoding,其他 header 則不應該出現在 request body 中,而且就算出現也應該被忽略。</p>
<h2 id="multi-file-for-one-field"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/multipart-form-data/#multi-file-for-one-field">#</a> Multi File for One Field</h2>
<p>同一個 filed 可能會需要上傳多個檔案,例如一次要上傳很多張照片。根據 [section 4.4] 如果有這種需求,則每個檔案都要在不同的 part,也就是每個檔案都要用,但是可以使用相同的 field name 來表示這些檔案應該要被視作同一個 field。</p>
<h2 id="%E5%B0%8F%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/multipart-form-data/#%E5%B0%8F%E7%B5%90">#</a> 小結</h2>
<p>以上是我覺得 RFC 7578 section4 比較重要的規範,知道了這些就可以初步知道一個 multipart/form-data 是什麼樣子。</p>
<p>首先,在 http request headers 先用 Content-Type 表示這個 request 是 multipart/form-data,並且定義出 request body 要使用的 boundary。<br />
接著則是要根據 spec 做出符合規範的並且合理的 request body。body 的開頭、結尾以及每個 part 之間都要有一行用 <code>--</code> 與 boundary 組成的分隔行。每個 part 都要先定義這個 part 的 Content-Disposition,以及如果需要的話再定義這個 part 的 Content-Type 以及 Content-Transfer-Encoding,接著再把這個 part 要傳送的資料放入。</p>
<h1 id="%E7%B5%90%E8%AB%96"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/multipart-form-data/#%E7%B5%90%E8%AB%96">#</a> 結論</h1>
<p>這次的介紹範圍較少,只侷限在 RCF 7578,不過在這篇 RCF 中其實有提到其他的 RCF,包含 RCF 的用詞定義以及 multipart 這個 media type 的 RCF 都在 RCF 7578 裡面被提到,我也有點開瀏覽。如果讀者有興趣的話,可以去看相關章節,可以對 http 有更深入的了解。這是我第一次認真讀 RCF 並且寫簡介文章,如果有不足或者錯誤的地方,請不吝留言指教。</p>
帶你入門區塊鏈(二)
2022-05-01T00:00:00Z
https://blog.errorbaker.tw/posts/xiang/block-chain-02/
<!-- summary -->
<p>寫給想要了解區塊鏈運作機制的你</p>
<!-- summary -->
<!-- more -->
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/block-chain-02/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<p>前一篇 <a href="https://blog.errorbaker.tw/posts/xiang/block-chain-01/">帶你入門區塊鏈(ㄧ)</a>以生活化的角度帶大家理解區塊鏈的基本概念。但是生活化的舉例,畢竟不容易理解實際上運作的方式。所以這篇文章將直接透過程式碼,讓大家理解區塊鏈會如何運作。</p>
<p>使用 Python 寫出一個小範例,內容包含建立創世塊、產生公私鑰、簽署合約、驗證區塊、調整挖礦難度...等等。除了提供程式碼參考外,也會附上各個步驟的說明,方便大家了解每個環節的作用是什麼。</p>
<blockquote>
<p>此範例僅會展示單一節點運作的原理,暫不考慮多個礦工同時進行挖礦的情境</p>
</blockquote>
<h2 id="%E4%BB%8B%E7%B4%B9"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/block-chain-02/#%E4%BB%8B%E7%B4%B9">#</a> 介紹</h2>
<p>區塊鏈底下有三個主要的 Class:</p>
<ul>
<li>交易 (Transaction)</li>
<li>區塊 (Block)</li>
<li>區塊鏈 (BlockChain)</li>
</ul>
<h5 id="%E4%BA%A4%E6%98%93"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/block-chain-02/#%E4%BA%A4%E6%98%93">#</a> 交易</h5>
<pre class="language-py"><code class="language-py"><span class="token keyword">class</span> <span class="token class-name">Transaction</span><span class="token punctuation">:</span><br /> <span class="token keyword">def</span> <span class="token function">__init__</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> sender<span class="token punctuation">,</span> receiver<span class="token punctuation">,</span> amounts<span class="token punctuation">,</span> fee<span class="token punctuation">,</span> message<span class="token punctuation">)</span><span class="token punctuation">:</span><br /> self<span class="token punctuation">.</span>sender <span class="token operator">=</span> sender<br /> self<span class="token punctuation">.</span>receiver <span class="token operator">=</span> receiver<br /> self<span class="token punctuation">.</span>amounts <span class="token operator">=</span> amounts<br /> self<span class="token punctuation">.</span>fee <span class="token operator">=</span> fee<br /> self<span class="token punctuation">.</span>message <span class="token operator">=</span> message</code></pre>
<p>每一筆交易底下會包含幾個項目:</p>
<ul>
<li>付款方 (sender)</li>
<li>收款方 (receiver)</li>
<li>金額 (amounts)</li>
<li>手續費 (fee)</li>
<li>訊息 (message)</li>
</ul>
<p>上面這些資訊都會在交易被建立的時候被帶入</p>
<h5 id="%E5%8D%80%E5%A1%8A"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/block-chain-02/#%E5%8D%80%E5%A1%8A">#</a> 區塊</h5>
<pre class="language-py"><code class="language-py"><span class="token keyword">class</span> <span class="token class-name">Block</span><span class="token punctuation">:</span><br /> <span class="token keyword">def</span> <span class="token function">__init__</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> previous_hash<span class="token punctuation">,</span> difficulty<span class="token punctuation">,</span> miner<span class="token punctuation">,</span> miner_rewards<span class="token punctuation">)</span><span class="token punctuation">:</span><br /> self<span class="token punctuation">.</span>previous_hash <span class="token operator">=</span> previous_hash<br /> self<span class="token punctuation">.</span><span class="token builtin">hash</span> <span class="token operator">=</span> <span class="token string">''</span><br /> self<span class="token punctuation">.</span>difficulty <span class="token operator">=</span> difficulty<br /> self<span class="token punctuation">.</span>nonce <span class="token operator">=</span> <span class="token number">0</span><br /> self<span class="token punctuation">.</span>timestamp <span class="token operator">=</span> <span class="token builtin">int</span><span class="token punctuation">(</span>time<span class="token punctuation">.</span>time<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br /> self<span class="token punctuation">.</span>transactions <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><br /> self<span class="token punctuation">.</span>miner <span class="token operator">=</span> miner<br /> self<span class="token punctuation">.</span>miner_rewards <span class="token operator">=</span> miner_rewards<br /></code></pre>
<p>每一個區塊底下,會包含幾個項目:</p>
<ul>
<li>前一個區塊的 hash</li>
<li>該區塊自己的 hash
<ul>
<li>預設為空字串</li>
<li>會隨挖礦的過程不斷改變</li>
</ul>
</li>
<li>挖礦難度
<ul>
<li>型別是數字</li>
<li>挖礦難度代表 hash 值開頭需要滿足幾個 0</li>
<li>ex.如果設定難度是 5,代表 hash 值開頭需要為 5 個 0 開頭才算合格</li>
</ul>
</li>
<li>nonce 值
<ul>
<li>亂數值,型別是數字</li>
<li>挖礦時會不斷改變 nonce 值,來找出匹配難度要求的 hash</li>
</ul>
</li>
<li>建立時間</li>
<li>交易內容
<ul>
<li>型別是陣列,每一個區塊可以寫入多筆交易</li>
<li>需符合區塊的容量限制</li>
</ul>
</li>
<li>挖掘此區塊的礦工</li>
<li>出塊獎勵</li>
</ul>
<h5 id="%E5%8D%80%E5%A1%8A%E9%8F%88"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/block-chain-02/#%E5%8D%80%E5%A1%8A%E9%8F%88">#</a> 區塊鏈</h5>
<pre class="language-py"><code class="language-py"><span class="token keyword">class</span> <span class="token class-name">BlockChain</span><span class="token punctuation">:</span><br /> <span class="token keyword">def</span> <span class="token function">__init__</span><span class="token punctuation">(</span>self<span class="token punctuation">)</span><span class="token punctuation">:</span><br /> self<span class="token punctuation">.</span>adjust_difficulty_blocks <span class="token operator">=</span> <span class="token number">10</span><br /> self<span class="token punctuation">.</span>difficulty <span class="token operator">=</span> <span class="token number">5</span><br /> self<span class="token punctuation">.</span>block_time <span class="token operator">=</span> <span class="token number">15</span><br /> self<span class="token punctuation">.</span>miner_rewards <span class="token operator">=</span> <span class="token number">50</span><br /> self<span class="token punctuation">.</span>block_limitation <span class="token operator">=</span> <span class="token number">10</span><br /> self<span class="token punctuation">.</span>chain <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><br /> self<span class="token punctuation">.</span>pending_transactions <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span> </code></pre>
<p>區塊鏈會包含幾個項目:</p>
<ul>
<li>幾個區塊調整一次難度
<ul>
<li>型別為數字</li>
<li>如果設定為 10,代表每挖出 10 個區塊要調整一次挖礦難度</li>
</ul>
</li>
<li>預設挖礦難度
<ul>
<li>型別為數字</li>
<li>挖礦難度代表 hash 值開頭需要滿足幾個 0</li>
<li>ex.如果設定難度是 5,代表 hash 值開頭需要為 5 個 0 開頭才算合格</li>
</ul>
</li>
<li>期望出塊時間
<ul>
<li>型別為數字</li>
<li>如果設定為 15,代表希望每一個區塊的出塊時間為 15 秒</li>
</ul>
</li>
<li>出塊獎勵
<ul>
<li>型別為數字</li>
<li>當有區塊被挖掘時,給予礦工的出塊獎勵</li>
</ul>
</li>
<li>區塊容量限制
<ul>
<li>型別為數字</li>
<li>每個區塊可以放入的交易數目上限</li>
</ul>
</li>
<li>鏈
<ul>
<li>型別為陣列</li>
<li>已經挖掘出的區塊,會被放入此陣列</li>
</ul>
</li>
<li>交易等待池
<ul>
<li>型別為陣列</li>
<li>剛建立的交易會被放入此陣列</li>
<li>礦工會從此陣列取出交易放入區塊當中</li>
</ul>
</li>
</ul>
<h2 id="%E5%8A%9F%E8%83%BD"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/block-chain-02/#%E5%8A%9F%E8%83%BD">#</a> 功能</h2>
<p>區塊鏈底下需要擁有的功能如下:</p>
<ul>
<li>產生創世塊</li>
<li>產生公私鑰</li>
<li>建立交易</li>
<li>挖礦</li>
<li>其他</li>
</ul>
<p>底下的 function,都是定義在 <code>BlockChain</code> 這個 class 底下,所以 function 當中用到的 <code>self</code> 會指向 <code>BlockChain</code> 這個類別。</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/block-chain-02/">☞ 產生創世塊</a></strong></p>
<pre class="language-py"><code class="language-py"><span class="token keyword">def</span> <span class="token function">create_genesis_block</span><span class="token punctuation">(</span>self<span class="token punctuation">)</span><span class="token punctuation">:</span><br /> new_block <span class="token operator">=</span> Block<span class="token punctuation">(</span><span class="token string">'Hello World! By Xiang'</span><span class="token punctuation">,</span> self<span class="token punctuation">.</span>difficulty<span class="token punctuation">,</span> <span class="token string">'xiang'</span><span class="token punctuation">,</span> self<span class="token punctuation">.</span>miner_rewards<span class="token punctuation">)</span><br /> new_block<span class="token punctuation">.</span><span class="token builtin">hash</span> <span class="token operator">=</span> self<span class="token punctuation">.</span>get_hash<span class="token punctuation">(</span>new_block<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><br /> self<span class="token punctuation">.</span>chain<span class="token punctuation">.</span>append<span class="token punctuation">(</span>new_block<span class="token punctuation">)</span></code></pre>
<p>建立一個新的 <code>Block</code> 區塊,傳入的引數依序為: previous_hash, difficulty, miner, miner_rewards。<br />
因為是創世塊,所以 previous_hash 可以直接給定一個自定義的字串,至於難度以及出塊獎勵,則是使用 <code>BlockChain</code> 初始化時設定的數值。</p>
<p>當 new_block 產生以後,先透過 <code>get_hash</code> 方法(下面會提到),找出這個創世塊的 hash,找到以後就可以把創世塊上到 <code>chain</code> 鏈上。</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/block-chain-02/">☞ 產生公私鑰</a></strong></p>
<p>因為區塊鏈的交易,必須要透過私鑰進行簽署,礦工再利用公鑰進行解密。所以每個使用者都會有一組對應的公私鑰。<br />
因為我們程式碼只是要 Demo 區塊鏈運作,所以先利用 <a href="https://pycryptodome.readthedocs.io/en/latest/src/examples.html#generate-public-key-and-private-key">PyCryptodome</a> 來做產生公私鑰的範例,實際的區塊鏈產生公私鑰的方法跟這邊是不同的。</p>
<pre class="language-py"><code class="language-py"><span class="token keyword">def</span> <span class="token function">generate_address</span><span class="token punctuation">(</span>self<span class="token punctuation">)</span><span class="token punctuation">:</span><br /> public<span class="token punctuation">,</span> private <span class="token operator">=</span> rsa<span class="token punctuation">.</span>newkeys<span class="token punctuation">(</span><span class="token number">512</span><span class="token punctuation">)</span><br /> public_key <span class="token operator">=</span> public<span class="token punctuation">.</span>save_pkcs1<span class="token punctuation">(</span><span class="token punctuation">)</span><br /> private_key <span class="token operator">=</span> private<span class="token punctuation">.</span>save_pkcs1<span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">return</span> self<span class="token punctuation">.</span>get_address_from_public<span class="token punctuation">(</span>public_key<span class="token punctuation">)</span><span class="token punctuation">,</span> \<br /> self<span class="token punctuation">.</span>extract_from_private<span class="token punctuation">(</span>private_key<span class="token punctuation">)</span></code></pre>
<p>上面這個 function,會隨機產生一組公私鑰,並且回傳出去。</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/block-chain-02/">☞ 建立交易</a></strong></p>
<p>建立交易會有幾個流程:</p>
<ul>
<li>初始化交易</li>
<li>利用私鑰簽署交易</li>
<li>礦工驗證交易</li>
</ul>
<pre class="language-py"><code class="language-py"><span class="token comment"># 初始化交易</span><br /><span class="token keyword">def</span> <span class="token function">initialize_transaction</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> sender<span class="token punctuation">,</span> receiver<span class="token punctuation">,</span> amount<span class="token punctuation">,</span> fee<span class="token punctuation">,</span> message<span class="token punctuation">)</span><span class="token punctuation">:</span><br /> <span class="token keyword">if</span> self<span class="token punctuation">.</span>get_balance<span class="token punctuation">(</span>sender<span class="token punctuation">)</span> <span class="token operator"><</span> amount <span class="token operator">+</span> fee<span class="token punctuation">:</span><br /> <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"Balance not enough!"</span><span class="token punctuation">)</span><br /> <span class="token keyword">return</span> <span class="token boolean">False</span><br /> new_transaction <span class="token operator">=</span> Transaction<span class="token punctuation">(</span>sender<span class="token punctuation">,</span> receiver<span class="token punctuation">,</span> amount<span class="token punctuation">,</span> fee<span class="token punctuation">,</span> message<span class="token punctuation">)</span><br /> <span class="token keyword">return</span> new_transaction<br /><br /><span class="token comment"># 利用私鑰簽署交易</span><br /><span class="token keyword">def</span> <span class="token function">sign_transaction</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> transaction<span class="token punctuation">,</span> private<span class="token punctuation">)</span><span class="token punctuation">:</span><br /> private_key_pkcs <span class="token operator">=</span> rsa<span class="token punctuation">.</span>PrivateKey<span class="token punctuation">.</span>load_pkcs1<span class="token punctuation">(</span>private<span class="token punctuation">.</span>encode<span class="token punctuation">(</span><span class="token string">'utf-8'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br /> transaction_str <span class="token operator">=</span> self<span class="token punctuation">.</span>transaction_to_string<span class="token punctuation">(</span>transaction<span class="token punctuation">)</span><br /> signature <span class="token operator">=</span> rsa<span class="token punctuation">.</span>sign<span class="token punctuation">(</span>transaction_str<span class="token punctuation">.</span>encode<span class="token punctuation">(</span><span class="token string">'utf-8'</span><span class="token punctuation">)</span><span class="token punctuation">,</span> private_key_pkcs<span class="token punctuation">,</span> <span class="token string">'SHA-1'</span><span class="token punctuation">)</span><br /> <span class="token keyword">return</span> signature<br /><br /><span class="token comment"># 礦工驗證交易</span><br /> <span class="token keyword">def</span> <span class="token function">add_transaction</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> transaction<span class="token punctuation">,</span> signature<span class="token punctuation">)</span><span class="token punctuation">:</span><br /> <span class="token comment"># 透過地址反推公鑰</span><br /> public_key <span class="token operator">=</span> transaction<span class="token punctuation">.</span>sender<br /> public_key_pkcs <span class="token operator">=</span> rsa<span class="token punctuation">.</span>PublicKey<span class="token punctuation">.</span>load_pkcs1<span class="token punctuation">(</span>public_key<span class="token punctuation">.</span>encode<span class="token punctuation">(</span><span class="token string">'utf-8'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br /><br /> transaction_str <span class="token operator">=</span> self<span class="token punctuation">.</span>transaction_to_string<span class="token punctuation">(</span>transaction<span class="token punctuation">)</span><br /> <span class="token comment"># 確認餘額是否足夠</span><br /> <span class="token keyword">if</span> transaction<span class="token punctuation">.</span>fee <span class="token operator">+</span> transaction<span class="token punctuation">.</span>amounts <span class="token operator">></span> self<span class="token punctuation">.</span>get_balance<span class="token punctuation">(</span>transaction<span class="token punctuation">.</span>sender<span class="token punctuation">)</span><span class="token punctuation">:</span><br /> <span class="token keyword">return</span> <span class="token boolean">False</span><span class="token punctuation">,</span> <span class="token string">"Balance not enough!"</span><br /> <span class="token comment"># 驗證簽證是否為真</span><br /> <span class="token keyword">try</span><span class="token punctuation">:</span><br /> rsa<span class="token punctuation">.</span>verify<span class="token punctuation">(</span>transaction_str<span class="token punctuation">.</span>encode<span class="token punctuation">(</span><span class="token string">'utf-8'</span><span class="token punctuation">)</span><span class="token punctuation">,</span> signature<span class="token punctuation">,</span> public_key_pkcs<span class="token punctuation">)</span><br /> self<span class="token punctuation">.</span>pending_transactions<span class="token punctuation">.</span>append<span class="token punctuation">(</span>transaction<span class="token punctuation">)</span><br /> <span class="token keyword">return</span> <span class="token boolean">True</span><span class="token punctuation">,</span> <span class="token string">"Authorized successfully!"</span><br /> <span class="token keyword">except</span> Exception<span class="token punctuation">:</span><br /> <span class="token keyword">return</span> <span class="token boolean">False</span><span class="token punctuation">,</span> <span class="token string">"RSA Verified wrong!"</span></code></pre>
<p>初始化交易,會先檢查匯款方的餘額是否大於交易金額 + 手續費,並且透過 <code>Transaction</code> 建立一筆新的交易<br />
簽署交易會先把剛剛建立的交易轉換成字串,再把它與私鑰一同進行簽署。<br />
礦工驗證交易,會先透過匯款方的地址取得匯款方的公鑰,並且確認匯款方有足夠的餘額能進行交易,最後是透過公鑰進行解密,來驗證這筆交易是否是匯款方本人進行簽署的。</p>
<p>當交易被驗證通過以後,就會進入交易等待池中。</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/block-chain-02/">☞ 挖礦</a></strong></p>
<ul>
<li>從等待池中取得交易</li>
<li>將交易內容、時間以及 nonce 值,進行雜湊</li>
<li>調整挖礦難度</li>
</ul>
<pre class="language-py"><code class="language-py"><br /><span class="token comment"># 挖礦</span><br /> <span class="token keyword">def</span> <span class="token function">mine_block</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> miner<span class="token punctuation">)</span><span class="token punctuation">:</span><br /> start <span class="token operator">=</span> time<span class="token punctuation">.</span>process_time<span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /> last_block <span class="token operator">=</span> self<span class="token punctuation">.</span>chain<span class="token punctuation">[</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">]</span><br /> new_block <span class="token operator">=</span> Block<span class="token punctuation">(</span>last_block<span class="token punctuation">.</span><span class="token builtin">hash</span><span class="token punctuation">,</span> self<span class="token punctuation">.</span>difficulty<span class="token punctuation">,</span> miner<span class="token punctuation">,</span> self<span class="token punctuation">.</span>miner_rewards<span class="token punctuation">)</span><br /><br /> <span class="token comment"># 把交易打包入目前區塊</span><br /> self<span class="token punctuation">.</span>add_transaction_to_block<span class="token punctuation">(</span>new_block<span class="token punctuation">)</span><br /> new_block<span class="token punctuation">.</span>previous_hash <span class="token operator">=</span> last_block<span class="token punctuation">.</span><span class="token builtin">hash</span><br /> new_block<span class="token punctuation">.</span>difficulty <span class="token operator">=</span> self<span class="token punctuation">.</span>difficulty<br /> new_block<span class="token punctuation">.</span><span class="token builtin">hash</span> <span class="token operator">=</span> self<span class="token punctuation">.</span>get_hash<span class="token punctuation">(</span>new_block<span class="token punctuation">,</span> new_block<span class="token punctuation">.</span>nonce<span class="token punctuation">)</span><br /><br /> <span class="token comment"># 改變 nonce 值直到符合難度</span><br /> <span class="token keyword">while</span> new_block<span class="token punctuation">.</span><span class="token builtin">hash</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">:</span>self<span class="token punctuation">.</span>difficulty<span class="token punctuation">]</span> <span class="token operator">!=</span> <span class="token string">'0'</span> <span class="token operator">*</span> self<span class="token punctuation">.</span>difficulty<span class="token punctuation">:</span><br /> new_block<span class="token punctuation">.</span>nonce <span class="token operator">+=</span> <span class="token number">1</span><br /> new_block<span class="token punctuation">.</span><span class="token builtin">hash</span> <span class="token operator">=</span> self<span class="token punctuation">.</span>get_hash<span class="token punctuation">(</span>new_block<span class="token punctuation">,</span> new_block<span class="token punctuation">.</span>nonce<span class="token punctuation">)</span><br /> <br /> time_consumed <span class="token operator">=</span> <span class="token builtin">round</span><span class="token punctuation">(</span>time<span class="token punctuation">.</span>process_time<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span> start<span class="token punctuation">,</span> <span class="token number">5</span><span class="token punctuation">)</span><br /> <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"Hash found: </span><span class="token interpolation"><span class="token punctuation">{</span>new_block<span class="token punctuation">.</span><span class="token builtin">hash</span><span class="token punctuation">}</span></span><span class="token string"> @ difficulty </span><span class="token interpolation"><span class="token punctuation">{</span>self<span class="token punctuation">.</span>difficulty<span class="token punctuation">}</span></span><span class="token string">, time cost: </span><span class="token interpolation"><span class="token punctuation">{</span>time_consumed<span class="token punctuation">}</span></span><span class="token string">s"</span></span><span class="token punctuation">)</span><br /> self<span class="token punctuation">.</span>chain<span class="token punctuation">.</span>append<span class="token punctuation">(</span>new_block<span class="token punctuation">)</span><br /><br /><span class="token comment"># 將等待池中的交易放入區塊</span><br /> <span class="token keyword">def</span> <span class="token function">add_transaction_to_block</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> block<span class="token punctuation">)</span><span class="token punctuation">:</span><br /> <br /> <span class="token comment"># 按照手續費多寡排序</span><br /> self<span class="token punctuation">.</span>pending_transactions<span class="token punctuation">.</span>sort<span class="token punctuation">(</span>key<span class="token operator">=</span><span class="token keyword">lambda</span> x<span class="token punctuation">:</span> x<span class="token punctuation">.</span>fee<span class="token punctuation">,</span> reverse<span class="token operator">=</span><span class="token boolean">True</span><span class="token punctuation">)</span><br /><br /> <span class="token comment"># 依照交易數量是否大於區塊上限,判斷是只收最高的那些,還是全收</span><br /> <span class="token keyword">if</span> <span class="token builtin">len</span><span class="token punctuation">(</span>self<span class="token punctuation">.</span>pending_transactions<span class="token punctuation">)</span> <span class="token operator">></span> self<span class="token punctuation">.</span>block_limitation<span class="token punctuation">:</span><br /> transaction_accepted <span class="token operator">=</span> self<span class="token punctuation">.</span>pending_transactions<span class="token punctuation">[</span><span class="token punctuation">:</span>self<span class="token punctuation">.</span>block_limitaion<span class="token punctuation">]</span><br /> self<span class="token punctuation">.</span>pending_transactions <span class="token operator">=</span> self<span class="token punctuation">.</span>pending_transactions<span class="token punctuation">[</span>self<span class="token punctuation">.</span>block_limitaion<span class="token punctuation">:</span><span class="token punctuation">]</span><br /> <span class="token keyword">else</span><span class="token punctuation">:</span><br /> transaction_accepted <span class="token operator">=</span> self<span class="token punctuation">.</span>pending_transactions<br /> self<span class="token punctuation">.</span>pending_transactions <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><br /> block<span class="token punctuation">.</span>transactions <span class="token operator">=</span> transaction_accepted<br /><br /><span class="token comment"># 調節挖礦難度</span><br /> <span class="token keyword">def</span> <span class="token function">adjust_difficulty</span><span class="token punctuation">(</span>self<span class="token punctuation">)</span><span class="token punctuation">:</span><br /><br /> <span class="token comment"># 如果還沒到需要驗證的數量,或者現在還在創世塊,無需調整</span><br /> <span class="token keyword">if</span> <span class="token builtin">len</span><span class="token punctuation">(</span>self<span class="token punctuation">.</span>chain<span class="token punctuation">)</span> <span class="token operator">%</span> self<span class="token punctuation">.</span>adjust_difficulty_blocks <span class="token operator">!=</span> <span class="token number">0</span><span class="token punctuation">:</span><br /> <span class="token keyword">return</span> self<span class="token punctuation">.</span>difficulty<br /> <span class="token keyword">elif</span> <span class="token builtin">len</span><span class="token punctuation">(</span>self<span class="token punctuation">.</span>chain<span class="token punctuation">)</span> <span class="token operator"><=</span> self<span class="token punctuation">.</span>adjust_difficulty_blocks<span class="token punctuation">:</span><br /> <span class="token keyword">return</span> self<span class="token punctuation">.</span>difficulty<br /> <span class="token keyword">else</span><span class="token punctuation">:</span><br /> start <span class="token operator">=</span> self<span class="token punctuation">.</span>chain<span class="token punctuation">[</span><span class="token operator">-</span><span class="token number">1</span> <span class="token operator">*</span> self<span class="token punctuation">.</span>adjust_difficulty_blocks <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">.</span>timestamp<br /> finish <span class="token operator">=</span> self<span class="token punctuation">.</span>chain<span class="token punctuation">[</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">.</span>timestamp<br /> <br /> <span class="token comment"># 用最後一個區塊的 timestamp 與待計算的第一個區塊的 timestamp 相減取平均</span><br /> average_time_consumed <span class="token operator">=</span> <span class="token builtin">round</span><span class="token punctuation">(</span><span class="token punctuation">(</span>finish <span class="token operator">-</span> start<span class="token punctuation">)</span> <span class="token operator">/</span> <span class="token punctuation">(</span>self<span class="token punctuation">.</span>adjust_difficulty_blocks<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">)</span><br /> <span class="token comment"># 出塊時間過長減少難度,出塊時間過短增加難度</span><br /> <span class="token keyword">if</span> average_time_consumed <span class="token operator">></span> self<span class="token punctuation">.</span>block_time<span class="token punctuation">:</span><br /> <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"Average block time: </span><span class="token interpolation"><span class="token punctuation">{</span>average_time_consumed<span class="token punctuation">}</span></span><span class="token string">s. Lower the difficulty"</span></span><span class="token punctuation">)</span><br /> self<span class="token punctuation">.</span>difficulty <span class="token operator">-=</span> <span class="token number">1</span><br /> <span class="token keyword">else</span><span class="token punctuation">:</span><br /> <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"Average block time: </span><span class="token interpolation"><span class="token punctuation">{</span>average_time_consumed<span class="token punctuation">}</span></span><span class="token string">s. High up the difficulty"</span></span><span class="token punctuation">)</span><br /> self<span class="token punctuation">.</span>difficulty <span class="token operator">+=</span> <span class="token number">1</span><br /><br /><span class="token comment"># 雜湊</span><br /><span class="token keyword">def</span> <span class="token function">get_hash</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> block<span class="token punctuation">,</span> nonce<span class="token punctuation">)</span><span class="token punctuation">:</span><br /> s <span class="token operator">=</span> hashlib<span class="token punctuation">.</span>sha1<span class="token punctuation">(</span><span class="token punctuation">)</span><br /> s<span class="token punctuation">.</span>update<span class="token punctuation">(</span><br /> <span class="token punctuation">(</span><br /> block<span class="token punctuation">.</span>previous_hash<br /> <span class="token operator">+</span> <span class="token builtin">str</span><span class="token punctuation">(</span>block<span class="token punctuation">.</span>timestamp<span class="token punctuation">)</span><br /> <span class="token operator">+</span> self<span class="token punctuation">.</span>get_transactions_string<span class="token punctuation">(</span>block<span class="token punctuation">)</span><br /> <span class="token operator">+</span> <span class="token builtin">str</span><span class="token punctuation">(</span>nonce<span class="token punctuation">)</span><br /> <span class="token punctuation">)</span><span class="token punctuation">.</span>encode<span class="token punctuation">(</span><span class="token string">"utf-8"</span><span class="token punctuation">)</span><br /> <span class="token punctuation">)</span><br /> h <span class="token operator">=</span> s<span class="token punctuation">.</span>hexdigest<span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">return</span> h</code></pre>
<p>挖礦首先會透過 <code>Block</code> 建立一個新的區塊,將 previous_hash, difficulty, miner, miner_rewards 傳進去。<br />
從等待池中找出手續費最高的交易,依照手續費高低排序,在不超出區塊容量限制的前提下,把等待池中的交易們放進來區塊當中。<br />
把區塊當中的 previous_hash, timestamp, transactions, nonce 進行雜湊,找出符合難度標準的 hash 值。<br />
最後把新的區塊放上區塊鏈。</p>
<p>每一次產生新的區塊以後,會呼叫一次 <code>adjust_difficulty</code>,這個 function 是用來判斷現在是否需要調整挖礦難度的。<br />
它會根據目前區塊鏈的長度判斷是否有需要調整難度,例如我們設定每 10 塊調整一次,就會在長度為 10, 20, 30... 的時候分別進行調整。調整方式是將最後一個區塊的 timestamp 與待計算的第一個區塊的 timestamp 相減取平均,看看是大於還是小於我們期望的出塊時間。如果時間太久了,就把難度 -1,如果時間太快了,就把難度 +1,確保未來的每一次出塊時間盡量保持在我們設定的期望時間附近。</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/block-chain-02/">☞ 其他</a></strong></p>
<ul>
<li>計算帳戶餘額</li>
<li>驗證區塊鏈</li>
</ul>
<pre class="language-py"><code class="language-py"><br /><span class="token comment"># 計算帳戶餘額</span><br /> <span class="token keyword">def</span> <span class="token function">get_balance</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> account<span class="token punctuation">)</span><span class="token punctuation">:</span><br /> balance <span class="token operator">=</span> <span class="token number">0</span><br /> <span class="token keyword">for</span> block <span class="token keyword">in</span> self<span class="token punctuation">.</span>chain<span class="token punctuation">:</span><br /> miner <span class="token operator">=</span> <span class="token boolean">False</span><br /><br /> <span class="token comment"># 如果該帳號是這個區塊的礦工,加上出塊獎勵</span><br /> <span class="token keyword">if</span> block<span class="token punctuation">.</span>miner <span class="token operator">==</span> account<span class="token punctuation">:</span><br /> miner <span class="token operator">=</span> <span class="token boolean">True</span><br /> balance <span class="token operator">+=</span> block<span class="token punctuation">.</span>miner_rewards<br /> <br /> <span class="token comment"># 對照區塊中的每一筆交易,如果這隻帳號是礦工就加上手續費,如果這隻帳號是匯款方就減掉支出,如果這隻帳號是收款方就加上收入</span><br /> <span class="token keyword">for</span> transaction <span class="token keyword">in</span> block<span class="token punctuation">.</span>transactions<span class="token punctuation">:</span><br /> <span class="token keyword">if</span> miner<span class="token punctuation">:</span><br /> balance <span class="token operator">+=</span> transaction<span class="token punctuation">.</span>fee<br /> <span class="token keyword">if</span> transaction<span class="token punctuation">.</span>sender <span class="token operator">==</span> account<span class="token punctuation">:</span><br /> balance <span class="token operator">-=</span> transaction<span class="token punctuation">.</span>amounts<br /> balance <span class="token operator">-=</span> transaction<span class="token punctuation">.</span>fee<br /> <span class="token keyword">elif</span> transaction<span class="token punctuation">.</span>receiver <span class="token operator">==</span> account<span class="token punctuation">:</span><br /> balance <span class="token operator">+=</span> transaction<span class="token punctuation">.</span>amounts<br /> <span class="token keyword">return</span> balance<br /> <br /> <span class="token comment"># 驗證區塊鏈</span><br /> <span class="token keyword">def</span> <span class="token function">verify_blockchain</span><span class="token punctuation">(</span>self<span class="token punctuation">)</span><span class="token punctuation">:</span><br /> previous_hash <span class="token operator">=</span> <span class="token string">''</span><br /> <span class="token keyword">for</span> idx<span class="token punctuation">,</span> block <span class="token keyword">in</span> <span class="token builtin">enumerate</span><span class="token punctuation">(</span>self<span class="token punctuation">.</span>chain<span class="token punctuation">)</span><span class="token punctuation">:</span><br /> <br /> <span class="token comment"># 比對每一個區塊的 hash 是否正確</span><br /> <span class="token keyword">if</span> self<span class="token punctuation">.</span>get_hash<span class="token punctuation">(</span>block<span class="token punctuation">,</span> block<span class="token punctuation">.</span>nonce<span class="token punctuation">)</span> <span class="token operator">!=</span> block<span class="token punctuation">.</span><span class="token builtin">hash</span><span class="token punctuation">:</span><br /> <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"Error: Hash not matched!"</span><span class="token punctuation">)</span><br /> <span class="token keyword">return</span> <span class="token boolean">False</span><br /> <span class="token keyword">elif</span> previous_hash <span class="token operator">!=</span> block<span class="token punctuation">.</span>previous_hash <span class="token keyword">and</span> idx<span class="token punctuation">:</span><br /> <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"Error: Hash not matched to previous_hash"</span><span class="token punctuation">)</span><br /> <span class="token keyword">return</span> <span class="token boolean">False</span><br /> previous_hash <span class="token operator">=</span> block<span class="token punctuation">.</span><span class="token builtin">hash</span><br /> <br /> <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"Hash correct!"</span><span class="token punctuation">)</span><br /> <span class="token keyword">return</span> <span class="token boolean">True</span></code></pre>
<p>如果想要取得某個人的帳戶餘額,會從區塊鏈的第一塊開始檢查到最後一塊,如果有匯款出去就剪掉會出去的金額及手續費,如果有收到款項則加上金額。如果自己是該區塊的礦工,會把該區塊的手續費與出塊獎勵加上去。如此一來最後就能得出這個使用者的帳戶餘額。</p>
<p>如果想要驗證區塊鏈,只要從第一快檢查到最後一塊,看看每一塊的 hash 值是否都有符合該區塊區需要滿足的難度,如果該區塊難度為 5,hash 值就必須要是 5 個 0 開頭,難度為 6 則需要滿足 6 個 0 開頭...,以此類推。只要每一個區塊的驗證都是正確的,就可以確保整條區塊鏈都是沒問題的。</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/block-chain-02/">☞ 執行方法</a></strong></p>
<pre class="language-py"><code class="language-py"><span class="token comment"># 測試執行</span><br /> <span class="token keyword">def</span> <span class="token function">start</span><span class="token punctuation">(</span>self<span class="token punctuation">)</span><span class="token punctuation">:</span><br /> address<span class="token punctuation">,</span> private <span class="token operator">=</span> self<span class="token punctuation">.</span>generate_address<span class="token punctuation">(</span><span class="token punctuation">)</span><br /> self<span class="token punctuation">.</span>create_genesis_block<span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token keyword">while</span><span class="token punctuation">(</span><span class="token boolean">True</span><span class="token punctuation">)</span><span class="token punctuation">:</span><br /> <span class="token comment"># 初始化一筆交易</span><br /> transaction <span class="token operator">=</span> self<span class="token punctuation">.</span>initialize_transaction<span class="token punctuation">(</span>address<span class="token punctuation">,</span> <span class="token string">'test123'</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token string">'Test'</span><span class="token punctuation">)</span><br /> <span class="token keyword">if</span> transaction<span class="token punctuation">:</span><br /> <span class="token comment"># 將交易透過私鑰進行簽證</span><br /> signature <span class="token operator">=</span> self<span class="token punctuation">.</span>sign_transaction<span class="token punctuation">(</span>transaction<span class="token punctuation">,</span> private<span class="token punctuation">)</span><br /> <span class="token comment"># 礦工進行驗證</span><br /> self<span class="token punctuation">.</span>add_transaction<span class="token punctuation">(</span>transaction<span class="token punctuation">,</span> signature<span class="token punctuation">)</span><br /> self<span class="token punctuation">.</span>mine_block<span class="token punctuation">(</span>address<span class="token punctuation">)</span><br /> <span class="token keyword">print</span><span class="token punctuation">(</span>self<span class="token punctuation">.</span>get_balance<span class="token punctuation">(</span>address<span class="token punctuation">)</span><span class="token punctuation">)</span><br /> self<span class="token punctuation">.</span>adjust_difficulty<span class="token punctuation">(</span><span class="token punctuation">)</span></code></pre>
<p>測試執行的方式,需要先建立創世塊,並透過 while 迴圈進行挖礦的動作。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/block-chain-02-01.png" alt="" /></p>
<h2 id="%E7%B8%BD%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/block-chain-02/#%E7%B8%BD%E7%B5%90">#</a> 總結</h2>
<p>這次的 Demo 僅由一個節點運行,透過 function 的說明來簡易描述區塊鏈的運作。<br />
完整的程式碼在 <a href="https://github.com/krebikshaw/blockchain/blob/master/Blockchain.py">這邊</a>,同一個資料夾底下有另外兩個檔案是展示多節點下的區塊鏈運作,有興趣的人可以自己複製到本地端跑跑看。</p>
<p>希望這兩篇文章,有成功帶給大家一些幫助,讓我們一起入門區塊鏈。</p>
來畫個圖吧 - P5.js 入門
2022-05-03T00:00:00Z
https://blog.errorbaker.tw/posts/benben/05-p5-js/
<!-- summary -->
<!-- 好無聊喔!來畫個圖吧! -->
<!-- summary -->
<p><img src="https://i.imgur.com/nlXgHMj.png" alt="p5 demo" /></p>
<blockquote>
<p>圖片來源:我畫的</p>
</blockquote>
<h2 id="intro"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/05-p5-js/#intro">#</a> Intro</h2>
<p>嗨嗨!大家最近還好嗎?</p>
<p>(久違的文章)</p>
<p>前陣子真的忙爆了 QQ,轉換新工作、接了一個小案子,最近小弟終於有點時間,可以回歸 <strong>Error Baker</strong> 了,先跟大家還有其他持續在寫文章的夥伴說聲 Sorry!我回來了!</p>
<p>這次要跟大家聊聊的是:大家敲碗已久的(並沒有) <a href="https://p5js.org/">P5.js</a> ,顧名思義,<code>.js</code> 結尾的是一個 JavaScript 的 package ,他可以用來使用在藝術創作上,或許你可能聽過 <a href="https://d3js.org/">D3.js</a> 之類的 package,但兩者的差別在於:D3.js 是資料導向的、P5.js 是藝術導向的,雖然都算是視覺化的部分,但其實差別還是蠻大的,像是:D3 的資料大多是有意義的、P5 大多是隨機的亂數,對於新手來說,這樣理解會好懂一點(?)</p>
<p>如果說論實用性,筆者絕對推薦去學 D3.js ,有的公司面試時還會加分,因為可以製作很多的圖表、圓餅圖 ... 等,P5.js 的話真的就是學來藝術創作的(修身養性?),如果想稍為了解看看的話,還是可以看一下這一篇,這一篇算是稍為介紹一下 P5.js 是做什麼的,沒興趣的話也沒關係,那麼還有興趣的朋朋們,讓我們開始吧!</p>
<h2 id="tl%3Bdr"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/05-p5-js/#tl%3Bdr">#</a> TL;DR</h2>
<blockquote>
<p>成為工程師時,學到的新潮用語 Too Long; Don't Read</p>
</blockquote>
<p>先來看一下成品 ~</p>
<p><img src="https://i.imgur.com/nlXgHMj.png" alt="project demo" /></p>
<blockquote>
<p>可以猜猜筆者畫在什麼,文末公布答案。</p>
</blockquote>
<p>登勒!就一個樸實無華的小畫家,就是先讓大家看個畫面比較有感覺,對絕不是想玩 <a href="https://garticphone.com/zh-TW">電話遊戲</a> (?</p>
<p>在實作之前,筆者要大家先想一下,如果用 HTML/CSS/JS 的話大家會怎麼實作?(先想想就好不用真的做出來)</p>
<p><a href="https://openprocessing.org/sketch/1554977">DEMO / code</a> 連結</p>
<h2 id="init"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/05-p5-js/#init">#</a> Init</h2>
<p>首先要講的是畫布,一張圖最重要的就是畫布了,有了畫布就可以讓藝術家在上面盡情的發揮創意。</p>
<p>但在生成畫布之前,要先註冊一個 <a href="https://openprocessing.org/">OpenProcessing</a> 的帳號,這一個 Open source 的平台,可以免費註冊,就可以繼續我們的旅程了~</p>
<p><img src="https://i.imgur.com/YJI6iF7.png" alt="openProcessing" /></p>
<blockquote>
<p><a href="https://openprocessing.org/">OpenProcessing</a> 的官網就是潮(但就是慢了點?)</p>
</blockquote>
<p>開完新的專案後,畫面應該會長得跟我一樣,操作蠻人性化的(跟其他 Adxxx 的產品來說),但筆者還是稍為講解一下:</p>
<ol>
<li>頁面正中間的三角形按鈕,按下去就開始執行你的 code 了</li>
<li><code></></code> 應該不用我說了吧 XD 就是回到 code 的地方</li>
<li>右上角的 Save 就是存儲唷(專業中翻英?)</li>
<li>右側是一些設定,基本上不用去動</li>
</ol>
<p><img src="https://i.imgur.com/97LoeAN.png" alt="OpenProcessing" /></p>
<blockquote>
<p>非常人性化的介面,小巧而強大</p>
</blockquote>
<p>首先什麼都不用做,就開一個 project 就好了,什麼?這樣就可以開始畫畫了嗎?對喔!沒錯,就是這麼輕鬆!</p>
<h2 id="create"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/05-p5-js/#create">#</a> Create</h2>
<p>接著在畫布上發揮你的創意吧!</p>
<p>先什麼都不用動就按下 "三角行的執行按鈕" 吧!</p>
<p><img src="https://i.imgur.com/MVfRLzn.png" alt="OpenProcessing" /></p>
<blockquote>
<p>你已經進入 P5.js 的絕對領域了!</p>
</blockquote>
<p>可以先稍為玩一下,體驗一下 P5.js 的威力,到這邊已經離我們的最終成果不遠了(蛤?你說你什麼都還沒做啊!)</p>
<p>先別急,code 這不是來了嗎?<br />
筆者花了點時候加了註解(<s>翻譯,英文真的非常非常難</s>)</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token comment">// 只會在初始化的時候執行一次</span><br /><span class="token keyword">function</span> <span class="token function">setup</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// 初始化畫布</span><br /> <span class="token function">createCanvas</span><span class="token punctuation">(</span>windowWidth<span class="token punctuation">,</span> windowHeight<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token comment">// 背景:只有一個數值 (0 ~ 255) 黑 ~ 白</span><br /> <span class="token function">background</span><span class="token punctuation">(</span><span class="token number">200</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><br /><span class="token comment">// 每個 "frame" 都執行</span><br /><span class="token keyword">function</span> <span class="token function">draw</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// 當滑鼠按下的時候才畫</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>mouseIsPressed<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// 要畫的顏色:(0, 0, 0) ~ (255, 255, 255) => 黑色 ~ 白色</span><br /> <span class="token function">fill</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token comment">// 畫線,上一個 previous (pmouseX, pmouseY) 到現在滑鼠位置(mouseX, mouseY)</span><br /> <span class="token function">line</span><span class="token punctuation">(</span>mouseX<span class="token punctuation">,</span> mouseY<span class="token punctuation">,</span> pmouseX<span class="token punctuation">,</span> pmouseY<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>接著你就可以像是小畫家的方式再上面畫畫了,可以看到有很多的功能,例如:<code>line</code> 這個 <a href="https://p5js.org/reference/#/p5/line">function 的用法</a>,每次都要去查文件才會用,因為真多東西啦。</p>
<p>也可以參閱 <a href="https://p5js.org/reference/">P5.js 的文件</a></p>
<p>畫點點、畫線、畫正方形,你想要畫的,基本上文件都會有。</p>
<p>剩下的就是你的超能力了!</p>
<h2 id="summary"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/05-p5-js/#summary">#</a> Summary</h2>
<p>今天我們學到了 P5.js 可以讓我們用短短幾行 code 就可以完成偉大的創作。</p>
<p>帶大家稍為玩一下簡單的小畫家,雖然不是什麼很大的 project,但是試想:「如果不用 P5.js ,只使用原生的 HTML/CSS/JavaScript 去寫出一個小畫家,需要多少行的 code ?」但這邊 P5.js 都幫我們把 <strong>粗重</strong> 的工作都包好了,讓藝術家們可以專心的創作,當然這還只是 P5.js 的一小部分而已,如果你也有興趣,還可以試試更多的 <strong>生成式藝術</strong>,試試看在 P5.js 裡加入迴圈吧!</p>
<p>像是這樣:</p>
<p><img src="https://i.imgur.com/pvX2mAH.jpg" alt="p5 project demo" /></p>
<blockquote>
<p>好像突然跳太多了,有人敲碗的話再分享怎麼做好了 XD</p>
</blockquote>
<p>登勒!我們下回見!</p>
<h2 id="references"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/05-p5-js/#references">#</a> References</h2>
<ul>
<li><a href="https://p5js.org/">p5.js</a></li>
<li><a href="https://p5js.org/reference/">p5.js | reference</a></li>
<li><a href="https://openprocessing.org/">OpenProcessing</a></li>
<li><a href="https://hahow.in/courses/5d1ba52a0d5f3b0021dbb996/main?mts_s=ap&mts_m=ha&oasId=5f4793e9211da0aa8ae2f514">Hahow | 互動藝術程式創作入門 (Creative Coding)</a></li>
</ul>
<p>最上圖的答案是:胡立(虎力) 你猜對了嗎?</p>
<blockquote>
<p>免責聲名</p>
</blockquote>
<p>以上均為筆者自身經驗,難免小有主觀意見,供讀者們參考,也歡迎分享經驗交流。<br />
如果有錯誤的地方還請大大們指正,筆者會立刻修改,再次感謝大家!</p>
<p><a href="https://creativecommons.org/licenses/by/4.0/"><img src="https://i.creativecommons.org/l/by/4.0/88x31.png" alt="Creative Commons License" /></a></p>
<p>本著作係採用 <a href="https://creativecommons.org/licenses/by/4.0/">創用 CC 姓名標示 4.0 國際授權條款</a> 授權。您可以在 <a href="https://benben.me/">benben.me</a> 找到我。</p>
<p>This work is licensed under a <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>. You can find me at <a href="https://benben.me/">benben.me</a></p>
Uno CSS - 一統天下的明日之星?
2022-05-18T00:00:00Z
https://blog.errorbaker.tw/posts/benben/06-uno-css/
<!-- summary -->
<!-- 用 Uno CSS 統一天下 CSS!? -->
<!-- summary -->
<h2 id="intro"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/06-uno-css/#intro">#</a> Intro</h2>
<p>嗨嗨!又是我!</p>
<p>之前接的一個小案子,也準備要結案啦!開心開心,希望可以順利結案,這樣才有更多的時間寫 Error Baker 啊!</p>
<p>那麼直接進入正題,這次要分享的是:<strong>Uno CSS</strong> !</p>
<center>
<img src="https://i.imgur.com/LQrk0DN.png" class="post-image-width" alt="uno css logo" />
</center>
<blockquote>
<p>圖片來源: <a href="https://github.com/unocss/unocss">Uno CSS Github</a></p>
</blockquote>
<p>簡單說,他是一個 <strong>Tailwind</strong> 的替代 CSS 解決方案,沒有繁瑣的設定(<s>我沒有說 Tailwind 的設定很繁瑣喔</s>),可以直接使用,你可以說他抄襲 Tailwind ,但是他還有更強大的功能,例如:<code>正則表達式</code> 的配置 等等(這邊容筆者先賣的關子),看完這篇或許會有不一樣的想法喔!</p>
<p>比起目前前端的三大框架, CSS 的寫法百百種(SASS/SCSS, Bootstrap, Tailwind, WindyCSS, CSS in JS 系列 ... 等),那要學還是不學 Uno CSS?我覺得就看個人囉!但是如果說,你已經會了 Bootstrap 或是 Tailwind 了,那麼學習 Uno CSS 非常 <strong>直覺</strong> ,因為 Uno CSS 整合了這些習慣用法!</p>
<p>有點心動了嗎?還是還在猶豫呢?那麼我稍為介紹一下作者:<a href="https://github.com/antfu">antfu (Anthony Fu)</a> ,如果是熟悉 Vue 生態圈的讀者們,一定聽過他的大名,他是 Vue、Vite 核心成員之一;Windi CSS、VueUse 開發者之一,我也是學習 Vue 才開始 follow 他的,大大呢非常的狂,有機會再寫一篇介紹 antfu 大大的文章。</p>
<h2 id="tl%3Bdr"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/06-uno-css/#tl%3Bdr">#</a> TL;DR</h2>
<blockquote>
<p>成為工程師時,學到的新潮用語 Too Long; Didn't Read</p>
</blockquote>
<p>剛剛說學習 Uno CSS 很直覺?怎麼說呢?以 Tailwind 為例,剛接觸 Tailwind 的開發者(甚至熟練的開發者也還是常碰到),在開發一定遇到的錯折。</p>
<p>例如:</p>
<blockquote>
<p>in tailwind css</p>
</blockquote>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>w-25<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /><span class="token comment"><!-- error!: no `w-25` class --></span><br /><span class="token comment"><!-- notice!: this `w-25` class mean 6.25rem, because 1 : 0.25rem in tailwind --></span></code></pre>
<p>首先,在 tailwind 中,沒有 <code>w-25</code> 這個 class ,因為他通常是 2 的倍數,1, 2, 3, 4 ,5, 6, 12, 24, 48, 60, 96 ... 等(有可能還會漏掉),有個比較要注意的小地方,這邊的 <code>w-25</code> 指的是 <code>6.25 rem</code> 的寬,因為在 tailwind 中 w 的單位是 <code>1 : 0.25 rem</code>。</p>
<p>再來,還很常碰到一種狀況,你不知道數字代表的單位是什麼,看同一個例子:<code>w-25</code> 的 25 是什麼單位?%?px?rem?0.25rem?除非去看文件或裝 tailwind 的插件,才知道是什麼,而且常常不同的單位都不一樣,<code>m-4</code>, <code>border-3</code>, <code>text-lg</code>, <code>shadow-sm</code> ,你能夠用看得就說出分別是多少嗎?如果能夠直接說出來的人,筆者給你拍拍手。</p>
<p>最後,如果我就是要用 <code>w-25</code> 這個 class 怎麼辨?必須要去翻文件找設定檔,然後加了一堆設定檔,一時加一時爽,一直加一直爽,最後你的設定檔可能比你自己寫 CSS 之類的還來的多,這時的你一定會冒出:我為什麼要用 tailwind ?還不如自己寫?這是對於新手如我來說很常碰到的狀況,但這部分隨著熟悉度還會越來越好的啦!</p>
<p>另一方面,來看看 Uno CSS 的情況:</p>
<blockquote>
<p>in uno css</p>
</blockquote>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>w-25<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /><span class="token comment"><!-- ok!: auto generate `w-25` class --></span><br /><span class="token comment"><!-- notice!: this `w-25` class mean 6.25rem, uno css take care this for us --></span><br /><br /><span class="token comment"><!-- If I want to `25px` width --></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>w-25px<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /><span class="token comment"><!-- ok!: auto generate `w-25px` class --></span><br /><span class="token comment"><!-- notice!: this `w-25px` class mean 25px, uno css take care this for us without other syntax --></span><br /><br /><span class="token comment"><!-- If I want to `25rem` width --></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>w-25rem<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /><span class="token comment"><!-- ok!: auto generate `w-25rem` class --></span><br /><span class="token comment"><!-- notice!: this `w-25rem` class mean 25rem, uno css take care this for us without other syntax --></span></code></pre>
<p>用了 Uno CSS 上述的問題全部解決:數字問題、單位問題、設定檔問題。</p>
<p>(自從用了 Uno CSS 切板都 100 分呢!)</p>
<p>筆者第一次遇到這種情形,非常之驚奇,這是什麼巫術!</p>
<p>再來牛刀小試一下:</p>
<blockquote>
<p>in uno css</p>
</blockquote>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>w-777<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /><span class="token comment"><!-- ok!: auto generate `w-777` class --></span></code></pre>
<p><img src="https://i.imgur.com/4Rnomte.png" alt="uno css class" /></p>
<blockquote>
<p>圖片來源: 筆者的 VScode</p>
</blockquote>
<p>沒有在跟你五四三的!0 設定檔、自動生成!</p>
<p>要不要試試看 77777777777777 行不行?這邊留給有興趣的人試試看了,先刷一波 77777777777777 。</p>
<p>其實這不是什麼魔法,聰明的小當家一定猜到了,是「<strong>正則表達式</strong>」!我加了正則表達式。</p>
<blockquote>
<p>延伸閱讀:<a href="https://blog.huli.tw/2020/05/16/introduction-to-regular-expression/">簡易 Regular Expression 入門指南 - Huli</a></p>
</blockquote>
<h2 id="efficacy"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/06-uno-css/#efficacy">#</a> Efficacy</h2>
<p>光上述正則表達式的能功就打動我的心了!其實自動推導的功能在 Windi CSS 就有了,但 Uno CSS 在效能方面也是非常之神速啊!</p>
<p>來看看跟其他工具的跑分:</p>
<pre class="language-md"><code class="language-md">3/26/2022, 11:41:26 PM<br />1656 utilities | x50 runs (min build time)<br /><br />none 12.42 ms / delta. 0.00 ms<br />unocss v0.30.6 20.98 ms / delta. 8.57 ms (x1.00)<br />tailwindcss v3.0.23 1621.38 ms / delta. 1608.96 ms (x187.79)<br />windicss v3.5.1 1855.86 ms / delta. 1843.45 ms (x215.16)</code></pre>
<blockquote>
<p>資料來源:<a href="https://github.com/unocss/unocss">uno css github</a></p>
</blockquote>
<p>哇!這速度可以說是海放所有人了吧!</p>
<p>但 <code>Uno CSS</code> 非常低調(?)頁面中 <a href="https://github.com/unocss/unocss">uno css github page</a> 加入了這項說明: <code>Inspired by Windi CSS, Tailwind CSS, and Twind, but: ...</code></p>
<p>也不會說 X 打某某某、最棒的 CSS Library ...等(<s>迷之音:PHP 是最棒的程式語言</s>)。</p>
<p>相反地,Uno CSS 甚至 <strong>整合了其他常用 CSS Library 的 CSS 樣式</strong>!</p>
<p>例如: ml-3 (Tailwind), ms-2 (Bootstrap), ma4 (Tachyons), and mt-10px (Windi CSS) 全部都可以用啦!</p>
<blockquote>
<p>in Uno CSS</p>
</blockquote>
<pre class="language-css"><code class="language-css"><span class="token selector">.ma4</span> <span class="token punctuation">{</span> <span class="token property">margin</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span> <span class="token punctuation">}</span><br /><span class="token selector">.ml-3</span> <span class="token punctuation">{</span> <span class="token property">margin-left</span><span class="token punctuation">:</span> 0.75rem<span class="token punctuation">;</span> <span class="token punctuation">}</span><br /><span class="token selector">.ms-2</span> <span class="token punctuation">{</span> <span class="token property">margin-inline-start</span><span class="token punctuation">:</span> 0.5rem<span class="token punctuation">;</span> <span class="token punctuation">}</span><br /><span class="token selector">.mt-10px</span> <span class="token punctuation">{</span> <span class="token property">margin-top</span><span class="token punctuation">:</span> 10px<span class="token punctuation">;</span> <span class="token punctuation">}</span><br />// all works!</code></pre>
<p>這也是為什麼筆者說,只要學了 Tailwind, Bootstrap ... 等,在學習 Uno CSS 的時候會很快了,因為你已經熟悉了某些樣式的寫法了,但是如果 <code>只學了 Bootstrap 再學 Tailwind</code> 或是 <code>只學了 Tailwind 再學 Bootstrap</code> 都會多一個時間成本!因為不同工具寫法略有不同。</p>
<h2 id="document"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/06-uno-css/#document">#</a> Document</h2>
<p>一個好的 Library 怎麼可以沒有文件!</p>
<p>其中文件的好壞又會影響 DX(develop experience),有好的 DX 其實也是蠻重要的!</p>
<p>工程師寫 code 寫得開心,bug 就會減少;<br />
bug 減少,使用者 / 客戶就開心;<br />
使用者 / 客戶開心,老闆就開心;<br />
老闆開心,你就加薪(<s>並不會</s>)。</p>
<p>以 DX 來說 Vue 生態系的其實都不錯,例如:Vite (Vue 生態系的開發工具,而且也支援 React 等各大框架唷),這真的用了就回不去了,開發神之快速!Vite 甚至有套件支援 Ruby, Laravel!</p>
<blockquote>
<p>延伸閱讀:<a href="https://vitejs.dev/">Vite</a> 真的不用一下嗎?(<s>就是不想用的話,那我也沒辨法了 XD</s>)</p>
</blockquote>
<p>另外 Vue 的官網 - <a href="https://vuejs.org/">Vue</a> 已經支援暗色主題(約 2022/02 開始)啦!真香!</p>
<p>React 的官網 - <a href="https://reactjs.org/">React</a> 目前 React <code>18.1.0</code> 版(約 2022/05)依然沒有暗色主題(<s>每次翻 React 文件都刺眼</s>),官網甚至有點 outdated (但還是寫得不錯啦),我想初學 React 的開發者都很頭痛,我應該要先學 classes component 還是 function component + hooks?這邊應該有體驗過的朋友們,真的是有苦說不出!這邊就留給讀者自行意會(其實應該要先學好 JavaScript?)。</p>
<p>好了!這邊先打住,不然要戰起來了。</p>
<p>其實兩邊官網的內容都還不錯,都寫得很詳細!有心的話,認真讀完文件,應該都是可以上手的(前提是要有心,不然還是建議去找個教學會比較省時間)</p>
<p>回來看看這次 Uno CSS 的文件吧!</p>
<p><img src="https://i.imgur.com/sdBwpo0.png" alt="uno css document" /></p>
<blockquote>
<p>圖片來源:<a href="https://uno.antfu.me/">UnoCSS Interactive Docs</a></p>
</blockquote>
<p>其實文件也才剛 Beta 沒多久,一進來看到非常的潔簡,沒有酷炫特效、沒有跨大的標題、非常直覺的,就是找你的要找的東西,有種 Google 的既視感(?)。</p>
<p>來查查剛剛的 <code>w-25</code> class ...</p>
<p><img src="https://i.imgur.com/qKurIGO.png" alt="uno css document search" /></p>
<blockquote>
<p>圖片來源:<a href="https://uno.antfu.me/">UnoCSS Interactive Docs</a></p>
</blockquote>
<p>搜尋結果一出來,除了最重要的 <code>width: 6.25rem</code> 外,還有 <code>regex</code> 的用法、連結,甚至連 <code>MDN</code> 的連結都有,全部幫你整合好了!</p>
<p>真的是目前為止 DX 體驗最棒的文件,開發時間不多的話查到想要的資訊就可以閃了,時間多的話可以深入了解一下 CSS 的原理、複習一下 regex 用法等等,真的閒到發荒的話(<s>可以跟我說是哪間公司嗎</s>),可以試試按 <code>random</code> 看看。</p>
<h2 id="summary"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/06-uno-css/#summary">#</a> Summary</h2>
<p>在前端的路上,一路從學習 React 到默默變成 Vue 形狀了,我覺得蠻棒的,可以同時學習兩邊的一些技巧,沒有哪個好哪個不好,優缺點可以一起比較,在前端這個快速發展的領域中,更要保持開放的心態,通常會去爭論哪個框架好的,通常也只有學那一個框架,少有看到有人各框架都精通了,然後說哪個框架最棒,各有不同的 <code>trade off</code> ,爭論該用哪個框架,應該已經是個偽議題了,就如你不會說我學了 HTML ,不學 CSS 一樣,只是你要先學哪個?又要學得多深?</p>
<p>現況(截至 2022/05 )來說 <strong>React 確實較多人用、薪水比較高</strong>,但在<strong>文件、DX 體驗跟包容性 Vue 真的好的沒話說</strong>,更是難以想像 Vue 背後沒有大公司的支持,甚至在 Vue 作者(尤大大)的推文中,不時會看到 React 派的留言,如:<code>Why not React?</code></p>
<p>回到 <code>Uno CSS</code> 身上,仿佛可以看到一點點 Vue 當時的影子,成長性非常強,蠻有機會成為明日之星的,我覺得好的工具值得被更多人知道,但軟體的世界是 <strong>沒有銀彈的</strong>,但要不要使用就是看個人了。</p>
<p>那麼最重要的 <code>Uno CSS</code> 可以用在 Production 上了嗎?現階段來說,還不是正式版,最後的 API 不保証不會改動,但可以在小專案試試,筆者在自己小專案用了,開發體驗非常棒,所以才有了這篇文章,筆者也非常期待 <code>Uno CSS</code> 的上線版!</p>
<p>個人是覺得,不管有沒有要用都可以觀注一下!想了解更多的話,非常推薦看一下 <code>Uno CSS</code> 作者 AntFu 寫的這一篇:<a href="https://antfu.me/posts/reimagine-atomic-css-zh">重新构想原子化 CSS</a>,就算沒有要用 <code>Uno CSS</code> 也可以學到很多東西。</p>
<h2 id="ref"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/06-uno-css/#ref">#</a> Ref</h2>
<ul>
<li><a href="https://uno.antfu.me/">UnoCSS Interactive Docs</a></li>
<li><a href="https://windicss.org/">Windi CSS</a></li>
<li><a href="https://getbootstrap.com/">Bootstrap</a></li>
<li><a href="https://sass-lang.com/">Sass</a></li>
<li><a href="https://blog.huli.tw/2020/05/16/introduction-to-regular-expression/">簡易 Regular Expression 入門指南 - Huli</a></li>
<li><a href="https://vitejs.dev/">Vite</a></li>
<li><a href="https://antfu.me/posts/reimagine-atomic-css-zh">重新构想原子化 CSS</a></li>
</ul>
<blockquote>
<p>免責聲名</p>
</blockquote>
<p>以上均為筆者自身經驗,難免小有主觀意見,供讀者們參考,也歡迎分享經驗交流。<br />
如果有錯誤的地方還請大大們指正,筆者會立刻修改,再次感謝大家!</p>
<p><a href="https://creativecommons.org/licenses/by/4.0/"><img src="https://i.creativecommons.org/l/by/4.0/88x31.png" alt="Creative Commons License" /></a></p>
<p>本著作係採用 <a href="https://creativecommons.org/licenses/by/4.0/">創用 CC 姓名標示 4.0 國際授權條款</a> 授權。您可以在 <a href="https://benben.me/">benben.me</a> 找到我。</p>
<p>This work is licensed under a <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>. You can find me at <a href="https://benben.me/">benben.me</a></p>
用 sheetJS 匯出 excel
2022-05-22T00:00:00Z
https://blog.errorbaker.tw/posts/ruofan/xlsx/
<!-- summary -->
<p>Hi 大家好! 近期確診,不適的症狀蠻嚴重的,深刻感受到健康的重要!<br />
這篇文章會帶大家看如何透過 sheetJS 將資料匯出成 excel 格式。</p>
<!-- summary -->
<!-- more -->
<h4 id="sheetjs-%E8%A7%A3%E6%B1%BA%E4%BA%86%E5%93%AA%E4%BA%9B%E5%95%8F%E9%A1%8C-%3F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/xlsx/#sheetjs-%E8%A7%A3%E6%B1%BA%E4%BA%86%E5%93%AA%E4%BA%9B%E5%95%8F%E9%A1%8C-%3F">#</a> sheetJS 解決了哪些問題 ?</h4>
<p>透過下方的程式碼得知,sheetJS 讓我們可以用簡潔的程式碼匯出 excel。</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token operator"><</span>script setup<span class="token operator">></span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> utils<span class="token punctuation">,</span> writeFileXLSX <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'xlsx'</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> reactive<span class="token punctuation">,</span> watch <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@vue/composition-api'</span><br /><br /><span class="token keyword">const</span> xlsx <span class="token operator">=</span> <span class="token function">reactive</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token literal-property property">sheets</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token operator">...</span>your data<span class="token punctuation">]</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><br /><br /><span class="token keyword">function</span> <span class="token function">onExport</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">/* make the worksheet */</span><br /> <span class="token keyword">const</span> fieldsWS <span class="token operator">=</span> utils<span class="token punctuation">.</span><span class="token function">json_to_sheet</span><span class="token punctuation">(</span>xlsx<span class="token punctuation">.</span>sheets<span class="token punctuation">)</span><br /> <span class="token comment">/* add to workbook */</span><br /> <span class="token keyword">const</span> wb <span class="token operator">=</span> utils<span class="token punctuation">.</span><span class="token function">book_new</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> utils<span class="token punctuation">.</span><span class="token function">book_append_sheet</span><span class="token punctuation">(</span>wb<span class="token punctuation">,</span> fieldsWS<span class="token punctuation">,</span> <span class="token string">'sheets'</span><span class="token punctuation">)</span><br /> <span class="token comment">/* generate an XLSX file */</span><br /> <span class="token function">writeFileXLSX</span><span class="token punctuation">(</span>wb<span class="token punctuation">,</span> <span class="token string">'sheets.xlsx'</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><span class="token operator"><</span><span class="token operator">/</span>script<span class="token operator">></span><br /><br /><span class="token operator"><</span>template<span class="token operator">></span><br /> <span class="token operator"><</span>v<span class="token operator">-</span>btn color<span class="token operator">=</span><span class="token string">"primary"</span> @click<span class="token operator">=</span><span class="token string">"onExport"</span><span class="token operator">></span>匯出 excel<span class="token operator"><</span><span class="token operator">/</span>v<span class="token operator">-</span>btn<span class="token operator">></span><br /><span class="token operator"><</span><span class="token operator">/</span>template<span class="token operator">></span></code></pre>
<p>並且在許多平台上都可以正常匯出,下方是 <a href="https://app.saucelabs.com/open_sauce/user/sheetjs/tests/vdc">SAUCE LABS</a> 列出的支援度。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/saucelabs.svg" alt="" /><br />
圖片來源: <a href="https://app.saucelabs.com/open_sauce/user/sheetjs/tests/vdc">saucelabs</a></p>
<h4 id="%E5%BE%9E-source-code-%E4%BE%86%E8%A7%80%E5%AF%9F-sheetjs-%E8%83%8C%E5%BE%8C%E7%9A%84%E5%AF%A6%E4%BD%9C"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/xlsx/#%E5%BE%9E-source-code-%E4%BE%86%E8%A7%80%E5%AF%9F-sheetjs-%E8%83%8C%E5%BE%8C%E7%9A%84%E5%AF%A6%E4%BD%9C">#</a> 從 <a href="https://github.com/SheetJS/sheetjs/blob/b7d3eae3b7a02de1d03f0e627140c616443e40b0/bits/90_utils.js#L252">source code</a> 來觀察 sheetJS 背後的實作</h4>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">onExport</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">/* make the worksheet */</span><br /> <span class="token keyword">const</span> fieldsWS <span class="token operator">=</span> utils<span class="token punctuation">.</span><span class="token function">json_to_sheet</span><span class="token punctuation">(</span>xlsx<span class="token punctuation">.</span>sheets<span class="token punctuation">)</span><br /> <span class="token comment">/* add to workbook */</span><br /> <span class="token keyword">const</span> wb <span class="token operator">=</span> utils<span class="token punctuation">.</span><span class="token function">book_new</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> utils<span class="token punctuation">.</span><span class="token function">book_append_sheet</span><span class="token punctuation">(</span>wb<span class="token punctuation">,</span> fieldsWS<span class="token punctuation">,</span> <span class="token string">'sheets'</span><span class="token punctuation">)</span><br /> <span class="token comment">/* generate an XLSX file */</span><br /> <span class="token function">writeFileXLSX</span><span class="token punctuation">(</span>wb<span class="token punctuation">,</span> <span class="token string">'sheets.xlsx'</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span></code></pre>
<ul>
<li>
<p><a href="https://github.com/SheetJS/sheetjs/blob/b7d3eae3b7a02de1d03f0e627140c616443e40b0/bits/90_utils.js#L252">json_to_sheet</a> 透過 <a href="https://github.com/SheetJS/sheetjs/blob/b7d3eae3b7a02de1d03f0e627140c616443e40b0/bits/90_utils.js#L192">sheet_add_json Fn</a> 實作<br />
傳入的資料格式是 <code>array of objects</code>, xlsx 欄位的順序透過 <code>Object.keys</code> 來排序</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">json_to_sheet</span><span class="token punctuation">(</span>js<span class="token comment">/*:Array<any>*/</span><span class="token punctuation">,</span> opts<span class="token punctuation">)</span><span class="token comment">/*:Worksheet*/</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token function">sheet_add_json</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">,</span> js<span class="token punctuation">,</span> opts<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre>
</li>
<li>
<p><a href="https://github.com/SheetJS/sheetjs/blob/b7d3eae3b7a02de1d03f0e627140c616443e40b0/bits/98_exports.js#L12">writeFileXLSX</a> 透過 <a href="https://github.com/SheetJS/sheetjs/blob/b7d3eae3b7a02de1d03f0e627140c616443e40b0/bits/88_write.js#L188">writeFileSyncXLSX</a> 實作<br />
在 <a href="https://github.com/SheetJS/sheetjs/blob/b7d3eae3b7a02de1d03f0e627140c616443e40b0/bits/88_write.js#L112">writeSyncXLSX Fn</a> 中使用的 <a href="https://github.com/SheetJS/sheetjs/blob/b7d3eae3b7a02de1d03f0e627140c616443e40b0/bits/05_buf.js#L32">s2ab Fn</a> 這邊的實作使用到了 <code>new ArrayBuffer</code> ,稍微研究了一下。</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">s2ab</span><span class="token punctuation">(</span>s<span class="token comment">/*:string*/</span><span class="token punctuation">)</span><span class="token comment">/*:any*/</span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span><span class="token punctuation">(</span><span class="token keyword">typeof</span> ArrayBuffer <span class="token operator">===</span> <span class="token string">'undefined'</span><span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token function">s2a</span><span class="token punctuation">(</span>s<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">var</span> buf <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayBuffer</span><span class="token punctuation">(</span>s<span class="token punctuation">.</span>length<span class="token punctuation">)</span><span class="token punctuation">,</span> view <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Uint8Array</span><span class="token punctuation">(</span>buf<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">var</span> i<span class="token operator">=</span><span class="token number">0</span><span class="token punctuation">;</span> i<span class="token operator">!=</span>s<span class="token punctuation">.</span>length<span class="token punctuation">;</span> <span class="token operator">++</span>i<span class="token punctuation">)</span> view<span class="token punctuation">[</span>i<span class="token punctuation">]</span> <span class="token operator">=</span> s<span class="token punctuation">.</span><span class="token function">charCodeAt</span><span class="token punctuation">(</span>i<span class="token punctuation">)</span> <span class="token operator">&</span> <span class="token number">0xFF</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> buf<span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
</li>
</ul>
<p>下方是來自 <a href="https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer">mdn</a> 的介紹</p>
<blockquote>
<p>The ArrayBuffer is a data type that is used to represent a generic, fixed-length binary data buffer.<br />
you create a typed array view or a DataView which represents the buffer in a specific format, and use that to read and write the contents of the buffer.</p>
</blockquote>
<p>固定大小的原始二進制資料緩衝,讓存取資料更有效率。<br />
透過閱讀原始碼,認識到了平常沒有機會接觸到的資料型態!</p>
<h3 id="%E5%B0%8F%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/xlsx/#%E5%B0%8F%E7%B5%90">#</a> 小結</h3>
<p>實作的過程,蠻快速的。比較大的收穫是在閱讀原始碼的過程,整體來說非常有趣!<br />
在閱讀文章時如果有遇到什麼問題,或是有什麼建議,都歡迎留言告訴我,謝謝。😃</p>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/xlsx/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays">Document | JavaScript typed arrays</a></li>
<li><a href="https://javascript.info/arraybuffer-binary-arrays">Document | arraybuffer-binary-arrays</a></li>
<li><a href="https://docs.sheetjs.com/">Document | sheetjs</a></li>
</ul>
HSTORE vs JSONB in PostgreSQL
2022-06-29T00:00:00Z
https://blog.errorbaker.tw/posts/ruofan/postgresql-datatypes/
<!-- summary -->
<p>hi 大家好,使用 PostgreSQL 開發了一陣子,近期認識了 HSTORE 這個資料型態, 但 HSTORE 跟 JSONB 有什麼不同呢 ?<br />
這篇文章會帶大家認識在 PostgreSQL 中 HSTORE 與 JSONB 的差別。</p>
<!-- summary -->
<!-- more -->
<h4 id="data-types---hstore"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/postgresql-datatypes/#data-types---hstore">#</a> Data types - HSTORE</h4>
<p>從 sequelize <a href="https://github.com/sequelize/sequelize/blob/main/src/data-types.js#L574">source code</a> 中可以看到 這個資料型態是以 key / value 的方式儲存,相較於資料型態 JSONB 從下方的圖片可以看到 query 的 performance 是相對好一些的。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/performance-postgres.png" alt="" /></p>
<p>圖片資料來源: <a href="https://www.evanjones.ca/postgres-large-json-performance.html">Postgres large JSON value query performance</a></p>
<h4 id="data-types---jsonb"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/postgresql-datatypes/#data-types---jsonb">#</a> Data types - JSONB</h4>
<p>在實務上處理 json 類型的資料還是會選擇 JSONB 居多,一方面是因為 schemaless ,不需要細部定義 attributes 內有哪些 key / value,二方面是 儲存資料時將 JSON format 成 binary,因此 query 已經相較於一般的 JSON 有效率了。</p>
<p>從下方圖片可以看到 JSONB 資料型態的出現, postgresql 的使用率開始慢慢爬升。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/ranking-postgres.png" alt="" /></p>
<p>圖片資料來源:<a href="http://www.sai.msu.su/~megera/postgres/talks/jsonb-pgconfnyc-2021.pdf">Understanding JSONB Performance - PGCONF NYC 2021</a></p>
<h4 id="postgres-%E8%83%8C%E5%BE%8C%E5%A6%82%E4%BD%95%E8%99%95%E7%90%86%E8%88%87%E5%84%B2%E5%AD%98%E5%A4%A7%E5%9E%8B%E7%9A%84%E8%B3%87%E6%96%99%E5%91%A2%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/postgresql-datatypes/#postgres-%E8%83%8C%E5%BE%8C%E5%A6%82%E4%BD%95%E8%99%95%E7%90%86%E8%88%87%E5%84%B2%E5%AD%98%E5%A4%A7%E5%9E%8B%E7%9A%84%E8%B3%87%E6%96%99%E5%91%A2%EF%BC%9F">#</a> Postgres 背後如何處理與儲存大型的資料呢?</h4>
<p>以下為 postgresql <a href="https://www.postgresql.org/docs/current/storage-toast.html">官方文件</a> 上提到的 TOAST infrastructure :</p>
<blockquote>
<p>PostgreSQL uses a fixed page size (commonly 8 kB), and does not allow tuples to span multiple pages. Therefore, it is not possible to store very large field values directly. To overcome this limitation, large field values are compressed and/or broken up into multiple physical rows. This happens transparently to the user, with only small impact on most of the backend code. The technique is affectionately known as TOAST (or “the best thing since sliced bread”). The TOAST infrastructure is also used to improve handling of large data values in-memory.</p>
</blockquote>
<blockquote>
<p>The TOAST management code is triggered only when a row value to be stored in a table is wider than TOAST_TUPLE_THRESHOLD bytes (normally 2 kB).</p>
</blockquote>
<p>簡單來說,資料量大於 2 kB 時 資料經由壓縮後會被切分成固定大小的 TOAST chunks (1996B for 8KB page)儲存,從下方圖片可以看到每個 chunk 各自會有自己的 chunk_id。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/toast.png" alt="" /></p>
<p>圖片資料來源:<a href="http://www.sai.msu.su/~megera/postgres/talks/jsonb-pgconfnyc-2021.pdf">Understanding JSONB Performance - PGCONF NYC 2021</a></p>
<h4 id="%E7%95%B6-toast-storage-%E8%A2%AB%E8%A7%B8%E7%99%BC%E6%99%82%EF%BC%8C-jsonb-%E7%9A%84%E6%95%88%E8%83%BD%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/postgresql-datatypes/#%E7%95%B6-toast-storage-%E8%A2%AB%E8%A7%B8%E7%99%BC%E6%99%82%EF%BC%8C-jsonb-%E7%9A%84%E6%95%88%E8%83%BD%EF%BC%9F">#</a> 當 TOAST storage 被觸發時, JSONB 的效能?</h4>
<blockquote>
<p>When you update a value that's in TOAST, it always duplicates the whole value. That means even if you have a megabyte JSONB document and you're just updating a single part of that, with the correct syntax and whatnot in Postgres, Postgres for these large values has no way to avoid making a full copy of that TOAST value. And it's a problem because of the WAL traffic.<br />
資料來源:<a href="http://www.sai.msu.su/~megera/postgres/talks/jsonb-pgconfnyc-2021.pdf">Understanding JSONB Performance - PGCONF NYC 2021</a></p>
</blockquote>
<p>簡單來說,在 TOAST 的實作下 JSONB 的效能會不太好。</p>
<h2 id="%E5%B0%8F%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/postgresql-datatypes/#%E5%B0%8F%E7%B5%90">#</a> 小結</h2>
<p>透過了解在不同型態與資料量背後被處理與儲存的方式後,可以更有依據的思考如何定義資料型態。<br />
在閱讀文章時如果有遇到什麼問題,或是有什麼建議,都歡迎留言告訴我,謝謝。😃</p>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/postgresql-datatypes/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li><a href="https://www.postgresql.org/docs/current/storage-toast.html">Documentation | storage-toast</a></li>
<li><a href="http://rachbelaid.com/introduction-to-postgres-physical-storage/">Blog | Introduction to PostgreSQL physical storage</a></li>
<li><a href="https://www.evanjones.ca/postgres-large-json-performance.html">Blog | Postgres large JSON value query performance</a></li>
<li><a href="http://www.sai.msu.su/~megera/postgres/talks/jsonb-pgconfnyc-2021.pdf">Presentation | Understanding JSONB Performance - PGCONF NYC 2021</a></li>
</ul>
RE [前端引路人計劃] - 我也能夠當個 mentor 嗎?
2022-07-01T00:00:00Z
https://blog.errorbaker.tw/posts/benben/07-could-i-be-a-mentor-enough/
<!-- summary -->
<!-- 烽火連天、民不聊生,就在這時候,有一個人發起了「前端引路人計畫」 -->
<!-- summary -->
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/07-could-i-be-a-mentor-enough/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<p>!<strong>這篇文章算是偏心態跟自我提升的部分,也算是記錄一個故事</strong></p>
<p>時間過好快啊,不知不覺 2022 也過完一半了!年度目標還算有達成,下半年度要提升英文了,也算是講很久了,所以 6 月底跟朋友們報了 <a href="https://hero.voicetube.com/challenge-youth">VoiceTube 挑戰賽</a> (<br />
9 折優惠碼:v_rnc0xg),究竟破英文的我會不會挑戰成功呢?三個月後見。</p>
<p>我是 <a href="https://bootcamp.lidemy.com/">Lidemy 第五期</a> 的學生(也可能是最一期了?),轉職前端也半年多了,轉職的血淚史這邊就不多做解釋了,開心認識 Lidemy 的各路大神</p>
<p>雖然已經是前端工程師了,也依然保持著持繼學習的心態,對於前端領域的熱情,我也不知道會燒到什麼時候,但目前燒了 1 ~ 2 年,沒意外的話應該還會繼續,為了要成為資深前端,領導這個技能也算必點了,那麼如果公司都是大神,沒有人可以帶怎麼辨?</p>
<p>只好自己去找囉(?)</p>
<p>結果,我很幸運的被別人找了,所以有了這篇文章,究竟是不是命運的安排呢?我也不知道。</p>
<p>找我的人是 K ,我們是在 <a href="https://zerotomastery.io/community/developer-community-discord">Zero To Mastery (ZTM)</a> 的社群上相遇的,ZTM 列系的課也很棒,是 Udemy 上很火紅的系列,這邊也不做太多解釋,基本上,社群上面幾乎都外國人,所以能遇到台灣人真的很幸運!</p>
<p>K 說他那邊也找了 2 ~ 3 還在學習的 junior 打算開讀書會互相學習,非常迎歡有經驗的人一起,所以我也就順利地「混入」進去了,順利的開起我的「微 mentor 之路」</p>
<p>為什麼筆者會想開始「微 mentor 計劃」呢?讓我們繼續看下去~</p>
<h2 id="%E6%98%9F%E7%81%AB%E7%87%8E%E5%8E%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/07-could-i-be-a-mentor-enough/#%E6%98%9F%E7%81%AB%E7%87%8E%E5%8E%9F">#</a> 星火燎原</h2>
<p>所以我是誰不重要,重要的是,在很久以前、遙遠的銀河系 ...</p>
<p>(A long time ago in a galaxy far, far away....)</p>
<p>那時候的前端世界,還處於各種大戰時期,瀏覽器大戰、編輯器大戰、前端框架大戰 ... 各種大戰(<s>當然還有星際大戰</s>),戰到老子學不動了,戰到孩子都出生了,總之戰的你不要不要的。</p>
<p>烽火連天、民不聊生,就在這時候,有一個人發起了「前端引路人計畫」</p>
<blockquote>
<p>延伸閱讀: <a href="https://hulitw.medium.com/mentorship-program-350db93d5c9c">真正的 Mentorship program:前端引路人計畫 | by Huli | Medium</a></p>
</blockquote>
<p>「前端引路人計畫」是一個免費的活動,主要是由一個「mentor」帶領其他工程師一起成長,不局限於前端,要做的 project 也不限制,還算自由,重點是免費!</p>
<p>這微小的星火,是這戰亂前端世界的一小盞明燈,引路人計劃的前期階段也許成果不盡理想,但每次的一小步都是燎原的基石,也是最後演變為程式導師計劃的前身。</p>
<p>從非常早期的只收 5 個人左右,到第五期有快 100 人報名,因為有了助教系統所以可以招收更多的學生,挑戰成功人數也有 25 人左右(這邊是指完課後 2 個月內,找到工程師工作者,但其實更多,因為很多人後來還是有找到,只是晚了一點),另外那些沒有成功的人也是不用付學費的,因為這是一個保證班,保證轉職,但保證學習多少就是修行在個人了。</p>
<h2 id="%E6%83%BA%E6%83%BA%E7%9B%B8%E6%83%9C"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/07-could-i-be-a-mentor-enough/#%E6%83%BA%E6%83%BA%E7%9B%B8%E6%83%9C">#</a> 惺惺相惜</h2>
<p>後來順利加入了 Lidemy - 前端導師計劃第五期,非常地開心,覺得自己大概是世界上最幸運的人了。</p>
<p>因為像老師這麼好的人,這世界上已經很少了,出了社會才明白現實的殘酷,前輩憑什麼要教你技術?教會徒弟,餓死師父。</p>
<p>何況真正想學的人也是少之又少,有一部分是走馬看花的伸手黨也是不在少數,但參加程師導師計劃的都是有初步篩選的人,我看你骨骼精奇,是百年難得一見的練武奇才,要不要來當 <s>爆肝</s> 工程師。</p>
<p>呃 ... 好啊!</p>
<p>總之能跟一群有目標、有理想的人一起奮門,感覺真的很棒!</p>
<p>比較不一樣的是:這個計劃 <strong>沒有固定的 Final Project</strong> ,一般來說,很多 boot camp 都會有一個規劃好的 Final Project 最常見的就是:電商購物車、Twitter, Netflix Clone 之類的,所以能找到一樣有想法的人一起做 Final Project 本身就是一件難能可貴的事,再加上整個專案從零開始,從 user story 的規劃、 figma 的制作、前後端的實作,甚至到部署,全部只有你跟你的組員們一起,有問題可以詢問助教或老師,但基本上都是要跟組員們獨力完成,雖然辛苦,但最後完成真的很有成就感!</p>
<p>其實這邊的惺惺相惜指的不只是老師,也還有組員們、同學們,緲小我能找這樣的環境、同學、助教們,真的是很難能可貴。</p>
<p>每天寫 code 寫到半夜的日子,我大概一輩子不會忘記吧!</p>
<h2 id="%E8%96%AA%E7%81%AB%E7%9B%B8%E5%82%B3"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/07-could-i-be-a-mentor-enough/#%E8%96%AA%E7%81%AB%E7%9B%B8%E5%82%B3">#</a> 薪火相傳</h2>
<p>老師的始祖計劃文中提到:</p>
<blockquote>
<p>如果這一年過後你很感謝我這樣免費帶人,不用感謝我,你帶著這份謝意也開一樣的計畫去幫助其他人,就是最好的回報了。</p>
</blockquote>
<p>條件是:</p>
<blockquote>
<p>一年之後你也要開啟相同的計畫,收的人數不能少於 2 個。</p>
</blockquote>
<p>雖然我不是很資深,但也想著要回報老師,但誰知道想著想著,它就發生了,就是這麼神奇。</p>
<p>接下來,就是換我薪火相傳的部分了,來稍為介紹一下這次「微 mentor」計劃吧,我們主要是使用 discord 做為交流的工具,但分享的時候使用 google meet ,比較不會卡頓。</p>
<p>目前成員有(這邊用簡寫代稱):</p>
<ul>
<li>
<p>K,主揪,強而有力的自學者,目前在外商半導體產業,是寫 python 處理資料的,基本上他已經看到 react 了,看得差不多了,然後再刷一些 leetcode 了,我覺得根本就可以去轉職了 XD</p>
</li>
<li>
<p>S,目前還是學生,未來想往區塊鍊發展,在學 solidity 中,也用 next.js 寫了前台,但 react 的掌握度還算不高(?,她自己說的)但她主要也不是往前端發展,也可能往研究所前進。</p>
</li>
<li>
<p>A,目前在資策會上課,正在學習 PHP 後台中,也是估計今年完課完後,年底去求職,也有在看一些 PM 的課程,未來可能會往 PM 走也不一定。</p>
</li>
<li>
<p>我,廢物前端新手,看到大家都這麼認真,我真的是汗顏 QQ</p>
</li>
</ul>
<p>好啦,也許我沒這麼廢,但還是要學著韜光養晦,把自己的光茫收起來,把時間、空間留給這些明日之星發揮吧!沒,我就想當個廢物。</p>
<p>那麼就未來半年見囉,各位軟體工程師們。</p>
<p>就讓這渺小的星(薪)火燒下去吧。</p>
<p>$ to be continued? (y/n)<br />
$ y<br />
$ ...<br />
$ .</p>
<p>- 2022/06/31.</p>
<h2 id="ref"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/07-could-i-be-a-mentor-enough/#ref">#</a> Ref</h2>
<ul>
<li><a href="https://hulitw.medium.com/mentorship-program-350db93d5c9c">真正的 Mentorship program:前端引路人計畫 | by Huli | Medium</a></li>
<li><a href="https://zerotomastery.io/community/developer-community-discord">Zero To Mastery (ZTM)</a></li>
<li><a href="https://hero.voicetube.com/challenge-youth">VoiceTube hero</a></li>
</ul>
<p>以上均為筆者自身經驗,難免小有主觀意見,供讀者們參考,也歡迎分享經驗交流。<br />
如果有錯誤的地方還請大大們指正,筆者會立刻修改,再次感謝大家!</p>
<p><a href="https://creativecommons.org/licenses/by/4.0/"><img src="https://i.creativecommons.org/l/by/4.0/88x31.png" alt="Creative Commons License" /></a></p>
<p>本著作係採用 <a href="https://creativecommons.org/licenses/by/4.0/">創用 CC 姓名標示 4.0 國際授權條款</a> 授權。您可以在 <a href="https://benben.me/">benben.me</a> 找到我。</p>
<p>This work is licensed under a <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>. You can find me at <a href="https://benben.me/">benben.me</a></p>
Vue3 Composition API & React hooks
2022-07-30T00:00:00Z
https://blog.errorbaker.tw/posts/benben/08-vue-composition-api-and-react-hook/
<!-- summary -->
<!-- One side can't reach this altitude without another side. -->
<!-- summary -->
<p><strong>! this is a article about usage of Vue Composition API and React hooks. And assumes that you have knew some basic about one of them.</strong></p>
<center>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/i/296z018ivuyy4p3954at.png" alt="vue/react" /></p>
</center>
<blockquote>
<p><a href="https://dev.to/vuesomedev/write-vue-like-you-write-react-23p9">image resource</a></p>
</blockquote>
<h2 id="preface"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/08-vue-composition-api-and-react-hook/#preface">#</a> Preface</h2>
<p>Hi, there!</p>
<p>I'm Benben and this is my first time trying to write article with full English(Maybe the last time XD).</p>
<p>Why I wrote this article?</p>
<p>: I am still learning React and Vue, and very love both of them!</p>
<p>Why I wrote this article in English?</p>
<p>: Also I'm still learning English. Actually, My mother tongue is Mandarin(Tradition Chinese Specifically). But in real world, English is the most used language for software developer. There are so many many documents wrote in English or English only.</p>
<p>I'm not show off I'm good at English or something. In fact, I only got 550 points TOEIC (full score is 990), sounded incredible? But don't be, maybe this article just like a shit for someone. I have no argue with that. But you know what? Maybe 1 ~ 3 years later, this shit article could make me successful in my field, who knows?</p>
<p>Alright, bullshit enough. Let's do some coding.</p>
<h2 id="before-start"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/08-vue-composition-api-and-react-hook/#before-start">#</a> Before start</h2>
<p>So, when you joined the fight between Vue and React framework. You chose one side which is probably your favorite one. For most people, maybe that framework is the one and only they have learned.</p>
<p>But some developers will try to understand each other (Angular: I'm not made of plastic.). That is a good but rare situation. Most people like religious war and fight for their personal religion.</p>
<p>It's hard to acknowledge these fighting is not very helpful for anyone. Because in software field, there is <strong>no silver bullet</strong>. That say there is no the best tool can be used in any scenario.</p>
<p>You must take a long views. Not because old things change rapidly or because new things are hard to learn, but because <strong>everything fails all the time</strong>.</p>
<blockquote>
<p>everything fails all the time - Werner Vogels (Amazon vice president and CTO)</p>
</blockquote>
<h2 id="basic-usage"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/08-vue-composition-api-and-react-hook/#basic-usage">#</a> Basic Usage</h2>
<p>Let's see a basic usage - <code>Counter</code>!</p>
<p>A simple counter is always the best example to learn new Front-End framework. It show us how to definite reactive data and how reactive data drives the view layer.</p>
<p>Here are simple examples.</p>
<blockquote>
<p>in react</p>
</blockquote>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> useState <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'react'</span><br /><br /><span class="token keyword">const</span> <span class="token function-variable function">Counter</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>count<span class="token punctuation">,</span> setCount<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useSate</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h1</span><span class="token punctuation">></span></span><span class="token plain-text">Counter:</span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h1</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span> <span class="token attr-name">onClick</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">setCount</span><span class="token punctuation">(</span>count <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">}</span></span><span class="token punctuation">></span></span><span class="token plain-text">you click: </span><span class="token punctuation">{</span> count <span class="token punctuation">}</span><span class="token plain-text"> times!</span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /> <span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> Counter</code></pre>
<blockquote>
<p>in vue</p>
</blockquote>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">setup</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> ref <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'vue'</span><br /><span class="token keyword">const</span> count <span class="token operator">=</span> <span class="token function">ref</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><br /><span class="token keyword">const</span> <span class="token function-variable function">addCount</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> ref<span class="token punctuation">.</span>value<span class="token operator">++</span><br /></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span><br /><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>template</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h1</span><span class="token punctuation">></span></span>Counter:<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h1</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span> <span class="token attr-name">@click</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>addCount<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>you click: times!<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>template</span><span class="token punctuation">></span></span></code></pre>
<p>Both of them are not very different, right?</p>
<p>But still there are in different spirit.</p>
<p>I'm not talking about <code>MVC</code>, <code>MVVM</code> model or something like that. This topic really make many people confused, I guess. Take <code>MVC</code> for instance, what is <code>M</code>? what is <code>V</code>? what is <code>C</code>? And how do you implement them? There's no standard answer.</p>
<blockquote>
<p>Read more: <a href="https://stackoverflow.com/questions/667781/what-is-the-difference-between-mvc-and-mvvm">What is the difference between MVC and MVVM? - Stack Overflow</a></p>
</blockquote>
<p>Let's keep simple and see what we got.</p>
<p>In React, you build a component more like writing everything in JavaScript(CSS also can be in JavaScript, like CSS-in-JS).</p>
<blockquote>
<p>react</p>
</blockquote>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">const</span> <span class="token function-variable function">Component</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token comment">// JS/TS here! like logic, hooks etc.</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token punctuation">{</span><span class="token comment">/* Template here! */</span><span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /> <span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> Component</code></pre>
<p>In Vue, you build a component more like a mini HTML/JS/CSS instance.</p>
<blockquote>
<p>vue</p>
</blockquote>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">setup</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br /> <span class="token comment">// JS/TS here! like logic, composition API etc.</span><br /></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span><br /><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>template</span><span class="token punctuation">></span></span><br /> <span class="token comment"><!-- Template here! --></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>template</span><span class="token punctuation">></span></span></code></pre>
<p>In the another hand, CSS is the different story. There are a lot of solutions, you can choose what you need or just follow your religion, but that's kind of out of this topic.</p>
<p>In my opinion, I think Vue has a very open mindset about other competitor.</p>
<blockquote>
<p>Read more: <a href="https://vuejs.org/guide/extras/composition-api-faq.html#comparison-with-react-hooks">https://vuejs.org/guide/extras/composition-api-faq.html#comparison-with-react-hooks</a></p>
</blockquote>
<p>Moreover, I think React's document is some kind of out-dated. I sure that there are lots of people when they try to learn React (particular after 16.8) first time with a question: "Should learn Class component or Hooks first?"</p>
<p>In 2022, I would say learn Hooks first, but keep learning some Class component which you maybe need to maintain some legacy code written in it. In addition, class component contains a lot of good programming concepts like <code>classes</code>, <code>inherit</code>, <code>this</code> ...etc.</p>
<p>Recently React also renovate their document (2022.06). That is a good news for everyone.</p>
<blockquote>
<p>Read more: <a href="https://beta.reactjs.org/learn">https://beta.reactjs.org/learn</a></p>
</blockquote>
<p>As we can see, React Hooks feature released early than Vue Composition API. But the new document of Vue Composition API very early release than React's new document.</p>
<p>I bet this renovation been inspired by Vue. Vue3's new document released in 2022.02(about Chinese New Year) and it is very amazing. Both of two new document have dark mode, more newbie friendly, and more code example ...etc. I think it is a healthy competition and we developers take advantage of it. One side can't reach this altitude without another side.</p>
<h2 id="composition-api-and-hooks"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/08-vue-composition-api-and-react-hook/#composition-api-and-hooks">#</a> Composition API and Hooks</h2>
<p>Let's explore more in Composition API and React Hooks.</p>
<p>Here is an example like Counter but we add a <code>global</code> counter feature in it.</p>
<p>We will just use in <strong>plain React's or Vue's API</strong>, instead of using State management library like Redux/Zustand or Vuex/Pinia or something like that.</p>
<p>in <strong>Vue3 Composition API</strong></p>
<blockquote>
<p>useCounter.js</p>
</blockquote>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> <span class="token punctuation">{</span> ref <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'vue'</span><br /><br /><span class="token comment">// global</span><br /><span class="token keyword">const</span> globalCount <span class="token operator">=</span> <span class="token function">ref</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><br /><span class="token keyword">const</span> <span class="token function-variable function">addGlobalCount</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> globalCount<span class="token punctuation">.</span>value<span class="token operator">++</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token function-variable function">useCounter</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token comment">// local</span><br /> <span class="token keyword">const</span> localCount <span class="token operator">=</span> <span class="token function">ref</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><br /> <span class="token keyword">const</span> <span class="token function-variable function">addLocalCount</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> localCount<span class="token punctuation">.</span>value<span class="token operator">++</span><br /><br /> <span class="token keyword">return</span> <span class="token punctuation">{</span><br /> globalCount<span class="token punctuation">,</span><br /> addGlobalCount<span class="token punctuation">,</span><br /> localCount<span class="token punctuation">,</span><br /> addLocalCount<span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<blockquote>
<p>Counter.vue</p>
</blockquote>
<pre class="language-javascript"><code class="language-javascript"><span class="token operator"><</span>script setup<span class="token operator">></span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> useCounter <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../composition/useCounter'</span><br /><br /><span class="token keyword">const</span> <span class="token punctuation">{</span> msg <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">defineProps</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /><span class="token keyword">const</span> <span class="token punctuation">{</span><br /> globalCount<span class="token punctuation">,</span><br /> addGlobalCount<span class="token punctuation">,</span><br /> localCount<span class="token punctuation">,</span><br /> addLocalCount<span class="token punctuation">,</span><br /><span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">useCounter</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><span class="token operator"><</span><span class="token operator">/</span>script<span class="token operator">></span><br /><br /><span class="token operator"><</span>template<span class="token operator">></span><br /> <span class="token operator"><</span>div <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"card"</span><span class="token operator">></span><br /> <span class="token operator"><</span>button type<span class="token operator">=</span><span class="token string">"button"</span> @click<span class="token operator">=</span><span class="token string">"addLocalCount"</span><span class="token operator">></span><br /> count is<span class="token operator">:</span> <br /> <span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span>button type<span class="token operator">=</span><span class="token string">"button"</span> @click<span class="token operator">=</span><span class="token string">"addGlobalCount"</span><span class="token operator">></span><br /> global count is<span class="token operator">:</span> <br /> <span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><br /><span class="token operator"><</span><span class="token operator">/</span>template<span class="token operator">></span></code></pre>
<blockquote>
<p>app.vue</p>
</blockquote>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">setup</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br /><span class="token keyword">import</span> Counter <span class="token keyword">from</span> <span class="token string">'./components/Counter.vue'</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> useCounter <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./composition/useCounter'</span><br /><br /><span class="token keyword">const</span> <span class="token punctuation">{</span> globalCount <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">useCounter</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span><br /><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>template</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span>Global count is <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>Counter</span> <span class="token attr-name">msg</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>first<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>Counter</span> <span class="token attr-name">msg</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>second<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>template</span><span class="token punctuation">></span></span></code></pre>
<center>
<p><img src="https://hackmd.io/_uploads/rJectHun5.gif" alt="vite + vue" /></p>
</center>
<blockquote>
<p>Check out <a href="https://github.com/benben6515/counters/tree/main/vue-counter">full source code</a></p>
</blockquote>
<p>in <strong>React hook</strong></p>
<blockquote>
<p>Counter.jsx</p>
</blockquote>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> useState<span class="token punctuation">,</span> useContext <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'react'</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> GlobalCounterContext <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./contexts'</span><br /><br /><span class="token keyword">const</span> <span class="token function-variable function">Counter</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> id <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>count<span class="token punctuation">,</span> setCount<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><br /> <span class="token keyword">const</span> <span class="token punctuation">{</span> globalCount<span class="token punctuation">,</span> setGlobalCount <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">useContext</span><span class="token punctuation">(</span>GlobalCounterContext<span class="token punctuation">)</span><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span> <span class="token attr-name">onClick</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">setCount</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">count</span><span class="token punctuation">)</span> <span class="token operator">=></span> count <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">}</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token punctuation">{</span>id<span class="token punctuation">}</span><span class="token plain-text"> count is: </span><span class="token punctuation">{</span>count<span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span> <span class="token attr-name">onClick</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">setGlobalCount</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">count</span><span class="token punctuation">)</span> <span class="token operator">=></span> count <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">}</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> global count is </span><span class="token punctuation">{</span>globalCount<span class="token punctuation">}</span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /> <span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> Counter</code></pre>
<blockquote>
<p>App.jsx</p>
</blockquote>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> useState <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'react'</span><br /><span class="token keyword">import</span> Counter <span class="token keyword">from</span> <span class="token string">'./Counter'</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> GlobalCounterContext <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./contexts'</span><br /><br /><span class="token keyword">function</span> <span class="token function">App</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>globalCount<span class="token punctuation">,</span> setGlobalCount<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><br /><br /> <span class="token keyword">return</span> <span class="token punctuation">(</span><br /> <span class="token operator"><</span>GlobalCounterContext<span class="token punctuation">.</span>Provider <span class="token parameter">value</span><span class="token operator">=></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">className</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>App<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h1</span><span class="token punctuation">></span></span><span class="token plain-text">Vite + React</span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h1</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span><span class="token plain-text">global count: </span><span class="token punctuation">{</span>globalCount<span class="token punctuation">}</span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">className</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>card<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Counter</span></span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>first<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span><span class="token class-name">Counter</span></span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>second<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><span class="token plain-text"><br /> </span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span><span class="token class-name">GlobalCounterContext.Provider</span></span><span class="token punctuation">></span></span><br /> <span class="token punctuation">)</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> App</code></pre>
<center>
<p><img src="https://hackmd.io/_uploads/B1noYS_2c.gif" alt="vite + react" /></p>
</center>
<blockquote>
<p>Check out <a href="https://github.com/benben6515/counters/tree/main/vue-counter">full source code</a></p>
</blockquote>
<p>At this example, the most different between React and Vue is that the <strong>React's hook can <code>NOT</code> to write in the top level</strong> while <strong>Vue3's Composition API can do this!</strong></p>
<p>That why some people say composition API could replace Vuex(Pinia). And the <code>useContext</code> play a same role in React.</p>
<p>In fact, you can use them in a small project and that's no problem. But when you build a bigger project considering to use Redux/Vuex(Pinia) which are more reasonable option.</p>
<p>React's hook is very creative.</p>
<p>But in some cases like this example, Vue's Composition API is more flexible and intuitive.</p>
<blockquote>
<p>if you want see more about React and Vue: <a href="https://javascript.plainenglish.io/i-created-the-exact-same-app-in-react-and-vue-here-are-the-differences-e9a1ae8077fd">I created the exact same app in React and Vue. Here are the differences. | by Sunil Sandhu</a></p>
</blockquote>
<h2 id="react-and-vue"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/08-vue-composition-api-and-react-hook/#react-and-vue">#</a> React and Vue</h2>
<p>I think React and Vue are like friend and enemy, the <strong>React hooks do inspired Vue's Composition API</strong> which mention it in Vue's document and admitted it.</p>
<p>In my opinion, that is a healthy competition rather than "Involution".</p>
<p>Image the scenario which if only remain just React or just Vue in the world.</p>
<p>Maybe just React or Vue would not so powerful, but because there are both of them so people can discuss for them, argue for them, love them.</p>
<p>Thank React and Vue, they do make front-end development easier. Even ordinary me can build some simple web app.</p>
<p>I'm gratified that we are living in a best era.</p>
<p>Cheer and be happy codding.</p>
<h2 id="ref"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/08-vue-composition-api-and-react-hook/#ref">#</a> Ref</h2>
<ul>
<li><a href="https://javascript.plainenglish.io/i-created-the-exact-same-app-in-react-and-vue-here-are-the-differences-e9a1ae8077fd">I created the exact same app in React and Vue. Here are the differences. | by Sunil Sandhu</a></li>
<li><a href="https://overreacted.io/a-complete-guide-to-useeffect/">A Complete Guide to useEffect — Overreacted | by Dan</a></li>
<li><a href="https://vuejs.org/guide/extras/composition-api-faq.html">Composition API FAQ | Vue.js</a></li>
<li><a href="https://beta.reactjs.org/">React Docs Beta</a></li>
</ul>
<p><a href="https://creativecommons.org/licenses/by/4.0/"><img src="https://i.creativecommons.org/l/by/4.0/88x31.png" alt="Creative Commons License" /></a></p>
<p>This work is licensed under a <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>. You can find me at <a href="https://benben.me/">benben.me</a></p>
Setup your react native project
2022-08-13T00:00:00Z
https://blog.errorbaker.tw/posts/ruofan/react-native/
<!-- summary -->
<p>Hi,大家好! 前陣子在設置 react native 不同 simulator 的環境時踩了一些坑,這邊會和大家分享建制過程。</p>
<!-- summary -->
<!-- more -->
<h2 id="%E4%BB%80%E9%BA%BC%E6%98%AF-expo-%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/react-native/#%E4%BB%80%E9%BA%BC%E6%98%AF-expo-%EF%BC%9F">#</a> 什麼是 <code>Expo</code> ?</h2>
<p>在 react native 官方文件上提供了兩種方式讓我們啟動專案,分別是 <code>Expo CLI</code> 與 <code>React Native CLI</code>。<br />
下方是 <a href="https://reactnative.dev/docs/environment-setup">react native 官方文件</a> 上對於 Expo 的介紹:</p>
<blockquote>
<p>Expo is a set of tools built around React Native and, while it has many features, the most relevant feature for us right now is that it can get you writing a React Native app within minutes. You will only need a recent version of Node.js and a phone or emulator. If you'd like to try out React Native directly in your web browser before installing any tools, you can try out Snack.</p>
</blockquote>
<p>使用 <code>Expo</code> 的好處是,開發者不一定需要使用 mac 才能搭配 simulator 開發,如果是使用 windows 的開發者透過 <code>Expo</code> 也可以搭配 <a href="https://expo.dev/client">Expo Go</a> 進行開發。</p>
<h4 id="expo-%E6%9C%89%E4%BB%80%E9%BA%BC%E9%99%90%E5%88%B6%E5%97%8E-%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/react-native/#expo-%E6%9C%89%E4%BB%80%E9%BA%BC%E9%99%90%E5%88%B6%E5%97%8E-%EF%BC%9F">#</a> <code>Expo</code> 有什麼限制嗎 ?</h4>
<p>下方是 <a href="https://docs.expo.dev/introduction/why-not-expo/">expo 官方文件</a> 上的介紹:</p>
<blockquote>
<p>All libraries available to React Native apps are available to Expo managed workflow apps built with EAS Build, but some may require a Prebuild Config Plugin to be added.</p>
</blockquote>
<p>這邊的限制在於如果使用了像是第三方的推播服務或是 IOS 和 Android 原生的 libraries 在 build 專案時會需要搭配 config 設定檔。</p>
<h2 id="%E4%BB%80%E9%BA%BC%E6%98%AF-expo-go-%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/react-native/#%E4%BB%80%E9%BA%BC%E6%98%AF-expo-go-%EF%BC%9F">#</a> 什麼是 <code>Expo Go</code> ?</h2>
<p>下方是 <a href="https://docs.expo.dev/get-started/installation/#2-expo-go-app-for-ios-and">react native 官方文件</a> 上對於 <code>Expo Go</code> 的介紹:</p>
<blockquote>
<p>The fastest way to get up and running is to use the Expo Go client app on your iOS or Android device. It allows you to open up apps served through Expo CLI and run your projects faster when developing them. It is available on both the iOS App Store and Android Play Store.</p>
</blockquote>
<p>透過 <code>Expo Go</code> 可以直接在手機上將專案跑起來。</p>
<h4 id="expo-go-%E6%9C%89%E4%BB%80%E9%BA%BC%E9%99%90%E5%88%B6%E5%97%8E-%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/react-native/#expo-go-%E6%9C%89%E4%BB%80%E9%BA%BC%E9%99%90%E5%88%B6%E5%97%8E-%EF%BC%9F">#</a> <code>Expo Go</code> 有什麼限制嗎 ?</h4>
<p>下方是 <a href="https://docs.expo.dev/introduction/why-not-expo/">expo 官方文件</a> 上的介紹:</p>
<blockquote>
<p>additional native code cannot be added to Expo Go on the fly, so this means you are limited in terms of what dependencies you can add. This also applies to apps built with expo build:android and expo build:ios.</p>
</blockquote>
<blockquote>
<p>The size for an app built with expo build on iOS is approximately 20mb (download), and Android is about 15mb. This is because a bunch of APIs are included regardless of whether or not you are using them.<br />
If you'd like a smaller app size, you should use EAS Build.</p>
</blockquote>
<p>如果使用 <code>expo build</code> 專案的話會有以下限制:</p>
<ul>
<li>部分 IOS 和 Android 原生的 api 在 Expo Go 無法跑動。</li>
<li>部分的 api 儘管沒有被使用到還是會被打包在一起,造成 app 的最小 size 還是很大。</li>
</ul>
<p>官方提供了 <code>EAS Build</code> 來解決這些限制。</p>
<h2 id="%E4%BB%80%E9%BA%BC%E6%98%AF-eas-build-%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/react-native/#%E4%BB%80%E9%BA%BC%E6%98%AF-eas-build-%EF%BC%9F">#</a> 什麼是 <code>EAS Build</code> ?</h2>
<p>下方是 <a href="https://docs.expo.dev/build/setup/">expo 官方文件</a> 上的介紹:</p>
<blockquote>
<p>EAS Build allows you to build a ready-to-submit binary of your app for the Apple App Store or Google Play Store.<br />
EAS Build is a new service and we plan to address many of the current limitations in time.</p>
</blockquote>
<p>透過 <code>EAS Build</code> 可以 build 出 app 並且上至 app store 。</p>
<h4 id="eas-build-%E6%9C%89%E4%BB%80%E9%BA%BC%E9%99%90%E5%88%B6%E5%97%8E-%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/react-native/#eas-build-%E6%9C%89%E4%BB%80%E9%BA%BC%E9%99%90%E5%88%B6%E5%97%8E-%EF%BC%9F">#</a> <code>EAS Build</code> 有什麼限制嗎 ?</h4>
<p>是的,儘管 <code>EAS Build</code> 可以解決某些 <code>expo build</code> 的限制,但是 <code>EAS Build</code> 也是有些限制的! 💀</p>
<p>下方是 <a href="https://docs.expo.dev/build-reference/limitations/">expo 官方文件</a> 上的介紹:</p>
<blockquote>
<p>You may find that the resources available are not sufficient to build your app if your build process requires more than 12GB RAM. In the future we will be adding more powerful configurations to increase memory limits and speed up build times.</p>
</blockquote>
<p>官方的 <a href="https://docs.expo.dev/build-reference/infrastructure/">build infrastructure</a> 還有一些可以優化的部分,在未來會透過一些設定來新增記憶體的限制以及優化 build app 所花的時間。</p>
<blockquote>
<p>Build jobs for Android install npm and Maven dependencies from a local cache. Build jobs for iOS install npm dependencies from a local cache, but there is no caching for CocoaPods yet.</p>
</blockquote>
<p>在 build app 時所安裝的相依套件在 CocoaPods 上目前尚未使用 cache 機制處理。</p>
<blockquote>
<p>If your build takes longer than 2 hours to run, it will be cancelled. This limit is lower on the free plan, and is limit is subject to change in the future.</p>
</blockquote>
<p>如果 build app 所花的時間超過兩小時,排程會自動取消。</p>
<blockquote>
<p>We recommend using Yarn Workspaces because it is the only monorepo tool that we provide first-class integration with at the moment.</p>
</blockquote>
<p>官方建議使用 Yarn Workspaces 降低一些內部模組的相依。</p>
<h2 id="setup-simulator-on-computer"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/react-native/#setup-simulator-on-computer">#</a> setup simulator on computer</h2>
<p>這邊會以 Android Studio Emulator 為例:</p>
<ul>
<li>下載完 Android Studio 與 Java 後需要加上環境變數以及確認 adb 的版本</li>
</ul>
<h4 id="%E4%BB%80%E9%BA%BC%E6%98%AF-adb-%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/react-native/#%E4%BB%80%E9%BA%BC%E6%98%AF-adb-%EF%BC%9F">#</a> 什麼是 <code>adb</code> ?</h4>
<p>下方是 <a href="https://developer.android.com/studio/command-line/adb">ANDROID STUDIO 官方文件</a> 上的介紹:</p>
<blockquote>
<p>Android Debug Bridge (adb) is a versatile command-line tool that lets you communicate with a device. The adb command facilitates a variety of device actions, such as installing and debugging apps, and it provides access to a Unix shell that you can use to run a variety of commands on a device.</p>
</blockquote>
<p>透過 adb 指令可以方便我們安裝與在 Emulator debug。</p>
<h2 id="%E5%B0%8F%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/react-native/#%E5%B0%8F%E7%B5%90">#</a> 小結</h2>
<p>理解工具使用上的限制,後續思考應用哪些組合來因應需求會相對有依據。<br />
在閱讀文章時如果有遇到什麼問題,或是有什麼建議,都歡迎留言告訴我,謝謝。😃</p>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/react-native/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li><a href="https://docs.expo.dev/build-reference/infrastructure/">Documentation | expo</a></li>
<li><a href="https://reactnative.dev/docs/environment-setup">Documentation | react-native</a></li>
</ul>
Gatus demo
2022-09-08T00:00:00Z
https://blog.errorbaker.tw/posts/cwc329/gatus-demo/
<h1 id="gatus-demo"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/gatus-demo/#gatus-demo">#</a> Gatus Demo</h1>
<!-- summary -->
<p>筆者參加 iThome 主辦的 2022 雲端大會,其中一個議程是 Bo-Yi Wu 所主講的「自動化監控網站運行服務 – Gatus」,第一次聽到這個監控服務,於是自己練習一下做了一個小小的 demo 分享給大家。</p>
<!-- summary -->
<h2 id="gatus"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/gatus-demo/#gatus">#</a> Gatus</h2>
<p>Gatus 是一套以 Go 語言撰寫的開源網路監控軟體,可以為組織監控包含 http, tcp 等協定的網路通訊是否正常。</p>
<p>此軟體最常被問到的是:「目前市面上已經有很多監控服務,例如:Prometheus, Cloudwatch,Gatus 又有什麼不同,為何要選擇 Gatus 呢?」</p>
<p>這個問題在 <a href="https://github.com/TwiN/gatus#why-gatus">Gatus GitHub</a> 頁面已經很清楚了說明了:</p>
<blockquote>
<p>Neither of these(Prometheus etc.) can tell you that there’s a problem if there are no clients actively calling the endpoint. In other words, it's because monitoring metrics mostly rely on existing traffic, which effectively means that unless your clients are already experiencing a problem, you won't be notified.</p>
</blockquote>
<p>簡單來說,這些監控服務只有在監控對象有被使用的時候才會有記錄,而記錄中有問題才會被發現。假設今天有個 endpoint 本身有問題,不論是連線或者是回傳值,如果這個 api 沒有被使用的話,就無法從這些監控服務得知狀況。</p>
<p>而 Gatus 則是可以自動去測試這些 endpoint,並且透過預先寫好的 config 去判斷這個 endpoint 是否健康、是否正常。這就是 Gatus 與其他監控服務不同之處,也是其特色。</p>
<h2 id="demo"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/gatus-demo/#demo">#</a> Demo</h2>
<p>在 Gatus 的 GitHub 裡面就有一些基本的 config,供使用者在不同情境下使用。</p>
<p>筆者在這個基礎之上使用 docker 以及 docker-compose 寫了一個小小的 <a href="https://github.com/cwc329/gatus_demo">demo</a> 稍微展示一下 Gatus 的功能。</p>
<p>筆者在這個 demo 簡單地寫了一個小小的 express server,透過 docker 打包好之後用 docker-compose 把 demo server 以及 Gatus 都架起來,並且用 Gatus 去監控這個 demo server 的狀況。跑起來的話會是這樣:<br />
<img src="https://blog.errorbaker.tw/img/posts/cwc329/gatus_demo/1.png" alt="" /></p>
<p>筆者在 <a href="https://github.com/cwc329/gatus_demo/blob/main/index.js">demo server</a> 的一些 endpoint 動了一些手腳,並且在 <a href="https://github.com/cwc329/gatus_demo/blob/main/config/config.yaml">config</a> 裡面對這些 endpoint 設定不一樣的健康條件。</p>
<p>一般來說,對於 endpoint 最在意的就是他能否正常的回應。在 <code>/mayBreak</code> 這個 endpoint,筆者就隨機回傳 100 - 500 的 status code,而在 config 裡面檢查這個 endpoint 是否回傳介於 200 - 299 之間的 code。而在上圖可以看到 <code>/mayBreak</code> 這個 endpoint 時好時壞。</p>
<p>而有的時候即使 endpoint 都會回傳 2XX,但是速度很慢也值得注意,可能是程式有效能瓶頸,或者是系統有哪個地方出現問題。在 <code>/slow</code> 這個 endpoint 筆者就設定回傳時間會延遲 0-4 秒,而在 config 裡面設定這個 endpoint 需要在 2 秒內回傳。上圖的監控有能看到這個 endpoint 時好時壞。</p>
<h2 id="%E5%8F%AF%E8%83%BD%E4%BD%BF%E7%94%A8%E6%83%85%E5%A2%83"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/gatus-demo/#%E5%8F%AF%E8%83%BD%E4%BD%BF%E7%94%A8%E6%83%85%E5%A2%83">#</a> 可能使用情境</h2>
<p>在雲端大會看到這個開源軟體的時候,正好前陣子客戶環境遇到雲端主機都正常,但是 API 回傳很慢,導致客戶使用體驗不佳。而因為筆者平常只會去看機器的 log,並沒有將 API 的 log 導入 Cloudwatch,所以無法在第一時間發現問題,即使收到客戶回報問題,也要從茫茫的 log 海中找到對應的 log。不論是發現問題還是定位問題都很沒有效率。而且因為敝公司的系統滿複雜的,客戶並不是所有的服務都會用到。Gatus 這套工具看起來是能解決問題的。</p>
<p>不過重新想一下,因為商業模式以及合約的關係,因應維運需求工程師們有調閱程式 log 的權限,但是 RD 部門並不能進入客戶環境。也就是說即使知道某個 endpoint 有問題,工程師們也只能在開發環境下重現,並不能直接使用客戶的資料。更甚者,不能進入客戶環境,代表這些開放給客戶使用的 API 在沒有授權的狀況下 RD 也不能使用。所以如果 RD 部門想要使用 Gatus 來監控客戶環境的 API 是否正常,基本上是一項不可能的任務。</p>
<p>不過筆者在翻閱文件的時候發現 Gatus 除了監控 web API 之外,也能去看使用 TCP 協定的網路服務連線是否正常。剛好敝公司硬體部門前陣子的案例是客戶工廠的 IoT 服務有問題,找 Bug 找了好久,後來發現是有一台 Redis server 無法連上,在重啟這台 server 之後就恢復正常了。也就是說雖然敝公司無法使用 Gatus 作為監控客戶服務的工具,但是依然可以作為監控我們某些服務的工具。</p>
<p>所以筆者淺見,Gatus 要使用的話也要考慮到各家的商業模式以及合約。如果是做內部系統,基本上都有權限可以存取 web API 的話可以用這個作為一個輕量快速的監控系統。而如果需要監控沒有或者僅有受限權限的環境,Gatus 就可能不是一個好的解法。</p>
<h2 id="%E5%BB%B6%E4%BC%B8%E7%B7%B4%E7%BF%92"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/gatus-demo/#%E5%BB%B6%E4%BC%B8%E7%B7%B4%E7%BF%92">#</a> 延伸練習</h2>
<p>Gatus 還有很多可以使用的設定,用起來滿有趣的。<br />
我在 demo 中有附上使用 gmail 收到 alerting mail 的 config 範例,讀者如果有興趣可以把 demo clone 下來自己修改,讓自己的收到這些信件,不過記得可以先把 interval 調低,或者也可以設定 threshold 在一定錯誤之後才發送信件,避免被眾多警示信塞滿信箱。</p>
<p>這是我久違的文章,分享一下最近得知的新玩意兒,請大家多多指教。</p>
At a glance of Neo4j
2022-10-09T00:00:00Z
https://blog.errorbaker.tw/posts/ruofan/neo4j/
<!-- summary -->
<p>Hi 大家好! 前陣子到了新環境花了些時間適應,日常的學習是時候推進了。這篇文章會以 Neo4j 為例和大家分享什麼是 graph DB。</p>
<!-- summary -->
<!-- more -->
<blockquote>
<p>以下是這篇文章會談到的內容<br />
<a href="https://blog.errorbaker.tw/posts/ruofan/neo4j/#nosql-overview">NoSQL overview</a><br />
<a href="https://blog.errorbaker.tw/posts/ruofan/neo4j/#graph-database">Graph database</a><br />
<a href="https://blog.errorbaker.tw/posts/ruofan/neo4j/#neo4j">Neo4j</a></p>
</blockquote>
<h2 id="nosql-overview"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/neo4j/#nosql-overview">#</a> NoSQL overview</h2>
<p>在談什麼是 graph DB 之前,先帶大家總覽不同類型的 database,有別於一般常見的關聯式資料庫,NoSQL (Not Only SQL) 依據不同的型態又可細分為下方圖中的幾種類型。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/sql-no-sql.png" alt="" /></p>
<h2 id="%E4%BB%80%E9%BA%BC%E6%98%AF-graph-%3F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/neo4j/#%E4%BB%80%E9%BA%BC%E6%98%AF-graph-%3F">#</a> 什麼是 Graph ?</h2>
<p>從歷史上最早可以追朔到數學家 Leonhard Euler 為了求證 Seven Bridges of Königsberg 問題後來發展成為了數學中的 graph theory。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/Konigsberg_bridges.png" alt="" /></p>
<p>圖片資料來源:<a href="https://en.wikipedia.org/wiki/Seven_Bridges_of_K%C3%B6nigsberg">Seven Bridges of Königsberg</a></p>
<h2 id="graph-%E6%87%89%E7%94%A8%E5%A0%B4%E6%99%AF-%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/neo4j/#graph-%E6%87%89%E7%94%A8%E5%A0%B4%E6%99%AF-%EF%BC%9F">#</a> Graph 應用場景 ?</h2>
<p>先來釐清什麼樣的場景與需求適合使用 graph ? 這邊舉出兩種情境分享給讀者。</p>
<p>情境一:商品推薦系統的應用。在顧客的購物品項數據中分析出購物模式相似的族群,以下方圖示為例在商品推薦系統上可以推薦 Jennie 一些 Lisa 曾經買過的品項。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/scenario-1-neo4j.png" alt="" /></p>
<p>情境二:推薦路線系統的應用。在使用者的所在地與目的地中基於使用者選擇的偏好,舉例來說像是最佳路線, 轉乘次數最少等,推薦交通路線給使用者。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/scenario-2-neo4j.png" alt="" /></p>
<h2 id="graph-database"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/neo4j/#graph-database">#</a> Graph database</h2>
<p>在 graph database 中一個實體可以被當作一個 node,我們可以定義 node 的屬性, 分類, 標籤與關聯,以下圖示為例。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/node-graph.png" alt="" /></p>
<h2 id="%E5%A6%82%E4%BD%95%E5%9C%A8-graph-database-%E4%B8%AD%E4%BD%BF%E7%94%A8-query-%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/neo4j/#%E5%A6%82%E4%BD%95%E5%9C%A8-graph-database-%E4%B8%AD%E4%BD%BF%E7%94%A8-query-%EF%BC%9F">#</a> 如何在 graph database 中使用 query ?</h2>
<p>這邊先帶大家認識 Cypher (query language),下方是 <a href="https://en.wikipedia.org/wiki/Cypher_(query_language)#:~:text=Cypher%20is%20a%20declarative%20graph,formerly%20Neo%20Technology">wikipedia</a> 中的介紹:</p>
<blockquote>
<p>Cypher is a declarative graph query language that allows for expressive and efficient data querying in a property graph.</p>
</blockquote>
<pre class="language-c"><code class="language-c"># 以下方 query 為例可以看到這邊寫了一個 query 去取得在 <span class="token number">2005</span>年之後發行的所有電影。<br /><span class="token function">Match</span> <span class="token punctuation">(</span>m<span class="token operator">:</span>Movie<span class="token punctuation">)</span> where m<span class="token punctuation">.</span>released <span class="token operator">></span> <span class="token number">2005</span> RETURN m<br /><br /><span class="token macro property"><span class="token directive-hash">#</span> <span class="token expression">Create unique node property constraints to ensure that property values are unique <span class="token keyword">for</span> all nodes with a specific label<span class="token punctuation">.</span> Adding the unique constraint<span class="token punctuation">,</span> implicitly adds an index on that property<span class="token punctuation">.</span></span></span><br />CREATE CONSTRAINT <span class="token function">ON</span> <span class="token punctuation">(</span>n<span class="token operator">:</span>Movie<span class="token punctuation">)</span> <span class="token function">ASSERT</span> <span class="token punctuation">(</span>n<span class="token punctuation">.</span>title<span class="token punctuation">)</span> IS UNIQUE</code></pre>
<h2 id="neo4j"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/neo4j/#neo4j">#</a> Neo4j</h2>
<p>下方是 <a href="https://neo4j.com/">官方文件</a> 中的介紹:</p>
<blockquote>
<p>Neo4j is an open-source, NoSQL, native graph database that provides an ACID-compliant transactional backend for your applications that has been publicly available since 2007</p>
</blockquote>
<h2 id="%E4%B8%80%E8%88%AC-sql-%E8%B7%9F-neo4j-%E7%9A%84%E5%B7%AE%E5%88%A5%E5%9C%A8%E5%93%AA-%3F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/neo4j/#%E4%B8%80%E8%88%AC-sql-%E8%B7%9F-neo4j-%E7%9A%84%E5%B7%AE%E5%88%A5%E5%9C%A8%E5%93%AA-%3F">#</a> 一般 sql 跟 neo4j 的差別在哪 ?</h2>
<p>從下方 query 與 圖示可以看到在 neo4j 中我們不需要特別定義 foreign key 或是 join tables 而是透過 index 快速找到 node 之間的關聯。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/relational-graph.png" alt="" /></p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/sql-cypher.png" alt="" /></p>
<p>圖片資料來源:<a href="https://www.youtube.com/watch?v=RIWuA_K7_GY">Intro to Cypher for the SQL Developer</a></p>
<h2 id="%E5%93%AA%E4%BA%9B%E8%B3%87%E6%96%99%E4%B8%8D%E9%81%A9%E5%90%88%E4%BD%BF%E7%94%A8-neo4j-%E5%84%B2%E5%AD%98%EF%BC%9F"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/neo4j/#%E5%93%AA%E4%BA%9B%E8%B3%87%E6%96%99%E4%B8%8D%E9%81%A9%E5%90%88%E4%BD%BF%E7%94%A8-neo4j-%E5%84%B2%E5%AD%98%EF%BC%9F">#</a> 哪些資料不適合使用 neo4j 儲存?</h2>
<p>圖檔和影片檔 neo4j 官方建議存在 aws s3,node 儲存 s3 的 url 就好。</p>
<h2 id="%E7%B8%BD%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/neo4j/#%E7%B8%BD%E7%B5%90">#</a> 總結</h2>
<p>在準備 tech sharing 的過程中不斷的梳理如何表達得更清楚也是很棒的磨練呢!<br />
在閱讀文章時如果有遇到什麼問題,或是有什麼建議,都歡迎留言告訴我,謝謝。😃</p>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/neo4j/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li><a href="https://www.youtube.com/watch?v=ou2st6FYxR8">Youtube | Introduction to Neo4j - a hands-on crash course</a></li>
<li><a href="https://syntax.fm/show/487/supper-club-adam-cowley-and-neo4j-database#t=04:54">Podcast | Supper Club × Adam Cowley and Neo4j Database</a></li>
<li><a href="https://www.youtube.com/watch?v=nZwSo4vfw6c">Youtube | Seven Bridges of Königsberg</a></li>
<li><a href="https://en.wikipedia.org/wiki/Seven_Bridges_of_K%C3%B6nigsberg">wikipedia | Seven Bridges of Königsberg</a></li>
<li><a href="https://community.neo4j.com/t5/neo4j-graph-platform/video-and-photo-data-modeling/m-p/21282">neo4j | Video and Photo Data Modeling</a></li>
<li><a href="https://www.youtube.com/watch?v=RIWuA_K7_GY">Youtube | Intro to Cypher for the SQL Developer</a></li>
</ul>
如何提升打字速度 - 從初心者轉職超級初心者
2022-10-16T00:00:00Z
https://blog.errorbaker.tw/posts/benben/09-how-to-speed-up-typing/
<!-- summary -->
<!-- 如何提升打字效率,從劍士、法師、盜賊的方面來說 ... -->
<!-- summary -->
<p><strong>! 這一篇文章主要會講要提升打 Code 的速度,從各方面來說,包含物理、心理、視覺的方面,希望對大家有幫助!。</strong></p>
<center>
<p><img src="https://hackmd.io/_uploads/rJWJgRQ1i.gif" alt="typing" /></p>
</center>
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/09-how-to-speed-up-typing/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<p>大家安安,我終於又回來了,前陣子也是小忙,這次要跟大家分享是「打字效率」!</p>
<p>「打字快速」絕對是一門值得投資的技術,尤其是是做為長時間使用盤輸入的人,如:開發者、文字工作者,甚至仍至於 <strong>速錄師</strong> 等,如果你的打字速度非常快,別當 <s>爆肝</s> 工程師了,去當速錄師吧(有興趣的讀者可以去查看看速錄師的薪水)!筆者在學習的過程中,上過很多的線上課跟直播等等,發現有很多的優秀高手、大神,打字速度都非常快,手短如我,每每看到都很羨慕,要是自己的打字速快能有這麼快該有多好!然後就開始幻想快速 Coding ,下略 3000 字 ...</p>
<p>要先說明的是,注意我這邊講的是 <strong>效率</strong> ,而非速度,而效率也是主觀的感覺,所以我除了會介紹一些不錯的打字練習網站來提升速度,還會透過其他方式來增加 <strong>打字很快的感覺</strong>,如果你也有興趣就繼續看下去吧!</p>
<h2 id="%E5%88%9D%E5%BF%83%E8%80%85%E7%AF%87"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/09-how-to-speed-up-typing/#%E5%88%9D%E5%BF%83%E8%80%85%E7%AF%87">#</a> 初心者篇</h2>
<p>有人說最棒的 IDE (Integrated Development Environment) 是 VScode、有人說是 Notepad++ 、有人說是 VIM,有人說是 Office Word ,來聽看看前 Facebook 的工程師說說為什麼是 Office Word。</p>
<center>
<p><a href="https://www.youtube.com/watch?v=X34ZmkeZDos"><img src="https://img.youtube.com/vi/X34ZmkeZDos/0.jpg" alt="Best IDE" /></a></p>
</center>
<blockquote>
<p>Why Microsoft Word is the best IDE for programming | Joma Tech</p>
</blockquote>
<p>來試試看最棒的 IDE Microsoft Word 吧!等等!我依在 VScode 中的習慣打個 <code>! + tab</code>, <code>.wrapper > .item * 3</code> (emment 語法)然後馬上按 <code>Tab</code>,怎麼沒東西!應該會直接產生一個 html 的 template 的啊,窩不會寫扣了。</p>
<p>雖然表面上很好笑,但細思極恐,沒錯,我們都被現在的 IDE 寵壞了。</p>
<p>筆者認為 Word 很大,呃,不是!是 Word 很貴,要買正版的 Word 也是不便宜,看來要使用最棒的 IDE 也是要付出代價的!</p>
<p>還是說 ... 先使用純白的 txt 看看吧!</p>
<p>筆者真的幹過這種事,每次看完大神的技術分享後,滿腔熱血地打開一個空白記事本,直接進入心流狀態猛敲鍵盤,回過神來,已是一串 <code>console.log('Hello World')</code> ,筆者自己都不敢相信。</p>
<p>就讓我們回歸到最初的起點吧,只打開記事本,看看能不能在不 google 下就自己寫點什麼,如最簡單的:Counter, TodoList 等等。</p>
<hr />
<h2 id="%E5%8A%8D%E5%A3%AB%E7%AF%87"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/09-how-to-speed-up-typing/#%E5%8A%8D%E5%A3%AB%E7%AF%87">#</a> 劍士篇</h2>
<ul>
<li>技能:<strong>雙手鍵使用熟練度</strong></li>
</ul>
<p>最基本要提升打字速度的方法,沒什麼就是多練就對了!</p>
<p>打字練習的網站有很多,這邊就不依依介紹了,分享一些覺得不錯的網站,有些也是朋友介紹的,這邊就全部公開不私藏。</p>
<p>英打推薦網站:</p>
<ul>
<li><a href="https://www.ratatype.com/">https://www.ratatype.com/</a></li>
<li><a href="https://www.typing.com/">https://www.typing.com</a></li>
<li><a href="https://10fastfingers.com/typing-test/english">https://10fastfingers.com/typing-test/english</a></li>
<li><a href="https://qwerty.kaiyi.cool/">https://qwerty.kaiyi.cool/</a></li>
</ul>
<p>小建議是跟學習正確的指法一起練習,雖然短期來說可能打字會變慢,但習慣之後會發現,會比之前舊指法卡住的瓶頸還要快。</p>
<ul>
<li>技能:<strong>自動拼字防禦</strong></li>
</ul>
<p>再來是自動防禦的部分,字打的快,錯的也越多,這時候就可以試試 VScode 的插件:<a href="https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker">Code Spell Check</a></p>
<p>雖然這也是講到爛掉的插件,但因為真的很實用,尤其是打字快的時候,因為有時候自己再寫 function 的時候命名拼錯,結果整個 code base 的命名全部都錯,這時候想改可就是一個頭兩個大了,如果是個人專案還好,反正自己雷自己踩,踩久了就習慣了。</p>
<p>但如果是合作的專案可就不是開玩笑的了,例如:開發時程很趕,產生某個 typo ,結果就一直延用下去,環境變數也延用,後端也很延用,DataBase 也延用,這就不是自己改好就好的問題了。所以千萬不要 typo ,甚至用 typo 於雷到別人好嗎?自動防禦開下去就對了!</p>
<ul>
<li>技能:不要使用 <strong><code>Tab</code> 狂擊</strong></li>
</ul>
<p>再來是 <strong>不要使用 <code>Tab</code> (auto complete)</strong> 、還有在 Terminal 中一直按 "上" 只為了找一段 <code>npm run dev</code>,也這是很多新手開發者遇到的雷,反正現在的 IDE 都很強了,可以一路「<code>Tab</code>」到底,當一個 <code>Tab</code> 工程師,開發上是很快沒錯,但隨之而來的是:打字就是不快、甚至連常用的 Api 都記不起來(如:<code>document.getElementById('#app')</code>),測試自己看看不使用 <code>Tab</code> 能完成嗎?如果能完成那是花了多少秒?</p>
<p>有些讀者可能會認為,有必要嗎?用 <code>Tab</code> 不是很好嗎?筆者一開始也同意,但後來在一次的上課中,有同學請教了 <a href="https://blog.huli.tw/about/">胡立</a> 大大,英打如何打字跟老師一樣快,其中一建議就是「不用使用 Auto complete」從那一刻開始,我就很很少使用 Auto complete 了,我也覺得很有幫助,所以這邊也再分享這樣想法!</p>
<p>先說,筆者同意使用 <code>Tab</code> 的好處,也很多資深開發者也是 <code>Tab</code> 按好按滿,但前題是這些資深的開發者,大多 Api 都非常熟練、打字都非常快速了,使用 <code>Tab</code> 真的只是純粹幫他們節省時間。而就我看來,大多的新手開發者使用的 <code>Tab</code> 就不太一樣了,是啦,有節省到時間,但也省下了思考的機會、練習打字的機會。</p>
<blockquote>
<p>使用 <code>Tab</code> ,但只在你真的了解每個 <code>Tab</code> 下的意義。</p>
</blockquote>
<h2 id="%E6%B3%95%E5%B8%AB%E7%AF%87"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/09-how-to-speed-up-typing/#%E6%B3%95%E5%B8%AB%E7%AF%87">#</a> 法師篇</h2>
<ul>
<li>技能:<strong>隕石術</strong></li>
</ul>
<p>講了這麼多 hard core 的部分,是該來點「魔法」了,試試 VScode 插件:<a href="https://marketplace.visualstudio.com/items?itemName=hoovercj.vscode-power-mode">Power Mode</a></p>
<p>裝了之後,直接習得「隕石術」畫面真的很炫,可以直接看官網的 Demo,這邊就不再轉貼一次了。</p>
<p>用了之後寫 Code 都 100 分,連隕石砸下來都不怕了呢!沒 ... 老闆先不要再砸隕石下來了,窩快不行了。</p>
<p>一開始使用一下下,會覺得隕石術很棒,但可不可不要有地震術的效果(side effect?)?</p>
<p>有的,只要去設定的地方:</p>
<p>VScode 設定 -> 輸入「powermode」 -> 往下滑,找到「Shake:Enabled」 -> 把勾勾取消勾選</p>
<p>這樣就完成啦!</p>
<blockquote>
<p>之後就是隕石連發起來(不是開發的隕石喔)!打字直接 500x, 1000x 爽度一百啊!</p>
</blockquote>
<h2 id="%E7%9B%9C%E8%B3%8A%E7%AF%87"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/09-how-to-speed-up-typing/#%E7%9B%9C%E8%B3%8A%E7%AF%87">#</a> 盜賊篇</h2>
<ul>
<li>技能:<strong>殘影</strong></li>
</ul>
<p>來到了盜賊篇,就是要偷懶一下,來點被動技能「殘影」吧!</p>
<p>這邊什麼都不用安裝,因為這是原生 VScode 的「隱藏設定」,因為預設是關,好像也很多人不知道這個功能,筆者也是在某個大神直播中發現的,分享給朋友後,每個朋友都讚不絕口呢!</p>
<p>那麼筆者,就來教大家如何學習「殘影」:</p>
<ul>
<li>使用 VScode 的設定 <code>Cursor Smooth Create Animation</code>
<ol>
<li>打開 VScode 的設定檔畫面(<code>cmd</code> + <code>,</code>/<code>ctrl</code> + <code>,</code>)。</li>
<li>搜詢:<code>smooth</code>。</li>
<li>找到 <code>Editor: Cursor Smooth Create Animation</code>,並將它打勾。</li>
</ol>
</li>
</ul>
<p><img src="https://hackmd.io/_uploads/B1GLPaXJi.png" alt="Cursor Smooth Create Animation" /></p>
<blockquote>
<p>使用前</p>
</blockquote>
<p><img src="https://hackmd.io/_uploads/rJWJgRQ1i.gif" alt="before" /></p>
<blockquote>
<p>使用後</p>
</blockquote>
<p><img src="https://hackmd.io/_uploads/SkDylRQJs.gif" alt="after" /></p>
<p>好像 GIF 的截圖,因為有壓縮過,所以殘影的感覺不太明顯 QQ</p>
<p>但仔細看的話,使用後的滑鼠游標,會有殘影,變得 <strong>比較滑順</strong>。</p>
<blockquote>
<p>這樣就可以滑滑滑起來了,打起 code 來就是滑。</p>
</blockquote>
<h2 id="%E7%95%AA%E5%A4%96%E7%AF%87%EF%BC%9A%E5%95%86%E4%BA%BA%E7%AF%87"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/09-how-to-speed-up-typing/#%E7%95%AA%E5%A4%96%E7%AF%87%EF%BC%9A%E5%95%86%E4%BA%BA%E7%AF%87">#</a> 番外篇:商人篇</h2>
<ul>
<li>技能:<strong>買啦!哪次不買!</strong></li>
</ul>
<p>這邊就是裝備篇的部分了,身為一個優秀的工程師(?)一定找一個自己喜歡的鍵盤、配上喜歡軸體之類的,還有自己喜歡的手感,但這部分真的很因人而異。簡單說就是多試試就對了,才知道自己喜歡的軸體、材質手感等等,還有一個很重要的就是看你的需求,沒有攜帶的需求,RGB 燈效之類的。</p>
<p>筆者這邊也簡單分享自己的心得,因為喜歡手維持在鍵盤中,以保持效率,所以考慮的就會是 87%、60% 的鍵盤,因為數字鍵擺著也是佔位置,可以省下更多空間,再來是軸體的部分,也這也很看個人,因為我一開始也不熟悉各軸體的差別,所以第一把就買了萬用的茶軸,一開始感覺也還不錯,但用久了發現我的右手小指頭會有點痛(如果使用正統的指法打 code,會發現很多鍵都是右手小指),後來才改成紅軸,比較省力也比較安靜一點。</p>
<p>市面上常見的軸體,簡單來說:</p>
<ul>
<li>力道:紅軸 < 茶軸 < 青軸</li>
<li>音量:紅軸 < 茶軸 < 青軸</li>
</ul>
<p>但還是依各家的產品略有不同,簡單介紹給想入門的讀者(<s>但如果金錢不是問題的話,可以試試 HHKB</s>),還有盤冒材質的部分,我自己是黃油手,材質的部分就可以試試 PBT 或是更好的材質之類的,這部分也很多人做介紹了,就請各位商人們作點功課吧!</p>
<h2 id="%E7%B8%BD%E7%B5%90%EF%BC%9A%E8%B6%85%E7%B4%9A%E5%88%9D%E5%BF%83%E8%80%85%E7%AF%87"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/09-how-to-speed-up-typing/#%E7%B8%BD%E7%B5%90%EF%BC%9A%E8%B6%85%E7%B4%9A%E5%88%9D%E5%BF%83%E8%80%85%E7%AF%87">#</a> 總結:超級初心者篇</h2>
<ul>
<li>技能:<strong>我全都要</strong></li>
</ul>
<p>技術點數有限,前端學海無涯,回頭是岸啊,初心者!</p>
<p>但是我全都要!所以上述的方法可以依照個人的需求全部一起試試。或是你可以什麼都不做,只買一個自己爽的鍵盤;也可以只用公司的爛鍵盤去磨。只要你認為能提升「打字效率」就行了。</p>
<p>因為打字效率這個技能是會隨著使用者一起成長的,報酬率也是很高的!不要低估這個軟技能,有的公司就要求了開發者的打字速度,這個技能某部分來說,也可以說是區分了新手開發者跟中高階的開發者,很值得投值一點時間在上面的!</p>
<p>另外本篇文章中提到的這些技能,好像某的經典遊戲的內容,直接曝露年齡了(QQ),讀著們就別認了吧,回憶啊。</p>
<p>最後希望大家都能離理想中的駭客更進一步,但請當個道德駭客吧!</p>
<p>Happy Typing, Happy Hacking!</p>
<h2 id="ref"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/09-how-to-speed-up-typing/#ref">#</a> Ref</h2>
<ul>
<li><a href="https://www.youtube.com/watch?v=FCUi_dRU0tY">YouTube | VS Code tips — Enabling smoothly animated cursor movement</a></li>
<li><a href="https://www.ratatype.com/">https://www.ratatype.com/</a></li>
<li><a href="https://www.typing.com/">https://www.typing.com</a></li>
<li><a href="https://10fastfingers.com/typing-test/english">https://10fastfingers.com/typing-test/english</a></li>
<li><a href="https://qwerty.kaiyi.cool/">https://qwerty.kaiyi.cool/</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker">VScode Plugin | Code Spell Check</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=hoovercj.vscode-power-mode">VScode Plugin | Power Mode</a></li>
</ul>
<blockquote>
<p>免責聲名</p>
</blockquote>
<p>以上均為筆者自身經驗,難免小有主觀意見,供讀者們參考,也歡迎分享經驗交流。<br />
如果有錯誤的地方還請大大們指正,筆者會立刻修改,再次感謝大家!</p>
<p><a href="https://creativecommons.org/licenses/by/4.0/"><img src="https://i.creativecommons.org/l/by/4.0/88x31.png" alt="Creative Commons License" /></a></p>
<p>本著作係採用 <a href="https://creativecommons.org/licenses/by/4.0/">創用 CC 姓名標示 4.0 國際授權條款</a> 授權。您可以在 <a href="https://benben.me/">benben.me</a> 找到我。</p>
<p>This work is licensed under a <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>. You can find me at <a href="https://benben.me/">benben.me</a></p>
前端開發需了解的 GA 基礎
2022-11-05T00:00:00Z
https://blog.errorbaker.tw/posts/xiang/ga-basic/
<!-- summary -->
<p>針對 GA 前端可以做些什麼?前端可以如何去理解 GA?</p>
<!-- summary -->
<!-- more -->
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/ga-basic/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<p>有些時候客戶希望做到網站上的各種數據分析,例如:頁面瀏覽排行、取消訂閱次數、文件下載排名、站內搜尋關鍵字統計...等等。然後團隊在溝通過程中,選擇了 GA(Google Analytics)作為分析資料的工具,此時 PM 想找開發者一起討論,哪些數據的搜集是需要仰賴前端協助?需要進行哪些設定?程式碼要如何埋設...等等問題。</p>
<p>假設我們是第一次接觸 GA,我們可能會開始搜尋相關資料,查看 GA 的設置需要做哪些事情?如何去設定?能不能達成客戶的需求?你會發現網路上分享的相關資料,大多數都是教我們 GA 後台的使用,數據分析的技巧等等。五花八門的資料當中,很少是從開發者的角度,告訴前端該如何去認識 GA,也很難從這麼大量的資料中,去篩選並確認我現在這個專案需要如何設定來滿足客戶使用上的需求。</p>
<p>所以此篇文章,我會以前端的角度,帶開發者從程式的角度,認識 GA 的基本概念。文章當中不會做仔細的 GA 介紹,而是主要給個大方向,讓前端工程師理解在 GA 的領域當中,我們能如何提供協助,讓我們的產品 / 專案變得更好。</p>
<p>備註:本篇範例皆以 GA4 版本為主</p>
<h2 id="%E8%AA%8D%E8%AD%98-ga-%E5%89%8D%EF%BC%8C%E5%85%88%E4%B8%8D%E7%AE%A1-ga"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/ga-basic/#%E8%AA%8D%E8%AD%98-ga-%E5%89%8D%EF%BC%8C%E5%85%88%E4%B8%8D%E7%AE%A1-ga">#</a> 認識 GA 前,先不管 GA</h2>
<p>GA 的功能非常多,在第一次接觸 GA 的時候,看到一堆圖表一堆資料,容易產生一種我應該從哪裡開始研究的疑惑。GA 能做的事情五花八門,哪些是前端要處理?哪些又是其他人要處理?如果我要埋 code 我要埋哪裡?埋的內容是什麼?這些資訊該去哪裡搜尋?腦袋中會出現一大堆的問號。</p>
<p>回到 GA 的本質上,其實就是希望做到資料分析這件事情,所以我想把焦點拉回來資料分析這個主題上面,在文章的一開始,把資料的統計以及分析先搞清楚了,之後要了解 GA 就會變得容易許多,所以我們現在先不管 GA,來聊聊資料的統計與分析。</p>
<p>回到我們一般日常生活,其實常常會接觸到資料統計,比如說投票這件事它就是個資料搜集,搜集每個人的想法; 記帳這件事也算是資料搜集,搜集每一次的消費跟細節。所以撇除網站跟程式的架構,一般來說如果我們想做資料統計,我們會怎麼做?找紙筆記下來?記帳本?統一回報負責人進行紀錄?或者利用便利的軟體工具?</p>
<p>如果我們把 <code>搜集</code> 這個動作,再拆分的更仔細一點,我們一定會有一個 <code>紀錄的地方</code>,我們一定會有紀錄的 <code>行為</code>。</p>
<blockquote>
<p><strong>每一次行為的觸發,都會在紀錄的地方增添一筆紀錄。</strong></p>
</blockquote>
<p>以剛剛的例子來看:</p>
<ul>
<li>投票就是,每一次開票的動作觸發在白板上紀錄一筆數量。</li>
<li>記帳就是每一次消費的時候在記帳本上紀錄一筆消費的紀錄。</li>
</ul>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/ga-basic-01.png" alt="" /></p>
<p>而資料分析,就是在紀錄資料的地方把資料進行分析,看是要畫成表格還是圖表,總之就是做一切能視覺化呈現的行為。<br />
所以我們如果把 <code>分析</code> 這個動作用一句話來表達,它就會是:</p>
<blockquote>
<p><strong>紀錄的地方,接收到一筆紀錄以後,要做什麼事情?</strong></p>
</blockquote>
<p>以剛剛的例子來看:</p>
<ul>
<li>投票就是,每一次接收到票數資料,就把該選項的總票數加一。</li>
<li>記帳就是,每一次接收到消費資料,就把記帳本上的消費紀錄加上一筆,並且同步相關的表格與圖表數據。</li>
</ul>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/ga-basic-02.png" alt="" /></p>
<p>從上面的拆解我們可以理解到,<code>資料分析</code> 這件事情,可以把它拆分成 <code>搜集</code> 與 <code>分析</code> 兩件事。使用者會不斷觸發 <code>搜集</code> 行為,紀錄的地方則是在接收資料後進行 <code>分析</code>。</p>
<h2 id="%E4%BB%A5%E7%A8%8B%E5%BC%8F%E8%A7%92%E5%BA%A6%E6%80%9D%E8%80%83%E6%88%91%E5%80%91%E5%B8%B8%E5%B8%B8%E5%9C%A8%E5%81%9A%E7%9A%84%E4%BA%8B"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/ga-basic/#%E4%BB%A5%E7%A8%8B%E5%BC%8F%E8%A7%92%E5%BA%A6%E6%80%9D%E8%80%83%E6%88%91%E5%80%91%E5%B8%B8%E5%B8%B8%E5%9C%A8%E5%81%9A%E7%9A%84%E4%BA%8B">#</a> 以程式角度思考我們常常在做的事</h2>
<p>寫過 React 的人都知道,如果我們使用 Redux 進行資料管理,在元件需要跟 store 溝通的時候,需要透過 dispatch 來發送 action。透過 dispatch 來發送 action 之後,store 就會為這項行為進行一次紀錄。</p>
<p>透過 dispatch 發送 action,與 reducer 接收到 action 之後去改變 state,可以分成這兩件事情來看</p>
<ul>
<li>觸發 action</li>
<li>接收到 action 以後要做什麼</li>
</ul>
<p><img src="https://d33wubrfki0l68.cloudfront.net/01cc198232551a7e180f4e9e327b5ab22d9d14e7/b33f4/assets/images/reduxdataflowdiagram-49fa8c3968371d9ef6f2a1486bd40a26.gif" alt="" /></p>
<p>來源:<a href="https://redux.js.org/tutorials/essentials/part-1-overview-concepts">Redux 官網</a></p>
<p>action 是什麼?是開發者可以定義的行為,針對任何可能改動資料的行為我們都可以定義為 action。誰可以觸發 action?任何人都可以,可以是任何一個頁面,或任何一個元件,只要透過 dispatch 都可以將 action 發送到 reducer。而搜集資料的地方只有一個,且我們可以透過 reducer 定義要對哪些 action 進行處理,達成我們資料管理的目的。</p>
<p>意思是人人都可以發 action,但 reducer 可以決定要不要理他,如果是要處理的,reducer 就會去更新 state,如果是不需要處理的 reducer 就會忽略他,所以發 action,不代表一定會更新資料,錯誤的設定會導致資料管理出現我們不要的結果。</p>
<p>所以,正確的資料管理要同時達到兩件事情:</p>
<ol>
<li>在正確的位置、正確的時機,發送正確的 action</li>
<li>reducer 接收到 action 時,要做出正確的狀態處理</li>
</ol>
<h2 id="%E5%85%B6%E5%AF%A6%E6%88%91%E5%80%91%E5%B7%B2%E7%B6%93%E4%BA%86%E8%A7%A3-ga-%E4%BA%86"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/ga-basic/#%E5%85%B6%E5%AF%A6%E6%88%91%E5%80%91%E5%B7%B2%E7%B6%93%E4%BA%86%E8%A7%A3-ga-%E4%BA%86">#</a> 其實我們已經了解 GA 了</h2>
<p>回到資料分析本身,不論我們要產出什麼報表,或者我們要進行什麼分析,都離不開這兩件事情。</p>
<ul>
<li>搜集資料</li>
<li>接收到資料之後,進行處理、分析</li>
</ul>
<p>GA 的運作基礎,同樣也離不開這兩件事情。只是中間多了一層轉換而已,搜集資料是在網站上,資料處理、資料分析則是在 GA 後台完成。而每一次的資料搜集,GA 都把它稱作一次 <code>事件(event)</code>,這些事件會透過 GA 的 api 發送到 GA 後台進行處理。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/ga-basic-03.png" alt="" /></p>
<p>GA API 的結構跟 action 很像,我們知道 action 會有 action type 跟 action payload,event 也會有 event name,以及 payload。</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// ga API 架構</span><br /><span class="token function">gtag</span><span class="token punctuation">(</span><span class="token string">"event"</span><span class="token punctuation">,</span> <span class="token operator"><</span>event<span class="token operator">-</span>name<span class="token operator">></span><span class="token punctuation">,</span> <span class="token operator"><</span>event<span class="token operator">-</span>payload<span class="token operator">></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token comment">// 範例</span><br /><span class="token function">gtag</span><span class="token punctuation">(</span><span class="token string">"event"</span><span class="token punctuation">,</span> <span class="token string">"search"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">search_term</span><span class="token operator">:</span> <span class="token string">"t-shirts"</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>我們可以利用 GA 提供的 API 把事件打給 GA 統計後台,這個行為跟我們透過 dispatch 把 action 傳進去 store 概念是一樣的。<br />
所以針對任何想要紀錄的資訊我們都可以定義為 event。任何一個頁面,或任何一個元件,只要透過 GA API 都可以將 event 發送到 GA 後台。我們可以透過 GA 後台定義要對哪些 event 進行處理,達成我們資料分析的目的。</p>
<p>意思是人人都可以發 event,但 GA 後台可以決定要不要理他,如果是要處理的,GA 後台就會去更新報表,如果是不需要處理的 GA 後台就會忽略他,所以發 event,不代表一定會更新資料。</p>
<p>正確的資料分析要同時達到兩件事情:</p>
<ol>
<li>在正確的位置、正確的時機,發送正確的事件 (event)</li>
<li>GA 後台在接收到事件 (event) 以後,要透過正確的設定來產生分析結果</li>
</ol>
<p>前端要處理的事情,就是 event 的觸發(搜集),而 GA 後台就專心在處理資料分析。<br />
只要清楚 GA API 該如何使用,我們就能依據不同專案需求,在程式碼當中進行對應的設置了。<br />
我們把這個概念留在腦海當中,下面開始介紹 GA API,大家可以同時思考看看,這些方法與概念間的對應關係。</p>
<p>備註:除了前端主動 call GA API 發送的 event 之外,GA 也會預設去主動搜集一些特定事件(底下會提到)。</p>
<h2 id="%E5%89%8D%E7%BD%AE%E4%BD%9C%E6%A5%AD"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/ga-basic/#%E5%89%8D%E7%BD%AE%E4%BD%9C%E6%A5%AD">#</a> 前置作業</h2>
<p>在我們要運用 GA API 來做資料統計以前,會有一些前置作業需要處理</p>
<p>☞ 第一步 Create a Google Analytics 4 account and property:</p>
<ul>
<li>建立 GA 帳戶,這個帳戶就是用來登入 GA 後台的管理帳戶</li>
<li>其實就是單純的註冊帳號</li>
<li>可參考 <a href="https://support.google.com/analytics/answer/9304153#account">官方文件</a></li>
</ul>
<p>☞ 第二步 Create a web data stream for your website:</p>
<ul>
<li>在 GA 後台新增網站(web data stream)</li>
<li>一個 GA 後台可以同時管理多個網站的資料分析,當有新的網站想要加入 GA 時,就得把要進行分析的網站在 GA 後台進行登錄</li>
<li>可以參考 <a href="https://support.google.com/analytics/answer/9304153#stream&zippy=%2Cweb">官網文件</a></li>
</ul>
<p>☞ 第三步 Place the Google tag on your website:</p>
<ul>
<li>把 GA 後台上給我們的 JavaScript 程式碼片段,埋入專案當中。</li>
<li>當我們進行完這一步以後,就可以在專案中使用 <code>gtag (GA API)</code> 方法了。</li>
<li>程式碼片段範例如下</li>
</ul>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/ga-basic-04.png" alt="" /></p>
<p>第一步、第二步,是 GA 後台的使用者要建立的,可能是客戶或需要進行資料分析的其他角色。開發者只要等到第二步完成以後,拿著後台上指示要我們埋設的內容,把它埋入專案當中,這樣前置作業就完成了。</p>
<p>前置作業完成以後,基本上 GA 就會開始搜集該網站的資訊了(通常需要等待大約 1~2 天後台才會開始出現資料)。</p>
<h2 id="ga-%E4%BA%8B%E4%BB%B6%EF%BC%88event%EF%BC%89"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/ga-basic/#ga-%E4%BA%8B%E4%BB%B6%EF%BC%88event%EF%BC%89">#</a> GA 事件(event)</h2>
<p>上面提到了,GA 對於前端而言最重要的重點就在於掌握好 event 的觸發(搜集),在對的時間、對的位置,觸發對的 event,所以認識 <code>事件 (event)</code> 是一件很重要的事情。</p>
<p>GA 官方有針對事件 (event) 把它區分成 4 種類型,建議我們依據這些分類來做使用,認識這些分類的好處是,當我們知道:噢!原來某某事件已經預設就會幫我們處理的,那我們就不必自己設定了。因為這些預設事件除了會自己搜集資料以外,連報表都是幫我們預先設定好的,等於它已經幫我們省去了前面 60% 的基礎作業,或者完成了前 50% 的客戶需求。在開發時程有限的情況下,官方已經預設好的基礎設定,我們當然就是直接拿來用嘛!所以下面就來介紹事件的類別。</p>
<p>據 <a href="https://support.google.com/analytics/answer/9322688?hl=zh-Hant&ref_topic=9756175">官方說明</a> 所述,它把事件主要分成四種類別:<br />
- 自動收集事件<br />
- 加強評估事件<br />
- 建議事件<br />
- 自訂事件</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/ga-basic/#">☞ 自動收集事件</a></strong><br />
我們可以把 <code>自動收集</code> 當做預設事件,意思就是前置作業完成以後,就已經有了這些事件!我們不用自己打 API、不必自己埋 code,這些事件自己會去運作,自動收集它所需要搜集的數據。這些自動收集事件多半是 google 自己想收集的資訊,跟 APP 比較有關係,但我們還是可以稍微認識一下。</p>
<p>常見的 <code>自動收集</code> 事件,數據搜集的觸發條件:</p>
<ul>
<li>廣告相關(限於 APP):
<ul>
<li>ad_impression:使用者看到一次廣告曝光時</li>
<li>ad_click:使用者點按廣告時</li>
<li>ad_query:Mobile Ads SDK 送出廣告請求時</li>
</ul>
</li>
<li>App store 相關(限於 APP):
<ul>
<li>app_store_subscription_cancel:在 Google Play 取消付費訂閱時</li>
<li>app_store_subscription_convert:使用者從免費試用訂閱轉換成付費訂閱時</li>
<li>app_update:應用程式更新為新版本並再次啟動時</li>
</ul>
</li>
</ul>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/ga-basic/#">☞ 加強評估事件</a></strong><br />
這個跟網頁開發很有關係,<code>加強評估事件</code> 主要就是針對網頁 (Web) 做資料收集,它也是預設事件的一種,跟預設事件的差別在於,它需要在後台選擇開啟,它才會開始自動搜集這些事件,所以多了個開關可以控制(如圖)。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/ga-basic-05.png" alt="" /></p>
<p>常見的 <code>加強評估事件</code>,觸發條件:</p>
<ul>
<li>page_view:每次網頁載入或有效網站變更了瀏覽器記錄狀態時</li>
<li>scroll:使用者在各個網頁上第一次瀏覽到網頁底部時 (意即可見的網頁垂直捲動深度達 90%)</li>
<li>click:每次使用者點擊的連結會將其帶離目前網域時</li>
<li>view_search_results:每次使用者進行站內搜尋時 (會出現網址查詢參數)</li>
<li>file_download:當使用者點按的連結會連往特定檔案</li>
</ul>
<p>我們會發現,這些 <code>加強評估事件</code> 很多可能是客戶想要搜集的事件,頁面瀏覽排行、文件下載排名、站內搜尋關鍵字統計...等等,都可以利用這些加強評估事件來完成,而且由於 <code>自動收集事件</code>、<code>加強評估事件</code> 都屬於官方預設的事件,所以連報表都是幫我們拉好的,所以好好利用這些預設事件,能加快我們完成客戶需求的速度。</p>
<blockquote>
<p>備註:雖然 <code>加強評估事件</code> 是預設事件,不用特別埋 code,但還是得留意事件觸發的條件,比如說 <code>view_search_results</code> 會搜集使用者進行站內搜尋的事件,但它觸發的條件是當網址上的 query string 有出現符合 GA 後台設定 query 的值(預設是 <code>q</code>)的時候,事件才會觸發。所以沒滿足觸發條件的話預設事件也是不會觸發的,這點需要特別注意!</p>
</blockquote>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/ga-basic/#">☞ 建議事件</a></strong><br />
如果把 <code>加強評估事件</code> 想像成搜集「訪客」的操作事件,那 <code>建議事件</code> 主要就是 GA 提供了一些針對「客戶」操作的資料搜集建議。相較於「訪客」只有對網站進行瀏覽,「客戶」則是可以對網站進行更細節的操作,像是 <code>登入行為</code>、<code>購買行為</code>...等等,這些行為是會具備特定身份的。</p>
<p>由於搜集「客戶」特定行為,在觸發事件時,需要把當下數據透過 API 傳送給 GA 後台(例如:購買事件的金額),所以 <code>建議事件</code> 它不是預設事件,它需要透過前端把程式碼埋進專案當中,在需要觸發事件時,把指定的參數透過 API 發送給後台。</p>
<p>這邊舉一個 <a href="https://developers.google.com/analytics/devguides/collection/ga4/reference/events#add_to_cart">官方的例子</a>,<code>add_to_cart</code> 這個建議事件,紀錄使用者把商品加入購物車的行為,它就有規範使用這個建議事件時,要符合什麼樣 API 的格式:</p>
<pre class="language-js"><code class="language-js"><span class="token function">gtag</span><span class="token punctuation">(</span><span class="token string">"event"</span><span class="token punctuation">,</span> <span class="token string">"add_to_cart"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">currency</span><span class="token operator">:</span> <span class="token string">"USD"</span><span class="token punctuation">,</span> <span class="token comment">// 幣別</span><br /> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token number">7.77</span><span class="token punctuation">,</span> <span class="token comment">// 金額</span><br /> <span class="token literal-property property">items</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">item_id</span><span class="token operator">:</span> <span class="token string">"SKU_12345"</span><span class="token punctuation">,</span> <span class="token comment">// 商品 ID</span><br /> <span class="token literal-property property">item_name</span><span class="token operator">:</span> <span class="token string">"Stan and Friends Tee"</span><span class="token punctuation">,</span> <span class="token comment">// 商品名稱</span><br /> <span class="token literal-property property">price</span><span class="token operator">:</span> <span class="token number">9.99</span><span class="token punctuation">,</span> <span class="token comment">// 商品單價</span><br /> <span class="token literal-property property">quantity</span><span class="token operator">:</span> <span class="token number">1</span> <span class="token comment">// 數量</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">]</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>文件提供好 API 格式以後,使用上也很容易,只需要在觸發事件時,call 這支 api 就可以了。</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token function-variable function">Cart</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /><br /> <span class="token keyword">const</span> <span class="token function-variable function">handleAddToCart</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">id<span class="token punctuation">,</span> name<span class="token punctuation">,</span> price<span class="token punctuation">,</span> quantity</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token function">gtag</span><span class="token punctuation">(</span><span class="token string">"event"</span><span class="token punctuation">,</span> <span class="token string">"add_to_cart"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">currency</span><span class="token operator">:</span> <span class="token string">"USD"</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token number">7.77</span><span class="token punctuation">,</span><br /> <span class="token literal-property property">items</span><span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token literal-property property">item_id</span><span class="token operator">:</span> id<span class="token punctuation">,</span><br /> <span class="token literal-property property">item_name</span><span class="token operator">:</span> name<span class="token punctuation">,</span><br /> price<span class="token punctuation">,</span><br /> quantity<span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">]</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">return</span> <span class="token punctuation">{</span><br /> <span class="token operator">...</span> 省略 <span class="token operator">...</span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">handleAddToCart</span><span class="token punctuation">(</span>item<span class="token punctuation">.</span>id<span class="token punctuation">,</span> item<span class="token punctuation">.</span>name<span class="token punctuation">,</span> item<span class="token punctuation">.</span>price<span class="token punctuation">,</span> item<span class="token punctuation">.</span>quantity<span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token operator">></span><br /> 加入購物車<br /> <span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator">...</span> 省略 <span class="token operator">...</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> Cart<span class="token punctuation">;</span></code></pre>
<p>這些 <code>建議事件</code> 都是有 <a href="https://developers.google.com/analytics/devguides/collection/ga4/reference/events">官方文件</a> 做輔助的,當我們需要搜集的資訊是有被列入建議事件當中的,我們就可以參考官方文件,依據建議的格式來埋設 event。</p>
<p>常見的 <code>建議事件</code>:</p>
<ul>
<li>sign_up:使用者註冊 (以評估各種註冊方式的熱門程度)</li>
<li>login:使用者登入</li>
<li>join_group:使用者加入群組</li>
<li>select_content:使用者選取內容</li>
<li>share:使用者分享內容</li>
<li>spend_virtual_currency:使用者以虛擬貨幣 (錢幣、寶石或代幣等) 進行消費時</li>
<li>線上銷售相關:
<ul>
<li>add_payment_info:使用者提交付款資訊</li>
<li>add_shipping_info:使用者提交運送資訊</li>
<li>add_to_cart:使用者將商品加入購物車</li>
<li>add_to_wishlist:使用者將商品加入願望清單</li>
<li>purchase:使用者完成購買</li>
<li>refund:使用者收到退款</li>
</ul>
</li>
<li>遊戲相關:
<ul>
<li>earn_virtual_currency:使用者獲得虛擬貨幣 (錢幣、寶石或代幣等)</li>
<li>level_start:使用者在遊戲中展開新的關卡</li>
<li>level_end:使用者在遊戲中完成關卡</li>
<li>level_up:使用者在遊戲中升級</li>
<li>unlock_achievement:使用者解鎖成就</li>
</ul>
</li>
</ul>
<p>官方建議事件非常多,這裡只列出了一小部分,全部的建議事件內容,大家可以直接參考 <a href="https://developers.google.com/analytics/devguides/collection/ga4/reference/events">官方文件</a>。</p>
<p><strong><a href="https://blog.errorbaker.tw/posts/xiang/ga-basic/#">☞ 自訂事件</a></strong><br />
自訂事件是分類當中的最後一種事件了,我們可以把 <code>自訂事件</code> 想像成客製化事件 (custom event),當 <a href="https://developers.google.com/analytics/devguides/collection/ga4/reference/events">官方文件</a> 所列出的預設事件或建議事件都沒有任何可以滿足我們需求的項目時,再來使用 <code>自訂事件</code>。</p>
<p>所以 <code>自訂事件</code> 也是自由度最高的,我們想怎麼打就怎麼打,想搜集什麼就搜集什麼,<s>例如搜集使用者的帳號密碼</s>,這個絕對不可以做!打咩!</p>
<p>舉一個比較常見的例子,就是搜集用戶「取消訂閱」的次數。那要怎麼搜集呢?看客戶的需求是什麼?假如客戶的需求是計算「取消訂閱按鈕」被點擊的次數,那我們就可以來定義一個 <code>自訂事件</code>,把事件埋在「取消訂閱按鈕」上面,每一次點擊就觸發一次事件。</p>
<p>由於只是要搜集次數,event payload 甚至不需要傳,所以這個事件就只需要有一個 event name 就可以了 <code>gtag('event', 'unsubscribe')</code>。</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// 自訂事件範例:搜集取消訂閱次數</span><br /><br /><span class="token keyword">const</span> <span class="token function-variable function">MySubscribe</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span><br /> <span class="token operator">...</span> 省略 <span class="token operator">...</span><br /> <span class="token operator"><</span>button onClick<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">gtag</span><span class="token punctuation">(</span><span class="token string">'event'</span><span class="token punctuation">,</span> <span class="token string">'unsubscribe'</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token operator">></span>取消訂閱<span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator">...</span> 省略 <span class="token operator">...</span><br /><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> MySubscribe<span class="token punctuation">;</span></code></pre>
<p>那假設客戶的需求變成,除了要搜集「取消訂閱按鈕」被點擊的次數以外,還需要搜集用戶取消訂閱的是什麼樣的類別。那我們就只需要在事件中加入我們需要帶入的 event payload 就可以了。</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// 自訂事件範例:搜集取消訂閱次數 + 類別</span><br /><br /><span class="token keyword">const</span> <span class="token function-variable function">MySubscribe</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span><br /> <span class="token operator">...</span> 省略 <span class="token operator">...</span><br /> <span class="token operator"><</span>button <br /> onClick<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">gtag</span><span class="token punctuation">(</span><span class="token string">'event'</span><span class="token punctuation">,</span> <span class="token string">'unsubscribe'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">category</span><span class="token operator">:</span> currentCategory <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">}</span><br /> <span class="token operator">></span><br /> 取消訂閱<br /> <span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span><br /> <span class="token operator">...</span> 省略 <span class="token operator">...</span><br /><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">default</span> MySubscribe<span class="token punctuation">;</span></code></pre>
<p>從上面的例子當中,大家應該可以很容易理解,<code>自訂事件</code> 該如何使用,以及使用時如何依據不同的需求來埋設對應的程式碼。<br />
也就是說,根據客戶的需求,我們可以去分析,然後自己定義我們要在什麼時機點?什麼元件上?發送什麼樣的內容?這就是 <code>自訂事件</code>。</p>
<p>但 <code>自訂事件</code> 不是我們把事件埋進專案的程式碼,GA 後台就自己生成報表的。所以會需要 GA 用戶在後台也進行設定,才能把資料顯示在報表上面。就好像我們使用 Redux 發送新的 action,reducer 要加入對應的設定,state 才會發生改變,是一樣的意思。</p>
<p>上面介紹的 4 種事件分類當中,不論是哪一種類型的事件,工作團隊的分工關係都是不變的,前端專心處理 event 的觸發(搜集),而 GA 後台就專心處理資料的分析,彼此之間要互相溝通,協調好是用什麼事件名稱(event name),有沒有帶事件資料(event payload),兩邊都需要把事情做好,後台才能出現正確的數據報表。</p>
<blockquote>
<p>備註:不論是哪一種事件設定,只要我們在程式碼裡面埋 code,通常都需要等約 1~2 天 GA 後台才同步更新,所以有時候看不到資料,不一定是事件沒有埋好,有時候要等它一段時間。</p>
</blockquote>
<h2 id="%E8%B3%87%E6%96%99%E5%88%86%E6%9E%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/ga-basic/#%E8%B3%87%E6%96%99%E5%88%86%E6%9E%90">#</a> 資料分析</h2>
<p>這邊簡單補充一下資料分析的部分,讓大家稍微有個感覺,當我們發送 GA API 把事件打到後台,後台會如何設定資料。</p>
<p>如果是 <code>自訂事件</code>,GA 後台會有一個可以「新建事件」的地方<br />
<img src="https://blog.errorbaker.tw/img/posts/xiang/ga-basic-06.png" alt="" /></p>
<p>如果我們要新增事件,可以透過類似判斷的方法,把事件加入上去,以剛剛「取消訂閱」的範例來說,我們透過 GA API 發送了一個 <code>gtag('event', 'unsubscribe')</code> 事件。在 GA 後台要新增事件時,就可以判斷 <code>event_name</code> 等於 <code>unsubscribe</code>,這樣這個 <code>自訂事件</code> 就被加入後台了。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/xiang/ga-basic-07.png" alt="" /></p>
<p><code>自訂事件</code> 加入後台以後,我們還需要把 event payload 給我們的資訊,顯示在報表上面。這個就可以透過很多方式來處理了,看是要拉什麼圖表,或者跟其他資料相互關聯...等等,總之 GA 後台在自訂資料分析時,能夠透過「下拉式選單」讓我們選取我們傳進來的事件資訊,包括 event payload 夾帶的訊息。</p>
<p>由於本篇文章主要針對開發者角度做 GA 入門介紹,所以 GA 後台的操作或資料分析著墨的比較淺,大家只需要有一個感覺,就是後台的設定介面,能拿到我們透過 GA API 發送夾帶的所有資訊,後台也可以透過邏輯處理(ex. event_name 等於 unsubscribe)等方式,自訂分析資料的規則。</p>
<h2 id="%E7%B8%BD%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/xiang/ga-basic/#%E7%B8%BD%E7%B5%90">#</a> 總結</h2>
<p>最開始提到 GA 的重點,就是資料統計與資料分析。所有的功能都環繞在搜集資料與資料處理這兩件事情上。</p>
<p>你會發現站在前端的立場,關注的點其實就在 GA API 這段,我們只要把發送事件給處理好,剩下的其實就是在 GA 後台能進行設定就可以了。用 Redux 來比喻,就是我們把在發送 action 處理好,剩下的就交給 reducer 去解決就好。</p>
<p>所以當我們了解 GA API 如何使用以後,基本上就只是使用情境上的區別,哪些情境對應哪些事件?需要埋入哪些參數...等等,都能透過同樣的流程來處理,確認要搜集哪些事件,透過 <a href="https://developers.google.com/analytics/devguides/collection/ga4/reference/events">官方文件</a> 查看是不是有提供好預設或建議事件,如果有的話文件建議的 API 格式是什麼?需要帶上哪些資訊?如果需要自訂事件,就要跟 PM 討論好 event name 要取什麼?要帶入的 event payload 有哪些?</p>
<p>這篇文章沒辦法看完以後讓大家馬上上手 GA,但建立了一些基礎的概念以後,大家在參考文件時,應該能夠更快速理解或尋找到自己需要的答案。</p>
<p>強大的 GA 功能伴隨著複雜的文件,需要團隊多個角色之間去分工,回到文章一開始的問題:「身為前端開發者,我們能如何提供協助?」希望這篇文章能給大家一些方向,幫助大家入門 GA。</p>
前端菜雞 - 轉職 1 年又 2 個月心得
2023-02-01T00:00:00Z
https://blog.errorbaker.tw/posts/benben/10-one-year-and-two-month-experience/
<!-- summary -->
<!-- 轉職滿一年 ... 又 2 個月了啦!這是大神的簽名唷! -->
<!-- summary -->
<p><strong>!這是一篇從廢物保全的轉職前端的心得,轉職 1 年又 2 個月,年薪破百的故事(?)可以當作充滿倖存者偏差的個版來看看就好,當然跟個各種大大相比,我真的還只是菜難,如果大大已是年薪百萬 or 資深工程師就直接跳過本文章吧。</strong></p>
<center>
<img src="https://hackmd.io/_uploads/SJOHgbO3j.jpg" alt="antfu" width="480" />
</center>
<blockquote>
<p>2022 Vue X Laravel Conf 上的 antfu 大神的簽名(第一個簽名啦)</p>
</blockquote>
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/10-one-year-and-two-month-experience/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<p>筆者要先道歉,筆者就是想下這種爛標題試試(什麼奇怪的惡趣味),看有多少人會點進來看 😂</p>
<p>如果你點進來了,I got you !</p>
<p>本來是滿一年時候,想寫一篇的,但那時候真的太忙了(好啦,我知道我每次都這樣說),所以啦,本來是 1 年心得的,結果又拖了 2 個月多(快 3 個月了),<strong>筆者這個人很老實的</strong>, 1 年 2 個月就是 1 年 2 個月,不會說什麼 1 年轉職心得,畢竟 2 個月也可以做很多事了,就像 3 秒鐘可以做什麼,你自己知道就好。</p>
<p>或是更正確來說:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> milliSeconds <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token string">'2021/11/15'</span><span class="token punctuation">)</span> <span class="token comment">// 筆者成為開發者第一天上班日</span><br /><span class="token keyword">const</span> year <span class="token operator">=</span> milliSeconds <span class="token operator">/</span> <span class="token number">1000</span> <span class="token operator">/</span> <span class="token number">60</span> <span class="token operator">/</span> <span class="token number">60</span> <span class="token operator">/</span> <span class="token number">24</span> <span class="token operator">/</span> <span class="token number">365</span><br />console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>year<span class="token punctuation">)</span><br /><span class="token comment">// 1.176578354578894 年</span></code></pre>
<p>筆者會盡量用白話一點來打這篇心得,預期新手、正在轉職亦或是非專業人員都能夠看得懂,並也期望能幫助到大家。另外要先說明如果文中有內容覺得傷到你的話,你可以停下來思考一下,這條路真的適合你嗎?或是無視即可,但總要有人出來說真話,被討厭的話也是沒辨法的。</p>
<p>如果還是比較喜歡 <strong>輕描淡寫又很成功的心得文</strong>,好像有種隨便一個人也可以輕易做到的錯覺,如果你覺得有幫助,那就多去看看吧!筆者也說了我是個很老實的人,全都是幻覺嚇不倒我的!</p>
<p>所以才會想說來寫個比較嚴肅一點的心得文,很多時候早點勸退反而是省時間,早點停損你可以做更重要的事,省下高昂的課程費用,可以去做你更喜歡做的事情,甚至你做得比寫程式更好,那也非常棒,寫程式絕對不是轉職唯一選擇!</p>
<p>轉職前的故事,可以參考:<a href="https://github.com/Lidemy/mentor-program-5th/issues/31">[MTR-05] 學生心得:Benben</a> ,這邊會著重在轉職完後的故事。</p>
<p>好的好的,我知道大家想知道怎麼樣才可以百萬年薪,但先讓我們看下去吧。</p>
<h2 id="%E6%88%91%E6%80%8E%E9%BA%BC%E6%95%A2"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/10-one-year-and-two-month-experience/#%E6%88%91%E6%80%8E%E9%BA%BC%E6%95%A2">#</a> 我怎麼敢</h2>
<ul>
<li>不滿三個月就跳槽?</li>
</ul>
<p>筆者第一間上班的的公司是某數位設計公司 K,那時因應「元宇宙」風潮而跟上的,想說未來蠻有發展的,進去後才發現,我錯了!主管/PM 根本不懂技術、常態加黑班等,不過也不能怪誰,反而學到了一課,看到關鍵字,如:元宇宙、虛擬貨幣、NFT 等等,都要小心,很可能只是拿某個新名詞蹭流量,要好好研究公司背景。</p>
<p>同時也邊做邊觀察,<strong>試用期不是只公司在看你,你也在觀察公司</strong>,真的覺得不行就趕快換一間吧,不要浪費時間,也不要覺得沒做滿一年履歷不好看什麼的,舉個例子:一個在同一間公司待五年的,是好的履歷嗎?還是只是能力不足跳不走,所以只能待著?你不知道,只有跟他共事才知道。年資不滿一年就換真的沒什麼,想想你有多少個一年可以這樣試?</p>
<p>後來到了還不錯的小公司 R,因為想離開 K 公司,所以就打開了 104 ,那時候也是過年前吧,也有很多面試邀約,因為有了前一次面試經驗,所以看起來很雷的公司就先篩掉一波了,跟 R 公司的 CTO 聊蠻多的,一聊就是 2 個小時,也是馬上就拿到 Offer ,Offer 也還不錯,也沒什麼好猶預的就過去了。</p>
<ul>
<li>半年又請特休去面試?</li>
</ul>
<p>筆者個人也是 <strong>半年左右會整理一下自己履歷</strong> 的人,也不用多說,應該很多大大都這樣說過,定期去看看機會的同時,也看看自己還欠缺什麼,或是自己的價值在什麼水平,這都是對你有幫助的,何樂不為?</p>
<p>擔心別人眼光?免了吧,真的有多少離職後還會聯絡的朋友,何必被一個眼光耽誤了你的未來呢?但是也不要「太誇張」剛進去就一直想換工作,一直看工作機會、一直去面試,也就是俗稱的「換換病」,反而不充實自己,長期下來,只是變得「很會面試」擺了,筆者是建議 3 ~ 6 個月左右的頻率就好,當然你已經在很頂尖的公司或是高階主管就不一定了。</p>
<ul>
<li>小總結</li>
</ul>
<p>其實筆者也算是蠻幸運的,後來 R 公司的 CTO 真的是個好主管,技術很深很廣,寫 App 兼 PM ,不會亂開需求、不會亂開沒意義的會,下班後完全不會吵你,群組安靜的跟沒人一樣,對下面的人也是蠻好的,但是他也很忙就是了,小公司嘛!正常。</p>
<p>也不是說什麼我要走了就不做事,我到離職前一天都還上好上滿 8 小時,寫文件,整理之前的 code ;上班期間約一年,沒有遲到早退過,都是全心全意在為公司寫扣,任務完成了,還想著有哪些地方可以優化的。可以說是對這份工作問心無愧,我覺得這樣就夠了,再繼續做下去,我可能會更難抽離吧!</p>
<p>我會再離開 R 公司也是遲早的事,我的工作量幾乎是另一個前端的 2 ~ 3 倍,再說一次 <strong>筆者是個很老實的人</strong>,當然 CTO 真的太忙了,可能真的沒注意到,提完離職那天,下午跟 CTO 也聊了很多想法,他問我之後有沒有想當主管,我馬上說有!(不然我是為了什麼一直充實自己?我想成為主管或資深,希望啦),他只簡單說了:是這樣的話多做一點是好事,他的工作量換算下來應該可以抵 5 個人,我了不起就抵 2 個(打不了 10 個 QQ),我不知道我在他心中是不是有達到他的要求,我也不曉得我的選擇是不是正確的,但天曉得呢?</p>
<p>那天下班從內湖騎車回家上了麥帥一橋,夜空很清澈,旁邊的 101 依然亮著微微的藍光,慢慢地,閃著閃著。</p>
<h2 id="%E6%88%91%E6%86%91%E4%BB%80%E9%BA%BC"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/10-one-year-and-two-month-experience/#%E6%88%91%E6%86%91%E4%BB%80%E9%BA%BC">#</a> 我憑什麼</h2>
<ul>
<li>作品集</li>
</ul>
<p>很多前端開發者,非常討厭整理作品集,我也是!</p>
<p>每當換一個平台,履歷、作品集全都從新整理一次,名字、生日、地址,還有最麻煩的自傳,想到就很累,所以我非常討厭一直整理這些東西,在軟體開發的領域中,古有云「Don't repeat yourself」,重複的事做一次就好了,而且一直寫自己的資料、自傳,某種程度上也算 repeat yourself (笑)</p>
<p>身為一個前端工程師,自己做個網站,應該算是稀鬆平常的事了,所以幫自己的作品集好好整理一下,做個列表呈現一下作品集,也沒什麼困難吧?但這時候「很有熱情」的前端開發者又不見了,每個口口聲聲都說對前端很有熱情,但 github 上怎麼空空(但也不排除都是私人專案或是在其他平台啦)的呢?所以啊,跟筆者一樣當個老實的人吧,沒有的話,也寧願不要說有,有熱情的話自然就會呈現出來了,不需要特別裝。</p>
<p>做完履歷、作品集網站後,加上筆者有一些 <strong>個人原創</strong> 、<strong>合作</strong> 的作品集,認真要講每一個都可以講個一小時起吧,也呼喻大家 <strong>請不要抄作品集</strong>,這樣的話,履歷的回復率就會高很多,筆者把密訣都交給你了,別說筆者私藏啦 XD</p>
<ul>
<li>演算法</li>
</ul>
<p>很多前端開發者,非常討厭刷題,我也是!</p>
<p>首先,我討厭的是「刷題」,但我喜歡「解題」的感覺,主要用 JavaScript, TypeScript 刷,也是前端的主要語言,可以一起熟悉語言跟邏輯,現在語言跟語言的差異越來越小了(當然還是有分動態、靜態;強型別、弱型別),別人做得到的, JavaScript 也做得到(等等!沒有要戰語言),拿來刷題也沒什麼不可以,不用硬要跟別人一樣用 Python, C++ 等等(真的要選的話選 C++ 吧)</p>
<p>我覺得刷題題練的不是各個程式語言,而是練邏輯思考的,其他底層實現只是一些細節,如:宣告一個變數用要用 let, const, int, bool, char* 之類的,就用自己熟悉的語言即可,之後再來用第二、第三語言挑戰。</p>
<p>今年 2023/01/15 去了 <a href="https://www.accupass.com/event/2211140558381956060137">2023 Yourator 數位職涯博覽會</a> ,筆者到處問前端面試會考 leetcode 嗎?得到的答案大多是 <strong>不考或是項多考個 easy</strong> ,真的不喜歡刷題就不要刷也 OK ,但相對的,就要拿出對應的實作經驗來證明你的實力就是了。</p>
<ul>
<li>Vim</li>
</ul>
<p>很多前端開發者,非常討厭使用 Vim,我也是!</p>
<p>筆者的討厭是指剛學 Vim 的時候,連個退出 (<code>:q</code>) 都卡半天,移動只會一格一格移動(<code>hjkl</code>),輸入只會 <code>i</code>,保存退出只會 (<code>:wq</code>) ,但 基本上這樣就可以完成 <strong>所有的任務</strong> 了,只是 ... 花的時間長短。</p>
<p>直到看到各種大神的神操作,苦練了一番,筆者真的是從上面那樣的狀態開始使用 Vim ,慢慢寫扣開始的,後來終於能在 vim 中飛來飛去、使用各種腳本,操作算是行雲流水了,用上古神器寫前端就是潮到出水(X</p>
<p>學習 vim 看似沒什麼太大的幫助,舉個列子:vim 中<code>:q</code> 表示 quit 的意思,很多軟體也尊守這個傳統,如:<code>git log</code> 下輸入 <code>q</code> 也是退出的意思。還有像是遠端 server 進去預設就是使用 vim 來操作,這時候,你會感謝以前的你已經把 vim 學好了!</p>
<p>常在 twitter 或是社團上,有人戰編輯器的時候,總會看到 <strong>real pro use vim</strong> 這句話,表示專業的開發者使用 vim 開發,目前前端應該也沒什麼人在使用 vim 開發(等等!沒有要戰編輯器),好用的工具也越來越多,也可以做到 vim 做得到的事,所以要不要學又是見仁見智了。</p>
<p>總之,可以 <strong>提高你的產能的</strong> 都是好東西,或是可以試試 Emacs 或是 IntelliJ 都是不錯的選擇,但這兩個工具部分筆者就沒有涉略了。</p>
<ul>
<li>小總結</li>
</ul>
<p>有沒有發現 <strong>當別人討厭什麼,你就去做什麼</strong>,這將會是你的優勢,當然你也要付出心力去學習,優秀是什麼?就是 <strong>使別人變得平傭的能力</strong>,筆者同意,你可能會覺得筆者很 "捲",但這也是沒辨法,很抱歉世界就是這麼運作的,你會的技能越稀有且有需求,就是越是高級人才。或是想一下,Junior 跟 Senior 的差別是什麼?為什麼年資差不多,Senior 是別人而不是你?</p>
<p>等等!你說你也討厭切板?也沒關係啊,那去寫後端 SQL 如何?</p>
<p>有一個工程師笑話是這麼說的:前端不會切板、後端不會 SQL。</p>
<p>當然也很多工具們把這些事「包好」了,舉個不是很恰當的例子:Bootstrap 取代切板;ORM 取代 SQL。但是當你要改動 Bootstrap 時卻改不動;寫一個簡單 CRUD 的 SQL 時寫不出來。這時候,你就會明白了。不然真的以為工程師每天複製/貼上、喝喝咖啡就有錢領了嗎?哪裡來的幻夢泡泡,筆者先幫你戳破,還是如果真的有這種工作的話,請馬上跟筆者說還有沒有缺人(咦?)</p>
<h2 id="%E6%88%91%E5%81%9A%E4%BA%86%E4%BB%80%E9%BA%BC"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/10-one-year-and-two-month-experience/#%E6%88%91%E5%81%9A%E4%BA%86%E4%BB%80%E9%BA%BC">#</a> 我做了什麼</h2>
<ul>
<li>繼續的追 <strong>新技術</strong>,補你所欠缺的 <strong>舊技術</strong></li>
</ul>
<p>軟體這一個領域,唯一不變的就是一直在變,這應該也講到爛掉了,尤其是前端,框架、工具變化真的太快了!如果不跟上就等著被淘汰,React 都出到 18 了,只會 jQuery 夠嗎?Vite 打包快得跟閃電一樣、Vue 3 也很香,不學一下嗎?</p>
<p>追新技術對開發者來說也是家常便飯了,那 ... 舊技術呢?<strong>計算機概論、資料結構、演算法</strong>,有趣的是,這些都是萬年不變的舊技術,反而沒什麼人要學,比較起來這應該是 CP 值最高的技能,結果大家都在熱哀新技術,甚至在戰新技術哪個好,有時間戰的話,不如拿去提升自我,這些技能都會的話又何必戰呢?</p>
<p>人家資工本科訓練了 4 年才有的功力,又怎麼會覺得 3 ~ 6 個月就可以追上?還有時間的話,請好好的補完這些必要的技術知識,轉職的光環會有用完的一天的。未來的要跳槽的你,會感謝以前的你的(好饒舌),眼光要放長遠啊。</p>
<ul>
<li>讀各種開發者經典書籍(Clean Code ... 等)</li>
</ul>
<p>轉職後,當然也沒有閒著,筆者通常上班前會閱讀書籍,因為也不太吃早餐,09:00 上班的話,筆者通常會 08:50 到公司,然後看一下書,差不多一天看個 10 ~ 15 分鐘,筆者的座位上一定會放 2 ~ 3 書,累計到現在讀了:</p>
<ol>
<li><a href="https://www.tenlong.com.tw/products/9789867889188">人月神話:軟體專案管理之道 | 天瓏網路書店</a></li>
<li><a href="https://www.tenlong.com.tw/products/9789864345687">重新認識 Vue.js:008 天絕對看不完的 Vue.js 3 指南 | 天瓏網路書店</a></li>
<li><a href="https://www.tenlong.com.tw/products/9789864348954">你所不知道的必學前端 Debug 技巧:即學即用!讓你 Debug 不求人 | 天瓏網路書店</a></li>
<li><a href="https://www.tenlong.com.tw/products/9789862017050">無瑕的程式碼-敏捷軟體開發技巧守則 | 天瓏網路書店</a></li>
<li><a href="https://www.tenlong.com.tw/products/9789862017883">無瑕的程式碼 番外篇-專業程式設計師的生存之道 | 天瓏網路書店</a></li>
<li><a href="https://www.tenlong.com.tw/products/9789865025526">提升程式設計師的面試力|189 道面試題目與解答 | 天瓏網路書店</a> (這本是買來供俸的,大概只讀了封面)</li>
</ol>
<p>有時候也會讀一點非程式相關的,再告訴大家一個小密訣,現在人不太讀書的,所以你讀了就贏一半了(咦?),有 <strong>好的讀書習慣在各領域都有優勢的</strong>。</p>
<blockquote>
<p>好奇我還讀哪些書的話,可以參考:<a href="https://hackmd.io/@benben6515/reading-list">benben: reading-list | HackMD</a></p>
</blockquote>
<ul>
<li>追縱大神</li>
</ul>
<p>在轉職前,就偶爾會看一些程式的文章,筆者覺得最關鍵的一篇文章是 Huli 大神的:<a href="https://buzzorange.com/techorange/2016/04/13/self-study-program-with-datastructure-and-algorithm/">程式自學十年心得</a>,看到這一篇大約是在 2018 年尾左右吧,那時筆者還是個保全,想說原來自學也要花個十年啊!果然不容易啊!咦?這個人是誰啊?名字好特別喔?胡立?難道是網名?還是可能跟胡瓜有關係?</p>
<blockquote>
<p>延伸閱讀:<a href="https://buzzorange.com/techorange/2016/04/13/self-study-program-with-datastructure-and-algorithm/">Huli 程式自學十年心得 | TechOrange 科技報橘</a></p>
</blockquote>
<p>筆者的工作需要日夜輪班,那天正好職晚班,反正離下班時間還長,天也還沒亮(只要跟航空有關係的職業十之八九都要輪班,想一下飛機是不是 24 小時在飛的?是,那是不是 24 小時都要有人在?那就對了),去看一下這個人的 Blog 好了,不看還好,一看不得了,這個人跟我差不多年紀、大學沒畢業,已經是資深工程師了!</p>
<p>真的很感謝胡立大大,他把他的故事分享出來了,正如筆者正在分享自己的故事,讓我覺得這條路是可行的,所以筆者當下 <strong>做了一個選擇</strong>,並且 <strong>努力地執行</strong> 著。筆者大學唸的是私立的應用數學系,自認數學還行(學測數學滿級分,不要問我為什麼這樣還要讀私立大學),轉職也應該還算 OK ,就一路邊上班慢慢自學,反正我早就無法分辨白天或黑夜,直到後來參加全職學習,想說再去加強一下,就一路加強到現在了。</p>
<p>後來工作後,主要使用 Vue 框架,文件看著看著,也發現了 Vue 的核心成員 - <strong>antfu</strong> 大神,Github 排行前 100 ,其他就不多說了,真的很強又低調,開源了很多工具,有的你可能就有用過,也分享了很多文章、資源,全都開源的。</p>
<p>2022 的 Vue X Laravel Conf 筆者也有去聽 Vue 的議程,筆者特別去找 antfu 簽名,他還說他以前沒有簽過名的,但還是幫我簽了(羞),Yes!就這樣 <strong>史上第一個 antfu 的簽名</strong> Get!大概就是這種追大神當追星的感覺吧!</p>
<blockquote>
<p>了解更多:<a href="https://laravelconf.tw/">Laravel x Vue Conf Taiwan 2022</a></p>
</blockquote>
<p>總之,找幾個在自己領域的大神(或是身邊的人也都可以)追縱一下,會更有動力的,就像有 <strong>一盞明燈在為你指路</strong>,當你迷惘時、想放棄時,它依然那亮著。</p>
<ul>
<li>小總結</li>
</ul>
<p>所以我跟其他前端有什麼不一樣?也沒什麼太大的差別,上班一樣切切版、接接 Api 、寫寫文件,我算資深嗎?到底 <strong>資深的定義</strong> 是什麼呢?我也不知道,但好像也不是這麼重要了,筆者認為 <strong>能夠一直進步才是最重要的</strong>,何必居拘泥在一個職稱呢?</p>
<h2 id="%E6%88%91%E9%96%8B%E5%BF%83%E5%97%8E"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/10-one-year-and-two-month-experience/#%E6%88%91%E9%96%8B%E5%BF%83%E5%97%8E">#</a> 我開心嗎</h2>
<ul>
<li>當開發者是個很棒的工作嗎?</li>
</ul>
<p>老實說,沒什麼特別的感覺,<strong>開發是份工作;保全也是份工作</strong>。工作嘛,我覺得並無高下之分,<strong>有沒有很認真的保全?</strong> 一定有啊,像 R 公司樓下的警衛大哥人就很好,看到每個人都還會問好,筆者就時不時會請他飲料;那 <strong>有沒有很混的開發者?</strong> 不用跟我說,一定很多人非常有感覺。</p>
<p>重點是你怎麼做這份工作?</p>
<p>以前筆者在某航空警備(aka 保全)上班時,也看過很多人嘴臉,有很跩的機長,進公司時停車證愛拿不拿的,好像公司是他私人停車場;也有很有禮貌的飛行教官(負責教機長的,是可以把機長幹到飛起來,物理上飛起來那種),常常關心我們,天冷了要多穿一點,有時也會請飲料給我們喝;也有來面試的空服員,看起來都人模人樣,但有的對核資料時愛拿不拿的(其實我們警備大多都跟協理、副總很好,也有副總跟我們說不聽話的,編號直接登記給他們,直接叫他不用來面試了,其實我們就是隱藏的面試第一關啦),好,這邊扯遠了,回來,筆者也感謝這份工作,也許是因為有這份工作,筆者才這麼拼命的,也可能本來就習慣這麼拼了,筆者也不知道。</p>
<p>這邊再補充一個,筆者通常不會稱自己是工程師,因為這個詞已經被濫用了,應該多少遇過那種「我也是工程師欸」的那種朋友,結果聊一聊才發現他一個程式語言都不會寫,也不是說鄙視的意思(真要說的話,Web 前端可能才是鄙視鏈的底層 QQ),只是再聊下去真的很尬好嗎?還要想怎樣聊才可以讓你繼續裝逼又不戳破你,不曉得大家有沒有這種經驗呢?聊天說話也很門藝術呢。</p>
<ul>
<li>現在的工作又是如何呢?</li>
</ul>
<p>但現在的工作是真的很硬,筆者可能就很犯賤,明知山有虎,偏往虎山行,看看到底會有多硬,吃苦當吃補的概念,趁還年輕多一點練習的機會,或許搞不好之後公司起飛也不一定(一定是我的錯覺啦,錯覺)。</p>
<p>因為是全遠端,兼職上班第一天,主要管技術的全端就說:「你先 ping 一下 xxx.xx.xx.xx,看一下延遲是多少」</p>
<p>一般的 junior 前端應該是想說?蛤?拼?拼什麼?不要跟他拼拳,嘗試切他中路?</p>
<p>好在筆者還有讀過一點 <a href="https://linux.vbird.org/">鳥哥私房菜</a>,馬上就是打開 terminal 下指令,回復:「目前延遲約為 160 ms」</p>
<blockquote>
<p>延伸閱讀: <a href="https://linux.vbird.org/linux_server/centos6/0140networkcommand.php#ping">鳥哥私房菜 - 第五章、Linux 常用網路指令 ping</a></p>
</blockquote>
<p>全端就淡淡的回:「還行,那就這樣開發吧」</p>
<p>然後就吃著火鍋唱著歌(電影:讓子彈飛 的梗),直接分配任務,直接上工開發,就一路從 12 月(2022/12)到過年(2023/02)都在加班啦,一天差不多都 Coding 12 小時(正職加兼職)以上。</p>
<p>簡單列一下目前的時間軸:</p>
<pre class="language-markdown"><code class="language-markdown"><span class="token list punctuation">-</span> MTR05 = Lidemy 程式導師計劃第 5 期<br /><span class="token list punctuation">-</span> K = 公司 K<br /><span class="token list punctuation">-</span> R = 公司 R<br /><span class="token list punctuation">-</span> C = 公司 C<br /><br />2021/04 (MTR05) 開始全職學習<br />2021/10 (MTR05) 結業<br />2021/11 (K)正職 K 公司,轉職上工第一天<br />2022/02 (R)正職 R 公司、離開 K 公司<br />2022/12 (R、C)正職 R 公司、兼職 C 公司<br />2023/02 (C)正職 C 公司、離開 R 公司<br />2023/03 (<= 現在在這)<br />...</code></pre>
<ul>
<li>所以我說那個年薪百萬呢?</li>
</ul>
<p>這不是來了嗎?</p>
<p>目前 C 公司的 Offer 薪資年薪為約:50000 紐幣(紐幣比台幣約 19:1),因為公司是掛在紐西蘭的,所以發紐幣,有人可能會想說,哇!外商欸!不要以為公司不在台灣就算外商,老闆跟核心成員都是大陸人,也有印度的同事,就只是這樣的一間公司。</p>
<p>簡單換算一下紐幣,差不多就是 95 萬/年 台幣了,也還行,依對應的工作量來看的話,真的還行。</p>
<p>修旦幾勒!這樣也沒有百萬啊!還說自己是老實人?你騙人!</p>
<p>讓子彈飛一會兒,仔細看上頁的時間軸,筆者其實從去年(2022/12)就開始在 C 公司「兼職」了 2 個月,這邊的兼職是指 <strong>一周花 20 小</strong> (就是半個正職的概念)的工作時間計算,所以等於是 <strong>多了一個月</strong> 的薪水。</p>
<p>所以 95 萬再加上約 7 萬的兼職薪水了就是 102 萬了,只是不知道可不可以做滿一年就是了(咦?),就不用再說一次了吧,<strong>筆者這個人很老實的</strong>(被打),有百萬就有百萬的啦。</p>
<p>差不多是醬啦,希望這篇不知道在寫沙小的心得有幫助到大家,我們下次見 ~</p>
<blockquote>
<p>下一篇之後應該會來寫一個 vim 的入門系列(for Beginners),但可能會是英文的,希望啦。</p>
</blockquote>
<h2 id="ref"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/10-one-year-and-two-month-experience/#ref">#</a> Ref</h2>
<ul>
<li><a href="https://linux.vbird.org/">鳥哥私房菜</a></li>
<li><a href="https://laravelconf.tw/">Laravel x Vue Conf Taiwan 2022</a></li>
<li><a href="https://www.tenlong.com.tw/products/9789867889188">人月神話:軟體專案管理之道 | 天瓏網路書店</a></li>
<li><a href="https://www.tenlong.com.tw/products/9789864345687">重新認識 Vue.js:008 天絕對看不完的 Vue.js 3 指南 | 天瓏網路書店</a></li>
<li><a href="https://www.tenlong.com.tw/products/9789864348954">你所不知道的必學前端 Debug 技巧:即學即用!讓你 Debug 不求人 | 天瓏網路書店</a></li>
<li><a href="https://www.tenlong.com.tw/products/9789862017050">無瑕的程式碼-敏捷軟體開發技巧守則 | 天瓏網路書店</a></li>
<li><a href="https://www.tenlong.com.tw/products/9789862017883">無瑕的程式碼 番外篇-專業程式設計師的生存之道 | 天瓏網路書店</a></li>
<li><a href="https://www.tenlong.com.tw/products/9789865025526">提升程式設計師的面試力|189 道面試題目與解答 | 天瓏網路書店</a> (供奉用,大概只讀了封面)</li>
</ul>
<blockquote>
<p>免責聲名</p>
</blockquote>
<p>以上均為筆者自身經驗,難免小有主觀意見,供讀者們參考,也歡迎分享經驗交流。<br />
如果有錯誤的地方還請大大們指正,筆者會立刻修改,再次感謝大家!</p>
<p><a href="https://creativecommons.org/licenses/by/4.0/"><img src="https://i.creativecommons.org/l/by/4.0/88x31.png" alt="Creative Commons License" /></a></p>
<p>本著作係採用 <a href="https://creativecommons.org/licenses/by/4.0/">創用 CC 姓名標示 4.0 國際授權條款</a> 授權。您可以在 <a href="https://benben.me/">benben.me</a> 找到我。</p>
<p>This work is licensed under a <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>. You can find me at <a href="https://benben.me/">benben.me</a></p>
改善 CI/CD 中的 jest-coverage-report
2023-03-26T00:00:00Z
https://blog.errorbaker.tw/posts/cwc329/improve-jest-coverage-report/
<h1 id="%E6%94%B9%E5%96%84-ci%2Fcd-%E4%B8%AD%E7%9A%84-jest-coverage-report"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/improve-jest-coverage-report/#%E6%94%B9%E5%96%84-ci%2Fcd-%E4%B8%AD%E7%9A%84-jest-coverage-report">#</a> 改善 CI/CD 中的 jest-coverage-report</h1>
<!-- summary -->
<p>兩個月前的 1 on 1 中不知死活的我跟 lead 說最近在開發上感覺任務沒有突破,感覺沒什麼挑戰。<br />
於是 lead 給我一個題目:「我們 CI/CD 中的 test coverage report 跑的有點太久了,你有什麼除了加大機器以外的加速方法嗎?我們下個月討論一下。」</p>
<!-- summary -->
<h2 id="%E7%A2%B0%E5%88%B0%E7%9A%84%E5%95%8F%E9%A1%8C"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/improve-jest-coverage-report/#%E7%A2%B0%E5%88%B0%E7%9A%84%E5%95%8F%E9%A1%8C">#</a> 碰到的問題</h2>
<p>首先太慢到底是多慢呢?由於我們公司所有產品幾乎都在同一個 monorepo 中,大家都用 typescript 開發以及 jest 作測試框架。</p>
<p>我先盤點一下目前大概有 600 個 test suites,總共大概有 6000 個單元測試,這個量級其實還好,在 M1 Macbook pro 13" 上大概執行 5 分鐘。</p>
<p>不過同樣的東西放到 CI/CD 上面就要跑 40 分鐘起跳,這個速度嚴重拖慢開發的速度以及節奏,有的時候 PR approved 想說應該可以 merge 了,才發現 test coverage report 沒有過,在 local 改完之後推上去又要再等 40 分鐘。</p>
<p>更別說有的時候會遇到 github 或者 yarn 伺服器失靈導致 job fail,這個時候</p>
<h2 id="%E6%88%91%E7%9A%84%E8%A7%A3%E6%B3%95"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/improve-jest-coverage-report/#%E6%88%91%E7%9A%84%E8%A7%A3%E6%B3%95">#</a> 我的解法</h2>
<p>首先我先從 jest 官方文件下手,在 <a href="https://jestjs.io/docs/troubleshooting#tests-are-extremely-slow-on-docker-andor-continuous-integration-ci-server">troubleshooting</a> 就有人曾經遇到在 CI/CD 環境執行過慢的狀況。</p>
<p>這是因為 CI/CD 的機器通常沒有開發用的電腦好,核心數通常不多,有的甚至只有雙核,官方有的 cli option <code>--runInBand</code> 可以用。</p>
<p>根據說明,jest 預設會平行執行測試並且自動分配電腦資源,會預留一個 core 作為 master,其他所有 core 則為 worker。</p>
<p>而 <code>--runInBand</code> 其實就是取消平行執行,改成一次執行一個測試,這在資源比較小的機器上可以大幅增加執行速度,不過詳細原因在文件以及討論中都沒有提到。</p>
<p>看到這個我想:「竟然這麼簡單就解決了!?」我立刻發了個 PR 更新 test coverage report 的設定檔,用了一個 POC 的 workflow 測試,結果執行時間馬上減少一半。</p>
<p>我興高采烈的跟 lead 報告,lead 也 approve PR,整個流程大概只花了我一個禮拜,而且都是用開發之餘的瑣碎時間,一切實在是太順利了!</p>
<h2 id="nice-try"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/improve-jest-coverage-report/#nice-try">#</a> Nice Try</h2>
<p>我的 workflow 進主線之後兩天,我比較了一下前後的差異,問題出現了,test coverage report 的執行時間根本沒有減少。</p>
<p>我萬般不解,我之前的 POC 的確執行時間只有一半,而且我跑了五次結果都差不多,不可能出現這樣的事情啊。</p>
<p>我再 google 一個下午 jest 優化,發現沒有其他適合的解法,這時我想如果無法從 jest 下手,那是否可以從 github action 找看看有沒有機會呢?</p>
<p>我們的 test coverage report 其實是使用 github market 的 <a href="https://github.com/marketplace/actions/jest-coverage-report">Jest coverage report</a>,我直接去看他們的<a href="https://github.com/ArtiomTr/jest-coverage-report-action">文件以及原始碼</a>,總算讓我發現為何我的 action 執行時間沒有減少。</p>
<p>這個 action 可以提供這個 PR 的 HEAD 相較於 base 其測試涵蓋率變化,而為了要做出比較 action 會需要 HEAD 以及 base 的測試報告。</p>
<p>這就是問題所在,兩份測試報告就會需要跑兩次測試,但是我的 POC 並不是在 PR 的條件下跑,所以只有執行 HEAD 的 coverage report,所以這樣測試當然不準確。</p>
<p>而 <code>--runInBand</code> 這個 flag 之所以沒有幫助是因為目前所用的 runner 是雙核心的機器,在 jest 預設下保留一個 core 作為 master,也就只剩一個 worker,有沒有這個 flag 都是一樣的結果。</p>
<h2 id="%E9%87%8D%E6%96%B0%E4%BE%86%E9%81%8E"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/improve-jest-coverage-report/#%E9%87%8D%E6%96%B0%E4%BE%86%E9%81%8E">#</a> 重新來過</h2>
<p>第一次嘗試失敗了,我得再想出一個解法。我把腦筋動到 Jest coverage report 這個 GitHub action 上面。</p>
<p>我翻閱文件,發現他有提供一個選項 <a href="https://github.com/ArtiomTr/jest-coverage-report-action#use-existing-test-reports">base-coverage-report</a>,用這個選項可以指定 base branch 的 coverage report 在哪裡,從而跳過執行 base branch 的測試。</p>
<p>原本我想說要不要使用 git hook 的方式,在 push 之前強制大家都要跑 test,不過馬上就被否決,這樣等於每次推 code 都要五分鐘以上,這無疑是阻塞開發流程。</p>
<p>這時一個想法在我腦中閃過:目前 test coverage report 每次只要推 code 上去都會跑,但是其實 base branch 幾乎沒有變,如果我能保留 base branch 的 coverage report,這樣不就可以省下一半的時間了?</p>
<p>於是我參考了之前 lead 在 CI/CD 中使用 yarn cache 的方式,在我們的 test coverage report 的 workflow 中加入一個 job,這個 job 就是先去找有沒有 base branch coverage report 的快取,如果有那就直接抓下來,沒有的話就生成一份,然後存成快取。</p>
<p>這次模擬我除了改動 workflow 的設定檔,同時改動了一些 test file 去觸發 test coverage report。</p>
<p>結果如我所料,當 base branch 沒有已經建立的 coverage report 時,整個的執行時間跟之前差不多,大概是 40 分鐘;不過當第二次執行的時候,整體時間減少一半,只要 20 分鐘左右就完成。</p>
<p>在新的 workflow 進入開發主現後,我觀察三天,執行時間明顯下降,這階段的任務算是達成了</p>
<p>但是有個小問題,目前的方法,當主線有更新時並不會自動產生新的 coverage report 快取,而是要等 PR 的 test coverage report 觸發才會產生。這樣會導致當新的 base coverage report 快取還沒有建立的時候,在短時間內很多 PR 都有更新,這時這些 PR 產生 base coverage report 的 job 都會被觸發,不過這些 job 都是重工。</p>
<h2 id="%E5%B0%8F%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/cwc329/improve-jest-coverage-report/#%E5%B0%8F%E7%B5%90">#</a> 小結</h2>
<p>上面這些弄完也差不多到了下一次 1 on 1,lead 對著上個月的 memo 說:「這次你算是有達成一些東西了,雖然和我預想的解法不太一樣。」說話的同時 lead 點開 <a href="https://swc.rs/">swc</a> 的網站。「你知道這個嗎?」</p>
<p>我回答:「這個我知道,但是我那個時候並沒有想到這個解法,同時也沒有意識到有這個進路可以想。」</p>
<p>lead 的思路是 jest 原本是用 babel 作語法轉換,而 babel 已經知道有效能問題,swc 則是這個效能問題的一個解決方法。這個解法能達到的<a href="https://www.jameslmilner.com/posts/speeding-up-typescript-jest-tests/">效果</a>應該會比我的好,而且是更大範圍更根本的解決方法。</p>
<p>而我沒有意識到主要是因為我其實對於 jest 的底層實作不熟悉,我只能從我知道的工具下手。lead 對於我使用不同的進路表示這就是軟體工程,同樣的目標可以用不同的方式達成。</p>
<p>接著 lead 把 swc 放進這次的 memo,我大概知道我之後要做什麼事情了。</p>
Vim for beginners
2023-05-07T00:00:00Z
https://blog.errorbaker.tw/posts/benben/11-vim-for-beginners/
<!-- summary -->
<!-- Some people don't want to learn vim. Some people want to learn vim, but ... -->
<!-- summary -->
<p><strong>! This is an article about Vim for beginners. If you're a Pro with vim already, you are good to go now</strong> :)</p>
<center>
<img src="https://hackmd.io/_uploads/BkbJbumEh.gif" alt="vim-demo" class="post-image-width" />
</center>
<blockquote>
<p>Vim demo by myself</p>
</blockquote>
<p>You can see the screen magically moving and my mouse don't move any millimeter. That's Vim's power.</p>
<h2 id="0-preface"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/11-vim-for-beginners/#0-preface">#</a> 0 Preface</h2>
<p>Hi, nice to see you guys again!</p>
<p>This time, I want to talk about <strong>Vim</strong>. I will introduce Vim, but it does not include boring Vim history.</p>
<p>You don't need a mouse if you use Vim like Pro.</p>
<p>You might wonder if that is true. I would say it makes sense.</p>
<p>Let's dive in!</p>
<h2 id="1-sprit"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/11-vim-for-beginners/#1-sprit">#</a> 1 Sprit</h2>
<p>First thing first, what is Vim? Vim is an editor, no doubt. But it is not just an editor; it is <strong>almost everywhere</strong>. As a developer, you likely use "Git" to control source code or some remote machine whose editor is Vim by default.</p>
<p>Most people aren't aware of Vim's power; that is a pity. That is the same as me before, so don't worry about it. I was first aware of Vim's influence in some Tech YouTube. The first time I saw it, I was stung by Vim's power.</p>
<p>Also, Vim's spirit is in a lot of tools and plugins. For example, I wrote this article using the Vim plugin in VScode. I also use Vim's Chrome plugin(Vimium) to browse the web. Vim is everywhere.</p>
<h2 id="2-keys-to-escape-from-vim"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/11-vim-for-beginners/#2-keys-to-escape-from-vim">#</a> 2 keys to escape from Vim</h2>
<p>If you know how to escape from Vim, it's just <code>:q</code> and then <code>Enter</code>. You are good to go now. This command <code>:q</code> is most famous in Vim. There are a lot of memes about these two keys.</p>
<p>Additionally, if you want to quit with save, you can input <code>:qw</code> and then <code>Enter.</code> Alternately, you can input <code>:x</code> then <code>Enter</code> or press <code>ZZ</code> for quit, <code>ZQ</code> for save, and quit. Cool, now you can use these methods as you want. Vim is very fun.</p>
<p>The minimum knowledge of Vim are:</p>
<ol>
<li>How to input text</li>
<li>How to save text</li>
</ol>
<p>These two key points are the first things of <strong>any editor</strong>. Let us recall how we got started to edit text files. I suppose most of us probably use the built-in text editor to edit <code>.txt</code> file for the first time.</p>
<ul>
<li>How to input text in a built-in editor?</li>
</ul>
<blockquote>
<p>It is like just inputting any text you want.</p>
</blockquote>
<ul>
<li>How to save text in a built-in editor?</li>
</ul>
<blockquote>
<p>Use <code>Ctrl/Cmd + S</code>, then close <code>X</code> button with your mouse—no big deal.</p>
</blockquote>
<p>There you are, the same two key points! But in Vim, it is just a little different. Believe it or not, Vim provides <strong>richer and more fun</strong> features than a regular built-in editor.</p>
<h2 id="3-modes-of-vim"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/11-vim-for-beginners/#3-modes-of-vim">#</a> 3 Modes of Vim</h2>
<ol>
<li>Normal mode</li>
<li>Input mode</li>
<li>Select mode</li>
</ol>
<p>I suggest beginners know about <strong>normal mode</strong> and <strong>input mode</strong> at first, and it's enough.</p>
<p>When we first enter Vim world, it is always at <strong>normal mode</strong> by default (if you don't set your <code>.vimrc</code> file or stuff). In this mode, we can NOT input text <strong>except</strong> we push some specific keys. It is essential, you should know. In the past, I learned about this from the Internet as well. But I realized: "You can not type anything, " meaning you might push some specific keys intentionally. So, I just randomly pushed my keyboard. You guess what? Yeah, I changed some of my configured files, which I worked on.</p>
<p>I prefer to regard normal mode as "navigate mode" because you can look around in the file or document with some specific keys. Meanwhile, some keys can switch to other modes with different features as well.</p>
<blockquote>
<p>Fun fact: There is also an "easy mode" in Vim. Open a file with a command like this: <code>vim -y file.md</code>, then you will open a file with easy mode, which is like Nano editor. That means if you want to use a simple editor like Nano editor, you don't even install Nano! Just use Vim easy mode. I don't encourage using this mode because it can not show Vim's power.</p>
</blockquote>
<h2 id="4-direction"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/11-vim-for-beginners/#4-direction">#</a> 4 direction</h2>
<p>There are four directions in Vim world:</p>
<ol>
<li>h (<strong>←</strong>)</li>
<li>j (<strong>↓</strong>)</li>
<li>k (<strong>↑</strong>)</li>
<li>l (<strong>→</strong>)</li>
</ol>
<p>Yes, that is a little weird! I known! But if you look at your keyboard, you will find these four keys just under your right figure one by one. It's no coincidence, and reducing the chance of using a mouse makes more sense.</p>
<p>Instead of using traditional direction four arrow keys, Vim uses <strong>h, j, k, and l</strong>. In this way, you can keep your figures in the center above the keyboard. You might wonder if it is a big deal. Yes, I had this thought before. But now, I will say <strong>Yes, it is important</strong> and I use 60% keyboard as well. Those do make me more focused on my work.</p>
<p>They're more efficient ways to navigate in Vim, such as <code>H</code>, <code>L</code>, <code>M</code>, <code>ctrl + d</code>, and <code>ctrl + r</code>. But if you are not familiar with h, j, k, l, <strong>focus on these four keys is good enough, and they can do every navigation</strong> you want in Vim. This is true, but the difference is how long the time taken.</p>
<h2 id="5-ways-to-input"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/11-vim-for-beginners/#5-ways-to-input">#</a> 5 ways to input</h2>
<p>There are five ways to input:</p>
<ol>
<li>a、A</li>
<li>i、I</li>
<li>o、O</li>
<li>c、C</li>
<li>r、R</li>
</ol>
<p>But those five keys' uppercase and lowercase are a little different. That means there are 10+ ways to input text in Vim.</p>
<p>That may be a little overwhelming, I know. Again, just know <code>i</code> is enough. You might think, why so many ways to input text? Taking <code>i</code>, and <code>a</code> for example, they are very similar, but one is <strong>insert(i)</strong> at the blank spot, and the other is <strong>append(a)</strong> at the blank spot.</p>
<p>Actually, I bet you may remember <code>i</code> and <code>a</code> to input some text in Vim. Not really? Repeat after me: <code>i, insert</code>, <code>a, append</code>. The truth is don't memorize these commands, but say it out.</p>
<h2 id="recap"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/11-vim-for-beginners/#recap">#</a> Recap</h2>
<p>For some readers already familiar with Vim, this article may be very easy. But Vim is easy, like 1, 2, 3, 4 and 5. Not at all; I'm just kidding.</p>
<p>Vim is hard to learn. But once you learn it and master it, Vim will become your best investment in your skill, which not only in coding but also in document editing (literally any document).</p>
<ul>
<li>First, learn the minimum knowledge of Vim.</li>
</ul>
<p>They are <code>h</code>, <code>j</code>, <code>k</code>, <code>l</code>, <code>i</code>, <code>:q!</code>, <code>:wq</code>, and just believe they're enough to do everything in Vim.</p>
<ul>
<li>Second, learn Vim key by key.</li>
</ul>
<p>When you are familiar with the minimum knowledge, you can move on to the following key and check their function key by key. Recommend you pick up <code>w</code>, <code>e</code>, <code>b</code>, <code>zz</code>, <code>ctrl + d</code>, <code>ctrl + u</code>, <code>%</code>.</p>
<ul>
<li>Third, don't try to memorize any hotkey.</li>
</ul>
<p>Instead of memorizing them, you should use them as much as you can until one day you don't need to check the document of Vim or Google it. It is like you are learning your first hotkeys, <code>ctrl + c</code> and <code>ctrl + v</code>, but there are much more hotkeys(they are cool and magical) in the world of Vim. And these skills will grow with you. I really love the feeling it is like you are playing an RPG game in which you started with a not-really good(maybe suck) weapon. But in the end, it became a legendary weapon, and it matched their master.</p>
<h2 id="bonus%3A-vim-tutor-just-in-your-hand"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/11-vim-for-beginners/#bonus%3A-vim-tutor-just-in-your-hand">#</a> Bonus: Vim tutor just in your hand</h2>
<p>So you are excited and want to learn more about Vim.</p>
<p>But when you saw the cheat sheet of Vim like this:</p>
<center>
<img src="https://helloacm.com/wp-content/uploads/2015/09/vi-vim-cheat-sheet.jpg" alt="vim-cheat-sheet" class="post-image-width" />
</center>
<blockquote>
<p><a href="https://helloacm.com/vi-vim-cheat-sheet-jpg/">Vim cheat sheet</a></p>
</blockquote>
<p>And you decide to quit. Cool, you want to quit? And what key you need to key in?</p>
<p>Yeah, it is <code>:q!</code>. You know it.</p>
<p>But wait, if I told you there is a <strong>free Vim tutor</strong> for you, would you consider sticking for a while?</p>
<p>Let's meet him.</p>
<p>If you use OS based on Linux (you can use <code>Git bash terminal</code>, which is cross-platform).</p>
<p>Open your terminal and input <code>vimtutor</code> then <code>Enter</code>. You will see him show up in your terminal.</p>
<p>How to close Vim Tutor? You asked. But you know it already.</p>
<blockquote>
<p><code>:q</code></p>
</blockquote>
<p>In case you don't like him, there are some useful links:</p>
<ul>
<li><a href="https://vim-adventures.com/">VIM Adventures</a></li>
<li><a href="https://www.openvim.com/">Interactive Vim tutorial</a></li>
<li><a href="https://vimcolorschemes.com/">vimcolorschemes</a> (Vim is old school? Check out how many fashion themes Vim has)</li>
</ul>
<p>That's pretty much it. Ciao ~</p>
<h2 id="ref"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/11-vim-for-beginners/#ref">#</a> Ref</h2>
<ul>
<li><a href="https://www.vim.org/">vim official</a></li>
<li><a href="https://helloacm.com/vi-vim-cheat-sheet-jpg/">Vim cheat sheet</a></li>
<li><a href="https://vim-adventures.com/">VIM Adventures</a></li>
<li><a href="https://www.openvim.com/">Interactive Vim tutorial</a></li>
<li><a href="https://vimcolorschemes.com/">vimcolorschemes</a></li>
</ul>
<blockquote>
<p>Disclaimer</p>
</blockquote>
<p>The above content is based on author's own experience, and it is inevitable that there are some subjective opinions. This is for readers' reference, and sharing experiences with me is also welcome.<br />
If there are any mistakes, please correct me. I will modify it immediately; thank you again!</p>
<p><a href="https://creativecommons.org/licenses/by/4.0/"><img src="https://i.creativecommons.org/l/by/4.0/88x31.png" alt="Creative Commons License" /></a></p>
<p>This work is licensed under a <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>. You can find me at <a href="https://benben.me/">benben.me</a></p>
Warp | 你的 21 世紀 AI Terminal
2023-05-31T00:00:00Z
https://blog.errorbaker.tw/posts/benben/12-warp/
<!-- summary -->
<!-- 都什麼時代了,你還在用傳統的 terminal 並且設置各種 config 嗎? -->
<!-- summary -->
<p><strong>! 本篇文章將會介紹一個好用的開發工具 Warp,是一種 AI terminal,期望大家都能火速開發</strong> :D</p>
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/12-warp/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<center>
<img src="https://hackmd.io/_uploads/HJsA77LE3.gif" alt="warp-demo" class="post-image-width" />
</center>
<blockquote>
<p>Warp demo by myself</p>
</blockquote>
<p>閱讀本文前建議知道 <code>terminal</code> 是什麼最為佳,目前 Warp 只有推出 Mac 版本,所以可能要讀者斟酌一下,然後不管是職業或是業餘或是轉職中都可以閱讀,或是,筆者自己有簡單的分類:</p>
<ol>
<li>level 1: <code>ls</code></li>
<li>level 2: <code>ls -al</code></li>
<li>level 3: <code>ls -altr</code></li>
</ol>
<p>小小測驗,在不 google 的情況下,你知道上述指令的用途且知道參數的函意,你是屬於上面的哪一種呢?如果你選了其中之一,恭喜你本文你應該都能輕鬆看懂。</p>
<p>你也有 <strong>AI 焦慮</strong> 跟 <strong>工具焦慮</strong> 嗎?在這個什麼都要 AI 、什麼工具都不斷的推陳出新的時代,最不缺的就是所謂的 "AI 工具" 了,其中也有不少只是過個水來湊熱鬧的,剛出來確實蠻新奇的,但是身為工程師看久了,就有底他是用什麼做的,然後背後套一個 LLM(Large Language Model) 罷了,比較火紅的如 ChapGPT-3/4, BERT, XLNet 等等。</p>
<p>既然如此,究竟 <code>Warp</code> 這個 terminal 是不是只是套一個 AI 而已的 terminal 呢?讓我們繼續看下去 ~</p>
<p>想也知道當然不是!不然筆者就不會寫這篇文章了,你說是吧?那就先來個簡易的比較表格:</p>
<table>
<thead>
<tr>
<th>功能</th>
<th>Warp</th>
<th>Window Terminal</th>
<th>iTerm2</th>
</tr>
</thead>
<tbody>
<tr>
<td>個人化</td>
<td>⭐️⭐⭐</td>
<td>⭐⭐</td>
<td>⭐</td>
</tr>
<tr>
<td>輕鬆複製 input/output</td>
<td>⭐⭐⭐</td>
<td>⭐</td>
<td>⭐</td>
</tr>
<tr>
<td>Auto Completion</td>
<td>開箱即用</td>
<td>需安裝、設置</td>
<td>需安裝、設置</td>
</tr>
<tr>
<td>快捷鍵手冊</td>
<td>內鍵(cmd + /)</td>
<td>需另外找</td>
<td>需另外找</td>
</tr>
<tr>
<td>分頁、分割視窗</td>
<td>O</td>
<td>O</td>
<td>O</td>
</tr>
<tr>
<td>AI</td>
<td>O</td>
<td>X</td>
<td>X</td>
</tr>
<tr>
<td>滑鼠可使用</td>
<td>O</td>
<td>X</td>
<td>X</td>
</tr>
<tr>
<td>cmd + a/z/x/c/v 可使用</td>
<td>O</td>
<td>X</td>
<td>X</td>
</tr>
</tbody>
</table>
<p>在本篇文章中,筆者會介紹 Warp 以及一些心得,內容大至如下:</p>
<ul>
<li><a href="https://blog.errorbaker.tw/posts/benben/12-warp/#%E5%89%8D%E8%A8%80">前言</a></li>
<li><a href="https://blog.errorbaker.tw/posts/benben/12-warp/#%E7%B0%A1%E4%BB%8B%E5%8F%8A%E5%AE%89%E8%A3%9D">簡介及安裝</a></li>
<li><a href="https://blog.errorbaker.tw/posts/benben/12-warp/#%E5%80%8B%E4%BA%BA%E5%8C%96%E9%85%8D%E7%BD%AE">個人化配置</a></li>
<li><a href="https://blog.errorbaker.tw/posts/benben/12-warp/#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F%E5%8F%8A-ai">使用方式及 AI</a></li>
<li><a href="https://blog.errorbaker.tw/posts/benben/12-warp/#%E5%BF%83%E5%BE%97%E6%8E%A8%E5%9D%91">心得推坑</a></li>
<li><a href="https://blog.errorbaker.tw/posts/benben/12-warp/#ref">Ref</a></li>
</ul>
<h2 id="%E7%B0%A1%E4%BB%8B%E5%8F%8A%E5%AE%89%E8%A3%9D"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/12-warp/#%E7%B0%A1%E4%BB%8B%E5%8F%8A%E5%AE%89%E8%A3%9D">#</a> 簡介及安裝</h2>
<p>來看一下他們的官網,其實已經介紹的不錯了(如果英文 OK 的話,也可以直接看),文件都很清晰,筆者當時開始使用時(約 2023/02),還沒有 Live Demo 的影片(約 2023/04 更新的),只能看文件跟自己玩看看去探索,後來才發現他出來有一陣子了,差不多 2021 就推出了(根據 Youtube 上的官方頻道)好像這陣子比較火紅,當然也是很潮的用 <code>Rust</code> 開發,筆者會發現也是偶然看到其他 Tech Youtuber 介紹的。</p>
<center>
<p><a href="https://www.youtube.com/watch?v=XWQY8LgkiXM"><img src="https://img.youtube.com/vi/XWQY8LgkiXM/0.jpg" alt="Warp official demo" /></a></p>
</center>
<blockquote>
<p>廷伸閱讀 <a href="https://warp.dev/">Warp</a></p>
</blockquote>
<p>但 <strong>目前只有 Mac 平台</strong> 可供下載,使用 <code>Window</code>, <code>Linux</code> 等其他平台的讀者可能還要再等等了。</p>
<p>不過還是要先明聲,這不是業配(Warp 看到可以考慮一下,喂!),但是 warp 官方那邊也有提供一個小小推薦計劃,推滿 10 位好像會送一件衣服(比 leetcode 的衣服好拿太多了吧),但是不是一樣穿上就太 Nerd 到交不到女朋友,筆者就不好說了(汗),如果看完我的文章覺得有興趣的話,還是可以用一下筆者的連結唷。</p>
<center>
<img src="https://hackmd.io/_uploads/r1Atcp24n.png" alt="warp-referral" class="post-image-width" />
</center>
<blockquote>
<p>推薦連結:<a href="https://app.warp.dev/referral/VLL959">https://app.warp.dev/referral/VLL959</a> (拜偷 拜偷 感蝦 感蝦 🥹)</p>
</blockquote>
<p>個人使用的話,不會有任何費用,所以不用擔心還要填信用卡之類的,只會要你註冊一下帳號,就可以開始使用了。</p>
<p>對我的好處:</p>
<ol>
<li>教學相長、分享新知</li>
<li>更深入研究 Warp 的功能</li>
</ol>
<p>對你的好處:</p>
<ol>
<li>有個酷酷的 terminal ,上班同事都會偷看你工作(好像也不太好),成為話題人物</li>
<li>AI 加成工作效率提升,如:Git, Vim 指令</li>
</ol>
<p>不過在之前推薦給朋友,我還不知道有這個推薦計劃,就在讀書會推了一些朋友了(嗚嗚),後來想想反正都要推了,就好好的寫一篇文,之後有朋友有興趣的話,我也可以貼文章給他了而不用在 live demo 一次了(Don't repeat yourself 原則)</p>
<p>但其實不管有沒有推廣計劃,我本來就會分享好用的工具了,那讀者要不要裝,也是取決在個人,如何裝我也管不著,我就佛系推的概念啦。</p>
<p>安裝方式,就如官網上的點擊 <code>Download Now</code>,之後正常安裝就可以了,安裝、註冊好後,就會看到如我首面圖片的畫面了(可能有些設定、主題不一樣)</p>
<h2 id="%E5%80%8B%E4%BA%BA%E5%8C%96%E9%85%8D%E7%BD%AE"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/12-warp/#%E5%80%8B%E4%BA%BA%E5%8C%96%E9%85%8D%E7%BD%AE">#</a> 個人化配置</h2>
<ul>
<li>外觀</li>
</ul>
<p>有內鍵多個主題,<strong>萬一沒有喜歡的也可以自行設計</strong>,並且有官方完整的教學,自由感十足。當然也可以設定字體、大小,比較特別的是他還可以直接設定 <strong>透明度、Blur 效果</strong>,以前筆者在 Windows (就是原生醜醜的黑白視窗)上搞了很久,後來改用 <a href="https://apps.microsoft.com/store/detail/windows-terminal/9N0DX20HK701"><code>Window Terminal</code></a> 才有內鍵的設定。</p>
<center>
<img src="https://hackmd.io/_uploads/H1JZmAn43.png" alt="warp-setting" class="post-image-width" />
</center>
<blockquote>
<p>Warp 設定頁面</p>
</blockquote>
<p>主題配置我是覺得官方出品的都還算不錯(比起其他的內鍵主題真的還算都不錯),可以挑一個順眼的用就行了,字體預設也蠻好看的(預設是 Hack),這邊都還沒講到功能,只是外觀而已,但光這些就已經深得筆者的心了 XD</p>
<ul>
<li>Configuration</li>
</ul>
<p>Warp 標旁 <strong>零設定 (Zero Configuration)</strong> 、開箱即用,完全不用另外設定、安裝套件,不用再一直備份你的 <code>.zshrc</code>, <code>.bashrc</code></p>
<p>我們來試一些常用的功能:</p>
<p><strong>自動路徑完成</strong> 的功能:在根目錄 <code>/</code> 下,輸入 <code>cd</code> + 按 <code>Tab</code> 鍵就會呈現當前的資料夾,可以使用上、下切換,按 <code>Enter</code> 會自動完成</p>
<center>
<img src="https://hackmd.io/_uploads/BkqDHA2N3.png" alt="warp-setting" class="post-image-width" />
</center>
<blockquote>
<p>開箱即用 | 自動路徑完成</p>
</blockquote>
<p><strong>Git 指令</strong> 的功能:輸入 <code>git log</code> + 按下 <code>Tab</code> 鍵就會呈現當前可以用的指令,可以使用上、下切換,按 <code>Enter</code> 會自動完成</p>
<center>
<img src="https://hackmd.io/_uploads/BkKMUA24h.png" alt="warp-setting" class="post-image-width" />
</center>
<blockquote>
<p>開箱即用 | 自動 Git 指令完成</p>
</blockquote>
<p>當然還有很多更強大的功能,筆者這邊就不一一示範了,可以參考還有哪些 <a href="https://docs.warp.dev/features/completions">Warp 支援的指令</a></p>
<h2 id="%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F%E5%8F%8A-ai"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/12-warp/#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F%E5%8F%8A-ai">#</a> 使用方式及 AI</h2>
<ul>
<li>像一般的 Terminal 使用</li>
</ul>
<p>當然你可以像一般的 terminal 一樣使用他,輸入正常的 linux 指令,如開頭的指令 <code>ls</code> 都可以直接使用,如果不需要其他酷酷的功能也可以像一般的 terminal 一像使用它就好了,但這樣好像也就不用 warp 了(笑)</p>
<ul>
<li>分頁</li>
</ul>
<p>筆者以前就覺得分頁的功能很好用,不知道是以前的 terminal 沒有,還是筆者不會用。總之呢,以前要多開 terminal 就會多開一個 app,然後萬一開了很多個(像是 docker, front-end, back-end, 日常使用的每個都開一個 terminal),就常常要找來找去,這時候如果有一個整合好的 terminal 就好了。</p>
<p>有的,那時候筆者有找到一款 terminal。對,就是 <code>Window terminal</code> ,那時候也是主打可以整合多個 terminal 在一起,如果是 windows 的使用者還是可以考慮使用看看的,我知道傳統的 cmd 視窗、Power Shell 的各種問題,可能就先給使用者不好的印象了,但 Warp 也還沒支援 Windows,所以可以先試試看 <code>Window terminal</code>,實其筆者以前也在內部的讀書會,推薦給朋友過。</p>
<p>在 Warp 中分頁非常簡單直覺,操作如下:</p>
<ol>
<li>按下 <code>cmd + t</code> ,開敵新的分頁</li>
<li>按下 <code>cmd + shirt + [</code> ,向左邊切換視窗</li>
<li>按下 <code>cmd + shirt + ]</code> ,向右邊切換視窗</li>
<li>按下 <code>cmd + w</code> ,關閉當前的分頁</li>
</ol>
<p>基本上,跟使用 VScode 的操作差不多,熟悉的話也不太需要特別記憶。</p>
<ul>
<li>分割視窗</li>
</ul>
<p>這邊的 <code>分割視窗</code> 跟上面的 <code>分頁</code>,是完全不一樣的功能,我開始使用也常搞混。其實這也是 <code>Window terminal</code> 有的能功,但 warp 有內鍵得快捷鍵可以簡單使用,也可以設定自己喜歡的快捷鍵,但預設的也算好記。</p>
<p>在 Warp 中分割視窗,操作如下:</p>
<ol>
<li>按下 <code>cmd + d</code> ,開啟水平分割視窗</li>
<li>按下 <code>cmd + shift + d</code> ,開啟垂直分割視窗</li>
<li>按下 <code>cmd + option + 上下左右</code> ,在 Warp 中速切換分割視窗了</li>
</ol>
<ul>
<li>AI 功能</li>
</ul>
<p>在這個時代,好像什麼都要來點 AI ,但也算稀鬆平常了吧,Warp 當然也有內鍵的 AI 可以使用。</p>
<p>只要在輸入指令的地方前面加入 <code>#</code> ,就可以進入搜尋模式了,再輸入想查尋的內容,就可以輕鬆的使用 AI 查尋指令了,例如:</p>
<center>
<img src="https://hackmd.io/_uploads/HJsA77LE3.gif" alt="warp-demo" class="post-image-width" />
</center>
<blockquote>
<p>在上面的圖片中,筆者查尋了如何將檔案移除 Git(How to remove file from git)</p>
</blockquote>
<p>比較少用的指令真的很容易忘,有什候為了這點小事還要去某個藏在書籤中的 git cheat sheet 或是去翻 stack overflow ,就顯得有點浪費時間,這時候 Warp 就可以派上用場了,讀者可以簡單體驗一下 warp 的威力。</p>
<p>當然有時候比較複雜的使用情況,就可以試試更進階的搜尋功能</p>
<p>可以在 Warp 的右上角中點擊 <strong>閃電的圖示</strong> ,叫出進階 AI 搜尋面板,也可以按下快捷鍵 <code>ctrl</code> + <code>shift</code> + <code>space</code> (預設是這個快捷鍵),但有可能被其他的 App 蓋過去,需留意一下,也可以在設定的地方修改快捷鍵。</p>
<p>這樣就可以輸入更多的 prompt 讓 AI 去搜尋了,但是這邊的搜尋是有限制次數的,<strong>一天 100 次</strong>,隔天會歸 0 ,也不難理解啦,畢竟 Chat-GPT 也都收費的 Chat-GPT 4 了。</p>
<p>進階的搜尋功能可以多加利用,萬一次數用完了也不用擔心,上面使用 <code>#</code> 的 <strong>一般搜尋是沒有限制次數的</strong>,可以安心使用!</p>
<h2 id="%E5%BF%83%E5%BE%97%E6%8E%A8%E5%9D%91"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/12-warp/#%E5%BF%83%E5%BE%97%E6%8E%A8%E5%9D%91">#</a> 心得推坑</h2>
<p>雖說是推坑,但我個人是真的很喜歡 Warp,除了各種個人化的配置檔、很多好用的內鍵功能、AI 輔助,好工具推推。Warp 是個人免費使用的,但目前(2023/05)只有 Mac OS 平台上才有,但未來其他平台應該會推出,有興趣的讀者可能還要再等等了。</p>
<p>另外,私心覺得 Warp 官方的 Developer Advocate(這個不知道怎麼翻,倡導開發者?) Jess 很猛,口條很好、有時又帶點搞笑,完全顛覆我對這種技術教學的印象,也在她身上學到不少,想了解更多 Warp,推薦大家也可以看一下 Warp 的官方 demo。</p>
<p>如果看完筆者的介紹,也有興趣的話,可以用 <a href="https://app.warp.dev/referral/VLL959">筆者的推薦連結</a> 註冊唷 🥹 。</p>
<p>這邊再幫讀者整理一個 Warp 常用的懶人包:</p>
<ul>
<li>免設定麻煩的設定檔,開箱即用</li>
<li>分頁:
<ul>
<li>新增 <code>cmd + t</code></li>
<li>關閉 <code>cmd + w</code></li>
<li>往左邊切換 <code>cmd + shift + [</code></li>
<li>往右邊切換 <code>cmd + shift + ]</code></li>
</ul>
</li>
<li>分割視窗:
<ul>
<li>新增垂直視窗 <code>cmd + d</code></li>
<li>新增水平視窗 <code>cmd + shift + d</code></li>
<li>關閉 <code>exit</code></li>
<li>切換 <code>cmd + option + 上下左右</code></li>
</ul>
</li>
<li>AI:
<ul>
<li>一般搜尋輸入 <code>#</code> + 你要查的指令</li>
<li>完整搜尋 <code>ctrl</code> + <code>shift</code> + <code>space</code></li>
</ul>
</li>
</ul>
<p>光以上這些指令,已經可以減少不少使用滑鼠的時間了,如果再加上 <code>Raycast</code> 跟 <code>Vim</code>,根本不用滑鼠了吧(咦)一起加入鍵盤魔人吧!</p>
<p>差不多是這樣啦,我們下次見 ~</p>
<p>Happy Coding ~</p>
<h2 id="ref"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/12-warp/#ref">#</a> Ref</h2>
<ul>
<li><a href="https://warp.dev/">Warp</a></li>
<li><a href="https://www.youtube.com/watch?v=XWQY8LgkiXM">Warp official demo</a></li>
<li><a href="https://docs.warp.dev/features/completions">Warp 支援的指令</a></li>
<li><a href="https://apps.microsoft.com/store/detail/windows-terminal/9N0DX20HK701">Window Terminal</a></li>
<li><a href="https://app.warp.dev/referral/VLL959">筆者的推薦連結 🥹</a></li>
</ul>
<blockquote>
<p>免責聲名</p>
</blockquote>
<p>以上均為筆者自身經驗,難免小有主觀意見,供讀者們參考,也歡迎分享經驗交流。<br />
如果有錯誤的地方還請大大們指正,筆者會立刻修改,再次感謝大家!</p>
<p><a href="https://creativecommons.org/licenses/by/4.0/"><img src="https://i.creativecommons.org/l/by/4.0/88x31.png" alt="Creative Commons License" /></a></p>
<p>本著作係採用 <a href="https://creativecommons.org/licenses/by/4.0/">創用 CC 姓名標示 4.0 國際授權條款</a> 授權。您可以在 <a href="https://benben.me/">benben.me</a> 找到我。</p>
<p>This work is licensed under a <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>. You can find me at <a href="https://benben.me/">benben.me</a></p>
什麼時代了還在學 | 嘸蝦米輸入法
2023-08-31T00:00:00Z
https://blog.errorbaker.tw/posts/benben/13-boshiamy/
<!-- summary -->
<!-- 都什麼時代了還在學「嘸蝦米輸入法」?但是打起來就很爽啊! -->
<!-- summary -->
<p><strong>! 本篇文章將會介紹「嘸蝦米輸入法」以及筆者約兩年的心得</strong> :D</p>
<h2 id="%E5%89%8D%E8%A8%80"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/13-boshiamy/#%E5%89%8D%E8%A8%80">#</a> 前言</h2>
<center>
<img src="https://hackmd.io/_uploads/B1HXrmRTn.gif" class="post-image-width" alt="嘸蝦米 demo" />
</center>
<blockquote>
<p>嘸蝦米 demo, by @Benben</p>
</blockquote>
<p>大家好久不見!筆者最近也真的很忙禮拜六都加班,有時候禮拜日也加(嗚嗚嗚),越來越忙的概念,我還不逃,我一定是販劍。總之呢~這篇也算是比較多人問的主題,也是分享蠻多次的了,所以就到了差不多該寫一篇文章的時候啦!</p>
<p>是這樣的,在讀書會、或是工作時分享螢幕的時候,朋友都會問我是用什麼輸入法?我通常就看對方的反應分享,有的只是問問、有的是真的很有興趣、有的只是想找我聊天(?),少數遇到真的有去嘗試的,也真的讓筆者蠻開心的!</p>
<p>本篇內容大致如下:</p>
<ul>
<li>起蒙</li>
<li>苦練</li>
<li>遷移</li>
<li>一時打字一時爽,一直打字一直爽</li>
<li>番外篇:rime-liur</li>
<li>小結</li>
</ul>
<h2 id="%E8%B5%B7%E8%92%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/13-boshiamy/#%E8%B5%B7%E8%92%99">#</a> 起蒙</h2>
<p>說到嘸蝦米,大家腦中一定都是以前國、高中時,電腦課才會教的東西(曝露年齡了),會用的應該都是老人了吧,但筆者才學了 2 年左右,目前已經可以流暢的聊天、打文章了,當然有時候還是有打不快的字,但對我來說已經是利大於弊了。</p>
<p>最一開始是看了 Papaya 老師的介紹影片,看到老師嘸蝦米打得「快、狠、準」,立馬被深深吸引,之後筆者又看了好幾次影片,最後狠下心來學習一波,依現在的時間點算起來確實是 2 年前右左開始從零開始學習的。</p>
<center>
<p><a href="https://www.youtube.com/watch?v=fLrjzLPw5BE"><img src="https://img.youtube.com/vi/fLrjzLPw5BE/0.jpg" alt="嘸蝦米輸入法" /></a></p>
</center>
<p>在此之前,我完完全全的是新手小白,以前只有在電腦課時不小心切換到嘸蝦米,胡亂打幾個字,然後就無緣了,可能小時候沒電腦,連一般的注音中打都打不快,想說反正本來中打就不快了,切換的成本也不高,就乾脆切換過去順便練一下好了。</p>
<p>嘸蝦米吸引的我地方,除了最優勢的不用選字外,它可以只用「A ~ Z,共 <strong>26 個字母</strong>」就可以完成中打輸入,你可能覺得這沒什麼,除了比注音少了一些按鍵(ㄅ, ..., 一聲, ... 等等,<strong>共 41 個</strong>),更少的輸入可以打更快。</p>
<blockquote>
<p>試試輸入以下:「1 生、 2 人、 3 餐、 4 季」</p>
</blockquote>
<center>
<img src="https://hackmd.io/_uploads/B1HXrmRTn.gif" class="post-image-width" alt="嘸蝦米 demo" />
</center>
<blockquote>
<p>對,就是封面的圖(Gif 是原速度沒有加速喔)</p>
</blockquote>
<p>要嘛你切換了數次 「中/英」 模式;要嘛你使用獨立的數字鍵,但兩者都比較費時,世界上最遠的距離就是從鍵盤中間到方向鍵(獨立字鍵又更遠了),但是 <strong>嘸蝦米可以直接打出數字</strong>,因為它沒有佔用任何數字鍵,數字也是嘸蝦米有趣的地方,晚點會再提到然。試想其他的符號呢?,如:<code>!@#+-()[]{}</code> 等等,如果讀者剛好用到這些符號,在嘸蝦米中都不用切換啦,是不是很棒呢。</p>
<p>再來上面也提到,嘸蝦米只需要 26 個字母就可以,但是你要打的快,英打也要有一定的要求。如果你英打本來就很快那很好;如果不快那也很好,因為練好嘸蝦米也可以 <strong>一定程度的提升你的英打水平</strong>,可以一起練起來的概念。</p>
<p>所以基於以上總總原因,我決定要來入坑嘸蝦米(才不只是因為帥呢),再來會聊聊我的學習過程,也許是最無趣的但也是最重要的,希望上述的內容有些點可以打到你。</p>
<h2 id="%E8%8B%A6%E7%B7%B4"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/13-boshiamy/#%E8%8B%A6%E7%B7%B4">#</a> 苦練</h2>
<p>筆者下定決心之後,每天都到官網的線上練習練個 10 ~ 30 分鐘,一開始真的不知道在幹嘛,一堆字根要記、要練英打,本來想說也 A ~ Z 要打也就 26 個字母,應該也不會太難,但有的字根結合了形、音、義,就會覺得很難。但也可能是我那時候同時要忙很多事,2 年前我也差不多在全職轉職中、學習 Vim 、練英打、練嘸蝦米,所以學得比較慢。如果讀者的時間比較多、比較沒其他學習壓力,一定是可以比筆者快很多的啦!</p>
<p>雖說叫記字根,但其實嘸蝦米的字根都有依據的,因此 <strong>用上一點點聯想力,很多字根基本上不用背</strong>,基本字根都熟悉後,基本上就可以稍為打打字了。</p>
<p>整體來說,我覺得官網的練習小網站還不錯,慢慢地一個一個從好記的字根開始,再到需要一些聯想力的字根,我差不多每天上個 10 ~ 30 分鐘,差不多一個月後,可以打 50% 的字了,心態上不要覺得它很難,把他當作遊戲,今天學了「A 技能」,一天一天學到了「Z 技能」,等到都學完了,就可以來個組合技降龍十八掌了(誤)。</p>
<center>
<img src="https://hackmd.io/_uploads/BkAOrVA62.png" class="post-image-width" alt="輕鬆學會嘸蝦米" />
</center>
<blockquote>
<p>官網的「輕鬆學會嘸蝦米」</p>
</blockquote>
<p>學著學著會慢慢感受到 <strong>創始人 -- 劉重次</strong> 的創造力,些全都是經過精心設計的,包含後面的「一字根表」、「兩字根表」,這些是嘸蝦米快的關鍵,一樣後面會再詳細提到。</p>
<h2 id="%E9%81%B7%E7%A7%BB"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/13-boshiamy/#%E9%81%B7%E7%A7%BB">#</a> 遷移</h2>
<p>當你大概 90% 的字都會打的時候,並而速度大概 40 左右時,就表示到了可以直接上嘸蝦米打中打了,除非你是需要打遊戲時,需要大量的嘴炮需要打非常快,不然一般使用情況大約 60 就很夠用了,所以真的 <strong>差不多 40</strong> 的時候就可以直上了~</p>
<p>嘸蝦米官網也很貼心的準備了試用版,不過它的試用版呢~很佛心,<strong>是可以一直免費用的</strong>,只是每三個月要重新安裝一次,筆者一開始直上也是先用試用版的,大概用了一年才換付費版的(這邊也可以先不買,文章最後會在提到免費的替代方案,但筆者還是有支持正版的)</p>
<center>
<img src="https://hackmd.io/_uploads/rkyML4C6n.png" class="post-image-width" alt="輕鬆學會嘸蝦米" />
</center>
<blockquote>
<p>試用版下載:<a href="https://boshiamy.com/download_trial.php">https://boshiamy.com/download_trial.php</a></p>
</blockquote>
<p>當然,一路走來練了這麼多字根、完整的字,再來就是實戰了,實戰也是最快提升速度的時候,筆者剛換過來的時候也很卡,一定卡的啦,但也好像每個人都是這樣來的,沒什麼好怪的,就怪自己不夠熟練,繼續練習就對了!</p>
<p>筆者會將常常遇到不會打的字、不熟的字,先用筆記軟體記下來(有要拿去練習的也歡迎),然後用「最簡碼」練習一次,隔天再練習一次,久而久之這些詞語就一個一個就熟了,會慢慢變成反射動作。</p>
<blockquote>
<p><a href="https://hackmd.io/@benben6515/S10poEXG9">筆者練習的嘸蝦米字</a></p>
</blockquote>
<h2 id="%E4%B8%80%E6%99%82%E6%89%93%E5%AD%97%E4%B8%80%E6%99%82%E7%88%BD%EF%BC%8C%E4%B8%80%E7%9B%B4%E6%89%93%E5%AD%97%E4%B8%80%E7%9B%B4%E7%88%BD"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/13-boshiamy/#%E4%B8%80%E6%99%82%E6%89%93%E5%AD%97%E4%B8%80%E6%99%82%E7%88%BD%EF%BC%8C%E4%B8%80%E7%9B%B4%E6%89%93%E5%AD%97%E4%B8%80%E7%9B%B4%E7%88%BD">#</a> 一時打字一時爽,一直打字一直爽</h2>
<p>當你字都差不多會打的時候,你以為嘸蝦米就這樣了?你錯了!<strong>好玩的才正要開始呢!</strong>,如果你前面的某個階段還不熟,就先看到了這一段的話,再撐一下!到這個階段時,嘸蝦米的實力才會發揮出來,但前提是你要撐到這一階段,可以再去把字根練熟一點。</p>
<p>在嘸蝦米的世界中,有所謂的「一字根表」,也就是 A ~ Z,如字面上的意思,打 <strong>一個英文字 + 空格</strong> 就可以打出一個中文字了。</p>
<p>例如:<code>A => 對</code>, <code>D => 的</code>, ... 等等,像這種字都是可以神速打出來的。</p>
<p>這些通常都是精挑細選的常用字或是不好打的常用字,當中還有一個特別的「中文數字」,在嘸蝦米的設計上,也通通都是一字搞定,因此很推薦給常常需要打中文字數字的讀者,如:公文、筆錄等等,<strong>中文數字在注音很多字都是撞同音</strong>,幾乎 <s>一定</s> 要選字的。</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token parameter"><span class="token constant">E</span></span> <span class="token operator">=></span> 一<br /><span class="token parameter"><span class="token constant">R</span></span> <span class="token operator">=></span> 二<br /><span class="token parameter"><span class="token constant">S</span></span> <span class="token operator">=></span> 三<br /><span class="token parameter"><span class="token constant">F</span></span> <span class="token operator">=></span> 四<br /><span class="token parameter"><span class="token constant">W</span></span> <span class="token operator">=></span> 五<br /><span class="token parameter"><span class="token constant">L</span></span> <span class="token operator">=></span> 六<br /><span class="token parameter"><span class="token constant">C</span></span> <span class="token operator">=></span> 七<br /><span class="token parameter"><span class="token constant">B</span></span> <span class="token operator">=></span> 八<br /><span class="token parameter"><span class="token constant">K</span></span> <span class="token operator">=></span> 九<br /><span class="token parameter"><span class="token constant">J</span></span> <span class="token operator">=></span> 十</code></pre>
<blockquote>
<p>簡單唸看看再對照英文,你又會發現新大陸了!</p>
</blockquote>
<p>俗話說的好:「有一就有二」,當然嘸蝦米的世界中,也有所謂的「二字根表」,也就是 AA, AB, ..., ZZ,如字面上的意思,<strong>兩個英文字 + 空格</strong> 就可以打出一個中文字了。也就是說二字根表,共有 26 X 26 個字,通常都是常用字的簡碼,有趣的是一個字可以有好幾種打法,一般的、簡碼的,但是都不太用選字。</p>
<p>所以嘸蝦米有這些東西有什麼用?這背後的意思是:「假設熟練程度一樣,跟注音比起來,嘸蝦米的輸入少很多就可以打出一個字,讀者覺得哪個輸入法打的快(而且又準)?」</p>
<p>除了種種嘸蝦米的優勢:不用選字、只使用英文字母 ... 等,但是從這裡開始就是 <strong>注音追不上的地方了</strong>,只打一兩個字母就完成一個中文字的輸入,是注音做不到的(是可以啦,但就只是注音文,你懂的),所以繼續好好練你的嘸蝦米,現在打不快也沒關係,但至少你打的省力又不用選字,先速度練到 50 ~ 60,先這樣就很夠了,哪天突然想加強速度了,再回來練也行,你會發現前以注音練得要死,總是卡在一個瓶頸,嘸蝦米輕鬆就突破,甚至超越了!</p>
<h2 id="%E7%95%AA%E5%A4%96%E7%AF%87%EF%BC%9Arime-liur"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/13-boshiamy/#%E7%95%AA%E5%A4%96%E7%AF%87%EF%BC%9Arime-liur">#</a> 番外篇:rime-liur</h2>
<p><code>rime-liur</code> 是基於 <code>RIME</code> 輸入法設計的全功能嘸蝦米方案,<code>RIME</code> 又稱「鼠鬚管」,如果成為小蝦米的話,應該多少會聽過。</p>
<blockquote>
<p>GitHub Repo:<a href="https://github.com/hsuanyi-chou/rime-liur">hsuanyi-chou/rime-liur: 基於 RIME 輸入法設計的全功能嘸蝦米方案</a></p>
</blockquote>
<p><code>rime-liur</code> 是 @hsuanyi-chou 大大基於 <code>RIME</code> 開發,並且是 <strong>開源出來的</strong> (筆者已給 Star 支持一下),對於我們一般使用者來說可以 <s>算是免費使用</s>,那麼 <code>rime-liur</code> 有什麼特別之處呢?請見 GitHub 上的解說(逃)</p>
<p>好啦,這邊筆者挑幾個好用的功能說明一下:</p>
<ol>
<li>可以直接用 <code>shift</code> 切換中/英,一般是只能使用 <code>ctrl + space</code></li>
<li>注音模式、拼音模式、字碼反查</li>
<li>可以客製化輸入法的字體、主題、候選字顏色等等</li>
</ol>
<p>之前剛好有朋友遇到不會使用「加詞加字」功能(這算是官方就有的功能了,所以上面沒有特別提到),確實這對一般非開發者的人員來說,去改設定檔是有些困難的,可能少了一個空白設定檔就壞了(這是真的),所以筆者想想就簡單錄了一個教學給朋友,對我來說也算是一個紀錄,有需要的讀者也可以參考看看囉。</p>
<center>
<iframe src="https://www.veed.io/embed/8640877d-b536-4279-af39-b70a22025834" class="post-image-width" style="height: 400px;" frameborder="0" title="嘸蝦米加字加詞.mp4" webkitallowfullscreen="" mozallowfullscreen="" allowfullscreen=""></iframe>
</center>
<blockquote>
<p>Rime 嘸蝦米的加詞加字功能教學, by @Benben</p>
</blockquote>
<p>例如:影片中一開始的預設是 <code>name + 空格</code> 就會打出 <code>我的名字</code>,我在後面加了 <code>Benben</code>,所以之後只要打 <code>name + 空格</code> 就會打出 <code>我的名字:Benben</code>,加名字、信箱、地址等等只是基本;後來我再影片中加入了 <code>hw; + 空格</code> 就會打山 <code>Hello World</code>,其中取了 h 作為 hello、w 作為 world,應該很好理解。</p>
<p>眼尖的讀者就會發現:我在 <code>hw</code> 後加入了一個 <code>;</code> ,這是刻意的,因為在嘸蝦米中 1 ~ 4 英文字母的組合,很大的幾率會碰撞(俗稱「被估用」),所以筆者加了一個符號當作後綴,當然讀者也可以自行發揮創意,所以我說 <strong>好玩的才正要開始呢</strong>!</p>
<h2 id="%E5%B0%8F%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/13-boshiamy/#%E5%B0%8F%E7%B5%90">#</a> 小結</h2>
<p>嘸蝦米當然不是看完一篇文章就可以簡單學會的,大量的練習是必要,但付出的之後都會成為美麗的果實,省下的是你的時間,而俗話說的好「時間就是金錢」,假如在打字上每天幫你省下 10 分鐘就好,一個月就是 5 個小時,拿去做更多的運用不是很好嗎?</p>
<p>例如:假如是你客服或是賣家之類的,有一些常用的詞,都是可以加入到嘸蝦米的「加詞加字」功能了,設定完之後,隨便打幾個字就跟用噴的一樣,其他同事看了一定嚇嚇叫,也是你的省時好幫手,誠心推薦給常需要輸入大量文字的工作者。</p>
<p>嘸蝦米的水也很深、學習曲線高,但是之後的天花板也很高,可以玩的很多,也還有很多嘸蝦米的變體,例如:這篇文章中的 <code>rime-liur</code> 只是其中之一,還有很多好玩的功能、插件等著你去探索。</p>
<p>後來筆者請教一些前輩(大蝦米)們,真的很感謝前輩們願意分享,希望這個「快失傳的輸入法」可以傳承下去,這麼好用的神器不能只有我知道啊!</p>
<p>以下整理一些嘸蝦米密技:</p>
<ol>
<li>「,,SP」快打模式(最簡碼)</li>
<li>「,,C」簡體模式</li>
<li>「,,CT」打繁出簡模式</li>
<li>「,,J」日文模式</li>
<li>「,,T」切回繁體模式</li>
<li>「,,X」密語解碼模式</li>
<li>「、」同音字模式</li>
<li>「、;」注音輸入模式(一次性)</li>
<li>「,,box」加字加詞模式</li>
<li>右鍵 > 內容,查字根或注音模式</li>
</ol>
<ul>
<li><a href="https://boshiamy.com/">嘸蝦米輸入法 官網</a> (其實官網就有很多好用的資源了)</li>
<li><a href="https://www.ptt.cc/bbs/Liu/M.1504856517.A.6F9.html">嘸蝦米二碼表</a></li>
</ul>
<h2 id="ref"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/13-boshiamy/#ref">#</a> Ref</h2>
<ul>
<li><a href="https://www.youtube.com/watch?v=fLrjzLPw5BE">為什麼公司的前輩們打字都那麼快? 來談談那些年他們一起練過的嘸蝦米輸入法 ~</a></li>
<li><a href="https://boshiamy.com/">嘸蝦米輸入法 官網</a></li>
<li><a href="https://boshiamy.com/download_trial.php">嘸蝦米試用版下載</a></li>
<li><a href="https://www.ptt.cc/bbs/Liu/M.1504856517.A.6F9.html">嘸蝦米二碼表</a></li>
<li><a href="https://hackmd.io/@benben6515/S10poEXG9">筆者練習的嘸蝦米字</a></li>
</ul>
<blockquote>
<p>免責聲名</p>
</blockquote>
<p>以上均為筆者自身經驗,難免小有主觀意見,供讀者們參考,也歡迎分享經驗交流。<br />
如果有錯誤的地方還請大大們指正,筆者會立刻修改,再次感謝大家!</p>
<p><a href="https://creativecommons.org/licenses/by/4.0/"><img src="https://i.creativecommons.org/l/by/4.0/88x31.png" alt="Creative Commons License" /></a></p>
<p>本著作係採用 <a href="https://creativecommons.org/licenses/by/4.0/">創用 CC 姓名標示 4.0 國際授權條款</a> 授權。您可以在 <a href="https://benben.me/">benben.me</a> 找到我。</p>
<p>This work is licensed under a <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>. You can find me at <a href="https://benben.me/">benben.me</a></p>
Scroll-driven Animations
2023-10-19T00:00:00Z
https://blog.errorbaker.tw/posts/ruofan/scroll-driven-animations/
<!-- summary -->
<p>大家好!在這篇文章中,我想帶大家認識新的滾動驅動動畫 API。</p>
<!-- summary -->
<!-- more -->
<h2 id="scroll-driven-animations"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/scroll-driven-animations/#scroll-driven-animations">#</a> scroll-driven animations</h2>
<p>什麼是捲動畫面時會驅動的動畫呢?<br />
當我們在網頁上滾動時,有些動畫會隨著滾動的動作而觸發。以下是兩個範例:</p>
<p>當使用者滑動畫面時,頁面最上方和最下方的物件會展現出平移和淡出的效果。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/animation-1.gif" alt="" /></p>
<p>使用者在滑動畫面時,最上方會展示進度條的動畫效果。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/animation-2.gif" alt="" /></p>
<p>這些都是典型的捲動驅動的動畫效果。</p>
<h2 id="classic-way"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/scroll-driven-animations/#classic-way">#</a> Classic way</h2>
<p>傳統上,我們會使用 addEventListener 來監聽滾動事件,並在主執行緒 (main thread) 上執行 callback function。例如,以下的程式碼展示了如何根據滾動的百分比調整進度條的寬度:</p>
<pre class="language-html"><code class="language-html"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>body</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>progressBar<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br /> <span class="token keyword">const</span> progressBar <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">'progressBar'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /> window<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'scroll'</span><span class="token punctuation">,</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// Calculate the scroll progress in percentage</span><br /> <span class="token keyword">const</span> totalHeight <span class="token operator">=</span> document<span class="token punctuation">.</span>body<span class="token punctuation">.</span>scrollHeight <span class="token operator">-</span> window<span class="token punctuation">.</span>innerHeight<span class="token punctuation">;</span><br /> <span class="token keyword">const</span> scrollPosition <span class="token operator">=</span> window<span class="token punctuation">.</span>scrollY<span class="token punctuation">;</span><br /> <span class="token keyword">const</span> scrollPercentage <span class="token operator">=</span> <span class="token punctuation">(</span>scrollPosition <span class="token operator">/</span> totalHeight<span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">100</span><span class="token punctuation">;</span><br /><br /> progressBar<span class="token punctuation">.</span>style<span class="token punctuation">.</span>width <span class="token operator">=</span> scrollPercentage <span class="token operator">+</span> <span class="token string">'%'</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> </span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>body</span><span class="token punctuation">></span></span></code></pre>
<p>但這種方法可能會導致動畫在連續的幀之間錯過渲染,造成頁面出現不流暢的效果,這種現象稱之為 "janky"。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/jank.png" alt="" /></p>
<h2 id="new-in-web-animations"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/scroll-driven-animations/#new-in-web-animations">#</a> new in web animations</h2>
<p>Chrome 的新 API 提供了一種宣告式的方法來實作滾動驅動的動畫,並確保動畫在非主執行緒上運行。這使得開發者能夠更精確地控制動畫的播放時機。</p>
<p>例如,以下的程式碼展示了如何使用 scroll-timeline 和 view() 來實作滾動驅動的動畫:</p>
<h3 id="%E6%BB%BE%E5%8B%95%E9%80%B2%E5%BA%A6%E6%99%82%E9%96%93%E7%B7%9A"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/scroll-driven-animations/#%E6%BB%BE%E5%8B%95%E9%80%B2%E5%BA%A6%E6%99%82%E9%96%93%E7%B7%9A">#</a> 滾動進度時間線</h3>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>body</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>progress<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /> …<br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>body</span><span class="token punctuation">></span></span><br />html {<br /> scroll-timeline: --page-scroll block;<br />}<br /><br />@keyframes grow-progress {<br /> from { transform: scaleX(0); }<br /> to { transform: scaleX(1); }<br />}<br /><br />#progress {<br /> position: fixed;<br /> left: 0; top: 0;<br /> width: 100%; height: 1em;<br /> background: red;<br /><br /> transform-origin: 0 50%;<br /> animation: grow-progress auto linear;<br /> animation-timeline: --page-scroll;<br />}</code></pre>
<h3 id="%E8%A6%96%E5%9C%96%E9%80%B2%E5%BA%A6%E6%99%82%E9%96%93%E7%B7%9A"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/scroll-driven-animations/#%E8%A6%96%E5%9C%96%E9%80%B2%E5%BA%A6%E6%99%82%E9%96%93%E7%B7%9A">#</a> 視圖進度時間線</h3>
<pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@keyframes</span> animate-in-and-out</span> <span class="token punctuation">{</span><br /> <span class="token selector">entry 0%</span> <span class="token punctuation">{</span><br /> <span class="token property">opacity</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span> <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">translateY</span><span class="token punctuation">(</span>100%<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token selector">entry 100%</span> <span class="token punctuation">{</span><br /> <span class="token property">opacity</span><span class="token punctuation">:</span> 1<span class="token punctuation">;</span> <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">translateY</span><span class="token punctuation">(</span>0<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token selector">exit 0%</span> <span class="token punctuation">{</span><br /> <span class="token property">opacity</span><span class="token punctuation">:</span> 1<span class="token punctuation">;</span> <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">translateY</span><span class="token punctuation">(</span>0<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token selector">exit 100%</span> <span class="token punctuation">{</span><br /> <span class="token property">opacity</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span> <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">translateY</span><span class="token punctuation">(</span>-100%<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><br /><span class="token selector">#list-view li</span> <span class="token punctuation">{</span><br /> <span class="token property">animation</span><span class="token punctuation">:</span> linear animate-in-and-out<span class="token punctuation">;</span><br /> <span class="token property">animation-timeline</span><span class="token punctuation">:</span> <span class="token function">view</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<h2 id="%E7%B8%BD%E7%B5%90"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/scroll-driven-animations/#%E7%B8%BD%E7%B5%90">#</a> 總結</h2>
<p>新的滾動驅動動畫 API 提供了一種更簡單的方法來實作絲滑的動畫效果,但是目前看起來並不是所有瀏覽器都支援。</p>
<p><img src="https://blog.errorbaker.tw/img/posts/ruofan/animation-3.png" alt="" /></p>
<p>如果在閱讀過程中有任何疑問或建議,請隨時留言告訴我。謝謝大家!😃</p>
<h2 id="%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/ruofan/scroll-driven-animations/#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99">#</a> 參考資料</h2>
<ul>
<li>[Blog](Animate elements on scroll with Scroll-driven animations)</li>
<li>[Blog](Inside look at modern web browser (part 3))</li>
<li>[Document](Scroll-driven Animations)</li>
</ul>
2023 年度回顧 | 目標、效率、平衡
2023-12-30T00:00:00Z
https://blog.errorbaker.tw/posts/benben/14-2023-wrapped/
<!-- summary -->
<!-- 不要怕累或是多做,因為你多做的也許不會馬上體現出來,但都是你未來的籌碼! -->
<!-- summary -->
<p><strong>! 本篇文章將會介紹筆者約兩年多一點點的開發心得</strong> :D</p>
<center>
<img src="https://hackmd.io/_uploads/rydsCO3Da.png" class="post-image-width" style="width: 480px" alt="手寫 Hello world" />
</center>
<blockquote>
<p>好久沒寫的手寫,終於有點空再寫一下了(準備製做送給友人的書籤)</p>
</blockquote>
<p>大家安安,2023 要過完了,大家還好嗎?</p>
<p>筆者的話還是老樣子的忙啊忙啊,年前嘛~你懂的,感覺也停更一小段時間了,回歸一下跟大家說一下我還活著唷~ 😂</p>
<p>這篇主要會分為 3 個段落,如標題的:目標、效率、平衡</p>
<ul>
<li>
<p><strong>目標</strong> => 給正在轉職期的、轉換跳槽期的讀者們參考~如果剛要轉或是還在猶預的朋朋們可能會辛苦一點,但我也會分享我轉職前的目標,目標對我的影響,也希望對讀者們有點幫助。</p>
</li>
<li>
<p><strong>效率</strong> => 這部分會紹介我使用的一些工具、插件、App,以及我如何提升效率的(這裡筆者先立個 flag,通常這種標題的農場文呢,作者通常不是工程師,對工程師所謂的 <strong>效率</strong> 理解有蠻有出路的,很容易寫出看似很有道理的內容,像是:要區「分重要/不重要、緊急/不緊急」之類的 bala bala,但實質幫助有限,但筆者的經驗常常是:每個都很重要、每個都很緊急,這種時候又該怎麼辨?),當然這邊效率的面向會是學習、程式相關的,還是老樣子,接受對你有幫助的~</p>
</li>
<li>
<p><strong>平衡</strong> => 目前來說 2023 年算是比較失衡的一年,大量的加班時間,當中包含開會,常常會一開就是好幾個小時,最晚的一天到晚上 11 點吧,因為是非常小新創公司,也沒有正規的 PM (算是兼職的),所以可想之知,大部分的責任回到開發人員身上。</p>
</li>
</ul>
<p>筆者轉職完入職第一天是 <code>2020/11/15</code> ,算到 <code>2023/12/31</code> 的話,差不多是 2 年多一些時間。</p>
<blockquote>
<p>或是更正確的說:2.126027397260274 年</p>
</blockquote>
<p>不可避免的說目前的前端市場,越來越辛苦了,不只是因為疫情、ChatGPT,當然也因為粥少僧多的情況越來越明顯。以前 Senior 前端人數真的很少,但現在也很多當初的 Junior 也慢慢爬上來了,意味著也越來越少公司願意用沒有經驗的 Junior ,如果還在轉職中或是想轉職的讀者,可能要加緊腳步了,以筆者的角度來看目前的情況概大如下:</p>
<ul>
<li>4 年前(2018 ~ 2020)來看,會個 HTML、CSS、JavaScript 就可以轉職成功了</li>
<li>2 年前(2021 ~ 2022)來看,稍為碰過框架,基本的 HTML、CSS、JavaScript 不要太差,也可以拿到 Offer</li>
<li>現在(2023 ~ 2024)來看,基本的 HTML、CSS、JavaScript 不說,都要熟悉框架、TypeScript,甚至一些後端經驗,還不一定拿得到 Offer</li>
</ul>
<p>這邊指的是安全情況下,因為前端的技術變化很快,所以沒達標的話,履歷投個幾百封也不會有幾封回信,要練到上面的情況,回信率、拿 Offer 的機率才會好一點,以至於不會打擊信心太大導致轉職卡關。</p>
<p>前陣子也會跟資深前端朋朋們聊天,也覺得現在這個時間點連由前端從零開始也有點難度,對於比較沒自信一點但又想進 IT 產業的話,可以從 QA (測試人員)開始,至少先進產業感受一下。</p>
<h2 id="%E9%97%9C%E6%96%BC%E7%9B%AE%E6%A8%99"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/14-2023-wrapped/#%E9%97%9C%E6%96%BC%E7%9B%AE%E6%A8%99">#</a> 關於目標</h2>
<ul>
<li>顯化前的目標</li>
</ul>
<center>
<img src="https://hackmd.io/_uploads/B1_GJKhwa.png" class="post-image-width" style="width: 480px" alt="2020 年度目標" />
</center>
<blockquote>
<p>2020 年時,筆者轉職前寫下的目標,中二了一點,但還算有效</p>
</blockquote>
<p>俗話說「天下無難事,只怕有心人」筆者覺得只要有心轉職的機會還是蠻大的,重點在你的目標,你想達成什麼樣的目標?這個目標可行嗎?你願意付出什麼樣的努力?</p>
<p>那時候轉職都還沒開始,就看到老師的一篇很香工程師工作的文章,這是我想要的結果。</p>
<blockquote>
<p>延伸閱讀:<a href="https://hulitw.medium.com/working-at-onedegree-378a0401130e">Huli - 金融保險科技公司 OneDegree 工作半年心得 | Medium</a> (註:Huli 大大目前(2023/12)已在日本工作中囉~)</p>
</blockquote>
<p>我當下不是懷疑自己做不做得到,我一定做得到,只是 <strong>時間問題</strong>,所以我當下也另外下了一個中長期的目標。</p>
<p>中長期目標: 3 ~ 5 年達到老師在 OneDegree 的狀態(4 年 / 100k),<code>月薪 100k</code>,目標明確!</p>
<p>對於轉職的故事,我這裡就不著墨太多,大概是同期生或是身邊的夥伴們比較懂,雖然還是會有人覺得我怎麼好像當個工程師很輕鬆一般、薪水也還不錯、可以遠端工作、常去咖啡廳之類的,但是當中的酸甜苦辣,真的是只有經歷過的人才懂。轉職時,時間上是全職學習,但我差不多是把轉職當超過正職的心態去做吧,一般人一周差不多 40 小時的學習時間,我自己一周都超過 50 小時去學習,多學了一些演算法啊、框架之類的。</p>
<p>關於「冒牌者症候群」呢,目前沒有發生在我身上,我覺得是就不要想這麼多,衝就對了。蠻多人是想得太多,但沒有行動;也部分人是眼高手低,想高薪但又不想努力。總之就是保持好奇、學習心態,就比較不會有「冒牌者症候群」的狀況出現了。</p>
<ul>
<li>No pain, No gain.</li>
</ul>
<p>要高薪、要賺錢,我認為沒有不辛苦的事,高薪的原理很簡單:<strong>不可取代性高</strong>,再看看那些輕鬆的文書工作,幾乎是大學生剛畢業就可以做的,想當然薪水就不會高到哪去。</p>
<p>這讓筆者想到以前有一位高中老師很有料也很有趣,高中要考大學嘛,很多高中生一定問過老師要選什麼科系啊?讀哪裡啊?</p>
<p>應該也有不少學生直接問老師,「老師老師,什麼科系出來最賺錢?」</p>
<p>那名老師回:「我也不知道。」</p>
<p>學生們愣住了。</p>
<p>接著老師繼續說:「如果你知道,或是你之後知道了,拜托回來告訴我,老師也想知道。」<br />
「以前能賺錢不代表現在能賺錢,現在能賺錢不代表未來能賺錢。」<br />
「如果你對教學或是當老師有興趣的可以再來找我聊~」<br />
「但不管哪個科系,上大學唸書都只會更辛苦,你真的想好了再去唸,不要讓浪費你父母的錢!」<br />
「這一生中不會有陌生人無條件的養你,除了父母,而父母,說穿了也只是有血緣關系的陌生人。」</p>
<p>總地來說,那名老師影響我很多,各方面都是,例如閱讀吧,他常常跟我們分享一些書,像是:《密祕》就是聽到他介紹過!</p>
<ul>
<li>回顧今年我有達到我的目標嗎?</li>
</ul>
<p>現在轉職完 2 年多一點了,有達到 100 k 嗎?</p>
<p>老實說只能算接近,之前( 12 月初吧),面了一間雖說是大間的博奕公司,但 Offer 下來超越了我的目標一小段,本來真的有打算要去(為什要跟錢錢過不去 QQ),但提離職的時候被老闆留下了,也談了很久吧,應該談了 3 個小時有吧,雖然前面比較像在講公司前景,主要是公司的產品比起博奕來說,我還是比較喜歡一點,總之明年(2024)3 月開始會再調薪,然後會讓我嘗試當 Front-end team leader,那因為還是小新創,還沒真的開始賺錢,反正紅利、分紅什麼的我就先當沒有,但新的調薪合約有簽保密,但如果有順利領好領滿的話,各方面來計算的話,應該算有達到吧。</p>
<p>還是回歸到「可取代性」的問題,現在團隊總共就 2 個前端,我是其中之一,我算是做比較核心的部分,當然也是比較累(也可能是我自己覺得而已),但後來老闆願意加薪留我時,看來老闆還是有在看你付出的多少,我也鬆了一口氣的感覺,目前來說不算很高薪的工作,但為未的可能性比較高,就想說再拼一下看看~</p>
<p>不要怕累或是多做,因為 <strong>你多做的也許不會馬上體現出來,但都是你未來的籌碼</strong> ,老闆或是主管有眼的話會懂的,筆者入職以來都是帶著這樣的信念,開發到位並快速,主動挑戰困難的部分,往往結果都是這樣,都以某種方式變成你的籌碼,履試不爽,大概是這樣的一個故事。</p>
<center>
<img src="https://hackmd.io/_uploads/r1B4xt2D6.png" class="post-image-width" style="width: 480px" alt="2024/03 新 Offer" />
</center>
<blockquote>
<p>目前大概是這樣,其他部分就不方便透露了</p>
</blockquote>
<pre class="language-bash"><code class="language-bash"><span class="token variable">$_</span> to be continued? <span class="token punctuation">(</span>yes/no<span class="token punctuation">)</span><br /><span class="token variable">$_</span><br />$ <span class="token function">yes</span><br />$ <span class="token punctuation">..</span>.<br />$ <span class="token punctuation">..</span><br />$ <span class="token builtin class-name">.</span></code></pre>
<h2 id="%E9%97%9C%E6%96%BC%E6%95%88%E7%8E%87"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/14-2023-wrapped/#%E9%97%9C%E6%96%BC%E6%95%88%E7%8E%87">#</a> 關於效率</h2>
<ul>
<li>為了效率,我做了什麼事?</li>
</ul>
<p>我先承認,首先 <strong>我買了不少鍵盤</strong>。</p>
<p>雖然跟技術比較沒相關,但我覺得真的很值得。目前來說首推還是工程師開發神器:HHKB (Happy Hacking Keyboard,錢錢可接受的話,無腦買這個就對了)</p>
<p>但因為筆者想要更客製化一點,所以買了差不多 Layout 的器械鍵盤,偶爾交替使用。</p>
<p>只要聽到「客。製。化。」三個字就知道水很深,身為工程師,我相信也很害怕聽到這三個字,筆者這邊就不深入聊聊這個領域了(汗)。</p>
<center>
<img src="https://hackmd.io/_uploads/SyH4MF2wa.png" class="post-image-width" style="width: 480px" alt="Keychron Q60" />
</center>
<blockquote>
<p>筆者目前使用的鍵盤:Keychron Q60 (像是會發光的 HHKB !)</p>
</blockquote>
<ul>
<li>一些 "很冷門但高效" 的技術</li>
</ul>
<p>為了效率也學習了 <code>Vim</code>、<code>嘸蝦米輸入法</code>,這兩個技術讓我在做筆記、寫文件時都效率飛快,在規劃一些流程時,也都先打在 HackMD 上面了,因為 HackMD 除了跨平台外,還有 Vim 模式!有 Vim 模式!有 Vim 模式!因為很重要所以說三次。</p>
<blockquote>
<p>延伸閱讀:有興趣的讀者們,可以參加之前的 <a href="https://blog.errorbaker.tw/posts/benben/13-boshiamy/">嘸蝦米文章</a></p>
</blockquote>
<p>編輯器來說,不外乎還是 <code>VScode</code>,曾經跳到 <code>NeoVim</code> 去玩一下,但設定真的太煩瑣,加上很多 Plugin 還是只有 <code>VScode</code> 上才有,又回到 <code>VScode</code> 中,也在 <code>VScode</code> 中裝了 Vim Plugin ,之前是沒有 <code>NeoVim</code> 的支援的,但後來有了!在 <code>VScode</code> 中也可以像 Vim/NeoVim 一樣快速開發,但又多了 Plugin 生態系的支持,到此為止,我的痛點都被滿足了!</p>
<p>但是小孩子才做選擇,我全都要!目前來說我編輯器會這樣使用:</p>
<ol>
<li>較大型的專案 => <code>VScode</code></li>
<li>較小型的專案(如:刷題、筆記等) => <code>NeoVim</code></li>
</ol>
<p>這邊推薦一些我覺得還不錯又提升效率的插件、App,一些常見的我就不再推了,大家看了都煩了吧?</p>
<blockquote>
<p>VScode 插件</p>
</blockquote>
<ul>
<li><a href="https://console-ninja.com/">Console Ninja</a>: <code>console.log</code> 大師的好夥伴,使用 <code>console.log</code> 並不可恥,可恥的是你的畫面切來切去,就為了找一個 <code>console.log('hi')</code>。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=adpyke.codesnap">CodeSnap</a>: 截一些 Code 圖的好用工具,讓有口說不清的工程師仔更有說服力,爛 Code 看起來都沒這麼爛了。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=kisstkondoros.vscode-gutter-preview">Image Preview</a>: 可以在 VScode 中快速查看圖片縮圖。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=vscodevim.vim">Vim</a>: 身為 Vim 的使用者,一定要推一下 VScode 中的 Vim 插件啊(X)</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=Llam4u.nerdtree">NERDTree</a>: Vim 相關的功能,可以用類似 <a href="https://github.com/preservim/nerdtree">Vim nerd tree</a> 在 VScode 中操作檔案</li>
</ul>
<blockquote>
<p>Browser 插件</p>
</blockquote>
<ul>
<li><a href="https://github.com/polywock/globalSpeed">GlobalSpeed</a>: 在瀏覽器中設定播放速度,如:Netflix 最高只有 1.5 倍,可以客製化更快 or 更慢的倍速,看一些教學超級方便,因為有的平台甚至沒有可以調倍速選項</li>
<li><a href="https://darkreader.org/">Dark Reader</a>: 強制深色模式,在看一些沒有深色模式的時候很好用,也有很多客製化選項</li>
<li><a href="https://chromewebstore.google.com/detail/picture-in-picture-any-si/fgopnhbjlphjjcfbapfcbakjekpffkff">Picture-in-Picture any site</a>: 讓 <strong>所有網站</strong> 都可以使用 Picture in Picture 的模式,一般都只有影片的,但這個讓一些做筆記情況的更方便了</li>
<li><a href="https://chromewebstore.google.com/detail/vimium/dbepggeogbaibhgnhhndojpepiihcmeb">Vimium</a>: 可能讓你在瀏覽器中使用 Vim 的方式瀏覽網站</li>
</ul>
<blockquote>
<p>App (這邊會是 Mac 為主的)</p>
</blockquote>
<ul>
<li><a href="https://app.warp.dev/referral/VLL959">免費 | Warp</a>: 超級好用的 terminal ,開箱即用,可以參考筆者之前 <a href="https://blog.errorbaker.tw/posts/benben/12-warp/">寫的文章</a></li>
<li><a href="https://www.raycast.com/">免費 | Raycast</a>: Mac 超強的生產力工具,有超多功能,也支援本地開發,預計之後會寫一篇文章(應該吧)</li>
<li><a href="https://github.com/dexterleng/homerow">免費 | Homerow</a>: 也是 Vim 相關的操作工具,免費版大概每 50 次會跳出付費的提示,但不影響使用(佛系揪團中,有想買的讀者可以私訊我 😂)</li>
<li><a href="https://xnapper.com/">付費 | Xnapper</a>: 截圖神器,跟截歪的、醜醜的截圖說掰掰</li>
<li><a href="https://devutils.com/">付費 | DevUtils</a>: 綜合的開發小工具,如:Regex, 各種 beautify, 各種轉換等等</li>
</ul>
<blockquote>
<p>PS. 這兩個付費工具都是筆者黑五買的,半價入手,有興趣的讀者不訪再等等看有沒有活動吧!</p>
</blockquote>
<p>用起來差不多會像這樣的感覺:</p>
<center>
<p><a href="https://www.youtube.com/watch?v=qp7LPUcYWmE"><img src="https://img.youtube.com/vi/qp7LPUcYWmE/0.jpg" alt="No mouse demo" /></a></p>
</center>
<blockquote>
<p>一開始在 Discord 中;切換到 Edge 並搜尋 "HHKB";然後再回來 Discord 中,特定的留言上按了特定的表情符號(整個過程中沒有動到任何滑鼠!)</p>
</blockquote>
<h2 id="%E9%97%9C%E6%96%BC%E5%B9%B3%E8%A1%A1"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/14-2023-wrapped/#%E9%97%9C%E6%96%BC%E5%B9%B3%E8%A1%A1">#</a> 關於平衡</h2>
<p>感覺不管什麼事都要有個平衡才比較好,也許現在偏向某一邊,但也沒關係,因為每個時期不一樣。</p>
<ul>
<li>工作 vs 生活</li>
</ul>
<p>一直以來,筆者的狀態是:<code>工作(寫程式) >>> 生活</code>。</p>
<p>今年 2 月開始,幾乎禮拜六都加班,有時候禮拜日也加班 ... 但好像也習慣了吧,突然休假也不知道要幹嘛,當這種想法出現的時候,我就覺得我應該要平衡一下了(汗)</p>
<p>想想是也該平衡一下了,跟老闆談新的 Offer 合約的時候,有跟老闆說休假這一塊,是說年後禮拜六可能會雙周加班,不會連繼加班,之後也會再補人分擔一下工作量。</p>
<p>雖然累歸累,但回頭看看,也還好多人還在努力、還在拼命刷題;但與此同時也好多人在躺平(不小心就開了地圖炮)。也許就每個追求的不一樣吧,沒什麼絕對的對或錯。</p>
<p>後來為了生活平衡,前兩個月又回歸玩 Bass 啦,算是今年很開心一件事,打算找點跟程式比較沒相關的事情做平衡一下。常常會問自己拿掉工作、拿掉專業,自己還剩下什麼?大概基於這樣的想法,所以陪養了很多興趣吧。</p>
<center>
<img src="https://hackmd.io/_uploads/BydqHYhDT.png" class="post-image-width" alt="Bass" />
</center>
<blockquote>
<p>以前高中大學都是熱音的,繼大二後首次回歸 Bass !有團缺 Bass 手的嗎?(X</p>
</blockquote>
<p>大概就繼續這樣半躺吧,也挺好的,希望之後是可以多躺平一點啦。</p>
<ul>
<li>打工人心態 vs 拼事業心態</li>
</ul>
<p>其實這個心態上的差異,是我進這份工作比較有感的一塊,上面有提到這份工作會常常加班,但我加班的時候,老闆、合伙人也都在,因為這是他們的事業,他們是拼事業的心態。</p>
<p>有些人是工作做得差不多了,時間到就馬上下班,也許工作一直換是會加薪沒錯,但換了可能就會從零開始,常常會是 5 年經驗,但是是 1 年經驗 Repeat 5 次的 5 年經驗,這種就算是「打工人」心態。</p>
<p>但是拼事業的心態就不一樣了,可能是 1 年時間,就讓你有 5 倍經驗,因為會碰到很多問題,就算資源不足下你必須想辨法克服,也是個高挑戰、高成長的機會,你問會累嗎?當然會啊!但撐過去就是你的。</p>
<p>也確實常常聽到老闆跟合伙人身體出狀況,要去醫院一下之類的,回來又馬上跟我們開會、工作,我只能說創業真的不容易啊!</p>
<p>但是做到掛病號真的有點太跨張了,大陸那邊很多網路公司真的很多 「996、007」 的,真的有點恐怖。突然覺得我可能單純當個加加班的社畜好像還好一點了(並沒有)。</p>
<ul>
<li>最後</li>
</ul>
<p>真的如同上述所說凡事求個緣,不是,是求個「平衡」。</p>
<p>感覺很多事都待要保持平衡,才可以走的長久,平衡的藝術啊!</p>
<p>工作、轉職、生活、練琴各方面亦是。</p>
<p>總之很感謝在軟體開發又度過了一年,也許職涯前幾年也比較有想法跟熱情可以寫,就趁還有東西可以分享的時候多分享一點吧!</p>
<p>感謝看到最後的你!再來一個小廢物 VScode 插件 - <a href="https://marketplace.visualstudio.com/items?itemName=JohnHarrison.bongocat-buddy">Bongo cat</a>: 陪我寫 Code、Debug 的好夥伴!</p>
<blockquote>
<p>延伸閱讀:<a href="https://zh.wikipedia.org/wiki/%E9%82%A6%E9%BC%93%E8%B2%93">Wiki | Bongo cat</a></p>
</blockquote>
<h2 id="ref"><a class="direct-link" href="https://blog.errorbaker.tw/posts/benben/14-2023-wrapped/#ref">#</a> Ref</h2>
<ul>
<li>VScode 插件
<ul>
<li><a href="https://console-ninja.com/">Console Ninja</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=adpyke.codesnap">CodeSnap</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=kisstkondoros.vscode-gutter-preview">Image Preview</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=vscodevim.vim">Vim</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=Llam4u.nerdtree">NERDTree</a></li>
<li><a href="https://github.com/preservim/nerdtree">Vim nerd tree</a></li>
</ul>
</li>
<li>Browser 插件
<ul>
<li><a href="https://github.com/polywock/globalSpeed">GlobalSpeed</a></li>
<li><a href="https://darkreader.org/">Dark Reader</a></li>
<li><a href="https://chromewebstore.google.com/detail/picture-in-picture-any-si/fgopnhbjlphjjcfbapfcbakjekpffkff">Picture-in-Picture any site</a></li>
<li><a href="https://chromewebstore.google.com/detail/vimium/dbepggeogbaibhgnhhndojpepiihcmeb">Vimium</a></li>
</ul>
</li>
<li>App
<ul>
<li><a href="https://app.warp.dev/referral/VLL959">免費 | Warp</a></li>
<li><a href="https://www.raycast.com/">免費 | Raycast</a></li>
<li><a href="https://github.com/dexterleng/homerow">免費 | Homerow</a></li>
<li><a href="https://xnapper.com/">付費 | Xnapper</a></li>
<li><a href="https://devutils.com/">付費 | DevUtils</a></li>
</ul>
</li>
<li><a href="https://hulitw.medium.com/working-at-onedegree-378a0401130e">Huli - 金融保險科技公司 OneDegree 工作半年心得 | Medium</a></li>
<li><a href="https://blog.errorbaker.tw/posts/benben/12-warp/">Benben - 嘸蝦米輸入法</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=JohnHarrison.bongocat-buddy">VScode 插件 - Bongo cat</a></li>
</ul>
<blockquote>
<p>免責聲名</p>
</blockquote>
<p>以上均為筆者自身經驗,難免小有主觀意見,供讀者們參考,也歡迎分享經驗交流。<br />
如果有錯誤的地方還請大大們指正,筆者會立刻修改,再次感謝大家!</p>
<p><a href="https://creativecommons.org/licenses/by/4.0/"><img src="https://i.creativecommons.org/l/by/4.0/88x31.png" alt="Creative Commons License" /></a></p>
<p>本著作係採用 <a href="https://creativecommons.org/licenses/by/4.0/">創用 CC 姓名標示 4.0 國際授權條款</a> 授權。您可以在 <a href="https://benben.me/">benben.me</a> 找到我。</p>
<p>This work is licensed under a <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>. You can find me at <a href="https://benben.me/">benben.me</a></p>