擴充套件包原始碼解析 - PHP-Vars-To-JS-Transformer

心智極客發表於2019-10-15

上一篇 文章對 spatie/laravel-tail 這一擴充套件包進行了分析,受到不少網友的好評,今天繼續講解如何實現 PHP-Vars-To-Js-Transformer 這一擴充套件包。

介紹

laracasts 出品的 PHP-Vars-To-Js-Transformer 包可自動將 PHP 變數轉化為 JavaScript 變數。

使用說明

JavaScript::put('foo', 'bar');  
// window.foo = "bar";

JavaScript::put([
    'foo' => 'bar',
    'age' => 29
]);  
// window.foo = "bar";window.age = 29;"

實現

PHP-Vars-To-Js-Transformer 擴充套件包主要完成兩項工作

  1. 將 PHP 變數轉化為 JavaScript 變數;
  2. 利用檢視事件自動輸出 JavaScript 變數;

定義類,該類傳入需要繫結的檢視以及 JavaScript 變數的作用域

namespace App\Services\JavaScript;

class Transformer
{   
    /**
     * 名稱空間
     * 
     * @var string
     */
    private $namespace;

    /**
     * 要輸出變數的檢視
     * 
     * @var array
     */
    private $views;

    function __construct($views, string $namespace = 'window')
    {
        $this->namespace = $namespace;
        $this->views = str_replace('/', '.', (array)$views);
    }

    // 主要業務邏輯,todo
    public function put()
    {

    }

轉化變數

put 函式支援兩種型別的傳參

  • JavaScript::put('foo', 'bar')
  • JavaScript::put($arr)

這兩種傳參都需要標準化成陣列

use InvalidArgumentException;

public function put()
{
    $input = $this->normalizeInput(func_get_args());
}

/**
 * 格式化輸入
 * 
 * @param  string | array $variables 
 * @throws InvalidArgumentException
 * @return array
 */
public function normalizeInput($arguments) : array
{   
    if( is_array($arguments[0]) ){
        return $arguments[0];
    }

    if(count($arguments) == 2){
        return [
            $arguments[0] => $arguments[1]
        ];
    }

    throw new InvalidArgumentException('引數輸入錯誤');
}

將輸入標準化成陣列後,還需要進一步將其轉化為 JavaScript 變數,關鍵點在於 PHP 變數的值可能有多種型別(基本型別、陣列、物件等等),需要進行不同的處理

use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
use JsonSerializable;

public function put()
{
    $input = $this->normalizeInput(func_get_args());
    $js = $this->constructJavaScript($input);
}

/**
 * 轉化 PHP 變數
 * 
 * @param  array $variables PHP 變數
 * @return string
 */
public function constructJavaScript($variables)
{
    return collect($variables)->map(function($data, $key){
        return "{$this->namespace}.{$key} = ".$this->convertToJavaScript($data).";";
    })->implode('');
}

/**
 * 格式化值
 * 
 * @param mix $data 
 * 
 * @return mix
 */
public function convertToJavaScript($data)
{
    if($data instanceof Jsonable){
        return $data->toJson();
    } 

    if ($data instanceof JsonSerializable) {
        return json_encode($data->jsonSerialize());
    }

    if ($data instanceof Arrayable) {
        return json_encode($data->toArray());
    }

    return json_encode($data);
}

當名稱空間不為 window 時,還需要新增額外的宣告語句。例如,傳入的作用域為 laravel,則需要新增宣告語句 window.laravel = window.laravel || {};,具體實現如下


public function put()
{
    $input = $this->normalizeInput(func_get_args());
    $js = $this->constructJavaScript($input);
    $js = $this->constructNamespace().$js;
}

/**
 * 構造名稱空間
 * 
 * @return string
 */
public function constructNamespace() : string
{
    if($this->namespace == 'window'){
        return '';
    }

    return "window.{$this->namespace} = window.{$this->namespace} || {};";
}

輸出變數

為繫結的檢視新增對應的事件,以便自動輸出變數

public function put()
{   
    $input = $this->normalizeInput(func_get_args());
    $js = $this->constructJavaScript($input);
    $js = $this->constructNamespace().$js;

    foreach ($this->views as $view) {
        app('events')->listen("composing: {$view}", function () use ($js) {
            echo "<script>{$js}</script>";
        });
    }
}

完整程式碼

<?php

namespace App\Services\JavaScript;

use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
use InvalidArgumentException;
use JsonSerializable;

class Transformer
{   
    /**
     * 名稱空間
     * 
     * @var string
     */
    private $namespace;

    /**
     * 要輸出變數的檢視
     * 
     * @var array
     */
    private $views;

    function __construct($views, string $namespace = 'window')
    {
        $this->namespace = $namespace;
        $this->views = str_replace('/', '.', (array)$views);
    }

    public function put()
    {   
        $input = $this->normalizeInput(func_get_args());
        $js = $this->constructJavaScript($input);
        $js = $this->constructNamespace().$js;

        foreach ($this->views as $view) {
            app('events')->listen("composing: {$view}", function () use ($js) {
                echo "<script>{$js}</script>";
            });
        }
    }

    /**
     * 轉化 PHP 變數
     * 
     * @param  array $variables PHP 變數
     * @return string
     */
    public function constructJavaScript(array $variables) : string
    {
        return collect($variables)->map(function($data, $key){
            return "{$this->namespace}.{$key} = ".$this->convertToJavaScript($data).";";
        })->implode('');
    }

    /**
     * 格式化值
     * 
     * @param mix $data 
     * 
     * @return mix
     */
    public function convertToJavaScript($data)
    {
        if($data instanceof Jsonable){
            return $data->toJson();
        } 

        if ($data instanceof JsonSerializable) {
            return json_encode($data->jsonSerialize());
        }

        if ($data instanceof Arrayable) {
            return json_encode($data->toArray());
        }

        return json_encode($data);
    }

    /**
     * 格式化輸入
     * 
     * @param  string | array $variables 
     * @throws InvalidArgumentException
     * @return array
     */
    public function normalizeInput($arguments) : array
    {   
        if( is_array($arguments[0]) ){
            return $arguments[0];
        }

        if(count($arguments) == 2){
            return [
                $arguments[0] => $arguments[1]
            ];
        }

        throw new InvalidArgumentException('引數輸入錯誤');
    }

    /**
     * 構造名稱空間
     * 
     * @return string
     */
    public function constructNamespace() : string
    {
        if($this->namespace == 'window'){
            return '';
        }

        return "window.{$this->namespace} = window.{$this->namespace} || {};";
    }
}

執行

$input = [
    'foo' => 'bar',
    'age' => 29,
    'collection' => collect(['aaa', 'bbb', 'ccc'])
];

$views = ['footer'];

$transform = new Transformer($views, 'laravel');
$transform->put($input);

最佳化

Transformer 類主要完成了兩項工作:轉化變數以及繫結變數到檢視。該類的實現違反了 開放-封閉 原則

實體(類、方法等)應當對擴充套件開放,對修改封閉。

對於繫結變數到檢視這一行為而言,不同的框架有不同的實現方法,是可變的行為,應當將其分離出來,隱藏於介面背後。

<?php

namespace App\Services\JavaScript;

interface ViewBinderInterface
{
    /**
     * 繫結變數到檢視
     *
     * @param string $js
     */
    public function bind($js);
}

Laravel 對繫結變數到檢視這一行為的實現

<?php

namespace App\Services\JavaScript;

use Illuminate\Contracts\Events\Dispatcher;

class ViewBinder implements ViewBinderInterface
{
    /**
     * 事件分發器
     * 
     * @var Dispatcher
     */
    protected $event;

    /**
     * 變數繫結的檢視
     *
     * @var string
     */
    protected $views;

    /**
     * Create a new Laravel view binder instance.
     *
     * @param Dispatcher   $event
     * @param string|array $views
     */
    public function __construct(Dispatcher $event, $views)
    {
        $this->event = $event;
        $this->views = str_replace('/', '.', (array)$views);
    }

    /**
     * Bind the given JavaScript to the view.
     *
     * @param string $js
     */
    public function bind($js)
    {
        foreach ($this->views as $view) {
            $this->event->listen("composing: {$view}", function () use ($js) {
                echo "<script>{$js}</script>";
            });
        }
    }
}

Transformer 類就可以簡化成

<?php

namespace App\Services\JavaScript;

use App\Services\JavaScript\ViewBinderInterface;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
use InvalidArgumentException;
use JsonSerializable;

class Transformer
{   
    /**
     * 名稱空間
     * 
     * @var string
     */
    private $namespace;

    /**
     * @var App\Services\JavaScript\ViewBinderInterface
     */
    private $viewBinder;

    function __construct(ViewBinderInterface $viewBinder, string $namespace = 'window')
    {
        $this->namespace = $namespace;
        $this->viewBinder = $viewBinder;
    }

    public function put()
    {   
        $js = $this->constructNamespace().$this->constructJavaScript($this->normalizeInput(func_get_args()));
        $this->viewBinder->bind($js);

        return $js;
    }

}

執行

$input = [
    'foo' => 'bar',
    'age' => 29,
    'hello' => collect(['aaa', 'bbb', 'ccc'])
];

$viewBinder = new ViewBinder(
    app('events'), 
    ['footer']
);
$transform = new Transformer($viewBinder, 'laravel');
$transform->put($input);

整合到 Laravel 專案中

註冊容器

public function register()
{
    $this->app->singleton('JavaScript', function ($app) {
        return new Transformer(
            new ViewBinder($app['events'], ['footer']),
            'window'
        );
    });
}

建立門面

<?php

namespace App\Services\JavaScript;

use Illuminate\Support\Facades\Facade;

class JavaScriptFacade extends Facade
{
    protected static function getFacadeAccessor()
    {
        return 'JavaScript';
    }
}

註冊門面

'JavaScript' => App\Services\JavaScript\JavaScriptFacade::class,
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章