前言
前端是我剛入工程師最開始的入口,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。
引用的部分也有些竅門
- 導入為CDN
import workletURL from 'extra-scalloped-border/worklet.js?url'
CSS.paintWorklet.addModule(workletURL)
- 導入為RAW
import shaderString from './shader.glsl?raw'
- 導入為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
用途是定義開發與封裝的設定檔案,這項文檔極其重要。
相似文件:
[email protected]
的vue.config.js
webpack
的webpack.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]:
- 未來瀏覽器支援 ESM
- 放棄 IE 系列 (放棄
Object.defineProperty()
語法,擁抱Proxy
語法) [email protected]
開發人員以JS難以維護專案,[email protected]
更友善TS開發人員[email protected]
有支援[email protected]
的專案生成,但還是一樣由webpack
執行監聽封裝打包setup()
借鏡React-Hook
作法
基本上[email protected]
的寫法有繼續沿用,拓展了幾個介紹頁面
生命週期
Provide / Inject
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()
、computed
或methods
被解析之前。
// 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()
- setup() 內想調用 Vue 的 功能必須先經過 ref 才能帶有 資料的變化
- props 傳進的值 須將其 toRefs 才能資料變化
- setup() 只能訪問 property 不能用組件內的其他
this.*Func
功能 - 可使用生命週期功能
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 API | Hook inside setup |
---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
activated | onActivated |
deactivated | onDeactivated |
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,原因很簡單原廠都不支援了,在前端的發展才不會有包袱