vuex

关于vuex到底是什么,看到眼热就去查了下资料,然后扯出来一堆flux、redux、state、state之类的概念,以及大型工程必要性之类的,我只想说,整这么复杂干什么?走远了..

什么是vuex,作用又是什么?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态 。这里的关键在于 集中式存储管理 。这意味着本来需要共享状态的更新是需要组件之间通讯的,而现在有了vuex,就组件就都和store通讯了

简言之就是,当我们的应用遇到多个组件共享状态时解决这一问题的一个状态管理中间件

应用场景:

  • 多个组件依赖于同一状态。传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。
  • 来自不同组件的行为需要变更同一状态。我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。

    举例说明

    当我们需要制作一个加一,减一操作按钮的时候,用下面两个demo做一个演示

    未使用vuex

  • 单纯依赖于vue.js

    <div id="app">
          <p>{{count}}
                <button @click="inc">+</button>
                <button @click="dec">-</button>
          </p>
      </div>
      <script>
      new Vue({
        el:'#app',
        data () {
          return {
            count: 0
          }
        },
        methods: {
          inc () {
            this.count++
          },
          dec () {
            this.count--
          }
        }
      })
      </script>
    

    整个代码结构非常清晰,代码是代码,数据是数据,代码就是放在methods数组内的两个函数inc、dec,被指令@click关联到button上。而data内返回一个属性count,此属性通过绑定到标签p内。从而可以动态的改为count的值。

    使用vuex

  • 依赖vue.js,也使用了vuex技术

    <script src="https://unpkg.com/vuex@next"></script>
    <div id="app">
        <p>{{count}}
            <button @click="inc">+</button>
            <button @click="dec">-</button>
        </p>
    </div>
    <script>
        const store = new Vuex.Store({
            state: {
              count: 0
            },
            mutations: {
                inc: state => state.count++,
              dec: state => state.count--
            }
        })
          const app = new Vue({
            el: '#app',
            computed: {
              count () {
                 return store.state.count
              }
            },
            methods: {
              inc () {
                    store.commit('inc') //执行store.inc放大
              },
              dec () {
                  store.commit('dec')
              }
            }
        })
    </script>
    

    我们先看到有哪些重要的变化:

  1. 新的代码添加了对vuex@next脚本的依赖。这是当然的,因为你需要使用vuex的技术,当然需要引用它
  2. methods数组还是这两个方法,这和demo1是一样的;但是方法内的计算逻辑,不再是在函数内进行,而是提交给store对象!这是一个新的对象!
  3. count数据也不再是一个data函数返回的对象的属性;而是通过计算字段来返回,并且在计算字段内的代码也不是自己算的,而是转发给store对象
  4. store对象是Vuex.Store的实例。在store内有分为state对象和mutations对象,其中的state放置 状态 ,mutations则是一个会引发状态改变的所有方法。正如我们看到的,目前的state对象,其中的状态就只有一个count。而mutations有两个成员,它们参数为state,在函数体内对state内的count成员做加1和减1的操作。

总结:就是说,之前在vue实例内做的操作和数据的计算现在都不再自己做了,而是交由对象store来做了。

用了vuex感觉更复杂了?

你的感觉没错,这就是为什么官网再次会提到Vuex构建大型应用的价值。 如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex 。

OVER

扒一扒JQuery的几个技巧

1.jQuery方法$()实际上是拥有两个参数的

Create an element. The second argument is an object with jQuery methods to be called
var div = $('<div>',{
    "class": "bigBlue",
    "css": {
        "background-color":"purple",
        "width" : 20,
        "height": 20,
    },
    "animate" : {   // You can use any jQuery method as a property!
        "width": 200,
        "height":50
    }
});

div.appendTo('#eleID');

2.使用js给多个元素添加样式时更好的做法是创建一个style元素。

var style = $('<style>');
// Try commenting out this line, or change the color:
style.text('.div{ color:red;}');
style.prependTo('body');

3.阻止contextmenu默认事件。(右键菜单)

$(document).on("contextmenu",function(e){
    e.preventDefault();
});

4.监听不存在的元素上的事件。因为on方法可以传递一个元素的子元素选择器作为参数。

var list = $('#testList');
// Binding an event on the list, but listening for events on the li items:
list.on('click','li',function(){
    $(this).remove();
});
// This allows us to create li elements at a later time,
// while keeping the functionality(功能) in the event listener
list.append('<li>New item (click me!)</li>');

5.使用trigger模拟触发一个click等事件或其他自定义事件。

// Just a regular event listener:
press.on('click',function(e, how){
    how = how || '';
    alert('The buton was clicked ' + how + '!');
});

// Trigger the click event
press.trigger('click');

// Trigger it with an argument   也可以自定义事件
press.trigger('click',['fast']);

6.传递参数到e.data执行供执行的函数使用

//参数只会之心过一次,所以下面的random不管自行几次都是相同的结果
$(document).on('click', Math.round(Math.random()*20), function(e){
    // This will print the same number over and over again,
    // as the random number above is generated only once:
    console.log('Random number: ' + e.data,e.target);
});

7.Using event delegation 事件代理

$('#holder').on('click', '#clear', function(){
    clear();
});

8.绑定多个事件

clear.on('mouseup click',function(){
    //...
})

clear.on({
    'mousedown':function(){
       // ...
    },
    'mouseup':function(){
        // ...
    }
});

9.阻止默认事件

更快地阻止默认事件行为。
$('#goToGoogle').click(false);
函数handler的返回值为false,则表示阻止元素的默认事件行为,并停止事件在DOM树中冒泡。例如,\链接的click事件的处理函数返回false,可以阻止链接的默认URL跳转行为。
$('#goToGoogle').click(function(){
    return false;
});

10.平行的运行多个Ajax请求。

当我们需要发送多个Ajax请求是,相反于等待一个发送结束再发送下一个,我们可以平行地发送来加速Ajax请求发送。
// The trick is in the $.when() function:
$.when($.get('assets/misc/1.json'), $.get('assets/misc/2.json')).then(function(r1, r2){
    log(r1[0].message + " " + r2[0].message);
});

11.jQuery(使用ajax)提供了一个速记的方法来快速下载内容并添加在一个元素中。

<p class="content"></p> <p class="content"></p>
var contentDivs = $('.content');
// Fetch the contents of a text file:
contentDivs.eq(0).load('1.txt');
// Fetch the contents of a HTML file, and display a specific element:
contentDivs.eq(1).load('1.html #header');

12.JQuery扩展

jQuery.prototype = jQuery.fn = jQuery.fn.init.prototype
$.fn.abs=function(){
    console.log('abs')
}
$(document).abs();

OVER

开启一个简单的CDN加速

cdn加速是通过一个服务器集群对网站内容建立缓存和镜像,并且通过对网页静态资源的压缩来加快网站的最终访问速度,但是对于大部分的个人网站或者普通企业网站来说,自己建立cdn加速的服务器集群是一笔较高的费用,而且需要专业的技术。这里说一种最简单的网站cdn加速方法。

备注:CDN只是在你的服务器给出数据之后,进行加快传输。举个例子,你的服务器响应是10秒,网络传输占2秒,整体是12秒响应。而CDN可以将你的网络传输2秒加速到1秒,但是你的服务器吐出查询数据还是10秒,整体时间是11秒。

方法/步骤

1.登陆百度云加速网站:su.baidu.com 也可以直接百度搜索“百度云加速”找到官网地址。

2.然后用自己的百度账号登陆百度云加速,登陆后在你的用户名左侧能看到一个“我的网站”文字链接,点击这个链接。

3.进入我的网站管理界面后,点击“添加网站”按钮。

4.在添加网站页面,输入你的网址(不带http://和www),然后点击“选择接入方式”的文字链接,建议选择“NS方式”这种方式可以同时免费使用百度提供的dns服务器。最后点击“下一步”

5.如果你的网站解析正常,百度云加速会自动检测你的域名解析IP并帮你添加好,我们只需要按照界面上的提示,把域名原本的dns服务器修改成百度云加速提供的dns服务器地址即可。

6.登陆你的域名控制面板,修改域名的DNS设置,不同的域名注册商提供的域名管理面板可能不太一样,但是都有dns设置这一项,如果没有dns设置这一项,你就进入域名的“解析管理”找到ns解析项。

7.把dns解析的值替换成百度云加速提供的dns服务器地址即可。修改成功后,要等待2-24小时才能全面生效。

8.等待dns设置生效后,你的网站就会出现在“我的网站”列表中,并且右侧图标编程绿色的对勾。

9.生效后,我们就可以点击“配置选项”来配置网站的CDN加速 和 网站安全防护了,让你的网站打开速度更快,网站更安全,赶紧去体验吧!

注意事项

  • 网站域名备案后才能使用百度云加速的国内CDN加速节点。
  • 在dns修改并等待生效的时间内,不会影响网站的正常访问。
转自百度经验

jQuery对象之deferred

什么是deferred对象?简单说,deferred对象就是jQuery的回调函数解决方案。在英语中,defer的意思是”延迟”,所以deferred对象的含义就是”延迟”到未来某个点再执行。从而优雅的编写回调函数.

1,ajax操作写法

非链式

$.ajax({
   type:'get'
   url: "test.html",
   success: function(){
          alert("哈哈,成功了!");
      },
      error:function(){
          alert("出错啦!");
      }
});

链式

高于1.5.0版本的jq,ajax()返回的是deferred对象,可以直接进行链式操作。

$.ajax("test.html")
.done(function(){ alert("哈哈,成功了!"); })
.fail(function(){ alert("出错啦!"); });

可以看到,done()相当于success方法,fail()相当于error方法。采用链式写法以后,代码的可读性大大提高。

如果ajax操作成功后,除了原来的回调函数,我还想再运行一个回调函数,怎么办?直接在结尾追加就好了,回调函数可以添加任意多个,它们按照添加顺序执行。像酱紫:

$.ajax("test.html")
.done(function(){ alert("哈哈,成功了!");} )
.fail(function(){ alert("出错啦!"); } )
.done(function(){ alert("第二个回调函数!");} );
.done(function(){ alert("第三个回调函数!");} );

2,为多个操作指定回调函数

$.when($.ajax("test1.html"), $.ajax("test2.html"))
.done(function(){ alert("哈哈,成功了!"); })
.fail(function(){ alert("出错啦!"); });

这段代码的意思是,先执行两个操作$.ajax("test1.html")$.ajax("test2.html"),如果都成功了,就运行done()指定的回调函数;如果有一个失败或都失败了,就执行fail()指定的回调函数。

3,回调函数接口实现

deferred对象的最大优点,就是它把这一套回调函数接口,从ajax操作扩展到了所有操作。也就是说,任何一个操作—-不管是ajax操作还是本地操作,也不管是异步操作还是同步操作—-都可以使用deferred对象的各种方法,指定回调函数。

假定有一个很耗时的操作wait:

function wait(){
    var tasks = function(){
        alert("执行完毕!");
    };
    setTimeout(tasks,5000);
};

为它指定回调函数: 错误❌的写法如下

$.when(wait())
.done(function(){ alert("哈哈,成功了!"); })
.fail(function(){ alert("出错啦!"); });

上述写法,done()方法会立即执行,起不到回调函数的作用。原因在于$.when()的参数只能是deferred对象,所以必须对wait()进行改写:

var dtd = $.Deferred(); // 新建一个deferred对象
var wait = function(){
var tasks = function(){
alert(“执行完毕!”);
dtd.resolve(); // 改变deferred对象的执行状态为已完成
};
setTimeout(tasks,5000);
return dtd.promise();
};
现在,wait()函数返回的是deferred对象,这就可以加上链式操作了。

$.when(wait())
.done(function(){ alert(“哈哈,成功了!”); })
.fail(function(){ alert(“出错啦!”); });

jQuery规定,deferred对象有三种执行状态—-未完成,已完成和已失败。如果执行状态是”已完成”(resolved),deferred对象立刻调用done()方法指定的回调函数;如果执行状态是”已失败”,调用fail()方法指定的回调函数;如果执行状态是”未完成”,则继续等待.
deferred.reject()方法,作用是将dtd对象的执行状态从”未完成”改为”已失败”,从而触发fail()方法。
jQuery提供了deferred.promise()方法。它的作用是,在原来的deferred对象上返回另一个deferred对象,后者只开放与改变执行状态无关的方法(比如done()方法和fail()方法),屏蔽与改变执行状态有关的方法(比如resolve()方法和reject()方法),从而使得执行状态不能被外界改变造成干扰。

**********************************

上面说些原理,下面是2种经常使用的回调接口方法

**********************************

1,将deferred对象变成wait()函数的内部对象。

function wait(){
    var dtd = $.Deferred(); //在函数内部,新建一个Deferred对象
    var tasks = function(){
        alert("执行完毕!");
        dtd.resolve(); // 改变Deferred对象的执行状态
    };
    setTimeout(tasks,5000);
    return dtd.promise(); // 返回promise对象
};
$.when(wait())
.done(function(){ alert("哈哈,成功了!"); })
.fail(function(){ alert("出错啦!"); });

2,使用deferred对象的建构函数$.Deferred()

jQuery规定,$.Deferred()可以接受一个函数名(注意,是函数名)--当然也是立即执行该函数作为参数,$.Deferred()所生成的deferred对象将作为这个函数的默认参数

function wait(dtd){
var tasks = function(){
alert(“执行完毕!”);
dtd.resolve(); // 改变Deferred对象的执行状态
};
setTimeout(tasks,5000);
return dtd.promise(); // 返回promise对象
};
$.Deferred(wait)
.done(function(){ alert(“哈哈,成功了!”); })
.fail(function(){ alert(“出错啦!”); });

小结:deferred对象的方法

  1. $.Deferred() 生成一个deferred对象。
  2. deferred.done() 指定操作成功时的回调函数
  3. deferred.fail() 指定操作失败时的回调函数
  4. deferred.promise() 没有参数时,返回一个新的deferred对象,该对象的运行状态无法被改变;接受参数时,作用为在参数对象上部署deferred接口。
  5. deferred.resolve() 手动改变deferred对象的运行状态为”已完成”,从而立即触发done()方法。
  6. deferred.reject() 这个方法与deferred.resolve()正好相反,调用后将deferred对象的运行状态变为”已失败”,从而立即触发fail()方法。
  7. $.when() 为多个操作指定回调函数。

    除了这些方法以外,deferred对象还有二个重要方法,上面的教程中没有涉及到。

  8. deferred.then()有时为了省事,可以把done()和fail()合在一起写,这就是then()方法。

    $.when($.ajax( "/main.php" ))

    .then(successFunc, failureFunc );

    如果then()有两个参数,那么第一个参数是done()方法的回调函数,第二个参数是fail()方法的回调方法。如果then()只有一个参数,那么等同于done()。

  9. deferred.always()
    这个方法也是用来指定回调函数的,它的作用是,不管调用的是deferred.resolve()还是deferred.reject(),最后总是执行。

    $.ajax( "test.html" )

    .always( function() { alert("已执行!");} );

微信小程序

微信小程序实战,从入门到弃坑(转)

原文链接

最近集中开发了两款微信小程序,分别是好奇心日历(每天一条辞典+一个小投票)和好奇心日报(轻量版),直接上图:

本文将结合具体的实战经验,主要介绍微信小程序的基础知识、开发中遇到的难点、项目的架构设计、最佳实践以及踩过的坑。文章内容较多,如果想看架构设计和躲坑技巧,请直接浏览后面的正文,简书没有目录,也挺伤感的。

值得再次声明的是:微信小程序的内容部分是hybrid模式,并非原生,所以性能并不好,绑定的tap事件也有明显的延迟。

如上图每一个由边框围起来的部分,都是一个最小粒度的原生view可以看出,整个微信小程序的内容部分,就是一个原生view。

小程序有哪些基础知识?

一个完整的微信小程序是由一个App实例和多个Page实例构成,其中App实例表示该小程序应用,多个Page表示该小程序的多个页面。
此外,微信小程序并没有提供自定义组件的方式,这就导致微信小程序在开发较复杂应用时,可能会比较艰难。

微信小程序本身很简单,和一个模板语言的难度几乎相当,翻翻官方教程就可以开始动手搞。

我也建议大家先动起来,然后再细致啃啃官方文档。由于微信官方文档仍在不断大幅更新中,所以务必查看最新官方文档。

微信小程序的基础知识主要分为以下几个部分:

  • 两种配置文件 && 两个核心函数
  • WXML模板语法,页面渲染
  • 页面间的跳转
  • 交互事件
  • 官方组件和官方API

两种配置文件 && 两个核心函数

app.json应用的全局配置文件

app.json是针对微信小程序的全局配置,主要包含以下几个配置:

  • pages:页面路径的数组,表示小程序要加载的所有页面,其中数组第一项代表小程序的初始页面。
  • window:微信原生功能,定制化不强。可设置小程序的状态栏、导航条、标题以及窗口背景色。
  • tabBar:微信原生功能,定制化不强。适用于常规的Tab应用,Tab栏可置于顶部或底部;tabBar是一个数组,仅支持2-5个tab。
  • networkTimeout:配置小程序网络请求的超时时间。
  • debug:调试模式开关,开发模式下建议开启,正式发布别忘了关闭。


page.json 页面的全局配置文件

除了上面提到的全局配置,每个页面还可以单独配置page.json,page.json会覆盖app.json中的配置,并只对当前页面生效。

page.json只能对window配置,有两个比较有用的配置项分别是:

  • enablePullDownRefresh:是否开启下拉刷新
  • disableScroll:只能在page.json配置,禁止页面上下滚动,猜测可以实现完美滑屏滑动(未验证)

App() 小程序注册入口,全局唯一

App()用来注册一个小程序,全局只有一个,全局的数据也可以放到这里面来操作。

// 注册微信小程序,全局只有一个
let appConfig = {
    // 小程序生命周期的各个阶段
    onLaunch: function(){},
    onShow: function(){},
    onHide: function(){},
    onError: function(){},

    // 自定义函数或者属性
    ...
};
App(appConfig);

// 在别的地方可以获取这个全局唯一的小程序实例
const app = getApp();

小程序并没有提供销毁的方式,所以只有当小程序进入后台一定时间、或者系统资源占用过高的时候,才会被真正的销毁。

Page() 页面注册入口

Page()用来注册一个页面,维护该页面的生命周期以及数据。

// 注册微信小程序,全局只有一个
let pageConfig = {
    data: {},
    // 页面生命周期的各个阶段
    onLoad: function(){},
    onShow: function(){},
    onReady: function(){},
    onHide: function(){},
    onUnload: function(){},
    onPullDownRefresh: function(){},
    onReachBottom: function(){},
    onShareAppMessage: function(){},

    // 自定义函数或者属性
    ...
};
Page(pageConfig);

// 获取页面堆栈,表示历史访问过的页面,最后一个元素为当前页面
const page = getCurrentPages();

关于各个生命周期的细节以及流程,参考下图,可以细细品味:

app.jsonpage.json 维护了应用和页面的配置属性。App()Page() 维护了应用和页面的各个生命周期以及数据。

那么,APPPage 如何将数据传递到页面呢?页面又是如何渲染呢?

WXML模板语法,页面渲染

小程序虽然是hybrid模式,但并不使用HTML渲染,而是全部通过自定义标签来渲染页面。这样做的好处我不清楚,但问题却不少:不能跨浏览器、富文本解析困难,iframe视频不支持,没办法外链跳转。
和所有的模板语言一样,WXML支持数据绑定、条件渲染、循环、模块化等功能。

数据绑定

在 WXML 中可以使用双大括号Page 的变量或者表达式包裹起来,实现数据绑定,举个例子:

// 将message的值渲染到view中
<view> {{ message }} </view>

// 将id的值渲染到view的id属性中
<view id="item-{{id}}"> </view>

// 根据isSelected的值,输出不同的class
<view class="{{isSelected ? 'selected' : ''}}"> HelloWorld </view>

// 结合template,可以传入更复杂的数据
<template is="objectCombine" data="{{...article, categoty, tag: '埃隆马斯克'}}"></template>

条件渲染

循环渲染,适合遍历数据输出多段 WXML,举个例子:

// wx:for 表示需要遍历的数据
// wx:key 使用唯一的字段来标识,有利于提升性能
// wx:for-index 表示数组的下标
// wx:for-item 表示数组的元素
<view wx:for="{{[{id:1, message: 'HelloWorld1'}, {id:2, message: 	'HelloWorld2'}]}}" 
    wx:key="id" 
    wx:for-index="idx" 
    wx:for-item="item">
    {{idx}}: {{item.message}}
</view>

wx:key 有利于提升重新渲染时的效率,建议添加

模块化

WXML的模块化,可以让我们复用一些wxml片段,还挺重要的,举个例子:

// 引入wxml模块
<import src="../../components/grid-article/index"></import>

<block wx:for="{{posts}}" wx:for-item="post" wx:key="id">
    // 调用wxml模块,同时可传入数据
    <template is="grid-article" data="{{post}}"></template>
</block>

数据和页面的状态是一一对应的,微信小程序中,设计一份好的数据结构,对于Page和页面的代码都有很大的帮助。
微信小程序并不支持a标签,那么多个页面之间如何跳转呢?

页面间的跳转

小程序以栈的形式维护了历史访问的所有页面,并提供了多种页面间的跳转方式;结合前文提到的App()和Page()的各个生命周期,不同的跳转方式和不同的生命周期关联,如下图:

举个例子,Tab 切换对应的生命周期(以 A、B 页面为 Tabbar 页面,C 是从 A 页面打开的页面,D 页面是从 C 页面打开的页面为例)

好了,APP和Page负责维护小程序的生命周期和数据,模板负责接受数据完成页面渲染,页面间的跳转负责将多个页面贯穿起来,那么,如何发生交互呢?接下来我们简单说一下事件。

交互事件

事件绑定

// bindtap 和 catchtap的区别在于
// bindtap 不会阻止事件冒泡
// catchtap会冒泡事件冒泡
<view id="tapTest" data-hi="WeChat" bindtap="tapName"> Click me! </view>
<view id="tapTest" data-hi="WeChat" catchtap="tapName"> Click me! </view>

// 绑定的函数tapName只是一个函数名称,默认接受一个event对象作为参数
Page({
    tapName: function(event) {
        console.log(event)
        }
    })

接下来,另一个问题是:tapName() 如何接受自定义参数呢?

事件传参

传递自定义参数主要有两种方式:

第一种:将参数绑定到wxml标签上,然后通过event.target.dataset获取

第二种:直接使用Page.data或其他数据

到目前为止,一个完整的小程序框架已经实现

  • 小程序只有逻辑和视图两部分,而且不提供组件化解决方案
  • 逻辑主要包含四个东西:两个配置文件 && 两个核心函数
  • 视图很简单,模板语法稍微有点不完善
  • 逻辑层的数据绑定到视图层是由小程序框架自动支持,数据变化,视图自动变化
  • 视图层到逻辑层的,主要通过事件的方式来实现
  • 视图之间的跳转,小程序也提供了它自己的方式,并不支持a标签

框架有了,小程序还提供了官方组件以便快速开发,提供了API以增强应用能力。

这块就不具体介绍了,也需要注意小程序的官方文档还在大规模的更新中,务必查看最新版

官方组件:https://mp.weixin.qq.com/debug/wxadoc/dev/component/?t=20161222

官方API:https://mp.weixin.qq.com/debug/wxadoc/dev/api/?t=20161222

微信小程序的基础知识就是以上的内容,并不复杂,边查边写都可以。

接下来会介绍更进阶一些的内容,内容主要结合好奇心日报这个小程序项目,先看效果:

如何设计微信小程序?

构建系统 && 目录结构

构建系统

由于微信小程序本身对工程化几乎没有任何的支持,所以动手搭建一份:wxapp-redux-starter

使用gulp进行编译构建,主要功能包括:

  • 集成了Redux,数据管理更方便
  • 开发过程中,使用xml取代wxml,对开发工具更友好
  • 开发过程中,使用less取代wxss,功能更强大
  • 引入es-promise,以便可以创建并使用Promise
  • 添加promisify工具函数,可以便捷的将官方Api转换成Pormise模式
  • 引入normalizr,可以将数据扁平化,更方便进行数据管理
  • 引入babel自动进行ES2015特性转换
  • wxml/wxss/js/img压缩,相对开发者工具提供的压缩,会减小一丢丢体积。

    目录结构设计

    按照pages、components、redux、vendors/libs、images几个核心部分拆分,直接上目录。
  • dist目录:构建输出的文件存放到这个目录。
  • src目录:开发模式的文件,包括app、页面、组件、图片等静态资源、辅助函数库、Redux数据管理器、第三方工具库。
  • gulpfile.js:不用多说,gulp构建任务的入口文件。
  • package.json:不用多说,管理者构建任务的依赖。
  • thirdPlugins:由于小程序并不支持直接使用npm,我们可以自主拉取构建,然后拷贝到vendors里,有时候需要简单修改。

构建系统会将src目录下的代码,编译处理后输出到dist目录,小程序开发工具只需要引入dist目录即可。

有了构建工具,代码开发起来更舒心,但很快就遇到另外一个糟心的问题,那就是如何管理散布在各处的数据?
要知道,微信小程序并没有提供组件化方案,所以把组件写成无状态组件特别适合,但是页面管理太多数据很凌乱,有什么办法可以将数据集中管理呢?

引入Redux进行数据集中管理

关于Redux相关的内容,之前有三篇博客详细介绍,有兴趣的建议先移步:

这儿就简单介绍一下,如何在微信小程序中引入Redux 以及 如何将微信小程序和Redux连接起来。

引入Redux

直接在 thirdPlugins目录 运行 yarn add redux / npm install redux,等redux安装好了之后,将 dist目录 的 redux.js/redux.min.js 拷贝到vendors目录中。
需要进行简单的修改才能使用,将压缩版208行代码 (i) 改成 (i || {})即可。

连接微信小程序和Redux

将Redux和微信小程序连接起来才会真的有用处。找了个现成的方案charleyw/wechat-weapp-redux,可以直接使用。

一个完整的Redux方案如下,包括:将Store注入到App中、将state的数据和reducer的方法映射到Page中。一旦state发生变化,Page.data也会更新,进而触发页面的重新渲染。

// APP的逻辑
import { createStore, applyMiddleware, combineReducers } from './vendors/redux.js';
import thunk from './vendors/redux-thunk.js';
import { Provider } from './vendors/weapp-redux.js';

// import reducers
import { rootReducer } from './redux/reducer.js';

// 从Storage读取数据
let entities = wx.getStorageSync('entities') || {};

const store = createStore(
    rootReducer, {
        // 将读取的数据注入到store中
        entities: entities
    },
    applyMiddleware(
        thunk
    )
);

let appConfig = {
    onLaunch: function() {},

    onHide: function() {
        let state = store.getState(),
            cacheEntities = {};

        // 体积大于2M,直接清空,防止缓存占用过大体积
        if (sizeof(state.entities) <= 2 * 1024 * 1024) {
            cacheEntities = state.entities;
        }

        // 退出时将entities缓存下来
        wx.setStorageSync('entities', cacheEntities);
    }
};

App(Provider(store)(appConfig));
// Page的逻辑
import { connect } from '../../vendors/weapp-redux.js';

import { fetchArticleDetail } from '../../redux/models/articles.js';

let pageConfig = {
    data: {
        id: 0,
        postsHash: {}
    },
    onLoad: function(params) {
        var me = this,
            { id, postsHash } = me.data;

        me.fetchArticleDetail(id, function() {}, function() {});
    }
}

// 考虑到列表页已经获取到部分数据
// 为了在详情页第一时间利用这些数据,我们将params传入
// 防止以后需要用data的数据,我们将data也一并传入
let mapStateToData = (state, params, data) => {
    return {
        id: params.id,
        postsHash: state.entities.posts
    }
};

let mapDispatchToPage = dispatch => ({
    fetchArticleDetail: (id, callback, errorCallback) => dispatch(fetchArticleDetail(id, callback, errorCallback)),
});

pageConfig = connect(mapStateToData, mapDispatchToPage)(pageConfig)
Page(pageConfig);

需要注意的是,为了保证第一时间能拿到数据,我们对wechat-weapp-redux/src/connect.js做了优化调整,修改的地方如下

// 修改了以下两个函数
// 可以对照原项目修改,也可以直接拿我的模板项目使用
function handleChange(options) {
    if (!this.unsubscribe) {
        return
    }

    const state = this.store.getState();
    // 将data也一并传入
    const mappedState = mapState(state, options, this.data);
    if (!this.data || shallowEqual(this.data, mappedState)) {
        return;
    }
    this.setData(mappedState)
}

function onLoad(options) {
    this.store = app.store;
    if (!this.store) {
        warning("Store对象不存在!")
    }
    if (shouldSubscribe) {
        this.unsubscribe = this.store.subscribe(handleChange.bind(this, options))
        // 第一次处理的时候也传入options
        handleChange.apply(this, [options])
    }
    if (typeof _onLoad === 'function') {
        _onLoad.call(this, options)
    }
}

引入Redux的优势

引入Redux的好处在于可以集中管理数据,并且让Page的代码保持绝对简单,让所有的组件都变成简单可复用的无状态组件。
此外,Redux还让离线缓存更方便,数据复用更简单。

引入Redux解决了数据散布各处的问题,参考Redux的管理思路,我们构思了一套简单组件化解决方案:假设我们把所有的组件都设计成无状态组件,而组件的数据来源是Page.data,那么我们是否也可以将组件数据的管理抽离到一个单独的文件中呢?接下来讲讲我们使用的简单的组件化解决方案。

简单的组件化解决方案

这份组件化解决方案的核心就在于把组件的关联数据集中起来管理,只暴露出默认数据和数据的操作函数。

比如好奇心日报的详情页有个Toolbar,该Toolbar并不复杂,主要提供返回和点赞功能,其中点赞功能只对文章详情有效,研究所详情页会将点赞功能隐藏。

// components/toolbar/index.js 文件
// 仅提供默认值,不需要和page中的数据保持同步
let defaultData = {
    isPraised: false,
    praiseCount: 0,
    showPraiseIcon: false,
};
// 切换点赞状态
function togglePraise() {
    // 本质上是修改Page.data中的toolbarData
}
// 返回上一级
function navigateToBack(wx) {
    wx.navigateBack({ delta: 1 });
}
module.exports = {
    defaultData,

    togglePraise,
    navigateToBack
}

// pages/articles/show.js 文件
import Toolbar from '../../components/toolbar/index.js';

let pageConfig = {
    data: {
        // 其他数据
        id: 0,
        // Toolbar数据,单独的一份数据,便于维护
        toolbarData: Toolbar.defaultData
    },
    // 点赞或者取消赞
    togglePraise: function() {
        let me = this;

        Toolbar.togglePraise.call(me);
    }
}

// 这儿的组件化并不是真正的组件化
// 而是将组件相关的逻辑和函数抽离到单独的文件中,保证Page代码清晰。
// 同时也为这部分组件逻辑复用提供了可能。
// 本质上来说,抽离出的组件都是“操作Page.data的工具函数”,他们也是纯函数,和“操作state的reducer”类似。

这种Redux的组件化解决方案既简单又好用,保持一定的代码规范即可。这样设计当然是为了复用,同时也让Page的逻辑保持极度精简。

开发中遇到了哪些难点 && 微信小程序有多少坑?

微信小程序目前的确算不上公测的版本,开发者工具不完善、真机表现和开发环境差异很大、部分组件性能较差、部分功能有缺陷,只有经历了这些大坑,才会真的知道你有多“爱”微信小程序。这儿总结了开发中的难点以及微信小程序中遇到的比较明显的坑。

富文本解析

微信小程序并不支持HTML标签,所以对于富文本解析来说,难度较大,而且有些功能还没有办法实现,比如:iframe视频、连接跳转等
这块功能建议由后台统一转换,如果非得前端转换,建议参考下面的思路。

非常感谢 wxParse 这款组件,替我省了不少时间。推荐大家使用,期间遇到一些问题,也分享给大家。

  • wxParse 默认层级只支持10层html嵌套,如果想要支持更深的层级,在wxParse.xml复制几份template即可。
  • wxParse 提供了图片加载成功的回调wxParseImgLoad,很好用。但如果富文本中的图片已经预设宽高比,那么可以不用依赖该回调,在html2jons.js中根据屏幕宽度直接计算出图片高度,先占位,可以避免页面频繁抖动的问题。
  • 如果你的富文本中有自定义模块,对wxParse.xml中的template进行改造即可。

    数据扁平化

    具体如何扁平化,请移步上一篇博客 State设计,Redux 开发第一步
    这儿只简单介绍下扁平化应用场景:
    好奇心日报的研究所是三级表结构:papers > questions > options,后台返回的数据是三级嵌套数据,如果想要修改option.selected字段,需要三级嵌套循环!如果想要获取所有选中的option,需要三级嵌套循环!

    页面展现速度优化

    数据复用,比如复用列表页的数据,可以让详情页的标题等字段第一时间呈现出来。
    离线缓存,同样可以让列表页和详情页第一时间呈现出来,甚至有可能减少请求数量。

无论是数据复用还是离线缓存,配合数据扁平化,都非常好用。

小程序默认设置代理,会和Shadowsocks等VPN冲突(最新版已修复)

解决方法很简单,设置微信小程序不使用代理;或者临时关闭VPN即可。
最新版开发者工具已经解决该问题。

最新版微信小程序移除了对Promise的支持。

开发者自行引入兼容库即可,推荐es6-promise。使用的时候,直接引入Promise即可。

// 引入Promise
import Promise from '../vendors/es6-promise.js';

// 用Promise封装wx.request网络请求
function request(method = 'GET') {
    return function(url, data = {}) {
        return new Promise(function(resolve, reject) {
            wx.request({
                url,
                data,
                method,
                header: {
                    'Content-Type': 'application/json'
                },
                success: function(res) {
                    let { statusCode, errMsg, data } = res;

                    if (statusCode == 200 && data.meta && data.meta.status == 200) {
                        resolve(data.response);
                    } else {
                        reject('网路请求错误,请稍后再试~');
                    }
                },
                fail: function(err) {
                    reject('网路请求不符合规范,请检查域名是否符合要求~');
                }
            });
        })
    }
}
export const GET = request('GET');
export const POST = request('POST');
export const PUT = request('PUT');
export const DELETE = request('DELETE');

// 用Promise封装小程序的其他API
export const promisify = (api) => {
    return (options, ...params) => {
        return new Promise((resolve, reject) => {
            api(Object.assign({}, options, { success: resolve, fail: reject }), ...params);
        });
    }
}
// 使用
const getLocation = promisify(wx.getLocation);

不清楚微信为何会临时移除Promise,统一内置不也挺好?

小程序不能实现完美的fullpage效果,会出现上下拉扯的感觉(最新版预计已修复,待实际验证)。

小程序一旦滚动顶部或者底部,继续滑动的时候,就会出现拉扯现象。而这个拉扯现象还无法禁止。
最新版可以对页面配置disableScroll,预计可以修复这个问题,待实际验证。

swiper组件不支持轮播,性能差,文档模糊(部分最新版已修复)

  • swiper组件之前并不支持轮播,最新版已修复
  • swiper组件之前是通过设置left属性来实现动画,在小米5、华为V8等高端等安卓机上能够感受到明显的卡顿,当然原因是X5内核引起的。最新版已修复,换成了transform,性能有一定的提升。
  • 文档并未标记可以垂直轮播,但其实是可以的。

    // 简单设置vertical即可,但由于官方文档并未备注,尽量不要使用。可以自己开发一个swiper组件。
    <swiper vertical="true"></swiper>
    
  • swiper组件的小圆点其实是可以定制化的,但是官方文档并未说明,而且开发者工具也看不出来,只有鼠标hover到元素上的时候可以看到相关的class,简单猜测一下,最后分析出来它的实现方式。

// 圆点的父元素,用来控制圆点间的间距
.wx-swiper-dot {
    width: 30rpx;
    // 圆点,可以通过font-size修改圆点的大小,color修改圆点的颜色
    &:before {
        width: 100%;
        display: inline-block;
        font-size: 56rpx;
        content: '圆点编码';
    }
    // active状态的圆点
    &.wx-swiper-dot-active {
        &:before {
            color: #ffc81f;
        }
    }
}

小程序WXSS的font-face的url不接受路径作为参数

可以将字体文件转换为base64,然后引用。

同样,如果想要使用iconfont,也可以使用类似的方案,将iconfont字体文件base64之后再引入。

小程序的margin表现有问题(最新版已经修复)

之前发生margin折叠的时候,会取小的那个值。会导致底部留白等设置失效。

canvas问题

canvas并没有深入研究,目前的发现的问题主要是两个,如下图标记:

  • 层级问题,canvas总是会盖在其他元素上面。
  • 支持度不好,在小米5、iPhone5s画图会出现畸形。

最后通过CSS3的方式绘制饼图

<template name="pie">
    <view class="com-pie">
        <!-- 小于50% -->
        <view class="percent-1" style="transform: rotate(0.4turn);"></view>
        <view class="percent-2"></view>
        <!-- 大于50% -->
        <view class="percent-1" style="transform: rotate(0.5turn);"></view>
        <view class="percent-2" style="opacity:1; transform: rotate(0.3turn);"></view>
    </view>
</template>

.com-pie {
    position: relative;
    z-index: 0;
    display: inline-block;
    width: 100rpx;
    height: 100rpx;
    line-height: 100rpx;
    border-radius: 50%;
    color: #000;
    background-color: #ebebeb;
    background-image: linear-gradient(to right, transparent 50%, #cccccc 0);
    overflow: hidden;
    .percent-1,
    .percent-2 {
        position: absolute;
        top: 0;
        width: 60%;
        height: 100%;
        left: 50%;
        transform-origin: center left;
    }
    .percent-1 {
        background-color: inherit;
        z-index: -2;
    }
    .percent-2 {
        height: 110%;
        opacity: 0;
        z-index: -1;
        background-color: #cccccc;
    }
    &.selected {
        background-color: #ffe9a5;
        background-image: linear-gradient(to right, transparent 50%, #ffc81f 0);
        .percent-2 {
            background-color: #ffc81f;
        }
    }
}

微信小程序的rpx会出现精度问题

设置margin-left/margin-right负值,可能导致页面能够左右晃动。猜测 是rpx导致的精度问题。
rpx本质上会转换为px,在不同宽度的设备上,实际的rpx值会转换为带小数的px值,四舍五入可能出现问题,之前使用rem布局的时候在QQ浏览器遇到过类似的问题。

wx.request表现不合理,并且携带特殊字符会报错

  • 请求返回404错误,也会触发success回调。
    不要想当然的认为会触发fail回调,判断一个请求成功或失败,请使用wx.request返回的状态来判断。只有不符合规范的请求,才会触发fail。
  • 请求的数据中,如果有特殊字符(比如\u2820),会报错。
    只会在真机上出现,开发者工具没毛病。估计会有更多的特殊字符会导致这个问题。

    开发者工具,切换页面的时候,有时候wxml不会同步切换

    希望微信什么时候能解决一下。

    微信小程序给wxml模板赋值的时候,解构放到前面可能会报错

    最新版会遇到这个问题,老版本虽然不会报错,但是在部分真机上会出现问题。
    原因未知,遇到这个问题的朋友可以考虑绕过去。

    微信小程序的scroll-view暴露的bindscroll函数并不能实时监听

    依赖实时获取滚动位置的功能不能实现。比如滚动时toolbar的动态隐藏和显示。

    最新版开发工具不能关掉自动刷新

    微信小程序的会默认监听文件变化,然后自动刷新。
    但不足的是每次都是全量刷新,而不是模块的热替换,反而会影响开发速度,尤其对于喜欢频繁Command + S的开发者,你会发现你的小程序在不断的刷新。建议关闭。

    但最新版开发者工具,不勾选也会自动刷新。

微信小程序不支持requestAnimationFrame

微信小程序不支持requestAnimationFrame,所以部分性能优化做不了。不支持的原因未知。

Page.onload函数可以接受参数

该参数是有URL决定的,也就是URL携带的参数。
官方文档这块写的有点混淆,特意拿出来说一下。举个例子:url中传递的时候id=1,那么option.id=1,而不是什么option.query

真机上有概率卡死,目前不确定是代码问题还是小程序的问题。

有遇到类似问题的朋友欢迎指出。

总结说点啥?

本文主要围绕微信小程序的基础知识、如何设计微信小程序、开发过程中遇到的问题三个方面介绍。
微信小程序的基础知识主要包括:

  • 两种配置文件 && 两个核心函数
  • WXML模板语法,页面渲染
  • 页面间的跳转
  • 交互事件
  • 官方组件和官方API

如何设计微信小程序的内容主要包括:

  • 构建系统 && 目录结构
  • 引入Redux进行数据集中管理
  • 简单的组件化解决方案

最后还介绍开发过程中遇到的难点 以及 微信小程序的大小坑。

微信小程序本身并不复杂,开发过程却比较艰辛,尤其是第一次在真机上运行的时候,觉得这个世界恶意满满。

mobiscroll

mobiscroll一款使用简单而强大的JQ插件,依赖文件可以在我的github中找到,这种timepicker在手机端还是非常常见的,实用!

基本使用

引入依赖文件
<link rel="stylesheet" href="mobiscroll.custom-2.17.0.min.css">
<script src='jquery.min.js'></script>
<script src='mobiscroll.custom-2.17.0.min.js'></script>


目标对象
<input id="demo" placeholder="Please Select..." />
<button id="show">show</button>
<button id="clear">clear</button>


初始化,及调用mobiscroll方法
$(function () {
    var now = new Date(),
        minDate = new Date(now.getFullYear() - 10, now.getMonth(), now.getDate()),
        maxDate = new Date(now.getFullYear() + 10, now.getMonth(), now.getDate());

    $('#demo').mobiscroll().datetime({
        theme: 'mobiscroll',
        lang: 'zh',
        display: 'bottom',
        min: minDate,
        max: maxDate,
        stepMinute: 1 , //设置分钟间隔

        dateFormat: 'yy-mm-dd', //日期格式
        // timeFormat: 'HH:ii', //事件格式 大写24H 小写12H
           // timeWheels: 'HHii'
           //dateOrder: 'ddyymm', //面板中日期排列格式

    });

    $('#show').click(function () {  
        $('#demo').mobiscroll('show');
        return false;  
    }); 

    $('#clear').click(function () {  
        $('#demo').mobiscroll('clear');  
        return false;  
    });
});

样式肯定不止这一种啦,更多使用demo请直接转官网demo.mobiscroll.com

修改基本样式

如果你愿意,当然后找一找还是能找到很多样式接口:
<style>
    .mbsc-mobiscroll .dwb0{
        color: red;
        /*确定按钮的颜色*/
    }
    .dw-ul{
        color: lightgrey;  
        /*所有字体的颜色*/
    }
    .dw-sel{
        color: brown;
        /*当前选中字体的颜色*/
    }
    ....    
</style>

效果预览

不要下手太重,轻轻戳我预览😁

不只是timepicker

看一个完整的例子

引入文件

<meta name="viewport" content="width:device-width,initial-scale=1.0">
<link rel="stylesheet" href="content/Content/mobiscroll-2.13.2.full.min.css">
<script src='jquery.min.js'></script>
<script src='content/Scripts/mobiscroll-2.13.2.full.min.js'></script>

HTML

<style>  
.mbsc-android-holo .dwv { text-align:left;text-indent:.8em; }  
</style>  
<ul id="treelist"> 
  <li>  
      <span>奥迪</span>  
      <ul>  
          <li>奥迪A3</li>  
          <li>奥迪A4L</li>  
          <li>奥迪A6L</li>  
          <li>奥迪Q3</li>  
          <li>奥迪Q5</li>  
          <li>奥迪A4</li>  
          <li>奥迪A6</li>  
          <li>奥迪A1</li>  
          <li>奥迪A3(进口)</li>  
      </ul>  
  </li>  
  <li>  
      <span>宝马</span>  
      <ul>  
          <li>宝马X1</li>  
          <li>宝马i3</li>  
          <li>宝马1系</li>  
          <li>宝马3系</li>  
          <li>宝马5系</li>  
      </ul>  
  </li>  
  <li>  
      <span>奔驰</span>  
      <ul>  
          <li>奔驰A级</li>  
          <li>奔驰C级</li>  
          <li>奔驰E级</li>  
          <li>奔驰S级</li>  
          <li>奔驰GLK级</li>  
          <li>奔驰CLA级</li>  
          <li>奔驰CLS级</li>  
      </ul>  
  </li>
</ul>

JS

$(function () {
  var i = Math.floor($('#treelist>li').length / 2),  
      j = Math.floor($('#treelist>li').eq(i).find('ul li').length / 2);  
  $("#treelist").mobiscroll().treelist({  
      theme: "android-ics light",  
      lang: "zh",  
      defaultValue: [i,j],  
      cancelText: '取消',  
      placeholder: '选择车型',  
      headerText: function (valueText) { return "选择车型"; },  
      formatResult: function (array) { //返回自定义格式结果  
          return $('#treelist>li').eq(array[0]).children('span').text() +' --'+ $('#treelist>li').eq(array[0]).find('ul li').eq(array[1]).text().trim(' ');  
      }  
  }); 
}) 

效果预览

轻轻戳我预览😁,注意这个自定义用的文件上是mobiscroll-2.13.2.full.min文件哦。

API参考

初始化

theme: 'android-ics light', //皮肤样式
display: 'modal', //显示方式
mode:'scroller',
dateFormat: 'yy-mm-dd', // 日期格式
setText: '确定', //确认按钮名称
cancelText: '取消',//取消按钮
dateOrder: 'yymmdd', //面板中日期排列格式
headerText: function (valueText) { array = valueText.split('-'); return array[0] + "年" + array[1] + "月"+array[2]+"日"; }, //自定义弹出框头部格式
rows:5,//滚动区域内的行数 

Event

(1)onBeforeClose(valueText, btn, inst)
描述:在list关闭之前执行,如果返回false则不会关闭list。
demo:
$('#mobiscroll').mobiscroll().list({
    onBeforeClose: function (valueText, btn, inst) {
    }
});

(2)onBeforeShow(inst)
描述:在list显示之前执行的事件,一样如果返回false则list不会显示。
demo:
$('#mobiscroll').mobiscroll().list({
    onBeforeShow: function (inst) {
    }
});

(3)onCancel(valueText, inst)
描述:当取消按钮被点击的时候执行的事件。
demo:
$('#mobiscroll').mobiscroll().list({
    onCancel: function (valueText, inst) {
    }
});

(4)onChange(valueText, inst)
描述:当value值改变的时候执行
demo:
$('#mobiscroll').mobiscroll().list({
    onChange: function (valueText, inst) {
    }
});

(5)onClosed(valueText, inst)
描述:当list关闭的时候执行的回调函数
demo:
$('#mobiscroll').mobiscroll().list({
    onClosed: function (valueText, inst) {
    }
});

(6)onDestroy(inst)
描述:当miboscroll实例被destoroy的时候回调
demo:
$('#mobiscroll').mobiscroll().list({
    onDestroy: function (inst) {
    }
});

(7)onHide(inst)
描述:list 关闭的动画执行完毕后回调
demo:
$('#mobiscroll').mobiscroll().list({
    onHide: function (inst) {
    }
});

(8)onInit(inst)
描述:mobiscroll实例初始化完成后执行
demo:
$('#mobiscroll').mobiscroll().list({
    onInit: function (inst) {
    }
});

(9)onMarkupReady(html, inst)
描述:list的html代码已经生成,但是还没有显示到页面中的时候,这个时候可以进行标签的修改。例如添加自定义元素。它会在定位完成前执行。
demo:
$('#mobiscroll').mobiscroll().list({
    onMarkupReady: function (html, inst) {
    }
});

(10)onPosition(html, windowWidth, windowHeight, inst)
描述:list定位完成后调用(包括初始化完成以及大小/方向调整之后)
demo:
$('#mobiscroll').mobiscroll().list({
    onPosition: function (html, windowWidth, windowHeight, inst) {
    }
});

(11)onSelect(valueText, inst)
描述:当value被设置的时候,简单理解就是选择好了原始点击确定之后
demo:
$('#mobiscroll').mobiscroll().list({
    onSelect: function (valueText, inst) {
    }
});
(12)onShow(html, valueText, inst)
描述:当list出现的时候
demo:
$('#mobiscroll').mobiscroll().list({
    onShow: function (html, valueText, inst) {
    }
});

(13)onValueTap(item, inst)
描述:当用户点击list上面的值是调用
demo:
$('#mobiscroll').mobiscroll().list({
    onValueTap: function (item, inst) {
    }
});

彻底弄懂事件委托

事件委托又叫事件代理,JavaScript高级程序设计上讲:事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。何为事件冒泡呢?就是事件从最深的节点开始,然后逐步向上传播事件,举个例子:页面上有这么一个节点树,div>ul>li>a;比如给最里面的a加一个click点击事件,那么这个事件就会一层一层的往外执行,执行顺序a>li>ul>div,有这样一个机制,那么我们给最外面的div加点击事件,那么里面的ul,li,a做点击事件的时候,都会冒泡到最外层的div上,所以都会触发,这就是事件委托,委托它们父级代为执行事件。

举例说明:

有三个同事预计会在周一收到快递。为签收快递,有两种办法:一是三个人在公司门口等快递;二是委托给前台MM代为签收。现实当中,我们大都采用委托的方案。前台MM收到快递后,她会判断收件人是谁,然后按照收件人的要求签收,甚至代为付款。这种方案还有一个优势,那就是即使公司里来了新员工(不管多少),前台MM也会在收到寄给新员工的快递后核实并代为签收

Why we use❓

先看一段代码

<ul id="ul1">
    <li>111</li>
    <li>222</li>
    <li>333</li>
    <li>444</li>
</ul>

window.onload = function(){
    var oUl = document.getElementById("ul1");
    var aLi = oUl.getElementsByTagName('li');
    for(var i=0;i<aLi.length;i++){
        aLi[i].onclick = function(){
            alert(123);
        }
    }
}

看看有多少次的dom操作,首先要找到ul,然后遍历li,然后点击li的时候,又要找一次目标的li的位置,才能执行最后的操作,每次点击都要找一次li



当然,这是常规的做法,一般来说,dom需要有事件处理程序,我们都会直接给它设事件处理程序就好了,那如果是很多的dom需要添加事件处理呢?比如我们有100个li,1000个li,每个li都有相同的click点击事件,可能我们会用for循环的方法,来遍历所有的li,然后给它们添加事件,那这么做会存在什么影响呢?

在JavaScript中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能,因为需要不断的与dom节点进行交互,访问dom的次数越多,引起浏览器重绘与重排的次数也就越多,就会延长整个页面的交互就绪时间,这就是为什么性能优化的主要思想之一就是减少DOM操作的原因;如果要用事件委托,就会将所有的操作放到js程序里面,与dom的操作就只需要交互一次,这样就能大大的减少与dom的交互次数,提高性能;

事件委托触发相同事件

window.onload = function(){
    var oUl = document.getElementById("ul1");
    oUl.onclick = function(){
        alert(123);
    }
}

这里用父级ul做事件处理,当li被点击时,由于冒泡原理,事件就会冒泡到ul上,因为ul上有点击事件,所以事件就会触发,这里是用ul的点击达到同样的效果那么问题来了,如果单纯的点击ul也会触发li本该触发的事件,更精确点,我们只想要li点击时才触发事件怎么办?继续往下走...

Event对象提供了一个属性叫target,可以返回事件的目标节点,我们称为事件源,也就是说,target就可以表示为当前的事件操作的dom,当然,这个是有兼容性的,标准浏览器用ev.target,IE浏览器用event.srcElement,此时只是获取了当前节点的位置,并不知道是什么节点名称,这里我们用nodeName来获取具体是什么标签名,这个返回的是一个大写的,我们需要转成小写再做比较(强迫症是很难改的…)

window.onload = function(){
    var oUl = document.getElementById("ul1");
    oUl.onclick = function(ev){
        var ev = ev || window.event;
        var target = ev.target || ev.srcElement;
        if(target.nodeName.toLowerCase() == 'li'){
            alert(123);
            alert(target.innerHTML);
        }
    }
}

这样改下就只有点击li会触发事件了,且每次只执行一次dom操作,如果li数量很多的话,将大大减少dom的操作,优化的性能可想而知!


事件委托触发不同事件

上面的例子是说li操作的是同样的效果,要是每个li被点击的效果都不一样,那么用事件委托还有用吗?答案是肯定的,如果有标记的话😁

未使用委托:4个按钮,点击每一个做不同的操作,那么至少需要4次dom操作,

<div id="box">
    <input type="button" id="add" value="添加" />
    <input type="button" id="remove" value="删除" />
    <input type="button" id="move" value="移动" />
    <input type="button" id="select" value="选择" />
</div>

window.onload = function(){
   var Add = document.getElementById("add");
   var Remove = document.getElementById("remove");
   var Move = document.getElementById("move");
   var Select = document.getElementById("select");

   Add.onclick = function(){
       alert('添加');
   };
   Remove.onclick = function(){
       alert('删除');
   };
   Move.onclick = function(){
       alert('移动');
   };
   Select.onclick = function(){
       alert('选择');
   }

}

使用委托:可以只用一次dom操作就能实现所有的效果

window.onload = function(){
        var oBox = document.getElementById("box");
        oBox.onclick = function (ev) {
            var ev = ev || window.event;
            var target = ev.target || ev.srcElement;
            if(target.nodeName.toLocaleLowerCase() == 'input'){
                switch(target.id){
                    case 'add' :
                        alert('添加');
                        break;
                    case 'remove' :
                        alert('删除');
                        break;
                    case 'move' :
                        alert('移动');
                        break;
                    case 'select' :
                        alert('选择');
                        break;
                }
            }
        }

    }

关于动态添加新元素

未使用事件委托

for(var i=0; i<aLi.length;i++){
    aLi[i].onclick = function(){
        this.style.background = 'red';
    };
}
//添加新节点
oBtn.onclick = function(){
    var oLi = document.createElement('li');
    oLi.innerHTML = 111*num;
    oUl.appendChild(oLi);
};

这是一般的做法,你会发现,新增的li是没有点击事件的

使用事件委托

oUl.onclick = function(ev){
  var ev = ev || window.event;
  var target = ev.target || ev.srcElement;
  if(target.nodeName.toLowerCase() == 'li'){
      target.style.background = "red";
  }
};
oBtn.onclick = function(){
  var oLi = document.createElement('li');
  oLi.innerHTML = 111*num;
  oUl.appendChild(oLi);
};

这里你会发现,新添加的li元素无需做其他操作,已有点击事件


事件委托适用场景

  • 适合用事件委托的事件:click,mousedown,mouseup,keydown,keyup,keypress。
  • 不适合使用的事件:mouseover,mouseout和mousemove,因为不是单纯地点击等操作,每次触发都需要重新计算一下位置,处理起来也不是很方便,就不用喽
  • focus,blur之类的,本身就没用冒泡的特性,自然就不能用事件委托了。

OVER

博观而约取,厚积而薄发

出自宋代诗人苏轼的《杂说送张琥》

博观而约取,厚积而薄发,吾告子止于此矣。

多读书,取其精华,去其糟粕,我认为人生也是如此,应该记着开心的,忘掉烦心的。

无论知识,还是其他都是需要积累的,人要一步一个脚印,只有脚踏实地,才能仰望苍穹。

博观:指大量的看书,多多阅读,了解事物;约取:指少量的慢慢的拿出来。 厚积:指大量地、充分地积蓄;薄发:指少量地、慢慢地放出。多多积蓄,慢慢放出。形容只有准备充分才能办好事情。 其实上面两句话的意思差不多,总的意思就是指要经过长时间有准备的积累即将大有可为,施展作为。





左耳是靠近心脏最近的地方,甜言蜜语要说给左耳听






踮起脚尖,离阳光更近一点







会有人想你的那个地方,就是你回去的地方

文件拖拽

拖放是一种常见的特性,即抓取对象以后拖到另一个位置。在 HTML5 中,拖放是标准的一部分,任何元素都能够拖放。Internet Explorer 9、Firefox、Opera 12、Chrome 以及 Safari 5 支持拖放。注意即使元素可以拖拽,在拖拽的过程中(ondragover)默认情况下是无法将数据、元素放到其他元素中的,如果需要允许,就要阻止默认处理;

拖拽API

draggable :
–    设置为true,元素就可以拖拽了

拖拽元素事件 :  事件对象为被拖拽元素
–    dragstart ,  拖拽前触发 
–    drag ,拖拽前、拖拽结束之间,连续触发
–    dragend  , 拖拽结束触发

目标元素事件 :  事件对象为目标元素
–    dragenter ,  进入目标元素触发,相当于mouseover
–    dragover  ,进入目标、离开目标之间,连续触发
–    dragleave ,  离开目标元素触发,相当于mouseout
–    drop  ,  在目标元素上释放鼠标触发

Demo1-拖拽删除

效果示例

完整代码

<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Untitled Document</title>
    <style>
        li{ list-style:none; width:100px; height:30px; background:yellow; margin:10px;}
        #div1{ width:100px; height:100px; background:red; margin:200px;}
    </style>
</head>

<body>
    <ul>
        <li draggable="true">a</li>
        <li draggable="true">b</li>
        <li draggable="true">c</li>
    </ul>
    <div id="div1"></div>
    <script>
    window.onload = function(){
        var aLi = document.getElementsByTagName('li');
        var oUl=document.getElementsByTagName('ul')[0];
        var oDiv = document.getElementById('div1');
        var oImg = document.getElementById('img1');

        for(var i=0;i<aLi.length;i++){
            aLi[i].index = i;
            aLi[i].ondragstart = function(ev){
                ev.dataTransfer.setData('name',this.index);
                this.style.background = 'green';
                //ev.dataTransfer.setDragImage(this,0,0); //设置拖动时显示的对象和坐标
            };

            aLi[i].ondrag = function(){  //开始与结束连续触发
                document.title = i++;
            };

            aLi[i].ondragend = function(){
                this.style.background = 'yellow';
            };
        }

        oDiv.ondragenter = function(){
            this.style.background = 'blue';
        };

        oDiv.ondragover = function(ev){
            //enter和leave之间连续触发
            document.title = i++;
            //要想触发drop事件,就必须在dragover当中阻止默认事件
            ev.preventDefault();
        };

        oDiv.ondragleave = function(){
            this.style.background = 'red';
        };

        oDiv.ondrop = function(ev){
            ev.preventDefault();   //阻止默认事件,不然是外部图片就直接打开了
            var n=ev.dataTransfer.getData('name');
            // ev.dataTransfer.clearData("name");
            oUl.removeChild( aLi[n]); //删除以后要从新算index
            for(var i=0;i<aLi.length;i++){
                aLi[i].index = i;
            }
            this.style.background = 'red';    
        };    
    };
    </script>
</body>
</html>

Demo2-外部文件拖入显示

效果示例

完整代码

<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Untitled Document</title>
    <style>
    ul,li{ margin:0;padding:0;list-style:none;}
    div{
        width:200px; height:200px; background:red; text-align:center;
    }
    ul{
        width:600px; background-color:#000; 
    }
    li {
        margin:10px 20px;
    }
    ul:after {
        content:'';
        display:block;
        clear:both;
    }
    </style>
</head>

<body>
    <div>把图片拖到这里</div>
    <ul></ul>

    <script>
    window.onload=function(){
        var oDiv = document.querySelector('div');
        var oUl = document.querySelector('ul');
        oDiv.ondragenter=function(){
            this.innerHTML='可以松开了哦';
        }
        oDiv.ondragover=function(ev){
            var ev=ev||event;
            ev.preventDefault(); 
        }
        oDiv.ondragleave=function(){
            this.innerHTML='把图片拖到这里';
        }
        oDiv.ondrop=function(ev){
            var ev=ev||event;
            ev.preventDefault(); //阻止默认事件,不然是外部图片就直接打开了
            this.innerHTML='把图片拖到这里';

            var fs=ev.dataTransfer.files; //拖拽的文件集合
            for(var i=0; i<fs.length; i++){
                if(fs[i].type.indexOf('image')!=-1){ //判断拖拽内容的类型是image
                    var fd=new FileReader();  //创建可以读文件的对象
                    fd.readAsDataURL(fs[i]);  //读
                    fd.onload=function(){  //读取成功直接执行onload
                        var oLi    =document.createElement('li');
                        oLi.style.float='left';
                        var oImg= document.createElement('img');
                        oImg.style.height='200px';
                        oImg.src = this.result;  //地址就是获取到的url信息
                        oLi.appendChild(oImg);
                        oUl.appendChild(oLi);
                    }
                }else{
                    alert('非法图片');
                }
            }
        }
    }

    </script>
</body>
</html>

图片预览上传

上传图片之前应该让用户可以预览一下刚才选中的图片,这对于优化用户体验非常重要,本文主要介绍一下简单的图片预览的效果及ajax的formData上传和php接收添加数据库的基本操作

直接上代码

HTML

<style>
  button{
    margin-top: 50px;
  }
</style>
<body> 
    <label for="username">用户名:</label>
    <input type="text" placeholder="请输入用户名" id="username">
    <!--这里限制了可以上传的文件只能是图片,且可以一次上传多张图片-->
      <input type="file" id="file" accept="image/gif,image/jpeg,image/jpg,image/png" multiple><br/>
      <button>上传</button>
      <script src="jquery.min.js"></script> 
</body>

JS

$(function(){
    //图片预览
    var FileReader = window.FileReader;   //创建文件读取对象
    $("#file").change(function() {  
        if (FileReader) {                 //chrome浏览器处理  
            var file = this.files;        //获取一次上传的全部文件
            var length = file.length; 
            for(var i=0; i<length;i++){
                upload(this,i)            //逐个上传
            } 
        }  
        else {//其他浏览器处理
           var path = $(this).val();  
            if (/"\w\W"/.test(path)) {  
                path = path.slice(1,-1);  
            }  
            $("#image").attr("src",path);
        }  
    });  

    function upload(obj,i){
        var reader = new FileReader();
        var file = obj.files;
        reader.readAsDataURL(file[i]);   //读取文件路径
        reader.onload = function(e) {  
            var Img = new Image()
            Img.src=e.target.result;     //将读取到的文件给到新创建的image
            Img.width=100;
            Img.height=100;
            document.body.appendChild(Img);//这里是把图片转成64位数据存入<img>中的src里
        };  
    }  

    //图片上传
    $('button').click(function(){
        var username =$('#username').val();
        var form = new FormData();
        var file = $('#file')[0]['files'][0]; //点击提交的时候获取图片地址    
        form.append('username',username);
        form.append('file',file);     
        $.ajax({
            type:'post',
            url:'../php/add.php',
            data:form,
            processData:false,   //ajax 提交的时候不会序列化data,而是直接使用data
            contentType:false,   //发送信息至服务器时内容编码类型设置为不需要编码
            success:function(data){
                alert(data)
            }
        })

    })
})

PHP

if(!empty($_POST['username'])){
    $username = $_POST['username'];
}    
//处理文件
if(!file_exists('../uploads')){
    mkdir('../uploads');  //创建文件夹接收图片
}
$file = $_FILES['file']['tmp_name'];  //获取文件路径
$newName = $_FILES['file']['name']; //获取文件的名字
$res = move_uploaded_file($file,"../uploads/{$newName}"); //原名上传到指定文件夹
if($res){
    $url="../uploads/{$newName}";
}

$sql ="INSERT INTO addmore (username,url) VALUES('$username','$url')";
$res =add($sql);

//增加数据
function add($sql){
    $mysqli = new mysqli('localhost','root','','mybook');
    //设置编码
    $mysqli->query('set names utf8');
    $res = $mysqli->query($sql);
    if($res){
        echo '添加成功';
    }else{
        echo '添加失败';
    }
}

OVER