Skip to content

xfz1987/nextjs-blog

Repository files navigation

基于 React 的 framework 框架,它包含了 SSR、ISG、SSG 渲染等,非常简单方便,它抽象了自动配置工具,无绪关心工程化配置,专注于 coding 逻辑 npx create-next-app@latest [project]

Core API 静态路由 根据 pages 下的文件/文件夹自动配置路由 /index.tsx localhost:3000/ /about.tsx localhost:3000/about /help localhost:3000/help 动态路由 比如 posts 文件夹下: ● [...params].tsx/ts ○ localhost:3000/posts/a,localhost:3000/posts/a/b,localhost:3000/posts/a/b/... ● [id].tsx/ts ○ localhost:3000/posts/1,localhost:3000/posts/[id] ● [[id]].tsx/ts 可有可无 ○ localhost:3000/posts,localhost:3000/posts/[id] ● [id]/index.tsx 或者 [id]/page.tsx localhost:3000/posts/[id]

(auth) /login/index.tsx --> localhost:3000/login 生成无数据静态网页 next export SSG(静态生成) / ISR(增量静态生成) ● 通过 getStaticPaths 和 getStatifcProps 这两个方法在服务端 build 时生成静态页面 // pages/posts/[id].tsx

// react 组件 const Page = ({ data }: any) => { return (

{data}
); };

export default Page;

/*** 静态生成:SSG */ export async function getStaticProps(context: NextPageContext) { const { id } = context.params; const data = await fetch(https://api/${id}/).then(data => data.json());

return {
	props: { data },
};

}

export async function getStaticPaths() { return { paths: [{ params: { id: '1' } }, { params: { id: '2' } }], fallback: false, // ‘blocking’ / true / false // ‘blocking’ 按需生成 // false 没有静态页面,则 404 // true 自己返回 loading 等待静态生成 }; } ● getStaticPaths 通过配置,我们在 dev/build 时,预先生成相关的静态页面,这里生成如下: ○ localhost:3000/posts/1 和 localhost:3000/posts/2 ○ fallback ■ false:localhost:3000/posts/3 --> 404 ■ blocking: localhost:3000/posts/3 --> 访问时生成静态页面,用户等待静态页面生成完成时,才渲染页面(服务端压力增加,不要使用), ■ true:localhost:3000/posts/3 --> 访问时,用户端可以监控路由的 isFallback,先显示 loading 提示,待静态页面生成后,显示渲染的内容(服务端压力增加,不要使用) ● ○ 说白了,就是 blocking 和 true,是按需生成静态,当用户访问路由时,才会生成,这样效率和性能都不好,不要使用, ● ISR:增量静态色生成(不建议使用) ○ 自动触发:getStaticProps 设置 revalidata,比如设置为 10,表示当前页面在服务端的超时时间为 10s,当用户访问时,如果没超过 10s 中,则使用原来的生成静态页面,否则重新生成新的静态页面 ■ ○ 手动触发 ■ 不建议使用

SSR 使用 getServerSideProps // pages/posts/[id].tsx

// react 组件 const Page = ({ data }: any) => { return (

{data}
); };

// SSR,这里的代码在 server 端执行 export async function getServerSideProps(context: NextPageContext) { const { id } = context.params; const data = await fetch(https://api/${id}/).then(data => data.json());

return {
	props: { data },
};

} a component has only getServerSideProps or getStaticProps 不能同时存在一个组件中,说白了,要么是 SSG,要么是 SSR

Hit the road FileSystem router

[id] 与 [..slug] all exist 会优先匹配 [id] ● http://localhost:3000/events/a -> [eventId] ● http://localhost:3000/events/a/b -> [...slug] [...slug] 获取参数

Link 和手动设置路由

预渲染 why ● 传统 React: 应用返回的 HTML 文件中,不包含应用的信息,因为页面是在客户端进行渲染的,所以服务端返回的源码通常只有一个 id 为 root 的 div 标签,不利于做 SEO(搜索引擎优化) ● pre-rendering:浏览器收到的 HTML 文件源码是包含了页面信息的代码,Good for SEO

  • pre-rendering will not execute useEffect in server SSG SSG 是静态站点生成,就是在文件打包阶段,预先生成页面 ● Next.js 默认会预渲染所有没有动态数据的页面,而动态的数据还是像 React 一样在客户端渲染的 ● 如果要在 HTML 源码中展现动态数据,可以使用 page 下 getStaticProps 方法。这个方法是跑在服务端环境下的,可以在服务端获取数据并渲染,并且客户端不会收到方法内任何的代码。此外,Next.js 拓展了一些功能,比如 fetch 是浏览器的接口,在服务端是不能用的,而在 getStaticProps 方法中是可以使用 fetch API 的 (通过 node-fetch 这个库实现的。(Node.js18.0.0 版本开始原生支持了 fetch 方法) getStaticProps

export type GetStaticPropsResult

= | { props: P; revalidate?: number | boolean } | { redirect: Redirect; revalidate?: number | boolean } | { notFound: true; revalidate?: number | boolean } ● props 是服务端获取的需要传给组件的数据,revalidate 可以定义生产环境下 getStaticProps 调用的间隔秒数,600 就是 600 秒,10 分钟。测试环境下这个配置项无效,每次访问页面都会触发此方法。而后两种情况适用于获取数据失败时,引导用户进行下一步操作,重定向或直接返回 404 错误。此外如果需要在 getStaticProps 中访问路径参数,可以在方法的 context 参数的 params 属性获取 getStaticPaths 上面指的没有动态数据的页面,也不能是动态路由(文件名带[]的 js),否则也不会自动生成静态页面。如果需要生成静态页面,需要使用 getStaticPaths 方法。 ● getStaticPaths 方法定义了一组需要生成静态页面的列表,每项数据都会调用 getStaticProps 来获取数据,所以要使用 getStaticPaths 一定先要有定义 getStaticProps export async function getStaticPaths() { return { paths: [{ params: { id: '1' } }, { params: { id: '2' } }], fallback: false, } } SSR SSR 是服务端渲染,getServerSideProps 方法可以针对每次请求作出处理,适用于数据变化比较频繁的页面 ● getStaticProps 与 getServerSideProps 只能二选一 ● getServerSideProps 也是运行在服务器上的方法,这个方法的参数 context 可以完整获取请求的所有数据 ● 没有 revalidate 属性,因为每次请求都会重新渲染 export type GetServerSidePropsContext< Q extends ParsedUrlQuery = ParsedUrlQuery, D extends PreviewData = PreviewData

= ({ req: IncomingMessage & {

cookies: NextApiRequestCookies

} res: ServerResponse params?: Q query: ParsedUrlQuery preview?: boolean previewData?: D resolvedUrl: string locale?: string locales?: string[] defaultLocale?: string }) => GetServerSidePropsResult

export type GetServerSidePropsResult

= | { props: P | Promise

} | { redirect: Redirect } | { notFound: true }

不适合预渲染的情况 以下三种情况不适合使用服务端预渲染: ● 数据变化非常频繁的页面(比如股票数据) ● 与用户身份高度耦合的页面(比如用户信息) ● 页面中只有某一小部分数据不同的情况

碰到这些情况,还是在客户端使用 useEffect 中 fetch 来获取数据,Next.js 团队也编写了一个 React 钩子库 SWR(https://swr.vercel.app/zh-CN) 来简化客户端请求 import useSWR from 'swr'

const fetcher = (...args) => fetch(...args).then((res) => res.json())

function Profile() { const { data, error } = useSWR('/api/profile-data', fetcher)

if (error) return

Failed to load
if (!data) return
Loading...

return (

{data.name}

{data.bio}

) }

SEC-Head+Meta 统一加上 Head 信息 ● 所有页面都都会统一加上 head 信息

页面独立 Head ● 页面中加入的 Head 会 merge _app 中的 head,做到个性化

多个 Head 会 merge

根据数据动态设置 Head

_document.js _app.js 相当于 body 中的内容,_document.js 相当于整个 HTML 文档 ● _app.js 会进入到 div#_next 容器下(默认自动创建) import { Html, Head, Main, NextScript } from 'next/document'

export default function Document() { return (

) }

● MyDocument import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document { render() { return (

); } }

export default MyDocument;

图片优化 Next.js 提供了优化图片的方案——Image 组件,使用 Image 组件有四点好处: ● 优化图片大小:webp 格式 ○ 对各个设备使用合适的尺寸与格式(使用 Chrome 访问页面时,图片会转换成 webp 格式) ● 防止 CLS(累计布局偏移) ● 懒加载:图片在视图中才会被加载 ● 自定义图片尺寸,width、height ○ Next.js 会根据 Image 的 width 与 height 值,在页面请求服务端时,转换并缓存相应大小的图片 import Image from 'next/image'

export default function About(props) { return <> <Image src={'/img.jpeg'} alt="图片" width={100} height={100} /> <img src={'/img.jpeg'} alt="图片" /> </> }

API 路由 /pages/api 文件下的 JS 文件不会导出页面组件,Next.js 会将这些文件映射成 /api/* 的 API 端点,与文件路由创建一样,也支持动态路由 ● Wonderful,next 会缓存 get 请求,自动开启 Etag,下次请求进行协商缓存 Etag,当服务端数据又变化,Etag 会变化 -> 200,没有变化 --> 304 export default function handler(req, res) { if (req.method === 'POST') { // 处理 POST 请求 } else { // 处理其他 HTTP 方法请求 // res.status(200).json({ message: 'ok' }); } } ● 类型 export declare type NextApiHandler<T = any> = (req: NextApiRequest, res: NextApiResponse) => unknown | Promise;

quest extends IncomingMessage { /** _ Object of query values from url _/ query: Partial<{ [key: string]: string | string[]; }>; /** _ Object of cookies from header _/ cookies: Partial<{ [key: string]: string; }>; body: any; env: Env; preview?: boolean; /** _ Preview data set on the request, if any _ */ previewData?: PreviewData; }

export declare type NextApiResponse<T = any> = ServerResponse & { /** _ Send data any data in response _/ send: Send; /** _ Send data json data in response _/ json: Send; status: (statusCode: number) => NextApiResponse; redirect(url: string): NextApiResponse; redirect(status: number, url: string): NextApiResponse; /** _ Set preview data for Next.js' prerender mode _/ setPreviewData: (data: object | string, options?: { /** _ Specifies the number (in seconds) for the preview session to last for. _ The given number will be converted to an integer by rounding down. _ By default, no maximum age is set and the preview session finishes _ when the client shuts down (browser is closed). _/ maxAge?: number; /** _ Specifies the path for the preview session to work under. By default, _ the path is considered the "default path", i.e., any pages under "/". _/ path?: string; }) => NextApiResponse; /** _ Clear preview data for Next.js' prerender mode _/ clearPreviewData: (options?: { path?: string; }) => NextApiResponse; revalidate: (urlPath: string, opts?: { unstable_onlyGenerated?: boolean; }) => Promise; };

● API 路由映射 ○ /pages/api/feedback/index.js <===> GET api/feedback ■ /pages/api/posts/[postId].js <===> GET api/posts/12345 ○ /pages/api/feedback/index.js <===> POST api/feedback // GET function loadFeedbackHandler() { fetch('/api/feedback') .then(response => response.json()) .then(data => { setFeedbackItems(data.data); }); }

// POST fetch('/api/feedback', { method: 'POST', body: JSON.stringify(reqBody), headers: { 'Content-Type': 'application/json', }, }) .then(response => response.json()) .then(data => console.log(data));

MongoDB

使用云 MongoDB https://cloud.mongodb.com/ 注册 --> 创建集群 Cluster

Database Access 创建数据库 user

Network Access RESTFUL ● npm i -S mongodb ● 连接 URL ○ 'mongodb+srv://username:@cluster0.jdiygge.mongodb.net/<db_name>?retryWrites=true&w=majority' ○ await MongoClient.connect(url) ==> promise

● db 工具 // /helpers/db-util import { MongoClient, ObjectId } from 'mongodb';

export async function connectDatabase() { const client = await MongoClient.connect( 'mongodb+srv://xfz:[email protected]/test?retryWrites=true&w=majority' );

return client; }

export async function insertDocument(client, collection, document) { const db = client.db();

const result = await db.collection(collection).insertOne(document);

return result; }

export async function getAllDocuments(client, collection, sort) { const db = client.db();

const documents = await db.collection(collection).find().sort(sort).toArray();

return documents; }

export async function getDocumentById(client, collection, id) { const db = client.db(); const document = await db .collection(collection) .find({ _id: new ObjectId(id) }) .toArray();

return document[0]; }

export async function deleteDocumentById(client, collection, id) { const db = client.db(); const document = await db.collection(collection).deleteOne({ _id: new ObjectId(id) }); const { deletedCount } = document; return deletedCount > 0; }

● API ○ /api/list GET & POST ○ /api/detail/[id] DELETE // /api/list import { connectDatabase, getAllDocuments, insertDocument } from '../../helpers/db-util';

export default async function handler(req, res) { let client;

try {
	client = await connectDatabase();
} catch (error) {
	res.status(500).json({ message: 'Connecting to the database failed!' });
	return;
}

if (req.method === 'GET') {
	try {
		// products - table
		const documents = await getAllDocuments(client, 'products', { _id: -1 });
		res.status(200).json({ data: documents });
		console.log('查询所有数据', documents);
	} catch (error) {
		res.status(500).json({ message: 'Getting comments failed.' });
	}
} else {
	const { text } = req.body;
	if (!text || !text.trim()) {
		res.status(422).json({ message: 'Invalid input.' });
		client.close();
		return;
	}

	const newProduct = { text };
	try {
		const result = await insertDocument(client, 'products', newProduct);
		console.log('insert success:', result.insertedId);
		res.status(201).json({ message: 'Added success.', data: { _id: result.insertedId, ...newProduct } });
	} catch (error) {
		res.status(500).json({ message: 'Inserting comment failed!' });
	}
}

client.close();

}

// /api/detial/[id] import { connectDatabase, getDocumentById, deleteDocumentById } from '../../../helpers/db-util';

export default async function handler(req, res) { let client;

try {
	client = await connectDatabase();
} catch (error) {
	res.status(500).json({ message: 'Connecting to the database failed!' });
	return;
}

if (req.method === 'DELETE') {
	try {
		const { id } = req.query;
		console.log(`delete id: ${id}`);
		const response = await getDocumentById(client, 'products', id);
		if (!response) {
			res.status(500).json({ message: 'there is no data by id', result: false });
		} else {
			const result = await deleteDocumentById(client, 'products', id);
			if (result) {
				res.status(200).json({ message: 'delete success', result: true });
			} else {
				res.status(500).json({ message: 'there is no data by id', result: false });
			}
		}
	} catch (e) {
		console.log(e);
		res.status(500).json({ message: 'Delete failed!', result: false });
	}
}

client.close();

}

● client code // GET try { const res = await fetch('http://localhost:3000/api/list'); const resData = await res.json(); data = resData.data; } catch (e) { console.log(e); }

// POST try { setLoading(true); const result = await fetch('/api/list', { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json', }, }); const res = await result.json(); setList([...list, res.data]); inputRef.current.value = ''; } catch (err) { console.error(err); } finally { setLoading(false); }

// DELETE try { const response = await fetch(/api/detail/${id}, { method: 'DELETE', }); const { result } = await response.json(); if (result) { setList(prev => prev.filter(item => item._id !== id)); } } catch (e) { console.error(e); }

部署 构建 构建 Next.js 应用有两种方式 ● 标准构建:next build,前端+nodejs ○ 使用这种方式构建,我们会得到优化后的前端项目 + 一个 NodeJS 服务端程序。这个服务端程序提供了 API 路由、SSR 与页面重验证等功能。所以如果要部署这个应用,需要服务器有 NodeJS 环境 ● 完全静态构建:next export,只有前端 ○ 使用这种方式生成的代码,只会包含纯前端的内容,HTML、CSS、JS 以及静态资源。没有 NodeJS 服务端程序,所以部署可以不需要 NodeJS 环境。当然这样的话,API 路由、SSR 等 Next.js 提供的特性就不能使用了 配置 next.config.js /*_ @type {import('next').NextConfig} _/ const nextConfig = { reactStrictMode: true, }

module.exports = nextConfig 这个文件中的代码也是服务端代码,在构建过程中以及构建生成的 NodeJS 服务端程序中会使用到。此外这个文件不会被 Webpack, Babel 或 TypeScript 处理,所以确保使用与机器 NodeJS 版本相匹配的语法

完整的配置项接口如下,不过一般还是看官方文档(https://nextjs.org/docs/api-reference/next.config.js/introduction)根据具体需求来配置 export interface NextConfig extends Record<string, any> { exportPathMap?: (defaultMap: ExportPathMap, ctx: { dev: boolean; dir: string; outDir: string | null; distDir: string; buildId: string; }) => Promise | ExportPathMap; /** _ Internationalization configuration _ _ @see Internationalization docs _/ i18n?: I18NConfig | null; /** _ @since version 11 _ @see ESLint configuration _/ eslint?: ESLintConfig; /** _ @see Next.js TypeScript documentation _/ typescript?: TypeScriptConfig; /** _ Headers allow you to set custom HTTP headers for an incoming request path. * _ @see Headers configuration documentation _/ headers?: () => Promise<Header[]>; /** _ Rewrites allow you to map an incoming request path to a different destination path. _ _ @see Rewrites configuration documentation _/ rewrites?: () => Promise<Rewrite[] | { beforeFiles: Rewrite[]; afterFiles: Rewrite[]; fallback: Rewrite[]; }>; /** _ Redirects allow you to redirect an incoming request path to a different destination path. _ _ @see Redirects configuration documentation _/ redirects?: () => Promise<Redirect[]>; /** _ @see Moment.js locales excluded by default _/ excludeDefaultMomentLocales?: boolean; /** _ Before continuing to add custom webpack configuration to your application make sure Next.js doesn't already support your use-case _ _ @see Custom Webpack Config documentation _/ webpack?: NextJsWebpackConfig | null; /** _ By default Next.js will redirect urls with trailing slashes to their counterpart without a trailing slash. _ _ @default false _ @see Trailing Slash Configuration */ trailingSlash?: boolean; /** _ Next.js comes with built-in support for environment variables _ _ @see Environment Variables documentation _/ env?: Record<string, string>; /** _ Destination directory (defaults to .next) _/ distDir?: string; /** _ The build output directory (defaults to .next) is now cleared by default except for the Next.js caches. _/ cleanDistDir?: boolean; /** _ To set up a CDN, you can set up an asset prefix and configure your CDN's origin to resolve to the domain that Next.js is hosted on. _ _ @see CDN Support with Asset Prefix _/ assetPrefix?: string; /** _ By default, Next will serve each file in the pages folder under a pathname matching the filename. _ To disable this behavior and prevent routing based set this to true. * _ @default true _ @see Disabling file-system routing _/ useFileSystemPublicRoutes?: boolean; /** _ @see Configuring the build ID _/ generateBuildId?: () => string | null | Promise<string | null>; /** @see Disabling ETag Configuration _/ generateEtags?: boolean; /** @see Including non-page files in the pages directory */ pageExtensions?: string[]; /** @see Compression documentation _/ compress?: boolean; /** _ The field should only be used when a Next.js project is not hosted on Vercel while using Vercel Analytics. _ Vercel provides zero-configuration analytics for Next.js projects hosted on Vercel. _ _ @default '' _ @see Next.js Analytics _/ analyticsId?: string; /** @see Disabling x-powered-by _/ poweredByHeader?: boolean; /** @see Using the Image Component */ images?: ImageConfig; /** Configure indicators in development environment _/ devIndicators?: { /** Show "building..."" indicator in development _/ buildActivity?: boolean; /** Position of "building..." indicator in browser */ buildActivityPosition?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'; }; /** _ Next.js exposes some options that give you some control over how the server will dispose or keep in memory built pages in development. _ _ @see Configuring onDemandEntries _/ onDemandEntries?: { /** period (in ms) where the server will keep pages in the buffer */ maxInactiveAge?: number; /** number of pages that should be kept simultaneously without being disposed _/ pagesBufferLength?: number; }; /** @see next/amp _/ amp?: { canonicalBase?: string; }; /** _ Deploy a Next.js application under a sub-path of a domain _ _ @see Base path configuration _/ basePath?: string; /** @see Customizing sass options _/ sassOptions?: { [key: string]: any; }; /** _ Enable browser source map generation during the production build * _ @see Source Maps _/ productionBrowserSourceMaps?: boolean; /** _ By default, Next.js will automatically inline font CSS at build time _ _ @default true _ @since version 10.2 _ @see Font Optimization _/ optimizeFonts?: boolean; /** _ The Next.js runtime is Strict Mode-compliant. _ _ @see React Strict Mode _/ reactStrictMode?: boolean | null; /** _ Add public (in browser) runtime configuration to your app _ _ @see Runtime configuration _/ publicRuntimeConfig?: { [key: string]: any; }; /** _ Add server runtime configuration to your app _ _ @see Runtime configuration _/ serverRuntimeConfig?: { [key: string]: any; }; /** _ Next.js automatically polyfills node-fetch and enables HTTP Keep-Alive by default. _ You may want to disable HTTP Keep-Alive for certain fetch() calls or globally. * _ @see Disabling HTTP Keep-Alive _/ httpAgentOptions?: { keepAlive?: boolean; }; /** _ During a build, Next.js will automatically trace each page and its dependencies to determine all of the files _ that are needed for deploying a production version of your application. * _ @see Output File Tracing _/ outputFileTracing?: boolean; /** _ Timeout after waiting to generate static pages in seconds _ _ @default 60 _/ staticPageGenerationTimeout?: number; /** _ Add "crossorigin" attribute to generated <script> elements generated by <Head /> or <NextScript /> components _ * _ @see crossorigin attribute documentation _/ crossOrigin?: false | 'anonymous' | 'use-credentials'; /** _ Use SWC compiler to minify the generated JavaScript _ _ @see SWC Minification _/ swcMinify?: boolean; /** _ Optionally enable compiler transforms _ _ @see Supported Compiler Options _/ compiler?: { reactRemoveProperties?: boolean | { properties?: string[]; }; relay?: { src: string; artifactDirectory?: string; language?: 'typescript' | 'javascript' | 'flow'; }; removeConsole?: boolean | { exclude?: string[]; }; styledComponents?: boolean | { /** _ Enabled by default in development, disabled in production to reduce file size, _ setting this will override the default for all environments. */ displayName?: boolean; topLevelImportPaths?: string[]; ssr?: boolean; fileName?: boolean; meaninglessFileNames?: string[]; minify?: boolean; transpileTemplateLiterals?: boolean; namespace?: string; pure?: boolean; cssProp?: boolean; }; emotion?: boolean | { sourceMap?: boolean; autoLabel?: 'dev-only' | 'always' | 'never'; labelFormat?: string; importMap?: { [importName: string]: { [exportName: string]: { canonicalImport?: [string, string]; styledBaseImport?: [string, string]; }; }; }; }; }; output?: 'standalone'; transpilePackages?: string[]; skipMiddlewareUrlNormalize?: boolean; skipTrailingSlashRedirect?: boolean; modularizeImports?: Record<string, { transform: string; preventFullImport?: boolean; skipDefaultConversion?: boolean; }>; /** _ Enable experimental features. Note that all experimental features are subject to breaking changes in the future. _/ experimental?: ExperimentalConfig; }

这个文件还可以通过函数来根据不同环境返回不同的配置参数 module.exports = async (phase, { defaultConfig }) => { /**

  • @type {import('next').NextConfig} / const nextConfig = { / 配置 */ } return nextConfig }

phase 参数会根据不同的 next 命令来传入不同的值 export declare const PHASE_EXPORT = "phase-export"; export declare const PHASE_PRODUCTION_BUILD = "phase-production-build"; export declare const PHASE_PRODUCTION_SERVER = "phase-production-server"; export declare const PHASE_DEVELOPMENT_SERVER = "phase-development-server"; export declare const PHASE_TEST = "phase-test";

通过 next/constants 模块引入常量 const { PHASE_PRODUCTION_BUILD } = require('next/constants')

module.exports = (phase, { defaultConfig }) => { if (phase === PHASE_PRODUCTION_BUILD) { return { /_ 生产构建配置 _/ } }

return { /_ 其他情况配置 _/ } }

部署 ● vercel ● cloudflare