Phát triển ứng dụng di động đa nền tảng với Vue.js và HBuilder

Trong những năm gần đây, mô hình phát triển ứng dụng di động lai (Hybrid App) đã trở nên phổ biến, đặc biệt là với các doanh nghiệp vừa và nhỏ, nhờ khả năng tiết kiệm đáng kể chi phí và nhân lực so với phát triển ứng dụng gốc (Native App).

Hybrid App là loại ứng dụng nằm giữa Web App và Native App, kế thừa những ưu điểm về trải nghiệm người dùng tốt của Native App và khả năng phát triển đa nền tảng của Web App.

Nếu áp dụng phương pháp phát triển truyền thống, các công ty có sản phẩm hoạt động trên nhiều nền tảng sẽ cần tuyển dụng đội ngũ phát triển riêng cho từng nền tảng (ví dụ: một lập trình viên cho Android, một cho iOS). Điều này không chỉ tăng gấp đôi chi phí nhân sự mà còn gây ra sự phức tạp và tốn kém trong việc bảo trì, cập nhật khi có thay đổi về chức năng. Việc xây dựng một bộ mã nguồn duy nhất có thể biên dịch cho nhiều nền tảng như iOS, Android, H5, và thậm chí cả các ứng dụng mini (mini-program) sẽ là một giải pháp tối ưu, tiết kiệm thời gian và công sức.

Bài viết này sẽ hướng dẫn cách sử dụng Vue.js để xây dựng giao diện frontend dựa trên nền tảng H5, có khả năng tùy biến cho nhiều nền tảng khác nhau. Sau đó, chúng ta sẽ sử dụng HBuilder để đóng gói mã nguồn này thành các tệp cài đặt ứng dụng Android (APK), đạt được mục tiêu "một bộ mã nguồn, chạy đa nền tảng".

Trước hết, hãy cài đặt phiên bản đầy đủ của Vue CLI. Trước đây có thể bạn đã sử dụng phiên bản đơn giản (simple), nhưng đối với phát triển ứng dụng di động, phiên bản đầy đủ (webpack) là cần thiết.

# Cài đặt cnpm (npm registry của Alibaba)
npm install -g cnpm --registry=https://registry.npm.taobao.org

# Cài đặt Vue.js phiên bản đầy đủ
cnpm install -g vue-cli

# Dọn dẹp cache
cnpm cache clean --force

# Tạo dự án mới
vue init webpack my-vue-app

# Di chuyển vào thư mục dự án và chạy với hot-reloading
cd my-vue-app
cnpm run dev

Sau khi tạo dự án, làm thế nào để thiết kế giao diện có khả năng đáp ứng (responsive design), tức là hiển thị tốt trên nhiều kích thước màn hình khác nhau? Chúng ta sẽ sử dụng framework Bootstrap. Để tích hợp Bootstrap, chỉnh sửa tệp `main.js` (hoặc tệp entry point chính của bạn) và thêm dòng sau:

// Sử dụng webpack loader để import CSS
require('!style-loader!css-loader!./assets/css/main.css');

Nếu gặp lỗi, có thể bạn cần cài đặt các thư viện phụ thuộc:

cnpm install style-loader css-loader --save

Tiếp theo, tạo một tệp component cho trang chủ, ví dụ `Home.vue`, và thêm nội dung sau:

<template>
  <div class="app-container">
    <header class="app-header">
      <!-- Mobile header toggle -->
      <div class="navbar navbar-default visible-xs">
        <button type="button" class="navbar-toggle collapsed" @click="toggleMenu">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a href="/" class="navbar-brand">Awesome Video Platform</a>
      </div>

      <!-- Sidebar navigation -->
      <nav :class="['sidebar', { 'is-open': menuOpen }]" id="app-sidebar">
        <div class="navbar-collapse collapse" id="navbar-collapse">
          <div class="site-header hidden-xs">
            <a class="site-brand" href="./index.html" title="Go to homepage">
              <img class="img-responsive site-logo" alt="Logo" src="../assets/images/app-logo.svg">
              Awesome Video Platform
            </a>
            <p>
              Video Categories
            </p>
          </div>
          <ul class="nav">
            <li><a href="./index.html" title="">Home</a></li>
            <li><a href="./about.html" title="">Video Details</a></li>
            <li><a href="./services.html" title="">Services</a></li>
            <li><a href="./contact.html" title="">Contact</a></li>
            <li><a href="./components.html" title="">Components</a></li>
          </ul>

          <nav class="nav-footer">
            <p class="nav-footer-social-buttons">
              <a class="fa-icon" href="https://www.instagram.com/" title="Instagram" target="_blank">
                <i class="fa fa-instagram"></i>
              </a>
              <a class="fa-icon" href="#" title="Dribbble">
                <i class="fa fa-dribbble"></i>
              </a>
              <a class="fa-icon" href="#" title="Twitter">
                <i class="fa fa-twitter"></i>
              </a>
            </p>
          </nav>
        </div>
      </nav>
    </header>

    <!-- Main content area -->
    <main class="app-main-content" id="main-content">
      <div class="hero-full-wrapper">
        <!-- Masonry layout -->
        <div v-masonry :options="masonryOptions" transition-duration="0.3s" item-selector=".grid-item" class="grid">
          <div class="gutter-sizer"></div>
          <div class="grid-sizer"></div>

          <div v-masonry-tile class="grid-item" v-for="(video, index) in videoList" :key="index">
            <img class="img-responsive" :alt="video.title" :src="video.thumbnail">
            <a href="./project.html" class="project-description">
              <div class="project-text-holder">
                <div class="project-text-inner">
                  <h3>{{ video.title }}</h3>
                  <p>{{ video.description }}</p>
                </div>
              </div>
            </a>
          </div>

        </div>
      </div>
    </main>
  </div>
</template>

<script>
export default {
  data() {
    return {
      msg: "Welcome to the Vue App",
      videoList: [
        { title: "Video Title 1", description: "A brief description.", thumbnail: require("../assets/images/img-12.jpg") },
        { title: "Video Title 2", description: "Discover more content.", thumbnail: require("../assets/images/img-05.jpg") },
        { title: "Video Title 3", description: "More details here.", thumbnail: require("../assets/images/img-13.jpg") },
        { title: "Video Title 4", description: "Explore this topic.", thumbnail: require("../assets/images/img-04.jpg") },
        { title: "Video Title 5", description: "Interesting insights.", thumbnail: require("../assets/images/img-07.jpg") },
        { title: "Video Title 6", description: "Find out more.", thumbnail: require("../assets/images/img-11.jpg") },
        { title: "Video Title 7", description: "Learn something new.", thumbnail: require("../assets/images/img-10.jpg") },
        { title: "Video Title 8", description: "Check out our collection.", thumbnail: require("../assets/images/img-03.jpg") },
      ],
      menuOpen: false, // State for mobile menu
      masonryOptions: {
        // Masonry options if needed
      }
    };
  },
  mounted() {
    // Initialize masonry after component is mounted if not using the plugin directly in main.js
    // Ensure your CSS correctly sets up grid-item and gutter-sizer
  },
  methods: {
    toggleMenu() {
      this.menuOpen = !this.menuOpen;
    }
  }
};
</script>

<style>
/* Add your component-specific styles here or import from a separate CSS file */
.app-container {
  display: flex;
  min-height: 100vh;
}

.app-header {
  width: 250px; /* Fixed width for sidebar */
  flex-shrink: 0;
}

.app-main-content {
  flex-grow: 1;
  padding: 20px;
}

/* Responsive adjustments */
@media (max-width: 768px) {
  .app-header {
    width: 0; /* Hide sidebar by default on mobile */
    position: fixed;
    top: 0;
    left: 0;
    height: 100%;
    z-index: 1000;
    background-color: #f8f8f8; /* Example background */
    transform: translateX(-100%);
    transition: transform 0.3s ease-in-out;
  }
  .app-header.is-open {
    transform: translateX(0);
  }
  .app-main-content {
    width: 100%;
  }
}

/* Masonry specific styles would typically be in a global CSS file */
.grid {
  margin: 0 auto;
  max-width: 1200px; /* Example max width */
}
.grid-sizer, .grid-item {
  width: 33.333%; /* Example: 3 columns */
}
.gutter-sizer { width: 1%; } /* Adjust gutter */
.grid-item {
  float: left;
  box-sizing: border-box;
  padding: 10px; /* Spacing around items */
  position: relative;
}
.grid-item img {
  display: block;
  width: 100%;
  height: auto;
}
.project-description {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: rgba(0,0,0,0.7);
  color: white;
  opacity: 0;
  transition: opacity 0.3s ease;
}
.grid-item:hover .project-description {
  opacity: 1;
}
/* Add more styles for .project-text-holder, .project-text-inner */
</style>

Sau khi áp dụng các thay đổi về CSS, bạn có thể nhận thấy bố cục đã thay đổi. Để tạo hiệu ứng bố cục dạng thác nước (waterfall layout) như trên các nền tảng như Kuaishou hay Douyin, chúng ta cần cài đặt plugin `vue-masonry`.

Cài đặt thư viện:

cnpm install vue-masonry --save

Sau đó, nhập và sử dụng plugin này trong tệp `main.js` của bạn:

import Vue from 'vue'
import App from './App'
import router from './router'
import { VueMasonryPlugin } from 'vue-masonry'; // Import plugin

import 'bootstrap/dist/css/bootstrap.min.css'; // Import Bootstrap CSS
import './assets/css/main.css'; // Import your custom styles

Vue.use(VueMasonryPlugin); // Use the plugin

new Vue({
  el: '#app',
  router,
  components: { App },
  template: ''
})

Cuối cùng, cập nhật tệp template của bạn để sử dụng các directive của `vue-masonry`:

<div v-masonry :options="masonryOptions" transition-duration="0.3s" item-selector=".grid-item" class="grid">
  <div class="gutter-sizer"></div>
  <div class="grid-sizer"></div>
  <div v-masonry-tile class="grid-item" v-for="(item, index) in blocks" :key="index">
    <!-- Nội dung của từng block -->
  </div>
</div>

Khi quá trình build dự án, hãy chỉnh sửa tệp `config/index.js`. Trong phần `build`, thay đổi thuộc tính `assetsPublicPath` thành đường dẫn tương đối: `assetsPublicPath: './'`.

Ngoài ra, nếu bạn đang sử dụng chế độ router `history`, hãy chuyển sang chế độ `hash` hoặc giữ nguyên chế độ mặc định. Chế độ `history` không được hỗ trợ tốt trong môi trường ứng dụng di động. Sau đó, tiến hành build dự án:

cnpm run build

Vue.js sẽ đóng gói toàn bộ dự án vào thư mục `dist`. Tiếp theo, tải và cài đặt HBuilder (phiên bản phát triển ứng dụng - App Development Edition) từ trang chủ http://www.dcloud.io/hbuilderx.html. Kéo thả thư mục `dist` vào giao diện của HBuilder, sau đó chuột phải và chọn tùy chọn "Convert to Mobile App" (Chuyển đổi thành Ứng dụng Di động).

HBuilder sẽ tự động tạo tệp cấu hình `manifest.json`. Sau khi chuyển đổi hoàn tất, nhấp đúp vào tệp `index.html`, chọn "Run" (Chạy) và sau đó chọn "Browser Test" (Kiểm tra trên trình duyệt) để thử nghiệm.

Nếu gặp phải màn hình trắng, hãy kiểm tra lại cấu hình của Vue.js và đảm bảo rằng bạn đã thiết lập đường dẫn tương đối cho các tài nguyên. Sau khi kiểm tra thành công, chọn thư mục `dist`, chuột phải và chọn "Generate Native Project" (Tạo Dự án Gốc).

Sau khi quá trình đóng gói hoàn tất, bạn sẽ nhận được tệp APK có thể tải về và cài đặt trên điện thoại của mình.

Trải nghiệm ứng dụng sau khi cài đặt sẽ tương đương với ứng dụng Native, với một bộ mã nguồn duy nhất, hoạt động hoàn hảo trên cả máy tính và thiết bị di động. Toàn bộ mã nguồn dự án có sẵn tại https://gitee.com/QiHanXiBei/vue_app.

Thẻ: Vue.js HBuilder Hybrid App Mobile development responsive design

Đăng vào ngày 6 tháng 8 lúc 02:30