Yii2 ajax 自动跳转至登录页面原理分析

某些路由(页面,下同)需要登录才能访问,我们通常使用访问控制过滤器(Access Control Filter)来进行处理。如果未登录用户访问对应路由,会返回 302 并带上 Lacation 的 HTTP Header,此时浏览器会跳转到对应页面。当使用 ajax 请求需要登录的路由时,页面也会跳转到登录页面。仔细查看 Response Header 会发现存在名为 X-Redirect 的 HTTP Header,yii 框架自带的 yii.js(YiiAsset) 里有如下逻辑:

$(document).ajaxComplete(function (event, xhr, settings) {
	var url = xhr && xhr.getResponseHeader('X-Redirect');
	if (url) {
		window.location = url;
	}
});

Yii2 AssetBundle js 和 css 没有自动更新的问题

最近几天看了很多基于 Yii2 的一些开源项目。很多功能都在可以找到开源的以 Yii2 extension 的形式存在,集成到自己的项目中相当简单。这么说吧,基于这些组件,你可以写很少或者几乎不用写一行代码就可以搭建起来一套很好用的程序。常见的富文本编辑器、权限管理、导航栏、菜单等等这些都有很多不错的扩展可以直接集成。之前我一直以为PHP程序源码不含图片的话超过10M就很恐怖了,但最近几天接触的这些基于 Yii2 的项目,composer install 之后整个源码目录大小超过100M很常见。当然,这还没有 node 恐怖,npm install 之后,随随便便 200M+ 更常见。

以上是题外话。前段时间我就发现了如题的问题。那时没注意。今天仔细读了下相关源码,弄明白了这个问题的根源。

假设有如下 HelloAsset

<?php
namespace app\assets;

use yii\web\AssetBundle;

class HelloAsset extends AssetBundle
{

    public $sourcePath = '@app/hello_assets';

    public $js = [
        'hello.js',
        'js/hello2.js',
    ];

}

在视图文件里面注入这个 Bundle

<?php

\app\assets\HelloAsset::register($this);

?>

<h1>Hello, world!</h1>

问题表现为:当我们修改 hello.js 后,刷新页面,加载的是我们修改过后的最新的 js。但是当我们修改 js/hello2.js 的内容后,刷新页面,发现最新的 js 并没有出现在浏览器中。

一路追踪代码到 AssetManager.php 里面有个 hash 方法,这个方法会生成 bundle 对应的 assets 目录,代码如下:

    /**
     * Generate a CRC32 hash for the directory path. Collisions are higher
     * than MD5 but generates a much smaller hash string.
     * @param string $path string to be hashed.
     * @return string hashed string.
     */
    protected function hash($path)
    {
        if (is_callable($this->hashCallback)) {
            return call_user_func($this->hashCallback, $path);
        }
        $path = (is_file($path) ? dirname($path) : $path) . filemtime($path);
        return sprintf('%x', crc32($path . Yii::getVersion()));
    }

上面方法的参数 $path 就是 AssetBundle::$sourcePath 应用 Yii::getAlias 得到的路径。可以看到这个方法的逻辑很清晰,如果没有配置 hashCallback,默认生成 hash 与 $path 的最近一次修改时间相关。怀疑问题出现在这里。

linux 下用 stat 命令可以获取文件的最近访问更改以及改动时间:

$ stat hello_assets 
  文件:"hello_assets"
  大小:4096      	块:8          IO 块:4096   目录
设备:fc00h/64512d	Inode:1321721     硬链接:3
权限:(0775/drwxrwxr-x)  Uid:(  900/ vagrant)   Gid:(  900/ vagrant)
最近访问:2016-03-16 17:46:07.011451000 +0800
最近更改:2016-03-16 17:46:06.995443000 +0800
最近改动:2016-03-16 17:46:06.995443000 +0800
创建时间:-

当我更改 hello.js 的内容后再次运行 stat 命令:

$ stat hello_assets 
  文件:"hello_assets"
  大小:4096      	块:8          IO 块:4096   目录
设备:fc00h/64512d	Inode:1321721     硬链接:3
权限:(0775/drwxrwxr-x)  Uid:(  900/ vagrant)   Gid:(  900/ vagrant)
最近访问:2016-03-16 17:56:31.279429000 +0800
最近更改:2016-03-16 17:56:31.271425000 +0800
最近改动:2016-03-16 17:56:31.271425000 +0800
创建时间:-

很明显,最近改动时间已经变了,这点是没问题的。

当我更改 js/hello2.js 的内容后再运行 stat:

$ stat hello_assets            
  文件:"hello_assets"
  大小:4096      	块:8          IO 块:4096   目录
设备:fc00h/64512d	Inode:1321721     硬链接:3
权限:(0775/drwxrwxr-x)  Uid:(  900/ vagrant)   Gid:(  900/ vagrant)
最近访问:2016-03-16 17:56:31.279429000 +0800
最近更改:2016-03-16 17:56:31.271425000 +0800
最近改动:2016-03-16 17:56:31.271425000 +0800
创建时间:-

可以看到这时 stat hello_assets 的最近改动时间并没有变化。

所以得出结论,当我们更改 $sourcePath 下面再下一级目录下的文件时,$sourcePath 目录的 filemtime 时间并不会改变,因此 hash 函数生成的哈希值也就没有变化。进一步,AssetManager 就没有再进行 publish 资源文件。

解决这个问题的一个方案就是更换生成哈希的实现。我做了一个简单的实现,在配置文件里面 components 项配置 assetManager 的 hashCallback 属性

$config = [
    /*...*/
    'components' => [
        /*...*/
        'assetManager' => [
            'hashCallback' => function ($path) {

                if (!function_exists('_myhash_')) {
                    function _myhash_($path) {
                        if (is_dir($path)) {
                            $handle = opendir($path);
                            $hash = '';
                            while (false !== ($entry = readdir($handle))) {
                                if ($entry === '.' || $entry === '..') {
                                    continue;
                                }
                                $entry = $path . '/' . $entry;
                                $hash .= _myhash_($entry);
                            }
                            $result = sprintf('%x', crc32($hash . Yii::getVersion()));
                        } else {
                            $result = sprintf('%x', crc32(filemtime($path) . Yii::getVersion()));
                        }
                        return $result;
                    }
                }

                return _myhash_($path);
            }
        ],
        /*...*/
    ],
    /*...*/
];