Skip to content

Instantly share code, notes, and snippets.

@moorscode
Last active July 1, 2020 11:25
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save moorscode/b04e648200c844bf3f6d to your computer and use it in GitHub Desktop.
Save moorscode/b04e648200c844bf3f6d to your computer and use it in GitHub Desktop.
Add inline local script fallback for remotely loaded scripts in WordPress
<?php
/**
* wp_register_script wrapper with local fallback
*
* @param $handle
* @param $src
* @param $js_test JavaScript code to test for availability of object
* @param bool|false $local_src Load source file if test fails
* @param array $deps
* @param bool|false $ver
* @param bool|false $in_footer
*/
function wp_register_script_local_fallback( $handle, $src, $js_test, $local_src = false, $deps = array(), $ver = false, $in_footer = false ) {
wp_register_script( $handle, $src, $deps, $ver, $in_footer );
script_local_fallback::register_fallback( $handle, $src, $js_test, $local_src );
}
/**
* wp_enqueue_script wrapper with local fallback
*
* @param $handle
* @param $src
* @param $js_test JavaScript code to test for availability of object
* @param bool|false $local_src Load source file if test fails
* @param array $deps
* @param bool|false $ver
* @param bool|false $in_footer
*/
function wp_enqueue_script_local_fallback( $handle, $src, $js_test, $local_src = false, $deps = array(), $ver = false, $in_footer = false ) {
wp_enqueue_script( $handle, $src, $deps, $ver, $in_footer );
script_local_fallback::register_fallback( $handle, $src, $js_test, $local_src );
}
/**
* wp_deregister_script wrapper for cleaning up local fallback
*
* @param $handle
*/
function wp_deregister_script_local_fallback( $handle ) {
wp_deregister_script( $handle );
script_local_fallback::deregister_fallback( $handle );
}
class script_local_fallback {
private static $fallbacks = [ ];
private static $registered_hook = false;
/**
* Registers a local fallback for a handle
*
* @param $handle
* @param $src
* @param $js_test
* @param bool|false $local_src
*/
public static function register_fallback( $handle, $src, $js_test, $local_src = false ) {
/**
* uses get_stylesheet_directory_uri instead of get_template_uri for child-theme support
*/
$local_src = empty( $local_src ) ? get_stylesheet_directory_uri() . '/js/' . basename( $src ) : $local_src;
self::$fallbacks[ $handle ] = array(
'test' => $js_test,
'local' => $local_src
);
if ( ! self::$registered_hook ) {
add_action( 'script_loader_tag', 'script_local_fallback::script_loader_tag', 10, 3 );
self::$registered_hook = true;
}
}
/**
* Clean up registered fallback
*
* @param $handle
*/
public static function deregister_fallback( $handle ) {
unset( self::$fallbacks[ $handle ] );
}
/**
* Add additional local fallback code to script tag
*
* @param $tag
* @param $handle
* @param $src
*
* @return string
*/
public static function script_loader_tag( $tag, $handle, $src ) {
if ( isset( self::$fallbacks[ $handle ] ) ) {
$data = self::$fallbacks[ $handle ];
$tag .= <<<EO_SCRIPT
<script type="text/javascript">{$data['test']} || document.write('<script src="{$data['local']}"><\/script>');</script>
EO_SCRIPT;
}
return $tag;
}
}
@rwilsond
Copy link

@bryanwillis - Does your customized function work independently from the full function that @moorscode wrote, or does yours replace part of his code?

i.e. I'm using the same two CDN's for the exact same two scripts on my site. Could I add just your 'my_register_jquery_cdn_local_fallback' to my functions.php, or do I have to add everything else too?

Thanks!

@bryanwillis
Copy link

@rwilsond sorry for the delayed reply. I never saw your reply until now. Unfortunately, my original comment wouldn't have helped you as I didn't look very long at how this worked and was doing it wrong. You have to pass a value for $js_test which I had left out originally.

Examples of possible values for $js_test:
jQuery: window.jQuery
Modernizr: window.Modernizr
Bootstrap: window.jQuery.fn.modal

Here's a more complete example that adds Modernizr and jQuery with local fallbacks and allows for script debugging just like wordpress core would. It also moves the scripts to the footer (beside modernizr which is required in the head). This should work well however there are a few plugins like Gravity Forms and Revslider that don't quite do things right and aren't compatible with moving scripts to the footer. I'm using jsdeliver, because it lets you compress and combine scripts together which is pretty cool. Using this below has sped up my site a ton.

<?php
/**
 * Functions.php File
 */

// Path the the enqueue local fallback script
include_once( get_stylesheet_directory() . '/includes/script_local_fallback.php');

function register_theme_scripts_cdn_fallback() {

  // Script Versions
  $jquery_version = wp_scripts()->registered['jquery']->ver;
  $migrate_version = wp_scripts()->registered['jquery-migrate']->ver;

  // Script Debugging
  if (defined('SCRIPT_DEBUG') && SCRIPT_DEBUG) {
    $modernizr = '(modernizr.js)';
    $migrate_url = '(jquery-migrate.js)';
  }
  else {
    $modernizr = '';
    $migrate_url = '';
  }

  // Modernizr (html5shiv is included in modernizr)
  wp_enqueue_script_local_fallback('modernizr', 'https://cdn.jsdelivr.net/g/modernizr@2.8.3' . $modernizr_url . ',detectizr@2.2.0', 'window.Modernizr', get_stylesheet_directory_uri() . '/assets/js/vendor/modernizr/modernizr' . $modernizr_url . '.js', array() , false, false);

  // Deregister default jquery
  wp_deregister_script('jquery');
  wp_deregister_script('jquery-migrate');

  // Register CDN jquery with fallback
  wp_register_script('jquery-migrate', false);
  wp_register_script_local_fallback('jquery', 'https://cdn.jsdelivr.net/g/jquery@' . $jquery_version . ',jquery.migrate@' . $migrate_version . $migrate_url, 'window.jQuery', includes_url('/js/jquery/jquery.js') , array() , false, true);
  wp_enqueue_script('jquery');

  // Move scripts to footer
  foreach(wp_scripts()->registered as $script) {
    if ('modernizr' == $script->handle) {
      wp_script_add_data($script->handle, 'group', 0);
    }
    else {
      wp_script_add_data($script->handle, 'group', 1);
    }
  }
}
add_action('wp_enqueue_scripts', 'register_theme_scripts_cdn_fallback', 100);

@bryanwillis
Copy link

Also, to answer your question @rwilsond, technically you don't need all that code that @moorscode wrote, it just makes things easier down the road if you plan on trying to add local fallbacks to scripts frequently. I believe his code was adapted from the Roots/Soil plugin on github, which did something like this:

function register_scripts_fallback() {
  $jquery_version = wp_scripts()->registered['jquery']->ver;
  $migrate_version = wp_scripts()->registered['jquery-migrate']->ver;
  wp_deregister_script('jquery');
  wp_deregister_script('jquery-migrate');
  wp_register_script('jquery-migrate', false);
  wp_register_script( 'jquery', 'https://cdn.jsdelivr.net/g/jquery@' . $jquery_version . ',jquery.migrate@' . $migrate_version, array(), null, true );
  wp_enqueue_script('jquery');
  add_filter('script_loader_src', 'jquery_local_fallback', 10, 2);
}
add_action('wp_enqueue_scripts', 'register_scripts_fallback', 100);

function jquery_local_fallback($src, $handle = null) {
  static $add_jquery_fallback = false;
  if ($add_jquery_fallback) {
    echo '<script>window.jQuery || document.write(\'<script src="' . $add_jquery_fallback .'"><\/script>\')</script>' . "\n";
    $add_jquery_fallback = false;
  }
  if ($handle === 'jquery') { $add_jquery_fallback = apply_filters('script_loader_src', , 'jquery-fallback'); }
  return $src;
}
add_action('wp_head', . 'jquery_local_fallback');

While it's shorter you'd have to write repetitive code each time you wanted to do something like this unless you combined all your scripts (not a bad idea).

To be honest, while I add local fallback myself because I'm ocd, it's most certainly unnecessary for most sites. If your main audience is in the US, you're uptime with Google, MaxCDN, etc will be above 99%. I've actually never witnessed them go down as far as I can remember. Looking at the numbers here you can see that in the past 30 days CDNJS, Google, and BootstrapCDN have all had a 100% uptime!

However, if you're developing a theme for wordpress.org, they require you to have local version of the files for "best practice". If your just doing this for your personal site and it's not the end of the world if your site goes down for five minutes every 3 months then don't bother adding a local fallback. I'm guessing Google has a better uptime than your local file on Godaddy/Bluehost/etc. anyway.

Here's how you can quickly include Bootstrap in it's entirety (without fallbacks):

/**
 * Enqueue Theme Scripts and Styles for Bootstrap 3
 * 
 * @author Bryan Willis
 * @link https://gist.github.com/bryanwillis/7fd5356a9d18d0c7815f
 */
function bw_enqueue_html5shiv_respond_bootstrap()  {
  // Jquery
  $jquery_version = wp_scripts()->registered['jquery']->ver;
  wp_deregister_script('jquery');
  wp_register_script('jquery', 'https://ajax.googleapis.com/ajax/libs/jquery/'.$jquery_version.'/jquery.min.js', array(), false, true);

  // Bootstrap CSS - Must be local for respondjs to work (unless using a proxy)
  wp_enqueue_style( 'bootstrap', get_stylesheet_directory_uri().'/css/bootstrap.min.css', false, '3.3.6', 'all' );

  // Bootstrap JS
  wp_register_script('bootstrap', 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js', array('jquery'), false, true);

  // Legacy IE scripts
  wp_enqueue_script( 'html5shiv', 'https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js', array(), false, false );
  wp_enqueue_script( 'respond', 'https://oss.maxcdn.com/respond/1.4.2/respond.min.js', array(), false, false );
  wp_script_add_data( 'html5shiv', 'conditional', 'lt IE 9' );
  wp_script_add_data( 'respond', 'conditional', 'lt IE 9' );
}
add_action('wp_enqueue_scripts', 'bw_enqueue_html5shiv_respond_bootstrap');

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment