Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React SSR 之 Next.js 使用总结 #117

Open
yangtao2o opened this issue May 13, 2020 · 0 comments
Open

React SSR 之 Next.js 使用总结 #117

yangtao2o opened this issue May 13, 2020 · 0 comments

Comments

@yangtao2o
Copy link
Owner

yangtao2o commented May 13, 2020

Next.js 使用总结

SSR VS CSR

概念

SSR 即服务端渲染(Server Side Rendering),对应的就是 CSR ,客户端渲染(Client Side Rendering)。

区别:

  • SSR,由服务端把渲染的完整的页面吐给客户端,减少了一次客户端到服务端的一次 http 请求,加快相应速度,一般用于首屏的性能优化
  • CSR,它依赖的是运行在客户端的 JS,用户首次发送请求只能得到小部分的指引性 HTML 代码。第二次请求将会请求更多包含 HTML 字符串的 JS 文件。

作用:

  • SSR 返回的页面是完整的 HTML 页面,有利于首屏渲染,以及 SEO,比如 PHP 等
  • CSR 是包含有 js 链接的 script 标签,有利于页面交互,比如 React、Vue 等

同构

由于服务端渲染的页面交互能力有限,如果要实现复杂交互,还是要通过引入 js 文件来辅助实现,我们把页面的展示内容和交互写在一起,让代码执行两次,这种方式就叫 同构

对于一些 js 操作,如事件绑定,dom 操作等,在服务端渲染的 html 文本无法执行,所以这些 js 逻辑必须是在浏览器端才能执行,这里我们将目标页面的代码,在浏览器进行二次渲染:

ReactDOM.hydrate(<Intro />, document.getElementById('root'))

render() 相同,但它用于在 ReactDOMServer 渲染的容器中对 HTML 的内容进行 hydrate 操作。React 会尝试在已有标记上绑定事件监听器。

看到这里,会有个疑问:在 Node 环境下,是没有 DOM 这个概念存在的,那 Node 环境下执行,必定会报错,那为何 React 它们就不会报错呢?这一切源于 React 的虚拟 DOM。

因为使用的是虚拟 DOM,而虚拟 DOM 是真实 DOM 的一个 JavaScript 对象映射,React 在做页面操作时,实际上不是直接操作 DOM,而是操作虚拟 DOM,也就是操作普通的 JavaScript 对象,这就使得 SSR 成为了可能。在服务器,我可以操作 JavaScript 对象,判断环境是服务器环境,我们把虚拟 DOM 映射成字符串输出;在客户端,我也可以操作 JavaScript 对象,判断环境是客户端环境,我就直接将虚拟 DOM 映射成真实 DOM,完成页面挂载。

关于 SSR 原理讲得比较好的一篇文章:React 中同构(SSR)原理脉络梳理

Next.js

功能:

  • 服务器端渲染(默认)
  • 自动代码切分, 加速页面加载
  • 简单的客户端路由(基于页面)
  • 基于 Webpack 的开发环境, 支持热模块替换(HMR: Hot Module Replacement)
  • 使用 React 的 JSX 和 ES6 的 module,模块化和维护更方便
  • 可以使用 Express 或其他 Node.js 服务器实现
  • 使用 Babel 和 Webpack 配置定制

静态文件服务

Next.js 支持将静态文件(例如图片)存放到根目录下的 public 目录中,并对外提供访问。public 目录下存放的静态文件的对外访问路径以 (/) 作为起始路径。如:

public
├── favicon.ico
├── robots.txt
├── static
│   ├── css
│   │   └── animate.min.css
│   ├── img
│   │   └── logo.png
│   └── js
│       └── wow.min.js
└── vercel.svg

然后直接引入:<img src="/static/img/logo.png" />,public 文件夹还可用于存放 robots.txtfavicon.ico等静态文件。

自定义 Document

pages 下自定义_document.js,这里可以配置一些通用 meta 信息,以及埋点信息等,如:

<Head>
  <meta content="yes" name="apple-mobile-web-app-capable" />
  <meta httpEquiv="X-UA-Compatible" content="IE=edge" />
  <meta name="renderer" content="webkit" />
  <meta property="og:image" content="https://www.zhixi.com/favicon.ico" />
  <script
    dangerouslySetInnerHTML={{
      __html: `
        ;(function (para) {
          if (typeof window['sensorsDataAnalytic201505'] !== 'undefined') {
            return false
          }
          window['sensorsDataAnalytic201505'] = para.name
          window[para.name] = {
            para: para,
          }
        })({
          is_track_single_page: true, // !important
          name: 'sensors',
          server_url: 'https://sa.aunload.com:4006/sa?project=${process.env.sa}',
          heatmap: {},
          show_log: false
        })
      `,
    }}
  />
  <script src="https://cdn-oss-static.aunbox.cn/Sensors/sensorsdata.min.js"></script>
  <script dangerouslySetInnerHTML={{ __html: `sensors.quick('autoTrack');` }} />
</Head>

自定义配置文件

在根目录下增加 next.config.js 文件,比如配置了 env:

module.exports = {
  env: {
    sa: process.env.SA_ENV || 'production',
    topic: '知犀思维导图',
  },
}

获取数据

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

import fetch from 'isomorphic-unfetch'

const Post = props => {
  if (props && props.show) {
    return (
      <>
        <h1>{props.show.name}</h1>
        <p>{props.show.summary.replace(/<[/]?p>/g, '')}</p>
        <img src={props.show.image ? props.show.image.medium : ''} />
      </>
    )
  }
}

Post.getInitialProps = async function (context) {
  const { id } = context.query
  const res = await fetch(`http://api.tvmaze.com/shows/${id}`)
  const show = await res.json()

  return { show }
}

export default Post

项目打包

  • next build 打包项目;
  • next start 启动打包后的项目,先运行 next build 命令才能运行该命令;
{
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start",
  }
}

配置 Babel

在根目录下增加 .babelrc 文件,由于使用了 Ant Design of React,为了兼容 IE11,需要添加相应的 Polyfill,在内置的next/babel里可以直接使用targets配置:

{
  "presets": [
    [
      "next/babel",
      {
        "preset-env": {
          "targets": {
            "ie": 11
          }
        }
      }
    ]
  ],
  "plugins": [
    [
      "import",
      {
        "libraryName": "antd"
      }
    ]
  ]
}

绝对路径引用

在根目录下增加 jsconfig.json 文件

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "src/*"
      ]
    }
  }
}

Window is not defined

有时候我们使用的插件,会被告知 window is not defined,怎么办?可以如下解决:

let Masonry = null
if (typeof window !== 'undefined') {
  import('masonry-layout').then((module) => {
    Masonry = module.default
  })
}

如果当模块包含仅在浏览器中可用的库时,可利用next/dynamic 设置ssr为false,则仅在浏览器中加载:

import dynamic from 'next/dynamic'

const DynamicComponentWithNoSSR = dynamic(
  () => import('../components/hello3'),
  { ssr: false }
)

function Home() {
  return (
    <div>
      <DynamicComponentWithNoSSR />
    </div>
  )
}

export default Home

使用 react-redux

项目结构

使用www.mindatoz.cn项目中的结构:

├── store
│   ├── index.js
│   ├── modules
│   │   └── user.js
│   └── rootReducer.js

rootReducer.js

import { combineReducers } from 'redux'
import { reducer as user } from './modules/user'

export default combineReducers({
  user,
})

modules/user.js 部分内容

引入 immer

import produce, { enableES5 } from 'immer'
import { profileService } from '@kxhz/user-service-sdk'

enableES5() // 兼容IE

// Actions Types
export const types = {
  SAVE_USER_INFO: 'USER/SAVE_USER_INFO',
  SAVE_LOGIN_STATUS: 'USER/SAVE_LOGIN_STATUS',
}

// Reducer
const initState = {
  userInfo: {},
  isLogin: false,
}

export function reducer(state = initState, action = {}) {
  switch (action.type) {
    case types.SAVE_USER_INFO:
      return produce(state, draft => {
        draft.userInfo = action.data
      })
    case types.SAVE_LOGIN_STATUS:
      return produce(state, draft => {
        draft.isLogin = action.data
      })
    default:
      return state
  }
}

// Action Creators
export const saveUserInfo = data => ({
  type: types.SAVE_USER_INFO,
  data,
})

export const saveLoginStatus = data => ({
  type: types.SAVE_LOGIN_STATUS,
  data,
})

// 获取用户信息、更新用户信息
export const getUserInfo = (token = {}) => {
  return async dispatch => {
    const res = await profileService.getProfile()
    if (res && !res.code) {
      dispatch(saveUserInfo(res))
      dispatch(saveLoginStatus(true))
    }
  }
}

index.js

引入 redux redux-thunk

import { createStore, compose, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import rootReducer from './rootReducer'

// redux_devtools
const composeEnhancers =
  (typeof window !== 'undefined' &&
    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
  compose

const store = createStore(
  rootReducer,
  /* preloadedState, */ composeEnhancers(applyMiddleware(thunkMiddleware))
)

export default store

_app.js

pages/_app.js下引入 react-reduxnext-redux-wrapper:

import { Provider } from 'react-redux'
import { createWrapper } from 'next-redux-wrapper'
import store from '@/store'

function MyApp(props) {
  const { Component, pageProps } = props

  return (
    <Provider store={store}>
      <Component {...pageProps} />
    </Provider>
  )
}

const wrapper = createWrapper(() => store)

export default wrapper.withRedux(MyApp)

使用 Hooks 获取、更新

import { useSelector, useDispatch } from 'react-redux'
import { getUserInfo } from '@/store/modules/user.js'

const Account = () => {
  // useSelector 获取状态
  const { userInfo, isLogin, token } = useSelector(state => state.user)

  // useDispatch 更新状态
  const dispatch = useDispatch()

  //获取用户信息
  useEffect(() => {
    if (isLogin) {
      dispatch(getUserInfo(token))
    }
  }, [isLogin])

  return <>111</>
}

export default Account

使用 Class 获取、更新

如果使用了 Class,需要借助 connect 高阶组件函数:

import { connect } from 'react-redux'

class DrawingWrap extends Component {...}

const mapStateToProps = (state) => ({ user: state.user })
const mapDispatchToProps = (dispatch) => ({
  getUserInfo: (token) => dispatch(getUserInfo(token)),
})

export default connect(mapStateToProps, mapDispatchToProps)(DrawingWrap)

自定义启动服务

在根目录下增加如 server-local.js,并在 package.json 配置启动,这样就可以通过访问本机 ip 同步测试跨端浏览器:

"scripts": {
  "dev:local": "next build && node server-local.js",
}
const { createServer } = require('http')
const os = require('os')
const { parse } = require('url')
const next = require('next')

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

function getLocalIP() {
  const interfaces = os.networkInterfaces()
  for (let devName in interfaces) {
    const iface = interfaces[devName]
    for (var i = 0; i < iface.length; i++) {
      const alias = iface[i]
      if (
        alias.family === 'IPv4' &&
        alias.address !== '127.0.0.1' &&
        !alias.internal
      ) {
        return alias.address
      }
    }
  }
}

app.prepare().then(() => {
  createServer((req, res) => {
    const parsedUrl = parse(req.url, true)

    handle(req, res, parsedUrl)
  }).listen(PORT, err => {
    if (err) throw err
    console.log(`Server running at http://${myHost}:${PORT}`)
  })
})

参考资料

@yangtao2o yangtao2o changed the title React SSR React SSR 之 Next.js 使用总结 Mar 22, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant