輕前端首選 - Vue.js 3 讀書筆記
1 |
前端?後端?全端?
小閃光問了個問題:非本科轉職資訊走前端是不是比較容易?身為全端老司機,依多年經驗與觀察給了分析:
- 前端技術工具常砍掉重練,大家一起被拉回起跑線,老鳥優勢不明顯
- 前端知識較收斂,打好 HTML、CSS、JavaScript 基礎即能上場
- 前端設計成果回饋直覺,驗收規格明確,也易靠作品集證明實力,新人較有機會被委以重任
- 前端常有修改、調整、修新需求,創造較多工作機會
- 初階前端工作對 Domain Knowhow 需求較低,無經驗新人進場容易
這串不專業分析在臉書意外引發迴響,大家對前端框架與工具放煙火式的生命週期都很有感慨(也可能是讀者圈形成的同溫層),不少人偏愛後端,雖然也得花時間深耕架構、效能議題,熟悉異質系統及了解產業知識,但知識技能較易累積,不會動不動砍掉重練。也有人提醒,前端入門容易,但初階工作常陷入無止境的畫面調整修改,技能(及薪水)成長受限,需另訂長期規劃轉進或突圍。
我把自己定位成全端工程師,多年下來對系統各段都有所涉獵,能從 DB、WebAPI、MVC 一路寫到 HTML/CSS/JavaScript,必要時來點排程、Windows Service 串場,期許自己能成為一條龍服務中的那條龍。(笑)
既已決定走全端路線,沒有一目十行過目不忘的天分、又是需要睡覺的凡人,能撥給前端的時間有限,很難把前端當成終生志業,不管技術怎麼翻新都緊緊跟隨,無怨無悔。因此,在技術選型時,安裝方便、容易上手是首選。
輕前端
早期的 JavaScript 程式庫如 jQuery、Knockout.js、AngularJS (註:我都用過,後二者已乘黃鶴去,唯有 jQuery 仍有一席之地,但隨著 IE 消失及瀏覽器規格漸趨統一,jQuery 的必要性亦大不如昔),用起來沒那麼麻煩,在既有 HTML/WebForm/MVC 網頁加個 <script src="...">
載入程式庫,便能享受新技術。而當代前端框架主流已不是這種玩法,你必須另起專案,按照各框架要求的專案架構將畫面拆解成大小元件、模組,編譯打包後才會生出 HTML、JS、CSS 檔。這意味著若你想在 WebForm/ASP.NET MVC 專案應用這些前端框架,必須捨棄原本的 .aspx、.cshtml 概念,另起一個前端專案,後端改為只留 WebAPI/SSE/WebSocket 接口與其整合。這條路不是不能走,前端框架的複雜能換來架構分明、易維護擴充的好處,但也有代價,你必須付出較高的學習成本高並重構既有專案將前後端分離,再者,若有一天要換用其他前端框架,有極大部分得丟棄重寫。
與傳統 <script src="...">
即插即用的做法相比,當代前端框架像「嵌入式冷氣機」,美觀大方,但安裝費錢費工,重新裝潢時一併規劃進去還好,要裝在既有住家,難逃拆掉天花板重做的命運。而前端牌嵌入式冷氣機又常因某些莫名其妙原因被迫淘汰更換,工程之大不難想像。以我的觀點,若沒有要追求極致的美觀舒適,傳統 <script src="...">
比較像挖個洞走明管連室外機的分離冷氣,好裝好拆,適合不想破壞既有裝潢的屋主,如果你跟我抱持相似想法,應該也會選擇「輕前端」這條路。
照片來源:吊隱式/內嵌式冷氣安裝實例
Vue.js!
我現在寫網頁,簡單需求會用 jQuery 甚至香草 JS 解決,但複雜網頁沒有 MVVM 不如一刀給我個痛快,但之前慣用的 Knockout.js/AngularJS 已走入歷史。當代前端的三大主流框架:React.js、Angular、Vue.js 中,只有 Vue 自許為漸進式框架,保留了 <script src="...">
即插即用的引用方式,可加裝在 WebForm/MVC 網頁就讓你享受 MVVM 的便利,而不用修改專案架構,學習 npm、webpack 等不知哪天會通通換掉的工具。讓專案維持 WebForm/MVC 為主,JavaScript 輔助的精神,對全端工程師負擔較小,也夠應付內部網站或簡單應用。等哪天需要寫出 Gmail、FB 概念的 SPA,再乖乖去學 CLI/npm/webpack 搞定。 (或許這時該「閃開,叫專業的來」才是上策)
下圖是 Vue.js 的 Progressive JavaScript Framework 漸進式 JavaScript 框架概念圖,若走輕前端路線,只會使用 到 Declarative Rendering (即在元件加上 v-bind、v-model 引用 Vue 功能) 及 Component System (自定元件封裝邏輯重複使用),要學的東西不多,不難上手。
圖片來源:Vue.js 與前端生態圈 / 重新認識 Vue.js by Kuro Hsu
前陣子到圖書館借書,順便預約了 Kuro 大大的 重新認識 Vue.js:008 天絕對看不完的 Vue.js 3 指南,小排了一下前陣子拿到書。全書共六章,而寫輕前端只需讀前兩章(基礎入門及元件系統),嘿,我成功在 8 天內看完。而最酷的是書裡的第一二章範例都有放上 Github,猜猜怎麼著?每個範例都是一個獨立 HTML 檔,直接用瀏覽器開啟就能完整執行,符合輕前端精神。
筆記(自用)
讀完照例留下筆記,方便日後拾回記憶。(筆記以瑣碎關鍵字為主,細節請參閱原書或線上版)
- Vue 0.8 2014 HackerNews / Reddit 公開
- Vue 1.0 2015 發佈,輕量級 AngularJS,vue-router, vuex, vue-cli
- Vue 2.0 2016 Oct,React Virtual DOM,效能大增
- Vue 3.0 2019-09-18 核心以 TypeScript 重寫,效能提升,體積下䧏
- 與 AngularJS,React 最大不同 - Progressive JS Framework
宣告式渲染 + 元件系統 - 傳統為指令式程式設計 Imperative Programming,自掛 onchange, onclick 事件
- Vue 3 特色
- Fragment 元件不限單一根元素
- object.defineProperty 換成 Proxy API,效能好,支援深度偵測物件及陣列更新
- 模板編譯:靜態節點優化,VDOM 更新只遍歷動態節點,減少浪費
- Teleport, Suspense 功能性元件
- Composition API, 取代 mixins (createApp() 寫法叫 Options-Based)
- setup, ref 語法糖
- 用 _ 或 $ 開頭的屬性不會被 Proxy 代理
- Clone 做法:ES6 解構 clone = 為淺 Copy,深 Copy 用 JSON clone = JSON.parse(JSON.stringify(srcObject))
- vm.$data 可取得原始資料物件
- methods 方法不能用 Arrow Function () => ,因為 this 會指向 window
- computed 只會在涉及屬性有異動時觸發重算
- computed 可自訂讀寫邏輯 computed: { jpy { get() set() }
- v-bind, v-on, v-model... 這些叫指令 Directive
- v-model 雙向綁定,限表單元素,故不能用在 DIV,設 contenteditable 也不行
- v-model 綁 SELECT 若未對映到相符值,在 iOS 會有選不到第一個選項的狀況(沒觸發change),解法為第一個 option 設空值並 disabled
- 修飾子 v-model.lazy 由按揵觸發改 change 觸發、v-model.number 將值視為數字、v-model.trim 取字串時先 Trim()
- v-text 或 {{ text }} 設文字內容
- v-html 注入 HTML 內容
- v-once 只更新一次
- <div v-pre>{{ text }}</div> 不當模板直接輸出
- <div v-bind:class="{active:isActive,'text-danger':hasError}">
- <div :style="msgStyle">,而 computed msgStyle() { return { 'border': ...., 'color': ... } }
- v-on 事件,<button v-on:click="plus">, plus(event)
<button @click="plus(amount, $event)"> + plus(amount, event)
event.target <= 觸發元素 - 修飾子
v-on.stop 加 event.stopPropagation()
v-on.prevent 加 event.preventDefault()
v-on.capture 會觸發外層元素事件再內層
v-on.self 只觸發本身,不含子元素浮上來的
v-on.once 只觸發一次
v-on.passive 等同 addEventListener passive 常用於 scroll 事件 - 鍵盤修飾子 .enter, .tab, .delete, .esc, .space, .up, .down, .left, .right, .ctrl, .alt, .shift, .meta (Windows 鍵,mac Command)
- @keydown.enter.exact=".." Enter 可,Ctrl-Enter 不可
- 滑鼠修飾子 .left, .right, .middle
- v-model="msg" 可拆解成 :value="msg" @input="msg = $event.target.value"
- v-if 元素會動態刪增,也會重複使用 DOM 元素,Vue 2 要加 key 防殘留,Vue 3 己修正
- v-for="item in items", v-for="(item,index) in items", v-for="propValue in someObject", v-for="(val, key) in someObject", v-for="pageNo in 10" (1 ~ 10)
- <template v-for="i in links"> 隱形容器
- v-for 要篩選排序用 computed 自訂計算屬性配 JS filter(), sort()
- v-for="i in list" :key="i.id" 確保識別 DOM 重複利用時不殘留
- Vue 2 使用 Object.defineProperty() 注入 setter,getter 偵測異動,對陣列操作加裝偵測,但有可能感應不到。Vue 3 用 Proxy API,無此問題。
- 元件生命週期(Vue3):(beforeCreate, created, beforeMount, mounted 建立階段), (beforeUpdate, updated 更新階段), (beforeUnmount, unmounted 銷毁階段), errorCaptured (CompositionAPI 的 Hook 不同)
- data, prop, computed 在 created 後才能存取。取得 template 進入 beforeMount。將 template 內容替換後進入 mounted,可以操作 DOM(querySelector, addEventListener),不然操作結果可能被 Vue 蓋掉
- watch 偵測狀況更新多次,也只會在 Queue 出現一筆,在下個事件迴圈(Event Loop 或 Tick)更新 DOM。所以你如果在 watch 中透過 this.$el 存取綁定的DOM,其狀況仍是更新前。
解法1 setTimout 大絕、解法2 this.$nextTick(() => { this.$el... }) - 元件種類:展示型元件 (Presentation) UI 呈現、容器型元件(Container)與Server溝通、互動型元件(Interactive)elementUI/bootstrap UI library、功能性元件(Functions)不渲染內容,功能或機制的封裝
- Vue3 app.component('my-component', )
- app.use(...), app.mixin(...), app.component(...), app.directive(...), app.mount('#app') 不像 Vue2 掛在 Vue 全域物件
- 區域性元件 Vue.createApp({ components: { 'my-component': ... }})
- ES Module 寫法
import myComponent from './com/my-com.js' const app = Vue.createApp({ components: { myComponent } ... })
- 元件名稱建議用連字線與標準元素區隔 <todo-item>,註冊時用 Pascal-Case 'TodoItem' 或 'todo-item',瀏覽器解析標籤無大小寫區別,模板一律用 <todo-item>
- x-template 封裝
<script id="media-block" type="text/x-template">
app.component('media-block', template: '#media-block') - Vue3 支援多個根元素,此時 this.$el 不能用,改用 <div ref="title"> + this.$refs.title 存取
- data 記得 data() { return } 傳回新物件,避免多元件共用 data 產生污染
- 恢復初始狀態 resetData() { Object.assign(this.$data, this.$options.data()); }
- 元件與外部溝通使用 props + v-bind,自身的則可用 data() { return }
props 屬性採 Camel 命名,引用時要改連字線,例 props: ['someProp'] -> v-bine:some-prop。
v-bind:some-prop 或 :some-prop 可綁定外部狀態,若寫成 some-prop="string" 則傳入字串 - props 可指定型別:String, Number, Boolean, Array, Object, Date, Function, Symbol、指定預設值、設定驗證規則(函式內不能用 this)
props: { 'prop1': { type: Number, validator: value => value > 0 }, 'prop2': { type: [String, Number], default: 'Hello' } }
- 注意:用 props 傳物件,若在元件修改其屬性,物件內容會因此異動,為不好的設計。可善用自動解構:book: { name:..,author:.. }、props: ['name', 'author'] 則寫成 v-bind="book",name, author 會自動對映上
- 未定義的 props 或事件會傳到子元件上
<my-com :class="parent-css"></my-com>
而 my-com props 沒有 props: ['class'],template: '<div class="child-css"></div>'
渲染結果 class 會包含 child-css 及 parent-css 屬性值
若有多個根節點,元件 template 可用<main v-bind="$attrs">...
指定接收 - 元件 props 屬性是不能修改的,解法為用 data 承接
props: ['name','author'], data() { return { bookName: this.name, bookAuthor: this.author }} - 元件也可以遞迴,例如:ViewTree
- Vue 父子元件溝通口訣:Props In,Event Out
- 事件更新範例:
呼叫端:props: ['name','author'], data 宣告 bookInfo: { bookName:this.name, bookAuthor:this.author } watch: { bookInfo: { deep: true, //記得要加 handler(val) { this.$emit('update', val) } } }
<my-com @update="updateBook">
methods: { updateBook(val) } - 父子元件直接溝通巧門 this.$parent,
<my-com ref="child1">
this.$refs.child1 - 子元件的 v-model:props: ['modelValue'], $emit('update:modelValue', value)
- 父孫溝通:createApp 時
provide() { return { provideMsg: this.msg }}
子孫元件用inject: ['provideMsg']
取得。但這種做法異動時不會更新,要用provide() { return { provideMsg: Vue.computed(() => this.msg) } }
+inject: ['provideMsg']
+{{ provideMsg.value }}
- EventBus 在 Vue3 己不建議使用,請用 vuex,一個受控的 store;或是 Vue Composition API
- 動態元件:例如 註冊三個元件 tab-home, tab-posts, tab-archive,
<component :is="currTabCom"></component>
,而 computed: { currTabCom() { return `tab-$`; } } - 與 v-show 相比,:is 無法保留元件狀態(切換走再切回來,輸入值會不見),解法是用
<keep-alive><component :is=...></component></keep-alive>
(註:keep-alive 也可搭配 v-if 使用) - keep-alive 可用 include, exclude 指定哪些元件狀態要保留,元件要加 name: '...' 供識別,並支援 RegExp 及陣列
<keep-alive :include="/tab-(home|posts)/">
<keep-alive :include="['tab-home', 'tab-posts']">
另有 max="n" 指定保存暫存子元件數量 - keep-alive 專用生命周期 Hook: activated, deactivated
- 非同步元件 Async Component
const AsyncComp = Vue.defineAsyncComponent(() => new Promise((resolve, reject) => { resolve({ template: '<div>I am async!</div>' }) }) ); app.component('async-example', AsyncComp);
- Slot 插槽 / 編輯作用域(元件的scope)
- 在元件模板加入
<slot></slot>
會代入父元素用<my-com><blah></blah></my-com>
塞入的 blah。<slot name="header"></slot>
具名插槽,父層用<template v-slot:header></template>
分別插入指定插槽 - 動態切換具名插槽:
<template v-slot:[dynamic_slot_name]>
- teleport (Vue 3 +)
<teleport to="body"></teleport>
將內容元素放在 body 層 (不會刪除後重建,會有 keep-alive 保留效果 - transition 漸變
定義 CSS .v-enter-from/.v-enter-active/.v-enter-to 進場過程的三個樣式
.v-leave-from/.v-leave-active/.v-leave-to 消失的三個樣式
<transition><div v-show="..."></transition>
<transition name="slide">
-->.slide-enter-active
transition 若有多個元素用 if 切換,要用 v-if, v-else-if,不然就是改用 transition-group
Hooks 函式:before-enter, enter, after-enter, enter-cancelled, before-leave, leave, after-leave, appear, after-appear, appear-cancelled
BUT!! 其實用 CSS 動畫就好了
要反覆播放<div ref="block" :class="{shake: noActivated}">Block</div> <button @click="noActivated = true">Shake It!</button> methods: { reactivated() { this.noActivated = false; } }, mounted() { this.$refs.block.addEventListener('animationend', this.reactivated); }, unmounted() { this.$ref.block.removeEventListener('animationend', this.reactivated); }
- 現成動畫特效 Animate.css (npm install animate.css --save, 也有 CDN 版)
- 非輕前端部分:(未來有用到再學)
- Vue CLI
- Vue SFC
- Vue Router
- Vuex 狀態管理
- Composition API
- ES6 必備知識
- var / let / const 的區別
- ES Module
- Arrow Function Expression 與 this
- Template Literals `...$...`
- Destructuring Assignment const { propA, propB } = object_with_propA_and_propB; const [x, y]= [1,2,3,4];
- Spread Operator
const arr = ['to-insert', ...another_array ]
Rest Operatorconst {x, y, ...z} = { x:1, y:2, a:3, b:4 }
則z = { a:3, b:4 }
- Promise, await, async
Notes of reading book: Vue.js 3 guide.
Comments
# by jc
黑大好 vue 組件內資料傳遞推薦可以用 provide inject 來替代 vuex 另外若是要用 vuex,但又希望 typescript 強型別,推薦可以用 pinia