Vue2.0 豆瓣电影项目实例

作者: 陈枫C | 来源:发表于2017-10-17 18:26 被阅读512次

    vue-movie

    vue2.0 豆瓣电影项目实例,包含三个功能模块(首页列表、搜索列表、详情页) ,适合刚入门学习

    所需组件

    我们会用到vue的核心组件:vue-router、axios、vue-cli

    1. 使用vue-cli脚手架创建项目(使用webpack模板)
    2. 使用vue-router实现单页面应用
    3. 使用axios请求豆瓣API的数据
    4. 使用bootstrap样式库

    安装

    PS:请先安装node和npm,这个就不做多介绍。嫌npm下载太慢,可以安装一个cnpm淘宝npm镜像,安装方式:npm install -g cnpm --registry=https://registry.npm.taobao.org

    第一步安装vue-cli

    cnpm i -g vue-cli
    

    然后使用vue-cli脚手架创建一个webpack项目,根据提示一步步来,在是否安装vue-router的时候选择y,省的后面再安装

    vue init webpack vue-movie
    
    image

    安装成功后根据提示进入项目中

    cd vue-movie
    

    安装依赖模块

    cnpm i
    

    启动服务, 它会去package.json中找到script对象下的start,进行热加载部署

    cnpm run dev
    

    我们可以在浏览器中输入:localhost:8080进入下面界面就代表我们第一步环境搭建已经完成。给大家提供一个网址,里面详细介绍了vue-cli的webpack模板项目配置文件分析:http://blog.csdn.net/hongchh/article/details/55113751

    image

    效果图

    先感受这三个页面的效果图,不好也不允许吐槽,O(∩_∩)O哈哈~


    image
    image
    image

    正式开工

    首先,我们把剩下需要的组件下下来:

    cnpm i bootstrap axios -S
    

    项目结构

    .
    ├── build // 构建后路径
    │   ├── build.js
    │   ├── check-versions.js
    │   ├── dev-client.js
    │   ├── dev-server.js
    │   ├── utils.js
    │   ├── vue-loader.conf.js
    │   ├── webpack.base.conf.js
    │   ├── webpack.dev.conf.js
    │   └── webpack.prod.conf.js
    ├── config  // 配置文件
    │   ├── dev.env.js
    │   ├── index.js
    │   └── prod.env.js
    ├── src
    │   ├── assets
    │   ├── components
    │   │   ├── MovieList.vue // 电影列表组件
    │   │   └── Header.vue  // 头部导航栏组件
    │   ├── router // 路由
    │   │   ├── index.js
    │   └── views // 视图
    │       ├── Index.vue // 首页-显示正在上映电影
    │       └── Search.vue // 搜索页
    │       └── Detail.vue // 详情页
    │   ├── App.vue
    │   ├── main.js // 入口文件
    ├── static
    ├── README.md
    ├── index.html
    ├── package.json
    

    电影项目展示主要包含三个模块:首页、搜索页、详情页,根据这三个页面我们在views下新建三个.vue文件。其中头部是固定的,首页和首页页都要展示影片列表,所以我们抽出来2个共用组件MovieList.vue和Header.vue。

    初始化 main.js

    import Vue from 'vue'
    import axios from 'axios'
    import App from './App'
    import router from './router'
    import 'bootstrap/dist/css/bootstrap.css
    
    // 请求绑定Vue属性上面,方便使用
    Vue.prototype.$http = axios;
    
    new Vue({
      el: '#app',
      router,
      template: '<App/>',
      components: { App }
    })
    

    引入组件、路由、样式库、数据请求,并初始化Vue实例,这里我们引入了一个入口App组件以及一个路由配置文件,接下来我们就先开始实行他们

    创建App组件

    // src/App.vue
    <template>
      <div id="app">
        <v-header></v-header>
        <div class="container">
          <keep-alive>
            <router-view></router-view>
          </keep-alive>
        </div>
      </div>
    </template>
    
    <script>
      import vHeader from './components/Header.vue'
      export default{
        components: {
          vHeader
        }
      }
    </script>
    

    这里使用了keep-alive来缓存页面的组件,减少性能消耗。 还引入了头部组件,下面就开始写头部代码。

    头部组件

    // src/components/Header.vue
    <template>
      <nav class="navbar navbar-default">
        <div class="container-fluid">
          <div class="navbar-brand">
            <router-link to="/">豆瓣电影</router-link>
          </div>
          <form class="navbar-form navbar-left" role="search" @submit.prevent="submit">
            <div class="form-group">
              <input type="text" class="form-control" placeholder="输入电影名称" v-model="searchKey">
            </div>
            <button type="submit" class="btn btn-default">搜索</button>
          </form>
        </div>
      </nav>
    </template>
    
    <script>
      export default{
        data(){
          return {
            searchKey: ''
          }
        },
        methods: {
          submit(){
            if (!this.searchKey) {
              alert('请输入搜索内容');
              return;
            }
            // 搜索页面跳转
            this.$router.push({
              path: '/search/' + this.searchKey,
            })
            this.searchKey = "";
          }
        }
      }
    </script>
    

    这里包含了搜索功能,输入关键字进行搜索, 下面开始配置各个页面之间的路由。

    路由配置

    // src/router/index.js
    import Vue from 'vue'
    import Router from 'vue-router'
    import Index from '@/views/Index'
    import Search from '@/views/Search'
    import Detail from '@/views/Detail'
    
    Vue.use(Router)
    
    export default new Router({
      routes: [
        {
          path: '/',
          component: Index
        },
        {
          path: '/search/:searchKey',
          component: Search
        },
        {
          path:'/detail/:id',
          component: Detail
        }
      ]
    })
    

    豆瓣API

    该应用使用了下面3个api:

    • /v2/movie/search?q=关键字 电影搜索api
    • /v2/movie/in_theaters 正在上映的电影
    • /v2/movie/subject/:id 单个电影条目信息

    PS:更多关于豆瓣的api可以前往豆瓣api官网查看。

    电影列表

    首页展示的是正在热映的电影(Index.vue),搜索页展示的是搜索后的结果集(Search.vue),他们都是展示电影列表,所以我们共用了一个MovieList.vue组件,先上代码:

    // src/views/Index.vue
    <template>
      <div>
        <movie-list :movie-type="movieType"></movie-list>
      </div>
    </template>
    
    <script>
      import movieList from '@/components/MovieList.vue'
      export default {
        data(){
          return {
            movieType: 'in_theaters' // 正在热映类型
          }
        },
        components: {
          'movie-list': movieList
        }
      }
    </script>
    
    // src/views/Search.vue
    <template>
      <div>
        <movie-list :movie-type="movieType" ref="updateList"></movie-list>
      </div>
    </template>
    
    <script>
      import movieList from '@/components/MovieList.vue'
      export default{
        data(){
          return {
            movieType: 'search' // 搜索类型
          }
        },
        watch: {
          // 监听路由,搜索页重复搜索的时候改变路由状态,页面重新加载,不监听的话组件实例会被复用
          '$route'(to, from){
            // 防止返回重复调用
            if (to.path.indexOf('/search/') == 0) {
              // 调用子组件方法
              this.$refs.updateList.loadMovieList();
            }
          }
        },
        components: {
          'movie-list': movieList
        }
      }
    </script>
    
    // src/components/MovieList.vue
    <template>
      <div class="container">
        <div class="canvas" v-show="loading">
          <div class="spinner"></div>
        </div>
        <h2>{{title}}</h2>
        <div class="row">
          <div class="col-md-2 text-center" v-for="item in list" :key="item.id">
            <router-link :to="{path:'/detail/'+item.id}">
              ![](item.images.large)
              <div class="title">{{item.title}}</div>
            </router-link>
          </div>
        </div>
      </div>
    </template>
    
    <script>
      export default {
        data () {
          return {
            loading: true,
            title: '',
            list: []
          }
        },
        props: ['movieType'],// 接收父组件传过来的值 --in_theaters=正在上映的电影  --search==搜索电影
        mounted(){
          this.loadMovieList();
        },
        methods: {
          loadMovieList(){
            this.loading = true;
            // 请求参数
            let params = {
                count: 18
              },
              // 请求路径
              movieUrl = '/api/movie/' + this.movieType;
            // 如果是搜索进入,新增搜索关键字参数
            if (this.movieType == 'search') {
              params['q'] = this.$route.params.searchKey;
            }
            this.$http.post(movieUrl, params).then((res) => {
              console.log(res.data)
              // 这里不做多校验,可自己加,直接上数据
              this.list = res.data.subjects;
              this.title = res.data.title;
              this.loading = false;
            })
          }
        }
      }
    </script>
    
    <style scoped>
      img {
        width: 100%;
        height: 230px;
        vertical-align: middle;
      }
      .row > div {
        margin-bottom: 20px;
      }
      .title {
        height: 20px;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
      }
    </style>
    
    

    组件实例的作用域是孤立的。这意味着不能 (也不应该) 在子组件的模板内直接引用父组件的数据。父组件的数据需要通过 prop 才能下发到子组件中。 所以我们在Index.vue 和 Search.vue中的movie-list我们定义了一个Prop属性movie-type, 下发给子组件MovieList.vue,告知当前类型是什么,并根据类型请求相应类型的数据。

    // 父级
    <movie-list :movie-type="movieType"></movie-list>
    
    // 子级
    export default {
      ...
      props: ['movieType'],// 接收父组件传过来的值 --in_theaters=正在上映的电影  --search==搜索电影
      ...
    }
    

    在重复搜索的时候,我们会发现页面根本不会重新请求数据,并渲染页面。这是因为:当使用路由参数时,例如从 /search/a 导航到 search/b,原来的组件实例会被复用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用。复用组件时,想对路由参数的变化作出响应的话,可以简单地 watch(监测变化) $route 对象 ,所以在Search.vue中加入如下代码

    watch: {
      // 监听路由,搜索页重复搜索的时候改变路由状态,页面重新加载,不监听的话组件实例会被复用
      '$route'(to, from){
        // 防止返回重复调用
        if (to.path.indexOf('/search/') == 0) {
          // 调用子组件方法
          this.$refs.updateList.loadMovieList();
        }
      }
    }
    

    当我们高高兴兴的进入页面后发现页面没反应, 打开控制台一看,waht,跨域了,我勒个去。好在vue-cli中webpack模板提供了代理的配置,根据官网文档的给出的方法进行配置。

    // config/index.js
    module.exports = {
      ...
      dev:{
        proxyTable: {
          '/api': {
            target: 'http://api.douban.com/v2',// 有使用次数限制
            changeOrigin: true,
            pathRewrite: {
              '^/api': ''
            }
          }
        }
      }
      ...
    }
    

    这样我们就可以使用/api/movie/in_theaters代替http://api.douban.com/v2/movie/in_theaters进行访问了, 跨域问题也就解决。

    详情

    // src/views/Details.vue
    <template>
      <div class="container">
        <div class="canvas" v-show="loading">
          <div class="spinner"></div>
        </div>
        <h2>{{movie.title}}({{movie.year}})</h2>
        <div class="box">
          <div class="left">
            ![](movie.images.large)
          </div>
          <div class="main">
            <div>导演:{{getDirectors}}</div>
            <div>主演:{{getCasts}}</div>
            <div>评分:{{movie.rating.average}}</div>
            <div>
              <h3>{{movie.title}}剧情介绍</h3>
              {{movie.summary}}
            </div>
          </div>
        </div>
      </div>
    </template>
    

    这里唯一要注意的是movie.xx.xx多级情况下,如果不在data里面定义好,在初始化的时候会报错,请求数据填充后又成功了。

    // src/views/Details.vue
    <script>
      export default {
        data () {
          return {
            loading: true,
            movie: {
              images: {
                large: ''
              },
              rating: {
                average: 0
              }
            }
          }
        },
        /*mounted(){
          // 获取电影详情
          this.getMovieDetail();
        },
        watch: {
          // 监听路由,搜索页重复搜索的时候改变路由状态,页面重新加载,不监听的话组件实例会被复用
          '$route'(to, from){
            // 防止返回重复调用
            if (to.path.indexOf('/detail/') == 0) {
              this.getMovieDetail();
            }
          }
        },*/
        activated(){
          // 在App.vue中使用了构建组件keep-alive,所以可用钩子函数activated属性来代替watch中的$route监听
          // 获取电影详情
          this.getMovieDetail();
        },
        methods: {
          // 获取电影详情
          getMovieDetail(){
            let detailUrl = '/api/movie/subject/' + this.$route.params.id;
            this.$http.get(detailUrl).then((res) => {
              console.log(res.data);
              this.movie = res.data;
    
              this.loading = false;
            })
          },
          // 过滤数据
          getFilterData(obj){
            let arr = [];
            if (!obj || obj.length == 0)return '';
    
            for (let i = 0; i < obj.length; i++) {
              arr.push(obj[i].name)
            }
            return arr.join('/');
          },
        },
        computed: {
          // 获取导演
          getDirectors(){
            return this.getFilterData(this.movie.directors);
          },
          // 获取主演
          getCasts(){
            return this.getFilterData(this.movie.casts);
          },
        }
      }
    </script>
    

    这个也会碰到跟重复搜索一样的问题,进入详情后返回在进入另一个详情页面不变,原理一样。但是还有另一种实现方式,由于我们在App.vue中使用了构建组件keep-alive,所以可用钩子函数activated属性来代替watch中的$route监听。可多了解下vue的生命周期

    // src/views/Details.vue
    <style scoped>
      .box {
        display: flex;
        flex-flow: row;
        justify-content: space-between;
      }
    
      .left {
        width: 200px;
        height: 300px;
      }
    
      .left img {
        width: 100%;
        height: 100%;
      }
    
      .main {
        margin-left: 20px;
        flex: 1;
      }
    </style>
    

    总结

    至此,我们一个简单的豆瓣电影实例已经完成,踩了很多坑,但是也学到了很多东西,此处省略N个字的心得...

    源码地址:Vue2.0 豆瓣电影项目实例 ,如果觉得有帮助,可star一下啊

    进阶篇

    好了,Vue会用了,不会Vuex怎么行呢。下一目标Vuex 豆瓣电影项目实战

    相关文章

      网友评论

      • 8e2fe413a741:请问图片出不来是咋回事?
        陈枫C:这应该是权限问题导致的, 可以试下用Node.js 转发API请求

      本文标题:Vue2.0 豆瓣电影项目实例

      本文链接:https://www.haomeiwen.com/subject/uetjuxtx.html