基于 Next.js14的前端工程完整搭建

大家好,这篇文章主要是介绍基于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? @/*
  1. 项目的名称
  2. 是否使用 TypeScript
  3. 是否启用 Eslint
  4. 是否使用 src 作为目录
  5. 是否使用 APP 路由模式
  6. 是否使用 @ 设置别名

运行

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...

相关推荐
优联前端10 分钟前
uni-app-通过vue-cli命令行快速上手
开发语言·前端·vue.js·uni-app·优联前端
点燃银河尽头的篝火(●'◡'●)1 小时前
【BurpSuite】Cross-site scripting (XSS 学徒部分:1-9)
前端·web安全·网络安全·xss
Jiaberrr1 小时前
手把手教你:微信小程序实现语音留言功能
前端·微信小程序·小程序·语音·录音
熊猫在哪1 小时前
安装nuxt3
前端·nuxt.js
安冬的码畜日常2 小时前
【CSS in Depth 2 精译_036】5.6 Grid 网格布局中与对齐相关的属性 + 5.7本章小结
前端·css·css3·html5·网格布局·grid·css网格
啧不应该啊3 小时前
vue配置axios
前端·javascript·vue.js
__fuys__3 小时前
【HTML样式】加载动画专题 每周更新
前端·javascript·html
Want5953 小时前
HTML粉色烟花秀
前端·css·html
让开,我要吃人了3 小时前
HarmonyOS鸿蒙开发实战(5.0)自定义全局弹窗实践
前端·华为·移动开发·harmonyos·鸿蒙·鸿蒙系统·鸿蒙开发
yanlele4 小时前
前端面试第 66 期 - Vue 专题第二篇 - 2024.09.22 更新前端面试问题总结(20道题)
前端·javascript·面试