返回
Featured image of post VueJS - ReVue - 認識 Vue3 和 Vite 環境安裝篇

VueJS - ReVue - 認識 Vue3 和 Vite 環境安裝篇

Vue3.x 也是好一陣子了,更新一下對於Vue的認識,也認識一下 Vite(並非放棄 vue-cli)

前言

前端是我剛入工程師最開始的入口,Vue2.x 算是我剛入行的時候學習的,過了這幾年也越來越多企業有採納React以外的前端框架,不外乎就是 Vue,前些年最熱門的 CSS 框架是 Bootstrap ,當然也還是很多的人使用,近年串紅的 tailwindcss 也是陸陸續續越多人使用,前端的領域蠻複雜且變化速度算是非常的快速。


Vite - Next Generation Frontend Tooling

注意! 此框架封裝方式為 ESM (Native ES modules) 所以請審思框架選用。
Vue 官網官方CLI工具以外的就是 Vite,所以想嘗試看看這款新的 CLI工具。

特點

  • Vue3.x - 直接使用 Vue3.x 進行 template 配置。
  • rollup - 打包工具 對於長期使用 webpack 的使用者想更了解 rollup 工具 可能要改變一下。

兼容性注意

  • NodeJS >= 12.0.0

建置專案

# npm 6.x
npm init vite@latest my-vue-app --template vue

# npm 7+
npm init vite@latest my-vue-app -- --template vue

靜態資源

預設資料夾為 /public,所以可以在 public資料夾製作分類 css/js/img可以輕鬆引用。

  • 不被引用(例如 robots.txt)
  • 必須保持原文件名(無hash)
  • …或者不想引入該資源,只想得其 URL。

引用的部分也有些竅門

  1. 導入為CDN
import workletURL from 'extra-scalloped-border/worklet.js?url'
CSS.paintWorklet.addModule(workletURL)
  1. 導入為RAW
import shaderString from './shader.glsl?raw'
  1. 導入為Worker
// 在 Build 時 chunk
import Worker from './shader.js?worker'
const worker = new Worker()
// sharedworker
import SharedWorker from './shader.js?sharedworker'
const sharedWorker = new SharedWorker()
// 為 base64 字符串
import InlineWorker from './shader.js?worker&inline'

vite.config.js

用途是定義開發與封裝的設定檔案,這項文檔極其重要。
相似文件:

套件引用

// vite.config.js
import { defineConfig } from 'vite'
import typescript2 from 'rollup-plugin-typescript2'
import image from '@rollup/plugin-image' // vite Rollup 套件,注意 `enforce` 標籤與使用說明。
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [
    vue(),
    /**
    * 強制套件排序:enforce
    * `pre`:在 Vite 核心套件之前使用 
    * `post`:在 Vite 核心套件之後使用
    **/
    { ...image(), enforce: 'pre' },
    /**
    * 按需應用:apply
    * `build`:在打包時使用
    * `serve`:在運行時使用
    **/
    { ...typescript2(), apply: 'build' }
  ]
})
vite 套件列表

Build 自定義

預設就已經有定義好一些方式了,可以在自訂去更改 rollout 設定的方式:

// vite.config.js
module.exports = defineConfig({
  build: {
      // `vite build`
    rollupOptions: {
      // https://rollupjs.org/guide/en/#big-list-of-options
    },
    watch: {
      // `vite build --watch`
      // https://rollupjs.org/guide/en/#watch-options
    }
  }
})

環境變量 env

// [mode] 就是 package script 所指定的版本
.env                # 所有情况下都會加载
.env.local          # 所有情况下都會加载,但會被 git 忽略
.env.[mode]         # 只在指定模式下加载
.env.[mode].local   # 只在指定模式下加载,但會被 git 忽略

為了防止環境變量暴露給客端,只有以 VITE_ 為前綴才會經過 vite

VITE_SOME_KEY=123 // 使用方式 `import.meta.env.VITE_SOME_KEY`

[email protected]

當然 [email protected] 大概有這些原因導致開發 [email protected]

  1. 未來瀏覽器支援 ESM
  2. 放棄 IE 系列 (放棄 Object.defineProperty() 語法,擁抱 Proxy 語法)
  3. [email protected]開發人員以JS難以維護專案,[email protected]更友善TS開發人員
  4. [email protected] 有支援 [email protected] 的專案生成,但還是一樣由 webpack 執行監聽封裝打包
  5. setup() 借鏡 React-Hook 作法

基本上[email protected]的寫法有繼續沿用,拓展了幾個介紹頁面

生命週期

圖片來源來至 https://v3.cn.vuejs.org/guide/instance.html
圖片來源來至 https://v3.cn.vuejs.org/guide/instance.html

Provide / Inject

圖片來源來至 https://v3.cn.vuejs.org/guide/component-provide-inject.html
圖片來源來至 https://v3.cn.vuejs.org/guide/component-provide-inject.html
default時,provide/inject 並不是響應的,若父子更動其值是互不干涉的,若要其改值需使用 ref ,下方Composition API 時會介紹

app.component('todo-list', {
  // ...
  provide() {
    return {
      todoLength: Vue.computed(() => this.todos.length)
    }
  }
})

app.component('todo-list-statistics', {
  inject: ['todoLength'],
  created() {
    console.log(`Injected property: ${this.todoLength.value}`) // > Injected property: 5
  }
})

Reactivity API - 響應式 API

  • Basic Reactivity
  • Refs
  • Computed & Watch
  • Effect Scope 略過

Basic Reactivity

reactive
/**
* reactive()
* TypeScript:
* function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
*/
const count = ref(1)
const obj = reactive({ count })
// 更新 `obj.count` 也會更新 `count` ref
count.value++
console.log(count.value) // 2
console.log(obj.count) // 2

Refs

ref
/**
* ref()
* TypeScript:
* interface Ref<T> { value: T }
* function ref<T>(value: T): Ref<T>
*/
const count = ref(1)
const obj = reactive({ count })
// 更新 `obj.count` 也會更新 `count` ref
count.value++
console.log(count.value) // 2
console.log(obj.count) // 2
toRef
const state = reactive({ foo: 1, bar: 2 })
const fooRef = toRef(state, 'foo')
fooRef.value++
console.log(state.foo) // 2
state.foo++
console.log(fooRef.value) // 3
// 將 prop 的 ref 傳遞给下去時,`toRef` 很有用
export default {
  setup(props) {
    useSomeFeature(toRef(props, 'foo'))
  }
}
toRefs
const state = reactive({ foo: 1,bar: 2 })
const stateAsRefs: { foo: Ref<number>,bar: Ref<number> } = toRefs(state)

state.foo++
console.log(stateAsRefs.foo.value) // 2

stateAsRefs.foo.value++
console.log(state.foo) // 3

toRefs 只會為原Object 包含的 property 生成 ref
若為特定的 property 創建 ref,則使用 toRef

Computed & Watch

這部分和 [email protected] 的其實差不多,因為 [email protected] 希望程式人員可以往TS邁進 解釋了TS的註解

Computed
const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: val => {
    count.value = val - 1
  }
})

plusOne.value = 1
console.log(count.value) // 0
// read-only
function computed<T>(
  getter: () => T,
  debuggerOptions?: DebuggerOptions
): Readonly<Ref<Readonly<T>>>

// writable
function computed<T>(
  options: {
    get: () => T
    set: (value: T) => void
  },
  debuggerOptions?: DebuggerOptions
): Ref<T>
Watch

與 this.$watch 完全等效,不多介紹


Composition API - 組合式 API

  • setup()
  • Lifecycle Hooks
  • Provide / Inject
// 這是個]常見的一個component
// src/components/UserRepositories.vue
export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: { 
      type: String,
      required: true
    }
  },
  data () {
    return {
      repositories: [], // 1
      filters: { ... }, // 3
      searchQuery: '' // 2
    }
  },
  computed: {
    filteredRepositories () { ... }, // 3
    repositoriesMatchingSearchQuery () { ... }, // 2
  },
  watch: {
    user: 'getUserRepositories' // 1
  },
  methods: {
    getUserRepositories () {
      // 使用 `this.user` 獲取用戶
    }, // 1
    updateFilters () { ... }, // 3
  },
  mounted () {
    this.getUserRepositories() // 1
  }
}

setup() 中應該避免使用 this,因為它找不到組件實例。
setup() 的調用發生在 data()computedmethods被解析之前。

// src/composables/useUserRepositories.js

import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch } from 'vue'

export default function useUserRepositories(user) {
  // [email protected] 之後的 ref 是帶有響應的 在 setup 內得註冊
  const repositories = ref([])
  const getUserRepositories = async () => { repositories.value = await fetchUserRepositories(user.value) }
  // 可以 `setup` 內使用 生命周期鉤子 ex: `mounted` 像 `onMounted`
  onMounted(getUserRepositories) // 在 `mounted` 時使用 `getUserRepositories`
  watch(user, getUserRepositories) // 在 user prop 使用監聽

  return {
    repositories,
    getUserRepositories
  }
}
// src/composables/useRepositoryNameSearch.js

import { ref, computed } from 'vue'

export default function useRepositoryNameSearch(repositories) {
  const searchQuery = ref('')
  // computed 在 [email protected] 版本也可以在組件外做計算
  const repositoriesMatchingSearchQuery = computed(() => {
    return repositories.value.filter(repository => { return repository.name.includes(searchQuery.value) })
  })

  return {
    searchQuery,
    repositoriesMatchingSearchQuery
  }
}
// src/components/UserRepositories.vue
import { toRefs } from 'vue'
import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import useRepositoryFilters from '@/composables/useRepositoryFilters'

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: {
      type: String,
      required: true
    }
  },
  setup(props) {
    const { user } = toRefs(props) // 若要帶著轉換 value 必須使用 toRefs() in setup()

    const { repositories, getUserRepositories } = useUserRepositories(user)
    const { searchQuery, repositoriesMatchingSearchQuery } = useRepositoryNameSearch(repositories)
    const { filters, updateFilters, filteredRepositories } = useRepositoryFilters(repositoriesMatchingSearchQuery)

    return {
      repositories: filteredRepositories,
      getUserRepositories,
      searchQuery,
      filters,
      updateFilters
    }
  }
}

setup()

  1. setup() 內想調用 Vue 的 功能必須先經過 ref 才能帶有 資料的變化
  2. props 傳進的值 須將其 toRefs 才能資料變化
  3. setup() 只能訪問 property 不能用組件內的其他 this.*Func 功能
  4. 可使用生命週期功能
import { toRef } from 'vue'
export default {
  props: {
    title: String
  },
  setup(props: any, context) {
    // 需這樣才能註冊
    const title = toRef(props, 'title')
    console.log(title.value)
    // Attribute = $attrs
    console.log(context.attrs) 
    // slot = $slots
    console.log(context.slots) 
    // 觸發事件 = $emit
    console.log(context.emit) 
    // 暴露公共 property (函数)
    console.log(context.expose)
  }
}

Lifecycle Hooks

Options APIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
activatedonActivated
deactivatedonDeactivated

Provide / Inject

首先建立一個組件

<!-- src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
import MyMarker from './MyMarker.vue'

export default {
  components: { MyMarker },
  // 使用 Provide
  provide: {
    location: 'North Pole',
    geolocation: { longitude: 90, latitude: 135 }
  }
}
</script>

添加響應式 ref

<!-- src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue'

export default {
  components: { MyMarker },
  setup() {
    const location = ref('North Pole')
    const geolocation = reactive({ longitude: 90, latitude: 135 })

    provide('location', location)
    provide('geolocation', geolocation)
  }
}
</script>

property 若有更新,MyMarker 組件也會更新


優質好文

結語

[email protected],算是在我剛接觸程式人員選用的框架,也導入專案進而維護,的確也是順利完成專案,那時候對於 mixin 用得著迷,但也覺得用起來說不上怪異,但直到專案越大不得不說 Build 的時間也是越來越長,後續也想往其他領域發展和認知,所以就開始走擁抱後端,才發現前端的學習曲線原來這麼的高攀(個人認為)
對於 Build 的認知我最開始是使用 Gulp,然後才遇到 [email protected],記得當時的webpack散落在專案內大大小小的套件組裝起來後變成vue.config.js(其實也是webpack封裝),近期 esbuild.js 殺出一個封裝高度,改天來去玩玩,看看是否能夠優化專案。
[email protected],我對於react-hook,推出的時候也是蠻驚喜的,這種感覺在[email protected]身上也感受得到,有機會也是希望可以運用在專案內,目前會考慮 React 開發專案(運行中),比較關係因為開發人員生態其實兩邊的擁護者都很多,比較多平台的時候會發現在native的生態,可能react-native擁護人更多和市場上既有的專案也存在,當然也有與其 native 不如使用 flutter 甚至是原生,這也是考慮的範疇其實。
我個人是蠻期待主要是瀏覽器使用者確確實實的拋棄IE11,原因很簡單原廠都不支援了,在前端的發展才不會有包袱

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus