从零开始构建一个现代化前端项目:一个微信小程序老油条的React开荒记

编译器不爱我
2025-12-12 20:48
阅读 755

上周五晚上十点半,北京国贸地铁站的人流终于稀疏下来。我拖着疲惫的身体走出公司大楼,耳机里放着Lo-fi beats,脑子里还在盘算着明天上线的新活动页面——是的,又被产品经理临时加需求了。这次要做的不是小程序,而是一个独立的 React Web 应用,说是为了“多端覆盖”、“生态协同”。行吧,反正我们组现在既要维护小程序,又要搞 H5,还得支援 App 内嵌页,早就成了“全栈缝合怪”。

我是腾讯某事业群干了三年客户端的老兵,主要搞微信小程序业务。说实话,以前我对“纯前端”一直有点偏见——不就是写个页面、调个接口、加点动画?直到去年双11,我们小程序因为承载不了突发流量,临时拉了个 H5 页面做兜底,结果那个页面加载慢得像在用 2G 网络,用户跳出率直接飙到 70%。老板当场拍桌子:“这体验怎么拿得出手?” 那一刻我才意识到:前端,真不是随便糊弄就行的。

于是今年开年,我主动请缨牵头这个新项目,想从头到尾搭一套“现代化”的前端工程体系。别笑,我知道这个词已经被用烂了,但这次我真的不想再用“create-react-app 跑起来就完事”那种糙快猛的方式了。毕竟,在腾讯,代码不仅是功能实现,更是用户体验的基石——尤其当你面对的是上亿用户的时候。

为什么不能直接 npx create-react-app

相信很多兄弟和我一样,一开始学 React 就是靠 CRA(Create React App)入门的。它确实香:一行命令,开发服务器、热更新、ESLint 全配好了。但一旦你开始认真对待性能、可维护性和 CI/CD,CRA 的黑盒就变成枷锁了。

比如我们项目需要:

  • 支持微前端架构(未来可能被集成进主站)
  • 按需加载 + 资源预加载
  • 自动化生成 changelog 和版本管理
  • 严格的 TypeScript 类型校验
  • 所有静态资源走 CDN,并自动打 hash

这些需求,CRA 要么做不到,要么得 eject 出一堆 webpack 配置然后自己维护——那还不如从零开始。

所以,我决定“手搓”一个脚手架。别慌,不是真的从零写打包器,而是用社区成熟的工具链组合出最适合我们团队的方案。

工具链选型:现代前端的“瑞士军刀”

经过和组里几个前端老哥(对,虽然我是客户端出身,但现在也得自称前端了)反复 battle,我们最终敲定了这套组合:

工具 用途 为啥选它
Vite 构建工具 启动快如闪电,HMR 秒级响应,原生支持 TS/JSX
React 18 + TypeScript 核心框架 Concurrent Mode 对复杂交互更友好,TS 提升协作效率
Tailwind CSS 样式方案 原子化 CSS,避免样式冲突,设计系统一致性高
ESLint + Prettier + Husky 代码质量 提交前自动 lint/format,避免 Code Review 时互相伤害
Vitest 单元测试 跑得比 Jest 快十倍,和 Vite 共享插件生态
Storybook 组件文档 UI 组件独立开发调试,设计走查不再扯皮

这里重点夸一下 Vite。以前用 webpack 开发时,改一行代码等十几秒刷新,简直想砸键盘。Vite 利用 ES modules 原生支持,启动速度从“泡杯咖啡的时间”缩短到“眨个眼的功夫”。上周我本地跑项目,同事路过看了一眼终端,惊呼:“你这项目刚 clone 下来?怎么已经跑起来了?”

npm init 到可交付产物

第一步:初始化项目骨架

npm create vite@latest my-awesome-project -- --template react-ts
cd my-awesome-project
npm install

别小看这三行,背后是尤雨溪团队对开发者体验的极致打磨。项目结构清晰:

my-awesome-project/
├── public/          # 静态资源(不会被处理)
├── src/
│   ├── assets/      # 图片、字体等
│   ├── components/  # 可复用组件
│   ├── pages/       # 页面级组件
│   ├── hooks/       # 自定义 Hooks
│   ├── utils/       # 工具函数
│   ├── App.tsx
│   └── main.tsx
├── index.html       # 唯一 HTML 入口
├── vite.config.ts   # 配置文件
└── package.json

第二步:配置 Vite —— 让它更“生产友好”

默认配置适合开发,但上线前得加点料。我们在 vite.config.ts 里做了这些调整:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { visualizer } from 'rollup-plugin-visualizer' // 分析包体积

export default defineConfig({
  plugins: [
    react(),
    // 仅在分析时开启
    process.env.ANALYZE && visualizer({ open: true })
  ],
  build: {
    // 代码分割策略:每个路由单独 chunk
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            // 将大型依赖单独拆包
            if (id.includes('lodash') || id.includes('moment')) {
              return 'vendor-heavy'
            }
            return 'vendor'
          }
        }
      }
    },
    // 资源文件名带 hash,利于 CDN 缓存
    assetsDir: 'assets',
    sourcemap: true // 方便线上错误定位
  },
  server: {
    // 本地代理解决跨域
    proxy: {
      '/api': {
        target: 'https://test-api.example.com',
        changeOrigin: true
      }
    }
  }
})

这里有个坑:manualChunks 如果写得太粗暴(比如所有 node_modules 打成一个包),会导致首屏加载变慢。我们通过分析发现 lodash 和 moment 特别大,就单独拆出来,配合 <link rel="prefetch"> 预加载,首屏 FCP(First Contentful Paint)从 2.4s 降到 1.1s。

第三步:TypeScript + ESLint —— 团队协作的“防呆设计”

在腾讯,一个项目往往多人并行开发。没有强类型约束,光靠口头约定“这个字段是 string”,三天后肯定有人传 number。所以我们启用了严格模式:

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "jsx": "react-jsx"
  }
}

配合 ESLint 规则,连 console.log 都不允许提交(除非加 // eslint-disable-next-line no-console 注释)。起初后端转前端的同事抱怨:“写个 demo 还要 type interface,烦死了!” 但两周后,当他在重构时不小心删了一个必传 prop,TS 直接报错阻止了 bug 上线,他默默给我点了杯奶茶。

第四步:Tailwind CSS —— 告别“样式战争”

以前我们用 CSS Modules,但设计师给的间距、颜色总和代码对不上。现在 Tailwind 让我们直接在 JSX 里写 className="p-4 bg-blue-500 rounded-lg",所见即所得。配合 tailwind.config.js 定制主题:

// tailwind.config.js
module.exports = {
  content: ['./index.html', './src/**/*.{js,jsx,ts,tsx}'],
  theme: {
    extend: {
      colors: {
        primary: '#1890ff', // 腾讯蓝
      },
      spacing: {
        'screen-1/2': '50vh'
      }
    }
  }
}

UI 组件库我们没选 Ant Design(太重),而是基于 Tailwind 自建了一套轻量级组件,比如按钮:

// Button.tsx
type ButtonProps = {
  variant?: 'primary' | 'secondary';
  size?: 'sm' | 'md' | 'lg';
  children: React.ReactNode;
  onClick?: () => void;
};

export const Button = ({ 
  variant = 'primary', 
  size = 'md', 
  children, 
  ...props 
}: ButtonProps) => {
  const baseClasses = 'rounded font-medium transition-colors';
  
  const variantClasses = {
    primary: 'bg-primary text-white hover:bg-primary/90',
    secondary: 'bg-gray-100 text-gray-800 hover:bg-gray-200'
  };
  
  const sizeClasses = {
    sm: 'px-3 py-1 text-sm',
    md: 'px-4 py-2',
    lg: 'px-6 py-3 text-lg'
  };

  return (
    <button
      className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`}
      {...props}
    >
      {children}
    </button>
  );
};

这样既保证一致性,又避免引入整个 UI 库的体积负担。

性能优化:不是炫技,是底线

上线前一周,测试同学拿着 Lighthouse 报告来找我:“首屏性能只有 60 分,老板说至少要 85。” 当时我内心 OS:你们知道为了兼容 IE11 我掉了多少头发吗?(开玩笑,现在谁还管 IE)

我们做了几件事:

  1. 懒加载路由:用 React.lazy + Suspense
const HomePage = lazy(() => import('./pages/Home'));
const AboutPage = lazy(() => import('./pages/About'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/about" element={<AboutPage />} />
      </Routes>
    </Suspense>
  );
}
  1. 图片优化:所有图片转 WebP,配合 <picture> 标签降级
  2. 关键 CSS 内联:用 critters 插件自动提取首屏 CSS 内联到 HTML
  3. 移除 console:生产构建时用 terser 插件自动删除

最终 Lighthouse 分数冲到 92,测试同学看我的眼神都温柔了。

最后一点感悟

从零搭建项目的过程,其实是在回答一个问题:“我们到底想要什么样的开发体验?” 在腾讯,我们常说“用户为本,科技向善”,落实到代码层面,就是让开发者写得爽,让用户用得爽。

以前我觉得前端就是“切图仔”,现在才明白,现代化前端工程是一门系统艺术——工具链是骨架,性能是肌肉,用户体验才是灵魂。

现在这个项目已经稳定运行两个月,日活 50w+,0 P0 事故。上周产品又来找我:“下个需求能不能周三上线?” 我笑着回他:“行啊,只要你敢提,我就敢肝。” —— 毕竟,凌晨两点的北京,代码跑起来的感觉,真的很爽。

(完)


PS:如果你也在从零搭建项目,别怕折腾。工具会变,框架会换,但解决问题的思路永远通用。共勉!

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝