简介
基于React的framework框架,它包含了SSR、ISG、SSG渲染等,非常简单方便,它抽象了自动配置工具,无需关心工程化配置,专注于coding逻辑
- 安装 npx create-next-app@latest [project]
Core API
静态路由
根据pages下的文件/文件夹自动配置路由
bash
/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时生成静态页面
javacript
// pages/posts/[id].tsx
// react 组件
const Page = ({ data }: any) => {
return (
<div>
{data}
</div>
);
};
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
javascript
// pages/posts/[id].tsx
// react 组件
const Page = ({ data }: any) => {
return (
<div>
{data}
</div>
);
};
// 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
typescript
export type GetStaticPropsResult<P> =
| { 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
csharp
export async function getStaticPaths() {
return {
paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
fallback: false,
}
}
SSR
SSR 是服务端渲染,getServerSideProps 方法可以针对每次请求作出处理,适用于数据变化比较频繁的页面
- getStaticProps 与 getServerSideProps 只能二选一
- getServerSideProps 也是运行在服务器上的方法,这个方法的参数 context 可以完整获取请求的所有数据
- 没有 revalidate 属性,因为每次请求都会重新渲染
typescript
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<P> =
| { props: P | Promise<P> }
| { redirect: Redirect }
| { notFound: true }
不适合预渲染的情况
以下三种情况不适合使用服务端预渲染:
- 数据变化非常频繁的页面(比如股票数据)
- 与用户身份高度耦合的页面(比如用户信息)
- 页面中只有某一小部分数据不同的情况
碰到这些情况,还是在客户端使用 useEffect 中 fetch 来获取数据,Next.js 团队也编写了一个React 钩子库 SWR(swr.vercel.app/zh-CN) 来简化客户端请求
javascript
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 <div>Failed to load</div>
if (!data) return <div>Loading...</div>
return (
<div>
<h1>{data.name}</h1>
<p>{data.bio}</p>
</div>
)
}
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 容器下(默认自动创建)
javascript
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
- MyDocument
javascript
import Document, { Html, Head, Main, NextScript } from 'next/document';
class MyDocument extends Document {
render() {
return (
<Html lang='en'>
<Head />
<body>
<div id='overlays' />
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
图片优化
Next.js 提供了优化图片的方案------Image 组件,使用 Image 组件有四点好处:
- 优化图片大小:webp格式
-
- 对各个设备使用合适的尺寸与格式(使用Chrome访问页面时,图片会转换成webp格式)
- 防止CLS(累计布局偏移)
- 懒加载:图片在视图中才会被加载
- 自定义图片尺寸,width、height
-
- Next.js 会根据 Image 的 width 与 height 值,在页面请求服务端时,转换并缓存相应大小的图片
javascript
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
javascript
export default function handler(req, res) {
if (req.method === 'POST') {
// 处理POST请求
} else {
// 处理其他HTTP方法请求
// res.status(200).json({ message: 'ok' });
}
}
- 类型
typescript
export declare type NextApiHandler<T = any> = (req: NextApiRequest, res: NextApiResponse<T>) => unknown | Promise<unknown>;
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<T>;
/**
* Send data `json` data in response
*/
json: Send<T>;
status: (statusCode: number) => NextApiResponse<T>;
redirect(url: string): NextApiResponse<T>;
redirect(status: number, url: string): NextApiResponse<T>;
/**
* 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<T>;
/**
* Clear preview data for Next.js' prerender mode
*/
clearPreviewData: (options?: {
path?: string;
}) => NextApiResponse<T>;
revalidate: (urlPath: string, opts?: {
unstable_onlyGenerated?: boolean;
}) => Promise<void>;
};
- 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
javascript
// 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
注册 --> 创建集群 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 工具
javascript
// /helpers/db-util
import { MongoClient, ObjectId } from 'mongodb';
export async function connectDatabase() {
const client = await MongoClient.connect(
'mongodb+srv://xxx:xxxxxx@cluster0.jdiygge.mongodb.net/xxx?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
vbnet
// /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();
}
javascript
// /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
javascript
// 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
java
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}
module.exports = nextConfig
这个文件中的代码也是服务端代码,在构建过程中以及构建生成的 NodeJS 服务端程序中会使用到。此外这个文件不会被 Webpack, Babel 或 TypeScript 处理,所以确保使用与机器NodeJS 版本相匹配的语法
完整的配置项接口如下,不过一般还是看官方文档(nextjs.org/docs/api-re...)根据具体需求来配置
css
export interface NextConfig extends Record<string, any> {
exportPathMap?: (defaultMap: ExportPathMap, ctx: {
dev: boolean;
dir: string;
outDir: string | null;
distDir: string;
buildId: string;
}) => Promise<ExportPathMap> | ExportPathMap;
/**
* Internationalization configuration
*
* @see [Internationalization docs](https://nextjs.org/docs/advanced-features/i18n-routing)
*/
i18n?: I18NConfig | null;
/**
* @since version 11
* @see [ESLint configuration](https://nextjs.org/docs/basic-features/eslint)
*/
eslint?: ESLintConfig;
/**
* @see [Next.js TypeScript documentation](https://nextjs.org/docs/basic-features/typescript)
*/
typescript?: TypeScriptConfig;
/**
* Headers allow you to set custom HTTP headers for an incoming request path.
*
* @see [Headers configuration documentation](https://nextjs.org/docs/api-reference/next.config.js/headers)
*/
headers?: () => Promise<Header[]>;
/**
* Rewrites allow you to map an incoming request path to a different destination path.
*
* @see [Rewrites configuration documentation](https://nextjs.org/docs/api-reference/next.config.js/rewrites)
*/
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](https://nextjs.org/docs/api-reference/next.config.js/redirects)
*/
redirects?: () => Promise<Redirect[]>;
/**
* @see [Moment.js locales excluded by default](https://nextjs.org/docs/upgrading#momentjs-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](https://nextjs.org/docs/api-reference/next.config.js/custom-webpack-config)
*/
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](https://nextjs.org/docs/api-reference/next.config.js/trailing-slash)
*/
trailingSlash?: boolean;
/**
* Next.js comes with built-in support for environment variables
*
* @see [Environment Variables documentation](https://nextjs.org/docs/api-reference/next.config.js/environment-variables)
*/
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](https://nextjs.org/docs/api-reference/next.config.js/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](https://nextjs.org/docs/advanced-features/custom-server#disabling-file-system-routing)
*/
useFileSystemPublicRoutes?: boolean;
/**
* @see [Configuring the build ID](https://nextjs.org/docs/api-reference/next.config.js/configuring-the-build-id)
*/
generateBuildId?: () => string | null | Promise<string | null>;
/** @see [Disabling ETag Configuration](https://nextjs.org/docs/api-reference/next.config.js/disabling-etag-generation) */
generateEtags?: boolean;
/** @see [Including non-page files in the pages directory](https://nextjs.org/docs/api-reference/next.config.js/custom-page-extensions) */
pageExtensions?: string[];
/** @see [Compression documentation](https://nextjs.org/docs/api-reference/next.config.js/compression) */
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](https://nextjs.org/analytics)
*/
analyticsId?: string;
/** @see [Disabling x-powered-by](https://nextjs.org/docs/api-reference/next.config.js/disabling-x-powered-by) */
poweredByHeader?: boolean;
/** @see [Using the Image Component](https://nextjs.org/docs/basic-features/image-optimization#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`](https://nextjs.org/docs/api-reference/next.config.js/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`](https://nextjs.org/docs/api-reference/next/amp) */
amp?: {
canonicalBase?: string;
};
/**
* Deploy a Next.js application under a sub-path of a domain
*
* @see [Base path configuration](https://nextjs.org/docs/api-reference/next.config.js/basepath)
*/
basePath?: string;
/** @see [Customizing sass options](https://nextjs.org/docs/basic-features/built-in-css-support#customizing-sass-options) */
sassOptions?: {
[key: string]: any;
};
/**
* Enable browser source map generation during the production build
*
* @see [Source Maps](https://nextjs.org/docs/advanced-features/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](https://nextjs.org/docs/basic-features/font-optimization)
*/
optimizeFonts?: boolean;
/**
* The Next.js runtime is Strict Mode-compliant.
*
* @see [React Strict Mode](https://nextjs.org/docs/api-reference/next.config.js/react-strict-mode)
*/
reactStrictMode?: boolean | null;
/**
* Add public (in browser) runtime configuration to your app
*
* @see [Runtime configuration](https://nextjs.org/docs/api-reference/next.config.js/runtime-configuration)
*/
publicRuntimeConfig?: {
[key: string]: any;
};
/**
* Add server runtime configuration to your app
*
* @see [Runtime configuration](https://nextjs.org/docs/api-reference/next.config.js/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](https://nextjs.org/docs/api-reference/next.config.js/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](https://nextjs.org/docs/advanced-features/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](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin)
*/
crossOrigin?: false | 'anonymous' | 'use-credentials';
/**
* Use [SWC compiler](https://swc.rs) to minify the generated JavaScript
*
* @see [SWC Minification](https://nextjs.org/docs/advanced-features/compiler#minification)
*/
swcMinify?: boolean;
/**
* Optionally enable compiler transforms
*
* @see [Supported Compiler Options](https://nextjs.org/docs/advanced-features/compiler#supported-features)
*/
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;
}
这个文件还可以通过函数来根据不同环境返回不同的配置参数
java
module.exports = async (phase, { defaultConfig }) => {
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
/* 配置 */
}
return nextConfig
}
phase 参数会根据不同的 next 命令来传入不同的值
ini
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 模块引入常量
javascript
const { PHASE_PRODUCTION_BUILD } = require('next/constants')
module.exports = (phase, { defaultConfig }) => {
if (phase === PHASE_PRODUCTION_BUILD) {
return {
/* 生产构建配置 */
}
}
return {
/* 其他情况配置 */
}
}
部署
- vercel
- cloudflare
注意:部署成功后,需要配置 mongodb 的 Network access
Authenication
- Signup & Login
- Controlling Page Access
session vs token
Type | session | token(例如 JWT) |
---|---|---|
存储方式 | 当用户登录时,服务器会创建一个 session,并为其生成一个唯一的 session ID。这个 session ID 会被存储在客户端的 cookie 中。服务器通常在后端存储 session 数据,例如在内存、数据库或其他存储系统中 | 当用户登录时,服务器会生成一个 token。这个 token 包含了一些用户的数据,以及签名来确保其完整性。客户端会存储这个 token,例如在 cookie、localStorage 或其他地方。当客户端进行请求时,它会将 token 发送到服务器,服务器会验证 token 的有效性 |
状态 | 通常是有状态的。意味着服务器需要存储关于每个 session 的信息 | 通常是无状态的。服务器不需要存储 token 的信息,因为每次请求都会带有 token,服务器只需验证其有效性 |
跨域 | Cookies 在默认情况下不支持跨域 | 可以轻松地在不同的域之间传输,因为它们只是一个字符串 |
过期方式 | 可以设置 session 的过期时间,在此时间后,session 就会失效 | 可以在生成 token 时设置过期时间,也可以为 token 设置刷新策略 |
安全性 | 由于 session ID 通常存储在 cookie 中,它可能容易受到 CSRF(跨站请求伪造)攻击 | 如果存储在 HTTP-only 的 cookie 中,可以减少 XSS(跨站脚本)攻击的风险,但如果不当地使用,可能会暴露于其他攻击。 |
可扩展性 | 对于大型应用,管理服务器上的 session 可能会变得复杂,尤其是在负载均衡环境中 | 由于是无状态的,因此很容易扩展,适用于大型、分布式应用 |
使用场景 | 适合传统的、单一的 web 应用 | 适合 SPA(单页应用)、移动应用、API 服务和跨域场景 |
JSON Web Tokens
token 是一个随机字符串,这个令牌数据由单部分组成:
- Isuuer Data:: 发行人数据
- Custom Data:自定义数据,如用户信息
- Secret Signing Key:密钥(存储在服务端,客户端永远看不到)
start now
鉴权
npm i -S next-auth
在 API 路由中,创建特殊文件 /api/auth/[...nextauth].js,在文件内引入 next-auth 包并实现相关逻辑
javascript
/**
* Authentication
* used for login
* http://localhost:3000/api/auth/callback/credentials?
* arter login, cookie has next-auth.session-token. this is session-token
*/
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { verifyPassword } from '@/lib/auth';
import { connectToDatabase } from '@/lib/db';
export const authOptions = {
providers: [
CredentialsProvider({
name: 'Credentials',
session: {
strategy: 'jwt',
},
// verify logic
async authorize(credentials, req) {
const client = await connectToDatabase();
const usersCollection = client.db().collection('users');
const user = await usersCollection.findOne({
email: credentials.email,
});
if (!user) {
client.close();
throw new Error('No user found!');
}
const isValid = await verifyPassword(credentials.password, user.password);
if (!isValid) {
client.close();
throw new Error('Could not log you in!');
}
client.close();
return { email: user.email };
},
}),
// ...other providers
],
// callbacks: {
// async session({ session, token, user }) {
// return session;
// },
// },
};
export default NextAuth(authOptions);
- 登录: 使用 signIn, 会访问 http://localhost:3000/api/auth/callback/credentials?
javascript
import { signIn } from 'next-auth/react';
try {
const result = await signIn('credentials', {
redirect: false,
email: enteredEmail,
password: enteredPassword,
});
if (!result.ok || result.error) {
throw new Error(result.error);
}
// set some auth state
router.replace('/profile');
} catch (e) {
console.error(e);
} finally {
、
}
- 退出:signOut,会访问 http://localhost:3000/api/auth/signout
javascript
import { signOut } from 'next-auth/react';
const logoutHandler = () => signOut();
- _app.js 中,使用 provider来包裹,以便在前端使用 useSession
javascript
import { SessionProvider } from 'next-auth/react';
import Layout from '@/components/layout/layout';
import '@/styles/globals.css';
function MyApp({ Component, pageProps: { session, ...rest } }) {
return (
<SessionProvider session={session}>
<Layout>
<Component {...rest} />
</Layout>
</SessionProvider>
);
}
export default MyApp;
- 客户端使用 useSession 或 getSession (async)
javascript
import { useSession, getSession } from 'next-auth/react';
export default function xxxx(props) {
const { data: session, status } = useSession()
console.log('session ', session)
console.log('status ', status) // loading | authenticated | unauthenticated
// 异步请求方式
getSession().then(session => {
console.log('-----', session);
// ...
});
return <>
<ul>
{status !== 'authenticated' && (
<li>
<Link href="/auth">Login</Link>
</li>
)}
{status === 'authenticated' && (
<>
<li>
<Link href="/profile">Profile</Link>
</li>
<li>
<button onClick={logoutHandler}>Logout</button>
</li>
</>
)}
</ul>
</>
}
- 加密:比如:bcryptjs
javascript
import { hash, compare } from 'bcryptjs'
// 通过 hash 函数加密明文密码
const hashedPwd = await hash(pwd, 12)
// 通过 compare 函数比较两个密码是否相同,返回布尔值
const isValid = await compare(newPwd, hashedPwd)
- serverSideProps
javascript
import { getSession } from 'next-auth/react';
export async function getServerSideProps(context) {
const session = await getSession({ req: context.req });
if (!session) {
return {
redirect: {
destination: '/auth',
permanent: false, // 永久重定向
},
};
}
return {
props: { session },
};
}
路由守卫
我们不但要对页面的访问权限进行校验,同时需要对接口访问进行校验session或token
这时获取 session,就不能用客户端的 getSession了,需要使用 getServerSession
javascript
// /api/update
import { getServerSession } from 'next-auth/next';
import { authOptions } from '../auth/[...nextauth]';
export default async handler(req, res) {
const session = await getServerSession(req, res, authOptions);
if (!session) {
res.status(401).json({ message: 'Not authenticated!' });
return;
}
// ....
}