Vue 筆記 === ## Vue 簡單用法 [TOC] --- ## Props 當我們使用Props的時候都是避免直接把文字寫死在某個元件中,或是單一文字在很多個元件要共用,所以採用這樣的做法,Props可以自定義之後把訊息或是東西送出,可以達到物件可以重複使用的效果 ``` //Modal.vue <script> export default { props: ['heading', 'text', 'theme'], methods: { closeModal() { this.$emit('closeclick') } } } </script> ``` 這樣就可以寫好之後把定義的東西從APP.Vue這邊做控制,也可以保持Modal能夠在別處重複使用 ``` //App.vue <div v-if="showModal"> <Modal :heading="heading" :text="text" theme="sale" @closeclick="toggleModal" /> </div> ``` ## Event Modifier 我們的很多事件都可以有modifier把這些東西可以作用的區域限縮在特定的地方 如下方這個,當我做點擊事件的時候我點class modal他也會觸發click事件然後關閉 ``` <div class="backdrop" @click="closeModal"> <div class="modal" :class="{ sale: theme === 'sale' }"> <h1>{{ heading }}</h1> <p>{{ text }}</p> </div> </div> ``` 但當我使用下面的寫法,事件後面加上.self的時候,就只有backdrop本身可以被觸發,然後modal就沒事情了。 ``` <div class="backdrop" @click.self="closeModal"> <div class="modal" :class="{ sale: theme === 'sale' }"> <h1>{{ heading }}</h1> <p>{{ text }}</p> </div> </div> ``` 還有很多的其他modifier,這邊只針對click做說明 > 寫成.left就只有左鍵點有效 > 寫成.right就只有右鍵點擊 > 寫成.shift就是點擊的時候要按著shift > 寫成.alt就是點擊的時候按著alt > ....之後以此類推 ## Slot slot可以pass 格式,不單單只有資訊,比起props有更多的用處 ### 一般Slot 以下是一個example ``` #App.vue <template> <h1>{{ title }}</h1> <p>Welcome...</p> <div v-if="showModal"> <Modal theme="sale" @closeclick="toggleModal" > <h1>Ninja Givaway</h1> <p>Grab your time to register</p> </Modal> </div> <button @click="toggleModal">open modal</button> </template> <script> import Modal from './components/Modal' </script> ``` App有使用到Modal這個component,同時用這種方式把訊息跟格式傳到Modal,Modal在其中改一個Slot,就能讓App.vue插入任何格式進來。 ``` #Modal.vue <template> <div class="backdrop" @click.self="closeModal"> <div class="modal" :class="{ sale: theme === 'sale' }"> <slot></slot> </div> </div> </template> ``` #### default slot slot若沒有人使用的時候,你可以在子元件那邊設定default,這樣在app.vue沒有給他任何值的時候他就只會顯示Default slot的內容。 ``` #Modal.vue <template> <div class="backdrop" @click.self="closeModal"> <div class="modal" :class="{ sale: theme === 'sale' }"> <slot>Default slot</slot> </div> </div> </template> ``` ### name slot 用name slot可以直接用名稱找到特定的slot,跟一般的不同 ``` <!--App.vue--> <div v-if="showModal"> <Modal theme="sale" @closeclick="toggleModal" > <template v-slot:links> <a href="#">sign up now</a><br/> <a href="/hello"> more information</a> </template> <h1>Ninja Givaway</h1> <p>Grab your time to register</p> </Modal> </div> ``` 這樣把Modal裡面寫一個slot name= links就可以把特定的name slot放入 ``` <!--Modal.vue--> <div class="backdrop" @click.self="closeModal"> <div class="modal" :class="{ sale: theme === 'sale' }"> <slot></slot> <div class="actions"> <slot name="links"></slot> </div> </div> </div> ``` ## Teleport 有時候一些DOM想避免被遮擋或是可以直接跳出來,可以直接使用Teleport把東西傳到html上。 ``` ##index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <link rel="icon" href="<%= BASE_URL %>favicon.ico"> <title><%= htmlWebpackPlugin.options.title %></title> </head> <body> <noscript> <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <div id="app"></div> <div id="modals"></div> <!-- built files will be auto injected --> </body> </html> ``` 這樣就可以把東西直接傳到html檔案上了。 ``` # App.vue <template> <h1>{{ title }}</h1> <p>Welcome...</p> <teleport to='#modals' v-if="showModal"> <Modal theme="sale" @close="toggleModal"> <template v-slot:links> <a href="#">sign up now</a> <a href="#">more info</a> </template> <h1>Ninja Givaway!</h1> <p>Grab your ninja swag for half price!</p> </Modal> </teleport> <teleport to='#modals' v-if="showModalTwo"> <Modal @close="toggleModalTwo"> <h1>Sign up to the Newsletter</h1> <p>For updates and promo codes!</p> </Modal> </teleport> <button @click="toggleModal">open modal (alt click)</button> <button @click="toggleModalTwo">open modal 2</button> </template> ``` 記得同時要調整css檔案 ``` #app, #modals { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } ``` 要注意的是,若html是用id tag,就要用#modals來teleport,若使用className就使用.modals。 ## Data Binding V-model可以 ## Vue Router 一般而言我們可以透過Vue Router來達到頁面快速切換,他跟以往的頁面切換不同是因為以往的頁面切換是透過 href 回到server重新送需求,而vue router卻是使用router-link來送需求,這樣只要切換不同的component就可以了,比起重新送需求給server來得切換更快。 寫法差異如下。 ``` <template> <nav> <router-link to="/">Home</router-link> | <router-link to="/about">About</router-link> | <a href="/about">About</a> </nav> <router-view/> </template> ``` ### 使用name來做為url切換 當然當我們在router/index.js裡面若有設定的話,也可以這樣寫,直接把他的name放上去就好。 ``` //index.js import { createRouter, createWebHistory } from 'vue-router' import Home from '../views/HomeView.vue' import About from '../views/AboutView.vue' const routes = [ { path: '/', name: 'home', component: Home }, { path: '/about', name: 'about', component: About } ] const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes }) export default router ``` 下面是router-link寫法,這種寫法的好處是,我若是哪天想要改url的時候我就不用把所有之前寫過的url都重改一次,保留name他就會追到我最新的修改了。 ``` //App.vue <template> <router-link :to="{ name: 'about'}">About</router-link> </template> ``` ### Dynamic Links 當我要新增一個Job details page我可以這樣做 ``` //JobDetails.vue <template> <h1>Job Details Page</h1> <p>The job id is {{ id }}</p> </template> <script> export default { data () { return{ id: this.$route.params.id } } } </script> <style> </style> ``` index.js這樣改 ``` import { createRouter, createWebHistory } from 'vue-router' import Home from '../views/HomeView.vue' import About from '../views/AboutView.vue' import Jobs from '../views/jobs/Jobs.vue' import JobDetails from '../views/jobs/JobDetails.vue' const routes = [ { path: '/', name: 'home', component: Home }, { path: '/about', name: 'about', component: About }, { path: '/jobs', name: 'Jobs', component: Jobs }, { path:'/jobs/:id', name: 'JobDetails', component: JobDetails } ] const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes }) export default router ``` ### Dynamic Links use Props 當然我若是接受props就可以直接轉過去JobDetails了,就不需要寫data甚麼之類 可以這樣改 ``` //JobDetails.uve <template> <h1>Job Details Page</h1> <p>The job id is {{ id }}</p> </template> <script> export default { props: [`id`], } </script> <style> </style> ``` index把props設定true ``` //index.js import { createRouter, createWebHistory } from 'vue-router' import Home from '../views/HomeView.vue' import About from '../views/AboutView.vue' import Jobs from '../views/jobs/Jobs.vue' import JobDetails from '../views/jobs/JobDetails.vue' const routes = [ { path: '/', name: 'home', component: Home }, { path: '/about', name: 'about', component: About }, { path: '/jobs', name: 'Jobs', component: Jobs }, { path:'/jobs/:id', name: 'JobDetails', component: JobDetails, props: true } ] const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes }) export default router ``` ### Redirect 可以用下列方式做REDIRECT,下面是將/all-jobs的URL轉到/jobs ``` import { createRouter, createWebHistory } from 'vue-router' import Home from '../views/HomeView.vue' import About from '../views/AboutView.vue' import Jobs from '../views/jobs/Jobs.vue' import JobDetails from '../views/jobs/JobDetails.vue' const routes = [ { path: '/', name: 'home', component: Home }, { path: '/about', name: 'about', component: About }, { path: '/jobs', name: 'Jobs', component: Jobs }, { path:'/jobs/:id', name: 'JobDetails', component: JobDetails, props: true }, { path: '/all-jobs', redirect: '/jobs' } ] const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes }) export default router ``` ### 設定404網頁 若使用者點擊到我們沒有定義的網頁,可以設定404讓他跳轉,這樣也很方便 首先設定一個NotFound.vue ``` //NotFound.vue <template> Not Found la </template> ``` 然後把它放進去index中,:catchAll(.*)這個語法會抓到我們其他沒定義的url把它塞到notfound那邊 ``` //index.js import { createRouter, createWebHistory } from 'vue-router' import Home from '../views/HomeView.vue' import About from '../views/AboutView.vue' import Jobs from '../views/jobs/Jobs.vue' import JobDetails from '../views/jobs/JobDetails.vue' import NotFound from '../views/NotFound.vue' const routes = [ { path: '/', name: 'home', component: Home }, { path: '/about', name: 'about', component: About }, { path: '/jobs', name: 'Jobs', component: Jobs }, { path:'/jobs/:id', name: 'JobDetails', component: JobDetails, props: true }, { path: '/all-jobs', redirect: '/jobs' }, { path: '/:catchAll(.*)', name: 'NotFound', component: NotFound } ] const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes }) export default router ``` ### Programming Navigation 我們可以透過操作Route來達到往返上一頁或直接跳轉頁面的方法,寫法也不難。 透過methods內部建立this.$router.push可以直接跳轉,this.$router.go裡面寫正數可以往前,負數可以往回。記得要加上<router-view/> ``` //App.vue <template> <nav> <router-link to="/">Home</router-link> | <router-link to="/about">About</router-link> | <router-link :to="{ name: 'Jobs' }">Jobs</router-link> </nav> <button @click="redirect">Redirect</button> <button @click="back">Go Back</button> <button @click="forward">Go Forward</button> <router-view/> </template> <script> export default { methods: { redirect() { this.$router.push({ name: 'home'}) }, back() { this.$router.go(-1) }, forward() { this.$router.go(1) } } } </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; } nav { padding: 30px; } nav a { font-weight: bold; color: #2c3e50; text-decoration: none; padding: 10px; border-radius: 15px; } nav a.router-link-exact-active { color: #fdfffe; background: crimson; } button { margin: 0 10px; padding: 10px; border: none; border-radius: 4px; } </style> ``` ### Lazy-Loading 若使用Lazy Loadging可以使你點擊的時候才會載入那個vue檔,可以使你一開始進到頁面時不用載入這麼多東西,優化使用效能。 ``` //index.js import { createRouter, createWebHistory } from 'vue-router' import Home from '../views/HomeView.vue' // import About from '../views/AboutView.vue' import Jobs from '../views/jobs/Jobs.vue' import JobDetails from '../views/jobs/JobDetails.vue' import NotFound from '../views/NotFound.vue' const routes = [ { path: '/', name: 'home', component: Home }, { path: '/about', name: 'about', component: () => import('../views/AboutView.vue') }, { path: '/jobs', name: 'Jobs', component: Jobs }, { path:'/jobs/:id', name: 'JobDetails', component: JobDetails, props: true }, { path: '/all-jobs', redirect: '/jobs' }, { path: '/:catchAll(.*)', name: 'NotFound', component: NotFound } ] const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes }) export default router ``` [Lazy Routing-參考頁面](https://router.vuejs.org/guide/advanced/lazy-loading.html) ## Composition API ### 基礎介紹 過往使用options API 會寫得非常長,然後一個資料的變更、初始化或是lifecycle就會散落在不同的行列中 ![image](https://hackmd.io/_uploads/HyeXrNV26.png) 但composition api可以寫得比較集合且更單純一點 ![image](https://hackmd.io/_uploads/ByBBBNV2a.png) composition API 有幾個好處 - group logic together in a setup function - easily create reusable logic (functions) 當然composition API 並不是總被使用,也可以跟Opsition API結合使用。但隨著專案變大變複雜的時候,這樣可以有效管理很多狀態跟function,在專案進行的時候有很大的效益。 ### ref 響應使用 基本上composition api是沒有辦法做響應式的,若是更改之後沒有辦法再跟一般寫data return一樣再回去給template,但可以透過ref來做設定,這樣就可以做更改並且返回到template上面。 但要注意的是ref一開始定義的時候,p就是一個proxy object,在setup要使用p.value才有ref的值。下面的寫法一開始寫法就是null,後面點擊的時候才可以有資料。 要注意的是,在setup裡面做修改都要寫.vlaue,但在template中只要寫name跟value就好。 ``` <template> <div class="home"> home <p ref="p">My name is {{ _name }} and my age is {{ _age }}</p> <button @click="handleClick">click me</button> </div> </template> <script> import { ref } from 'vue' // @ is an alias to /src export default { name: 'Home', setup() { const p = ref(null) let name ='mario' let age = 30 const handleClick =() =>{ console.log(p, p.value) p.value.classList.add('test') p.value.textContent= 'hello, ninjas' } return { name, age, handleClick, p } }, data() { return { age: 40 } } } </script> ``` ### ref v.s. reactive reactive 比較明顯的差異是ref改東西的時候需要加上.value,但reactive卻不用。 但reactive原生地狀況是沒有辦法做訊息回饋的,所以要避免直接僅傳數值給reactive。 ``` export default { name: 'Home', setup() { const ninjaOne = ref({ name: 'mario', age: 30}) const ninjaTwo = reactive({ name: 'luigi', age: 35 }) const updateNinjaOne = () => { ninjaOne.value.age = 40 } const updateNinjaTwo = () => { ninjaTwo.age = 45 } return {ninjaOne, updateNinjaOne, ninjaTwo, updateNinjaTwo} } } ``` ### watch v.s. watchEffect watch跟watchEffect最大的差別是,watch一開始不會載入,但watchEffect會。 watch在每一次變動的時候都會寫進去,但watchEffect只會追蹤你有給他的東西,所以你要追蹤變化的話後面記得要加入你要追蹤的變數。 ``` export default { name: 'Home', setup() { const search = ref('') const names = ref(['mario', 'yoshi', 'luigi', 'toad', 'bowser', 'koopa', 'peach' ]) watch(search, () => { console.log('watch function ran') }) watchEffect(()=> { console.log('watchEffect function run', search.value) }) const matchingNames = computed(() => { return names.value.filter((name) => name.includes(search.value)) }) return { names, search, matchingNames } } } ``` 有趣的是,若是我們想要停止watch跟watch effect,可以透過js製造function呼叫watcheffect跟watch。只要收到呼叫這兩個函數就會直接停止她們的監聽行為了 ``` <template> <div class="home"> <h1>Home</h1> <input type="text" v-model="search"> <p>search term - {{ search }}</p> <div v-for="name in matchingNames" :key="name">{{ name }}</div> <button @click="handleClick">stopWatch</button> </div> </template> <script> import { ref, reactive, computed, watchEffect,watch } from 'vue' export default { name: 'Home', setup() { const search = ref('') const names = ref(['mario', 'yoshi', 'luigi', 'toad', 'bowser', 'koopa', 'peach' ]) const stopWatch = watch(search, () => { console.log('watch function ran') }) const stopEffect = watchEffect(()=> { console.log('watchEffect function run', search.value) }) const matchingNames = computed(() => { return names.value.filter((name) => name.includes(search.value)) }) const handleClick = () => { stopWatch() stopEffect() } return { names, search, matchingNames, handleClick } } } </script> ``` ## lifecycle hook inside setup 基本上那些lifecycle的hook都還是可以使用。 但基本上都會要加上on才可以用。記得要從vue 中import才可以 ``` <script> import { onMounted, onUnmounted,onUpdated} from 'vue' import SinglePost from './SinglePost.vue' export default { props: ['posts'], components: { SinglePost }, setup(props) { console.log(props.posts) onMounted(() => console.log('component mounted')) , onUnmounted(()=> console.log('unmounted')), onUpdated(()=> console.log('component updated')) } } </script> ``` ### composable function 若自己在fetch data的時候發現整個資料太大,也可以把單獨的fetch功能拆出來變成個別的.js檔案,import回到主要的.vue檔案中。 ``` //home.vue <template> <div class="home"> <div v-if="error">{{ error }}</div> <div v-if="posts.length"> <PostList :posts="posts" /> </div> <div v-else>Loading...</div> </div> </template> <script> import { ref } from 'vue' // component imports import PostList from '../components/PostList.vue' import getPosts from '../composables/getPosts' export default { name: 'Home', components: { PostList }, setup() { const {posts, error, load} = getPosts() load() return { posts, error } } } </script> ``` 把getPosts拆出來。 這樣程式碼維護上就沒有太大問題 ``` import { ref } from 'vue' const getPosts =() => { const posts = ref([]) const error = ref(null) const load = async () => { try { let data = await fetch('http://localhost:3000/posts') if(!data.ok) { throw Error('no available data') } posts.value = await data.json() console.log(posts.value) } catch(err) { error.value = err.message console.log(error.value) } } return {posts, error, load} } export default getPosts ``` ## Vue-Router 若使用composition API就不能用之前router的用法,要引用下面的方式 這樣可以直接導回去HOME PAGE ``` const router = useRouter() router.push({ name: 'Home' }) ``` ## Firebase realtime data 使用onSnapshot是可以即時性的讓firebase把更新通知送進來,讓同時有多個使用者使用的時候不用自己手動更新就可以直接拿到最新訊息的方式。 ``` export default { setup() { const posts =ref([]) projectFirestore.coolection('posts') .orderBy('createdAt', 'desc') .onSnapshot((snap) => { let docs = snap.docs.map(doc => { return { ...doc.data(), id: doc.id } }) posts.value = docs }) return {posts} } } ```