中文亚洲精品无码_熟女乱子伦免费_人人超碰人人爱国产_亚洲熟妇女综合网

當(dāng)前位置: 首頁 > news >正文

網(wǎng)站建設(shè)入門教程對(duì)網(wǎng)絡(luò)營(yíng)銷的認(rèn)識(shí)800字

網(wǎng)站建設(shè)入門教程,對(duì)網(wǎng)絡(luò)營(yíng)銷的認(rèn)識(shí)800字,工商局網(wǎng)上注冊(cè)公司,官方網(wǎng)站免費(fèi)建設(shè)action 與 mutation 的區(qū)別 mutation 是同步更新, $watch 嚴(yán)格模式下會(huì)報(bào)錯(cuò) action 是異步操作,可以獲取數(shù)據(jù)后調(diào)用 mutation 提交最終數(shù)據(jù) MVVM的優(yōu)缺點(diǎn)? 優(yōu)點(diǎn): 分離視圖(View)和模型(Model)&#xff…

action 與 mutation 的區(qū)別

  • mutation 是同步更新, $watch 嚴(yán)格模式下會(huì)報(bào)錯(cuò)
  • action 是異步操作,可以獲取數(shù)據(jù)后調(diào)用 mutation 提交最終數(shù)據(jù)

MVVM的優(yōu)缺點(diǎn)?

優(yōu)點(diǎn):

  • 分離視圖(View)和模型(Model),降低代碼耦合,提?視圖或者邏輯的重?性: ?如視圖(View)可以獨(dú)?于Model變化和修改,?個(gè)ViewModel可以綁定不同的"View"上,當(dāng)View變化的時(shí)候Model不可以不變,當(dāng)Model變化的時(shí)候View也可以不變。你可以把?些視圖邏輯放在?個(gè)ViewModel??,讓很多view重?這段視圖邏輯
  • 提?可測(cè)試性: ViewModel的存在可以幫助開發(fā)者更好地編寫測(cè)試代碼
  • ?動(dòng)更新dom: 利?雙向綁定,數(shù)據(jù)更新后視圖?動(dòng)更新,讓開發(fā)者從繁瑣的?動(dòng)dom中解放

缺點(diǎn):

  • Bug很難被調(diào)試: 因?yàn)槭?雙向綁定的模式,當(dāng)你看到界?異常了,有可能是你View的代碼有Bug,也可能是Model的代碼有問題。數(shù)據(jù)綁定使得?個(gè)位置的Bug被快速傳遞到別的位置,要定位原始出問題的地?就變得不那么容易了。另外,數(shù)據(jù)綁定的聲明是指令式地寫在View的模版當(dāng)中的,這些內(nèi)容是沒辦法去打斷點(diǎn)debug的
  • ?個(gè)?的模塊中model也會(huì)很?,雖然使??便了也很容易保證了數(shù)據(jù)的?致性,當(dāng)時(shí)?期持有,不釋放內(nèi)存就造成了花費(fèi)更多的內(nèi)存
  • 對(duì)于?型的圖形應(yīng)?程序,視圖狀態(tài)較多,ViewModel的構(gòu)建和維護(hù)的成本都會(huì)?較?。

描述下Vue自定義指令

在 Vue2.0 中,代碼復(fù)用和抽象的主要形式是組件。然而,有的情況下,你仍然需要對(duì)普通 DOM 元素進(jìn)行底層操作,這時(shí)候就會(huì)用到自定義指令。
一般需要對(duì)DOM元素進(jìn)行底層操作時(shí)使用,盡量只用來操作 DOM展示,不修改內(nèi)部的值。當(dāng)使用自定義指令直接修改 value 值時(shí)綁定v-model的值也不會(huì)同步更新;如必須修改可以在自定義指令中使用keydown事件,在vue組件中使用 change事件,回調(diào)中修改vue數(shù)據(jù);

(1)自定義指令基本內(nèi)容

  • 全局定義:Vue.directive("focus",{})

  • 局部定義:directives:{focus:{}}

  • 鉤子函數(shù):指令定義對(duì)象提供鉤子函數(shù)

    o bind:只調(diào)用一次,指令第一次綁定到元素時(shí)調(diào)用。在這里可以進(jìn)行一次性的初始化設(shè)置。

    o inSerted:被綁定元素插入父節(jié)點(diǎn)時(shí)調(diào)用(僅保證父節(jié)點(diǎn)存在,但不一定已被插入文檔中)。

    o update:所在組件的VNode更新時(shí)調(diào)用,但是可能發(fā)生在其子VNode更新之前調(diào)用。指令的值可能發(fā)生了改變,也可能沒有。但是可以通過比較更新前后的值來忽略不必要的模板更新。

    o ComponentUpdate:指令所在組件的 VNode及其子VNode全部更新后調(diào)用。

    o unbind:只調(diào)用一次,指令與元素解綁時(shí)調(diào)用。

  • 鉤子函數(shù)參數(shù)
    o el:綁定元素

    o bing: 指令核心對(duì)象,描述指令全部信息屬性

    o name

    o value

    o oldValue

    o expression

    o arg

    o modifers

    o vnode 虛擬節(jié)點(diǎn)

    o oldVnode:上一個(gè)虛擬節(jié)點(diǎn)(更新鉤子函數(shù)中才有用)

(2)使用場(chǎng)景

  • 普通DOM元素進(jìn)行底層操作的時(shí)候,可以使用自定義指令

  • 自定義指令是用來操作DOM的。盡管Vue推崇數(shù)據(jù)驅(qū)動(dòng)視圖的理念,但并非所有情況都適合數(shù)據(jù)驅(qū)動(dòng)。自定義指令就是一種有效的補(bǔ)充和擴(kuò)展,不僅可用于定義任何的DOM操作,并且是可復(fù)用的。

(3)使用案例

初級(jí)應(yīng)用:

  • 鼠標(biāo)聚焦
  • 下拉菜單
  • 相對(duì)時(shí)間轉(zhuǎn)換
  • 滾動(dòng)動(dòng)畫

高級(jí)應(yīng)用:

  • 自定義指令實(shí)現(xiàn)圖片懶加載
  • 自定義指令集成第三方插件

Vue-Router 的懶加載如何實(shí)現(xiàn)

非懶加載:

import List from '@/components/list.vue'
const router = new VueRouter({routes: [{ path: '/list', component: List }]
})

(1)方案一(常用):使用箭頭函數(shù)+import動(dòng)態(tài)加載

const List = () => import('@/components/list.vue')
const router = new VueRouter({routes: [{ path: '/list', component: List }]
})

(2)方案二:使用箭頭函數(shù)+require動(dòng)態(tài)加載

const router = new Router({routes: [{path: '/list',component: resolve => require(['@/components/list'], resolve)}]
})

(3)方案三:使用webpack的require.ensure技術(shù),也可以實(shí)現(xiàn)按需加載。 這種情況下,多個(gè)路由指定相同的chunkName,會(huì)合并打包成一個(gè)js文件。

// r就是resolve
const List = r => require.ensure([], () => r(require('@/components/list')), 'list');
// 路由也是正常的寫法  這種是官方推薦的寫的 按模塊劃分懶加載 
const router = new Router({routes: [{path: '/list',component: List,name: 'list'}]
}))

Vue 3.0 中的 Vue Composition API?

在 Vue2 中,代碼是 Options API 風(fēng)格的,也就是通過填充 (option) data、methods、computed 等屬性來完成一個(gè) Vue 組件。這種風(fēng)格使得 Vue 相對(duì)于 React極為容易上手,同時(shí)也造成了幾個(gè)問題:

  1. 由于 Options API 不夠靈活的開發(fā)方式,使得Vue開發(fā)缺乏優(yōu)雅的方法來在組件間共用代碼。
  2. Vue 組件過于依賴this上下文,Vue 背后的一些小技巧使得 Vue 組件的開發(fā)看起來與 JavaScript 的開發(fā)原則相悖,比如在methods 中的this竟然指向組件實(shí)例來不指向methods所在的對(duì)象。這也使得 TypeScript 在Vue2 中很不好用。

于是在 Vue3 中,舍棄了 Options API,轉(zhuǎn)而投向 Composition API。Composition API本質(zhì)上是將 Options API 背后的機(jī)制暴露給用戶直接使用,這樣用戶就擁有了更多的靈活性,也使得 Vue3 更適合于 TypeScript 結(jié)合。

如下,是一個(gè)使用了 Vue Composition API 的 Vue3 組件:

<template><button @click="increment">Count: {{ count }}  </button>
</template><script>
// Composition API 將組件屬性暴露為函數(shù),因此第一步是導(dǎo)入所需的函數(shù)
import { ref, computed, onMounted } from 'vue'export default {  setup() {
// 使用 ref 函數(shù)聲明了稱為 count 的響應(yīng)屬性,對(duì)應(yīng)于Vue2中的data函數(shù)const count = ref(0) 
// Vue2中需要在methods option中聲明的函數(shù),現(xiàn)在直接聲明function increment() {      count.value++    } // 對(duì)應(yīng)于Vue2中的mounted聲明周期onMounted(() => console.log('component mounted!'))     return {      count,      increment    }  }}
</script>

顯而易見,Vue Composition API 使得 Vue3 的開發(fā)風(fēng)格更接近于原生 JavaScript,帶給開發(fā)者更多地靈活性

SPA、SSR的區(qū)別是什么

我們現(xiàn)在編寫的Vue、ReactAngular應(yīng)用大多數(shù)情況下都會(huì)在一個(gè)頁面中,點(diǎn)擊鏈接跳轉(zhuǎn)頁面通常是內(nèi)容切換而非頁面跳轉(zhuǎn),由于良好的用戶體驗(yàn)逐漸成為主流的開發(fā)模式。但同時(shí)也會(huì)有首屏加載時(shí)間長(zhǎng),SEO不友好的問題,因此有了SSR,這也是為什么面試中會(huì)問到兩者的區(qū)別

  1. SPA(Single Page Application)即單頁面應(yīng)用。一般也稱為 客戶端渲染(Client Side Render), 簡(jiǎn)稱 CSR。SSR(Server Side Render)即 服務(wù)端渲染。一般也稱為 多頁面應(yīng)用(Mulpile Page Application),簡(jiǎn)稱 MPA
  2. SPA應(yīng)用只會(huì)首次請(qǐng)求html文件,后續(xù)只需要請(qǐng)求JSON數(shù)據(jù)即可,因此用戶體驗(yàn)更好,節(jié)約流量,服務(wù)端壓力也較小。但是首屏加載的時(shí)間會(huì)變長(zhǎng),而且SEO不友好。為了解決以上缺點(diǎn),就有了SSR方案,由于HTML內(nèi)容在服務(wù)器一次性生成出來,首屏加載快,搜索引擎也可以很方便的抓取頁面信息。但同時(shí)SSR方案也會(huì)有性能,開發(fā)受限等問題
  3. 在選擇上,如果我們的應(yīng)用存在首屏加載優(yōu)化需求,SEO需求時(shí),就可以考慮SSR
  4. 但并不是只有這一種替代方案,比如對(duì)一些不常變化的靜態(tài)網(wǎng)站,SSR反而浪費(fèi)資源,我們可以考慮預(yù)渲染(prerender)方案。另外nuxt.js/next.js中給我們提供了SSG(Static Site Generate)靜態(tài)網(wǎng)站生成方案也是很好的靜態(tài)站點(diǎn)解決方案,結(jié)合一些CI手段,可以起到很好的優(yōu)化效果,且能節(jié)約服務(wù)器資源

內(nèi)容生成上的區(qū)別:

SSR

SPA

部署上的區(qū)別

參考 前端進(jìn)階面試題詳細(xì)解答

delete和Vue.delete刪除數(shù)組的區(qū)別?

  • delete只是被刪除的元素變成了 empty/undefined 其他的元素的鍵值還是不變。
  • Vue.delete直接刪除了數(shù)組 改變了數(shù)組的鍵值。
var a=[1,2,3,4]
var b=[1,2,3,4]
delete a[0]
console.log(a)  //[empty,2,3,4]
this.$delete(b,0)
console.log(b)  //[2,3,4]

說說你對(duì)slot的理解?slot使用場(chǎng)景有哪些

一、slot是什么

在HTML中 slot 元素 ,作為 Web Components 技術(shù)套件的一部分,是Web組件內(nèi)的一個(gè)占位符

該占位符可以在后期使用自己的標(biāo)記語言填充

舉個(gè)栗子

<template id="element-details-template"><slot name="element-name">Slot template</slot>
</template>
<element-details><span slot="element-name">1</span>
</element-details>
<element-details><span slot="element-name">2</span>
</element-details>

template不會(huì)展示到頁面中,需要用先獲取它的引用,然后添加到DOM中,

customElements.define('element-details',class extends HTMLElement {constructor() {super();const template = document.getElementById('element-details-template').content;const shadowRoot = this.attachShadow({mode: 'open'}).appendChild(template.cloneNode(true));}
})

Vue中的概念也是如此

Slot 藝名插槽,花名“占坑”,我們可以理解為solt在組件模板中占好了位置,當(dāng)使用該組件標(biāo)簽時(shí)候,組件標(biāo)簽里面的內(nèi)容就會(huì)自動(dòng)填坑(替換組件模板中slot位置),作為承載分發(fā)內(nèi)容的出口

二、使用場(chǎng)景

通過插槽可以讓用戶可以拓展組件,去更好地復(fù)用組件和對(duì)其做定制化處理

如果父組件在使用到一個(gè)復(fù)用組件的時(shí)候,獲取這個(gè)組件在不同的地方有少量的更改,如果去重寫組件是一件不明智的事情

通過slot插槽向組件內(nèi)部指定位置傳遞內(nèi)容,完成這個(gè)復(fù)用組件在不同場(chǎng)景的應(yīng)用

比如布局組件、表格列、下拉選、彈框顯示內(nèi)容等

如果讓你從零開始寫一個(gè)vuex,說說你的思路

思路分析

這個(gè)題目很有難度,首先思考vuex解決的問題:存儲(chǔ)用戶全局狀態(tài)并提供管理狀態(tài)API。

  • vuex需求分析
  • 如何實(shí)現(xiàn)這些需求

回答范例

  1. 官方說vuex是一個(gè)狀態(tài)管理模式和庫(kù),并確保這些狀態(tài)以可預(yù)期的方式變更。可見要實(shí)現(xiàn)一個(gè)vuex
  • 要實(shí)現(xiàn)一個(gè)Store存儲(chǔ)全局狀態(tài)
  • 要提供修改狀態(tài)所需API:commit(type, payload), dispatch(type, payload)
  1. 實(shí)現(xiàn)Store時(shí),可以定義Store類,構(gòu)造函數(shù)接收選項(xiàng)options,設(shè)置屬性state對(duì)外暴露狀態(tài),提供commitdispatch修改屬性state。這里需要設(shè)置state為響應(yīng)式對(duì)象,同時(shí)將Store定義為一個(gè)Vue插件
  2. commit(type, payload)方法中可以獲取用戶傳入mutations并執(zhí)行它,這樣可以按用戶提供的方法修改狀態(tài)。 dispatch(type, payload)類似,但需要注意它可能是異步的,需要返回一個(gè)Promise給用戶以處理異步結(jié)果

實(shí)踐

Store的實(shí)現(xiàn):

class Store {constructor(options) {this.state = reactive(options.state)this.options = options}commit(type, payload) {this.options.mutations[type].call(this, this.state, payload)}
}

vuex簡(jiǎn)易版

/*** 1 實(shí)現(xiàn)插件,掛載$store* 2 實(shí)現(xiàn)store*/let Vue;class Store {constructor(options) {// state響應(yīng)式處理// 外部訪問: this.$store.state.***// 第一種寫法// this.state = new Vue({//   data: options.state// })// 第二種寫法:防止外界直接接觸內(nèi)部vue實(shí)例,防止外部強(qiáng)行變更this._vm = new Vue({data: {$$state: options.state}})this._mutations = options.mutationsthis._actions = options.actionsthis.getters = {}options.getters && this.handleGetters(options.getters)this.commit = this.commit.bind(this)this.dispatch = this.dispatch.bind(this)}get state () {return this._vm._data.$$state}set state (val) {return new Error('Please use replaceState to reset state')}handleGetters (getters) {Object.keys(getters).map(key => {Object.defineProperty(this.getters, key, {get: () => getters[key](this.state)})})}commit (type, payload) {let entry = this._mutations[type]if (!entry) {return new Error(`${type} is not defined`)}entry(this.state, payload)}dispatch (type, payload) {let entry = this._actions[type]if (!entry) {return new Error(`${type} is not defined`)}entry(this, payload)}
}const install = (_Vue) => {Vue = _VueVue.mixin({beforeCreate () {if (this.$options.store) {Vue.prototype.$store = this.$options.store}},})
}export default { Store, install }

驗(yàn)證方式

import Vue from 'vue'
import Vuex from './vuex'
// this.$store
Vue.use(Vuex)export default new Vuex.Store({state: {counter: 0},mutations: {// state從哪里來的add (state) {state.counter++}},getters: {doubleCounter (state) {return state.counter * 2}},actions: {add ({ commit }) {setTimeout(() => {commit('add')}, 1000)}},modules: {}
})

Vue中v-html會(huì)導(dǎo)致哪些問題

  • 可能會(huì)導(dǎo)致 xss 攻擊
  • v-html 會(huì)替換掉標(biāo)簽內(nèi)部的子元素
let template = require('vue-template-compiler'); 
let r = template.compile(`<div v-html="'<span>hello</span>'"></div>`) // with(this){return _c('div',{domProps: {"innerHTML":_s('<span>hello</span>')}})} 
console.log(r.render);// _c 定義在core/instance/render.js 
// _s 定義在core/instance/render-helpers/index,js
if (key === 'textContent' || key === 'innerHTML') { if (vnode.children) vnode.children.length = 0 if (cur === oldProps[key]) continue // #6601 work around Chrome version <= 55 bug where single textNode // replaced by innerHTML/textContent retains its parentNode property if (elm.childNodes.length === 1) { elm.removeChild(elm.childNodes[0]) } 
}

v-model實(shí)現(xiàn)原理

我們?cè)?vue 項(xiàng)目中主要使用 v-model 指令在表單 input、textareaselect 等元素上創(chuàng)建雙向數(shù)據(jù)綁定,我們知道 v-model 本質(zhì)上不過是語法糖(可以看成是value + input方法的語法糖),v-model 在內(nèi)部為不同的輸入元素使用不同的屬性并拋出不同的事件:

  • texttextarea 元素使用 value 屬性和 input 事件
  • checkboxradio 使用 checked 屬性和 change 事件
  • select 字段將 value 作為 prop 并將 change 作為事件

所以我們可以v-model進(jìn)行如下改寫:

<input v-model="sth" />
<!-- 等同于 -->
<input :value="sth" @input="sth = $event.target.value" />

當(dāng)在input元素中使用v-model實(shí)現(xiàn)雙數(shù)據(jù)綁定,其實(shí)就是在輸入的時(shí)候觸發(fā)元素的input事件,通過這個(gè)語法糖,實(shí)現(xiàn)了數(shù)據(jù)的雙向綁定

  • 這個(gè)語法糖必須是固定的,也就是說屬性必須為value,方法名必須為:input。
  • 知道了v-model的原理,我們可以在自定義組件上實(shí)現(xiàn)v-model
//Parent
<template>{{num}}<Child v-model="num">
</template>
export default {data(){return {num: 0}}
}//Child
<template><div @click="add">Add</div>
</template>
export default {props: ['value'], // 屬性必須為valuemethods:{add(){// 方法名為inputthis.$emit('input', this.value + 1)}}
}

原理

會(huì)將組件的 v-model 默認(rèn)轉(zhuǎn)化成value+input

const VueTemplateCompiler = require('vue-template-compiler'); 
const ele = VueTemplateCompiler.compile('<el-checkbox v-model="check"></el- checkbox>'); // 觀察輸出的渲染函數(shù):
// with(this) { 
//     return _c('el-checkbox', { 
//         model: { 
//             value: (check), 
//             callback: function ($$v) { check = $$v }, 
//             expression: "check" 
//         } 
//     }) 
// }
// 源碼位置 core/vdom/create-component.js line:155function transformModel (options, data: any) { const prop = (options.model && options.model.prop) || 'value' const event = (options.model && options.model.event) || 'input' ;(data.attrs || (data.attrs = {}))[prop] = data.model.value const on = data.on || (data.on = {}) const existing = on[event] const callback = data.model.callback if (isDef(existing)) { if (Array.isArray(existing) ? existing.indexOf(callback) === -1 : existing !== callback ) {on[event] = [callback].concat(existing) } } else { on[event] = callback } 
}

原生的 v-model,會(huì)根據(jù)標(biāo)簽的不同生成不同的事件和屬性

const VueTemplateCompiler = require('vue-template-compiler'); 
const ele = VueTemplateCompiler.compile('<input v-model="value"/>');// with(this) { 
//     return _c('input', { 
//         directives: [{ name: "model", rawName: "v-model", value: (value), expression: "value" }], 
//         domProps: { "value": (value) },
//         on: {"input": function ($event) { 
//             if ($event.target.composing) return;
//             value = $event.target.value
//         }
//         }
//     })
// }

編譯時(shí):不同的標(biāo)簽解析出的內(nèi)容不一樣 platforms/web/compiler/directives/model.js

if (el.component) { genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime return false 
} else if (tag === 'select') { genSelect(el, value, modifiers) 
} else if (tag === 'input' && type === 'checkbox') { genCheckboxModel(el, value, modifiers) 
} else if (tag === 'input' && type === 'radio') { genRadioModel(el, value, modifiers) 
} else if (tag === 'input' || tag === 'textarea') { genDefaultModel(el, value, modifiers) 
} else if (!config.isReservedTag(tag)) { genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime return false 
}

運(yùn)行時(shí):會(huì)對(duì)元素處理一些關(guān)于輸入法的問題 platforms/web/runtime/directives/model.js

inserted (el, binding, vnode, oldVnode) { if (vnode.tag === 'select') { // #6903 if (oldVnode.elm && !oldVnode.elm._vOptions) { mergeVNodeHook(vnode, 'postpatch', () => { directive.componentUpdated(el, binding, vnode) }) } else { setSelected(el, binding, vnode.context) }el._vOptions = [].map.call(el.options, getValue) } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) { el._vModifiers = binding.modifiers if (!binding.modifiers.lazy) { el.addEventListener('compositionstart', onCompositionStart) el.addEventListener('compositionend', onCompositionEnd) // Safari < 10.2 & UIWebView doesn't fire compositionend when // switching focus before confirming composition choice // this also fixes the issue where some browsers e.g. iOS Chrome// fires "change" instead of "input" on autocomplete. el.addEventListener('change', onCompositionEnd) /* istanbul ignore if */ if (isIE9) { el.vmodel = true }}}
}

子組件可以直接改變父組件的數(shù)據(jù)么,說明原因

這是一個(gè)實(shí)踐知識(shí)點(diǎn),組件化開發(fā)過程中有個(gè)單項(xiàng)數(shù)據(jù)流原則,不在子組件中修改父組件是個(gè)常識(shí)問題

思路

  • 講講單項(xiàng)數(shù)據(jù)流原則,表明為何不能這么做
  • 舉幾個(gè)常見場(chǎng)景的例子說說解決方案
  • 結(jié)合實(shí)踐講講如果需要修改父組件狀態(tài)應(yīng)該如何做

回答范例

  1. 所有的 prop 都使得其父子之間形成了一個(gè)單向下行綁定:父級(jí) prop 的更新會(huì)向下流動(dòng)到子組件中,但是反過來則不行。這樣會(huì)防止從子組件意外變更父級(jí)組件的狀態(tài),從而導(dǎo)致你的應(yīng)用的數(shù)據(jù)流向難以理解。另外,每次父級(jí)組件發(fā)生變更時(shí),子組件中所有的 prop 都將會(huì)刷新為最新的值。這意味著你不應(yīng)該在一個(gè)子組件內(nèi)部改變 prop。如果你這樣做了,Vue 會(huì)在瀏覽器控制臺(tái)中發(fā)出警告
const props = defineProps(['foo'])
// ? 下面行為會(huì)被警告, props是只讀的!
props.foo = 'bar'
  1. 實(shí)際開發(fā)過程中有兩個(gè)場(chǎng)景會(huì)想要修改一個(gè)屬性:

這個(gè) prop 用來傳遞一個(gè)初始值;這個(gè)子組件接下來希望將其作為一個(gè)本地的 prop 數(shù)據(jù)來使用。 在這種情況下,最好定義一個(gè)本地的 data,并將這個(gè) prop 用作其初始值:

const props = defineProps(['initialCounter'])
const counter = ref(props.initialCounter)

這個(gè) prop 以一種原始的值傳入且需要進(jìn)行轉(zhuǎn)換。 在這種情況下,最好使用這個(gè) prop 的值來定義一個(gè)計(jì)算屬性:

const props = defineProps(['size'])
// prop變化,計(jì)算屬性自動(dòng)更新
const normalizedSize = computed(() => props.size.trim().toLowerCase())
  1. 實(shí)踐中如果確實(shí)想要改變父組件屬性應(yīng)該emit一個(gè)事件讓父組件去做這個(gè)變更。注意雖然我們不能直接修改一個(gè)傳入的對(duì)象或者數(shù)組類型的prop,但是我們還是能夠直接改內(nèi)嵌的對(duì)象或?qū)傩?/li>

怎么緩存當(dāng)前的組件?緩存后怎么更新

緩存組件使用keep-alive組件,這是一個(gè)非常常見且有用的優(yōu)化手段,vue3keep-alive有比較大的更新,能說的點(diǎn)比較多

思路

  • 緩存用keep-alive,它的作用與用法
  • 使用細(xì)節(jié),例如緩存指定/排除、結(jié)合routertransition
  • 組件緩存后更新可以利用activated或者beforeRouteEnter
  • 原理闡述

回答范例

  1. 開發(fā)中緩存組件使用keep-alive組件,keep-alivevue內(nèi)置組件,keep-alive包裹動(dòng)態(tài)組件component時(shí),會(huì)緩存不活動(dòng)的組件實(shí)例,而不是銷毀它們,這樣在組件切換過程中將狀態(tài)保留在內(nèi)存中,防止重復(fù)渲染DOM
<keep-alive><component :is="view"></component>
</keep-alive>
  1. 結(jié)合屬性includeexclude可以明確指定緩存哪些組件或排除緩存指定組件。vue3中結(jié)合vue-router時(shí)變化較大,之前是keep-alive包裹router-view,現(xiàn)在需要反過來用router-view包裹keep-alive
<router-view v-slot="{ Component }"><keep-alive><component :is="Component"></component></keep-alive>
</router-view>
  1. 緩存后如果要獲取數(shù)據(jù),解決方案可以有以下兩種
  • beforeRouteEnter:在有vue-router的項(xiàng)目,每次進(jìn)入路由的時(shí)候,都會(huì)執(zhí)行beforeRouteEnter
beforeRouteEnter(to, from, next){next(vm=>{console.log(vm)// 每次進(jìn)入路由執(zhí)行vm.getData()  // 獲取數(shù)據(jù)})
},
  • actived:在keep-alive緩存的組件被激活的時(shí)候,都會(huì)執(zhí)行actived鉤子
activated(){this.getData() // 獲取數(shù)據(jù)
},
  1. keep-alive是一個(gè)通用組件,它內(nèi)部定義了一個(gè)map,緩存創(chuàng)建過的組件實(shí)例,它返回的渲染函數(shù)內(nèi)部會(huì)查找內(nèi)嵌的component組件對(duì)應(yīng)組件的vnode,如果該組件在map中存在就直接返回它。由于componentis屬性是個(gè)響應(yīng)式數(shù)據(jù),因此只要它變化,keep-aliverender函數(shù)就會(huì)重新執(zhí)行

Vue中修飾符.sync與v-model的區(qū)別

sync的作用

  • .sync修飾符可以實(shí)現(xiàn)父子組件之間的雙向綁定,并且可以實(shí)現(xiàn)子組件同步修改父組件的值,相比較與v-model來說,sync修飾符就簡(jiǎn)單很多了
  • 一個(gè)組件上可以有多個(gè).sync修飾符
<!-- 正常父?jìng)髯?-->
<Son :a="num" :b="num2" /><!-- 加上sync之后的父?jìng)髯?-->
<Son :a.sync="num" :b.sync="num2" /><!-- 它等價(jià)于 -->
<Son :a="num" :b="num2" @update:a="val=>num=val" @update:b="val=>num2=val" 
/><!-- 相當(dāng)于多了一個(gè)事件監(jiān)聽,事件名是update:a, -->
<!-- 回調(diào)函數(shù)中,會(huì)把接收到的值賦值給屬性綁定的數(shù)據(jù)項(xiàng)中。 -->

v-model的工作原理

<com1 v-model="num"></com1>
<!-- 等價(jià)于 -->
<com1 :value="num" @input="(val)=>num=val"></com1>
  • 相同點(diǎn)
    • 都是語法糖,都可以實(shí)現(xiàn)父子組件中的數(shù)據(jù)的雙向通信
  • 區(qū)別點(diǎn)
    • 格式不同:v-model="num", :num.sync="num"
    • v-model: @input + value
    • :num.sync: @update:num
    • v-model只能用一次;.sync可以有多個(gè)

Vue組件之間通信方式有哪些

Vue 組件間通信是面試??嫉闹R(shí)點(diǎn)之一,這題有點(diǎn)類似于開放題,你回答出越多方法當(dāng)然越加分,表明你對(duì) Vue 掌握的越熟練。 Vue 組件間通信只要指以下 3 類通信父子組件通信、隔代組件通信兄弟組件通信,下面我們分別介紹每種通信方式且會(huì)說明此種方法可適用于哪類組件間通信

組件傳參的各種方式

組件通信常用方式有以下幾種

  • props / $emit 適用 父子組件通信
    • 父組件向子組件傳遞數(shù)據(jù)是通過 prop 傳遞的,子組件傳遞數(shù)據(jù)給父組件是通過$emit 觸發(fā)事件來做到的
  • ref$parent / $children(vue3廢棄) 適用 父子組件通信
    • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子組件上,引用就指向組件實(shí)例
    • $parent / $children:訪問訪問父組件的屬性或方法 / 訪問子組件的屬性或方法
  • EventBus ($emit / $on) 適用于 父子、隔代、兄弟組件通信
    • 這種方法通過一個(gè)空的 Vue 實(shí)例作為中央事件總線(事件中心),用它來觸發(fā)事件和監(jiān)聽事件,從而實(shí)現(xiàn)任何組件間的通信,包括父子、隔代、兄弟組件
  • $attrs / $listeners(vue3廢棄) 適用于 隔代組件通信
    • $attrs:包含了父作用域中不被 prop 所識(shí)別 (且獲取) 的特性綁定 ( classstyle 除外 )。當(dāng)一個(gè)組件沒有聲明任何 prop時(shí),這里會(huì)包含所有父作用域的綁定 ( classstyle 除外 ),并且可以通過 v-bind="$attrs" 傳入內(nèi)部組件。通常配合 inheritAttrs 選項(xiàng)一起使用
    • $listeners:包含了父作用域中的 (不含 .native 修飾器的) v-on 事件監(jiān)聽器。它可以通過 v-on="$listeners" 傳入內(nèi)部組件
  • provide / inject 適用于 隔代組件通信
    • 祖先組件中通過 provider 來提供變量,然后在子孫組件中通過 inject 來注入變量。 provide / inject API 主要解決了跨級(jí)組件間的通信問題, 不過它的使用場(chǎng)景,主要是子組件獲取上級(jí)組件的狀態(tài) ,跨級(jí)組件間建立了一種主動(dòng)提供與依賴注入的關(guān)系
  • $root 適用于 隔代組件通信 訪問根組件中的屬性或方法,是根組件,不是父組件。$root只對(duì)根組件有用
  • Vuex 適用于 父子、隔代、兄弟組件通信
    • Vuex 是一個(gè)專為 Vue.js 應(yīng)用程序開發(fā)的狀態(tài)管理模式。每一個(gè) Vuex 應(yīng)用的核心就是 store(倉(cāng)庫(kù))?!皊tore” 基本上就是一個(gè)容器,它包含著你的應(yīng)用中大部分的狀態(tài) ( state )
    • Vuex 的狀態(tài)存儲(chǔ)是響應(yīng)式的。當(dāng) Vue 組件從 store 中讀取狀態(tài)的時(shí)候,若 store 中的狀態(tài)發(fā)生變化,那么相應(yīng)的組件也會(huì)相應(yīng)地得到高效更新。
    • 改變 store 中的狀態(tài)的唯一途徑就是顯式地提交 (commit) mutation。這樣使得我們可以方便地跟蹤每一個(gè)狀態(tài)的變化。

根據(jù)組件之間關(guān)系討論組件通信最為清晰有效

  • 父子組件:props/$emit/$parent/ref
  • 兄弟組件:$parent/eventbus/vuex
  • 跨層級(jí)關(guān)系:eventbus/vuex/provide+inject/$attrs + $listeners/$root

下面演示組件之間通訊三種情況: 父?jìng)髯?、子傳父、兄弟組件之間的通訊

1. 父子組件通信

使用props,父組件可以使用props向子組件傳遞數(shù)據(jù)。

父組件vue模板father.vue:

<template><child :msg="message"></child>
</template><script>
import child from './child.vue';
export default {components: {child},data () {return {message: 'father message';}}
}
</script>

子組件vue模板child.vue:

<template><div>{{msg}}</div>
</template><script>
export default {props: {msg: {type: String,required: true}}
}
</script>

回調(diào)函數(shù)(callBack)

父?jìng)髯?#xff1a;將父組件里定義的method作為props傳入子組件

// 父組件Parent.vue:
<Child :changeMsgFn="changeMessage">
methods: {changeMessage(){this.message = 'test'}
}
// 子組件Child.vue:
<button @click="changeMsgFn">
props:['changeMsgFn']

子組件向父組件通信

父組件向子組件傳遞事件方法,子組件通過$emit觸發(fā)事件,回調(diào)給父組件

父組件vue模板father.vue:

<template><child @msgFunc="func"></child>
</template><script>
import child from './child.vue';
export default {components: {child},methods: {func (msg) {console.log(msg);}}
}
</script>

子組件vue模板child.vue:

<template><button @click="handleClick">點(diǎn)我</button>
</template><script>
export default {props: {msg: {type: String,required: true}},methods () {handleClick () {//........this.$emit('msgFunc');}}
}
</script>

2. provide / inject 跨級(jí)訪問祖先組件的數(shù)據(jù)

父組件通過使用provide(){return{}}提供需要傳遞的數(shù)據(jù)

export default {data() {return {title: '我是父組件',name: 'poetry'}},methods: {say() {alert(1)}},// provide屬性 能夠?yàn)楹竺娴暮蟠M件/嵌套的組件提供所需要的變量和方法provide() {return {message: '我是祖先組件提供的數(shù)據(jù)',name: this.name, // 傳遞屬性say: this.say}}
}

子組件通過使用inject:[“參數(shù)1”,”參數(shù)2”,…]接收父組件傳遞的參數(shù)

<template><p>曾孫組件</p><p>{{message}}</p>
</template>
<script>
export default {// inject 注入/接收祖先組件傳遞的所需要的數(shù)據(jù)即可 //接收到的數(shù)據(jù) 變量 跟data里面的變量一樣 可以直接綁定到頁面 {{}}inject: [ "message","say"],mounted() {this.say();},
};
</script>

3. $parent + $children 獲取父組件實(shí)例和子組件實(shí)例的集合

  • this.$parent 可以直接訪問該組件的父實(shí)例或組件
  • 父組件也可以通過 this.$children 訪問它所有的子組件;需要注意 $children 并不保證順序,也不是響應(yīng)式的
<!-- parent.vue -->
<template>
<div><child1></child1>   <child2></child2> <button @click="clickChild">$children方式獲取子組件值</button>
</div>
</template>
<script>
import child1 from './child1'
import child2 from './child2'
export default {data(){return {total: 108}},components: {child1,child2  },methods: {funa(e){console.log("index",e)},clickChild(){console.log(this.$children[0].msg);console.log(this.$children[1].msg);}}
}
</script>
<!-- child1.vue -->
<template><div><button @click="parentClick">點(diǎn)擊訪問父組件</button></div>
</template>
<script>
export default {data(){return {msg:"child1"}},methods: {// 訪問父組件數(shù)據(jù)parentClick(){this.$parent.funa("xx")console.log(this.$parent.total);}}
}
</script>
<!-- child2.vue -->
<template><div>child2</div>
</template>
<script>
export default {data(){return {msg: 'child2'}}
}
</script>

4. $attrs + $listeners多級(jí)組件通信

$attrs 包含了從父組件傳過來的所有props屬性

// 父組件Parent.vue:
<Child :name="name" :age="age"/>// 子組件Child.vue:
<GrandChild v-bind="$attrs" />// 孫子組件GrandChild
<p>姓名:{{$attrs.name}}</p>
<p>年齡:{{$attrs.age}}</p>

$listeners包含了父組件監(jiān)聽的所有事件

// 父組件Parent.vue:
<Child :name="name" :age="age" @changeNameFn="changeName"/>// 子組件Child.vue:
<button @click="$listeners.changeNameFn"></button>

5. ref 父子組件通信

// 父組件Parent.vue:
<Child ref="childComp"/>
<button @click="changeName"></button>
changeName(){console.log(this.$refs.childComp.age);this.$refs.childComp.changeAge()
}// 子組件Child.vue:
data(){return{age:20}
},
methods(){changeAge(){this.age=15}
}

6. 非父子, 兄弟組件之間通信

vue2中廢棄了broadcast廣播和分發(fā)事件的方法。父子組件中可以用props$emit()。如何實(shí)現(xiàn)非父子組件間的通信,可以通過實(shí)例一個(gè)vue實(shí)例Bus作為媒介,要相互通信的兄弟組件之中,都引入Bus,然后通過分別調(diào)用Bus事件觸發(fā)和監(jiān)聽來實(shí)現(xiàn)通信和參數(shù)傳遞。Bus.js可以是這樣:

// Bus.js// 創(chuàng)建一個(gè)中央時(shí)間總線類  
class Bus {  constructor() {  this.callbacks = {};   // 存放事件的名字  }  $on(name, fn) {  this.callbacks[name] = this.callbacks[name] || [];  this.callbacks[name].push(fn);  }  $emit(name, args) {  if (this.callbacks[name]) {  this.callbacks[name].forEach((cb) => cb(args));  }  }  
}  // main.js  
Vue.prototype.$bus = new Bus() // 將$bus掛載到vue實(shí)例的原型上  
// 另一種方式  
Vue.prototype.$bus = new Vue() // Vue已經(jīng)實(shí)現(xiàn)了Bus的功能  
<template><button @click="toBus">子組件傳給兄弟組件</button>
</template><script>
export default{methods: {toBus () {this.$bus.$emit('foo', '來自兄弟組件')}}
}
</script>

另一個(gè)組件也在鉤子函數(shù)中監(jiān)聽on事件

export default {data() {return {message: ''}},mounted() {this.$bus.$on('foo', (msg) => {this.message = msg})}
}

7. $root 訪問根組件中的屬性或方法

  • 作用:訪問根組件中的屬性或方法
  • 注意:是根組件,不是父組件。$root只對(duì)根組件有用
var vm = new Vue({el: "#app",data() {return {rootInfo:"我是根元素的屬性"}},methods: {alerts() {alert(111)}},components: {com1: {data() {return {info: "組件1"}},template: "<p>{{ info }} <com2></com2></p>",components: {com2: {template: "<p>我是組件1的子組件</p>",created() {this.$root.alerts()// 根組件方法console.log(this.$root.rootInfo)// 我是根元素的屬性}}}}}
});

8. vuex

  • 適用場(chǎng)景: 復(fù)雜關(guān)系的組件數(shù)據(jù)傳遞
  • Vuex作用相當(dāng)于一個(gè)用來存儲(chǔ)共享變量的容器

  • state用來存放共享變量的地方
  • getter,可以增加一個(gè)getter派生狀態(tài),(相當(dāng)于store中的計(jì)算屬性),用來獲得共享變量的值
  • mutations用來存放修改state的方法。
  • actions也是用來存放修改state的方法,不過action是在mutations的基礎(chǔ)上進(jìn)行。常用來做一些異步操作

小結(jié)

  • 父子關(guān)系的組件數(shù)據(jù)傳遞選擇 props$emit進(jìn)行傳遞,也可選擇ref
  • 兄弟關(guān)系的組件數(shù)據(jù)傳遞可選擇$bus,其次可以選擇$parent進(jìn)行傳遞
  • 祖先與后代組件數(shù)據(jù)傳遞可選擇attrslisteners或者 ProvideInject
  • 復(fù)雜關(guān)系的組件數(shù)據(jù)傳遞可以通過vuex存放共享的變量

vue-loader是什么?它有什么作用?

回答范例

  1. vue-loader是用于處理單文件組件(SFCSingle-File Component)的webpack loader
  2. 因?yàn)橛辛?code>vue-loader,我們就可以在項(xiàng)目中編寫SFC格式的Vue組件,我們可以把代碼分割為<template>、<script><style>,代碼會(huì)異常清晰。結(jié)合其他loader我們還可以用Pug編寫<template>,用SASS編寫<style>,用TS編寫<script>。我們的<style>還可以單獨(dú)作用當(dāng)前組件
  3. webpack打包時(shí),會(huì)以loader的方式調(diào)用vue-loader
  4. vue-loader被執(zhí)行時(shí),它會(huì)對(duì)SFC中的每個(gè)語言塊用單獨(dú)的loader鏈處理。最后將這些單獨(dú)的塊裝配成最終的組件模塊

原理

vue-loader會(huì)調(diào)用@vue/compiler-sfc模塊解析SFC源碼為一個(gè)描述符(Descriptor),然后為每個(gè)語言塊生成import代碼,返回的代碼類似下面

// source.vue被vue-loader處理之后返回的代碼
?
// import the <template> block
import render from 'source.vue?vue&type=template'
// import the <script> block
import script from 'source.vue?vue&type=script'
export * from 'source.vue?vue&type=script'
// import <style> blocks
import 'source.vue?vue&type=style&index=1'
?
script.render = render
export default script

我們想要script塊中的內(nèi)容被作為js處理(當(dāng)然如果是<script lang="ts">被作為ts理),這樣我們想要webpack把配置中跟.js匹配的規(guī)則都應(yīng)用到形如source.vue?vue&type=script的這個(gè)請(qǐng)求上。例如我們對(duì)所有*.js配置了babel-loader,這個(gè)規(guī)則將被克隆并應(yīng)用到所在Vue SFC

import script from 'source.vue?vue&type=script

將被展開為:

import script from 'babel-loader!vue-loader!source.vue?vue&type=script'

類似的,如果我們對(duì).sass文件配置了style-loader + css-loader + sass-loader,對(duì)下面的代碼

<style scoped lang="scss">

vue-loader將會(huì)返回給我們下面結(jié)果:

import 'source.vue?vue&type=style&index=1&scoped&lang=scss'

然后webpack會(huì)展開如下:

import 'style-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
  • 當(dāng)處理展開請(qǐng)求時(shí),vue-loader將被再次調(diào)用。這次,loader將會(huì)關(guān)注那些有查詢串的請(qǐng)求,且僅針對(duì)特定塊,它會(huì)選中特定塊內(nèi)部的內(nèi)容并傳遞給后面匹配的loader
  • 對(duì)于<script>塊,處理到這就可以了,但是<template><style>還有一些額外任務(wù)要做,比如
    • 需要用 Vue 模板編譯器編譯template,從而得到render函數(shù)
    • 需要對(duì) <style scoped>中的CSS做后處理(post-process),該操作在css-loader之后但在style-loader之前

實(shí)現(xiàn)上這些附加的loader需要被注入到已經(jīng)展開的loader鏈上,最終的請(qǐng)求會(huì)像下面這樣:

// <template lang="pug">
import 'vue-loader/template-loader!pug-loader!source.vue?vue&type=template'
?
// <style scoped lang="scss">
import 'style-loader!vue-loader/style-post-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'

diff算法

時(shí)間復(fù)雜度: 個(gè)樹的完全 diff 算法是一個(gè)時(shí)間復(fù)雜度為 O(n*3) ,vue進(jìn)行優(yōu)化轉(zhuǎn)化成 O(n) 。

理解:

  • 最小量更新, key 很重要。這個(gè)可以是這個(gè)節(jié)點(diǎn)的唯一標(biāo)識(shí),告訴 diff 算法,在更改前后它們是同一個(gè)DOM節(jié)點(diǎn)
    • 擴(kuò)展 v-for 為什么要有 key ,沒有 key 會(huì)暴力復(fù)用,舉例子的話隨便說一個(gè)比如移動(dòng)節(jié)點(diǎn)或者增加節(jié)點(diǎn)(修改DOM),加 key 只會(huì)移動(dòng)減少操作DOM。
  • 只有是同一個(gè)虛擬節(jié)點(diǎn)才會(huì)進(jìn)行精細(xì)化比較,否則就是暴力刪除舊的,插入新的。
  • 只進(jìn)行同層比較,不會(huì)進(jìn)行跨層比較。

diff算法的優(yōu)化策略:四種命中查找,四個(gè)指針

  1. 舊前與新前(先比開頭,后插入和刪除節(jié)點(diǎn)的這種情況)
  2. 舊后與新后(比結(jié)尾,前插入或刪除的情況)
  3. 舊前與新后(頭與尾比,此種發(fā)生了,涉及移動(dòng)節(jié)點(diǎn),那么新前指向的節(jié)點(diǎn),移動(dòng)到舊后之后)
  4. 舊后與新前(尾與頭比,此種發(fā)生了,涉及移動(dòng)節(jié)點(diǎn),那么新前指向的節(jié)點(diǎn),移動(dòng)到舊前之前)

vue3中 watch、watchEffect區(qū)別

  • watch是惰性執(zhí)行,也就是只有監(jiān)聽的值發(fā)生變化的時(shí)候才會(huì)執(zhí)行,但是watchEffect不同,每次代碼加載watchEffect都會(huì)執(zhí)行(忽略watch第三個(gè)參數(shù)的配置,如果修改配置項(xiàng)也可以實(shí)現(xiàn)立即執(zhí)行)
  • watch需要傳遞監(jiān)聽的對(duì)象,watchEffect不需要
  • watch只能監(jiān)聽響應(yīng)式數(shù)據(jù):ref定義的屬性和reactive定義的對(duì)象,如果直接監(jiān)聽reactive定義對(duì)象中的屬性是不允許的(會(huì)報(bào)警告),除非使用函數(shù)轉(zhuǎn)換一下。其實(shí)就是官網(wǎng)上說的監(jiān)聽一個(gè)getter
  • watchEffect如果監(jiān)聽reactive定義的對(duì)象是不起作用的,只能監(jiān)聽對(duì)象中的屬性

看一下watchEffect的代碼

<template>
<div>請(qǐng)輸入firstName:<input type="text" v-model="firstName">
</div>
<div>請(qǐng)輸入lastName:<input type="text" v-model="lastName">
</div>
<div>請(qǐng)輸入obj.text:<input type="text" v-model="obj.text">
</div><div>【obj.text】 {{obj.text}}</div>
</template><script>
import {ref, reactive, watch, watchEffect} from 'vue'
export default {name: "HelloWorld",props: {msg: String,},setup(props,content){let firstName = ref('')let lastName = ref('')let obj= reactive({text:'hello'})watchEffect(()=>{console.log('觸發(fā)了watchEffect');console.log(`組合后的名稱為:${firstName.value}${lastName.value}`)})return{obj,firstName,lastName}}
};
</script>

改造一下代碼

watchEffect(()=>{console.log('觸發(fā)了watchEffect');// 這里我們不使用firstName.value/lastName.value ,相當(dāng)于是監(jiān)控整個(gè)ref,對(duì)應(yīng)第四點(diǎn)上面的結(jié)論console.log(`組合后的名稱為:${firstName}${lastName}`)
})

watchEffect(()=>{console.log('觸發(fā)了watchEffect');console.log(obj);
})

稍微改造一下

let obj = reactive({text:'hello'
})
watchEffect(()=>{console.log('觸發(fā)了watchEffect');console.log(obj.text);
})

再看一下watch的代碼,驗(yàn)證一下

let obj= reactive({text:'hello'
})
// watch是惰性執(zhí)行, 默認(rèn)初始化之后不會(huì)執(zhí)行,只有值有變化才會(huì)觸發(fā),可通過配置參數(shù)實(shí)現(xiàn)默認(rèn)執(zhí)行
watch(obj, (newValue, oldValue) => {// 回調(diào)函數(shù)console.log('觸發(fā)監(jiān)控更新了new',  newValue);console.log('觸發(fā)監(jiān)控更新了old',  oldValue);
},{// 配置immediate參數(shù),立即執(zhí)行,以及深層次監(jiān)聽immediate: true,deep: true
})

  • 監(jiān)控整個(gè)reactive對(duì)象,從上面的圖可以看到 deep 實(shí)際默認(rèn)是開啟的,就算我們?cè)O(shè)置為false也還是無效。而且舊值獲取不到。
  • 要獲取舊值則需要監(jiān)控對(duì)象的屬性,也就是監(jiān)聽一個(gè)getter,看下圖

總結(jié)

  • 如果定義了reactive的數(shù)據(jù),想去使用watch監(jiān)聽數(shù)據(jù)改變,則無法正確獲取舊值,并且deep屬性配置無效,自動(dòng)強(qiáng)制開啟了深層次監(jiān)聽。
  • 如果使用 ref 初始化一個(gè)對(duì)象或者數(shù)組類型的數(shù)據(jù),會(huì)被自動(dòng)轉(zhuǎn)成reactive的實(shí)現(xiàn)方式,生成proxy代理對(duì)象。也會(huì)變得無法正確取舊值。
  • 用任何方式生成的數(shù)據(jù),如果接收的變量是一個(gè)proxy代理對(duì)象,就都會(huì)導(dǎo)致watch這個(gè)對(duì)象時(shí),watch回調(diào)里無法正確獲取舊值。
  • 所以當(dāng)大家使用watch監(jiān)聽對(duì)象時(shí),如果在不需要使用舊值的情況,可以正常監(jiān)聽對(duì)象沒關(guān)系;但是如果當(dāng)監(jiān)聽改變函數(shù)里面需要用到舊值時(shí),只能監(jiān)聽 對(duì)象.xxx`屬性 的方式才行

watch和watchEffect異同總結(jié)

體驗(yàn)

watchEffect立即運(yùn)行一個(gè)函數(shù),然后被動(dòng)地追蹤它的依賴,當(dāng)這些依賴改變時(shí)重新執(zhí)行該函數(shù)

const count = ref(0)
?
watchEffect(() => console.log(count.value))
// -> logs 0
?
count.value++
// -> logs 1

watch偵測(cè)一個(gè)或多個(gè)響應(yīng)式數(shù)據(jù)源并在數(shù)據(jù)源變化時(shí)調(diào)用一個(gè)回調(diào)函數(shù)

const state = reactive({ count: 0 })
watch(() => state.count,(count, prevCount) => {/* ... */}
)

回答范例

  1. watchEffect立即運(yùn)行一個(gè)函數(shù),然后被動(dòng)地追蹤它的依賴,當(dāng)這些依賴改變時(shí)重新執(zhí)行該函數(shù)。watch偵測(cè)一個(gè)或多個(gè)響應(yīng)式數(shù)據(jù)源并在數(shù)據(jù)源變化時(shí)調(diào)用一個(gè)回調(diào)函數(shù)
  2. watchEffect(effect)是一種特殊watch,傳入的函數(shù)既是依賴收集的數(shù)據(jù)源,也是回調(diào)函數(shù)。如果我們不關(guān)心響應(yīng)式數(shù)據(jù)變化前后的值,只是想拿這些數(shù)據(jù)做些事情,那么watchEffect就是我們需要的。watch更底層,可以接收多種數(shù)據(jù)源,包括用于依賴收集的getter函數(shù),因此它完全可以實(shí)現(xiàn)watchEffect的功能,同時(shí)由于可以指定getter函數(shù),依賴可以控制的更精確,還能獲取數(shù)據(jù)變化前后的值,因此如果需要這些時(shí)我們會(huì)使用watch
  3. watchEffect在使用時(shí),傳入的函數(shù)會(huì)立刻執(zhí)行一次。watch默認(rèn)情況下并不會(huì)執(zhí)行回調(diào)函數(shù),除非我們手動(dòng)設(shè)置immediate選項(xiàng)
  4. 從實(shí)現(xiàn)上來說,watchEffect(fn)相當(dāng)于watch(fn,fn,{immediate:true})

watchEffect定義如下

export function watchEffect(effect: WatchEffect,options?: WatchOptionsBase
): WatchStopHandle {return doWatch(effect, null, options)
}

watch定義如下

export function watch<T = any, Immediate extends Readonly<boolean> = false>(source: T | WatchSource<T>,cb: any,options?: WatchOptions<Immediate>
): WatchStopHandle {return doWatch(source as any, cb, options)
}

很明顯watchEffect就是一種特殊的watch實(shí)現(xiàn)。

vue要做權(quán)限管理該怎么做?如果控制到按鈕級(jí)別的權(quán)限怎么做

一、是什么

權(quán)限是對(duì)特定資源的訪問許可,所謂權(quán)限控制,也就是確保用戶只能訪問到被分配的資源

而前端權(quán)限歸根結(jié)底是請(qǐng)求的發(fā)起權(quán),請(qǐng)求的發(fā)起可能有下面兩種形式觸發(fā)

  • 頁面加載觸發(fā)
  • 頁面上的按鈕點(diǎn)擊觸發(fā)

總的來說,所有的請(qǐng)求發(fā)起都觸發(fā)自前端路由或視圖

所以我們可以從這兩方面入手,對(duì)觸發(fā)權(quán)限的源頭進(jìn)行控制,最終要實(shí)現(xiàn)的目標(biāo)是:

  • 路由方面,用戶登錄后只能看到自己有權(quán)訪問的導(dǎo)航菜單,也只能訪問自己有權(quán)訪問的路由地址,否則將跳轉(zhuǎn) 4xx 提示頁
  • 視圖方面,用戶只能看到自己有權(quán)瀏覽的內(nèi)容和有權(quán)操作的控件
  • 最后再加上請(qǐng)求控制作為最后一道防線,路由可能配置失誤,按鈕可能忘了加權(quán)限,這種時(shí)候請(qǐng)求控制可以用來兜底,越權(quán)請(qǐng)求將在前端被攔截

二、如何做

前端權(quán)限控制可以分為四個(gè)方面:

  • 接口權(quán)限
  • 按鈕權(quán)限
  • 菜單權(quán)限
  • 路由權(quán)限

接口權(quán)限

接口權(quán)限目前一般采用jwt的形式來驗(yàn)證,沒有通過的話一般返回401,跳轉(zhuǎn)到登錄頁面重新進(jìn)行登錄

登錄完拿到token,將token存起來,通過axios請(qǐng)求攔截器進(jìn)行攔截,每次請(qǐng)求的時(shí)候頭部攜帶token

axios.interceptors.request.use(config => {config.headers['token'] = cookie.get('token')return config
})
axios.interceptors.response.use(res=>{},{response}=>{if (response.data.code === 40099 || response.data.code === 40098) { //token過期或者錯(cuò)誤router.push('/login')}
})

路由權(quán)限控制

方案一

初始化即掛載全部路由,并且在路由上標(biāo)記相應(yīng)的權(quán)限信息,每次路由跳轉(zhuǎn)前做校驗(yàn)

const routerMap = [{path: '/permission',component: Layout,redirect: '/permission/index',alwaysShow: true, // will always show the root menumeta: {title: 'permission',icon: 'lock',roles: ['admin', 'editor'] // you can set roles in root nav},children: [{path: 'page',component: () => import('@/views/permission/page'),name: 'pagePermission',meta: {title: 'pagePermission',roles: ['admin'] // or you can only set roles in sub nav}}, {path: 'directive',component: () => import('@/views/permission/directive'),name: 'directivePermission',meta: {title: 'directivePermission'// if do not set roles, means: this page does not require permission}}]}]

這種方式存在以下四種缺點(diǎn):

  • 加載所有的路由,如果路由很多,而用戶并不是所有的路由都有權(quán)限訪問,對(duì)性能會(huì)有影響。
  • 全局路由守衛(wèi)里,每次路由跳轉(zhuǎn)都要做權(quán)限判斷。
  • 菜單信息寫死在前端,要改個(gè)顯示文字或權(quán)限信息,需要重新編譯
  • 菜單跟路由耦合在一起,定義路由的時(shí)候還有添加菜單顯示標(biāo)題,圖標(biāo)之類的信息,而且路由不一定作為菜單顯示,還要多加字段進(jìn)行標(biāo)識(shí)

方案二

初始化的時(shí)候先掛載不需要權(quán)限控制的路由,比如登錄頁,404等錯(cuò)誤頁。如果用戶通過URL進(jìn)行強(qiáng)制訪問,則會(huì)直接進(jìn)入404,相當(dāng)于從源頭上做了控制

登錄后,獲取用戶的權(quán)限信息,然后篩選有權(quán)限訪問的路由,在全局路由守衛(wèi)里進(jìn)行調(diào)用addRoutes添加路由

import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css'// progress bar style
import { getToken } from '@/utils/auth' // getToken from cookieNProgress.configure({ showSpinner: false })// NProgress Configuration// permission judge function
function hasPermission(roles, permissionRoles) {if (roles.indexOf('admin') >= 0) return true // admin permission passed directlyif (!permissionRoles) return truereturn roles.some(role => permissionRoles.indexOf(role) >= 0)
}const whiteList = ['/login', '/authredirect']// no redirect whitelistrouter.beforeEach((to, from, next) => {NProgress.start() // start progress barif (getToken()) { // determine if there has token/* has token*/if (to.path === '/login') {next({ path: '/' })NProgress.done() // if current page is dashboard will not trigger    afterEach hook, so manually handle it} else {if (store.getters.roles.length === 0) { // 判斷當(dāng)前用戶是否已拉取完user_info信息store.dispatch('GetUserInfo').then(res => { // 拉取user_infoconst roles = res.data.roles // note: roles must be a array! such as: ['editor','develop']store.dispatch('GenerateRoutes', { roles }).then(() => { // 根據(jù)roles權(quán)限生成可訪問的路由表router.addRoutes(store.getters.addRouters) // 動(dòng)態(tài)添加可訪問路由表next({ ...to, replace: true }) // hack方法 確保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record})}).catch((err) => {store.dispatch('FedLogOut').then(() => {Message.error(err || 'Verification failed, please login again')next({ path: '/' })})})} else {// 沒有動(dòng)態(tài)改變權(quán)限的需求可直接next() 刪除下方權(quán)限判斷 ↓if (hasPermission(store.getters.roles, to.meta.roles)) {next()//} else {next({ path: '/401', replace: true, query: { noGoBack: true }})}// 可刪 ↑}}} else {/* has no token*/if (whiteList.indexOf(to.path) !== -1) { // 在免登錄白名單,直接進(jìn)入next()} else {next('/login') // 否則全部重定向到登錄頁NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it}}
})router.afterEach(() => {NProgress.done() // finish progress bar
})

按需掛載,路由就需要知道用戶的路由權(quán)限,也就是在用戶登錄進(jìn)來的時(shí)候就要知道當(dāng)前用戶擁有哪些路由權(quán)限

這種方式也存在了以下的缺點(diǎn):

  • 全局路由守衛(wèi)里,每次路由跳轉(zhuǎn)都要做判斷
  • 菜單信息寫死在前端,要改個(gè)顯示文字或權(quán)限信息,需要重新編譯
  • 菜單跟路由耦合在一起,定義路由的時(shí)候還有添加菜單顯示標(biāo)題,圖標(biāo)之類的信息,而且路由不一定作為菜單顯示,還要多加字段進(jìn)行標(biāo)識(shí)

菜單權(quán)限

菜單權(quán)限可以理解成將頁面與理由進(jìn)行解耦

方案一

菜單與路由分離,菜單由后端返回

前端定義路由信息

{name: "login",path: "/login",component: () => import("@/pages/Login.vue")
}

name字段都不為空,需要根據(jù)此字段與后端返回菜單做關(guān)聯(lián),后端返回的菜單信息中必須要有name對(duì)應(yīng)的字段,并且做唯一性校驗(yàn)

全局路由守衛(wèi)里做判斷

function hasPermission(router, accessMenu) {if (whiteList.indexOf(router.path) !== -1) {return true;}let menu = Util.getMenuByName(router.name, accessMenu);if (menu.name) {return true;}return false;}Router.beforeEach(async (to, from, next) => {if (getToken()) {let userInfo = store.state.user.userInfo;if (!userInfo.name) {try {await store.dispatch("GetUserInfo")await store.dispatch('updateAccessMenu')if (to.path === '/login') {next({ name: 'home_index' })} else {//Util.toDefaultPage([...routers], to.name, router, next);next({ ...to, replace: true })//菜單權(quán)限更新完成,重新進(jìn)一次當(dāng)前路由}}  catch (e) {if (whiteList.indexOf(to.path) !== -1) { // 在免登錄白名單,直接進(jìn)入next()} else {next('/login')}}} else {if (to.path === '/login') {next({ name: 'home_index' })} else {if (hasPermission(to, store.getters.accessMenu)) {Util.toDefaultPage(store.getters.accessMenu,to, routes, next);} else {next({ path: '/403',replace:true })}}}} else {if (whiteList.indexOf(to.path) !== -1) { // 在免登錄白名單,直接進(jìn)入next()} else {next('/login')}}let menu = Util.getMenuByName(to.name, store.getters.accessMenu);Util.title(menu.title);
});Router.afterEach((to) => {window.scrollTo(0, 0);
});

每次路由跳轉(zhuǎn)的時(shí)候都要判斷權(quán)限,這里的判斷也很簡(jiǎn)單,因?yàn)椴藛蔚?code>name與路由的name是一一對(duì)應(yīng)的,而后端返回的菜單就已經(jīng)是經(jīng)過權(quán)限過濾的

如果根據(jù)路由name找不到對(duì)應(yīng)的菜單,就表示用戶有沒權(quán)限訪問

如果路由很多,可以在應(yīng)用初始化的時(shí)候,只掛載不需要權(quán)限控制的路由。取得后端返回的菜單后,根據(jù)菜單與路由的對(duì)應(yīng)關(guān)系,篩選出可訪問的路由,通過addRoutes動(dòng)態(tài)掛載

這種方式的缺點(diǎn):

  • 菜單需要與路由做一一對(duì)應(yīng),前端添加了新功能,需要通過菜單管理功能添加新的菜單,如果菜單配置的不對(duì)會(huì)導(dǎo)致應(yīng)用不能正常使用
  • 全局路由守衛(wèi)里,每次路由跳轉(zhuǎn)都要做判斷

方案二

菜單和路由都由后端返回

前端統(tǒng)一定義路由組件

const Home = () => import("../pages/Home.vue");
const UserInfo = () => import("../pages/UserInfo.vue");
export default {home: Home,userInfo: UserInfo
};

后端路由組件返回以下格式

[{name: "home",path: "/",component: "home"},{name: "home",path: "/userinfo",component: "userInfo"}
]

在將后端返回路由通過addRoutes動(dòng)態(tài)掛載之間,需要將數(shù)據(jù)處理一下,將component字段換為真正的組件

如果有嵌套路由,后端功能設(shè)計(jì)的時(shí)候,要注意添加相應(yīng)的字段,前端拿到數(shù)據(jù)也要做相應(yīng)的處理

這種方法也會(huì)存在缺點(diǎn):

  • 全局路由守衛(wèi)里,每次路由跳轉(zhuǎn)都要做判斷
  • 前后端的配合要求更高

按鈕權(quán)限

方案一

按鈕權(quán)限也可以用v-if判斷

但是如果頁面過多,每個(gè)頁面頁面都要獲取用戶權(quán)限role和路由表里的meta.btnPermissions,然后再做判斷

這種方式就不展開舉例了

方案二

通過自定義指令進(jìn)行按鈕權(quán)限的判斷

首先配置路由

{path: '/permission',component: Layout,name: '權(quán)限測(cè)試',meta: {btnPermissions: ['admin', 'supper', 'normal']},//頁面需要的權(quán)限children: [{path: 'supper',component: _import('system/supper'),name: '權(quán)限測(cè)試頁',meta: {btnPermissions: ['admin', 'supper']} //頁面需要的權(quán)限},{path: 'normal',component: _import('system/normal'),name: '權(quán)限測(cè)試頁',meta: {btnPermissions: ['admin']} //頁面需要的權(quán)限}]
}

自定義權(quán)限鑒定指令

import Vue from 'vue'
/**權(quán)限指令**/
const has = Vue.directive('has', {bind: function (el, binding, vnode) {// 獲取頁面按鈕權(quán)限let btnPermissionsArr = [];if(binding.value){// 如果指令傳值,獲取指令參數(shù),根據(jù)指令參數(shù)和當(dāng)前登錄人按鈕權(quán)限做比較。btnPermissionsArr = Array.of(binding.value);}else{// 否則獲取路由中的參數(shù),根據(jù)路由的btnPermissionsArr和當(dāng)前登錄人按鈕權(quán)限做比較。btnPermissionsArr = vnode.context.$route.meta.btnPermissions;}if (!Vue.prototype.$_has(btnPermissionsArr)) {el.parentNode.removeChild(el);}}
});
// 權(quán)限檢查方法
Vue.prototype.$_has = function (value) {let isExist = false;// 獲取用戶按鈕權(quán)限let btnPermissionsStr = sessionStorage.getItem("btnPermissions");if (btnPermissionsStr == undefined || btnPermissionsStr == null) {return false;}if (value.indexOf(btnPermissionsStr) > -1) {isExist = true;}return isExist;
};
export {has}

在使用的按鈕中只需要引用v-has指令

<el-button @click='editClick' type="primary" v-has>編輯</el-button>

小結(jié)

關(guān)于權(quán)限如何選擇哪種合適的方案,可以根據(jù)自己項(xiàng)目的方案項(xiàng)目,如考慮路由與菜單是否分離

權(quán)限需要前后端結(jié)合,前端盡可能的去控制,更多的需要后臺(tái)判斷

SPA首屏加載速度慢的怎么解決

一、什么是首屏加載

首屏?xí)r間(First Contentful Paint),指的是瀏覽器從響應(yīng)用戶輸入網(wǎng)址地址,到首屏內(nèi)容渲染完成的時(shí)間,此時(shí)整個(gè)網(wǎng)頁不一定要全部渲染完成,但需要展示當(dāng)前視窗需要的內(nèi)容

首屏加載可以說是用戶體驗(yàn)中最重要的環(huán)節(jié)

關(guān)于計(jì)算首屏?xí)r間

利用performance.timing提供的數(shù)據(jù):

通過DOMContentLoad或者performance來計(jì)算出首屏?xí)r間

// 方案一:
document.addEventListener('DOMContentLoaded', (event) => {console.log('first contentful painting');
});
// 方案二:
performance.getEntriesByName("first-contentful-paint")[0].startTime// performance.getEntriesByName("first-contentful-paint")[0]
// 會(huì)返回一個(gè) PerformancePaintTiming的實(shí)例,結(jié)構(gòu)如下:
{name: "first-contentful-paint",entryType: "paint",startTime: 507.80000002123415,duration: 0,
};

二、加載慢的原因

在頁面渲染的過程,導(dǎo)致加載速度慢的因素可能如下:

  • 網(wǎng)絡(luò)延時(shí)問題
  • 資源文件體積是否過大
  • 資源是否重復(fù)發(fā)送請(qǐng)求去加載了
  • 加載腳本的時(shí)候,渲染內(nèi)容堵塞了

三、解決方案

常見的幾種SPA首屏優(yōu)化方式

  • 減小入口文件積
  • 靜態(tài)資源本地緩存
  • UI框架按需加載
  • 圖片資源的壓縮
  • 組件重復(fù)打包
  • 開啟GZip壓縮
  • 使用SSR

1. 減小入口文件體積

常用的手段是路由懶加載,把不同路由對(duì)應(yīng)的組件分割成不同的代碼塊,待路由被請(qǐng)求的時(shí)候會(huì)單獨(dú)打包路由,使得入口文件變小,加載速度大大增加

vue-router配置路由的時(shí)候,采用動(dòng)態(tài)加載路由的形式

routes:[ path: 'Blogs',name: 'ShowBlogs',component: () => import('./components/ShowBlogs.vue')
]

以函數(shù)的形式加載路由,這樣就可以把各自的路由文件分別打包,只有在解析給定的路由時(shí),才會(huì)加載路由組件

2. 靜態(tài)資源本地緩存

后端返回資源問題:

  • 采用HTTP緩存,設(shè)置Cache-ControlLast-ModifiedEtag等響應(yīng)頭
  • 采用Service Worker離線緩存

前端合理利用localStorage

3. UI框架按需加載

在日常使用UI框架,例如element-UI、或者antd,我們經(jīng)常性直接引用整個(gè)UI庫(kù)

import ElementUI from 'element-ui'
Vue.use(ElementUI)

但實(shí)際上我用到的組件只有按鈕,分頁,表格,輸入與警告 所以我們要按需引用

import { Button, Input, Pagination, Table, TableColumn, MessageBox } from 'element-ui';
Vue.use(Button)
Vue.use(Input)
Vue.use(Pagination)

4. 組件重復(fù)打包

假設(shè)A.js文件是一個(gè)常用的庫(kù),現(xiàn)在有多個(gè)路由使用了A.js文件,這就造成了重復(fù)下載

解決方案:在webpackconfig文件中,修改CommonsChunkPlugin的配置

minChunks: 3

minChunks為3表示會(huì)把使用3次及以上的包抽離出來,放進(jìn)公共依賴文件,避免了重復(fù)加載組件

5. 圖片資源的壓縮

圖片資源雖然不在編碼過程中,但它卻是對(duì)頁面性能影響最大的因素

對(duì)于所有的圖片資源,我們可以進(jìn)行適當(dāng)?shù)膲嚎s

對(duì)頁面上使用到的icon,可以使用在線字體圖標(biāo),或者雪碧圖,將眾多小圖標(biāo)合并到同一張圖上,用以減輕http請(qǐng)求壓力。

6. 開啟GZip壓縮

拆完包之后,我們?cè)儆?code>gzip做一下壓縮 安裝compression-webpack-plugin

cnmp i compression-webpack-plugin -D

vue.congig.js中引入并修改webpack配置

const CompressionPlugin = require('compression-webpack-plugin')configureWebpack: (config) => {if (process.env.NODE_ENV === 'production') {// 為生產(chǎn)環(huán)境修改配置...config.mode = 'production'return {plugins: [new CompressionPlugin({test: /\.js$|\.html$|\.css/, //匹配文件名threshold: 10240, //對(duì)超過10k的數(shù)據(jù)進(jìn)行壓縮deleteOriginalAssets: false //是否刪除原文件})]}}

在服務(wù)器我們也要做相應(yīng)的配置 如果發(fā)送請(qǐng)求的瀏覽器支持gzip,就發(fā)送給它gzip格式的文件 我的服務(wù)器是用express框架搭建的 只要安裝一下compression就能使用

const compression = require('compression')
app.use(compression())  // 在其他中間件使用之前調(diào)用

7. 使用SSR

SSR(Server side ),也就是服務(wù)端渲染,組件或頁面通過服務(wù)器生成html字符串,再發(fā)送到瀏覽器

從頭搭建一個(gè)服務(wù)端渲染是很復(fù)雜的,vue應(yīng)用建議使用Nuxt.js實(shí)現(xiàn)服務(wù)端渲染

四、小結(jié)

減少首屏渲染時(shí)間的方法有很多,總的來講可以分成兩大部分 :資源加載優(yōu)化頁面渲染優(yōu)化

下圖是更為全面的首屏優(yōu)化的方案

大家可以根據(jù)自己項(xiàng)目的情況選擇各種方式進(jìn)行首屏渲染的優(yōu)化

http://www.risenshineclean.com/news/55098.html

相關(guān)文章:

  • 網(wǎng)頁工具東莞優(yōu)化排名公司
  • 農(nóng)業(yè)網(wǎng)站建設(shè)方案 ppt搜索推廣平臺(tái)
  • 有什么網(wǎng)站是做平面設(shè)計(jì)的百度搜索的優(yōu)勢(shì)
  • 真人性做爰video網(wǎng)站公司網(wǎng)站建站要多少錢
  • 深圳做積分商城網(wǎng)站建設(shè)怎么制作鏈接網(wǎng)頁
  • 深圳分銷網(wǎng)站設(shè)計(jì)電話企業(yè)網(wǎng)站建設(shè)
  • 公司建一個(gè)網(wǎng)站嗎搜什么關(guān)鍵詞能搜到好片
  • 廣州知名網(wǎng)站建設(shè)哪家公司好seo文案范例
  • 產(chǎn)品網(wǎng)站建設(shè)軟文推廣營(yíng)銷平臺(tái)
  • 家里電腦做網(wǎng)站服務(wù)器seo推廣怎么樣
  • 網(wǎng)站開發(fā)什么意思徐州seo公司
  • 河南新鄉(xiāng)市建設(shè)銀行網(wǎng)站系統(tǒng)優(yōu)化軟件排行榜
  • 競(jìng)價(jià)網(wǎng)站做seo教育培訓(xùn)機(jī)構(gòu)官網(wǎng)
  • 武漢網(wǎng)絡(luò)推廣有哪些公司鄂爾多斯seo
  • 湛江找人做網(wǎng)站排名球隊(duì)排名榜實(shí)時(shí)排名
  • 網(wǎng)站建設(shè)中 html營(yíng)銷課程
  • 做網(wǎng)站如何設(shè)計(jì)數(shù)據(jù)庫(kù)個(gè)人網(wǎng)頁設(shè)計(jì)作品模板
  • wordpress .htaccess 規(guī)則seo代碼優(yōu)化工具
  • 門戶網(wǎng)站功能清單萬網(wǎng)域名查詢注冊(cè)商
  • 做的很好的黑白網(wǎng)站如何在百度上做產(chǎn)品推廣
  • 西寧網(wǎng)站設(shè)計(jì)公司seo外鏈平臺(tái)
  • 網(wǎng)站 國(guó)外空間不需要icp許可證嗎推廣引流吸引人的文案
  • 成都網(wǎng)站推廣排名怎么把自己的網(wǎng)站發(fā)布到網(wǎng)上
  • 官方網(wǎng)站建設(shè)報(bào)價(jià)怎么讓百度快速收錄網(wǎng)站
  • 古冶區(qū)城鄉(xiāng)建設(shè)局網(wǎng)站seo技術(shù)培訓(xùn)江門
  • 旅游網(wǎng)站首頁圖片最近的新聞大事10條
  • 加關(guān)鍵詞的網(wǎng)站黃頁污水
  • 全國(guó)做網(wǎng)站公司排名營(yíng)銷手段和技巧
  • 免費(fèi)表格制作app關(guān)鍵詞優(yōu)化哪家好
  • 做外鏈權(quán)重高的女性網(wǎng)站霸屏seo服務(wù)