這節課我們一起來完成專案的架構 原始碼github
react服務端渲染框架Next.js踩坑(一)
react服務端渲染框架Next.js踩坑(二)
react服務端渲染框架Next.js踩坑(三)
react服務端渲染框架Next.js踩坑(四)
一、引入less
為了更方便的處理我們的樣式,我們選擇使用less來處理樣式,並使用css modules。css modules很容易學,因為它的規則少,同時又非常有用,可以保證某個元件的樣式,不會影響到其他元件。
安裝less npm install less @zeit/next-less --save
,然後更改一下next.config.js,配置css modules很簡單,cssModules設定為true就OK了。
// next.config.js
const path = require('path')
const withLess = require('@zeit/next-less')
if (typeof require !== 'undefined') {
require.extensions['.less'] = () => {}
}
module.exports = withLess({
cssModules: true, // 開啟css modules
cssLoaderOptions: {
importLoaders: 1,
localIdentName: '[local]___[hash:base64:5]',
},
webpack(config) {
const eslintRule = {
enforce: 'pre',
test: /.(js|jsx)$/,
loader: 'eslint-loader',
exclude: [
path.resolve(__dirname, '/node_modules'),
],
}
config.module.rules.push(eslintRule)
return config
},
})
複製程式碼
在pages目錄下新建一個style.less檔案
.container{
background-color: red;
}
複製程式碼
在pages/index.js中引入style.less,cssModules呼叫樣式的時候不需要直接填寫class名,直接通過引入的style呼叫樣式 className={style.container}
// pages/index.js
import React from 'react'
import style from './style.less'
const Home = () => {
return (
<div className={style.container}>hello world</div>
)
}
export default Home
複製程式碼
二、引入redux
寫過react的同學應該對它都不陌生,按照官方教程一頓複製貼上基本都能用。那如何在next中使用redux呢?next本身整合了專案入口,但是我們需要自定義入口,這樣我們才能在這裡配置redux和引入公用的header和footer元件。
在pages目錄新建_app.js檔案,安裝reduxnpm install redux react-redux redux-thunk next-redux-wrapper --save
// pages/_app.js
import App, { Container } from 'next/app'
import React from 'react'
import { Provider } from 'react-redux'
import withRedux from 'next-redux-wrapper'
import makeStore from '../store'
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
let pageProps = {}
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
return { pageProps }
}
componentDidMount() {}
render() {
const { Component, pageProps, store } = this.props
return (
<Container>
<Provider store={store}>
<Component {...pageProps} />
</Provider>
</Container>
)
}
}
export default withRedux(makeStore)(MyApp);
複製程式碼
然後在根目錄下建立store資料夾,store裡建立index.js 和 reducer.js。
一個專案裡我們會建立很多個頁面,如果我們把所有頁面的 reducer 全部放在一起的話,很明顯不利於我們程式碼的簡潔性,也不利於我們後期維護,所以我們需要分拆各個頁面的reducer,為每個頁面獨立建立自己的 reducer,然後在/store/reducer.js
裡整合。拆分之後的 reducer 都是相同的結構(state, action),並且各自獨立管理該頁面 state 的更新。
Redux 提供了 combineReducers 去實現這個模式。
// ~/store/reducer.js
import { combineReducers } from 'redux'
const reducer = combineReducers({
});
export default reducer;
複製程式碼
以後我們每建立一個reducer的時候,只需要在這裡引入,並且新增到 combineReducers 裡就可以使用他了。
// ~/store/index.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk'
import reducer from './reducer'
const exampleInitialState = { }
const makeStore = (initialState = exampleInitialState) => {
return createStore(reducer, initialState, applyMiddleware(thunk));
};
export default makeStore;
複製程式碼
基本redux結構配置好了,後面我們在寫頁面的時候再慢慢完善。
三、自定義服務端路由server.js
在next.js中是根據資料夾名來確定路由,但是出現動態路由也就是後面帶引數比如/a/:id
的時候就會出現問題了,雖然我們可以在 getInitialProps
方法中獲取到 id 引數,但是這樣會和 /a
路由衝突,並且很不優雅。所以我們需要自定義服務端路由。
在根目錄建立server.js檔案,安裝express npm install express --save
// ~server.js
const express = require('express')
const next = require('next')
const port = parseInt(process.env.PORT, 10) || 3000
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
app.prepare()
.then(() => {
const server = express()
// 頁面
server.get('/', (req, res) => {
return app.render(req, res, '/home', req.query)
})
server.get('*', (req, res) => {
return handle(req, res)
})
server.listen(port, (err) => {
if (err) throw err
console.log(`> Ready on http://localhost:${port}`)
})
})
複製程式碼
當然我們的啟動檔案也要改,package.json 裡的 scripts 改成
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
}
複製程式碼
這樣路由 /
就會指向到 pages資料夾下的home目錄。
我們在pages目錄下建立home資料夾,並且把pages下的 index.js 和 style.less移動到home下,這個以後就是我們的首頁了。移完後我們pages的目錄結構如下。
|-- pages
|-- home
|-- index.js
|-- style.less
|-- _app.js
複製程式碼
在命令列執行 npm run dev
程式,瀏覽器開啟http://localhost:3000
四、配置頁面的redux
home資料夾建立好了,我們來為首頁新增redux。在home目錄下建立store資料夾,store建立 index.js、reducer.js、action.js和constants.js。index.js是用來暴露出reducer和action,方便我們的呼叫。constants.js是用來存放 action 的type常量,以後我們就可以直接在這裡維護 action 的type常量,避免出現寫錯type的低階錯誤。
首先是reducer
// ~pages/home/store/reducer.js
import * as constants from './constants'
const defaultState = {
homeData: '我是首頁',
}
export default (state = defaultState, action) => {
switch (action.type) {
case constants.CHANGE_HOME_DATA:
return Object.assign({}, state, {
homeData: action.data,
})
default:
return state;
}
}
複製程式碼
我們定義一個改變 homeData 的reducer,如果action.type
等於 constants.CHANGE_HOME_DATA
常量我們就改變 state 裡 homeData 的值。
用過redux的同學都知道,在 reducer 裡原先的 state 是不允許被改變的,所以我們這裡使用 Object.assign
進行深拷貝。
// ~pages/home/store/action.js
import * as constants from './constants'
/* eslint-disable import/prefer-default-export */
export const changeHomeData = (data) => {
return {
type: constants.CHANGE_HOME_DATA,
data,
}
}
複製程式碼
定義一個 action 來管理reducer,傳入 type 和 data 給reducer。
當然別忘記建立type常量 constants.CHANGE_HOME_DATA
// ~pages/home/store/constants.js
/* eslint-disable import/prefer-default-export */
export const CHANGE_HOME_DATA = 'home/CHANGE_HOME_DATA'
複製程式碼
// ~pages/home/store/index.js
import reducer from './reducer'
import * as actionCreators from './action'
import * as constants from './constants'
export { reducer, actionCreators, constants }
複製程式碼
把reducer,actionCreators和constants暴露出來,方便其他頁面引用。
接下來需要在根目錄的reducer進行耦合
// ~store/reducer
import { combineReducers } from 'redux'
import { reducer as homeReducer } from '../pages/home/store'
const reducer = combineReducers({
home: homeReducer,
});
export default reducer;
複製程式碼
這樣我們就建立好了 home 頁面的 store,我們只需要 state.home.homeData
就可以引用 homeData 資料。
既然建立好了,那我們怎麼在home是用呢?編輯~pages/home/index.js
// ~pages/home/index.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import style from './style.less'
import { actionCreators } from './store'
class Home extends Component {
static async getInitialProps() {
return { }
}
render() {
const { homeData, changeHome } = this.props
return (
<div>
<div className={style.container}>{homeData}</div>
<button type="button" onClick={() => { changeHome() }}>改變homeData</button>
</div>
)
}
}
const mapState = (state) => {
return {
homeData: state.home.homeData,
}
}
const mapDispatch = (dispatch, props) => {
return {
changeHome() {
const data = '我改變了'
dispatch(actionCreators.changeHomeData(data));
},
}
}
export default connect(mapState, mapDispatch)(Home)
複製程式碼
next.js的redux使用和 react的redux使用一模一樣,通過 mapState 獲得state, mapDispatch來定義方法,在mapDispatch可以通過dispatch觸發action,然後action傳給reducer引數,使其進行相關操作。 重啟服務後,頁面就會出現
點選按鈕後變成四、優化
上面我們自定義了服務端路由server.js,這裡有一個問題,我們訪問 http://localhost:3000/
時候會出現首頁,但是如果我們訪問 http://localhost:3000/home
也是會訪問到同一個頁面,因為next的檔案路由我們並沒有禁止掉,所以我們需要修改一下 next.config.js,新增一個欄位 useFileSystemPublicRoutes: false
// next.config.js
const path = require('path')
const withLess = require('@zeit/next-less')
if (typeof require !== 'undefined') {
require.extensions['.less'] = () => {}
}
module.exports = withLess({
useFileSystemPublicRoutes: false,
cssModules: true,
cssLoaderOptions: {
importLoaders: 1,
localIdentName: '[local]___[hash:base64:5]',
},
webpack(config) {
const eslintRule = {
enforce: 'pre',
test: /.(js|jsx)$/,
loader: 'eslint-loader',
exclude: [
path.resolve(__dirname, '/node_modules'),
],
}
config.module.rules.push(eslintRule)
return config
},
})
複製程式碼
重啟後再訪問 http://localhost:3000/home
就無法訪問到頁面了。
五、總結
至此我們完成了專案的基本架構,在接下去的章節我們開始學習寫具體的頁面。當前的目錄結構
|-- next-cnode
|-- .babelrc
|-- .editorconfig
|-- .eslintrc
|-- next.config.js
|-- package.json
|-- server.js
|-- components
|-- store
| |-- index.js
| |-- reducer.js
|-- pages
| |-- home
| |-- store
| |-- index.js
| |-- reducer.js
| |-- action.js
| |-- constants.js
| |-- index.js
| |-- style.less
| |-- _app.js
|-- static
複製程式碼