阅读视图

发现新文章,点击刷新页面。

都2026年了还不会Vite插件开发?手写一个版本管理插件,5分钟包会!

2026年了,不会还有人觉得Vite插件开发很难吧?今天就用一个实战案例,让你彻底掌握它!

开篇:为什么2026年你还需要学Vite插件?

说实话,2026年的前端生态已经相当成熟,各种轮子应有尽有。但正是这样的环境下,能解决特定场景痛点的定制化插件,才更能体现一个开发者的工程化能力。

之前项目上遇到个老生常谈的问题:

  • 线上出bug了,是哪个代码版本?
  • 测试环境明明修复了,生产怎么还有问题?
  • 构建时间是多少,缓存要不要刷新?

手动查Git?太low了。写个脚本?不够优雅。于是,我花10分钟写了个Vite插件发布到了npm仓库(搜索 vite-plugin-unified-version),从此版本信息自动注入构建产物,一劳永逸。

今天就手把手带你写出来,保证看完就会,会了就能用!

一、Vite插件到底是啥?3句话说明白

  • 本质:就是一个普通的JavaScript对象
  • 灵魂:对象里的各种钩子函数(Hooks)
  • 作用:在Vite构建的不同阶段「搞事情」

就像你在煮泡面时(构建过程),可以:

  • 烧水前决定用什么锅(config钩子)
  • 煮面时加点调料(transform钩子)
  • 煮完了关火盛出来(closeBundle钩子)

就这么简单!

二、实战:5分钟开发一个版本管理插件

Step 1:搭个架子

// vite-plugin-version.js
export default function versionPlugin(options = {}) {
  return {
    name: 'vite-plugin-version', // 插件名,必须唯一
    // 钩子函数往这里加
  }
}

这就完事了?对!一个合法的Vite插件就这么简单!

Step 2:获取版本信息

我们要拿到Git commit ID和构建时间:

import { execSync } from 'child_process';

export default function versionPlugin(options = {}) {
  // 获取Git commit ID
  let commitId = 'unknown';
  try {
    commitId = execSync('git rev-parse --short HEAD').toString().trim();
  } catch {
    console.log('⚠️ 不是Git仓库,使用unknown版本');
  }
  
  // 记录构建时间
  const buildTime = new Date().toLocaleString('zh-CN');
  
  return {
    name: 'vite-plugin-version',
    // 钩子函数...
  }
}

知识点execSync可以执行系统命令,但别忘了try-catch,不是所有项目都用Git!

Step 3:注入到HTML

这是最核心的部分,用 transformIndexHtml 钩子:

transformIndexHtml(html) {
  // 要注入的内容
  const injectContent = `
    <!-- 版本信息-自动注入 -->
    <meta name="app_version" content="${commitId}" />
    <meta name="app_build_time" content="${buildTime}" />
    <script>
      window.app_version = "${commitId}";
      window.app_build_time = "${buildTime}";
    </script>
  `;
  
  // 插入到</head>前面
  return html.replace('</head>', injectContent + '\n</head>');
}

核心技巧:字符串替换是最简单可靠的注入方式,不用怕出错!

Step 4:添加编译时常量)

想在Vue/React组件里直接用?用 config 钩子:

config() {
  return {
    define: {
      __APP_VERSION__: JSON.stringify(commitId),
      __BUILD_TIME__: JSON.stringify(buildTime)
    }
  };
}

然后在组件里:

<script setup>
console.log('当前版本:', __APP_VERSION__)
console.log('构建时间:', __BUILD_TIME__)
</script>

Step 5:友好的控制台提示

closeBundle 在构建完成后给点反馈:

closeBundle() {
  console.log(`
    ✅ 版本注入成功!
    版本号:${commitId}
    构建时间:${buildTime}
    访问方式:window.app_version 或 __APP_VERSION__
  `);
}

三、完整代码:拿去就能用!

把上面拼起来,再加点配置选项:

import { execSync } from 'child_process';

export default function versionPlugin(options = {}) {
  // 可配置的键名
  const VERSION_KEY = options.versionKey || 'app_version';
  const TIME_KEY = options.timeKey || 'app_build_time';
  const INJECT_META = options.injectMeta !== false;
  
  // 获取版本信息
  let commitId = 'unknown';
  let buildTime = new Date().toLocaleString('zh-CN');
  
  try {
    commitId = execSync('git rev-parse --short HEAD').toString().trim();
  } catch {}
  
  return {
    name: 'vite-plugin-version',
    
    // 注入编译时常量
    config() {
      return {
        define: {
          [`__${VERSION_KEY.toUpperCase()}__`]: JSON.stringify(commitId),
          [`__${TIME_KEY.toUpperCase()}__`]: JSON.stringify(buildTime)
        }
      };
    },
    
    // 注入HTML
    transformIndexHtml(html) {
      let injectContent = '';
      
      if (INJECT_META) {
        injectContent += `
    <meta name="${VERSION_KEY}" content="${commitId}" />
    <meta name="${TIME_KEY}" content="${buildTime}" />`;
      }
      
      injectContent += `
    <script>
      window.${VERSION_KEY} = "${commitId}";
      window.${TIME_KEY} = "${buildTime}";
    </script>`;
      
      return html.replace('</head>', injectContent + '\n</head>');
    },
    
    // 构建完成提示
    closeBundle() {
      console.log(`\n✅ [版本插件] 构建成功 v-${commitId}`);
    }
  };
}

四、如何使用?

// vite.config.js
import versionPlugin from './vite-plugin-version';

export default {
  plugins: [
    versionPlugin({
      versionKey: 'my_app_version',  // 自定义版本key
      injectMeta: true                // 是否注入meta标签
    })
  ]
}

运行 npm run build,你的HTML就会自动带上版本信息:

<head>
  <meta name="my_app_version" content="a3b9c2d" />
  <meta name="app_build_time" content="2026/3/15 14:30:22" />
  <script>
    window.my_app_version = "a3b9c2d";
    window.app_build_time = "2026/3/15 14:30:22";
  </script>
</head>

五、还能怎么玩?

学会了基础,你可以:

  1. 注入更多信息:分支名、构建环境、打包时间
  2. 生成版本文件:用generateBundle钩子输出version.json
  3. 版本对比:开发环境提醒版本更新
  4. 自动标签:构建成功自动打Git tag
// 扩展:生成version.json
generateBundle() {
  this.emitFile({
    type: 'asset',
    fileName: 'version.json',
    source: JSON.stringify({
      version: commitId,
      buildTime: buildTime,
      env: process.env.NODE_ENV
    })
  });
}

六、总结:学Vite插件值不值?

值!而且很值!

  • 学习成本低:一个对象+几个钩子函数
  • 应用场景广:任何自动化需求都能用插件解决
  • 提升工程化能力:从「用工具」到「造工具」的跨越

记住核心三要素:

  1. name:插件身份证
  2. 钩子函数:在正确的时间做正确的事
  3. 配置选项:让插件更灵活

最后留个作业:给这个插件加个功能,打包时如果版本号没变就警告,防止忘记更新版本。评论区等你答案!


关注我的公众号 大前端历险记 获取更多前端姿势!

模块化与组件化:90%的前端开发者都没搞懂的本质区别

一位刚入职不久的网友留言问我:"我们一直在说模块化开发、组件化设计,这两个概念到底有什么区别?我感觉它们不就是把代码拆分开来吗?"

今天,我想从自己的角度,聊聊我对这两个概念的深度理解。

什么是模块化?

模块化是代码组织层面的哲学,关注的是"职责边界"。

简单来说,模块化就是把一个复杂的系统,按照功能职责拆分成独立的文件或代码单元。每个模块负责完成特定的功能,对外暴露必要的接口,隐藏内部实现细节。

看一个最朴素的例子:

// math.js - 一个纯粹的数学计算模块
export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

// 内部实现细节,不对外暴露
function validateNumber(num) {
  if (typeof num !== 'number') {
    throw new Error('参数必须是数字');
  }
}
// app.js - 使用模块
import { add, multiply } from './math.js';

console.log(add(5, 3)); // 8

模块化的核心特征是:

  1. 高内聚:相关功能紧密放在一起
  2. 低耦合:模块之间通过明确定义的接口通信
  3. 封装性:隐藏内部实现细节
  4. 关注点分离:每个模块解决一个特定问题

在ES6之前,我们通过IIFE实现模块化,现在有了原生的ES Module,模块化已经成为JavaScript的基础设施。

什么是组件化?

组件化是UI构建层面的哲学,关注的是"呈现与交互"。

组件化将用户界面拆分成独立的、可复用的部件。每个组件封装了自己的结构(HTML)、样式(CSS)和行为(JavaScript),可以被组合成更复杂的界面。

看一个React组件的例子:

// Button.jsx - 一个UI组件
import React from 'react';
import './Button.css'; // 组件自己的样式

const Button = ({ variant = 'primary', size = 'medium', children, onClick }) => {
  // 内部状态管理
  const [isHovered, setIsHovered] = useState(false);
  
  // 内部逻辑处理
  const handleMouseEnter = () => setIsHovered(true);
  const handleMouseLeave = () => setIsHovered(false);
  
  return (
    <button
      className={`btn btn-${variant} btn-${size} ${isHovered ? 'hovered' : ''}`}
      onClick={onClick}
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
    >
      {children}
    </button>
  );
};

export default Button;
// App.jsx - 组合组件
import React from 'react';
import Button from './Button';

const App = () => {
  return (
    <div>
      <Button variant="primary" size="large" onClick={() => alert('点击')}>
        主要按钮
      </Button>
      <Button variant="secondary" size="small">
        次要按钮
      </Button>
    </div>
  );
};

组件化的核心特征是:

  1. 可组合性:组件可以嵌套组合成复杂界面
  2. 可复用性:同一组件可在不同地方重复使用
  3. 自包含:组件包含自身所需的资源
  4. 接口明确:通过props定义清晰的输入输出

本质区别:一个思想实验

假设我们要开发一个电商网站的用户中心页面。

模块化视角

  • 把用户相关的API请求封装成 userAPI.js 模块
  • 把价格格式化功能封装成 priceFormatter.js 模块
  • 把购物车计算逻辑封装成 cartCalculator.js 模块
  • 这些模块可以在任何地方使用,甚至不在浏览器环境

组件化视角

  • 把用户头像区域做成 UserAvatar 组件
  • 把订单列表做成 OrderList 组件
  • 把商品卡片做成 ProductCard 组件
  • 这些组件组合在一起形成完整的页面

现在,最关键的区别来了:

模块化解决的是"如何组织代码"的问题,组件化解决的是"如何构建界面"的问题。

更本质地说:

  • 模块化的最小单位是函数或文件,关注的是逻辑、数据、功能的封装
  • 组件化的最小单位是UI元素,关注的是视图、交互、样式的封装

但最深刻的认识是:模块化是组件化的基础,组件化是模块化在UI层的具体体现。

实战中的混淆与重构

让我用一个真实的重构案例来说明这两者的区别。

重构前(混淆概念)

// UserProfile.jsx - 一个"组件",但实际上什么都做
import React, { useState, useEffect } from 'react';

const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [orders, setOrders] = useState([]);
  
  // 直接在这里写API调用
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser);
      
    fetch(`/api/users/${userId}/orders`)
      .then(res => res.json())
      .then(setOrders);
  }, [userId]);
  
  // 直接在这里写复杂的数据处理
  const totalSpent = orders.reduce((sum, order) => {
    // 各种复杂的价格计算逻辑
    return sum + order.amount;
  }, 0);
  
  // 格式化函数直接写在组件里
  const formatDate = (dateStr) => {
    const date = new Date(dateStr);
    return `${date.getFullYear()}-${date.getMonth()+1}-${date.getDate()}`;
  };
  
  return (
    <div>
      <h1>{user?.name}</h1>
      <p>总消费: ¥{totalSpent}</p>
      <div>
        {orders.map(order => (
          <div key={order.id}>
            <span>{formatDate(order.createdAt)}</span>
            <span>¥{order.amount}</span>
          </div>
        ))}
      </div>
    </div>
  );
};

这个"组件"的问题在于:它混淆了组件化和模块化的边界,导致:

  • 组件臃肿难以维护
  • 业务逻辑无法复用
  • 难以测试
  • 代码重复

重构后(明确职责)

// modules/userAPI.js - 纯模块,处理用户数据获取
export const fetchUser = async (userId) => {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
};

export const fetchUserOrders = async (userId) => {
  const response = await fetch(`/api/users/${userId}/orders`);
  return response.json();
};
// modules/orderCalculator.js - 纯模块,处理订单计算逻辑
export const calculateTotalSpent = (orders) => {
  return orders.reduce((sum, order) => sum + order.amount, 0);
};

export const formatCurrency = (amount) => {
  return new Intl.NumberFormat('zh-CN', { 
    style: 'currency', 
    currency: 'CNY' 
  }).format(amount);
};
// modules/dateFormatter.js - 纯模块,处理日期格式化
export const formatDate = (dateStr, format = 'simple') => {
  const date = new Date(dateStr);
  if (format === 'simple') {
    return `${date.getFullYear()}-${date.getMonth()+1}-${date.getDate()}`;
  }
  // 其他格式...
  return date.toLocaleDateString();
};
// components/OrderItem.jsx - 纯粹的展示组件
const OrderItem = ({ order }) => {
  return (
    <div className="order-item">
      <span>{formatDate(order.createdAt)}</span>
      <span>{formatCurrency(order.amount)}</span>
    </div>
  );
};
// components/UserProfile.jsx - 组合组件,只负责组合和状态管理
import React, { useState, useEffect } from 'react';
import { fetchUser, fetchUserOrders } from '../modules/userAPI';
import { calculateTotalSpent, formatCurrency } from '../modules/orderCalculator';
import OrderItem from './OrderItem';

const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [orders, setOrders] = useState([]);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);
    fetchUserOrders(userId).then(setOrders);
  }, [userId]);
  
  const totalSpent = calculateTotalSpent(orders);
  
  return (
    <div className="user-profile">
      <h1>{user?.name}</h1>
      <p className="total-spent">
        总消费: {formatCurrency(totalSpent)}
      </p>
      <div className="order-list">
        {orders.map(order => (
          <OrderItem key={order.id} order={order} />
        ))}
      </div>
    </div>
  );
};

重构后的代码清晰地体现了:

  • 模块负责数据获取、计算逻辑、格式化等非UI相关的功能
  • 组件负责UI渲染和交互逻辑
  • 模块可以在任何地方使用(甚至在Node.js环境)
  • 组件专注于界面呈现,通过props接收数据和回调

总结

回到最初的问题:模块化和组件化的本质区别是什么?

模块化是一种代码组织思想,它让我们能够将复杂的系统分解成独立的、可维护的代码单元。它关注的是功能的内聚和依赖的管理,解决的是"代码怎么写才不乱"的问题。

组件化是一种UI构建思想,它让我们能够将界面分解成独立的、可复用的部件。它关注的是视图的拆分和组合,解决的是"界面怎么搭才灵活"的问题。

当你能清晰区分这两个概念,你的代码会变得更清晰、更可维护、更容易测试。

互动

看完这篇文章,你对模块化和组件化有了新的认识吗?欢迎在评论区分享你的想法。

如果你觉得这篇文章对你有帮助,点赞、收藏、转发给更多需要的朋友。我们下期再见!

你的项目真的需要SSR吗?还是只是你的简历需要?

技术选型不是为了简历好看,是为了解决问题

上周和一个前同事约饭,他现在在一家创业公司带前端团队。

聊到一半他突然问我:“我们准备把项目重构成 Next.js,上服务端渲染,你觉得怎么样?”

我放下筷子:“你们现在遇到什么坑了要重构?”

他挠挠头:“坑倒是没有……就是现在出去面试,人家都问有没有 Next.js 经验,我们不用会不会显得技术栈太旧了。”

“那你们产品要 SEO 吗?用户网络环境咋样?”

“用户都在一二线城市,网速挺好的。SEO 的话……产品得登录才能用,搜索引擎也爬不到。”

我看着他,不知道该说什么。

又一个被技术流行绑架的老同事。


一、SSR不是银弹,它是一把双刃剑

这两年,Next.js、Nuxt.js确实火得不行。

打开技术社区,满屏都是“从CSR迁移到SSR后,FCP提升50%”、“SSR才是前端正确的打开方式”。

但你有没有发现,很少有人告诉你:为了这点性能提升,你和你团队接下来半年要填多少坑。

1. 服务器账单:以前不要钱,现在要钱了

纯静态页面放OSS上,流量不大时一年可能就几百块。

上了SSR呢?

  • 你需要一台服务器,或者云函数实例
  • 你需要考虑并发,一台扛不住要上负载均衡
  • 你还要担心服务器宕机,得配监控、告警、容灾

我一个朋友的项目,上了SSR后第一个月,云账单从300涨到了3000。老板拿着账单问他:“用户感受到变快了吗?多出来的钱能从收入里赚回来吗?”

他答不上来。

更扎心的是,后来发现大部分用户都是从首页跳详情页,首屏优化的收益根本覆盖不了成本

2. 复杂度转移:以前是纯前端问题,现在是全栈问题

CSR项目,前端只管写页面,接口调不通那是后端的事。

SSR项目呢?

// 以前写页面,岁月静好
function UserProfile({ userId }) {
  const [user, setUser] = useState(null)
  
  useEffect(() => {
    fetch(`/api/user/${userId}`).then(setUser)
  }, [userId])
  
  return <div>{user?.name}</div>
}
// 上了SSR之后,噩梦开始
export async function getServerSideProps({ params }) {
  try {
    // 要处理接口超时
    const controller = new AbortController()
    const timeoutId = setTimeout(() => controller.abort(), 3000)
    
    const res = await fetch(`http://internal-api/user/${params.id}`, {
      signal: controller.signal
    }).catch(() => null)
    
    clearTimeout(timeoutId)
    
    if (!res || !res.ok) {
      // 降级策略怎么写?
      return { props: { user: null, fallback: true } }
    }
    
    const user = await res.json()
    return { props: { user } }
  } catch (error) {
    // 服务端报错,用户看到什么?
    return { notFound: true }
  }
}

function UserProfile({ user, fallback }) {
  // 客户端还要再校验一遍状态
  const [mounted, setMounted] = useState(false)
  
  useEffect(() => {
    setMounted(true)
  }, [])
  
  if (!mounted) {
    return <div>加载中...</div> // 防止hydrate报错
  }
  
  return <div>{user?.name}</div>
}

数据在服务端取,取不到怎么办?页面直接500还是降级成CSR? 服务端取的接口超时了,是要等待还是超时返回? 服务端和客户端的状态怎么对齐?一不小心就 hydrate 报错

我一个同事转做Next.js项目后发朋友圈:“自从上了SSR,我不仅要写React,还要会配Nginx、懂PM2、会分析内存泄漏。工资没涨,责任翻倍。”

底下点赞的全是前端。

3. 开发体验的割裂感

以前写CSR,window、document随便用,反正都在浏览器里。

写了SSR之后:

// 以前一行代码
const width = window.innerWidth
const token = localStorage.getItem('token')
const height = document.getElementById('app').offsetHeight
// 现在
const width = typeof window !== 'undefined' ? window.innerWidth : 1024
const token = typeof window !== 'undefined' 
  ? localStorage.getItem('token') 
  : null
const [height, setHeight] = useState(0)

useEffect(() => {
  setHeight(document.getElementById('app')?.offsetHeight || 0)
}, [])
  • 第三方库如果不兼容服务端渲染,要动态导入
  • localStorage、sessionStorage都不能直接用
  • 路由跳转要小心,服务端没有history API

原本简单的逻辑,现在要写一堆防御代码。你在给代码做安检,但业务逻辑一点没变复杂。


二、你真的需要SSR吗?问自己三个扎心的问题

每次有人问我该不该上SSR,我都让他先回答三个问题。回答完,80%的人自己就放弃了。

问题一:你的产品靠搜索引擎吃饭吗?

这是最硬性的指标,也是最容易被拿来当借口的。

如果你的产品是:

  • 内容型网站(博客、新闻、官网)
  • 电商网站(需要被搜索引擎收录商品页)

那SSR确实有必要。因为爬虫可能不执行JS,或者执行不完整。

但如果你的产品是:

  • 需要登录的后台管理系统
  • 工具类、游戏类H5
  • B端SaaS应用
  • 社区类App的H5版(用户得先登录)

搜索引擎根本爬不到,SEO就是伪需求。

别拿SEO当借口,你只是想让简历里多一行Next.js。

问题二:你的首屏速度真的慢到不能忍了吗?

很多时候,我们觉得首屏慢,其实不是因为CSR不行,是代码写得太糙。

我见过一个“慢”的项目,分析下来:

// 问题代码示例
function App() {
  const [data, setData] = useState(null)
  const [user, setUser] = useState(null)
  const [config, setConfig] = useState(null)
  
  useEffect(() => {
    // 串行调用,一个等一个
    fetch('/api/data').then(res => res.json()).then(data => {
      setData(data)
      return fetch('/api/user')
    }).then(res => res.json()).then(user => {
      setUser(user)
      return fetch('/api/config')
    }).then(res => res.json()).then(setConfig)
    
    // 图片没处理
    new Image().src = 'https://example.com/big-banner.png'
    
    // 第三方脚本同步加载
    const script = document.createElement('script')
    script.src = 'https://analytics.com/sdk.js'
    document.head.appendChild(script)
  }, [])
  
  return <div>...</div>
}

这些问题,优化代码比换架构性价比高得多

我去年优化过一个Vue2项目,纯CSR,首屏从3.2秒优化到1.1秒,只做了四件事:

// 优化后
useEffect(() => {
  // 1. 并行调用
  Promise.all([
    fetch('/api/data'),
    fetch('/api/user'),
    fetch('/api/config')
  ]).then(...)
  
  // 2. 图片转WebP + 懒加载
  // 3. 第三方脚本异步
  const script = document.createElement('script')
  script.async = true
  script.src = 'https://analytics.com/sdk.js'
  
  // 4. 路由懒加载
  const List = lazy(() => import('./pages/List'))
}, [])

没动架构,没重构,没加班。

问题三:你的团队准备好了吗?

这是最容易被忽略的,也是上线后最痛苦的。

上SSR意味着你的前端团队要开始写服务端代码:

  • 有人写过Node.js吗?
  • 有人配过Nginx吗?
  • 有人处理过内存泄漏吗?
# 线上出问题了,你能处理吗?
curl -X POST https://your-site.com/api/user -H "Content-Type: application/json" -d '{"id":123}'

# 如果返回 502
# 是Node进程挂了?Nginx配置错了?接口超时了?

# 登录服务器
ssh user@your-server
pm2 logs
df -h # 磁盘满了?
free -m # 内存泄漏?
top # CPU爆了?
  • 线上出问题了,有人能在凌晨两点爬起来回滚吗?

如果答案都是“没有”,那SSR上线的那天,就是团队噩梦的开始。

技术选型不仅要看技术好不好,还要看团队接不接得住。


三、那些比SSR更香的选择

如果你确实有性能痛点,但又不是非要SSR,其实有很多折中方案。

方案一:静态站点生成(SSG)

如果你的内容是静态的,或者更新不频繁,SSG是完美的选择。

// next.config.js
module.exports = {
  // 构建时生成HTML
  exportPathMap: async function() {
    return {
      '/': { page: '/' },
      '/about': { page: '/about' },
      '/blog/1': { page: '/blog/[id]', query: { id: '1' } },
    }
  }
}
  • 构建时生成HTML,部署在CDN上
  • 首屏速度极快,SEO友好
  • 没有服务器成本,没有运维负担

Next.js、Nuxt.js、VitePress都支持。既有了SSR的首屏优势,又保留了CSR的简单部署。

方案二:静态部署 + 客户端渲染

大部分场景,这才是最优解。

// index.html
<!DOCTYPE html>
<html>
<head>
  <!-- 骨架屏,让用户看到东西 -->
  <style>
    .skeleton { background: #f0f0f0; height: 20px; margin: 10px; }
  </style>
</head>
<body>
  <div id="root">
    <div class="skeleton"></div>
    <div class="skeleton"></div>
    <div class="skeleton"></div>
  </div>
  
  <!-- 资源放CDN,并行加载 -->
  <link rel="preconnect" href="https://api.example.com">
  <script src="https://cdn.example.com/react.js" async></script>
  <script src="https://cdn.example.com/app.js" async></script>
</body>
</html>
  • HTML放CDN,全球加速
  • 接口走API网关,BFF层做聚合
  • 配合预加载、懒加载、骨架屏,体验一点都不差

我现在的项目就是这种方案:React + Vite,打包后放OSS,CDN加速。首屏1.2秒,月PV百万,服务器成本主要是BFF层的几个云函数,加起来不到500块。

够用就好,别为了炫技给自己挖坑。

方案三:部分页面SSR,大部分页面CSR

如果你确实有少数页面需要SEO(比如官网、 landing page),其他页面需要登录。

// next.config.js
module.exports = {
  // 只有这些页面走SSR
  pageExtensions: ['ssr.js', 'page.js'],
  
  async rewrites() {
    return [
      // landing page走SSR
      {
        source: '/',
        destination: '/landing.ssr',
      },
      // 其他页面走静态CSR
      {
        source: '/app/:path*',
        destination: '/app.html',
      }
    ]
  }
}

那可以只把这几个页面抽出来做SSR,剩下的保持CSR。

Next.js支持多页应用模式,Vue也有混合渲染方案。没必要为了10%的页面,让90%的页面承担复杂度。


四、写在最后:技术选型不是为了简历好看

写这篇文章,不是为了否定SSR。

SSR是好技术,Next.js是好框架。我在合适的项目里也用它,确实能解决问题。

但我不喜欢一种风气:明明是个简单的H5活动页,非要上Next.js;明明是个后台管理系统,非要搞服务端渲染;明明首屏已经1.5秒了,非要重构到1.2秒。

为了那0.5秒的优化,搭进去团队半年的维护成本,值吗?

技术选型的标准,不是“别人都用”,也不是“大厂在用”,而是:

  • 能解决我们现在的痛点吗?
  • 团队能驾驭吗?
  • 维护成本扛得住吗?
  • 换来的收益对得起付出的代价吗?

别让技术选型,变成一场给简历镀金的表演。


最后想问问大家: 你们见过哪些“没必要上SSR但硬上”的项目?踩过哪些坑?欢迎在评论区互相伤害。

如果你也看不惯那些为了炫技而复杂化的技术选型,欢迎关注我,一起聊聊真实的前端。

❌