首页 > 解决方案 > 尝试异步 Javascript,无法正常工作。更新

问题描述

更新:我对 JS 承诺没有完全理解,所以我删除了该代码,并添加了一个通过 AJAX 而不是脚本注入启动服务器进程的选项,但除非我在服务器上分叉 php 进程,否则它仍然会阻塞。我已将下面的代码块替换为文件的更新版本,它允许所有四种方法(脚本注入或 AJAX,以及分叉或非分叉)。要运行它,需要一个包含 pcntl 模块的 php 构建(最新的 homebrew 可访问的似乎是 5.6)。

这是一个完整的 php 脚本,用于使用 HTML5 和 CSS 实现为异步 javascript 进度条生成测试页面。测试运行没有错误,但我无法让它加载 progress.js 文件并运行它,更新进度条。间隔终止后控制台会更新,但在此期间浏览器中没有任何反应。在当前版本的 Firefox 和 Chrome 中进行了测试,结果相同。

注意:在生成 HTML 的代码中使用 heredoc 语法只是为了方便代码折叠,因为我的编辑器不能为此目的在同一文档中识别 php 和 javascript 语法。

<?php   //  UPDATED: 2020.05.06 - 12.36

if( isset($_SERVER['QUERY_STRING']) ){
    $qry = $_SERVER['QUERY_STRING'];
    if( $qry == 'pi' ) phpinfo();
    else{
        file_put_contents( 'progress.js', '' );
        header('Content-Type: text/javascript');
        if( $qry == 'fork' ){
            if( function_exists('pcntl_fork') ) $pid = pcntl_fork();
            else $pid = false;
            if( $pid === -1 ) die('could not fork');
            else if( $pid  > 0 ) file_put_contents('__fork_pid', $pid);
            else if( $pid === 0 ){
                prog('timer = 0');
                for( $x = 0; $x < 101; $x++ ){  //  Simulate a task taking an indeterminate amount of time
                    $s = rand(0,10) * 25000;    //      E.g. copying a set number of files
                    usleep( $s );               //      Timer value would reflect percentage of file transfers completed
                    prog("timer = $x");         //      Update the timer after each file is copied
                }
                if( file_exists('__fork_pid') ){
                    $pid = trim( file_get_contents('__fork_pid') );
                    unlink('__fork_pid');
                    `kill $pid`;
                }
            }
            else echo "console.log('fork failed!'); progress(0);";
        }
        else{
            prog('timer = 0');
            for( $x = 0; $x < 101; $x++ ){
                $s = rand(0,10) * 25000;
                usleep( $s );
                prog("timer = $x");
            }
        }
    }
}
else{
    echo /* HTML OPEN */ (<<<ENDINDEX
<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Async Progress Bar Test</title>
        <script>
            var prgs, timer = 0;
ENDINDEX
    ).chr(10)
    /* JS doAsyncTask */.(<<<ENDINDEX
            function doAsyncTask(fork){
                document.querySelector('#status').textContent = 'Running...';
                setTimeout( () => {
                    progress(0);
                    if( fork < 2 ) scload('index.php?'+(fork==1?'fork':'nofork'));
                    else doAJAX(fork==3?'fork':'nofork');
                }, 100,  );
                prgs = setInterval( () => {
                    progress(timer++); scload('progress.js');
                }, 250 );
                return 'doing asynchronous task...';
            }
ENDINDEX
    ).chr(10)
    /* JS doAJAX */     .(<<<ENDINDEX
            function doAJAX(opt){                  //  This method appears NOT to be non-blocking, so fails.
                ajax = new XMLHttpRequest();
                ajax.open('GET', 'index.php?'+opt, true);
                ajax.send();
                ajax.onreadystatechange = function(){ if (ajax.readyState == 4) progress(ajax.responseText) };
            }
ENDINDEX
    ).chr(10)
    /* JS scload */     .(<<<ENDINDEX
            function scload(src){
                if(document.querySelector('#scriptloader'))
                    document.querySelector('#scriptloader').remove();
                let s = document.createElement('script');
                s.id = 'scriptloader';
                s.setAttribute('async',true);
                s.src = src;
                document.body.appendChild(s);
            }
ENDINDEX
    ).chr(10)
    /* JS progress */   .(<<<ENDINDEX
            function progress(t=false){
                if(t!==false && parseInt(t)>=0) timer = t;
                document.querySelector('#progress').classList.toggle('hide',false);
                document.querySelector('#status').textContent = 'Running... '+timer+'%';
                if( timer >= 100 ){
                    clearInterval(prgs);
                    document.querySelector('#status').textContent = 'Complete.';
                    document.querySelector('#progress').classList.toggle('hide',true);
                    timer = 0;
                }
                document.querySelector('#progress span').style.width = timer+'%';
            }
ENDINDEX
    ).chr(10)
    /* CSS STYLE */     .(<<<ENDINDEX
        </script>
        <style>
            body { background: #fff; }
            button { display: inline-block; margin: 20px; width: 200px; height: 24px; font-weight: bold; 
                background: #eee; border-radius: 8px; }
            button:active { background: #ccc; }
            #progress { position: relative; width: 450px; height: 20px; background: #aaa; margin: 20px;
                border-radius: 10px; border: 1px inset #000; box-sizing: border-box; overflow: hidden; }
            #progress > span { position: absolute; height: 100%; background: #88f;
                box-sizing: border-box; border-radius: 10px 0 0 10px; }
            #status { margin: 20px; }
        </style>
    </head>
ENDINDEX
    ).chr(10)
    /* HTML BODY */     .(<<<ENDINDEX
    <body>
        <button onclick="doAsyncTask(1)">Start SCLOAD (fork)</button>
        <button onclick="doAsyncTask(0)">Start SCLOAD (no fork)</button> <br>
        <button onclick="doAsyncTask(3)">Start AJAX (fork)</button>
        <button onclick="doAsyncTask(2)">Start AJAX (no fork)</button>
        <div id="status">&nbsp;</div>
        <div id="progress"><span></span></div>
    </body>
</html>
ENDINDEX
    );
}

function prog($do,$complete=false){
    $action = "$do; \n";
    file_put_contents( 'progress.js', $action );
}

标签: javascript

解决方案


您可以简单地让该脚本立即返回当前进度,并且只需一个 setInterval XHR 循环轮询服务器以获取当前进度。

如果您正在尝试测量上传进度,https: //codepen.io/PerfectIsShit/pen/zogMXP 是使用该progress事件的一个很好的演示。 xhr.upload.addEventListener("progress", progressHandler, false);.

如果您想从服务器推送实时更新,您可以查看 SSE/EventSource。 https://www.html5rocks.com/en/tutorials/eventsource/basics/

我不完全确定您正在解决什么问题或您想要实现什么,但我已经概述了下面的所有内容。您似乎误解了“阻塞”和异步的含义。您似乎希望将流式处理和长轮询行为作为 XHR 和 PHP 页面的神奇内置功能,这与异步并不相同。这不是异步和“非阻塞”所指的。这两个术语是指“阻塞”,因为执行 Javascript 语句while(true){}将“阻塞”执行并锁定您的浏览器,或者XHR.send('url','GET',async=false)会导致 Javascript 执行流程在该语句处停止,直到 XHR 请求完成(以及冻结浏览器)。


您尝试执行的操作将需要 XHR 流式传输或 ReadableStream 并获取,并且类似于长轮询或分块编码轮询。
通常,当您需要服务器推送时,您会考虑这一点。您可以查看 SSE/EventSource 和 Websockets。另一个用例是大型下载/上传或提交时需要一些处理时间的东西。
我不推荐您采用的方法,因为它依赖于无限期保持 XHR 连接,这违反了一般设计,并且您正在建模的问题类别的解决方案具有更简单的解决方案。如果您必须这样做,您可能需要查看一些库来为您执行此操作。

解决 OP 中提出的问题的最佳方法是决定用什么衡量进展并返回它。您可以拥有一个服务器守护程序来更新您的服务器端 API 将返回的值。对于 OP 中的问题,您可以使用 $_SESSION 并存储时间,然后使用它来计算进度。

至于“阻塞”的问题,UI 模式通常是有某种“正在加载”的指示符,它会一直持续到超时。您将发出一个等待的异步请求,然后您的 UI 可以在请求解析时继续更新,并且依赖于数据的代码可以继续执行。或者它拒绝,或者花费太长时间,你处理错误。

Promise 就像将回调包装在对象中一样。调用一个异步函数,并立即返回一个 Promise,然后可以使用这个 Promise 来使用.thenand来挂钩异步请求的完成.reject
例如:

var aPromise = myAsyncFetchRequest();
function callbackHandler(fetchReturnValue){ /* take my data and do something with it*/  }
var promise2 = aPromise.then(callbackHandler);
// do stuff ... stuff that can continue to execute while the Promise resolves
// stuff that could take aPromise and pass it to other functions or add on more handlers
promise2.then(anotherCallbackHandler);

我又看了一遍,您的 PHP 代码看起来就是问题所在。该代码看起来不起作用。对 index.php?start 的第一个请求不会终止。

我强烈建议避免使用脚本注入进行数据传输,除非您有特殊需要。


我已经对您的代码进行了快速模拟演示。它似乎工作正常。我会检查您的网络流量以确保正在下载脚本。您可以在进度调用中放置“调试器”断点,以尝试查看它到达的位置并遍历执行流程。
虽然我强烈建议不要使用单个文件来处理进度,并且可能尝试在 Javascript 和 HTML 和 PHP 代码之间创建更好的分离。

我所做的是模拟您的服务器端代码,并将脚本插入的 src 替换为从数组内容的伪造文件中提取的文本。“逻辑”似乎是合理的,如果有点不稳定的话。

<script>

var files={};

function file_put_contents(file, x){ files[file]=x }

function progress_php($do,$complete=false){
    $action = `${$do}; \n`;
    if( $complete ) $action = "clearInterval(prgs);\n" + $action;
    file_put_contents( 'progress.js', $action );
}

var ii=0;

files['index.php?start']='progress(0);start()';
function start(){setInterval(()=> (ii++>=100?progress_php(`progress(${ii})`,true):progress_php(`progress(${ii})`)),100);
}

// dirty mock server side code
</script>
<script>
            function doAsync( task ){
                document.querySelector('#status').textContent = 'Running...';
                setTimeout( () => { scload('index.php?start') }, 100 );
                return new Promise( (resolve,reject) => {
                    resolve( prgs = setInterval( () => { scload('progress.js') }, 1000 ) );
                } );
            }
            </script>
    <script>
            function scload(src){
console.log(files[src]);
                if(document.querySelector('#scriptloader'))
                    document.querySelector('#scriptloader').remove();
                let s = document.createElement('script');
                s.id = 'scriptloader';
//                 s.setAttribute('async',true);
                s.text = files[src];
                document.body.appendChild(s);
            }
            </script>
    <script>
            function progress(x){
            console.log(x);
                document.querySelector('.progress').classList.toggle('hide',true);
                if( x >= 100 ){
                    x = 100;
                    document.querySelector('#status').textContent = 'Complete.';
                }
                document.querySelector('progress').value = x;
                document.querySelector('#progress span').style.width = x+'%';
                document.querySelector('.progress').classList.toggle('hide',false);
            }
            </script>
        <style>
            body { background: #fff; }
            progress { width: 500px; height: 40px; display: block; }
            .progress.hide { visibility: hidden; }
            #progress { position: relative; width: 500px; height: 20px; background: #aaa;
                border-radius: 10px; border: 1px inset #000; box-sizing: border-box; overflow: hidden; }
            #progress > span { position: absolute; height: 100%; background: #88f;
                box-sizing: border-box; border-radius: 10px 0 0 10px; }
        </style>
    </head>
    <body>
        <div class="progress">
            <progress class="" value="" max="100"></progress>
            <div id="progress"><span></span></div><br>
        </div>
        <button onclick="doAsync()">Start</button>
        &nbsp; &nbsp;
        <span id="status"></span>
    </body>
</html>


推荐阅读