(从0到1搭建-简单demo,编程思想)仿今日头条-微信小程序版

1,551 阅读14分钟

前言

最近正在学习微信小程序开发,学习过程中模仿了今日头条app写了一个极简版的微信小程序。这里主要分享一些我的学习过程及踩过的一些坑,希望对您有所帮助。

通过本篇文章你可以学习到:

  • 小程序的基本运用
  • 引入第三方组件库
  • 自定义组件
  • ES6的箭头函数,解构及缩写
  • 通过fastmock建立一个为后端接口
  • 发送数据请求
  • 数据驱动思想等

开发准备

总体架构

  • 该项目基于组件化开发,使用了模拟后端接口实现数据分离。前端使用小程序所支持的wxml + wxss + js + json开发模式,采用了vant第三方库。后端数据则是借助fastmock存储了项目相关的json数据。项目中的数据请求操作大致如下(一部分数据写在了app.js中,通过在onLaunch中调用wx.request() 获取接口数据,再把数据存入globalData中,方便其他页面取用);项目中也使用了vant组件,使用前需先安装node.js,安装步骤链接Nodejs安装教程,之后需要在构建npm包时引入vant,详情可见vant的快速引入(看前四个步骤就可以了) 。页面使用第三方组件时须在对应json文件中声明,为了不做重复工作可直接在app.json中声明。例:("usingComponents": "van-search": "@vant/weapp/search/index"}
|> components
    |> jr-explore     探索内容组件
        |> childCpns
    |> jr-hot         热榜组件
    |> jr-recommend   推荐内容组件
    |> jr-stick       置顶组件
|> images
    |> icon
|> miniprogram_npm
    |> @vant
|>pages
    |> index    新闻首页
    |> user     用户页
    |> video    视频页

项目规划

  • 在做该小程序之前,我先是分析每个页面对应功能,了解这款小程序的交互细节,清楚数据集合数据项。这样大概可以分为分析页面,创建数据集合,解构页面基本布局,数据绑定四步来展开。

参照今日头条APP,下面是我的小程序的tabBar

tabBar.png

进入app.json中,”sitemapLocation“之后添加”tabBar",注意:"list"中至少得含有2组图标,最多5组,并且所有list中的路径前面不用加根路径/ (正确写法:pages/index/index;错误写法:/pages/index/index)。

其中,所有要显示的页面都放入pages目录下。每组中的pagePath:点击图标之后要跳转的页面,值为跳转页面的路径;text:图标下面的文字;iconPath:未被选中时的图标,值为未被选中图标的路径;selectedIconPath:被选中时的图标,值为被选中图标的路径;

当点击某个图标时,该图标从iconPath切换成selectedIconPath,效果图如下:

1.gif

图标可直接从阿里巴巴矢量图标库中下载,下载时记得选择颜色,一种未选中,另一种选中时,并且记得收藏,以防以后的项目重复用

icon.jpg

app.json部分代码:后面代码基本都是展示关键部分,源码在最后面可以查看下载)

{
  "pages": [
  ],
  "window": {
  },
  "sitemapLocation": "sitemap.json",
  "tabBar": {
    "selectedColor": "#d81e06",   //标签被选中时的字体
    "list": [
      {
        "pagePath": "pages/index/index",     // 点击之后跳转的页面
        "text": "首页",                      // 图标下面的文字
        "iconPath": "images/icon/news.png", // 未被选中时的图标
        "selectedIconPath": "images/icon/news-active.png"   // 被选中时的图标
      },
      {
        "pagePath": "pages/video/video",
        "text": "视频",
        "iconPath": "images/icon/video.png",
        "selectedIconPath": "images/icon/video-active.png"
      },
      {
        "pagePath": "pages/user/user",
        "text": "我的",
        "iconPath": "images/icon/user.png",
        "selectedIconPath": "images/icon/user-active.png"
      }
    ]
  }
}

项目结构

以下是我主要实现的今日头条小程序界面 app.json部分代码

1.png2.png3.png
4.png5.png6.png

接下来对每个页面的细节进行解构。

首页

index.gif

index.wxml全部代码

<!--index.wxml-->
<van-tabs active="{{ active }}" swipeable sticky animated> // 引入vant组件库
  <van-tab title="推荐">
    <jr-stick top="{{recommendTop}}" />             // 自定义组件
    <jr-recommend cardList="{{recommendList}}" />   // 自定义组件
  </van-tab>
  <van-tab title="探索">
    <jr-stick top="{{exploreTop}}" />               // 自定义组件
    <jr-explore cardList="{{exploreList}}"/>        // 自定义组件
  </van-tab>
  <van-tab title="热榜">
    <jr-hot hot="{{hot}}" />                        // 自定义组件
  </van-tab>
</van-tabs>

整体首页界面导入了vant第三方组件库中的Tab 标签页,该组件可以在不同的内容区域之间进行切换,导入第三方组件库的好处是能够快速的开发项目。使用前需先安装node.js,安装步骤链接Nodejs安装教程,之后需要在构建npm包时引入vant,详情可见vant的快速引入(看前四个步骤就可以了)。

准备完成之后,在app.json中紧接着"tabBar"后面配置"usingComponents"

{
  "pages": [],
  "window": {},
  "sitemapLocation": "sitemap.json",
  "tabBar": {},
  "usingComponents": {
    "van-tab": "/miniprogram_npm/@vant/weapp/tab/index",
    "van-tabs": "/miniprogram_npm/@vant/weapp/tabs/index"
  }
}

注意:每项后面都要逗号',',并且引入vant组件时一定要记得全都放到app.json,否则在使用时会无效(当时引入的时候傻傻地配置到index.json中,结果弄了老半天)。引入时可以按自己的需求查看官方文档,但一定要注意路径,只需在复制官方文档中对应的路径前加上/miniprogram_npm/

vant.png

所有配置完成之后,直接上van-tabs,其中active是当前显示子标签页面的下标,swipeable sticky animated等属性表示开启粘性布局,启用切换 tab 时的动画,开启滑动切换标签页;而van-tab则是对应的子标签,里面用于写相关的页面,title是子标签的名字。

探索页面

explore.gif

探索页面样式

explore.png

 <van-tab title="探索">
    <jr-stick top="{{exploreTop}}" />               // 自定义组件
    <jr-explore cardList="{{exploreList}}"/>        // 自定义组件
  </van-tab>

自定义组件是把一堆view等标签封装到一个文件中,在大型项目中有许多页面需要重复使用,这时只需把数据分离,并且把相关的数据传入,就轻松地实现了一个页面,极大地减缓了开发压力

在根目录下新建一个components目录,该目录放入整个项目中会重复利用的组件;新建jr-stick,jr-recommend文件夹装入组件,单击鼠标右键,快捷生成component(pages下的page也是同理)

component.png

接着就是配置了,需要在哪个页面引入自定义组件,就在相应的json中引入。比如: 此时我们需要在index页面中引入jr-stick,jr-recommend两个组件,就在index.json中配置"usingComponents"属性,路径一定要正确。一旦新建了一个component,务必要记得及时配置,以免后期会遗忘。

{
  // **index.json部分代码**
  "usingComponents": {
    "jr-stick": "/components/jr-stick/jr-stick",
    "jr-explore": "/components/jr-explore/jr-explore"
  }
}

探索页面设置数据

// **inde.js部分代码**

Page({
  data: {
    active: 1,
     "exploreTop": [{
      "id": "1",
      "title": "习近平同蒙古国总统呼日勒苏赫会谈",
      "up": "置顶",
      "author": "新华社",
      "comment": "1252评论"
    }],
    "exploreList": [{
      "id": 1,
      "pic": "https://p3-passport.byteimg.com/img/user-avatar/f6aa5172433d44799ebe53f5938e6e2a~100x100.awebp",
      "author": "娱评人糖微甜",
      "date": "9小时前",
      "article": "“京城四少”:有人破产,有人入狱,有人为爱隐退,有人一地鸡毛11年前",
      "img": "https://w.wallhaven.cc/full/q2/wallhaven-q2e16r.png"
    }]
  }
})

设置好数据后,把组件需要的数据传入,exploreTop,exploreList表示在index.js中设置的数据名字,而top,cardList则是在组件中使用的数据名字

 <jr-stick top="{{exploreTop}}" />           
 <jr-explore cardList="{{exploreList}}"/>       

探索页面组件

传入数据后,需要在组件中的js中的properties组件的属性列表中声明数据,以jr-stick组件为例,在top为在组件中传入数据设置的名字,type为传入数据的类型,有Array, Obeject, Number等,value为初始化数据,值是对应的类型,有[]、{}、0

// components/jr-stick/jr-stick.js
Component({
  /**
   * 组件的属性列表
   */
  properties: {
    top: {
      type: Array,
      value: []
    }
  },
})
<!--components/jr-stick/jr-stick.wxml-->
<view class="container">
  <view wx:for="{{top}}" wx:key="id" class="item">
    <view class="title">{{item.title}}</view>
    <view class="description">
      <text class="up">{{item.up}}</text>
      <text class="author">{{item.author}}</text>
      <text class="comment">{{item.comment}}</text>
    </view>
  </view>
</view>

在jr-stick中使用了wx:for指令,它类似for(let item of top)一样,可以循环获取top中的数据,每一个item代表着当前top数组中的每一个对象**,还是没看懂的可以去看看官方文档详解 wx:for列表渲染

<!--components/jr-explore/jr-explore.wxml-->
  <view class="item">
   <explore-item wx:for="{{cardList}}" wx:key="id" item="{{item}}" />
  </view>

jr-explore也类似一样,不过是此基础上把每篇文章再构成一个子组件,再嵌套一个子组件是为了对单独文章进行一些js操作,而不会影响到其他文章。

推荐界面

recommend.gif

推荐页面样式

recommedn.png

<van-tab title="推荐">
    <jr-stick top="{{recommendTop}}" />
    <jr-recommend cardList="{{recommendList}}" />
  </van-tab>

推荐页面和探索页面相似,不过在样式上比探索页面更加的简洁,在该页面复用了jr-stick组件,并且对jr-explore组件进行一些微调整就行差不多形成了jr-recommend组件,在传入一些数据就完成了一个页面。:别忘了在inde.json中配置jr-recommend哦~。

推荐页面设置数据


**index.js部分代码**

Page({
  data: {
   "recommendTop": [{
      "id": "1",
      "title": "从二十大看中国共产党的成功密码之十",
      "up": "置顶",
      "author": "新华杜",
      "comment": "2484评论"
    }],
    "recommendList": [{
      "id": 1,
      "article": "《海绵宝宝》何以成为新时代的精神污染?",
      "img": "https://w.wallhaven.cc/full/l8/wallhaven-l8qy8l.jpg",
      "author": "游研社",
      "comment": "534评论",
      "date": "5天前"
    }]
  }
})

同样在设置好数据后,把数据传入组件中

<jr-stick top="{{recommendTop}}" />
<jr-recommend cardList="{{recommendList}}" />

推荐页面组件

// components/explore-content/explore-content.js
Component({
  /**
   * 组件的属性列表
   */
  properties: {
    cardList: {
      type: Array,
      value: []
    }
  }
})
<!--components/explore-content/explore-content.wxml-->
<view class="container">
  <view class="item" wx:for="{{cardList}}" wx:key="id">
    <view class="content">
      <view class="article">{{item.article}}</view>
      <view class="img">
        <image src="{{item.img}}" mode="widthFix" />
      </view>
    </view>
    <view class="foot">
      <view class="description">
        <text class="author">{{item.author}}</text>
        <text class="comment">{{item.comment}}</text>
        <text class="date">{{item.date}}</text>
      </view>
      <view class="close">
        <image src="/images/icon/close.png" mode=""/>
      </view>
    </view>
  </view>
</view>

声明数据与jr-stick组件一样,同样的也和jr-explore组件一样,只是它们的数据名字换了而已,同样使用了wx:for指令遍历。

热榜页面

hot.gif

热榜页面样式

hot.png

<van-tab title="热榜">
    <jr-hot hot="{{hot}}" />
 </van-tab>

注意:记住在inde.json中配置好jr-hot组件

热榜页面设置数据

// **index.js**
Page({
  data: {
  "hot": {
    "top": "习近平同贝宁总统塔隆互致贺电",
    "hotTab": ["热榜","疫情榜","科技榜","足球榜","军事榜","教育榜","游戏榜","娱乐榜"],
    "hotList": [{
        "id": 1,
        "title": "余华的钓鱼哲学:愿者上钩"
      },
      {
        "id": 2,
        "title": "蔡英文妄称“台湾够强就不会变战场”"
      }]
    }
  }
})
​

同样在设置好数据后,把数据传入组件中,不过这次仔细看了,数据中hot对象Object而不在是数组Array

热榜页面自定义组件

Component({
  /**
   * 组件的属性列表
   */
  properties: {
    hot: {
      type: Object,
      value: {}
    }
  }
})
<!--components/jr-hot/jr-hot.wxml-->
<view class="tab">
  <van-tabs active="{{ active }}" type="card">
    <van-tab wx:for="{{hot.hotTab}}" title="{{item}}"></van-tab>
  </van-tabs>
</view>
<view class="item">
  <view class="top">
    <image src="/images/icon/top.png" mode="widthFix" />
    <text>{{hot.top}}</text>
  </view>
  <view class="content" wx:for="{{hot.hotList}}" wx:key="id">
    <text class="id">{{item.id}}</text>
    <text class="title">{{item.title}}</text>
  </view>
</view>
<view class="pull">
  <text>展开更多</text>
  <image src="/images/icon/pull.png" mode="widthFix" />
</view>

热榜页面与其他首页页面对比,又多加了一个van-tabs组件标签页(这里做的有点点糙,勉强能看),其中van-tabs中的type属性为card样式风格。iamge标签中的mode属性为widthFix,代表着缩放模式,宽度不变,高度自动变化,保持原图宽高比不变,关于iamge详细的属性可以看看iamge微信官方文档

视频

video.gif

视频页面样式

video.png

<!--pages/video/video.wxml-->
<view class="item" wx:for="{{viedoList}}" wx:key="id">
  <view class="title">
    <image class="photo" src="{{item.pic}}" mode="widthFix" />
    <view class="description">
      <view class="author">{{item.author}}</view>
    </view>
    <view class="aside">
      <view class="follow">关注</view>
      <image class="more" src="/images/icon/more.png" mode="widthFix" />
    </view>
  </view>
<view class="content">
  <view class="article">{{item.article}}</view>
  <view class="video">
    <video
      poster="{{item.image}}"
      object-fit="cover"
      duration="{{item.duration_raw}}"
      src="{{item.video}}"
    ></video>
  </view>
</view>
<view class="foot">
  <view class="share">
    <image src="/images/icon/share.png" mode="" />
    <text>分享</text>
  </view>
  <view class="comment">
    <image src="/images/icon/comment.png" mode="" />
    <text>9213</text>
  </view>
  <view class="like">
    <image src="/images/icon/like.png" mode="" />
    <text>1.3万</text>
  </view>
  <view class="favorite">
    <image src="/images/icon/favorite.png" mode="" />
    <text>收藏</text>
  </view>
</view>
</view>

该页面整体上与前面首页的文章页面相似,也可以把一条视频封装成一个组件,不过为了显示一个js效果,该项目中并没有这么做

视频页面设置数据

// pages/video/video.js
Page({
  /**
   * 页面的初始数据
   */
  data: {
   "viedoList": [
    {
      "id": 1,
      "pic": "https://p3-passport.byteimg.com/img/user-avatar/f6aa5172433d44799ebe53f5938e6e2a~100x100.awebp",
      "author": "厚大罗翔说刑法",
      "article": "我天天晚上做梦都想着把张三干掉!罗翔说刑法",
      "video": "https://resources.ninghao.net/landrover/finding-adventure-at-home-480.mp4",
      "image": "https://resources.ninghao.net/landrover/finding-adventure-at-home-480.jpg",
      "duration_raw": 269,
      "duration": "04:30"
    }
    ]
  },
​
})

视频页面没有采用自定义组件,所以在json无需配置组件。数据中,pic是作者的头像,author是作者的名字,article是文章的标题

用户

6.png

用户页面样式

user.png

{
  "usingComponents": {
    "jr-user_head": "./childCpns/jr-user_head/jr-user_head",
    "jr-user_body": "./childCpns/jr-user_body/jr-user_body",
    "jr-user_foot": "./childCpns/jr-user_foot/jr-user_foot"
  }
}
<!--pages/user/user.wxml-->
<jr-user_head/>
<jr-user_body gridList="{{gridList}}" />
<jr-user_foot/>

用户页面采用了上中下三部分布局,分别划分为jr-user_head,jr-user_body,jr-user_foot。这里再次强调一下,一旦新建了一个component,务必要记得及时配置,因为整个项目越写到后面脑子就会和浆糊一样,特别容易忘记。

用户页面设置数据

// pages/user/user.js
Page({
​
  /**
   * 页面的初始数据
   */
  data: {
    "gridList": [
        {
          "icon": "/images/icon/message.png",
          "name": "消息私信"
        }
    ]
  }
})

这里的数据中icon是图标的照片,name是图标下的标题

用户页面组件

// pages/user/childCpns/jr-user_body/jr-user_body.js
Component({
  /**
   * 组件的属性列表
   */
  properties: {
    gridList: {
      type: Array,
      value: []
    }
  }
})
<!--pages/user/childCpns/jr-user_body/jr-user_body.wxml-->
<view class="page">
  <view class="grid-list">
    <view class="grid-item" wx:for="{{gridList}}" wx:key="id">
      <image src="{{item.icon}}" mode="" />
      <text>{{item.name}}</text>
    </view>
  </view>
</view>

用户页面又三个自定义组件,这里主要说一下jr-user_body组件。该组件使用了flex弹性布局,使页面形成了类似与表格的样式。先在父元素中开启弹性布局,之后在子元素中使用justify-content: center;使所有子元素水平居中,之后再通过align-items: center;使所有子元素垂直居中,在适当的调整以下子元素的样式就可以了。flex弹性布局对于移动端项目来说非常的遍历。

/* pages/user/childCpns/jr-user_body/jr-user_body.wxss */
.grid-list {
  display: flex;            // 开启弹性布局
  flex-wrap: wrap;          // 让子元素能自动换行
}
.grid-item {
  width: 25%;               // 设置子元素宽度,一行4justify-content: center;  // 所有子元素水平居中
  align-items: center;      // 所有子元素垂直居中
}

重点难点

数据驱动和建立模拟接口

  • 数据驱动其实就是一种把数据和view等标签分离开来,把数据集中在一起,让数据驱动整个页面
  • 建立模拟接口,需要fastmock这个网站来存储数据,步骤如下:
f1.png1f2.png2
f3.png3f4.png4
f5.png5f6.png6

获取到数据接口后,就有两种方式实现数据请求(其实本质上是一种,只是运用的方法不一样)

  • 第一种,以首页的页面为例

    // index.js
    // 获取应用实例
    const app = getApp()
    Page({
      data: {
        active: 1,
      },
      // 事件处理函数
      onLoad(options) {
        wx.request({
          // 向接口地址请求数据
          url: 'https://www.fastmock.site/mock/86972139cfd668eaea8d8f030f197ea0/jrtt/index',
          success: (res) => {   // 请求成功后把获取的数据设置到index.js中的data上
            const {             // ES6语法中的解构
              exploreTop, 
              recommendTop, 
              exploreList, 
              recommendList, 
              hot
            } = res.data
            this.setData({
              exploreTop,       // ES6语法中的缩写
              recommendTop,
              exploreList,
              recommendList,
              hot
            })
          }
        })  
      },
    })
    

    index.js中,onLoad()函数是在页面加载的时候就会自动运行的函数,接着调用wx.request(),该API是微信中用于数据请求的,url为从哪个接口地址请求数据,success为数据请求成功后执行的函数;(res) => {}ES6语法中的箭头函数,相当于function(res) {}res为数据请求成功后的传过来的数据,不过res里面除了真正的值(也就是data)外,还有一些其他东西,构成了一个res对象,自己可以在函数里输入console.log(res)看一下;const { exploreTop, recommendTop, exploreList, recommendList, hot} = res.data也是ES6语法,意思是从获取到的数据中单独分离出exploreTop, recommendTop, exploreList, recommendList, hot这些数据。最后,调用this.setData()把各个分离出来的数据设置到data中,其中exploreTop等也使用了ES6语法中的缩写,exploreTop相当于exploreTop: exploreTop

    tips:使用ES6语法可以使整个代码的可读性更高,也就是代码更加的简洁

  • 第二种,以用户页面和视频页面为例

    // app.js
    App({
      onLaunch() {
        wx.request({
          url: 'https://www.fastmock.site/mock/86972139cfd668eaea8d8f030f197ea0/jrtt/home',
          success: (response) => {
            const { gridList, viedoList } = response.data
            this.globalData.gridList = gridList
            this.globalData.viedoList = viedoList
          }
        })
      },
      globalData: {
        gridList: null,
        viedoList: null 
      }
    })
    

    首先,onLaunch()onLoad()作用相似,只不过onLaunch()是相对于整个项目的,为一登录小程序就加载数据,直接把所有数据放到了app.js总体上,不至于像第一种方法一样每个页面都需要为它设置一个接口,并且不用多次请求数据。数据请求成功后,还需设置一个globalData,把获取到的数据放入globalData中。

    // pages/user/user.js
    const app = getApp()
    Page({
      data: {},
      /**
       * 生命周期函数--监听页面加载
       */
      onLoad(options) {
        const gridList = app.globalData.gridList
        this.setData({
          gridList
        })
      }
    })
    

    之后,只需在每个页面加载时把app.js中的数据存入相对应js中的data就行了。const app = getApp()是获取到app对象,以至于能够从app中拿到数据。(不过第二种方法很容易获取不到数据,要在页面上随便修改后再删除保存才会获取到数据,并且用手机预览时更是无法拿到数据,真诚的希望有大佬能帮忙解决一下这个问题)

    如果实在无法获取到接口数据,可以使用我的接口链接

小建议

在自己写项目时,多使用console.log()打印,跟进数据变化;多查看文档w3cschool微信开发文档Vant-Weapp。而时间和精力有多的小伙伴自己可以试试在导航头部加一个搜索组件,或者做个文章的详情页面,或者点赞的js操作及分享时的弹出框,或者今日头条APP上的商城页面。

进阶学习小程序商场项目

learn.png

源码

本项目源码

下载Watt Toolkit可以加快github网站的访问速度哦~

结语

写项目的过程对我来说就是一个挑战,必须时刻得保持着头脑逻辑清晰,把控着全局,否者时不时冒出的bug令人烦躁。但当真正的写出来了之后成就感还是非常得大,非常感谢在我写项目过程中帮助过我的老师和同学。如果你喜欢我的这篇文章或者看到这里对你有些许帮助,不妨点个赞吧😺!同时也非常希望看到文章的你能给我一些建议,期待与你一起讨论学习微信小程序!