本文首發於 kuusei.moe, 仍在建設中 (文章內容支持 markdown react, 界面樣式更豐富,歡迎訪問), 同步於 xLog
前情提要#
自從去年換了份工作,已經寫 Angular 一年多了。雖然 Angular 也很好用,但束縛真的太多了,配合 Service/Rxjs 怎麼寫怎麼感覺在寫 JavaScript 的前綴 (, 再不寫寫別的東西,都要不會寫主流前端了.
同時,正好前段時間用 Hexo 寫的 blog 因為太長時間沒維護掛掉了。趁這個機會,學點新東西,從 Angular 的大坑裡跳出來,因此選的技術棧都是和工作上用的不一樣的東西.
前幾天在 v2ex 看到一個討論,發覺 react 有好多新東西。以前雖然也學過一會 react, 但一直都很不喜歡用,就是因為 16 以前那套 class component, jsx 以及 css in js 這些東西都長得很醜,實在是跟優雅沒什麼關係.
但是現在有 tailwindcss 和 函數式組件了,radix/shadcn 這種 headless 組件庫也是真的好用,我在工作上也受夠了羸弱的諸如 v-for/ng-for
這樣的 html 模版了。之前的缺點一轉攻勢,不是消失了,就是變成優點了.
從未來前端發展趨勢來看,完全獨立的組件化我覺得是一個很有趣的方向。最開始的時候,前端 html/css/js 是三分離的,我還記得我剛學那會,如果在 html 寫 inline style 或者 script 都是很被人鄙視的 (不是。之後,隨著像 react (js+html) vue (all in one) 這種框架的興起,前端才有了組件這個叫法.
一些關於 Angular 的不吐不快#
回到 Angular 的視角,寫 Angular 的時候,ivy 的 aot 雖然對性能很有幫助,但無論幹什麼都要註冊一大堆東西,拿最近的一個項目已經簡化了非常多的來舉例子。當你要創建一個組件的時候,你需要做什麼:
-
寫一個 @Component 註解,裡面要定義組件的自定義 html 元素,以及 html 和 css 文件路徑。當然你也可以寫註解裡,那你就只能忍受一種比 jsx 還要醜的寫法 --- 將 html 和 css 都寫在字符串裡.
-
寫一個 class 並 implements 一些生命週期接口,然後還需要在 constructor 注入 service, 寫一堆生命週期函數。這一塊相對還好,類似於 vue2 的寫法,由於我寫了很久 vue2, 對這套還是挺接受的.
-
將這個 class 傳遞到 @NgModule 註解的 declarations 和 exports 裡去,值得注意的是,這一步不能 import * as, 因此每當你註冊一個新組件,你都需要配置這些東西.
-
當你在 @NgModule 中註冊一個組件出錯或者編譯錯誤後,所有在 @NgModule 註冊的組件都會報錯 (只要他們有相互依賴), 緊接著就會收穫滿屏幕的報錯,同時不知道是哪里錯了,因為所有的組件都紅了,僅當知道是因為組件註冊錯誤時,才能快速定位錯誤.
-
如果使用類似 antd 這樣的 ui 框架,由於他們提供了 drawer/modal 這樣的彈窗能力,自然會使用這些工具。但這些工具都需要再分別註冊一個組件,然後又會重複 1-4 的冗餘工作.
當然,單純寫著很麻煩的話,可以寫腳本,用快速工具。因此麻煩不麻煩不重要,問題是太重了。無論做是要註冊組件也好,註冊服務也罷,哪怕是寫個 store, 都需要註冊和配置一大堆的東西,然後才開始寫業務邏輯,這才是最大的问题.
什麼才是真正的模組化#
前端開發從設計上來說,不可能一開始就把所有的組件都拆出來 (基礎 ui 按鈕除外). 很多功能性的組件,都是在寫的過程中,才發現可以將一部分可重用的邏輯拆分出來,構成一個組件,從而實現模組化.
理解了這一點,我們回到之前說的組件,現在的組件在 css 上還是很耦合的,特別是由於 scss 這種 postcss 工具,導致 scss 寫的越來越難拆.
而 html 和 js 又是另一種情況下,js 和 html 通過現代的 MVVM 方式基本解耦了。但當我們想把一塊代碼變成組件時,又需要將 js 內部的邏輯拆分,將只屬於組件的邏輯挑出來,再進行拆分.
使用 react jsx, 能天然的將邏輯和頁面組織關聯,通常情況下 jsx 某一層級的代碼不會被外層代碼調用,拆分組件時,只需要將選擇的部分直接轉走,將外部參數作為 props 即可.
使用 tailwind 這樣寫在標籤的 css, 能將 scss 的複雜嵌套關係直接對應到界面上,拆分組件時,同樣也只需要將代碼轉走,無需任何操作.
一句話總結:在拆組件時,不再需要根據 html 找 css, 不用根據 html 找 js, 拆分 js, 可以將原來複雜的組件註冊流程全部放棄。只需要做一件事 -- 選擇需要的代碼,拆分,傳遞 props, 組件化就完成了.
技術選型#
總結上面說的許多內容,大體上的技術選型就是圍繞 react 全家桶的
-
React 18
-
Next.js 13
-
Tailwind + radix/shadcn ui
-
jotai
-
swr
其中的有些內容還沒提及,將在後續的學習筆記中一點一點講.
額外內容:踩坑記錄 01#
App router#
其實學 next.js 的時機有點巧,正好 next 13 大改了一堆東西,之前的文檔都過時了,學新的這套歷史包袱少多了。但問題是教程也很少,正好也啃啃官方文檔,就當學英語了.
app router 一個核心的改變就是把之前的 getStaticPaths/getStaticProps 等幾個 ssr/ssg api 改了 (見 file-conventions, 順便提一嘴,這個文檔也是我第一次看到文檔有寫項目結構和文件結構的,每個文件裡要寫什麼能寫什麼,例子都給的很全,寫的很不錯), getStaticPaths 換成用 generateStaticParams 了,原來的一些參數也都改成 Route Segment Config 了。然後 getStaticProps 就直接去掉了,總體來說影響不大,現在自己隨便寫一個方法調用就好了,寫起來其實比之前還舒服一些.
部署#
最開始我還以為 build 完和別的框架差不多,會暴露出一個類似 index.html 之類的東西,結果 build 完,.next 裡面一堆亂七八糟的東西,查了查才看到有個 output 選項,用這個就可以 build 出來一堆 html 文件,然後隨便用啥就可以部署了.
const nextConfig = {
output: "export",
images: { unoptimized: true },
};
目前版本代碼沒有 ssr, 純靜態,這個設置自然就開著了。當然對於我這種 ssg 的項目來說,隨便找個什麼地方部署都行,那不如直接用 vercel 的工具部署,每次推送一下更新就自動編譯了,CI 都不用自己搞了.
Eslint 和 自動化#
順其自然的用 eslint 和 perttier 這些很久了,也都沒怎麼研究過 src 外面的配置文件一大堆都是幹嘛的.
這次花了很多時間自己配了一套 eslint 的配置,其實配起來也是滿簡單的,也就是給 eslint 配置引入一下 extends 和 plugin, 然後根據手動調整一下規則就好了,弄完之後 lint 的感覺特別爽 (
另外一塊就是弄了 prettier 的配置,前段時間一直有強迫症,都用重排序 import, 但是默認配置其實就只是字母排序一下,排完其實變得更亂了,各種不同的依賴都亂拍,看著強迫症犯了。這次看到 shadcn 給的模版用了一个 prettier-plugin-sort-imports 庫,研究了一下這個怎麼用,終於可以根據自己想要的格式來排序依賴了,感覺特別特別爽 (
importOrder: [
"^(react/(.*)$)|^(react$)",
"^(next/(.*)$)|^(next$)",
"<THIRD_PARTY_MODULES>",
"",
"^types$",
"^@/types/(.*)$",
"^@/config/(.*)$",
"^@/lib/(.*)$",
"^@/utils/(.*)$",
"^@/hooks/(.*)$",
"^@/components/ui/(.*)$",
"^@/components/(.*)$",
"^@/styles/(.*)$",
"^@/app/(.*)$",
"",
"^[./]",
]
最後還弄了點自動化,husky 配合 lintstaged, 可以自動 eslint 檢查以及文章自動更新時間等等功能,靜態博客也可以寫的很方便 (
window is not defined#
@How are Client Components Rendered?
在 next 13 之後,app router 下的 tsx 和以前的是完全不一樣的,首先它會在服務端預渲染服務端組件的類 json 文件,然後再客戶端顯示並 hydration.
這裡有一個誤區,當使用 "use client" 時,很容易以為在整個函數式組件執行過程中都有 window, dom 等東西,但實際上需要等到 useLayoutEffect 時,你才能獲取到這個值,否則哪怕你對 window 屬性用了可選鏈,他仍然會報錯 (這裡還是有點疑惑的,之後會繼續研究)
具體渲染過程將在後續文章中分析,這裡先根據目前淺薄的 next.js 經驗總結一下 服務端組件 和 客戶端組件 的使用場景,未來理解更深入一些再更新.
在 RSC 渲染中,會混用 服務端組件 和 客戶端組件,通常的設計是:
- 將數據獲取的部分寫成服務端組件,這部分代碼甚至可以使用諸如 fs path 這樣的 node.js 庫
- 然後將服務端組件的獲取到的數據傳給客戶端組件
- 將固定的,沒有複雜 js 操作的部分寫成服務端組件,例如 button, link 這樣的輕組件
- 將複雜的,對頁面改動極大的組件設置寫成客戶端組件
- 將需要在線數據的,或需要 useEffect 的地方寫成客戶端組件
- 將本地儲存,編譯時就確定的數據寫成 SSG, SSG 不影響使用服務端組件和客戶端組件
One More Thing#
最後補充一段 react 官方兩年前視頻,有時候不得不感嘆,架構設計的超前,與我的落後:-X.