Xây Dựng Ứng Dụng Thương Mại Điện Tử Với Vue.js Trên PC

Cấu trúc dự án Vue cơ bản

Quy trình phát triển module frontend thường bao gồm:

  • Xây dựng giao diện tĩnh và chia thành các thành phần độc lập
  • Thực hiện các yêu cầu API
  • Quản lý trạng thái bằng Vuex (actions, mutations, state)
  • Hiển thị dữ liệu động từ kho lưu trữ

Cấu trúc thư mục

Thư mục public: Chứa tài nguyên tĩnh, được webpack giữ nguyên khi đóng gói vào thư mục dist.

public/index.html: File template tạo ra trang chủ, tự động chèn các file JS/CSS đã đóng gói. Trình duyệt sẽ hiển thị file này khi truy cập dự án.

Thư mục src (mã nguồn):

  • assets: Tài nguyên chung
  • components: Thành phần không thuộc route (toàn cục)
  • App.vue: Thành phần gốc duy nhất
  • main.js: Điểm vào đầu tiên của ứng dụng

babel.config.js: Cấu hình Babel
package.json: Thông tin chi tiết dự án
package-lock.json: File lưu trữ phiên bản các gói

Cấu hình dự án

Khởi động tự động trình duyệt

Trong package.json:

"scripts": {
  "start": "vue-cli-service serve --open --host 0.0.0.0",
  "build": "vue-cli-service build",
  "lint": "vue-cli-service lint"
}

Tắt ESLint

Tạo file vue.config.js tại gốc dự án:

module.exports = {
  lintOnSave: false,
  configureWebpack: {
    devtool: 'source-map',
    optimization: {
      splitChunks: {
        chunks: 'all'
      }
    }
  }
}

Cấu hình đường dẫn alias

Tạo jsconfig.json:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "exclude": ["node_modules", "dist"]
}

Xử lý样式

Để sử dụng LESS cho các thành phần:

npm install --save less less-loader@5

Khai báo trong component:

<script lang="less" scoped>

Xóa样式 mặc định

Thêm vào public/index.html:

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css">

Cấu hình router

Tạo thư mục router với file index.js:

import Vue from 'vue';
import Router from 'vue-router';
import Home from '@/pages/Home';

Vue.use(Router);

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    }
  ]
});

Đăng ký trong main.js:

import router from './router';
new Vue({
  router,
  render: h => h(App)
}).$mount('#app');

Ẩn/hiện footer

Sử dụng v-show thay vì v-if để tránh thao tác DOM thường xuyên. Cấu hình meta trong route:

{
  path: '/login',
  component: Login,
  meta: { showFooter: false }
}

Sử dụng trong component:

<Footer v-show="$route.meta.showFooter" />

Chuyển hướng route

Query và params

query: Tham số trong URL dạng /search?k1=v1
params: Tham số trong đường dẫn /search/:keyword

Xử lý params tùy chọn:

path: "/search/:keyword?"

Xử lý params rỗng:

this.$router.push({ name: "Search", params: { keyword: '' || undefined } })

Chuyển hướng bằng props

// Cách 3: Sử dụng hàm trả về props
props(route) {
  return {
    id: route.query.id,
    title: route.query.title
  }
}

Giải quyết cảnh báo push lặp lại

Viết lại phương thức push trong router:

const originalPush = VueRouter.prototype.push;
VueRouter.prototype.push = function(location, resolve, reject) {
  if (resolve && reject) {
    originalPush.call(this, location, resolve, reject);
  } else {
    originalPush.call(this, location, () => {}, () => {});
  }
};

封装 Axios

Tạo file api/request.js:

import axios from 'axios';

const apiService = axios.create({
  baseURL: process.env.VUE_APP_API_BASE_URL,
  timeout: 10000
});

apiService.interceptors.request.use(config => {
  const token = localStorage.getItem('user_token');
  if (token) {
    config.headers['Authorization'] = `Bearer ${token}`;
  }
  return config;
});

apiService.interceptors.response.use(response => {
  return response.data;
}, error => {
  console.error('Lỗi phản hồi:', error);
  return Promise.reject(new Error('Lỗi kết nối'));
});

export default apiService;

Giải quyết vấn đề CORS

Trong vue.config.js:

module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://39.98.123.211',
        changeOrigin: true
      }
    }
  }
};

Xử lý dữ liệu Vuex

Trong store/index.js:

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    bannerList: []
  },
  mutations: {
    SET_BANNER_LIST(state, data) {
      state.bannerList = data;
    }
  },
  actions: {
    async fetchBannerList({ commit }) {
      const result = await apiService.get('/banner');
      commit('SET_BANNER_LIST', result);
    }
  }
});

Swiper cho slider

Sử dụng this.$nextTick() để đảm bảo DOM đã render:

watch: {
  bannerList: {
    immediate: true,
    handler() {
      this.$nextTick(() => {
        new Swiper(this.$refs.slider, {
          loop: true,
          pagination: { el: '.swiper-pagination' },
          navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' }
        });
      });
    }
  }
}

Thiết kế component

Component cha truyền dữ liệu cho con qua props:

<!-- Component cha -->
<Floor :data="floorItem" />

<!-- Component con -->
<script>
export default {
  props: ['data']
}
</script>

Xử lý dữ liệu động

Sử dụng watch theo dõi thay đổi route:

watch: {
  $route: {
    handler(newVal) {
      Object.assign(this.params, newVal.query, newVal.params);
      this.fetchData();
    }
  }
}

Thực hiện phân trang

Tính toán trang hiển thị:

computed: {
  pageRange() {
    const start = Math.max(1, this.currentPage - 2);
    const end = Math.min(this.totalPages, start + 4);
    return { start, end };
  }
}

Xử lý sự kiện blur

So sánh giá trị trước và sau khi blur:

methods: {
  handleBlur(event) {
    if (event.target.value !== this.originalValue) {
      this.saveChanges();
    }
  }
}

Thiết kế thanh điều hướng

Thay đổi trạng thái dựa trên route:

computed: {
  isActive() {
    return this.$route.path === this.routePath;
  }
}

Quản lý trạng thái đăng nhập

Trong router/index.js:

router.beforeEach(async (to, from, next) => {
  const token = localStorage.getItem('auth_token');
  if (to.path === '/login' && token) {
    next('/dashboard');
  } else if (!token && to.meta.requiresAuth) {
    next('/login');
  } else {
    next();
  }
});

Tối ưu hóa tải

Sử dụng lazy loading cho components:

component: () => import('@/views/Profile')

Thẻ: vue Vuex Axios Vue Router NProgress

Đăng vào ngày 27 tháng 6 lúc 19:20