17

輕前端筆記 - 使用 Vue SFC (.vue 元件)

 2 years ago
source link: https://blog.darkthread.net/blog/vue3-sfc-loader/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

使用 Vue SFC (.vue 元件)-黑暗執行緒

前幾天提到在 ASP.NET 使用輕前端 整合 Vue.js 做 MVVM,若遇到伺服端元件輸出 script/style 的注意事項。FB 留言區 Died Liu 分享透過 defineAsyncComponent 方法載入 .vue 元件檔的做法,不需要 npm、webpack 也能享受用 .vue 封裝元件邏輯的好處,讓我十分心動。

複雜的前端自訂元件,通常會包含 HTML 模板、JavaScript 程式跟 CSS 樣式,最理想做法是將該元件需要的東西放在一起,引用時一個檔案搞定,如此能簡化開發、測試及部署複雜度,較符合模組化設計。Vue.js 也有這種單檔元件 (Single File Component,SFC) 概念,副檔名為 .vue,格式如下,將 HTML 模板、JavaScript 程式及 CSS 樣式分別放入 <tempalte>、<script>、<style> 存成 .vue 檔案:

<script>
export default {
  data() {
    return {
      greeting: 'Hello World!'
    }
  }
}
</script>

<template>
  <p class="greeting">{{ greeting }}</p>
</template>

<style>
.greeting {
  color: red;
  font-weight: bold;
}
</style>

不過,之前看到的範例,要用 SFC 都需要結合 JavaScript Module (起手式是 import MyComponent from './MyComponent.vue') 配合 Vue CLI、webpack 打包使用,我一直覺得輕前端與它無緣。所以,當看到 createApp 時 import('./some.vue') 寫法,我眼睛為之一亮。

createApp({ /*...*/ 
  components: { 
      AsyncComponent: defineAsyncComponent(() => import('./components/AsyncComponent.vue'))  
  } 
})

經過一番研究與摸索,總算試出輕前端使用 .vue 的方法。由於 SFC 的獨有格式必須先經過解析轉譯成 JavaScript 程式碼,才能在瀏覽器執行,Vue CLI 打包部署時會使用 vue-loader 進行轉譯;輕前端要從瀏覽器直接載入 .vue,則需依賴第三方程式庫 vue3-sfc-loader進行前置處理。參考

vue3-sfc-loader 使用範例如下:

<!DOCTYPE html>
<html>

<body>
    <div id="app">
        <drk-timer secs="20"></drk-timer>
        <drk-timer></drk-timer>
    </div>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue3-sfc-loader"></script>
    <script>
        const options = {
            moduleCache: { vue: Vue },
            async getFile(url) {
                //if (url === '/myComponent.vue')
                //    return Promise.resolve(vueString);
                const res = await fetch(url);
                if (!res.ok)
                    throw Object.assign(new Error(url + ' ' + res.statusText), { res });
                return await res.text();
            },
            addStyle(textContent) {
                const style = Object.assign(document.createElement('style'), { textContent });
                const ref = document.head.getElementsByTagName('style')[0] || null;
                document.head.insertBefore(style, ref);
            },
            log(type, ...args) { console[type](...args); },
            compiledCache: {
                set(key, str) {
                    // naive storage space management
                    for (; ;) {
                        try {
                            // doc: https://developer.mozilla.org/en-US/docs/Web/API/Storage
                            window.localStorage.setItem(key, str);
                            break;
                        } catch (ex) {
                            // handle: Uncaught DOMException: Failed to execute 'setItem' on 'Storage': Setting the value of 'XXX' exceeded the quota
                            window.localStorage.removeItem(window.localStorage.key(0));
                        }
                    }
                },
                get(key) {
                    return window.localStorage.getItem(key);
                },
            }
        }
        const { loadModule } = window['vue3-sfc-loader'];
        var vm = Vue.createApp({
            components: {
                drkTimer: Vue.defineAsyncComponent(() => loadModule('./timer.vue', options))
            }
        }).mount('#app');
    </script>
</body>

</html>

使用 vue3-sfc-loader 時要先宣告一個 options 物件,其中包含:

  • moduleCache 屬性
    一般寫 即可
  • getFile() 方法
    依 url 取回 .vue 內容,也可用其他方法傳回字串,不一定要從伺服器下載。這段自訂邏輯非常好用,提供無限的擴充彈性,例如:正式上線時可打包多個 .vue 一次下載,減少 HttpRequest 次數
  • addStyle() 方法
    決定如何將 <style<區塊內容掛到目前的網頁
  • log()
    訊息輸出方式
  • compiledCache
    將編譯結果存入 localStorage 重複使用以提升效能

要載入 .vue 的地方則寫成:

components: {
    drkTimer: Vue.defineAsyncComponent(() => loadModule('./timer.vue', options))
}

我寫了一個倒數計時器當練習:

我人生第一個 .vue 檔,HTML、JavaScript 跟 Style 氣刀體一致的感覺真棒!

<template>
  <div class="timer" v-bind:class="[status]">
    <div class="screen">{{ mm }}:{{ ss }}</div>
    <div class="buttons">
      <button @click="start()">開始</button>
      <button @click="pauseOrResume()">
        {{ status == "P" ? "繼續" : "暫停" }}
      </button>
      <button @click="reset()">重置</button>
    </div>
  </div>
</template>
<script>
export default {
  name: "drk-timer",
  props: ["secs"],
  data() {
    return {
      //W-Waiting, R-Running, P-Paused, O-Timeout
      status: "W",
      lastTime: 0,
      totalTime: 0,
      countdownSecs: 0,
      intHandle: 0,
    };
  },
  computed: {
    remainingSecs() {
      if (!this.countdownSecs) return 0;
      return Math.max(
        0,
        this.countdownSecs - Math.round(this.totalTime / 1000)
      );
    },
    mm() {
      return Math.floor(this.remainingSecs / 60)
        .toString()
        .padStart(2, "0");
    },
    ss() {
      return (this.remainingSecs % 60).toString().padStart(2, "0");
    },
  },
  watch: {
    secs() {
      this.start();
    },
  },
  methods: {
    start() {
      this.reset();
      this.status = "R";
    },
    pauseOrResume() {
      if (this.status == "R") this.status = "P";
      else if (this.status == "P") {
        //resume
        this.lastTime = new Date().getTime();
        this.status = "R";
      }
    },
    reset() {
      this.lastTime = new Date().getTime();
      this.totalTime = 0;
      this.status = 'W';
    },
    tick() {
      if (this.status == "R") {
        const currTime = new Date().getTime();
        this.totalTime += currTime - this.lastTime;
        this.lastTime = currTime;
      }
      if (this.mm + this.ss == 0) {
        this.status = "O";
      }      
    },
  },
  mounted() {
    this.countdownSecs = parseInt(this.secs);
    if (!this.countdownSecs) this.countdownSecs = 180;
    this.intHandle = setInterval(this.tick, 500);
  },
  unmounted() {
    clearInterval(this.intHandle);
  },
};
</script>
<style scoped>
.W .screen { color: white; }
.R .screen { color: lightgreen; }
.P .screen { color: #888; }
.O .screen { color: rgb(255, 7, 7); }
.timer { 
  display: inline-block; margin: 3px;
  background-color: #ddd;
}
.buttons { padding: 6px; text-align: center; }
@font-face {
  /* https://torinak.com/font/7-segment */
  font-family: "7segments"; font-weight: 200; font-style: normal;
  src: url("data:application/font-woff;base64,d09GRgABAAAAA...略...LAAACxFAeMA") format("woff");
}
.screen {
  font-family: "7segments"; font-size: 64px; width: 140px;
  margin: 6px; padding: 6px; background-color: #222;
  border: 2px solid #aaa; border-style: none none solid solid;
}
</style>

打通在輕前端用 .vue 寫元件的關卡,元件模板及程式不用硬塞進 .js,連樣式都能放在一起,回到文明的開發方式,感覺前進了一大步,讚!!

(範例已上傳到 Github)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK