跳到主要内容

nextjs

Pages

  • 基于文件系统的路由

需要获取数据的静态生成 预渲染1

  • 您的页面 内容 取决于外部数据:使用 getStaticProps。
  • 你的页面 paths(路径) 取决于外部数据:使用 getStaticPaths (通常还要同时使用 getStaticProps)。

你一个React组件 通常是需要请求接口来获取数据的,Next约定在组件页面定义异步的getStaticProps会在静态生成时自动调用获取数据。

还有一种是具有 动态路由 的页面。例如,你可以创建一个名为 pages/posts/[id].js 的文件用以展示以 id 标识的单篇博客文章。当你访问 posts/1 路径时将展示 id: 1 的博客文章。 此时在组件页面在加一个getStaticPaths的异步钩子函数来取你想渲染的路径。我估计这个路径或影响getStaticProps中的params.id


function Post({ post }) {
// Render post...
}

// 此函数在构建时被调用
export async function getStaticPaths() {
// 调用外部 API 获取博文列表
const res = await fetch('https://.../posts')
const posts = await res.json()

// 根据博文列表生成所有需要预渲染的路径
const paths = posts.map((post) => `/posts/${post.id}`)

// We'll pre-render only these paths at build time.
// { fallback: false } means other routes should 404.
return { paths, fallback: false }
}

// 在构建时也会被调用
export async function getStaticProps({ params }) {
// params 包含此片博文的 `id` 信息。
// 如果路由是 /posts/1,那么 params.id 就是 1
const res = await fetch(`https://.../posts/${params.id}`)
const post = await res.json()

// 通过 props 参数向页面传递博文的数据
return { props: { post } }
}


export default Post

营销页面、博客文章、电商产品列表、帮助和文档,推荐使用静态生成 (带有或不带数据),因为你的所有 page(页面)都可以只构建一次并托管到 CDN 上,这比让服务器根据每个页面请求来渲染页面快得多

服务器端渲染SSR 预渲染2

如果 page(页面)使用的是 服务器端渲染,则会在 每次页面请求时 重新生成页面的 HTML

要对 page(页面)使用服务器端渲染,你需要 export 一个名为 getServerSideProps 的 async 函数。服务器将在每次页面请求时调用此函数。

function Page({ data }) {
// Render data...
}

// This gets called on every request
export async function getServerSideProps() {
// Fetch data from external API
const res = await fetch(`https://.../data`)
const data = await res.json()

// Pass data to the page via props
return { props: { data } }
}

export default Page

getServerSideProps 类似于 getStaticProps,但两者的区别在于 getServerSideProps 在每次页面请求时都会运行,而在构建时不运行。

Fast Refresh

有点像热跟新那种?

静态文件服务

Next.js 支持将静态文件(例如图片)存放到根目录下的 public 目录中,并对外提供访问。

React SSR

通用代码

通用代码中不可在不判定执行环境的情况下引用 DOM、调用 window、document 这些浏览器特异和引用 global process 这些服务器端特异的操作,这往往是引起 Node.js 服务出问题的根本原因;

为了兼容两端,在选择库时,需要也同时需要支持两端,比如 axios,lodash 等;

差异代码

React 和 Vue 都有生命周期,需要区分哪些生命周期是在浏览器中运行,哪些会在服务器端运行,或者是同时运行,如使用 Redux 或者 Vuex 等库,最好在组件上引入 asyncData 钩子进行数据请求,同时供两端使用;

判定不同的执行环境可以通过注入 process.env.EXEC_ENV 来解决,形如:

if (process.env.EXEC_ENV === 'client') {
window.addEventListener(...);
}

if (process.env.EXEC_ENV === 'server') {
}

状态处理

而在服务器端,页面一旦确定内容,就没有办法 Rerender 了,这就要求组件显示的时候,就要把 Store 的数据都准备好,所以服务器端异步数据结合 Redux 的使用方式,流程是下面的样子

  • 创建 Store
  • 根据路由分析 Store 中需要的数据
  • 派发 Action 获取数据
  • 更新Store 中的数据
  • 结合数据和组件生成 HTML,一次性返回

客户端: beforeMount -> render -> mounted 服务端: preFetch -> beforeMount -> render // 服务端进行了数据的预先加载

路由同构

  • 客户端:在App.jsx中使用BrowserRouter组件包裹根节点,用NavLink组件包裹li标签中的文本
  • 服务端路由不同于客户端,它是无状态的。React提供了一个无状态的组件StaticRouter,向StaticRouter传递url,调用ReactDOMServer.renderToString()就能匹配到路由视图

事件同构处理

所谓同构,就是指前后端共用一套代码或逻辑,而在这套代码或逻辑中,理想的状况是在浏览器端进一步渲染的过程中,判断已有的DOM结构和即将渲染出的结构是否相同,若相同,则不重新渲染DOM结构,只需要进行事件绑定即可。React提供了只绑定事件的API:https://zh-hans.reactjs.org/docs/react-dom.html#hydrate

renderToString 并没有做事件相关的处理,因此返回给浏览器的内容不会有事件绑定。

那怎么解决这个问题呢?

这就需要进行同构了。所谓同构,通俗的讲,就是一套 React 代码在服务器上运行一遍,到达浏览器又运行一遍。服务端渲染完成页面结构,浏览器端渲染完成事件绑定。

那如何进行浏览器端的事件绑定呢?

让浏览器去拉取 JS 文件执行,让 JS 代码来控制。

修改 server/index.js, 添加 <script src="/index.js"></script>

<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>

index.js加载后进行客户端渲染,indexjs里是browser的react渲染逻辑,而这里使用ReactDOM.hydrate, 官方解释是该api和我们平时使用是ReactDOM.render是一样一样的, 但是当container的HTML内容是由ReactDOMServer渲染, 那么我们需要调用hydrate, 与 render() 相同,但它用于在 ReactDOMServer 渲染的容器中对 HTML 的内容进行 hydrate 操作。React 会尝试在已有标记上绑定事件监听器。它会尝试将事件绑定在已渲染的dom上.

构建与运行

在使用 webpack 进行构建时,需要将公共 App 部分打包出来,形成公共代码,由服务器端引入执行,而客户端可以引用打包好的公共代码,再用 webpack 引入之后进行特异处理即可; 需要引入 Node.js 中间层,负责请求数据,提供渲染能力,提供 HTTP 服务,由于 HTML 模板需要在服务端引入,CDN 文件需要自行处理; 至于 babel 的使用,可以在浏览器中通用处理,服务端只解决特殊语法,如 jsx,vue template;

由于要从一份源码构建出2份不同的代码,需要有2份 Webpack 配置文件分别与之对应。 构建用于浏览器环境的配置和前面讲的没有差别,本节侧重于讲如何构建用于服务端渲染的代码。

const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
// JS 执行入口文件
entry: './main_server.js',
// 为了不把 Node.js 内置的模块打包进输出文件中,例如 fs net 模块等
target: 'node',
// 为了不把 node_modules 目录下的第三方模块打包进输出文件中 因为 Node.js 默认会去 node_modules 目录下寻找和使用第三方模块;
externals: [nodeExternals()],
output: {
// 为了以 CommonJS2 规范导出渲染函数,以给采用 Node.js 编写的 HTTP 服务调用
libraryTarget: 'commonjs2',
// 把最终可在 Node.js 中运行的代码输出到一个 bundle_server.js 文件
filename: 'bundle_server.js',
// 输出文件都放到 dist 目录下
path: path.resolve(__dirname, './dist'),
},
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
exclude: path.resolve(__dirname, 'node_modules'),
},
{
// CSS 代码不能被打包进用于服务端的代码中去,忽略掉 CSS 文件
test: /\.css/,
use: ['ignore-loader'],
},
]
},
devtool: 'source-map' // 输出 source-map 方便直接调试 ES6 源码
};

以上代码有几个关键的地方,分别是:

  • target: 'node' 由于输出代码的运行环境是 Node.js,源码中依赖的 Node.js 原生模块没必要打包进去;
  • externals: [nodeExternals()] webpack-node-externals 的目的是为了防止 node_modules 目录下的第三方模块被打包进去,因为 Node.js 默认会去 node_modules 目录下寻找和使用第三方模块;
  • {test: /.css/, use: ['ignore-loader']} 忽略掉依赖的 CSS 文件,CSS 会影响服务端渲染性能,又是做服务端渲不重要的部分;
  • libraryTarget: 'commonjs2' 以 CommonJS2 规范导出渲染函数,以供给采用 Node.js 编写的 HTTP 服务器代码调用。

在构建客户端代码时,使用的是客户端的执行入口文件,构建结束后,将构建结果在浏览器运行即可,但是在服务端渲染中,HTML是由服务端渲染的,也就是说,我们要加载那些JavaScript脚本,是服务端决定的,因为HTML中的script标签是由服务端拼接的,所以在客户端代码构建的时候,我们需要使用插件,生成一个构建结果清单,这个清单是用来告诉服务端,当前页面需要加载哪些JS脚本和CSS样式表。

缓存控制

一般的业务场景下,我们需要在 Node.js 中通过内网将数据获取到,然后通过 render 函数渲染出 HTML(一般需要将数据附带给 HTML 输出以便重复利用),这个时候我们可以通过页面访问地址和生成的 HTML 字符串做缓存策略,在缓存(一般选择 redis 等方案)之后,下次直接将同样的页面直接输出到前端,可大幅提高渲染性能。

参考