🔍 搜尋結果:'

🔍 搜尋結果:'

用Github Page整理作品集

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

改善重用性:Vue Composables 的推薦技巧&Design Patterns

本文將分為三個部分: 1. 通用設計模式 2. 我的建議 3. 進一步閱讀 享受並且讓我知道您在專案中使用的模式和實踐🚀 原文出處:https://dev.to/jacobandrewsky/good-practices-and-design-patterns-for-vue-composables-24lk ## 通用設計模式 我認為了解建置可組合項模式的最佳來源實際上是 Vue.js 文件,您可以在[此處](https://vuejs.org/guide/reusability/composables.html)查看 ### 基本可組合項 Vue 文件顯示了以下 useMouse 可組合項的示例: ``` // mouse.js import { ref, onMounted, onUnmounted } from 'vue' // by convention, composable function names start with "use" export function useMouse() { // state encapsulated and managed by the composable const x = ref(0) const y = ref(0) // a composable can update its managed state over time. function update(event) { x.value = event.pageX y.value = event.pageY } // a composable can also hook into its owner component's // lifecycle to setup and teardown side effects. onMounted(() => window.addEventListener('mousemove', update)) onUnmounted(() => window.removeEventListener('mousemove', update)) // expose managed state as return value return { x, y } } ``` 稍後可以在元件中使用它,如下所示: ``` <script setup> import { useMouse } from './mouse.js' const { x, y } = useMouse() </script> <template>Mouse position is at: {{ x }}, {{ y }}</template> ``` ### 異步可組合項 為了獲取資料,Vue 建議使用以下可組合結構: ``` import { ref, watchEffect, toValue } from 'vue' export function useFetch(url) { const data = ref(null) const error = ref(null) watchEffect(() => { // reset state before fetching.. data.value = null error.value = null // toValue() unwraps potential refs or getters fetch(toValue(url)) .then((res) => res.json()) .then((json) => (data.value = json)) .catch((err) => (error.value = err)) }) return { data, error } } ``` 然後可以在元件中使用它,如下所示: ``` <script setup> import { useFetch } from './fetch.js' const { data, error } = useFetch('...') </script> ``` ### 可組合合約 根據上面的示例,以下是所有可組合項都應遵循的約定: 1. 可組合文件名應以 use 開頭,例如 `useSomeAmazingFeature.ts` 2. 它可以接受輸入參數,這些參數可以是字串等基本類型,也可以接受 refs 和 getter,但需要使用 toValue 幫助器 3. Composable 應該返回一個 ref 值,該值可以在解構可組合項後存取,例如 `const { x, y } = useMouse()` 4. 可組合項可以保存可以在整個應用程式中存取和修改的全局狀態。 5. 可組合性可能會產生副作用,例如加入窗口事件偵聽器,但在卸載元件時應清除它們。 6. 可組合項只能在 `<script setup>` 或 `setup()` 掛鉤中呼叫。它們也應該在這些上下文中同步呼叫。在某些情況下,您還可以在生命週期掛鉤中呼叫它們,例如“onMounted()”。 7. 可組合項可以呼叫內部的其他可組合項。 8. 可組合項應在內部包裝某些邏輯,當過於復雜時,應將它們提取到單獨的可組合項中以便於測試。 ## 我的建議 我已經為我的工作專案和開源專案建置了多個可組合項 - NuxtAlgolia、NuxtCloudinary、NuxtMedusa,因此基於這些,我想根據我的經驗在上面的合同中加入一些要點。 ### 有狀態或/和純函數可組合項 在程式碼標準化的某個時刻,您可能會得出這樣的結論:您希望對可組合項中的狀態保留做出決定。 最容易測試的函數是那些不存儲任何狀態的函數(即它們是簡單的輸入/輸出函數),例如負責將字節轉換為人類可讀值的可組合函數。它接受一個值並返回一個不同的值 - 它不存儲任何狀態。 不要誤會我的意思,你不必做出“或”的決定。您可以完全保留有狀態和無狀態可組合項。但這應該是一個書面決定,以便以後更容易與他們合作 🙂 ### 可組合項的單元測試 我們希望使用 Vitest 為我們的前端應用程式實施單元測試。在後端工作時,進行單元測試程式碼覆蓋率非常有用,因為您主要關注邏輯。然而,在前端,您通常使用視覺效果。 因此,我們認為對整個元件進行單元測試可能不是最好的主意,因為我們基本上將對框架本身進行單元測試(如果按下按鈕,檢查狀態是否更改或模式是否打開)。 由於我們已將所有業務邏輯移至可組合項(基本上是 TypeScript 函數)內,因此它們很容易使用 Vitest 進行測試,並且允許我們擁有更穩定的系統。 ### 可組合項的範圍 不久前,在 VueStorefront 中,我們開發了自己的可組合方法(早在它們實際上像這樣被呼叫之前)。在我們的方法中,我們使用可組合項來映射電子商務的業務領域,如下所示: ``` const { cart, load, addItem, removeItem, remove, ... } = useCart() ``` 這種方法絕對有用,因為它允許將域包裝在一個函數中。在“useProduct”或“useCategory”等更簡單的示例中,實現和維護相對簡單。然而,正如您在此處的“useCart”示例中看到的那樣,當包裝一個包含更多邏輯而不僅僅是資料獲取的域時,這個可組合項正在發展成為一種非常難以開發和維護的形狀。 此時,我開始為 Nuxt 生態系統做出貢獻,其中引入了不同的方法。在這種新方法中,每個可組合項僅負責一件事。因此,我們的想法不是建置一個巨大的“useCart”可組合項,而是為每個功能建置可組合項,即“useAddToCart”、“useFetchCart”、“useRemovefromCart”等。 因此,維護和測試這些可組合項應該更容易 🙂 ## 進一步閱讀 這將是我的研究的全部內容。如果您想了解有關此主題的更多訊息,請務必查看以下文章: * https://vuejs.org/guide/reusability/composables.html * https://www.youtube.com/watch?v=bcZM3EogPJE * https://vueschool.io/articles/vuejs-tutorials/what-is-a-vue-js-composable/ * https://blog.logrocket.com/getting-started-vue-composables/ * https://macopedia.com/blog/news/how-can-vue-3-composables-make-your-life-easier

如何在微前端 Micro Frontends 架構中處理 CSS

如何在微前端中處理 CSS?畢竟,樣式始終是*任何* UI 片段所需要的東西,但是,它也是全局共享的東西,因此是潛在的衝突來源。 在這篇文章中,我想回顧一下現有的不同策略來馴服 CSS 並使其擴展以開發微前端。如果這裡的任何內容對您來說聽起來很合理,那麼也可以考慮研究一下[“微前端的藝術”](https://microfrontends.art/)。 **本文的程式碼可以在[github.com/piral-samples/css-in-mf](https://github.com/piral-samples/css-in-mf)找到。請務必查看示例實現。** CSS 的處理是否會影響每個微前端解決方案?讓我們檢查可用的類型來驗證這一點。 原文出處:https://dev.to/florianrappl/css-in-micro-frontends-4jai ## 微前端的類型 過去我寫了很多關於存在哪些類型的微前端、為什麼存在以及何時應該使用什麼類型的微前端架構的文章。採用 Web 方法意味著使用 iframe 來使用來自不同微前端的 UI 片段。在這種情況下,沒有任何限制,因為無論如何每個片段都是完全隔離的。 在任何其他情況下,無論您的解決方案使用客戶端還是伺服器端組合(或介於兩者之間的東西),您最終都會得到在瀏覽器中評估的樣式。因此,在所有其他情況下,您都會關心 CSS。讓我們看看這裡有哪些選項。 ## 無特殊處理 好吧,第一個 - 也許是最(或根據觀點,最不)明顯的解決方案是不進行任何特殊處理。相反,每個微前端都可以附帶額外的樣式表,然後在渲染微前端的元件時附加這些樣式表。 理想情況下,每個元件僅在首次渲染時加載所需的樣式,但是,由於這些樣式中的任何一個都可能與現有樣式衝突,我們也可以假裝在微前端的任何元件渲染時加載所有“有問題的”樣式。 ![衝突](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fewl99l6y04edyjnni2j.png) 這種方法的問題在於,當給出諸如“div”或“div a”之類的通用選擇器時,我們還將重新設置其他元素的樣式,而不僅僅是原始微前端的片段。更糟糕的是,類和屬性也不是故障保護措施。像“.foobar”這樣的類也可以在另一個微前端中使用。 **您將在引用的演示存儲庫中找到兩個衝突的微前端的示例,網址為 [solutions/default](https://github.com/piral-samples/css-in-mf/tree/main/solutions)。** 擺脫這種痛苦的一個好方法是進一步隔離元件 - 就像 Web 元件一樣。 ## 影子 DOM 在自定義元素中,我們可以打開一個影子根來將元素附加到專用的迷你文件,該迷你文件實際上與其父文件屏蔽。總的來說,這聽起來是一個好主意,但與這裡介紹的所有其他解決方案一樣,沒有硬性要求。 ![Shadow DOM](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xp18uw9sxffq1l7oj6mc.png) 理想情況下,微前端可以自由決定“如何”實現元件。因此,實際的 Shadow DOM 集成必須由微前端完成。 使用 Shadow DOM 有一些缺點。最重要的是,雖然 Shadow DOM 內部的樣式保留在內部,但全局樣式也不會影響 Shadow DOM。乍一看,這似乎是一個優勢,但是,由於整篇文章的主要目標只是隔離微前端的樣式,因此您可能會錯過諸如應用某些全局設計系統(例如 Bootstrap)之類的要求。 要使用 Shadow DOM 進行樣式設置,我們可以通過“link”引用或“style”標籤將樣式放入 Shadow DOM 中。由於 Shadow DOM 是無樣式的,並且外部的樣式不會傳播到其中,因此我們實際上需要它。除了編寫一些內聯樣式之外,我們還可以使用捆綁器將“.css”(或者類似“.shadow.css”的內容)視為原始文本。這樣,我們只會得到一些文本。 對於 esbuild,我們可以配置 `piral-cli-esbuild` 的預製配置,如下所示: ``` module.exports = function(options) { options.loader['.css'] = 'text'; options.plugins.splice(0, 1); return options; }; ``` 這會刪除初始 CSS 處理器 (SASS) 並為“.css”文件配置標準加載器。現在,shadow DOM 中的某些樣式的工作方式如下: ``` import css from "./style.css"; customElements.define(name, class extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); } connectedCallback() { this.style.display = "contents"; const style = this.shadowRoot.appendChild(document.createElement('style')); style.textContent = css; } }); ``` 上面的程式碼是一個有效的自定義元素,從樣式角度來看它是透明的(“display:contents”),即只有其內容會反映在渲染樹中。它託管一個包含單個“style”元素的影子 DOM。 `style` 的內容設置為 `style.css` 文件的文本。 **您將在 [`solutions/shadow-dom`](https://github.com/piral-samples/css-in-mf/tree/main) 引用的演示存儲庫中找到兩個衝突的微前端的示例/解決方案/shadow-dom)。** 域元件避免使用影子 DOM 的另一個原因是,並非每個 UI 框架都能夠處理影子 DOM 中的元素。因此,無論如何都必須尋找替代解決方案。一種方法是轉而使用一些 CSS 約定。 ## 使用命名約定 如果每個微前端都遵循全局 CSS 約定,那麼就可以在元級別上避免衝突。最簡單的約定是在每個類前面加上微前端的名稱。因此,舉例來說,如果一個微前端稱為“shopping”,另一個微前端稱為“checkout”,那麼兩者都會將其“active”類分別重命名為“shopping-active”/“checkout-active”。 ![約定](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6l8r1s59v1dcu3yp022u.png) 這同樣適用於其他可能存在衝突的名稱。舉個例子,在微前端稱為“shopping”的情況下,我們將其稱為“shopping-primary-button”,而不是像“primary-button”這樣的ID。如果由於某種原因,我們需要設置元素的樣式,我們應該使用後代選擇器(例如“.shopping img”)來設置“img”標籤的樣式。現在,這適用於具有“shopping”類的 *some* 元素中的“img”元素。這種方法的問題是購物微前端也可能使用其他微前端的元素。如果我們看到“div.shopping > div.checkout img”怎麼辦?儘管“img”現在由通過“checkout”微前端帶來的元件託管/集成,但它的樣式將由“shopping”微前端 CSS 設計。這並不理想。 儘管命名約定在一定程度上解決了問題,但它們仍然容易出錯並且使用起來很麻煩。如果我們重命名微前端會怎樣?如果微前端在不同的應用程式中獲得不同的名稱怎麼辦?如果我們在某些時候忘記應用命名約定怎麼辦?這就是工具幫助我們的地方。 ## CSS 模塊 自動引入一些前綴並避免命名衝突的最簡單方法之一是使用 CSS 模塊。根據您選擇的捆綁器,這可以是開箱即用的,也可以通過一些配置更改來實現。 ![CSS 模塊](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o49rdmokn5kvn8ncjjrt.png) ``` // Import "default export" from CSS import styles from './style.modules.css'; // Apply <div className={styles.active}>Active</div> ``` 導入的模塊是一個生成的模塊,保存將其原始類名(例如“active”)映射到生成的類名的值。生成的類名通常是 CSS 規則內容與原始類名混合的哈希值。這樣,名稱應該盡可能唯一。 作為示例,讓我們考慮使用“esbuild”建置的微前端。對於“esbuild”,您需要一個插件(“esbuild-css-modules-plugin”)和相應的配置更改以包含 CSS 模塊。 使用 Piral 我們只需要調整 `piral-cli-esbuild` 已經帶來的配置。我們刪除標準 CSS 處理(使用 SASS)並用插件替換: ``` const cssModulesPlugin = require('esbuild-css-modules-plugin'); module.exports = function(options) { options.plugins.splice(0, 1, cssModulesPlugin()); return options; }; ``` 現在我們可以在程式碼中使用 CSS 模塊,如上所示。 CSS 模塊有一些缺點。首先,它附帶了一些標準 CSS 的語法擴展。這對於區分我們想要導入的樣式(因此要進行預處理/哈希)和應保持原樣的樣式(即稍後在不導入的情況下使用)是必要的。另一種方法是將CSS直接帶入JS文件中。 ## CSS-in-JS CSS-in-JS 最近的名聲很差,但是,我認為這是一個誤解。我也更喜歡將其稱為“CSS-in-Components”,因為它為元件本身帶來了樣式。一些框架(Astro、Svelte 等)甚至允許通過其他方式直接執行此操作。經常被提及的缺點是性能 - 這通常是由於在瀏覽器中編寫 CSS 造成的。然而,這並不總是必要的,在最好的情況下,CSS-in-JS 庫實際上是建置時間驅動的,即沒有任何性能缺陷。 ![CSS-in-JS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/36w7yuksmdzgstastjxv.png) 然而,當我們談論 CSS-in-JS(或 CSS-in-Components)時,我們需要考慮現有的各種選項。為簡單起見,我只包含三個:情感、樣式元件和香草提取物。讓我們看看它們如何幫助我們在將微前端整合到一個應用程式中時避免衝突。 ### Emotion Emotion 是一個非常酷的庫,它附帶了 React 等框架的幫助程序,但沒有將這些框架設置為先決條件。情感可以很好地優化和預先計算,並允許我們使用可用的 CSS 技術的完整庫。 使用“純粹”情感相當容易;首先安裝包: ``` npm i @emotion/css ``` 現在您可以在程式碼中使用它,如下所示: ``` import { css } from '@emotion/css'; const tile = css` background: blue; color: yellow; flex: 1; display: flex; justify-content: center; align-items: center; `; // later <div className={tile}>Hello from Blue!</div> ``` `css` 幫助器允許我們編寫被解析並放置在樣式表中的 CSS。返回值是生成的類的名稱。 如果我們特別想使用 React,我們還可以使用 Emotion 中的 jsx 工廠(引入了一個名為 css 的新標準 prop)或 styled 幫助器: ``` npm i @emotion/react @emotion/styled ``` 現在感覺很像樣式是 React 本身的一部分。例如,“styled”幫助器允許我們定義新元件: ``` const Output = styled.output` border: 1px dashed red; padding: 1rem; font-weight: bold; `; // later <Output>I am groot (from red)</Output> ``` 相比之下,“css”輔助屬性使我們能夠稍微縮短符號: ``` <div css={` background: red; color: white; flex: 1; display: flex; justify-content: center; align-items: center; `}> Hello from Red! </div> ``` 總而言之,這會生成不會衝突的類名,並提供避免樣式混合的穩健性。 “styled”助手尤其受到流行的“styled-components”庫的啟發。 ### 樣式元件 “styled-components”庫可以說是最流行的 CSS-in-JS 解決方案,並且常常是此類解決方案聲譽不佳的原因。從歷史上看,這實際上是在瀏覽器中編寫 CSS 的全部內容,但在過去幾年中,他們確實極大地推進了這一點。今天,您也可以對所使用的樣式進行一些非常好的伺服器端組合。 與“emotion”相比,安裝(針對 React)需要更少的軟體包。唯一的缺點是打字是事後才想到的 - 所以你需要安裝兩個包才能完全喜歡 TypeScript: ``` npm i styled-components --save npm i @types/styled-components --save-dev ``` 安裝後,該庫就已經完全可用: ``` import styled from 'styled-components'; const Tile = styled.div` background: blue; color: yellow; flex: 1; display: flex; justify-content: center; align-items: center; `; // later <Tile>Hello from Blue!</Tile> ``` 其原理與“情感”相同。因此,讓我們探索另一種選擇,嘗試從一開始就實現零成本,而不是事後的想法。 ### Vanilla Extract 我之前寫的關於利用類型更接近元件(並避免不必要的執行時成本)的內容正是最新一代 CSS-in-JS 庫所涵蓋的內容。最有前途的庫之一是“@vanilla-extract/css”。 使用該庫有兩種主要方式: - 與您的捆綁器/框架集成 - 直接使用 CLI 在此示例中,我們選擇前者 - 並將其集成到“esbuild”。為了使集成正常工作,我們需要使用“@vanilla-extract/esbuild-plugin”包。 現在我們將其集成到建置過程中。使用 `piral-cli-esbuild` 配置,我們只需將其加入到配置的插件中: ``` const { vanillaExtractPlugin } = require("@vanilla-extract/esbuild-plugin"); module.exports = function (options) { options.plugins.push(vanillaExtractPlugin()); return options; }; ``` 為了使 Vanilla Extract 正常工作,我們需要編寫 `.css.ts` 文件,而不是普通的 `.css` 或 `.sass` 文件。這樣的文件可能如下所示: ``` import { style } from "@vanilla-extract/css"; export const heading = style({ color: "blue", }); ``` 這都是有效的 TypeScript。我們最終會得到一個類名的導出 - 就像我們從 CSS 模塊、Emotion 中得到的一樣 - 你明白了。 所以最後,上面的樣式將像這樣應用: ``` import { heading } from "./Page.css.ts"; // later <h2 className={heading}>Blue Title (should be blue)</h2> ``` 這將在建置時完全處理——而不是執行時成本。 您可能會感興趣的另一種方法是使用 CSS 實用程序庫,例如 Tailwind。 ## CSS 實用程序,例如 Tailwind 這是一個獨立的類別,但我認為既然 Tailwind 是這個類別中的主要工具,我將只介紹 Tailwind。 Tailwind 的主導地位甚至達到了甚至有人問“你寫 CSS 還是 Tailwind?”之類的問題。這與 jQuery 在 DOM 操作領域的統治地位非常相似。 2010 年,人們問“這是 JavaScript 還是 jQuery?”。 無論如何,使用 CSS 實用程序庫的優點是根據使用情況生成樣式。這些樣式不會衝突,因為實用程序庫始終以相同的方式定義它們。因此,每個微前端將僅附帶實用程序庫中根據需要顯示微前端所需的部分。 ![Tailwind](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8waj7k3823oq77qhkzmb.png) 如果使用 Tailwind 和 esbuild,我們還需要安裝以下軟體包: ``` npm i autoprefixer tailwindcss esbuild-style-plugin ``` esbuild的配置比之前複雜一點。 `esbuild-style-plugin` 本質上是 esbuild 的 PostCSS 插件;所以必須正確配置: ``` const postCssPlugin = require("esbuild-style-plugin"); module.exports = function (options) { const postCss = postCssPlugin({ postcss: { plugins: [require("tailwindcss"), require("autoprefixer")], }, }); options.plugins.splice(0, 1, postCss); return options; }; ``` 在這裡,我們刪除了默認的 CSS 處理插件 (SASS),並將其替換為 PostCSS 插件 - 使用 PostCSS 的“autoprefixer”和“tailwindcss”擴展。 現在我們需要加入一個有效的 *tailwind.config.js* 文件: ``` module.exports = { content: ["./src/**/*.tsx"], theme: { extend: {}, }, plugins: [], }; ``` 這本質上是配置 Tailwind 的最低要求。它只是提到應該掃描 `tsx` 文件以了解 Tailwind 實用程序類的使用情況。然後找到的類將被放入 CSS 文件中。 因此,CSS 文件還需要知道生成/使用的聲明應包含在哪裡。至少我們只有以下 CSS: ``` @tailwind utilities; ``` 還有其他“@tailwind”指令。例如,Tailwind 帶有重置和基礎層。然而,在微前端中,我們通常不關心這些層。這屬於應用程式 shell 或編排應用程式的關注範圍 - 而不是域應用程式。 然後,CSS 將被替換為 Tailwind 中已指定的類: ``` <div className="bg-red-600 text-white flex flex-1 justify-center items-center">Hello from Red!</div> ``` ## 比較 迄今為止提出的幾乎每種方法都是微前端的可行競爭者。一般來說,這些溶液也可以混合。一個微前端可以採用影子 DOM 方法,而另一個微前端則對 Emotion 感到滿意。第三個圖書館可能會選擇使用香草精。 最後,唯一重要的是所選擇的解決方案是無碰撞的,並且不會帶來(巨大的)執行時成本。雖然某些方法比其他方法更有效,但它們都提供了所需的樣式隔離。 |方法|遷移工作|可讀性|穩健性|性能影響| | ----------- | -------------- | -------------- | ---------- | ------------------ | |大會 |中等|高|低|無 | | CSS 模塊 |低|高|中等|從無到低| |影子 DOM |低到中|高|高|低| | JS 中的 CSS |高|中到高|高|從無到高| |順風|高|中等|高|無 | 性能影響很大程度上取決於實施。例如,對於 CSS-in-JS,如果解析和組合在執行時完全完成,您可能會產生很大的影響。如果樣式已經預先解析但僅在執行時組合,則影響可能很小。如果使用像香草精這樣的解決方案,您基本上不會產生任何影響。 對於 Shadow DOM,主要的性能影響可能是 Shadow DOM 內部元素的投影或移動(本質上為零)以及“style”標籤的重新評估。然而,這是相當低的,甚至可能會產生一些性能優勢,給定的樣式總是切中要害,並且僅專用於要在影子 DOM 中顯示的某個元件。 在示例中,我們有以下捆綁包大小: |方法|索引 [kB] |頁碼 [kB] |表 [kB] |總體 [kB] |尺寸 [%] | | ----------------- | ---------- | --------- | ----------- | ------------ | ------ | |默認| 1.719 | 1.719 1.203 | 1.203 0.245 | 0.245 3.167 | 3.167 100% | |大會 | 1.761 | 1.761 1.241 | 1.241 0.269 | 0.269 3.271 | 3.271 103% | | CSS 模塊 | 2.149 | 2.149 2.394 | 2.394 0 | 4.543 | 4.543 143% | |影子 DOM | 10.044 | 10.044 1.264 | 1.264 0 | 11.308 | 11.308 357% | |情感| 1.670 | 1.670 1.632 | 1.632 25.785 | 25.785 29.087 | 29.087 918% | |樣式元件 | 1.618 | 1.618 1.612 | 1.612 63.073 | 63.073 66.303 | 66.303 2093% | |香草精 | 1.800 | 1.800 1.257 | 1.257 0.314 | 0.314 3.371 | 3.371 106% | |順風| 1.853 | 1.853 1.247 | 1.247 0.714 | 0.714 3.814 | 3.814 120% | 對這些數字持保留態度,因為在情感和样式元件的情況下,執行時可以(並且可能甚至應該)共享。另外,給定的示例微前端確實很小(所有 UI 片段的總體大小為 3kB)。對於更大的微前端,增長肯定不會像這裡描述的那麼問題。 Shadow DOM 解決方案的大小增加可以通過我們提供的簡單實用腳本來解釋,該腳本可以輕鬆地將現有的 React 渲染包裝到 Shadow DOM 中(無需生成新樹)。如果這樣的實用程序是集中共享的,那麼其大小將更接近其他更輕量級的解決方案。 ## 結論 在微前端解決方案中處理 CSS 並不困難 - 只需從一開始就以結構化和有序的方式完成,否則就會出現衝突和問題。一般來說,建議選擇 CSS 模塊、Tailwind 或可擴展的 CSS-in-JS 實現等解決方案。

簡單說明 JavaScript 中常看到的 Strict Mode 字串

在 JavaScript 中,提供了一種稱為“嚴格模式”的功能,該功能在 ECMAScript 5 (ES5) 中引入,可幫助開發人員避免常見的 JavaScript 陷阱。在本文中,我們將深入探討什麼是嚴格模式、如何啟用它以及它提供的好處。 原文出處:https://dev.to/accreditly/understanding-javascript-strict-mode-4e3j ## 什麼是嚴格模式? 嚴格模式是一種選擇受限的 JavaScript 變體的方式。在嚴格模式下,JavaScript 通過將它們更改為拋出錯誤來消除一些 JavaScript 靜默錯誤。它修復了使 JavaScript 引擎難以執行優化的錯誤,並禁止了一些可能在未來版本的 ECMAScript 中定義的語法。 ## 啟用嚴格模式 要在 JavaScript 中啟用嚴格模式,您可以使用字串“use strict”。這可以針對整個腳本或在單個函數中完成。 對於整個腳本: ``` "use strict"; var v = "Hello, I'm in strict mode!"; ``` 對於單個函數: ``` function strictFunc() { "use strict"; var v = "Hello, I'm in strict mode inside a function!"; } ``` `"use strict"` 指令只能在腳本或函數的開頭辨識。 ## 使用嚴格模式的好處 嚴格模式以兩種方式提供幫助: 1. 它捕捉常見的編碼錯誤和“不安全”的行為。 例如,變數必須用 `var`、`let` 或 `const` 聲明。未聲明的變數將導致錯誤。 ``` "use strict"; x = 3.14; // This will cause an error because x is not declared ``` 2. 它防止使用未來的 ECMAScript 保留字。例如: ``` "use strict"; var let = "Hello"; // This will cause an error because "let" is a reserved word in ES6 ``` 3. 它簡化了 eval() 和參數。 在嚴格模式下,在 eval() 語句中聲明的變數不會在周圍範圍內建立變數。 ``` "use strict"; eval("var x = 10;"); // This will cause an error because x is not defined outside the scope of eval() console.log(x); ``` 4. 對於不是方法或構造函數的函數,它將 this 限制為未定義。 在非嚴格模式下,`this` 將默認為全局物件,瀏覽器上下文中的`window`。 ``` "use strict"; function myFunction() { console.log(this); // Will output: undefined } myFunction(); ``` 使用嚴格模式可以幫助您捕獲否則會被靜默忽略的錯誤。它還有助於防止您使用可能有問題的語法和做出低效的編碼決策。嚴格模式可以使您的 JavaScript 程式碼更加健壯和可維護,最好的做法是使用“use strict”指令啟動您的腳本。

7 個進階、好用的 TypeScript 技巧用法分享

**TypeScript** 是一種出色的工具,可以讓我們的生活更輕鬆並避免 **bug**,但有時使用起來感覺不知所措。 本文概述了專業人士使用的 7 個 **TypeScript** 技巧,它們將使您的生活更輕鬆。 原文出處:https://dev.to/ruppysuppy/7-secret-typescript-tricks-pros-use-3ckg ## 1. 型別推理 **Typescript** 足夠聰明,可以在您幫助縮小資料類型時**推斷資料類型**。 ``` enum CounterActionType { Increment = "INCREMENT", IncrementBy = "INCREMENT_BY", } interface IncrementAction { type: CounterActionType.Increment; } interface IncrementByAction { type: CounterActionType.IncrementBy; payload: number; } type CounterAction = | IncrementAction | IncrementByAction; function reducer(state: number, action: CounterAction) { switch (action.type) { case CounterActionType.Increment: // TS infers that the action is IncrementAction // & has no payload return state + 1; case CounterActionType.IncrementBy: // TS infers that the action is IncrementByAction // & has a number as a payload return state + action.payload; default: return state; } } ``` 如上所示,**TypeScript** 根據 `type` 屬性推斷操作的類型,因此您無需檢查 `payload` 是否存在。 ## 2. 字串類型 通常你需要一個變數的特定值,這就是**文字類型**派上用場的地方。 ``` type Status = "idle" | "loading" | "success" | "error"; ``` 它也適用於數字: ``` type Review = 1 | 2 | 3 | 4 | 5; // or better yet: const reviewMap = { terrible: 1, average: 2, good: 3, great: 4, incredible: 5, } as const; // This will generate the same type as above, // but it's much more maintainable type Review = typeof reviewMap[keyof typeof reviewMap]; ``` ## 3.類型守衛 **類型保護** 是另一種縮小變數類型的方法: ``` function isNumber(value: any): value is number { return typeof value === "number"; } const validateAge = (age: any) => { if (isNumber(age)) { // validation logic // ... } else { console.error("The age must be a number"); } }; ``` 注意:在上面的示例中,最好使用: ``` const validateAge = (age: number) => { // ... }; ``` 這個例子是為了展示 **type guards** 是如何工作的一個簡化。 ## 4.索引簽名 當對像中有**動態鍵**時,可以使用**索引簽名**來定義其類型: ``` enum PaticipationStatus { Joined = "JOINED", Left = "LEFT", Pending = "PENDING", } interface ParticipantData { [id: string]: PaticipationStatus; } const participants: ParticipantData = { id1: PaticipationStatus.Joined, id2: PaticipationStatus.Left, id3: PaticipationStatus.Pending, // ... }; ``` ## 5.泛型 **泛型** 是一個強大的工具,可以讓您的程式碼更易於重用。它允許您**定義一個類型,該類型將由您的函數的使用決定**。 在以下示例中,`T` 是通用類型: ``` const clone = <T>(object: T) => { const clonedObject: T = JSON.parse(JSON.stringify(object)); return clonedObject; }; const obj = { a: 1, b: { c: 3, }, }; const obj2 = clone(obj); ``` ## 6. 不可變類型 您可以通過加入 `as const` 使您的類型**不可變**。這確保您不會意外更改值。 ``` const ErrorMessages = { InvalidEmail: "Invalid email", InvalidPassword: "Invalid password", // ... } as const; // This will throw an error ErrorMessages.InvalidEmail = "New error message"; ``` ## 7. 部分、選擇、省略和必需類型 通常在使用 **server** 和 **local data** 時,您需要將一些屬性設置為 **optional** 或 **required**。 而不是使用相同資料的略微更改版本定義數百個接口。您可以使用 `Partial`、`Pick`、`Omit` 和 `Required` 類型來做到這一點。 ``` interface User { name: string; age?: number; email: string; } type PartialUser = Partial<User>; type PickUser = Pick<User, "name" | "age">; type OmitUser = Omit<User, "age">; type RequiredUser = Required<User>; // PartialUser is equivalent to: // interface PartialUser { // name?: string; // age?: number; // email?: string; // } // PickUser is equivalent to: // interface PickUser { // name: string; // age?: number; // } // OmitUser is equivalent to: // interface OmitUser { // name: string; // email: string; // } // RequiredUser is equivalent to: // interface RequiredUser { // name: string; // age: number; // email: string; // } ``` 當然,你可以使用 **intersection** 來組合它們: ``` type A = B & C; ``` 其中 `B` 和 `C` 是任何類型。 --- 謝謝閱讀,希望對您有幫助!

給軟體工程師:50 個好用的 ChatGPT 咒語指令

下列 ChatGPT-4 咒語,對開發者很有幫助。附上原文與中文指令,供您參考。 原文出處:https://dev.to/hackertab_org/50-chat-gpt-prompts-every-software-developer-should-know-tested-9al ### **程式碼生成** - Generate a boilerplate `[language]` code for a `[class/module/component]` named [name] with the following functionality: `[functionality description].` - 為名為 [name] 的 `[class/module/component]` 生成樣板 `[language]` 程式碼,具有以下功能:`[functionality description]。 - Create a [language] function to perform `[operation]` on `[data structure]` with the following inputs: [input variables] and expected output: `[output description]`. - 建立一個 [語言] 函數以使用以下輸入對 `[資料結構]` 執行 `[操作]`:[輸入變數] 和預期輸出:`[輸出描述]`。 - Generate a `[language]` class for a `[domain]` application that includes methods for `[methods list]` and properties `[properties list]`. - 為包含“[方法列表]”的方法和屬性“[屬性列表]”的“[域]”應用程式生成一個“[語言]”類。 - Based on the [design pattern], create a code snippet in [language] that demonstrates its implementation for a [use case]. - 基於[設計模式],用[語言]建立一個程式碼片段,演示其對[用例]的實現。 **例子:** ``` Generate a boilerplate Python code for a shopping cart module named "ShoppingCart" with the following functionality: - A constructor that initializes an empty list to store cart items. - A method called "add_item" that takes in an item object and adds it to the cart. - A method called "remove_item" that takes in an item object and removes it from the cart if it exists. - A method called "get_items" that returns the list of items in the cart. - A method called "get_total" that calculates and returns the total price of all items in the cart. ``` ### **程式碼完成** - In `[language]`, complete the following code snippet that initializes a [data structure] with `[values]`: `[code snippet]`. - 在“[語言]”中,完成以下使用“[值]”初始化[資料結構]的程式碼片段:“[程式碼片段]”。 - Finish the `[language]` function that calculates [desired output] given the following input parameters: `[function signature]`. - 在給定以下輸入參數的情況下完成計算[期望輸出]的[語言]函數:[函數簽名]。 - Complete the `[language]` code to make an API call to `[API endpoint]` with [parameters] and process the response: `[code snippet]`. - 完成“[語言]”程式碼以使用[參數]對“[API 端點]”進行 API 呼叫並處理響應:“[程式碼片段]”。 **Example** : Finish the Python function that calculates the average of a list of numbers given the following input parameters: **示例**:完成計算給定以下輸入參數的數字列表的平均值的 Python 函數: ``` def calculate_average(num_list) ``` ### **錯誤檢測** - Identify any potential bugs in the following [language] code snippet: `[code snippet]`. - 確定以下 [語言] 程式碼片段中的任何潛在錯誤:`[程式碼片段]`。 - Analyze the given [language] code and suggest improvements to prevent [error type]: `[code snippet]`. - 分析給定的[語言]程式碼並提出改進建議以防止[錯誤類型]:`[程式碼片段]`。 - Find any memory leaks in the following [language] code and suggest fixes: `[code snippet]`. - 在以下 [語言] 程式碼中查找任何內存洩漏並提出修復建議:`[程式碼片段]`。 **Example** : Identify any potential bugs in the following Python code snippet: **示例**:辨識以下 Python 程式碼片段中的任何潛在錯誤: ``` def calculate_sum(num_list): sum = 0 for i in range(len(num_list)): sum += num_list[i] return sum ``` ### **程式碼審查** - Review the following `[language]` code for best practices and suggest improvements: `[code snippet]`. - 查看以下“[語言]”程式碼以獲得最佳實踐並提出改進建議:“[程式碼片段]”。 - Analyze the given `[language]` code for adherence to `[coding style guidelines]`: `[code snippet]`. - 分析給定的“[語言]”程式碼是否符合“[編碼風格指南]”:“[程式碼片段]”。 - Check the following [language] code for proper error handling and suggest enhancements: `[code snippet]`. - 檢查以下 [語言] 程式碼以正確處理錯誤並提出改進建議:`[程式碼片段]`。 - Evaluate the modularity and maintainability of the given `[language]` code: `[code snippet]`. - 評估給定“[語言]”程式碼的模塊化和可維護性:“[程式碼片段]”。 **Example** : Review the following Python code for best practices and suggest improvements: **示例**:查看以下 Python 程式碼以獲得最佳實踐並提出改進建議: ``` def multiply_list(lst): result = 1 for num in lst: result *= num return result ``` ### **API 文件生成** - Generate API documentation for the following `[language]` code: `[code snippet]`. - 為以下“[語言]”程式碼生成 API 文件:“[程式碼片段]”。 - Create a concise API reference for the given `[language]` class: `[code snippet]`. - 為給定的“[語言]”類建立簡明的 API 參考:“[程式碼片段]”。 - Generate usage examples for the following `[language]` API: `[code snippet]`. - 為以下“[語言]”API 生成用法示例:“[程式碼片段]”。 **Example** : Generate API documentation for the following JavaScript code: **示例**:為以下 JavaScript 程式碼生成 API 文件: ``` /** * Returns the sum of two numbers. * @param {number} a - The first number to add. * @param {number} b - The second number to add. * @returns {number} The sum of a and b. */ function sum(a, b) { return a + b; } ``` ### **查詢優化** - Optimize the following SQL query for better performance: `[SQL query]`. - 優化以下 SQL 查詢以獲得更好的性能:`[SQL 查詢]`。 - Analyze the given SQL query for any potential bottlenecks: `[SQL query]`. - 分析給定的 SQL 查詢是否存在任何潛在瓶頸:`[SQL 查詢]`。 - Suggest indexing strategies for the following SQL query: `[SQL query]`. - 為以下 SQL 查詢建議索引策略:`[SQL 查詢]`。 - Optimize the following NoSQL query for better performance and resource usage: `[NoSQL query]`. - 優化以下 NoSQL 查詢以獲得更好的性能和資源使用:`[NoSQL 查詢]`。 **Example** : Optimize the following SQL query for better performance: **示例**:優化以下 SQL 查詢以獲得更好的性能: ``` SELECT * FROM orders WHERE order_date BETWEEN '2022-01-01' AND '2022-12-31' ORDER BY order_date DESC LIMIT 100; ``` ### **用戶界面設計** - Generate a UI mockup for a `[web/mobile]` application that focuses on [`user goal or task]`. - 為專注於 [`用戶目標或任務]` 的`[web/mobile]` 應用程式生成 UI 模型。 - Suggest improvements to the existing user interface of `[app or website]` to enhance `[usability, accessibility, or aesthetics]`. - 建議改進“[應用程式或網站]”的現有用戶界面,以增強“[可用性、可存取性或美學]”。 - Design a responsive user interface for a `[web/mobile]` app that adapts to different screen sizes and orientations. - 為適應不同螢幕尺寸和方向的“[web/mobile]”應用程式設計響應式用戶界面。 **Example** : Generate a UI mockup for a mobile application that focuses on managing personal finances. **示例**:為專注於管理個人財務的移動應用程式生成 UI 模型。 ### **自動化測試** - Generate test cases for the following [language] function based on the input parameters and expected output: `[function signature]`. - 根據輸入參數和預期輸出為以下 [語言] 函數生成測試用例:`[函數簽名]`。 - Create a test script for the given [language] code that covers [unit/integration/system] testing: `[code snippet]`. - 為涵蓋[單元/集成/系統]測試的給定[語言]程式碼建立測試腳本:`[程式碼片段]`。 - Generate test data for the following [language] function that tests various edge cases: `[function signature]`. - 為以下測試各種邊緣情況的[語言]函數生成測試資料:`[函數簽名]`。 - Design a testing strategy for a [web/mobile] app that includes [unit, integration, system, and/or performance] testing. - 為 [網絡/移動] 應用程式設計測試策略,包括 [單元、集成、系統和/或性能] 測試。 **Example:** Generate test cases for the following Python function based on the input parameters and expected output: **示例:** 根據輸入參數和預期輸出為以下 Python 函數生成測試用例: ``` def divide(a: float, b: float) -> float: if b == 0: raise ZeroDivisionError('division by zero') return a / b ``` ### **程式碼重構** - Suggest refactoring improvements for the following [language] code to enhance readability and maintainability: `[code snippet]`. - 建議對以下 [語言] 程式碼進行重構改進,以增強可讀性和可維護性:`[程式碼片段]`。 - Identify opportunities to apply [design pattern] in the given [language] code: `[code snippet]`. - 確定在給定的[語言]程式碼中應用[設計模式]的機會:`[程式碼片段]`。 - Optimize the following [language] code for better performance: `[code snippet]`. - 優化以下 [語言] 程式碼以獲得更好的性能:`[程式碼片段]`。 **Example** : Optimize the following Python code for better performance: **示例**:優化以下 Python 程式碼以獲得更好的性能: ``` def find_max(numbers): max_num = numbers[0] for num in numbers: if num > max_num: max_num = num return max_num ``` ### **設計模式建議** - Based on the given [language] code, recommend a suitable design pattern to improve its structure: `[code snippet]`. - 根據給定的[語言]程式碼,推薦合適的設計模式來改進其結構:`[程式碼片段]`。 - Identify opportunities to apply the [design pattern] in the following [language] codebase: `[repository URL or codebase description]`. - 確定在以下 [語言] 程式碼庫中應用 [設計模式] 的機會:`[存儲庫 URL 或程式碼庫描述]`。 - Suggest an alternative design pattern for the given [language] code that may provide additional benefits: `[code snippet]`. - 為可能提供額外好處的給定 [語言] 程式碼建議替代設計模式:`[程式碼片段]`。 **Example:** Based on the given Python code, recommend a suitable design pattern to improve its structure: **例子:** 根據給定的Python程式碼,推薦合適的設計模式來改進其結構: ``` class TotalPriceCalculator: def calculate_total(self, items): pass class NormalTotalPriceCalculator(TotalPriceCalculator): def calculate_total(self, items): total = 0 for item in items: total += item.price * item.quantity return total class DiscountedTotalPriceCalculator(TotalPriceCalculator): def calculate_total(self, items): total = 0 for item in items: total += item.price * item.quantity * 0.9 # apply 10% discount return total class Order: def __init__ (self, items, total_price_calculator): self.items = items self.total_price_calculator = total_price_calculator def calculate_total(self): return self.total_price_calculator.calculate_total(self.items) class Item: def __init__ (self, name, price, quantity): self.name = name self.price = price self.quantity = quantity ``` ### **算法開發** - Suggest an optimal algorithm to solve the following problem: `[problem description]`. - 建議解決以下問題的最佳算法:`[問題描述]`。 - Improve the efficiency of the given algorithm for `[specific use case]`: `[algorithm or pseudocode]`. - 為“[特定用例]”提高給定算法的效率:“[算法或偽程式碼]”。 - Design an algorithm that can handle `[large-scale data or high-throughput]` for `[specific task or operation]`. - 為“[特定任務或操作]”設計一種可以處理“[大規模資料或高吞吐量]”的算法。 - Propose a parallel or distributed version of the following algorithm to improve performance: `[algorithm or pseudocode]`. - 提出以下算法的並行或分佈式版本以提高性能:`[算法或偽程式碼]`。 ### **程式碼翻譯** - Translate the following `[source language]` code to `[target language]`: `[code snippet]`. - 將以下“[源語言]”程式碼翻譯成“[目標語言]”:“[程式碼片段]”。 - Convert the given `[source language]` class or module to `[target language]` while preserving its functionality and structure: `[code snippet]`. - 將給定的“[源語言]”類或模塊轉換為“[目標語言]”,同時保留其功能和結構:“[程式碼片段]”。 - Migrate the following `[source language]` code that uses `[library or framework]` to [target language] with a similar library or framework: `[code snippet]`. - 將以下使用“[庫或框架]”的“[源語言]”程式碼遷移到具有類似庫或框架的“目標語言”:“[程式碼片段]”。 **Example:** Translate the following Python code to JavaScript: **示例:**將以下 Python 程式碼轉換為 JavaScript: ``` def factorial(n): if n == 0: return 1 else: return n * factorial(n-1) ``` ### **個性化學習** - Curate a list of resources to learn `[programming language or technology]` based on my current skill level: `[beginner/intermediate/advanced]`. - 根據我目前的技能水平,策劃學習`[編程語言或技術]`的資源列表:`[初學者/中級/高級]`。 - Recommend a learning path to become proficient in `[specific programming domain or technology]` considering my background in `[existing skills or experience]`. - 考慮到我在“[現有技能或經驗]”方面的背景,推薦精通“[特定編程領域或技術]”的學習路徑。 - Suggest project ideas or coding exercises to practice and improve my skills in `[programming language or technology]`. - 建議專案想法或編碼練習,以練習和提高我在“[編程語言或技術]”方面的技能。 ### **程式碼可視化** - Generate a UML diagram for the following `[language]` code: `[code snippet]`. - 為以下“[語言]”程式碼生成一個 UML 圖:“[程式碼片段]”。 - Create a flowchart or visual representation of the given `[language]` algorithm: `[algorithm or pseudocode]`. - 建立給定“[語言]”算法的流程圖或可視化表示:“[算法或偽程式碼]”。 - Visualize the call graph or dependencies of the following `[language]` code: `[code snippet]`. - 可視化以下“[語言]”程式碼的呼叫圖或依賴關係:“[程式碼片段]”。 **Example** : Generate a UML diagram for the following Java code: **示例**:為以下 Java 程式碼生成 UML 圖: ``` public abstract class Vehicle { private String model; public Vehicle(String model) { this.model = model; } public String getModel() { return model; } public abstract void start(); public abstract void stop(); } public class Car extends Vehicle { public Car(String model) { super(model); } @Override public void start() { System.out.println("Starting car engine"); } @Override public void stop() { System.out.println("Stopping car engine"); } } public class Motorcycle extends Vehicle { public Motorcycle(String model) { super(model); } @Override public void start() { System.out.println("Starting motorcycle engine"); } @Override public void stop() { System.out.println("Stopping motorcycle engine"); } } ``` ### **資料可視化** - Generate a bar chart that represents the following data: `[data or dataset description]`. - 生成代表以下資料的條形圖:`[資料或資料集描述]`。 - Create a line chart that visualizes the trend in the following time series data: `[data or dataset description]`. - 建立一個折線圖,將以下時間序列資料的趨勢可視化:`[資料或資料集描述]`。 - Design a heatmap that represents the correlation between the following variables: `[variable list]`. - 設計一個表示以下變數之間相關性的熱圖:`[變數列表]`。 --- 以上,簡單分享,希望對您有幫助!

Composer 進階原理:PHP命名空間與PSR-0

上次的說明了PHP套件管理的歷史與社群提出的解決之道,本篇文章接著談類別管理的進階議題。 # 當類別名稱一樣... 當專案大了起來,有時候會有類別名稱重複的問題。 假設今天要撰寫一個論壇模組,提供討論區與留言板功能。 你一定很想將討論區的文章與留言板的文章都命名類別為Article: ``` // BoardArticle.php <?php class Article{ //... } ?> // ForumArticle.php <?php class Article{ //... } ?> ``` 當然了,這麼做會得到一個結果: ``` Fatal error: Cannot redeclare class Article ``` 這種問題有一個簡單的解決辦法,就是加上前綴字。 類別分別命名為ForumArticle與BoardArticle就可以了。 *Q1: 等一下!這個解法好陽春!我看到至少4個問題:* 1. 類別名稱容易變得冗長。 2. 有些類別一開始你以為不會跟人重複,結果之後真的重複了。難道永遠替類別加前綴字? 3. 類別名稱寫Article俐落多了!文章就是概念上的文章,不要逼我告訴你是討論區還是留言板!如果專案用到兩種留言板模組,分別由以前的兩個前輩寫好,難道還要逼我把作者名稱寫進去? ``` class TonyBoardArticle{ //... } class JackBoardArticle{ //... } ``` 4. 如果我在打造框架(framework)呢?幾乎會把所有常見名詞用過一次(像是Request、Loader、Response、Controller、Model等類別)!難道前面全部前綴? 看看 Codeigniter 的原始碼,全部用CI_當作前綴。超醜的。 # 命名空間(namespace)登場 於是PHP從5.3版之後支援了命名空間(namespace)。 所以可以用Article替類別定義了: ``` // BoardArticle.php <?php namespace Board; class Article{ //... } ?> // ForumArticle.php <?php namespace Forum; class Article{ //... } ?> ``` 使用類別時只要加上命名空間即可: ``` //index.php <?php include 'BoardArticle.php'; include 'ForumArticle.php'; $article = new Forum\Article(); $post = new Board\Article(); ``` 如果當前的php檔只用到其中一個Article類別,可以指定當前的php檔只用哪個命名空間+類別的組合: ``` <?php include 'BoardArticle.php'; include 'ForumArticle.php'; use Forum\Article; $article = new Article(); $fArticle = new Forum\Article(); $bArticle = new Board\Article(); ``` 如此一來,當php找不到Article類別時,便會去使用use關鍵字宣告的組合。 當然了,就算用use指定過,原本的宣告方式還是可以用的。(如最下方兩行所示) # 當東西多了起來... OK,可以繼續完成我們的論壇模組了! 討論區跟留言板有各自的文章,再來還需要「推文」: ``` <?php namespace Board; class Comment{ // ... } ?> <?php namespace Forum; class Comment{ // ... } ?> ``` 使用剛剛學到的命名空間去載入他們: ``` <?php // index.php include 'BoardArticle.php'; include 'ForumArticle.php'; include 'BoardComment.php'; include 'ForumComment.php'; $article = new Forum\Article(); $comment = new Forum\Comment(); // ... ``` 果然是漂亮的各種命名阿! *Q2: include有好多行!上次提到了Composer可以解決這種問題,當引入命名空間之後,Composer也能發揮作用嗎?* **是的。** # Composer登場 跟上次初學Composer一樣,建立一個composer.json檔: ``` { "autoload": { "files": [ "ForumArticle.php", "ForumComment.php", "BoardArticle.php", "BoardComment.php" ] } } ``` 注意,上次我們用"classmap"指定資料夾、把資料夾內檔案全掃一次,這次我們用"files"分別設定各個檔案。 再來,在terminal輸入 ``` composer install ``` 執行完畢之後,跟上次一樣,只要載入一個檔案: ``` <?php require 'vendor/autoload.php'; $article = new Forum\Article(); $comment = new Forum\Comment(); ``` 就可以使用各個類別囉! *Q3: 等一下!看起來跟沒有命名空間的時候差不多啊!一樣是把php檔自動require進去而已?* 對啊,你最上面的原始寫法,也只是手動載入好幾個檔案,在載入的時候本來就沒有特別之處: ``` <?php // index.php include 'BoardArticle.php'; include 'ForumArticle.php'; include 'BoardComment.php'; include 'ForumComment.php'; $article = new Forum\Article(); $comment = new Forum\Comment(); // ... ``` 載入php檔就只是載入,跟命名空間是兩回事。 *Q4: 還是不太對啊!上次我用classmap一次把好幾個資料夾內容掃過,這次我用files分別指定每個檔案幹嘛?Composer不是厲害在能指定資料夾去自動掃過?* ......你說的沒錯。 開個my_lib資料夾,把4個php檔都丟進去吧。composer.json這樣寫就好了: ``` { "autoload": { "classmap": [ "my_lib" ] } } ``` 再來,在terminal輸入 ``` composer install ``` 這樣就搞定了! 其實我只是想示範,除了用classmap設定資料夾之外,也可以用files直接指定檔案。 *Q5: OK,原諒你。不過,我必須說,我今天什麼都沒學到。最後還是在composer.json寫classmap而已,跟上次一模一樣。* 是的...我剛說了,載入php檔就只是載入,跟命名空間是兩回事。 我今天介紹的namespace功能是PHP本身提供的。而Composer只是協助你載入的工具、當然不可能改變程式語言本身。 Composer只是幫助你少打那一串require而已。 *Q6: ㄟ等等...有個地方我覺得很醜。我們現在的my_lib資料夾裏面有4個檔案,檔名很醜:* ``` "ForumArticle.php" "ForumComment.php" "BoardArticle.php" "BoardComment.php" ``` 類別名稱本身都是俐落的Article跟Comment了,檔案名稱還是有前綴字。但也不可能有兩個Article.php、兩個Comment.php。你有沒有辦法解決這個美感問題? 這個問題簡單,把那個my_lib資料夾刪掉,開一個Forum資料夾、一個Board資料夾。本來的ForumArticle.php改成Article.php丟進Forum資料夾、本來的BoardArticle.php改成Article.php丟進Board資料夾。composer.json改寫成這樣: ``` { "autoload": { "classmap": [ "Board", "Forum" ] } } ``` 再來,在terminal輸入 ``` composer install ``` 這招不錯吧!檔案名稱就是類別名稱,乾淨俐落! 而且資料夾的名字本身就是namespace的名稱! 以後都這樣做啦!一用到命名空間就開個同名資料夾把檔案丟進去! *Q7: 這招我覺得還好耶...。本來檔案都放在my_lib,我在composer.json只要填my_lib一行就好,現在變成要填兩行。要是我這個論壇模組有一大堆類別、用了一大堆命名空間呢?那我classmap底下不就要填入好幾行?那我寧可全部丟進my_lib,只填my_lib一行!* 唔,這樣說也是有道理。那重新建立my_lib資料夾,把Board跟Forum資料夾丟進my_lib資料夾。 composer.json改回這樣寫: ``` { "autoload": { "classmap": [ "my_lib" ] } } ``` 再來,在terminal輸入 ``` composer install ``` classmap不只是告訴Composer去載入哪幾個資料夾內的檔案,還會把資料夾內的資料夾也全部掃過一次。 怎麼樣,Composer夠神吧。 *Q8: 原來classmap底下會遞迴掃描下去...。我決定了,我的Forum跟Board都是在提供線上討論功能,我決定替我這個模組命名為Discussion。我要在my_lib底下開Discussion資料夾,然後把原本的Forum跟Board資料夾都丟進去。你覺得這個想法如何?* 還不錯。一個Discussion資料夾就是你的整個Discussion模組。 提供了討論區、留言板功能的Discussion模組,我喜歡。 *Q9: 好像忘了什麼...。啊,剛剛說命名空間跟檔案結構符合會最漂亮。那我要把那4個檔案的namespace改成這樣:* ``` <?php namespace Dicussion/Forum; class Article{ //... } ``` 剛剛說了,載入檔案就只是載入檔案,跟命名空間無關。 現在檔案結構沒變,所以我應該不用重新輸入composer install。 讓我試試...。 靠!怎麼噴error了!你騙我? ``` Fatal error: Class 'Discussion\Forum\Comment' not found ``` 呃...,我前面的說法確實有點誤導。 PHP自動載入的基本函式長這樣: ``` void __autoload ( string $class ) ``` 如你所見,PHP至少需要Composer提供資訊指出$class該去哪個檔案找。 namespace改變之後,PHP會找不到對應的$class在哪。 所以還是輸入一下composer install吧!Composer會把需要的資訊整理好的。 *Q10: OKOK,我知道了,我駕馭這一切了。我覺得這個Dicsussion模組真的超屌的,不但命名空間漂亮,連檔案結構都漂亮。我要把這個Discussion資料夾整個丟給我朋友,他們公司最近需要論壇模組。* 讓我打電話給他...。 「什麼?你們已經做好半個論壇模組了?你只需要我模組的其中幾個功能?你們的模組也是放在Discussion資料夾?」 糟糕,資料夾名稱重複了!所以我的模組拿給別人還是有不相容的可能,怎麼辦? 沒有錯..還記得你Q1提到的第3個狀況嗎?確實有把作者名稱加進去的必要! 別怕,我教你。你開一個Tony資料夾,把整個Discussion資料夾丟進去。 接著所有檔案namespace改成像這樣: ``` <?php namespace Tony/Dicussion/Forum; class Article{ //... } ``` 要用的時候就這樣喔: ``` <?php require 'vendor/autoload.php'; $article = new Tony\Discussion\Forum\Article(); $comment = new Tony\Discussion\Forum\Comment(); ``` 是變得有點長啦。 但這下搞定了吧!作者名稱再撞到的話,就改個獨特的名稱就是了! # 終於。讓我們談談PSR-0 你一定常聽到PSR-0對吧! PSR-0是PHP跨框架相容性統一標準組織訂出來的自動載入慣例。 來談談PSR-0幾個最重要的要求吧! * 命名空間加上類別名稱一定要長這樣: ``` {vendor name}\{namespace}\{class name} ``` * 前面一定要是作者名稱 * 中間可以有任意層次的命名空間、最後是類別名稱。 * 中間任意層次的命名空間直接對應到檔案結構。 發現了嗎?在剛剛Q1~Q10的過程中,其實你已經把PSR-0學完了,連設計原理都一起搞懂了。 懂這些之後,你也可以做好自己的模組、發佈到 Packagist 給全世界透過composer下載、使用了! 最後,如果遵守psr-0的話,composer.json可以這樣寫: ``` { "autoload": { "psr-0": { "Tony\\Discussion\\": "my_lib/" } } } ``` 注意,雙引號、兩次反斜線並沒有特別意思,只是 json 規定的格式。 跟classmap一樣都可以完成任務。兩者其實是有差別的...,我們下次再談。 # 結語 Composer工具以及PHP-FIG組織的出現,讓一直以來散落各地的PHP社群開始有集中的趨勢。 換句話說,各社群終於能共享彼此的library了。 然而,如你所見,psr-0不但導致檔案結構容易變得深層,還要求檔案結構必須配合程式碼,這也招致了不少批評。 https://r.je/php-psr-0-pretty-shortsighted-really.html 除此之外,composer autoload內的classmap跟psr-0到底如何分工?效能差異又為何?這些問題也都還在爭論與驗證當中。 http://stackoverflow.com/questions/22803419/why-use-a-psr-0-or-psr-4-autoload-in-composer-if-classmap-is-actually-faster 不過,PSR-0在各框架已被廣泛支援,因此建議你還是需要有所瞭解。 # 最後... 現在已經出現psr-0的替代方案,稱為psr-4。 PSR-0從2014-10-21開始被註明為不建議使用。 至於PSR-4..我們下次再談。

COMPOSER 設計原理與基本用法

相信有在用PHP的朋友近年來常聽到composer這個套件管理工具。 它到底是做什麼用的?又是為了解決什麼問題而存在呢? 要瞭解這個,得先從歷史開始說起...。 # PHP最早讀取套件的方法 初學PHP時,最早會面對的問題之一就是require與include差別何在? require_once與include_once又是什麼? 弄懂這些問題之後,如果不使用framework,直接開發,便常出現類似這樣的code: ``` // whatever.php // 這檔案需要用到幾個類別 require 'xxx_class.php'; require 'yyy_class.php'; require 'zzz_class.php'; // ... ``` 然後在其他檔案會出現: ``` // another.php // 這檔案需要用到幾個類別 require 'yyy_class.php'; require 'zzz_class.php'; // ... ``` 這樣的結果,會產生至少兩個問題: 1. 許多檔案用到同樣幾個class,於是在不同地方都需要載入一次。 2. 當類別多了起來,會顯得很亂、忘記載入時還會出現error。 那麼,不如試試一種懶惰的作法? 寫一個php,負責載入所有類別: ``` // load_everything.php require 'xxx_class.php'; require 'yyy_class.php'; require 'zzz_class.php'; require 'aaa_class.php'; require 'bbb_class.php'; require 'ccc_class.php'; ``` 然後在其他檔案都載入這支檔案即可: ``` require 'load_everything.php' ``` 結果新問題又來了:當類別很多的時候,隨便一個web page都會載入一堆code,吃爆記憶體,怎麼辦呢? # __autoload 為了解決這個問題,PHP 5開始提供__autoload這種俗稱「magic method」的函式。 當你要使用的類別PHP找不到時,它會將類別名稱當成字串丟進這個函式,在PHP噴error投降之前,做最後的嘗試: ```// autoload.php function __autoload($classname) { if ($classname === 'xxx.php'){ $filename = "./". $classname .".php"; include_once($filename); } else if ($classname === 'yyy.php'){ $filename = "./other_library/". $classname .".php"; include_once($filename); } else if ($classname === 'zzz.php'){ $filename = "./my_library/". $classname .".php"; include_once($filename); } // blah } ``` 也因為PHP這種「投降前最後一次嘗試」的行為,有時會讓沒注意到的人困惑「奇怪我的code怎麼跑得動?我根本沒有require啊..」,所以被稱為「magic method」。 如此一來,問題似乎解決了? 可惜還是有小缺點..,就是這個__autoload函式內容會變得很巨大。以上面的例子來說,一下會去根目錄找、一下會去other_library資料夾、一下會去my_library資料夾尋找。在整理檔案的時候,顯得有些混亂。 # spl_autoload_register 於是PHP從5.1.2開始,多提供了一個函式。 可以多寫幾個autoload函式,然後註冊起來,效果跟直接使用__autoload相同。 現在可以針對不同用途的類別,分批autoload了。 ```spl_autoload_register('my_library_loader'); spl_autoload_register('other_library_loader'); spl_autoload_register('basic_loader'); function my_library_loader($classname) { $filename = "./my_library/". $classname .".php"; include_once($filename); } function other_library_loader($classname) { $filename = "./other_library/". $classname .".php"; include_once($filename); } function basic_loader($classname) { $filename = "./". $classname .".php"; include_once($filename); } ``` 每個loader內容可以做很多變化。可以多寫判斷式讓它更智慧、可以進行字串處理...。 自動載入類別的問題終於解決了...。 *但是光上面的code也有15行,而且在每個project一定都會寫類似的東西。有沒有辦法自動產生這15行呢? 我的願望很簡單,我告訴你,反正我有my_library資料夾跟other_library資料夾,你自己進去看到什麼類別就全部載入好不好...?* *阿不對,全部載入剛又說效能不好,那你進去看到什麼就全部想辦法用spl_autoload_register記起來好不好...?* *我懶得打15行了,我只想打這幾個字:* ``` $please_autoload = array( 'my_library', 'other_library'); ``` *可不可以發明一個工具,去吃$please_autoload這個變數,然後自己想辦法載入一切啊...?* *ㄟ等等,我連php程式碼都懶得打了,在web領域JSON格式更簡潔。允許我這樣打,好嗎?* ``` { "autoload": [ "my_library", "other_library" ] } ``` *然後誰來個工具幫我產生一大串autoload相關的php程式碼吧...,可以嗎?* **可以。** # Composer登場 首先,裝好composer(本文不介紹如何安裝。) 再來,建立一個composer.json檔,裏面輸入這些: ``` { "autoload": { "classmap": [ "my_library", "other_library" ] } } ``` 比原本希望的多打了一些字,不過差不多。 再來,在terminal輸入 ``` composer install ``` 執行成功之後,你會看到一個vendor資料夾,內含一個autoload.php。 沒錯,跟你夢想的一樣。你只要載入這個檔案: ``` require 'vendor/autoload.php'; ``` 你需要的所有類別,都會在適當的時候、以適當的方式自動載入。 php再也不會噴error說你「類別尚未定義」了! 這vendor資料夾裏面的一切,都只是php code而已,並沒有特別神奇的地方。只要去看autoload.php的原始碼,就能知道composer到底寫了哪些php code給你。 *ㄟ等等,我寫的類別都放在my_library裏面了,other_library都是網路上copy下來的現成類別。我想要用Google API的Client類別、Doctrine資料庫管理抽象層類別、還有guzzlehttp的發送request類別。* *我連去下載這些檔案、然後丟進這個資料夾都懶得做了,我根本不想手動建立other_library這個資料夾。composer真那麼神...不如連下載都幫我自動下載?可以嗎?* **可以。** 查詢一下那幾個套件在 https://packagist.org/ 的名稱、還有你需要的版本號。 把剛剛的composer.json改成這樣: ``` { "require": { "google/apiclient": "1.0.*@beta", "guzzlehttp/guzzle": "~4.0", "doctrine/dbal": "~2.4" }, "autoload": { "classmap": [ "my_library" ] } } ``` 然後'composer install'指令除了自動載入你的類別之外、還會自動下載你需要的類別、然後自動載入它們。 一樣require 'vendor/autoload.php'就可以了。composer實在是太棒了。 其實composer解決的問題不只這樣。 類別多了起來之後,各種程式語言都提供namespace功能協助分類。 在有namespace的情況下,PHP社群與composer是如何解決自動載入的問題呢? 這些比較進階的內容,下回分曉。

後端 JS 訓練一:第7課 ── 學習 node 匯入/匯出模組

## 課程目標 - 學習 node 匯入/匯出模組 ## 課程內容 教大家怎麼把多個程式組裝起來,變成一個強大的主程式 順便學習 node 裡面自訂模組,匯出、匯入的語法! --- 建立 `task1.js` 放入以下內容 ``` function task1() { console.log('====== start ======') console.log('this is task 1'); console.log('====== finish ======') } module.exports = { task1: task1 }; ``` 建立 `task2.js` 放入以下內容 ``` function task2() { console.log('====== start ======') console.log('this is task 2'); console.log('====== finish ======') } module.exports = { task2: task2 }; ``` 建立 `task3.js` 放入以下內容 ``` function task3() { console.log('====== start ======') console.log('this is task 3'); console.log('====== finish ======') } module.exports = { task3: task3 }; ``` --- 在 node 中,要匯出模組時,只要使用全域變數 `module` 物件的屬性 `exports` 即可,這樣 node 就知道怎麼處理了 我們設計這些 `task` 程式,不再是直接拿來執行的腳本,而是提供特定功能的模組,所以記得把要匯出的任務寫在函式裡面 --- 接著來寫主程式,通常會命名為 index 或 main 我們就建立 `main.js` 吧,放入以下內容 ``` const readlineSync = require('readline-sync'); var module1 = require('./task1'); var module2 = require('./task2'); var module3 = require('./task3'); function showMenu() { const answer = readlineSync.question('您要執行什麼?\n1. 任務1\n2. 任務2\n3. 任務3\n4. 離開\n'); if (answer === '1') { module1.task1(); showMenu(); } else if (answer === '2') { module2.task2(); showMenu(); } else if (answer === '3') { module3.task3(); showMenu(); } else if (answer === '4') { process.exit(); } else { showMenu(); } } showMenu(); ``` 注意看 `require('./task1')` 那個 `.` 是當前檔案路徑的意思,當 require 看到檔案路徑,就會去從檔案路徑匯入模組 如果看起來不是檔案路徑,而是純粹模組名稱 `require('readline-sync')` 那麼 node 就會從 node_modules 資料夾尋找模組 去終端機輸入 ``` node main.js ``` 很好玩吧!這已經很接近實務上會操作的 CLI 應用程式了! ## 課後作業 接續前一課的作業,現在來把之前的多個檔案,打包成一個程式吧! 請將之前的多的程式改寫,讓內容能夠匯出 接著建立一個 `index.js` 檔案,匯入上面提到的所有函式 使用者輸入 `node index.js` 之後,終端機會詢問 ``` 您要執行下列什麼指令? 1. 建立待辦事項 2. 讀取待辦事項 3. 更新待辦事項 4. 刪除待辦事項 5. 離開 ``` 使用者可以輸入數字,程式會分別去執行任務,也就是呼叫前面匯入的對應函式 完成以上任務,你就完成這次的課程目標了!

後端 JS 訓練一:第4課 ── 把 node 非同步設計改寫為同步設計

## 課程目標 - 把 node 非同步設計改寫為同步設計 ## 課程內容 前面我說過,node 有些地方的寫法設計,看起來很怪、很醜 在瀏覽器 js 環境,我們的確不知道用戶何時會點擊按鈕、不知道 AJAX 回應何時會拿到 不能讓網頁整個停下來卡住、等待上述事件發生,所以必須用「非同步程式設計」處理 可是檔案系統模組,絕大多數情況下,你就是要現在就讀取好檔案、讀取完才讓程式接著繼續跑,才有意義 實在沒必要用「非同步程式設計」處理! 取得用戶輸入就更不用說了,程式就是該停在這裡、等用戶輸入完再接著繼續執行,才有意義 何必用「非同步程式設計」處理! 其他程式語言,大多都是「同步程式設計」,例如用 Python 寫同樣程式時 就是一行一行寫下來即可,不用像這邊這樣把函式當參數傳進去 簡單講,javascript 的非同步設計,在網頁畫面上運作時,有其優勢與必要 但是在用來寫腳本時,顯得多餘而不必要 --- 來學習怎麼改寫吧,打開 `read-my-name.js`,改寫成以下內容 ``` var fs = require('fs'); var data = fs.readFileSync('my-name.txt'); console.log("您好," + data); process.exit(0); ``` 執行看看! ``` node read-my-name.js ``` 跑起來一模一樣,但是不是好讀多了? 注意 `fs.readFileSync` 後面的 sync,那就是同步的意思 --- 接著來改寫第二個程式吧,打開 `write-my-name.js`,改寫成以下內容 ``` var fs = require('fs'); const readlineSync = require('readline-sync'); var answer = readlineSync.question('請問您的大名?\n'); fs.writeFileSync('my-name.txt', answer); console.log('儲存成功。'); process.exit(0); ``` 是不是好讀多了? 新函式名稱,就是後面加上 sync 而已,node 官方知道大家的抱怨,所以有提供這些同步函式 但是,這段 code 不能直接執行,因為 `readline-sync` 不是官方內建模組,是社群開發的,需要透過 npm 安裝 怎麼辦? --- NPM 全稱為 Node Package Manager,也就是 Node 套件管理工具 關於 npm 的安裝,網路上有非常多教學,請根據你的作業系統,自己找一套安裝方法 安裝好之後,你就可以使用「終端機」運行 npm 指令 --- 請在終端機輸入 `npm install readline-sync` 即可安裝 `readline-sync` 套件 會多出兩個檔案 `package-lock.json` `package.json` 用來記錄目前專案用到的 npm 套件,你可以打開這兩個檔案看一看 還會多出一個資料夾 `node_modules`,套件本身會下載到這個地方,你可以打開資料夾逛一逛 接著執行看看! ``` node write-my-name.js ``` 跑起來一模一樣,但是好讀多了! ## 課後作業 接續前一課的作業,現在來嘗試「同步」的語法 請把「非同步」的語法改寫,通通改用「同步」的語法 改完之後,程式碼中,應該就不會出現 callback function 了 也就是不會出現「把一段函式定義當作參數傳進另一個函式」這種情況 完成以上任務,你就完成這次的課程目標了!

後端 JS 訓練一:第3課 ── 用 node 寫入檔案內容

## 課程目標 - 用 node 寫入檔案內容 ## 課程內容 來學習一下用 node 寫入檔案的方法 學會這方法,可以用 node 寫腳本處理工作上的文書瑣事 --- 建立 `write-my-name.js` 程式,裡面輸入 ``` var fs = require('fs'); const readline = require('readline').createInterface({ input: process.stdin, output: process.stdout }); readline.question('請問您的大名?\n', function (answer) { fs.writeFile('my-name.txt', answer, function (err) { console.log('儲存成功。'); process.exit(0); }); }); ``` 然後去終端機輸入 ``` node write-my-name.js ``` 你會看到終端機要求你輸入內容,接著會去更新文字檔內容! --- 第一段的 `readline` 模組,是處理 `輸入/輸出` 資訊的模組,後面要呼叫 `createInterface` 並且用全域變數 `process` 來設定輸入、輸出來源 照做就好,背後意義不用細究,能用就好,有興趣自行 google `readline.question` 從終端機請求用戶輸入內容,`\n` 是換行字元,讓畫面更好看而已 注意 `readline.question` 後面還是把函式當參數傳,也就是依然採用「非同步程式設計」 很怪、很醜,對嗎?我同意,這語法設計實在意義不明 又不是在等待 AJAX 回應的同時,用戶可以先做別的事情 這邊就是要等用戶輸入完內容,程式往下走才有意義呀! 後面會教你如何改寫成「同步程式設計」,先照做就好 ## 課後作業 接續前一課的作業,現在來寫「新增」功能 請建立一個 `create.js` 檔案 使用者輸入 `node create.js` 之後,終端機會詢問 ``` 您要新增什麼待辦事項? ``` 使用者可以輸入內容,接著終端機會顯示 ``` 新增事項:XXXXXX ``` 打開 `todos.json` 查看,會看到剛剛輸入的項目出現在裡面 完成以上任務,你就完成這次的課程目標了!

後端 JS 訓練一:第2課 ── 用 node 讀取檔案內容

## 課程目標 - 用 node 讀取檔案內容 ## 課程內容 來學習一下用 node 讀取檔案的方法 學會這方法,可以用 node 寫出簡易的檔案內容分析程式 --- 建立一個 `my-name.txt` 文字檔,在裡面輸入你的名字 然後建立 `read-my-name.js` 程式,裡面輸入 ``` var fs = require('fs'); fs.readFile('my-name.txt', function(err, data) { console.log("您好," + data); process.exit(0); }); ``` 然後去終端機輸入 ``` node read-my-name.js ``` 你會看到一段打招呼的訊息! --- 讓我們逐行說明一下 ``` var fs = require('fs'); ``` 這是載入「檔案系統模組」的意思,`fs` 是 `file system` 的縮寫 `readFile` 函式,第一個參數是檔案名稱,第二個參數是「一個函式定義」 看起來有點怪,其實是「非同步程式設計」的關係 之前在學網頁元素的事件處理時,就是用「非同步」程式設計處理 也就是我不確定「點擊」事件何時會發生,但我先「綁定」好事件發生時要做的任務 還有在網頁呼叫 AJAX 時也是,我不確定「主機回應」何時會拿到,但我先「綁定」好拿到之後要做的任務 這邊 `fs` 的意思就是:我不確定何時會「檔案讀取完畢」,但我先「綁定」好讀取完畢之後,要做的任務 看起來很怪、很醜,這種設計的意義不明,對嗎?我也覺得!後面會教你如何改寫成「同步程式設計」,先照做就好 -- `console.log` 是印出訊息到終端機(語法就跟在瀏覽器環境一樣!) `process.exit(0);` 是結束程式的意思。那個 `process` 是代表當前 node 程式的全域變數 --- 在瀏覽器裡面,無法去操作「檔案系統模組」,否則也太可怕,每個人上網時,都能用 js 亂改電腦上的檔案,資安大問題! 在瀏覽器裡面,也沒有 `process` 這個全域變數 用 node 跑完這段程式,你應該會發現,這段程式拿到網頁環境,是無法執行的! 在瀏覽器、node 跑 js 程式,有很多相似,也有很多相異之處,希望你慢慢抓到兩者的感覺! ## 課後作業 讓我們嘗試開發一個 CLI 版本的待辦管理工具 請建立一個檔案 `todos.json` 並放入以下內容 ``` [ { "title": "去操場跑步" }, { "title": "去市場採購" }, { "title": "找朋友吃飯" } ] ``` 然後建立一個 `read.js` 檔案 使用者輸入 `node read.js` 之後,終端機會去讀取 `todos.json` 並且顯示 ``` 您的待辦事項: #0 去操場跑步 #1 去市場採購 #2 找朋友吃飯 ``` 完成以上任務,你就完成這次的課程目標了! --- 交作業的方法: **方法一** 直接截圖視窗內容,上傳到留言區 **方法二** 開一個新資料夾,用 git 初始化這個資料夾 接下來的作業,都放在這個資料夾,然後上傳 github 然後把 github 專案連結,貼到留言區即可

Git 入門上手教材:第7課 ── 學會處理 git 衝突

## 課程目標 - 學會處理 git 衝突 ## 課程內容 這課來學一下 commit 彼此有衝突,而 git 沒辦法自動合併的情況吧 先挑一個資料夾,把 `my-work-1.html` 裡面隨意加上幾行內容 ``` <p>我的第一個網頁檔案</p> <p>new content a</p> <p>new content b</p> <p>new content c</p> ``` 接著 ``` git add my-work-1.html git commit -m 'new content abc' git push ``` 順利送出! 然後去另一個資料夾,把 `my-work-1.html` 裡面隨意加上幾行內容 ``` <p>我的第一個網頁檔案</p> <p>new content x</p> <p>new content y</p> <p>new content z</p> ``` 接著 ``` git add my-work-1.html git commit -m 'new content xyz' git push ``` 結果失敗了!一如預期,被 git 喊停了 ``` To github.com:howtomakeaturn/my-first-testing-repo.git ! [rejected] main -> main (fetch first) error: failed to push some refs to 'github.com:howtomakeaturn/my-first-testing-repo.git' hint: Updates were rejected because the remote contains work that you do hint: not have locally. This is usually caused by another repository pushing hint: to the same ref. You may want to first integrate the remote changes hint: (e.g., 'git pull ...') before pushing again. hint: See the 'Note about fast-forwards' in 'git push --help' for details. ``` 預期之中,這時應該 pull 對吧? ``` git pull ``` ``` remote: Enumerating objects: 14, done. remote: Counting objects: 100% (12/12), done. remote: Compressing objects: 100% (4/4), done. remote: Total 8 (delta 3), reused 8 (delta 3), pack-reused 0 Unpacking objects: 100% (8/8), 792 bytes | 79.00 KiB/s, done. From github.com:howtomakeaturn/my-first-testing-repo d999c25..2cf01f4 main -> origin/main hint: Pulling without specifying how to reconcile divergent branches is hint: discouraged. You can squelch this message by running one of the following hint: commands sometime before your next pull: hint: hint: git config pull.rebase false # merge (the default strategy) hint: git config pull.rebase true # rebase hint: git config pull.ff only # fast-forward only hint: hint: You can replace "git config" with "git config --global" to set a default hint: preference for all repositories. You can also pass --rebase, --no-rebase, hint: or --ff-only on the command line to override the configured default per hint: invocation. Auto-merging my-work-1.html CONFLICT (content): Merge conflict in my-work-1.html Automatic merge failed; fix conflicts and then commit the result. ``` 在前一課,我們 pull 之後,會被 git 自動把雲端版本、跟本機你剛改過的版本,合併在一起,然後在終端機編輯器內,請你打一段小訊息,備註這次合併 這次,卻沒有進入終端機編輯器內,而是跳出一串訊息! 注意看最後幾行! ``` Auto-merging my-work-1.html CONFLICT (content): Merge conflict in my-work-1.html Automatic merge failed; fix conflicts and then commit the result. ``` 這就是 commit 衝突的情況:兩個 commit 都有改到同樣檔案,雖然先後時間不同,但是 git 不敢直接用新的蓋掉舊的! git status 看一下 ``` Unmerged paths: (use "git add <file>..." to mark resolution) both modified: my-work-1.html ``` 清楚寫出了:both modified,兩邊都修改過! 打開 `my-work-1.html` 看一下 ``` <p>我的第一個網頁檔案</p> <<<<<<< HEAD <p>new content x</p> <p>new content y</p> <p>new content z</p> ======= <p>new content a</p> <p>new content b</p> <p>new content c</p> >>>>>>> 2cf01f4f63f5ea9040610495688f08032761a170 ``` 看起來很嚇人,其實,只是 git 怕你看不清楚,用一種誇飾法,列出衝突的地方而已! HEAD 段落,是你剛提交的 commit 部份 `2cf01f4f63f5ea9040610495688f08032761a170` 的部份呢?則是剛剛從 github 抓下來的 commit 代號! 要如何處理衝突呢?其實,你就決定一下,衝突這幾行,到底要怎麼合併就可以了,然後把 git 誇飾法的地方刪掉 比方說,我決定讓行數交錯呈現吧 ``` <p>我的第一個網頁檔案</p> <p>new content a</p> <p>new content x</p> <p>new content b</p> <p>new content y</p> <p>new content c</p> <p>new content z</p> ``` 改完之後 ``` git add my-work-1.html ``` ``` git commit -m 'handle conflict' ``` 手動合併的 commit,我個人通常習慣訊息就寫 handle conflict ``` git push ``` 大功告成!這就是所謂的 git 衝突處理 ## 課後作業 接續前一課的作業 在上一次的作業,我們嘗試了兩個資料夾,同時 push 送出 commit,導致 git 比對 github 上的雲端版本時,發現有衝突,請你先 pull 再 push 的情況 上次的情況比較單純,因為雖然有衝突,但是 git 一看就知道影響不大,所以 pull 時自動處理了衝突,把事情都安排好了 這次的作業,我們要模擬「遇到衝突,而且 git 無法自動處理衝突」的情況 --- 請按照以下步驟,送出 commit **第一步** 到 `at-home` 資料夾,打開檔案 `about.html` 的內容,原本是 ``` <h1>我是誰</h1> <p>我是XXX,目前在自學網頁開發</p> ``` 請改成 ``` <h1>我是誰</h1> <p>我是XXX,目前在自學網頁開發,目標是成為厲害的前端工程師</p> ``` 然後送出 commit,接著 `git push` 出去 你會看到 github 上面就被更新了 **第二步** 到 `at-laptop` 資料夾,假設你今天出門工作,忘記先 pull 了,也忘記檔案其實你已經改好了 打開檔案 `about.html` 的內容,依然是 ``` <h1>我是誰</h1> <p>我是XXX,目前在自學網頁開發</p> ``` 請改成 ``` <h1>我是誰</h1> <p>我是XXX,目前在自學網頁開發,目標是成為厲害的軟體工程師</p> ``` 然後送出 commit,接著 `git push` 出去 這時 git 會請你先更新,於是你輸入 `git pull` 你會發現 git 表示遇到衝突,無法自動合併,請你處理! 原來有一邊是寫「前端工程師」,另一邊是寫「軟體工程師」 git 無法自動判斷你到底是想要以哪個為準!(其實用哪個都可以吧!) 請處理這個 conflict 衝突,處理完之後 push 到 github 上面 完成以上任務,你就完成這次的課程目標了! --- 交作業的方法: 可以把 github 專案連結,貼到留言區

Git 入門上手教材:第6課 ── 學會 git pull

## 課程目標 - 學會 git pull ## 課程內容 學會把專案上傳 github 了,這次來試試把專案從 github 載下來吧 假設你現在使用另一台新電腦,專案還不在你的電腦上 打開專案頁面 https://github.com/howtomakeaturn/my-first-testing-repo 頁面上有一個 `Code` 的按鈕,點下去有 clone(複製)的指示 在電腦上先移動到別的資料夾底下,使用 clone 指令把專案抓下來 ``` git clone [email protected]:howtomakeaturn/my-first-testing-repo.git ``` 完成之後,會看到在新資料夾底下,有一份一模一樣的專案! --- 在新資料夾底下,把 `my-work-3.html` 裡面隨意加上幾個字,接著 ``` git add my-work-3.html git commit -m 'commit from new folder' git push ``` 打開 github,會在上面看到有被更新! --- 回到之前的「舊資料夾」底下,把 `my-work-4.html` 裡面隨意加上幾個字,接著 ``` git add my-work-4.html git commit -m 'commit from old folder' git push ``` 會發現上傳失敗! ``` To github.com:howtomakeaturn/my-first-testing-repo.git ! [rejected] main -> main (fetch first) error: failed to push some refs to 'github.com:howtomakeaturn/my-first-testing-repo.git' hint: Updates were rejected because the remote contains work that you do hint: not have locally. This is usually caused by another repository pushing hint: to the same ref. You may want to first integrate the remote changes hint: (e.g., 'git pull ...') before pushing again. hint: See the 'Note about fast-forwards' in 'git push --help' for details. ``` 原因是,git 發現,你從兩個不同的地方,分別更新過不同檔案,git 怕你會搞混編輯歷史紀錄 (github 上現在有4筆 commit,目前資料夾上也是4筆 commit,git 無法分辨哪個第4筆是最新的) 所以 git 希望你能先把最新版本抓下來,再送出你的版本,比較不會有爭議! 把新版本抓下來的指令是 ``` git pull ``` 抓完之後,因為 git 自動把雲端版本、跟本機你剛改過的版本,合併在一起了 會請你打一段小訊息,備註這次合併 通常你就使用 `ctrl + x` 或者 `:q` 離開終端機編輯器就可以了 完成之後,使用 `git log` 會看到,現在有6筆 commit 紀錄,最後一筆是剛剛自動合併的 commit ``` Merge branch 'main' of github.com:howtomakeaturn/my-first-testing-repo ``` 使用 `git status` 也會看到 ``` On branch main Your branch is ahead of 'origin/main' by 2 commits. (use "git push" to publish your local commits) nothing to commit, working tree clean ``` git 提示你目前比雲端版本還多了 2 筆 commit(一筆是你剛加的,一筆是關於自動合併的) 使用 `git push` 通通推上去 github 吧! --- 看到這邊你可能會想,剛剛那筆自動合併的 commit 好像有點多餘? 就把 `commit from new folder` 以及 `commit from old folder` 按照時間順序排列,不就好了嗎? 其實,那是因為這兩筆 commit 內容沒有重疊,所以看起來很單純 實務上很多時候,兩筆 commit 會更新到「相同的檔案」之中的「同樣幾行程式碼」 這時 git 如果按照時間順序排列,就會讓「較晚送出 commit 的人」把「較早送出 commit 的人」的工作內容覆蓋掉! 這樣一來,根本沒辦法團隊工作:晚提交的人,會一直破壞掉早提交的內容! 所以 git 一律會多一筆「合併 commit」。平常就自動合併沒問題,有衝突時,就可以在這筆 commit 處理 有點不懂沒關係,我們下一課會練習 ## 課後作業 接續前一課的作業,專案已經傳到 github 了 假設你原本是用家裡的電腦開發,現在你打算帶著筆電,出門到咖啡廳繼續開發 請拿出你的筆電,把 github 上那個專案 `git clone` 到筆電上面 如果你沒有筆電,沒關係,在電腦上另外找個資料夾,用 `git clone` 從 github 抓專案下來 這樣兩個資料夾對應同一個專案,也可以,模擬跨裝置同步專案 --- 現在分別有兩個資料夾,內容一模一樣,我這邊分別用 `at-home` 以及 `at-laptop` 稱呼兩個資料夾 請按照以下步驟,送出 commit **第一步** 到 `at-home` 資料夾,建立一個檔案 `create-this-file-at-home.html` 內容放 ``` <p>我在家裡新增這個檔案</p> ``` 然後送出 commit,接著 `git push` 出去 你會看到 github 上面就被更新了 **第二步** 到 `at-laptop` 資料夾,建立一個檔案 `create-this-file-at-laptop.html` 內容放 ``` <p>我在咖啡廳新增這個檔案</p> ``` 然後送出 commit,接著 `git push` 出去 你會看到錯誤訊息! git 會跟你說,有衝突,請先將資料夾內容下載同步,再更新 **第三步** 所以請輸入 `git pull` 先把資料夾更新到最新版本 接著用 `git log` 看一下,會看到有關 `create-this-file-at-home.html` 那個檔案的 commit **第四步** 這時再 `git push` 出去 你會看到 github 上面就被更新了 完成以上任務,你就完成這次的課程目標了! --- 交作業的方法: 可以把 github 專案連結,貼到留言區

Git 入門上手教材:第2課 ── 學會 git 基本指令

## 課程目標 - 學會 git 基本指令 ## 課程內容 來建立第一個檔案吧,請建立 `my-work-1.html` 並放入以下內容 ``` <p>我的第一個網頁檔案</p> ``` 接著用 `git status` 看看狀況 會看到檔案名稱顯示為「紅色」,並放在「Untracked」訊息下方 這代表 git 已經發現它的存在了 --- 接著來開始追蹤檔案 ``` git add my-work-1.html ``` 然後輸入 `git status` 看看狀況 會看到檔案名稱顯示為「綠色」,並放在「to be committed」訊息下方 這代表 git 已經把它放入暫存區,準備好正式納入歷史紀錄了 --- 來建立第二個檔案試試吧,請建立 `my-work-2.html` 並放入以下內容 ``` <p>我的第二個網頁檔案</p> ``` 接著用 `git status` 看看狀況 會看到新的檔案又顯示為紅色,暫存區的檔案一樣是綠色沒變 接著再繼續追蹤 ``` git add my-work-2.html ``` 然後輸入 `git status` 看看狀況 會看到兩個檔案都顯示為綠色,代表都放入暫存區了 --- 之所以會分成那麼多步驟,是因為開發者寫程式,很像是作家寫小說/漫畫家畫漫畫,交給出版社那樣 首先,你手上會有很多隨手草稿,這些看不順眼可以隨時丟掉,也就是 git 顯示為紅字的部份 接著,你覺得比較滿意的幾張紙會挑出來,放在旁邊,但你不會直接交給出版社,而是先整理在旁邊,也就是 git 顯示為綠字的部份 然後,哪些是滿意的?哪些是草稿?這兩部份你會一直改變心意,也就是有的會從紅色移進綠色,有的會從綠色移進紅色,一直來回修改,到你滿意為止 最後,你才會把這次要交給出版社的那些滿意部份,通通放進牛皮紙袋,一口氣交給出版社,而 git 版本管理就是在模擬這個過程 --- 來正式提交我們的第一次版本紀錄吧! 需要打一段提示訊息,方便日後翻閱,大致知道每個提交的內容 ``` git commit -m 'this is my first commit, i am so happy!' ``` 完成之後,請再輸入 `git status` 看看,會看到又變得乾乾淨淨了,因為目前沒有正在「工作」的稿件 然後輸入 `git log` 看看,應該會看到剛剛那筆提交紀錄囉! --- 讓我們多提交幾次檔案,試試看多次提交的感覺! 先建立新檔案,請建立 `my-work-3.html` 並放入以下內容 ``` <p>我的第三個網頁檔案</p> ``` ``` git add my-work-3.html ``` ``` git commit -m 'add another work' ``` --- 然後再建立新檔案一次,請建立 `my-work-4.html` 並放入以下內容 ``` <p>我的第四個網頁檔案</p> ``` ``` git add my-work-4.html ``` ``` git commit -m 'add one more work' ``` --- 然後輸入 `git log` 看看,應該會看到總共三筆提交紀錄! --- 在 git 專有名詞中,檔案的狀態分為 untracked/unmodified/modified/staged/commited 提交則稱為 commit 不同文章,有時會用不同的稱呼,一開始有點混亂沒關係,久了會慢慢習慣 記得我說的作家與出版社的比喻即可,我會用 `草稿/紅色 vs 滿意區/綠色` 來描述檔案狀態 反正就是「新增檔案 -> 編寫草稿 -> 放進滿意區 -> 最後一口氣把滿意區的所有檔案提交出去」 然後身為作家,就是每個截稿日之前,都重複一次上面這個流程,就這樣而已 學習的過程,養成習慣,經常 `git status` `git log` 看一下,就會更清楚了! ## 課後作業 假設你正要做一個網站,來做自我介紹、經營個人品牌 請繼續用前一課作業的資料夾,用來放整個網站的檔案 然後要模擬實際開發的時候,先寫筆記規劃,接著一直增減檔案的過程 請按照以下順序送出 commit **第一個 commit:** 新增一個 `README.md` 檔案,裡面放入以下內容(實務上,每個專案通常都會放這個檔案,內容是給自己看的專案筆記,不會出現在網站上): ``` # 簡介 這專案用來放我的個人品牌網站內容 # 規格 預計包含簡介、學經歷,等等頁面 ``` **第二個 commit:** 新增一個 `index.html` 檔案,裡面放入以下內容 ``` <h1>我是誰</h1> <p>我是XXX,目前在自學網頁開發</p> <h1>我的學歷</h1> <p>高中:積極高中</p> <p>大學:品質大學</p> ``` **第三個 commit:** 你改變心意,覺得拆成多個頁面比較好看,也比較容易分別追蹤瀏覽次數 請把 `index.html` 檔案的內容清空,改放以下內容 ``` 我是XXX,請參考其它頁面,瞭解更多我的介紹與背景。 ``` 新增 `about.html` 以及 `background.html` 兩個檔案 並且把 `我是誰` 的內容放進 `about.html`,把 `我的學歷` 的內容放進 `background.html` --- 最後,請輸入 `git log`,應該會看到以上三個 commit 的訊息! 完成以上任務,你就完成這次的課程目標了! --- 交作業的方法: 可以把整段 `git log` 顯示的文字內容複製,貼到留言區 也可以直接截圖視窗內 `git log` 的內容,上傳到留言區

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

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

給OOP初學者的建議:先搞懂「資料跟行為在一起」就好,其它的慢慢來

初學者接觸OOP,幾乎都會有以下疑惑: 我到底為什麼要學OOP?OOP解決了什麼問題?書上這些範例就算不用OOP也寫得出來吧? 然後覺得「繼承」、「多型」、「介面」、「抽象類別」等等的名詞很難,覺得OOP很難。 其實這些名詞雖然重要,但對新手來說,本來就很難在一開始就搞懂。 建議先搞懂「資料跟行為在一起」是什麼,以及它的好處在哪,就可以了,其它的慢慢來。 # 什麼叫做「資料跟行為在一起」? 假設我們在開發一個「中英文互助學習網」,鼓勵中文人士與英語人士登入討論。 這個系統的貼文、留言功能會顯示「發文日期」。 發文日期要根據使用者的註冊身份(台灣人、英語人士)顯示不同格式(台灣格式、西方格式)。 下面就以這個日期格式的功能舉例說明「資料跟行為在一起」是什麼意思。 ## 作法一:直接硬寫(不OOP、資料跟行為混在一起) 初學者通常會用最簡單、也最直覺的作法,直接硬寫出來,像這樣: ``` <?php $postDate = '2016-06-02'; # 假設資料庫取出來的發文日期長這樣 if (/* 判斷是否顯示台灣格式 */) { # 轉換成這樣 2016.6.2 $arr = explode('-', $postDate); $year = $arr[0]; $month = $arr[1]; $day = $arr[2]; echo "$year.$month.$day"; } else { // 西方格式 # 轉換成這樣 6/2/2016 $arr = explode('-', $postDate); $year = $arr[0]; $month = $arr[1]; $day = $arr[2]; echo "$month/$day/$year"; } ``` 這種寫法的資料(日期)跟行為(轉換成各種格式)混在一起。 它的優點是寫起來很簡單,缺點則有兩個: * 日期格式的邏輯會重複出現在很多地方,整段code會到處重複出現 * 整段code大概會塞在 `div` 或是 `span` 的裡面,導致它跟HTML混在一起,很亂 ## 作法二:自訂函數(不OOP、資料跟行為沒混在一起) 為了解決作法一遇到的問題,聰明的初學者很快就想到可以用「自訂函數」!就像這樣: ``` <?php function localFormat($date) { $arr = explode('-', $date); $year = $arr[0]; $month = $arr[1]; $day = $arr[2]; return "$year.$month.$day"; } function englishFormat($date) { $arr = explode('-', $date); $year = $arr[0]; $month = $arr[1]; $day = $arr[2]; return "$month/$day/$year"; } $postDate = '2016-06-02'; # 假設資料庫取出來的發文日期長這樣 if (/* 判斷是否為台灣人身份 */) { echo localFormat($postDate); } else { // 英語人士身份 echo englishFormat($postDate); } ``` 這種寫法將行為(轉換成各種格式)用自訂函數給獨立出來,也大幅改善了作法一遇到的問題。 對小型的網頁程式來說,這招非常好用,不但開發快速、簡單,還漂亮地將資料跟行為拆開。 但是程式規模變大之後,為了將各種行為拆出來,會寫出很多自訂函數,類似這樣: ``` ?php function localFormat($param) { // blah blah ... } function englishFormat($param) { // blah blah ... } function someTask($param) { // blah blah ... } function anotherTask($param) { // blah blah ... } function otherTask($param) { // blah blah ... } //... ``` 於是又衍生出三個問題: 1. 像localFormat、englishFormat這樣的函數名稱意義模糊,看不出是處理日期、人名,還是什麼東西的格式 2. 這些自訂函數各有不同的行為,全部放在一起顯得很亂,應該要想辦法分類、整理這些函數 3. 像localFormat、englishFormat這樣的函數,只吃特定格式的參數,最好能跟某種資料的形式綁在一起,以後要改程式時,能讓相關的資料跟行為一起被看到 問題1很好解決,只要替函數名稱加前綴字變成dateLocalFormat、dateEnglishFormat就行了。 問題2也很好解決,只要多開幾個檔案,把相關的函數放進同一個檔案就行了。 問題3就很棘手,資料跟行為拆開之後,如何在概念上又找方法整理在一起? ## 作法三:使用class(OOP、資料跟行為在一起) 正是這些處理資料、整理行為的問題,導致了OOP的誕生: ``` <?php class Date { public $year; public $month; public $day; public function __construct($date) { $arr = explode('-', $date); $this->year = $arr[0]; $this->month = $arr[1]; $this->day = $arr[2]; } public function localFormat() { return $this->year . '.' .$this->month . '.' . $this->day; } public function englishFormat() { return $this->month . '/' .$this->day . '/' . $this->year; } } $postDate = '2016-06-02'; # 假設資料庫取出來的發文日期長這樣 $date = new Date($postDate); if (/* 判斷是否為台灣人身份 */) { echo $date->localFormat(); } else { // 英語人士身份 echo $date->englishFormat(); } ``` OOP的寫法,一次解決了前述三個問題: 問題1 => 現在從類別名稱就可以知道底下方法的意義了 問題2 => 現在相關的函數都整理進同一個類別底下成為方法了 問題3 => 現在資料的形式都統一在constructor處理一次,之後不管新增多少方法都不用處理資料了 這就是所謂的「資料跟行為在一起」,也正是OOP的核心概念。 利用這種方式整理程式碼、寫出一個又一個的類別,可以大幅提昇程式碼的品質。 # 結論 上述的作法一跟作法二並沒那麼糟糕,但確實會帶來一些問題。 對於小型的網頁程式來說,可能還算夠用。 但是隨著程式規模變大,如果將概念上相關的資料跟行為整理在一起,會很有幫助。 實務上也可以先從作法二開始寫起,直到發現某些資料跟行為關係密切,再拉出來整理成類別即可。 至於很多OOP教學會提到的「繼承」、「多型」、「介面」、「抽象類別」等等名詞,一時搞不懂沒有關係,你可能實務上也暫時用不到。之後找時間慢慢搞懂它們的用途就好。 光是知道「將資料跟行為放在一起」的技巧,就能夠開始寫OOP程式碼了。 (註:本篇文章的程式碼純屬教學用途。實務上PHP已經有DateTime類別可以使用,或是用更漂亮的Carbon類別。) ## Q&A Q1:我常常設計一些類別,只有資料沒有行為,聽起來OK嗎? 不OK,這很不OOP,而且沒意義。 乾脆直接用關聯式陣列去表示那些資料就好。 Q2:我常常設計一些類別,只有行為沒有資料,聽起來OK嗎? 這個要看情況,不一定。 但唯一可以確定的是,這種作法很不OOP。 因為OOP的核心是「資料跟行為在一起」。 這也是為什麼你會看到有人明明寫了類別、用了物件,別人卻說「這不夠OOP」。 然後你又會看到像JavaScript這樣連「類別」關鍵字都沒有(ES5以前),卻能夠寫出很OOP程式碼的關係。 判定的標準都是一樣的,而且也就只有這麼一個標準:資料跟行為有沒有在一起。 Q3:一個類別包含的概念是越大越好,還是越小越好? 不一定。不過我們從作法一到作法三的過程,有一個明確目的:希望讓程式碼更好懂。 如果聲稱一個類別包含的概念很大(例:設計LanguageHelpWebsite類別,用來代表「中英文互助學習網」需要的所有功能),那會把幾乎整個網站的所有行為跟資料都放進去,成為所謂的God object。它可沒有讓程式碼更好懂。 相反地,如果聲稱一個類別包含的概念很小(例:分別設計LocalDate、EnglishDate類別),雖然意義可能更精準了,但用一整個Date類別的概念去思考,程式碼會更容易理解,也就是所謂的內聚性(Cohesion)更高。 所以要替OOP就是「資料跟行為在一起」加個但書: 要以方便理解程式為前提,將資料和行為放在一起。

分享最近面試心得

正想分享近期面試心得,站長就先PO文了,哈哈 由於我本身是美術科系畢業,之前也是做包裝和平面設計的工作,所以網頁設計和前端的職缺我都有投,目標是先讓自己進入業界環境。 剛開始蠻挫折的因為沒什麼前端junior職缺,且職缺要求五花八門技術又要有相關工作經驗或是相關科系,工作內容好像什麼都要會,也很多我看不懂的工具... 過一兩週後,開始有人找我,但大多都是網頁設計相關的工作,至於工作內容也很雜,且薪資也不在預期之中。 這幾天陸續面試,其實沒什麼自信且緊張,有些公司面試有筆試,拿到試題腦袋一片空白,平常用電腦打字,現在變成拿筆寫出code或回答問題,最後就把試題會的部分寫一寫,完全看不懂的就跳過,之後面試官來面試我,原本想說試題好多都不會,根本亂寫一通,可能會被洗臉,先做好心理準備XD 結果面試官一題一題有耐心的問我為什麼會這樣回答,不管我答對還是錯面試官都詳細的告訴我釐清觀念,其實考官有說題目都比較刁專有陷阱,因為知道我是初學者,所以也在他預料範圍內,很感謝他們耐心教導,對方也告訴我一些這方面的經驗或關鍵字,真感人! 總之,近期的面試都吸取很多知識和建議,也被面試官提點我的JS部分要在深入學習扎實一點,還有去看看什麼是clean code --- 分享一下面試題目(我比較有印象的) ``` if ([]) { console.log('Y') } else { console.log('N') } //結果為如何 ``` ``` console.log( (0.1 + 0.2) === 0.3 ) //結果為如何 ``` ``` console.log( NaN === NaN ) //結果為如何 ``` ``` console.log(parseInt('123ABC')) //結果為如何 ``` ``` const a = '3' const b = 1 console.log (a+b) //結果為如何 ``` ``` console.log ([...'...'].length) //結果為如何 ``` 還有一系列的`typeof`型別比較問題,但我無法**直覺信心**的直接寫出答案 因為平常都是寫code時順便用`console.log`來確認型別,這部分要再重新複習😥 還有一些`class`和`this`的試題,我完全看不懂也不會😵 先分享這樣,希望近期能收到好結果~~

  近期留言