服务端渲染框架 nextjs

692 阅读3分钟

next.js

是一个轻量级的 React 服务端渲染应用框架

目录结构

|-- .gitignore
|-- package.json
|-- yarn.lock
|-- layouts  //布局组件
|   |-- layout.js
|-- components  //组件文件夹
|   |-- nav.js
|-- pages
|   |-- index.js 
|   |-- _error.js //错误文件
|   |-- _app.js  //全局布局组件入口
|-- static //静态资源文件夹,编译打包不被执行
|-- public
|-- favicon.ico

一、脚手架工具

 yarn create next-app <项目名称>

安装样式解析包

yarn add @zeit/next-css@1.0.1 @zeit/next-less@1.0.1 less@3.8.1 -S

修改next.config.js文件内容

const withCSS = require('@zeit/next-css')
const withLess = require('@zeit/next-less')

// 解决ant/lib/style/index.css 不认识@font-face 异常
if (typeof require !== 'undefined') {
  require.extensions['.css'] = (file) => {
  }
}

module.exports = Object.assign({},
  withCSS(withLess({
    // 开启Less模块化,但是同时也会开启css的模块化,这样会使得antd的样式加载不成功
    cssModules: true,
    webpack(config, {isServer}) {
      if (!isServer) {

        // 关闭css模块化,使antd样式生效
        config.module.rules[2].use[2].options.modules = false;
      }

      // 添加actions和reducers快速引用
      // config.resolve.alias.actions = path.resolve(__dirname, 'actions');
      // config.resolve.alias.reducers = path.resolve(__dirname, 'reducers');
      return config;
    }
  }))
)

二、路由

1.基本路由

Link
//1.不能直接在Link组件中写字符串,应该用一个标签包裹起来,这个标签可以是任意一个标签,但是Link组件中只能有一个子元素,不能包含多个
//2.不能直接给Link组件设置样式,因为它是一个HOC(高阶组件),给他设置样式无效,可以给Link组件的子元素设置样式

import Link from 'next/link'

<Link href="/next-route/teacher">
   <a style={{color:'red'}}>teacher</a>
</Link>

<Link href={{pathname:'/next-route/teacher',query:{id:1}}}>
   <a style={{color:'red'}}>teacher</a>
</Link>

2.命名式路由

import Router from 'next/router'

<span onClick={()=> Router.push('/next-route/teacher')}>student</span>

3.路由传参

传参

<ul>
      {
        teacherList.map(item => (
          <li key={item.id}>
            {/* 可以通过Link组件的as属性给路径取别名 */}
            <Link href={`/next-route/teacher/detail?id=${item.id}`}>
              <a>{item.name}</a>
            </Link>
          </li>
        ))
      }
    </ul>

接参

import { withRouter } from 'next/router'
// withRouter这个高阶组件会将当前的路由对象注入到组件中去,并将路由对象绑定到组件的props上
const Detail = withRouter((props) => (
  <div>
    这是{props.router.query.id}号老师的详情
  </div>
))
export default Detail

4.浅层路由

//可以通过Link组件的as属性给路径取别名

<ul>
{
  teacherList.map(item => (
     <li key={item.id}>
      {/* 可以通过Link组件的as属性给路径取别名 */}
        <Link as={`/next-route/teacher/${item.id}`} href={`/next-route/teacher/detail?id=${item.id}`}>
          <a>{item.name}</a>
        </Link>
      </li>
   ))
}
</ul>

解决浅层路由刷新页面404的问题

//1.安装 express
cnpm install -S express




//2.在项目的根目录 创建server.js

const express = require('express')
const next = require('next')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare()
.then(() => {
  const server = express()

  server.get('/next-route/teacher/:id', (req, res) => {
    const actualPage = '/next-route/teacher/detail'
    const queryParams = { id: req.params.id } 
    app.render(req, res, actualPage, queryParams)
  })

  server.get('*', (req, res) => {
    return handle(req, res)
  })

  server.listen(3000, (err) => {
    if (err) throw err
    console.log('> Ready on http://localhost:3000')
  })
})
.catch((ex) => {
  console.error(ex.stack)
  process.exit(1)
})




//3.修改package.json文件中script字段
"scripts": {
    "dev": "node server.js",
    "build": "next build",
    "start": "NODE_ENV=production node server.js"
},
    
    
    
    
//4.创建自定义路由
server.get('/next-route/teacher/:id', (req, res) => {
    const actualPage = '/next-route/teacher/detail'
    const queryParams = { id: req.params.id } 
    app.render(req, res, actualPage, queryParams)
})

5.错误页面

_error.js

const ErrorPage = () => (
  <div>
    <h1>不好意思,你访问的页面去火星了!</h1>
  </div>
)
export default ErrorPage

三、布局组件

mylayout.js

<div>
	<Mynav></Mynav>
    {props.children}
    <Myfooter></Myfooter>
</div>

全局布局组件

_app.js
import React from 'react'
import App, { Container } from 'next/app'
import MovieLayout from '../layouts/MovieLayout'

class Layout extends React.Component {
  render () {
    const {children} = this.props
    return (
      <MovieLayout>
        {children}
      </MovieLayout>
    )
  }
}

export default class MyApp extends App {
  render () {
    const {Component, pageProps} = this.props
    return <Container>
      <Layout>
        <Component {...pageProps} />
      </Layout>
    </Container>
  }
}

四、请求数据

Isomorphic Unfetch

使用异步静态方法 getInitialProps 获取数据,此静态方法能够获取所有的数据,并将其解析成一个 JavaScript 对象,然后将其作为属性附加到 props 对象上

当页面初次加载时,getInitialProps 只会在服务端执行一次。getInitialProps 只有在路由切换的时候(如Link组件跳转或命名式路由跳转)时,客户端的才会被执行。

注意:getInitialProps 不能 在子组件上使用,只能使用在 pages 页面中。

1.安装
//安装
yarn add isomorphic-unfetch@2.2.1 es6-promise -S
//or
cnpm i -S isomorphic-unfetch

//引入
import fetch from 'isomorphic-unfetch'


// Index是一个组件
Index.getInitialProps = async function() {
  const res = await fetch('http://localhost:3301/in_theaters')
  const data = await res.json()
	
	// 这段数据会在服务器端打印,客户端连请求都不会发
  console.log(data)

  return {
    // 组件中通过props.shows可以访问到数据
    movieList: data
  }
}

如果你的组件是一个类组件,你需要这样写:

export default class extends React.Component {
  static async getInitialProps() {
    const res = await fetch('http://localhost:3301/in_theaters')
    const data = await res.json()
    console.log(data);
    return {movieList: data}
  }
  render() {
    return (
      <div>
        {this.props.movieList.map(item => (
          <p key={item.id}>{item.title}</p>
        ))}
      </div>
    )
  }
}

getInitialProps: 接收的上下文对象包含以下属性:

  • pathnameURLpath部分
  • queryURLquery string部分,并且其已经被解析成了一个对象
  • asPath: 在浏览器上展示的实际路径(包括 query字符串)
  • reqHTTP request 对象 (只存在于服务器端)
  • resHTTP response 对象 (只存在于服务器端)
  • jsonPageRes: 获取的响应数据对象 Fetch Response (只存在于客户端)
  • err: 渲染时发生错误抛出的错误对象
// 在另外一个组件中,可以使用context参数(即上下文对象)来获取页面中的query
Post.getInitialProps = async function (context) {
  const { id } = context.query
  const res = await fetch(`http://localhost:3301/in_theaters/${id}?_embed=details`)
  const data = await res.json()
  console.log(data)

  return {movieDetail: data}
}

isomorphic-fetch

github.com/matthew-and…

1.安装
yarn add isomorphic-fetch@2.2.1 es6-promise -S

isomorphic-fetch是基于Fetch语法封装好的用于同构应用开发库
基于标准 Promise 实现,支持 async/await
注:同构就是使前后端运行同一套代码的意思,后端一般是指 NodeJS 环境。


require('es6-promise').polyfill();  //保证使低版本浏览器(IE<9)中可以正常使用promise
import 'isomorphic-fetch' 
2.参数说明
method: 表示当前请求 GET/POST/PUT/DELETE
headers: 自定义请求头对象
	Content-Type: application/json;charset=UTF-8 #表示POST请求报文体中的数据格式为JSON
body: POST请求报文对象
cache: 设置cache模式 no-cache 请求不被缓存
credentials: 设置cookies是否随请求一起发送,可以设置 include 允许跨域请求携带cookies
3.封装fetch请求
//1.定义数据服务基础url
const baseUrl = 'http://157.122.54.189:9092'

//2.基本的fetchHelper请求
function fetchHelper(urlPath,options){
  return fetch(baseUrl+urlPath,options)
}
//3.get请求
fetchHelper.get = (urlPath)=>{
  return fetch(baseUrl+urlPath,{
    method:'GET',
    cache:'no-cache',
    credentials:'include'
  }).then(response=>{
    if (response.status === 200) {
      return response.json()
    }else{
      throw new Error('请求状态异常')
    }
  }).catch(error=>{
    throw new Error('请求错误')
  })
}

//4.post请求
fetchHelper.post = (urlPath,body)=>{
  return fetch(baseUrl+urlPath,{
    method:'POST',
    headers:{
      'Content-Type':'application/json;charset=UTF-8'
    },
    body:JSON.stringify(body),
    cache:'no-cache',
    credentials:'include'
  }).then(response=>{
    if (response.status === 200) {
      return response.json()
    }else{
      throw new Error('请求状态异常')
    }
  }).catch(error=>{
    throw new Error('请求错误')
  })
}

//导出
export default fetchHelper
4.使用fetch请求

在_app.js中导入

五、样式

组件样式

  1. css样式文件
  2. css in js
  3. styled-jsx
  • scoped

    如果添加了 jsx属性,只作用于当前组件,不包括子组件

   <style jsx>{`
     h1, a {
       font-family: "Arial";
     }

     ul {
       padding: 0;
     }

     li {
       list-style: none;
       margin: 5px 0;
     }

     a {
       text-decoration: none;
       color: blue;
     }

     a:hover {
       opacity: 0.6;
     }
   `}</style>

  • global

    作用于当前组件,包括子组件

<style jsx global>{``}</style>

全局样式

# 在pages目录下新建 _document.js 文件


import Document, { Head, Main, NextScript } from 'next/document'

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx)
    return { ...initialProps }
  }

  render() {
    return (
      <html>
        <Head>
          <link rel="stylesheet"  href="./../static/css/antd.css"/>
        </Head>
        <body className="custom_class">
          <Main />
          <NextScript />
        </body>
      </html>
    )
  }
}

六、redux(状态管理器)

1.安装react中redux相关的包

yarn add redux@4.0.0 react-redux@5.0.7 next-redux-wrapper@2.0.0 -S

- redux : 数据流框架
- react-redux:数据流在react中的实现包装
- next-redux-wrapper:在next.js中对redux进行包装,实现store全局共享,可以跨组件访问

2.初步使用

在根目录创建reducer文件夹在里面创建一个testReducer.js 业务逻辑

//参数1:state 全局对象,将来可以被store中的dispatch修改,同时会触发依赖于次对象中的属性的组件中的内容的相关业务改变

//参数2:action 当store的dispatch被调用的时候,会传入此action,来告诉store应用做什么操作

export default function testReducer(state={color:'red'},action){
  switch (action.type) {
    case 'CHANGE_COLOR':
      return {
        color:action.color
      }  
    default:
      return state;
  }
}

在根目录创建store文件夹,创建index.js文件,状态管理仓库

//导入redux中的两个函数createStore,combineReducers
import {createStore,combineReducers} from 'redux'
//导入具体的业务reducer
import testReducer from './../reducers/testReducer'

// 将多个reducer编译成统一的rootReducer
const rootReducer = combineReducers({
  testReducer  
})

//调用createStore创建一个store对象
const store = createStore(rootReducer)

// 构建初始化Store的函数,交给 next-redux-wrapper的withRedux 使用
export const initStore = () => store
/* const initStore = (initialState,options) => {
  return createStore(rootReducer,initialState);
}
export default initStore */

调用方法改变状态

import {connect} from 'react-redux'

class head extends React.Component {
   render() {
   	  return (
      	<a onClick={()=>{this.props.onChangeColor('blue')}}>蓝色</a>
        <a onClick={()=>{this.props.onChangeColor('red')}}>红色</a>
      )
   }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onChangeColor:(color)=>{
      dispatch({type:'CHANGE_COLOR',color:color})
    }
  }
}

export default connect(null,mapDispatchToProps)(head)

使用状态

import React from 'react'
import {connect} from 'react-redux'

class Home extends React.Component {
  render() {
    return (
      <div className="container">
        <main>
          <h1 style={{color:this.props.testReducer.color}}>哈哈哈</h1>
        </main>
      </div>
    );
  }
};

const mapStateToProps = (state) => {
  return {...state}
}

export default connect(mapStateToProps)(Home);

3.自动保存到localStorage中

安装redux-persist

www.npmjs.com/package/red…

yarn add redux-persist