微信小程序开发

1、微信小程序简介

1.1、什么是微信小程序

​    百度百科给出的定义:

微信小程序,英文名Wechat Mini Program,是一种不需要下载安装即可使用的应用,它实现了应用“触手可及”的梦想,用户扫一扫或搜一下即可打开应用。

​    然后看下我未认证的小程序效果:

主页:

博客系列:

整体效果:

1.2、微信及小程序的历史

    2010年腾讯正筹划一项大事,那就是做一款比QQ更纯粹,更高级的交流软件——微信。

    2011年1月21日,微信1.0版本正式上线,那时候大家还停留在QQ的世界里,对微信也并不敏感。

    2011年5月10日,微信发布2.0版本,推出了一项语音功能,微信也算迈出了自己的第一步;

    2011年8月,微信发布2.5版本,新增“查看附近的人”交友功能,此时用户突破1500万;

    2011年10月,微信发布3.0版本,新增“摇一摇”和“漂流瓶”功能,这段时间也是微信用户高速增长的阶段;

    2011年12月,微信发布3.5 版本,也带来了全新的二维码“扫一扫”交友功能;

    2012年3月,微信用户数突破1亿;4月,微信发布4.0版本,新增了“朋友圈相册”功能,并确定英文名称为“Wechat”;

    2012年7月,微信发布4.2版本,增加视频聊天功能,同时发布网页版微信;

    2012年8月23日,微信公众平台上线,微信开始构建内容生态;

    2013年1月15日深夜,腾讯微信团队宣布:微信用户数突破3亿;

    2013年7月,微信国内注册用户突破4亿,8月5日, 微信支付正式上线;

    2013年10月24日,微信的注册用户突破6亿,每日活跃用户1亿;

    2014年1月,微信红包在春节前夕正式上线;

    2014年年3月,开放微信支付功能,并开放微信支付接口;

    2016年1月11日,微信之父张小龙时隔多年的公开亮相,解读了微信的四大价值观。张小龙指出,越来越多产品通过公众号来做,因为这里开发、获取用户和传播成本更低。拆分出来的服务号并没有提供更好的服务,所以微信内部正在研究新的形态,叫「微信小程序」;

    2016年3月1日,微信支付对转账功能停止收取手续费。同时,对提现功能开始收取手续费;

    2016年8月,微信获香港首批支付牌照;

    2016年9月21日,微信小程序正式开启内测;

    2017年1月9日,张小龙宣布小程序正式上线微信小程序的存在让我们既省了手机容量,又省了下载流量,为我们带来了巨大的便利;

    2017年12月,微信发布6.6.1 版本,开放了小游戏,还重点推荐了小游戏「跳一跳];

    2018年2月,微信全球用户月活数首次突破10亿;

    2019年11月,微信新增“腾讯QQ”小程序;

    2020年6月17日,微信上线“拍一拍”功能;

    直到今天,微信触及了我们生活的方方面面,似乎大家的生活已经和微信绑在一起了。

    而小程序是一种新的开放能力,开发者可以快速地开发一个小程序。小程序可以在微信内被便捷地获取和传播,同时具有出色的使用体验。

2、微信小程序的开发简介

2.1、小程序的开发流程

   ​ 官网给我们的列举的步骤:

1)注册

   ​ 在微信公众平台注册小程序,完成注册后可以同步进行信息完善和开发。官网注册说明

2)小程序信息完善

   ​ 填写小程序基本信息,包括名称、头像、介绍及服务范围等。

3)开发小程序

   ​ 完成小程序开发者绑定、开发信息配置后,开发者可下载开发者工具、参考开发文档进行小程序的开发和调试。

   ​ 下载开发者工具,如果是win7,最高版:wechat_devtools_1.05的版本。

   ​ 下载好微信开发者工具:

然后创建小程序:

   ​ AppID:注册成功后,我们可以在小程序后台,找到AppID;

   ​ 后端服务:微信云开发是腾讯云为移动开发者提供的一站式后端云服务,弱化后端和运维概念,让开发者可以专注于业务逻辑的实现,无需搭建服务器,使用平台提供的 API (云函数、云数据库、云存储)进行业务开发即可,云开发中提供的少部分模板是免费的;而不使用云开发,也就是传统的开发,需要自己搭建环境,毕竟上线后需要定期进行数据维护等工作。

   ​ 模板选择:可以选择使用Typescript和JavaScript等语言来开发;

​ 注:Typescript是微软开发的一个开源的编程语言,通过在JavaScript的基础上添加静态类型定义构建而成,最终可以转译成JavaScript代码。

新建好一个项目:

4)提交审核和发布

   ​ 完成小程序开发后,提交代码至微信团队审核,审核通过后即可发布(公测期间不能发布)。

2.2、目录结构

官网的描述:小程序包含一个描述整体程序的 app 和多个描述各自页面的 page

​ 1)一个小程序主体部分由三个文件组成,必须放在项目的根目录

文件必需作用
app.js小程序逻辑(管理整个程序的生命周期)
app.json小程序公共配置(包括了小程序的所有页面路径、界面表现、网络超时时间、底部 tab 等)
app.wxss小程序公共样式表(全局CSS样式)

​ 2)一个小程序页面由四个文件组成

文件类型必需作用
js页面逻辑
wxml页面结构
json页面配置,json是一种数据格式
wxss页面样式表

3)其他文件

文件类型必需作用
project.config.json开发者工具的配置文件
project.private.config.json项目私有配置文件
sitemap.json配置小程序及其页面是否允许被微信索引

3、微信小程序开发详解

   ​ 微信小程序采用的原生框架叫做MINA,关于小程序更详细的教程可以看官方教程,这里只挑选主要内容。

3.1、全局配置

   ​ app.js包括了小程序的所有页面路径、界面表现、网络超时时间、底部 tab 等。小程序默认的配置:

{
  "pages":[
    "pages/index/index",
    "pages/logs/logs"
  ],
  "window":{
    "backgroundTextStyle":"light",
    "navigationBarBackgroundColor": "#fff",
    "navigationBarTitleText": "Weixin",
    "navigationBarTextStyle":"black"
  },
  "style": "v2",
  "sitemapLocation": "sitemap.json"
}

3.1.1、app.json的pages属性

   ​ 用于指定小程序由哪些页面组成,每一项都对应一个页面的 路径(含文件名) 信息。文件名不需要写文件后缀,框架会自动去寻找对应位置的 .json.js.wxml.wxss 四个文件进行处理。

   ​ 未指定 entryPagePath 时,数组的第一项代表小程序的初始页面(首页)。

   ​ 我们使用微信开发者工具直接在pages中新增一个page页面,工具自动帮我们生成了一个目录及相应的页面文件。(如果是使用的VSCODE开发工具的话是不会帮我们新建的)

3.1.2、app.json的entryPagePath属性

   ​ 指定小程序的默认启动路径(首页),常见情景是从微信聊天列表页下拉启动、小程序列表启动等。如果不填,将默认为 pages 列表的第一项。

   ​ 在app.json配置文件中新增entryPagePath字段,并设置启动路径:

{
  "pages":[
    "pages/blog/blog",
    "pages/index/index",
    "pages/logs/logs"
  ],
  "window":{
    "backgroundTextStyle":"light",
    "navigationBarBackgroundColor": "#fff",
    "navigationBarTitleText": "Weixin",
    "navigationBarTextStyle":"black"
  },
  "style": "v2",
  "sitemapLocation": "sitemap.json",
  "entryPagePath":"pages/index/index"
}

​ 案例效果:页面多了个主页图标

3.1.3、app.json的window属性

   ​ 定义小程序所有页面的顶部背景颜色,文字颜色定义等。

   ​ 默认的window就是我们前面看到的Weixin头部信息。官网window字段文档

"window":{
    "backgroundTextStyle":"light",
    "navigationBarBackgroundColor": "#fff",
    "navigationBarTitleText": "Weixin",
    "navigationBarTextStyle":"black"
}

​ 接下来我们修改点app.json里的内容:

{
  "pages":[
    "pages/blog/blog",
    "pages/index/index",
    "pages/logs/logs"
  ],
  "window":{
    "backgroundTextStyle":"dark",
    "navigationBarBackgroundColor": "#00bfff",
    "navigationBarTitleText": "穆瑾轩的博客",
    "navigationBarTextStyle":"white",
    "enablePullDownRefresh":true
  },
  "style": "v2",
  "sitemapLocation": "sitemap.json"
}

​ backgroundTextStyle,下拉 loading 的样式,enablePullDownRefresh开启全局的下拉刷新。

​ 案例效果:

 

通过上面的学习,我们大致了解到微信小程序的页面布局:

    但是有人会想,怎么看有的小程序的标题有其他颜色或者字体比我的大?微信对于window的设置并没有开放字体大小及颜色的设置(固定的样式,页面比较稳定,不会溢出)。但是微信提供了navigationStyle:'custom'来设置自定义的导航,官方对于navigationStyle的说明:

​     自定义导航需要了解小程序的自定义组件,这个后面再演示。

3.1.4、app.json的tabBar属性

   ​ 如果小程序是一个多 tab 应用(客户端窗口的底部或顶部有 tab 栏可以切换页面),可以通过 tabBar 配置项指定 tab 栏的表现,以及 tab 切换时显示的对应页面。

   ​ 通常用的比较多的是:顶部tabBar和底部的tabBar。

​ tabBar的一些属性:

​ 1)一级属性

属性类型必填默认值描述最低版本
colorHexColortab 上的文字默认颜色,仅支持十六进制颜色(未选中时)
selectedColorHexColortab 上的文字选中时的颜色,仅支持十六进制颜色
backgroundColorHexColortab 的背景色,仅支持十六进制颜色
listArraytab 的列表,详见 list 属性说明,最少 2 个、最多 5 个 tab
positionstringbottomtabBar 的位置,仅支持 bottom / top
custombooleanfalse自定义 tabBar,见详情2.5.0

​ 2)二级属性

属性类型必填说明
pagePathstring页面路径,必须在 pages 中先定义
textstringtab 上按钮文字
iconPathstring图片路径,icon 大小限制为 40kb,建议尺寸为 81px * 81px,不支持网络图片。 当 position 为 top 时,不显示 icon。
selectedIconPathstring选中时的图片路径,icon 大小限制为 40kb,建议尺寸为 81px * 81px,不支持网络图片。 当 position 为 top 时,不显示 icon。

​ 官网中有给我们说明一些属性的意思:

 

 新增tabBar配置:

{
  "pages":[
    "pages/blog/blog",
    "pages/person/person",
    "pages/index/index",
    "pages/logs/logs"
  ],
  "window":{
    "backgroundTextStyle":"dark",
    "navigationBarBackgroundColor": "#00bfff",
    "navigationBarTitleText": "穆瑾轩的博客",
    "navigationBarTextStyle":"white",
    "enablePullDownRefresh":true
  },
  "style": "v2",
  "sitemapLocation": "sitemap.json",
  "tabBar":{
    "list":[
      {
        "pagePath":"pages/blog/blog",
        "text": "博客",
        "iconPath":"images/bk3.png",
        "selectedIconPath":"images/bk2.png"
      },
      {
        "pagePath":"pages/person/person",
        "text": "关于我",
        "iconPath":"images/bk_gyw1.png",
        "selectedIconPath":"images/bk_gyw2.png"
      }
    ],
    "selectedColor":"#FF6F00",
    "position":"bottom"
  }
}

​ 案例效果:

3.2、页面配置

  ​ 每一个小程序页面也可以使用同名 .json 文件来对本页面的窗口表现进行配置,页面中配置项会覆盖 app.json 的 window 中相同的配置项。

​ 注:并不是所有的都能覆盖,具体以官网列举的为准。

3.2.1、页面配置index.json

  ​ 例如,我配置person.json文件如下:

{
  "usingComponents": {},
  "enablePullDownRefresh":false,
  "navigationBarBackgroundColor":"#00cccc"
}

​ 案例效果:

​ 导航栏的背景颜色被改变,且无法下拉刷新。

3.3、小程序的组件

     在学习页面配置前,我们先了解下小程序给我提供了哪些组件来渲染页面。打开官网的组件页面,我们可以看到有这些组件:

    1)视图容器 2)基础内容 3)表单组件 4)导航 5)媒体组件 6)地图 7)画布 8)开放能力 9)原生组件说明 10)无障碍访问 11)导航栏 12)页面属性配置节点

​    如果之前了解过VUE,对组件的概念应该就不那么陌生了。组件是视图层的基本组成单元,可以扩展页面元素,是对UI层的封装。

   ​ 这里只挑选几个常用的(视图容器、基础内容、表单组件、导航)演示。

3.3.1、视图容器

   ​ 一个容器可以容纳多个组件,并使他们成为一个整体。容器可以简化图形化界面的设计,以整体结构来布局界面。

   ​ 常用视图容器有view、scroll-view、swiper、swiper-item

3.3.1.1、view组件

   ​ view组件是页面中最基本的容器组件。类似于Html中的div,用来进行页面布局的,具有块级盒子特性,能够接受其他组件的嵌入。

  ​ view组件可以实现横向布局、纵向布局、嵌套等效果。

属性类型默认值必填说明最低版本
hover-classstringnone指定按下去的样式类。当 hover-class="none" 时,没有点击态效果1.0.0
hover-stop-propagationbooleanfalse指定是否阻止本节点的祖先节点出现点击态1.5.0
hover-start-timenumber50按住后多久出现点击态,单位毫秒1.0.0
hover-stay-timenumber400手指松开后点击态保留时间,单位毫秒1.0.0

1)横向布局

​ person.wxml

<!--pages/person/person.wxml-->
<view class="container">
  <view>
      <text>flex-direction: row——横向布局</text>
  </view>
  <view class="flex-wrp">
      <view class="flex-item demo-text-1">A</view>
      <view class="flex-item demo-text-2">B</view>
      <view class="flex-item demo-text-3">C</view>
  </view>
</view>

​ person.wxss

/* pages/person/person.wxss */
.flex-item{
  width: 200rpx;
  height: 200rpx;
  text-align: center;
  line-height: 200rpx;
}
.demo-text-1{
  background-color: red;
}
.demo-text-2{
  background-color: green;
}
.demo-text-3{
  background-color: pink;
}

.container .flex-wrp{
  display: flex;
  flex-direction: row;
}

​ 案例效果:

2)纵向布局

​ 修改显示方式为按列显示

.container .flex-wrp{
  display: flex;
  flex-direction: column;
}

​ 案例效果:

3)嵌套

person.wxml

<!--pages/person/person.wxml-->
<view class="container">
  <view>
      <text>view-hover:视图点击生态-不阻止父类</text>
  </view>
  <view class="view-parent" hover-class="view-hover">我是父类容器
    <view class="view-son" hover-class="view-hover">我是子类容器</view>
  </view>
  <view>
      <text>view-hover:视图点击生态-阻止父类</text>
  </view>
  <view class="view-parent" hover-class="view-hover">我是父类容器
    <view class="view-son" hover-class="view-hover" hover-stop-propagation>我是子类容器</view>
  </view>
</view>

person.wxss

/* pages/person/person.wxss */
.view-parent {
  width: 100%;
  height: 350rpx;
  background-color: pink;
  text-align: center;
}
.view-son {
  width: 50%;
  height: 200rpx;
  background-color: blue;
  margin: 20rpx auto;
  text-align: center;
  line-height: 200rpx;
}

.view-hover {
  background-color: green;
}

案例效果:不阻止的点击子容器全部变绿,阻止的点击子容器只有子容器变绿。

3.3.1.2、scroll-view组件

​ scroll-view是可滚动视图容器组件。当视图无法完全展示内容时,可以采取滑动的方式,使控件显示其完整内容。

​ scroll-view的属性比较多,详细查看官网介绍。

1)纵向滚动

person.wxml

<!--pages/person/person.wxml-->
<view class="container">
  <view>
      <text>scroll-view:可滚动视图-纵向滚动</text>
  </view>
  <!--scroll-y 属性 允许纵向滚动-->
  <scroll-view scroll-y class="scrolly">
    <view class="scroll-y-item bg_green">A</view> 
    <view class="scroll-y-item bg_red">B</view> 
    <view class="scroll-y-item bg_blue">C</view> 
    <view class="scroll-y-item bg_yellow">D</view>
  </scroll-view>
</view>

person.wxss

.scroll-y-item{
  width: 200rpx;
  height: 200rpx;
  text-align: center;
  line-height: 200rpx;
}

.scrolly{
  height: 200rpx;
  width: 200rpx;
}

.bg_green{
  background-color: green;
}

.bg_red{
  background-color: red;
}

.bg_blue{
  background-color: blue;
}

.bg_yellow{
  background-color: yellow;
}

​ 案例效果:

2)横向滚动

person.wxml

<!--pages/person/person.wxml-->
<view class="container">
  <view>
      <text>scroll-view:可滚动视图-横向滚动</text>
  </view>
  <!--scroll-x 属性 允许横向滚动-->
  <scroll-view scroll-x class="scrollx">
    <view class="scroll-x-item bg_green">A</view> 
    <view class="scroll-x-item bg_red">B</view> 
    <view class="scroll-x-item bg_blue">C</view> 
    <view class="scroll-x-item bg_yellow">D</view>
  </scroll-view>
</view>

person.wxss

/* pages/person/person.wxss */
.scroll-x-item{
  width: 200rpx;
  height: 200rpx;
  display: inline-block;
  text-align: center;
  line-height: 200rpx;
}

.scrollx{
  width: 400rpx;
  white-space: nowrap;
  border: 1px red solid;
}

.bg_green{
  background-color: green;
}

.bg_red{
  background-color: red;
}

.bg_blue{
  background-color: blue;
}

.bg_yellow{
  background-color: yellow;
}

​ 案例效果:

3.3.1.3、swiper和swiper-item组件

​    swiper是滑块视图容器组件,其中只可放置swiper-item组件,否则会导致未定义的行为。通常用于图片间的切换,也就是所谓的轮播图。

person.wxml

<!--pages/person/person.wxml-->
<view class="swiper-container">
  <view>
      <text>swiper:滑块视图容器-轮播图</text>
  </view>
  <swiper class="swiper" indicator-dots autoplay interval="3000" indicator-color="white">
    <swiper-item class="swiper-item bg_green">A</swiper-item> 
    <swiper-item class="swiper-item bg_red">B</swiper-item> 
    <swiper-item class="swiper-item bg_blue">C</swiper-item> 
    <swiper-item class="swiper-item bg_yellow">D</swiper-item>
  </swiper>
</view>

person.wxss

/* pages/person/person.wxss */
.swiper-item{
  width: 100%;
  text-align: center;
  line-height: 300rpx;
}

.swiper{
  height: 300rpx;
}
.bg_green{
  background-color: green;
}

.bg_red{
  background-color: red;
}

.bg_blue{
  background-color: blue;
}

.bg_yellow{
  background-color: yellow;
}

​ 案例效果:

​ 其他视图容器组件cover-view,movable-view,page-container等等使用方式看官网。

3.3.2、基础内容

3.3.2.1、icon组件和progress组件

   ​ icon图标组件,这个对于我们来说应该不陌生了,很多系统图标、软件图标就是用的扩展名为*.icon、*.ico格式的图片。

   ​ progress是进度条组件。

​ 这个官网上就比较详细了。

3.3.2.1、text组件和rich-text组件

   ​ text是文本组建,类似于HTML中的span标签,是一个行内元素。

   ​ text 组件内只支持 text 嵌套,text组件和view组件的区别就是除了文本节点以外的其他节点都无法长按选中

   ​ rich-text是富文本组建,支持把HTML字符串渲染成wxml结构。

1)text组件测试

<!--pages/person/person.wxml-->
<view class="container" >
    我是view组件
    <text>我是text组件,测试超长是否自动换行的哈哈哈哈哈哈哈哈哈</text>
    
    <text user-select>我是text组件,支持长按选中,\n测试手动换行</text>
    
</view>

​ 案例效果:

3.3.3、表单组件

​    表单组件有:button、checkbox、form、input、label、picker-view、radio、slider、switch、textarea等等。

组件名称组件说明
button按钮组件
checkbox复选框组件
form表单组件
input输入框组件
label标签组件
picker-view滚动选择器组件
radio单选组件
slider滑动选择器组件
switch开关组件
textarea多行文本框组件

3.3.3.1、button组件

   ​ button是按钮组件,功能比HTML中的button丰富很多,通过open-type属性可以调用微信提供的各种功能(打开客户会话、转发、获取用户手机号、获取用户信息、打开app、打开授权页面、获取用户头像等)。

<!--pages/person/person.wxml-->
<view class="container" >
    <!--全局设置里的app.json中的"style": "v2"是新版样式,删除掉就是旧版样式-->
    <label>我是button组件:</label>
    <button>普通按钮</button>
    <button type="primary">主色调按钮</button>
    <!--size设置按钮的大小-->
    <button type="warn" size="mini">警告按钮</button>
    <!--plain镂空,背景色透明-->
    <button plain size="mini">警告按钮</button>
    <!--open-type="contact"打开客户会话-->
    <button type="primary" size="mini" open-type="contact">打开客户会话
    </button>
</view>

​ 案例效果:新版样式

旧版样式:

3.3.4、媒体组件

   ​ 媒体组件主要是用来处理图片、视频、音频的组件。主要包括:

组件名称组件说明
camera系统相机,如:扫描二维码(微信6.7.3版本)
image图片
live-player实时音视频播放
live-pusher实时音视频录制
video视频
viop-room多人音频视频对话

3.3.4.1、camera组件

​    camera组件可以调用系统相机,支持拍照、扫码二维码功能。同一页面只能插入一个 camera 组件。

person.wxml

<!--pages/person/person.wxml-->
<view class="container" >
  <camera device-position="back" flash="off" binderror="error" style="width: 100%; height: 300px;"></camera>
  <button type="primary" bindtap="takePhoto">拍照</button>
  <view>预览</view>
  <image mode="widthFix" src="{{src}}"></image>
    
</view>

person.js

// pages/person/person.js
Page({
  takePhoto() {
    const ctx = wx.createCameraContext()
    ctx.takePhoto({
      quality: 'high',
      success: (res) => {
        this.setData({
          src: res.tempImagePath
        })
      }
    })
  },
  error(e) {
    console.log(e.detail)
  }
})

​ 案例效果:

3.3.4.2、image组件

   ​ image图片组件,支持 JPG、PNG、SVG、WEBP、GIF 等格式。

   ​ image组件默认宽度320px、高度240px;image组件中二维码/小程序码图片不支持长按识别。仅在 wx.previewImage 中支持长按识别。

​ 原图:

   使用image组件的mode属性来指定图片裁剪和缩放模式。

<!--pages/person/person.wxml-->
<view class="page" style="margin-left: 1rem;">
  <view class="page__hd">
    <text class="page__title">image</text>
    <text class="page__desc">图片</text>
  </view>
  <view class="page__bd">
    <view class="section section_gap" wx:for="{{array}}" wx:for-item="item">
      <view class="section__title">{{item.text}}</view>
      <view class="section__ctn">
        <image style="width: 150px; height: 180px; background-color: #eeeeee;" mode="{{item.mode}}" src="{{src}}"></image>
      </view>
    </view>
  </view>
</view>

person.js

Page({
  data: {
    array: [{
      mode: 'scaleToFill',
      text: 'scaleToFill:不保持纵横比缩放图片,使图片完全适应'
    }, {
      mode: 'aspectFit',
      text: 'aspectFit:保持纵横比缩放图片,使图片的长边能完全显示出来'
    }, {
      mode: 'aspectFill',
      text: 'aspectFill:保持纵横比缩放图片,只保证图片的短边能完全显示出来'
    }, {
      mode: 'top',
      text: 'top:不缩放图片,只显示图片的顶部区域'
    }, {
      mode: 'bottom',
      text: 'bottom:不缩放图片,只显示图片的底部区域'
    }, {
      mode: 'center',
      text: 'center:不缩放图片,只显示图片的中间区域'
    }, {
      mode: 'left',
      text: 'left:不缩放图片,只显示图片的左边区域'
    }, {
      mode: 'right',
      text: 'right:不缩放图片,只显示图片的右边边区域'
    }, {
      mode: 'top left',
      text: 'top left:不缩放图片,只显示图片的左上边区域'
    }, {
      mode: 'top right',
      text: 'top right:不缩放图片,只显示图片的右上边区域'
    }, {
      mode: 'bottom left',
      text: 'bottom left:不缩放图片,只显示图片的左下边区域'
    }, {
      mode: 'bottom right',
      text: 'bottom right:不缩放图片,只显示图片的右下边区域'
    }],
    src: '/images/image_mm.jpg'
  },
  imageError: function(e) {
    console.log('image3发生error事件,携带值为', e.detail.errMsg)
  }
})

​ 案例效果:

​ 其他组件用到时在学习。

2.3、小程序中的基础语法及生命周期

   ​ 前面我们学习了小程序的基础配置和一些常用组件。在开发小程序的页面时,我们会在wxml用一些组件,并且附上一些wxss样式及一些逻辑*.js代码去渲染我们的页面,所以我们经常会去编辑这些文件(*.wxml、*.wxss、*.js)。

   ​ 小程序中的法语和vue中的语法类似,但是又有一些区别。

2.3.1、wxml语法简介

   ​ WXML(WeiXin Markup Language)是框架设计的一套标签语言,结合基础组件事件系统,可以构建出页面的结构。

 2.3.1.1、WXML与HTML区别

   ​ WXML与HTML的标签名不同

  • HTML(div、span、img、a)

  • WXML有自己的一套内置的组件(view、text、image、navigator)

2.3.1.2、数据绑定

​    WXML提供了类似于Vue中的模板语法。在data中定义数据,WXML中使用数据:

<!--Vue的动态绑定 :src-->
<img :src="imgPath"></img>
<!--小程序的动态绑定-->
<image src="{{imgPath}}"></image>

例如:

blog.wxml

<!--pages/blog/blog.wxml-->
<image src="{{imgPath}}"></image>

blog.js

// pages/blog/blog.js
Page({

  /**
   * 页面的初始数据
   */
  data: {
    imgPath:"/images/image_mm.jpg"
  }
})

案例效果:

2.3.1.3、列表渲染

<!--Vue-->
<li v-for="item in items">
    {{ item.message }}
</li>
<!--wxml-->
<view wx:for="{{array}}"> {{item}} </view>
  • wx:for

   ​ 在组件上使用 wx:for 控制属性绑定一个数组,即可使用数组中各项的数据重复渲染该组件。

   ​ 默认数组的当前项的下标变量名默认为 index,数组当前项的变量名默认为 item

   ​ 使用 wx:for-item可以修改当前项的默认变量(item)名称。

   ​ 使用 wx:for-index 可以指定数组当前下标的变量(index)名称。

  • wx:key

   ​ 优点类似于vue中的:key的用法。如果列表中项目的位置会动态改变或者有新的项目添加到列表中,并且希望列表中的项目保持自己的特征和状态时需要提供,如不提供 wx:key,会报一个 warning, 如果明确知道该列表是静态,或者不必关注其顺序,可以选择忽略。

   ​ wx:key 的值以两种形式提供:

   ​ a)字符串,数组中 item 的某个 property,要确保项保持自己的特征,此时的property的值是需要唯一的。

   ​ b)保留关键字 *this,代表在 for 循环中的 item 本身,items本身作为一个唯一的字符串或者数字。

例如:

blog.js

<span style="color:#000000"><span style="background-color:#ffffff"><code>Page({

  <span style="color:#5c6370"><em>/**
   * 页面的初始数据
   */</em></span>
  <span style="color:#d19a66">data</span>: {
    <span style="color:#d19a66">array1</span>:[<span style="color:#d19a66">1</span>,<span style="color:#d19a66">2</span>,<span style="color:#d19a66">3</span>,<span style="color:#d19a66">4</span>],
    <span style="color:#d19a66">array2</span>:[
      {<span style="color:#d19a66">id</span>:<span style="color:#98c379">"1"</span>,<span style="color:#d19a66">username</span>:<span style="color:#98c379">"a"</span>},
      {<span style="color:#d19a66">id</span>:<span style="color:#98c379">"2"</span>,<span style="color:#d19a66">username</span>:<span style="color:#98c379">"b"</span>},
      {<span style="color:#d19a66">id</span>:<span style="color:#98c379">"3"</span>,<span style="color:#d19a66">username</span>:<span style="color:#98c379">"c"</span>}
    ]
  }
})
</code></span></span>

​ 案例效果:

Page({

  /**
   * 页面的初始数据
   */
  data: {
    array1:[1,2,3,4],
    array2:[
      {id:"1",username:"a"},
      {id:"2",username:"b"},
      {id:"3",username:"c"}
    ]
  }
})

2.3.1.4、条件渲染

  • wx:if

​ vue中使用v-if和v-show控制元素的显示和隐藏。

​ 小程序中使用 wx:if="" 和hidden控制元素的显示和隐藏。

<view wx:if="{{condition}}"> true </view>

<view hidden="{{condition}}">false</view>
  • block wx:if

<block wx:if="{{true}}">
  <view> view1 </view>
  <view> view2 </view>
</block>

​    <block/> 并不是一个组件,它仅仅是一个包装元素,不会在页面中做任何渲染,只接受控制属性。

​ wx:if和hidden的区别:

​    一般来说,wx:if 有更高的切换消耗而 hidden 有更高的初始渲染消耗。因此,如果需要频繁切换的情景下,用 hidden 更好,如果在运行时条件不大可能改变则 wx:if 较好。

2.3.1.5、其他

​    WXML提供模板(template),可以在模板中定义代码片段,然后在不同的地方调用。模板用法,具体参考官网。

​    WXML 提供两种文件引用方式importinclude,具体用法,参照官网。

2.3.2、wxss语法简介

   ​ WXSS (WeiXin Style Sheets)是一套样式语言,用于描述 WXML 的组件样式。

2.3.2.1、WXSS和CSS的区别

  • CSS中需要手动进行像素单位换算;

  • WXSS在底层支持新的尺寸单位rpx,在不同大小的屏幕上小程序会自动进行换算;

  • WXSS支持样式导入,使用@import语句;

  • WXSS仅支持部分CSS选择器。

    详细内容至官网查看。

2.3.3、生命周期

​    生命周期是指一个对象从创建-运行-销毁的整个过程。相比Vue的生命周期,小程序的生命周期要简单一点。

   ​ 小程序中的生命周期分为两类:

​ 1)应用生命周期:小程序从启动-运行-销毁的过程;

​ 2)页面生命周期:每个页面的加载-渲染-销毁的过程;

2.3.3.1、应用生命周期

   ​ 小程序的应用生命周期在app.js中进行声明。详解内容,看官网

// app.js
App({
  onLaunch() {
    // 小程序初始化完成时执行,全局只触发一次,做一些初始化工作
    console.log("onLaunch");
  },
  onShow(){
    //小程序启动或从后台进入前台显示时触发
    console.log("onShow");
  },
  onHide(){
    //小程序从前台进入后台时触发
    console.log("onHide");
  },
  onError(msg){
    //小程序发生脚本错误或 API 调用报错时触发
    console.log("onHide"+msg);
  },
  onPageNotFound(res){
    //小程序要打开的页面不存在时触发。
    wx.redirectTo({
      url: 'pages/...'
    })
  },
  onUnhandledRejection(){
    //小程序有未处理的 Promise 拒绝时触发
    console.log("onUnhandledRejection");
  },
  onThemeChange(){
    //系统切换主题时触发
    console.log("onThemeChange");
  }
})

2.3.3.2、页面生命周期

​ 小程序的页面生命周期需要在页面的.js文件中进行声明。更详细的看官网

Page({
  onLoad: function(options) {
    // 生命周期回调—监听页面加载.
  },
  onShow: function() {
    // 生命周期回调—监听页面显示.
  },
  onReady: function() {
    // 生命周期回调—监听页面初次渲染完成.
  },
  onHide: function() {
    // 生命周期回调—监听页面隐藏.
  },
  onUnload: function() {
    // 生命周期回调—监听页面卸载.
  }
  ...
})

   打开小程序:(App)onLaunch --> (App)onShow --> (Pages)onLoad --> (Pages)onShow --> (pages)onRead

   进入下一个页面:(Pages)onHide --> (Next)onLoad --> (Next)onShow --> (Next)onReady

   离开小程序:(App)onHide

3.4、小程序的API

​    MINA 框架提供丰富的微信原生 API,如获取用户信息,本地存储,支付功能等。api在开发中实践,具体看官网

4、我的小程序开发

4.1、首页的实现

4.1.1、首页底部导航栏

   ​ 首页底部导航栏是比较好实现的,设置navigationBar窗口信息,再添加tabBar,包括:推荐、博客、关于我即可。

{
  "pages":[
    "pages/index/index",
    "pages/blog/blog",
    "pages/person/person",
    "pages/logs/logs"
  ],
  "window":{
    "backgroundTextStyle":"dark",
    "navigationBarBackgroundColor": "#00bfff",
    "navigationBarTitleText": "穆瑾轩的博客",
    "navigationBarTextStyle":"white",
    "enablePullDownRefresh":true
  },
  "style": "v2",
  "sitemapLocation": "sitemap.json",
  "tabBar":{
    "list":[
      {
        "pagePath":"pages/index/index",
        "text": "推荐",
        "iconPath":"images/home.png",
        "selectedIconPath":"images/homeSelected.png"
      },
      {
        "pagePath":"pages/blog/blog",
        "text": "博客",
        "iconPath":"images/bk3.png",
        "selectedIconPath":"images/bkSelected.png"
      },
      {
        "pagePath":"pages/person/person",
        "text": "关于我",
        "iconPath":"images/bk_gyw1.png",
        "selectedIconPath":"images/bk_gywSelected.png"
      }
    ],
    "selectedColor":"#FF6F00",
    "position":"bottom"
  }
}

案例效果:

4.1.2、首页轮播图

   ​ 首页轮播图的实现比较简单,前面我们在学习swiper组件时候就实现过。

1)index.wxml

<view class="container">
  <page class="page">
      <!-- 轮播图 -->
      <swiper class="banner" indicator-dots="true" autoplay="true" interval="3000" duration="1000" style="margin-top: 1rpx;">
            <swiper-item wx:for="{{runbo}}" wx:key="id" class="banner-item" >
              <image src="{{item.imgsrc}}" style="width: 100%;"  mode="widthFix"></image>
            </swiper-item>
      </swiper>
  </page>
</view>

2)index.wxss

.page{
  background-color: #FFFFFF;
  height: 100%;
}

.banner{
  width: 750rpx;
  height: 375rpx;
  white-space: nowrap;
}

3) index.js

// index.js
Page({
  data: {
    runbo:[
      {
        id:'1',
        imgsrc:'/images/rb_image1.jpg'
      },
      {
        id:'2',
        imgsrc:'/images/rb_image2.jpg'
      },
      {
        id:'3',
        imgsrc:'/images/rb_image3.jpg'
      }
    ]
  }
})

案例效果:

4.1.3、首页推荐内容

   ​ 首页推荐内容分两块,一块是最新内容,一块是推荐内容。本来想接入公众号的,无奈非认证的个人小程序没有这个资格。

1)index.wxml

<!--index.wxml-->
<view class="container">
  <page class="page">
      <!-- 首页 -->
      <!-- <scroll-view scroll-into-view="item_0">
          <view class="weui-tabs-bar__wrp" style="margin-top: 3rpx;">
            <view class="weui-tabs-bar__content">
                <view class="weui-tabs-bar__title tab-bar-title__selected"  style="border-bottom-color: #07c160">
                    <text>首页</text>
                </view>
            </view>
          </view>
      </scroll-view> -->
      <!-- 轮播图 -->
      <swiper class="banner" indicator-dots="true" autoplay="true" interval="3000" duration="1000" style="margin-top: 1rpx;">
            <swiper-item wx:for="{{runbo}}" wx:key="id" class="banner-item" >
              <image src="{{item.imgsrc}}" style="width: 100%;"  mode="widthFix"></image>
            </swiper-item>
      </swiper>
       <!-- 最新 -->
      <view class="article-follow">—— 最新 ——</view>
     
      <!-- 文章列表 -->
      <view class="article-card" wx:for="{{zx_articles}}" wx:for-item="articles" wx:key="id">
          <navigator class="content" url="{{articles.contentPath}}">
            <image class="cover" src="{{articles.imageUrl}}" mode="widthFix"></image>
            <text class="title">{{articles.title}}</text>
            <text class="desc">{{articles.content}}</text>
          </navigator>
          <view class="operation">
            <text class="date">{{articles.createTime}}</text>
          </view>
      </view>

      <!-- 推荐 -->
      <view class="article-follow">—— 推荐 ——</view>

      <!-- 文章列表 -->
      <view class="article-card" wx:for="{{tj_articles}}" wx:for-item="articles" wx:key="id">
          <navigator class="content" url="{{articles.contentPath}}">
            <image class="cover" src="{{articles.imageUrl}}" mode="widthFix"></image>
            <text class="title">{{articles.title}}</text>
            <text class="desc">{{articles.content}}</text>
          </navigator>
          <view class="operation">
            <text class="date">{{articles.createTime}}</text>
          </view>
      </view>
  </page>  

</view>

2)index.wxss

.page{
  background-color: #FFFFFF;
  height: 100%;
}

.banner{
  width: 750rpx;
  height: 375rpx;
  white-space: nowrap;
}

.article-follow{
  box-sizing: border-box;
  padding: 0;
  margin: 0;
  line-height: 100rpx;
  font-size: 32rpx;
  text-align: center;
  color: #666666;
}
.weui-tabs-bar__wrp {
  border-bottom: 1px solid #eee;
  margin-top: 10px;
}

.weui-tabs-swiper {
  width: 100%;
  height: 100px;
}

.tab-content .tab-content-item{
  height: 100px;
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  box-sizing: border-box;
  padding: 40rpx;
}

.weui-tabs-bar__title {
  margin: 0px 10px;
  font-size: 14rpx;
}

.tab-bar-title__selected {
  font-size: 16px;
  font-weight: bold;
}

.article-card{
  margin: 5rpx 0rpx 100rpx 0rpx;
  background: #ffffff;
  border-style: solid;
  box-shadow:0px 2px 4px 2px #DDDDDD;
  border-width: 0rpx;
}

.article-card .content {
  width: 750rpx;
  padding-bottom: 5rpx;
}

.article-card .operation {
  padding: 10rpx 30rpx 0rpx 30rpx;
}
.article-card .operation .date {
  font-size: 26rpx;
  color:#666666;
}

.article-card .content .cover {
  width: 750rpx;
  height: 100%;
}

.article-card .content .title {
  font-size: 32rpx;
  padding: 10rpx 30rpx 5rpx 30rpx;
  display: block;
}

.article-card .content .desc {
  font-size: 26rpx;
  color:#CCCCCC;
  padding: 0rpx 30rpx;
  display: block;
}

3)index.js

Page({
  data: {
    runbo:[
      {
        id:'1',
        imgsrc:'/images/rb_image1.jpg'
      },
      {
        id:'2',
        imgsrc:'/images/rb_image2.jpg'
      },
      {
        id:'3',
        imgsrc:'/images/rb_image3.jpg'
      }
    ],
    zx_articles: [],
    tj_articles: [],
  },
  // 事件处理函数
  bindViewTap() {
    
  },
  onLoad() {
    //设置文章列表
    const zx_articles = [
      {
        id: 1,
        imageUrl: '/images/bk_article.jpeg',
        title: '重学设计模式-设计模式总结',
        content:'设计模式并不是一种具体的技术,它讲述的是解决问题的思想,是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案...',
        createTime:'2022-07-02 23:22:59',
        contentPath:'/pages/article/article'
      }
    ]
    this.setData({ zx_articles });

    const tj_articles = [
      {
        id: 2,
        imageUrl: '/images/bk_article_gui.jpeg',
        title: 'Java GUI——Java图形用户界面',
        content:' 早期,电脑向用户提供的是单调、枯燥、纯字符状态的“命令行界面(CLI)...',
        createTime:'2021-10-10 23:22:59',
        contentPath:'/pages/article/article'
      },
      {
        id: 3,
        imageUrl: '/images/bk_article_vue.png',
        title: 'vuepress使用简介及个人博客搭建',
        content:' vuepress 是 Vuejs 官方提供的一个是Vue驱动的静态网站生成器,基于Markdown语法生成网页...',
        createTime:'2021-08-20 23:22:59',
        contentPath:'/pages/article/article'
      }
    ]
    this.setData({ tj_articles });
  }
})

案例效果:

4.1.4、首页推荐详情页面

   ​ 首页推荐列表完成了,接下来实现以下文章详情页面。由于未认证的小程序的各种限制,导致我无法从我的博客或者公众号中直接获取文章信息。那文章详情页面我们在app.json中再加一个detail页面。然后再新建一个template模板,文章的格式比较固定,不同的文章我们用模板去实现。

1)articles.wxml

<!--入口模版-->
<template name="articleDetail">
      <view class="container">
        <!-- 标题 -->
        <view class="article-title">{{articlesData.title}}</view>
        <!-- 作者 -->
        <view class="article-sub-title">
            <text style="padding-right:21px">{{articlesData.author}}</text>
            <text>{{articlesData.createTime}}</text>
        </view>
        <view class="article-common-content">
            <view class="commonContent">{{articlesData.commonContent}}</view>
        </view>
        <!-- 文章内容 -->
        <template is="articlesContent" data="{{articlesData}}" />
    </view>
</template>

<!-- 文章内容 -->
<template name="articlesContent">
    <!-- 内容解析 -->
    <block wx:for="{{articlesData.content}}" wx:key="index" wx:for-item="contents">
      <!-- 目录 -->
      <block wx:if="{{contents.type == 'h1' || contents.type == 'h2' || contents.type == 'h3' || contents.type == 'h4'|| contents.type == 'h5'}}">
          <view class="contents-{{contents.type}}">
              <text>{{contents.text}}</text>
          </view>
      </block>

      <block wx:if="{{contents.type == 'p'}}">
         <view class="contents-{{contents.type}}">
              <text>{{contents.text}}</text>
          </view>
      </block>

      <block wx:if="{{contents.type == 'img'}}">
          <view class="contents-{{contents.type}}">
              <image src="{{contents.src}}"></image>
          </view>
      </block>
    </block>
</template>

2)detail.wxml

<!--pages/detail/detail.wxml-->
<import src="./template/articles" />
<template is="articleDetail" data="{{articlesData}}"></template>

3)detail.js

// pages/detail/detail.js
Page({

  /**
   * 页面的初始数据
   */
  data: {
    articlesData:{}
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
    var that = this;
    if(options.id=='1'){
      that.setData({
        articlesData:{
          id:'1',
          title:'设计模式总结',
          author:'作者:穆瑾轩',
          createTime:'2022-07-02 23:22:59',
          commonContent:'公众号:java穆瑾轩;原文:CSDN-https://blog.csdn.net/xiaoxianer321',
          content:[
            {
              type:'h3',
              text:'1、设计模式总结', 
            },
            {
              type:'p',
              text:'  至此,我们已经完成了23种设计模式的学习,最后很有必要做个总结。\n \n   设计模式并不是一种具体的技术,它讲述的是解决问题的思想,是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案,是对类的封装性、继承性和多态性,以及类的关联关系和组合关系的充分理解与应用。\n\n  在软件工程中有个概念,叫高内聚低耦合,它是作为评判一个软件设计的好坏标准之一。\n\n  高内聚:内聚针对的是模块的内部设计,是指一个软件模块内的各个元素彼此结合的紧密程度,高内聚就是一个模块内各个元素彼此结合的紧密度高。简单的说就是一个模块是否由相关性很强的代码组成,没有冗余和过于复杂的逻辑,只负责一项任务,也就是常说的符合单一职责原则。\n\n  低耦合:耦合针对的是模块(或类)与模块(或类)之间交互的复杂度,模块间的耦合度取决于模块间接口的复杂性、调用方式以及传递信息,传递的信息越少往往耦合度就越低。高内聚低耦合可以保证服务的独立性以及系统的灵活性。因此我们学习设计模式的核心思想与之不谋而合——解耦合(减少可能增加耦合度的设计)。\n\n  不管是前端、还是后端都在趋向于组件化,甚至是应用架构上也诞生了微服务,而组件化、微服务化的思想都是高内聚低耦合的体现。\n\n  我们学习设计模式的核心思想就是解耦合,并不是消除耦合,而是把耦合控制在一定范围,找到稳定点和变化点,运用抽象,把变化点隔离起来。先满足设计原则,再迭代出设计模式。\n\n  其实各设计模式之间都是有共通之处的(在java中是抽象、继承、多态、组合的综合运用),有些看起来十分类似但又能解决不同的问题,这些都是前人总结的经验,你也可以组合出更适用于你所编写的程序的一种模式。'
            }
          ]
        }
      })
    }else if(options.id=='2'){
      var that = this;
      that.setData({
        articlesData:{
          id:'2',
          title:'Java GUI——Java图形用户界面',
          author:'作者:穆瑾轩',
          createTime:'2021-10-10 23:22:59',
          commonContent:'公众号:java穆瑾轩;原文:CSDN-https://blog.csdn.net/xiaoxianer321',
          content:[
            {
              type:'h3',
              text:'1、Java GUI概述', 
            },
            {
              type:'h4',
              text:'1.1、GUI的前世今生', 
            },
            {
              type:'p',
              text:'  早期,电脑向用户提供的是单调、枯燥、纯字符状态的“命令行界面(CLI)”。如:Windows中的DOS窗口。后来,Apple公司率先在电脑的操作系统中实现了图形化的用户界面(Graphical User Interface,简称GUI),但由于Apple公司封闭的市场策略,与其它PC不兼容。这使得Apple公司错过了一次一统全球PC的好机会。后来,Microsoft公司推出了风靡全球的Windows操作系统,它凭借着优秀的图形化用户界面,一举奠定了操作系统标准的地位。\n\n  在这图形用户界面风行于世的今天,一个应用软件没有良好的GUI是无法让用户接受的。而Java语言也深知这一点的重要性,它提供了一套可以轻松构建GUI的工具。\n\n  AWT,Java最早的界面库。(java.awt:Abstract Windows ToolKit(抽象窗口工具包),需要调用本地系统方法来实现功能,属重量级控件。)\n\n  Swing,是对AWT的扩展。(javax.swing:在AWT的基础上, 建立的一套图像界面系统,其中提供了更多的组件,而且完全由Java实现。增强了移植性,属轻量级组件。)\n\n  JavaFX,JDK1.8引入的新的界面库。\n\n  SWT,Eclipse使用的界面库。它吸收了AWT和Swing实现的最好的部分,SWT于2001年与Eclipse IDE(Integrated Development Environment)一起集成发布。在这个最初发布版之后,SWT发展和演化为一个独立的版本。 JFace的构建基于SWT,它提供了SWT的功能和更简易的MVC模式。SWT和JFace不仅使Java成为一个构建桌面应用程序的可行的选择,也使之成为一个具有优势的开发平台。'
            }
          ]
        }
      })
    }else if(options.id=='3'){
      var that = this;
      that.setData({
        articlesData:{
          id:'3',
          title:'vuepress使用简介及个人博客搭建',
          author:'作者:穆瑾轩',
          createTime:'2021-08-20 23:22:59',
          commonContent:'公众号:java穆瑾轩;原文:CSDN-https://blog.csdn.net/xiaoxianer321',
          content:[
            {
              type:'h3',
              text:'1、vuepress概述 ', 
            },
            {
              type:'p',
              text:'  vuepress 是 Vuejs 官方提供的一个是Vue驱动的静态网站生成器,基于Markdown语法生成网页。简单的说它就是一个快速建设文档站点的工具,在简单配置好功能后,需要做的事情就剩下写好一个个 Markdown 文档,并且可以将其发布到github。\n'
            },
            {
              type:'h3',
              text:'2、vuepress简介 ', 
            },
            {
              type:'h4',
              text:'2.1、vuepress搭建 ', 
            },
            {
              type:'h4',
              text:'2.2、vuepress目录结构说明 ', 
            },
            {
              type:'h4',
              text:'2.3、MarkDown语法简介 ', 
            }
          ]
        }
      })
    }
  },
})

4)detail.json

{
  "navigationBarTitleText":"文章详情"
}

5)detail.wxss

/* pages/detail/detail.wxss */
.article-title{
  box-sizing: border-box;
  padding: 0;
  margin: 0;
  word-break: break-all;
  overflow: auto;
  font-weight: bold;
  font-size: 40rpx;
  justify-content:center;
  display: flex;
}

.article-sub-title{
  box-sizing: border-box;
  margin: 0;
  font-size: 30rpx;
  padding: 10rpx 0rpx 10rpx 10rpx;
}

.article-common-content{
  box-sizing: border-box;
  font-size: 24rpx;
  align-items: center;
  color: deepskyblue;
  padding: 10rpx 0rpx 10rpx 10rpx;
}

.contents-p{
  font-size: 30rpx;
  margin-left: 4rpx;
  padding: 10rpx 0rpx 10rpx 10rpx;
}

.contents-text{
  box-sizing: border-box;
  min-height: 50rpx;
  font-size: 30rpx;
}

.contents-h1{
  font-size: 40rpx;
  font-weight: bold;
  padding: 10rpx 0rpx 10rpx 10rpx;
}

.contents-h2{
  font-size: 32rpx;
  font-weight: bold;
  font-size: 16px;
  padding: 10rpx 0rpx 10rpx 10rpx;
}

.contents-h3{
  font-size: 32rpx;
  font-weight: bold;
  padding: 10rpx 0rpx 10rpx 10rpx;
}

.contents-h4{
  font-size: 32rpx;
  font-weight: bold;
  padding: 10rpx 0rpx 10rpx 10rpx;
}

.contents-h5{
  font-size: 32rpx;
  font-weight: bold;
  padding: 10rpx 0rpx 10rpx 10rpx;
}

案例效果:

4.2、博客系列页面实现

   ​ 我们看到有些小程序不仅仅有底部导航栏,也有顶部导航栏。博客系列页面,就来实现这一效果。

4.2.1、博客页顶部导航实现1

   ​ 顶部导航栏官网给我们提供了一个Tab组件的案例

1)使用步骤:

1)首先在项目目录下执行:
npm init
会生成package.json和package-lock.json两个文件
2)再执行安装
npm install @miniprogram-component-plus/tabs
安装成功后,项目目录下会生成node_modules目录
3)在开发工具中选择:工具-构建npm
项目目录下会生成miniprogram_npm目录
4)在需要使用的地方*.json引入即可
{
  "usingComponents": {
    "mp-tabs": "@miniprogram-component-plus/tabs"
   }
}
5)接着再把官方案例中的样式也顺过来吧(weui.wxss和common.wxss)

2)blog.json

{
  "usingComponents": {
    "mp-tabs": "@miniprogram-component-plus/tabs"
   }
}

3)pages/blog/blog.wxml

<view class="container">
  <view class="page">
  <mp-tabs 
    tabs="{{tabs}}" 
    activeTab="{{activeTab}}" 
    swiperClass="weui-tabs-swiper"
    bindtabclick="onTabClick"
    bindchange="onChange"
    activeClass="tab-bar-title__selected"
    
  >
    <block wx:for="{{tabs}}" wx:key="title">
      <view class="tab-content" data-set="{{item}}" slot="tab-content-{{index}}" bind:tap="handleClick" >
        <image src="{{item.img}}" mode="widthFix"></image>
        <view class="item-title">
          {{item.title2}}
        </view>
        <view class="item-desc">
          {{item.desc}}
        </view>
      </view>
    </block>
  </mp-tabs>
</view>

</view>

4)pages/blog/blog.js

// pages/blog/blog.js
Page({
  data: {
    tabs: [],
    activeTab: 0,
  },
  onLoad() {
    const tabs = [
      {
        title: '博客系列',
        title2: '博客系列',
        img: 'http://mmbiz.qpic.cn/sz_mmbiz_jpg/GEWVeJPFkSEV5QjxLDJaL6ibHLSZ02TIcve0ocPXrdTVqGGbqAmh5Mw9V7504dlEiatSvnyibibHCrVQO2GEYsJicPA/0?wx_fmt=jpeg',
        desc: '描述1..........',
      },
      {
        title: 'java合集',
        title2: 'java合集',
        img: 'http://mmbiz.qpic.cn/sz_mmbiz_png/GEWVeJPFkSHALb0g5rCc4Jf5IqDfdwhWJ43I1IvriaV5uFr9fLAuv3uxHR7DQstbIxhNXFoQEcxGzWwzQUDBd6Q/0?wx_fmt=png',
        desc: '微信小程序直播系列课程持续更新中,帮助大家更好地理解、应用微信小程序直播功能。',
      },
      {
        title: '前端知识',
        title2: '常见问题和解决方案',
        img: 'http://mmbiz.qpic.cn/sz_mmbiz_jpg/GEWVeJPFkSGqys4ibO2a8L9nnIgH0ibjNXfbicNbZQQYfxxUpmicQglAEYQ2btVXjOhY9gRtSTCxKvAlKFek7sRUFA/0?wx_fmt=jpeg',
        desc: '提高审核质量',
      },
      {
        title: 'python合集',
        title2: '流量主小程序',
        img: 'http://mmbiz.qpic.cn/sz_mmbiz_jpg/GEWVeJPFkSH2Eic4Lt0HkZeEN08pWXTticVRgyNGgBVHMJwMtRhmB0hE4m4alSuwsBk3uBBOhdCr91bZlSFbYhFg/0?wx_fmt=jpeg',
        desc: '本课程共四节,将分阶段为开发者展示如何开通流量主功能、如何接入广告组件、不同类型小程序接入的建议,以及如何通过工具调优小程序变现效率。',
      },
      {
        title: '小游戏',
        title2:'2020中国高校计算机大赛',
        img: 'http://mmbiz.qpic.cn/mmbiz_jpg/TcDuyasB5T3Eg34AYwjMw7xbEK2n01ekiaicPiaMInEMTkOQtuv1yke5KziaYF4MLia4IAbxlm0m5NxkibicFg4IZ92EA/0?wx_fmt=jpeg',
        desc: '微信小程序应用开发赛',
      },
    ]
    this.setData({ tabs })
  },
  getUserProfile(e) {

  },
  getUserInfo(e) {
   
  },
  onTabClick(e) {
    const index = e.detail.index
    this.setData({ 
      activeTab: index 
    })
  },

  onChange(e) {
    const index = e.detail.index
    this.setData({ 
      activeTab: index 
    })
  },
  handleClick(e) {
    
  }
})

案例效果:

4.2.2、博客页顶部导航栏实现2

   ​ 我们还可以借助Vant Weapp库的Tab标签来实现。由于小程序目前最大支持上传2048kb,直接使用npm依赖包的方式去引用的话,会占据我更多的空间。我直接拷贝它的源码tab和tabs等组件源码来实现,这种实现难度要大些,需要自己修改一些组件的代码。

1)拷贝vant库的源码:

2)新建推荐页

<!--index.wxml-->
<!--在公共页面新建如下页面,待后续实现-->
<import src="../../lib/common/bkxlTemplate.wxml"/>
<import src="../../lib/common/javaTemplate.wxml"/>
<import src="../../lib/common/qdzsTemplate.wxml"/>
<import src="../../lib/common/xyxTemplate.wxml"/>
<import src="../../lib/common/pythonTemplate.wxml"/>

<view class="con">
  <van-tabs swipeable animated tab-class="tab-box" title-active-color="#A6894E" color="#DFBF7D" bind:change="onTabChange" active="{{ active }}">
  <!--可以单独添加上面的导航栏-->
  <van-tab title="推荐" class="tab">
    <swiper class="banner" indicator-dots="true" autoplay="true" interval="3000" duration="1000">
        <swiper-item wx:for="{{runbo}}" wx:key="id" class="banner-item" >
          <image src="{{item.imgsrc}}" mode="scaleToFill" style="width: 100%;" mode="widthFix"></image>
        </swiper-item>
    </swiper>
  </van-tab>


<!--也可以在index.js中配置使用模板引入-->
  <van-tab title="{{item.tabsName}}" wx:for="{{fatherList}}" wx:key="index" wx:for-index="index" class="tab">
    <view class="" wx:for="{{item.list}}" wx:key="idx" wx:for-index="idx" class="van-item">
      <view>{{item.title}}</view>
      <view>{{item.temp}}</view>
      <template is='{{item.temp}}'/>
    </view>
  </van-tab>
</van-tabs>
</view>

3)index.js

Page({
  data: {
    fatherList: [
      {
        tabsName: '博客系列',
        list: [
          {
            temp: 'bkxlTemplate',
            title: '博客系列'
          }
        ]
      },
      {
        tabsName: 'java合集',
        list: [
          {
            temp: 'javaTemplate',
            title: 'java合集'
          }
        ]
      },
      {
        tabsName: '前端知识',
        list: [
          {
            temp: 'qdzsTemplate',
            title: '前端知识'
          }
        ]
      },{
        tabsName: '小游戏',
        list: [
          {
            temp: 'xyxTemplate',
            title: '小游戏'
          }
        ]
      },
      {
        tabsName: 'Python',
        list: [
          {
            temp: 'pythonTemplate',
            title: 'Python'
          }
        ]
      }
    ],
    runbo:[
      {
        id:'1',
        imgsrc:'/images/rb_image1.jpg'
      },
      {
        id:'2',
        imgsrc:'/images/rb_image2.jpg'
      },
      {
        id:'3',
        imgsrc:'/images/rb_image3.jpg'
      }
    ],
    active: 0
  },
  onLoad: function(options) {

  },
  onReachBottom() {
    
  },
  onReady: function() {
    // 页面渲染完成
  },
  onShow: function() {

  },
  onHide: function() {
    // 页面隐藏
  },
  onUnload: function() {
    // 页面关闭
  },
  getAccessToken(){
    //小程序中无法直接使用公众号明文的AppID和AppSecret来获取小程序的文章,所以我放弃了
    var $appid="";// AppID
    var $appSecret="";// AppSecret
    var $url="https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={$appid}&secret={$appSecret}";
    return $url;
  },
  onTabChange(event) {
    
  }
})

4)index.wxss

/**index.wxss**/
.container{
  height: 100%;
  width: 100%;
}
/**style="width: 100%;" mode="widthFix"
   解决各种不同设备的轮播图适屏问题
**/
.banner {
  width: 100%;
  height: 375rpx;
}

案例效果:

4.2.3、博客系列页面动画实现

1)blog.wxml

<!--pages/blog/blog.wxml-->
<view class="container">
  <view class="page">
  <mp-tabs 
    tabs="{{tabs}}" 
    activeTab="{{activeTab}}" 
    swiperClass="weui-tabs-swiper"
    bindtabclick="onTabClick"
    bindchange="onChange"
    activeClass="tab-bar-title__selected"
  >
  <block wx:for="{{tabs}}" wx:key="title">
    <view class="tab-contentsss" data-set="{{item}}" slot="tab-content-{{index}}">
        <view style="width: 100%;white-space: nowrap;">
          <image src="/images/rb_image1.jpg" style="width: 100%;height:375rpx;"  mode="widthFix"></image>
        </view>
        <!-- 正常应该做公共页面,使用模板引入的方式能减少挺多代码 -->
        <!-- 博客系列展示 -->
        <block wx:if="{{item.type === 'bkxl'}}">
            <view class="bkxl_lw">
                 <view class="bkxl_zt_left topImage" bindtap="transImg" data-id="bkxl_hd">
                      <view style="background-color: #FF9E7A;" class="bkxl_jb">
                        <view class="bkxl_loge_lf"  id="bkxl_hd">
                            <view class="bkxl_img">
                                <image class="bkxl_img" animation="{{animation}}" src="/images/bkxl_hd.png" class="bkxl_tp" ></image>
                            </view>
                        </view>
                      </view>
                     <view class="bkxl_mc">后端开发</view>
                 </view>

                 <view class="bkxl_zt_right topImage" bindtap="transImg" data-id="bkxl_qd">
                    <view style="background-color: #41fc89;" class="bkxl_jb">
                      <view class="bkxl_loge_lf" id="bkxl_qd">
                          <view class="bkxl_img">
                              <image animation="{{animation}}" src="/images/bkxl_qd.png" class="bkxl_tp" ></image>
                          </view>
                      </view>
                    </view>
                    <view class="bkxl_mc">前端开发</view>
                </view>
              </view>

              <view class="bkxl_lw">
                 <view class="bkxl_zt_left topImage" bindtap="transImg" data-id="bkxl_yd">
                      <view style="background-color: #8ac5ec;" class="bkxl_jb">
                        <view class="bkxl_loge_lf" id="bkxl_yd">
                          <view class="bkxl_img">
                              <image animation="{{animation}}" src="/images/bkxl_ydd.png" class="bkxl_tp" ></image>
                          </view>
                        </view>
                      </view>
                     <view class="bkxl_mc">移动端开发</view>
                 </view>

                 <view class="bkxl_zt_right topImage" bindtap="transImg" data-id="bkxl_yx">
                    <view style="background-color: #dafa68;" class="bkxl_jb">
                      <view class="bkxl_loge_lf" id="bkxl_yx">
                          <view class="bkxl_img">
                              <image animation="{{animation}}" src="/images/bkxl_yx.png" class="bkxl_tp" ></image>
                              </view>
                          </view>
                    </view>
                    <view class="bkxl_mc">游戏开发</view>
                </view>
                <view style="height: 10px;"></view>
              </view>
        </block>
      </view>
  </block>
  </mp-tabs>
  </view>
</view>

2)blog.wxss

/* pages/blog/blog.wxss */
@import '../common.wxss';

.page{
  background-color: #FFFFFF;
  height: 100%;
}

.weui-tabs-bar__wrp {
  border-bottom: 1px solid #eee;
  margin-top: 10px;
}

/* 默认是150高度,此处解决swiper不同滑块高度问题 */
.weui-tabs-swiper {
  width: 100%;
  overflow: auto;
  height: 105vh;
}

.tab-content {
  height: 100px;
  width: 100%;
  /* display: flex; */
  /* justify-content: center; */
  /* align-items: center; */
  box-sizing: border-box;
  padding: 40rpx;
}

.weui-tabs-bar__title {
  margin: 0px 10px;
}

.tab-bar-title__selected {
  font-size: 20px;
  font-weight: bold;
}

/* 图片上下浮动 */
.bkxl_img{
  animation: image 1.5s infinite; 
}

@keyframes image {
  0% {
      transform: translate(0px, 0px);
  }
  50% {
      transform: translate(0px, -9px);
  }
  100% {
      transform: translate(0px, 0px);
  }
} 

/* 图片翻转css代码改用微信api */
/* .topImage{
  -webkit-animation: transform-5 1.5s ease 500ms alternate none 1;
    animation: transform-5 1.5s ease 500ms alternate none 1;
}

.topImage:hover{
  -webkit-animation: transform-5 1.5s ease 500ms alternate none 1;
    animation: transform-5 1.5s ease 500ms alternate none 1;
}
@-webkit-keyframes transform-5 { 
	from{   
		-webkit-transform:perspective(400px) rotateY(91deg);
		transform:perspective(400px) rotateY(91deg);
	} 
    to{
    	-webkit-transform:perspective(400px) rotateY(0deg);
    	transform:perspective(400px) rotateY(0deg);
    } 
}
@keyframes transform-5 { 
	from{
		-webkit-transform:perspective(400px) rotateY(91deg);
		transform:perspective(400px) rotateY(91deg);
	} 
    to{
    	-webkit-transform:perspective(400px) rotateY(0deg);
    	transform:perspective(400px) rotateY(0deg);
    } 
} */

.bkxl_lw{
  display: flex;
  flex-direction: row;
  margin-top: 50rpx;
}

.bkxl_zt_left{
  margin-right: auto;
  margin-left: 30rpx; 
  width: 300rpx; 
  height: 300rpx;
  border:1rpx rgb(177, 170, 170) solid;
  border-width: 0rpx;
  border-radius: 1ch;
  box-shadow:0px 2px 4px 2px #DDDDDD;
}
.bkxl_mc{
  height: 90rpx;
  border-radius: 0ch 0ch 1ch 1ch;
  text-align: center;
  line-height: 90rpx;
}

.bkxl_jb{
  height: 210rpx;
  background-color: #FF9E7A;
  border-radius: 1ch 1ch 0ch 0ch;
}

.bkxl_tp{
  width: 240rpx;
  height:230rpx;
  margin: auto;
}

.bkxl_img{
  margin: auto;
  margin-left: 30rpx;
}

.bkxl_zt_right{
  margin-left: auto;
  margin-right: 30rpx; 
  width: 300rpx; 
  height: 300rpx;
  border:1rpx rgb(177, 170, 170) solid;
  border-width: 0rpx;
  border-radius: 1ch;
  box-shadow:0px 2px 4px 2px #DDDDDD;
}

3)blog.js(使用wx.createAnimation)实现动画效果

// pages/blog/blog.js
Page({
  data: {
    tabs: [],
    activeTab: 0,
    animation: '',
    swiperClass: 'mybk',
  },
  onLoad() {
    const tabs = [
      {
        title: '博客系列',type: 'bkxl',
      },
      {
        title: 'java合集',type: 'java',
      },
      {
        title: '前端知识',type: 'qdzs',
      },
      {
        title: 'python合集',type: 'python',
      },
      {
        title: '小游戏',type: 'xyx',
      },
    ]
    this.setData({ tabs })
  },
  onTabClick(e) {
    const index = e.detail.index
    this.setData({ 
      activeTab: index 
    })
  },
  onChange(e) {
    const index = e.detail.index
    this.setData({ 
      activeTab: index 
    })
  },
  handleClick(e) {
    
  },
  onReady(){
    
  },
  transImg() { 
    //实现动画
    this.animation = wx.createAnimation({
      duration: 1000, // 动画持续时间,单位ms,默认值 400
      timingFunction: 'ease',  //ease 慢-块-慢,linear动画从头到尾的速度是相同的
      delay: 100,    //动画延迟时间
      transformOrigin: '50% 50% 0', //默认50% 50% 0 (x,y,z)设置动画的基点
      success: function(res) {
        console.log(res)
      }
    })
    // 沿着Y轴顺时针转动360°
    this.animation.rotateY(360).step();  //.step()就是一组动画
    this.setData({
      //输出动画
      animation: this.animation.export()
    })
    setTimeout(()=>{
      //因为转动了360°,需要复原
      this.animation.rotateY(0).step({duration:0})
      this.setData({
        //输出动画
        animation: this.animation.export()
      })
    }, 1100);
  },
  transImg_0(){
    this.transImg();
  },
  transImg_1(){
    this.transImg();
  },
  transImg_2(){
    this.transImg();
  },
  transImg_3(){
    this.transImg();
  }
})

​ 案例效果:

​    使用wx.createAnimation实现动画效果,如果不复原,则只会触发一次(终态-动画)。在解决无法触发动画时,看到微信官方提供了关键帧动画来代替旧的 wx.createAnimation,确实方便了很多。

// pages/blog/blog.js
Page({
  data: {
    tabs: [],
    activeTab: 0,
    animation: '',
    swiperClass: 'mybk',
  },
  onLoad() {
    const tabs = [
      {
        title: '博客系列',type: 'bkxl',
      },
      {
        title: 'java合集',type: 'java',
      },
      {
        title: '前端知识',type: 'qdzs',
      },
      {
        title: 'python合集',type: 'python',
      },
      {
        title: '小游戏',type: 'xyx',
      },
    ]
    this.setData({ tabs })
  },
  onTabClick(e) {
    const index = e.detail.index
    this.setData({ 
      activeTab: index 
    })
  },
  onChange(e) {
    const index = e.detail.index
    this.setData({ 
      activeTab: index 
    })
  },
  handleClick(e) {
    
  },
  onReady(){
    
  },
  // transImg() { 
  //   //实现动画
  //   this.animation = wx.createAnimation({
  //     duration: 1000, // 动画持续时间,单位ms,默认值 400
  //     timingFunction: 'ease',  //ease 慢-块-慢,linear动画从头到尾的速度是相同的
  //     delay: 100,    //动画延迟时间
  //     transformOrigin: '50% 50% 0', //默认50% 50% 0 (x,y,z)设置动画的基点
  //     success: function(res) {
  //       console.log(res)
  //     }
  //   })
  //   // 沿着Y轴顺时针转动360°
  //   this.animation.rotateY(360).step();  //.step()就是一组动画
  //   this.setData({
  //     //输出动画
  //     animation: this.animation.export()
  //   })
  //   setTimeout(()=>{
  //     //因为转动了360°,需要复原
  //     this.animation.rotateY(0).step({duration:0})
  //     this.setData({
  //       //输出动画
  //       animation: this.animation.export()
  //     })
  //   }, 1100);
  // },
  transImg(e){
    var id = "#"+ e.currentTarget.dataset.id;
    //获取data-id e.currentTarget.dataset.id
    //获取id e.currentTarget.id
    this.animate(id, [
      { rotateY: 0,ease: 'ease-in-out'},
      {  rotateY: 360,ease: 'ease-in-out'},
      ], 1000, function () {
        this.clearAnimation(id, {rotateY: true,ease: true }, function () {
          // console.log("清除了当前动画属性")
        })
    }.bind(this))
  }
})

案例效果:

4.3、小游戏页面实现

4.3.1、2048小游戏

1)2048.wxml

<view class="container">

  <view class="game-body">
    <loading hidden="{{hidden}}">
        加载中...
    </loading>
    <view class="heading">
      	<text class="title">2048</text>
      	<view class="scores-container">
        	<view class="score-container">{{score}}</view>
      		<view class="best-container">{{highscore}}</view>
      	</view>
    </view>

    <view class="above-game" style="margin-bottom: 30px;">
      	<text class="game-intro">你能拿到2048吗?</text>
      	<text class="restart-button" bindtap="restart">新游戏</text>
    </view>

    <view class="game-container" style="margin: auto;">
        <view class="game-message game-{{over ? (win ? 'won' : 'over') : ''}}">
          	<text class="over-msg">{{overMsg}}</text>
          	<view class="lower">
	        	<!-- <text class="keep-playing-button">继续</text> -->
          		<text class="retry-button" bindtap="restart">再试一次</text>
        	</view>
        </view>

	    <view class="grid-container" bindtouchstart="touchStart" bindtouchmove="touchMove" bindtouchend="touchEnd">
			<view wx:for="{{grids}}" wx:for-index="rowIdx" wx:for-item="row" class="grid-row">
			  	<view wx:for="{{row}}" wx:for-index="colIdx" wx:for-item="cell" class="grid-cell">
			  		<view class="tile tile-{{cell.value}}">
			  			<view wx:if="{{cell}}" class="tile-inner">
			      			{{cell.value}}
			      		</view>
			  		</view>
			  	</view>
			</view>
	    </view>
  	</view>
</view>
</view>

2)2048.js

var app = getApp();

var Grid = require('./grid.js');
var Tile = require('./tile.js');
var GameManager = require('./game_manager.js');

var config = {
    data: {
        hidden: false,

        // 游戏数据可以通过参数控制
        grids: [],
        over: false,
        win: false,
        score: 0,
        highscore: 0,
        overMsg: '游戏结束'
    },
    onLoad: function() {
        this.GameManager = new GameManager(4);

        this.setData({
            grids: this.GameManager.setup(),
            highscore: wx.getStorageSync('highscore') || 0
        });

    },
    onReady: function() {
        var that = this;

        // 页面渲染完毕隐藏loading
        that.setData({
            hidden: true
        });
    },
    onShow: function() {
        // 页面展示
    },
    onHide: function() {
        // 页面隐藏
    },
    onUnload: function() {
        // 页面关闭
    },

    // 更新视图数据
    updateView: function(data) {
        // 游戏结束
        if(data.over){
            data.overMsg = '游戏结束';
        }

        // 获胜
        if(data.win){
            data.overMsg = '恭喜';
        }

        this.setData(data);
    },

    // 重新开始
    restart: function() {
        this.updateView({
            grids: this.GameManager.restart(),
            over: false,
            won: false,
            score: 0
        });
    },

    touchStartClienX: 0,
    touchStartClientY: 0,
    touchEndClientX: 0,
    touchEndClientY: 0,
    isMultiple: false, // 多手指操作

    touchStart: function(events) {

        // 多指操作
        this.isMultiple = events.touches.length > 1;
        if (this.isMultiple) {
            return;
        }

        var touch = events.touches[0];

        this.touchStartClientX = touch.clientX;
        this.touchStartClientY = touch.clientY;

    },

    touchMove: function(events) {
        var touch = events.touches[0];
        this.touchEndClientX = touch.clientX;
        this.touchEndClientY = touch.clientY;
    },

    touchEnd: function(events) {
        if (this.isMultiple) {
            return;
        }

        var dx = this.touchEndClientX - this.touchStartClientX;
        var absDx = Math.abs(dx);
        var dy = this.touchEndClientY - this.touchStartClientY;
        var absDy = Math.abs(dy);

        if (Math.max(absDx, absDy) > 10) {
            var direction = absDx > absDy ? (dx > 0 ? 1 : 3) : (dy > 0 ? 2 : 0);

            var data = this.GameManager.move(direction) || {
                grids: this.data.grids,
                over: this.data.over,
                won: this.data.won,
                score: this.data.score
            };

            var highscore = wx.getStorageSync('highscore') || 0;
            if(data.score > highscore){
                wx.setStorageSync('highscore', data.score);
            }

            this.updateView({
                grids: data.grids,
                over: data.over,
                won: data.won,
                score: data.score,
                highscore: Math.max(highscore, data.score)
            });

        }

    }
};

Page(config);

3)2048.json

{
    "navigationBarTitleText": "2048小游戏",
    "backgroundColor":"#faf8ef",
    "backgroundTextStyle":"dark"
}

4)2048.wxss


.container {
  margin: 0;
  padding: 20px 0;
  background: #faf8ef;
  color: #776e65;
  font-family: "Helvetica Neue", Arial, sans-serif;
  font-size: 18px;
}

.heading:after {
  content: "";
  display: block;
  clear: both;
}
.title {
  font-size: 80px;
  font-weight: bold;
  margin: 0;
  display: block;
  float: left;
}

.scores-container {
  float: right;
  text-align: right;
}
.score-container, .best-container {
  position: relative;
  display: inline-block;
  background: #bbada0;
  padding: 15px 25px;
  font-size: 25px;
  height: 25px;
  line-height: 47px;
  font-weight: bold;
  border-radius: 3px;
  color: white;
  text-align: center;
  margin: 8px 0 0 8px;
}
.score-container:after, .best-container:after {
  position: absolute;
  width: 100%;
  top: 10px;
  left: 0;
  text-transform: uppercase;
  font-size: 13px;
  line-height: 13px;
  text-align: center;
  color: #eee4da;
}
.score-container .score-addition, .best-container .score-addition {
  position: absolute;
  right: 30px;
  color: red;
  font-size: 25px;
  line-height: 25px;
  font-weight: bold;
  color: rgba(119, 110, 101, 0.9);
  z-index: 100;
 
}
.score-container:after {
  content: "Score";
}
.best-container:after {
  content: "Best";
}
p {
  margin-top: 0;
  margin-bottom: 10px;
  line-height: 1.65;
}
a {
  color: #776e65;
  font-weight: bold;
  text-decoration: underline;
  cursor: pointer;
}
strong.important {
  text-transform: uppercase;
}
hr {
  border: none;
  border-bottom: 1px solid #d8d4d0;
  margin-top: 20px;
  margin-bottom: 30px;
}

.game-container {
  margin-top: 40px;
  position: relative;
  padding: 15px;
  cursor: default;
  -webkit-touch-callout: none;
  -ms-touch-callout: none;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  -ms-touch-action: none;
  touch-action: none;
  background: #bbada0;
  border-radius: 6px;
  width: 500px;
  height: 500px;
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
}
.game-container .game-message {
  /*display: none;*/
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background: rgba(238, 228, 218, 0.5);
  z-index: 100;
  text-align: center;
}
.game-container .game-message p {
  font-size: 60px;
  font-weight: bold;
  height: 60px;
  line-height: 60px;
  margin-top: 222px;
}
.game-container .game-message .lower {
  display: block;
  margin-top: 59px;
}
.game-container .game-message a {
  display: inline-block;
  background: #8f7a66;
  border-radius: 3px;
  padding: 0 20px;
  text-decoration: none;
  color: #f9f6f2;
  height: 40px;
  line-height: 42px;
  margin-left: 9px;
}
.game-container .game-message .keep-playing-button {
  display: none;
}
.game-container .game-message.game-won {
  background: rgba(237, 194, 46, 0.5);
  color: #f9f6f2;
}
.game-container .game-message.game-won .keep-playing-button {
  display: inline-block;
}
.game-container .game-message.game-won, .game-container .game-message.game-over {
  display: block;
}
.grid-container {
  position: absolute;
  z-index: 1;
}
.grid-row {
  margin-bottom: 15px;
}
.grid-row:last-child {
  margin-bottom: 0;
}
.grid-row:after {
  content: "";
  display: block;
  clear: both;
}
.grid-cell {
  width: 106.25px;
  height: 106.25px;
  margin-right: 15px;
  float: left;
  border-radius: 3px;
  background: rgba(238, 228, 218, 0.35);
}
.grid-cell:last-child {
  margin-right: 0;
}
.tile-container {
  position: absolute;
  z-index: 2;
}
.tile, .tile .tile-inner {
  width: 107px;
  height: 107px;
  line-height: 107px;
}
.tile.tile-position-1-1 {
  -webkit-transform: translate(0px, 0px);
  -moz-transform: translate(0px, 0px);
  -ms-transform: translate(0px, 0px);
  transform: translate(0px, 0px);
}
.tile.tile-position-1-2 {
  -webkit-transform: translate(0px, 121px);
  -moz-transform: translate(0px, 121px);
  -ms-transform: translate(0px, 121px);
  transform: translate(0px, 121px);
}
.tile.tile-position-1-3 {
  -webkit-transform: translate(0px, 242px);
  -moz-transform: translate(0px, 242px);
  -ms-transform: translate(0px, 242px);
  transform: translate(0px, 242px);
}
.tile.tile-position-1-4 {
  -webkit-transform: translate(0px, 363px);
  -moz-transform: translate(0px, 363px);
  -ms-transform: translate(0px, 363px);
  transform: translate(0px, 363px);
}
.tile.tile-position-2-1 {
  -webkit-transform: translate(121px, 0px);
  -moz-transform: translate(121px, 0px);
  -ms-transform: translate(121px, 0px);
  transform: translate(121px, 0px);
}
.tile.tile-position-2-2 {
  -webkit-transform: translate(121px, 121px);
  -moz-transform: translate(121px, 121px);
  -ms-transform: translate(121px, 121px);
  transform: translate(121px, 121px);
}
.tile.tile-position-2-3 {
  -webkit-transform: translate(121px, 242px);
  -moz-transform: translate(121px, 242px);
  -ms-transform: translate(121px, 242px);
  transform: translate(121px, 242px);
}
.tile.tile-position-2-4 {
  -webkit-transform: translate(121px, 363px);
  -moz-transform: translate(121px, 363px);
  -ms-transform: translate(121px, 363px);
  transform: translate(121px, 363px);
}
.tile.tile-position-3-1 {
  -webkit-transform: translate(242px, 0px);
  -moz-transform: translate(242px, 0px);
  -ms-transform: translate(242px, 0px);
  transform: translate(242px, 0px);
}
.tile.tile-position-3-2 {
  -webkit-transform: translate(242px, 121px);
  -moz-transform: translate(242px, 121px);
  -ms-transform: translate(242px, 121px);
  transform: translate(242px, 121px);
}
.tile.tile-position-3-3 {
  -webkit-transform: translate(242px, 242px);
  -moz-transform: translate(242px, 242px);
  -ms-transform: translate(242px, 242px);
  transform: translate(242px, 242px);
}
.tile.tile-position-3-4 {
  -webkit-transform: translate(242px, 363px);
  -moz-transform: translate(242px, 363px);
  -ms-transform: translate(242px, 363px);
  transform: translate(242px, 363px);
}
.tile.tile-position-4-1 {
  -webkit-transform: translate(363px, 0px);
  -moz-transform: translate(363px, 0px);
  -ms-transform: translate(363px, 0px);
  transform: translate(363px, 0px);
}
.tile.tile-position-4-2 {
  -webkit-transform: translate(363px, 121px);
  -moz-transform: translate(363px, 121px);
  -ms-transform: translate(363px, 121px);
  transform: translate(363px, 121px);
}
.tile.tile-position-4-3 {
  -webkit-transform: translate(363px, 242px);
  -moz-transform: translate(363px, 242px);
  -ms-transform: translate(363px, 242px);
  transform: translate(363px, 242px);
}
.tile.tile-position-4-4 {
  -webkit-transform: translate(363px, 363px);
  -moz-transform: translate(363px, 363px);
  -ms-transform: translate(363px, 363px);
  transform: translate(363px, 363px);
}
.tile {
  position: absolute;
  -webkit-transition: 100ms ease-in-out;
  -moz-transition: 100ms ease-in-out;
  transition: 100ms ease-in-out;
  -webkit-transition-property: -webkit-transform;
  -moz-transition-property: -moz-transform;
  transition-property: transform;
}
.tile .tile-inner {
  border-radius: 3px;
  background: #eee4da;
  text-align: center;
  font-weight: bold;
  z-index: 10;
  font-size: 55px;
}
.tile.tile-2 .tile-inner {
  background: #eee4da;
  box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0), inset 0 0 0 1px rgba(255, 255, 255, 0);
}
.tile.tile-4 .tile-inner {
  background: #ede0c8;
  box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0), inset 0 0 0 1px rgba(255, 255, 255, 0);
}
.tile.tile-8 .tile-inner {
  color: #f9f6f2;
  background: #f2b179;
}
.tile.tile-16 .tile-inner {
  color: #f9f6f2;
  background: #f59563;
}
.tile.tile-32 .tile-inner {
  color: #f9f6f2;
  background: #f67c5f;
}
.tile.tile-64 .tile-inner {
  color: #f9f6f2;
  background: #f65e3b;
}
.tile.tile-128 .tile-inner {
  color: #f9f6f2;
  background: #edcf72;
  box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.2381), inset 0 0 0 1px rgba(255, 255, 255, 0.14286);
  font-size: 45px;
}
@media screen and (max-width:520px) {
  .tile.tile-128 .tile-inner {
    font-size: 25px;
  }
}
.tile.tile-256 .tile-inner {
  color: #f9f6f2;
  background: #edcc61;
  box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.31746), inset 0 0 0 1px rgba(255, 255, 255, 0.19048);
  font-size: 45px;
}
@media screen and (max-width:520px) {
  .tile.tile-256 .tile-inner {
    font-size: 25px;
  }
}
.tile.tile-512 .tile-inner {
  color: #f9f6f2;
  background: #edc850;
  box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.39683), inset 0 0 0 1px rgba(255, 255, 255, 0.2381);
  font-size: 45px;
}
@media screen and (max-width:520px) {
  .tile.tile-512 .tile-inner {
    font-size: 25px;
  }
}
.tile.tile-1024 .tile-inner {
  color: #f9f6f2;
  background: #edc53f;
  box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.47619), inset 0 0 0 1px rgba(255, 255, 255, 0.28571);
  font-size: 35px;
}
@media screen and (max-width:520px) {
  .tile.tile-1024 .tile-inner {
    font-size: 15px;
  }
}
.tile.tile-2048 .tile-inner {
  color: #f9f6f2;
  background: #edc22e;
  box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.55556), inset 0 0 0 1px rgba(255, 255, 255, 0.33333);
  font-size: 35px;
}
@media screen and (max-width:520px) {
  .tile.tile-2048 .tile-inner {
    font-size: 15px;
  }
}
.tile.tile-super .tile-inner {
  color: #f9f6f2;
  background: #3c3a32;
  font-size: 30px;
}
@media screen and (max-width:520px) {
  .tile.tile-super .tile-inner {
    font-size: 10px;
  }
}

.tile-merged .tile-inner {
  z-index: 20;
}
.above-game:after {
  content: "";
  display: block;
  clear: both;
}
.game-intro {
  float: left;
  line-height: 42px;
  margin-bottom: 0;
}
.restart-button {
  display: inline-block;
  background: #8f7a66;
  border-radius: 3px;
  padding: 0 20px;
  text-decoration: none;
  color: #f9f6f2;
  height: 40px;
  line-height: 42px;
  display: block;
  text-align: center;
  margin-left: auto;
}
.game-explanation {
  margin-top: 50px;
}
@media screen and (max-width:520px) {
  html, body {
    font-size: 15px;
  }
  body {
    margin: 20px 0;
    padding: 0 20px;
  }
  .title {
    font-size: 27px;
    margin-top: 15px;
  }
  /*.container {
    width: 280px;
    margin: 0 auto;
  }*/
  .score-container, .best-container {
    margin-top: 0;
    padding: 15px 10px;
    min-width: 40px;
  }
  .heading {
    margin-bottom: 10px;
  }
  .game-intro {
    width: 55%;
    display: block;
    box-sizing: border-box;
    line-height: 1.65;
  }
  .restart-button {
    width: 42%;
    padding: 0;
    display: block;
    box-sizing: border-box;
    margin-top: 2px;
  }
  .game-container {
    margin-top: 17px;
    position: relative;
    padding: 10px;
    cursor: default;
    -webkit-touch-callout: none;
    -ms-touch-callout: none;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    -ms-touch-action: none;
    touch-action: none;
    background: #bbada0;
    border-radius: 6px;
    width: 280px;
    height: 280px;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
  }
  .game-container .game-message {
    display: none;
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    background: rgba(238, 228, 218, 0.5);
    z-index: 100;
    text-align: center;
   
  }
  .game-container .game-message .over-msg {
    display: block;
    font-size: 30px;
    font-weight: bold;
    height: 30px;
    line-height: 30px;
    /*margin-top: 222px;*/
    margin-top: 59px;
  }
  .game-container .game-message .lower {
    display: block;
    margin-top: 59px;
  }
  .game-container .game-message .retry-button {
    display: inline-block;
    background: #8f7a66;
    border-radius: 3px;
    padding: 0 20px;
    text-decoration: none;
    color: #f9f6f2;
    height: 40px;
    line-height: 42px;
    margin-left: 9px;
  }
  .game-container .game-message .keep-playing-button {
    display: none;
  }
  .game-container .game-message.game-won {
    background: rgba(237, 194, 46, 0.5);
    color: #f9f6f2;
  }
  .game-container .game-message.game-won .keep-playing-button {
    display: inline-block;
  }
  .game-container .game-message.game-won, .game-container .game-message.game-over {
    display: block;
  }
  .grid-container {
    position: absolute;
    z-index: 1;
  }
  .grid-row {
    margin-bottom: 10px;
  }
  .grid-row:last-child {
    margin-bottom: 0;
  }
  .grid-row:after {
    content: "";
    display: block;
    clear: both;
  }
  .grid-cell {
    width: 57.5px;
    height: 57.5px;
    margin-right: 10px;
    float: left;
    border-radius: 3px;
    background: rgba(238, 228, 218, 0.35);
  }
  .grid-cell:last-child {
    margin-right: 0;
  }
 
  .tile, .tile .tile-inner {
    width: 58px;
    height: 58px;
    line-height: 58px;
  }
  .tile.tile-position-1-1 {
    -webkit-transform: translate(0px, 0px);
    -moz-transform: translate(0px, 0px);
    -ms-transform: translate(0px, 0px);
    transform: translate(0px, 0px);
  }
  .tile.tile-position-1-2 {
    -webkit-transform: translate(0px, 67px);
    -moz-transform: translate(0px, 67px);
    -ms-transform: translate(0px, 67px);
    transform: translate(0px, 67px);
  }
  .tile.tile-position-1-3 {
    -webkit-transform: translate(0px, 135px);
    -moz-transform: translate(0px, 135px);
    -ms-transform: translate(0px, 135px);
    transform: translate(0px, 135px);
  }
  .tile.tile-position-1-4 {
    -webkit-transform: translate(0px, 202px);
    -moz-transform: translate(0px, 202px);
    -ms-transform: translate(0px, 202px);
    transform: translate(0px, 202px);
  }
  .tile.tile-position-2-1 {
    -webkit-transform: translate(67px, 0px);
    -moz-transform: translate(67px, 0px);
    -ms-transform: translate(67px, 0px);
    transform: translate(67px, 0px);
  }
  .tile.tile-position-2-2 {
    -webkit-transform: translate(67px, 67px);
    -moz-transform: translate(67px, 67px);
    -ms-transform: translate(67px, 67px);
    transform: translate(67px, 67px);
  }
  .tile.tile-position-2-3 {
    -webkit-transform: translate(67px, 135px);
    -moz-transform: translate(67px, 135px);
    -ms-transform: translate(67px, 135px);
    transform: translate(67px, 135px);
  }
  .tile.tile-position-2-4 {
    -webkit-transform: translate(67px, 202px);
    -moz-transform: translate(67px, 202px);
    -ms-transform: translate(67px, 202px);
    transform: translate(67px, 202px);
  }
  .tile.tile-position-3-1 {
    -webkit-transform: translate(135px, 0px);
    -moz-transform: translate(135px, 0px);
    -ms-transform: translate(135px, 0px);
    transform: translate(135px, 0px);
  }
  .tile.tile-position-3-2 {
    -webkit-transform: translate(135px, 67px);
    -moz-transform: translate(135px, 67px);
    -ms-transform: translate(135px, 67px);
    transform: translate(135px, 67px);
  }
  .tile.tile-position-3-3 {
    -webkit-transform: translate(135px, 135px);
    -moz-transform: translate(135px, 135px);
    -ms-transform: translate(135px, 135px);
    transform: translate(135px, 135px);
  }
  .tile.tile-position-3-4 {
    -webkit-transform: translate(135px, 202px);
    -moz-transform: translate(135px, 202px);
    -ms-transform: translate(135px, 202px);
    transform: translate(135px, 202px);
  }
  .tile.tile-position-4-1 {
    -webkit-transform: translate(202px, 0px);
    -moz-transform: translate(202px, 0px);
    -ms-transform: translate(202px, 0px);
    transform: translate(202px, 0px);
  }
  .tile.tile-position-4-2 {
    -webkit-transform: translate(202px, 67px);
    -moz-transform: translate(202px, 67px);
    -ms-transform: translate(202px, 67px);
    transform: translate(202px, 67px);
  }
  .tile.tile-position-4-3 {
    -webkit-transform: translate(202px, 135px);
    -moz-transform: translate(202px, 135px);
    -ms-transform: translate(202px, 135px);
    transform: translate(202px, 135px);
  }
  .tile.tile-position-4-4 {
    -webkit-transform: translate(202px, 202px);
    -moz-transform: translate(202px, 202px);
    -ms-transform: translate(202px, 202px);
    transform: translate(202px, 202px);
  }
  .tile .tile-inner {
    font-size: 35px;
  }
  .game-message p {
    font-size: 30px !important;
    height: 30px !important;
    line-height: 30px !important;
    margin-top: 90px !important;
  }
  .game-message .lower {
    margin-top: 30px !important;
  }
}

5)grid.js

function Grid(size) {
    this.size = size;
    this.cells = this.empty();
}

Grid.prototype = {

    // 构造一个空的矩阵[[null,..,size.length],[]]
    empty: function() {
        var cells = [];

        for (var x = 0; x < this.size; x++) {
            var row = cells[x] = [];

            for (var y = 0; y < this.size; y++) {
                row.push(null);
            }
        }

        // [[{x:0,y:0},{x:0,y:1}],[]]
        return cells;
    },

    // 在空格子中随机挑选出一个格子
    randomAvailableCell: function() {
        var cells = this.availableCells();

        // 存在可填充的格子
        if (cells.length) {
            return cells[Math.floor(Math.random() * cells.length)];
        }
    },

    // 获取可填充的格子坐标
    availableCells: function() {
        var cells = [];

        for (var i = 0; i < this.size; i++) {
            for (var j = 0; j < this.size; j++) {

                // 当前格子无内容
                if (!this.cells[i][j]) {
                    cells.push({
                        x: i,
                        y: j
                    });
                }
            }
        }

        return cells;
    },

    // 是否存在空单元格
    cellsAvailable: function() {
        return !!this.availableCells().length;
    },

    cellAvailable: function(cell) {
        return !this.cellContent(cell);
    },

    insertTile: function(tile) {
        this.cells[tile.x][tile.y] = tile;
    },

    removeTile: function(tile) {
        this.cells[tile.x][tile.y] = null;
    },

    /* 
     * 获取单元格内容
     * @param {object} cell {x:0,y:0} 单元格坐标
     */
    cellContent: function(cell) {
        if (this.withinBounds(cell)) {
            return this.cells[cell.x][cell.y] || null;
        } else {
            return null;
        }
    },

    /*
     * 空单元格,格子还未填充数字
     */
    emptyCell: function(cell) {
        return !this.cellContent(cell);
    },

    withinBounds: function(cell) {
        return cell.x >= 0 && cell.x < this.size && cell.y >= 0 && cell.y < this.size;
    },

    eachCell: function(callback) {
        for (var x = 0; x < this.size; x++) {
            for (var y = 0; y < this.size; y++) {
                callback(x, y, this.cells[x][y]);
            }
        }
    }
}

module.exports = Grid;

6)game_manager.js

var Grid = require('./grid.js');
var Tile = require('./tile.js');

function GameManager(size) {
    this.size = size;
    this.startTiles = 2;
}

GameManager.prototype = {
    setup: function() {

        this.grid = new Grid(this.size);
        this.score = 0;
        this.over = false;
        this.won = false;
        this.addStartTiles();
        return this.grid.cells;
    },

    // 初始化数据
    addStartTiles: function() {
        for (var x = 0; x < this.startTiles; x++) {
            this.addRandomTiles();
        }
    },

    // 在一个随机单元格中随机填充2或4
    addRandomTiles: function() {

        if (this.grid.cellsAvailable()) {
            var value = Math.random() < 0.9 ? 2 : 4;
            var cell = this.grid.randomAvailableCell();
            var tile = new Tile(cell, value);
            this.grid.insertTile(tile); // 插入一个单元格
        }

    },

    actuate: function() {

        return {
            grids: this.grid.cells,
            over: this.over,
            won: this.won,
            score: this.score
        }
    },

    // 偏移向量
    getVector: function(direction) {
        
        var map = {
            0: { // 上
                x: -1,
                y: 0
            },
            1: { // 右
                x: 0,
                y: 1
            },
            2: { // 下
                x: 1,
                y: 0
            },
            3: { // 左
                x: 0,
                y: -1
            }
        };
        return map[direction];
    },

    buildTraversals: function(vector) {
        var traversals = {
            x: [],
            y: []
        };

        for (var pos = 0; pos < this.size; pos++) {
            traversals.x.push(pos);
            traversals.y.push(pos);
        }

        // 为什么要加这个,看findFarthestTail
        if (vector.x === 1) {
            // 向右时
            traversals.x = traversals.x.reverse();
        }

        if (vector.y === 1) {
            // 向下
            traversals.y = traversals.y.reverse();
        }

        return traversals;
    },

    // 把当前单元格挪至下一个可放置的区域
    moveTile: function(tile, cell) {
        this.grid.cells[tile.x][tile.y] = null;
        this.grid.cells[cell.x][cell.y] = tile;
        tile.updatePosition(cell);
    },

    // 特定方向移动单元格
    move: function(direction) {
        // 0: up, 1: right, 2: down, 3: left
        var self = this;
        var vector = this.getVector(direction);
        var traversals = this.buildTraversals(vector);

        var cell;
        var tile;
        var moved = false;
        self.prepareTiles();

        traversals.x.forEach(function(x) {
            traversals.y.forEach(function(y) {
                // console.log('x:', x, 'y:', y);
                cell = {
                    x: x,
                    y: y
                };
                tile = self.grid.cellContent(cell);

                if (tile) { // 单元格有内容
                    var positions = self.findFarthestTail(cell, vector);
                    var next = self.grid.cellContent(positions.next);

                    if (next && next.value === tile.value && !next.mergedFrom) {
                        // 当前格子和其移动方向格子内容相同,需要合并
                        var merged = new Tile(positions.next, tile.value * 2); // 合并后的格子信息

                        merged.mergedFrom = [tile, next];

                        self.grid.insertTile(merged); // 把合并的盒子插入到当前格子数据中
                        self.grid.removeTile(tile); // 删除当前格子内容

                        tile.updatePosition(positions.next);

                        self.score += merged.value;
                        if (merged.value === 2048) self.won = true;
                    } else {
                        self.moveTile(tile, positions.farthest);
                    }

                    // 是否从当前位置移到当前位置
                    if (!self.positionsEqual(cell, tile)) {
                        moved = true;
                    }
                }
            });
        });

        if (moved) {
            this.addRandomTiles();

            if (!this.movesAvailable()) {
                this.over = true;
            }

            return this.actuate();
        }

        // return this.grid.cells

    },

    prepareTiles: function() {

        var tile;
        for (var x = 0; x < this.size; x++) {
            for (var y = 0; y < this.size; y++) {
                tile = this.grid.cells[x][y];
                if (tile) {
                    tile.mergedFrom = null;
                    tile.savePosition();
                }
            }
        }
    },

    positionsEqual: function(first, second) {
        return first.x === second.x && first.y === second.y;
    },

    movesAvailable: function() {
        return this.grid.cellsAvailable() || this.tileMatchesAvailable();
    },

    tileMatchesAvailable: function() {
        var self = this;

        var tile;

        for (var x = 0; x < this.size; x++) {
            for (var y = 0; y < this.size; y++) {
                tile = this.grid.cellContent({ x: x, y: y });

                if (tile) {
                    for (var direction = 0; direction < 4; direction++) {
                        var vector = self.getVector(direction);
                        var cell = { x: x + vector.x, y: y + vector.y };

                        var other = self.grid.cellContent(cell);

                        if (other && other.value === tile.value) {
                            return true;
                        }
                    }
                }
            }
        }

        return false;
    },

    // 找到当前偏移方向存在最远的空单元格
    // 如:向右偏移,那么返回当前行最靠右的空单元格及其右侧距离其最远的一个格子,向下一样
    findFarthestTail: function(cell, vector) {
        var previous;

        // 当前单元格在范围内且存在可用单元格
        do {
            previous = cell;
            cell = {
                x: previous.x + vector.x,
                y: previous.y + vector.y
            };
        }
        while (this.grid.withinBounds(cell) && this.grid.emptyCell(cell));

        return {
            farthest: previous,
            next: cell
        }
    },

    // 重新开始
    restart: function() {
        return this.setup();
    }
}

module.exports = GameManager;

7)tile.js

function Tile(position, value) {
    this.x = position.x;
    this.y = position.y;
    this.value = value || 2;

    this.previousPosition = null;
    this.mergedFrom = null;
}

Tile.prototype = {

    // 记录格子上次的位置
    savePosition: function() {
        this.previousPosition = { 
        	x: this.x, 
        	y: this.y 
        };
    },

    // 更新当前格子的位置
    updatePosition: function(position) {
        this.x = position.x;
        this.y = position.y;
    },

    serialize: function() {
        return {
            position: {
                x: this.x,
                y: this.y
            },
            value: this.value
        };
    }
}

module.exports = Tile;

8)blog.wxml

 <!-- 小游戏页面 -->
       <block wx:if="{{item.type === 'xyx'}}">
            <view wx:for-items="{{gameList}}"  wx:key="*this" class="usermotto">
              <button class="game-2048" type="primary" disabled="{{disabled}}" bindtap="start{{item}}"> {{item}} </button>
            </view>
        </block>

案例效果:

4.4、关于我页面实现

   ​ 因非认证的个人小程序的原因,关于我的页面也只能草草了事。

  • 13
    点赞
  • 69
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

穆瑾轩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值