Make WordPress Core


Ignore:
Timestamp:
06/26/2023 01:40:31 PM (8 months ago)
Author:
joemcgill
Message:

Script Loader: Add support for HTML 5 "async" and "defer" attributes.

This allows developers to register scripts with an intended loading strategy by changing the $in_footer parameter of wp_register_script and wp_enqueue_script to an array that accepts both an in_footer and strategy argument. If present, the loading strategy attribute will be added to the script tag when that script is printed to the page as long as it is not a dependency of any blocking scripts, including any inline scripts attached to the script or any of its dependents.

Props 10upsimon, thekt12, westonruter, costdev, flixos90, spacedmonkey, adamsilverstein, azaozz, mukeshpanchal27, mor10, scep, wpnook, vanaf1979, Otto42.
Fixes #12009.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-scripts.php

    r55703 r56033  
    135135
    136136    /**
     137     * Holds a mapping of dependents (as handles) for a given script handle.
     138     * Used to optimize recursive dependency tree checks.
     139     *
     140     * @since 6.3.0
     141     * @var array
     142     */
     143    private $dependents_map = array();
     144
     145    /**
     146     * Holds a reference to the delayed (non-blocking) script loading strategies.
     147     * Used by methods that validate loading strategies.
     148     *
     149     * @since 6.3.0
     150     * @var string[]
     151     */
     152    private $delayed_strategies = array( 'defer', 'async' );
     153
     154    /**
    137155     * Constructor.
    138156     *
     
    285303        }
    286304
    287         $src         = $obj->src;
    288         $cond_before = '';
    289         $cond_after  = '';
    290         $conditional = isset( $obj->extra['conditional'] ) ? $obj->extra['conditional'] : '';
     305        $src               = $obj->src;
     306        $strategy          = $this->get_eligible_loading_strategy( $handle );
     307        $intended_strategy = (string) $this->get_data( $handle, 'strategy' );
     308        $cond_before       = '';
     309        $cond_after        = '';
     310        $conditional       = isset( $obj->extra['conditional'] ) ? $obj->extra['conditional'] : '';
     311
     312        if ( ! $this->is_delayed_strategy( $intended_strategy ) ) {
     313            $intended_strategy = '';
     314        }
    291315
    292316        if ( $conditional ) {
     
    295319        }
    296320
    297         $before_handle = $this->print_inline_script( $handle, 'before', false );
    298         $after_handle  = $this->print_inline_script( $handle, 'after', false );
    299 
    300         if ( $before_handle ) {
    301             $before_handle = sprintf( "<script%s id='%s-js-before'>\n%s\n</script>\n", $this->type_attr, esc_attr( $handle ), $before_handle );
    302         }
    303 
    304         if ( $after_handle ) {
    305             $after_handle = sprintf( "<script%s id='%s-js-after'>\n%s\n</script>\n", $this->type_attr, esc_attr( $handle ), $after_handle );
    306         }
    307 
    308         if ( $before_handle || $after_handle ) {
    309             $inline_script_tag = $cond_before . $before_handle . $after_handle . $cond_after;
     321        $before_script = $this->get_inline_script_tag( $handle, 'before' );
     322        $after_script  = $this->get_inline_script_tag( $handle, 'after' );
     323
     324        if ( $before_script || $after_script ) {
     325            $inline_script_tag = $cond_before . $before_script . $after_script . $cond_after;
    310326        } else {
    311327            $inline_script_tag = '';
     
    334350            $srce = apply_filters( 'script_loader_src', $src, $handle );
    335351
    336             if ( $this->in_default_dir( $srce ) && ( $before_handle || $after_handle || $translations_stop_concat ) ) {
     352            if (
     353                $this->in_default_dir( $srce )
     354                && ( $before_script || $after_script || $translations_stop_concat || $this->is_delayed_strategy( $strategy ) )
     355            ) {
    337356                $this->do_concat = false;
    338357
     
    391410        }
    392411
    393         $tag  = $translations . $cond_before . $before_handle;
    394         $tag .= sprintf( "<script%s src='%s' id='%s-js'></script>\n", $this->type_attr, $src, esc_attr( $handle ) );
    395         $tag .= $after_handle . $cond_after;
     412        $tag  = $translations . $cond_before . $before_script;
     413        $tag .= sprintf(
     414            "<script%s src='%s' id='%s-js'%s%s></script>\n",
     415            $this->type_attr,
     416            $src, // Value is escaped above.
     417            esc_attr( $handle ),
     418            $strategy ? " {$strategy}" : '',
     419            $intended_strategy ? " data-wp-strategy='{$intended_strategy}'" : ''
     420        );
     421        $tag .= $after_script . $cond_after;
    396422
    397423        /**
     
    446472     *
    447473     * @since 4.5.0
    448      *
    449      * @param string $handle   Name of the script to add the inline script to.
     474     * @deprecated 6.3.0 Use methods get_inline_script_tag() or get_inline_script_data() instead.
     475     *
     476     * @param string $handle   Name of the script to print inline scripts for.
    450477     *                         Must be lowercase.
    451478     * @param string $position Optional. Whether to add the inline script
    452479     *                         before the handle or after. Default 'after'.
    453      * @param bool   $display  Optional. Whether to print the script
    454      *                         instead of just returning it. Default true.
    455      * @return string|false Script on success, false otherwise.
     480     * @param bool   $display  Optional. Whether to print the script tag
     481     *                         instead of just returning the script data. Default true.
     482     * @return string|false Script data on success, false otherwise.
    456483     */
    457484    public function print_inline_script( $handle, $position = 'after', $display = true ) {
    458         $output = $this->get_data( $handle, $position );
    459 
     485        _deprecated_function( __METHOD__, '6.3.0', 'WP_Scripts::get_inline_script_data() or WP_Scripts::get_inline_script_tag()' );
     486
     487        $output = $this->get_inline_script_data( $handle, $position );
    460488        if ( empty( $output ) ) {
    461489            return false;
    462490        }
    463491
    464         $output = trim( implode( "\n", $output ), "\n" );
    465 
    466492        if ( $display ) {
    467             printf( "<script%s id='%s-js-%s'>\n%s\n</script>\n", $this->type_attr, esc_attr( $handle ), esc_attr( $position ), $output );
    468         }
    469 
     493            echo $this->get_inline_script_tag( $handle, $position );
     494        }
    470495        return $output;
     496    }
     497
     498    /**
     499     * Gets data for inline scripts registered for a specific handle.
     500     *
     501     * @since 6.3.0
     502     *
     503     * @param string $handle   Name of the script to get data for.
     504     *                         Must be lowercase.
     505     * @param string $position Optional. Whether to add the inline script
     506     *                         before the handle or after. Default 'after'.
     507     * @return string Inline script, which may be empty string.
     508     */
     509    public function get_inline_script_data( $handle, $position = 'after' ) {
     510        $data = $this->get_data( $handle, $position );
     511        if ( empty( $data ) || ! is_array( $data ) ) {
     512            return '';
     513        }
     514
     515        return trim( implode( "\n", $data ), "\n" );
     516    }
     517
     518    /**
     519     * Gets unaliased dependencies.
     520     *
     521     * An alias is a dependency whose src is false. It is used as a way to bundle multiple dependencies in a single
     522     * handle. This in effect flattens an alias dependency tree.
     523     *
     524     * @since 6.3.0
     525     *
     526     * @param string[] $deps Dependency handles.
     527     * @return string[] Unaliased handles.
     528     */
     529    private function get_unaliased_deps( array $deps ) {
     530        $flattened = array();
     531        foreach ( $deps as $dep ) {
     532            if ( ! isset( $this->registered[ $dep ] ) ) {
     533                continue;
     534            }
     535
     536            if ( $this->registered[ $dep ]->src ) {
     537                $flattened[] = $dep;
     538            } elseif ( $this->registered[ $dep ]->deps ) {
     539                array_push( $flattened, ...$this->get_unaliased_deps( $this->registered[ $dep ]->deps ) );
     540            }
     541        }
     542        return $flattened;
     543    }
     544
     545    /**
     546     * Gets tags for inline scripts registered for a specific handle.
     547     *
     548     * @since 6.3.0
     549     *
     550     * @param string $handle   Name of the script to get associated inline script tag for.
     551     *                         Must be lowercase.
     552     * @param string $position Optional. Whether to get tag for inline
     553     *                         scripts in the before or after position. Default 'after'.
     554     * @return string Inline script, which may be empty string.
     555     */
     556    public function get_inline_script_tag( $handle, $position = 'after' ) {
     557        $js = $this->get_inline_script_data( $handle, $position );
     558        if ( empty( $js ) ) {
     559            return '';
     560        }
     561
     562        $id = "{$handle}-js-{$position}";
     563
     564        return wp_get_inline_script_tag( $js, compact( 'id' ) );
    471565    }
    472566
     
    716810
    717811    /**
     812     * This overrides the add_data method from WP_Dependencies, to support normalizing of $args.
     813     *
     814     * @since 6.3.0
     815     *
     816     * @param string $handle Name of the item. Should be unique.
     817     * @param string $key    The data key.
     818     * @param mixed  $value  The data value.
     819     * @return bool True on success, false on failure.
     820     */
     821    public function add_data( $handle, $key, $value ) {
     822        if ( ! isset( $this->registered[ $handle ] ) ) {
     823            return false;
     824        }
     825
     826        if ( 'strategy' === $key ) {
     827            if ( ! empty( $value ) && ! $this->is_delayed_strategy( $value ) ) {
     828                _doing_it_wrong(
     829                    __METHOD__,
     830                    sprintf(
     831                        /* translators: 1: $strategy, 2: $handle */
     832                        __( 'Invalid strategy `%1$s` defined for `%2$s` during script registration.' ),
     833                        $value,
     834                        $handle
     835                    ),
     836                    '6.3.0'
     837                );
     838                return false;
     839            } elseif ( ! $this->registered[ $handle ]->src && $this->is_delayed_strategy( $value ) ) {
     840                _doing_it_wrong(
     841                    __METHOD__,
     842                    sprintf(
     843                        /* translators: 1: $strategy, 2: $handle */
     844                        __( 'Cannot supply a strategy `%1$s` for script `%2$s` because it is an alias (it lacks a `src` value).' ),
     845                        $value,
     846                        $handle
     847                    ),
     848                    '6.3.0'
     849                );
     850                return false;
     851            }
     852        }
     853        return parent::add_data( $handle, $key, $value );
     854    }
     855
     856    /**
     857     * Gets all dependents of a script.
     858     *
     859     * @since 6.3.0
     860     *
     861     * @param string $handle The script handle.
     862     * @return string[] Script handles.
     863     */
     864    private function get_dependents( $handle ) {
     865        // Check if dependents map for the handle in question is present. If so, use it.
     866        if ( isset( $this->dependents_map[ $handle ] ) ) {
     867            return $this->dependents_map[ $handle ];
     868        }
     869
     870        $dependents = array();
     871
     872        // Iterate over all registered scripts, finding dependents of the script passed to this method.
     873        foreach ( $this->registered as $registered_handle => $args ) {
     874            if ( in_array( $handle, $args->deps, true ) ) {
     875                $dependents[] = $registered_handle;
     876            }
     877        }
     878
     879        // Add the handles dependents to the map to ease future lookups.
     880        $this->dependents_map[ $handle ] = $dependents;
     881
     882        return $dependents;
     883    }
     884
     885    /**
     886     * Checks if the strategy passed is a valid delayed (non-blocking) strategy.
     887     *
     888     * @since 6.3.0
     889     *
     890     * @param string $strategy The strategy to check.
     891     * @return bool True if $strategy is one of the delayed strategies, otherwise false.
     892     */
     893    private function is_delayed_strategy( $strategy ) {
     894        return in_array(
     895            $strategy,
     896            $this->delayed_strategies,
     897            true
     898        );
     899    }
     900
     901    /**
     902     * Gets the best eligible loading strategy for a script.
     903     *
     904     * @since 6.3.0
     905     *
     906     * @param string $handle The script handle.
     907     * @return string The best eligible loading strategy.
     908     */
     909    private function get_eligible_loading_strategy( $handle ) {
     910        $eligible = $this->filter_eligible_strategies( $handle );
     911
     912        // Bail early once we know the eligible strategy is blocking.
     913        if ( empty( $eligible ) ) {
     914            return '';
     915        }
     916
     917        return in_array( 'async', $eligible, true ) ? 'async' : 'defer';
     918    }
     919
     920    /**
     921     * Filter the list of eligible loading strategies for a script.
     922     *
     923     * @since 6.3.0
     924     *
     925     * @param string              $handle   The script handle.
     926     * @param string[]|null       $eligible Optional. The list of strategies to filter. Default null.
     927     * @param array<string, true> $checked  Optional. An array of already checked script handles, used to avoid recursive loops.
     928     * @return string[] A list of eligible loading strategies that could be used.
     929     */
     930    private function filter_eligible_strategies( $handle, $eligible = null, $checked = array() ) {
     931        // If no strategies are being passed, all strategies are eligible.
     932        if ( null === $eligible ) {
     933            $eligible = $this->delayed_strategies;
     934        }
     935
     936        // If this handle was already checked, return early.
     937        if ( isset( $checked[ $handle ] ) ) {
     938            return $eligible;
     939        }
     940
     941        // Mark this handle as checked.
     942        $checked[ $handle ] = true;
     943
     944        // If this handle isn't registered, don't filter anything and return.
     945        if ( ! isset( $this->registered[ $handle ] ) ) {
     946            return $eligible;
     947        }
     948
     949        // If the handle is not enqueued, don't filter anything and return.
     950        if ( ! $this->query( $handle, 'enqueued' ) ) {
     951            return $eligible;
     952        }
     953
     954        $is_alias = (bool) ! $this->registered[ $handle ]->src;
     955        $intended_strategy = $this->get_data( $handle, 'strategy' );
     956
     957        // For non-alias handles, an empty intended strategy filters all strategies.
     958        if ( ! $is_alias && empty( $intended_strategy ) ) {
     959            return array();
     960        }
     961
     962        // Handles with inline scripts attached in the 'after' position cannot be delayed.
     963        if ( $this->has_inline_script( $handle, 'after' ) ) {
     964            return array();
     965        }
     966
     967        // If the intended strategy is 'defer', filter out 'async'.
     968        if ( 'defer' === $intended_strategy ) {
     969            $eligible = array( 'defer' );
     970        }
     971
     972        $dependents = $this->get_dependents( $handle );
     973
     974        // Recursively filter eligible strategies for dependents.
     975        foreach ( $dependents as $dependent ) {
     976            // Bail early once we know the eligible strategy is blocking.
     977            if ( empty( $eligible ) ) {
     978                return array();
     979            }
     980
     981            $eligible = $this->filter_eligible_strategies( $dependent, $eligible, $checked );
     982        }
     983
     984        return $eligible;
     985    }
     986
     987    /**
     988     * Gets data for inline scripts registered for a specific handle.
     989     *
     990     * @since 6.3.0
     991     *
     992     * @param string $handle   Name of the script to get data for. Must be lowercase.
     993     * @param string $position The position of the inline script.
     994     * @return bool Whether the handle has an inline script (either before or after).
     995     */
     996    private function has_inline_script( $handle, $position = null ) {
     997        if ( $position && in_array( $position, array( 'before', 'after' ), true ) ) {
     998            return (bool) $this->get_data( $handle, $position );
     999        }
     1000
     1001        return (bool) ( $this->get_data( $handle, 'before' ) || $this->get_data( $handle, 'after' ) );
     1002    }
     1003
     1004    /**
    7181005     * Resets class properties.
    7191006     *
Note: See TracChangeset for help on using the changeset viewer.