Vuejs组件入门

Published on 2016 - 06 - 16

What are Components?

Components are one of the most powerful features of Vue.js. They help you extend basic HTML elements to encapsulate reusable code. At a high level, Components are custom elements that Vue.js’ compiler would attach specified behavior to. In some cases, they may also appear as a native HTML element extended with the special is attribute.

It is a really clever and powerful way to extend HTML to do new things. we are going to start out with an extremely simple example and next we are going to see how Components can help us improve the code we have created.

Using Components

We are going to start with a simple Component. In order to use a component we have to register it first.

One way to register a component is to use the Vue.component method and pass in the tag and the constructor. Think of the tag as the name of the Component and the constructor as the options. In our occasion, we’ll name the Component story and we’ll define the property story (again). The option template (how we would like our story to be displayed), is inside the constructor where other options will be added as well.

Our story component will be registered like this

Vue.component('story', {
    template: '<h1>My horse is amazing!</h1>' 
});

Now that we have registered the component we will make use of it. We will add the custom element inside the HTML to display the story.

<html> 
<head>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.mi\ n.css" rel="stylesheet">
    <title>Hello Vue</title> 
</head>
<body>
    <div class="container">
        <story></story> 
    </div>
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.18/vue.js"></script> <script type="text/javascript">
Vue.component('story', {
    template: '<h1>My horse is amazing!</h1>'
});
new Vue({
    el: '.container'
}) 
</script> 
</html>

components are reusable which means you can append as many elements as you want. The following HTML snippet will display our story 3 times.

<body>
    <div class="container">
        <story></story> 
        <story></story> 
        <story></story>
    </div> 
</body>

Templates

There is more than one way of using a template for our component. The inline template we’ve used before can get “dirty” very fast.

Another way to declare a template is to create a script tag with type set to text/template and set an id of story-template. To use this template we need to reference a selector in the template option of our component to this script.

<script type="text/template" id="story-template">
    <h1>My horse is amazing!</h1>
</script>
<script type="text/javascript">
    Vue.component('story', {
        template: "#story-template"
    }); 
</script>

My favorite way to define a template is to create a template HTML tag and give it an id. Then we can reference a selector as we did before. Using this technique the above component will look like this:

<template id="story-template"> 
    <h1>My horse is amazing!</h1>
</template>
<script type="text/javascript">
    Vue.component('story', {
        template: "#story-template"
    }); 
</script>

Properties

Lets see now how we can use multiple instances of our story component to display a list of stories. We have to update the template to not display always the same story, but the plot of any story we want.

<template id="story-template"> 
<h1>{{ plot }}</h1>
</template>

We also have to update our component to use this property. To do so we will add the new property, ‘plot’, to props attribute of the component.

Vue.component('story', {
    props: ['plot'],
    template: "#story-template"
});

Now we can pass a plot and a plain string to it, every time we use the element.

<body>
    <div class="container">
    <story plot="My horse is amazing."></story>
    <story plot="Narwhals invented Shish Kebab."></story>
    <story plot="The dark side of the Force is stronger."></story>
    </div> 
</body>

As you may have imagined, a component can have more than one property. For example, if we want to display the writer along with the plot for every story, we have to pass the writer too.

<story plot="My horse is amazing." writer="Mr. Weebl"></story>

If you have a lot of properties and your elements are becoming dirty you can pass an object and display its properties.

We will refactor our example one more time to wrap it up.

<html> 
<head>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.mi\ n.css" rel="stylesheet">
    <title>Awesome Stories</title> 
</head>
<body>
<div class="container">
    <story v-bind:story="{plot: 'My horse is amazing.', writer: 'Mr. Weebl'}">
    </story>
    <story v-bind:story="{plot: 'Narwhals invented Shish Kebab.', writer: 'M\ r. Weebl'}">
    </story>
    <story v-bind:story="{plot: 'The dark side of the Force is stronger.', writer: 'Darth Vader'}">
    </story>
    <template id="story-template">
        <h1>{{ story.writer }} said "{{ story.plot }}"</h1> 
    </template>
</div> 
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.18/vue.js"></script> <script type="text/javascript">
Vue.component('story', {
    props: ['story'],
    template: "#story-template"
});
new Vue({
    el: '.container'
}) 
</script> 
</html>

v-bind is used to dynamically bind one or more attributes, or a component prop to an expression.
Since story property is not a string but a javascript object instead of story="..." we use v-bind:story="..." to bind story property with the passed object.
The shorthand for v-bind is :, so from now on we are gonna use it like this: :story="...".

Reusability

Let’s take a look again at our Filtered Results example. Assume this time we take the stories variable data from an external API through an http call. The API developers decide to rename plot story property to body. So now, we have to go through our code and make the necessary changes.

<div class="container">
    <h1>Lets hear some stories!</h1>
    <div>
        <h3>Alex's stories</h3>
        <ul class="list-group">
            <li v-for="story in stories | filterBy 'Alex' in 'writer'" class="list-group-item">
                {{ story.writer }} said "{{ story.plot }}"(should change to following)
                {{ story.writer }} said  "{{ story.body }}"
            </li>
        </ul>
        <h3>John's stories</h3>
        <ul class="list-group">
            <li v-for="story in stories | filterBy 'John' in 'writer'" class="list-group-item">
                {{ story.writer }} said "{{ story.plot }}"(should change to following)
                {{ story.writer }} said  "{{ story.body }}"
            </li>
        </ul>                
        <div class="form-group">
            <label for="query">
                What are you looking for?
            </label>
            <input v-model="query" class="form-control">
        </div>
        <h3>Search results:</h3>
        <ul class="list-group">
            <li v-for="story in stories | filterBy query in 'body'" class="list-group-item">
                {{ story.writer }} said "{{ story.plot }}"(should change to following)
                {{ story.writer }} said "{{ story.body }}"
            </li> 
        </ul>
     </div>
</div>

As you may have noticed, we had to do the exact same change 3 times and I don’t know about you, but I hate repeating myself. If it doesn’t seem like a big deal for you, imagine that you may use the above code block in 100 places, what would you do then? Fortunately, ‘Vue’ has a solution for that kind of situations, and this solution has a name, Component.

Luckily we have created a story Component in the previous example, which displays the writer and the body for a specified story. We can use the custom element inside our HTML and pass each story as we did before with :story tag but this time we will do it inside v-for directive.

So our code will be:

<div class="container">
    <h1>Lets hear some stories!</h1> 
    <div>
        <h3>Alex's stories</h3> 
        <ul class="list-group">
            <story v-for="story in stories | filterBy 'Alex' in 'writer'" :story="story"></story>               
        </ul>
        <h3>John's stories</h3> 
        <ul class="list-group">
            <story v-for="story in stories | filterBy 'John' in 'writer'" :story="story"></story>   
        </ul>
        <div class="form-group">
            <label for="query">What are you looking for?</label> 
            <input v-model="query" class="form-control">
        </div>
        <h3>Search results:</h3> 
        <ul class="list-group">
            <story v-for="story in stories | filterBy query in 'body'" :story="story"></story> 
        </ul>
    </div> 
</div>

If you try to run this code you will get the following warning.

Vue warn: Unknown custom element: - did you register the component correctly? For recursive components, make sure to provide the “name” option.

To fix this we need to register the Component again. This time we have to make some changes to the component’s template. We will change plot attribute to body and <h1> tag to <li> to suit our needs.

So, the story’s template will be:

<template id="story-template"> 
    <li class="list-group-item">
        {{ story.writer }} said "{{ story.plot }}" 
    </li>
</template>

But the component will be the same.

Vue.component('story', {
    props: ['story'],
    template: '#story-template'
});

If you run the above code you will see for yourself that everything works same as before but this time with the use of a custom component.

Altogether now

Using our newly acquired knowledge we should be able to build something a bit more complex. Taking the structure example from before, we are going to create a voting system for our stories, and add a favorite feature. The way to accomplish these is through methods, directives, and of course, components.

Lets start with the stories setup.

<html>
<head>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.cs\ s" rel="stylesheet">
    <title>Hello Vue</title> 
</head>
<body>
<div id="app">
    <div class="container">
        <h1>Let's hear some stories!</h1> 
        <ul class="list-group">
            <story v-for="story in stories" :story="story"></story> 
        </ul>
        <pre>{{ $data | json }}</pre> 
    </div>
</div>
<template id="story-template">
    <li class="list-group-item">
        {{ story.writer }} said "{{ story.plot }}"
    </li> 
</template>
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.18/vue.js"></script> <script type="text/javascript">
Vue.component('story', {
        template: "#story-template",
        props: ['story'],
});
new Vue({
el: '#app',
    data: {
        stories: [
            {
                plot: 'My horse is amazing.',
                writer: 'Mr. Weebl',
            },
             {
                plot: 'Narwhals invented Shish Kebab.',
                writer: 'Mr. Weebl',
            },
            {
                plot: 'The dark side of the Force is stronger.',
                writer: 'Darth Vader',
            },
            {
                plot: 'One does not simply walk into Mordor',
                writer: 'Boromir',
            }, 
        ]
    }
})
</script> 
</html>

The next step is to give the user a way to vote once, the story he desires to. To apply this limit (1 vote per story) we will display the ‘Upvote’ button only if user has not already voted. So, every story must have a voted property that becomes true when upvote function executes.

<template id="story-template"> 
    <li class="list-group-item">
         {{ story.writer }} said "{{ story.plot }}". 
         Story upvotes {{ story.upvotes }}.
        <button v-show="!story.voted" @click="upvote" class="btn btn-default">
            Upvote 
        </button>
    </li> 
</template>

Vue.component('story', {
    template: "#story-template",
    props: ['story'],
    methods:{
        upvote: function(){ 
            this.story.upvotes += 1; 
            this.story.voted = true;
        },
    }
});

new Vue({
    el: '#app',
    data: {
        stories: [
             {
                plot: 'My horse is amazing.',
                writer: 'Mr. Weebl',
                upvotes: 28,
                voted: false,
            },
            {
                plot: 'Narwhals invented Shish Kebab.',
                writer: 'Mr. Weebl',
                upvotes: 8,
                voted: false,
            },
            {
                plot: 'The dark side of the Force is stronger.',
                writer: 'Darth Vader',
                upvotes: 49,
                voted: false,
            },
            {
                plot: 'One does not simply walk into Mordor',
                writer: 'Boromir',
                upvotes: 74,
                voted: false,
            },
        ]
    }
})

We have implemented, with the use of methods, the voting system. I think it looks good, so we can continue with the ‘favorite story’ part. We want the user to be able to choose only one story to be his favorite. The first thing that comes to my mind is to add one new empty object (favorite) and whenever the user chooses one story to be his favorite, update favorite variable. This way we will be able to check if a story is equal to the user’s favorite story. Let’s do this.

<template id="story-template">
    <li class="list-group-item">
        {{ story.writer }} said "{{ story.plot }}".
        Story upvotes {{ story.upvotes }}.
        <button v-show="!story.voted" @click="upvote" class="btn btn-default">
            Upvote
        </button>
        <button v-show="!isFavorite" @click="setFavorite" class="btn btn-primary">
            Favorite
        </button>
        <span v-show="isFavorite" class="glyphicon glyphicon-star pull-right" aria-hidden="true">
        </span>
    </li> 
</template>

Vue.component('story', {
    template: "#story-template",
    props: ['story'],
    methods:{
        upvote: function(){ 
            this.story.upvotes += 1; 
            this.story.voted = true;
        },
        setFavorite: function(){
            this.favorite = this.story; 
        },
    },
    computed:{
        isFavorite: function(){
            return this.story == this.favorite;
        }, 
    }
});

new Vue({
    el: '#app',
    data: {
        stories: [
            ...
        ],
        favorite: {}
    }
})

If you try to run the above code, you will notice that it does not work as it should be. Whenever you try to favorite a story, the variable favorite inside $data remains null and we get none response.

It seems that our story component is unable to update favorite object, so we are going to pass it on each story and add favorite to component’s properties.

<ul class="list-group">
    <story v-for="story in stories" :story="story" :favorite="favorite">
    </story>
</ul>
    Vue.component('story', {
        ...
        props: ['story', 'favorite'],
       ...
});

Hmmm, favorite still doesn’t get updated when setFavorite is executed. The button disappears as expected and a star icon appears but variable favorite is still null. This results in the user being able to favorite all stories.

The problem with this approach is that we don’t keep things synced. By default, all props form a one-way-down binding between the child property and the parent one. When the parent property updates, it will flow down to the child, but not the other way around.

This may be confusing but stick with me. In Vue, you can enforce a two-way binding with .sync binding type modifier. So, we will pass the variable favorite to each story like this :favorite.sync="favorite".

<div id="app">
    <div class="container"> 
        <h1>Let's hear some stories!</h1> 
        <ul class="list-group">
            <story v-for="story in stories" :story="story" :favorite.sync="favorite">
            </story> 
        </ul>
        <pre>
            {{ $data | json }}
        </pre>
    </div> 
</div>

Now, the desired result is achieved and the user is able to choose only one story to be his favorite while he can vote as many stories as he wants. With the use of .sync we have synced the property ‘favorite’ and made the binding two-way with the favorite object.

Before the .sync is a one-way down binding, after is a two-way binding, keeping them synchronized.

Reference