サードパーティ製アプリの遅延読み込み最適化
Shopify に統合されたサードパーティ製アプリスクリプトの読み込み方式を管理画面から制御し、ページ表示速度を改善するセクション。スクロール、ユーザーインタラクション、または即座読み込みのいずれかに切り替えられる。
コード
769 行 / liquid{%- if section.settings.enable -%}
{%- liquid
assign page = request.page_type | split: '/' | first
assign block_key = 'block_' | append: page
assign scroll_key = 'scroll_' | append: page
assign interaction_key = 'interaction_' | append: page
assign arr_block = '' | split: ''
assign arr_scroll = '' | split: ''
assign arr_interaction = '' | split: ''
for block in section.blocks
assign url = block.settings.url | replace: '/', '\/'
if block.settings[block_key] == true and content_for_header contains url
assign arr_block = block | concat: arr_block
endif
if block.settings[scroll_key] == true and content_for_header contains url
assign arr_scroll = block | concat: arr_scroll
endif
if block.settings[interaction_key] == true and content_for_header contains url
assign arr_interaction = block | concat: arr_interaction
endif
endfor
assign settings_data = arr_block | concat: arr_scroll | concat: arr_interaction | uniq | map: 'settings'
-%}
{%- if settings_data.size != 0 -%}
<!-- section/app-optimization.liquid -->
<script>
(function AppOptimization() {
'use strict';
var supportsPassive = getPassiveSupport();
var page = {{ page | json }};
var debug = {{ section.settings.debug_enable | json }};
modifyCreateElement({{ settings_data | json }}.map(loadTypeFromSettings).map(eventsFromSettings).map(outputLogic));
function loadTypeFromSettings(settings) {
if (settings['settings_' + page]) settings.loadType = 'settings';
else if (settings['scroll_' + page]) settings.loadType = 'scroll';
else settings.loadType = 'interaction';
return settings;
}
function eventsFromSettings(settings) {
if (settings.loadType !== 'interaction') return settings;
settings.interactionEvents = [];
var selectorList = [settings.interaction_selectors_1, settings.interaction_selectors_2];
var eventList = [settings.interaction_event_1, settings.interaction_event_2];
selectorList.forEach(function(selector, index) {
if (selector.length) {
settings.interactionEvents.push({
nodes: nodeList(selector),
event: eventList[index]
});
}
});
return settings;
}
function outputLogic(settings) {
var data = {
url: settings.url,
loadType: settings.loadType,
interactionEvents: settings.interactionEvents
}
if (debug) console.log('App Optimiztion Data:', data);
return data;
}
function modifyCreateElement(logic) {
var createElementBackup = document.createElement;
document.createElement = function() {
var args = Array.prototype.slice.call(arguments);
var node = createElementBackup.bind(document).apply(undefined, args);
// Skip if this is not a script tag
if (first(args).toLowerCase() !== 'script') return node;
var originalSetAttribute = node.setAttribute.bind(node);
Object.defineProperties(node, {
'src': {
get() {
return node.getAttribute('src') || ''
},
set(src) {
var loadRule = getLoadRule(logic, src);
if (loadRule) {
// Remove app from logic once matched
logic.forEach(function(item, index){
if (item.url === loadRule.url) logic.splice(index, 1);
});
// Change script MIME type if script should not load
originalSetAttribute('type', 'javascript/blocked');
loadRule.originalUrl = src;
if (loadRule.loadType === 'interaction') window.addEventListener('load', interactionLoad(loadRule), false);
if (loadRule.loadType === 'scroll') window.addEventListener('load', scrollLoad(loadRule), false);
}
originalSetAttribute('src', src);
return true;
}
}
});
return node;
}
}
function interactionLoad(loadRule) {
loadRule.interactionEvents.forEach(function(interactionEvent) {
interactionEvent.nodes.forEach(function(node) {
var eventFn = partial(loadScript, loadRule.originalUrl);
var eventOptions = supportsPassive ? {once: true} : false;
node.addEventListener(interactionEvent.event, eventFn, eventOptions);
});
});
}
function scrollLoad(loadRule) {
var eventFn = partial(loadOnScroll, loadRule.originalUrl);
var eventOptions = supportsPassive ? {passive: true} : false;
window.addEventListener('scroll', eventFn, eventOptions);
function loadOnScroll(src) {
loadScript(src);
window.removeEventListener('scroll', eventFn, eventOptions);
}
}
function getLoadRule(logic, src) {
return first(logic.filter(function(item) {
return stringIncludes(src, item.url);
}));
}
function getPassiveSupport() {
var supportsPassive = false;
try {
var opts = Object.defineProperty({}, 'passive', {
get: function() { supportsPassive = true; }
});
window.addEventListener('testPassive', null, opts);
window.removeEventListener('testPassive', null, opts);
} catch (e) {}
return supportsPassive;
}
function loadScript(src) {
var script = document.querySelector('script[type="javascript/blocked"][src="' + src + '"]');
if (!script) return;
var newScript = document.createElement('script');
newScript.src = script.src;
newScript.async = true;
newScript.setAttribute('data-app-optimization', true);
document.body.appendChild(newScript);
if (debug) console.log('App Optimization Script Load:', newScript);
}
function nodeList(str) {
var nodes = []
try { nodes = Array.prototype.slice.call(document.querySelectorAll(str));
} catch {};
return nodes;
}
function stringIncludes(str1, str2) {
return String(str1).indexOf(String(str2)) != -1;
}
function first(arr) {
return arr[0];
}
function partial(fn) {
var slice = Array.prototype.slice;
var args = slice.call(arguments, 1);
return function() {
return fn.apply(this, args.concat(slice.call(arguments, 0)));
};
}
})();
</script>
{%- endif -%}
{%- endif -%}
{%- if request.design_mode -%}
{%- comment -%}
The code below will be loaded only in the Theme Editor
{%- endcomment-%}
{%- assign urls = content_for_header | split: 'var urls =' | last | split: 'for (var i = 0' | first | remove: ';' | split: ',' -%}
<template id="sd-app-list-template">
<style>
:host {
display: block;
contain: content;
padding: 3.2rem 1rem;
background: #f6f6f7;
width: 100%;
box-shadow: 0 0 0 1px rgb(63 63 68 / 5%), 0 1px 3px 0 rgb(63 63 68 / 15%);
}
.w-full { width: 100% }
.grid-container {
max-width: 1100px;
margin: 0 auto;
padding: 16px 2px;
}
* {
font-family: -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue,sans-serif;
font-size: 14px;
color: #202223;
text-align: left
}
p {
padding: 0;
margin: 0;
}
a {
text-decoration: none;
}
table {
border-collapse: collapse;
}
td, th {
padding: 16px
}
th p {
text-transform: uppercase;
font-size: 12px;
font-weight: 600;
}
tr {
border-bottom: 1px solid #e1e3e5;
}
tr:hover,
tr:hover td:first-child {
background: #fafbfb;
}
a:hover {
text-decoration: underline;
}
td:first-child,
th:first-child {
position: sticky;
left: 0;
background: #fff;
}
th, th:first-child { background: #fafbfb; }
[data-badge] {
background-color: #e4e5e7;
display: inline-block;
border-radius: 2rem;
font-size: 13px;
line-height: 16px;
padding: 3px 8px;
}
[data-badge="Interaction"],
[data-badge="Scroll"] {
background-color: #aee9d1;
}
[data-badge="Block"] {
background-color: #fed3d1;
}
.card {
background-color: #ffffff;
box-shadow: rgb(23 24 24 / 5%) 0px 0px 5px 0px, rgb(0 0 0 / 15%) 0px 1px 2px 0px;
outline: .1rem solid transparent;
border-radius: 8px;
overflow: auto;
-webkit-overflow-scrolling: touch;
}
button {
background: transparent;
border: 0;
cursor: pointer
}
button svg {
display: block;
fill: #5C5F62;
}
button:hover svg {
fill: #008060;
}
h2 {
font-size: 16px;
font-weight: 600;
line-height: 24px;
}
.text-right { text-align: right }
.text-center { text-align: center }
.pr-8 { padding-right: 8px }
.mb-4 { margin-bottom: 4px }
.mb-32 { margin-bottom: 32px }
.flex { display: flex }
.whitespace-no-wrap { white-space: nowrap }
.btn-outline {
padding: 9px 16px;
border: 1px solid rgb(140, 145, 150);
border-radius: 4px;
display: inline-block;
margin-top: 16px;
margin-left: 8px;
font-size: 14px;
line-height: 16px;
}
.btn-primary {
background: #008060;
color: #fff;
border-color: #008060;
}
.header-grid {
display: grid;
grid-template-columns: 1.7fr 1fr;
}
</style>
<div class="grid-container">
<div class="header-grid mb-32">
<div>
<h2 class="mb-4">App Optimization by Sections.design</h2>
<p>Improve performance by optimizing how, when, and where your apps will load.</p>
</div>
<div class="text-right">
<a class="btn-outline text-center" target="_blank" href="https://sections.design/blogs/shopify/app-optimization">Read blog post</a> <a class="btn-outline btn-primary text-center" target="_blank" href="https://youtu.be/UFdTKDPCc_Q">View setup video</a>
</div>
</div>
<div class="card w-full">
<table class="w-full">
<thead>
<tr>
<th><p>Application</p></th>
<th><p>Script URL</p></th>
<th><p class="text-right pr-8">{{ page }} page load</p></th>
</tr>
</thead>
<tbody>
{%- for url in urls -%}
{%- liquid
assign name = 'App name'
assign load = 'Default'
assign block_key = 'block_' | append: page
assign scroll_key = 'scroll_' | append: page
assign interaction_key = 'interaction_' | append: page
assign url_clean = url | remove: '"' | remove: '[' | remove: ']' | replace: '\/', '/' | replace: '\u0026', '&' | remove: 'https://' | remove: '//' | strip
assign url_display = url_clean | split: '?' | first
for block in section.blocks
if block.settings.url != blank and url_clean contains block.settings.url
assign name = block.settings.title
if block.settings[block_key] == true
assign load = 'Block'
elsif block.settings[scroll_key] == true
assign load = 'Scroll'
elsif block.settings[interaction_key] == true
assign load = 'Interaction'
endif
endif
endfor
-%}
<tr>
<td><p>{{ name }}</p></td>
<td>
<p class="flex whitespace-no-wrap">
<a href="https://{{ url_clean }}" target="_blank" rel="noreferrer">{{ url_display }}</a>
<button type="button" data-clipboard="{{ url_display }}" >
<svg aria-hidden="true" width="14" height="14" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M7.5 2A1.5 1.5 0 006 3.5V13a1 1 0 001 1h9.5a1.5 1.5 0 001.5-1.5v-9A1.5 1.5 0 0016.5 2h-9zm-4 4H4v10h10v.5a1.5 1.5 0 01-1.5 1.5h-9A1.5 1.5 0 012 16.5v-9A1.5 1.5 0 013.5 6z"/></svg>
</button>
</p>
</td>
<td class="text-right"><p data-badge="{{ load }}">{{ load }}</p></td>
</tr>
{%- endfor -%}
</tbody>
</table>
</div>
</div>
</template>
<sd-app-list></sd-app-list>
<script>
(function ThemeEditor() {
'use strict';
document.addEventListener('shopify:section:select', select);
document.addEventListener('shopify:section:load', load);
document.addEventListener('shopify:section:deselect', deselect);
function select(evt) {
if (evt.detail.sectionId !== 'app-optimization') return;
let node = document.querySelector('sd-app-list');
if (node) node.removeAttribute('style');
if (customElements.get('sd-app-list')) return;
defineElement();
initCopyEvents(node.shadowRoot);
}
function load(evt) {
if (evt.detail.sectionId !== 'app-optimization') return;
let shadowRoot = document.querySelector('sd-app-list').shadowRoot;
let nodes = Array.prototype.slice.call(shadowRoot.children);
nodes.forEach(node => node.parentNode.removeChild(node));
shadowRoot.appendChild(templateContent());
initCopyEvents(shadowRoot);
}
function deselect(evt) {
if (evt.detail.sectionId !== 'app-optimization') return;
let node = document.querySelector('sd-app-list');
if (node) node.style.display = 'none';
}
function defineElement() {
customElements.define('sd-app-list',
class extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'}).appendChild(templateContent());
}
}
);
}
function templateContent() {
let template = document.getElementById('sd-app-list-template');
return template.content.cloneNode(true);
}
function initCopyEvents(shadowRoot) {
let nodes = Array.prototype.slice.call(shadowRoot.querySelectorAll('[data-clipboard]'));
nodes.forEach(node => node.addEventListener('click', copyText));
}
function copyText() {
navigator.clipboard.writeText(this.getAttribute('data-clipboard'));
}
})();
</script>
{%- endif -%}
{% schema %}
{
"name": "App Optimiztion",
"settings": [
{
"type": "checkbox",
"id": "enable",
"label": "Enable App Optimization?",
"default": true
},
{
"type": "checkbox",
"id": "debug_enable",
"label": "Enable debug?",
"default": false,
"info": "Output script loading information in console.log"
}
],
"blocks": [
{
"type": "app",
"name": "Application",
"settings": [
{
"type": "text",
"id": "title",
"label": "App title"
},
{
"type": "text",
"id": "url",
"label": "App ScriptTag URL",
"placeholder": "e.g. shopifycdn.com/assets/v4/spr.js",
"info": "Use partial URL, e.g: shopifycdn.com/assets/v4/spr.js"
},
{
"type": "header",
"content": "Block script from loading"
},
{
"type": "paragraph",
"content": "On selected pages, the app will not be loaded"
},
{
"type": "checkbox",
"id": "block_index",
"label": "Index"
},
{
"type": "checkbox",
"id": "block_product",
"label": "Product"
},
{
"type": "checkbox",
"id": "block_collection",
"label": "Collection"
},
{
"type": "checkbox",
"id": "block_page",
"label": "Page"
},
{
"type": "checkbox",
"id": "block_blog",
"label": "Blog"
},
{
"type": "checkbox",
"id": "block_article",
"label": "Article"
},
{
"type": "checkbox",
"id": "block_cart",
"label": "Cart"
},
{
"type": "checkbox",
"id": "block_search",
"label": "Search"
},
{
"type": "checkbox",
"id": "block_customers",
"label": "Customers"
},
{
"type": "checkbox",
"id": "block_gift_card",
"label": "Gift card"
},
{
"type": "header",
"content": "Load script on user page scroll"
},
{
"type": "paragraph",
"content": "On selected pages, the app will start to load when the user scrolls the page."
},
{
"type": "checkbox",
"id": "scroll_index",
"label": "Index"
},
{
"type": "checkbox",
"id": "scroll_product",
"label": "Product"
},
{
"type": "checkbox",
"id": "scroll_collection",
"label": "Collection"
},
{
"type": "checkbox",
"id": "scroll_page",
"label": "Page"
},
{
"type": "checkbox",
"id": "scroll_blog",
"label": "Blog"
},
{
"type": "checkbox",
"id": "scroll_article",
"label": "Article"
},
{
"type": "checkbox",
"id": "scroll_cart",
"label": "Cart"
},
{
"type": "checkbox",
"id": "scroll_search",
"label": "Search"
},
{
"type": "checkbox",
"id": "scroll_customers",
"label": "Customers"
},
{
"type": "checkbox",
"id": "scroll_gift_card",
"label": "Gift card"
},
{
"type": "header",
"content": "Load script on user interaction"
},
{
"type": "paragraph",
"content": "On selected pages, the app will start to load when the user will trigger the assigned events."
},
{
"type": "checkbox",
"id": "interaction_index",
"label": "Index"
},
{
"type": "checkbox",
"id": "interaction_product",
"label": "Product"
},
{
"type": "checkbox",
"id": "interaction_collection",
"label": "Collection"
},
{
"type": "checkbox",
"id": "interaction_page",
"label": "Page"
},
{
"type": "checkbox",
"id": "interaction_blog",
"label": "Blog"
},
{
"type": "checkbox",
"id": "interaction_article",
"label": "Article"
},
{
"type": "checkbox",
"id": "interaction_cart",
"label": "Cart"
},
{
"type": "checkbox",
"id": "interaction_search",
"label": "Search"
},
{
"type": "checkbox",
"id": "interaction_customers",
"label": "Customers"
},
{
"type": "checkbox",
"id": "interaction_gift_card",
"label": "Gift card"
},
{
"type": "header",
"content": "User interaction event 1"
},
{
"type": "text",
"id": "interaction_selectors_1",
"label": "CSS selectors list",
"info": "Comma separated CSS selectors",
"placeholder": ".btn, .menu"
},
{
"type": "select",
"id": "interaction_event_1",
"label": "Interaction event name",
"options": [
{
"value": "click",
"label": "click"
},
{
"value": "mousedown",
"label": "mousedown"
},
{
"value": "mouseup",
"label": "mouseup"
},
{
"value": "focus",
"label": "focus"
}
],
"default": "click"
},
{
"type": "header",
"content": "User interaction event 2"
},
{
"type": "text",
"id": "interaction_selectors_2",
"label": "CSS selectors list",
"info": "Comma separated CSS selectors",
"placeholder": ".btn, .menu"
},
{
"type": "select",
"id": "interaction_event_2",
"label": "Interaction event name",
"options": [
{
"value": "click",
"label": "click"
},
{
"value": "mousedown",
"label": "mousedown"
},
{
"value": "mouseup",
"label": "mouseup"
},
{
"value": "focus",
"label": "focus"
}
],
"default": "click"
}
]
}
]
}
{% endschema %}
出典・ライセンス
- Repository:
- https://github.com/mirceapiturca/Sections
- License:
- MIT
このコードは mirceapiturca 著作の MIT ライセンスソースです。 原本の著作権は mirceapiturca が保有します。日本語訳は ALSEL によるものです。
関連項目
定期販売プランのデータ属性生成
商品バリアントと定期販売プランの ID を HTML data 属性として動的に生成するスニペット。ループで複数プランの ID を data-sellingId-1、data-sellingId-2 のように番号付けして出力する。
カスタム要素の親コンポーネント
Web Components の親要素(`<parent-element>`)でラップし、子スニペットを描画するコンポーネント。カスタム要素に依存した構造を実装する際のテンプレート。
javascript タグ内の JS コード整形
Liquid の `{% javascript %}` タグ内に記述した JavaScript コードを Prettier で自動整形する。純粋な JS であれば完全整形、Liquid 変数を含む場合はインデント調整のみで対応する。
JavaScriptタグ内のコード整形テスト
Liquid の {% javascript %} タグ内に埋め込まれた JavaScript コードを Prettier で整形するテストケース。純粋な JS、Liquid 混在、タブ設定、シングルクォート対応など複数のシナリオをカバーしている。
クイズ結果用の商品データとフィルタ条件を JSON 出力
選択したコレクションの全商品を JSON 形式で抽出し、クイズの回答ごとに設定されたフィルタ条件(価格・タグ・タイプ・ベンダー・オプション)と紐づけることで、JavaScript 側でリアルタイムに商品をマッチングできるようにする。
Vue コンポーネントのデモ
Vue コンポーネント、スロット、プロップ、Vuex ストア、グローバルミックス、カスタムディレクティブを一堂に展示するセクション。Shopify Theme Lab フレームワークの主要機能をインタラクティブに試せる。