【React】使用Next.js构建并部署个人博客

news2024/11/17 17:52:14

👉 TypeScript学习:TypeScript从入门到精通

👉 蓝桥杯真题解析:蓝桥杯Web国赛真题解析

👉 个人简介:一个又菜又爱玩的前端小白🍬
👉 你的一键三连是我更新的最大动力❤️!

🏆分享博主自用牛客网🏆:一个非常全面的面试刷题求职网站,真的超级好用🍬


前言

关于博客系统,相信大家早已驾轻就熟,网上有很多以markdown驱动的博客框架,如vuepress,hexo等,这类框架的本质是生成静态站点,而个人开发的博客系统大多是使用数据库的全栈项目,这两种方式各有各的好处,这里就不做比较了

这篇文章我们将自己独立去开发并部署一个以markdown驱动的静态站点博客,所用技术栈如下:

  • React
  • TypeScript
  • Next.js
  • tailwindcss
  • Vercel部署

注意: 本文只是演示使用Next.js从0到1构建并部署一个个人博客项目,不会对项目构建过程中所用到的技术做详细的讲解,不过不用担心,只要跟着文章一步一步来,小白都能成功部署自己的个人博客!

项目仓库地址:https://github.com/Chen0807AiLJX/next-blog
最终效果可见:https://next-blog-eosin-six.vercel.app/

现在让我们开始吧!

开始之前请确保自己电脑上配置的有Node.js 12.13.0 或更高版本。

文章目录

  • 前言
  • 1、创建Next.js项目
  • 2、安装tailwindcss
  • 3、添加布局页面
  • 4、新建markdown文章
  • 5、解析markdown内容
  • 6、添加首页
  • 7、添加文章详情页面
  • 8、Vercel部署
  • 结语

1、创建Next.js项目

要创建 Next.js 应用程序,请打开终端,cd进入到要在其中创建应用程序的目录,然后运行以下命令:

npx create-next-app@latest --typescript ailjx-blog

上述代码表示:通过create-next-app创建名为ailjx-blogTypeScript版本的Next.js应用程序

vscode打开ailjx-blog项目,目录结构如下:

在这里插入图片描述

在项目根目录终端运行以下命令启动项目

npm run dev

打开http://localhost:3000/显示如下页面:
在这里插入图片描述

2、安装tailwindcss

在项目根目录终端运行以下命令:

npm install -D tailwindcss@latest postcss@latest autoprefixer@latest

生成tailwindcss配置文件:

npx tailwindcss init -p 

此时项目里会多出两个文件:tailwind.config.jspostcss.config.js

修改tailwind.config.js文件里的content为:

    content: [
        "./pages/**/*.{js,ts,jsx,tsx}",
        "./components/**/*.{js,ts,jsx,tsx}",
        "./styles/**/*.css",
    ],

pages文件夹下的_app.tsx文件的第一行添加:

import "tailwindcss/tailwind.css";

之后重新启动项目

3、添加布局页面

准备一张自己的头像(建议比例为1:1,这里演示用的头像文件名为author.jpg

public文件夹下新建images文件夹,将你的头像图片放入其中,并删除public文件夹下的svg文件

public文件为项目的静态文件,可直接通过地址访问,如访问演示所用头像:http://localhost:3000/images/author.jpg

项目根目录下新建components文件夹,并添加布局文件layout.tsx

import Head from "next/head";
import Image from "next/image";
import Link from "next/link";

const name = "Ailjx"; // 名称,根据需要修改
export const siteTitle = "Ailjx Blog"; // 网站标题,根据需要修改

interface Props {
    children: React.ReactNode;
    home?: boolean;
}

export default function Layout({ children, home }: Props) {
    return (
        <div className='max-w-2xl mx-auto px-4 mt-12 mb-24'>
            <Head>
                <link rel='icon' href='/favicon.ico' />
                <meta name='description' content='AiljxBlog——Ailjx的博客' />
                <meta
                    property='og:image'
                    content={`https://og-image.vercel.app/${encodeURI(
                        siteTitle
                    )}.png?theme=light&md=0&fontSize=75px&images=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg`}
                />
                <meta name='og:title' content={siteTitle} />
                <meta name='twitter:card' content='summary_large_image' />
            </Head>
            <header className='flex flex-col items-center'>
                {home ? (
                    <>
                        <Image
                            priority
                            src='/images/author.jpg'
                            className='rounded-full'
                            height={144}
                            width={144}
                            alt={name}
                        />
                        <h1 className='text-5xl font-extrabold tracking-tighter my-4'>
                            {name}
                        </h1>
                    </>
                ) : (
                    <>
                        <Link href='/'>
                            <a>
                                <Image
                                    priority
                                    src='/images/author.jpg'
                                    className='rounded-full'
                                    height={108}
                                    width={108}
                                    alt={name}
                                />
                            </a>
                        </Link>
                        <h2 className='text-2xl my-4'>
                            <Link href='/'>
                                <a>{name}</a>
                            </Link>
                        </h2>
                    </>
                )}
            </header>
            <main>{children}</main>
            {!home && (
                <div className='mt-12'>
                    <Link href='/'>
                        <a>← 返回首页</a>
                    </Link>
                </div>
            )}
        </div>
    );
}

这里使用了几个Next自带的组件:

  • Head:向Html页面的head内添加内容,里面内容自己根据需要修改
  • Image:渲染图像的组件,src地址修改为自己头像的地址
  • Link :页面间跳转组件

4、新建markdown文章

项目根目录下新建posts文件夹,添加一个markdown文件,如:

欢迎来到我的博客.md

---
title: "欢迎来到我的博客"
date: "2022-08-08"
---

## 欢迎你!

注意: 需要在每个markdown文件的顶部通过---添加元数据,元数据需要有title字段表示文章标题,date字段表示日期,如上面欢迎来到我的博客.md的元数据为:

---
title: "欢迎来到我的博客"
date: "2022-08-08"
---

这些数据在我们渲染markdown内容时需要用到

5、解析markdown内容

需要安装以下插件:

  • remark-prism:代码高亮插件
  • date-fns:处理日期
  • gray-matter:获取元数据
  • next-mdx-remote:用于解析和渲染markdown内容
  • remark-external-links:对markdown内的链接添加reltarget,使其能够在新页面打开

在项目根目录终端运行以下命令安装上述插件:

 npm i remark-prism date-fns gray-matter next-mdx-remote remark-external-links
npm i @types/remark-prism --D

在项目根目录新建存放工具函数的utils文件夹,里面新建处理markdown文件的posts.ts

import fs from "fs";
import path from "path";
// gray-matter:获取元数据
import matter from "gray-matter";
// date-fns:处理日期
import { parseISO } from "date-fns";
import { serialize } from "next-mdx-remote/serialize";
// remark-prism:markdown代码高亮
import prism from "remark-prism";
// externalLinks:使markdown的链接是在新页面打开链接
import externalLinks from "remark-external-links";

interface MatterMark {
    data: { date: string; title: string };
    content: string;
    [key: string]: unknown;
}

// posts目录的路径
const postsDirectory = path.join(process.cwd(), "posts");
// 获取posts目录下的所有文件名(带后缀)
const fileNames = fs.readdirSync(postsDirectory);

// 获取所有文章用于展示首页列表的数据
export function getSortedPostsData() {
    // 获取所有md文件用于展示首页列表的数据,包含id,元数据(标题,时间)
    const allPostsData = fileNames.map((fileName) => {
        // 去除文件名的md后缀,使其作为文章id使用
        const id = fileName.replace(/\.md$/, "");

        // 获取md文件路径
        const fullPath = path.join(postsDirectory, fileName);

        // 读取md文件内容
        const fileContents = fs.readFileSync(fullPath, "utf8");

        // 使用matter提取md文件元数据:{data:{//元数据},content:'内容'}
        const matterResult = matter(fileContents);

        return {
            id,
            ...(matterResult.data as MatterMark["data"]),
        };
    });

    // 按照日期从进到远排序
    return allPostsData.sort(({ date: a }, { date: b }) =>
        // parseISO:字符串转日期
        parseISO(a) < parseISO(b) ? 1 : -1
    );
}

// 获取格式化后的所有文章id(文件名)
export function getAllPostIds() {
    // 这是返回的格式:
    // [
    //   {
    //     params: {
    //       id: '......'
    //     }
    //   },
    //   {
    //     params: {
    //       id: '......'
    //     }
    //   }
    // ]

    return fileNames.map((fileName) => {
        return {
            params: {
                id: fileName.replace(/\.md$/, ""),
            },
        };
    });
}

// 获取指定文章内容
export async function getPostData(id: string) {
    // 文章路径
    const fullPath = path.join(postsDirectory, `${id}.md`);

    // 读取文章内容
    const fileContents = fs.readFileSync(fullPath, "utf8");

    // 使用matter解析markdown元数据和内容
    const matterResult = matter(fileContents);

    return {
        content: await serialize(matterResult.content, {
            mdxOptions: { remarkPlugins: [prism, externalLinks] },
        }),
        ...(matterResult.data as MatterMark["data"]),
    };
}

posts.ts里有三个主要的函数:

  • getSortedPostsData:在首页用于展示文章列表

  • getAllPostIds:获取指定格式的所有文章id(文件名),这个格式是Next所要求的

    因为我们在写文章详情页面时需要使用动态路由,每个文章的id就是一个路由,并且我们使用的Next静态站点生成会在项目打包构建时直接生成所有的html文件,需要把每一个路由对应的页面都构建出来,Next会根据getAllPostIds函数返回的这种格式的数据去构建每一个html页面

  • getPostData:获取文章详情,在文章详情页面会用到

6、添加首页

首页会展示文章列表,会用到一个日期渲染组件,我们先创建一下

components文件夹下新建date.tsx文件:

import { parseISO, format } from "date-fns";

interface Props {
    dateString: string;
}

export default function Date({ dateString }: Props) {
    const date = parseISO(dateString);
    return (
        <time dateTime={dateString} className='text-gray-500'>
            {format(date, "yyyy年MM月dd日")}
        </time>
    );
}

修改pages文件夹下的index.tsx文件如下:

import type { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import Layout, { siteTitle } from "../components/layout";
import Link from "next/link";
import Date from "../components/date";

import { getSortedPostsData } from "../utils/posts";

interface Props {
    allPostsData: {
        date: string;
        title: string;
        id: string;
    }[];
}

const Home: NextPage<Props> = ({ allPostsData }) => {
    return (
        <Layout home>
            <div>
                <Head>
                    <title>{siteTitle}</title>
                </Head>

                <section className='text-xl leading-normal text-center'>
                    <p>你好,我是 Ailjx</p>
                    <p>一个又菜又爱玩的前端小白,欢迎来到我的博客!</p>
                </section>

                <section className='text-xl leading-normal pt-4'>
                    <h2 className=' text-2xl my-4 font-bold'>Blog</h2>
                    <ul>
                        {allPostsData.map(({ id, date, title }) => (
                            <li key={id} className='mb-5'>
                                <Link href={`/posts/${id}`}>
                                    <a>{title}</a>
                                </Link>
                                <br />
                                <small>
                                    <Date dateString={date} />
                                </small>
                            </li>
                        ))}
                    </ul>
                </section>
            </div>
        </Layout>
    );
};

export const getStaticProps: GetStaticProps = async () => {
  	// 获取文章列表
    const allPostsData = getSortedPostsData();

    return {
        props: {
            allPostsData,
        },
    };
};

export default Home;

修改styles文件夹下的globals.css如下:

a {
    color: #0070f3;
    text-decoration: none;
}

a:hover {
    text-decoration: underline;
}

img {
    max-width: 100%;
    display: block;
}
::-webkit-scrollbar {
    width: 5px;
    height: 5px;
    position: absolute;
}
::-webkit-scrollbar-thumb {
    background-color: #0070f3;
}
::-webkit-scrollbar-track {
    background-color: #ddd;
}

删除style文件夹下的Home.module.css

此时运行项目,打开http://localhost:3000/可见:
在这里插入图片描述

7、添加文章详情页面

pages文件夹下创建posts文件夹,在其中创建[id].tsx文件:

import type { GetStaticProps, GetStaticPaths } from "next";
import Layout from "../../components/layout";
import { getAllPostIds, getPostData } from "../../utils/posts";
import Head from "next/head";
import Date from "../../components/date";
import { MDXRemote, MDXRemoteProps } from "next-mdx-remote";
// 引入代码高亮css
import "prismjs/themes/prism-okaidia.min.css";

interface Props {
    postData: {
        title: string;
        date: string;
        content: MDXRemoteProps;
    };
}

export default function Post({ postData }: Props) {
    return (
        <Layout>
            <Head>
                <title>{postData.title}</title>
            </Head>
            <h1 className='text-3xl font-extrabold my-4 tracking-tighter'>
                {postData.title}
            </h1>

            <Date dateString={postData.date} />

            <article className='py-8 prose  prose-h1:mt-8'>
                <MDXRemote {...postData.content} />
            </article>
        </Layout>
    );
}

// getStaticProps和getStaticPaths只在服务器端运行,永远不会在客户端运行
export const getStaticPaths: GetStaticPaths = async () => {
    // 获取所有文章id,即所有路由
    const paths = getAllPostIds();
    return {
        paths,
        fallback: false,
    };
};

export const getStaticProps: GetStaticProps = async ({ params }) => {
	// 获取文章内容 
    const postData = await getPostData(params!.id as string);
    return {
        props: {
            postData,
        },
    };
};

之后在首页点击文章列表跳转到文章详情页面:
在这里插入图片描述
到此一个简单的博客项目就写好了

8、Vercel部署

没有Github账号的先去注册一个账号

在Github上新建一个名为next-blog的仓库(名称自己根据需要修改):

在这里插入图片描述

仓库权限公共私有都可,并且不需要使用README 或其他文件对其进行初始化

在我们的博客项目根目录下运行以下命令推送代码到Github仓库里:

git remote add origin https://github.com/<username>/next-blog.git
git branch -M main
git push -u origin main

请将上述第一行命令origin后面的地址替换成你的仓库地址,一般是将<username>替换为你Gitub的用户名,next-blog替换成你仓库的名称

之后刷新仓库查看代码:

在这里插入图片描述

项目仓库地址:https://github.com/Chen0807AiLJX/next-blog

细心的大佬应该会发现我们这样提交代码是有问题的,因为我们并没有合并本地代码到本地仓库,所以提交到Github仓库的代码并不是我们最终的效果,而是创建Next.js时的初始效果。

不过不用担心,我们在后面会对其进行处理。当然,你也可以现在处理,直接将最新的代码同步到仓库,这样你就免了后面我们对其处理的操作

打开Vercel,没有Vercel账号的点击右上角的注册按钮进行注册,注册时选择通过Github注册,登录时也使用Github登录

登录Vecel成功后打开 https://vercel.com/import/git或https://vercel.com/new或点击新建项目按钮,之后进入到以下页面:

在这里插入图片描述
这个页面中会自动获取你的Github仓库,选择你刚刚推送博客项目的仓库,点击Import按钮,之后直接点击Deploy按钮:

在这里插入图片描述

稍等片刻,出现以下页面就部署成功了:

在这里插入图片描述

点击上述页面左侧的页面预览框就能跳转到你部署成功的网页了,但这时你会发现部署的页面不是我们最终的页面,而是创建Next.js时的初始页面,这是因为我们在Git提交代码到仓库时没有合并本地代码,我们重新提交一下就行了

我们可以在VScode里快速提交代码到仓库:

在这里插入图片描述
在这里插入图片描述

点击同步更改后会开始转圈,等待转圈结束就提交成功了,之后什么都不用干,仓库代码更新后Vercel会自动部署!!!

打开https://vercel.com/dashboard能查看到你已经部署的项目和对应的网页地址:

在这里插入图片描述

好啦,到此我们的任务就全部完成了,之后需要添加文章只需要在项目的posts文件内新建markdown文件就行了(不要忘记在markdown顶部添加元数据),更新完文章提交代码到仓库即可

结语

这次使用Next.js搭建个人博客只是一个小小的尝试,可以说是只搭建了一个骨架,其实走完整个流程你应该会有很多有趣的想法去完善填充你的博客,因为基础功能我们已经实现,剩下的就是锦上添花的操作了,这完全取决于你

项目仓库地址:https://github.com/Chen0807AiLJX/next-blog
最终效果可见:https://next-blog-eosin-six.vercel.app/

参考资料:

  • Next.js官网
  • tailwindcss中文文档
  • date-fns文档
  • next-mdx-remote仓库
  • remark文档

如果本篇文章对你有所帮助,还请客官一件四连!❤️

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/409763.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Server Tomcat v9.0 Server at localhost failed to start问题

Server Tomcat v9.0 Server at localhost failed to start问题解决办法。 在我们使用eclipse启动Tomcat时&#xff0c;有时会出现Server Tomcat v9.0 Server at localhost failed to start 的错误提示&#xff0c;导致无法成功启动&#xff0c;下面给出出现这种问题的简单解决…

Python开发自定义Web框架

文章目录开发自定义Web框架1.开发Web服务器主体程序2.开发Web框架主体程序3.使用模板来展示响应内容4.开发框架的路由列表功能5.采用装饰器的方式添加路由6.电影列表页面的开发案例开发自定义Web框架 接收web服务器的动态资源请求&#xff0c;给web服务器提供处理动态资源请求…

Web项目(Vue)部署到阿里云服务器【超详细】

超详细Vue项目部署篇&#xff01;&#xff01;&#xff01; 小白的部署之路 前段时间白嫖了一年的阿里云服务器&#xff0c;想着手上有个项目&#xff0c;那就部署上去吧。找了很多教程&#xff0c;没有一篇是完整细致的&#xff0c;对于小白的我来说太难了&#xff0c;然后就…

最全面的SpringBoot教程(三)——SpringBoot Web开发

前言 本文为SpringBoot Web开发相关内容介绍&#xff0c;下边将对静态资源管理&#xff08;包括&#xff1a;静态资源访问&#xff0c;静态资源前缀&#xff0c;webjar&#xff0c;首页支持&#xff09;&#xff0c;请求参数处理&#xff08;包括&#xff1a;Rest风格&#xff…

【微信小程序】-- 自定义组件 - 父子组件之间的通信(三十八)

&#x1f48c; 所属专栏&#xff1a;【微信小程序开发教程】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &…

也许是全网最全的 Angular 新手入门指南

文章目录Angular概述Angular程序架构Angular优势angular/cli脚手架文件加载顺序项目目录结构Angular模块NgModule 装饰器内置模块自定义模块模块的tipsAngular组件Component 元数据数据绑定脏值检测父子组件通讯投影组件Angular指令内置属性型指令内置结构型指令指令事件样式绑…

若依框架(前后端分离)打war包部署到linux

一、前端部署 1.找到ruoyi-ui目录。 2.安装依赖。 npm install 3.执行以下操作&#xff0c;解决 npm 下载速度慢的问题。 npm install --registryhttps://registry.npmmirror.com 4.修改vue.config.js,若后端采用的是默认8080端口&#xff0c;则不用修改&#xff0c;默认就是…

2023最新最全vscode插件精选

文章简介 本文介绍最新、最实用、最强大的 vscode 精选扩展。好用的扩展&#xff0c;犹如神兵利器&#xff0c;帮助程序员在代码的世界中&#xff0c;所向披靡&#xff0c;战无不胜&#xff01; 作者介绍 随易出品&#xff0c;必属精品&#xff0c;只写有深度&#xff0c;有质…

vue 路由钩子

路由钩子分为三种 全局钩子&#xff1a; beforeEach、 afterEach、beforeResolve单个路由里面的钩子&#xff1a; beforeEnter组件路由&#xff1a;beforeRouteEnter、 beforeRouteUpdate、 beforeRouteLeave 它的三个参数&#xff1a; to: (Route路由对象) 即将要进入的目标…

【前端知识体系梳理(三)】Diff策略

​ 目录 &#x1f349;前言 &#x1f349;传统Diff算法 &#x1f349;React Diff &#x1f353;&#x1f353;&#x1f353;1、tree diff &#x1f353;&#x1f353;&#x1f353;2、component diff &#x1f353;&#x1f353;&#x1f353;3、element diff &#x1…

前端页面项目——博客系统

目录 1.实现博客列表页 1.1 实现导航栏 1.2 实现中间版心 1.3 实现个人信息 1.4 实现博客列表 2. 实现博客正文页 3. 实现博客登陆页 4. 实现博客编辑 4.1 实现编辑区 4.2 引入编辑器 展示 1&#xff09;登录页面 2&#xff09;博客列表页 3&#xff09;博客详情页 4&am…

【JavaScript】手撕前端面试题:手写Object.create | 手写Function.call | 手写Function.bind

&#x1f5a5;️ NodeJS专栏&#xff1a;Node.js从入门到精通 &#x1f5a5;️ 博主的前端之路&#xff08;源创征文一等奖作品&#xff09;&#xff1a;前端之行&#xff0c;任重道远&#xff08;来自大三学长的万字自述&#xff09; &#x1f5a5;️ TypeScript知识总结&…

PyQt5之进度条:QProgressBar

PyQt5之进度条&#xff1a;QProgressBar 在软件中&#xff0c;在处理特别冗长的任务时&#xff0c;如果没有相关的进度信息&#xff0c;这个等待的过程会比较考验用户的耐心&#xff0c;根据相关理论&#xff0c;进度条可以缓解用户在等待过程中的焦虑&#xff0c;所以&#x…

前端学习笔记(14)-Vue3组件传参

1.props&#xff08;父组件传递给子组件&#xff09;1.1 实现如果你没有使用 <script setup>&#xff0c;props 必须以 props 选项的方式声明&#xff0c;props 对象会作为 setup() 函数的第一个参数被传入&#xff1a;在子组件中&#xff1a;export default {props: {ti…

微信小程序头像昵称填写能力

1、基本介绍 微信小程序获取头像昵称的能力&#xff0c;最近又进行了一次调整&#xff0c;如果没有记错这是今年第三次调整了&#xff0c;每次调整每个开发者心中我相信都跟我一样&#xff0c;万马奔腾。。。今天写个demo体验下实际效果如何。 详细信息请见小程序用户头像昵称…

微信小程序实现PDF预览功能——pdf.js(含源码解析)

文章目录前言一、pdf.js 是什么&#xff1f;二、使用步骤1.下载库文件2.使用方式微信小程序端——使用 web-view 标签H5 端——使用 iframe 标签&#xff08;使用vue框架&#xff09;3.更改源码如何隐藏顶部工具栏如何让用户强制阅读一定时间如何获取pdf总页数如何获取pdf当前页…

【折腾电脑】Edge浏览器看B站视频卡顿最全解决办法合集

开头碎碎念&#xff1a;更新频率明显和疫情呈正相关&#xff0c;祝大家健健康康吃好喝好&#xff01; 使用Microsoft Edge浏览器观看B站视频&#xff0c;卡得无法忍受。 在网络上搜索相关问题&#xff0c;最早的一条是2016/04/17微软问题反馈的记录。任何原因的卡顿都是正常的&…

Vue样式穿透

Vue样式穿透 vue文件的style标签的scoped属性作用&#xff1a;PostCSS在元素标签上添加特殊属性值&#xff0c;在样式的选择器后面添加属性选择器&#xff0c;实现了组件样式的私有化&#xff0c;防止组件之间的样式污染&#xff08;比如相同类名的元素&#xff09;。 但在使…

【CSS】盒子模型内边距 ② ( 内边距复合写法 | 代码示例 )

文章目录一、内边距复合写法1、语法2、代码示例 - 设置 1 个值3、代码示例 - 设置 2 个值4、代码示例 - 设置 3 个值5、代码示例 - 设置 4 个值一、内边距复合写法 1、语法 盒子模型内边距 可以通过 padding-left 左内边距padding-right 右内边距padding-top 上内边距padding-…

前端开发服务器中的 Proxy 代理跨域实现原理解读

各位朋友你们好&#xff0c;我是桃小瑞&#xff0c;微信公众 桃小瑞。在这给大家拜个晚年&#xff0c;祝各位朋友新年快乐。 前言 在前端的开发过程中&#xff0c;尤其是在浏览器环境下&#xff0c;跨域是个绕不开的话题&#xff0c;相信每个前端都会涉及到这个问题&#xf…