據上節文章發佈已經有了兩個星期了。期間收到了 1000+ 個贊,30000+ 閱讀量,這是我萬萬沒想到的。本身的文章能有這麼高的關注度,真的很使人滿意!javascript
可是相反,寫文章的壓力更加大了。一篇文章老是反反覆覆的修改,老是擔憂本身的認知水平和技術水平不夠,甚至致使有些地方會誤導讀者。css
也會揣摩本身寫做風格有沒有什麼問題、會不會太囉嗦、哪些地方沒講清楚等等...若是有很差的地方能夠評論指出來,接受批評,批評也是一種進步的動力!html
原本準備上下兩節寫徹底部內容,發現實際不太可能,還沒寫完 4 章,就已經 6000—+ 字了。最後一節寫完以後就準備回家過年了,這裏提早祝你們新年快樂!前端
常規操做,先點贊後觀看哦!你的點贊是我創做的動力之一!vue
這節我將從 5 個方面來論述 vue 開發過程當中的一些技巧和原理。若是你還未觀看上節文章,能夠移步至16個方面深刻前端工程化開發技巧《上》觀看。java
本節內容主要圍繞下列問題展開:ios
實踐以前:我但願你有以下準備,或者知識儲備。git
- 瞭解
npm/yarn/git/sass/pug/vue/vuex/vue-router/axios/mock/ssr/jest
的使用和原理。- 固然上面知識不瞭解也不要緊哈哈哈,文章中會提到大體用法和做用。
vue 編寫組件有兩種方式,一種是單文件組件,另一種函數組件。根據組件引入和建立還能夠分爲動態組件和異步組件。github
動態組件
keep-alive
使之緩存。異步組件原理和異步路由同樣,使用import()
實現異步加載也就是按需加載。vue-router
所謂 vue 單文件組件,就是咱們最多見的這種形式:
<template lang="pug">
.demo
h1 hello
</template>
<script> export default { name: 'demo', data() { return {} } } </script>
<style lang="scss" scoped> .demo { h1 { color: #f00; } } </style>
複製代碼
這裏的template
也可使用 render
函數來編寫
Vue.component('demo', {
render: function (createElement) {
return createElement(
'h1',
'hello',
// ...
)
}
})
複製代碼
咱們能夠發現render
函數寫模版讓咱們更有編程的感受。對模版也能夠編程,在vue
裏面咱們能夠很容易聯想到,不少地方都有兩種寫法,一種是 template
, 一種是js
。
好比:對於路由,咱們既可使用:to=""
,又可使用$router.push
,這也許是 vue 用起來比較爽的緣由。
函數式組件是什麼呢?
functional
,這意味它無狀態 (沒有響應式數據),也沒有實例 (沒有 this
上下文)
2.5.0+
<template functional>
</template>
複製代碼
Vue.component('my-component', {
functional: true,
render function (createElement, context) {
return createElement('div')
}
}
複製代碼
爲何要使用函數組件呢?
最重要的緣由就是函數組件開銷低,也就是對性能有好處,在不須要響應式和this
的狀況下,寫成函數式組件算是一種優化方案。
組件寫好了,須要將組件註冊才能使用
組件註冊分爲兩種,一種是全局註冊,一種是局部註冊
局部註冊就是咱們經常使用的 Vue.component('s-button', { /* ... */ })
,比較簡單不詳細論述
全局註冊上節已經提到,在new Vue
以前在 mian.js
註冊,這裏還提到一種自動全局註冊的方法 require.text
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'
const requireComponent = require.context(
'./components',
// 是否查詢其子目錄
false,
/Base[A-Z]\w+\.(vue|js)$/
)
requireComponent.keys().forEach(fileName => {
// 獲取組件配置
const componentConfig = requireComponent(fileName)
const componentName = upperFirst(
camelCase(
// 獲取和目錄深度無關的文件名
fileName
.split('/')
.pop()
.replace(/\.\w+$/, '')
)
)
// 全局註冊組件
Vue.component(
componentName,
componentConfig.default || componentConfig
)
})
複製代碼
基本原理和全局註冊同樣,就是將 components
中的組件文件名,appButton
變成 AppButton
做爲註冊的組件名。把原來須要手動複製的,變成之間使用 keys
方法批量註冊。
如今,咱們以寫一個簡單的原生button
組件爲例,探討一下組件開發的一些關鍵點。 寫以前,咱們須要抓住 4 個核心的要點:
button
仍是 div
標籤button
組件的顏色 color
、形狀 type
、大小 size
button
組件的點擊事件button
組件的內容這些思考點在其餘原生組件開發和高階組件封裝裏面也須要考慮到
首先看第一個問題,大部分原生組件第一考慮的地方,就是主要標籤用原生<button></button>
標籤仍是用<div></div>
去模擬。
爲何不考慮
<input/>
呢,由於<button>
元素比<input>
元素更容易添加內部元素。你能夠在元素內添加HTML內容(像<em>
、<strong>
甚至<img>
),以及::after
和::before
僞元素來實現複雜的效果,而<input>
只支持文本內容。
下面分析這兩種寫法的優劣
使用原生button
標籤的優點:
buff
,一些自帶的鍵盤事件行爲等爲何說更好的語義化呢?有人可能會說,可使用
role
來加強div
的語義化。確實能夠,可是可能存在問題——有些爬蟲並不會根據role
來肯定這個標籤的含義。
另一點,對開發者來講,
<button></button>
比<div role="button"></div>
閱讀起來更好。
使用 div
模擬的優點:
button
原生樣式帶來的一些干擾,少寫一些覆蓋原生 css
的代碼,更乾淨純粹。div
,不須要再去找原生標籤、深刻了解原生標籤的一些兼容相關的詭異問題。div
做爲組件主體的緣由。貌似 div 除了語義不是很好之外,其餘方面都還行,可是具體用哪種其實均可以,只要代碼寫的健壯適配性強,基本都沒啥大問題。
咱們這裏使用原生<button></button>
做爲主要標籤,使用s-xx
做爲class
前綴
爲何須要使用前綴,由於在有些時候,好比使用第三方組件,多個組件之間的 class 可能會產生衝突,前綴用來充當組件 css 的一個命名空間,不一樣組件之間不會干擾。
<template lang="pug">
button.s-button(:class="xxx" :style="xxx" )
slot
</template>
複製代碼
而後,咱們看第二個問題:
如何根據屬性來控制 button
的樣式 其實這個很簡單,基本原理就是:
使用 props
獲取父組件傳過來的屬性。
根據相關屬性,生成不一樣的class
,使用 :class="{xxx: true, xxx: 's-button--' + size}"
這種形式,在 style
裏面對不一樣的s-button--xxx
作出不一樣的處理。
<template lang="pug">
button.s-button(:class="" :style="" )
slot
</template>
<script> export default { name: 's-button' data: return {} props: { theme: {}, size: {}, type: {} } } </script>
複製代碼
如何使用事件以及如何擴展組件
擴展組件的原理,主要就是使用 props
控制組件屬性,模版中使用 v-if/v-show
增長組件功,好比增長內部 ICON,使用:style``class
控制組件樣式。
type
類型,原生默認是
submit
,這裏咱們默認設置爲
button
<template lang="pug">
button.s-button(:class="" :style="" :type="nativeType")
slot
s-icon(v-if="icon && $slots.icon" name="loading")
</template>
<script> export default { name: 's-button' data: return {} props: { nativeType: { type: String, default: 'button' }, theme: {}, size: {}, type: {} } } </script>
複製代碼
控制事件,直接使用 @click=""
+ emit
<template lang="pug">
button.s-button(@click="handleClick")
</template>
<script> export default { methods: { handleClick (evt) { this.$emit('click', evt) } } } </script>
複製代碼
通常就直接使用
template
單文件編寫組件,須要加強 js編寫能力可使用render()
常規編寫組件須要考慮:1. 使用什麼標籤 2. 如何控制各類屬性的表現 3. 如何加強組件擴展性 4. 如何處理組件事件
對響應式
this
要求不高,使用函數functional
組件優化性能。
基礎組件一般全局註冊,業務組件一般局部註冊
使用
keys()
遍歷文件來實現自動批量全局註冊
使用
import()
異步加載組件提高減小首次加載開銷,使用keep-alive + is
緩存組件減小二次加載開銷
咱們知道組件中通訊有如下幾種方式:
props
傳遞給子組件,不詳細論述emit
事件傳遞數據給父組件,父組件經過 on
監聽,也就是一個典型的訂閱-發佈模型
@
爲v-on:
的簡寫
<template lang="pug">
<!--子組件-->
div.component-1
<template>
export default {
mounted() {
this.$emit('eventName', params)
}
}
</script>
複製代碼
<!-- 父組件-->
<template lang="pug">
Component-1(@eventName="handleEventName")
<template>
<script> export default { methods: { handleEventName (params) { console.log(params) } } } </script>
複製代碼
原理很簡單其實就是在 emit
與 on
的基礎上加了一個事件中轉站 「bus」。我以爲更像是現實生活中的集線器。
廣泛的實現原理大概是這樣的 「bus」 爲 vue
的一個實例,實例裏面能夠調用emit
,off
,on
這些方法。
var eventHub = new Vue()
// 發佈
eventHub.$emit('add', params)
// 訂閱/響應
eventHub.$on('add', params)
// 銷燬
eventHub.$off('add', params)
複製代碼
可是稍微複雜點的狀況,使用這種方式就太繁鎖了。仍是使用 vuex 比較好。
從某種意義而言,我以爲 vuex 不只僅是它的一種進化版。
store
做爲狀態管理的倉庫,而且引入了狀態這個概念bus
模型感受像一個電話中轉站與
git
相似,它不能直接修改代碼,須要參與者提交commit
,提交完的commit
修改倉庫,倉庫更新,參與者fetch
代碼更新本身的代碼。不一樣的是代碼倉庫須要合併,而vuex
是直接覆蓋以前的狀態。
「store」基本上就是一個容器,它包含着你的應用中大部分的狀態 (
state
)。Vuex 和單純的全局對象有如下兩點不一樣
dom
)mutation
)
基本用法:就是在 state
裏面定義各類屬性,頁面或組件組件中,直接使用 $store.state
或者$store.getters
來使用。若是想要改變狀態state
呢,就commit
一個mutation
可是假如我想提交一連串動做呢?能夠定義一個action
,而後使用 $store.dispatch
執行這個 action
使用action
不只能省略很多代碼,並且關鍵是action
中可使用異步相關函數,還能夠直接返回一個promise
而爲何不直接到mutation
中寫異步呢? 由於mutation
必定是個同步,它是惟一能改變 state 的,一旦提交了 mutation
,mutation
必須給定一個明確結果。不然會阻塞狀態的變化。
下面給出經常使用 vuex 的使用方式
新建一個store
並將其餘各個功能化分文件管理
import Vue from 'vue'
import Vuex from 'vuex'
import state from './states'
import getters from './getters'
import mutations from './mutations'
import actions from './actions'
import user from './modules/user'
Vue.use(Vuex)
export default new Vuex.Store({
//在非生產環境下,使用嚴格模式
strict: process.env.NODE_ENV !== 'production',
state,
getters,
mutations,
actions,
modules: {
user
}
})
複製代碼
操做狀態兩種方式
console.log(store.state.count)
複製代碼
store.commit('xxx')
複製代碼
單一狀態樹, 這也意味着,每一個應用將僅僅包含一個 store 實例。單一狀態樹讓咱們可以直接地定位任一特定的狀態片斷,在調試的過程當中也能輕易地取得整個當前應用狀態的快照。
// states 文件
export default {
count: 0
}
複製代碼
計算屬性中返回,每當 state
中屬性變化的時候, 其餘組件都會從新求取計算屬性,而且觸發更新相關聯的 DOM
const Counter = {
template: '<div>{{count}}<div>',
computed: {
count() {
return store.state.count
}
}
}
複製代碼
getters
至關於store
的計算屬性。不須要每次都要在計算屬性中過濾一下,也是一種代碼複用。 咱們在getters
文件中管理
export default {
count: (state) => Math.floor(state.count)
}
複製代碼
更改 Vuex 的 store 中的狀態的惟一方法是提交 mutation。Vuex 中的 mutation 很是相似於事件:每一個 mutation 都有一個字符串的 事件類型 (type) 和 一個 回調函數 (handler)。這個回調函數就是咱們實際進行狀態更改的地方
使用 types 大寫用於調試,在mutationTypes
文件中export const ROUTE_ADD = 'ROUTE_ADD'
而後在mutations
文件中管理
import * as MutationTypes from './mutationTypes'
export default {
[MutationTypes.ADDONE]: function(state) {
state.count = state.count + 1
},
//...
}
複製代碼
this.$store.commit(MutationTypes.ADDONE)
複製代碼
和 mutations
相似,actions
對應的是dispatch
,不一樣的是action
可使用異步函數,有種更高一級的封裝。
// 簡化
actions: {
increment ({ commit }) {
setTimeout(() => {
commit(MutationTypes.ADDONE)
}, 1000)
}
}
// 觸發
store.dispatch('increment')
複製代碼
上述用法均可以使用載荷的形式,引入也可使用
mapXxxx
進行批量引入,這裏不詳細論述,有興趣能夠查看官網。
狀態太多太雜,分模塊管理是一個良好的代碼組織方式。
import count from './modules/count'
export default new Vuex.Store({
modules: {
count
}
})
複製代碼
每個模塊均可以有獨立的相關屬性
import * as ActionTypes from './../actionTypes'
export default {
state: {
count: 0
},
mutations: {
ADD_ONE: function(state) {
state.count = state.count + 1
}
},
actions: {
[ActionTypes.INIT_INTENT_ORDER]: function({ commit }) {
commit('ADD_ONE')
}
},
getters: {
pageBackToLoan: (state) => Math.floor(state.count)
}
}
複製代碼
vuex
主要有幾個應用場景,一個是用於組件通訊和狀態共享,一個是用於數據緩存,還有就是用於減小請求。這些場景歸根節點都是對於緩存和共享來講的。
首先,狀態統一管理在倉庫,就實現了組件通訊的可能性。
當一個組件經過 commit
提交 mutation
就改了 state
,其餘組件就能夠經過store.state
獲取最新的state
,這樣一來就至關於更新的值經過 store
傳遞給了其餘組件,不只實現了一對一的通訊,還實現了一對多的通訊。
咱們常用的一個場景就是權限管理。
寫權限管理時候,首次進入頁面就要將權限所有拿到,而後須要分發給各個頁面使用,來控制各個路由、按鈕的權限。
/** * 判斷用戶有沒有權限訪問頁面 */
function hasPermission(routeItem, menus) {
return menus.some(menuItem => {
return menuItem.name === routeItem.name
})
}
/** * 遞歸過濾異步路由表,返回符合用戶角色權限的路由表 * @param {*} routes 路由表 * @param {*} menus 菜單信息 */
function filterRoutes(routes, menus) {
return routes.filter(routeItem => {
if (routeItem.hidden) {
return true
} else if (hasPermission(routeItem, menus)) {
const menuItem = menus.find(item => item.name === routeItem.name)
if (menuItem && menuItem.children && menuItem.children.length > 0) {
routeItem.children = filterRoutes(routeItem.children, menuItem.children)
if (!routeItem.children.length) return false
}
return true
} else {
return false
}
})
}
const permission = {
state: {
routers: constantRouterMap,
addRouters: [],
roles: [],
user_name: '',
avatar_url: '',
onlyEditor: false,
menus: null,
personal: true,
teamList: []
},
mutations: {}
}
export default permission
複製代碼
並且權限還能夠被更改,更改後的權限直接分發到其餘頁面組件中。這個場景要是不使用 vuex
,代碼將會比較複雜。
store
是一個倉庫,它從建立開始就一直存在,只有頁面 Vuex.store
實例被銷燬,state 纔會被清空。具體表現就是刷新頁面。
這個數據緩存適用於:頁面加載後緩存數據,刷新頁面請求數據的場景。在通常Hybrid
中,通常不存在刷新頁面這個按鈕,因此使用 vuex 緩存數據能夠應對大多數場景。
export default {
state: {
// 緩存修改手機號須要的信息
changePhoneInfo: {
nameUser: '',
email: '',
phone: ''
},
}
}
複製代碼
若是須要持久化緩存,結合瀏覽器或 APP 緩存更佳。
export default {
// 登錄成功後,vuex 寫入token,並寫入app緩存,存儲持久化
[ActionTypes.LOGIN_SUCCESS]: function(store, token) {
store.commit(MutationTypes.SET_TOKEN, token)
setStorage('token', token)
router.replace({ name: 'Home', params: { source: 'login' } })
}
}
複製代碼
在寫後臺管理平臺時候,常常會有 list
選型組件,裏面數據從服務端拿的數據。若是咱們把這個 list
數據存儲起來,下次再次使用,直接從 store
裏面拿,這樣咱們就不用再去請求數據了。至關於減小了一次請求。
假設我如今有個需求,須要將性別0、一、2,分別轉換成男、女、不肯定這三個漢字展現。頁面中多處地方須要使用。
<template lang="pug">
.user-info
.gender
label(for="性別") 性別
span {{gender}}
</template>
複製代碼
完成這個需求,咱們知道有 4 種方式:
應該選擇哪一種方式呢?
我從下面三個方面來論述這個問題
1. 可實現性
computed
實現成功,咱們知道computed
不是一個函數是沒法傳參的,這裏有個技巧,return
一個函數接受傳過來的參數// ...
computed: {
convertIdToName() {
return function(value) {
const item = list.find(function(item) {
return item.id === value
})
return item.name
}
}
}
複製代碼
methods
實現成功,這裏直接能夠傳參數,一種常規的寫法。注意
methods
、computed
和data
相互以前是不能同名的
// ...
methods: {
convertIdToName(value) {
const item = list.find(function(item) {
return item.id === value
})
return item.name
}
}
複製代碼
utils
和 methods
差很少基本上也能夠實現filter
也是實現的,有個能夠和methods
、computed
同名哦filters: {
console.log(this.render)
convertIdToName(value) {
const item = list.find(function(item) {
return item.id === value
})
return item.name
}
},
複製代碼
總的來講他們所有均可以實現這個需求
2. 侷限性
computed
、methods
和data
三者互不一樣名,他們沒辦法被其餘組件使用,除非經過 mixins
filters
與 utils
沒法訪問 this
,也就是於響應式絕緣。可是經過定義全局filters
,能夠其餘地方使用,另外還能夠直接加載第三方filter
和utils
3. 總結比較
filters
與 utils
歸屬一對,他們既是脫離了 this
,得到了自由,又是被this
棄之門外。相反 methods
與 computed
與 this
牢牢站在一塊兒,但又是沒法得到自由。
export const thousandBitSeparator = (value) => {
return value && (value
.toString().indexOf('.') !== -1 ? value.toString().replace(/(\d)(?=(\d{3})+\.)/g, function($0, $1) {
return $1 + ',';
}) : value.toString().replace(/(\d)(?=(\d{3})+$)/g, function($0, $1) {
return $1 + ',';
}));
}
複製代碼
兩款插件
vue-filter:www.npmjs.com/package/vue… 使用 use
引入
vue2-filters:www.npmjs.com/package/vue… 使用mixins
引入
有須要的話,我通常就用第二個了,大多數都是本身寫一下小過濾器
自定義過濾器以後,直接全局自動註冊,其餘地方均可以使用
遍歷過濾屬性值,一次性所有註冊
for (const key in filters) {
Vue.filter(key, filters[key])
}
複製代碼
咱們思考一下測試 js 代碼須要哪些東西
若是是測試 vue 代碼呢? 那得再加一個 vue 測試容器
{
"@vue/cli-plugin-unit-jest": "^4.0.5",
"@vue/test-utils": "1.0.0-beta.29",
"jest": "^24.9.0",
// ...
}
複製代碼
// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html
module.exports = {
preset: '@vue/cli-plugin-unit-jest',
automock: false,
"/private/var/folders/10/bb2hb93j34999j9cqr587ts80000gn/T/jest_dx",
clearMocks: true,
// collectCoverageFrom: null,
coverageDirectory: 'tests/coverage'
//...
}
複製代碼
對咱們以前寫的一個性別名稱轉換工具進行測試
import { convertIdToName } from './convertIdToName'
describe('測試convertIdToName方法', () => {
const list = [
{ id: 0, name: '男' },
{ id: 1, name: '女' },
{ id: 2, name: '未知' }
]
it('測試正常輸入', () => {
const usage = list
usage.forEach((item) => {
expect(convertIdToName(item.id, list)).toBe(item.name)
})
})
it('測試非正常輸入', () => {
const usage = ['a', null, undefined, NaN]
usage.forEach((item) => {
expect(convertIdToName(item, list)).toBe('')
})
})
})
複製代碼
這樣一測試,發現原來咱們以前寫的工具備這麼多漏洞
export const convertIdToName = (value, list) => {
if (value !== 0 && value !== 1 && value !== 2) return ''
const item = list.find(function(item) {
return item.id === value
})
return item.name
}
複製代碼
如今測試都經過了呢
對咱們最簡單的
hello world
進行測試
<template lang="pug">
.hello
h1 {{ msg }}
</template>
<script> export default { props: { msg: String } } </script>
複製代碼
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
it('renders props.msg when passed', () => {
const msg = 'new message'
const wrapper = shallowMount(HelloWorld, {
propsData: { msg }
})
expect(wrapper.text()).toMatch(msg)
})
})
複製代碼
異步測試有幾種常見寫法
async
與 await
done()
簡單的異步測試,測試一個簡單的登錄請求
export const login = (data) => post('/user/login', data)
複製代碼
測試代碼
import { login } from '@/api/index'
describe('login api', () => {
const response = {
code: '1000',
data: {}
}
const errorResponse = {
code: '5000',
data: {},
message: '用戶名或密碼錯誤'
}
it('測試正常登錄', async () => {
const params = {
user: 'admin',
password: '123456'
}
expect(await login(params)).toEqual(response)
})
it('測試異常登錄', async () => {
const params = {
user: 'admin',
password: '123123'
}
expect(await login(params)).toEqual(errorResponse)
})
})
複製代碼
組件,api
,工具這些零零碎碎都測試了,並且這些都是比較通用、和業務關係不大的代碼,它們改動較少,測試到這裏其實已經足夠了,已經達到了 20%
的測試工做量了很大一部分代碼的目的。
爲何我說只有 20% 的工做量呢?由於這些都是不怎麼變化的邏輯,是一勞永逸的事情。長遠來講佔用的工做量確實不多。
可是有些狀況業務仍是必須得測,也就是必需要功能模塊集成測試。
常常得迴歸的業務,那種迭代對原有的系統比較大,避免改動以後使舊的代碼各類新的問題。這種常常回歸測試,採用 BDD
+ 集成測試,比不停改 bug
要輕鬆的多。
像版本同樣,每次測試以後生成一個版本,比較與上一個版本的區別。 這是一種粒度及其小的測試,能夠測試到每個符號。
這是咱們一個配置文件
export const api = {
develop: 'http://xxxx:8080',
mock: 'http://xxxx',
feature: 'http://xxxx',
test: 'http://xxxx',
production: 'http://xxxx'
}
export default api[process.env.NODE_ENV || 'dev']
複製代碼
使用快照測試
import { api } from './config'
describe('配置文件測試', () => {
it('測試配置文件是否變更', () => {
expect(api).toMatchSnapshot({
develop: 'http://xxxx:8080',
mock: 'http://xxxx',
feature: 'http://xxxx',
test: 'http://xxxx',
production: 'http://xxxx'
})
})
})
複製代碼
使用快照第一次測試後,經過測試,代碼被寫入快照
最近討論比較多的算是測試驅動開發和行爲驅動開發,其實總得來講是 4 種
bug
多,代碼質量低。那麼你是哪種? 反正我比較佛系哈,有的不寫測試,也有的寫滿測試。
本篇文章耗費做者一個多星期的業餘時間,存手工敲打 6000+字,同時收集,整理以前不少技巧和邊寫做邊思考和總結。若是能對你有幫助,即是它最大的價值。都看到這裏還不點贊,太過不去啦!😄
因爲技術水平有限,文章中若有錯誤地方,請在評論區指出,感謝!
文中大多數代碼將在suo-design-pro 中更新
項目有時間會盡可能完善
寫實踐總結性文章真的很耗費時間。如何文章中有幫到你的地方分享下唄,讓更多人看到!