大家好,这篇文章主要是介绍基于nextjs14 搭建项目基础的最佳实现,并持续更新中,其中路由采用的是官方推荐的 APP router 模式,那咱们话不多说直接上干货。
项目地址:zhaoth/React-Next-Admin (github.com)
线上地址:react-next-admin.pages.dev
项目构建
环境
Next.js 14版本对于Node.js最低的版本要求是 18.17.0 。同时也建议大家 node 版本的选择最好选择双数版本,众所周知 nodejs 的单数版本维护周期较短。同时如果同时有很多项目需要维护建议大家用 nvm 或者 volta 来管理 node 环境,这里就不详细介绍了。
创建
建议使用 create-next-app
启动新的 Next.js 应用程序,它会自动为您设置所有内容。若要创建项目,请运行:
shell
npx create-next-app@latest
安装时,你将看到以下提示:
shell
What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like to use `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to customize the default import alias (@/*)? No / Yes
What import alias would you like configured? @/*
- 项目的名称
- 是否使用 TypeScript
- 是否启用 Eslint
- 是否使用 src 作为目录
- 是否使用 APP 路由模式
- 是否使用 @ 设置别名
运行
json
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
}
}
开发环境运行
shell
npm run dev
项目打包
shell
npm run build
开发端口号修改
dev默认的运行端口号是 3000 ,但是开发过程中如果需要修改,可以直接加上 -p 修改运行端口号
json
"dev": "next dev -p 3000 --turbo",
--turbo 是在开发环境中启动对Turbopack的支持,目前暂时不支持在bulid中开启
项目目录
- public目录 静态资源目录
- src 源文件夹
- apis 请求接口
- app 页面组件
- components 共用组件库
- hooks 全局 hooks
- i18n 国际化
- lib 共用库
- static 静态变量管理
- store 状态管理
- typing 全局TypeScript管理
- env 基础环境变量
- env.development 开发环境变量
- env.development.local 本地开发环境变量
- env.production 发布版本环境变量
- .eslinttrc.json eslint配置信息
- .gitignore git忽略文件
- .lintstagedrc lint-staged配置
- .prettierignore 代码格式化忽略文件
- .prettierrc 代码格式设置
- commitlint.config commit 提交设置
- Dockerfile docker 构建配置
- next.config nextjs 配置
- tailwind.config tailwindcss 全局设置
Git Hooks
Husky githooks工具可以预设git不同阶段的钩子函数
安装Husky
npx husky install
安装commitlint
commitlint约定提交时的message的标准
sql
yarn add -D @commitlint/config-conventional @commitlint/cli
安装lint-staged
lint-staged避免每次提交全局lint,设置需要lint的文件
css
npm install --save-dev lint-staged
执行husky 命令
husky install
执行完这个命令后,工程目录中会生成.husky
目录,存储 hooks
设置.lintstagedrc.js
javascript
const path = require('path');
//如果你想使用 next lint 和 lint-staged 在暂存的 git 文件上运行 linter,
// 则必须将以下内容添加到项目根目录中的 .lintstagedrc.js 文件中,以便指定 --file 标志的使用
const buildEslintCommand = (filenames) =>
`next lint --fix --file ${filenames
.map((f) => path.relative(process.cwd(), f))
.join(' --file ')}`;
module.exports = {
'*.{js,jsx,ts,tsx}': [buildEslintCommand],
};
添加 hooks 命令
shell
npx husky add .husky/pre-commit "npm exec lint-staged" // 提交前的格式化
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"' // 提交信息格式校验
配置完成后 git commit 的时候就会对相关文件执行 lint 和 message 校验的工作了】
通过 axios 调用服务端接口
安装 axios 和 @siyuan0215/easier-axios-dsl
wangsiyuan0215/easier-axios-dsl: A DSL for axios, which makes it easier to use axios. (github.com)是一个通过 dsl 方式简化请求配置的插件
shell
npm install @siyuan0215/easier-axios-dsl axios
添加 request.ts工具类
统一配置请求的 request 和 response 信息
typescript
import { StatusCodes } from 'http-status-codes';
import { G, requestCreator } from '@siyuan0215/easier-axios-dsl';
const TIMEOUT = {
DEFAULT: 3 * 60000,
UPLOADING: 5 * 60000,
};
export const request = requestCreator({
baseURL: process.env.NEXT_PUBLIC_BASE_URL,
timeout: TIMEOUT.DEFAULT,
withCredentials: true,
requestInterceptors: [
(config) => {
return {
...config,
timeout: TIMEOUT.UPLOADING,
headers: {
...config.headers,
authorization: '1',
},
};
},
(error: any) => Promise.reject(error),
],
responseInterceptors: [
(response) => {
const { data, status } = response;
if (status === StatusCodes.OK) {
return response;
}
return Promise.reject(response);
},
(error: string) => {
return Promise.reject(error);
},
],
});
export const generatorAPIS = <T extends {}>(apiConfig: T) => G<T>(request, apiConfig);
注意process.env.NEXT_PUBLIC_BASE_URL是需要在 env 文件中初始化的,next.js中如果想要在静态部署的环境下能读取到就是必须在每个变量前面加 NEXT_PUBLIC_ 进行声明
在 apis 文件夹中添加接口信息
typescript
/* eslint-disable no-unused-vars */
import { generatorAPIS } from '@/lib/request';
/**
* '[POST|GET|PUT|DELETE|...] [url] [d(data)(.(f|formData)):|q(query):|path:][keys with `,`]'
*
* d|data => data for POST and PUT
* - if data in request is array, you can use `[(d|data)]`;
* - if you want to pass all params to backend, you can use `(d|data):*`;
* - if you want to pass FormData to backend, you can use `(d|data).(f|formData):`;
*
* q|query => query for GET and DELETE;
*
* path => dynamic parameters in url, like: vehicle/tips/vehicleBaseInfo/{vin};
*
* eg.:
*
* import APIS from '@/api/XXX';
*
* APIS.testRequestUrlMethod(majorParams: Record<string, any>, otherParams?: Record<string, any>)
*
* If `majorParams` is array, and at the same time, you have to pass other params, you should use second param `otherParams`.
*
* POST:
* - `POST tipscase/save d:*`;
* equal: (params: unknown) => api.post<RoleListItem>({ url: baseUrl + 'tipscase/save', params }, true)
*
* - `POST static-files d:sourceType,systemType,fileName,file,remark`;
* equal: (types: string[]) => api.post<Record<string, DictionaryItem[]>>({ url: baseUrl + 'static-files', params: types })
*
* - `POST tipscase/save q:a,b,c`; => POST case-dict/search-types?a=[value of otherParams[a]]
* equal: (params: unknown) => api.post({ url: baseUrl + 'tipscase/save', params })
*
* - `POST case-dict/search-types [d] q:a` => POST case-dict/search-types?a=[value of otherParams[a]] and taking majorParams as data
* equal: (types: string[]) => api.post<Record<string, DictionaryItem[]>>({ url: baseUrl + 'case-dict/search-types' + '?=languageType=CN', params: types })
*
* ! What final `data` is depends on the keys of `d:[keys]`
*
* GET:
* - `GET tipscase/getInitInfo q:symptomId` => GET tipscase/getInitInfo?symptomId=[value of majorParams[symptomId]]
* equal: (symptomId: string) => api.get({ url: baseUrl + 'tipscase/getInitInfo' + symptomId })
*
* - `GET tipscase/get/{id} path:id` => GET tipscase/get/[value of majorParams[id]]
* equal: (id: string) => api.get({ url: baseUrl + 'tipscase/get/' + id })
* */
enum apis {
getTableList = 'GET api query:results,page,size',
}
export default generatorAPIS<typeof apis>(apis);
设置开发代理
在next.config.js 中添加
javascript
const isProd = ['production'].includes(process.env.NODE_ENV);
// 转发
const rewrites = () => {
if (!isProd) {
return [
{
source: '/api/:slug*',
destination: process.env.PROXY,
},
];
} else {
return [];
}
};
添加状态管理插件
本项目状态管理没有选择传统的 redux 而是选择了比较轻巧的 zsutand该状态管理对于一般的项目已经足够用了
安装zustand
shell
npm instlal zustand
在 store 文件夹添加 store 并已 use 开头
typescript
import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
import { defaultLocale, locales } from '@/static/locales';
// Custom types for theme
interface SettingState {
defaultLocale: string;
locales: string[];
setDefaultLocale: (newVal: string) => void;
}
const useSettingStore = create<SettingState>()(
persist(
(set, get) => ({
defaultLocale: get()?.defaultLocale ? get()?.defaultLocale : defaultLocale,
locales: locales,
setDefaultLocale: (newVal) => set((state: any) => ({
defaultLocale: state.defaultLocale = newVal,
})),
}),
{
name: 'setting',
storage: createJSONStorage(() => sessionStorage), // default localstorage
},
),
);
export default useSettingStore;
persist是 zustand 的插件可以对 store 数据进行缓存
使用
typescript
const defaultLocale = useSettigStore((state) => state.defaultLocale);
集成 antdesign 组件库
安装antdesign
shell
npm install antd --save
安装 @ant-design/nextjs-registry
nodejs
npm install @ant-design/nextjs-registry --save
如果你在 Next.js 当中使用了 App Router, 并使用 antd 作为页面组件库,为了让 antd 组件库在你的 Next.js 应用中能够更好的工作,提供更好的用户体验,你可以尝试使用下面的方式将 antd 首屏样式按需抽离并植入到 HTML 中,以避免页面闪动的情况
封装antd-registry
由于 antd 还有全局 config 的设置,为了以后方便还需要把AntdRegistry和ConfigProvide进行一次封装
typescript
'use client';
import React, { useEffect, useState } from 'react';
import { App, ConfigProvider, ConfigProviderProps } from 'antd';
import 'antd/dist/reset.css';
import { AntdRegistry } from '@ant-design/nextjs-registry';
import enUS from 'antd/locale/en_US';
import zhCN from 'antd/locale/zh_CN';
import dayjs from 'dayjs';
import useSettingStore from '@/store/useSettingStore';
import { locales } from '@/static/locales';
type Locale = ConfigProviderProps['locale'];
const AntdConfigProvider: React.FC<React.PropsWithChildren> = ({ children }) => {
const defaultLocale = useSettingStore((state) => state.defaultLocale);
const [locale, setLocal] = useState<Locale>(enUS);
useEffect(() => {
dayjs.locale('en');
if(defaultLocale === locales[0]){
setLocal(enUS)
dayjs.locale('en');
}else{
setLocal(zhCN)
dayjs.locale('zh-cn');
}
}, []);
return (
<ConfigProvider
componentSize="large"
locale={locale}
>
<div style={{ height: '100vh' }}>{children}</div>
</ConfigProvider>
);
};
const AntdStyleRegistry: React.FC<React.PropsWithChildren> = ({ children }) => {
return (
<AntdRegistry>
<AntdConfigProvider>
<App className="max-h-full min-h-full bg-white">{children}</App>
</AntdConfigProvider>
</AntdRegistry>
);
};
export default AntdStyleRegistry;
在 layout.tsx
中使用
typescript
export default function EmptyLayout({ children, params: { locale } }: Props) {
const messages = locale === 'en' ? en : zh;
return (
<AntdStyledComponentsRegistry>
{children}
</AntdStyledComponentsRegistry>
);
}
组件引用
Next.js App Router 当前不支持直接使用 .
引入的子组件,如 <Select.Option />
、<Typography.Text />
等,需要从路径引入这些子组件来避免错误。
国际化(基于 APP Router & Static export)
本项目国际化是基于 APP Router & Static export模式为基础进行国际化设置的
安装next-intl
shell
npm install next-intl
next-intl 国内目前文档无法正常访问
设置 next.config.js
添加 output: 'export'配置,由于此配置,当运行 npm run build 时,Next.js 将在 out 文件夹中生成静态 HTML/CSS/JS 文件。
javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
}
module.exports = nextConfig
设置国际化文件结构
创建翻译文件
typescript
export default {
welcome: 'Welcome',
dashboard: 'Dashboard',
list: 'List',
ahookList: 'ahookList',
proList: 'proList',
};
引入新添加的翻译文件
typescript
import login from './locales/en/login';
import menu from '@/i18n/locales/en/menu';
export default {
login,
menu,
};
更新文件结构
首先,创建 [locale] 文件夹并移动其中的现有 page.tsx 文件、layout.tsx 文件和 about 文件夹。不要忘记更新导入。 然后在跟目录的 page.tsx中将用户重定向到默认语言,如下:
typescript
'use client';
import { redirect } from 'next/navigation';
import useSettingStore from '@/store/useSettingStore';
import { staticRouter } from '@/static/staticRouter';
export default function Home() {
const defaultLocale = useSettingStore((state) => state.defaultLocale);
// 静态 build 模式下 不能用 next/router 需要用next/navigation
redirect(`/${defaultLocale}/${staticRouter.login}`);
}
注意:使用静态导出时,不能在没有前缀的情况下使用默认区域设置。我们必须将传入的请求重定向到默认语言。如文档中所述。
更新 app/[locale]/layout.tsx
typescript
import { EmptyLayout } from '@/components';
import React from 'react';
import { Props } from '@/typing/Layout';
import { locales } from '@/static/locales';
//function to generate the routes for all the locales
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export default function Layout({ children, params: { locale } }: Props) {
return (
<>
<EmptyLayout params={{
locale: locale,
}}>{children}</EmptyLayout>
</>
);
}
EmptyLayout.tsx
typescript
import '@/app/globals.css';
import AntdStyledComponentsRegistry from '@/lib/antd-registry';
import React from 'react';
import { NextIntlClientProvider } from 'next-intl';
import { Props } from '@/typing/Layout';
import en from '@/i18n/en';
import zh from '@/i18n/zh';
import { timeZone } from '@/static/locales';
export const metadata: { title: string, description: string } = {
title: 'React Next Admin',
description: '',
};
export default function EmptyLayout({ children, params: { locale } }: Props) {
const messages = locale === 'en' ? en : zh;
return (
<NextIntlClientProvider locale={locale} messages={messages} timeZone={timeZone}>
<AntdStyledComponentsRegistry>
{children}
</AntdStyledComponentsRegistry>
</NextIntlClientProvider>
);
}
在这里我们除了往布局里面传递了 children 之后还传递了一个params参数,并添加 generateStaticParams 函数以生成所有区域设置的静态路由,同时我们在 emptylayout添加上下文提供程序 NextIntlClientProvider
更新页面和组件以使用翻译
typescript
'use client'
import { useTranslations } from 'next-intl'
export default function HomePage() {
const t = useTranslations('HomePage')
return (
<div>
{t('helloWorld')}
</div>
)
}
添加"use client"(截至目前,仅在客户端组件中支持使用 next-intl 的翻译)导入 useTranslations 钩子并在我们的 jsx 中使用它
创建语言切换组件
typescript
import React, { useState } from 'react';
import { Group } from 'antd/es/radio';
import { usePathname as useIntlPathname, useRouter as useIntlRouter } from '@/lib/language';
import useSettingStore from '@/store/useSettingStore';
import { RadioChangeEvent } from 'antd';
export default function ChangeLanguage() {
const options = [
{ label: 'EN', value: 'en' },
{ label: '中', value: 'zh' },
];
const intlPathname = useIntlPathname();
const intlRouter = useIntlRouter();
const setDefaultLocale = useSettingStore((state) => state.setDefaultLocale);
const defaultLocale = useSettingStore((state) => state.defaultLocale);
const [value, setValue] = useState(defaultLocale);
const onLanguageChange = ({ target: { value } }: RadioChangeEvent) => {
setValue(value);
setDefaultLocale(value);
intlRouter.replace(intlPathname, { locale: value });
};
return (
<>
<Group options={options} onChange={onLanguageChange} value={value} key={value}>
</Group>
</>
);
}
封装基于国际化的Link, redirect, usePathname, useRouter
由于静态导出模式,用 nextjs 自带的路由跳转的时候都必须添加 locale 较为麻烦下面是基于next-intl的createLocalizedPathnamesNavigation封装的路由,用法和 nextjs 路由一致
typescript
export const { Link, redirect, usePathname, useRouter } =
createLocalizedPathnamesNavigation({
locales,
pathnames,
localePrefix,
});
部署
静态模式
要启用静态导出,请更改其中 next.config.js 的输出模式:
javascript
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
output: 'export',
}
module.exports = nextConfig
运行 next build 后,Next.js 将生成一个 out 文件夹,其中包含应用程序的 HTML/CSS/JS
注意:APP路使用next/router是会出现 router 未挂载的问题,需要使用next/navigation这个钩子函数
NodeJS环境部署
注意 运行 npm run start 来构建你的应用时,next.config.js 中 output: 'export' 需要去掉
shell
npm run build
运行 npm run start 启动 Node.js 服务器
shell
npm run start
Docker部署
设置dockfile
docker
# Install dependencies only when needed
FROM node:alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN yarn install --frozen-lockfile --registry=https://registry.npm.taobao.org
# Rebuild the source code only when needed
FROM node:alpine AS builder
WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
RUN yarn build && yarn install --production --ignore-scripts --prefer-offline --registry=https://registry.npm.taobao.org
# Production image, copy all the files and run next
FROM node:alpine AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
# You only need to copy next.config.js if you are NOT using the default configuration
# COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
USER nextjs
EXPOSE 4000
ENV PORT 4000
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry.
# ENV NEXT_TELEMETRY_DISABLED 1
CMD ["node_modules/.bin/next", "start"]
构建 docker 镜像
docker
docker image build -t react-next-admin .
docker image ls
运行镜像
docker
docker container run -d -p 8080:4000 -it react-next-admin
最后
在本文中,我们讨论了如何用最新版的 NEXT,js 搭建一个完整的前端工程,本工程一直在持续开发中,有兴趣的可以访问github.com/zhaoth/Reac...