react服務端渲染框架Next.js踩坑(二)

vision1發表於2019-04-01

這節課我們一起來完成專案的架構 原始碼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引數,使其進行相關操作。 重啟服務後,頁面就會出現

react服務端渲染框架Next.js踩坑(二)
點選按鈕後變成

react服務端渲染框架Next.js踩坑(二)

四、優化

上面我們自定義了服務端路由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
複製程式碼

相關文章