剛好最近在玩 vue 就來記錄一下
參考文件:
- vue
- Vuex
- vue-router 2
- vue + webpack 起手式
- vue2 vue-router2 webpack
- 用 Vuex 建構一個筆記應用
- Learn Vue 2: Step By Step
Javascript:
Vue - Week1 Todo list
之前就有玩過 vue 不過太久沒碰,就變得生疏,剛好這次同事有開 vue 的 bookclub,就再來複習一下吧~
vs code 套件
Vetur
Vue 2 Snippets
環境建置
1. 安裝 vue-cl
// -g 為 global 的意思,沒有加的話,會裝在當下的
npm install -g vue-cli
yarn global add vue-cli
2. 建立專案
vue init webpack vue-book-club
3.專案選項
? Project name vue-book-club // 直接enter
? Project description A Vue.js project // 直接enter
? Author GuaHsu <guaswork@gmail.com> // 直接enter
? Vue build standalone
// ❯ Runtime + Compiler: recommended for most users
// Runtime-only: about 6KB lighter min+gzip, but templates (or any Vue-specific HTML) are ONLY allowed in .vue files - render functions are required elsewhere
? Install vue-router? Yes
? Use ESLint to lint your code? Yes // [可選]程式碼規範,建議安裝
? Pick an ESLint preset Standard
// ❯ Standard (https://github.com/standard/standard)
// Airbnb (https://github.com/airbnb/javascript)
// none (configure it yourself)
? Set up unit tests Yes // [可選]建議先裝JEST日後有空可讀這個
? Pick a test runner jest
// ❯ Jest
// Karma and Mocha
// none (configure it yourself)
? Setup e2e tests with Nightwatch? Yes // [可選]建議先裝起來,日後有空可讀這個
? Should we run `npm install` for you after the project has been created? (recommended) npm // [可選] npm or yarn
// ❯ Yes, use NPM
// Yes, use Yarn
// No, I will handle that myself
4.安裝套件
- Sass(Scss) - for css 建議使用
npm install sass sass-loader node-sass --save
yarn add sass sass-loader node-sass
- Pug - for html 可選用
npm install pug pug-loader pug-filters --save
yarn add pug pug-loader pug-filters
- 安裝
package.json
內其它套件
npm install
yarn install
5. 修改一下eslint規則 - for Prettier code / standard
Prettier code 目前不支援 standard 的 function 空格設定所以改一下
// vue-book-club/.eslintrc.js
rules: {
// allow async-await
'generator-star-spacing': 'off',
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
// 新增的是下面這兩個
'space-before-function-paren': 0,
'new-cap': [
'error', {
'properties': false
}
]
}
TODO List
vue 主要都是在 src
的資料夾底下
src
├── App.vue
├── assets
│ └── logo.png
├── components
│ ├── HelloWorld.vue
│ └── TodoList.vue
├── main.js
└── router
└── index.js
使用 v-router
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
// 這邊的 @ 指的是 src/
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld
}
]
})
// name 主要在之後要 mapping 可以用 name 去對應
// component 對應最上面 import 進來的
vue 的一個 component 大致上分
- template - html
- script - js
- style - css
template
<template>
<div id="TodoList">
<div class="top-area">
<input class="top-area-input" type="text" v-model.trim="todoMessage">
<button class="top-area-add" @click="addTodo">Add Todo</button>
<div class="top-area-info">
共有: 筆資料,有 筆完成
</div>
<div v-if="isMaxLimit" class="top-area-info--full">已達新增上限!v-if</div>
<div v-show="isMaxLimit" class="top-area-info--full">已達新增上限!v-show</div>
</div>
<div class="content">
<ul class="todolist">
<li v-for="(todo, index) in todoList" :key="index" class="todolist-item">
<input :id="`todo-${index}`" type="checkbox" v-model="todo.checked">
<label :for="`todo-${index}`" :class="[{ 'todolist-item--checked': todo.checked }]"></label>
<button class="todolist-item-remove" @click="removeTodo(index)">remove</button>
</li>
</ul>
</div>
</div>
</template>
v-model
設定 data
<input class="top-area-input" type="text" v-model.trim="todoMessage">
// trim 去掉前後多於的空白
v-bind
屬性繫結,綁在 HTML 上的屬性 (attribute)
<label :for="`todo-${index}`" :class="[{ 'todolist-item--checked': todo.checked }]"></label>
// : = v-bind
// :class = "{checked: true}" 後面是 true 才會吃
// 複數加 [], :class = [anotherClass, "{checked: true}"]
// 字串裡塞變數,必須外面包 ``,變數用 ${}
v-on
在 click 的時候觸發 addTodo method
<button class="top-area-add" @click="addTodo">Add Todo</button>
// @ = v-on
v-for
<li v-for="(todo, index) in todoList" :key="index" class="todolist-item">
// 可識別唯一性的 key,可用來排序
v-show, v-if
<div v-if="isMaxLimit" class="top-area-info--full">已達新增上限!v-if</div>
// 要是 true 的時候才會有畫面,並且 dom 會長出來
<div v-show="isMaxLimit" class="top-area-info--full">已達新增上限!v-show</div>
// 如果是 false 就是畫面隱藏,dom 還在
// 另外還有 v-else v-else-id,同一組必須寫在一起
script
export default {
name: 'TodoList',
data() {
return {
maxLimit: 3,
todoMessage: '',
todoList: [{ name: 'test', checked: false }]
}
},
computed: {
checkedItemCount() {
return this.todoList.filter(todo => todo.checked)
},
isMaxLimit() {
return this.todoList.length === this.maxLimit
}
},
watch: {
// todoMessage (newVal, oldVal) {
// console.log(newVal, oldVal)
// }
// todoList: {
// handler(newVal, oldVal) {
// console.log(newVal, oldVal)
// this.checkedItemCount = this.todoList.filter(todo => todo.checked)
// },
// deep: true
// }
},
methods: {
addTodo() {
if (!this.isMaxLimit && this.todoMessage) {
this.todoList.push({
name: this.todoMessage,
checked: false
})
this.todoMessage = ''
}
},
removeTodo(index) {
this.todoList.splice(index, 1)
}
}
}
</script>
// computed 可當作一種屬性,當裡面值有變動會跟著變動
// watch 不能 watch array object 必須用 deep watch (比較耗效能)
// methods 可以 call 的 method
style
<style lang="scss" scoped>
#TodoList {
* {
user-select: none;
}
.top-area {
&-info {
&--full {
color: pink;
}
}
}
.todolist {
&-item {
&--checked {
text-decoration: line-through;
}
}
}
}
</style>
/*
scoped 為了將每個 components 會產生亂數的屬性 data-v-3de47834,這樣每個 components 就都可以用同樣的 class & id name
*/
run
npm run dev
yarn run start
範例
官方文件
Vue - Week 2 Lifecycle & nvm
npm 之前有寫過 Ruby on Rails 裝機趴 (Only Mac)
所以主要是 vue 的 life cycle
Instance Lifecycle Hooks
Vue 提供了每個 liftcycle 的各種 hook,可以藉由這些 hook,在不同階段,處理不同事情
var vm = new Vue({
beforeCreate: function() {
//vue instance 被 constructor 建立前
console.log('beforeCreate');
},
created: function() {
//vue instance 被 constructor 建立後,在這裡完成 data binding
// call API 可以在這邊處理好,這樣 DOM 綁定前就可以拿到資料
console.log('created');
},
beforeMount: function() {
//繫結 DOM 之前
console.log('beforeMount');
},
mounted: function() {
//繫結 DOM 之後
console.log('mounted');
},
beforeUpdate: function() {
//資料更新,但尚未更新 DOM
console.log('beforeUpdate');
},
updated: function() {
//因資料更新,而更新 DOM
console.log('updated');
},
beforeDestroy: function() {
//移除 vue instance 之前
console.log('beforeDestroy');
},
destroyed: function() {
//移除 vue instance 之後
console.log('destroyed');
}
});
- 一般狀況使用v-if和v-for來控制資料即可,不需要用到vm.$destroy()。
- 如果是使用 server-side rendering,則除了beforeCreate和created,其他 method 都不會被呼叫。
// 官方解釋: 在下次 DOM 更新循環結束之後執行延遲回調。在修改數據之後立即使用這個方法,獲取更新後的 DOM。
// nextTick 可以到下一個階段再去執行,比如說等 update 完 dom 的值,再透過 reference 將 dom 的值拉下來
this.$nextTick(() => {
this.msg = this.$refs.msgDiv.innerHTML
})
範例
參考文件
Vue - Week3 Component
主要介紹如何使用 component 來架構頁面
透過 import 將要使用的 components 引入,並放置在 script 裡面的 components
父層
使用與 script 裡面引入同名的 components 當作 tag 來使用 TabA
TabB
TabC
<template>
<div id="W3Index">
<h1>Index </h1>
<h2>ChildMessage: </h2>
<h2></h2>
<input v-model="message" type="text">
<div class="tab-buttons">
<span
v-for="(tab, index) in tabs"
@click="tabIndex = index"
:class="{ 'active': tabIndex === index }"
:key="index">
</span>
</div>
<div class="tab-content">
<!-- data 是由 TabA 作定義的 props 屬性 -->
<TabA v-show="tabIndex === 0" :data="obj"></TabA>
<TabB v-show="tabIndex === 1" :data="childMessage">
<!-- mapping component 的 slot 名稱,就可將 template 傳到下層 -->ㄋ
<div slot="content">
<div style="width: 100px; height: 100px; border: solid 1px #333"> I am slot </div>
</div>
</TabB>
<!-- 監聽 childMessage,監聽到就執行 getChildMessage -->
<TabC v-show="tabIndex === 2" :message="message" @childMessage="getChildMessage"></TabC>
</div>
</div>
</template>
透過 import 將需要的 component 引入,並放進 components object
<script>
import TabA from '@/components/week3/TabA'
import TabB from '@/components/week3/TabB'
import TabC from '@/components/week3/TabC'
export default {
name: 'W3Index',
components: {
// es6 於許 key value 一樣時,只需要寫一次,如果不一樣就必須都寫 TabZ: TabA
TabA,
TabB,
TabC
},
data() {
return {
message: '',
childMessage: '',
obj: {
name: 'AAAAA',
value: 123
},
tabIndex: 0,
tabs: [{ name: 'TabA' }, { name: 'TabB' }, { name: 'TabC' }]
}
},
methods: {
getChildMessage(msg) {
this.childMessage = msg
}
}
}
</script>
子層
props
可接收上層給的 data,如果子層需要更改到資料,必須使用deep-copy
,否則像obejct
array
用reference
就會更改到slot
可接收上層給的 template$emit
可回傳給上層的 event,上層必須有人接收,並且上層接收後執行的 method,自動會將$emit
後面的參數傳進去
deep-copy: JSON.parse(JSON.stringify(this.data))
TabA
<template>
<div id="TabA">
<h1>TabA</h1>
<h2></h2>
<button @click="changeChildObj">changeChildObj</button>
</div>
</template>
<script>
export default {
name: 'TabA',
// props: ['data'], 可以這樣寫需要的屬性,但就沒辦法限制 type 等等..
props: {
// 這樣寫就可以限制每個屬性要的條件
data: {
type: Object,
default: () => { return {} },
required: false
}
},
data() {
return {
// 要改 props 資料,必須做深拷貝,不然原本 object 會 reference 上一層的資料
childData: JSON.parse(JSON.stringify(this.data))
}
},
methods: {
changeChildObj() {
this.childData.name = 'BBBBBB'
}
}
}
</script>
TabB
<template>
<div id="TabB">
<h1>TabB</h1>
<h2></h2>
<!-- slot 設定,外面必須 name 也是 content -->
<slot name="content"></slot>
</div>
</template>
<script>
export default {
name: 'TabB',
props: ['data', 'data2'],
data() {
return {
}
}
}
</script>
TabC
<template>
<div id="TabC">
<h2>TabC</h2>
<input v-model="childMessage" type="text">
<h3 :class="{'hasPink': hasPink}">Message: </h3>
<button @click="emitMsg">emit</button>
</div>
</template>
<script>
export default {
name: 'TabC',
props: {
message: {
type: String,
default: '',
required: true
}
},
data() {
return {
childMessage: ''
}
},
computed: {
hasPink() {
return this.message.match(/pink/gi)
}
},
methods: {
emitMsg() {
// 傳回上一層,上一層必須有在監聽 childMessage
this.$emit('childMessage', this.childMessage)
}
}
}
</script>
<style lang="scss">
#TabC {
.hasPink {
color: pink;
}
}
</style>
範例
參考文件
Week4 - Vuex
yarn add vuex
照慣例會將 vux 統一放在 src/store/
裡面
因此在 main.js
import store from './store'
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})
預設都會抓 index.js
// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
// 第一種方式全部都分開
import { state, mutations } from './mutations.js'
import * as getters from './getters.js'
import * as actions from './actions.js'
// 第二種方式全都放在一起
import shop from './modules/shop'
Vue.use(Vuex)
export default new Vuex.Store({
state,
mutations,
getters,
actions,
modules: {
shop
},
// 嚴格模式,禁止直接修改 state
strict: true
})
- State: 所有資料狀態存取的地方,
- Getter 給前端資料 (像是計算屬性 compute)
- Mutation: 更改資料只能用 commit 的方式透過 mutation 來更改
- 必須是同步函數
- Action 類似於 mutation,不同在於:
- Action 提交的是 mutation,而不是直接變更狀態。
- Action 可以包含任意非同步操作。
// src/store/modules/shop.js
import shopAPI from '@/api/shop'
// 命名 muation 觸發的名稱,方便管理
const types = {
ADD_CART_ITEM: 'ADD_CART_ITEM',
REMOVE_CART_ITEM: 'REMOVE_CART_ITEM',
SET_PRODUCT_LIST_DATA: 'SET_PRODUCT_LIST_DATA',
SET_COUNTRY: 'SET_COUNTRY'
}
// 拿資料可以透過 state 或是 getter 來拿,要怎麼組合就看自己
// 也可以資料透過 state 要計算的資料就透過 getter 去拿取
const state = {
country: 'tw',
cartItems: [],
productListData: []
}
const getters = {
country: state => state.country,
productListData: state => state.productListData,
cartItemCount: state => state.cartItems.length,
cartItemTotalAmount: state => state.cartItems.reduce((total, product) => total + product.price, 0)
}
// actions 主要是執行一些非同步,最後更改資料還是要透過 mutation
// 跟 state 和 getter 一樣,都可以組合
const actions = {
setCountry({ commit }, country) {
commit(types.SET_COUNTRY, country)
},
getProductListData({ commit }, data) {
shopAPI.getProductData().then(res => {
commit(types.SET_PRODUCT_LIST_DATA, res)
})
},
addCartItem({ commit }, data) {
// if 庫存檢查 then
commit(types.ADD_CART_ITEM, data)
// else alert('庫存不足)
},
removeCartItem({ commit }, index) {
commit(types.REMOVE_CART_ITEM, index)
}
}
const mutations = {
[types.ADD_CART_ITEM](state, data) {
state.cartItems.push(data)
},
[types.REMOVE_CART_ITEM](state, index) {
state.cartItems.splice(index, 1)
},
[types.SET_PRODUCT_LIST_DATA](state, data) {
state.productListData = data
},
[types.SET_COUNTRY](state, country) {
state.country = country
}
}
export default {
// 宣告 namespaced true 在 import 時就必須指定 file 名稱
namespaced: true,
state,
getters,
actions,
mutations
}
// src/components/ProductList.vue
<template>
<div id="Index">
<AppHeader></AppHeader>
<h1 v-if="!productListData.length">讀取中</h1>
<div class="product" v-for="product in productListData" :key="product.id">
<img class="product-img" src="@/assets/logo.png">
<div class="product-name"></div>
<div class="product-price"></div>
<div class="product-add" @click="addToCart(product)">加入購物車</div>
</div>
</div>
</template>
<script>
// import shopAPI from '@/api/shop'
import AppHeader from './AppHeader'
// 先 iport 進來才可以使用
import { mapActions, mapGetters } from 'vuex'
export default {
name: 'ProductList',
components: {
AppHeader
},
data() {
return {
productList: []
}
},
computed: {
// states 和 getters 寫在 computed 裡面
...mapGetters('shop', ['productListData'])
},
created() {
this.getProductListData()
// shopAPI.getProductData().then(res => {
// this.productList = res
// })
},
methods: {
// actions 和 mutations 寫在 methods 裡面
...mapActions('shop', ['addCartItem', 'getProductListData']),
addToCart(product) {
this.addCartItem(product)
// 原本 mutation 觸發的寫法
// this.$store.commit('shop/ADD_CART_ITEM', product)
// 原本 action 觸發的寫法
// this.$store.dispatch('shop/addCartItem', product)
}
}
}
</script>
參考文件