大涛子客栈

Next.js 为您提供生产环境所需的所有功能以及最佳的开发体验:包括静态及服务器端融合渲染、 支持 TypeScript、智能化打包、 路由预取等功能,无需任何配置。 — 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 逻辑必须是在浏览器端才能执行,这里我们将目标页面的代码,在浏览器进行二次渲染:

1
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 目录下存放的静态文件的对外访问路径以 (/) 作为起始路径。如:

1
2
3
4
5
6
7
8
9
10
11
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 信息,以及埋点信息等,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<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:

1
2
3
4
5
6
module.exports = {
env: {
sa: process.env.SA_ENV || 'production',
topic: '知犀思维导图'
}
}

获取数据

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 命令才能运行该命令;
1
2
3
4
5
6
7
{
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start",
}
}

配置 Babel

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"presets": [
[
"next/babel",
{
"preset-env": {
"targets": {
"ie": 11
}
}
}
]
],
"plugins": [
[
"import",
{
"libraryName": "antd"
}
]
]
}

绝对路径引用

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

1
2
3
4
5
6
7
8
9
10
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
}
}
}

Window is not defined

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

1
2
3
4
5
6
let Masonry = null
if (typeof window !== 'undefined') {
import('masonry-layout').then(module => {
Masonry = module.default
})
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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项目中的结构:

1
2
3
4
5
├── store
│ ├── index.js
│ ├── modules
│ │ └── user.js
│ └── rootReducer.js

rootReducer.js

1
2
3
4
5
6
import { combineReducers } from 'redux'
import { reducer as user } from './modules/user'

export default combineReducers({
user
})

modules/user.js 部分内容

引入 immer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 获取、更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 高阶组件函数:

1
2
3
4
5
6
7
8
9
10
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 同步测试跨端浏览器:

1
2
3
"scripts": {
"dev:local": "next build && node server-local.js",
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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}`)
})
})

参考资料

 评论