# C端Next.js项目踩坑

# C端和B端的不同

  • C端 对样式要求较高,需要注意相关约定
  • 框架不同,需选用 SSR组件库
  • 引入 SSG SSR

# 设计稿规范

需要和 UI 约定主体设计内容宽度。推荐 1200px 可以满足大部分屏幕需要。

# 约定主题色并定义全局变量

定义 CSS 全局变量

// 定义
html {
  --font-color: #000000;
  --bg-color: #0000;
}
// 入口文件引入全局样式
// 使用
.main {
  color: var(--font-color);
}

# 约定并引入指定字体

@font-face {
  font-family: 'Inter-Semi Bold, Inter';
  src: url(../common/font/Inter-SemiBold.ttf);
}

html {
  --semi-bold-font: 'Inter-Semi Bold, Inter';
}

# reset.css

磨平浏览器样式差异

# M端需要自适应字体

通过 postcss-pxtorem 插件,把px转换成rem,同时需要监听 ua resize 根元素 html 的 rem

# 定义交互规范,比如按钮以及输入框样式

以 input 为例,需要注意 default foucs hover 等状态及样式

页面的 loading list-empty 图片占位图等需要提前和 UI 约定一致

# 项目构建注意事项

使用 yarn create next-app --typescript 命令即可初始化一个 Next.js 项目。

package.json 有几个比较常用的命令

{
  "dev": "next dev",
  "build": "next build",
  "start": "next start"
}

dev 命令在开发时使用,可以实现快速刷新。

build 用于打包。start 用于启动生产模式的 server 必须在 build 之后才能执行。

Next.js 不需要过多的配置,可以直接上手开发。可以按需配置一些功能。

# 接入 eslint/prettier

next.js 默认的规则较少,需要用到的是一些 eslit/styleint/prettier 校验及格式化规则,项目中用的是内部的 npm 包,可以配置开源社区相关流行库

// .eslintrc.js
module.exports = {
  extends: [
    'eslint:recommended',
    'next',
    require.resolve('xxx'),
  ],
};

// .prettierrc.js
module.exports = {
  ...require('xxx'),
};

// .stylelintrc.js
module.exports = {
  extends: [require.resolve('xxx')],
};

可以新建 .vscode/settings.json 添加统一的 VS Code 配置,方便团队统一管理

主要配置 autoFormat 以及 format 默认采用的扩展选项

{
  "eslint.format.enable": true,
  "editor.codeActionsOnSave": {
    "source.fixAll": true,
    "source.fixAll.eslint": true,
    "source.fixAll.stylelint": true
  },
  "eslint.validate": ["javascript", "typescript"],
  "editor.formatOnSave": true,
  // ...
}

# 配置 next.config.js

由于 Next.js 是动态服务,需要用 Docker Node 镜像,使用不了 Nginx 镜像。需要配置环境变量

在 Docker 容器会注入环境变量, Next 项目可以获取到这个环境变量

// 配置前缀路径
module.exports = {
  basePath: process.env.BASE_URL || 'http://xxx.com',
};

在本地开发时,如果直接调用测试环境,会存在跨域问题,使用 Next.js ,推荐使用中间的 Node 层,即在请求路径加上 /api 前缀,Next.js 会自动通过 Node 发送请求。就不会存在跨域问题。

例如 /api 目录下的一个 demo

import type { NextApiRequest, NextApiResponse } from 'next';
import { fetcher } from 'utils/index';

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  const { method, body } = req;
  const { token, siteuid } = req?.headers;
  const options = {
    method,
    body,
  };
  // ... 处理参数
  const response = await fetcher('/', options);
  res.status(200).json(response);
};

export default handler;

# 封装 fetch

  • 对权限报错统一处理,并允许跳过报错处理
  • 客户端/Node层 通用
  • 支持 mock api http 等调用方式

const getEnv = () => {
  const { publicRuntimeConfig: env } = getConfig();
  return env || {};
};

// fetch 函数
const fetcher = async (url, options, proxy = '') => {
  if (typeof url !== 'string')
    throw new TypeError('url must be required and of string type');
  const env = getEnv();
  const isClient = typeof window !== 'undefined';

  // ...

  //  不同环境区分不同的 请求路径 地址
  let targetUrl;
  if (options.mock) {
    targetUrl = `${env.MOCK_URL}${parseUrl.replace('/api', '')}`;
  } else if (!isClient) {
    targetUrl = `${env.BASE_URL}${parseUrl}`;
  } else if (url.startsWith('http')) {
    targetUrl = url;
  } else if (parseUrl.startsWith('/api/')) {
    targetUrl = parseUrl;
  }

  return fetch(targetUrl, newOptions)
    .then((response) => {
      return response.json();
    })
    .then((res) => {
      // 是否跳过 token失效跳转 等 错误处理。例如获取用户信息接口
      const skipErrorHandler = newOptions?.skipErrorHandler;
      if (skipErrorHandler) {
        return res;
      }
      // token expired/forbidden
      if (isClient && (res?.code === '100002' || res?.code === '100003')) {
        router.push(`/login?redirect=${window?.location?.href}`);
        store.dispatch(updateUserInfo({ login: false }));
        return res;
      }
      // 其他报错
      if (
        isClient &&
        !(String(res?.code) === '0' || String(res?.status) === '0')
      ) {
        Toast.error(String(res?.code) || res?.msg);
        return res;
      }
      return res;
    })
    .catch((e) => {
      console.error('fetch error: ', targetUrl, e);
    });
};

# 其他技术栈

# 接入极验与风控

极验验证是一种在计算机领域用于区分自然人和机器人的,通过简单集成的方式,为开发者提供安全、便捷的云端验证服务。

前端主要做的事情是,引入极验实例,并且初始化实例。

  • 页面上对按钮做了透明处理,用户点击极验按钮的时候,无感知。
  • 在每次点击完极验按钮之后,需要手动 destroy 极验实例,并且重新初始化极验实例
  • 传给极验初始化的 Login 函数,不能够获取到最新的 state,需要把要传递的数据记录在 ref 上面进行动态更新
  • 触发风险二次校验,需要配置 bind 模式,并且手动调用 gtInstance.click,这样验证图片可以直接弹出

# 埋点

一种是曝光/点击埋点

还有一种是报错埋点,通过 React 的 ErrorBounding、componentDidCatch

# 国际化

国家化需要使用 getStaticProps api 生成 翻译文件,并传递给页面级组件。

# redux

如何在使用 updateState 的时候,进行友好提示,即提示更新的属性

import { createSlice, SliceCaseReducers } from '@reduxjs/toolkit';
import store from './root-store';

interface State {
  /** 登录状态 */
  login: boolean;
}

export type UserState = Partial<State>;

// 此处需要给 SliceCaseReducers 传递泛型
export const userStore = createSlice<UserState, SliceCaseReducers<UserState>>({
  name: 'user',
  initialState: {
  },
  reducers: {
    updateUserInfo: (state: UserState, action) => {
      return { ...state, ...action.payload };
    },
  },
});

export const { updateUserInfo } = userStore.actions;

export const updateUserState = (state: UserState) => {
  store.dispatch(updateUserInfo(state));
};

export default userStore.reducer;

# Swiper插件

LastEditTime: 2023/4/9 16:56:44