面試被問到Vue?想進(jìn)一步提升?那就停下來看一下吧
Vue作為最近最炙手可熱的前端框架,其簡單的入門方式和功能強(qiáng)大的API是其優(yōu)點(diǎn)。而同時因?yàn)槠銩PI的多樣性和豐富性,所以他的很多開發(fā)方式就和一切基于組件的React不同,如果沒有對Vue的API(有一些甚至文檔都沒提到)有一個全面的了解,那么在開發(fā)和設(shè)計一個組件的時候有可能就會繞一個大圈子,所以我非常推薦各位在學(xué)習(xí)Vue的時候先要對Vue核心的所有API都有一個了解。這篇文章我會從實(shí)踐出發(fā),遇到一些知識點(diǎn)會順帶總結(jié)一下。文章很長,一次看不完可以先收藏,如果你剛?cè)腴Tvue,那么相信這篇文章對你以后的提升絕對有幫助。
進(jìn)入正題,我相信不論什么項(xiàng)目幾乎都會有一個必不可少的功能,就是用戶操作反饋、或者提醒,像這樣(簡單的一個demo)。
其實(shí)在vue的中大型項(xiàng)目中,這些類似的小功能會更加豐富以及嚴(yán)謹(jǐn),而在以Vue作為核心框架的前端項(xiàng)目中,因?yàn)閂ue本身是一個組件化和虛擬Dom的框架,要實(shí)現(xiàn)一個通知組件的展示當(dāng)然是非常簡單的。但因?yàn)橥ㄖM件的使用特性,直接在模板當(dāng)中書寫組件并通過v-show或者props控制通知組件的顯示顯然是非常不方便的并且這樣意味著你的代碼結(jié)構(gòu)要變,當(dāng)各種各樣的彈層變多的時候,我們都將其掛載到APP或者一個組件下顯然不太合理,而且如果要在action或者其他非組件場景中要用到通知,那么純組件模式的用法也無法實(shí)現(xiàn)。那么有沒有辦法即用到Vue組件化特性方便得實(shí)現(xiàn)一個通知組件的展現(xiàn),那么我們可否用一個方法來控制彈層組件的顯示和隱藏呢?
目標(biāo)一
實(shí)現(xiàn)一個簡單的反饋通知,可以通過方法在組件內(nèi)直接調(diào)用。比如Vue.$confirm({...obj})
首先,我們來實(shí)現(xiàn)通知組件,相信這個大部分人都能寫出來一個像模像樣的組件,不啰嗦,直接上代碼:
- <template>
- <div
- :class="type"
- class="eqc-notifier">
- <i
- :class="iconClass"
- class="icon fl"/>
- <span>{{ msg }}</span>
- <!-- <span class="close fr eqf-no" @click="close"></span> -->
- </div>
- </template>
- <script>
- export default {
- name: 'Notification',
- props: {
- type: {
- type: String,
- default: ''
- },
- msg: {
- type: String,
- default: ''
- }
- },
- computed: {
- iconClass() {
- switch (this.type) {
- case 'success':
- return 'eqf-info-f'
- case 'fail':
- return 'eqf-no-f'
- case 'info':
- return 'eqf-info-f'
- case 'warn':
- return 'eqf-alert-f'
- }
- }
- },
- mounted() {
- setTimeout(() => this.close(), 4000)
- },
- methods: {
- close() {
- }
- }
- }
- </script>
- <style lang="scss">
- .eqc-notifier {
- position: fixed;
- top: 68px;
- left: 50%;
- height: 36px;
- padding-right: 10px;
- line-height: 36px;
- box-shadow: 0 0 16px 0 rgba(0, 0, 0, 0.16);
- border-radius: 3px;
- background: #fff;
- z-index: 100; // 層級最高
- transform: translateX(-50%);
- animation: fade-in 0.3s;
- .icon {
- margin: 10px;
- font-size: 16px;
- }
- .close {
- margin: 8px;
- font-size: 20px;
- color: #666;
- transition: all 0.3s;
- cursor: pointer;
- &:hover {
- color: #ff296a;
- }
- }
- &.success {
- color: #1bc7b1;
- }
- &.fail {
- color: #ff296a;
- }
- &.info {
- color: #1593ff;
- }
- &.warn {
- color: #f89300;
- }
- &.close {
- animation: fade-out 0.3s;
- }
- }
- </style>
在這里需要注意,我們定義了一個close方法,但內(nèi)容是空的,雖然在模板上有用到,但是似乎沒什么意義,在后面我們要擴(kuò)展組件的時候我會講到為什么要這么做。
創(chuàng)建完這個組件之后,我們就可以在模板中使用了<notification type="xxx" msg="xxx" />
實(shí)現(xiàn)通過方法調(diào)用該通知組件
其實(shí)在實(shí)現(xiàn)通過方法調(diào)用之前,我們需要擴(kuò)展一下這個組件,因?yàn)閮H僅這些屬性,并不夠我們使用。在使用方法調(diào)用的時候,我們需要考慮一下幾個問題:
- 顯示反饋的定位
- 組件的出現(xiàn)和自動消失控制
- 連續(xù)多次調(diào)用通知方法,如何排版多個通知
在這個前提下,我們需要擴(kuò)展該組件,但是擴(kuò)展的這些屬性不能直接放在原組件內(nèi),因?yàn)檫@些可能會影響組件在模板內(nèi)的使用,那怎么辦呢?這時候我們就要用到Vue里面非常好用的一個API,extend,通過他去繼承原組件的屬性并擴(kuò)展他。
來看代碼
- import Notifier from './Notifier.vue'
- function install(Vue) {
- VueVue.notifier = Vue.prototype.notifier = {
- success,
- fail,
- info,
- warn
- }
- }
- function open(type, msg) {
- let UiNotifier = Vue.extend(Notifier)
- let vm = new UiNotifier({
- propsData: { type, msg },
- methods: {
- close: function () {
- let dialog = this.$el
- dialog.addEventListener('animationend', () => {
- document.body.removeChild(dialog)
- this.$destroy()
- })
- dialog.className = `${this.type} eqc-notifier close`
- dialog = null
- }
- }
- }).$mount()
- document.body.appendChild(vm.$el)
- }
- function success(msg) {
- open('success', msg)
- }
- function fail(msg) {
- open('fail', msg)
- }
- function info(msg) {
- open('info', msg)
- }
- function warn(msg) {
- open('warn', msg)
- }
- Vue.use(install)
- export default install
可以看到close方法在這里被實(shí)現(xiàn)了,那么為什么要在原組件上面加上那些方法的定義呢?因?yàn)樾枰谀0迳辖壎?,而模板是無法extend的,只能覆蓋,如果要覆蓋重新實(shí)現(xiàn),那擴(kuò)展的意義就不是很大了。其實(shí)這里只是一個消息彈窗組件,是可以在模板中就被實(shí)現(xiàn),還有插件怎么注入,大家都可以自己抉擇。
同時在使用extend的時候要注意:
- 方法和屬性的定義是直接覆蓋的
- 生命周期方法類似余mixin,會合并,也就是原組件和繼承之后的組件都會被調(diào)用,原組件先調(diào)用
首先通過 let UiNotifier = Vue.extend(Notifier),我們得到了一個類似于Vue的子類,接著就可以通過new UiNotifier({...options})的方式去創(chuàng)建Vue的實(shí)例了,同時通過該方式創(chuàng)建的實(shí)例,會有組件定義里面的所有屬性。
在創(chuàng)建實(shí)例之后,vm.$mount()手動將組件掛載到DOM上面,這樣我們可以不依賴Vue組件樹來輸出DOM片段,達(dá)到自由顯示通知的效果。
擴(kuò)展:
(
說一下$mount,我們也許很多項(xiàng)目的主文件是這樣的
- new Vue({
- router,
- store,
- el: '#app',
- render: h => h(App)
- })
其實(shí)el與$mount在使用效果上沒有任何區(qū)別,都是為了將實(shí)例化后的vue掛載到指定的dom元素中。如果在實(shí)例化vue的時候指定el,則該vue將會渲染在此el對應(yīng)的dom中,反之,若沒有指定el,則vue實(shí)例會處于一種“未掛載”的狀態(tài),此時可以通過$mount來手動執(zhí)行掛載。值得注意的是如果$mount沒有提供參數(shù),模板將被渲染為文檔之外的的元素,并且你必須使用原生DOM API把它插入文檔中,所以我上面寫的你應(yīng)該明白了吧!
- 這是$mount的一個源碼片段,其實(shí)$mount的方法支持傳入2個參數(shù)的,第一個是 el,它表示掛載的元素,可以是字符串,也可以是 DOM 對象,如果是字符串在瀏覽器環(huán)境下會調(diào)用 query 方法轉(zhuǎn)換成 DOM 對象的。第二個參數(shù)是和服務(wù)端渲染相關(guān),在瀏覽器環(huán)境下不需要傳第二個參數(shù)。
)
好了,我們現(xiàn)在其實(shí)就可以在組件中:
- this.notifier[state](msg)來調(diào)用了,是不是很方便?
進(jìn)階
我們剛才實(shí)現(xiàn)了在Vue中通過方法來進(jìn)行用戶反饋的提醒,再增加一個難度:
我們vue項(xiàng)目中應(yīng)該也遇到過這種情況,彈出一個對話框或是選擇框?不但要求用方法彈出,并且能接收到對話框交互所返回的結(jié)果。
這里就不詳細(xì)的分析了直接上代碼說(之前的代碼,用render來寫的組件,懶得改了,直接拿來用...),先創(chuàng)建一個對話框組件---Confirm.vue。
- <script>
- let __this = null
- export default {
- name: 'Confirm',
- data() {
- return {
- config: {
- msg: '',
- ifBtn: '',
- top: null
- }
- }
- },
- created() {
- __this = this
- },
- methods: {
- createBox(h) {
- let config = {}
- config.attrs = {
- id: '__confirm'
- }
- let children = []
- children.push(this.createContainer(h))
- children.push(this.createBg(h))
- return h('div', config, children)
- },
- createBg(h) {
- return h('div', {
- class: 'bg',
- on: {
- click: __this.$cancel
- }
- })
- },
- createContainer(h) {
- let config = {}
- config.class = {
- 'box-container': true
- }
- if (__this.config.top) {
- config.style = {
- 'top': __this.config.top + 'px',
- 'transform': 'translate(-50%, 0)'
- }
- }
- let children = []
- children.push(this.createContentBox(h))
- children.push(this.createClose(h))
- if (__this.config.ifBtn) {
- children.push(__this.createBtnBox(h))
- }
- return h('div', config, children)
- },
- createContentBox(h) {
- let config = {}
- config.class = {
- 'content-box': true
- }
- return h('div', config, [__this.createContent(h)])
- },
- createContent(h) {
- let config = {}
- config.domProps = {
- innerHTML: __this.config.msg
- }
- return h('p', config)
- },
- createClose(h) {
- return h('i', {
- class: 'eqf-no pointer close-btn',
- on: {
- click: __this.$cancel
- }
- })
- },
- createBtnBox(h) {
- return h(
- 'div', {
- class: {
- 'btn-box': true
- }
- }, [
- __this.createBtn(h, 'btn-cancel middle mr10', '取消', __this.$cancel),
- __this.createBtn(h, 'btn-primary middle mr10', '確定', __this.$confirm)
- ])
- },
- createBtn(h, styles, content, callBack) {
- return h('button', {
- class: styles,
- on: {
- click: callBack
- }
- }, content)
- }
- },
- render(h) {
- return this.createBox(h)
- }
- }
- </script>
- <style scoped>
- #__confirm {
- position: fixed;
- top: 0;
- left: 0;
- z-index: 10;
- width: 100%;
- height: 100%;
- }
- #__confirm .bg {
- position: fixed;
- top: 0;
- left: 0;
- z-index: 0;
- width: 100%;
- height: 100%;
- }
- #__confirm .box-container {
- position: absolute;
- width: 500px;
- padding: 20px;
- padding-top: 30px;
- border-radius: 3px;
- background: #fff;
- z-index: 1;
- box-shadow: 2px 2px 10px rgba(0,0,0,0.4);
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- }
- #__confirm .content-box {
- font-size: 14px;
- line-height: 20px;
- margin-bottom: 10px;
- }
- #__confirm .btn-box {
- margin-top: 20px;
- text-align: right;
- }
- #__confirm .close-btn {
- position: absolute;
- top: 15px;
- right: 20px;
- font-size: 16px;
- color: #666666;
- }
- #__confirm .close-btn:hover {
- color: #1593FF;
- }
- #__confirm .bg {
- position: fixed;
- }
- </style>
然后創(chuàng)建confirm.js:
- 'use strict'
- import Confirm from './Confirm.vue'
- const confirmConstructor = Vue.extend(Confirm)
- const ConfirmViewStyle = config => {
- const confirmInstance = new confirmConstructor({
- data() {
- return {
- config
- }
- }
- })
- confirmInstanceconfirmInstance.vm = confirmInstance.$mount()
- confirmInstanceconfirmInstance.dom = confirmInstance.vm.$el
- document.body.appendChild(confirmInstance.dom)
- }
- const close = () => {
- let dom = document.querySelector('body .modelServe-container')
- dom && dom.remove()
- Vue.prototype.$receive = null
- }
- const closeConfirm = () => {
- let dom = document.getElementById('__confirm')
- dom && dom.remove()
- Vue.prototype.$confirm = null
- }
- function install(Vue) {
- Vue.prototype.modelServe = {
- confirm: (obj) => {
- return new Promise(resolve => {
- Vue.prototype.$confirm = (data) => {
- resolve(data)
- closeConfirm()
- }
- ConfirmViewStyle(obj)
- })
- }
- }
- Vue.prototype.$dismiss = close
- Vue.prototype.$cancel = closeConfirm
- }
- Vue.use(install)
- export default install
思路很簡單,在我們創(chuàng)建的時候同時返回一個promise,同時將resolve通行證暴露給vue的一個全局方法也就是將控制權(quán)暴露給外部,這樣我們就可以向這樣,我上面的confiram.vue是直接把取消綁定成了$cancel,把確定綁定成了$confirm,所以點(diǎn)擊確定會進(jìn)入full,也就是.then中,當(dāng)然你也可以傳參數(shù)。
- this.modelServe.confirm({
- msg: '返回后數(shù)據(jù)不會被保存,確認(rèn)?',
- ifBtn: true
- }).then(_ => {
- this.goBack()
- }).catch()
寫的有點(diǎn)多,其實(shí)還可以擴(kuò)展出好多技巧,比如模態(tài)框中傳一個完整的組件,并展示出來,簡單地寫一下,其實(shí)只需改動一點(diǎn)。
- import Model from './Model.vue'
- const modelConstructor = Vue.extend(Model)
- const modelViewStyle = (obj) => {
- let component = obj.component
- const modelViewInstance = new modelConstructor({
- data() {
- return {
- disabledClick: obj.stopClick // 是否禁止點(diǎn)擊遮罩層關(guān)閉
- }
- }
- })
- let app = document.getElementById('container')
- modelViewInstancemodelViewInstance.vm = modelViewInstance.$mount()
- modelViewInstancemodelViewInstance.dom = modelViewInstance.vm.$el
- app.appendChild(modelViewInstance.dom)
- new Vue({
- el: '#__model__',
- mixins: [component],
- data() {
- return {
- serveObj: obj.obj
- }
- }
- })
- }
- ...
- Vue.prototype.modelServe = {
- open: (obj) => {
- return new Promise(resolve => {
- modelViewStyle(obj, resolve)
- Vue.prototype.$receive = (data) => {
- resolve(data)
- close()
- }
- })
- }
- }
調(diào)用:
- sendCallBack() {
- this.modelServe.open({
- component: AddCallback,
- stopClick: true
- }).then(data =>
- if (data === 1) {
- this.addInit()
- } else {
- this.goBack()
- }
- })
},
這里我們用了mixins,最后最后再簡單地介紹一下mixins,extend,extends的區(qū)別。
**- Vue.extend使用基礎(chǔ) Vue 構(gòu)造器,創(chuàng)建一個“子類”。參數(shù)是一個包含組件選項(xiàng)的對象。
- mixins 選項(xiàng)接受一個混入對象的數(shù)組。這些混入實(shí)例對象可以像正常的實(shí)例對象一樣包含選項(xiàng),他們將在 Vue.extend() 里最終選擇使用相同的選項(xiàng)合并邏輯合并。舉例:如果你的混入包含一個鉤子而創(chuàng)建組件本身也有一個,兩個函數(shù)將被調(diào)用。Mixin 鉤子按照傳入順序依次調(diào)用,并在調(diào)用組件自身的鉤子之前被調(diào)用。
注意(data混入組件數(shù)據(jù)優(yōu)先鉤子函數(shù)將混合為一個數(shù)組,混入對象的鉤子將在組件自身鉤子之前調(diào)用,值為對象的選項(xiàng),例如 methods, components 和 directives,將被混合為同一個對象。兩個對象鍵名沖突時,取組件對象的鍵值對。)
- extends 允許聲明擴(kuò)展另一個組件(可以是一個簡單的選項(xiàng)對象或構(gòu)造函數(shù)),而無需使用 Vue.extend。這主要是為了便于擴(kuò)展單文件組件。這和 mixins 類似。**
概括
- extend用于創(chuàng)建vue實(shí)例
- mixins可以混入多個mixin,extends只能繼承一個
- mixins類似于面向切面的編程(AOP),extends類似于面向?qū)ο蟮木幊?nbsp;
- 優(yōu)先級Vue.extend>extends>mixins
總結(jié)
到這里,關(guān)于如何實(shí)現(xiàn)通過方法調(diào)用一個Vue組件內(nèi)容以及用到的一些API以及原理就差不多了,代碼如有不懂得地方可以隨時提問,歡迎交流。