聊聊前端MPA(Multi-Page Application)与SPA(Single-Page Application)架构实战篇(2)

35 阅读2分钟

上一篇中介绍了MPA(Multi-Page Application)与SPA(Single-Page Application)的定义、工作原理、优缺点、适用场景以及特点,如果没有看过的小伙伴,点击下方链接查看 > juejin.cn/post/756424…

下面进入正题

1、项目实战

1.1 MPA项目实战

技术栈选择

// package.json - 传统MPA配置
{
  "name": "mpa-project",
  "scripts": {
    "dev": "nodemon server.js",
    "build:css": "postcss src/css/*.css -d public/css",
    "build:js": "webpack --mode=production"
  },
  "dependencies": {
    "express": "^4.18.0",
    "ejs": "^3.1.8"
  },
  "devDependencies": {
    "webpack": "^5.0.0",
    "postcss": "^8.0.0"
  }
}

服务端配置

// server.js
const express = require('express');
const app = express();

// 设置模板引擎
app.set('view engine', 'ejs');
app.set('views', './views');

// 静态资源
app.use(express.static('public'));

// 路由配置
app.use('/', require('./routes/index'));
app.use('/products', require('./routes/products'));
app.use('/cart', require('./routes/cart'));

// 启动服务器
app.listen(3000, () => {
  console.log('MPA服务器运行在 http://localhost:3000');
});

// routes/products.js
const express = require('express');
const router = express.Router();
const Product = require('../models/product');

router.get('/', async (req, res) => {
  try {
    const products = await Product.findAll();
    res.render('products', { 
      title: '产品列表',
      products: products,
      user: req.session.user 
    });
  } catch (error) {
    res.status(500).render('error', { error });
  }
});

module.exports = router;

1.2 SPA项目实战

技术栈选择

// package.json - 现代SPA配置
{
  "name": "spa-project",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.3.0",
    "vue-router": "^4.2.0",
    "pinia": "^2.1.0",
    "axios": "^1.4.0"
  },
  "devDependencies": {
    "vite": "^4.3.0",
    "@vitejs/plugin-vue": "^4.2.0"
  }
}

核心代码实现

// src/main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';

const app = createApp(App);
app.use(createPinia());
app.use(router);
app.mount('#app');

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

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('../views/Home.vue'),
    meta: { title: '首页' }
  },
  {
    path: '/products',
    name: 'Products', 
    component: () => import('../views/Products.vue'),
    meta: { title: '产品列表', requiresAuth: true }
  },
  {
    path: '/products/:id',
    name: 'ProductDetail',
    component: () => import('../views/ProductDetail.vue'),
    props: true
  },
  {
    path: '/cart',
    name: 'Cart',
    component: () => import('../views/Cart.vue'),
    meta: { requiresAuth: true }
  }
];

const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition;
    } else {
      return { top: 0 };
    }
  }
});

// 路由守卫
router.beforeEach((to, from, next) => {
  const authStore = useAuthStore();
  
  // 设置页面标题
  if (to.meta.title) {
    document.title = to.meta.title;
  }
  
  // 验证身份
  if (to.meta.requiresAuth && !authStore.isAuthenticated) {
    next('/login');
  } else {
    next();
  }
});

export default router;

// src/stores/auth.js
import { defineStore } from 'pinia';

export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null,
    token: localStorage.getItem('token')
  }),
  
  getters: {
    isAuthenticated: (state) => !!state.token
  },
  
  actions: {
    async login(credentials) {
      try {
        const response = await api.post('/auth/login', credentials);
        this.token = response.data.token;
        this.user = response.data.user;
        localStorage.setItem('token', this.token);
      } catch (error) {
        throw new Error('登录失败');
      }
    },
    
    logout() {
      this.token = null;
      this.user = null;
      localStorage.removeItem('token');
      router.push('/');
    }
  }
});

// src/composables/useApi.js
import { ref } from 'vue';

export function useApi() {
  const loading = ref(false);
  const error = ref(null);
  
  const request = async (url, options = {}) => {
    loading.value = true;
    error.value = null;
    
    try {
      const response = await fetch(url, {
        headers: {
          'Content-Type': 'application/json',
          ...options.headers
        },
        ...options
      });
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      return await response.json();
    } catch (err) {
      error.value = err.message;
      throw err;
    } finally {
      loading.value = false;
    }
  };
  
  return { loading, error, request };
}