如何优雅地重构一个企业官网 Nextjs 前端项目

前言

最近接手了公司企业官网的开发项目,采用的是 Next.js 14

由于项目初期时间紧、任务重,前任同事直接在公司海外官网的 Next.js 项目基础上进行修改,因此代码结构和需求并不完全匹配。

这篇文章将记录并分享我在改造过程中的一些思路与经验,同时也会介绍几个具体的交互效果是如何实现的。希望能对有类似需求的你有所帮助。

主要做了以下事情:

  • 添加 eslint-plugin-tailwindcsseslint-plugin-jsx-a11y 相关 eslint,让编码更加规范
  • 添加 code-inspector-plugin 提升开发体验
  • 所有幻灯片 Swiper相关代码从命令式写法改写为声明式写法,易读易理解
  • 千行大文件修改为更合理的组件划分,文件目录清晰可读
  • 添加 SEO 相关会爬取的 head meta 内容
  • 使用工具 knip + 人工二次确认,剔除冗余的deadcode,让代码更加清爽干净,剔除未使用的第三方包,减少包体积
  • 双端开发经验分享,图片资源处理

总计修改剔除掉自动变更的 lock 文件(+3336,-5143),变更内容

改造后的Lighthouse得分:

打开项目并运行

Readme 里面写的内容不多,直接运行项目 pnpm installpnpm dev

我首先在终端看到了一个提示,未能开启默认的 SWC 编译,因为有一个 .babelrc 文件存在

Disabled SWC as replacement for Babel because of custom Babel configuration ".babelrc" nextjs.org/docs/messag...

发现是为了使用 @emotion/styled 才配置的。其实我们可以删掉 .babelrc 文件了,按照官网的建议Next.js 14 已经可以在 config 里面配置来支持 emotion nextjs.org/docs/14/arc...

再次启动没有警告了。变快了 2s。

剔除无用代码 (Dead Code)

虽然我们都知道 ESM 的导入导出机制,加上现代构建工具(如 webpackvite)都有摇树优化(tree-shaking),能够在生产环境中自动剔除未使用的模块,不会将无用代码打包进去。

但如果项目中存在大量 dead code,即使不会影响打包体积,依然会影响 开发体验

比如这个项目,功能其实很简单:只有首页、四大产品介绍页和"关于我们"等六个路由页面,但代码体量却看起来非常庞大。主要原因是:它是直接基于公司海外官网项目改的,之前的同事因为时间紧、任务重,没有做彻底的代码清理和优化。

我在群里发了这样一段话,是我自己的理解。

问:为什么需要清理?不清理有什么问题?
答: 懂软件工程的都知道这里面门道有多深(狗头)。随着项目的演进和需求变化,代码会不断增加,导致熵增。虽然不清理冗余代码对界面效果没有直接影响,但开发体验会受损。代码中会充斥大量干扰文件和注释,这使得开发者难以专注于有效的开发工作。 我们又不是没有 git 啊,代码还能丢了吗。如果所有开发者接手代码,你不动我不动的话,就会逐渐演变成为仓库内杂草丛生。后续开发者也会因为担心清理后出现问题而不敢动手,导致开发效率下降。因为每个人都只想做自己分内的事情不想多做,毕竟万一清理了文件出现了 bug,这个锅就到自己头上了。 最终,这种"破窗效应"会使代码质量不断恶化。这个是官网的前端项目,我既希望面子(呈现的内容)好看的同时,里子(代码质量)也好看。

如何清理

肯定不是手动一个个查是不是有用到啦,我使用了开源工具 knip.dev/ 按照官方教程操作一下。

然后就会在终端显示出检测到的 dead code,我们再从终端按下 cmd + 鼠标左键点进去二次确认一下应不应该删除。

配置需要用到的 lint,提升编码规范

项目写样式主要使用 tailwind,然后由于是官网网站需要注意一下 SEO,所以我选择安装下这两个 lint

添加 eslint-plugin-tailwindcsseslint-plugin-jsx-a11y

幻灯片 Swiper 相关改动

Swiper 极大方便了我们做幻灯片效果,感恩!

原先的写法大概是这样的,通过 useEffect 来绑定幻灯片。

其实更推荐这种声明式的写法从 swiper/react 可导出。

再分享一个我觉得挺好看的 Swiper 效果及完整代码

tsx 复制代码
import { ReactNode } from 'react';
import { Swiper, SwiperSlide } from 'swiper/react';
import { EffectCreative, Pagination } from 'swiper';
import 'swiper/css';
import { MarketingItemData } from '..';
import MobileImageInfo from './mobileImageInfo';
import styled from '@emotion/styled';
type MobileImageSwiperProps = {
  images: MarketingItemData[];
  index: string;
  describe: ReactNode;
  active: string;
};

const MobileImageSwiperStyled = styled(Swiper)`
  padding-bottom: 40px;
  .mobile-img-swiper-pagination {
    bottom: 0;
    .swiper-pagination-bullet-active {
      background-color: #ff6868;
    }
  }
`;

// 移动端案例展示图片的轮播图
export default function MobileImageSwiper({ images, index, describe }: MobileImageSwiperProps) {
  return (
    <div className="relative h-full py-8">
      <div className="mx-auto mb-6 flex flex-col items-center justify-center px-2.5 text-center text-base">
        <div>{describe}</div>
      </div>
      <div className="mobile-swiper-wrapper relative">
        <MobileImageSwiperStyled
          modules={[EffectCreative, Pagination]}
          slidesPerView={1.4}
          centeredSlides={true}
          spaceBetween={10}
          loop={true}
          speed={600}
          pagination={{
            clickable: true,
            clickableClass: 'mobile-img-swiper-pagination',
          }}
          className={`mobile-img-swiper-${index} relative w-full overflow-hidden`}
          onSwiper={(swiper) => {
            const updateSlides = () => {
              swiper.slides?.forEach((slide: HTMLElement) => {
                const isActive = slide.classList.contains('swiper-slide-active');
                slide.style.opacity = isActive ? '1' : '0.5';
                slide.style.transform = isActive ? 'scale(1)' : 'scale(0.91)';
                slide.style.marginTop = isActive ? '0' : '';
                slide.style.transition = 'all 0.5s ease';
              });
            };
            setTimeout(updateSlides, 100);
            swiper.on('slideChangeTransitionStart', updateSlides);
          }}
        >
          {images.map((item, i) => (
            <SwiperSlide
              key={`${i}${item.casename}`}
              className="overflow-hidden rounded-2xl bg-white"
            >
              <div className="flex flex-col">
                <div className="relative aspect-[1.5] w-full">
                  <img
                    className="size-full object-cover"
                    src={item.img}
                    alt={item.casename || 'marketing-img'}
                  />
                </div>
                <MobileImageInfo data={item} />
              </div>
            </SwiperSlide>
          ))}
        </MobileImageSwiperStyled>
      </div>
    </div>
  );
}

双端开发经验分享

一般都会在项目左下角放置一个开发模式显示的媒体查询指示器,我们可以直观的看到当前处于什么宽度条件下。

tsx 复制代码
export function TailwindIndicator() {
  if (process.env.NODE_ENV === 'production') return null;

  return (
    <div className="fixed bottom-1 left-1 z-50 flex size-6 items-center justify-center rounded-full bg-gray-800 p-3 font-mono text-xs text-white">
      <div className="block sm:hidden">xs</div>
      <div className="hidden sm:block md:hidden lg:hidden xl:hidden 2xl:hidden">sm</div>
      <div className="hidden md:block lg:hidden xl:hidden 2xl:hidden">md</div>
      <div className="hidden lg:block xl:hidden 2xl:hidden">lg</div>
      <div className="hidden xl:block 2xl:hidden">xl</div>
      <div className="hidden 2xl:block">2xl</div>
    </div>
  );
}

由于使用的是 tailwind,就可以很方便的在 className 中使用各种媒体查询前缀 tailwind 媒体查询

下面是我新增在项目 Readme 中的备注

xl:xxx (xl 尺寸下的样式) max-xl:xxx(小于 xl 时候的样式),tailwind 官网建议移动端优先,但我们的官网是 web 优先的,所以移动端的媒体查询就作为辅助副产品,先开发 web 端。

有一些模块如果 web 和 Mobile 差异较大的话,无法通过媒体查询修改状态可以参考 Swiper分为两个组件,然后通过媒体查询控制显隐。不要通过 js,因为有 ssr,会报警告 dom 不匹配的问题水合报错。

tsx 复制代码
<div className="block lg:hidden">
    <MobileSwiper active={active} setActive={setActive} setSwiperObj={setSwiperObj} />
</div>
<div className="hidden lg:block">
    <WebSwiper active={active} setActive={setActive} setSwiperObj={setSwiperObj} />
</div>

网站的图片资源

官网一般会展示许许多多的图片素材,我们选用格式的时候推荐使用 webp 格式,现在大部分应该已经支持的了。我这个项目使用的图片都存储在腾讯云的对象存储中,将 figma 中的图片素材导出后上传到自己公司内的 cos 中。

腾讯云的 cos 提供了方便的数据万象功能 腾讯云 cos 图片基础压缩(WebP)服务 只需要在末尾加上字符串 ?imageMogr2/format/webp即可进行图片转换成 webp

shell 复制代码
http://example-1258125638.cos.ap-shanghai.myqcloud.com/sample.png?imageMogr2/format/webp

SEO 辅助工具

一个专门分析 SEO 的浏览器插件,也是看掘金小伙伴们发的文章了解到的

aitdk.com/extension?u...

结语

以上就是本次 nextjs 官网的改造过程经验分享了,希望对大家有帮助~

悄悄吐槽一句:不知道是不是因为我用的是 Intel iMac,Next.js 14 的开发体验偶尔会突然卡顿,整个电脑都跟着变慢了......

我也试着在 Next.js 14 中启用了 Turbopack(虽然它在这个版本中还属于实验性功能),结果体验确实不太理想。

考虑到 Next.js 15 才正式启用 Turbopack,如果以后再让我从零搭建企业官网这类项目,我可能会倾向选用 Remix ------ 现在也就是 React Router v7

相关推荐
前端之虎陈随易9 小时前
编程语言级别的Skill市场,AI Agent 的未来形态
前端·vue.js·人工智能·typescript·node.js
一路向北he9 小时前
字节钢铁军团--“提供情境,而非控制”
java·开发语言·前端
kyriewen9 小时前
豆包和千问同时关了智能体,我用它们搭的 3 个自动化全废了——迁移方案整理
前端·javascript·ai编程
前端一小卒9 小时前
我用 TypeScript 从零手写了一个 Claude Code,然后发现它的核心只有 30 行
前端·agent
大圣编程11 小时前
Python中continue语句的用法是什么?
开发语言·前端·python
yuhaiqiang11 小时前
随手 vibecoding 的浏览器插件已经 6000 多次下载,聊聊他的产品设计
前端·后端·面试
之歆11 小时前
Vue商品详情与放大镜组件
前端·javascript·vue.js
再吃一根胡萝卜12 小时前
如何把小米 MiMo 接入 CodeBuddy,打造私有 Agent
前端
负责的蛋挞13 小时前
异步HttpModule的实现方式
java·服务器·前端
YFF菲菲兔14 小时前
其他 Hooks 解析
react.js