Vue - build một phần

3/5/2022 vue

Ban đầu trang này của mình dùng Vuejs do mình quen code Vue hơn. Và vì chỉ là một trang note cơ bản nên mình muốn là SPA và phần content phải là static file. Tuy nhiên phần get list mình muốn sử dụng API, một phần vì dễ control việc paging, một phần vì sau này khi filter theo các điều kiện cũng dễ (như filter theo tag, category...). Thật ra cũng có thể build static file cho các page như tag, category. Rất nhiều framework đã hỗ trợ nhưng mình muốn build từ đầu, mà implement tính năng đó khá phức tạp và mất thời gian. Nên thôi mình viết API cho việc get list, đằng nào cũng cần backend cho việc viết bài, comment…

#Quá trình phát triển

Ban đầu, khi publish một bài viết mình sẽ generate 1 file HTML cùng với images của nó vào 1 folder riêng. Khi access bài viết sẽ load nội dung file HTML tương ứng và display bằng Directive v-html của Vuejs. Cách này mặc dù đáp ứng được yêu cầu SPA của mình nhưng mỗi lần vào bài viết lại cần request để lấy content HTML làm tốc độ hiển thị khá chậm.

Do đó, mình chuyển sang cách khi publish sẽ generate bài viết dưới dạng một Vue component vào folder của project Frontend. Sau đó build lại project Frontend bao gồm tất cả các component bài viết đã generate. Lúc này trang note của mình sẽ đáp ứng được SPA hoàn toàn. Bên cạnh đó mình sử dụng Vue router lazy loading để tách riêng built file của các bài viết.

Cách này khá ổn, tuy nhiên có 2 vấn đề:

  1. Click vào bài viết mới bắt đầu load file tương ứng khiến tốc độ hiển thị bài viết cũng giảm phần nào. (vấn đề 1)
  2. Mỗi lần thêm bài viết lại phải build lại cả project Frontend, việc này khá tốn thời gian và lãng phí. (vấn đề 2)

Về vấn đề 1, mình tìm hiểu được 2 kỹ thuật là Link prefetchingIntersection Observer API thông qua tham khảo source code của VitePress. Sử dụng Intersection Observer API để theo dõi việc một thẻ a được hiển thị trên viewport. Khi thẻ a của 1 bài viết được hiển thị thì append link prefetching với chunk file tương ứng với bài viết đó. Do đó bài viết được load từ trước khi click vào nên độ trễ sẽ rất thấp.

Kết quả

Để lấy được đúng tên file chunk thì khi build mình sẽ tạo một file js, chứa biến global mapping giữa slug bài viết và file name. Như trên hình, file đó tên là note-chunk-map.js.

Về vấn đề 2, đầu tiên mình sẽ build project Frontend mà không bao gồm các component bài viết trước, rồi deploy. Mỗi khi publish bài viết mình sẽ tạo Vue component cho bài viết đó vào 1 folder cụ thể trong project Frontend. Viết script chỉ build cho các component trong folder đó. Lúc này để tránh build cả Vue, mình sử dụng option external: ['vue'] của Rollup.js (do mình đang sử dụng Vite, và Vite sử dụng Rollup cho việc bundle). Lúc này phần import từ Vue của component bài viết có dạng:

import {
  defineComponent as i,
  resolveComponent as r,
  openBlock as s,
  ...
} from "vue";

Công việc tiếp theo là mình phải mapping các tên import from "vue" với file chunk vendor chứa Vue mà trước đó mình đã build được ở lần build và deploy project Frontend (không bao gồm các component bài viết). Vấn đề xảy ra là các exports của Vue trong file chunk vendor đã bị minify (không còn các tên gốc dạng defineComponent, resolveComponent...).

Để xử lý việc này, mình tạo một module vue-modules.ts export tất cả từ Vue và config như là một entry point để Rollup bundle ra một file riêng:

// src/vue-modules.ts
export * from 'vue';

// vite.config.ts
export default defineConfig({
  plugins: [vue()],
  build: {
    rollupOptions: {
      input: {
        app: resolve(__dirname, 'index.html'),
        'vue-modules': resolve(__dirname, 'src/vue-modules.ts')
      }
    }
  }
});

Và đây là kết quả:

// vue-modules.<hash>.js
import {
  ...
  d as F,
  r as Wa,
  ...
} from './vendor.<hash>.js';
export {
  ...
  F as defineComponent,
  Wa as resolveComponent,
  ...
};

Bây giờ mình chỉ cần replace from "vue" thành from "vue-modules.<hash>.js". Bằng cách này mình sẽ không phải lo việc Vue có thể thay đổi tên export, hay Rollup có minify thành một tên khác ở lần build sau nữa. Và do đó mình đã tách riêng được việc build cả project Frontend và build bài viết.

Về mặt logic là vậy, còn khi implement mình phải làm khá nhiều việc:

  • Tạo file bài viết dưới dạng Vue SFC ra 1 thư muc riêng
  • Build bài viết:
    • Sau khi build thành công, replace from "vue" thành from "vue-modules.<hash>.js"
    • Lấy tên file, update nội dung vào file note-chunk-map.js
    • Update file HTML để tránh browser cache file note-chunk-map.js

Và đây đây là kết quả: vite config.


Về sau khi thêm các thành phần mới, trang càng phức tạp thêm nên mình đã không tiếp tục cách làm này nữa, vì chỉnh sửa khá mất thời gian. Thay vào đó, giờ mình đã chuyển sang dùng Nextjs pepe_hug. Tuy hơi hơi tiếc công sức nhưng mà thôi.