最開始先擺出地址,有在線 demo:https://github.com/OrangeXC/gank前端
鑑於最近 vue 相關的文章寫的比較多,抽出時間寫點 react 的項目,當時用 react 仍是 v15 如今都 v16 了,感慨跟不上全部框架的節奏(玩笑話),框架的本質都是大同小異的,每次高 star 框架更新看一下 change 是個好習慣。vue
以前用 Nuxt 寫了個簡單的 v2ex,今天的主角依然是 SSR 服務端渲染node
Nuxt 文檔裏有寫到靈感源於 Next.js,那麼就是說 Next.js 算是 SSR 框架中的元老級別的了。react
前面的文章老是在官方文檔上小費功夫說明下,這裏對於不熟悉 Next.js 的讀者建議直接轉到 Github,別猶豫,固然熟悉 Nuxt 也能夠無障礙閱讀本文android
不管是 Next.js 或 Nuxt,服務端渲染框架主要兩個重要功能ios
首屏 node.js 服務端渲染git
生成純靜態的 web 站github
至於它們是基於哪一個前端庫封裝的,還要看庫自己是否支持 SSR,而後就是對外提供 render 函數。web
用此類庫的緣由也沒必要多說,節省開發成本,再也不糾結於環境搭建以及渲染細節。docker
本次要實現的是基於 gank api 的項目,仍是看人家支持什麼 api,點前面連接查看詳細 api
大致總結爲 => 列表,搜索,提叫到審覈
列表分爲了許多類型,主要的 menu 也是針對不一樣類型的列表展開
經過已知的 api 能夠輕鬆的定義路由
/ (主頁,最近的所有類型乾貨列表)
/fe (前端乾貨列表)
/android (安卓乾貨列表)
/ios (iOS乾貨列表)
/app (App乾貨列表)
/expand (拓展資源乾貨列表)
/videos (休息視頻乾貨列表)
/welfare (福利列表,前方高能,全是乾貨。。。)
/timelien (時間軸,記錄歷史全部更新過乾貨的日期)
/day (某天詳情,分爲以上幾種類型的 tab 列表)
/uplaod (發送乾貨到審覈)
/search (搜索頁)
同 Nuxt 路由配置文件不須要手動建立,/pages 下默認會渲染爲頁面,文件名天然就是路由名
路由文件都建立完了,下一步思考如抽離出公共模板 Layout 代碼,Next.js 提供了 layout-component example
咱們能夠在裏面定義 Head,Header,Footer,固然要留出一個內容區域的插槽 { children }
引用於 example 的 layout.js 代碼
import Link from 'next/link' import Head from 'next/head' export default ({ children, title = 'This is the default title' }) => ( <div> <Head> <title>{ title }</title> <meta charSet='utf-8' /> <meta name='viewport' content='initial-scale=1.0, width=device-width' /> </Head> <header> <nav> <Link href='/'><a>Home</a></Link> | <Link href='/about'><a>About</a></Link> | <Link href='/contact'><a>Contact</a></Link> </nav> </header> { children } <footer> {'I`m here to stay'} </footer> </div> )
由於本次使用的是 antd 作 ui,固實現動態的導航展現上要注意些小問題,咱們須要根據 path 動態的給 menu 激活狀態。
兩個解決方案:
1.在 pages 裏面的每個路由頁面裏獲取 pathname,初始化方法 getInitialProps
裏能夠拿到 pathname,所有列表以下
pathname - path section of URL
query - query string section of URL parsed as an object
asPath - String of the actual path (including the query) shows in the browser
req - HTTP request object (server only)
res - HTTP response object (server only)
jsonPageRes - Fetch Response object (client only)
err - Error object if any error is encountered during the rendering
調用方法也簡單
static async getInitialProps({ pathname }) { return { pathname } }
這樣一來能夠經過傳參到 layout 組件的方式 <Layout pathname={this.props.pathname}></Layout>
在 Layout 裏面改變 Meun 的 active
2.寫一個 ActiveLink 組件,再封裝一層原有的 Menu
在選擇方案前仍是要看官方有沒有 example,因而找到了 using-with-router
引用於 example 的 ActiveLink.js 代碼
import { withRouter } from 'next/router' // typically you want to use `next/link` for this usecase // but this example shows how you can also access the router // using the withRouter utility. const ActiveLink = ({ children, router, href }) => { const style = { marginRight: 10, color: router.pathname === href ? 'red' : 'black' } const handleClick = (e) => { e.preventDefault() router.push(href) } return ( <a href={href} onClick={handleClick} style={style}> {children} </a> ) } export default withRouter(ActiveLink)
簡單易懂,在 withRouter 方法裏能夠取到 router 實例,這樣能夠取到 pathname,query 等等。
這裏只須要稍稍修改下 style,變成 antd 的 className,以下
const ActiveLink = ({ children, router, href }) => { const active = router.pathname === href const className = active ? 'ant-menu-item-selected ant-menu-item' : 'ant-menu-item' return ( <li href='#' onClick={onClickHandler(href)} className={className} role="menuitem" aria-selected="false"> {children} </li> ) }
在 Layout 組件的 Menu 裏直接使用 ActiveLink 組件便可,到這爲止解決了所有路由相關問題和 Layout 組件問題
解決了路由問題下一步就是每一個頁面的 content 的數據填充
咱們依舊是在 getInitialProps
裏面獲取數據,至關於 prefatch 方法,服務端渲染會提早執行這個方法獲取數據渲染到模板
這裏涉及到一個 node 和 Browserify 同構的 fetch 庫 isomorphic-fetch,cli 工具應該會自帶這個庫,沒有的話提早安裝下。
到這裏就不用擔憂 fetch api 在服務端的問題了,這裏獲取的列表數據走的接口基本一致 https://gank.io/api/data/{type}/{perPage}/{page}
三個變量 type-類型、perPage-每頁數量、page-頁數
接下來能夠把 List 和 ListItem 抽象出來,成爲共用的組件,每一個頁面均可以調用,這裏不詳細展開說明,簡單的使用 antd 的 Card 組件,沒有特殊功能。
每一個頁面的請求數據部分也基本一致,將數據存到 props 裏,傳入 List 組件中去
造成了簡單的單向數據流動
列表頁面
page組件(fetch data) -> List組件(繼承自 Layout) -> ListItem組件
時間軸頁面
page組件(fetch data) -> Timeline組件(繼承自 Layout)
提交乾貨頁面
page組件 -> Form組件(繼承自 Layout) -> post請求(發送formData)
搜索頁面
page組件 -> Input組件+空ListItem組件(繼承自 Layout) -> get請求(獲取關鍵詞對應query的列表數據) -> ListItem組件
既然前面說清楚了數據流都十分簡單,那麼爲何要引入全局狀態管理徒增煩惱呢?
有一點無奈的地方是 getInitialProps
自己 return 的就是 props,在 react 裏面 props 是單向的,只能向下傳遞,且不能修改
這裏咱們要分頁功能,可是首屏數據是 props 的,咱們換頁以後沒辦法更新 props 的值,也就是沒辦法再次執行 getInitialProps
最簡單粗暴的方式就是放棄 spa 的動態切換數據,咱們每次 Router.push({some page}/{per page}/{current page})
,一朝回到解放前的 MVC 版路由切換。
能不能解決問題,答案是能解決問題,那麼既然是分頁組件,人家 antd 也提供了 Pagination 組件,問題一個接着一個,人家返回的列表並無告訴你 totalCount,沒有 totalCount 就沒辦法知道有多少頁。。。
好尷尬的問題,這個分頁無法作,怒臉~~~
也不是沒辦法作,這個問題變向思考下能夠作 loadMore,沒錯加載更多,當加載到最後一頁(即的列表長度小於 perPage)或是此頁恰巧等於 perPage 但下一頁爲空數組時,咱們給一個提示,沒有更多內容了。
涉及到向 props 的 list 裏 concat
數組,咱們不得不引入全局狀態來解決這個問題,不管是 redux 仍是 mobx 均可以解決問題,須要注意的是,next.js 中的用法和普通 spa 的 react 應用有所差異。
仍是去找 example,with mobx
引用於 example 的 store.js 代碼
export function initStore (isServer, lastUpdate = Date.now()) { if (isServer) { return new Store(isServer, lastUpdate) } else { if (store === null) { store = new Store(isServer, lastUpdate) } return store } }
這段代碼太簡單,不必解釋了,總之咱們在初始化頁面時調用 initStore 就行了,isServer 經過 getInitialProps
的 req 參數 !!req
判斷
而後在 loadMore 時出發一個 action
@action loadMoreList = (more) => { this.list = this.list.concat(more) }
到這加載更多的功能也就實現了,不足的一點是 List 組件裏的 handleScroll
方法寫的有點簡陋,雖然說能用,但存在問題,如屢次觸發、未寫兼容代碼(後續會改進),放出代碼供你們一笑
handleScroll () { if (document.documentElement.offsetHeight + document.documentElement.scrollTop > document.documentElement.scrollHeight - 50) { this.handleLoadMore() } }
其它代碼感興趣能夠直接取倉庫看,沒有閱讀難度。
說到其它頁的 fetch list 沒什麼可將全都是 get 請求,fetch 發一個 get 請求十分簡單,不用聲明請求類型。
fetch 操做 post 也僅僅在於設置 method 爲 POST
之因此單獨一章說表單提交,由於在提交表單時遇到了一些問題,因爲要 fetch 模擬 form 的 post 請求
看了這個 issue:https://github.com/matthew-an...
開始懷疑人生,試了全部方法 POST,也走的通,可是接口返回的 msg 就是沒接收到參數。
想了想仍是迴歸到笨方法一個一個將參數拼接進去,沒想到較優雅的方式,給出代碼,同時歡迎討論
handleSubmit = (e) => { e.preventDefault() this.props.form.validateFieldsAndScroll(async (err, values) => { if (!err) { this.setState({ submitLoading: true }) let strList = [] Object.keys(values).forEach(item => { strList.push(`${item}=${values[item]}`) }) const res = await fetch("https://gank.io/api/add2gank", { method: "POST", headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: strList.join('&') }) const json = await res.json() if (json.error) { message.error(json.msg) } else { message.success(json.msg) } this.setState({ submitLoading: false }) } }) }
看過網站的讀者也發現提交表單頁面上方有提示語,讓你們文明使用三方 api 提供者 gank 的發表乾貨接口,把真正的好內容提交上去,想測試接口的請走默認的 debug 模式,這裏再次強調下,感謝配合
既然功能差很少了,再微交互上再加把勁,用過 NUXT 的知道 NUXT 內置了 Loading bar,切換路由時在頁面頂端會有 loading 條,體驗較好。
next.js 並無內置這個功能,頁面看起來會顯得十分怪異,點擊切換路由沒有反應,頓一下再跳轉,頓的時候在獲取初始化數據。
官方推薦使用 nprogress
關鍵代碼以下,寫在了 Layout.js 組件裏
Router.onRouteChangeStart = (url) => { console.log(`Loading: ${url}`) NProgress.start() } Router.onRouteChangeComplete = () => NProgress.done() Router.onRouteChangeError = () => NProgress.done()
這樣整個網站看起來洋氣多了,切換 router 頁面頂端有 loading bar,右上角還有 loading icon
開發 next.js 的組織叫 zeit,在官網他們的得意做品是 now,一個快速部署的工具,同時爲免費用戶提供三個免費的服務,支持 docker,node 等
看 5 分鐘文檔就能上手部署 node 項目,比 Heroku 簡單的多
在項目根路徑下一句命令部署
now
線上的路徑就不貼出來了,時刻關注 Github 上方的 website 地址,由於每次部署不綁定域名的狀況下是 項目名+隨機哈希
的域名,綁定域名須要 money。
至於上線就講這麼多,有疑問歡迎交流。
下一步要解決幾個問題
加載更多時的 bug
支持移動端
福利頁面直接展現圖片(點擊能夠全屏大輪播)
美化時間軸樣式
說到福利頁面本想着不加來着,由於個別寫 demo 的人專門把福利列表拎出來作成妹子 App,既然是乾貨集中營,就應該多些技術元素,福利都是次要的。
到這爲止一個 next.js 版本的 gank(乾貨集中營)完成了,感慨如今開發工具愈來愈好用,仍是以前的想法把好用的工具分享給你們,給一個完整的例子供學習者參考,再也不每次都看各個版本的 Hacker News,而是給國內的學習者一箇中文版的例子,同時文中也會將實現的時候遇到的問題。
本人 orange 也是再不斷的學習當中,本文也是第一次接觸學習 next.js 寫的項目,文章或項目有不足之處歡迎指正,感謝閱讀!
文章出自 orange 的 我的博客 http://orangexc.xyz/