如何优雅地重构一个企业官网 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

相关推荐
ElasticPDF-新国产PDF编辑器11 分钟前
Vue 项目 PDF 批注插件库在线版 API 示例教程
前端·vue.js·pdf
拉不动的猪17 分钟前
react基础2
前端·javascript·面试
kovlistudio18 分钟前
红宝书第二十九讲:详解编辑器和IDE:VS Code与WebStorm
开发语言·前端·javascript·ide·学习·编辑器·webstorm
拉不动的猪20 分钟前
react基础1
前端·javascript·面试
鱼樱前端1 小时前
Vite 工程化深度解析与最佳实践
前端·javascript
鱼樱前端1 小时前
Webpack 在前端工程化中的核心应用解析-构建老大
前端·javascript
Moment1 小时前
多人协同编辑算法 —— CRDT 算法 🐂🐂🐂
前端·javascript·面试
小付同学呀1 小时前
前端快速入门学习4——CSS盒子模型、浮动、定位
前端·css·学习
OpenTiny社区3 小时前
TinyPro 中后台管理系统使用指南——让页面搭建变得如此简单!
前端·vue.js·开源