Skip to content
目录

3 月 6 号面试题(蚂蚁金服 一面)

1. 有对项目进行哪些配置吗

1. 环境配置

根据项目运行的环境,配置不同的环境变量。例如开发环境、测试环境和生产环境可能需要请求不同的 API 地址、token 等;

在 ops 项目中:

项目的开发、测试环境和生产环境所请求的 API 是不一致的。这个时候我们在 utils 文件中会配置 env 的值,以及其相对的 api 地址。切换不同环境时,修改 env 的值以确保使用正确的 api 地址。

js
let env = 'prod';
let apiHost = '';
let poHost = '';
if (env == 'test') {
    apiHost = 'https://testapi.grosiraja.com/cms/';
    poHost = 'https://testapi.grosiraja.com/supply/';
}
if (env == 'dev') {
    apiHost = 'https://devapi.grosiraja.com/cms/';
    poHost = 'https://devapi.grosiraja.com/supply/';
}
if (env == 'prod') {
    apiHost = 'https://api.grosiraja.com/cms/';
    poHost = 'https://api.grosiraja.com/supply/';
}
export { apiHost, poHost };

web 和 wap 项目也是如此

js
let env = 'gro';
let ipUrl = '';
let prodIpUrl = '';

if (env == 'gro') {
    ipUrl = 'https://api.gor.grosiraja.com'; // gro
    prodIpUrl = 'https://api.gor.grosiraja.com';
}
if (env == 'dev') {
    ipUrl = 'https://dev-api.gocart.ph'; // dev
    prodIpUrl = 'http://10.60.13.197:8000';
}
if (env == 'staging') {
    ipUrl = 'https://staging-api.gocart.ph'; //staging
    prodIpUrl = 'http://10.60.13.198:8000'; // prod http
}
if (env == 'master') {
    ipUrl = 'https://pre-prod-api.gocart.ph'; // prod https
    prodIpUrl = 'http://10.60.14.51:8000'; // prod http
}

2. 依赖环境

使用包管理器(如 npm、Maven、Gradle 等)来管理项目的依赖。这些依赖可能包括库、框架、插件等。

web 项目中:

  • .babelrc 文件

文件相关配置

json
{
    "presets": ["next/babel"], //Next.js的总配置文件,相当于继承了它本身的所有配置
    "plugins": [
        //增加新的插件,这个插件就是让antd可以按需引入,包括CSS
        [
            "import",
            {
                "libraryName": "antd",
                "libraryDirectory": "lib"
            },
            "antd"
        ]
    ]
}
  • .gitignore 文件

用于忽略不需要被(git)跟踪的目录和文件

shell
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules

# testing
/coverage

# next.js
/.next/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local

# vercel
.vercel

.history

扩展:一文详解.gitignore 与.gitkeep 的作用与使用规则

  • next.config.js
js
const withLess = require('next-with-less');
const withVideos = require('next-videos'); // 加载视频 video
const CopyPlugin = require('copy-webpack-plugin');

module.exports = withLess(
    withVideos({
        // reactStrictMode: true,
        // productionBrowserSourceMaps: true,
        lessLoaderOptions: {
            /* ... */
            lessOptions: {
                /* ... */
                modifyVars: {
                    'primary-color': '#21BC88',
                    'primary-backgroundcolor':
                        'linear-gradient(265.12deg, #38EF7D -118.43%, #11998E 72.97%)',
                    /* ... */
                },
            },
        },
        experimental: {
            outputStandalone: true,
        },
        images: {
            domains: [
                'files.rrhiapps.ph',
                'firebasestorage.googleapis.com',
                'dummyimage.com',
                'uploads.gocart.ph',
            ],
        },
        webpack: (config, options) => {
            // Important: return the modified config
            config.plugins.push(
                new CopyPlugin({
                    patterns: [
                        {
                            from: './docker/healthcheck.js',
                            to: './',
                        },
                    ],
                })
            );

            // Important: return the modified config
            return config;
        },
        // env: {
        //   // Add any logic you want here, returning `true` to enable password protect.
        //   PASSWORD_PROTECT: true,
        // },
        // async redirects() {
        //   return [
        //     {
        //       source: '/',
        //       destination: '/mycart',
        //       permanent: true,
        //     },
        //   ]
        // },
    })
);

3. 代码规范

配置代码规范检查工具(如 ESLint、TSLint、Checkstyle 等),以确保代码风格一致、符合规范,并减少潜在的错误。

比如统一代码缩进 2 个或 4 个字符。修改 vscode 的 Tab Size 的值

.eslintrc.json

json
{
    "extends": "next/core-web-vitals" // 严格:包括 Next.js 的基本 ESLint 配置以及更严格的Core Web Vitals 规则集。这是首次设置 ESLint 的开发人员的推荐配置。
}

4. 构建和部署

配置构建工具(如 webpack、gradle builddeng)来自动化构建过程,包括代码编译、打包、测试等。同时,配置部署工具(如 Docker、Kubernetesdeng)以自动化部署应用到服务器。

比如 ops 项目: 这是一个已有的项目,主要是进行项目维护

使用的是 umi 框架搭建

  • 因为项目代码量大,所以会遇到打包速度慢的情况 如果遇到编译慢、增量编译慢、内存爆、OOM 等问题,可以尝试以下方法。

所以进行在 config 目录下的 config.js 文件里配置了

js
nodeModulesTransform: {
  type: 'none',
  exclude: [],
}

这也是官方提供的一个解决方案

  • 当遇到打包占用内存太大,电脑可用内存不足,打包失败

我们进行打包的时候,项目过大时,可能会遇到(webpack)打包过程中占用的内存堆栈超出了 node.js 中采用 V8 引擎对内存的限制,V8 引擎对内存的使用的默认大小限制是 1536 (1.5 GiB),那我们就可以通过修改这个内存限制来解决。

所以对 package.json 文件的 build 命令进行一个修改:

js
// 修改前
"build": "umi build",
// 修改后
"build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 umi build"

配置部署工具

dockerfile
# Install dependencies only when needed
FROM node:16-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 yarn.lock ./
RUN yarn install --frozen-lockfile

# If using npm with a `package-lock.json` comment out above and use below instead
# COPY ./package.json ./package-lock.json ./
# RUN npm ci

# Rebuild the source code only when needed
FROM node:16-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# 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 during the build.
# ENV NEXT_TELEMETRY_DISABLED 1

RUN yarn build
# RUN npm run build

# Production image, copy all the files and run next
FROM node:16-alpine AS runner
WORKDIR /app

ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# 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 /app/package.json ./package.json

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/healthcheck.js ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000

CMD ["node", "server.js"]

docker 目录

deploy.sh

sh
#!/bin/bash

DOCKER_USERNAME=$1
DOCKER_PASSWORD=$2
TAG=$3
BASEDIR=$4
APPNAME=$5
PORT=$6
REPLICAS=$7
PARALLELISM=$8

docker_install()
{
	echo "检查Docker......"
	docker -v
    if [ $? -eq  0 ]; then
        echo "检查到Docker已安装!"
    else
    	echo "安装Docker环境..."
        curl -sSL https://get.daocloud.io/docker | sh
        echo "安装Docker环境...安装完成!"
        echo "启动Docker..."
        systemctl start docker
        echo "启动Docker...启动成功!"
    fi
    # 创建公用网络==bridge模式
    #docker network create share_network
}

docker_install

echo "登录Docker镜像仓库..."
echo $DOCKER_PASSWORD | docker login --username $DOCKER_USERNAME --password-stdin registry.cn-beijing.aliyuncs.com
echo "登录Docker镜像仓库...登录成功!"

# docker swarm init

echo "无中断部署服务..."
TAG=$DATETIME PORT=$PORT REPLICAS=$REPLICAS PARALLELISM=$PARALLELISM docker stack deploy --with-registry-auth -c $BASEDIR/docker-compose.yml $APPNAME
echo "无中断部署服务...部署成功!"

docker-compose.yml

yml
version: '3.7'

services:
    nodejs:
        image: registry.cn-beijing.aliyuncs.com/gocart/web:${TAG}
        ports:
            - '${PORT}:3000'
        command: node server.js
        deploy:
            replicas: ${REPLICAS}
            update_config:
                parallelism: ${PARALLELISM}
                order: start-first
                failure_action: rollback
                delay: 10s
            rollback_config:
                parallelism: 0
                order: stop-first
            restart_policy:
                condition: any
                delay: 5s
                max_attempts: 3
                window: 120s
        healthcheck:
            test: ['CMD', 'node', 'healthcheck.js']

healthcheck.js

js
const http = require('http');
const options = {
    host: '0.0.0.0',
    port: 3000,
    timeout: 2000,
};

const healthCheck = http.request(options, (res) => {
    console.log(`HEALTHCHECK STATUS: ${res.statusCode}`);
    if (res.statusCode == 200) {
        process.exit(0);
    } else {
        process.exit(1);
    }
});

healthCheck.on('error', function (err) {
    console.error('ERROR');
    process.exit(1);
});

healthCheck.end();

.env

js
TAG=1.0.1
PORT=3001
REPLICAS=2
PARALLELISM=3001

5. 安全性配置

配置项目的安全策略,如身份验证、授权、加密等,以确保项目的安全性

比如 ops

前端利用 jsencrypt.js 进行 RSA 加密

对于请求数据有进行 RSA 非对称加密,使用的是 jsencrypt 库

js
var encryptor = new JSEncrypt(); // 创建加密对象实例
//之前ssl生成的公钥,复制的时候要小心不要有空格
var pubKey =
    '-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC1QQRl0HlrVv6kGqhgonD6A9SU6ZJpnEN+Q0blT/ue6Ndt97WRfxtSAs0QoquTreaDtfC4RRX4o+CU6BTuHLUm+eSvxZS9TzbwoYZq7ObbQAZAY+SYDgAA5PHf1wNN20dGMFFgVS/y0ZWvv1UNa2laEz0I8Vmr5ZlzIn88GkmSiQIDAQAB-----END PUBLIC KEY-----';
encryptor.setPublicKey(pubKey); //设置公钥
var rsaPassWord = encryptor.encrypt('要加密的内容'); // 对内容进行加密

ops 的实现

公钥会根据 env 的值来进行调整,不同的环境使用不同的公钥

js
SEncrypt.prototype.encryptLong = function (string) {
    var k = this.getKey();
    try {
        var ct = '';
        //RSA每次加密117bytes,需要辅助方法判断字符串截取位置
        //1.获取字符串截取点
        var bytes = new Array();
        bytes.push(0);
        var byteNo = 0;
        var len, c;
        len = string.length;
        var temp = 0;
        for (var i = 0; i < len; i++) {
            c = string.charCodeAt(i);
            if (c >= 0x010000 && c <= 0x10ffff) {
                //特殊字符,如Ř,Ţ
                byteNo += 4;
            } else if (c >= 0x000800 && c <= 0x00ffff) {
                //中文以及标点符号
                byteNo += 3;
            } else if (c >= 0x000080 && c <= 0x0007ff) {
                //特殊字符,如È,Ò
                byteNo += 2;
            } else {
                // 英文以及标点符号
                byteNo += 1;
            }
            if (byteNo % 117 >= 114 || byteNo % 117 == 0) {
                if (byteNo - temp >= 114) {
                    bytes.push(i);
                    temp = byteNo;
                }
            }
        }
        //2.截取字符串并分段加密
        if (bytes.length > 1) {
            for (var i = 0; i < bytes.length - 1; i++) {
                var str;
                if (i == 0) {
                    str = string.substring(0, bytes[i + 1] + 1);
                } else {
                    str = string.substring(bytes[i] + 1, bytes[i + 1] + 1);
                }
                var t1 = k.encrypt(str);
                ct += t1;
            }
            if (bytes[bytes.length - 1] != string.length - 1) {
                var lastStr = string.substring(bytes[bytes.length - 1] + 1);
                ct += k.encrypt(lastStr);
            }
            return ct;
        }
        var t = k.encrypt(string);
        // var y = hex2b64(t);
        return t;
    } catch (ex) {
        return false;
    }
};
const encrypt = new JSEncrypt();
if (env == 'test') {
    encrypt.setPublicKey(
        '-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz5sQUPqadstAXXEbXe+kKaRyxvuAQFUXLRzncCFG5Z5nPEuIW5UqnquUW5H08lkg+HMm7qipDRKveU3nWLV325sYib7CRAGMt1knsoOYi6d6OX9yvb27Fxm/lzDnJD0+pRUZKIz+sq4cC5fTovR9kqY3AO6wYUoAZw7xmYCMnMQF43Nimrn01/P8JNj9YgdaYZq6Jn6KUqjF8kRM8+Q2HdcwXoL9WMPILqbf+fa3V3tyXK2DMarLOLvmOmRLS9lIIvTZ6tH09OoTKl6Pg6FMyd5uvfphF407CM5CsSW3pNckw8Aernp9O3+s6Wi4FnxbrzSfhYCPlm0k01VEmzsdPwIDAQAB-----END PUBLIC KEY-----'
    );
}

if (env == 'dev') {
    encrypt.setPublicKey(
        '-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnDk/VvHj+rB83SL3vYxFasr7FQLu7ZvPhQhCv5GJM0JoQeTQ+Uyxs3QZZD6roOiXl74hcr/2WZz5wH5UzPTQ4gi/xdmDqfBVZNiPyMoCwQag+BZ3hKoasV6csy6GGf9hxu4oM/qNlcH3a4+cRh8JoGIYRmBDHVNBcUz6g/q6HB9jd4DptFCdGYHLA+fi2tyZby/+kf5UXuevVt+65CRcSsvfF88MpdDx+QTkC879UZ16B0H++0uB98LShIH1Kt1kSESaG9c3bUp3o0vJHbyK05Bu/UfspD0qdk8JsJkI2uxiVH6l3NHfmQor5oMLQAcnJZDcrbtQxYiymh9pfr1DeQIDAQAB-----END PUBLIC KEY-----'
    );
}

if (env == 'prod') {
    encrypt.setPublicKey(
        '-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkQEICxoRgjffbzsXAGhvdlR4D+h+cuqwNyFeVKi9ZVJqLiq+qKTIsvwwopqMvoJ/+gHu0Qyq11Di7bl7COvrr0BLecunJsqEfhc45A7aUvb+mwHRh4/5pgRVci2TvUQEkYonkfe9eKaogESSHt7ET1RsJLA2nUGapZqcVsOLfqqf3556oi4BpqmmvOgR1Pz3DZ9jm46gVoormASNkizwkkfvnKjaEt1hFVQtus5yfKzPL7eSBzwl7naAW9l6e+sN9IFGHpOWZoQhXzG6vRipL5uvYoyvh3oHmqShZdVTnuNG1CbQTNB+9BiElUqX8kE+VOEK3cazden3MzP+t6u/FwIDAQAB-----END PUBLIC KEY-----'
    );
}

2. React 里面常用的 Hooks

1. useState

用于在函数组件中添加状态。状态(state)是变化的数据,是组件甚至前端应用的核心。useState 有传入值和函数两种参数,返回的 setState 也有传入值和函数两种参数。

js
// 传入值
const [state, setState] = useState(0);
// 传入函数
const [num, setNum] = useState(() => {
    let a = 1,
        b = 2;
    return a + b;
});

setState(1);
setNum((state) => state + 1); // 函数的参数是上一次的 state

2. useEffect

用于在函数组件中执行副作用操作,例如 数据获取、订阅或手动更改 React 组件的 DOM 等。副作用 effect 函数是在渲染之外额外执行的一些逻辑。它是根据第二个参数的依赖数组是否变化来决定是否执行 effect,可以返回一个清理函数,会在组件卸载前执行或每次使用更改的依赖项重新渲染之前运行。

执行时机:在渲染结束之后

js
useEffect(() => {
    let timer = setTimeout(() => {
        console.log(num);
    }, 5000);
    return () => {
        // 清理函数
        clearTimeout(timer);
    };
}, [num]); // 如果不传第二个参数时,每次都会执行;传递第二个参数时,第二个参数有变化时执行
  1. useLayoutEffect

和 useEffect 差不多,但是 useEffect 的 effect 函数是异步执行的,所以可能中间有次渲染会闪屏。而 useLayoutEffect 是同步执行的,所以不会闪屏,但如果计算量大可能会导致掉帧,阻塞渲染。所以,仅当在浏览器渲染之前运行效果至关重要时才需要此功能,例如:在用户看到工具提示之前测量和定位工具提示。(只有在关键时刻需要在用户看到之前运行你的 Effect 时才需要使用它,例如,在显示提示工具提示之前测量和定位位置。)

  1. useInsertionEffect

回调函数会在 commit 阶段的 Mutation 子阶段同步执行,与 useLayoutEffect 的区别在于执行的时候无法访问 DOM 的引用。这个 Hook 是专门为 CSS-in-JS 库插入全局的 style 元素而设计。

5. useReducer

用于在函数组件中管理复杂的状态逻辑。它接受一个 reducer 函数和一个初始状态值作为参数,并返回一个包含当前状态和一个更新状态的 dispatch 函数的数组。使用 useReducer 可以更好地组织和管理状态更新逻辑,特别是在处理多个状态变量或执行异步操作时。

封装一些修改状态的逻辑到 reducer,通过 action 触发。当修改深层对象的时候,创建新对象比较麻烦,可以结合 immer 来解决。

6. useRef

可以保存 dom 引用或其他内容,通过.current来取,改变它的内容不会触发重新渲染。

返回一个可变的 ref 对象,其.current 属性被初始化为传入的参数。返回的 ref 对象在组件的整个生命周期内保持不变。这对于管理 DOM 对象、定时器或其他需要在组件生命周期内保持引用的值很有用。

  1. forwardRef + useImperativeHandle

通过 forwardRef 可以从子组件转发 ref 到父组件。如果想自定义 ref 内容可以使用 useImperativeHandle 来实现。

8. useContext

用于在函数组件中访问 React 的 Context API。它接受一个 Context 对象作为参数,并返回该 Context 的当前值。这样,你可以在函数组件中使用 Context,而无需手动传递 props。

9. memo + useMemo + useCallback

memo 包裹的组件只有在 props 变化的时候才会重新渲染,useMemo、useCallback 可以防止 props 不必要的变化,两者一般是结合使用。不过当用来缓存计算结果等场景的时候,也可以单独使用 useMemo、useCallback。

useMemo 返回一个记忆化的值,该值只在依赖项数组发生变化时才会重新计算。这对于避免重复计算和提高性能很有用。

useCallback 返回一个记忆化的版本的回调函数,该回调函数在依赖项数组发生变化时才会更新。这对于防止不必要的渲染和提高性能很有用。

补充提问:说一说 useEffect 和 useLayoutEffect 的区别

在 React 中,用于定义有副作用因变量的 Hook 有:

  • useEffect:回调函数会在 commit 阶段完成后异步执行,所以不会阻塞视图渲染
  • useLayoutEffect:回调函数会在 commit 阶段的 Layout 子阶段同步执行,一般用于执行 DOM 相关的操作。所以如果计算量大可能会导致掉帧,阻塞渲染。

所以 useLayoutEffect 会比 useEffect 先执行。

每一个 effect 会与当前 FC 其他的 effect 形成环状链表,连接方式为单向环状链表。

其中 useEffect 工作流程可以分为:

  • 声明阶段
  • 调度阶段
  • 执行阶段

useLayoutEffect 的工作流程可以分为:

  • 声明阶段
  • 执行阶段

之所以 useEffect 会比 useLayoutEffect 多一个阶段,就是因为 useEffect 的回调函数会在 commit 阶段完成后异步执行,因此需要经历调度阶段。

补充提问:项目中有没有用到 redux

redux 是用于数据状态管理,而 react 是一个视图层面的库

如果将两者连接在一起,可以使用官方推荐 react-redux 库,其具有高效且灵活的特性

react-redux 将组件分成:

  • 容器组件:存在逻辑处理
  • UI 组件:只负责现显示和交互,内部不处理逻辑,状态由外部控制

通过 redux 将整个应用状态存储到 store 中,组件可以派发 dispatch 行为 action 给 store

其他组件通过订阅 store 中的状态 state 来更新自身的视图

3. 场景题 - 页面优化

前端页面会向后端去获取一些数据,用于列表展示,比如博客列表、菜谱列表,那数据量相对来说是比较大的,所以当接口请求数据较慢的时候,前端可以做哪些优化处理呢?

后端数据请求比较慢的时候,前端可以在体验上做一些优化,毕竟数据层面不是我们可以去掌控的。比如使用 Skeleton 骨架屏、loading、虚拟滚动、分页等。

  • 懒加载 对于非首屏显示的数据,如滚动列表中的项目,可以使用懒加载。这意味着在用户滚动到需要显示数据的区域之前,不会请求数据。这减少了初始加载时的请求量,提高了首屏加载速度。

长列表加载(懒加载、虚拟列表)

  • 虚拟滚动

    • 按需渲染:在同一时间内,只渲染我们看得见(视口)的 DOM 节点,这样浏览器需要渲染的节点就少了,从而降低渲染时长。
    • 模拟滚动:在用户滚动滑轮或滑动屏幕的时候,模拟滚动行为去滚动列表,即根据滚动的位置重新渲染可见的列表元素。

虚拟滚动

  • 分页

对于大量数据,不要一次性加载全部,而是将数据分为多个页面,用户可以按需加载。这减轻了服务器的压力,并降低了每次请求的数据量。

  1. 数据缓存

对于不经常变更的数据,前端可以使用本地缓存技术(如 localStroage、sessionStorage 或 IndexedDB)来存储之前请求过的数据。当再次需要这些数据时,可以先检查本地缓存,如果数据存在且未过期,则直接使用缓存数据,避免重复请求后端。

  1. 请求合并

如果多个请求可以合并为一个,那么应该尽量减少单独的请求次数。例如,当页面需要加载多个资源时,可以合并这些请求为一个请求。

  1. 请求优化

优化请求参数,减少不必要的字段或数据,确保请求的数据量尽可能小。此外,可以考虑使用 HTTP/2 或更高版本的协议,利用多路复用、头部压缩等技术来减少传输延迟。

  1. 加载动画与占位符

在数据加载过程中,为用户提供加载动画或占位符,使用户知道数据正在加载中,而不是让页面处于空白或停滞状态。

  1. 预加载

预加载是一种预测性加载技术,根据用户的操作习惯或页面上下文,提前加载用户可能需要的数据。例如,在滚动列表时,可以提前加载下一页的数据。

  1. 延迟加载

对于非核心功能或用户可能不会立即注意到的数据,可以使用延迟加载。即先加载核心数据,然后等待一段时间(如用户滚动到页面底部)后再加载其他数据。

9.服务端渲染或预渲染

对于需要大量数据渲染的页面,可以考虑使用服务端渲染技术。这样,用户在首屏加载时就能看到完整的数据,而不是等待前端请求和渲染。

预渲染则是一种在构建阶段生成静态 HTML 页面的技术,适用于内容不经常变更的场景。

  1. 优化后端接口性能

前端优化只是提升用户体验的一部分,同时也需要关注后端接口的性能。确保后端接口响应迅速,数据处理高效,以及数据库查询优化等。

4. 跨域是怎么解决

什么时候发生跨域

跨域是由于浏览器的同源策略所导致的,是发生在页面服务端请求的过程中

怎么解决跨域

1. Nginx 反向代理(可以使用 docker 开镜像)

使用 Nginx 充当代理服务器,分发请求到目标服务器

js
server {
    listen      5000;
    server_name 127.0.0.1;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location /api {
        proxy_pass http://127.0.0.1:4000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

2. 搭建 BFF 层来解决跨域问题(Node 环境)

Node.js 同域部署页面,搭建 BFF 层,服务对服务请求

js
const KoaRouter = require('koa-router');
const router = new KoaROuter();

router.post('/api/task', async (ctx, next) => {
    const res = await axios.post('http://127.0.0.1:4000/api/task');
    ctx.body = res.data;
});

app.use(router.routes());
app.use(router.allowedMethos());
延伸问题:引入 BFF 层有什么好处

BFF 全称 Backend For Frontend,一般是指在前端与服务器端搭建一层由前端维护的 Node Server 服务。

其好处有:

  • 数据处理。对数据进行校验、清洗及格式化,使得数据与前端更契合。
  • 数据聚合。后端无需处理大量的表连接工作,第三方接口聚合工作,业务逻辑简化为各个资源的增删改查,由 BFF 层聚合各个资源的数据,后端可集中处理性能问题、监控问题、消息队列等。
  • 权限前移。在 BFF 层统一认证鉴权,后端无需做权限校验,后端可直接部署在集群内网,无需向外网暴露服务,减少了后端复杂度。

其坏处是:

  • 引入复杂度。新的 BFF 服务需要一套基础设施的支持,如日志、异常、部署、监控等。

3. 设置 CORS 头部字段

服务器端配置 CORS 策略,可以允许指定源(协议、域名、端口)的请求,设置Access-Control-Allow-Origin

CORS 是一种基于 HTTP 头的跨域解决方案,它允许服务器指定哪些域可以访问其资源。通过在响应头中添加 Access-Control-Allow-Origin 标签,服务器可以允许指定域的请求访问其资源。CORS 支持多种 HTTP 请求方法,包括 GET、POST、PUT、DELETE 等。

js
Access-Control-Allow-Origin: http://api.bob.com  // 允许跨域的源地址
Access-Control-Allow-Methods: GET, POST, PUT // 服务器支持的所有跨域请求的方法
Access-Control-Allow-Headers: X-Custom-Header  // 服务器支持的所有头信息字段
Access-Control-Allow-Credentials: true   // 表示是否允许发送Cookie
Access-Control-Max-Age: 1728000  // 用来指定本次预检请求的有效期,单位为秒

4. iframe 通讯

通过在主页嵌入一个隐藏的 iframe,将目标页面加载到 iframe 中,并通过在主页面和 iframe 页面之间使用 postMessage() 方法进行消息传递,从而实现跨域的数据交换。

5. 如果想在页面加载的时候发送请求,类组件和函数组件分别写在哪里

类组件

React 中请求通常在 componentDidMount 生命周期函数中发送。这个生命周期函数在组件已经挂载到页面上,并且可以操作 DOM 元素时调用。

函数组件

在 useEffect 中发送请求

6. 用过 lodash 吗?常用的方法有些?

用过。

常用的方法有:

compact

返回一个新数组,包含原数组中所有的非假值元素。例如 false, null, 0, "", undefined, 和 NaN 都是被认为是“假值”。 例子:

js
_.compact([0, 1, false, 2, '', null, 3]);
// => [1, 2, 3]

自己实现:

js
[0, 1, false, 2, '', null, 3].filter(Boolean);
// => [1, 2, 3]

flatten

减少一级 array 嵌套深度。 例子:

js
_.flatten([1, [2, [3, [4]], 5]]);
// => [1, 2, [3, [4]], 5]

自己实现:

js
[].concat(...[1, [2, [3, [4]], 5]]);
// => [1, 2, [3, [4]], 5]

flattenDeep

将 array 扁平化为一维数组。 例子:

js
_.flattenDeep([1, [2, [3, [4]], 5]]);
// => [1, 2, 3, 4, 5]

自己实现:

js
function flattenDeep(arr) {
    let ret = [];
    for (let i = 0; i < arr.length; i++) {
        const item = arr[i];
        if (Array.isArray(item)) {
            ret = ret.concat(flattenDeep(item));
        } else {
            ret.push(item);
        }
    }
    return ret;
}
flattenDeep([1, [2, [3, [4]], 5]]);
// => [1, 2, 3, 4, 5]

nth

获取 array 数组的第 n 个元素。如果 n 为负数,则返回从数组结尾开始的第 n 个元素。 例子:

js
var array = ['a', 'b', 'c', 'd'];

_.nth(array, 1);
// => 'b'

_.nth(array, -2);
// => 'c'

一般情况下,我们使用下标的方式可能会更方便,但是如果要获取数组结尾开始的第 n 个元素 使用 nth 更直观,因为 js 中数组不支持通过负数索引获取结尾的元素,只能通过数组长度来计算正数索引

自己实现:

js
var array = ['a', 'b', 'c', 'd'];

array[1];
// => 'b'

array[array.length - 2];
// => 'c'

intersection

返回两个数组的交集 例子:

js
_.intersection([2, 1, 3], [4, 2, 3]);
// => [2, 3]

自己实现:

js
function intersection(a, b) {
    return a.filter((v) => b.indexOf(v) >= 0);
}
intersection([2, 1, 3], [4, 2, 3]);
// => [2, 3, 1]

开发中有时候并不是上面的单纯的数值集合,大部分都是对象集合,那么可以使用 intersectionBy

lodash 中很多方法(名字 By 结尾的方法)都支持,iteratee(迭代函数)参数,调用每一个数组(array)的每个元素以产生唯一性计算的标准 返回两个对象数组中某个属性集合的交集 例子:

js
var a = [{ val: 2 }, { val: 1 }, { val: 3 }];
var b = [{ val: 4 }, { val: 2 }, { val: 3 }];
_.intersectionBy(a, b, (v) => v.val);
// _.intersectionBy(a, b, 'val')
// => [{ val: 2 }, { val: 3 }]

union

返回两个数组的并集 例子:

js
_.union([2, 3], [1, 2, 3]);
// => [2, 3, 1]

自己实现:

js
function union(a, b) {
    return a.concat(b.filter((v) => !a.includes(v)));
}
union([2, 3], [1, 2, 3]);
// => [2, 3, 1]

union 也有对应的 unionBy 方法,返回两个对象数组中某个属性集合的并集 例子:

js
var a = [{ val: 2 }, { val: 1 }, { val: 3 }];
var b = [{ val: 4 }, { val: 2 }, { val: 3 }];
_.unionBy(a, b, (v) => v.val);
// _.unionBy(a, b, 'val')
// => [{ val: 2 }, { val: 1 }, { val: 3 }, { val: 4}]

uniq

返回一个去重后的新数组 例子:

js
_.uniq([3, 2, 1, 3, 2, 1])
// => [3, 2, 1]

uniqnBy

返回一个去重后的新数组 例子:

js
var a = [
  {val: 3},
  {val: 2},
  {val: 1},
  {val: 3},
  {val: 2},
  {val: 1}
]
_.uniqBy(a, v => v.val)
// _.uniqBy(a, 'val')
// => [{val: 3}, {val: 2}, {val: 1}]

debounce

函数防抖,一般使用在 输入框 远程搜索、输入框 校验 例子:

js
function handler() {
    // async query
}
var debounced = _.debounce(handler, 200);
// 200 ms 内不再输入,则会调用 handler 函数
el.addEventListener('input', debounced);

throttle

函数节流,一般使用在 滚动事件、窗口大小变化事件 例子:

js
function handler() {
    // some code
}
var throttled = _.throttle(handler, 200);
// 滚动过程中,每隔 200 ms 调用一次 handler 函数
window.addEventListener('scroll', throttled);

cloneDeep

对象深拷贝 例子:

js
var arr = [{ a: 1 }, { b: 2 }];

var deep = _.cloneDeep(arr);
arr[0].a = 3;
console.log(arr); // [{ a: 3 }, { b: 2 }]
console.log(deep); // [{ a: 1 }, { b: 2 }]

isEqual

执行深比较来确定两者的值是否相等,是比较值不是比较引用 例子:

js
var object = { a: 1 };
var other = { a: 1 };

_.isEqual(object, other);
// => true

object === other;
// => false

maxBy

对于一个数组项为对象的数组,给定一个属性值,返回这个属性值最小的数组项 例子:

js
var arr = [{ a: 1 }, { a: 2 }];

_.maxBy(arr, (v) => v.a);
// _.maxBy(arr, 'a')
// => { a: 2 }

minBy

对于一个数组项为对象的数组,给定一个属性值,返回这个属性值最小的数组项 例子:

js
var arr = [{ a: 1 }, { a: 2 }];

_.minBy(arr, (v) => v.a);
// _.minBy(arr, 'a')
// => { a: 1 }

mean

计算 array 的平均值 例子:

js
var arr = [4, 2, 8, 6];

_.mean(arr);
// => 5

对应的 meanBy 方法

js
var arr = [{ val: 4 }, { val: 2 }, { val: 8 }, { val: 6 }];

_.meanBy(arr, (v) => v.val);
// _.meanBy(arr, 'val')
// => 5

sum

计算 array 中值的总和 例子:

js
var arr = [4, 2, 8, 6];

_.sum(arr);
// => 20

对应的 sumBy 方法

js
var arr = [{ val: 4 }, { val: 2 }, { val: 8 }, { val: 6 }];

_.sumBy(arr, (v) => v.val);
// _.sumBy(arr, 'val')
// => 20

pick

创建一个从 object 中选中的属性的对象 例子:

js
var object = { a: 1, b: '2', c: 3 };

_.pick(object, ['a', 'c']);
// => { a: 1, c: 3 }

omit

创建一个从 object 中排除的属性的对象 例子:

js
var object = { a: 1, b: '2', c: 3 };

_.omit(object, ['a', 'c']);
// => { b: '2' }

flow

pipe 函数,从左到右调用传入的函数,上一个函数的输出为下一个函数的输入 例子:

js
function add(a) {
    return a + 10;
}
function multiply(a) {
    return a * 10;
}
// var result = add(multiply(10))
var calculate = flow([multiply, add]);
var result = calculate(10);
console.log(res); // 110

flowRight

compose 函数,从右到左调用传入的函数,上一个函数的输出为下一个函数的输入。 react 开发中,可以将多个高阶函数组合起来,单参数 HOC 具有签名 Component => Component。 输出类型与输入类型相同的函数很容易组合在一起。 例子:

js
function add(a) {
    return a + 10;
}
function multiply(a) {
    return a * 10;
}
// var result = multiply(add(10))
var calculate = flowRight([multiply, add]);
var result = calculate(10);
console.log(res); // 200

7. antd 的图表在使用时,哪个图表给你带来了困扰?有问题,但是也解决了

8. upload 上传的时候,文件大小超过 5M 就上传失败,应该怎么处理

9. upload 上传组件的进度条样式和 UI 的不一样,现在是上下布局,希望改成左右布局,重构进度条部分的功能,应该怎么做?

让后端提供文件读取的百分比

10. 博客分享链接给别人的时候,我希望别人点进来之后只能看到博客正文,其他的侧边栏之类的都看不到,应该怎么做?

隐藏样式

比如 url 多携带一个参数,用来判断是否需要隐藏元素

要在前端实现这样的功能,你可以通过结合 URL 参数和 JavaScript/CSS 来控制页面上元素的显示与隐藏。下面是一个详细的步骤说明:

步骤 1: 定义 URL 参数

首先,你需要为你的分享链接定义一个 URL 参数,比如?view=clean,这样链接看起来可能是这样的:https://yourblog.com/your-post?view=clean

步骤 2: 读取 URL 参数

接下来,你需要在前端 JavaScript 代码中读取这个 URL 参数。你可以使用原生的 JavaScript 方法,也可以使用一些库(如 jQuery)来简化这个过程。以下是一个使用原生 JavaScript 读取 URL 参数的例子:

js
function getQueryParam(name) {
    const searchParams = new URLSearchParams(window.location.search);
    return searchParams.get(name);
}

const viewParam = getQueryParam('view');

步骤 3: 根据参数隐藏元素

现在,你可以根据读取到的参数值来决定是否隐藏某些元素。比如,如果viewParam的值是clean,你就隐藏侧边栏和页脚:

js
if (viewParam === 'clean') {
    // 隐藏侧边栏
    const sidebar = document.getElementById('sidebar');
    if (sidebar) {
        sidebar.style.display = 'none';
    }

    // 隐藏页脚
    const footer = document.getElementById('footer');
    if (footer) {
        footer.style.display = 'none';
    }

    // 隐藏其他任何你不想显示的元素...
}

步骤 4: 在适当的时候执行 JavaScript 代码

确保你的 JavaScript 代码在 DOM 加载完成后执行。你可以将上述代码放在window.onload事件处理器中,或者使用DOMContentLoaded事件,或者将<script>标签放在 HTML 文档的底部。

js
document.addEventListener('DOMContentLoaded', function () {
    // ...将上述代码放在这里...
});

步骤 5: 测试

最后,测试你的实现是否按预期工作。尝试访问带有?view=clean参数的 URL,并确认侧边栏和其他不想要的元素确实被隐藏了。同时,也要确保在没有该参数的情况下,页面正常显示。

注意事项

  • 安全性:这种方法仅依赖于前端技术,因此并不是完全安全的。用户仍然可以通过修改 URL 或禁用 JavaScript 来查看原本被隐藏的内容。
  • 可维护性:如果你的博客模板经常变动,确保更新你的 JavaScript 代码以匹配最新的 DOM 结构。
  • 性能:虽然这种方法对性能的影响通常很小,但最好还是尽量减少不必要的 DOM 操作和页面重绘。
  • 用户体验:考虑分享页面的用户体验,确保即使在“清洁”视图下,用户也能轻松地导航回你的博客的其他部分或找到他们需要的信息。