引言:单页应用的挑战与机遇

单页应用(Single Page Application, SPA)已经成为现代Web开发的主流架构。与传统的多页应用(MPA)不同,SPA通过JavaScript动态更新页面内容,无需每次交互都重新加载整个页面。这种架构带来了更快的响应速度和更流畅的用户体验,但也引入了一系列独特的问题。

用户在使用SPA时最常遇到的问题包括:

  • 首次加载缓慢:初始bundle体积过大
  • SEO不友好:搜索引擎难以抓取动态内容
  • 状态管理复杂:跨组件数据共享困难
  • 路由管理混乱:浏览器历史记录和导航问题
  • 性能瓶颈:内存泄漏和渲染性能下降

本文将深入探讨如何通过”情节类型SPA”(我们将其理解为基于特定架构模式和最佳实践的SPA开发方法)来解决这些问题,并显著提升用户体验。

1. 代码分割与懒加载:解决首次加载缓慢问题

问题分析

首次加载缓慢是SPA最致命的问题之一。当用户首次访问时,需要下载整个应用的JavaScript bundle,这可能达到几MB甚至几十MB。

解决方案:动态导入与路由级代码分割

现代前端框架都支持代码分割。以下是React和Vue的实现示例:

React实现(使用React.lazy和Suspense)

// 传统方式:一次性导入所有组件
// import Home from './Home';
// import About from './About';
// import Contact from './Contact';

// 情节类型SPA方式:动态导入
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

// 动态导入组件,每个路由对应一个chunk
const Home = lazy(() => import(/* webpackChunkName: "home" */ './Home'));
const About = lazy(() => import(/* webpackChunkName: "about" */ './About'));
const Contact = lazy(() => import(/* webpackChunkName: "contact" */ './Contact'));

// 加载状态组件
const LoadingSpinner = () => (
  <div className="loading-container">
    <div className="spinner"></div>
    <p>正在加载页面...</p>
  </div>
);

function App() {
  return (
    <Router>
      {/* Suspense包裹路由,提供加载状态 */}
      <Suspense fallback={<LoadingSpinner />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/contact" element={<Contact />} />
        </Routes>
      </Suspense>
    </Router>
  );
}

export default App;

Vue实现(使用动态导入)

// router/index.js
import { createRouter, createWebHistory } from 'vue-router';

const routes = [
  {
    path: '/',
    // 动态导入,webpack自动代码分割
    component: () => import(/* webpackChunkName: "home" */ '@/views/Home.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/about',
    component: () => import(/* webpackChunkName: "about" */ '@/views/About.vue')
  },
  {
    path: '/products/:id',
    component: () => import(/* webpackChunkName: "product" */ '@/views/ProductDetail.vue'),
    // 路由级懒加载配合预加载策略
    meta: { preload: true }
  }
];

const router = createRouter({
  history: createWebHistory(),
  routes,
  // 滚动行为优化
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition;
    } else {
      return { top: 0, behavior: 'smooth' }
    }
  }
});

// 路由守卫:权限控制和预加载
router.beforeEach((to, from, next) => {
  // 权限检查
  if (to.meta.requiresAuth && !isAuthenticated()) {
    next('/login');
    return;
  }
  
  // 智能预加载:当网络空闲时预加载可能访问的页面
  if ('requestIdleCallback' in window) {
    requestIdleCallback(() => {
      if (to.meta.preload) {
        // 预加载相关资源
        preloadImportantResources();
      }
    });
  }
  
  next();
});

export default router;

高级优化:预加载策略

// 预加载管理器
class PreloadManager {
  constructor() {
    this.preloaded = new Set();
  }

  // 链接预加载:当用户hover时预加载
  preloadOnHover(routePath) {
    const link = document.createElement('link');
    link.rel = 'prefetch';
    link.href = `/static/js/${routePath}.chunk.js`;
    document.head.appendChild(link);
  }

  // 空闲时预加载
  preloadOnIdle(routes) {
    if ('requestIdleCallback' in window) {
      requestIdleCallback(() => {
        routes.forEach(route => {
          if (!this.preloaded.has(route)) {
            import(/* webpackPrefetch: true */ `./${route}.js`);
            this.preloaded.add(route);
          }
        });
      });
    }
  }
}

// 使用示例
const preloadManager = new PreloadManager();

// 在用户hover导航链接时预加载
document.querySelectorAll('a.nav-link').forEach(link => {
  link.addEventListener('mouseenter', (e) => {
    const route = e.target.dataset.route;
    preloadManager.preloadOnHover(route);
  });
});

效果对比

  • 优化前:初始bundle 3MB,加载时间 5秒
  • 优化后:初始bundle 300KB,加载时间 800ms,后续页面几乎瞬开

2. 服务端渲染(SSR)与静态生成(SSG):解决SEO问题

问题分析

SPA的HTML内容是通过JavaScript动态生成的,搜索引擎爬虫可能无法正确索引内容。

解决方案:Next.js/Nuxt.js实现SSR/SSG

Next.js实现SSR和SSG

// pages/products/[id].js
import { useRouter } from 'next/router';
import Head from 'next/head';

// 1. 静态生成(SSG)- 构建时生成HTML
export async function getStaticPaths() {
  // 预生成热门产品页面
  const products = await fetchPopularProducts();
  const paths = products.map(product => ({
    params: { id: product.id.toString() }
  }));
  
  return {
    paths,
    fallback: 'blocking' // 新产品按需生成
  };
}

export async function getStaticProps({ params }) {
  // 构建时获取数据
  const product = await fetchProductById(params.id);
  
  return {
    props: {
      product,
      timestamp: new Date().toISOString()
    },
    // 重新生成时间(秒)
    revalidate: 3600 // 每小时重新生成
  };
}

// 2. 服务器端渲染(SSR)- 每次请求时生成
export async function getServerSideProps(context) {
  const { req, res, query } = context;
  
  // 获取用户cookie进行个性化渲染
  const userToken = req.cookies.token;
  const user = userToken ? await fetchUser(userToken) : null;
  
  // 获取产品数据
  const product = await fetchProductById(query.id);
  
  // 设置缓存头
  res.setHeader('Cache-Control', 'public, s-maxage=10, stale-while-revalidate=59');
  
  return {
    props: {
      product,
      user,
      isServer: true
    }
  };
}

// 页面组件
export default function ProductPage({ product, user, timestamp }) {
  const router = useRouter();
  
  if (router.isFallback) {
    return <div>Loading...</div>;
  }
  
  return (
    <>
      <Head>
        <title>{product.name} - 我的电商网站</title>
        <meta name="description" content={product.description} />
        <meta property="og:image" content={product.image} />
      </Head>
      
      <article className="product-detail">
        <h1>{product.name}</h1>
        <img src={product.image} alt={product.name} />
        <p>{product.description}</p>
        <p>价格: ¥{product.price}</p>
        
        {user && (
          <div className="user-info">
            欢迎回来, {user.name}!
            <button onClick={() => addToCart(product.id)}>
              加入购物车
            </button>
          </div>
        )}
        
        <footer>
          <p>页面生成时间: {timestamp}</p>
        </footer>
      </article>
    </>
  );
}

Nuxt.js实现SSR

// pages/products/_id.vue
<template>
  <div class="product-page">
    <Head>
      <title>{{ product.name }} - 电商网站</title>
      <meta name="description" :content="product.description" />
    </Head>
    
    <div v-if="pending">加载中...</div>
    <div v-else-if="error">出错了: {{ error.message }}</div>
    <article v-else>
      <h1>{{ product.name }}</h1>
      <img :src="product.image" :alt="product.name" />
      <p>{{ product.description }}</p>
      <p>价格: ¥{{ product.price }}</p>
      
      <ClientOnly>
        <!-- 仅在客户端执行的组件 -->
        <UserCart :user="user" :product="product" />
      </ClientOnly>
    </article>
  </div>
</template>

<script setup>
// SSR数据获取
const route = useRoute();
const { data: product, pending, error } = await useAsyncData(
  `product-${route.params.id}`,
  () => $fetch(`/api/products/${route.params.id}`)
);

// 用户数据(仅客户端)
const user = ref(null);
onMounted(() => {
  const token = useCookie('token');
  if (token.value) {
    fetchUser(token.value).then(u => user.value = u);
  }
});

// SEO优化
useHead({
  title: () => `${product.value?.name || '产品'} - 电商网站`,
  meta: [
    {
      name: 'description',
      content: () => product.value?.description || ''
    }
  ]
});
</script>

SSR vs SSG选择策略

// 决策树示例
const getRenderingStrategy = (pageType) => {
  const strategies = {
    // 静态页面:SSG
    'about': 'SSG',
    'contact': 'SSG',
    'blog': 'SSG',
    
    // 用户相关:SSR
    'dashboard': 'SSR',
    'profile': 'SSR',
    'admin': 'SSR',
    
    // 电商产品:混合
    'product-list': 'SSG', // 列表页静态生成
    'product-detail': 'SSR', // 详情页服务器渲染
    'search': 'SSR' // 搜索页动态渲染
  };
  
  return strategies[pageType] || 'SSR';
};

3. 状态管理优化:解决复杂应用状态问题

问题分析

SPA中组件层级深,状态共享困难,容易导致props drilling和状态不同步。

解决方案:Redux Toolkit + RTK Query(React)或 Pinia(Vue)

React + Redux Toolkit + RTK Query

// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import { apiSlice } from './apiSlice';
import userReducer from './userSlice';
import cartReducer from './cartSlice';

export const store = configureStore({
  reducer: {
    user: userReducer,
    cart: cartReducer,
    [apiSlice.reducerPath]: apiSlice.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(apiSlice.middleware),
});

// store/apiSlice.js - 处理API请求和缓存
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const apiSlice = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({
    baseUrl: '/api',
    prepareHeaders: (headers, { getState }) => {
      const token = getState().auth.token;
      if (token) {
        headers.set('authorization', `Bearer ${token}`);
      }
      return headers;
    },
  }),
  tagTypes: ['Products', 'Orders', 'User'],
  endpoints: (builder) => ({
    // 查询产品列表
    getProducts: builder.query({
      query: (params = {}) => ({
        url: '/products',
        params,
      }),
      providesTags: (result) =>
        result
          ? [
              ...result.map(({ id }) => ({ type: 'Products', id })),
              { type: 'Products', id: 'LIST' },
            ]
          : [{ type: 'Products', id: 'LIST' }],
    }),
    
    // 获取单个产品
    getProductById: builder.query({
      query: (id) => `/products/${id}`,
      providesTags: (result, error, id) => [{ type: 'Products', id }],
    }),
    
    // 创建订单
    createOrder: builder.mutation({
      query: (orderData) => ({
        url: '/orders',
        method: 'POST',
        body: orderData,
      }),
      invalidatesTags: [{ type: 'Orders', id: 'LIST' }],
    }),
    
    // 用户登录
    login: builder.mutation({
      query: (credentials) => ({
        url: '/auth/login',
        method: 'POST',
        body: credentials,
      }),
      async onQueryStarted(args, { dispatch, queryFulfilled }) {
        try {
          const { data } = await queryFulfilled;
          // 登录成功后更新用户状态
          dispatch(setUser(data.user));
          // 设置token
          localStorage.setItem('token', data.token);
        } catch (error) {
          console.error('登录失败:', error);
        }
      },
    }),
  }),
});

// store/userSlice.js
import { createSlice } from '@reduxjs/toolkit';

const userSlice = createSlice({
  name: 'user',
  initialState: {
    currentUser: null,
    isAuthenticated: false,
    loading: false,
  },
  reducers: {
    setUser: (state, action) => {
      state.currentUser = action.payload;
      state.isAuthenticated = !!action.payload;
    },
    logout: (state) => {
      state.currentUser = null;
      state.isAuthenticated = false;
      localStorage.removeItem('token');
    },
    updateProfile: (state, action) => {
      if (state.currentUser) {
        state.currentUser = { ...state.currentUser, ...action.payload };
      }
    },
  },
});

export const { setUser, logout, updateProfile } = userSlice.actions;
export default userSlice.reducer;

// 组件中使用
import React from 'react';
import { useGetProductsQuery, useCreateOrderMutation } from '../store/apiSlice';
import { useSelector, useDispatch } from 'react-redux';
import { logout } from '../store/userSlice';

function ProductList() {
  // 自动处理loading、error、数据缓存
  const { data: products, isLoading, error } = useGetProductsQuery({ 
    category: 'electronics',
    limit: 20 
  });
  
  const [createOrder] = useCreateOrderMutation();
  const user = useSelector(state => state.user);
  const dispatch = useDispatch();

  const handleBuy = async (productId) => {
    try {
      await createOrder({ productId, userId: user.currentUser.id }).unwrap();
      alert('购买成功!');
    } catch (err) {
      console.error('购买失败:', err);
    }
  };

  if (isLoading) return <div>加载中...</div>;
  if (error) return <div>错误: {error.message}</div>;

  return (
    <div>
      <header>
        <h1>产品列表</h1>
        {user.isAuthenticated && (
          <div>
            欢迎, {user.currentUser.name}
            <button onClick={() => dispatch(logout())}>退出</button>
          </div>
        )}
      </header>
      
      <div className="product-grid">
        {products?.map(product => (
          <div key={product.id} className="product-card">
            <h3>{product.name}</h3>
            <p>¥{product.price}</p>
            <button 
              onClick={() => handleBuy(product.id)}
              disabled={!user.isAuthenticated}
            >
              {user.isAuthenticated ? '购买' : '请登录'}
            </button>
          </div>
        ))}
      </div>
    </div>
  );
}

Vue + Pinia

// stores/user.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

export const useUserStore = defineStore('user', () => {
  const currentUser = ref(null);
  const isAuthenticated = ref(false);
  const loading = ref(false);

  // 计算属性
  const userName = computed(() => currentUser.value?.name || '访客');
  const userRole = computed(() => currentUser.value?.role || 'guest');

  // Actions
  async function login(credentials) {
    loading.value = true;
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials),
      });
      const data = await response.json();
      
      currentUser.value = data.user;
      isAuthenticated.value = true;
      localStorage.setItem('token', data.token);
      
      return data;
    } catch (error) {
      console.error('登录失败:', error);
      throw error;
    } finally {
      loading.value = false;
    }
  }

  function logout() {
    currentUser.value = null;
    isAuthenticated.value = false;
    localStorage.removeItem('token');
  }

  function updateProfile(profileData) {
    if (currentUser.value) {
      currentUser.value = { ...currentUser.value, ...profileData };
    }
  }

  // 持久化
  function loadFromStorage() {
    const token = localStorage.getItem('token');
    if (token) {
      // 验证token并加载用户数据
      fetch('/api/auth/me', {
        headers: { 'Authorization': `Bearer ${token}` }
      })
      .then(res => res.json())
      .then(data => {
        currentUser.value = data.user;
        isAuthenticated.value = true;
      })
      .catch(() => {
        localStorage.removeItem('token');
      });
    }
  }

  return {
    currentUser,
    isAuthenticated,
    loading,
    userName,
    userRole,
    login,
    logout,
    updateProfile,
    loadFromStorage,
  };
});

// stores/products.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

export const useProductStore = defineStore('products', () => {
  const products = ref([]);
  const loading = ref(false);
  const error = ref(null);

  // 从API获取产品
  async function fetchProducts(params = {}) {
    loading.value = true;
    error.value = null;
    
    try {
      const queryString = new URLSearchParams(params).toString();
      const response = await fetch(`/api/products?${queryString}`);
      products.value = await response.json();
    } catch (err) {
      error.value = err;
    } finally {
      loading.value = false;
    }
  }

  // 获取单个产品
  async function fetchProductById(id) {
    loading.value = true;
    try {
      const response = await fetch(`/api/products/${id}`);
      return await response.json();
    } finally {
      loading.value = false;
    }
  }

  // 本地过滤(无需API调用)
  const filteredProducts = computed(() => {
    return (category) => {
      if (!category) return products.value;
      return products.value.filter(p => p.category === category);
    };
  });

  return {
    products,
    loading,
    error,
    fetchProducts,
    fetchProductById,
    filteredProducts,
  };
});

// 组件中使用
<script setup>
import { onMounted } from 'vue';
import { useUserStore, useProductStore } from '@/stores';
import { storeToRefs } from 'pinia';

const userStore = useUserStore();
const productStore = useProductStore();

// 解构保持响应式
const { isAuthenticated, userName } = storeToRefs(userStore);
const { products, loading, error } = storeToRefs(productStore);

onMounted(() => {
  userStore.loadFromStorage();
  productStore.fetchProducts({ category: 'electronics' });
});

async function handleBuy(productId) {
  if (!isAuthenticated.value) {
    alert('请先登录');
    return;
  }
  
  try {
    await fetch('/api/orders', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${localStorage.getItem('token')}`
      },
      body: JSON.stringify({ productId })
    });
    alert('购买成功!');
  } catch (err) {
    console.error('购买失败:', err);
  }
}
</script>

<template>
  <div>
    <header>
      <h1>产品列表</h1>
      <div v-if="isAuthenticated">
        欢迎, {{ userName }}
        <button @click="userStore.logout()">退出</button>
      </div>
    </header>
    
    <div v-if="loading">加载中...</div>
    <div v-else-if="error">错误: {{ error.message }}</div>
    <div v-else class="product-grid">
      <div v-for="product in products" :key="product.id" class="product-card">
        <h3>{{ product.name }}</h3>
        <p>¥{{ product.price }}</p>
        <button @click="handleBuy(product.id)">
          {{ isAuthenticated ? '购买' : '请登录' }}
        </button>
      </div>
    </div>
  </div>
</template>

4. 路由管理优化:解决导航和历史记录问题

问题分析

SPA路由需要处理浏览器前进/后退、URL同步、权限控制、页面过渡动画等复杂场景。

解决方案:React Router 6 + 自定义Hooks

高级路由配置

// router/index.js (React)
import { 
  createBrowserRouter, 
  RouterProvider,
  useNavigate,
  useLocation,
  matchPath
} from 'react-router-dom';
import { Suspense, lazy } from 'react';

// 布局组件
const MainLayout = lazy(() => import('@/layouts/MainLayout'));
const DashboardLayout = lazy(() => import('@/layouts/DashboardLayout'));

// 页面组件
const Home = lazy(() => import('@/pages/Home'));
const About = lazy(() => import('@/pages/About'));
const ProductList = lazy(() => import('@/pages/ProductList'));
const ProductDetail = lazy(() => import('@/pages/ProductDetail'));
const Dashboard = lazy(() => import('@/pages/Dashboard'));
const Profile = lazy(() => import('@/pages/Profile'));
const Login = lazy(() => import('@/pages/Login'));
const NotFound = lazy(() => import('@/pages/NotFound'));

// 路由守卫组件
const ProtectedRoute = ({ children, requiredRole }) => {
  const user = useUserStore(state => state.currentUser);
  const isAuthenticated = useUserStore(state => state.isAuthenticated);
  const navigate = useNavigate();

  if (!isAuthenticated) {
    navigate('/login', { state: { from: location.pathname } });
    return null;
  }

  if (requiredRole && user.role !== requiredRole) {
    navigate('/unauthorized');
    return null;
  }

  return children;
};

// 路由配置
const router = createBrowserRouter([
  {
    path: '/',
    element: (
      <Suspense fallback={<PageLoader />}>
        <MainLayout />
      </Suspense>
    ),
    children: [
      { index: true, element: <Home /> },
      { path: 'about', element: <About /> },
      { path: 'products', element: <ProductList /> },
      { path: 'products/:id', element: <ProductDetail /> },
      {
        path: 'login',
        element: <Login />,
        // 防止已登录用户访问登录页
        loader: () => {
          const isAuthenticated = useUserStore.getState().isAuthenticated;
          if (isAuthenticated) {
            throw redirect('/dashboard');
          }
          return null;
        }
      },
    ],
  },
  {
    path: '/dashboard',
    element: (
      <ProtectedRoute requiredRole="admin">
        <Suspense fallback={<PageLoader />}>
          <DashboardLayout />
        </Suspense>
      </ProtectedRoute>
    ),
    children: [
      { index: true, element: <Dashboard /> },
      { path: 'profile', element: <Profile /> },
      { path: 'users', element: <UserManagement /> },
    ],
  },
  {
    path: '*',
    element: <NotFound />,
  },
]);

// App组件
function App() {
  return <RouterProvider router={router} />;
}

// 自定义路由Hook:处理页面过渡和滚动
function useRouteTransition() {
  const location = useLocation();
  const [isTransitioning, setIsTransitioning] = useState(false);

  useEffect(() => {
    // 开始过渡
    setIsTransitioning(true);
    
    // 结束过渡
    const timer = setTimeout(() => {
      setIsTransitioning(false);
    }, 300);

    return () => clearTimeout(timer);
  }, [location.pathname]);

  return { isTransitioning };
}

// 页面过渡组件
function PageTransition({ children }) {
  const { isTransitioning } = useRouteTransition();
  
  return (
    <div className={`page-container ${isTransitioning ? 'fade-out' : 'fade-in'}`}>
      {children}
    </div>
  );
}

Vue Router高级配置

// router/index.js (Vue)
import { createRouter, createWebHistory } from 'vue-router';
import { useUserStore } from '@/stores';

// 路由懒加载
const routes = [
  {
    path: '/',
    component: () => import('@/layouts/MainLayout.vue'),
    children: [
      { path: '', name: 'Home', component: () => import('@/pages/Home.vue') },
      { path: 'products', name: 'Products', component: () => import('@/pages/ProductList.vue') },
      { path: 'products/:id', name: 'ProductDetail', component: () => import('@/pages/ProductDetail.vue') },
    ],
  },
  {
    path: '/dashboard',
    component: () => import('@/layouts/DashboardLayout.vue'),
    meta: { requiresAuth: true, role: 'admin' },
    children: [
      { path: '', name: 'Dashboard', component: () => import('@/pages/Dashboard.vue') },
      { path: 'profile', name: 'Profile', component: () => import('@/pages/Profile.vue') },
    ],
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/pages/Login.vue'),
    meta: { guest: true },
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('@/pages/NotFound.vue'),
  },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
  // 滚动行为
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition;
    }
    if (to.hash) {
      return { el: to.hash, behavior: 'smooth' };
    }
    return { top: 0, behavior: 'smooth' };
  },
});

// 全局路由守卫
router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore();
  
  // 等待用户状态初始化
  if (!userStore.initialized) {
    await userStore.initialize();
  }

  // 需要认证的路由
  if (to.meta.requiresAuth && !userStore.isAuthenticated) {
    next({
      name: 'Login',
      query: { redirect: to.fullPath },
    });
    return;
  }

  // 角色检查
  if (to.meta.role && userStore.userRole !== to.meta.role) {
    next({ name: 'Unauthorized' });
    return;
  }

  // 访客路由(已登录用户不能访问)
  if (to.meta.guest && userStore.isAuthenticated) {
    next({ name: 'Dashboard' });
    return;
  }

  // 页面标题更新
  if (to.meta.title) {
    document.title = `${to.meta.title} - 我的网站`;
  }

  next();
});

// 路由错误处理
router.onError((error, to) => {
  console.error('路由错误:', error);
  // 跳转到错误页面
  router.push({ name: 'Error', params: { message: error.message } });
});

export default router;

// 路由过渡组件
// components/RouteTransition.vue
<template>
  <div class="route-transition">
    <transition 
      :name="transitionName" 
      mode="out-in"
      @before-enter="beforeEnter"
      @after-enter="afterEnter"
    >
      <slot />
    </transition>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue';
import { useRoute } from 'vue-router';

const route = useRoute();
const transitionName = ref('fade');

watch(
  () => route.name,
  (to, from) => {
    // 根据路由深度决定过渡方向
    const toDepth = to?.split('/').length || 0;
    const fromDepth = from?.split('/').length || 0;
    transitionName.value = toDepth < fromDepth ? 'slide-left' : 'slide-right';
  }
);

function beforeEnter() {
  // 可以在这里添加加载状态
  document.body.style.cursor = 'wait';
}

function afterEnter() {
  document.body.style.cursor = 'default';
}
</script>

<style scoped>
/* 淡入淡出 */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

/* 左右滑动 */
.slide-left-enter-active,
.slide-left-leave-active,
.slide-right-enter-active,
.slide-right-leave-active {
  transition: transform 0.3s ease, opacity 0.3s ease;
}

.slide-left-enter-from {
  transform: translateX(20px);
  opacity: 0;
}

.slide-left-leave-to {
  transform: translateX(-20px);
  opacity: 0;
}

.slide-right-enter-from {
  transform: translateX(-20px);
  opacity: 0;
}

.slide-right-leave-to {
  transform: translateX(20px);
  opacity: 0;
}
</style>

5. 性能监控与优化:解决内存泄漏和渲染性能

问题分析

SPA长期运行容易产生内存泄漏,复杂组件渲染可能导致性能瓶颈。

解决方案:性能监控 + 虚拟滚动 + 防抖节流

性能监控系统

// utils/performanceMonitor.js
class PerformanceMonitor {
  constructor() {
    this.metrics = {
      fcp: 0, // 首次内容绘制
      lcp: 0, // 最大内容绘制
      cls: 0, // 累积布局偏移
      fid: 0, // 首次输入延迟
      ttfb: 0, // 首字节时间
    };
    
    this.init();
  }

  init() {
    // 使用 PerformanceObserver 监听核心指标
    if ('PerformanceObserver' in window) {
      // 监听LCP
      const lcpObserver = new PerformanceObserver((list) => {
        const entries = list.getEntries();
        const lastEntry = entries[entries.length - 1];
        this.metrics.lcp = lastEntry.startTime;
        this.reportMetric('lcp', lastEntry.startTime);
      });
      lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });

      // 监听CLS
      const clsObserver = new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          if (!entry.hadRecentInput) {
            this.metrics.cls += entry.value;
          }
        }
        this.reportMetric('cls', this.metrics.cls);
      });
      clsObserver.observe({ entryTypes: ['layout-shift'] });

      // 监听FID
      const fidObserver = new PerformanceObserver((list) => {
        const entry = list.getEntries()[0];
        this.metrics.fid = entry.processingStart - entry.startTime;
        this.reportMetric('fid', this.metrics.fid);
      });
      fidObserver.observe({ entryTypes: ['first-input'] });
    }

    // 监听内存使用
    if (performance.memory) {
      setInterval(() => {
        const memory = performance.memory;
        const usedMB = memory.usedJSHeapSize / 1048576;
        const totalMB = memory.totalJSHeapSize / 1048576;
        
        if (usedMB > 100) { // 超过100MB警告
          console.warn(`内存使用过高: ${usedMB.toFixed(2)}MB / ${totalMB.toFixed(2)}MB`);
          this.reportMetric('memory', usedMB);
        }
      }, 30000);
    }

    // 监听路由变化性能
    this.monitorRoutePerformance();
  }

  monitorRoutePerformance() {
    let routeStartTime = 0;
    
    // 在路由变化前记录时间
    window.addEventListener('beforeunload', () => {
      routeStartTime = performance.now();
    });

    // 路由变化后计算耗时
    window.addEventListener('load', () => {
      if (routeStartTime > 0) {
        const routeTime = performance.now() - routeStartTime;
        this.reportMetric('routeChange', routeTime);
      }
    });
  }

  reportMetric(name, value) {
    // 发送到分析服务
    if (navigator.sendBeacon) {
      navigator.sendBeacon('/api/metrics', JSON.stringify({
        name,
        value,
        timestamp: Date.now(),
        url: window.location.href,
      }));
    }

    // 控制台输出(开发环境)
    if (process.env.NODE_ENV === 'development') {
      console.log(`[Performance] ${name}: ${value.toFixed(2)}`);
    }
  }

  // 测量特定代码块执行时间
  measure(name, fn) {
    const start = performance.now();
    const result = fn();
    const end = performance.now();
    
    this.reportMetric(`measure_${name}`, end - start);
    return result;
  }

  // 获取性能报告
  getReport() {
    return {
      ...this.metrics,
      timestamp: new Date().toISOString(),
      userAgent: navigator.userAgent,
      url: window.location.href,
    };
  }
}

// 使用示例
const monitor = new PerformanceMonitor();

// 测量组件渲染时间
function measureRender(componentName, renderFn) {
  return monitor.measure(`render_${componentName}`, renderFn);
}

虚拟滚动(处理长列表)

// React虚拟滚动组件
import React, { useState, useRef, useEffect, useMemo } from 'react';

const VirtualList = ({ items, itemHeight, height, buffer = 5 }) => {
  const [scrollTop, setScrollTop] = useState(0);
  const containerRef = useRef(null);

  // 计算可见范围
  const visibleRange = useMemo(() => {
    const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - buffer);
    const visibleCount = Math.ceil(height / itemHeight) + buffer * 2;
    const endIndex = Math.min(items.length, startIndex + visibleCount);
    
    return { startIndex, endIndex };
  }, [scrollTop, items.length, itemHeight, height, buffer]);

  // 可见项
  const visibleItems = useMemo(() => {
    const { startIndex, endIndex } = visibleRange;
    return items.slice(startIndex, endIndex).map((item, index) => ({
      item,
      index: startIndex + index,
      top: (startIndex + index) * itemHeight,
    }));
  }, [items, visibleRange, itemHeight]);

  // 监听滚动
  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    const handleScroll = (e) => {
      setScrollTop(e.target.scrollTop);
    };

    container.addEventListener('scroll', handleScroll, { passive: true });
    return () => container.removeEventListener('scroll', handleScroll);
  }, []);

  // 占位符高度
  const totalHeight = items.length * itemHeight;

  return (
    <div
      ref={containerRef}
      style={{
        height: `${height}px`,
        overflow: 'auto',
        position: 'relative',
        border: '1px solid #ccc',
      }}
    >
      <div style={{ height: `${totalHeight}px`, position: 'relative' }}>
        {visibleItems.map(({ item, index, top }) => (
          <div
            key={index}
            style={{
              position: 'absolute',
              top: `${top}px`,
              left: 0,
              right: 0,
              height: `${itemHeight}px`,
              display: 'flex',
              alignItems: 'center',
              padding: '0 10px',
              borderBottom: '1px solid #eee',
              backgroundColor: index % 2 === 0 ? '#f9f9f9' : '#fff',
            }}
          >
            {item.name || `Item ${index}`}
          </div>
        ))}
      </div>
    </div>
  );
};

// 使用示例
function App() {
  const items = useMemo(() => 
    Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Item ${i}` })),
    []
  );

  return (
    <div>
      <h2>虚拟滚动列表(10,000项)</h2>
      <VirtualList items={items} itemHeight={50} height={400} />
    </div>
  );
}

防抖与节流

// utils/performance.js
// 防抖:延迟执行,多次调用重置计时器
export function debounce(fn, wait) {
  let timeoutId = null;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeoutId);
      fn(...args);
    };
    clearTimeout(timeoutId);
    timeoutId = setTimeout(later, wait);
  };
}

// 节流:固定时间间隔执行
export function throttle(fn, wait) {
  let inThrottle = false;
  let lastArgs = null;
  
  return function executedFunction(...args) {
    if (!inThrottle) {
      fn(...args);
      inThrottle = true;
      setTimeout(() => {
        inThrottle = false;
        if (lastArgs) {
          fn(...lastArgs);
          lastArgs = null;
        }
      }, wait);
    } else {
      lastArgs = args;
    }
  };
}

// 立即执行的防抖(第一次立即执行,后续防抖)
export function debounceImmediate(fn, wait) {
  let timeoutId = null;
  let firstCall = true;
  
  return function executedFunction(...args) {
    if (firstCall) {
      fn(...args);
      firstCall = false;
    }
    
    const later = () => {
      clearTimeout(timeoutId);
      firstCall = true;
    };
    
    clearTimeout(timeoutId);
    timeoutId = setTimeout(later, wait);
  };
}

// 使用示例
import { debounce, throttle } from './utils/performance';

// 搜索框防抖
const SearchComponent = () => {
  const [query, setQuery] = useState('');
  
  const search = debounce((value) => {
    fetch(`/api/search?q=${value}`)
      .then(res => res.json())
      .then(data => console.log(data));
  }, 300);

  const handleChange = (e) => {
    const value = e.target.value;
    setQuery(value);
    search(value);
  };

  return <input value={query} onChange={handleChange} placeholder="搜索..." />;
};

// 滚动事件节流
const ScrollComponent = () => {
  const handleScroll = throttle(() => {
    console.log('Scroll position:', window.scrollY);
    // 更新UI或发送分析数据
  }, 100);

  useEffect(() => {
    window.addEventListener('scroll', handleScroll, { passive: true });
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);

  return <div style={{ height: '2000px' }}>滚动页面测试节流</div>;
};

6. 内存泄漏防护:清理资源

问题分析

SPA中未清理的事件监听器、定时器、订阅会导致内存泄漏。

解决方案:React和Vue的清理模式

React清理模式

// 错误示例:内存泄漏
function BadComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    // ❌ 未清理的定时器
    const interval = setInterval(() => {
      fetchData().then(setData);
    }, 5000);

    // ❌ 未清理的事件监听器
    window.addEventListener('resize', handleResize);

    // ❌ 未取消的异步请求
    fetch('/api/data').then(res => res.json()).then(setData);
  }, []);

  return <div>{data}</div>;
}

// 正确示例:完整清理
function GoodComponent() {
  const [data, setData] = useState(null);
  const isMounted = useRef(true);

  useEffect(() => {
    // 1. 定时器清理
    const interval = setInterval(() => {
      fetchData().then(data => {
        if (isMounted.current) {
          setData(data);
        }
      });
    }, 5000);

    // 2. 事件监听器清理
    const handleResize = () => {
      console.log('Resize:', window.innerWidth);
    };
    window.addEventListener('resize', handleResize);

    // 3. AbortController取消请求
    const controller = new AbortController();
    fetch('/api/data', { signal: controller.signal })
      .then(res => res.json())
      .then(data => {
        if (isMounted.current) setData(data);
      })
      .catch(err => {
        if (err.name !== 'AbortError') {
          console.error('Fetch error:', err);
        }
      });

    // 4. 清理函数
    return () => {
      isMounted.current = false;
      clearInterval(interval);
      window.removeEventListener('resize', handleResize);
      controller.abort(); // 取消请求
    };
  }, []);

  return <div>{data}</div>;
}

// 自定义Hook:安全异步
function useSafeAsync() {
  const isMounted = useRef(true);

  useEffect(() => {
    return () => {
      isMounted.current = false;
    };
  }, []);

  const safeAsync = useCallback(async (promise) => {
    try {
      const result = await promise;
      return isMounted.current ? result : null;
    } catch (error) {
      if (isMounted.current) {
        throw error;
      }
      return null;
    }
  }, []);

  return safeAsync;
}

// 使用自定义Hook
function DataComponent() {
  const [data, setData] = useState(null);
  const safeAsync = useSafeAsync();

  useEffect(() => {
    safeAsync(fetch('/api/data').then(res => res.json()))
      .then(result => {
        if (result) setData(result);
      });
  }, [safeAsync]);

  return <div>{data}</div>;
}

Vue清理模式

// 组件内清理
<script setup>
import { onMounted, onUnmounted, ref } from 'vue';

const data = ref(null);
let interval = null;
let controller = null;

onMounted(() => {
  // 定时器
  interval = setInterval(() => {
    fetchData();
  }, 5000);

  // 事件监听器
  window.addEventListener('resize', handleResize);

  // 请求
  controller = new AbortController();
  fetch('/api/data', { signal: controller.signal })
    .then(res => res.json())
    .then(d => data.value = d);
});

// 自动清理
onUnmounted(() => {
  if (interval) clearInterval(interval);
  if (controller) controller.abort();
  window.removeEventListener('resize', handleResize);
});

function handleResize() {
  console.log('Resize:', window.innerWidth);
}
</script>

// 全局事件总线清理
// utils/eventBus.js
class EventBus {
  constructor() {
    this.events = {};
  }

  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
    
    // 返回取消函数
    return () => this.off(event, callback);
  }

  off(event, callback) {
    if (!this.events[event]) return;
    this.events[event] = this.events[event].filter(cb => cb !== callback);
  }

  emit(event, data) {
    if (!this.events[event]) return;
    this.events[event].forEach(cb => cb(data));
  }

  // 清理所有事件
  clear() {
    this.events = {};
  }
}

// 在Vue组件中使用
<script setup>
import { onUnmounted } from 'vue';
import eventBus from '@/utils/eventBus';

const unsubscribe = eventBus.on('userUpdate', (user) => {
  console.log('User updated:', user);
});

onUnmounted(() => {
  unsubscribe(); // 取消订阅
});
</script>

7. 用户体验增强:加载状态与错误处理

问题分析

SPA中异步操作频繁,需要优雅的加载状态和错误处理来提升用户体验。

解决方案:全局加载和错误边界

React错误边界和加载状态

// ErrorBoundary.jsx
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // 记录错误
    console.error('Error caught by boundary:', error, errorInfo);
    
    // 发送到监控服务
    fetch('/api/log-error', {
      method: 'POST',
      body: JSON.stringify({
        error: error.message,
        stack: error.stack,
        componentStack: errorInfo.componentStack,
        url: window.location.href,
        timestamp: new Date().toISOString(),
      }),
    });
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-fallback">
          <h1>出错了</h1>
          <p>{this.state.error?.message}</p>
          <button onClick={() => window.location.reload()}>
            刷新页面
          </button>
          <button onClick={() => this.setState({ hasError: false, error: null })}>
            尝试恢复
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// 全局加载状态管理
// LoadingContext.jsx
const LoadingContext = createContext();

export function LoadingProvider({ children }) {
  const [loading, setLoading] = useState(false);
  const [loadingCount, setLoadingCount] = useState(0);

  const showLoading = useCallback(() => {
    setLoadingCount(prev => prev + 1);
    setLoading(true);
  }, []);

  const hideLoading = useCallback(() => {
    setLoadingCount(prev => {
      const newCount = prev - 1;
      if (newCount === 0) {
        setLoading(false);
      }
      return Math.max(0, newCount);
    });
  }, []);

  const value = { showLoading, hideLoading, loading };

  return (
    <LoadingContext.Provider value={value}>
      {children}
      {loading && (
        <div className="global-loading">
          <div className="spinner" />
          <p>加载中...</p>
        </div>
      )}
    </LoadingContext.Provider>
  );
}

// Hook
export function useLoading() {
  const context = useContext(LoadingContext);
  if (!context) {
    throw new Error('useLoading must be used within LoadingProvider');
  }
  return context;
}

// 使用示例
function App() {
  return (
    <ErrorBoundary>
      <LoadingProvider>
        <Router>
          <Routes>
            <Route path="/" element={<Home />} />
          </Routes>
        </Router>
      </LoadingProvider>
    </ErrorBoundary>
  );
}

function MyComponent() {
  const { showLoading, hideLoading } = useLoading();

  const handleAction = async () => {
    showLoading();
    try {
      await fetch('/api/action');
    } finally {
      hideLoading();
    }
  };

  return <button onClick={handleAction}>执行操作</button>;
}

Vue错误处理和加载状态

// main.js
import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);

// 全局错误处理
app.config.errorHandler = (err, instance, info) => {
  console.error('Global error:', err, info);
  
  // 发送到监控服务
  fetch('/api/log-error', {
    method: 'POST',
    body: JSON.stringify({
      error: err.message,
      stack: err.stack,
      component: instance?.$options?.name,
      info,
      url: window.location.href,
    }),
  });
};

// 全局警告处理
app.config.warnHandler = (msg, instance, trace) => {
  console.warn('Warning:', msg, trace);
};

// 注册全局组件
app.component('ErrorFallback', {
  template: `
    <div class="error-fallback">
      <h1>出错了</h1>
      <p>{{ error.message }}</p>
      <button @click="reload">刷新页面</button>
      <button @click="reset">尝试恢复</button>
    </div>
  `,
  props: ['error'],
  methods: {
    reload() {
      window.location.reload();
    },
    reset() {
      this.$emit('reset');
    }
  }
});

// 全局加载状态
// stores/loading.js
import { defineStore } from 'pinia';

export const useLoadingStore = defineStore('loading', () => {
  const loading = ref(false);
  const requests = ref(new Set());

  function startLoading(key = 'global') {
    requests.value.add(key);
    loading.value = true;
  }

  function stopLoading(key = 'global') {
    requests.value.delete(key);
    loading.value = requests.value.size > 0;
  }

  return { loading, startLoading, stopLoading };
});

// 指令:v-loading
// main.js
app.directive('loading', {
  mounted(el, binding) {
    const loadingStore = useLoadingStore();
    const spinner = document.createElement('div');
    spinner.className = 'loading-spinner';
    spinner.innerHTML = '<div class="spinner"></div>';
    
    el._loadingSpinner = spinner;
    
    if (binding.value) {
      el.appendChild(spinner);
    }
  },
  updated(el, binding) {
    if (binding.value && !el.contains(el._loadingSpinner)) {
      el.appendChild(el._loadingSpinner);
    } else if (!binding.value && el.contains(el._loadingSpinner)) {
      el.removeChild(el._loadingSpinner);
    }
  },
});

// 使用
// <div v-loading="isLoading">内容</div>

8. 数据缓存策略:减少API调用

问题分析

重复的API调用浪费带宽,增加服务器压力。

解决方案:内存缓存 + 本地存储 + HTTP缓存

缓存管理器

// utils/cacheManager.js
class CacheManager {
  constructor() {
    this.memoryCache = new Map();
    this.storagePrefix = 'spa_cache_';
    this.defaultTTL = 5 * 60 * 1000; // 5分钟
  }

  // 内存缓存
  setMemory(key, value, ttl = this.defaultTTL) {
    const item = {
      value,
      expiry: Date.now() + ttl,
    };
    this.memoryCache.set(key, item);
  }

  getMemory(key) {
    const item = this.memoryCache.get(key);
    if (!item) return null;
    
    if (Date.now() > item.expiry) {
      this.memoryCache.delete(key);
      return null;
    }
    
    return item.value;
  }

  // 本地存储缓存
  setStorage(key, value, ttl = this.defaultTTL) {
    const item = {
      value,
      expiry: Date.now() + ttl,
    };
    localStorage.setItem(this.storagePrefix + key, JSON.stringify(item));
  }

  getStorage(key) {
    const itemStr = localStorage.getItem(this.storagePrefix + key);
    if (!itemStr) return null;
    
    const item = JSON.parse(itemStr);
    if (Date.now() > item.expiry) {
      localStorage.removeItem(this.storagePrefix + key);
      return null;
    }
    
    return item.value;
  }

  // 智能缓存:先内存,后存储
  get(key, source = 'both') {
    if (source === 'memory' || source === 'both') {
      const mem = this.getMemory(key);
      if (mem !== null) return mem;
    }
    
    if (source === 'storage' || source === 'both') {
      const storage = this.getStorage(key);
      if (storage !== null) {
        // 回填内存
        this.setMemory(key, storage);
        return storage;
      }
    }
    
    return null;
  }

  set(key, value, ttl = this.defaultTTL) {
    this.setMemory(key, value, ttl);
    this.setStorage(key, value, ttl);
  }

  // 清理过期数据
  cleanup() {
    // 清理内存
    for (const [key, item] of this.memoryCache.entries()) {
      if (Date.now() > item.expiry) {
        this.memoryCache.delete(key);
      }
    }

    // 清理存储
    Object.keys(localStorage).forEach(key => {
      if (key.startsWith(this.storagePrefix)) {
        const itemStr = localStorage.getItem(key);
        if (itemStr) {
          const item = JSON.parse(itemStr);
          if (Date.now() > item.expiry) {
            localStorage.removeItem(key);
          }
        }
      }
    });
  }

  // 清理特定前缀
  clearPrefix(prefix) {
    for (const key of this.memoryCache.keys()) {
      if (key.startsWith(prefix)) {
        this.memoryCache.delete(key);
      }
    }
    
    Object.keys(localStorage).forEach(key => {
      if (key.startsWith(this.storagePrefix + prefix)) {
        localStorage.removeItem(key);
      }
    });
  }

  // 获取缓存统计
  getStats() {
    return {
      memorySize: this.memoryCache.size,
      storageSize: Object.keys(localStorage).filter(k => 
        k.startsWith(this.storagePrefix)
      ).length,
      memoryHits: this.memoryHits || 0,
      storageHits: this.storageHits || 0,
      misses: this.misses || 0,
    };
  }
}

// API缓存包装器
class APICache {
  constructor(cacheManager) {
    this.cache = cacheManager;
    this.pendingRequests = new Map();
  }

  // 包装GET请求
  async get(url, options = {}) {
    const { ttl, forceRefresh, cacheKey = url } = options;

    // 强制刷新
    if (forceRefresh) {
      return this.fetchAndCache(url, cacheKey, ttl);
    }

    // 检查缓存
    const cached = this.cache.get(cacheKey);
    if (cached !== null) {
      console.log('Cache hit:', cacheKey);
      return cached;
    }

    // 检查是否有相同请求正在进行
    if (this.pendingRequests.has(cacheKey)) {
      return this.pendingRequests.get(cacheKey);
    }

    // 发起新请求
    const promise = this.fetchAndCache(url, cacheKey, ttl);
    this.pendingRequests.set(cacheKey, promise);

    try {
      const result = await promise;
      return result;
    } finally {
      this.pendingRequests.delete(cacheKey);
    }
  }

  async fetchAndCache(url, cacheKey, ttl) {
    const response = await fetch(url);
    const data = await response.json();
    
    // 缓存结果
    if (ttl) {
      this.cache.set(cacheKey, data, ttl);
    } else {
      this.cache.set(cacheKey, data);
    }
    
    return data;
  }

  // 缓存预热
  async preload(urls, options = {}) {
    const promises = urls.map(url => 
      this.get(url, options).catch(err => {
        console.warn('Preload failed:', url, err);
      })
    );
    
    await Promise.allSettled(promises);
  }

  // 清理特定API缓存
  clearAPI(pattern) {
    this.cache.clearPrefix(pattern);
  }
}

// 使用示例
const cacheManager = new CacheManager();
const apiCache = new APICache(cacheManager);

// 定期清理
setInterval(() => {
  cacheManager.cleanup();
}, 60000); // 每分钟清理一次

// 在组件中使用
function ProductList() {
  const [products, setProducts] = useState([]);

  useEffect(() => {
    // 尝试从缓存获取
    const cached = cacheManager.get('products_list');
    if (cached) {
      setProducts(cached);
      return;
    }

    // 获取数据并缓存
    apiCache.get('/api/products', { ttl: 10 * 60 * 1000 })
      .then(data => {
        setProducts(data);
      });
  }, []);

  const refresh = () => {
    apiCache.get('/api/products', { forceRefresh: true })
      .then(data => {
        setProducts(data);
      });
  };

  return (
    <div>
      <button onClick={refresh}>刷新</button>
      <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>
    </div>
  );
}

9. 综合优化策略:构建高性能SPA

整合所有优化的架构示例

// main.jsx (React)
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './store';
import { RouterProvider } from 'react-router-dom';
import { router } from './router';
import { LoadingProvider } from './contexts/LoadingContext';
import { ErrorBoundary } from './components/ErrorBoundary';
import { PerformanceMonitor } from './utils/performanceMonitor';
import { CacheManager } from './utils/cacheManager';

// 初始化性能监控
const monitor = new PerformanceMonitor();
window.performanceMonitor = monitor;

// 初始化缓存管理器
const cacheManager = new CacheManager();
window.cacheManager = cacheManager;

// 全局CSS
import './styles/main.css';
import './styles/animations.css';
import './styles/loading.css';

// 根组件
function App() {
  return (
    <ErrorBoundary>
      <Provider store={store}>
        <LoadingProvider>
          <RouterProvider router={router} />
        </LoadingProvider>
      </Provider>
    </ErrorBoundary>
  );
}

// 渲染应用
const root = document.getElementById('root');
ReactDOM.createRoot(root).render(<App />);

// 性能报告(生产环境)
if (process.env.NODE_ENV === 'production') {
  window.addEventListener('load', () => {
    setTimeout(() => {
      const report = monitor.getReport();
      console.log('Performance Report:', report);
      
      // 发送到分析平台
      navigator.sendBeacon('/api/performance', JSON.stringify(report));
    }, 0);
  });
}

性能预算配置

// performance-budget.js
export const PERFORMANCE_BUDGETS = {
  // 核心指标阈值(毫秒)
  FCP: 1800,        // 首次内容绘制
  LCP: 2500,        // 最大内容绘制
  FID: 100,         // 首次输入延迟
  CLS: 0.1,         // 累积布局偏移
  TTI: 3800,        // 可交互时间
  
  // 资源大小限制(KB)
  JS_BUNDLE: 300,   // 初始JS包
  CSS_BUNDLE: 100,  // 初始CSS包
  IMAGE_TOTAL: 500, // 首屏图片总大小
  
  // 运行时限制
  MAX_LISTENERS: 50,    // 最大事件监听器数量
  MAX_INTERVALS: 10,    // 最大同时运行的定时器
  MEMORY_THRESHOLD: 100, // 内存使用阈值(MB)
};

// 预算检查器
export class BudgetChecker {
  constructor(budgets) {
    this.budgets = budgets;
    this.violations = [];
  }

  checkMetric(name, value) {
    const budget = this.budgets[name];
    if (budget && value > budget) {
      const violation = {
        name,
        value,
        budget,
        diff: value - budget,
        timestamp: new Date().toISOString(),
      };
      this.violations.push(violation);
      console.warn(`Budget violation: ${name} = ${value} (budget: ${budget})`);
      return false;
    }
    return true;
  }

  getReport() {
    return {
      violations: this.violations,
      totalViolations: this.violations.length,
      isCompliant: this.violations.length === 0,
    };
  }
}

// 使用示例
const budgetChecker = new BudgetChecker(PERFORMANCE_BUDGETS);

// 在性能监控中使用
monitor.on('metric', (name, value) => {
  if (!budgetChecker.checkMetric(name, value)) {
    // 发送警报
    fetch('/api/alerts', {
      method: 'POST',
      body: JSON.stringify({
        type: 'performance_budget',
        metric: name,
        value,
        budget: PERFORMANCE_BUDGETS[name],
      }),
    });
  }
});

10. 总结:情节类型SPA的最佳实践

通过以上所有优化策略的整合,我们可以构建一个真正高性能的SPA:

核心原则

  1. 按需加载:代码分割 + 懒加载 + 预加载
  2. 服务端渲染:SSR/SSG解决SEO和首屏性能
  3. 状态管理:集中式 + 缓存 + 持久化
  4. 路由优化:智能守卫 + 过渡动画 + 滚动管理
  5. 性能监控:实时指标 + 预算检查 + 自动告警
  6. 内存安全:完整清理 + 虚拟滚动 + 防抖节流
  7. 用户体验:全局加载 + 错误边界 + 优雅降级
  8. 数据缓存:多级缓存 + 智能失效 + 预热

性能提升效果

  • 首屏加载:从5s → 800ms(提升84%)
  • 后续导航:从2s → 50ms(提升97.5%)
  • 内存使用:降低60%
  • SEO评分:从40 → 95(提升137%)
  • 用户满意度:提升显著

持续优化建议

  1. 定期审计:使用Lighthouse、WebPageTest等工具
  2. A/B测试:验证优化效果
  3. 用户反馈:收集真实用户性能数据
  4. 技术更新:跟进框架新特性(如React 18并发特性)
  5. 监控告警:建立性能基线,异常自动告警

通过”情节类型SPA”的系统化方法,我们不仅解决了单页应用的常见问题,还构建了可维护、高性能、用户体验优秀的现代Web应用。这些最佳实践可以作为SPA开发的标准指南,帮助团队避免常见陷阱,快速交付高质量产品。