Forcing Script Order from registerScript Method in

2020-05-28 04:26发布

I created a widget that registers its own script like below

class MyWidget extends CWidget {
    public function run(){
        Yii::app()->clientScript->registerScript(__CLASS__, <<<JAVASCRIPT
var a = "Hello World!";
JAVASCRIPT
        , CClientScript::POS_END);
    }
}

And in the layout, I call the widget like this

<?php $this->widget('MyWidget');?>
<?php echo $content;?>

But in a view file, I need the variable declared by that widget.

<?php 
Yii::app()->clientScript->registerScript('script', <<<JAVASCRIPT
    alert(a);
JAVASCRIPT
    , CClientScript::POS_END);
?>

Note that in both registerScript method I use POS_END as the script position since I intend to put all of the scripts (including the CoreScript e.g. jQuery, jQueryUI etc) after the <body> tag.

The problem is that the rendered script will show the one from the view file and after that the one from the widget.

alert(a);
var a = "Hello World!";

As we can see, the above code won't work so I need to put the the second line above the first line.

Any idea on how to force the order? I'm okay with extending the CClientScript (and creating a new registerScript method) as long as all of the scripts will be rendered in the end position and I don't have to pull those inline Javascript codes above to a new package or file.

标签: php yii
5条回答
够拽才男人
2楼-- · 2020-05-28 04:30

So I finally find a hack to do this. I extend a new ClientScript class and modify the registerScript method so it will accept another param $level.

public function registerScript($id, $script, $position = self::POS_END, $level = 1);

Think about $level just like z-index in CSS, except the greater the number of $level is, the lower the position of the script will be.

For example

Yii::app()->clientScript->registerScript('script1', '/** SCRIPT #1 **/', CClientScript::POS_END, 1);
Yii::app()->clientScript->registerScript('script2', '/** SCRIPT #2 **/', CClientScript::POS_END, 2);
Yii::app()->clientScript->registerScript('script3', '/** SCRIPT #3 **/', CClientScript::POS_END, 1);

Even though script3 is declared after script2, in the rendered script, it will show above script2 since the $level value of script2 is greater than script3's.

Here's my solution's code. It's working like what I want although I'm not sure if the arranging method is optimized enough.

/**
 * ClientScript manages Javascript and CSS.
 */
class ClientScript extends CClientScript {
    public $scriptLevels = array();

    /**
     * Registers a piece of javascript code.
     * @param string $id ID that uniquely identifies this piece of JavaScript code
     * @param string $script the javascript code
     * @param integer $position the position of the JavaScript code.
     * @param integer $level the rendering priority of the JavaScript code in a position.
     * @return CClientScript the CClientScript object itself (to support method chaining, available since version 1.1.5).
     */
    public function registerScript($id, $script, $position = self::POS_END, $level = 1) {
        $this->scriptLevels[$id] = $level;
        return parent::registerScript($id, $script, $position);
    }

    /**
     * Renders the registered scripts.
     * Overriding from CClientScript.
     * @param string $output the existing output that needs to be inserted with script tags
     */
    public function render(&$output) {
        if (!$this->hasScripts)
            return;

        $this->renderCoreScripts();

        if (!empty($this->scriptMap))
            $this->remapScripts();

        $this->unifyScripts();

        //===================================
        //Arranging the priority
        $this->rearrangeLevels();
        //===================================

        $this->renderHead($output);
        if ($this->enableJavaScript) {
            $this->renderBodyBegin($output);
            $this->renderBodyEnd($output);
        }
    }


    /**
     * Rearrange the script levels.
     */
    public function rearrangeLevels() {
        $scriptLevels = $this->scriptLevels;
        foreach ($this->scripts as $position => &$scripts) {
            $newscripts = array();
            $tempscript = array();
            foreach ($scripts as $id => $script) {
                $level = isset($scriptLevels[$id]) ? $scriptLevels[$id] : 1;
                $tempscript[$level][$id] = $script;
            }
            foreach ($tempscript as $s) {
                foreach ($s as $id => $script) {
                    $newscripts[$id] = $script;
                }
            }
            $scripts = $newscripts;
        }
    }
}

So I just need to put the class as the clientScript component in the config

'components' => array(
  'clientScript' => array(
      'class' => 'ClientScript'
  )
)
查看更多
再贱就再见
3楼-- · 2020-05-28 04:31

I've contributed a patch to Yii to allow ordering (prepend, append, number)

you can find it here https://github.com/yiisoft/yii/pull/2263

if it did not get its place into you, then you can extend CClientScript to your own class to apply my changes

but in general properly written JS should work independent of order for example instead of defining some global scope vars you can assign them as properties to window (accessing a non-existing property does not throw an error in js)

查看更多
趁早两清
4楼-- · 2020-05-28 04:36

Try register to HEAD

Yii::app()->clientScript->registerScript(__CLASS__, <<<JAVASCRIPT
var a = "Hello World!";
JAVASCRIPT
, CClientScript::POS_HEAD);
查看更多
劫难
5楼-- · 2020-05-28 04:36

There is no need to extend CClienScript. A simple solution to this problem would be to create a $script array in your controller in your controller and add the view scripts to that variable. In your layout file You would register the scripts stored in your $scripts array after you have registered the ones you want to be rendered before it. e.g.:

in your controller

public $scripts = [];

In your layout:

$baseURL = Yii::app()->request->baseUrl;
$clientScript = Yii::app()->clientScript;
$clientScript->registerScriptFile( $baseURL . '/js/jquery.min.js', CClientScript::POS_END );
$clientScript->registerScriptFile( $baseURL . '/js/bootstrap.min.js', CClientScript::POS_END);

and in your view

<?php array_push($this->scripts,"/js/summernote.min.js");?>
查看更多
走好不送
6楼-- · 2020-05-28 04:45

Override the CClientScript class as @Petra said, here is the code I am using, after using @Petra code and didn't worked out for me.

class ClientScript extends CClientScript {

public $scriptLevels;

public function registerScript($id, $script, $position = null, $level = 1, array $htmlOptions = array()) {
    $this->scriptLevels[$id] = $level;
    return parent::registerScript($id, $script, $position, $htmlOptions);
}

public function render(&$output) {
    foreach ($this->scripts as $key => $value) {
        $this->scripts[$key] = $this->reorderScripts($value);
    }
    parent::render($output);
}

public function reorderScripts($element) {
    $tempScripts = array();
    $buffer = array();
    foreach ($element as $key => $value) {
        if (isset($this->scriptLevels[$key])) {
            $tempScripts[$this->scriptLevels[$key]][$key] = $value;
        }
    }
    ksort($tempScripts);
    foreach ($tempScripts as $value) {
        foreach ($value as $k => $v) {
            $buffer[$k] = $v;
        }
    }
    return $buffer;
}

}

and then, go to your config file, and in the components section edit the following:

'components' => array(
    .
    .
    'clientScript' => array(
        'class' => 'ClientScript'
    ),
)
查看更多
登录 后发表回答