和尤雨溪一起進階vue(二)

夏天來嘍發表於2019-03-28

這個系列文章將從下面幾個方面來介紹vue

  1. reactivity(響應式)
  2. plugin(外掛)
  3. render(渲染函式)
  4. routing(路由)
  5. state-management(狀態管理)
  6. international(多語言支援) 上一篇文章已經介紹了第一部分響應式reactivity, 連結地址 這篇準備介紹第外掛,渲染函式以及路由, 這一部分程式碼很多,看不懂自己多動手敲敲

plugin

開發vue的外掛很簡單,官網上有非常詳細的說明,對Vue外掛不瞭解的人建議移步Vue官網教程外掛

練個手吧, 寫個簡單的表單驗證外掛

<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.js"></script>
<div id="app">
    <form @submit="validate">
        <input v-model="text">
        <br>
        <input v-model="email">

        <ul v-if="!$v.valid" style="color:red">
            <li v-for="error in $v.errors">
                {{ error }}
            </li>
        </ul>
        <input type="submit" :disabled="!$v.valid">
    </form>
</div>
<script>
const emailRE = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
const validationPlugin = {
    install(Vue) {
        // 全域性注入的方法
        Vue.mixin({
            computed: {
                $v() {
                    const rules = this.$options.validations
                    let valid = true
                    let errors = [] 
                    Object.keys(rules || {}).forEach(key => {
                        const rule = rules[key]
                        const value = this[key]
                        const result = rule.validate(value)
                        if(!result) {
                            valid = false
                            errors.push(
                            rule.message(key, value)
                            )
                        }
                    })
                    return {
                        valid,
                        errors
                    }
                }
            }
        })
    }
}
Vue.use(validationPlugin)
new Vue({
  el: '#app',
  data: {
    text: 'foo',
    email: ''
  },
  validations: {
    text: {
      validate: value => value.length >= 5,
      message: (key, value) => `${key} should have a min length of 5, but got ${value.length}`
    },
    email: {
      validate: value => emailRE.test(value),
      message: key => `${key} must be a valid email`
    }
  },
  methods: {
    validate (e) {
      if (!this.$v.valid) {
        e.preventDefault()
        alert('not valid!')
      }
    }
  }
})
</script>
複製程式碼

render

我們來看看vue的渲染函式render

平時開發寫vue檔案都是用模板template的方法寫html,模板會被編譯成render函式,流程如下:

初始化的時候

  • 模板會被編譯成render函式
  • render函式返回虛擬DOM(virtual Dom)
  • 生成真正的DOM

資料更新的時候

  • render函式返回新的virtual Dom
  • 新的virtual Dom和舊的virtual Dom做diff
  • 將差異運用到真實DOM

如果我們直接用render函式的方式而不是template,就可以免去模板編譯成render函式的步驟,效能會更高

virtaul DOM

真正的DOM

  • 建立方式: document.createElement('div')
  • 內容: [document Element]
  • 真實的DOM是瀏覽器引擎用C++實現的,我們無法操控,只是暴露出上面的JS介面供我們呼叫,操作很昂貴

virtual DOM

  • 建立方式:vm.$createElement('div')
  • 內容: {tag: 'div', data: {attr: {}}, children: []}
  • 虛擬DOM就是js物件
  • 操作cheap

render API

    render(h) {
        h(tag, data, children)
    }
複製程式碼

template, jsx, render本質都是一樣的, 都是一種dom和資料狀態之間關係的表示

用法:

    // tag可以是原生的html標籤
    render(h) {
        return h('div', { attrs: {}}, [])
    }
 
    // 也可以是一個vue component
    import vueComponent from '...'
    render(h) {
        h(vueComponent, {
            props: {} 
        })
    }
複製程式碼

練手時間

<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.js"></script>
<div id="app">
    <example :tags="['h1','h2', 'h3']"></example>
</div>
<script>
    Vue.component('example', {
        props: ['tags'],
        render(h) {
            return h('div', {attrs: {
                class: 'hello'
            }},
            this.tags.map((tag, i) => h(tag, i))
            )
        }
    })
    new Vue({el: '#app'})
</script>

複製程式碼

函式元件

函式元件,其實是一個接收引數的函式,沒有自己的狀態,沒有生命週期,不建立元件例項,也不可以在render裡面呼叫this 練手時間

<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.js"></script>
<div id="app">
    <function-component :tags="['h1','h2','h3']"></function-component>
</div>
<script>
    // 函式元件的渲染函式還會接收一個額外的 context 引數,為沒有例項的函式元件提供上下文資訊。
    // 這裡我們用物件解構context
    const FunctionComponent = {
        functional: true, // 標記元件為 functional
        render (h, {props:{tags}}){
            return h('div', {attrs: {
                class:'function'
            }},
            tags.map((tag, i) => h(tag, i))
            )
        }
    }
    Vue.component('function-component', FunctionComponent)
    new Vue({el: '#app'})
</script>

複製程式碼

高階元件

A higher-order component is a function that takes a component and returns a new component.

React的高階元件很火,其實Vue也可以寫高階元件,我們可以寫一個來看看

現在有一個元件Avatar

  const Avatar = {
        props: ['src'],
        template: `<img :src="src" />`
    }
複製程式碼

Avatar這個元件的功能就是接受src顯示圖片,它是一個非常簡單的元件,沒有其他多餘的功能,很多地方都會使用這個元件,但是獲取src的邏輯可能不一樣,在父元件裡面使用這個元件的時候,如果在父元件裡面調取介面獲取圖片地址傳遞給它,那麼就會汙染了父元件,因為這不是父元件的邏輯

這個時候我們就可以建立一個高階元件,根據不同邏輯進行封裝,假設現在是根據username獲取圖片地址,我們來封裝一個smart-avatar,我們在使用的這個元件的時候不需要傳入完整圖片路徑,只需要傳入username就可以。

一句話,高階元件其實就是對簡單的元件的和封裝, 從而可以和父元件解耦

<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.js"></script>
<div id="app">
    <smart-avatar username="vuejs" id="hello">
        <div slot="foo">
            這是一個具名插槽
        </div>
    </smart-avatar>
</div>
<script>
    // mock API
    function fetchURL(username, cb) {
        setTimeout(() => {
            cb('https://avatars3.githubusercontent.com/u/6128107?v=4&s=200')
        }, 500)
    }
    const Avatar = {
        props: ['src'],
        template: `<img :src="src" />`
    }
    // 高階元件withAvatarUrl
    function withAvatarUrl(innerComponent, fetchURL) {
        return {
            props: ['username'],
            data() {
                return {
                    url: `http://via.placeholder.com/200*200`
                }
            },
            created() {
                fetchURL(this.username, url => {
                    this.url = url;
                })
            },
            render(h) {
                // console.log(this.$slots.default);
                // console.log(this.$slots.foo);
                return h(innerComponent, {
                    props: {
                        src: this.url,
                        attrs: this.$attrs
                    }
                }, this.$slots.default)
            }
        }
    }
    const SmartAvatar = withAvatarUrl(Avatar, fetchURL);
    new Vue({
        el: '#app',
        components: {SmartAvatar}
    })
</script>
複製程式碼

路由

用過vue開發專案的應該都知道vue-router, url變化的時候會匹配對應的元件,其實原理就是hash事件的監控, 我們來看看不借助這個外掛如何實現路由的功能

1 初級版

藉助vue的動態元件,可以實現一個簡單的路由功能,如下

<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.js"></script>
<div id="app">
    <a href="#foo">foo</a>
    <a href="#bar">bar</a>
    <component :is="url"></component>
</div>
<script>
    // 這是一個比較簡單的解決方案,但是有一個問題,初始化的時候無法匹配
    window.addEventListener('hashchange', () => {
        app.url = window.location.hash.slice(1)
    });
    let app = new Vue({
        el: '#app',
        data() {
            return {
                url: null
            }
        },
        components: {
            foo: {template: `<div>foo-component</div>`},
            bar: {template: `<div>bar-component</div>`}
        }
    })
</script>
複製程式碼

2 改進版

解耦微改進一下,將路由提取到一個路由表中,

<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.js"></script>
<div id="app">
</div>
<script>
const Foo = {template: '<div>foo</div>'}
const Bar = {template: '<div>bar</div>'}
const NotFound = {template: '<div>not found</div>'}

// 在物件裡面統一配置路由
const routeTable = {
    'foo': Foo,
    'bar': Bar

}
window.addEventListener('hashchange', () => {
    app.url = window.location.hash.slice(1)
})
let app = new Vue({
        el:'#app',
        data() {
            return {
                url: window.location.hash.slice(1)
            }
        },
        render(h) {
            return h('div', [
                h('a', {attrs: {href: '#foo'}}, 'foo'),
                '|',
                h('a', {attrs: {href: '#bar'}}, 'bar'),
                h(routeTable[this.url] || NotFound),
            ])
        }
})
</script>
複製程式碼

3 最終版

上面都是處理簡單的url, 實際開發的時候,配置的路由都是多頁面,多元件,路由的path也會比較長,如/a/b, /a/b/c, 還有動態路由,比如/a/:id,這個時候上面的方法就不能準確匹配了, 如果你是正則達人,你可以自己試試解析這些複雜的path,不是的話就使用別人封裝好的第三方庫吧,這裡推薦path-to-regexp, 這個庫的作用作者一句話就概述完了:

Turn a path string such as/user/:nameinto a regular expression

大家可以點進去連結瞭解一下用法,這裡我們介紹接下來要用的部分

const keys = []
const regexp = pathToRegexp('/foo/:id', keys)
// regexp = /^\/foo\/((?:[^\/]+?))(?:\/(?=$))?$/i
// keys = [{ delimiter: "/", name: "id", optional: false, partial: false, pattern: "[^\/]+?", prefix: "/", repeat: false}]
// 得到了正規表示式regexp, 傳入實際的url執行正則的exec方法
// 不匹配
const match1 = regexp.exec('/test/route'); // null
const match3 = regexp.exec('/foo');        // null
// 匹配
const match2 = regexp.exec('/foo/fooId'); // ["/foo/fooId", "fooId", index: 0, input: "/foo/fooId", groups: undefined]
複製程式碼

ok, 我們可以解析path了,來看看接下來如何實現

<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.js"></script>
// 這裡是下載到本地同目錄引入的
<script src='./path-to-regexp.js'></script>
<div id="app"></div>
<script>
    const Foo = {
        props: ['id'],
        template: `<div>foo with id: {{id}} </div>`
    }
    const Bar = {
        template: `<div>bar</div>`
    }
    const NotFound = {
        template: `<div>not found</div>`
    }
    const routeTable = {
        '/foo/:id': Foo,
        '/bar': Bar,
    }
    // 處理路由
    const compiledRoutes = [];
    Object.keys(routeTable).forEach(path => {
        const dynamicSegments = []
        const regex = pathToRegexp(path, dynamicSegments)
        const component = routeTable[path]
        compiledRoutes.push({
            component,
            regex,
            dynamicSegments
        })
    })
    window.addEventListener('hashchange', () => {
        app.url = window.location.hash.slice(1);
    })
    const app = new Vue({
        el: '#app',
        data() {
            return {
                url: window.location.hash.slice(1)
            }
        },
        // 渲染那個路由,路由屬性
        render(h) {
            const url = '/' + this.url
            let componentToRender
            let props = {}
            compiledRoutes.some(route => {
                const match = route.regex.exec(url)
                if (match) {
                    componentToRender = route.component
                    // 上一步已經可以匹配到url對應的元件了
                    // 這裡多做一步,獲取動態id作為props的屬性傳入元件
                    route.dynamicSegments.forEach((segment,index) => {
                        props[segment.name] = match[index+1]
                    })
                }
            })
             return h('div', [
                h('a', { attrs: { href: '#foo/123' } }, 'foo123'),
                '|',
                h('a', { attrs: { href: '#foo/234' } }, 'foo234'),
                '|',
                h('a', { attrs: { href: '#bar' } }, 'bar'),
                h(componentToRender || NotFound, { props })
            ])
        }
    })
</script>
複製程式碼

第二篇完結撒花!!!!, 第三篇月底不知道有沒有時間更,沒時間就清明節更了。

相關文章