Progress Loading bar for PWAs (프로그레스 로딩바)

Appendix/Daily Story

PWA (프로그레시브 웹 앱, Progressive Web Apps) 를 더욱 네이티브 앱처럼 보이기 위한 것으로 여러가지 방법 중 오늘은 페이지 최상단 영역에 프로그레스 로딩바를 설치해 보려합니다.

 

로딩바
로딩 바

 

 

목차(차례)

     

     

    Progress Loading bar for PWAs (프로그레스 로딩바)

    먼저, 유튜브 웹사이트를 방문해 보시면, 첫 화면에 최상단 빨간색으로 로딩바가 지나가는 것을 볼 수 있습니다. 유튜브뿐만 아니라 보여줘야 할것이 많은 최대수의 메이저 플랫폼에서는 많이 적용하고 있는 것중에 하나이기도 하지요.

     

    오늘 우리가 할 일이 바로 이것입니다.

    페이지 상단에 로딩 상태만큼 좌에서 우측으로 흘러가는

    Progress Loading bar (프로그레스 로딩바)

     

    유튜브 상단 로딩바
    유튜브 페이지상단 로딩바

      

    Progress Loading bar 설치요소

    웹앱을 더욱 앱처럼 보이기 위한 시작적인 요소가 아닐까 생각됩니다. 물론 기능적인 요소도 두말하면 잔소리겠지요.

    먼저, 페이지상단 진행바, 로딩바 등을 만들기 위해 3가지 요소가 필요합니다.

     

    웹에서는 늘 마찬가지지만

    HTML, CSS, 그리고 JS이지요.

     

     

    로딩바 HTML

    HTML에서는 head 영역내 아래와 같이 스크립트 js를 호출합니다.

    <head>
    
    <!-- PageTop Loading Progress Bar -->
    <script src="./images/pace.min.js"></script>
    
    </head>

     

    로딩바 CSS

    CSS 영역에서는 다음과 같은 CSS 코드를 삽입하시고, 아래 주석처리해논 백그라운드 칼라는 자신의 웹에 맞춰 변경하시면 변경된 칼라가 적용되어 반영됩니다.

    /* PageTop Loading Progress Bar CSS */
    .pace {
    	-webkit-pointer-events: none;
    	pointer-events: none;
    	-webkit-user-select: none;
    	-moz-user-select: none;
    	user-select: none;
    }
        
    .pace-inactive {
    	display: none;
    }
        
    .pace .pace-progress {
    	background: rgba(51, 204, 204, 1);  /* 자신의 사이트에 맞게 칼러변경 */
    	position: fixed;
    	z-index: 2000;
    	top: 0;
    	right: 100%;
    	width: 100%;
    	height: 2px;
    }

     

     

    로딩바 마지막은 js

    js 영역은 pace.min.js 라는 파일를 따로 만들어 아래코드를 삽입하고 저장한 다음 HTML 호출영역으로 업로드 하시면 작동됩니다.

     

    pace.js 버전 

    (function() {
    	var AjaxMonitor, Bar, DocumentMonitor, ElementMonitor, ElementTracker, EventLagMonitor, Evented, Events, NoTargetError, Pace, RequestIntercept, SOURCE_KEYS, Scaler, SocketRequestTracker, XHRRequestTracker, addEventListener, animation, avgAmplitude, bar, cancelAnimation, cancelAnimationFrame, defaultOptions, extend, extendNative, getFromDOM, getIntercept, handlePushState, ignoreStack, init, now, options, requestAnimationFrame, result, runAnimation, scalers, shouldIgnoreURL, shouldTrack, source, sources, uniScaler, _WebSocket, _XDomainRequest, _XMLHttpRequest, _i, _intercept, _len, _pushState, _ref, _ref1, _replaceState,
    		__slice = [].slice,
    		__hasProp = {}.hasOwnProperty,
    		__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
    		__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
    		__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
    
    	defaultOptions = {
    		className: '',
    		catchupTime: 100,
    		initialRate: .03,
    		minTime: 250,
    		ghostTime: 100,
    		maxProgressPerFrame: 20,
    		easeFactor: 1.25,
    		startOnPageLoad: true,
    		restartOnPushState: true,
    		restartOnRequestAfter: 500,
    		target: 'body',
    		elements: {
    			checkInterval: 100,
    			selectors: ['body']
    		},
    		eventLag: {
    			minSamples: 10,
    			sampleCount: 3,
    			lagThreshold: 3
    		},
    		ajax: {
    			trackMethods: ['GET'],
    			trackWebSockets: true,
    			ignoreURLs: []
    		}
    	};
    
    	now = function() {
    		var _ref;
    		return (_ref = typeof performance !== "undefined" && performance !== null ? typeof performance.now === "function" ? performance.now() : void 0 : void 0) != null ? _ref : +(new Date);
    	};
    
    	requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
    
    	cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame;
    
    	addEventListener = function(obj, event, callback) {
    		if (typeof obj.addEventListener === "function") {
    			return obj.addEventListener(event, callback, false);
    		} else {
    			return function() {
    				if (typeof obj["on" + event] !== "function" || typeof obj["on" + event].eventListeners !== "object") {
    					var eventListeners = new Events();
    					if (typeof obj["on" + event] === "function") {
    						eventListeners.on(event, obj["on" + event]);
    					}
    					obj["on" + event] = function(evt) {
    						return eventListeners.trigger(event, evt);
    					};
    					obj["on" + event].eventListeners = eventListeners;
    				} else {
    					var eventListeners = obj["on" + event].eventListeners;
    				}
    				eventListeners.on(event, callback);
    			}();
    		}
    	};
    
    	if (requestAnimationFrame == null) {
    		requestAnimationFrame = function(fn) {
    			return setTimeout(fn, 50);
    		};
    		cancelAnimationFrame = function(id) {
    			return clearTimeout(id);
    		};
    	}
    
    	runAnimation = function(fn) {
    		var last, tick;
    		last = now();
    		tick = function() {
    			var diff;
    			diff = now() - last;
    			if (diff >= 33) {
    				last = now();
    				return fn(diff, function() {
    					return requestAnimationFrame(tick);
    				});
    			} else {
    				return setTimeout(tick, 33 - diff);
    			}
    		};
    		return tick();
    	};
    
    	result = function() {
    		var args, key, obj;
    		obj = arguments[0], key = arguments[1], args = 3 <= arguments.length ? __slice.call(arguments, 2) : [];
    		if (typeof obj[key] === 'function') {
    			return obj[key].apply(obj, args);
    		} else {
    			return obj[key];
    		}
    	};
    
    	extend = function() {
    		var key, out, source, sources, val, _i, _len;
    		out = arguments[0], sources = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
    		for (_i = 0, _len = sources.length; _i < _len; _i++) {
    			source = sources[_i];
    			if (source) {
    				for (key in source) {
    					if (!__hasProp.call(source, key)) continue;
    					val = source[key];
    					if ((out[key] != null) && typeof out[key] === 'object' && (val != null) && typeof val === 'object') {
    						extend(out[key], val);
    					} else {
    						out[key] = val;
    					}
    				}
    			}
    		}
    		return out;
    	};
    
    	avgAmplitude = function(arr) {
    		var count, sum, v, _i, _len;
    		sum = count = 0;
    		for (_i = 0, _len = arr.length; _i < _len; _i++) {
    			v = arr[_i];
    			sum += Math.abs(v);
    			count++;
    		}
    		return sum / count;
    	};
    
    	getFromDOM = function(key, json) {
    		var data, e, el;
    		if (key == null) {
    			key = 'options';
    		}
    		if (json == null) {
    			json = true;
    		}
    		el = document.querySelector("[data-pace-" + key + "]");
    		if (!el) {
    			return;
    		}
    		data = el.getAttribute("data-pace-" + key);
    		if (!json) {
    			return data;
    		}
    		try {
    			return JSON.parse(data);
    		} catch (_error) {
    			e = _error;
    			return typeof console !== "undefined" && console !== null ? console.error("Error parsing inline pace options", e) : void 0;
    		}
    	};
    
    	Evented = (function() {
    		function Evented() {}
    
    		Evented.prototype.on = function(event, handler, ctx, once) {
    			var _base;
    			if (once == null) {
    				once = false;
    			}
    			if (this.bindings == null) {
    				this.bindings = {};
    			}
    			if ((_base = this.bindings)[event] == null) {
    				_base[event] = [];
    			}
    			return this.bindings[event].push({
    				handler: handler,
    				ctx: ctx,
    				once: once
    			});
    		};
    
    		Evented.prototype.once = function(event, handler, ctx) {
    			return this.on(event, handler, ctx, true);
    		};
    
    		Evented.prototype.off = function(event, handler) {
    			var i, _ref, _results;
    			if (((_ref = this.bindings) != null ? _ref[event] : void 0) == null) {
    				return;
    			}
    			if (handler == null) {
    				return delete this.bindings[event];
    			} else {
    				i = 0;
    				_results = [];
    				while (i < this.bindings[event].length) {
    					if (this.bindings[event][i].handler === handler) {
    						_results.push(this.bindings[event].splice(i, 1));
    					} else {
    						_results.push(i++);
    					}
    				}
    				return _results;
    			}
    		};
    
    		Evented.prototype.trigger = function() {
    			var args, ctx, event, handler, i, once, _ref, _ref1, _results;
    			event = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
    			if ((_ref = this.bindings) != null ? _ref[event] : void 0) {
    				i = 0;
    				_results = [];
    				while (i < this.bindings[event].length) {
    					_ref1 = this.bindings[event][i], handler = _ref1.handler, ctx = _ref1.ctx, once = _ref1.once;
    					handler.apply(ctx != null ? ctx : this, args);
    					if (once) {
    						_results.push(this.bindings[event].splice(i, 1));
    					} else {
    						_results.push(i++);
    					}
    				}
    				return _results;
    			}
    		};
    
    		return Evented;
    
    	})();
    
    	Pace = window.Pace || {};
    
    	window.Pace = Pace;
    
    	extend(Pace, Evented.prototype);
    
    	options = Pace.options = extend({}, defaultOptions, window.paceOptions, getFromDOM());
    
    	_ref = ['ajax', 'document', 'eventLag', 'elements'];
    	for (_i = 0, _len = _ref.length; _i < _len; _i++) {
    		source = _ref[_i];
    		if (options[source] === true) {
    			options[source] = defaultOptions[source];
    		}
    	}
    
    	NoTargetError = (function(_super) {
    		__extends(NoTargetError, _super);
    
    		function NoTargetError() {
    			_ref1 = NoTargetError.__super__.constructor.apply(this, arguments);
    			return _ref1;
    		}
    
    		return NoTargetError;
    
    	})(Error);
    
    	Bar = (function() {
    		function Bar() {
    			this.progress = 0;
    		}
    
    		Bar.prototype.getElement = function() {
    			var targetElement;
    			if (this.el == null) {
    				targetElement = document.querySelector(options.target);
    				if (!targetElement) {
    					throw new NoTargetError;
    				}
    				this.el = document.createElement('div');
    				this.el.className = "pace pace-active";
    				document.body.className = document.body.className.replace(/(pace-done )|/, 'pace-running ');
    				var _custom_class_name = (options.className !== '') ? ' '+options.className : '';
    				this.el.innerHTML = '<div class="pace-progress'+_custom_class_name+'">\n  <div class="pace-progress-inner"></div>\n</div>\n<div class="pace-activity"></div>';
    				if (targetElement.firstChild != null) {
    					targetElement.insertBefore(this.el, targetElement.firstChild);
    				} else {
    					targetElement.appendChild(this.el);
    				}
    			}
    			return this.el;
    		};
    
    		Bar.prototype.finish = function() {
    			var el;
    			el = this.getElement();
    			el.className = el.className.replace('pace-active', 'pace-inactive');
    			return document.body.className = document.body.className.replace('pace-running ', 'pace-done ');
    		};
    
    		Bar.prototype.update = function(prog) {
    			this.progress = prog;
    			Pace.trigger('progress', prog);
    			return this.render();
    		};
    
    		Bar.prototype.destroy = function() {
    			try {
    				this.getElement().parentNode.removeChild(this.getElement());
    			} catch (_error) {
    				NoTargetError = _error;
    			}
    			return this.el = void 0;
    		};
    
    		Bar.prototype.render = function() {
    			var el, key, progressStr, transform, _j, _len1, _ref2;
    			if (document.querySelector(options.target) == null) {
    				return false;
    			}
    			el = this.getElement();
    			transform = "translate3d(" + this.progress + "%, 0, 0)";
    			_ref2 = ['webkitTransform', 'msTransform', 'transform'];
    			for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
    				key = _ref2[_j];
    				el.children[0].style[key] = transform;
    			}
    			if (!this.lastRenderedProgress || this.lastRenderedProgress | 0 !== this.progress | 0) {
    				el.children[0].setAttribute('data-progress-text', "" + (this.progress | 0) + "%");
    				if (this.progress >= 100) {
    					progressStr = '99';
    				} else {
    					progressStr = this.progress < 10 ? "0" : "";
    					progressStr += this.progress | 0;
    				}
    				el.children[0].setAttribute('data-progress', "" + progressStr);
    			}
    			Pace.trigger('change', this.progress);
    			return this.lastRenderedProgress = this.progress;
    		};
    
    		Bar.prototype.done = function() {
    			return this.progress >= 100;
    		};
    
    		return Bar;
    
    	})();
    
    	Events = (function() {
    		function Events() {
    			this.bindings = {};
    		}
    
    		Events.prototype.trigger = function(name, val) {
    			var binding, _j, _len1, _ref2, _results;
    			if (this.bindings[name] != null) {
    				_ref2 = this.bindings[name];
    				_results = [];
    				for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
    					binding = _ref2[_j];
    					_results.push(binding.call(this, val));
    				}
    				return _results;
    			}
    		};
    
    		Events.prototype.on = function(name, fn) {
    			var _base;
    			if ((_base = this.bindings)[name] == null) {
    				_base[name] = [];
    			}
    			return this.bindings[name].push(fn);
    		};
    
    		return Events;
    
    	})();
    
    	_XMLHttpRequest = window.XMLHttpRequest;
    
    	_XDomainRequest = window.XDomainRequest;
    
    	_WebSocket = window.WebSocket;
    
    	extendNative = function(to, from) {
    		var e, key, _results;
    		_results = [];
    		for (key in from.prototype) {
    			try {
    				if ((to[key] == null) && typeof from[key] !== 'function') {
    					if (typeof Object.defineProperty === 'function') {
    						_results.push(Object.defineProperty(to, key, {
    							get: (function(key) {
    								return function() {
    									return from.prototype[key];
    								};
    							})(key),
    							configurable: true,
    							enumerable: true
    						}));
    					} else {
    						_results.push(to[key] = from.prototype[key]);
    					}
    				} else {
    					_results.push(void 0);
    				}
    			} catch (_error) {
    				e = _error;
    			}
    		}
    		return _results;
    	};
    
    	ignoreStack = [];
    
    	Pace.ignore = function() {
    		var args, fn, ret;
    		fn = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
    		ignoreStack.unshift('ignore');
    		ret = fn.apply(null, args);
    		ignoreStack.shift();
    		return ret;
    	};
    
    	Pace.track = function() {
    		var args, fn, ret;
    		fn = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
    		ignoreStack.unshift('track');
    		ret = fn.apply(null, args);
    		ignoreStack.shift();
    		return ret;
    	};
    
    	shouldTrack = function(method) {
    		var _ref2;
    		if (method == null) {
    			method = 'GET';
    		}
    		if (ignoreStack[0] === 'track') {
    			return 'force';
    		}
    		if (!ignoreStack.length && options.ajax) {
    			if (method === 'socket' && options.ajax.trackWebSockets) {
    				return true;
    			} else if (_ref2 = method.toUpperCase(), __indexOf.call(options.ajax.trackMethods, _ref2) >= 0) {
    				return true;
    			}
    		}
    		return false;
    	};
    
    	RequestIntercept = (function(_super) {
    		__extends(RequestIntercept, _super);
    
    		function RequestIntercept() {
    			var monitorXHR,
    				_this = this;
    			RequestIntercept.__super__.constructor.apply(this, arguments);
    			monitorXHR = function(req) {
    				var _open;
    				_open = req.open;
    				return req.open = function(type, url, async) {
    					if (shouldTrack(type)) {
    						_this.trigger('request', {
    							type: type,
    							url: url,
    							request: req
    						});
    					}
    					return _open.apply(req, arguments);
    				};
    			};
    			window.XMLHttpRequest = function(flags) {
    				var req;
    				req = new _XMLHttpRequest(flags);
    				monitorXHR(req);
    				return req;
    			};
    			try {
    				extendNative(window.XMLHttpRequest, _XMLHttpRequest);
    			} catch (_error) {}
    			if (_XDomainRequest != null) {
    				window.XDomainRequest = function() {
    					var req;
    					req = new _XDomainRequest;
    					monitorXHR(req);
    					return req;
    				};
    				try {
    					extendNative(window.XDomainRequest, _XDomainRequest);
    				} catch (_error) {}
    			}
    			if ((_WebSocket != null) && options.ajax.trackWebSockets) {
    				window.WebSocket = function(url, protocols) {
    					var req;
    					if (protocols != null) {
    						req = new _WebSocket(url, protocols);
    					} else {
    						req = new _WebSocket(url);
    					}
    					if (shouldTrack('socket')) {
    						_this.trigger('request', {
    							type: 'socket',
    							url: url,
    							protocols: protocols,
    							request: req
    						});
    					}
    					return req;
    				};
    				try {
    					extendNative(window.WebSocket, _WebSocket);
    				} catch (_error) {}
    			}
    		}
    
    		return RequestIntercept;
    
    	})(Events);
    
    	_intercept = null;
    
    	getIntercept = function() {
    		if (_intercept == null) {
    			_intercept = new RequestIntercept;
    		}
    		return _intercept;
    	};
    
    	shouldIgnoreURL = function(url) {
    		var pattern, _j, _len1, _ref2;
    		_ref2 = options.ajax.ignoreURLs;
    		for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
    			pattern = _ref2[_j];
    			if (typeof pattern === 'string') {
    				if (url.indexOf(pattern) !== -1) {
    					return true;
    				}
    			} else {
    				if (pattern.test(url)) {
    					return true;
    				}
    			}
    		}
    		return false;
    	};
    
    	getIntercept().on('request', function(_arg) {
    		var after, args, request, type, url;
    		type = _arg.type, request = _arg.request, url = _arg.url;
    		if (shouldIgnoreURL(url)) {
    			return;
    		}
    		if (!Pace.running && (options.restartOnRequestAfter !== false || shouldTrack(type) === 'force')) {
    			args = arguments;
    			after = options.restartOnRequestAfter || 0;
    			if (typeof after === 'boolean') {
    				after = 0;
    			}
    			return setTimeout(function() {
    				var stillActive, _j, _len1, _ref2, _ref3, _results;
    				if (type === 'socket') {
    					stillActive = request.readyState < 1;
    				} else {
    					stillActive = (0 < (_ref2 = request.readyState) && _ref2 < 4);
    				}
    				if (stillActive) {
    					Pace.restart();
    					_ref3 = Pace.sources;
    					_results = [];
    					for (_j = 0, _len1 = _ref3.length; _j < _len1; _j++) {
    						source = _ref3[_j];
    						if (source instanceof AjaxMonitor) {
    							source.watch.apply(source, args);
    							break;
    						} else {
    							_results.push(void 0);
    						}
    					}
    					return _results;
    				}
    			}, after);
    		}
    	});
    
    	AjaxMonitor = (function() {
    		function AjaxMonitor() {
    			this.complete = __bind(this.complete, this);
    			var _this = this;
    			this.elements = [];
    			getIntercept().on('request', function() {
    				return _this.watch.apply(_this, arguments);
    			});
    		}
    
    		AjaxMonitor.prototype.watch = function(_arg) {
    			var request, tracker, type, url;
    			type = _arg.type, request = _arg.request, url = _arg.url;
    			if (shouldIgnoreURL(url)) {
    				return;
    			}
    			if (type === 'socket') {
    				tracker = new SocketRequestTracker(request, this.complete);
    			} else {
    				tracker = new XHRRequestTracker(request, this.complete);
    			}
    			return this.elements.push(tracker);
    		};
    
    		AjaxMonitor.prototype.complete = function(tracker) {
    			return this.elements = this.elements.filter(function(e) {
    				return e !== tracker;
    			});
    		};
    
    		return AjaxMonitor;
    
    	})();
    
    	XHRRequestTracker = (function() {
    		function XHRRequestTracker(request, completeCallback) {
    			var event, size, _j, _len1, _onreadystatechange, _ref2,
    				_this = this;
    			this.progress = 0;
    			if (window.ProgressEvent != null) {
    				size = null;
    				addEventListener(request, 'progress', function(evt) {
    					if (evt.lengthComputable) {
    						return _this.progress = 100 * evt.loaded / evt.total;
    					} else {
    						return _this.progress = _this.progress + (100 - _this.progress) / 2;
    					}
    				}, false);
    				_ref2 = ['load', 'abort', 'timeout', 'error'];
    				for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
    					event = _ref2[_j];
    					addEventListener(request, event, function() {
    						completeCallback(_this);
    						return _this.progress = 100;
    					}, false);
    				}
    			} else {
    				_onreadystatechange = request.onreadystatechange;
    				request.onreadystatechange = function() {
    					var _ref3;
    					if ((_ref3 = request.readyState) === 0 || _ref3 === 4) {
    						completeCallback(_this);
    						_this.progress = 100;
    					} else if (request.readyState === 3) {
    						_this.progress = 50;
    					}
    					return typeof _onreadystatechange === "function" ? _onreadystatechange.apply(null, arguments) : void 0;
    				};
    			}
    		}
    
    		return XHRRequestTracker;
    
    	})();
    
    	SocketRequestTracker = (function() {
    		function SocketRequestTracker(request, completeCallback) {
    			var event, _j, _len1, _ref2,
    				_this = this;
    			this.progress = 0;
    			_ref2 = ['error', 'open'];
    			for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
    				event = _ref2[_j];
    				addEventListener(request, event, function() {
    					completeCallback(_this);
    					return _this.progress = 100;
    				}, false);
    			}
    		}
    
    		return SocketRequestTracker;
    
    	})();
    
    	ElementMonitor = (function() {
    		function ElementMonitor(options) {
    			var selector, _j, _len1, _ref2;
    			if (options == null) {
    				options = {};
    			}
    			this.complete = __bind(this.complete, this);
    			this.elements = [];
    			if (options.selectors == null) {
    				options.selectors = [];
    			}
    			_ref2 = options.selectors;
    			for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
    				selector = _ref2[_j];
    				this.elements.push(new ElementTracker(selector, this.complete));
    			}
    		}
    
    		ElementMonitor.prototype.complete = function(tracker) {
    			return this.elements = this.elements.filter(function(e) {
    				return e !== tracker;
    			});
    		};
    
    		return ElementMonitor;
    
    	})();
    
    	ElementTracker = (function() {
    		function ElementTracker(selector, completeCallback) {
    			this.selector = selector;
    			this.completeCallback = completeCallback;
    			this.progress = 0;
    			this.check();
    		}
    
    		ElementTracker.prototype.check = function() {
    			var _this = this;
    			if (document.querySelector(this.selector)) {
    				return this.done();
    			} else {
    				return setTimeout((function() {
    					return _this.check();
    				}), options.elements.checkInterval);
    			}
    		};
    
    		ElementTracker.prototype.done = function() {
    			this.completeCallback(this);
    			this.completeCallback = null;
    			return this.progress = 100;
    		};
    
    		return ElementTracker;
    
    	})();
    
    	DocumentMonitor = (function() {
    		DocumentMonitor.prototype.states = {
    			loading: 0,
    			interactive: 50,
    			complete: 100
    		};
    
    		function DocumentMonitor() {
    			var _onreadystatechange, _ref2,
    				_this = this;
    			this.progress = (_ref2 = this.states[document.readyState]) != null ? _ref2 : 100;
    			_onreadystatechange = document.onreadystatechange;
    			document.onreadystatechange = function() {
    				if (_this.states[document.readyState] != null) {
    					_this.progress = _this.states[document.readyState];
    				}
    				return typeof _onreadystatechange === "function" ? _onreadystatechange.apply(null, arguments) : void 0;
    			};
    		}
    
    		return DocumentMonitor;
    
    	})();
    
    	EventLagMonitor = (function() {
    		function EventLagMonitor() {
    			var avg, interval, last, points, samples,
    				_this = this;
    			this.progress = 0;
    			avg = 0;
    			samples = [];
    			points = 0;
    			last = now();
    			interval = setInterval(function() {
    				var diff;
    				diff = now() - last - 50;
    				last = now();
    				samples.push(diff);
    				if (samples.length > options.eventLag.sampleCount) {
    					samples.shift();
    				}
    				avg = avgAmplitude(samples);
    				if (++points >= options.eventLag.minSamples && avg < options.eventLag.lagThreshold) {
    					_this.progress = 100;
    					return clearInterval(interval);
    				} else {
    					return _this.progress = 100 * (3 / (avg + 3));
    				}
    			}, 50);
    		}
    
    		return EventLagMonitor;
    
    	})();
    
    	Scaler = (function() {
    		function Scaler(source) {
    			this.source = source;
    			this.last = this.sinceLastUpdate = 0;
    			this.rate = options.initialRate;
    			this.catchup = 0;
    			this.progress = this.lastProgress = 0;
    			if (this.source != null) {
    				this.progress = result(this.source, 'progress');
    			}
    		}
    
    		Scaler.prototype.tick = function(frameTime, val) {
    			var scaling;
    			if (val == null) {
    				val = result(this.source, 'progress');
    			}
    			if (val >= 100) {
    				this.done = true;
    			}
    			if (val === this.last) {
    				this.sinceLastUpdate += frameTime;
    			} else {
    				if (this.sinceLastUpdate) {
    					this.rate = (val - this.last) / this.sinceLastUpdate;
    				}
    				this.catchup = (val - this.progress) / options.catchupTime;
    				this.sinceLastUpdate = 0;
    				this.last = val;
    			}
    			if (val > this.progress) {
    				this.progress += this.catchup * frameTime;
    			}
    			scaling = 1 - Math.pow(this.progress / 100, options.easeFactor);
    			this.progress += scaling * this.rate * frameTime;
    			this.progress = Math.min(this.lastProgress + options.maxProgressPerFrame, this.progress);
    			this.progress = Math.max(0, this.progress);
    			this.progress = Math.min(100, this.progress);
    			this.lastProgress = this.progress;
    			return this.progress;
    		};
    
    		return Scaler;
    
    	})();
    
    	sources = null;
    
    	scalers = null;
    
    	bar = null;
    
    	uniScaler = null;
    
    	animation = null;
    
    	cancelAnimation = null;
    
    	Pace.running = false;
    
    	handlePushState = function() {
    		if (options.restartOnPushState) {
    			return Pace.restart();
    		}
    	};
    
    	if (window.history.pushState != null) {
    		_pushState = window.history.pushState;
    		window.history.pushState = function() {
    			handlePushState();
    			return _pushState.apply(window.history, arguments);
    		};
    	}
    
    	if (window.history.replaceState != null) {
    		_replaceState = window.history.replaceState;
    		window.history.replaceState = function() {
    			handlePushState();
    			return _replaceState.apply(window.history, arguments);
    		};
    	}
    
    	SOURCE_KEYS = {
    		ajax: AjaxMonitor,
    		elements: ElementMonitor,
    		document: DocumentMonitor,
    		eventLag: EventLagMonitor
    	};
    
    	(init = function() {
    		var type, _j, _k, _len1, _len2, _ref2, _ref3, _ref4;
    		Pace.sources = sources = [];
    		_ref2 = ['ajax', 'elements', 'document', 'eventLag'];
    		for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
    			type = _ref2[_j];
    			if (options[type] !== false) {
    				sources.push(new SOURCE_KEYS[type](options[type]));
    			}
    		}
    		_ref4 = (_ref3 = options.extraSources) != null ? _ref3 : [];
    		for (_k = 0, _len2 = _ref4.length; _k < _len2; _k++) {
    			source = _ref4[_k];
    			sources.push(new source(options));
    		}
    		Pace.bar = bar = new Bar;
    		scalers = [];
    		return uniScaler = new Scaler;
    	})();
    
    	Pace.stop = function() {
    		Pace.trigger('stop');
    		Pace.running = false;
    		bar.destroy();
    		cancelAnimation = true;
    		if (animation != null) {
    			if (typeof cancelAnimationFrame === "function") {
    				cancelAnimationFrame(animation);
    			}
    			animation = null;
    		}
    		return init();
    	};
    
    	Pace.restart = function() {
    		Pace.trigger('restart');
    		Pace.stop();
    		return Pace.start();
    	};
    
    	Pace.go = function() {
    		var start;
    		Pace.running = true;
    		bar.render();
    		start = now();
    		cancelAnimation = false;
    		return animation = runAnimation(function(frameTime, enqueueNextFrame) {
    			var avg, count, done, element, elements, i, j, remaining, scaler, scalerList, sum, _j, _k, _len1, _len2, _ref2;
    			remaining = 100 - bar.progress;
    			count = sum = 0;
    			done = true;
    			for (i = _j = 0, _len1 = sources.length; _j < _len1; i = ++_j) {
    				source = sources[i];
    				scalerList = scalers[i] != null ? scalers[i] : scalers[i] = [];
    				elements = (_ref2 = source.elements) != null ? _ref2 : [source];
    				for (j = _k = 0, _len2 = elements.length; _k < _len2; j = ++_k) {
    					element = elements[j];
    					scaler = scalerList[j] != null ? scalerList[j] : scalerList[j] = new Scaler(element);
    					done &= scaler.done;
    					if (scaler.done) {
    						continue;
    					}
    					count++;
    					sum += scaler.tick(frameTime);
    				}
    			}
    			avg = sum / count;
    			bar.update(uniScaler.tick(frameTime, avg));
    			if (bar.done() || done || cancelAnimation) {
    				bar.update(100);
    				Pace.trigger('done');
    				return setTimeout(function() {
    					bar.finish();
    					Pace.running = false;
    					return Pace.trigger('hide');
    				}, Math.max(options.ghostTime, Math.max(options.minTime - (now() - start), 0)));
    			} else {
    				return enqueueNextFrame();
    			}
    		});
    	};
    
    	Pace.start = function(_options) {
    		extend(options, _options);
    		Pace.running = true;
    		try {
    			bar.render();
    		} catch (_error) {
    			NoTargetError = _error;
    		}
    		if (!document.querySelector('.pace')) {
    			return setTimeout(Pace.start, 50);
    		} else {
    			Pace.trigger('start');
    			return Pace.go();
    		}
    	};
    
    	if (typeof define === 'function' && define.amd) {
    		define(function() {
    			return Pace;
    		});
    	} else if (typeof exports === 'object') {
    		module.exports = Pace;
    	} else {
    		if (options.startOnPageLoad) {
    			Pace.start();
    		}
    	}
    
    }).call(this);

     

    pace.min.js 버전 

    !function(){function o(t,e){return function(){return t.apply(e,arguments)}}var u,c,i,s,n,y,t,l,v,r,a,p,e,h,w,b,f,g,d,m,k,S,q,L,x,P,T,R,j,O,E,M,A,C,N,_,F,U,W,X,D,H,I,z,G,B,J=[].slice,K={}.hasOwnProperty,Q=function(t,e){for(var n in e)K.call(e,n)&&(t[n]=e[n]);function r(){this.constructor=t}return r.prototype=e.prototype,t.prototype=new r,t.__super__=e.prototype,t},V=[].indexOf||function(t){for(var e=0,n=this.length;e<n;e++)if(e in this&&this[e]===t)return e;return-1};function Y(){}for(g={className:"",catchupTime:100,initialRate:.03,minTime:250,ghostTime:100,maxProgressPerFrame:20,easeFactor:1.25,startOnPageLoad:!0,restartOnPushState:!0,restartOnRequestAfter:500,target:"body",elements:{checkInterval:100,selectors:["body"]},eventLag:{minSamples:10,sampleCount:3,lagThreshold:3},ajax:{trackMethods:["GET"],trackWebSockets:!0,ignoreURLs:[]}},P=function(){var t;return null!=(t="undefined"!=typeof performance&&null!==performance&&"function"==typeof performance.now?performance.now():void 0)?t:+new Date},R=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame,f=window.cancelAnimationFrame||window.mozCancelAnimationFrame,p=function(t,e,n){if("function"==typeof t.addEventListener)return t.addEventListener(e,n,!1);var r;"function"!=typeof t["on"+e]||"object"!=typeof t["on"+e].eventListeners?(r=new s,"function"==typeof t["on"+e]&&r.on(e,t["on"+e]),t["on"+e]=function(t){return r.trigger(e,t)},t["on"+e].eventListeners=r):r=t["on"+e].eventListeners,r.on(e,n)},null==R&&(R=function(t){return setTimeout(t,50)},f=function(t){return clearTimeout(t)}),O=function(e){var n=P(),r=function(){var t=P()-n;return 33<=t?(n=P(),e(t,function(){return R(r)})):setTimeout(r,33-t)};return r()},j=function(){var t=arguments[0],e=arguments[1],n=3<=arguments.length?J.call(arguments,2):[];return"function"==typeof t[e]?t[e].apply(t,n):t[e]},d=function(){for(var t,e,n,r=arguments[0],s=2<=arguments.length?J.call(arguments,1):[],o=0,i=s.length;o<i;o++)if(e=s[o])for(t in e)K.call(e,t)&&(n=e[t],null!=r[t]&&"object"==typeof r[t]&&null!=n&&"object"==typeof n?d(r[t],n):r[t]=n);return r},h=function(t){for(var e,n,r=e=0,s=0,o=t.length;s<o;s++)n=t[s],r+=Math.abs(n),e++;return r/e},k=function(t,e){var n,r;if(null==t&&(t="options"),null==e&&(e=!0),r=document.querySelector("[data-pace-"+t+"]")){if(n=r.getAttribute("data-pace-"+t),!e)return n;try{return JSON.parse(n)}catch(t){return"undefined"!=typeof console&&null!==console?console.error("Error parsing inline pace options",t):void 0}}},Y.prototype.on=function(t,e,n,r){var s;return null==r&&(r=!1),null==this.bindings&&(this.bindings={}),null==(s=this.bindings)[t]&&(s[t]=[]),this.bindings[t].push({handler:e,ctx:n,once:r})},Y.prototype.once=function(t,e,n){return this.on(t,e,n,!0)},Y.prototype.off=function(t,e){var n,r,s;if(null!=(null!=(r=this.bindings)?r[t]:void 0)){if(null==e)return delete this.bindings[t];for(n=0,s=[];n<this.bindings[t].length;)this.bindings[t][n].handler===e?s.push(this.bindings[t].splice(n,1)):s.push(n++);return s}},Y.prototype.trigger=function(){var t,e,n,r,s,o,i=arguments[0],a=2<=arguments.length?J.call(arguments,1):[];if(null!=(r=this.bindings)&&r[i]){for(n=0,o=[];n<this.bindings[i].length;)e=(s=this.bindings[i][n]).handler,t=s.ctx,s=s.once,e.apply(null!=t?t:this,a),s?o.push(this.bindings[i].splice(n,1)):o.push(n++);return o}},B=Y,y=window.Pace||{},window.Pace=y,d(y,B.prototype),T=y.options=d({},g,window.paceOptions,k()),X=0,H=(z=["ajax","document","eventLag","elements"]).length;X<H;X++)!0===T[C=z[X]]&&(T[C]=g[C]);function Z(){return Z.__super__.constructor.apply(this,arguments)}function $(){this.progress=0}function tt(){this.bindings={}}function et(){var e,o=this;et.__super__.constructor.apply(this,arguments),e=function(r){var s=r.open;return r.open=function(t,e,n){return A(t)&&o.trigger("request",{type:t,url:e,request:r}),s.apply(r,arguments)}},window.XMLHttpRequest=function(t){t=new W(t);return e(t),t};try{m(window.XMLHttpRequest,W)}catch(t){}if(null!=U){window.XDomainRequest=function(){var t=new U;return e(t),t};try{m(window.XDomainRequest,U)}catch(t){}}if(null!=F&&T.ajax.trackWebSockets){window.WebSocket=function(t,e){var n=null!=e?new F(t,e):new F(t);return A("socket")&&o.trigger("request",{type:"socket",url:t,protocols:e,request:n}),n};try{m(window.WebSocket,F)}catch(t){}}}function nt(){this.complete=o(this.complete,this);var t=this;this.elements=[],S().on("request",function(){return t.watch.apply(t,arguments)})}function rt(t){var e,n,r,s;for(null==t&&(t={}),this.complete=o(this.complete,this),this.elements=[],null==t.selectors&&(t.selectors=[]),n=0,r=(s=t.selectors).length;n<r;n++)e=s[n],this.elements.push(new i(e,this.complete))}function st(t,e){this.selector=t,this.completeCallback=e,this.progress=0,this.check()}function ot(){var t,e,n=this;this.progress=null!=(e=this.states[document.readyState])?e:100,t=document.onreadystatechange,document.onreadystatechange=function(){return null!=n.states[document.readyState]&&(n.progress=n.states[document.readyState]),"function"==typeof t?t.apply(null,arguments):void 0}}function it(t){this.source=t,this.last=this.sinceLastUpdate=0,this.rate=T.initialRate,this.catchup=0,this.progress=this.lastProgress=0,null!=this.source&&(this.progress=j(this.source,"progress"))}B=Error,Q(Z,B),n=Z,$.prototype.getElement=function(){var t;if(null==this.el){if(!(t=document.querySelector(T.target)))throw new n;this.el=document.createElement("div"),this.el.className="pace pace-active",document.body.className=document.body.className.replace(/(pace-done )|/,"pace-running ");var e=""!==T.className?" "+T.className:"";this.el.innerHTML='<div class="pace-progress'+e+'">\n  <div class="pace-progress-inner"></div>\n</div>\n<div class="pace-activity"></div>',null!=t.firstChild?t.insertBefore(this.el,t.firstChild):t.appendChild(this.el)}return this.el},$.prototype.finish=function(){var t=this.getElement();return t.className=t.className.replace("pace-active","pace-inactive"),document.body.className=document.body.className.replace("pace-running ","pace-done ")},$.prototype.update=function(t){return this.progress=t,y.trigger("progress",t),this.render()},$.prototype.destroy=function(){try{this.getElement().parentNode.removeChild(this.getElement())}catch(t){n=t}return this.el=void 0},$.prototype.render=function(){var t,e,n,r,s,o,i;if(null==document.querySelector(T.target))return!1;for(t=this.getElement(),r="translate3d("+this.progress+"%, 0, 0)",s=0,o=(i=["webkitTransform","msTransform","transform"]).length;s<o;s++)e=i[s],t.children[0].style[e]=r;return(!this.lastRenderedProgress||this.lastRenderedProgress|0!==this.progress|0)&&(t.children[0].setAttribute("data-progress-text",(0|this.progress)+"%"),100<=this.progress?n="99":(n=this.progress<10?"0":"",n+=0|this.progress),t.children[0].setAttribute("data-progress",""+n)),y.trigger("change",this.progress),this.lastRenderedProgress=this.progress},$.prototype.done=function(){return 100<=this.progress},c=$,tt.prototype.trigger=function(t,e){var n,r,s,o,i;if(null!=this.bindings[t]){for(i=[],r=0,s=(o=this.bindings[t]).length;r<s;r++)n=o[r],i.push(n.call(this,e));return i}},tt.prototype.on=function(t,e){var n;return null==(n=this.bindings)[t]&&(n[t]=[]),this.bindings[t].push(e)},s=tt,W=window.XMLHttpRequest,U=window.XDomainRequest,F=window.WebSocket,m=function(t,e){var n,r=[];for(n in e.prototype)try{null==t[n]&&"function"!=typeof e[n]?"function"==typeof Object.defineProperty?r.push(Object.defineProperty(t,n,{get:function(t){return function(){return e.prototype[t]}}(n),configurable:!0,enumerable:!0})):r.push(t[n]=e.prototype[n]):r.push(void 0)}catch(t){0}return r},L=[],y.ignore=function(){var t=arguments[0],e=2<=arguments.length?J.call(arguments,1):[];return L.unshift("ignore"),e=t.apply(null,e),L.shift(),e},y.track=function(){var t=arguments[0],e=2<=arguments.length?J.call(arguments,1):[];return L.unshift("track"),e=t.apply(null,e),L.shift(),e},A=function(t){if(null==t&&(t="GET"),"track"===L[0])return"force";if(!L.length&&T.ajax){if("socket"===t&&T.ajax.trackWebSockets)return!0;if(t=t.toUpperCase(),0<=V.call(T.ajax.trackMethods,t))return!0}return!1},Q(et,s),t=et,D=null,M=function(t){for(var e,n=T.ajax.ignoreURLs,r=0,s=n.length;r<s;r++)if("string"==typeof(e=n[r])){if(-1!==t.indexOf(e))return!0}else if(e.test(t))return!0;return!1},(S=function(){return D=null==D?new t:D})().on("request",function(t){var o,i=t.type,a=t.request,e=t.url;if(!M(e))return y.running||!1===T.restartOnRequestAfter&&"force"!==A(i)?void 0:(o=arguments,"boolean"==typeof(e=T.restartOnRequestAfter||0)&&(e=0),setTimeout(function(){var t,e,n,r,s="socket"===i?a.readyState<1:0<(s=a.readyState)&&s<4;if(s){for(y.restart(),r=[],t=0,e=(n=y.sources).length;t<e;t++){if((C=n[t])instanceof u){C.watch.apply(C,o);break}r.push(void 0)}return r}},e))}),nt.prototype.watch=function(t){var e=t.type,n=t.request,t=t.url;if(!M(t))return n=new("socket"===e?r:a)(n,this.complete),this.elements.push(n)},nt.prototype.complete=function(e){return this.elements=this.elements.filter(function(t){return t!==e})},u=nt,a=function(e,n){var t,r,s,o,i=this;if(this.progress=0,null!=window.ProgressEvent)for(p(e,"progress",function(t){return t.lengthComputable?i.progress=100*t.loaded/t.total:i.progress=i.progress+(100-i.progress)/2}),t=0,r=(o=["load","abort","timeout","error"]).length;t<r;t++)p(e,o[t],function(){return n(i),i.progress=100});else s=e.onreadystatechange,e.onreadystatechange=function(){var t;return 0===(t=e.readyState)||4===t?(n(i),i.progress=100):3===e.readyState&&(i.progress=50),"function"==typeof s?s.apply(null,arguments):void 0}},r=function(t,e){for(var n,r=this,s=this.progress=0,o=(n=["error","open"]).length;s<o;s++)p(t,n[s],function(){return e(r),r.progress=100})},rt.prototype.complete=function(e){return this.elements=this.elements.filter(function(t){return t!==e})},k=rt,st.prototype.check=function(){var t=this;return document.querySelector(this.selector)?this.done():setTimeout(function(){return t.check()},T.elements.checkInterval)},st.prototype.done=function(){return this.completeCallback(this),this.completeCallback=null,this.progress=100},i=st,ot.prototype.states={loading:0,interactive:50,complete:100},B=ot,Q=function(){var e,n,r,s,o,i=this;this.progress=0,o=[],s=0,r=P(),n=setInterval(function(){var t=P()-r-50;return r=P(),o.push(t),o.length>T.eventLag.sampleCount&&o.shift(),e=h(o),++s>=T.eventLag.minSamples&&e<T.eventLag.lagThreshold?(i.progress=100,clearInterval(n)):i.progress=3/(e+3)*100},50)},it.prototype.tick=function(t,e){return 100<=(e=null==e?j(this.source,"progress"):e)&&(this.done=!0),e===this.last?this.sinceLastUpdate+=t:(this.sinceLastUpdate&&(this.rate=(e-this.last)/this.sinceLastUpdate),this.catchup=(e-this.progress)/T.catchupTime,this.sinceLastUpdate=0,this.last=e),e>this.progress&&(this.progress+=this.catchup*t),e=1-Math.pow(this.progress/100,T.easeFactor),this.progress+=e*this.rate*t,this.progress=Math.min(this.lastProgress+T.maxProgressPerFrame,this.progress),this.progress=Math.max(0,this.progress),this.progress=Math.min(100,this.progress),this.lastProgress=this.progress,this.progress},v=it,b=e=_=w=E=N=null,y.running=!1,q=function(){if(T.restartOnPushState)return y.restart()},null!=window.history.pushState&&(I=window.history.pushState,window.history.pushState=function(){return q(),I.apply(window.history,arguments)}),null!=window.history.replaceState&&(G=window.history.replaceState,window.history.replaceState=function(){return q(),G.apply(window.history,arguments)}),l={ajax:u,elements:k,document:B,eventLag:Q},(x=function(){var t,e,n,r,s,o,i,a;for(y.sources=N=[],e=0,r=(o=["ajax","elements","document","eventLag"]).length;e<r;e++)!1!==T[t=o[e]]&&N.push(new l[t](T[t]));for(n=0,s=(a=null!=(i=T.extraSources)?i:[]).length;n<s;n++)C=a[n],N.push(new C(T));return y.bar=w=new c,E=[],_=new v})(),y.stop=function(){return y.trigger("stop"),y.running=!1,w.destroy(),b=!0,null!=e&&("function"==typeof f&&f(e),e=null),x()},y.restart=function(){return y.trigger("restart"),y.stop(),y.start()},y.go=function(){var m;return y.running=!0,w.render(),m=P(),b=!1,e=O(function(t,e){w.progress;for(var n,r,s,o,i,a,u,c,l,p,h=a=0,f=!0,g=u=0,d=N.length;u<d;g=++u)for(C=N[g],i=null!=E[g]?E[g]:E[g]=[],s=c=0,l=(r=null!=(p=C.elements)?p:[C]).length;c<l;s=++c)o=r[s],f&=(o=null!=i[s]?i[s]:i[s]=new v(o)).done,o.done||(h++,a+=o.tick(t));return n=a/h,w.update(_.tick(t,n)),w.done()||f||b?(w.update(100),y.trigger("done"),setTimeout(function(){return w.finish(),y.running=!1,y.trigger("hide")},Math.max(T.ghostTime,Math.max(T.minTime-(P()-m),0)))):e()})},y.start=function(t){d(T,t),y.running=!0;try{w.render()}catch(t){n=t}return document.querySelector(".pace")?(y.trigger("start"),y.go()):setTimeout(y.start,50)},"function"==typeof define&&define.amd?define(function(){return y}):"object"==typeof exports?module.exports=y:T.startOnPageLoad&&y.start()}.call(this);

     

     

    로딩바 결론

    PWA를 더욱 네이트브 앱 처럼 하기위한 오늘의 것은 바로 페이지 상단에 노출되는 로딩 진행바 였습니다. 생각보다 손쉽게 적용할 수 있어서 다행이며, 이 글을 보시는 분들도 필요하시면 적용해 보시길 바랍니다. 오늘은 로딩 진행바 였습니다.

     

    소스를 공개해 주신 HubSpot, Inc. 에 감사드리며, Licensed MIT 저작권 공개소스 원칙에 따라 PWA를 적용하시는 많은 개발자 분들의 활용되었으면 하는 바램이네요. https://www.hubspot.com/

     

    HubSpot | Software, Tools, Resources for Your Business

    HubSpot's CRM platform contains the marketing, sales, service, operations, and website-building software you need to grow your business.

    www.hubspot.com

    Licensed MIT란?

     

     

     

    다른 사람들이 많이 본 콘텐츠