微信小程序电影推荐demo实战开发小结(附源码及思维导图) ... ...

第一步 项目配置 一、编写app.json 对整个项目的公共配置 1、pages:配置页面路径(必须),列出所有的页面的路径,所有存在的页面都需要在此写出,否则在页面跳转的时候会报出找不到页面的错误2、window:窗口配置 ...

本文已获作者sesine授权转载,授权地址:http://www.jianshu.com/p/447b36463f09
更新日志

2016-11-30 更新内容:

  • 1.将网络请求从fetch改为官方的wx.request
  • 2.添加网络请求失败的提示
  • 3.将wxss中引入的网络图片路径改为base64的方式
  • 4.现已支持手机预览 : )
目录

[TOC]

github地址:https://github.com/yesifeng/wechat-weapp-movie

第一步 项目配置
一、编写app.json

对整个项目的公共配置

1、pages:配置页面路径(必须),列出所有的页面的路径,所有存在的页面都需要在此写出,否则在页面跳转的时候会报出找不到页面的错误
2、window:窗口配置,配置导航及窗口的背景色和文字颜色,还有导航文字和是否允许窗口进行下拉刷新
3、tabBar:tab栏配置,配置tab栏背景色及出现位置,上边框的颜色(目前只支持黑或白),文字颜色及文字选中颜色,最核心的配置是list即tab栏的列表,官方规定最少2个,最多5个,每个列表项目可配置页面路径、文字、图标及选中时图标的地址
4、network:网络配置,配置网络请求、上传下载文件、socket连接的超时时间
5、debug:调试模式,建议开发时开启(true),可以看到页面注册、页面跳转及数据初始化的信息,另外报错的错误信息也会比较详细

{
  "pages": [
    "pages/popular/popular",
    "pages/coming/coming",
    "pages/top/top",
    "pages/search/search",
    "pages/filmDetail/filmDetail",
    "pages/personDetail/personDetail",
    "pages/searchResult/searchResult"
  ],
  "window": {
    "navigationBarBackgroundColor": "#47a86c",
    "navigationBarTextStyle": "white",
    "navigationBarTitleText":  "电影推荐",
    "backgroundColor": "#fff",
    "backgroundTextStyle": "dark"
  },
  "tabBar": {
    "color": "#686868",
    "selectedColor": "#47a86c",
    "backgroundColor": "#ffffff",
    "borderStyle": "white",
    "list": [{
      "pagePath": "pages/popular/popular",
      "iconPath": "dist/images/popular_icon.png",
      "selectedIconPath": "dist/images/popular_active_icon.png",
      "text": "热映"
    }, {
      "pagePath": "pages/coming/coming",
      "iconPath": "dist/images/coming_icon.png",
      "selectedIconPath": "dist/images/coming_active_icon.png",
      "text": "待映"
    },{
      "pagePath": "pages/search/search",
      "iconPath": "dist/images/search_icon.png",
      "selectedIconPath": "dist/images/search_active_icon.png",
      "text": "搜索"
    },
    {
      "pagePath": "pages/top/top",
      "iconPath": "dist/images/top_icon.png",
      "selectedIconPath": "dist/images/top_active_icon.png",
      "text": "口碑"
    }]
  },
  "networkTimeout": {
    "request": 10000,
    "downloadFile": 10000
  },
  "debug": true
}
二、确定目录结构

根据UI图,提取组件和公共样式/脚本,以及page的目录

  • comm - 公用的脚本及样式
    • script - 公共脚本
      • config.js 配置信息 (单页数据量,城市等)
      • fetch.js 接口调用 (电影列表及详情,人物详情、搜索)
    • style - 公共样式
      • animation.wxss 动画
  • component - 公用的组件
    • filmList - 电影列表
      • filmList.wxml - 组件结构
      • filmList.wxss - 组件样式
  • dist - 静态资源
    • images 本地图片,主要存导航的图标 (样式中不可引用本地图像资源)
  • pages - 页面
    • popular - 页面文件夹 ("popular"为自定义的页面名称,页面相关文件的文件名需与页面名相同)
      • popular.js 页面逻辑
      • popular.wxml 页面结构
      • popular.wxss 页面样式
      • popular.json 页面窗口配置 (可参考app.json中的window配置)
  • app.js - 小程序整体逻辑 (初始化、显示、隐藏的事件,以及存放全局数据)
  • app.json - 小程序公共配置
  • app.wxss - 小程序公共样式
第二步 编写组件  电影列表

结构

<template name="filmList">
<block wx:if="{{showLoading}}">
    <view class="loading">玩命加载中…view>
block>
<block wx:else>
    <scroll-view scroll-y="true" style="height: {{windowHeight}}rpx" bindscroll="scroll" bindscrolltolower="scrolltolower">
        <view class="film">
            <block wx:for="{{films}}" wx:for-index="filmIndex" wx:for-item="filmItem" wx:key="film">
                <view data-id="{{filmItem.id}}" class="film-item" catchtap="viewFilmDetail">
                    <view class="film-cover">
                        <image src="{{filmItem.images.large}}" class="film-cover-img">image>
                        <view class="film-rating">
                            <block wx:if="{{filmItem.rating.average == 0}}">
                                暂无评分
                            block>
                            <block wx:else>
                                {{filmItem.rating.average}}分
                            block>
                        view>
                    view>
                    <view class="file-intro">
                        <view class="film-title">{{filmItem.title}}view>
                        <view class="film-tag">
                            <view class="film-tag-item" wx:for="{{filmItem.genres}}" wx:for-item="filmTagItem" wx:key="filmTag" data-tag="{{filmTagItem}}" catchtap="viewFilmByTag">
                                {{filmTagItem}}
                            view>
                        view>
                    view>
                view>
            block>
            <block wx:if="{{hasMore}}">
                <view class="loading-tip">拼命加载中…view>
            block>
            <block wx:else>
                <view class="loading-tip">没有更多内容了view>
            block>
        view>
    scroll-view>
block>
template>

样式

import "../../comm/style/animation.wxss";
.film {
    box-sizing: border-box;
    width: 750rpx;
    padding: 10rpx;
    display: flex;
    flex-wrap: wrap;
    flex-direction: row;
    justify-content: space-around;
    box-shadow: 0 0 40rpx #f4f4f4 inset;
}
.film-item {
    width: 350rpx;
    margin-bottom: 20rpx;
    border-radius: 10rpx;
    background-color: #fff;
    border: 1px solid #e4e4e4;
    box-shadow: 0 20rpx 40rpx #eee;
    overflow: hidden;
    animation: fadeIn 1s;
}
.film-cover, .film-cover-img {
    width: 350rpx;
    height: 508rpx;
}
.film-cover {
    position: relative;
    border-radius: 10rpx;
    overflow: hidden;
}
.film-rating {
    box-sizing: border-box;
    position: absolute;
    bottom: 0;
    left: 0;
    width: 100%;
    height: 50rpx;
    padding-right: 20rpx;
    font-size: 12px;
    text-align: right;
    line-height: 50rpx;
    background-color: rgba(0, 0, 0, .65);
    color: #fff;
}
.file-intro {
    padding: 16rpx;
    margin-top: -8rpx;
}
.film-title {
    white-space: nowrap;
    text-overflow: ellipsis;
    overflow: hidden;
}
.film-tag {
    width: 100%;
    margin-top: 10rpx;
    display: flex;
    justify-content: flex-start;
}
.film-tag-item {
    padding: 4rpx 6rpx;
    margin-right: 10rpx;
    font-size: 24rpx;
    box-shadow: 0 0 0 1px #ccc;
    border-top: 1px solid #fff;
    border-radius: 10rpx;
    background-color: #fafafa;
    color: #666;
}
.loading-tip {
    width: 100%;
    height: 80rpx;
    line-height: 80rpx;
    text-align: center;
    color: #ccc;
}

使用方法

以popular(热映)页面为例

在popular.wxml中插入以下代码引入组件结构:

<import src="../../component/filmList/filmList.wxml"/>
<template is="filmList" data="{{films: films, hasMore: hasMore, showLoading: showLoading, start: start, windowHeight: windowHeight}}"/>

在popular.wcss中插入一下代码引入组件样式:

import "../../component/filmList/filmList.wxss";
  • import 引入组件(模板)
  • template 使用组件(模板) data属性可以给模板传入数据
消息提示

结构

<template name="message">
    <view class="message-area" hidden="{{message.visiable ? false : true}}">
        <view class="message">
            <view class="message-icon message-icon-{{message.icon}}">view>
            <view class="message-content">{{message.content}}view>
        view>
    view>
template>

样式

@import "../../component/filmList/filmList.wxss";
.message-area {
    position: fixed;
    width: 100%;
    height: 100%;
    z-index: 99;
}
.message {
    box-sizing: border-box;
    position: fixed;
    z-index: 999;
    left: 50%;
    top: 50%;
    width: 200rpx;
    height: 200rpx;
    padding: 30rpx;
    margin-top: -100rpx;
    margin-left: -100rpx;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    border-radius: 16rpx;
    background-color: rgba(0, 0, 0, .75);
    color: #fff;
    animation: fadeIn .3s;
}
.message-icon {
    height: 100rpx;
    width: 100rpx;
    background-position: center;
    background-repeat: no-repeat;
    background-size: 100rpx;
}
.message-icon-success {
    background-image: url('http://139.196.214.241:8093/cdn/images/weui-success-icon.png');
}
.message-icon-warning {
    background-image: url('http://139.196.214.241:8093/cdn/images/weui-warning-icon.png');
}
.message-icon-info {
    background-image: url('http://139.196.214.241:8093/cdn/images/weui-info-icon.png');
}
.message-content {
    margin-top: 15rpx;
    text-align: center;
}

逻辑

module.exports = {
    show: function(cfg) {
        var that = this
        that.setData({
            message: {
                content: cfg.content,
                icon: cfg.icon,
                visiable: true
            }
        })
        if (typeof cfg.duration !== 'undefined') {
            setTimeout(function(){
                that.setData({
                    message: {
                        visiable: false
                    }
                })
            }, cfg.duration)
        }
    },
    hide: function() {
        var that = this
        that.setData({
            message: {
                visiable: true
            }
        })
    }
}

使用方法

以search(搜索)页面为例

在search.wxml中插入以下代码引入组件结构:

<import src="../../component/message/message.wxml"/>
<template is="message" data="{{message: message}}"/>

在search.wcss中插入一下代码引入组件样式:

@import "../../component/message/message.wxss";

在search.js中插入一下代码引入组件逻辑:

var message = require('../../component/message/message')
message.show.call(that,{
  content: '请输入内容',
  icon: 'info',
  duration: 1500
})
  • import 引入组件(模板)
  • template 使用组件(模板) data属性可以给模板传入数据
  • require 引入文件中 module.exports导出的数据或方法
  • 调用方法:message.show.call(cfg)
第二步 编写公共脚本  请求接口

列表:

  • fetchFilms:获取电影列表(热映、待映、排行、搜索结果页面)
  • fetchFilmDetail:获取电影详情
  • fetchPersonDetail:获取人物详情
  • search:搜索关键词或是类型(返回的是电影列表)
var config = require('./config.js')
module.exports = {
    fetchFilms: function(url, city, start, count, cb) {
    var that = this
      if (that.data.hasMore) {
        fetch(url + '?city=' + config.city + '&start=' + start + '&count=' + count).then(function(response){
          response.json().then(function(data){
            if(data.subjects.length === 0){
              that.setData({
                hasMore: false,
              })
            }else{
              that.setData({
                films: that.data.films.concat(data.subjects),
                start: that.data.start + data.subjects.length,
                showLoading: false
              })
            }
            typeof cb == 'function' && cb(res.data)
          })
        })
      }
    },
    fetchFilmDetail: function(url, id, cb) {
      var that = this;
      fetch(url + id).then(function(response){
        response.json().then(function(data){
          that.setData({
            showLoading: false,
            filmDetail: data
          })
          wx.setNavigationBarTitle({
              title: data.title
          })
          typeof cb == 'function' && cb(data)
        })
      })
    },
    fetchPersonDetail: function(url, id, cb) {
      var that = this;
      fetch(url + id).then(function(response){
        response.json().then(function(data){
          that.setData({
            showLoading: false,
            personDetail: data
          })
          wx.setNavigationBarTitle({
              title: data.name
          })
          typeof cb == 'function' && cb(data)
        })
      })
    },
    search: function(url, keyword, start, count, cb){
      var that = this
      var url = decodeURIComponent(url)
      if (that.data.hasMore) {
        fetch(url + keyword + '&start=' + start + '&count=' + count).then(function(response){
          response.json().then(function(data){
            if(data.subjects.length === 0){
              that.setData({
                hasMore: false,
                showLoading: false
              })
            }else{
              that.setData({
                films: that.data.films.concat(data.subjects),
                start: that.data.start + data.subjects.length,
                showLoading: false
              })
              wx.setNavigationBarTitle({
                  title: keyword
              })
            }
            typeof cb == 'function' && cb(res.data)
          })
        })
      }
    }
}
项目配置

city:城市
count:数量

module.exports = {
    city: '杭州',
    count: 20
}
第三步 编写页面
popular(热映)页面

这里以热映页面为例
待映、口碑排行、搜索结果页面可以以此类推

  • popular.json

配置窗口标题以及允许下拉刷新

{
  "navigationBarTitleText": "正在热映",
  "enablePullDownRefresh": true
}
  • popular.wxml

直接引入电影列表组件 与coming(待映)页、top(口碑排行)页、搜索结果页相同

<import src="../../component/filmList/filmList.wxml"/>
<template is="filmList" data="{{films: films, hasMore: hasMore, showLoading: showLoading, start: start, windowHeight: windowHeight}}"/>
  • popular.wxss
@import "../../component/filmList/filmList.wxss";
  • popular.js
  • 数据说明
    • films:电影列表
    • hasmore:上拉加载时是否还有更多数据
    • showLoading:是否显示loading动画
    • start:数据开始位置,用于上拉加载(每次累加)
    • windowHeight: 获取窗口高度,用于给scroll-view加高度(小程序中样式好像不能获取到窗口高度,用100%也没用,所以用js获取)
  • 页面事件
    • onLoad:页面载入,通过引入的fetch请求网络接口获取电影列表
      douban.fetchFilms.call(that, url, config.city, that.data.start, config.count)
    • onShow:页面显示,通过 wx.getSystemInfo() 获取窗口高度
    • scroll:scroll-view滚动事件
    • scrolltolower:滚动到底部触发的事件,即上拉加载更多数据。调用载入时请求的方法(此时start的值为20)
      douban.fetchFilms.call(that, url, config.city, that.data.start, config.count)
    • onPullDownRefresh:下拉刷新,初始化数据、显示加载动画并再次调用数据
    • viewFilmDetail:查看电影详情,通过e.currentTarget.dataset 获取标签中的data-*的数据,然后在路径中传递id给filmDetail页面
    • viewFilmByTag:点击标签(类型)时进入对应类型的搜索页
var douban = require('../../comm/script/fetch')
var config = require('../../comm/script/config')
var url = 'https://api.douban.com/v2/movie/in_theaters'
var searchByTagUrl = 'https://api.douban.com/v2/movie/search?tag='
Page({
    data: {
        films: [],
        hasMore: true,
        showLoading: true,
        start: 0,
        windowHeight: 0
    },
    onLoad: function() {
        var that = this
        douban.fetchFilms.call(that, url, config.city, that.data.start, config.count)
    },
    onShow: function() {
        var that = this
        wx.getSystemInfo({
          success: function(res) {
              that.setData({
                  windowHeight: res.windowHeight*2
              })
          }
        })
    },
    scroll: function(e) {
        console.log(e)
    },
    scrolltolower: function() {
        var that = this
        douban.fetchFilms.call(that, url, config.city, that.data.start, config.count)
    },
    onPullDownRefresh: function() {
        var that = this
        that.setData({
            films: [],
            hasMore: true,
            showLoading: true,
            start: 0
        })
        douban.fetchFilms.call(that, url, config.city, that.data.start, config.count)
    },
    viewFilmDetail: function(e) {
        var data = e.currentTarget.dataset;
        wx.navigateTo({
            url: "../filmDetail/filmDetail?id=" + data.id
        })
    },
    viewFilmByTag: function(e) {
        var data = e.currentTarget.dataset
        var keyword = data.tag
        wx.navigateTo({
            url: '../searchResult/searchResult?url=' + encodeURIComponent(searchByTagUrl) + '&keyword=' + keyword
        })
    }
})
filmDetail(电影详情)页面

这里以电影页面为例
人物详情页面可以以此类推

  • filmDetail.json

配置窗口标题

{
  "navigationBarTitleText": "电影详情"
}
  • filmDetail.wxml
<view class="container">
    <block wx:if="{{showLoading}}">
        <view class="loading">玩命加载中…view>
    block>
    <block wx:else>
    
        <view class="fd-hd">
            <view class="fd-hd-bg" style="background-image: url({{filmDetail.images.large}})">view>
            <image src="{{filmDetail.images.large}}" class="fd-cover">image>
            <view class="fd-intro">
                <view class="fd-title">{{filmDetail.title}}view>
                <view class="fd-intro-txt">导演:{{filmDetail.directors[0].name}}view>
                <view class="fd-intro-txt">
                    演员:
                    <block wx:for="{{filmDetail.casts}}" wx:for-item="filmDetailCastItem" wx:for-index="filmDetailCastIndex" wx:key="filmDetailCastItem">
                        {{filmDetailCastItem.name}}
                        <block wx:if="{{filmDetailCastIndex !== filmDetail.casts.length - 1}}">/block>
                    block>
                view>
                <view class="fd-intro-txt">豆瓣评分:
                    <block wx:if="{{filmDetail.rating.average == 0}}">
                        暂无评分
                    block>
                    <block wx:else>
                        {{filmDetail.rating.average}}分
                    block>
                view>
                <view class="fd-intro-txt">上映年份:{{filmDetail.year}}年view>
            view>
        view>
        <view class="fd-data">
            <view class="fd-data-item">
                <view class="fd-data-num">{{filmDetail.collect_count}}view>
                <view class="fd-data-title">看过view>
            view>
            <view class="fd-data-item">
                <view class="fd-data-num">{{filmDetail.wish_count}}view>
                <view class="fd-data-title">想看view>
            view>
            <view class="fd-data-item">
                <view class="fd-data-num">{{filmDetail.ratings_count}}view>
                <view class="fd-data-title">评分人数view>
            view>
        view>
        <view class="fd-bd">
            <view class="fd-bd-title">剧情简介view>
            <view class="fd-bd-intro">{{filmDetail.summary}}view>
            <view class="fd-bd-title">导演/演员view>
            <view class="fd-bd-person">
                <view class="fd-bd-person-item" data-id="{{filmDetail.directors[0].id}}" bindtap="viewPersonDetail">
                    <image class="fd-bd-person-avatar" src="{{filmDetail.directors[0].avatars.medium}}">image>
                    <view class="fd-bd-person-name">{{filmDetail.directors[0].name}}view>
                    <view class="fd-bd-person-role">导演view>
                view>
                <block wx:for="{{filmDetail.casts}}" wx:for-item="filmDetailCastItem" wx:key="filmDetailCastItem">
                    <view class="fd-bd-person-item" data-id="{{filmDetailCastItem.id}}" bindtap="viewPersonDetail">
                        <image class="fd-bd-person-avatar" src="{{filmDetailCastItem.avatars.medium}}">image>
                        <view class="fd-bd-person-name">{{filmDetailCastItem.name}}view>
                        <view class="fd-bd-person-role">演员view>
                    view>
                block>
            view>
            <view class="fd-bd-title">标签view>
            <view class="fd-bd-tag">
                <block wx:for="{{filmDetail.genres}}" wx:for-item="filmDetailTagItem" wx:key="filmDetailTagItem">
                    <view class="fd-bd-tag-item" data-tag="{{filmDetailTagItem}}" catchtap="viewFilmByTag">{{filmDetailTagItem}}view>
                block>
            view>
        view>
    block>
view>
  • filmDetail.wxss
@import "../../comm/style/animation.wxss";
.fd-hd {
    position: relative;
    width: 100%;
    height: 600rpx;
    display: flex;
    justify-content: center;
    align-content: center;
    overflow: hidden;
}
.fd-hd:before {
    content: '';
    display: block;
    position: absolute;
    z-index: 1;
    width: 100%;
    height: 600rpx;
    background-color: rgba(0, 0, 0, .6);
}
.fd-hd-bg {
    position: absolute;
    z-index: 0;
    width: 100%;
    height: 600rpx;
    background-size: cover;
    background-position: center;
    filter: blur(30rpx);
}
.fd-cover {
    z-index: 2;
    width: 300rpx;
    height: 420rpx;
    margin-top: 80rpx;
    border-radius: 8rpx;
    box-shadow: 0 30rpx 150rpx rgba(255, 255, 255, .3) } .fd-intro { z-index: 2;
    width: 320rpx;
    margin-top: 80rpx;
    margin-left: 40rpx;
    color: #fff;
}
.fd-title {
    margin-bottom: 30rpx;
    font-size: 42rpx;
}
.fd-intro-txt {
    margin-bottom: 18rpx;
    color: #eee;
}
.fd-data {
    display: flex;
    height: 150rpx;
    justify-content: space-around;
    align-items: center;
    border-bottom: 1px solid #f4f4f4;
}
.fd-data-item {
    width: 33.33%;
    text-align: center;
}
.fd-data-item {
    border-left: 1px solid #eee;
}
.fd-data-item:first-child {
    border-left: none;
}
.fd-data-num {
    font-size: 40rpx;
    font-weight: 100;
    color: #444;
}
.fd-data-title {
    color: #999;
}
.fd-bd {
    padding: 0 40rpx 40rpx;
}
.fd-bd-title {
    padding-left: 20rpx;
    margin-top: 40rpx;
    margin-bottom: 20rpx;
    font-size: 32rpx;
    font-weight: bold;
    color: #444;
    border-left: 10rpx solid #47a86c;
}
.fd-bd-intro {
    text-align: justify;
    line-height: 1.5;
    color: #666;
}
.fd-bd-tag {
    display: flex;
}
.fd-bd-tag-item {
    padding: 5rpx 10rpx;
    margin-right: 15rpx;
    border: 1px solid #ccc;
    border-radius: 10rpx;
    color: #666;
}
.fd-bd-person {
    display: flex;
    width: 100%;
    height: 480rpx;
    overflow-x: scroll;
    overflow-y: hidden;
}
.fd-bd-person-item {
    margin-left: 20rpx;
    text-align: center;
}
.fd-bd-person-item:first-child {
    margin-left: 0;
}
.fd-bd-person-avatar {
    width: 280rpx;
    height: 400rpx;
}
.fd-bd-person-name {
    color: #666;
}
.fd-bd-person-role {
    color: #999 }
  • filmDetail.js
  • 数据说明
    • filmDetail:电影详情数据
    • showLoading:是否显示loading动画
  • 页面事件
    • onLoad:页面载入,获取从电影列表传入的id (在options中),通过fetch请求网络接口获取电影详情
      douban.fetchFilmDetail.call(that, url, id)
    • viewPersonDetail:查看人物详情,通过e.currentTarget.dataset 获取标签中的data-*的数据,然后在路径中传递id给personDetail页面
    • viewFilmByTag:点击标签(类型)时进入对应类型的搜索页
var douban = require('../../comm/script/fetch')
var url = 'https://api.douban.com/v2/movie/subject/'
var searchByTagUrl = 'https://api.douban.com/v2/movie/search?tag='
Page({
    data: {
        filmDetail: {},
        showLoading: true
    },
    onLoad: function(options) {
        var that = this
        var id = options.id
        douban.fetchFilmDetail.call(that, url, id)
    },
    viewPersonDetail: function(e) {
        var data = e.currentTarget.dataset;
        wx.redirectTo({
          url: '../personDetail/personDetail?id=' + data.id
        })
    },
    viewFilmByTag: function(e) {
        var data = e.currentTarget.dataset
        var keyword = data.tag
        wx.navigateTo({
            url: '../searchResult/searchResult?url=' + encodeURIComponent(searchByTagUrl) + '&keyword=' + keyword
        })
    }
})
search(搜索)页面
  • search.json

配置窗口标题

{
  "navigationBarTitleText": "搜索"
}
  • search.wxml

引用了message组件,当没有输入内容时给出提示

"../../component/message/message.wxml"/>