初言
浏览器是怎么渲染一个页面的?
浏览器渲染一个网页,简单来说可以分为以下几个步骤:
- HTML 解析:在这个过程之前,浏览器会进行 DNS 解析及 TCP 握手等网络协议相关的操作,来与用户需要访问的域名服务器建议连接,域名服务器会给用户返回一个 HTML 文本用于后面的渲染 (这一点很关键,要注意)。
- 渲染树的构建:浏览器客户端在收到服务端返回的 HTML 文本后,会对 HTML 的文本进行相关的解析,其中 DOM 会用于生成 DOM 树来决定页面的布局结构,CSS 则用于生成 CSSOM 树来决定页面元素的样式。如果在这个过程遇到脚本或是静态资源,会执行预加载对静态资源进行提前请求,最后将它们生成一个渲染树。
- 布局:浏览器在拿到渲染树后,会进行布局操作,来确定页面上每个对象的大小和位置,再进行渲染。
- 渲染:我们电脑的视图都是通过 GPU 的图像帧来显示出来的,渲染的过程其实就是将上面拿到的渲染树转化成 GPU 的图像帧来显示。首先浏览器会根据布局树的位置进行栅格化(用过组件库的同学应该不陌生,就是把页面按行列分成对应的层,比如 12 栅格,根据对应的格列来确定位置),最后得到一个合成帧,包括文本、颜色、边框等,最后将合成帧提升到 GPU 的图像帧,进而显示到页面中,就可以在电脑上看到我们的页面了。
而服务器端渲染对 C 端 网站 的优势,主要也是在于它拿到的 HTML 不同。 这样的差异,会给 Web 应用带来不同的表现。
SSR 优势
易传播性: SSR爬虫精度更高
搜索引擎可以理解是一种爬虫
,它会爬取指定页面的 HTML,并根据用户输入的关键词对页面内容进行排序检索,最后形成我们看到的结果。
页面渲染过程中,HTML 解析过程中从服务器端拉取的 HTML 并不是页面最终预期的结果,对于一些高级爬虫,会待页面渲染完成后进行页面数据的拉取和关键词匹配,但是也有一些低级爬虫,它们爬取的将是服务器端拉取的 HTML,那么服务器端拉取下来的HTML中包含的实际页面关键词和数据越多,搜索引擎匹配的精度也会越高。
SSR 会在服务器端完成对页面数据数据的请求,将对应数据注入 DOM 一同返回,会得到一个完整可预览的 HTML。以掘金首页举例,可以看到下图服务器端拉取的 HTML 是包含这个页面中所将展示的实际数据。
而对于客户端渲染,数据的拉取将会在客户端完成,请求服务器拿到的 HTML 将是一个空的包含有执行脚本的 HTML,也就是说,客户端渲染页面的服务器响应的 HTML 并不包含页面中实际数据,也可以参考下图一个 B 端管理平台的 HTML 响应。
服务器端渲染和客户端渲染的差异,决定了服务器端渲染在 爬虫 关键词爬取的精准度上会远胜客户端渲染,使得站点更容易获得相关关键词更高的排名
。
交互稳定性: SSR 更高效
交互稳定性,这个也与服务器端渲染和客户端渲染的 HTML 差异有关。对于客户端渲染,实际的数据需要在执行脚本后请求数据后才可以得到,而对于服务器端渲染,数据请求的过程在在服务器端已经完成了,这就使得服务器渲染将不再需要进行数据请求,可以拥有更短的首屏时间
。
项目初始化
- 创建项目
yarn create next-app --typescript
配置别名
// next.config.js
const path = require("path");
module.exports = {
reactStrictMode: true,
swcMinify: true,
webpack: (config) => {
config.resolve.alias = {
...config.resolve.alias,
"@": path.resolve(__dirname),
};
return config;
},
};
- tsconfig.json 中我们也需要加一下对应的别名解析识别(baseurl , paths)。
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@public/*": ["public/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
目录
eslint
- 安装依赖
yarn add -D eslint eslint-plugin-react @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-simple-import-sort
npx eslint --init
- .eslintrc.js
// .eslintrc.js
module.exports = {
env: {
browser: true,
commonjs: true, // ADD, 支持对commonjs全局变量的识别
es2021: true,
node: true,
},
extends: [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: "latest",
},
plugins: ["react", "@typescript-eslint", "eslint-plugin-simple-import-sort"],
rules: {
"react/jsx-uses-react": "off", // 必须增加对import React from 'react',jsx 的页面已经不再需要引入 React了,所以我们去掉这条 lint 规则
"react/react-in-jsx-scope": "off", // 同上
"@typescript-eslint/no-var-requires": "off", // 关闭 禁用使用 require 来定义
"react/display-name": "off", // 关闭组件定义缺少显示名称
"simple-import-sort/imports": "error", // import 自动排序,eslint-plugin-simple-import-sort 自动修正
"simple-import-sort/exports": "error",
"no-duplicate-imports": ["off", { includeExports: true }], // import不能重复重复,自动合并插件 eslint-plugin-import,添加extends:plugin:import/recommended
"import/no-unresolved": "off", // 关闭 eslint 无法解析的导入
},
};
在原来的基础上,我们在 env 的配置中加上了 commonjs: true
,这个是为了支持对 commonjs 全局变量的识别
- react/jsx-uses-react:必须增加对
import React from 'react';
的引入,在 React 17 之后,jsx 的页面已经不再需要引入 React了,所以我们去掉这条 lint 规则。 - react/react-in-jsx-scope:同上。
- @typescript-eslint/no-var-requires:禁用使用 require 来定义,node 很多相关的依赖没有对 es module 的定义,所以我们也去掉这条 lint 规则。
- react/display-name:关闭组件定义缺少显示名称
- simple-import-sort/imports:import 自动排序,安装 eslint-plugin-simple-import-sort 自动修正
- no-duplicate-imports:import 重复导入,自动修正:安装 eslint-plugin-import,并加入extends: plugin:import/recommended
- import/no-unresolved:关闭 eslint 无法解析的导入
commit
- 安装依赖
yarn add -D @commitlint/config-conventional @commitlint/cli
- 新增 commitlint.config.js
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: {
"type-enum": [2, "always", ["feat", "fix", "revert"]],
"subject-max-length": [1, "always", 30],
},
};
其中 type-enum 是指 commit 正文的前缀,通常我们会用到这三种:
- Feat:一个新的功能;
- Fix: 一次修复,之前已有问题的修复;
- Revert:一次回滚,书写异常代码后的撤销。
subject-max-length
则对应实际的 commit 长度(不包括前缀),这里我们设置为30
husky
- 安装依赖
yarn add -D husky
npx husky install
npx husky add .husky/pre-commit
- husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx commitlint --edit $1
npm run lint
Prettier
- vscode 安装 Prettier - Code formatter
- 安装依赖
yarn add -D prettier eslint-plugin-prettier
- .prettierrc.js
module.exports = {
singleQuote: false,
trailingComma: "all",
printWidth: 80,
htmlWhitespaceSensitivity: "ignore",
};
- .Vscode/settings.json
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.fixAll.stylelint": true
},
"stylelint.validate": ["css", "less", "scss", "vue"],
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
- 添加 .editorconfig 定义编码风格
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
stylelint
- 安装依赖
yarn add -D stylelint stylelint-config-clean-order stylelint-config-prettier stylelint-config-standard stylelint-config-standard-scss stylelint-prettier
- stylelint.config.js
module.exports = {
processors: [],
extends: [
"stylelint-config-standard-scss",
"stylelint-config-standard",
"stylelint-prettier/recommended",
"stylelint-config-prettier",
"stylelint-config-clean-order",
],
rules: {
"prettier/prettier": true,
"at-rule-no-unknown": null,
"no-empty-source": null,
"unit-no-unknown": null,
"no-descending-specificity": null,
"selector-pseudo-class-no-unknown": null,
"declaration-block-no-duplicate-properties": null,
"selector-type-no-unknown": null,
"block-no-empty": null,
"font-family-no-missing-generic-family-keyword": null,
"declaration-block-no-shorthand-property-overrides": null,
"selector-class-pattern": null,
"no-duplicate-selectors": null,
"selector-pseudo-class-parentheses-space-inside": null,
"selector-combinator-space-before": null,
},
};
部署打包环境
在开发中,为了减少上线后遇到的并发问题或者在开发中并没发现的奇葩问题,我们可以时不时打包出静态文件,在本地看一下效果。在引入组件库之前,这一步其实尤为重要。
- 安装
cross-env
,区分环境变量,cross-env:运行跨平台设置和使用环境变量的脚本 - 安装依赖
yarn add -D cross-env
- 常规添加
.env.development、.env.test、.env.production
,并写入配置。
注意:nextjs 如果想在浏览器环境访问变量,意思就是除了构建时调用,还想在平时调用接口啥的使用,就必须添加前缀 NEXT_PUBLIC_
,否则打包后将无法访问该变量
NEXT_PUBLIC_HOST = https://junfeng530.xyz
- 添加打包脚本
"build": "cross-env NODE_ENV=test next build",
"export:test": "cross-env NODE_ENV=test next build && next export",
"export:prod": "cross-env NODE_ENV=production next build && next export",
- 安装 http-server
全局安装 http-server,npm管理安装依赖教程地址
npm install -g http-server
- 查看静态文件
yarn export:test
cd out
http-server
样式、模块化代码提示
- 安装依赖
Nextjs 已经提供了对 css 和 sass 的支持,只需要安装一下
sass
的依赖即可。
这里 next 有个坑,如果版本超过 13.1.1 ,将会报错 unhandledRejection: Error: Cannot find module 'D:\nextjs\node_modules\next\dist\compiled\sass-loader/fibers.js'
因此将 next 版本锁住降级 yarn add next@13.1.1
。问题导航
yarn add -D sass
next.config.js
配置,自定义页面扩展名,项目将会打包指定后缀的文件为页面
const path = require('path')
module.exports = {
pageExtensions: ['page.tsx', 'page.ts', 'page.jsx', 'page.js'], // 指定项目扩展名
reactStrictMode: true,
swcMinify: true,
webpack: config => {
config.resolve.alias = {
...config.resolve.alias,
'@': path.resolve(__dirname),
}
return config
},
}
index.ts
文件名换成index.page.tsx
,_app.tsx
改成_app.page.tsx
。
修改 index.page.tsx
// @/pages/index.page.tsx
import styles from './home/index.module.scss'
export default function () {
return <div className={styles.home}>官网实战</div>
}
此处应该 eslint 应该会报错:组件缺少DisplayName
,.eslintrc.js 增加规则关闭此限制
'react/display-name': 'off'
- 修改一下 pages 目录
首页还是 index.page.tsx
,使用的是 home 目录下的文件,每个页面都有类似:api.ts、index.module.scss、index.page.tsx、components
等文件
-
添加样式代码提示 页面中只能使用 cssModule 的方式,全局样式放到
@/styles
文件目录下,并在_app.tsx
中引入 -
安装 vscode 插件添加代码提示 CSS Modules
-
修改配置
next.config.js
,兼容驼峰风格
const path = require("path");
/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ["page.tsx", "page.ts", "page.jsx", "page.js"],
reactStrictMode: true,
images: {
loader: "akamai",
path: "/",
},
webpack: (config) => {
config.resolve.alias = {
...config.resolve.alias,
"@": path.resolve(__dirname),
};
const rules = config.module.rules
.find((rule) => typeof rule.oneOf === "object")
.oneOf.filter((rule) => Array.isArray(rule.use));
rules.forEach((rule) => {
rule.use.forEach((moduleLoader) => {
if (
moduleLoader.loader !== undefined &&
moduleLoader.loader.includes("css-loader") &&
typeof moduleLoader.options.modules === "object"
) {
moduleLoader.options = {
...moduleLoader.options,
modules: {
...moduleLoader.options.modules,
// This is where we allow camelCase class names
exportLocalsConvention: "camelCase",
},
};
}
});
});
return config;
},
};
module.exports = nextConfig;
在页面中引入样式.module.scss 后,使用 styls. 就会有代码提示
响应式布局
- 安装 postcss-px-to-viewport
yarn add -D postcss-px-to-viewport
- postcss.config.js
module.exports = {
plugins: {
"postcss-px-to-viewport": {
unitToConvert: "px", // 要转化的单位
viewportWidth: 1920, // 设置成设计稿宽度
unitPrecision: 5, // 转换后的精度,即小数点位数
propList: ["*"], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
viewportUnit: "vw", // 指定需要转换成的视窗单位,默认vw
fontViewportUnit: "vw", // 指定字体需要转换成的视窗单位,默认vw
selectorBlackList: [], // 指定不转换为视窗单位的类名,
minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
mediaQuery: false, // 是否在媒体查询的css代码中也进行转换,默认false,这点可以用来写固定弹窗类的样式
replace: true, // 是否转换后直接更换属性值
exclude: undefined, // 设置忽略文件,用正则做目录名匹配
include: undefined,
landscape: false, // 是否处理横屏情况
landscapeUnit: "vw",
landscapeWidth: 568,
},
},
};
媒体查询
在 px 端的适配,我们有一些弹窗它本身就很小,并不需要响应式布局,我们可以通过 postcss 的 mediaQuery
特性,我们给样式添加一个媒体查询即可避开 vw 的转换
比如:
@media (min-width: 1px) {
}
其它适配移动端媒体查询就是常规用法~
设备判断
如果根据服务器请求的 user-agent 请求头去判断设备,如果我们打开客户端没有请求那么打包后将无法正确判断设备,推荐使用以下方式:
- 安装
react-use
yarn add react-use
- 封装 hooks 方法,
@/components/useDevice.ts
import { useEffect, useState } from "react";
import { useWindowSize } from "react-use";
export const useDevice = () => {
const [isMobile, setMobile] = useState(true);
const size = useWindowSize();
useEffect(() => {
const userAgent =
typeof window.navigator === "undefined" ? "" : navigator.userAgent;
const mobile =
/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i.test(
userAgent,
);
setMobile(size.width <= 750 || mobile);
}, [size.width]);
return {
isMobile,
};
};
index.page.tsx
import { useDevice } from "@/hooks/useDevice";
const { isMobile } = useDevice();
{!isMobile && <div>pc端布局</div>}
{isMobile && <div>移动端布局</div>}
引入antd
最新版 antd5.0,采用 CSS-in-JS,CSS-in-JS 本身具有按需加载的能力,不再需要插件支持,
不再支持 babel-plugin-import
, 因此只需下载依赖,引入使用即可。antd引入官网文档
- 安装依赖
yarn add antd
- 为了兼容旧浏览器,比如在安卓微信中打开某些样式会失效,可以通过 @ant-design/cssinjs 的 StyleProvider 去除降权操作。
_app.page.tsx
import type { AppProps } from "next/app";
import "@/styles/globals.scss";
import { StyleProvider } from "@ant-design/cssinjs";
import { ConfigProvider } from "antd";
import zhCN from "antd/locale/zh_CN";
export default function App({ Component, pageProps }: AppProps) {
return (
<StyleProvider hashPriority="high">
<ConfigProvider locale={zhCN}>
<Component {...pageProps} />
</ConfigProvider>
</StyleProvider>
);
}
@/_app.page.tsx
引入antd默认样式文件(可选)
import 'antd/dist/reset.css';
@/index.page.tsx
import { Button } from "antd";
<Button type="primary">antd 按钮</Button>
- antdV5 打包 test 环境样式丢失,修改
_document.page.tsx
,问题解决参考地址
import { createCache, extractStyle, StyleProvider } from "@ant-design/cssinjs";
import Document, {
DocumentContext,
Head,
Html,
Main,
NextScript,
} from "next/document";
export default class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const cache = createCache();
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
(
<StyleProvider cache={cache}>
<App {...props} />
</StyleProvider>
),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
<style
data-test="extract"
dangerouslySetInnerHTML={{ __html: extractStyle(cache) }}
/>
</>
),
};
}
render() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
引入 antd-mobile
- 安装依赖
yarn add antd-mobile
- next.config.js
方式一:此方式会有一堆警告
experimental: {
transpilePackages: ["antd-mobile"],
},
方式二:建议用这个
yarn add -D next-transpile-modules
// 修改 next.config.js
const withTM = require('next-transpile-modules')([
'antd-mobile',
]);
module.exports = withTM({
// 你项目中其他的 Next.js 配置
});
- antd-mobile 去除
postcss-px-to-viewport
的转换
postcss.config.js
exclude: [/antd-mobile/]
- antd-mobile 会自动按需加载,只需引入使用即可
@/index.page.tsx
import { Button as ButtonMobile } from "antd-mobile";
<ButtonMobile size="large" color="primary">
antd-mobile 按钮
</ButtonMobile>
封装 axios
- 安装依赖
yarn add axios
@/utils/request.ts
import { notification } from "antd";
import type { AxiosError, AxiosRequestConfig } from "axios";
import axios from "axios";
const instance = axios.create({
timeout: 30 * 1000,
})
// 请求拦截
instance.interceptors.request.use(
config => config,
error => Promise.reject(error)
)
// 响应拦截
instance.interceptors.response.use(
res => {
if (
res.data.code !== undefined &&
res.data.code !== 0 &&
res.data.code !== 200 &&
!(res.config as AxiosRequestConfig & { skipErrorHandler?: boolean }).skipErrorHandler
) {
notification.error({
message: '异常',
description: res.data.msg || res.data.message,
})
return Promise.reject(res.data)
}
return Promise.resolve(res.data)
},
(error: AxiosError<{ code: number; message?: string; msg?: string }>) => {
const { skipErrorHandler } = error.config as AxiosRequestConfig & {
skipErrorHandler?: boolean
}
if (error.response?.status === 401 && !skipErrorHandler) {
return
}
if (!skipErrorHandler) {
notification.error({
message: '异常',
description: error.response?.data?.message || error.response?.data?.msg || error.message,
})
}
return Promise.reject(error)
}
)
type Request = <T = unknown>(
config: AxiosRequestConfig & { skipErrorHandler?: boolean }
) => Promise<T>
export const request = instance.request as Request
搭建 mock 环境
- 根目录下新增 mock 文件夹,新增如下两个文件
// mock/data.json
{
"indexStore": {
"store": {
"深圳": [
{
"name": "坂田店",
"address": "福田街道xxx",
"marker": [
114.294773,
22.587251
]
},
{
"name": "坂田店",
"address": "福田街道xxx",
"marker": [
114.294773,
22.587251
]
}
],
"广州": [
{
"name": "天河店",
"address": "天河街道xxx",
"marker": [
114.294773,
22.587251
]
},
{
"name": "天河店",
"address": "天河街道xxx",
"marker": [
114.294773,
22.587251
]
}
],
"佛山": [
{
"name": "好地方店",
"address": "而得到街道xxx",
"marker": [
114.294773,
22.587251
]
},
{
"name": "好地方店",
"address": "而得到街道xxx",
"marker": [
114.294773,
22.587251
]
}
]
},
"seo": {
"content": "坂田店、福田街道xxx、天河店、天河街道xxx、好地方店、而得到街道xxx"
}
}
}
// mock/routes.json
{
"/api/*": "/$1"
}
- 安装 json-server
yarn add -D json-server
- 同时运行 mock 以及 next dev 两个终端,安装 concurrently
yarn add -D concurrently
- 添加命令
dev:mock": "concurrently \"yarn mock\" \"next dev\"",
"mock": "cd ./mock && json-server --watch data.json --routes routes.json --port 4000"
服务端获取接口数据
nextjs 提供
getStaticProps
方法让我们在项目构建时获取服务器的静态数据,注意该方法只在 build 时执行一次,数据必须是发布时更新的才使用这个,且必须是在页面级别上使用。
mock 数据只能在本地调试使用,打包构建时记得切换
@/home/api.ts
import { request } from "@/utils/request";
export interface IMockData {
store: {
[key: string]: {
name: string;
address: string;
marker: number[];
}[];
};
seo: string;
}
// 获取mock数据
export function fetchMockData() {
return request<IMockData>({
url: `${process.env.NEXT_PUBLIC_HOST}/api/indexStore`,
method: "GET",
});
}
// export function fetchMockData() {
// return new Promise<IMockData>((resolve) => {
// resolve({
// store: {
// 深圳: [
// {
// name: "111",
// address: "222",
// marker: [11, 22],
// },
// ],
// },
// seo: "333",
// });
// });
// }
index.page.tsx
import { Button } from "antd";
import { Button as ButtonMobile } from "antd-mobile";
import { fetchMockData, IMockData } from "./home/api";
import styles from "./home/index.module.scss";
export default function (props: { mockData: IMockData }) {
console.log("mockData", props.mockData);
return (
<div>
<Button type="primary">antd 按钮</Button>
<ButtonMobile color="primary">antd-mobile 按钮</ButtonMobile>
<div className={styles["home-container"]}>官网实战</div>;
</div>
);
}
// 静态生成 SSG ,往下会介绍 getStaticProps
export async function getStaticProps() {
// 获取门店列表
const res = await fetchMockData();
const mockData = res;
return {
props: { mockData },
};
}
我们在终端控制台可以看到 mock 数据已被打印出来,之后我们就能在页面组件中通过 props 拿到它返回的数据,并可以传递给组件使用。
封装通用 Layout
import styles from "./index.module.scss";
export default function () {
return (
<div id="footer" className={styles["footer-container"]}>
底部
</div>
);
}
@/components/headSeo/index.tsx
import Head from "next/head";
export default function (seo: {
content: {
keywords: string;
description: string;
title: string;
};
}) {
return (
<Head>
<meta charSet="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;"
/>
<meta name="keywords" content={seo.content.keywords} />
<meta name="description" content={seo.content.description} />
<meta name="robots" content="index, follow" />
<meta name="applicable-device" content="pc,mobile" />
<meta name="format-detection" content="telephone=no" />
<title>{seo.content.title}</title>
</Head>
);
}
@/components/layout/index.tsx
import Footer from "../footer";
import HeadSeo from "../headSeo";
import Navbar from "../navbar";
export default function Layout(props: {
children: React.ReactNode;
seo: {
keywords: string;
description: string;
title: string;
};
}) {
return (
<>
<HeadSeo content={props.seo} />
<Navbar />
<main>{props.children}</main>
<Footer />
</>
);
}
@/components/navbar/index.tsx
import styles from "./index.module.scss";
export default function () {
return (
<div id="footer" className={styles["navbar-container"]}>
头部
</div>
);
}
- 我们可以为每个页面都传入不同的
headseo
,加到description
标签中,这样搜索引擎就可以爬取到我们这些信息。
`@/index.page.tsx
import { Button } from "antd";
import { Button as ButtonMobile } from "antd-mobile";
import Layout from "../components/layout";
import type { IMockData } from "./home/api";
import { fetchMockData } from "./home/api";
import styles from "./home/index.module.scss";
export default function (props: { mockData: IMockData }) {
console.log("mockData", props.mockData);
const headSeo = {
keywords: "sso、nextjs、antd、jiang",
description: `seo实践 ${props.mockData.seo}`,
title: "nextJs 官网 SSR 实战",
};
return (
<Layout seo={headSeo}>
<div>
<Button type="primary">antd 按钮</Button>
<ButtonMobile color="primary">antd-mobile 按钮</ButtonMobile>
<div className={styles["home-container"]}>官网实战</div>;
</div>
</Layout>
);
}
export async function getStaticProps() {
// 获取mock数据
const res = await fetchMockData();
const mockData = res;
return {
props: { mockData },
};
}
图片优化 webp + cdn
- 封装 useWebp hooks,
@/hooks/useWebp.ts
import { useEffect, useState } from "react";
export const useWebp = () => {
const [isSupportWebp, setIsSupportWebp] = useState(true);
useEffect(() => {
if (typeof window !== "undefined") {
const supportWebp =
window.document
.createElement("canvas")
.toDataURL("image/webp")
.indexOf("data:image/webp") > -1;
setIsSupportWebp(supportWebp);
}
}, []);
return {
isSupportWebp,
};
};
- 封装 useOss hooks ,
@/hooks/useOss.ts
import { useCallback } from "react";
import { useWebp } from "./useWebp";
export const useOSS = () => {
const { isSupportWebp } = useWebp();
const getOssImage = useCallback(
(option: {
originUrl: string;
/**
* @description 不支持 webp,降级处理宽度
* @type {number}
*/
notSupportWebpWidth?: number;
/**
* @description 不支持 webp,降级处理高度
* @type {number}
*/
notSupportWebpHeight?: number;
width?: number; // 不使用 oss,正常传即可
height?: number;
}) => {
let process = "";
if ((option.notSupportWebpWidth && !isSupportWebp) || option.width) {
process = `w_${option.notSupportWebpWidth || option.width},`;
}
if ((option.notSupportWebpHeight && !isSupportWebp) || option.height) {
process = `${process}h_${
option.notSupportWebpHeight || option.height
},`;
}
if (process) {
process = `x-oss-process=image/resize,m_fill,limit_0,${process},`;
}
if (isSupportWebp && process) {
process = `${process}/format,webp`;
}
if (isSupportWebp && !process) {
process = `x-oss-process=image/format,webp`;
}
return `${option.originUrl}?${process}`;
},
[isSupportWebp],
);
return { getOssImage };
};
- 封装 ossImage 组件,
@/components/OssImage/index.tsx
/* eslint-disable react/require-default-props */
import { useOSS } from "@/hooks/useOss";
type Props = React.DetailedHTMLProps<
React.ImgHTMLAttributes<HTMLImageElement>,
HTMLImageElement
> & {
notSupportWebpWidth?: number;
notSupportWebpHeight?: number;
ossWidth?: number;
ossHeight?: number;
};
export default function (props: Props) {
const { getOssImage } = useOSS();
return (
<img
{...props}
src={getOssImage({
originUrl: props.src || "",
notSupportWebpWidth: props.notSupportWebpWidth,
notSupportWebpHeight: props.notSupportWebpHeight,
width: props.ossWidth,
height: props.ossHeight,
})}
loading="lazy"
/>
);
}
- 在页面中使用
index.page.tsx
import OssImage from "@/components/OssImage";
{/* 使用 oss,自动判断是否支持 webp*/}
<OssImage
style={{
background: "beige",
}}
src="https://img.alicdn.com/tfs/TB11B9iM7voK1RjSZPfXXXPKFXa-338-80.png"
notSupportWebpWidth={338}
notSupportWebpHeight={80}
></OssImage>
{/* 不使用 oss,正常传宽高*/}
<OssImage
style={{
background: "beige",
}}
src="https://img.alicdn.com/tfs/TB11B9iM7voK1RjSZPfXXXPKFXa-338-80.png"
width={338}
height={80}
></OssImage>
数据渲染
- next 会根据导出的函数来区分这个页面是哪种渲染,这两个函数(
getStaticProps
、getServerSideProps
)只能存在一个 - 调用时机都是在浏览器渲染之前,也就是说没有
document、window
之类的对象,开发时,请在终端查看数据
getStaticProps SSG (静态生成)
- 项目构建打包时调用,并生成 html(开发时是每次请求都更新),理解为写死了传到服务器上,想要更新请重新打包。
- 适用于不变的数据,能够做 seo
- 在页面中使用,
index.page.tsx
// 静态 SSG
export async function getStaticProps() {
// 获取mock数据
const res = await fetchMockData();
const mockData = res;
return {
props: { mockData },
};
}
getServerSideProps SSR (服务端渲染)
- 每次在服务器接收到请求时更新
- 适用于经常改变的数据,无法做 seo
- getServerSideProps 返回值除了可以设置 props 外还可以使用
notFound
来强制页面跳转到 404,或者是使用redirect
来将页面重定向。
export async function getServerSideProps() {
const data = await fetchMockData();
console.log("data", data);
if (!data) {
return {
redirect: {
destination: "/",
permanent: false,
},
// return {
// notFound: true
// };
};
}
return {
props: { data },
};
}
getStaticPaths 生成多页面
比如我们的项目有一个新闻页面,它需要做 seo,这样一个页面肯定无法满足,我们可以通过 getStaticPaths 去生成多个页面,搭配 getStaticProps 去构造每个页面不同的页面数据,文件名只需要使用
[变量名].page.tsx
@/static-path/[id].page.tsx
export default function ({ post }: { post: string }) {
return (
<div>
<h1>Post: {post}</h1>
</div>
);
}
export async function getStaticPaths() {
const paths = new Array(10).fill(0).map((_, i) => ({
params: { id: i + 1 + "" },
}));
console.log("paths", paths);
return { paths, fallback: false };
}
export async function getStaticProps({ params }: { params: { id: string } }) {
// 在这里我们可以获取需要的数据,然后根据不同的 id 去返回到页面上
console.log("params", params);
return { props: { post: `post ${params.id}` } };
}
总结
服务器部署自动上传可以参考我的文章
至此初步的项目结构已经完成,本文将持续更新,后面将加入埋点、监控系统、后台管理系统等,同时也会将尝试将自己的博客换成 ssr 方式。