V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
bili
V2EX  ›  微信

微信小程序实战练习(仿五洲到家微信版)

  •  
  •   bili · 2017-08-28 17:07:40 +08:00 · 2308 次点击
    这是一个创建于 2648 天前的主题,其中的信息可能已经有所发展或是发生改变。

    微信小程序实战

    github 地址(欢迎 star ): https://github.com/xiaobinwu/dj

    版本:0.17.172600 ( wxss 背景图设置会不成功)

    图片描述图片描述

    目录结构

    图片描述

    • css => 放置公用 wxss,目前只有一个 font.wcss
    • image => 静态资源目录
    • lib => 第三方库(如:qqmap-wx-jssdk.min.js )
    • pages => 小程序页面(包括四个文件,.wxml/.wxss/.js/.json )
    • template => 抽离出来的 template,具有复用性
    • utils => 工具类
    • app.js/app.json/app.wxss => 配置

    踩过的坑

    1. promise 封装

    官方 request 代码:

    wx.request({
      url: 'test.php', //仅为示例,并非真实的接口地址
      data: {
         x: '' ,
         y: ''
      },
      header: {
          'content-type': 'application/json'
      },
      success: function(res) {
        console.log(res.data)
      }
    })
    

    但是有很多场景需要 promise 化的,所以使用第三方 promise 库( es6-promise.min.js ),对 request 进行了一层包装:

    /* utils/util.js */
    /* api 接口 promise 柯里化*/
    var Promise = require('../lib/es6-promise.min.js'); 
    function wxPromisify(fn, scope) {  
      return function (obj = {}) {    
        return new Promise((resolve, reject) => {      
          obj.success = function (res) {        
            resolve(res);      
          }      
          obj.fail = function (res) {        
            reject(res);      
          }
          if(scope){
            //改变 this 指向
            var newFn = fn.bind(scope);
            newFn(obj);
          }else{
            fn(obj);
          }      
        })  
      }
    }
    /* request 封装*/
    var wxrequest = wxPromisify(wx.request);
    function wxRequest(options, tokenNotRequired){
        return wxrequest(options).then(res => {
          var data = res.data;
          if(data.status === 404404) {
              if(tokenNotRequired){
                    delete options.headers;
                    return wxRequest(options);
              }else{
                    return updateToken().then(token => {
                        return wxRequest(object.assignIn(options, {
                            headers: { 'X-Auth-Token': token }
                        }));
                    });
              }
          }else {
              return Promise.resolve(data);
          }
        }).catch(err => {
          return Promise.reject(err);
        });
    }
    

    2. Javascript 作用域问题

    由于小程序默认给的微信地图 api 有些需求达不到要求,于是使用第三方库( qqmap-wx-jssdk.min.js ,这是绝配),这样定位功能也比较好做,以及后续要做的地址管理模块也比较好下手,但是有个问题,对微信地图 jdk 接口进行 promise 化后,使用过程会报错,导致定位失败,所以需要改变其执行作用,于是对 wxPromisify()方法做了些改造,重新绑定作用域至 qqmapsdk,调用如下:

    //address.js
    // 引入 SDK 核心类
    var QQMapWX = require('../lib/qqmap-wx-jssdk.min.js');
    // 实例化 API 核心类(需要配置安全域名 https://apis.map.qq.com)
    var qqmapsdk = new QQMapWX({
        key: 'xxxxx' //需要到腾地图上申请 key
    });
    
    ...
    ...
    
    // 请求用户授权定位
    //逆地址解析
    var ReverseGeocoder = util.wxPromisify(qqmapsdk.reverseGeocoder, qqmapsdk); //需改变作用域
    
    

    对于小程序是需要配置对应的安全域名的,这样才能执行 request

    3. 如何衍生出组件模板

    模板页( template )没有天生配对 js,但是也可以实现,实现面向对象的思想,对模板所需要的 js 进行一层类的封装,保证构造函数需要接受父页面的上下文对象,然后可以把声明好的类方法绑定到父页面上面去,对于模板页 js 方法,以_FUN()方式命名。下面是为图片懒加载优化而做的 swiper 模板组件,可以参考一下。

    /**
     * 图片预加载组件
     *
     * @author xiaobin_wu
     * template/silder/silder.js
     */
    class Slider {
        constructor(pageContext, options = { picList: [], showArr:[] }){
            this.page = pageContext; //获取页面上下文
            this.page.data.slider = {
                picList: options.picList,
                showArr: options.showArr
            }; //初始化 data
            this.page._sliderChange = this._sliderChange.bind(this);
        }
        //监听滑动事件,实现图片懒加载
        _sliderChange(e){
            if(this.page.data.slider.showArr){
                let showArr = this.page.data.slider.showArr;
                for(let i = 0; i < showArr.length; i++){
                    if(i === e.detail.current){
                        showArr[i] = true;
                    }
                }
                this.page.setData({
                    'slider.showArr': showArr
                });
            }
        }
        initData(imgs){
            const arr = new Array(imgs.length).fill(false);
            this.page.setData({
                'slider.picList': imgs,
                'slider.showArr': arr.fill(true, 0 , 1)
            });
        }
    }
    module.exports = Slider
    
    

    以类形式 module.exports 出去,Page 页面,以var Slider = require('../../template/slider/slider.js');形式引入,然后new操作,模板 wxml 也参考template/silder/silder.wxml,也可以对应写 wxss,这样做模板页复用性高,类似组件的模式。

    4. scroll-view 使用 scroll-x 失效问题

    刚开始使用 scroll-view,scroll-x 一直失效,不能水平 scroll,折腾了好多时间,结果这样就成了,大概如下结构( home.wxml ):

    
    <scroll-view  scroll-x="{{true}}" scroll-left="{{scrollLeft}}"  class="scroll-bar" style="width:100%;" >
        <view style="width: {{idxData.navbar.length * 168}}rpx">
            <view wx:for="{{idxData.navbar}}" wx:for-item="cate" class="cate-item {{index == currentIndex ? 'active' : ''}}" data-id="{{cate.nav_id}}" data-index="{{index}}" bindtap="cateClick">{{cate.nav_name}}</view>
        </view>
    </scroll-view>
    
    

    忽略其他乱起八糟的代码,主要是这个<view style="width: {{idxData.navbar.length * 168}}rpx">,需要保证 scroll-view 下面的 view 的 width 必须要大于 100%,充满整个 scroll-view

    4. swiper 高度无法自动撑开,暂时不支持

    图片描述 于是对于红线部分的产品分类 swiper,就只能手动计算 swiper 高度,来实现 swiper 的效果,但是由于对应每个 swiper-item 还会有个下拉加载,所以产品数目会一直变化,所以计算起来相当于耗性能,希望官方能尽快让 swiper 高度允许自动撑开

    5. template 模板

    template 模板,对象传递方式=>data={{a: x1,b: x2}},x1、x2 对应 data 绑定的变量

    6. setData 设置动态数据

    可能你会遇到这种情况(设置动态数据):

    this.setData({
        'array[0]': 1
    });
    /*
        上面这样设置是没问题的,但是是动态的,那该怎么办?这样...
    
     */
    this.setData({
        'array['+ index +']': 1
    });
    /*
        很遗憾,无法怎么做
    
    */
    

    解决办法,声明中间量,如下:

    /* utils/util.js */
    //动态 setData
    function dynamicSetData(field, index, value, suffix, type='object'){
        var param = {};
        var string = field + '[' + index + ']' + (typeof suffix !== 'undefined' ?  type === 'object' ? '.' + suffix  : '[' + suffix + ']' : '');
        param[string] = value;
        return param;
    }
    

    这样最后就可以这样,this.setData(util.dynamicSetData('firstLoadDataFlag', index, true));,即可用于对象的改变,也可以用于数组的改变。

    7. 设置顶层标签 Page 的样式,处理安卓机的背景色问题

    8 px 与 rpx 之间转化

    对于小程序中,也有一些组件需要传递变量单位为 px 的,如果这个变量是需要计算出来的,但是我们使用的确是 rpx 单位,那么他们之间的转化比例是有必要知道的

    /* utils/util.js */
    //获取 px 与 rpx 之间的比列
    function getRpx(){
        var winWidth = wx.getSystemInfoSync().windowWidth;
        return 750/winWidth;
    }
    

    8. image 问题

    image 组件,其实对于 src 图片路径,是以背景图展示的,并不是真的类似 img,auto 是不生效的。

    9. wx.navigateBack 返回无法传参数通知

    wx.navigateBack 返回通知上一页执行指定函数的作用,可以使用 getCurrentPages()来获取上一页 page 对象,事先执行,如下:

    /* pages/order-detail/order-detail.js */
    //返回执行上一个页面的函数,good
    navigateBackFun: function(){
      var pages = getCurrentPages();
      var prevPage = pages[pages.length - 2];
      if(prevPage.__route__.indexOf("pages/order/order") != -1) {
            prevPage.actionCallback(this.data.btnAction,this.data.page);
      }
    }
    

    10. class 可以多组操作

     <view class="status-item {{index == orderData.progress.last_index? 'active' : ''}} {{index === orderData.progress.info.length - 1 ? 'last-status-item' : ''}}"></view>
    

    11. 对于字体文件的使用

    对于下面的字体文件的引用会导致报错,微信小程序似乎不支持怎么使用

    @font-face {
      font-family: 'Glyphicons Halflings';
    
      src: url('/assets/fonts/glyphicons-halflings-regular.eot');
      src: url('/assets/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('/assets/fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('/assets/fonts/glyphicons-halflings-regular.woff') format('woff'), url('/assets/fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('/assets/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
    }
    

    解决办法,将 ttf 文件拿出,转化成 base64,以 wxss 引入。base64 转化

    12. 购物车功能

    对于购车功能也是相当折腾的,通过在 app.js 定义全局变量:

    cartData:{
        list:[],
        totalCount:1,
        totalPrice:0,
    
        // 起送价
        floorPrice:0,
    
        // 总价达到此价免配送费
        freeShipPrice:0,
    
        // 运费
        deliveryFee:0,
    
        storeId:0,
        storeName:''
    }
    

    然后每次加减产品,清空购物车来操作 cartData 的变化,list 存储购物车产品数据,在首页和产品详情页,可以来获取购物车的数据,当然也会把购物车数据的商品 id 和门店 id 存储到 Storage,可以用来异步更新最新的购物车数据,在首页和产品详情页的来回切换,对于购物车需要时刻去检查,映射到对应分类的 swiper 产品的加减变化,这里有没有像 vue 中 vuex 的状态管理能对数据集中管理,(对于 vuex 的使用 点击),导致监听变化变得很复杂,有把加减部件 cart-ctrl 和购物车 cart 提取成 template 模板组件,结果处理起来,这里一万个省略号,很悲催!

    13. require 原理

    微信小程序类似浏览器一样,借助一个 HTML 页面来引用加载所有的 JS 文件,但是并不会马上去执行,代理服务部分代码又怎么两个全局函数 define 和 require,类似 amd,使用 define 函数对请求回来的 js 文件内容进行包装,但是不去执行,然后可以使用 require 函数按需递归式进行初始化,这个时候 js 就会执行了,并且只执行一次,那么对于 Page 页面为什么 js 能一开始不使用 require 函数就能加载呢?因为默认 page 页面 js 文件,会自动添加上 require 自己,加载后立即初始化。这样是不需要担心多次 require 会执行多次 js 的。 这篇文章写得很详细 微信小程序的 require 机制浅析,顺便贴一下几段代码(帮助理解):

    代理服务部分代码:
    (projectManager.js)
    function getScripts(projInfo, callback) {
      ...
        fs.readFile(fname, 'utf8', function(err, scripts) {
            ....
            scripts = 'define("' + moduleName + '", function(require, module, exports, ' + noBrowserStr +
            '){ ' + scripts + '\n});',
            needRequire && (scripts += 'require("' + moduleName + '")'), //page 页面 js 文件,会添加上 require 自己,加载后立即初始化。
            .....
            callback(null, scripts) //scripts 串内容作为 HTTP GET 的返回
    
        var 
        ......
        moduleList = {}; 
        define = function(moduleName, factory) {  //define 是全局函数,每个 JS 文件都默认会调用. 
            moduleList[moduleName] = { status: status1, factory: factory }
        };
    
    ....
        require = function(moduleName) {       
            ....
            var module = moduleList[moduleName]; //define 函数调用时为 moduleList 赋的值
            .....
            if (module.status === status1) {  
                //如果未初始化,则初始化
                var factory = module.factory,  //这个 factory 就是这个 JS 文件的脚本.
                obj = { exports: {} }, u = void 0;
                factory && (u = factory(o(moduleName), obj, obj.exports)), module.exports = obj.exports || u, module.status = status2
            }
            return module.exports
        }
    
    2 条回复    2017-08-28 18:09:56 +08:00
    q409195961
        1
    q409195961  
       2017-08-28 17:38:17 +08:00
    Star + Fork
    yangg
        2
    yangg  
       2017-08-28 18:09:56 +08:00
    setData({
    ["a" + i]: "val"
    }) // 不需要变量
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3785 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 10:37 · PVG 18:37 · LAX 02:37 · JFK 05:37
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.