在Phalcon中为View添加缓存

我们可以为PhalconView添加事件监听,View一共有5个事件:

Event Name Triggered Can stop operation ?
beforeRender Triggered before starting the render process Yes
beforeRenderView Triggered before rendering an existing view Yes
afterRenderView Triggered after rendering an existing view No
afterRender Triggered after completing the render process No
notFoundView Triggered when a view was not found No

di注入是view时添加事件:

<?php

use Phalcon\Events\Event;
use Phalcon\Events\Manager as EventsManager;
use Phalcon\Mvc\View;

$di->set(
    'view',
    function () {
        // Create an events manager
        $eventsManager = new EventsManager();

        // Attach a listener for type 'view'
        $eventsManager->attach(
            'view',
            function (Event $event, $view) {
                echo $event->getType(), ' - ', $view->getActiveRenderPath(), PHP_EOL;
            }
        );

        $view = new View();

        $view->setViewsDir('../app/views/');

        // Bind the eventsManager to the view component
        $view->setEventsManager($eventsManager);

        return $view;
    },
    true
);

上面是监听了View的所有事件,如果要监听某个具体的,如监听beforeRender

$eventsManager->attach('view:beforeRender', function(){

})

现在,就可以利用afterRender事件来缓存页面,提升性能。

首先,需要向di注入一个viewCache服务:

<?php

use Phalcon\Cache\Frontend\Output as OutputFrontend;
use Phalcon\Cache\Backend\Memcache as MemcacheBackend;

// Set the views cache service
$di->set(
    'viewCache',
    function () {
        // Cache data for one day by default
        $frontCache = new OutputFrontend(
            [
                'lifetime' => 86400,
            ]
        );

        // Memcached connection settings
        $cache = new MemcacheBackend(
            $frontCache,
            [
                'host' => 'localhost',
                'port' => '11211',
            ]
        );

        return $cache;
    }
);

这里的cache也可以换成Redis等你在使用的。lifetime为默认缓存时间,当调用该服务时,可以另外给出lifetime的值。键为keyPhalcon会默认使用MD5加密当前的Controller/view

定义afterRender事件触发时要做的事情:

class ViewPlugin extends \Phalcon\Mvc\User\Plugin {

    public function afterRender(\Phalcon\Events\Event $event, $view){
        $request    = \Phalcon\Di::getDefault()->get('request');
        $key        = md5($request->getURI());
        if (!$view->getCache()->exists($key)) {
            $view->getCache()->save($key, $view->getContent(), DEFAULT_VIEW_EXPIRE_TIME);
        }
    }
}

这样当访问一个新页面后就会缓存下来。接下来就是第二步,再次访问该页面时,直接向浏览器输出缓存内容,可以定义在父类里:

$this->view_cache_key = md5($this->request->getURI());
if ($this->view->getCache()->exists($this->view_cache_key)) {
    echo $this->view->getCache()->get($this->view_cache_key);
    exit;
}

这就是利用View的事件缓存页面,当然也可以用于其他场景,如生成静态HTML页面

参考文档:

Phalcon view

如何阻止表单自动填充?

默认情况下,我们再次填写提交过的表单时,就会出现浏览器已经自动帮我们填写好或者输入框获取到焦点时展示下拉选项供我们自己选择,这样方便我们快捷的完成表单。但是也有问题,如果是在公共电脑上,又或者是这个表单时关系到你的钱包的,那么你是不希望所输入的东西被浏览器自动记录或填充的。

  • 禁用自动填充

要禁用自动填充,可以通过属性autocomplete实现,它有两个值onoff

autocomplete="off"

该属性可以用在form标签上,也可以用在input标签上。加上上面的代码后,浏览器的处理如下:

1. 告诉浏览器不再保存用户输入的数据来自动填充表单
2. 浏览器停止缓存数据到会话历史(session history)。即:我们点击浏览器回退箭头时,之前提交的表单数据不会展示出来。
  • 阻止密码管理自动填充

上面这样就可以了吗?当我们登录、注册时,浏览器会弹出提示是否记住密码,如果选择记住密码,下次进入这个页面依然会自动完成。为什么?因为现在的浏览器都实现了集成密码管理。如果有浏览器账号,还会在任意一台你登录账号的电脑上自动填充。这样,登录表单设置autocomplete="off"就无效了。

对于现代浏览器这一特性,要阻止登录表单自动填充,autocomplete属性有了一个新的值new-password。当对input输入框设置时,便不再会自动填充了。

小程序的模板使用介绍

模板,即一部分业务代码片段,具有通用性。使用模板的好处,自然是方便维护。前端时间在做一个视频小程序时,便遇到了需要模板的场景。

这个小程序是个关于视频的项目,在首页,分类,搜索,个人中心(我发布的视频)都存在视频列表(页面包含列表),所以我打算将列表拿出来做个模板。

小程序定义模板语法:

<template name="yourTemplateName">
    <!--
        这里是页面布局的代码,例如:
        <view>
            <text>hello world!</text>
        </view>
    -->
</template>

定义好模板后保存为一个wxml的文件。

在需要使用模板的页面,首先引入模板

<import src="path/to/template/template.wxml" />

然后,在需要展示模板的地方使用模板

<template is="yourTemplateName" />

如果,模板里有变量,可以通过data传入

<template is="yourTemplateName" data="{{item}}"/>

如果有多个值,这样传入

<template is="yourTemplateName" data="{{name: theName, age: theAge, gender: theGender}}"/>

这是wxml模板的使用,与wxml文件相关联的还有两个文件wxss文件、js文件。wxss文件没有什么可说的,直接在需要的页面的wxss文件里使用import引入即可,这里说下js文件。

小程序每个页面都需要在js文件里使用Page函数注册页面,对于模板里的事件等一系列操作,我们不可能在每个地方都去写一遍。和定义模板一样,我们新建一个独立的js文件,内容如下:

export function functionName(params){
    #coding here
}

然后在需要的js页面引入

var lib = require(path/to/function);

var fn = lib.functionName;

这样,在Page函数里,我们就可以使用fn这个函数了。

如何在ios的微信webview里强制刷新

移动端网页webview中遇到了一个问题,A页面进入B页面做了一些操作,影响了A页面部分数据,再返回到A页面,受影响数据应该显示新数据。在android的手机里面使用自带虚拟返回按钮,不会存在这样的问题。但是在ios里面,使用自带返回按钮回到的界面并没有更新。这是为什么?原因是**bfcache(back-forward cache**)。详细描述看这里

解决方案链接的文章里也有说道,我这里采用的是监听pageonshow事件:

window.onpageshow = function(event) {
      if (event.persisted) {
            window.location.reload()
      }
};

MySQL导出数据到文件

从数据库导出数据的方式很多,如命令的mysqldump可以将整个数据库或某些表或某些数据导出。

整个数据库导出:

mysqldump [-h] -u root -p database>back.sql

某部分表导出:

mysqldump [-h] -u root -p database table1 table2 > back.sql

某些数据:

mysqldump [-h] -u root -p database table1 --where="id<100" > back.sql

上面的方式一般作为备份或数据迁移使用,自己想查看并不是那么直观,结果里都是些mysql命令,如何只将查询结果导出来呢?可以使用这个语句:

select * into OUTFILE '/path/to/file' from table where conditions

这里需要注意的是,如我们使用下面这样的语句:

select * into OUTFILE '/data/back/tmp/order.txt' from order where create_date='2017-03-27'

如果你已经将order.txt文件建好了,会报错说文件已经存在。那么说明文件不需要我们去建,于是删掉。再次运行命令可以又报错说没有写的权限,暴力点就给目录加上0777的权限就可以了。

MySQL如何支持emoji表情

emoji表情对于移动端来说,十分常见。可是,最近才发现我们项目还不支持emoji表情。数据库使用的字符集是UTF-8,WTF?还有UTF-8搞不定的?那该用什么?该用utf8mb4。那么,改吧。

要想支持emoji表情,首先数据库版本必须是5.5.3及其以上。如果你的数据库版本还没有达到要求,那么请升级。版本符合要求的,开始修改数据库、表、字段信息吧。

  • 数据库
ALTER DATABASE database_name CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;

将要存储emoji表情的表修改如下:

ALTER TABLE table_name CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
  • 字段
ALTER TABLE table_name CHANGE column_name column_name VARCHAR(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

对于字段,除了字符集设置,其他信息字段类型,长度等请根据自己项目的实际情况设置。

使用Canvas压缩图片

为什么要前端压缩?

首先,图片在前端压缩是可以实现的。为什么要在前端压缩呢?有什么好处?在前端压缩会不会更耗时……对于这一系列的问题,可以很肯定的回答,前端压缩是需要的。尤其对于流量大的网站,可以节省很多带宽呢(都是RMB呀)。那么,怎么压缩呢?当然,这项工作就交给HTML5Javascript吧。有他俩搭档,很容易就能完成这件事情。

原理

当你在网上搜索*HTML5上传文件*时,你会找到很多拖拽的demo。不论是拖拽还是按钮上传,原理都这样:

  • 对事件(change,drop)的监听
  • 通过文件API–FileReader获取图片
  • 通过canvas绘制图片,在这里可以对图片进行缩放,大小压缩

实践

HTML代码:

<form>
<input type="file" id="uploadfile"/>
</form>

Javascript代码:

<script>
document.getElementById('uploadfile').addEventListener('change', function(event){
    //code for resize
}, false);
</script>

压缩代码

;(function ($,doc) {
    'use strict';

    var defaultOps = {
        url: null,
        quality: 40,
        resize: false,
        scale:{
            maxWidth: 640,
            maxHeight: null
        },
        output: null,
        formData: {},
        uploadProgress: function () {

        },
        uploadComplete: function () {

        },
        uploadFailed: function () {

        },
        uploadCanceled: function () {

        },
        uploading: function () {

        }
    };

    function _compload(evt, options) {
        var self = this;

        this.defaults = $.extend(true, defaultOps, options);
        if(!this.defaults.url){
            throw new Error('url is invalid');
        }

        this.afterSelect = function () {
            var file = evt.target.files[0],
                reg  = new RegExp('^image.*$'),
                type = this.defaults.output || file.type;

            if(!file){
                throw new Error('文件为空');
            }
            if(!reg.test(file.type)){
                throw new Error('只能上传图片');
            }

            var reader = new FileReader(),
                i      = doc.createElement('img');

            reader.onload = function (e) {
                i.src = e.target.result;
            }
            reader.readAsDataURL(file);

            i.onload = function (e) {
                var compressImg = self.compress(i, self.defaults.quality, type),
                    imgFile     = self.convert(compressImg.src);

                self.upload(imgFile);
            };
        }

        this.compress = function (source_img_obj, quality, outputType) {

            var cvs     = doc.createElement('canvas'),
                nWidth  = source_img_obj.naturalWidth,
                nHeight = source_img_obj.naturalHeight,
                sWidth  = self.defaults.scale.maxWidth,
                sHeight = self.defaults.scale.maxHeight,
                scale   = 1;

        if(self.defaults.resize){
                    if(sWidth && sHeight){
                        scale = Math.min(sWidth/nWidth, sHeight/nHeight);
                    }else if (sWidth && nWidth >= sWidth && !sHeight){
                        scale = sWidth/nWidth;
                    }else if (!sWidth && nHeight >= sHeight && sHeight){
                        scale = sHeight/nHeight;
                    }
                }

            cvs.width  = nWidth * scale;
            cvs.height = nHeight * scale;

            var ctx              = cvs.getContext("2d").drawImage(source_img_obj, 0, 0, cvs.width, cvs.height),
                newImageData     = cvs.toDataURL(outputType, quality/100),
                result_image_obj = new Image();

            result_image_obj.src = newImageData;

            return result_image_obj;
        }

        this.convert = function (img_src) {
            var BASE64_MARKER = ';base64,',
                dataURL = img_src;
            if (dataURL.indexOf(BASE64_MARKER) == -1) {
                var parts = dataURL.split(',');
                var contentType = parts[0].split(':')[1];
                var raw = parts[1];

                return new Blob([raw], {type: contentType});
            }

            var parts = dataURL.split(BASE64_MARKER);
            var contentType = parts[0].split(':')[1];
            var raw = window.atob(parts[1]);
            var rawLength = raw.length;

            var uInt8Array = new Uint8Array(rawLength);

            for (var i = 0; i < rawLength; ++i) {
                uInt8Array[i] = raw.charCodeAt(i);
            }

            return new Blob([uInt8Array], {type: contentType});
        }

        this.upload = function (file) {
            self.defaults.uploading();
            setTimeout(function () {
                var fd = new FormData();
                fd.append("file", file);
                for (var k in self.defaults.formData){
                    fd.append(k, self.defaults.formData[k]);
                }
                var xhr = new XMLHttpRequest();
                xhr.upload.addEventListener("progress", self.defaults.uploadProgress, false);
                xhr.addEventListener("load", self.defaults.uploadComplete, false);
                xhr.addEventListener("error", self.defaults.uploadFailed, false);
                xhr.addEventListener("abort", self.defaults.uploadCanceled, false);
                xhr.open("POST", self.defaults.url);
                xhr.send(fd);
            }, 10);
        }

        this.afterSelect();
    }

    window.Compload = _compload;
})(jQuery,document);

如何防止频繁调用Javascript一段代码?

应用场景
首先说下什么时候会用到,当我们给页面注册了scroll,resize等这类事件时,可能就需要用到了。为什么呢?比如,我们需要做一个页面滚动条滚动到底部时就自动加载下一页的功能,那么当滚动条滚动时就会频繁的触发scroll事件。为了减少js的执行,提升页面的性能,我们就需要滚动条在页面底部时才触发加载。

代码实现

function debounce(func, wait, immediate) {
        var timeout;
        return function() {
            var context = this, args = arguments;
            var later = function() {
                timeout = null;
                if (!immediate) func.apply(context, args);
            };
            var callNow = immediate && !timeout;
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
            if (callNow) func.apply(context, args);
        };
    }

Uncaught RangeError:Maximum call stack size exceeded

在做ajax交互时,出现了点击按钮毫无反应,初初以为是事件绑定问题,检查过代码过后确认应该是其他问题。后来chromeconsole控制台提示:

Uncaught RangeError:Maximum call stack size exceeded

于是,使用firefox developer版调试,console控制台报错:

TypeError: 'stepUp' called on an object that does not implement interface HTMLInputElement

终于找到了一个比较重要的错误提示,于是上Google搜索,在stackoverflow上找到问题,是因为$.ajaxdata属性里加入了html元素,即:

data = $('[name=name]').val()

后面的val()没有写导致的。

Web版2048实现

2048是2014年出现的一款游戏,虽然距今已有两年时间,但是依然是让人爱的根本听不下来。我也玩过很长时间,如今网上也有各种版本(不同的语言实现)的2048。但是,我依然想自己实现一次。于是就有了今天的分享。源码地址在这里

分析

2048这个游戏在开始时,界面会出现两个为2的方块,然后玩家可以向上下左右四个方向任意方向操作来移动方块,并向界面再新增一个为2的方块,含有相同数值的方块会合并为一个方块,该方块值为二者相加,由此不断直到方块填满界面且没有可合并方块时,游戏结束。

纵观整个过程,涉及的操作有

  1. 初始化
  2. 移动方块
  3. 合并方块
  4. 插入方块
  5. 游戏是否结束? 未结束那么重复第2到第4步,否则结束游戏

现在就按照上面5个步骤开始代码实现。

定义游戏类

function game(){
    //为了不每次检查是否要合并方块都从DOM去取,所以给类定义一个map一一对应每个方格。
    this.map = [
                [
                    [],[],[],[]
                ],
                [
                    [],[],[],[]
                ],
                [
                    [],[],[],[]
                ],
                [
                    [],[],[],[]
                ]
            ];
    //方块的颜色值,以标识不同的值
    this.color = [
	 	'#99CCCC','#FFCC99','#FFCCCC',
	 	'#FFFF99','#CCCCFF','#CC9966',
	 	'#666666','#CC9999','#993333',
	 	'#CCCC00','#663366','#CC0033',
	 	'#333333','#CCCC00','#FF0033',
	 	'#333399','#999933','#333300'
	 	];
    //玩家操作方向
    this.direction = {
	 	37: 'left',
	 	38: 'up',
	 	39: 'right',
	 	40: 'down'
	 	};
    //获取所有行
    this.tr = (function(){
		    var tbody = document.querySelector('tbody');
		    return tbody.children;
		}());
    //分数
    this.score = 0;
}

初始化

初始化是向界面随意两个位置插入两个方块,位置怎么选是后面每一次操作都会遇到的问题,所以得先有一个方法来指定位置。

获取位置

game.prototype = {
    selectPosition: function(){
        
    }
}

最开始我是随机选择一行i,然后判断这行是否有空余位置(即this.map[i]中是否存在空数组),但是这样会出现问题就是肯能随机10次得到的i都是满员了,所以改作一下这种方式(也不知道这样好不好),先遍历map找出至少还有一个还有位置的行,然后在这些行里随机出一行:

getLine: function(){
			 var line = [];
				for(var i=0; i<4; ++i){
					for(var j=0; j<4; ++j){
						if(!this.map[i][j].length){
							line.push(i);
							break;
						}
					}
				}

				return line;
		},

得到行之后再从得到当前行存在几个空位,从空位中找出一个位置返回。位置得到了,现在就是将方块显示在界面中,所以需要一个插入方块的方法。但是这个方块哪里来呢?所以还需要一个制造方块的工厂。

生产方块

game.prototype.create = function(){

}

颜色选择

由于不同的值,方块颜色不同,怎么来确定选用哪个颜色呢?因为出现的值都是2的n次方,所以我打算由n来选择颜色,代码如下:

game.prototype.color = function(val){
    return Math.log(val)/Math.log(2);
}

插入方块

现在,插入方块所需的条件都已经具备了。so,去吧,皮卡丘!

game.prototype.insert = function(){
    
}

在界面中新增方块过后,需要及时更新map。接下来就是玩家操作了,我们可以在document上注册键盘事件以判断玩家操作。

game.prototype.move = function(){
    var direc = this.direction;
    document.addEventListener('keyup', function(e){
        var keycode = e.keyCode || e.which;
        keycode in direc ? self[direc[keycode]]() : '';
    },false);
}

操作

来到最后一步了,那就是上下左右的四个操作:

game.prototype.up = function(){}
game.prototype.right = function(){}
game.prototype.down = function(){}
game.prototype.left = function(){}

在做完这里之后,我觉得这四个方法可以优化合并成一个,但是没有想到怎么处理。

试玩地址:点击这里