diff --git a/404.html b/404.html new file mode 100644 index 00000000..fac0f047 --- /dev/null +++ b/404.html @@ -0,0 +1,21 @@ + + + + + + 404 | Vue3 入门指南与实战案例 + + + + + + + + + + +
Skip to content

404

PAGE NOT FOUND

But if you don't change your direction, and if you keep looking, you may end up where you are heading.

Released under the MIT License.

+ + + + \ No newline at end of file diff --git a/CNAME b/CNAME new file mode 100644 index 00000000..cbf63dd1 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +vue3.chengpeiquan.com \ No newline at end of file diff --git a/ads.txt b/ads.txt new file mode 100644 index 00000000..2082b131 --- /dev/null +++ b/ads.txt @@ -0,0 +1 @@ +google.com, pub-7109929923549092, DIRECT, f08c47fec0942fa0 \ No newline at end of file diff --git a/assets/app.9f5d8b3e.js b/assets/app.9f5d8b3e.js new file mode 100644 index 00000000..ba7f68cb --- /dev/null +++ b/assets/app.9f5d8b3e.js @@ -0,0 +1,197 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{u as Fe,A as ke,v as Ie,b as Ae,e as bn,f as Wn,F as qe,_ as He,d as Be,r as Tn,l as wn,D as Cn,X as xn,B as jn,R as In,I as ye,a2 as An,c as Ln,H as Rn,a3 as Un,a4 as Bn,a5 as Kn,a6 as Sn,a7 as Nn,a8 as zn,a9 as Fn,aa as kn,ab as Hn,ac as Yn,ad as Vn,ae as Xn,af as Gn,S as Qn}from"./chunks/framework.0d8bea05.js";import{t as $n}from"./chunks/theme.edde7d9a.js";function Oe(){return typeof window<"u"?navigator.userAgent.toLowerCase():""}const Ye=typeof window<"u";/Android/i.test(Oe());/iPhone|iPod|iPad|iOS/i.test(Oe());/uni-app|html5plus/.test(Oe());/MicroMessenger/i.test(Oe());/\sQQ|mqqbrowser|qzone|qqbrowser/i.test(Oe());/mqqbrowser|qqbrowser/i.test(Oe());/qzone\/.*_qz_([\d.]+)/i.test(Oe());/(weibo).*weibo__([\d.]+)/i.test(Oe());/(baiduboxapp)\/([\d.]+)/i.test(Oe());function Jn([R,...H]){return R?R.toUpperCase()+H.join(""):""}function Zn([R,...H]){return R?(R.toLowerCase()+H.join("")).replace(/[-_](\w)/g,(l,i)=>i?i.toUpperCase():""):""}function qn(R){return R?Jn(Zn(R)):""}function er({type:R,id:H,resource:l}){return new Promise((i,e)=>{if(!Ye||document.querySelector(`#${H}`)){e();return}function t(a){a.addEventListener("load",i),a.addEventListener("error",e),a.addEventListener("abort",e)}switch(R){case"js":{const a=document.createElement("script");a.id=H,a.async=!0,a.src=l,t(a),document.head.appendChild(a);break}case"css":{const a=document.createElement("link");a.id=H,a.rel="stylesheet",a.href=l,t(a),document.head.appendChild(a);break}case"style":{const a=document.createElement("style");a.id=H,t(a),document.head.appendChild(a),a.appendChild(document.createTextNode(l));break}}})}function tr(){try{return"production"}catch{return}}tr();function Ve(R){try{return R==="production"}catch{return!1}}Ve("development");Ve("test");Ve("production");var ar=Object.defineProperty,nr=(R,H,l)=>H in R?ar(R,H,{enumerable:!0,configurable:!0,writable:!0,value:l}):R[H]=l,Ce=(R,H,l)=>(nr(R,typeof H!="symbol"?H+"":H,l),l);const rr="web-analytics-core",Xe=["baidu","cnzz"];var Te=(R=>(R.SetAutoPageview="_setAutoPageview",R.SetAccount="_setAccount",R.TrackPageview="_trackPageview",R.TrackEvent="_trackEvent",R))(Te||{});function ir({pluginId:R,platform:H,websiteId:l,propertyKey:i,args:e}){const t=[];switch(i){case"loadSdk":{t.push("JS-SDK load done.");break}case"setAccount":{t.push("set account done.");break}case"trackPageview":{const{pageUrl:r,fromUrl:o}=e[0]||{};t.push("track pageview done."),t.push(`pageUrl: ${Le(r)}`),H==="cnzz"&&o&&t.push(`fromUrl: ${Le(o)}`);break}case"trackEvent":{const{category:r,action:o,label:n,value:s,nodeId:u}=e[0]||{};t.push("track event done."),t.push(`category: ${r}`),t.push(`action: ${o}`),t.push(`label: ${Se(n)}`),t.push(`value: ${Ne(s)}`),H==="cnzz"&&t.push(`nodeId: ${et(u)}`);break}}t.push(`websiteId: ${l}`),t.push(`time: ${new Date}`);const a=t.join(` +`);return` +[${R}] ${qn(H)} Analytics ${a} + +`}function Le(R){if((!R||typeof R!="string")&&(R="/"),R.startsWith("http")){const H=R.split("/"),l=`${H[0]}//${H[2]}`;R=R.replace(l,"")}return R}function or(R){return(!R||R&&typeof R!="string")&&(R=""),typeof R=="string"&&!R.includes("http")&&(R=""),R}function et(R){return(!R||typeof R!="string")&&(R=""),R}function Se(R){return(!R||typeof R!="string")&&(R=""),R}function Ne(R){return(!R||!Number(R))&&(R=1),R}function Ge(R,H,l){const i=l.value;return l.value=function(...e){const{debug:t,pluginId:a,platform:r,websiteId:o}=this;if(t){const n=ir({pluginId:a,platform:r,websiteId:o,propertyKey:H,args:e});console.log(n)}i.apply(this,e)},l}function sr(R,H,l){const i=l.value;return l.value=function(...e){const[t]=e,{category:a,action:r}=t;if(typeof a!="string"||typeof r!="string"||!a||!r){this.throwError("Valid `category` and `action` are missing from the track event options.");return}i.apply(this,e)},l}var ur=Object.defineProperty,dr=Object.getOwnPropertyDescriptor,lr=(R,H,l,i)=>{for(var e=i>1?void 0:i?dr(H,l):H,t=R.length-1,a;t>=0;t--)(a=R[t])&&(e=(i?a(H,l,e):a(e))||e);return i&&e&&ur(H,l,e),e};class tt{constructor({pluginId:H,platform:l,websiteId:i,debug:e}){Ce(this,"pluginId"),Ce(this,"platform"),Ce(this,"sdkInstance"),Ce(this,"sdkUrl",""),Ce(this,"websiteId",""),Ce(this,"debug"),this.pluginId=H||rr,this.platform=l,this.websiteId=i,this.debug=e??!1,this.updatePlatformInfo(),this.loadSdk()}updatePlatformInfo(){if(Ye)switch(this.platform){case"baidu":{this.sdkInstance=window._hmt||[],this.sdkUrl=`https://hm.baidu.com/hm.js?${this.websiteId}`;break}case"cnzz":{this.sdkInstance=window._czc||[],this.sdkUrl=`https://v1.cnzz.com/z_stat.php?id=${this.websiteId}&web_id=${this.websiteId}`;break}default:{const H=Xe.join(", ");this.throwError(`Unsupported platform options, only supported: ${H}.`)}}}loadSdk(){!this.sdkInstance||!this.sdkUrl||(this.sdkInstance.push([Te.SetAutoPageview,!1]),er({type:"js",id:`${this.pluginId}-${this.platform}-${this.websiteId}`,resource:this.sdkUrl}).catch(H=>{console.log(H)}))}throwError(H){throw new Error(`[${this.pluginId}] ${H}`)}}lr([Ge],tt.prototype,"loadSdk",1);var mr=Object.defineProperty,fr=Object.getOwnPropertyDescriptor,at=(R,H,l,i)=>{for(var e=i>1?void 0:i?fr(H,l):H,t=R.length-1,a;t>=0;t--)(a=R[t])&&(e=(i?a(H,l,e):a(e))||e);return i&&e&&mr(H,l,e),e};let Re=class extends tt{constructor({pluginId:H,platform:l,websiteId:i,debug:e}){super({pluginId:H,platform:l,websiteId:i,debug:e})}setAccount(){this.sdkInstance&&this.sdkInstance.push([Te.SetAccount,this.websiteId])}trackPageview({pageUrl:H,fromUrl:l}){if(this.sdkInstance)switch(this.setAccount(),this.platform){case"baidu":{this.sdkInstance.push([Te.TrackPageview,Le(H)]);break}case"cnzz":{this.sdkInstance.push([Te.TrackPageview,Le(H),l?or(l):""]);break}}}trackEvent({category:H,action:l,label:i,value:e,nodeId:t}){if(this.sdkInstance)switch(this.setAccount(),this.platform){case"baidu":{this.sdkInstance.push([Te.TrackEvent,H,l,Se(i),Ne(e)]);break}case"cnzz":{this.sdkInstance.push([Te.TrackEvent,H,l,Se(i),Ne(e),et(t)]);break}}}};at([Ge],Re.prototype,"trackPageview",1);at([Ge,sr],Re.prototype,"trackEvent",1);function cr(R){return new Re({platform:"baidu",...R})}function _r(R){return new Re({platform:"cnzz",...R})}var hr=Object.defineProperty,vr=(R,H,l)=>H in R?hr(R,H,{enumerable:!0,configurable:!0,writable:!0,value:l}):R[H]=l,je=(R,H,l)=>(vr(R,typeof H!="symbol"?H+"":H,l),l);let gr=class{constructor({platform:H,websiteIds:l,debug:i}){je(this,"pluginId"),je(this,"platform"),je(this,"websiteIds"),je(this,"instances"),je(this,"debug"),this.pluginId="web-analytics-vue",this.platform=H,this.websiteIds=l||[],this.instances=[],this.debug=i??!1,this.init()}init(){try{switch(this.platform){case"baidu":{this.websiteIds.forEach(H=>{const l=cr({pluginId:this.pluginId,websiteId:H,debug:this.debug});this.instances.push(l)});break}case"cnzz":{this.websiteIds.forEach(H=>{const l=_r({pluginId:this.pluginId,websiteId:H,debug:this.debug});this.instances.push(l)});break}default:{const H=Xe.join(", ");this.throwError(`Unsupported platform options, only supported: ${H}.`);break}}}catch(H){this.throwError(`Plugin initialization failed: ${H}.`)}}trackPageview(H){this.instances.forEach(l=>l.trackPageview({...H}))}trackEvent(H){this.instances.forEach(l=>l.trackEvent({...H}))}throwError(H){throw new Error(`[${this.pluginId}] ${H}`)}};function Pr({version:R}){try{const[H]=R.split(".");return H?Number(H):3}catch{return 3}}function Mr(R){return Xe.includes(R)?`$${R}Analytics`:""}function Er({analytics:R,platform:H}){return(l,i)=>{if(!Ye)return;const e=new gr({platform:H,...i});for(const r in e)Object.prototype.hasOwnProperty.call(e,r)&&(R[r]=e[r]);const t=Object.getPrototypeOf(e);Object.setPrototypeOf(R,t);const a=Mr(H);if(a)switch(Pr(l)){case 2:{l.prototype[a]=R;break}case 3:{l.config.globalProperties[a]=R;break}}}}function pr(){const R={};return{baiduAnalytics:R,registerBaiduAnalytics:Er({analytics:R,platform:"baidu"})}}var Dr=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function nt(R){return R&&R.__esModule&&Object.prototype.hasOwnProperty.call(R,"default")?R.default:R}var rt={exports:{}},it={exports:{}};(function(){var R="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",H={rotl:function(l,i){return l<>>32-i},rotr:function(l,i){return l<<32-i|l>>>i},endian:function(l){if(l.constructor==Number)return H.rotl(l,8)&16711935|H.rotl(l,24)&4278255360;for(var i=0;i0;l--)i.push(Math.floor(Math.random()*256));return i},bytesToWords:function(l){for(var i=[],e=0,t=0;e>>5]|=l[e]<<24-t%32;return i},wordsToBytes:function(l){for(var i=[],e=0;e>>5]>>>24-e%32&255);return i},bytesToHex:function(l){for(var i=[],e=0;e>>4).toString(16)),i.push((l[e]&15).toString(16));return i.join("")},hexToBytes:function(l){for(var i=[],e=0;e>>6*(3-a)&63)):i.push("=");return i.join("")},base64ToBytes:function(l){l=l.replace(/[^A-Z0-9+\/]/ig,"");for(var i=[],e=0,t=0;e>>6-t*2);return i}};it.exports=H})();var yr=it.exports,ze={utf8:{stringToBytes:function(R){return ze.bin.stringToBytes(unescape(encodeURIComponent(R)))},bytesToString:function(R){return decodeURIComponent(escape(ze.bin.bytesToString(R)))}},bin:{stringToBytes:function(R){for(var H=[],l=0;l + * @license MIT + */var Or=function(R){return R!=null&&(ot(R)||br(R)||!!R._isBuffer)};function ot(R){return!!R.constructor&&typeof R.constructor.isBuffer=="function"&&R.constructor.isBuffer(R)}function br(R){return typeof R.readFloatLE=="function"&&typeof R.slice=="function"&&ot(R.slice(0,0))}(function(){var R=yr,H=Je.utf8,l=Or,i=Je.bin,e=function(t,a){t.constructor==String?a&&a.encoding==="binary"?t=i.stringToBytes(t):t=H.stringToBytes(t):l(t)?t=Array.prototype.slice.call(t,0):!Array.isArray(t)&&t.constructor!==Uint8Array&&(t=t.toString());for(var r=R.bytesToWords(t),o=t.length*8,n=1732584193,s=-271733879,u=-1732584194,d=271733878,m=0;m>>24)&16711935|(r[m]<<24|r[m]>>>8)&4278255360;r[o>>>5]|=128<>>9<<4)+14]=o;for(var f=e._ff,_=e._gg,c=e._hh,v=e._ii,m=0;m>>0,s=s+P>>>0,u=u+h>>>0,d=d+y>>>0}return R.endian([n,s,u,d])};e._ff=function(t,a,r,o,n,s,u){var d=t+(a&r|~a&o)+(n>>>0)+u;return(d<>>32-s)+a},e._gg=function(t,a,r,o,n,s,u){var d=t+(a&o|r&~o)+(n>>>0)+u;return(d<>>32-s)+a},e._hh=function(t,a,r,o,n,s,u){var d=t+(a^r^o)+(n>>>0)+u;return(d<>>32-s)+a},e._ii=function(t,a,r,o,n,s,u){var d=t+(r^(a|~o))+(n>>>0)+u;return(d<>>32-s)+a},e._blocksize=16,e._digestsize=16,rt.exports=function(t,a){if(t==null)throw new Error("Illegal argument "+t);var r=R.wordsToBytes(e(t,a));return a&&a.asBytes?r:a&&a.asString?i.bytesToString(r):R.bytesToHex(r)}})();var Wr=rt.exports;const Tr=nt(Wr);var st={exports:{}};(function(R,H){(function(i,e){R.exports=e()})(Dr,function(){return function(l){var i={};function e(t){if(i[t])return i[t].exports;var a=i[t]={i:t,l:!1,exports:{}};return l[t].call(a.exports,a,a.exports,e),a.l=!0,a.exports}return e.m=l,e.c=i,e.d=function(t,a,r){e.o(t,a)||Object.defineProperty(t,a,{configurable:!1,enumerable:!0,get:r})},e.n=function(t){var a=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(a,"a",a),a},e.o=function(t,a){return Object.prototype.hasOwnProperty.call(t,a)},e.p="/dist",e(e.s=191)}([function(l,i,e){i.a=t;function t(a,r){if(r.length1?"s":"")+" required, but only "+r.length+" present")}},function(l,i,e){i.a=a;var t=e(0);function a(r){t.a(1,arguments);var o=Object.prototype.toString.call(r);return r instanceof Date||typeof r=="object"&&o==="[object Date]"?new Date(r.getTime()):typeof r=="number"||o==="[object Number]"?new Date(r):((typeof r=="string"||o==="[object String]")&&typeof console<"u"&&(console.warn("Starting with v2.0.0-beta.1 date-fns doesn't accept strings as date arguments. Please use `parseISO` to parse strings. See: https://git.io/fjule"),console.warn(new Error().stack)),new Date(NaN))}},function(l,i,e){i.a=t;function t(a){return function(r){var o=r||{},n=o.width?String(o.width):a.defaultWidth,s=a.formats[n]||a.formats[a.defaultWidth];return s}}},function(l,i,e){i.a=t;function t(a){return function(r,o){var n=o||{},s=n.context?String(n.context):"standalone",u;if(s==="formatting"&&a.formattingValues){var d=a.defaultFormattingWidth||a.defaultWidth,m=n.width?String(n.width):d;u=a.formattingValues[m]||a.formattingValues[d]}else{var f=a.defaultWidth,_=n.width?String(n.width):a.defaultWidth;u=a.values[_]||a.values[f]}var c=a.argumentCallback?a.argumentCallback(r):r;return u[c]}}},function(l,i,e){i.a=t;function t(a){return function(r,o){var n=String(r),s=o||{},u=n.match(a.matchPattern);if(!u)return null;var d=u[0],m=n.match(a.parsePattern);if(!m)return null;var f=a.valueCallback?a.valueCallback(m[0]):m[0];return f=s.valueCallback?s.valueCallback(f):f,{value:f,rest:n.slice(d.length)}}}},function(l,i,e){i.a=t;function t(o){return function(n,s){var u=String(n),d=s||{},m=d.width,f=m&&o.matchPatterns[m]||o.matchPatterns[o.defaultMatchWidth],_=u.match(f);if(!_)return null;var c=_[0],v=m&&o.parsePatterns[m]||o.parsePatterns[o.defaultParseWidth],g;return Object.prototype.toString.call(v)==="[object Array]"?g=r(v,function(P){return P.test(c)}):g=a(v,function(P){return P.test(c)}),g=o.valueCallback?o.valueCallback(g):g,g=d.valueCallback?d.valueCallback(g):g,{value:g,rest:u.slice(c.length)}}}function a(o,n){for(var s in o)if(o.hasOwnProperty(s)&&n(o[s]))return s}function r(o,n){for(var s=0;s"u"}function n(M){return M!==null&&!o(M)&&M.constructor!==null&&!o(M.constructor)&&typeof M.constructor.isBuffer=="function"&&M.constructor.isBuffer(M)}function s(M){return a.call(M)==="[object ArrayBuffer]"}function u(M){return typeof FormData<"u"&&M instanceof FormData}function d(M){var D;return typeof ArrayBuffer<"u"&&ArrayBuffer.isView?D=ArrayBuffer.isView(M):D=M&&M.buffer&&M.buffer instanceof ArrayBuffer,D}function m(M){return typeof M=="string"}function f(M){return typeof M=="number"}function _(M){return M!==null&&typeof M=="object"}function c(M){return a.call(M)==="[object Date]"}function v(M){return a.call(M)==="[object File]"}function g(M){return a.call(M)==="[object Blob]"}function P(M){return a.call(M)==="[object Function]"}function h(M){return _(M)&&P(M.pipe)}function y(M){return typeof URLSearchParams<"u"&&M instanceof URLSearchParams}function W(M){return M.replace(/^\s*/,"").replace(/\s*$/,"")}function T(){return typeof navigator<"u"&&(navigator.product==="ReactNative"||navigator.product==="NativeScript"||navigator.product==="NS")?!1:typeof window<"u"&&typeof document<"u"}function j(M,D){if(!(M===null||typeof M>"u"))if(typeof M!="object"&&(M=[M]),r(M))for(var p=0,w=M.length;p"u"||!t.env||!1;function v(){return null}var g=o.h("a",null).constructor;g.prototype.$$typeof=u,g.prototype.preactCompatUpgraded=!1,g.prototype.preactCompatNormalized=!1,Object.defineProperty(g.prototype,"type",{get:function(){return this.nodeName},set:function(x){this.nodeName=x},configurable:!0}),Object.defineProperty(g.prototype,"props",{get:function(){return this.attributes},set:function(x){this.attributes=x},configurable:!0});var P=o.options.event;o.options.event=function(x){return P&&(x=P(x)),x.persist=Object,x.nativeEvent=x,x};var h=o.options.vnode;o.options.vnode=function(x){if(!x.preactCompatUpgraded){x.preactCompatUpgraded=!0;var N=x.nodeName,Y=x.attributes=J({},x.attributes);typeof N=="function"?(N[d]===!0||N.prototype&&"isReactComponent"in N.prototype)&&(x.children&&String(x.children)===""&&(x.children=void 0),x.children&&(Y.children=x.children),x.preactCompatNormalized||oe(x),y(x)):(x.children&&String(x.children)===""&&(x.children=void 0),x.children&&(Y.children=x.children),Y.defaultValue&&(!Y.value&&Y.value!==0&&(Y.value=Y.defaultValue),delete Y.defaultValue),W(x,Y))}h&&h(x)};function y(x){var N=x.nodeName,Y=x.attributes;x.attributes={},N.defaultProps&&J(x.attributes,N.defaultProps),Y&&J(x.attributes,Y)}function W(x,N){var Y,$,Q;if(N){for(Q in N)if(Y=f.test(Q))break;if(Y){$=x.attributes={};for(Q in N)N.hasOwnProperty(Q)&&($[f.test(Q)?Q.replace(/([A-Z0-9])/,"-$1").toLowerCase():Q]=N[Q])}}}function T(x,N,Y){var $=N&&N._preactCompatRendered&&N._preactCompatRendered.base;$&&$.parentNode!==N&&($=null),$||($=N.children[0]);for(var Q=N.childNodes.length;Q--;)N.childNodes[Q]!==$&&N.removeChild(N.childNodes[Q]);var ae=o.render(x,N,$);return N&&(N._preactCompatRendered=ae&&(ae._component||{base:ae})),typeof Y=="function"&&Y(),ae&&ae._component||ae}var j=function(){};j.prototype.getChildContext=function(){return this.props.context},j.prototype.render=function(x){return x.children[0]};function b(x,N,Y,$){var Q=o.h(j,{context:x.context},N),ae=T(Q,Y);return $&&$(ae),ae._component||ae.base}function C(x){var N=x._preactCompatRendered&&x._preactCompatRendered.base;return N&&N.parentNode===x?(o.render(o.h(v),x,N),!0):!1}var I=[],M={map:function(x,N,Y){return x==null?null:(x=M.toArray(x),Y&&Y!==x&&(N=N.bind(Y)),x.map(N))},forEach:function(x,N,Y){if(x==null)return null;x=M.toArray(x),Y&&Y!==x&&(N=N.bind(Y)),x.forEach(N)},count:function(x){return x&&x.length||0},only:function(x){if(x=M.toArray(x),x.length!==1)throw new Error("Children.only() expects only one child.");return x[0]},toArray:function(x){return x==null?[]:Array.isArray&&Array.isArray(x)?x:I.concat(x)}},D;function p(x){return ue.bind(null,x)}for(var w={},L=s.length;L--;)w[s[L]]=p(s[L]);function z(x,N){for(var Y=N||0;Y0;)Y[$]=arguments[$+2];if(!le(x))return x;var Q=x.attributes||x.props,ae=o.h(x.nodeName||x.type,Q,x.children||Q&&Q.children),me=[ae,N];return Y&&Y.length?me.push(Y):N&&N.children&&me.push(N.children),oe(o.cloneElement.apply(void 0,me))}function le(x){return x&&(x instanceof g||x.$$typeof===u)}function F(x,N){return N._refProxies[x]||(N._refProxies[x]=function(Y){N&&N.refs&&(N.refs[x]=Y,Y===null&&(delete N._refProxies[x],N=null))})}function S(x){var N=x.nodeName,Y=x.attributes;if(!(!Y||typeof N!="string")){var $={};for(var Q in Y)$[Q.toLowerCase()]=Q;if($.ondoubleclick&&(Y.ondblclick=Y[$.ondoubleclick],delete Y[$.ondoubleclick]),$.onchange&&(N==="textarea"||N.toLowerCase()==="input"&&!/^fil|che|rad/i.test(Y.type))){var ae=$.oninput||"oninput";Y[ae]||(Y[ae]=ee([Y[ae],Y[$.onchange]]),delete Y[$.onchange])}}}function X(x){var N=x.attributes;if(N){var Y=N.className||N.class;Y&&(N.className=Y)}}function J(x,N){for(var Y in N)N.hasOwnProperty(Y)&&(x[Y]=N[Y]);return x}function V(x,N){for(var Y in x)if(!(Y in N))return!0;for(var $ in N)if(x[$]!==N[$])return!0;return!1}function E(x){return x&&x.base||x}function O(){}function B(x){function N(Y,$){U(this),fe.call(this,Y,$,_),q.call(this,Y,$)}return x=J({constructor:N},x),x.mixins&&k(x,A(x.mixins)),x.statics&&J(N,x.statics),x.propTypes&&(N.propTypes=x.propTypes),x.defaultProps&&(N.defaultProps=x.defaultProps),x.getDefaultProps&&(N.defaultProps=x.getDefaultProps()),O.prototype=fe.prototype,N.prototype=J(new O,x),N.displayName=x.displayName||"Component",N}function A(x){for(var N={},Y=0;Y1)for(var y=1;y0,d=u?(t+a(n))%t:a(n);return s*t+d}},function(l,i){l.exports=function(e){return typeof e=="object"?e!==null:typeof e=="function"}},function(l,i,e){i.a=o;var t=e(6),a=e(1),r=e(0);function o(n,s){r.a(2,arguments);var u=a.a(n),d=t.a(s);return isNaN(d)?new Date(NaN):(d&&u.setDate(u.getDate()+d),u)}},function(l,i,e){i.a=o;var t=e(1),a=e(6),r=e(0);function o(n,s){r.a(1,arguments);var u=s||{},d=u.locale,m=d&&d.options&&d.options.weekStartsOn,f=m==null?0:a.a(m),_=u.weekStartsOn==null?f:a.a(u.weekStartsOn);if(!(_>=0&&_<=6))throw new RangeError("weekStartsOn must be between 0 and 6 inclusively");var c=t.a(n),v=c.getDay(),g=(v<_?7:0)+v-_;return c.setDate(c.getDate()-g),c.setHours(0,0,0,0),c}},function(l,i,e){var t=e(16),a=e(39);l.exports=e(17)?function(r,o,n){return t.f(r,o,a(1,n))}:function(r,o,n){return r[o]=n,r}},function(l,i){var e={}.hasOwnProperty;l.exports=function(t,a){return e.call(t,a)}},function(l,i,e){i.a=r;var t=e(22),a=e(0);function r(o){return a.a(1,arguments),t.a(o,{weekStartsOn:1})}},function(l,i,e){i.a=n;var t=e(19),a=e(57),r=e(0),o=864e5;function n(s,u){r.a(2,arguments);var d=a.a(s),m=a.a(u),f=d.getTime()-t.a(d),_=m.getTime()-t.a(m);return Math.round((f-_)/o)}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o,n){a.a(2,arguments);var s=t.a(o),u=t.a(n),d=s.getTime()-u.getTime();return d<0?-1:d>0?1:d}},function(l,i,e){var t=e(48),a=e(341),r=e(35),o=e(36),n=e(37),s={code:"en-US",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:0,firstWeekContainsDate:1}};i.a=s},function(l,i,e){var t=e(51);l.exports=function(a,r,o){if(t(a),r===void 0)return a;switch(o){case 1:return function(n){return a.call(r,n)};case 2:return function(n,s){return a.call(r,n,s)};case 3:return function(n,s,u){return a.call(r,n,s,u)}}return function(){return a.apply(r,arguments)}}},function(l,i){l.exports=function(e){try{return!!e()}catch{return!0}}},function(l,i){l.exports={}},function(l,i,e){var t=e(104),a=e(70);l.exports=function(r){return t(a(r))}},function(l,i,e){var t=e(70);l.exports=function(a){return Object(t(a))}},function(l,i,e){i.a=o;var t=e(1),a=e(25),r=e(0);function o(n){r.a(1,arguments);var s=t.a(n),u=s.getFullYear(),d=new Date(0);d.setFullYear(u+1,0,4),d.setHours(0,0,0,0);var m=a.a(d),f=new Date(0);f.setFullYear(u,0,4),f.setHours(0,0,0,0);var _=a.a(f);return s.getTime()>=m.getTime()?u+1:s.getTime()>=_.getTime()?u:u-1}},function(l,i,e){i.a=a;var t={lastWeek:"'last' eeee 'at' p",yesterday:"'yesterday at' p",today:"'today at' p",tomorrow:"'tomorrow at' p",nextWeek:"eeee 'at' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["B","A"],abbreviated:["BC","AD"],wide:["Before Christ","Anno Domini"]},r={narrow:["1","2","3","4"],abbreviated:["Q1","Q2","Q3","Q4"],wide:["1st quarter","2nd quarter","3rd quarter","4th quarter"]},o={narrow:["J","F","M","A","M","J","J","A","S","O","N","D"],abbreviated:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],wide:["January","February","March","April","May","June","July","August","September","October","November","December"]},n={narrow:["S","M","T","W","T","F","S"],short:["Su","Mo","Tu","We","Th","Fr","Sa"],abbreviated:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],wide:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]},s={narrow:{am:"a",pm:"p",midnight:"mi",noon:"n",morning:"morning",afternoon:"afternoon",evening:"evening",night:"night"},abbreviated:{am:"AM",pm:"PM",midnight:"midnight",noon:"noon",morning:"morning",afternoon:"afternoon",evening:"evening",night:"night"},wide:{am:"a.m.",pm:"p.m.",midnight:"midnight",noon:"noon",morning:"morning",afternoon:"afternoon",evening:"evening",night:"night"}},u={narrow:{am:"a",pm:"p",midnight:"mi",noon:"n",morning:"in the morning",afternoon:"in the afternoon",evening:"in the evening",night:"at night"},abbreviated:{am:"AM",pm:"PM",midnight:"midnight",noon:"noon",morning:"in the morning",afternoon:"in the afternoon",evening:"in the evening",night:"at night"},wide:{am:"a.m.",pm:"p.m.",midnight:"midnight",noon:"noon",morning:"in the morning",afternoon:"in the afternoon",evening:"in the evening",night:"at night"}};function d(f,_){var c=Number(f),v=c%100;if(v>20||v<10)switch(v%10){case 1:return c+"st";case 2:return c+"nd";case 3:return c+"rd"}return c+"th"}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(th|st|nd|rd)?/i,o=/\d+/i,n={narrow:/^(b|a)/i,abbreviated:/^(b\.?\s?c\.?|b\.?\s?c\.?\s?e\.?|a\.?\s?d\.?|c\.?\s?e\.?)/i,wide:/^(before christ|before common era|anno domini|common era)/i},s={any:[/^b/i,/^(a|c)/i]},u={narrow:/^[1234]/i,abbreviated:/^q[1234]/i,wide:/^[1234](th|st|nd|rd)? quarter/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[jfmasond]/i,abbreviated:/^(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/i,wide:/^(january|february|march|april|may|june|july|august|september|october|november|december)/i},f={narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^mar/i,/^ap/i,/^may/i,/^jun/i,/^jul/i,/^au/i,/^s/i,/^o/i,/^n/i,/^d/i]},_={narrow:/^[smtwf]/i,short:/^(su|mo|tu|we|th|fr|sa)/i,abbreviated:/^(sun|mon|tue|wed|thu|fri|sat)/i,wide:/^(sunday|monday|tuesday|wednesday|thursday|friday|saturday)/i},c={narrow:[/^s/i,/^m/i,/^t/i,/^w/i,/^t/i,/^f/i,/^s/i],any:[/^su/i,/^m/i,/^tu/i,/^w/i,/^th/i,/^f/i,/^sa/i]},v={narrow:/^(a|p|mi|n|(in the|at) (morning|afternoon|evening|night))/i,any:/^([ap]\.?\s?m\.?|midnight|noon|(in the|at) (morning|afternoon|evening|night))/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/^mi/i,noon:/^no/i,morning:/morning/i,afternoon:/afternoon/i,evening:/evening/i,night:/night/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){i.a=t;function t(a,r){for(var o=a<0?"-":"",n=Math.abs(a).toString();n.length=a.length?{value:void 0,done:!0}:(o=t(a,r),this._i+=o.length,{value:o,done:!1})})},function(l,i){l.exports=!0},function(l,i,e){var t=e(103),a=e(75);l.exports=Object.keys||function(o){return t(o,a)}},function(l,i){var e={}.toString;l.exports=function(t){return e.call(t).slice(8,-1)}},function(l,i,e){i.a=o;var t=e(6),a=e(1),r=e(0);function o(n,s){r.a(2,arguments);var u=a.a(n),d=t.a(s);if(isNaN(d))return new Date(NaN);if(!d)return u;var m=u.getDate(),f=new Date(u.getTime());f.setMonth(u.getMonth()+d+1,0);var _=f.getDate();return m>=_?f:(u.setFullYear(f.getFullYear(),f.getMonth(),m),u)}},function(l,i,e){i.a=o;var t=e(6),a=e(1),r=e(0);function o(n,s){r.a(2,arguments);var u=a.a(n).getTime(),d=t.a(s);return new Date(u+d)}},function(l,i,e){i.a=o;var t=e(34),a=e(25),r=e(0);function o(n){r.a(1,arguments);var s=t.a(n),u=new Date(0);u.setFullYear(s,0,4),u.setHours(0,0,0,0);var d=a.a(u);return d}},function(l,i,e){i.a=r;var t=e(57),a=e(0);function r(o,n){a.a(2,arguments);var s=t.a(o),u=t.a(n);return s.getTime()===u.getTime()}},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"less than a second",other:"less than {{count}} seconds"},xSeconds:{one:"1 second",other:"{{count}} seconds"},halfAMinute:"half a minute",lessThanXMinutes:{one:"less than a minute",other:"less than {{count}} minutes"},xMinutes:{one:"1 minute",other:"{{count}} minutes"},aboutXHours:{one:"about 1 hour",other:"about {{count}} hours"},xHours:{one:"1 hour",other:"{{count}} hours"},xDays:{one:"1 day",other:"{{count}} days"},aboutXWeeks:{one:"about 1 week",other:"about {{count}} weeks"},xWeeks:{one:"1 week",other:"{{count}} weeks"},aboutXMonths:{one:"about 1 month",other:"about {{count}} months"},xMonths:{one:"1 month",other:"{{count}} months"},aboutXYears:{one:"about 1 year",other:"about {{count}} years"},xYears:{one:"1 year",other:"{{count}} years"},overXYears:{one:"over 1 year",other:"over {{count}} years"},almostXYears:{one:"almost 1 year",other:"almost {{count}} years"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"in "+s:s+" ago":s}},function(l,i,e){i.a=o;var t=e(6),a=e(45),r=e(0);function o(n,s){r.a(2,arguments);var u=t.a(s);return a.a(n,-u)}},function(l,i,e){i.a=o;var t=e(6),a=e(1),r=e(0);function o(n,s){r.a(1,arguments);var u=s||{},d=u.locale,m=d&&d.options&&d.options.weekStartsOn,f=m==null?0:t.a(m),_=u.weekStartsOn==null?f:t.a(u.weekStartsOn);if(!(_>=0&&_<=6))throw new RangeError("weekStartsOn must be between 0 and 6 inclusively");var c=a.a(n),v=c.getUTCDay(),g=(v<_?7:0)+v-_;return c.setUTCDate(c.getUTCDate()-g),c.setUTCHours(0,0,0,0),c}},function(l,i){l.exports=function(e){if(typeof e!="function")throw TypeError(e+" is not a function!");return e}},function(l,i){var e=0,t=Math.random();l.exports=function(a){return"Symbol(".concat(a===void 0?"":a,")_",(++e+t).toString(36))}},function(l,i,e){var t=e(16).f,a=e(24),r=e(9)("toStringTag");l.exports=function(o,n,s){o&&!a(o=s?o:o.prototype,r)&&t(o,r,{configurable:!0,value:n})}},function(l,i,e){e(215);for(var t=e(8),a=e(23),r=e(31),o=e(9)("toStringTag"),n="CSSRuleList,CSSStyleDeclaration,CSSValueList,ClientRectList,DOMRectList,DOMStringList,DOMTokenList,DataTransferItemList,FileList,HTMLAllCollection,HTMLCollection,HTMLFormElement,HTMLSelectElement,MediaList,MimeTypeArray,NamedNodeMap,NodeList,PaintRequestList,Plugin,PluginArray,SVGLengthList,SVGNumberList,SVGPathSegList,SVGPointList,SVGStringList,SVGTransformList,SourceBufferList,StyleSheetList,TextTrackCueList,TextTrackList,TouchList".split(","),s=0;s0?Math.floor(s):Math.ceil(s)}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=t.a(o);return n.setDate(1),n.setHours(0,0,0,0),n}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=1,s=t.a(o),u=s.getUTCDay(),d=(u0?t:e)(a)}},function(l,i){l.exports=function(e){if(e==null)throw TypeError("Can't call method on "+e);return e}},function(l,i,e){var t=e(14),a=e(212),r=e(75),o=e(73)("IE_PROTO"),n=function(){},s="prototype",u=function(){var d=e(66)("iframe"),m=r.length,f="<",_=">",c;for(d.style.display="none",e(105).appendChild(d),d.src="javascript:",c=d.contentWindow.document,c.open(),c.write(f+"script"+_+"document.F=Object"+f+"/script"+_),c.close(),u=c.F;m--;)delete u[s][r[m]];return u()};l.exports=Object.create||function(m,f){var _;return m!==null?(n[s]=t(m),_=new n,n[s]=null,_[o]=m):_=u(),f===void 0?_:a(_,f)}},function(l,i,e){var t=e(69),a=Math.min;l.exports=function(r){return r>0?a(t(r),9007199254740991):0}},function(l,i,e){var t=e(74)("keys"),a=e(52);l.exports=function(r){return t[r]||(t[r]=a(r))}},function(l,i,e){var t=e(7),a=e(8),r="__core-js_shared__",o=a[r]||(a[r]={});(l.exports=function(n,s){return o[n]||(o[n]=s!==void 0?s:{})})("versions",[]).push({version:t.version,mode:e(41)?"pure":"global",copyright:"© 2019 Denis Pushkarev (zloirock.ru)"})},function(l,i){l.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(l,i,e){var t=e(43),a=e(9)("toStringTag"),r=t(function(){return arguments}())=="Arguments",o=function(n,s){try{return n[s]}catch{}};l.exports=function(n){var s,u,d;return n===void 0?"Undefined":n===null?"Null":typeof(u=o(s=Object(n),a))=="string"?u:r?t(s):(d=t(s))=="Object"&&typeof s.callee=="function"?"Arguments":d}},function(l,i,e){var t=e(76),a=e(9)("iterator"),r=e(31);l.exports=e(7).getIteratorMethod=function(o){if(o!=null)return o[a]||o["@@iterator"]||r[t(o)]}},function(l,i,e){var t=e(51);function a(r){var o,n;this.promise=new r(function(s,u){if(o!==void 0||n!==void 0)throw TypeError("Bad Promise constructor");o=s,n=u}),this.resolve=t(o),this.reject=t(n)}l.exports.f=function(r){return new a(r)}},function(l,i){i.f=Object.getOwnPropertySymbols},function(l,i,e){i.f=e(9)},function(l,i,e){var t=e(8),a=e(7),r=e(41),o=e(80),n=e(16).f;l.exports=function(s){var u=a.Symbol||(a.Symbol=r?{}:t.Symbol||{});s.charAt(0)!="_"&&!(s in u)&&n(u,s,{value:o.f(s)})}},function(l,i,e){Object.defineProperty(i,"__esModule",{value:!0}),i.omit=t,i.arraysEqual=a,i.isElementAnSFC=function(n){var s=typeof n.type=="string";return s?!1:!n.type.prototype.isReactComponent};function t(o){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:[],s={};return Object.keys(o).forEach(function(u){n.indexOf(u)===-1&&(s[u]=o[u])}),s}function a(o,n){var s=o===n;if(s)return!0;var u=!Array.isArray(o)||!Array.isArray(n),d=o.length!==n.length;return u||d?!1:o.every(function(m,f){return m===n[f]})}function r(o){var n={};return function(s){return n[s]||(n[s]=o(s)),n[s]}}i.hyphenate=r(function(o){return o.replace(/([A-Z])/g,"-$1").toLowerCase()})},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){return a.a(1,arguments),t.a(o).getDay()===0}},function(l,i,e){i.a=n;var t=e(6),a=e(45),r=e(0),o=36e5;function n(s,u){r.a(2,arguments);var d=t.a(u);return a.a(s,d*o)}},function(l,i,e){i.a=o;var t=e(6),a=e(44),r=e(0);function o(n,s){r.a(2,arguments);var u=t.a(s),d=u*3;return a.a(n,d)}},function(l,i,e){i.a=n;var t=e(1),a=e(26),r=e(0);function o(s,u){var d=s.getFullYear()-u.getFullYear()||s.getMonth()-u.getMonth()||s.getDate()-u.getDate()||s.getHours()-u.getHours()||s.getMinutes()-u.getMinutes()||s.getSeconds()-u.getSeconds()||s.getMilliseconds()-u.getMilliseconds();return d<0?-1:d>0?1:d}function n(s,u){r.a(2,arguments);var d=t.a(s),m=t.a(u),f=o(d,m),_=Math.abs(a.a(d,m));d.setDate(d.getDate()-f*_);var c=o(d,m)===-f,v=f*(_-c);return v===0?0:v}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=t.a(o),s=n.getMonth(),u=s-s%3;return n.setMonth(u,1),n.setHours(0,0,0,0),n}},function(l,i,e){i.a=n;var t=e(148),a=e(83),r=e(56),o=e(0);function n(s){o.a(1,arguments);for(var u=t.a(s),d=[],m=0;m=1&&g<=7))throw new RangeError("firstWeekContainsDate must be between 1 and 7 inclusively");var P=new Date(0);P.setUTCFullYear(m+1,0,g),P.setUTCHours(0,0,0,0);var h=r.a(P,u),y=new Date(0);y.setUTCFullYear(m,0,g),y.setUTCHours(0,0,0,0);var W=r.a(y,u);return d.getTime()>=h.getTime()?m+1:d.getTime()>=W.getTime()?m:m-1}},function(l,i,e){i.a=n;var t=e(170),a=e(22),r=e(6),o=e(0);function n(s,u){o.a(1,arguments);var d=u||{},m=d.locale,f=m&&m.options&&m.options.firstWeekContainsDate,_=f==null?1:r.a(f),c=d.firstWeekContainsDate==null?_:r.a(d.firstWeekContainsDate),v=t.a(s,u),g=new Date(0);g.setFullYear(v,0,c),g.setHours(0,0,0,0);var P=a.a(g,u);return P}},function(l,i,e){i.a=o;var t=e(6),a=e(21),r=e(0);function o(n,s){r.a(2,arguments);var u=t.a(s);return a.a(n,-u)}},function(l,i,e){i.a=r;var t=e(22),a=e(0);function r(o,n,s){a.a(2,arguments);var u=t.a(o,s),d=t.a(n,s);return u.getTime()===d.getTime()}},function(l,i,e){i.a=n;var t=e(6),a=e(1),r=e(164),o=e(0);function n(s,u){o.a(2,arguments);var d=a.a(s),m=t.a(u),f=d.getFullYear(),_=d.getDate(),c=new Date(0);c.setFullYear(f,m,15),c.setHours(0,0,0,0);var v=r.a(c);return d.setMonth(m,Math.min(_,v)),d}},function(l,i,e){var t=e(3),a={locale:{1:"१",2:"२",3:"३",4:"४",5:"५",6:"६",7:"७",8:"८",9:"९",0:"०"},number:{"१":"1","२":"2","३":"3","४":"4","५":"5","६":"6","७":"7","८":"8","९":"9","०":"0"}},r={narrow:["ईसा-पूर्व","ईस्वी"],abbreviated:["ईसा-पूर्व","ईस्वी"],wide:["ईसा-पूर्व","ईसवी सन"]},o={narrow:["1","2","3","4"],abbreviated:["ति1","ति2","ति3","ति4"],wide:["पहली तिमाही","दूसरी तिमाही","तीसरी तिमाही","चौथी तिमाही"]},n={narrow:["ज","फ़","मा","अ","मई","जू","जु","अग","सि","अक्तू","न","दि"],abbreviated:["जन","फ़र","मार्च","अप्रैल","मई","जून","जुल","अग","सित","अक्तू","नव","दिस"],wide:["जनवरी","फ़रवरी","मार्च","अप्रैल","मई","जून","जुलाई","अगस्त","सितंबर","अक्तूबर","नवंबर","दिसंबर"]},s={narrow:["र","सो","मं","बु","गु","शु","श"],short:["र","सो","मं","बु","गु","शु","श"],abbreviated:["रवि","सोम","मंगल","बुध","गुरु","शुक्र","शनि"],wide:["रविवार","सोमवार","मंगलवार","बुधवार","गुरुवार","शुक्रवार","शनिवार"]},u={narrow:{am:"पूर्वाह्न",pm:"अपराह्न",midnight:"मध्यरात्रि",noon:"दोपहर",morning:"सुबह",afternoon:"दोपहर",evening:"शाम",night:"रात"},abbreviated:{am:"पूर्वाह्न",pm:"अपराह्न",midnight:"मध्यरात्रि",noon:"दोपहर",morning:"सुबह",afternoon:"दोपहर",evening:"शाम",night:"रात"},wide:{am:"पूर्वाह्न",pm:"अपराह्न",midnight:"मध्यरात्रि",noon:"दोपहर",morning:"सुबह",afternoon:"दोपहर",evening:"शाम",night:"रात"}},d={narrow:{am:"पूर्वाह्न",pm:"अपराह्न",midnight:"मध्यरात्रि",noon:"दोपहर",morning:"सुबह",afternoon:"दोपहर",evening:"शाम",night:"रात"},abbreviated:{am:"पूर्वाह्न",pm:"अपराह्न",midnight:"मध्यरात्रि",noon:"दोपहर",morning:"सुबह",afternoon:"दोपहर",evening:"शाम",night:"रात"},wide:{am:"पूर्वाह्न",pm:"अपराह्न",midnight:"मध्यरात्रि",noon:"दोपहर",morning:"सुबह",afternoon:"दोपहर",evening:"शाम",night:"रात"}};function m(v){var g=c.localeToNumber(v),P=c.numberToLocale(g),h=g%10;switch(h){case 2:case 3:case 4:case 6:case 1:case 5:case 7:case 8:case 9:case 0:return P}}function f(v){var g=v.toString().replace(/[१२३४५६७८९०]/g,function(P){return a.number[P]});return Number(g)}function _(v){return v.toString().replace(/\d/g,function(g){return a.locale[g]})}var c={localeToNumber:f,numberToLocale:_,ordinalNumber:m,era:t.a({values:r,defaultWidth:"wide"}),quarter:t.a({values:o,defaultWidth:"wide",argumentCallback:function(v){return Number(v)-1}}),month:t.a({values:n,defaultWidth:"wide"}),day:t.a({values:s,defaultWidth:"wide"}),dayPeriod:t.a({values:u,defaultWidth:"wide",formattingValues:d,defaultFormattingWidth:"wide"})};i.a=c},function(l,i,e){l.exports=!e(17)&&!e(30)(function(){return Object.defineProperty(e(66)("div"),"a",{get:function(){return 7}}).a!=7})},function(l,i,e){(function(t){l.exports=e(196)}).call(i,e(18))},function(l,i){},function(l,i,e){var t=e(41),a=e(11),r=e(102),o=e(23),n=e(31),s=e(211),u=e(53),d=e(106),m=e(9)("iterator"),f=!([].keys&&"next"in[].keys()),_="@@iterator",c="keys",v="values",g=function(){return this};l.exports=function(P,h,y,W,T,j,b){s(y,h,W);var C=function(oe){if(!f&&oe in p)return p[oe];switch(oe){case c:return function(){return new y(this,oe)};case v:return function(){return new y(this,oe)}}return function(){return new y(this,oe)}},I=h+" Iterator",M=T==v,D=!1,p=P.prototype,w=p[m]||p[_]||T&&p[T],L=w||C(T),z=T?M?C("entries"):L:void 0,ne=h=="Array"&&p.entries||w,te,de,ue;if(ne&&(ue=d(ne.call(new P)),ue!==Object.prototype&&ue.next&&(u(ue,I,!0),!t&&typeof ue[m]!="function"&&o(ue,m,g))),M&&w&&w.name!==v&&(D=!0,L=function(){return w.call(this)}),(!t||b)&&(f||D||!p[m])&&o(p,m,L),n[h]=L,n[I]=g,T)if(te={values:M?L:C(v),keys:j?L:C(c),entries:z},b)for(de in te)de in p||r(p,de,te[de]);else a(a.P+a.F*(f||D),h,te);return te}},function(l,i,e){l.exports=e(23)},function(l,i,e){var t=e(24),a=e(32),r=e(213)(!1),o=e(73)("IE_PROTO");l.exports=function(n,s){var u=a(n),d=0,m=[],f;for(f in u)f!=o&&t(u,f)&&m.push(f);for(;s.length>d;)t(u,f=s[d++])&&(~r(m,f)||m.push(f));return m}},function(l,i,e){var t=e(43);l.exports=Object("z").propertyIsEnumerable(0)?Object:function(a){return t(a)=="String"?a.split(""):Object(a)}},function(l,i,e){var t=e(8).document;l.exports=t&&t.documentElement},function(l,i,e){var t=e(24),a=e(33),r=e(73)("IE_PROTO"),o=Object.prototype;l.exports=Object.getPrototypeOf||function(n){return n=a(n),t(n,r)?n[r]:typeof n.constructor=="function"&&n instanceof n.constructor?n.constructor.prototype:n instanceof Object?o:null}},function(l,i,e){var t=e(14);l.exports=function(a,r,o,n){try{return n?r(t(o)[0],o[1]):r(o)}catch(u){var s=a.return;throw s!==void 0&&t(s.call(a)),u}}},function(l,i,e){var t=e(31),a=e(9)("iterator"),r=Array.prototype;l.exports=function(o){return o!==void 0&&(t.Array===o||r[a]===o)}},function(l,i,e){var t=e(14),a=e(51),r=e(9)("species");l.exports=function(o,n){var s=t(o).constructor,u;return s===void 0||(u=t(s)[r])==null?n:a(u)}},function(l,i,e){var t=e(29),a=e(221),r=e(105),o=e(66),n=e(8),s=n.process,u=n.setImmediate,d=n.clearImmediate,m=n.MessageChannel,f=n.Dispatch,_=0,c={},v="onreadystatechange",g,P,h,y=function(){var T=+this;if(c.hasOwnProperty(T)){var j=c[T];delete c[T],j()}},W=function(T){y.call(T.data)};(!u||!d)&&(u=function(j){for(var b=[],C=1;arguments.length>C;)b.push(arguments[C++]);return c[++_]=function(){a(typeof j=="function"?j:Function(j),b)},g(_),_},d=function(j){delete c[j]},e(43)(s)=="process"?g=function(T){s.nextTick(t(y,T,1))}:f&&f.now?g=function(T){f.now(t(y,T,1))}:m?(P=new m,h=P.port2,P.port1.onmessage=W,g=t(h.postMessage,h,1)):n.addEventListener&&typeof postMessage=="function"&&!n.importScripts?(g=function(T){n.postMessage(T+"","*")},n.addEventListener("message",W,!1)):v in o("script")?g=function(T){r.appendChild(o("script"))[v]=function(){r.removeChild(this),y.call(T)}}:g=function(T){setTimeout(t(y,T,1),0)}),l.exports={set:u,clear:d}},function(l,i){l.exports=function(e){try{return{e:!1,v:e()}}catch(t){return{e:!0,v:t}}}},function(l,i,e){var t=e(14),a=e(20),r=e(78);l.exports=function(o,n){if(t(o),a(n)&&n.constructor===o)return n;var s=r.f(o),u=s.resolve;return u(n),s.promise}},function(l,i,e){var t=e(9)("iterator"),a=!1;try{var r=[7][t]();r.return=function(){a=!0},Array.from(r,function(){throw 2})}catch{}l.exports=function(o,n){if(!n&&!a)return!1;var s=!1;try{var u=[7],d=u[t]();d.next=function(){return{done:s=!0}},u[t]=function(){return d},o(u)}catch{}return s}},function(l,i,e){l.exports={default:e(232),__esModule:!0}},function(l,i,e){var t=e(11),a=e(7),r=e(30);l.exports=function(o,n){var s=(a.Object||{})[o]||Object[o],u={};u[o]=n(s),t(t.S+t.F*r(function(){s(1)}),"Object",u)}},function(l,i,e){i.__esModule=!0;var t=e(117),a=r(t);function r(o){return o&&o.__esModule?o:{default:o}}i.default=function(o,n){if(!o)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return n&&((typeof n>"u"?"undefined":(0,a.default)(n))==="object"||typeof n=="function")?n:o}},function(l,i,e){i.__esModule=!0;var t=e(234),a=s(t),r=e(236),o=s(r),n=typeof o.default=="function"&&typeof a.default=="symbol"?function(u){return typeof u}:function(u){return u&&typeof o.default=="function"&&u.constructor===o.default&&u!==o.default.prototype?"symbol":typeof u};function s(u){return u&&u.__esModule?u:{default:u}}i.default=typeof o.default=="function"&&n(a.default)==="symbol"?function(u){return typeof u>"u"?"undefined":n(u)}:function(u){return u&&typeof o.default=="function"&&u.constructor===o.default&&u!==o.default.prototype?"symbol":typeof u>"u"?"undefined":n(u)}},function(l,i,e){var t=e(103),a=e(75).concat("length","prototype");i.f=Object.getOwnPropertyNames||function(o){return t(o,a)}},function(l,i,e){var t=e(55),a=e(39),r=e(32),o=e(67),n=e(24),s=e(98),u=Object.getOwnPropertyDescriptor;i.f=e(17)?u:function(m,f){if(m=r(m),f=o(f,!0),s)try{return u(m,f)}catch{}if(n(m,f))return a(!t.f.call(m,f),m[f])}},function(l,i,e){i.__esModule=!0;var t=e(245),a=u(t),r=e(249),o=u(r),n=e(117),s=u(n);function u(d){return d&&d.__esModule?d:{default:d}}i.default=function(d,m){if(typeof m!="function"&&m!==null)throw new TypeError("Super expression must either be null or a function, not "+(typeof m>"u"?"undefined":(0,s.default)(m)));d.prototype=(0,o.default)(m&&m.prototype,{constructor:{value:d,enumerable:!1,writable:!0,configurable:!0}}),m&&(a.default?(0,a.default)(d,m):d.__proto__=m)}},function(l,i,e){Object.defineProperty(i,"__esModule",{value:!0}),i.hasClassInParent=i.formatErrorMsg=i.getMetaContent=i.axiosGithub=i.axiosJSON=i.queryStringify=i.queryParse=void 0;var t=e(274),a=u(t),r=e(277),o=u(r),n=e(284),s=u(n);function u(d){return d&&d.__esModule?d:{default:d}}i.queryParse=function(){var m=arguments.length>0&&arguments[0]!==void 0?arguments[0]:window.location.search;if(!m)return{};var f=m[0]==="?"?m.substring(1):m,_={};return f.split("&").forEach(function(c){var v=c.split("="),g=(0,o.default)(v,2),P=g[0],h=g[1];P&&(_[decodeURIComponent(P)]=decodeURIComponent(h))}),_},i.queryStringify=function(m){var f=(0,a.default)(m).map(function(_){return _+"="+encodeURIComponent(m[_]||"")}).join("&");return f},i.axiosJSON=s.default.create({headers:{Accept:"application/json"}}),i.axiosGithub=s.default.create({baseURL:"https://api.github.com",headers:{Accept:"application/json"}}),i.getMetaContent=function(m,f){f||(f="content");var _=window.document.querySelector("meta[name='"+m+"']");return _&&_.getAttribute(f)},i.formatErrorMsg=function(m){var f="Error: ";return m.response&&m.response.data&&m.response.data.message?(f+=m.response.data.message+". ",m.response.data.errors&&(f+=m.response.data.errors.map(function(_){return _.message}).join(", "))):f+=m.message,f},i.hasClassInParent=function d(m){for(var f=arguments.length,_=Array(f>1?f-1:0),c=1;c"u")return!1;var g=m.className.split(" ");return _.forEach(function(P,h){v=v||g.indexOf(P)>=0}),v||m.parentNode&&d(m.parentNode,_)}},function(l,i,e){l.exports=function(a,r){return function(){for(var n=new Array(arguments.length),s=0;s"u"||(t.isArray(_)?c=c+"[]":_=[_],t.forEach(_,function(g){t.isDate(g)?g=g.toISOString():t.isObject(g)&&(g=JSON.stringify(g)),d.push(a(c)+"="+a(g))}))}),u=d.join("&")}if(u){var m=o.indexOf("#");m!==-1&&(o=o.slice(0,m)),o+=(o.indexOf("?")===-1?"?":"&")+u}return o}},function(l,i,e){l.exports=function(a){return!!(a&&a.__CANCEL__)}},function(l,i,e){(function(t){var a=e(10),r=e(290),o={"Content-Type":"application/x-www-form-urlencoded"};function n(d,m){!a.isUndefined(d)&&a.isUndefined(d["Content-Type"])&&(d["Content-Type"]=m)}function s(){var d;return(typeof XMLHttpRequest<"u"||typeof t<"u"&&Object.prototype.toString.call(t)==="[object process]")&&(d=e(126)),d}var u={adapter:s(),transformRequest:[function(m,f){return r(f,"Accept"),r(f,"Content-Type"),a.isFormData(m)||a.isArrayBuffer(m)||a.isBuffer(m)||a.isStream(m)||a.isFile(m)||a.isBlob(m)?m:a.isArrayBufferView(m)?m.buffer:a.isURLSearchParams(m)?(n(f,"application/x-www-form-urlencoded;charset=utf-8"),m.toString()):a.isObject(m)?(n(f,"application/json;charset=utf-8"),JSON.stringify(m)):m}],transformResponse:[function(m){if(typeof m=="string")try{m=JSON.parse(m)}catch{}return m}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,validateStatus:function(m){return m>=200&&m<300}};u.headers={common:{Accept:"application/json, text/plain, */*"}},a.forEach(["delete","get","head"],function(m){u.headers[m]={}}),a.forEach(["post","put","patch"],function(m){u.headers[m]=a.merge(o)}),l.exports=u}).call(i,e(18))},function(l,i,e){var t=e(10),a=e(291),r=e(123),o=e(293),n=e(296),s=e(297),u=e(127);l.exports=function(m){return new Promise(function(_,c){var v=m.data,g=m.headers;t.isFormData(v)&&delete g["Content-Type"];var P=new XMLHttpRequest;if(m.auth){var h=m.auth.username||"",y=m.auth.password||"";g.Authorization="Basic "+btoa(h+":"+y)}var W=o(m.baseURL,m.url);if(P.open(m.method.toUpperCase(),r(W,m.params,m.paramsSerializer),!0),P.timeout=m.timeout,P.onreadystatechange=function(){if(!(!P||P.readyState!==4)&&!(P.status===0&&!(P.responseURL&&P.responseURL.indexOf("file:")===0))){var C="getAllResponseHeaders"in P?n(P.getAllResponseHeaders()):null,I=!m.responseType||m.responseType==="text"?P.responseText:P.response,M={data:I,status:P.status,statusText:P.statusText,headers:C,config:m,request:P};a(_,c,M),P=null}},P.onabort=function(){P&&(c(u("Request aborted",m,"ECONNABORTED",P)),P=null)},P.onerror=function(){c(u("Network Error",m,null,P)),P=null},P.ontimeout=function(){var C="timeout of "+m.timeout+"ms exceeded";m.timeoutErrorMessage&&(C=m.timeoutErrorMessage),c(u(C,m,"ECONNABORTED",P)),P=null},t.isStandardBrowserEnv()){var T=e(298),j=(m.withCredentials||s(W))&&m.xsrfCookieName?T.read(m.xsrfCookieName):void 0;j&&(g[m.xsrfHeaderName]=j)}if("setRequestHeader"in P&&t.forEach(g,function(C,I){typeof v>"u"&&I.toLowerCase()==="content-type"?delete g[I]:P.setRequestHeader(I,C)}),t.isUndefined(m.withCredentials)||(P.withCredentials=!!m.withCredentials),m.responseType)try{P.responseType=m.responseType}catch(b){if(m.responseType!=="json")throw b}typeof m.onDownloadProgress=="function"&&P.addEventListener("progress",m.onDownloadProgress),typeof m.onUploadProgress=="function"&&P.upload&&P.upload.addEventListener("progress",m.onUploadProgress),m.cancelToken&&m.cancelToken.promise.then(function(C){P&&(P.abort(),c(C),P=null)}),v===void 0&&(v=null),P.send(v)})}},function(l,i,e){var t=e(292);l.exports=function(r,o,n,s,u){var d=new Error(r);return t(d,o,n,s,u)}},function(l,i,e){var t=e(10);l.exports=function(r,o){o=o||{};var n={},s=["url","method","params","data"],u=["headers","auth","proxy"],d=["baseURL","url","transformRequest","transformResponse","paramsSerializer","timeout","withCredentials","adapter","responseType","xsrfCookieName","xsrfHeaderName","onUploadProgress","onDownloadProgress","maxContentLength","validateStatus","maxRedirects","httpAgent","httpsAgent","cancelToken","socketPath"];t.forEach(s,function(c){typeof o[c]<"u"&&(n[c]=o[c])}),t.forEach(u,function(c){t.isObject(o[c])?n[c]=t.deepMerge(r[c],o[c]):typeof o[c]<"u"?n[c]=o[c]:t.isObject(r[c])?n[c]=t.deepMerge(r[c]):typeof r[c]<"u"&&(n[c]=r[c])}),t.forEach(d,function(c){typeof o[c]<"u"?n[c]=o[c]:typeof r[c]<"u"&&(n[c]=r[c])});var m=s.concat(u).concat(d),f=Object.keys(o).filter(function(c){return m.indexOf(c)===-1});return t.forEach(f,function(c){typeof o[c]<"u"?n[c]=o[c]:typeof r[c]<"u"&&(n[c]=r[c])}),n}},function(l,i,e){function t(a){this.message=a}t.prototype.toString=function(){return"Cancel"+(this.message?": "+this.message:"")},t.prototype.__CANCEL__=!0,l.exports=t},function(l,i,e){Object.defineProperty(i,"__esModule",{value:!0});var t=e(12),a=r(t);function r(o){return o&&o.__esModule?o:{default:o}}i.default=function(o){var n=o.src,s=o.className,u=o.alt,d=o.defaultSrc,m=d===void 0?"//cdn.jsdelivr.net/npm/gitalk@1/src/assets/icon/github.svg":d;return a.default.createElement("div",{className:"gt-avatar "+s},a.default.createElement("img",{src:n||m,alt:"@"+u,onError:function(_){_.target.src=m}}))}},function(l,i,e){Object.defineProperty(i,"__esModule",{value:!0});var t=e(12),a=r(t);function r(o){return o&&o.__esModule?o:{default:o}}i.default=function(o){var n=o.className,s=o.text,u=o.name;return a.default.createElement("span",{className:"gt-ico "+n},a.default.createElement("span",{className:"gt-svg",dangerouslySetInnerHTML:{__html:e(304)("./"+u+".svg")}}),s&&a.default.createElement("span",{className:"gt-ico-text"},s))}},function(l,i,e){i.a=u;var t=e(56),a=e(1),r=e(6),o=e(0),n=e(83),s=e(133);function u(d,m){o.a(2,arguments);var f=a.a(d),_=t.a(f),c=r.a(m);if(isNaN(c))return new Date(NaN);var v=f.getHours(),g=c<0?-1:1,P=r.a(c/5);f.setDate(f.getDate()+P*7);for(var h=Math.abs(c%5);h>0;)f.setDate(f.getDate()+g),t.a(f)||(h-=1);return _&&t.a(f)&&c!==0&&(s.a(f)&&f.setDate(f.getDate()+(g<0?2:-1)),n.a(f)&&f.setDate(f.getDate()+(g<0?1:-2))),f.setHours(v),f}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){return a.a(1,arguments),t.a(o).getDay()===6}},function(l,i,e){i.a=n;var t=e(6),a=e(34),r=e(135),o=e(0);function n(s,u){o.a(2,arguments);var d=t.a(u);return r.a(s,a.a(s)+d)}},function(l,i,e){i.a=s;var t=e(6),a=e(1),r=e(46),o=e(26),n=e(0);function s(u,d){n.a(2,arguments);var m=a.a(u),f=t.a(d),_=o.a(m,r.a(m)),c=new Date(0);return c.setFullYear(f,0,4),c.setHours(0,0,0,0),m=r.a(c),m.setDate(m.getDate()+_),m}},function(l,i,e){i.a=n;var t=e(6),a=e(45),r=e(0),o=6e4;function n(s,u){r.a(2,arguments);var d=t.a(u);return a.a(s,d*o)}},function(l,i,e){i.a=o;var t=e(6),a=e(45),r=e(0);function o(n,s){r.a(2,arguments);var u=t.a(s);return a.a(n,u*1e3)}},function(l,i,e){i.a=o;var t=e(6),a=e(44),r=e(0);function o(n,s){r.a(2,arguments);var u=t.a(s);return a.a(n,u*12)}},function(l,i,e){i.a=r;var t=e(34),a=e(0);function r(o,n){return a.a(2,arguments),t.a(o)-t.a(n)}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o,n){a.a(2,arguments);var s=t.a(o),u=t.a(n),d=s.getFullYear()-u.getFullYear(),m=s.getMonth()-u.getMonth();return d*12+m}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=t.a(o),s=Math.floor(n.getMonth()/3)+1;return s}},function(l,i,e){i.a=n;var t=e(22),a=e(19),r=e(0),o=6048e5;function n(s,u,d){r.a(2,arguments);var m=t.a(s,d),f=t.a(u,d),_=m.getTime()-a.a(m),c=f.getTime()-a.a(f);return Math.round((_-c)/o)}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o,n){a.a(2,arguments);var s=t.a(o),u=t.a(n);return s.getFullYear()-u.getFullYear()}},function(l,i,e){i.a=o;var t=e(59),a=e(0),r=36e5;function o(n,s){a.a(2,arguments);var u=t.a(n,s)/r;return u>0?Math.floor(u):Math.ceil(u)}},function(l,i,e){i.a=o;var t=e(6),a=e(134),r=e(0);function o(n,s){r.a(2,arguments);var u=t.a(s);return a.a(n,-u)}},function(l,i,e){i.a=o;var t=e(59),a=e(0),r=6e4;function o(n,s){a.a(2,arguments);var u=t.a(n,s)/r;return u>0?Math.floor(u):Math.ceil(u)}},function(l,i,e){i.a=n;var t=e(1),a=e(143),r=e(27),o=e(0);function n(s,u){o.a(2,arguments);var d=t.a(s),m=t.a(u),f=r.a(d,m),_=Math.abs(a.a(d,m));d.setFullYear("1584"),m.setFullYear("1584");var c=r.a(d,m)===-f,v=f*(_-c);return v===0?0:v}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o,n){a.a(1,arguments);var s=o||{},u=t.a(s.start),d=t.a(s.end),m=d.getTime();if(!(u.getTime()<=m))throw new RangeError("Invalid interval");var f=[],_=u;_.setHours(0,0,0,0);var c=n&&"step"in n?Number(n.step):1;if(c<1||isNaN(c))throw new RangeError("`options.step` must be a number greater than 1");for(;_.getTime()<=m;)f.push(t.a(_)),_.setDate(_.getDate()+c),_.setHours(0,0,0,0);return f}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=t.a(o),s=n.getFullYear();return n.setFullYear(s+1,0,0),n.setHours(23,59,59,999),n}},function(l,i,e){i.a=o;var t=e(1),a=e(6),r=e(0);function o(n,s){r.a(1,arguments);var u=s||{},d=u.locale,m=d&&d.options&&d.options.weekStartsOn,f=m==null?0:a.a(m),_=u.weekStartsOn==null?f:a.a(u.weekStartsOn);if(!(_>=0&&_<=6))throw new RangeError("weekStartsOn must be between 0 and 6 inclusively");var c=t.a(n),v=c.getDay(),g=(v<_?-7:0)+6-(v-_);return c.setDate(c.getDate()+g),c.setHours(23,59,59,999),c}},function(l,i,e){i.a=h;var t=e(15),a=e(28),r=e(49),o=e(1),n=e(342),s=e(156),u=e(19),d=e(157),m=e(6),f=e(0),_=/[yYQqMLwIdDecihHKkms]o|(\w)\1*|''|'(''|[^'])+('|$)|./g,c=/P+p+|P+|p+|''|'(''|[^'])+('|$)|./g,v=/^'([^]*?)'?$/,g=/''/g,P=/[a-zA-Z]/;function h(W,T,j){f.a(2,arguments);var b=String(T),C=j||{},I=C.locale||a.a,M=I.options&&I.options.firstWeekContainsDate,D=M==null?1:m.a(M),p=C.firstWeekContainsDate==null?D:m.a(C.firstWeekContainsDate);if(!(p>=1&&p<=7))throw new RangeError("firstWeekContainsDate must be between 1 and 7 inclusively");var w=I.options&&I.options.weekStartsOn,L=w==null?0:m.a(w),z=C.weekStartsOn==null?L:m.a(C.weekStartsOn);if(!(z>=0&&z<=6))throw new RangeError("weekStartsOn must be between 0 and 6 inclusively");if(!I.localize)throw new RangeError("locale must contain localize property");if(!I.formatLong)throw new RangeError("locale must contain formatLong property");var ne=o.a(W);if(!t.a(ne))throw new RangeError("Invalid time value");var te=u.a(ne),de=r.a(ne,te),ue={firstWeekContainsDate:p,weekStartsOn:z,locale:I,_originalDate:ne},oe=b.match(c).map(function(ie){var le=ie[0];if(le==="p"||le==="P"){var F=s.a[le];return F(ie,I.formatLong,ue)}return ie}).join("").match(_).map(function(ie){if(ie==="''")return"'";var le=ie[0];if(le==="'")return y(ie);var F=n.a[le];if(F)return!C.useAdditionalWeekYearTokens&&d.b(ie)&&d.c(ie,T,W),!C.useAdditionalDayOfYearTokens&&d.a(ie)&&d.c(ie,T,W),F(de,ie,I.localize,ue);if(le.match(P))throw new RangeError("Format string contains an unescaped latin alphabet character `"+le+"`");return ie}).join("");return oe}function y(W){return W.match(v)[1].replace(g,"'")}},function(l,i,e){var t=e(38),a={y:function(r,o){var n=r.getUTCFullYear(),s=n>0?n:1-n;return t.a(o==="yy"?s%100:s,o.length)},M:function(r,o){var n=r.getUTCMonth();return o==="M"?String(n+1):t.a(n+1,2)},d:function(r,o){return t.a(r.getUTCDate(),o.length)},a:function(r,o){var n=r.getUTCHours()/12>=1?"pm":"am";switch(o){case"a":case"aa":case"aaa":return n.toUpperCase();case"aaaaa":return n[0];case"aaaa":default:return n==="am"?"a.m.":"p.m."}},h:function(r,o){return t.a(r.getUTCHours()%12||12,o.length)},H:function(r,o){return t.a(r.getUTCHours(),o.length)},m:function(r,o){return t.a(r.getUTCMinutes(),o.length)},s:function(r,o){return t.a(r.getUTCSeconds(),o.length)},S:function(r,o){var n=o.length,s=r.getUTCMilliseconds(),u=Math.floor(s*Math.pow(10,n-3));return t.a(u,o.length)}};i.a=a},function(l,i,e){i.a=s;var t=e(1),a=e(63),r=e(344),o=e(0),n=6048e5;function s(u){o.a(1,arguments);var d=t.a(u),m=a.a(d).getTime()-r.a(d).getTime();return Math.round(m/n)+1}},function(l,i,e){i.a=o;var t=e(1),a=e(63),r=e(0);function o(n){r.a(1,arguments);var s=t.a(n),u=s.getUTCFullYear(),d=new Date(0);d.setUTCFullYear(u+1,0,4),d.setUTCHours(0,0,0,0);var m=a.a(d),f=new Date(0);f.setUTCFullYear(u,0,4),f.setUTCHours(0,0,0,0);var _=a.a(f);return s.getTime()>=m.getTime()?u+1:s.getTime()>=_.getTime()?u:u-1}},function(l,i,e){i.a=s;var t=e(1),a=e(50),r=e(345),o=e(0),n=6048e5;function s(u,d){o.a(1,arguments);var m=t.a(u),f=a.a(m,d).getTime()-r.a(m,d).getTime();return Math.round(f/n)+1}},function(l,i,e){function t(n,s){switch(n){case"P":return s.date({width:"short"});case"PP":return s.date({width:"medium"});case"PPP":return s.date({width:"long"});case"PPPP":default:return s.date({width:"full"})}}function a(n,s){switch(n){case"p":return s.time({width:"short"});case"pp":return s.time({width:"medium"});case"ppp":return s.time({width:"long"});case"pppp":default:return s.time({width:"full"})}}function r(n,s){var u=n.match(/(P+)(p+)?/),d=u[1],m=u[2];if(!m)return t(n,s);var f;switch(d){case"P":f=s.dateTime({width:"short"});break;case"PP":f=s.dateTime({width:"medium"});break;case"PPP":f=s.dateTime({width:"long"});break;case"PPPP":default:f=s.dateTime({width:"full"});break}return f.replace("{{date}}",t(d,s)).replace("{{time}}",a(m,s))}var o={p:a,P:r};i.a=o},function(l,i,e){i.a=r,i.b=o,i.c=n;var t=["D","DD"],a=["YY","YYYY"];function r(s){return t.indexOf(s)!==-1}function o(s){return a.indexOf(s)!==-1}function n(s,u,d){if(s==="YYYY")throw new RangeError("Use `yyyy` instead of `YYYY` (in `".concat(u,"`) for formatting years to the input `").concat(d,"`; see: https://git.io/fxCyr"));if(s==="YY")throw new RangeError("Use `yy` instead of `YY` (in `".concat(u,"`) for formatting years to the input `").concat(d,"`; see: https://git.io/fxCyr"));if(s==="D")throw new RangeError("Use `d` instead of `D` (in `".concat(u,"`) for formatting days of the month to the input `").concat(d,"`; see: https://git.io/fxCyr"));if(s==="DD")throw new RangeError("Use `dd` instead of `DD` (in `".concat(u,"`) for formatting days of the month to the input `").concat(d,"`; see: https://git.io/fxCyr"))}},function(l,i,e){i.a=v;var t=e(27),a=e(60),r=e(61),o=e(28),n=e(1),s=e(159),u=e(19),d=e(0),m=1440,f=2520,_=43200,c=86400;function v(g,P,h){d.a(2,arguments);var y=h||{},W=y.locale||o.a;if(!W.formatDistance)throw new RangeError("locale must contain formatDistance property");var T=t.a(g,P);if(isNaN(T))throw new RangeError("Invalid time value");var j=s.a(y);j.addSuffix=!!y.addSuffix,j.comparison=T;var b,C;T>0?(b=n.a(P),C=n.a(g)):(b=n.a(g),C=n.a(P));var I=r.a(C,b),M=(u.a(C)-u.a(b))/1e3,D=Math.round((I-M)/60),p;if(D<2)return y.includeSeconds?I<5?W.formatDistance("lessThanXSeconds",5,j):I<10?W.formatDistance("lessThanXSeconds",10,j):I<20?W.formatDistance("lessThanXSeconds",20,j):I<40?W.formatDistance("halfAMinute",null,j):I<60?W.formatDistance("lessThanXMinutes",1,j):W.formatDistance("xMinutes",1,j):D===0?W.formatDistance("lessThanXMinutes",1,j):W.formatDistance("xMinutes",D,j);if(D<45)return W.formatDistance("xMinutes",D,j);if(D<90)return W.formatDistance("aboutXHours",1,j);if(D0?(T=r.a(v),j=r.a(c)):(T=r.a(c),j=r.a(v));var b=P.roundingMethod==null?"round":String(P.roundingMethod),C;if(b==="floor")C=Math.floor;else if(b==="ceil")C=Math.ceil;else if(b==="round")C=Math.round;else throw new RangeError("roundingMethod must be 'floor', 'ceil' or 'round'");var I=o.a(j,T),M=(t.a(j)-t.a(T))/1e3,D=C((I-M)/60),p;if(P.unit==null?D<1?p="second":D<60?p="minute":D=1&&g<=7))throw new RangeError("firstWeekContainsDate must be between 1 and 7 inclusively");var P=new Date(0);P.setFullYear(m+1,0,g),P.setHours(0,0,0,0);var h=t.a(P,u),y=new Date(0);y.setFullYear(m,0,g),y.setHours(0,0,0,0);var W=t.a(y,u);return d.getTime()>=h.getTime()?m+1:d.getTime()>=W.getTime()?m:m-1}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=t.a(o),s=n.getMonth();return n.setFullYear(n.getFullYear(),s+1,0),n.setHours(0,0,0,0),n}},function(l,i,e){i.a=s;var t=e(94),a=e(173),r=e(1),o=e(0),n=e(6);function s(u,d){if(o.a(2,arguments),!d||typeof d!="object")return new Date(NaN);var m="years"in d?n.a(d.years):0,f="months"in d?n.a(d.months):0,_="weeks"in d?n.a(d.weeks):0,c="days"in d?n.a(d.days):0,v="hours"in d?n.a(d.hours):0,g="minutes"in d?n.a(d.minutes):0,P="seconds"in d?n.a(d.seconds):0,h=a.a(r.a(u),f+m*12),y=t.a(h,c+_*7),W=g+v*60,T=P+W*60,j=T*1e3,b=new Date(y.getTime()-j);return b}},function(l,i,e){i.a=o;var t=e(6),a=e(44),r=e(0);function o(n,s){r.a(2,arguments);var u=t.a(s);return a.a(n,-u)}},function(l,i,e){i.a=W;var t=e(28),a=e(49),r=e(1),o=e(160),n=e(156),s=e(19),u=e(157),d=e(6),m=e(381),f=e(0),_=10,c=/[yYQqMLwIdDecihHKkms]o|(\w)\1*|''|'(''|[^'])+('|$)|./g,v=/P+p+|P+|p+|''|'(''|[^'])+('|$)|./g,g=/^'([^]*?)'?$/,P=/''/g,h=/\S/,y=/[a-zA-Z]/;function W(b,C,I,M){f.a(3,arguments);var D=String(b),p=String(C),w=M||{},L=w.locale||t.a;if(!L.match)throw new RangeError("locale must contain match property");var z=L.options&&L.options.firstWeekContainsDate,ne=z==null?1:d.a(z),te=w.firstWeekContainsDate==null?ne:d.a(w.firstWeekContainsDate);if(!(te>=1&&te<=7))throw new RangeError("firstWeekContainsDate must be between 1 and 7 inclusively");var de=L.options&&L.options.weekStartsOn,ue=de==null?0:d.a(de),oe=w.weekStartsOn==null?ue:d.a(w.weekStartsOn);if(!(oe>=0&&oe<=6))throw new RangeError("weekStartsOn must be between 0 and 6 inclusively");if(p==="")return D===""?r.a(I):new Date(NaN);var ie={firstWeekContainsDate:te,weekStartsOn:oe,locale:L},le=[{priority:_,subPriority:-1,set:T,index:0}],F,S=p.match(v).map(function(fe){var ge=fe[0];if(ge==="p"||ge==="P"){var Ee=n.a[ge];return Ee(fe,L.formatLong,ie)}return fe}).join("").match(c),X=[];for(F=0;F0&&h.test(D))return new Date(NaN);var G=le.map(function(fe){return fe.priority}).sort(function(fe,ge){return ge-fe}).filter(function(fe,ge,Ee){return Ee.indexOf(fe)===ge}).map(function(fe){return le.filter(function(ge){return ge.priority===fe}).sort(function(ge,Ee){return Ee.subPriority-ge.subPriority})}).map(function(fe){return fe[0]}),ee=r.a(I);if(isNaN(ee))return new Date(NaN);var q=a.a(ee,s.a(ee)),re={};for(F=0;F=0&&_<=6))throw new RangeError("weekStartsOn must be between 0 and 6");var c=t.a(n),v=c.getDay(),g=(v<_?-7:0)+6-(v-_);return c.setHours(0,0,0,0),c.setDate(c.getDate()+g),c}},function(l,i,e){var t=e(3),a={locale:{1:"১",2:"২",3:"৩",4:"৪",5:"৫",6:"৬",7:"৭",8:"৮",9:"৯",0:"০"},number:{"১":"1","২":"2","৩":"3","৪":"4","৫":"5","৬":"6","৭":"7","৮":"8","৯":"9","০":"0"}},r={narrow:["খ্রিঃপূঃ","খ্রিঃ"],abbreviated:["খ্রিঃপূর্ব","খ্রিঃ"],wide:["খ্রিস্টপূর্ব","খ্রিস্টাব্দ"]},o={narrow:["১","২","৩","৪"],abbreviated:["১ত্রৈ","২ত্রৈ","৩ত্রৈ","৪ত্রৈ"],wide:["১ম ত্রৈমাসিক","২য় ত্রৈমাসিক","৩য় ত্রৈমাসিক","৪র্থ ত্রৈমাসিক"]},n={narrow:["জানু","ফেব্রু","মার্চ","এপ্রিল","মে","জুন","জুলাই","আগস্ট","সেপ্ট","অক্টো","নভে","ডিসে"],abbreviated:["জানু","ফেব্রু","মার্চ","এপ্রিল","মে","জুন","জুলাই","আগস্ট","সেপ্ট","অক্টো","নভে","ডিসে"],wide:["জানুয়ারি","ফেব্রুয়ারি","মার্চ","এপ্রিল","মে","জুন","জুলাই","আগস্ট","সেপ্টেম্বর","অক্টোবর","নভেম্বর","ডিসেম্বর"]},s={narrow:["র","সো","ম","বু","বৃ","শু","শ"],short:["রবি","সোম","মঙ্গল","বুধ","বৃহ","শুক্র","শনি"],abbreviated:["রবি","সোম","মঙ্গল","বুধ","বৃহ","শুক্র","শনি"],wide:["রবিবার","সোমবার","মঙ্গলবার","বুধবার","বৃহস্পতিবার ","শুক্রবার","শনিবার"]},u={narrow:{am:"পূ",pm:"অপ",midnight:"মধ্যরাত",noon:"মধ্যাহ্ন",morning:"সকাল",afternoon:"বিকাল",evening:"সন্ধ্যা",night:"রাত"},abbreviated:{am:"পূর্বাহ্ন",pm:"অপরাহ্ন",midnight:"মধ্যরাত",noon:"মধ্যাহ্ন",morning:"সকাল",afternoon:"বিকাল",evening:"সন্ধ্যা",night:"রাত"},wide:{am:"পূর্বাহ্ন",pm:"অপরাহ্ন",midnight:"মধ্যরাত",noon:"মধ্যাহ্ন",morning:"সকাল",afternoon:"বিকাল",evening:"সন্ধ্যা",night:"রাত"}},d={narrow:{am:"পূ",pm:"অপ",midnight:"মধ্যরাত",noon:"মধ্যাহ্ন",morning:"সকাল",afternoon:"বিকাল",evening:"সন্ধ্যা",night:"রাত"},abbreviated:{am:"পূর্বাহ্ন",pm:"অপরাহ্ন",midnight:"মধ্যরাত",noon:"মধ্যাহ্ন",morning:"সকাল",afternoon:"বিকাল",evening:"সন্ধ্যা",night:"রাত"},wide:{am:"পূর্বাহ্ন",pm:"অপরাহ্ন",midnight:"মধ্যরাত",noon:"মধ্যাহ্ন",morning:"সকাল",afternoon:"বিকাল",evening:"সন্ধ্যা",night:"রাত"}};function m(g,P){if(g>18&&g<=31)return P+"শে";switch(g){case 1:return P+"লা";case 2:case 3:return P+"রা";case 4:return P+"ঠা";default:return P+"ই"}}function f(g,P){var h=v.localeToNumber(g),y=v.numberToLocale(h),W=P.unit;if(W==="date")return m(h,y);if(h>10||h===0)return y+"তম";var T=h%10;switch(T){case 2:case 3:return y+"য়";case 4:return y+"র্থ";case 6:return y+"ষ্ঠ";case 1:case 5:case 7:case 8:case 9:case 0:return y+"ম"}}function _(g){var P=g.toString().replace(/[১২৩৪৫৬৭৮৯০]/g,function(h){return a.number[h]});return Number(P)}function c(g){return g.toString().replace(/\d/g,function(P){return a.locale[P]})}var v={localeToNumber:_,numberToLocale:c,ordinalNumber:f,era:t.a({values:r,defaultWidth:"wide"}),quarter:t.a({values:o,defaultWidth:"wide",argumentCallback:function(g){return Number(g)-1}}),month:t.a({values:n,defaultWidth:"wide"}),day:t.a({values:s,defaultWidth:"wide"}),dayPeriod:t.a({values:u,defaultWidth:"wide",formattingValues:d,defaultFormattingWidth:"wide"})};i.a=v},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"moins d’une seconde",other:"moins de {{count}} secondes"},xSeconds:{one:"1 seconde",other:"{{count}} secondes"},halfAMinute:"30 secondes",lessThanXMinutes:{one:"moins d’une minute",other:"moins de {{count}} minutes"},xMinutes:{one:"1 minute",other:"{{count}} minutes"},aboutXHours:{one:"environ 1 heure",other:"environ {{count}} heures"},xHours:{one:"1 heure",other:"{{count}} heures"},xDays:{one:"1 jour",other:"{{count}} jours"},aboutXWeeks:{one:"environ 1 semaine",other:"environ {{count}} semaines"},xWeeks:{one:"1 semaine",other:"{{count}} semaines"},aboutXMonths:{one:"environ 1 mois",other:"environ {{count}} mois"},xMonths:{one:"1 mois",other:"{{count}} mois"},aboutXYears:{one:"environ 1 an",other:"environ {{count}} ans"},xYears:{one:"1 an",other:"{{count}} ans"},overXYears:{one:"plus d’un an",other:"plus de {{count}} ans"},almostXYears:{one:"presqu’un an",other:"presque {{count}} ans"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"dans "+s:"il y a "+s:s}},function(l,i,e){i.a=a;var t={lastWeek:"eeee 'dernier à' p",yesterday:"'hier à' p",today:"'aujourd’hui à' p",tomorrow:"'demain à' p'",nextWeek:"eeee 'prochain à' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["av. J.-C","ap. J.-C"],abbreviated:["av. J.-C","ap. J.-C"],wide:["avant Jésus-Christ","après Jésus-Christ"]},r={narrow:["T1","T2","T3","T4"],abbreviated:["1er trim.","2ème trim.","3ème trim.","4ème trim."],wide:["1er trimestre","2ème trimestre","3ème trimestre","4ème trimestre"]},o={narrow:["J","F","M","A","M","J","J","A","S","O","N","D"],abbreviated:["janv.","févr.","mars","avr.","mai","juin","juil.","août","sept.","oct.","nov.","déc."],wide:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"]},n={narrow:["D","L","M","M","J","V","S"],short:["di","lu","ma","me","je","ve","sa"],abbreviated:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],wide:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"]},s={narrow:{am:"AM",pm:"PM",midnight:"minuit",noon:"midi",morning:"mat.",afternoon:"ap.m.",evening:"soir",night:"mat."},abbreviated:{am:"AM",pm:"PM",midnight:"minuit",noon:"midi",morning:"matin",afternoon:"après-midi",evening:"soir",night:"matin"},wide:{am:"AM",pm:"PM",midnight:"minuit",noon:"midi",morning:"du matin",afternoon:"de l’après-midi",evening:"du soir",night:"du matin"}};function u(m,f){var _=Number(m),c=f||{},v=String(c.unit),g;return _===0?_:(v==="year"||v==="hour"||v==="week"?_===1?g="ère":g="ème":_===1?g="er":g="ème",_+g)}var d={ordinalNumber:u,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(m){return Number(m)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide"})};i.a=d},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(ième|ère|ème|er|e)?/i,o=/\d+/i,n={narrow:/^(av\.J\.C|ap\.J\.C|ap\.J\.-C)/i,abbreviated:/^(av\.J\.-C|av\.J-C|apr\.J\.-C|apr\.J-C|ap\.J-C)/i,wide:/^(avant Jésus-Christ|après Jésus-Christ)/i},s={any:[/^av/i,/^ap/i]},u={narrow:/^[1234]/i,abbreviated:/^t[1234]/i,wide:/^[1234](er|ème|e)? trimestre/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[jfmasond]/i,abbreviated:/^(janv|févr|mars|avr|mai|juin|juill|juil|août|sept|oct|nov|déc)\.?/i,wide:/^(janvier|février|mars|avril|mai|juin|juillet|août|septembre|octobre|novembre|décembre)/i},f={narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^mar/i,/^av/i,/^ma/i,/^juin/i,/^juil/i,/^ao/i,/^s/i,/^o/i,/^n/i,/^d/i]},_={narrow:/^[lmjvsd]/i,short:/^(di|lu|ma|me|je|ve|sa)/i,abbreviated:/^(dim|lun|mar|mer|jeu|ven|sam)\.?/i,wide:/^(dimanche|lundi|mardi|mercredi|jeudi|vendredi|samedi)/i},c={narrow:[/^d/i,/^l/i,/^m/i,/^m/i,/^j/i,/^v/i,/^s/i],any:[/^di/i,/^lu/i,/^ma/i,/^me/i,/^je/i,/^ve/i,/^sa/i]},v={narrow:/^(a|p|minuit|midi|mat\.?|ap\.?m\.?|soir|nuit)/i,any:/^([ap]\.?\s?m\.?|du matin|de l'après[-\s]midi|du soir|de la nuit)/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/^min/i,noon:/^mid/i,morning:/mat/i,afternoon:/ap/i,evening:/soir/i,night:/nuit/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(64),a=f(t),r=e(65),o=f(r),n=e(12),s=f(n),u=e(12);e(203);var d=e(207),m=f(d);function f(c){return c&&c.__esModule?c:{default:c}}var _=function(){function c(){var v=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};(0,a.default)(this,c),this.options=v}return(0,o.default)(c,[{key:"render",value:function(g,P){var h=null;if(g=g||this.options.container,!g)throw new Error("Container is required: "+g);if(g instanceof HTMLElement)h=g;else if(h=window.document.getElementById(g),!h)throw new Error("Container not found, window.document.getElementById: "+g);return P||(P=function(){}),(0,u.render)(s.default.createElement(m.default,{options:this.options}),h,P)}}]),c}();l.exports=_},function(l,i,e){l.exports={default:e(193),__esModule:!0}},function(l,i,e){e(194);var t=e(7).Object;l.exports=function(r,o,n){return t.defineProperty(r,o,n)}},function(l,i,e){var t=e(11);t(t.S+t.F*!e(17),"Object",{defineProperty:e(16).f})},function(l,i,e){(function(t){l.exports=e(201)()}).call(i,e(18))},function(l,i,e){/** @license React v16.12.0 + * react-is.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */Object.defineProperty(i,"__esModule",{value:!0});var t=typeof Symbol=="function"&&Symbol.for,a=t?Symbol.for("react.element"):60103,r=t?Symbol.for("react.portal"):60106,o=t?Symbol.for("react.fragment"):60107,n=t?Symbol.for("react.strict_mode"):60108,s=t?Symbol.for("react.profiler"):60114,u=t?Symbol.for("react.provider"):60109,d=t?Symbol.for("react.context"):60110,m=t?Symbol.for("react.async_mode"):60111,f=t?Symbol.for("react.concurrent_mode"):60111,_=t?Symbol.for("react.forward_ref"):60112,c=t?Symbol.for("react.suspense"):60113,v=t?Symbol.for("react.suspense_list"):60120,g=t?Symbol.for("react.memo"):60115,P=t?Symbol.for("react.lazy"):60116,h=t?Symbol.for("react.fundamental"):60117,y=t?Symbol.for("react.responder"):60118,W=t?Symbol.for("react.scope"):60119;function T(b){if(typeof b=="object"&&b!==null){var C=b.$$typeof;switch(C){case a:switch(b=b.type,b){case m:case f:case o:case s:case n:case c:return b;default:switch(b=b&&b.$$typeof,b){case d:case _:case P:case g:case u:return b;default:return C}}case r:return C}}}function j(b){return T(b)===f}i.typeOf=T,i.AsyncMode=m,i.ConcurrentMode=f,i.ContextConsumer=d,i.ContextProvider=u,i.Element=a,i.ForwardRef=_,i.Fragment=o,i.Lazy=P,i.Memo=g,i.Portal=r,i.Profiler=s,i.StrictMode=n,i.Suspense=c,i.isValidElementType=function(b){return typeof b=="string"||typeof b=="function"||b===o||b===f||b===s||b===n||b===c||b===v||typeof b=="object"&&b!==null&&(b.$$typeof===P||b.$$typeof===g||b.$$typeof===u||b.$$typeof===d||b.$$typeof===_||b.$$typeof===h||b.$$typeof===y||b.$$typeof===W)},i.isAsyncMode=function(b){return j(b)||T(b)===m},i.isConcurrentMode=j,i.isContextConsumer=function(b){return T(b)===d},i.isContextProvider=function(b){return T(b)===u},i.isElement=function(b){return typeof b=="object"&&b!==null&&b.$$typeof===a},i.isForwardRef=function(b){return T(b)===_},i.isFragment=function(b){return T(b)===o},i.isLazy=function(b){return T(b)===P},i.isMemo=function(b){return T(b)===g},i.isPortal=function(b){return T(b)===r},i.isProfiler=function(b){return T(b)===s},i.isStrictMode=function(b){return T(b)===n},i.isSuspense=function(b){return T(b)===c}},function(l,i,e){(function(t){/** @license React v16.12.0 +* react-is.development.js +* +* Copyright (c) Facebook, Inc. and its affiliates. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/}).call(i,e(18))},function(l,i,e){(function(t){var a=e(99),r=e(199),o=e(68),n=e(200),s=Function.call.bind(Object.prototype.hasOwnProperty),u=function(){};function d(){return null}l.exports=function(m,f){var _=typeof Symbol=="function"&&Symbol.iterator,c="@@iterator";function v(F){var S=F&&(_&&F[_]||F[c]);if(typeof S=="function")return S}var g="<>",P={array:T("array"),bool:T("boolean"),func:T("function"),number:T("number"),object:T("object"),string:T("string"),symbol:T("symbol"),any:j(),arrayOf:b,element:C(),elementType:I(),instanceOf:M,node:L(),objectOf:p,oneOf:D,oneOfType:w,shape:z,exact:ne};function h(F,S){return F===S?F!==0||1/F===1/S:F!==F&&S!==S}function y(F){this.message=F,this.stack=""}y.prototype=Error.prototype;function W(F){function S(J,V,E,O,B,A,k){if(O=O||g,A=A||E,k!==o&&f){var U=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use `PropTypes.checkPropTypes()` to call them. Read more at http://fb.me/use-check-prop-types");throw U.name="Invariant Violation",U}return V[E]==null?J?V[E]===null?new y("The "+B+" `"+A+"` is marked as required "+("in `"+O+"`, but its value is `null`.")):new y("The "+B+" `"+A+"` is marked as required in "+("`"+O+"`, but its value is `undefined`.")):null:F(V,E,O,B,A)}var X=S.bind(null,!1);return X.isRequired=S.bind(null,!0),X}function T(F){function S(X,J,V,E,O,B){var A=X[J],k=ue(A);if(k!==F){var U=oe(A);return new y("Invalid "+E+" `"+O+"` of type "+("`"+U+"` supplied to `"+V+"`, expected ")+("`"+F+"`."))}return null}return W(S)}function j(){return W(d)}function b(F){function S(X,J,V,E,O){if(typeof F!="function")return new y("Property `"+O+"` of component `"+V+"` has invalid PropType notation inside arrayOf.");var B=X[J];if(!Array.isArray(B)){var A=ue(B);return new y("Invalid "+E+" `"+O+"` of type "+("`"+A+"` supplied to `"+V+"`, expected an array."))}for(var k=0;k"u"||F===null)return""+F;var S=ue(F);if(S==="object"){if(F instanceof Date)return"date";if(F instanceof RegExp)return"regexp"}return S}function ie(F){var S=oe(F);switch(S){case"array":case"object":return"an "+S;case"boolean":case"date":case"regexp":return"a "+S;default:return S}}function le(F){return!F.constructor||!F.constructor.name?g:F.constructor.name}return P.checkPropTypes=n,P.resetWarningCache=n.resetWarningCache,P.PropTypes=P,P}}).call(i,e(18))},function(l,i,e){/* +object-assign +(c) Sindre Sorhus +@license MIT +*/var t=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,r=Object.prototype.propertyIsEnumerable;function o(s){if(s==null)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(s)}function n(){try{if(!Object.assign)return!1;var s=new String("abc");if(s[5]="de",Object.getOwnPropertyNames(s)[0]==="5")return!1;for(var u={},d=0;d<10;d++)u["_"+String.fromCharCode(d)]=d;var m=Object.getOwnPropertyNames(u).map(function(_){return u[_]});if(m.join("")!=="0123456789")return!1;var f={};return"abcdefghijklmnopqrst".split("").forEach(function(_){f[_]=_}),Object.keys(Object.assign({},f)).join("")==="abcdefghijklmnopqrst"}catch{return!1}}l.exports=n()?Object.assign:function(s,u){for(var d,m=o(s),f,_=1;_2;)de.push(arguments[U]);for(O&&O.children!=null&&(de.length||de.push(O.children),delete O.children);de.length;)if((A=de.pop())&&A.pop!==void 0)for(U=A.length;U--;)de.push(A[U]);else(A===!0||A===!1)&&(A=null),(k=typeof E!="function")&&(A==null?A="":typeof A=="number"?A=String(A):typeof A!="string"&&(k=!1)),k&&B?G[G.length-1]+=A:G===ue?G=[A]:G.push(A),B=k;var ee=new t;return ee.nodeName=E,ee.children=G,ee.attributes=O??void 0,ee.key=O==null?void 0:O.key,te.vnode!==void 0&&te.vnode(ee),ee}function r(E,O){for(var B in O)E[B]=O[B];return E}function o(E,O){return a(E.nodeName,r(r({},E.attributes),O),arguments.length>2?[].slice.call(arguments,2):E.children)}function n(E){!E.__d&&(E.__d=!0)&&ie.push(E)==1&&(te.debounceRendering||setTimeout)(s)}function s(){var E,O=ie;for(ie=[];E=O.pop();)E.__d&&p(E)}function u(E,O,B){return typeof O=="string"||typeof O=="number"?E.splitText!==void 0:typeof O.nodeName=="string"?!E._componentConstructor&&d(E,O.nodeName):B||E._componentConstructor===O.nodeName}function d(E,O){return E.__n===O||E.nodeName.toLowerCase()===O.toLowerCase()}function m(E){var O=r({},E.attributes);O.children=E.children;var B=E.nodeName.defaultProps;if(B!==void 0)for(var A in B)O[A]===void 0&&(O[A]=B[A]);return O}function f(E,O){var B=O?document.createElementNS("http://www.w3.org/2000/svg",E):document.createElement(E);return B.__n=E,B}function _(E){E.parentNode&&E.parentNode.removeChild(E)}function c(E,O,B,A,k){if(O==="className"&&(O="class"),O!=="key")if(O==="ref")B&&B(null),A&&A(E);else if(O==="class"&&!k)E.className=A||"";else if(O==="style"){if((!A||typeof A=="string"||typeof B=="string")&&(E.style.cssText=A||""),A&&typeof A=="object"){if(typeof B!="string")for(var U in B)U in A||(E.style[U]="");for(var U in A)E.style[U]=typeof A[U]=="number"&&oe.test(U)===!1?A[U]+"px":A[U]}}else if(O==="dangerouslySetInnerHTML")A&&(E.innerHTML=A.__html||"");else if(O[0]=="o"&&O[1]=="n"){var G=O!==(O=O.replace(/Capture$/,""));O=O.toLowerCase().substring(2),A?B||E.addEventListener(O,g,G):E.removeEventListener(O,g,G),(E.__l||(E.__l={}))[O]=A}else if(O!=="list"&&O!=="type"&&!k&&O in E)v(E,O,A??""),(A==null||A===!1)&&E.removeAttribute(O);else{var ee=k&&O!==(O=O.replace(/^xlink\:?/,""));A==null||A===!1?ee?E.removeAttributeNS("http://www.w3.org/1999/xlink",O.toLowerCase()):E.removeAttribute(O):typeof A!="function"&&(ee?E.setAttributeNS("http://www.w3.org/1999/xlink",O.toLowerCase(),A):E.setAttribute(O,A))}}function v(E,O,B){try{E[O]=B}catch{}}function g(E){return this.__l[E.type](te.event&&te.event(E)||E)}function P(){for(var E;E=le.pop();)te.afterMount&&te.afterMount(E),E.componentDidMount&&E.componentDidMount()}function h(E,O,B,A,k,U){F++||(S=k!=null&&k.ownerSVGElement!==void 0,X=E!=null&&!("__preactattr_"in E));var G=y(E,O,B,A,U);return k&&G.parentNode!==k&&k.appendChild(G),--F||(X=!1,U||P()),G}function y(E,O,B,A,k){var U=E,G=S;if(O==null&&(O=""),typeof O=="string")return E&&E.splitText!==void 0&&E.parentNode&&(!E._component||k)?E.nodeValue!=O&&(E.nodeValue=O):(U=document.createTextNode(O),E&&(E.parentNode&&E.parentNode.replaceChild(U,E),T(E,!0))),U.__preactattr_=!0,U;if(typeof O.nodeName=="function")return w(E,O,B,A);if(S=O.nodeName==="svg"?!0:O.nodeName==="foreignObject"?!1:S,(!E||!d(E,String(O.nodeName)))&&(U=f(String(O.nodeName),S),E)){for(;E.firstChild;)U.appendChild(E.firstChild);E.parentNode&&E.parentNode.replaceChild(U,E),T(E,!0)}var ee=U.firstChild,q=U.__preactattr_||(U.__preactattr_={}),re=O.children;return!X&&re&&re.length===1&&typeof re[0]=="string"&&ee!=null&&ee.splitText!==void 0&&ee.nextSibling==null?ee.nodeValue!=re[0]&&(ee.nodeValue=re[0]):(re&&re.length||ee!=null)&&W(U,re,B,A,X||q.dangerouslySetInnerHTML!=null),b(U,O.attributes,q),S=G,U}function W(E,O,B,A,k){var U,G,ee,q,re=E.childNodes,ce=[],he={},fe=0,ge=0,Ee=re.length,x=0,N=O?O.length:0;if(Ee!==0)for(var Y=0;Y=Ee?E.appendChild(q):q!==re[Y]&&(q===re[Y+1]?_(re[Y]):E.insertBefore(q,re[Y]||null)))}if(fe)for(var Y in he)he[Y]!==void 0&&T(he[Y],!1);for(;ge<=x;)(q=ce[x--])!==void 0&&T(q,!1)}function T(E,O){var B=E._component;B?L(B):(E.__preactattr_!=null&&E.__preactattr_.ref&&E.__preactattr_.ref(null),(O===!1||E.__preactattr_==null)&&_(E),j(E))}function j(E){for(E=E.lastChild;E;){var O=E.previousSibling;T(E,!0),E=O}}function b(E,O,B){var A;for(A in B)(!O||O[A]==null)&&B[A]!=null&&c(E,A,B[A],B[A]=void 0,S);for(A in O)A==="children"||A==="innerHTML"||A in B&&O[A]===(A==="value"||A==="checked"?E[A]:B[A])||c(E,A,B[A],B[A]=O[A],S)}function C(E){var O=E.constructor.name;(J[O]||(J[O]=[])).push(E)}function I(E,O,B){var A,k=J[E.name];if(E.prototype&&E.prototype.render?(A=new E(O,B),z.call(A,O,B)):(A=new z(O,B),A.constructor=E,A.render=M),k){for(var U=k.length;U--;)if(k[U].constructor===E){A.__b=k[U].__b,k.splice(U,1);break}}return A}function M(E,O,B){return this.constructor(E,B)}function D(E,O,B,A,k){E.__x||(E.__x=!0,(E.__r=O.ref)&&delete O.ref,(E.__k=O.key)&&delete O.key,!E.base||k?E.componentWillMount&&E.componentWillMount():E.componentWillReceiveProps&&E.componentWillReceiveProps(O,A),A&&A!==E.context&&(E.__c||(E.__c=E.context),E.context=A),E.__p||(E.__p=E.props),E.props=O,E.__x=!1,B!==0&&(B===1||te.syncComponentUpdates!==!1||!E.base?p(E,1,k):n(E)),E.__r&&E.__r(E))}function p(E,O,B,A){if(!E.__x){var k,U,G,ee=E.props,q=E.state,re=E.context,ce=E.__p||ee,he=E.__s||q,fe=E.__c||re,ge=E.base,Ee=E.__b,x=ge||Ee,N=E._component,Y=!1;if(ge&&(E.props=ce,E.state=he,E.context=fe,O!==2&&E.shouldComponentUpdate&&E.shouldComponentUpdate(ee,q,re)===!1?Y=!0:E.componentWillUpdate&&E.componentWillUpdate(ee,q,re),E.props=ee,E.state=q,E.context=re),E.__p=E.__s=E.__c=E.__b=null,E.__d=!1,!Y){k=E.render(ee,q,re),E.getChildContext&&(re=r(r({},re),E.getChildContext()));var $,Q,ae=k&&k.nodeName;if(typeof ae=="function"){var me=m(k);U=N,U&&U.constructor===ae&&me.key==U.__k?D(U,me,1,re,!1):($=U,E._component=U=I(ae,me,re),U.__b=U.__b||Ee,U.__u=E,D(U,me,0,re,!1),p(U,1,B,!0)),Q=U.base}else G=x,$=N,$&&(G=E._component=null),(x||O===1)&&(G&&(G._component=null),Q=h(G,k,re,B||!ge,x&&x.parentNode,!0));if(x&&Q!==x&&U!==N){var ve=x.parentNode;ve&&Q!==ve&&(ve.replaceChild(Q,x),$||(x._component=null,T(x,!1)))}if($&&L($),E.base=Q,Q&&!A){for(var pe=E,K=E;K=K.__u;)(pe=K).base=Q;Q._component=pe,Q._componentConstructor=pe.constructor}}if(!ge||B?le.unshift(E):Y||(P(),E.componentDidUpdate&&E.componentDidUpdate(ce,he,fe),te.afterUpdate&&te.afterUpdate(E)),E.__h!=null)for(;E.__h.length;)E.__h.pop().call(E);!F&&!A&&P()}}function w(E,O,B,A){for(var k=E&&E._component,U=k,G=E,ee=k&&E._componentConstructor===O.nodeName,q=ee,re=m(O);k&&!q&&(k=k.__u);)q=k.constructor===O.nodeName;return k&&q&&(!A||k._component)?(D(k,re,3,B,A),E=k.base):(U&&!ee&&(L(U),E=G=null),k=I(O.nodeName,re,B),E&&!k.__b&&(k.__b=E,G=null),D(k,re,1,B,A),E=k.base,G&&E!==G&&(G._component=null,T(G,!1))),E}function L(E){te.beforeUnmount&&te.beforeUnmount(E);var O=E.base;E.__x=!0,E.componentWillUnmount&&E.componentWillUnmount(),E.base=null;var B=E._component;B?L(B):O&&(O.__preactattr_&&O.__preactattr_.ref&&O.__preactattr_.ref(null),E.__b=O,_(O),C(E),j(O)),E.__r&&E.__r(null)}function z(E,O){this.__d=!0,this.context=O,this.props=E,this.state=this.state||{}}function ne(E,O,B){return h(B,E,{},!1,O,!1)}var te={},de=[],ue=[],oe=/acit|ex(?:s|g|n|p|$)|rph|ows|mnc|ntw|ine[ch]|zoo|^ord/i,ie=[],le=[],F=0,S=!1,X=!1,J={};r(z.prototype,{setState:function(E,O){var B=this.state;this.__s||(this.__s=r({},B)),r(B,typeof E=="function"?E(B,this.props):E),O&&(this.__h=this.__h||[]).push(O),n(this)},forceUpdate:function(E){E&&(this.__h=this.__h||[]).push(E),p(this,2)},render:function(){}});var V={h:a,createElement:a,cloneElement:o,Component:z,render:ne,rerender:s,options:te};l.exports=V})()},function(l,i,e){l.exports=e(204).polyfill()},function(l,i,e){(function(t,a){var r;/*! +* @overview es6-promise - a tiny implementation of Promises/A+. +* @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald) +* @license Licensed under MIT license +* See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE +* @version 4.1.1 +*/(function(o,n){l.exports=n()})(this,function(){function o(K){var Z=typeof K;return K!==null&&(Z==="object"||Z==="function")}function n(K){return typeof K=="function"}var s=void 0;Array.isArray?s=Array.isArray:s=function(K){return Object.prototype.toString.call(K)==="[object Array]"};var u=s,d=0,m=void 0,f=void 0,_=function(Z,se){M[d]=Z,M[d+1]=se,d+=2,d===2&&(f?f(D):w())};function c(K){f=K}function v(K){_=K}var g=typeof window<"u"?window:void 0,P=g||{},h=P.MutationObserver||P.WebKitMutationObserver,y=typeof self>"u"&&typeof t<"u"&&{}.toString.call(t)==="[object process]",W=typeof Uint8ClampedArray<"u"&&typeof importScripts<"u"&&typeof MessageChannel<"u";function T(){return function(){return t.nextTick(D)}}function j(){return typeof m<"u"?function(){m(D)}:I()}function b(){var K=0,Z=new h(D),se=document.createTextNode("");return Z.observe(se,{characterData:!0}),function(){se.data=K=++K%2}}function C(){var K=new MessageChannel;return K.port1.onmessage=D,function(){return K.port2.postMessage(0)}}function I(){var K=setTimeout;return function(){return K(D,1)}}var M=new Array(1e3);function D(){for(var K=0;K=x.comments||fe.data.length "+re}),q.push(""),q.push(""),G&&q.unshift(""),E.setState({comment:G+q.join(` +`)},function(){j.default.update(E.commentEL),E.commentEL.focus()})}},E.handlePopup=function(U){U.preventDefault(),U.stopPropagation();var G=!E.state.isPopupVisible,ee=function q(re){(0,I.hasClassInParent)(re.target,"gt-user","gt-popup")||(window.document.removeEventListener("click",q),E.setState({isPopupVisible:!1}))};E.setState({isPopupVisible:G}),G?window.document.addEventListener("click",ee):window.document.removeEventListener("click",ee)},E.handleLogin=function(){var U=E.state.comment;window.localStorage.setItem(oe.GT_COMMENT,encodeURIComponent(U)),window.location.href=E.loginLink},E.handleIssueCreate=function(){E.setState({isIssueCreating:!0}),E.createIssue().then(function(U){return E.setState({isIssueCreating:!1,isOccurError:!1}),E.getComments(U)}).catch(function(U){E.setState({isIssueCreating:!1,isOccurError:!0,errorMsg:(0,I.formatErrorMsg)(U)})}).then(function(U){U&&E.setState({isNoInit:!1})})},E.handleCommentCreate=function(U){if(!E.state.comment.length){U&&U.preventDefault(),E.commentEL.focus();return}E.setState(function(G){if(!G.isCreating)return E.createComment().then(function(){return E.setState({isCreating:!1,isOccurError:!1})}).catch(function(ee){E.setState({isCreating:!1,isOccurError:!0,errorMsg:(0,I.formatErrorMsg)(ee)})}),{isCreating:!0}})},E.handleCommentPreview=function(U){E.setState({isPreview:!E.state.isPreview}),E.state.isPreview&&I.axiosGithub.post("/markdown",{text:E.state.comment},{headers:E.accessToken&&{Authorization:"token "+E.accessToken}}).then(function(G){E.setState({previewHtml:G.data})}).catch(function(G){E.setState({isOccurError:!0,errorMsg:(0,I.formatErrorMsg)(G)})})},E.handleCommentLoad=function(){var U=E.state,G=U.issue,ee=U.isLoadMore;ee||(E.setState({isLoadMore:!0}),E.getComments(G).then(function(){return E.setState({isLoadMore:!1})}))},E.handleCommentChange=function(U){return E.setState({comment:U.target.value})},E.handleLogout=function(){E.logout(),window.location.reload()},E.handleCommentFocus=function(U){var G=E.options.distractionFreeMode;if(!G)return U.preventDefault();E.setState({isInputFocused:!0})},E.handleCommentBlur=function(U){var G=E.options.distractionFreeMode;if(!G)return U.preventDefault();E.setState({isInputFocused:!1})},E.handleSort=function(U){return function(G){E.setState({pagerDirection:U})}},E.handleCommentKeyDown=function(U){var G=E.options.enableHotKey;G&&(U.metaKey||U.ctrlKey)&&U.keyCode===13&&(E.publicBtnEL&&E.publicBtnEL.focus(),E.handleCommentCreate())},E.options=(0,o.default)({},{id:window.location.href,number:-1,labels:["Gitalk"],title:window.document.title,body:"",language:window.navigator.language||window.navigator.userLanguage,perPage:10,pagerDirection:"last",createIssueManually:!1,distractionFreeMode:!1,proxy:"https://cors-anywhere.azm.workers.dev/https://github.com/login/oauth/access_token",flipMoveOptions:{staggerDelayBy:150,appearAnimation:"accordionVertical",enterAnimation:"accordionVertical",leaveAnimation:"accordionVertical"},enableHotKey:!0,url:window.location.href,defaultAuthor:{avatarUrl:"//avatars1.githubusercontent.com/u/29697133?s=50",login:"null",url:""},updateCountCallback:null},V.options),E.state.pagerDirection=E.options.pagerDirection;var O=window.localStorage.getItem(oe.GT_COMMENT);O&&(E.state.comment=decodeURIComponent(O),window.localStorage.removeItem(oe.GT_COMMENT));var B=(0,I.queryParse)();if(B.code){var A=B.code;delete B.code;var k=""+window.location.origin+window.location.pathname+"?"+(0,I.queryStringify)(B)+window.location.hash;history.replaceState(null,null,k),E.options=(0,o.default)({},E.options,{url:k,id:k},V.options),I.axiosJSON.post(E.options.proxy,{code:A,client_id:E.options.clientID,client_secret:E.options.clientSecret}).then(function(U){U.data&&U.data.access_token?(E.accessToken=U.data.access_token,E.getInit().then(function(){return E.setState({isIniting:!1})}).catch(function(G){console.log("err:",G),E.setState({isIniting:!1,isOccurError:!0,errorMsg:(0,I.formatErrorMsg)(G)})})):(console.log("res.data err:",U.data),E.setState({isOccurError:!0,errorMsg:(0,I.formatErrorMsg)(new Error("no access token"))}))}).catch(function(U){console.log("err: ",U),E.setState({isOccurError:!0,errorMsg:(0,I.formatErrorMsg)(U)})})}else E.getInit().then(function(){return E.setState({isIniting:!1})}).catch(function(U){console.log("err:",U),E.setState({isIniting:!1,isOccurError:!0,errorMsg:(0,I.formatErrorMsg)(U)})});return E.i18n=(0,C.default)(E.options.language),E}return(0,f.default)(J,[{key:"componentDidUpdate",value:function(){this.commentEL&&(0,j.default)(this.commentEL)}},{key:"getInit",value:function(){var E=this;return this.getUserInfo().then(function(){return E.getIssue()}).then(function(O){return E.getComments(O)})}},{key:"getUserInfo",value:function(){var E=this;return this.accessToken?I.axiosGithub.get("/user",{headers:{Authorization:"token "+this.accessToken}}).then(function(O){E.setState({user:O.data})}).catch(function(O){E.logout()}):new a.default(function(O){O()})}},{key:"getIssueById",value:function(){var E=this,O=this.options,B=O.owner,A=O.repo,k=O.number,U=O.clientID,G=O.clientSecret,ee="/repos/"+B+"/"+A+"/issues/"+k;return new a.default(function(q,re){I.axiosGithub.get(ee,{auth:{username:U,password:G},params:{t:Date.now()}}).then(function(ce){var he=null;ce&&ce.data&&ce.data.number===k&&(he=ce.data,E.setState({issue:he,isNoInit:!1})),q(he)}).catch(function(ce){ce.response.status===404&&q(null),re(ce)})})}},{key:"getIssueByLabels",value:function(){var E=this,O=this.options,B=O.owner,A=O.repo,k=O.id,U=O.labels,G=O.clientID,ee=O.clientSecret;return I.axiosGithub.get("/repos/"+B+"/"+A+"/issues",{auth:{username:G,password:ee},params:{labels:U.concat(k).join(","),t:Date.now()}}).then(function(q){var re=E.options.createIssueManually,ce=!1,he=null;if(q&&q.data&&q.data.length)he=q.data[0];else{if(!re&&E.isAdmin)return E.createIssue();ce=!0}return E.setState({issue:he,isNoInit:ce}),he})}},{key:"getIssue",value:function(){var E=this,O=this.options.number,B=this.state.issue;return B?(this.setState({isNoInit:!1}),a.default.resolve(B)):typeof O=="number"&&O>0?this.getIssueById().then(function(A){return A||E.getIssueByLabels()}):this.getIssueByLabels()}},{key:"createIssue",value:function(){var E=this,O=this.options,B=O.owner,A=O.repo,k=O.title,U=O.body,G=O.id,ee=O.labels,q=O.url;return I.axiosGithub.post("/repos/"+B+"/"+A+"/issues",{title:k,labels:ee.concat(G),body:U||q+` + + `+((0,I.getMetaContent)("description")||(0,I.getMetaContent)("description","og:description")||"")},{headers:{Authorization:"token "+this.accessToken}}).then(function(re){return E.setState({issue:re.data}),re.data})}},{key:"getComments",value:function(E){if(E)return this.accessToken?le.default.call(this,E):this.getCommentsV3(E)}},{key:"createComment",value:function(){var E=this,O=this.state,B=O.comment,A=O.localComments,k=O.comments;return this.getIssue().then(function(U){return I.axiosGithub.post(U.comments_url,{body:B},{headers:{Accept:"application/vnd.github.v3.full+json",Authorization:"token "+E.accessToken}})}).then(function(U){E.setState({comment:"",comments:k.concat(U.data),localComments:A.concat(U.data)})})}},{key:"logout",value:function(){this.setState({user:null}),window.localStorage.removeItem(oe.GT_ACCESS_TOKEN)}},{key:"like",value:function(E){var O=this,B=this.options,A=B.owner,k=B.repo,U=this.state.user,G=this.state.comments;I.axiosGithub.post("/repos/"+A+"/"+k+"/issues/comments/"+E.id+"/reactions",{content:"heart"},{headers:{Authorization:"token "+this.accessToken,Accept:"application/vnd.github.squirrel-girl-preview"}}).then(function(ee){G=G.map(function(q){return q.id===E.id?(q.reactions?~q.reactions.nodes.findIndex(function(re){return re.user.login===U.login})||(q.reactions.totalCount+=1):(q.reactions={nodes:[]},q.reactions.totalCount=1),q.reactions.nodes.push(ee.data),q.reactions.viewerHasReacted=!0,(0,o.default)({},q)):q}),O.setState({comments:G})})}},{key:"unLike",value:function(E){var O=this,B=this.state.user,A=this.state.comments,k=function(G){return{operationName:"RemoveReaction",query:` + mutation RemoveReaction{ + removeReaction (input:{ + subjectId: "`+G+`", + content: HEART + }) { + reaction { + content + } + } + } + `}};I.axiosGithub.post("/graphql",k(E.gId),{headers:{Authorization:"bearer "+this.accessToken}}).then(function(U){U.data&&(A=A.map(function(G){if(G.id===E.id){var ee=G.reactions.nodes.findIndex(function(q){return q.user.login===B.login});return~ee&&(G.reactions.totalCount-=1,G.reactions.nodes.splice(ee,1)),G.reactions.viewerHasReacted=!1,(0,o.default)({},G)}return G}),O.setState({comments:A}))})}},{key:"initing",value:function(){return h.default.createElement("div",{className:"gt-initing"},h.default.createElement("i",{className:"gt-loader"}),h.default.createElement("p",{className:"gt-initing-text"},this.i18n.t("init")))}},{key:"noInit",value:function(){var E=this.state,O=E.user,B=E.isIssueCreating,A=this.options,k=A.owner,U=A.repo,G=A.admin;return h.default.createElement("div",{className:"gt-no-init",key:"no-init"},h.default.createElement("p",{dangerouslySetInnerHTML:{__html:this.i18n.t("no-found-related",{link:'Issues'})}}),h.default.createElement("p",null,this.i18n.t("please-contact",{user:[].concat(G).map(function(ee){return"@"+ee}).join(" ")})),this.isAdmin?h.default.createElement("p",null,h.default.createElement(w.default,{onClick:this.handleIssueCreate,isLoading:B,text:this.i18n.t("init-issue")})):null,!O&&h.default.createElement(w.default,{className:"gt-btn-login",onClick:this.handleLogin,text:this.i18n.t("login-with-github")}))}},{key:"header",value:function(){var E=this,O=this.state,B=O.user,A=O.comment,k=O.isCreating,U=O.previewHtml,G=O.isPreview;return h.default.createElement("div",{className:"gt-header",key:"header"},B?h.default.createElement(D.default,{className:"gt-header-avatar",src:B.avatar_url,alt:B.login}):h.default.createElement("a",{className:"gt-avatar-github",onClick:this.handleLogin},h.default.createElement(ue.default,{className:"gt-ico-github",name:"github"})),h.default.createElement("div",{className:"gt-header-comment"},h.default.createElement("textarea",{ref:function(q){E.commentEL=q},className:"gt-header-textarea "+(G?"hide":""),value:A,onChange:this.handleCommentChange,onFocus:this.handleCommentFocus,onBlur:this.handleCommentBlur,onKeyDown:this.handleCommentKeyDown,placeholder:this.i18n.t("leave-a-comment")}),h.default.createElement("div",{className:"gt-header-preview markdown-body "+(G?"":"hide"),dangerouslySetInnerHTML:{__html:U}}),h.default.createElement("div",{className:"gt-header-controls"},h.default.createElement("a",{className:"gt-header-controls-tip",href:"https://guides.github.com/features/mastering-markdown/",target:"_blank",rel:"noopener noreferrer"},h.default.createElement(ue.default,{className:"gt-ico-tip",name:"tip",text:this.i18n.t("support-markdown")})),B&&h.default.createElement(w.default,{getRef:this.getRef,className:"gt-btn-public",onClick:this.handleCommentCreate,text:this.i18n.t("comment"),isLoading:k}),h.default.createElement(w.default,{className:"gt-btn-preview",onClick:this.handleCommentPreview,text:G?this.i18n.t("edit"):this.i18n.t("preview")}),!B&&h.default.createElement(w.default,{className:"gt-btn-login",onClick:this.handleLogin,text:this.i18n.t("login-with-github")}))))}},{key:"comments",value:function(){var E=this,O=this.state,B=O.user,A=O.comments,k=O.isLoadOver,U=O.isLoadMore,G=O.pagerDirection,ee=this.options,q=ee.language,re=ee.flipMoveOptions,ce=ee.admin,he=A.concat([]);return G==="last"&&this.accessToken&&he.reverse(),h.default.createElement("div",{className:"gt-comments",key:"comments"},h.default.createElement(W.default,re,he.map(function(fe){return h.default.createElement(te.default,{comment:fe,key:fe.id,user:B,language:q,commentedText:E.i18n.t("commented"),admin:ce,replyCallback:E.reply(fe),likeCallback:fe.reactions&&fe.reactions.viewerHasReacted?E.unLike.bind(E,fe):E.like.bind(E,fe)})})),!he.length&&h.default.createElement("p",{className:"gt-comments-null"},this.i18n.t("first-comment-person")),!k&&he.length?h.default.createElement("div",{className:"gt-comments-controls"},h.default.createElement(w.default,{className:"gt-btn-loadmore",onClick:this.handleCommentLoad,isLoading:U,text:this.i18n.t("load-more")})):null)}},{key:"meta",value:function(){var E=this.state,O=E.user,B=E.issue,A=E.isPopupVisible,k=E.pagerDirection,U=E.localComments,G=(B&&B.comments)+U.length,ee=k==="last",q=this.options.updateCountCallback;if(q&&{}.toString.call(q)==="[object Function]")try{q(G)}catch(re){console.log("An error occurred executing the updateCountCallback:",re)}return h.default.createElement("div",{className:"gt-meta",key:"meta"},h.default.createElement("span",{className:"gt-counts",dangerouslySetInnerHTML:{__html:this.i18n.t("counts",{counts:''+G+"",smart_count:G})}}),A&&h.default.createElement("div",{className:"gt-popup"},O?h.default.createElement(z.default,{className:"gt-action-sortasc"+(ee?"":" is--active"),onClick:this.handleSort("first"),text:this.i18n.t("sort-asc")}):null,O?h.default.createElement(z.default,{className:"gt-action-sortdesc"+(ee?" is--active":""),onClick:this.handleSort("last"),text:this.i18n.t("sort-desc")}):null,O?h.default.createElement(z.default,{className:"gt-action-logout",onClick:this.handleLogout,text:this.i18n.t("logout")}):h.default.createElement("a",{className:"gt-action gt-action-login",onClick:this.handleLogin},this.i18n.t("login-with-github")),h.default.createElement("div",{className:"gt-copyright"},h.default.createElement("a",{className:"gt-link gt-link-project",href:"https://github.com/gitalk/gitalk",target:"_blank",rel:"noopener noreferrer"},"Gitalk"),h.default.createElement("span",{className:"gt-version"},oe.GT_VERSION))),h.default.createElement("div",{className:"gt-user"},O?h.default.createElement("div",{className:A?"gt-user-inner is--poping":"gt-user-inner",onClick:this.handlePopup},h.default.createElement("span",{className:"gt-user-name"},O.login),h.default.createElement(ue.default,{className:"gt-ico-arrdown",name:"arrow_down"})):h.default.createElement("div",{className:A?"gt-user-inner is--poping":"gt-user-inner",onClick:this.handlePopup},h.default.createElement("span",{className:"gt-user-name"},this.i18n.t("anonymous")),h.default.createElement(ue.default,{className:"gt-ico-arrdown",name:"arrow_down"}))))}},{key:"render",value:function(){var E=this.state,O=E.isIniting,B=E.isNoInit,A=E.isOccurError,k=E.errorMsg,U=E.isInputFocused;return h.default.createElement("div",{className:"gt-container"+(U?" gt-input-focused":"")},O&&this.initing(),!O&&(B?[]:[this.meta()]),A&&h.default.createElement("div",{className:"gt-error"},k),!O&&(B?[this.noInit()]:[this.header(),this.comments()]))}},{key:"accessToken",get:function(){return this._accessToke||window.localStorage.getItem(oe.GT_ACCESS_TOKEN)},set:function(E){window.localStorage.setItem(oe.GT_ACCESS_TOKEN,E),this._accessToken=E}},{key:"loginLink",get:function(){var E="https://github.com/login/oauth/authorize",O=this.options.clientID,B={client_id:O,redirect_uri:window.location.href,scope:"public_repo"};return E+"?"+(0,I.queryStringify)(B)}},{key:"isAdmin",get:function(){var E=this.options.admin,O=this.state.user;return O&&~[].concat(E).map(function(B){return B.toLowerCase()}).indexOf(O.login.toLowerCase())}}]),J}(P.Component);l.exports=S},function(l,i,e){l.exports={default:e(209),__esModule:!0}},function(l,i,e){e(100),e(40),e(54),e(218),e(226),e(227),l.exports=e(7).Promise},function(l,i,e){var t=e(69),a=e(70);l.exports=function(r){return function(o,n){var s=String(a(o)),u=t(n),d=s.length,m,f;return u<0||u>=d?r?"":void 0:(m=s.charCodeAt(u),m<55296||m>56319||u+1===d||(f=s.charCodeAt(u+1))<56320||f>57343?r?s.charAt(u):m:r?s.slice(u,u+2):(m-55296<<10)+(f-56320)+65536)}}},function(l,i,e){var t=e(71),a=e(39),r=e(53),o={};e(23)(o,e(9)("iterator"),function(){return this}),l.exports=function(n,s,u){n.prototype=t(o,{next:a(1,u)}),r(n,s+" Iterator")}},function(l,i,e){var t=e(16),a=e(14),r=e(42);l.exports=e(17)?Object.defineProperties:function(n,s){a(n);for(var u=r(s),d=u.length,m=0,f;d>m;)t.f(n,f=u[m++],s[f]);return n}},function(l,i,e){var t=e(32),a=e(72),r=e(214);l.exports=function(o){return function(n,s,u){var d=t(n),m=a(d.length),f=r(u,m),_;if(o&&s!=s){for(;m>f;)if(_=d[f++],_!=_)return!0}else for(;m>f;f++)if((o||f in d)&&d[f]===s)return o||f||0;return!o&&-1}}},function(l,i,e){var t=e(69),a=Math.max,r=Math.min;l.exports=function(o,n){return o=t(o),o<0?a(o+n,0):r(o,n)}},function(l,i,e){var t=e(216),a=e(217),r=e(31),o=e(32);l.exports=e(101)(Array,"Array",function(n,s){this._t=o(n),this._i=0,this._k=s},function(){var n=this._t,s=this._k,u=this._i++;return!n||u>=n.length?(this._t=void 0,a(1)):s=="keys"?a(0,u):s=="values"?a(0,n[u]):a(0,[u,n[u]])},"values"),r.Arguments=r.Array,t("keys"),t("values"),t("entries")},function(l,i){l.exports=function(){}},function(l,i){l.exports=function(e,t){return{value:t,done:!!e}}},function(l,i,e){var t=e(41),a=e(8),r=e(29),o=e(76),n=e(11),s=e(20),u=e(51),d=e(219),m=e(220),f=e(109),_=e(110).set,c=e(222)(),v=e(78),g=e(111),P=e(223),h=e(112),y="Promise",W=a.TypeError,T=a.process,j=T&&T.versions,b=j&&j.v8||"",C=a[y],I=o(T)=="process",M=function(){},D,p,w,L,z=p=v.f,ne=!!function(){try{var S=C.resolve(1),X=(S.constructor={})[e(9)("species")]=function(J){J(M,M)};return(I||typeof PromiseRejectionEvent=="function")&&S.then(M)instanceof X&&b.indexOf("6.6")!==0&&P.indexOf("Chrome/66")===-1}catch{}}(),te=function(S){var X;return s(S)&&typeof(X=S.then)=="function"?X:!1},de=function(S,X){if(!S._n){S._n=!0;var J=S._c;c(function(){for(var V=S._v,E=S._s==1,O=0,B=function(A){var k=E?A.ok:A.fail,U=A.resolve,G=A.reject,ee=A.domain,q,re,ce;try{k?(E||(S._h==2&&ie(S),S._h=1),k===!0?q=V:(ee&&ee.enter(),q=k(V),ee&&(ee.exit(),ce=!0)),q===A.promise?G(W("Promise-chain cycle")):(re=te(q))?re.call(q,U,G):U(q)):G(V)}catch(he){ee&&!ce&&ee.exit(),G(he)}};J.length>O;)B(J[O++]);S._c=[],S._n=!1,X&&!S._h&&ue(S)})}},ue=function(S){_.call(a,function(){var X=S._v,J=oe(S),V,E,O;if(J&&(V=g(function(){I?T.emit("unhandledRejection",X,S):(E=a.onunhandledrejection)?E({promise:S,reason:X}):(O=a.console)&&O.error&&O.error("Unhandled promise rejection",X)}),S._h=I||oe(S)?2:1),S._a=void 0,J&&V.e)throw V.v})},oe=function(S){return S._h!==1&&(S._a||S._c).length===0},ie=function(S){_.call(a,function(){var X;I?T.emit("rejectionHandled",S):(X=a.onrejectionhandled)&&X({promise:S,reason:S._v})})},le=function(S){var X=this;X._d||(X._d=!0,X=X._w||X,X._v=S,X._s=2,X._a||(X._a=X._c.slice()),de(X,!0))},F=function(S){var X=this,J;if(!X._d){X._d=!0,X=X._w||X;try{if(X===S)throw W("Promise can't be resolved itself");(J=te(S))?c(function(){var V={_w:X,_d:!1};try{J.call(S,r(F,V,1),r(le,V,1))}catch(E){le.call(V,E)}}):(X._v=S,X._s=1,de(X,!1))}catch(V){le.call({_w:X,_d:!1},V)}}};ne||(C=function(X){d(this,C,y,"_h"),u(X),D.call(this);try{X(r(F,this,1),r(le,this,1))}catch(J){le.call(this,J)}},D=function(X){this._c=[],this._a=void 0,this._s=0,this._d=!1,this._v=void 0,this._h=0,this._n=!1},D.prototype=e(224)(C.prototype,{then:function(X,J){var V=z(f(this,C));return V.ok=typeof X=="function"?X:!0,V.fail=typeof J=="function"&&J,V.domain=I?T.domain:void 0,this._c.push(V),this._a&&this._a.push(V),this._s&&de(this,!1),V.promise},catch:function(S){return this.then(void 0,S)}}),w=function(){var S=new D;this.promise=S,this.resolve=r(F,S,1),this.reject=r(le,S,1)},v.f=z=function(S){return S===C||S===L?new w(S):p(S)}),n(n.G+n.W+n.F*!ne,{Promise:C}),e(53)(C,y),e(225)(y),L=e(7)[y],n(n.S+n.F*!ne,y,{reject:function(X){var J=z(this),V=J.reject;return V(X),J.promise}}),n(n.S+n.F*(t||!ne),y,{resolve:function(X){return h(t&&this===L?C:this,X)}}),n(n.S+n.F*!(ne&&e(113)(function(S){C.all(S).catch(M)})),y,{all:function(X){var J=this,V=z(J),E=V.resolve,O=V.reject,B=g(function(){var A=[],k=0,U=1;m(X,!1,function(G){var ee=k++,q=!1;A.push(void 0),U++,J.resolve(G).then(function(re){q||(q=!0,A[ee]=re,--U||E(A))},O)}),--U||E(A)});return B.e&&O(B.v),V.promise},race:function(X){var J=this,V=z(J),E=V.reject,O=g(function(){m(X,!1,function(B){J.resolve(B).then(V.resolve,E)})});return O.e&&E(O.v),V.promise}})},function(l,i){l.exports=function(e,t,a,r){if(!(e instanceof t)||r!==void 0&&r in e)throw TypeError(a+": incorrect invocation!");return e}},function(l,m,e){var t=e(29),a=e(107),r=e(108),o=e(14),n=e(72),s=e(77),u={},d={},m=l.exports=function(f,_,c,v,g){var P=g?function(){return f}:s(f),h=t(c,v,_?2:1),y=0,W,T,j,b;if(typeof P!="function")throw TypeError(f+" is not iterable!");if(r(P)){for(W=n(f.length);W>y;y++)if(b=_?h(o(T=f[y])[0],T[1]):h(f[y]),b===u||b===d)return b}else for(j=P.call(f);!(T=j.next()).done;)if(b=a(j,h,T.value,_),b===u||b===d)return b};m.BREAK=u,m.RETURN=d},function(l,i){l.exports=function(e,t,a){var r=a===void 0;switch(t.length){case 0:return r?e():e.call(a);case 1:return r?e(t[0]):e.call(a,t[0]);case 2:return r?e(t[0],t[1]):e.call(a,t[0],t[1]);case 3:return r?e(t[0],t[1],t[2]):e.call(a,t[0],t[1],t[2]);case 4:return r?e(t[0],t[1],t[2],t[3]):e.call(a,t[0],t[1],t[2],t[3])}return e.apply(a,t)}},function(l,i,e){var t=e(8),a=e(110).set,r=t.MutationObserver||t.WebKitMutationObserver,o=t.process,n=t.Promise,s=e(43)(o)=="process";l.exports=function(){var u,d,m,f=function(){var g,P;for(s&&(g=o.domain)&&g.exit();u;){P=u.fn,u=u.next;try{P()}catch(h){throw u?m():d=void 0,h}}d=void 0,g&&g.enter()};if(s)m=function(){o.nextTick(f)};else if(r&&!(t.navigator&&t.navigator.standalone)){var _=!0,c=document.createTextNode("");new r(f).observe(c,{characterData:!0}),m=function(){c.data=_=!_}}else if(n&&n.resolve){var v=n.resolve(void 0);m=function(){v.then(f)}}else m=function(){a.call(t,f)};return function(g){var P={fn:g,next:void 0};d&&(d.next=P),u||(u=P,m()),d=P}}},function(l,i,e){var t=e(8),a=t.navigator;l.exports=a&&a.userAgent||""},function(l,i,e){var t=e(23);l.exports=function(a,r,o){for(var n in r)o&&a[n]?a[n]=r[n]:t(a,n,r[n]);return a}},function(l,i,e){var t=e(8),a=e(7),r=e(16),o=e(17),n=e(9)("species");l.exports=function(s){var u=typeof a[s]=="function"?a[s]:t[s];o&&u&&!u[n]&&r.f(u,n,{configurable:!0,get:function(){return this}})}},function(l,i,e){var t=e(11),a=e(7),r=e(8),o=e(109),n=e(112);t(t.P+t.R,"Promise",{finally:function(s){var u=o(this,a.Promise||r.Promise),d=typeof s=="function";return this.then(d?function(m){return n(u,s()).then(function(){return m})}:s,d?function(m){return n(u,s()).then(function(){throw m})}:s)}})},function(l,i,e){var t=e(11),a=e(78),r=e(111);t(t.S,"Promise",{try:function(o){var n=a.f(this),s=r(o);return(s.e?n.reject:n.resolve)(s.v),n.promise}})},function(l,i,e){l.exports={default:e(229),__esModule:!0}},function(l,i,e){e(230),l.exports=e(7).Object.assign},function(l,i,e){var t=e(11);t(t.S+t.F,"Object",{assign:e(231)})},function(l,i,e){var t=e(17),a=e(42),r=e(79),o=e(55),n=e(33),s=e(104),u=Object.assign;l.exports=!u||e(30)(function(){var d={},m={},f=Symbol(),_="abcdefghijklmnopqrst";return d[f]=7,_.split("").forEach(function(c){m[c]=c}),u({},d)[f]!=7||Object.keys(u({},m)).join("")!=_})?function(m,f){for(var _=n(m),c=arguments.length,v=1,g=r.f,P=o.f;c>v;)for(var h=s(arguments[v++]),y=g?a(h).concat(g(h)):a(h),W=y.length,T=0,j;W>T;)j=y[T++],(!t||P.call(h,j))&&(_[j]=h[j]);return _}:u},function(l,i,e){e(233),l.exports=e(7).Object.getPrototypeOf},function(l,i,e){var t=e(33),a=e(106);e(115)("getPrototypeOf",function(){return function(o){return a(t(o))}})},function(l,i,e){l.exports={default:e(235),__esModule:!0}},function(l,i,e){e(40),e(54),l.exports=e(80).f("iterator")},function(l,i,e){l.exports={default:e(237),__esModule:!0}},function(l,i,e){e(238),e(100),e(243),e(244),l.exports=e(7).Symbol},function(l,i,e){var t=e(8),a=e(24),r=e(17),o=e(11),n=e(102),s=e(239).KEY,u=e(30),d=e(74),m=e(53),f=e(52),_=e(9),c=e(80),v=e(81),g=e(240),P=e(241),h=e(14),y=e(20),W=e(33),T=e(32),j=e(67),b=e(39),C=e(71),I=e(242),M=e(119),D=e(79),p=e(16),w=e(42),L=M.f,z=p.f,ne=I.f,te=t.Symbol,de=t.JSON,ue=de&&de.stringify,oe="prototype",ie=_("_hidden"),le=_("toPrimitive"),F={}.propertyIsEnumerable,S=d("symbol-registry"),X=d("symbols"),J=d("op-symbols"),V=Object[oe],E=typeof te=="function"&&!!D.f,O=t.QObject,B=!O||!O[oe]||!O[oe].findChild,A=r&&u(function(){return C(z({},"a",{get:function(){return z(this,"a",{value:7}).a}})).a!=7})?function($,Q,ae){var me=L(V,Q);me&&delete V[Q],z($,Q,ae),me&&$!==V&&z(V,Q,me)}:z,k=function($){var Q=X[$]=C(te[oe]);return Q._k=$,Q},U=E&&typeof te.iterator=="symbol"?function($){return typeof $=="symbol"}:function($){return $ instanceof te},G=function(Q,ae,me){return Q===V&&G(J,ae,me),h(Q),ae=j(ae,!0),h(me),a(X,ae)?(me.enumerable?(a(Q,ie)&&Q[ie][ae]&&(Q[ie][ae]=!1),me=C(me,{enumerable:b(0,!1)})):(a(Q,ie)||z(Q,ie,b(1,{})),Q[ie][ae]=!0),A(Q,ae,me)):z(Q,ae,me)},ee=function(Q,ae){h(Q);for(var me=g(ae=T(ae)),ve=0,pe=me.length,K;pe>ve;)G(Q,K=me[ve++],ae[K]);return Q},q=function(Q,ae){return ae===void 0?C(Q):ee(C(Q),ae)},re=function(Q){var ae=F.call(this,Q=j(Q,!0));return this===V&&a(X,Q)&&!a(J,Q)?!1:ae||!a(this,Q)||!a(X,Q)||a(this,ie)&&this[ie][Q]?ae:!0},ce=function(Q,ae){if(Q=T(Q),ae=j(ae,!0),!(Q===V&&a(X,ae)&&!a(J,ae))){var me=L(Q,ae);return me&&a(X,ae)&&!(a(Q,ie)&&Q[ie][ae])&&(me.enumerable=!0),me}},he=function(Q){for(var ae=ne(T(Q)),me=[],ve=0,pe;ae.length>ve;)!a(X,pe=ae[ve++])&&pe!=ie&&pe!=s&&me.push(pe);return me},fe=function(Q){for(var ae=Q===V,me=ne(ae?J:T(Q)),ve=[],pe=0,K;me.length>pe;)a(X,K=me[pe++])&&(!ae||a(V,K))&&ve.push(X[K]);return ve};E||(te=function(){if(this instanceof te)throw TypeError("Symbol is not a constructor!");var Q=f(arguments.length>0?arguments[0]:void 0),ae=function(me){this===V&&ae.call(J,me),a(this,ie)&&a(this[ie],Q)&&(this[ie][Q]=!1),A(this,Q,b(1,me))};return r&&B&&A(V,Q,{configurable:!0,set:ae}),k(Q)},n(te[oe],"toString",function(){return this._k}),M.f=ce,p.f=G,e(118).f=I.f=he,e(55).f=re,D.f=fe,r&&!e(41)&&n(V,"propertyIsEnumerable",re,!0),c.f=function($){return k(_($))}),o(o.G+o.W+o.F*!E,{Symbol:te});for(var ge="hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables".split(","),Ee=0;ge.length>Ee;)_(ge[Ee++]);for(var x=w(_.store),N=0;x.length>N;)v(x[N++]);o(o.S+o.F*!E,"Symbol",{for:function($){return a(S,$+="")?S[$]:S[$]=te($)},keyFor:function(Q){if(!U(Q))throw TypeError(Q+" is not a symbol!");for(var ae in S)if(S[ae]===Q)return ae},useSetter:function(){B=!0},useSimple:function(){B=!1}}),o(o.S+o.F*!E,"Object",{create:q,defineProperty:G,defineProperties:ee,getOwnPropertyDescriptor:ce,getOwnPropertyNames:he,getOwnPropertySymbols:fe});var Y=u(function(){D.f(1)});o(o.S+o.F*Y,"Object",{getOwnPropertySymbols:function(Q){return D.f(W(Q))}}),de&&o(o.S+o.F*(!E||u(function(){var $=te();return ue([$])!="[null]"||ue({a:$})!="{}"||ue(Object($))!="{}"})),"JSON",{stringify:function(Q){for(var ae=[Q],me=1,ve,pe;arguments.length>me;)ae.push(arguments[me++]);if(pe=ve=ae[1],!(!y(ve)&&Q===void 0||U(Q)))return P(ve)||(ve=function(K,Z){if(typeof pe=="function"&&(Z=pe.call(this,K,Z)),!U(Z))return Z}),ae[1]=ve,ue.apply(de,ae)}}),te[oe][le]||e(23)(te[oe],le,te[oe].valueOf),m(te,"Symbol"),m(Math,"Math",!0),m(t.JSON,"JSON",!0)},function(l,i,e){var t=e(52)("meta"),a=e(20),r=e(24),o=e(16).f,n=0,s=Object.isExtensible||function(){return!0},u=!e(30)(function(){return s(Object.preventExtensions({}))}),d=function(v){o(v,t,{value:{i:"O"+ ++n,w:{}}})},m=function(v,g){if(!a(v))return typeof v=="symbol"?v:(typeof v=="string"?"S":"P")+v;if(!r(v,t)){if(!s(v))return"F";if(!g)return"E";d(v)}return v[t].i},f=function(v,g){if(!r(v,t)){if(!s(v))return!0;if(!g)return!1;d(v)}return v[t].w},_=function(v){return u&&c.NEED&&s(v)&&!r(v,t)&&d(v),v},c=l.exports={KEY:t,NEED:!1,fastKey:m,getWeak:f,onFreeze:_}},function(l,i,e){var t=e(42),a=e(79),r=e(55);l.exports=function(o){var n=t(o),s=a.f;if(s)for(var u=s(o),d=r.f,m=0,f;u.length>m;)d.call(o,f=u[m++])&&n.push(f);return n}},function(l,i,e){var t=e(43);l.exports=Array.isArray||function(r){return t(r)=="Array"}},function(l,i,e){var t=e(32),a=e(118).f,r={}.toString,o=typeof window=="object"&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[],n=function(s){try{return a(s)}catch{return o.slice()}};l.exports.f=function(u){return o&&r.call(u)=="[object Window]"?n(u):a(t(u))}},function(l,i,e){e(81)("asyncIterator")},function(l,i,e){e(81)("observable")},function(l,i,e){l.exports={default:e(246),__esModule:!0}},function(l,i,e){e(247),l.exports=e(7).Object.setPrototypeOf},function(l,i,e){var t=e(11);t(t.S,"Object",{setPrototypeOf:e(248).set})},function(l,i,e){var t=e(20),a=e(14),r=function(o,n){if(a(o),!t(n)&&n!==null)throw TypeError(n+": can't set as prototype!")};l.exports={set:Object.setPrototypeOf||("__proto__"in{}?function(o,n,s){try{s=e(29)(Function.call,e(119).f(Object.prototype,"__proto__").set,2),s(o,[]),n=!(o instanceof Array)}catch{n=!0}return function(d,m){return r(d,m),n?d.__proto__=m:s(d,m),d}}({},!1):void 0),check:r}},function(l,i,e){l.exports={default:e(250),__esModule:!0}},function(l,i,e){e(251);var t=e(7).Object;l.exports=function(r,o){return t.create(r,o)}},function(l,i,e){var t=e(11);t(t.S,"Object",{create:e(71)})},function(l,i,e){Object.defineProperty(i,"__esModule",{value:!0});var t=e(253),a=r(t);function r(o){return o&&o.__esModule?o:{default:o}}i.default=a.default,l.exports=i.default},function(l,i,e){Object.defineProperty(i,"__esModule",{value:!0});var t=function(){function W(T,j){var b=[],C=!0,I=!1,M=void 0;try{for(var D=T[Symbol.iterator](),p;!(C=(p=D.next()).done)&&(b.push(p.value),!(j&&b.length===j));C=!0);}catch(w){I=!0,M=w}finally{try{!C&&D.return&&D.return()}finally{if(I)throw M}}return b}return function(T,j){if(Array.isArray(T))return T;if(Symbol.iterator in Object(T))return W(T,j);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}(),a=Object.assign||function(W){for(var T=1;T>>0,o=arguments[1],n=void 0,s=0;s>>0;if(typeof t!="function")throw new TypeError;for(arguments.length>1&&(r=a),o=0;o"u"});M||(0,u.statelessFunctionalComponentSupplied)()}}},{key:"convertProps",value:function(C){var I={children:C.children,easing:C.easing,onStart:C.onStart,onFinish:C.onFinish,onStartAll:C.onStartAll,onFinishAll:C.onFinishAll,typeName:C.typeName,disableAllAnimations:C.disableAllAnimations,getPosition:C.getPosition,maintainContainerHeight:C.maintainContainerHeight,verticalAlignment:C.verticalAlignment,duration:this.convertTimingProp("duration"),delay:this.convertTimingProp("delay"),staggerDurationBy:this.convertTimingProp("staggerDurationBy"),staggerDelayBy:this.convertTimingProp("staggerDelayBy"),appearAnimation:this.convertAnimationProp(C.appearAnimation,d.appearPresets),enterAnimation:this.convertAnimationProp(C.enterAnimation,d.enterPresets),leaveAnimation:this.convertAnimationProp(C.leaveAnimation,d.leavePresets),delegated:{}};this.checkForStatelessFunctionalComponents(I.children),typeof C.disableAnimations<"u"&&(g!=="production"&&(0,u.deprecatedDisableAnimations)(),I.disableAllAnimations=C.disableAnimations);var M=Object.keys(I),D=(0,m.omit)(this.props,M);return D.style=r({position:"relative"},D.style),I.delegated=D,I}},{key:"convertTimingProp",value:function(C){var I=this.props[C],M=typeof I=="number"?I:parseInt(I,10);if(isNaN(M)){var D=j.defaultProps[C];return g!=="production"&&(0,u.invalidTypeForTimingProp)({prop:C,value:I,defaultValue:D}),D}return M}},{key:"convertAnimationProp",value:function(C,I){switch(typeof C>"u"?"undefined":a(C)){case"boolean":return I[C?d.defaultPreset:d.disablePreset];case"string":{var M=Object.keys(I);return M.indexOf(C)===-1?(g!=="production"&&(0,u.invalidEnterLeavePreset)({value:C,acceptableValues:M.join(", "),defaultValue:d.defaultPreset}),I[d.defaultPreset]):I[C]}default:return C}}},{key:"render",value:function(){return s.default.createElement(h,this.convertProps(this.props))}}]),j}(n.Component),y.defaultProps={easing:"ease-in-out",duration:350,delay:0,staggerDurationBy:0,staggerDelayBy:0,typeName:"div",enterAnimation:d.defaultPreset,leaveAnimation:d.defaultPreset,disableAllAnimations:!1,getPosition:function(j){return j.getBoundingClientRect()},maintainContainerHeight:!1,verticalAlignment:"top"},W}i.default=P,l.exports=i.default}).call(i,e(18))},function(l,i,e){Object.defineProperty(i,"__esModule",{value:!0});function t(a){var r=!1;return function(){r||(console.warn(a),r=!0)}}i.statelessFunctionalComponentSupplied=t(` +>> Error, via react-flip-move << + +You provided a stateless functional component as a child to . Unfortunately, SFCs aren't supported, because Flip Move needs access to the backing instances via refs, and SFCs don't have a public instance that holds that info. + +Please wrap your components in a native element (eg.
), or a non-functional component. +`),i.invalidTypeForTimingProp=function(r){return console.error(` +>> Error, via react-flip-move << + +The prop you provided for '`+r.prop+"' is invalid. It needs to be a positive integer, or a string that can be resolved to a number. The value you provided is '"+r.value+`'. + +As a result, the default value for this parameter will be used, which is '`+r.defaultValue+`'. +`)},i.deprecatedDisableAnimations=t(` +>> Warning, via react-flip-move << + +The 'disableAnimations' prop you provided is deprecated. Please switch to use 'disableAllAnimations'. + +This will become a silent error in future versions of react-flip-move. +`),i.invalidEnterLeavePreset=function(r){return console.error(` +>> Error, via react-flip-move << + +The enter/leave preset you provided is invalid. We don't currently have a '`+r.value+` preset.' + +Acceptable values are `+r.acceptableValues+". The default value of '"+r.defaultValue+`' will be used. +`)}},function(l,i,e){Object.defineProperty(i,"__esModule",{value:!0});var t=i.enterPresets={elevator:{from:{transform:"scale(0)",opacity:"0"},to:{transform:"",opacity:""}},fade:{from:{opacity:"0"},to:{opacity:""}},accordionVertical:{from:{transform:"scaleY(0)",transformOrigin:"center top"},to:{transform:"",transformOrigin:"center top"}},accordionHorizontal:{from:{transform:"scaleX(0)",transformOrigin:"left center"},to:{transform:"",transformOrigin:"left center"}},none:null},a=i.leavePresets={elevator:{from:{transform:"scale(1)",opacity:"1"},to:{transform:"scale(0)",opacity:"0"}},fade:{from:{opacity:"1"},to:{opacity:"0"}},accordionVertical:{from:{transform:"scaleY(1)",transformOrigin:"center top"},to:{transform:"scaleY(0)",transformOrigin:"center top"}},accordionHorizontal:{from:{transform:"scaleX(1)",transformOrigin:"left center"},to:{transform:"scaleX(0)",transformOrigin:"left center"}},none:null};i.appearPresets=t,t.accordianVertical=t.accordionVertical,t.accordianHorizontal=t.accordionHorizontal,a.accordianVertical=a.accordionVertical,a.accordianHorizontal=a.accordionHorizontal,i.defaultPreset="elevator",i.disablePreset="none"},function(l,i,e){Object.defineProperty(i,"__esModule",{value:!0}),i.createTransitionString=i.getNativeNode=i.updateHeightPlaceholder=i.removeNodeFromDOMFlow=i.getPositionDelta=i.getRelativeBoundingBox=void 0;var t=Object.assign||function(u){for(var d=1;d"u")return"";var d=document.createElement("fakeelement"),m=Object.keys(u).find(function(f){return d.style.getPropertyValue(f)!==void 0});return m?u[m]:""}i.getRelativeBoundingBox=function(d){var m=d.childDomNode,f=d.parentDomNode,_=d.getPosition,c=_(f),v=_(m),g=v.top,P=v.left,h=v.right,y=v.bottom,W=v.width,T=v.height;return{top:g-c.top,left:P-c.left,right:c.right-h,bottom:c.bottom-y,width:W,height:T}},i.getPositionDelta=function(d){var m=d.childDomNode,f=d.childBoundingBox,_=d.parentBoundingBox,c=d.getPosition,v={top:0,left:0,right:0,bottom:0,height:0,width:0},g=f||v,P=_||v,h=c(m),y={top:h.top-P.top,left:h.left-P.left};return[g.left-y.left,g.top-y.top]},i.removeNodeFromDOMFlow=function(d,m){var f=d.domNode,_=d.boundingBox;if(!(!f||!_)){var c=window.getComputedStyle(f),v=["margin-top","margin-left","margin-right"],g=v.reduce(function(y,W){var T=c.getPropertyValue(W);return t({},y,o({},W,Number(T.replace("px",""))))},{}),P=m==="bottom"?_.top-_.height:_.top,h={position:"absolute",top:P-g["margin-top"]+"px",left:_.left-g["margin-left"]+"px",right:_.right-g["margin-right"]+"px"};n({domNode:f,styles:h})}},i.updateHeightPlaceholder=function(d){var m=d.domNode,f=d.parentData,_=d.getPosition,c=f.domNode,v=f.boundingBox;if(!(!c||!v)){n({domNode:m,styles:{height:"0"}});var g=v.height,P=_(c).height,h=g-P,y={height:h>0?h+"px":"0"};n({domNode:m,styles:y})}},i.getNativeNode=function(d){if(typeof HTMLElement>"u")return null;if(d instanceof HTMLElement)return d;var m=(0,a.findDOMNode)(d);return m instanceof HTMLElement?m:null},i.createTransitionString=function(d,m){var f=m.delay,_=m.duration,c=m.staggerDurationBy,v=m.staggerDelayBy,g=m.easing;f+=d*v,_+=d*c;var P=["transform","opacity"];return P.map(function(h){return h+" "+_+"ms "+g+" "+f+"ms"}).join(", ")}},function(l,i,e){var t,a,r;/*! + Autosize 3.0.21 + license: MIT + http://www.jacklmoore.com/autosize +*/(function(o,n){a=[i,l],t=n,r=typeof t=="function"?t.apply(i,a):t,r!==void 0&&(l.exports=r)})(this,function(o,n){var s=typeof Map=="function"?new Map:function(){var c=[],v=[];return{has:function(P){return c.indexOf(P)>-1},get:function(P){return v[c.indexOf(P)]},set:function(P,h){c.indexOf(P)===-1&&(c.push(P),v.push(h))},delete:function(P){var h=c.indexOf(P);h>-1&&(c.splice(h,1),v.splice(h,1))}}}(),u=function(v){return new Event(v,{bubbles:!0})};try{new Event("test")}catch{u=function(v){var g=document.createEvent("Event");return g.initEvent(v,!0,!1),g}}function d(c){if(!c||!c.nodeName||c.nodeName!=="TEXTAREA"||s.has(c))return;var v=null,g=c.clientWidth,P=null;function h(){var I=window.getComputedStyle(c,null);I.resize==="vertical"?c.style.resize="none":I.resize==="both"&&(c.style.resize="horizontal"),I.boxSizing==="content-box"?v=-(parseFloat(I.paddingTop)+parseFloat(I.paddingBottom)):v=parseFloat(I.borderTopWidth)+parseFloat(I.borderBottomWidth),isNaN(v)&&(v=0),j()}function y(I){{var M=c.style.width;c.style.width="0px",c.offsetWidth,c.style.width=M}c.style.overflowY=I}function W(I){for(var M=[];I&&I.parentNode&&I.parentNode instanceof Element;)I.parentNode.scrollTop&&M.push({node:I.parentNode,scrollTop:I.parentNode.scrollTop}),I=I.parentNode;return M}function T(){var I=c.style.height,M=W(c),D=document.documentElement&&document.documentElement.scrollTop;c.style.height="auto";var p=c.scrollHeight+v;if(c.scrollHeight===0){c.style.height=I;return}c.style.height=p+"px",g=c.clientWidth,M.forEach(function(w){w.node.scrollTop=w.scrollTop}),D&&(document.documentElement.scrollTop=D)}function j(){T();var I=Math.round(parseFloat(c.style.height)),M=window.getComputedStyle(c,null),D=M.boxSizing==="content-box"?Math.round(parseFloat(M.height)):c.offsetHeight;if(D!==I?M.overflowY==="hidden"&&(y("scroll"),T(),D=M.boxSizing==="content-box"?Math.round(parseFloat(window.getComputedStyle(c,null).height)):c.offsetHeight):M.overflowY!=="hidden"&&(y("hidden"),T(),D=M.boxSizing==="content-box"?Math.round(parseFloat(window.getComputedStyle(c,null).height)):c.offsetHeight),P!==D){P=D;var p=u("autosize:resized");try{c.dispatchEvent(p)}catch{}}}var b=function(){c.clientWidth!==g&&j()},C=(function(I){window.removeEventListener("resize",b,!1),c.removeEventListener("input",j,!1),c.removeEventListener("keyup",j,!1),c.removeEventListener("autosize:destroy",C,!1),c.removeEventListener("autosize:update",j,!1),Object.keys(I).forEach(function(M){c.style[M]=I[M]}),s.delete(c)}).bind(c,{height:c.style.height,resize:c.style.resize,overflowY:c.style.overflowY,overflowX:c.style.overflowX,wordWrap:c.style.wordWrap});c.addEventListener("autosize:destroy",C,!1),"onpropertychange"in c&&"oninput"in c&&c.addEventListener("keyup",j,!1),window.addEventListener("resize",b,!1),c.addEventListener("input",j,!1),c.addEventListener("autosize:update",j,!1),c.style.overflowX="hidden",c.style.wordWrap="break-word",s.set(c,{destroy:C,update:j}),h()}function m(c){var v=s.get(c);v&&v.destroy()}function f(c){var v=s.get(c);v&&v.update()}var _=null;typeof window>"u"||typeof window.getComputedStyle!="function"?(_=function(c){return c},_.destroy=function(c){return c},_.update=function(c){return c}):(_=function(c,v){return c&&Array.prototype.forEach.call(c.length?c:[c],function(g){return d(g)}),c},_.destroy=function(c){return c&&Array.prototype.forEach.call(c.length?c:[c],m),c},_.update=function(c){return c&&Array.prototype.forEach.call(c.length?c:[c],f),c}),n.exports=_})},function(l,i,e){Object.defineProperty(i,"__esModule",{value:!0}),i.default=function(w){return new a.default({phrases:p[w]||p.en,locale:w})};var t=e(261),a=D(t),r=e(262),o=D(r),n=e(263),s=D(n),u=e(264),d=D(u),m=e(265),f=D(m),_=e(266),c=D(_),v=e(267),g=D(v),P=e(268),h=D(P),y=e(269),W=D(y),T=e(270),j=D(T),b=e(271),C=D(b),I=e(272),M=D(I);function D(w){return w&&w.__esModule?w:{default:w}}var p={zh:o.default,"zh-CN":o.default,"zh-TW":s.default,en:d.default,"es-ES":f.default,fr:c.default,ru:g.default,de:h.default,pl:W.default,ko:j.default,fa:C.default,ja:M.default}},function(l,i,e){var t,a;(function(r,o){t=[],a=(function(){return o(r)}).apply(i,t),a!==void 0&&(l.exports=a)})(this,function(r){function o(h){h=h||{},this.phrases={},this.extend(h.phrases||{}),this.currentLocale=h.locale||"en",this.allowMissing=!!h.allowMissing,this.warn=h.warn||g}o.VERSION="0.4.3",o.prototype.locale=function(h){return h&&(this.currentLocale=h),this.currentLocale},o.prototype.extend=function(h,y){var W;for(var T in h)h.hasOwnProperty(T)&&(W=h[T],y&&(T=y+"."+T),typeof W=="object"?this.extend(W,T):this.phrases[T]=W)},o.prototype.clear=function(){this.phrases={}},o.prototype.replace=function(h){this.clear(),this.extend(h)},o.prototype.t=function(h,y){var W,T;return y=y??{},typeof y=="number"&&(y={smart_count:y}),typeof this.phrases[h]=="string"?W=this.phrases[h]:typeof y._=="string"?W=y._:this.allowMissing?W=h:(this.warn('Missing translation for key: "'+h+'"'),T=h),typeof W=="string"&&(y=P(y),T=f(W,this.currentLocale,y.smart_count),T=v(T,y)),T},o.prototype.has=function(h){return h in this.phrases};var n="||||",s={chinese:function(h){return 0},german:function(h){return h!==1?1:0},french:function(h){return h>1?1:0},russian:function(h){return h%10===1&&h%100!==11?0:h%10>=2&&h%10<=4&&(h%100<10||h%100>=20)?1:2},czech:function(h){return h===1?0:h>=2&&h<=4?1:2},polish:function(h){return h===1?0:h%10>=2&&h%10<=4&&(h%100<10||h%100>=20)?1:2},icelandic:function(h){return h%10!==1||h%100===11?1:0}},u={chinese:["fa","id","ja","ko","lo","ms","th","tr","zh"],german:["da","de","en","es","fi","el","he","hu","it","nl","no","pt","sv"],french:["fr","tl","pt-br"],russian:["hr","ru"],czech:["cs"],polish:["pl"],icelandic:["is"]};function d(h){var y,W,T,j={};for(y in h)if(h.hasOwnProperty(y)){W=h[y];for(T in W)j[W[T]]=y}return j}function m(h){var y=/^\s+|\s+$/g;return h.replace(y,"")}function f(h,y,W){var T,j,b;return W!=null&&h?(j=h.split(n),b=j[c(y,W)]||j[0],T=m(b)):T=h,T}function _(h){var y=d(u);return y[h]||y.en}function c(h,y){return s[_(h)](y)}function v(h,y){for(var W in y)W!=="_"&&y.hasOwnProperty(W)&&(h=h.replace(new RegExp("%\\{"+W+"\\}","g"),y[W]));return h}function g(h){r.console&&r.console.warn&&r.console.warn("WARNING: "+h)}function P(h){var y={};for(var W in h)y[W]=h[W];return y}return o})},function(l,i){l.exports={init:"Gitalk 加载中 ...","no-found-related":"未找到相关的 %{link} 进行评论","please-contact":"请联系 %{user} 初始化创建","init-issue":"初始化 Issue","leave-a-comment":"说点什么",preview:"预览",edit:"编辑",comment:"评论","support-markdown":"支持 Markdown 语法","login-with-github":"使用 GitHub 登录","first-comment-person":"来做第一个留言的人吧!",commented:"发表于","load-more":"加载更多",counts:"%{counts} 条评论","sort-asc":"从旧到新排序","sort-desc":"从新到旧排序",logout:"注销",anonymous:"未登录用户"}},function(l,i){l.exports={init:"Gitalk 載入中…","no-found-related":"未找到相關的 %{link}","please-contact":"請聯絡 %{user} 初始化評論","init-issue":"初始化 Issue","leave-a-comment":"寫點什麼",preview:"預覽",edit:"編輯",comment:"評論","support-markdown":"支援 Markdown 語法","login-with-github":"使用 GitHub 登入","first-comment-person":"成為首個留言的人吧!",commented:"評論於","load-more":"載入更多",counts:"%{counts} 筆評論","sort-asc":"從舊至新排序","sort-desc":"從新至舊排序",logout:"登出",anonymous:"訪客"}},function(l,i){l.exports={init:"Gitalking ...","no-found-related":"Related %{link} not found","please-contact":"Please contact %{user} to initialize the comment","init-issue":"Init Issue","leave-a-comment":"Leave a comment",preview:"Preview",edit:"Edit",comment:"Comment","support-markdown":"Markdown is supported","login-with-github":"Login with GitHub","first-comment-person":"Be the first person to leave a comment!",commented:"commented","load-more":"Load more",counts:"%{counts} comment |||| %{counts} comments","sort-asc":"Sort by Oldest","sort-desc":"Sort by Latest",logout:"Logout",anonymous:"Anonymous"}},function(l,i){l.exports={init:"Gitalking ...","no-found-related":"Link %{link} no encontrado","please-contact":"Por favor contacta con %{user} para inicializar el comentario","init-issue":"Iniciar Issue","leave-a-comment":"Deja un comentario",preview:"Avance",edit:"Editar",comment:"Comentario","support-markdown":"Markdown es soportado","login-with-github":"Entrar con GitHub","first-comment-person":"Sé el primero en dejar un comentario!",commented:"comentó","load-more":"Cargar más",counts:"%{counts} comentario |||| %{counts} comentarios","sort-asc":"Ordenar por Antiguos","sort-desc":"Ordenar por Recientes",logout:"Salir",anonymous:"Anónimo"}},function(l,i){l.exports={init:"Gitalking ...","no-found-related":"Lien %{link} non trouvé","please-contact":"S’il vous plaît contactez %{user} pour initialiser les commentaires","init-issue":"Initialisation des issues","leave-a-comment":"Laisser un commentaire",preview:"Aperçu",edit:"Modifier",comment:"Commentaire","support-markdown":"Markdown est supporté","login-with-github":"Se connecter avec GitHub","first-comment-person":"Être le premier à laisser un commentaire !",commented:"commenter","load-more":"Charger plus",counts:"%{counts} commentaire |||| %{counts} commentaires","sort-asc":"Trier par plus ancien","sort-desc":"Trier par plus récent",logout:"Déconnexion",anonymous:"Anonyme"}},function(l,i){l.exports={init:"Gitalking ...","no-found-related":"Связанные %{link} не найдены","please-contact":"Пожалуйста, свяжитесь с %{user} чтобы инициализировать комментарий","init-issue":"Выпуск инициализации","leave-a-comment":"Оставить комментарий",preview:"Предварительный просмотр",edit:"Pедактировать",comment:"Комментарий","support-markdown":"Поддерживается Markdown","login-with-github":"Вход через GitHub","first-comment-person":"Будьте первым, кто оставил комментарий",commented:"прокомментированный","load-more":"Загрузить ещё",counts:"%{counts} комментарий |||| %{counts} комментариев","sort-asc":"Сортировать по старым","sort-desc":"Сортировать по последним",logout:"Выход",anonymous:"Анонимный"}},function(l,i){l.exports={init:"Gitalking ...","no-found-related":"Zugehöriger %{link} nicht gefunden","please-contact":"Bitte kontaktiere %{user} um den Kommentar zu initialisieren","init-issue":"Initialisiere Issue","leave-a-comment":"Hinterlasse einen Kommentar",preview:"Vorschau",edit:"Editieren",comment:"Kommentieren","support-markdown":"Markdown wird unterstützt","login-with-github":"Mit GitHub-Account anmelden","first-comment-person":"Sei die erste Person, welche einen Kommentar hinterlässt!",commented:"kommentierte","load-more":"Zeige mehr",counts:"%{counts} Kommentar |||| %{counts} Kommentare","sort-asc":"Älteste zuerst","sort-desc":"Neuste zuerst",logout:"Abmelden",anonymous:"Anonym"}},function(l,i){l.exports={init:"Gitalking ...","no-found-related":"Nie znaleziono powiązanego zgłoszenia: %{link}","please-contact":"Skontaktuj się z %{user}, aby umożliwić komentowanie","init-issue":"Utwórz zgłoszenie (GitHub Issue)","leave-a-comment":"Skomentuj",preview:"Podgląd",edit:"Edytuj",comment:"Wyślij","support-markdown":"Możesz użyć składni Markdown","login-with-github":"Zaloguj się poprzez GitHub","first-comment-person":"Skomentuj jako pierwszy!",commented:"skomentowany","load-more":"Załaduj więcej",counts:"%{counts} komentarz |||| %{counts} komentarze |||| %{counts} komentarzy","sort-asc":"Sortuj od najstarszych","sort-desc":"Sortuj od najnowszych",logout:"Wyloguj",anonymous:"Anonimowy"}},function(l,i){l.exports={init:"초기화 중 ...","no-found-related":"관련 링크를 찾을 수 없습니다: %{link} ","please-contact":"초기화를 위해 %{user} 에게 연락해 주세요","init-issue":"이슈 초기화","leave-a-comment":"댓글을 남겨보세요",preview:"미리보기",edit:"수정하기",comment:"댓글 달기","support-markdown":"마크다운(Markdown) 문법 지원","login-with-github":"GitHub로 로그인하기","first-comment-person":"첫 번째로 댓글을 남겨보세요!",commented:"님이 작성함","load-more":"더 보기",counts:"댓글 %{counts} 개","sort-asc":"오래된 댓글 먼저","sort-desc":"최신 댓글 먼저",logout:"로그아웃",anonymous:"익명"}},function(l,i){l.exports={init:"بارگزاری ...","no-found-related":"%{link} مرتبط پیدا نشد.","please-contact":"لطفاً با %{user} اطلاع دهید تا اضافه کند.","init-issue":"شروع نظرات","leave-a-comment":"نظرتان را بنویسید",preview:"پیش‌نمایش",edit:"ویرایش",comment:"ثبت","support-markdown":"پشتیبانی از مارک‌داون","login-with-github":"ورود با گیت‌هاب","first-comment-person":"اولین نظر را شما بنویسید!",commented:" - ","load-more":"نظرات بیشتر",counts:"%{counts} نظر |||| %{counts} نظر","sort-asc":"مرتب‌سازی از قدیمی‌ترین","sort-desc":"مرتب‌سازی از جدیدترین",logout:"خروج",anonymous:"بی‌نام"}},function(l,i){l.exports={init:"Gitalk を読み込んでいます...","no-found-related":"%{link} に関連するものが見当たりません","please-contact":"コメントを初期化するために %{user} に連絡してください","init-issue":"イシューを初期化する","leave-a-comment":"コメントを残す",preview:"プレビュー",edit:"編集する",comment:"コメント","support-markdown":"Markdown 記法をサポートしています","login-with-github":"GitHub にログインする","first-comment-person":"最初にコメントを残しましょう!",commented:"によるコメント","load-more":"もっと見る",counts:"%{counts} つのコメント","sort-asc":"投稿順に並び替え","sort-desc":"最新順で並び替え",logout:"ログアウト",anonymous:"匿名"}},function(l,i){},function(l,i,e){l.exports={default:e(275),__esModule:!0}},function(l,i,e){e(276),l.exports=e(7).Object.keys},function(l,i,e){var t=e(33),a=e(42);e(115)("keys",function(){return function(o){return a(t(o))}})},function(l,i,e){i.__esModule=!0;var t=e(278),a=n(t),r=e(281),o=n(r);function n(s){return s&&s.__esModule?s:{default:s}}i.default=function(){function s(u,d){var m=[],f=!0,_=!1,c=void 0;try{for(var v=(0,o.default)(u),g;!(f=(g=v.next()).done)&&(m.push(g.value),!(d&&m.length===d));f=!0);}catch(P){_=!0,c=P}finally{try{!f&&v.return&&v.return()}finally{if(_)throw c}}return m}return function(u,d){if(Array.isArray(u))return u;if((0,a.default)(Object(u)))return s(u,d);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}()},function(l,i,e){l.exports={default:e(279),__esModule:!0}},function(l,i,e){e(54),e(40),l.exports=e(280)},function(l,i,e){var t=e(76),a=e(9)("iterator"),r=e(31);l.exports=e(7).isIterable=function(o){var n=Object(o);return n[a]!==void 0||"@@iterator"in n||r.hasOwnProperty(t(n))}},function(l,i,e){l.exports={default:e(282),__esModule:!0}},function(l,i,e){e(54),e(40),l.exports=e(283)},function(l,i,e){var t=e(14),a=e(77);l.exports=e(7).getIterator=function(r){var o=a(r);if(typeof o!="function")throw TypeError(r+" is not iterable!");return t(o.call(r))}},function(l,i,e){l.exports=e(285)},function(l,i,e){var t=e(10),a=e(122),r=e(286),o=e(128),n=e(125);function s(d){var m=new r(d),f=a(r.prototype.request,m);return t.extend(f,r.prototype,m),t.extend(f,m),f}var u=s(n);u.Axios=r,u.create=function(m){return s(o(u.defaults,m))},u.Cancel=e(129),u.CancelToken=e(299),u.isCancel=e(124),u.all=function(m){return Promise.all(m)},u.spread=e(300),l.exports=u,l.exports.default=u},function(l,i,e){var t=e(10),a=e(123),r=e(287),o=e(288),n=e(128);function s(u){this.defaults=u,this.interceptors={request:new r,response:new r}}s.prototype.request=function(d){typeof d=="string"?(d=arguments[1]||{},d.url=arguments[0]):d=d||{},d=n(this.defaults,d),d.method?d.method=d.method.toLowerCase():this.defaults.method?d.method=this.defaults.method.toLowerCase():d.method="get";var m=[o,void 0],f=Promise.resolve(d);for(this.interceptors.request.forEach(function(c){m.unshift(c.fulfilled,c.rejected)}),this.interceptors.response.forEach(function(c){m.push(c.fulfilled,c.rejected)});m.length;)f=f.then(m.shift(),m.shift());return f},s.prototype.getUri=function(d){return d=n(this.defaults,d),a(d.url,d.params,d.paramsSerializer).replace(/^\?/,"")},t.forEach(["delete","get","head","options"],function(d){s.prototype[d]=function(m,f){return this.request(t.merge(f||{},{method:d,url:m}))}}),t.forEach(["post","put","patch"],function(d){s.prototype[d]=function(m,f,_){return this.request(t.merge(_||{},{method:d,url:m,data:f}))}}),l.exports=s},function(l,i,e){var t=e(10);function a(){this.handlers=[]}a.prototype.use=function(o,n){return this.handlers.push({fulfilled:o,rejected:n}),this.handlers.length-1},a.prototype.eject=function(o){this.handlers[o]&&(this.handlers[o]=null)},a.prototype.forEach=function(o){t.forEach(this.handlers,function(s){s!==null&&o(s)})},l.exports=a},function(l,i,e){var t=e(10),a=e(289),r=e(124),o=e(125);function n(s){s.cancelToken&&s.cancelToken.throwIfRequested()}l.exports=function(u){n(u),u.headers=u.headers||{},u.data=a(u.data,u.headers,u.transformRequest),u.headers=t.merge(u.headers.common||{},u.headers[u.method]||{},u.headers),t.forEach(["delete","get","head","post","put","patch","common"],function(f){delete u.headers[f]});var d=u.adapter||o.adapter;return d(u).then(function(f){return n(u),f.data=a(f.data,f.headers,u.transformResponse),f},function(f){return r(f)||(n(u),f&&f.response&&(f.response.data=a(f.response.data,f.response.headers,u.transformResponse))),Promise.reject(f)})}},function(l,i,e){var t=e(10);l.exports=function(r,o,n){return t.forEach(n,function(u){r=u(r,o)}),r}},function(l,i,e){var t=e(10);l.exports=function(r,o){t.forEach(r,function(s,u){u!==o&&u.toUpperCase()===o.toUpperCase()&&(r[o]=s,delete r[u])})}},function(l,i,e){var t=e(127);l.exports=function(r,o,n){var s=n.config.validateStatus;!s||s(n.status)?r(n):o(t("Request failed with status code "+n.status,n.config,null,n.request,n))}},function(l,i,e){l.exports=function(a,r,o,n,s){return a.config=r,o&&(a.code=o),a.request=n,a.response=s,a.isAxiosError=!0,a.toJSON=function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:this.config,code:this.code}},a}},function(l,i,e){var t=e(294),a=e(295);l.exports=function(o,n){return o&&!t(n)?a(o,n):n}},function(l,i,e){l.exports=function(a){return/^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(a)}},function(l,i,e){l.exports=function(a,r){return r?a.replace(/\/+$/,"")+"/"+r.replace(/^\/+/,""):a}},function(l,i,e){var t=e(10),a=["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"];l.exports=function(o){var n={},s,u,d;return o&&t.forEach(o.split(` +`),function(f){if(d=f.indexOf(":"),s=t.trim(f.substr(0,d)).toLowerCase(),u=t.trim(f.substr(d+1)),s){if(n[s]&&a.indexOf(s)>=0)return;s==="set-cookie"?n[s]=(n[s]?n[s]:[]).concat([u]):n[s]=n[s]?n[s]+", "+u:u}}),n}},function(l,i,e){var t=e(10);l.exports=t.isStandardBrowserEnv()?function(){var r=/(msie|trident)/i.test(navigator.userAgent),o=document.createElement("a"),n;function s(u){var d=u;return r&&(o.setAttribute("href",d),d=o.href),o.setAttribute("href",d),{href:o.href,protocol:o.protocol?o.protocol.replace(/:$/,""):"",host:o.host,search:o.search?o.search.replace(/^\?/,""):"",hash:o.hash?o.hash.replace(/^#/,""):"",hostname:o.hostname,port:o.port,pathname:o.pathname.charAt(0)==="/"?o.pathname:"/"+o.pathname}}return n=s(window.location.href),function(d){var m=t.isString(d)?s(d):d;return m.protocol===n.protocol&&m.host===n.host}}():function(){return function(){return!0}}()},function(l,i,e){var t=e(10);l.exports=t.isStandardBrowserEnv()?function(){return{write:function(o,n,s,u,d,m){var f=[];f.push(o+"="+encodeURIComponent(n)),t.isNumber(s)&&f.push("expires="+new Date(s).toGMTString()),t.isString(u)&&f.push("path="+u),t.isString(d)&&f.push("domain="+d),m===!0&&f.push("secure"),document.cookie=f.join("; ")},read:function(o){var n=document.cookie.match(new RegExp("(^|;\\s*)("+o+")=([^;]*)"));return n?decodeURIComponent(n[3]):null},remove:function(o){this.write(o,"",Date.now()-864e5)}}}():function(){return{write:function(){},read:function(){return null},remove:function(){}}}()},function(l,i,e){var t=e(129);function a(r){if(typeof r!="function")throw new TypeError("executor must be a function.");var o;this.promise=new Promise(function(u){o=u});var n=this;r(function(u){n.reason||(n.reason=new t(u),o(n.reason))})}a.prototype.throwIfRequested=function(){if(this.reason)throw this.reason},a.source=function(){var o,n=new a(function(u){o=u});return{token:n,cancel:o}},l.exports=a},function(l,i,e){l.exports=function(a){return function(o){return a.apply(null,o)}}},function(l,i,e){Object.defineProperty(i,"__esModule",{value:!0});var t=e(12),a=r(t);function r(o){return o&&o.__esModule?o:{default:o}}i.default=function(o){var n=o.className,s=o.getRef,u=o.onClick,d=o.onMouseDown,m=o.text,f=o.isLoading;return a.default.createElement("button",{ref:function(c){return s&&s(c)},className:"gt-btn "+n,onClick:u,onMouseDown:d},a.default.createElement("span",{className:"gt-btn-text"},m),f&&a.default.createElement("span",{className:"gt-btn-loading gt-spinner"}))}},function(l,i,e){Object.defineProperty(i,"__esModule",{value:!0});var t=e(12),a=r(t);function r(o){return o&&o.__esModule?o:{default:o}}i.default=function(o){var n=o.className,s=o.onClick,u=o.text;return a.default.createElement("a",{className:"gt-action "+n,onClick:s},a.default.createElement("span",{className:"gt-action-text"},u))}},function(l,i,e){Object.defineProperty(i,"__esModule",{value:!0});var t=e(114),a=T(t),r=e(64),o=T(r),n=e(65),s=T(n),u=e(116),d=T(u),m=e(120),f=T(m),_=e(12),c=T(_),v=e(130),g=T(v),P=e(131),h=T(P),y=e(312),W=e(441);e(857);function T(b){return b&&b.__esModule?b:{default:b}}typeof window<"u"&&(window.GT_i18n_LocaleMap={zh:W.zhCN,"zh-CN":W.zhCN,"zh-TW":W.zhTW,"es-ES":W.es,fr:W.fr,ru:W.ru,pl:W.pl,ko:W.ko,de:W.de});var j=function(b){(0,f.default)(C,b);function C(){return(0,o.default)(this,C),(0,d.default)(this,(C.__proto__||(0,a.default)(C)).apply(this,arguments))}return(0,s.default)(C,[{key:"shouldComponentUpdate",value:function(M){var D=M.comment;return D!==this.props.comment}},{key:"componentDidMount",value:function(){var M=this.node,D=M.querySelector(".email-hidden-toggle>a");D&&D.addEventListener("click",function(p){p.preventDefault(),M.querySelector(".email-hidden-reply").classList.toggle("expanded")},!0)}},{key:"render",value:function(){var M=this,D=this.props,p=D.comment,w=D.user,L=D.language,z=D.commentedText,ne=z===void 0?"":z,te=D.admin,de=te===void 0?[]:te,ue=D.replyCallback,oe=D.likeCallback,ie=w&&p.user.login===w.login,le=~[].concat(de).map(function(X){return X.toLowerCase()}).indexOf(p.user.login.toLowerCase()),F=p.reactions,S="";return F&&F.totalCount&&(S=F.totalCount,F.totalCount===100&&F.pageInfo&&F.pageInfo.hasNextPage&&(S="100+")),c.default.createElement("div",{ref:function(J){M.node=J},className:"gt-comment "+(le?"gt-comment-admin":"")},c.default.createElement(g.default,{className:"gt-comment-avatar",src:p.user&&p.user.avatar_url,alt:p.user&&p.user.login}),c.default.createElement("div",{className:"gt-comment-content"},c.default.createElement("div",{className:"gt-comment-header"},c.default.createElement("div",{className:"gt-comment-block-"+(w?"2":"1")}),c.default.createElement("a",{className:"gt-comment-username",href:p.user&&p.user.html_url},p.user&&p.user.login),c.default.createElement("span",{className:"gt-comment-text"},ne),c.default.createElement("span",{className:"gt-comment-date"},(0,y.formatDistanceToNow)((0,y.parseISO)(p.created_at),{addSuffix:!0,locale:window.GT_i18n_LocaleMap[L]})),F&&c.default.createElement("a",{className:"gt-comment-like",title:"Like",onClick:oe},F.viewerHasReacted?c.default.createElement(h.default,{className:"gt-ico-heart",name:"heart_on",text:S}):c.default.createElement(h.default,{className:"gt-ico-heart",name:"heart",text:S})),ie?c.default.createElement("a",{href:p.html_url,className:"gt-comment-edit",title:"Edit",target:"_blank",rel:"noopener noreferrer"},c.default.createElement(h.default,{className:"gt-ico-edit",name:"edit"})):c.default.createElement("a",{className:"gt-comment-reply",title:"Reply",onClick:ue},c.default.createElement(h.default,{className:"gt-ico-reply",name:"reply"}))),c.default.createElement("div",{className:"gt-comment-body markdown-body",dangerouslySetInnerHTML:{__html:p.body_html}})))}}]),C}(_.Component);i.default=j},function(l,i,e){var t={"./arrow_down.svg":305,"./edit.svg":306,"./github.svg":307,"./heart.svg":308,"./heart_on.svg":309,"./reply.svg":310,"./tip.svg":311};function a(o){return e(r(o))}function r(o){var n=t[o];if(!(n+1))throw new Error("Cannot find module '"+o+"'.");return n}a.keys=function(){return Object.keys(t)},a.resolve=r,l.exports=a,a.id=304},function(l,i){l.exports=''},function(l,i){l.exports=` + + +`},function(l,i){l.exports=` + + +`},function(l,i){l.exports=` + + +`},function(l,i){l.exports=` + + +`},function(l,i){l.exports=` + + +`},function(l,i){l.exports=` + + + + +`},function(l,i,e){Object.defineProperty(i,"__esModule",{value:!0});var t=e(313);e.d(i,"add",function(){return t.a});var a=e(132);e.d(i,"addBusinessDays",function(){return a.a});var r=e(21);e.d(i,"addDays",function(){return r.a});var o=e(84);e.d(i,"addHours",function(){return o.a});var n=e(134);e.d(i,"addISOWeekYears",function(){return n.a});var s=e(45);e.d(i,"addMilliseconds",function(){return s.a});var u=e(136);e.d(i,"addMinutes",function(){return u.a});var d=e(44);e.d(i,"addMonths",function(){return d.a});var m=e(85);e.d(i,"addQuarters",function(){return m.a});var f=e(137);e.d(i,"addSeconds",function(){return f.a});var _=e(58);e.d(i,"addWeeks",function(){return _.a});var c=e(138);e.d(i,"addYears",function(){return c.a});var v=e(314);e.d(i,"areIntervalsOverlapping",function(){return v.a});var g=e(315);e.d(i,"closestIndexTo",function(){return g.a});var P=e(316);e.d(i,"closestTo",function(){return P.a});var h=e(27);e.d(i,"compareAsc",function(){return h.a});var y=e(317);e.d(i,"compareDesc",function(){return y.a});var W=e(318);e.d(i,"differenceInBusinessDays",function(){return W.a});var T=e(26);e.d(i,"differenceInCalendarDays",function(){return T.a});var j=e(139);e.d(i,"differenceInCalendarISOWeekYears",function(){return j.a});var b=e(319);e.d(i,"differenceInCalendarISOWeeks",function(){return b.a});var C=e(140);e.d(i,"differenceInCalendarMonths",function(){return C.a});var I=e(320);e.d(i,"differenceInCalendarQuarters",function(){return I.a});var M=e(142);e.d(i,"differenceInCalendarWeeks",function(){return M.a});var D=e(143);e.d(i,"differenceInCalendarYears",function(){return D.a});var p=e(86);e.d(i,"differenceInDays",function(){return p.a});var w=e(144);e.d(i,"differenceInHours",function(){return w.a});var L=e(321);e.d(i,"differenceInISOWeekYears",function(){return L.a});var z=e(59);e.d(i,"differenceInMilliseconds",function(){return z.a});var ne=e(146);e.d(i,"differenceInMinutes",function(){return ne.a});var te=e(60);e.d(i,"differenceInMonths",function(){return te.a});var de=e(322);e.d(i,"differenceInQuarters",function(){return de.a});var ue=e(61);e.d(i,"differenceInSeconds",function(){return ue.a});var oe=e(323);e.d(i,"differenceInWeeks",function(){return oe.a});var ie=e(147);e.d(i,"differenceInYears",function(){return ie.a});var le=e(148);e.d(i,"eachDayOfInterval",function(){return le.a});var F=e(324);e.d(i,"eachHourOfInterval",function(){return F.a});var S=e(325);e.d(i,"eachMonthOfInterval",function(){return S.a});var X=e(326);e.d(i,"eachQuarterOfInterval",function(){return X.a});var J=e(327);e.d(i,"eachWeekOfInterval",function(){return J.a});var V=e(88);e.d(i,"eachWeekendOfInterval",function(){return V.a});var E=e(328);e.d(i,"eachWeekendOfMonth",function(){return E.a});var O=e(329);e.d(i,"eachWeekendOfYear",function(){return O.a});var B=e(330);e.d(i,"eachYearOfInterval",function(){return B.a});var A=e(91);e.d(i,"endOfDay",function(){return A.a});var k=e(331);e.d(i,"endOfDecade",function(){return k.a});var U=e(332);e.d(i,"endOfHour",function(){return U.a});var G=e(333);e.d(i,"endOfISOWeek",function(){return G.a});var ee=e(334);e.d(i,"endOfISOWeekYear",function(){return ee.a});var q=e(335);e.d(i,"endOfMinute",function(){return q.a});var re=e(89);e.d(i,"endOfMonth",function(){return re.a});var ce=e(336);e.d(i,"endOfQuarter",function(){return ce.a});var he=e(337);e.d(i,"endOfSecond",function(){return he.a});var fe=e(338);e.d(i,"endOfToday",function(){return fe.a});var ge=e(339);e.d(i,"endOfTomorrow",function(){return ge.a});var Ee=e(150);e.d(i,"endOfWeek",function(){return Ee.a});var x=e(149);e.d(i,"endOfYear",function(){return x.a});var N=e(340);e.d(i,"endOfYesterday",function(){return N.a});var Y=e(151);e.d(i,"format",function(){return Y.a});var $=e(158);e.d(i,"formatDistance",function(){return $.a});var Q=e(161);e.d(i,"formatDistanceStrict",function(){return Q.a});var ae=e(346);e.d(i,"formatDistanceToNow",function(){return ae.a});var me=e(347);e.d(i,"formatDistanceToNowStrict",function(){return me.a});var ve=e(348);e.d(i,"formatDuration",function(){return ve.a});var pe=e(349);e.d(i,"formatISO",function(){return pe.a});var K=e(350);e.d(i,"formatISO9075",function(){return K.a});var Z=e(351);e.d(i,"formatISODuration",function(){return Z.a});var se=e(352);e.d(i,"formatRFC3339",function(){return se.a});var _e=e(353);e.d(i,"formatRFC7231",function(){return _e.a});var Pe=e(354);e.d(i,"formatRelative",function(){return Pe.a});var Me=e(355);e.d(i,"fromUnixTime",function(){return Me.a});var De=e(162);e.d(i,"getDate",function(){return De.a});var We=e(163);e.d(i,"getDay",function(){return We.a});var we=e(356);e.d(i,"getDayOfYear",function(){return we.a});var Ue=e(164);e.d(i,"getDaysInMonth",function(){return Ue.a});var mt=e(357);e.d(i,"getDaysInYear",function(){return mt.a});var ft=e(358);e.d(i,"getDecade",function(){return ft.a});var ct=e(359);e.d(i,"getHours",function(){return ct.a});var _t=e(166);e.d(i,"getISODay",function(){return _t.a});var ht=e(167);e.d(i,"getISOWeek",function(){return ht.a});var vt=e(34);e.d(i,"getISOWeekYear",function(){return vt.a});var gt=e(360);e.d(i,"getISOWeeksInYear",function(){return gt.a});var Pt=e(361);e.d(i,"getMilliseconds",function(){return Pt.a});var Mt=e(362);e.d(i,"getMinutes",function(){return Mt.a});var Et=e(363);e.d(i,"getMonth",function(){return Et.a});var pt=e(364);e.d(i,"getOverlappingDaysInIntervals",function(){return pt.a});var Dt=e(141);e.d(i,"getQuarter",function(){return Dt.a});var yt=e(365);e.d(i,"getSeconds",function(){return yt.a});var Ot=e(168);e.d(i,"getTime",function(){return Ot.a});var bt=e(366);e.d(i,"getUnixTime",function(){return bt.a});var Wt=e(169);e.d(i,"getWeek",function(){return Wt.a});var Tt=e(367);e.d(i,"getWeekOfMonth",function(){return Tt.a});var wt=e(170);e.d(i,"getWeekYear",function(){return wt.a});var Ct=e(368);e.d(i,"getWeeksInMonth",function(){return Ct.a});var xt=e(369);e.d(i,"getYear",function(){return xt.a});var jt=e(370);e.d(i,"intervalToDuration",function(){return jt.a});var It=e(371);e.d(i,"isAfter",function(){return It.a});var At=e(372);e.d(i,"isBefore",function(){return At.a});var Lt=e(373);e.d(i,"isDate",function(){return Lt.a});var Rt=e(374);e.d(i,"isEqual",function(){return Rt.a});var Ut=e(375);e.d(i,"isExists",function(){return Ut.a});var Bt=e(376);e.d(i,"isFirstDayOfMonth",function(){return Bt.a});var Kt=e(377);e.d(i,"isFriday",function(){return Kt.a});var St=e(378);e.d(i,"isFuture",function(){return St.a});var Nt=e(379);e.d(i,"isLastDayOfMonth",function(){return Nt.a});var zt=e(165);e.d(i,"isLeapYear",function(){return zt.a});var Ft=e(380);e.d(i,"isMatch",function(){return Ft.a});var kt=e(386);e.d(i,"isMonday",function(){return kt.a});var Ht=e(387);e.d(i,"isPast",function(){return Ht.a});var Yt=e(47);e.d(i,"isSameDay",function(){return Yt.a});var Vt=e(175);e.d(i,"isSameHour",function(){return Vt.a});var Xt=e(177);e.d(i,"isSameISOWeek",function(){return Xt.a});var Gt=e(388);e.d(i,"isSameISOWeekYear",function(){return Gt.a});var Qt=e(178);e.d(i,"isSameMinute",function(){return Qt.a});var $t=e(180);e.d(i,"isSameMonth",function(){return $t.a});var Jt=e(181);e.d(i,"isSameQuarter",function(){return Jt.a});var Zt=e(182);e.d(i,"isSameSecond",function(){return Zt.a});var qt=e(95);e.d(i,"isSameWeek",function(){return qt.a});var ea=e(184);e.d(i,"isSameYear",function(){return ea.a});var ta=e(133);e.d(i,"isSaturday",function(){return ta.a});var aa=e(83);e.d(i,"isSunday",function(){return aa.a});var na=e(389);e.d(i,"isThisHour",function(){return na.a});var ra=e(390);e.d(i,"isThisISOWeek",function(){return ra.a});var ia=e(391);e.d(i,"isThisMinute",function(){return ia.a});var oa=e(392);e.d(i,"isThisMonth",function(){return oa.a});var sa=e(393);e.d(i,"isThisQuarter",function(){return sa.a});var ua=e(394);e.d(i,"isThisSecond",function(){return ua.a});var da=e(395);e.d(i,"isThisWeek",function(){return da.a});var la=e(396);e.d(i,"isThisYear",function(){return la.a});var ma=e(397);e.d(i,"isThursday",function(){return ma.a});var fa=e(398);e.d(i,"isToday",function(){return fa.a});var ca=e(399);e.d(i,"isTomorrow",function(){return ca.a});var _a=e(400);e.d(i,"isTuesday",function(){return _a.a});var ha=e(15);e.d(i,"isValid",function(){return ha.a});var va=e(401);e.d(i,"isWednesday",function(){return va.a});var ga=e(56);e.d(i,"isWeekend",function(){return ga.a});var Pa=e(402);e.d(i,"isWithinInterval",function(){return Pa.a});var Ma=e(403);e.d(i,"isYesterday",function(){return Ma.a});var Ea=e(404);e.d(i,"lastDayOfDecade",function(){return Ea.a});var pa=e(405);e.d(i,"lastDayOfISOWeek",function(){return pa.a});var Da=e(406);e.d(i,"lastDayOfISOWeekYear",function(){return Da.a});var ya=e(171);e.d(i,"lastDayOfMonth",function(){return ya.a});var Oa=e(407);e.d(i,"lastDayOfQuarter",function(){return Oa.a});var ba=e(185);e.d(i,"lastDayOfWeek",function(){return ba.a});var Wa=e(408);e.d(i,"lastDayOfYear",function(){return Wa.a});var Ta=e(409);e.d(i,"lightFormat",function(){return Ta.a});var wa=e(410);e.d(i,"max",function(){return wa.a});var Ca=e(411);e.d(i,"min",function(){return Ca.a});var xa=e(174);e.d(i,"parse",function(){return xa.a});var ja=e(412);e.d(i,"parseISO",function(){return ja.a});var Ia=e(413);e.d(i,"parseJSON",function(){return Ia.a});var Aa=e(414);e.d(i,"roundToNearestMinutes",function(){return Aa.a});var La=e(415);e.d(i,"set",function(){return La.a});var Ra=e(416);e.d(i,"setDate",function(){return Ra.a});var Ua=e(417);e.d(i,"setDay",function(){return Ua.a});var Ba=e(418);e.d(i,"setDayOfYear",function(){return Ba.a});var Ka=e(419);e.d(i,"setHours",function(){return Ka.a});var Sa=e(420);e.d(i,"setISODay",function(){return Sa.a});var Na=e(421);e.d(i,"setISOWeek",function(){return Na.a});var za=e(135);e.d(i,"setISOWeekYear",function(){return za.a});var Fa=e(422);e.d(i,"setMilliseconds",function(){return Fa.a});var ka=e(423);e.d(i,"setMinutes",function(){return ka.a});var Ha=e(96);e.d(i,"setMonth",function(){return Ha.a});var Ya=e(424);e.d(i,"setQuarter",function(){return Ya.a});var Va=e(425);e.d(i,"setSeconds",function(){return Va.a});var Xa=e(426);e.d(i,"setWeek",function(){return Xa.a});var Ga=e(427);e.d(i,"setWeekYear",function(){return Ga.a});var Qa=e(428);e.d(i,"setYear",function(){return Qa.a});var $a=e(57);e.d(i,"startOfDay",function(){return $a.a});var Ja=e(429);e.d(i,"startOfDecade",function(){return Ja.a});var Za=e(176);e.d(i,"startOfHour",function(){return Za.a});var qa=e(25);e.d(i,"startOfISOWeek",function(){return qa.a});var en=e(46);e.d(i,"startOfISOWeekYear",function(){return en.a});var tn=e(179);e.d(i,"startOfMinute",function(){return tn.a});var an=e(62);e.d(i,"startOfMonth",function(){return an.a});var nn=e(87);e.d(i,"startOfQuarter",function(){return nn.a});var rn=e(183);e.d(i,"startOfSecond",function(){return rn.a});var on=e(430);e.d(i,"startOfToday",function(){return on.a});var sn=e(431);e.d(i,"startOfTomorrow",function(){return sn.a});var un=e(22);e.d(i,"startOfWeek",function(){return un.a});var dn=e(93);e.d(i,"startOfWeekYear",function(){return dn.a});var ln=e(90);e.d(i,"startOfYear",function(){return ln.a});var mn=e(432);e.d(i,"startOfYesterday",function(){return mn.a});var fn=e(172);e.d(i,"sub",function(){return fn.a});var cn=e(433);e.d(i,"subBusinessDays",function(){return cn.a});var _n=e(94);e.d(i,"subDays",function(){return _n.a});var hn=e(434);e.d(i,"subHours",function(){return hn.a});var vn=e(145);e.d(i,"subISOWeekYears",function(){return vn.a});var gn=e(49);e.d(i,"subMilliseconds",function(){return gn.a});var Pn=e(435);e.d(i,"subMinutes",function(){return Pn.a});var Mn=e(173);e.d(i,"subMonths",function(){return Mn.a});var En=e(436);e.d(i,"subQuarters",function(){return En.a});var pn=e(437);e.d(i,"subSeconds",function(){return pn.a});var Dn=e(438);e.d(i,"subWeeks",function(){return Dn.a});var yn=e(439);e.d(i,"subYears",function(){return yn.a});var On=e(1);e.d(i,"toDate",function(){return On.a});var $e=e(440);e.d(i,"maxTime",function(){return $e.a}),e.d(i,"minTime",function(){return $e.b})},function(l,i,e){i.a=s;var t=e(21),a=e(44),r=e(1),o=e(0),n=e(6);function s(u,d){if(o.a(2,arguments),!d||typeof d!="object")return new Date(NaN);var m="years"in d?n.a(d.years):0,f="months"in d?n.a(d.months):0,_="weeks"in d?n.a(d.weeks):0,c="days"in d?n.a(d.days):0,v="hours"in d?n.a(d.hours):0,g="minutes"in d?n.a(d.minutes):0,P="seconds"in d?n.a(d.seconds):0,h=r.a(u),y=f||m?a.a(h,f+m*12):h,W=c||_?t.a(y,c+_*7):y,T=g+v*60,j=P+T*60,b=j*1e3,C=new Date(W.getTime()+b);return C}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o,n){var s=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{};a.a(2,arguments);var u=o||{},d=n||{},m=t.a(u.start).getTime(),f=t.a(u.end).getTime(),_=t.a(d.start).getTime(),c=t.a(d.end).getTime();if(!(m<=f&&_<=c))throw new RangeError("Invalid interval");return s.inclusive?m<=c&&_<=f:m0?-1:d<0?1:d}},function(l,i,e){i.a=m;var t=e(15),a=e(56),r=e(1),o=e(26),n=e(21),s=e(47),u=e(6),d=e(0);function m(f,_){d.a(2,arguments);var c=r.a(f),v=r.a(_);if(!t.a(c)||!t.a(v))return new Date(NaN);var g=o.a(c,v),P=g<0?-1:1,h=u.a(g/7),y=h*5;for(v=n.a(v,h*7);!s.a(c,v);)y+=a.a(v)?0:P,v=n.a(v,P);return y===0?0:y}},function(l,i,e){i.a=n;var t=e(19),a=e(25),r=e(0),o=6048e5;function n(s,u){r.a(2,arguments);var d=a.a(s),m=a.a(u),f=d.getTime()-t.a(d),_=m.getTime()-t.a(m);return Math.round((f-_)/o)}},function(l,i,e){i.a=o;var t=e(141),a=e(1),r=e(0);function o(n,s){r.a(2,arguments);var u=a.a(n),d=a.a(s),m=u.getFullYear()-d.getFullYear(),f=t.a(u)-t.a(d);return m*4+f}},function(l,i,e){i.a=s;var t=e(1),a=e(139),r=e(27),o=e(145),n=e(0);function s(u,d){n.a(2,arguments);var m=t.a(u),f=t.a(d),_=r.a(m,f),c=Math.abs(a.a(m,f));m=o.a(m,_*c);var v=r.a(m,f)===-_,g=_*(c-v);return g===0?0:g}},function(l,i,e){i.a=r;var t=e(60),a=e(0);function r(o,n){a.a(2,arguments);var s=t.a(o,n)/3;return s>0?Math.floor(s):Math.ceil(s)}},function(l,i,e){i.a=r;var t=e(86),a=e(0);function r(o,n){a.a(2,arguments);var s=t.a(o,n)/7;return s>0?Math.floor(s):Math.ceil(s)}},function(l,i,e){i.a=o;var t=e(84),a=e(1),r=e(0);function o(n,s){r.a(1,arguments);var u=n||{},d=a.a(u.start),m=a.a(u.end),f=d.getTime(),_=m.getTime();if(!(f<=_))throw new RangeError("Invalid interval");var c=[],v=d;v.setMinutes(0,0,0);var g=s&&"step"in s?Number(s.step):1;if(g<1||isNaN(g))throw new RangeError("`options.step` must be a number greater than 1");for(;v.getTime()<=_;)c.push(a.a(v)),v=t.a(v,g);return c}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=o||{},s=t.a(n.start),u=t.a(n.end),d=u.getTime();if(!(s.getTime()<=d))throw new RangeError("Invalid interval");var m=[],f=s;for(f.setHours(0,0,0,0),f.setDate(1);f.getTime()<=d;)m.push(t.a(f)),f.setMonth(f.getMonth()+1);return m}},function(l,i,e){i.a=n;var t=e(85),a=e(87),r=e(1),o=e(0);function n(s){o.a(1,arguments);var u=s||{},d=r.a(u.start),m=r.a(u.end),f=m.getTime();if(!(d.getTime()<=f))throw new RangeError("Invalid interval");var _=a.a(d),c=a.a(m);f=c.getTime();for(var v=[],g=_;g.getTime()<=f;)v.push(r.a(g)),g=t.a(g,1);return v}},function(l,i,e){i.a=n;var t=e(58),a=e(22),r=e(1),o=e(0);function n(s,u){o.a(1,arguments);var d=s||{},m=r.a(d.start),f=r.a(d.end),_=f.getTime();if(!(m.getTime()<=_))throw new RangeError("Invalid interval");var c=a.a(m,u),v=a.a(f,u);c.setHours(15),v.setHours(15),_=v.getTime();for(var g=[],P=c;P.getTime()<=_;)P.setHours(0),g.push(r.a(P)),P=t.a(P,1),P.setHours(15);return g}},function(l,i,e){i.a=n;var t=e(88),a=e(62),r=e(89),o=e(0);function n(s){o.a(1,arguments);var u=a.a(s);if(isNaN(u))throw new RangeError("The passed date is invalid");var d=r.a(s);return t.a({start:u,end:d})}},function(l,i,e){i.a=n;var t=e(88),a=e(90),r=e(149),o=e(0);function n(s){o.a(1,arguments);var u=a.a(s);if(isNaN(u))throw new RangeError("The passed date is invalid");var d=r.a(s);return t.a({start:u,end:d})}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=o||{},s=t.a(n.start),u=t.a(n.end),d=u.getTime();if(!(s.getTime()<=d))throw new RangeError("Invalid interval");var m=[],f=s;for(f.setHours(0,0,0,0),f.setMonth(0,1);f.getTime()<=d;)m.push(t.a(f)),f.setFullYear(f.getFullYear()+1);return m}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=t.a(o),s=n.getFullYear(),u=9+Math.floor(s/10)*10;return n.setFullYear(u,11,31),n.setHours(23,59,59,999),n}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=t.a(o);return n.setMinutes(59,59,999),n}},function(l,i,e){i.a=r;var t=e(150),a=e(0);function r(o){return a.a(1,arguments),t.a(o,{weekStartsOn:1})}},function(l,i,e){i.a=o;var t=e(34),a=e(25),r=e(0);function o(n){r.a(1,arguments);var s=t.a(n),u=new Date(0);u.setFullYear(s+1,0,4),u.setHours(0,0,0,0);var d=a.a(u);return d.setMilliseconds(d.getMilliseconds()-1),d}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=t.a(o);return n.setSeconds(59,999),n}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=t.a(o),s=n.getMonth(),u=s-s%3+3;return n.setMonth(u,0),n.setHours(23,59,59,999),n}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=t.a(o);return n.setMilliseconds(999),n}},function(l,i,e){i.a=a;var t=e(91);function a(){return t.a(Date.now())}},function(l,i,e){i.a=t;function t(){var a=new Date,r=a.getFullYear(),o=a.getMonth(),n=a.getDate(),s=new Date(0);return s.setFullYear(r,o,n+1),s.setHours(23,59,59,999),s}},function(l,i,e){i.a=t;function t(){var a=new Date,r=a.getFullYear(),o=a.getMonth(),n=a.getDate(),s=new Date(0);return s.setFullYear(r,o,n-1),s.setHours(23,59,59,999),s}},function(l,i,e){var t=e(2),a={full:"EEEE, MMMM do, y",long:"MMMM do, y",medium:"MMM d, y",short:"MM/dd/yyyy"},r={full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},o={full:"{{date}} 'at' {{time}}",long:"{{date}} 'at' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){var t=e(152),a=e(343),r=e(153),o=e(154),n=e(155),s=e(92),u=e(38),d={am:"am",pm:"pm",midnight:"midnight",noon:"noon",morning:"morning",afternoon:"afternoon",evening:"evening",night:"night"},m={G:function(v,g,P){var h=v.getUTCFullYear()>0?1:0;switch(g){case"G":case"GG":case"GGG":return P.era(h,{width:"abbreviated"});case"GGGGG":return P.era(h,{width:"narrow"});case"GGGG":default:return P.era(h,{width:"wide"})}},y:function(v,g,P){if(g==="yo"){var h=v.getUTCFullYear(),y=h>0?h:1-h;return P.ordinalNumber(y,{unit:"year"})}return t.a.y(v,g)},Y:function(v,g,P,h){var y=s.a(v,h),W=y>0?y:1-y;if(g==="YY"){var T=W%100;return u.a(T,2)}return g==="Yo"?P.ordinalNumber(W,{unit:"year"}):u.a(W,g.length)},R:function(v,g){var P=o.a(v);return u.a(P,g.length)},u:function(v,g){var P=v.getUTCFullYear();return u.a(P,g.length)},Q:function(v,g,P){var h=Math.ceil((v.getUTCMonth()+1)/3);switch(g){case"Q":return String(h);case"QQ":return u.a(h,2);case"Qo":return P.ordinalNumber(h,{unit:"quarter"});case"QQQ":return P.quarter(h,{width:"abbreviated",context:"formatting"});case"QQQQQ":return P.quarter(h,{width:"narrow",context:"formatting"});case"QQQQ":default:return P.quarter(h,{width:"wide",context:"formatting"})}},q:function(v,g,P){var h=Math.ceil((v.getUTCMonth()+1)/3);switch(g){case"q":return String(h);case"qq":return u.a(h,2);case"qo":return P.ordinalNumber(h,{unit:"quarter"});case"qqq":return P.quarter(h,{width:"abbreviated",context:"standalone"});case"qqqqq":return P.quarter(h,{width:"narrow",context:"standalone"});case"qqqq":default:return P.quarter(h,{width:"wide",context:"standalone"})}},M:function(v,g,P){var h=v.getUTCMonth();switch(g){case"M":case"MM":return t.a.M(v,g);case"Mo":return P.ordinalNumber(h+1,{unit:"month"});case"MMM":return P.month(h,{width:"abbreviated",context:"formatting"});case"MMMMM":return P.month(h,{width:"narrow",context:"formatting"});case"MMMM":default:return P.month(h,{width:"wide",context:"formatting"})}},L:function(v,g,P){var h=v.getUTCMonth();switch(g){case"L":return String(h+1);case"LL":return u.a(h+1,2);case"Lo":return P.ordinalNumber(h+1,{unit:"month"});case"LLL":return P.month(h,{width:"abbreviated",context:"standalone"});case"LLLLL":return P.month(h,{width:"narrow",context:"standalone"});case"LLLL":default:return P.month(h,{width:"wide",context:"standalone"})}},w:function(v,g,P,h){var y=n.a(v,h);return g==="wo"?P.ordinalNumber(y,{unit:"week"}):u.a(y,g.length)},I:function(v,g,P){var h=r.a(v);return g==="Io"?P.ordinalNumber(h,{unit:"week"}):u.a(h,g.length)},d:function(v,g,P){return g==="do"?P.ordinalNumber(v.getUTCDate(),{unit:"date"}):t.a.d(v,g)},D:function(v,g,P){var h=a.a(v);return g==="Do"?P.ordinalNumber(h,{unit:"dayOfYear"}):u.a(h,g.length)},E:function(v,g,P){var h=v.getUTCDay();switch(g){case"E":case"EE":case"EEE":return P.day(h,{width:"abbreviated",context:"formatting"});case"EEEEE":return P.day(h,{width:"narrow",context:"formatting"});case"EEEEEE":return P.day(h,{width:"short",context:"formatting"});case"EEEE":default:return P.day(h,{width:"wide",context:"formatting"})}},e:function(v,g,P,h){var y=v.getUTCDay(),W=(y-h.weekStartsOn+8)%7||7;switch(g){case"e":return String(W);case"ee":return u.a(W,2);case"eo":return P.ordinalNumber(W,{unit:"day"});case"eee":return P.day(y,{width:"abbreviated",context:"formatting"});case"eeeee":return P.day(y,{width:"narrow",context:"formatting"});case"eeeeee":return P.day(y,{width:"short",context:"formatting"});case"eeee":default:return P.day(y,{width:"wide",context:"formatting"})}},c:function(v,g,P,h){var y=v.getUTCDay(),W=(y-h.weekStartsOn+8)%7||7;switch(g){case"c":return String(W);case"cc":return u.a(W,g.length);case"co":return P.ordinalNumber(W,{unit:"day"});case"ccc":return P.day(y,{width:"abbreviated",context:"standalone"});case"ccccc":return P.day(y,{width:"narrow",context:"standalone"});case"cccccc":return P.day(y,{width:"short",context:"standalone"});case"cccc":default:return P.day(y,{width:"wide",context:"standalone"})}},i:function(v,g,P){var h=v.getUTCDay(),y=h===0?7:h;switch(g){case"i":return String(y);case"ii":return u.a(y,g.length);case"io":return P.ordinalNumber(y,{unit:"day"});case"iii":return P.day(h,{width:"abbreviated",context:"formatting"});case"iiiii":return P.day(h,{width:"narrow",context:"formatting"});case"iiiiii":return P.day(h,{width:"short",context:"formatting"});case"iiii":default:return P.day(h,{width:"wide",context:"formatting"})}},a:function(v,g,P){var h=v.getUTCHours(),y=h/12>=1?"pm":"am";switch(g){case"a":case"aa":case"aaa":return P.dayPeriod(y,{width:"abbreviated",context:"formatting"});case"aaaaa":return P.dayPeriod(y,{width:"narrow",context:"formatting"});case"aaaa":default:return P.dayPeriod(y,{width:"wide",context:"formatting"})}},b:function(v,g,P){var h=v.getUTCHours(),y;switch(h===12?y=d.noon:h===0?y=d.midnight:y=h/12>=1?"pm":"am",g){case"b":case"bb":case"bbb":return P.dayPeriod(y,{width:"abbreviated",context:"formatting"});case"bbbbb":return P.dayPeriod(y,{width:"narrow",context:"formatting"});case"bbbb":default:return P.dayPeriod(y,{width:"wide",context:"formatting"})}},B:function(v,g,P){var h=v.getUTCHours(),y;switch(h>=17?y=d.evening:h>=12?y=d.afternoon:h>=4?y=d.morning:y=d.night,g){case"B":case"BB":case"BBB":return P.dayPeriod(y,{width:"abbreviated",context:"formatting"});case"BBBBB":return P.dayPeriod(y,{width:"narrow",context:"formatting"});case"BBBB":default:return P.dayPeriod(y,{width:"wide",context:"formatting"})}},h:function(v,g,P){if(g==="ho"){var h=v.getUTCHours()%12;return h===0&&(h=12),P.ordinalNumber(h,{unit:"hour"})}return t.a.h(v,g)},H:function(v,g,P){return g==="Ho"?P.ordinalNumber(v.getUTCHours(),{unit:"hour"}):t.a.H(v,g)},K:function(v,g,P){var h=v.getUTCHours()%12;return g==="Ko"?P.ordinalNumber(h,{unit:"hour"}):u.a(h,g.length)},k:function(v,g,P){var h=v.getUTCHours();return h===0&&(h=24),g==="ko"?P.ordinalNumber(h,{unit:"hour"}):u.a(h,g.length)},m:function(v,g,P){return g==="mo"?P.ordinalNumber(v.getUTCMinutes(),{unit:"minute"}):t.a.m(v,g)},s:function(v,g,P){return g==="so"?P.ordinalNumber(v.getUTCSeconds(),{unit:"second"}):t.a.s(v,g)},S:function(v,g){return t.a.S(v,g)},X:function(v,g,P,h){var y=h._originalDate||v,W=y.getTimezoneOffset();if(W===0)return"Z";switch(g){case"X":return _(W);case"XXXX":case"XX":return c(W);case"XXXXX":case"XXX":default:return c(W,":")}},x:function(v,g,P,h){var y=h._originalDate||v,W=y.getTimezoneOffset();switch(g){case"x":return _(W);case"xxxx":case"xx":return c(W);case"xxxxx":case"xxx":default:return c(W,":")}},O:function(v,g,P,h){var y=h._originalDate||v,W=y.getTimezoneOffset();switch(g){case"O":case"OO":case"OOO":return"GMT"+f(W,":");case"OOOO":default:return"GMT"+c(W,":")}},z:function(v,g,P,h){var y=h._originalDate||v,W=y.getTimezoneOffset();switch(g){case"z":case"zz":case"zzz":return"GMT"+f(W,":");case"zzzz":default:return"GMT"+c(W,":")}},t:function(v,g,P,h){var y=h._originalDate||v,W=Math.floor(y.getTime()/1e3);return u.a(W,g.length)},T:function(v,g,P,h){var y=h._originalDate||v,W=y.getTime();return u.a(W,g.length)}};function f(v,g){var P=v>0?"-":"+",h=Math.abs(v),y=Math.floor(h/60),W=h%60;if(W===0)return P+String(y);var T=g||"";return P+String(y)+T+u.a(W,2)}function _(v,g){if(v%60===0){var P=v>0?"-":"+";return P+u.a(Math.abs(v)/60,2)}return c(v,g)}function c(v,g){var P=g||"",h=v>0?"-":"+",y=Math.abs(v),W=u.a(Math.floor(y/60),2),T=u.a(y%60,2);return h+W+P+T}i.a=m},function(l,i,e){i.a=o;var t=e(1),a=e(0),r=864e5;function o(n){a.a(1,arguments);var s=t.a(n),u=s.getTime();s.setUTCMonth(0,1),s.setUTCHours(0,0,0,0);var d=s.getTime(),m=u-d;return Math.floor(m/r)+1}},function(l,i,e){i.a=o;var t=e(154),a=e(63),r=e(0);function o(n){r.a(1,arguments);var s=t.a(n),u=new Date(0);u.setUTCFullYear(s,0,4),u.setUTCHours(0,0,0,0);var d=a.a(u);return d}},function(l,i,e){i.a=n;var t=e(6),a=e(92),r=e(50),o=e(0);function n(s,u){o.a(1,arguments);var d=u||{},m=d.locale,f=m&&m.options&&m.options.firstWeekContainsDate,_=f==null?1:t.a(f),c=d.firstWeekContainsDate==null?_:t.a(d.firstWeekContainsDate),v=a.a(s,u),g=new Date(0);g.setUTCFullYear(v,0,c),g.setUTCHours(0,0,0,0);var P=r.a(g,u);return P}},function(l,i,e){i.a=r;var t=e(158),a=e(0);function r(o,n){return a.a(1,arguments),t.a(o,Date.now(),n)}},function(l,i,e){i.a=r;var t=e(161),a=e(0);function r(o,n){return a.a(1,arguments),t.a(o,Date.now(),n)}},function(l,i,e){i.a=r;var t=e(28),a=["years","months","weeks","days","hours","minutes","seconds"];function r(o){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};if(arguments.length<1)throw new TypeError("1 argument required, but only ".concat(arguments.length," present"));var s=n.format||a,u=n.locale||t.a,d=n.zero||!1,m=n.delimiter||" ",f=s.reduce(function(_,c){var v="x".concat(c.replace(/(^.)/,function(P){return P.toUpperCase()})),g=typeof o[c]=="number"&&(d||o[c]);return g?_.concat(u.formatDistance(v,o[c])):_},[]).join(m);return f}},function(l,i,e){i.a=o;var t=e(1),a=e(15),r=e(38);function o(n,s){if(arguments.length<1)throw new TypeError("1 argument required, but only ".concat(arguments.length," present"));var u=t.a(n);if(!a.a(u))throw new RangeError("Invalid time value");var d=s||{},m=d.format==null?"extended":String(d.format),f=d.representation==null?"complete":String(d.representation);if(m!=="extended"&&m!=="basic")throw new RangeError("format must be 'extended' or 'basic'");if(f!=="date"&&f!=="time"&&f!=="complete")throw new RangeError("representation must be 'date', 'time', or 'complete'");var _="",c="",v=m==="extended"?"-":"",g=m==="extended"?":":"";if(f!=="time"){var P=r.a(u.getDate(),2),h=r.a(u.getMonth()+1,2),y=r.a(u.getFullYear(),4);_="".concat(y).concat(v).concat(h).concat(v).concat(P)}if(f!=="date"){var W=u.getTimezoneOffset();if(W!==0){var T=Math.abs(W),j=r.a(Math.floor(T/60),2),b=r.a(T%60,2),C=W<0?"+":"-";c="".concat(C).concat(j,":").concat(b)}else c="Z";var I=r.a(u.getHours(),2),M=r.a(u.getMinutes(),2),D=r.a(u.getSeconds(),2),p=_===""?"":"T",w=[I,M,D].join(g);_="".concat(_).concat(p).concat(w).concat(c)}return _}},function(l,i,e){i.a=o;var t=e(1),a=e(15),r=e(38);function o(n,s){if(arguments.length<1)throw new TypeError("1 argument required, but only ".concat(arguments.length," present"));var u=t.a(n);if(!a.a(u))throw new RangeError("Invalid time value");var d=s||{},m=d.format==null?"extended":String(d.format),f=d.representation==null?"complete":String(d.representation);if(m!=="extended"&&m!=="basic")throw new RangeError("format must be 'extended' or 'basic'");if(f!=="date"&&f!=="time"&&f!=="complete")throw new RangeError("representation must be 'date', 'time', or 'complete'");var _="",c=m==="extended"?"-":"",v=m==="extended"?":":"";if(f!=="time"){var g=r.a(u.getDate(),2),P=r.a(u.getMonth()+1,2),h=r.a(u.getFullYear(),4);_="".concat(h).concat(c).concat(P).concat(c).concat(g)}if(f!=="date"){var y=r.a(u.getHours(),2),W=r.a(u.getMinutes(),2),T=r.a(u.getSeconds(),2),j=_===""?"":" ";_="".concat(_).concat(j).concat(y).concat(v).concat(W).concat(v).concat(T)}return _}},function(l,i,e){i.a=a;var t=e(0);function a(r){if(t.a(1,arguments),typeof r!="object")throw new Error("Duration must be an object");var o=r.years,n=o===void 0?0:o,s=r.months,u=s===void 0?0:s,d=r.days,m=d===void 0?0:d,f=r.hours,_=f===void 0?0:f,c=r.minutes,v=c===void 0?0:c,g=r.seconds,P=g===void 0?0:g;return"P".concat(n,"Y").concat(u,"M").concat(m,"DT").concat(_,"H").concat(v,"M").concat(P,"S")}},function(l,i,e){i.a=n;var t=e(1),a=e(15),r=e(38),o=e(6);function n(s,u){if(arguments.length<1)throw new TypeError("1 arguments required, but only ".concat(arguments.length," present"));var d=t.a(s);if(!a.a(d))throw new RangeError("Invalid time value");var m=u||{},f=m.fractionDigits==null?0:o.a(m.fractionDigits);if(!(f>=0&&f<=3))throw new RangeError("fractionDigits must be between 0 and 3 inclusively");var _=r.a(d.getDate(),2),c=r.a(d.getMonth()+1,2),v=d.getFullYear(),g=r.a(d.getHours(),2),P=r.a(d.getMinutes(),2),h=r.a(d.getSeconds(),2),y="";if(f>0){var W=d.getMilliseconds(),T=Math.floor(W*Math.pow(10,f-3));y="."+r.a(T,f)}var j="",b=d.getTimezoneOffset();if(b!==0){var C=Math.abs(b),I=r.a(o.a(C/60),2),M=r.a(C%60,2),D=b<0?"+":"-";j="".concat(D).concat(I,":").concat(M)}else j="Z";return"".concat(v,"-").concat(c,"-").concat(_,"T").concat(g,":").concat(P,":").concat(h).concat(y).concat(j)}},function(l,i,e){i.a=s;var t=e(1),a=e(15),r=e(38),o=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],n=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function s(u){if(arguments.length<1)throw new TypeError("1 arguments required, but only ".concat(arguments.length," present"));var d=t.a(u);if(!a.a(d))throw new RangeError("Invalid time value");var m=o[d.getUTCDay()],f=r.a(d.getUTCDate(),2),_=n[d.getUTCMonth()],c=d.getUTCFullYear(),v=r.a(d.getUTCHours(),2),g=r.a(d.getUTCMinutes(),2),P=r.a(d.getUTCSeconds(),2);return"".concat(m,", ").concat(f," ").concat(_," ").concat(c," ").concat(v,":").concat(g,":").concat(P," GMT")}},function(l,i,e){i.a=d;var t=e(26),a=e(151),r=e(28),o=e(49),n=e(1),s=e(19),u=e(0);function d(m,f,_){u.a(2,arguments);var c=n.a(m),v=n.a(f),g=_||{},P=g.locale||r.a;if(!P.localize)throw new RangeError("locale must contain localize property");if(!P.formatLong)throw new RangeError("locale must contain formatLong property");if(!P.formatRelative)throw new RangeError("locale must contain formatRelative property");var h=t.a(c,v);if(isNaN(h))throw new RangeError("Invalid time value");var y;h<-6?y="other":h<-1?y="lastWeek":h<0?y="yesterday":h<1?y="today":h<2?y="tomorrow":h<7?y="nextWeek":y="other";var W=o.a(c,s.a(c)),T=o.a(v,s.a(v)),j=P.formatRelative(y,W,T,g);return a.a(c,j,g)}},function(l,i,e){i.a=o;var t=e(1),a=e(6),r=e(0);function o(n){r.a(1,arguments);var s=a.a(n);return t.a(s*1e3)}},function(l,i,e){i.a=n;var t=e(1),a=e(90),r=e(26),o=e(0);function n(s){o.a(1,arguments);var u=t.a(s),d=r.a(u,a.a(u)),m=d+1;return m}},function(l,i,e){i.a=o;var t=e(1),a=e(165),r=e(0);function o(n){r.a(1,arguments);var s=t.a(n);return isNaN(s)?NaN:a.a(s)?366:365}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=t.a(o),s=n.getFullYear(),u=Math.floor(s/10)*10;return u}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=t.a(o),s=n.getHours();return s}},function(l,i,e){i.a=n;var t=e(46),a=e(58),r=e(0),o=6048e5;function n(s){r.a(1,arguments);var u=t.a(s),d=t.a(a.a(u,60)),m=d.valueOf()-u.valueOf();return Math.round(m/o)}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=t.a(o),s=n.getMilliseconds();return s}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=t.a(o),s=n.getMinutes();return s}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=t.a(o),s=n.getMonth();return s}},function(l,i,e){i.a=o;var t=e(1),a=e(0),r=24*60*60*1e3;function o(n,s){a.a(2,arguments);var u=n||{},d=s||{},m=t.a(u.start).getTime(),f=t.a(u.end).getTime(),_=t.a(d.start).getTime(),c=t.a(d.end).getTime();if(!(m<=f&&_<=c))throw new RangeError("Invalid interval");var v=mf?f:c,h=P-g;return Math.ceil(h/r)}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=t.a(o),s=n.getSeconds();return s}},function(l,i,e){i.a=r;var t=e(168),a=e(0);function r(o){return a.a(1,arguments),Math.floor(t.a(o)/1e3)}},function(l,i,e){i.a=s;var t=e(162),a=e(163),r=e(62),o=e(6),n=e(0);function s(u,d){n.a(1,arguments);var m=d||{},f=m.locale,_=f&&f.options&&f.options.weekStartsOn,c=_==null?0:o.a(_),v=m.weekStartsOn==null?c:o.a(m.weekStartsOn);if(!(v>=0&&v<=6))throw new RangeError("weekStartsOn must be between 0 and 6 inclusively");var g=t.a(u);if(isNaN(g))return g;var P=a.a(r.a(u)),h=0;P>=v?h=v+7-P:h=v-P;var y=1;if(g>h){var W=g-h;y=y+Math.ceil(W/7)}return y}},function(l,i,e){i.a=n;var t=e(142),a=e(171),r=e(62),o=e(0);function n(s,u){return o.a(1,arguments),t.a(a.a(s),r.a(s),u)+1}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=t.a(o),s=n.getFullYear();return s}},function(l,i,e){i.a=c;var t=e(27),a=e(147),r=e(60),o=e(86),n=e(144),s=e(146),u=e(61),d=e(15),m=e(0),f=e(1),_=e(172);function c(v){var g=v.start,P=v.end;m.a(1,arguments);var h=f.a(g),y=f.a(P);if(!d.a(h))throw new RangeError("Start Date is invalid");if(!d.a(y))throw new RangeError("End Date is invalid");var W={years:0,months:0,days:0,hours:0,minutes:0,seconds:0},T=t.a(h,y);W.years=Math.abs(a.a(h,y));var j=_.a(h,{years:T*W.years});W.months=Math.abs(r.a(j,y));var b=_.a(j,{months:T*W.months});W.days=Math.abs(o.a(b,y));var C=_.a(b,{days:T*W.days});W.hours=Math.abs(n.a(C,y));var I=_.a(C,{hours:T*W.hours});W.minutes=Math.abs(s.a(I,y));var M=_.a(I,{minutes:T*W.minutes});return W.seconds=Math.abs(u.a(M,y)),W}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o,n){a.a(2,arguments);var s=t.a(o),u=t.a(n);return s.getTime()>u.getTime()}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o,n){a.a(2,arguments);var s=t.a(o),u=t.a(n);return s.getTime()Date.now()}},function(l,i,e){i.a=n;var t=e(1),a=e(91),r=e(89),o=e(0);function n(s){o.a(1,arguments);var u=t.a(s);return a.a(u).getTime()===r.a(u).getTime()}},function(l,i,e){i.a=o;var t=e(174),a=e(15),r=e(0);function o(n,s,u){return r.a(2,arguments),a.a(t.a(n,s,new Date,u))}},function(l,i,e){var t=e(92),a=e(382),r=e(383),o=e(384),n=e(385),s=e(63),u=e(50),d=36e5,m=6e4,f=1e3,_={month:/^(1[0-2]|0?\d)/,date:/^(3[0-1]|[0-2]?\d)/,dayOfYear:/^(36[0-6]|3[0-5]\d|[0-2]?\d?\d)/,week:/^(5[0-3]|[0-4]?\d)/,hour23h:/^(2[0-3]|[0-1]?\d)/,hour24h:/^(2[0-4]|[0-1]?\d)/,hour11h:/^(1[0-1]|0?\d)/,hour12h:/^(1[0-2]|0?\d)/,minute:/^[0-5]?\d/,second:/^[0-5]?\d/,singleDigit:/^\d/,twoDigits:/^\d{1,2}/,threeDigits:/^\d{1,3}/,fourDigits:/^\d{1,4}/,anyDigitsSigned:/^-?\d+/,singleDigitSigned:/^-?\d/,twoDigitsSigned:/^-?\d{1,2}/,threeDigitsSigned:/^-?\d{1,3}/,fourDigitsSigned:/^-?\d{1,4}/},c={basicOptionalMinutes:/^([+-])(\d{2})(\d{2})?|Z/,basic:/^([+-])(\d{2})(\d{2})|Z/,basicOptionalSeconds:/^([+-])(\d{2})(\d{2})((\d{2}))?|Z/,extended:/^([+-])(\d{2}):(\d{2})|Z/,extendedOptionalSeconds:/^([+-])(\d{2}):(\d{2})(:(\d{2}))?|Z/};function v(M,D,p){var w=D.match(M);if(!w)return null;var L=parseInt(w[0],10);return{value:p?p(L):L,rest:D.slice(w[0].length)}}function g(M,D){var p=D.match(M);if(!p)return null;if(p[0]==="Z")return{value:0,rest:D.slice(1)};var w=p[1]==="+"?1:-1,L=p[2]?parseInt(p[2],10):0,z=p[3]?parseInt(p[3],10):0,ne=p[5]?parseInt(p[5],10):0;return{value:w*(L*d+z*m+ne*f),rest:D.slice(p[0].length)}}function P(M,D){return v(_.anyDigitsSigned,M,D)}function h(M,D,p){switch(M){case 1:return v(_.singleDigit,D,p);case 2:return v(_.twoDigits,D,p);case 3:return v(_.threeDigits,D,p);case 4:return v(_.fourDigits,D,p);default:return v(new RegExp("^\\d{1,"+M+"}"),D,p)}}function y(M,D,p){switch(M){case 1:return v(_.singleDigitSigned,D,p);case 2:return v(_.twoDigitsSigned,D,p);case 3:return v(_.threeDigitsSigned,D,p);case 4:return v(_.fourDigitsSigned,D,p);default:return v(new RegExp("^-?\\d{1,"+M+"}"),D,p)}}function W(M){switch(M){case"morning":return 4;case"evening":return 17;case"pm":case"noon":case"afternoon":return 12;case"am":case"midnight":case"night":default:return 0}}function T(M,D){var p=D>0,w=p?D:1-D,L;if(w<=50)L=M||100;else{var z=w+50,ne=Math.floor(z/100)*100,te=M>=z%100;L=M+ne-(te?100:0)}return p?L:1-L}var j=[31,28,31,30,31,30,31,31,30,31,30,31],b=[31,29,31,30,31,30,31,31,30,31,30,31];function C(M){return M%400===0||M%4===0&&M%100!==0}var I={G:{priority:140,parse:function(M,D,p,w){switch(D){case"G":case"GG":case"GGG":return p.era(M,{width:"abbreviated"})||p.era(M,{width:"narrow"});case"GGGGG":return p.era(M,{width:"narrow"});case"GGGG":default:return p.era(M,{width:"wide"})||p.era(M,{width:"abbreviated"})||p.era(M,{width:"narrow"})}},set:function(M,D,p,w){return D.era=p,M.setUTCFullYear(p,0,1),M.setUTCHours(0,0,0,0),M},incompatibleTokens:["R","u","t","T"]},y:{priority:130,parse:function(M,D,p,w){var L=function(z){return{year:z,isTwoDigitYear:D==="yy"}};switch(D){case"y":return h(4,M,L);case"yo":return p.ordinalNumber(M,{unit:"year",valueCallback:L});default:return h(D.length,M,L)}},validate:function(M,D,p){return D.isTwoDigitYear||D.year>0},set:function(M,D,p,w){var L=M.getUTCFullYear();if(p.isTwoDigitYear){var z=T(p.year,L);return M.setUTCFullYear(z,0,1),M.setUTCHours(0,0,0,0),M}var ne=!("era"in D)||D.era===1?p.year:1-p.year;return M.setUTCFullYear(ne,0,1),M.setUTCHours(0,0,0,0),M},incompatibleTokens:["Y","R","u","w","I","i","e","c","t","T"]},Y:{priority:130,parse:function(M,D,p,w){var L=function(z){return{year:z,isTwoDigitYear:D==="YY"}};switch(D){case"Y":return h(4,M,L);case"Yo":return p.ordinalNumber(M,{unit:"year",valueCallback:L});default:return h(D.length,M,L)}},validate:function(M,D,p){return D.isTwoDigitYear||D.year>0},set:function(M,D,p,w){var L=t.a(M,w);if(p.isTwoDigitYear){var z=T(p.year,L);return M.setUTCFullYear(z,0,w.firstWeekContainsDate),M.setUTCHours(0,0,0,0),u.a(M,w)}var ne=!("era"in D)||D.era===1?p.year:1-p.year;return M.setUTCFullYear(ne,0,w.firstWeekContainsDate),M.setUTCHours(0,0,0,0),u.a(M,w)},incompatibleTokens:["y","R","u","Q","q","M","L","I","d","D","i","t","T"]},R:{priority:130,parse:function(M,D,p,w){return y(D==="R"?4:D.length,M)},set:function(M,D,p,w){var L=new Date(0);return L.setUTCFullYear(p,0,4),L.setUTCHours(0,0,0,0),s.a(L)},incompatibleTokens:["G","y","Y","u","Q","q","M","L","w","d","D","e","c","t","T"]},u:{priority:130,parse:function(M,D,p,w){return y(D==="u"?4:D.length,M)},set:function(M,D,p,w){return M.setUTCFullYear(p,0,1),M.setUTCHours(0,0,0,0),M},incompatibleTokens:["G","y","Y","R","w","I","i","e","c","t","T"]},Q:{priority:120,parse:function(M,D,p,w){switch(D){case"Q":case"QQ":return h(D.length,M);case"Qo":return p.ordinalNumber(M,{unit:"quarter"});case"QQQ":return p.quarter(M,{width:"abbreviated",context:"formatting"})||p.quarter(M,{width:"narrow",context:"formatting"});case"QQQQQ":return p.quarter(M,{width:"narrow",context:"formatting"});case"QQQQ":default:return p.quarter(M,{width:"wide",context:"formatting"})||p.quarter(M,{width:"abbreviated",context:"formatting"})||p.quarter(M,{width:"narrow",context:"formatting"})}},validate:function(M,D,p){return D>=1&&D<=4},set:function(M,D,p,w){return M.setUTCMonth((p-1)*3,1),M.setUTCHours(0,0,0,0),M},incompatibleTokens:["Y","R","q","M","L","w","I","d","D","i","e","c","t","T"]},q:{priority:120,parse:function(M,D,p,w){switch(D){case"q":case"qq":return h(D.length,M);case"qo":return p.ordinalNumber(M,{unit:"quarter"});case"qqq":return p.quarter(M,{width:"abbreviated",context:"standalone"})||p.quarter(M,{width:"narrow",context:"standalone"});case"qqqqq":return p.quarter(M,{width:"narrow",context:"standalone"});case"qqqq":default:return p.quarter(M,{width:"wide",context:"standalone"})||p.quarter(M,{width:"abbreviated",context:"standalone"})||p.quarter(M,{width:"narrow",context:"standalone"})}},validate:function(M,D,p){return D>=1&&D<=4},set:function(M,D,p,w){return M.setUTCMonth((p-1)*3,1),M.setUTCHours(0,0,0,0),M},incompatibleTokens:["Y","R","Q","M","L","w","I","d","D","i","e","c","t","T"]},M:{priority:110,parse:function(M,D,p,w){var L=function(z){return z-1};switch(D){case"M":return v(_.month,M,L);case"MM":return h(2,M,L);case"Mo":return p.ordinalNumber(M,{unit:"month",valueCallback:L});case"MMM":return p.month(M,{width:"abbreviated",context:"formatting"})||p.month(M,{width:"narrow",context:"formatting"});case"MMMMM":return p.month(M,{width:"narrow",context:"formatting"});case"MMMM":default:return p.month(M,{width:"wide",context:"formatting"})||p.month(M,{width:"abbreviated",context:"formatting"})||p.month(M,{width:"narrow",context:"formatting"})}},validate:function(M,D,p){return D>=0&&D<=11},set:function(M,D,p,w){return M.setUTCMonth(p,1),M.setUTCHours(0,0,0,0),M},incompatibleTokens:["Y","R","q","Q","L","w","I","D","i","e","c","t","T"]},L:{priority:110,parse:function(M,D,p,w){var L=function(z){return z-1};switch(D){case"L":return v(_.month,M,L);case"LL":return h(2,M,L);case"Lo":return p.ordinalNumber(M,{unit:"month",valueCallback:L});case"LLL":return p.month(M,{width:"abbreviated",context:"standalone"})||p.month(M,{width:"narrow",context:"standalone"});case"LLLLL":return p.month(M,{width:"narrow",context:"standalone"});case"LLLL":default:return p.month(M,{width:"wide",context:"standalone"})||p.month(M,{width:"abbreviated",context:"standalone"})||p.month(M,{width:"narrow",context:"standalone"})}},validate:function(M,D,p){return D>=0&&D<=11},set:function(M,D,p,w){return M.setUTCMonth(p,1),M.setUTCHours(0,0,0,0),M},incompatibleTokens:["Y","R","q","Q","M","w","I","D","i","e","c","t","T"]},w:{priority:100,parse:function(M,D,p,w){switch(D){case"w":return v(_.week,M);case"wo":return p.ordinalNumber(M,{unit:"week"});default:return h(D.length,M)}},validate:function(M,D,p){return D>=1&&D<=53},set:function(M,D,p,w){return u.a(n.a(M,p,w),w)},incompatibleTokens:["y","R","u","q","Q","M","L","I","d","D","i","t","T"]},I:{priority:100,parse:function(M,D,p,w){switch(D){case"I":return v(_.week,M);case"Io":return p.ordinalNumber(M,{unit:"week"});default:return h(D.length,M)}},validate:function(M,D,p){return D>=1&&D<=53},set:function(M,D,p,w){return s.a(o.a(M,p,w),w)},incompatibleTokens:["y","Y","u","q","Q","M","L","w","d","D","e","c","t","T"]},d:{priority:90,subPriority:1,parse:function(M,D,p,w){switch(D){case"d":return v(_.date,M);case"do":return p.ordinalNumber(M,{unit:"date"});default:return h(D.length,M)}},validate:function(M,D,p){var w=M.getUTCFullYear(),L=C(w),z=M.getUTCMonth();return L?D>=1&&D<=b[z]:D>=1&&D<=j[z]},set:function(M,D,p,w){return M.setUTCDate(p),M.setUTCHours(0,0,0,0),M},incompatibleTokens:["Y","R","q","Q","w","I","D","i","e","c","t","T"]},D:{priority:90,subPriority:1,parse:function(M,D,p,w){switch(D){case"D":case"DD":return v(_.dayOfYear,M);case"Do":return p.ordinalNumber(M,{unit:"date"});default:return h(D.length,M)}},validate:function(M,D,p){var w=M.getUTCFullYear(),L=C(w);return L?D>=1&&D<=366:D>=1&&D<=365},set:function(M,D,p,w){return M.setUTCMonth(0,p),M.setUTCHours(0,0,0,0),M},incompatibleTokens:["Y","R","q","Q","M","L","w","I","d","E","i","e","c","t","T"]},E:{priority:90,parse:function(M,D,p,w){switch(D){case"E":case"EE":case"EEE":return p.day(M,{width:"abbreviated",context:"formatting"})||p.day(M,{width:"short",context:"formatting"})||p.day(M,{width:"narrow",context:"formatting"});case"EEEEE":return p.day(M,{width:"narrow",context:"formatting"});case"EEEEEE":return p.day(M,{width:"short",context:"formatting"})||p.day(M,{width:"narrow",context:"formatting"});case"EEEE":default:return p.day(M,{width:"wide",context:"formatting"})||p.day(M,{width:"abbreviated",context:"formatting"})||p.day(M,{width:"short",context:"formatting"})||p.day(M,{width:"narrow",context:"formatting"})}},validate:function(M,D,p){return D>=0&&D<=6},set:function(M,D,p,w){return M=a.a(M,p,w),M.setUTCHours(0,0,0,0),M},incompatibleTokens:["D","i","e","c","t","T"]},e:{priority:90,parse:function(M,D,p,w){var L=function(z){var ne=Math.floor((z-1)/7)*7;return(z+w.weekStartsOn+6)%7+ne};switch(D){case"e":case"ee":return h(D.length,M,L);case"eo":return p.ordinalNumber(M,{unit:"day",valueCallback:L});case"eee":return p.day(M,{width:"abbreviated",context:"formatting"})||p.day(M,{width:"short",context:"formatting"})||p.day(M,{width:"narrow",context:"formatting"});case"eeeee":return p.day(M,{width:"narrow",context:"formatting"});case"eeeeee":return p.day(M,{width:"short",context:"formatting"})||p.day(M,{width:"narrow",context:"formatting"});case"eeee":default:return p.day(M,{width:"wide",context:"formatting"})||p.day(M,{width:"abbreviated",context:"formatting"})||p.day(M,{width:"short",context:"formatting"})||p.day(M,{width:"narrow",context:"formatting"})}},validate:function(M,D,p){return D>=0&&D<=6},set:function(M,D,p,w){return M=a.a(M,p,w),M.setUTCHours(0,0,0,0),M},incompatibleTokens:["y","R","u","q","Q","M","L","I","d","D","E","i","c","t","T"]},c:{priority:90,parse:function(M,D,p,w){var L=function(z){var ne=Math.floor((z-1)/7)*7;return(z+w.weekStartsOn+6)%7+ne};switch(D){case"c":case"cc":return h(D.length,M,L);case"co":return p.ordinalNumber(M,{unit:"day",valueCallback:L});case"ccc":return p.day(M,{width:"abbreviated",context:"standalone"})||p.day(M,{width:"short",context:"standalone"})||p.day(M,{width:"narrow",context:"standalone"});case"ccccc":return p.day(M,{width:"narrow",context:"standalone"});case"cccccc":return p.day(M,{width:"short",context:"standalone"})||p.day(M,{width:"narrow",context:"standalone"});case"cccc":default:return p.day(M,{width:"wide",context:"standalone"})||p.day(M,{width:"abbreviated",context:"standalone"})||p.day(M,{width:"short",context:"standalone"})||p.day(M,{width:"narrow",context:"standalone"})}},validate:function(M,D,p){return D>=0&&D<=6},set:function(M,D,p,w){return M=a.a(M,p,w),M.setUTCHours(0,0,0,0),M},incompatibleTokens:["y","R","u","q","Q","M","L","I","d","D","E","i","e","t","T"]},i:{priority:90,parse:function(M,D,p,w){var L=function(z){return z===0?7:z};switch(D){case"i":case"ii":return h(D.length,M);case"io":return p.ordinalNumber(M,{unit:"day"});case"iii":return p.day(M,{width:"abbreviated",context:"formatting",valueCallback:L})||p.day(M,{width:"short",context:"formatting",valueCallback:L})||p.day(M,{width:"narrow",context:"formatting",valueCallback:L});case"iiiii":return p.day(M,{width:"narrow",context:"formatting",valueCallback:L});case"iiiiii":return p.day(M,{width:"short",context:"formatting",valueCallback:L})||p.day(M,{width:"narrow",context:"formatting",valueCallback:L});case"iiii":default:return p.day(M,{width:"wide",context:"formatting",valueCallback:L})||p.day(M,{width:"abbreviated",context:"formatting",valueCallback:L})||p.day(M,{width:"short",context:"formatting",valueCallback:L})||p.day(M,{width:"narrow",context:"formatting",valueCallback:L})}},validate:function(M,D,p){return D>=1&&D<=7},set:function(M,D,p,w){return M=r.a(M,p,w),M.setUTCHours(0,0,0,0),M},incompatibleTokens:["y","Y","u","q","Q","M","L","w","d","D","E","e","c","t","T"]},a:{priority:80,parse:function(M,D,p,w){switch(D){case"a":case"aa":case"aaa":return p.dayPeriod(M,{width:"abbreviated",context:"formatting"})||p.dayPeriod(M,{width:"narrow",context:"formatting"});case"aaaaa":return p.dayPeriod(M,{width:"narrow",context:"formatting"});case"aaaa":default:return p.dayPeriod(M,{width:"wide",context:"formatting"})||p.dayPeriod(M,{width:"abbreviated",context:"formatting"})||p.dayPeriod(M,{width:"narrow",context:"formatting"})}},set:function(M,D,p,w){return M.setUTCHours(W(p),0,0,0),M},incompatibleTokens:["b","B","H","K","k","t","T"]},b:{priority:80,parse:function(M,D,p,w){switch(D){case"b":case"bb":case"bbb":return p.dayPeriod(M,{width:"abbreviated",context:"formatting"})||p.dayPeriod(M,{width:"narrow",context:"formatting"});case"bbbbb":return p.dayPeriod(M,{width:"narrow",context:"formatting"});case"bbbb":default:return p.dayPeriod(M,{width:"wide",context:"formatting"})||p.dayPeriod(M,{width:"abbreviated",context:"formatting"})||p.dayPeriod(M,{width:"narrow",context:"formatting"})}},set:function(M,D,p,w){return M.setUTCHours(W(p),0,0,0),M},incompatibleTokens:["a","B","H","K","k","t","T"]},B:{priority:80,parse:function(M,D,p,w){switch(D){case"B":case"BB":case"BBB":return p.dayPeriod(M,{width:"abbreviated",context:"formatting"})||p.dayPeriod(M,{width:"narrow",context:"formatting"});case"BBBBB":return p.dayPeriod(M,{width:"narrow",context:"formatting"});case"BBBB":default:return p.dayPeriod(M,{width:"wide",context:"formatting"})||p.dayPeriod(M,{width:"abbreviated",context:"formatting"})||p.dayPeriod(M,{width:"narrow",context:"formatting"})}},set:function(M,D,p,w){return M.setUTCHours(W(p),0,0,0),M},incompatibleTokens:["a","b","t","T"]},h:{priority:70,parse:function(M,D,p,w){switch(D){case"h":return v(_.hour12h,M);case"ho":return p.ordinalNumber(M,{unit:"hour"});default:return h(D.length,M)}},validate:function(M,D,p){return D>=1&&D<=12},set:function(M,D,p,w){var L=M.getUTCHours()>=12;return L&&p<12?M.setUTCHours(p+12,0,0,0):!L&&p===12?M.setUTCHours(0,0,0,0):M.setUTCHours(p,0,0,0),M},incompatibleTokens:["H","K","k","t","T"]},H:{priority:70,parse:function(M,D,p,w){switch(D){case"H":return v(_.hour23h,M);case"Ho":return p.ordinalNumber(M,{unit:"hour"});default:return h(D.length,M)}},validate:function(M,D,p){return D>=0&&D<=23},set:function(M,D,p,w){return M.setUTCHours(p,0,0,0),M},incompatibleTokens:["a","b","h","K","k","t","T"]},K:{priority:70,parse:function(M,D,p,w){switch(D){case"K":return v(_.hour11h,M);case"Ko":return p.ordinalNumber(M,{unit:"hour"});default:return h(D.length,M)}},validate:function(M,D,p){return D>=0&&D<=11},set:function(M,D,p,w){var L=M.getUTCHours()>=12;return L&&p<12?M.setUTCHours(p+12,0,0,0):M.setUTCHours(p,0,0,0),M},incompatibleTokens:["a","b","h","H","k","t","T"]},k:{priority:70,parse:function(M,D,p,w){switch(D){case"k":return v(_.hour24h,M);case"ko":return p.ordinalNumber(M,{unit:"hour"});default:return h(D.length,M)}},validate:function(M,D,p){return D>=1&&D<=24},set:function(M,D,p,w){var L=p<=24?p%24:p;return M.setUTCHours(L,0,0,0),M},incompatibleTokens:["a","b","h","H","K","t","T"]},m:{priority:60,parse:function(M,D,p,w){switch(D){case"m":return v(_.minute,M);case"mo":return p.ordinalNumber(M,{unit:"minute"});default:return h(D.length,M)}},validate:function(M,D,p){return D>=0&&D<=59},set:function(M,D,p,w){return M.setUTCMinutes(p,0,0),M},incompatibleTokens:["t","T"]},s:{priority:50,parse:function(M,D,p,w){switch(D){case"s":return v(_.second,M);case"so":return p.ordinalNumber(M,{unit:"second"});default:return h(D.length,M)}},validate:function(M,D,p){return D>=0&&D<=59},set:function(M,D,p,w){return M.setUTCSeconds(p,0),M},incompatibleTokens:["t","T"]},S:{priority:30,parse:function(M,D,p,w){var L=function(z){return Math.floor(z*Math.pow(10,-D.length+3))};return h(D.length,M,L)},set:function(M,D,p,w){return M.setUTCMilliseconds(p),M},incompatibleTokens:["t","T"]},X:{priority:10,parse:function(M,D,p,w){switch(D){case"X":return g(c.basicOptionalMinutes,M);case"XX":return g(c.basic,M);case"XXXX":return g(c.basicOptionalSeconds,M);case"XXXXX":return g(c.extendedOptionalSeconds,M);case"XXX":default:return g(c.extended,M)}},set:function(M,D,p,w){return D.timestampIsSet?M:new Date(M.getTime()-p)},incompatibleTokens:["t","T","x"]},x:{priority:10,parse:function(M,D,p,w){switch(D){case"x":return g(c.basicOptionalMinutes,M);case"xx":return g(c.basic,M);case"xxxx":return g(c.basicOptionalSeconds,M);case"xxxxx":return g(c.extendedOptionalSeconds,M);case"xxx":default:return g(c.extended,M)}},set:function(M,D,p,w){return D.timestampIsSet?M:new Date(M.getTime()-p)},incompatibleTokens:["t","T","X"]},t:{priority:40,parse:function(M,D,p,w){return P(M)},set:function(M,D,p,w){return[new Date(p*1e3),{timestampIsSet:!0}]},incompatibleTokens:"*"},T:{priority:20,parse:function(M,D,p,w){return P(M)},set:function(M,D,p,w){return[new Date(p),{timestampIsSet:!0}]},incompatibleTokens:"*"}};i.a=I},function(l,i,e){i.a=o;var t=e(6),a=e(1),r=e(0);function o(n,s,u){r.a(2,arguments);var d=u||{},m=d.locale,f=m&&m.options&&m.options.weekStartsOn,_=f==null?0:t.a(f),c=d.weekStartsOn==null?_:t.a(d.weekStartsOn);if(!(c>=0&&c<=6))throw new RangeError("weekStartsOn must be between 0 and 6 inclusively");var v=a.a(n),g=t.a(s),P=v.getUTCDay(),h=g%7,y=(h+7)%7,W=(y=d&&u<=m}},function(l,i,e){i.a=o;var t=e(47),a=e(94),r=e(0);function o(n){return r.a(1,arguments),t.a(n,a.a(Date.now(),1))}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=t.a(o),s=n.getFullYear(),u=9+Math.floor(s/10)*10;return n.setFullYear(u+1,0,0),n.setHours(0,0,0,0),n}},function(l,i,e){i.a=r;var t=e(185),a=e(0);function r(o){return a.a(1,arguments),t.a(o,{weekStartsOn:1})}},function(l,i,e){i.a=o;var t=e(34),a=e(25),r=e(0);function o(n){r.a(1,arguments);var s=t.a(n),u=new Date(0);u.setFullYear(s+1,0,4),u.setHours(0,0,0,0);var d=a.a(u);return d.setDate(d.getDate()-1),d}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=t.a(o),s=n.getMonth(),u=s-s%3+3;return n.setMonth(u,0),n.setHours(0,0,0,0),n}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=t.a(o),s=n.getFullYear();return n.setFullYear(s+1,0,0),n.setHours(0,0,0,0),n}},function(l,i,e){i.a=_;var t=e(1),a=e(152),r=e(19),o=e(15),n=e(49),s=e(0),u=/(\w)\1*|''|'(''|[^'])+('|$)|./g,d=/^'([^]*?)'?$/,m=/''/g,f=/[a-zA-Z]/;function _(v,g){s.a(2,arguments);var P=String(g),h=t.a(v);if(!o.a(h))throw new RangeError("Invalid time value");var y=r.a(h),W=n.a(h,y),T=P.match(u).map(function(j){if(j==="''")return"'";var b=j[0];if(b==="'")return c(j);var C=a.a[b];if(C)return C(W,j,null,{});if(b.match(f))throw new RangeError("Format string contains an unescaped latin alphabet character `"+b+"`");return j}).join("");return T}function c(v){return v.match(d)[1].replace(m,"'")}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n;if(o&&typeof o.forEach=="function")n=o;else if(typeof o=="object"&&o!==null)n=Array.prototype.slice.call(o);else return new Date(NaN);var s;return n.forEach(function(u){var d=t.a(u);(s===void 0||sd||isNaN(d))&&(s=d)}),s||new Date(NaN)}},function(l,i,e){i.a=f;var t=e(6),a=e(0),r=36e5,o=6e4,n=2,s={dateTimeDelimiter:/[T ]/,timeZoneDelimiter:/[Z ]/i,timezone:/([Z+-].*)$/},u=/^-?(?:(\d{3})|(\d{2})(?:-?(\d{2}))?|W(\d{2})(?:-?(\d{1}))?|)$/,d=/^(\d{2}(?:[.,]\d*)?)(?::?(\d{2}(?:[.,]\d*)?))?(?::?(\d{2}(?:[.,]\d*)?))?$/,m=/^([+-])(\d{2})(?::?(\d{2}))?$/;function f(p,w){a.a(1,arguments);var L=w||{},z=L.additionalDigits==null?n:t.a(L.additionalDigits);if(z!==2&&z!==1&&z!==0)throw new RangeError("additionalDigits must be 0, 1 or 2");if(!(typeof p=="string"||Object.prototype.toString.call(p)==="[object String]"))return new Date(NaN);var ne=_(p),te;if(ne.date){var de=c(ne.date,z);te=v(de.restDateString,de.year)}if(isNaN(te)||!te)return new Date(NaN);var ue=te.getTime(),oe=0,ie;if(ne.time&&(oe=P(ne.time),isNaN(oe)||oe===null))return new Date(NaN);if(ne.timezone){if(ie=y(ne.timezone),isNaN(ie))return new Date(NaN)}else{var le=new Date(ue+oe),F=new Date(le.getUTCFullYear(),le.getUTCMonth(),le.getUTCDate(),le.getUTCHours(),le.getUTCMinutes(),le.getUTCSeconds(),le.getUTCMilliseconds());return F.setFullYear(le.getUTCFullYear()),F}return new Date(ue+oe+ie)}function _(p){var w={},L=p.split(s.dateTimeDelimiter),z;if(L.length>2)return w;if(/:/.test(L[0])?(w.date=null,z=L[0]):(w.date=L[0],z=L[1],s.timeZoneDelimiter.test(w.date)&&(w.date=p.split(s.timeZoneDelimiter)[0],z=p.substr(w.date.length,p.length))),z){var ne=s.timezone.exec(z);ne?(w.time=z.replace(ne[1],""),w.timezone=ne[1]):w.time=z}return w}function c(p,w){var L=new RegExp("^(?:(\\d{4}|[+-]\\d{"+(4+w)+"})|(\\d{2}|[+-]\\d{"+(2+w)+"})$)"),z=p.match(L);if(!z)return{year:null};var ne=z[1]&&parseInt(z[1]),te=z[2]&&parseInt(z[2]);return{year:te==null?ne:te*100,restDateString:p.slice((z[1]||z[2]).length)}}function v(p,w){if(w===null)return null;var L=p.match(u);if(!L)return null;var z=!!L[4],ne=g(L[1]),te=g(L[2])-1,de=g(L[3]),ue=g(L[4]),oe=g(L[5])-1;if(z)return I(w,ue,oe)?W(w,ue,oe):new Date(NaN);var ie=new Date(0);return!b(w,te,de)||!C(w,ne)?new Date(NaN):(ie.setUTCFullYear(w,te,Math.max(ne,de)),ie)}function g(p){return p?parseInt(p):1}function P(p){var w=p.match(d);if(!w)return null;var L=h(w[1]),z=h(w[2]),ne=h(w[3]);return M(L,z,ne)?L*r+z*o+ne*1e3:NaN}function h(p){return p&&parseFloat(p.replace(",","."))||0}function y(p){if(p==="Z")return 0;var w=p.match(m);if(!w)return 0;var L=w[1]==="+"?-1:1,z=parseInt(w[2]),ne=w[3]&&parseInt(w[3])||0;return D(z,ne)?L*(z*r+ne*o):NaN}function W(p,w,L){var z=new Date(0);z.setUTCFullYear(p,0,4);var ne=z.getUTCDay()||7,te=(w-1)*7+L+1-ne;return z.setUTCDate(z.getUTCDate()+te),z}var T=[31,null,31,30,31,30,31,31,30,31,30,31];function j(p){return p%400===0||p%4===0&&p%100}function b(p,w,L){return w>=0&&w<=11&&L>=1&&L<=(T[w]||(j(p)?29:28))}function C(p,w){return w>=1&&w<=(j(p)?366:365)}function I(p,w,L){return w>=1&&w<=53&&L>=0&&L<=6}function M(p,w,L){return p===24?w===0&&L===0:L>=0&&L<60&&w>=0&&w<60&&p>=0&&p<25}function D(p,w){return w>=0&&w<=59}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){if(a.a(1,arguments),typeof o=="string"){var n=o.match(/(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})(?:\.(\d{0,7}))?(?:Z|\+00:?00)?/);return n?new Date(Date.UTC(+n[1],n[2]-1,+n[3],+n[4],+n[5],+n[6],+((n[7]||"0")+"00").substring(0,3))):new Date(NaN)}return t.a(o)}},function(l,i,e){i.a=r;var t=e(1),a=e(6);function r(o,n){if(arguments.length<1)throw new TypeError("1 argument required, but only none provided present");var s=n&&"nearestTo"in n?a.a(n.nearestTo):1;if(s<1||s>30)throw new RangeError("`options.nearestTo` must be between 1 and 30");var u=t.a(o),d=u.getSeconds(),m=u.getMinutes()+d/60,f=Math.floor(m/s)*s,_=m%s,c=Math.round(_/s)*s;return new Date(u.getFullYear(),u.getMonth(),u.getDate(),u.getHours(),f+c)}},function(l,i,e){i.a=n;var t=e(1),a=e(96),r=e(6),o=e(0);function n(s,u){if(o.a(2,arguments),typeof u!="object"||u===null)throw new RangeError("values parameter must be an object");var d=t.a(s);return isNaN(d)?new Date(NaN):(u.year!=null&&d.setFullYear(u.year),u.month!=null&&(d=a.a(d,u.month)),u.date!=null&&d.setDate(r.a(u.date)),u.hours!=null&&d.setHours(r.a(u.hours)),u.minutes!=null&&d.setMinutes(r.a(u.minutes)),u.seconds!=null&&d.setSeconds(r.a(u.seconds)),u.milliseconds!=null&&d.setMilliseconds(r.a(u.milliseconds)),d)}},function(l,i,e){i.a=o;var t=e(6),a=e(1),r=e(0);function o(n,s){r.a(2,arguments);var u=a.a(n),d=t.a(s);return u.setDate(d),u}},function(l,i,e){i.a=n;var t=e(21),a=e(1),r=e(6),o=e(0);function n(s,u,d){o.a(2,arguments);var m=d||{},f=m.locale,_=f&&f.options&&f.options.weekStartsOn,c=_==null?0:r.a(_),v=m.weekStartsOn==null?c:r.a(m.weekStartsOn);if(!(v>=0&&v<=6))throw new RangeError("weekStartsOn must be between 0 and 6 inclusively");var g=a.a(s,m),P=r.a(u),h=g.getDay(),y=P%7,W=(y+7)%7,T=7-v,j=P<0||P>6?P-(h+T)%7:(W+T)%7-(h+T)%7;return t.a(g,j,m)}},function(l,i,e){i.a=o;var t=e(6),a=e(1),r=e(0);function o(n,s){r.a(2,arguments);var u=a.a(n),d=t.a(s);return u.setMonth(0),u.setDate(d),u}},function(l,i,e){i.a=o;var t=e(6),a=e(1),r=e(0);function o(n,s){r.a(2,arguments);var u=a.a(n),d=t.a(s);return u.setHours(d),u}},function(l,i,e){i.a=s;var t=e(6),a=e(1),r=e(21),o=e(166),n=e(0);function s(u,d){n.a(2,arguments);var m=a.a(u),f=t.a(d),_=o.a(m),c=f-_;return r.a(m,c)}},function(l,i,e){i.a=n;var t=e(6),a=e(1),r=e(167),o=e(0);function n(s,u){o.a(2,arguments);var d=a.a(s),m=t.a(u),f=r.a(d)-m;return d.setDate(d.getDate()-f*7),d}},function(l,i,e){i.a=o;var t=e(6),a=e(1),r=e(0);function o(n,s){r.a(2,arguments);var u=a.a(n),d=t.a(s);return u.setMilliseconds(d),u}},function(l,i,e){i.a=o;var t=e(6),a=e(1),r=e(0);function o(n,s){r.a(2,arguments);var u=a.a(n),d=t.a(s);return u.setMinutes(d),u}},function(l,i,e){i.a=n;var t=e(6),a=e(1),r=e(96),o=e(0);function n(s,u){o.a(2,arguments);var d=a.a(s),m=t.a(u),f=Math.floor(d.getMonth()/3)+1,_=m-f;return r.a(d,d.getMonth()+_*3)}},function(l,i,e){i.a=o;var t=e(6),a=e(1),r=e(0);function o(n,s){r.a(2,arguments);var u=a.a(n),d=t.a(s);return u.setSeconds(d),u}},function(l,i,e){i.a=n;var t=e(169),a=e(1),r=e(6),o=e(0);function n(s,u,d){o.a(2,arguments);var m=a.a(s),f=r.a(u),_=t.a(m,d)-f;return m.setDate(m.getDate()-_*7),m}},function(l,i,e){i.a=s;var t=e(26),a=e(93),r=e(1),o=e(6),n=e(0);function s(u,d,m){n.a(2,arguments);var f=m||{},_=f.locale,c=_&&_.options&&_.options.firstWeekContainsDate,v=c==null?1:o.a(c),g=f.firstWeekContainsDate==null?v:o.a(f.firstWeekContainsDate),P=r.a(u),h=o.a(d),y=t.a(P,a.a(P,m)),W=new Date(0);return W.setFullYear(h,0,g),W.setHours(0,0,0,0),P=a.a(W,m),P.setDate(P.getDate()+y),P}},function(l,i,e){i.a=o;var t=e(6),a=e(1),r=e(0);function o(n,s){r.a(2,arguments);var u=a.a(n),d=t.a(s);return isNaN(u)?new Date(NaN):(u.setFullYear(d),u)}},function(l,i,e){i.a=r;var t=e(1),a=e(0);function r(o){a.a(1,arguments);var n=t.a(o),s=n.getFullYear(),u=Math.floor(s/10)*10;return n.setFullYear(u,0,1),n.setHours(0,0,0,0),n}},function(l,i,e){i.a=a;var t=e(57);function a(){return t.a(Date.now())}},function(l,i,e){i.a=t;function t(){var a=new Date,r=a.getFullYear(),o=a.getMonth(),n=a.getDate(),s=new Date(0);return s.setFullYear(r,o,n+1),s.setHours(0,0,0,0),s}},function(l,i,e){i.a=t;function t(){var a=new Date,r=a.getFullYear(),o=a.getMonth(),n=a.getDate(),s=new Date(0);return s.setFullYear(r,o,n-1),s.setHours(0,0,0,0),s}},function(l,i,e){i.a=o;var t=e(6),a=e(132),r=e(0);function o(n,s){r.a(2,arguments);var u=t.a(s);return a.a(n,-u)}},function(l,i,e){i.a=o;var t=e(6),a=e(84),r=e(0);function o(n,s){r.a(2,arguments);var u=t.a(s);return a.a(n,-u)}},function(l,i,e){i.a=o;var t=e(6),a=e(136),r=e(0);function o(n,s){r.a(2,arguments);var u=t.a(s);return a.a(n,-u)}},function(l,i,e){i.a=o;var t=e(6),a=e(85),r=e(0);function o(n,s){r.a(2,arguments);var u=t.a(s);return a.a(n,-u)}},function(l,i,e){i.a=o;var t=e(6),a=e(137),r=e(0);function o(n,s){r.a(2,arguments);var u=t.a(s);return a.a(n,-u)}},function(l,i,e){i.a=o;var t=e(6),a=e(58),r=e(0);function o(n,s){r.a(2,arguments);var u=t.a(s);return a.a(n,-u)}},function(l,i,e){i.a=o;var t=e(6),a=e(138),r=e(0);function o(n,s){r.a(2,arguments);var u=t.a(s);return a.a(n,-u)}},function(l,i,e){e.d(i,"a",function(){return t}),e.d(i,"b",function(){return a});var t=Math.pow(10,8)*24*60*60*1e3,a=-t},function(l,i,e){Object.defineProperty(i,"__esModule",{value:!0});var t=e(442);e.d(i,"af",function(){return t.a});var a=e(448);e.d(i,"arDZ",function(){return a.a});var r=e(454);e.d(i,"arMA",function(){return r.a});var o=e(460);e.d(i,"arSA",function(){return o.a});var n=e(466);e.d(i,"az",function(){return n.a});var s=e(472);e.d(i,"be",function(){return s.a});var u=e(478);e.d(i,"bg",function(){return u.a});var d=e(484);e.d(i,"bn",function(){return d.a});var m=e(489);e.d(i,"ca",function(){return m.a});var f=e(495);e.d(i,"cs",function(){return f.a});var _=e(501);e.d(i,"cy",function(){return _.a});var c=e(507);e.d(i,"da",function(){return c.a});var v=e(513);e.d(i,"de",function(){return v.a});var g=e(519);e.d(i,"el",function(){return g.a});var P=e(525);e.d(i,"enAU",function(){return P.a});var h=e(527);e.d(i,"enCA",function(){return h.a});var y=e(530);e.d(i,"enGB",function(){return y.a});var W=e(532);e.d(i,"enIN",function(){return W.a});var T=e(534);e.d(i,"enNZ",function(){return T.a});var j=e(28);e.d(i,"enUS",function(){return j.a});var b=e(536);e.d(i,"eo",function(){return b.a});var C=e(542);e.d(i,"es",function(){return C.a});var I=e(548);e.d(i,"et",function(){return I.a});var M=e(554);e.d(i,"eu",function(){return M.a});var D=e(560);e.d(i,"faIR",function(){return D.a});var p=e(566);e.d(i,"fi",function(){return p.a});var w=e(572);e.d(i,"fr",function(){return w.a});var L=e(574);e.d(i,"frCA",function(){return L.a});var z=e(576);e.d(i,"frCH",function(){return z.a});var ne=e(582);e.d(i,"gd",function(){return ne.a});var te=e(588);e.d(i,"gl",function(){return te.a});var de=e(594);e.d(i,"gu",function(){return de.a});var ue=e(600);e.d(i,"he",function(){return ue.a});var oe=e(606);e.d(i,"hi",function(){return oe.a});var ie=e(611);e.d(i,"hr",function(){return ie.a});var le=e(617);e.d(i,"hu",function(){return le.a});var F=e(623);e.d(i,"hy",function(){return F.a});var S=e(629);e.d(i,"id",function(){return S.a});var X=e(635);e.d(i,"is",function(){return X.a});var J=e(641);e.d(i,"it",function(){return J.a});var V=e(647);e.d(i,"ja",function(){return V.a});var E=e(653);e.d(i,"ka",function(){return E.a});var O=e(659);e.d(i,"kk",function(){return O.a});var B=e(665);e.d(i,"kn",function(){return B.a});var A=e(671);e.d(i,"ko",function(){return A.a});var k=e(677);e.d(i,"lb",function(){return k.a});var U=e(683);e.d(i,"lt",function(){return U.a});var G=e(689);e.d(i,"lv",function(){return G.a});var ee=e(695);e.d(i,"mk",function(){return ee.a});var q=e(701);e.d(i,"ms",function(){return q.a});var re=e(707);e.d(i,"mt",function(){return re.a});var ce=e(713);e.d(i,"nb",function(){return ce.a});var he=e(719);e.d(i,"nl",function(){return he.a});var fe=e(725);e.d(i,"nlBE",function(){return fe.a});var ge=e(731);e.d(i,"nn",function(){return ge.a});var Ee=e(737);e.d(i,"pl",function(){return Ee.a});var x=e(743);e.d(i,"pt",function(){return x.a});var N=e(749);e.d(i,"ptBR",function(){return N.a});var Y=e(755);e.d(i,"ro",function(){return Y.a});var $=e(761);e.d(i,"ru",function(){return $.a});var Q=e(767);e.d(i,"sk",function(){return Q.a});var ae=e(773);e.d(i,"sl",function(){return ae.a});var me=e(779);e.d(i,"sr",function(){return me.a});var ve=e(785);e.d(i,"srLatn",function(){return ve.a});var pe=e(791);e.d(i,"sv",function(){return pe.a});var K=e(797);e.d(i,"ta",function(){return K.a});var Z=e(803);e.d(i,"te",function(){return Z.a});var se=e(809);e.d(i,"th",function(){return se.a});var _e=e(815);e.d(i,"tr",function(){return _e.a});var Pe=e(821);e.d(i,"ug",function(){return Pe.a});var Me=e(827);e.d(i,"uk",function(){return Me.a});var De=e(833);e.d(i,"uz",function(){return De.a});var We=e(839);e.d(i,"vi",function(){return We.a});var we=e(845);e.d(i,"zhCN",function(){return we.a});var Ue=e(851);e.d(i,"zhTW",function(){return Ue.a})},function(l,i,e){var t=e(443),a=e(444),r=e(445),o=e(446),n=e(447),s={code:"af",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:0,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"minder as 'n sekonde",other:"minder as {{count}} sekondes"},xSeconds:{one:"1 sekonde",other:"{{count}} sekondes"},halfAMinute:"'n halwe minuut",lessThanXMinutes:{one:"minder as 'n minuut",other:"minder as {{count}} minute"},xMinutes:{one:"'n minuut",other:"{{count}} minute"},aboutXHours:{one:"ongeveer 1 uur",other:"ongeveer {{count}} ure"},xHours:{one:"1 uur",other:"{{count}} ure"},xDays:{one:"1 dag",other:"{{count}} dae"},aboutXWeeks:{one:"ongeveer 1 week",other:"ongeveer {{count}} weke"},xWeeks:{one:"1 week",other:"{{count}} weke"},aboutXMonths:{one:"ongeveer 1 maand",other:"ongeveer {{count}} maande"},xMonths:{one:"1 maand",other:"{{count}} maande"},aboutXYears:{one:"ongeveer 1 jaar",other:"ongeveer {{count}} jaar"},xYears:{one:"1 jaar",other:"{{count}} jaar"},overXYears:{one:"meer as 1 jaar",other:"meer as {{count}} jaar"},almostXYears:{one:"byna 1 jaar",other:"byna {{count}} jaar"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"oor "+s:s+" gelede":s}},function(l,i,e){var t=e(2),a={full:"EEEE, d MMMM yyyy",long:"d MMMM yyyy",medium:"d MMM yyyy",short:"yyyy/MM/dd"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'om' {{time}}",long:"{{date}} 'om' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'verlede' eeee 'om' p",yesterday:"'gister om' p",today:"'vandag om' p",tomorrow:"'môre om' p",nextWeek:"eeee 'om' p",other:"P"};function a(r){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["vC","nC"],abbreviated:["vC","nC"],wide:["voor Christus","na Christus"]},r={narrow:["1","2","3","4"],abbreviated:["K1","K2","K3","K4"],wide:["1ste kwartaal","2de kwartaal","3de kwartaal","4de kwartaal"]},o={narrow:["J","F","M","A","M","J","J","A","S","O","N","D"],abbreviated:["Jan","Feb","Mrt","Apr","Mei","Jun","Jul","Aug","Sep","Okt","Nov","Des"],wide:["Januarie","Februarie","Maart","April","Mei","Junie","Julie","Augustus","September","Oktober","November","Desember"]},n={narrow:["S","M","D","W","D","V","S"],short:["So","Ma","Di","Wo","Do","Vr","Sa"],abbreviated:["Son","Maa","Din","Woe","Don","Vry","Sat"],wide:["Sondag","Maandag","Dinsdag","Woensdag","Donderdag","Vrydag","Saterdag"]},s={narrow:{am:"vm",pm:"nm",midnight:"middernag",noon:"middaguur",morning:"oggend",afternoon:"middag",evening:"laat middag",night:"aand"},abbreviated:{am:"vm",pm:"nm",midnight:"middernag",noon:"middaguur",morning:"oggend",afternoon:"middag",evening:"laat middag",night:"aand"},wide:{am:"vm",pm:"nm",midnight:"middernag",noon:"middaguur",morning:"oggend",afternoon:"middag",evening:"laat middag",night:"aand"}},u={narrow:{am:"vm",pm:"nm",midnight:"middernag",noon:"uur die middag",morning:"uur die oggend",afternoon:"uur die middag",evening:"uur die aand",night:"uur die aand"},abbreviated:{am:"vm",pm:"nm",midnight:"middernag",noon:"uur die middag",morning:"uur die oggend",afternoon:"uur die middag",evening:"uur die aand",night:"uur die aand"},wide:{am:"vm",pm:"nm",midnight:"middernag",noon:"uur die middag",morning:"uur die oggend",afternoon:"uur die middag",evening:"uur die aand",night:"uur die aand"}};function d(f){var _=Number(f),c=_%100;if(c<20)switch(c){case 1:case 8:return _+"ste";default:return _+"de"}return _+"ste"}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(ste|de)?/i,o=/\d+/i,n={narrow:/^([vn]\.? ?C\.?)/,abbreviated:/^([vn]\. ?C\.?)/,wide:/^((voor|na) Christus)/},s={any:[/^v/,/^n/]},u={narrow:/^[1234]/i,abbreviated:/^K[1234]/i,wide:/^[1234](st|d)e kwartaal/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[jfmasond]/i,abbreviated:/^(Jan|Feb|Mrt|Apr|Mei|Jun|Jul|Aug|Sep|Okt|Nov|Dec)\.?/i,wide:/^(Januarie|Februarie|Maart|April|Mei|Junie|Julie|Augustus|September|Oktober|November|Desember)/i},f={narrow:[/^J/i,/^F/i,/^M/i,/^A/i,/^M/i,/^J/i,/^J/i,/^A/i,/^S/i,/^O/i,/^N/i,/^D/i],any:[/^Jan/i,/^Feb/i,/^Mrt/i,/^Apr/i,/^Mei/i,/^Jun/i,/^Jul/i,/^Aug/i,/^Sep/i,/^Okt/i,/^Nov/i,/^Dec/i]},_={narrow:/^[smdwv]/i,short:/^(So|Ma|Di|Wo|Do|Vr|Sa)/i,abbreviated:/^(Son|Maa|Din|Woe|Don|Vry|Sat)/i,wide:/^(Sondag|Maandag|Dinsdag|Woensdag|Donderdag|Vrydag|Saterdag)/i},c={narrow:[/^S/i,/^M/i,/^D/i,/^W/i,/^D/i,/^V/i,/^S/i],any:[/^So/i,/^Ma/i,/^Di/i,/^Wo/i,/^Do/i,/^Vr/i,/^Sa/i]},v={any:/^(vm|nm|middernag|(?:uur )?die (oggend|middag|aand))/i},g={any:{am:/^vm/i,pm:/^nm/i,midnight:/^middernag/i,noon:/^middaguur/i,morning:/oggend/i,afternoon:/middag/i,evening:/laat middag/i,night:/aand/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(449),a=e(450),r=e(451),o=e(452),n=e(453),s={code:"ar-DZ",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:0,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"أقل من ثانية واحدة",two:"أقل من ثانتين",threeToTen:"أقل من {{count}} ثواني",other:"أقل من {{count}} ثانية"},xSeconds:{one:"ثانية واحدة",two:"ثانتين",threeToTen:"{{count}} ثواني",other:"{{count}} ثانية"},halfAMinute:"نصف دقيقة",lessThanXMinutes:{one:"أقل من دقيقة",two:"أقل من دقيقتين",threeToTen:"أقل من {{count}} دقائق",other:"أقل من {{count}} دقيقة"},xMinutes:{one:"دقيقة واحدة",two:"دقيقتين",threeToTen:"{{count}} دقائق",other:"{{count}} دقيقة"},aboutXHours:{one:"ساعة واحدة تقريباً",two:"ساعتين تقريباً",threeToTen:"{{count}} ساعات تقريباً",other:"{{count}} ساعة تقريباً"},xHours:{one:"ساعة واحدة",two:"ساعتين",threeToTen:"{{count}} ساعات",other:"{{count}} ساعة"},xDays:{one:"يوم واحد",two:"يومين",threeToTen:"{{count}} أيام",other:"{{count}} يوم"},aboutXWeeks:{one:"أسبوع واحد تقريباً",two:"أسبوعين تقريباً",threeToTen:"{{count}} أسابيع تقريباً",other:"{{count}} أسبوع تقريباً"},xWeeks:{one:"أسبوع واحد",two:"أسبوعين",threeToTen:"{{count}} أسابيع",other:"{{count}} أسبوع"},aboutXMonths:{one:"شهر واحد تقريباً",two:"شهرين تقريباً",threeToTen:"{{count}} أشهر تقريباً",other:"{{count}} شهر تقريباً"},xMonths:{one:"شهر واحد",two:"شهرين",threeToTen:"{{count}} أشهر",other:"{{count}} شهر"},aboutXYears:{one:"عام واحد تقريباً",two:"عامين تقريباً",threeToTen:"{{count}} أعوام تقريباً",other:"{{count}} عام تقريباً"},xYears:{one:"عام واحد",two:"عامين",threeToTen:"{{count}} أعوام",other:"{{count}} عام"},overXYears:{one:"أكثر من عام",two:"أكثر من عامين",threeToTen:"أكثر من {{count}} أعوام",other:"أكثر من {{count}} عام"},almostXYears:{one:"عام واحد تقريباً",two:"عامين تقريباً",threeToTen:"{{count}} أعوام تقريباً",other:"{{count}} عام تقريباً"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:o===2?s=t[r].two:o<=10?s=t[r].threeToTen.replace("{{count}}",o):s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"في خلال "+s:"منذ "+s:s}},function(l,i,e){var t=e(2),a={full:"EEEE, MMMM do, y",long:"MMMM do, y",medium:"MMM d, y",short:"MM/dd/yyyy"},r={full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},o={full:"{{date}} 'عند' {{time}}",long:"{{date}} 'عند' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'أخر' eeee 'عند' p",yesterday:"'أمس عند' p",today:"'اليوم عند' p",tomorrow:"'غداً عند' p",nextWeek:"eeee 'عند' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["ق","ب"],abbreviated:["ق.م.","ب.م."],wide:["قبل الميلاد","بعد الميلاد"]},r={narrow:["1","2","3","4"],abbreviated:["ر1","ر2","ر3","ر4"],wide:["الربع الأول","الربع الثاني","الربع الثالث","الربع الرابع"]},o={narrow:["ج","ف","م","أ","م","ج","ج","أ","س","أ","ن","د"],abbreviated:["جانـ","فيفـ","مارس","أفريل","مايـ","جوانـ","جويـ","أوت","سبتـ","أكتـ","نوفـ","ديسـ"],wide:["جانفي","فيفري","مارس","أفريل","ماي","جوان","جويلية","أوت","سبتمبر","أكتوبر","نوفمبر","ديسمبر"]},n={narrow:["ح","ن","ث","ر","خ","ج","س"],short:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت"],abbreviated:["أحد","اثنـ","ثلا","أربـ","خميـ","جمعة","سبت"],wide:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"]},s={narrow:{am:"ص",pm:"م",midnight:"ن",noon:"ظ",morning:"صباحاً",afternoon:"بعد الظهر",evening:"مساءاً",night:"ليلاً"},abbreviated:{am:"ص",pm:"م",midnight:"نصف الليل",noon:"ظهر",morning:"صباحاً",afternoon:"بعد الظهر",evening:"مساءاً",night:"ليلاً"},wide:{am:"ص",pm:"م",midnight:"نصف الليل",noon:"ظهر",morning:"صباحاً",afternoon:"بعد الظهر",evening:"مساءاً",night:"ليلاً"}},u={narrow:{am:"ص",pm:"م",midnight:"ن",noon:"ظ",morning:"في الصباح",afternoon:"بعد الظـهر",evening:"في المساء",night:"في الليل"},abbreviated:{am:"ص",pm:"م",midnight:"نصف الليل",noon:"ظهر",morning:"في الصباح",evening:"في المساء",night:"في الليل"},wide:{am:"ص",pm:"م",midnight:"نصف الليل",noon:"ظهر",afternoon:"بعد الظـهر",evening:"في المساء",night:"في الليل"}};function d(f){return String(f)}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(th|st|nd|rd)?/i,o=/\d+/i,n={narrow:/^(ق|ب)/i,abbreviated:/^(ق\.?\s?م\.?|ق\.?\s?م\.?\s?|a\.?\s?d\.?|c\.?\s?)/i,wide:/^(قبل الميلاد|قبل الميلاد|بعد الميلاد|بعد الميلاد)/i},s={any:[/^قبل/i,/^بعد/i]},u={narrow:/^[1234]/i,abbreviated:/^ر[1234]/i,wide:/^الربع [1234]/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[جفمأسند]/i,abbreviated:/^(جان|فيف|مار|أفر|ماي|جوا|جوي|أوت|سبت|أكت|نوف|ديس)/i,wide:/^(جانفي|فيفري|مارس|أفريل|ماي|جوان|جويلية|أوت|سبتمبر|أكتوبر|نوفمبر|ديسمبر)/i},f={narrow:[/^ج/i,/^ف/i,/^م/i,/^أ/i,/^م/i,/^ج/i,/^ج/i,/^أ/i,/^س/i,/^أ/i,/^ن/i,/^د/i],any:[/^جان/i,/^فيف/i,/^مار/i,/^أفر/i,/^ماي/i,/^جوا/i,/^جوي/i,/^أوت/i,/^سبت/i,/^أكت/i,/^نوف/i,/^ديس/i]},_={narrow:/^[حنثرخجس]/i,short:/^(أحد|اثنين|ثلاثاء|أربعاء|خميس|جمعة|سبت)/i,abbreviated:/^(أحد|اثن|ثلا|أرب|خمي|جمعة|سبت)/i,wide:/^(الأحد|الاثنين|الثلاثاء|الأربعاء|الخميس|الجمعة|السبت)/i},c={narrow:[/^ح/i,/^ن/i,/^ث/i,/^ر/i,/^خ/i,/^ج/i,/^س/i],wide:[/^الأحد/i,/^الاثنين/i,/^الثلاثاء/i,/^الأربعاء/i,/^الخميس/i,/^الجمعة/i,/^السبت/i],any:[/^أح/i,/^اث/i,/^ث/i,/^أر/i,/^خ/i,/^ج/i,/^س/i]},v={narrow:/^(a|p|mi|n|(in the|at) (morning|afternoon|evening|night))/i,any:/^([ap]\.?\s?m\.?|midnight|noon|(in the|at) (morning|afternoon|evening|night))/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/^mi/i,noon:/^no/i,morning:/morning/i,afternoon:/afternoon/i,evening:/evening/i,night:/night/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(455),a=e(456),r=e(457),o=e(458),n=e(459),s={code:"ar-MA",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"أقل من ثانية واحدة",two:"أقل من ثانتين",threeToTen:"أقل من {{count}} ثواني",other:"أقل من {{count}} ثانية"},xSeconds:{one:"ثانية واحدة",two:"ثانتين",threeToTen:"{{count}} ثواني",other:"{{count}} ثانية"},halfAMinute:"نصف دقيقة",lessThanXMinutes:{one:"أقل من دقيقة",two:"أقل من دقيقتين",threeToTen:"أقل من {{count}} دقائق",other:"أقل من {{count}} دقيقة"},xMinutes:{one:"دقيقة واحدة",two:"دقيقتين",threeToTen:"{{count}} دقائق",other:"{{count}} دقيقة"},aboutXHours:{one:"ساعة واحدة تقريباً",two:"ساعتين تقريباً",threeToTen:"{{count}} ساعات تقريباً",other:"{{count}} ساعة تقريباً"},xHours:{one:"ساعة واحدة",two:"ساعتين",threeToTen:"{{count}} ساعات",other:"{{count}} ساعة"},xDays:{one:"يوم واحد",two:"يومين",threeToTen:"{{count}} أيام",other:"{{count}} يوم"},aboutXWeeks:{one:"أسبوع واحد تقريباً",two:"أسبوعين تقريباً",threeToTen:"{{count}} أسابيع تقريباً",other:"{{count}} أسبوع تقريباً"},xWeeks:{one:"أسبوع واحد",two:"أسبوعين",threeToTen:"{{count}} أسابيع",other:"{{count}} أسبوع"},aboutXMonths:{one:"شهر واحد تقريباً",two:"شهرين تقريباً",threeToTen:"{{count}} أشهر تقريباً",other:"{{count}} شهر تقريباً"},xMonths:{one:"شهر واحد",two:"شهرين",threeToTen:"{{count}} أشهر",other:"{{count}} شهر"},aboutXYears:{one:"عام واحد تقريباً",two:"عامين تقريباً",threeToTen:"{{count}} أعوام تقريباً",other:"{{count}} عام تقريباً"},xYears:{one:"عام واحد",two:"عامين",threeToTen:"{{count}} أعوام",other:"{{count}} عام"},overXYears:{one:"أكثر من عام",two:"أكثر من عامين",threeToTen:"أكثر من {{count}} أعوام",other:"أكثر من {{count}} عام"},almostXYears:{one:"عام واحد تقريباً",two:"عامين تقريباً",threeToTen:"{{count}} أعوام تقريباً",other:"{{count}} عام تقريباً"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:o===2?s=t[r].two:o<=10?s=t[r].threeToTen.replace("{{count}}",o):s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"في خلال "+s:"منذ "+s:s}},function(l,i,e){var t=e(2),a={full:"EEEE, MMMM do, y",long:"MMMM do, y",medium:"MMM d, y",short:"MM/dd/yyyy"},r={full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},o={full:"{{date}} 'عند' {{time}}",long:"{{date}} 'عند' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'أخر' eeee 'عند' p",yesterday:"'أمس عند' p",today:"'اليوم عند' p",tomorrow:"'غداً عند' p",nextWeek:"eeee 'عند' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["ق","ب"],abbreviated:["ق.م.","ب.م."],wide:["قبل الميلاد","بعد الميلاد"]},r={narrow:["1","2","3","4"],abbreviated:["ر1","ر2","ر3","ر4"],wide:["الربع الأول","الربع الثاني","الربع الثالث","الربع الرابع"]},o={narrow:["ي","ف","م","أ","م","ي","ي","غ","ش","أ","ن","د"],abbreviated:["ينا","فبر","مارس","أبريل","ماي","يونـ","يولـ","غشت","شتنـ","أكتـ","نونـ","دجنـ"],wide:["يناير","فبراير","مارس","أبريل","ماي","يونيو","يوليوز","غشت","شتنبر","أكتوبر","نونبر","دجنبر"]},n={narrow:["ح","ن","ث","ر","خ","ج","س"],short:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت"],abbreviated:["أحد","اثنـ","ثلا","أربـ","خميـ","جمعة","سبت"],wide:["الأحد","الإثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"]},s={narrow:{am:"ص",pm:"م",midnight:"ن",noon:"ظ",morning:"صباحاً",afternoon:"بعد الظهر",evening:"مساءاً",night:"ليلاً"},abbreviated:{am:"ص",pm:"م",midnight:"نصف الليل",noon:"ظهر",morning:"صباحاً",afternoon:"بعد الظهر",evening:"مساءاً",night:"ليلاً"},wide:{am:"ص",pm:"م",midnight:"نصف الليل",noon:"ظهر",morning:"صباحاً",afternoon:"بعد الظهر",evening:"مساءاً",night:"ليلاً"}},u={narrow:{am:"ص",pm:"م",midnight:"ن",noon:"ظ",morning:"في الصباح",afternoon:"بعد الظـهر",evening:"في المساء",night:"في الليل"},abbreviated:{am:"ص",pm:"م",midnight:"نصف الليل",noon:"ظهر",morning:"في الصباح",evening:"في المساء",night:"في الليل"},wide:{am:"ص",pm:"م",midnight:"نصف الليل",noon:"ظهر",afternoon:"بعد الظـهر",evening:"في المساء",night:"في الليل"}};function d(f){return String(f)}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(th|st|nd|rd)?/i,o=/\d+/i,n={narrow:/^(ق|ب)/i,abbreviated:/^(ق\.?\s?م\.?|ق\.?\s?م\.?\s?|a\.?\s?d\.?|c\.?\s?)/i,wide:/^(قبل الميلاد|قبل الميلاد|بعد الميلاد|بعد الميلاد)/i},s={any:[/^قبل/i,/^بعد/i]},u={narrow:/^[1234]/i,abbreviated:/^ر[1234]/i,wide:/^الربع [1234]/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[يفمأمسند]/i,abbreviated:/^(ين|ف|مار|أب|ماي|يون|يول|غش|شت|أك|ن|د)/i,wide:/^(ين|ف|مار|أب|ماي|يون|يول|غش|شت|أك|ن|د)/i},f={narrow:[/^ي/i,/^ف/i,/^م/i,/^أ/i,/^م/i,/^ي/i,/^ي/i,/^غ/i,/^ش/i,/^أ/i,/^ن/i,/^د/i],any:[/^ين/i,/^فب/i,/^مار/i,/^أب/i,/^ماي/i,/^يون/i,/^يول/i,/^غشت/i,/^ش/i,/^أك/i,/^ن/i,/^د/i]},_={narrow:/^[حنثرخجس]/i,short:/^(أحد|إثنين|ثلاثاء|أربعاء|خميس|جمعة|سبت)/i,abbreviated:/^(أحد|إثن|ثلا|أرب|خمي|جمعة|سبت)/i,wide:/^(الأحد|الإثنين|الثلاثاء|الأربعاء|الخميس|الجمعة|السبت)/i},c={narrow:[/^ح/i,/^ن/i,/^ث/i,/^ر/i,/^خ/i,/^ج/i,/^س/i],wide:[/^الأحد/i,/^الإثنين/i,/^الثلاثاء/i,/^الأربعاء/i,/^الخميس/i,/^الجمعة/i,/^السبت/i],any:[/^أح/i,/^إث/i,/^ث/i,/^أر/i,/^خ/i,/^ج/i,/^س/i]},v={narrow:/^(a|p|mi|n|(in the|at) (morning|afternoon|evening|night))/i,any:/^([ap]\.?\s?m\.?|midnight|noon|(in the|at) (morning|afternoon|evening|night))/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/^mi/i,noon:/^no/i,morning:/morning/i,afternoon:/afternoon/i,evening:/evening/i,night:/night/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(461),a=e(462),r=e(463),o=e(464),n=e(465),s={code:"ar-SA",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:0,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"أقل من ثانية واحدة",two:"أقل من ثانتين",threeToTen:"أقل من {{count}} ثواني",other:"أقل من {{count}} ثانية"},xSeconds:{one:"ثانية واحدة",two:"ثانتين",threeToTen:"{{count}} ثواني",other:"{{count}} ثانية"},halfAMinute:"نصف دقيقة",lessThanXMinutes:{one:"أقل من دقيقة",two:"أقل من دقيقتين",threeToTen:"أقل من {{count}} دقائق",other:"أقل من {{count}} دقيقة"},xMinutes:{one:"دقيقة واحدة",two:"دقيقتين",threeToTen:"{{count}} دقائق",other:"{{count}} دقيقة"},aboutXHours:{one:"ساعة واحدة تقريباً",two:"ساعتين تقريباً",threeToTen:"{{count}} ساعات تقريباً",other:"{{count}} ساعة تقريباً"},xHours:{one:"ساعة واحدة",two:"ساعتين",threeToTen:"{{count}} ساعات",other:"{{count}} ساعة"},xDays:{one:"يوم واحد",two:"يومين",threeToTen:"{{count}} أيام",other:"{{count}} يوم"},aboutXWeeks:{one:"أسبوع واحد تقريباً",two:"أسبوعين تقريباً",threeToTen:"{{count}} أسابيع تقريباً",other:"{{count}} أسبوع تقريباً"},xWeeks:{one:"أسبوع واحد",two:"أسبوعين",threeToTen:"{{count}} أسابيع",other:"{{count}} أسبوع"},aboutXMonths:{one:"شهر واحد تقريباً",two:"شهرين تقريباً",threeToTen:"{{count}} أشهر تقريباً",other:"{{count}} شهر تقريباً"},xMonths:{one:"شهر واحد",two:"شهرين",threeToTen:"{{count}} أشهر",other:"{{count}} شهر"},aboutXYears:{one:"عام واحد تقريباً",two:"عامين تقريباً",threeToTen:"{{count}} أعوام تقريباً",other:"{{count}} عام تقريباً"},xYears:{one:"عام واحد",two:"عامين",threeToTen:"{{count}} أعوام",other:"{{count}} عام"},overXYears:{one:"أكثر من عام",two:"أكثر من عامين",threeToTen:"أكثر من {{count}} أعوام",other:"أكثر من {{count}} عام"},almostXYears:{one:"عام واحد تقريباً",two:"عامين تقريباً",threeToTen:"{{count}} أعوام تقريباً",other:"{{count}} عام تقريباً"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:o===2?s=t[r].two:o<=10?s=t[r].threeToTen.replace("{{count}}",o):s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"في خلال "+s:"منذ "+s:s}},function(l,i,e){var t=e(2),a={full:"EEEE, MMMM do, y",long:"MMMM do, y",medium:"MMM d, y",short:"MM/dd/yyyy"},r={full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},o={full:"{{date}} 'عند' {{time}}",long:"{{date}} 'عند' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'أخر' eeee 'عند' p",yesterday:"'أمس عند' p",today:"'اليوم عند' p",tomorrow:"'غداً عند' p",nextWeek:"eeee 'عند' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["ق","ب"],abbreviated:["ق.م.","ب.م."],wide:["قبل الميلاد","بعد الميلاد"]},r={narrow:["1","2","3","4"],abbreviated:["ر1","ر2","ر3","ر4"],wide:["الربع الأول","الربع الثاني","الربع الثالث","الربع الرابع"]},o={narrow:["ي","ف","م","أ","م","ي","ي","أ","س","أ","ن","د"],abbreviated:["ينا","فبر","مارس","أبريل","مايو","يونـ","يولـ","أغسـ","سبتـ","أكتـ","نوفـ","ديسـ"],wide:["يناير","فبراير","مارس","أبريل","مايو","يونيو","يوليو","أغسطس","سبتمبر","أكتوبر","نوفمبر","ديسمبر"]},n={narrow:["ح","ن","ث","ر","خ","ج","س"],short:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت"],abbreviated:["أحد","اثنـ","ثلا","أربـ","خميـ","جمعة","سبت"],wide:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"]},s={narrow:{am:"ص",pm:"م",midnight:"ن",noon:"ظ",morning:"صباحاً",afternoon:"بعد الظهر",evening:"مساءاً",night:"ليلاً"},abbreviated:{am:"ص",pm:"م",midnight:"نصف الليل",noon:"ظهر",morning:"صباحاً",afternoon:"بعد الظهر",evening:"مساءاً",night:"ليلاً"},wide:{am:"ص",pm:"م",midnight:"نصف الليل",noon:"ظهر",morning:"صباحاً",afternoon:"بعد الظهر",evening:"مساءاً",night:"ليلاً"}},u={narrow:{am:"ص",pm:"م",midnight:"ن",noon:"ظ",morning:"في الصباح",afternoon:"بعد الظـهر",evening:"في المساء",night:"في الليل"},abbreviated:{am:"ص",pm:"م",midnight:"نصف الليل",noon:"ظهر",morning:"في الصباح",evening:"في المساء",night:"في الليل"},wide:{am:"ص",pm:"م",midnight:"نصف الليل",noon:"ظهر",afternoon:"بعد الظـهر",evening:"في المساء",night:"في الليل"}};function d(f){return String(f)}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(th|st|nd|rd)?/i,o=/\d+/i,n={narrow:/^(ق|ب)/i,abbreviated:/^(ق\.?\s?م\.?|ق\.?\s?م\.?\s?|a\.?\s?d\.?|c\.?\s?)/i,wide:/^(قبل الميلاد|قبل الميلاد|بعد الميلاد|بعد الميلاد)/i},s={any:[/^قبل/i,/^بعد/i]},u={narrow:/^[1234]/i,abbreviated:/^ر[1234]/i,wide:/^الربع [1234]/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[يفمأمسند]/i,abbreviated:/^(ين|ف|مار|أب|ماي|يون|يول|أغ|س|أك|ن|د)/i,wide:/^(ين|ف|مار|أب|ماي|يون|يول|أغ|س|أك|ن|د)/i},f={narrow:[/^ي/i,/^ف/i,/^م/i,/^أ/i,/^م/i,/^ي/i,/^ي/i,/^أ/i,/^س/i,/^أ/i,/^ن/i,/^د/i],any:[/^ين/i,/^ف/i,/^مار/i,/^أب/i,/^ماي/i,/^يون/i,/^يول/i,/^أغ/i,/^س/i,/^أك/i,/^ن/i,/^د/i]},_={narrow:/^[حنثرخجس]/i,short:/^(أحد|اثنين|ثلاثاء|أربعاء|خميس|جمعة|سبت)/i,abbreviated:/^(أحد|اثن|ثلا|أرب|خمي|جمعة|سبت)/i,wide:/^(الأحد|الاثنين|الثلاثاء|الأربعاء|الخميس|الجمعة|السبت)/i},c={narrow:[/^ح/i,/^ن/i,/^ث/i,/^ر/i,/^خ/i,/^ج/i,/^س/i],wide:[/^الأحد/i,/^الاثنين/i,/^الثلاثاء/i,/^الأربعاء/i,/^الخميس/i,/^الجمعة/i,/^السبت/i],any:[/^أح/i,/^اث/i,/^ث/i,/^أر/i,/^خ/i,/^ج/i,/^س/i]},v={narrow:/^(a|p|mi|n|(in the|at) (morning|afternoon|evening|night))/i,any:/^([ap]\.?\s?m\.?|midnight|noon|(in the|at) (morning|afternoon|evening|night))/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/^mi/i,noon:/^no/i,morning:/morning/i,afternoon:/afternoon/i,evening:/evening/i,night:/night/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(467),a=e(468),r=e(469),o=e(470),n=e(471),s={code:"az",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"bir saniyədən az",other:"{{count}} bir saniyədən az"},xSeconds:{one:"1 saniyə",other:"{{count}} saniyə"},halfAMinute:"yarım dəqiqə",lessThanXMinutes:{one:"bir dəqiqədən az",other:"{{count}} bir dəqiqədən az"},xMinutes:{one:"bir dəqiqə",other:"{{count}} dəqiqə"},aboutXHours:{one:"təxminən 1 saat",other:"təxminən {{count}} saat"},xHours:{one:"1 saat",other:"{{count}} saat"},xDays:{one:"1 gün",other:"{{count}} gün"},aboutXWeeks:{one:"təxminən 1 həftə",other:"təxminən {{count}} həftə"},xWeeks:{one:"1 həftə",other:"{{count}} həftə"},aboutXMonths:{one:"təxminən 1 ay",other:"təxminən {{count}} ay"},xMonths:{one:"1 ay",other:"{{count}} ay"},aboutXYears:{one:"təxminən 1 il",other:"təxminən {{count}} il"},xYears:{one:"1 il",other:"{{count}} il"},overXYears:{one:"1 ildən çox",other:"{{count}} ildən çox"},almostXYears:{one:"demək olar ki 1 il",other:"demək olar ki {{count}} il"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?s+" əvvəl":s+" sonra":s}},function(l,i,e){var t=e(2),a={full:"EEEE, do MMMM y 'il'",long:"do MMMM y 'il'",medium:"d MMM y 'il'",short:"dd.MM.yyyy"},r={full:"H:mm:ss zzzz",long:"H:mm:ss z",medium:"H:mm:ss",short:"H:mm"},o={full:"{{date}} {{time}} - 'də'",long:"{{date}} {{time}} - 'də'",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'sonuncu' eeee p -'də'",yesterday:"'dünən' p -'də'",today:"'bugün' p -'də'",tomorrow:"'sabah' p -'də'",nextWeek:"eeee p -'də'",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["B","A"],abbreviated:["BC","AD"],wide:["Hz. İsa'dan öncə","Anno Domini"]},r={narrow:["1","2","3","4"],abbreviated:["K1","K2","K3","K4"],wide:["1ci kvartal","2ci kvartal","3cü kvartal","4cü kvartal"]},o={narrow:["Y","F","M","A","M","I","I","A","S","O","N","D"],abbreviated:["Yan","Fev","Mar","Apr","May","İyun","İyul","Avq","Sen","Okt","Noy","Dek"],wide:["Yanvar","Fevral","Mart","Aprel","May","İyun","İyul","Avqust","Sentyabr","Oktyabr","Noyabr","Dekabr"]},n={narrow:["B.","B.e","Ç.a","Ç.","C.a","C.","Ş."],short:["B.","B.e","Ç.a","Ç.","C.a","C.","Ş."],abbreviated:["Baz","Baz.e","Çər.a","Çər","Cüm.a","Cüm","Şə"],wide:["Bazar","Bazar ertəsi","Çərşənbə axşamı","Çərşənbə","Cümə axşamı","Cümə","Şənbə"]},s={narrow:{am:"am",pm:"pm",midnight:"gecəyarı",noon:"gün",morning:"səhər",afternoon:"gündüz",evening:"axşam",night:"gecə"},abbreviated:{am:"AM",pm:"PM",midnight:"gecəyarı",noon:"gün",morning:"səhər",afternoon:"gündüz",evening:"axşam",night:"gecə"},wide:{am:"a.m.",pm:"p.m.",midnight:"gecəyarı",noon:"gün",morning:"səhər",afternoon:"gündüz",evening:"axşam",night:"gecə"}},u={narrow:{am:"a",pm:"p",midnight:"gecəyarı",noon:"gün",morning:"səhər",afternoon:"gündüz",evening:"axşam",night:"gecə"},abbreviated:{am:"AM",pm:"PM",midnight:"gecəyarı",noon:"gün",morning:"səhər",afternoon:"gündüz",evening:"axşam",night:"gecə"},wide:{am:"a.m.",pm:"p.m.",midnight:"gecəyarı",noon:"gün",morning:"səhər",afternoon:"gündüz",evening:"axşam",night:"gecə"}},d={1:"-inci",5:"-inci",8:"-inci",70:"-inci",80:"-inci",2:"-nci",7:"-nci",20:"-nci",50:"-nci",3:"-üncü",4:"-üncü",100:"-üncü",6:"-ncı",9:"-uncu",10:"-uncu",30:"-uncu",60:"-ıncı",90:"-ıncı"};function m(c){if(c===0)return c+"-ıncı";var v=c%10,g=c%100-v,P=c>=100?100:null;return d[v]||d[g]||d[P]}function f(c,v){var g=Number(c),P=m(g);return g+P}var _={ordinalNumber:f,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(c){return Number(c)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=_},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(-?(ci|inci|nci|uncu|üncü|ncı))?/i,o=/\d+/i,n={narrow:/^(b|a)$/i,abbreviated:/^(b\.?\s?c\.?|b\.?\s?c\.?\s?e\.?|a\.?\s?d\.?|c\.?\s?e\.?)$/i,wide:/^(Hz. İsa'dan öncə|ümumi eradan əvvəl|anno domini|ümumi dövr)$/i},s={any:[/^b$/i,/^(a|c)$/i]},u={narrow:/^[1234]$/i,abbreviated:/^K[1234]$/i,wide:/^[1234](ci)? kvartal$/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[(?-i)yfmaisond]$/i,abbreviated:/^(Yan|Fev|Mar|Apr|May|İyun|İyul|Avq|Sen|Okt|Noy|Dek)$/i,wide:/^(Yanvar|Fevral|Mart|Aprel|May|İyun|İyul|Avgust|Sentyabr|Oktyabr|Noyabr|Dekabr)$/i},f={narrow:[/^[(?-i)y]$/i,/^[(?-i)f]$/i,/^[(?-i)m]$/i,/^[(?-i)a]$/i,/^[(?-i)m]$/i,/^[(?-i)i]$/i,/^[(?-i)i]$/i,/^[(?-i)a]$/i,/^[(?-i)s]$/i,/^[(?-i)o]$/i,/^[(?-i)n]$/i,/^[(?-i)d]$/i],abbreviated:[/^Yan$/i,/^Fev$/i,/^Mar$/i,/^Apr$/i,/^May$/i,/^İyun$/i,/^İyul$/i,/^Avg$/i,/^Sen$/i,/^Okt$/i,/^Noy$/i,/^Dek$/i],wide:[/^Yanvar$/i,/^Fevral$/i,/^Mart$/i,/^Aprel$/i,/^May$/i,/^İyun$/i,/^İyul$/i,/^Avgust$/i,/^Sentyabr$/i,/^Oktyabr$/i,/^Noyabr$/i,/^Dekabr$/i]},_={narrow:/^(B\.|B\.e|Ç\.a|Ç\.|C\.a|C\.|Ş\.)$/i,short:/^(B\.|B\.e|Ç\.a|Ç\.|C\.a|C\.|Ş\.)$/i,abbreviated:/^(Baz\.e|Çər|Çər\.a|Cüm|Cüm\.a|Şə)$/i,wide:/^(Bazar|Bazar ertəsi|Çərşənbə axşamı|Çərşənbə|Cümə axşamı|Cümə|Şənbə)$/i},c={narrow:[/^B\.$/i,/^B\.e$/i,/^Ç\.a$/i,/^Ç\.$/i,/^C\.a$/i,/^C\.$/i,/^Ş\.$/i],abbreviated:[/^Baz\.e$/i,/^Çər$/i,/^Çər\.a$/i,/^Cüm$/i,/^Cüm\.a$/i,/^Şə$/i],wide:[/^Bazar$/i,/^Bazar ertəsi$/i,/^Çərşənbə axşamı$/i,/^Çərşənbə$/i,/^Cümə axşamı$/i,/^Cümə$/i,/^Şənbə$/i],any:[/^B\.$/i,/^B\.e$/i,/^Ç\.a$/i,/^Ç\.$/i,/^C\.a$/i,/^C\.$/i,/^Ş\.$/i]},v={narrow:/^(a|p|gecəyarı|gün|səhər|gündüz|axşam|gecə)$/i,any:/^(am|pm|a\.m\.|p\.m\.|AM|PM|gecəyarı|gün|səhər|gündüz|axşam|gecə)$/i},g={any:{am:/^a$/i,pm:/^p$/i,midnight:/^gecəyarı$/i,noon:/^gün$/i,morning:/səhər$/i,afternoon:/gündüz$/i,evening:/axşam$/i,night:/gecə$/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(473),a=e(474),r=e(475),o=e(476),n=e(477),s={code:"be",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=o;function t(n,s){if(n.one!==void 0&&s===1)return n.one;var u=s%10,d=s%100;return u===1&&d!==11?n.singularNominative.replace("{{count}}",s):u>=2&&u<=4&&(d<10||d>20)?n.singularGenitive.replace("{{count}}",s):n.pluralGenitive.replace("{{count}}",s)}function a(n){return function(s,u){return u.addSuffix?u.comparison>0?n.future?t(n.future,s):"праз "+t(n.regular,s):n.past?t(n.past,s):t(n.regular,s)+" таму":t(n.regular,s)}}var r={lessThanXSeconds:a({regular:{one:"менш за секунду",singularNominative:"менш за {{count}} секунду",singularGenitive:"менш за {{count}} секунды",pluralGenitive:"менш за {{count}} секунд"},future:{one:"менш, чым праз секунду",singularNominative:"менш, чым праз {{count}} секунду",singularGenitive:"менш, чым праз {{count}} секунды",pluralGenitive:"менш, чым праз {{count}} секунд"}}),xSeconds:a({regular:{singularNominative:"{{count}} секунда",singularGenitive:"{{count}} секунды",pluralGenitive:"{{count}} секунд"},past:{singularNominative:"{{count}} секунду таму",singularGenitive:"{{count}} секунды таму",pluralGenitive:"{{count}} секунд таму"},future:{singularNominative:"праз {{count}} секунду",singularGenitive:"праз {{count}} секунды",pluralGenitive:"праз {{count}} секунд"}}),halfAMinute:function(n,s){return s.addSuffix?s.comparison>0?"праз паўхвіліны":"паўхвіліны таму":"паўхвіліны"},lessThanXMinutes:a({regular:{one:"менш за хвіліну",singularNominative:"менш за {{count}} хвіліну",singularGenitive:"менш за {{count}} хвіліны",pluralGenitive:"менш за {{count}} хвілін"},future:{one:"менш, чым праз хвіліну",singularNominative:"менш, чым праз {{count}} хвіліну",singularGenitive:"менш, чым праз {{count}} хвіліны",pluralGenitive:"менш, чым праз {{count}} хвілін"}}),xMinutes:a({regular:{singularNominative:"{{count}} хвіліна",singularGenitive:"{{count}} хвіліны",pluralGenitive:"{{count}} хвілін"},past:{singularNominative:"{{count}} хвіліну таму",singularGenitive:"{{count}} хвіліны таму",pluralGenitive:"{{count}} хвілін таму"},future:{singularNominative:"праз {{count}} хвіліну",singularGenitive:"праз {{count}} хвіліны",pluralGenitive:"праз {{count}} хвілін"}}),aboutXHours:a({regular:{singularNominative:"каля {{count}} гадзіны",singularGenitive:"каля {{count}} гадзін",pluralGenitive:"каля {{count}} гадзін"},future:{singularNominative:"прыблізна праз {{count}} гадзіну",singularGenitive:"прыблізна праз {{count}} гадзіны",pluralGenitive:"прыблізна праз {{count}} гадзін"}}),xHours:a({regular:{singularNominative:"{{count}} гадзіна",singularGenitive:"{{count}} гадзіны",pluralGenitive:"{{count}} гадзін"},past:{singularNominative:"{{count}} гадзіну таму",singularGenitive:"{{count}} гадзіны таму",pluralGenitive:"{{count}} гадзін таму"},future:{singularNominative:"праз {{count}} гадзіну",singularGenitive:"праз {{count}} гадзіны",pluralGenitive:"праз {{count}} гадзін"}}),xDays:a({regular:{singularNominative:"{{count}} дзень",singularGenitive:"{{count}} дні",pluralGenitive:"{{count}} дзён"}}),aboutXWeeks:a({regular:{singularNominative:"каля {{count}} месяца",singularGenitive:"каля {{count}} месяцаў",pluralGenitive:"каля {{count}} месяцаў"},future:{singularNominative:"прыблізна праз {{count}} месяц",singularGenitive:"прыблізна праз {{count}} месяцы",pluralGenitive:"прыблізна праз {{count}} месяцаў"}}),xWeeks:a({regular:{singularNominative:"{{count}} месяц",singularGenitive:"{{count}} месяцы",pluralGenitive:"{{count}} месяцаў"}}),aboutXMonths:a({regular:{singularNominative:"каля {{count}} месяца",singularGenitive:"каля {{count}} месяцаў",pluralGenitive:"каля {{count}} месяцаў"},future:{singularNominative:"прыблізна праз {{count}} месяц",singularGenitive:"прыблізна праз {{count}} месяцы",pluralGenitive:"прыблізна праз {{count}} месяцаў"}}),xMonths:a({regular:{singularNominative:"{{count}} месяц",singularGenitive:"{{count}} месяцы",pluralGenitive:"{{count}} месяцаў"}}),aboutXYears:a({regular:{singularNominative:"каля {{count}} года",singularGenitive:"каля {{count}} гадоў",pluralGenitive:"каля {{count}} гадоў"},future:{singularNominative:"прыблізна праз {{count}} год",singularGenitive:"прыблізна праз {{count}} гады",pluralGenitive:"прыблізна праз {{count}} гадоў"}}),xYears:a({regular:{singularNominative:"{{count}} год",singularGenitive:"{{count}} гады",pluralGenitive:"{{count}} гадоў"}}),overXYears:a({regular:{singularNominative:"больш за {{count}} год",singularGenitive:"больш за {{count}} гады",pluralGenitive:"больш за {{count}} гадоў"},future:{singularNominative:"больш, чым праз {{count}} год",singularGenitive:"больш, чым праз {{count}} гады",pluralGenitive:"больш, чым праз {{count}} гадоў"}}),almostXYears:a({regular:{singularNominative:"амаль {{count}} год",singularGenitive:"амаль {{count}} гады",pluralGenitive:"амаль {{count}} гадоў"},future:{singularNominative:"амаль праз {{count}} год",singularGenitive:"амаль праз {{count}} гады",pluralGenitive:"амаль праз {{count}} гадоў"}})};function o(n,s,u){return u=u||{},r[n](s,u)}},function(l,i,e){var t=e(2),a={full:"EEEE, d MMMM y 'г.'",long:"d MMMM y 'г.'",medium:"d MMM y 'г.'",short:"dd.MM.y"},r={full:"H:mm:ss zzzz",long:"H:mm:ss z",medium:"H:mm:ss",short:"H:mm"},o={any:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"any"})};i.a=n},function(l,i,e){i.a=u;var t=e(13),a=["нядзелю","панядзелак","аўторак","сераду","чацвер","пятніцу","суботу"];function r(d){var m=a[d];switch(d){case 0:case 3:case 5:case 6:return"'у мінулую "+m+" а' p";case 1:case 2:case 4:return"'у мінулы "+m+" а' p"}}function o(d){var m=a[d];return"'у "+m+" а' p"}function n(d){var m=a[d];switch(d){case 0:case 3:case 5:case 6:return"'у наступную "+m+" а' p";case 1:case 2:case 4:return"'у наступны "+m+" а' p"}}var s={lastWeek:function(d,m,f){var _=d.getUTCDay();return t.a(d,m,f)?o(_):r(_)},yesterday:"'учора а' p",today:"'сёння а' p",tomorrow:"'заўтра а' p",nextWeek:function(d,m,f){var _=d.getUTCDay();return t.a(d,m,f)?o(_):n(_)},other:"P"};function u(d,m,f,_){var c=s[d];return typeof c=="function"?c(m,f,_):c}},function(l,i,e){var t=e(3),a={narrow:["да н.э.","н.э."],abbreviated:["да н. э.","н. э."],wide:["да нашай эры","нашай эры"]},r={narrow:["1","2","3","4"],abbreviated:["1-ы кв.","2-і кв.","3-і кв.","4-ы кв."],wide:["1-ы квартал","2-і квартал","3-і квартал","4-ы квартал"]},o={narrow:["С","Л","С","К","М","Ч","Л","Ж","В","К","Л","С"],abbreviated:["студз.","лют.","сак.","крас.","май","чэрв.","ліп.","жн.","вер.","кастр.","ліст.","снеж."],wide:["студзень","люты","сакавік","красавік","май","чэрвень","ліпень","жнівень","верасень","кастрычнік","лістапад","снежань"]},n={narrow:["С","Л","С","К","М","Ч","Л","Ж","В","К","Л","С"],abbreviated:["студз.","лют.","сак.","крас.","мая","чэрв.","ліп.","жн.","вер.","кастр.","ліст.","снеж."],wide:["студзеня","лютага","сакавіка","красавіка","мая","чэрвеня","ліпеня","жніўня","верасня","кастрычніка","лістапада","снежня"]},s={narrow:["Н","П","А","С","Ч","П","С"],short:["нд","пн","аў","ср","чц","пт","сб"],abbreviated:["нядз","пан","аўт","сер","чац","пят","суб"],wide:["нядзеля","панядзелак","аўторак","серада","чацвер","пятніца","субота"]},u={narrow:{am:"ДП",pm:"ПП",midnight:"поўн.",noon:"поўд.",morning:"ран.",afternoon:"дзень",evening:"веч.",night:"ноч"},abbreviated:{am:"ДП",pm:"ПП",midnight:"поўн.",noon:"поўд.",morning:"ран.",afternoon:"дзень",evening:"веч.",night:"ноч"},wide:{am:"ДП",pm:"ПП",midnight:"поўнач",noon:"поўдзень",morning:"раніца",afternoon:"дзень",evening:"вечар",night:"ноч"}},d={narrow:{am:"ДП",pm:"ПП",midnight:"поўн.",noon:"поўд.",morning:"ран.",afternoon:"дня",evening:"веч.",night:"ночы"},abbreviated:{am:"ДП",pm:"ПП",midnight:"поўн.",noon:"поўд.",morning:"ран.",afternoon:"дня",evening:"веч.",night:"ночы"},wide:{am:"ДП",pm:"ПП",midnight:"поўнач",noon:"поўдзень",morning:"раніцы",afternoon:"дня",evening:"вечара",night:"ночы"}};function m(_,c){var v=c||{},g=String(v.unit),P=Number(_),h;return g==="date"?h="-га":g==="hour"||g==="minute"||g==="second"?h="-я":h=(P%10===2||P%10===3)&&P%100!==12&&P%100!==13?"-і":"-ы",P+h}var f={ordinalNumber:m,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(_){return Number(_)-1}}),month:t.a({values:o,defaultWidth:"wide",formattingValues:n,defaultFormattingWidth:"wide"}),day:t.a({values:s,defaultWidth:"wide"}),dayPeriod:t.a({values:u,defaultWidth:"any",formattingValues:d,defaultFormattingWidth:"wide"})};i.a=f},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(-?(е|я|га|і|ы|ае|ая|яя|шы|гі|ці|ты|мы))?/i,o=/\d+/i,n={narrow:/^((да )?н\.?\s?э\.?)/i,abbreviated:/^((да )?н\.?\s?э\.?)/i,wide:/^(да нашай эры|нашай эры|наша эра)/i},s={any:[/^д/i,/^н/i]},u={narrow:/^[1234]/i,abbreviated:/^[1234](-?[ыі]?)? кв.?/i,wide:/^[1234](-?[ыі]?)? квартал/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[слкмчжв]/i,abbreviated:/^(студз|лют|сак|крас|ма[йя]|чэрв|ліп|жн|вер|кастр|ліст|снеж)\.?/i,wide:/^(студзен[ья]|лют(ы|ага)|сакавіка?|красавіка?|ма[йя]|чэрвен[ья]|ліпен[ья]|жні(вень|ўня)|верас(ень|ня)|кастрычніка?|лістапада?|снеж(ань|ня))/i},f={narrow:[/^с/i,/^л/i,/^с/i,/^к/i,/^м/i,/^ч/i,/^л/i,/^ж/i,/^в/i,/^к/i,/^л/i,/^с/i],any:[/^ст/i,/^лю/i,/^са/i,/^кр/i,/^ма/i,/^ч/i,/^ліп/i,/^ж/i,/^в/i,/^ка/i,/^ліс/i,/^сн/i]},_={narrow:/^[нпасч]/i,short:/^(нд|ня|пн|па|аў|ат|ср|се|чц|ча|пт|пя|сб|су)\.?/i,abbreviated:/^(нядз?|ндз|пнд|пан|аўт|срд|сер|чцв|чац|птн|пят|суб).?/i,wide:/^(нядзел[яі]|панядзел(ак|ка)|аўтор(ак|ка)|серад[аы]|чацв(ер|ярга)|пятніц[аы]|субот[аы])/i},c={narrow:[/^н/i,/^п/i,/^а/i,/^с/i,/^ч/i,/^п/i,/^с/i],any:[/^н/i,/^п[ан]/i,/^а/i,/^с[ер]/i,/^ч/i,/^п[ят]/i,/^с[уб]/i]},v={narrow:/^([дп]п|поўн\.?|поўд\.?|ран\.?|дзень|дня|веч\.?|ночы?)/i,abbreviated:/^([дп]п|поўн\.?|поўд\.?|ран\.?|дзень|дня|веч\.?|ночы?)/i,wide:/^([дп]п|поўнач|поўдзень|раніц[аы]|дзень|дня|вечара?|ночы?)/i},g={any:{am:/^дп/i,pm:/^пп/i,midnight:/^поўн/i,noon:/^поўд/i,morning:/^р/i,afternoon:/^д[зн]/i,evening:/^в/i,night:/^н/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"wide",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(479),a=e(480),r=e(481),o=e(482),n=e(483),s={code:"bg",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"по-малко от секунда",other:"по-малко от {{count}} секунди"},xSeconds:{one:"1 секунда",other:"{{count}} секунди"},halfAMinute:"половин минута",lessThanXMinutes:{one:"по-малко от минута",other:"по-малко от {{count}} минути"},xMinutes:{one:"1 минута",other:"{{count}} минути"},aboutXHours:{one:"около час",other:"около {{count}} часа"},xHours:{one:"1 час",other:"{{count}} часа"},xDays:{one:"1 ден",other:"{{count}} дни"},aboutXWeeks:{one:"около седмица",other:"около {{count}} седмици"},xWeeks:{one:"1 седмица",other:"{{count}} седмици"},aboutXMonths:{one:"около месец",other:"около {{count}} месеца"},xMonths:{one:"1 месец",other:"{{count}} месеца"},aboutXYears:{one:"около година",other:"около {{count}} години"},xYears:{one:"1 година",other:"{{count}} години"},overXYears:{one:"над година",other:"над {{count}} години"},almostXYears:{one:"почти година",other:"почти {{count}} години"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"след "+s:"преди "+s:s}},function(l,i,e){var t=e(2),a={full:"EEEE, dd MMMM yyyy",long:"dd MMMM yyyy",medium:"dd MMM yyyy",short:"dd/MM/yyyy"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"H:mm"},o={any:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"any"})};i.a=n},function(l,i,e){i.a=u;var t=e(13),a=["неделя","понеделник","вторник","сряда","четвъртък","петък","събота"];function r(d){var m=a[d];switch(d){case 0:case 3:case 6:return"'миналата "+m+" в' p";case 1:case 2:case 4:case 5:return"'миналия "+m+" в' p"}}function o(d){var m=a[d];return d===2?"'във "+m+" в' p":"'в "+m+" в' p"}function n(d){var m=a[d];switch(d){case 0:case 3:case 6:return"'следващата "+m+" в' p";case 1:case 2:case 4:case 5:return"'следващия "+m+" в' p"}}var s={lastWeek:function(d,m,f){var _=d.getUTCDay();return t.a(d,m,f)?o(_):r(_)},yesterday:"'вчера в' p",today:"'днес в' p",tomorrow:"'утре в' p",nextWeek:function(d,m,f){var _=d.getUTCDay();return t.a(d,m,f)?o(_):n(_)},other:"P"};function u(d,m,f,_){var c=s[d];return typeof c=="function"?c(m,f,_):c}},function(l,i,e){var t=e(3),a={narrow:["пр.н.е.","н.е."],abbreviated:["преди н. е.","н. е."],wide:["преди новата ера","новата ера"]},r={narrow:["1","2","3","4"],abbreviated:["1-во тримес.","2-ро тримес.","3-то тримес.","4-то тримес."],wide:["1-во тримесечие","2-ро тримесечие","3-то тримесечие","4-то тримесечие"]},o={abbreviated:["яну","фев","мар","апр","май","юни","юли","авг","сеп","окт","ное","дек"],wide:["януари","февруари","март","април","май","юни","юли","август","септември","октомври","ноември","декември"]},n={narrow:["Н","П","В","С","Ч","П","С"],short:["нд","пн","вт","ср","чт","пт","сб"],abbreviated:["нед","пон","вто","сря","чет","пет","съб"],wide:["неделя","понеделник","вторник","сряда","четвъртък","петък","събота"]},s={wide:{am:"преди обяд",pm:"след обяд",midnight:"в полунощ",noon:"на обяд",morning:"сутринта",afternoon:"следобед",evening:"вечерта",night:"през нощта"}};function u(c){return c==="year"||c==="week"||c==="minute"||c==="second"}function d(c){return c==="quarter"}function m(c,v,g,P,h){var y=d(v)?h:u(v)?P:g;return c+"-"+y}function f(c,v){var g=v||{},P=String(g.unit),h=Number(c);if(h===0)return m(0,P,"ев","ева","ево");if(h%1e3===0)return m(h,P,"ен","на","но");if(h%100===0)return m(h,P,"тен","тна","тно");var y=h%100;if(y>20||y<10)switch(y%10){case 1:return m(h,P,"ви","ва","во");case 2:return m(h,P,"ри","ра","ро");case 7:case 8:return m(h,P,"ми","ма","мо")}return m(h,P,"ти","та","то")}var _={ordinalNumber:f,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(c){return Number(c)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide"})};i.a=_},function(l,i,e){var t=e(5),a=e(4),r=/^(\d+)(-?[врмт][аи]|-?т?(ен|на)|-?(ев|ева))?/i,o=/\d+/i,n={narrow:/^((пр)?н\.?\s?е\.?)/i,abbreviated:/^((пр)?н\.?\s?е\.?)/i,wide:/^(преди новата ера|новата ера|нова ера)/i},s={any:[/^п/i,/^н/i]},u={narrow:/^[1234]/i,abbreviated:/^[1234](-?[врт]?o?)? тримес.?/i,wide:/^[1234](-?[врт]?о?)? тримесечие/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[нпвсч]/i,short:/^(нд|пн|вт|ср|чт|пт|сб)/i,abbreviated:/^(нед|пон|вто|сря|чет|пет|съб)/i,wide:/^(неделя|понеделник|вторник|сряда|четвъртък|петък|събота)/i},f={narrow:[/^н/i,/^п/i,/^в/i,/^с/i,/^ч/i,/^п/i,/^с/i],any:[/^н[ед]/i,/^п[он]/i,/^вт/i,/^ср/i,/^ч[ет]/i,/^п[ет]/i,/^с[ъб]/i]},_={abbreviated:/^(яну|фев|мар|апр|май|юни|юли|авг|сеп|окт|ное|дек)/i,wide:/^(януари|февруари|март|април|май|юни|юли|август|септември|октомври|ноември|декември)/i},c={any:[/^я/i,/^ф/i,/^мар/i,/^ап/i,/^май/i,/^юн/i,/^юл/i,/^ав/i,/^се/i,/^окт/i,/^но/i,/^де/i]},v={any:/^(преди о|след о|в по|на о|през|веч|сут|следо)/i},g={any:{am:/^преди о/i,pm:/^след о/i,midnight:/^в пол/i,noon:/^на об/i,morning:/^сут/i,afternoon:/^следо/i,evening:/^веч/i,night:/^през н/i}},P={ordinalNumber:a.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:t.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:t.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:t.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),day:t.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),dayPeriod:t.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(485),a=e(486),r=e(487),o=e(186),n=e(488),s={code:"bn",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:0,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=r;var t=e(186),a={lessThanXSeconds:{one:"প্রায় ১ সেকেন্ড",other:"প্রায় {{count}} সেকেন্ড"},xSeconds:{one:"১ সেকেন্ড",other:"{{count}} সেকেন্ড"},halfAMinute:"আধ মিনিট",lessThanXMinutes:{one:"প্রায় ১ মিনিট",other:"প্রায় {{count}} মিনিট"},xMinutes:{one:"১ মিনিট",other:"{{count}} মিনিট"},aboutXHours:{one:"প্রায় ১ ঘন্টা",other:"প্রায় {{count}} ঘন্টা"},xHours:{one:"১ ঘন্টা",other:"{{count}} ঘন্টা"},xDays:{one:"১ দিন",other:"{{count}} দিন"},aboutXWeeks:{one:"প্রায় ১ সপ্তাহ",other:"প্রায় {{count}} সপ্তাহ"},xWeeks:{one:"১ সপ্তাহ",other:"{{count}} সপ্তাহ"},aboutXMonths:{one:"প্রায় ১ মাস",other:"প্রায় {{count}} মাস"},xMonths:{one:"১ মাস",other:"{{count}} মাস"},aboutXYears:{one:"প্রায় ১ বছর",other:"প্রায় {{count}} বছর"},xYears:{one:"১ বছর",other:"{{count}} বছর"},overXYears:{one:"১ বছরের বেশি",other:"{{count}} বছরের বেশি"},almostXYears:{one:"প্রায় ১ বছর",other:"প্রায় {{count}} বছর"}};function r(o,n,s){s=s||{};var u;return typeof a[o]=="string"?u=a[o]:n===1?u=a[o].one:u=a[o].other.replace("{{count}}",t.a.numberToLocale(n)),s.addSuffix?s.comparison>0?u+" এর মধ্যে":u+" আগে":u}},function(l,i,e){var t=e(2),a={full:"EEEE, MMMM do, y",long:"MMMM do, y",medium:"MMM d, y",short:"MM/dd/yyyy"},r={full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},o={full:"{{date}} {{time}} 'সময়'",long:"{{date}} {{time}} 'সময়'",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'গত' eeee 'সময়' p",yesterday:"'গতকাল' 'সময়' p",today:"'আজ' 'সময়' p",tomorrow:"'আগামীকাল' 'সময়' p",nextWeek:"eeee 'সময়' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(ম|য়|র্থ|ষ্ঠ|শে|ই|তম)?/i,o=/\d+/i,n={narrow:/^(খ্রিঃপূঃ|খ্রিঃ)/i,abbreviated:/^(খ্রিঃপূর্ব|খ্রিঃ)/i,wide:/^(খ্রিস্টপূর্ব|খ্রিস্টাব্দ)/i},s={narrow:[/^খ্রিঃপূঃ/i,/^খ্রিঃ/i],abbreviated:[/^খ্রিঃপূর্ব/i,/^খ্রিঃ/i],wide:[/^খ্রিস্টপূর্ব/i,/^খ্রিস্টাব্দ/i]},u={narrow:/^[১২৩৪]/i,abbreviated:/^[১২৩৪]ত্রৈ/i,wide:/^[১২৩৪](ম|য়|র্থ)? ত্রৈমাসিক/i},d={any:[/১/i,/২/i,/৩/i,/৪/i]},m={narrow:/^(জানু|ফেব্রু|মার্চ|এপ্রিল|মে|জুন|জুলাই|আগস্ট|সেপ্ট|অক্টো|নভে|ডিসে)/i,abbreviated:/^(জানু|ফেব্রু|মার্চ|এপ্রিল|মে|জুন|জুলাই|আগস্ট|সেপ্ট|অক্টো|নভে|ডিসে)/i,wide:/^(জানুয়ারি|ফেব্রুয়ারি|মার্চ|এপ্রিল|মে|জুন|জুলাই|আগস্ট|সেপ্টেম্বর|অক্টোবর|নভেম্বর|ডিসেম্বর)/i},f={any:[/^জানু/i,/^ফেব্রু/i,/^মার্চ/i,/^এপ্রিল/i,/^মে/i,/^জুন/i,/^জুলাই/i,/^আগস্ট/i,/^সেপ্ট/i,/^অক্টো/i,/^নভে/i,/^ডিসে/i]},_={narrow:/^(র|সো|ম|বু|বৃ|শু|শ)+/i,short:/^(রবি|সোম|মঙ্গল|বুধ|বৃহ|শুক্র|শনি)+/i,abbreviated:/^(রবি|সোম|মঙ্গল|বুধ|বৃহ|শুক্র|শনি)+/i,wide:/^(রবিবার|সোমবার|মঙ্গলবার|বুধবার|বৃহস্পতিবার |শুক্রবার|শনিবার)+/i},c={narrow:[/^র/i,/^সো/i,/^ম/i,/^বু/i,/^বৃ/i,/^শু/i,/^শ/i],short:[/^রবি/i,/^সোম/i,/^মঙ্গল/i,/^বুধ/i,/^বৃহ/i,/^শুক্র/i,/^শনি/i],abbreviated:[/^রবি/i,/^সোম/i,/^মঙ্গল/i,/^বুধ/i,/^বৃহ/i,/^শুক্র/i,/^শনি/i],wide:[/^রবিবার/i,/^সোমবার/i,/^মঙ্গলবার/i,/^বুধবার/i,/^বৃহস্পতিবার /i,/^শুক্রবার/i,/^শনিবার/i]},v={narrow:/^(পূ|অপ|মধ্যরাত|মধ্যাহ্ন|সকাল|বিকাল|সন্ধ্যা|রাত)/i,abbreviated:/^(পূর্বাহ্ন|অপরাহ্ন|মধ্যরাত|মধ্যাহ্ন|সকাল|বিকাল|সন্ধ্যা|রাত)/i,wide:/^(পূর্বাহ্ন|অপরাহ্ন|মধ্যরাত|মধ্যাহ্ন|সকাল|বিকাল|সন্ধ্যা|রাত)/i},g={any:{am:/^পূ/i,pm:/^অপ/i,midnight:/^মধ্যরাত/i,noon:/^মধ্যাহ্ন/i,morning:/সকাল/i,afternoon:/বিকাল/i,evening:/সন্ধ্যা/i,night:/রাত/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(490),a=e(491),r=e(492),o=e(493),n=e(494),s={code:"ca",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"menys d'un segon",eleven:"menys d'onze segons",other:"menys de {{count}} segons"},xSeconds:{one:"1 segon",other:"{{count}} segons"},halfAMinute:"mig minut",lessThanXMinutes:{one:"menys d'un minut",eleven:"menys d'onze minuts",other:"menys de {{count}} minuts"},xMinutes:{one:"1 minut",other:"{{count}} minuts"},aboutXHours:{one:"aproximadament una hora",other:"aproximadament {{count}} hores"},xHours:{one:"1 hora",other:"{{count}} hores"},xDays:{one:"1 dia",other:"{{count}} dies"},aboutXWeeks:{one:"aproximadament una setmana",other:"aproximadament {{count}} setmanes"},xWeeks:{one:"1 setmana",other:"{{count}} setmanes"},aboutXMonths:{one:"aproximadament un mes",other:"aproximadament {{count}} mesos"},xMonths:{one:"1 mes",other:"{{count}} mesos"},aboutXYears:{one:"aproximadament un any",other:"aproximadament {{count}} anys"},xYears:{one:"1 any",other:"{{count}} anys"},overXYears:{one:"més d'un any",eleven:"més d'onze anys",other:"més de {{count}} anys"},almostXYears:{one:"gairebé un any",other:"gairebé {{count}} anys"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:o===11&&t[r].eleven?s=t[r].eleven:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"en "+s:"fa "+s:s}},function(l,i,e){var t=e(2),a={full:"EEEE, d 'de' MMMM y",long:"d 'de' MMMM y",medium:"d MMM y",short:"dd/MM/y"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'a les' {{time}}",long:"{{date}} 'a les' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=r;var t={lastWeek:"'el' eeee 'passat a la' LT",yesterday:"'ahir a la' p",today:"'avui a la' p",tomorrow:"'demà a la' p",nextWeek:"eeee 'a la' p",other:"P"},a={lastWeek:"'el' eeee 'passat a les' p",yesterday:"'ahir a les' p",today:"'avui a les' p",tomorrow:"'demà a les' p",nextWeek:"eeee 'a les' p",other:"P"};function r(o,n,s,u){return n.getUTCHours()!==1?a[o]:t[o]}},function(l,i,e){var t=e(3),a={narrow:["aC","dC"],abbreviated:["a. de C.","d. de C."],wide:["abans de Crist","després de Crist"]},r={narrow:["1","2","3","4"],abbreviated:["T1","T2","T3","T4"],wide:["1r trimestre","2n trimestre","3r trimestre","4t trimestre"]},o={narrow:["GN","FB","MÇ","AB","MG","JN","JL","AG","ST","OC","NV","DS"],abbreviated:["gen.","febr.","març","abr.","maig","juny","jul.","ag.","set.","oct.","nov.","des."],wide:["gener","febrer","març","abril","maig","juny","juliol","agost","setembre","octubre","novembre","desembre"]},n={narrow:["dg.","dl.","dt.","dm.","dj.","dv.","ds."],short:["dg.","dl.","dt.","dm.","dj.","dv.","ds."],abbreviated:["dg.","dl.","dt.","dm.","dj.","dv.","ds."],wide:["diumenge","dilluns","dimarts","dimecres","dijous","divendres","dissabte"]},s={narrow:{am:"am",pm:"pm",midnight:"mitjanit",noon:"migdia",morning:"matí",afternoon:"tarda",evening:"vespre",night:"nit"},abbreviated:{am:"a.m.",pm:"p.m.",midnight:"mitjanit",noon:"migdia",morning:"matí",afternoon:"tarda",evening:"vespre",night:"nit"},wide:{am:"ante meridiem",pm:"post meridiem",midnight:"mitjanit",noon:"migdia",morning:"matí",afternoon:"tarda",evening:"vespre",night:"nit"}},u={narrow:{am:"am",pm:"pm",midnight:"de la mitjanit",noon:"del migdia",morning:"del matí",afternoon:"de la tarda",evening:"del vespre",night:"de la nit"},abbreviated:{am:"AM",pm:"PM",midnight:"de la mitjanit",noon:"del migdia",morning:"del matí",afternoon:"de la tarda",evening:"del vespre",night:"de la nit"},wide:{am:"ante meridiem",pm:"post meridiem",midnight:"de la mitjanit",noon:"del migdia",morning:"del matí",afternoon:"de la tarda",evening:"del vespre",night:"de la nit"}};function d(f,_){var c=Number(f),v=c%100;if(v>20||v<10)switch(v%10){case 1:return c+"r";case 2:return c+"n";case 3:return c+"r";case 4:return c+"t"}return c+"è"}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(5),a=e(4),r=/^(\d+)(è|r|n|r|t)?/i,o=/\d+/i,n={narrow:/^(aC|dC)/i,abbreviated:/^(a. de C.|d. de C.)/i,wide:/^(abans de Crist|despr[eé]s de Crist)/i},s={narrow:[/^aC/i,/^dC/i],abbreviated:[/^(a. de C.)/i,/^(d. de C.)/i],wide:[/^(abans de Crist)/i,/^(despr[eé]s de Crist)/i]},u={narrow:/^[1234]/i,abbreviated:/^T[1234]/i,wide:/^[1234](è|r|n|r|t)? trimestre/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^(GN|FB|MÇ|AB|MG|JN|JL|AG|ST|OC|NV|DS)/i,abbreviated:/^(gen.|febr.|març|abr.|maig|juny|jul.|ag.|set.|oct.|nov.|des.)/i,wide:/^(gener|febrer|març|abril|maig|juny|juliol|agost|setembre|octubre|novembre|desembre)/i},f={narrow:[/^GN/i,/^FB/i,/^MÇ/i,/^AB/i,/^MG/i,/^JN/i,/^JL/i,/^AG/i,/^ST/i,/^OC/i,/^NV/i,/^DS/i],abbreviated:[/^gen./i,/^febr./i,/^març/i,/^abr./i,/^maig/i,/^juny/i,/^jul./i,/^ag./i,/^set./i,/^oct./i,/^nov./i,/^des./i],wide:[/^gener/i,/^febrer/i,/^març/i,/^abril/i,/^maig/i,/^juny/i,/^juliol/i,/^agost/i,/^setembre/i,/^octubre/i,/^novembre/i,/^desembre/i]},_={narrow:/^(dg\.|dl\.|dt\.|dm\.|dj\.|dv\.|ds\.)/i,short:/^(dg\.|dl\.|dt\.|dm\.|dj\.|dv\.|ds\.)/i,abbreviated:/^(dg\.|dl\.|dt\.|dm\.|dj\.|dv\.|ds\.)/i,wide:/^(diumenge|dilluns|dimarts|dimecres|dijous|divendres|dissabte)/i},c={narrow:[/^dg./i,/^dl./i,/^dt./i,/^dm./i,/^dj./i,/^dv./i,/^ds./i],abbreviated:[/^dg./i,/^dl./i,/^dt./i,/^dm./i,/^dj./i,/^dv./i,/^ds./i],wide:[/^diumenge/i,/^dilluns/i,/^dimarts/i,/^dimecres/i,/^dijous/i,/^divendres/i,/^disssabte/i]},v={narrow:/^(a|p|mn|md|(del|de la) (matí|tarda|vespre|nit))/i,abbreviated:/^([ap]\.?\s?m\.?|mitjanit|migdia|(del|de la) (matí|tarda|vespre|nit))/i,wide:/^(ante meridiem|post meridiem|mitjanit|migdia|(del|de la) (matí|tarda|vespre|nit))/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/^mitjanit/i,noon:/^migdia/i,morning:/matí/i,afternoon:/tarda/i,evening:/vespre/i,night:/nit/i}},P={ordinalNumber:a.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:t.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:t.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:t.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:t.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:t.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(496),a=e(497),r=e(498),o=e(499),n=e(500),s={code:"cs",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:{regular:"méně než vteřina",past:"před méně než vteřinou",future:"za méně než vteřinu"},few:{regular:"méně než {{count}} vteřiny",past:"před méně než {{count}} vteřinami",future:"za méně než {{count}} vteřiny"},many:{regular:"méně než {{count}} vteřin",past:"před méně než {{count}} vteřinami",future:"za méně než {{count}} vteřin"}},xSeconds:{one:{regular:"vteřina",past:"před vteřinou",future:"za vteřinu"},few:{regular:"{{count}} vteřiny",past:"před {{count}} vteřinami",future:"za {{count}} vteřiny"},many:{regular:"{{count}} vteřin",past:"před {{count}} vteřinami",future:"za {{count}} vteřin"}},halfAMinute:{other:{regular:"půl minuty",past:"před půl minutou",future:"za půl minuty"}},lessThanXMinutes:{one:{regular:"méně než minuta",past:"před méně než minutou",future:"za méně než minutu"},few:{regular:"méně než {{count}} minuty",past:"před méně než {{count}} minutami",future:"za méně než {{count}} minuty"},many:{regular:"méně než {{count}} minut",past:"před méně než {{count}} minutami",future:"za méně než {{count}} minut"}},xMinutes:{one:{regular:"minuta",past:"před minutou",future:"za minutu"},few:{regular:"{{count}} minuty",past:"před {{count}} minutami",future:"za {{count}} minuty"},many:{regular:"{{count}} minut",past:"před {{count}} minutami",future:"za {{count}} minut"}},aboutXHours:{one:{regular:"přibližně hodina",past:"přibližně před hodinou",future:"přibližně za hodinu"},few:{regular:"přibližně {{count}} hodiny",past:"přibližně před {{count}} hodinami",future:"přibližně za {{count}} hodiny"},many:{regular:"přibližně {{count}} hodin",past:"přibližně před {{count}} hodinami",future:"přibližně za {{count}} hodin"}},xHours:{one:{regular:"hodina",past:"před hodinou",future:"za hodinu"},few:{regular:"{{count}} hodiny",past:"před {{count}} hodinami",future:"za {{count}} hodiny"},many:{regular:"{{count}} hodin",past:"před {{count}} hodinami",future:"za {{count}} hodin"}},xDays:{one:{regular:"den",past:"před dnem",future:"za den"},few:{regular:"{{count}} dny",past:"před {{count}} dny",future:"za {{count}} dny"},many:{regular:"{{count}} dní",past:"před {{count}} dny",future:"za {{count}} dní"}},aboutXWeeks:{one:{regular:"přibližně týden",past:"přibližně před týdnem",future:"přibližně za týden"},few:{regular:"přibližně {{count}} týdny",past:"přibližně před {{count}} týdny",future:"přibližně za {{count}} týdny"},many:{regular:"přibližně {{count}} týdnů",past:"přibližně před {{count}} týdny",future:"přibližně za {{count}} týdnů"}},xWeeks:{one:{regular:"týden",past:"před týdnem",future:"za týden"},few:{regular:"{{count}} týdny",past:"před {{count}} týdny",future:"za {{count}} týdny"},many:{regular:"{{count}} týdnů",past:"před {{count}} týdny",future:"za {{count}} týdnů"}},aboutXMonths:{one:{regular:"přibližně měsíc",past:"přibližně před měsícem",future:"přibližně za měsíc"},few:{regular:"přibližně {{count}} měsíce",past:"přibližně před {{count}} měsíci",future:"přibližně za {{count}} měsíce"},many:{regular:"přibližně {{count}} měsíců",past:"přibližně před {{count}} měsíci",future:"přibližně za {{count}} měsíců"}},xMonths:{one:{regular:"měsíc",past:"před měsícem",future:"za měsíc"},few:{regular:"{{count}} měsíce",past:"před {{count}} měsíci",future:"za {{count}} měsíce"},many:{regular:"{{count}} měsíců",past:"před {{count}} měsíci",future:"za {{count}} měsíců"}},aboutXYears:{one:{regular:"přibližně rok",past:"přibližně před rokem",future:"přibližně za rok"},few:{regular:"přibližně {{count}} roky",past:"přibližně před {{count}} roky",future:"přibližně za {{count}} roky"},many:{regular:"přibližně {{count}} roků",past:"přibližně před {{count}} roky",future:"přibližně za {{count}} roků"}},xYears:{one:{regular:"rok",past:"před rokem",future:"za rok"},few:{regular:"{{count}} roky",past:"před {{count}} roky",future:"za {{count}} roky"},many:{regular:"{{count}} roků",past:"před {{count}} roky",future:"za {{count}} roků"}},overXYears:{one:{regular:"více než rok",past:"před více než rokem",future:"za více než rok"},few:{regular:"více než {{count}} roky",past:"před více než {{count}} roky",future:"za více než {{count}} roky"},many:{regular:"více než {{count}} roků",past:"před více než {{count}} roky",future:"za více než {{count}} roků"}},almostXYears:{one:{regular:"skoro rok",past:"skoro před rokem",future:"skoro za rok"},few:{regular:"skoro {{count}} roky",past:"skoro před {{count}} roky",future:"skoro za {{count}} roky"},many:{regular:"skoro {{count}} roků",past:"skoro před {{count}} roky",future:"skoro za {{count}} roků"}}};function a(r,o,n){n=n||{};var s=t[r],u;typeof s.other=="object"?u="other":o===1?u="one":o>1&&o<5||o===0?u="few":u="many";var d=n.addSuffix===!0,m=n.comparison,f;return d&&m===-1?f="past":d&&m===1?f="future":f="regular",s[u][f].replace("{{count}}",o)}},function(l,i,e){var t=e(2),a={full:"EEEE, d. MMMM yyyy",long:"d. MMMM yyyy",medium:"d.M.yyyy",short:"d.M.yy"},r={full:"H:mm:ss zzzz",long:"H:mm:ss z",medium:"H:mm:ss",short:"H:mm"},o={full:"{{date}} 'v' {{time}}",long:"{{date}} 'v' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=r;var t=["neděli","pondělí","úterý","středu","čtvrtek","pátek","sobotu"],a={lastWeek:"'poslední' eeee 've' p",yesterday:"'včera v' p",today:"'dnes v' p",tomorrow:"'zítra v' p",nextWeek:function(o,n,s){var u=o.getUTCDay();return"'v "+t[u]+" o' p"},other:"P"};function r(o,n,s,u){var d=a[o];return typeof d=="function"?d(n,s,u):d}},function(l,i,e){var t=e(3),a={narrow:["př. n. l.","n. l."],abbreviated:["př. n. l.","n. l."],wide:["před naším letopočtem","našeho letopočtu"]},r={narrow:["1","2","3","4"],abbreviated:["1. čtvrtletí","2. čtvrtletí","3. čtvrtletí","4. čtvrtletí"],wide:["1. čtvrtletí","2. čtvrtletí","3. čtvrtletí","4. čtvrtletí"]},o={narrow:["L","Ú","B","D","K","Č","Č","S","Z","Ř","L","P"],abbreviated:["led","úno","bře","dub","kvě","čvn","čvc","srp","zář","říj","lis","pro"],wide:["leden","únor","březen","duben","květen","červen","červenec","srpen","září","říjen","listopad","prosinec"]},n={narrow:["L","Ú","B","D","K","Č","Č","S","Z","Ř","L","P"],abbreviated:["led","úno","bře","dub","kvě","čvn","čvc","srp","zář","říj","lis","pro"],wide:["ledna","února","března","dubna","května","června","července","srpna","září","října","listopadu","prosince"]},s={narrow:["ne","po","út","st","čt","pá","so"],short:["ne","po","út","st","čt","pá","so"],abbreviated:["ned","pon","úte","stř","čtv","pát","sob"],wide:["neděle","pondělí","úterý","středa","čtvrtek","pátek","sobota"]},u={narrow:{am:"dop.",pm:"odp.",midnight:"půlnoc",noon:"poledne",morning:"ráno",afternoon:"odpoledne",evening:"večer",night:"noc"},abbreviated:{am:"dop.",pm:"odp.",midnight:"půlnoc",noon:"poledne",morning:"ráno",afternoon:"odpoledne",evening:"večer",night:"noc"},wide:{am:"dopoledne",pm:"odpoledne",midnight:"půlnoc",noon:"poledne",morning:"ráno",afternoon:"odpoledne",evening:"večer",night:"noc"}},d={narrow:{am:"dop.",pm:"odp.",midnight:"půlnoc",noon:"poledne",morning:"ráno",afternoon:"odpoledne",evening:"večer",night:"noc"},abbreviated:{am:"dop.",pm:"odp.",midnight:"půlnoc",noon:"poledne",morning:"ráno",afternoon:"odpoledne",evening:"večer",night:"noc"},wide:{am:"dopoledne",pm:"odpoledne",midnight:"půlnoc",noon:"poledne",morning:"ráno",afternoon:"odpoledne",evening:"večer",night:"noc"}};function m(_){var c=Number(_);return c+"."}var f={ordinalNumber:m,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(_){return Number(_)-1}}),month:t.a({values:o,defaultWidth:"wide",formattingValues:n,defaultFormattingWidth:"wide"}),day:t.a({values:s,defaultWidth:"wide"}),dayPeriod:t.a({values:u,defaultWidth:"wide",formattingValues:d,defaultFormattingWidth:"wide"})};i.a=f},function(l,i,e){var t=e(5),a=e(4),r=/^(\d+)\.?/i,o=/\d+/i,n={narrow:/^(p[řr]ed Kr\.|pred n\. l\.|po Kr\.|n\. l\.)/i,abbreviated:/^(pe[řr]ed Kr\.|pe[řr]ed n\. l\.|po Kr\.|n\. l\.)/i,wide:/^(p[řr]ed Kristem|pred na[šs][íi]m letopo[čc]tem|po Kristu|na[šs]eho letopo[čc]tu)/i},s={any:[/^p[řr]/i,/^(po|n)/i]},u={narrow:/^[1234]/i,abbreviated:/^[1234]\. [čc]tvrtlet[íi]/i,wide:/^[1234]\. [čc]tvrtlet[íi]/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[lúubdkčcszřrlp]/i,abbreviated:/^(led|[úu]no|b[řr]e|dub|kv[ěe]|[čc]vn|[čc]vc|srp|z[áa][řr]|[řr][íi]j|lis|pro)/i,wide:/^(leden|ledna|[úu]nora?|b[řr]ezen|b[řr]ezna|duben|dubna|kv[ěe]ten|kv[ěe]tna|[čc]erven(ec|ce)?|[čc]ervna|srpen|srpna|z[áa][řr][íi]|[řr][íi]jen|[řr][íi]jna|listopad(a|u)?|prosinec|prosince)/i},f={narrow:[/^l/i,/^[úu]/i,/^b/i,/^d/i,/^k/i,/^[čc]/i,/^[čc]/i,/^s/i,/^z/i,/^[řr]/i,/^l/i,/^p/i],any:[/^led/i,/^[úu]n/i,/^b[řr]e/i,/^dub/i,/^kv[ěe]/i,/^[čc]vn|[čc]erven(?!\w)|[čc]ervna/i,/^[čc]vc|[čc]erven(ec|ce)/i,/^srp/i,/^z[áa][řr]/i,/^[řr][íi]j/i,/^lis/i,/^pro/i]},_={narrow:/^[npuúsčps]/i,short:/^(ne|po|[úu]t|st|[čc]t|p[áa]|so)/i,abbreviated:/^(ne|po|[úu]t|st|[čc]t|p[áa]|so)/i,wide:/^(ned[ěe]le|pond[ěe]l[íi]|[úu]ter[ýy]|st[řr]eda|[čc]tvrtek|p[áa]tek|sobota)/i},c={narrow:[/^n/i,/^p/i,/^[úu]/i,/^s/i,/^[čc]/i,/^p/i,/^s/i],any:[/^ne/i,/^po/i,/^ut/i,/^st/i,/^[čc]t/i,/^p/i,/^so/i]},v={any:/^dopoledne|dop\.?|odpoledne|odp\.?|půlnoc|poledne|r[áa]no|odpoledne|ve[čc]er|(v )?noci/i},g={any:{am:/^dop/i,pm:/^odp/i,midnight:/^p[ůu]lnoc/i,noon:/^poledne/i,morning:/r[áa]no/i,afternoon:/odpoledne/i,evening:/ve[čc]er/i,night:/noc/i}},P={ordinalNumber:a.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:t.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:t.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:t.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:t.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:t.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(502),a=e(503),r=e(504),o=e(505),n=e(506),s={code:"cy",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:0,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"llai na eiliad",other:"llai na {{count}} eiliad"},xSeconds:{one:"1 eiliad",other:"{{count}} eiliad"},halfAMinute:"hanner munud",lessThanXMinutes:{one:"llai na munud",two:"llai na 2 funud",other:"llai na {{count}} munud"},xMinutes:{one:"1 munud",two:"2 funud",other:"{{count}} munud"},aboutXHours:{one:"tua 1 awr",other:"tua {{count}} awr"},xHours:{one:"1 awr",other:"{{count}} awr"},xDays:{one:"1 diwrnod",two:"2 ddiwrnod",other:"{{count}} diwrnod"},aboutXWeeks:{one:"tua 1 wythnos",two:"tua pythefnos",other:"tua {{count}} wythnos"},xWeeks:{one:"1 wythnos",two:"pythefnos",other:"{{count}} wythnos"},aboutXMonths:{one:"tua 1 mis",two:"tua 2 fis",other:"tua {{count}} mis"},xMonths:{one:"1 mis",two:"2 fis",other:"{{count}} mis"},aboutXYears:{one:"tua 1 flwyddyn",two:"tua 2 flynedd",other:"tua {{count}} mlynedd"},xYears:{one:"1 flwyddyn",two:"2 flynedd",other:"{{count}} mlynedd"},overXYears:{one:"dros 1 flwyddyn",two:"dros 2 flynedd",other:"dros {{count}} mlynedd"},almostXYears:{one:"bron 1 flwyddyn",two:"bron 2 flynedd",other:"bron {{count}} mlynedd"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:o===2&&t[r].two?s=t[r].two:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"mewn "+s:s+" yn ôl":s}},function(l,i,e){var t=e(2),a={full:"EEEE, d MMMM yyyy",long:"d MMMM yyyy",medium:"d MMM yyyy",short:"dd/MM/yyyy"},r={full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},o={full:"{{date}} 'am' {{time}}",long:"{{date}} 'am' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"eeee 'diwethaf am' p",yesterday:"'ddoe am' p",today:"'heddiw am' p",tomorrow:"'yfory am' p",nextWeek:"eeee 'am' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["C","O"],abbreviated:["CC","OC"],wide:["Cyn Crist","Ar ôl Crist"]},r={narrow:["1","2","3","4"],abbreviated:["Ch1","Ch2","Ch3","Ch4"],wide:["Chwarter 1af","2ail chwarter","3ydd chwarter","4ydd chwarter"]},o={narrow:["I","Ch","Ma","E","Mi","Me","G","A","Md","H","T","Rh"],abbreviated:["Ion","Chwe","Maw","Ebr","Mai","Meh","Gor","Aws","Med","Hyd","Tach","Rhag"],wide:["Ionawr","Chwefror","Mawrth","Ebrill","Mai","Mehefin","Gorffennaf","Awst","Medi","Hydref","Tachwedd","Rhagfyr"]},n={narrow:["S","Ll","M","M","I","G","S"],short:["Su","Ll","Ma","Me","Ia","Gw","Sa"],abbreviated:["Sul","Llun","Maw","Mer","Iau","Gwe","Sad"],wide:["dydd Sul","dydd Llun","dydd Mawrth","dydd Mercher","dydd Iau","dydd Gwener","dydd Sadwrn"]},s={narrow:{am:"b",pm:"h",midnight:"hn",noon:"hd",morning:"bore",afternoon:"prynhawn",evening:"gyda'r nos",night:"nos"},abbreviated:{am:"yb",pm:"yh",midnight:"hanner nos",noon:"hanner dydd",morning:"bore",afternoon:"prynhawn",evening:"gyda'r nos",night:"nos"},wide:{am:"y.b.",pm:"y.h.",midnight:"hanner nos",noon:"hanner dydd",morning:"bore",afternoon:"prynhawn",evening:"gyda'r nos",night:"nos"}},u={narrow:{am:"b",pm:"h",midnight:"hn",noon:"hd",morning:"yn y bore",afternoon:"yn y prynhawn",evening:"gyda'r nos",night:"yn y nos"},abbreviated:{am:"yb",pm:"yh",midnight:"hanner nos",noon:"hanner dydd",morning:"yn y bore",afternoon:"yn y prynhawn",evening:"gyda'r nos",night:"yn y nos"},wide:{am:"y.b.",pm:"y.h.",midnight:"hanner nos",noon:"hanner dydd",morning:"yn y bore",afternoon:"yn y prynhawn",evening:"gyda'r nos",night:"yn y nos"}};function d(f,_){var c=Number(f);if(c<20)switch(c){case 0:return c+"fed";case 1:return c+"af";case 2:return c+"ail";case 3:case 4:return c+"ydd";case 5:case 6:return c+"ed";case 7:case 8:case 9:case 10:case 12:case 15:case 18:return c+"fed";case 11:case 13:case 14:case 16:case 17:case 19:return c+"eg"}else if(c>=50&&c<=60||c===80||c>=100)return c+"fed";return c+"ain"}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(af|ail|ydd|ed|fed|eg|ain)?/i,o=/\d+/i,n={narrow:/^(c|o)/i,abbreviated:/^(c\.?\s?c\.?|o\.?\s?c\.?)/i,wide:/^(cyn christ|ar ôl crist|ar ol crist)/i},s={wide:[/^c/i,/^(ar ôl crist|ar ol crist)/i],any:[/^c/i,/^o/i]},u={narrow:/^[1234]/i,abbreviated:/^ch[1234]/i,wide:/^(chwarter 1af)|([234](ail|ydd)? chwarter)/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^(i|ch|m|e|g|a|h|t|rh)/i,abbreviated:/^(ion|chwe|maw|ebr|mai|meh|gor|aws|med|hyd|tach|rhag)/i,wide:/^(ionawr|chwefror|mawrth|ebrill|mai|mehefin|gorffennaf|awst|medi|hydref|tachwedd|rhagfyr)/i},f={narrow:[/^i/i,/^ch/i,/^m/i,/^e/i,/^m/i,/^m/i,/^g/i,/^a/i,/^m/i,/^h/i,/^t/i,/^rh/i],any:[/^io/i,/^ch/i,/^maw/i,/^e/i,/^mai/i,/^meh/i,/^g/i,/^a/i,/^med/i,/^h/i,/^t/i,/^rh/i]},_={narrow:/^(s|ll|m|i|g)/i,short:/^(su|ll|ma|me|ia|gw|sa)/i,abbreviated:/^(sul|llun|maw|mer|iau|gwe|sad)/i,wide:/^dydd (sul|llun|mawrth|mercher|iau|gwener|sadwrn)/i},c={narrow:[/^s/i,/^ll/i,/^m/i,/^m/i,/^i/i,/^g/i,/^s/i],wide:[/^dydd su/i,/^dydd ll/i,/^dydd ma/i,/^dydd me/i,/^dydd i/i,/^dydd g/i,/^dydd sa/i],any:[/^su/i,/^ll/i,/^ma/i,/^me/i,/^i/i,/^g/i,/^sa/i]},v={narrow:/^(b|h|hn|hd|(yn y|y|yr|gyda'r) (bore|prynhawn|nos|hwyr))/i,any:/^(y\.?\s?[bh]\.?|hanner nos|hanner dydd|(yn y|y|yr|gyda'r) (bore|prynhawn|nos|hwyr))/i},g={any:{am:/^b|(y\.?\s?b\.?)/i,pm:/^h|(y\.?\s?h\.?)|(yr hwyr)/i,midnight:/^hn|hanner nos/i,noon:/^hd|hanner dydd/i,morning:/bore/i,afternoon:/prynhawn/i,evening:/^gyda'r nos$/i,night:/blah/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(508),a=e(509),r=e(510),o=e(511),n=e(512),s={code:"da",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"mindre end ét sekund",other:"mindre end {{count}} sekunder"},xSeconds:{one:"1 sekund",other:"{{count}} sekunder"},halfAMinute:"ét halvt minut",lessThanXMinutes:{one:"mindre end ét minut",other:"mindre end {{count}} minutter"},xMinutes:{one:"1 minut",other:"{{count}} minutter"},aboutXHours:{one:"cirka 1 time",other:"cirka {{count}} timer"},xHours:{one:"1 time",other:"{{count}} timer"},xDays:{one:"1 dag",other:"{{count}} dage"},aboutXWeeks:{one:"cirka 1 uge",other:"cirka {{count}} uger"},xWeeks:{one:"1 uge",other:"{{count}} uger"},aboutXMonths:{one:"cirka 1 måned",other:"cirka {{count}} måneder"},xMonths:{one:"1 måned",other:"{{count}} måneder"},aboutXYears:{one:"cirka 1 år",other:"cirka {{count}} år"},xYears:{one:"1 år",other:"{{count}} år"},overXYears:{one:"over 1 år",other:"over {{count}} år"},almostXYears:{one:"næsten 1 år",other:"næsten {{count}} år"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"om "+s:s+" siden":s}},function(l,i,e){var t=e(2),a={full:"EEEE 'den' d. MMMM y",long:"d. MMMM y",medium:"d. MMM y",short:"dd/MM/y"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'kl'. {{time}}",long:"{{date}} 'kl'. {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'sidste' eeee 'kl.' p",yesterday:"'i går kl.' p",today:"'i dag kl.' p",tomorrow:"'i morgen kl.' p",nextWeek:"'på' eeee 'kl.' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["fvt","vt"],abbreviated:["f.v.t.","v.t."],wide:["før vesterlandsk tidsregning","vesterlandsk tidsregning"]},r={narrow:["1","2","3","4"],abbreviated:["1. kvt.","2. kvt.","3. kvt.","4. kvt."],wide:["1. kvartal","2. kvartal","3. kvartal","4. kvartal"]},o={narrow:["J","F","M","A","M","J","J","A","S","O","N","D"],abbreviated:["jan.","feb.","mar.","apr.","maj","jun.","jul.","aug.","sep.","okt.","nov.","dec."],wide:["januar","februar","marts","april","maj","juni","juli","august","september","oktober","november","december"]},n={narrow:["S","M","T","O","T","F","L"],short:["sø","ma","ti","on","to","fr","lø"],abbreviated:["søn.","man.","tir.","ons.","tor.","fre.","lør."],wide:["søndag","mandag","tirsdag","onsdag","torsdag","fredag","lørdag"]},s={narrow:{am:"a",pm:"p",midnight:"midnat",noon:"middag",morning:"morgen",afternoon:"eftermiddag",evening:"aften",night:"nat"},abbreviated:{am:"AM",pm:"PM",midnight:"midnat",noon:"middag",morning:"morgen",afternoon:"eftermiddag",evening:"aften",night:"nat"},wide:{am:"a.m.",pm:"p.m.",midnight:"midnat",noon:"middag",morning:"morgen",afternoon:"eftermiddag",evening:"aften",night:"nat"}},u={narrow:{am:"a",pm:"p",midnight:"midnat",noon:"middag",morning:"om morgenen",afternoon:"om eftermiddagen",evening:"om aftenen",night:"om natten"},abbreviated:{am:"AM",pm:"PM",midnight:"midnat",noon:"middag",morning:"om morgenen",afternoon:"om eftermiddagen",evening:"om aftenen",night:"om natten"},wide:{am:"a.m.",pm:"p.m.",midnight:"midnat",noon:"middag",morning:"om morgenen",afternoon:"om eftermiddagen",evening:"om aftenen",night:"om natten"}};function d(f){var _=Number(f);return _+"."}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(5),a=e(4),r=/^(\d+)(\.)?/i,o=/\d+/i,n={narrow:/^(fKr|fvt|eKr|vt)/i,abbreviated:/^(f\.Kr\.?|f\.v\.t\.?|e\.Kr\.?|v\.t\.)/i,wide:/^(f.Kr.|før vesterlandsk tidsregning|e.Kr.|vesterlandsk tidsregning)/i},s={any:[/^f/i,/^(v|e)/i]},u={narrow:/^[1234]/i,abbreviated:/^[1234]. kvt\./i,wide:/^[1234]\.? kvartal/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[jfmasond]/i,abbreviated:/^(jan.|feb.|mar.|apr.|maj|jun.|jul.|aug.|sep.|okt.|nov.|dec.)/i,wide:/^(januar|februar|marts|april|maj|juni|juli|august|september|oktober|november|december)/i},f={narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^mar/i,/^ap/i,/^maj/i,/^jun/i,/^jul/i,/^au/i,/^s/i,/^o/i,/^n/i,/^d/i]},_={narrow:/^[smtofl]/i,short:/^(søn.|man.|tir.|ons.|tor.|fre.|lør.)/i,abbreviated:/^(søn|man|tir|ons|tor|fre|lør)/i,wide:/^(søndag|mandag|tirsdag|onsdag|torsdag|fredag|lørdag)/i},c={narrow:[/^s/i,/^m/i,/^t/i,/^o/i,/^t/i,/^f/i,/^l/i],any:[/^s/i,/^m/i,/^ti/i,/^o/i,/^to/i,/^f/i,/^l/i]},v={narrow:/^(a|p|midnat|middag|(om) (morgenen|eftermiddagen|aftenen|natten))/i,any:/^([ap]\.?\s?m\.?|midnat|middag|(om) (morgenen|eftermiddagen|aftenen|natten))/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/midnat/i,noon:/middag/i,morning:/morgen/i,afternoon:/eftermiddag/i,evening:/aften/i,night:/nat/i}},P={ordinalNumber:a.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:t.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:t.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:t.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:t.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:t.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(514),a=e(515),r=e(516),o=e(517),n=e(518),s={code:"de",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{standalone:{one:"weniger als eine Sekunde",other:"weniger als {{count}} Sekunden"},withPreposition:{one:"weniger als einer Sekunde",other:"weniger als {{count}} Sekunden"}},xSeconds:{standalone:{one:"eine Sekunde",other:"{{count}} Sekunden"},withPreposition:{one:"einer Sekunde",other:"{{count}} Sekunden"}},halfAMinute:{standalone:"eine halbe Minute",withPreposition:"einer halben Minute"},lessThanXMinutes:{standalone:{one:"weniger als eine Minute",other:"weniger als {{count}} Minuten"},withPreposition:{one:"weniger als einer Minute",other:"weniger als {{count}} Minuten"}},xMinutes:{standalone:{one:"eine Minute",other:"{{count}} Minuten"},withPreposition:{one:"einer Minute",other:"{{count}} Minuten"}},aboutXHours:{standalone:{one:"etwa eine Stunde",other:"etwa {{count}} Stunden"},withPreposition:{one:"etwa einer Stunde",other:"etwa {{count}} Stunden"}},xHours:{standalone:{one:"eine Stunde",other:"{{count}} Stunden"},withPreposition:{one:"einer Stunde",other:"{{count}} Stunden"}},xDays:{standalone:{one:"ein Tag",other:"{{count}} Tage"},withPreposition:{one:"einem Tag",other:"{{count}} Tagen"}},aboutXWeeks:{standalone:{one:"etwa ein Woche",other:"etwa {{count}} Wochen"},withPreposition:{one:"etwa einem Woche",other:"etwa {{count}} Wochen"}},xWeeks:{standalone:{one:"ein Woche",other:"{{count}} Wochen"},withPreposition:{one:"einem Woche",other:"{{count}} Wochen"}},aboutXMonths:{standalone:{one:"etwa ein Monat",other:"etwa {{count}} Monate"},withPreposition:{one:"etwa einem Monat",other:"etwa {{count}} Monaten"}},xMonths:{standalone:{one:"ein Monat",other:"{{count}} Monate"},withPreposition:{one:"einem Monat",other:"{{count}} Monaten"}},aboutXYears:{standalone:{one:"etwa ein Jahr",other:"etwa {{count}} Jahre"},withPreposition:{one:"etwa einem Jahr",other:"etwa {{count}} Jahren"}},xYears:{standalone:{one:"ein Jahr",other:"{{count}} Jahre"},withPreposition:{one:"einem Jahr",other:"{{count}} Jahren"}},overXYears:{standalone:{one:"mehr als ein Jahr",other:"mehr als {{count}} Jahre"},withPreposition:{one:"mehr als einem Jahr",other:"mehr als {{count}} Jahren"}},almostXYears:{standalone:{one:"fast ein Jahr",other:"fast {{count}} Jahre"},withPreposition:{one:"fast einem Jahr",other:"fast {{count}} Jahren"}}};function a(r,o,n){n=n||{};var s=n.addSuffix?t[r].withPreposition:t[r].standalone,u;return typeof s=="string"?u=s:o===1?u=s.one:u=s.other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"in "+u:"vor "+u:u}},function(l,i,e){var t=e(2),a={full:"EEEE, do MMMM y",long:"do MMMM y",medium:"do MMM. y",short:"dd.MM.y"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'um' {{time}}",long:"{{date}} 'um' {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'letzten' eeee 'um' p",yesterday:"'gestern um' p",today:"'heute um' p",tomorrow:"'morgen um' p",nextWeek:"eeee 'um' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["v.Chr.","n.Chr."],abbreviated:["v.Chr.","n.Chr."],wide:["vor Christus","nach Christus"]},r={narrow:["1","2","3","4"],abbreviated:["Q1","Q2","Q3","Q4"],wide:["1. Quartal","2. Quartal","3. Quartal","4. Quartal"]},o={narrow:["J","F","M","A","M","J","J","A","S","O","N","D"],abbreviated:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],wide:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"]},n={narrow:["S","M","D","M","D","F","S"],short:["So","Mo","Di","Mi","Do","Fr","Sa"],abbreviated:["So.","Mo.","Di.","Mi.","Do.","Fr.","Sa."],wide:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"]},s={narrow:{am:"vm.",pm:"nm.",midnight:"Mitternacht",noon:"Mittag",morning:"Morgen",afternoon:"Nachm.",evening:"Abend",night:"Nacht"},abbreviated:{am:"vorm.",pm:"nachm.",midnight:"Mitternacht",noon:"Mittag",morning:"Morgen",afternoon:"Nachmittag",evening:"Abend",night:"Nacht"},wide:{am:"vormittags",pm:"nachmittags",midnight:"Mitternacht",noon:"Mittag",morning:"Morgen",afternoon:"Nachmittag",evening:"Abend",night:"Nacht"}},u={narrow:{am:"vm.",pm:"nm.",midnight:"Mitternacht",noon:"Mittag",morning:"morgens",afternoon:"nachm.",evening:"abends",night:"nachts"},abbreviated:{am:"vorm.",pm:"nachm.",midnight:"Mitternacht",noon:"Mittag",morning:"morgens",afternoon:"nachmittags",evening:"abends",night:"nachts"},wide:{am:"vormittags",pm:"nachmittags",midnight:"Mitternacht",noon:"Mittag",morning:"morgens",afternoon:"nachmittags",evening:"abends",night:"nachts"}};function d(f,_){var c=Number(f);return c+"."}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(\.)?/i,o=/\d+/i,n={narrow:/^(v\.? ?Chr\.?|n\.? ?Chr\.?)/i,abbreviated:/^(v\.? ?Chr\.?|n\.? ?Chr\.?)/i,wide:/^(vor Christus|vor unserer Zeitrechnung|nach Christus|unserer Zeitrechnung)/i},s={any:[/^v/i,/^n/i]},u={narrow:/^[1234]/i,abbreviated:/^q[1234]/i,wide:/^[1234](\.)? Quartal/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[jfmasond]/i,abbreviated:/^(jan|feb|mär|apr|mai|jun|jul|aug|sep|okt|nov|dez)/i,wide:/^(januar|februar|märz|april|mai|juni|juli|august|september|oktober|november|dezember)/i},f={narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^mär/i,/^ap/i,/^mai/i,/^jun/i,/^jul/i,/^au/i,/^s/i,/^o/i,/^n/i,/^d/i]},_={narrow:/^[smdmf]/i,short:/^(so|mo|di|mi|do|fr|sa)/i,abbreviated:/^(son?|mon?|die?|mit?|don?|fre?|sam?)\.?/i,wide:/^(sonntag|montag|dienstag|mittwoch|donnerstag|freitag|samstag)/i},c={any:[/^so/i,/^mo/i,/^di/i,/^mi/i,/^do/i,/^f/i,/^sa/i]},v={narrow:/^(vm\.?|nm\.?|Mitternacht|Mittag|morgens|nachm\.?|abends|nachts)/i,abbreviated:/^(vorm\.?|nachm\.?|Mitternacht|Mittag|morgens|nachm\.?|abends|nachts)/i,wide:/^(vormittags|nachmittags|Mitternacht|Mittag|morgens|nachmittags|abends|nachts)/i},g={any:{am:/^v/i,pm:/^n/i,midnight:/^Mitte/i,noon:/^Mitta/i,morning:/morgens/i,afternoon:/nachmittags/i,evening:/abends/i,night:/nachts/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"wide",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(520),a=e(521),r=e(522),o=e(523),n=e(524),s={code:"el",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"λιγότερο από ένα δευτερόλεπτο",other:"λιγότερο από {{count}} δευτερόλεπτα"},xSeconds:{one:"1 δευτερόλεπτο",other:"{{count}} δευτερόλεπτα"},halfAMinute:"μισό λεπτό",lessThanXMinutes:{one:"λιγότερο από ένα λεπτό",other:"λιγότερο από {{count}} λεπτά"},xMinutes:{one:"1 λεπτό",other:"{{count}} λεπτά"},aboutXHours:{one:"περίπου 1 ώρα",other:"περίπου {{count}} ώρες"},xHours:{one:"1 ώρα",other:"{{count}} ώρες"},xDays:{one:"1 ημέρα",other:"{{count}} ημέρες"},aboutXWeeks:{one:"περίπου 1 εβδομάδα",other:"περίπου {{count}} εβδομάδες"},xWeeks:{one:"1 εβδομάδα",other:"{{count}} εβδομάδες"},aboutXMonths:{one:"περίπου 1 μήνας",other:"περίπου {{count}} μήνες"},xMonths:{one:"1 μήνας",other:"{{count}} μήνες"},aboutXYears:{one:"περίπου 1 χρόνο",other:"περίπου {{count}} χρόνια"},xYears:{one:"1 χρόνο",other:"{{count}} χρόνια"},overXYears:{one:"πάνω από 1 χρόνο",other:"πάνω από {{count}} χρόνια"},almostXYears:{one:"περίπου 1 χρόνο",other:"περίπου {{count}} χρόνια"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"σε "+s:s+" πριν":s}},function(l,i,e){var t=e(2),a={full:"EEEE, d MMMM y",long:"d MMMM y",medium:"d MMM y",short:"d/M/yy"},r={full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},o={full:"{{date}} - {{time}}",long:"{{date}} - {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:function(r){switch(r.getUTCDay()){case 6:return"'το προηγούμενο' eeee 'στις' p";default:return"'την προηγούμενη' eeee 'στις' p"}},yesterday:"'χθες στις' p",today:"'σήμερα στις' p",tomorrow:"'αύριο στις' p",nextWeek:"eeee 'στις' p",other:"P"};function a(r,o,n,s){var u=t[r];return typeof u=="function"?u(o,n,s):u}},function(l,i,e){var t=e(3),a={narrow:["πΧ","μΧ"],abbreviated:["π.Χ.","μ.Χ."],wide:["προ Χριστού","μετά Χριστόν"]},r={narrow:["1","2","3","4"],abbreviated:["Τ1","Τ2","Τ3","Τ4"],wide:["1ο τρίμηνο","2ο τρίμηνο","3ο τρίμηνο","4ο τρίμηνο"]},o={narrow:["Ι","Φ","Μ","Α","Μ","Ι","Ι","Α","Σ","Ο","Ν","Δ"],abbreviated:["Ιαν","Φεβ","Μάρ","Απρ","Μάι","Ιούν","Ιούλ","Αύγ","Σεπ","Οκτ","Νοέ","Δεκ"],wide:["Ιανουάριος","Φεβρουάριος","Μάρτιος","Απρίλιος","Μάιος","Ιούνιος","Ιούλιος","Αύγουστος","Σεπτέμβριος","Οκτώβριος","Νοέμβριος","Δεκέμβριος"]},n={narrow:["Ι","Φ","Μ","Α","Μ","Ι","Ι","Α","Σ","Ο","Ν","Δ"],abbreviated:["Ιαν","Φεβ","Μαρ","Απρ","Μαΐ","Ιουν","Ιουλ","Αυγ","Σεπ","Οκτ","Νοε","Δεκ"],wide:["Ιανουαρίου","Φεβρουαρίου","Μαρτίου","Απριλίου","Μαΐου","Ιουνίου","Ιουλίου","Αυγούστου","Σεπτεμβρίου","Οκτωβρίου","Νοεμβρίου","Δεκεμβρίου"]},s={narrow:["Κ","Δ","T","Τ","Π","Π","Σ"],short:["Κυ","Δε","Τρ","Τε","Πέ","Πα","Σά"],abbreviated:["Κυρ","Δευ","Τρί","Τετ","Πέμ","Παρ","Σάβ"],wide:["Κυριακή","Δευτέρα","Τρίτη","Τετάρτη","Πέμπτη","Παρασκευή","Σάββατο"]},u={narrow:{am:"πμ",pm:"μμ",midnight:"μεσάνυχτα",noon:"μεσημέρι",morning:"πρωί",afternoon:"απόγευμα",evening:"βράδυ",night:"νύχτα"},abbreviated:{am:"π.μ.",pm:"μ.μ.",midnight:"μεσάνυχτα",noon:"μεσημέρι",morning:"πρωί",afternoon:"απόγευμα",evening:"βράδυ",night:"νύχτα"},wide:{am:"π.μ.",pm:"μ.μ.",midnight:"μεσάνυχτα",noon:"μεσημέρι",morning:"πρωί",afternoon:"απόγευμα",evening:"βράδυ",night:"νύχτα"}};function d(f,_){var c=_||{},v=String(c.unit),g;return v==="year"||v==="month"?g="ος":v==="week"||v==="dayOfYear"||v==="day"||v==="hour"||v==="date"?g="η":g="ο",f+g}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide",formattingValues:n,defaultFormattingWidth:"wide"}),day:t.a({values:s,defaultWidth:"wide"}),dayPeriod:t.a({values:u,defaultWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(ος|η|ο)?/i,o=/\d+/i,n={narrow:/^(πΧ|μΧ)/i,abbreviated:/^(π\.?\s?χ\.?|π\.?\s?κ\.?\s?χ\.?|μ\.?\s?χ\.?|κ\.?\s?χ\.?)/i,wide:/^(προ Χριστο(ύ|υ)|πριν απ(ό|ο) την Κοιν(ή|η) Χρονολογ(ί|ι)α|μετ(ά|α) Χριστ(ό|ο)ν|Κοιν(ή|η) Χρονολογ(ί|ι)α)/i},s={any:[/^π/i,/^(μ|κ)/i]},u={narrow:/^[1234]/i,abbreviated:/^τ[1234]/i,wide:/^[1234]ο? τρ(ί|ι)μηνο/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[ιφμαμιιασονδ]/i,abbreviated:/^(ιαν|φεβ|μ[άα]ρ|απρ|μ[άα][ιΐ]|ιο[ύυ]ν|ιο[ύυ]λ|α[ύυ]γ|σεπ|οκτ|νο[έε]|δεκ)/i,wide:/^(μ[άα][ιΐ]|α[ύυ]γο[υύ]στ)(ος|ου)|(ιανου[άα]ρ|φεβρου[άα]ρ|μ[άα]ρτ|απρ[ίι]λ|ιο[ύυ]ν|ιο[ύυ]λ|σεπτ[έε]μβρ|οκτ[ώω]βρ|νο[έε]μβρ|δεκ[έε]μβρ)(ιος|ίου)/i},f={narrow:[/^ι/i,/^φ/i,/^μ/i,/^α/i,/^μ/i,/^ι/i,/^ι/i,/^α/i,/^σ/i,/^ο/i,/^ν/i,/^δ/i],any:[/^ια/i,/^φ/i,/^μ[άα]ρ/i,/^απ/i,/^μ[άα][ιΐ]/i,/^ιο[ύυ]ν/i,/^ιο[ύυ]λ/i,/^α[ύυ]/i,/^σ/i,/^ο/i,/^ν/i,/^δ/i]},_={narrow:/^[κδτπσ]/i,short:/^(κυ|δε|τρ|τε|π[εέ]|π[αά]|σ[αά])/i,abbreviated:/^(κυρ|δευ|τρι|τετ|πεμ|παρ|σαβ)/i,wide:/^(κυριακ(ή|η)|δευτ(έ|ε)ρα|τρ(ί|ι)τη|τετ(ά|α)ρτη|π(έ|ε)μπτη|παρασκευ(ή|η)|σ(ά|α)ββατο)/i},c={narrow:[/^κ/i,/^δ/i,/^τ/i,/^τ/i,/^π/i,/^π/i,/^σ/i],any:[/^κ/i,/^δ/i,/^τρ/i,/^τε/i,/^π[εέ]/i,/^π[αά]/i,/^σ/i]},v={narrow:/^(πμ|μμ|μεσ(ά|α)νυχτα|μεσημ(έ|ε)ρι|πρω(ί|ι)|απ(ό|ο)γευμα|βρ(ά|α)δυ|ν(ύ|υ)χτα)/i,any:/^([πμ]\.?\s?μ\.?|μεσ(ά|α)νυχτα|μεσημ(έ|ε)ρι|πρω(ί|ι)|απ(ό|ο)γευμα|βρ(ά|α)δυ|ν(ύ|υ)χτα)/i},g={any:{am:/^πμ|π\.\s?μ\./i,pm:/^μμ|μ\.\s?μ\./i,midnight:/^μεσάν/i,noon:/^μεσημ(έ|ε)/i,morning:/πρω(ί|ι)/i,afternoon:/απ(ό|ο)γευμα/i,evening:/βρ(ά|α)δυ/i,night:/ν(ύ|υ)χτα/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(48),a=e(526),r=e(35),o=e(36),n=e(37),s={code:"en-AU",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){var t=e(2),a={full:"EEEE, d MMMM yyyy",long:"d MMMM yyyy",medium:"d MMM yyyy",short:"dd/MM/yyyy"},r={full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},o={full:"{{date}} 'at' {{time}}",long:"{{date}} 'at' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){var t=e(528),a=e(529),r=e(35),o=e(36),n=e(37),s={code:"en-CA",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:0,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"less than a second",other:"less than {{count}} seconds"},xSeconds:{one:"a second",other:"{{count}} seconds"},halfAMinute:"half a minute",lessThanXMinutes:{one:"less than a minute",other:"less than {{count}} minutes"},xMinutes:{one:"a minute",other:"{{count}} minutes"},aboutXHours:{one:"about an hour",other:"about {{count}} hours"},xHours:{one:"an hour",other:"{{count}} hours"},xDays:{one:"a day",other:"{{count}} days"},aboutXWeeks:{one:"about a week",other:"about {{count}} weeks"},xWeeks:{one:"a week",other:"{{count}} weeks"},aboutXMonths:{one:"about a month",other:"about {{count}} months"},xMonths:{one:"a month",other:"{{count}} months"},aboutXYears:{one:"about a year",other:"about {{count}} years"},xYears:{one:"a year",other:"{{count}} years"},overXYears:{one:"over a year",other:"over {{count}} years"},almostXYears:{one:"almost a year",other:"almost {{count}} years"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"in "+s:s+" ago":s}},function(l,i,e){var t=e(2),a={full:"EEEE, MMMM do, yyyy",long:"MMMM do, yyyy",medium:"MMM d, yyyy",short:"yyyy-MM-dd"},r={full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},o={full:"{{date}} 'at' {{time}}",long:"{{date}} 'at' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){var t=e(48),a=e(531),r=e(35),o=e(36),n=e(37),s={code:"en-GB",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){var t=e(2),a={full:"EEEE, d MMMM yyyy",long:"d MMMM yyyy",medium:"d MMM yyyy",short:"dd/MM/yyyy"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'at' {{time}}",long:"{{date}} 'at' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){var t=e(48),a=e(533),r=e(35),o=e(36),n=e(37),s={code:"en-IN",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){var t=e(2),a={full:"EEEE, d MMMM yyyy",long:"d MMMM, yyyy",medium:"d MMM, yyyy",short:"dd/MM/yyyy"},r={full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},o={full:"{{date}} 'at' {{time}}",long:"{{date}} 'at' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){var t=e(48),a=e(535),r=e(35),o=e(36),n=e(37),s={code:"en-NZ",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){var t=e(2),a={full:"EEEE, d MMMM yyyy",long:"d MMMM yyyy",medium:"d MMM yyyy",short:"dd/MM/yyyy"},r={full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},o={full:"{{date}} 'at' {{time}}",long:"{{date}} 'at' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){var t=e(537),a=e(538),r=e(539),o=e(540),n=e(541),s={code:"eo",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"malpli ol sekundo",other:"malpli ol {{count}} sekundoj"},xSeconds:{one:"1 sekundo",other:"{{count}} sekundoj"},halfAMinute:"duonminuto",lessThanXMinutes:{one:"malpli ol minuto",other:"malpli ol {{count}} minutoj"},xMinutes:{one:"1 minuto",other:"{{count}} minutoj"},aboutXHours:{one:"proksimume 1 horo",other:"proksimume {{count}} horoj"},xHours:{one:"1 horo",other:"{{count}} horoj"},xDays:{one:"1 tago",other:"{{count}} tagoj"},aboutXMonths:{one:"proksimume 1 monato",other:"proksimume {{count}} monatoj"},xWeeks:{one:"1 semajno",other:"{{count}} semajnoj"},aboutXWeeks:{one:"proksimume 1 semajno",other:"proksimume {{count}} semajnoj"},xMonths:{one:"1 monato",other:"{{count}} monatoj"},aboutXYears:{one:"proksimume 1 jaro",other:"proksimume {{count}} jaroj"},xYears:{one:"1 jaro",other:"{{count}} jaroj"},overXYears:{one:"pli ol 1 jaro",other:"pli ol {{count}} jaroj"},almostXYears:{one:"preskaŭ 1 jaro",other:"preskaŭ {{count}} jaroj"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"post "+s:"antaŭ "+s:s}},function(l,i,e){var t=e(2),a={full:"EEEE, do 'de' MMMM y",long:"y-MMMM-dd",medium:"y-MMM-dd",short:"yyyy-MM-dd"},r={full:"Ho 'horo kaj' m:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={any:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"any"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'pasinta' eeee 'je' p",yesterday:"'hieraŭ je' p",today:"'hodiaŭ je' p",tomorrow:"'morgaŭ je' p",nextWeek:"eeee 'je' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["aK","pK"],abbreviated:["a.K.E.","p.K.E."],wide:["antaŭ Komuna Erao","Komuna Erao"]},r={narrow:["1","2","3","4"],abbreviated:["K1","K2","K3","K4"],wide:["1-a kvaronjaro","2-a kvaronjaro","3-a kvaronjaro","4-a kvaronjaro"]},o={narrow:["J","F","M","A","M","J","J","A","S","O","N","D"],abbreviated:["jan","feb","mar","apr","maj","jun","jul","aŭg","sep","okt","nov","dec"],wide:["januaro","februaro","marto","aprilo","majo","junio","julio","aŭgusto","septembro","oktobro","novembro","decembro"]},n={narrow:["D","L","M","M","Ĵ","V","S"],short:["di","lu","ma","me","ĵa","ve","sa"],abbreviated:["dim","lun","mar","mer","ĵaŭ","ven","sab"],wide:["dimanĉo","lundo","mardo","merkredo","ĵaŭdo","vendredo","sabato"]},s={narrow:{am:"a",pm:"p",midnight:"noktomezo",noon:"tagmezo",morning:"matene",afternoon:"posttagmeze",evening:"vespere",night:"nokte"},abbreviated:{am:"a.t.m.",pm:"p.t.m.",midnight:"noktomezo",noon:"tagmezo",morning:"matene",afternoon:"posttagmeze",evening:"vespere",night:"nokte"},wide:{am:"antaŭtagmeze",pm:"posttagmeze",midnight:"noktomezo",noon:"tagmezo",morning:"matene",afternoon:"posttagmeze",evening:"vespere",night:"nokte"}};function u(m){var f=Number(m);return f+"-a"}var d={ordinalNumber:u,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(m){return Number(m)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide"})};i.a=d},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(-?a)?/i,o=/\d+/i,n={narrow:/^([ap]k)/i,abbreviated:/^([ap]\.?\s?k\.?\s?e\.?)/i,wide:/^((antaǔ |post )?komuna erao)/i},s={any:[/^a/i,/^[kp]/i]},u={narrow:/^[1234]/i,abbreviated:/^k[1234]/i,wide:/^[1234](-?a)? kvaronjaro/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[jfmasond]/i,abbreviated:/^(jan|feb|mar|apr|maj|jun|jul|a(ŭ|ux|uh|u)g|sep|okt|nov|dec)/i,wide:/^(januaro|februaro|marto|aprilo|majo|junio|julio|a(ŭ|ux|uh|u)gusto|septembro|oktobro|novembro|decembro)/i},f={narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^mar/i,/^ap/i,/^maj/i,/^jun/i,/^jul/i,/^a(u|ŭ)/i,/^s/i,/^o/i,/^n/i,/^d/i]},_={narrow:/^[dlmĵjvs]/i,short:/^(di|lu|ma|me|(ĵ|jx|jh|j)a|ve|sa)/i,abbreviated:/^(dim|lun|mar|mer|(ĵ|jx|jh|j)a(ŭ|ux|uh|u)|ven|sab)/i,wide:/^(diman(ĉ|cx|ch|c)o|lundo|mardo|merkredo|(ĵ|jx|jh|j)a(ŭ|ux|uh|u)do|vendredo|sabato)/i},c={narrow:[/^d/i,/^l/i,/^m/i,/^m/i,/^(j|ĵ)/i,/^v/i,/^s/i],any:[/^d/i,/^l/i,/^ma/i,/^me/i,/^(j|ĵ)/i,/^v/i,/^s/i]},v={narrow:/^([ap]|(posttagmez|noktomez|tagmez|maten|vesper|nokt)[eo])/i,abbreviated:/^([ap][.\s]?t[.\s]?m[.\s]?|(posttagmez|noktomez|tagmez|maten|vesper|nokt)[eo])/i,wide:/^(anta(ŭ|ux)tagmez|posttagmez|noktomez|tagmez|maten|vesper|nokt)[eo]/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/^noktom/i,noon:/^t/i,morning:/^m/i,afternoon:/^posttagmeze/i,evening:/^v/i,night:/^n/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"wide",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(543),a=e(544),r=e(545),o=e(546),n=e(547),s={code:"es",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"menos de un segundo",other:"menos de {{count}} segundos"},xSeconds:{one:"1 segundo",other:"{{count}} segundos"},halfAMinute:"medio minuto",lessThanXMinutes:{one:"menos de un minuto",other:"menos de {{count}} minutos"},xMinutes:{one:"1 minuto",other:"{{count}} minutos"},aboutXHours:{one:"alrededor de 1 hora",other:"alrededor de {{count}} horas"},xHours:{one:"1 hora",other:"{{count}} horas"},xDays:{one:"1 día",other:"{{count}} días"},aboutXWeeks:{one:"alrededor de 1 semana",other:"alrededor de {{count}} semanas"},xWeeks:{one:"1 semana",other:"{{count}} semanas"},aboutXMonths:{one:"alrededor de 1 mes",other:"alrededor de {{count}} meses"},xMonths:{one:"1 mes",other:"{{count}} meses"},aboutXYears:{one:"alrededor de 1 año",other:"alrededor de {{count}} años"},xYears:{one:"1 año",other:"{{count}} años"},overXYears:{one:"más de 1 año",other:"más de {{count}} años"},almostXYears:{one:"casi 1 año",other:"casi {{count}} años"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"en "+s:"hace "+s:s}},function(l,i,e){var t=e(2),a={full:"EEEE, d 'de' MMMM 'de' y",long:"d 'de' MMMM 'de' y",medium:"d MMM y",short:"dd/MM/y"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'a las' {{time}}",long:"{{date}} 'a las' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=r;var t={lastWeek:"'el' eeee 'pasado a la' LT",yesterday:"'ayer a la' p",today:"'hoy a la' p",tomorrow:"'mañana a la' p",nextWeek:"eeee 'a la' p",other:"P"},a={lastWeek:"'el' eeee 'pasado a las' p",yesterday:"'ayer a las' p",today:"'hoy a las' p",tomorrow:"'mañana a las' p",nextWeek:"eeee 'a las' p",other:"P"};function r(o,n,s,u){return n.getUTCHours()!==1?a[o]:t[o]}},function(l,i,e){var t=e(3),a={narrow:["AC","DC"],abbreviated:["AC","DC"],wide:["antes de cristo","después de cristo"]},r={narrow:["1","2","3","4"],abbreviated:["T1","T2","T3","T4"],wide:["1º trimestre","2º trimestre","3º trimestre","4º trimestre"]},o={narrow:["e","f","m","a","m","j","j","a","s","o","n","d"],abbreviated:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],wide:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"]},n={narrow:["d","l","m","m","j","v","s"],short:["do","lu","ma","mi","ju","vi","sa"],abbreviated:["dom","lun","mar","mié","jue","vie","sáb"],wide:["domingo","lunes","martes","miércoles","jueves","viernes","sábado"]},s={narrow:{am:"a",pm:"p",midnight:"mn",noon:"md",morning:"mañana",afternoon:"tarde",evening:"tarde",night:"noche"},abbreviated:{am:"AM",pm:"PM",midnight:"medianoche",noon:"mediodia",morning:"mañana",afternoon:"tarde",evening:"tarde",night:"noche"},wide:{am:"a.m.",pm:"p.m.",midnight:"medianoche",noon:"mediodia",morning:"mañana",afternoon:"tarde",evening:"tarde",night:"noche"}},u={narrow:{am:"a",pm:"p",midnight:"mn",noon:"md",morning:"de la mañana",afternoon:"de la tarde",evening:"de la tarde",night:"de la noche"},abbreviated:{am:"AM",pm:"PM",midnight:"medianoche",noon:"mediodia",morning:"de la mañana",afternoon:"de la tarde",evening:"de la tarde",night:"de la noche"},wide:{am:"a.m.",pm:"p.m.",midnight:"medianoche",noon:"mediodia",morning:"de la mañana",afternoon:"de la tarde",evening:"de la tarde",night:"de la noche"}};function d(f){var _=Number(f);return _+"º"}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(º)?/i,o=/\d+/i,n={narrow:/^(ac|dc|a|d)/i,abbreviated:/^(a\.?\s?c\.?|a\.?\s?e\.?\s?c\.?|d\.?\s?c\.?|e\.?\s?c\.?)/i,wide:/^(antes de cristo|antes de la era com[uú]n|despu[eé]s de cristo|era com[uú]n)/i},s={any:[/^ac/i,/^dc/i],wide:[/^(antes de cristo|antes de la era com[uú]n)/i,/^(despu[eé]s de cristo|era com[uú]n)/i]},u={narrow:/^[1234]/i,abbreviated:/^T[1234]/i,wide:/^[1234](º)? trimestre/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[efmajsond]/i,abbreviated:/^(ene|feb|mar|abr|may|jun|jul|ago|sep|oct|nov|dic)/i,wide:/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre)/i},f={narrow:[/^e/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^en/i,/^feb/i,/^mar/i,/^abr/i,/^may/i,/^jun/i,/^jul/i,/^ago/i,/^sep/i,/^oct/i,/^nov/i,/^dic/i]},_={narrow:/^[dlmjvs]/i,short:/^(do|lu|ma|mi|ju|vi|sa)/i,abbreviated:/^(dom|lun|mar|mie|jue|vie|sab)/i,wide:/^(domingo|lunes|martes|mi[ée]rcoles|jueves|viernes|s[áa]bado)/i},c={narrow:[/^d/i,/^l/i,/^m/i,/^m/i,/^j/i,/^v/i,/^s/i],any:[/^do/i,/^lu/i,/^ma/i,/^mi/i,/^ju/i,/^vi/i,/^sa/i]},v={narrow:/^(a|p|mn|md|(de la|a las) (mañana|tarde|noche))/i,any:/^([ap]\.?\s?m\.?|medianoche|mediodia|(de la|a las) (mañana|tarde|noche))/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/^mn/i,noon:/^md/i,morning:/mañana/i,afternoon:/tarde/i,evening:/tarde/i,night:/noche/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(549),a=e(550),r=e(551),o=e(552),n=e(553),s={code:"et",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{standalone:{one:"vähem kui üks sekund",other:"vähem kui {{count}} sekundit"},withPreposition:{one:"vähem kui ühe sekundi",other:"vähem kui {{count}} sekundi"}},xSeconds:{standalone:{one:"üks sekund",other:"{{count}} sekundit"},withPreposition:{one:"ühe sekundi",other:"{{count}} sekundi"}},halfAMinute:{standalone:"pool minutit",withPreposition:"poole minuti"},lessThanXMinutes:{standalone:{one:"vähem kui üks minut",other:"vähem kui {{count}} minutit"},withPreposition:{one:"vähem kui ühe minuti",other:"vähem kui {{count}} minuti"}},xMinutes:{standalone:{one:"üks minut",other:"{{count}} minutit"},withPreposition:{one:"ühe minuti",other:"{{count}} minuti"}},aboutXHours:{standalone:{one:"umbes üks tund",other:"umbes {{count}} tundi"},withPreposition:{one:"umbes ühe tunni",other:"umbes {{count}} tunni"}},xHours:{standalone:{one:"üks tund",other:"{{count}} tundi"},withPreposition:{one:"ühe tunni",other:"{{count}} tunni"}},xDays:{standalone:{one:"üks päev",other:"{{count}} päeva"},withPreposition:{one:"ühe päeva",other:"{{count}} päeva"}},aboutXWeeks:{standalone:{one:"umbes üks nädal",other:"umbes {{count}} nädalat"},withPreposition:{one:"umbes ühe nädala",other:"umbes {{count}} nädala"}},xWeeks:{standalone:{one:"üks nädal",other:"{{count}} nädalat"},withPreposition:{one:"ühe nädala",other:"{{count}} nädala"}},aboutXMonths:{standalone:{one:"umbes üks kuu",other:"umbes {{count}} kuud"},withPreposition:{one:"umbes ühe kuu",other:"umbes {{count}} kuu"}},xMonths:{standalone:{one:"üks kuu",other:"{{count}} kuud"},withPreposition:{one:"ühe kuu",other:"{{count}} kuu"}},aboutXYears:{standalone:{one:"umbes üks aasta",other:"umbes {{count}} aastat"},withPreposition:{one:"umbes ühe aasta",other:"umbes {{count}} aasta"}},xYears:{standalone:{one:"üks aasta",other:"{{count}} aastat"},withPreposition:{one:"ühe aasta",other:"{{count}} aasta"}},overXYears:{standalone:{one:"rohkem kui üks aasta",other:"rohkem kui {{count}} aastat"},withPreposition:{one:"rohkem kui ühe aasta",other:"rohkem kui {{count}} aasta"}},almostXYears:{standalone:{one:"peaaegu üks aasta",other:"peaaegu {{count}} aastat"},withPreposition:{one:"peaaegu ühe aasta",other:"peaaegu {{count}} aasta"}}};function a(r,o,n){n=n||{};var s=n.addSuffix?t[r].withPreposition:t[r].standalone,u;return typeof s=="string"?u=s:o===1?u=s.one:u=s.other.replace("{{count}}",o),n.addSuffix?n.comparison>0?u+" pärast":u+" eest":u}},function(l,i,e){var t=e(2),a={full:"eeee, d. MMMM y",long:"d. MMMM y",medium:"d. MMM y",short:"dd.MM.y"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'kell' {{time}}",long:"{{date}} 'kell' {{time}}",medium:"{{date}}. {{time}}",short:"{{date}}. {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'eelmine' eeee 'kell' p",yesterday:"'eile kell' p",today:"'täna kell' p",tomorrow:"'homme kell' p",nextWeek:"'järgmine' eeee 'kell' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["e.m.a","m.a.j"],abbreviated:["e.m.a","m.a.j"],wide:["enne meie ajaarvamist","meie ajaarvamise järgi"]},r={narrow:["1","2","3","4"],abbreviated:["K1","K2","K3","K4"],wide:["1. kvartal","2. kvartal","3. kvartal","4. kvartal"]},o={narrow:["J","V","M","A","M","J","J","A","S","O","N","D"],abbreviated:["jaan","veebr","märts","apr","mai","juuni","juuli","aug","sept","okt","nov","dets"],wide:["jaanuar","veebruar","märts","aprill","mai","juuni","juuli","august","september","oktoober","november","detsember"]},n={narrow:["P","E","T","K","N","R","L"],short:["P","E","T","K","N","R","L"],abbreviated:["pühap.","esmasp.","teisip.","kolmap.","neljap.","reede.","laup."],wide:["pühapäev","esmaspäev","teisipäev","kolmapäev","neljapäev","reede","laupäev"]},s={narrow:{am:"AM",pm:"PM",midnight:"kesköö",noon:"keskpäev",morning:"hommik",afternoon:"pärastlõuna",evening:"õhtu",night:"öö"},abbreviated:{am:"AM",pm:"PM",midnight:"kesköö",noon:"keskpäev",morning:"hommik",afternoon:"pärastlõuna",evening:"õhtu",night:"öö"},wide:{am:"AM",pm:"PM",midnight:"kesköö",noon:"keskpäev",morning:"hommik",afternoon:"pärastlõuna",evening:"õhtu",night:"öö"}},u={narrow:{am:"AM",pm:"PM",midnight:"keskööl",noon:"keskpäeval",morning:"hommikul",afternoon:"pärastlõunal",evening:"õhtul",night:"öösel"},abbreviated:{am:"AM",pm:"PM",midnight:"keskööl",noon:"keskpäeval",morning:"hommikul",afternoon:"pärastlõunal",evening:"õhtul",night:"öösel"},wide:{am:"AM",pm:"PM",midnight:"keskööl",noon:"keskpäeval",morning:"hommikul",afternoon:"pärastlõunal",evening:"õhtul",night:"öösel"}};function d(f){var _=Number(f);return _+"."}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,formattingValues:o,defaultWidth:"wide"}),day:t.a({values:n,formattingValues:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,formattingValues:u,defaultWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^\d+\./i,o=/\d+/i,n={narrow:/^(e\.m\.a|m\.a\.j|eKr|pKr)/i,abbreviated:/^(e\.m\.a|m\.a\.j|eKr|pKr)/i,wide:/^(enne meie ajaarvamist|meie ajaarvamise järgi|enne Kristust|pärast Kristust)/i},s={any:[/^e/i,/^(m|p)/i]},u={narrow:/^[1234]/i,abbreviated:/^K[1234]/i,wide:/^[1234](\.)? kvartal/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[jvmasond]/i,abbreviated:/^('jaan|veebr|märts|apr|mai|juuni|juuli|aug|sept|okt|nov|dets')/i,wide:/^('jaanuar|veebruar|märts|aprill|mai|juuni|juuli|august|september|oktoober|november|detsember')/i},f={narrow:[/^j/i,/^v/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^v/i,/^mär/i,/^ap/i,/^mai/i,/^jun/i,/^jul/i,/^au/i,/^s/i,/^o/i,/^n/i,/^d/i]},_={narrow:/^[petknrl]/i,short:/^[petknrl]/i,abbreviated:/^(püh?|esm?|tei?|kolm?|nel?|ree?|laup?)\.?/i,wide:/^('pühapäev|esmaspäev|teisipäev|kolmapäev|neljapäev|reede|laupäev')/i},c={any:[/^p/i,/^e/i,/^t/i,/^k/i,/^n/i,/^r/i,/^l/i]},v={any:/^(am|pm|kesköö|keskpäev|hommik|pärastlõuna|õhtu|öö)/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/^keskö/i,noon:/^keskp/i,morning:/hommik/i,afternoon:/pärastlõuna/i,evening:/õhtu/i,night:/öö/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(555),a=e(556),r=e(557),o=e(558),n=e(559),s={code:"eu",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"segundo bat baino gutxiago",other:"{{count}} segundo baino gutxiago"},xSeconds:{one:"1 segundo",other:"{{count}} segundo"},halfAMinute:"minutu erdi",lessThanXMinutes:{one:"minutu bat baino gutxiago",other:"{{count}} minutu baino gutxiago"},xMinutes:{one:"1 minutu",other:"{{count}} minutu"},aboutXHours:{one:"1 ordu gutxi gorabehera",other:"{{count}} ordu gutxi gorabehera"},xHours:{one:"1 ordu",other:"{{count}} ordu"},xDays:{one:"1 egun",other:"{{count}} egun"},aboutXWeeks:{one:"aste 1 inguru",other:"{{count}} aste inguru"},xWeeks:{one:"1 aste",other:"{{count}} astean"},aboutXMonths:{one:"1 hilabete gutxi gorabehera",other:"{{count}} hilabete gutxi gorabehera"},xMonths:{one:"1 hilabete",other:"{{count}} hilabete"},aboutXYears:{one:"1 urte gutxi gorabehera",other:"{{count}} urte gutxi gorabehera"},xYears:{one:"1 urte",other:"{{count}} urte"},overXYears:{one:"1 urte baino gehiago",other:"{{count}} urte baino gehiago"},almostXYears:{one:"ia 1 urte",other:"ia {{count}} urte"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"en "+s:"duela "+s:s}},function(l,i,e){var t=e(2),a={full:"EEEE, d 'de' MMMM y",long:"d 'de' MMMM y",medium:"d MMM y",short:"dd/MM/y"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'tan' {{time}}",long:"{{date}} 'tan' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=r;var t={lastWeek:"'joan den' eeee, LT",yesterday:"'atzo,' p",today:"'gaur,' p",tomorrow:"'bihar,' p",nextWeek:"eeee, p",other:"P"},a={lastWeek:"'joan den' eeee, p",yesterday:"'atzo,' p",today:"'gaur,' p",tomorrow:"'bihar,' p",nextWeek:"eeee, p",other:"P"};function r(o,n,s,u){return n.getUTCHours()!==1?a[o]:t[o]}},function(l,i,e){var t=e(3),a={narrow:["k.a.","k.o."],abbreviated:["k.a.","k.o."],wide:["kristo aurretik","kristo ondoren"]},r={narrow:["1","2","3","4"],abbreviated:["1H","2H","3H","4H"],wide:["1. hiruhilekoa","2. hiruhilekoa","3. hiruhilekoa","4. hiruhilekoa"]},o={narrow:["u","o","m","a","m","e","u","a","i","u","a","a"],abbreviated:["urt","ots","mar","api","mai","eka","uzt","abu","ira","urr","aza","abe"],wide:["urtarrila","otsaila","martxoa","apirila","maiatza","ekaina","uztaila","abuztua","iraila","urria","azaroa","abendua"]},n={narrow:["i","a","a","a","o","o","l"],short:["ig","al","as","az","og","or","lr"],abbreviated:["iga","ast","ast","ast","ost","ost","lar"],wide:["igandea","astelehena","asteartea","asteazkena","osteguna","ostirala","larunbata"]},s={narrow:{am:"a",pm:"p",midnight:"ge",noon:"eg",morning:"goiza",afternoon:"arratsaldea",evening:"arratsaldea",night:"gaua"},abbreviated:{am:"AM",pm:"PM",midnight:"gauerdia",noon:"eguerdia",morning:"goiza",afternoon:"arratsaldea",evening:"arratsaldea",night:"gaua"},wide:{am:"a.m.",pm:"p.m.",midnight:"gauerdia",noon:"eguerdia",morning:"goiza",afternoon:"arratsaldea",evening:"arratsaldea",night:"gaua"}},u={narrow:{am:"a",pm:"p",midnight:"ge",noon:"eg",morning:"goizean",afternoon:"arratsaldean",evening:"arratsaldean",night:"gauean"},abbreviated:{am:"AM",pm:"PM",midnight:"gauerdia",noon:"eguerdia",morning:"goizean",afternoon:"arratsaldean",evening:"arratsaldean",night:"gauean"},wide:{am:"a.m.",pm:"p.m.",midnight:"gauerdia",noon:"eguerdia",morning:"goizean",afternoon:"arratsaldean",evening:"arratsaldean",night:"gauean"}};function d(f){var _=Number(f);return _+"."}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(.)?/i,o=/\d+/i,n={narrow:/^(k.a.|k.o.)/i,abbreviated:/^(k.a.|k.o.)/i,wide:/^(kristo aurretik|kristo ondoren)/i},s={narrow:[/^k.a./i,/^k.o./i],abbreviated:[/^(k.a.)/i,/^(k.o.)/i],wide:[/^(kristo aurretik)/i,/^(kristo ondoren)/i]},u={narrow:/^[1234]/i,abbreviated:/^[1234]H/i,wide:/^[1234](.)? hiruhilekoa/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[uomaei]/i,abbreviated:/^(urt|ots|mar|api|mai|eka|uzt|abu|ira|urr|aza|abe)/i,wide:/^(urtarrila|otsaila|martxoa|apirila|maiatza|ekaina|uztaila|abuztua|iraila|urria|azaroa|abendua)/i},f={narrow:[/^u/i,/^o/i,/^m/i,/^a/i,/^m/i,/^e/i,/^u/i,/^a/i,/^i/i,/^u/i,/^a/i,/^a/i],any:[/^urt/i,/^ots/i,/^mar/i,/^api/i,/^mai/i,/^eka/i,/^uzt/i,/^abu/i,/^ira/i,/^urr/i,/^aza/i,/^abe/i]},_={narrow:/^[iaol]/i,short:/^(ig|al|as|az|og|or|lr)/i,abbreviated:/^(iga|ast|ast|ast|ost|ost|lar)/i,wide:/^(igandea|astelehena|asteartea|asteazkena|osteguna|ostirala|larunbata)/i},c={narrow:[/^i/i,/^a/i,/^a/i,/^a/i,/^o/i,/^o/i,/^l/i],short:[/^ig/i,/^al/i,/^as/i,/^az/i,/^og/i,/^or/i,/^lr/i],abbreviated:[/^iga/i,/^ast/i,/^ast/i,/^ast/i,/^ost/i,/^ost/i,/^lar/i],wide:[/^igandea/i,/^astelehena/i,/^asteartea/i,/^asteazkena/i,/^osteguna/i,/^ostirala/i,/^larunbata/i]},v={narrow:/^(a|p|ge|eg|((goiza|goizean)|arratsaldea|(gaua|gauean)))/i,any:/^([ap]\.?\s?m\.?|gauerdia|eguerdia|((goiza|goizean)|arratsaldea|(gaua|gauean)))/i},g={narrow:{am:/^a/i,pm:/^p/i,midnight:/^ge/i,noon:/^eg/i,morning:/goiz/i,afternoon:/arratsaldea/i,evening:/arratsaldea/i,night:/gau/i},any:{am:/^a/i,pm:/^p/i,midnight:/^gauerdia/i,noon:/^eguerdia/i,morning:/goiz/i,afternoon:/arratsaldea/i,evening:/arratsaldea/i,night:/gau/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(561),a=e(562),r=e(563),o=e(564),n=e(565),s={code:"fa-IR",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:0,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"کمتر از یک ثانیه",other:"کمتر از {{count}} ثانیه"},xSeconds:{one:"1 ثانیه",other:"{{count}} ثانیه"},halfAMinute:"نیم دقیقه",lessThanXMinutes:{one:"کمتر از یک دقیقه",other:"کمتر از {{count}} دقیقه"},xMinutes:{one:"1 دقیقه",other:"{{count}} دقیقه"},aboutXHours:{one:"حدود 1 ساعت",other:"حدود {{count}} ساعت"},xHours:{one:"1 ساعت",other:"{{count}} ساعت"},xDays:{one:"1 روز",other:"{{count}} روز"},aboutXWeeks:{one:"حدود 1 هفته",other:"حدود {{count}} هفته"},xWeeks:{one:"1 هفته",other:"{{count}} هفته"},aboutXMonths:{one:"حدود 1 ماه",other:"حدود {{count}} ماه"},xMonths:{one:"1 ماه",other:"{{count}} ماه"},aboutXYears:{one:"حدود 1 سال",other:"حدود {{count}} سال"},xYears:{one:"1 سال",other:"{{count}} سال"},overXYears:{one:"بیشتر از 1 سال",other:"بیشتر از {{count}} سال"},almostXYears:{one:"نزدیک 1 سال",other:"نزدیک {{count}} سال"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"در "+s:s+" قبل":s}},function(l,i,e){var t=e(2),a={full:"EEEE do MMMM y",long:"do MMMM y",medium:"d MMM y",short:"yyyy/MM/dd"},r={full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},o={full:"{{date}} 'در' {{time}}",long:"{{date}} 'در' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"eeee 'گذشته در' p",yesterday:"'دیروز در' p",today:"'امروز در' p",tomorrow:"'فردا در' p",nextWeek:"eeee 'در' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["ق","ب"],abbreviated:["ق.م.","ب.م."],wide:["قبل از میلاد","بعد از میلاد"]},r={narrow:["1","2","3","4"],abbreviated:["س‌م1","س‌م2","س‌م3","س‌م4"],wide:["سه‌ماهه 1","سه‌ماهه 2","سه‌ماهه 3","سه‌ماهه 4"]},o={narrow:["ژ","ف","م","آ","م","ج","ج","آ","س","ا","ن","د"],abbreviated:["ژانـ","فور","مارس","آپر","می","جون","جولـ","آگو","سپتـ","اکتـ","نوامـ","دسامـ"],wide:["ژانویه","فوریه","مارس","آپریل","می","جون","جولای","آگوست","سپتامبر","اکتبر","نوامبر","دسامبر"]},n={narrow:["ی","د","س","چ","پ","ج","ش"],short:["1ش","2ش","3ش","4ش","5ش","ج","ش"],abbreviated:["یکشنبه","دوشنبه","سه‌شنبه","چهارشنبه","پنجشنبه","جمعه","شنبه"],wide:["یکشنبه","دوشنبه","سه‌شنبه","چهارشنبه","پنجشنبه","جمعه","شنبه"]},s={narrow:{am:"ق",pm:"ب",midnight:"ن",noon:"ظ",morning:"ص",afternoon:"ب.ظ.",evening:"ع",night:"ش"},abbreviated:{am:"ق.ظ.",pm:"ب.ظ.",midnight:"نیمه‌شب",noon:"ظهر",morning:"صبح",afternoon:"بعدازظهر",evening:"عصر",night:"شب"},wide:{am:"قبل‌ازظهر",pm:"بعدازظهر",midnight:"نیمه‌شب",noon:"ظهر",morning:"صبح",afternoon:"بعدازظهر",evening:"عصر",night:"شب"}},u={narrow:{am:"ق",pm:"ب",midnight:"ن",noon:"ظ",morning:"ص",afternoon:"ب.ظ.",evening:"ع",night:"ش"},abbreviated:{am:"ق.ظ.",pm:"ب.ظ.",midnight:"نیمه‌شب",noon:"ظهر",morning:"صبح",afternoon:"بعدازظهر",evening:"عصر",night:"شب"},wide:{am:"قبل‌ازظهر",pm:"بعدازظهر",midnight:"نیمه‌شب",noon:"ظهر",morning:"صبح",afternoon:"بعدازظهر",evening:"عصر",night:"شب"}};function d(f){return String(f)}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(th|st|nd|rd)?/i,o=/\d+/i,n={narrow:/^(ق|ب)/i,abbreviated:/^(ق\.?\s?م\.?|ق\.?\s?د\.?\s?م\.?|م\.?\s?|د\.?\s?م\.?)/i,wide:/^(قبل از میلاد|قبل از دوران مشترک|میلادی|دوران مشترک|بعد از میلاد)/i},s={any:[/^قبل/i,/^بعد/i]},u={narrow:/^[1234]/i,abbreviated:/^س‌م[1234]/i,wide:/^سه‌ماهه [1234]/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[جژفمآاماسند]/i,abbreviated:/^(جنو|ژانـ|ژانویه|فوریه|فور|مارس|آوریل|آپر|مه|می|ژوئن|جون|جول|جولـ|ژوئیه|اوت|آگو|سپتمبر|سپتامبر|اکتبر|اکتوبر|نوامبر|نوامـ|دسامبر|دسامـ|دسم)/i,wide:/^(ژانویه|جنوری|فبروری|فوریه|مارچ|مارس|آپریل|اپریل|ایپریل|آوریل|مه|می|ژوئن|جون|جولای|ژوئیه|آگست|اگست|آگوست|اوت|سپتمبر|سپتامبر|اکتبر|اکتوبر|نوامبر|نومبر|دسامبر|دسمبر)/i},f={narrow:[/^(ژ|ج)/i,/^ف/i,/^م/i,/^(آ|ا)/i,/^م/i,/^(ژ|ج)/i,/^(ج|ژ)/i,/^(آ|ا)/i,/^س/i,/^ا/i,/^ن/i,/^د/i],any:[/^ژا/i,/^ف/i,/^ما/i,/^آپ/i,/^(می|مه)/i,/^(ژوئن|جون)/i,/^(ژوئی|جول)/i,/^(اوت|آگ)/i,/^س/i,/^(اوک|اک)/i,/^ن/i,/^د/i]},_={narrow:/^[شیدسچپج]/i,short:/^(ش|ج|1ش|2ش|3ش|4ش|5ش)/i,abbreviated:/^(یکشنبه|دوشنبه|سه‌شنبه|چهارشنبه|پنج‌شنبه|جمعه|شنبه)/i,wide:/^(یکشنبه|دوشنبه|سه‌شنبه|چهارشنبه|پنج‌شنبه|جمعه|شنبه)/i},c={narrow:[/^ی/i,/^دو/i,/^س/i,/^چ/i,/^پ/i,/^ج/i,/^ش/i],any:[/^(ی|1ش|یکشنبه)/i,/^(د|2ش|دوشنبه)/i,/^(س|3ش|سه‌شنبه)/i,/^(چ|4ش|چهارشنبه)/i,/^(پ|5ش|پنجشنبه)/i,/^(ج|جمعه)/i,/^(ش|شنبه)/i]},v={narrow:/^(ب|ق|ن|ظ|ص|ب.ظ.|ع|ش)/i,abbreviated:/^(ق.ظ.|ب.ظ.|نیمه‌شب|ظهر|صبح|بعدازظهر|عصر|شب)/i,wide:/^(قبل‌ازظهر|نیمه‌شب|ظهر|صبح|بعدازظهر|عصر|شب)/i},g={any:{am:/^(ق|ق.ظ.|قبل‌ازظهر)/i,pm:/^(ب|ب.ظ.|بعدازظهر)/i,midnight:/^(‌نیمه‌شب|ن)/i,noon:/^(ظ|ظهر)/i,morning:/(ص|صبح)/i,afternoon:/(ب|ب.ظ.|بعدازظهر)/i,evening:/(ع|عصر)/i,night:/(ش|شب)/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(567),a=e(568),r=e(569),o=e(570),n=e(571),s={code:"fi",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=m;function t(f){return f.replace(/sekuntia?/,"sekunnin")}function a(f){return f.replace(/minuuttia?/,"minuutin")}function r(f){return f.replace(/tuntia?/,"tunnin")}function o(f){return f.replace(/päivää?/,"päivän")}function n(f){return f.replace(/(viikko|viikkoa)/,"viikon")}function s(f){return f.replace(/(kuukausi|kuukautta)/,"kuukauden")}function u(f){return f.replace(/(vuosi|vuotta)/,"vuoden")}var d={lessThanXSeconds:{one:"alle sekunti",other:"alle {{count}} sekuntia",futureTense:t},xSeconds:{one:"sekunti",other:"{{count}} sekuntia",futureTense:t},halfAMinute:{one:"puoli minuuttia",other:"puoli minuuttia",futureTense:function(f){return"puolen minuutin"}},lessThanXMinutes:{one:"alle minuutti",other:"alle {{count}} minuuttia",futureTense:a},xMinutes:{one:"minuutti",other:"{{count}} minuuttia",futureTense:a},aboutXHours:{one:"noin tunti",other:"noin {{count}} tuntia",futureTense:r},xHours:{one:"tunti",other:"{{count}} tuntia",futureTense:r},xDays:{one:"päivä",other:"{{count}} päivää",futureTense:o},aboutXWeeks:{one:"noin viikko",other:"noin {{count}} viikkoa",futureTense:n},xWeeks:{one:"viikko",other:"{{count}} viikkoa",futureTense:n},aboutXMonths:{one:"noin kuukausi",other:"noin {{count}} kuukautta",futureTense:s},xMonths:{one:"kuukausi",other:"{{count}} kuukautta",futureTense:s},aboutXYears:{one:"noin vuosi",other:"noin {{count}} vuotta",futureTense:u},xYears:{one:"vuosi",other:"{{count}} vuotta",futureTense:u},overXYears:{one:"yli vuosi",other:"yli {{count}} vuotta",futureTense:u},almostXYears:{one:"lähes vuosi",other:"lähes {{count}} vuotta",futureTense:u}};function m(f,_,c){c=c||{};var v=d[f],g=_===1?v.one:v.other.replace("{{count}}",_);return c.addSuffix?c.comparison>0?v.futureTense(g)+" kuluttua":g+" sitten":g}},function(l,i,e){var t=e(2),a={full:"eeee d. MMMM y",long:"d. MMMM y",medium:"d. MMM y",short:"d.M.y"},r={full:"HH.mm.ss zzzz",long:"HH.mm.ss z",medium:"HH.mm.ss",short:"HH.mm"},o={full:"{{date}} 'klo' {{time}}",long:"{{date}} 'klo' {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'viime' eeee 'klo' p",yesterday:"'eilen klo' p",today:"'tänään klo' p",tomorrow:"'huomenna klo' p",nextWeek:"'ensi' eeee 'klo' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["eaa.","jaa."],abbreviated:["eaa.","jaa."],wide:["ennen ajanlaskun alkua","jälkeen ajanlaskun alun"]},r={narrow:["1","2","3","4"],abbreviated:["Q1","Q2","Q3","Q4"],wide:["1. kvartaali","2. kvartaali","3. kvartaali","4. kvartaali"]},o={narrow:["T","H","M","H","T","K","H","E","S","L","M","J"],abbreviated:["tammi","helmi","maalis","huhti","touko","kesä","heinä","elo","syys","loka","marras","joulu"],wide:["tammikuu","helmikuu","maaliskuu","huhtikuu","toukokuu","kesäkuu","heinäkuu","elokuu","syyskuu","lokakuu","marraskuu","joulukuu"]},n={narrow:o.narrow,abbreviated:o.abbreviated,wide:o.wide.map(function(_){return _+"ta"})},s={narrow:["S","M","T","K","T","P","L"],short:["su","ma","ti","ke","to","pe","la"],abbreviated:["sunn.","maan.","tiis.","kesk.","torst.","perj.","la"],wide:["sunnuntai","maanantai","tiistai","keskiviikko","torstai","perjantai","lauantai"]},u={narrow:s.narrow,short:s.short,abbreviated:s.abbreviated,wide:s.wide.map(function(_){return _+"na"})},d={narrow:{am:"ap",pm:"ip",midnight:"keskiyö",noon:"keskipäivä",morning:"ap",afternoon:"ip",evening:"illalla",night:"yöllä"},abbreviated:{am:"ap",pm:"ip",midnight:"keskiyö",noon:"keskipäivä",morning:"ap",afternoon:"ip",evening:"illalla",night:"yöllä"},wide:{am:"ap",pm:"ip",midnight:"keskiyöllä",noon:"keskipäivällä",morning:"aamupäivällä",afternoon:"iltapäivällä",evening:"illalla",night:"yöllä"}};function m(_){var c=Number(_);return c+"."}var f={ordinalNumber:m,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(_){return Number(_)-1}}),month:t.a({values:o,formattingValues:n,defaultWidth:"wide"}),day:t.a({values:s,formattingValues:u,defaultWidth:"wide"}),dayPeriod:t.a({values:d,defaultWidth:"wide"})};i.a=f},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(\.)/i,o=/\d+/i,n={narrow:/^(e|j)/i,abbreviated:/^(eaa.|jaa.)/i,wide:/^(ennen ajanlaskun alkua|jälkeen ajanlaskun alun)/i},s={any:[/^e/i,/^j/i]},u={narrow:/^[1234]/i,abbreviated:/^q[1234]/i,wide:/^[1234]\.? kvartaali/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[thmkeslj]/i,abbreviated:/^(tammi|helmi|maalis|huhti|touko|kesä|heinä|elo|syys|loka|marras|joulu)/i,wide:/^(tammikuu|helmikuu|maaliskuu|huhtikuu|toukokuu|kesäkuu|heinäkuu|elokuu|syyskuu|lokakuu|marraskuu|joulukuu)(ta)?/i},f={narrow:[/^t/i,/^h/i,/^m/i,/^h/i,/^t/i,/^k/i,/^h/i,/^e/i,/^s/i,/^l/i,/^m/i,/^j/i],any:[/^ta/i,/^hel/i,/^maa/i,/^hu/i,/^to/i,/^k/i,/^hei/i,/^e/i,/^s/i,/^l/i,/^mar/i,/^j/i]},_={narrow:/^[smtkpl]/i,short:/^(su|ma|ti|ke|to|pe|la)/i,abbreviated:/^(sunn.|maan.|tiis.|kesk.|torst.|perj.|la)/i,wide:/^(sunnuntai|maanantai|tiistai|keskiviikko|torstai|perjantai|lauantai)(na)?/i},c={narrow:[/^s/i,/^m/i,/^t/i,/^k/i,/^t/i,/^p/i,/^l/i],any:[/^s/i,/^m/i,/^ti/i,/^k/i,/^to/i,/^p/i,/^l/i]},v={narrow:/^(ap|ip|keskiyö|keskipäivä|aamupäivällä|iltapäivällä|illalla|yöllä)/i,any:/^(ap|ip|keskiyöllä|keskipäivällä|aamupäivällä|iltapäivällä|illalla|yöllä)/i},g={any:{am:/^ap/i,pm:/^ip/i,midnight:/^keskiyö/i,noon:/^keskipäivä/i,morning:/aamupäivällä/i,afternoon:/iltapäivällä/i,evening:/illalla/i,night:/yöllä/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(187),a=e(573),r=e(188),o=e(189),n=e(190),s={code:"fr",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){var t=e(2),a={full:"EEEE d MMMM y",long:"d MMMM y",medium:"d MMM y",short:"dd/MM/y"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'à' {{time}}",long:"{{date}} 'à' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){var t=e(187),a=e(188),r=e(189),o=e(190),n=e(575),s={code:"fr-CA",formatDistance:t.a,formatLong:n.a,formatRelative:a.a,localize:r.a,match:o.a,options:{weekStartsOn:0,firstWeekContainsDate:1}};i.a=s},function(l,i,e){var t=e(2),a={full:"EEEE d MMMM y",long:"d MMMM y",medium:"d MMM y",short:"yy-MM-dd"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'à' {{time}}",long:"{{date}} 'à' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){var t=e(577),a=e(578),r=e(579),o=e(580),n=e(581),s={code:"fr-CH",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"moins d’une seconde",other:"moins de {{count}} secondes"},xSeconds:{one:"1 seconde",other:"{{count}} secondes"},halfAMinute:"30 secondes",lessThanXMinutes:{one:"moins d’une minute",other:"moins de {{count}} minutes"},xMinutes:{one:"1 minute",other:"{{count}} minutes"},aboutXHours:{one:"environ 1 heure",other:"environ {{count}} heures"},xHours:{one:"1 heure",other:"{{count}} heures"},xDays:{one:"1 jour",other:"{{count}} jours"},aboutXWeeks:{one:"environ 1 semaine",other:"environ {{count}} semaines"},xWeeks:{one:"1 semaine",other:"{{count}} semaines"},aboutXMonths:{one:"environ 1 mois",other:"environ {{count}} mois"},xMonths:{one:"1 mois",other:"{{count}} mois"},aboutXYears:{one:"environ 1 an",other:"environ {{count}} ans"},xYears:{one:"1 an",other:"{{count}} ans"},overXYears:{one:"plus d’un an",other:"plus de {{count}} ans"},almostXYears:{one:"presqu’un an",other:"presque {{count}} ans"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"dans "+s:"il y a "+s:s}},function(l,i,e){var t=e(2),a={full:"EEEE d MMMM y",long:"d MMMM y",medium:"d MMM y",short:"dd.MM.y"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'à' {{time}}",long:"{{date}} 'à' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"eeee 'la semaine dernière à' p",yesterday:"'hier à' p",today:"'aujourd’hui à' p",tomorrow:"'demain à' p'",nextWeek:"eeee 'la semaine prochaine à' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["av. J.-C","ap. J.-C"],abbreviated:["av. J.-C","ap. J.-C"],wide:["avant Jésus-Christ","après Jésus-Christ"]},r={narrow:["T1","T2","T3","T4"],abbreviated:["1er trim.","2ème trim.","3ème trim.","4ème trim."],wide:["1er trimestre","2ème trimestre","3ème trimestre","4ème trimestre"]},o={narrow:["J","F","M","A","M","J","J","A","S","O","N","D"],abbreviated:["janv.","févr.","mars","avr.","mai","juin","juil.","août","sept.","oct.","nov.","déc."],wide:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"]},n={narrow:["D","L","M","M","J","V","S"],short:["di","lu","ma","me","je","ve","sa"],abbreviated:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],wide:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"]},s={narrow:{am:"AM",pm:"PM",midnight:"minuit",noon:"midi",morning:"mat.",afternoon:"ap.m.",evening:"soir",night:"mat."},abbreviated:{am:"AM",pm:"PM",midnight:"minuit",noon:"midi",morning:"matin",afternoon:"après-midi",evening:"soir",night:"matin"},wide:{am:"AM",pm:"PM",midnight:"minuit",noon:"midi",morning:"du matin",afternoon:"de l’après-midi",evening:"du soir",night:"du matin"}};function u(m,f){var _=Number(m),c=f||{},v=String(c.unit),g;return _===0?_:(v==="year"||v==="hour"||v==="week"?_===1?g="ère":g="ème":_===1?g="er":g="ème",_+g)}var d={ordinalNumber:u,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(m){return Number(m)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide"})};i.a=d},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(ième|ère|ème|er|e)?/i,o=/\d+/i,n={narrow:/^(av\.J\.C|ap\.J\.C|ap\.J\.-C)/i,abbreviated:/^(av\.J\.-C|av\.J-C|apr\.J\.-C|apr\.J-C|ap\.J-C)/i,wide:/^(avant Jésus-Christ|après Jésus-Christ)/i},s={any:[/^av/i,/^ap/i]},u={narrow:/^[1234]/i,abbreviated:/^t[1234]/i,wide:/^[1234](er|ème|e)? trimestre/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[jfmasond]/i,abbreviated:/^(janv|févr|mars|avr|mai|juin|juill|juil|août|sept|oct|nov|déc)\.?/i,wide:/^(janvier|février|mars|avril|mai|juin|juillet|août|septembre|octobre|novembre|décembre)/i},f={narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^mar/i,/^av/i,/^ma/i,/^juin/i,/^juil/i,/^ao/i,/^s/i,/^o/i,/^n/i,/^d/i]},_={narrow:/^[lmjvsd]/i,short:/^(di|lu|ma|me|je|ve|sa)/i,abbreviated:/^(dim|lun|mar|mer|jeu|ven|sam)\.?/i,wide:/^(dimanche|lundi|mardi|mercredi|jeudi|vendredi|samedi)/i},c={narrow:[/^d/i,/^l/i,/^m/i,/^m/i,/^j/i,/^v/i,/^s/i],any:[/^di/i,/^lu/i,/^ma/i,/^me/i,/^je/i,/^ve/i,/^sa/i]},v={narrow:/^(a|p|minuit|midi|mat\.?|ap\.?m\.?|soir|nuit)/i,any:/^([ap]\.?\s?m\.?|du matin|de l'après[-\s]midi|du soir|de la nuit)/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/^min/i,noon:/^mid/i,morning:/mat/i,afternoon:/ap/i,evening:/soir/i,night:/nuit/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(583),a=e(584),r=e(585),o=e(586),n=e(587),s={code:"gd",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:0,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"nas lugha na diog",other:"nas lugha na {{count}} diogan"},xSeconds:{one:"1 diog",two:"2 dhiog",twenty:"20 diog",other:"{{count}} diogan"},halfAMinute:"leth mhionaid",lessThanXMinutes:{one:"nas lugha na mionaid",other:"nas lugha na {{count}} mionaidean"},xMinutes:{one:"1 mionaid",two:"2 mhionaid",twenty:"20 mionaid",other:"{{count}} mionaidean"},aboutXHours:{one:"mu uair de thìde",other:"mu {{count}} uairean de thìde"},xHours:{one:"1 uair de thìde",two:"2 uair de thìde",twenty:"20 uair de thìde",other:"{{count}} uairean de thìde"},xDays:{one:"1 là",other:"{{count}} là"},aboutXWeeks:{one:"mu 1 seachdain",other:"mu {{count}} seachdainean"},xWeeks:{one:"1 seachdain",other:"{{count}} seachdainean"},aboutXMonths:{one:"mu mhìos",other:"mu {{count}} mìosan"},xMonths:{one:"1 mìos",other:"{{count}} mìosan"},aboutXYears:{one:"mu bhliadhna",other:"mu {{count}} bliadhnaichean"},xYears:{one:"1 bhliadhna",other:"{{count}} bliadhna"},overXYears:{one:"còrr is bliadhna",other:"còrr is {{count}} bliadhnaichean"},almostXYears:{one:"cha mhòr bliadhna",other:"cha mhòr {{count}} bliadhnaichean"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:o===2&&t[r].two?s=t[r].two:o===9&&t[r].nine?s=t[r].nine:o===20&&t[r].twenty?s=t[r].twenty:o===30&&t[r].thirty?s=t[r].thirty:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"ann an "+s:"o chionn "+s:s}},function(l,i,e){var t=e(2),a={full:"EEEE, MMMM do, y",long:"MMMM do, y",medium:"MMM d, y",short:"MM/dd/yyyy"},r={full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},o={full:"{{date}} 'aig' {{time}}",long:"{{date}} 'aig' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'mu dheireadh' eeee 'aig' p",yesterday:"'an-dè aig' p",today:"'an-diugh aig' p",tomorrow:"'a-màireach aig' p",nextWeek:"eeee 'aig' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["R","A"],abbreviated:["RC","AD"],wide:["ro Chrìosta","anno domini"]},r={narrow:["1","2","3","4"],abbreviated:["C1","C2","C3","C4"],wide:["a' chiad chairteal","an dàrna cairteal","an treas cairteal","an ceathramh cairteal"]},o={narrow:["F","G","M","G","C","Ò","I","L","S","D","S","D"],abbreviated:["Faoi","Gear","Màrt","Gibl","Cèit","Ògmh","Iuch","Lùn","Sult","Dàmh","Samh","Dùbh"],wide:["Am Faoilleach","An Gearran","Am Màrt","An Giblean","An Cèitean","An t-Ògmhios","An t-Iuchar","An Lùnastal","An t-Sultain","An Dàmhair","An t-Samhain","An Dùbhlachd"]},n={narrow:["D","L","M","C","A","H","S"],short:["Dò","Lu","Mà","Ci","Ar","Ha","Sa"],abbreviated:["Did","Dil","Dim","Dic","Dia","Dih","Dis"],wide:["Didòmhnaich","Diluain","Dimàirt","Diciadain","Diardaoin","Dihaoine","Disathairne"]},s={narrow:{am:"m",pm:"f",midnight:"m.o.",noon:"m.l.",morning:"madainn",afternoon:"feasgar",evening:"feasgar",night:"oidhche"},abbreviated:{am:"M.",pm:"F.",midnight:"meadhan oidhche",noon:"meadhan là",morning:"madainn",afternoon:"feasgar",evening:"feasgar",night:"oidhche"},wide:{am:"m.",pm:"f.",midnight:"meadhan oidhche",noon:"meadhan là",morning:"madainn",afternoon:"feasgar",evening:"feasgar",night:"oidhche"}},u={narrow:{am:"m",pm:"f",midnight:"m.o.",noon:"m.l.",morning:"sa mhadainn",afternoon:"feasgar",evening:"feasgar",night:"air an oidhche"},abbreviated:{am:"M.",pm:"F.",midnight:"meadhan oidhche",noon:"meadhan là",morning:"sa mhadainn",afternoon:"feasgar",evening:"feasgar",night:"air an oidhche"},wide:{am:"m.",pm:"f.",midnight:"meadhan oidhche",noon:"meadhan là",morning:"sa mhadainn",afternoon:"feasgar",evening:"feasgar",night:"air an oidhche"}};function d(f,_){var c=Number(f),v=c%100;if(v>20||v<10)switch(v%10){case 1:return c+"d";case 2:return c+"na"}return v===12?c+"na":c+"mh"}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(d|na|tr|mh)?/i,o=/\d+/i,n={narrow:/^(r|a)/i,abbreviated:/^(r\.?\s?c\.?|r\.?\s?a\.?\s?c\.?|a\.?\s?d\.?|a\.?\s?c\.?)/i,wide:/^(ro Chrìosta|ron aois choitchinn|anno domini|aois choitcheann)/i},s={any:[/^b/i,/^(a|c)/i]},u={narrow:/^[1234]/i,abbreviated:/^c[1234]/i,wide:/^[1234](cd|na|tr|mh)? cairteal/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[fgmcòilsd]/i,abbreviated:/^(faoi|gear|màrt|gibl|cèit|ògmh|iuch|lùn|sult|dàmh|samh|dùbh)/i,wide:/^(am faoilleach|an gearran|am màrt|an giblean|an cèitean|an t-Ògmhios|an t-Iuchar|an lùnastal|an t-Sultain|an dàmhair|an t-Samhain|an dùbhlachd)/i},f={narrow:[/^f/i,/^g/i,/^m/i,/^g/i,/^c/i,/^ò/i,/^i/i,/^l/i,/^s/i,/^d/i,/^s/i,/^d/i],any:[/^fa/i,/^ge/i,/^mà/i,/^gi/i,/^c/i,/^ò/i,/^i/i,/^l/i,/^su/i,/^d/i,/^sa/i,/^d/i]},_={narrow:/^[dlmcahs]/i,short:/^(dò|lu|mà|ci|ar|ha|sa)/i,abbreviated:/^(did|dil|dim|dic|dia|dih|dis)/i,wide:/^(didòmhnaich|diluain|dimàirt|diciadain|diardaoin|dihaoine|disathairne)/i},c={narrow:[/^d/i,/^l/i,/^m/i,/^c/i,/^a/i,/^h/i,/^s/i],any:[/^d/i,/^l/i,/^m/i,/^c/i,/^a/i,/^h/i,/^s/i]},v={narrow:/^(a|p|mi|n|(san|aig) (madainn|feasgar|feasgar|oidhche))/i,any:/^([ap]\.?\s?m\.?|meadhan oidhche|meadhan là|(san|aig) (madainn|feasgar|feasgar|oidhche))/i},g={any:{am:/^m/i,pm:/^f/i,midnight:/^meadhan oidhche/i,noon:/^meadhan là/i,morning:/sa mhadainn/i,afternoon:/feasgar/i,evening:/feasgar/i,night:/air an oidhche/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(589),a=e(590),r=e(591),o=e(592),n=e(593),s={code:"gl",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"menos dun segundo",other:"menos de {{count}} segundos"},xSeconds:{one:"1 segundo",other:"{{count}} segundos"},halfAMinute:"medio minuto",lessThanXMinutes:{one:"menos dun minuto",other:"menos de {{count}} minutos"},xMinutes:{one:"1 minuto",other:"{{count}} minutos"},aboutXHours:{one:"arredor dunha hora",other:"arredor de {{count}} horas"},xHours:{one:"1 hora",other:"{{count}} horas"},xDays:{one:"1 día",other:"{{count}} días"},aboutXWeeks:{one:"arredor dunha semana",other:"arredor de {{count}} semanas"},xWeeks:{one:"1 semana",other:"{{count}} semanas"},aboutXMonths:{one:"arredor de 1 mes",other:"arredor de {{count}} meses"},xMonths:{one:"1 mes",other:"{{count}} meses"},aboutXYears:{one:"arredor dun ano",other:"arredor de {{count}} anos"},xYears:{one:"1 ano",other:"{{count}} anos"},overXYears:{one:"máis dun ano",other:"máis de {{count}} anos"},almostXYears:{one:"case un ano",other:"case {{count}} anos"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"en "+s:"hai "+s:s}},function(l,i,e){var t=e(2),a={full:"EEEE, d 'de' MMMM y",long:"d 'de' MMMM y",medium:"d MMM y",short:"dd/MM/y"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'ás' {{time}}",long:"{{date}} 'ás' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=r;var t={lastWeek:"'o' eeee 'pasado á' LT",yesterday:"'onte á' p",today:"'hoxe á' p",tomorrow:"'mañá á' p",nextWeek:"eeee 'á' p",other:"P"},a={lastWeek:"'o' eeee 'pasado ás' p",yesterday:"'onte ás' p",today:"'hoxe ás' p",tomorrow:"'mañá ás' p",nextWeek:"eeee 'ás' p",other:"P"};function r(o,n,s,u){return n.getUTCHours()!==1?a[o]:t[o]}},function(l,i,e){var t=e(3),a={narrow:["AC","DC"],abbreviated:["AC","DC"],wide:["antes de cristo","despois de cristo"]},r={narrow:["1","2","3","4"],abbreviated:["T1","T2","T3","T4"],wide:["1º trimestre","2º trimestre","3º trimestre","4º trimestre"]},o={narrow:["e","f","m","a","m","j","j","a","s","o","n","d"],abbreviated:["xan","feb","mar","abr","mai","xun","xul","ago","set","out","nov","dec"],wide:["xaneiro","febreiro","marzo","abril","maio","xuño","xullo","agosto","setembro","outubro","novembro","decembro"]},n={narrow:["d","l","m","m","j","v","s"],short:["do","lu","ma","me","xo","ve","sa"],abbreviated:["dom","lun","mar","mer","xov","ven","sab"],wide:["domingo","luns","martes","mércores","xoves","venres","sábado"]},s={narrow:{am:"a",pm:"p",midnight:"mn",noon:"md",morning:"mañá",afternoon:"tarde",evening:"tarde",night:"noite"},abbreviated:{am:"AM",pm:"PM",midnight:"medianoite",noon:"mediodía",morning:"mañá",afternoon:"tarde",evening:"tardiña",night:"noite"},wide:{am:"a.m.",pm:"p.m.",midnight:"medianoite",noon:"mediodía",morning:"mañá",afternoon:"tarde",evening:"tardiña",night:"noite"}},u={narrow:{am:"a",pm:"p",midnight:"mn",noon:"md",morning:"da mañá",afternoon:"da tarde",evening:"da tardiña",night:"da noite"},abbreviated:{am:"AM",pm:"PM",midnight:"medianoite",noon:"mediodía",morning:"da mañá",afternoon:"da tarde",evening:"da tardiña",night:"da noite"},wide:{am:"a.m.",pm:"p.m.",midnight:"medianoite",noon:"mediodía",morning:"da mañá",afternoon:"da tarde",evening:"da tardiña",night:"da noite"}};function d(f){var _=Number(f);return _+"º"}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(º)?/i,o=/\d+/i,n={narrow:/^(ac|dc|a|d)/i,abbreviated:/^(a\.?\s?c\.?|a\.?\s?e\.?\s?c\.?|d\.?\s?c\.?|e\.?\s?c\.?)/i,wide:/^(antes de cristo|antes da era com[uú]n|despois de cristo|era com[uú]n)/i},s={any:[/^ac/i,/^dc/i],wide:[/^(antes de cristo|antes da era com[uú]n)/i,/^(despois de cristo|era com[uú]n)/i]},u={narrow:/^[1234]/i,abbreviated:/^T[1234]/i,wide:/^[1234](º)? trimestre/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[xfmasond]/i,abbreviated:/^(xan|feb|mar|abr|mai|xun|xul|ago|set|out|nov|dec)/i,wide:/^(xaneiro|febreiro|marzo|abril|maio|xuño|xullo|agosto|setembro|outubro|novembro|decembro)/i},f={narrow:[/^x/i,/^f/i,/^m/i,/^a/i,/^m/i,/^x/i,/^x/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^xan/i,/^feb/i,/^mar/i,/^abr/i,/^mai/i,/^xun/i,/^xul/i,/^ago/i,/^set/i,/^out/i,/^nov/i,/^dec/i]},_={narrow:/^[dlmxvs]/i,short:/^(do|lu|ma|me|xo|ve|sa)/i,abbreviated:/^(dom|lun|mar|mer|xov|ven|sab)/i,wide:/^(domingo|luns|martes|m[eé]rcores|xoves|venres|s[áa]bado)/i},c={narrow:[/^d/i,/^l/i,/^m/i,/^m/i,/^x/i,/^v/i,/^s/i],any:[/^do/i,/^lu/i,/^ma/i,/^me/i,/^xo/i,/^ve/i,/^sa/i]},v={narrow:/^(a|p|mn|md|(da|[aá]s) (mañ[aá]|tarde|noite))/i,any:/^([ap]\.?\s?m\.?|medianoite|mediod[ií]a|(da|[aá]s) (mañ[aá]|tarde|noite))/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/^mn/i,noon:/^md/i,morning:/mañ[aá]/i,afternoon:/tarde/i,evening:/tardiña/i,night:/noite/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(595),a=e(596),r=e(597),o=e(598),n=e(599),s={code:"gu",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"હમણાં",other:"​આશરે {{count}} સેકંડ"},xSeconds:{one:"1 સેકંડ",other:"{{count}} સેકંડ"},halfAMinute:"અડધી મિનિટ",lessThanXMinutes:{one:"આ મિનિટ",other:"​આશરે {{count}} મિનિટ"},xMinutes:{one:"1 મિનિટ",other:"{{count}} મિનિટ"},aboutXHours:{one:"​આશરે 1 કલાક",other:"​આશરે {{count}} કલાક"},xHours:{one:"1 કલાક",other:"{{count}} કલાક"},xDays:{one:"1 દિવસ",other:"{{count}} દિવસ"},aboutXWeeks:{one:"આશરે 1 અઠવાડિયું",other:"આશરે {{count}} અઠવાડિયા"},xWeeks:{one:"1 અઠવાડિયું",other:"{{count}} અઠવાડિયા"},aboutXMonths:{one:"આશરે 1 મહિનો",other:"આશરે {{count}} મહિના"},xMonths:{one:"1 મહિનો",other:"{{count}} મહિના"},aboutXYears:{one:"આશરે 1 વર્ષ",other:"આશરે {{count}} વર્ષ"},xYears:{one:"1 વર્ષ",other:"{{count}} વર્ષ"},overXYears:{one:"1 વર્ષથી વધુ",other:"{{count}} વર્ષથી વધુ"},almostXYears:{one:"લગભગ 1 વર્ષ",other:"લગભગ {{count}} વર્ષ"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?s+"માં":s+" પહેલાં":s}},function(l,i,e){var t=e(2),a={full:"EEEE, d MMMM, y",long:"d MMMM, y",medium:"d MMM, y",short:"d/M/yy"},r={full:"hh:mm:ss a zzzz",long:"hh:mm:ss a z",medium:"hh:mm:ss a",short:"hh:mm a"},o={full:"{{date}} {{time}}",long:"{{date}} {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'પાછલા' eeee p",yesterday:"'ગઈકાલે' p",today:"'આજે' p",tomorrow:"'આવતીકાલે' p",nextWeek:"eeee p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["ઈસપૂ","ઈસ"],abbreviated:["ઈ.સ.પૂર્વે","ઈ.સ."],wide:["ઈસવીસન પૂર્વે","ઈસવીસન"]},r={narrow:["1","2","3","4"],abbreviated:["Q1","Q2","Q3","Q4"],wide:["1લો ત્રિમાસ","2જો ત્રિમાસ","3જો ત્રિમાસ","4થો ત્રિમાસ"]},o={narrow:["જા","ફે","મા","એ","મે","જૂ","જુ","ઓ","સ","ઓ","ન","ડિ"],abbreviated:["જાન્યુ","ફેબ્રુ","માર્ચ","એપ્રિલ","મે","જૂન","જુલાઈ","ઑગસ્ટ","સપ્ટે","ઓક્ટો","નવે","ડિસે"],wide:["જાન્યુઆરી","ફેબ્રુઆરી","માર્ચ","એપ્રિલ","મે","જૂન","જુલાઇ","ઓગસ્ટ","સપ્ટેમ્બર","ઓક્ટોબર","નવેમ્બર","ડિસેમ્બર"]},n={narrow:["ર","સો","મં","બુ","ગુ","શુ","શ"],short:["ર","સો","મં","બુ","ગુ","શુ","શ"],abbreviated:["રવિ","સોમ","મંગળ","બુધ","ગુરુ","શુક્ર","શનિ"],wide:["રવિવાર","સોમવાર","મંગળવાર","બુધવાર","ગુરુવાર","શુક્રવાર","શનિવાર"]},s={narrow:{am:"AM",pm:"PM",midnight:"મ.રાત્રિ",noon:"બ.",morning:"સવારે",afternoon:"બપોરે",evening:"સાંજે",night:"રાત્રે"},abbreviated:{am:"AM",pm:"PM",midnight:"​મધ્યરાત્રિ",noon:"બપોરે",morning:"સવારે",afternoon:"બપોરે",evening:"સાંજે",night:"રાત્રે"},wide:{am:"AM",pm:"PM",midnight:"​મધ્યરાત્રિ",noon:"બપોરે",morning:"સવારે",afternoon:"બપોરે",evening:"સાંજે",night:"રાત્રે"}},u={narrow:{am:"AM",pm:"PM",midnight:"મ.રાત્રિ",noon:"બપોરે",morning:"સવારે",afternoon:"બપોરે",evening:"સાંજે",night:"રાત્રે"},abbreviated:{am:"AM",pm:"PM",midnight:"મધ્યરાત્રિ",noon:"બપોરે",morning:"સવારે",afternoon:"બપોરે",evening:"સાંજે",night:"રાત્રે"},wide:{am:"AM",pm:"PM",midnight:"​મધ્યરાત્રિ",noon:"બપોરે",morning:"સવારે",afternoon:"બપોરે",evening:"સાંજે",night:"રાત્રે"}};function d(f,_){var c=Number(f);return c}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(લ|જ|થ|ઠ્ઠ|મ)?/i,o=/\d+/i,n={narrow:/^(ઈસપૂ|ઈસ)/i,abbreviated:/^(ઈ\.સ\.પૂર્વે|ઈ\.સ\.)/i,wide:/^(ઈસવીસન\sપૂર્વે|ઈસવીસન)/i},s={any:[/^(ઈસપૂ|ઈસ)/i,/^(ઈ\.સ\.પૂર્વે|ઈ\.સ\.)/i,/^(ઈસવીસન\sપૂર્વે|ઈસવીસન)/i]},u={narrow:/^[1234]/i,abbreviated:/^q[1234]/i,wide:/^[1234](લો|જો|થો)? ત્રિમાસ/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[જાફેમાએમેજૂજુઓસઓનડિ]/i,abbreviated:/^(જાન્યુ|ફેબ્રુ|માર્ચ|એપ્રિલ|મે|જૂન|જુલાઈ|ઑગસ્ટ|સપ્ટે|ઓક્ટો|નવે|ડિસે)/i,wide:/^(જાન્યુઆરી|ફેબ્રુઆરી|માર્ચ|એપ્રિલ|મે|જૂન|જુલાઇ|ઓગસ્ટ|સપ્ટેમ્બર|ઓક્ટોબર|નવેમ્બર|ડિસેમ્બર)/i},f={narrow:[/^જા/i,/^ફે/i,/^મા/i,/^એ/i,/^મે/i,/^જૂ/i,/^જુ/i,/^ઑગ/i,/^સ/i,/^ઓક્ટો/i,/^ન/i,/^ડિ/i],any:[/^જા/i,/^ફે/i,/^મા/i,/^એ/i,/^મે/i,/^જૂ/i,/^જુ/i,/^ઑગ/i,/^સ/i,/^ઓક્ટો/i,/^ન/i,/^ડિ/i]},_={narrow:/^(ર|સો|મં|બુ|ગુ|શુ|શ)/i,short:/^(ર|સો|મં|બુ|ગુ|શુ|શ)/i,abbreviated:/^(રવિ|સોમ|મંગળ|બુધ|ગુરુ|શુક્ર|શનિ)/i,wide:/^(રવિવાર|સોમવાર|મંગળવાર|બુધવાર|ગુરુવાર|શુક્રવાર|શનિવાર)/i},c={narrow:[/^ર/i,/^સો/i,/^મં/i,/^બુ/i,/^ગુ/i,/^શુ/i,/^શ/i],any:[/^ર/i,/^સો/i,/^મં/i,/^બુ/i,/^ગુ/i,/^શુ/i,/^શ/i]},v={narrow:/^(a|p|મ\.?|સ|બ|સાં|રા)/i,any:/^(a|p|મ\.?|સ|બ|સાં|રા)/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/^મ\.?/i,noon:/^બ/i,morning:/સ/i,afternoon:/બ/i,evening:/સાં/i,night:/રા/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(601),a=e(602),r=e(603),o=e(604),n=e(605),s={code:"he",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:0,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"פחות משנייה",two:"פחות משתי שניות",other:"פחות מ־{{count}} שניות"},xSeconds:{one:"שנייה",two:"שתי שניות",other:"{{count}} שניות"},halfAMinute:"חצי דקה",lessThanXMinutes:{one:"פחות מדקה",two:"פחות משתי דקות",other:"פחות מ־{{count}} דקות"},xMinutes:{one:"דקה",two:"שתי דקות",other:"{{count}} דקות"},aboutXHours:{one:"כשעה",two:"כשעתיים",other:"כ־{{count}} שעות"},xHours:{one:"שעה",two:"שעתיים",other:"{{count}} שעות"},xDays:{one:"יום",two:"יומיים",other:"{{count}} ימים"},aboutXWeeks:{one:"כשבוע",two:"כשבועיים",other:"כ־{{count}} שבועות"},xWeeks:{one:"שבוע",two:"שבועיים",other:"{{count}} שבועות"},aboutXMonths:{one:"כחודש",two:"כחודשיים",other:"כ־{{count}} חודשים"},xMonths:{one:"חודש",two:"חודשיים",other:"{{count}} חודשים"},aboutXYears:{one:"כשנה",two:"כשנתיים",other:"כ־{{count}} שנים"},xYears:{one:"שנה",two:"שנתיים",other:"{{count}} שנים"},overXYears:{one:"יותר משנה",two:"יותר משנתיים",other:"יותר מ־{{count}} שנים"},almostXYears:{one:"כמעט שנה",two:"כמעט שנתיים",other:"כמעט {{count}} שנים"}};function a(r,o,n){if(n=n||{},r==="xDays"&&n.addSuffix&&o<=2){var s={1:"אתמול",2:"שלשום"},u={1:"מחר",2:"מחרתיים"};return n.comparison>0?u[o]:s[o]}var d;return typeof t[r]=="string"?d=t[r]:o===1?d=t[r].one:o===2?d=t[r].two:d=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"בעוד "+d:"לפני "+d:d}},function(l,i,e){var t=e(2),a={full:"EEEE, d בMMMM y",long:"d בMMMM y",medium:"d בMMM y",short:"d.M.y"},r={full:"H:mm:ss zzzz",long:"H:mm:ss z",medium:"H:mm:ss",short:"H:mm"},o={full:"{{date}} 'בשעה' {{time}}",long:"{{date}} 'בשעה' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"eeee 'שעבר בשעה' p",yesterday:"'אתמול בשעה' p",today:"'היום בשעה' p",tomorrow:"'מחר בשעה' p",nextWeek:"eeee 'בשעה' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["לפנה״ס","לספירה"],abbreviated:["לפנה״ס","לספירה"],wide:["לפני הספירה","לספירה"]},r={narrow:["1","2","3","4"],abbreviated:["Q1","Q2","Q3","Q4"],wide:["רבעון 1","רבעון 2","רבעון 3","רבעון 4"]},o={narrow:["1","2","3","4","5","6","7","8","9","10","11","12"],abbreviated:["ינו׳","פבר׳","מרץ","אפר׳","מאי","יוני","יולי","אוג׳","ספט׳","אוק׳","נוב׳","דצמ׳"],wide:["ינואר","פברואר","מרץ","אפריל","מאי","יוני","יולי","אוגוסט","ספטמבר","אוקטובר","נובמבר","דצמבר"]},n={narrow:["א׳","ב׳","ג׳","ד׳","ה׳","ו׳","ש׳"],short:["א׳","ב׳","ג׳","ד׳","ה׳","ו׳","ש׳"],abbreviated:["יום א׳","יום ב׳","יום ג׳","יום ד׳","יום ה׳","יום ו׳","שבת"],wide:["יום ראשון","יום שני","יום שלישי","יום רביעי","יום חמישי","יום שישי","יום שבת"]},s={narrow:{am:"לפנה״צ",pm:"אחה״צ",midnight:"חצות",noon:"צהריים",morning:"בוקר",afternoon:"אחר הצהריים",evening:"ערב",night:"לילה"},abbreviated:{am:"לפנה״צ",pm:"אחה״צ",midnight:"חצות",noon:"צהריים",morning:"בוקר",afternoon:"אחר הצהריים",evening:"ערב",night:"לילה"},wide:{am:"לפנה״צ",pm:"אחה״צ",midnight:"חצות",noon:"צהריים",morning:"בוקר",afternoon:"אחר הצהריים",evening:"ערב",night:"לילה"}},u={narrow:{am:"לפנה״צ",pm:"אחה״צ",midnight:"חצות",noon:"צהריים",morning:"בבוקר",afternoon:"בצהריים",evening:"בערב",night:"בלילה"},abbreviated:{am:"לפנה״צ",pm:"אחה״צ",midnight:"חצות",noon:"צהריים",morning:"בבוקר",afternoon:"אחר הצהריים",evening:"בערב",night:"בלילה"},wide:{am:"לפנה״צ",pm:"אחה״צ",midnight:"חצות",noon:"צהריים",morning:"בבוקר",afternoon:"אחר הצהריים",evening:"בערב",night:"בלילה"}};function d(f,_){var c=Number(f);if(c<=0||c>10)return c;var v=_||{},g=String(v.unit),P=["year","hour","minute","second"].indexOf(g)>=0,h=["ראשון","שני","שלישי","רביעי","חמישי","שישי","שביעי","שמיני","תשיעי","עשירי"],y=["ראשונה","שנייה","שלישית","רביעית","חמישית","שישית","שביעית","שמינית","תשיעית","עשירית"],W=c-1;return P?y[W]:h[W]}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+|(ראשון|שני|שלישי|רביעי|חמישי|שישי|שביעי|שמיני|תשיעי|עשירי|ראשונה|שנייה|שלישית|רביעית|חמישית|שישית|שביעית|שמינית|תשיעית|עשירית))/i,o=/^(\d+|רא|שנ|של|רב|ח|שי|שב|שמ|ת|ע)/i,n={narrow:/^ל(ספירה|פנה״ס)/i,abbreviated:/^ל(ספירה|פנה״ס)/i,wide:/^ל(פני ה)?ספירה/i},s={any:[/^לפ/i,/^לס/i]},u={narrow:/^[1234]/i,abbreviated:/^q[1234]/i,wide:/^רבעון [1234]/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^\d+/i,abbreviated:/^(ינו|פבר|מרץ|אפר|מאי|יוני|יולי|אוג|ספט|אוק|נוב|דצמ)׳?/i,wide:/^(ינואר|פברואר|מרץ|אפריל|מאי|יוני|יולי|אוגוסט|ספטמבר|אוקטובר|נובמבר|דצמבר)/i},f={narrow:[/^1$/i,/^2/i,/^3/i,/^4/i,/^5/i,/^6/i,/^7/i,/^8/i,/^9/i,/^10/i,/^11/i,/^12/i],any:[/^ינ/i,/^פ/i,/^מר/i,/^אפ/i,/^מא/i,/^יונ/i,/^יול/i,/^אוג/i,/^ס/i,/^אוק/i,/^נ/i,/^ד/i]},_={narrow:/^[אבגדהוש]׳/i,short:/^[אבגדהוש]׳/i,abbreviated:/^(שבת|יום (א|ב|ג|ד|ה|ו)׳)/i,wide:/^יום (ראשון|שני|שלישי|רביעי|חמישי|שישי|שבת)/i},c={abbreviated:[/א׳$/i,/ב׳$/i,/ג׳$/i,/ד׳$/i,/ה׳$/i,/ו׳$/i,/^ש/i],wide:[/ן$/i,/ני$/i,/לישי$/i,/עי$/i,/מישי$/i,/שישי$/i,/ת$/i],any:[/^א/i,/^ב/i,/^ג/i,/^ד/i,/^ה/i,/^ו/i,/^ש/i]},v={any:/^(אחר ה|ב)?(חצות|צהריים|בוקר|ערב|לילה|אחה״צ|לפנה״צ)/i},g={any:{am:/^לפ/i,pm:/^אחה/i,midnight:/^ח/i,noon:/^צ/i,morning:/בוקר/i,afternoon:/בצ|אחר/i,evening:/ערב/i,night:/לילה/i}},P=["רא","שנ","של","רב","ח","שי","שב","שמ","ת","ע"],h={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(y){var W=parseInt(y,10);return isNaN(W)?P.indexOf(y)+1:W}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(y){return y+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=h},function(l,i,e){var t=e(607),a=e(608),r=e(609),o=e(97),n=e(610),s={code:"hi",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:0,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=r;var t=e(97),a={lessThanXSeconds:{one:"१ सेकंड से कम",other:"{{count}} सेकंड से कम"},xSeconds:{one:"१ सेकंड",other:"{{count}} सेकंड"},halfAMinute:"आधा मिनट",lessThanXMinutes:{one:"१ मिनट से कम",other:"{{count}} मिनट से कम"},xMinutes:{one:"१ मिनट",other:"{{count}} मिनट"},aboutXHours:{one:"लगभग १ घंटा",other:"लगभग {{count}} घंटे"},xHours:{one:"१ घंटा",other:"{{count}} घंटे"},xDays:{one:"१ दिन",other:"{{count}} दिन"},aboutXWeeks:{one:"लगभग १ सप्ताह",other:"लगभग {{count}} सप्ताह"},xWeeks:{one:"१ सप्ताह",other:"{{count}} सप्ताह"},aboutXMonths:{one:"लगभग १ महीना",other:"लगभग {{count}} महीने"},xMonths:{one:"१ महीना",other:"{{count}} महीने"},aboutXYears:{one:"लगभग १ वर्ष",other:"लगभग {{count}} वर्ष"},xYears:{one:"१ वर्ष",other:"{{count}} वर्ष"},overXYears:{one:"१ वर्ष से अधिक",other:"{{count}} वर्ष से अधिक"},almostXYears:{one:"लगभग १ वर्ष",other:"लगभग {{count}} वर्ष"}};function r(o,n,s){s=s||{};var u;return typeof a[o]=="string"?u=a[o]:n===1?u=a[o].one:u=a[o].other.replace("{{count}}",t.a.numberToLocale(n)),s.addSuffix?s.comparison>0?u+"मे ":u+" पहले":u}},function(l,i,e){var t=e(2),a={full:"EEEE, do MMMM, y",long:"do MMMM, y",medium:"d MMM, y",short:"dd/MM/yyyy"},r={full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},o={full:"{{date}} 'को' {{time}}",long:"{{date}} 'को' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'पिछले' eeee p",yesterday:"'कल' p",today:"'आज' p",tomorrow:"'कल' p",nextWeek:"eeee 'को' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(4),a=e(5),r=e(97),o=/^[०१२३४५६७८९]+/i,n=/^[०१२३४५६७८९]+/i,s={narrow:/^(ईसा-पूर्व|ईस्वी)/i,abbreviated:/^(ईसा\.?\s?पूर्व\.?|ईसा\.?)/i,wide:/^(ईसा-पूर्व|ईसवी पूर्व|ईसवी सन|ईसवी)/i},u={any:[/^b/i,/^(a|c)/i]},d={narrow:/^[1234]/i,abbreviated:/^ति[1234]/i,wide:/^[1234](पहली|दूसरी|तीसरी|चौथी)? तिमाही/i},m={any:[/1/i,/2/i,/3/i,/4/i]},f={narrow:/^[जफ़माअप्मईजूनजुअगसिअक्तनदि]/i,abbreviated:/^(जन|फ़र|मार्च|अप्|मई|जून|जुल|अग|सित|अक्तू|नव|दिस)/i,wide:/^(जनवरी|फ़रवरी|मार्च|अप्रैल|मई|जून|जुलाई|अगस्त|सितंबर|अक्तूबर|नवंबर|दिसंबर)/i},_={narrow:[/^ज/i,/^फ़/i,/^मा/i,/^अप्/i,/^मई/i,/^जू/i,/^जु/i,/^अग/i,/^सि/i,/^अक्तू/i,/^न/i,/^दि/i],any:[/^जन/i,/^फ़/i,/^मा/i,/^अप्/i,/^मई/i,/^जू/i,/^जु/i,/^अग/i,/^सि/i,/^अक्तू/i,/^नव/i,/^दिस/i]},c={narrow:/^[रविसोममंगलबुधगुरुशुक्रशनि]/i,short:/^(रवि|सोम|मंगल|बुध|गुरु|शुक्र|शनि)/i,abbreviated:/^(रवि|सोम|मंगल|बुध|गुरु|शुक्र|शनि)/i,wide:/^(रविवार|सोमवार|मंगलवार|बुधवार|गुरुवार|शुक्रवार|शनिवार)/i},v={narrow:[/^रवि/i,/^सोम/i,/^मंगल/i,/^बुध/i,/^गुरु/i,/^शुक्र/i,/^शनि/i],any:[/^रवि/i,/^सोम/i,/^मंगल/i,/^बुध/i,/^गुरु/i,/^शुक्र/i,/^शनि/i]},g={narrow:/^(पू|अ|म|द.\?|सु|दो|शा|रा)/i,any:/^(पूर्वाह्न|अपराह्न|म|द.\?|सु|दो|शा|रा)/i},P={any:{am:/^पूर्वाह्न/i,pm:/^अपराह्न/i,midnight:/^मध्य/i,noon:/^दो/i,morning:/सु/i,afternoon:/दो/i,evening:/शा/i,night:/रा/i}},h={ordinalNumber:t.a({matchPattern:o,parsePattern:n,valueCallback:r.a.localeToNumber}),era:a.a({matchPatterns:s,defaultMatchWidth:"wide",parsePatterns:u,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:d,defaultMatchWidth:"wide",parsePatterns:m,defaultParseWidth:"any",valueCallback:function(y){return y+1}}),month:a.a({matchPatterns:f,defaultMatchWidth:"wide",parsePatterns:_,defaultParseWidth:"any"}),day:a.a({matchPatterns:c,defaultMatchWidth:"wide",parsePatterns:v,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:g,defaultMatchWidth:"any",parsePatterns:P,defaultParseWidth:"any"})};i.a=h},function(l,i,e){var t=e(612),a=e(613),r=e(614),o=e(615),n=e(616),s={code:"hr",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:{standalone:"manje od 1 sekunde",withPrepositionAgo:"manje od 1 sekunde",withPrepositionIn:"manje od 1 sekundu"},dual:"manje od {{count}} sekunde",other:"manje od {{count}} sekundi"},xSeconds:{one:{standalone:"1 sekunda",withPrepositionAgo:"1 sekunde",withPrepositionIn:"1 sekundu"},dual:"{{count}} sekunde",other:"{{count}} sekundi"},halfAMinute:"pola minute",lessThanXMinutes:{one:{standalone:"manje od 1 minute",withPrepositionAgo:"manje od 1 minute",withPrepositionIn:"manje od 1 minutu"},dual:"manje od {{count}} minute",other:"manje od {{count}} minuta"},xMinutes:{one:{standalone:"1 minuta",withPrepositionAgo:"1 minute",withPrepositionIn:"1 minutu"},dual:"{{count}} minute",other:"{{count}} minuta"},aboutXHours:{one:{standalone:"oko 1 sat",withPrepositionAgo:"oko 1 sat",withPrepositionIn:"oko 1 sat"},dual:"oko {{count}} sata",other:"oko {{count}} sati"},xHours:{one:{standalone:"1 sat",withPrepositionAgo:"1 sat",withPrepositionIn:"1 sat"},dual:"{{count}} sata",other:"{{count}} sati"},xDays:{one:{standalone:"1 dan",withPrepositionAgo:"1 dan",withPrepositionIn:"1 dan"},dual:"{{count}} dana",other:"{{count}} dana"},aboutXWeeks:{one:{standalone:"oko 1 tjedan",withPrepositionAgo:"oko 1 tjedan",withPrepositionIn:"oko 1 tjedan"},dual:"oko {{count}} tjedna",other:"oko {{count}} tjedana"},xWeeks:{one:{standalone:"1 tjedan",withPrepositionAgo:"1 tjedan",withPrepositionIn:"1 tjedan"},dual:"{{count}} tjedna",other:"{{count}} tjedana"},aboutXMonths:{one:{standalone:"oko 1 mjesec",withPrepositionAgo:"oko 1 mjesec",withPrepositionIn:"oko 1 mjesec"},dual:"oko {{count}} mjeseca",other:"oko {{count}} mjeseci"},xMonths:{one:{standalone:"1 mjesec",withPrepositionAgo:"1 mjesec",withPrepositionIn:"1 mjesec"},dual:"{{count}} mjeseca",other:"{{count}} mjeseci"},aboutXYears:{one:{standalone:"oko 1 godinu",withPrepositionAgo:"oko 1 godinu",withPrepositionIn:"oko 1 godinu"},dual:"oko {{count}} godine",other:"oko {{count}} godina"},xYears:{one:{standalone:"1 godina",withPrepositionAgo:"1 godine",withPrepositionIn:"1 godinu"},dual:"{{count}} godine",other:"{{count}} godina"},overXYears:{one:{standalone:"preko 1 godinu",withPrepositionAgo:"preko 1 godinu",withPrepositionIn:"preko 1 godinu"},dual:"preko {{count}} godine",other:"preko {{count}} godina"},almostXYears:{one:{standalone:"gotovo 1 godinu",withPrepositionAgo:"gotovo 1 godinu",withPrepositionIn:"gotovo 1 godinu"},dual:"gotovo {{count}} godine",other:"gotovo {{count}} godina"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?n.addSuffix?n.comparison>0?s=t[r].one.withPrepositionIn:s=t[r].one.withPrepositionAgo:s=t[r].one.standalone:o%10>1&&o%10<5&&String(o).substr(-2,1)!=="1"?s=t[r].dual.replace("{{count}}",o):s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"za "+s:"prije "+s:s}},function(l,i,e){var t=e(2),a={full:"EEEE, d. MMMM y.",long:"d. MMMM y.",medium:"d. MMM y.",short:"dd. MM. y."},r={full:"HH:mm:ss (zzzz)",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'u' {{time}}",long:"{{date}} 'u' {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:function(r){var o=r.getUTCDay();switch(o){case 0:return"'prošlu nedjelju u' p";case 3:return"'prošlu srijedu u' p";case 6:return"'prošlu subotu u' p";default:return"'prošli' EEEE 'u' p"}},yesterday:"'jučer u' p",today:"'danas u' p",tomorrow:"'sutra u' p",nextWeek:function(r){var o=r.getUTCDay();switch(o){case 0:return"'iduću nedjelju u' p";case 3:return"'iduću srijedu u' p";case 6:return"'iduću subotu u' p";default:return"'prošli' EEEE 'u' p"}},other:"P"};function a(r,o,n,s){var u=t[r];return typeof u=="function"?u(o):u}},function(l,i,e){var t=e(3);function a(_){var c=Number(_);return String(c).concat(".")}var r={narrow:["pr.n.e.","AD"],abbreviated:["pr. Kr.","po. Kr."],wide:["Prije Krista","Poslije Krista"]},o={narrow:["1.","2.","3.","4.","5.","6.","7.","8.","9.","10.","11.","12."],abbreviated:["sij","velj","ožu","tra","svi","lip","srp","kol","ruj","lis","stu","pro"],wide:["siječanj","veljača","ožujak","travanj","svibanj","lipanj","srpanj","kolovoz","rujan","listopad","studeni","prosinac"]},n={narrow:["1.","2.","3.","4.","5.","6.","7.","8.","9.","10.","11.","12."],abbreviated:["sij","velj","ožu","tra","svi","lip","srp","kol","ruj","lis","stu","pro"],wide:["siječnja","veljače","ožujka","travnja","svibnja","lipnja","srpnja","kolovoza","rujna","listopada","studenog","prosinca"]},s={narrow:["1.","2.","3.","4."],abbreviated:["1. kv.","2. kv.","3. kv.","4. kv."],wide:["1. kvartal","2. kvartal","3. kvartal","4. kvartal"]},u={narrow:["N","P","U","S","Č","P","S"],short:["ned","pon","uto","sri","čet","pet","sub"],abbreviated:["ned","pon","uto","sri","čet","pet","sub"],wide:["nedjelja","ponedjeljak","utorak","srijeda","četvrtak","petak","subota"]},d={narrow:{am:"AM",pm:"PM",midnight:"ponoć",noon:"podne",morning:"ujutro",afternoon:"popodne",evening:"navečer",night:"noću"},abbreviated:{am:"AM",pm:"PM",midnight:"ponoć",noon:"podne",morning:"ujutro",afternoon:"popodne",evening:"navečer",night:"noću"},wide:{am:"AM",pm:"PM",midnight:"ponoć",noon:"podne",morning:"ujutro",afternoon:"poslije podne",evening:"navečer",night:"noću"}},m={narrow:{am:"AM",pm:"PM",midnight:"ponoć",noon:"podne",morning:"ujutro",afternoon:"popodne",evening:"navečer",night:"noću"},abbreviated:{am:"AM",pm:"PM",midnight:"ponoć",noon:"podne",morning:"ujutro",afternoon:"popodne",evening:"navečer",night:"noću"},wide:{am:"AM",pm:"PM",midnight:"ponoć",noon:"podne",morning:"ujutro",afternoon:"poslije podne",evening:"navečer",night:"noću"}},f={ordinalNumber:a,era:t.a({values:r,defaultWidth:"wide"}),quarter:t.a({values:s,defaultWidth:"wide",argumentCallback:function(_){return Number(_)-1}}),month:t.a({values:o,defaultWidth:"wide",formattingValues:n,defaultFormattingWidth:"wide"}),day:t.a({values:u,defaultWidth:"wide"}),dayPeriod:t.a({values:m,defaultWidth:"wide",formattingValues:d,defaultFormattingWidth:"wide"})};i.a=f},function(l,i,e){var t=e(5),a=e(4),r=/^(\d+)\./i,o=/\d+/i,n={narrow:/^(pr\.n\.e\.|AD)/i,abbreviated:/^(pr\.\s?Kr\.|po\.\s?Kr\.)/i,wide:/^(Prije Krista|prije nove ere|Poslije Krista|nova era)/i},s={any:[/^pr/i,/^(po|nova)/i]},u={narrow:/^[1234]/i,abbreviated:/^[1234]\.\s?kv\.?/i,wide:/^[1234]\. kvartal/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^(10|11|12|[123456789])\./i,abbreviated:/^(sij|velj|(ožu|ozu)|tra|svi|lip|srp|kol|ruj|lis|stu|pro)/i,wide:/^((siječanj|siječnja|sijecanj|sijecnja)|(veljača|veljače|veljaca|veljace)|(ožujak|ožujka|ozujak|ozujka)|(travanj|travnja)|(svibanj|svibnja)|(lipanj|lipnja)|(srpanj|srpnja)|(kolovoz|kolovoza)|(rujan|rujna)|(listopad|listopada)|(studeni|studenog)|(prosinac|prosinca))/i},f={narrow:[/(10|11|12|[123456789])/i],abbreviated:[/^sij/i,/^velj/i,/^(ožu|ozu)/i,/^tra/i,/^svi/i,/^lip/i,/^srp/i,/^kol/i,/^ruj/i,/^lis/i,/^stu/i,/^pro/i],wide:[/^sij/i,/^velj/i,/^(ožu|ozu)/i,/^tra/i,/^svi/i,/^lip/i,/^srp/i,/^kol/i,/^ruj/i,/^lis/i,/^stu/i,/^pro/i]},_={narrow:/^[npusčc]/i,short:/^(ned|pon|uto|sri|(čet|cet)|pet|sub)/i,abbreviated:/^(ned|pon|uto|sri|(čet|cet)|pet|sub)/i,wide:/^(nedjelja|ponedjeljak|utorak|srijeda|(četvrtak|cetvrtak)|petak|subota)/i},c={narrow:[/^s/i,/^m/i,/^t/i,/^w/i,/^t/i,/^f/i,/^s/i],any:[/^su/i,/^m/i,/^tu/i,/^w/i,/^th/i,/^f/i,/^sa/i]},v={any:/^(am|pm|ponoc|ponoć|(po)?podne|navecer|navečer|noću|poslije podne|ujutro)/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/^pono/i,noon:/^pod/i,morning:/jutro/i,afternoon:/(poslije\s|po)+podne/i,evening:/(navece|naveče)/i,night:/(nocu|noću)/i}},P={ordinalNumber:a.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:t.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:t.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:t.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"wide"}),day:t.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:t.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(618),a=e(619),r=e(620),o=e(621),n=e(622),s={code:"hu",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=n;var t={about:"körülbelül",over:"több mint",almost:"majdnem",lessthan:"kevesebb mint"},a={xseconds:" másodperc",halfaminute:"fél perc",xminutes:" perc",xhours:" óra",xdays:" nap",xweeks:" hét",xmonths:" hónap",xyears:" év"},r={xseconds:{"-1":" másodperccel ezelőtt",1:" másodperc múlva",0:" másodperce"},halfaminute:{"-1":"fél perccel ezelőtt",1:"fél perc múlva",0:"fél perce"},xminutes:{"-1":" perccel ezelőtt",1:" perc múlva",0:" perce"},xhours:{"-1":" órával ezelőtt",1:" óra múlva",0:" órája"},xdays:{"-1":" nappal ezelőtt",1:" nap múlva",0:" napja"},xweeks:{"-1":" héttel ezelőtt",1:" hét múlva",0:" hete"},xmonths:{"-1":" hónappal ezelőtt",1:" hónap múlva",0:" hónapja"},xyears:{"-1":" évvel ezelőtt",1:" év múlva",0:" éve"}};function o(s,u,d,m){var f=u?r[d][m]:a[d];return d==="halfaminute"?f:s+f}function n(s,u,d){d=d||{};var m=s.match(/about|over|almost|lessthan/i),f=s.replace(m,""),_;return _=o(u,d.addSuffix,f.toLowerCase(),d.comparison),m&&(_=t[m[0].toLowerCase()]+" "+_),_}},function(l,i,e){var t=e(2),a={full:"y. MMMM d., EEEE",long:"y. MMMM d.",medium:"y. MMM d.",short:"y. MM. dd."},r={full:"H:mm:ss zzzz",long:"H:mm:ss z",medium:"H:mm:ss",short:"H:mm"},o={full:"{{date}} {{time}}",long:"{{date}} {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=o;var t=["vasárnap","hétfőn","kedden","szerdán","csütörtökön","pénteken","szombaton"];function a(n){return function(s,u,d){var m=s.getUTCDay();return(n?"":"'múlt' ")+"'"+t[m]+"' p'-kor'"}}var r={lastWeek:a(!1),yesterday:"'tegnap' p'-kor'",today:"'ma' p'-kor'",tomorrow:"'holnap' p'-kor'",nextWeek:a(!0),other:"P"};function o(n,s,u,d){var m=r[n];return typeof m=="function"?m(s,u,d):m}},function(l,i,e){var t=e(3),a={narrow:["ie.","isz."],abbreviated:["i. e.","i. sz."],wide:["Krisztus előtt","időszámításunk szerint"]},r={narrow:["1.","2.","3.","4."],abbreviated:["1. n.év","2. n.év","3. n.év","4. n.év"],wide:["1. negyedév","2. negyedév","3. negyedév","4. negyedév"]},o={narrow:["I.","II.","III.","IV."],abbreviated:["I. n.év","II. n.év","III. n.év","IV. n.év"],wide:["I. negyedév","II. negyedév","III. negyedév","IV. negyedév"]},n={narrow:["J","F","M","Á","M","J","J","A","Sz","O","N","D"],abbreviated:["jan.","febr.","márc.","ápr.","máj.","jún.","júl.","aug.","szept.","okt.","nov.","dec."],wide:["január","február","március","április","május","június","július","augusztus","szeptember","október","november","december"]},s={narrow:["V","H","K","Sz","Cs","P","Sz"],short:["V","H","K","Sze","Cs","P","Szo"],abbreviated:["V","H","K","Sze","Cs","P","Szo"],wide:["vasárnap","hétfő","kedd","szerda","csütörtök","péntek","szombat"]},u={narrow:{am:"de.",pm:"du.",midnight:"éjfél",noon:"dél",morning:"reggel",afternoon:"du.",evening:"este",night:"éjjel"},abbreviated:{am:"de.",pm:"du.",midnight:"éjfél",noon:"dél",morning:"reggel",afternoon:"du.",evening:"este",night:"éjjel"},wide:{am:"de.",pm:"du.",midnight:"éjfél",noon:"dél",morning:"reggel",afternoon:"délután",evening:"este",night:"éjjel"}};function d(f,_){var c=Number(f);return c+"."}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",formattingValues:o,argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:n,defaultWidth:"wide"}),day:t.a({values:s,defaultWidth:"wide"}),dayPeriod:t.a({values:u,defaultWidth:"wide",defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)\.?/i,o=/\d+/i,n={narrow:/^(ie\.|isz\.)/i,abbreviated:/^(i\.\s?e\.?|b?\s?c\s?e|i\.\s?sz\.?)/i,wide:/^(Krisztus előtt|időszámításunk előtt|időszámításunk szerint|i\. sz\.)/i},s={narrow:[/ie/i,/isz/i],abbreviated:[/^(i\.?\s?e\.?|b\s?ce)/i,/^(i\.?\s?sz\.?|c\s?e)/i],any:[/előtt/i,/(szerint|i. sz.)/i]},u={narrow:/^[1234]\.?/i,abbreviated:/^[1234]?\.?\s?n\.év/i,wide:/^([1234]|I|II|III|IV)?\.?\s?negyedév/i},d={any:[/1|I$/i,/2|II$/i,/3|III/i,/4|IV/i]},m={narrow:/^[jfmaásond]|sz/i,abbreviated:/^(jan\.?|febr\.?|márc\.?|ápr\.?|máj\.?|jún\.?|júl\.?|aug\.?|szept\.?|okt\.?|nov\.?|dec\.?)/i,wide:/^(január|február|március|április|május|június|július|augusztus|szeptember|október|november|december)/i},f={narrow:[/^j/i,/^f/i,/^m/i,/^a|á/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s|sz/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^már/i,/^áp/i,/^máj/i,/^jún/i,/^júl/i,/^au/i,/^s/i,/^o/i,/^n/i,/^d/i]},_={narrow:/^([vhkpc]|sz|cs|sz)/i,short:/^([vhkp]|sze|cs|szo)/i,abbreviated:/^([vhkp]|sze|cs|szo)/i,wide:/^(vasárnap|hétfő|kedd|szerda|csütörtök|péntek|szombat)/i},c={narrow:[/^v/i,/^h/i,/^k/i,/^sz/i,/^c/i,/^p/i,/^sz/i],any:[/^v/i,/^h/i,/^k/i,/^sze/i,/^c/i,/^p/i,/^szo/i]},v={any:/^((de|du)\.?|éjfél|délután|dél|reggel|este|éjjel)/i},g={any:{am:/^de\.?/i,pm:/^du\.?/i,midnight:/^éjf/i,noon:/^dé/i,morning:/reg/i,afternoon:/^délu\.?/i,evening:/es/i,night:/éjj/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(624),a=e(625),r=e(626),o=e(627),n=e(628),s={code:"hy",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"ավելի քիչ քան 1 վայրկյան",other:"ավելի քիչ քան {{count}} վայրկյան"},xSeconds:{one:"1 վայրկյան",other:"{{count}} վայրկյան"},halfAMinute:"կես րոպե",lessThanXMinutes:{one:"ավելի քիչ քան 1 րոպե",other:"ավելի քիչ քան {{count}} րոպե"},xMinutes:{one:"1 րոպե",other:"{{count}} րոպե"},aboutXHours:{one:"մոտ 1 ժամ",other:"մոտ {{count}} ժամ"},xHours:{one:"1 ժամ",other:"{{count}} ժամ"},xDays:{one:"1 օր",other:"{{count}} օր"},aboutXWeeks:{one:"մոտ 1 շաբաթ",other:"մոտ {{count}} շաբաթ"},xWeeks:{one:"1 շաբաթ",other:"{{count}} շաբաթ"},aboutXMonths:{one:"մոտ 1 ամիս",other:"մոտ {{count}} ամիս"},xMonths:{one:"1 ամիս",other:"{{count}} ամիս"},aboutXYears:{one:"մոտ 1 տարի",other:"մոտ {{count}} տարի"},xYears:{one:"1 տարի",other:"{{count}} տարի"},overXYears:{one:"ավելի քան 1 տարի",other:"ավելի քան {{count}} տարի"},almostXYears:{one:"համարյա 1 տարի",other:"համարյա {{count}} տարի"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?s+" հետո":s+" առաջ":s}},function(l,i,e){var t=e(2),a={full:"d MMMM, y, EEEE",long:"d MMMM, y",medium:"d MMM, y",short:"dd.MM.yyyy"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'ժ․'{{time}}",long:"{{date}} 'ժ․'{{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'նախորդ' eeee p'֊ին'",yesterday:"'երեկ' p'֊ին'",today:"'այսօր' p'֊ին'",tomorrow:"'վաղը' p'֊ին'",nextWeek:"'հաջորդ' eeee p'֊ին'",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["Ք","Մ"],abbreviated:["ՔԱ","ՄԹ"],wide:["Քրիստոսից առաջ","Մեր թվարկության"]},r={narrow:["1","2","3","4"],abbreviated:["Ք1","Ք2","Ք3","Ք4"],wide:["1֊ին քառորդ","2֊րդ քառորդ","3֊րդ քառորդ","4֊րդ քառորդ"]},o={narrow:["Հ","Փ","Մ","Ա","Մ","Հ","Հ","Օ","Ս","Հ","Ն","Դ"],abbreviated:["հուն","փետ","մար","ապր","մայ","հուն","հուլ","օգս","սեպ","հոկ","նոյ","դեկ"],wide:["հունվար","փետրվար","մարտ","ապրիլ","մայիս","հունիս","հուլիս","օգոստոս","սեպտեմբեր","հոկտեմբեր","նոյեմբեր","դեկտեմբեր"]},n={narrow:["Կ","Ե","Ե","Չ","Հ","Ո","Շ"],short:["կր","եր","եք","չք","հգ","ուր","շբ"],abbreviated:["կիր","երկ","երք","չոր","հնգ","ուրբ","շաբ"],wide:["կիրակի","երկուշաբթի","երեքշաբթի","չորեքշաբթի","հինգշաբթի","ուրբաթ","շաբաթ"]},s={narrow:{am:"a",pm:"p",midnight:"կեսգշ",noon:"կեսօր",morning:"առավոտ",afternoon:"ցերեկ",evening:"երեկո",night:"գիշեր"},abbreviated:{am:"AM",pm:"PM",midnight:"կեսգիշեր",noon:"կեսօր",morning:"առավոտ",afternoon:"ցերեկ",evening:"երեկո",night:"գիշեր"},wide:{am:"a.m.",pm:"p.m.",midnight:"կեսգիշեր",noon:"կեսօր",morning:"առավոտ",afternoon:"ցերեկ",evening:"երեկո",night:"գիշեր"}},u={narrow:{am:"a",pm:"p",midnight:"կեսգշ",noon:"կեսօր",morning:"առավոտը",afternoon:"ցերեկը",evening:"երեկոյան",night:"գիշերը"},abbreviated:{am:"AM",pm:"PM",midnight:"կեսգիշերին",noon:"կեսօրին",morning:"առավոտը",afternoon:"ցերեկը",evening:"երեկոյան",night:"գիշերը"},wide:{am:"a.m.",pm:"p.m.",midnight:"կեսգիշերին",noon:"կեսօրին",morning:"առավոտը",afternoon:"ցերեկը",evening:"երեկոյան",night:"գիշերը"}};function d(f,_){var c=Number(f),v=c%100;return v<10&&v%10===1?c+"֊ին":c+"֊րդ"}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)((-|֊)?(ին|րդ))?/i,o=/\d+/i,n={narrow:/^(Ք|Մ)/i,abbreviated:/^(Ք\.?\s?Ա\.?|Մ\.?\s?Թ\.?\s?Ա\.?|Մ\.?\s?Թ\.?|Ք\.?\s?Հ\.?)/i,wide:/^(քրիստոսից առաջ|մեր թվարկությունից առաջ|մեր թվարկության|քրիստոսից հետո)/i},s={any:[/^(ք|մ)/i]},u={narrow:/^[1234]/i,abbreviated:/^ք[1234]/i,wide:/^[1234]((-|֊)?(ին|րդ)) քառորդ/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[հփմաօսնդ]/i,abbreviated:/^(հուն|փետ|մար|ապր|մայ|հուն|հուլ|օգս|սեպ|հոկ|նոյ|դեկ)/i,wide:/^(հունվար|փետրվար|մարտ|ապրիլ|մայիս|հունիս|հուլիս|օգոստոս|սեպտեմբեր|հոկտեմբեր|նոյեմբեր|դեկտեմբեր)/i},f={narrow:[/^հ/i,/^փ/i,/^մ/i,/^ա/i,/^մ/i,/^հ/i,/^հ/i,/^օ/i,/^ս/i,/^հ/i,/^ն/i,/^դ/i],any:[/^հու/i,/^փ/i,/^մար/i,/^ա/i,/^մայ/i,/^հուն/i,/^հուլ/i,/^օ/i,/^ս/i,/^հոկ/i,/^ն/i,/^դ/i]},_={narrow:/^[եչհոշկ]/i,short:/^(կր|եր|եք|չք|հգ|ուր|շբ)/i,abbreviated:/^(կիր|երկ|երք|չոր|հնգ|ուրբ|շաբ)/i,wide:/^(կիրակի|երկուշաբթի|երեքշաբթի|չորեքշաբթի|հինգշաբթի|ուրբաթ|շաբաթ)/i},c={narrow:[/^կ/i,/^ե/i,/^ե/i,/^չ/i,/^հ/i,/^(ո|Ո)/,/^շ/i],short:[/^կ/i,/^եր/i,/^եք/i,/^չ/i,/^հ/i,/^(ո|Ո)/,/^շ/i],abbreviated:[/^կ/i,/^երկ/i,/^երք/i,/^չ/i,/^հ/i,/^(ո|Ո)/,/^շ/i],wide:[/^կ/i,/^երկ/i,/^երե/i,/^չ/i,/^հ/i,/^(ո|Ո)/,/^շ/i]},v={narrow:/^([ap]|կեսգշ|կեսօր|(առավոտը?|ցերեկը?|երեկո(յան)?|գիշերը?))/i,any:/^([ap]\.?\s?m\.?|կեսգիշեր(ին)?|կեսօր(ին)?|(առավոտը?|ցերեկը?|երեկո(յան)?|գիշերը?))/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/կեսգիշեր/i,noon:/կեսօր/i,morning:/առավոտ/i,afternoon:/ցերեկ/i,evening:/երեկո/i,night:/գիշեր/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(630),a=e(631),r=e(632),o=e(633),n=e(634),s={code:"id",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"kurang dari 1 detik",other:"kurang dari {{count}} detik"},xSeconds:{one:"1 detik",other:"{{count}} detik"},halfAMinute:"setengah menit",lessThanXMinutes:{one:"kurang dari 1 menit",other:"kurang dari {{count}} menit"},xMinutes:{one:"1 menit",other:"{{count}} menit"},aboutXHours:{one:"sekitar 1 jam",other:"sekitar {{count}} jam"},xHours:{one:"1 jam",other:"{{count}} jam"},xDays:{one:"1 hari",other:"{{count}} hari"},aboutXWeeks:{one:"sekitar 1 minggu",other:"sekitar {{count}} minggu"},xWeeks:{one:"1 minggu",other:"{{count}} minggu"},aboutXMonths:{one:"sekitar 1 bulan",other:"sekitar {{count}} bulan"},xMonths:{one:"1 bulan",other:"{{count}} bulan"},aboutXYears:{one:"sekitar 1 tahun",other:"sekitar {{count}} tahun"},xYears:{one:"1 tahun",other:"{{count}} tahun"},overXYears:{one:"lebih dari 1 tahun",other:"lebih dari {{count}} tahun"},almostXYears:{one:"hampir 1 tahun",other:"hampir {{count}} tahun"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"dalam waktu "+s:s+" yang lalu":s}},function(l,i,e){var t=e(2),a={full:"EEEE, d MMMM yyyy",long:"d MMMM yyyy",medium:"d MMM yyyy",short:"d/M/yyyy"},r={full:"HH.mm.ss",long:"HH.mm.ss",medium:"HH.mm",short:"HH.mm"},o={full:"{{date}} 'pukul' {{time}}",long:"{{date}} 'pukul' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"eeee 'lalu pukul' p",yesterday:"'Kemarin pukul' p",today:"'Hari ini pukul' p",tomorrow:"'Besok pukul' p",nextWeek:"eeee 'pukul' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["SM","M"],abbreviated:["SM","M"],wide:["Sebelum Masehi","Masehi"]},r={narrow:["1","2","3","4"],abbreviated:["K1","K2","K3","K4"],wide:["Kuartal ke-1","Kuartal ke-2","Kuartal ke-3","Kuartal ke-4"]},o={narrow:["J","F","M","A","M","J","J","A","S","O","N","D"],abbreviated:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Ags","Sep","Okt","Nov","Des"],wide:["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","November","Desember"]},n={narrow:["M","S","S","R","K","J","S"],short:["Min","Sen","Sel","Rab","Kam","Jum","Sab"],abbreviated:["Min","Sen","Sel","Rab","Kam","Jum","Sab"],wide:["Minggu","Senin","Selasa","Rabu","Kamis","Jumat","Sabtu"]},s={narrow:{am:"AM",pm:"PM",midnight:"tengah malam",noon:"tengah hari",morning:"pagi",afternoon:"siang",evening:"sore",night:"malam"},abbreviated:{am:"AM",pm:"PM",midnight:"tengah malam",noon:"tengah hari",morning:"pagi",afternoon:"siang",evening:"sore",night:"malam"},wide:{am:"AM",pm:"PM",midnight:"tengah malam",noon:"tengah hari",morning:"pagi",afternoon:"siang",evening:"sore",night:"malam"}},u={narrow:{am:"AM",pm:"PM",midnight:"tengah malam",noon:"tengah hari",morning:"pagi",afternoon:"siang",evening:"sore",night:"malam"},abbreviated:{am:"AM",pm:"PM",midnight:"tengah malam",noon:"tengah hari",morning:"pagi",afternoon:"siang",evening:"sore",night:"malam"},wide:{am:"AM",pm:"PM",midnight:"tengah malam",noon:"tengah hari",morning:"pagi",afternoon:"siang",evening:"sore",night:"malam"}};function d(f,_){var c=Number(f);switch(c){default:return"ke-"+c}}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(5),a=e(4),r=/^ke-(\d+)?/i,o=/\d+/i,n={narrow:/^(sm|m)/i,abbreviated:/^(s\.?\s?m\.?|s\.?\s?e\.?\s?u\.?|m\.?|e\.?\s?u\.?)/i,wide:/^(sebelum masehi|sebelum era umum|masehi|era umum)/i},s={any:[/^s/i,/^(m|e)/i]},u={narrow:/^[1234]/i,abbreviated:/^K-?\s[1234]/i,wide:/^Kuartal ke-?\s?[1234]/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[jfmasond]/i,abbreviated:/^(jan|feb|mar|apr|mei|jun|jul|agt|sep|okt|nov|des)/i,wide:/^(januari|februari|maret|april|mei|juni|juli|agustus|september|oktober|november|desember)/i},f={narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^ma/i,/^ap/i,/^me/i,/^jun/i,/^jul/i,/^ag/i,/^s/i,/^o/i,/^n/i,/^d/i]},_={narrow:/^[srkjm]/i,short:/^(min|sen|sel|rab|kam|jum|sab)/i,abbreviated:/^(min|sen|sel|rab|kam|jum|sab)/i,wide:/^(minggu|senin|selasa|rabu|kamis|jumat|sabtu)/i},c={narrow:[/^m/i,/^s/i,/^s/i,/^r/i,/^k/i,/^j/i,/^s/i],any:[/^m/i,/^sen/i,/^sel/i,/^r/i,/^k/i,/^j/i,/^sa/i]},v={narrow:/^(a|p|tengah m|tengah h|(di(\swaktu)?) (pagi|siang|sore|malam))/i,any:/^([ap]\.?\s?m\.?|tengah malam|tengah hari|(di(\swaktu)?) (pagi|siang|sore|malam))/i},g={any:{am:/^a/i,pm:/^pm/i,midnight:/^tengah m/i,noon:/^tengah h/i,morning:/pagi/i,afternoon:/siang/i,evening:/sore/i,night:/malam/i}},P={ordinalNumber:a.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:t.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:t.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:t.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:t.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:t.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(636),a=e(637),r=e(638),o=e(639),n=e(640),s={code:"is",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"minna en 1 sekúnda",other:"minna en {{count}} sekúndur"},xSeconds:{one:"1 sekúnda",other:"{{count}} sekúndur"},halfAMinute:"hálf mínúta",lessThanXMinutes:{one:"minna en 1 mínúta",other:"minna en {{count}} mínútur"},xMinutes:{one:"1 mínúta",other:"{{count}} mínútur"},aboutXHours:{one:"u.þ.b. 1 klukkustund",other:"u.þ.b. {{count}} klukkustundir"},xHours:{one:"1 klukkustund",other:"{{count}} klukkustundir"},xDays:{one:"1 dagur",other:"{{count}} dagar"},aboutXWeeks:{one:"um viku",other:"um {{count}} vikur"},xWeeks:{one:"1 viku",other:"{{count}} vikur"},aboutXMonths:{one:"u.þ.b. 1 mánuður",other:"u.þ.b. {{count}} mánuðir"},xMonths:{one:"1 mánuður",other:"{{count}} mánuðir"},aboutXYears:{one:"u.þ.b. 1 ár",other:"u.þ.b. {{count}} ár"},xYears:{one:"1 ár",other:"{{count}} ár"},overXYears:{one:"meira en 1 ár",other:"meira en {{count}} ár"},almostXYears:{one:"næstum 1 ár",other:"næstum {{count}} ár"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"í "+s:s+" síðan":s}},function(l,i,e){var t=e(2),a={full:"EEEE, do MMMM y",long:"do MMMM y",medium:"do MMM y",short:"d.MM.y"},r={full:"'kl'. HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'kl.' {{time}}",long:"{{date}} 'kl.' {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'síðasta' dddd 'kl.' p",yesterday:"'í gær kl.' p",today:"'í dag kl.' p",tomorrow:"'á morgun kl.' p",nextWeek:"dddd 'kl.' p",other:"L"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["f.Kr.","e.Kr."],abbreviated:["f.Kr.","e.Kr."],wide:["fyrir Krist","eftir Krist"]},r={narrow:["1","2","3","4"],abbreviated:["1F","2F","3F","4F"],wide:["1. fjórðungur","2. fjórðungur","3. fjórðungur","4. fjórðungur"]},o={narrow:["J","F","M","A","M","J","J","Á","S","Ó","N","D"],abbreviated:["jan.","feb.","mars","apríl","maí","júní","júlí","ágúst","sept.","okt.","nóv.","des."],wide:["janúar","febrúar","mars","apríl","maí","júní","júlí","ágúst","september","október","nóvember","desember"]},n={narrow:["S","M","Þ","M","F","F","L"],short:["Su","Má","Þr","Mi","Fi","Fö","La"],abbreviated:["sun.","mán.","þri.","mið.","fim.","fös.","lau"],wide:["sunnudagur","mánudagur","þriðjudagur","miðvikudagur","fimmtudagur","föstudagur","laugardagur"]},s={narrow:{am:"f",pm:"e",midnight:"miðnætti",noon:"hádegi",morning:"morgunn",afternoon:"síðdegi",evening:"kvöld",night:"nótt"},abbreviated:{am:"f.h.",pm:"e.h.",midnight:"miðnætti",noon:"hádegi",morning:"morgunn",afternoon:"síðdegi",evening:"kvöld",night:"nótt"},wide:{am:"fyrir hádegi",pm:"eftir hádegi",midnight:"miðnætti",noon:"hádegi",morning:"morgunn",afternoon:"síðdegi",evening:"kvöld",night:"nótt"}},u={narrow:{am:"f",pm:"e",midnight:"á miðnætti",noon:"á hádegi",morning:"að morgni",afternoon:"síðdegis",evening:"um kvöld",night:"um nótt"},abbreviated:{am:"f.h.",pm:"e.h.",midnight:"á miðnætti",noon:"á hádegi",morning:"að morgni",afternoon:"síðdegis",evening:"um kvöld",night:"um nótt"},wide:{am:"fyrir hádegi",pm:"eftir hádegi",midnight:"á miðnætti",noon:"á hádegi",morning:"að morgni",afternoon:"síðdegis",evening:"um kvöld",night:"um nótt"}};function d(f,_){var c=Number(f);return c+"."}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(th|st|nd|rd)?/i,o=/\d+/i,n={narrow:/^(f\.Kr\.|e\.Kr\.)/i,abbreviated:/^(f\.Kr\.|e\.Kr\.)/i,wide:/^(fyrir Krist|eftir Krist)/i},s={any:[/^(f\.Kr\.|e\.Kr\.)/i]},u={narrow:/^[1234]/i,abbreviated:/^q[1234]/i,wide:/^[1234] fjórðungur/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[jfmásónd]/i,abbreviated:/^(jan\.|feb\.|mars\.|apríl\.|maí|júní|júlí|águst|sep\.|oct\.|nov\.|dec\.)/i,wide:/^(januar|februar|mars|apríl|maí|júní|júlí|águst|september|október|nóvember|desember)/i},f={narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^á/i,/^s/i,/^ó/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^mar/i,/^ap/i,/^maí/i,/^jún/i,/^júl/i,/^áu/i,/^s/i,/^ó/i,/^n/i,/^d/i]},_={narrow:/^[smtwf]/i,short:/^(su|má|þr|mi|fi|fö|la)/i,abbreviated:/^(sun|mán|þri|mið|fim|fös|lau)\.?/i,wide:/^(sunnudagur|mánudagur|þriðjudagur|miðvikudagur|fimmtudagur|föstudagur|laugardagur)/i},c={narrow:[/^s/i,/^m/i,/^þ/i,/^m/i,/^f/i,/^f/i,/^l/i],any:[/^su/i,/^má/i,/^þr/i,/^mi/i,/^fi/i,/^fö/i,/^la/i]},v={narrow:/^(f|e|síðdegis|(á|að|um) (morgni|kvöld|nótt|miðnætti))/i,any:/^(fyrir hádegi|eftir hádegi|[ef]\.?h\.?|síðdegis|morgunn|(á|að|um) (morgni|kvöld|nótt|miðnætti))/i},g={any:{am:/^f/i,pm:/^e/i,midnight:/^mi/i,noon:/^há/i,morning:/morgunn/i,afternoon:/síðdegi/i,evening:/kvöld/i,night:/nótt/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(642),a=e(643),r=e(644),o=e(645),n=e(646),s={code:"it",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"meno di un secondo",other:"meno di {{count}} secondi"},xSeconds:{one:"un secondo",other:"{{count}} secondi"},halfAMinute:"alcuni secondi",lessThanXMinutes:{one:"meno di un minuto",other:"meno di {{count}} minuti"},xMinutes:{one:"un minuto",other:"{{count}} minuti"},aboutXHours:{one:"circa un'ora",other:"circa {{count}} ore"},xHours:{one:"un'ora",other:"{{count}} ore"},xDays:{one:"un giorno",other:"{{count}} giorni"},aboutXWeeks:{one:"circa una settimana",other:"circa {{count}} settimane"},xWeeks:{one:"una settimana",other:"{{count}} settimane"},aboutXMonths:{one:"circa un mese",other:"circa {{count}} mesi"},xMonths:{one:"un mese",other:"{{count}} mesi"},aboutXYears:{one:"circa un anno",other:"circa {{count}} anni"},xYears:{one:"un anno",other:"{{count}} anni"},overXYears:{one:"più di un anno",other:"più di {{count}} anni"},almostXYears:{one:"quasi un anno",other:"quasi {{count}} anni"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"tra "+s:s+" fa":s}},function(l,i,e){var t=e(2),a={full:"EEEE d MMMM y",long:"d MMMM y",medium:"d MMM y",short:"dd/MM/y"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} {{time}}",long:"{{date}} {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=u;var t=e(13),a=["domenica","lunedì","martedì","mercoledì","giovedì","venerdì","sabato"];function r(d){switch(d){case 0:return"'domenica scorsa alle' p";default:return"'"+a[d]+" scorso alle' p"}}function o(d){return"'"+a[d]+" alle' p"}function n(d){switch(d){case 0:return"'domenica prossima alle' p";default:return"'"+a[d]+" prossimo alle' p"}}var s={lastWeek:function(d,m,f){var _=d.getUTCDay();return t.a(d,m,f)?o(_):r(_)},yesterday:"'ieri alle' p",today:"'oggi alle' p",tomorrow:"'domani alle' p",nextWeek:function(d,m,f){var _=d.getUTCDay();return t.a(d,m,f)?o(_):n(_)},other:"P"};function u(d,m,f,_){var c=s[d];return typeof c=="function"?c(m,f,_):c}},function(l,i,e){var t=e(3),a={narrow:["aC","dC"],abbreviated:["a.C.","d.C."],wide:["avanti Cristo","dopo Cristo"]},r={narrow:["1","2","3","4"],abbreviated:["T1","T2","T3","T4"],wide:["1º trimestre","2º trimestre","3º trimestre","4º trimestre"]},o={narrow:["G","F","M","A","M","G","L","A","S","O","N","D"],abbreviated:["gen","feb","mar","apr","mag","giu","lug","ago","set","ott","nov","dic"],wide:["gennaio","febbraio","marzo","aprile","maggio","giugno","luglio","agosto","settembre","ottobre","novembre","dicembre"]},n={narrow:["D","L","M","M","G","V","S"],short:["dom","lun","mar","mer","gio","ven","sab"],abbreviated:["dom","lun","mar","mer","gio","ven","sab"],wide:["domenica","lunedì","martedì","mercoledì","giovedì","venerdì","sabato"]},s={narrow:{am:"m.",pm:"p.",midnight:"mezzanotte",noon:"mezzogiorno",morning:"mattina",afternoon:"pomeriggio",evening:"sera",night:"notte"},abbreviated:{am:"AM",pm:"PM",midnight:"mezzanotte",noon:"mezzogiorno",morning:"mattina",afternoon:"pomeriggio",evening:"sera",night:"notte"},wide:{am:"AM",pm:"PM",midnight:"mezzanotte",noon:"mezzogiorno",morning:"mattina",afternoon:"pomeriggio",evening:"sera",night:"notte"}},u={narrow:{am:"m.",pm:"p.",midnight:"mezzanotte",noon:"mezzogiorno",morning:"di mattina",afternoon:"del pomeriggio",evening:"di sera",night:"di notte"},abbreviated:{am:"AM",pm:"PM",midnight:"mezzanotte",noon:"mezzogiorno",morning:"di mattina",afternoon:"del pomeriggio",evening:"di sera",night:"di notte"},wide:{am:"AM",pm:"PM",midnight:"mezzanotte",noon:"mezzogiorno",morning:"di mattina",afternoon:"del pomeriggio",evening:"di sera",night:"di notte"}};function d(f){var _=Number(f);return _+"º"}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(º)?/i,o=/\d+/i,n={narrow:/^(aC|dC)/i,abbreviated:/^(a\.?\s?C\.?|a\.?\s?e\.?\s?v\.?|d\.?\s?C\.?|e\.?\s?v\.?)/i,wide:/^(avanti Cristo|avanti Era Volgare|dopo Cristo|Era Volgare)/i},s={any:[/^a/i,/^(d|e)/i]},u={narrow:/^[1234]/i,abbreviated:/^t[1234]/i,wide:/^[1234](º)? trimestre/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[gfmalsond]/i,abbreviated:/^(gen|feb|mar|apr|mag|giu|lug|ago|set|ott|nov|dic)/i,wide:/^(gennaio|febbraio|marzo|aprile|maggio|giugno|luglio|agosto|settembre|ottobre|novembre|dicembre)/i},f={narrow:[/^g/i,/^f/i,/^m/i,/^a/i,/^m/i,/^g/i,/^l/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ge/i,/^f/i,/^mar/i,/^ap/i,/^mag/i,/^gi/i,/^l/i,/^ag/i,/^s/i,/^o/i,/^n/i,/^d/i]},_={narrow:/^[dlmgvs]/i,short:/^(do|lu|ma|me|gi|ve|sa)/i,abbreviated:/^(dom|lun|mar|mer|gio|ven|sab)/i,wide:/^(domenica|luned[i|ì]|marted[i|ì]|mercoled[i|ì]|gioved[i|ì]|venerd[i|ì]|sabato)/i},c={narrow:[/^d/i,/^l/i,/^m/i,/^m/i,/^g/i,/^v/i,/^s/i],any:[/^d/i,/^l/i,/^ma/i,/^me/i,/^g/i,/^v/i,/^s/i]},v={narrow:/^(a|m\.|p|mezzanotte|mezzogiorno|(di|del) (mattina|pomeriggio|sera|notte))/i,any:/^([ap]\.?\s?m\.?|mezzanotte|mezzogiorno|(di|del) (mattina|pomeriggio|sera|notte))/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/^mezza/i,noon:/^mezzo/i,morning:/mattina/i,afternoon:/pomeriggio/i,evening:/sera/i,night:/notte/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(648),a=e(649),r=e(650),o=e(651),n=e(652),s={code:"ja",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"1秒未満",other:"{{count}}秒未満",oneWithSuffix:"約1秒",otherWithSuffix:"約{{count}}秒"},xSeconds:{one:"1秒",other:"{{count}}秒"},halfAMinute:"30秒",lessThanXMinutes:{one:"1分未満",other:"{{count}}分未満",oneWithSuffix:"約1分",otherWithSuffix:"約{{count}}分"},xMinutes:{one:"1分",other:"{{count}}分"},aboutXHours:{one:"約1時間",other:"約{{count}}時間"},xHours:{one:"1時間",other:"{{count}}時間"},xDays:{one:"1日",other:"{{count}}日"},aboutXWeeks:{one:"約1週間",other:"約{{count}}週間"},xWeeks:{one:"1週間",other:"{{count}}週間"},aboutXMonths:{one:"約1か月",other:"約{{count}}か月"},xMonths:{one:"1か月",other:"{{count}}か月"},aboutXYears:{one:"約1年",other:"約{{count}}年"},xYears:{one:"1年",other:"{{count}}年"},overXYears:{one:"1年以上",other:"{{count}}年以上"},almostXYears:{one:"1年近く",other:"{{count}}年近く"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?n.addSuffix&&t[r].oneWithSuffix?s=t[r].oneWithSuffix:s=t[r].one:n.addSuffix&&t[r].otherWithSuffix?s=t[r].otherWithSuffix.replace("{{count}}",o):s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?s+"後":s+"前":s}},function(l,i,e){var t=e(2),a={full:"y年M月d日EEEE",long:"y年M月d日",medium:"y/MM/dd",short:"y/MM/dd"},r={full:"H時mm分ss秒 zzzz",long:"H:mm:ss z",medium:"H:mm:ss",short:"H:mm"},o={full:"{{date}} {{time}}",long:"{{date}} {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"先週のeeeeのp",yesterday:"昨日のp",today:"今日のp",tomorrow:"明日のp",nextWeek:"翌週のeeeeのp",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["BC","AC"],abbreviated:["紀元前","西暦"],wide:["紀元前","西暦"]},r={narrow:["1","2","3","4"],abbreviated:["Q1","Q2","Q3","Q4"],wide:["第1四半期","第2四半期","第3四半期","第4四半期"]},o={narrow:["1","2","3","4","5","6","7","8","9","10","11","12"],abbreviated:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],wide:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"]},n={narrow:["日","月","火","水","木","金","土"],short:["日","月","火","水","木","金","土"],abbreviated:["日","月","火","水","木","金","土"],wide:["日曜日","月曜日","火曜日","水曜日","木曜日","金曜日","土曜日"]},s={narrow:{am:"午前",pm:"午後",midnight:"深夜",noon:"正午",morning:"朝",afternoon:"午後",evening:"夜",night:"深夜"},abbreviated:{am:"午前",pm:"午後",midnight:"深夜",noon:"正午",morning:"朝",afternoon:"午後",evening:"夜",night:"深夜"},wide:{am:"午前",pm:"午後",midnight:"深夜",noon:"正午",morning:"朝",afternoon:"午後",evening:"夜",night:"深夜"}},u={narrow:{am:"午前",pm:"午後",midnight:"深夜",noon:"正午",morning:"朝",afternoon:"午後",evening:"夜",night:"深夜"},abbreviated:{am:"午前",pm:"午後",midnight:"深夜",noon:"正午",morning:"朝",afternoon:"午後",evening:"夜",night:"深夜"},wide:{am:"午前",pm:"午後",midnight:"深夜",noon:"正午",morning:"朝",afternoon:"午後",evening:"夜",night:"深夜"}};function d(f,_){var c=Number(f),v=_||{},g=String(v.unit);return g==="date"?c+"日":c}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^第?\d+(日)?/i,o=/\d+/i,n={narrow:/^(B\.?C\.?|A\.?D\.?)/i,abbreviated:/^(紀元[前後]|西暦)/i,wide:/^(紀元[前後]|西暦)/i},s={narrow:[/^B/i,/^A/i],any:[/^(紀元前)/i,/^(西暦|紀元後)/i]},u={narrow:/^[1234]/i,abbreviated:/^Q[1234]/i,wide:/^第[1234一二三四1234]四半期/i},d={any:[/(1|一|1)/i,/(2|二|2)/i,/(3|三|3)/i,/(4|四|4)/i]},m={narrow:/^([123456789]|1[012])/,abbreviated:/^([123456789]|1[012])月/i,wide:/^([123456789]|1[012])月/i},f={any:[/^1\D/,/^2/,/^3/,/^4/,/^5/,/^6/,/^7/,/^8/,/^9/,/^10/,/^11/,/^12/]},_={narrow:/^[日月火水木金土]/,short:/^[日月火水木金土]/,abbreviated:/^[日月火水木金土]/,wide:/^[日月火水木金土]曜日/},c={any:[/^日/,/^月/,/^火/,/^水/,/^木/,/^金/,/^土/]},v={any:/^(AM|PM|午前|午後|正午|深夜|真夜中|夜|朝)/i},g={any:{am:/^(A|午前)/i,pm:/^(P|午後)/i,midnight:/^深夜|真夜中/i,noon:/^正午/i,morning:/^朝/i,afternoon:/^午後/i,evening:/^夜/i,night:/^深夜/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(654),a=e(655),r=e(656),o=e(657),n=e(658),s={code:"ka",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{past:"{{count}} წამზე ნაკლები ხნის წინ",present:"{{count}} წამზე ნაკლები",future:"{{count}} წამზე ნაკლებში"},xSeconds:{past:"{{count}} წამის წინ",present:"{{count}} წამი",future:"{{count}} წამში"},halfAMinute:{past:"ნახევარი წუთის წინ",present:"ნახევარი წუთი",future:"ნახევარი წუთში"},lessThanXMinutes:{past:"{{count}} წუთზე ნაკლები ხნის წინ",present:"{{count}} წუთზე ნაკლები",future:"{{count}} წუთზე ნაკლებში"},xMinutes:{past:"{{count}} წუთის წინ",present:"{{count}} წუთი",future:"{{count}} წუთში"},aboutXHours:{past:"დაახლოებით {{count}} საათის წინ",present:"დაახლოებით {{count}} საათი",future:"დაახლოებით {{count}} საათში"},xHours:{past:"{{count}} საათის წინ",present:"{{count}} საათი",future:"{{count}} საათში"},xDays:{past:"{{count}} დღის წინ",present:"{{count}} დღე",future:"{{count}} დღეში"},aboutXWeeks:{past:"დაახლოებით {{count}} კვირას წინ",present:"დაახლოებით {{count}} კვირა",future:"დაახლოებით {{count}} კვირაში"},xWeeks:{past:"{{count}} კვირას კვირა",present:"{{count}} კვირა",future:"{{count}} კვირაში"},aboutXMonths:{past:"დაახლოებით {{count}} თვის წინ",present:"დაახლოებით {{count}} თვე",future:"დაახლოებით {{count}} თვეში"},xMonths:{past:"{{count}} თვის წინ",present:"{{count}} თვე",future:"{{count}} თვეში"},aboutXYears:{past:"დაახლოებით {{count}} წლის წინ",present:"დაახლოებით {{count}} წელი",future:"დაახლოებით {{count}} წელში"},xYears:{past:"{{count}} წლის წინ",present:"{{count}} წელი",future:"{{count}} წელში"},overXYears:{past:"{{count}} წელზე მეტი ხნის წინ",present:"{{count}} წელზე მეტი",future:"{{count}} წელზე მეტი ხნის შემდეგ"},almostXYears:{past:"თითქმის {{count}} წლის წინ",present:"თითქმის {{count}} წელი",future:"თითქმის {{count}} წელში"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:n.addSuffix&&n.comparison>0?s=t[r].future.replace("{{count}}",o):n.addSuffix&&n.comparison<=0?s=t[r].past.replace("{{count}}",o):s=t[r].present.replace("{{count}}",o),s}},function(l,i,e){var t=e(2),a={full:"EEEE, do MMMM, y",long:"do, MMMM, y",medium:"d, MMM, y",short:"dd/MM/yyyy"},r={full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},o={full:"{{date}} {{time}}'-ზე'",long:"{{date}} {{time}}'-ზე'",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'წინა' eeee LT'-ზე'",yesterday:"'გუშინ' LT'-ზე'",today:"'დღეს' LT'-ზე'",tomorrow:"'ხვალ' LT'-ზე'",nextWeek:"'შემდეგი' eeee LT'-ზე'",other:"L"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["ჩ.წ-მდე","ჩ.წ"],abbreviated:["ჩვ.წ-მდე","ჩვ.წ"],wide:["ჩვენს წელთაღრიცხვამდე","ჩვენი წელთაღრიცხვით"]},r={narrow:["1","2","3","4"],abbreviated:["1-ლი კვ","2-ე კვ","3-ე კვ","4-ე კვ"],wide:["1-ლი კვარტალი","2-ე კვარტალი","3-ე კვარტალი","4-ე კვარტალი"]},o={narrow:["ია","თე","მა","აპ","მს","ვნ","ვლ","აგ","სე","ოქ","ნო","დე"],abbreviated:["იან","თებ","მარ","აპრ","მაი","ივნ","ივლ","აგვ","სექ","ოქტ","ნოე","დეკ"],wide:["იანვარი","თებერვალი","მარტი","აპრილი","მაისი","ივნისი","ივლისი","აგვისტო","სექტემბერი","ოქტომბერი","ნოემბერი","დეკემბერი"]},n={narrow:["კვ","ორ","სა","ოთ","ხუ","პა","შა"],short:["კვი","ორშ","სამ","ოთხ","ხუთ","პარ","შაბ"],abbreviated:["კვი","ორშ","სამ","ოთხ","ხუთ","პარ","შაბ"],wide:["კვირა","ორშაბათი","სამშაბათი","ოთხშაბათი","ხუთშაბათი","პარასკევი","შაბათი"]},s={narrow:{am:"a",pm:"p",midnight:"შუაღამე",noon:"შუადღე",morning:"დილა",afternoon:"საღამო",evening:"საღამო",night:"ღამე"},abbreviated:{am:"AM",pm:"PM",midnight:"შუაღამე",noon:"შუადღე",morning:"დილა",afternoon:"საღამო",evening:"საღამო",night:"ღამე"},wide:{am:"a.m.",pm:"p.m.",midnight:"შუაღამე",noon:"შუადღე",morning:"დილა",afternoon:"საღამო",evening:"საღამო",night:"ღამე"}},u={narrow:{am:"a",pm:"p",midnight:"შუაღამით",noon:"შუადღისას",morning:"დილით",afternoon:"ნაშუადღევს",evening:"საღამოს",night:"ღამით"},abbreviated:{am:"AM",pm:"PM",midnight:"შუაღამით",noon:"შუადღისას",morning:"დილით",afternoon:"ნაშუადღევს",evening:"საღამოს",night:"ღამით"},wide:{am:"a.m.",pm:"p.m.",midnight:"შუაღამით",noon:"შუადღისას",morning:"დილით",afternoon:"ნაშუადღევს",evening:"საღამოს",night:"ღამით"}};function d(f,_){var c=Number(f);return c===1?c+"-ლი":c+"-ე"}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(-ლი|-ე)?/i,o=/\d+/i,n={narrow:/^(ჩვ?\.წ)/i,abbreviated:/^(ჩვ?\.წ)/i,wide:/^(ჩვენს წელთაღრიცხვამდე|ქრისტეშობამდე|ჩვენი წელთაღრიცხვით|ქრისტეშობიდან)/i},s={any:[/^(ჩვენს წელთაღრიცხვამდე|ქრისტეშობამდე)/i,/^(ჩვენი წელთაღრიცხვით|ქრისტეშობიდან)/i]},u={narrow:/^[1234]/i,abbreviated:/^[1234]-(ლი|ე)? კვ/i,wide:/^[1234]-(ლი|ე)? კვარტალი/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={any:/^(ია|თე|მა|აპ|მს|ვნ|ვლ|აგ|სე|ოქ|ნო|დე)/i},f={any:[/^ია/i,/^თ/i,/^მარ/i,/^აპ/i,/^მაი/i,/^ი?ვნ/i,/^ი?ვლ/i,/^აგ/i,/^ს/i,/^ო/i,/^ნ/i,/^დ/i]},_={narrow:/^(კვ|ორ|სა|ოთ|ხუ|პა|შა)/i,short:/^(კვი|ორშ|სამ|ოთხ|ხუთ|პარ|შაბ)/i,long:/^(კვირა|ორშაბათი|სამშაბათი|ოთხშაბათი|ხუთშაბათი|პარასკევი|შაბათი)/i},c={any:[/^კვ/i,/^ორ/i,/^სა/i,/^ოთ/i,/^ხუ/i,/^პა/i,/^შა/i]},v={any:/^([ap]\.?\s?m\.?|შუაღ|დილ)/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/^შუაღ/i,noon:/^შუადღ/i,morning:/^დილ/i,afternoon:/ნაშუადღევს/i,evening:/საღამო/i,night:/ღამ/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(660),a=e(661),r=e(662),o=e(663),n=e(664),s={code:"kk",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=o;function t(n,s){if(n.one!==void 0&&s===1)return n.one;var u=s%10,d=s%100;return u===1&&d!==11?n.singularNominative.replace("{{count}}",s):u>=2&&u<=4&&(d<10||d>20)?n.singularGenitive.replace("{{count}}",s):n.pluralGenitive.replace("{{count}}",s)}function a(n){return function(s,u){return u.addSuffix?u.comparison>0?n.future?t(n.future,s):t(n.regular,s)+" кейін":n.past?t(n.past,s):t(n.regular,s)+" бұрын":t(n.regular,s)}}var r={lessThanXSeconds:a({regular:{one:"1 секундтан аз",singularNominative:"{{count}} секундтан аз",singularGenitive:"{{count}} секундтан аз",pluralGenitive:"{{count}} секундтан аз"},future:{one:"бір секундтан кейін",singularNominative:"{{count}} секундтан кейін",singularGenitive:"{{count}} секундтан кейін",pluralGenitive:"{{count}} секундтан кейін"}}),xSeconds:a({regular:{singularNominative:"{{count}} секунд",singularGenitive:"{{count}} секунд",pluralGenitive:"{{count}} секунд"},past:{singularNominative:"{{count}} секунд бұрын",singularGenitive:"{{count}} секунд бұрын",pluralGenitive:"{{count}} секунд бұрын"},future:{singularNominative:"{{count}} секундтан кейін",singularGenitive:"{{count}} секундтан кейін",pluralGenitive:"{{count}} секундтан кейін"}}),halfAMinute:function(n,s){return s.addSuffix?s.comparison>0?"жарты минут ішінде":"жарты минут бұрын":"жарты минут"},lessThanXMinutes:a({regular:{one:"1 минуттан аз",singularNominative:"{{count}} минуттан аз",singularGenitive:"{{count}} минуттан аз",pluralGenitive:"{{count}} минуттан аз"},future:{one:"минуттан кем ",singularNominative:"{{count}} минуттан кем",singularGenitive:"{{count}} минуттан кем",pluralGenitive:"{{count}} минуттан кем"}}),xMinutes:a({regular:{singularNominative:"{{count}} минут",singularGenitive:"{{count}} минут",pluralGenitive:"{{count}} минут"},past:{singularNominative:"{{count}} минут бұрын",singularGenitive:"{{count}} минут бұрын",pluralGenitive:"{{count}} минут бұрын"},future:{singularNominative:"{{count}} минуттан кейін",singularGenitive:"{{count}} минуттан кейін",pluralGenitive:"{{count}} минуттан кейін"}}),aboutXHours:a({regular:{singularNominative:"шамамен {{count}} сағат",singularGenitive:"шамамен {{count}} сағат",pluralGenitive:"шамамен {{count}} сағат"},future:{singularNominative:"шамамен {{count}} сағаттан кейін",singularGenitive:"шамамен {{count}} сағаттан кейін",pluralGenitive:"шамамен {{count}} сағаттан кейін"}}),xHours:a({regular:{singularNominative:"{{count}} сағат",singularGenitive:"{{count}} сағат",pluralGenitive:"{{count}} сағат"}}),xDays:a({regular:{singularNominative:"{{count}} күн",singularGenitive:"{{count}} күн",pluralGenitive:"{{count}} күн"},future:{singularNominative:"{{count}} күннен кейін",singularGenitive:"{{count}} күннен кейін",pluralGenitive:"{{count}} күннен кейін"}}),aboutXWeeks:{one:"шамамен 1 апта",other:"шамамен {{count}} апта"},xWeeks:{one:"1 апта",other:"{{count}} апта"},aboutXMonths:a({regular:{singularNominative:"шамамен {{count}} ай",singularGenitive:"шамамен {{count}} ай",pluralGenitive:"шамамен {{count}} ай"},future:{singularNominative:"шамамен {{count}} айдан кейін",singularGenitive:"шамамен {{count}} айдан кейін",pluralGenitive:"шамамен {{count}} айдан кейін"}}),xMonths:a({regular:{singularNominative:"{{count}} ай",singularGenitive:"{{count}} ай",pluralGenitive:"{{count}} ай"}}),aboutXYears:a({regular:{singularNominative:"шамамен {{count}} жыл",singularGenitive:"шамамен {{count}} жыл",pluralGenitive:"шамамен {{count}} жыл"},future:{singularNominative:"шамамен {{count}} жылдан кейін",singularGenitive:"шамамен {{count}} жылдан кейін",pluralGenitive:"шамамен {{count}} жылдан кейін"}}),xYears:a({regular:{singularNominative:"{{count}} жыл",singularGenitive:"{{count}} жыл",pluralGenitive:"{{count}} жыл"},future:{singularNominative:"{{count}} жылдан кейін",singularGenitive:"{{count}} жылдан кейін",pluralGenitive:"{{count}} жылдан кейін"}}),overXYears:a({regular:{singularNominative:"{{count}} жылдан астам",singularGenitive:"{{count}} жылдан астам",pluralGenitive:"{{count}} жылдан астам"},future:{singularNominative:"{{count}} жылдан астам",singularGenitive:"{{count}} жылдан астам",pluralGenitive:"{{count}} жылдан астам"}}),almostXYears:a({regular:{singularNominative:"{{count}} жылға жақын",singularGenitive:"{{count}} жылға жақын",pluralGenitive:"{{count}} жылға жақын"},future:{singularNominative:"{{count}} жылдан кейін",singularGenitive:"{{count}} жылдан кейін",pluralGenitive:"{{count}} жылдан кейін"}})};function o(n,s,u){return u=u||{},r[n](s,u)}},function(l,i,e){var t=e(2),a={full:"EEEE, do MMMM y 'ж.'",long:"do MMMM y 'ж.'",medium:"d MMM y 'ж.'",short:"dd.MM.yyyy"},r={full:"H:mm:ss zzzz",long:"H:mm:ss z",medium:"H:mm:ss",short:"H:mm"},o={any:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"any"})};i.a=n},function(l,i,e){i.a=u;var t=e(13),a=["жексенбіде","дүйсенбіде","сейсенбіде","сәрсенбіде","бейсенбіде","жұмада","сенбіде"];function r(d){var m=a[d];return"'өткен "+m+" сағат' p'-де'"}function o(d){var m=a[d];return"'"+m+" сағат' p'-де'"}function n(d){var m=a[d];return"'келесі "+m+" сағат' p'-де'"}var s={lastWeek:function(d,m,f){var _=d.getUTCDay();return t.a(d,m,f)?o(_):r(_)},yesterday:"'кеше сағат' p'-де'",today:"'бүгін сағат' p'-де'",tomorrow:"'ертең сағат' p'-де'",nextWeek:function(d,m,f){var _=d.getUTCDay();return t.a(d,m,f)?o(_):n(_)},other:"P"};function u(d,m,f,_){var c=s[d];return typeof c=="function"?c(m,f,_):c}},function(l,i,e){var t=e(3),a={narrow:["б.з.д.","б.з."],abbreviated:["б.з.д.","б.з."],wide:["біздің заманымызға дейін","біздің заманымыз"]},r={narrow:["1","2","3","4"],abbreviated:["1-ші тоқ.","2-ші тоқ.","3-ші тоқ.","4-ші тоқ."],wide:["1-ші тоқсан","2-ші тоқсан","3-ші тоқсан","4-ші тоқсан"]},o={narrow:["Қ","А","Н","С","М","М","Ш","Т","Қ","Қ","Қ","Ж"],abbreviated:["қаң","ақп","нау","сәу","мам","мау","шіл","там","қыр","қаз","қар","жел"],wide:["қаңтар","ақпан","наурыз","сәуір","мамыр","маусым","шілде","тамыз","қыркүйек","қазан","қараша","желтоқсан"]},n={narrow:["Қ","А","Н","С","М","М","Ш","Т","Қ","Қ","Қ","Ж"],abbreviated:["қаң","ақп","нау","сәу","мам","мау","шіл","там","қыр","қаз","қар","жел"],wide:["қаңтар","ақпан","наурыз","сәуір","мамыр","маусым","шілде","тамыз","қыркүйек","қазан","қараша","желтоқсан"]},s={narrow:["Ж","Д","С","С","Б","Ж","С"],short:["жс","дс","сс","ср","бс","жм","сб"],abbreviated:["жс","дс","сс","ср","бс","жм","сб"],wide:["жексенбі","дүйсенбі","сейсенбі","сәрсенбі","бейсенбі","жұма","сенбі"]},u={narrow:{am:"ТД",pm:"ТК",midnight:"түн ортасы",noon:"түс",morning:"таң",afternoon:"күндіз",evening:"кеш",night:"түн"},wide:{am:"ТД",pm:"ТК",midnight:"түн ортасы",noon:"түс",morning:"таң",afternoon:"күндіз",evening:"кеш",night:"түн"}},d={narrow:{am:"ТД",pm:"ТК",midnight:"түн ортасында",noon:"түс",morning:"таң",afternoon:"күн",evening:"кеш",night:"түн"},wide:{am:"ТД",pm:"ТК",midnight:"түн ортасында",noon:"түсте",morning:"таңертең",afternoon:"күндіз",evening:"кеште",night:"түнде"}};function m(_,c){var v=c||{},g=String(v.unit),P;return P="-ші",_+P}var f={ordinalNumber:m,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(_){return Number(_)-1}}),month:t.a({values:o,defaultWidth:"wide",formattingValues:n,defaultFormattingWidth:"wide"}),day:t.a({values:s,defaultWidth:"wide"}),dayPeriod:t.a({values:u,defaultWidth:"any",formattingValues:d,defaultFormattingWidth:"wide"})};i.a=f},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(-?(ші|шы))?/i,o=/\d+/i,n={narrow:/^((б )?з\.?\s?д\.?)/i,abbreviated:/^((б )?з\.?\s?д\.?)/i,wide:/^(біздің заманымызға дейін|біздің заманымыз|біздің заманымыздан)/i},s={any:[/^б/i,/^з/i]},u={narrow:/^[1234]/i,abbreviated:/^[1234](-?ші)? тоқ.?/i,wide:/^[1234](-?ші)? тоқсан/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^(қ|а|н|с|м|мау|ш|т|қыр|қаз|қар|ж)/i,abbreviated:/^(қаң|ақп|нау|сәу|мам|мау|шіл|там|қыр|қаз|қар|жел)/i,wide:/^(қаңтар|ақпан|наурыз|сәуір|мамыр|маусым|шілде|тамыз|қыркүйек|қазан|қараша|желтоқсан)/i},f={narrow:[/^қ/i,/^а/i,/^н/i,/^с/i,/^м/i,/^м/i,/^ш/i,/^т/i,/^қ/i,/^қ/i,/^қ/i,/^ж/i],abbreviated:[/^қаң/i,/^ақп/i,/^нау/i,/^сәу/i,/^мам/i,/^мау/i,/^шіл/i,/^там/i,/^қыр/i,/^қаз/i,/^қар/i,/^жел/i],any:[/^қ/i,/^а/i,/^н/i,/^с/i,/^м/i,/^м/i,/^ш/i,/^т/i,/^қ/i,/^қ/i,/^қ/i,/^ж/i]},_={narrow:/^(ж|д|с|с|б|ж|с)/i,short:/^(жс|дс|сс|ср|бс|жм|сб)/i,wide:/^(жексенбі|дүйсенбі|сейсенбі|сәрсенбі|бейсенбі|жұма|сенбі)/i},c={narrow:[/^ж/i,/^д/i,/^с/i,/^с/i,/^б/i,/^ж/i,/^с/i],short:[/^жс/i,/^дс/i,/^сс/i,/^ср/i,/^бс/i,/^жм/i,/^сб/i],any:[/^ж[ек]/i,/^д[үй]/i,/^сe[й]/i,/^сә[р]/i,/^б[ей]/i,/^ж[ұм]/i,/^се[н]/i]},v={narrow:/^Т\.?\s?[ДК]\.?|түн ортасында|((түсте|таңертең|таңда|таңертең|таңмен|таң|күндіз|күн|кеште|кеш|түнде|түн)\.?)/i,wide:/^Т\.?\s?[ДК]\.?|түн ортасында|((түсте|таңертең|таңда|таңертең|таңмен|таң|күндіз|күн|кеште|кеш|түнде|түн)\.?)/i,any:/^Т\.?\s?[ДК]\.?|түн ортасында|((түсте|таңертең|таңда|таңертең|таңмен|таң|күндіз|күн|кеште|кеш|түнде|түн)\.?)/i},g={any:{am:/^ТД/i,pm:/^ТК/i,midnight:/^түн орта/i,noon:/^күндіз/i,morning:/таң/i,afternoon:/түс/i,evening:/кеш/i,night:/түн/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"wide",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(666),a=e(667),r=e(668),o=e(669),n=e(670),s={code:"kn",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=r;var t={lessThanXSeconds:{one:{default:"1 ಸೆಕೆಂಡ್‌ಗಿಂತ ಕಡಿಮೆ",future:"1 ಸೆಕೆಂಡ್‌ಗಿಂತ ಕಡಿಮೆ",past:"1 ಸೆಕೆಂಡ್‌ಗಿಂತ ಕಡಿಮೆ"},other:{default:"{{count}} ಸೆಕೆಂಡ್‌ಗಿಂತ ಕಡಿಮೆ",future:"{{count}} ಸೆಕೆಂಡ್‌ಗಿಂತ ಕಡಿಮೆ",past:"{{count}} ಸೆಕೆಂಡ್‌ಗಿಂತ ಕಡಿಮೆ"}},xSeconds:{one:{default:"1 ಸೆಕೆಂಡ್",future:"1 ಸೆಕೆಂಡ್‌ನಲ್ಲಿ",past:"1 ಸೆಕೆಂಡ್ ಹಿಂದೆ"},other:{default:"{{count}} ಸೆಕೆಂಡುಗಳು",future:"{{count}} ಸೆಕೆಂಡ್‌ಗಳಲ್ಲಿ",past:"{{count}} ಸೆಕೆಂಡ್ ಹಿಂದೆ"}},halfAMinute:{other:{default:"ಅರ್ಧ ನಿಮಿಷ",future:"ಅರ್ಧ ನಿಮಿಷದಲ್ಲಿ",past:"ಅರ್ಧ ನಿಮಿಷದ ಹಿಂದೆ"}},lessThanXMinutes:{one:{default:"1 ನಿಮಿಷಕ್ಕಿಂತ ಕಡಿಮೆ",future:"1 ನಿಮಿಷಕ್ಕಿಂತ ಕಡಿಮೆ",past:"1 ನಿಮಿಷಕ್ಕಿಂತ ಕಡಿಮೆ"},other:{default:"{{count}} ನಿಮಿಷಕ್ಕಿಂತ ಕಡಿಮೆ",future:"{{count}} ನಿಮಿಷಕ್ಕಿಂತ ಕಡಿಮೆ",past:"{{count}} ನಿಮಿಷಕ್ಕಿಂತ ಕಡಿಮೆ"}},xMinutes:{one:{default:"1 ನಿಮಿಷ",future:"1 ನಿಮಿಷದಲ್ಲಿ",past:"1 ನಿಮಿಷದ ಹಿಂದೆ"},other:{default:"{{count}} ನಿಮಿಷಗಳು",future:"{{count}} ನಿಮಿಷಗಳಲ್ಲಿ",past:"{{count}} ನಿಮಿಷಗಳ ಹಿಂದೆ"}},aboutXHours:{one:{default:"ಸುಮಾರು 1 ಗಂಟೆ",future:"ಸುಮಾರು 1 ಗಂಟೆಯಲ್ಲಿ",past:"ಸುಮಾರು 1 ಗಂಟೆ ಹಿಂದೆ"},other:{default:"ಸುಮಾರು {{count}} ಗಂಟೆಗಳು",future:"ಸುಮಾರು {{count}} ಗಂಟೆಗಳಲ್ಲಿ",past:"ಸುಮಾರು {{count}} ಗಂಟೆಗಳ ಹಿಂದೆ"}},xHours:{one:{default:"1 ಗಂಟೆ",future:"1 ಗಂಟೆಯಲ್ಲಿ",past:"1 ಗಂಟೆ ಹಿಂದೆ"},other:{default:"{{count}} ಗಂಟೆಗಳು",future:"{{count}} ಗಂಟೆಗಳಲ್ಲಿ",past:"{{count}} ಗಂಟೆಗಳ ಹಿಂದೆ"}},xDays:{one:{default:"1 ದಿನ",future:"1 ದಿನದಲ್ಲಿ",past:"1 ದಿನದ ಹಿಂದೆ"},other:{default:"{{count}} ದಿನಗಳು",future:"{{count}} ದಿನಗಳಲ್ಲಿ",past:"{{count}} ದಿನಗಳ ಹಿಂದೆ"}},aboutXMonths:{one:{default:"ಸುಮಾರು 1 ತಿಂಗಳು",future:"ಸುಮಾರು 1 ತಿಂಗಳಲ್ಲಿ",past:"ಸುಮಾರು 1 ತಿಂಗಳ ಹಿಂದೆ"},other:{default:"ಸುಮಾರು {{count}} ತಿಂಗಳು",future:"ಸುಮಾರು {{count}} ತಿಂಗಳುಗಳಲ್ಲಿ",past:"ಸುಮಾರು {{count}} ತಿಂಗಳುಗಳ ಹಿಂದೆ"}},xMonths:{one:{default:"1 ತಿಂಗಳು",future:"1 ತಿಂಗಳಲ್ಲಿ",past:"1 ತಿಂಗಳ ಹಿಂದೆ"},other:{default:"{{count}} ತಿಂಗಳು",future:"{{count}} ತಿಂಗಳುಗಳಲ್ಲಿ",past:"{{count}} ತಿಂಗಳುಗಳ ಹಿಂದೆ"}},aboutXYears:{one:{default:"ಸುಮಾರು 1 ವರ್ಷ",future:"ಸುಮಾರು 1 ವರ್ಷದಲ್ಲಿ",past:"ಸುಮಾರು 1 ವರ್ಷದ ಹಿಂದೆ"},other:{default:"ಸುಮಾರು {{count}} ವರ್ಷಗಳು",future:"ಸುಮಾರು {{count}} ವರ್ಷಗಳಲ್ಲಿ",past:"ಸುಮಾರು {{count}} ವರ್ಷಗಳ ಹಿಂದೆ"}},xYears:{one:{default:"1 ವರ್ಷ",future:"1 ವರ್ಷದಲ್ಲಿ",past:"1 ವರ್ಷದ ಹಿಂದೆ"},other:{default:"{{count}} ವರ್ಷಗಳು",future:"{{count}} ವರ್ಷಗಳಲ್ಲಿ",past:"{{count}} ವರ್ಷಗಳ ಹಿಂದೆ"}},overXYears:{one:{default:"1 ವರ್ಷದ ಮೇಲೆ",future:"1 ವರ್ಷದ ಮೇಲೆ",past:"1 ವರ್ಷದ ಮೇಲೆ"},other:{default:"{{count}} ವರ್ಷಗಳ ಮೇಲೆ",future:"{{count}} ವರ್ಷಗಳ ಮೇಲೆ",past:"{{count}} ವರ್ಷಗಳ ಮೇಲೆ"}},almostXYears:{one:{default:"ಬಹುತೇಕ 1 ವರ್ಷದಲ್ಲಿ",future:"ಬಹುತೇಕ 1 ವರ್ಷದಲ್ಲಿ",past:"ಬಹುತೇಕ 1 ವರ್ಷದಲ್ಲಿ"},other:{default:"ಬಹುತೇಕ {{count}} ವರ್ಷಗಳಲ್ಲಿ",future:"ಬಹುತೇಕ {{count}} ವರ್ಷಗಳಲ್ಲಿ",past:"ಬಹುತೇಕ {{count}} ವರ್ಷಗಳಲ್ಲಿ"}}};function a(o,n){return n.addSuffix?n.comparison>0?o.future:o.past:o.default}function r(o,n,s){s=s||{};var u;return typeof t[o]=="string"?u=t[o]:n===1?u=a(t[o].one,s):u=a(t[o].other,s),u.replace("{{count}}",n)}},function(l,i,e){var t=e(2),a={full:"EEEE, MMMM d, y",long:"MMMM d, y",medium:"MMM d, y",short:"d/M/yy"},r={full:"hh:mm:ss a zzzz",long:"hh:mm:ss a z",medium:"hh:mm:ss a",short:"hh:mm a"},o={full:"{{date}} {{time}}",long:"{{date}} {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'ಕಳೆದ' eeee p 'ಕ್ಕೆ'",yesterday:"'ನಿನ್ನೆ' p 'ಕ್ಕೆ'",today:"'ಇಂದು' p 'ಕ್ಕೆ'",tomorrow:"'ನಾಳೆ' p 'ಕ್ಕೆ'",nextWeek:"eeee p 'ಕ್ಕೆ'",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["ಕ್ರಿ.ಪೂ","ಕ್ರಿ.ಶ"],abbreviated:["ಕ್ರಿ.ಪೂ","ಕ್ರಿ.ಶ"],wide:["ಕ್ರಿಸ್ತ ಪೂರ್ವ","ಕ್ರಿಸ್ತ ಶಕ"]},r={narrow:["1","2","3","4"],abbreviated:["ತ್ರೈ 1","ತ್ರೈ 2","ತ್ರೈ 3","ತ್ರೈ 4"],wide:["1ನೇ ತ್ರೈಮಾಸಿಕ","2ನೇ ತ್ರೈಮಾಸಿಕ","3ನೇ ತ್ರೈಮಾಸಿಕ","4ನೇ ತ್ರೈಮಾಸಿಕ"]},o={narrow:["ಜ","ಫೆ","ಮಾ","ಏ","ಮೇ","ಜೂ","ಜು","ಆ","ಸೆ","ಅ","ನ","ಡಿ"],abbreviated:["ಜನ","ಫೆಬ್ರ","ಮಾರ್ಚ್","ಏಪ್ರಿ","ಮೇ","ಜೂನ್","ಜುಲೈ","ಆಗ","ಸೆಪ್ಟೆಂ","ಅಕ್ಟೋ","ನವೆಂ","ಡಿಸೆಂ"],wide:["ಜನವರಿ","ಫೆಬ್ರವರಿ","ಮಾರ್ಚ್","ಏಪ್ರಿಲ್","ಮೇ","ಜೂನ್","ಜುಲೈ","ಆಗಸ್ಟ್","ಸೆಪ್ಟೆಂಬರ್","ಅಕ್ಟೋಬರ್","ನವೆಂಬರ್","ಡಿಸೆಂಬರ್"]},n={narrow:["ಭಾ","ಸೋ","ಮಂ","ಬು","ಗು","ಶು","ಶ"],short:["ಭಾನು","ಸೋಮ","ಮಂಗಳ","ಬುಧ","ಗುರು","ಶುಕ್ರ","ಶನಿ"],abbreviated:["ಭಾನು","ಸೋಮ","ಮಂಗಳ","ಬುಧ","ಗುರು","ಶುಕ್ರ","ಶನಿ"],wide:["ಭಾನುವಾರ","ಸೋಮವಾರ","ಮಂಗಳವಾರ","ಬುಧವಾರ","ಗುರುವಾರ","ಶುಕ್ರವಾರ","ಶನಿವಾರ"]},s={narrow:{am:"ಪೂರ್ವಾಹ್ನ",pm:"ಅಪರಾಹ್ನ",midnight:"ಮಧ್ಯರಾತ್ರಿ",noon:"ಮಧ್ಯಾಹ್ನ",morning:"ಬೆಳಗ್ಗೆ",afternoon:"ಮಧ್ಯಾಹ್ನ",evening:"ಸಂಜೆ",night:"ರಾತ್ರಿ"},abbreviated:{am:"ಪೂರ್ವಾಹ್ನ",pm:"ಅಪರಾಹ್ನ",midnight:"ಮಧ್ಯರಾತ್ರಿ",noon:"ಮಧ್ಯಾನ್ಹ",morning:"ಬೆಳಗ್ಗೆ",afternoon:"ಮಧ್ಯಾನ್ಹ",evening:"ಸಂಜೆ",night:"ರಾತ್ರಿ"},wide:{am:"ಪೂರ್ವಾಹ್ನ",pm:"ಅಪರಾಹ್ನ",midnight:"ಮಧ್ಯರಾತ್ರಿ",noon:"ಮಧ್ಯಾನ್ಹ",morning:"ಬೆಳಗ್ಗೆ",afternoon:"ಮಧ್ಯಾನ್ಹ",evening:"ಸಂಜೆ",night:"ರಾತ್ರಿ"}},u={narrow:{am:"ಪೂ",pm:"ಅ",midnight:"ಮಧ್ಯರಾತ್ರಿ",noon:"ಮಧ್ಯಾನ್ಹ",morning:"ಬೆಳಗ್ಗೆ",afternoon:"ಮಧ್ಯಾನ್ಹ",evening:"ಸಂಜೆ",night:"ರಾತ್ರಿ"},abbreviated:{am:"ಪೂರ್ವಾಹ್ನ",pm:"ಅಪರಾಹ್ನ",midnight:"ಮಧ್ಯ ರಾತ್ರಿ",noon:"ಮಧ್ಯಾನ್ಹ",morning:"ಬೆಳಗ್ಗೆ",afternoon:"ಮಧ್ಯಾನ್ಹ",evening:"ಸಂಜೆ",night:"ರಾತ್ರಿ"},wide:{am:"ಪೂರ್ವಾಹ್ನ",pm:"ಅಪರಾಹ್ನ",midnight:"ಮಧ್ಯ ರಾತ್ರಿ",noon:"ಮಧ್ಯಾನ್ಹ",morning:"ಬೆಳಗ್ಗೆ",afternoon:"ಮಧ್ಯಾನ್ಹ",evening:"ಸಂಜೆ",night:"ರಾತ್ರಿ"}};function d(f,_){var c=Number(f);return c+"ನೇ"}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(ನೇ|ನೆ)?/i,o=/\d+/i,n={narrow:/^(ಕ್ರಿ.ಪೂ|ಕ್ರಿ.ಶ)/i,abbreviated:/^(ಕ್ರಿ\.?\s?ಪೂ\.?|ಕ್ರಿ\.?\s?ಶ\.?|ಪ್ರ\.?\s?ಶ\.?)/i,wide:/^(ಕ್ರಿಸ್ತ ಪೂರ್ವ|ಕ್ರಿಸ್ತ ಶಕ|ಪ್ರಸಕ್ತ ಶಕ)/i},s={any:[/^ಪೂ/i,/^(ಶ|ಪ್ರ)/i]},u={narrow:/^[1234]/i,abbreviated:/^ತ್ರೈ[1234]|ತ್ರೈ [1234]| [1234]ತ್ರೈ/i,wide:/^[1234](ನೇ)? ತ್ರೈಮಾಸಿಕ/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^(ಜೂ|ಜು|ಜ|ಫೆ|ಮಾ|ಏ|ಮೇ|ಆ|ಸೆ|ಅ|ನ|ಡಿ)/i,abbreviated:/^(ಜನ|ಫೆಬ್ರ|ಮಾರ್ಚ್|ಏಪ್ರಿ|ಮೇ|ಜೂನ್|ಜುಲೈ|ಆಗ|ಸೆಪ್ಟೆಂ|ಅಕ್ಟೋ|ನವೆಂ|ಡಿಸೆಂ)/i,wide:/^(ಜನವರಿ|ಫೆಬ್ರವರಿ|ಮಾರ್ಚ್|ಏಪ್ರಿಲ್|ಮೇ|ಜೂನ್|ಜುಲೈ|ಆಗಸ್ಟ್|ಸೆಪ್ಟೆಂಬರ್|ಅಕ್ಟೋಬರ್|ನವೆಂಬರ್|ಡಿಸೆಂಬರ್)/i},f={narrow:[/^ಜ$/i,/^ಫೆ/i,/^ಮಾ/i,/^ಏ/i,/^ಮೇ/i,/^ಜೂ/i,/^ಜು$/i,/^ಆ/i,/^ಸೆ/i,/^ಅ/i,/^ನ/i,/^ಡಿ/i],any:[/^ಜನ/i,/^ಫೆ/i,/^ಮಾ/i,/^ಏ/i,/^ಮೇ/i,/^ಜೂನ್/i,/^ಜುಲೈ/i,/^ಆ/i,/^ಸೆ/i,/^ಅ/i,/^ನ/i,/^ಡಿ/i]},_={narrow:/^(ಭಾ|ಸೋ|ಮ|ಬು|ಗು|ಶು|ಶ)/i,short:/^(ಭಾನು|ಸೋಮ|ಮಂಗಳ|ಬುಧ|ಗುರು|ಶುಕ್ರ|ಶನಿ)/i,abbreviated:/^(ಭಾನು|ಸೋಮ|ಮಂಗಳ|ಬುಧ|ಗುರು|ಶುಕ್ರ|ಶನಿ)/i,wide:/^(ಭಾನುವಾರ|ಸೋಮವಾರ|ಮಂಗಳವಾರ|ಬುಧವಾರ|ಗುರುವಾರ|ಶುಕ್ರವಾರ|ಶನಿವಾರ)/i},c={narrow:[/^ಭಾ/i,/^ಸೋ/i,/^ಮ/i,/^ಬು/i,/^ಗು/i,/^ಶು/i,/^ಶ/i],any:[/^ಭಾ/i,/^ಸೋ/i,/^ಮ/i,/^ಬು/i,/^ಗು/i,/^ಶು/i,/^ಶ/i]},v={narrow:/^(ಪೂ|ಅ|ಮಧ್ಯರಾತ್ರಿ|ಮಧ್ಯಾನ್ಹ|ಬೆಳಗ್ಗೆ|ಸಂಜೆ|ರಾತ್ರಿ)/i,any:/^(ಪೂರ್ವಾಹ್ನ|ಅಪರಾಹ್ನ|ಮಧ್ಯರಾತ್ರಿ|ಮಧ್ಯಾನ್ಹ|ಬೆಳಗ್ಗೆ|ಸಂಜೆ|ರಾತ್ರಿ)/i},g={any:{am:/^ಪೂ/i,pm:/^ಅ/i,midnight:/ಮಧ್ಯರಾತ್ರಿ/i,noon:/ಮಧ್ಯಾನ್ಹ/i,morning:/ಬೆಳಗ್ಗೆ/i,afternoon:/ಮಧ್ಯಾನ್ಹ/i,evening:/ಸಂಜೆ/i,night:/ರಾತ್ರಿ/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(672),a=e(673),r=e(674),o=e(675),n=e(676),s={code:"ko",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:0,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"1초 미만",other:"{{count}}초 미만"},xSeconds:{one:"1초",other:"{{count}}초"},halfAMinute:"30초",lessThanXMinutes:{one:"1분 미만",other:"{{count}}분 미만"},xMinutes:{one:"1분",other:"{{count}}분"},aboutXHours:{one:"약 1시간",other:"약 {{count}}시간"},xHours:{one:"1시간",other:"{{count}}시간"},xDays:{one:"1일",other:"{{count}}일"},aboutXWeeks:{one:"약 1주",other:"약 {{count}}주"},xWeeks:{one:"1주",other:"{{count}}주"},aboutXMonths:{one:"약 1개월",other:"약 {{count}}개월"},xMonths:{one:"1개월",other:"{{count}}개월"},aboutXYears:{one:"약 1년",other:"약 {{count}}년"},xYears:{one:"1년",other:"{{count}}년"},overXYears:{one:"1년 이상",other:"{{count}}년 이상"},almostXYears:{one:"거의 1년",other:"거의 {{count}}년"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?s+" 후":s+" 전":s}},function(l,i,e){var t=e(2),a={full:"y년 M월 d일 EEEE",long:"y년 M월 d일",medium:"y.MM.dd",short:"y.MM.dd"},r={full:"a H시 mm분 ss초 zzzz",long:"a H:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} {{time}}",long:"{{date}} {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'지난' eeee p",yesterday:"'어제' p",today:"'오늘' p",tomorrow:"'내일' p",nextWeek:"'다음' eeee p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["BC","AD"],abbreviated:["BC","AD"],wide:["기원전","서기"]},r={narrow:["1","2","3","4"],abbreviated:["Q1","Q2","Q3","Q4"],wide:["1분기","2분기","3분기","4분기"]},o={narrow:["1","2","3","4","5","6","7","8","9","10","11","12"],abbreviated:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],wide:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"]},n={narrow:["일","월","화","수","목","금","토"],short:["일","월","화","수","목","금","토"],abbreviated:["일","월","화","수","목","금","토"],wide:["일요일","월요일","화요일","수요일","목요일","금요일","토요일"]},s={narrow:{am:"오전",pm:"오후",midnight:"자정",noon:"정오",morning:"아침",afternoon:"오후",evening:"저녁",night:"밤"},abbreviated:{am:"오전",pm:"오후",midnight:"자정",noon:"정오",morning:"아침",afternoon:"오후",evening:"저녁",night:"밤"},wide:{am:"오전",pm:"오후",midnight:"자정",noon:"정오",morning:"아침",afternoon:"오후",evening:"저녁",night:"밤"}},u={narrow:{am:"오전",pm:"오후",midnight:"자정",noon:"정오",morning:"아침",afternoon:"오후",evening:"저녁",night:"밤"},abbreviated:{am:"오전",pm:"오후",midnight:"자정",noon:"정오",morning:"아침",afternoon:"오후",evening:"저녁",night:"밤"},wide:{am:"오전",pm:"오후",midnight:"자정",noon:"정오",morning:"아침",afternoon:"오후",evening:"저녁",night:"밤"}};function d(f,_){var c=Number(f),v=_||{},g=String(v.unit);switch(g){case"minute":case"second":return c;case"date":return c+"일";default:return c+"번째"}}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(일|번째)?/i,o=/\d+/i,n={narrow:/^(b\.?\s?c\.?|b\.?\s?c\.?\s?e\.?|a\.?\s?d\.?|c\.?\s?e\.?)/i,abbreviated:/^(b\.?\s?c\.?|b\.?\s?c\.?\s?e\.?|a\.?\s?d\.?|c\.?\s?e\.?)/i,wide:/^(기원전|서기)/i},s={any:[/^(bc|기원전)/i,/^(ad|서기)/i]},u={narrow:/^[1234]/i,abbreviated:/^q[1234]/i,wide:/^[1234]사?분기/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^(1[012]|[123456789])/,abbreviated:/^(1[012]|[123456789])월/i,wide:/^(1[012]|[123456789])월/i},f={any:[/^1월?$/,/^2/,/^3/,/^4/,/^5/,/^6/,/^7/,/^8/,/^9/,/^10/,/^11/,/^12/]},_={narrow:/^[일월화수목금토]/,short:/^[일월화수목금토]/,abbreviated:/^[일월화수목금토]/,wide:/^[일월화수목금토]요일/},c={any:[/^일/,/^월/,/^화/,/^수/,/^목/,/^금/,/^토/]},v={any:/^(am|pm|오전|오후|자정|정오|아침|저녁|밤)/i},g={any:{am:/^(am|오전)/i,pm:/^(pm|오후)/i,midnight:/^자정/i,noon:/^정오/i,morning:/^아침/i,afternoon:/^오후/i,evening:/^저녁/i,night:/^밤/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(678),a=e(679),r=e(680),o=e(681),n=e(682),s={code:"lb",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=u;var t={lessThanXSeconds:{standalone:{one:"manner wéi eng Sekonn",other:"manner wéi {{count}} Sekonnen"},withPreposition:{one:"manner wéi enger Sekonn",other:"manner wéi {{count}} Sekonnen"}},xSeconds:{standalone:{one:"eng Sekonn",other:"{{count}} Sekonnen"},withPreposition:{one:"enger Sekonn",other:"{{count}} Sekonnen"}},halfAMinute:{standalone:"eng hallef Minutt",withPreposition:"enger hallwer Minutt"},lessThanXMinutes:{standalone:{one:"manner wéi eng Minutt",other:"manner wéi {{count}} Minutten"},withPreposition:{one:"manner wéi enger Minutt",other:"manner wéi {{count}} Minutten"}},xMinutes:{standalone:{one:"eng Minutt",other:"{{count}} Minutten"},withPreposition:{one:"enger Minutt",other:"{{count}} Minutten"}},aboutXHours:{standalone:{one:"ongeféier eng Stonn",other:"ongeféier {{count}} Stonnen"},withPreposition:{one:"ongeféier enger Stonn",other:"ongeféier {{count}} Stonnen"}},xHours:{standalone:{one:"eng Stonn",other:"{{count}} Stonnen"},withPreposition:{one:"enger Stonn",other:"{{count}} Stonnen"}},xDays:{standalone:{one:"een Dag",other:"{{count}} Deeg"},withPreposition:{one:"engem Dag",other:"{{count}} Deeg"}},aboutXWeeks:{standalone:{one:"ongeféier eng Woch",other:"ongeféier {{count}} Wochen"},withPreposition:{one:"ongeféier enger Woche",other:"ongeféier {{count}} Wochen"}},xWeeks:{standalone:{one:"eng Woch",other:"{{count}} Wochen"},withPreposition:{one:"enger Woch",other:"{{count}} Wochen"}},aboutXMonths:{standalone:{one:"ongeféier ee Mount",other:"ongeféier {{count}} Méint"},withPreposition:{one:"ongeféier engem Mount",other:"ongeféier {{count}} Méint"}},xMonths:{standalone:{one:"ee Mount",other:"{{count}} Méint"},withPreposition:{one:"engem Mount",other:"{{count}} Méint"}},aboutXYears:{standalone:{one:"ongeféier ee Joer",other:"ongeféier {{count}} Joer"},withPreposition:{one:"ongeféier engem Joer",other:"ongeféier {{count}} Joer"}},xYears:{standalone:{one:"ee Joer",other:"{{count}} Joer"},withPreposition:{one:"engem Joer",other:"{{count}} Joer"}},overXYears:{standalone:{one:"méi wéi ee Joer",other:"méi wéi {{count}} Joer"},withPreposition:{one:"méi wéi engem Joer",other:"méi wéi {{count}} Joer"}},almostXYears:{standalone:{one:"bal ee Joer",other:"bal {{count}} Joer"},withPreposition:{one:"bal engem Joer",other:"bal {{count}} Joer"}}},a=["d","h","n","t","z"],r=["a,","e","i","o","u"],o=[0,1,2,3,8,9],n=[40,50,60,70];function s(d){var m=d.charAt(0).toLowerCase();if(r.indexOf(m)!=-1||a.indexOf(m)!=-1)return!0;var f=d.split(" ")[0],_=parseInt(f);return!isNaN(_)&&o.indexOf(_%10)!=-1&&n.indexOf(parseInt(f.substring(0,2)))==-1}function u(d,m,f){f=f||{};var _=f.addSuffix?t[d].withPreposition:t[d].standalone,c;return typeof _=="string"?c=_:m===1?c=_.one:c=_.other.replace("{{count}}",m),f.addSuffix?f.comparison>0?"a"+(s(c)?"n":"")+" "+c:"viru"+(s(c)?"n":"")+" "+c:c}},function(l,i,e){var t=e(2),a={full:"EEEE, do MMMM y",long:"do MMMM y",medium:"do MMM y",short:"dd.MM.yy"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'um' {{time}}",long:"{{date}} 'um' {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:function(r){var o=r.getUTCDay(),n="'läschte";return(o===2||o===4)&&(n+="n"),n+="' eeee 'um' p",n},yesterday:"'gëschter um' p",today:"'haut um' p",tomorrow:"'moien um' p",nextWeek:"eeee 'um' p",other:"P"};function a(r,o,n,s){var u=t[r];return typeof u=="function"?u(o):u}},function(l,i,e){var t=e(3),a={narrow:["v.Chr.","n.Chr."],abbreviated:["v.Chr.","n.Chr."],wide:["viru Christus","no Christus"]},r={narrow:["1","2","3","4"],abbreviated:["Q1","Q2","Q3","Q4"],wide:["1. Quartal","2. Quartal","3. Quartal","4. Quartal"]},o={narrow:["J","F","M","A","M","J","J","A","S","O","N","D"],abbreviated:["Jan","Feb","Mäe","Abr","Mee","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],wide:["Januar","Februar","Mäerz","Abrëll","Mee","Juni","Juli","August","September","Oktober","November","Dezember"]},n={narrow:["S","M","D","M","D","F","S"],short:["So","Mé","Dë","Më","Do","Fr","Sa"],abbreviated:["So.","Mé.","Dë.","Më.","Do.","Fr.","Sa."],wide:["Sonndeg","Méindeg","Dënschdeg","Mëttwoch","Donneschdeg","Freideg","Samschdeg"]},s={narrow:{am:"mo.",pm:"nomë.",midnight:"Mëtternuecht",noon:"Mëtteg",morning:"Moien",afternoon:"Nomëtteg",evening:"Owend",night:"Nuecht"},abbreviated:{am:"moies",pm:"nomëttes",midnight:"Mëtternuecht",noon:"Mëtteg",morning:"Moien",afternoon:"Nomëtteg",evening:"Owend",night:"Nuecht"},wide:{am:"moies",pm:"nomëttes",midnight:"Mëtternuecht",noon:"Mëtteg",morning:"Moien",afternoon:"Nomëtteg",evening:"Owend",night:"Nuecht"}},u={narrow:{am:"mo.",pm:"nom.",midnight:"Mëtternuecht",noon:"mëttes",morning:"moies",afternoon:"nomëttes",evening:"owes",night:"nuets"},abbreviated:{am:"moies",pm:"nomëttes",midnight:"Mëtternuecht",noon:"mëttes",morning:"moies",afternoon:"nomëttes",evening:"owes",night:"nuets"},wide:{am:"moies",pm:"nomëttes",midnight:"Mëtternuecht",noon:"mëttes",morning:"moies",afternoon:"nomëttes",evening:"owes",night:"nuets"}};function d(f,_){var c=Number(f);return c+"."}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(\.)?/i,o=/\d+/i,n={narrow:/^(v\.? ?Chr\.?|n\.? ?Chr\.?)/i,abbreviated:/^(v\.? ?Chr\.?|n\.? ?Chr\.?)/i,wide:/^(viru Christus|virun eiser Zäitrechnung|no Christus|eiser Zäitrechnung)/i},s={any:[/^v/i,/^n/i]},u={narrow:/^[1234]/i,abbreviated:/^q[1234]/i,wide:/^[1234](\.)? Quartal/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[jfmasond]/i,abbreviated:/^(jan|feb|mäe|abr|mee|jun|jul|aug|sep|okt|nov|dez)/i,wide:/^(januar|februar|mäerz|abrëll|mee|juni|juli|august|september|oktober|november|dezember)/i},f={narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^mä/i,/^ab/i,/^me/i,/^jun/i,/^jul/i,/^au/i,/^s/i,/^o/i,/^n/i,/^d/i]},_={narrow:/^[smdf]/i,short:/^(so|mé|dë|më|do|fr|sa)/i,abbreviated:/^(son?|méi?|dën?|mët?|don?|fre?|sam?)\.?/i,wide:/^(sonndeg|méindeg|dënschdeg|mëttwoch|donneschdeg|freideg|samschdeg)/i},c={any:[/^so/i,/^mé/i,/^dë/i,/^më/i,/^do/i,/^f/i,/^sa/i]},v={narrow:/^(mo\.?|nomë\.?|Mëtternuecht|mëttes|moies|nomëttes|owes|nuets)/i,abbreviated:/^(moi\.?|nomët\.?|Mëtternuecht|mëttes|moies|nomëttes|owes|nuets)/i,wide:/^(moies|nomëttes|Mëtternuecht|mëttes|moies|nomëttes|owes|nuets)/i},g={any:{am:/^m/i,pm:/^n/i,midnight:/^Mëtter/i,noon:/^mëttes/i,morning:/moies/i,afternoon:/nomëttes/i,evening:/owes/i,night:/nuets/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"wide",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(684),a=e(685),r=e(686),o=e(687),n=e(688),s={code:"lt",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=d;var t={lessThanXSeconds:{one:r,other:u},xSeconds:{one:r,other:u},halfAMinute:"pusė minutės",lessThanXMinutes:{one:o,other:u},xMinutes:{one:o,other:u},aboutXHours:{one:o,other:u},xHours:{one:o,other:u},xDays:{one:o,other:u},aboutWeeks:{one:o,other:u},xWeeks:{one:o,other:u},aboutXMonths:{one:o,other:u},xMonths:{one:o,other:u},aboutXYears:{one:o,other:u},xYears:{one:o,other:u},overXYears:{one:o,other:u},almostXYears:{one:o,other:u}},a={xseconds_other:"sekundė_sekundžių_sekundes",xminutes_one:"minutė_minutės_minutę",xminutes_other:"minutės_minučių_minutes",xhours_one:"valanda_valandos_valandą",xhours_other:"valandos_valandų_valandas",xdays_one:"diena_dienos_dieną",xdays_other:"dienos_dienų_dienas",xweeks_one:"savaitė_savaitės_savaitę",xweeks_other:"savaitės_savaičių_savaites",xmonths_one:"mėnuo_mėnesio_mėnesį",xmonths_other:"mėnesiai_mėnesių_mėnesius",xyears_one:"metai_metų_metus",xyears_other:"metai_metų_metus",about:"apie",over:"daugiau nei",almost:"beveik",lessthan:"mažiau nei"};function r(m,f,_,c){return f?c?"kelių sekundžių":"kelias sekundes":"kelios sekundės"}function o(m,f,_,c){return f?c?s(_)[1]:s(_)[2]:s(_)[0]}function n(m){return m%10===0||m>10&&m<20}function s(m){return a[m].split("_")}function u(m,f,_,c){var v=m+" ";return m===1?v+o(m,f,_[0],c):f?c?v+s(_)[1]:v+(n(m)?s(_)[1]:s(_)[2]):v+(n(m)?s(_)[1]:s(_)[0])}function d(m,f,_){_=_||{};var c=m.match(/about|over|almost|lessthan/i),v=m.replace(c,""),g;return typeof t[m]=="string"?g=t[m]:f===1?g=t[m].one(f,_.addSuffix,v.toLowerCase()+"_one"):g=t[m].other(f,_.addSuffix,v.toLowerCase()+"_other"),c&&(g=a[c[0].toLowerCase()]+" "+g),_.addSuffix?_.comparison>0?"po "+g:"prieš "+g:g}},function(l,i,e){var t=e(2),a={full:"y 'm'. MMMM d 'd'., EEEE",long:"y 'm'. MMMM d 'd'.",medium:"y-MM-dd",short:"y-MM-dd"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} {{time}}",long:"{{date}} {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'Praėjusį' eeee p",yesterday:"'Vakar' p",today:"'Šiandien' p",tomorrow:"'Rytoj' p",nextWeek:"eeee p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["pr. Kr.","po Kr."],abbreviated:["pr. Kr.","po Kr."],wide:["prieš Kristų","po Kristaus"]},r={narrow:["1","2","3","4"],abbreviated:["I ketv.","II ketv.","III ketv.","IV ketv."],wide:["I ketvirtis","II ketvirtis","III ketvirtis","IV ketvirtis"]},o={narrow:["1","2","3","4"],abbreviated:["I k.","II k.","III k.","IV k."],wide:["I ketvirtis","II ketvirtis","III ketvirtis","IV ketvirtis"]},n={narrow:["S","V","K","B","G","B","L","R","R","S","L","G"],abbreviated:["saus.","vas.","kov.","bal.","geg.","birž.","liep.","rugp.","rugs.","spal.","lapkr.","gruod."],wide:["sausis","vasaris","kovas","balandis","gegužė","birželis","liepa","rugpjūtis","rugsėjis","spalis","lapkritis","gruodis"]},s={narrow:["S","V","K","B","G","B","L","R","R","S","L","G"],abbreviated:["saus.","vas.","kov.","bal.","geg.","birž.","liep.","rugp.","rugs.","spal.","lapkr.","gruod."],wide:["sausio","vasario","kovo","balandžio","gegužės","birželio","liepos","rugpjūčio","rugsėjo","spalio","lapkričio","gruodžio"]},u={narrow:["S","P","A","T","K","P","Š"],short:["Sk","Pr","An","Tr","Kt","Pn","Št"],abbreviated:["sk","pr","an","tr","kt","pn","št"],wide:["sekmadienis","pirmadienis","antradienis","trečiadienis","ketvirtadienis","penktadienis","šeštadienis"]},d={narrow:["S","P","A","T","K","P","Š"],short:["Sk","Pr","An","Tr","Kt","Pn","Št"],abbreviated:["sk","pr","an","tr","kt","pn","št"],wide:["sekmadienį","pirmadienį","antradienį","trečiadienį","ketvirtadienį","penktadienį","šeštadienį"]},m={narrow:{am:"pr. p.",pm:"pop.",midnight:"vidurnaktis",noon:"vidurdienis",morning:"rytas",afternoon:"diena",evening:"vakaras",night:"naktis"},abbreviated:{am:"priešpiet",pm:"popiet",midnight:"vidurnaktis",noon:"vidurdienis",morning:"rytas",afternoon:"diena",evening:"vakaras",night:"naktis"},wide:{am:"priešpiet",pm:"popiet",midnight:"vidurnaktis",noon:"vidurdienis",morning:"rytas",afternoon:"diena",evening:"vakaras",night:"naktis"}},f={narrow:{am:"pr. p.",pm:"pop.",midnight:"vidurnaktis",noon:"perpiet",morning:"rytas",afternoon:"popietė",evening:"vakaras",night:"naktis"},abbreviated:{am:"priešpiet",pm:"popiet",midnight:"vidurnaktis",noon:"perpiet",morning:"rytas",afternoon:"popietė",evening:"vakaras",night:"naktis"},wide:{am:"priešpiet",pm:"popiet",midnight:"vidurnaktis",noon:"perpiet",morning:"rytas",afternoon:"popietė",evening:"vakaras",night:"naktis"}};function _(v,g){var P=Number(v);return P+"-oji"}var c={ordinalNumber:_,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",formattingValues:o,defaultFormattingWidth:"wide",argumentCallback:function(v){return Number(v)-1}}),month:t.a({values:n,defaultWidth:"wide",formattingValues:s,defaultFormattingWidth:"wide"}),day:t.a({values:u,defaultWidth:"wide",formattingValues:d,defaultFormattingWidth:"wide"}),dayPeriod:t.a({values:m,defaultWidth:"wide",formattingValues:f,defaultFormattingWidth:"wide"})};i.a=c},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(-oji)?/i,o=/\d+/i,n={narrow:/^p(r|o)\.?\s?(kr\.?|me)/i,abbreviated:/^(pr\.\s?(kr\.|m\.\s?e\.)|po\s?kr\.|mūsų eroje)/i,wide:/^(prieš Kristų|prieš mūsų erą|po Kristaus|mūsų eroje)/i},s={wide:[/prieš/i,/(po|mūsų)/i],any:[/^pr/i,/^(po|m)/i]},u={narrow:/^([1234])/i,abbreviated:/^(I|II|III|IV)\s?ketv?\.?/i,wide:/^(I|II|III|IV)\s?ketvirtis/i},d={narrow:[/1/i,/2/i,/3/i,/4/i],any:[/I$/i,/II$/i,/III/i,/IV/i]},m={narrow:/^[svkbglr]/i,abbreviated:/^(saus\.|vas\.|kov\.|bal\.|geg\.|birž\.|liep\.|rugp\.|rugs\.|spal\.|lapkr\.|gruod\.)/i,wide:/^(sausi(s|o)|vasari(s|o)|kov(a|o)s|balandž?i(s|o)|gegužės?|birželi(s|o)|liep(a|os)|rugpjū(t|č)i(s|o)|rugsėj(is|o)|spali(s|o)|lapkri(t|č)i(s|o)|gruodž?i(s|o))/i},f={narrow:[/^s/i,/^v/i,/^k/i,/^b/i,/^g/i,/^b/i,/^l/i,/^r/i,/^r/i,/^s/i,/^l/i,/^g/i],any:[/^saus/i,/^vas/i,/^kov/i,/^bal/i,/^geg/i,/^birž/i,/^liep/i,/^rugp/i,/^rugs/i,/^spal/i,/^lapkr/i,/^gruod/i]},_={narrow:/^[spatkš]/i,short:/^(sk|pr|an|tr|kt|pn|št)/i,abbreviated:/^(sk|pr|an|tr|kt|pn|št)/i,wide:/^(sekmadien(is|į)|pirmadien(is|į)|antradien(is|į)|trečiadien(is|į)|ketvirtadien(is|į)|penktadien(is|į)|šeštadien(is|į))/i},c={narrow:[/^s/i,/^p/i,/^a/i,/^t/i,/^k/i,/^p/i,/^š/i],wide:[/^se/i,/^pi/i,/^an/i,/^tr/i,/^ke/i,/^pe/i,/^še/i],any:[/^sk/i,/^pr/i,/^an/i,/^tr/i,/^kt/i,/^pn/i,/^št/i]},v={narrow:/^(pr.\s?p.|pop.|vidurnaktis|(vidurdienis|perpiet)|rytas|(diena|popietė)|vakaras|naktis)/i,any:/^(priešpiet|popiet$|vidurnaktis|(vidurdienis|perpiet)|rytas|(diena|popietė)|vakaras|naktis)/i},g={narrow:{am:/^pr/i,pm:/^pop./i,midnight:/^vidurnaktis/i,noon:/^(vidurdienis|perp)/i,morning:/rytas/i,afternoon:/(die|popietė)/i,evening:/vakaras/i,night:/naktis/i},any:{am:/^pr/i,pm:/^popiet$/i,midnight:/^vidurnaktis/i,noon:/^(vidurdienis|perp)/i,morning:/rytas/i,afternoon:/(die|popietė)/i,evening:/vakaras/i,night:/naktis/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(690),a=e(691),r=e(692),o=e(693),n=e(694),s={code:"lv",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=r;function t(o){return function(n,s){if(n===1)return s.addSuffix?o.one[0].replace("{{time}}",o.one[2]):o.one[0].replace("{{time}}",o.one[1]);var u=n%10===1&&n%100!==11;return s.addSuffix?o.other[0].replace("{{time}}",u?o.other[3]:o.other[4]).replace("{{count}}",n):o.other[0].replace("{{time}}",u?o.other[1]:o.other[2]).replace("{{count}}",n)}}var a={lessThanXSeconds:t({one:["mazāk par {{time}}","sekundi","sekundi"],other:["mazāk nekā {{count}} {{time}}","sekunde","sekundes","sekundes","sekundēm"]}),xSeconds:t({one:["1 {{time}}","sekunde","sekundes"],other:["{{count}} {{time}}","sekunde","sekundes","sekundes","sekundēm"]}),halfAMinute:function(o,n){return n.addSuffix?"pusminūtes":"pusminūte"},lessThanXMinutes:t({one:["mazāk par {{time}}","minūti","minūti"],other:["mazāk nekā {{count}} {{time}}","minūte","minūtes","minūtes","minūtēm"]}),xMinutes:t({one:["1 {{time}}","minūte","minūtes"],other:["{{count}} {{time}}","minūte","minūtes","minūtes","minūtēm"]}),aboutXHours:t({one:["apmēram 1 {{time}}","stunda","stundas"],other:["apmēram {{count}} {{time}}","stunda","stundas","stundas","stundām"]}),xHours:t({one:["1 {{time}}","stunda","stundas"],other:["{{count}} {{time}}","stunda","stundas","stundas","stundām"]}),xDays:t({one:["1 {{time}}","diena","dienas"],other:["{{count}} {{time}}","diena","dienas","dienas","dienām"]}),aboutXWeeks:t({one:["apmēram 1 {{time}}","nedēļa","nedēļas"],other:["apmēram {{count}} {{time}}","nedēļa","nedēļu","nedēļas","nedēļām"]}),xWeeks:t({one:["1 {{time}}","nedēļa","nedēļas"],other:["{{count}} {{time}}","nedēļa","nedēļu","nedēļas","nedēļām"]}),aboutXMonths:t({one:["apmēram 1 {{time}}","mēnesis","mēneša"],other:["apmēram {{count}} {{time}}","mēnesis","mēneši","mēneša","mēnešiem"]}),xMonths:t({one:["1 {{time}}","mēnesis","mēneša"],other:["{{count}} {{time}}","mēnesis","mēneši","mēneša","mēnešiem"]}),aboutXYears:t({one:["apmēram 1 {{time}}","gads","gada"],other:["apmēram {{count}} {{time}}","gads","gadi","gada","gadiem"]}),xYears:t({one:["1 {{time}}","gads","gada"],other:["{{count}} {{time}}","gads","gadi","gada","gadiem"]}),overXYears:t({one:["ilgāk par 1 {{time}}","gadu","gadu"],other:["vairāk nekā {{count}} {{time}}","gads","gadi","gada","gadiem"]}),almostXYears:t({one:["gandrīz 1 {{time}}","gads","gada"],other:["vairāk nekā {{count}} {{time}}","gads","gadi","gada","gadiem"]})};function r(o,n,s){s=s||{};var u=a[o](n,s);return s.addSuffix?s.comparison>0?"pēc "+u:"pirms "+u:u}},function(l,i,e){var t=e(2),a={full:"y. 'gada' M. MMMM., EEEE",long:"y. 'gada' M. MMMM",medium:"dd.MM.y.",short:"dd.MM.y."},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'plkst.' {{time}}",long:"{{date}} 'plkst.' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=o;var t=e(13),a=["svētdienā","pirmdienā","otrdienā","trešdienā","ceturtdienā","piektdienā","sestdienā"],r={lastWeek:function(n,s,u){if(t.a(n,s,u))return"eeee 'plkst.' p";var d=a[n.getUTCDay()];return"'Pagājušā "+d+" plkst.' p"},yesterday:"'Vakar plkst.' p",today:"'Šodien plkst.' p",tomorrow:"'Rīt plkst.' p",nextWeek:function(n,s,u){if(t.a(n,s,u))return"eeee 'plkst.' p";var d=a[n.getUTCDay()];return"'Nākamajā "+d+" plkst.' p"},other:"P"};function o(n,s,u,d){var m=r[n];return typeof m=="function"?m(s,u,d):m}},function(l,i,e){var t=e(3),a={narrow:["p.m.ē","m.ē"],abbreviated:["p. m. ē.","m. ē."],wide:["pirms mūsu ēras","mūsu ērā"]},r={narrow:["1","2","3","4"],abbreviated:["1. cet.","2. cet.","3. cet.","4. cet."],wide:["pirmais ceturksnis","otrais ceturksnis","trešais ceturksnis","ceturtais ceturksnis"]},o={narrow:["1","2","3","4"],abbreviated:["1. cet.","2. cet.","3. cet.","4. cet."],wide:["pirmajā ceturksnī","otrajā ceturksnī","trešajā ceturksnī","ceturtajā ceturksnī"]},n={narrow:["J","F","M","A","M","J","J","A","S","O","N","D"],abbreviated:["janv.","febr.","marts","apr.","maijs","jūn.","jūl.","aug.","sept.","okt.","nov.","dec."],wide:["janvāris","februāris","marts","aprīlis","maijs","jūnijs","jūlijs","augusts","septembris","oktobris","novembris","decembris"]},s={narrow:["J","F","M","A","M","J","J","A","S","O","N","D"],abbreviated:["janv.","febr.","martā","apr.","maijs","jūn.","jūl.","aug.","sept.","okt.","nov.","dec."],wide:["janvārī","februārī","martā","aprīlī","maijā","jūnijā","jūlijā","augustā","septembrī","oktobrī","novembrī","decembrī"]},u={narrow:["S","P","O","T","C","P","S"],short:["Sv","P","O","T","C","Pk","S"],abbreviated:["svētd.","pirmd.","otrd.","trešd.","ceturtd.","piektd.","sestd."],wide:["svētdiena","pirmdiena","otrdiena","trešdiena","ceturtdiena","piektdiena","sestdiena"]},d={narrow:["S","P","O","T","C","P","S"],short:["Sv","P","O","T","C","Pk","S"],abbreviated:["svētd.","pirmd.","otrd.","trešd.","ceturtd.","piektd.","sestd."],wide:["svētdienā","pirmdienā","otrdienā","trešdienā","ceturtdienā","piektdienā","sestdienā"]},m={narrow:{am:"am",pm:"pm",midnight:"pusn.",noon:"pusd.",morning:"rīts",afternoon:"diena",evening:"vakars",night:"nakts"},abbreviated:{am:"am",pm:"pm",midnight:"pusn.",noon:"pusd.",morning:"rīts",afternoon:"pēcpusd.",evening:"vakars",night:"nakts"},wide:{am:"am",pm:"pm",midnight:"pusnakts",noon:"pusdienlaiks",morning:"rīts",afternoon:"pēcpusdiena",evening:"vakars",night:"nakts"}},f={narrow:{am:"am",pm:"pm",midnight:"pusn.",noon:"pusd.",morning:"rītā",afternoon:"dienā",evening:"vakarā",night:"naktī"},abbreviated:{am:"am",pm:"pm",midnight:"pusn.",noon:"pusd.",morning:"rītā",afternoon:"pēcpusd.",evening:"vakarā",night:"naktī"},wide:{am:"am",pm:"pm",midnight:"pusnaktī",noon:"pusdienlaikā",morning:"rītā",afternoon:"pēcpusdienā",evening:"vakarā",night:"naktī"}};function _(v,g){return v+"."}var c={ordinalNumber:_,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",formattingValues:o,defaultFormattingWidth:"wide",argumentCallback:function(v){return Number(v)-1}}),month:t.a({values:n,defaultWidth:"wide",formattingValues:s,defaultFormattingWidth:"wide"}),day:t.a({values:u,defaultWidth:"wide",formattingValues:d,defaultFormattingWidth:"wide"}),dayPeriod:t.a({values:m,defaultWidth:"wide",formattingValues:f,defaultFormattingWidth:"wide"})};i.a=c},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)\./i,o=/\d+/i,n={narrow:/^(p\.m\.ē|m\.ē)/i,abbreviated:/^(p\. m\. ē\.|m\. ē\.)/i,wide:/^(pirms mūsu ēras|mūsu ērā)/i},s={any:[/^p/i,/^m/i]},u={narrow:/^[1234]/i,abbreviated:/^[1234](\. cet\.)/i,wide:/^(pirma(is|jā)|otra(is|jā)|treša(is|jā)|ceturta(is|jā)) ceturksn(is|ī)/i},d={narrow:[/^1/i,/^2/i,/^3/i,/^4/i],abbreviated:[/^1/i,/^2/i,/^3/i,/^4/i],wide:[/^p/i,/^o/i,/^t/i,/^c/i]},m={narrow:/^[jfmasond]/i,abbreviated:/^(janv\.|febr\.|marts|apr\.|maijs|jūn\.|jūl\.|aug\.|sept\.|okt\.|nov\.|dec\.)/i,wide:/^(janvār(is|ī)|februār(is|ī)|mart[sā]|aprīl(is|ī)|maij[sā]|jūnij[sā]|jūlij[sā]|august[sā]|septembr(is|ī)|oktobr(is|ī)|novembr(is|ī)|decembr(is|ī))/i},f={narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^mar/i,/^ap/i,/^mai/i,/^jūn/i,/^jūl/i,/^au/i,/^s/i,/^o/i,/^n/i,/^d/i]},_={narrow:/^[spotc]/i,short:/^(sv|pi|o|t|c|pk|s)/i,abbreviated:/^(svētd\.|pirmd\.|otrd.\|trešd\.|ceturtd\.|piektd\.|sestd\.)/i,wide:/^(svētdien(a|ā)|pirmdien(a|ā)|otrdien(a|ā)|trešdien(a|ā)|ceturtdien(a|ā)|piektdien(a|ā)|sestdien(a|ā))/i},c={narrow:[/^s/i,/^p/i,/^o/i,/^t/i,/^c/i,/^p/i,/^s/i],any:[/^sv/i,/^pi/i,/^o/i,/^t/i,/^c/i,/^p/i,/^se/i]},v={narrow:/^(am|pm|pusn\.|pusd\.|rīt(s|ā)|dien(a|ā)|vakar(s|ā)|nakt(s|ī))/,abbreviated:/^(am|pm|pusn\.|pusd\.|rīt(s|ā)|pēcpusd\.|vakar(s|ā)|nakt(s|ī))/,wide:/^(am|pm|pusnakt(s|ī)|pusdienlaik(s|ā)|rīt(s|ā)|pēcpusdien(a|ā)|vakar(s|ā)|nakt(s|ī))/i},g={any:{am:/^am/i,pm:/^pm/i,midnight:/^pusn/i,noon:/^pusd/i,morning:/^r/i,afternoon:/^(d|pēc)/i,evening:/^v/i,night:/^n/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(696),a=e(697),r=e(698),o=e(699),n=e(700),s={code:"mk",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"помалку од секунда",other:"помалку од {{count}} секунди"},xSeconds:{one:"1 секунда",other:"{{count}} секунди"},halfAMinute:"половина минута",lessThanXMinutes:{one:"помалку од минута",other:"помалку од {{count}} минути"},xMinutes:{one:"1 минута",other:"{{count}} минути"},aboutXHours:{one:"околу 1 час",other:"околу {{count}} часа"},xHours:{one:"1 час",other:"{{count}} часа"},xDays:{one:"1 ден",other:"{{count}} дена"},aboutXWeeks:{one:"околу 1 недела",other:"околу {{count}} месеци"},xWeeks:{one:"1 недела",other:"{{count}} недели"},aboutXMonths:{one:"околу 1 месец",other:"околу {{count}} недели"},xMonths:{one:"1 месец",other:"{{count}} месеци"},aboutXYears:{one:"околу 1 година",other:"околу {{count}} години"},xYears:{one:"1 година",other:"{{count}} години"},overXYears:{one:"повеќе од 1 година",other:"повеќе од {{count}} години"},almostXYears:{one:"безмалку 1 година",other:"безмалку {{count}} години"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"за "+s:"пред "+s:s}},function(l,i,e){var t=e(2),a={full:"EEEE, dd MMMM yyyy",long:"dd MMMM yyyy",medium:"dd MMM yyyy",short:"dd/MM/yyyy"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"H:mm"},o={any:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"any"})};i.a=n},function(l,i,e){i.a=u;var t=e(13),a=["недела","понеделник","вторник","среда","четврток","петок","сабота"];function r(d){var m=a[d];switch(d){case 0:case 3:case 6:return"'минатата "+m+" во' p";case 1:case 2:case 4:case 5:return"'минатиот "+m+" во' p"}}function o(d){var m=a[d];switch(d){case 0:case 3:case 6:return"'ова "+m+" вo' p";case 1:case 2:case 4:case 5:return"'овој "+m+" вo' p"}}function n(d){var m=a[d];switch(d){case 0:case 3:case 6:return"'следната "+m+" вo' p";case 1:case 2:case 4:case 5:return"'следниот "+m+" вo' p"}}var s={lastWeek:function(d,m,f){var _=d.getUTCDay();return t.a(d,m,f)?o(_):r(_)},yesterday:"'вчера во' p",today:"'денес во' p",tomorrow:"'утре во' p",nextWeek:function(d,m,f){var _=d.getUTCDay();return t.a(d,m,f)?o(_):n(_)},other:"P"};function u(d,m,f,_){var c=s[d];return typeof c=="function"?c(m,f,_):c}},function(l,i,e){var t=e(3),a={narrow:["пр.н.е.","н.е."],abbreviated:["пред н. е.","н. е."],wide:["пред нашата ера","нашата ера"]},r={narrow:["1","2","3","4"],abbreviated:["1-ви кв.","2-ри кв.","3-ти кв.","4-ти кв."],wide:["1-ви квартал","2-ри квартал","3-ти квартал","4-ти квартал"]},o={abbreviated:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],wide:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"]},n={narrow:["Н","П","В","С","Ч","П","С"],short:["не","по","вт","ср","че","пе","са"],abbreviated:["нед","пон","вто","сре","чет","пет","саб"],wide:["недела","понеделник","вторник","среда","четврток","петок","сабота"]},s={wide:{am:"претпладне",pm:"попладне",midnight:"полноќ",noon:"напладне",morning:"наутро",afternoon:"попладне",evening:"навечер",night:"ноќе"}};function u(m){var f=Number(m),_=f%100;if(_>20||_<10)switch(_%10){case 1:return f+"-ви";case 2:return f+"-ри";case 7:case 8:return f+"-ми"}return f+"-ти"}var d={ordinalNumber:u,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(m){return Number(m)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide"})};i.a=d},function(l,i,e){var t=e(5),a=e(4),r=/^(\d+)(-?[врмт][и])?/i,o=/\d+/i,n={narrow:/^((пр)?н\.?\s?е\.?)/i,abbreviated:/^((пр)?н\.?\s?е\.?)/i,wide:/^(пред нашата ера|нашата ера)/i},s={any:[/^п/i,/^н/i]},u={narrow:/^[1234]/i,abbreviated:/^[1234](-?[врт]?и?)? кв.?/i,wide:/^[1234](-?[врт]?и?)? квартал/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[нпвсч]/i,short:/^(не|по|вт|ср|че|пе|са)/i,abbreviated:/^(нед|пон|вто|сре|чет|пет|саб)/i,wide:/^(недела|понеделник|вторник|среда|четврток|петок|сабота)/i},f={narrow:[/^н/i,/^п/i,/^в/i,/^с/i,/^ч/i,/^п/i,/^с/i],any:[/^н[ед]/i,/^п[он]/i,/^вт/i,/^ср/i,/^ч[ет]/i,/^п[ет]/i,/^с[аб]/i]},_={abbreviated:/^(јан|фев|мар|апр|мај|јун|јул|авг|сеп|окт|ноем|дек)/i,wide:/^(јануари|февруари|март|април|мај|јуни|јули|август|септември|октомври|ноември|декември)/i},c={any:[/^ја/i,/^Ф/i,/^мар/i,/^ап/i,/^мај/i,/^јун/i,/^јул/i,/^ав/i,/^се/i,/^окт/i,/^но/i,/^де/i]},v={any:/^(претп|попл|полноќ|утро|пладне|вечер|ноќ)/i},g={any:{am:/претпладне/i,pm:/попладне/i,midnight:/полноќ/i,noon:/напладне/i,morning:/наутро/i,afternoon:/попладне/i,evening:/навечер/i,night:/ноќе/i}},P={ordinalNumber:a.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:t.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:t.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:t.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),day:t.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),dayPeriod:t.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(702),a=e(703),r=e(704),o=e(705),n=e(706),s={code:"ms",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"kurang dari 1 saat",other:"kurang dari {{count}} saat"},xSeconds:{one:"1 saat",other:"{{count}} saat"},halfAMinute:"setengah minit",lessThanXMinutes:{one:"kurang dari 1 minit",other:"kurang dari {{count}} minit"},xMinutes:{one:"1 minit",other:"{{count}} minit"},aboutXHours:{one:"sekitar 1 jam",other:"sekitar {{count}} jam"},xHours:{one:"1 jam",other:"{{count}} jam"},xDays:{one:"1 hari",other:"{{count}} hari"},aboutXWeeks:{one:"sekitar 1 minggu",other:"sekitar {{count}} minggu"},xWeeks:{one:"1 minggu",other:"{{count}} minggu"},aboutXMonths:{one:"sekitar 1 bulan",other:"sekitar {{count}} bulan"},xMonths:{one:"1 bulan",other:"{{count}} bulan"},aboutXYears:{one:"sekitar 1 tahun",other:"sekitar {{count}} tahun"},xYears:{one:"1 tahun",other:"{{count}} tahun"},overXYears:{one:"lebih dari 1 tahun",other:"lebih dari {{count}} tahun"},almostXYears:{one:"hampir 1 tahun",other:"hampir {{count}} tahun"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"dalam masa "+s:s+" yang lalu":s}},function(l,i,e){var t=e(2),a={full:"EEEE, d MMMM yyyy",long:"d MMMM yyyy",medium:"d MMM yyyy",short:"d/M/yyyy"},r={full:"HH.mm.ss",long:"HH.mm.ss",medium:"HH.mm",short:"HH.mm"},o={full:"{{date}} 'pukul' {{time}}",long:"{{date}} 'pukul' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"eeee 'lepas pada jam' p",yesterday:"'Semalam pada jam' p",today:"'Hari ini pada jam' p",tomorrow:"'Esok pada jam' p",nextWeek:"eeee 'pada jam' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["SM","M"],abbreviated:["SM","M"],wide:["Sebelum Masihi","Masihi"]},r={narrow:["1","2","3","4"],abbreviated:["S1","S2","S3","S4"],wide:["Suku pertama","Suku kedua","Suku ketiga","Suku keempat"]},o={narrow:["J","F","M","A","M","J","J","O","S","O","N","D"],abbreviated:["Jan","Feb","Mac","Apr","Mei","Jun","Jul","Ogo","Sep","Okt","Nov","Dis"],wide:["Januari","Februari","Mac","April","Mei","Jun","Julai","Ogos","September","Oktober","November","Disember"]},n={narrow:["A","I","S","R","K","J","S"],short:["Ahd","Isn","Sel","Rab","Kha","Jum","Sab"],abbreviated:["Ahd","Isn","Sel","Rab","Kha","Jum","Sab"],wide:["Ahad","Isnin","Selasa","Rabu","Khamis","Jumaat","Sabtu"]},s={narrow:{am:"am",pm:"pm",midnight:"tgh malam",noon:"tgh hari",morning:"pagi",afternoon:"tengah hari",evening:"petang",night:"malam"},abbreviated:{am:"AM",pm:"PM",midnight:"tengah malam",noon:"tengah hari",morning:"pagi",afternoon:"tengah hari",evening:"petang",night:"malam"},wide:{am:"a.m.",pm:"p.m.",midnight:"tengah malam",noon:"tengah hari",morning:"pagi",afternoon:"tengah hari",evening:"petang",night:"malam"}},u={narrow:{am:"am",pm:"pm",midnight:"tengah malam",noon:"tengah hari",morning:"pagi",afternoon:"tengah hari",evening:"petang",night:"malam"},abbreviated:{am:"AM",pm:"PM",midnight:"tengah malam",noon:"tengah hari",morning:"pagi",afternoon:"tengah hari",evening:"petang",night:"malam"},wide:{am:"a.m.",pm:"p.m.",midnight:"tengah malam",noon:"tengah hari",morning:"pagi",afternoon:"tengah hari",evening:"petang",night:"malam"}};function d(f,_){var c=Number(f);switch(c){default:return"ke-"+c}}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(5),a=e(4),r=/^ke-(\d+)?/i,o=/petama|\d+/i,n={narrow:/^(sm|m)/i,abbreviated:/^(s\.?\s?m\.?|m\.?)/i,wide:/^(sebelum masihi|masihi)/i},s={any:[/^s/i,/^(m)/i]},u={narrow:/^[1234]/i,abbreviated:/^S[1234]/i,wide:/Suku (pertama|kedua|ketiga|keempat)/i},d={any:[/pertama|1/i,/kedua|2/i,/ketiga|3/i,/keempat|4/i]},m={narrow:/^[jfmasond]/i,abbreviated:/^(jan|feb|mac|apr|mei|jun|jul|ogo|sep|okt|nov|dis)/i,wide:/^(januari|februari|mac|april|mei|jun|julai|ogos|september|oktober|november|disember)/i},f={narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^o/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^ma/i,/^ap/i,/^me/i,/^jun/i,/^jul/i,/^og/i,/^s/i,/^ok/i,/^n/i,/^d/i]},_={narrow:/^[aisrkj]/i,short:/^(ahd|isn|sel|rab|kha|jum|sab)/i,abbreviated:/^(ahd|isn|sel|rab|kha|jum|sab)/i,wide:/^(ahad|isnin|selasa|rabu|khamis|jumaat|sabtu)/i},c={narrow:[/^a/i,/^i/i,/^s/i,/^r/i,/^k/i,/^j/i,/^s/i],any:[/^a/i,/^i/i,/^se/i,/^r/i,/^k/i,/^j/i,/^sa/i]},v={narrow:/^(am|pm|tengah malam|tengah hari|pagi|petang|malam)/i,any:/^([ap]\.?\s?m\.?|tengah malam|tengah hari|pagi|petang|malam)/i},g={any:{am:/^a/i,pm:/^pm/i,midnight:/^tengah m/i,noon:/^tengah h/i,morning:/pa/i,afternoon:/tengah h/i,evening:/pe/i,night:/m/i}},P={ordinalNumber:a.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:t.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:t.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:t.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:t.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:t.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(708),a=e(709),r=e(710),o=e(711),n=e(712),s={code:"mt",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"inqas minn sekonda",other:"inqas minn {{count}} sekondi"},xSeconds:{one:"sekonda",other:"{{count}} sekondi"},halfAMinute:"nofs minuta",lessThanXMinutes:{one:"inqas minn minuta",other:"inqas minn {{count}} minuti"},xMinutes:{one:"minuta",other:"{{count}} minuti"},aboutXHours:{one:"madwar siegħa",other:"madwar {{count}} siegħat"},xHours:{one:"siegħa",other:"{{count}} siegħat"},xDays:{one:"ġurnata",other:"{{count}} ġranet"},aboutXWeeks:{one:"madwar ġimgħa",other:"madwar {{count}} ġimgħat"},xWeeks:{one:"ġimgħa",other:"{{count}} ġimgħat"},aboutXMonths:{one:"madwar xahar",other:"madwar {{count}} xhur"},xMonths:{one:"xahar",other:"{{count}} xhur"},aboutXYears:{one:"madwar sena",two:"madwar sentejn",other:"madwar {{count}} snin"},xYears:{one:"sena",two:"sentejn",other:"{{count}} snin"},overXYears:{one:"aktar minn sena",two:"aktar minn sentejn",other:"aktar minn {{count}} snin"},almostXYears:{one:"kważi sena",two:"kważi sentejn",other:"kważi {{count}} snin"}};function a(r,o,n){n=n||{};var s=r.match(/years/i),u;return typeof t[r]=="string"?u=t[r]:o===1?u=t[r].one:o===2&&s?u=t[r].two:u=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"f'"+u:u+" ilu":u}},function(l,i,e){var t=e(2),a={full:"EEEE, d MMMM yyyy",long:"d MMMM yyyy",medium:"d MMM yyyy",short:"dd/MM/yyyy"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} {{time}}",long:"{{date}} {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"eeee 'li għadda' 'fil-'p",yesterday:"'Il-bieraħ fil-'p",today:"'Illum fil-'p",tomorrow:"'Għada fil-'p",nextWeek:"eeee 'fil-'p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["Q","W"],abbreviated:["QK","WK"],wide:["qabel Kristu","wara Kristu"]},r={narrow:["1","2","3","4"],abbreviated:["K1","K2","K3","K4"],wide:["1. kwart","2. kwart","3. kwart","4. kwart"]},o={narrow:["J","F","M","A","M","Ġ","L","A","S","O","N","D"],abbreviated:["Jan","Fra","Mar","Apr","Mej","Ġun","Lul","Aww","Set","Ott","Nov","Diċ"],wide:["Jannar","Frar","Marzu","April","Mejju","Ġunju","Lulju","Awwissu","Settembru","Ottubru","Novembru","Diċembru"]},n={narrow:["Ħ","T","T","E","Ħ","Ġ","S"],short:["Ħa","Tn","Tl","Er","Ħa","Ġi","Si"],abbreviated:["Ħad","Tne","Tli","Erb","Ħam","Ġim","Sib"],wide:["Il-Ħadd","It-Tnejn","It-Tlieta","L-Erbgħa","Il-Ħamis","Il-Ġimgħa","Is-Sibt"]},s={narrow:{am:"a",pm:"p",midnight:"nofsillejl",noon:"nofsinhar",morning:"għodwa",afternoon:"wara nofsinhar",evening:"filgħaxija",night:"lejl"},abbreviated:{am:"AM",pm:"PM",midnight:"nofsillejl",noon:"nofsinhar",morning:"għodwa",afternoon:"wara nofsinhar",evening:"filgħaxija",night:"lejl"},wide:{am:"a.m.",pm:"p.m.",midnight:"nofsillejl",noon:"nofsinhar",morning:"għodwa",afternoon:"wara nofsinhar",evening:"filgħaxija",night:"lejl"}},u={narrow:{am:"a",pm:"p",midnight:"f'nofsillejl",noon:"f'nofsinhar",morning:"filgħodu",afternoon:"wara nofsinhar",evening:"filgħaxija",night:"billejl"},abbreviated:{am:"AM",pm:"PM",midnight:"f'nofsillejl",noon:"f'nofsinhar",morning:"filgħodu",afternoon:"wara nofsinhar",evening:"filgħaxija",night:"billejl"},wide:{am:"a.m.",pm:"p.m.",midnight:"f'nofsillejl",noon:"f'nofsinhar",morning:"filgħodu",afternoon:"wara nofsinhar",evening:"filgħaxija",night:"billejl"}};function d(f){var _=Number(f);return _+"º"}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(º)?/i,o=/\d+/i,n={narrow:/^(q|w)/i,abbreviated:/^(q\.?\s?k\.?|b\.?\s?c\.?\s?e\.?|w\.?\s?k\.?)/i,wide:/^(qabel kristu|before common era|wara kristu|common era)/i},s={any:[/^(q|b)/i,/^(w|c)/i]},u={narrow:/^[1234]/i,abbreviated:/^k[1234]/i,wide:/^[1234](\.)? kwart/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[jfmaglsond]/i,abbreviated:/^(jan|fra|mar|apr|mej|ġun|lul|aww|set|ott|nov|diċ)/i,wide:/^(jannar|frar|marzu|april|mejju|ġunju|lulju|awwissu|settembru|ottubru|novembru|diċembru)/i},f={narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^ġ/i,/^l/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^mar/i,/^ap/i,/^mej/i,/^ġ/i,/^l/i,/^aw/i,/^s/i,/^o/i,/^n/i,/^d/i]},_={narrow:/^[ħteġs]/i,short:/^(ħa|tn|tl|er|ħa|ġi|si)/i,abbreviated:/^(ħad|tne|tli|erb|ħam|ġim|sib)/i,wide:/^(il-ħadd|it-tnejn|it-tlieta|l-erbgħa|il-ħamis|il-ġimgħa|is-sibt)/i},c={narrow:[/^ħ/i,/^t/i,/^t/i,/^e/i,/^ħ/i,/^ġ/i,/^s/i],any:[/^(il-)?ħad/i,/^(it-)?tn/i,/^(it-)?tl/i,/^(l-)?er/i,/^(il-)?ham/i,/^(il-)?ġi/i,/^(is-)?si/i]},v={narrow:/^(a|p|f'nofsillejl|f'nofsinhar|(ta') (għodwa|wara nofsinhar|filgħaxija|lejl))/i,any:/^([ap]\.?\s?m\.?|f'nofsillejl|f'nofsinhar|(ta') (għodwa|wara nofsinhar|filgħaxija|lejl))/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/^f'nofsillejl/i,noon:/^f'nofsinhar/i,morning:/għodwa/i,afternoon:/wara(\s.*)nofsinhar/i,evening:/filgħaxija/i,night:/lejl/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(714),a=e(715),r=e(716),o=e(717),n=e(718),s={code:"nb",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=r;var t={lessThanXSeconds:{singular:"mindre enn ett sekund",plural:"mindre enn {{count}} sekunder"},xSeconds:{singular:"ett sekund",plural:"{{count}} sekunder"},halfAMinute:"et halvt minutt",lessThanXMinutes:{singular:"mindre enn ett minutt",plural:"mindre enn {{count}} minutter"},xMinutes:{singular:"ett minutt",plural:"{{count}} minutter"},aboutXHours:{singular:"omtrent en time",plural:"omtrent {{count}} timer"},xHours:{singular:"en time",plural:"{{count}} timer"},xDays:{singular:"en dag",plural:"{{count}} dager"},aboutXWeeks:{singular:"omtrent en uke",plural:"omtrent {{count}} uker"},xWeeks:{singular:"en uke",plural:"{{count}} uker"},aboutXMonths:{singular:"omtrent en måned",plural:"omtrent {{count}} måneder"},xMonths:{singular:"en måned",plural:"{{count}} måneder"},aboutXYears:{singular:"omtrent ett år",plural:"omtrent {{count}} år"},xYears:{singular:"ett år",plural:"{{count}} år"},overXYears:{singular:"over ett år",plural:"over {{count}} år"},almostXYears:{singular:"nesten ett år",plural:"nesten {{count}} år"}},a=["null","en","to","tre","fire","fem","seks","sju","åtte","ni","ti","elleve","tolv"];function r(o,n,s){s=s||{onlyNumeric:!1};var u=t[o],d;return typeof u=="string"?d=u:n===0||n>1?s.onlyNumeric?d=u.plural.replace("{{count}}",n):d=u.plural.replace("{{count}}",n<13?a[n]:n):d=u.singular,s.addSuffix?s.comparison>0?"om "+d:d+" siden":d}},function(l,i,e){var t=e(2),a={full:"EEEE d. MMMM y",long:"d. MMMM y",medium:"d. MMM y",short:"dd.MM.y"},r={full:"'kl'. HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'kl.' {{time}}",long:"{{date}} 'kl.' {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'forrige' eeee 'kl.' p",yesterday:"'i går kl.' p",today:"'i dag kl.' p",tomorrow:"'i morgen kl.' p",nextWeek:"EEEE 'kl.' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["f.Kr.","e.Kr."],abbreviated:["f.Kr.","e.Kr."],wide:["før Kristus","etter Kristus"]},r={narrow:["1","2","3","4"],abbreviated:["Q1","Q2","Q3","Q4"],wide:["1. kvartal","2. kvartal","3. kvartal","4. kvartal"]},o={narrow:["J","F","M","A","M","J","J","A","S","O","N","D"],abbreviated:["jan.","feb.","mars","apr.","mai","juni","juli","aug.","sep.","okt.","nov.","des."],wide:["januar","februar","mars","april","mai","juni","juli","august","september","oktober","november","desember"]},n={narrow:["S","M","T","O","T","F","L"],short:["sø","ma","ti","on","to","fr","lø"],abbreviated:["søn","man","tir","ons","tor","fre","lør"],wide:["søndag","mandag","tirsdag","onsdag","torsdag","fredag","lørdag"]},s={narrow:{am:"a",pm:"p",midnight:"midnatt",noon:"middag",morning:"på morg.",afternoon:"på etterm.",evening:"på kvelden",night:"på natten"},abbreviated:{am:"a.m.",pm:"p.m.",midnight:"midnatt",noon:"middag",morning:"på morg.",afternoon:"på etterm.",evening:"på kvelden",night:"på natten"},wide:{am:"a.m.",pm:"p.m.",midnight:"midnatt",noon:"middag",morning:"på morgenen",afternoon:"på ettermiddagen",evening:"på kvelden",night:"på natten"}};function u(m){var f=Number(m);return f+"."}var d={ordinalNumber:u,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(m){return Number(m)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide"})};i.a=d},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)\.?/i,o=/\d+/i,n={narrow:/^(f\.? ?Kr\.?|fvt\.?|e\.? ?Kr\.?|evt\.?)/i,abbreviated:/^(f\.? ?Kr\.?|fvt\.?|e\.? ?Kr\.?|evt\.?)/i,wide:/^(før Kristus|før vår tid|etter Kristus|vår tid)/i},s={any:[/^f/i,/^e/i]},u={narrow:/^[1234]/i,abbreviated:/^q[1234]/i,wide:/^[1234](\.)? kvartal/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[jfmasond]/i,abbreviated:/^(jan|feb|mars?|apr|mai|juni?|juli?|aug|sep|okt|nov|des)\.?/i,wide:/^(januar|februar|mars|april|mai|juni|juli|august|september|oktober|november|desember)/i},f={narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^mar/i,/^ap/i,/^mai/i,/^jun/i,/^jul/i,/^aug/i,/^s/i,/^o/i,/^n/i,/^d/i]},_={narrow:/^[smtofl]/i,short:/^(sø|ma|ti|on|to|fr|lø)/i,abbreviated:/^(søn|man|tir|ons|tor|fre|lør)/i,wide:/^(søndag|mandag|tirsdag|onsdag|torsdag|fredag|lørdag)/i},c={any:[/^s/i,/^m/i,/^ti/i,/^o/i,/^to/i,/^f/i,/^l/i]},v={narrow:/^(midnatt|middag|(på) (morgenen|ettermiddagen|kvelden|natten)|[ap])/i,any:/^([ap]\.?\s?m\.?|midnatt|middag|(på) (morgenen|ettermiddagen|kvelden|natten))/i},g={any:{am:/^a(\.?\s?m\.?)?$/i,pm:/^p(\.?\s?m\.?)?$/i,midnight:/^midn/i,noon:/^midd/i,morning:/morgen/i,afternoon:/ettermiddag/i,evening:/kveld/i,night:/natt/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(720),a=e(721),r=e(722),o=e(723),n=e(724),s={code:"nl",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"minder dan een seconde",other:"minder dan {{count}} seconden"},xSeconds:{one:"1 seconde",other:"{{count}} seconden"},halfAMinute:"een halve minuut",lessThanXMinutes:{one:"minder dan een minuut",other:"minder dan {{count}} minuten"},xMinutes:{one:"een minuut",other:"{{count}} minuten"},aboutXHours:{one:"ongeveer 1 uur",other:"ongeveer {{count}} uur"},xHours:{one:"1 uur",other:"{{count}} uur"},xDays:{one:"1 dag",other:"{{count}} dagen"},aboutXWeeks:{one:"ongeveer 1 week",other:"ongeveer {{count}} weken"},xWeeks:{one:"1 week",other:"{{count}} weken"},aboutXMonths:{one:"ongeveer 1 maand",other:"ongeveer {{count}} maanden"},xMonths:{one:"1 maand",other:"{{count}} maanden"},aboutXYears:{one:"ongeveer 1 jaar",other:"ongeveer {{count}} jaar"},xYears:{one:"1 jaar",other:"{{count}} jaar"},overXYears:{one:"meer dan 1 jaar",other:"meer dan {{count}} jaar"},almostXYears:{one:"bijna 1 jaar",other:"bijna {{count}} jaar"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"over "+s:s+" geleden":s}},function(l,i,e){var t=e(2),a={full:"EEEE d MMMM y",long:"d MMMM y",medium:"d MMM y",short:"dd-MM-y"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'om' {{time}}",long:"{{date}} 'om' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'afgelopen' eeee 'om' p",yesterday:"'gisteren om' p",today:"'vandaag om' p",tomorrow:"'morgen om' p",nextWeek:"eeee 'om' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["v.C.","n.C."],abbreviated:["v.Chr.","n.Chr."],wide:["voor Christus","na Christus"]},r={narrow:["1","2","3","4"],abbreviated:["K1","K2","K3","K4"],wide:["1e kwartaal","2e kwartaal","3e kwartaal","4e kwartaal"]},o={narrow:["J","F","M","A","M","J","J","A","S","O","N","D"],abbreviated:["jan.","feb.","mrt.","apr.","mei","jun.","jul.","aug.","sep.","okt.","nov.","dec."],wide:["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"]},n={narrow:["Z","M","D","W","D","V","Z"],short:["zo","ma","di","wo","do","vr","za"],abbreviated:["zon","maa","din","woe","don","vri","zat"],wide:["zondag","maandag","dinsdag","woensdag","donderdag","vrijdag","zaterdag"]},s={narrow:{am:"AM",pm:"PM",midnight:"middernacht",noon:"het middaguur",morning:"'s ochtends",afternoon:"'s middags",evening:"'s avonds",night:"'s nachts"},abbreviated:{am:"AM",pm:"PM",midnight:"middernacht",noon:"het middaguur",morning:"'s ochtends",afternoon:"'s middags",evening:"'s avonds",night:"'s nachts"},wide:{am:"AM",pm:"PM",midnight:"middernacht",noon:"het middaguur",morning:"'s ochtends",afternoon:"'s middags",evening:"'s avonds",night:"'s nachts"}};function u(m){var f=Number(m);return f+"e"}var d={ordinalNumber:u,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(m){return Number(m)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide"})};i.a=d},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)e?/i,o=/\d+/i,n={narrow:/^([vn]\.? ?C\.?)/,abbreviated:/^([vn]\. ?Chr\.?)/,wide:/^((voor|na) Christus)/},s={any:[/^v/,/^n/]},u={narrow:/^[1234]/i,abbreviated:/^K[1234]/i,wide:/^[1234]e kwartaal/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[jfmasond]/i,abbreviated:/^(jan.|feb.|mrt.|apr.|mei|jun.|jul.|aug.|sep.|okt.|nov.|dec.)/i,wide:/^(januari|februari|maart|april|mei|juni|juli|augustus|september|oktober|november|december)/i},f={narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^jan/i,/^feb/i,/^m(r|a)/i,/^apr/i,/^mei/i,/^jun/i,/^jul/i,/^aug/i,/^sep/i,/^okt/i,/^nov/i,/^dec/i]},_={narrow:/^[zmdwv]/i,short:/^(zo|ma|di|wo|do|vr|za)/i,abbreviated:/^(zon|maa|din|woe|don|vri|zat)/i,wide:/^(zondag|maandag|dinsdag|woensdag|donderdag|vrijdag|zaterdag)/i},c={narrow:[/^z/i,/^m/i,/^d/i,/^w/i,/^d/i,/^v/i,/^z/i],any:[/^zo/i,/^ma/i,/^di/i,/^wo/i,/^do/i,/^vr/i,/^za/i]},v={any:/^(am|pm|middernacht|het middaguur|'s (ochtends|middags|avonds|nachts))/i},g={any:{am:/^am/i,pm:/^pm/i,midnight:/^middernacht/i,noon:/^het middaguur/i,morning:/ochtend/i,afternoon:/middag/i,evening:/avond/i,night:/nacht/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(726),a=e(727),r=e(728),o=e(729),n=e(730),s={code:"nl-BE",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"minder dan een seconde",other:"minder dan {{count}} seconden"},xSeconds:{one:"1 seconde",other:"{{count}} seconden"},halfAMinute:"een halve minuut",lessThanXMinutes:{one:"minder dan een minuut",other:"minder dan {{count}} minuten"},xMinutes:{one:"een minuut",other:"{{count}} minuten"},aboutXHours:{one:"ongeveer 1 uur",other:"ongeveer {{count}} uur"},xHours:{one:"1 uur",other:"{{count}} uur"},xDays:{one:"1 dag",other:"{{count}} dagen"},aboutXWeeks:{one:"ongeveer 1 week",other:"ongeveer {{count}} weken"},xWeeks:{one:"1 week",other:"{{count}} weken"},aboutXMonths:{one:"ongeveer 1 maand",other:"ongeveer {{count}} maanden"},xMonths:{one:"1 maand",other:"{{count}} maanden"},aboutXYears:{one:"ongeveer 1 jaar",other:"ongeveer {{count}} jaar"},xYears:{one:"1 jaar",other:"{{count}} jaar"},overXYears:{one:"meer dan 1 jaar",other:"meer dan {{count}} jaar"},almostXYears:{one:"bijna 1 jaar",other:"bijna {{count}} jaar"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"over "+s:s+" geleden":s}},function(l,i,e){var t=e(2),a={full:"EEEE d MMMM y",long:"d MMMM y",medium:"d MMM y",short:"dd-MM-y"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'om' {{time}}",long:"{{date}} 'om' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'vorige' eeee 'om' p",yesterday:"'gisteren om' p",today:"'vandaag om' p",tomorrow:"'morgen om' p",nextWeek:"eeee 'om' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["v.C.","n.C."],abbreviated:["v.Chr.","n.Chr."],wide:["voor Christus","na Christus"]},r={narrow:["1","2","3","4"],abbreviated:["K1","K2","K3","K4"],wide:["1e kwartaal","2e kwartaal","3e kwartaal","4e kwartaal"]},o={narrow:["J","F","M","A","M","J","J","A","S","O","N","D"],abbreviated:["jan.","feb.","mrt.","apr.","mei","jun.","jul.","aug.","sep.","okt.","nov.","dec."],wide:["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"]},n={narrow:["Z","M","D","W","D","V","Z"],short:["zo","ma","di","wo","do","vr","za"],abbreviated:["zon","maa","din","woe","don","vri","zat"],wide:["zondag","maandag","dinsdag","woensdag","donderdag","vrijdag","zaterdag"]},s={narrow:{am:"AM",pm:"PM",midnight:"middernacht",noon:"het middag",morning:"'s ochtends",afternoon:"'s namiddags",evening:"'s avonds",night:"'s nachts"},abbreviated:{am:"AM",pm:"PM",midnight:"middernacht",noon:"het middag",morning:"'s ochtends",afternoon:"'s namiddags",evening:"'s avonds",night:"'s nachts"},wide:{am:"AM",pm:"PM",midnight:"middernacht",noon:"het middag",morning:"'s ochtends",afternoon:"'s namiddags",evening:"'s avonds",night:"'s nachts"}};function u(m){var f=Number(m);return f+"e"}var d={ordinalNumber:u,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(m){return Number(m)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide"})};i.a=d},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)e?/i,o=/\d+/i,n={narrow:/^([vn]\.? ?C\.?)/,abbreviated:/^([vn]\. ?Chr\.?)/,wide:/^((voor|na) Christus)/},s={any:[/^v/,/^n/]},u={narrow:/^[1234]/i,abbreviated:/^K[1234]/i,wide:/^[1234]e kwartaal/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[jfmasond]/i,abbreviated:/^(jan.|feb.|mrt.|apr.|mei|jun.|jul.|aug.|sep.|okt.|nov.|dec.)/i,wide:/^(januari|februari|maart|april|mei|juni|juli|augustus|september|oktober|november|december)/i},f={narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^jan/i,/^feb/i,/^m(r|a)/i,/^apr/i,/^mei/i,/^jun/i,/^jul/i,/^aug/i,/^sep/i,/^okt/i,/^nov/i,/^dec/i]},_={narrow:/^[zmdwv]/i,short:/^(zo|ma|di|wo|do|vr|za)/i,abbreviated:/^(zon|maa|din|woe|don|vri|zat)/i,wide:/^(zondag|maandag|dinsdag|woensdag|donderdag|vrijdag|zaterdag)/i},c={narrow:[/^z/i,/^m/i,/^d/i,/^w/i,/^d/i,/^v/i,/^z/i],any:[/^zo/i,/^ma/i,/^di/i,/^wo/i,/^do/i,/^vr/i,/^za/i]},v={any:/^(am|pm|middernacht|het middaguur|'s (ochtends|middags|avonds|nachts))/i},g={any:{am:/^am/i,pm:/^pm/i,midnight:/^middernacht/i,noon:/^het middaguur/i,morning:/ochtend/i,afternoon:/middag/i,evening:/avond/i,night:/nacht/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(732),a=e(733),r=e(734),o=e(735),n=e(736),s={code:"nn",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=r;var t={lessThanXSeconds:{singular:"mindre enn eitt sekund",plural:"mindre enn {{count}} sekund"},xSeconds:{singular:"eitt sekund",plural:"{{count}} sekund"},halfAMinute:"eit halvt minutt",lessThanXMinutes:{singular:"mindre enn eitt minutt",plural:"mindre enn {{count}} minutt"},xMinutes:{singular:"eitt minutt",plural:"{{count}} minutt"},aboutXHours:{singular:"omtrent ein time",plural:"omtrent {{count}} timar"},xHours:{singular:"ein time",plural:"{{count}} timar"},xDays:{singular:"ein dag",plural:"{{count}} dagar"},aboutXWeeks:{singular:"omtrent ei veke",plural:"omtrent {{count}} veker"},xWeeks:{singular:"ei veke",plural:"{{count}} veker"},aboutXMonths:{singular:"omtrent ein månad",plural:"omtrent {{count}} månader"},xMonths:{singular:"ein månad",plural:"{{count}} månader"},aboutXYears:{singular:"omtrent eitt år",plural:"omtrent {{count}} år"},xYears:{singular:"eitt år",plural:"{{count}} år"},overXYears:{singular:"over eitt år",plural:"over {{count}} år"},almostXYears:{singular:"nesten eitt år",plural:"nesten {{count}} år"}},a=["null","ein","to","tre","fire","fem","seks","sju","åtte","ni","ti","elleve","tolv"];function r(o,n,s){s=s||{onlyNumeric:!1};var u=t[o],d;return typeof u=="string"?d=u:n===0||n>1?s.onlyNumeric?d=u.plural.replace("{{count}}",n):d=u.plural.replace("{{count}}",n<13?a[n]:n):d=u.singular,s.addSuffix?s.comparison>0?"om "+d:d+" sidan":d}},function(l,i,e){var t=e(2),a={full:"EEEE d. MMMM y",long:"d. MMMM y",medium:"d. MMM y",short:"dd.MM.y"},r={full:"'kl'. HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'kl.' {{time}}",long:"{{date}} 'kl.' {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'førre' eeee 'kl.' p",yesterday:"'i går kl.' p",today:"'i dag kl.' p",tomorrow:"'i morgon kl.' p",nextWeek:"EEEE 'kl.' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["f.Kr.","e.Kr."],abbreviated:["f.Kr.","e.Kr."],wide:["før Kristus","etter Kristus"]},r={narrow:["1","2","3","4"],abbreviated:["Q1","Q2","Q3","Q4"],wide:["1. kvartal","2. kvartal","3. kvartal","4. kvartal"]},o={narrow:["J","F","M","A","M","J","J","A","S","O","N","D"],abbreviated:["jan.","feb.","mars","apr.","mai","juni","juli","aug.","sep.","okt.","nov.","des."],wide:["januar","februar","mars","april","mai","juni","juli","august","september","oktober","november","desember"]},n={narrow:["S","M","T","O","T","F","L"],short:["su","må","ty","on","to","fr","lau"],abbreviated:["sun","mån","tys","ons","tor","fre","laur"],wide:["sundag","måndag","tysdag","onsdag","torsdag","fredag","laurdag"]},s={narrow:{am:"a",pm:"p",midnight:"midnatt",noon:"middag",morning:"på morg.",afternoon:"på etterm.",evening:"på kvelden",night:"på natta"},abbreviated:{am:"a.m.",pm:"p.m.",midnight:"midnatt",noon:"middag",morning:"på morg.",afternoon:"på etterm.",evening:"på kvelden",night:"på natta"},wide:{am:"a.m.",pm:"p.m.",midnight:"midnatt",noon:"middag",morning:"på morgonen",afternoon:"på ettermiddagen",evening:"på kvelden",night:"på natta"}};function u(m){var f=Number(m);return f+"."}var d={ordinalNumber:u,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(m){return Number(m)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide"})};i.a=d},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)\.?/i,o=/\d+/i,n={narrow:/^(f\.? ?Kr\.?|fvt\.?|e\.? ?Kr\.?|evt\.?)/i,abbreviated:/^(f\.? ?Kr\.?|fvt\.?|e\.? ?Kr\.?|evt\.?)/i,wide:/^(før Kristus|før vår tid|etter Kristus|vår tid)/i},s={any:[/^f/i,/^e/i]},u={narrow:/^[1234]/i,abbreviated:/^q[1234]/i,wide:/^[1234](\.)? kvartal/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[jfmasond]/i,abbreviated:/^(jan|feb|mars?|apr|mai|juni?|juli?|aug|sep|okt|nov|des)\.?/i,wide:/^(januar|februar|mars|april|mai|juni|juli|august|september|oktober|november|desember)/i},f={narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^mar/i,/^ap/i,/^mai/i,/^jun/i,/^jul/i,/^aug/i,/^s/i,/^o/i,/^n/i,/^d/i]},_={narrow:/^[smtofl]/i,short:/^(su|må|ty|on|to|fr|la)/i,abbreviated:/^(sun|mån|tys|ons|tor|fre|laur)/i,wide:/^(sundag|måndag|tysdag|onsdag|torsdag|fredag|laurdag)/i},c={any:[/^s/i,/^m/i,/^ty/i,/^o/i,/^to/i,/^f/i,/^l/i]},v={narrow:/^(midnatt|middag|(på) (morgonen|ettermiddagen|kvelden|natta)|[ap])/i,any:/^([ap]\.?\s?m\.?|midnatt|middag|(på) (morgonen|ettermiddagen|kvelden|natta))/i},g={any:{am:/^a(\.?\s?m\.?)?$/i,pm:/^p(\.?\s?m\.?)?$/i,midnight:/^midn/i,noon:/^midd/i,morning:/morgon/i,afternoon:/ettermiddag/i,evening:/kveld/i,night:/natt/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(738),a=e(739),r=e(740),o=e(741),n=e(742),s={code:"pl",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=o;function t(n,s){if(s===1)return n.one;var u=s%100;if(u<=20&&u>10)return n.other;var d=u%10;return d>=2&&d<=4?n.twoFour:n.other}function a(n,s,u){u=u||"regular";var d=t(n,s),m=d[u]||d;return m.replace("{{count}}",s)}var r={lessThanXSeconds:{one:{regular:"mniej niż sekunda",past:"mniej niż sekundę",future:"mniej niż sekundę"},twoFour:"mniej niż {{count}} sekundy",other:"mniej niż {{count}} sekund"},xSeconds:{one:{regular:"sekunda",past:"sekundę",future:"sekundę"},twoFour:"{{count}} sekundy",other:"{{count}} sekund"},halfAMinute:{one:"pół minuty",twoFour:"pół minuty",other:"pół minuty"},lessThanXMinutes:{one:{regular:"mniej niż minuta",past:"mniej niż minutę",future:"mniej niż minutę"},twoFour:"mniej niż {{count}} minuty",other:"mniej niż {{count}} minut"},xMinutes:{one:{regular:"minuta",past:"minutę",future:"minutę"},twoFour:"{{count}} minuty",other:"{{count}} minut"},aboutXHours:{one:{regular:"około godzina",past:"około godziny",future:"około godzinę"},twoFour:"około {{count}} godziny",other:"około {{count}} godzin"},xHours:{one:{regular:"godzina",past:"godzinę",future:"godzinę"},twoFour:"{{count}} godziny",other:"{{count}} godzin"},xDays:{one:{regular:"dzień",past:"dzień",future:"1 dzień"},twoFour:"{{count}} dni",other:"{{count}} dni"},aboutXWeeks:{one:"około tygodnia",twoFour:"około {{count}} tygodni",other:"około {{count}} tygodni"},xWeeks:{one:"tydzień",twoFour:"{{count}} tygodnie",other:"{{count}} tygodni"},aboutXMonths:{one:"około miesiąc",twoFour:"około {{count}} miesiące",other:"około {{count}} miesięcy"},xMonths:{one:"miesiąc",twoFour:"{{count}} miesiące",other:"{{count}} miesięcy"},aboutXYears:{one:"około rok",twoFour:"około {{count}} lata",other:"około {{count}} lat"},xYears:{one:"rok",twoFour:"{{count}} lata",other:"{{count}} lat"},overXYears:{one:"ponad rok",twoFour:"ponad {{count}} lata",other:"ponad {{count}} lat"},almostXYears:{one:"prawie rok",twoFour:"prawie {{count}} lata",other:"prawie {{count}} lat"}};function o(n,s,u){u=u||{};var d=r[n];return u.addSuffix?u.comparison>0?"za "+a(d,s,"future"):a(d,s,"past")+" temu":a(d,s)}},function(l,i,e){var t=e(2),a={full:"EEEE, do MMMM y",long:"do MMMM y",medium:"do MMM y",short:"dd.MM.y"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} {{time}}",long:"{{date}} {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=f;var t=e(13),a={masculine:"ostatni",feminine:"ostatnia"},r={masculine:"ten",feminine:"ta"},o={masculine:"następny",feminine:"następna"},n={0:"feminine",1:"masculine",2:"masculine",3:"feminine",4:"masculine",5:"masculine",6:"feminine"};function s(_,c,v,g){if(t.a(c,v,g))return r;if(_==="lastWeek")return a;if(_==="nextWeek")return o;throw new Error("Cannot determine adjectives for token ".concat(_))}function u(_,c,v,g){var P=c.getUTCDay(),h=s(_,c,v,g),y=n[P];return h[y]}function d(_,c,v,g){var P=u(_,c,v,g);return"'".concat(P,"' eeee 'o' p")}var m={lastWeek:d,yesterday:"'wczoraj o' p",today:"'dzisiaj o' p",tomorrow:"'jutro o' p",nextWeek:d,other:"P"};function f(_,c,v,g){var P=m[_];return typeof P=="function"?P(_,c,v,g):P}},function(l,i,e){var t=e(3);function a(c){var v=Number(c);return String(v)}var r={narrow:["p.n.e.","n.e."],abbreviated:["p.n.e.","n.e."],wide:["przed naszą erą","naszej ery"]},o={narrow:["1","2","3","4"],abbreviated:["I kw.","II kw.","III kw.","IV kw."],wide:["I kwartał","II kwartał","III kwartał","IV kwartał"]},n={narrow:["S","L","M","K","M","C","L","S","W","P","L","G"],abbreviated:["sty","lut","mar","kwi","maj","cze","lip","sie","wrz","paź","lis","gru"],wide:["styczeń","luty","marzec","kwiecień","maj","czerwiec","lipiec","sierpień","wrzesień","październik","listopad","grudzień"]},s={narrow:["s","l","m","k","m","c","l","s","w","p","l","g"],abbreviated:["sty","lut","mar","kwi","maj","cze","lip","sie","wrz","paź","lis","gru"],wide:["stycznia","lutego","marca","kwietnia","maja","czerwca","lipca","sierpnia","września","października","listopada","grudnia"]},u={narrow:["N","P","W","Ś","C","P","S"],short:["nie","pon","wto","śro","czw","pią","sob"],abbreviated:["niedz.","pon.","wt.","śr.","czw.","pt.","sob."],wide:["niedziela","poniedziałek","wtorek","środa","czwartek","piątek","sobota"]},d={narrow:["n","p","w","ś","c","p","s"],short:["nie","pon","wto","śro","czw","pią","sob"],abbreviated:["niedz.","pon.","wt.","śr.","czw.","pt.","sob."],wide:["niedziela","poniedziałek","wtorek","środa","czwartek","piątek","sobota"]},m={narrow:{am:"a",pm:"p",midnight:"półn.",noon:"poł",morning:"rano",afternoon:"popoł.",evening:"wiecz.",night:"noc"},abbreviated:{am:"AM",pm:"PM",midnight:"północ",noon:"południe",morning:"rano",afternoon:"popołudnie",evening:"wieczór",night:"noc"},wide:{am:"AM",pm:"PM",midnight:"północ",noon:"południe",morning:"rano",afternoon:"popołudnie",evening:"wieczór",night:"noc"}},f={narrow:{am:"a",pm:"p",midnight:"o półn.",noon:"w poł.",morning:"rano",afternoon:"po poł.",evening:"wiecz.",night:"w nocy"},abbreviated:{am:"AM",pm:"PM",midnight:"o północy",noon:"w południe",morning:"rano",afternoon:"po południu",evening:"wieczorem",night:"w nocy"},wide:{am:"AM",pm:"PM",midnight:"o północy",noon:"w południe",morning:"rano",afternoon:"po południu",evening:"wieczorem",night:"w nocy"}},_={ordinalNumber:a,era:t.a({values:r,defaultWidth:"wide"}),quarter:t.a({values:o,defaultWidth:"wide",argumentCallback:function(c){return Number(c)-1}}),month:t.a({values:n,defaultWidth:"wide",formattingValues:s,defaultFormattingWidth:"wide"}),day:t.a({values:u,defaultWidth:"wide",formattingValues:d,defaultFormattingWidth:"wide"}),dayPeriod:t.a({values:m,defaultWidth:"wide",formattingValues:f,defaultFormattingWidth:"wide"})};i.a=_},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)?/i,o=/\d+/i,n={narrow:/^(p\.?\s*n\.?\s*e\.?\s*|n\.?\s*e\.?\s*)/i,abbreviated:/^(p\.?\s*n\.?\s*e\.?\s*|n\.?\s*e\.?\s*)/i,wide:/^(przed\s*nasz(ą|a)\s*er(ą|a)|naszej\s*ery)/i},s={any:[/^p/i,/^n/i]},u={narrow:/^[1234]/i,abbreviated:/^(I|II|III|IV)\s*kw\.?/i,wide:/^(I|II|III|IV)\s*kwarta(ł|l)/i},d={narrow:[/1/i,/2/i,/3/i,/4/i],any:[/^I kw/i,/^II kw/i,/^III kw/i,/^IV kw/i]},m={narrow:/^[slmkcwpg]/i,abbreviated:/^(sty|lut|mar|kwi|maj|cze|lip|sie|wrz|pa(ź|z)|lis|gru)/i,wide:/^(stycznia|stycze(ń|n)|lutego|luty|marca|marzec|kwietnia|kwiecie(ń|n)|maja|maj|czerwca|czerwiec|lipca|lipiec|sierpnia|sierpie(ń|n)|wrze(ś|s)nia|wrzesie(ń|n)|pa(ź|z)dziernika|pa(ź|z)dziernik|listopada|listopad|grudnia|grudzie(ń|n))/i},f={narrow:[/^s/i,/^l/i,/^m/i,/^k/i,/^m/i,/^c/i,/^l/i,/^s/i,/^w/i,/^p/i,/^l/i,/^g/i],any:[/^st/i,/^lu/i,/^mar/i,/^k/i,/^maj/i,/^c/i,/^lip/i,/^si/i,/^w/i,/^p/i,/^lis/i,/^g/i]},_={narrow:/^[npwścs]/i,short:/^(nie|pon|wto|(ś|s)ro|czw|pi(ą|a)|sob)/i,abbreviated:/^(niedz|pon|wt|(ś|s)r|czw|pt|sob)\.?/i,wide:/^(niedziela|poniedzia(ł|l)ek|wtorek|(ś|s)roda|czwartek|pi(ą|a)tek|sobota)/i},c={narrow:[/^n/i,/^p/i,/^w/i,/^ś/i,/^c/i,/^p/i,/^s/i],abbreviated:[/^n/i,/^po/i,/^w/i,/^(ś|s)r/i,/^c/i,/^pt/i,/^so/i],any:[/^n/i,/^po/i,/^w/i,/^(ś|s)r/i,/^c/i,/^pi/i,/^so/i]},v={narrow:/^(^a$|^p$|pó(ł|l)n\.?|o\s*pó(ł|l)n\.?|po(ł|l)\.?|w\s*po(ł|l)\.?|po\s*po(ł|l)\.?|rano|wiecz\.?|noc|w\s*nocy)/i,any:/^(am|pm|pó(ł|l)noc|o\s*pó(ł|l)nocy|po(ł|l)udnie|w\s*po(ł|l)udnie|popo(ł|l)udnie|po\s*po(ł|l)udniu|rano|wieczór|wieczorem|noc|w\s*nocy)/i},g={narrow:{am:/^a$/i,pm:/^p$/i,midnight:/pó(ł|l)n/i,noon:/po(ł|l)/i,morning:/rano/i,afternoon:/po\s*po(ł|l)/i,evening:/wiecz/i,night:/noc/i},any:{am:/^am/i,pm:/^pm/i,midnight:/pó(ł|l)n/i,noon:/po(ł|l)/i,morning:/rano/i,afternoon:/po\s*po(ł|l)/i,evening:/wiecz/i,night:/noc/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(744),a=e(745),r=e(746),o=e(747),n=e(748),s={code:"pt",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"menos de um segundo",other:"menos de {{count}} segundos"},xSeconds:{one:"1 segundo",other:"{{count}} segundos"},halfAMinute:"meio minuto",lessThanXMinutes:{one:"menos de um minuto",other:"menos de {{count}} minutos"},xMinutes:{one:"1 minuto",other:"{{count}} minutos"},aboutXHours:{one:"aproximadamente 1 hora",other:"aproximadamente {{count}} horas"},xHours:{one:"1 hora",other:"{{count}} horas"},xDays:{one:"1 dia",other:"{{count}} dias"},aboutXWeeks:{one:"aproximadamente 1 mês",other:"aproximadamente {{count}} meses"},xWeeks:{one:"1 mês",other:"{{count}} meses"},aboutXMonths:{one:"aproximadamente 1 mês",other:"aproximadamente {{count}} meses"},xMonths:{one:"1 mês",other:"{{count}} meses"},aboutXYears:{one:"aproximadamente 1 ano",other:"aproximadamente {{count}} anos"},xYears:{one:"1 ano",other:"{{count}} anos"},overXYears:{one:"mais de 1 ano",other:"mais de {{count}} anos"},almostXYears:{one:"quase 1 ano",other:"quase {{count}} anos"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"daqui a "+s:"há "+s:s}},function(l,i,e){var t=e(2),a={full:"EEEE, d 'de' MMMM 'de' y",long:"d 'de' MMMM 'de' y",medium:"d 'de' MMM 'de' y",short:"dd/MM/y"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'às' {{time}}",long:"{{date}} 'às' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'na última' eeee 'às' p",yesterday:"'ontem às' p",today:"'hoje às' p",tomorrow:"'amanhã às' p",nextWeek:"eeee 'às' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3);function a(f){var _=Number(f);return _+"º"}var r={narrow:["aC","dC"],abbreviated:["a.C.","d.C."],wide:["antes de Cristo","depois de Cristo"]},o={narrow:["1","2","3","4"],abbreviated:["T1","T2","T3","T4"],wide:["1º trimestre","2º trimestre","3º trimestre","4º trimestre"]},n={narrow:["j","f","m","a","m","j","j","a","s","o","n","d"],abbreviated:["jan","fev","mar","abr","mai","jun","jul","ago","set","out","nov","dez"],wide:["janeiro","fevereiro","março","abril","maio","junho","julho","agosto","setembro","outubro","novembro","dezembro"]},s={narrow:["d","s","t","q","q","s","s"],short:["dom","seg","ter","qua","qui","sex","sáb"],abbreviated:["dom","seg","ter","qua","qui","sex","sáb"],wide:["domingo","segunda-feira","terça-feira","quarta-feira","quinta-feira","sexta-feira","sábado"]},u={narrow:{am:"AM",pm:"PM",midnight:"meia-noite",noon:"meio-dia",morning:"manhã",afternoon:"tarde",evening:"noite",night:"madrugada"},abbreviated:{am:"AM",pm:"PM",midnight:"meia-noite",noon:"meio-dia",morning:"manhã",afternoon:"tarde",evening:"noite",night:"madrugada"},wide:{am:"AM",pm:"PM",midnight:"meia-noite",noon:"meio-dia",morning:"manhã",afternoon:"tarde",evening:"noite",night:"madrugada"}},d={narrow:{am:"AM",pm:"PM",midnight:"meia-noite",noon:"meio-dia",morning:"da manhã",afternoon:"da tarde",evening:"da noite",night:"da madrugada"},abbreviated:{am:"AM",pm:"PM",midnight:"meia-noite",noon:"meio-dia",morning:"da manhã",afternoon:"da tarde",evening:"da noite",night:"da madrugada"},wide:{am:"AM",pm:"PM",midnight:"meia-noite",noon:"meio-dia",morning:"da manhã",afternoon:"da tarde",evening:"da noite",night:"da madrugada"}},m={ordinalNumber:a,era:t.a({values:r,defaultWidth:"wide"}),quarter:t.a({values:o,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:n,defaultWidth:"wide"}),day:t.a({values:s,defaultWidth:"wide"}),dayPeriod:t.a({values:u,defaultWidth:"wide",formattingValues:d,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(º|ª)?/i,o=/\d+/i,n={narrow:/^(ac|dc|a|d)/i,abbreviated:/^(a\.?\s?c\.?|a\.?\s?e\.?\s?c\.?|d\.?\s?c\.?|e\.?\s?c\.?)/i,wide:/^(antes de cristo|antes da era comum|depois de cristo|era comum)/i},s={any:[/^ac/i,/^dc/i],wide:[/^(antes de cristo|antes da era comum)/i,/^(depois de cristo|era comum)/i]},u={narrow:/^[1234]/i,abbreviated:/^T[1234]/i,wide:/^[1234](º|ª)? trimestre/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[jfmasond]/i,abbreviated:/^(jan|fev|mar|abr|mai|jun|jul|ago|set|out|nov|dez)/i,wide:/^(janeiro|fevereiro|março|abril|maio|junho|julho|agosto|setembro|outubro|novembro|dezembro)/i},f={narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^mar/i,/^ab/i,/^mai/i,/^jun/i,/^jul/i,/^ag/i,/^s/i,/^o/i,/^n/i,/^d/i]},_={narrow:/^[dstq]/i,short:/^(dom|seg|ter|qua|qui|sex|s[áa]b)/i,abbreviated:/^(dom|seg|ter|qua|qui|sex|s[áa]b)/i,wide:/^(domingo|segunda-?\s?feira|terça-?\s?feira|quarta-?\s?feira|quinta-?\s?feira|sexta-?\s?feira|s[áa]bado)/i},c={narrow:[/^d/i,/^s/i,/^t/i,/^q/i,/^q/i,/^s/i,/^s/i],any:[/^d/i,/^seg/i,/^t/i,/^qua/i,/^qui/i,/^sex/i,/^s[áa]/i]},v={narrow:/^(a|p|meia-?\s?noite|meio-?\s?dia|(da) (manh[ãa]|tarde|noite|madrugada))/i,any:/^([ap]\.?\s?m\.?|meia-?\s?noite|meio-?\s?dia|(da) (manh[ãa]|tarde|noite|madrugada))/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/^meia/i,noon:/^meio/i,morning:/manh[ãa]/i,afternoon:/tarde/i,evening:/noite/i,night:/madrugada/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(750),a=e(751),r=e(752),o=e(753),n=e(754),s={code:"pt-BR",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:0,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"menos de um segundo",other:"menos de {{count}} segundos"},xSeconds:{one:"1 segundo",other:"{{count}} segundos"},halfAMinute:"meio minuto",lessThanXMinutes:{one:"menos de um minuto",other:"menos de {{count}} minutos"},xMinutes:{one:"1 minuto",other:"{{count}} minutos"},aboutXHours:{one:"cerca de 1 hora",other:"cerca de {{count}} horas"},xHours:{one:"1 hora",other:"{{count}} horas"},xDays:{one:"1 dia",other:"{{count}} dias"},aboutXWeeks:{one:"cerca de 1 mês",other:"cerca de {{count}} meses"},xWeeks:{one:"1 mês",other:"{{count}} meses"},aboutXMonths:{one:"cerca de 1 mês",other:"cerca de {{count}} meses"},xMonths:{one:"1 mês",other:"{{count}} meses"},aboutXYears:{one:"cerca de 1 ano",other:"cerca de {{count}} anos"},xYears:{one:"1 ano",other:"{{count}} anos"},overXYears:{one:"mais de 1 ano",other:"mais de {{count}} anos"},almostXYears:{one:"quase 1 ano",other:"quase {{count}} anos"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"em "+s:"há "+s:s}},function(l,i,e){var t=e(2),a={full:"EEEE, d 'de' MMMM 'de' y",long:"d 'de' MMMM 'de' y",medium:"d MMM y",short:"dd/MM/yyyy"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'às' {{time}}",long:"{{date}} 'às' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:function(r,o,n){var s=r.getUTCDay(),u=s===0||s===6?"último":"última";return"'"+u+"' eeee 'às' p"},yesterday:"'ontem às' p",today:"'hoje às' p",tomorrow:"'amanhã às' p",nextWeek:"eeee 'às' p",other:"P"};function a(r,o,n,s){var u=t[r];return typeof u=="function"?u(o,n,s):u}},function(l,i,e){var t=e(3),a={narrow:["AC","DC"],abbreviated:["AC","DC"],wide:["antes de cristo","depois de cristo"]},r={narrow:["1","2","3","4"],abbreviated:["T1","T2","T3","T4"],wide:["1º trimestre","2º trimestre","3º trimestre","4º trimestre"]},o={narrow:["j","f","m","a","m","j","j","a","s","o","n","d"],abbreviated:["jan","fev","mar","abr","mai","jun","jul","ago","set","out","nov","dez"],wide:["janeiro","fevereiro","março","abril","maio","junho","julho","agosto","setembro","outubro","novembro","dezembro"]},n={narrow:["do","2ª","3ª","4ª","5ª","6ª","sá"],short:["do","2ª","3ª","4ª","5ª","6ª","sá"],abbreviated:["dom","seg","ter","qua","qui","sex","sáb"],wide:["domingo","segunda","terça","quarta","quinta","sexta","sábado"]},s={narrow:{am:"a",pm:"p",midnight:"mn",noon:"md",morning:"manhã",afternoon:"tarde",evening:"tarde",night:"noite"},abbreviated:{am:"AM",pm:"PM",midnight:"meia-noite",noon:"meio-dia",morning:"manhã",afternoon:"tarde",evening:"tarde",night:"noite"},wide:{am:"a.m.",pm:"p.m.",midnight:"meia-noite",noon:"meio-dia",morning:"manhã",afternoon:"tarde",evening:"tarde",night:"noite"}},u={narrow:{am:"a",pm:"p",midnight:"mn",noon:"md",morning:"da manhã",afternoon:"da tarde",evening:"da tarde",night:"da noite"},abbreviated:{am:"AM",pm:"PM",midnight:"meia-noite",noon:"meio-dia",morning:"da manhã",afternoon:"da tarde",evening:"da tarde",night:"da noite"},wide:{am:"a.m.",pm:"p.m.",midnight:"meia-noite",noon:"meio-dia",morning:"da manhã",afternoon:"da tarde",evening:"da tarde",night:"da noite"}};function d(f,_){var c=Number(f),v=_||{},g=String(v.unit);return g==="week"||g==="isoWeek"?c+"ª":c+"º"}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(5),a=e(4),r=/^(\d+)[ºªo]?/i,o=/\d+/i,n={narrow:/^(ac|dc|a|d)/i,abbreviated:/^(a\.?\s?c\.?|d\.?\s?c\.?)/i,wide:/^(antes de cristo|depois de cristo)/i},s={any:[/^ac/i,/^dc/i],wide:[/^antes de cristo/i,/^depois de cristo/i]},u={narrow:/^[1234]/i,abbreviated:/^T[1234]/i,wide:/^[1234](º)? trimestre/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[jfmajsond]/i,abbreviated:/^(jan|fev|mar|abr|mai|jun|jul|ago|set|out|nov|dez)/i,wide:/^(janeiro|fevereiro|março|abril|maio|junho|julho|agosto|setembro|outubro|novembro|dezembro)/i},f={narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^fev/i,/^mar/i,/^abr/i,/^mai/i,/^jun/i,/^jul/i,/^ago/i,/^set/i,/^out/i,/^nov/i,/^dez/i]},_={narrow:/^(dom|[23456]ª?|s[aá]b)/i,short:/^(dom|[23456]ª?|s[aá]b)/i,abbreviated:/^(dom|seg|ter|qua|qui|sex|s[aá]b)/i,wide:/^(domingo|(segunda|ter[cç]a|quarta|quinta|sexta)([- ]feira)?|s[aá]bado)/i},c={short:[/^d/i,/^2/i,/^3/i,/^4/i,/^5/i,/^6/i,/^s[aá]/i],narrow:[/^d/i,/^2/i,/^3/i,/^4/i,/^5/i,/^6/i,/^s[aá]/i],any:[/^d/i,/^seg/i,/^t/i,/^qua/i,/^qui/i,/^sex/i,/^s[aá]b/i]},v={narrow:/^(a|p|mn|md|(da) (manhã|tarde|noite))/i,any:/^([ap]\.?\s?m\.?|meia[-\s]noite|meio[-\s]dia|(da) (manhã|tarde|noite))/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/^mn|^meia[-\s]noite/i,noon:/^md|^meio[-\s]dia/i,morning:/manhã/i,afternoon:/tarde/i,evening:/tarde/i,night:/noite/i}},P={ordinalNumber:a.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:t.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:t.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:t.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:t.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:t.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(756),a=e(757),r=e(758),o=e(759),n=e(760),s={code:"ro",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"mai puțin de o secundă",other:"mai puțin de {{count}} secunde"},xSeconds:{one:"1 secundă",other:"{{count}} secunde"},halfAMinute:"jumătate de minut",lessThanXMinutes:{one:"mai puțin de un minut",other:"mai puțin de {{count}} minute"},xMinutes:{one:"1 minut",other:"{{count}} minute"},aboutXHours:{one:"circa 1 oră",other:"circa {{count}} ore"},xHours:{one:"1 oră",other:"{{count}} ore"},xDays:{one:"1 zi",other:"{{count}} zile"},aboutXWeeks:{one:"circa o săptămână",other:"circa {{count}} săptămâni"},xWeeks:{one:"1 săptămână",other:"{{count}} săptămâni"},aboutXMonths:{one:"circa 1 lună",other:"circa {{count}} luni"},xMonths:{one:"1 lună",other:"{{count}} luni"},aboutXYears:{one:"circa 1 an",other:"circa {{count}} ani"},xYears:{one:"1 an",other:"{{count}} ani"},overXYears:{one:"peste 1 an",other:"peste {{count}} ani"},almostXYears:{one:"aproape 1 an",other:"aproape {{count}} ani"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"în "+s:s+" în urmă":s}},function(l,i,e){var t=e(2),a={full:"EEEE, d MMMM yyyy",long:"d MMMM yyyy",medium:"d MMM yyyy",short:"dd/MM/yyyy"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'la' {{time}}",long:"{{date}} 'la' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"eeee 'trecută la' p",yesterday:"'ieri la' p",today:"'astăzi la' p",tomorrow:"'mâine la' p",nextWeek:"eeee 'viitoare la' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["Î","D"],abbreviated:["Î.d.C.","D.C."],wide:["Înainte de Cristos","După Cristos"]},r={narrow:["1","2","3","4"],abbreviated:["T1","T2","T3","T4"],wide:["primul trimestru","al doilea trimestru","al treilea trimestru","al patrulea trimestru"]},o={narrow:["I","F","M","A","M","I","I","A","S","O","N","D"],abbreviated:["ian","feb","mar","apr","mai","iun","iul","aug","sep","oct","noi","dec"],wide:["ianuarie","februarie","martie","aprilie","mai","iunie","iulie","august","septembrie","octombrie","noiembrie","decembrie"]},n={narrow:["d","l","m","m","j","v","s"],short:["du","lu","ma","mi","jo","vi","sâ"],abbreviated:["dum","lun","mar","mie","joi","vin","sâm"],wide:["duminică","luni","marți","miercuri","joi","vineri","sâmbătă"]},s={narrow:{am:"a",pm:"p",midnight:"mn",noon:"ami",morning:"dim",afternoon:"da",evening:"s",night:"n"},abbreviated:{am:"AM",pm:"PM",midnight:"miezul nopții",noon:"amiază",morning:"dimineață",afternoon:"după-amiază",evening:"seară",night:"noapte"},wide:{am:"a.m.",pm:"p.m.",midnight:"miezul nopții",noon:"amiază",morning:"dimineață",afternoon:"după-amiază",evening:"seară",night:"noapte"}},u={narrow:{am:"a",pm:"p",midnight:"mn",noon:"amiază",morning:"dimineață",afternoon:"după-amiază",evening:"seară",night:"noapte"},abbreviated:{am:"AM",pm:"PM",midnight:"miezul nopții",noon:"amiază",morning:"dimineață",afternoon:"după-amiază",evening:"seară",night:"noapte"},wide:{am:"a.m.",pm:"p.m.",midnight:"miezul nopții",noon:"amiază",morning:"dimineață",afternoon:"după-amiază",evening:"seară",night:"noapte"}};function d(f){var _=Number(f);return String(_)}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)?/i,o=/\d+/i,n={narrow:/^(Î|D)/i,abbreviated:/^(Î\.?\s?d\.?\s?C\.?|Î\.?\s?e\.?\s?n\.?|D\.?\s?C\.?|e\.?\s?n\.?)/i,wide:/^(Înainte de Cristos|Înaintea erei noastre|După Cristos|Era noastră)/i},s={any:[/^ÎC/i,/^DC/i],wide:[/^(Înainte de Cristos|Înaintea erei noastre)/i,/^(După Cristos|Era noastră)/i]},u={narrow:/^[1234]/i,abbreviated:/^T[1234]/i,wide:/^trimestrul [1234]/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[ifmaasond]/i,abbreviated:/^(ian|feb|mar|apr|mai|iun|iul|aug|sep|oct|noi|dec)/i,wide:/^(ianuarie|februarie|martie|aprilie|mai|iunie|iulie|august|septembrie|octombrie|noiembrie|decembrie)/i},f={narrow:[/^i/i,/^f/i,/^m/i,/^a/i,/^m/i,/^i/i,/^i/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ia/i,/^f/i,/^mar/i,/^ap/i,/^mai/i,/^iun/i,/^iul/i,/^au/i,/^s/i,/^o/i,/^n/i,/^d/i]},_={narrow:/^[dlmjvs]/i,short:/^(d|l|ma|mi|j|v|s)/i,abbreviated:/^(dum|lun|mar|mie|jo|vi|sâ)/i,wide:/^(duminica|luni|marţi|miercuri|joi|vineri|sâmbătă)/i},c={narrow:[/^d/i,/^l/i,/^m/i,/^m/i,/^j/i,/^v/i,/^s/i],any:[/^d/i,/^l/i,/^ma/i,/^mi/i,/^j/i,/^v/i,/^s/i]},v={narrow:/^(a|p|mn|a|(dimineaţa|după-amiaza|seara|noaptea))/i,any:/^([ap]\.?\s?m\.?|miezul nopții|amiaza|(dimineaţa|după-amiaza|seara|noaptea))/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/^mn/i,noon:/amiaza/i,morning:/dimineaţa/i,afternoon:/după-amiaza/i,evening:/seara/i,night:/noaptea/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(762),a=e(763),r=e(764),o=e(765),n=e(766),s={code:"ru",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=o;function t(n,s){if(n.one!==void 0&&s===1)return n.one;var u=s%10,d=s%100;return u===1&&d!==11?n.singularNominative.replace("{{count}}",s):u>=2&&u<=4&&(d<10||d>20)?n.singularGenitive.replace("{{count}}",s):n.pluralGenitive.replace("{{count}}",s)}function a(n){return function(s,u){return u.addSuffix?u.comparison>0?n.future?t(n.future,s):"через "+t(n.regular,s):n.past?t(n.past,s):t(n.regular,s)+" назад":t(n.regular,s)}}var r={lessThanXSeconds:a({regular:{one:"меньше секунды",singularNominative:"меньше {{count}} секунды",singularGenitive:"меньше {{count}} секунд",pluralGenitive:"меньше {{count}} секунд"},future:{one:"меньше, чем через секунду",singularNominative:"меньше, чем через {{count}} секунду",singularGenitive:"меньше, чем через {{count}} секунды",pluralGenitive:"меньше, чем через {{count}} секунд"}}),xSeconds:a({regular:{singularNominative:"{{count}} секунда",singularGenitive:"{{count}} секунды",pluralGenitive:"{{count}} секунд"},past:{singularNominative:"{{count}} секунду назад",singularGenitive:"{{count}} секунды назад",pluralGenitive:"{{count}} секунд назад"},future:{singularNominative:"через {{count}} секунду",singularGenitive:"через {{count}} секунды",pluralGenitive:"через {{count}} секунд"}}),halfAMinute:function(n,s){return s.addSuffix?s.comparison>0?"через полминуты":"полминуты назад":"полминуты"},lessThanXMinutes:a({regular:{one:"меньше минуты",singularNominative:"меньше {{count}} минуты",singularGenitive:"меньше {{count}} минут",pluralGenitive:"меньше {{count}} минут"},future:{one:"меньше, чем через минуту",singularNominative:"меньше, чем через {{count}} минуту",singularGenitive:"меньше, чем через {{count}} минуты",pluralGenitive:"меньше, чем через {{count}} минут"}}),xMinutes:a({regular:{singularNominative:"{{count}} минута",singularGenitive:"{{count}} минуты",pluralGenitive:"{{count}} минут"},past:{singularNominative:"{{count}} минуту назад",singularGenitive:"{{count}} минуты назад",pluralGenitive:"{{count}} минут назад"},future:{singularNominative:"через {{count}} минуту",singularGenitive:"через {{count}} минуты",pluralGenitive:"через {{count}} минут"}}),aboutXHours:a({regular:{singularNominative:"около {{count}} часа",singularGenitive:"около {{count}} часов",pluralGenitive:"около {{count}} часов"},future:{singularNominative:"приблизительно через {{count}} час",singularGenitive:"приблизительно через {{count}} часа",pluralGenitive:"приблизительно через {{count}} часов"}}),xHours:a({regular:{singularNominative:"{{count}} час",singularGenitive:"{{count}} часа",pluralGenitive:"{{count}} часов"}}),xDays:a({regular:{singularNominative:"{{count}} день",singularGenitive:"{{count}} дня",pluralGenitive:"{{count}} дней"}}),aboutXWeeks:a({regular:{singularNominative:"около {{count}} недели",singularGenitive:"около {{count}} недель",pluralGenitive:"около {{count}} недель"},future:{singularNominative:"приблизительно через {{count}} неделю",singularGenitive:"приблизительно через {{count}} недели",pluralGenitive:"приблизительно через {{count}} недель"}}),xWeeks:a({regular:{singularNominative:"{{count}} неделя",singularGenitive:"{{count}} недели",pluralGenitive:"{{count}} недель"}}),aboutXMonths:a({regular:{singularNominative:"около {{count}} месяца",singularGenitive:"около {{count}} месяцев",pluralGenitive:"около {{count}} месяцев"},future:{singularNominative:"приблизительно через {{count}} месяц",singularGenitive:"приблизительно через {{count}} месяца",pluralGenitive:"приблизительно через {{count}} месяцев"}}),xMonths:a({regular:{singularNominative:"{{count}} месяц",singularGenitive:"{{count}} месяца",pluralGenitive:"{{count}} месяцев"}}),aboutXYears:a({regular:{singularNominative:"около {{count}} года",singularGenitive:"около {{count}} лет",pluralGenitive:"около {{count}} лет"},future:{singularNominative:"приблизительно через {{count}} год",singularGenitive:"приблизительно через {{count}} года",pluralGenitive:"приблизительно через {{count}} лет"}}),xYears:a({regular:{singularNominative:"{{count}} год",singularGenitive:"{{count}} года",pluralGenitive:"{{count}} лет"}}),overXYears:a({regular:{singularNominative:"больше {{count}} года",singularGenitive:"больше {{count}} лет",pluralGenitive:"больше {{count}} лет"},future:{singularNominative:"больше, чем через {{count}} год",singularGenitive:"больше, чем через {{count}} года",pluralGenitive:"больше, чем через {{count}} лет"}}),almostXYears:a({regular:{singularNominative:"почти {{count}} год",singularGenitive:"почти {{count}} года",pluralGenitive:"почти {{count}} лет"},future:{singularNominative:"почти через {{count}} год",singularGenitive:"почти через {{count}} года",pluralGenitive:"почти через {{count}} лет"}})};function o(n,s,u){return u=u||{},r[n](s,u)}},function(l,i,e){var t=e(2),a={full:"EEEE, do MMMM y 'г.'",long:"do MMMM y 'г.'",medium:"d MMM y 'г.'",short:"dd.MM.y"},r={full:"H:mm:ss zzzz",long:"H:mm:ss z",medium:"H:mm:ss",short:"H:mm"},o={any:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"any"})};i.a=n},function(l,i,e){i.a=u;var t=e(13),a=["воскресенье","понедельник","вторник","среду","четверг","пятницу","субботу"];function r(d){var m=a[d];switch(d){case 0:return"'в прошлое "+m+" в' p";case 1:case 2:case 4:return"'в прошлый "+m+" в' p";case 3:case 5:case 6:return"'в прошлую "+m+" в' p"}}function o(d){var m=a[d];return d===2?"'во "+m+" в' p":"'в "+m+" в' p"}function n(d){var m=a[d];switch(d){case 0:return"'в следующее "+m+" в' p";case 1:case 2:case 4:return"'в следующий "+m+" в' p";case 3:case 5:case 6:return"'в следующую "+m+" в' p"}}var s={lastWeek:function(d,m,f){var _=d.getUTCDay();return t.a(d,m,f)?o(_):r(_)},yesterday:"'вчера в' p",today:"'сегодня в' p",tomorrow:"'завтра в' p",nextWeek:function(d,m,f){var _=d.getUTCDay();return t.a(d,m,f)?o(_):n(_)},other:"P"};function u(d,m,f,_){var c=s[d];return typeof c=="function"?c(m,f,_):c}},function(l,i,e){var t=e(3),a={narrow:["до н.э.","н.э."],abbreviated:["до н. э.","н. э."],wide:["до нашей эры","нашей эры"]},r={narrow:["1","2","3","4"],abbreviated:["1-й кв.","2-й кв.","3-й кв.","4-й кв."],wide:["1-й квартал","2-й квартал","3-й квартал","4-й квартал"]},o={narrow:["Я","Ф","М","А","М","И","И","А","С","О","Н","Д"],abbreviated:["янв.","фев.","март","апр.","май","июнь","июль","авг.","сент.","окт.","нояб.","дек."],wide:["январь","февраль","март","апрель","май","июнь","июль","август","сентябрь","октябрь","ноябрь","декабрь"]},n={narrow:["Я","Ф","М","А","М","И","И","А","С","О","Н","Д"],abbreviated:["янв.","фев.","мар.","апр.","мая","июн.","июл.","авг.","сент.","окт.","нояб.","дек."],wide:["января","февраля","марта","апреля","мая","июня","июля","августа","сентября","октября","ноября","декабря"]},s={narrow:["В","П","В","С","Ч","П","С"],short:["вс","пн","вт","ср","чт","пт","сб"],abbreviated:["вск","пнд","втр","срд","чтв","птн","суб"],wide:["воскресенье","понедельник","вторник","среда","четверг","пятница","суббота"]},u={narrow:{am:"ДП",pm:"ПП",midnight:"полн.",noon:"полд.",morning:"утро",afternoon:"день",evening:"веч.",night:"ночь"},abbreviated:{am:"ДП",pm:"ПП",midnight:"полн.",noon:"полд.",morning:"утро",afternoon:"день",evening:"веч.",night:"ночь"},wide:{am:"ДП",pm:"ПП",midnight:"полночь",noon:"полдень",morning:"утро",afternoon:"день",evening:"вечер",night:"ночь"}},d={narrow:{am:"ДП",pm:"ПП",midnight:"полн.",noon:"полд.",morning:"утра",afternoon:"дня",evening:"веч.",night:"ночи"},abbreviated:{am:"ДП",pm:"ПП",midnight:"полн.",noon:"полд.",morning:"утра",afternoon:"дня",evening:"веч.",night:"ночи"},wide:{am:"ДП",pm:"ПП",midnight:"полночь",noon:"полдень",morning:"утра",afternoon:"дня",evening:"вечера",night:"ночи"}};function m(_,c){var v=c||{},g=String(v.unit),P;return g==="date"?P="-е":g==="week"||g==="minute"||g==="second"?P="-я":P="-й",_+P}var f={ordinalNumber:m,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(_){return Number(_)-1}}),month:t.a({values:o,defaultWidth:"wide",formattingValues:n,defaultFormattingWidth:"wide"}),day:t.a({values:s,defaultWidth:"wide"}),dayPeriod:t.a({values:u,defaultWidth:"any",formattingValues:d,defaultFormattingWidth:"wide"})};i.a=f},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(-?(е|я|й|ое|ье|ая|ья|ый|ой|ий|ый))?/i,o=/\d+/i,n={narrow:/^((до )?н\.?\s?э\.?)/i,abbreviated:/^((до )?н\.?\s?э\.?)/i,wide:/^(до нашей эры|нашей эры|наша эра)/i},s={any:[/^д/i,/^н/i]},u={narrow:/^[1234]/i,abbreviated:/^[1234](-?[ыои]?й?)? кв.?/i,wide:/^[1234](-?[ыои]?й?)? квартал/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[яфмаисонд]/i,abbreviated:/^(янв|фев|март?|апр|ма[йя]|июн[ья]?|июл[ья]?|авг|сент?|окт|нояб?|дек)/i,wide:/^(январ[ья]|феврал[ья]|марта?|апрел[ья]|ма[йя]|июн[ья]|июл[ья]|августа?|сентябр[ья]|октябр[ья]|октябр[ья]|ноябр[ья]|декабр[ья])/i},f={narrow:[/^я/i,/^ф/i,/^м/i,/^а/i,/^м/i,/^и/i,/^и/i,/^а/i,/^с/i,/^о/i,/^н/i,/^я/i],any:[/^я/i,/^ф/i,/^мар/i,/^ап/i,/^ма[йя]/i,/^июн/i,/^июл/i,/^ав/i,/^с/i,/^о/i,/^н/i,/^д/i]},_={narrow:/^[впсч]/i,short:/^(вс|во|пн|по|вт|ср|чт|че|пт|пя|сб|су)\.?/i,abbreviated:/^(вск|вос|пнд|пон|втр|вто|срд|сре|чтв|чет|птн|пят|суб).?/i,wide:/^(воскресень[ея]|понедельника?|вторника?|сред[аы]|четверга?|пятниц[аы]|суббот[аы])/i},c={narrow:[/^в/i,/^п/i,/^в/i,/^с/i,/^ч/i,/^п/i,/^с/i],any:[/^в[ос]/i,/^п[он]/i,/^в/i,/^ср/i,/^ч/i,/^п[ят]/i,/^с[уб]/i]},v={narrow:/^([дп]п|полн\.?|полд\.?|утр[оа]|день|дня|веч\.?|ноч[ьи])/i,abbreviated:/^([дп]п|полн\.?|полд\.?|утр[оа]|день|дня|веч\.?|ноч[ьи])/i,wide:/^([дп]п|полночь|полдень|утр[оа]|день|дня|вечера?|ноч[ьи])/i},g={any:{am:/^дп/i,pm:/^пп/i,midnight:/^полн/i,noon:/^полд/i,morning:/^у/i,afternoon:/^д[ен]/i,evening:/^в/i,night:/^н/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"wide",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(768),a=e(769),r=e(770),o=e(771),n=e(772),s={code:"sk",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=d;function t(m,f){return f===1?m.one:f>=2&&f<=4?m.twoFour:m.other}function a(m,f,_){var c=t(m,f),v=c[_]||c;return v.replace("{{count}}",f)}function r(m){var f=["lessThan","about","over","almost"].filter(function(_){return!!m.match(new RegExp("^"+_))});return f[0]}function o(m){var f="";return m==="almost"&&(f="takmer"),m==="about"&&(f="približne"),f.length>0?f+" ":""}function n(m){var f="";return m==="lessThan"&&(f="menej než"),m==="over"&&(f="viac než"),f.length>0?f+" ":""}function s(m){return m.charAt(0).toLowerCase()+m.slice(1)}var u={xSeconds:{one:{regular:"sekunda",past:"sekundou",future:"sekundu"},twoFour:{regular:"{{count}} sekundy",past:"{{count}} sekundami",future:"{{count}} sekundy"},other:{regular:"{{count}} sekúnd",past:"{{count}} sekundami",future:"{{count}} sekúnd"}},halfAMinute:{other:{regular:"pol minúty",past:"pol minútou",future:"pol minúty"}},xMinutes:{one:{regular:"minúta",past:"minútou",future:"minútu"},twoFour:{regular:"{{count}} minúty",past:"{{count}} minútami",future:"{{count}} minúty"},other:{regular:"{{count}} minút",past:"{{count}} minútami",future:"{{count}} minút"}},xHours:{one:{regular:"hodina",past:"hodinou",future:"hodinu"},twoFour:{regular:"{{count}} hodiny",past:"{{count}} hodinami",future:"{{count}} hodiny"},other:{regular:"{{count}} hodín",past:"{{count}} hodinami",future:"{{count}} hodín"}},xDays:{one:{regular:"deň",past:"dňom",future:"deň"},twoFour:{regular:"{{count}} dni",past:"{{count}} dňami",future:"{{count}} dni"},other:{regular:"{{count}} dní",past:"{{count}} dňami",future:"{{count}} dní"}},xWeeks:{one:{regular:"mesiac",past:"mesiacom",future:"mesiac"},twoFour:{regular:"{{count}} mesiace",past:"{{count}} mesiacmi",future:"{{count}} mesiace"},other:{regular:"{{count}} mesiacov",past:"{{count}} mesiacmi",future:"{{count}} mesiacov"}},xMonths:{one:{regular:"mesiac",past:"mesiacom",future:"mesiac"},twoFour:{regular:"{{count}} mesiace",past:"{{count}} mesiacmi",future:"{{count}} mesiace"},other:{regular:"{{count}} mesiacov",past:"{{count}} mesiacmi",future:"{{count}} mesiacov"}},xYears:{one:{regular:"rok",past:"rokom",future:"rok"},twoFour:{regular:"{{count}} roky",past:"{{count}} rokmi",future:"{{count}} roky"},other:{regular:"{{count}} rokov",past:"{{count}} rokmi",future:"{{count}} rokov"}}};function d(m,f,_){_=_||{};var c=r(m)||"",v=s(m.substring(c.length)),g=u[v];return _.addSuffix?_.comparison>0?o(c)+"o "+n(c)+a(g,f,"future"):o(c)+"pred "+n(c)+a(g,f,"past"):o(c)+n(c)+a(g,f,"regular")}},function(l,i,e){var t=e(2),a={full:"EEEE d. MMMM y",long:"d. MMMM y",medium:"d. M. y",short:"d. M. y"},r={full:"H:mm:ss zzzz",long:"H:mm:ss z",medium:"H:mm:ss",short:"H:mm"},o={full:"{{date}}, {{time}}",long:"{{date}}, {{time}}",medium:"{{date}}, {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=u;var t=e(13),a=["nedeľu","pondelok","utorok","stredu","štvrtok","piatok","sobotu"];function r(d){var m=a[d];switch(d){case 0:case 3:case 6:return"'minulú "+m+" o' p";default:return"'minulý' eeee 'o' p"}}function o(d){var m=a[d];return d===4?"'vo' eeee 'o' p":"'v "+m+" o' p"}function n(d){var m=a[d];switch(d){case 0:case 4:case 6:return"'budúcu' "+m+" 'o' p";default:return"'budúci' eeee 'o' p"}}var s={lastWeek:function(d,m,f){var _=d.getUTCDay();return t.a(d,m,f)?o(_):r(_)},yesterday:"'včera o' p",today:"'dnes o' p",tomorrow:"'zajtra o' p",nextWeek:function(d,m,f){var _=d.getUTCDay();return t.a(d,m,f)?o(_):n(_)},other:"P"};function u(d,m,f,_){var c=s[d];return typeof c=="function"?c(m,f,_):c}},function(l,i,e){var t=e(3),a={narrow:["pred Kr.","po Kr."],abbreviated:["pred Kr.","po Kr."],wide:["pred Kristom","po Kristovi"]},r={narrow:["1","2","3","4"],abbreviated:["Q1","Q2","Q3","Q4"],wide:["1. štvrťrok","2. štvrťrok","3. štvrťrok","4. štvrťrok"]},o={narrow:["j","f","m","a","m","j","j","a","s","o","n","d"],abbreviated:["jan","feb","mar","apr","máj","jún","júl","aug","sep","okt","nov","dec"],wide:["január","február","marec","apríl","máj","jún","júl","august","september","október","november","december"]},n={narrow:["j","f","m","a","m","j","j","a","s","o","n","d"],abbreviated:["jan","feb","mar","apr","máj","jún","júl","aug","sep","okt","nov","dec"],wide:["januára","februára","marca","apríla","mája","júna","júla","augusta","septembra","októbra","novembra","decembra"]},s={narrow:["n","p","u","s","š","p","s"],short:["ne","po","ut","st","št","pi","so"],abbreviated:["ne","po","ut","st","št","pi","so"],wide:["nedeľa","pondelok","utorok","streda","štvrtok","piatok","sobota"]},u={narrow:{am:"AM",pm:"PM",midnight:"poln.",noon:"pol.",morning:"ráno",afternoon:"pop.",evening:"več.",night:"noc"},abbreviated:{am:"AM",pm:"PM",midnight:"poln.",noon:"pol.",morning:"ráno",afternoon:"popol.",evening:"večer",night:"noc"},wide:{am:"AM",pm:"PM",midnight:"polnoc",noon:"poludnie",morning:"ráno",afternoon:"popoludnie",evening:"večer",night:"noc"}},d={narrow:{am:"AM",pm:"PM",midnight:"o poln.",noon:"nap.",morning:"ráno",afternoon:"pop.",evening:"več.",night:"v n."},abbreviated:{am:"AM",pm:"PM",midnight:"o poln.",noon:"napol.",morning:"ráno",afternoon:"popol.",evening:"večer",night:"v noci"},wide:{am:"AM",pm:"PM",midnight:"o polnoci",noon:"napoludnie",morning:"ráno",afternoon:"popoludní",evening:"večer",night:"v noci"}};function m(_,c){var v=Number(_);return v+"."}var f={ordinalNumber:m,era:t.a({values:a}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(_){return Number(_)-1}}),month:t.a({values:o,defaultWidth:"wide",formattingValues:n,defaultFormattingWidth:"wide"}),day:t.a({values:s,defaultWidth:"wide"}),dayPeriod:t.a({values:u,defaultWidth:"wide",formattingValues:d,defaultFormattingWidth:"wide"})};i.a=f},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)\.?/i,o=/\d+/i,n={narrow:/^(pred Kr\.|pred n\. l\.|po Kr\.|n\. l\.)/i,abbreviated:/^(pred Kr\.|pred n\. l\.|po Kr\.|n\. l\.)/i,wide:/^(pred Kristom|pred na[šs][íi]m letopo[čc]tom|po Kristovi|n[áa][šs]ho letopo[čc]tu)/i},s={any:[/^pr/i,/^(po|n)/i]},u={narrow:/^[1234]/i,abbreviated:/^q[1234]/i,wide:/^[1234]\. [šs]tvr[ťt]rok/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[jfmasond]/i,abbreviated:/^(jan|feb|mar|apr|m[áa]j|j[úu]n|j[úu]l|aug|sep|okt|nov|dec)/i,wide:/^(janu[áa]ra?|febru[áa]ra?|(marec|marca)|apr[íi]la?|m[áa]ja?|j[úu]na?|j[úu]la?|augusta?|(september|septembra)|(okt[óo]ber|okt[óo]bra)|(november|novembra)|(december|decembra))/i},f={narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^mar/i,/^ap/i,/^m[áa]j/i,/^j[úu]n/i,/^j[úu]l/i,/^au/i,/^s/i,/^o/i,/^n/i,/^d/i]},_={narrow:/^[npusšp]/i,short:/^(ne|po|ut|st|št|pi|so)/i,abbreviated:/^(ne|po|ut|st|št|pi|so)/i,wide:/^(nede[ľl]a|pondelok|utorok|streda|[šs]tvrtok|piatok|sobota])/i},c={narrow:[/^n/i,/^p/i,/^u/i,/^s/i,/^š/i,/^p/i,/^s/i],any:[/^n/i,/^po/i,/^u/i,/^st/i,/^(št|stv)/i,/^pi/i,/^so/i]},v={narrow:/^(am|pm|(o )?poln\.?|(nap\.?|pol\.?)|r[áa]no|pop\.?|ve[čc]\.?|(v n\.?|noc))/i,abbreviated:/^(am|pm|(o )?poln\.?|(napol\.?|pol\.?)|r[áa]no|pop\.?|ve[čc]er|(v )?noci?)/i,any:/^(am|pm|(o )?polnoci?|(na)?poludnie|r[áa]no|popoludn(ie|í|i)|ve[čc]er|(v )?noci?)/i},g={any:{am:/^am/i,pm:/^pm/i,midnight:/poln/i,noon:/^(nap|(na)?pol(\.|u))/i,morning:/^r[áa]no/i,afternoon:/^pop/i,evening:/^ve[čc]/i,night:/^(noc|v n\.)/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(774),a=e(775),r=e(776),o=e(777),n=e(778),s={code:"sl",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=n;var t={lessThanXSeconds:{one:"manj kot {{count}} sekunda",two:"manj kot {{count}} sekundi",few:"manj kot {{count}} sekunde",other:"manj kot {{count}} sekund"},xSeconds:{one:"{{count}} sekunda",two:"{{count}} sekundi",few:"{{count}} sekunde",other:"{{count}} sekund"},halfAMinute:"pol minute",lessThanXMinutes:{one:"manj kot {{count}} minuta",two:"manj kot {{count}} minuti",few:"manj kot {{count}} minute",other:"manj kot {{count}} minut"},xMinutes:{one:"{{count}} minuta",two:"{{count}} minuti",few:"{{count}} minute",other:"{{count}} minut"},aboutXHours:{one:"približno {{count}} ura",two:"približno {{count}} uri",few:"približno {{count}} ure",other:"približno {{count}} ur"},xHours:{one:"{{count}} ura",two:"{{count}} uri",few:"{{count}} ure",other:"{{count}} ur"},xDays:{one:"{{count}} dan",two:"{{count}} dni",few:"{{count}} dni",other:"{{count}} dni"},aboutXWeeks:{one:"približno {{count}} teden",two:"približno {{count}} tedna",few:"približno {{count}} tedne",other:"približno {{count}} tednov"},xWeeks:{one:"{{count}} teden",two:"{{count}} tedna",few:"{{count}} tedne",other:"{{count}} tednov"},aboutXMonths:{one:"približno {{count}} mesec",two:"približno {{count}} meseca",few:"približno {{count}} mesece",other:"približno {{count}} mesecev"},xMonths:{one:"{{count}} mesec",two:"{{count}} meseca",few:"{{count}} meseci",other:"{{count}} mesecev"},aboutXYears:{one:"približno {{count}} leto",two:"približno {{count}} leti",few:"približno {{count}} leta",other:"približno {{count}} let"},xYears:{one:"{{count}} leto",two:"{{count}} leti",few:"{{count}} leta",other:"{{count}} let"},overXYears:{one:"več kot {{count}} leto",two:"več kot {{count}} leti",few:"več kot {{count}} leta",other:"več kot {{count}} let"},almostXYears:{one:"skoraj {{count}} leto",two:"skoraj {{count}} leti",few:"skoraj {{count}} leta",other:"skoraj {{count}} let"}},a={lessThanXSeconds:{one:"manj kot {{count}} sekundo",two:"manj kot {{count}} sekundama",few:"manj kot {{count}} sekundami",other:"manj kot {{count}} sekundami"},xSeconds:{one:"{{count}} sekundo",two:"{{count}} sekundama",few:"{{count}} sekundami",other:"{{count}} sekundami"},halfAMinute:"pol minute",lessThanXMinutes:{one:"manj kot {{count}} minuto",two:"manj kot {{count}} minutama",few:"manj kot {{count}} minutami",other:"manj kot {{count}} minutami"},xMinutes:{one:"{{count}} minuto",two:"{{count}} minutama",few:"{{count}} minutami",other:"{{count}} minutami"},aboutXHours:{one:"približno {{count}} uro",two:"približno {{count}} urama",few:"približno {{count}} urami",other:"približno {{count}} urami"},xHours:{one:"{{count}} uro",two:"{{count}} urama",few:"{{count}} urami",other:"{{count}} urami"},xDays:{one:"{{count}} dnem",two:"{{count}} dnevoma",few:"{{count}} dnevi",other:"{{count}} dnevi"},aboutXMonths:{one:"približno {{count}} mesecem",two:"približno {{count}} mesecema",few:"približno {{count}} meseci",other:"približno {{count}} meseci"},xMonths:{one:"{{count}} mesecem",two:"{{count}} mesecema",few:"{{count}} meseci",other:"{{count}} meseci"},aboutXYears:{one:"približno {{count}} letom",two:"približno {{count}} letoma",few:"približno {{count}} leti",other:"približno {{count}} leti"},xYears:{one:"{{count}} letom",two:"{{count}} letoma",few:"{{count}} leti",other:"{{count}} leti"},overXYears:{one:"več kot {{count}} letom",two:"več kot {{count}} letoma",few:"več kot {{count}} leti",other:"več kot {{count}} leti"},almostXYears:{one:"skoraj {{count}} letom",two:"skoraj {{count}} letoma",few:"skoraj {{count}} leti",other:"skoraj {{count}} leti"}},r={lessThanXSeconds:{one:"manj kot {{count}} sekundo",two:"manj kot {{count}} sekundi",few:"manj kot {{count}} sekunde",other:"manj kot {{count}} sekund"},xSeconds:{one:"{{count}} sekundo",two:"{{count}} sekundi",few:"{{count}} sekunde",other:"{{count}} sekund"},halfAMinute:"pol minute",lessThanXMinutes:{one:"manj kot {{count}} minuto",two:"manj kot {{count}} minuti",few:"manj kot {{count}} minute",other:"manj kot {{count}} minut"},xMinutes:{one:"{{count}} minuto",two:"{{count}} minuti",few:"{{count}} minute",other:"{{count}} minut"},aboutXHours:{one:"približno {{count}} uro",two:"približno {{count}} uri",few:"približno {{count}} ure",other:"približno {{count}} ur"},xHours:{one:"{{count}} uro",two:"{{count}} uri",few:"{{count}} ure",other:"{{count}} ur"},xDays:{one:"{{count}} dan",two:"{{count}} dni",few:"{{count}} dni",other:"{{count}} dni"},aboutXMonths:{one:"približno {{count}} mesec",two:"približno {{count}} meseca",few:"približno {{count}} mesece",other:"približno {{count}} mesecev"},xMonths:{one:"{{count}} mesec",two:"{{count}} meseca",few:"{{count}} mesece",other:"{{count}} mesecev"},aboutXYears:{one:"približno {{count}} leto",two:"približno {{count}} leti",few:"približno {{count}} leta",other:"približno {{count}} let"},xYears:{one:"{{count}} leto",two:"{{count}} leti",few:"{{count}} leta",other:"{{count}} let"},overXYears:{one:"več kot {{count}} leto",two:"več kot {{count}} leti",few:"več kot {{count}} leta",other:"več kot {{count}} let"},almostXYears:{one:"skoraj {{count}} leto",two:"skoraj {{count}} leti",few:"skoraj {{count}} leta",other:"skoraj {{count}} let"}};function o(s){switch(s%100){case 1:return"one";case 2:return"two";case 3:case 4:return"few";default:return"other"}}function n(s,u,d){d=d||{};var m=t,f="";if(d.addSuffix&&(d.comparison>0?(m=r,f+="čez "):(m=a,f+="pred ")),typeof m[s]=="string")f+=m[s];else{var _=o(u);f+=m[s][_].replace("{{count}}",u)}return f}},function(l,i,e){var t=e(2),a={full:"EEEE, dd. MMMM y",long:"dd. MMMM y",medium:"d. MMM y",short:"d. MM. yy"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} {{time}}",long:"{{date}} {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:function(r){var o=r.getUTCDay();switch(o){case 0:return"'prejšnjo nedeljo ob' p";case 3:return"'prejšnjo sredo ob' p";case 6:return"'prejšnjo soboto ob' p";default:return"'prejšnji' EEEE 'ob' p"}},yesterday:"'včeraj ob' p",today:"'danes ob' p",tomorrow:"'jutri ob' p",nextWeek:function(r){var o=r.getUTCDay();switch(o){case 0:return"'naslednjo nedeljo ob' p";case 3:return"'naslednjo sredo ob' p";case 6:return"'naslednjo soboto ob' p";default:return"'naslednji' EEEE 'ob' p"}},other:"P"};function a(r,o,n,s){var u=t[r];return typeof u=="function"?u(o):u}},function(l,i,e){var t=e(3);function a(f){var _=Number(f);return String(_).concat(".")}var r={narrow:["pr. n. št.","po n. št."],abbreviated:["pr. n. št.","po n. št."],wide:["pred našim štetjem","po našem štetju"]},o={narrow:["j","f","m","a","m","j","j","a","s","o","n","d"],abbreviated:["jan.","feb.","mar.","apr.","maj","jun.","jul.","avg.","sep.","okt.","nov.","dec."],wide:["januar","februar","marec","april","maj","junij","julij","avgust","september","oktober","november","december"]},n={narrow:["1","2","3","4"],abbreviated:["1. čet.","2. čet.","3. čet.","4. čet."],wide:["1. četrtletje","2. četrtletje","3. četrtletje","4. četrtletje"]},s={narrow:["n","p","t","s","č","p","s"],short:["ned.","pon.","tor.","sre.","čet.","pet.","sob."],abbreviated:["ned.","pon.","tor.","sre.","čet.","pet.","sob."],wide:["nedelja","ponedeljek","torek","sreda","četrtek","petek","sobota"]},u={narrow:{am:"d",pm:"p",midnight:"24.00",noon:"12.00",morning:"j",afternoon:"p",evening:"v",night:"n"},abbreviated:{am:"dop.",pm:"pop.",midnight:"poln.",noon:"pold.",morning:"jut.",afternoon:"pop.",evening:"več.",night:"noč"},wide:{am:"dop.",pm:"pop.",midnight:"polnoč",noon:"poldne",morning:"jutro",afternoon:"popoldne",evening:"večer",night:"noč"}},d={narrow:{am:"d",pm:"p",midnight:"24.00",noon:"12.00",morning:"zj",afternoon:"p",evening:"zv",night:"po"},abbreviated:{am:"dop.",pm:"pop.",midnight:"opoln.",noon:"opold.",morning:"zjut.",afternoon:"pop.",evening:"zveč.",night:"ponoči"},wide:{am:"dop.",pm:"pop.",midnight:"opolnoči",noon:"opoldne",morning:"zjutraj",afternoon:"popoldan",evening:"zvečer",night:"ponoči"}},m={ordinalNumber:a,era:t.a({values:r,defaultWidth:"wide"}),quarter:t.a({values:n,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:s,defaultWidth:"wide"}),dayPeriod:t.a({values:u,defaultWidth:"wide",formattingValues:d,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(5),a=e(4),r=/^(\d+)\./i,o=/\d+/i,n={abbreviated:/^(pr\. n\. št\.|po n\. št\.)/i,wide:/^(pred Kristusom|pred na[sš]im [sš]tetjem|po Kristusu|po na[sš]em [sš]tetju|na[sš]ega [sš]tetja)/i},s={any:[/^pr/i,/^(po|na[sš]em)/i]},u={narrow:/^[1234]/i,abbreviated:/^[1234]\.\s?[čc]et\.?/i,wide:/^[1234]\. [čc]etrtletje/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[jfmasond]/i,abbreviated:/^(jan\.|feb\.|mar\.|apr\.|maj|jun\.|jul\.|avg\.|sep\.|okt\.|nov\.|dec\.)/i,wide:/^(januar|februar|marec|april|maj|junij|julij|avgust|september|oktober|november|december)/i},f={narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],abbreviated:[/^ja/i,/^fe/i,/^mar/i,/^ap/i,/^maj/i,/^jun/i,/^jul/i,/^av/i,/^s/i,/^o/i,/^n/i,/^d/i],wide:[/^ja/i,/^fe/i,/^mar/i,/^ap/i,/^maj/i,/^jun/i,/^jul/i,/^av/i,/^s/i,/^o/i,/^n/i,/^d/i]},_={narrow:/^[nptsčc]/i,short:/^(ned\.|pon\.|tor\.|sre\.|[cč]et\.|pet\.|sob\.)/i,abbreviated:/^(ned\.|pon\.|tor\.|sre\.|[cč]et\.|pet\.|sob\.)/i,wide:/^(nedelja|ponedeljek|torek|sreda|[cč]etrtek|petek|sobota)/i},c={narrow:[/^n/i,/^p/i,/^t/i,/^s/i,/^[cč]/i,/^p/i,/^s/i],any:[/^n/i,/^po/i,/^t/i,/^sr/i,/^[cč]/i,/^pe/i,/^so/i]},v={narrow:/^(d|po?|z?v|n|z?j|24\.00|12\.00)/i,any:/^(dop\.|pop\.|o?poln(\.|o[cč]i?)|o?pold(\.|ne)|z?ve[cč](\.|er)|(po)?no[cč]i?|popold(ne|an)|jut(\.|ro)|zjut(\.|raj))/i},g={narrow:{am:/^d/i,pm:/^p/i,midnight:/^24/i,noon:/^12/i,morning:/^(z?j)/i,afternoon:/^p/i,evening:/^(z?v)/i,night:/^(n|po)/i},any:{am:/^dop\./i,pm:/^pop\./i,midnight:/^o?poln/i,noon:/^o?pold/i,morning:/j/i,afternoon:/^pop\./i,evening:/^z?ve/i,night:/(po)?no/i}},P={ordinalNumber:a.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:t.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:t.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:t.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"wide"}),day:t.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:t.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(780),a=e(781),r=e(782),o=e(783),n=e(784),s={code:"sr",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:{standalone:"мање од 1 секунде",withPrepositionAgo:"мање од 1 секунде",withPrepositionIn:"мање од 1 секунду"},dual:"мање од {{count}} секунде",other:"мање од {{count}} секунди"},xSeconds:{one:{standalone:"1 секунда",withPrepositionAgo:"1 секунде",withPrepositionIn:"1 секунду"},dual:"{{count}} секунде",other:"{{count}} секунди"},halfAMinute:"пола минуте",lessThanXMinutes:{one:{standalone:"мање од 1 минуте",withPrepositionAgo:"мање од 1 минуте",withPrepositionIn:"мање од 1 минуту"},dual:"мање од {{count}} минуте",other:"мање од {{count}} минута"},xMinutes:{one:{standalone:"1 минута",withPrepositionAgo:"1 минуте",withPrepositionIn:"1 минуту"},dual:"{{count}} минуте",other:"{{count}} минута"},aboutXHours:{one:{standalone:"око 1 сат",withPrepositionAgo:"око 1 сат",withPrepositionIn:"око 1 сат"},dual:"око {{count}} сата",other:"око {{count}} сати"},xHours:{one:{standalone:"1 сат",withPrepositionAgo:"1 сат",withPrepositionIn:"1 сат"},dual:"{{count}} сата",other:"{{count}} сати"},xDays:{one:{standalone:"1 дан",withPrepositionAgo:"1 дан",withPrepositionIn:"1 дан"},dual:"{{count}} дана",other:"{{count}} дана"},aboutXWeeks:{one:{standalone:"око 1 недељу",withPrepositionAgo:"око 1 недељу",withPrepositionIn:"око 1 недељу"},dual:"око {{count}} недеље",other:"око {{count}} недеље"},xWeeks:{one:{standalone:"1 недељу",withPrepositionAgo:"1 недељу",withPrepositionIn:"1 недељу"},dual:"{{count}} недеље",other:"{{count}} недеље"},aboutXMonths:{one:{standalone:"око 1 месец",withPrepositionAgo:"око 1 месец",withPrepositionIn:"око 1 месец"},dual:"око {{count}} месеца",other:"око {{count}} месеци"},xMonths:{one:{standalone:"1 месец",withPrepositionAgo:"1 месец",withPrepositionIn:"1 месец"},dual:"{{count}} месеца",other:"{{count}} месеци"},aboutXYears:{one:{standalone:"око 1 годину",withPrepositionAgo:"око 1 годину",withPrepositionIn:"око 1 годину"},dual:"око {{count}} године",other:"око {{count}} година"},xYears:{one:{standalone:"1 година",withPrepositionAgo:"1 године",withPrepositionIn:"1 годину"},dual:"{{count}} године",other:"{{count}} година"},overXYears:{one:{standalone:"преко 1 годину",withPrepositionAgo:"преко 1 годину",withPrepositionIn:"преко 1 годину"},dual:"преко {{count}} године",other:"преко {{count}} година"},almostXYears:{one:{standalone:"готово 1 годину",withPrepositionAgo:"готово 1 годину",withPrepositionIn:"готово 1 годину"},dual:"готово {{count}} године",other:"готово {{count}} година"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?n.addSuffix?n.comparison>0?s=t[r].one.withPrepositionIn:s=t[r].one.withPrepositionAgo:s=t[r].one.standalone:o%10>1&&o%10<5&&String(o).substr(-2,1)!=="1"?s=t[r].dual.replace("{{count}}",o):s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"за "+s:"пре "+s:s}},function(l,i,e){var t=e(2),a={full:"EEEE, d. MMMM yyyy.",long:"d. MMMM yyyy.",medium:"d. MMM yy.",short:"dd. MM. yy."},r={full:"HH:mm:ss (zzzz)",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'у' {{time}}",long:"{{date}} 'у' {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:function(r){var o=r.getUTCDay();switch(o){case 0:return"'прошле недеље у' p";case 3:return"'прошле среде у' p";case 6:return"'прошле суботе у' p";default:return"'прошли' EEEE 'у' p"}},yesterday:"'јуче у' p",today:"'данас у' p",tomorrow:"'сутра у' p",nextWeek:function(r){var o=r.getUTCDay();switch(o){case 0:return"'следеће недеље у' p";case 3:return"'следећу среду у' p";case 6:return"'следећу суботу у' p";default:return"'следећи' EEEE 'у' p"}},other:"P"};function a(r,o,n,s){var u=t[r];return typeof u=="function"?u(o):u}},function(l,i,e){var t=e(3);function a(_){var c=Number(_);return String(c).concat(".")}var r={narrow:["пр.н.е.","АД"],abbreviated:["пр. Хр.","по. Хр."],wide:["Пре Христа","После Христа"]},o={narrow:["1.","2.","3.","4.","5.","6.","7.","8.","9.","10.","11.","12."],abbreviated:["јан","феб","мар","апр","мај","јун","јул","авг","сеп","окт","нов","дец"],wide:["јануар","фебруар","март","април","мај","јун","јул","август","септембар","октобар","новембар","децембар"]},n={narrow:["1.","2.","3.","4.","5.","6.","7.","8.","9.","10.","11.","12."],abbreviated:["јан","феб","мар","апр","мај","јун","јул","авг","сеп","окт","нов","дец"],wide:["јануар","фебруар","март","април","мај","јун","јул","август","септембар","октобар","новембар","децембар"]},s={narrow:["1.","2.","3.","4."],abbreviated:["1. кв.","2. кв.","3. кв.","4. кв."],wide:["1. квартал","2. квартал","3. квартал","4. квартал"]},u={narrow:["Н","П","У","С","Ч","П","С"],short:["нед","пон","уто","сре","чет","пет","суб"],abbreviated:["нед","пон","уто","сре","чет","пет","суб"],wide:["недеља","понедељак","уторак","среда","четвртак","петак","субота"]},d={narrow:{am:"АМ",pm:"ПМ",midnight:"поноћ",noon:"подне",morning:"ујутру",afternoon:"поподне",evening:"увече",night:"ноћу"},abbreviated:{am:"АМ",pm:"ПМ",midnight:"поноћ",noon:"подне",morning:"ујутру",afternoon:"поподне",evening:"увече",night:"ноћу"},wide:{am:"AM",pm:"PM",midnight:"поноћ",noon:"подне",morning:"ујутру",afternoon:"после подне",evening:"увече",night:"ноћу"}},m={narrow:{am:"AM",pm:"PM",midnight:"поноћ",noon:"подне",morning:"ујутру",afternoon:"поподне",evening:"увече",night:"ноћу"},abbreviated:{am:"AM",pm:"PM",midnight:"поноћ",noon:"подне",morning:"ујутру",afternoon:"поподне",evening:"увече",night:"ноћу"},wide:{am:"AM",pm:"PM",midnight:"поноћ",noon:"подне",morning:"ујутру",afternoon:"после подне",evening:"увече",night:"ноћу"}},f={ordinalNumber:a,era:t.a({values:r,defaultWidth:"wide"}),quarter:t.a({values:s,defaultWidth:"wide",argumentCallback:function(_){return Number(_)-1}}),month:t.a({values:o,defaultWidth:"wide",formattingValues:n,defaultFormattingWidth:"wide"}),day:t.a({values:u,defaultWidth:"wide"}),dayPeriod:t.a({values:m,defaultWidth:"wide",formattingValues:d,defaultFormattingWidth:"wide"})};i.a=f},function(l,i,e){var t=e(5),a=e(4),r=/^(\d+)\./i,o=/\d+/i,n={narrow:/^(пр\.н\.е\.|АД)/i,abbreviated:/^(пр\.\s?Хр\.|по\.\s?Хр\.)/i,wide:/^(Пре Христа|пре нове ере|После Христа|нова ера)/i},s={any:[/^пр/i,/^(по|нова)/i]},u={narrow:/^[1234]/i,abbreviated:/^[1234]\.\s?кв\.?/i,wide:/^[1234]\. квартал/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^(10|11|12|[123456789])\./i,abbreviated:/^(јан|феб|мар|апр|мај|јун|јул|авг|сеп|окт|нов|дец)/i,wide:/^((јануар|јануара)|(фебруар|фебруара)|(март|марта)|(април|априла)|(мја|маја)|(јун|јуна)|(јул|јула)|(август|августа)|(септембар|септембра)|(октобар|октобра)|(новембар|новембра)|(децембар|децембра))/i},f={narrow:[/(10|11|12|[123456789])/i],any:[/^ја/i,/^ф/i,/^мар/i,/^ап/i,/^мај/i,/^јун/i,/^јул/i,/^авг/i,/^с/i,/^о/i,/^н/i,/^д/i]},_={narrow:/^[пусчн]/i,short:/^(нед|пон|уто|сре|чет|пет|суб)/i,abbreviated:/^(нед|пон|уто|сре|чет|пет|суб)/i,wide:/^(недеља|понедељак|уторак|среда|четвртак|петак|субота)/i},c={narrow:[/^п/i,/^у/i,/^с/i,/^ч/i,/^н/i],any:[/^нед/i,/^пон/i,/^уто/i,/^сре/i,/^чет/i,/^пет/i,/^суб/i]},v={any:/^(ам|пм|поноћ|(по)?подне|увече|ноћу|после подне|ујутру)/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/^поно/i,noon:/^под/i,morning:/ујутру/i,afternoon:/(после\s|по)+подне/i,evening:/(увече)/i,night:/(ноћу)/i}},P={ordinalNumber:a.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:t.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:t.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:t.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:t.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:t.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(786),a=e(787),r=e(788),o=e(789),n=e(790),s={code:"sr-Latn",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:{standalone:"manje od 1 sekunde",withPrepositionAgo:"manje od 1 sekunde",withPrepositionIn:"manje od 1 sekundu"},dual:"manje od {{count}} sekunde",other:"manje od {{count}} sekundi"},xSeconds:{one:{standalone:"1 sekunda",withPrepositionAgo:"1 sekunde",withPrepositionIn:"1 sekundu"},dual:"{{count}} sekunde",other:"{{count}} sekundi"},halfAMinute:"pola minute",lessThanXMinutes:{one:{standalone:"manje od 1 minute",withPrepositionAgo:"manje od 1 minute",withPrepositionIn:"manje od 1 minutu"},dual:"manje od {{count}} minute",other:"manje od {{count}} minuta"},xMinutes:{one:{standalone:"1 minuta",withPrepositionAgo:"1 minute",withPrepositionIn:"1 minutu"},dual:"{{count}} minute",other:"{{count}} minuta"},aboutXHours:{one:{standalone:"oko 1 sat",withPrepositionAgo:"oko 1 sat",withPrepositionIn:"oko 1 sat"},dual:"oko {{count}} sata",other:"oko {{count}} sati"},xHours:{one:{standalone:"1 sat",withPrepositionAgo:"1 sat",withPrepositionIn:"1 sat"},dual:"{{count}} sata",other:"{{count}} sati"},xDays:{one:{standalone:"1 dan",withPrepositionAgo:"1 dan",withPrepositionIn:"1 dan"},dual:"{{count}} dana",other:"{{count}} dana"},aboutXWeeks:{one:{standalone:"oko 1 nedelju",withPrepositionAgo:"oko 1 nedelju",withPrepositionIn:"oko 1 nedelju"},dual:"oko {{count}} nedelje",other:"oko {{count}} nedelje"},xWeeks:{one:{standalone:"1 nedelju",withPrepositionAgo:"1 nedelju",withPrepositionIn:"1 nedelju"},dual:"{{count}} nedelje",other:"{{count}} nedelje"},aboutXMonths:{one:{standalone:"oko 1 mesec",withPrepositionAgo:"oko 1 mesec",withPrepositionIn:"oko 1 mesec"},dual:"oko {{count}} meseca",other:"oko {{count}} meseci"},xMonths:{one:{standalone:"1 mesec",withPrepositionAgo:"1 mesec",withPrepositionIn:"1 mesec"},dual:"{{count}} meseca",other:"{{count}} meseci"},aboutXYears:{one:{standalone:"oko 1 godinu",withPrepositionAgo:"oko 1 godinu",withPrepositionIn:"oko 1 godinu"},dual:"oko {{count}} godine",other:"oko {{count}} godina"},xYears:{one:{standalone:"1 godina",withPrepositionAgo:"1 godine",withPrepositionIn:"1 godinu"},dual:"{{count}} godine",other:"{{count}} godina"},overXYears:{one:{standalone:"preko 1 godinu",withPrepositionAgo:"preko 1 godinu",withPrepositionIn:"preko 1 godinu"},dual:"preko {{count}} godine",other:"preko {{count}} godina"},almostXYears:{one:{standalone:"gotovo 1 godinu",withPrepositionAgo:"gotovo 1 godinu",withPrepositionIn:"gotovo 1 godinu"},dual:"gotovo {{count}} godine",other:"gotovo {{count}} godina"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?n.addSuffix?n.comparison>0?s=t[r].one.withPrepositionIn:s=t[r].one.withPrepositionAgo:s=t[r].one.standalone:o%10>1&&o%10<5&&String(o).substr(-2,1)!=="1"?s=t[r].dual.replace("{{count}}",o):s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?"za "+s:"pre "+s:s}},function(l,i,e){var t=e(2),a={full:"EEEE, d. MMMM yyyy.",long:"d. MMMM yyyy.",medium:"d. MMM yy.",short:"dd. MM. yy."},r={full:"HH:mm:ss (zzzz)",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'u' {{time}}",long:"{{date}} 'u' {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:function(r){var o=r.getUTCDay();switch(o){case 0:return"'prošle nedelje u' p";case 3:return"'prošle srede u' p";case 6:return"'prošle subote u' p";default:return"'prošli' EEEE 'u' p"}},yesterday:"'juče u' p",today:"'danas u' p",tomorrow:"'sutra u' p",nextWeek:function(r){var o=r.getUTCDay();switch(o){case 0:return"'sledeće nedelje u' p";case 3:return"'sledeću sredu u' p";case 6:return"'sledeću subotu u' p";default:return"'sledeći' EEEE 'u' p"}},other:"P"};function a(r,o,n,s){var u=t[r];return typeof u=="function"?u(o):u}},function(l,i,e){var t=e(3);function a(_){var c=Number(_);return String(c).concat(".")}var r={narrow:["pr.n.e.","AD"],abbreviated:["pr. Hr.","po. Hr."],wide:["Pre Hrista","Posle Hrista"]},o={narrow:["1.","2.","3.","4.","5.","6.","7.","8.","9.","10.","11.","12."],abbreviated:["jan","feb","mar","apr","maj","jun","jul","avg","sep","okt","nov","dec"],wide:["januar","februar","mart","april","maj","jun","jul","avgust","septembar","oktobar","novembar","decembar"]},n={narrow:["1.","2.","3.","4.","5.","6.","7.","8.","9.","10.","11.","12."],abbreviated:["jan","feb","mar","apr","maj","jun","jul","avg","sep","okt","nov","dec"],wide:["januar","februar","mart","april","maj","jun","jul","avgust","septembar","oktobar","novembar","decembar"]},s={narrow:["1.","2.","3.","4."],abbreviated:["1. kv.","2. kv.","3. kv.","4. kv."],wide:["1. kvartal","2. kvartal","3. kvartal","4. kvartal"]},u={narrow:["N","P","U","S","Č","P","S"],short:["ned","pon","uto","sre","čet","pet","sub"],abbreviated:["ned","pon","uto","sre","čet","pet","sub"],wide:["nedelja","ponedeljak","utorak","sreda","četvrtak","petak","subota"]},d={narrow:{am:"AM",pm:"PM",midnight:"ponoć",noon:"podne",morning:"ujutru",afternoon:"popodne",evening:"uveče",night:"noću"},abbreviated:{am:"AM",pm:"PM",midnight:"ponoć",noon:"podne",morning:"ujutru",afternoon:"popodne",evening:"uveče",night:"noću"},wide:{am:"AM",pm:"PM",midnight:"ponoć",noon:"podne",morning:"ujutru",afternoon:"posle podne",evening:"uveče",night:"noću"}},m={narrow:{am:"AM",pm:"PM",midnight:"ponoć",noon:"podne",morning:"ujutru",afternoon:"popodne",evening:"uveče",night:"noću"},abbreviated:{am:"AM",pm:"PM",midnight:"ponoć",noon:"podne",morning:"ujutru",afternoon:"popodne",evening:"uveče",night:"noću"},wide:{am:"AM",pm:"PM",midnight:"ponoć",noon:"podne",morning:"ujutru",afternoon:"posle podne",evening:"uveče",night:"noću"}},f={ordinalNumber:a,era:t.a({values:r,defaultWidth:"wide"}),quarter:t.a({values:s,defaultWidth:"wide",argumentCallback:function(_){return Number(_)-1}}),month:t.a({values:o,defaultWidth:"wide",formattingValues:n,defaultFormattingWidth:"wide"}),day:t.a({values:u,defaultWidth:"wide"}),dayPeriod:t.a({values:m,defaultWidth:"wide",formattingValues:d,defaultFormattingWidth:"wide"})};i.a=f},function(l,i,e){var t=e(5),a=e(4),r=/^(\d+)\./i,o=/\d+/i,n={narrow:/^(pr\.n\.e\.|AD)/i,abbreviated:/^(pr\.\s?Hr\.|po\.\s?Hr\.)/i,wide:/^(Pre Hrista|pre nove ere|Posle Hrista|nova era)/i},s={any:[/^pr/i,/^(po|nova)/i]},u={narrow:/^[1234]/i,abbreviated:/^[1234]\.\s?kv\.?/i,wide:/^[1234]\. kvartal/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^(10|11|12|[123456789])\./i,abbreviated:/^(jan|feb|mar|apr|maj|jun|jul|avg|sep|okt|nov|dec)/i,wide:/^((januar|januara)|(februar|februara)|(mart|marta)|(april|aprila)|(maj|maja)|(jun|juna)|(jul|jula)|(avgust|avgusta)|(septembar|septembra)|(oktobar|oktobra)|(novembar|novembra)|(decembar|decembra))/i},f={narrow:[/(10|11|12|[123456789])/i],any:[/^ja/i,/^f/i,/^mar/i,/^ap/i,/^maj/i,/^jun/i,/^jul/i,/^avg/i,/^s/i,/^o/i,/^n/i,/^d/i]},_={narrow:/^[npusčc]/i,short:/^(ned|pon|uto|sre|(čet|cet)|pet|sub)/i,abbreviated:/^(ned|pon|uto|sre|(čet|cet)|pet|sub)/i,wide:/^(nedelja|ponedeljak|utorak|sreda|(četvrtak|cetvrtak)|petak|subota)/i},c={narrow:[/^s/i,/^m/i,/^t/i,/^w/i,/^t/i,/^f/i,/^s/i],any:[/^su/i,/^m/i,/^tu/i,/^w/i,/^th/i,/^f/i,/^sa/i]},v={any:/^(am|pm|ponoc|ponoć|(po)?podne|uvece|uveče|noću|posle podne|ujutru)/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/^pono/i,noon:/^pod/i,morning:/jutro/i,afternoon:/(posle\s|po)+podne/i,evening:/(uvece|uveče)/i,night:/(nocu|noću)/i}},P={ordinalNumber:a.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:t.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:t.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:t.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:t.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:t.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(792),a=e(793),r=e(794),o=e(795),n=e(796),s={code:"sv",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=r;var t={lessThanXSeconds:{singular:"mindre än en sekund",plural:"mindre än {{count}} sekunder"},xSeconds:{singular:"en sekund",plural:"{{count}} sekunder"},halfAMinute:"en halv minut",lessThanXMinutes:{singular:"mindre än en minut",plural:"mindre än {{count}} minuter"},xMinutes:{singular:"en minut",plural:"{{count}} minuter"},aboutXHours:{singular:"ungefär en timme",plural:"ungefär {{count}} timmar"},xHours:{singular:"en timme",plural:"{{count}} timmar"},xDays:{singular:"en dag",plural:"{{count}} dagar"},aboutXWeeks:{singular:"ungefär en vecka",plural:"ungefär {{count}} vecka"},xWeeks:{singular:"en vecka",plural:"{{count}} vecka"},aboutXMonths:{singular:"ungefär en månad",plural:"ungefär {{count}} månader"},xMonths:{singular:"en månad",plural:"{{count}} månader"},aboutXYears:{singular:"ungefär ett år",plural:"ungefär {{count}} år"},xYears:{singular:"ett år",plural:"{{count}} år"},overXYears:{singular:"över ett år",plural:"över {{count}} år"},almostXYears:{singular:"nästan ett år",plural:"nästan {{count}} år"}},a=["noll","en","två","tre","fyra","fem","sex","sju","åtta","nio","tio","elva","tolv"];function r(o,n,s){s=s||{onlyNumeric:!1};var u=t[o],d;return typeof u=="string"?d=u:n===0||n>1?s.onlyNumeric?d=u.plural.replace("{{count}}",n):d=u.plural.replace("{{count}}",n<13?a[n]:n):d=u.singular,s.addSuffix?s.comparison>0?"om "+d:d+" sedan":d}},function(l,i,e){var t=e(2),a={full:"EEEE d MMMM y",long:"d MMMM y",medium:"d MMM y",short:"y-MM-dd"},r={full:"'kl'. HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'kl.' {{time}}",long:"{{date}} 'kl.' {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'i' EEEE's kl.' p",yesterday:"'igår kl.' p",today:"'idag kl.' p",tomorrow:"'imorgon kl.' p",nextWeek:"EEEE 'kl.' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["f.Kr.","e.Kr."],abbreviated:["f.Kr.","e.Kr."],wide:["före Kristus","efter Kristus"]},r={narrow:["1","2","3","4"],abbreviated:["Q1","Q2","Q3","Q4"],wide:["1:a kvartalet","2:a kvartalet","3:e kvartalet","4:e kvartalet"]},o={narrow:["J","F","M","A","M","J","J","A","S","O","N","D"],abbreviated:["jan.","feb.","mars","apr.","maj","juni","juli","aug.","sep.","okt.","nov.","dec."],wide:["januari","februari","mars","april","maj","juni","juli","augusti","september","oktober","november","december"]},n={narrow:["S","M","T","O","T","F","L"],short:["sö","må","ti","on","to","fr","lö"],abbreviated:["sön","mån","tis","ons","tor","fre","lör"],wide:["söndag","måndag","tisdag","onsdag","torsdag","fredag","lördag"]},s={narrow:{am:"fm",pm:"em",midnight:"midnatt",noon:"middag",morning:"morg.",afternoon:"efterm.",evening:"kväll",night:"natt"},abbreviated:{am:"f.m.",pm:"e.m.",midnight:"midnatt",noon:"middag",morning:"morgon",afternoon:"efterm.",evening:"kväll",night:"natt"},wide:{am:"förmiddag",pm:"eftermiddag",midnight:"midnatt",noon:"middag",morning:"morgon",afternoon:"eftermiddag",evening:"kväll",night:"natt"}},u={narrow:{am:"fm",pm:"em",midnight:"midnatt",noon:"middag",morning:"på morg.",afternoon:"på efterm.",evening:"på kvällen",night:"på natten"},abbreviated:{am:"fm",pm:"em",midnight:"midnatt",noon:"middag",morning:"på morg.",afternoon:"på efterm.",evening:"på kvällen",night:"på natten"},wide:{am:"fm",pm:"em",midnight:"midnatt",noon:"middag",morning:"på morgonen",afternoon:"på eftermiddagen",evening:"på kvällen",night:"på natten"}};function d(f){var _=Number(f),c=_%100;if(c>20||c<10)switch(c%10){case 1:case 2:return _+":a"}return _+":e"}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(:a|:e)?/i,o=/\d+/i,n={narrow:/^(f\.? ?Kr\.?|f\.? ?v\.? ?t\.?|e\.? ?Kr\.?|v\.? ?t\.?)/i,abbreviated:/^(f\.? ?Kr\.?|f\.? ?v\.? ?t\.?|e\.? ?Kr\.?|v\.? ?t\.?)/i,wide:/^(före Kristus|före vår tid|efter Kristus|vår tid)/i},s={any:[/^f/i,/^[ev]/i]},u={narrow:/^[1234]/i,abbreviated:/^q[1234]/i,wide:/^[1234](:a|:e)? kvartalet/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[jfmasond]/i,abbreviated:/^(jan|feb|mar|apr|maj|jun|jul|aug|sep|okt|nov|dec)\.?/i,wide:/^(januari|februari|mars|april|maj|juni|juli|augusti|september|oktober|november|december)/i},f={narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^mar/i,/^ap/i,/^maj/i,/^jun/i,/^jul/i,/^au/i,/^s/i,/^o/i,/^n/i,/^d/i]},_={narrow:/^[smtofl]/i,short:/^(sö|må|ti|on|to|fr|lö)/i,abbreviated:/^(sön|mån|tis|ons|tor|fre|lör)/i,wide:/^(söndag|måndag|tisdag|onsdag|torsdag|fredag|lördag)/i},c={any:[/^s/i,/^m/i,/^ti/i,/^o/i,/^to/i,/^f/i,/^l/i]},v={any:/^([fe]\.?\s?m\.?|midn(att)?|midd(ag)?|(på) (morgonen|eftermiddagen|kvällen|natten))/i},g={any:{am:/^f/i,pm:/^e/i,midnight:/^midn/i,noon:/^midd/i,morning:/morgon/i,afternoon:/eftermiddag/i,evening:/kväll/i,night:/natt/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(798),a=e(799),r=e(800),o=e(801),n=e(802),s={code:"ta",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=r;var t={lessThanXSeconds:{one:{default:"ஒரு வினாடிக்கு குறைவாக",in:"ஒரு வினாடிக்குள்",ago:"ஒரு வினாடிக்கு முன்பு"},other:{default:"{{count}} வினாடிகளுக்கு குறைவாக",in:"{{count}} வினாடிகளுக்குள்",ago:"{{count}} வினாடிகளுக்கு முன்பு"}},xSeconds:{one:{default:"1 வினாடி",in:"1 வினாடியில்",ago:"1 வினாடி முன்பு"},other:{default:"{{count}} விநாடிகள்",in:"{{count}} வினாடிகளில்",ago:"{{count}} விநாடிகளுக்கு முன்பு"}},halfAMinute:{default:"அரை நிமிடம்",in:"அரை நிமிடத்தில்",ago:"அரை நிமிடம் முன்பு"},lessThanXMinutes:{one:{default:"ஒரு நிமிடத்திற்கும் குறைவாக",in:"ஒரு நிமிடத்திற்குள்",ago:"ஒரு நிமிடத்திற்கு முன்பு"},other:{default:"{{count}} நிமிடங்களுக்கும் குறைவாக",in:"{{count}} நிமிடங்களுக்குள்",ago:"{{count}} நிமிடங்களுக்கு முன்பு"}},xMinutes:{one:{default:"1 நிமிடம்",in:"1 நிமிடத்தில்",ago:"1 நிமிடம் முன்பு"},other:{default:"{{count}} நிமிடங்கள்",in:"{{count}} நிமிடங்களில்",ago:"{{count}} நிமிடங்களுக்கு முன்பு"}},aboutXHours:{one:{default:"சுமார் 1 மணி நேரம்",in:"சுமார் 1 மணி நேரத்தில்",ago:"சுமார் 1 மணி நேரத்திற்கு முன்பு"},other:{default:"சுமார் {{count}} மணி நேரம்",in:"சுமார் {{count}} மணி நேரத்திற்கு முன்பு",ago:"சுமார் {{count}} மணி நேரத்தில்"}},xHours:{one:{default:"1 மணி நேரம்",in:"1 மணி நேரத்தில்",ago:"1 மணி நேரத்திற்கு முன்பு"},other:{default:"{{count}} மணி நேரம்",in:"{{count}} மணி நேரத்தில்",ago:"{{count}} மணி நேரத்திற்கு முன்பு"}},xDays:{one:{default:"1 நாள்",in:"1 நாளில்",ago:"1 நாள் முன்பு"},other:{default:"{{count}} நாட்கள்",in:"{{count}} நாட்களில்",ago:"{{count}} நாட்களுக்கு முன்பு"}},aboutXWeeks:{one:{default:"சுமார் 1 வாரம்",in:"சுமார் 1 வாரத்தில்",ago:"சுமார் 1 வாரம் முன்பு"},other:{default:"சுமார் {{count}} வாரங்கள்",in:"சுமார் {{count}} வாரங்களில்",ago:"சுமார் {{count}} வாரங்களுக்கு முன்பு"}},xWeeks:{one:{default:"1 வாரம்",in:"1 வாரத்தில்",ago:"1 வாரம் முன்பு"},other:{default:"{{count}} வாரங்கள்",in:"{{count}} வாரங்களில்",ago:"{{count}} வாரங்களுக்கு முன்பு"}},aboutXMonths:{one:{default:"சுமார் 1 மாதம்",in:"சுமார் 1 மாதத்தில்",ago:"சுமார் 1 மாதத்திற்கு முன்பு"},other:{default:"சுமார் {{count}} மாதங்கள்",in:"சுமார் {{count}} மாதங்களில்",ago:"சுமார் {{count}} மாதங்களுக்கு முன்பு"}},xMonths:{one:{default:"1 மாதம்",in:"1 மாதத்தில்",ago:"1 மாதம் முன்பு"},other:{default:"{{count}} மாதங்கள்",in:"{{count}} மாதங்களில்",ago:"{{count}} மாதங்களுக்கு முன்பு"}},aboutXYears:{one:{default:"சுமார் 1 வருடம்",in:"சுமார் 1 ஆண்டில்",ago:"சுமார் 1 வருடம் முன்பு"},other:{default:"சுமார் {{count}} ஆண்டுகள்",in:"சுமார் {{count}} ஆண்டுகளில்",ago:"சுமார் {{count}} ஆண்டுகளுக்கு முன்பு"}},xYears:{one:{default:"1 வருடம்",in:"1 ஆண்டில்",ago:"1 வருடம் முன்பு"},other:{default:"{{count}} ஆண்டுகள்",in:"{{count}} ஆண்டுகளில்",ago:"{{count}} ஆண்டுகளுக்கு முன்பு"}},overXYears:{one:{default:"1 வருடத்திற்கு மேல்",in:"1 வருடத்திற்கும் மேலாக",ago:"1 வருடம் முன்பு"},other:{default:"{{count}} ஆண்டுகளுக்கும் மேலாக",in:"{{count}} ஆண்டுகளில்",ago:"{{count}} ஆண்டுகளுக்கு முன்பு"}},almostXYears:{one:{default:"கிட்டத்தட்ட 1 வருடம்",in:"கிட்டத்தட்ட 1 ஆண்டில்",ago:"கிட்டத்தட்ட 1 வருடம் முன்பு"},other:{default:"கிட்டத்தட்ட {{count}} ஆண்டுகள்",in:"கிட்டத்தட்ட {{count}} ஆண்டுகளில்",ago:"கிட்டத்தட்ட {{count}} ஆண்டுகளுக்கு முன்பு"}}};function a(o,n){return n.addSuffix?n.comparison>0?o.in:o.ago:o.default}function r(o,n){var s=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{},u;return t[o].default?u=a(t[o],s):n===1?u=a(t[o].one,s):u=a(t[o].other,s),u.replace("{{count}}",n)}},function(l,i,e){var t=e(2),a={full:"EEEE, d MMMM, y",long:"d MMMM, y",medium:"d MMM, y",short:"d/M/yy"},r={full:"a h:mm:ss zzzz",long:"a h:mm:ss z",medium:"a h:mm:ss",short:"a h:mm"},o={full:"{{date}} {{time}}",long:"{{date}} {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'கடந்த' eeee p 'மணிக்கு'",yesterday:"'நேற்று ' p 'மணிக்கு'",today:"'இன்று ' p 'மணிக்கு'",tomorrow:"'நாளை ' p 'மணிக்கு'",nextWeek:"eeee p 'மணிக்கு'",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["கி.மு.","கி.பி."],abbreviated:["கி.மு.","கி.பி."],wide:["கிறிஸ்துவுக்கு முன்","அன்னோ டோமினி"]},r={narrow:["1","2","3","4"],abbreviated:["காலா.1","காலா.2","காலா.3","காலா.4"],wide:["ஒன்றாம் காலாண்டு","இரண்டாம் காலாண்டு","மூன்றாம் காலாண்டு","நான்காம் காலாண்டு"]},o={narrow:["ஜ","பி","மா","ஏ","மே","ஜூ","ஜூ","ஆ","செ","அ","ந","டி"],abbreviated:["ஜன.","பிப்.","மார்.","ஏப்.","மே","ஜூன்","ஜூலை","ஆக.","செப்.","அக்.","நவ.","டிச."],wide:["ஜனவரி","பிப்ரவரி","மார்ச்","ஏப்ரல்","மே","ஜூன்","ஜூலை","ஆகஸ்ட்","செப்டம்பர்","அக்டோபர்","நவம்பர்","டிசம்பர்"]},n={narrow:["ஞா","தி","செ","பு","வி","வெ","ச"],short:["ஞா","தி","செ","பு","வி","வெ","ச"],abbreviated:["ஞாயி.","திங்.","செவ்.","புத.","வியா.","வெள்.","சனி"],wide:["ஞாயிறு","திங்கள்","செவ்வாய்","புதன்","வியாழன்","வெள்ளி","சனி"]},s={narrow:{am:"மு.ப",pm:"பி.ப",midnight:"நள்.",noon:"நண்.",morning:"கா.",afternoon:"மதி.",evening:"மா.",night:"இர."},abbreviated:{am:"முற்பகல்",pm:"பிற்பகல்",midnight:"நள்ளிரவு",noon:"நண்பகல்",morning:"காலை",afternoon:"மதியம்",evening:"மாலை",night:"இரவு"},wide:{am:"முற்பகல்",pm:"பிற்பகல்",midnight:"நள்ளிரவு",noon:"நண்பகல்",morning:"காலை",afternoon:"மதியம்",evening:"மாலை",night:"இரவு"}},u={narrow:{am:"மு.ப",pm:"பி.ப",midnight:"நள்.",noon:"நண்.",morning:"கா.",afternoon:"மதி.",evening:"மா.",night:"இர."},abbreviated:{am:"முற்பகல்",pm:"பிற்பகல்",midnight:"நள்ளிரவு",noon:"நண்பகல்",morning:"காலை",afternoon:"மதியம்",evening:"மாலை",night:"இரவு"},wide:{am:"முற்பகல்",pm:"பிற்பகல்",midnight:"நள்ளிரவு",noon:"நண்பகல்",morning:"காலை",afternoon:"மதியம்",evening:"மாலை",night:"இரவு"}};function d(f,_){var c=Number(f);return c}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(வது)?/i,o=/\d+/i,n={narrow:/^(கி.மு.|கி.பி.)/i,abbreviated:/^(கி\.?\s?மு\.?|கி\.?\s?பி\.?)/,wide:/^(கிறிஸ்துவுக்கு\sமுன்|அன்னோ\sடோமினி)/i},s={any:[/கி\.?\s?மு\.?/,/கி\.?\s?பி\.?/]},u={narrow:/^[1234]/i,abbreviated:/^காலா.[1234]/i,wide:/^(ஒன்றாம்|இரண்டாம்|மூன்றாம்|நான்காம்) காலாண்டு/i},d={narrow:[/1/i,/2/i,/3/i,/4/i],any:[/(1|காலா.1|ஒன்றாம்)/i,/(2|காலா.2|இரண்டாம்)/i,/(3|காலா.3|மூன்றாம்)/i,/(4|காலா.4|நான்காம்)/i]},m={narrow:/^(ஜ|பி|மா|ஏ|மே|ஜூ|ஆ|செ|அ|ந|டி)$/i,abbreviated:/^(ஜன.|பிப்.|மார்.|ஏப்.|மே|ஜூன்|ஜூலை|ஆக.|செப்.|அக்.|நவ.|டிச.)/i,wide:/^(ஜனவரி|பிப்ரவரி|மார்ச்|ஏப்ரல்|மே|ஜூன்|ஜூலை|ஆகஸ்ட்|செப்டம்பர்|அக்டோபர்|நவம்பர்|டிசம்பர்)/i},f={narrow:[/^ஜ$/i,/^பி/i,/^மா/i,/^ஏ/i,/^மே/i,/^ஜூ/i,/^ஜூ/i,/^ஆ/i,/^செ/i,/^அ/i,/^ந/i,/^டி/i],any:[/^ஜன/i,/^பி/i,/^மா/i,/^ஏ/i,/^மே/i,/^ஜூன்/i,/^ஜூலை/i,/^ஆ/i,/^செ/i,/^அ/i,/^ந/i,/^டி/i]},_={narrow:/^(ஞா|தி|செ|பு|வி|வெ|ச)/i,short:/^(ஞா|தி|செ|பு|வி|வெ|ச)/i,abbreviated:/^(ஞாயி.|திங்.|செவ்.|புத.|வியா.|வெள்.|சனி)/i,wide:/^(ஞாயிறு|திங்கள்|செவ்வாய்|புதன்|வியாழன்|வெள்ளி|சனி)/i},c={narrow:[/^ஞா/i,/^தி/i,/^செ/i,/^பு/i,/^வி/i,/^வெ/i,/^ச/i],any:[/^ஞா/i,/^தி/i,/^செ/i,/^பு/i,/^வி/i,/^வெ/i,/^ச/i]},v={narrow:/^(மு.ப|பி.ப|நள்|நண்|காலை|மதியம்|மாலை|இரவு)/i,any:/^(மு.ப|பி.ப|முற்பகல்|பிற்பகல்|நள்ளிரவு|நண்பகல்|காலை|மதியம்|மாலை|இரவு)/i},g={any:{am:/^மு/i,pm:/^பி/i,midnight:/^நள்/i,noon:/^நண்/i,morning:/காலை/i,afternoon:/மதியம்/i,evening:/மாலை/i,night:/இரவு/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(804),a=e(805),r=e(806),o=e(807),n=e(808),s={code:"te",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:0,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{standalone:{one:"సెకను కన్నా తక్కువ",other:"{{count}} సెకన్ల కన్నా తక్కువ"},withPreposition:{one:"సెకను",other:"{{count}} సెకన్ల"}},xSeconds:{standalone:{one:"ఒక సెకను",other:"{{count}} సెకన్ల"},withPreposition:{one:"ఒక సెకను",other:"{{count}} సెకన్ల"}},halfAMinute:{standalone:"అర నిమిషం",withPreposition:"అర నిమిషం"},lessThanXMinutes:{standalone:{one:"ఒక నిమిషం కన్నా తక్కువ",other:"{{count}} నిమిషాల కన్నా తక్కువ"},withPreposition:{one:"ఒక నిమిషం",other:"{{count}} నిమిషాల"}},xMinutes:{standalone:{one:"ఒక నిమిషం",other:"{{count}} నిమిషాలు"},withPreposition:{one:"ఒక నిమిషం",other:"{{count}} నిమిషాల"}},aboutXHours:{standalone:{one:"సుమారు ఒక గంట",other:"సుమారు {{count}} గంటలు"},withPreposition:{one:"సుమారు ఒక గంట",other:"సుమారు {{count}} గంటల"}},xHours:{standalone:{one:"ఒక గంట",other:"{{count}} గంటలు"},withPreposition:{one:"ఒక గంట",other:"{{count}} గంటల"}},xDays:{standalone:{one:"ఒక రోజు",other:"{{count}} రోజులు"},withPreposition:{one:"ఒక రోజు",other:"{{count}} రోజుల"}},aboutXWeeks:{standalone:{one:"సుమారు ఒక వారం",other:"సుమారు {{count}} వారాలు"},withPreposition:{one:"సుమారు ఒక వారం",other:"సుమారు {{count}} వారాలల"}},xWeeks:{standalone:{one:"ఒక వారం",other:"{{count}} వారాలు"},withPreposition:{one:"ఒక వారం",other:"{{count}} వారాలల"}},aboutXMonths:{standalone:{one:"సుమారు ఒక నెల",other:"సుమారు {{count}} నెలలు"},withPreposition:{one:"సుమారు ఒక నెల",other:"సుమారు {{count}} నెలల"}},xMonths:{standalone:{one:"ఒక నెల",other:"{{count}} నెలలు"},withPreposition:{one:"ఒక నెల",other:"{{count}} నెలల"}},aboutXYears:{standalone:{one:"సుమారు ఒక సంవత్సరం",other:"సుమారు {{count}} సంవత్సరాలు"},withPreposition:{one:"సుమారు ఒక సంవత్సరం",other:"సుమారు {{count}} సంవత్సరాల"}},xYears:{standalone:{one:"ఒక సంవత్సరం",other:"{{count}} సంవత్సరాలు"},withPreposition:{one:"ఒక సంవత్సరం",other:"{{count}} సంవత్సరాల"}},overXYears:{standalone:{one:"ఒక సంవత్సరం పైగా",other:"{{count}} సంవత్సరాలకు పైగా"},withPreposition:{one:"ఒక సంవత్సరం",other:"{{count}} సంవత్సరాల"}},almostXYears:{standalone:{one:"దాదాపు ఒక సంవత్సరం",other:"దాదాపు {{count}} సంవత్సరాలు"},withPreposition:{one:"దాదాపు ఒక సంవత్సరం",other:"దాదాపు {{count}} సంవత్సరాల"}}};function a(r,o,n){n=n||{};var s=n.addSuffix?t[r].withPreposition:t[r].standalone,u;return typeof s=="string"?u=s:o===1?u=s.one:u=s.other.replace("{{count}}",o),n.addSuffix?n.comparison>0?u+"లో":u+" క్రితం":u}},function(l,i,e){var t=e(2),a={full:"d, MMMM y, EEEE",long:"d MMMM, y",medium:"d MMM, y",short:"dd-MM-yy"},r={full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},o={full:"{{date}} {{time}}'కి'",long:"{{date}} {{time}}'కి'",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'గత' eeee p",yesterday:"'నిన్న' p",today:"'ఈ రోజు' p",tomorrow:"'రేపు' p",nextWeek:"'తదుపరి' eeee p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["క్రీ.పూ.","క్రీ.శ."],abbreviated:["క్రీ.పూ.","క్రీ.శ."],wide:["క్రీస్తు పూర్వం","క్రీస్తుశకం"]},r={narrow:["1","2","3","4"],abbreviated:["త్రై1","త్రై2","త్రై3","త్రై4"],wide:["1వ త్రైమాసికం","2వ త్రైమాసికం","3వ త్రైమాసికం","4వ త్రైమాసికం"]},o={narrow:["జ","ఫి","మా","ఏ","మే","జూ","జు","ఆ","సె","అ","న","డి"],abbreviated:["జన","ఫిబ్ర","మార్చి","ఏప్రి","మే","జూన్","జులై","ఆగ","సెప్టెం","అక్టో","నవం","డిసెం"],wide:["జనవరి","ఫిబ్రవరి","మార్చి","ఏప్రిల్","మే","జూన్","జులై","ఆగస్టు","సెప్టెంబర్","అక్టోబర్","నవంబర్","డిసెంబర్"]},n={narrow:["ఆ","సో","మ","బు","గు","శు","శ"],short:["ఆది","సోమ","మంగళ","బుధ","గురు","శుక్ర","శని"],abbreviated:["ఆది","సోమ","మంగళ","బుధ","గురు","శుక్ర","శని"],wide:["ఆదివారం","సోమవారం","మంగళవారం","బుధవారం","గురువారం","శుక్రవారం","శనివారం"]},s={narrow:{am:"పూర్వాహ్నం",pm:"అపరాహ్నం",midnight:"అర్ధరాత్రి",noon:"మిట్టమధ్యాహ్నం",morning:"ఉదయం",afternoon:"మధ్యాహ్నం",evening:"సాయంత్రం",night:"రాత్రి"},abbreviated:{am:"పూర్వాహ్నం",pm:"అపరాహ్నం",midnight:"అర్ధరాత్రి",noon:"మిట్టమధ్యాహ్నం",morning:"ఉదయం",afternoon:"మధ్యాహ్నం",evening:"సాయంత్రం",night:"రాత్రి"},wide:{am:"పూర్వాహ్నం",pm:"అపరాహ్నం",midnight:"అర్ధరాత్రి",noon:"మిట్టమధ్యాహ్నం",morning:"ఉదయం",afternoon:"మధ్యాహ్నం",evening:"సాయంత్రం",night:"రాత్రి"}},u={narrow:{am:"పూర్వాహ్నం",pm:"అపరాహ్నం",midnight:"అర్ధరాత్రి",noon:"మిట్టమధ్యాహ్నం",morning:"ఉదయం",afternoon:"మధ్యాహ్నం",evening:"సాయంత్రం",night:"రాత్రి"},abbreviated:{am:"పూర్వాహ్నం",pm:"అపరాహ్నం",midnight:"అర్ధరాత్రి",noon:"మిట్టమధ్యాహ్నం",morning:"ఉదయం",afternoon:"మధ్యాహ్నం",evening:"సాయంత్రం",night:"రాత్రి"},wide:{am:"పూర్వాహ్నం",pm:"అపరాహ్నం",midnight:"అర్ధరాత్రి",noon:"మిట్టమధ్యాహ్నం",morning:"ఉదయం",afternoon:"మధ్యాహ్నం",evening:"సాయంత్రం",night:"రాత్రి"}};function d(f,_){var c=Number(f);return c+"వ"}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(వ)?/i,o=/\d+/i,n={narrow:/^(క్రీ\.పూ\.|క్రీ\.శ\.)/i,abbreviated:/^(క్రీ\.?\s?పూ\.?|ప్ర\.?\s?శ\.?\s?పూ\.?|క్రీ\.?\s?శ\.?|సా\.?\s?శ\.?)/i,wide:/^(క్రీస్తు పూర్వం|ప్రస్తుత శకానికి పూర్వం|క్రీస్తు శకం|ప్రస్తుత శకం)/i},s={any:[/^(పూ|శ)/i,/^సా/i]},u={narrow:/^[1234]/i,abbreviated:/^త్రై[1234]/i,wide:/^[1234](వ)? త్రైమాసికం/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^(జూ|జు|జ|ఫి|మా|ఏ|మే|ఆ|సె|అ|న|డి)/i,abbreviated:/^(జన|ఫిబ్ర|మార్చి|ఏప్రి|మే|జూన్|జులై|ఆగ|సెప్|అక్టో|నవ|డిసె)/i,wide:/^(జనవరి|ఫిబ్రవరి|మార్చి|ఏప్రిల్|మే|జూన్|జులై|ఆగస్టు|సెప్టెంబర్|అక్టోబర్|నవంబర్|డిసెంబర్)/i},f={narrow:[/^జ/i,/^ఫి/i,/^మా/i,/^ఏ/i,/^మే/i,/^జూ/i,/^జు/i,/^ఆ/i,/^సె/i,/^అ/i,/^న/i,/^డి/i],any:[/^జన/i,/^ఫి/i,/^మా/i,/^ఏ/i,/^మే/i,/^జూన్/i,/^జులై/i,/^ఆగ/i,/^సె/i,/^అ/i,/^న/i,/^డి/i]},_={narrow:/^(ఆ|సో|మ|బు|గు|శు|శ)/i,short:/^(ఆది|సోమ|మం|బుధ|గురు|శుక్ర|శని)/i,abbreviated:/^(ఆది|సోమ|మం|బుధ|గురు|శుక్ర|శని)/i,wide:/^(ఆదివారం|సోమవారం|మంగళవారం|బుధవారం|గురువారం|శుక్రవారం|శనివారం)/i},c={narrow:[/^ఆ/i,/^సో/i,/^మ/i,/^బు/i,/^గు/i,/^శు/i,/^శ/i],any:[/^ఆది/i,/^సోమ/i,/^మం/i,/^బుధ/i,/^గురు/i,/^శుక్ర/i,/^శని/i]},v={narrow:/^(పూర్వాహ్నం|అపరాహ్నం|అర్ధరాత్రి|మిట్టమధ్యాహ్నం|ఉదయం|మధ్యాహ్నం|సాయంత్రం|రాత్రి)/i,any:/^(పూర్వాహ్నం|అపరాహ్నం|అర్ధరాత్రి|మిట్టమధ్యాహ్నం|ఉదయం|మధ్యాహ్నం|సాయంత్రం|రాత్రి)/i},g={any:{am:/^పూర్వాహ్నం/i,pm:/^అపరాహ్నం/i,midnight:/^అర్ధ/i,noon:/^మిట్ట/i,morning:/ఉదయం/i,afternoon:/మధ్యాహ్నం/i,evening:/సాయంత్రం/i,night:/రాత్రి/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(810),a=e(811),r=e(812),o=e(813),n=e(814),s={code:"th",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:0,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"น้อยกว่า 1 วินาที",other:"น้อยกว่า {{count}} วินาที"},xSeconds:{one:"1 วินาที",other:"{{count}} วินาที"},halfAMinute:"ครึ่งนาที",lessThanXMinutes:{one:"น้อยกว่า 1 นาที",other:"น้อยกว่า {{count}} นาที"},xMinutes:{one:"1 นาที",other:"{{count}} นาที"},aboutXHours:{one:"ประมาณ 1 ชั่วโมง",other:"ประมาณ {{count}} ชั่วโมง"},xHours:{one:"1 ชั่วโมง",other:"{{count}} ชั่วโมง"},xDays:{one:"1 วัน",other:"{{count}} วัน"},aboutXWeeks:{one:"ประมาณ 1 สัปดาห์",other:"ประมาณ {{count}} สัปดาห์"},xWeeks:{one:"1 สัปดาห์",other:"{{count}} สัปดาห์"},aboutXMonths:{one:"ประมาณ 1 เดือน",other:"ประมาณ {{count}} เดือน"},xMonths:{one:"1 เดือน",other:"{{count}} เดือน"},aboutXYears:{one:"ประมาณ 1 ปี",other:"ประมาณ {{count}} ปี"},xYears:{one:"1 ปี",other:"{{count}} ปี"},overXYears:{one:"มากกว่า 1 ปี",other:"มากกว่า {{count}} ปี"},almostXYears:{one:"เกือบ 1 ปี",other:"เกือบ {{count}} ปี"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?r==="halfAMinute"?"ใน"+s:"ใน "+s:s+"ที่ผ่านมา":s}},function(l,i,e){var t=e(2),a={full:"วันEEEEที่ do MMMM y",long:"do MMMM y",medium:"d MMM y",short:"dd/MM/yyyy"},r={full:"H:mm:ss น. zzzz",long:"H:mm:ss น. z",medium:"H:mm:ss น.",short:"H:mm น."},o={full:"{{date}} 'เวลา' {{time}}",long:"{{date}} 'เวลา' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"medium"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"eeee'ที่แล้วเวลา' p",yesterday:"'เมื่อวานนี้เวลา' p",today:"'วันนี้เวลา' p",tomorrow:"'พรุ่งนี้เวลา' p",nextWeek:"eeee 'เวลา' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["B","คศ"],abbreviated:["BC","ค.ศ."],wide:["ปีก่อนคริสตกาล","คริสต์ศักราช"]},r={narrow:["1","2","3","4"],abbreviated:["Q1","Q2","Q3","Q4"],wide:["ไตรมาสแรก","ไตรมาสที่สอง","ไตรมาสที่สาม","ไตรมาสที่สี่"]},o={narrow:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],short:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],abbreviated:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],wide:["อาทิตย์","จันทร์","อังคาร","พุธ","พฤหัสบดี","ศุกร์","เสาร์"]},n={narrow:["ม.ค.","ก.พ.","มี.ค.","เม.ย.","พ.ค.","มิ.ย.","ก.ค.","ส.ค.","ก.ย.","ต.ค.","พ.ย.","ธ.ค."],abbreviated:["ม.ค.","ก.พ.","มี.ค.","เม.ย.","พ.ค.","มิ.ย.","ก.ค.","ส.ค.","ก.ย.","ต.ค.","พ.ย.","ธ.ค."],wide:["มกราคม","กุมภาพันธ์","มีนาคม","เมษายน","พฤษภาคม","มิถุนายน","กรกฎาคม","สิงหาคม","กันยายน","ตุลาคม","พฤศจิกายน","ธันวาคม"]},s={narrow:{am:"ก่อนเที่ยง",pm:"หลังเที่ยง",midnight:"เที่ยงคืน",noon:"เที่ยง",morning:"เช้า",afternoon:"บ่าย",evening:"เย็น",night:"กลางคืน"},abbreviated:{am:"ก่อนเที่ยง",pm:"หลังเที่ยง",midnight:"เที่ยงคืน",noon:"เที่ยง",morning:"เช้า",afternoon:"บ่าย",evening:"เย็น",night:"กลางคืน"},wide:{am:"ก่อนเที่ยง",pm:"หลังเที่ยง",midnight:"เที่ยงคืน",noon:"เที่ยง",morning:"เช้า",afternoon:"บ่าย",evening:"เย็น",night:"กลางคืน"}},u={narrow:{am:"ก่อนเที่ยง",pm:"หลังเที่ยง",midnight:"เที่ยงคืน",noon:"เที่ยง",morning:"ตอนเช้า",afternoon:"ตอนกลางวัน",evening:"ตอนเย็น",night:"ตอนกลางคืน"},abbreviated:{am:"ก่อนเที่ยง",pm:"หลังเที่ยง",midnight:"เที่ยงคืน",noon:"เที่ยง",morning:"ตอนเช้า",afternoon:"ตอนกลางวัน",evening:"ตอนเย็น",night:"ตอนกลางคืน"},wide:{am:"ก่อนเที่ยง",pm:"หลังเที่ยง",midnight:"เที่ยงคืน",noon:"เที่ยง",morning:"ตอนเช้า",afternoon:"ตอนกลางวัน",evening:"ตอนเย็น",night:"ตอนกลางคืน"}};function d(f){var _=Number(f);return _}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:n,defaultWidth:"wide"}),day:t.a({values:o,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^\d+/i,o=/\d+/i,n={narrow:/^([bB]|[aA]|คศ)/i,abbreviated:/^([bB]\.?\s?[cC]\.?|b\.?\s?c\.?\s?e\.?|a\.?\s?d\.?|c\.?\s?e\.?|ค\.?ศ\.?)/i,wide:/^(ก่อนคริสตกาล|คริสต์ศักราช|คริสตกาล)/i},s={any:[/^[bB]/i,/^(^[aA]|ค\.?ศ\.?|คริสตกาล|คริสต์ศักราช|)/i]},u={narrow:/^[1234]/i,abbreviated:/^q[1234]/i,wide:/^ไตรมาส(ที่)? ?[1234]/i},d={any:[/(1|แรก|หนึ่ง)/i,/(2|สอง)/i,/(3|สาม)/i,/(4|สี่)/i]},m={narrow:/^(ม\.?ค\.?|ก\.?พ\.?|มี\.?ค\.?|เม\.?ย\.?|พ\.?ค\.?|มิ\.?ย\.?|ก\.?ค\.?|ส\.?ค\.?|ก\.?ย\.?|ต\.?ค\.?|พ\.?ย\.?|ธ\.?ค\.?)/i,abbreviated:/^(ม\.?ค\.?|ก\.?พ\.?|มี\.?ค\.?|เม\.?ย\.?|พ\.?ค\.?|มิ\.?ย\.?|ก\.?ค\.?|ส\.?ค\.?|ก\.?ย\.?|ต\.?ค\.?|พ\.?ย\.?|ธ\.?ค\.?')/i,wide:/^(มกราคม|กุมภาพันธ์|มีนาคม|เมษายน|พฤษภาคม|มิถุนายน|กรกฎาคม|สิงหาคม|กันยายน|ตุลาคม|พฤศจิกายน|ธันวาคม)/i},f={wide:[/^มก/i,/^กุม/i,/^มี/i,/^เม/i,/^พฤษ/i,/^มิ/i,/^กรก/i,/^ส/i,/^กัน/i,/^ต/i,/^พฤศ/i,/^ธ/i],any:[/^ม\.?ค\.?/i,/^ก\.?พ\.?/i,/^มี\.?ค\.?/i,/^เม\.?ย\.?/i,/^พ\.?ค\.?/i,/^มิ\.?ย\.?/i,/^ก\.?ค\.?/i,/^ส\.?ค\.?/i,/^ก\.?ย\.?/i,/^ต\.?ค\.?/i,/^พ\.?ย\.?/i,/^ธ\.?ค\.?/i]},_={narrow:/^(อา\.?|จ\.?|อ\.?|พฤ\.?|พ\.?|ศ\.?|ส\.?)/i,short:/^(อา\.?|จ\.?|อ\.?|พฤ\.?|พ\.?|ศ\.?|ส\.?)/i,abbreviated:/^(อา\.?|จ\.?|อ\.?|พฤ\.?|พ\.?|ศ\.?|ส\.?)/i,wide:/^(อาทิตย์|จันทร์|อังคาร|พุธ|พฤหัสบดี|ศุกร์|เสาร์)/i},c={wide:[/^อา/i,/^จั/i,/^อั/i,/^พุธ/i,/^พฤ/i,/^ศ/i,/^เส/i],any:[/^อา/i,/^จ/i,/^อ/i,/^พ(?!ฤ)/i,/^พฤ/i,/^ศ/i,/^ส/i]},v={any:/^(ก่อนเที่ยง|หลังเที่ยง|เที่ยงคืน|เที่ยง|(ตอน.*?)?.*(เที่ยง|เช้า|บ่าย|เย็น|กลางคืน))/i},g={any:{am:/^ก่อนเที่ยง/i,pm:/^หลังเที่ยง/i,midnight:/^เที่ยงคืน/i,noon:/^เที่ยง/i,morning:/เช้า/i,afternoon:/บ่าย/i,evening:/เย็น/i,night:/กลางคืน/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(816),a=e(817),r=e(818),o=e(819),n=e(820),s={code:"tr",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"bir saniyeden az",other:"{{count}} saniyeden az"},xSeconds:{one:"1 saniye",other:"{{count}} saniye"},halfAMinute:"yarım dakika",lessThanXMinutes:{one:"bir dakikadan az",other:"{{count}} dakikadan az"},xMinutes:{one:"1 dakika",other:"{{count}} dakika"},aboutXHours:{one:"yaklaşık 1 saat",other:"yaklaşık {{count}} saat"},xHours:{one:"1 saat",other:"{{count}} saat"},xDays:{one:"1 gün",other:"{{count}} gün"},aboutXWeeks:{one:"yaklaşık 1 hafta",other:"yaklaşık {{count}} hafta"},xWeeks:{one:"1 hafta",other:"{{count}} hafta"},aboutXMonths:{one:"yaklaşık 1 ay",other:"yaklaşık {{count}} ay"},xMonths:{one:"1 ay",other:"{{count}} ay"},aboutXYears:{one:"yaklaşık 1 yıl",other:"yaklaşık {{count}} yıl"},xYears:{one:"1 yıl",other:"{{count}} yıl"},overXYears:{one:"1 yıldan fazla",other:"{{count}} yıldan fazla"},almostXYears:{one:"neredeyse 1 yıl",other:"neredeyse {{count}} yıl"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?s+" sonra":s+" önce":s}},function(l,i,e){var t=e(2),a={full:"d MMMM y EEEE",long:"d MMMM y",medium:"d MMM y",short:"dd.MM.yyyy"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} 'saat' {{time}}",long:"{{date}} 'saat' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'geçen hafta' eeee 'saat' p",yesterday:"'dün saat' p",today:"'bugün saat' p",tomorrow:"'yarın saat' p",nextWeek:"eeee 'saat' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={abbreviated:["MÖ","MS"],narrow:["MÖ","MS"],wide:["Milattan Önce","Milattan Sonra"]},r={narrow:["1","2","3","4"],abbreviated:["1Ç","2Ç","3Ç","4Ç"],wide:["İlk çeyrek","İkinci Çeyrek","Üçüncü çeyrek","Son çeyrek"]},o={narrow:["O","Ş","M","N","M","H","T","A","E","E","K","A"],abbreviated:["Oca","Şub","Mar","Nis","May","Haz","Tem","Ağu","Eyl","Eki","Kas","Ara"],wide:["Ocak","Şubat","Mart","Nisan","Mayıs","Haziran","Temmuz","Ağustos","Eylül","Ekim","Kasım","Aralık"]},n={narrow:["P","P","S","Ç","P","C","C"],short:["Pz","Pt","Sa","Ça","Pe","Cu","Ct"],abbreviated:["Paz","Pts","Sal","Çar","Per","Cum","Cts"],wide:["Pazar","Pazartesi","Salı","Çarşamba","Perşembe","Cuma","Cumartesi"]},s={narrow:{am:"öö",pm:"ös",midnight:"gy",noon:"ö",morning:"sa",afternoon:"ös",evening:"ak",night:"ge"},abbreviated:{am:"ÖÖ",pm:"ÖS",midnight:"gece yarısı",noon:"öğle",morning:"sabah",afternoon:"öğleden sonra",evening:"akşam",night:"gece"},wide:{am:"Ö.Ö.",pm:"Ö.S.",midnight:"gece yarısı",noon:"öğle",morning:"sabah",afternoon:"öğleden sonra",evening:"akşam",night:"gece"}},u={narrow:{am:"öö",pm:"ös",midnight:"gy",noon:"ö",morning:"sa",afternoon:"ös",evening:"ak",night:"ge"},abbreviated:{am:"ÖÖ",pm:"ÖS",midnight:"gece yarısı",noon:"öğlen",morning:"sabahleyin",afternoon:"öğleden sonra",evening:"akşamleyin",night:"geceleyin"},wide:{am:"ö.ö.",pm:"ö.s.",midnight:"gece yarısı",noon:"öğlen",morning:"sabahleyin",afternoon:"öğleden sonra",evening:"akşamleyin",night:"geceleyin"}};function d(f,_){var c=Number(f);return c+"."}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaulFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(\.)?/i,o=/\d+/i,n={narrow:/^(mö|ms)/i,abbreviated:/^(mö|ms)/i,wide:/^(milattan önce|milattan sonra)/i},s={any:[/(^mö|^milattan önce)/i,/(^ms|^milattan sonra)/i]},u={narrow:/^[1234]/i,abbreviated:/^[1234]ç/i,wide:/^((i|İ)lk|(i|İ)kinci|üçüncü|son) çeyrek/i},d={any:[/1/i,/2/i,/3/i,/4/i],abbreviated:[/1ç/i,/2ç/i,/3ç/i,/4ç/i],wide:[/^(i|İ)lk çeyrek/i,/(i|İ)kinci çeyrek/i,/üçüncü çeyrek/i,/son çeyrek/i]},m={narrow:/^[oşmnhtaek]/i,abbreviated:/^(oca|şub|mar|nis|may|haz|tem|ağu|eyl|eki|kas|ara)/i,wide:/^(ocak|şubat|mart|nisan|mayıs|haziran|temmuz|ağustos|eylül|ekim|kasım|aralık)/i},f={narrow:[/^o/i,/^ş/i,/^m/i,/^n/i,/^m/i,/^h/i,/^t/i,/^a/i,/^e/i,/^e/i,/^k/i,/^a/i],any:[/^o/i,/^ş/i,/^mar/i,/^n/i,/^may/i,/^h/i,/^t/i,/^ağ/i,/^ey/i,/^ek/i,/^k/i,/^ar/i]},_={narrow:/^[psçc]/i,short:/^(pz|pt|sa|ça|pe|cu|ct)/i,abbreviated:/^(paz|pts|sal|çar|per|cum|cts)/i,wide:/^(pazar|pazartesi|salı|çarşamba|perşembe|cuma|cumartesi)/i},c={narrow:[/^p/i,/^p/i,/^s/i,/^ç/i,/^p/i,/^c/i,/^c/i],any:[/^pz/i,/^pt/i,/^sa/i,/^ça/i,/^pe/i,/^cu/i,/^ct/i],wide:[/^pazar/i,/^pazartesi/i,/^salı/i,/^çarşamba/i,/^perşembe/i,/^cuma/i,/cumartesi/i]},v={narrow:/^(öö|ös|gy|ö|sa|ös|ak|ge)/i,any:/^(ö\.?\s?[ös]\.?|öğleden sonra|gece yarısı|öğle|(sabah|öğ|akşam|gece)(leyin))/i},g={any:{am:/^ö\.?ö\.?/i,pm:/^ö\.?s\.?/i,midnight:/^(gy|gece yarısı)/i,noon:/^öğ/i,morning:/^sa/i,afternoon:/^öğleden sonra/i,evening:/^ak/i,night:/^ge/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(822),a=e(823),r=e(824),o=e(825),n=e(826),s={code:"ug",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:0,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"بىر سىكۇنت ئىچىدە",other:"سىكۇنت ئىچىدە {{count}}"},xSeconds:{one:"بىر سىكۇنت",other:"سىكۇنت {{count}}"},halfAMinute:"يىرىم مىنۇت",lessThanXMinutes:{one:"بىر مىنۇت ئىچىدە",other:"مىنۇت ئىچىدە {{count}}"},xMinutes:{one:"بىر مىنۇت",other:"مىنۇت {{count}}"},aboutXHours:{one:"تەخمىنەن بىر سائەت",other:"سائەت {{count}} تەخمىنەن"},xHours:{one:"بىر سائەت",other:"سائەت {{count}}"},xDays:{one:"بىر كۈن",other:"كۈن {{count}}"},aboutXWeeks:{one:"تەخمىنەن بىرھەپتە",other:"ھەپتە {{count}} تەخمىنەن"},xWeeks:{one:"بىرھەپتە",other:"ھەپتە {{count}}"},aboutXMonths:{one:"تەخمىنەن بىر ئاي",other:"ئاي {{count}} تەخمىنەن"},xMonths:{one:"بىر ئاي",other:"ئاي {{count}}"},aboutXYears:{one:"تەخمىنەن بىر يىل",other:"يىل {{count}} تەخمىنەن"},xYears:{one:"بىر يىل",other:"يىل {{count}}"},overXYears:{one:"بىر يىلدىن ئارتۇق",other:"يىلدىن ئارتۇق {{count}}"},almostXYears:{one:"ئاساسەن بىر يىل",other:"يىل {{count}} ئاساسەن"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?s:s+" بولدى":s}},function(l,i,e){var t=e(2),a={full:"EEEE, MMMM do, y",long:"MMMM do, y",medium:"MMM d, y",short:"MM/dd/yyyy"},r={full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},o={full:"{{date}} 'دە' {{time}}",long:"{{date}} 'دە' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'ئ‍ۆتكەن' eeee 'دە' p",yesterday:"'تۈنۈگۈن دە' p",today:"'بۈگۈن دە' p",tomorrow:"'ئەتە دە' p",nextWeek:"eeee 'دە' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["ب","ك"],abbreviated:["ب","ك"],wide:["مىيلادىدىن بۇرۇن","مىيلادىدىن كىيىن"]},r={narrow:["1","2","3","4"],abbreviated:["1","2","3","4"],wide:["بىرىنجى چارەك","ئىككىنجى چارەك","ئۈچىنجى چارەك","تۆتىنجى چارەك"]},o={narrow:["ي","ف","م","ا","م","ى","ى","ا","س","ۆ","ن","د"],abbreviated:["يانۋار","فېۋىرال","مارت","ئاپرىل","ماي","ئىيۇن","ئىيول","ئاۋغۇست","سىنتەبىر","ئۆكتەبىر","نويابىر","دىكابىر"],wide:["يانۋار","فېۋىرال","مارت","ئاپرىل","ماي","ئىيۇن","ئىيول","ئاۋغۇست","سىنتەبىر","ئۆكتەبىر","نويابىر","دىكابىر"]},n={narrow:["ي","د","س","چ","پ","ج","ش"],short:["ي","د","س","چ","پ","ج","ش"],abbreviated:["يەكشەنبە","دۈشەنبە","سەيشەنبە","چارشەنبە","پەيشەنبە","جۈمە","شەنبە"],wide:["يەكشەنبە","دۈشەنبە","سەيشەنبە","چارشەنبە","پەيشەنبە","جۈمە","شەنبە"]},s={narrow:{am:"ئە",pm:"چ",midnight:"ك",noon:"چ",morning:"ئەتىگەن",afternoon:"چۈشتىن كىيىن",evening:"ئاخشىم",night:"كىچە"},abbreviated:{am:"ئە",pm:"چ",midnight:"ك",noon:"چ",morning:"ئەتىگەن",afternoon:"چۈشتىن كىيىن",evening:"ئاخشىم",night:"كىچە"},wide:{am:"ئە",pm:"چ",midnight:"ك",noon:"چ",morning:"ئەتىگەن",afternoon:"چۈشتىن كىيىن",evening:"ئاخشىم",night:"كىچە"}},u={narrow:{am:"ئە",pm:"چ",midnight:"ك",noon:"چ",morning:"ئەتىگەندە",afternoon:"چۈشتىن كىيىن",evening:"ئاخشامدا",night:"كىچىدە"},abbreviated:{am:"ئە",pm:"چ",midnight:"ك",noon:"چ",morning:"ئەتىگەندە",afternoon:"چۈشتىن كىيىن",evening:"ئاخشامدا",night:"كىچىدە"},wide:{am:"ئە",pm:"چ",midnight:"ك",noon:"چ",morning:"ئەتىگەندە",afternoon:"چۈشتىن كىيىن",evening:"ئاخشامدا",night:"كىچىدە"}};function d(f,_){return String(f)}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(th|st|nd|rd)?/i,o=/\d+/i,n={narrow:/^(ب|ك)/i,wide:/^(مىيلادىدىن بۇرۇن|مىيلادىدىن كىيىن)/i},s={any:[/^بۇرۇن/i,/^كىيىن/i]},u={narrow:/^[1234]/i,abbreviated:/^چ[1234]/i,wide:/^چارەك [1234]/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[يفمئامئ‍ئاسۆند]/i,abbreviated:/^(يانۋار|فېۋىرال|مارت|ئاپرىل|ماي|ئىيۇن|ئىيول|ئاۋغۇست|سىنتەبىر|ئۆكتەبىر|نويابىر|دىكابىر)/i,wide:/^(يانۋار|فېۋىرال|مارت|ئاپرىل|ماي|ئىيۇن|ئىيول|ئاۋغۇست|سىنتەبىر|ئۆكتەبىر|نويابىر|دىكابىر)/i},f={narrow:[/^ي/i,/^ف/i,/^م/i,/^ا/i,/^م/i,/^ى‍/i,/^ى‍/i,/^ا‍/i,/^س/i,/^ۆ/i,/^ن/i,/^د/i],any:[/^يان/i,/^فېۋ/i,/^مار/i,/^ئاپ/i,/^ماي/i,/^ئىيۇن/i,/^ئىيول/i,/^ئاۋ/i,/^سىن/i,/^ئۆك/i,/^نوي/i,/^دىك/i]},_={narrow:/^[دسچپجشي]/i,short:/^(يە|دۈ|سە|چا|پە|جۈ|شە)/i,abbreviated:/^(يە|دۈ|سە|چا|پە|جۈ|شە)/i,wide:/^(يەكشەنبە|دۈشەنبە|سەيشەنبە|چارشەنبە|پەيشەنبە|جۈمە|شەنبە)/i},c={narrow:[/^ي/i,/^د/i,/^س/i,/^چ/i,/^پ/i,/^ج/i,/^ش/i],any:[/^ي/i,/^د/i,/^س/i,/^چ/i,/^پ/i,/^ج/i,/^ش/i]},v={narrow:/^(ئە|چ|ك|چ|(دە|ئەتىگەن) ( ئە‍|چۈشتىن كىيىن|ئاخشىم|كىچە))/i,any:/^(ئە|چ|ك|چ|(دە|ئەتىگەن) ( ئە‍|چۈشتىن كىيىن|ئاخشىم|كىچە))/i},g={any:{am:/^ئە/i,pm:/^چ/i,midnight:/^ك/i,noon:/^چ/i,morning:/ئەتىگەن/i,afternoon:/چۈشتىن كىيىن/i,evening:/ئاخشىم/i,night:/كىچە/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(828),a=e(829),r=e(830),o=e(831),n=e(832),s={code:"uk",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=o;function t(n,s){if(n.one!==void 0&&s===1)return n.one;var u=s%10,d=s%100;return u===1&&d!==11?n.singularNominative.replace("{{count}}",s):u>=2&&u<=4&&(d<10||d>20)?n.singularGenitive.replace("{{count}}",s):n.pluralGenitive.replace("{{count}}",s)}function a(n){return function(s,u){return u.addSuffix?u.comparison>0?n.future?t(n.future,s):"за "+t(n.regular,s):n.past?t(n.past,s):t(n.regular,s)+" тому":t(n.regular,s)}}var r={lessThanXSeconds:a({regular:{one:"менше секунди",singularNominative:"менше {{count}} секунди",singularGenitive:"менше {{count}} секунд",pluralGenitive:"менше {{count}} секунд"},future:{one:"менше, ніж за секунду",singularNominative:"менше, ніж за {{count}} секунду",singularGenitive:"менше, ніж за {{count}} секунди",pluralGenitive:"менше, ніж за {{count}} секунд"}}),xSeconds:a({regular:{singularNominative:"{{count}} секунда",singularGenitive:"{{count}} секунди",pluralGenitive:"{{count}} секунд"},past:{singularNominative:"{{count}} секунду тому",singularGenitive:"{{count}} секунди тому",pluralGenitive:"{{count}} секунд тому"},future:{singularNominative:"за {{count}} секунду",singularGenitive:"за {{count}} секунди",pluralGenitive:"за {{count}} секунд"}}),halfAMinute:function(n,s){return s.addSuffix?s.comparison>0?"за півхвилини":"півхвилини тому":"півхвилини"},lessThanXMinutes:a({regular:{one:"менше хвилини",singularNominative:"менше {{count}} хвилини",singularGenitive:"менше {{count}} хвилин",pluralGenitive:"менше {{count}} хвилин"},future:{one:"менше, ніж за хвилину",singularNominative:"менше, ніж за {{count}} хвилину",singularGenitive:"менше, ніж за {{count}} хвилини",pluralGenitive:"менше, ніж за {{count}} хвилин"}}),xMinutes:a({regular:{singularNominative:"{{count}} хвилина",singularGenitive:"{{count}} хвилини",pluralGenitive:"{{count}} хвилин"},past:{singularNominative:"{{count}} хвилину тому",singularGenitive:"{{count}} хвилини тому",pluralGenitive:"{{count}} хвилин тому"},future:{singularNominative:"за {{count}} хвилину",singularGenitive:"за {{count}} хвилини",pluralGenitive:"за {{count}} хвилин"}}),aboutXHours:a({regular:{singularNominative:"близько {{count}} години",singularGenitive:"близько {{count}} годин",pluralGenitive:"близько {{count}} годин"},future:{singularNominative:"приблизно за {{count}} годину",singularGenitive:"приблизно за {{count}} години",pluralGenitive:"приблизно за {{count}} годин"}}),xHours:a({regular:{singularNominative:"{{count}} годину",singularGenitive:"{{count}} години",pluralGenitive:"{{count}} годин"}}),xDays:a({regular:{singularNominative:"{{count}} день",singularGenitive:"{{count}} дня",pluralGenitive:"{{count}} днів"}}),aboutXWeeks:a({regular:{singularNominative:"близько {{count}} тижня",singularGenitive:"близько {{count}} тижнів",pluralGenitive:"близько {{count}} тижнів"},future:{singularNominative:"приблизно за {{count}} тиждень",singularGenitive:"приблизно за {{count}} тижні",pluralGenitive:"приблизно за {{count}} тижні"}}),xWeeks:a({regular:{singularNominative:"{{count}} тиждень",singularGenitive:"{{count}} тижня",pluralGenitive:"{{count}} тижні"}}),aboutXMonths:a({regular:{singularNominative:"близько {{count}} місяця",singularGenitive:"близько {{count}} місяців",pluralGenitive:"близько {{count}} місяців"},future:{singularNominative:"приблизно за {{count}} місяць",singularGenitive:"приблизно за {{count}} місяця",pluralGenitive:"приблизно за {{count}} місяців"}}),xMonths:a({regular:{singularNominative:"{{count}} місяць",singularGenitive:"{{count}} місяця",pluralGenitive:"{{count}} місяців"}}),aboutXYears:a({regular:{singularNominative:"близько {{count}} року",singularGenitive:"близько {{count}} років",pluralGenitive:"близько {{count}} років"},future:{singularNominative:"приблизно за {{count}} рік",singularGenitive:"приблизно за {{count}} роки",pluralGenitive:"приблизно за {{count}} років"}}),xYears:a({regular:{singularNominative:"{{count}} рік",singularGenitive:"{{count}} роки",pluralGenitive:"{{count}} років"}}),overXYears:a({regular:{singularNominative:"більше {{count}} року",singularGenitive:"більше {{count}} років",pluralGenitive:"більше {{count}} років"},future:{singularNominative:"більше, ніж за {{count}} рік",singularGenitive:"більше, ніж за {{count}} роки",pluralGenitive:"більше, ніж за {{count}} років"}}),almostXYears:a({regular:{singularNominative:"майже {{count}} рік",singularGenitive:"майже {{count}} роки",pluralGenitive:"майже {{count}} років"},future:{singularNominative:"майже за {{count}} рік",singularGenitive:"майже за {{count}} роки",pluralGenitive:"майже за {{count}} років"}})};function o(n,s,u){return u=u||{},r[n](s,u)}},function(l,i,e){var t=e(2),a={full:"EEEE, do MMMM y 'р.'",long:"do MMMM y 'р.'",medium:"d MMM y 'р.'",short:"dd.MM.y"},r={full:"H:mm:ss zzzz",long:"H:mm:ss z",medium:"H:mm:ss",short:"H:mm"},o={full:"{{date}} 'о' {{time}}",long:"{{date}} 'о' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=u;var t=e(13),a=["неділю","понеділок","вівторок","середу","четвер","п’ятницю","суботу"];function r(d){var m=a[d];switch(d){case 0:case 3:case 5:case 6:return"'у минулу "+m+" о' p";case 1:case 2:case 4:return"'у минулий "+m+" о' p"}}function o(d){var m=a[d];return"'у "+m+" о' p"}function n(d){var m=a[d];switch(d){case 0:case 3:case 5:case 6:return"'у наступну "+m+" о' p";case 1:case 2:case 4:return"'у наступний "+m+" о' p"}}var s={lastWeek:function(d,m,f){var _=d.getUTCDay();return t.a(d,m,f)?o(_):r(_)},yesterday:"'вчора о' p",today:"'сьогодні о' p",tomorrow:"'завтра о' p",nextWeek:function(d,m,f){var _=d.getUTCDay();return t.a(d,m,f)?o(_):n(_)},other:"P"};function u(d,m,f,_){var c=s[d];return typeof c=="function"?c(m,f,_):c}},function(l,i,e){var t=e(3),a={narrow:["до н.е.","н.е."],abbreviated:["до н. е.","н. е."],wide:["до нашої ери","нашої ери"]},r={narrow:["1","2","3","4"],abbreviated:["1-й кв.","2-й кв.","3-й кв.","4-й кв."],wide:["1-й квартал","2-й квартал","3-й квартал","4-й квартал"]},o={narrow:["С","Л","Б","К","Т","Ч","Л","С","В","Ж","Л","Г"],abbreviated:["січ.","лют.","берез.","квіт.","трав.","черв.","лип.","серп.","верес.","жовт.","листоп.","груд."],wide:["січень","лютий","березень","квітень","травень","червень","липень","серпень","вересень","жовтень","листопад","грудень"]},n={narrow:["С","Л","Б","К","Т","Ч","Л","С","В","Ж","Л","Г"],abbreviated:["січ.","лют.","берез.","квіт.","трав.","черв.","лип.","серп.","верес.","жовт.","листоп.","груд."],wide:["січня","лютого","березня","квітня","травня","червня","липня","серпня","вересня","жовтня","листопада","грудня"]},s={narrow:["Н","П","В","С","Ч","П","С"],short:["нд","пн","вт","ср","чт","пт","сб"],abbreviated:["нед","пон","вів","сер","чтв","птн","суб"],wide:["неділя","понеділок","вівторок","середа","четвер","п’ятниця","субота"]},u={narrow:{am:"ДП",pm:"ПП",midnight:"півн.",noon:"пол.",morning:"ранок",afternoon:"день",evening:"веч.",night:"ніч"},abbreviated:{am:"ДП",pm:"ПП",midnight:"півн.",noon:"пол.",morning:"ранок",afternoon:"день",evening:"веч.",night:"ніч"},wide:{am:"ДП",pm:"ПП",midnight:"північ",noon:"полудень",morning:"ранок",afternoon:"день",evening:"вечір",night:"ніч"}},d={narrow:{am:"ДП",pm:"ПП",midnight:"півн.",noon:"пол.",morning:"ранку",afternoon:"дня",evening:"веч.",night:"ночі"},abbreviated:{am:"ДП",pm:"ПП",midnight:"півн.",noon:"пол.",morning:"ранку",afternoon:"дня",evening:"веч.",night:"ночі"},wide:{am:"ДП",pm:"ПП",midnight:"північ",noon:"полудень",morning:"ранку",afternoon:"дня",evening:"веч.",night:"ночі"}};function m(_,c){var v=c||{},g=String(v.unit),P;return g==="date"?_===3||_===23?P="-є":P="-е":g==="minute"||g==="second"||g==="hour"?P="-а":P="-й",_+P}var f={ordinalNumber:m,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(_){return Number(_)-1}}),month:t.a({values:o,defaultWidth:"wide",formattingValues:n,defaultFormattingWidth:"wide"}),day:t.a({values:s,defaultWidth:"wide"}),dayPeriod:t.a({values:u,defaultWidth:"any",formattingValues:d,defaultFormattingWidth:"wide"})};i.a=f},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(-?(е|й|є|а|я))?/i,o=/\d+/i,n={narrow:/^((до )?н\.?\s?е\.?)/i,abbreviated:/^((до )?н\.?\s?е\.?)/i,wide:/^(до нашої ери|нашої ери|наша ера)/i},s={any:[/^д/i,/^н/i]},u={narrow:/^[1234]/i,abbreviated:/^[1234](-?[иі]?й?)? кв.?/i,wide:/^[1234](-?[иі]?й?)? квартал/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[слбктчвжг]/i,abbreviated:/^(січ|лют|бер|берез|кві|трав?|чер|лип|сер|вер|жов|лис(топ)?|груд)\.?/i,wide:/^(січень|січня|лютий|лютого|березень|березня|квітень|квітня|травень|травня|липень|липня|серпень|серпня|вересень|вересня|жовтень|жовтня|листопада?|грудень|грудня)/i},f={narrow:[/^с/i,/^л/i,/^б/i,/^к/i,/^т/i,/^ч/i,/^л/i,/^с/i,/^в/i,/^ж/i,/^л/i,/^г/i],any:[/^сі/i,/^лю/i,/^б/i,/^к/i,/^т/i,/^ч/i,/^лип/i,/^се/i,/^в/i,/^ж/i,/^лис/i,/^г/i]},_={narrow:/^[нпвсч]/i,short:/^(нд|пн|вт|ср|чт|пт|сб)\.?/i,abbreviated:/^(нед|пон|вів|сер|че?тв|птн?|суб)\.?/i,wide:/^(неділ[яі]|понеділ[ок][ка]|вівтор[ок][ка]|серед[аи]|четвер(га)?|п\W*?ятниц[яі]|субот[аи])/i},c={narrow:[/^н/i,/^п/i,/^в/i,/^с/i,/^ч/i,/^п/i,/^с/i],any:[/^н/i,/^п[он]/i,/^в/i,/^с[ер]/i,/^ч/i,/^п\W*?[ят]/i,/^с[уб]/i]},v={narrow:/^([дп]п|півн\.?|пол\.?|ранок|ранку|день|дня|веч\.?|ніч|ночі)/i,abbreviated:/^([дп]п|півн\.?|пол\.?|ранок|ранку|день|дня|веч\.?|ніч|ночі)/i,wide:/^([дп]п|північ|полудень|ранок|ранку|день|дня|вечір|вечора|ніч|ночі)/i},g={any:{am:/^дп/i,pm:/^пп/i,midnight:/^півн/i,noon:/^пол/i,morning:/^р/i,afternoon:/^д[ен]/i,evening:/^в/i,night:/^н/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"wide",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(834),a=e(835),r=e(836),o=e(837),n=e(838),s={code:"uz",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"sekunddan kam",other:"{{count}} sekunddan kam"},xSeconds:{one:"1 sekund",other:"{{count}} sekund"},halfAMinute:"yarim minut",lessThanXMinutes:{one:"bir minutdan kam",other:"{{count}} minutdan kam"},xMinutes:{one:"1 minut",other:"{{count}} minut"},aboutXHours:{one:"tahminan 1 soat",other:"tahminan {{count}} soat"},xHours:{one:"1 soat",other:"{{count}} soat"},xDays:{one:"1 kun",other:"{{count}} kun"},aboutXWeeks:{one:"tahminan 1 hafta",other:"tahminan {{count}} hafta"},xWeeks:{one:"1 hafta",other:"{{count}} hafta"},aboutXMonths:{one:"tahminan 1 oy",other:"tahminan {{count}} oy"},xMonths:{one:"1 oy",other:"{{count}} oy"},aboutXYears:{one:"tahminan 1 yil",other:"tahminan {{count}} yil"},xYears:{one:"1 yil",other:"{{count}} yil"},overXYears:{one:"1 yildan ko'p",other:"{{count}} yildan ko'p"},almostXYears:{one:"deyarli 1 yil",other:"deyarli {{count}} yil"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?s+" dan keyin":s+" oldin":s}},function(l,i,e){var t=e(2),a={full:"EEEE, do MMMM, y",long:"do MMMM, y",medium:"d MMM, y",short:"dd/MM/yyyy"},r={full:"h:mm:ss zzzz",long:"h:mm:ss z",medium:"h:mm:ss",short:"h:mm"},o={any:"{{date}}, {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"any"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'oldingi' eeee p 'da'",yesterday:"'kecha' p 'da'",today:"'bugun' p 'da'",tomorrow:"'ertaga' p 'da'",nextWeek:"eeee p 'da'",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["M.A","M."],abbreviated:["M.A","M."],wide:["Miloddan Avvalgi","Milodiy"]},r={narrow:["1","2","3","4"],abbreviated:["CH.1","CH.2","CH.3","CH.4"],wide:["1-chi chorak","2-chi chorak","3-chi chorak","4-chi chorak"]},o={narrow:["Y","F","M","A","M","I","I","A","S","O","N","D"],abbreviated:["Yan","Fev","Mar","Apr","May","Iyun","Iyul","Avg","Sen","Okt","Noy","Dek"],wide:["Yanvar","Fevral","Mart","Aprel","May","Iyun","Iyul","Avgust","Sentabr","Oktabr","Noyabr","Dekabr"]},n={narrow:["Y","D","S","CH","P","J","SH"],short:["Ya","Du","Se","Cho","Pa","Ju","Sha"],abbreviated:["Yak","Dush","Sesh","Chor","Pay","Jum","Shan"],wide:["Yakshanba","Dushanba","Seshanba","Chorshanba","Payshanba","Juma","Shanba"]},s={narrow:{am:"a",pm:"p",midnight:"y.t",noon:"p.",morning:"ertalab",afternoon:"tushdan keyin",evening:"kechqurun",night:"tun"},abbreviated:{am:"AM",pm:"PM",midnight:"yarim tun",noon:"peshin",morning:"ertalab",afternoon:"tushdan keyin",evening:"kechqurun",night:"tun"},wide:{am:"a.m.",pm:"p.m.",midnight:"yarim tun",noon:"peshin",morning:"ertalab",afternoon:"tushdan keyin",evening:"kechqurun",night:"tun"}},u={narrow:{am:"a",pm:"p",midnight:"y.t",noon:"p.",morning:"ertalab",afternoon:"tushdan keyin",evening:"kechqurun",night:"tun"},abbreviated:{am:"AM",pm:"PM",midnight:"yarim tun",noon:"peshin",morning:"ertalab",afternoon:"tushdan keyin",evening:"kechqurun",night:"tun"},wide:{am:"a.m.",pm:"p.m.",midnight:"yarim tun",noon:"peshin",morning:"ertalab",afternoon:"tushdan keyin",evening:"kechqurun",night:"tun"}};function d(f,_){var c=Number(f);return c}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)(chi)?/i,o=/\d+/i,n={narrow:/^(m\.a|m\.)/i,abbreviated:/^(m\.a\.?\s?m\.?)/i,wide:/^(miloddan avval|miloddan keyin)/i},s={any:[/^b/i,/^(a|c)/i]},u={narrow:/^[1234]/i,abbreviated:/^q[1234]/i,wide:/^[1234](chi)? chorak/i},d={any:[/1/i,/2/i,/3/i,/4/i]},m={narrow:/^[yfmasond]/i,abbreviated:/^(yan|fev|mar|apr|may|iyun|iyul|avg|sen|okt|noy|dek)/i,wide:/^(yanvar|fevral|mart|aprel|may|iyun|iyul|avgust|sentabr|oktabr|noyabr|dekabr)/i},f={narrow:[/^y/i,/^f/i,/^m/i,/^a/i,/^m/i,/^i/i,/^i/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ya/i,/^f/i,/^mar/i,/^ap/i,/^may/i,/^iyun/i,/^iyul/i,/^av/i,/^s/i,/^o/i,/^n/i,/^d/i]},_={narrow:/^[ydschj]/i,short:/^(ya|du|se|cho|pa|ju|sha)/i,abbreviated:/^(yak|dush|sesh|chor|pay|jum|shan)/i,wide:/^(yakshanba|dushanba|seshanba|chorshanba|payshanba|juma|shanba)/i},c={narrow:[/^y/i,/^d/i,/^s/i,/^ch/i,/^p/i,/^j/i,/^sh/i],any:[/^ya/i,/^d/i,/^se/i,/^ch/i,/^p/i,/^j/i,/^sh/i]},v={narrow:/^(a|p|y\.t|p| (ertalab|tushdan keyin|kechqurun|tun))/i,any:/^([ap]\.?\s?m\.?|yarim tun|peshin| (ertalab|tushdan keyin|kechqurun|tun))/i},g={any:{am:/^a/i,pm:/^p/i,midnight:/^y\.t/i,noon:/^pe/i,morning:/ertalab/i,afternoon:/tushdan keyin/i,evening:/kechqurun/i,night:/tun/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(840),a=e(841),r=e(842),o=e(843),n=e(844),s={code:"vi",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:1}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"dưới 1 giây",other:"dưới {{count}} giây"},xSeconds:{one:"1 giây",other:"{{count}} giây"},halfAMinute:"nửa phút",lessThanXMinutes:{one:"dưới 1 phút",other:"dưới {{count}} phút"},xMinutes:{one:"1 phút",other:"{{count}} phút"},aboutXHours:{one:"khoảng 1 giờ",other:"khoảng {{count}} giờ"},xHours:{one:"1 giờ",other:"{{count}} giờ"},xDays:{one:"1 ngày",other:"{{count}} ngày"},aboutXWeeks:{one:"khoảng 1 tuần",other:"khoảng {{count}} tuần"},xWeeks:{one:"1 tuần",other:"{{count}} tuần"},aboutXMonths:{one:"khoảng 1 tháng",other:"khoảng {{count}} tháng"},xMonths:{one:"1 tháng",other:"{{count}} tháng"},aboutXYears:{one:"khoảng 1 năm",other:"khoảng {{count}} năm"},xYears:{one:"1 năm",other:"{{count}} năm"},overXYears:{one:"hơn 1 năm",other:"hơn {{count}} năm"},almostXYears:{one:"gần 1 năm",other:"gần {{count}} năm"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?s+" nữa":s+" trước":s}},function(l,i,e){var t=e(2),a={full:"EEEE, 'ngày' d MMMM 'năm' y",long:"'ngày' d MMMM 'năm' y",medium:"d MMM 'năm' y",short:"dd/MM/y"},r={full:"HH:mm:ss zzzz",long:"HH:mm:ss z",medium:"HH:mm:ss",short:"HH:mm"},o={full:"{{date}} {{time}}",long:"{{date}} {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"eeee 'tuần trước vào lúc' p",yesterday:"'hôm qua vào lúc' p",today:"'hôm nay vào lúc' p",tomorrow:"'ngày mai vào lúc' p",nextWeek:"eeee 'tới vào lúc' p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["TCN","SCN"],abbreviated:["trước CN","sau CN"],wide:["trước Công Nguyên","sau Công Nguyên"]},r={narrow:["1","2","3","4"],abbreviated:["Q1","Q2","Q3","Q4"],wide:["Quý 1","Quý 2","Quý 3","Quý 4"]},o={narrow:["1","2","3","4"],abbreviated:["Q1","Q2","Q3","Q4"],wide:["quý I","quý II","quý III","quý IV"]},n={narrow:["1","2","3","4","5","6","7","8","9","10","11","12"],abbreviated:["Thg 1","Thg 2","Thg 3","Thg 4","Thg 5","Thg 6","Thg 7","Thg 8","Thg 9","Thg 10","Thg 11","Thg 12"],wide:["Tháng Một","Tháng Hai","Tháng Ba","Tháng Tư","Tháng Năm","Tháng Sáu","Tháng Bảy","Tháng Tám","Tháng Chín","Tháng Mười","Tháng Mười Một","Tháng Mười Hai"]},s={narrow:["01","02","03","04","05","06","07","08","09","10","11","12"],abbreviated:["thg 1","thg 2","thg 3","thg 4","thg 5","thg 6","thg 7","thg 8","thg 9","thg 10","thg 11","thg 12"],wide:["tháng 01","tháng 02","tháng 03","tháng 04","tháng 05","tháng 06","tháng 07","tháng 08","tháng 09","tháng 10","tháng 11","tháng 12"]},u={narrow:["CN","T2","T3","T4","T5","T6","T7"],short:["CN","Th 2","Th 3","Th 4","Th 5","Th 6","Th 7"],abbreviated:["CN","Thứ 2","Thứ 3","Thứ 4","Thứ 5","Thứ 6","Thứ 7"],wide:["Chủ Nhật","Thứ Hai","Thứ Ba","Thứ Tư","Thứ Năm","Thứ Sáu","Thứ Bảy"]},d={narrow:{am:"am",pm:"pm",midnight:"nửa đêm",noon:"tr",morning:"sg",afternoon:"ch",evening:"tối",night:"đêm"},abbreviated:{am:"AM",pm:"PM",midnight:"nửa đêm",noon:"trưa",morning:"sáng",afternoon:"chiều",evening:"tối",night:"đêm"},wide:{am:"SA",pm:"CH",midnight:"nửa đêm",noon:"trưa",morning:"sáng",afternoon:"chiều",evening:"tối",night:"đêm"}},m={narrow:{am:"am",pm:"pm",midnight:"nửa đêm",noon:"tr",morning:"sg",afternoon:"ch",evening:"tối",night:"đêm"},abbreviated:{am:"AM",pm:"PM",midnight:"nửa đêm",noon:"trưa",morning:"sáng",afternoon:"chiều",evening:"tối",night:"đêm"},wide:{am:"SA",pm:"CH",midnight:"nửa đêm",noon:"giữa trưa",morning:"vào buổi sáng",afternoon:"vào buổi chiều",evening:"vào buổi tối",night:"vào ban đêm"}};function f(c,v){var g=v||{},P=String(g.unit),h=parseInt(c,10);if(P==="quarter")switch(h){case 1:return"I";case 2:return"II";case 3:return"III";case 4:return"IV"}else if(P==="day")switch(h){case 1:return"thứ 2";case 2:return"thứ 3";case 3:return"thứ 4";case 4:return"thứ 5";case 5:return"thứ 6";case 6:return"thứ 7";case 7:return"chủ nhật"}else{if(P==="week")return h===1?"thứ nhất":"thứ "+h;if(P==="dayOfYear")return h===1?"đầu tiên":"thứ "+h}return h}var _={ordinalNumber:f,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",formattingValues:o,defaultFormattingWidth:"wide",argumentCallback:function(c){return Number(c)-1}}),month:t.a({values:n,defaultWidth:"wide",formattingValues:s,defaultFormattingWidth:"wide"}),day:t.a({values:u,defaultWidth:"wide"}),dayPeriod:t.a({values:d,defaultWidth:"wide",formattingValues:m,defaultFormattingWidth:"wide"})};i.a=_},function(l,i,e){var t=e(4),a=e(5),r=/^(\d+)/i,o=/\d+/i,n={narrow:/^(tcn|scn)/i,abbreviated:/^(trước CN|sau CN)/i,wide:/^(trước Công Nguyên|sau Công Nguyên)/i},s={any:[/^t/i,/^s/i]},u={narrow:/^([1234]|i{1,3}v?)/i,abbreviated:/^q([1234]|i{1,3}v?)/i,wide:/^quý ([1234]|i{1,3}v?)/i},d={any:[/(1|i)$/i,/(2|ii)$/i,/(3|iii)$/i,/(4|iv)$/i]},m={narrow:/^(0?[2-9]|10|11|12|0?1)/i,abbreviated:/^thg[ _]?(0?[1-9](?!\d)|10|11|12)/i,wide:/^tháng ?(Một|Hai|Ba|Tư|Năm|Sáu|Bảy|Tám|Chín|Mười|Mười ?Một|Mười ?Hai|0?[1-9](?!\d)|10|11|12)/i},f={narrow:[/0?1$/i,/0?2/i,/3/,/4/,/5/,/6/,/7/,/8/,/9/,/10/,/11/,/12/],abbreviated:[/^thg[ _]?0?1(?!\d)/i,/^thg[ _]?0?2/i,/^thg[ _]?0?3/i,/^thg[ _]?0?4/i,/^thg[ _]?0?5/i,/^thg[ _]?0?6/i,/^thg[ _]?0?7/i,/^thg[ _]?0?8/i,/^thg[ _]?0?9/i,/^thg[ _]?10/i,/^thg[ _]?11/i,/^thg[ _]?12/i],wide:[/^tháng ?(Một|0?1(?!\d))/i,/^tháng ?(Hai|0?2)/i,/^tháng ?(Ba|0?3)/i,/^tháng ?(Tư|0?4)/i,/^tháng ?(Năm|0?5)/i,/^tháng ?(Sáu|0?6)/i,/^tháng ?(Bảy|0?7)/i,/^tháng ?(Tám|0?8)/i,/^tháng ?(Chín|0?9)/i,/^tháng ?(Mười|10)/i,/^tháng ?(Mười ?Một|11)/i,/^tháng ?(Mười ?Hai|12)/i]},_={narrow:/^(CN|T2|T3|T4|T5|T6|T7)/i,short:/^(CN|Th ?2|Th ?3|Th ?4|Th ?5|Th ?6|Th ?7)/i,abbreviated:/^(CN|Th ?2|Th ?3|Th ?4|Th ?5|Th ?6|Th ?7)/i,wide:/^(Chủ ?Nhật|Chúa ?Nhật|thứ ?Hai|thứ ?Ba|thứ ?Tư|thứ ?Năm|thứ ?Sáu|thứ ?Bảy)/i},c={narrow:[/CN/i,/2/i,/3/i,/4/i,/5/i,/6/i,/7/i],short:[/CN/i,/2/i,/3/i,/4/i,/5/i,/6/i,/7/i],abbreviated:[/CN/i,/2/i,/3/i,/4/i,/5/i,/6/i,/7/i],wide:[/(Chủ|Chúa) ?Nhật/i,/Hai/i,/Ba/i,/Tư/i,/Năm/i,/Sáu/i,/Bảy/i]},v={narrow:/^(a|p|nửa đêm|trưa|(giờ) (sáng|chiều|tối|đêm))/i,abbreviated:/^(am|pm|nửa đêm|trưa|(giờ) (sáng|chiều|tối|đêm))/i,wide:/^(ch[^i]*|sa|nửa đêm|trưa|(giờ) (sáng|chiều|tối|đêm))/i},g={any:{am:/^(a|sa)/i,pm:/^(p|ch[^i]*)/i,midnight:/nửa đêm/i,noon:/trưa/i,morning:/sáng/i,afternoon:/chiều/i,evening:/tối/i,night:/^đêm/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(846),a=e(847),r=e(848),o=e(849),n=e(850),s={code:"zh-CN",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"不到 1 秒",other:"不到 {{count}} 秒"},xSeconds:{one:"1 秒",other:"{{count}} 秒"},halfAMinute:"半分钟",lessThanXMinutes:{one:"不到 1 分钟",other:"不到 {{count}} 分钟"},xMinutes:{one:"1 分钟",other:"{{count}} 分钟"},xHours:{one:"1 小时",other:"{{count}} 小时"},aboutXHours:{one:"大约 1 小时",other:"大约 {{count}} 小时"},xDays:{one:"1 天",other:"{{count}} 天"},aboutXWeeks:{one:"大约 1 个星期",other:"大约 {{count}} 个星期"},xWeeks:{one:"1 个星期",other:"{{count}} 个星期"},aboutXMonths:{one:"大约 1 个月",other:"大约 {{count}} 个月"},xMonths:{one:"1 个月",other:"{{count}} 个月"},aboutXYears:{one:"大约 1 年",other:"大约 {{count}} 年"},xYears:{one:"1 年",other:"{{count}} 年"},overXYears:{one:"超过 1 年",other:"超过 {{count}} 年"},almostXYears:{one:"将近 1 年",other:"将近 {{count}} 年"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?s+"内":s+"前":s}},function(l,i,e){var t=e(2),a={full:"y'年'M'月'd'日' EEEE",long:"y'年'M'月'd'日'",medium:"yyyy-MM-dd",short:"yy-MM-dd"},r={full:"zzzz a h:mm:ss",long:"z a h:mm:ss",medium:"a h:mm:ss",short:"a h:mm"},o={full:"{{date}} {{time}}",long:"{{date}} {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=o;var t=e(13);function a(n,s,u,d){return t.a(n,s,u)?d:n.getTime()>s.getTime()?"'下个'"+d:"'上个'"+d}var r={lastWeek:a,yesterday:"'昨天' p",today:"'今天' p",tomorrow:"'明天' p",nextWeek:a,other:"PP p"};function o(n,s,u,d){var m=r[n];return typeof m=="function"?m(s,u,d,"eeee p"):m}},function(l,i,e){var t=e(3),a={narrow:["前","公元"],abbreviated:["前","公元"],wide:["公元前","公元"]},r={narrow:["1","2","3","4"],abbreviated:["第一刻","第二刻","第三刻","第四刻"],wide:["第一刻钟","第二刻钟","第三刻钟","第四刻钟"]},o={narrow:["一","二","三","四","五","六","七","八","九","十","十一","十二"],abbreviated:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],wide:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"]},n={narrow:["日","一","二","三","四","五","六"],short:["日","一","二","三","四","五","六"],abbreviated:["周日","周一","周二","周三","周四","周五","周六"],wide:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"]},s={narrow:{am:"上",pm:"下",midnight:"凌晨",noon:"午",morning:"早",afternoon:"下午",evening:"晚",night:"夜"},abbreviated:{am:"上午",pm:"下午",midnight:"凌晨",noon:"中午",morning:"早晨",afternoon:"中午",evening:"晚上",night:"夜间"},wide:{am:"上午",pm:"下午",midnight:"凌晨",noon:"中午",morning:"早晨",afternoon:"中午",evening:"晚上",night:"夜间"}},u={narrow:{am:"上",pm:"下",midnight:"凌晨",noon:"午",morning:"早",afternoon:"下午",evening:"晚",night:"夜"},abbreviated:{am:"上午",pm:"下午",midnight:"凌晨",noon:"中午",morning:"早晨",afternoon:"中午",evening:"晚上",night:"夜间"},wide:{am:"上午",pm:"下午",midnight:"凌晨",noon:"中午",morning:"早晨",afternoon:"中午",evening:"晚上",night:"夜间"}};function d(f,_){var c=Number(f),v=_||{},g=String(v.unit);switch(g){case"date":return c.toString()+"日";case"hour":return c.toString()+"时";case"minute":return c.toString()+"分";case"second":return c.toString()+"秒";default:return"第 "+c.toString()}}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(第\s*)?\d+(日|时|分|秒)?/i,o=/\d+/i,n={narrow:/^(前)/i,abbreviated:/^(前)/i,wide:/^(公元前|公元)/i},s={any:[/^(前)/i,/^(公元)/i]},u={narrow:/^[1234]/i,abbreviated:/^第[一二三四]刻/i,wide:/^第[一二三四]刻钟/i},d={any:[/(1|一)/i,/(2|二)/i,/(3|三)/i,/(4|四)/i]},m={narrow:/^(一|二|三|四|五|六|七|八|九|十[二一])/i,abbreviated:/^(一|二|三|四|五|六|七|八|九|十[二一]|\d|1[12])月/i,wide:/^(一|二|三|四|五|六|七|八|九|十[二一])月/i},f={narrow:[/^一/i,/^二/i,/^三/i,/^四/i,/^五/i,/^六/i,/^七/i,/^八/i,/^九/i,/^十(?!(一|二))/i,/^十一/i,/^十二/i],any:[/^一|1/i,/^二|2/i,/^三|3/i,/^四|4/i,/^五|5/i,/^六|6/i,/^七|7/i,/^八|8/i,/^九|9/i,/^十(?!(一|二))|10/i,/^十一|11/i,/^十二|12/i]},_={narrow:/^[一二三四五六日]/i,short:/^[一二三四五六日]/i,abbreviated:/^周[一二三四五六日]/i,wide:/^星期[一二三四五六日]/i},c={any:[/日/i,/一/i,/二/i,/三/i,/四/i,/五/i,/六/i]},v={any:/^(上午?|下午?|午夜|[中正]午|早上?|下午|晚上?|凌晨|)/i},g={any:{am:/^上午?/i,pm:/^下午?/i,midnight:/^午夜/i,noon:/^[中正]午/i,morning:/^早上/i,afternoon:/^下午/i,evening:/^晚上?/i,night:/^凌晨/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i,e){var t=e(852),a=e(853),r=e(854),o=e(855),n=e(856),s={code:"zh-TW",formatDistance:t.a,formatLong:a.a,formatRelative:r.a,localize:o.a,match:n.a,options:{weekStartsOn:1,firstWeekContainsDate:4}};i.a=s},function(l,i,e){i.a=a;var t={lessThanXSeconds:{one:"少於 1 秒",other:"少於 {{count}} 秒"},xSeconds:{one:"1 秒",other:"{{count}} 秒"},halfAMinute:"半分鐘",lessThanXMinutes:{one:"少於 1 分鐘",other:"少於 {{count}} 分鐘"},xMinutes:{one:"1 分鐘",other:"{{count}} 分鐘"},xHours:{one:"1 小時",other:"{{count}} 小時"},aboutXHours:{one:"大約 1 小時",other:"大約 {{count}} 小時"},xDays:{one:"1 天",other:"{{count}} 天"},aboutXWeeks:{one:"大約 1 个星期",other:"大約 {{count}} 个星期"},xWeeks:{one:"1 个星期",other:"{{count}} 个星期"},aboutXMonths:{one:"大約 1 個月",other:"大約 {{count}} 個月"},xMonths:{one:"1 個月",other:"{{count}} 個月"},aboutXYears:{one:"大約 1 年",other:"大約 {{count}} 年"},xYears:{one:"1 年",other:"{{count}} 年"},overXYears:{one:"超過 1 年",other:"超過 {{count}} 年"},almostXYears:{one:"將近 1 年",other:"將近 {{count}} 年"}};function a(r,o,n){n=n||{};var s;return typeof t[r]=="string"?s=t[r]:o===1?s=t[r].one:s=t[r].other.replace("{{count}}",o),n.addSuffix?n.comparison>0?s+"內":s+"前":s}},function(l,i,e){var t=e(2),a={full:"y'年'M'月'd'日' EEEE",long:"y'年'M'月'd'日'",medium:"yyyy-MM-dd",short:"yy-MM-dd"},r={full:"zzzz a h:mm:ss",long:"z a h:mm:ss",medium:"a h:mm:ss",short:"a h:mm"},o={full:"{{date}} {{time}}",long:"{{date}} {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},n={date:t.a({formats:a,defaultWidth:"full"}),time:t.a({formats:r,defaultWidth:"full"}),dateTime:t.a({formats:o,defaultWidth:"full"})};i.a=n},function(l,i,e){i.a=a;var t={lastWeek:"'上個' eeee p",yesterday:"'昨天' p",today:"'今天' p",tomorrow:"'明天' p",nextWeek:"'下個' eeee p",other:"P"};function a(r,o,n,s){return t[r]}},function(l,i,e){var t=e(3),a={narrow:["前","公元"],abbreviated:["前","公元"],wide:["公元前","公元"]},r={narrow:["1","2","3","4"],abbreviated:["第一刻","第二刻","第三刻","第四刻"],wide:["第一刻鐘","第二刻鐘","第三刻鐘","第四刻鐘"]},o={narrow:["一","二","三","四","五","六","七","八","九","十","十一","十二"],abbreviated:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],wide:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"]},n={narrow:["日","一","二","三","四","五","六"],short:["日","一","二","三","四","五","六"],abbreviated:["週日","週一","週二","週三","週四","週五","週六"],wide:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"]},s={narrow:{am:"上",pm:"下",midnight:"凌晨",noon:"午",morning:"早",afternoon:"下午",evening:"晚",night:"夜"},abbreviated:{am:"上午",pm:"下午",midnight:"凌晨",noon:"中午",morning:"早晨",afternoon:"中午",evening:"晚上",night:"夜間"},wide:{am:"上午",pm:"下午",midnight:"凌晨",noon:"中午",morning:"早晨",afternoon:"中午",evening:"晚上",night:"夜間"}},u={narrow:{am:"上",pm:"下",midnight:"凌晨",noon:"午",morning:"早",afternoon:"下午",evening:"晚",night:"夜"},abbreviated:{am:"上午",pm:"下午",midnight:"凌晨",noon:"中午",morning:"早晨",afternoon:"中午",evening:"晚上",night:"夜間"},wide:{am:"上午",pm:"下午",midnight:"凌晨",noon:"中午",morning:"早晨",afternoon:"中午",evening:"晚上",night:"夜間"}};function d(f,_){var c=Number(f),v=_||{},g=String(v.unit);switch(g){case"date":return c.toString()+"日";case"hour":return c.toString()+"時";case"minute":return c.toString()+"分";case"second":return c.toString()+"秒";default:return"第 "+c.toString()}}var m={ordinalNumber:d,era:t.a({values:a,defaultWidth:"wide"}),quarter:t.a({values:r,defaultWidth:"wide",argumentCallback:function(f){return Number(f)-1}}),month:t.a({values:o,defaultWidth:"wide"}),day:t.a({values:n,defaultWidth:"wide"}),dayPeriod:t.a({values:s,defaultWidth:"wide",formattingValues:u,defaultFormattingWidth:"wide"})};i.a=m},function(l,i,e){var t=e(4),a=e(5),r=/^(第\s*)?\d+(日|時|分|秒)?/i,o=/\d+/i,n={narrow:/^(前)/i,abbreviated:/^(前)/i,wide:/^(公元前|公元)/i},s={any:[/^(前)/i,/^(公元)/i]},u={narrow:/^[1234]/i,abbreviated:/^第[一二三四]刻/i,wide:/^第[一二三四]刻鐘/i},d={any:[/(1|一)/i,/(2|二)/i,/(3|三)/i,/(4|四)/i]},m={narrow:/^(一|二|三|四|五|六|七|八|九|十[二一])/i,abbreviated:/^(一|二|三|四|五|六|七|八|九|十[二一]|\d|1[12])月/i,wide:/^(一|二|三|四|五|六|七|八|九|十[二一])月/i},f={narrow:[/^一/i,/^二/i,/^三/i,/^四/i,/^五/i,/^六/i,/^七/i,/^八/i,/^九/i,/^十(?!(一|二))/i,/^十一/i,/^十二/i],any:[/^一|1/i,/^二|2/i,/^三|3/i,/^四|4/i,/^五|5/i,/^六|6/i,/^七|7/i,/^八|8/i,/^九|9/i,/^十(?!(一|二))|10/i,/^十一|11/i,/^十二|12/i]},_={narrow:/^[一二三四五六日]/i,short:/^[一二三四五六日]/i,abbreviated:/^週[一二三四五六日]/i,wide:/^星期[一二三四五六日]/i},c={any:[/日/i,/一/i,/二/i,/三/i,/四/i,/五/i,/六/i]},v={any:/^(上午?|下午?|午夜|[中正]午|早上?|下午|晚上?|凌晨)/i},g={any:{am:/^上午?/i,pm:/^下午?/i,midnight:/^午夜/i,noon:/^[中正]午/i,morning:/^早上/i,afternoon:/^下午/i,evening:/^晚上?/i,night:/^凌晨/i}},P={ordinalNumber:t.a({matchPattern:r,parsePattern:o,valueCallback:function(h){return parseInt(h,10)}}),era:a.a({matchPatterns:n,defaultMatchWidth:"wide",parsePatterns:s,defaultParseWidth:"any"}),quarter:a.a({matchPatterns:u,defaultMatchWidth:"wide",parsePatterns:d,defaultParseWidth:"any",valueCallback:function(h){return h+1}}),month:a.a({matchPatterns:m,defaultMatchWidth:"wide",parsePatterns:f,defaultParseWidth:"any"}),day:a.a({matchPatterns:_,defaultMatchWidth:"wide",parsePatterns:c,defaultParseWidth:"any"}),dayPeriod:a.a({matchPatterns:v,defaultMatchWidth:"any",parsePatterns:g,defaultParseWidth:"any"})};i.a=P},function(l,i){},function(l,i,e){Object.defineProperty(i,"__esModule",{value:!0}),i.GT_ACCESS_TOKEN="GT_ACCESS_TOKEN",i.GT_VERSION="1.8.0",i.GT_COMMENT="GT_COMMENT"},function(l,i,e){Object.defineProperty(i,"__esModule",{value:!0});var t=e(860),a=o(t),r=e(121);function o(u){return u&&u.__esModule?u:{default:u}}var n=function(d,m){var f=m==="last"?"before":"after",_=` + query getIssueAndComments( + $owner: String!, + $repo: String!, + $id: Int!, + $cursor: String, + $pageSize: Int! + ) { + repository(owner: $owner, name: $repo) { + issue(number: $id) { + title + url + bodyHTML + createdAt + comments(`+m+": $pageSize, "+f+`: $cursor) { + totalCount + pageInfo { + `+(m==="last"?"hasPreviousPage":"hasNextPage")+` + `+(f==="before"?"startCursor":"endCursor")+` + } + nodes { + id + databaseId + author { + avatarUrl + login + url + } + bodyHTML + body + createdAt + reactions(first: 100, content: HEART) { + totalCount + viewerHasReacted + pageInfo{ + hasNextPage + } + nodes { + id + databaseId + user { + login + } + } + } + } + } + } + } + } + `;return d.cursor===null&&delete d.cursor,{operationName:"getIssueAndComments",query:_,variables:d}};function s(u){var d=this,m=this.options,f=m.owner,_=m.repo,c=m.perPage,v=m.pagerDirection,g=m.defaultAuthor,P=this.state,h=P.cursor,y=P.comments;return r.axiosGithub.post("/graphql",n({owner:f,repo:_,id:u.number,pageSize:c,cursor:h},v),{headers:{Authorization:"bearer "+this.accessToken}}).then(function(W){var T=W.data.data.repository.issue.comments,j=T.nodes.map(function(I){var M=I.author||g;return{id:I.databaseId,gId:I.id,user:{avatar_url:M.avatarUrl,login:M.login,html_url:M.url},created_at:I.createdAt,body_html:I.bodyHTML,body:I.body,html_url:"https://github.com/"+f+"/"+_+"/issues/"+u.number+"#issuecomment-"+I.databaseId,reactions:I.reactions}}),b=void 0;v==="last"?b=[].concat((0,a.default)(j),(0,a.default)(y)):b=[].concat((0,a.default)(y),(0,a.default)(j));var C=T.pageInfo.hasPreviousPage===!1||T.pageInfo.hasNextPage===!1;return d.setState({comments:b,isLoadOver:C,cursor:T.pageInfo.startCursor||T.pageInfo.endCursor}),b})}i.default=s},function(l,i,e){i.__esModule=!0;var t=e(861),a=r(t);function r(o){return o&&o.__esModule?o:{default:o}}i.default=function(o){if(Array.isArray(o)){for(var n=0,s=Array(o.length);n1?arguments[1]:void 0,P=g!==void 0,h=0,y=d(_),W,T,j,b;if(P&&(g=t(g,v>2?arguments[2]:void 0,2)),y!=null&&!(c==Array&&n(y)))for(b=y.call(_),T=new c;!(j=b.next()).done;h++)u(T,h,P?o(b,g,[j.value,h],!0):j.value);else for(W=s(_.length),T=new c(W);W>h;h++)u(T,h,P?g(_[h],h):_[h]);return T.length=h,T}})},function(l,i,e){var t=e(16),a=e(39);l.exports=function(r,o,n){o in r?t.f(r,o,a(0,n)):r[o]=n}}])})})(st);var wr=st.exports;const Cr=nt(wr);const xr=R=>(bn("data-v-c2acb4b7"),R=R(),Wn(),R),jr={class:"gitalk-comment"},Ir=xr(()=>qe("div",{id:"gitalk-container",class:"gitalk-container"},null,-1)),Ar=[Ir],Lr=Fe({__name:"GitalkComment",props:{issueId:{}},setup(R){const H=R;function l(){new Cr({clientID:"b83f94a1becad40b30df",clientSecret:"43f498e9660b0fb385a0a139277f13e87f26074d",repo:"learning-vue3",owner:"chengpeiquan",admin:["chengpeiquan"],id:Tr(location.pathname),number:H.issueId}).render("gitalk-container")}return ke(l),(i,e)=>(Ie(),Ae("div",jr,Ar))}});const Rr=He(Lr,[["__scopeId","data-v-c2acb4b7"]]),Ur={class:"img-wrap"},Br=["src","alt"],Kr={key:0,class:"desc"},Sr=Fe({__name:"ImgWrap",props:{src:{},dark:{},alt:{},maxWidth:{}},setup(R){const H=R,l=Be(),i=Be(!1),e=Tn(()=>i.value&&H.dark?H.dark:H.src),t=Be();function a(){const r=document.querySelector("html");r&&(l.value=r,i.value=l.value.classList.contains("dark"),t.value=new MutationObserver(o=>{o.forEach(n=>{const{type:s,target:u}=n;s==="attributes"&&(i.value=u.classList.contains("dark"),console.log(i.value))})}),t.value.observe(l.value,{subtree:!1,attributes:!0,attributeFilter:["class"]}))}return ke(()=>{a()}),wn(()=>{t.value&&t.value.disconnect()}),(r,o)=>(Ie(),Ae("div",Ur,[qe("img",{class:"img",style:Cn({maxWidth:`${r.maxWidth}px`||"100%"}),src:e.value,alt:r.alt||"Vue3"},null,12,Br),r.alt?(Ie(),Ae("p",Kr,xn(r.alt),1)):jn("",!0)]))}});const Nr=He(Sr,[["__scopeId","data-v-04203bbc"]]);JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"../.vitepress/theme/components/ReadingTips.md","filePath":"../.vitepress/theme/components/ReadingTips.md"}');const zr={name:"../.vitepress/theme/components/ReadingTips.md"},Fr=In('

温馨提示

这是笔者写于 2024.02.20 的补充提示,为读者解答为何前面的章节都未提及 script-setup 组件语法的信息。

笔者在执笔之初,考虑到降低初学过程中的各种心智负担,以及当时处于 script-setup 组件语法尚未完全稳定的背景下,所以在最后一章 高效开发 之前的章节,都只提供了组件的标准写法的讲解。

除非遇到框架进行了 Breaking Change (例如从 Vue 2 升级到了 Vue 3 这种大版本更新),否则标准写法是始终有效的,在笔者多年的学习经历和实际收获里,也是比较相信一点:不论学习什么语言或者框架,只要基础打的好,未来总是可以轻松地举一反三去解决遇到的各类问题。

因此如果读者通过各类脚手架创建的 Vue 3 项目,在组件看到 <script setup> 这种风格的组件,以及诸如 defineXxx 风格的 API ,这是 .vue 组件所支持的一种语法糖,可以随时在 高效开发 一章了解相关的 API 用法。

另外从 Vue 3.4 版本之后所新增的 API 未在本书的介绍范围内,请读者在阅读的过程中也可以查阅官方的 API 一起学习,不论是本书还是其他计算机书籍,官方文档永远是最好的查询手册。

TIP

语法糖(英语:Syntactic sugar)是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。

',7),kr=[Fr];function Hr(R,H,l,i,e,t){return Ie(),Ae("div",null,kr)}const Yr=He(zr,[["render",Hr]]);function be(){return typeof window<"u"?navigator.userAgent.toLowerCase():""}const Vr=typeof window<"u";/Android/i.test(be());/iPhone|iPod|iPad|iOS/i.test(be());/uni-app|html5plus/.test(be());/MicroMessenger/i.test(be());/\sQQ|mqqbrowser|qzone|qqbrowser/i.test(be());/mqqbrowser|qqbrowser/i.test(be());/qzone\/.*_qz_([\d.]+)/i.test(be());/(weibo).*weibo__([\d.]+)/i.test(be());/(baiduboxapp)\/([\d.]+)/i.test(be());function Xr({type:R,id:H,resource:l}){return new Promise((i,e)=>{if(!Vr||document.querySelector(`#${H}`)){e();return}function t(a){a.addEventListener("load",i),a.addEventListener("error",e),a.addEventListener("abort",e)}switch(R){case"js":{const a=document.createElement("script");a.id=H,a.async=!0,a.src=l,t(a),document.head.appendChild(a);break}case"css":{const a=document.createElement("link");a.id=H,a.rel="stylesheet",a.href=l,t(a),document.head.appendChild(a);break}case"style":{const a=document.createElement("style");a.id=H,t(a),document.head.appendChild(a),a.appendChild(document.createTextNode(l));break}}})}function Gr(){try{return"production"}catch{return}}Gr();function Qe(R){try{return R==="production"}catch{return!1}}Qe("development");Qe("test");Qe("production");const Ke={hot:" ~hot",new:" ~new"},Qr={hot:'',new:''};function $r(){if(ye)try{Xr({type:"style",id:"symbol-plugin",resource:` + .sidebar__icon--default { + position: relative; + display: inline-block; + width: 18px; + height: 18px; + color: #fff; + font-size: 13px; + font-weight: bold; + font-style: normal; + vertical-align: middle; + margin: 0 5px; + transform: scale(0.7) rotate(30deg); + } + .sidebar__icon--default:before { + position: absolute; + top: 0; + left: 0; + width: 18px; + height: 18px; + line-height: 18px; + text-align: center; + transform: rotate(135deg); + } + .sidebar__icon--hot { + background-color: #da5961; + } + .sidebar__icon--hot:before { + content: "H"; + background-color: #da5961; + } + .sidebar__icon--new { + background-color: #3eaf7c; + } + .sidebar__icon--new:before { + content: "N"; + background-color: #3eaf7c; + } + `}).catch(H=>{console.log(H)})}catch(R){console.log(R)}}function Jr(){ye&&setTimeout(()=>{try{const R=document.querySelectorAll(".aside-container nav .outline-link")||[],H=document.querySelectorAll(".content-container h2")||[],l=document.querySelectorAll(".content-container h3")||[],i=document.querySelectorAll(".content-container h4")||[];[...R,...H,...l,...i].forEach(t=>{let a=t.innerHTML;for(const r in Ke)if(Object.hasOwnProperty.call(Ke,r)){const o=r,n=Ke[o],s=Qr[o],u=new RegExp(n,"img");if(a.includes(n)){const{nodeName:d}=t;switch(d){case"H2":case"H3":case"H4":a=a.replace(u,"");break;default:a=a.replace(u,s)}t.innerHTML=a}}})}catch(R){console.log(R)}},100)}const ut={update:"upgrade"};function dt(){if(!ye)return"";const{pathname:R}=window.location;return R.slice(1).replace(".html","")}function Zr(){if(!ye)return!1;const R=dt();return Object.keys(ut).includes(R)}function qr(){if(!ye)return"/";const R=dt();return`/${ut[R]}.html`||"/"}function ei(){ye&&window.location.replace(qr())}const{baiduAnalytics:Ze,registerBaiduAnalytics:ti}=pr(),ai={...$n,enhanceApp({app:R,router:H}){R.component("GitalkComment",Rr),R.component("ImgWrap",Nr),R.component("ReadingTips",Yr),ye&&(Zr()&&ei(),$r(),ti(R,{websiteIds:["8dca8e2532df48ea7f1b15c714588691","025e7d9acbc7359afa71bdae5aa03f33"]}),window.addEventListener("hashchange",()=>{Ze.trackPageview({pageUrl:window.location.href})}),H.onAfterRouteChanged=l=>{Jr(),Ze.trackPageview({pageUrl:l})})}};function lt(R){if(R.extends){const H=lt(R.extends);return{...H,...R,async enhanceApp(l){H.enhanceApp&&await H.enhanceApp(l),R.enhanceApp&&await R.enhanceApp(l)}}}return R}const xe=lt(ai),ni=Fe({name:"VitePressApp",setup(){const{site:R}=Ln();return ke(()=>{Rn(()=>{document.documentElement.lang=R.value.lang,document.documentElement.dir=R.value.dir})}),Un(),Bn(),Kn(),xe.setup&&xe.setup(),()=>Sn(xe.Layout)}});async function ri(){const R=oi(),H=ii();H.provide(Nn,R);const l=zn(R.route);return H.provide(Fn,l),H.component("Content",kn),H.component("ClientOnly",Hn),Object.defineProperties(H.config.globalProperties,{$frontmatter:{get(){return l.frontmatter.value}},$params:{get(){return l.page.value.params}}}),xe.enhanceApp&&await xe.enhanceApp({app:H,router:R,siteData:Yn}),{app:H,router:R,data:l}}function ii(){return Vn(ni)}function oi(){let R=ye,H;return Xn(l=>{let i=Gn(l);return i?(R&&(H=i),(R||H===i)&&(i=i.replace(/\.js$/,".lean.js")),ye&&(R=!1),Qn(()=>import(i),[])):null},xe.NotFound)}ye&&ri().then(({app:R,router:H,data:l})=>{H.go().then(()=>{An(H.route,l.site),R.mount("#app")})});export{ri as createApp}; diff --git a/assets/changelog.md.1e825a6d.js b/assets/changelog.md.1e825a6d.js new file mode 100644 index 00000000..c43fed41 --- /dev/null +++ b/assets/changelog.md.1e825a6d.js @@ -0,0 +1,8 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as a,v as e,b as l,R as t}from"./chunks/framework.0d8bea05.js";const f=JSON.parse('{"title":"更新记录","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"changelog.md","filePath":"changelog.md"}'),i={name:"changelog.md"},r=t('

更新记录

由于平时也比较忙,都是利用碎片时间整理的文档,习惯勤备份,因此 commit 记录比较多,而且有些提交记录跟内容也无关,比如文档程序的调整和优化等代码提交是和内容无关的。

考虑到后面还会不定期更新内容,所以翻了一下之前跟朋友的微信聊天记录整理了前期的更新记录,之后当文档有再次更新的时候也会继续整理更新记录,方便读者们查阅。

2024-02-20

单组件的编写路由的使用插件的开发和使用组件之间的通信全局状态管理 这几张内容的最前面,补充了一个阅读提示,解答近期收到的读者反馈的一些疑惑。

2024-01-16

插件部分增加一个 path/to 的典故 扩展阅读。

2023-05-07

感谢各位读者长期以来的支持,经过长达一年时间的打磨和优化,本书已于 2023 年 5 月份正式出版上市,纸质版书籍的正式名称为《前端工程化:基于 Vue.js 3.0 的设计与实践》。

如果您对笔者的作品认可,建议购买纸质版,纸质书在电子书的基础上,经过机械工业出版社的编辑老师们的内容优化、校对勘误、排版美化,更成体系!

各位读者可以在各大电商平台购买纸质版书籍:

☞ 访问 京东商城 购买

☞ 访问 天猫商城 购买

可点击 出版说明 了解更多信息。

2023-03-01

在 “单组件的编写” 的 DOM 元素与子组件 一节里,添加了 InstanceType<T> 帮助类型的使用说明,感谢 @rayadaschn#62 (comment) 的反馈。

2022-11-20

在 “高效开发” 新增了 命名技巧 一节,希望能够帮助有命名困扰的开发者解决选择困难的问题。

2022-11-17

更新了 “用 ES Module 设计模块” 中关于 命名导出和导入 的讲解,减少在理解上的歧义,感谢 @Yeshan-Taoist#163 (commont) 的反馈。

2022-11-12

优化了 “高效开发” 一章,考虑到现在的 Vue 3 项目基本都是最新版本的 Vue ,因此移除了过渡时期的一些过时的使用提示信息,同时优化了演示代码使其更容易理解。

2022-11-11

优化了 “组件之间的通信” 一章:

  • 把一些原来没有特地注明是父组件的代码还是子组件的代码,进行了明确的标明,并优化了一部分示范代码
  • 修正了之前一部分示范代码的 TypeScript 类型(之前有一些地方用了 any ,现在更新为明确的类型)
  • 优化了 provide / inject 的相关内容,大幅度缩短了篇幅,但通过新的例子使其更容易看懂
  • 增加了 Reactive State 创建一个小型的状态中心案例
  • Vuex 添加了一些使用提示,建议使用 Pinia 代替 Vuex 作为状态管理库使用

2022-11-06

优化了 “路由的使用” 一章:

  • 涉及 Webpack 的环境变量和配置,统一调整为 Vite 的配置,并标注了这些配置在使用时需要留意当前所使用的构建工具是否匹配
  • 将原本指向 Vue Router 英文官网的参考内容地址修改为指向其中文官网(因为当时编写的时候还没有中文官网)
  • 优化了里面的一些举例,使其更容易理解

优化了 “插件的开发和使用” 一章:

  • 简化了一些例子

2022-11-04

比较大幅度的优化了 “单组件的编写” 一章的 响应式 API 之 toRef 与 toRefs 的内容,使其更容易理解。

2022-11-02

优化了 “单组件的编写” 一章的 “响应式 API 之 ref ” 一节,对其 类型声明 作了一个更容易理解和更详细的讲解。

2022-10-25

  • 把 hello-node 和 hello-lib 这两个演示仓库同步到了 Gitee ,如果访问 GitHub 失败,也可以从 Gitee 拉取。
  • 在 “脚手架的升级与配置” 一章更新了 Vue Devtools 的介绍,因为插件更新后变化较大,重新截图进行了说明,并且对图片适配了文档的 Light / Dark 皮肤切换

2022-10-23

在 “工程化的前期准备” 一章的 “用 ES Module 设计模块” 一节,增加了 在浏览器里访问 ESM 小节,讲解了浏览器对原生 ESM 的访问支持和限制,以及在 HTML 页面里使用 ESM 的代码演示。

另外贯穿 “前端工程化入门” 所演示的 hello-node 项目已托管至 learning-vue3/hello-node 仓库,可使用 Git 克隆命令拉取至本地:

bash
git clone https://github.com/learning-vue3/hello-node.git

成品项目可作为学习过程中的代码参考,但更建议按照教程的讲解步骤,从零开始亲手搭建一个新项目并完成 node 开发的体验,可以更有效的提升学习效果。

2022-10-13

  1. 本书目前已不再仅限于 Vue 3 的学习,因此重写了 前言 的内容
  2. 不同时期写的内容存在一定程度的上下文不统一(例如早期使用了 @vue/cli 创建的项目做演示,后期因为 Vite 的出现并且被 Vue 官方作为其主推的构建工具,因此新内容使用的是 Vite 项目),因此对内容进行了检查和统一修正
  3. 减少了全文的人称代词,剩下不可删除的则进行了上下文统一,减少前后不一致可能会引起一些理解困惑
  4. 优化了不同时期的内容之间的章节衔接

其中第 2 、 3 、 4 点可能还有遗漏,近期持续检查修正。

2022-10-11

在 “开发 npm 包” 部分 常用的构建工具 一节增加 Node 版本检查的提示,在开发之前请确保当前的 Node 版本是否在构建工具的支持范围内,感谢 @rayadaschn#204 的建议。

2022-10-10

发布 npm 包 小节增加一个取消镜像源的提示,否则可能引起发布失败。

2022-10-09

在 “开发 npm 包” 部分所演示的 hello-lib 项目已托管至 learning-vue3/hello-lib 仓库,可使用 Git 克隆命令拉取至本地:

bash
git clone https://github.com/learning-vue3/hello-lib.git

成品项目可作为学习过程中的代码参考,但更建议按照教程的讲解步骤,从零开始亲手搭建一个新项目并完成 npm 包的开发流程,可以更有效的提升学习效果。

2022-10-08

在 “开发 npm 包” 的相关内容处增加了 添加版权注释 的构建说明,它的作用除了声明版权归属之外,还会告知使用者关于项目的主页地址、版本号、发布日期、 BUG 反馈渠道等信息,例如 jQuery / Lodash / Swiper 等库都有这样的版权注释,因此补充了这部分的内容,供有需要的开发者了解和使用。

2022-10-07

  1. 文档程序从 VuePress 2.x 迁移到 VitePress 1.x ,并做了简单的主题适配
  2. 原来的 “脚手架的升级与配置” 一章的访问地址从 update 调整为 upgrade ,访问地址已配置重定向
  3. 由于原来的 “起步准备” 一章内容有点太长,这一次续写拆分了一下内容,重新划分为单独的三章,分别是:
    • 独立出来了 了解前端工程化 一章,用于科普现代前端开发所要求的工程化概念,属于讲故事性质的一章内容
    • 工程化的前期准备 是原来 “起步准备” 的核心内容部分,主要是科普前端工程化的入门实践,属于动手实操章节
    • 另外考虑到本书是直接使用 TypeScript 来入门 Vue 3 ,语言部分也是内容的重点之一,所以把 TypeScript 部分也独立成了 快速上手 TypeScript 一章
  4. 在 “了解前端工程化” 一章里,增加以下内容:
  5. 在 “工程化的前期准备” 一章里,增加以下内容:
  6. 在 “插件的开发和使用” 一章里,增加了以下内容:

2022-09-01

更新了引用到的官方文档链接,同时修复了部分官方文档未做好重定向的无效链接,感谢 @alleluya-young#191 的反馈!

2022-07-03

补充了关于 Node.js 版本之间的区别 的一些说明。

2022-06-23

根据 #163 (comment) 的反馈,对一些 ES6+ 可能容易混淆的新语法进行了一些 Tips 补充,减少学习过程中的理解成本。

2022-05-03

更新了 “升级与配置” 一章的部分内容:

  1. 更新了使用 Create Preset 创建项目的说明,现在可以通过 --template 选项来指定模板创建,例如:
bash
# 全局安装\nnpm install -g create-preset\n\n# 将使用 `vue3-ts-vite` 模板创建一个名为 `hello-vue3` 的项目\npreset init hello-vue3 --template vue3-ts-vite
  1. 配合 1 的更新,相应的增加了 Hello Vue3 一节
  2. 配合 @vue/cli 5.x 版本的命令行交互,更新了 使用 @vue/cli 创建项目 的内容
  3. 更新了 调整 TS Config 部分的内容,由于现在通过各类脚手架创建的 TypeScript 项目都自带了该文件,所以这部分内容移除了之前的配置参考,仅保留 paths 的配置提醒

2022-04-30

最近有时间,所以对内容做了一大波更新,主要是补充了在开始开发 Vue 项目之前的一些基础知识点,当然 Vue 相关的内容也做了一些新增或者完善。

  1. 增加了全新的一章 “起步准备” ,面向对前端工程化开发不太熟悉的开发者,包含了以下内容:

  2. 在 “升级与配置” 一章里,增加了以下内容:

  3. 在 “单组件的编写” 一章里,增加了以下内容:

    • 响应式数据 设计上的变化
    • 增加了 指令 一节的内容,主要讲解如何在 Vue 3 开发自定义指令
    • 增加了 插槽 一节的内容
  4. 在 “路由的使用” 一章里,增加了以下内容,整理了一些常见的部署问题原因以及解决方案:

  5. 在 “插件的使用” 一章里,对 “本地插件” 作了进一步的细化讲解:

2022-04-20

更正了 watch API 在 侦听选项之 deep 部分内容的错误,并增加了如何检测特例的用法,感谢 @zcc0329 的反馈!

2022-04-10

重写了单组件关于 数据的侦听 这一节的内容,因为在写 Pinia 的订阅功能的时候,有很多跟 watch 相关联的知识点,发现之前这部分内容写的比较简单,所以完善了一下。

2022-04-07

补充了昨天新增的关于 Pinia 订阅 state 部分的一些内容。

2022-04-06

  1. 创建基于 Vite 的 Vue 3 项目增加了一些内容:

    • 增加了使用 Create Vue 创建 Vite 项目的说明
    • 使用 Create Preset 创建的 vue3-ts-vite 预设项目,使用 Pinia 代替 Vuex
  2. 补充了 Pinia 的部分内容:

2022-04-04

先跟大家说声抱歉,在 2022-02-09 的更新里说要写这部分内容,结果因为现实里的一些事情还有工作比较忙,现在借着假期才得以继续更新。

增加了 全局状态的管理 一章,主要是面向 Vue 3 全新的状态管理工具 Pinia 展开,里面的大部分内容也加入了和 Vuex 的对比。

虽然可以归入 组件之间的通信 里,但 Pinia 作为被官方推荐在 Vue 3 项目里作为全局状态管理的新工具,写着写着我觉得还是单独开一章来写会更方便阅读和理解。

2022-03-04

把 Algolia 的搜素功能申请下来了,看看文档的搜索体验会不会比较好(搜索框在页面的右上角)。

2022-02-28

升级了文档的程序版本,加入了 Dark Mode 暗黑模式的支持,默认跟随系统模式进行适配,可以在右上角的 “主题切换” 按钮手动切换主题。

2022-02-09

  1. 优化了 升级与配置 一章,配合 2022-02-07 Vue 3 成为默认版本带来的调整变化,主要改动如下:

  2. 高效开发 一章开头的 WARNING 改成了 TIP ,里面提到的功能在最新版的 Vue 下发挥稳定,后续如果有实验性的 API 再单独标记。

  3. 把原先演示命令里的 @next 版本都改成了 @latest ,现在不需要指定 next 就能使用 Vue 3 了。

  4. 更新了 官方文档 里面的链接说明,因为各个官网在 2022-02-07 都上线了新版本,更换了新地址。

  5. 组件之间的通信 里加入了 Pinia 的介绍,不过暂时没有写实践记录(还没有时间哈哈哈),欢迎先自行体验,这是一个官方推荐的适合 Vue 3 的状态管理工具,因为它更加面向 Composition API 的编程方式(至少在 Vuex 5 推出之前,建议用来代替 Vuex 4)。

  6. 改了一下文档名称:《Vue3.0 学习教程与实战案例》 --> 《Vue 3 入门指南与实战案例》,其他原先使用 Vue 3.0 叫法的地方,后面也会陆续改成 Vue 3 了。

2022-01-20

  1. 更新了 cnpm 的用法,原因是旧的 cnpm 源即将下线,需要使用新的域名啦!(感谢 @alleluya-young#135 的反馈!)

  2. 添加了 pnpm 的用法说明,目前开源社区越来越流行使用 pnpm ,对大型项目管理也有好处,值得了解和体验。

2021-12-05

由于离第一次执笔到现在有了差不多一年的时间,当时的前言信息有些陈旧,重新写了一下 前言

另外经过 2021 年的开发实践,很多地方我又有了新的认知,打算在接下来的时间里,我会再继续抽空把各部分的内容再精细化一下,把以前可能写的比较简单的部分,或者没有写进来的一些功能点,都再做一次完善。

2021-11-16

重写了 数据的计算 一节,之前对于计算数据的介绍,描述上确实有点难以理解,而且有点简单带过,所以重新写了这部分内容,感谢读者 @superficial 的反馈!

2021-10-07

补充了 全局编译器宏 的配置说明,本来很久前就想写的,一直偷懒,国庆结束前补充进来啦!

2021-08-17

更新了 3.2.0 版本新增的一些知识点:

  1. 增加了 使用 v-bind 动态修改 style 功能说明,同时结合上下文知识点补充了 动态绑定 CSS 一节

  2. 增加了 CSS Modules 一节的内容

  3. 增加了 useCssModule 一节的内容

2021-08-16

  1. 更新了 深度操作符 的内容,原有的 API 已被废弃,请注意使用新写法

  2. 补充了几个拓展阅读链接到 教程工具

2021-07-28

迎接 Vue 3.2.0 的新版本内容,对 script-setup 做了较多的调整。

目前 3.2 还处于 Beta 阶段,不过从这个版本开始,script-setup 脱离实验性阶段,正式进入 Vue 3.0 的队伍,本次的更新内容较多,很多测试阶段的 API 被移除或者修改,如果之前有用过 script-setup ,建议先简单看我之前的博客,了解本次的一些变更:

Vue3.0 最新动态:script-setup 定稿 部分实验性 API 将弃用

2021-03-31

  1. 调整了 使用全局 API 一节的内容

原因是原来推荐的 const { proxy } = getCurrentInstance() 的方案,在 TS 新版本校验下,使用成本非常高,原来的代码不做多层判断的情况下已经无法正常运行,所以按照目前的 “最佳实践” 重新更新了这一节。

  1. 新增了一节 全局 API 的替代方案

由于 Vue 3.0 的有意为之,这个 “最佳实践” 实际上用起来也挺繁琐(在官网和 GitHub 对此都有说明,不推荐使用),所以补充了这一节内容。

2021-03-23

DOM 元素与子组件 一节增加了 TS 类型的定义说明,以及编译失败的原因说明及解决方案补充,非常感谢 @aierong 的反馈和支持。

2021-03-21

增加了 高效开发 一章,将记录一些提高 Vue 3.0 开发效率的内容,面像对 Vue 3.0 已上手,想提高开发效率的开发者。

本次更新了 script-setup 一节的内容。

2021-01-21

增加了 组件之间的通信 一章的内容。

2020-12-18

本文档的第一个版本上线,完成了 升级与配置单组件的编写路由的使用插件的使用 等章节的编写。

2020-10-01

国庆期间开始开荒 Vue 3.0,陆陆续续打了不少笔记,业余时间着手整理成文档。

',122),h=[r];function n(o,p,s,u,c,d){return e(),l("div",null,h)}const g=a(i,[["render",n]]);export{f as __pageData,g as default}; diff --git a/assets/changelog.md.1e825a6d.lean.js b/assets/changelog.md.1e825a6d.lean.js new file mode 100644 index 00000000..c43fed41 --- /dev/null +++ b/assets/changelog.md.1e825a6d.lean.js @@ -0,0 +1,8 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as a,v as e,b as l,R as t}from"./chunks/framework.0d8bea05.js";const f=JSON.parse('{"title":"更新记录","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"changelog.md","filePath":"changelog.md"}'),i={name:"changelog.md"},r=t('

更新记录

由于平时也比较忙,都是利用碎片时间整理的文档,习惯勤备份,因此 commit 记录比较多,而且有些提交记录跟内容也无关,比如文档程序的调整和优化等代码提交是和内容无关的。

考虑到后面还会不定期更新内容,所以翻了一下之前跟朋友的微信聊天记录整理了前期的更新记录,之后当文档有再次更新的时候也会继续整理更新记录,方便读者们查阅。

2024-02-20

单组件的编写路由的使用插件的开发和使用组件之间的通信全局状态管理 这几张内容的最前面,补充了一个阅读提示,解答近期收到的读者反馈的一些疑惑。

2024-01-16

插件部分增加一个 path/to 的典故 扩展阅读。

2023-05-07

感谢各位读者长期以来的支持,经过长达一年时间的打磨和优化,本书已于 2023 年 5 月份正式出版上市,纸质版书籍的正式名称为《前端工程化:基于 Vue.js 3.0 的设计与实践》。

如果您对笔者的作品认可,建议购买纸质版,纸质书在电子书的基础上,经过机械工业出版社的编辑老师们的内容优化、校对勘误、排版美化,更成体系!

各位读者可以在各大电商平台购买纸质版书籍:

☞ 访问 京东商城 购买

☞ 访问 天猫商城 购买

可点击 出版说明 了解更多信息。

2023-03-01

在 “单组件的编写” 的 DOM 元素与子组件 一节里,添加了 InstanceType<T> 帮助类型的使用说明,感谢 @rayadaschn#62 (comment) 的反馈。

2022-11-20

在 “高效开发” 新增了 命名技巧 一节,希望能够帮助有命名困扰的开发者解决选择困难的问题。

2022-11-17

更新了 “用 ES Module 设计模块” 中关于 命名导出和导入 的讲解,减少在理解上的歧义,感谢 @Yeshan-Taoist#163 (commont) 的反馈。

2022-11-12

优化了 “高效开发” 一章,考虑到现在的 Vue 3 项目基本都是最新版本的 Vue ,因此移除了过渡时期的一些过时的使用提示信息,同时优化了演示代码使其更容易理解。

2022-11-11

优化了 “组件之间的通信” 一章:

  • 把一些原来没有特地注明是父组件的代码还是子组件的代码,进行了明确的标明,并优化了一部分示范代码
  • 修正了之前一部分示范代码的 TypeScript 类型(之前有一些地方用了 any ,现在更新为明确的类型)
  • 优化了 provide / inject 的相关内容,大幅度缩短了篇幅,但通过新的例子使其更容易看懂
  • 增加了 Reactive State 创建一个小型的状态中心案例
  • Vuex 添加了一些使用提示,建议使用 Pinia 代替 Vuex 作为状态管理库使用

2022-11-06

优化了 “路由的使用” 一章:

  • 涉及 Webpack 的环境变量和配置,统一调整为 Vite 的配置,并标注了这些配置在使用时需要留意当前所使用的构建工具是否匹配
  • 将原本指向 Vue Router 英文官网的参考内容地址修改为指向其中文官网(因为当时编写的时候还没有中文官网)
  • 优化了里面的一些举例,使其更容易理解

优化了 “插件的开发和使用” 一章:

  • 简化了一些例子

2022-11-04

比较大幅度的优化了 “单组件的编写” 一章的 响应式 API 之 toRef 与 toRefs 的内容,使其更容易理解。

2022-11-02

优化了 “单组件的编写” 一章的 “响应式 API 之 ref ” 一节,对其 类型声明 作了一个更容易理解和更详细的讲解。

2022-10-25

  • 把 hello-node 和 hello-lib 这两个演示仓库同步到了 Gitee ,如果访问 GitHub 失败,也可以从 Gitee 拉取。
  • 在 “脚手架的升级与配置” 一章更新了 Vue Devtools 的介绍,因为插件更新后变化较大,重新截图进行了说明,并且对图片适配了文档的 Light / Dark 皮肤切换

2022-10-23

在 “工程化的前期准备” 一章的 “用 ES Module 设计模块” 一节,增加了 在浏览器里访问 ESM 小节,讲解了浏览器对原生 ESM 的访问支持和限制,以及在 HTML 页面里使用 ESM 的代码演示。

另外贯穿 “前端工程化入门” 所演示的 hello-node 项目已托管至 learning-vue3/hello-node 仓库,可使用 Git 克隆命令拉取至本地:

bash
git clone https://github.com/learning-vue3/hello-node.git

成品项目可作为学习过程中的代码参考,但更建议按照教程的讲解步骤,从零开始亲手搭建一个新项目并完成 node 开发的体验,可以更有效的提升学习效果。

2022-10-13

  1. 本书目前已不再仅限于 Vue 3 的学习,因此重写了 前言 的内容
  2. 不同时期写的内容存在一定程度的上下文不统一(例如早期使用了 @vue/cli 创建的项目做演示,后期因为 Vite 的出现并且被 Vue 官方作为其主推的构建工具,因此新内容使用的是 Vite 项目),因此对内容进行了检查和统一修正
  3. 减少了全文的人称代词,剩下不可删除的则进行了上下文统一,减少前后不一致可能会引起一些理解困惑
  4. 优化了不同时期的内容之间的章节衔接

其中第 2 、 3 、 4 点可能还有遗漏,近期持续检查修正。

2022-10-11

在 “开发 npm 包” 部分 常用的构建工具 一节增加 Node 版本检查的提示,在开发之前请确保当前的 Node 版本是否在构建工具的支持范围内,感谢 @rayadaschn#204 的建议。

2022-10-10

发布 npm 包 小节增加一个取消镜像源的提示,否则可能引起发布失败。

2022-10-09

在 “开发 npm 包” 部分所演示的 hello-lib 项目已托管至 learning-vue3/hello-lib 仓库,可使用 Git 克隆命令拉取至本地:

bash
git clone https://github.com/learning-vue3/hello-lib.git

成品项目可作为学习过程中的代码参考,但更建议按照教程的讲解步骤,从零开始亲手搭建一个新项目并完成 npm 包的开发流程,可以更有效的提升学习效果。

2022-10-08

在 “开发 npm 包” 的相关内容处增加了 添加版权注释 的构建说明,它的作用除了声明版权归属之外,还会告知使用者关于项目的主页地址、版本号、发布日期、 BUG 反馈渠道等信息,例如 jQuery / Lodash / Swiper 等库都有这样的版权注释,因此补充了这部分的内容,供有需要的开发者了解和使用。

2022-10-07

  1. 文档程序从 VuePress 2.x 迁移到 VitePress 1.x ,并做了简单的主题适配
  2. 原来的 “脚手架的升级与配置” 一章的访问地址从 update 调整为 upgrade ,访问地址已配置重定向
  3. 由于原来的 “起步准备” 一章内容有点太长,这一次续写拆分了一下内容,重新划分为单独的三章,分别是:
    • 独立出来了 了解前端工程化 一章,用于科普现代前端开发所要求的工程化概念,属于讲故事性质的一章内容
    • 工程化的前期准备 是原来 “起步准备” 的核心内容部分,主要是科普前端工程化的入门实践,属于动手实操章节
    • 另外考虑到本书是直接使用 TypeScript 来入门 Vue 3 ,语言部分也是内容的重点之一,所以把 TypeScript 部分也独立成了 快速上手 TypeScript 一章
  4. 在 “了解前端工程化” 一章里,增加以下内容:
  5. 在 “工程化的前期准备” 一章里,增加以下内容:
  6. 在 “插件的开发和使用” 一章里,增加了以下内容:

2022-09-01

更新了引用到的官方文档链接,同时修复了部分官方文档未做好重定向的无效链接,感谢 @alleluya-young#191 的反馈!

2022-07-03

补充了关于 Node.js 版本之间的区别 的一些说明。

2022-06-23

根据 #163 (comment) 的反馈,对一些 ES6+ 可能容易混淆的新语法进行了一些 Tips 补充,减少学习过程中的理解成本。

2022-05-03

更新了 “升级与配置” 一章的部分内容:

  1. 更新了使用 Create Preset 创建项目的说明,现在可以通过 --template 选项来指定模板创建,例如:
bash
# 全局安装\nnpm install -g create-preset\n\n# 将使用 `vue3-ts-vite` 模板创建一个名为 `hello-vue3` 的项目\npreset init hello-vue3 --template vue3-ts-vite
  1. 配合 1 的更新,相应的增加了 Hello Vue3 一节
  2. 配合 @vue/cli 5.x 版本的命令行交互,更新了 使用 @vue/cli 创建项目 的内容
  3. 更新了 调整 TS Config 部分的内容,由于现在通过各类脚手架创建的 TypeScript 项目都自带了该文件,所以这部分内容移除了之前的配置参考,仅保留 paths 的配置提醒

2022-04-30

最近有时间,所以对内容做了一大波更新,主要是补充了在开始开发 Vue 项目之前的一些基础知识点,当然 Vue 相关的内容也做了一些新增或者完善。

  1. 增加了全新的一章 “起步准备” ,面向对前端工程化开发不太熟悉的开发者,包含了以下内容:

  2. 在 “升级与配置” 一章里,增加了以下内容:

  3. 在 “单组件的编写” 一章里,增加了以下内容:

    • 响应式数据 设计上的变化
    • 增加了 指令 一节的内容,主要讲解如何在 Vue 3 开发自定义指令
    • 增加了 插槽 一节的内容
  4. 在 “路由的使用” 一章里,增加了以下内容,整理了一些常见的部署问题原因以及解决方案:

  5. 在 “插件的使用” 一章里,对 “本地插件” 作了进一步的细化讲解:

2022-04-20

更正了 watch API 在 侦听选项之 deep 部分内容的错误,并增加了如何检测特例的用法,感谢 @zcc0329 的反馈!

2022-04-10

重写了单组件关于 数据的侦听 这一节的内容,因为在写 Pinia 的订阅功能的时候,有很多跟 watch 相关联的知识点,发现之前这部分内容写的比较简单,所以完善了一下。

2022-04-07

补充了昨天新增的关于 Pinia 订阅 state 部分的一些内容。

2022-04-06

  1. 创建基于 Vite 的 Vue 3 项目增加了一些内容:

    • 增加了使用 Create Vue 创建 Vite 项目的说明
    • 使用 Create Preset 创建的 vue3-ts-vite 预设项目,使用 Pinia 代替 Vuex
  2. 补充了 Pinia 的部分内容:

2022-04-04

先跟大家说声抱歉,在 2022-02-09 的更新里说要写这部分内容,结果因为现实里的一些事情还有工作比较忙,现在借着假期才得以继续更新。

增加了 全局状态的管理 一章,主要是面向 Vue 3 全新的状态管理工具 Pinia 展开,里面的大部分内容也加入了和 Vuex 的对比。

虽然可以归入 组件之间的通信 里,但 Pinia 作为被官方推荐在 Vue 3 项目里作为全局状态管理的新工具,写着写着我觉得还是单独开一章来写会更方便阅读和理解。

2022-03-04

把 Algolia 的搜素功能申请下来了,看看文档的搜索体验会不会比较好(搜索框在页面的右上角)。

2022-02-28

升级了文档的程序版本,加入了 Dark Mode 暗黑模式的支持,默认跟随系统模式进行适配,可以在右上角的 “主题切换” 按钮手动切换主题。

2022-02-09

  1. 优化了 升级与配置 一章,配合 2022-02-07 Vue 3 成为默认版本带来的调整变化,主要改动如下:

  2. 高效开发 一章开头的 WARNING 改成了 TIP ,里面提到的功能在最新版的 Vue 下发挥稳定,后续如果有实验性的 API 再单独标记。

  3. 把原先演示命令里的 @next 版本都改成了 @latest ,现在不需要指定 next 就能使用 Vue 3 了。

  4. 更新了 官方文档 里面的链接说明,因为各个官网在 2022-02-07 都上线了新版本,更换了新地址。

  5. 组件之间的通信 里加入了 Pinia 的介绍,不过暂时没有写实践记录(还没有时间哈哈哈),欢迎先自行体验,这是一个官方推荐的适合 Vue 3 的状态管理工具,因为它更加面向 Composition API 的编程方式(至少在 Vuex 5 推出之前,建议用来代替 Vuex 4)。

  6. 改了一下文档名称:《Vue3.0 学习教程与实战案例》 --> 《Vue 3 入门指南与实战案例》,其他原先使用 Vue 3.0 叫法的地方,后面也会陆续改成 Vue 3 了。

2022-01-20

  1. 更新了 cnpm 的用法,原因是旧的 cnpm 源即将下线,需要使用新的域名啦!(感谢 @alleluya-young#135 的反馈!)

  2. 添加了 pnpm 的用法说明,目前开源社区越来越流行使用 pnpm ,对大型项目管理也有好处,值得了解和体验。

2021-12-05

由于离第一次执笔到现在有了差不多一年的时间,当时的前言信息有些陈旧,重新写了一下 前言

另外经过 2021 年的开发实践,很多地方我又有了新的认知,打算在接下来的时间里,我会再继续抽空把各部分的内容再精细化一下,把以前可能写的比较简单的部分,或者没有写进来的一些功能点,都再做一次完善。

2021-11-16

重写了 数据的计算 一节,之前对于计算数据的介绍,描述上确实有点难以理解,而且有点简单带过,所以重新写了这部分内容,感谢读者 @superficial 的反馈!

2021-10-07

补充了 全局编译器宏 的配置说明,本来很久前就想写的,一直偷懒,国庆结束前补充进来啦!

2021-08-17

更新了 3.2.0 版本新增的一些知识点:

  1. 增加了 使用 v-bind 动态修改 style 功能说明,同时结合上下文知识点补充了 动态绑定 CSS 一节

  2. 增加了 CSS Modules 一节的内容

  3. 增加了 useCssModule 一节的内容

2021-08-16

  1. 更新了 深度操作符 的内容,原有的 API 已被废弃,请注意使用新写法

  2. 补充了几个拓展阅读链接到 教程工具

2021-07-28

迎接 Vue 3.2.0 的新版本内容,对 script-setup 做了较多的调整。

目前 3.2 还处于 Beta 阶段,不过从这个版本开始,script-setup 脱离实验性阶段,正式进入 Vue 3.0 的队伍,本次的更新内容较多,很多测试阶段的 API 被移除或者修改,如果之前有用过 script-setup ,建议先简单看我之前的博客,了解本次的一些变更:

Vue3.0 最新动态:script-setup 定稿 部分实验性 API 将弃用

2021-03-31

  1. 调整了 使用全局 API 一节的内容

原因是原来推荐的 const { proxy } = getCurrentInstance() 的方案,在 TS 新版本校验下,使用成本非常高,原来的代码不做多层判断的情况下已经无法正常运行,所以按照目前的 “最佳实践” 重新更新了这一节。

  1. 新增了一节 全局 API 的替代方案

由于 Vue 3.0 的有意为之,这个 “最佳实践” 实际上用起来也挺繁琐(在官网和 GitHub 对此都有说明,不推荐使用),所以补充了这一节内容。

2021-03-23

DOM 元素与子组件 一节增加了 TS 类型的定义说明,以及编译失败的原因说明及解决方案补充,非常感谢 @aierong 的反馈和支持。

2021-03-21

增加了 高效开发 一章,将记录一些提高 Vue 3.0 开发效率的内容,面像对 Vue 3.0 已上手,想提高开发效率的开发者。

本次更新了 script-setup 一节的内容。

2021-01-21

增加了 组件之间的通信 一章的内容。

2020-12-18

本文档的第一个版本上线,完成了 升级与配置单组件的编写路由的使用插件的使用 等章节的编写。

2020-10-01

国庆期间开始开荒 Vue 3.0,陆陆续续打了不少笔记,业余时间着手整理成文档。

',122),h=[r];function n(o,p,s,u,c,d){return e(),l("div",null,h)}const g=a(i,[["render",n]]);export{f as __pageData,g as default}; diff --git a/assets/chunks/VPAlgoliaSearchBox.661ef68a.js b/assets/chunks/VPAlgoliaSearchBox.661ef68a.js new file mode 100644 index 00000000..68cfa6fe --- /dev/null +++ b/assets/chunks/VPAlgoliaSearchBox.661ef68a.js @@ -0,0 +1,24 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{u as fo,ag as mo,k as po,A as vo,q as ho,v as yo,b as go}from"./framework.0d8bea05.js";import{u as bo}from"./theme.edde7d9a.js";/*! @docsearch/js 3.5.1 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */function cn(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(o){return Object.getOwnPropertyDescriptor(t,o).enumerable})),n.push.apply(n,r)}return n}function I(t){for(var e=1;e=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}function st(t,e){return function(n){if(Array.isArray(n))return n}(t)||function(n,r){var o=n==null?null:typeof Symbol<"u"&&n[Symbol.iterator]||n["@@iterator"];if(o!=null){var i,a,u=[],c=!0,s=!1;try{for(o=o.call(n);!(c=(i=o.next()).done)&&(u.push(i.value),!r||u.length!==r);c=!0);}catch(l){s=!0,a=l}finally{try{c||o.return==null||o.return()}finally{if(s)throw a}}return u}}(t,e)||yr(t,e)||function(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}()}function ft(t){return function(e){if(Array.isArray(e))return Lt(e)}(t)||function(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}(t)||yr(t)||function(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}()}function yr(t,e){if(t){if(typeof t=="string")return Lt(t,e);var n=Object.prototype.toString.call(t).slice(8,-1);return n==="Object"&&t.constructor&&(n=t.constructor.name),n==="Map"||n==="Set"?Array.from(t):n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?Lt(t,e):void 0}}function Lt(t,e){(e==null||e>t.length)&&(e=t.length);for(var n=0,r=new Array(e);n3)for(n=[n],i=3;i0?Pe(v.type,v.props,v.key,null,v.__v):v)!=null){if(v.__=n,v.__b=n.__b+1,(p=b[l])===null||p&&v.key==p.key&&v.type===p.type)b[l]=void 0;else for(m=0;m<_;m++){if((p=b[m])&&v.key==p.key&&v.type===p.type){b[m]=void 0;break}p=null}Zt(t,v,p=p||mt,o,i,a,u,c,s),d=v.__e,(m=v.ref)&&p.ref!=m&&(y||(y=[]),p.ref&&y.push(p.ref,null,v),y.push(m,v.__c||d,v)),d!=null?(h==null&&(h=d),typeof v.type=="function"&&v.__k!=null&&v.__k===p.__k?v.__d=c=wr(v,c,t):c=jr(t,v,p,b,d,c),s||n.type!=="option"?typeof n.type=="function"&&(n.__d=c):t.value=""):c&&p.__e==c&&c.parentNode!=t&&(c=Ve(p))}for(n.__e=h,l=_;l--;)b[l]!=null&&(typeof n.type=="function"&&b[l].__e!=null&&b[l].__e==n.__d&&(n.__d=Ve(r,l+1)),Ir(b[l],b[l]));if(y)for(l=0;l3)for(n=[n],i=3;i=n.__.length&&n.__.push({}),n.__[t]}function kr(t){return pe=1,Ar(xr,t)}function Ar(t,e,n){var r=ze(de++,2);return r.t=t,r.__c||(r.__=[n?n(e):xr(void 0,e),function(o){var i=r.t(r.__[0],o);r.__[0]!==i&&(r.__=[i,r.__[1]],r.__c.setState({}))}],r.__c=q),r.__}function Cr(t,e){var n=ze(de++,3);!w.__s&&Yt(n.__H,e)&&(n.__=t,n.__H=e,q.__H.__h.push(n))}function gn(t,e){var n=ze(de++,4);!w.__s&&Yt(n.__H,e)&&(n.__=t,n.__H=e,q.__h.push(n))}function Pt(t,e){var n=ze(de++,7);return Yt(n.__H,e)&&(n.__=t(),n.__H=e,n.__h=t),n.__}function Po(){Ht.forEach(function(t){if(t.__P)try{t.__H.__h.forEach(at),t.__H.__h.forEach(Ut),t.__H.__h=[]}catch(e){t.__H.__h=[],w.__e(e,t.__v)}}),Ht=[]}w.__b=function(t){q=null,pn&&pn(t)},w.__r=function(t){vn&&vn(t),de=0;var e=(q=t.__c).__H;e&&(e.__h.forEach(at),e.__h.forEach(Ut),e.__h=[])},w.diffed=function(t){dn&&dn(t);var e=t.__c;e&&e.__H&&e.__H.__h.length&&(Ht.push(e)!==1&&mn===w.requestAnimationFrame||((mn=w.requestAnimationFrame)||function(n){var r,o=function(){clearTimeout(i),bn&&cancelAnimationFrame(r),setTimeout(n)},i=setTimeout(o,100);bn&&(r=requestAnimationFrame(o))})(Po)),q=void 0},w.__c=function(t,e){e.some(function(n){try{n.__h.forEach(at),n.__h=n.__h.filter(function(r){return!r.__||Ut(r)})}catch(r){e.some(function(o){o.__h&&(o.__h=[])}),e=[],w.__e(r,n.__v)}}),hn&&hn(t,e)},w.unmount=function(t){yn&&yn(t);var e=t.__c;if(e&&e.__H)try{e.__H.__.forEach(at)}catch(n){w.__e(n,e.__v)}};var bn=typeof requestAnimationFrame=="function";function at(t){var e=q;typeof t.__c=="function"&&t.__c(),q=e}function Ut(t){var e=q;t.__c=t.__(),q=e}function Yt(t,e){return!t||t.length!==e.length||e.some(function(n,r){return n!==t[r]})}function xr(t,e){return typeof e=="function"?e(t):e}function Nr(t,e){for(var n in e)t[n]=e[n];return t}function Ft(t,e){for(var n in t)if(n!=="__source"&&!(n in e))return!0;for(var r in e)if(r!=="__source"&&t[r]!==e[r])return!0;return!1}function Bt(t){this.props=t}(Bt.prototype=new W).isPureReactComponent=!0,Bt.prototype.shouldComponentUpdate=function(t,e){return Ft(this.props,t)||Ft(this.state,e)};var _n=w.__b;w.__b=function(t){t.type&&t.type.__f&&t.ref&&(t.props.ref=t.ref,t.ref=null),_n&&_n(t)};var Io=typeof Symbol<"u"&&Symbol.for&&Symbol.for("react.forward_ref")||3911,On=function(t,e){return t==null?null:J(J(t).map(e))},Do={map:On,forEach:On,count:function(t){return t?J(t).length:0},only:function(t){var e=J(t);if(e.length!==1)throw"Children.only";return e[0]},toArray:J},ko=w.__e;function ct(){this.__u=0,this.t=null,this.__b=null}function Tr(t){var e=t.__.__c;return e&&e.__e&&e.__e(t)}function we(){this.u=null,this.o=null}w.__e=function(t,e,n){if(t.then){for(var r,o=e;o=o.__;)if((r=o.__c)&&r.__c)return e.__e==null&&(e.__e=n.__e,e.__k=n.__k),r.__c(t,e)}ko(t,e,n)},(ct.prototype=new W).__c=function(t,e){var n=e.__c,r=this;r.t==null&&(r.t=[]),r.t.push(n);var o=Tr(r.__v),i=!1,a=function(){i||(i=!0,n.componentWillUnmount=n.__c,o?o(u):u())};n.__c=n.componentWillUnmount,n.componentWillUnmount=function(){a(),n.__c&&n.__c()};var u=function(){if(!--r.__u){if(r.state.__e){var s=r.state.__e;r.__v.__k[0]=function m(p,v,d){return p&&(p.__v=null,p.__k=p.__k&&p.__k.map(function(h){return m(h,v,d)}),p.__c&&p.__c.__P===v&&(p.__e&&d.insertBefore(p.__e,p.__d),p.__c.__e=!0,p.__c.__P=d)),p}(s,s.__c.__P,s.__c.__O)}var l;for(r.setState({__e:r.__b=null});l=r.t.pop();)l.forceUpdate()}},c=e.__h===!0;r.__u++||c||r.setState({__e:r.__b=r.__v.__k[0]}),t.then(a,a)},ct.prototype.componentWillUnmount=function(){this.t=[]},ct.prototype.render=function(t,e){if(this.__b){if(this.__v.__k){var n=document.createElement("div"),r=this.__v.__k[0].__c;this.__v.__k[0]=function i(a,u,c){return a&&(a.__c&&a.__c.__H&&(a.__c.__H.__.forEach(function(s){typeof s.__c=="function"&&s.__c()}),a.__c.__H=null),(a=Nr({},a)).__c!=null&&(a.__c.__P===c&&(a.__c.__P=u),a.__c=null),a.__k=a.__k&&a.__k.map(function(s){return i(s,u,c)})),a}(this.__b,n,r.__O=r.__P)}this.__b=null}var o=e.__e&&V(X,null,t.fallback);return o&&(o.__h=null),[V(X,null,e.__e?null:t.children),o]};var Sn=function(t,e,n){if(++n[1]===n[0]&&t.o.delete(e),t.props.revealOrder&&(t.props.revealOrder[0]!=="t"||!t.o.size))for(n=t.u;n;){for(;n.length>3;)n.pop()();if(n[1]>>1,1),e.i.removeChild(r)}}),We(V(Ao,{context:e.context},t.__v),e.l)):e.l&&e.componentWillUnmount()}function Rr(t,e){return V(Co,{__v:t,i:e})}(we.prototype=new W).__e=function(t){var e=this,n=Tr(e.__v),r=e.o.get(t);return r[0]++,function(o){var i=function(){e.props.revealOrder?(r.push(o),Sn(e,t,r)):o()};n?n(i):i()}},we.prototype.render=function(t){this.u=null,this.o=new Map;var e=J(t.children);t.revealOrder&&t.revealOrder[0]==="b"&&e.reverse();for(var n=e.length;n--;)this.o.set(e[n],this.u=[1,0,this.u]);return t.children},we.prototype.componentDidUpdate=we.prototype.componentDidMount=function(){var t=this;this.o.forEach(function(e,n){Sn(t,n,e)})};var qr=typeof Symbol<"u"&&Symbol.for&&Symbol.for("react.element")||60103,xo=/^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|fill|flood|font|glyph(?!R)|horiz|marker(?!H|W|U)|overline|paint|stop|strikethrough|stroke|text(?!L)|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/,No=function(t){return(typeof Symbol<"u"&&Be(Symbol())=="symbol"?/fil|che|rad/i:/fil|che|ra/i).test(t)};function Lr(t,e,n){return e.__k==null&&(e.textContent=""),We(t,e),typeof n=="function"&&n(),t?t.__c:null}W.prototype.isReactComponent={},["componentWillMount","componentWillReceiveProps","componentWillUpdate"].forEach(function(t){Object.defineProperty(W.prototype,t,{configurable:!0,get:function(){return this["UNSAFE_"+t]},set:function(e){Object.defineProperty(this,t,{configurable:!0,writable:!0,value:e})}})});var wn=w.event;function To(){}function Ro(){return this.cancelBubble}function qo(){return this.defaultPrevented}w.event=function(t){return wn&&(t=wn(t)),t.persist=To,t.isPropagationStopped=Ro,t.isDefaultPrevented=qo,t.nativeEvent=t};var Mr,jn={configurable:!0,get:function(){return this.class}},En=w.vnode;w.vnode=function(t){var e=t.type,n=t.props,r=n;if(typeof e=="string"){for(var o in r={},n){var i=n[o];o==="value"&&"defaultValue"in n&&i==null||(o==="defaultValue"&&"value"in n&&n.value==null?o="value":o==="download"&&i===!0?i="":/ondoubleclick/i.test(o)?o="ondblclick":/^onchange(textarea|input)/i.test(o+e)&&!No(n.type)?o="oninput":/^on(Ani|Tra|Tou|BeforeInp)/.test(o)?o=o.toLowerCase():xo.test(o)?o=o.replace(/[A-Z0-9]/,"-$&").toLowerCase():i===null&&(i=void 0),r[o]=i)}e=="select"&&r.multiple&&Array.isArray(r.value)&&(r.value=J(n.children).forEach(function(a){a.props.selected=r.value.indexOf(a.props.value)!=-1})),e=="select"&&r.defaultValue!=null&&(r.value=J(n.children).forEach(function(a){a.props.selected=r.multiple?r.defaultValue.indexOf(a.props.value)!=-1:r.defaultValue==a.props.value})),t.props=r}e&&n.class!=n.className&&(jn.enumerable="className"in n,n.className!=null&&(r.class=n.className),Object.defineProperty(r,"className",jn)),t.$$typeof=qr,En&&En(t)};var Pn=w.__r;w.__r=function(t){Pn&&Pn(t),Mr=t.__c};var Lo={ReactCurrentDispatcher:{current:{readContext:function(t){return Mr.__n[t.__c].props.value}}}};(typeof performance>"u"?"undefined":Be(performance))=="object"&&typeof performance.now=="function"&&performance.now.bind(performance);function In(t){return!!t&&t.$$typeof===qr}var f={useState:kr,useReducer:Ar,useEffect:Cr,useLayoutEffect:gn,useRef:function(t){return pe=5,Pt(function(){return{current:t}},[])},useImperativeHandle:function(t,e,n){pe=6,gn(function(){typeof t=="function"?t(e()):t&&(t.current=e())},n==null?n:n.concat(t))},useMemo:Pt,useCallback:function(t,e){return pe=8,Pt(function(){return t},e)},useContext:function(t){var e=q.context[t.__c],n=ze(de++,9);return n.__c=t,e?(n.__==null&&(n.__=!0,e.sub(q)),e.props.value):t.__},useDebugValue:function(t,e){w.useDebugValue&&w.useDebugValue(e?e(t):t)},version:"16.8.0",Children:Do,render:Lr,hydrate:function(t,e,n){return Dr(t,e),typeof n=="function"&&n(),t?t.__c:null},unmountComponentAtNode:function(t){return!!t.__k&&(We(null,t),!0)},createPortal:Rr,createElement:V,createContext:function(t,e){var n={__c:e="__cC"+br++,__:t,Consumer:function(r,o){return r.children(o)},Provider:function(r){var o,i;return this.getChildContext||(o=[],(i={})[e]=this,this.getChildContext=function(){return i},this.shouldComponentUpdate=function(a){this.props.value!==a.value&&o.some(Mt)},this.sub=function(a){o.push(a);var u=a.componentWillUnmount;a.componentWillUnmount=function(){o.splice(o.indexOf(a),1),u&&u.call(a)}}),r.children}};return n.Provider.__=n.Consumer.contextType=n},createFactory:function(t){return V.bind(null,t)},cloneElement:function(t){return In(t)?Eo.apply(null,arguments):t},createRef:function(){return{current:null}},Fragment:X,isValidElement:In,findDOMNode:function(t){return t&&(t.base||t.nodeType===1&&t)||null},Component:W,PureComponent:Bt,memo:function(t,e){function n(o){var i=this.props.ref,a=i==o.ref;return!a&&i&&(i.call?i(null):i.current=null),e?!e(this.props,o)||!a:Ft(this.props,o)}function r(o){return this.shouldComponentUpdate=n,V(t,o)}return r.displayName="Memo("+(t.displayName||t.name)+")",r.prototype.isReactComponent=!0,r.__f=!0,r},forwardRef:function(t){function e(n,r){var o=Nr({},n);return delete o.ref,t(o,(r=n.ref||r)&&(Be(r)!="object"||"current"in r)?r:null)}return e.$$typeof=Io,e.render=e,e.prototype.isReactComponent=e.__f=!0,e.displayName="ForwardRef("+(t.displayName||t.name)+")",e},unstable_batchedUpdates:function(t,e){return t(e)},StrictMode:X,Suspense:ct,SuspenseList:we,lazy:function(t){var e,n,r;function o(i){if(e||(e=t()).then(function(a){n=a.default||a},function(a){r=a}),r)throw r;if(!n)throw e;return V(n,i)}return o.displayName="Lazy",o.__f=!0,o},__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED:Lo};function Mo(){return f.createElement("svg",{width:"15",height:"15",className:"DocSearch-Control-Key-Icon"},f.createElement("path",{d:"M4.505 4.496h2M5.505 5.496v5M8.216 4.496l.055 5.993M10 7.5c.333.333.5.667.5 1v2M12.326 4.5v5.996M8.384 4.496c1.674 0 2.116 0 2.116 1.5s-.442 1.5-2.116 1.5M3.205 9.303c-.09.448-.277 1.21-1.241 1.203C1 10.5.5 9.513.5 8V7c0-1.57.5-2.5 1.464-2.494.964.006 1.134.598 1.24 1.342M12.553 10.5h1.953",strokeWidth:"1.2",stroke:"currentColor",fill:"none",strokeLinecap:"square"}))}function Hr(){return f.createElement("svg",{width:"20",height:"20",className:"DocSearch-Search-Icon",viewBox:"0 0 20 20"},f.createElement("path",{d:"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z",stroke:"currentColor",fill:"none",fillRule:"evenodd",strokeLinecap:"round",strokeLinejoin:"round"}))}var Ho=["translations"];function Vt(){return Vt=Object.assign||function(t){for(var e=1;et.length)&&(e=t.length);for(var n=0,r=new Array(e);n=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}var Bo=f.forwardRef(function(t,e){var n=t.translations,r=n===void 0?{}:n,o=Fo(t,Ho),i=r.buttonText,a=i===void 0?"Search":i,u=r.buttonAriaLabel,c=u===void 0?"Search":u,s=Uo(kr(null),2),l=s[0],m=s[1];return Cr(function(){typeof navigator<"u"&&(/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)?m("⌘"):m("Ctrl"))},[]),f.createElement("button",Vt({type:"button",className:"DocSearch DocSearch-Button","aria-label":c},o,{ref:e}),f.createElement("span",{className:"DocSearch-Button-Container"},f.createElement(Hr,null),f.createElement("span",{className:"DocSearch-Button-Placeholder"},a)),f.createElement("span",{className:"DocSearch-Button-Keys"},l!==null&&f.createElement(f.Fragment,null,f.createElement("kbd",{className:"DocSearch-Button-Key"},l==="Ctrl"?f.createElement(Mo,null):l),f.createElement("kbd",{className:"DocSearch-Button-Key"},"K"))))});function Ur(t,e){var n=void 0;return function(){for(var r=arguments.length,o=new Array(r),i=0;it.length)&&(e=t.length);for(var n=0,r=new Array(e);nt.length)&&(e=t.length);for(var n=0,r=new Array(e);n=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}function xn(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(o){return Object.getOwnPropertyDescriptor(t,o).enumerable})),n.push.apply(n,r)}return n}function ve(t){for(var e=1;e1&&arguments[1]!==void 0?arguments[1]:20,n=[],r=0;r=3||n===2&&r>=4||n===1&&r>=10);function i(a,u,c){if(o&&c!==void 0){var s=c[0].__autocomplete_algoliaCredentials,l={"X-Algolia-Application-Id":s.appId,"X-Algolia-API-Key":s.apiKey};t.apply(void 0,[a].concat(Ye(u),[{headers:l}]))}else t.apply(void 0,[a].concat(Ye(u)))}return{init:function(a,u){t("init",{appId:a,apiKey:u})},setUserToken:function(a){t("setUserToken",a)},clickedObjectIDsAfterSearch:function(){for(var a=arguments.length,u=new Array(a),c=0;c0&&i("clickedObjectIDsAfterSearch",Ge(u),u[0].items)},clickedObjectIDs:function(){for(var a=arguments.length,u=new Array(a),c=0;c0&&i("clickedObjectIDs",Ge(u),u[0].items)},clickedFilters:function(){for(var a=arguments.length,u=new Array(a),c=0;c0&&t.apply(void 0,["clickedFilters"].concat(u))},convertedObjectIDsAfterSearch:function(){for(var a=arguments.length,u=new Array(a),c=0;c0&&i("convertedObjectIDsAfterSearch",Ge(u),u[0].items)},convertedObjectIDs:function(){for(var a=arguments.length,u=new Array(a),c=0;c0&&i("convertedObjectIDs",Ge(u),u[0].items)},convertedFilters:function(){for(var a=arguments.length,u=new Array(a),c=0;c0&&t.apply(void 0,["convertedFilters"].concat(u))},viewedObjectIDs:function(){for(var a=arguments.length,u=new Array(a),c=0;c0&&u.reduce(function(s,l){var m=l.items,p=Br(l,$o);return[].concat(Ye(s),Ye(Qo(ve(ve({},p),{},{objectIDs:(m==null?void 0:m.map(function(v){return v.objectID}))||p.objectIDs})).map(function(v){return{items:m,payload:v}})))},[]).forEach(function(s){var l=s.items;return i("viewedObjectIDs",[s.payload],l)})},viewedFilters:function(){for(var a=arguments.length,u=new Array(a),c=0;c0&&t.apply(void 0,["viewedFilters"].concat(u))}}}function Yo(t){var e=t.items.reduce(function(n,r){var o;return n[r.__autocomplete_indexName]=((o=n[r.__autocomplete_indexName])!==null&&o!==void 0?o:[]).concat(r),n},{});return Object.keys(e).map(function(n){return{index:n,items:e[n],algoliaSource:["autocomplete"]}})}function Dt(t){return t.objectID&&t.__autocomplete_indexName&&t.__autocomplete_queryID}function De(t){return De=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},De(t)}function ae(t){return function(e){if(Array.isArray(e))return kt(e)}(t)||function(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}(t)||function(e,n){if(e){if(typeof e=="string")return kt(e,n);var r=Object.prototype.toString.call(e).slice(8,-1);if(r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set")return Array.from(e);if(r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return kt(e,n)}}(t)||function(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}()}function kt(t,e){(e==null||e>t.length)&&(e=t.length);for(var n=0,r=new Array(e);n0&&ei({onItemsChange:r,items:p,insights:u,state:m}))}},0);return{name:"aa.algoliaInsightsPlugin",subscribe:function(l){var m=l.setContext,p=l.onSelect,v=l.onActive;a("addAlgoliaAgent","insights-plugin"),m({algoliaInsightsPlugin:{__algoliaSearchParameters:{clickAnalytics:!0},insights:u}}),p(function(d){var h=d.item,y=d.state,b=d.event;Dt(h)&&o({state:y,event:b,insights:u,item:h,insightsEvents:[G({eventName:"Item Selected"},An({item:h,items:c.current}))]})}),v(function(d){var h=d.item,y=d.state,b=d.event;Dt(h)&&i({state:y,event:b,insights:u,item:h,insightsEvents:[G({eventName:"Item Active"},An({item:h,items:c.current}))]})})},onStateChange:function(l){var m=l.state;s({state:m})},__autocomplete_pluginOptions:t}}function ut(t,e){var n=e;return{then:function(r,o){return ut(t.then(Xe(r,n,t),Xe(o,n,t)),n)},catch:function(r){return ut(t.catch(Xe(r,n,t)),n)},finally:function(r){return r&&n.onCancelList.push(r),ut(t.finally(Xe(r&&function(){return n.onCancelList=[],r()},n,t)),n)},cancel:function(){n.isCanceled=!0;var r=n.onCancelList;n.onCancelList=[],r.forEach(function(o){o()})},isCanceled:function(){return n.isCanceled===!0}}}function Tn(t){return ut(t,{isCanceled:!1,onCancelList:[]})}function Xe(t,e,n){return t?function(r){return e.isCanceled?r:t(r)}:n}function Rn(t,e,n,r){if(!n)return null;if(t<0&&(e===null||r!==null&&e===0))return n+t;var o=(e===null?-1:e)+t;return o<=-1||o>=n?r===null?null:0:o}function qn(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(o){return Object.getOwnPropertyDescriptor(t,o).enumerable})),n.push.apply(n,r)}return n}function Ln(t){for(var e=1;et.length)&&(e=t.length);for(var n=0,r=new Array(e);n0},reshape:function(i){return i.sources}},t),{},{id:(n=t.id)!==null&&n!==void 0?n:"autocomplete-".concat(Vo++),plugins:o,initialState:ce({activeItemId:null,query:"",completion:null,collections:[],isOpen:!1,status:"idle",context:{}},t.initialState),onStateChange:function(i){var a;(a=t.onStateChange)===null||a===void 0||a.call(t,i),o.forEach(function(u){var c;return(c=u.onStateChange)===null||c===void 0?void 0:c.call(u,i)})},onSubmit:function(i){var a;(a=t.onSubmit)===null||a===void 0||a.call(t,i),o.forEach(function(u){var c;return(c=u.onSubmit)===null||c===void 0?void 0:c.call(u,i)})},onReset:function(i){var a;(a=t.onReset)===null||a===void 0||a.call(t,i),o.forEach(function(u){var c;return(c=u.onReset)===null||c===void 0?void 0:c.call(u,i)})},getSources:function(i){return Promise.all([].concat(ci(o.map(function(a){return a.getSources})),[t.getSources]).filter(Boolean).map(function(a){return function(u,c){var s=[];return Promise.resolve(u(c)).then(function(l){return Promise.all(l.filter(function(m){return!!m}).map(function(m){if(m.sourceId,s.includes(m.sourceId))throw new Error("[Autocomplete] The `sourceId` ".concat(JSON.stringify(m.sourceId)," is not unique."));s.push(m.sourceId);var p={getItemInputValue:function(d){return d.state.query},getItemUrl:function(){},onSelect:function(d){(0,d.setIsOpen)(!1)},onActive:vt,onResolve:vt};Object.keys(p).forEach(function(d){p[d].__default=!0});var v=Ln(Ln({},p),m);return Promise.resolve(v)}))})}(a,i)})).then(function(a){return Ke(a)}).then(function(a){return a.map(function(u){return ce(ce({},u),{},{onSelect:function(c){u.onSelect(c),e.forEach(function(s){var l;return(l=s.onSelect)===null||l===void 0?void 0:l.call(s,c)})},onActive:function(c){u.onActive(c),e.forEach(function(s){var l;return(l=s.onActive)===null||l===void 0?void 0:l.call(s,c)})},onResolve:function(c){u.onResolve(c),e.forEach(function(s){var l;return(l=s.onResolve)===null||l===void 0?void 0:l.call(s,c)})}})})})},navigator:ce({navigate:function(i){var a=i.itemUrl;r.location.assign(a)},navigateNewTab:function(i){var a=i.itemUrl,u=r.open(a,"_blank","noopener");u==null||u.focus()},navigateNewWindow:function(i){var a=i.itemUrl;r.open(a,"_blank","noopener")}},t.navigator)})}function Ne(t){return Ne=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Ne(t)}function Fn(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(o){return Object.getOwnPropertyDescriptor(t,o).enumerable})),n.push.apply(n,r)}return n}function tt(t){for(var e=1;et.length)&&(e=t.length);for(var n=0,r=new Array(e);n=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}var Wn,xt,rt,Se=null,Kn=(Wn=-1,xt=-1,rt=void 0,function(t){var e=++Wn;return Promise.resolve(t).then(function(n){return rt&&e=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}function Le(t){return Le=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Le(t)}var _i=["props","refresh","store"],Oi=["inputElement","formElement","panelElement"],Si=["inputElement"],wi=["inputElement","maxLength"],ji=["sourceIndex"],Ei=["sourceIndex"],Pi=["item","source","sourceIndex"];function $n(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(o){return Object.getOwnPropertyDescriptor(t,o).enumerable})),n.push.apply(n,r)}return n}function R(t){for(var e=1;e=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}function Di(t){var e=t.props,n=t.refresh,r=t.store,o=ne(t,_i),i=function(a,u){return u!==void 0?"".concat(a,"-").concat(u):a};return{getEnvironmentProps:function(a){var u=a.inputElement,c=a.formElement,s=a.panelElement;function l(m){!r.getState().isOpen&&r.pendingRequests.isEmpty()||m.target===u||[c,s].some(function(p){return v=p,d=m.target,v===d||v.contains(d);var v,d})===!1&&(r.dispatch("blur",null),e.debug||r.pendingRequests.cancelAll())}return R({onTouchStart:l,onMouseDown:l,onTouchMove:function(m){r.getState().isOpen!==!1&&u===e.environment.document.activeElement&&m.target!==u&&u.blur()}},ne(a,Oi))},getRootProps:function(a){return R({role:"combobox","aria-expanded":r.getState().isOpen,"aria-haspopup":"listbox","aria-owns":r.getState().isOpen?"".concat(e.id,"-list"):void 0,"aria-labelledby":"".concat(e.id,"-label")},a)},getFormProps:function(a){return a.inputElement,R({action:"",noValidate:!0,role:"search",onSubmit:function(u){var c;u.preventDefault(),e.onSubmit(R({event:u,refresh:n,state:r.getState()},o)),r.dispatch("submit",null),(c=a.inputElement)===null||c===void 0||c.blur()},onReset:function(u){var c;u.preventDefault(),e.onReset(R({event:u,refresh:n,state:r.getState()},o)),r.dispatch("reset",null),(c=a.inputElement)===null||c===void 0||c.focus()}},ne(a,Si))},getLabelProps:function(a){var u=a||{},c=u.sourceIndex,s=ne(u,ji);return R({htmlFor:"".concat(i(e.id,c),"-input"),id:"".concat(i(e.id,c),"-label")},s)},getInputProps:function(a){var u;function c(y){(e.openOnFocus||r.getState().query)&&se(R({event:y,props:e,query:r.getState().completion||r.getState().query,refresh:n,store:r},o)),r.dispatch("focus",null)}var s=a||{},l=(s.inputElement,s.maxLength),m=l===void 0?512:l,p=ne(s,wi),v=fe(r.getState()),d=function(y){return!!(y&&y.match(ri))}(((u=e.environment.navigator)===null||u===void 0?void 0:u.userAgent)||""),h=v!=null&&v.itemUrl&&!d?"go":"search";return R({"aria-autocomplete":"both","aria-activedescendant":r.getState().isOpen&&r.getState().activeItemId!==null?"".concat(e.id,"-item-").concat(r.getState().activeItemId):void 0,"aria-controls":r.getState().isOpen?"".concat(e.id,"-list"):void 0,"aria-labelledby":"".concat(e.id,"-label"),value:r.getState().completion||r.getState().query,id:"".concat(e.id,"-input"),autoComplete:"off",autoCorrect:"off",autoCapitalize:"off",enterKeyHint:h,spellCheck:"false",autoFocus:e.autoFocus,placeholder:e.placeholder,maxLength:m,type:"search",onChange:function(y){se(R({event:y,props:e,query:y.currentTarget.value.slice(0,m),refresh:n,store:r},o))},onKeyDown:function(y){(function(b){var _=b.event,S=b.props,O=b.refresh,g=b.store,P=bi(b,yi);if(_.key==="ArrowUp"||_.key==="ArrowDown"){var C=function(){var M=S.environment.document.getElementById("".concat(S.id,"-item-").concat(g.getState().activeItemId));M&&(M.scrollIntoViewIfNeeded?M.scrollIntoViewIfNeeded(!1):M.scrollIntoView(!1))},L=function(){var M=fe(g.getState());if(g.getState().activeItemId!==null&&M){var Ot=M.item,St=M.itemInputValue,$e=M.itemUrl,B=M.source;B.onActive(te({event:_,item:Ot,itemInputValue:St,itemUrl:$e,refresh:O,source:B,state:g.getState()},P))}};_.preventDefault(),g.getState().isOpen===!1&&(S.openOnFocus||g.getState().query)?se(te({event:_,props:S,query:g.getState().query,refresh:O,store:g},P)).then(function(){g.dispatch(_.key,{nextActiveItemId:S.defaultActiveItemId}),L(),setTimeout(C,0)}):(g.dispatch(_.key,{}),L(),C())}else if(_.key==="Escape")_.preventDefault(),g.dispatch(_.key,null),g.pendingRequests.cancelAll();else if(_.key==="Tab")g.dispatch("blur",null),g.pendingRequests.cancelAll();else if(_.key==="Enter"){if(g.getState().activeItemId===null||g.getState().collections.every(function(M){return M.items.length===0}))return void(S.debug||g.pendingRequests.cancelAll());_.preventDefault();var x=fe(g.getState()),k=x.item,N=x.itemInputValue,U=x.itemUrl,F=x.source;if(_.metaKey||_.ctrlKey)U!==void 0&&(F.onSelect(te({event:_,item:k,itemInputValue:N,itemUrl:U,refresh:O,source:F,state:g.getState()},P)),S.navigator.navigateNewTab({itemUrl:U,item:k,state:g.getState()}));else if(_.shiftKey)U!==void 0&&(F.onSelect(te({event:_,item:k,itemInputValue:N,itemUrl:U,refresh:O,source:F,state:g.getState()},P)),S.navigator.navigateNewWindow({itemUrl:U,item:k,state:g.getState()}));else if(!_.altKey){if(U!==void 0)return F.onSelect(te({event:_,item:k,itemInputValue:N,itemUrl:U,refresh:O,source:F,state:g.getState()},P)),void S.navigator.navigate({itemUrl:U,item:k,state:g.getState()});se(te({event:_,nextState:{isOpen:!1},props:S,query:N,refresh:O,store:g},P)).then(function(){F.onSelect(te({event:_,item:k,itemInputValue:N,itemUrl:U,refresh:O,source:F,state:g.getState()},P))})}}})(R({event:y,props:e,refresh:n,store:r},o))},onFocus:c,onBlur:vt,onClick:function(y){a.inputElement!==e.environment.document.activeElement||r.getState().isOpen||c(y)}},p)},getPanelProps:function(a){return R({onMouseDown:function(u){u.preventDefault()},onMouseLeave:function(){r.dispatch("mouseleave",null)}},a)},getListProps:function(a){var u=a||{},c=u.sourceIndex,s=ne(u,Ei);return R({role:"listbox","aria-labelledby":"".concat(i(e.id,c),"-label"),id:"".concat(i(e.id,c),"-list")},s)},getItemProps:function(a){var u=a.item,c=a.source,s=a.sourceIndex,l=ne(a,Pi);return R({id:"".concat(i(e.id,s),"-item-").concat(u.__autocomplete_id),role:"option","aria-selected":r.getState().activeItemId===u.__autocomplete_id,onMouseMove:function(m){if(u.__autocomplete_id!==r.getState().activeItemId){r.dispatch("mousemove",u.__autocomplete_id);var p=fe(r.getState());if(r.getState().activeItemId!==null&&p){var v=p.item,d=p.itemInputValue,h=p.itemUrl,y=p.source;y.onActive(R({event:m,item:v,itemInputValue:d,itemUrl:h,refresh:n,source:y,state:r.getState()},o))}}},onMouseDown:function(m){m.preventDefault()},onClick:function(m){var p=c.getItemInputValue({item:u,state:r.getState()}),v=c.getItemUrl({item:u,state:r.getState()});(v?Promise.resolve():se(R({event:m,nextState:{isOpen:!1},props:e,query:p,refresh:n,store:r},o))).then(function(){c.onSelect(R({event:m,item:u,itemInputValue:p,itemUrl:v,refresh:n,source:c,state:r.getState()},o))})}},l)}}}function Me(t){return Me=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Me(t)}function Jn(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(o){return Object.getOwnPropertyDescriptor(t,o).enumerable})),n.push.apply(n,r)}return n}function ki(t){for(var e=1;et.length)&&(e=t.length);for(var n=0,r=new Array(e);n=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}function Yi(t){var e=t.translations,n=e===void 0?{}:e,r=Zi(t,Ji),o=n.noResultsText,i=o===void 0?"No results for":o,a=n.suggestedQueryText,u=a===void 0?"Try searching for":a,c=n.reportMissingResultsText,s=c===void 0?"Believe this query should return results?":c,l=n.reportMissingResultsLinkText,m=l===void 0?"Let us know.":l,p=r.state.context.searchSuggestions;return f.createElement("div",{className:"DocSearch-NoResults"},f.createElement("div",{className:"DocSearch-Screen-Icon"},f.createElement(zi,null)),f.createElement("p",{className:"DocSearch-Title"},i,' "',f.createElement("strong",null,r.state.query),'"'),p&&p.length>0&&f.createElement("div",{className:"DocSearch-NoResults-Prefill-List"},f.createElement("p",{className:"DocSearch-Help"},u,":"),f.createElement("ul",null,p.slice(0,3).reduce(function(v,d){return[].concat(Qi(v),[f.createElement("li",{key:d},f.createElement("button",{className:"DocSearch-Prefill",key:d,type:"button",onClick:function(){r.setQuery(d.toLowerCase()+" "),r.refresh(),r.inputRef.current.focus()}},d))])},[]))),r.getMissingResultsUrl&&f.createElement("p",{className:"DocSearch-Help"},"".concat(s," "),f.createElement("a",{href:r.getMissingResultsUrl({query:r.state.query}),target:"_blank",rel:"noopener noreferrer"},m)))}var Gi=["hit","attribute","tagName"];function Xn(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(o){return Object.getOwnPropertyDescriptor(t,o).enumerable})),n.push.apply(n,r)}return n}function er(t){for(var e=1;e=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}function tr(t,e){return e.split(".").reduce(function(n,r){return n!=null&&n[r]?n[r]:null},t)}function le(t){var e=t.hit,n=t.attribute,r=t.tagName;return V(r===void 0?"span":r,er(er({},ea(t,Gi)),{},{dangerouslySetInnerHTML:{__html:tr(e,"_snippetResult.".concat(n,".value"))||tr(e,n)}}))}function nr(t,e){return function(n){if(Array.isArray(n))return n}(t)||function(n,r){var o=n==null?null:typeof Symbol<"u"&&n[Symbol.iterator]||n["@@iterator"];if(o!=null){var i,a,u=[],c=!0,s=!1;try{for(o=o.call(n);!(c=(i=o.next()).done)&&(u.push(i.value),!r||u.length!==r);c=!0);}catch(l){s=!0,a=l}finally{try{c||o.return==null||o.return()}finally{if(s)throw a}}return u}}(t,e)||function(n,r){if(n){if(typeof n=="string")return rr(n,r);var o=Object.prototype.toString.call(n).slice(8,-1);if(o==="Object"&&n.constructor&&(o=n.constructor.name),o==="Map"||o==="Set")return Array.from(n);if(o==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(o))return rr(n,r)}}(t,e)||function(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}()}function rr(t,e){(e==null||e>t.length)&&(e=t.length);for(var n=0,r=new Array(e);n|<\/mark>)/g,ra=RegExp(zr.source);function $r(t){var e,n,r,o,i,a=t;if(!a.__docsearch_parent&&!t._highlightResult)return t.hierarchy.lvl0;var u=((a.__docsearch_parent?(e=a.__docsearch_parent)===null||e===void 0||(n=e._highlightResult)===null||n===void 0||(r=n.hierarchy)===null||r===void 0?void 0:r.lvl0:(o=t._highlightResult)===null||o===void 0||(i=o.hierarchy)===null||i===void 0?void 0:i.lvl0)||{}).value;return u&&ra.test(u)?u.replace(zr,""):u}function $t(){return $t=Object.assign||function(t){for(var e=1;e=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}function ca(t){var e=t.translations,n=e===void 0?{}:e,r=aa(t,ia),o=n.recentSearchesTitle,i=o===void 0?"Recent":o,a=n.noRecentSearchesText,u=a===void 0?"No recent searches":a,c=n.saveRecentSearchButtonTitle,s=c===void 0?"Save this search":c,l=n.removeRecentSearchButtonTitle,m=l===void 0?"Remove this search from history":l,p=n.favoriteSearchesTitle,v=p===void 0?"Favorite":p,d=n.removeFavoriteSearchButtonTitle,h=d===void 0?"Remove this search from favorites":d;return r.state.status==="idle"&&r.hasCollections===!1?r.disableUserPersonalization?null:f.createElement("div",{className:"DocSearch-StartScreen"},f.createElement("p",{className:"DocSearch-Help"},u)):r.hasCollections===!1?null:f.createElement("div",{className:"DocSearch-Dropdown-Container"},f.createElement(zt,ht({},r,{title:i,collection:r.state.collections[0],renderIcon:function(){return f.createElement("div",{className:"DocSearch-Hit-icon"},f.createElement(Hi,null))},renderAction:function(y){var b=y.item,_=y.runFavoriteTransition,S=y.runDeleteTransition;return f.createElement(f.Fragment,null,f.createElement("div",{className:"DocSearch-Hit-action"},f.createElement("button",{className:"DocSearch-Hit-action-button",title:s,type:"submit",onClick:function(O){O.preventDefault(),O.stopPropagation(),_(function(){r.favoriteSearches.add(b),r.recentSearches.remove(b),r.refresh()})}},f.createElement(Gn,null))),f.createElement("div",{className:"DocSearch-Hit-action"},f.createElement("button",{className:"DocSearch-Hit-action-button",title:m,type:"submit",onClick:function(O){O.preventDefault(),O.stopPropagation(),S(function(){r.recentSearches.remove(b),r.refresh()})}},f.createElement(Kt,null))))}})),f.createElement(zt,ht({},r,{title:v,collection:r.state.collections[1],renderIcon:function(){return f.createElement("div",{className:"DocSearch-Hit-icon"},f.createElement(Gn,null))},renderAction:function(y){var b=y.item,_=y.runDeleteTransition;return f.createElement("div",{className:"DocSearch-Hit-action"},f.createElement("button",{className:"DocSearch-Hit-action-button",title:h,type:"submit",onClick:function(S){S.preventDefault(),S.stopPropagation(),_(function(){r.favoriteSearches.remove(b),r.refresh()})}},f.createElement(Kt,null)))}})))}var ua=["translations"];function yt(){return yt=Object.assign||function(t){for(var e=1;e=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}var sa=f.memo(function(t){var e=t.translations,n=e===void 0?{}:e,r=la(t,ua);if(r.state.status==="error")return f.createElement($i,{translations:n==null?void 0:n.errorScreen});var o=r.state.collections.some(function(i){return i.items.length>0});return r.state.query?o===!1?f.createElement(Yi,yt({},r,{translations:n==null?void 0:n.noResultsScreen})):f.createElement(oa,r):f.createElement(ca,yt({},r,{hasCollections:o,translations:n==null?void 0:n.startScreen}))},function(t,e){return e.state.status==="loading"||e.state.status==="stalled"}),fa=["translations"];function gt(){return gt=Object.assign||function(t){for(var e=1;e=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}function pa(t){var e=t.translations,n=e===void 0?{}:e,r=ma(t,fa),o=n.resetButtonTitle,i=o===void 0?"Clear the query":o,a=n.resetButtonAriaLabel,u=a===void 0?"Clear the query":a,c=n.cancelButtonText,s=c===void 0?"Cancel":c,l=n.cancelButtonAriaLabel,m=l===void 0?"Cancel":l,p=r.getFormProps({inputElement:r.inputRef.current}).onReset;return f.useEffect(function(){r.autoFocus&&r.inputRef.current&&r.inputRef.current.focus()},[r.autoFocus,r.inputRef]),f.useEffect(function(){r.isFromSelection&&r.inputRef.current&&r.inputRef.current.select()},[r.isFromSelection,r.inputRef]),f.createElement(f.Fragment,null,f.createElement("form",{className:"DocSearch-Form",onSubmit:function(v){v.preventDefault()},onReset:p},f.createElement("label",gt({className:"DocSearch-MagnifierLabel"},r.getLabelProps()),f.createElement(Hr,null)),f.createElement("div",{className:"DocSearch-LoadingIndicator"},f.createElement(Mi,null)),f.createElement("input",gt({className:"DocSearch-Input",ref:r.inputRef},r.getInputProps({inputElement:r.inputRef.current,autoFocus:r.autoFocus,maxLength:64}))),f.createElement("button",{type:"reset",title:i,className:"DocSearch-Reset","aria-label":u,hidden:!r.state.query},f.createElement(Kt,null))),f.createElement("button",{className:"DocSearch-Cancel",type:"reset","aria-label":m,onClick:r.onClose},s))}var va=["_highlightResult","_snippetResult"];function da(t,e){if(t==null)return{};var n,r,o=function(a,u){if(a==null)return{};var c,s,l={},m=Object.keys(a);for(s=0;s=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}function ha(t){return function(){var e="__TEST_KEY__";try{return localStorage.setItem(e,""),localStorage.removeItem(e),!0}catch{return!1}}()===!1?{setItem:function(){},getItem:function(){return[]}}:{setItem:function(e){return window.localStorage.setItem(t,JSON.stringify(e))},getItem:function(){var e=window.localStorage.getItem(t);return e?JSON.parse(e):[]}}}function ar(t){var e=t.key,n=t.limit,r=n===void 0?5:n,o=ha(e),i=o.getItem().slice(0,r);return{add:function(a){var u=a,c=(u._highlightResult,u._snippetResult,da(u,va)),s=i.findIndex(function(l){return l.objectID===c.objectID});s>-1&&i.splice(s,1),i.unshift(c),i=i.slice(0,r),o.setItem(i)},remove:function(a){i=i.filter(function(u){return u.objectID!==a.objectID}),o.setItem(i)},getAll:function(){return i}}}var ya=["facetName","facetQuery"];function ga(t){var e,n="algoliasearch-client-js-".concat(t.key),r=function(){return e===void 0&&(e=t.localStorage||window.localStorage),e},o=function(){return JSON.parse(r().getItem(n)||"{}")};return{get:function(i,a){var u=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{miss:function(){return Promise.resolve()}};return Promise.resolve().then(function(){var c=JSON.stringify(i),s=o()[c];return Promise.all([s||a(),s!==void 0])}).then(function(c){var s=st(c,2),l=s[0],m=s[1];return Promise.all([l,m||u.miss(l)])}).then(function(c){return st(c,1)[0]})},set:function(i,a){return Promise.resolve().then(function(){var u=o();return u[JSON.stringify(i)]=a,r().setItem(n,JSON.stringify(u)),a})},delete:function(i){return Promise.resolve().then(function(){var a=o();delete a[JSON.stringify(i)],r().setItem(n,JSON.stringify(a))})},clear:function(){return Promise.resolve().then(function(){r().removeItem(n)})}}}function je(t){var e=ft(t.caches),n=e.shift();return n===void 0?{get:function(r,o){var i=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{miss:function(){return Promise.resolve()}};return o().then(function(a){return Promise.all([a,i.miss(a)])}).then(function(a){return st(a,1)[0]})},set:function(r,o){return Promise.resolve(o)},delete:function(r){return Promise.resolve()},clear:function(){return Promise.resolve()}}:{get:function(r,o){var i=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{miss:function(){return Promise.resolve()}};return n.get(r,o,i).catch(function(){return je({caches:e}).get(r,o,i)})},set:function(r,o){return n.set(r,o).catch(function(){return je({caches:e}).set(r,o)})},delete:function(r){return n.delete(r).catch(function(){return je({caches:e}).delete(r)})},clear:function(){return n.clear().catch(function(){return je({caches:e}).clear()})}}}function Tt(){var t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{serializable:!0},e={};return{get:function(n,r){var o=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{miss:function(){return Promise.resolve()}},i=JSON.stringify(n);if(i in e)return Promise.resolve(t.serializable?JSON.parse(e[i]):e[i]);var a=r(),u=o&&o.miss||function(){return Promise.resolve()};return a.then(function(c){return u(c)}).then(function(){return a})},set:function(n,r){return e[JSON.stringify(n)]=t.serializable?JSON.stringify(r):r,Promise.resolve(r)},delete:function(n){return delete e[JSON.stringify(n)],Promise.resolve()},clear:function(){return e={},Promise.resolve()}}}function ba(t){for(var e=t.length-1;e>0;e--){var n=Math.floor(Math.random()*(e+1)),r=t[e];t[e]=t[n],t[n]=r}return t}function Jr(t,e){return e&&Object.keys(e).forEach(function(n){t[n]=e[n](t)}),t}function bt(t){for(var e=arguments.length,n=new Array(e>1?e-1:0),r=1;r0?r:void 0,timeout:n.timeout||e,headers:n.headers||{},queryParameters:n.queryParameters||{},cacheable:n.cacheable}}var me={Read:1,Write:2,Any:3},Qr=1,_a=2,Zr=3;function Yr(t){var e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:Qr;return I(I({},t),{},{status:e,lastUpdate:Date.now()})}function Gr(t){return typeof t=="string"?{protocol:"https",url:t,accept:me.Any}:{protocol:t.protocol||"https",url:t.url,accept:t.accept||me.Any}}var ur="GET",_t="POST";function Oa(t,e){return Promise.all(e.map(function(n){return t.get(n,function(){return Promise.resolve(Yr(n))})})).then(function(n){var r=n.filter(function(a){return function(u){return u.status===Qr||Date.now()-u.lastUpdate>12e4}(a)}),o=n.filter(function(a){return function(u){return u.status===Zr&&Date.now()-u.lastUpdate<=12e4}(a)}),i=[].concat(ft(r),ft(o));return{getTimeout:function(a,u){return(o.length===0&&a===0?1:o.length+3+a)*u},statelessHosts:i.length>0?i.map(function(a){return Gr(a)}):e}})}function lr(t,e,n,r){var o=[],i=function(p,v){if(!(p.method===ur||p.data===void 0&&v.data===void 0)){var d=Array.isArray(p.data)?p.data:I(I({},p.data),v.data);return JSON.stringify(d)}}(n,r),a=function(p,v){var d=I(I({},p.headers),v.headers),h={};return Object.keys(d).forEach(function(y){var b=d[y];h[y.toLowerCase()]=b}),h}(t,r),u=n.method,c=n.method!==ur?{}:I(I({},n.data),r.data),s=I(I(I({"x-algolia-agent":t.userAgent.value},t.queryParameters),c),r.queryParameters),l=0,m=function p(v,d){var h=v.pop();if(h===void 0)throw{name:"RetryError",message:"Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.",transporterStackTrace:sr(o)};var y={data:i,headers:a,method:u,url:wa(h,n.path,s),connectTimeout:d(l,t.timeouts.connect),responseTimeout:d(l,r.timeout)},b=function(S){var O={request:y,response:S,host:h,triesLeft:v.length};return o.push(O),O},_={onSucess:function(S){return function(O){try{return JSON.parse(O.content)}catch(g){throw function(P,C){return{name:"DeserializationError",message:P,response:C}}(g.message,O)}}(S)},onRetry:function(S){var O=b(S);return S.isTimedOut&&l++,Promise.all([t.logger.info("Retryable failure",eo(O)),t.hostsCache.set(h,Yr(h,S.isTimedOut?Zr:_a))]).then(function(){return p(v,d)})},onFail:function(S){throw b(S),function(O,g){var P=O.content,C=O.status,L=P;try{L=JSON.parse(P).message}catch{}return function(x,k,N){return{name:"ApiError",message:x,status:k,transporterStackTrace:N}}(L,C,g)}(S,sr(o))}};return t.requester.send(y).then(function(S){return function(O,g){return function(P){var C=P.status;return P.isTimedOut||function(L){var x=L.isTimedOut,k=L.status;return!x&&~~k==0}(P)||~~(C/100)!=2&&~~(C/100)!=4}(O)?g.onRetry(O):~~(O.status/100)==2?g.onSucess(O):g.onFail(O)}(S,_)})};return Oa(t.hostsCache,e).then(function(p){return m(ft(p.statelessHosts).reverse(),p.getTimeout)})}function Sa(t){var e={value:"Algolia for JavaScript (".concat(t,")"),add:function(n){var r="; ".concat(n.segment).concat(n.version!==void 0?" (".concat(n.version,")"):"");return e.value.indexOf(r)===-1&&(e.value="".concat(e.value).concat(r)),e}};return e}function wa(t,e,n){var r=Xr(n),o="".concat(t.protocol,"://").concat(t.url,"/").concat(e.charAt(0)==="/"?e.substr(1):e);return r.length&&(o+="?".concat(r)),o}function Xr(t){return Object.keys(t).map(function(e){return bt("%s=%s",e,(n=t[e],Object.prototype.toString.call(n)==="[object Object]"||Object.prototype.toString.call(n)==="[object Array]"?JSON.stringify(t[e]):t[e]));var n}).join("&")}function sr(t){return t.map(function(e){return eo(e)})}function eo(t){var e=t.request.headers["x-algolia-api-key"]?{"x-algolia-api-key":"*****"}:{};return I(I({},t),{},{request:I(I({},t.request),{},{headers:I(I({},t.request.headers),e)})})}var ja=function(t){var e=t.appId,n=function(i,a,u){var c={"x-algolia-api-key":u,"x-algolia-application-id":a};return{headers:function(){return i===lt.WithinHeaders?c:{}},queryParameters:function(){return i===lt.WithinQueryParameters?c:{}}}}(t.authMode!==void 0?t.authMode:lt.WithinHeaders,e,t.apiKey),r=function(i){var a=i.hostsCache,u=i.logger,c=i.requester,s=i.requestsCache,l=i.responsesCache,m=i.timeouts,p=i.userAgent,v=i.hosts,d=i.queryParameters,h={hostsCache:a,logger:u,requester:c,requestsCache:s,responsesCache:l,timeouts:m,userAgent:p,headers:i.headers,queryParameters:d,hosts:v.map(function(y){return Gr(y)}),read:function(y,b){var _=cr(b,h.timeouts.read),S=function(){return lr(h,h.hosts.filter(function(g){return(g.accept&me.Read)!=0}),y,_)};if((_.cacheable!==void 0?_.cacheable:y.cacheable)!==!0)return S();var O={request:y,mappedRequestOptions:_,transporter:{queryParameters:h.queryParameters,headers:h.headers}};return h.responsesCache.get(O,function(){return h.requestsCache.get(O,function(){return h.requestsCache.set(O,S()).then(function(g){return Promise.all([h.requestsCache.delete(O),g])},function(g){return Promise.all([h.requestsCache.delete(O),Promise.reject(g)])}).then(function(g){var P=st(g,2);return P[0],P[1]})})},{miss:function(g){return h.responsesCache.set(O,g)}})},write:function(y,b){return lr(h,h.hosts.filter(function(_){return(_.accept&me.Write)!=0}),y,cr(b,h.timeouts.write))}};return h}(I(I({hosts:[{url:"".concat(e,"-dsn.algolia.net"),accept:me.Read},{url:"".concat(e,".algolia.net"),accept:me.Write}].concat(ba([{url:"".concat(e,"-1.algolianet.com")},{url:"".concat(e,"-2.algolianet.com")},{url:"".concat(e,"-3.algolianet.com")}]))},t),{},{headers:I(I(I({},n.headers()),{"content-type":"application/x-www-form-urlencoded"}),t.headers),queryParameters:I(I({},n.queryParameters()),t.queryParameters)})),o={transporter:r,appId:e,addAlgoliaAgent:function(i,a){r.userAgent.add({segment:i,version:a})},clearCache:function(){return Promise.all([r.requestsCache.clear(),r.responsesCache.clear()]).then(function(){})}};return Jr(o,t.methods)},to=function(t){return function(e){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},r={transporter:t.transporter,appId:t.appId,indexName:e};return Jr(r,n.methods)}},fr=function(t){return function(e,n){var r=e.map(function(o){return I(I({},o),{},{params:Xr(o.params||{})})});return t.transporter.read({method:_t,path:"1/indexes/*/queries",data:{requests:r},cacheable:!0},n)}},mr=function(t){return function(e,n){return Promise.all(e.map(function(r){var o=r.params,i=o.facetName,a=o.facetQuery,u=Oo(o,ya);return to(t)(r.indexName,{methods:{searchForFacetValues:no}}).searchForFacetValues(i,a,I(I({},n),u))}))}},Ea=function(t){return function(e,n,r){return t.transporter.read({method:_t,path:bt("1/answers/%s/prediction",t.indexName),data:{query:e,queryLanguages:n},cacheable:!0},r)}},Pa=function(t){return function(e,n){return t.transporter.read({method:_t,path:bt("1/indexes/%s/query",t.indexName),data:{query:e},cacheable:!0},n)}},no=function(t){return function(e,n,r){return t.transporter.read({method:_t,path:bt("1/indexes/%s/facets/%s/query",t.indexName,e),data:{facetQuery:n},cacheable:!0},r)}},Ia=1,Da=2,ka=3;function ro(t,e,n){var r,o={appId:t,apiKey:e,timeouts:{connect:1,read:2,write:30},requester:{send:function(i){return new Promise(function(a){var u=new XMLHttpRequest;u.open(i.method,i.url,!0),Object.keys(i.headers).forEach(function(m){return u.setRequestHeader(m,i.headers[m])});var c,s=function(m,p){return setTimeout(function(){u.abort(),a({status:0,content:p,isTimedOut:!0})},1e3*m)},l=s(i.connectTimeout,"Connection timeout");u.onreadystatechange=function(){u.readyState>u.OPENED&&c===void 0&&(clearTimeout(l),c=s(i.responseTimeout,"Socket timeout"))},u.onerror=function(){u.status===0&&(clearTimeout(l),clearTimeout(c),a({content:u.responseText||"Network request failed",status:u.status,isTimedOut:!1}))},u.onload=function(){clearTimeout(l),clearTimeout(c),a({content:u.responseText,status:u.status,isTimedOut:!1})},u.send(i.data)})}},logger:(r=ka,{debug:function(i,a){return Ia>=r&&console.debug(i,a),Promise.resolve()},info:function(i,a){return Da>=r&&console.info(i,a),Promise.resolve()},error:function(i,a){return console.error(i,a),Promise.resolve()}}),responsesCache:Tt(),requestsCache:Tt({serializable:!1}),hostsCache:je({caches:[ga({key:"".concat("4.8.5","-").concat(t)}),Tt()]}),userAgent:Sa("4.8.5").add({segment:"Browser",version:"lite"}),authMode:lt.WithinQueryParameters};return ja(I(I(I({},o),n),{},{methods:{search:fr,searchForFacetValues:mr,multipleQueries:fr,multipleSearchForFacetValues:mr,initIndex:function(i){return function(a){return to(i)(a,{methods:{search:Pa,searchForFacetValues:no,findAnswers:Ea}})}}}}))}ro.version="4.8.5";var Aa=["footer","searchBox"];function Fe(){return Fe=Object.assign||function(t){for(var e=1;et.length)&&(e=t.length);for(var n=0,r=new Array(e);n=0||(l[c]=a[c]);return l}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}function Ta(t){var e=t.appId,n=t.apiKey,r=t.indexName,o=t.placeholder,i=o===void 0?"Search docs":o,a=t.searchParameters,u=t.maxResultsPerGroup,c=t.onClose,s=c===void 0?na:c,l=t.transformItems,m=l===void 0?ir:l,p=t.hitComponent,v=p===void 0?Li:p,d=t.resultsFooterComponent,h=d===void 0?function(){return null}:d,y=t.navigator,b=t.initialScrollY,_=b===void 0?0:b,S=t.transformSearchClient,O=S===void 0?ir:S,g=t.disableUserPersonalization,P=g!==void 0&&g,C=t.initialQuery,L=C===void 0?"":C,x=t.translations,k=x===void 0?{}:x,N=t.getMissingResultsUrl,U=t.insights,F=U!==void 0&&U,M=k.footer,Ot=k.searchBox,St=Na(k,Aa),$e=xa(f.useState({query:"",collections:[],completion:null,context:{},isOpen:!1,activeItemId:null,status:"idle"}),2),B=$e[0],oo=$e[1],Gt=f.useRef(null),wt=f.useRef(null),Xt=f.useRef(null),Je=f.useRef(null),he=f.useRef(null),Q=f.useRef(10),en=f.useRef(typeof window<"u"?window.getSelection().toString().slice(0,64):"").current,ee=f.useRef(L||en).current,tn=function(j,D,T){return f.useMemo(function(){var H=ro(j,D);return H.addAlgoliaAgent("docsearch","3.5.1"),/docsearch.js \(.*\)/.test(H.transporter.userAgent.value)===!1&&H.addAlgoliaAgent("docsearch-react","3.5.1"),T(H)},[j,D,T])}(e,n,O),oe=f.useRef(ar({key:"__DOCSEARCH_FAVORITE_SEARCHES__".concat(r),limit:10})).current,ye=f.useRef(ar({key:"__DOCSEARCH_RECENT_SEARCHES__".concat(r),limit:oe.getAll().length===0?7:4})).current,ge=f.useCallback(function(j){if(!P){var D=j.type==="content"?j.__docsearch_parent:j;D&&oe.getAll().findIndex(function(T){return T.objectID===D.objectID})===-1&&ye.add(D)}},[oe,ye,P]),io=f.useCallback(function(j){if(B.context.algoliaInsightsPlugin&&j.__autocomplete_id){var D=j,T={eventName:"Item Selected",index:D.__autocomplete_indexName,items:[D],positions:[j.__autocomplete_id],queryID:D.__autocomplete_queryID};B.context.algoliaInsightsPlugin.insights.clickedObjectIDsAfterSearch(T)}},[B.context.algoliaInsightsPlugin]),be=f.useMemo(function(){return Ti({id:"docsearch",defaultActiveItemId:0,placeholder:i,openOnFocus:!0,initialState:{query:ee,context:{searchSuggestions:[]}},insights:F,navigator:y,onStateChange:function(j){oo(j.state)},getSources:function(j){var D=j.query,T=j.state,H=j.setContext,Z=j.setStatus;if(!D)return P?[]:[{sourceId:"recentSearches",onSelect:function(A){var K=A.item,ie=A.event;ge(K),it(ie)||s()},getItemUrl:function(A){return A.item.url},getItems:function(){return ye.getAll()}},{sourceId:"favoriteSearches",onSelect:function(A){var K=A.item,ie=A.event;ge(K),it(ie)||s()},getItemUrl:function(A){return A.item.url},getItems:function(){return oe.getAll()}}];var Y=!!F;return tn.search([{query:D,indexName:r,params:Rt({attributesToRetrieve:["hierarchy.lvl0","hierarchy.lvl1","hierarchy.lvl2","hierarchy.lvl3","hierarchy.lvl4","hierarchy.lvl5","hierarchy.lvl6","content","type","url"],attributesToSnippet:["hierarchy.lvl1:".concat(Q.current),"hierarchy.lvl2:".concat(Q.current),"hierarchy.lvl3:".concat(Q.current),"hierarchy.lvl4:".concat(Q.current),"hierarchy.lvl5:".concat(Q.current),"hierarchy.lvl6:".concat(Q.current),"content:".concat(Q.current)],snippetEllipsisText:"…",highlightPreTag:"",highlightPostTag:"",hitsPerPage:20,clickAnalytics:Y},a)}]).catch(function(A){throw A.name==="RetryError"&&Z("error"),A}).then(function(A){var K=A.results,ie=K[0],uo=ie.hits,lo=ie.nbHits,jt=or(uo,function(Et){return $r(Et)},u);T.context.searchSuggestions.length0&&(nn(),he.current&&he.current.focus())},[ee,nn]),f.useEffect(function(){function j(){if(wt.current){var D=.01*window.innerHeight;wt.current.style.setProperty("--docsearch-vh","".concat(D,"px"))}}return j(),window.addEventListener("resize",j),function(){window.removeEventListener("resize",j)}},[]),f.createElement("div",Fe({ref:Gt},co({"aria-expanded":!0}),{className:["DocSearch","DocSearch-Container",B.status==="stalled"&&"DocSearch-Container--Stalled",B.status==="error"&&"DocSearch-Container--Errored"].filter(Boolean).join(" "),role:"button",tabIndex:0,onMouseDown:function(j){j.target===j.currentTarget&&s()}}),f.createElement("div",{className:"DocSearch-Modal",ref:wt},f.createElement("header",{className:"DocSearch-SearchBar",ref:Xt},f.createElement(pa,Fe({},be,{state:B,autoFocus:ee.length===0,inputRef:he,isFromSelection:!!ee&&ee===en,translations:Ot,onClose:s}))),f.createElement("div",{className:"DocSearch-Dropdown",ref:Je},f.createElement(sa,Fe({},be,{indexName:r,state:B,hitComponent:v,resultsFooterComponent:h,disableUserPersonalization:P,recentSearches:ye,favoriteSearches:oe,inputRef:he,translations:St,getMissingResultsUrl:N,onItemClick:function(j,D){io(j),ge(j),it(D)||s()}}))),f.createElement("footer",{className:"DocSearch-Footer"},f.createElement(qi,{translations:M}))))}function Jt(){return Jt=Object.assign||function(t){for(var e=1;et.length)&&(e=t.length);for(var n=0,r=new Array(e);n1&&arguments[1]!==void 0?arguments[1]:window;return typeof e=="string"?n.document.querySelector(e):e}(t.container,t.environment))}const La={id:"docsearch"},Ua=fo({__name:"VPAlgoliaSearchBox",props:{algolia:{}},setup(t){const e=t,n=mo(),r=po(),{site:o,localeIndex:i,lang:a}=bo();vo(u),ho(i,u);function u(){var v,d;const l={...e.algolia,...(v=e.algolia.locales)==null?void 0:v[i.value]},m=((d=l.searchParameters)==null?void 0:d.facetFilters)??[],p=[...(Array.isArray(m)?m:[m]).filter(h=>!h.startsWith("lang:")),`lang:${a.value}`];c({...l,searchParameters:{...l.searchParameters,facetFilters:p}})}function c(l){const m=Object.assign({},l,{container:"#docsearch",navigator:{navigate({itemUrl:p}){const{pathname:v}=new URL(window.location.origin+p);r.path===v?window.location.assign(window.location.origin+p):n.go(p)}},transformItems(p){return p.map(v=>Object.assign({},v,{url:s(v.url)}))},hitComponent({hit:p,children:v}){return{__v:null,type:"a",ref:void 0,constructor:void 0,key:void 0,props:{href:p.url,children:v}}}});qa(m)}function s(l){const{pathname:m,hash:p}=new URL(l);return m.replace(/\.html$/,o.value.cleanUrls?"":".html")+p}return(l,m)=>(yo(),go("div",La))}});export{Ua as default}; diff --git a/assets/chunks/framework.0d8bea05.js b/assets/chunks/framework.0d8bea05.js new file mode 100644 index 00000000..9b344888 --- /dev/null +++ b/assets/chunks/framework.0d8bea05.js @@ -0,0 +1,9 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +function mn(t,e){const n=Object.create(null),o=t.split(",");for(let r=0;r!!n[r.toLowerCase()]:r=>!!n[r]}const Z={},ae=[],Xt=()=>{},$s=()=>!1,Ms=/^on[^a-z]/,Ue=t=>Ms.test(t),Qn=t=>t.startsWith("onUpdate:"),et=Object.assign,Zn=(t,e)=>{const n=t.indexOf(e);n>-1&&t.splice(n,1)},Is=Object.prototype.hasOwnProperty,G=(t,e)=>Is.call(t,e),U=Array.isArray,ue=t=>gn(t)==="[object Map]",_r=t=>gn(t)==="[object Set]",D=t=>typeof t=="function",st=t=>typeof t=="string",to=t=>typeof t=="symbol",Q=t=>t!==null&&typeof t=="object",br=t=>Q(t)&&D(t.then)&&D(t.catch),Cr=Object.prototype.toString,gn=t=>Cr.call(t),wr=t=>gn(t)==="[object Object]",eo=t=>st(t)&&t!=="NaN"&&t[0]!=="-"&&""+parseInt(t,10)===t,Se=mn(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),vn=t=>{const e=Object.create(null);return n=>e[n]||(e[n]=t(n))},Ns=/-(\w)/g,Mt=vn(t=>t.replace(Ns,(e,n)=>n?n.toUpperCase():"")),Us=/\B([A-Z])/g,ie=vn(t=>t.replace(Us,"-$1").toLowerCase()),yn=vn(t=>t.charAt(0).toUpperCase()+t.slice(1)),Ze=vn(t=>t?`on${yn(t)}`:""),Fe=(t,e)=>!Object.is(t,e),Fn=(t,e)=>{for(let n=0;n{Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value:n})},js=t=>{const e=parseFloat(t);return isNaN(e)?t:e},Vs=t=>{const e=st(t)?Number(t):NaN;return isNaN(e)?t:e};let Eo;const Bs=mn("Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console");function no(t){if(U(t)){const e={};for(let n=0;n{if(n){const o=n.split(Ds);o.length>1&&(e[o[0].trim()]=o[1].trim())}}),e}function oo(t){let e="";if(st(t))e=t;else if(U(t))for(let n=0;nst(t)?t:t==null?"":U(t)||Q(t)&&(t.toString===Cr||!D(t.toString))?JSON.stringify(t,xr,2):String(t),xr=(t,e)=>e&&e.__v_isRef?xr(t,e.value):ue(e)?{[`Map(${e.size})`]:[...e.entries()].reduce((n,[o,r])=>(n[`${o} =>`]=r,n),{})}:_r(e)?{[`Set(${e.size})`]:[...e.values()]}:!Q(e)||U(e)||wr(e)?e:String(e);let yt;class Ks{constructor(e=!1){this.detached=e,this._active=!0,this.effects=[],this.cleanups=[],this.parent=yt,!e&&yt&&(this.index=(yt.scopes||(yt.scopes=[])).push(this)-1)}get active(){return this._active}run(e){if(this._active){const n=yt;try{return yt=this,e()}finally{yt=n}}}on(){yt=this}off(){yt=this.parent}stop(e){if(this._active){let n,o;for(n=0,o=this.effects.length;n{const e=new Set(t);return e.w=0,e.n=0,e},Sr=t=>(t.w&Gt)>0,Er=t=>(t.n&Gt)>0,Bn=new WeakMap;let xe=0,Gt=1,Rt;const se=Symbol(""),Hn=Symbol("");class so{constructor(e,n=null,o){this.fn=e,this.scheduler=n,this.active=!0,this.deps=[],this.parent=void 0,Gs(this,o)}run(){if(!this.active)return this.fn();let e=Rt,n=Jt;for(;e;){if(e===this)return;e=e.parent}try{return this.parent=Rt,Rt=this,Jt=!0,Gt=1<<++xe,xe<=30?(({deps:o})=>{if(o.length)for(let r=0;r{const{deps:r}=o;if(r.length){let s=0;for(let l=0;l{(f==="length"||f>=c)&&i.push(a)})}else switch(n!==void 0&&i.push(l.get(n)),e){case"add":U(t)?eo(n)&&i.push(l.get("length")):(i.push(l.get(se)),ue(t)&&i.push(l.get(Hn)));break;case"delete":U(t)||(i.push(l.get(se)),ue(t)&&i.push(l.get(Hn)));break;case"set":ue(t)&&i.push(l.get(se))}if(i.length===1)i[0]&&Dn(i[0]);else{const c=[];for(const a of i)a&&c.push(...a);Dn(ro(c))}}function Dn(t,e){const n=U(t)?t:[...t];for(const o of n)o.computed&&To(o);for(const o of n)o.computed||To(o)}function To(t,e){(t!==Rt||t.allowRecurse)&&(t.scheduler?t.scheduler():t.run())}const Js=mn("__proto__,__v_isRef,__isVue"),Tr=new Set(Object.getOwnPropertyNames(Symbol).filter(t=>t!=="arguments"&&t!=="caller").map(t=>Symbol[t]).filter(to)),Ys=_n(),Qs=_n(!1,!0),Zs=_n(!0),tl=_n(!0,!0),Ro=el();function el(){const t={};return["includes","indexOf","lastIndexOf"].forEach(e=>{t[e]=function(...n){const o=X(this);for(let s=0,l=this.length;s{t[e]=function(...n){he();const o=X(this)[e].apply(this,n);return me(),o}}),t}function nl(t){const e=X(this);return gt(e,0,t),e.hasOwnProperty(t)}function _n(t=!1,e=!1){return function(n,o,r){if(o==="__v_isReactive")return!t;if(o==="__v_isReadonly")return t;if(o==="__v_isShallow")return e;if(o==="__v_raw"&&r===(t?e?dl:$r:e?Pr:Or).get(n))return n;const s=U(n);if(!t){if(s&&G(Ro,o))return Reflect.get(Ro,o,r);if(o==="hasOwnProperty")return nl}const l=Reflect.get(n,o,r);return(to(o)?Tr.has(o):Js(o))?l:(t||gt(n,0,o),e?l:at(l)?s&&eo(o)?l:l.value:Q(l)?t?Mr(l):Cn(l):l)}}function Rr(t=!1){return function(e,n,o,r){let s=e[n];if(ge(s)&&at(s)&&!at(o))return!1;if(!t&&(rn(o)||ge(o)||(s=X(s),o=X(o)),!U(e)&&at(s)&&!at(o)))return s.value=o,!0;const l=U(e)&&eo(n)?Number(n)!0,deleteProperty:(t,e)=>!0},ol=et({},Lr,{get:Qs,set:Rr(!0)});et({},Fr,{get:tl});const lo=t=>t,bn=t=>Reflect.getPrototypeOf(t);function We(t,e,n=!1,o=!1){const r=X(t=t.__v_raw),s=X(e);n||(e!==s&>(r,0,e),gt(r,0,s));const{has:l}=bn(r),i=o?lo:n?ao:Oe;return l.call(r,e)?i(t.get(e)):l.call(r,s)?i(t.get(s)):void(t!==r&&t.get(e))}function qe(t,e=!1){const n=this.__v_raw,o=X(n),r=X(t);return e||(t!==r&>(o,0,t),gt(o,0,r)),t===r?n.has(t):n.has(t)||n.has(r)}function ze(t,e=!1){return t=t.__v_raw,!e&>(X(t),0,se),Reflect.get(t,"size",t)}function Lo(t){t=X(t);const e=X(this);return bn(e).has.call(e,t)||(e.add(t),jt(e,"add",t,t)),this}function Fo(t,e){e=X(e);const n=X(this),{has:o,get:r}=bn(n);let s=o.call(n,t);s||(t=X(t),s=o.call(n,t));const l=r.call(n,t);return n.set(t,e),s?Fe(e,l)&&jt(n,"set",t,e):jt(n,"add",t,e),this}function Oo(t){const e=X(this),{has:n,get:o}=bn(e);let r=n.call(e,t);r||(t=X(t),r=n.call(e,t)),o&&o.call(e,t);const s=e.delete(t);return r&&jt(e,"delete",t,void 0),s}function Po(){const t=X(this),e=t.size!==0,n=t.clear();return e&&jt(t,"clear",void 0,void 0),n}function Ke(t,e){return function(n,o){const r=this,s=r.__v_raw,l=X(s),i=e?lo:t?ao:Oe;return!t&>(l,0,se),s.forEach((c,a)=>n.call(o,i(c),i(a),r))}}function Ge(t,e,n){return function(...o){const r=this.__v_raw,s=X(r),l=ue(s),i=t==="entries"||t===Symbol.iterator&&l,c=t==="keys"&&l,a=r[t](...o),f=n?lo:e?ao:Oe;return!e&>(s,0,c?Hn:se),{next(){const{value:p,done:g}=a.next();return g?{value:p,done:g}:{value:i?[f(p[0]),f(p[1])]:f(p),done:g}},[Symbol.iterator](){return this}}}}function Dt(t){return function(...e){return t!=="delete"&&this}}function rl(){const t={get(r){return We(this,r)},get size(){return ze(this)},has:qe,add:Lo,set:Fo,delete:Oo,clear:Po,forEach:Ke(!1,!1)},e={get(r){return We(this,r,!1,!0)},get size(){return ze(this)},has:qe,add:Lo,set:Fo,delete:Oo,clear:Po,forEach:Ke(!1,!0)},n={get(r){return We(this,r,!0)},get size(){return ze(this,!0)},has(r){return qe.call(this,r,!0)},add:Dt("add"),set:Dt("set"),delete:Dt("delete"),clear:Dt("clear"),forEach:Ke(!0,!1)},o={get(r){return We(this,r,!0,!0)},get size(){return ze(this,!0)},has(r){return qe.call(this,r,!0)},add:Dt("add"),set:Dt("set"),delete:Dt("delete"),clear:Dt("clear"),forEach:Ke(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(r=>{t[r]=Ge(r,!1,!1),n[r]=Ge(r,!0,!1),e[r]=Ge(r,!1,!0),o[r]=Ge(r,!0,!0)}),[t,n,e,o]}const[sl,ll,il,cl]=rl();function io(t,e){const n=e?t?cl:il:t?ll:sl;return(o,r,s)=>r==="__v_isReactive"?!t:r==="__v_isReadonly"?t:r==="__v_raw"?o:Reflect.get(G(n,r)&&r in o?n:o,r,s)}const al={get:io(!1,!1)},ul={get:io(!1,!0)},fl={get:io(!0,!1)},Or=new WeakMap,Pr=new WeakMap,$r=new WeakMap,dl=new WeakMap;function pl(t){return t.__v_skip||!Object.isExtensible(t)?0:function(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}((e=>gn(e).slice(8,-1))(t))}function Cn(t){return ge(t)?t:co(t,!1,Lr,al,Or)}function hl(t){return co(t,!1,ol,ul,Pr)}function Mr(t){return co(t,!0,Fr,fl,$r)}function co(t,e,n,o,r){if(!Q(t)||t.__v_raw&&(!e||!t.__v_isReactive))return t;const s=r.get(t);if(s)return s;const l=pl(t);if(l===0)return t;const i=new Proxy(t,l===2?o:n);return r.set(t,i),i}function fe(t){return ge(t)?fe(t.__v_raw):!(!t||!t.__v_isReactive)}function ge(t){return!(!t||!t.__v_isReadonly)}function rn(t){return!(!t||!t.__v_isShallow)}function Ir(t){return fe(t)||ge(t)}function X(t){const e=t&&t.__v_raw;return e?X(e):t}function Ee(t){return tn(t,"__v_skip",!0),t}const Oe=t=>Q(t)?Cn(t):t,ao=t=>Q(t)?Mr(t):t;function Nr(t){Jt&&Rt&&Ar((t=X(t)).dep||(t.dep=ro()))}function Ur(t,e){const n=(t=X(t)).dep;n&&Dn(n)}function at(t){return!(!t||t.__v_isRef!==!0)}function ke(t){return jr(t,!1)}function ml(t){return jr(t,!0)}function jr(t,e){return at(t)?t:new gl(t,e)}class gl{constructor(e,n){this.__v_isShallow=n,this.dep=void 0,this.__v_isRef=!0,this._rawValue=n?e:X(e),this._value=n?e:Oe(e)}get value(){return Nr(this),this._value}set value(e){const n=this.__v_isShallow||rn(e)||ge(e);e=n?e:X(e),Fe(e,this._rawValue)&&(this._rawValue=e,this._value=n?e:Oe(e),Ur(this))}}function vl(t){return at(t)?t.value:t}const yl={get:(t,e,n)=>vl(Reflect.get(t,e,n)),set:(t,e,n,o)=>{const r=t[e];return at(r)&&!at(n)?(r.value=n,!0):Reflect.set(t,e,n,o)}};function Vr(t){return fe(t)?t:new Proxy(t,yl)}class _l{constructor(e,n,o,r){this._setter=n,this.dep=void 0,this.__v_isRef=!0,this.__v_isReadonly=!1,this._dirty=!0,this.effect=new so(e,()=>{this._dirty||(this._dirty=!0,Ur(this))}),this.effect.computed=this,this.effect.active=this._cacheable=!r,this.__v_isReadonly=o}get value(){const e=X(this);return Nr(e),!e._dirty&&e._cacheable||(e._dirty=!1,e._value=e.effect.run()),e._value}set value(e){this._setter(e)}}function Yt(t,e,n,o){let r;try{r=o?t(...o):t()}catch(s){je(s,e,n)}return r}function Ft(t,e,n,o){if(D(t)){const s=Yt(t,e,n,o);return s&&br(s)&&s.catch(l=>{je(l,e,n)}),s}const r=[];for(let s=0;s>>1;$e(ct[r])$e(n)-$e(o)),ee=0;eet.id==null?1/0:t.id,Cl=(t,e)=>{const n=$e(t)-$e(e);if(n===0){if(t.pre&&!e.pre)return-1;if(e.pre&&!t.pre)return 1}return n};function Wr(t){Wn=!1,Pe=!0,ct.sort(Cl);try{for($t=0;$tst(E)?E.trim():E)),p&&(r=n.map(js))}let i,c=o[i=Ze(e)]||o[i=Ze(Mt(e))];!c&&s&&(c=o[i=Ze(ie(e))]),c&&Ft(c,t,6,r);const a=o[i+"Once"];if(a){if(t.emitted){if(t.emitted[i])return}else t.emitted={};t.emitted[i]=!0,Ft(a,t,6,r)}}function qr(t,e,n=!1){const o=e.emitsCache,r=o.get(t);if(r!==void 0)return r;const s=t.emits;let l={},i=!1;if(!D(t)){const c=a=>{const f=qr(a,e,!0);f&&(i=!0,et(l,f))};!n&&e.mixins.length&&e.mixins.forEach(c),t.extends&&c(t.extends),t.mixins&&t.mixins.forEach(c)}return s||i?(U(s)?s.forEach(c=>l[c]=null):et(l,s),Q(t)&&o.set(t,l),l):(Q(t)&&o.set(t,null),null)}function ln(t,e){return!(!t||!Ue(e))&&(e=e.slice(2).replace(/Once$/,""),G(t,e[0].toLowerCase()+e.slice(1))||G(t,ie(e))||G(t,e))}let ut=null,xn=null;function cn(t){const e=ut;return ut=t,xn=t&&t.type.__scopeId||null,e}function Di(t){xn=t}function Wi(){xn=null}function xl(t,e=ut,n){if(!e||t._n)return t;const o=(...r)=>{o._d&&Xo(-1);const s=cn(e);let l;try{l=t(...r)}finally{cn(s),o._d&&Xo(1)}return l};return o._n=!0,o._c=!0,o._d=!0,o}function On(t){const{type:e,vnode:n,proxy:o,withProxy:r,props:s,propsOptions:[l],slots:i,attrs:c,emit:a,render:f,renderCache:p,data:g,setupState:E,ctx:L,inheritAttrs:$}=t;let m,C;const N=cn(t);try{if(4&n.shapeFlag){const B=r||o;m=Tt(f.call(B,B,p,s,E,g,L)),C=c}else{const B=e;m=Tt(B(s,B.length>1?{attrs:c,slots:i,emit:a}:null)),C=e.props?c:Sl(c)}}catch(B){Le.length=0,je(B,t,1),m=ot(_t)}let q=m;if(C&&$!==!1){const B=Object.keys(C),{shapeFlag:K}=q;B.length&&7&K&&(l&&B.some(Qn)&&(C=El(C,l)),q=Qt(q,C))}return n.dirs&&(q=Qt(q),q.dirs=q.dirs?q.dirs.concat(n.dirs):n.dirs),n.transition&&(q.transition=n.transition),m=q,cn(N),m}const Sl=t=>{let e;for(const n in t)(n==="class"||n==="style"||Ue(n))&&((e||(e={}))[n]=t[n]);return e},El=(t,e)=>{const n={};for(const o in t)Qn(o)&&o.slice(9)in e||(n[o]=t[o]);return n};function Mo(t,e,n){const o=Object.keys(e);if(o.length!==Object.keys(t).length)return!0;for(let r=0;rt.__isSuspense;function zr(t,e){e&&e.pendingBranch?U(t)?e.effects.push(...t):e.effects.push(t):bl(t)}function Tl(t,e){return Sn(t,null,e)}function qi(t,e){return Sn(t,null,{flush:"post"})}const Xe={};function en(t,e,n){return Sn(t,e,n)}function Sn(t,e,{immediate:n,deep:o,flush:r}=Z){var s;const l=Xs()===((s=it)==null?void 0:s.scope)?it:null;let i,c,a=!1,f=!1;if(at(t)?(i=()=>t.value,a=rn(t)):fe(t)?(i=()=>t,o=!0):U(t)?(f=!0,a=t.some(m=>fe(m)||rn(m)),i=()=>t.map(m=>at(m)?m.value:fe(m)?ce(m):D(m)?Yt(m,l,2):void 0)):i=D(t)?e?()=>Yt(t,l,2):()=>{if(!l||!l.isUnmounted)return c&&c(),Ft(t,l,3,[p])}:Xt,e&&o){const m=i;i=()=>ce(m())}let p=m=>{c=$.onStop=()=>{Yt(m,l,4)}},g=f?new Array(t.length).fill(Xe):Xe;const E=()=>{if($.active)if(e){const m=$.run();(o||a||(f?m.some((C,N)=>Fe(C,g[N])):Fe(m,g)))&&(c&&c(),Ft(e,l,3,[m,g===Xe?void 0:f&&g[0]===Xe?[]:g,p]),g=m)}else $.run()};let L;E.allowRecurse=!!e,r==="sync"?L=E:r==="post"?L=()=>mt(E,l&&l.suspense):(E.pre=!0,l&&(E.id=l.uid),L=()=>wn(E));const $=new so(i,L);return e?n?E():g=$.run():r==="post"?mt($.run.bind($),l&&l.suspense):$.run(),()=>{$.stop(),l&&l.scope&&Zn(l.scope.effects,$)}}function Rl(t,e,n){const o=this.proxy,r=st(t)?t.includes(".")?Kr(o,t):()=>o[t]:t.bind(o,o);let s;D(e)?s=e:(s=e.handler,n=e);const l=it;ye(this);const i=Sn(r,s.bind(o),n);return l?ye(l):le(),i}function Kr(t,e){const n=e.split(".");return()=>{let o=t;for(let r=0;r{ce(n,e)});else if(wr(t))for(const n in t)ce(t[n],e);return t}function Pt(t,e,n,o){const r=t.dirs,s=e&&e.dirs;for(let l=0;l{t.isMounted=!0}),Zr(()=>{t.isUnmounting=!0}),t}const St=[Function,Array],Xr={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:St,onEnter:St,onAfterEnter:St,onEnterCancelled:St,onBeforeLeave:St,onLeave:St,onAfterLeave:St,onLeaveCancelled:St,onBeforeAppear:St,onAppear:St,onAfterAppear:St,onAppearCancelled:St},Ll={name:"BaseTransition",props:Xr,setup(t,{slots:e}){const n=yo(),o=Gr();let r;return()=>{const s=e.default&&fo(e.default(),!0);if(!s||!s.length)return;let l=s[0];if(s.length>1){for(const $ of s)if($.type!==_t){l=$;break}}const i=X(t),{mode:c}=i;if(o.isLeaving)return Pn(l);const a=Io(l);if(!a)return Pn(l);const f=Me(a,i,o,n);Ie(a,f);const p=n.subTree,g=p&&Io(p);let E=!1;const{getTransitionKey:L}=a.type;if(L){const $=L();r===void 0?r=$:$!==r&&(r=$,E=!0)}if(g&&g.type!==_t&&(!ne(a,g)||E)){const $=Me(g,i,o,n);if(Ie(g,$),c==="out-in")return o.isLeaving=!0,$.afterLeave=()=>{o.isLeaving=!1,n.update.active!==!1&&n.update()},Pn(l);c==="in-out"&&a.type!==_t&&($.delayLeave=(m,C,N)=>{Jr(o,g)[String(g.key)]=g,m._leaveCb=()=>{C(),m._leaveCb=void 0,delete f.delayedLeave},f.delayedLeave=N})}return l}}};function Jr(t,e){const{leavingVNodes:n}=t;let o=n.get(e.type);return o||(o=Object.create(null),n.set(e.type,o)),o}function Me(t,e,n,o){const{appear:r,mode:s,persisted:l=!1,onBeforeEnter:i,onEnter:c,onAfterEnter:a,onEnterCancelled:f,onBeforeLeave:p,onLeave:g,onAfterLeave:E,onLeaveCancelled:L,onBeforeAppear:$,onAppear:m,onAfterAppear:C,onAppearCancelled:N}=e,q=String(t.key),B=Jr(n,t),K=(M,W)=>{M&&Ft(M,o,9,W)},J=(M,W)=>{const z=W[1];K(M,W),U(M)?M.every(nt=>nt.length<=1)&&z():M.length<=1&&z()},H={mode:s,persisted:l,beforeEnter(M){let W=i;if(!n.isMounted){if(!r)return;W=$||i}M._leaveCb&&M._leaveCb(!0);const z=B[q];z&&ne(t,z)&&z.el._leaveCb&&z.el._leaveCb(),K(W,[M])},enter(M){let W=c,z=a,nt=f;if(!n.isMounted){if(!r)return;W=m||c,z=C||a,nt=N||f}let T=!1;const k=M._enterCb=V=>{T||(T=!0,K(V?nt:z,[M]),H.delayedLeave&&H.delayedLeave(),M._enterCb=void 0)};W?J(W,[M,k]):k()},leave(M,W){const z=String(t.key);if(M._enterCb&&M._enterCb(!0),n.isUnmounting)return W();K(p,[M]);let nt=!1;const T=M._leaveCb=k=>{nt||(nt=!0,W(),K(k?L:E,[M]),M._leaveCb=void 0,B[z]===t&&delete B[z])};B[z]=t,g?J(g,[M,T]):T()},clone:M=>Me(M,e,n,o)};return H}function Pn(t){if(Ve(t))return(t=Qt(t)).children=null,t}function Io(t){return Ve(t)?t.children?t.children[0]:void 0:t}function Ie(t,e){6&t.shapeFlag&&t.component?Ie(t.component.subTree,e):128&t.shapeFlag?(t.ssContent.transition=e.clone(t.ssContent),t.ssFallback.transition=e.clone(t.ssFallback)):t.transition=e}function fo(t,e=!1,n){let o=[],r=0;for(let s=0;s1)for(let s=0;set({name:t.name},e,{setup:t}))():t}const pe=t=>!!t.type.__asyncLoader;function zi(t){D(t)&&(t={loader:t});const{loader:e,loadingComponent:n,errorComponent:o,delay:r=200,timeout:s,suspensible:l=!0,onError:i}=t;let c,a=null,f=0;const p=()=>{let g;return a||(g=a=e().catch(E=>{if(E=E instanceof Error?E:new Error(String(E)),i)return new Promise((L,$)=>{i(E,()=>L((f++,a=null,p())),()=>$(E),f+1)});throw E}).then(E=>g!==a&&a?a:(E&&(E.__esModule||E[Symbol.toStringTag]==="Module")&&(E=E.default),c=E,E)))};return po({name:"AsyncComponentWrapper",__asyncLoader:p,get __asyncResolved(){return c},setup(){const g=it;if(c)return()=>$n(c,g);const E=C=>{a=null,je(C,g,13,!o)};if(l&&g.suspense)return p().then(C=>()=>$n(C,g)).catch(C=>(E(C),()=>o?ot(o,{error:C}):null));const L=ke(!1),$=ke(),m=ke(!!r);return r&&setTimeout(()=>{m.value=!1},r),s!=null&&setTimeout(()=>{if(!L.value&&!$.value){const C=new Error(`Async component timed out after ${s}ms.`);E(C),$.value=C}},s),p().then(()=>{L.value=!0,g.parent&&Ve(g.parent.vnode)&&wn(g.parent.update)}).catch(C=>{E(C),$.value=C}),()=>L.value&&c?$n(c,g):$.value&&o?ot(o,{error:$.value}):n&&!m.value?ot(n):void 0}})}function $n(t,e){const{ref:n,props:o,children:r,ce:s}=e.vnode,l=ot(t,o,r);return l.ref=n,l.ce=s,delete e.vnode.ce,l}const Ve=t=>t.type.__isKeepAlive;function Fl(t,e){Yr(t,"a",e)}function Ol(t,e){Yr(t,"da",e)}function Yr(t,e,n=it){const o=t.__wdc||(t.__wdc=()=>{let r=n;for(;r;){if(r.isDeactivated)return;r=r.parent}return t()});if(En(e,o,n),n){let r=n.parent;for(;r&&r.parent;)Ve(r.parent.vnode)&&Pl(o,e,n,r),r=r.parent}}function Pl(t,e,n,o){const r=En(e,t,o,!0);An(()=>{Zn(o[e],r)},n)}function En(t,e,n=it,o=!1){if(n){const r=n[t]||(n[t]=[]),s=e.__weh||(e.__weh=(...l)=>{if(n.isUnmounted)return;he(),ye(n);const i=Ft(e,n,t,l);return le(),me(),i});return o?r.unshift(s):r.push(s),s}}const Vt=t=>(e,n=it)=>(!hn||t==="sp")&&En(t,(...o)=>e(...o),n),$l=Vt("bm"),kn=Vt("m"),Ml=Vt("bu"),Qr=Vt("u"),Zr=Vt("bum"),An=Vt("um"),Il=Vt("sp"),Nl=Vt("rtg"),Ul=Vt("rtc");function jl(t,e=it){En("ec",t,e)}const ho="components";function Ki(t,e){return es(ho,t,!0,e)||t}const ts=Symbol.for("v-ndc");function Gi(t){return st(t)?es(ho,t,!1)||t:t||ts}function es(t,e,n=!0,o=!1){const r=ut||it;if(r){const s=r.type;if(t===ho){const i=ei(s,!1);if(i&&(i===e||i===Mt(e)||i===yn(Mt(e))))return s}const l=No(r[t]||s[t],e)||No(r.appContext[t],e);return!l&&o?s:l}}function No(t,e){return t&&(t[e]||t[Mt(e)]||t[yn(Mt(e))])}function Xi(t,e,n,o){let r;const s=n&&n[o];if(U(t)||st(t)){r=new Array(t.length);for(let l=0,i=t.length;le(l,i,void 0,s&&s[i]));else{const l=Object.keys(t);r=new Array(l.length);for(let i=0,c=l.length;i!dn(e)||e.type!==_t&&!(e.type===pt&&!ns(e.children)))?t:null}function Yi(t,e){const n={};for(const o in t)n[e&&/[A-Z]/.test(o)?`on:${o}`:Ze(o)]=t[o];return n}const qn=t=>t?hs(t)?bo(t)||t.proxy:qn(t.parent):null,Ae=et(Object.create(null),{$:t=>t,$el:t=>t.vnode.el,$data:t=>t.data,$props:t=>t.props,$attrs:t=>t.attrs,$slots:t=>t.slots,$refs:t=>t.refs,$parent:t=>qn(t.parent),$root:t=>qn(t.root),$emit:t=>t.emit,$options:t=>mo(t),$forceUpdate:t=>t.f||(t.f=()=>wn(t.update)),$nextTick:t=>t.n||(t.n=Hr.bind(t.proxy)),$watch:t=>Rl.bind(t)}),Mn=(t,e)=>t!==Z&&!t.__isScriptSetup&&G(t,e),zn={get({_:t},e){const{ctx:n,setupState:o,data:r,props:s,accessCache:l,type:i,appContext:c}=t;let a;if(e[0]!=="$"){const E=l[e];if(E!==void 0)switch(E){case 1:return o[e];case 2:return r[e];case 4:return n[e];case 3:return s[e]}else{if(Mn(o,e))return l[e]=1,o[e];if(r!==Z&&G(r,e))return l[e]=2,r[e];if((a=t.propsOptions[0])&&G(a,e))return l[e]=3,s[e];if(n!==Z&&G(n,e))return l[e]=4,n[e];Kn&&(l[e]=0)}}const f=Ae[e];let p,g;return f?(e==="$attrs"&>(t,0,e),f(t)):(p=i.__cssModules)&&(p=p[e])?p:n!==Z&&G(n,e)?(l[e]=4,n[e]):(g=c.config.globalProperties,G(g,e)?g[e]:void 0)},set({_:t},e,n){const{data:o,setupState:r,ctx:s}=t;return Mn(r,e)?(r[e]=n,!0):o!==Z&&G(o,e)?(o[e]=n,!0):!G(t.props,e)&&(e[0]!=="$"||!(e.slice(1)in t))&&(s[e]=n,!0)},has({_:{data:t,setupState:e,accessCache:n,ctx:o,appContext:r,propsOptions:s}},l){let i;return!!n[l]||t!==Z&&G(t,l)||Mn(e,l)||(i=s[0])&&G(i,l)||G(o,l)||G(Ae,l)||G(r.config.globalProperties,l)},defineProperty(t,e,n){return n.get!=null?t._.accessCache[e]=0:G(n,"value")&&this.set(t,e,n.value,null),Reflect.defineProperty(t,e,n)}};et({},zn,{get(t,e){if(e!==Symbol.unscopables)return zn.get(t,e,t)},has:(t,e)=>e[0]!=="_"&&!Bs(e)});function Qi(){return Vl().slots}function Vl(){const t=yo();return t.setupContext||(t.setupContext=gs(t))}function Uo(t){return U(t)?t.reduce((e,n)=>(e[n]=null,e),{}):t}let Kn=!0;function Bl(t){const e=mo(t),n=t.proxy,o=t.ctx;Kn=!1,e.beforeCreate&&jo(e.beforeCreate,t,"bc");const{data:r,computed:s,methods:l,watch:i,provide:c,inject:a,created:f,beforeMount:p,mounted:g,beforeUpdate:E,updated:L,activated:$,deactivated:m,beforeUnmount:C,unmounted:N,render:q,renderTracked:B,renderTriggered:K,errorCaptured:J,serverPrefetch:H,expose:M,inheritAttrs:W,components:z,directives:nt}=e;if(a&&function(k,V,tt=Xt){U(k)&&(k=Gn(k));for(const bt in k){const Ct=k[bt];let wt;wt=Q(Ct)?"default"in Ct?Te(Ct.from||bt,Ct.default,!0):Te(Ct.from||bt):Te(Ct),at(wt)?Object.defineProperty(V,bt,{enumerable:!0,configurable:!0,get:()=>wt.value,set:Ot=>wt.value=Ot}):V[bt]=wt}}(a,o,null),l)for(const k in l){const V=l[k];D(V)&&(o[k]=V.bind(n))}if(r){const k=r.call(n,n);Q(k)&&(t.data=Cn(k))}if(Kn=!0,s)for(const k in s){const V=s[k],tt=D(V)?V.bind(n,n):D(V.get)?V.get.bind(n,n):Xt,bt=!D(V)&&D(V.set)?V.set.bind(n):Xt,Ct=At({get:tt,set:bt});Object.defineProperty(o,k,{enumerable:!0,configurable:!0,get:()=>Ct.value,set:wt=>Ct.value=wt})}if(i)for(const k in i)os(i[k],o,n,k);if(c){const k=D(c)?c.call(n):c;Reflect.ownKeys(k).forEach(V=>{ql(V,k[V])})}function T(k,V){U(V)?V.forEach(tt=>k(tt.bind(n))):V&&k(V.bind(n))}if(f&&jo(f,t,"c"),T($l,p),T(kn,g),T(Ml,E),T(Qr,L),T(Fl,$),T(Ol,m),T(jl,J),T(Ul,B),T(Nl,K),T(Zr,C),T(An,N),T(Il,H),U(M))if(M.length){const k=t.exposed||(t.exposed={});M.forEach(V=>{Object.defineProperty(k,V,{get:()=>n[V],set:tt=>n[V]=tt})})}else t.exposed||(t.exposed={});q&&t.render===Xt&&(t.render=q),W!=null&&(t.inheritAttrs=W),z&&(t.components=z),nt&&(t.directives=nt)}function jo(t,e,n){Ft(U(t)?t.map(o=>o.bind(e.proxy)):t.bind(e.proxy),e,n)}function os(t,e,n,o){const r=o.includes(".")?Kr(n,o):()=>n[o];if(st(t)){const s=e[t];D(s)&&en(r,s)}else if(D(t))en(r,t.bind(n));else if(Q(t))if(U(t))t.forEach(s=>os(s,e,n,o));else{const s=D(t.handler)?t.handler.bind(n):e[t.handler];D(s)&&en(r,s,t)}}function mo(t){const e=t.type,{mixins:n,extends:o}=e,{mixins:r,optionsCache:s,config:{optionMergeStrategies:l}}=t.appContext,i=s.get(e);let c;return i?c=i:r.length||n||o?(c={},r.length&&r.forEach(a=>an(c,a,l,!0)),an(c,e,l)):c=e,Q(e)&&s.set(e,c),c}function an(t,e,n,o=!1){const{mixins:r,extends:s}=e;s&&an(t,s,n,!0),r&&r.forEach(l=>an(t,l,n,!0));for(const l in e)if(!(o&&l==="expose")){const i=Hl[l]||n&&n[l];t[l]=i?i(t[l],e[l]):e[l]}return t}const Hl={data:Vo,props:Bo,emits:Bo,methods:Ce,computed:Ce,beforeCreate:dt,created:dt,beforeMount:dt,mounted:dt,beforeUpdate:dt,updated:dt,beforeDestroy:dt,beforeUnmount:dt,destroyed:dt,unmounted:dt,activated:dt,deactivated:dt,errorCaptured:dt,serverPrefetch:dt,components:Ce,directives:Ce,watch:function(t,e){if(!t)return e;if(!e)return t;const n=et(Object.create(null),t);for(const o in e)n[o]=dt(t[o],e[o]);return n},provide:Vo,inject:function(t,e){return Ce(Gn(t),Gn(e))}};function Vo(t,e){return e?t?function(){return et(D(t)?t.call(this,this):t,D(e)?e.call(this,this):e)}:e:t}function Gn(t){if(U(t)){const e={};for(let n=0;n(s.has(c)||(c&&D(c.install)?(s.add(c),c.install(i,...a)):D(c)&&(s.add(c),c(i,...a))),i),mixin:c=>(r.mixins.includes(c)||r.mixins.push(c),i),component:(c,a)=>a?(r.components[c]=a,i):r.components[c],directive:(c,a)=>a?(r.directives[c]=a,i):r.directives[c],mount(c,a,f){if(!l){const p=ot(n,o);return p.appContext=r,a&&e?e(p,c):t(p,c,f),l=!0,i._container=c,c.__vue_app__=i,bo(p.component)||p.component.proxy}},unmount(){l&&(t(null,i._container),delete i._container.__vue_app__)},provide:(c,a)=>(r.provides[c]=a,i),runWithContext(c){un=i;try{return c()}finally{un=null}}};return i}}let un=null;function ql(t,e){if(it){let n=it.provides;const o=it.parent&&it.parent.provides;o===n&&(n=it.provides=Object.create(o)),n[t]=e}}function Te(t,e,n=!1){const o=it||ut;if(o||un){const r=o?o.parent==null?o.vnode.appContext&&o.vnode.appContext.provides:o.parent.provides:un._context.provides;if(r&&t in r)return r[t];if(arguments.length>1)return n&&D(e)?e.call(o&&o.proxy):e}}function Ho(t,e,n,o){const[r,s]=t.propsOptions;let l,i=!1;if(e)for(let c in e){if(Se(c))continue;const a=e[c];let f;r&&G(r,f=Mt(c))?s&&s.includes(f)?(l||(l={}))[f]=a:n[f]=a:ln(t.emitsOptions,c)||c in o&&a===o[c]||(o[c]=a,i=!0)}if(s){const c=X(n),a=l||Z;for(let f=0;f{c=!0;const[g,E]=ss(p,e,!0);et(l,g),E&&i.push(...E)};!n&&e.mixins.length&&e.mixins.forEach(f),t.extends&&f(t.extends),t.mixins&&t.mixins.forEach(f)}if(!s&&!c)return Q(t)&&o.set(t,ae),ae;if(U(s))for(let f=0;f-1,E[1]=$<0||L<$,(L>-1||G(E,"default"))&&i.push(p)}}}const a=[l,i];return Q(t)&&o.set(t,a),a}function Do(t){return t[0]!=="$"}function Wo(t){const e=t&&t.toString().match(/^\s*(function|class) (\w+)/);return e?e[2]:t===null?"null":""}function qo(t,e){return Wo(t)===Wo(e)}function zo(t,e){return U(e)?e.findIndex(n=>qo(n,t)):D(e)&&qo(e,t)?0:-1}const ls=t=>t[0]==="_"||t==="$stable",go=t=>U(t)?t.map(Tt):[Tt(t)],zl=(t,e,n)=>{if(e._n)return e;const o=xl((...r)=>go(e(...r)),n);return o._c=!1,o},Ko=(t,e,n)=>{const o=t._ctx;for(const r in t){if(ls(r))continue;const s=t[r];if(D(s))e[r]=zl(0,s,o);else if(s!=null){const l=go(s);e[r]=()=>l}}},Go=(t,e)=>{const n=go(e);t.slots.default=()=>n};function fn(t,e,n,o,r=!1){if(U(t))return void t.forEach((g,E)=>fn(g,e&&(U(e)?e[E]:e),n,o,r));if(pe(o)&&!r)return;const s=4&o.shapeFlag?bo(o.component)||o.component.proxy:o.el,l=r?null:s,{i,r:c}=t,a=e&&e.r,f=i.refs===Z?i.refs={}:i.refs,p=i.setupState;if(a!=null&&a!==c&&(st(a)?(f[a]=null,G(p,a)&&(p[a]=null)):at(a)&&(a.value=null)),D(c))Yt(c,i,12,[l,f]);else{const g=st(c),E=at(c);if(g||E){const L=()=>{if(t.f){const $=g?G(p,c)?p[c]:f[c]:c.value;r?U($)&&Zn($,s):U($)?$.includes(s)||$.push(s):g?(f[c]=[s],G(p,c)&&(p[c]=f[c])):(c.value=[s],t.k&&(f[t.k]=c.value))}else g?(f[c]=l,G(p,c)&&(p[c]=l)):E&&(c.value=l,t.k&&(f[t.k]=l))};l?(L.id=-1,mt(L,n)):L()}}}let Wt=!1;const Je=t=>/svg/.test(t.namespaceURI)&&t.tagName!=="foreignObject",Ye=t=>t.nodeType===8;function Kl(t){const{mt:e,p:n,o:{patchProp:o,createText:r,nextSibling:s,parentNode:l,remove:i,insert:c,createComment:a}}=t,f=(m,C,N,q,B,K=!1)=>{const J=Ye(m)&&m.data==="[",H=()=>L(m,C,N,q,B,J),{type:M,ref:W,shapeFlag:z,patchFlag:nt}=C;let T=m.nodeType;C.el=m,nt===-2&&(K=!1,C.dynamicChildren=null);let k=null;switch(M){case ve:T!==3?C.children===""?(c(C.el=r(""),l(m),m),k=m):k=H():(m.data!==C.children&&(Wt=!0,m.data=C.children),k=s(m));break;case _t:k=T!==8||J?H():s(m);break;case Re:if(J&&(T=(m=s(m)).nodeType),T===1||T===3){k=m;const V=!C.children.length;for(let tt=0;tt{K=K||!!C.dynamicChildren;const{type:J,props:H,patchFlag:M,shapeFlag:W,dirs:z}=C,nt=J==="input"&&z||J==="option";if(nt||M!==-1){if(z&&Pt(C,null,N,"created"),H)if(nt||!K||48&M)for(const k in H)(nt&&k.endsWith("value")||Ue(k)&&!Se(k))&&o(m,k,null,H[k],!1,void 0,N);else H.onClick&&o(m,"onClick",null,H.onClick,!1,void 0,N);let T;if((T=H&&H.onVnodeBeforeMount)&&Et(T,N,C),z&&Pt(C,null,N,"beforeMount"),((T=H&&H.onVnodeMounted)||z)&&zr(()=>{T&&Et(T,N,C),z&&Pt(C,null,N,"mounted")},q),16&W&&(!H||!H.innerHTML&&!H.textContent)){let k=g(m.firstChild,C,m,N,q,B,K);for(;k;){Wt=!0;const V=k;k=k.nextSibling,i(V)}}else 8&W&&m.textContent!==C.children&&(Wt=!0,m.textContent=C.children)}return m.nextSibling},g=(m,C,N,q,B,K,J)=>{J=J||!!C.dynamicChildren;const H=C.children,M=H.length;for(let W=0;W{const{slotScopeIds:J}=C;J&&(B=B?B.concat(J):J);const H=l(m),M=g(s(m),C,H,N,q,B,K);return M&&Ye(M)&&M.data==="]"?s(C.anchor=M):(Wt=!0,c(C.anchor=a("]"),H,M),M)},L=(m,C,N,q,B,K)=>{if(Wt=!0,C.el=null,K){const M=$(m);for(;;){const W=s(m);if(!W||W===M)break;i(W)}}const J=s(m),H=l(m);return i(m),n(null,C,H,J,N,q,Je(H),B),J},$=m=>{let C=0;for(;m;)if((m=s(m))&&Ye(m)&&(m.data==="["&&C++,m.data==="]")){if(C===0)return s(m);C--}return m};return[(m,C)=>{if(!C.hasChildNodes())return n(null,m,C),sn(),void(C._vnode=m);Wt=!1,f(C.firstChild,m,null,null,null),sn(),C._vnode=m,Wt&&console.error("Hydration completed but contains mismatches.")},f]}const mt=zr;function Gl(t){return Xl(t,Kl)}function Xl(t,e){(Eo||(Eo=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{})).__VUE__=!0;const{insert:n,remove:o,patchProp:r,createElement:s,createText:l,createComment:i,setText:c,setElementText:a,parentNode:f,nextSibling:p,setScopeId:g=Xt,insertStaticContent:E}=t,L=(u,d,v,x=null,y=null,_=null,F=!1,w=null,b=!!d.dynamicChildren)=>{if(u===d)return;u&&!ne(u,d)&&(x=Be(u),Ot(u,y,_,!0),u=null),d.patchFlag===-2&&(b=!1,d.dynamicChildren=null);const{type:h,ref:R,shapeFlag:A}=d;switch(h){case ve:$(u,d,v,x);break;case _t:m(u,d,v,x);break;case Re:u==null&&C(d,v,x,F);break;case pt:W(u,d,v,x,y,_,F,w,b);break;default:1&A?N(u,d,v,x,y,_,F,w,b):6&A?z(u,d,v,x,y,_,F,w,b):(64&A||128&A)&&h.process(u,d,v,x,y,_,F,w,b,_e)}R!=null&&y&&fn(R,u&&u.ref,_,d||u,!d)},$=(u,d,v,x)=>{if(u==null)n(d.el=l(d.children),v,x);else{const y=d.el=u.el;d.children!==u.children&&c(y,d.children)}},m=(u,d,v,x)=>{u==null?n(d.el=i(d.children||""),v,x):d.el=u.el},C=(u,d,v,x)=>{[u.el,u.anchor]=E(u.children,d,v,x,u.el,u.anchor)},N=(u,d,v,x,y,_,F,w,b)=>{F=F||d.type==="svg",u==null?q(d,v,x,y,_,F,w,b):J(u,d,y,_,F,w,b)},q=(u,d,v,x,y,_,F,w)=>{let b,h;const{type:R,props:A,shapeFlag:O,transition:S,dirs:P}=u;if(b=u.el=s(u.type,_,A&&A.is,A),8&O?a(b,u.children):16&O&&K(u.children,b,null,x,y,_&&R!=="foreignObject",F,w),P&&Pt(u,null,x,"created"),B(b,u,u.scopeId,F,x),A){for(const I in A)I==="value"||Se(I)||r(b,I,null,A[I],_,u.children,x,y,It);"value"in A&&r(b,"value",null,A.value),(h=A.onVnodeBeforeMount)&&Et(h,x,u)}P&&Pt(u,null,x,"beforeMount");const j=(!y||y&&!y.pendingBranch)&&S&&!S.persisted;j&&S.beforeEnter(b),n(b,d,v),((h=A&&A.onVnodeMounted)||j||P)&&mt(()=>{h&&Et(h,x,u),j&&S.enter(b),P&&Pt(u,null,x,"mounted")},y)},B=(u,d,v,x,y)=>{if(v&&g(u,v),x)for(let _=0;_{for(let h=b;h{const w=d.el=u.el;let{patchFlag:b,dynamicChildren:h,dirs:R}=d;b|=16&u.patchFlag;const A=u.props||Z,O=d.props||Z;let S;v&&Zt(v,!1),(S=O.onVnodeBeforeUpdate)&&Et(S,v,d,u),R&&Pt(d,u,v,"beforeUpdate"),v&&Zt(v,!0);const P=y&&d.type!=="foreignObject";if(h?H(u.dynamicChildren,h,w,v,x,P,_):F||tt(u,d,w,null,v,x,P,_,!1),b>0){if(16&b)M(w,d,A,O,v,x,y);else if(2&b&&A.class!==O.class&&r(w,"class",null,O.class,y),4&b&&r(w,"style",A.style,O.style,y),8&b){const j=d.dynamicProps;for(let I=0;I{S&&Et(S,v,d,u),R&&Pt(d,u,v,"updated")},x)},H=(u,d,v,x,y,_,F)=>{for(let w=0;w{if(v!==x){if(v!==Z)for(const w in v)Se(w)||w in x||r(u,w,v[w],null,F,d.children,y,_,It);for(const w in x){if(Se(w))continue;const b=x[w],h=v[w];b!==h&&w!=="value"&&r(u,w,h,b,F,d.children,y,_,It)}"value"in x&&r(u,"value",v.value,x.value)}},W=(u,d,v,x,y,_,F,w,b)=>{const h=d.el=u?u.el:l(""),R=d.anchor=u?u.anchor:l("");let{patchFlag:A,dynamicChildren:O,slotScopeIds:S}=d;S&&(w=w?w.concat(S):S),u==null?(n(h,v,x),n(R,v,x),K(d.children,v,R,y,_,F,w,b)):A>0&&64&A&&O&&u.dynamicChildren?(H(u.dynamicChildren,O,v,y,_,F,w),(d.key!=null||y&&d===y.subTree)&&is(u,d,!0)):tt(u,d,v,R,y,_,F,w,b)},z=(u,d,v,x,y,_,F,w,b)=>{d.slotScopeIds=w,u==null?512&d.shapeFlag?y.ctx.activate(d,v,x,F,b):nt(d,v,x,y,_,F,b):T(u,d,b)},nt=(u,d,v,x,y,_,F)=>{const w=u.component=function(b,h,R){const A=b.type,O=(h?h.appContext:b.appContext)||Zl,S={uid:ti++,vnode:b,type:A,parent:h,appContext:O,root:null,next:null,subTree:null,effect:null,update:null,scope:new Ks(!0),render:null,proxy:null,exposed:null,exposeProxy:null,withProxy:null,provides:h?h.provides:Object.create(O.provides),accessCache:null,renderCache:[],components:null,directives:null,propsOptions:ss(A,O),emitsOptions:qr(A,O),emit:null,emitted:null,propsDefaults:Z,inheritAttrs:A.inheritAttrs,ctx:Z,data:Z,props:Z,attrs:Z,slots:Z,refs:Z,setupState:Z,setupContext:null,attrsProxy:null,slotsProxy:null,suspense:R,suspenseId:R?R.pendingId:0,asyncDep:null,asyncResolved:!1,isMounted:!1,isUnmounted:!1,isDeactivated:!1,bc:null,c:null,bm:null,m:null,bu:null,u:null,um:null,bum:null,da:null,a:null,rtg:null,rtc:null,ec:null,sp:null};return S.ctx={_:S},S.root=h?h.root:S,S.emit=wl.bind(null,S),b.ce&&b.ce(S),S}(u,x,y);if(Ve(u)&&(w.ctx.renderer=_e),function(b,h=!1){hn=h;const{props:R,children:A}=b.vnode,O=hs(b);(function(S,P,j,I=!1){const Y={},rt={};tn(rt,pn,1),S.propsDefaults=Object.create(null),Ho(S,P,Y,rt);for(const vt in S.propsOptions[0])vt in Y||(Y[vt]=void 0);S.props=j?I?Y:hl(Y):S.type.props?Y:rt,S.attrs=rt})(b,R,O,h),((S,P)=>{if(32&S.vnode.shapeFlag){const j=P._;j?(S.slots=X(P),tn(P,"_",j)):Ko(P,S.slots={})}else S.slots={},P&&Go(S,P);tn(S.slots,pn,1)})(b,A),O&&function(S,P){const j=S.type;S.accessCache=Object.create(null),S.proxy=Ee(new Proxy(S.ctx,zn));const{setup:I}=j;if(I){const Y=S.setupContext=I.length>1?gs(S):null;ye(S),he();const rt=Yt(I,S,0,[S.props,Y]);if(me(),le(),br(rt)){if(rt.then(le,le),P)return rt.then(vt=>{Yo(S,vt,P)}).catch(vt=>{je(vt,S,0)});S.asyncDep=rt}else Yo(S,rt,P)}else ms(S,P)}(b,h),hn=!1}(w),w.asyncDep){if(y&&y.registerDep(w,k),!u.el){const b=w.subTree=ot(_t);m(null,b,d,v)}}else k(w,u,d,v,y,_,F)},T=(u,d,v)=>{const x=d.component=u.component;if(function(y,_,F){const{props:w,children:b,component:h}=y,{props:R,children:A,patchFlag:O}=_,S=h.emitsOptions;if(_.dirs||_.transition)return!0;if(!(F&&O>=0))return!(!b&&!A||A&&A.$stable)||w!==R&&(w?!R||Mo(w,R,S):!!R);if(1024&O)return!0;if(16&O)return w?Mo(w,R,S):!!R;if(8&O){const P=_.dynamicProps;for(let j=0;j$t&&ct.splice(_,1)}(x.update),x.update()}else d.el=u.el,x.vnode=d},k=(u,d,v,x,y,_,F)=>{const w=u.effect=new so(()=>{if(u.isMounted){let h,{next:R,bu:A,u:O,parent:S,vnode:P}=u,j=R;Zt(u,!1),R?(R.el=P.el,V(u,R,F)):R=P,A&&Fn(A),(h=R.props&&R.props.onVnodeBeforeUpdate)&&Et(h,S,R,P),Zt(u,!0);const I=On(u),Y=u.subTree;u.subTree=I,L(Y,I,f(Y.el),Be(Y),u,y,_),R.el=I.el,j===null&&kl(u,I.el),O&&mt(O,y),(h=R.props&&R.props.onVnodeUpdated)&&mt(()=>Et(h,S,R,P),y)}else{let h;const{el:R,props:A}=d,{bm:O,m:S,parent:P}=u,j=pe(d);if(Zt(u,!1),O&&Fn(O),!j&&(h=A&&A.onVnodeBeforeMount)&&Et(h,P,d),Zt(u,!0),R&&Rn){const I=()=>{u.subTree=On(u),Rn(R,u.subTree,u,y,null)};j?d.type.__asyncLoader().then(()=>!u.isUnmounted&&I()):I()}else{const I=u.subTree=On(u);L(null,I,v,x,u,y,_),d.el=I.el}if(S&&mt(S,y),!j&&(h=A&&A.onVnodeMounted)){const I=d;mt(()=>Et(h,P,I),y)}(256&d.shapeFlag||P&&pe(P.vnode)&&256&P.vnode.shapeFlag)&&u.a&&mt(u.a,y),u.isMounted=!0,d=v=x=null}},()=>wn(b),u.scope),b=u.update=()=>w.run();b.id=u.uid,Zt(u,!0),b()},V=(u,d,v)=>{d.component=u;const x=u.vnode.props;u.vnode=d,u.next=null,function(y,_,F,w){const{props:b,attrs:h,vnode:{patchFlag:R}}=y,A=X(b),[O]=y.propsOptions;let S=!1;if(!(w||R>0)||16&R){let P;Ho(y,_,b,h)&&(S=!0);for(const j in A)_&&(G(_,j)||(P=ie(j))!==j&&G(_,P))||(O?!F||F[j]===void 0&&F[P]===void 0||(b[j]=Xn(O,A,j,void 0,y,!0)):delete b[j]);if(h!==A)for(const j in h)_&&G(_,j)||(delete h[j],S=!0)}else if(8&R){const P=y.vnode.dynamicProps;for(let j=0;j{const{vnode:w,slots:b}=y;let h=!0,R=Z;if(32&w.shapeFlag){const A=_._;A?F&&A===1?h=!1:(et(b,_),F||A!==1||delete b._):(h=!_.$stable,Ko(_,b)),R=_}else _&&(Go(y,_),R={default:1});if(h)for(const A in b)ls(A)||A in R||delete b[A]})(u,d.children,v),he(),$o(),me()},tt=(u,d,v,x,y,_,F,w,b=!1)=>{const h=u&&u.children,R=u?u.shapeFlag:0,A=d.children,{patchFlag:O,shapeFlag:S}=d;if(O>0){if(128&O)return void Ct(h,A,v,x,y,_,F,w,b);if(256&O)return void bt(h,A,v,x,y,_,F,w,b)}8&S?(16&R&&It(h,y,_),A!==h&&a(v,A)):16&R?16&S?Ct(h,A,v,x,y,_,F,w,b):It(h,y,_,!0):(8&R&&a(v,""),16&S&&K(A,v,x,y,_,F,w,b))},bt=(u,d,v,x,y,_,F,w,b)=>{const h=(u=u||ae).length,R=(d=d||ae).length,A=Math.min(h,R);let O;for(O=0;OR?It(u,y,_,!0,!1,A):K(d,v,x,y,_,F,w,b,A)},Ct=(u,d,v,x,y,_,F,w,b)=>{let h=0;const R=d.length;let A=u.length-1,O=R-1;for(;h<=A&&h<=O;){const S=u[h],P=d[h]=b?Kt(d[h]):Tt(d[h]);if(!ne(S,P))break;L(S,P,v,null,y,_,F,w,b),h++}for(;h<=A&&h<=O;){const S=u[A],P=d[O]=b?Kt(d[O]):Tt(d[O]);if(!ne(S,P))break;L(S,P,v,null,y,_,F,w,b),A--,O--}if(h>A){if(h<=O){const S=O+1,P=SO)for(;h<=A;)Ot(u[h],y,_,!0),h++;else{const S=h,P=h,j=new Map;for(h=P;h<=O;h++){const lt=d[h]=b?Kt(d[h]):Tt(d[h]);lt.key!=null&&j.set(lt.key,h)}let I,Y=0;const rt=O-P+1;let vt=!1,xo=0;const be=new Array(rt);for(h=0;h=rt){Ot(lt,y,_,!0);continue}let ht;if(lt.key!=null)ht=j.get(lt.key);else for(I=P;I<=O;I++)if(be[I-P]===0&&ne(lt,d[I])){ht=I;break}ht===void 0?Ot(lt,y,_,!0):(be[ht-P]=h+1,ht>=xo?xo=ht:vt=!0,L(lt,d[ht],v,null,y,_,F,w,b),Y++)}const So=vt?function(lt){const ht=lt.slice(),ft=[0];let Bt,Ln,xt,Ht,He;const Ps=lt.length;for(Bt=0;Bt>1,lt[ft[He]]0&&(ht[Bt]=ft[xt-1]),ft[xt]=Bt)}}for(xt=ft.length,Ht=ft[xt-1];xt-- >0;)ft[xt]=Ht,Ht=ht[Ht];return ft}(be):ae;for(I=So.length-1,h=rt-1;h>=0;h--){const lt=P+h,ht=d[lt],ft=lt+1{const{el:_,type:F,transition:w,children:b,shapeFlag:h}=u;if(6&h)return void wt(u.component.subTree,d,v,x);if(128&h)return void u.suspense.move(d,v,x);if(64&h)return void F.move(u,d,v,_e);if(F===pt){n(_,d,v);for(let R=0;R{let P;for(;R&&R!==A;)P=p(R),n(R,O,S),R=P;n(A,O,S)})(u,d,v);if(x!==2&&1&h&&w)if(x===0)w.beforeEnter(_),n(_,d,v),mt(()=>w.enter(_),y);else{const{leave:R,delayLeave:A,afterLeave:O}=w,S=()=>n(_,d,v),P=()=>{R(_,()=>{S(),O&&O()})};A?A(_,S,P):P()}else n(_,d,v)},Ot=(u,d,v,x=!1,y=!1)=>{const{type:_,props:F,ref:w,children:b,dynamicChildren:h,shapeFlag:R,patchFlag:A,dirs:O}=u;if(w!=null&&fn(w,null,v,u,!0),256&R)return void d.ctx.deactivate(u);const S=1&R&&O,P=!pe(u);let j;if(P&&(j=F&&F.onVnodeBeforeUnmount)&&Et(j,d,u),6&R)Os(u.component,v,x);else{if(128&R)return void u.suspense.unmount(v,x);S&&Pt(u,null,d,"beforeUnmount"),64&R?u.type.remove(u,d,v,y,_e,x):h&&(_!==pt||A>0&&64&A)?It(h,d,v,!1,!0):(_===pt&&384&A||!y&&16&R)&&It(b,d,v),x&&Co(u)}(P&&(j=F&&F.onVnodeUnmounted)||S)&&mt(()=>{j&&Et(j,d,u),S&&Pt(u,null,d,"unmounted")},v)},Co=u=>{const{type:d,el:v,anchor:x,transition:y}=u;if(d===pt)return void Fs(v,x);if(d===Re)return void(({el:F,anchor:w})=>{let b;for(;F&&F!==w;)b=p(F),o(F),F=b;o(w)})(u);const _=()=>{o(v),y&&!y.persisted&&y.afterLeave&&y.afterLeave()};if(1&u.shapeFlag&&y&&!y.persisted){const{leave:F,delayLeave:w}=y,b=()=>F(v,_);w?w(u.el,_,b):b()}else _()},Fs=(u,d)=>{let v;for(;u!==d;)v=p(u),o(u),u=v;o(d)},Os=(u,d,v)=>{const{bum:x,scope:y,update:_,subTree:F,um:w}=u;x&&Fn(x),y.stop(),_&&(_.active=!1,Ot(F,u,d,v)),w&&mt(w,d),mt(()=>{u.isUnmounted=!0},d),d&&d.pendingBranch&&!d.isUnmounted&&u.asyncDep&&!u.asyncResolved&&u.suspenseId===d.pendingId&&(d.deps--,d.deps===0&&d.resolve())},It=(u,d,v,x=!1,y=!1,_=0)=>{for(let F=_;F6&u.shapeFlag?Be(u.component.subTree):128&u.shapeFlag?u.suspense.next():p(u.anchor||u.el),wo=(u,d,v)=>{u==null?d._vnode&&Ot(d._vnode,null,null,!0):L(d._vnode||null,u,d,null,null,null,v),$o(),sn(),d._vnode=u},_e={p:L,um:Ot,m:wt,r:Co,mt:nt,mc:K,pc:tt,pbc:H,n:Be,o:t};let Tn,Rn;return e&&([Tn,Rn]=e(_e)),{render:wo,hydrate:Tn,createApp:Wl(wo,Tn)}}function Zt({effect:t,update:e},n){t.allowRecurse=e.allowRecurse=n}function is(t,e,n=!1){const o=t.children,r=e.children;if(U(o)&&U(r))for(let s=0;s0?Lt||ae:null,Jl(),Ne>0&&Lt&&Lt.push(t),t}function Zi(t,e,n,o,r,s){return as(ds(t,e,n,o,r,s,!0))}function us(t,e,n,o,r){return as(ot(t,e,n,o,r,!0))}function dn(t){return!!t&&t.__v_isVNode===!0}function ne(t,e){return t.type===e.type&&t.key===e.key}const pn="__vInternal",fs=({key:t})=>t??null,nn=({ref:t,ref_key:e,ref_for:n})=>(typeof t=="number"&&(t=""+t),t!=null?st(t)||at(t)||D(t)?{i:ut,r:t,k:e,f:!!n}:t:null);function ds(t,e=null,n=null,o=0,r=null,s=t===pt?0:1,l=!1,i=!1){const c={__v_isVNode:!0,__v_skip:!0,type:t,props:e,key:e&&fs(e),ref:e&&nn(e),scopeId:xn,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetAnchor:null,staticCount:0,shapeFlag:s,patchFlag:o,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:ut};return i?(vo(c,n),128&s&&t.normalize(c)):n&&(c.shapeFlag|=st(n)?8:16),Ne>0&&!l&&Lt&&(c.patchFlag>0||6&s)&&c.patchFlag!==32&&Lt.push(c),c}const ot=function(t,e=null,n=null,o=0,r=null,s=!1){if(t&&t!==ts||(t=_t),dn(t)){const c=Qt(t,e,!0);return n&&vo(c,n),Ne>0&&!s&&Lt&&(6&c.shapeFlag?Lt[Lt.indexOf(t)]=c:Lt.push(c)),c.patchFlag|=-2,c}l=t,D(l)&&"__vccOpts"in l&&(t=t.__vccOpts);var l;if(e){e=Yl(e);let{class:c,style:a}=e;c&&!st(c)&&(e.class=oo(c)),Q(a)&&(Ir(a)&&!U(a)&&(a=et({},a)),e.style=no(a))}const i=st(t)?1:Al(t)?128:(c=>c.__isTeleport)(t)?64:Q(t)?4:D(t)?2:0;return ds(t,e,n,o,r,i,s,!0)};function Yl(t){return t?Ir(t)||pn in t?et({},t):t:null}function Qt(t,e,n=!1){const{props:o,ref:r,patchFlag:s,children:l}=t,i=e?Ql(o||{},e):o;return{__v_isVNode:!0,__v_skip:!0,type:t.type,props:i,key:i&&fs(i),ref:e&&e.ref?n&&r?U(r)?r.concat(nn(e)):[r,nn(e)]:nn(e):r,scopeId:t.scopeId,slotScopeIds:t.slotScopeIds,children:l,target:t.target,targetAnchor:t.targetAnchor,staticCount:t.staticCount,shapeFlag:t.shapeFlag,patchFlag:e&&t.type!==pt?s===-1?16:16|s:s,dynamicProps:t.dynamicProps,dynamicChildren:t.dynamicChildren,appContext:t.appContext,dirs:t.dirs,transition:t.transition,component:t.component,suspense:t.suspense,ssContent:t.ssContent&&Qt(t.ssContent),ssFallback:t.ssFallback&&Qt(t.ssFallback),el:t.el,anchor:t.anchor,ctx:t.ctx,ce:t.ce}}function ps(t=" ",e=0){return ot(ve,null,t,e)}function tc(t,e){const n=ot(Re,null,t);return n.staticCount=e,n}function ec(t="",e=!1){return e?(cs(),us(_t,null,t)):ot(_t,null,t)}function Tt(t){return t==null||typeof t=="boolean"?ot(_t):U(t)?ot(pt,null,t.slice()):typeof t=="object"?Kt(t):ot(ve,null,String(t))}function Kt(t){return t.el===null&&t.patchFlag!==-1||t.memo?t:Qt(t)}function vo(t,e){let n=0;const{shapeFlag:o}=t;if(e==null)e=null;else if(U(e))n=16;else if(typeof e=="object"){if(65&o){const r=e.default;return void(r&&(r._c&&(r._d=!1),vo(t,r()),r._c&&(r._d=!0)))}{n=32;const r=e._;r||pn in e?r===3&&ut&&(ut.slots._===1?e._=1:(e._=2,t.patchFlag|=1024)):e._ctx=ut}}else D(e)?(e={default:e,_ctx:ut},n=32):(e=String(e),64&o?(n=16,e=[ps(e)]):n=8);t.children=e,t.shapeFlag|=n}function Ql(...t){const e={};for(let n=0;nit||ut;let _o;_o=t=>{it=t};const ye=t=>{_o(t),t.scope.on()},le=()=>{it&&it.scope.off(),_o(null)};function hs(t){return 4&t.vnode.shapeFlag}let Jo,hn=!1;function Yo(t,e,n){D(e)?t.render=e:Q(e)&&(t.setupState=Vr(e)),ms(t,n)}function ms(t,e,n){const o=t.type;if(!t.render){if(!e&&Jo&&!o.render){const r=o.template||mo(t).template;if(r){const{isCustomElement:s,compilerOptions:l}=t.appContext.config,{delimiters:i,compilerOptions:c}=o,a=et(et({isCustomElement:s,delimiters:i},l),c);o.render=Jo(r,a)}}t.render=o.render||Xt}ye(t),he(),Bl(t),me(),le()}function gs(t){const e=n=>{t.exposed=n||{}};return{get attrs(){return function(n){return n.attrsProxy||(n.attrsProxy=new Proxy(n.attrs,{get:(o,r)=>(gt(n,0,"$attrs"),o[r])}))}(t)},slots:t.slots,emit:t.emit,expose:e}}function bo(t){if(t.exposed)return t.exposeProxy||(t.exposeProxy=new Proxy(Vr(Ee(t.exposed)),{get:(e,n)=>n in e?e[n]:n in Ae?Ae[n](t):void 0,has:(e,n)=>n in e||n in Ae}))}function ei(t,e=!0){return D(t)?t.displayName||t.name:t.name||e&&t.__name}const At=(t,e)=>function(n,o,r=!1){let s,l;const i=D(n);return i?(s=n,l=Xt):(s=n.get,l=n.set),new _l(s,l,i||!l,r)}(t,0,hn);function Jn(t,e,n){const o=arguments.length;return o===2?Q(e)&&!U(e)?dn(e)?ot(t,null,[e]):ot(t,e):ot(t,null,e):(o>3?n=Array.prototype.slice.call(arguments,2):o===3&&dn(n)&&(n=[n]),ot(t,e,n))}const ni="3.3.4",oe=typeof document<"u"?document:null,Qo=oe&&oe.createElement("template"),oi={insert:(t,e,n)=>{e.insertBefore(t,n||null)},remove:t=>{const e=t.parentNode;e&&e.removeChild(t)},createElement:(t,e,n,o)=>{const r=e?oe.createElementNS("http://www.w3.org/2000/svg",t):oe.createElement(t,n?{is:n}:void 0);return t==="select"&&o&&o.multiple!=null&&r.setAttribute("multiple",o.multiple),r},createText:t=>oe.createTextNode(t),createComment:t=>oe.createComment(t),setText:(t,e)=>{t.nodeValue=e},setElementText:(t,e)=>{t.textContent=e},parentNode:t=>t.parentNode,nextSibling:t=>t.nextSibling,querySelector:t=>oe.querySelector(t),setScopeId(t,e){t.setAttribute(e,"")},insertStaticContent(t,e,n,o,r,s){const l=n?n.previousSibling:e.lastChild;if(r&&(r===s||r.nextSibling))for(;e.insertBefore(r.cloneNode(!0),n),r!==s&&(r=r.nextSibling););else{Qo.innerHTML=o?`${t}`:t;const i=Qo.content;if(o){const c=i.firstChild;for(;c.firstChild;)i.appendChild(c.firstChild);i.removeChild(c)}e.insertBefore(i,n)}return[l?l.nextSibling:e.firstChild,n?n.previousSibling:e.lastChild]}},Zo=/\s*!important$/;function Yn(t,e,n){if(U(n))n.forEach(o=>Yn(t,e,o));else if(n==null&&(n=""),e.startsWith("--"))t.setProperty(e,n);else{const o=function(r,s){const l=In[s];if(l)return l;let i=Mt(s);if(i!=="filter"&&i in r)return In[s]=i;i=yn(i);for(let c=0;c{if(E._vts){if(E._vts<=g.attached)return}else E._vts=Date.now();Ft(function(L,$){if(U($)){const m=L.stopImmediatePropagation;return L.stopImmediatePropagation=()=>{m.call(L),L._stopped=!0},$.map(C=>N=>!N._stopped&&C&&C(N))}return $}(E,g.value),p,5,[E])};return g.value=f,g.attached=(()=>Nn||(li.then(()=>Nn=0),Nn=Date.now()))(),g}(o,r);ri(t,i,a,c)}else l&&(function(a,f,p,g){a.removeEventListener(f,p,g)}(t,i,l,c),s[e]=void 0)}}const nr=/(?:Once|Passive|Capture)$/;let Nn=0;const li=Promise.resolve(),or=/^on[a-z]/,qt="transition",we="animation",vs=(t,{slots:e})=>Jn(Ll,_s(t),e);vs.displayName="Transition";const ys={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},ii=vs.props=et({},Xr,ys),te=(t,e=[])=>{U(t)?t.forEach(n=>n(...e)):t&&t(...e)},rr=t=>!!t&&(U(t)?t.some(e=>e.length>1):t.length>1);function _s(t){const e={};for(const T in t)T in ys||(e[T]=t[T]);if(t.css===!1)return e;const{name:n="v",type:o,duration:r,enterFromClass:s=`${n}-enter-from`,enterActiveClass:l=`${n}-enter-active`,enterToClass:i=`${n}-enter-to`,appearFromClass:c=s,appearActiveClass:a=l,appearToClass:f=i,leaveFromClass:p=`${n}-leave-from`,leaveActiveClass:g=`${n}-leave-active`,leaveToClass:E=`${n}-leave-to`}=t,L=function(T){if(T==null)return null;if(Q(T))return[Un(T.enter),Un(T.leave)];{const k=Un(T);return[k,k]}}(r),$=L&&L[0],m=L&&L[1],{onBeforeEnter:C,onEnter:N,onEnterCancelled:q,onLeave:B,onLeaveCancelled:K,onBeforeAppear:J=C,onAppear:H=N,onAppearCancelled:M=q}=e,W=(T,k,V)=>{zt(T,k?f:i),zt(T,k?a:l),V&&V()},z=(T,k)=>{T._isLeaving=!1,zt(T,p),zt(T,E),zt(T,g),k&&k()},nt=T=>(k,V)=>{const tt=T?H:N,bt=()=>W(k,T,V);te(tt,[k,bt]),sr(()=>{zt(k,T?c:s),Nt(k,T?f:i),rr(tt)||lr(k,o,$,bt)})};return et(e,{onBeforeEnter(T){te(C,[T]),Nt(T,s),Nt(T,l)},onBeforeAppear(T){te(J,[T]),Nt(T,c),Nt(T,a)},onEnter:nt(!1),onAppear:nt(!0),onLeave(T,k){T._isLeaving=!0;const V=()=>z(T,k);Nt(T,p),Cs(),Nt(T,g),sr(()=>{T._isLeaving&&(zt(T,p),Nt(T,E),rr(B)||lr(T,o,m,V))}),te(B,[T,V])},onEnterCancelled(T){W(T,!1),te(q,[T])},onAppearCancelled(T){W(T,!0),te(M,[T])},onLeaveCancelled(T){z(T),te(K,[T])}})}function Un(t){return Vs(t)}function Nt(t,e){e.split(/\s+/).forEach(n=>n&&t.classList.add(n)),(t._vtc||(t._vtc=new Set)).add(e)}function zt(t,e){e.split(/\s+/).forEach(o=>o&&t.classList.remove(o));const{_vtc:n}=t;n&&(n.delete(e),n.size||(t._vtc=void 0))}function sr(t){requestAnimationFrame(()=>{requestAnimationFrame(t)})}let ci=0;function lr(t,e,n,o){const r=t._endId=++ci,s=()=>{r===t._endId&&o()};if(n)return setTimeout(s,n);const{type:l,timeout:i,propCount:c}=bs(t,e);if(!l)return o();const a=l+"end";let f=0;const p=()=>{t.removeEventListener(a,g),s()},g=E=>{E.target===t&&++f>=c&&p()};setTimeout(()=>{f(n[E]||"").split(", "),r=o(`${qt}Delay`),s=o(`${qt}Duration`),l=ir(r,s),i=o(`${we}Delay`),c=o(`${we}Duration`),a=ir(i,c);let f=null,p=0,g=0;return e===qt?l>0&&(f=qt,p=l,g=s.length):e===we?a>0&&(f=we,p=a,g=c.length):(p=Math.max(l,a),f=p>0?l>a?qt:we:null,g=f?f===qt?s.length:c.length:0),{type:f,timeout:p,propCount:g,hasTransform:f===qt&&/\b(transform|all)(,|$)/.test(o(`${qt}Property`).toString())}}function ir(t,e){for(;t.lengthcr(n)+cr(t[o])))}function cr(t){return 1e3*Number(t.slice(0,-1).replace(",","."))}function Cs(){return document.body.offsetHeight}const ws=new WeakMap,xs=new WeakMap;et({},ii,{tag:String,moveClass:String});function ai(t){const e=t.el;e._moveCb&&e._moveCb(),e._enterCb&&e._enterCb()}function ui(t){xs.set(t,t.el.getBoundingClientRect())}function fi(t){const e=ws.get(t),n=xs.get(t),o=e.left-n.left,r=e.top-n.top;if(o||r){const s=t.el.style;return s.transform=s.webkitTransform=`translate(${o}px,${r}px)`,s.transitionDuration="0s",t}}const di=["ctrl","shift","alt","meta"],pi={stop:t=>t.stopPropagation(),prevent:t=>t.preventDefault(),self:t=>t.target!==t.currentTarget,ctrl:t=>!t.ctrlKey,shift:t=>!t.shiftKey,alt:t=>!t.altKey,meta:t=>!t.metaKey,left:t=>"button"in t&&t.button!==0,middle:t=>"button"in t&&t.button!==1,right:t=>"button"in t&&t.button!==2,exact:(t,e)=>di.some(n=>t[`${n}Key`]&&!e.includes(n))},nc=(t,e)=>(n,...o)=>{for(let r=0;rn=>{if(!("key"in n))return;const o=ie(n.key);return e.some(r=>r===o||hi[r]===o)?t(n):void 0},mi=et({patchProp:(t,e,n,o,r=!1,s,l,i,c)=>{e==="class"?function(a,f,p){const g=a._vtc;g&&(f=(f?[f,...g]:[...g]).join(" ")),f==null?a.removeAttribute("class"):p?a.setAttribute("class",f):a.className=f}(t,o,r):e==="style"?function(a,f,p){const g=a.style,E=st(p);if(p&&!E){if(f&&!st(f))for(const L in f)p[L]==null&&Yn(g,L,"");for(const L in p)Yn(g,L,p[L])}else{const L=g.display;E?f!==p&&(g.cssText=p):f&&a.removeAttribute("style"),"_vod"in a&&(g.display=L)}}(t,n,o):Ue(e)?Qn(e)||si(t,e,0,o,l):(e[0]==="."?(e=e.slice(1),1):e[0]==="^"?(e=e.slice(1),0):function(a,f,p,g){return g?f==="innerHTML"||f==="textContent"||!!(f in a&&or.test(f)&&D(p)):f==="spellcheck"||f==="draggable"||f==="translate"||f==="form"||f==="list"&&a.tagName==="INPUT"||f==="type"&&a.tagName==="TEXTAREA"||or.test(f)&&st(p)?!1:f in a}(t,e,o,r))?function(a,f,p,g,E,L,$){if(f==="innerHTML"||f==="textContent")return g&&$(g,E,L),void(a[f]=p??"");const m=a.tagName;if(f==="value"&&m!=="PROGRESS"&&!m.includes("-")){a._value=p;const N=p??"";return(m==="OPTION"?a.getAttribute("value"):a.value)!==N&&(a.value=N),void(p==null&&a.removeAttribute(f))}let C=!1;if(p===""||p==null){const N=typeof a[f];N==="boolean"?p=ko(p):p==null&&N==="string"?(p="",C=!0):N==="number"&&(p=0,C=!0)}try{a[f]=p}catch{}C&&a.removeAttribute(f)}(t,e,o,s,l,i,c):(e==="true-value"?t._trueValue=o:e==="false-value"&&(t._falseValue=o),function(a,f,p,g,E){if(g&&f.startsWith("xlink:"))p==null?a.removeAttributeNS(er,f.slice(6,f.length)):a.setAttributeNS(er,f,p);else{const L=zs(f);p==null||L&&!ko(p)?a.removeAttribute(f):a.setAttribute(f,L?"":p)}}(t,e,o,r))}},oi);let jn,ar=!1;function gi(){return jn=ar?jn:Gl(mi),ar=!0,jn}const rc=(...t)=>{const e=gi().createApp(...t),{mount:n}=e;return e.mount=o=>{const r=vi(o);if(r)return n(r,!0,r instanceof SVGElement)},e};function vi(t){return st(t)?document.querySelector(t):t}const sc=(t,e)=>{const n=t.__vccOpts||t;for(const[o,r]of e)n[o]=r;return n},yi="modulepreload",_i=function(t){return"/"+t},ur={},lc=function(e,n,o){if(!n||n.length===0)return e();const r=document.getElementsByTagName("link");return Promise.all(n.map(s=>{if(s=_i(s),s in ur)return;ur[s]=!0;const l=s.endsWith(".css"),i=l?'[rel="stylesheet"]':"";if(!!o)for(let f=r.length-1;f>=0;f--){const p=r[f];if(p.href===s&&(!l||p.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${s}"]${i}`))return;const a=document.createElement("link");if(a.rel=l?"stylesheet":yi,l||(a.as="script",a.crossOrigin=""),a.href=s,document.head.appendChild(a),l)return new Promise((f,p)=>{a.addEventListener("load",f),a.addEventListener("error",()=>p(new Error(`Unable to preload CSS for ${s}`)))})})).then(()=>e()).catch(s=>{const l=new Event("vite:preloadError",{cancelable:!0});if(l.payload=s,window.dispatchEvent(l),!l.defaultPrevented)throw s})},bi=window.__VP_SITE_DATA__,Ss=/^[a-z]+:/i,ic=/^pathname:\/\//,cc="vitepress-theme-appearance",Es=/#.*$/,Ci=/(index)?\.(md|html)$/,kt=typeof document<"u",ks={relativePath:"",filePath:"",title:"404",description:"Not Found",headers:[],frontmatter:{sidebar:!1,layout:"page"},lastUpdated:0,isNotFound:!0};function wi(t,e,n=!1){if(e===void 0)return!1;if(t=fr(`/${t}`),n)return new RegExp(e).test(t);if(fr(e)!==t)return!1;const o=e.match(Es);return o?(kt?location.hash:"")===o[0]:!0}function fr(t){return decodeURI(t).replace(Es,"").replace(Ci,"")}function xi(t){return Ss.test(t)}function Si(t,e){var o,r,s,l,i,c,a;const n=Object.keys(t.locales).find(f=>f!=="root"&&!xi(f)&&wi(e,`/${f}/`,!0))||"root";return Object.assign({},t,{localeIndex:n,lang:((o=t.locales[n])==null?void 0:o.lang)??t.lang,dir:((r=t.locales[n])==null?void 0:r.dir)??t.dir,title:((s=t.locales[n])==null?void 0:s.title)??t.title,titleTemplate:((l=t.locales[n])==null?void 0:l.titleTemplate)??t.titleTemplate,description:((i=t.locales[n])==null?void 0:i.description)??t.description,head:Ts(t.head,((c=t.locales[n])==null?void 0:c.head)??[]),themeConfig:{...t.themeConfig,...(a=t.locales[n])==null?void 0:a.themeConfig}})}function As(t,e){const n=e.title||t.title,o=e.titleTemplate??t.titleTemplate;if(typeof o=="string"&&o.includes(":title"))return o.replace(/:title/g,n);const r=Ei(t.title,o);return`${n}${r}`}function Ei(t,e){return e===!1?"":e===!0||e===void 0?` | ${t}`:t===e?"":` | ${e}`}function ki(t,e){const[n,o]=e;if(n!=="meta")return!1;const r=Object.entries(o)[0];return r==null?!1:t.some(([s,l])=>s===n&&l[r[0]]===r[1])}function Ts(t,e){return[...t.filter(n=>!ki(e,n)),...e]}const Ai=/[\u0000-\u001F"#$&*+,:;<=>?[\]^`{|}\u007F]/g,Ti=/^[a-z]:/i;function dr(t){const e=Ti.exec(t),n=e?e[0]:"";return n+t.slice(n.length).replace(Ai,"_").replace(/(^|\/)_+(?=[^/]*$)/,"$1")}const Ri=Symbol(),re=ml(bi);function ac(t){const e=At(()=>Si(re.value,t.data.relativePath));return{site:e,theme:At(()=>e.value.themeConfig),page:At(()=>t.data),frontmatter:At(()=>t.data.frontmatter),params:At(()=>t.data.params),lang:At(()=>e.value.lang),dir:At(()=>e.value.dir),localeIndex:At(()=>e.value.localeIndex||"root"),title:At(()=>As(e.value,t.data)),description:At(()=>t.data.description||e.value.description),isDark:ke(!1)}}function uc(){const t=Te(Ri);if(!t)throw new Error("vitepress data not properly injected in app");return t}function Li(t,e){return`${t}${e}`.replace(/\/+/g,"/")}function pr(t){return Ss.test(t)||t.startsWith(".")?t:Li(re.value.base,t)}function Fi(t){let e=t.replace(/\.html$/,"");if(e=decodeURIComponent(e),e=e.replace(/\/$/,"/index"),kt){const n="/";e=dr(e.slice(n.length).replace(/\//g,"_")||"index")+".md";let o=__VP_HASH_MAP__[e.toLowerCase()];if(o||(e=e.endsWith("_index.md")?e.slice(0,-9)+".md":e.slice(0,-3)+"_index.md",o=__VP_HASH_MAP__[e.toLowerCase()]),!o)return null;e=`${n}assets/${e}.${o}.js`}else e=`./${dr(e.slice(1).replace(/\//g,"_"))}.md.js`;return e}let on=[];function fc(t){on.push(t),An(()=>{on=on.filter(e=>e!==t)})}const Oi=Symbol(),hr="http://a.com",Pi=()=>({path:"/",component:null,data:ks});function dc(t,e){const n=Cn(Pi()),o={route:n,go:r};async function r(i=kt?location.href:"/"){var a,f;if(await((a=o.onBeforeRouteChange)==null?void 0:a.call(o,i))===!1)return;const c=new URL(i,hr);re.value.cleanUrls||!c.pathname.endsWith("/")&&!c.pathname.endsWith(".html")&&(c.pathname+=".html",i=c.pathname+c.search+c.hash),kt&&i!==location.href&&(history.replaceState({scrollPosition:window.scrollY},document.title),history.pushState(null,"",i)),await l(i),await((f=o.onAfterRouteChanged)==null?void 0:f.call(o,i))}let s=null;async function l(i,c=0,a=!1){var g;if(await((g=o.onBeforePageLoad)==null?void 0:g.call(o,i))===!1)return;const f=new URL(i,hr),p=s=f.pathname;try{let E=await t(p);if(!E)throw new Error(`Page not found: ${p}`);if(s===p){s=null;const{default:L,__pageData:$}=E;if(!L)throw new Error(`Invalid route component: ${L}`);n.path=kt?p:pr(p),n.component=Ee(L),n.data=Ee($),kt&&Hr(()=>{let m=re.value.base+$.relativePath.replace(/(?:(^|\/)index)?\.md$/,"$1");if(!re.value.cleanUrls&&!m.endsWith("/")&&(m+=".html"),m!==f.pathname&&(f.pathname=m,i=m+f.search+f.hash,history.replaceState(null,"",i)),f.hash&&!c){let C=null;try{C=document.getElementById(decodeURIComponent(f.hash).slice(1))}catch(N){console.warn(N)}if(C){mr(C,f.hash);return}}window.scrollTo(0,c)})}}catch(E){if(!/fetch|Page not found/.test(E.message)&&!/^\/404(\.html|\/)?$/.test(i)&&console.error(E),!a)try{const L=await fetch(re.value.base+"hashmap.json");window.__VP_HASH_MAP__=await L.json(),await l(i,c,!0);return}catch{}s===p&&(s=null,n.path=kt?p:pr(p),n.component=e?Ee(e):null,n.data=ks)}}return kt&&(window.addEventListener("click",i=>{if(i.target.closest("button"))return;const a=i.target.closest("a");if(a&&!a.closest(".vp-raw")&&(a instanceof SVGElement||!a.download)){const{target:f}=a,{href:p,origin:g,pathname:E,hash:L,search:$}=new URL(a.href instanceof SVGAnimatedString?a.href.animVal:a.href,a.baseURI),m=window.location,C=E.match(/\.\w+$/);!i.ctrlKey&&!i.shiftKey&&!i.altKey&&!i.metaKey&&!f&&g===m.origin&&!(C&&C[0]!==".html")&&(i.preventDefault(),E===m.pathname&&$===m.search?L&&(L!==m.hash&&(history.pushState(null,"",L),window.dispatchEvent(new Event("hashchange"))),mr(a,L,a.classList.contains("header-anchor"))):r(p))}},{capture:!0}),window.addEventListener("popstate",i=>{l(location.href,i.state&&i.state.scrollPosition||0)}),window.addEventListener("hashchange",i=>{i.preventDefault()})),o}function $i(){const t=Te(Oi);if(!t)throw new Error("useRouter() is called without provider.");return t}function Rs(){return $i().route}function mr(t,e,n=!1){let o=null;try{o=t.classList.contains("header-anchor")?t:document.getElementById(decodeURIComponent(e).slice(1))}catch(r){console.warn(r)}if(o){let c=function(){!n||Math.abs(i-window.scrollY)>window.innerHeight?window.scrollTo(0,i):window.scrollTo({left:0,top:i,behavior:"smooth"})};const r=re.value.scrollOffset;let s=0;if(typeof r=="number")s=r;else if(typeof r=="string")s=gr(r);else if(Array.isArray(r))for(const a of r){const f=gr(a);if(f){s=f;break}}const l=parseInt(window.getComputedStyle(o).paddingTop,10),i=window.scrollY+o.getBoundingClientRect().top-s+l;requestAnimationFrame(c)}}function gr(t){const e=document.querySelector(t);if(!e)return 0;const n=e.getBoundingClientRect().bottom;return n<0?0:n+24}const vr=()=>on.forEach(t=>t()),pc=po({name:"VitePressContent",props:{as:{type:[Object,String],default:"div"}},setup(t){const e=Rs();return()=>Jn(t.as,{style:{position:"relative"}},[e.component?Jn(e.component,{onVnodeMounted:vr,onVnodeUpdated:vr}):"404 Page Not Found"])}}),hc=po({setup(t,{slots:e}){const n=ke(!1);return kn(()=>{n.value=!0}),()=>n.value&&e.default?e.default():null}});function mc(){kt&&window.addEventListener("click",t=>{var n,o;const e=t.target;if(e.matches(".vp-code-group input")){const r=(n=e.parentElement)==null?void 0:n.parentElement,s=Array.from((r==null?void 0:r.querySelectorAll("input"))||[]).indexOf(e),l=r==null?void 0:r.querySelector('div[class*="language-"].active'),i=(o=r==null?void 0:r.querySelectorAll('div[class*="language-"]:not(.language-id)'))==null?void 0:o[s];l&&i&&l!==i&&(l.classList.remove("active"),i.classList.add("active"))}})}function gc(){if(kt){const t=new WeakMap;window.addEventListener("click",e=>{var o;const n=e.target;if(n.matches('div[class*="language-"] > button.copy')){const r=n.parentElement,s=(o=n.nextElementSibling)==null?void 0:o.nextElementSibling;if(!r||!s)return;const l=/language-(shellscript|shell|bash|sh|zsh)/.test(r.className);let i="";s.querySelectorAll("span.line:not(.diff.remove)").forEach(c=>i+=(c.textContent||"")+` +`),i=i.slice(0,-1),l&&(i=i.replace(/^ *(\$|>) /gm,"").trim()),Mi(i).then(()=>{n.classList.add("copied"),clearTimeout(t.get(n));const c=setTimeout(()=>{n.classList.remove("copied"),n.blur(),t.delete(n)},2e3);t.set(n,c)})}})}}async function Mi(t){try{return navigator.clipboard.writeText(t)}catch{const e=document.createElement("textarea"),n=document.activeElement;e.value=t,e.setAttribute("readonly",""),e.style.contain="strict",e.style.position="absolute",e.style.left="-9999px",e.style.fontSize="12pt";const o=document.getSelection(),r=o?o.rangeCount>0&&o.getRangeAt(0):null;document.body.appendChild(e),e.select(),e.selectionStart=0,e.selectionEnd=t.length,document.execCommand("copy"),document.body.removeChild(e),r&&(o.removeAllRanges(),o.addRange(r)),n&&n.focus()}}function vc(t,e){let n=[],o=!0;const r=s=>{if(o){o=!1;return}n.forEach(l=>document.head.removeChild(l)),n=[],s.forEach(l=>{const i=yr(l);document.head.appendChild(i),n.push(i)})};Tl(()=>{const s=t.data,l=e.value,i=s&&s.description,c=s&&s.frontmatter.head||[];document.title=As(l,s);const a=i||l.description;let f=document.querySelector("meta[name=description]");f?f.setAttribute("content",a):yr(["meta",{name:"description",content:a}]),r(Ts(l.head,Ni(c)))})}function yr([t,e,n]){const o=document.createElement(t);for(const r in e)o.setAttribute(r,e[r]);return n&&(o.innerHTML=n),o}function Ii(t){return t[0]==="meta"&&t[1]&&t[1].name==="description"}function Ni(t){return t.filter(e=>!Ii(e))}const Vn=new Set,Ls=()=>document.createElement("link"),Ui=t=>{const e=Ls();e.rel="prefetch",e.href=t,document.head.appendChild(e)},ji=t=>{const e=new XMLHttpRequest;e.open("GET",t,e.withCredentials=!0),e.send()};let Qe;const Vi=kt&&(Qe=Ls())&&Qe.relList&&Qe.relList.supports&&Qe.relList.supports("prefetch")?Ui:ji;function yc(){if(!kt||!window.IntersectionObserver)return;let t;if((t=navigator.connection)&&(t.saveData||/2g/.test(t.effectiveType)))return;const e=window.requestIdleCallback||setTimeout;let n=null;const o=()=>{n&&n.disconnect(),n=new IntersectionObserver(s=>{s.forEach(l=>{if(l.isIntersecting){const i=l.target;n.unobserve(i);const{pathname:c}=i;if(!Vn.has(c)){Vn.add(c);const a=Fi(c);a&&Vi(a)}}})}),e(()=>{document.querySelectorAll("#app a").forEach(s=>{const{hostname:l,pathname:i}=new URL(s.href instanceof SVGAnimatedString?s.href.animVal:s.href,s.baseURI),c=i.match(/\.\w+$/);c&&c[0]!==".html"||s.target!=="_blank"&&l===location.hostname&&(i!==location.pathname?n.observe(s):Vn.add(i))})})};kn(o);const r=Rs();en(()=>r.path,o),An(()=>{n&&n.disconnect()})}export{vs as $,kn as A,ec as B,us as C,no as D,Ss as E,ds as F,Hr as G,Tl as H,kt as I,cc as J,Mr as K,ps as L,Ki as M,vl as N,xl as O,ic as P,zi as Q,tc as R,lc as S,Qr as T,ql as U,Ql as V,Yi as W,Bi as X,oc as Y,qi as Z,sc as _,oo as a,nc as a0,Qi as a1,vc as a2,yc as a3,gc as a4,mc as a5,Jn as a6,Oi as a7,ac as a8,Ri as a9,pc as aa,hc as ab,re as ac,rc as ad,dc as ae,Fi as af,$i as ag,Zi as b,uc as c,ke as d,Di as e,Wi as f,Hi as g,yo as h,xi as i,wi as j,Rs as k,An as l,pt as m,Xi as n,Xs as o,ml as p,en as q,At as r,fc as s,ot as t,po as u,cs as v,pr as w,Gi as x,Te as y,Ji as z}; diff --git a/assets/chunks/theme.edde7d9a.js b/assets/chunks/theme.edde7d9a.js new file mode 100644 index 00000000..2d0e3e50 --- /dev/null +++ b/assets/chunks/theme.edde7d9a.js @@ -0,0 +1,14 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{u as g,v as a,b as i,z as c,a as T,L as x,X as S,_ as m,C as b,O as v,B as f,$ as he,c as et,i as tt,P as nt,w as fe,r as k,d as w,A as F,F as u,N as l,e as z,f as D,o as st,g as ot,q as K,H as J,h as at,j as W,k as te,l as ie,T as rt,M as U,m as C,n as H,p as me,s as Z,t as h,E as De,x as j,V as se,y as ge,D as Fe,G as lt,R as it,I as Oe,J as xe,K as ct,Q as ut,S as dt,U as oe,W as _t,Y as vt,Z as pt,a0 as ht,a1 as ft}from"./framework.0d8bea05.js";const mt=g({__name:"VPBadge",props:{text:{},type:{}},setup(n){return(e,t)=>(a(),i("span",{class:T(["VPBadge",e.type??"tip"])},[c(e.$slots,"default",{},()=>[x(S(e.text),1)],!0)],2))}});const gt=m(mt,[["__scopeId","data-v-1aa52f9a"]]),yt={key:0,class:"VPBackdrop"},bt=g({__name:"VPBackdrop",props:{show:{type:Boolean}},setup(n){return(e,t)=>(a(),b(he,{name:"fade"},{default:v(()=>[e.show?(a(),i("div",yt)):f("",!0)]),_:1}))}});const kt=m(bt,[["__scopeId","data-v-c6055897"]]),P=et;function $t(n,e){let t,s=!1;return()=>{t&&clearTimeout(t),s?t=setTimeout(n,e):(n(),s=!0,setTimeout(()=>{s=!1},e))}}function _e(n){return/^\//.test(n)?n:`/${n}`}function ee(n){if(tt(n))return n.replace(nt,"");const{site:e}=P(),{pathname:t,search:s,hash:o}=new URL(n,"http://a.com"),r=t.endsWith("/")||t.endsWith(".html")?n:n.replace(/(?:(^\.+)\/)?.*$/,`$1${t.replace(/(\.md)?$/,e.value.cleanUrls?"":".html")}${s}${o}`);return fe(r)}function ne({removeCurrent:n=!0,correspondingLink:e=!1}={}){const{site:t,localeIndex:s,page:o,theme:r}=P(),d=k(()=>{var _,y;return{label:(_=t.value.locales[s.value])==null?void 0:_.label,link:((y=t.value.locales[s.value])==null?void 0:y.link)||(s.value==="root"?"/":`/${s.value}/`)}});return{localeLinks:k(()=>Object.entries(t.value.locales).flatMap(([_,y])=>n&&d.value.label===y.label?[]:{text:y.label,link:Pt(y.link||(_==="root"?"/":`/${_}/`),r.value.i18nRouting!==!1&&e,o.value.relativePath.slice(d.value.link.length-1),!t.value.cleanUrls)})),currentLang:d}}function Pt(n,e,t,s){return e?n.replace(/\/$/,"")+_e(t.replace(/(^|\/)index\.md$/,"$1").replace(/\.md$/,s?".html":"")):n}const ce=n=>(z("data-v-35559be6"),n=n(),D(),n),Vt={class:"NotFound"},wt=ce(()=>u("p",{class:"code"},"404",-1)),St=ce(()=>u("h1",{class:"title"},"PAGE NOT FOUND",-1)),Lt=ce(()=>u("div",{class:"divider"},null,-1)),Mt=ce(()=>u("blockquote",{class:"quote"}," But if you don't change your direction, and if you keep looking, you may end up where you are heading. ",-1)),Ct={class:"action"},It=["href"],Tt=g({__name:"NotFound",setup(n){const{site:e}=P(),{localeLinks:t}=ne({removeCurrent:!1}),s=w("/");return F(()=>{var r;const o=window.location.pathname.replace(e.value.base,"").replace(/(^.*?\/).*$/,"/$1");t.value.length&&(s.value=((r=t.value.find(({link:d})=>d.startsWith(o)))==null?void 0:r.link)||t.value[0].link)}),(o,r)=>(a(),i("div",Vt,[wt,St,Lt,Mt,u("div",Ct,[u("a",{class:"link",href:l(fe)(s.value),"aria-label":"go to home"}," Take me home ",8,It)])]))}});const Bt=m(Tt,[["__scopeId","data-v-35559be6"]]);function Ge(n){return st()?(ot(n),!0):!1}function ye(n){return typeof n=="function"?n():l(n)}const At=typeof window<"u",Nt=()=>{};function xt(n){var e;const t=ye(n);return(e=t==null?void 0:t.$el)!=null?e:t}const be=At?window:void 0;function Ht(...n){let e,t,s,o;if(typeof n[0]=="string"||Array.isArray(n[0])?([t,s,o]=n,e=be):[e,t,s,o]=n,!e)return Nt;Array.isArray(t)||(t=[t]),Array.isArray(s)||(s=[s]);const r=[],d=()=>{r.forEach(L=>L()),r.length=0},p=(L,A,I,V)=>(L.addEventListener(A,I,V),()=>L.removeEventListener(A,I,V)),_=K(()=>[xt(e),ye(o)],([L,A])=>{d(),L&&r.push(...t.flatMap(I=>s.map(V=>p(L,I,V,A))))},{immediate:!0,flush:"post"}),y=()=>{_(),d()};return Ge(y),y}function Et(){const n=w(!1);return at()&&F(()=>{n.value=!0}),n}function zt(n){const e=Et();return k(()=>(e.value,!!n()))}function ve(n,e={}){const{window:t=be}=e,s=zt(()=>t&&"matchMedia"in t&&typeof t.matchMedia=="function");let o;const r=w(!1),d=y=>{r.value=y.matches},p=()=>{o&&("removeEventListener"in o?o.removeEventListener("change",d):o.removeListener(d))},_=J(()=>{s.value&&(p(),o=t.matchMedia(ye(n)),"addEventListener"in o?o.addEventListener("change",d):o.addListener(d),r.value=o.matches)});return Ge(()=>{_(),p(),o=void 0}),r}function Re({window:n=be}={}){if(!n)return{x:w(0),y:w(0)};const e=w(n.scrollX),t=w(n.scrollY);return Ht(n,"scroll",()=>{e.value=n.scrollX,t.value=n.scrollY},{capture:!1,passive:!0}),{x:e,y:t}}function Ue(n,e){if(Array.isArray(n))return n;if(n==null)return[];e=_e(e);const t=Object.keys(n).sort((s,o)=>o.split("/").length-s.split("/").length).find(s=>e.startsWith(_e(s)));return t?n[t]:[]}function Dt(n){const e=[];let t=0;for(const s in n){const o=n[s];if(o.items){t=e.push(o);continue}e[t]||e.push({items:[]}),e[t].items.push(o)}return e}function Ft(n){const e=[];function t(s){for(const o of s)o.text&&o.link&&e.push({text:o.text,link:o.link}),o.items&&t(o.items)}return t(n),e}function pe(n,e){return Array.isArray(e)?e.some(t=>pe(n,t)):W(n,e.link)?!0:e.items?pe(n,e.items):!1}function O(){const n=te(),{theme:e,frontmatter:t}=P(),s=ve("(min-width: 960px)"),o=w(!1),r=k(()=>{const N=e.value.sidebar,M=n.data.relativePath;return N?Ue(N,M):[]}),d=k(()=>t.value.sidebar!==!1&&r.value.length>0&&t.value.layout!=="home"),p=k(()=>_?t.value.aside==null?e.value.aside==="left":t.value.aside==="left":!1),_=k(()=>t.value.layout==="home"?!1:t.value.aside!=null?!!t.value.aside:e.value.aside!==!1),y=k(()=>d.value&&s.value),L=k(()=>d.value?Dt(r.value):[]);function A(){o.value=!0}function I(){o.value=!1}function V(){o.value?I():A()}return{isOpen:o,sidebar:r,sidebarGroups:L,hasSidebar:d,hasAside:_,leftAside:p,isSidebarEnabled:y,open:A,close:I,toggle:V}}function Ot(n,e){let t;J(()=>{t=n.value?document.activeElement:void 0}),F(()=>{window.addEventListener("keyup",s)}),ie(()=>{window.removeEventListener("keyup",s)});function s(o){o.key==="Escape"&&n.value&&(e(),t==null||t.focus())}}function Gt(n){const{page:e}=P(),t=w(!1),s=k(()=>n.value.collapsed!=null),o=k(()=>!!n.value.link),r=k(()=>W(e.value.relativePath,n.value.link)),d=k(()=>r.value?!0:n.value.items?pe(e.value.relativePath,n.value.items):!1),p=k(()=>!!(n.value.items&&n.value.items.length));J(()=>{t.value=!!(s.value&&n.value.collapsed)}),J(()=>{(r.value||d.value)&&(t.value=!1)});function _(){s.value&&(t.value=!t.value)}return{collapsed:t,collapsible:s,isLink:o,isActiveLink:r,hasActiveLink:d,hasChildren:p,toggle:_}}function Rt(){const{hasSidebar:n}=O(),e=ve("(min-width: 960px)"),t=ve("(min-width: 1280px)");return{isAsideEnabled:k(()=>!t.value&&!e.value?!1:n.value?t.value:e.value)}}const Ut=71;function ke(n){return typeof n.outline=="object"&&!Array.isArray(n.outline)&&n.outline.label||n.outlineTitle||"On this page"}function $e(n){const e=[...document.querySelectorAll(".VPDoc h2,h3,h4,h5,h6")].filter(t=>t.id&&t.hasChildNodes()).map(t=>{const s=Number(t.tagName[1]);return{title:jt(t),link:"#"+t.id,level:s}});return qt(e,n)}function jt(n){let e="";for(const t of n.childNodes)if(t.nodeType===1){if(t.classList.contains("VPBadge")||t.classList.contains("header-anchor"))continue;e+=t.textContent}else t.nodeType===3&&(e+=t.textContent);return e.trim()}function qt(n,e){if(e===!1)return[];const t=(typeof e=="object"&&!Array.isArray(e)?e.level:e)||2,[s,o]=typeof t=="number"?[t,t]:t==="deep"?[2,6]:t;n=n.filter(d=>d.level>=s&&d.level<=o);const r=[];e:for(let d=0;d=0;_--){const y=n[_];if(y.level{requestAnimationFrame(r),window.addEventListener("scroll",s)}),rt(()=>{d(location.hash)}),ie(()=>{window.removeEventListener("scroll",s)});function r(){if(!t.value)return;const p=[].slice.call(n.value.querySelectorAll(".outline-link")),_=[].slice.call(document.querySelectorAll(".content .header-anchor")).filter(V=>p.some(N=>N.hash===V.hash&&V.offsetParent!==null)),y=window.scrollY,L=window.innerHeight,A=document.body.offsetHeight,I=Math.abs(y+L-A)<1;if(_.length&&I){d(_[_.length-1].hash);return}for(let V=0;V<_.length;V++){const N=_[V],M=_[V+1],[$,B]=Wt(V,N,M);if($){d(B);return}}}function d(p){o&&o.classList.remove("active"),p!==null&&(o=n.value.querySelector(`a[href="${decodeURIComponent(p)}"]`));const _=o;_?(_.classList.add("active"),e.value.style.top=_.offsetTop+33+"px",e.value.style.opacity="1"):(e.value.style.top="33px",e.value.style.opacity="0")}}function He(n){return n.parentElement.offsetTop-Ut}function Wt(n,e,t){const s=window.scrollY;return n===0&&s===0?[!0,null]:s{const o=U("VPDocOutlineItem",!0);return a(),i("ul",{class:T(t.root?"root":"nested")},[(a(!0),i(C,null,H(t.headers,({children:r,link:d,title:p})=>(a(),i("li",null,[u("a",{class:"outline-link",href:d,onClick:e,title:p},S(p),9,Yt),r!=null&&r.length?(a(),b(o,{key:0,headers:r},null,8,["headers"])):f("",!0)]))),256))],2)}}});const Pe=m(Xt,[["__scopeId","data-v-748e15f2"]]),Qt=n=>(z("data-v-bd3a7332"),n=n(),D(),n),Jt={class:"content"},Zt={class:"outline-title"},en={"aria-labelledby":"doc-outline-aria-label"},tn=Qt(()=>u("span",{class:"visually-hidden",id:"doc-outline-aria-label"}," Table of Contents for current page ",-1)),nn=g({__name:"VPDocAsideOutline",setup(n){const{frontmatter:e,theme:t}=P(),s=me([]);Z(()=>{s.value=$e(e.value.outline??t.value.outline)});const o=w(),r=w();return Kt(o,r),(d,p)=>(a(),i("div",{class:T(["VPDocAsideOutline",{"has-outline":s.value.length>0}]),ref_key:"container",ref:o},[u("div",Jt,[u("div",{class:"outline-marker",ref_key:"marker",ref:r},null,512),u("div",Zt,S(l(ke)(l(t))),1),u("nav",en,[tn,h(Pe,{headers:s.value,root:!0},null,8,["headers"])])])],2))}});const sn=m(nn,[["__scopeId","data-v-bd3a7332"]]),on={class:"VPDocAsideCarbonAds"},an=g({__name:"VPDocAsideCarbonAds",props:{carbonAds:{}},setup(n){const e=()=>null;return(t,s)=>(a(),i("div",on,[h(l(e),{"carbon-ads":t.carbonAds},null,8,["carbon-ads"])]))}}),rn=n=>(z("data-v-61b1a3c5"),n=n(),D(),n),ln={class:"VPDocAside"},cn=rn(()=>u("div",{class:"spacer"},null,-1)),un=g({__name:"VPDocAside",setup(n){const{theme:e}=P();return(t,s)=>(a(),i("div",ln,[c(t.$slots,"aside-top",{},void 0,!0),c(t.$slots,"aside-outline-before",{},void 0,!0),h(sn),c(t.$slots,"aside-outline-after",{},void 0,!0),cn,c(t.$slots,"aside-ads-before",{},void 0,!0),l(e).carbonAds?(a(),b(an,{key:0,"carbon-ads":l(e).carbonAds},null,8,["carbon-ads"])):f("",!0),c(t.$slots,"aside-ads-after",{},void 0,!0),c(t.$slots,"aside-bottom",{},void 0,!0)]))}});const dn=m(un,[["__scopeId","data-v-61b1a3c5"]]);function _n(){const{theme:n,page:e}=P();return k(()=>{const{text:t="Edit this page",pattern:s=""}=n.value.editLink||{};let o;return typeof s=="function"?o=s(e.value):o=s.replace(/:path/g,e.value.filePath),{url:o,text:t}})}function vn(){const{page:n,theme:e,frontmatter:t}=P();return k(()=>{var _,y,L,A,I,V;const s=Ue(e.value.sidebar,n.value.relativePath),o=Ft(s),r=o.findIndex(N=>W(n.value.relativePath,N.link)),d=((_=e.value.docFooter)==null?void 0:_.prev)===!1&&!t.value.prev||t.value.prev===!1,p=((y=e.value.docFooter)==null?void 0:y.next)===!1&&!t.value.next||t.value.next===!1;return{prev:d?void 0:{text:(typeof t.value.prev=="string"?t.value.prev:typeof t.value.prev=="object"?t.value.prev.text:void 0)??((L=o[r-1])==null?void 0:L.text),link:(typeof t.value.prev=="object"?t.value.prev.link:void 0)??((A=o[r-1])==null?void 0:A.link)},next:p?void 0:{text:(typeof t.value.next=="string"?t.value.next:typeof t.value.next=="object"?t.value.next.text:void 0)??((I=o[r+1])==null?void 0:I.text),link:(typeof t.value.next=="object"?t.value.next.link:void 0)??((V=o[r+1])==null?void 0:V.link)}}})}const pn={},hn={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},fn=u("path",{d:"M18,23H4c-1.7,0-3-1.3-3-3V6c0-1.7,1.3-3,3-3h7c0.6,0,1,0.4,1,1s-0.4,1-1,1H4C3.4,5,3,5.4,3,6v14c0,0.6,0.4,1,1,1h14c0.6,0,1-0.4,1-1v-7c0-0.6,0.4-1,1-1s1,0.4,1,1v7C21,21.7,19.7,23,18,23z"},null,-1),mn=u("path",{d:"M8,17c-0.3,0-0.5-0.1-0.7-0.3C7,16.5,6.9,16.1,7,15.8l1-4c0-0.2,0.1-0.3,0.3-0.5l9.5-9.5c1.2-1.2,3.2-1.2,4.4,0c1.2,1.2,1.2,3.2,0,4.4l-9.5,9.5c-0.1,0.1-0.3,0.2-0.5,0.3l-4,1C8.2,17,8.1,17,8,17zM9.9,12.5l-0.5,2.1l2.1-0.5l9.3-9.3c0.4-0.4,0.4-1.1,0-1.6c-0.4-0.4-1.2-0.4-1.6,0l0,0L9.9,12.5z M18.5,2.5L18.5,2.5L18.5,2.5z"},null,-1),gn=[fn,mn];function yn(n,e){return a(),i("svg",hn,gn)}const bn=m(pn,[["render",yn]]),G=g({__name:"VPLink",props:{tag:{},href:{},noIcon:{type:Boolean},target:{},rel:{}},setup(n){const e=n,t=k(()=>e.tag??(e.href?"a":"span")),s=k(()=>e.href&&De.test(e.href));return(o,r)=>(a(),b(j(t.value),{class:T(["VPLink",{link:o.href,"vp-external-link-icon":s.value,"no-icon":o.noIcon}]),href:o.href?l(ee)(o.href):void 0,target:o.target??(s.value?"_blank":void 0),rel:o.rel??(s.value?"noreferrer":void 0)},{default:v(()=>[c(o.$slots,"default")]),_:3},8,["class","href","target","rel"]))}}),kn={class:"VPLastUpdated"},$n=["datetime"],Pn=g({__name:"VPDocFooterLastUpdated",setup(n){const{theme:e,page:t}=P(),s=k(()=>new Date(t.value.lastUpdated)),o=k(()=>s.value.toISOString()),r=w("");return F(()=>{J(()=>{var d;r.value=new Intl.DateTimeFormat(void 0,((d=e.value.lastUpdated)==null?void 0:d.formatOptions)??{dateStyle:"short",timeStyle:"short"}).format(s.value)})}),(d,p)=>{var _;return a(),i("p",kn,[x(S(((_=l(e).lastUpdated)==null?void 0:_.text)||l(e).lastUpdatedText||"Last updated")+": ",1),u("time",{datetime:o.value},S(r.value),9,$n)])}}});const Vn=m(Pn,[["__scopeId","data-v-06008453"]]),wn={key:0,class:"VPDocFooter"},Sn={key:0,class:"edit-info"},Ln={key:0,class:"edit-link"},Mn={key:1,class:"last-updated"},Cn={key:1,class:"prev-next"},In={class:"pager"},Tn=["href"],Bn=["innerHTML"],An=["innerHTML"],Nn={class:"pager"},xn=["href"],Hn=["innerHTML"],En=["innerHTML"],zn=g({__name:"VPDocFooter",setup(n){const{theme:e,page:t,frontmatter:s}=P(),o=_n(),r=vn(),d=k(()=>e.value.editLink&&s.value.editLink!==!1),p=k(()=>t.value.lastUpdated&&s.value.lastUpdated!==!1),_=k(()=>d.value||p.value||r.value.prev||r.value.next);return(y,L)=>{var A,I,V,N,M,$;return _.value?(a(),i("footer",wn,[c(y.$slots,"doc-footer-before",{},void 0,!0),d.value||p.value?(a(),i("div",Sn,[d.value?(a(),i("div",Ln,[h(G,{class:"edit-link-button",href:l(o).url,"no-icon":!0},{default:v(()=>[h(bn,{class:"edit-link-icon","aria-label":"edit icon"}),x(" "+S(l(o).text),1)]),_:1},8,["href"])])):f("",!0),p.value?(a(),i("div",Mn,[h(Vn)])):f("",!0)])):f("",!0),(A=l(r).prev)!=null&&A.link||(I=l(r).next)!=null&&I.link?(a(),i("nav",Cn,[u("div",In,[(V=l(r).prev)!=null&&V.link?(a(),i("a",{key:0,class:"pager-link prev",href:l(ee)(l(r).prev.link)},[u("span",{class:"desc",innerHTML:((N=l(e).docFooter)==null?void 0:N.prev)||"Previous page"},null,8,Bn),u("span",{class:"title",innerHTML:l(r).prev.text},null,8,An)],8,Tn)):f("",!0)]),u("div",Nn,[(M=l(r).next)!=null&&M.link?(a(),i("a",{key:0,class:"pager-link next",href:l(ee)(l(r).next.link)},[u("span",{class:"desc",innerHTML:(($=l(e).docFooter)==null?void 0:$.next)||"Next page"},null,8,Hn),u("span",{class:"title",innerHTML:l(r).next.text},null,8,En)],8,xn)):f("",!0)])])):f("",!0)])):f("",!0)}}});const Dn=m(zn,[["__scopeId","data-v-4a75099f"]]),Fn={},On={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},Gn=u("path",{d:"M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"},null,-1),Rn=[Gn];function Un(n,e){return a(),i("svg",On,Rn)}const Ve=m(Fn,[["render",Un]]),jn={key:0,class:"VPDocOutlineDropdown"},qn={key:0,class:"items"},Kn=g({__name:"VPDocOutlineDropdown",setup(n){const{frontmatter:e,theme:t}=P(),s=w(!1);Z(()=>{s.value=!1});const o=me([]);return Z(()=>{o.value=$e(e.value.outline??t.value.outline)}),(r,d)=>o.value.length>0?(a(),i("div",jn,[u("button",{onClick:d[0]||(d[0]=p=>s.value=!s.value),class:T({open:s.value})},[x(S(l(ke)(l(t)))+" ",1),h(Ve,{class:"icon"})],2),s.value?(a(),i("div",qn,[h(Pe,{headers:o.value},null,8,["headers"])])):f("",!0)])):f("",!0)}});const Wn=m(Kn,[["__scopeId","data-v-ff90d418"]]),Yn=n=>(z("data-v-ff46f99c"),n=n(),D(),n),Xn={class:"container"},Qn=Yn(()=>u("div",{class:"aside-curtain"},null,-1)),Jn={class:"aside-container"},Zn={class:"aside-content"},es={class:"content"},ts={class:"content-container"},ns={class:"main"},ss=g({__name:"VPDoc",setup(n){const{theme:e}=P(),t=te(),{hasSidebar:s,hasAside:o,leftAside:r}=O(),d=k(()=>t.path.replace(/[./]+/g,"_").replace(/_html$/,""));return(p,_)=>{const y=U("Content");return a(),i("div",{class:T(["VPDoc",{"has-sidebar":l(s),"has-aside":l(o)}])},[c(p.$slots,"doc-top",{},void 0,!0),u("div",Xn,[l(o)?(a(),i("div",{key:0,class:T(["aside",{"left-aside":l(r)}])},[Qn,u("div",Jn,[u("div",Zn,[h(dn,null,{"aside-top":v(()=>[c(p.$slots,"aside-top",{},void 0,!0)]),"aside-bottom":v(()=>[c(p.$slots,"aside-bottom",{},void 0,!0)]),"aside-outline-before":v(()=>[c(p.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":v(()=>[c(p.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":v(()=>[c(p.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":v(()=>[c(p.$slots,"aside-ads-after",{},void 0,!0)]),_:3})])])],2)):f("",!0),u("div",es,[u("div",ts,[c(p.$slots,"doc-before",{},void 0,!0),h(Wn),u("main",ns,[h(y,{class:T(["vp-doc",[d.value,l(e).externalLinkIcon&&"external-link-icon-enabled"]])},null,8,["class"])]),h(Dn,null,{"doc-footer-before":v(()=>[c(p.$slots,"doc-footer-before",{},void 0,!0)]),_:3}),c(p.$slots,"doc-after",{},void 0,!0)])])]),c(p.$slots,"doc-bottom",{},void 0,!0)],2)}}});const os=m(ss,[["__scopeId","data-v-ff46f99c"]]),as=g({__name:"VPButton",props:{tag:{},size:{},theme:{},text:{},href:{}},setup(n){const e=n,t=k(()=>[e.size??"medium",e.theme??"brand"]),s=k(()=>e.href&&De.test(e.href)),o=k(()=>e.tag?e.tag:e.href?"a":"button");return(r,d)=>(a(),b(j(o.value),{class:T(["VPButton",t.value]),href:r.href?l(ee)(r.href):void 0,target:s.value?"_blank":void 0,rel:s.value?"noreferrer":void 0},{default:v(()=>[x(S(r.text),1)]),_:1},8,["class","href","target","rel"]))}});const rs=m(as,[["__scopeId","data-v-fceb97bb"]]),ls=["src","alt"],is={inheritAttrs:!1},cs=g({...is,__name:"VPImage",props:{image:{},alt:{}},setup(n){return(e,t)=>{const s=U("VPImage",!0);return e.image?(a(),i(C,{key:0},[typeof e.image=="string"||"src"in e.image?(a(),i("img",se({key:0,class:"VPImage"},typeof e.image=="string"?e.$attrs:{...e.image,...e.$attrs},{src:l(fe)(typeof e.image=="string"?e.image:e.image.src),alt:e.alt??(typeof e.image=="string"?"":e.image.alt||"")}),null,16,ls)):(a(),i(C,{key:1},[h(s,se({class:"dark",image:e.image.dark,alt:e.image.alt},e.$attrs),null,16,["image","alt"]),h(s,se({class:"light",image:e.image.light,alt:e.image.alt},e.$attrs),null,16,["image","alt"])],64))],64)):f("",!0)}}});const we=m(cs,[["__scopeId","data-v-d79cd251"]]),us=n=>(z("data-v-d29a3d56"),n=n(),D(),n),ds={class:"container"},_s={class:"main"},vs={key:0,class:"name"},ps=["innerHTML"],hs=["innerHTML"],fs=["innerHTML"],ms={key:0,class:"actions"},gs={key:0,class:"image"},ys={class:"image-container"},bs=us(()=>u("div",{class:"image-bg"},null,-1)),ks=g({__name:"VPHero",props:{name:{},text:{},tagline:{},image:{},actions:{}},setup(n){const e=ge("hero-image-slot-exists");return(t,s)=>(a(),i("div",{class:T(["VPHero",{"has-image":t.image||l(e)}])},[u("div",ds,[u("div",_s,[c(t.$slots,"home-hero-info",{},()=>[t.name?(a(),i("h1",vs,[u("span",{innerHTML:t.name,class:"clip"},null,8,ps)])):f("",!0),t.text?(a(),i("p",{key:1,innerHTML:t.text,class:"text"},null,8,hs)):f("",!0),t.tagline?(a(),i("p",{key:2,innerHTML:t.tagline,class:"tagline"},null,8,fs)):f("",!0)],!0),t.actions?(a(),i("div",ms,[(a(!0),i(C,null,H(t.actions,o=>(a(),i("div",{key:o.link,class:"action"},[h(rs,{tag:"a",size:"medium",theme:o.theme,text:o.text,href:o.link},null,8,["theme","text","href"])]))),128))])):f("",!0)]),t.image||l(e)?(a(),i("div",gs,[u("div",ys,[bs,c(t.$slots,"home-hero-image",{},()=>[t.image?(a(),b(we,{key:0,class:"image-src",image:t.image},null,8,["image"])):f("",!0)],!0)])])):f("",!0)])],2))}});const $s=m(ks,[["__scopeId","data-v-d29a3d56"]]),Ps=g({__name:"VPHomeHero",setup(n){const{frontmatter:e}=P();return(t,s)=>l(e).hero?(a(),b($s,{key:0,class:"VPHomeHero",name:l(e).hero.name,text:l(e).hero.text,tagline:l(e).hero.tagline,image:l(e).hero.image,actions:l(e).hero.actions},{"home-hero-info":v(()=>[c(t.$slots,"home-hero-info")]),"home-hero-image":v(()=>[c(t.$slots,"home-hero-image")]),_:3},8,["name","text","tagline","image","actions"])):f("",!0)}}),Vs={},ws={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Ss=u("path",{d:"M19.9,12.4c0.1-0.2,0.1-0.5,0-0.8c-0.1-0.1-0.1-0.2-0.2-0.3l-7-7c-0.4-0.4-1-0.4-1.4,0s-0.4,1,0,1.4l5.3,5.3H5c-0.6,0-1,0.4-1,1s0.4,1,1,1h11.6l-5.3,5.3c-0.4,0.4-0.4,1,0,1.4c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3l7-7C19.8,12.6,19.9,12.5,19.9,12.4z"},null,-1),Ls=[Ss];function Ms(n,e){return a(),i("svg",ws,Ls)}const Cs=m(Vs,[["render",Ms]]),Is={class:"box"},Ts=["innerHTML"],Bs=["innerHTML"],As=["innerHTML"],Ns={key:3,class:"link-text"},xs={class:"link-text-value"},Hs=g({__name:"VPFeature",props:{icon:{},title:{},details:{},link:{},linkText:{},rel:{}},setup(n){return(e,t)=>(a(),b(G,{class:"VPFeature",href:e.link,rel:e.rel,"no-icon":!0,tag:e.link?"a":"div"},{default:v(()=>[u("article",Is,[typeof e.icon=="object"?(a(),b(we,{key:0,image:e.icon,alt:e.icon.alt,height:e.icon.height,width:e.icon.width},null,8,["image","alt","height","width"])):e.icon?(a(),i("div",{key:1,class:"icon",innerHTML:e.icon},null,8,Ts)):f("",!0),u("h2",{class:"title",innerHTML:e.title},null,8,Bs),e.details?(a(),i("p",{key:2,class:"details",innerHTML:e.details},null,8,As)):f("",!0),e.linkText?(a(),i("div",Ns,[u("p",xs,[x(S(e.linkText)+" ",1),h(Cs,{class:"link-text-icon"})])])):f("",!0)])]),_:1},8,["href","rel","tag"]))}});const Es=m(Hs,[["__scopeId","data-v-cf6e479c"]]),zs={key:0,class:"VPFeatures"},Ds={class:"container"},Fs={class:"items"},Os=g({__name:"VPFeatures",props:{features:{}},setup(n){const e=n,t=k(()=>{const s=e.features.length;if(s){if(s===2)return"grid-2";if(s===3)return"grid-3";if(s%3===0)return"grid-6";if(s>3)return"grid-4"}else return});return(s,o)=>s.features?(a(),i("div",zs,[u("div",Ds,[u("div",Fs,[(a(!0),i(C,null,H(s.features,r=>(a(),i("div",{key:r.title,class:T(["item",[t.value]])},[h(Es,{icon:r.icon,title:r.title,details:r.details,link:r.link,"link-text":r.linkText,rel:r.rel},null,8,["icon","title","details","link","link-text","rel"])],2))),128))])])])):f("",!0)}});const Gs=m(Os,[["__scopeId","data-v-8ce6a87c"]]),Rs=g({__name:"VPHomeFeatures",setup(n){const{frontmatter:e}=P();return(t,s)=>l(e).features?(a(),b(Gs,{key:0,class:"VPHomeFeatures",features:l(e).features},null,8,["features"])):f("",!0)}}),Us={class:"VPHome"},js=g({__name:"VPHome",setup(n){return(e,t)=>{const s=U("Content");return a(),i("div",Us,[c(e.$slots,"home-hero-before",{},void 0,!0),h(Ps,null,{"home-hero-info":v(()=>[c(e.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-image":v(()=>[c(e.$slots,"home-hero-image",{},void 0,!0)]),_:3}),c(e.$slots,"home-hero-after",{},void 0,!0),c(e.$slots,"home-features-before",{},void 0,!0),h(Rs),c(e.$slots,"home-features-after",{},void 0,!0),h(s)])}}});const qs=m(js,[["__scopeId","data-v-5d1897af"]]),Ks={},Ws={class:"VPPage"};function Ys(n,e){const t=U("Content");return a(),i("div",Ws,[c(n.$slots,"page-top"),h(t),c(n.$slots,"page-bottom")])}const Xs=m(Ks,[["render",Ys]]),Qs=g({__name:"VPContent",setup(n){const{page:e,frontmatter:t}=P(),{hasSidebar:s}=O();return(o,r)=>(a(),i("div",{class:T(["VPContent",{"has-sidebar":l(s),"is-home":l(t).layout==="home"}]),id:"VPContent"},[l(e).isNotFound?c(o.$slots,"not-found",{key:0},()=>[h(Bt)],!0):l(t).layout==="page"?(a(),b(Xs,{key:1},{"page-top":v(()=>[c(o.$slots,"page-top",{},void 0,!0)]),"page-bottom":v(()=>[c(o.$slots,"page-bottom",{},void 0,!0)]),_:3})):l(t).layout==="home"?(a(),b(qs,{key:2},{"home-hero-before":v(()=>[c(o.$slots,"home-hero-before",{},void 0,!0)]),"home-hero-info":v(()=>[c(o.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-image":v(()=>[c(o.$slots,"home-hero-image",{},void 0,!0)]),"home-hero-after":v(()=>[c(o.$slots,"home-hero-after",{},void 0,!0)]),"home-features-before":v(()=>[c(o.$slots,"home-features-before",{},void 0,!0)]),"home-features-after":v(()=>[c(o.$slots,"home-features-after",{},void 0,!0)]),_:3})):(a(),b(os,{key:3},{"doc-top":v(()=>[c(o.$slots,"doc-top",{},void 0,!0)]),"doc-bottom":v(()=>[c(o.$slots,"doc-bottom",{},void 0,!0)]),"doc-footer-before":v(()=>[c(o.$slots,"doc-footer-before",{},void 0,!0)]),"doc-before":v(()=>[c(o.$slots,"doc-before",{},void 0,!0)]),"doc-after":v(()=>[c(o.$slots,"doc-after",{},void 0,!0)]),"aside-top":v(()=>[c(o.$slots,"aside-top",{},void 0,!0)]),"aside-outline-before":v(()=>[c(o.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":v(()=>[c(o.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":v(()=>[c(o.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":v(()=>[c(o.$slots,"aside-ads-after",{},void 0,!0)]),"aside-bottom":v(()=>[c(o.$slots,"aside-bottom",{},void 0,!0)]),_:3}))],2))}});const Js=m(Qs,[["__scopeId","data-v-35450758"]]),Zs={class:"container"},eo=["innerHTML"],to=["innerHTML"],no=g({__name:"VPFooter",setup(n){const{theme:e,frontmatter:t}=P(),{hasSidebar:s}=O();return(o,r)=>l(e).footer&&l(t).footer!==!1?(a(),i("footer",{key:0,class:T(["VPFooter",{"has-sidebar":l(s)}])},[u("div",Zs,[l(e).footer.message?(a(),i("p",{key:0,class:"message",innerHTML:l(e).footer.message},null,8,eo)):f("",!0),l(e).footer.copyright?(a(),i("p",{key:1,class:"copyright",innerHTML:l(e).footer.copyright},null,8,to)):f("",!0)])],2)):f("",!0)}});const so=m(no,[["__scopeId","data-v-a51421e3"]]),oo=g({__name:"VPLocalNavOutlineDropdown",props:{headers:{},navHeight:{}},setup(n){const e=n,{theme:t}=P(),s=w(!1),o=w(0),r=w();Z(()=>{s.value=!1});function d(){s.value=!s.value,o.value=window.innerHeight+Math.min(window.scrollY-e.navHeight,0)}function p(y){y.target.classList.contains("outline-link")&&(r.value&&(r.value.style.transition="none"),lt(()=>{s.value=!1}))}function _(){s.value=!1,window.scrollTo({top:0,left:0,behavior:"smooth"})}return(y,L)=>(a(),i("div",{class:"VPLocalNavOutlineDropdown",style:Fe({"--vp-vh":o.value+"px"})},[y.headers.length>0?(a(),i("button",{key:0,onClick:d,class:T({open:s.value})},[x(S(l(ke)(l(t)))+" ",1),h(Ve,{class:"icon"})],2)):(a(),i("button",{key:1,onClick:_},S(l(t).returnToTopLabel||"Return to top"),1)),h(he,{name:"flyout"},{default:v(()=>[s.value?(a(),i("div",{key:0,ref_key:"items",ref:r,class:"items",onClick:p},[u("a",{class:"top-link",href:"#",onClick:_},S(l(t).returnToTopLabel||"Return to top"),1),h(Pe,{headers:y.headers},null,8,["headers"])],512)):f("",!0)]),_:1})],4))}});const ao=m(oo,[["__scopeId","data-v-e7d97842"]]),ro={},lo={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},io=u("path",{d:"M17,11H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S17.6,11,17,11z"},null,-1),co=u("path",{d:"M21,7H3C2.4,7,2,6.6,2,6s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,7,21,7z"},null,-1),uo=u("path",{d:"M21,15H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,15,21,15z"},null,-1),_o=u("path",{d:"M17,19H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S17.6,19,17,19z"},null,-1),vo=[io,co,uo,_o];function po(n,e){return a(),i("svg",lo,vo)}const ho=m(ro,[["render",po]]),fo=["aria-expanded"],mo={class:"menu-text"},go=g({__name:"VPLocalNav",props:{open:{type:Boolean}},emits:["open-menu"],setup(n){const{theme:e,frontmatter:t}=P(),{hasSidebar:s}=O(),{y:o}=Re(),r=me([]),d=w(0);F(()=>{d.value=parseInt(getComputedStyle(document.documentElement).getPropertyValue("--vp-nav-height"))}),Z(()=>{r.value=$e(t.value.outline??e.value.outline)});const p=k(()=>r.value.length===0&&!s.value),_=k(()=>({VPLocalNav:!0,fixed:p.value,"reached-top":o.value>=d.value}));return(y,L)=>l(t).layout!=="home"&&(!p.value||l(o)>=d.value)?(a(),i("div",{key:0,class:T(_.value)},[l(s)?(a(),i("button",{key:0,class:"menu","aria-expanded":y.open,"aria-controls":"VPSidebarNav",onClick:L[0]||(L[0]=A=>y.$emit("open-menu"))},[h(ho,{class:"menu-icon"}),u("span",mo,S(l(e).sidebarMenuLabel||"Menu"),1)],8,fo)):f("",!0),h(ao,{headers:r.value,navHeight:d.value},null,8,["headers","navHeight"])],2)):f("",!0)}});const yo=m(go,[["__scopeId","data-v-341c3ee5"]]);function bo(){const n=w(!1);function e(){n.value=!0,window.addEventListener("resize",o)}function t(){n.value=!1,window.removeEventListener("resize",o)}function s(){n.value?t():e()}function o(){window.outerWidth>=768&&t()}const r=te();return K(()=>r.path,t),{isScreenOpen:n,openScreen:e,closeScreen:t,toggleScreen:s}}const ko={},$o={class:"VPSwitch",type:"button",role:"switch"},Po={class:"check"},Vo={key:0,class:"icon"};function wo(n,e){return a(),i("button",$o,[u("span",Po,[n.$slots.default?(a(),i("span",Vo,[c(n.$slots,"default",{},void 0,!0)])):f("",!0)])])}const So=m(ko,[["render",wo],["__scopeId","data-v-40b19bac"]]),Lo={},Mo={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},Co=it('',9),Io=[Co];function To(n,e){return a(),i("svg",Mo,Io)}const Bo=m(Lo,[["render",To]]),Ao={},No={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},xo=u("path",{d:"M12.1,22c-0.3,0-0.6,0-0.9,0c-5.5-0.5-9.5-5.4-9-10.9c0.4-4.8,4.2-8.6,9-9c0.4,0,0.8,0.2,1,0.5c0.2,0.3,0.2,0.8-0.1,1.1c-2,2.7-1.4,6.4,1.3,8.4c2.1,1.6,5,1.6,7.1,0c0.3-0.2,0.7-0.3,1.1-0.1c0.3,0.2,0.5,0.6,0.5,1c-0.2,2.7-1.5,5.1-3.6,6.8C16.6,21.2,14.4,22,12.1,22zM9.3,4.4c-2.9,1-5,3.6-5.2,6.8c-0.4,4.4,2.8,8.3,7.2,8.7c2.1,0.2,4.2-0.4,5.8-1.8c1.1-0.9,1.9-2.1,2.4-3.4c-2.5,0.9-5.3,0.5-7.5-1.1C9.2,11.4,8.1,7.7,9.3,4.4z"},null,-1),Ho=[xo];function Eo(n,e){return a(),i("svg",No,Ho)}const zo=m(Ao,[["render",Eo]]),Do=g({__name:"VPSwitchAppearance",setup(n){const{site:e,isDark:t}=P(),s=w(!1),o=Oe?r():()=>{};F(()=>{s.value=document.documentElement.classList.contains("dark")});function r(){const d=window.matchMedia("(prefers-color-scheme: dark)"),p=document.documentElement.classList;let _=localStorage.getItem(xe),y=e.value.appearance==="dark"&&_==null||(_==="auto"||_==null?d.matches:_==="dark");d.onchange=I=>{_==="auto"&&A(y=I.matches)};function L(){A(y=!y),_=y?d.matches?"auto":"dark":d.matches?"light":"auto",localStorage.setItem(xe,_)}function A(I){const V=document.createElement("style");V.type="text/css",V.appendChild(document.createTextNode(`:not(.VPSwitchAppearance):not(.VPSwitchAppearance *) { + -webkit-transition: none !important; + -moz-transition: none !important; + -o-transition: none !important; + -ms-transition: none !important; + transition: none !important; +}`)),document.head.appendChild(V),s.value=I,p[I?"add":"remove"]("dark"),window.getComputedStyle(V).opacity,document.head.removeChild(V)}return L}return K(s,d=>{t.value=d}),(d,p)=>(a(),b(So,{title:"toggle dark mode",class:"VPSwitchAppearance","aria-checked":s.value,onClick:l(o)},{default:v(()=>[h(Bo,{class:"sun"}),h(zo,{class:"moon"})]),_:1},8,["aria-checked","onClick"]))}});const Se=m(Do,[["__scopeId","data-v-535a86eb"]]),Fo={key:0,class:"VPNavBarAppearance"},Oo=g({__name:"VPNavBarAppearance",setup(n){const{site:e}=P();return(t,s)=>l(e).appearance?(a(),i("div",Fo,[h(Se)])):f("",!0)}});const Go=m(Oo,[["__scopeId","data-v-c4c830a7"]]),Le=w();let je=!1,de=0;function Ro(n){const e=w(!1);if(Oe){!je&&Uo(),de++;const t=K(Le,s=>{var o,r,d;s===n.el.value||(o=n.el.value)!=null&&o.contains(s)?(e.value=!0,(r=n.onFocus)==null||r.call(n)):(e.value=!1,(d=n.onBlur)==null||d.call(n))});ie(()=>{t(),de--,de||jo()})}return ct(e)}function Uo(){document.addEventListener("focusin",qe),je=!0,Le.value=document.activeElement}function jo(){document.removeEventListener("focusin",qe)}function qe(){Le.value=document.activeElement}const qo={},Ko={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},Wo=u("path",{d:"M12,16c-0.3,0-0.5-0.1-0.7-0.3l-6-6c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l5.3,5.3l5.3-5.3c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-6,6C12.5,15.9,12.3,16,12,16z"},null,-1),Yo=[Wo];function Xo(n,e){return a(),i("svg",Ko,Yo)}const Ke=m(qo,[["render",Xo]]),Qo={},Jo={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},Zo=u("circle",{cx:"12",cy:"12",r:"2"},null,-1),ea=u("circle",{cx:"19",cy:"12",r:"2"},null,-1),ta=u("circle",{cx:"5",cy:"12",r:"2"},null,-1),na=[Zo,ea,ta];function sa(n,e){return a(),i("svg",Jo,na)}const oa=m(Qo,[["render",sa]]),aa={class:"VPMenuLink"},ra=g({__name:"VPMenuLink",props:{item:{}},setup(n){const{page:e}=P();return(t,s)=>(a(),i("div",aa,[h(G,{class:T({active:l(W)(l(e).relativePath,t.item.activeMatch||t.item.link,!!t.item.activeMatch)}),href:t.item.link,target:t.item.target,rel:t.item.rel},{default:v(()=>[x(S(t.item.text),1)]),_:1},8,["class","href","target","rel"])]))}});const ue=m(ra,[["__scopeId","data-v-2b1427af"]]),la={class:"VPMenuGroup"},ia={key:0,class:"title"},ca=g({__name:"VPMenuGroup",props:{text:{},items:{}},setup(n){return(e,t)=>(a(),i("div",la,[e.text?(a(),i("p",ia,S(e.text),1)):f("",!0),(a(!0),i(C,null,H(e.items,s=>(a(),i(C,null,["link"in s?(a(),b(ue,{key:0,item:s},null,8,["item"])):f("",!0)],64))),256))]))}});const ua=m(ca,[["__scopeId","data-v-f69155d8"]]),da={class:"VPMenu"},_a={key:0,class:"items"},va=g({__name:"VPMenu",props:{items:{}},setup(n){return(e,t)=>(a(),i("div",da,[e.items?(a(),i("div",_a,[(a(!0),i(C,null,H(e.items,s=>(a(),i(C,{key:s.text},["link"in s?(a(),b(ue,{key:0,item:s},null,8,["item"])):(a(),b(ua,{key:1,text:s.text,items:s.items},null,8,["text","items"]))],64))),128))])):f("",!0),c(e.$slots,"default",{},void 0,!0)]))}});const pa=m(va,[["__scopeId","data-v-bcc9fed1"]]),ha=["aria-expanded","aria-label"],fa={key:0,class:"text"},ma={class:"menu"},ga=g({__name:"VPFlyout",props:{icon:{},button:{},label:{},items:{}},setup(n){const e=w(!1),t=w();Ro({el:t,onBlur:s});function s(){e.value=!1}return(o,r)=>(a(),i("div",{class:"VPFlyout",ref_key:"el",ref:t,onMouseenter:r[1]||(r[1]=d=>e.value=!0),onMouseleave:r[2]||(r[2]=d=>e.value=!1)},[u("button",{type:"button",class:"button","aria-haspopup":"true","aria-expanded":e.value,"aria-label":o.label,onClick:r[0]||(r[0]=d=>e.value=!e.value)},[o.button||o.icon?(a(),i("span",fa,[o.icon?(a(),b(j(o.icon),{key:0,class:"option-icon"})):f("",!0),x(" "+S(o.button)+" ",1),h(Ke,{class:"text-icon"})])):(a(),b(oa,{key:1,class:"icon"}))],8,ha),u("div",ma,[h(pa,{items:o.items},{default:v(()=>[c(o.$slots,"default",{},void 0,!0)]),_:3},8,["items"])])],544))}});const Me=m(ga,[["__scopeId","data-v-87f2bbca"]]),ya={discord:'Discord',facebook:'Facebook',github:'GitHub',instagram:'Instagram',linkedin:'LinkedIn',mastodon:'Mastodon',slack:'Slack',twitter:'Twitter',youtube:'YouTube'},ba=["href","aria-label","innerHTML"],ka=g({__name:"VPSocialLink",props:{icon:{},link:{},ariaLabel:{}},setup(n){const e=n,t=k(()=>typeof e.icon=="object"?e.icon.svg:ya[e.icon]);return(s,o)=>(a(),i("a",{class:"VPSocialLink no-icon",href:s.link,"aria-label":s.ariaLabel??(typeof s.icon=="string"?s.icon:""),target:"_blank",rel:"noopener",innerHTML:t.value},null,8,ba))}});const $a=m(ka,[["__scopeId","data-v-39e5dc3a"]]),Pa={class:"VPSocialLinks"},Va=g({__name:"VPSocialLinks",props:{links:{}},setup(n){return(e,t)=>(a(),i("div",Pa,[(a(!0),i(C,null,H(e.links,({link:s,icon:o,ariaLabel:r})=>(a(),b($a,{key:s,icon:o,link:s,ariaLabel:r},null,8,["icon","link","ariaLabel"]))),128))]))}});const Ce=m(Va,[["__scopeId","data-v-726a530e"]]),wa={key:0,class:"group translations"},Sa={class:"trans-title"},La={key:1,class:"group"},Ma={class:"item appearance"},Ca={class:"label"},Ia={class:"appearance-action"},Ta={key:2,class:"group"},Ba={class:"item social-links"},Aa=g({__name:"VPNavBarExtra",setup(n){const{site:e,theme:t}=P(),{localeLinks:s,currentLang:o}=ne({correspondingLink:!0}),r=k(()=>s.value.length&&o.value.label||e.value.appearance||t.value.socialLinks);return(d,p)=>r.value?(a(),b(Me,{key:0,class:"VPNavBarExtra",label:"extra navigation"},{default:v(()=>[l(s).length&&l(o).label?(a(),i("div",wa,[u("p",Sa,S(l(o).label),1),(a(!0),i(C,null,H(l(s),_=>(a(),b(ue,{key:_.link,item:_},null,8,["item"]))),128))])):f("",!0),l(e).appearance?(a(),i("div",La,[u("div",Ma,[u("p",Ca,S(l(t).darkModeSwitchLabel||"Appearance"),1),u("div",Ia,[h(Se)])])])):f("",!0),l(t).socialLinks?(a(),i("div",Ta,[u("div",Ba,[h(Ce,{class:"social-links-list",links:l(t).socialLinks},null,8,["links"])])])):f("",!0)]),_:1})):f("",!0)}});const Na=m(Aa,[["__scopeId","data-v-4b777cbc"]]),xa=n=>(z("data-v-1363b7e8"),n=n(),D(),n),Ha=["aria-expanded"],Ea=xa(()=>u("span",{class:"container"},[u("span",{class:"top"}),u("span",{class:"middle"}),u("span",{class:"bottom"})],-1)),za=[Ea],Da=g({__name:"VPNavBarHamburger",props:{active:{type:Boolean}},emits:["click"],setup(n){return(e,t)=>(a(),i("button",{type:"button",class:T(["VPNavBarHamburger",{active:e.active}]),"aria-label":"mobile navigation","aria-expanded":e.active,"aria-controls":"VPNavScreen",onClick:t[0]||(t[0]=s=>e.$emit("click"))},za,10,Ha))}});const Fa=m(Da,[["__scopeId","data-v-1363b7e8"]]),Oa=g({__name:"VPNavBarMenuLink",props:{item:{}},setup(n){const{page:e}=P();return(t,s)=>(a(),b(G,{class:T({VPNavBarMenuLink:!0,active:l(W)(l(e).relativePath,t.item.activeMatch||t.item.link,!!t.item.activeMatch)}),href:t.item.link,target:t.item.target,rel:t.item.rel,tabindex:"0"},{default:v(()=>[x(S(t.item.text),1)]),_:1},8,["class","href","target","rel"]))}});const Ga=m(Oa,[["__scopeId","data-v-6b3352fb"]]),Ra=g({__name:"VPNavBarMenuGroup",props:{item:{}},setup(n){const{page:e}=P();return(t,s)=>(a(),b(Me,{class:T({VPNavBarMenuGroup:!0,active:l(W)(l(e).relativePath,t.item.activeMatch,!!t.item.activeMatch)}),button:t.item.text,items:t.item.items},null,8,["class","button","items"]))}}),Ua=n=>(z("data-v-bc887b7f"),n=n(),D(),n),ja={key:0,"aria-labelledby":"main-nav-aria-label",class:"VPNavBarMenu"},qa=Ua(()=>u("span",{id:"main-nav-aria-label",class:"visually-hidden"},"Main Navigation",-1)),Ka=g({__name:"VPNavBarMenu",setup(n){const{theme:e}=P();return(t,s)=>l(e).nav?(a(),i("nav",ja,[qa,(a(!0),i(C,null,H(l(e).nav,o=>(a(),i(C,{key:o.text},["link"in o?(a(),b(Ga,{key:0,item:o},null,8,["item"])):(a(),b(Ra,{key:1,item:o},null,8,["item"]))],64))),128))])):f("",!0)}});const Wa=m(Ka,[["__scopeId","data-v-bc887b7f"]]);const Ya={type:"button",class:"DocSearch DocSearch-Button","aria-label":"Search"},Xa={class:"DocSearch-Button-Container"},Qa=u("svg",{class:"DocSearch-Search-Icon",width:"20",height:"20",viewBox:"0 0 20 20","aria-label":"search icon"},[u("path",{d:"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z",stroke:"currentColor",fill:"none","fill-rule":"evenodd","stroke-linecap":"round","stroke-linejoin":"round"})],-1),Ja={class:"DocSearch-Button-Placeholder"},Za=u("span",{class:"DocSearch-Button-Keys"},[u("kbd",{class:"DocSearch-Button-Key"}),u("kbd",{class:"DocSearch-Button-Key"},"K")],-1),Ee=g({__name:"VPNavBarSearchButton",props:{placeholder:{}},setup(n){return(e,t)=>(a(),i("button",Ya,[u("span",Xa,[Qa,u("span",Ja,S(e.placeholder),1)]),Za]))}});const er={id:"local-search"},tr={key:1,id:"docsearch"},nr=g({__name:"VPNavBarSearch",setup(n){const e=()=>null,t=ut(()=>dt(()=>import("./VPAlgoliaSearchBox.661ef68a.js"),["assets/chunks/VPAlgoliaSearchBox.661ef68a.js","assets/chunks/framework.0d8bea05.js"])),{theme:s,localeIndex:o}=P(),r=w(!1),d=w(!1),p=k(()=>{var $,B,E,Y,Be,Ae,Ne;const M=(($=s.value.search)==null?void 0:$.options)??s.value.algolia;return((Be=(Y=(E=(B=M==null?void 0:M.locales)==null?void 0:B[o.value])==null?void 0:E.translations)==null?void 0:Y.button)==null?void 0:Be.buttonText)||((Ne=(Ae=M==null?void 0:M.translations)==null?void 0:Ae.button)==null?void 0:Ne.buttonText)||"Search"}),_=()=>{const M="VPAlgoliaPreconnect";(window.requestIdleCallback||setTimeout)(()=>{var E;const B=document.createElement("link");B.id=M,B.rel="preconnect",B.href=`https://${(((E=s.value.search)==null?void 0:E.options)??s.value.algolia).appId}-dsn.algolia.net`,B.crossOrigin="",document.head.appendChild(B)})};F(()=>{_();const M=B=>{(B.key.toLowerCase()==="k"&&(B.metaKey||B.ctrlKey)||!A(B)&&B.key==="/")&&(B.preventDefault(),y(),$())},$=()=>{window.removeEventListener("keydown",M)};window.addEventListener("keydown",M),ie($)});function y(){r.value||(r.value=!0,setTimeout(L,16))}function L(){const M=new Event("keydown");M.key="k",M.metaKey=!0,window.dispatchEvent(M),setTimeout(()=>{document.querySelector(".DocSearch-Modal")||L()},16)}function A(M){const $=M.target,B=$.tagName;return $.isContentEditable||B==="INPUT"||B==="SELECT"||B==="TEXTAREA"}const I=w(!1),V=w("'Meta'");F(()=>{V.value=/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)?"'⌘'":"'Ctrl'"});const N="algolia";return(M,$)=>{var B;return a(),i("div",{class:"VPNavBarSearch",style:Fe({"--vp-meta-key":V.value})},[l(N)==="local"?(a(),i(C,{key:0},[I.value?(a(),b(l(e),{key:0,placeholder:p.value,onClose:$[0]||($[0]=E=>I.value=!1)},null,8,["placeholder"])):f("",!0),u("div",er,[h(Ee,{placeholder:p.value,onClick:$[1]||($[1]=E=>I.value=!0)},null,8,["placeholder"])])],64)):l(N)==="algolia"?(a(),i(C,{key:1},[r.value?(a(),b(l(t),{key:0,algolia:((B=l(s).search)==null?void 0:B.options)??l(s).algolia,onVnodeBeforeMount:$[2]||($[2]=E=>d.value=!0)},null,8,["algolia"])):f("",!0),d.value?f("",!0):(a(),i("div",tr,[h(Ee,{placeholder:p.value,onClick:y},null,8,["placeholder"])]))],64)):f("",!0)],4)}}});const sr=g({__name:"VPNavBarSocialLinks",setup(n){const{theme:e}=P();return(t,s)=>l(e).socialLinks?(a(),b(Ce,{key:0,class:"VPNavBarSocialLinks",links:l(e).socialLinks},null,8,["links"])):f("",!0)}});const or=m(sr,[["__scopeId","data-v-ed745fe3"]]),ar=["href"],rr=g({__name:"VPNavBarTitle",setup(n){const{site:e,theme:t}=P(),{hasSidebar:s}=O(),{currentLang:o}=ne();return(r,d)=>(a(),i("div",{class:T(["VPNavBarTitle",{"has-sidebar":l(s)}])},[u("a",{class:"title",href:l(ee)(l(o).link)},[c(r.$slots,"nav-bar-title-before",{},void 0,!0),l(t).logo?(a(),b(we,{key:0,class:"logo",image:l(t).logo},null,8,["image"])):f("",!0),l(t).siteTitle?(a(),i(C,{key:1},[x(S(l(t).siteTitle),1)],64)):l(t).siteTitle===void 0?(a(),i(C,{key:2},[x(S(l(e).title),1)],64)):f("",!0),c(r.$slots,"nav-bar-title-after",{},void 0,!0)],8,ar)],2))}});const lr=m(rr,[["__scopeId","data-v-edfdb996"]]),ir={},cr={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},ur=u("path",{d:"M0 0h24v24H0z",fill:"none"},null,-1),dr=u("path",{d:" M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z ",class:"css-c4d79v"},null,-1),_r=[ur,dr];function vr(n,e){return a(),i("svg",cr,_r)}const We=m(ir,[["render",vr]]),pr={class:"items"},hr={class:"title"},fr=g({__name:"VPNavBarTranslations",setup(n){const{theme:e}=P(),{localeLinks:t,currentLang:s}=ne({correspondingLink:!0});return(o,r)=>l(t).length&&l(s).label?(a(),b(Me,{key:0,class:"VPNavBarTranslations",icon:We,label:l(e).langMenuLabel||"Change language"},{default:v(()=>[u("div",pr,[u("p",hr,S(l(s).label),1),(a(!0),i(C,null,H(l(t),d=>(a(),b(ue,{key:d.link,item:d},null,8,["item"]))),128))])]),_:1},8,["label"])):f("",!0)}});const mr=m(fr,[["__scopeId","data-v-e2ffc50f"]]),gr=n=>(z("data-v-cf1858b3"),n=n(),D(),n),yr={class:"container"},br={class:"title"},kr={class:"content"},$r=gr(()=>u("div",{class:"curtain"},null,-1)),Pr={class:"content-body"},Vr=g({__name:"VPNavBar",props:{isScreenOpen:{type:Boolean}},emits:["toggle-screen"],setup(n){const{y:e}=Re(),{hasSidebar:t}=O(),s=k(()=>({"has-sidebar":t.value,fill:e.value>0}));return(o,r)=>(a(),i("div",{class:T(["VPNavBar",s.value])},[u("div",yr,[u("div",br,[h(lr,null,{"nav-bar-title-before":v(()=>[c(o.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":v(()=>[c(o.$slots,"nav-bar-title-after",{},void 0,!0)]),_:3})]),u("div",kr,[$r,u("div",Pr,[c(o.$slots,"nav-bar-content-before",{},void 0,!0),h(nr,{class:"search"}),h(Wa,{class:"menu"}),h(mr,{class:"translations"}),h(Go,{class:"appearance"}),h(or,{class:"social-links"}),h(Na,{class:"extra"}),c(o.$slots,"nav-bar-content-after",{},void 0,!0),h(Fa,{class:"hamburger",active:o.isScreenOpen,onClick:r[0]||(r[0]=d=>o.$emit("toggle-screen"))},null,8,["active"])])])])],2))}});const wr=m(Vr,[["__scopeId","data-v-cf1858b3"]]);function Sr(n){if(Array.isArray(n)){for(var e=0,t=Array(n.length);e1),q=[],re=!1,Te=-1,X=void 0,R=void 0,Q=void 0,Ye=function(e){return q.some(function(t){return!!(t.options.allowTouchMove&&t.options.allowTouchMove(e))})},le=function(e){var t=e||window.event;return Ye(t.target)||t.touches.length>1?!0:(t.preventDefault&&t.preventDefault(),!1)},Lr=function(e){if(Q===void 0){var t=!!e&&e.reserveScrollBarGap===!0,s=window.innerWidth-document.documentElement.clientWidth;if(t&&s>0){var o=parseInt(window.getComputedStyle(document.body).getPropertyValue("padding-right"),10);Q=document.body.style.paddingRight,document.body.style.paddingRight=o+s+"px"}}X===void 0&&(X=document.body.style.overflow,document.body.style.overflow="hidden")},Mr=function(){Q!==void 0&&(document.body.style.paddingRight=Q,Q=void 0),X!==void 0&&(document.body.style.overflow=X,X=void 0)},Cr=function(){return window.requestAnimationFrame(function(){if(R===void 0){R={position:document.body.style.position,top:document.body.style.top,left:document.body.style.left};var e=window,t=e.scrollY,s=e.scrollX,o=e.innerHeight;document.body.style.position="fixed",document.body.style.top=-t,document.body.style.left=-s,setTimeout(function(){return window.requestAnimationFrame(function(){var r=o-window.innerHeight;r&&t>=o&&(document.body.style.top=-(t+r))})},300)}})},Ir=function(){if(R!==void 0){var e=-parseInt(document.body.style.top,10),t=-parseInt(document.body.style.left,10);document.body.style.position=R.position,document.body.style.top=R.top,document.body.style.left=R.left,window.scrollTo(t,e),R=void 0}},Tr=function(e){return e?e.scrollHeight-e.scrollTop<=e.clientHeight:!1},Br=function(e,t){var s=e.targetTouches[0].clientY-Te;return Ye(e.target)?!1:t&&t.scrollTop===0&&s>0||Tr(t)&&s<0?le(e):(e.stopPropagation(),!0)},Xe=function(e,t){if(!e){console.error("disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices.");return}if(!q.some(function(o){return o.targetElement===e})){var s={targetElement:e,options:t||{}};q=[].concat(Sr(q),[s]),ae?Cr():Lr(t),ae&&(e.ontouchstart=function(o){o.targetTouches.length===1&&(Te=o.targetTouches[0].clientY)},e.ontouchmove=function(o){o.targetTouches.length===1&&Br(o,e)},re||(document.addEventListener("touchmove",le,Ie?{passive:!1}:void 0),re=!0))}},Qe=function(){ae&&(q.forEach(function(e){e.targetElement.ontouchstart=null,e.targetElement.ontouchmove=null}),re&&(document.removeEventListener("touchmove",le,Ie?{passive:!1}:void 0),re=!1),Te=-1),ae?Ir():Mr(),q=[]};const Ar=g({__name:"VPNavScreenMenuLink",props:{item:{}},setup(n){const e=ge("close-screen");return(t,s)=>(a(),b(G,{class:"VPNavScreenMenuLink",href:t.item.link,target:t.item.target,rel:t.item.rel,onClick:l(e)},{default:v(()=>[x(S(t.item.text),1)]),_:1},8,["href","target","rel","onClick"]))}});const Nr=m(Ar,[["__scopeId","data-v-067d5c32"]]),xr={},Hr={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},Er=u("path",{d:"M18.9,10.9h-6v-6c0-0.6-0.4-1-1-1s-1,0.4-1,1v6h-6c-0.6,0-1,0.4-1,1s0.4,1,1,1h6v6c0,0.6,0.4,1,1,1s1-0.4,1-1v-6h6c0.6,0,1-0.4,1-1S19.5,10.9,18.9,10.9z"},null,-1),zr=[Er];function Dr(n,e){return a(),i("svg",Hr,zr)}const Fr=m(xr,[["render",Dr]]),Or=g({__name:"VPNavScreenMenuGroupLink",props:{item:{}},setup(n){const e=ge("close-screen");return(t,s)=>(a(),b(G,{class:"VPNavScreenMenuGroupLink",href:t.item.link,target:t.item.target,rel:t.item.rel,onClick:l(e)},{default:v(()=>[x(S(t.item.text),1)]),_:1},8,["href","target","rel","onClick"]))}});const Je=m(Or,[["__scopeId","data-v-7f48e5dc"]]),Gr={class:"VPNavScreenMenuGroupSection"},Rr={key:0,class:"title"},Ur=g({__name:"VPNavScreenMenuGroupSection",props:{text:{},items:{}},setup(n){return(e,t)=>(a(),i("div",Gr,[e.text?(a(),i("p",Rr,S(e.text),1)):f("",!0),(a(!0),i(C,null,H(e.items,s=>(a(),b(Je,{key:s.text,item:s},null,8,["item"]))),128))]))}});const jr=m(Ur,[["__scopeId","data-v-5def7a83"]]),qr=["aria-controls","aria-expanded"],Kr={class:"button-text"},Wr=["id"],Yr={key:1,class:"group"},Xr=g({__name:"VPNavScreenMenuGroup",props:{text:{},items:{}},setup(n){const e=n,t=w(!1),s=k(()=>`NavScreenGroup-${e.text.replace(" ","-").toLowerCase()}`);function o(){t.value=!t.value}return(r,d)=>(a(),i("div",{class:T(["VPNavScreenMenuGroup",{open:t.value}])},[u("button",{class:"button","aria-controls":s.value,"aria-expanded":t.value,onClick:o},[u("span",Kr,S(r.text),1),h(Fr,{class:"button-icon"})],8,qr),u("div",{id:s.value,class:"items"},[(a(!0),i(C,null,H(r.items,p=>(a(),i(C,{key:p.text},["link"in p?(a(),i("div",{key:p.text,class:"item"},[h(Je,{item:p},null,8,["item"])])):(a(),i("div",Yr,[h(jr,{text:p.text,items:p.items},null,8,["text","items"])]))],64))),128))],8,Wr)],2))}});const Qr=m(Xr,[["__scopeId","data-v-90941ff9"]]),Jr={key:0,class:"VPNavScreenMenu"},Zr=g({__name:"VPNavScreenMenu",setup(n){const{theme:e}=P();return(t,s)=>l(e).nav?(a(),i("nav",Jr,[(a(!0),i(C,null,H(l(e).nav,o=>(a(),i(C,{key:o.text},["link"in o?(a(),b(Nr,{key:0,item:o},null,8,["item"])):(a(),b(Qr,{key:1,text:o.text||"",items:o.items},null,8,["text","items"]))],64))),128))])):f("",!0)}}),el={key:0,class:"VPNavScreenAppearance"},tl={class:"text"},nl=g({__name:"VPNavScreenAppearance",setup(n){const{site:e,theme:t}=P();return(s,o)=>l(e).appearance?(a(),i("div",el,[u("p",tl,S(l(t).darkModeSwitchLabel||"Appearance"),1),h(Se)])):f("",!0)}});const sl=m(nl,[["__scopeId","data-v-a4ad84ce"]]),ol={class:"list"},al=g({__name:"VPNavScreenTranslations",setup(n){const{localeLinks:e,currentLang:t}=ne({correspondingLink:!0}),s=w(!1);function o(){s.value=!s.value}return(r,d)=>l(e).length&&l(t).label?(a(),i("div",{key:0,class:T(["VPNavScreenTranslations",{open:s.value}])},[u("button",{class:"title",onClick:o},[h(We,{class:"icon lang"}),x(" "+S(l(t).label)+" ",1),h(Ke,{class:"icon chevron"})]),u("ul",ol,[(a(!0),i(C,null,H(l(e),p=>(a(),i("li",{key:p.link,class:"item"},[h(G,{class:"link",href:p.link},{default:v(()=>[x(S(p.text),1)]),_:2},1032,["href"])]))),128))])],2)):f("",!0)}});const rl=m(al,[["__scopeId","data-v-bd8c50c0"]]),ll=g({__name:"VPNavScreenSocialLinks",setup(n){const{theme:e}=P();return(t,s)=>l(e).socialLinks?(a(),b(Ce,{key:0,class:"VPNavScreenSocialLinks",links:l(e).socialLinks},null,8,["links"])):f("",!0)}}),il={class:"container"},cl=g({__name:"VPNavScreen",props:{open:{type:Boolean}},setup(n){const e=w(null);function t(){Xe(e.value,{reserveScrollBarGap:!0})}function s(){Qe()}return(o,r)=>(a(),b(he,{name:"fade",onEnter:t,onAfterLeave:s},{default:v(()=>[o.open?(a(),i("div",{key:0,class:"VPNavScreen",ref_key:"screen",ref:e,id:"VPNavScreen"},[u("div",il,[c(o.$slots,"nav-screen-content-before",{},void 0,!0),h(Zr,{class:"menu"}),h(rl,{class:"translations"}),h(sl,{class:"appearance"}),h(ll,{class:"social-links"}),c(o.$slots,"nav-screen-content-after",{},void 0,!0)])],512)):f("",!0)]),_:3}))}});const ul=m(cl,[["__scopeId","data-v-b67951f6"]]),dl={class:"VPNav"},_l=g({__name:"VPNav",setup(n){const{isScreenOpen:e,closeScreen:t,toggleScreen:s}=bo();return oe("close-screen",t),(o,r)=>(a(),i("header",dl,[h(wr,{"is-screen-open":l(e),onToggleScreen:l(s)},{"nav-bar-title-before":v(()=>[c(o.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":v(()=>[c(o.$slots,"nav-bar-title-after",{},void 0,!0)]),"nav-bar-content-before":v(()=>[c(o.$slots,"nav-bar-content-before",{},void 0,!0)]),"nav-bar-content-after":v(()=>[c(o.$slots,"nav-bar-content-after",{},void 0,!0)]),_:3},8,["is-screen-open","onToggleScreen"]),h(ul,{open:l(e)},{"nav-screen-content-before":v(()=>[c(o.$slots,"nav-screen-content-before",{},void 0,!0)]),"nav-screen-content-after":v(()=>[c(o.$slots,"nav-screen-content-after",{},void 0,!0)]),_:3},8,["open"])]))}});const vl=m(_l,[["__scopeId","data-v-640c0ee4"]]),pl=n=>(z("data-v-db75407b"),n=n(),D(),n),hl=["role","tabindex"],fl=pl(()=>u("div",{class:"indicator"},null,-1)),ml=["onKeydown"],gl={key:1,class:"items"},yl=g({__name:"VPSidebarItem",props:{item:{},depth:{}},setup(n){const e=n,{collapsed:t,collapsible:s,isLink:o,isActiveLink:r,hasActiveLink:d,hasChildren:p,toggle:_}=Gt(k(()=>e.item)),y=k(()=>p.value?"section":"div"),L=k(()=>o.value?"a":"div"),A=k(()=>p.value?e.depth+2===7?"p":`h${e.depth+2}`:"p"),I=k(()=>o.value?void 0:"button"),V=k(()=>[[`level-${e.depth}`],{collapsible:s.value},{collapsed:t.value},{"is-link":o.value},{"is-active":r.value},{"has-active":d.value}]);function N($){"key"in $&&$.key!=="Enter"||!e.item.link&&_()}function M(){e.item.link&&_()}return($,B)=>{const E=U("VPSidebarItem",!0);return a(),b(j(y.value),{class:T(["VPSidebarItem",V.value])},{default:v(()=>[$.item.text?(a(),i("div",se({key:0,class:"item",role:I.value},_t($.item.items?{click:N,keydown:N}:{},!0),{tabindex:$.item.items&&0}),[fl,$.item.link?(a(),b(G,{key:0,tag:L.value,class:"link",href:$.item.link},{default:v(()=>[(a(),b(j(A.value),{class:"text",innerHTML:$.item.text},null,8,["innerHTML"]))]),_:1},8,["tag","href"])):(a(),b(j(A.value),{key:1,class:"text",innerHTML:$.item.text},null,8,["innerHTML"])),$.item.collapsed!=null?(a(),i("div",{key:2,class:"caret",role:"button","aria-label":"toggle section",onClick:M,onKeydown:vt(M,["enter"]),tabindex:"0"},[h(Ve,{class:"caret-icon"})],40,ml)):f("",!0)],16,hl)):f("",!0),$.item.items&&$.item.items.length?(a(),i("div",gl,[$.depth<5?(a(!0),i(C,{key:0},H($.item.items,Y=>(a(),b(E,{key:Y.text,item:Y,depth:$.depth+1},null,8,["item","depth"]))),128)):f("",!0)])):f("",!0)]),_:1},8,["class"])}}});const bl=m(yl,[["__scopeId","data-v-db75407b"]]),Ze=n=>(z("data-v-4489ee23"),n=n(),D(),n),kl=Ze(()=>u("div",{class:"curtain"},null,-1)),$l={class:"nav",id:"VPSidebarNav","aria-labelledby":"sidebar-aria-label",tabindex:"-1"},Pl=Ze(()=>u("span",{class:"visually-hidden",id:"sidebar-aria-label"}," Sidebar Navigation ",-1)),Vl=g({__name:"VPSidebar",props:{open:{type:Boolean}},setup(n){const e=n,{sidebarGroups:t,hasSidebar:s}=O();let o=w(null);function r(){Xe(o.value,{reserveScrollBarGap:!0})}function d(){Qe()}return pt(async()=>{var p;e.open?(r(),(p=o.value)==null||p.focus()):d()}),(p,_)=>l(s)?(a(),i("aside",{key:0,class:T(["VPSidebar",{open:p.open}]),ref_key:"navEl",ref:o,onClick:_[0]||(_[0]=ht(()=>{},["stop"]))},[kl,u("nav",$l,[Pl,c(p.$slots,"sidebar-nav-before",{},void 0,!0),(a(!0),i(C,null,H(l(t),y=>(a(),i("div",{key:y.text,class:"group"},[h(bl,{item:y,depth:0},null,8,["item"])]))),128)),c(p.$slots,"sidebar-nav-after",{},void 0,!0)])],2)):f("",!0)}});const wl=m(Vl,[["__scopeId","data-v-4489ee23"]]),Sl=g({__name:"VPSkipLink",setup(n){const e=te(),t=w();K(()=>e.path,()=>t.value.focus());function s({target:o}){const r=document.getElementById(decodeURIComponent(o.hash).slice(1));if(r){const d=()=>{r.removeAttribute("tabindex"),r.removeEventListener("blur",d)};r.setAttribute("tabindex","-1"),r.addEventListener("blur",d),r.focus(),window.scrollTo(0,0)}}return(o,r)=>(a(),i(C,null,[u("span",{ref_key:"backToTop",ref:t,tabindex:"-1"},null,512),u("a",{href:"#VPContent",class:"VPSkipLink visually-hidden",onClick:s}," Skip to content ")],64))}});const Ll=m(Sl,[["__scopeId","data-v-9fa85018"]]),Ml=g({__name:"Layout",setup(n){const{isOpen:e,open:t,close:s}=O(),o=te();K(()=>o.path,s),Ot(e,s),oe("close-sidebar",s),oe("is-sidebar-open",e);const{frontmatter:r}=P(),d=ft(),p=k(()=>!!d["home-hero-image"]);return oe("hero-image-slot-exists",p),(_,y)=>{const L=U("Content");return l(r).layout!==!1?(a(),i("div",{key:0,class:T(["Layout",l(r).pageClass])},[c(_.$slots,"layout-top",{},void 0,!0),h(Ll),h(kt,{class:"backdrop",show:l(e),onClick:l(s)},null,8,["show","onClick"]),l(r).navbar!==!1?(a(),b(vl,{key:0},{"nav-bar-title-before":v(()=>[c(_.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":v(()=>[c(_.$slots,"nav-bar-title-after",{},void 0,!0)]),"nav-bar-content-before":v(()=>[c(_.$slots,"nav-bar-content-before",{},void 0,!0)]),"nav-bar-content-after":v(()=>[c(_.$slots,"nav-bar-content-after",{},void 0,!0)]),"nav-screen-content-before":v(()=>[c(_.$slots,"nav-screen-content-before",{},void 0,!0)]),"nav-screen-content-after":v(()=>[c(_.$slots,"nav-screen-content-after",{},void 0,!0)]),_:3})):f("",!0),h(yo,{open:l(e),onOpenMenu:l(t)},null,8,["open","onOpenMenu"]),h(wl,{open:l(e)},{"sidebar-nav-before":v(()=>[c(_.$slots,"sidebar-nav-before",{},void 0,!0)]),"sidebar-nav-after":v(()=>[c(_.$slots,"sidebar-nav-after",{},void 0,!0)]),_:3},8,["open"]),h(Js,null,{"page-top":v(()=>[c(_.$slots,"page-top",{},void 0,!0)]),"page-bottom":v(()=>[c(_.$slots,"page-bottom",{},void 0,!0)]),"not-found":v(()=>[c(_.$slots,"not-found",{},void 0,!0)]),"home-hero-before":v(()=>[c(_.$slots,"home-hero-before",{},void 0,!0)]),"home-hero-info":v(()=>[c(_.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-image":v(()=>[c(_.$slots,"home-hero-image",{},void 0,!0)]),"home-hero-after":v(()=>[c(_.$slots,"home-hero-after",{},void 0,!0)]),"home-features-before":v(()=>[c(_.$slots,"home-features-before",{},void 0,!0)]),"home-features-after":v(()=>[c(_.$slots,"home-features-after",{},void 0,!0)]),"doc-footer-before":v(()=>[c(_.$slots,"doc-footer-before",{},void 0,!0)]),"doc-before":v(()=>[c(_.$slots,"doc-before",{},void 0,!0)]),"doc-after":v(()=>[c(_.$slots,"doc-after",{},void 0,!0)]),"doc-top":v(()=>[c(_.$slots,"doc-top",{},void 0,!0)]),"doc-bottom":v(()=>[c(_.$slots,"doc-bottom",{},void 0,!0)]),"aside-top":v(()=>[c(_.$slots,"aside-top",{},void 0,!0)]),"aside-bottom":v(()=>[c(_.$slots,"aside-bottom",{},void 0,!0)]),"aside-outline-before":v(()=>[c(_.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":v(()=>[c(_.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":v(()=>[c(_.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":v(()=>[c(_.$slots,"aside-ads-after",{},void 0,!0)]),_:3}),h(so),c(_.$slots,"layout-bottom",{},void 0,!0)],2)):(a(),b(L,{key:1}))}}});const Cl=m(Ml,[["__scopeId","data-v-1f16208e"]]);const Tl={Layout:Cl,enhanceApp:({app:n})=>{n.component("Badge",gt)}};export{Tl as t,P as u}; diff --git a/assets/communication.md.03c12ebf.js b/assets/communication.md.03c12ebf.js new file mode 100644 index 00000000..66811ddf --- /dev/null +++ b/assets/communication.md.03c12ebf.js @@ -0,0 +1,672 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as c,v as r,b as y,t as s,O as p,R as a,M as n}from"./chunks/framework.0d8bea05.js";const E=JSON.parse('{"title":"组件之间的通信","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"communication.md","filePath":"communication.md"}'),F={name:"communication.md"},D=a('

组件之间的通信

经过前面几章的阅读,相信开发者已经可以搭建一个基础的 Vue 3 项目了!

但实际业务开发过程中,还会遇到一些组件之间的通信问题,父子组件通信、兄弟组件通信、爷孙组件通信,还有一些全局通信的场景。

TIP

这一章节的内容,Vue 3 对比 Vue 2 的变化都比较大!

这一章就按使用场景来划分对应的章节吧,在什么场景下遇到问题,也方便快速找到对应的处理办法。

通信场景快速定位
父子组件通信点击查看
爷孙组件通信点击查看
兄弟组件通信点击查看
全局组件通信点击查看
',6),i=a(`

父子组件通信

父子组件通信是指,B 组件引入到 A 组件里渲染,此时 A 是 B 的父级;B 组件的一些数据需要从 A 组件拿,B 组件有时也要告知 A 组件一些数据变化情况。

他们之间的关系如下, Child.vue 是直接挂载在 Father.vue 下面:

bash
# 父组件
+Father.vue
+ # 子组件
+└─Child.vue

常用的方法有:

方案父组件向子组件子组件向父组件对应章节传送门
props / emitspropsemits点击查看
v-model / emitsv-modelemits点击查看
ref / emitsrefemits点击查看
provide / injectprovideinject点击查看
EventBusemit / onemit / on点击查看
Reactive State--点击查看
Vuex--点击查看
Pinia--点击查看

为了方便阅读,下面的父组件统一叫 Father.vue ,子组件统一叫 Child.vue 。

WARNING

在 Vue 2 ,有的开发者可能喜欢用 $attrs / $listeners 来进行通信,但该方案在 Vue 3 已经移除了,详见 移除 $listeners

props / emits

这是 Vue 跨组件通信最常用,也是基础的一个方案,它的通信过程是:

  1. 父组件 Father.vue 通过 props 向子组件 Child.vue 传值
  2. 子组件 Child.vue 则可以通过 emits 向父组件 Father.vue 发起事件通知

最常见的场景就是统一在父组件发起 AJAX 请求,拿到数据后,再根据子组件的渲染需要传递不同的 props 给不同的子组件使用。

下发 props

注:这一小节的步骤是在 Father.vue 里操作。

下发的过程是在 Father.vue 里完成的,父组件在向子组件下发 props 之前,需要导入子组件并启用它作为自身的模板,然后在 setup 里处理好数据并 return 给 <template /> 用。

在 Father.vue 的 <script /> 里:

ts
// Father.vue
+import { defineComponent } from 'vue'
+import Child from '@cp/Child.vue'
+
+interface Member {
+  id: number
+  name: string
+}
+
+export default defineComponent({
+  // 需要启用子组件作为模板
+  components: {
+    Child,
+  },
+
+  // 定义一些数据并 \`return\` 给 \`<template />\` 用
+  setup() {
+    const userInfo: Member = {
+      id: 1,
+      name: 'Petter',
+    }
+
+    // 不要忘记 \`return\` ,否则 \`<template />\` 拿不到数据
+    return {
+      userInfo,
+    }
+  },
+})

然后在 Father.vue 的 <template /> 这边拿到 return 出来的数据,把要传递的数据通过属性的方式绑定在组件标签上。

vue
<!-- Father.vue -->
+<template>
+  <Child
+    title="用户信息"
+    :index="1"
+    :uid="userInfo.id"
+    :user-name="userInfo.name"
+  />
+</template>

这样就完成了 props 数据的下发。

<template /> 绑定属性这里,如果是普通的字符串,比如上面的 title,则直接给属性名赋值就可以。

如果是变量名,或者其他类型如 numberboolean 等,比如上面的 index,则需要通过属性动态绑定的方式来添加,使用 v-bind: 或者 : 符号进行绑定。

另外官方文档推荐对 camelCase 风格(小驼峰)命名的 props ,在绑定时使用和 HTML attribute 一样的 kebab-case 风格(短横线),例如使用 user-name 代替 userName 传递,详见官网的 传递 prop 的细节 一节。

接收 props

注:这一小节的步骤是在 Child.vue 里操作。

接收的过程是在 Child.vue 里完成的,在 <script /> 部分,子组件通过与 setup 同级的 props 来接收数据。

它可以是一个 string[] 数组,把要接受的变量名放到这个数组里,直接放进来作为数组的 item

ts
// Child.vue
+export default defineComponent({
+  props: ['title', 'index', 'userName', 'uid'],
+})

但这种情况下,使用者不知道这些属性的使用限制,例如是什么类型的值、是否必传等等。

带有类型限制的 props

注:这一小节的步骤是在 Child.vue 里操作。

和 TypeScript 一样,类型限制可以为程序带来更好的健壮性, Vue 的 props 也支持增加类型限制。

相对于传递一个 string[] 类型的数组,更推荐的方式是把 props 定义为一个对象,以对象形式列出,每个 Property 的名称和值分别是各自的名称和类型,只有合法的类型才允许传入。

TIP

注意,和 TS 的类型定义不同, props 这里的类型,首字母需要大写,也就是 JavaScript 的基本类型。

支持的类型有:

类型含义
String字符串
Number数值
Boolean布尔值
Array数组
Object对象
Date日期数据,e.g. new Date()
Function函数,e.g. 普通函数、箭头函数、构造函数
PromisePromise 类型的函数
SymbolSymbol 类型的值

了解了基本的类型限制用法之后,接下来给 props 加上类型限制:

ts
// Child.vue
+export default defineComponent({
+  props: {
+    title: String,
+    index: Number,
+    userName: String,
+    uid: Number,
+  },
+})

现在如果传入不正确的类型,程序就会抛出警告信息,告知开发者必须正确传值。

如果需要对某个 Prop 允许多类型,比如这个 uid 字段,可能是数值,也可能是字符串,那么可以在类型这里,使用一个数组,把允许的类型都加进去。

ts
// Child.vue
+export default defineComponent({
+  props: {
+    // 单类型
+    title: String,
+    index: Number,
+    userName: String,
+
+    // 这里使用了多种类型
+    uid: [Number, String],
+  },
+})

可选以及带有默认值的 props

注:这一小节的步骤是在 Child.vue 里操作。

所有 props 默认都是可选的,如果不传递具体的值,则默认值都是 undefined ,可能引起程序运行崩溃, Vue 支持对可选的 props 设置默认值,也是通过对象的形式配置 props 的选项。

其中支持配置的选项有:

选项类型含义
typestring类型
requiredboolean是否必传, true 代表必传, false 代表可选
defaultanytype 选项的类型相对应的默认值,如果 required 选项是 false ,但这里不设置默认值,则会默认为 undefined
validatorfunction自定义验证函数,需要 return 一个布尔值, true 代表校验通过, false 代表校验不通过,当校验不通过时,控制台会抛出警告信息

了解了配置选项后,接下来再对 props 进行改造,将其中部分选项设置为可选,并提供默认值:

ts
// Child.vue
+export default defineComponent({
+  props: {
+    // 可选,并提供默认值
+    title: {
+      type: String,
+      required: false,
+      default: '默认标题',
+    },
+
+    // 默认可选,单类型
+    index: Number,
+
+    // 添加一些自定义校验
+    userName: {
+      type: String,
+
+      // 在这里校验用户名必须至少 3 个字
+      validator: (v) => v.length >= 3,
+    },
+
+    // 默认可选,但允许多种类型
+    uid: [Number, String],
+  },
+})

使用 props ~new

注:这一小节的步骤是在 Child.vue 里操作。

<template /> 部分, Vue 3 的使用方法和 Vue 2 是一样的,比如要渲染父组件传入的 props :

vue
<!-- Child.vue -->
+<template>
+  <p>标题:{{ title }}</p>
+  <p>索引:{{ index }}</p>
+  <p>用户id:{{ uid }}</p>
+  <p>用户名:{{ userName }}</p>
+</template>

但是 <script /> 部分,变化非常大!

在 Vue 2 里,只需要通过 this.uidthis.userName 就可以使用父组件传下来的 Prop ,但是 Vue 3 没有了 this ,所以是通过 setup 的入参进行操作。

ts
// Child.vue
+export default defineComponent({
+  props: {
+    title: String,
+    index: Number,
+    userName: String,
+    uid: Number,
+  },
+
+  // 在这里需要添加一个入参
+  setup(props) {
+    // 该入参包含了当前组件定义的所有 props
+    console.log(props)
+  },
+})

关于 Setup 函数的第一个入参 props

  1. 该入参包含了当前组件定义的所有 props (如果父组件 Father.vue 传进来的数据在 Child.vue 里未定义,不仅不会拿到,并且在控制台会有警告信息)。
  2. 该入参可以随意命名,比如可以写成一个下划线 _ ,通过 _.uid 也可以拿到数据,但是语义化命名是一个良好的编程习惯。
  3. 该入参具备响应性,父组件修改了传递下来的值,子组件也会同步得到更新,因此请不要直接解构,可以通过 toRef 或 toRefs API 转换为响应式变量

传递非 props 的属性

上一小节最后有一句提示是:

如果父组件 Father.vue 传进来的数据在 Child.vue 里未定义,不仅不会拿到,并且在控制台会有警告信息。

这种情况虽然无法从 props 里拿到对应的数据,但也不意味着不能传递任何未定义的属性数据,在父组件,除了可以给子组件绑定 props ,还可以根据实际需要去绑定一些特殊的属性。

比如给子组件设置 classid,或者 data-xxx 之类的一些自定义属性,如果子组件 Child.vue 的 <template /> 里只有一个根节点,那么这些属性默认会自动继承并渲染在 Node 节点上。

假设当前在子组件 Child.vue 是如下这样只有一个根节点,并且未接收任何 props :

TIP

如果已安装 Vue VSCode Snippets 这个 VSCode 插件,可以在空的 .vue 文件里输入 v3 ,在出现的代码片段菜单里选择 vbase-3-ts 生成一个 Vue 组件的基础代码片段。

vue
<!-- Child.vue -->
+<template>
+  <div class="child">子组件</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup() {
+    return {}
+  },
+})
+</script>
+
+<style scoped>
+.child {
+  width: 100%;
+}
+</style>

在 Father.vue 里对 Child.vue 传递了多个属性:

vue
<!-- Father.vue -->
+<template>
+  <Child
+    id="child-component"
+    class="class-name-from-father"
+    :keys="['foo', 'bar']"
+    :obj="{ foo: 'bar' }"
+    data-hash="b10a8db164e0754105b7a99be72e3fe5"
+  />
+</template>

回到浏览器,通过 Chrome 的审查元素可以看到子组件 Child.vue 在渲染后,按照 HTML 属性的渲染规则生成了多个属性:

html
<!-- Child.vue 在浏览器里渲染后的 HTML DOM 结构 -->
+<div
+  class="child class-name-from-father"
+  id="child-component"
+  keys="foo,bar"
+  obj="[object Object]"
+  data-hash="b10a8db164e0754105b7a99be72e3fe5"
+  data-v-2dcc19c8=""
+  data-v-7eb2bc79=""
+>
+  子组件
+</div>

TIP

其中有两个以 data-v- 开头的属性是 <style /> 标签开启了 Style Scoped 功能自动生成的 Hash 值。

可以在 Child.vue 配置 inheritAttrsfalse 来屏蔽这些非 props 属性的渲染。

ts
// Child.vue
+export default defineComponent({
+  inheritAttrs: false,
+  setup() {
+    // ...
+  },
+})

关闭了 之后,现在的 DOM 结构如下,只保留了两个由 Style Scoped 生成的 Hash 值:

html
<!-- Child.vue 在浏览器里渲染后的 HTML DOM 解构 -->
+<div class="child" data-v-2dcc19c8="" data-v-7eb2bc79="">子组件</div>

这一类非 props 属性通常称之为 attrs 。

刚接触 Vue 的开发者可能容易混淆这两者,确实是非常接近,都是由父组件传递,由子组件接收,支持传递的数据类型也一样,但为什么一部分是在 props 获取,一部分在 attrs 获取呢?笔者给出一个比较容易记忆的方式,不一定特别准确,但相信可以帮助开发者加深两者的区别理解。

根据它们的缩写,其实是可以知道 Prop 是指 Property ,而 Attr 是指 Attribute ,虽然都是 “属性” ,但 Property 更接近于事物本身的属性,因此需要在组件里声明,而 Attribute 更偏向于赋予的属性,因此用于指代父组件传递的其他未被声明为 Property 的属性。

获取非 props 的属性 ~new

注:这一小节的步骤是在 Child.vue 里操作。

在上一小节 传递非 props 的属性 已经在父组件 Father.vue 里向子组件 Child.vue 传递了一些 attrs 自定义属性,在子组件里想要拿到这些属性,使用原生 JavaScript 操作是需要通过 Element.getAttribute() 方法,但 Vue 提供了更简单的操作方式。

在 Child.vue 里,可以通过 setup 的第二个参数 context 里的 attrs 来获取到这些属性,并且父组件传递了什么类型的值,获取到的也是一样的类型,这一点和使用 Element.getAttribute() 完全不同。

ts
// Child.vue
+export default defineComponent({
+  setup(props, { attrs }) {
+    // \`attrs\` 是个对象,每个 Attribute 都是它的 \`key\`
+    console.log(attrs.id) // child-component
+    console.log(attrs.class) // class-name-from-father
+
+    // 传递数组会被保留类型,不会被转换为 \`key1,key2\` 这样的字符串
+    // 这一点与 \`Element.getAttribute()\` 完全不同
+    console.log(attrs.keys) // ['foo', 'bar']
+
+    // 传递对象也可以正常获取
+    console.log(attrs.obj) // {foo: 'bar'}
+
+    // 如果传下来的 Attribute 带有短横线,需要通过这种方式获取
+    console.log(attrs['data-hash']) // b10a8db164e0754105b7a99be72e3fe5
+  },
+})

TIP

子组件不论是否设置 inheritAttrs 属性,都可以通过 attrs 拿到父组件传递下来的数据,但是如果要使用 Element.getAttribute() 则只有当 inheritAttrstrue 的时候才可以,因为此时在 DOM 上才会渲染这些属性。

与 Vue 2 的 <template /> 只能有一个根节点不同, Vue 3 允许多个根节点,多个根节点的情况下,无法直接继承这些 attrs 属性(在 inheritAttrs: true 的情况也下无法默认继承),需要在子组件 Child.vue 里通过 v-bind 绑定到要继承在节点上。

可以通过 Vue 实例属性 $attrs 或者从 setup 函数里把 attrs return 出来使用。

vue
<!-- Child.vue -->
+<template>
+  <!-- 默认不会继承属性 -->
+  <div class="child">不会继承</div>
+
+  <!-- 绑定后可继承, \`$attrs\` 是一个 Vue 提供的实例属性 -->
+  <div class="child" v-bind="$attrs">使用 $attrs 继承</div>
+
+  <!-- 绑定后可继承, \`attrs\` 是从 \`setup\` 里 \`return\` 出来的变量 -->
+  <div class="child" v-bind="attrs">使用 attrs 继承</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup(props, { attrs }) {
+
+    return {
+      attrs,
+    }
+  },
+})
+</script>

绑定 emits ~new

注:这一小节的步骤是在 Father.vue 里操作。

如果父组件 Father.vue 需要获取子组件 Child.vue 的数据更新情况,可以由子组件通过 emits 进行通知,下面这个更新用户年龄的例子可以学习如何给子组件绑定 emit 事件。

事件的逻辑是由父组件决定的,因此需要在父组件 Father.vue 的 <script /> 里先声明数据变量和一个更新函数,并且这个更新函数通常会有一个入参作为数据的新值接收。

在本例子里,父组件声明了一个 updateAge 方法,它接受一个入参 newAge ,代表新的年龄数据,这个入参的值将由子组件 Child.vue 在触发 emits 时传入。

因为还需要在 <template /> 部分绑定给子组件,所以请记得 return 出来。

ts
// Father.vue
+import { defineComponent, reactive } from 'vue'
+import Child from '@cp/Child.vue'
+
+interface Member {
+  id: number
+  name: string
+  age: number
+}
+
+export default defineComponent({
+  components: {
+    Child,
+  },
+  setup() {
+    const userInfo: Member = reactive({
+      id: 1,
+      name: 'Petter',
+      age: 0,
+    })
+
+    /**
+     * 声明一个更新年龄的方法
+     * @param newAge - 新的年龄,由子组件触发 emits 时传递
+     */
+    function updateAge(newAge: number) {
+      userInfo.age = newAge
+    }
+
+    return {
+      userInfo,
+      updateAge,
+    }
+  },
+})

再看 Father.vue 的 <template /> 部分,和 Click 事件使用 @click 一样,自定义的 emits 事件也是通过 v-on 或者是 @ 来绑定:

vue
<!-- Father.vue -->
+<template>
+  <Child @update-age="updateAge" />
+</template>

和 props 一样,官方文档也推荐将 camelCase 风格(小驼峰)命名的函数,在绑定时使用 kebab-case 风格(短横线),例如使用 update-age 代替 updateAge 传递。

接收并调用 emits ~new

注:这一小节的步骤是在 Child.vue 里操作。

和 props 一样,可以指定是一个数组,把要接收的 emit 事件名称写进去:

ts
// Child.vue
+export default defineComponent({
+  emits: ['update-age'],
+})

和 props 不同,通常情况下 emits 这样配置就足够使用了。

接下来如果子组件需要更新数据并通知父组件,可以使用 setup 第二个参数 context 里的 emit 方法触发:

ts
// Child.vue
+export default defineComponent({
+  emits: ['update-age'],
+  setup(props, { emit }) {
+    // 通知父组件将年龄设置为 \`2\`
+    emit('update-age', 2)
+  },
+})

emit 方法最少要传递一个参数:事件名称。

事件名称是指父组件 Father.vue 绑定事件时 @update-age="updateAge" 里的 update-age ,如果改成 @hello="updateAge" ,那么事件名称就需要使用 hello ,一般情况下事件名称和更新函数的名称会保持一致,方便维护。

对于需要更新数据的情况, emit 还支持传递更多的参数,对应更新函数里的入参,所以可以看到上面例子里的 emit('update-age', 2) 有第二个参数,传递了一个 2 的数值,就是作为父组件 updateAge 的入参 newAge 传递。

如果需要通信的数据很多,建议第二个入参使用一个对象来管理数据,例如父组件调整为:

ts
// Father.vue
+function updateInfo({ name, age }: Member) {
+  // 当 \`name\` 变化时更新 \`name\` 的值
+  if (name && name !== userInfo.name) {
+    userInfo.name = name
+  }
+
+  // 当 \`age\` 变化并且新值在正确的范围内时,更新 \`age\` 的值
+  if (age > 0 && age !== userInfo.age) {
+    userInfo.age = age
+  }
+}

子组件在传递新数据时,就应该使用对象的形式传递:

ts
// Child.vue
+emit('update-info', {
+  name: 'Tom',
+  age: 18,
+})

这对于更新表单等数据量较多的场景非常好用。

接收 emits 时做一些校验

注:这一小节的步骤是在 Child.vue 里操作。

和 props 一样,子组件在接收 emits 时也可以对这些事件做一些验证,这个时候就需要将 emits 配置为对象,然后把事件名称作为 keyvalue 则对应为一个用来校验的方法。

还是用回上文那个更新年龄的方法,如果需要增加一个条件:当达到成年人的年龄时才会更新父组件的数据,那么就可以将 emits 调整为:

ts
// Child.vue
+export default defineComponent({
+  emits: {
+    // 需要校验
+    'update-age': (age: number) => {
+      // 写一些条件拦截,返回 \`false\` 表示验证不通过
+      if (age < 18) {
+        console.log('未成年人不允许参与')
+        return false
+      }
+
+      // 通过则返回 \`true\`
+      return true
+    },
+
+    // 一些无需校验的,设置为 \`null\` 即可
+    'update-name': null,
+  },
+})

接下来如果提交 emit('update-age', 2) ,因为不满足验证条件,浏览器控制台将会出现一段 [Vue warn]: Invalid event arguments: event validation failed for event "update-age". 这样的警告信息。

v-model / emits

相对于 props / emits 这一对通信方案,使用 v-model 的方式更为简单:

  1. 在 Father.vue ,通过 v-model 向 Child.vue 传值

  2. Child.vue 通过自身设定的 emits 向 Father.vue 通知数据更新

v-model 的用法和 props 非常相似,但是很多操作上更为简化,但操作简单带来的 “副作用” ,就是功能上也没有 props 那么多。

绑定 v-model ~new

注:这一小节的步骤是在 Father.vue 里操作。

和下发 props 的方式类似,都是在子组件上绑定 Father.vue 定义好的数据,这是绑定一个数据的例子:

vue
<!-- Father.vue -->
+<template>
+  <Child v-model:username="userInfo.name" />
+</template>

和 Vue 2 不同, Vue 3 可以直接绑定 v-model ,而无需在子组件指定 model 选项 ,并且 Vue 3 的 v-model 需要使用英文冒号 : 指定要绑定的属性名,同时也支持绑定多个 v-model 。

如果要绑定多个数据,写多个 v-model 即可:

vue
<!-- Father.vue -->
+<template>
+  <Child
+    v-model:uid="userInfo.id"
+    v-model:username="userInfo.name"
+    v-model:age="userInfo.age"
+  />
+</template>

看到这里应该能明白了,一个 v-model 其实就是一个 prop ,它支持的数据类型和 prop 是一样的,所以子组件在接收数据的时候,完全按照 props 去定义就可以了。

点击回顾:接收 props ,了解在 Child.vue 如何接收 props,以及相关的 props 类型限制等部分内容。

配置 emits ~new

注:这一小节的步骤是在 Child.vue 里操作。

虽然 v-model 的配置和 props 相似,但是为什么出这么两个相似的东西?自然是为了简化一些开发上的操作。

使用 props / emits ,如果要更新父组件的数据,还需要在父组件声明一个更新函数并绑定事件给子组件,才能够更新。

而使用 v-model / emits ,无需在父组件声明更新函数,只需要在子组件 Child.vue 里通过 update: 前缀加上 v-model 的属性名这样的格式,即可直接定义一个更新事件。

ts
// Child.vue
+export default defineComponent({
+  props: {
+    uid: Number,
+    username: String,
+    age: Number,
+  },
+  // 注意这里的 \`update:\` 前缀
+  emits: ['update:uid', 'update:username', 'update:age'],
+})

这里的 update 后面的属性名,支持驼峰写法,这一部分和 Vue 2 的使用是相同的。

在配置 emits 时,也可以对数据更新做一些校验,配置方式和讲解 props / emits 时 接收 emits 时做一些校验 这一小节的操作是一样的。

在 Child.vue 配置好 emits 之后,就可以在 setup 里直接操作数据的更新了:

ts
// Child.vue
+export default defineComponent({
+  setup(props, { emit }) {
+    // 2s 后更新用户名
+    setTimeout(() => {
+      emit('update:username', 'Tom')
+    }, 2000)
+  },
+})

子组件通过调用 emit('update:xxx') 即可让父组件更新对应的数据。

ref / emits

在学习 响应式 API 之 ref 的时候,已讲解过 ref 是可以用在 DOM 元素与子组件 上面,所以也可以使用 ref 配合 emits 完成父子组件的通信。

父组件操作子组件 ~new

注:这一小节的步骤是在 Father.vue 里操作。

父组件可以给子组件绑定 ref 属性,然后通过 Ref 变量操作子组件的数据或者调用子组件里面的方法。

先在 <template /> 处给子组件标签绑定 ref 属性:

vue
<!-- Father.vue -->
+<template>
+  <Child ref="child" />
+</template>

然后在 <script /> 部分定义好对应的变量名称 child (记得要 return 出来哦),即可通过该变量操作子组件上的变量或方法:

ts
// Father.vue
+import { defineComponent, onMounted, ref } from 'vue'
+import Child from '@cp/Child.vue'
+
+export default defineComponent({
+  components: {
+    Child,
+  },
+  setup() {
+    // 给子组件定义一个 \`ref\` 变量
+    const child = ref<InstanceType<typeof Child>>()
+
+    // 请保证视图渲染完毕后再执行操作
+    onMounted(async () => {
+      // 执行子组件里面的 AJAX 请求函数
+      await child.value!.queryList()
+
+      // 显示子组件里面的弹窗
+      child.value!.isShowDialog = true
+    })
+
+    // 必须 \`return\` 出去才可以给到 \`<template />\` 使用
+    return {
+      child,
+    }
+  },
+})

需要注意的是,在子组件 Child.vue 里,变量和方法也需要在 setup 里 return 出来才可以被父组件调用到。

子组件通知父组件

子组件如果想主动向父组件通讯,也需要使用 emits ,详细的配置方法可见:绑定 emits

爷孙组件通信

顾名思义,爷孙组件是比 父子组件通信 要更深层次的引用关系(也有称之为 “隔代组件” )。

C 组件被引入到 B 组件里, B 组件又被引入到 A 组件里渲染,此时 A 是 C 的爷爷级别(可能还有更多层级关系),它们之间的关系可以假设如下:

Grandfather.vue
+└─Son.vue
+  └─Grandson.vue

可以看到 Grandson.vue 并非直接挂载在 Grandfather.vue 下面,他们之间还隔着至少一个 Son.vue (在实际业务中可能存在更多层级),如果使用 props ,只能一级组件一级组件传递下去,就太繁琐了。

`,158),C=a('

因此需要更直接的通信方式来解决这种问题,这一 Part 就是讲一讲 C 和 A 之间的数据传递,常用的方法有:

方案爷组件向孙组件孙组件向爷组件对应章节传送门
provide / injectprovideinject点击查看
EventBusemit / onemit / on点击查看
Reactive State--点击查看
Vuex--点击查看
Pinia--点击查看

因为上下级的关系的一致性,爷孙组件通信的方案也适用于 父子组件通信 ,只需要把爷孙关系换成父子关系即可,为了方便阅读,下面的爷组件统一叫 Grandfather.vue,子组件统一叫 Grandson.vue 。

provide / inject

这个通信方式也是有两部分:

  1. Grandfather.vue 通过 provide 向孙组件 Grandson.vue 提供数据和方法
  2. Grandson.vue 通过 inject 注入爷爷组件 Grandfather.vue 的数据和方法

无论组件层次结构有多深,发起 provide 的组件都可以作为其所有下级组件的依赖提供者。

',7),A=a(`

Vue 3 的这一部分内容对比 Vue 2 来说变化很大,但使用起来其实也很简单,开发者学到这里不用慌,它们之间也有相同的地方:

  1. 爷组件不需要知道哪些子组件使用它 provide 的数据
  2. 子组件不需要知道 inject 的数据来自哪里

另外要切记一点就是: provide 和 inject 绑定并不是可响应的,这是刻意为之的,除非传入了一个可侦听的对象。

发起 provide ~new

注:这一小节的步骤是在 Grandfather.vue 里操作。

先来回顾一下 Vue 2 的用法:

ts
export default {
+  // 在 \`data\` 选项里定义好数据
+  data() {
+    return {
+      tags: ['中餐', '粤菜', '烧腊'],
+    }
+  },
+  // 在 \`provide\` 选项里添加要提供的数据
+  provide() {
+    return {
+      tags: this.tags,
+    }
+  },
+}

旧版的 provide 用法和 data 类似,都是配置为一个返回对象的函数,而 Vue 3 的新版 provide ,和 Vue 2 的用法区别比较大。

在 Vue 3 , provide 需要导入并在 setup 里启用,并且现在是一个全新的方法,每次要 provide 一个数据的时候,就要单独调用一次。

provide 的 TS 类型如下:

ts
// \`provide\` API 本身的类型
+function provide<T>(key: InjectionKey<T> | string, value: T): void
+
+// 入参 \`key\` 的其中一种类型
+interface InjectionKey<T> extends Symbol {}

每次调用 provide 的时候都需要传入两个参数:

参数说明
key数据的名称
value数据的值

其中 key 一般使用 string 类型就可以满足大部分业务场景,如果有特殊的需要(例如开发插件时可以避免和用户的业务冲突),可以使用 InjectionKey<T> 类型,这是一个继承自 Symbol 的泛型:

ts
import type { InjectionKey } from 'vue'
+const key = Symbol() as InjectionKey<string>

还需要注意的是, provide 不是响应式的,如果要使其具备响应性,需要传入响应式数据,详见:响应性数据的传递与接收

下面来试试在爷组件 Grandfather.vue 里创建数据 provide 下去:

ts
// Grandfather.vue
+import { defineComponent, provide, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 声明一个响应性变量并 provide 其自身
+    // 孙组件获取后可以保持响应性
+    const msg = ref('Hello World!')
+    provide('msg', msg)
+
+    // 只 provide 响应式变量的值
+    // 孙组件获取后只会得到当前的值
+    provide('msgValue', msg.value)
+
+    // 声明一个方法并 provide
+    function printMsg() {
+      console.log(msg.value)
+    }
+    provide('printMsg', printMsg)
+  },
+})

接收 inject ~new

注:这一小节的步骤是在 Grandson.vue 里操作。

也是先回顾一下在 Vue 2 里的用法,和接收 props 类似:

ts
export default {
+  // 通过 \`inject\` 选项获取
+  inject: ['tags'],
+  mounted() {
+    console.log(this.tags)
+  },
+}

Vue 3 的新版 inject 和 Vue 2 的用法区别也是比较大,在 Vue 3 , inject 和 provide 一样,也是需要先导入然后在 setup 里启用,也是一个全新的方法,每次要 inject 一个数据的时候,也是要单独调用一次。

另外还有一个特殊情况需要注意,当 Grandson.vue 的父级、爷级组件都 provide 了相同名字的数据下来,那么在 inject 的时候,会优先选择离它更近的组件的数据。

根据不同的场景, inject 可以接受不同数量的入参,入参类型也各不相同。

默认用法

默认情况下, inject API 的 TS 类型如下:

ts
function inject<T>(key: InjectionKey<T> | string): T | undefined

每次调用时只需要传入一个参数:

参数类型说明
keystring与 provide 相对应的数据名称

接下来看看如何在孙组件里 inject 爷组件 provide 下来的数据:

ts
// Grandson.vue
+import { defineComponent, inject } from 'vue'
+import type { Ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 获取响应式变量
+    const msg = inject<Ref<string>>('msg')
+    console.log(msg!.value)
+
+    // 获取普通的字符串
+    const msgValue = inject<string>('msgValue')
+    console.log(msgValue)
+
+    // 获取函数
+    const printMsg = inject<() => void>('printMsg')
+    if (typeof printMsg === 'function') {
+      printMsg()
+    }
+  },
+})

可以看到在每个 inject 都使用尖括号 <> 添加了相应的 TS 类型,并且在调用变量的时候都进行了判断,这是因为默认的情况下, inject 除了返回指定类型的数据之外,还默认带上 undefined 作为可能的值。

如果明确数据不会是 undefined ,也可以在后面添加 as 关键字指定其 TS 类型,这样 TypeScript 就不再因为可能出现 undefined 而提示代码有问题。

ts
// Grandson.vue
+import { defineComponent, inject } from 'vue'
+import type { Ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 获取响应式变量
+    const msg = inject('msg') as Ref<string>
+    console.log(msg.value)
+
+    // 获取普通的字符串
+    const msgValue = inject('msgValue') as string
+    console.log(msgValue)
+
+    // 获取函数
+    const printMsg = inject('printMsg') as () => void
+    printMsg()
+  },
+})

设置默认值

inject API 还支持设置默认值,可以接受更多的参数。

默认情况下,只需要传入第二个参数指定默认值即可,此时它的 TS 类型如下,

ts
function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T

对于不可控的情况,建议在 inject 时添加一个兜底的默认值,防止程序报错:

ts
// Grandson.vue
+import { defineComponent, inject, ref } from 'vue'
+import type { Ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 获取响应式变量
+    const msg = inject<Ref<string>>('msg', ref('Hello'))
+    console.log(msg.value)
+
+    // 获取普通的字符串
+    const msgValue = inject<string>('msgValue', 'Hello')
+    console.log(msgValue)
+
+    // 获取函数
+    const printMsg = inject<() => void>('printMsg', () => {
+      console.log('Hello')
+    })
+    printMsg()
+  },
+})

需要注意的是, inject 的什么类型的数据,其默认值也需要保持相同的类型。

工厂函数选项

inject API 在第二个 TS 类型的基础上,还有第三个 TS 类型,可以传入第三个参数:

ts
function inject<T>(
+  key: InjectionKey<T> | string,
+  defaultValue: () => T,
+  treatDefaultAsFactory?: false
+): T

当第二个参数是一个工厂函数,那么可以添加第三个值,将其设置为 true ,此时默认值一定会是其 return 的值。

在 Grandson.vue 里新增一个 inject ,接收一个不存在的函数名,并提供一个工厂函数作为默认值:

ts
// Grandson.vue
+import { defineComponent, inject } from 'vue'
+
+interface Food {
+  name: string
+  count: number
+}
+
+export default defineComponent({
+  setup() {
+    // 获取工厂函数
+    const getFood = inject<() => Food>('nonexistentFunction', () => {
+      return {
+        name: 'Pizza',
+        count: 1,
+      }
+    })
+    console.log(typeof getFood) // function
+
+    const food = getFood()
+    console.log(food) // {name: 'Pizza', count: 1}
+  },
+})

此时因为第三个参数默认为 Falsy 值 ,所以可以得到一个函数作为默认值,并可以调用该函数获得一个 Food 对象。

如果将第三个参数传入为 true ,再运行程序则会在 const food = getFood() 这一行报错:

ts
// Grandson.vue
+import { defineComponent, inject } from 'vue'
+
+interface Food {
+  name: string
+  count: number
+}
+
+export default defineComponent({
+  setup() {
+    // 获取工厂函数
+    const getFood = inject<() => Food>(
+      'nonexistentFunction',
+      () => {
+        return {
+          name: 'Pizza',
+          count: 1,
+        }
+      },
+      true
+    )
+    console.log(typeof getFood) // object
+
+    // 此时下面的代码无法运行
+    // 报错 Uncaught (in promise) TypeError: getMsg is not a function
+    const food = getFood()
+    console.log(food)
+  },
+})

因为此时第三个入参告知 inject ,默认值是一个工厂函数,因此默认值不再是函数本身,而是函数的返回值,所以 typeof getFood 得到的不再是一个 function 而是一个 object

这个参数对于需要通过工厂函数返回数据的情况非常有用!

兄弟组件通信

兄弟组件是指两个组件都挂载在同一个 Father.vue 下,但两个组件之间并没有什么直接的关联,先看看它们的关系:

Father.vue
+├─Brother.vue
+└─LittleBrother.vue

这种层级关系下,如果组件之间要进行通信,目前通常有这两类选择:

  1. 【不推荐】先把数据传给 Father.vue ,再使用 父子组件通信 方案处理
  2. 【推荐】借助 全局组件通信 的方案达到目的

下面的内容将进入全局通信的讲解。

全局组件通信

全局组件通信是指项目下两个任意组件,不管是否有直接关联(例如父子关系、爷孙关系)都可以直接进行交流的通信方案。

举个例子,像下面这种项目结构, B2.vue 可以采用全局通信方案直接向 D2.vue 发起交流,而无需经过它们各自的父组件。

bash
A.vue
+├─B1.vue
+├───C1.vue
+├─────D1.vue
+├─────D2.vue
+├───C2.vue
+├─────D3.vue
+└─B2.vue

常用的方法有:

方案发起方接收方对应章节传送门
EventBusemiton点击查看
Reactive State--点击查看
Vuex--点击查看
Pinia--点击查看

EventBus ~new

EventBus 通常被称之为 “全局事件总线” ,是用在全局范围内通信的一个常用方案,在 Vue 2 时期该方案非常流行,其特点就是 “简单” 、 “灵活” 、 “轻量级” 。

回顾 Vue 2

在 Vue 2 ,使用 EventBus 无需导入第三方插件,可以在项目下的 libs 文件夹里,创建一个名为 eventBus.ts 的文件,导出一个新的 Vue 实例即可。

ts
// src/libs/eventBus.ts
+import Vue from 'vue'
+export default new Vue()

上面短短两句代码已完成了一个 EventBus 的创建,接下来就可以开始进行通信。

先在负责接收事件的组件里,利用 Vue 的生命周期,通过 eventBus.$on 添加事件侦听,通过 eventBus.$off 移除事件侦听。

ts
import eventBus from '@libs/eventBus'
+
+export default {
+  mounted() {
+    // 在组件创建时,添加一个名为 \`hello\` 的事件侦听
+    eventBus.$on('hello', () => {
+      console.log('Hello World')
+    })
+  },
+  beforeDestroy() {
+    // 在组件销毁前,通过 \`hello\` 这个名称移除该事件侦听
+    eventBus.$off('hello')
+  },
+}

然后在另外一个组件里通过 eventBus.$emit 触发事件侦听。

ts
import eventBus from './eventBus'
+
+export default {
+  methods: {
+    sayHello() {
+      // 触发名为 \`hello\` 的事件
+      eventBus.$emit('hello')
+    },
+  },
+}

这样一个简单的全局方案就完成了。

了解 Vue 3 ~new

Vue 3 应用实例不再实现事件触发接口,因此移除了 $on$off$once 这几个事件 API ,无法像 Vue 2 一样利用 Vue 实例创建 EventBus 。

根据官方文档在 事件 API 迁移策略 的推荐,可以使用 mitt 或者 tiny-emitter 等第三方插件实现 EventBus 。

创建 Vue 3 的 EventBus ~new

这里以 mitt 为例,示范如何创建一个 Vue 3 的 EventBus ,首先需要安装它。

npm i mitt

然后在 src/libs 文件夹下,创建一个名为 eventBus.ts 的文件,文件内容和 Vue 2 的写法其实是一样的,只不过是把 Vue 实例换成了 mitt 实例。

ts
// src/libs/eventBus.ts
+import mitt from 'mitt'
+export default mitt()

接下来就可以定义通信的相关事件了,常用的 API 和参数如下:

方法名称作用
on注册一个侦听事件,用于接收数据
emit调用方法发起数据传递
off用来移除侦听事件

on 的参数:

参数类型作用
typestring | symbol方法名
handlerfunction接收到数据之后要做什么处理的回调函数

这里的 handler 建议使用具名函数,因为匿名函数无法销毁。

emit 的参数:

参数类型作用
typestring | symbol与 on 对应的方法名
dataany与 on 对应的,允许接收的数据

off 的参数:

参数类型作用
typestring | symbol与 on 对应的方法名
handlerfunction要被删除的,与 on 对应的 handler 函数名

更多的 API 可以查阅 插件的官方文档 ,在了解了最基本的用法之后,来开始配置一对组件通信。

创建和移除侦听事件 ~new

在需要暴露交流事件的组件里,通过 on 配置好接收方法,同时为了避免路由切换过程中造成事件多次被绑定,从而引起多次触发,需要在适当的时机 off 掉:

ts
import { defineComponent, onBeforeUnmount } from 'vue'
+import eventBus from '@libs/eventBus'
+
+export default defineComponent({
+  setup() {
+    // 声明一个打招呼的方法
+    function sayHi(msg = 'Hello World!') {
+      console.log(msg)
+    }
+
+    // 启用侦听
+    eventBus.on('sayHi', sayHi)
+
+    // 在组件卸载之前移除侦听
+    onBeforeUnmount(() => {
+      eventBus.off('sayHi', sayHi)
+    })
+  },
+})

关于销毁的时机,可以参考 组件的生命周期

调用侦听事件 ~new

在需要调用交流事件的组件里,通过 emit 进行调用:

ts
import { defineComponent } from 'vue'
+import eventBus from '@libs/eventBus'
+
+export default defineComponent({
+  setup() {
+    // 调用打招呼事件,传入消息内容
+    eventBus.emit('sayHi', 'Hello')
+  },
+})

旧项目升级 EventBus

在 Vue 3 的 EventBus 里,可以看到它的 API 和旧版是非常接近的,只是去掉了 $ 符号。

如果要对旧的项目进行升级改造,由于原来都是使用了 $on$emit 等旧的 API ,一个一个组件去修改成新的 API 容易遗漏或者全局替换出错。

因此可以在创建 eventBus.ts 的时候,通过自定义一个 eventBus 对象来挂载 mitt 的 API 。

在 eventBus.ts 里,改成以下代码:

ts
// src/libs/eventBus.ts
+import mitt from 'mitt'
+
+// 初始化一个 mitt 实例
+const emitter = mitt()
+
+// 在导出时使用旧的 API 名称去调用 mitt 的 API
+export default {
+  $on: (...args) => emitter.on(...args),
+  $emit: (...args) => emitter.emit(...args),
+  $off: (...args) => emitter.off(...args),
+}

这样在组件里就可以继续使用 eventBus.$oneventBus.$emit 等旧 API ,不会影响旧项目的升级使用。

Reactive State ~new

在 Vue 3 里,使用响应式的 reactive API 也可以实现一个小型的状态共享库,如果运用在一个简单的 H5 活动页面这样小需求里,完全可以满足使用。

创建状态中心

首先在 src 目录下创建一个 state 文件夹,并添加一个 index.ts 文件,写入以下代码:

ts
// src/state/index.ts
+import { reactive } from 'vue'
+
+// 如果有多个不同业务的内部状态共享
+// 使用具名导出更容易维护
+export const state = reactive({
+  // 设置一个属性并赋予初始值
+  message: 'Hello World',
+
+  // 添加一个更新数据的方法
+  setMessage(msg: string) {
+    this.message = msg
+  },
+})

这就完成了一个简单的 Reactive State 响应式状态中心的创建。

设定状态更新逻辑

接下来在一个组件 Child.vue 的 <script /> 里添加以下代码,分别进行了以下操作:

  1. 打印初始值
  2. 对 state 里的数据启用侦听器
  3. 使用 state 里的方法更新数据
  4. 直接更新 state 的数据
ts
// Child.vue
+import { defineComponent, watch } from 'vue'
+import { state } from '@/state'
+
+export default defineComponent({
+  setup() {
+    console.log(state.message)
+    // Hello World
+
+    // 因为是响应式数据,所以可以侦听数据变化
+    watch(
+      () => state.message,
+      (val) => {
+        console.log('Message 发生变化:', val)
+      }
+    )
+
+    setTimeout(() => {
+      state.setMessage('Hello Hello')
+      // Message 发生变化: Hello Hello
+    }, 1000)
+
+    setTimeout(() => {
+      state.message = 'Hi Hi'
+      // Message 发生变化: Hi Hi
+    }, 2000)
+  },
+})

观察全局状态变化

继续在另外一个组件 Father.vue 里写入以下代码,导入 state 并在 <template /> 渲染其中的数据:

vue
<!-- Father.vue -->
+<template>
+  <div>{{ state.message }}</div>
+  <Child />
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+import Child from '@cp/Child.vue'
+import { state } from '@/state'
+
+export default defineComponent({
+  components: {
+    Child,
+  },
+  setup() {
+    return {
+      state,
+    }
+  },
+})
+</script>

可以观察到当 Child.vue 里的定时器执行时, Father.vue 的视图也会同步得到更新。

一个无需额外插件即可实现的状态中心就这么完成了!

Vuex ~new

Vuex 是 Vue 生态里面非常重要的一个成员,运用于状态管理模式。

它也是一个全局的通信方案,对比 EventBus,Vuex 的功能更多,更灵活,但对应的学习成本和体积也相对较大,通常大型项目才会用上 Vuex 。

在了解之前

摘自 Vuex 仓库 README 文档的一段官方提示:

Pinia is now the new default
The official state management library for Vue has changed to Pinia. Pinia has almost the exact same or enhanced API as Vuex 5, described in Vuex 5 RFC. You could simply consider Pinia as Vuex 5 with a different name. Pinia also works with Vue 2.x as well.
Vuex 3 and 4 will still be maintained. However, it's unlikely to add new functionalities to it. Vuex and Pinia can be installed in the same project. If you're migrating existing Vuex app to Pinia, it might be a suitable option. However, if you're planning to start a new project, we highly recommend using Pinia instead.

意思是 Pinia 已经成为 Vue 生态最新的官方状态管理库,不仅适用于 Vue 3 ,也支持 Vue 2 ,而 Vuex 将进入维护状态,不再增加新功能, Vue 官方强烈建议在新项目中使用 Pinia 。

TIP

笔者建议:如果是全新的项目,建议直接使用 Pinia ,不仅更加适配 Vue 3 组合式 API 的使用,对 TypeScript 的支持也更完善,上手难度和使用舒适度均比 Vuex 更好, Vuex 正在逐渐退出舞台,请根据实际需求决定是否需要启用它。

Vuex 的目录结构

在 Vue 3 里使用 Vuex ,需要选择 4.x 版本,也是当前的 @latest 标签对应的版本,请先安装它。

bash
npm i vuex

接下来按照下面的目录结构创建对应的目录与文件:

bash
src
+ # Vuex 的目录
+├─store
+├───index.ts
+└─main.ts

一般情况下一个 index.ts 足矣,它是 Vuex 的入口文件,如果的项目比较庞大,可以在 store 目录下创建一个命名为 modules 的文件夹,使用 Vuex Modules 的方式导入到 index.ts 里去注册。

回顾 Vue 2

在 Vue 2 ,需要先分别导入 vuevuex,使用 use 方法启用 Vuex 后,通过 new Vuex.Store(...) 的方式进行初始化。

ts
// src/store/index.ts
+import Vue from 'vue'
+import Vuex from 'vuex'
+
+Vue.use(Vuex)
+
+export default new Vuex.Store({
+  state: {},
+  mutations: {},
+  actions: {},
+  modules: {},
+})

之后在组件里就可以通过 this.$store 操作 Vuex 上的方法了。

ts
export default {
+  mounted() {
+    // 通过 \`this.$store\` 操作 Vuex
+    this.$store.commit('increment')
+    console.log(this.$store.state.count)
+  },
+}

了解 Vue 3 ~new

Vue 3 需要从 Vuex 里导入 createStore 创建实例:

ts
// src/store/index.ts
+import { createStore } from 'vuex'
+
+export default createStore({
+  state: {},
+  mutations: {},
+  actions: {},
+  modules: {},
+})

之后在 src/main.ts 里启用 Vuex :

ts
// src/main.ts
+import { createApp } from 'vue'
+import App from './App.vue'
+import store from './store'
+
+createApp(App)
+  .use(store) // 启用 Vuex
+  .mount('#app')

Vue 3 在组件里使用 Vuex 的方式和 Vue 2 有所不同,需要像使用路由那样通过一个组合式 API useStore 启用。

ts
import { defineComponent } from 'vue'
+import { useStore } from 'vuex'
+
+export default defineComponent({
+  setup() {
+    // 需要创建一个 store 变量
+    const store = useStore()
+
+    // 再使用 store 去操作 Vuex 的 API
+    // ...
+  },
+})

Vuex 的配置

除了初始化方式有一定的改变, Vuex 在 Vue 3 的其他配置和 Vue 2 是一样的。

由于现在在 Vue 3 里已经更推荐使用 Pinia , Vuex 已处于维护状态,因此关于 Vuex 的使用将不展开更多的介绍,有需要的开发者可以查看 Vuex 官网的 使用指南 了解更多。

Pinia ~new

Pinia 和 Vuex 一样,也是 Vue 生态里面非常重要的一个成员,也都是运用于全局的状态管理。

但面向 Componsition API 而生的 Pinia ,更受 Vue 3 喜爱,已被钦定为官方推荐的新状态管理工具。

为了阅读上的方便,对 Pinia 单独开了一章,请在 全局状态的管理 一章阅读。

`,156);function d(h,g,u,f,m,v){const e=n("ReadingTips"),o=n("ImgWrap"),l=n("ClientOnly"),t=n("GitalkComment");return r(),y("div",null,[D,s(e),i,s(l,null,{default:p(()=>[s(o,{src:"/assets/img/communication-prop-drilling.png",alt:"Props 的多级传递会非常繁琐(摘自 Vue 官网)"})]),_:1}),C,s(l,null,{default:p(()=>[s(o,{src:"/assets/img/communication-provide-inject.png",alt:"使用 provide / inject 后,问题将变得非常简单(摘自 Vue 官网)"})]),_:1}),A,s(l,null,{default:p(()=>[s(t,{issueId:62})]),_:1})])}const x=c(F,[["render",d]]);export{E as __pageData,x as default}; diff --git a/assets/communication.md.03c12ebf.lean.js b/assets/communication.md.03c12ebf.lean.js new file mode 100644 index 00000000..66811ddf --- /dev/null +++ b/assets/communication.md.03c12ebf.lean.js @@ -0,0 +1,672 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as c,v as r,b as y,t as s,O as p,R as a,M as n}from"./chunks/framework.0d8bea05.js";const E=JSON.parse('{"title":"组件之间的通信","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"communication.md","filePath":"communication.md"}'),F={name:"communication.md"},D=a('

组件之间的通信

经过前面几章的阅读,相信开发者已经可以搭建一个基础的 Vue 3 项目了!

但实际业务开发过程中,还会遇到一些组件之间的通信问题,父子组件通信、兄弟组件通信、爷孙组件通信,还有一些全局通信的场景。

TIP

这一章节的内容,Vue 3 对比 Vue 2 的变化都比较大!

这一章就按使用场景来划分对应的章节吧,在什么场景下遇到问题,也方便快速找到对应的处理办法。

通信场景快速定位
父子组件通信点击查看
爷孙组件通信点击查看
兄弟组件通信点击查看
全局组件通信点击查看
',6),i=a(`

父子组件通信

父子组件通信是指,B 组件引入到 A 组件里渲染,此时 A 是 B 的父级;B 组件的一些数据需要从 A 组件拿,B 组件有时也要告知 A 组件一些数据变化情况。

他们之间的关系如下, Child.vue 是直接挂载在 Father.vue 下面:

bash
# 父组件
+Father.vue
+ # 子组件
+└─Child.vue

常用的方法有:

方案父组件向子组件子组件向父组件对应章节传送门
props / emitspropsemits点击查看
v-model / emitsv-modelemits点击查看
ref / emitsrefemits点击查看
provide / injectprovideinject点击查看
EventBusemit / onemit / on点击查看
Reactive State--点击查看
Vuex--点击查看
Pinia--点击查看

为了方便阅读,下面的父组件统一叫 Father.vue ,子组件统一叫 Child.vue 。

WARNING

在 Vue 2 ,有的开发者可能喜欢用 $attrs / $listeners 来进行通信,但该方案在 Vue 3 已经移除了,详见 移除 $listeners

props / emits

这是 Vue 跨组件通信最常用,也是基础的一个方案,它的通信过程是:

  1. 父组件 Father.vue 通过 props 向子组件 Child.vue 传值
  2. 子组件 Child.vue 则可以通过 emits 向父组件 Father.vue 发起事件通知

最常见的场景就是统一在父组件发起 AJAX 请求,拿到数据后,再根据子组件的渲染需要传递不同的 props 给不同的子组件使用。

下发 props

注:这一小节的步骤是在 Father.vue 里操作。

下发的过程是在 Father.vue 里完成的,父组件在向子组件下发 props 之前,需要导入子组件并启用它作为自身的模板,然后在 setup 里处理好数据并 return 给 <template /> 用。

在 Father.vue 的 <script /> 里:

ts
// Father.vue
+import { defineComponent } from 'vue'
+import Child from '@cp/Child.vue'
+
+interface Member {
+  id: number
+  name: string
+}
+
+export default defineComponent({
+  // 需要启用子组件作为模板
+  components: {
+    Child,
+  },
+
+  // 定义一些数据并 \`return\` 给 \`<template />\` 用
+  setup() {
+    const userInfo: Member = {
+      id: 1,
+      name: 'Petter',
+    }
+
+    // 不要忘记 \`return\` ,否则 \`<template />\` 拿不到数据
+    return {
+      userInfo,
+    }
+  },
+})

然后在 Father.vue 的 <template /> 这边拿到 return 出来的数据,把要传递的数据通过属性的方式绑定在组件标签上。

vue
<!-- Father.vue -->
+<template>
+  <Child
+    title="用户信息"
+    :index="1"
+    :uid="userInfo.id"
+    :user-name="userInfo.name"
+  />
+</template>

这样就完成了 props 数据的下发。

<template /> 绑定属性这里,如果是普通的字符串,比如上面的 title,则直接给属性名赋值就可以。

如果是变量名,或者其他类型如 numberboolean 等,比如上面的 index,则需要通过属性动态绑定的方式来添加,使用 v-bind: 或者 : 符号进行绑定。

另外官方文档推荐对 camelCase 风格(小驼峰)命名的 props ,在绑定时使用和 HTML attribute 一样的 kebab-case 风格(短横线),例如使用 user-name 代替 userName 传递,详见官网的 传递 prop 的细节 一节。

接收 props

注:这一小节的步骤是在 Child.vue 里操作。

接收的过程是在 Child.vue 里完成的,在 <script /> 部分,子组件通过与 setup 同级的 props 来接收数据。

它可以是一个 string[] 数组,把要接受的变量名放到这个数组里,直接放进来作为数组的 item

ts
// Child.vue
+export default defineComponent({
+  props: ['title', 'index', 'userName', 'uid'],
+})

但这种情况下,使用者不知道这些属性的使用限制,例如是什么类型的值、是否必传等等。

带有类型限制的 props

注:这一小节的步骤是在 Child.vue 里操作。

和 TypeScript 一样,类型限制可以为程序带来更好的健壮性, Vue 的 props 也支持增加类型限制。

相对于传递一个 string[] 类型的数组,更推荐的方式是把 props 定义为一个对象,以对象形式列出,每个 Property 的名称和值分别是各自的名称和类型,只有合法的类型才允许传入。

TIP

注意,和 TS 的类型定义不同, props 这里的类型,首字母需要大写,也就是 JavaScript 的基本类型。

支持的类型有:

类型含义
String字符串
Number数值
Boolean布尔值
Array数组
Object对象
Date日期数据,e.g. new Date()
Function函数,e.g. 普通函数、箭头函数、构造函数
PromisePromise 类型的函数
SymbolSymbol 类型的值

了解了基本的类型限制用法之后,接下来给 props 加上类型限制:

ts
// Child.vue
+export default defineComponent({
+  props: {
+    title: String,
+    index: Number,
+    userName: String,
+    uid: Number,
+  },
+})

现在如果传入不正确的类型,程序就会抛出警告信息,告知开发者必须正确传值。

如果需要对某个 Prop 允许多类型,比如这个 uid 字段,可能是数值,也可能是字符串,那么可以在类型这里,使用一个数组,把允许的类型都加进去。

ts
// Child.vue
+export default defineComponent({
+  props: {
+    // 单类型
+    title: String,
+    index: Number,
+    userName: String,
+
+    // 这里使用了多种类型
+    uid: [Number, String],
+  },
+})

可选以及带有默认值的 props

注:这一小节的步骤是在 Child.vue 里操作。

所有 props 默认都是可选的,如果不传递具体的值,则默认值都是 undefined ,可能引起程序运行崩溃, Vue 支持对可选的 props 设置默认值,也是通过对象的形式配置 props 的选项。

其中支持配置的选项有:

选项类型含义
typestring类型
requiredboolean是否必传, true 代表必传, false 代表可选
defaultanytype 选项的类型相对应的默认值,如果 required 选项是 false ,但这里不设置默认值,则会默认为 undefined
validatorfunction自定义验证函数,需要 return 一个布尔值, true 代表校验通过, false 代表校验不通过,当校验不通过时,控制台会抛出警告信息

了解了配置选项后,接下来再对 props 进行改造,将其中部分选项设置为可选,并提供默认值:

ts
// Child.vue
+export default defineComponent({
+  props: {
+    // 可选,并提供默认值
+    title: {
+      type: String,
+      required: false,
+      default: '默认标题',
+    },
+
+    // 默认可选,单类型
+    index: Number,
+
+    // 添加一些自定义校验
+    userName: {
+      type: String,
+
+      // 在这里校验用户名必须至少 3 个字
+      validator: (v) => v.length >= 3,
+    },
+
+    // 默认可选,但允许多种类型
+    uid: [Number, String],
+  },
+})

使用 props ~new

注:这一小节的步骤是在 Child.vue 里操作。

<template /> 部分, Vue 3 的使用方法和 Vue 2 是一样的,比如要渲染父组件传入的 props :

vue
<!-- Child.vue -->
+<template>
+  <p>标题:{{ title }}</p>
+  <p>索引:{{ index }}</p>
+  <p>用户id:{{ uid }}</p>
+  <p>用户名:{{ userName }}</p>
+</template>

但是 <script /> 部分,变化非常大!

在 Vue 2 里,只需要通过 this.uidthis.userName 就可以使用父组件传下来的 Prop ,但是 Vue 3 没有了 this ,所以是通过 setup 的入参进行操作。

ts
// Child.vue
+export default defineComponent({
+  props: {
+    title: String,
+    index: Number,
+    userName: String,
+    uid: Number,
+  },
+
+  // 在这里需要添加一个入参
+  setup(props) {
+    // 该入参包含了当前组件定义的所有 props
+    console.log(props)
+  },
+})

关于 Setup 函数的第一个入参 props

  1. 该入参包含了当前组件定义的所有 props (如果父组件 Father.vue 传进来的数据在 Child.vue 里未定义,不仅不会拿到,并且在控制台会有警告信息)。
  2. 该入参可以随意命名,比如可以写成一个下划线 _ ,通过 _.uid 也可以拿到数据,但是语义化命名是一个良好的编程习惯。
  3. 该入参具备响应性,父组件修改了传递下来的值,子组件也会同步得到更新,因此请不要直接解构,可以通过 toRef 或 toRefs API 转换为响应式变量

传递非 props 的属性

上一小节最后有一句提示是:

如果父组件 Father.vue 传进来的数据在 Child.vue 里未定义,不仅不会拿到,并且在控制台会有警告信息。

这种情况虽然无法从 props 里拿到对应的数据,但也不意味着不能传递任何未定义的属性数据,在父组件,除了可以给子组件绑定 props ,还可以根据实际需要去绑定一些特殊的属性。

比如给子组件设置 classid,或者 data-xxx 之类的一些自定义属性,如果子组件 Child.vue 的 <template /> 里只有一个根节点,那么这些属性默认会自动继承并渲染在 Node 节点上。

假设当前在子组件 Child.vue 是如下这样只有一个根节点,并且未接收任何 props :

TIP

如果已安装 Vue VSCode Snippets 这个 VSCode 插件,可以在空的 .vue 文件里输入 v3 ,在出现的代码片段菜单里选择 vbase-3-ts 生成一个 Vue 组件的基础代码片段。

vue
<!-- Child.vue -->
+<template>
+  <div class="child">子组件</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup() {
+    return {}
+  },
+})
+</script>
+
+<style scoped>
+.child {
+  width: 100%;
+}
+</style>

在 Father.vue 里对 Child.vue 传递了多个属性:

vue
<!-- Father.vue -->
+<template>
+  <Child
+    id="child-component"
+    class="class-name-from-father"
+    :keys="['foo', 'bar']"
+    :obj="{ foo: 'bar' }"
+    data-hash="b10a8db164e0754105b7a99be72e3fe5"
+  />
+</template>

回到浏览器,通过 Chrome 的审查元素可以看到子组件 Child.vue 在渲染后,按照 HTML 属性的渲染规则生成了多个属性:

html
<!-- Child.vue 在浏览器里渲染后的 HTML DOM 结构 -->
+<div
+  class="child class-name-from-father"
+  id="child-component"
+  keys="foo,bar"
+  obj="[object Object]"
+  data-hash="b10a8db164e0754105b7a99be72e3fe5"
+  data-v-2dcc19c8=""
+  data-v-7eb2bc79=""
+>
+  子组件
+</div>

TIP

其中有两个以 data-v- 开头的属性是 <style /> 标签开启了 Style Scoped 功能自动生成的 Hash 值。

可以在 Child.vue 配置 inheritAttrsfalse 来屏蔽这些非 props 属性的渲染。

ts
// Child.vue
+export default defineComponent({
+  inheritAttrs: false,
+  setup() {
+    // ...
+  },
+})

关闭了 之后,现在的 DOM 结构如下,只保留了两个由 Style Scoped 生成的 Hash 值:

html
<!-- Child.vue 在浏览器里渲染后的 HTML DOM 解构 -->
+<div class="child" data-v-2dcc19c8="" data-v-7eb2bc79="">子组件</div>

这一类非 props 属性通常称之为 attrs 。

刚接触 Vue 的开发者可能容易混淆这两者,确实是非常接近,都是由父组件传递,由子组件接收,支持传递的数据类型也一样,但为什么一部分是在 props 获取,一部分在 attrs 获取呢?笔者给出一个比较容易记忆的方式,不一定特别准确,但相信可以帮助开发者加深两者的区别理解。

根据它们的缩写,其实是可以知道 Prop 是指 Property ,而 Attr 是指 Attribute ,虽然都是 “属性” ,但 Property 更接近于事物本身的属性,因此需要在组件里声明,而 Attribute 更偏向于赋予的属性,因此用于指代父组件传递的其他未被声明为 Property 的属性。

获取非 props 的属性 ~new

注:这一小节的步骤是在 Child.vue 里操作。

在上一小节 传递非 props 的属性 已经在父组件 Father.vue 里向子组件 Child.vue 传递了一些 attrs 自定义属性,在子组件里想要拿到这些属性,使用原生 JavaScript 操作是需要通过 Element.getAttribute() 方法,但 Vue 提供了更简单的操作方式。

在 Child.vue 里,可以通过 setup 的第二个参数 context 里的 attrs 来获取到这些属性,并且父组件传递了什么类型的值,获取到的也是一样的类型,这一点和使用 Element.getAttribute() 完全不同。

ts
// Child.vue
+export default defineComponent({
+  setup(props, { attrs }) {
+    // \`attrs\` 是个对象,每个 Attribute 都是它的 \`key\`
+    console.log(attrs.id) // child-component
+    console.log(attrs.class) // class-name-from-father
+
+    // 传递数组会被保留类型,不会被转换为 \`key1,key2\` 这样的字符串
+    // 这一点与 \`Element.getAttribute()\` 完全不同
+    console.log(attrs.keys) // ['foo', 'bar']
+
+    // 传递对象也可以正常获取
+    console.log(attrs.obj) // {foo: 'bar'}
+
+    // 如果传下来的 Attribute 带有短横线,需要通过这种方式获取
+    console.log(attrs['data-hash']) // b10a8db164e0754105b7a99be72e3fe5
+  },
+})

TIP

子组件不论是否设置 inheritAttrs 属性,都可以通过 attrs 拿到父组件传递下来的数据,但是如果要使用 Element.getAttribute() 则只有当 inheritAttrstrue 的时候才可以,因为此时在 DOM 上才会渲染这些属性。

与 Vue 2 的 <template /> 只能有一个根节点不同, Vue 3 允许多个根节点,多个根节点的情况下,无法直接继承这些 attrs 属性(在 inheritAttrs: true 的情况也下无法默认继承),需要在子组件 Child.vue 里通过 v-bind 绑定到要继承在节点上。

可以通过 Vue 实例属性 $attrs 或者从 setup 函数里把 attrs return 出来使用。

vue
<!-- Child.vue -->
+<template>
+  <!-- 默认不会继承属性 -->
+  <div class="child">不会继承</div>
+
+  <!-- 绑定后可继承, \`$attrs\` 是一个 Vue 提供的实例属性 -->
+  <div class="child" v-bind="$attrs">使用 $attrs 继承</div>
+
+  <!-- 绑定后可继承, \`attrs\` 是从 \`setup\` 里 \`return\` 出来的变量 -->
+  <div class="child" v-bind="attrs">使用 attrs 继承</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup(props, { attrs }) {
+
+    return {
+      attrs,
+    }
+  },
+})
+</script>

绑定 emits ~new

注:这一小节的步骤是在 Father.vue 里操作。

如果父组件 Father.vue 需要获取子组件 Child.vue 的数据更新情况,可以由子组件通过 emits 进行通知,下面这个更新用户年龄的例子可以学习如何给子组件绑定 emit 事件。

事件的逻辑是由父组件决定的,因此需要在父组件 Father.vue 的 <script /> 里先声明数据变量和一个更新函数,并且这个更新函数通常会有一个入参作为数据的新值接收。

在本例子里,父组件声明了一个 updateAge 方法,它接受一个入参 newAge ,代表新的年龄数据,这个入参的值将由子组件 Child.vue 在触发 emits 时传入。

因为还需要在 <template /> 部分绑定给子组件,所以请记得 return 出来。

ts
// Father.vue
+import { defineComponent, reactive } from 'vue'
+import Child from '@cp/Child.vue'
+
+interface Member {
+  id: number
+  name: string
+  age: number
+}
+
+export default defineComponent({
+  components: {
+    Child,
+  },
+  setup() {
+    const userInfo: Member = reactive({
+      id: 1,
+      name: 'Petter',
+      age: 0,
+    })
+
+    /**
+     * 声明一个更新年龄的方法
+     * @param newAge - 新的年龄,由子组件触发 emits 时传递
+     */
+    function updateAge(newAge: number) {
+      userInfo.age = newAge
+    }
+
+    return {
+      userInfo,
+      updateAge,
+    }
+  },
+})

再看 Father.vue 的 <template /> 部分,和 Click 事件使用 @click 一样,自定义的 emits 事件也是通过 v-on 或者是 @ 来绑定:

vue
<!-- Father.vue -->
+<template>
+  <Child @update-age="updateAge" />
+</template>

和 props 一样,官方文档也推荐将 camelCase 风格(小驼峰)命名的函数,在绑定时使用 kebab-case 风格(短横线),例如使用 update-age 代替 updateAge 传递。

接收并调用 emits ~new

注:这一小节的步骤是在 Child.vue 里操作。

和 props 一样,可以指定是一个数组,把要接收的 emit 事件名称写进去:

ts
// Child.vue
+export default defineComponent({
+  emits: ['update-age'],
+})

和 props 不同,通常情况下 emits 这样配置就足够使用了。

接下来如果子组件需要更新数据并通知父组件,可以使用 setup 第二个参数 context 里的 emit 方法触发:

ts
// Child.vue
+export default defineComponent({
+  emits: ['update-age'],
+  setup(props, { emit }) {
+    // 通知父组件将年龄设置为 \`2\`
+    emit('update-age', 2)
+  },
+})

emit 方法最少要传递一个参数:事件名称。

事件名称是指父组件 Father.vue 绑定事件时 @update-age="updateAge" 里的 update-age ,如果改成 @hello="updateAge" ,那么事件名称就需要使用 hello ,一般情况下事件名称和更新函数的名称会保持一致,方便维护。

对于需要更新数据的情况, emit 还支持传递更多的参数,对应更新函数里的入参,所以可以看到上面例子里的 emit('update-age', 2) 有第二个参数,传递了一个 2 的数值,就是作为父组件 updateAge 的入参 newAge 传递。

如果需要通信的数据很多,建议第二个入参使用一个对象来管理数据,例如父组件调整为:

ts
// Father.vue
+function updateInfo({ name, age }: Member) {
+  // 当 \`name\` 变化时更新 \`name\` 的值
+  if (name && name !== userInfo.name) {
+    userInfo.name = name
+  }
+
+  // 当 \`age\` 变化并且新值在正确的范围内时,更新 \`age\` 的值
+  if (age > 0 && age !== userInfo.age) {
+    userInfo.age = age
+  }
+}

子组件在传递新数据时,就应该使用对象的形式传递:

ts
// Child.vue
+emit('update-info', {
+  name: 'Tom',
+  age: 18,
+})

这对于更新表单等数据量较多的场景非常好用。

接收 emits 时做一些校验

注:这一小节的步骤是在 Child.vue 里操作。

和 props 一样,子组件在接收 emits 时也可以对这些事件做一些验证,这个时候就需要将 emits 配置为对象,然后把事件名称作为 keyvalue 则对应为一个用来校验的方法。

还是用回上文那个更新年龄的方法,如果需要增加一个条件:当达到成年人的年龄时才会更新父组件的数据,那么就可以将 emits 调整为:

ts
// Child.vue
+export default defineComponent({
+  emits: {
+    // 需要校验
+    'update-age': (age: number) => {
+      // 写一些条件拦截,返回 \`false\` 表示验证不通过
+      if (age < 18) {
+        console.log('未成年人不允许参与')
+        return false
+      }
+
+      // 通过则返回 \`true\`
+      return true
+    },
+
+    // 一些无需校验的,设置为 \`null\` 即可
+    'update-name': null,
+  },
+})

接下来如果提交 emit('update-age', 2) ,因为不满足验证条件,浏览器控制台将会出现一段 [Vue warn]: Invalid event arguments: event validation failed for event "update-age". 这样的警告信息。

v-model / emits

相对于 props / emits 这一对通信方案,使用 v-model 的方式更为简单:

  1. 在 Father.vue ,通过 v-model 向 Child.vue 传值

  2. Child.vue 通过自身设定的 emits 向 Father.vue 通知数据更新

v-model 的用法和 props 非常相似,但是很多操作上更为简化,但操作简单带来的 “副作用” ,就是功能上也没有 props 那么多。

绑定 v-model ~new

注:这一小节的步骤是在 Father.vue 里操作。

和下发 props 的方式类似,都是在子组件上绑定 Father.vue 定义好的数据,这是绑定一个数据的例子:

vue
<!-- Father.vue -->
+<template>
+  <Child v-model:username="userInfo.name" />
+</template>

和 Vue 2 不同, Vue 3 可以直接绑定 v-model ,而无需在子组件指定 model 选项 ,并且 Vue 3 的 v-model 需要使用英文冒号 : 指定要绑定的属性名,同时也支持绑定多个 v-model 。

如果要绑定多个数据,写多个 v-model 即可:

vue
<!-- Father.vue -->
+<template>
+  <Child
+    v-model:uid="userInfo.id"
+    v-model:username="userInfo.name"
+    v-model:age="userInfo.age"
+  />
+</template>

看到这里应该能明白了,一个 v-model 其实就是一个 prop ,它支持的数据类型和 prop 是一样的,所以子组件在接收数据的时候,完全按照 props 去定义就可以了。

点击回顾:接收 props ,了解在 Child.vue 如何接收 props,以及相关的 props 类型限制等部分内容。

配置 emits ~new

注:这一小节的步骤是在 Child.vue 里操作。

虽然 v-model 的配置和 props 相似,但是为什么出这么两个相似的东西?自然是为了简化一些开发上的操作。

使用 props / emits ,如果要更新父组件的数据,还需要在父组件声明一个更新函数并绑定事件给子组件,才能够更新。

而使用 v-model / emits ,无需在父组件声明更新函数,只需要在子组件 Child.vue 里通过 update: 前缀加上 v-model 的属性名这样的格式,即可直接定义一个更新事件。

ts
// Child.vue
+export default defineComponent({
+  props: {
+    uid: Number,
+    username: String,
+    age: Number,
+  },
+  // 注意这里的 \`update:\` 前缀
+  emits: ['update:uid', 'update:username', 'update:age'],
+})

这里的 update 后面的属性名,支持驼峰写法,这一部分和 Vue 2 的使用是相同的。

在配置 emits 时,也可以对数据更新做一些校验,配置方式和讲解 props / emits 时 接收 emits 时做一些校验 这一小节的操作是一样的。

在 Child.vue 配置好 emits 之后,就可以在 setup 里直接操作数据的更新了:

ts
// Child.vue
+export default defineComponent({
+  setup(props, { emit }) {
+    // 2s 后更新用户名
+    setTimeout(() => {
+      emit('update:username', 'Tom')
+    }, 2000)
+  },
+})

子组件通过调用 emit('update:xxx') 即可让父组件更新对应的数据。

ref / emits

在学习 响应式 API 之 ref 的时候,已讲解过 ref 是可以用在 DOM 元素与子组件 上面,所以也可以使用 ref 配合 emits 完成父子组件的通信。

父组件操作子组件 ~new

注:这一小节的步骤是在 Father.vue 里操作。

父组件可以给子组件绑定 ref 属性,然后通过 Ref 变量操作子组件的数据或者调用子组件里面的方法。

先在 <template /> 处给子组件标签绑定 ref 属性:

vue
<!-- Father.vue -->
+<template>
+  <Child ref="child" />
+</template>

然后在 <script /> 部分定义好对应的变量名称 child (记得要 return 出来哦),即可通过该变量操作子组件上的变量或方法:

ts
// Father.vue
+import { defineComponent, onMounted, ref } from 'vue'
+import Child from '@cp/Child.vue'
+
+export default defineComponent({
+  components: {
+    Child,
+  },
+  setup() {
+    // 给子组件定义一个 \`ref\` 变量
+    const child = ref<InstanceType<typeof Child>>()
+
+    // 请保证视图渲染完毕后再执行操作
+    onMounted(async () => {
+      // 执行子组件里面的 AJAX 请求函数
+      await child.value!.queryList()
+
+      // 显示子组件里面的弹窗
+      child.value!.isShowDialog = true
+    })
+
+    // 必须 \`return\` 出去才可以给到 \`<template />\` 使用
+    return {
+      child,
+    }
+  },
+})

需要注意的是,在子组件 Child.vue 里,变量和方法也需要在 setup 里 return 出来才可以被父组件调用到。

子组件通知父组件

子组件如果想主动向父组件通讯,也需要使用 emits ,详细的配置方法可见:绑定 emits

爷孙组件通信

顾名思义,爷孙组件是比 父子组件通信 要更深层次的引用关系(也有称之为 “隔代组件” )。

C 组件被引入到 B 组件里, B 组件又被引入到 A 组件里渲染,此时 A 是 C 的爷爷级别(可能还有更多层级关系),它们之间的关系可以假设如下:

Grandfather.vue
+└─Son.vue
+  └─Grandson.vue

可以看到 Grandson.vue 并非直接挂载在 Grandfather.vue 下面,他们之间还隔着至少一个 Son.vue (在实际业务中可能存在更多层级),如果使用 props ,只能一级组件一级组件传递下去,就太繁琐了。

`,158),C=a('

因此需要更直接的通信方式来解决这种问题,这一 Part 就是讲一讲 C 和 A 之间的数据传递,常用的方法有:

方案爷组件向孙组件孙组件向爷组件对应章节传送门
provide / injectprovideinject点击查看
EventBusemit / onemit / on点击查看
Reactive State--点击查看
Vuex--点击查看
Pinia--点击查看

因为上下级的关系的一致性,爷孙组件通信的方案也适用于 父子组件通信 ,只需要把爷孙关系换成父子关系即可,为了方便阅读,下面的爷组件统一叫 Grandfather.vue,子组件统一叫 Grandson.vue 。

provide / inject

这个通信方式也是有两部分:

  1. Grandfather.vue 通过 provide 向孙组件 Grandson.vue 提供数据和方法
  2. Grandson.vue 通过 inject 注入爷爷组件 Grandfather.vue 的数据和方法

无论组件层次结构有多深,发起 provide 的组件都可以作为其所有下级组件的依赖提供者。

',7),A=a(`

Vue 3 的这一部分内容对比 Vue 2 来说变化很大,但使用起来其实也很简单,开发者学到这里不用慌,它们之间也有相同的地方:

  1. 爷组件不需要知道哪些子组件使用它 provide 的数据
  2. 子组件不需要知道 inject 的数据来自哪里

另外要切记一点就是: provide 和 inject 绑定并不是可响应的,这是刻意为之的,除非传入了一个可侦听的对象。

发起 provide ~new

注:这一小节的步骤是在 Grandfather.vue 里操作。

先来回顾一下 Vue 2 的用法:

ts
export default {
+  // 在 \`data\` 选项里定义好数据
+  data() {
+    return {
+      tags: ['中餐', '粤菜', '烧腊'],
+    }
+  },
+  // 在 \`provide\` 选项里添加要提供的数据
+  provide() {
+    return {
+      tags: this.tags,
+    }
+  },
+}

旧版的 provide 用法和 data 类似,都是配置为一个返回对象的函数,而 Vue 3 的新版 provide ,和 Vue 2 的用法区别比较大。

在 Vue 3 , provide 需要导入并在 setup 里启用,并且现在是一个全新的方法,每次要 provide 一个数据的时候,就要单独调用一次。

provide 的 TS 类型如下:

ts
// \`provide\` API 本身的类型
+function provide<T>(key: InjectionKey<T> | string, value: T): void
+
+// 入参 \`key\` 的其中一种类型
+interface InjectionKey<T> extends Symbol {}

每次调用 provide 的时候都需要传入两个参数:

参数说明
key数据的名称
value数据的值

其中 key 一般使用 string 类型就可以满足大部分业务场景,如果有特殊的需要(例如开发插件时可以避免和用户的业务冲突),可以使用 InjectionKey<T> 类型,这是一个继承自 Symbol 的泛型:

ts
import type { InjectionKey } from 'vue'
+const key = Symbol() as InjectionKey<string>

还需要注意的是, provide 不是响应式的,如果要使其具备响应性,需要传入响应式数据,详见:响应性数据的传递与接收

下面来试试在爷组件 Grandfather.vue 里创建数据 provide 下去:

ts
// Grandfather.vue
+import { defineComponent, provide, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 声明一个响应性变量并 provide 其自身
+    // 孙组件获取后可以保持响应性
+    const msg = ref('Hello World!')
+    provide('msg', msg)
+
+    // 只 provide 响应式变量的值
+    // 孙组件获取后只会得到当前的值
+    provide('msgValue', msg.value)
+
+    // 声明一个方法并 provide
+    function printMsg() {
+      console.log(msg.value)
+    }
+    provide('printMsg', printMsg)
+  },
+})

接收 inject ~new

注:这一小节的步骤是在 Grandson.vue 里操作。

也是先回顾一下在 Vue 2 里的用法,和接收 props 类似:

ts
export default {
+  // 通过 \`inject\` 选项获取
+  inject: ['tags'],
+  mounted() {
+    console.log(this.tags)
+  },
+}

Vue 3 的新版 inject 和 Vue 2 的用法区别也是比较大,在 Vue 3 , inject 和 provide 一样,也是需要先导入然后在 setup 里启用,也是一个全新的方法,每次要 inject 一个数据的时候,也是要单独调用一次。

另外还有一个特殊情况需要注意,当 Grandson.vue 的父级、爷级组件都 provide 了相同名字的数据下来,那么在 inject 的时候,会优先选择离它更近的组件的数据。

根据不同的场景, inject 可以接受不同数量的入参,入参类型也各不相同。

默认用法

默认情况下, inject API 的 TS 类型如下:

ts
function inject<T>(key: InjectionKey<T> | string): T | undefined

每次调用时只需要传入一个参数:

参数类型说明
keystring与 provide 相对应的数据名称

接下来看看如何在孙组件里 inject 爷组件 provide 下来的数据:

ts
// Grandson.vue
+import { defineComponent, inject } from 'vue'
+import type { Ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 获取响应式变量
+    const msg = inject<Ref<string>>('msg')
+    console.log(msg!.value)
+
+    // 获取普通的字符串
+    const msgValue = inject<string>('msgValue')
+    console.log(msgValue)
+
+    // 获取函数
+    const printMsg = inject<() => void>('printMsg')
+    if (typeof printMsg === 'function') {
+      printMsg()
+    }
+  },
+})

可以看到在每个 inject 都使用尖括号 <> 添加了相应的 TS 类型,并且在调用变量的时候都进行了判断,这是因为默认的情况下, inject 除了返回指定类型的数据之外,还默认带上 undefined 作为可能的值。

如果明确数据不会是 undefined ,也可以在后面添加 as 关键字指定其 TS 类型,这样 TypeScript 就不再因为可能出现 undefined 而提示代码有问题。

ts
// Grandson.vue
+import { defineComponent, inject } from 'vue'
+import type { Ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 获取响应式变量
+    const msg = inject('msg') as Ref<string>
+    console.log(msg.value)
+
+    // 获取普通的字符串
+    const msgValue = inject('msgValue') as string
+    console.log(msgValue)
+
+    // 获取函数
+    const printMsg = inject('printMsg') as () => void
+    printMsg()
+  },
+})

设置默认值

inject API 还支持设置默认值,可以接受更多的参数。

默认情况下,只需要传入第二个参数指定默认值即可,此时它的 TS 类型如下,

ts
function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T

对于不可控的情况,建议在 inject 时添加一个兜底的默认值,防止程序报错:

ts
// Grandson.vue
+import { defineComponent, inject, ref } from 'vue'
+import type { Ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 获取响应式变量
+    const msg = inject<Ref<string>>('msg', ref('Hello'))
+    console.log(msg.value)
+
+    // 获取普通的字符串
+    const msgValue = inject<string>('msgValue', 'Hello')
+    console.log(msgValue)
+
+    // 获取函数
+    const printMsg = inject<() => void>('printMsg', () => {
+      console.log('Hello')
+    })
+    printMsg()
+  },
+})

需要注意的是, inject 的什么类型的数据,其默认值也需要保持相同的类型。

工厂函数选项

inject API 在第二个 TS 类型的基础上,还有第三个 TS 类型,可以传入第三个参数:

ts
function inject<T>(
+  key: InjectionKey<T> | string,
+  defaultValue: () => T,
+  treatDefaultAsFactory?: false
+): T

当第二个参数是一个工厂函数,那么可以添加第三个值,将其设置为 true ,此时默认值一定会是其 return 的值。

在 Grandson.vue 里新增一个 inject ,接收一个不存在的函数名,并提供一个工厂函数作为默认值:

ts
// Grandson.vue
+import { defineComponent, inject } from 'vue'
+
+interface Food {
+  name: string
+  count: number
+}
+
+export default defineComponent({
+  setup() {
+    // 获取工厂函数
+    const getFood = inject<() => Food>('nonexistentFunction', () => {
+      return {
+        name: 'Pizza',
+        count: 1,
+      }
+    })
+    console.log(typeof getFood) // function
+
+    const food = getFood()
+    console.log(food) // {name: 'Pizza', count: 1}
+  },
+})

此时因为第三个参数默认为 Falsy 值 ,所以可以得到一个函数作为默认值,并可以调用该函数获得一个 Food 对象。

如果将第三个参数传入为 true ,再运行程序则会在 const food = getFood() 这一行报错:

ts
// Grandson.vue
+import { defineComponent, inject } from 'vue'
+
+interface Food {
+  name: string
+  count: number
+}
+
+export default defineComponent({
+  setup() {
+    // 获取工厂函数
+    const getFood = inject<() => Food>(
+      'nonexistentFunction',
+      () => {
+        return {
+          name: 'Pizza',
+          count: 1,
+        }
+      },
+      true
+    )
+    console.log(typeof getFood) // object
+
+    // 此时下面的代码无法运行
+    // 报错 Uncaught (in promise) TypeError: getMsg is not a function
+    const food = getFood()
+    console.log(food)
+  },
+})

因为此时第三个入参告知 inject ,默认值是一个工厂函数,因此默认值不再是函数本身,而是函数的返回值,所以 typeof getFood 得到的不再是一个 function 而是一个 object

这个参数对于需要通过工厂函数返回数据的情况非常有用!

兄弟组件通信

兄弟组件是指两个组件都挂载在同一个 Father.vue 下,但两个组件之间并没有什么直接的关联,先看看它们的关系:

Father.vue
+├─Brother.vue
+└─LittleBrother.vue

这种层级关系下,如果组件之间要进行通信,目前通常有这两类选择:

  1. 【不推荐】先把数据传给 Father.vue ,再使用 父子组件通信 方案处理
  2. 【推荐】借助 全局组件通信 的方案达到目的

下面的内容将进入全局通信的讲解。

全局组件通信

全局组件通信是指项目下两个任意组件,不管是否有直接关联(例如父子关系、爷孙关系)都可以直接进行交流的通信方案。

举个例子,像下面这种项目结构, B2.vue 可以采用全局通信方案直接向 D2.vue 发起交流,而无需经过它们各自的父组件。

bash
A.vue
+├─B1.vue
+├───C1.vue
+├─────D1.vue
+├─────D2.vue
+├───C2.vue
+├─────D3.vue
+└─B2.vue

常用的方法有:

方案发起方接收方对应章节传送门
EventBusemiton点击查看
Reactive State--点击查看
Vuex--点击查看
Pinia--点击查看

EventBus ~new

EventBus 通常被称之为 “全局事件总线” ,是用在全局范围内通信的一个常用方案,在 Vue 2 时期该方案非常流行,其特点就是 “简单” 、 “灵活” 、 “轻量级” 。

回顾 Vue 2

在 Vue 2 ,使用 EventBus 无需导入第三方插件,可以在项目下的 libs 文件夹里,创建一个名为 eventBus.ts 的文件,导出一个新的 Vue 实例即可。

ts
// src/libs/eventBus.ts
+import Vue from 'vue'
+export default new Vue()

上面短短两句代码已完成了一个 EventBus 的创建,接下来就可以开始进行通信。

先在负责接收事件的组件里,利用 Vue 的生命周期,通过 eventBus.$on 添加事件侦听,通过 eventBus.$off 移除事件侦听。

ts
import eventBus from '@libs/eventBus'
+
+export default {
+  mounted() {
+    // 在组件创建时,添加一个名为 \`hello\` 的事件侦听
+    eventBus.$on('hello', () => {
+      console.log('Hello World')
+    })
+  },
+  beforeDestroy() {
+    // 在组件销毁前,通过 \`hello\` 这个名称移除该事件侦听
+    eventBus.$off('hello')
+  },
+}

然后在另外一个组件里通过 eventBus.$emit 触发事件侦听。

ts
import eventBus from './eventBus'
+
+export default {
+  methods: {
+    sayHello() {
+      // 触发名为 \`hello\` 的事件
+      eventBus.$emit('hello')
+    },
+  },
+}

这样一个简单的全局方案就完成了。

了解 Vue 3 ~new

Vue 3 应用实例不再实现事件触发接口,因此移除了 $on$off$once 这几个事件 API ,无法像 Vue 2 一样利用 Vue 实例创建 EventBus 。

根据官方文档在 事件 API 迁移策略 的推荐,可以使用 mitt 或者 tiny-emitter 等第三方插件实现 EventBus 。

创建 Vue 3 的 EventBus ~new

这里以 mitt 为例,示范如何创建一个 Vue 3 的 EventBus ,首先需要安装它。

npm i mitt

然后在 src/libs 文件夹下,创建一个名为 eventBus.ts 的文件,文件内容和 Vue 2 的写法其实是一样的,只不过是把 Vue 实例换成了 mitt 实例。

ts
// src/libs/eventBus.ts
+import mitt from 'mitt'
+export default mitt()

接下来就可以定义通信的相关事件了,常用的 API 和参数如下:

方法名称作用
on注册一个侦听事件,用于接收数据
emit调用方法发起数据传递
off用来移除侦听事件

on 的参数:

参数类型作用
typestring | symbol方法名
handlerfunction接收到数据之后要做什么处理的回调函数

这里的 handler 建议使用具名函数,因为匿名函数无法销毁。

emit 的参数:

参数类型作用
typestring | symbol与 on 对应的方法名
dataany与 on 对应的,允许接收的数据

off 的参数:

参数类型作用
typestring | symbol与 on 对应的方法名
handlerfunction要被删除的,与 on 对应的 handler 函数名

更多的 API 可以查阅 插件的官方文档 ,在了解了最基本的用法之后,来开始配置一对组件通信。

创建和移除侦听事件 ~new

在需要暴露交流事件的组件里,通过 on 配置好接收方法,同时为了避免路由切换过程中造成事件多次被绑定,从而引起多次触发,需要在适当的时机 off 掉:

ts
import { defineComponent, onBeforeUnmount } from 'vue'
+import eventBus from '@libs/eventBus'
+
+export default defineComponent({
+  setup() {
+    // 声明一个打招呼的方法
+    function sayHi(msg = 'Hello World!') {
+      console.log(msg)
+    }
+
+    // 启用侦听
+    eventBus.on('sayHi', sayHi)
+
+    // 在组件卸载之前移除侦听
+    onBeforeUnmount(() => {
+      eventBus.off('sayHi', sayHi)
+    })
+  },
+})

关于销毁的时机,可以参考 组件的生命周期

调用侦听事件 ~new

在需要调用交流事件的组件里,通过 emit 进行调用:

ts
import { defineComponent } from 'vue'
+import eventBus from '@libs/eventBus'
+
+export default defineComponent({
+  setup() {
+    // 调用打招呼事件,传入消息内容
+    eventBus.emit('sayHi', 'Hello')
+  },
+})

旧项目升级 EventBus

在 Vue 3 的 EventBus 里,可以看到它的 API 和旧版是非常接近的,只是去掉了 $ 符号。

如果要对旧的项目进行升级改造,由于原来都是使用了 $on$emit 等旧的 API ,一个一个组件去修改成新的 API 容易遗漏或者全局替换出错。

因此可以在创建 eventBus.ts 的时候,通过自定义一个 eventBus 对象来挂载 mitt 的 API 。

在 eventBus.ts 里,改成以下代码:

ts
// src/libs/eventBus.ts
+import mitt from 'mitt'
+
+// 初始化一个 mitt 实例
+const emitter = mitt()
+
+// 在导出时使用旧的 API 名称去调用 mitt 的 API
+export default {
+  $on: (...args) => emitter.on(...args),
+  $emit: (...args) => emitter.emit(...args),
+  $off: (...args) => emitter.off(...args),
+}

这样在组件里就可以继续使用 eventBus.$oneventBus.$emit 等旧 API ,不会影响旧项目的升级使用。

Reactive State ~new

在 Vue 3 里,使用响应式的 reactive API 也可以实现一个小型的状态共享库,如果运用在一个简单的 H5 活动页面这样小需求里,完全可以满足使用。

创建状态中心

首先在 src 目录下创建一个 state 文件夹,并添加一个 index.ts 文件,写入以下代码:

ts
// src/state/index.ts
+import { reactive } from 'vue'
+
+// 如果有多个不同业务的内部状态共享
+// 使用具名导出更容易维护
+export const state = reactive({
+  // 设置一个属性并赋予初始值
+  message: 'Hello World',
+
+  // 添加一个更新数据的方法
+  setMessage(msg: string) {
+    this.message = msg
+  },
+})

这就完成了一个简单的 Reactive State 响应式状态中心的创建。

设定状态更新逻辑

接下来在一个组件 Child.vue 的 <script /> 里添加以下代码,分别进行了以下操作:

  1. 打印初始值
  2. 对 state 里的数据启用侦听器
  3. 使用 state 里的方法更新数据
  4. 直接更新 state 的数据
ts
// Child.vue
+import { defineComponent, watch } from 'vue'
+import { state } from '@/state'
+
+export default defineComponent({
+  setup() {
+    console.log(state.message)
+    // Hello World
+
+    // 因为是响应式数据,所以可以侦听数据变化
+    watch(
+      () => state.message,
+      (val) => {
+        console.log('Message 发生变化:', val)
+      }
+    )
+
+    setTimeout(() => {
+      state.setMessage('Hello Hello')
+      // Message 发生变化: Hello Hello
+    }, 1000)
+
+    setTimeout(() => {
+      state.message = 'Hi Hi'
+      // Message 发生变化: Hi Hi
+    }, 2000)
+  },
+})

观察全局状态变化

继续在另外一个组件 Father.vue 里写入以下代码,导入 state 并在 <template /> 渲染其中的数据:

vue
<!-- Father.vue -->
+<template>
+  <div>{{ state.message }}</div>
+  <Child />
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+import Child from '@cp/Child.vue'
+import { state } from '@/state'
+
+export default defineComponent({
+  components: {
+    Child,
+  },
+  setup() {
+    return {
+      state,
+    }
+  },
+})
+</script>

可以观察到当 Child.vue 里的定时器执行时, Father.vue 的视图也会同步得到更新。

一个无需额外插件即可实现的状态中心就这么完成了!

Vuex ~new

Vuex 是 Vue 生态里面非常重要的一个成员,运用于状态管理模式。

它也是一个全局的通信方案,对比 EventBus,Vuex 的功能更多,更灵活,但对应的学习成本和体积也相对较大,通常大型项目才会用上 Vuex 。

在了解之前

摘自 Vuex 仓库 README 文档的一段官方提示:

Pinia is now the new default
The official state management library for Vue has changed to Pinia. Pinia has almost the exact same or enhanced API as Vuex 5, described in Vuex 5 RFC. You could simply consider Pinia as Vuex 5 with a different name. Pinia also works with Vue 2.x as well.
Vuex 3 and 4 will still be maintained. However, it's unlikely to add new functionalities to it. Vuex and Pinia can be installed in the same project. If you're migrating existing Vuex app to Pinia, it might be a suitable option. However, if you're planning to start a new project, we highly recommend using Pinia instead.

意思是 Pinia 已经成为 Vue 生态最新的官方状态管理库,不仅适用于 Vue 3 ,也支持 Vue 2 ,而 Vuex 将进入维护状态,不再增加新功能, Vue 官方强烈建议在新项目中使用 Pinia 。

TIP

笔者建议:如果是全新的项目,建议直接使用 Pinia ,不仅更加适配 Vue 3 组合式 API 的使用,对 TypeScript 的支持也更完善,上手难度和使用舒适度均比 Vuex 更好, Vuex 正在逐渐退出舞台,请根据实际需求决定是否需要启用它。

Vuex 的目录结构

在 Vue 3 里使用 Vuex ,需要选择 4.x 版本,也是当前的 @latest 标签对应的版本,请先安装它。

bash
npm i vuex

接下来按照下面的目录结构创建对应的目录与文件:

bash
src
+ # Vuex 的目录
+├─store
+├───index.ts
+└─main.ts

一般情况下一个 index.ts 足矣,它是 Vuex 的入口文件,如果的项目比较庞大,可以在 store 目录下创建一个命名为 modules 的文件夹,使用 Vuex Modules 的方式导入到 index.ts 里去注册。

回顾 Vue 2

在 Vue 2 ,需要先分别导入 vuevuex,使用 use 方法启用 Vuex 后,通过 new Vuex.Store(...) 的方式进行初始化。

ts
// src/store/index.ts
+import Vue from 'vue'
+import Vuex from 'vuex'
+
+Vue.use(Vuex)
+
+export default new Vuex.Store({
+  state: {},
+  mutations: {},
+  actions: {},
+  modules: {},
+})

之后在组件里就可以通过 this.$store 操作 Vuex 上的方法了。

ts
export default {
+  mounted() {
+    // 通过 \`this.$store\` 操作 Vuex
+    this.$store.commit('increment')
+    console.log(this.$store.state.count)
+  },
+}

了解 Vue 3 ~new

Vue 3 需要从 Vuex 里导入 createStore 创建实例:

ts
// src/store/index.ts
+import { createStore } from 'vuex'
+
+export default createStore({
+  state: {},
+  mutations: {},
+  actions: {},
+  modules: {},
+})

之后在 src/main.ts 里启用 Vuex :

ts
// src/main.ts
+import { createApp } from 'vue'
+import App from './App.vue'
+import store from './store'
+
+createApp(App)
+  .use(store) // 启用 Vuex
+  .mount('#app')

Vue 3 在组件里使用 Vuex 的方式和 Vue 2 有所不同,需要像使用路由那样通过一个组合式 API useStore 启用。

ts
import { defineComponent } from 'vue'
+import { useStore } from 'vuex'
+
+export default defineComponent({
+  setup() {
+    // 需要创建一个 store 变量
+    const store = useStore()
+
+    // 再使用 store 去操作 Vuex 的 API
+    // ...
+  },
+})

Vuex 的配置

除了初始化方式有一定的改变, Vuex 在 Vue 3 的其他配置和 Vue 2 是一样的。

由于现在在 Vue 3 里已经更推荐使用 Pinia , Vuex 已处于维护状态,因此关于 Vuex 的使用将不展开更多的介绍,有需要的开发者可以查看 Vuex 官网的 使用指南 了解更多。

Pinia ~new

Pinia 和 Vuex 一样,也是 Vue 生态里面非常重要的一个成员,也都是运用于全局的状态管理。

但面向 Componsition API 而生的 Pinia ,更受 Vue 3 喜爱,已被钦定为官方推荐的新状态管理工具。

为了阅读上的方便,对 Pinia 单独开了一章,请在 全局状态的管理 一章阅读。

`,156);function d(h,g,u,f,m,v){const e=n("ReadingTips"),o=n("ImgWrap"),l=n("ClientOnly"),t=n("GitalkComment");return r(),y("div",null,[D,s(e),i,s(l,null,{default:p(()=>[s(o,{src:"/assets/img/communication-prop-drilling.png",alt:"Props 的多级传递会非常繁琐(摘自 Vue 官网)"})]),_:1}),C,s(l,null,{default:p(()=>[s(o,{src:"/assets/img/communication-provide-inject.png",alt:"使用 provide / inject 后,问题将变得非常简单(摘自 Vue 官网)"})]),_:1}),A,s(l,null,{default:p(()=>[s(t,{issueId:62})]),_:1})])}const x=c(F,[["render",d]]);export{E as __pageData,x as default}; diff --git a/assets/component.md.b59f8845.js b/assets/component.md.b59f8845.js new file mode 100644 index 00000000..2d260347 --- /dev/null +++ b/assets/component.md.b59f8845.js @@ -0,0 +1,1949 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as e,v as t,b as c,t as s,O as r,R as a,M as n}from"./chunks/framework.0d8bea05.js";const m=JSON.parse('{"title":"单组件的编写","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"component.md","filePath":"component.md"}'),y={name:"component.md"},F=a('

单组件的编写

项目搭好了,第一个需要了解的是 Vue 组件的变化,由于这部分篇幅会非常大,所以会分成很多个小节,一部分一部分按照开发顺序来逐步了解。

因为 Vue 3 对 TypeScript 的支持真的是太完善了,并且 TypeScript 的发展趋势和市场需求度越来越高,所以接下来都将直接使用 TypeScript 进行编程。

TIP

对 TypeScript 不太熟悉的开发者,建议先阅读 快速上手 TypeScript 一章,有了一定的语言基础之后,再一边写代码一边加深印象。

',4),D=a(`

全新的 setup 函数 ~new

在开始编写 Vue 组件之前,需要了解两个全新的前置知识点:

  • 全新的 setup 函数,关系到组件的生命周期和渲染等问题
  • 写 TypeScript 组件离不开的 defineComponent API

setup 的含义

Vue 3 的 Composition API 系列里,推出了一个全新的 setup 函数,它是一个组件选项,在创建组件之前执行,一旦 props 被解析,并作为组合式 API 的入口点。

TIP

说的通俗一点,就是在使用 Vue 3 生命周期的情况下,整个组件相关的业务代码,都可以放在 setup 里执行。

因为在 setup 之后,其他的生命周期才会被启用(点击了解:组件的生命周期)。

基本语法:

ts
// 这是一个基于 TypeScript 的 Vue 组件
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup(props, context) {
+    // 在这里声明数据,或者编写函数并在这里执行它
+
+    return {
+      // 需要给 \`<template />\` 用的数据或函数,在这里 \`return\` 出去
+    }
+  },
+})

可以发现在这段代码里还导入了一个 defineComponent API ,也是 Vue 3 带来的新功能,下文的 defineComponent 的作用 将介绍其用法。

在使用 setup 的情况下,请牢记一点:不能再用 this 来获取 Vue 实例,也就是无法和 Vue 2 一样,通过 this.foothis.bar() 这样来获取实例上的数据,或者执行实例上的方法。

关于全新的 Vue 3 组件编写,笔者将在下文一步步说明。

setup 的参数使用

setup 函数包含了两个入参:

参数类型含义是否必传
propsobject由父组件传递下来的数据
contextobject组件的执行上下文

第一个参数 props

它是响应式的,当父组件传入新的数据时,它将被更新。

TIP

请不要解构它,这样会让数据失去响应性,一旦父组件发生数据变化,解构后的变量将无法同步更新为最新的值。

可以使用 Vue 3 全新的响应式 API toRef / toRefs 进行响应式数据转换,下文将会介绍全新的响应式 API 的用法。

第二个参数 context

context 只是一个普通的对象,它暴露三个组件的 Property :

属性类型作用
attrs非响应式对象未在 Props 里定义的属性都将变成 Attrs
slots非响应式对象组件插槽,用于接收父组件传递进来的模板内容
emit方法触发父组件绑定下来的事件

因为 context 只是一个普通对象,所以可以直接使用 ES6 解构。

平时使用可以通过直接传入 { emit } ,即可用 emit('xxx') 来代替使用 context.emit('xxx'),另外两个功能也是如此。

但是 attrsslots 请保持 attrs.xxxslots.xxx 的方式来使用其数据,不要进行解构,虽然这两个属性不是响应式对象,但对应的数据会随组件本身的更新而更新。

两个参数的具体使用,可查阅 组件之间的通信 一章详细了解。

defineComponent 的作用

defineComponent 是 Vue 3 推出的一个全新 API ,可用于对 TypeScript 代码的类型推导,帮助开发者简化掉很多编码过程中的类型声明。

比如,原本需要这样才可以使用 setup 函数:

ts
import { Slots } from 'vue'
+
+// 声明 \`props\` 和 \`return\` 的数据类型
+interface Data {
+  [key: string]: unknown
+}
+
+// 声明 \`context\` 的类型
+interface SetupContext {
+  attrs: Data
+  slots: Slots
+  emit: (event: string, ...args: unknown[]) => void
+}
+
+// 使用的时候入参要加上声明, \`return\` 也要加上声明
+export default {
+  setup(props: Data, context: SetupContext): Data {
+    // ...
+
+    return {
+      // ...
+    }
+  },
+}

每个组件都这样进行类型声明,会非常繁琐,如果使用了 defineComponent ,就可以省略这些类型声明:

ts
import { defineComponent } from 'vue'
+
+// 使用 \`defineComponent\` 包裹组件的内部逻辑
+export default defineComponent({
+  setup(props, context) {
+    // ...
+
+    return {
+      // ...
+    }
+  },
+})

代码量瞬间大幅度减少,只要是 Vue 本身的 API , defineComponent 都可以自动推导其类型,这样开发者在编写组件的过程中,只需要维护自己定义的数据类型就可以了,可专注于业务。

组件的生命周期 ~new

在了解了 Vue 3 组件的两个前置知识点后,不着急写组件,还需要先了解组件的生命周期,这个知识点非常重要,只有理解并记住组件的生命周期,才能够灵活的把控好每一处代码的执行,使程序的运行结果可以达到预期。

升级变化

从 Vue 2 升级到 Vue 3 ,在保留对 Vue 2 的生命周期支持的同时,Vue 3 也带来了一定的调整。

Vue 2 的生命周期写法名称是 Options API (选项式 API ), Vue 3 新的生命周期写法名称是 Composition API (组合式 API )。

Vue 3 组件默认支持 Options API ,而 Vue 2 可以通过 @vue/composition-api 插件获得 Composition API 的功能支持(其中 Vue 2.7 版本内置了该插件, 2.6 及以下的版本需要单独安装)。

为了减少理解成本,笔者将从读者的使用习惯上,使用 “ Vue 2 的生命周期” 代指 Options API 写法,用 “ Vue 3 的生命周期” 代指 Composition API 写法。

关于 Vue 生命周期的变化,可以从下表直观地了解:

Vue 2 生命周期Vue 3 生命周期执行时间说明
beforeCreatesetup组件创建前执行
createdsetup组件创建后执行
beforeMountonBeforeMount组件挂载到节点上之前执行
mountedonMounted组件挂载完成后执行
beforeUpdateonBeforeUpdate组件更新之前执行
updatedonUpdated组件更新完成之后执行
beforeDestroyonBeforeUnmount组件卸载之前执行
destroyedonUnmounted组件卸载完成后执行
errorCapturedonErrorCaptured当捕获一个来自子孙组件的异常时激活钩子函数

可以看到 Vue 2 生命周期里的 beforeCreatecreated ,在 Vue 3 里已被 setup 替代。

熟悉 Vue 2 的开发者应该都知道 Vue 有一个全局组件 <KeepAlive /> ,用于在多个组件间动态切换时缓存被移除的组件实例,当组件被包含在 <KeepAlive /> 组件里时,会多出两个生命周期钩子函数:

Vue 2 生命周期Vue 3 生命周期执行时间说明
activatedonActivated被激活时执行
deactivatedonDeactivated切换组件后,原组件消失前执行

TIP

虽然 Vue 3 依然支持 Vue 2 的生命周期,但是不建议混搭使用,前期可以继续使用 Vue 2 的生命周期作为过度阶段慢慢适应,但还是建议尽快熟悉并完全使用 Vue 3 的生命周期编写组件。

使用 3.x 的生命周期

在 Vue 3 的 Composition API 写法里,每个生命周期函数都要先导入才可以使用,并且所有生命周期函数统一放在 setup 里运行。

如果需要达到 Vue 2 的 beforeCreatecreated 生命周期的执行时机,直接在 setup 里执行函数即可。

以下是几个生命周期的执行顺序对比:

ts
import { defineComponent, onBeforeMount, onMounted } from 'vue'
+
+export default defineComponent({
+  setup() {
+    console.log(1)
+
+    onBeforeMount(() => {
+      console.log(2)
+    })
+
+    onMounted(() => {
+      console.log(3)
+    })
+
+    console.log(4)
+  },
+})

最终将按照生命周期的顺序输出:

js
// 1
+// 4
+// 2
+// 3

组件的基本写法

如果想在 Vue 2 里使用 TypeScript 编写组件,需要通过 Options API 的 Vue.extend 语法,或者是另外一种风格 Class Component 的语法声明组件,其中为了更好的进行类型推导, Class Component 语法更受开发者欢迎。

但是 Class Component 语法和默认的组件语法相差较大,带来了一定的学习成本,对于平时编写 JavaScript 代码很少使用 Class 的开发者,适应时间应该也会比较长。

因此 Vue 3 在保留对 Class Component 支持的同时,推出了全新的 Function-based Component ,更贴合 JavaScript 的函数式编程风格,这也是接下来要讲解并贯穿全文使用的 Composition API 新写法。

Composition API 虽然也是一个步伐迈得比较大的改动,但其组件结构并没有特别大的变化,区别比较大的地方在于组件生命周期和响应式 API 的使用,只要掌握了这些核心功能,上手 Vue 3 非常容易!

看到这里可能有开发者心里在想:

“这几种组件写法,加上视图部分又有 Template 和 TSX 的写法之分,生命周期方面 Vue 3 对 Vue 2 的写法又保持了兼容,在 Vue 里写 TypeScript 的组合方式一只手数不过来,在入门时选择合适的编程风格就遇到了困难,可怎么办?”

不用担心!笔者将九种常见的组合方式以表格的形式进行对比, Vue 3 组件最好的写法一目了然!

回顾 Vue 2

在 Vue 2 ,常用以下三种写法声明 TypeScript 组件:

适用版本基本写法视图写法
Vue 2Vue.extendTemplate
Vue 2Class ComponentTemplate
Vue 2Class ComponentTSX

其中最接近 Options API 的写法是使用 Vue.extend API 声明组件:

ts
// 这是一段摘选自 Vue 2 官网的代码演示
+import Vue from 'vue'
+
+// 推荐使用 Vue.extend 声明组件
+const Component = Vue.extend({
+  // 类型推断已启用
+})
+
+// 不推荐这种方式声明
+const Component = {
+  // 这里不会有类型推断,
+  // 因为 TypeScript 不能确认这是 Vue 组件的选项
+}

而为了更好地获得 TypeScript 类型推导支持,通常使用 Class Component 的写法,这是 Vue 官方推出的一个装饰器插件(需要单独安装):

ts
// 这是一段摘选自 Vue 2 官网的代码演示
+import Vue from 'vue'
+import Component from 'vue-class-component'
+
+// @Component 修饰符注明了此类为一个 Vue 组件
+@Component({
+  // 所有的组件选项都可以放在这里
+  template: '<button @click="onClick">Click!</button>',
+})
+
+// 使用 Class 声明一个组件
+export default class MyComponent extends Vue {
+  // 初始数据可以直接声明为实例的 property
+  message: string = 'Hello!'
+
+  // 组件方法也可以直接声明为实例的方法
+  onClick(): void {
+    window.alert(this.message)
+  }
+}

可在 Vue 2 官网的 TypeScript 支持 一章了解更多配置说明。

了解 Vue 3 ~new

Vue 3 从设计初期就考虑了 TypeScript 的支持,其中 defineComponent 这个 API 就是为了解决 Vue 2 对 TypeScript 类型推导不完善等问题而推出的。

在 Vue 3 ,至少有以下六种写法可以声明 TypeScript 组件:

适用版本基本写法视图写法生命周期版本官方是否推荐
Vue 3Class ComponentTemplateVue 2×
Vue 3defineComponentTemplateVue 2×
Vue 3defineComponentTemplateVue 3
Vue 3Class ComponentTSXVue 2×
Vue 3defineComponentTSXVue 2×
Vue 3defineComponentTSXVue 3

其中 defineComponent + Composition API + Template 的组合是 Vue 官方最为推荐的组件声明方式,本书接下来的内容都会以这种写法作为示范案例,也推荐开发者在学习的过程中,使用该组合进行入门。

下面看看如何使用 Composition API 编写一个最简单的 Hello World 组件:

vue
<!-- Template 代码和 Vue 2 一样 -->
+<template>
+  <p class="msg">{{ msg }}</p>
+</template>
+
+<!-- Script 代码需要使用 Vue 3 的新写法-->
+<script lang="ts">
+// Vue 3 的 API 需要导入才能使用
+import { defineComponent } from 'vue'
+
+// 使用 \`defineComponent\` 包裹组件代码
+// 即可获得完善的 TypeScript 类型推导支持
+export default defineComponent({
+  setup() {
+    // 在 \`setup\` 方法里声明变量
+    const msg = 'Hello World!'
+
+    // 将需要在 \`<template />\` 里使用的变量 \`return\` 出去
+    return {
+      msg,
+    }
+  },
+})
+</script>
+
+<!-- CSS 代码和 Vue 2 一样 -->
+<style scoped>
+.msg {
+  font-size: 14px;
+}
+</style>

可以看到 Vue 3 的组件也是 <template /> + <script /> + <style /> 的三段式组合,上手非常简单。

其中 Template 沿用了 Vue 2 时期类似 HTML 风格的模板写法, Style 则是使用原生 CSS 语法或者 Less 等 CSS 预处理器编写。

但需要注意的是,在 Vue 3 的 Composition API 写法里,数据或函数如果需要在 <template /> 中使用,就必须在 setup 里将其 return 出去,而仅在 <script /> 里被调用的函数或变量,不需要渲染到模板则无需 return

响应式数据的变化 ~new

响应式数据是 MVVM 数据驱动编程的特色, Vue 的设计也是受 MVVM 模型的启发,相信大部分开发者选择 MVVM 框架都是因为数据驱动编程比传统的事件驱动编程要来得方便,而选择 Vue ,则是方便中的方便。

TIP

Model-View-ViewModel (简称 MVVM ) 是一种软件架构模式,将视图 UI 和业务逻辑分开,通过对逻辑数据的修改即可驱动视图 UI 的更新,因此常将这种编程方式称为 “数据驱动” ,与之对应的需要操作 DOM 才能完成视图更新的编程方式则称为 “事件驱动” 。

设计上的变化

作为最重要的一个亮点, Vue 3 的响应式数据在设计上和 Vue 2 有着很大的不同。

回顾 Vue 2

Vue 2 是使用了 Object.defineProperty API 的 getter/setter 来实现数据的响应性,这个方法的具体用法可以参考 MDN 的文档: Object.defineProperty - MDN

下面使用 Object.defineProperty 实现一个简单的双向绑定 demo ,亲自敲代码试一下可以有更多的理解:

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>DefineProperty Demo</title>
+  </head>
+  <body>
+    <!-- 输入框和按钮 -->
+    <div>
+      <input type="text" id="input" />
+      <button onclick="vm.text = 'Hello World'">设置为 Hello World</button>
+    </div>
+    <!-- 输入框和按钮 -->
+
+    <!-- 文本展示 -->
+    <div id="output"></div>
+    <!-- 文本展示 -->
+
+    <script>
+      // 声明一个响应式数据
+      const vm = {}
+      Object.defineProperty(vm, 'text', {
+        set(value) {
+          document.querySelector('#input').value = value
+          document.querySelector('#output').innerText = value
+        },
+      })
+
+      // 处理输入行为
+      document.querySelector('#input').oninput = function (e) {
+        vm.text = e.target.value
+      }
+    </script>
+  </body>
+</html>

这个小 demo 实现了这两个功能:

  1. 输入框的输入行为只修改 vm.text 的数据,但会同时更新 output 标签的文本内容
  2. 点击按钮修改 vm.text 的数据,也会触发输入框和 output 文本的更新

当然 Vue 做了非常多的工作,而非只是简单的调用了 Object.defineProperty ,可以在官网 深入 Vue 2 的响应式原理 一章了解更多 Vue 2 的响应式原理。

了解 Vue 3

Vue 3 是使用了 Proxy API 的 getter/setter 来实现数据的响应性,这个方法的具体用法可以参考 MDN 的文档: Proxy - MDN

同样的,也来实现一个简单的双向绑定 demo ,这次使用 Proxy 来实现:

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Proxy Demo</title>
+  </head>
+  <body>
+    <!-- 输入框和按钮 -->
+    <div>
+      <input type="text" id="input" />
+      <button onclick="vm.text = 'Hello World'">设置为 Hello World</button>
+    </div>
+    <!-- 输入框和按钮 -->
+
+    <!-- 文本展示 -->
+    <div id="output"></div>
+    <!-- 文本展示 -->
+
+    <script>
+      // 声明一个响应式数据
+      const vm = new Proxy(
+        {},
+        {
+          set(obj, key, value) {
+            document.querySelector('#input').value = value
+            document.querySelector('#output').innerText = value
+          },
+        }
+      )
+
+      // 处理输入行为
+      document.querySelector('#input').oninput = function (e) {
+        vm.text = e.target.value
+      }
+    </script>
+  </body>
+</html>

这个 demo 实现的功能和使用 Object.defineProperty 的 demo 是完全一样的,也都是基于 setter 的行为完成数据更新的实现,那么为什么 Vue 3 要舍弃 Object.defineProperty ,换成 Proxy 呢?

主要原因在于 Object.defineProperty 有以下的不足:

  1. 无法侦听数组下标的变化,通过 arr[i] = newValue 这样的操作无法实时响应
  2. 无法侦听数组长度的变化,例如通过 arr.length = 10 去修改数组长度,无法响应
  3. 只能侦听对象的属性,对于整个对象需要遍历,特别是多级对象更是要通过嵌套来深度侦听
  4. 使用 Object.assign() 等方法给对象添加新属性时,也不会触发更新
  5. 更多细节上的问题 …

这也是为什么 Vue 2 要提供一个 Vue.set API 的原因,可以在官网 Vue 2 中检测变化的注意事项 一章了解更多说明。

而这些问题在 Proxy 都可以得到解决,可以在官网 深入 Vue 3 的响应式原理 一章了解更多这部分的内容。

用法上的变化

本书只使用 Composition API 编写组件,这是使用 Vue 3 的最大优势。

TIP

虽然官方文档在各个 API 的使用上都做了一定的举例,但在实际使用过程中可能会遇到一些问题,常见的情况就是有些数据用着用着就失去了响应,或者是在 TypeScript 里出现类型不匹配的报错等等。

当然,一般遇到这种情况并不一定是框架的 BUG ,而可能是使用方式不对,本章节将结合笔者最初入门 Vue 3 时遇到的问题和解决问题的心得,复盘这些响应式 API 的使用。

相对于 Vue 2 在 data 里声明后即可通过 this.xxx 调用响应式数据,在 Vue 3 的生命周期里没有了 Vue 实例的 this 指向,需要导入 refreactive 等响应式 API 才能声明并使用响应式数据。

ts
// 这里导入的 \`ref\` 是一个响应式 API
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 通过响应式 API 创建的变量具备了响应性
+    const msg = ref<string>('Hello World!')
+  },
+})

由于 Vue 3 新的 API 非常多,但有些 API 的使用场景却不多,因此本书当前只对常用的部分 API 的使用和常见问题进行说明,更多的 API 可以在官方文档的 响应性 API 一章查阅。

响应式 API 之 ref ~new

ref 是最常用的一个响应式 API,它可以用来定义所有类型的数据,包括 Node 节点和组件。

没错,在 Vue 2 常用的 this.$refs.xxx 来取代 document.querySelector('.xxx') 获取 Node 节点的方式,也是使用这个 API 来取代。

类型声明

在开始使用 API 之前,需要先了解在 TypeScript 中如何声明 Ref 变量的类型。

API 本身的类型

先看 API 本身, ref API 是一个函数,通过接受一个泛型入参,返回一个响应式对象,所有的值都通过 .value 属性获取,这是 API 本身的 TS 类型:

ts
// \`ref\` API 的 TS 类型
+function ref<T>(value: T): Ref<UnwrapRef<T>>
+
+// \`ref\` API 的返回值的 TS 类型
+interface Ref<T> {
+  value: T
+}

因此在声明变量时,是使用尖括号 <> 包裹其 TS 类型,紧跟在 ref API 之后:

ts
// 显式指定 \`msg.value\` 是 \`string\` 类型
+const msg = ref<string>('Hello World!')

再回看该 API 本身的类型,其中使用了 T 泛型,这表示在传入函数的入参时,可以不需要手动指定其 TS 类型, TypeScript 会根据这个 API 所返回的响应式对象的 .value 属性的类型,确定当前变量的类型。

因此也可以省略显式的类型指定,像下面这样声明变量,其类型交给 TypeScript 去自动推导:

ts
// TypeScript 会推导 \`msg.value\` 是 \`string\` 类型
+const msg = ref('Hello World')

对于声明时会赋予初始值,并且在使用过程中不会改变其类型的变量,是可以省略类型的显式指定的。

而如果有显式的指定的类型,那么在一些特殊情况下,初始化时可以不必赋值,这样 TypeScript 会自动添加 undefined 类型:

ts
const msg = ref<string>()
+console.log(msg.value) // undefined
+
+msg.value = 'Hello World!'
+console.log(msg.value) // Hello World!

因为入参留空时,虽然指定了 string 类型,但实际上此时的值是 undefined ,因此实际上这个时候的 msg.value 是一个 string | undefined 的联合类型。

对于声明时不知道是什么值,在某种条件下才进行初始化的情况,就可以省略其初始值,但是切记在调用该变量的时候对 .value 值进行有效性判断。

而如果既不显式指定类型,也不赋予初始值,那么会被默认为 any 类型,除非真的无法确认类型,否则不建议这么做。

API 返回值的类型

细心的开发者还会留意到 ref API 类型里面还标注了一个返回值的 TS 类型:

ts
interface Ref<T> {
+  value: T
+}

它是代表整个 Ref 变量的完整类型:

  • 上文声明 Ref 变量时,提到的 string 类型都是指 msg.value 这个 .value 属性的类型
  • msg 这个响应式变量,其本身是 Ref<string> 类型

如果在开发过程中需要在函数里返回一个 Ref 变量,那么其 TypeScript 类型就可以这样写(请留意 Calculator 里的 num 变量的类型):

ts
// 导入 \`ref\` API
+import { ref } from 'vue'
+// 导入 \`ref\` API 的返回值类型
+import type { Ref } from 'vue'
+
+// 声明 \`useCalculator\` 函数的返回值类型
+interface Calculator {
+  // 这里包含了一个 Ref 变量
+  num: Ref<number>
+  add: () => void
+}
+
+// 声明一个 “使用计算器” 的函数
+function useCalculator(): Calculator {
+  const num = ref<number>(0)
+
+  function add() {
+    num.value++
+  }
+
+  return {
+    num,
+    add,
+  }
+}
+
+// 在执行使用计算器函数时,可以获取到一个 Ref 变量和其他方法
+const { num, add } = useCalculator()
+add()
+console.log(num.value) // 1

上面这个简单的例子演示了如何手动指定 Ref 变量的类型,对于逻辑复用时的函数代码抽离、插件开发等场景非常有用!当然大部分情况下可以交给 TypeScript 自动推导,但掌握其用法,在必要的时候就派得上用场了!

变量的定义

在了解了如何对 Ref 变量进行类型声明之后,面对不同的数据类型,相信都得心应手了!但不同类型的值之间还是有少许差异和注意事项,例如上文提及到该 API 可以用来定义所有类型的数据,包括 Node 节点和组件,具体可以参考下文的示例。

基本类型

对字符串、布尔值等基本类型的定义方式,比较简单:

ts
// 字符串
+const msg = ref<string>('Hello World!')
+
+// 数值
+const count = ref<number>(1)
+
+// 布尔值
+const isVip = ref<boolean>(false)

引用类型

对于对象、数组等引用类型也适用,比如要定义一个对象:

ts
// 先声明对象的格式
+interface Member {
+  id: number
+  name: string
+}
+
+// 在定义对象时指定该类型
+const userInfo = ref<Member>({
+  id: 1,
+  name: 'Tom',
+})

定义一个普通数组:

ts
// 数值数组
+const uids = ref<number[]>([1, 2, 3])
+
+// 字符串数组
+const names = ref<string[]>(['Tom', 'Petter', 'Andy'])

定义一个对象数组:

ts
// 声明对象的格式
+interface Member {
+  id: number
+  name: string
+}
+
+// 定义一个对象数组
+const memberList = ref<Member[]>([
+  {
+    id: 1,
+    name: 'Tom',
+  },
+  {
+    id: 2,
+    name: 'Petter',
+  },
+])

DOM 元素与子组件

除了可以定义数据,ref 也有熟悉的用途,就是用来挂载节点,也可以挂在子组件上,也就是对应在 Vue 2 时常用的 this.$refs.xxx 获取 DOM 元素信息的作用。

模板部分依然是熟悉的用法,在要引用的 DOM 上添加一个 ref 属性:

vue
<template>
+  <!-- 给 DOM 元素添加 \`ref\` 属性 -->
+  <p ref="msg">请留意该节点,有一个 ref 属性</p>
+
+  <!-- 子组件也是同样的方式添加 -->
+  <Child ref="child" />
+</template>

<script /> 部分有三个最基本的注意事项:

  1. <template /> 代码里添加的 ref 属性的值,是对应 <script /> 里使用 ref API 声明的变量的名称;

  2. 请保证视图渲染完毕后再执行 DOM 或组件的相关操作(需要放到生命周期的 onMounted 或者 nextTick 函数里,这一点在 Vue 2 也是一样);

  3. 该 Ref 变量必须 return 出去才可以给到 <template /> 使用,这一点是 Vue 3 生命周期的硬性要求,子组件的数据和方法如果要给父组件操作,也要 return 出来才可以。

配合上面的 <template /> ,来看看 <script /> 部分的具体例子:

ts
import { defineComponent, onMounted, ref } from 'vue'
+import Child from '@cp/Child.vue'
+
+export default defineComponent({
+  components: {
+    Child,
+  },
+  setup() {
+    // 定义挂载节点,声明的类型详见下方附表
+    const msg = ref<HTMLElement>()
+    const child = ref<InstanceType<typeof Child>>()
+
+    // 请保证视图渲染完毕后再执行节点操作 e.g. \`onMounted\` / \`nextTick\`
+    onMounted(() => {
+      // 比如获取 DOM 的文本
+      console.log(msg.value.innerText)
+
+      // 或者操作子组件里的数据
+      child.value.isShowDialog = true
+    })
+
+    // 必须 \`return\` 出去才可以给到 \`<template />\` 使用
+    return {
+      msg,
+      child,
+    }
+  },
+})

关于 DOM 和子组件的 TS 类型声明,可参考以下规则:

节点类型声明类型参考文档
DOM 元素使用 HTML 元素接口HTML 元素接口
子组件使用 InstanceType 配合 typeof 获取子组件的类型typeof 操作符

TIP

单纯使用 typeof Child 虽然可以获得 Child.vue 组件的 Props 和方法等提示,但在 VSCode 的类型推导还不够智能,缺乏更有效的代码补全支持。

上文使用的 InstanceType<T> 是 TypeScript 提供的一个工具类型,可以获取构造函数类型的实例类型,因此将组件的类型声明为 InstanceType<typeof Child> ,不仅可以得到更完善的类型提示,在编程过程中还可以让编辑器提供更完善的代码补全功能。

另外,关于这一小节,有一个可能会引起 TS 编译报错的情况是,一些脚手架创建出来的项目会默认启用 --strictNullChecks 选项,会导致案例中的代码无法正常编译,出现如下报错:

bash
 npm run build
+
+> hello-vue3@0.0.0 build
+> vue-tsc --noEmit && vite build
+
+src/views/home.vue:27:7 - error TS2532: Object is possibly 'undefined'.
+
+27       child.value.isShowDialog = true
+         ~~~~~~~~~~~
+
+
+Found 1 error in src/views/home.vue:27

这是因为在默认情况下 nullundefined 是所有类型的子类型,但开启了 strictNullChecks 选项之后,会使 nullundefined 只能赋值给 void 和它们各自,这是一个更为严谨的选项,可以保障程序代码的健壮性,但对于刚接触 TypeScript 不久的开发者可能不太友好。

有以下几种解决方案可以参考:

  1. 在涉及到相关操作的时候,对节点变量增加一个判断:
ts
// 添加 \`if\` 分支,判断 \`.value\` 存在时才执行相关代码
+if (child.value) {
+  // 读取子组件的数据
+  console.log(child.value.num)
+
+  // 执行子组件的方法
+  child.value.sayHi('Use \`if\` in \`onMounted\` API.')
+}
  1. 通过 TS 的可选符 ? 将目标设置为可选,避免出现错误(这个方式不能直接修改子组件数据的值):
ts
// 读取子组件的数据(留意 \`.num\` 前面有一个 \`?\` 问号)
+console.log(child.value?.num)
+
+// 执行子组件的方法(留意 \`.sayHi\` 前面有一个 \`?\` 问号)
+child.value?.sayHi('use ? in onMounted')
  1. 在项目根目录下的 tsconfig.json 文件里,显式的关闭 strictNullChecks 选项,关闭后,需要开发者在写代码的时候,自行把控好是否需要对 nullundefined 进行判断:
json
{
+  "compilerOptions": {
+    // ...
+    "strictNullChecks": false
+  }
+  // ...
+}
  1. 使用 any 类型代替,但是写 TypeScript 还是尽量不要使用 any ,满屏的 AnyScript 不如直接使用 JavaScript

变量的读取与赋值

前面在介绍 API 类型的时候已经了解,通过 ref 声明的变量会全部变成对象,不管定义的是什么类型的值,都会转化为一个 Ref 对象,其中 Ref 对象具有指向内部值的单个 Property .value

也就是说,任何 Ref 对象的值都必须通过 xxx.value 才可以正确获取。

请牢记上面这句话,初拥 Vue 3 的开发者很多 BUG 都是由于这个问题引起的(包括笔者刚开始使用 Vue 3 的那段时间,嘿嘿)。

读取变量

平时对于普通变量的值,读取的时候都是直接调用其变量名即可:

ts
// 读取一个字符串
+const msg: string = 'Hello World!'
+console.log(msg)
+
+// 读取一个数组
+const uids: number[] = [1, 2, 3]
+console.log(uids[1])

而 Ref 对象的值的读取,切记!必须通过 .value

ts
// 读取一个字符串
+const msg = ref<string>('Hello World!')
+console.log(msg.value)
+
+// 读取一个数组
+const uids = ref<number[]>([1, 2, 3])
+console.log(uids.value[1])

为变量赋值

普通变量需要使用 let 声明才可以修改其值,由于 Ref 对象是个引用类型,所以可以使用 const 声明,直接通过 .value 修改。

ts
// 声明一个字符串变量
+const msg = ref<string>('Hi!')
+
+// 等待 1s 后修改它的值
+setTimeout(() => {
+  msg.value = 'Hello!'
+}, 1000)

因此日常业务中,像在对接服务端 API 的接口数据时,可以自由的使用 forEachmapfilter 等方法操作 Ref 数组,或者直接重置它,而不必担心数据失去响应性。

ts
const data = ref<string[]>([])
+
+// 提取接口的数据
+data.value = api.data.map((item: any) => item.text)
+
+// 重置数组
+data.value = []

为什么突然要说这个呢?因为涉及到下一部分的知识,关于 reactive API 在使用上的注意事项。

响应式 API 之 reactive ~new

reactive 是继 ref 之后最常用的一个响应式 API 了,相对于 ref ,它的局限性在于只适合对象、数组。

TIP

使用 reactive 的好处就是写法跟平时的对象、数组几乎一模一样,但它也带来了一些特殊注意点,请留意赋值部分的特殊说明。

类型声明与定义

reactive 变量的声明方式没有 ref 的变化那么大,基本上和普通变量一样,它的 TS 类型如下:

ts
function reactive<T extends object>(target: T): UnwrapNestedRefs<T>

可以看到其用法还是比较简单的,下面是一个 Reactive 对象的声明方式:

ts
// 声明对象的类型
+interface Member {
+  id: number
+  name: string
+}
+
+// 定义一个对象
+const userInfo: Member = reactive({
+  id: 1,
+  name: 'Tom',
+})

下面是 Reactive 数组的声明方式:

ts
const uids: number[] = reactive([1, 2, 3])

还可以声明一个 Reactive 对象数组:

ts
// 对象数组也是先声明其中的对象类型
+interface Member {
+  id: number
+  name: string
+}
+
+// 再定义一个为对象数组
+const userList: Member[] = reactive([
+  {
+    id: 1,
+    name: 'Tom',
+  },
+  {
+    id: 2,
+    name: 'Petter',
+  },
+  {
+    id: 3,
+    name: 'Andy',
+  },
+])

变量的读取与赋值

虽然 reactive API 在使用上没有像 ref API 一样有 .value 的心智负担,但也有一些注意事项要留意。

处理对象

Reactive 对象在读取字段的值,或者修改值的时候,与普通对象是一样的,这部分没有太多问题。

ts
// 声明对象的类型
+interface Member {
+  id: number
+  name: string
+}
+
+// 定义一个对象
+const userInfo: Member = reactive({
+  id: 1,
+  name: 'Tom',
+})
+
+// 读取用户名
+console.log(userInfo.name)
+
+// 修改用户名
+userInfo.name = 'Petter'

处理数组

但是对于 Reactive 数组,和普通数组会有一些区别。

普通数组在 “重置” 或者 “修改值” 时都是可以直接操作:

ts
// 定义一个普通数组
+let uids: number[] = [1, 2, 3]
+
+// 从另外一个对象数组里提取数据过来
+uids = api.data.map((item: any) => item.id)
+
+// 合并另外一个数组
+let newUids: number[] = [4, 5, 6]
+uids = [...uids, ...newUids]
+
+// 重置数组
+uids = []

Vue 2 在操作数组的时候,也可以和普通数组这样处理数据的变化,依然能够保持响应性,但在 Vue 3 ,如果使用 reactive 定义数组,则不能这么处理,必须只使用那些不会改变引用地址的操作。

笔者刚开始接触时,按照原来的思维去处理 reactive 数组,于是遇到了 “数据变了,但模板不会更新的问题” ,如果开发者在学习的过程中也遇到了类似的情况,可以从这里去入手排查问题所在。

举个例子,比如要从服务端 API 接口获取翻页数据时,通常要先重置数组,再异步添加数据,如果使用常规的重置,会导致这个变量失去响应性:

ts
let uids: number[] = reactive([1, 2, 3])
+
+/**
+ * 不推荐使用这种方式,会丢失响应性
+ * 异步添加数据后,模板不会响应更新
+ */
+uids = []
+
+// 异步获取数据后,模板依然是空数组
+setTimeout(() => {
+  uids.push(1)
+}, 1000)

要让数据依然保持响应性,则必须在关键操作时,不破坏响应性 API ,以下是推荐的操作方式,通过重置数组的 length 长度来实现数据的重置:

ts
const uids: number[] = reactive([1, 2, 3])
+
+/**
+ * 推荐使用这种方式,不会破坏响应性
+ */
+uids.length = 0
+
+// 异步获取数据后,模板可以正确的展示
+setTimeout(() => {
+  uids.push(1)
+}, 1000)

特别注意

不要对 Reactive 数据进行 ES6 的解构 操作,因为解构后得到的变量会失去响应性。

比如这些情况,在 2s 后都得不到新的 name 信息:

ts
import { defineComponent, reactive } from 'vue'
+
+interface Member {
+  id: number
+  name: string
+}
+
+export default defineComponent({
+  setup() {
+    // 定义一个带有响应性的对象
+    const userInfo: Member = reactive({
+      id: 1,
+      name: 'Petter',
+    })
+
+    // 在 2s 后更新 \`userInfo\`
+    setTimeout(() => {
+      userInfo.name = 'Tom'
+    }, 2000)
+
+    // 这个变量在 2s 后不会同步更新
+    const newUserInfo: Member = { ...userInfo }
+
+    // 这个变量在 2s 后不会再同步更新
+    const { name } = userInfo
+
+    // 这样 \`return\` 出去给模板用,在 2s 后也不会同步更新
+    return {
+      ...userInfo,
+    }
+  },
+})

响应式 API 之 toRef 与 toRefs ~new

相信各位开发者看到这里时,应该已经对 refreactive API 都有所了解了,为了方便开发者使用, Vue 3 还推出了两个与之相关的 API : toReftoRefs ,都是用于 reactiveref 转换。

各自的作用

这两个 API 在拼写上非常接近,顾名思义,一个是只转换一个字段,一个是转换所有字段,转换后将得到新的变量,并且新变量和原来的变量可以保持同步更新。

API作用
toRef创建一个新的 Ref 变量,转换 Reactive 对象的某个字段为 Ref 变量
toRefs创建一个新的对象,它的每个字段都是 Reactive 对象各个字段的 Ref 变量

光看概念可能不容易理解,来看下面的例子,先声明一个 reactive 变量:

ts
interface Member {
+  id: number
+  name: string
+}
+
+const userInfo: Member = reactive({
+  id: 1,
+  name: 'Petter',
+})

然后分别看看这两个 API 应该怎么使用。

使用 toRef

先看这个转换单个字段的 toRef API ,了解了它的用法之后,再去看 toRefs 就很容易理解了。

API 类型和基本用法

toRef API 的 TS 类型如下:

ts
// \`toRef\` API 的 TS 类型
+function toRef<T extends object, K extends keyof T>(
+  object: T,
+  key: K,
+  defaultValue?: T[K]
+): ToRef<T[K]>
+
+// \`toRef\` API 的返回值的 TS 类型
+type ToRef<T> = T extends Ref ? T : Ref<T>

通过接收两个必传的参数(第一个是 reactive 对象, 第二个是要转换的 key ),返回一个 Ref 变量,在适当的时候也可以传递第三个参数,为该变量设置默认值。

以上文声明好的 userInfo 为例,如果想转换 name 这个字段为 Ref 变量,只需要这样操作:

ts
const name = toRef(userInfo, 'name')
+console.log(name.value) // Petter

等号左侧的 name 变量此时是一个 Ref 变量,这里因为 TypeScript 可以对其自动推导,因此声明时可以省略 TS 类型的显式指定,实际上该变量的类型是 Ref<string>

所以之后在读取和赋值时,就需要使用 name.value 来操作,在重新赋值时会同时更新 nameuserInfo.name 的值:

ts
// 修改前先查看初始值
+const name = toRef(userInfo, 'name')
+console.log(name.value) // Petter
+console.log(userInfo.name) // Petter
+
+// 修改 Ref 变量的值,两者同步更新
+name.value = 'Tom'
+console.log(name.value) // Tom
+console.log(userInfo.name) // Tom
+
+// 修改 Reactive 对象上该属性的值,两者也是同步更新
+userInfo.name = 'Jerry'
+console.log(name.value) // Jerry
+console.log(userInfo.name) // Jerry

这个 API 也可以接收一个 Reactive 数组,此时第二个参数应该传入数组的下标:

ts
// 这一次声明的是数组
+const words = reactive(['a', 'b', 'c'])
+
+// 通过下标 \`0\` 转换第一个 item
+const a = toRef(words, 0)
+console.log(a.value) // a
+console.log(words[0]) // a
+
+// 通过下标 \`2\` 转换第三个 item
+const c = toRef(words, 2)
+console.log(c.value) // c
+console.log(words[2]) // c

设置默认值

如果 Reactive 对象上有一个属性本身没有初始值,也可以传递第三个参数进行设置(默认值仅对 Ref 变量有效):

ts
interface Member {
+  id: number
+  name: string
+  // 类型上新增一个属性,因为是可选的,因此默认值会是 \`undefined\`
+  age?: number
+}
+
+// 声明变量时省略 \`age\` 属性
+const userInfo: Member = reactive({
+  id: 1,
+  name: 'Petter',
+})
+
+// 此时为了避免程序运行错误,可以指定一个初始值
+// 但初始值仅对 Ref 变量有效,不会影响 Reactive 字段的值
+const age = toRef(userInfo, 'age', 18)
+console.log(age.value)  // 18
+console.log(userInfo.age) // undefined
+
+// 除非重新赋值,才会使两者同时更新
+age.value = 25
+console.log(age.value)  // 25
+console.log(userInfo.age) // 25

数组也是同理,对于可能不存在的下标,可以传入默认值避免项目的逻辑代码出现问题:

ts
const words = reactive(['a', 'b', 'c'])
+
+// 当下标对应的值不存在时,也是返回 \`undefined\`
+const d = toRef(words, 3)
+console.log(d.value) // undefined
+console.log(words[3]) // undefined
+
+// 设置了默认值之后,就会对 Ref 变量使用默认值, Reactive 数组此时不影响
+const e = toRef(words, 4, 'e')
+console.log(e.value) // e
+console.log(words[4]) // undefined

其他用法

这个 API 还有一个特殊用法,但不建议在 TypeScript 里使用。

toRef 的过程中,如果使用了原对象上面不存在的 key ,那么定义出来的 Ref 变量的 .value 值将会是 undefined

ts
// 众所周知, Petter 是没有女朋友的
+const girlfriend = toRef(userInfo, 'girlfriend')
+console.log(girlfriend.value) // undefined
+console.log(userInfo.girlfriend) // undefined
+
+// 此时 Reactive 对象上只有两个 Key
+console.log(Object.keys(userInfo)) // ['id', 'name']

如果对这个不存在的 key 的 Ref 变量进行赋值,那么原来的 Reactive 对象也会同步增加这个 key,其值也会同步更新。

ts
// 赋值后,不仅 Ref 变量得到了 \`Marry\` , Reactive 对象也得到了 \`Marry\`
+girlfriend.value = 'Marry'
+console.log(girlfriend.value) // 'Marry'
+console.log(userInfo.girlfriend) // 'Marry'
+
+// 此时 Reactive 对象上有了三个 Key
+console.log(Object.keys(userInfo)) // ['id', 'name', 'girlfriend']

为什么强调不要在 TypeScript 里使用呢?因为在编译时,无法通过 TypeScript 的类型检查:

bash
 npm run build
+
+> hello-vue3@0.0.0 build
+> vue-tsc --noEmit && vite build
+
+src/views/home.vue:37:40 - error TS2345: Argument of type '"girlfriend"'
+is not assignable to parameter of type 'keyof Member'.
+
+37     const girlfriend = toRef(userInfo, 'girlfriend')
+                                          ~~~~~~~~~~~~
+
+src/views/home.vue:39:26 - error TS2339: Property 'girlfriend' does not exist
+on type 'Member'.
+
+39     console.log(userInfo.girlfriend) // undefined
+                            ~~~~~~~~~~
+
+src/views/home.vue:45:26 - error TS2339: Property 'girlfriend' does not exist
+on type 'Member'.
+
+45     console.log(userInfo.girlfriend) // 'Marry'
+                            ~~~~~~~~~~
+
+
+Found 3 errors in the same file, starting at: src/views/home.vue:37

如果不得不使用这种情况,可以考虑使用 any 类型:

ts
// 将该类型直接指定为 \`any\`
+type Member = any
+// 当然一般都是 \`const userInfo: any\`
+
+// 或者保持接口类型的情况下,允许任意键值
+interface Member {
+  [key: string]: any
+}
+
+// 使用 \`Record\` 也是同理
+type Member = Record<string, any>

但笔者还是更推荐保持良好的类型声明习惯,尽量避免这种用法。

使用 toRefs

在了解了 toRef API 之后,来看看 toRefs 的用法。

API 类型和基本用法

先看看它的 TS 类型:

ts
function toRefs<T extends object>(
+  object: T
+): {
+  [K in keyof T]: ToRef<T[K]>
+}
+
+type ToRef = T extends Ref ? T : Ref<T>

toRef 不同, toRefs 只接收了一个参数,是一个 reactive 变量。

ts
interface Member {
+  id: number
+  name: string
+}
+
+// 声明一个 Reactive 变量
+const userInfo: Member = reactive({
+  id: 1,
+  name: 'Petter',
+})
+
+// 传给 \`toRefs\` 作为入参
+const userInfoRefs = toRefs(userInfo)

此时这个新的 userInfoRefs 变量,它的 TS 类型就不再是 Member 了,而应该是:

ts
// 导入 \`toRefs\` API 的类型
+import type { ToRefs } from 'vue'
+
+// 上下文代码省略...
+
+// 将原来的类型传给 API 的类型
+const userInfoRefs: ToRefs<Member> = toRefs(userInfo)

也可以重新编写一个新的类型来指定它,因为每个字段都是与原来关联的 Ref 变量,所以也可以这样声明:

ts
// 导入 \`ref\` API 的类型
+import type { Ref } from 'vue'
+
+// 上下文代码省略...
+
+// 新声明的类型每个字段都是一个 Ref 变量的类型
+interface MemberRefs {
+  id: Ref<number>
+  name: Ref<string>
+}
+
+// 使用新的类型进行声明
+const userInfoRefs: MemberRefs = toRefs(userInfo)

当然实际上日常使用时并不需要手动指定其类型, TypeScript 会自动推导,可以节约非常多的开发工作量。

toRef API 一样,这个 API 也是可以对数组进行转换:

ts
const words = reactive(['a', 'b', 'c'])
+const wordsRefs = toRefs(words)

此时新数组的类型是 Ref<string>[] ,不再是原来的 string[] 类型。

解构与赋值

转换后的 Reactive 对象或数组支持 ES6 的解构,并且不会失去响应性,因为解构后的每一个变量都具备响应性。

ts
// 为了提高开发效率,可以直接将 Ref 变量直接解构出来使用
+const { name } = toRefs(userInfo)
+console.log(name.value) // Petter
+
+// 此时对解构出来的变量重新赋值,原来的变量也可以同步更新
+name.value = 'Tom'
+console.log(name.value) // Tom
+console.log(userInfo.name) // Tom

这一点和直接解构 Reactive 变量有非常大的不同,直接解构 Reactive 变量,得到的是一个普通的变量,不再具备响应性。

这个功能在使用 Hooks 函数非常好用(在 Vue 3 里也叫可组合函数, Composable Functions ),还是以一个计算器函数为例,这一次将其修改为内部有一个 Reactive 的数据状态中心,在函数返回时解构为多个 Ref 变量:

ts
import { reactive, toRefs } from 'vue'
+
+// 声明 \`useCalculator\` 数据状态类型
+interface CalculatorState {
+  // 这是要用来计算操作的数据
+  num: number
+  // 这是每次计算时要增加的幅度
+  step: number
+}
+
+// 声明一个 “使用计算器” 的函数
+function useCalculator() {
+  // 通过数据状态中心的形式,集中管理内部变量
+  const state: CalculatorState = reactive({
+    num: 0,
+    step: 10,
+  })
+
+  // 功能函数也是通过数据中心变量去调用
+  function add() {
+    state.num += state.step
+  }
+
+  return {
+    ...toRefs(state),
+    add,
+  }
+}

这样在调用 useCalculator 函数时,可以通过解构直接获取到 Ref 变量,不需要再进行额外的转换工作。

ts
// 解构出来的 \`num\` 和 \`step\` 都是 Ref 变量
+const { num, step, add } = useCalculator()
+console.log(num.value) // 0
+console.log(step.value) // 10
+
+// 调用计算器的方法,数据也是会得到响应式更新
+add()
+console.log(num.value) // 10

为什么要进行转换

关于为什么要出这么两个 API ,官方文档没有特别说明,不过经过笔者在业务中的一些实际使用感受,以及在写上一节 reactive特别注意,可能知道一些使用理由。

关于 refreactive 这两个 API 的好处就不重复了,但是在使用的过程中,各自都有不方便的地方:

ref API 虽然在 <template /> 里使用起来方便,但是在 <script /> 里进行读取 / 赋值的时候,要一直记得加上 .value ,否则 BUG 就来了。

reactive API 虽然在使用的时候,因为知道它本身是一个对象,所以不会忘记通过 foo.bar 这样的格式去操作,但是在 <template /> 渲染的时候,又因此不得不每次都使用 foo.bar 的格式去渲染。

那么有没有办法,既可以在编写 <script /> 的时候不容易出错,在写 <template /> 的时候又比较简单呢?

于是, toReftoRefs 因此诞生。

什么场景下比较适合使用它们

从便利性和可维护性来说,最好只在功能单一、代码量少的组件里使用,比如一个表单组件,通常表单的数据都放在一个对象里。

当然也可以把所有的数据都定义到一个 data 里,再去 data 里面取值,但是没有必要为了转换而转换,否则不如使用 Options API 风格。

在业务中的具体运用

继续使用上文一直在使用的 userInfo 来当案例,以一个用户信息表的小 demo 做个演示。

<script /> 部分:

  1. 先用 reactive 定义一个源数据,所有的数据更新,都是修改这个对象对应的值,按照对象的写法维护数据

  2. 再通过 toRefs 定义一个给 <template /> 使用的对象,这样可以得到一个每个字段都是 Ref 变量的新对象

  3. return 的时候,对步骤 2 里的 toRefs 对象进行解构,这样导出去就是各个字段对应的 Ref 变量,而不是一整个对象

ts
import { defineComponent, reactive, toRefs } from 'vue'
+
+interface Member {
+  id: number
+  name: string
+  age: number
+  gender: string
+}
+
+export default defineComponent({
+  setup() {
+    // 定义一个 reactive 对象
+    const userInfo = reactive({
+      id: 1,
+      name: 'Petter',
+      age: 18,
+      gender: 'male',
+    })
+
+    // 定义一个新的对象,它本身不具备响应性,但是它的字段全部是 Ref 变量
+    const userInfoRefs = toRefs(userInfo)
+
+    // 在 2s 后更新 \`userInfo\`
+    setTimeout(() => {
+      userInfo.id = 2
+      userInfo.name = 'Tom'
+      userInfo.age = 20
+    }, 2000)
+
+    // 在这里解构 \`toRefs\` 对象才能继续保持响应性
+    return {
+      ...userInfoRefs,
+    }
+  },
+})

<template /> 部分:

由于 return 出来的都是 Ref 变量,所以在模板里可以直接使用 userInfo 各个字段的 key ,不再需要写很长的 userInfo.name 了。

vue
<template>
+  <ul class="user-info">
+    <li class="item">
+      <span class="key">ID:</span>
+      <span class="value">{{ id }}</span>
+    </li>
+
+    <li class="item">
+      <span class="key">name:</span>
+      <span class="value">{{ name }}</span>
+    </li>
+
+    <li class="item">
+      <span class="key">age:</span>
+      <span class="value">{{ age }}</span>
+    </li>
+
+    <li class="item">
+      <span class="key">gender:</span>
+      <span class="value">{{ gender }}</span>
+    </li>
+  </ul>
+</template>

需要注意的问题

请注意是否有相同命名的变量存在,比如上面在 return<template /> 使用时,在解构 userInfoRefs 的时候已经包含了一个 name 字段,此时如果还有一个单独的变量也叫 name ,就会出现渲染上的数据显示问题。

此时它们在 <template /> 里哪个会生效,取决于谁排在后面,因为 return 出去的其实是一个对象,在对象里,如果存在相同的 key ,则后面的会覆盖前面的。

下面这种情况,会以单独的 name 为渲染数据:

ts
return {
+  ...userInfoRefs,
+  name,
+}

而下面这种情况,则是以 userInfoRefs 里的 name 为渲染数据:

ts
return {
+  name,
+  ...userInfoRefs,
+}

所以当决定使用 toReftoRefs API 的时候,请注意这个特殊情况!

函数的声明和使用 ~new

在了解了响应式数据如何使用之后,接下来就要开始了解函数了。

在 Vue 2 ,函数通常是作为当前组件实例上的方法在 methods 里声明,然后再在 mounted 等生命周期里调用,或者是在模板里通过 Click 等行为触发,由于组件内部经常需要使用 this 获取组件实例,因此不能使用箭头函数。

js
export default {
+  data: () => {
+    return {
+      num: 0,
+    }
+  },
+  mounted: function () {
+    this.add()
+  },
+  methods: {
+    // 不可以使用 \`add: () => this.num++\`
+    add: function () {
+      this.num++
+    },
+  },
+}

在 Vue 3 则灵活了很多,可以使用普通函数、 Class 类、箭头函数、匿名函数等等进行声明,可以将其写在 setup 里直接使用,也可以抽离在独立的 .js / .ts 文件里再导入使用。

需要在组件创建时自动执行的函数,其执行时机需要遵循 Vue 3 的生命周期,需要在模板里通过 @click@change 等行为触发,和变量一样,需要把函数名在 setup 里进行 return 出去。

下面是一个简单的例子,方便开发者更直观地了解:

vue
<template>
+  <p>{{ msg }}</p>
+
+  <!-- 在这里点击执行 \`return\` 出来的方法 -->
+  <button @click="updateMsg">修改MSG</button>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const msg = ref<string>('Hello World!')
+
+    // 这个要暴露给模板使用,必须 \`return\` 才可以使用
+    function updateMsg() {
+      msg.value = 'Hi World!'
+    }
+
+    // 这个要在页面载入时执行,无需 \`return\` 出去
+    const init = () => {
+      console.log('init')
+    }
+
+    onMounted(() => {
+      init()
+    })
+
+    return {
+      msg,
+      updateMsg,
+    }
+  },
+})
+</script>

数据的侦听 ~new

侦听数据变化也是组件里的一项重要工作,比如侦听路由变化、侦听参数变化等等。

Vue 3 在保留原来的 watch 功能之外,还新增了一个 watchEffect 帮助更简单的进行侦听。

watch

在 Vue 3 ,新版的 watch 和 Vue 2 的旧版写法对比,在使用方式上变化非常大!

回顾 Vue 2

在 Vue 2 是这样用的,和 datamethods 都在同级配置:

ts
export default {
+  data() {
+    return {
+      // ...
+    }
+  },
+  // 注意这里,放在 \`data\` 、 \`methods\` 同个级别
+  watch: {
+    // ...
+  },
+  methods: {
+    // ...
+  },
+}

并且类型繁多,选项式 API 的类型如下:

ts
watch: { [key: string]: string | Function | Object | Array}

联合类型过多,意味着用法复杂,下面是个很好的例子,虽然出自 官网 的用法介绍,但过于繁多的用法也反映出来对初学者不太友好,初次接触可能会觉得一头雾水:

ts
export default {
+  data() {
+    return {
+      a: 1,
+      b: 2,
+      c: {
+        d: 4,
+      },
+      e: 5,
+      f: 6,
+    }
+  },
+  watch: {
+    // 侦听顶级 Property
+    a(val, oldVal) {
+      console.log(\`new: \${val}, old: \${oldVal}\`)
+    },
+    // 字符串方法名
+    b: 'someMethod',
+    // 该回调会在任何被侦听的对象的 Property 改变时被调用,不论其被嵌套多深
+    c: {
+      handler(val, oldVal) {
+        console.log('c changed')
+      },
+      deep: true,
+    },
+    // 侦听单个嵌套 Property
+    'c.d': function (val, oldVal) {
+      // do something
+    },
+    // 该回调将会在侦听开始之后被立即调用
+    e: {
+      handler(val, oldVal) {
+        console.log('e changed')
+      },
+      immediate: true,
+    },
+    // 可以传入回调数组,它们会被逐一调用
+    f: [
+      'handle1',
+      function handle2(val, oldVal) {
+        console.log('handle2 triggered')
+      },
+      {
+        handler: function handle3(val, oldVal) {
+          console.log('handle3 triggered')
+        },
+        /* ... */
+      },
+    ],
+  },
+  methods: {
+    someMethod() {
+      console.log('b changed')
+    },
+    handle1() {
+      console.log('handle 1 triggered')
+    },
+  },
+}

当然肯定也会有开发者会觉得这样选择多是个好事,选择适合自己的就好,但笔者还是认为这种写法对于初学者来说不是那么友好,有些过于复杂化,如果一个用法可以适应各种各样的场景,岂不是更妙?

TIP

另外需要注意的是,不能使用箭头函数来定义 Watcher 函数 (例如 searchQuery: newValue => this.updateAutocomplete(newValue) )。

因为箭头函数绑定了父级作用域的上下文,所以 this 将不会按照期望指向组件实例, this.updateAutocomplete 将是 undefined

Vue 2 也可以通过 this.$watch() 这个 API 的用法来实现对某个数据的侦听,它接受三个参数: sourcecallbackoptions

ts
export default {
+  data() {
+    return {
+      a: 1,
+    }
+  },
+  // 生命周期钩子
+  mounted() {
+    this.$watch('a', (newVal, oldVal) => {
+      // ...
+    })
+  },
+}

由于 this.$watch 的用法和 Vue 3 比较接近,所以这里不做过多的回顾,请直接看 了解 Vue 3 部分。

了解 Vue 3

在 Vue 3 的组合式 API 写法, watch 是一个可以接受 3 个参数的函数(保留了 Vue 2 的 this.$watch 这种用法),在使用层面上简单了很多。

ts
import { watch } from 'vue'
+
+// 一个用法走天下
+watch(
+  source, // 必传,要侦听的数据源
+  callback // 必传,侦听到变化后要执行的回调函数
+  // options // 可选,一些侦听选项
+)

下面的内容都基于 Vue 3 的组合式 API 用法展开讲解。

API 的 TS 类型

在了解用法之前,先对它的 TS 类型声明做一个简单的了解, watch 作为组合式 API ,根据使用方式有两种类型声明:

  1. 基础用法的 TS 类型,详见 基础用法 部分
ts
// watch 部分的 TS 类型
+// ...
+export declare function watch<T, Immediate extends Readonly<boolean> = false>(
+  source: WatchSource<T>,
+  cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
+  options?: WatchOptions<Immediate>
+): WatchStopHandle
+// ...
  1. 批量侦听的 TS 类型,详见 批量侦听 部分
ts
// watch 部分的 TS 类型
+// ...
+export declare function watch<
+  T extends MultiWatchSources,
+  Immediate extends Readonly<boolean> = false
+>(
+  sources: [...T],
+  cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
+  options?: WatchOptions<Immediate>
+): WatchStopHandle
+
+// MultiWatchSources 是一个数组
+declare type MultiWatchSources = (WatchSource<unknown> | object)[]
+// ...

但是不管是基础用法还是批量侦听,可以看到这个 API 都是接受三个入参:

参数是否可选含义
source必传数据源(详见:要侦听的数据源
callback必传侦听到变化后要执行的回调函数(详见:侦听后的回调函数
options可选一些侦听选项(详见:侦听的选项

并返回一个可以用来停止侦听的函数(详见:停止侦听)。

要侦听的数据源

在上面 API 的 TS 类型 已经对 watch API 的组成有一定的了解了,这里先对数据源的类型和使用限制做下说明。

TIP

如果不提前了解,在使用的过程中可能会遇到 “侦听了但没有反应” 的情况出现。

另外,这部分内容会先围绕基础用法展开说明,批量侦听会在 批量侦听 部分单独说明。

watch API 的第 1 个参数 source 是要侦听的数据源,它的 TS 类型如下:

ts
// watch 第 1 个入参的 TS 类型
+// ...
+export declare type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
+// ...

可以看到能够用于侦听的数据,是通过 响应式 API 定义的变量( Ref<T> ),或者是一个 计算数据ComputedRef<T> ),或者是一个 getter 函数() => T )。

所以要想定义的 watch 能够做出预期的行为,数据源必须具备响应性或者是一个 getter ,如果只是通过 let 定义一个普通变量,然后去改变这个变量的值,这样是无法侦听的。

TIP

如果要侦听响应式对象里面的某个值(这种情况下对象本身是响应式,但它的 property 不是),需要写成 getter 函数,简单的说就是需要写成有返回值的函数,这个函数 return 要侦听的数据, e.g. () => foo.bar ,可以结合下方 基础用法 的例子一起理解。

侦听后的回调函数

在上面 API 的 TS 类型 介绍了 watch API 的组成,和数据源一样,先了解一下回调函数的定义。

TIP

和数据源部分一样,回调函数的内容也是会先围绕基础用法展开说明,批量侦听会在 批量侦听 部分单独说明。

watch API 的第 2 个参数 callback 是侦听到数据变化时要做出的行为,它的 TS 类型如下:

ts
// watch 第 2 个入参的 TS 类型
+// ...
+export declare type WatchCallback<V = any, OV = any> = (
+  value: V,
+  oldValue: OV,
+  onCleanup: OnCleanup
+) => any
+// ...

乍一看它有三个参数,但实际上这些参数不是自己定义的,而是 watch API 传给的,所以不管用或者不用,它们都在那里:

参数作用
value变化后的新值,类型和数据源保持一致
oldValue变化前的旧值,类型和数据源保持一致
onCleanup注册一个清理函数,详见 侦听效果清理 部分

注意:第一个参数是新值,第二个才是原来的旧值!

如同其他 JS 函数,在使用 watch 的回调函数时,可以对这三个参数任意命名,比如把 value 命名为觉得更容易理解的 newValue

TIP

如果侦听的数据源是一个 引用类型 时( e.g. ObjectArrayDate … ), valueoldValue 是完全相同的,因为指向同一个对象。

另外,默认情况下,watch 是惰性的,也就是只有当被侦听的数据源发生变化时才执行回调。

基础用法

来到这里,对 2 个必传的参数都有一定的了解了,先看看基础的用法,也就是日常最常编写的方案,只需要先关注前 2 个必传的参数。

ts
// 不要忘了导入要用的 API
+import { defineComponent, reactive, watch } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 定义一个响应式数据
+    const userInfo = reactive({
+      name: 'Petter',
+      age: 18,
+    })
+
+    // 2s后改变数据
+    setTimeout(() => {
+      userInfo.name = 'Tom'
+    }, 2000)
+
+    /**
+     * 可以直接侦听这个响应式对象
+     * callback 的参数如果不用可以不写
+     */
+    watch(userInfo, () => {
+      console.log('侦听整个 userInfo ', userInfo.name)
+    })
+
+    /**
+     * 也可以侦听对象里面的某个值
+     * 此时数据源需要写成 getter 函数
+     */
+    watch(
+      // 数据源,getter 形式
+      () => userInfo.name,
+      // 回调函数 callback
+      (newValue, oldValue) => {
+        console.log('只侦听 name 的变化 ', userInfo.name)
+        console.log('打印变化前后的值', { oldValue, newValue })
+      }
+    )
+  },
+})

一般的业务场景,基础用法足以面对。

如果有多个数据源要侦听,并且侦听到变化后要执行的行为一样,那么可以使用 批量侦听

特殊的情况下,可以搭配 侦听的选项 做一些特殊的用法,详见下面部分的内容。

批量侦听

如果有多个数据源要侦听,并且侦听到变化后要执行的行为一样,第一反应可能是这样来写:

  1. 抽离相同的处理行为为公共函数
  2. 然后定义多个侦听操作,传入这个公共函数
ts
import { defineComponent, ref, watch } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const message = ref<string>('')
+    const index = ref<number>(0)
+
+    // 2s后改变数据
+    setTimeout(() => {
+      // 来到这里才会触发 watch 的回调
+      message.value = 'Hello World!'
+      index.value++
+    }, 2000)
+
+    // 抽离相同的处理行为为公共函数
+    const handleWatch = (
+      newValue: string | number,
+      oldValue: string | number
+    ): void => {
+      console.log({ newValue, oldValue })
+    }
+
+    // 然后定义多个侦听操作,传入这个公共函数
+    watch(message, handleWatch)
+    watch(index, handleWatch)
+  },
+})

这样写其实没什么问题,不过除了抽离公共代码的写法之外, watch API 还提供了一个批量侦听的用法,和 基础用法 的区别在于,数据源和回调参数都变成了数组的形式。

数据源:以数组的形式传入,里面每一项都是一个响应式数据。

回调参数:原来的 valuenewValue 也都变成了数组,每个数组里面的顺序和数据源数组排序一致。

可以看下面的这个例子更为直观:

ts
import { defineComponent, ref, watch } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 定义多个数据源
+    const message = ref<string>('')
+    const index = ref<number>(0)
+
+    // 2s后改变数据
+    setTimeout(() => {
+      message.value = 'Hello World!'
+      index.value++
+    }, 2000)
+
+    watch(
+      // 数据源改成了数组
+      [message, index],
+      // 回调的入参也变成了数组,每个数组里面的顺序和数据源数组排序一致
+      ([newMessage, newIndex], [oldMessage, oldIndex]) => {
+        console.log('message 的变化', { newMessage, oldMessage })
+        console.log('index 的变化', { newIndex, oldIndex })
+      }
+    )
+  },
+})

什么情况下可能会用到批量侦听呢?比如一个子组件有多个 props ,当有任意一个 prop 发生变化时,都需要执行初始化函数重置组件的状态,那么这个时候就可以用上这个功能啦!

TIP

在适当的业务场景,也可以使用 watchEffect 来完成批量侦听,但请留意 功能区别 部分的说明。

侦听的选项

API 的 TS 类型 里提到, watch API 还接受第 3 个参数 options ,可选的一些侦听选项。

它的 TS 类型如下:

ts
// watch 第 3 个入参的 TS 类型
+// ...
+export declare interface WatchOptions<Immediate = boolean>
+  extends WatchOptionsBase {
+  immediate?: Immediate
+  deep?: boolean
+}
+// ...
+
+// 继承的 base 类型
+export declare interface WatchOptionsBase extends DebuggerOptions {
+  flush?: 'pre' | 'post' | 'sync'
+}
+// ...
+
+// 继承的 debugger 选项类型
+export declare interface DebuggerOptions {
+  onTrack?: (event: DebuggerEvent) => void
+  onTrigger?: (event: DebuggerEvent) => void
+}
+// ...

options 是一个对象的形式传入,有以下几个选项:

选项类型默认值可选值作用
deepbooleanfalsetrue | false是否进行深度侦听
immediatebooleanfalsetrue | false是否立即执行侦听回调
flushstring'pre''pre' | 'post' | 'sync'控制侦听回调的调用时机
onTrack(e) => void在数据源被追踪时调用
onTrigger(e) => void在侦听回调被触发时调用

其中 onTrackonTriggere 是 debugger 事件,建议在回调内放置一个 debugger 语句 以调试依赖,这两个选项仅在开发模式下生效。

TIP

deep 默认是 false ,但是在侦听 reactive 对象或数组时,会默认为 true ,详见 侦听选项之 deep

侦听选项之 deep

deep 选项接受一个布尔值,可以设置为 true 开启深度侦听,或者是 false 关闭深度侦听,默认情况下这个选项是 false 关闭深度侦听的,但也存在特例。

设置为 false 的情况下,如果直接侦听一个响应式的 引用类型 数据(e.g. ObjectArray … ),虽然它的属性的值有变化,但对其本身来说是不变的,所以不会触发 watch 的 callback 。

下面是一个关闭了深度侦听的例子:

ts
import { defineComponent, ref, watch } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 定义一个响应式数据,注意用的是 ref 来定义
+    const nums = ref<number[]>([])
+
+    // 2s后给这个数组添加项目
+    setTimeout(() => {
+      nums.value.push(1)
+
+      // 可以打印一下,确保数据确实变化了
+      console.log('修改后', nums.value)
+    }, 2000)
+
+    // 但是这个 watch 不会按预期执行
+    watch(
+      nums,
+      // 这里的 callback 不会被触发
+      () => {
+        console.log('触发侦听', nums.value)
+      },
+      // 因为关闭了 deep
+      {
+        deep: false,
+      }
+    )
+  },
+})

类似这种情况,需要把 deep 设置为 true 才可以触发侦听。

可以看到上面的例子特地用了 ref API ,这是因为通过 reactive API 定义的对象无法将 deep 成功设置为 false (这一点在目前的官网文档未找到说明,最终是在 watch API 的源码 上找到了答案)。

ts
// ...
+if (isReactive(source)) {
+  getter = () => source
+  deep = true // 被强制开启了
+}
+// ...

这个情况就是上面所说的 “特例” ,可以通过 isReactive API 来判断是否需要手动开启深度侦听。

ts
// 导入 isReactive API
+import { defineComponent, isReactive, reactive, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 侦听这个数据时,会默认开启深度侦听
+    const foo = reactive({
+      name: 'Petter',
+      age: 18,
+    })
+    console.log(isReactive(foo)) // true
+
+    // 侦听这个数据时,不会默认开启深度侦听
+    const bar = ref({
+      name: 'Petter',
+      age: 18,
+    })
+    console.log(isReactive(bar)) // false
+  },
+})

侦听选项之 immediate

侦听后的回调函数 部分有了解过, watch 默认是惰性的,也就是只有当被侦听的数据源发生变化时才执行回调。

这句话是什么意思呢?来看一下这段代码,为了减少 deep 选项的干扰,换一个类型,换成 string 数据来演示,请留意注释:

ts
import { defineComponent, ref, watch } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 这个时候不会触发 watch 的回调
+    const message = ref<string>('')
+
+    // 2s后改变数据
+    setTimeout(() => {
+      // 来到这里才会触发 watch 的回调
+      message.value = 'Hello World!'
+    }, 2000)
+
+    watch(message, () => {
+      console.log('触发侦听', message.value)
+    })
+  },
+})

可以看到,数据在初始化的时候并不会触发侦听回调,如果有需要的话,通过 immediate 选项来让它直接触发。

immediate 选项接受一个布尔值,默认是 false ,可以设置为 true 让回调立即执行。

改成这样,请留意高亮的代码部分和新的注释:

ts
import { defineComponent, ref, watch } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 这一次在这里可以会触发 watch 的回调了
+    const message = ref<string>('')
+
+    // 2s后改变数据
+    setTimeout(() => {
+      // 这一次,这里是第二次触发 watch 的回调,不再是第一次
+      message.value = 'Hello World!'
+    }, 2000)
+
+    watch(
+      message,
+      () => {
+        console.log('触发侦听', message.value)
+      },
+      // 设置 immediate 选项
+      {
+        immediate: true,
+      }
+    )
+  },
+})

注意,在带有 immediate 选项时,不能在第一次回调时取消该数据源的侦听,详见 停止侦听 部分。

侦听选项之 flush

flush 选项是用来控制 侦听回调 的调用时机,接受指定的字符串,可选值如下,默认是 'pre'

可选值回调的调用时机使用场景
'pre'将在渲染前被调用允许回调在模板运行前更新了其他值
'sync'在渲染时被同步调用目前来说没什么好处,可以了解但不建议用…
'post'被推迟到渲染之后调用如果要通过 ref 操作 DOM 元素与子组件 ,需要使用这个值来启用该选项,以达到预期的执行效果

对于 'pre''post' ,回调使用队列进行缓冲。回调只被添加到队列中一次。

即使观察值变化了多次,值的中间变化将被跳过,不会传递给回调,这样做不仅可以提高性能,还有助于保证数据的一致性。

更多关于 flush 的信息,请参阅 回调的触发时机

停止侦听

如果在 setup 或者 script-setup 里使用 watch 的话, 组件被卸载 的时候也会一起被停止,一般情况下不太需要关心如何停止侦听。

不过有时候可能想要手动取消, Vue 3 也提供了方法。

TIP

随着组件被卸载一起停止的前提是,侦听器必须是 同步语句 创建的,这种情况下侦听器会绑定在当前组件上。

如果放在 setTimeout异步函数 里面创建,则不会绑定到当前组件,因此组件卸载的时候不会一起停止该侦听器,这种时候就需要手动停止侦听。

API 的 TS 类型 有提到,当在定义一个 watch 行为的时候,它会返回一个用来停止侦听的函数。

这个函数的 TS 类型如下:

ts
export declare type WatchStopHandle = () => void

用法很简单,做一下简单了解即可:

ts
// 定义一个取消观察的变量,它是一个函数
+const unwatch = watch(message, () => {
+  // ...
+})
+
+// 在合适的时期调用它,可以取消这个侦听
+unwatch()

但是也有一点需要注意的是,如果启用了 immediate 选项 ,不能在第一次触发侦听回调时执行它。

ts
// 注意:这是一段错误的代码,运行会报错
+const unwatch = watch(
+  message,
+  // 侦听的回调
+  () => {
+    // ...
+    // 在这里调用会有问题 ❌
+    unwatch()
+  },
+  // 启用 immediate 选项
+  {
+    immediate: true,
+  }
+)

会收获一段报错,告诉 unwatch 这个变量在初始化前无法被访问:

bash
Uncaught ReferenceError: Cannot access 'unwatch' before initialization

目前有两种方案可以让实现这个操作:

方案一:使用 var 并判断变量类型,利用 var 的变量提升 来实现目的。

ts
// 这里改成 var ,不要用 const 或 let
+var unwatch = watch(
+  message,
+  // 侦听回调
+  () => {
+    // 这里加一个判断,是函数才执行它
+    if (typeof unwatch === 'function') {
+      unwatch()
+    }
+  },
+  // 侦听选项
+  {
+    immediate: true,
+  }
+)

不过 var 已经属于过时的语句了,建议用方案二的 let

方案二:使用 let 并判断变量类型。

ts
// 如果不想用 any ,可以导入 TS 类型
+import type { WatchStopHandle } from 'vue'
+
+// 这里改成 let ,但是要另起一行,先定义,再赋值
+let unwatch: WatchStopHandle
+unwatch = watch(
+  message,
+  // 侦听回调
+  () => {
+    // 这里加一个判断,是函数才执行它
+    if (typeof unwatch === 'function') {
+      unwatch()
+    }
+  },
+  // 侦听选项
+  {
+    immediate: true,
+  }
+)

侦听效果清理

侦听后的回调函数 部分提及到一个参数 onCleanup ,它可以帮注册一个清理函数。

有时 watch 的回调会执行异步操作,当 watch 到数据变更的时候,需要取消这些操作,这个函数的作用就用于此,会在以下情况调用这个清理函数:

  • watcher 即将重新运行的时候
  • watcher 被停止(组件被卸载或者被手动 停止侦听

TS 类型:

ts
declare type OnCleanup = (cleanupFn: () => void) => void

用法方面比较简单,传入一个回调函数运行即可,不过需要注意的是,需要在停止侦听之前注册好清理行为,否则不会生效。

停止侦听 里的最后一个 immediate 例子的基础上继续添加代码,请注意注册的时机:

ts
let unwatch: WatchStopHandle
+unwatch = watch(
+  message,
+  (newValue, oldValue, onCleanup) => {
+    // 需要在停止侦听之前注册好清理行为
+    onCleanup(() => {
+      console.log('侦听清理ing')
+      // 根据实际的业务情况定义一些清理操作 ...
+    })
+    // 然后再停止侦听
+    if (typeof unwatch === 'function') {
+      unwatch()
+    }
+  },
+  {
+    immediate: true,
+  }
+)

watchEffect

如果一个函数里包含了多个需要侦听的数据,一个一个数据去侦听太麻烦了,在 Vue 3 ,可以直接使用 watchEffect API 来简化的操作。

API 的 TS 类型

这个 API 的类型如下,使用的时候需要传入一个副作用函数(相当于 watch 的 侦听后的回调函数 ),也可以根据的实际情况传入一些可选的 侦听选项

和 watch API 一样,它也会返回一个用于 停止侦听 的函数。

ts
// watchEffect 部分的 TS 类型
+// ...
+export declare type WatchEffect = (onCleanup: OnCleanup) => void
+
+export declare function watchEffect(
+  effect: WatchEffect,
+  options?: WatchOptionsBase
+): WatchStopHandle
+// ...

副作用函数也会传入一个清理回调作为参数,和 watch 的 侦听效果清理 一样的用法。

可以理解为它是一个简化版的 watch ,具体简化在哪里呢?请看下面的用法示例。

用法示例

它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

ts
import { defineComponent, ref, watchEffect } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 单独定义两个数据,后面用来分开改变数值
+    const name = ref<string>('Petter')
+    const age = ref<number>(18)
+
+    // 定义一个调用这两个数据的函数
+    const getUserInfo = (): void => {
+      console.log({
+        name: name.value,
+        age: age.value,
+      })
+    }
+
+    // 2s后改变第一个数据
+    setTimeout(() => {
+      name.value = 'Tom'
+    }, 2000)
+
+    // 4s后改变第二个数据
+    setTimeout(() => {
+      age.value = 20
+    }, 4000)
+
+    // 直接侦听调用函数,在每个数据产生变化的时候,它都会自动执行
+    watchEffect(getUserInfo)
+  },
+})

和 watch 的区别

虽然理论上 watchEffectwatch 的一个简化操作,可以用来代替 批量侦听 ,但它们也有一定的区别:

  1. watch 可以访问侦听状态变化前后的值,而 watchEffect 没有。

  2. watch 是在属性改变的时候才执行,而 watchEffect 则默认会执行一次,然后在属性改变的时候也会执行。

第二点的意思,看下面这段代码可以有更直观的理解:

使用 watch :

ts
export default defineComponent({
+  setup() {
+    const foo = ref<string>('')
+
+    setTimeout(() => {
+      foo.value = 'Hello World!'
+    }, 2000)
+
+    function bar() {
+      console.log(foo.value)
+    }
+
+    // 使用 watch 需要先手动执行一次
+    bar()
+
+    // 然后当 foo 有变动时,才会通过 watch 来执行 bar()
+    watch(foo, bar)
+  },
+})

使用 watchEffect :

ts
export default defineComponent({
+  setup() {
+    const foo = ref<string>('')
+
+    setTimeout(() => {
+      foo.value = 'Hello World!'
+    }, 2000)
+
+    function bar() {
+      console.log(foo.value)
+    }
+
+    // 可以通过 watchEffect 实现 bar() + watch(foo, bar) 的效果
+    watchEffect(bar)
+  },
+})

可用的侦听选项

虽然用法和 watch 类似,但也简化了一些选项,它的侦听选项 TS 类型如下:

ts
// 只支持 base 类型
+export declare interface WatchOptionsBase extends DebuggerOptions {
+  flush?: 'pre' | 'post' | 'sync'
+}
+// ...
+
+// 继承的 debugger 选项类型
+export declare interface DebuggerOptions {
+  onTrack?: (event: DebuggerEvent) => void
+  onTrigger?: (event: DebuggerEvent) => void
+}
+// ...

对比 watch API ,它不支持 deepimmediate ,请记住这一点,其他的用法是一样的。

flush 选项的使用详见 侦听选项之 flushonTrackonTrigger 详见 侦听的选项 部分内容。

watchPostEffect

watchEffect API 使用 flush: 'post' 选项时的别名,具体区别详见 侦听选项之 flush 部分。

TIP

Vue v3.2.0 及以上版本才支持该 API 。

watchSyncEffect

watchEffect API 使用 flush: 'sync' 选项时的别名,具体区别详见 侦听选项之 flush 部分。

TIP

Vue v3.2.0 及以上版本才支持该 API 。

数据的计算 ~new

和 Vue 2.0 一样,数据的计算也是使用 computed API ,它可以通过现有的响应式数据,去通过计算得到新的响应式变量,用过 Vue 2.0 的开发者应该不会太陌生,但是在 Vue 3.0 ,在使用方式上也是变化非常大!

TIP

这里的响应式数据,可以简单理解为通过 ref API 、 reactive API 定义出来的数据,当然 Vuex 、Vue Router 等 Vue 数据也都具备响应式,可以在 响应式数据的变化 了解。

用法变化

先从一个简单的用例来看看在 Vue 新旧版本的用法区别:

假设定义了两个分开的数据 firstName 名字和 lastName 姓氏,但是在 template 展示时,需要展示完整的姓名,那么就可以通过 computed 来计算一个新的数据:

回顾 Vue 2

在 Vue 2.0 ,computeddata 在同级配置,并且不可以和 data 里的数据同名重复定义:

ts
// 在 Vue 2 的写法:
+export default {
+  data() {
+    return {
+      firstName: 'Bill',
+      lastName: 'Gates',
+    }
+  },
+  // 注意这里定义的变量,都要通过函数的形式来返回它的值
+  computed: {
+    // 普通函数可以直接通过熟悉的 this 来拿到 data 里的数据
+    fullName() {
+      return \`\${this.firstName} \${this.lastName}\`
+    },
+    // 箭头函数则需要通过参数来拿到实例上的数据
+    fullName2: (vm) => \`\${vm.firstName} \${vm.lastName}\`,
+  },
+}

这样在需要用到全名的地方,只需要通过 this.fullName 就可以得到 Bill Gates

了解 Vue 3

在 Vue 3.0 ,跟其他 API 的用法一样,需要先导入 computed 才能使用:

ts
// 在 Vue 3 的写法:
+import { defineComponent, ref, computed } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 定义基本的数据
+    const firstName = ref<string>('Bill')
+    const lastName = ref<string>('Gates')
+
+    // 定义需要计算拼接结果的数据
+    const fullName = computed(() => \`\${firstName.value} \${lastName.value}\`)
+
+    // 2s 后改变某个数据的值
+    setTimeout(() => {
+      firstName.value = 'Petter'
+    }, 2000)
+
+    // template 那边在 2s 后也会显示为 Petter Gates
+    return {
+      fullName,
+    }
+  },
+})

可以把这个用法简单的理解为,传入一个回调函数,并 return 一个值,对,它需要有明确的返回值。

TIP

需要注意的是:

  1. 定义出来的 computed 变量,和 Ref 变量的用法一样,也是需要通过 .value 才能拿到它的值

  2. 但是区别在于,默认情况下 computedvalue 是只读的

原因详见下方的 类型声明

类型声明

之前说过,在 defineComponent 里,会自动帮推导 Vue API 的类型,所以一般情况下,是不需要显式的去定义 computed 出来的变量类型的。

在确实需要手动指定的情况下,也可以导入它的类型然后定义:

ts
import { computed } from 'vue'
+import type { ComputedRef } from 'vue'
+
+// 注意这里添加了类型声明
+const fullName: ComputedRef<string> = computed(
+  () => \`\${firstName.value} \${lastName.value}\`
+)

要返回一个字符串,就写 ComputedRef<string> ;返回布尔值,就写 ComputedRef<boolean> ;返回一些复杂对象信息,可以先定义好的类型,再诸如 ComputedRef<UserInfo> 去写。

ts
// 这是 ComputedRef 的类型声明:
+export declare interface ComputedRef<T = any> extends WritableComputedRef<T> {
+  readonly value: T
+  [ComoutedRefSymbol]: true
+}

优势对比和注意事项

在继续往下看之前,先来了解一下这个 API 的一些优势和注意事项(如果在 Vue 2 已经有接触过的话,可以跳过这一段,因为优势和需要注意的东西比较一致)。

优势对比

看到这里,相信刚接触的开发者可能会有疑问,既然 computed 也是通过一个函数来返回值,那么和普通的 function 有什么区别,或者说优势?

  1. 性能优势

这一点在 官网文档 其实是有提到的:

数据的计算是基于它们的响应依赖关系缓存的,只在相关响应式依赖发生改变时它们才会重新求值。

也就是说,只要原始数据没有发生改变,多次访问 computed ,都是会立即返回之前的计算结果,而不是再次执行函数;而普通的 function 调用多少次就执行多少次,每调用一次就计算一次。

至于为何要如此设计,官网文档也给出了原因:

为什么需要缓存?假设有一个性能开销比较大的计算数据 list,它需要遍历一个巨大的数组并做大量的计算。然后可能有其他的计算数据依赖于 list。如果没有缓存,将不可避免的多次执行 list 的 getter!如果不希望有缓存,请用 function 来替代。

TIP

在这部分内容里,将官方文档的一些用词做了更换,比如把 method 都替换成了 function ,也把 “计算属性” 都换成了 “计算数据”,原因在于官网很多地方是基于 Options API 的写法去描述,而本文档是基于 Composition API 。

点击了解: 如何理解 JavaScript 中方法(method)和函数(function)的区别?

  1. 书写统一

假定 foo1 是 Ref 变量, foo2 是 computed 变量, foo3 是普通函数返回值

看到这里的开发者应该都已经清楚 Vue 3 的 Ref 变量是通过 foo1.value 来拿到值的,而 computed 也是通过 foo2.value ,并且在 template 里都可以省略 .value ,在读取方面,他们是有一致的风格和简洁性。

而 foo3 不管是在 script 还是 template ,都需要通过 foo3() 才能拿到结果,相对来说会有那么一丢丢别扭。

当然,关于这一点,如果涉及到的数据不是响应式数据,那么还是老老实实的用函数返回值吧,原因请见下面的 注意事项

注意事项

有优势当然也就有一定的 “劣势” ,当然这也是 Vue 框架的有意为之,所以在使用上也需要注意一些问题:

  1. 只会更新响应式数据的计算

假设要获取当前的时间信息,因为不是响应式数据,所以这种情况下就需要用普通的函数去获取返回值,才能拿到最新的时间。

ts
const nowTime = computed(() => new Date())
+console.log(nowTime.value)
+// 输出 Sun Nov 14 2021 21:07:00 GMT+0800 (GMT+08:00)
+
+// 2s 后依然是跟上面一样的结果
+setTimeout(() => {
+  console.log(nowTime.value)
+  // 还是输出 Sun Nov 14 2021 21:07:00 GMT+0800 (GMT+08:00)
+}, 2000)
  1. 数据是只读的

通过 computed 定义的数据,它是只读的,这一点在 类型声明 已经有所了解。

如果直接赋值,不仅无法变更数据,而且会收获一个报错。

bash
TS2540: Cannot assign to 'value' because it is a read-only property.

虽然无法直接赋值,但是在必要的情况下,依然可以通过 computedsetter 来更新数据。

点击了解:computed 的 setter 用法

setter 的使用

通过 computed 定义的变量默认都是只读的形式(只有一个 getter ),但是在必要的情况下,也可以使用其 setter 属性来更新数据。

基本格式

当需要用到 setter 的时候, computed 就不再是一个传入 callback 的形式了,而是传入一个带有 2 个方法的对象。

ts
// 注意这里computed接收的入参已经不再是函数
+const foo = computed({
+  // 这里需要明确的返回一个值
+  get() {
+    // ...
+  },
+  // 这里接收一个参数,代表修改 foo 时,赋值下来的新值
+  set(newValue) {
+    // ...
+  },
+})

这里的 get 就是 computed 的 getter ,跟原来传入 callback 的形式一样,是用于 foo.value 的读取,所以这里必须有明确的返回值。

这里的 set 就是 computed 的 setter ,它会接收一个参数,代表新的值,当通过 foo.value = xxx 赋值的时候,赋入的这个值,就会通过这个入参来传递进来,可以根据的业务需要,把这个值,赋给相关的数据源。

TIP

请注意,必须使用 getset 这 2 个方法名,也只接受这 2 个方法。

在了解了基本格式后,可以查看下面的例子来了解具体的用法。

使用示范

官网的 例子 是一个 Options API 的案例,这里改成 Composition API 的写法来演示:

ts
// 还是这2个数据源
+const firstName = ref<string>('Bill')
+const lastName = ref<string>('Gates')
+
+// 这里配合setter的需要,改成了另外一种写法
+const fullName = computed({
+  // getter还是返回一个拼接起来的全名
+  get() {
+    return \`\${firstName.value} \${lastName.value}\`
+  },
+  // setter这里改成只更新firstName,注意参数也定义TS类型
+  set(newFirstName: string) {
+    firstName.value = newFirstName
+  },
+})
+console.log(fullName.value) // 输出 Bill Gates
+
+// 2s后更新一下数据
+setTimeout(() => {
+  // 对fullName的赋值,其实更新的是firstName
+  fullName.value = 'Petter'
+
+  // 此时firstName已经得到了更新
+  console.log(firstName.value) // 会输出 Petter
+
+  // 当然,由于firstName变化了,所以fullName的getter也会得到更新
+  console.log(fullName.value) // 会输出 Petter Gates
+}, 2000)

应用场景

计算 API 的作用,官网文档只举了一个非常简单的例子,那么在实际项目中,什么情况下用它会让更方便呢?

简单举几个比较常见的例子吧,加深一下对 computed 的理解。

数据的拼接和计算

如上面的案例,与其每个用到的地方都要用到 firstName + ' ' + lastName 这样的多变量拼接,不如用一个 fullName 来的简单。

当然,不止是字符串拼接,数据的求和等操作更是合适,比如说做一个购物车,购物车里有商品列表,同时还要显示购物车内的商品总金额,这种情况就非常适合用计算数据。

复用组件的动态数据

在一个项目里,很多时候组件会涉及到复用,比如说:“首页的文章列表 vs 列表页的文章列表 vs 作者详情页的文章列表” ,特别常见于新闻网站等内容资讯站点,这种情况下,往往并不需要每次都重新写 UI 、数据渲染等代码,仅仅是接口 URL 的区别。

这种情况就可以通过路由名称来动态获取要调用哪个列表接口:

ts
const route = useRoute()
+
+// 定义一个根据路由名称来获取接口URL的计算数据
+const apiUrl = computed(() => {
+  switch (route.name) {
+    // 首页
+    case 'home':
+      return '/api/list1'
+    // 列表页
+    case 'list':
+      return '/api/list2'
+    // 作者页
+    case 'author':
+      return '/api/list3'
+    // 默认是随机列表
+    default:
+      return '/api/random'
+  }
+})
+
+// 请求列表
+const getArticleList = async (): Promise<void> => {
+  // ...
+  articleList.value = await axios({
+    method: 'get',
+    url: apiUrl.value,
+    // ...
+  })
+  // ...
+}

当然,这种情况也可以在父组件通过 props 传递接口 URL ,如果已经学到了 组件通讯 一章的话。

获取多级对象的值

应该很经常的遇到要在 template 显示一些多级对象的字段,但是有时候又可能存在某些字段不一定有,需要做一些判断的情况,虽然有 v-if ,但是嵌套层级一多,模板代码会难以维护。

如果把这些工作量转移给计算数据,结合 try / catch ,这样就无需在 template 里处理很多判断了。

ts
// 例子比较极端,但在 Vuex 这种大型数据树上,也不是完全不可能存在
+const foo = computed(() => {
+  // 正常情况下返回需要的数据
+  try {
+    return store.state.foo3.foo2.foo1.foo
+  } catch (e) {
+    // 处理失败则返回一个默认值
+    return ''
+  }
+})

这样在 template 里要拿到 foo 的值,完全不需要关心中间一级又一级的字段是否存在,只需要区分是不是默认值。

不同类型的数据转换

有时候会遇到一些需求类似于,让用户在输入框里,按一定的格式填写文本,比如用英文逗号 , 隔开每个词,然后保存的时候,是用数组的格式提交给接口。

这个时候 computed 的 setter 就可以妙用了,只需要一个简单的 computed ,就可以代替 inputchange 事件或者 watch 侦听,可以减少很多业务代码的编写。

vue
<template>
+  <input
+    type="text"
+    v-model="tagsStr"
+    placeholder="请输入标签,多个标签用英文逗号隔开"
+  />
+</template>
+
+<script lang="ts">
+import { defineComponent, computed, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 这个是最终要用到的数组
+    const tags = ref<string[]>([])
+
+    // 因为input必须绑定一个字符串
+    const tagsStr = computed({
+      // 所以通过getter来转成字符串
+      get() {
+        return tags.value.join(',')
+      },
+      // 然后在用户输入的时候,切割字符串转换回数组
+      set(newValue: string) {
+        tags.value = newValue.split(',')
+      },
+    })
+
+    return {
+      tagsStr,
+    }
+  },
+})
+</script>

所以在实际业务开发中,开发者可以多考虑一下是否可以使用 computed 代替 watch ,避免过多的数组侦听带来项目性能的下降。

指令

指令是 Vue 模板语法里的特殊标记,在使用上和 HTML 的 data-* 属性十分相似,统一以 v- 开头( e.g. v-html )。

它以简单的方式实现了常用的 JavaScript 表达式功能,当表达式的值改变的时候,响应式地作用到 DOM 上。

内置指令

Vue 提供了一些内置指令可以直接使用,例如:

vue
<template>
+  <!-- 渲染一段文本 -->
+  <span v-text="msg"></span>
+
+  <!-- 渲染一段 HTML -->
+  <div v-html="html"></div>
+
+  <!-- 循环创建一个列表 -->
+  <ul v-if="items.length">
+    <li v-for="(item, index) in items" :key="index">
+      <span>{{ item }}</span>
+    </li>
+  </ul>
+
+  <!-- 一些事件( \`@\` 等价于 \`v-on\` ) -->
+  <button @click="hello">Hello</button>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const msg = ref<string>('Hello World!')
+    const html = ref<string>('<p>Hello World!</p>')
+    const items = ref<string[]>(['a', 'b', 'c', 'd'])
+
+    function hello() {
+      console.log(msg.value)
+    }
+
+    return {
+      msg,
+      html,
+      items,
+      hello,
+    }
+  },
+})
+</script>

内置指令在使用上都非常的简单,可以在官方文档的 内置指令 一章查询完整的指令列表和用法,在模板上使用时,请了解 指令的模板语法

TIP

有两个指令可以使用别名:

  • v-on 的别名是 @ ,使用 @click 等价于 v-on:click
  • v-bind 的别名是 : ,使用 :src 等价于 v-bind:src

自定义指令 ~new

如果 Vue 的内置指令不能满足业务需求,还可以开发自定义指令。

相关的 TS 类型

在开始编写代码之前,先了解一下自定义指令相关的 TypeScript 类型。

自定义指令有两种实现形式,一种是作为一个对象,其中的写法比较接近于 Vue 组件,除了 getSSRPropsdeep 选项 外,其他的每一个属性都是一个 钩子函数 ,下一小节会介绍钩子函数的内容。

ts
// 对象式写法的 TS 类型
+// ...
+export declare interface ObjectDirective<T = any, V = any> {
+  created?: DirectiveHook<T, null, V>
+  beforeMount?: DirectiveHook<T, null, V>
+  mounted?: DirectiveHook<T, null, V>
+  beforeUpdate?: DirectiveHook<T, VNode<any, T>, V>
+  updated?: DirectiveHook<T, VNode<any, T>, V>
+  beforeUnmount?: DirectiveHook<T, null, V>
+  unmounted?: DirectiveHook<T, null, V>
+  getSSRProps?: SSRDirectiveHook
+  deep?: boolean
+}
+// ...

另外一种是函数式写法,只需要定义成一个函数,但这种写法只在 mountedupdated 这两个钩子生效,并且触发一样的行为。

ts
// 函数式写法的 TS 类型
+// ...
+export declare type FunctionDirective<T = any, V = any> = DirectiveHook<
+  T,
+  any,
+  V
+>
+// ...

这是每个钩子函数对应的类型,它有 4 个入参:

ts
// 钩子函数的 TS 类型
+// ...
+export declare type DirectiveHook<
+  T = any,
+  Prev = VNode<any, T> | null,
+  V = any
+> = (
+  el: T,
+  binding: DirectiveBinding<V>,
+  vnode: VNode<any, T>,
+  prevVNode: Prev
+) => void
+// ...

钩子函数第二个参数的类型:

ts
// 钩子函数第二个参数的 TS 类型
+// ...
+export declare interface DirectiveBinding<V = any> {
+  instance: ComponentPublicInstance | null
+  value: V
+  oldValue: V | null
+  arg?: string
+  modifiers: DirectiveModifiers
+  dir: ObjectDirective<any, V>
+}
+// ...

可以看到自定义指令最核心的就是 “钩子函数” 了,接下来来了解这部分的知识点。

钩子函数

组件的生命周期 类似,自定义指令里的逻辑代码也有一些特殊的调用时机,在这里称之为钩子函数:

钩子函数调用时机
created在绑定元素的 attribute 或事件侦听器被应用之前调用
beforeMount当指令第一次绑定到元素并且在挂载父组件之前调用
mounted在绑定元素的父组件被挂载后调用
beforeUpdate在更新包含组件的 VNode 之前调用
updated在包含组件的 VNode 及其子组件的 VNode 更新后调用
beforeUnmount在卸载绑定元素的父组件之前调用
unmounted当指令与元素解除绑定且父组件已卸载时,只调用一次

TIP

因为自定义指令的默认写法是一个对象,所以在代码风格上是遵循 Options API 的生命周期命名,而非 Vue 3 的 Composition API 风格。

钩子函数在用法上就是这样子:

ts
const myDirective = {
+  created(el, binding, vnode, prevVnode) {
+    // ...
+  },
+  mounted(el, binding, vnode, prevVnode) {
+    // ...
+  },
+  // 其他钩子...
+}

相关的 TS 类型 已了解,每个钩子函数都有 4 个入参:

参数作用
el指令绑定的 DOM 元素,可以直接操作它
binding一个对象数据,见下方的单独说明
vnodeel 对应在 Vue 里的虚拟节点信息
prevVNodeUpdate 时的上一个虚拟节点信息,仅在 beforeUpdateupdated 可用

其中用的最多是 elbinding 了。

  • el 的值就是通过 document.querySelector 拿到的那个 DOM 元素。

  • binding 是一个对象,里面包含了以下属性:

属性作用
value传递给指令的值,例如 v-foo="bar" 里的 bar ,支持任意有效的 JS 表达式
oldValue指令的上一个值,仅对 beforeUpdateupdated 可用
arg传给指令的参数,例如 v-foo:bar 里的 bar
modifiers传给指令的修饰符,例如 v-foo.bar 里的 bar
instance使用指令的组件实例
dir指令定义的对象(就是上面的 const myDirective = { /* ... */ } 这个对象)

在了解了指令的写法和参数作用之后,来看看如何注册一个自定义指令。

局部注册

自定义指令可以在单个组件内定义并使用,通过和 setup 函数 同级别的 directives 选项进行定义,可以参考下面的例子和注释:

vue
<template>
+  <!-- 这个使用默认值 \`unset\` -->
+  <div v-highlight>{{ msg }}</div>
+
+  <!-- 这个使用传进去的黄色 -->
+  <div v-highlight="\`yellow\`">{{ msg }}</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  // 自定义指令在这里编写,和 \`setup\` 同级别
+  directives: {
+    // \`directives\` 下的每个字段名就是指令名称
+    highlight: {
+      // 钩子函数
+      mounted(el, binding) {
+        el.style.backgroundColor =
+          typeof binding.value === 'string' ? binding.value : 'unset'
+      },
+    },
+  },
+  setup() {
+    const msg = ref<string>('Hello World!')
+
+    return {
+      msg,
+    }
+  },
+})
+</script>

上面是对象式的写法,也可以写成函数式:

ts
export default defineComponent({
+  directives: {
+    highlight(el, binding) {
+      el.style.backgroundColor =
+        typeof binding.value === 'string' ? binding.value : 'unset'
+    },
+  },
+})

TIP

局部注册的自定义指令,默认在子组件内生效,子组件内无需重新注册即可使用父组件的自定义指令。

全局注册

自定义指令也可以注册成全局,这样就无需在每个组件里定义了,只要在入口文件 main.ts 里启用它,任意组件里都可以使用自定义指令。

请查看 开发本地 Vue 专属插件 一节的内容了解如何注册一个全局的自定义指令插件。

deep 选项

除了 钩子函数 ,在 相关的 TS 类型 里还可以看到有一个 deep 选项,它是一个布尔值,作用是:

如果自定义指令用于一个有嵌套属性的对象,并且需要在嵌套属性更新的时候触发 beforeUpdateupdated 钩子,那么需要将这个选项设置为 true 才能够生效。

vue
<template>
+  <div v-foo="foo"></div>
+</template>
+
+<script lang="ts">
+import { defineComponent, reactive } from 'vue'
+
+export default defineComponent({
+  directives: {
+    foo: {
+      beforeUpdate(el, binding) {
+        console.log('beforeUpdate', binding)
+      },
+      updated(el, binding) {
+        console.log('updated', binding)
+      },
+      mounted(el, binding) {
+        console.log('mounted', binding)
+      },
+      // 需要设置为 \`true\` ,如果是 \`false\` 则不会触发
+      deep: true,
+    },
+  },
+  setup() {
+    // 定义一个有嵌套属性的对象
+    const foo = reactive({
+      bar: {
+        baz: 1,
+      },
+    })
+
+    // 2s 后修改其中一个值,会触发 \`beforeUpdate\` 和 \`updated\`
+    setTimeout(() => {
+      foo.bar.baz = 2
+      console.log(foo)
+    }, 2000)
+
+    return {
+      foo,
+    }
+  },
+})
+</script>

插槽

Vue 在使用子组件的时候,子组件在 template 里类似一个 HTML 标签,可以在这个子组件标签里传入任意模板代码以及 HTML 代码,这个功能就叫做 “插槽” 。

默认插槽

默认情况下,子组件使用 <slot /> 标签即可渲染父组件传下来的插槽内容,例如:

在父组件这边:

vue
<template>
+  <Child>
+    <!-- 注意这里,子组件标签里面传入了 HTML 代码 -->
+    <p>这是插槽内容</p>
+  </Child>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+import Child from '@cp/Child.vue'
+
+export default defineComponent({
+  components: {
+    Child,
+  },
+})
+</script>

在子组件这边:

vue
<template>
+  <slot />
+</template>

默认插槽非常简单,一个 <slot /> 就可以了。

具名插槽

有时候可能需要指定多个插槽,例如一个子组件里有 “标题” 、 “作者”、 “内容” 等预留区域可以显示对应的内容,这时候就需要用到具名插槽来指定不同的插槽位。

子组件通过 name 属性来指定插槽名称:

vue
<template>
+  <!-- 显示标题的插槽内容 -->
+  <div class="title">
+    <slot name="title" />
+  </div>
+
+  <!-- 显示作者的插槽内容 -->
+  <div class="author">
+    <slot name="author" />
+  </div>
+
+  <!-- 其他插槽内容放到这里 -->
+  <div class="content">
+    <slot />
+  </div>
+</template>

父组件通过 template 标签绑定 v-slot:name 格式的属性,来指定传入哪个插槽里:

vue
<template>
+  <Child>
+    <!-- 传给标题插槽 -->
+    <template v-slot:title>
+      <h1>这是标题</h1>
+    </template>
+
+    <!-- 传给作者插槽 -->
+    <template v-slot:author>
+      <h1>这是作者信息</h1>
+    </template>
+
+    <!-- 传给默认插槽 -->
+    <p>这是插槽内容</p>
+  </Child>
+</template>

v-slot:name 有一个别名 #name 语法,上面父组件的代码也相当于:

vue
<template>
+  <Child>
+    <!-- 传给标题插槽 -->
+    <template #title>
+      <h1>这是标题</h1>
+    </template>
+
+    <!-- 传给作者插槽 -->
+    <template #author>
+      <h1>这是作者信息</h1>
+    </template>
+
+    <!-- 传给默认插槽 -->
+    <p>这是插槽内容</p>
+  </Child>
+</template>

TIP

在使用具名插槽的时候,子组件如果不指定默认插槽,那么在具名插槽之外的内容将不会被渲染。

默认内容

可以给 slot 标签添加内容,例如 <slot>默认内容</slot> ,当父组件没有传入插槽内容时,会使用默认内容来显示,默认插槽和具名插槽均支持该功能。

注意事项

有一条规则需要记住:

  • 父组件里的所有内容都是在父级作用域中编译的
  • 子组件里的所有内容都是在子作用域中编译的

CSS 样式与预处理器

Vue 组件的 CSS 样式部分,Vue 3 保留着和 Vue 2 完全一样的写法。

编写组件样式表

最基础的写法,就是在 .vue 文件里添加一个 <style /> 标签,即可在里面写 CSS 代码了。

vue
<template>
+  <div>
+    <!-- HTML 代码 -->
+  </div>
+</template>
+
+<script lang="ts">
+  // TypeScript 代码
+</script>
+
+<style>
+/* CSS 代码 */
+.msg {
+  width: 100%;
+}
+.msg p {
+  color: #333;
+  font-size: 14px;
+}
+</style>

动态绑定 CSS

动态绑定 CSS ,在 Vue 2 就已经存在了,在此之前常用的是 :class:style ,现在在 Vue 3 ,还可以通过 v-bind 来动态修改了。

使用 :class 动态修改样式名

它是绑定在 DOM 元素上面的一个属性,跟 class="class-name" 这样的属性同级别,它非常灵活!

TIP

使用 :class 是用来动态修改样式名,也就意味着必须提前把样式名对应的样式表先写好!

假设已经提前定义好了这几个变量:

vue
<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const activeClass = 'active-class'
+    const activeClass1 = 'active-class1'
+    const activeClass2 = 'active-class2'
+    const isActive = true
+
+    return {
+      activeClass,
+      activeClass1,
+      activeClass2,
+      isActive,
+    }
+  },
+})
+</script>

如果只想绑定一个单独的动态样式,可以传入一个字符串:

vue
<template>
+  <p :class="activeClass">Hello World!</p>
+</template>

如果有多个动态样式,也可以传入一个数组:

vue
<template>
+  <p :class="[activeClass1, activeClass2]">Hello World!</p>
+</template>

还可以对动态样式做一些判断,这个时候传入一个对象:

vue
<template>
+  <p :class="{ 'active-class': isActive }">Hello World!</p>
+</template>

多个判断的情况下,记得也用数组套起来:

vue
<template>
+  <p :class="[{ activeClass1: isActive }, { activeClass2: !isActive }]">
+    Hello World!
+  </p>
+</template>

那么什么情况下会用到 :class 呢?

最常见的场景,应该就是导航、选项卡了,比如要给一个当前选中的选项卡做一个突出高亮的状态,那么就可以使用 :class 来动态绑定一个样式。

vue
<template>
+  <ul class="list">
+    <li
+      class="item"
+      :class="{ cur: index === curIndex }"
+      v-for="(item, index) in 5"
+      :key="index"
+      @click="curIndex = index"
+    >
+      {{ item }}
+    </li>
+  </ul>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const curIndex = ref<number>(0)
+
+    return {
+      curIndex,
+    }
+  },
+})
+</script>
+
+<style scoped>
+.cur {
+  color: red;
+}
+</style>

这样就简单实现了一个点击切换选项卡高亮的功能。

使用 :style 动态修改内联样式

如果觉得使用 :class 需要提前先写样式,再去绑定样式名有点繁琐,有时候只想简简单单的修改几个样式,那么可以通过 :style 来处理。

默认的情况下,都是传入一个对象去绑定:

  • key 是符合 CSS 属性名的 “小驼峰式” 写法,或者套上引号的短横线分隔写法(原写法),例如在 CSS 里,定义字号是 font-size ,那么需要写成 fontSize 或者 'font-size' 作为它的键。

  • value 是 CSS 属性对应的 “合法值”,比如要修改字号大小,可以传入 13px0.4rem 这种带合法单位字符串值,但不可以是 13 这样的缺少单位的值,无效的 CSS 值会被过滤不渲染。

vue
<template>
+  <p
+    :style="{
+      fontSize: '13px',
+      'line-height': 2,
+      color: '#ff0000',
+      textAlign: 'center',
+    }"
+  >
+    Hello World!
+  </p>
+</template>

如果有些特殊场景需要绑定多套 style,需要在 script 先定义好各自的样式变量(也是符合上面说到的那几个要求的对象),然后通过数组来传入:

vue
<template>
+  <p :style="[style1, style2]">Hello World!</p>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const style1 = {
+      fontSize: '13px',
+      'line-height': 2,
+    }
+    const style2 = {
+      color: '#ff0000',
+      textAlign: 'center',
+    }
+
+    return {
+      style1,
+      style2,
+    }
+  },
+})
+</script>

使用 v-bind 动态修改 style ~new

当然,以上两种形式都是关于 <script /><template /> 部分的操作,如果觉得会给模板带来一定的维护成本的话,不妨考虑这个新方案,将变量绑定到 <style /> 部分去。

TIP

请注意这是一个在 3.2.0 版本之后才被归入正式队列的新功能!如果需要使用它,请确保的 vue 的版本号在 3.2.0 以上,最好是保持最新版本。

先来看看基本的用法:

vue
<template>
+  <p class="msg">Hello World!</p>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const fontColor = ref<string>('#ff0000')
+
+    return {
+      fontColor,
+    }
+  },
+})
+</script>
+
+<style scoped>
+.msg {
+  color: v-bind(fontColor);
+}
+</style>

如上面的代码,将渲染出一句红色文本的 Hello World!

这其实是利用了现代浏览器支持的 CSS 变量来实现的一个功能(所以如果打算用它的话,需要提前注意一下兼容性噢,点击查看:CSS Variables 兼容情况 )。

它渲染到 DOM 上,其实也是通过绑定 style 来实现,可以看到渲染出来的样式是:

html
<p class="msg" data-v-7eb2bc79="" style="--7eb2bc79-fontColor:#ff0000;">
+  Hello World!
+</p>

对应的 CSS 变成了:

css
.msg[data-v-7eb2bc79] {
+  color: var(--7eb2bc79-fontColor);
+}

理论上 v-bind 函数可以在 Vue 内部支持任意的 JavaScript 表达式,但由于可能包含在 CSS 标识符中无效的字符,因此官方是建议在大多数情况下,用引号括起来,如:

css
.text {
+  font-size: v-bind('theme.font.size');
+}

由于 CSS 变量的特性,因此对 CSS 响应式属性的更改不会触发模板的重新渲染(这也是和 :class:style 的最大不同)。

TIP

不管有没有开启 <style scoped> ,使用 v-bind 渲染出来的 CSS 变量,都会带上 scoped 的随机 hash 前缀,避免样式污染(永远不会意外泄漏到子组件中),所以请放心使用!

如果对 CSS 变量的使用还不是很了解的话,可以先阅读一下相关的基础知识点。

相关阅读:使用 CSS 自定义属性(变量) - MDN

样式表的组件作用域

CSS 不像 JS ,是没有作用域的概念的,一旦写了某个样式,直接就是全局污染。所以 BEM 命名法 等规范才应运而生。

但在 Vue 组件里,有两种方案可以避免出现这种污染问题:一个是 Vue 2 就有的 <style scoped> ,一个是 Vue 3 新推出的 <style module>

Style Scoped

Vue 组件在设计的时候,就想到了一个很优秀的解决方案,通过 scoped 来支持创建一个 CSS 作用域,使这部分代码只运行在这个组件渲染出来的虚拟 DOM 上。

使用方式很简单,只需要在 <style /> 上添加 scoped 属性:

vue
<!-- 注意这里多了一个 \`scoped\` -->
+<style scoped>
+.msg {
+  width: 100%;
+}
+.msg p {
+  color: #333;
+  font-size: 14px;
+}
+</style>

编译后,虚拟 DOM 都会带有一个 data-v-xxxxx 这样的属性,其中 xxxxx 是一个随机生成的 Hash ,同一个组件的 Hash 是相同并且唯一的:

html
<div class="msg" data-v-7eb2bc79>
+  <p data-v-7eb2bc79>Hello World!</p>
+</div>

而 CSS 则也会带上与 HTML 相同的属性,从而达到样式作用域的目的。

css
.msg[data-v-7eb2bc79] {
+  width: 100%;
+}
+.msg p[data-v-7eb2bc79] {
+  color: #333;
+  font-size: 14px;
+}

使用 scoped 可以有效的避免全局样式污染,可以在不同的组件里面都使用相同的 className,而不必担心会相互覆盖,不必再定义很长很长的样式名来防止冲突了。

TIP

添加 scoped 生成的样式,只作用于当前组件中的元素,并且权重高于全局 CSS ,可以覆盖全局样式

Style Module ~new

这是在 Vue 3 才推出的一个新方案,和 <style scoped> 不同,scoped 是通过给 DOM 元素添加自定义属性的方式来避免冲突,而 <style module> 则更为激进,将会编译成 CSS Modules

对于 CSS Modules 的处理方式,也可以通过一个小例子来更直观的了解它:

css
/* 案例来自阮一峰老师的博文《CSS Modules 用法教程》 */
+/* https://www.ruanyifeng.com/blog/2016/06/css_modules.html */
+
+/* 编译前 */
+.title {
+  color: red;
+}
+
+/* 编译后 */
+._3zyde4l1yATCOkgn-DBWEL {
+  color: red;
+}

可以看出,是通过比较 “暴力” 的方式,把编写的 “好看的” 样式名,直接改写成一个随机 Hash 样式名,来避免样式互相污染。

所以回到 Vue 这边,看看 <style module> 是怎么操作的。

vue
<template>
+  <p :class="$style.msg">Hello World!</p>
+</template>
+
+<style module>
+.msg {
+  color: #ff0000;
+}
+</style>

于是,将渲染出一句红色文本的 Hello World!

TIP

  1. 使用这个方案,需要了解如何 使用 :class 动态修改样式名

  2. 如果单纯只使用 <style module> ,那么在绑定样式的时候,是默认使用 $style 对象来操作的

  3. 必须显示的指定绑定到某个样式,比如 $style.msg ,才能生效

  4. 如果单纯的绑定 $style ,并不能得到 “把全部样式名直接绑定” 的期望结果

  5. 如果指定的 className 是短横杆命名,比如 .user-name ,那么需要通过 $style['user-name'] 去绑定

也可以给 module 进行命名,然后就可以通过命名的 “变量名” 来操作:

vue
<template>
+  <p :class="classes.msg">Hello World!</p>
+</template>
+
+<style module="classes">
+.msg {
+  color: #ff0000;
+}
+</style>

TIP

需要注意的一点是,一旦开启 <style module> ,那么在 <style module> 里所编写的样式都必须手动绑定才能生效,没有被绑定的样式虽然也会被编译,但不会主动生效到 DOM 上。

原因是编译出来的样式名已经变化,而原来的 DOM 未指定对应的样式名,或者指定的是编译前的命名,所以并不能匹配到正确的样式。

useCssModule ~new

这是一个全新的 API ,面向在 script 部分操作 CSS Modules 。

在上面的 CSS Modules 部分可以知道,可以在 style 定义好样式,然后在 template 部分通过变量名来绑定样式。

那么如果有一天有个需求,需要通过 v-html 来渲染 HTML 代码,那这里的样式岂不是凉凉了?当然不会!

Vue 3 提供了一个 Composition API useCssModule 来帮助在 setup 函数里操作的 CSS Modules (对,只能在 setup 或者 script setup 里使用)。

基本用法:

绑定多几个样式,再来操作:

vue
<template>
+  <p :class="$style.msg">
+    <span :class="$style.text">Hello World!</span>
+  </p>
+</template>
+
+<script lang="ts">
+import { defineComponent, useCssModule } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const style = useCssModule()
+    console.log(style)
+  },
+})
+</script>
+
+<style module>
+.msg {
+  color: #ff0000;
+}
+.text {
+  font-size: 14px;
+}
+</style>

可以看到打印出来的 style 是一个对象:

  • key 是在 <style modules> 里定义的原始样式名

  • value 则是编译后的新样式名

js
{
+  msg: 'home_msg_37Xmr',
+  text: 'home_text_2woQJ'
+}

所以来配合 模板字符串 的使用,看看刚刚说的,要通过 v-html 渲染出来的内容应该如何绑定样式:

vue
<template>
+  <div v-html="content"></div>
+</template>
+
+<script lang="ts">
+import { defineComponent, useCssModule } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 获取样式
+    const style = useCssModule()
+
+    // 编写模板内容
+    const content = \`<p class="\${style.msg}">
+      <span class="\${style.text}">Hello World! —— from v-html</span>
+    </p>\`
+
+    return {
+      content,
+    }
+  },
+})
+</script>
+
+<style module>
+.msg {
+  color: #ff0000;
+}
+.text {
+  font-size: 14px;
+}
+</style>

是不是也非常简单?可能刚开始不太习惯,但写多几次其实也蛮好玩的这个功能!

另外,需要注意的是,如果是指定了 modules 的名称,那么必须传入对应的名称作为入参才可以正确拿到这些样式:

比如指定了一个 classes 作为名称:

vue
<style module="classes">
+/* ... */
+</style>

那么需要通过传入 classes 这个名称才能拿到样式,否则会是一个空对象:

ts
const style = useCssModule('classes')

TIP

const style = useCssModule() 的时候,命名是随意的,跟在 <style module="classes"> 这里指定的命名没有关系。

深度操作符 ~new

样式表的组件作用域 部分了解到,使用 scoped 后,父组件的样式将不会渗透到子组件中,但也不能直接修改子组件的样式。

如果确实需要进行修改子组件的样式,必须通过 ::v-deep(完整写法) 或者 :deep(快捷写法) 操作符来实现。

TIP

  1. 旧版的深度操作符是 >>>/deep/::v-deep,现在 >>>/deep/ 已进入弃用阶段(虽然暂时还没完全移除)

  2. 同时需要注意的是,旧版 ::v-deep 的写法是作为组合器的方式,写在样式或者元素前面,如:::v-deep .class-name { /* ... */ },现在这种写法也废弃了。

现在不论是 ::v-deep 还是 :deep ,使用方法非常统一,来假设 .b 是子组件的样式名:

vue
<style scoped>
+.a :deep(.b) {
+  /* ... */
+}
+</style>

编译后:

css
.a[data-v-f3f3eg9] .b {
+  /* ... */
+}

TIP

可以看到,新的 deep 写法是作为一个类似 JS “函数” 那样去使用,需要深度操作的样式或者元素名,作为 “入参” 去传入。

同理,如果使用 Less 或者 Stylus 这种支持嵌套写法的预处理器,也是可以这样去深度操作的:

less
.a {
+  :deep(.b) {
+    /* ... */
+  }
+}

另外,除了操作子组件的样式,那些通过 v-html 创建的 DOM 内容,也不受作用域内的样式影响,也可以通过深度操作符来实现样式修改。

使用 CSS 预处理器

在工程化的现在,可以说前端都几乎不写 CSS 了,都是通过 sasslessstylus 等 CSS 预处理器来完成样式的编写。

为什么要用 CSS 预处理器?放一篇关于三大预处理器的点评,新开发者可以做个简单了解,具体的用法在对应的官网上有非常详细的说明。

可以查看了解:浅谈 CSS 预处理器,Sass、Less 和 Stylus

在 Vue 组件里使用预处理器非常简单,像 Vite 已内置了对预处理器文件的支持(可处理 .less.scss 之类的预处理器扩展名文件),因此只需要安装对应的依赖到项目里。

这里以 Less 为例,先安装该预处理器:

bash
# 因为是在开发阶段使用,所以添加到 \`devDependencies\`
+npm i -D less

接下来在 Vue 组件里,只需要在 <style /> 标签上,通过 lang="less" 属性指定使用哪个预处理器,即可直接编写对应的代码:

vue
<style lang="less" scoped>
+// 定义颜色变量
+@color-black: #333;
+@color-red: #ff0000;
+
+// 父级标签
+.msg {
+  width: 100%;
+  // 其子标签可以使用嵌套写法
+  p {
+    color: @color-black;
+    font-size: 14px;
+    // 支持多级嵌套
+    span {
+      color: @color-red;
+    }
+  }
+}
+</style>

编译后的 css 代码:

css
.msg {
+  width: 100%;
+}
+.msg p {
+  color: #333333;
+  font-size: 14px;
+}
+.msg p span {
+  color: #ff0000;
+}

预处理器也支持 scoped,用法请查阅 样式表的组件作用域 部分。

`,729);function C(i,A,d,u,g,h){const l=n("ReadingTips"),p=n("GitalkComment"),o=n("ClientOnly");return t(),c("div",null,[F,s(l),D,s(o,null,{default:r(()=>[s(p,{issueId:46})]),_:1})])}const E=e(y,[["render",C]]);export{m as __pageData,E as default}; diff --git a/assets/component.md.b59f8845.lean.js b/assets/component.md.b59f8845.lean.js new file mode 100644 index 00000000..2d260347 --- /dev/null +++ b/assets/component.md.b59f8845.lean.js @@ -0,0 +1,1949 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as e,v as t,b as c,t as s,O as r,R as a,M as n}from"./chunks/framework.0d8bea05.js";const m=JSON.parse('{"title":"单组件的编写","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"component.md","filePath":"component.md"}'),y={name:"component.md"},F=a('

单组件的编写

项目搭好了,第一个需要了解的是 Vue 组件的变化,由于这部分篇幅会非常大,所以会分成很多个小节,一部分一部分按照开发顺序来逐步了解。

因为 Vue 3 对 TypeScript 的支持真的是太完善了,并且 TypeScript 的发展趋势和市场需求度越来越高,所以接下来都将直接使用 TypeScript 进行编程。

TIP

对 TypeScript 不太熟悉的开发者,建议先阅读 快速上手 TypeScript 一章,有了一定的语言基础之后,再一边写代码一边加深印象。

',4),D=a(`

全新的 setup 函数 ~new

在开始编写 Vue 组件之前,需要了解两个全新的前置知识点:

  • 全新的 setup 函数,关系到组件的生命周期和渲染等问题
  • 写 TypeScript 组件离不开的 defineComponent API

setup 的含义

Vue 3 的 Composition API 系列里,推出了一个全新的 setup 函数,它是一个组件选项,在创建组件之前执行,一旦 props 被解析,并作为组合式 API 的入口点。

TIP

说的通俗一点,就是在使用 Vue 3 生命周期的情况下,整个组件相关的业务代码,都可以放在 setup 里执行。

因为在 setup 之后,其他的生命周期才会被启用(点击了解:组件的生命周期)。

基本语法:

ts
// 这是一个基于 TypeScript 的 Vue 组件
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup(props, context) {
+    // 在这里声明数据,或者编写函数并在这里执行它
+
+    return {
+      // 需要给 \`<template />\` 用的数据或函数,在这里 \`return\` 出去
+    }
+  },
+})

可以发现在这段代码里还导入了一个 defineComponent API ,也是 Vue 3 带来的新功能,下文的 defineComponent 的作用 将介绍其用法。

在使用 setup 的情况下,请牢记一点:不能再用 this 来获取 Vue 实例,也就是无法和 Vue 2 一样,通过 this.foothis.bar() 这样来获取实例上的数据,或者执行实例上的方法。

关于全新的 Vue 3 组件编写,笔者将在下文一步步说明。

setup 的参数使用

setup 函数包含了两个入参:

参数类型含义是否必传
propsobject由父组件传递下来的数据
contextobject组件的执行上下文

第一个参数 props

它是响应式的,当父组件传入新的数据时,它将被更新。

TIP

请不要解构它,这样会让数据失去响应性,一旦父组件发生数据变化,解构后的变量将无法同步更新为最新的值。

可以使用 Vue 3 全新的响应式 API toRef / toRefs 进行响应式数据转换,下文将会介绍全新的响应式 API 的用法。

第二个参数 context

context 只是一个普通的对象,它暴露三个组件的 Property :

属性类型作用
attrs非响应式对象未在 Props 里定义的属性都将变成 Attrs
slots非响应式对象组件插槽,用于接收父组件传递进来的模板内容
emit方法触发父组件绑定下来的事件

因为 context 只是一个普通对象,所以可以直接使用 ES6 解构。

平时使用可以通过直接传入 { emit } ,即可用 emit('xxx') 来代替使用 context.emit('xxx'),另外两个功能也是如此。

但是 attrsslots 请保持 attrs.xxxslots.xxx 的方式来使用其数据,不要进行解构,虽然这两个属性不是响应式对象,但对应的数据会随组件本身的更新而更新。

两个参数的具体使用,可查阅 组件之间的通信 一章详细了解。

defineComponent 的作用

defineComponent 是 Vue 3 推出的一个全新 API ,可用于对 TypeScript 代码的类型推导,帮助开发者简化掉很多编码过程中的类型声明。

比如,原本需要这样才可以使用 setup 函数:

ts
import { Slots } from 'vue'
+
+// 声明 \`props\` 和 \`return\` 的数据类型
+interface Data {
+  [key: string]: unknown
+}
+
+// 声明 \`context\` 的类型
+interface SetupContext {
+  attrs: Data
+  slots: Slots
+  emit: (event: string, ...args: unknown[]) => void
+}
+
+// 使用的时候入参要加上声明, \`return\` 也要加上声明
+export default {
+  setup(props: Data, context: SetupContext): Data {
+    // ...
+
+    return {
+      // ...
+    }
+  },
+}

每个组件都这样进行类型声明,会非常繁琐,如果使用了 defineComponent ,就可以省略这些类型声明:

ts
import { defineComponent } from 'vue'
+
+// 使用 \`defineComponent\` 包裹组件的内部逻辑
+export default defineComponent({
+  setup(props, context) {
+    // ...
+
+    return {
+      // ...
+    }
+  },
+})

代码量瞬间大幅度减少,只要是 Vue 本身的 API , defineComponent 都可以自动推导其类型,这样开发者在编写组件的过程中,只需要维护自己定义的数据类型就可以了,可专注于业务。

组件的生命周期 ~new

在了解了 Vue 3 组件的两个前置知识点后,不着急写组件,还需要先了解组件的生命周期,这个知识点非常重要,只有理解并记住组件的生命周期,才能够灵活的把控好每一处代码的执行,使程序的运行结果可以达到预期。

升级变化

从 Vue 2 升级到 Vue 3 ,在保留对 Vue 2 的生命周期支持的同时,Vue 3 也带来了一定的调整。

Vue 2 的生命周期写法名称是 Options API (选项式 API ), Vue 3 新的生命周期写法名称是 Composition API (组合式 API )。

Vue 3 组件默认支持 Options API ,而 Vue 2 可以通过 @vue/composition-api 插件获得 Composition API 的功能支持(其中 Vue 2.7 版本内置了该插件, 2.6 及以下的版本需要单独安装)。

为了减少理解成本,笔者将从读者的使用习惯上,使用 “ Vue 2 的生命周期” 代指 Options API 写法,用 “ Vue 3 的生命周期” 代指 Composition API 写法。

关于 Vue 生命周期的变化,可以从下表直观地了解:

Vue 2 生命周期Vue 3 生命周期执行时间说明
beforeCreatesetup组件创建前执行
createdsetup组件创建后执行
beforeMountonBeforeMount组件挂载到节点上之前执行
mountedonMounted组件挂载完成后执行
beforeUpdateonBeforeUpdate组件更新之前执行
updatedonUpdated组件更新完成之后执行
beforeDestroyonBeforeUnmount组件卸载之前执行
destroyedonUnmounted组件卸载完成后执行
errorCapturedonErrorCaptured当捕获一个来自子孙组件的异常时激活钩子函数

可以看到 Vue 2 生命周期里的 beforeCreatecreated ,在 Vue 3 里已被 setup 替代。

熟悉 Vue 2 的开发者应该都知道 Vue 有一个全局组件 <KeepAlive /> ,用于在多个组件间动态切换时缓存被移除的组件实例,当组件被包含在 <KeepAlive /> 组件里时,会多出两个生命周期钩子函数:

Vue 2 生命周期Vue 3 生命周期执行时间说明
activatedonActivated被激活时执行
deactivatedonDeactivated切换组件后,原组件消失前执行

TIP

虽然 Vue 3 依然支持 Vue 2 的生命周期,但是不建议混搭使用,前期可以继续使用 Vue 2 的生命周期作为过度阶段慢慢适应,但还是建议尽快熟悉并完全使用 Vue 3 的生命周期编写组件。

使用 3.x 的生命周期

在 Vue 3 的 Composition API 写法里,每个生命周期函数都要先导入才可以使用,并且所有生命周期函数统一放在 setup 里运行。

如果需要达到 Vue 2 的 beforeCreatecreated 生命周期的执行时机,直接在 setup 里执行函数即可。

以下是几个生命周期的执行顺序对比:

ts
import { defineComponent, onBeforeMount, onMounted } from 'vue'
+
+export default defineComponent({
+  setup() {
+    console.log(1)
+
+    onBeforeMount(() => {
+      console.log(2)
+    })
+
+    onMounted(() => {
+      console.log(3)
+    })
+
+    console.log(4)
+  },
+})

最终将按照生命周期的顺序输出:

js
// 1
+// 4
+// 2
+// 3

组件的基本写法

如果想在 Vue 2 里使用 TypeScript 编写组件,需要通过 Options API 的 Vue.extend 语法,或者是另外一种风格 Class Component 的语法声明组件,其中为了更好的进行类型推导, Class Component 语法更受开发者欢迎。

但是 Class Component 语法和默认的组件语法相差较大,带来了一定的学习成本,对于平时编写 JavaScript 代码很少使用 Class 的开发者,适应时间应该也会比较长。

因此 Vue 3 在保留对 Class Component 支持的同时,推出了全新的 Function-based Component ,更贴合 JavaScript 的函数式编程风格,这也是接下来要讲解并贯穿全文使用的 Composition API 新写法。

Composition API 虽然也是一个步伐迈得比较大的改动,但其组件结构并没有特别大的变化,区别比较大的地方在于组件生命周期和响应式 API 的使用,只要掌握了这些核心功能,上手 Vue 3 非常容易!

看到这里可能有开发者心里在想:

“这几种组件写法,加上视图部分又有 Template 和 TSX 的写法之分,生命周期方面 Vue 3 对 Vue 2 的写法又保持了兼容,在 Vue 里写 TypeScript 的组合方式一只手数不过来,在入门时选择合适的编程风格就遇到了困难,可怎么办?”

不用担心!笔者将九种常见的组合方式以表格的形式进行对比, Vue 3 组件最好的写法一目了然!

回顾 Vue 2

在 Vue 2 ,常用以下三种写法声明 TypeScript 组件:

适用版本基本写法视图写法
Vue 2Vue.extendTemplate
Vue 2Class ComponentTemplate
Vue 2Class ComponentTSX

其中最接近 Options API 的写法是使用 Vue.extend API 声明组件:

ts
// 这是一段摘选自 Vue 2 官网的代码演示
+import Vue from 'vue'
+
+// 推荐使用 Vue.extend 声明组件
+const Component = Vue.extend({
+  // 类型推断已启用
+})
+
+// 不推荐这种方式声明
+const Component = {
+  // 这里不会有类型推断,
+  // 因为 TypeScript 不能确认这是 Vue 组件的选项
+}

而为了更好地获得 TypeScript 类型推导支持,通常使用 Class Component 的写法,这是 Vue 官方推出的一个装饰器插件(需要单独安装):

ts
// 这是一段摘选自 Vue 2 官网的代码演示
+import Vue from 'vue'
+import Component from 'vue-class-component'
+
+// @Component 修饰符注明了此类为一个 Vue 组件
+@Component({
+  // 所有的组件选项都可以放在这里
+  template: '<button @click="onClick">Click!</button>',
+})
+
+// 使用 Class 声明一个组件
+export default class MyComponent extends Vue {
+  // 初始数据可以直接声明为实例的 property
+  message: string = 'Hello!'
+
+  // 组件方法也可以直接声明为实例的方法
+  onClick(): void {
+    window.alert(this.message)
+  }
+}

可在 Vue 2 官网的 TypeScript 支持 一章了解更多配置说明。

了解 Vue 3 ~new

Vue 3 从设计初期就考虑了 TypeScript 的支持,其中 defineComponent 这个 API 就是为了解决 Vue 2 对 TypeScript 类型推导不完善等问题而推出的。

在 Vue 3 ,至少有以下六种写法可以声明 TypeScript 组件:

适用版本基本写法视图写法生命周期版本官方是否推荐
Vue 3Class ComponentTemplateVue 2×
Vue 3defineComponentTemplateVue 2×
Vue 3defineComponentTemplateVue 3
Vue 3Class ComponentTSXVue 2×
Vue 3defineComponentTSXVue 2×
Vue 3defineComponentTSXVue 3

其中 defineComponent + Composition API + Template 的组合是 Vue 官方最为推荐的组件声明方式,本书接下来的内容都会以这种写法作为示范案例,也推荐开发者在学习的过程中,使用该组合进行入门。

下面看看如何使用 Composition API 编写一个最简单的 Hello World 组件:

vue
<!-- Template 代码和 Vue 2 一样 -->
+<template>
+  <p class="msg">{{ msg }}</p>
+</template>
+
+<!-- Script 代码需要使用 Vue 3 的新写法-->
+<script lang="ts">
+// Vue 3 的 API 需要导入才能使用
+import { defineComponent } from 'vue'
+
+// 使用 \`defineComponent\` 包裹组件代码
+// 即可获得完善的 TypeScript 类型推导支持
+export default defineComponent({
+  setup() {
+    // 在 \`setup\` 方法里声明变量
+    const msg = 'Hello World!'
+
+    // 将需要在 \`<template />\` 里使用的变量 \`return\` 出去
+    return {
+      msg,
+    }
+  },
+})
+</script>
+
+<!-- CSS 代码和 Vue 2 一样 -->
+<style scoped>
+.msg {
+  font-size: 14px;
+}
+</style>

可以看到 Vue 3 的组件也是 <template /> + <script /> + <style /> 的三段式组合,上手非常简单。

其中 Template 沿用了 Vue 2 时期类似 HTML 风格的模板写法, Style 则是使用原生 CSS 语法或者 Less 等 CSS 预处理器编写。

但需要注意的是,在 Vue 3 的 Composition API 写法里,数据或函数如果需要在 <template /> 中使用,就必须在 setup 里将其 return 出去,而仅在 <script /> 里被调用的函数或变量,不需要渲染到模板则无需 return

响应式数据的变化 ~new

响应式数据是 MVVM 数据驱动编程的特色, Vue 的设计也是受 MVVM 模型的启发,相信大部分开发者选择 MVVM 框架都是因为数据驱动编程比传统的事件驱动编程要来得方便,而选择 Vue ,则是方便中的方便。

TIP

Model-View-ViewModel (简称 MVVM ) 是一种软件架构模式,将视图 UI 和业务逻辑分开,通过对逻辑数据的修改即可驱动视图 UI 的更新,因此常将这种编程方式称为 “数据驱动” ,与之对应的需要操作 DOM 才能完成视图更新的编程方式则称为 “事件驱动” 。

设计上的变化

作为最重要的一个亮点, Vue 3 的响应式数据在设计上和 Vue 2 有着很大的不同。

回顾 Vue 2

Vue 2 是使用了 Object.defineProperty API 的 getter/setter 来实现数据的响应性,这个方法的具体用法可以参考 MDN 的文档: Object.defineProperty - MDN

下面使用 Object.defineProperty 实现一个简单的双向绑定 demo ,亲自敲代码试一下可以有更多的理解:

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>DefineProperty Demo</title>
+  </head>
+  <body>
+    <!-- 输入框和按钮 -->
+    <div>
+      <input type="text" id="input" />
+      <button onclick="vm.text = 'Hello World'">设置为 Hello World</button>
+    </div>
+    <!-- 输入框和按钮 -->
+
+    <!-- 文本展示 -->
+    <div id="output"></div>
+    <!-- 文本展示 -->
+
+    <script>
+      // 声明一个响应式数据
+      const vm = {}
+      Object.defineProperty(vm, 'text', {
+        set(value) {
+          document.querySelector('#input').value = value
+          document.querySelector('#output').innerText = value
+        },
+      })
+
+      // 处理输入行为
+      document.querySelector('#input').oninput = function (e) {
+        vm.text = e.target.value
+      }
+    </script>
+  </body>
+</html>

这个小 demo 实现了这两个功能:

  1. 输入框的输入行为只修改 vm.text 的数据,但会同时更新 output 标签的文本内容
  2. 点击按钮修改 vm.text 的数据,也会触发输入框和 output 文本的更新

当然 Vue 做了非常多的工作,而非只是简单的调用了 Object.defineProperty ,可以在官网 深入 Vue 2 的响应式原理 一章了解更多 Vue 2 的响应式原理。

了解 Vue 3

Vue 3 是使用了 Proxy API 的 getter/setter 来实现数据的响应性,这个方法的具体用法可以参考 MDN 的文档: Proxy - MDN

同样的,也来实现一个简单的双向绑定 demo ,这次使用 Proxy 来实现:

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Proxy Demo</title>
+  </head>
+  <body>
+    <!-- 输入框和按钮 -->
+    <div>
+      <input type="text" id="input" />
+      <button onclick="vm.text = 'Hello World'">设置为 Hello World</button>
+    </div>
+    <!-- 输入框和按钮 -->
+
+    <!-- 文本展示 -->
+    <div id="output"></div>
+    <!-- 文本展示 -->
+
+    <script>
+      // 声明一个响应式数据
+      const vm = new Proxy(
+        {},
+        {
+          set(obj, key, value) {
+            document.querySelector('#input').value = value
+            document.querySelector('#output').innerText = value
+          },
+        }
+      )
+
+      // 处理输入行为
+      document.querySelector('#input').oninput = function (e) {
+        vm.text = e.target.value
+      }
+    </script>
+  </body>
+</html>

这个 demo 实现的功能和使用 Object.defineProperty 的 demo 是完全一样的,也都是基于 setter 的行为完成数据更新的实现,那么为什么 Vue 3 要舍弃 Object.defineProperty ,换成 Proxy 呢?

主要原因在于 Object.defineProperty 有以下的不足:

  1. 无法侦听数组下标的变化,通过 arr[i] = newValue 这样的操作无法实时响应
  2. 无法侦听数组长度的变化,例如通过 arr.length = 10 去修改数组长度,无法响应
  3. 只能侦听对象的属性,对于整个对象需要遍历,特别是多级对象更是要通过嵌套来深度侦听
  4. 使用 Object.assign() 等方法给对象添加新属性时,也不会触发更新
  5. 更多细节上的问题 …

这也是为什么 Vue 2 要提供一个 Vue.set API 的原因,可以在官网 Vue 2 中检测变化的注意事项 一章了解更多说明。

而这些问题在 Proxy 都可以得到解决,可以在官网 深入 Vue 3 的响应式原理 一章了解更多这部分的内容。

用法上的变化

本书只使用 Composition API 编写组件,这是使用 Vue 3 的最大优势。

TIP

虽然官方文档在各个 API 的使用上都做了一定的举例,但在实际使用过程中可能会遇到一些问题,常见的情况就是有些数据用着用着就失去了响应,或者是在 TypeScript 里出现类型不匹配的报错等等。

当然,一般遇到这种情况并不一定是框架的 BUG ,而可能是使用方式不对,本章节将结合笔者最初入门 Vue 3 时遇到的问题和解决问题的心得,复盘这些响应式 API 的使用。

相对于 Vue 2 在 data 里声明后即可通过 this.xxx 调用响应式数据,在 Vue 3 的生命周期里没有了 Vue 实例的 this 指向,需要导入 refreactive 等响应式 API 才能声明并使用响应式数据。

ts
// 这里导入的 \`ref\` 是一个响应式 API
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 通过响应式 API 创建的变量具备了响应性
+    const msg = ref<string>('Hello World!')
+  },
+})

由于 Vue 3 新的 API 非常多,但有些 API 的使用场景却不多,因此本书当前只对常用的部分 API 的使用和常见问题进行说明,更多的 API 可以在官方文档的 响应性 API 一章查阅。

响应式 API 之 ref ~new

ref 是最常用的一个响应式 API,它可以用来定义所有类型的数据,包括 Node 节点和组件。

没错,在 Vue 2 常用的 this.$refs.xxx 来取代 document.querySelector('.xxx') 获取 Node 节点的方式,也是使用这个 API 来取代。

类型声明

在开始使用 API 之前,需要先了解在 TypeScript 中如何声明 Ref 变量的类型。

API 本身的类型

先看 API 本身, ref API 是一个函数,通过接受一个泛型入参,返回一个响应式对象,所有的值都通过 .value 属性获取,这是 API 本身的 TS 类型:

ts
// \`ref\` API 的 TS 类型
+function ref<T>(value: T): Ref<UnwrapRef<T>>
+
+// \`ref\` API 的返回值的 TS 类型
+interface Ref<T> {
+  value: T
+}

因此在声明变量时,是使用尖括号 <> 包裹其 TS 类型,紧跟在 ref API 之后:

ts
// 显式指定 \`msg.value\` 是 \`string\` 类型
+const msg = ref<string>('Hello World!')

再回看该 API 本身的类型,其中使用了 T 泛型,这表示在传入函数的入参时,可以不需要手动指定其 TS 类型, TypeScript 会根据这个 API 所返回的响应式对象的 .value 属性的类型,确定当前变量的类型。

因此也可以省略显式的类型指定,像下面这样声明变量,其类型交给 TypeScript 去自动推导:

ts
// TypeScript 会推导 \`msg.value\` 是 \`string\` 类型
+const msg = ref('Hello World')

对于声明时会赋予初始值,并且在使用过程中不会改变其类型的变量,是可以省略类型的显式指定的。

而如果有显式的指定的类型,那么在一些特殊情况下,初始化时可以不必赋值,这样 TypeScript 会自动添加 undefined 类型:

ts
const msg = ref<string>()
+console.log(msg.value) // undefined
+
+msg.value = 'Hello World!'
+console.log(msg.value) // Hello World!

因为入参留空时,虽然指定了 string 类型,但实际上此时的值是 undefined ,因此实际上这个时候的 msg.value 是一个 string | undefined 的联合类型。

对于声明时不知道是什么值,在某种条件下才进行初始化的情况,就可以省略其初始值,但是切记在调用该变量的时候对 .value 值进行有效性判断。

而如果既不显式指定类型,也不赋予初始值,那么会被默认为 any 类型,除非真的无法确认类型,否则不建议这么做。

API 返回值的类型

细心的开发者还会留意到 ref API 类型里面还标注了一个返回值的 TS 类型:

ts
interface Ref<T> {
+  value: T
+}

它是代表整个 Ref 变量的完整类型:

  • 上文声明 Ref 变量时,提到的 string 类型都是指 msg.value 这个 .value 属性的类型
  • msg 这个响应式变量,其本身是 Ref<string> 类型

如果在开发过程中需要在函数里返回一个 Ref 变量,那么其 TypeScript 类型就可以这样写(请留意 Calculator 里的 num 变量的类型):

ts
// 导入 \`ref\` API
+import { ref } from 'vue'
+// 导入 \`ref\` API 的返回值类型
+import type { Ref } from 'vue'
+
+// 声明 \`useCalculator\` 函数的返回值类型
+interface Calculator {
+  // 这里包含了一个 Ref 变量
+  num: Ref<number>
+  add: () => void
+}
+
+// 声明一个 “使用计算器” 的函数
+function useCalculator(): Calculator {
+  const num = ref<number>(0)
+
+  function add() {
+    num.value++
+  }
+
+  return {
+    num,
+    add,
+  }
+}
+
+// 在执行使用计算器函数时,可以获取到一个 Ref 变量和其他方法
+const { num, add } = useCalculator()
+add()
+console.log(num.value) // 1

上面这个简单的例子演示了如何手动指定 Ref 变量的类型,对于逻辑复用时的函数代码抽离、插件开发等场景非常有用!当然大部分情况下可以交给 TypeScript 自动推导,但掌握其用法,在必要的时候就派得上用场了!

变量的定义

在了解了如何对 Ref 变量进行类型声明之后,面对不同的数据类型,相信都得心应手了!但不同类型的值之间还是有少许差异和注意事项,例如上文提及到该 API 可以用来定义所有类型的数据,包括 Node 节点和组件,具体可以参考下文的示例。

基本类型

对字符串、布尔值等基本类型的定义方式,比较简单:

ts
// 字符串
+const msg = ref<string>('Hello World!')
+
+// 数值
+const count = ref<number>(1)
+
+// 布尔值
+const isVip = ref<boolean>(false)

引用类型

对于对象、数组等引用类型也适用,比如要定义一个对象:

ts
// 先声明对象的格式
+interface Member {
+  id: number
+  name: string
+}
+
+// 在定义对象时指定该类型
+const userInfo = ref<Member>({
+  id: 1,
+  name: 'Tom',
+})

定义一个普通数组:

ts
// 数值数组
+const uids = ref<number[]>([1, 2, 3])
+
+// 字符串数组
+const names = ref<string[]>(['Tom', 'Petter', 'Andy'])

定义一个对象数组:

ts
// 声明对象的格式
+interface Member {
+  id: number
+  name: string
+}
+
+// 定义一个对象数组
+const memberList = ref<Member[]>([
+  {
+    id: 1,
+    name: 'Tom',
+  },
+  {
+    id: 2,
+    name: 'Petter',
+  },
+])

DOM 元素与子组件

除了可以定义数据,ref 也有熟悉的用途,就是用来挂载节点,也可以挂在子组件上,也就是对应在 Vue 2 时常用的 this.$refs.xxx 获取 DOM 元素信息的作用。

模板部分依然是熟悉的用法,在要引用的 DOM 上添加一个 ref 属性:

vue
<template>
+  <!-- 给 DOM 元素添加 \`ref\` 属性 -->
+  <p ref="msg">请留意该节点,有一个 ref 属性</p>
+
+  <!-- 子组件也是同样的方式添加 -->
+  <Child ref="child" />
+</template>

<script /> 部分有三个最基本的注意事项:

  1. <template /> 代码里添加的 ref 属性的值,是对应 <script /> 里使用 ref API 声明的变量的名称;

  2. 请保证视图渲染完毕后再执行 DOM 或组件的相关操作(需要放到生命周期的 onMounted 或者 nextTick 函数里,这一点在 Vue 2 也是一样);

  3. 该 Ref 变量必须 return 出去才可以给到 <template /> 使用,这一点是 Vue 3 生命周期的硬性要求,子组件的数据和方法如果要给父组件操作,也要 return 出来才可以。

配合上面的 <template /> ,来看看 <script /> 部分的具体例子:

ts
import { defineComponent, onMounted, ref } from 'vue'
+import Child from '@cp/Child.vue'
+
+export default defineComponent({
+  components: {
+    Child,
+  },
+  setup() {
+    // 定义挂载节点,声明的类型详见下方附表
+    const msg = ref<HTMLElement>()
+    const child = ref<InstanceType<typeof Child>>()
+
+    // 请保证视图渲染完毕后再执行节点操作 e.g. \`onMounted\` / \`nextTick\`
+    onMounted(() => {
+      // 比如获取 DOM 的文本
+      console.log(msg.value.innerText)
+
+      // 或者操作子组件里的数据
+      child.value.isShowDialog = true
+    })
+
+    // 必须 \`return\` 出去才可以给到 \`<template />\` 使用
+    return {
+      msg,
+      child,
+    }
+  },
+})

关于 DOM 和子组件的 TS 类型声明,可参考以下规则:

节点类型声明类型参考文档
DOM 元素使用 HTML 元素接口HTML 元素接口
子组件使用 InstanceType 配合 typeof 获取子组件的类型typeof 操作符

TIP

单纯使用 typeof Child 虽然可以获得 Child.vue 组件的 Props 和方法等提示,但在 VSCode 的类型推导还不够智能,缺乏更有效的代码补全支持。

上文使用的 InstanceType<T> 是 TypeScript 提供的一个工具类型,可以获取构造函数类型的实例类型,因此将组件的类型声明为 InstanceType<typeof Child> ,不仅可以得到更完善的类型提示,在编程过程中还可以让编辑器提供更完善的代码补全功能。

另外,关于这一小节,有一个可能会引起 TS 编译报错的情况是,一些脚手架创建出来的项目会默认启用 --strictNullChecks 选项,会导致案例中的代码无法正常编译,出现如下报错:

bash
 npm run build
+
+> hello-vue3@0.0.0 build
+> vue-tsc --noEmit && vite build
+
+src/views/home.vue:27:7 - error TS2532: Object is possibly 'undefined'.
+
+27       child.value.isShowDialog = true
+         ~~~~~~~~~~~
+
+
+Found 1 error in src/views/home.vue:27

这是因为在默认情况下 nullundefined 是所有类型的子类型,但开启了 strictNullChecks 选项之后,会使 nullundefined 只能赋值给 void 和它们各自,这是一个更为严谨的选项,可以保障程序代码的健壮性,但对于刚接触 TypeScript 不久的开发者可能不太友好。

有以下几种解决方案可以参考:

  1. 在涉及到相关操作的时候,对节点变量增加一个判断:
ts
// 添加 \`if\` 分支,判断 \`.value\` 存在时才执行相关代码
+if (child.value) {
+  // 读取子组件的数据
+  console.log(child.value.num)
+
+  // 执行子组件的方法
+  child.value.sayHi('Use \`if\` in \`onMounted\` API.')
+}
  1. 通过 TS 的可选符 ? 将目标设置为可选,避免出现错误(这个方式不能直接修改子组件数据的值):
ts
// 读取子组件的数据(留意 \`.num\` 前面有一个 \`?\` 问号)
+console.log(child.value?.num)
+
+// 执行子组件的方法(留意 \`.sayHi\` 前面有一个 \`?\` 问号)
+child.value?.sayHi('use ? in onMounted')
  1. 在项目根目录下的 tsconfig.json 文件里,显式的关闭 strictNullChecks 选项,关闭后,需要开发者在写代码的时候,自行把控好是否需要对 nullundefined 进行判断:
json
{
+  "compilerOptions": {
+    // ...
+    "strictNullChecks": false
+  }
+  // ...
+}
  1. 使用 any 类型代替,但是写 TypeScript 还是尽量不要使用 any ,满屏的 AnyScript 不如直接使用 JavaScript

变量的读取与赋值

前面在介绍 API 类型的时候已经了解,通过 ref 声明的变量会全部变成对象,不管定义的是什么类型的值,都会转化为一个 Ref 对象,其中 Ref 对象具有指向内部值的单个 Property .value

也就是说,任何 Ref 对象的值都必须通过 xxx.value 才可以正确获取。

请牢记上面这句话,初拥 Vue 3 的开发者很多 BUG 都是由于这个问题引起的(包括笔者刚开始使用 Vue 3 的那段时间,嘿嘿)。

读取变量

平时对于普通变量的值,读取的时候都是直接调用其变量名即可:

ts
// 读取一个字符串
+const msg: string = 'Hello World!'
+console.log(msg)
+
+// 读取一个数组
+const uids: number[] = [1, 2, 3]
+console.log(uids[1])

而 Ref 对象的值的读取,切记!必须通过 .value

ts
// 读取一个字符串
+const msg = ref<string>('Hello World!')
+console.log(msg.value)
+
+// 读取一个数组
+const uids = ref<number[]>([1, 2, 3])
+console.log(uids.value[1])

为变量赋值

普通变量需要使用 let 声明才可以修改其值,由于 Ref 对象是个引用类型,所以可以使用 const 声明,直接通过 .value 修改。

ts
// 声明一个字符串变量
+const msg = ref<string>('Hi!')
+
+// 等待 1s 后修改它的值
+setTimeout(() => {
+  msg.value = 'Hello!'
+}, 1000)

因此日常业务中,像在对接服务端 API 的接口数据时,可以自由的使用 forEachmapfilter 等方法操作 Ref 数组,或者直接重置它,而不必担心数据失去响应性。

ts
const data = ref<string[]>([])
+
+// 提取接口的数据
+data.value = api.data.map((item: any) => item.text)
+
+// 重置数组
+data.value = []

为什么突然要说这个呢?因为涉及到下一部分的知识,关于 reactive API 在使用上的注意事项。

响应式 API 之 reactive ~new

reactive 是继 ref 之后最常用的一个响应式 API 了,相对于 ref ,它的局限性在于只适合对象、数组。

TIP

使用 reactive 的好处就是写法跟平时的对象、数组几乎一模一样,但它也带来了一些特殊注意点,请留意赋值部分的特殊说明。

类型声明与定义

reactive 变量的声明方式没有 ref 的变化那么大,基本上和普通变量一样,它的 TS 类型如下:

ts
function reactive<T extends object>(target: T): UnwrapNestedRefs<T>

可以看到其用法还是比较简单的,下面是一个 Reactive 对象的声明方式:

ts
// 声明对象的类型
+interface Member {
+  id: number
+  name: string
+}
+
+// 定义一个对象
+const userInfo: Member = reactive({
+  id: 1,
+  name: 'Tom',
+})

下面是 Reactive 数组的声明方式:

ts
const uids: number[] = reactive([1, 2, 3])

还可以声明一个 Reactive 对象数组:

ts
// 对象数组也是先声明其中的对象类型
+interface Member {
+  id: number
+  name: string
+}
+
+// 再定义一个为对象数组
+const userList: Member[] = reactive([
+  {
+    id: 1,
+    name: 'Tom',
+  },
+  {
+    id: 2,
+    name: 'Petter',
+  },
+  {
+    id: 3,
+    name: 'Andy',
+  },
+])

变量的读取与赋值

虽然 reactive API 在使用上没有像 ref API 一样有 .value 的心智负担,但也有一些注意事项要留意。

处理对象

Reactive 对象在读取字段的值,或者修改值的时候,与普通对象是一样的,这部分没有太多问题。

ts
// 声明对象的类型
+interface Member {
+  id: number
+  name: string
+}
+
+// 定义一个对象
+const userInfo: Member = reactive({
+  id: 1,
+  name: 'Tom',
+})
+
+// 读取用户名
+console.log(userInfo.name)
+
+// 修改用户名
+userInfo.name = 'Petter'

处理数组

但是对于 Reactive 数组,和普通数组会有一些区别。

普通数组在 “重置” 或者 “修改值” 时都是可以直接操作:

ts
// 定义一个普通数组
+let uids: number[] = [1, 2, 3]
+
+// 从另外一个对象数组里提取数据过来
+uids = api.data.map((item: any) => item.id)
+
+// 合并另外一个数组
+let newUids: number[] = [4, 5, 6]
+uids = [...uids, ...newUids]
+
+// 重置数组
+uids = []

Vue 2 在操作数组的时候,也可以和普通数组这样处理数据的变化,依然能够保持响应性,但在 Vue 3 ,如果使用 reactive 定义数组,则不能这么处理,必须只使用那些不会改变引用地址的操作。

笔者刚开始接触时,按照原来的思维去处理 reactive 数组,于是遇到了 “数据变了,但模板不会更新的问题” ,如果开发者在学习的过程中也遇到了类似的情况,可以从这里去入手排查问题所在。

举个例子,比如要从服务端 API 接口获取翻页数据时,通常要先重置数组,再异步添加数据,如果使用常规的重置,会导致这个变量失去响应性:

ts
let uids: number[] = reactive([1, 2, 3])
+
+/**
+ * 不推荐使用这种方式,会丢失响应性
+ * 异步添加数据后,模板不会响应更新
+ */
+uids = []
+
+// 异步获取数据后,模板依然是空数组
+setTimeout(() => {
+  uids.push(1)
+}, 1000)

要让数据依然保持响应性,则必须在关键操作时,不破坏响应性 API ,以下是推荐的操作方式,通过重置数组的 length 长度来实现数据的重置:

ts
const uids: number[] = reactive([1, 2, 3])
+
+/**
+ * 推荐使用这种方式,不会破坏响应性
+ */
+uids.length = 0
+
+// 异步获取数据后,模板可以正确的展示
+setTimeout(() => {
+  uids.push(1)
+}, 1000)

特别注意

不要对 Reactive 数据进行 ES6 的解构 操作,因为解构后得到的变量会失去响应性。

比如这些情况,在 2s 后都得不到新的 name 信息:

ts
import { defineComponent, reactive } from 'vue'
+
+interface Member {
+  id: number
+  name: string
+}
+
+export default defineComponent({
+  setup() {
+    // 定义一个带有响应性的对象
+    const userInfo: Member = reactive({
+      id: 1,
+      name: 'Petter',
+    })
+
+    // 在 2s 后更新 \`userInfo\`
+    setTimeout(() => {
+      userInfo.name = 'Tom'
+    }, 2000)
+
+    // 这个变量在 2s 后不会同步更新
+    const newUserInfo: Member = { ...userInfo }
+
+    // 这个变量在 2s 后不会再同步更新
+    const { name } = userInfo
+
+    // 这样 \`return\` 出去给模板用,在 2s 后也不会同步更新
+    return {
+      ...userInfo,
+    }
+  },
+})

响应式 API 之 toRef 与 toRefs ~new

相信各位开发者看到这里时,应该已经对 refreactive API 都有所了解了,为了方便开发者使用, Vue 3 还推出了两个与之相关的 API : toReftoRefs ,都是用于 reactiveref 转换。

各自的作用

这两个 API 在拼写上非常接近,顾名思义,一个是只转换一个字段,一个是转换所有字段,转换后将得到新的变量,并且新变量和原来的变量可以保持同步更新。

API作用
toRef创建一个新的 Ref 变量,转换 Reactive 对象的某个字段为 Ref 变量
toRefs创建一个新的对象,它的每个字段都是 Reactive 对象各个字段的 Ref 变量

光看概念可能不容易理解,来看下面的例子,先声明一个 reactive 变量:

ts
interface Member {
+  id: number
+  name: string
+}
+
+const userInfo: Member = reactive({
+  id: 1,
+  name: 'Petter',
+})

然后分别看看这两个 API 应该怎么使用。

使用 toRef

先看这个转换单个字段的 toRef API ,了解了它的用法之后,再去看 toRefs 就很容易理解了。

API 类型和基本用法

toRef API 的 TS 类型如下:

ts
// \`toRef\` API 的 TS 类型
+function toRef<T extends object, K extends keyof T>(
+  object: T,
+  key: K,
+  defaultValue?: T[K]
+): ToRef<T[K]>
+
+// \`toRef\` API 的返回值的 TS 类型
+type ToRef<T> = T extends Ref ? T : Ref<T>

通过接收两个必传的参数(第一个是 reactive 对象, 第二个是要转换的 key ),返回一个 Ref 变量,在适当的时候也可以传递第三个参数,为该变量设置默认值。

以上文声明好的 userInfo 为例,如果想转换 name 这个字段为 Ref 变量,只需要这样操作:

ts
const name = toRef(userInfo, 'name')
+console.log(name.value) // Petter

等号左侧的 name 变量此时是一个 Ref 变量,这里因为 TypeScript 可以对其自动推导,因此声明时可以省略 TS 类型的显式指定,实际上该变量的类型是 Ref<string>

所以之后在读取和赋值时,就需要使用 name.value 来操作,在重新赋值时会同时更新 nameuserInfo.name 的值:

ts
// 修改前先查看初始值
+const name = toRef(userInfo, 'name')
+console.log(name.value) // Petter
+console.log(userInfo.name) // Petter
+
+// 修改 Ref 变量的值,两者同步更新
+name.value = 'Tom'
+console.log(name.value) // Tom
+console.log(userInfo.name) // Tom
+
+// 修改 Reactive 对象上该属性的值,两者也是同步更新
+userInfo.name = 'Jerry'
+console.log(name.value) // Jerry
+console.log(userInfo.name) // Jerry

这个 API 也可以接收一个 Reactive 数组,此时第二个参数应该传入数组的下标:

ts
// 这一次声明的是数组
+const words = reactive(['a', 'b', 'c'])
+
+// 通过下标 \`0\` 转换第一个 item
+const a = toRef(words, 0)
+console.log(a.value) // a
+console.log(words[0]) // a
+
+// 通过下标 \`2\` 转换第三个 item
+const c = toRef(words, 2)
+console.log(c.value) // c
+console.log(words[2]) // c

设置默认值

如果 Reactive 对象上有一个属性本身没有初始值,也可以传递第三个参数进行设置(默认值仅对 Ref 变量有效):

ts
interface Member {
+  id: number
+  name: string
+  // 类型上新增一个属性,因为是可选的,因此默认值会是 \`undefined\`
+  age?: number
+}
+
+// 声明变量时省略 \`age\` 属性
+const userInfo: Member = reactive({
+  id: 1,
+  name: 'Petter',
+})
+
+// 此时为了避免程序运行错误,可以指定一个初始值
+// 但初始值仅对 Ref 变量有效,不会影响 Reactive 字段的值
+const age = toRef(userInfo, 'age', 18)
+console.log(age.value)  // 18
+console.log(userInfo.age) // undefined
+
+// 除非重新赋值,才会使两者同时更新
+age.value = 25
+console.log(age.value)  // 25
+console.log(userInfo.age) // 25

数组也是同理,对于可能不存在的下标,可以传入默认值避免项目的逻辑代码出现问题:

ts
const words = reactive(['a', 'b', 'c'])
+
+// 当下标对应的值不存在时,也是返回 \`undefined\`
+const d = toRef(words, 3)
+console.log(d.value) // undefined
+console.log(words[3]) // undefined
+
+// 设置了默认值之后,就会对 Ref 变量使用默认值, Reactive 数组此时不影响
+const e = toRef(words, 4, 'e')
+console.log(e.value) // e
+console.log(words[4]) // undefined

其他用法

这个 API 还有一个特殊用法,但不建议在 TypeScript 里使用。

toRef 的过程中,如果使用了原对象上面不存在的 key ,那么定义出来的 Ref 变量的 .value 值将会是 undefined

ts
// 众所周知, Petter 是没有女朋友的
+const girlfriend = toRef(userInfo, 'girlfriend')
+console.log(girlfriend.value) // undefined
+console.log(userInfo.girlfriend) // undefined
+
+// 此时 Reactive 对象上只有两个 Key
+console.log(Object.keys(userInfo)) // ['id', 'name']

如果对这个不存在的 key 的 Ref 变量进行赋值,那么原来的 Reactive 对象也会同步增加这个 key,其值也会同步更新。

ts
// 赋值后,不仅 Ref 变量得到了 \`Marry\` , Reactive 对象也得到了 \`Marry\`
+girlfriend.value = 'Marry'
+console.log(girlfriend.value) // 'Marry'
+console.log(userInfo.girlfriend) // 'Marry'
+
+// 此时 Reactive 对象上有了三个 Key
+console.log(Object.keys(userInfo)) // ['id', 'name', 'girlfriend']

为什么强调不要在 TypeScript 里使用呢?因为在编译时,无法通过 TypeScript 的类型检查:

bash
 npm run build
+
+> hello-vue3@0.0.0 build
+> vue-tsc --noEmit && vite build
+
+src/views/home.vue:37:40 - error TS2345: Argument of type '"girlfriend"'
+is not assignable to parameter of type 'keyof Member'.
+
+37     const girlfriend = toRef(userInfo, 'girlfriend')
+                                          ~~~~~~~~~~~~
+
+src/views/home.vue:39:26 - error TS2339: Property 'girlfriend' does not exist
+on type 'Member'.
+
+39     console.log(userInfo.girlfriend) // undefined
+                            ~~~~~~~~~~
+
+src/views/home.vue:45:26 - error TS2339: Property 'girlfriend' does not exist
+on type 'Member'.
+
+45     console.log(userInfo.girlfriend) // 'Marry'
+                            ~~~~~~~~~~
+
+
+Found 3 errors in the same file, starting at: src/views/home.vue:37

如果不得不使用这种情况,可以考虑使用 any 类型:

ts
// 将该类型直接指定为 \`any\`
+type Member = any
+// 当然一般都是 \`const userInfo: any\`
+
+// 或者保持接口类型的情况下,允许任意键值
+interface Member {
+  [key: string]: any
+}
+
+// 使用 \`Record\` 也是同理
+type Member = Record<string, any>

但笔者还是更推荐保持良好的类型声明习惯,尽量避免这种用法。

使用 toRefs

在了解了 toRef API 之后,来看看 toRefs 的用法。

API 类型和基本用法

先看看它的 TS 类型:

ts
function toRefs<T extends object>(
+  object: T
+): {
+  [K in keyof T]: ToRef<T[K]>
+}
+
+type ToRef = T extends Ref ? T : Ref<T>

toRef 不同, toRefs 只接收了一个参数,是一个 reactive 变量。

ts
interface Member {
+  id: number
+  name: string
+}
+
+// 声明一个 Reactive 变量
+const userInfo: Member = reactive({
+  id: 1,
+  name: 'Petter',
+})
+
+// 传给 \`toRefs\` 作为入参
+const userInfoRefs = toRefs(userInfo)

此时这个新的 userInfoRefs 变量,它的 TS 类型就不再是 Member 了,而应该是:

ts
// 导入 \`toRefs\` API 的类型
+import type { ToRefs } from 'vue'
+
+// 上下文代码省略...
+
+// 将原来的类型传给 API 的类型
+const userInfoRefs: ToRefs<Member> = toRefs(userInfo)

也可以重新编写一个新的类型来指定它,因为每个字段都是与原来关联的 Ref 变量,所以也可以这样声明:

ts
// 导入 \`ref\` API 的类型
+import type { Ref } from 'vue'
+
+// 上下文代码省略...
+
+// 新声明的类型每个字段都是一个 Ref 变量的类型
+interface MemberRefs {
+  id: Ref<number>
+  name: Ref<string>
+}
+
+// 使用新的类型进行声明
+const userInfoRefs: MemberRefs = toRefs(userInfo)

当然实际上日常使用时并不需要手动指定其类型, TypeScript 会自动推导,可以节约非常多的开发工作量。

toRef API 一样,这个 API 也是可以对数组进行转换:

ts
const words = reactive(['a', 'b', 'c'])
+const wordsRefs = toRefs(words)

此时新数组的类型是 Ref<string>[] ,不再是原来的 string[] 类型。

解构与赋值

转换后的 Reactive 对象或数组支持 ES6 的解构,并且不会失去响应性,因为解构后的每一个变量都具备响应性。

ts
// 为了提高开发效率,可以直接将 Ref 变量直接解构出来使用
+const { name } = toRefs(userInfo)
+console.log(name.value) // Petter
+
+// 此时对解构出来的变量重新赋值,原来的变量也可以同步更新
+name.value = 'Tom'
+console.log(name.value) // Tom
+console.log(userInfo.name) // Tom

这一点和直接解构 Reactive 变量有非常大的不同,直接解构 Reactive 变量,得到的是一个普通的变量,不再具备响应性。

这个功能在使用 Hooks 函数非常好用(在 Vue 3 里也叫可组合函数, Composable Functions ),还是以一个计算器函数为例,这一次将其修改为内部有一个 Reactive 的数据状态中心,在函数返回时解构为多个 Ref 变量:

ts
import { reactive, toRefs } from 'vue'
+
+// 声明 \`useCalculator\` 数据状态类型
+interface CalculatorState {
+  // 这是要用来计算操作的数据
+  num: number
+  // 这是每次计算时要增加的幅度
+  step: number
+}
+
+// 声明一个 “使用计算器” 的函数
+function useCalculator() {
+  // 通过数据状态中心的形式,集中管理内部变量
+  const state: CalculatorState = reactive({
+    num: 0,
+    step: 10,
+  })
+
+  // 功能函数也是通过数据中心变量去调用
+  function add() {
+    state.num += state.step
+  }
+
+  return {
+    ...toRefs(state),
+    add,
+  }
+}

这样在调用 useCalculator 函数时,可以通过解构直接获取到 Ref 变量,不需要再进行额外的转换工作。

ts
// 解构出来的 \`num\` 和 \`step\` 都是 Ref 变量
+const { num, step, add } = useCalculator()
+console.log(num.value) // 0
+console.log(step.value) // 10
+
+// 调用计算器的方法,数据也是会得到响应式更新
+add()
+console.log(num.value) // 10

为什么要进行转换

关于为什么要出这么两个 API ,官方文档没有特别说明,不过经过笔者在业务中的一些实际使用感受,以及在写上一节 reactive特别注意,可能知道一些使用理由。

关于 refreactive 这两个 API 的好处就不重复了,但是在使用的过程中,各自都有不方便的地方:

ref API 虽然在 <template /> 里使用起来方便,但是在 <script /> 里进行读取 / 赋值的时候,要一直记得加上 .value ,否则 BUG 就来了。

reactive API 虽然在使用的时候,因为知道它本身是一个对象,所以不会忘记通过 foo.bar 这样的格式去操作,但是在 <template /> 渲染的时候,又因此不得不每次都使用 foo.bar 的格式去渲染。

那么有没有办法,既可以在编写 <script /> 的时候不容易出错,在写 <template /> 的时候又比较简单呢?

于是, toReftoRefs 因此诞生。

什么场景下比较适合使用它们

从便利性和可维护性来说,最好只在功能单一、代码量少的组件里使用,比如一个表单组件,通常表单的数据都放在一个对象里。

当然也可以把所有的数据都定义到一个 data 里,再去 data 里面取值,但是没有必要为了转换而转换,否则不如使用 Options API 风格。

在业务中的具体运用

继续使用上文一直在使用的 userInfo 来当案例,以一个用户信息表的小 demo 做个演示。

<script /> 部分:

  1. 先用 reactive 定义一个源数据,所有的数据更新,都是修改这个对象对应的值,按照对象的写法维护数据

  2. 再通过 toRefs 定义一个给 <template /> 使用的对象,这样可以得到一个每个字段都是 Ref 变量的新对象

  3. return 的时候,对步骤 2 里的 toRefs 对象进行解构,这样导出去就是各个字段对应的 Ref 变量,而不是一整个对象

ts
import { defineComponent, reactive, toRefs } from 'vue'
+
+interface Member {
+  id: number
+  name: string
+  age: number
+  gender: string
+}
+
+export default defineComponent({
+  setup() {
+    // 定义一个 reactive 对象
+    const userInfo = reactive({
+      id: 1,
+      name: 'Petter',
+      age: 18,
+      gender: 'male',
+    })
+
+    // 定义一个新的对象,它本身不具备响应性,但是它的字段全部是 Ref 变量
+    const userInfoRefs = toRefs(userInfo)
+
+    // 在 2s 后更新 \`userInfo\`
+    setTimeout(() => {
+      userInfo.id = 2
+      userInfo.name = 'Tom'
+      userInfo.age = 20
+    }, 2000)
+
+    // 在这里解构 \`toRefs\` 对象才能继续保持响应性
+    return {
+      ...userInfoRefs,
+    }
+  },
+})

<template /> 部分:

由于 return 出来的都是 Ref 变量,所以在模板里可以直接使用 userInfo 各个字段的 key ,不再需要写很长的 userInfo.name 了。

vue
<template>
+  <ul class="user-info">
+    <li class="item">
+      <span class="key">ID:</span>
+      <span class="value">{{ id }}</span>
+    </li>
+
+    <li class="item">
+      <span class="key">name:</span>
+      <span class="value">{{ name }}</span>
+    </li>
+
+    <li class="item">
+      <span class="key">age:</span>
+      <span class="value">{{ age }}</span>
+    </li>
+
+    <li class="item">
+      <span class="key">gender:</span>
+      <span class="value">{{ gender }}</span>
+    </li>
+  </ul>
+</template>

需要注意的问题

请注意是否有相同命名的变量存在,比如上面在 return<template /> 使用时,在解构 userInfoRefs 的时候已经包含了一个 name 字段,此时如果还有一个单独的变量也叫 name ,就会出现渲染上的数据显示问题。

此时它们在 <template /> 里哪个会生效,取决于谁排在后面,因为 return 出去的其实是一个对象,在对象里,如果存在相同的 key ,则后面的会覆盖前面的。

下面这种情况,会以单独的 name 为渲染数据:

ts
return {
+  ...userInfoRefs,
+  name,
+}

而下面这种情况,则是以 userInfoRefs 里的 name 为渲染数据:

ts
return {
+  name,
+  ...userInfoRefs,
+}

所以当决定使用 toReftoRefs API 的时候,请注意这个特殊情况!

函数的声明和使用 ~new

在了解了响应式数据如何使用之后,接下来就要开始了解函数了。

在 Vue 2 ,函数通常是作为当前组件实例上的方法在 methods 里声明,然后再在 mounted 等生命周期里调用,或者是在模板里通过 Click 等行为触发,由于组件内部经常需要使用 this 获取组件实例,因此不能使用箭头函数。

js
export default {
+  data: () => {
+    return {
+      num: 0,
+    }
+  },
+  mounted: function () {
+    this.add()
+  },
+  methods: {
+    // 不可以使用 \`add: () => this.num++\`
+    add: function () {
+      this.num++
+    },
+  },
+}

在 Vue 3 则灵活了很多,可以使用普通函数、 Class 类、箭头函数、匿名函数等等进行声明,可以将其写在 setup 里直接使用,也可以抽离在独立的 .js / .ts 文件里再导入使用。

需要在组件创建时自动执行的函数,其执行时机需要遵循 Vue 3 的生命周期,需要在模板里通过 @click@change 等行为触发,和变量一样,需要把函数名在 setup 里进行 return 出去。

下面是一个简单的例子,方便开发者更直观地了解:

vue
<template>
+  <p>{{ msg }}</p>
+
+  <!-- 在这里点击执行 \`return\` 出来的方法 -->
+  <button @click="updateMsg">修改MSG</button>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const msg = ref<string>('Hello World!')
+
+    // 这个要暴露给模板使用,必须 \`return\` 才可以使用
+    function updateMsg() {
+      msg.value = 'Hi World!'
+    }
+
+    // 这个要在页面载入时执行,无需 \`return\` 出去
+    const init = () => {
+      console.log('init')
+    }
+
+    onMounted(() => {
+      init()
+    })
+
+    return {
+      msg,
+      updateMsg,
+    }
+  },
+})
+</script>

数据的侦听 ~new

侦听数据变化也是组件里的一项重要工作,比如侦听路由变化、侦听参数变化等等。

Vue 3 在保留原来的 watch 功能之外,还新增了一个 watchEffect 帮助更简单的进行侦听。

watch

在 Vue 3 ,新版的 watch 和 Vue 2 的旧版写法对比,在使用方式上变化非常大!

回顾 Vue 2

在 Vue 2 是这样用的,和 datamethods 都在同级配置:

ts
export default {
+  data() {
+    return {
+      // ...
+    }
+  },
+  // 注意这里,放在 \`data\` 、 \`methods\` 同个级别
+  watch: {
+    // ...
+  },
+  methods: {
+    // ...
+  },
+}

并且类型繁多,选项式 API 的类型如下:

ts
watch: { [key: string]: string | Function | Object | Array}

联合类型过多,意味着用法复杂,下面是个很好的例子,虽然出自 官网 的用法介绍,但过于繁多的用法也反映出来对初学者不太友好,初次接触可能会觉得一头雾水:

ts
export default {
+  data() {
+    return {
+      a: 1,
+      b: 2,
+      c: {
+        d: 4,
+      },
+      e: 5,
+      f: 6,
+    }
+  },
+  watch: {
+    // 侦听顶级 Property
+    a(val, oldVal) {
+      console.log(\`new: \${val}, old: \${oldVal}\`)
+    },
+    // 字符串方法名
+    b: 'someMethod',
+    // 该回调会在任何被侦听的对象的 Property 改变时被调用,不论其被嵌套多深
+    c: {
+      handler(val, oldVal) {
+        console.log('c changed')
+      },
+      deep: true,
+    },
+    // 侦听单个嵌套 Property
+    'c.d': function (val, oldVal) {
+      // do something
+    },
+    // 该回调将会在侦听开始之后被立即调用
+    e: {
+      handler(val, oldVal) {
+        console.log('e changed')
+      },
+      immediate: true,
+    },
+    // 可以传入回调数组,它们会被逐一调用
+    f: [
+      'handle1',
+      function handle2(val, oldVal) {
+        console.log('handle2 triggered')
+      },
+      {
+        handler: function handle3(val, oldVal) {
+          console.log('handle3 triggered')
+        },
+        /* ... */
+      },
+    ],
+  },
+  methods: {
+    someMethod() {
+      console.log('b changed')
+    },
+    handle1() {
+      console.log('handle 1 triggered')
+    },
+  },
+}

当然肯定也会有开发者会觉得这样选择多是个好事,选择适合自己的就好,但笔者还是认为这种写法对于初学者来说不是那么友好,有些过于复杂化,如果一个用法可以适应各种各样的场景,岂不是更妙?

TIP

另外需要注意的是,不能使用箭头函数来定义 Watcher 函数 (例如 searchQuery: newValue => this.updateAutocomplete(newValue) )。

因为箭头函数绑定了父级作用域的上下文,所以 this 将不会按照期望指向组件实例, this.updateAutocomplete 将是 undefined

Vue 2 也可以通过 this.$watch() 这个 API 的用法来实现对某个数据的侦听,它接受三个参数: sourcecallbackoptions

ts
export default {
+  data() {
+    return {
+      a: 1,
+    }
+  },
+  // 生命周期钩子
+  mounted() {
+    this.$watch('a', (newVal, oldVal) => {
+      // ...
+    })
+  },
+}

由于 this.$watch 的用法和 Vue 3 比较接近,所以这里不做过多的回顾,请直接看 了解 Vue 3 部分。

了解 Vue 3

在 Vue 3 的组合式 API 写法, watch 是一个可以接受 3 个参数的函数(保留了 Vue 2 的 this.$watch 这种用法),在使用层面上简单了很多。

ts
import { watch } from 'vue'
+
+// 一个用法走天下
+watch(
+  source, // 必传,要侦听的数据源
+  callback // 必传,侦听到变化后要执行的回调函数
+  // options // 可选,一些侦听选项
+)

下面的内容都基于 Vue 3 的组合式 API 用法展开讲解。

API 的 TS 类型

在了解用法之前,先对它的 TS 类型声明做一个简单的了解, watch 作为组合式 API ,根据使用方式有两种类型声明:

  1. 基础用法的 TS 类型,详见 基础用法 部分
ts
// watch 部分的 TS 类型
+// ...
+export declare function watch<T, Immediate extends Readonly<boolean> = false>(
+  source: WatchSource<T>,
+  cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
+  options?: WatchOptions<Immediate>
+): WatchStopHandle
+// ...
  1. 批量侦听的 TS 类型,详见 批量侦听 部分
ts
// watch 部分的 TS 类型
+// ...
+export declare function watch<
+  T extends MultiWatchSources,
+  Immediate extends Readonly<boolean> = false
+>(
+  sources: [...T],
+  cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
+  options?: WatchOptions<Immediate>
+): WatchStopHandle
+
+// MultiWatchSources 是一个数组
+declare type MultiWatchSources = (WatchSource<unknown> | object)[]
+// ...

但是不管是基础用法还是批量侦听,可以看到这个 API 都是接受三个入参:

参数是否可选含义
source必传数据源(详见:要侦听的数据源
callback必传侦听到变化后要执行的回调函数(详见:侦听后的回调函数
options可选一些侦听选项(详见:侦听的选项

并返回一个可以用来停止侦听的函数(详见:停止侦听)。

要侦听的数据源

在上面 API 的 TS 类型 已经对 watch API 的组成有一定的了解了,这里先对数据源的类型和使用限制做下说明。

TIP

如果不提前了解,在使用的过程中可能会遇到 “侦听了但没有反应” 的情况出现。

另外,这部分内容会先围绕基础用法展开说明,批量侦听会在 批量侦听 部分单独说明。

watch API 的第 1 个参数 source 是要侦听的数据源,它的 TS 类型如下:

ts
// watch 第 1 个入参的 TS 类型
+// ...
+export declare type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
+// ...

可以看到能够用于侦听的数据,是通过 响应式 API 定义的变量( Ref<T> ),或者是一个 计算数据ComputedRef<T> ),或者是一个 getter 函数() => T )。

所以要想定义的 watch 能够做出预期的行为,数据源必须具备响应性或者是一个 getter ,如果只是通过 let 定义一个普通变量,然后去改变这个变量的值,这样是无法侦听的。

TIP

如果要侦听响应式对象里面的某个值(这种情况下对象本身是响应式,但它的 property 不是),需要写成 getter 函数,简单的说就是需要写成有返回值的函数,这个函数 return 要侦听的数据, e.g. () => foo.bar ,可以结合下方 基础用法 的例子一起理解。

侦听后的回调函数

在上面 API 的 TS 类型 介绍了 watch API 的组成,和数据源一样,先了解一下回调函数的定义。

TIP

和数据源部分一样,回调函数的内容也是会先围绕基础用法展开说明,批量侦听会在 批量侦听 部分单独说明。

watch API 的第 2 个参数 callback 是侦听到数据变化时要做出的行为,它的 TS 类型如下:

ts
// watch 第 2 个入参的 TS 类型
+// ...
+export declare type WatchCallback<V = any, OV = any> = (
+  value: V,
+  oldValue: OV,
+  onCleanup: OnCleanup
+) => any
+// ...

乍一看它有三个参数,但实际上这些参数不是自己定义的,而是 watch API 传给的,所以不管用或者不用,它们都在那里:

参数作用
value变化后的新值,类型和数据源保持一致
oldValue变化前的旧值,类型和数据源保持一致
onCleanup注册一个清理函数,详见 侦听效果清理 部分

注意:第一个参数是新值,第二个才是原来的旧值!

如同其他 JS 函数,在使用 watch 的回调函数时,可以对这三个参数任意命名,比如把 value 命名为觉得更容易理解的 newValue

TIP

如果侦听的数据源是一个 引用类型 时( e.g. ObjectArrayDate … ), valueoldValue 是完全相同的,因为指向同一个对象。

另外,默认情况下,watch 是惰性的,也就是只有当被侦听的数据源发生变化时才执行回调。

基础用法

来到这里,对 2 个必传的参数都有一定的了解了,先看看基础的用法,也就是日常最常编写的方案,只需要先关注前 2 个必传的参数。

ts
// 不要忘了导入要用的 API
+import { defineComponent, reactive, watch } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 定义一个响应式数据
+    const userInfo = reactive({
+      name: 'Petter',
+      age: 18,
+    })
+
+    // 2s后改变数据
+    setTimeout(() => {
+      userInfo.name = 'Tom'
+    }, 2000)
+
+    /**
+     * 可以直接侦听这个响应式对象
+     * callback 的参数如果不用可以不写
+     */
+    watch(userInfo, () => {
+      console.log('侦听整个 userInfo ', userInfo.name)
+    })
+
+    /**
+     * 也可以侦听对象里面的某个值
+     * 此时数据源需要写成 getter 函数
+     */
+    watch(
+      // 数据源,getter 形式
+      () => userInfo.name,
+      // 回调函数 callback
+      (newValue, oldValue) => {
+        console.log('只侦听 name 的变化 ', userInfo.name)
+        console.log('打印变化前后的值', { oldValue, newValue })
+      }
+    )
+  },
+})

一般的业务场景,基础用法足以面对。

如果有多个数据源要侦听,并且侦听到变化后要执行的行为一样,那么可以使用 批量侦听

特殊的情况下,可以搭配 侦听的选项 做一些特殊的用法,详见下面部分的内容。

批量侦听

如果有多个数据源要侦听,并且侦听到变化后要执行的行为一样,第一反应可能是这样来写:

  1. 抽离相同的处理行为为公共函数
  2. 然后定义多个侦听操作,传入这个公共函数
ts
import { defineComponent, ref, watch } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const message = ref<string>('')
+    const index = ref<number>(0)
+
+    // 2s后改变数据
+    setTimeout(() => {
+      // 来到这里才会触发 watch 的回调
+      message.value = 'Hello World!'
+      index.value++
+    }, 2000)
+
+    // 抽离相同的处理行为为公共函数
+    const handleWatch = (
+      newValue: string | number,
+      oldValue: string | number
+    ): void => {
+      console.log({ newValue, oldValue })
+    }
+
+    // 然后定义多个侦听操作,传入这个公共函数
+    watch(message, handleWatch)
+    watch(index, handleWatch)
+  },
+})

这样写其实没什么问题,不过除了抽离公共代码的写法之外, watch API 还提供了一个批量侦听的用法,和 基础用法 的区别在于,数据源和回调参数都变成了数组的形式。

数据源:以数组的形式传入,里面每一项都是一个响应式数据。

回调参数:原来的 valuenewValue 也都变成了数组,每个数组里面的顺序和数据源数组排序一致。

可以看下面的这个例子更为直观:

ts
import { defineComponent, ref, watch } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 定义多个数据源
+    const message = ref<string>('')
+    const index = ref<number>(0)
+
+    // 2s后改变数据
+    setTimeout(() => {
+      message.value = 'Hello World!'
+      index.value++
+    }, 2000)
+
+    watch(
+      // 数据源改成了数组
+      [message, index],
+      // 回调的入参也变成了数组,每个数组里面的顺序和数据源数组排序一致
+      ([newMessage, newIndex], [oldMessage, oldIndex]) => {
+        console.log('message 的变化', { newMessage, oldMessage })
+        console.log('index 的变化', { newIndex, oldIndex })
+      }
+    )
+  },
+})

什么情况下可能会用到批量侦听呢?比如一个子组件有多个 props ,当有任意一个 prop 发生变化时,都需要执行初始化函数重置组件的状态,那么这个时候就可以用上这个功能啦!

TIP

在适当的业务场景,也可以使用 watchEffect 来完成批量侦听,但请留意 功能区别 部分的说明。

侦听的选项

API 的 TS 类型 里提到, watch API 还接受第 3 个参数 options ,可选的一些侦听选项。

它的 TS 类型如下:

ts
// watch 第 3 个入参的 TS 类型
+// ...
+export declare interface WatchOptions<Immediate = boolean>
+  extends WatchOptionsBase {
+  immediate?: Immediate
+  deep?: boolean
+}
+// ...
+
+// 继承的 base 类型
+export declare interface WatchOptionsBase extends DebuggerOptions {
+  flush?: 'pre' | 'post' | 'sync'
+}
+// ...
+
+// 继承的 debugger 选项类型
+export declare interface DebuggerOptions {
+  onTrack?: (event: DebuggerEvent) => void
+  onTrigger?: (event: DebuggerEvent) => void
+}
+// ...

options 是一个对象的形式传入,有以下几个选项:

选项类型默认值可选值作用
deepbooleanfalsetrue | false是否进行深度侦听
immediatebooleanfalsetrue | false是否立即执行侦听回调
flushstring'pre''pre' | 'post' | 'sync'控制侦听回调的调用时机
onTrack(e) => void在数据源被追踪时调用
onTrigger(e) => void在侦听回调被触发时调用

其中 onTrackonTriggere 是 debugger 事件,建议在回调内放置一个 debugger 语句 以调试依赖,这两个选项仅在开发模式下生效。

TIP

deep 默认是 false ,但是在侦听 reactive 对象或数组时,会默认为 true ,详见 侦听选项之 deep

侦听选项之 deep

deep 选项接受一个布尔值,可以设置为 true 开启深度侦听,或者是 false 关闭深度侦听,默认情况下这个选项是 false 关闭深度侦听的,但也存在特例。

设置为 false 的情况下,如果直接侦听一个响应式的 引用类型 数据(e.g. ObjectArray … ),虽然它的属性的值有变化,但对其本身来说是不变的,所以不会触发 watch 的 callback 。

下面是一个关闭了深度侦听的例子:

ts
import { defineComponent, ref, watch } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 定义一个响应式数据,注意用的是 ref 来定义
+    const nums = ref<number[]>([])
+
+    // 2s后给这个数组添加项目
+    setTimeout(() => {
+      nums.value.push(1)
+
+      // 可以打印一下,确保数据确实变化了
+      console.log('修改后', nums.value)
+    }, 2000)
+
+    // 但是这个 watch 不会按预期执行
+    watch(
+      nums,
+      // 这里的 callback 不会被触发
+      () => {
+        console.log('触发侦听', nums.value)
+      },
+      // 因为关闭了 deep
+      {
+        deep: false,
+      }
+    )
+  },
+})

类似这种情况,需要把 deep 设置为 true 才可以触发侦听。

可以看到上面的例子特地用了 ref API ,这是因为通过 reactive API 定义的对象无法将 deep 成功设置为 false (这一点在目前的官网文档未找到说明,最终是在 watch API 的源码 上找到了答案)。

ts
// ...
+if (isReactive(source)) {
+  getter = () => source
+  deep = true // 被强制开启了
+}
+// ...

这个情况就是上面所说的 “特例” ,可以通过 isReactive API 来判断是否需要手动开启深度侦听。

ts
// 导入 isReactive API
+import { defineComponent, isReactive, reactive, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 侦听这个数据时,会默认开启深度侦听
+    const foo = reactive({
+      name: 'Petter',
+      age: 18,
+    })
+    console.log(isReactive(foo)) // true
+
+    // 侦听这个数据时,不会默认开启深度侦听
+    const bar = ref({
+      name: 'Petter',
+      age: 18,
+    })
+    console.log(isReactive(bar)) // false
+  },
+})

侦听选项之 immediate

侦听后的回调函数 部分有了解过, watch 默认是惰性的,也就是只有当被侦听的数据源发生变化时才执行回调。

这句话是什么意思呢?来看一下这段代码,为了减少 deep 选项的干扰,换一个类型,换成 string 数据来演示,请留意注释:

ts
import { defineComponent, ref, watch } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 这个时候不会触发 watch 的回调
+    const message = ref<string>('')
+
+    // 2s后改变数据
+    setTimeout(() => {
+      // 来到这里才会触发 watch 的回调
+      message.value = 'Hello World!'
+    }, 2000)
+
+    watch(message, () => {
+      console.log('触发侦听', message.value)
+    })
+  },
+})

可以看到,数据在初始化的时候并不会触发侦听回调,如果有需要的话,通过 immediate 选项来让它直接触发。

immediate 选项接受一个布尔值,默认是 false ,可以设置为 true 让回调立即执行。

改成这样,请留意高亮的代码部分和新的注释:

ts
import { defineComponent, ref, watch } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 这一次在这里可以会触发 watch 的回调了
+    const message = ref<string>('')
+
+    // 2s后改变数据
+    setTimeout(() => {
+      // 这一次,这里是第二次触发 watch 的回调,不再是第一次
+      message.value = 'Hello World!'
+    }, 2000)
+
+    watch(
+      message,
+      () => {
+        console.log('触发侦听', message.value)
+      },
+      // 设置 immediate 选项
+      {
+        immediate: true,
+      }
+    )
+  },
+})

注意,在带有 immediate 选项时,不能在第一次回调时取消该数据源的侦听,详见 停止侦听 部分。

侦听选项之 flush

flush 选项是用来控制 侦听回调 的调用时机,接受指定的字符串,可选值如下,默认是 'pre'

可选值回调的调用时机使用场景
'pre'将在渲染前被调用允许回调在模板运行前更新了其他值
'sync'在渲染时被同步调用目前来说没什么好处,可以了解但不建议用…
'post'被推迟到渲染之后调用如果要通过 ref 操作 DOM 元素与子组件 ,需要使用这个值来启用该选项,以达到预期的执行效果

对于 'pre''post' ,回调使用队列进行缓冲。回调只被添加到队列中一次。

即使观察值变化了多次,值的中间变化将被跳过,不会传递给回调,这样做不仅可以提高性能,还有助于保证数据的一致性。

更多关于 flush 的信息,请参阅 回调的触发时机

停止侦听

如果在 setup 或者 script-setup 里使用 watch 的话, 组件被卸载 的时候也会一起被停止,一般情况下不太需要关心如何停止侦听。

不过有时候可能想要手动取消, Vue 3 也提供了方法。

TIP

随着组件被卸载一起停止的前提是,侦听器必须是 同步语句 创建的,这种情况下侦听器会绑定在当前组件上。

如果放在 setTimeout异步函数 里面创建,则不会绑定到当前组件,因此组件卸载的时候不会一起停止该侦听器,这种时候就需要手动停止侦听。

API 的 TS 类型 有提到,当在定义一个 watch 行为的时候,它会返回一个用来停止侦听的函数。

这个函数的 TS 类型如下:

ts
export declare type WatchStopHandle = () => void

用法很简单,做一下简单了解即可:

ts
// 定义一个取消观察的变量,它是一个函数
+const unwatch = watch(message, () => {
+  // ...
+})
+
+// 在合适的时期调用它,可以取消这个侦听
+unwatch()

但是也有一点需要注意的是,如果启用了 immediate 选项 ,不能在第一次触发侦听回调时执行它。

ts
// 注意:这是一段错误的代码,运行会报错
+const unwatch = watch(
+  message,
+  // 侦听的回调
+  () => {
+    // ...
+    // 在这里调用会有问题 ❌
+    unwatch()
+  },
+  // 启用 immediate 选项
+  {
+    immediate: true,
+  }
+)

会收获一段报错,告诉 unwatch 这个变量在初始化前无法被访问:

bash
Uncaught ReferenceError: Cannot access 'unwatch' before initialization

目前有两种方案可以让实现这个操作:

方案一:使用 var 并判断变量类型,利用 var 的变量提升 来实现目的。

ts
// 这里改成 var ,不要用 const 或 let
+var unwatch = watch(
+  message,
+  // 侦听回调
+  () => {
+    // 这里加一个判断,是函数才执行它
+    if (typeof unwatch === 'function') {
+      unwatch()
+    }
+  },
+  // 侦听选项
+  {
+    immediate: true,
+  }
+)

不过 var 已经属于过时的语句了,建议用方案二的 let

方案二:使用 let 并判断变量类型。

ts
// 如果不想用 any ,可以导入 TS 类型
+import type { WatchStopHandle } from 'vue'
+
+// 这里改成 let ,但是要另起一行,先定义,再赋值
+let unwatch: WatchStopHandle
+unwatch = watch(
+  message,
+  // 侦听回调
+  () => {
+    // 这里加一个判断,是函数才执行它
+    if (typeof unwatch === 'function') {
+      unwatch()
+    }
+  },
+  // 侦听选项
+  {
+    immediate: true,
+  }
+)

侦听效果清理

侦听后的回调函数 部分提及到一个参数 onCleanup ,它可以帮注册一个清理函数。

有时 watch 的回调会执行异步操作,当 watch 到数据变更的时候,需要取消这些操作,这个函数的作用就用于此,会在以下情况调用这个清理函数:

  • watcher 即将重新运行的时候
  • watcher 被停止(组件被卸载或者被手动 停止侦听

TS 类型:

ts
declare type OnCleanup = (cleanupFn: () => void) => void

用法方面比较简单,传入一个回调函数运行即可,不过需要注意的是,需要在停止侦听之前注册好清理行为,否则不会生效。

停止侦听 里的最后一个 immediate 例子的基础上继续添加代码,请注意注册的时机:

ts
let unwatch: WatchStopHandle
+unwatch = watch(
+  message,
+  (newValue, oldValue, onCleanup) => {
+    // 需要在停止侦听之前注册好清理行为
+    onCleanup(() => {
+      console.log('侦听清理ing')
+      // 根据实际的业务情况定义一些清理操作 ...
+    })
+    // 然后再停止侦听
+    if (typeof unwatch === 'function') {
+      unwatch()
+    }
+  },
+  {
+    immediate: true,
+  }
+)

watchEffect

如果一个函数里包含了多个需要侦听的数据,一个一个数据去侦听太麻烦了,在 Vue 3 ,可以直接使用 watchEffect API 来简化的操作。

API 的 TS 类型

这个 API 的类型如下,使用的时候需要传入一个副作用函数(相当于 watch 的 侦听后的回调函数 ),也可以根据的实际情况传入一些可选的 侦听选项

和 watch API 一样,它也会返回一个用于 停止侦听 的函数。

ts
// watchEffect 部分的 TS 类型
+// ...
+export declare type WatchEffect = (onCleanup: OnCleanup) => void
+
+export declare function watchEffect(
+  effect: WatchEffect,
+  options?: WatchOptionsBase
+): WatchStopHandle
+// ...

副作用函数也会传入一个清理回调作为参数,和 watch 的 侦听效果清理 一样的用法。

可以理解为它是一个简化版的 watch ,具体简化在哪里呢?请看下面的用法示例。

用法示例

它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

ts
import { defineComponent, ref, watchEffect } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 单独定义两个数据,后面用来分开改变数值
+    const name = ref<string>('Petter')
+    const age = ref<number>(18)
+
+    // 定义一个调用这两个数据的函数
+    const getUserInfo = (): void => {
+      console.log({
+        name: name.value,
+        age: age.value,
+      })
+    }
+
+    // 2s后改变第一个数据
+    setTimeout(() => {
+      name.value = 'Tom'
+    }, 2000)
+
+    // 4s后改变第二个数据
+    setTimeout(() => {
+      age.value = 20
+    }, 4000)
+
+    // 直接侦听调用函数,在每个数据产生变化的时候,它都会自动执行
+    watchEffect(getUserInfo)
+  },
+})

和 watch 的区别

虽然理论上 watchEffectwatch 的一个简化操作,可以用来代替 批量侦听 ,但它们也有一定的区别:

  1. watch 可以访问侦听状态变化前后的值,而 watchEffect 没有。

  2. watch 是在属性改变的时候才执行,而 watchEffect 则默认会执行一次,然后在属性改变的时候也会执行。

第二点的意思,看下面这段代码可以有更直观的理解:

使用 watch :

ts
export default defineComponent({
+  setup() {
+    const foo = ref<string>('')
+
+    setTimeout(() => {
+      foo.value = 'Hello World!'
+    }, 2000)
+
+    function bar() {
+      console.log(foo.value)
+    }
+
+    // 使用 watch 需要先手动执行一次
+    bar()
+
+    // 然后当 foo 有变动时,才会通过 watch 来执行 bar()
+    watch(foo, bar)
+  },
+})

使用 watchEffect :

ts
export default defineComponent({
+  setup() {
+    const foo = ref<string>('')
+
+    setTimeout(() => {
+      foo.value = 'Hello World!'
+    }, 2000)
+
+    function bar() {
+      console.log(foo.value)
+    }
+
+    // 可以通过 watchEffect 实现 bar() + watch(foo, bar) 的效果
+    watchEffect(bar)
+  },
+})

可用的侦听选项

虽然用法和 watch 类似,但也简化了一些选项,它的侦听选项 TS 类型如下:

ts
// 只支持 base 类型
+export declare interface WatchOptionsBase extends DebuggerOptions {
+  flush?: 'pre' | 'post' | 'sync'
+}
+// ...
+
+// 继承的 debugger 选项类型
+export declare interface DebuggerOptions {
+  onTrack?: (event: DebuggerEvent) => void
+  onTrigger?: (event: DebuggerEvent) => void
+}
+// ...

对比 watch API ,它不支持 deepimmediate ,请记住这一点,其他的用法是一样的。

flush 选项的使用详见 侦听选项之 flushonTrackonTrigger 详见 侦听的选项 部分内容。

watchPostEffect

watchEffect API 使用 flush: 'post' 选项时的别名,具体区别详见 侦听选项之 flush 部分。

TIP

Vue v3.2.0 及以上版本才支持该 API 。

watchSyncEffect

watchEffect API 使用 flush: 'sync' 选项时的别名,具体区别详见 侦听选项之 flush 部分。

TIP

Vue v3.2.0 及以上版本才支持该 API 。

数据的计算 ~new

和 Vue 2.0 一样,数据的计算也是使用 computed API ,它可以通过现有的响应式数据,去通过计算得到新的响应式变量,用过 Vue 2.0 的开发者应该不会太陌生,但是在 Vue 3.0 ,在使用方式上也是变化非常大!

TIP

这里的响应式数据,可以简单理解为通过 ref API 、 reactive API 定义出来的数据,当然 Vuex 、Vue Router 等 Vue 数据也都具备响应式,可以在 响应式数据的变化 了解。

用法变化

先从一个简单的用例来看看在 Vue 新旧版本的用法区别:

假设定义了两个分开的数据 firstName 名字和 lastName 姓氏,但是在 template 展示时,需要展示完整的姓名,那么就可以通过 computed 来计算一个新的数据:

回顾 Vue 2

在 Vue 2.0 ,computeddata 在同级配置,并且不可以和 data 里的数据同名重复定义:

ts
// 在 Vue 2 的写法:
+export default {
+  data() {
+    return {
+      firstName: 'Bill',
+      lastName: 'Gates',
+    }
+  },
+  // 注意这里定义的变量,都要通过函数的形式来返回它的值
+  computed: {
+    // 普通函数可以直接通过熟悉的 this 来拿到 data 里的数据
+    fullName() {
+      return \`\${this.firstName} \${this.lastName}\`
+    },
+    // 箭头函数则需要通过参数来拿到实例上的数据
+    fullName2: (vm) => \`\${vm.firstName} \${vm.lastName}\`,
+  },
+}

这样在需要用到全名的地方,只需要通过 this.fullName 就可以得到 Bill Gates

了解 Vue 3

在 Vue 3.0 ,跟其他 API 的用法一样,需要先导入 computed 才能使用:

ts
// 在 Vue 3 的写法:
+import { defineComponent, ref, computed } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 定义基本的数据
+    const firstName = ref<string>('Bill')
+    const lastName = ref<string>('Gates')
+
+    // 定义需要计算拼接结果的数据
+    const fullName = computed(() => \`\${firstName.value} \${lastName.value}\`)
+
+    // 2s 后改变某个数据的值
+    setTimeout(() => {
+      firstName.value = 'Petter'
+    }, 2000)
+
+    // template 那边在 2s 后也会显示为 Petter Gates
+    return {
+      fullName,
+    }
+  },
+})

可以把这个用法简单的理解为,传入一个回调函数,并 return 一个值,对,它需要有明确的返回值。

TIP

需要注意的是:

  1. 定义出来的 computed 变量,和 Ref 变量的用法一样,也是需要通过 .value 才能拿到它的值

  2. 但是区别在于,默认情况下 computedvalue 是只读的

原因详见下方的 类型声明

类型声明

之前说过,在 defineComponent 里,会自动帮推导 Vue API 的类型,所以一般情况下,是不需要显式的去定义 computed 出来的变量类型的。

在确实需要手动指定的情况下,也可以导入它的类型然后定义:

ts
import { computed } from 'vue'
+import type { ComputedRef } from 'vue'
+
+// 注意这里添加了类型声明
+const fullName: ComputedRef<string> = computed(
+  () => \`\${firstName.value} \${lastName.value}\`
+)

要返回一个字符串,就写 ComputedRef<string> ;返回布尔值,就写 ComputedRef<boolean> ;返回一些复杂对象信息,可以先定义好的类型,再诸如 ComputedRef<UserInfo> 去写。

ts
// 这是 ComputedRef 的类型声明:
+export declare interface ComputedRef<T = any> extends WritableComputedRef<T> {
+  readonly value: T
+  [ComoutedRefSymbol]: true
+}

优势对比和注意事项

在继续往下看之前,先来了解一下这个 API 的一些优势和注意事项(如果在 Vue 2 已经有接触过的话,可以跳过这一段,因为优势和需要注意的东西比较一致)。

优势对比

看到这里,相信刚接触的开发者可能会有疑问,既然 computed 也是通过一个函数来返回值,那么和普通的 function 有什么区别,或者说优势?

  1. 性能优势

这一点在 官网文档 其实是有提到的:

数据的计算是基于它们的响应依赖关系缓存的,只在相关响应式依赖发生改变时它们才会重新求值。

也就是说,只要原始数据没有发生改变,多次访问 computed ,都是会立即返回之前的计算结果,而不是再次执行函数;而普通的 function 调用多少次就执行多少次,每调用一次就计算一次。

至于为何要如此设计,官网文档也给出了原因:

为什么需要缓存?假设有一个性能开销比较大的计算数据 list,它需要遍历一个巨大的数组并做大量的计算。然后可能有其他的计算数据依赖于 list。如果没有缓存,将不可避免的多次执行 list 的 getter!如果不希望有缓存,请用 function 来替代。

TIP

在这部分内容里,将官方文档的一些用词做了更换,比如把 method 都替换成了 function ,也把 “计算属性” 都换成了 “计算数据”,原因在于官网很多地方是基于 Options API 的写法去描述,而本文档是基于 Composition API 。

点击了解: 如何理解 JavaScript 中方法(method)和函数(function)的区别?

  1. 书写统一

假定 foo1 是 Ref 变量, foo2 是 computed 变量, foo3 是普通函数返回值

看到这里的开发者应该都已经清楚 Vue 3 的 Ref 变量是通过 foo1.value 来拿到值的,而 computed 也是通过 foo2.value ,并且在 template 里都可以省略 .value ,在读取方面,他们是有一致的风格和简洁性。

而 foo3 不管是在 script 还是 template ,都需要通过 foo3() 才能拿到结果,相对来说会有那么一丢丢别扭。

当然,关于这一点,如果涉及到的数据不是响应式数据,那么还是老老实实的用函数返回值吧,原因请见下面的 注意事项

注意事项

有优势当然也就有一定的 “劣势” ,当然这也是 Vue 框架的有意为之,所以在使用上也需要注意一些问题:

  1. 只会更新响应式数据的计算

假设要获取当前的时间信息,因为不是响应式数据,所以这种情况下就需要用普通的函数去获取返回值,才能拿到最新的时间。

ts
const nowTime = computed(() => new Date())
+console.log(nowTime.value)
+// 输出 Sun Nov 14 2021 21:07:00 GMT+0800 (GMT+08:00)
+
+// 2s 后依然是跟上面一样的结果
+setTimeout(() => {
+  console.log(nowTime.value)
+  // 还是输出 Sun Nov 14 2021 21:07:00 GMT+0800 (GMT+08:00)
+}, 2000)
  1. 数据是只读的

通过 computed 定义的数据,它是只读的,这一点在 类型声明 已经有所了解。

如果直接赋值,不仅无法变更数据,而且会收获一个报错。

bash
TS2540: Cannot assign to 'value' because it is a read-only property.

虽然无法直接赋值,但是在必要的情况下,依然可以通过 computedsetter 来更新数据。

点击了解:computed 的 setter 用法

setter 的使用

通过 computed 定义的变量默认都是只读的形式(只有一个 getter ),但是在必要的情况下,也可以使用其 setter 属性来更新数据。

基本格式

当需要用到 setter 的时候, computed 就不再是一个传入 callback 的形式了,而是传入一个带有 2 个方法的对象。

ts
// 注意这里computed接收的入参已经不再是函数
+const foo = computed({
+  // 这里需要明确的返回一个值
+  get() {
+    // ...
+  },
+  // 这里接收一个参数,代表修改 foo 时,赋值下来的新值
+  set(newValue) {
+    // ...
+  },
+})

这里的 get 就是 computed 的 getter ,跟原来传入 callback 的形式一样,是用于 foo.value 的读取,所以这里必须有明确的返回值。

这里的 set 就是 computed 的 setter ,它会接收一个参数,代表新的值,当通过 foo.value = xxx 赋值的时候,赋入的这个值,就会通过这个入参来传递进来,可以根据的业务需要,把这个值,赋给相关的数据源。

TIP

请注意,必须使用 getset 这 2 个方法名,也只接受这 2 个方法。

在了解了基本格式后,可以查看下面的例子来了解具体的用法。

使用示范

官网的 例子 是一个 Options API 的案例,这里改成 Composition API 的写法来演示:

ts
// 还是这2个数据源
+const firstName = ref<string>('Bill')
+const lastName = ref<string>('Gates')
+
+// 这里配合setter的需要,改成了另外一种写法
+const fullName = computed({
+  // getter还是返回一个拼接起来的全名
+  get() {
+    return \`\${firstName.value} \${lastName.value}\`
+  },
+  // setter这里改成只更新firstName,注意参数也定义TS类型
+  set(newFirstName: string) {
+    firstName.value = newFirstName
+  },
+})
+console.log(fullName.value) // 输出 Bill Gates
+
+// 2s后更新一下数据
+setTimeout(() => {
+  // 对fullName的赋值,其实更新的是firstName
+  fullName.value = 'Petter'
+
+  // 此时firstName已经得到了更新
+  console.log(firstName.value) // 会输出 Petter
+
+  // 当然,由于firstName变化了,所以fullName的getter也会得到更新
+  console.log(fullName.value) // 会输出 Petter Gates
+}, 2000)

应用场景

计算 API 的作用,官网文档只举了一个非常简单的例子,那么在实际项目中,什么情况下用它会让更方便呢?

简单举几个比较常见的例子吧,加深一下对 computed 的理解。

数据的拼接和计算

如上面的案例,与其每个用到的地方都要用到 firstName + ' ' + lastName 这样的多变量拼接,不如用一个 fullName 来的简单。

当然,不止是字符串拼接,数据的求和等操作更是合适,比如说做一个购物车,购物车里有商品列表,同时还要显示购物车内的商品总金额,这种情况就非常适合用计算数据。

复用组件的动态数据

在一个项目里,很多时候组件会涉及到复用,比如说:“首页的文章列表 vs 列表页的文章列表 vs 作者详情页的文章列表” ,特别常见于新闻网站等内容资讯站点,这种情况下,往往并不需要每次都重新写 UI 、数据渲染等代码,仅仅是接口 URL 的区别。

这种情况就可以通过路由名称来动态获取要调用哪个列表接口:

ts
const route = useRoute()
+
+// 定义一个根据路由名称来获取接口URL的计算数据
+const apiUrl = computed(() => {
+  switch (route.name) {
+    // 首页
+    case 'home':
+      return '/api/list1'
+    // 列表页
+    case 'list':
+      return '/api/list2'
+    // 作者页
+    case 'author':
+      return '/api/list3'
+    // 默认是随机列表
+    default:
+      return '/api/random'
+  }
+})
+
+// 请求列表
+const getArticleList = async (): Promise<void> => {
+  // ...
+  articleList.value = await axios({
+    method: 'get',
+    url: apiUrl.value,
+    // ...
+  })
+  // ...
+}

当然,这种情况也可以在父组件通过 props 传递接口 URL ,如果已经学到了 组件通讯 一章的话。

获取多级对象的值

应该很经常的遇到要在 template 显示一些多级对象的字段,但是有时候又可能存在某些字段不一定有,需要做一些判断的情况,虽然有 v-if ,但是嵌套层级一多,模板代码会难以维护。

如果把这些工作量转移给计算数据,结合 try / catch ,这样就无需在 template 里处理很多判断了。

ts
// 例子比较极端,但在 Vuex 这种大型数据树上,也不是完全不可能存在
+const foo = computed(() => {
+  // 正常情况下返回需要的数据
+  try {
+    return store.state.foo3.foo2.foo1.foo
+  } catch (e) {
+    // 处理失败则返回一个默认值
+    return ''
+  }
+})

这样在 template 里要拿到 foo 的值,完全不需要关心中间一级又一级的字段是否存在,只需要区分是不是默认值。

不同类型的数据转换

有时候会遇到一些需求类似于,让用户在输入框里,按一定的格式填写文本,比如用英文逗号 , 隔开每个词,然后保存的时候,是用数组的格式提交给接口。

这个时候 computed 的 setter 就可以妙用了,只需要一个简单的 computed ,就可以代替 inputchange 事件或者 watch 侦听,可以减少很多业务代码的编写。

vue
<template>
+  <input
+    type="text"
+    v-model="tagsStr"
+    placeholder="请输入标签,多个标签用英文逗号隔开"
+  />
+</template>
+
+<script lang="ts">
+import { defineComponent, computed, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 这个是最终要用到的数组
+    const tags = ref<string[]>([])
+
+    // 因为input必须绑定一个字符串
+    const tagsStr = computed({
+      // 所以通过getter来转成字符串
+      get() {
+        return tags.value.join(',')
+      },
+      // 然后在用户输入的时候,切割字符串转换回数组
+      set(newValue: string) {
+        tags.value = newValue.split(',')
+      },
+    })
+
+    return {
+      tagsStr,
+    }
+  },
+})
+</script>

所以在实际业务开发中,开发者可以多考虑一下是否可以使用 computed 代替 watch ,避免过多的数组侦听带来项目性能的下降。

指令

指令是 Vue 模板语法里的特殊标记,在使用上和 HTML 的 data-* 属性十分相似,统一以 v- 开头( e.g. v-html )。

它以简单的方式实现了常用的 JavaScript 表达式功能,当表达式的值改变的时候,响应式地作用到 DOM 上。

内置指令

Vue 提供了一些内置指令可以直接使用,例如:

vue
<template>
+  <!-- 渲染一段文本 -->
+  <span v-text="msg"></span>
+
+  <!-- 渲染一段 HTML -->
+  <div v-html="html"></div>
+
+  <!-- 循环创建一个列表 -->
+  <ul v-if="items.length">
+    <li v-for="(item, index) in items" :key="index">
+      <span>{{ item }}</span>
+    </li>
+  </ul>
+
+  <!-- 一些事件( \`@\` 等价于 \`v-on\` ) -->
+  <button @click="hello">Hello</button>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const msg = ref<string>('Hello World!')
+    const html = ref<string>('<p>Hello World!</p>')
+    const items = ref<string[]>(['a', 'b', 'c', 'd'])
+
+    function hello() {
+      console.log(msg.value)
+    }
+
+    return {
+      msg,
+      html,
+      items,
+      hello,
+    }
+  },
+})
+</script>

内置指令在使用上都非常的简单,可以在官方文档的 内置指令 一章查询完整的指令列表和用法,在模板上使用时,请了解 指令的模板语法

TIP

有两个指令可以使用别名:

  • v-on 的别名是 @ ,使用 @click 等价于 v-on:click
  • v-bind 的别名是 : ,使用 :src 等价于 v-bind:src

自定义指令 ~new

如果 Vue 的内置指令不能满足业务需求,还可以开发自定义指令。

相关的 TS 类型

在开始编写代码之前,先了解一下自定义指令相关的 TypeScript 类型。

自定义指令有两种实现形式,一种是作为一个对象,其中的写法比较接近于 Vue 组件,除了 getSSRPropsdeep 选项 外,其他的每一个属性都是一个 钩子函数 ,下一小节会介绍钩子函数的内容。

ts
// 对象式写法的 TS 类型
+// ...
+export declare interface ObjectDirective<T = any, V = any> {
+  created?: DirectiveHook<T, null, V>
+  beforeMount?: DirectiveHook<T, null, V>
+  mounted?: DirectiveHook<T, null, V>
+  beforeUpdate?: DirectiveHook<T, VNode<any, T>, V>
+  updated?: DirectiveHook<T, VNode<any, T>, V>
+  beforeUnmount?: DirectiveHook<T, null, V>
+  unmounted?: DirectiveHook<T, null, V>
+  getSSRProps?: SSRDirectiveHook
+  deep?: boolean
+}
+// ...

另外一种是函数式写法,只需要定义成一个函数,但这种写法只在 mountedupdated 这两个钩子生效,并且触发一样的行为。

ts
// 函数式写法的 TS 类型
+// ...
+export declare type FunctionDirective<T = any, V = any> = DirectiveHook<
+  T,
+  any,
+  V
+>
+// ...

这是每个钩子函数对应的类型,它有 4 个入参:

ts
// 钩子函数的 TS 类型
+// ...
+export declare type DirectiveHook<
+  T = any,
+  Prev = VNode<any, T> | null,
+  V = any
+> = (
+  el: T,
+  binding: DirectiveBinding<V>,
+  vnode: VNode<any, T>,
+  prevVNode: Prev
+) => void
+// ...

钩子函数第二个参数的类型:

ts
// 钩子函数第二个参数的 TS 类型
+// ...
+export declare interface DirectiveBinding<V = any> {
+  instance: ComponentPublicInstance | null
+  value: V
+  oldValue: V | null
+  arg?: string
+  modifiers: DirectiveModifiers
+  dir: ObjectDirective<any, V>
+}
+// ...

可以看到自定义指令最核心的就是 “钩子函数” 了,接下来来了解这部分的知识点。

钩子函数

组件的生命周期 类似,自定义指令里的逻辑代码也有一些特殊的调用时机,在这里称之为钩子函数:

钩子函数调用时机
created在绑定元素的 attribute 或事件侦听器被应用之前调用
beforeMount当指令第一次绑定到元素并且在挂载父组件之前调用
mounted在绑定元素的父组件被挂载后调用
beforeUpdate在更新包含组件的 VNode 之前调用
updated在包含组件的 VNode 及其子组件的 VNode 更新后调用
beforeUnmount在卸载绑定元素的父组件之前调用
unmounted当指令与元素解除绑定且父组件已卸载时,只调用一次

TIP

因为自定义指令的默认写法是一个对象,所以在代码风格上是遵循 Options API 的生命周期命名,而非 Vue 3 的 Composition API 风格。

钩子函数在用法上就是这样子:

ts
const myDirective = {
+  created(el, binding, vnode, prevVnode) {
+    // ...
+  },
+  mounted(el, binding, vnode, prevVnode) {
+    // ...
+  },
+  // 其他钩子...
+}

相关的 TS 类型 已了解,每个钩子函数都有 4 个入参:

参数作用
el指令绑定的 DOM 元素,可以直接操作它
binding一个对象数据,见下方的单独说明
vnodeel 对应在 Vue 里的虚拟节点信息
prevVNodeUpdate 时的上一个虚拟节点信息,仅在 beforeUpdateupdated 可用

其中用的最多是 elbinding 了。

  • el 的值就是通过 document.querySelector 拿到的那个 DOM 元素。

  • binding 是一个对象,里面包含了以下属性:

属性作用
value传递给指令的值,例如 v-foo="bar" 里的 bar ,支持任意有效的 JS 表达式
oldValue指令的上一个值,仅对 beforeUpdateupdated 可用
arg传给指令的参数,例如 v-foo:bar 里的 bar
modifiers传给指令的修饰符,例如 v-foo.bar 里的 bar
instance使用指令的组件实例
dir指令定义的对象(就是上面的 const myDirective = { /* ... */ } 这个对象)

在了解了指令的写法和参数作用之后,来看看如何注册一个自定义指令。

局部注册

自定义指令可以在单个组件内定义并使用,通过和 setup 函数 同级别的 directives 选项进行定义,可以参考下面的例子和注释:

vue
<template>
+  <!-- 这个使用默认值 \`unset\` -->
+  <div v-highlight>{{ msg }}</div>
+
+  <!-- 这个使用传进去的黄色 -->
+  <div v-highlight="\`yellow\`">{{ msg }}</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  // 自定义指令在这里编写,和 \`setup\` 同级别
+  directives: {
+    // \`directives\` 下的每个字段名就是指令名称
+    highlight: {
+      // 钩子函数
+      mounted(el, binding) {
+        el.style.backgroundColor =
+          typeof binding.value === 'string' ? binding.value : 'unset'
+      },
+    },
+  },
+  setup() {
+    const msg = ref<string>('Hello World!')
+
+    return {
+      msg,
+    }
+  },
+})
+</script>

上面是对象式的写法,也可以写成函数式:

ts
export default defineComponent({
+  directives: {
+    highlight(el, binding) {
+      el.style.backgroundColor =
+        typeof binding.value === 'string' ? binding.value : 'unset'
+    },
+  },
+})

TIP

局部注册的自定义指令,默认在子组件内生效,子组件内无需重新注册即可使用父组件的自定义指令。

全局注册

自定义指令也可以注册成全局,这样就无需在每个组件里定义了,只要在入口文件 main.ts 里启用它,任意组件里都可以使用自定义指令。

请查看 开发本地 Vue 专属插件 一节的内容了解如何注册一个全局的自定义指令插件。

deep 选项

除了 钩子函数 ,在 相关的 TS 类型 里还可以看到有一个 deep 选项,它是一个布尔值,作用是:

如果自定义指令用于一个有嵌套属性的对象,并且需要在嵌套属性更新的时候触发 beforeUpdateupdated 钩子,那么需要将这个选项设置为 true 才能够生效。

vue
<template>
+  <div v-foo="foo"></div>
+</template>
+
+<script lang="ts">
+import { defineComponent, reactive } from 'vue'
+
+export default defineComponent({
+  directives: {
+    foo: {
+      beforeUpdate(el, binding) {
+        console.log('beforeUpdate', binding)
+      },
+      updated(el, binding) {
+        console.log('updated', binding)
+      },
+      mounted(el, binding) {
+        console.log('mounted', binding)
+      },
+      // 需要设置为 \`true\` ,如果是 \`false\` 则不会触发
+      deep: true,
+    },
+  },
+  setup() {
+    // 定义一个有嵌套属性的对象
+    const foo = reactive({
+      bar: {
+        baz: 1,
+      },
+    })
+
+    // 2s 后修改其中一个值,会触发 \`beforeUpdate\` 和 \`updated\`
+    setTimeout(() => {
+      foo.bar.baz = 2
+      console.log(foo)
+    }, 2000)
+
+    return {
+      foo,
+    }
+  },
+})
+</script>

插槽

Vue 在使用子组件的时候,子组件在 template 里类似一个 HTML 标签,可以在这个子组件标签里传入任意模板代码以及 HTML 代码,这个功能就叫做 “插槽” 。

默认插槽

默认情况下,子组件使用 <slot /> 标签即可渲染父组件传下来的插槽内容,例如:

在父组件这边:

vue
<template>
+  <Child>
+    <!-- 注意这里,子组件标签里面传入了 HTML 代码 -->
+    <p>这是插槽内容</p>
+  </Child>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+import Child from '@cp/Child.vue'
+
+export default defineComponent({
+  components: {
+    Child,
+  },
+})
+</script>

在子组件这边:

vue
<template>
+  <slot />
+</template>

默认插槽非常简单,一个 <slot /> 就可以了。

具名插槽

有时候可能需要指定多个插槽,例如一个子组件里有 “标题” 、 “作者”、 “内容” 等预留区域可以显示对应的内容,这时候就需要用到具名插槽来指定不同的插槽位。

子组件通过 name 属性来指定插槽名称:

vue
<template>
+  <!-- 显示标题的插槽内容 -->
+  <div class="title">
+    <slot name="title" />
+  </div>
+
+  <!-- 显示作者的插槽内容 -->
+  <div class="author">
+    <slot name="author" />
+  </div>
+
+  <!-- 其他插槽内容放到这里 -->
+  <div class="content">
+    <slot />
+  </div>
+</template>

父组件通过 template 标签绑定 v-slot:name 格式的属性,来指定传入哪个插槽里:

vue
<template>
+  <Child>
+    <!-- 传给标题插槽 -->
+    <template v-slot:title>
+      <h1>这是标题</h1>
+    </template>
+
+    <!-- 传给作者插槽 -->
+    <template v-slot:author>
+      <h1>这是作者信息</h1>
+    </template>
+
+    <!-- 传给默认插槽 -->
+    <p>这是插槽内容</p>
+  </Child>
+</template>

v-slot:name 有一个别名 #name 语法,上面父组件的代码也相当于:

vue
<template>
+  <Child>
+    <!-- 传给标题插槽 -->
+    <template #title>
+      <h1>这是标题</h1>
+    </template>
+
+    <!-- 传给作者插槽 -->
+    <template #author>
+      <h1>这是作者信息</h1>
+    </template>
+
+    <!-- 传给默认插槽 -->
+    <p>这是插槽内容</p>
+  </Child>
+</template>

TIP

在使用具名插槽的时候,子组件如果不指定默认插槽,那么在具名插槽之外的内容将不会被渲染。

默认内容

可以给 slot 标签添加内容,例如 <slot>默认内容</slot> ,当父组件没有传入插槽内容时,会使用默认内容来显示,默认插槽和具名插槽均支持该功能。

注意事项

有一条规则需要记住:

  • 父组件里的所有内容都是在父级作用域中编译的
  • 子组件里的所有内容都是在子作用域中编译的

CSS 样式与预处理器

Vue 组件的 CSS 样式部分,Vue 3 保留着和 Vue 2 完全一样的写法。

编写组件样式表

最基础的写法,就是在 .vue 文件里添加一个 <style /> 标签,即可在里面写 CSS 代码了。

vue
<template>
+  <div>
+    <!-- HTML 代码 -->
+  </div>
+</template>
+
+<script lang="ts">
+  // TypeScript 代码
+</script>
+
+<style>
+/* CSS 代码 */
+.msg {
+  width: 100%;
+}
+.msg p {
+  color: #333;
+  font-size: 14px;
+}
+</style>

动态绑定 CSS

动态绑定 CSS ,在 Vue 2 就已经存在了,在此之前常用的是 :class:style ,现在在 Vue 3 ,还可以通过 v-bind 来动态修改了。

使用 :class 动态修改样式名

它是绑定在 DOM 元素上面的一个属性,跟 class="class-name" 这样的属性同级别,它非常灵活!

TIP

使用 :class 是用来动态修改样式名,也就意味着必须提前把样式名对应的样式表先写好!

假设已经提前定义好了这几个变量:

vue
<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const activeClass = 'active-class'
+    const activeClass1 = 'active-class1'
+    const activeClass2 = 'active-class2'
+    const isActive = true
+
+    return {
+      activeClass,
+      activeClass1,
+      activeClass2,
+      isActive,
+    }
+  },
+})
+</script>

如果只想绑定一个单独的动态样式,可以传入一个字符串:

vue
<template>
+  <p :class="activeClass">Hello World!</p>
+</template>

如果有多个动态样式,也可以传入一个数组:

vue
<template>
+  <p :class="[activeClass1, activeClass2]">Hello World!</p>
+</template>

还可以对动态样式做一些判断,这个时候传入一个对象:

vue
<template>
+  <p :class="{ 'active-class': isActive }">Hello World!</p>
+</template>

多个判断的情况下,记得也用数组套起来:

vue
<template>
+  <p :class="[{ activeClass1: isActive }, { activeClass2: !isActive }]">
+    Hello World!
+  </p>
+</template>

那么什么情况下会用到 :class 呢?

最常见的场景,应该就是导航、选项卡了,比如要给一个当前选中的选项卡做一个突出高亮的状态,那么就可以使用 :class 来动态绑定一个样式。

vue
<template>
+  <ul class="list">
+    <li
+      class="item"
+      :class="{ cur: index === curIndex }"
+      v-for="(item, index) in 5"
+      :key="index"
+      @click="curIndex = index"
+    >
+      {{ item }}
+    </li>
+  </ul>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const curIndex = ref<number>(0)
+
+    return {
+      curIndex,
+    }
+  },
+})
+</script>
+
+<style scoped>
+.cur {
+  color: red;
+}
+</style>

这样就简单实现了一个点击切换选项卡高亮的功能。

使用 :style 动态修改内联样式

如果觉得使用 :class 需要提前先写样式,再去绑定样式名有点繁琐,有时候只想简简单单的修改几个样式,那么可以通过 :style 来处理。

默认的情况下,都是传入一个对象去绑定:

  • key 是符合 CSS 属性名的 “小驼峰式” 写法,或者套上引号的短横线分隔写法(原写法),例如在 CSS 里,定义字号是 font-size ,那么需要写成 fontSize 或者 'font-size' 作为它的键。

  • value 是 CSS 属性对应的 “合法值”,比如要修改字号大小,可以传入 13px0.4rem 这种带合法单位字符串值,但不可以是 13 这样的缺少单位的值,无效的 CSS 值会被过滤不渲染。

vue
<template>
+  <p
+    :style="{
+      fontSize: '13px',
+      'line-height': 2,
+      color: '#ff0000',
+      textAlign: 'center',
+    }"
+  >
+    Hello World!
+  </p>
+</template>

如果有些特殊场景需要绑定多套 style,需要在 script 先定义好各自的样式变量(也是符合上面说到的那几个要求的对象),然后通过数组来传入:

vue
<template>
+  <p :style="[style1, style2]">Hello World!</p>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const style1 = {
+      fontSize: '13px',
+      'line-height': 2,
+    }
+    const style2 = {
+      color: '#ff0000',
+      textAlign: 'center',
+    }
+
+    return {
+      style1,
+      style2,
+    }
+  },
+})
+</script>

使用 v-bind 动态修改 style ~new

当然,以上两种形式都是关于 <script /><template /> 部分的操作,如果觉得会给模板带来一定的维护成本的话,不妨考虑这个新方案,将变量绑定到 <style /> 部分去。

TIP

请注意这是一个在 3.2.0 版本之后才被归入正式队列的新功能!如果需要使用它,请确保的 vue 的版本号在 3.2.0 以上,最好是保持最新版本。

先来看看基本的用法:

vue
<template>
+  <p class="msg">Hello World!</p>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const fontColor = ref<string>('#ff0000')
+
+    return {
+      fontColor,
+    }
+  },
+})
+</script>
+
+<style scoped>
+.msg {
+  color: v-bind(fontColor);
+}
+</style>

如上面的代码,将渲染出一句红色文本的 Hello World!

这其实是利用了现代浏览器支持的 CSS 变量来实现的一个功能(所以如果打算用它的话,需要提前注意一下兼容性噢,点击查看:CSS Variables 兼容情况 )。

它渲染到 DOM 上,其实也是通过绑定 style 来实现,可以看到渲染出来的样式是:

html
<p class="msg" data-v-7eb2bc79="" style="--7eb2bc79-fontColor:#ff0000;">
+  Hello World!
+</p>

对应的 CSS 变成了:

css
.msg[data-v-7eb2bc79] {
+  color: var(--7eb2bc79-fontColor);
+}

理论上 v-bind 函数可以在 Vue 内部支持任意的 JavaScript 表达式,但由于可能包含在 CSS 标识符中无效的字符,因此官方是建议在大多数情况下,用引号括起来,如:

css
.text {
+  font-size: v-bind('theme.font.size');
+}

由于 CSS 变量的特性,因此对 CSS 响应式属性的更改不会触发模板的重新渲染(这也是和 :class:style 的最大不同)。

TIP

不管有没有开启 <style scoped> ,使用 v-bind 渲染出来的 CSS 变量,都会带上 scoped 的随机 hash 前缀,避免样式污染(永远不会意外泄漏到子组件中),所以请放心使用!

如果对 CSS 变量的使用还不是很了解的话,可以先阅读一下相关的基础知识点。

相关阅读:使用 CSS 自定义属性(变量) - MDN

样式表的组件作用域

CSS 不像 JS ,是没有作用域的概念的,一旦写了某个样式,直接就是全局污染。所以 BEM 命名法 等规范才应运而生。

但在 Vue 组件里,有两种方案可以避免出现这种污染问题:一个是 Vue 2 就有的 <style scoped> ,一个是 Vue 3 新推出的 <style module>

Style Scoped

Vue 组件在设计的时候,就想到了一个很优秀的解决方案,通过 scoped 来支持创建一个 CSS 作用域,使这部分代码只运行在这个组件渲染出来的虚拟 DOM 上。

使用方式很简单,只需要在 <style /> 上添加 scoped 属性:

vue
<!-- 注意这里多了一个 \`scoped\` -->
+<style scoped>
+.msg {
+  width: 100%;
+}
+.msg p {
+  color: #333;
+  font-size: 14px;
+}
+</style>

编译后,虚拟 DOM 都会带有一个 data-v-xxxxx 这样的属性,其中 xxxxx 是一个随机生成的 Hash ,同一个组件的 Hash 是相同并且唯一的:

html
<div class="msg" data-v-7eb2bc79>
+  <p data-v-7eb2bc79>Hello World!</p>
+</div>

而 CSS 则也会带上与 HTML 相同的属性,从而达到样式作用域的目的。

css
.msg[data-v-7eb2bc79] {
+  width: 100%;
+}
+.msg p[data-v-7eb2bc79] {
+  color: #333;
+  font-size: 14px;
+}

使用 scoped 可以有效的避免全局样式污染,可以在不同的组件里面都使用相同的 className,而不必担心会相互覆盖,不必再定义很长很长的样式名来防止冲突了。

TIP

添加 scoped 生成的样式,只作用于当前组件中的元素,并且权重高于全局 CSS ,可以覆盖全局样式

Style Module ~new

这是在 Vue 3 才推出的一个新方案,和 <style scoped> 不同,scoped 是通过给 DOM 元素添加自定义属性的方式来避免冲突,而 <style module> 则更为激进,将会编译成 CSS Modules

对于 CSS Modules 的处理方式,也可以通过一个小例子来更直观的了解它:

css
/* 案例来自阮一峰老师的博文《CSS Modules 用法教程》 */
+/* https://www.ruanyifeng.com/blog/2016/06/css_modules.html */
+
+/* 编译前 */
+.title {
+  color: red;
+}
+
+/* 编译后 */
+._3zyde4l1yATCOkgn-DBWEL {
+  color: red;
+}

可以看出,是通过比较 “暴力” 的方式,把编写的 “好看的” 样式名,直接改写成一个随机 Hash 样式名,来避免样式互相污染。

所以回到 Vue 这边,看看 <style module> 是怎么操作的。

vue
<template>
+  <p :class="$style.msg">Hello World!</p>
+</template>
+
+<style module>
+.msg {
+  color: #ff0000;
+}
+</style>

于是,将渲染出一句红色文本的 Hello World!

TIP

  1. 使用这个方案,需要了解如何 使用 :class 动态修改样式名

  2. 如果单纯只使用 <style module> ,那么在绑定样式的时候,是默认使用 $style 对象来操作的

  3. 必须显示的指定绑定到某个样式,比如 $style.msg ,才能生效

  4. 如果单纯的绑定 $style ,并不能得到 “把全部样式名直接绑定” 的期望结果

  5. 如果指定的 className 是短横杆命名,比如 .user-name ,那么需要通过 $style['user-name'] 去绑定

也可以给 module 进行命名,然后就可以通过命名的 “变量名” 来操作:

vue
<template>
+  <p :class="classes.msg">Hello World!</p>
+</template>
+
+<style module="classes">
+.msg {
+  color: #ff0000;
+}
+</style>

TIP

需要注意的一点是,一旦开启 <style module> ,那么在 <style module> 里所编写的样式都必须手动绑定才能生效,没有被绑定的样式虽然也会被编译,但不会主动生效到 DOM 上。

原因是编译出来的样式名已经变化,而原来的 DOM 未指定对应的样式名,或者指定的是编译前的命名,所以并不能匹配到正确的样式。

useCssModule ~new

这是一个全新的 API ,面向在 script 部分操作 CSS Modules 。

在上面的 CSS Modules 部分可以知道,可以在 style 定义好样式,然后在 template 部分通过变量名来绑定样式。

那么如果有一天有个需求,需要通过 v-html 来渲染 HTML 代码,那这里的样式岂不是凉凉了?当然不会!

Vue 3 提供了一个 Composition API useCssModule 来帮助在 setup 函数里操作的 CSS Modules (对,只能在 setup 或者 script setup 里使用)。

基本用法:

绑定多几个样式,再来操作:

vue
<template>
+  <p :class="$style.msg">
+    <span :class="$style.text">Hello World!</span>
+  </p>
+</template>
+
+<script lang="ts">
+import { defineComponent, useCssModule } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const style = useCssModule()
+    console.log(style)
+  },
+})
+</script>
+
+<style module>
+.msg {
+  color: #ff0000;
+}
+.text {
+  font-size: 14px;
+}
+</style>

可以看到打印出来的 style 是一个对象:

  • key 是在 <style modules> 里定义的原始样式名

  • value 则是编译后的新样式名

js
{
+  msg: 'home_msg_37Xmr',
+  text: 'home_text_2woQJ'
+}

所以来配合 模板字符串 的使用,看看刚刚说的,要通过 v-html 渲染出来的内容应该如何绑定样式:

vue
<template>
+  <div v-html="content"></div>
+</template>
+
+<script lang="ts">
+import { defineComponent, useCssModule } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 获取样式
+    const style = useCssModule()
+
+    // 编写模板内容
+    const content = \`<p class="\${style.msg}">
+      <span class="\${style.text}">Hello World! —— from v-html</span>
+    </p>\`
+
+    return {
+      content,
+    }
+  },
+})
+</script>
+
+<style module>
+.msg {
+  color: #ff0000;
+}
+.text {
+  font-size: 14px;
+}
+</style>

是不是也非常简单?可能刚开始不太习惯,但写多几次其实也蛮好玩的这个功能!

另外,需要注意的是,如果是指定了 modules 的名称,那么必须传入对应的名称作为入参才可以正确拿到这些样式:

比如指定了一个 classes 作为名称:

vue
<style module="classes">
+/* ... */
+</style>

那么需要通过传入 classes 这个名称才能拿到样式,否则会是一个空对象:

ts
const style = useCssModule('classes')

TIP

const style = useCssModule() 的时候,命名是随意的,跟在 <style module="classes"> 这里指定的命名没有关系。

深度操作符 ~new

样式表的组件作用域 部分了解到,使用 scoped 后,父组件的样式将不会渗透到子组件中,但也不能直接修改子组件的样式。

如果确实需要进行修改子组件的样式,必须通过 ::v-deep(完整写法) 或者 :deep(快捷写法) 操作符来实现。

TIP

  1. 旧版的深度操作符是 >>>/deep/::v-deep,现在 >>>/deep/ 已进入弃用阶段(虽然暂时还没完全移除)

  2. 同时需要注意的是,旧版 ::v-deep 的写法是作为组合器的方式,写在样式或者元素前面,如:::v-deep .class-name { /* ... */ },现在这种写法也废弃了。

现在不论是 ::v-deep 还是 :deep ,使用方法非常统一,来假设 .b 是子组件的样式名:

vue
<style scoped>
+.a :deep(.b) {
+  /* ... */
+}
+</style>

编译后:

css
.a[data-v-f3f3eg9] .b {
+  /* ... */
+}

TIP

可以看到,新的 deep 写法是作为一个类似 JS “函数” 那样去使用,需要深度操作的样式或者元素名,作为 “入参” 去传入。

同理,如果使用 Less 或者 Stylus 这种支持嵌套写法的预处理器,也是可以这样去深度操作的:

less
.a {
+  :deep(.b) {
+    /* ... */
+  }
+}

另外,除了操作子组件的样式,那些通过 v-html 创建的 DOM 内容,也不受作用域内的样式影响,也可以通过深度操作符来实现样式修改。

使用 CSS 预处理器

在工程化的现在,可以说前端都几乎不写 CSS 了,都是通过 sasslessstylus 等 CSS 预处理器来完成样式的编写。

为什么要用 CSS 预处理器?放一篇关于三大预处理器的点评,新开发者可以做个简单了解,具体的用法在对应的官网上有非常详细的说明。

可以查看了解:浅谈 CSS 预处理器,Sass、Less 和 Stylus

在 Vue 组件里使用预处理器非常简单,像 Vite 已内置了对预处理器文件的支持(可处理 .less.scss 之类的预处理器扩展名文件),因此只需要安装对应的依赖到项目里。

这里以 Less 为例,先安装该预处理器:

bash
# 因为是在开发阶段使用,所以添加到 \`devDependencies\`
+npm i -D less

接下来在 Vue 组件里,只需要在 <style /> 标签上,通过 lang="less" 属性指定使用哪个预处理器,即可直接编写对应的代码:

vue
<style lang="less" scoped>
+// 定义颜色变量
+@color-black: #333;
+@color-red: #ff0000;
+
+// 父级标签
+.msg {
+  width: 100%;
+  // 其子标签可以使用嵌套写法
+  p {
+    color: @color-black;
+    font-size: 14px;
+    // 支持多级嵌套
+    span {
+      color: @color-red;
+    }
+  }
+}
+</style>

编译后的 css 代码:

css
.msg {
+  width: 100%;
+}
+.msg p {
+  color: #333333;
+  font-size: 14px;
+}
+.msg p span {
+  color: #ff0000;
+}

预处理器也支持 scoped,用法请查阅 样式表的组件作用域 部分。

`,729);function C(i,A,d,u,g,h){const l=n("ReadingTips"),p=n("GitalkComment"),o=n("ClientOnly");return t(),c("div",null,[F,s(l),D,s(o,null,{default:r(()=>[s(p,{issueId:46})]),_:1})])}const E=e(y,[["render",C]]);export{m as __pageData,E as default}; diff --git a/assets/efficient.md.a41d0892.js b/assets/efficient.md.a41d0892.js new file mode 100644 index 00000000..cac3dc46 --- /dev/null +++ b/assets/efficient.md.a41d0892.js @@ -0,0 +1,552 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as p,v as o,b as e,t as s,O as t,R as c,M as n}from"./chunks/framework.0d8bea05.js";const g=JSON.parse('{"title":"高效开发","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"efficient.md","filePath":"efficient.md"}'),r={name:"efficient.md"},y=c(`

高效开发

可能很多开发者(包括笔者)刚上手 Vue 3 的那段时间,都会觉得开发过程似乎变得更繁琐了, Vue 官方团队当然不会无视群众的呼声,如果基于脚手架和 .vue 文件开发,那么可以享受到更高效率的开发体验。

在阅读这一章之前,需要对 Vue 3 的单组件开发有一定的了解,如果还处于完全没有接触过的阶段,请先抽点时间阅读 单组件的编写 一章。

TIP

要体验以下新特性,请确保项目下 package.json 里的 vue 版本在 3.2.0 以上,最好同步 npm 上当前最新的 @latest 版本,否则可能出现 API 未定义等问题。

script-setup ~new

script-setup 是 Vue 3 组件的一个语法糖,旨在帮助开发者降低 setup 函数需要 return 的心智负担。

Vue 的 3.1.2 版本是针对 script-setup 的一个分水岭版本,自 3.1.4 开始 script-setup 进入定稿状态,部分旧的 API 已被舍弃,本章节内容将以最新的 API 为准进行整理说明,如果需要查阅旧版 API 的使用,请参阅笔者的 这篇开荒博文

script-setup 方案已在 Vue 3.2.0-beta.1 版本中脱离实验状态,正式进入 Vue 3 的队伍,此后所有的新版本均可以作为一个官方标准的开发方案使用。

新特性的产生背景

在了解 script-setup 怎么用之前,可以先了解一下推出该语法糖的一些开发背景,通过对比开发体验上的异同点,了解为什么会有这个新模式。

在 Vue 3 的组件标准写法里,如果数据和方法需要在 <template /> 里使用,都需要在 <script /> 的 setup 函数里 return 出来。

如果使用的是 TypeScript ,还需要借助 defineComponent 对 API 类型进行自动推导。

vue
<!-- 标准组件格式 -->
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // ...
+
+    return {
+      // ...
+    }
+  },
+})
+</script>

关于标准 setup 和 defineComponent 的说明和用法,可以查阅 全新的 setup 函数 一节。

script-setup 的推出是为了让熟悉 Vue 3 的开发者可以更高效率地开发组件,减少编码过程中的心智负担,只需要给 <script /> 标签添加一个 setup 属性,那么整个 <script /> 就直接会变成 setup 函数,所有顶级变量、函数,均会自动暴露给模板使用(无需再一个个 return 了)。

Vue 会通过单组件编译器,在编译的时候将其处理回标准组件,所以目前这个方案只适合用 .vue 文件写的工程化项目。

vue
<!-- 使用 script-setup 格式 -->
+<script setup lang="ts">
+// ...
+</script>

对,就是这样,代码量瞬间大幅度减少!

因为 script-setup 的大部分功能在书写上和标准版是一致的,因此下面的内容只提及有差异的写法。

全局编译器宏

在 script-setup 模式下,新增了 4 个全局编译器宏,他们无需 import 就可以直接使用。

但是默认的情况下直接使用,如果项目开启了 ESLint ,可能会提示 API 没有导入,但导入 API 后,控制台的 Vue 编译助手又会提示不需要导入,就很尴尬… 不过不用着急!可以通过配置 Lint 规则解决这个问题!

将这几个编译助手写进全局规则里,这样不导入也不会报错了。

js
// 项目根目录下的 .eslintrc.js
+module.exports = {
+  // ...
+  // 在原来的 Lint 规则后面,补充下面的 \`globals\` 选项
+  globals: {
+    defineProps: 'readonly',
+    defineEmits: 'readonly',
+    defineExpose: 'readonly',
+    withDefaults: 'readonly',
+  },
+}

关于几个宏的说明都在下面的文档部分有说明,也可以从这里导航过去直接查看。

说明
defineProps点击查看
defineEmits点击查看
defineExpose点击查看
withDefaults点击查看

下面继续了解 script-setup 的变化。

template 操作简化

如果使用 JSX / TSX 写法,这一点没有太大影响,但对于习惯使用 <template /> 的开发者来说,这是一个非常爽的体验。

主要体现在这两点:

  1. 变量无需进行 return
  2. 子组件无需手动注册

变量无需进行 return

标准组件模式下,变量和方法都需要在 setup 函数里 return 出去,才可以在 <template /> 部分拿到。

vue
<!-- 标准组件格式 -->
+<template>
+  <p>{{ msg }}</p>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const msg = 'Hello World!'
+
+    // 要给 \`<template />\` 用的数据需要 \`return\` 出去才可以
+    return {
+      msg,
+    }
+  },
+})
+</script>

在 script-setup 模式下,定义了就可以直接使用。

vue
<!-- 使用 script-setup 格式 -->
+<template>
+  <p>{{ msg }}</p>
+</template>
+
+<script setup lang="ts">
+const msg = 'Hello World!'
+</script>

子组件无需手动注册

子组件的挂载,在标准组件里的写法是需要 import 后再放到 components 里才能够启用:

vue
<!-- 标准组件格式 -->
+<template>
+  <Child />
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+// 导入子组件
+import Child from '@cp/Child.vue'
+
+export default defineComponent({
+  // 需要启用子组件作为模板
+  components: {
+    Child,
+  },
+
+  // 组件里的业务代码
+  setup() {
+    // ...
+  },
+})
+</script>

在 script-setup 模式下,只需要导入组件即可,编译器会自动识别并启用。

vue
<!-- 使用 script-setup 格式 -->
+<template>
+  <Child />
+</template>
+
+<script setup lang="ts">
+import Child from '@cp/Child.vue'
+</script>

props 的接收方式变化

由于整个 script 都变成了一个大的 setup 函数,没有了组件选项,也没有了 setup 的入参,所以没办法和标准写法一样去接收 props 了。

这里需要使用一个全新的 API :defineProps 。

defineProps 是一个方法,内部返回一个对象,也就是挂载到这个组件上的所有 props ,它和普通的 props 用法一样,如果不指定为 props ,则传下来的属性会被放到 attrs 那边去。

TIP

前置知识点:接收 props - 组件之间的通信

defineProps 的基础用法

如果只是单纯在 <template /> 里使用,那么这么简单定义就可以了:

ts
defineProps(['name', 'userInfo', 'tags'])

使用 string[] 数组作为入参,把 prop 的名称作为数组的 item 传给 defineProps 就可以了。

如果 <script /> 里的方法要拿到 props 的值,也可以使用字面量定义:

ts
const props = defineProps(['name', 'userInfo', 'tags'])
+console.log(props.name)

但作为 Vue 的老玩家,都清楚不显性的指定 props 的类型很容易在协作中引起程序报错,那么应该如何对每个 prop 进行类型检查呢?

有两种方式来处理类型定义:

  1. 通过构造函数检查 prop
  2. 使用类型注解检查 prop

通过构造函数检查 prop

这是第一种方式:使用 JavaScript 原生构造函数进行类型规定,也就是跟平时定义 prop 类型时一样, Vue 会通过 instanceof 来进行 类型检查

使用这种方法,需要通过一个 “对象” 入参来传递给 defineProps ,比如:

ts
defineProps({
+  name: String,
+  userInfo: Object,
+  tags: Array,
+})

所有原来 props 具备的校验机制,都可以适用,比如除了要限制类型外,还想指定 name 是可选,并且带有一个默认值:

ts
defineProps({
+  name: {
+    type: String,
+    required: false,
+    default: 'Petter',
+  },
+  userInfo: Object,
+  tags: Array,
+})

更多的 props 校验机制,可以点击 带有类型限制的 props可选以及带有默认值的 props 了解更多。

使用类型注解检查 prop

这是第二种方式:使用 TypeScript 的类型注解,和 ref 等 API 的用法一样,defineProps 也是可以使用尖括号 <> 来包裹类型定义,紧跟在 API 后面。

另外,由于 defineProps 返回的是一个对象(因为 props 本身是一个对象),所以尖括号里面的类型还要用大括号包裹,通过 key: value 的键值对形式表示,如:

ts
defineProps<{ name: string }>()

注意到了吗?这里使用的类型,和第一种方法提到的指定类型是不一样的。这里不再使用构造函数校验,而是需要遵循使用 TypeScript 的类型,比如字符串是 string,而不是 String

如果有多个 prop ,就跟写 interface 一样:

ts
defineProps<{
+  name: string
+  phoneNumber: number
+  userInfo: object
+  tags: string[]
+}>()

其中,举例里的 userInfo 是一个对象,可以简单的指定为 object,也可以先定义好它对应的类型,再进行指定:

ts
interface UserInfo {
+  id: number
+  age: number
+}
+
+defineProps<{
+  name: string
+  userInfo: UserInfo
+}>()

如果想对某个数据设置为可选,也是遵循 TS 规范,通过英文问号 ? 来允许可选:

ts
// name 是可选
+defineProps<{
+  name?: string
+  tags: string[]
+}>()

如果想设置可选参数的默认值,需要借助 withDefaults API。

WARNING

需要强调的一点是:在 构造函数类型注解 这两种校验方式只能二选一,不能同时使用,否则会引起程序报错。

withDefaults 的基础用法

这个新的 withDefaults API 可以让在使用 TS 类型系统时,也可以指定 props 的默认值。

它接收两个入参:

参数含义
props通过 defineProps 传入的 props
defaultValues根据 props 的 key 传入默认值

光看描述可能不容易理解,看看下面这段演示代码会更直观:

ts
withDefaults(
+  // 这是第一个参数,声明 props
+  defineProps<{
+    size?: number
+    labels?: string[]
+  }>(),
+  // 这是第二个参数,设置默认值
+  {
+    size: 3,
+    labels: () => ['default label'],
+  }
+)

也可以通过字面量获取 props :

ts
// 上面的写法可能比较复杂,存在阅读成本
+// 也可以跟平时一样先通过 interface 声明其类型
+interface Props {
+  size?: number
+  labels?: string[]
+}
+
+// 再作为 \`defineProps\` 的类型传入
+// 代码风格上会简洁很多
+const props = withDefaults(defineProps<Props>(), {
+  size: 3,
+  labels: () => ['default label'],
+})
+
+// 这样就可以通过 \`props\` 变量拿到需要的值
+console.log(props.size)

emits 的接收方式变化

和 props 一样,emits 的接收也是需要使用一个全新的 API 来操作,这个 API 就是 defineEmits 。

和 defineProps 一样, defineEmits 也是一个方法,它接受的入参格式和标准组件的要求是一致的。

TIP

前置知识点:接收 emits - 组件之间的通信

defineEmits 的基础用法

需要通过字面量来定义 emits ,最基础的用法也是传递一个 string[] 数组进来,把每个 emit 的名称作为数组的 item 。

ts
// 获取 emit
+const emit = defineEmits(['update-name'])
+
+// 调用 emit
+emit('update-name', 'Tom')

由于 defineEmits 的用法和原来的 emits 选项差别不大,这里也不重复说明更多的诸如校验之类的用法了,可以查看 接收 emits 一节了解更多。

attrs 的接收方式变化

attrs 和 props 很相似,也是基于父子通信的数据,如果父组件绑定下来的数据没有被指定为 props ,那么就会被 attrs 接收。

在标准组件里, attrs 的数据是通过 setup 的第二个入参 context 里的 attrs API 获取的。

ts
// 标准组件的写法
+export default defineComponent({
+  setup(props, { attrs }) {
+    // attrs 是个对象,每个 Attribute 都是它的 key
+    console.log(attrs.class)
+
+    // 如果传下来的 Attribute 带有短横线,需要通过这种方式获取
+    console.log(attrs['data-hash'])
+  },
+})

但和 props 一样,由于没有了 context 参数,需要使用一个新的 API 来拿到 attrs 数据,这个 API 就是 useAttrs 。

useAttrs 的基础用法

顾名思义, useAttrs 可以是用来获取 attrs 数据的,它的用法非常简单:

ts
import { useAttrs } from 'vue'
+
+// 获取 attrs
+const attrs = useAttrs()
+
+// attrs 是个对象,和 props 一样,需要通过 \`key\` 来得到对应的单个 attr
+console.log(attrs.msg)

对 attrs 不太了解的话,可以查阅 获取非 Prop 的 Attribute

slots 的接收方式变化

slots 是 Vue 组件的插槽数据,也是在父子通信里的一个重要成员。

对于使用 <template /> 的开发者来说,在 script-setup 里获取插槽数据并不困难,因为跟标准组件的写法是完全一样的,可以直接在 <template /> 里使用 <slot /> 标签渲染。

vue
<template>
+  <div>
+    <!-- 插槽数据 -->
+    <slot />
+    <!-- 插槽数据 -->
+  </div>
+</template>

但对使用 JSX / TSX 的开发者来说,就影响比较大了,在标准组件里,想在 script 里获取插槽数据,也是需要在 setup 的第二个入参里拿到 slots API 。

ts
// 标准组件的写法
+export default defineComponent({
+  // 这里的 slots 就是插槽
+  setup(props, { slots }) {
+    // ...
+  },
+})

新版本的 Vue 也提供了一个全新的 useSlots API 来帮助 script-setup 用户获取插槽。

useSlots 的基础用法

先来看看父组件,父组件先为子组件传入插槽数据,支持 “默认插槽” 和 “命名插槽” :

vue
<template>
+  <!-- 子组件 -->
+  <ChildTSX>
+    <!-- 默认插槽 -->
+    <p>Default slot for TSX.</p>
+    <!-- 默认插槽 -->
+
+    <!-- 命名插槽 -->
+    <template #msg>
+      <p>Named slot for TSX.</p>
+    </template>
+    <!-- 命名插槽 -->
+  </ChildTSX>
+  <!-- 子组件 -->
+</template>
+
+<script setup lang="ts">
+// 实际上是导入 ChildTSX.tsx 文件,扩展名默认可以省略
+import ChildTSX from '@cp/ChildTSX'
+</script>

在使用 JSX / TSX 编写的子组件里,就可以通过 useSlots 来获取父组件传进来的 slots 数据进行渲染:

tsx
// src/components/ChildTSX.tsx
+import { defineComponent, useSlots } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 获取插槽数据
+    const slots = useSlots()
+
+    // 渲染组件
+    return () => (
+      <div>
+        {/* 渲染默认插槽 */}
+        <p>{slots.default ? slots.default() : ''}</p>
+
+        {/* 渲染命名插槽 */}
+        <p>{slots.msg ? slots.msg() : ''}</p>
+      </div>
+    )
+  },
+})

请注意,这里的 TSX 组件代码需要使用 .tsx 作为文件扩展名,并且构建工具可能默认没有对 JSX / TSX 作支持,以 Vite 为例,需要安装官方提供的 JSX / TSX 支持插件才可以正常使用。

bash
# 该插件支持使用 JSX 或 TSX 作为 Vue 组件
+npm i -D @vitejs/plugin-vue-jsx

并在 vite.config.ts 里启用插件,添加对 JSX 和 TSX 的支持。

ts
// vite.config.ts
+import { defineConfig } from 'vite'
+import vueJsx from '@vitejs/plugin-vue-jsx'
+
+export default defineConfig({
+  // ...
+  plugins: [
+    // ...
+    // 启用插件
+    vueJsx(),
+  ],
+})

如果还存在报错的情况,可以检查项目下的 tsconfig.json 文件里,编译选项 jsx 是否设置为 preserve

json
{
+  "compilerOptions": {
+    "jsx": "preserve"
+  }
+}

ref 的通信方式变化

在标准组件写法里,子组件的数据和方法可以通过在 setup 里 return 出来给父组件调用,也就是父组件可以通过 childComponent.value.foo 这样的方式直接操作子组件的数据(参见:DOM 元素与子组件 - 响应式 API 之 ref)。

但在 script-setup 模式下,所有数据只是默认隐式 return 给 <template /> 使用,不会暴露到组件外,所以父组件是无法直接通过挂载 ref 变量获取子组件的数据。

在 script-setup 模式下,如果要调用子组件的数据,需要先在子组件显式的暴露出来,才能够正确的拿到,这个操作,就是由 defineExpose API 来完成。

defineExpose 的基础用法

defineExpose 的用法非常简单,它本身是一个函数,可以接受一个对象参数。

在子组件里,像这样把需要暴露出去的数据通过 key: value 的形式作为入参(下面的例子是用到了 ES6 的 属性的简洁表示法 ):

vue
<script setup lang="ts">
+const msg = 'Hello World!'
+
+// 通过该 API 显式暴露的数据,才可以在父组件拿到
+defineExpose({
+  msg,
+})
+</script>

然后在父组件就可以通过挂载在子组件上的 ref 变量,去拿到暴露出来的数据了。

顶级 await 的支持

在 script-setup 模式下,不必再配合 async 就可以直接使用 await 了,这种情况下,组件的 setup 会自动变成 async setup 。

vue
<script setup lang="ts">
+const res = await fetch(\`https://example.com/api/foo\`)
+const json = await res.json()
+console.log(json)
+</script>

它转换成标准组件的写法就是:

vue
<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  async setup() {
+    const res = await fetch(\`https://example.com/api/foo\`)
+    const json = await res.json()
+    console.log(json)
+
+    return {
+      json,
+    }
+  },
+})
+</script>

命名技巧

对于接触编程不久的开发者,在个人练习 demo 或者简单的代码片段里可能会经常看到 var avar b 这样的命名,因为本身是一段练习代码,因此都是 “能跑就行”,问题不大。

但在工作中,很多开发团队都会有语义化命名的规范要求,严格的团队会有 Code Review 环节,使用这种无意义命名的代码将无法通过审查,在这种背景下,开发者可能会在命名上花费很多时间,在这里也分享笔者的一些常用技巧,希望能够帮助开发者节约在命名上的时间开销。

文件命名技巧

在开始讲变量命名之前,先说说文件的命名,因为代码都是保存在文件里,并且可能会互相引用,如果后期再修改文件名或者保存位置而忘记更新代码里的引用路径,那么就会影响程序编译和运行。

Vue 组件

在 Vue 项目里,会有放在 views 下的路由组件,有放在 components 目录下的公共组件,虽然都是以 .vue 为扩展名的 Vue 组件文件,但根据用途,它们其实并不相同,因此命名上也有不同的技巧。

路由组件

路由组件组件通常存放在 src/views 目录下,在命名上容易困惑的点应该是风格问题,开发者容易陷入是使用 camelCase 小驼峰还是 kebab-case 短横线风格,或者是 snake_case 下划线风格的选择困难。

一般情况下路由组件都是以单个名词或动词进行命名,例如个人资料页使用 profile 命名路由,路由的访问路径使用 /profile ,对应的路由组件使用 profile.vue 命名,下面是几个常见的例子。

ts
// src/router/routes.ts
+import type { RouteRecordRaw } from 'vue-router'
+
+const routes: RouteRecordRaw[] = [
+  // 首页
+  // e.g. \`https://example.com/\`
+  {
+    path: '/',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+  },
+  // 个人资料页
+  // e.g. \`https://example.com/profile\`
+  {
+    path: '/profile',
+    name: 'profile',
+    component: () => import('@views/profile.vue'),
+  },
+  // 登录页
+  // e.g. \`https://example.com/login\`
+  {
+    path: '/login',
+    name: 'login',
+    component: () => import('@views/login.vue'),
+  },
+]
+
+export default routes

如果是一些数据列表类的页面,使用名词复数,或者名词单数加上 -list 结尾的 kebab-case 短横线风格写法,推荐短横线风格是因为在 URL 的风格设计里更为常见。

像文章列表可以使用 articles 或者 article-list ,但同一个项目建议只使用其中一种方式,以保持整个项目的风格统一,下面是几个常见的例子。

ts
// src/router/routes.ts
+import type { RouteRecordRaw } from 'vue-router'
+
+const routes: RouteRecordRaw[] = [
+  // 文章列表页
+  // 翻页逻辑是改变页码进行跳转,因此需要添加动态参数 \`:page\`
+  // 可以在组件内使用路由实例 \`route.params.page\` 拿到页码
+  // e.g. \`https://example.com/articles/1\`
+  {
+    path: '/articles/:page',
+    name: 'articles',
+    component: () => import('@views/articles.vue'),
+  },
+  // 通知列表页
+  // 翻页逻辑使用 AJAX 无刷翻页,这种情况则可以不配置页码参数
+  // e.g. \`https://example.com/notifications\`
+  {
+    path: '/notifications',
+    name: 'notifications',
+    component: () => import('@views/notifications.vue'),
+  },
+]
+
+export default routes

列表里的资源详情页,因为访问的时候通常会带上具体的 ID 以通过接口查询详情数据,这种情况下资源就继续使用单数,例如下面这个例子。

ts
// src/router/routes.ts
+import type { RouteRecordRaw } from 'vue-router'
+
+const routes: RouteRecordRaw[] = [
+  // 文章详情页
+  // 可以在组件内使用路由实例 \`route.params.id\` 拿到文章 ID
+  // e.g. \`https://example.com/article/1\`
+  {
+    path: '/article/:id',
+    name: 'article',
+    component: () => import('@views/article.vue'),
+  },
+]
+
+export default routes

如果项目路由比较多,通常会对同一业务的路由增加文件夹归类,因此上面的文章列表和文章详情页,可以统一放到 article 目录下,使用 listdetail 区分是列表还是详情。

ts
// src/router/routes.ts
+import type { RouteRecordRaw } from 'vue-router'
+
+const routes: RouteRecordRaw[] = [
+  // 文章相关的路由统一放在这里管理
+  {
+    path: '/article',
+    name: 'article',
+    // 这是一个配置了 \`<router-view />\` 标签的路由中转站组件
+    // 目的是使其可以渲染子路由
+    component: () => import('@cp/TransferStation.vue'),
+    // 由于父级路由没有内容,所以重定向至列表的第 1 页
+    // e.g. \`https://example.com/article\`
+    redirect: {
+      name: 'article-list',
+      params: {
+        page: 1,
+      },
+    },
+    children: [
+      // 文章列表页
+      // e.g. \`https://example.com/article/list/1\`
+      {
+        path: 'list/:page',
+        name: 'article-list',
+        component: () => import('@views/article/list.vue'),
+      },
+      // 文章详情页
+      // e.g. \`https://example.com/article/detail/1\`
+      {
+        path: 'detail/:id',
+        name: 'article-detail',
+        component: () => import('@views/article/detail.vue'),
+      },
+    ],
+  },
+]
+
+export default routes

对于一些需要用多个单词才能描述的资源,可以使用 kebab-case 短横线风格命名,例如很常见的 “策划面对面” 这种栏目,在设置路由时,比较难用一个单词在 URL 里体现其含义,就需要使用这种多个单词的连接。

ts
// src/router/routes.ts
+import type { RouteRecordRaw } from 'vue-router'
+
+const routes: RouteRecordRaw[] = [
+  // 面对面栏目
+  {
+    path: '/face-to-face',
+    name: 'face-to-face',
+    component: () => import('@views/face-to-face.vue'),
+  },
+]
+
+export default routes

这种情况如果需要使用文件夹管理多个路由,同样建议使用 kebab-case 短横线风格命名,例如上面这个 “策划面对面” 栏目,可能会归属于 “开发计划” 这个业务下,那么其父级文件夹就可以使用 development-plan 这样的短横线命名。

公共组件

公共组件组件通常存放在 src/components 目录下,也可以视不同的使用情况,在路由文件夹下创建属于当前路由的 components 目录,作为一个小范围共享的公共组件目录来管理,而 src/components 则只存放全局性质的公共组件。

本节最开始提到了路由组件和公共组件并不相同,虽然都是组件,但路由组件代表的是整个页面,而公共组件更多是作为一个页面上的某个可复用的部件,如果开发者写过 Flutter ,应该能够更深刻的理解到这里的公共组件更接近于 Widget 性质的小部件。

公共组件通常使用 PascalCase 帕斯卡命名法,也就是大驼峰,为什么不用小驼峰呢?

这是源于 Vue 官网的一个 组件名格式 命名推荐:

使用 PascalCase 作为组件名的注册格式,这是因为:
PascalCase 是合法的 JavaScript 标识符。这使得在 JavaScript 中导入和注册组件都很容易,同时 IDE 也能提供较好的自动补全。
><PascalCase /> 在模板中更明显地表明了这是一个 Vue 组件,而不是原生 HTML 元素。同时也能够将 Vue 组件和自定义元素( web components )区分开来。

而且实际使用 PascalCase 风格的编码过程中,在 VSCode 里可以得到不同颜色的高亮效果,这与 kebab-case 风格的 HTML 标签可以快速区分。

vue
<template>
+  <!-- 普通的 HTML 标签 -->
+  <!-- 在笔者的 VSCode 风格里呈现为桃红色 -->
+  <div></div>
+
+  <!-- 大驼峰组件 -->
+  <!-- 在笔者的 VSCode 风格里呈现为绿色 -->
+  <PascalCase />
+</template>

养成这种习惯还有一个好处,就是使用 UI 框架的时候,例如 Ant Design Vue 的 Select 组件 ,在其文档上演示的是全局安装的写法:

vue
<template>
+  <a-select>
+    <a-select-option value="Hello">Hello</a-select-option>
+  </a-select>
+</template>

而实际使用时,为了更好的配合构建工具进行 Tree Shaking 移除没有用到的组件,都是按需引入 UI 框架的组件,因此如果平时有养成习惯使用 PascalCase 命名,就可以很轻松的知道上面的 <a-select-option /> 组件应该对应的是 <SelectOption /> ,因此是这样按需导入:

ts
import { Select, SelectOption } from 'ant-design-vue'

可以说, PascalCase 这个命名方式也是目前流行 UI 框架都在使用的命名规范。

TypeScript 文件

在 Vue 项目里,虽然 TypeScript 代码可以写在组件里,但由于很多功能实现是可以解耦并复用,所以经常会有专门的目录管理公共方法,这样做也可以避免在一个组件里写出一两千行代码从而导致维护成本提高。

libs 文件

笔者习惯将这些方法统一放到 src/libs 目录下,按照业务模块或者功能的相似度,以一个名词或者动词作为文件命名。

例如常用的正则表达式,可以归类到 regexp.ts 里。

ts
// src/libs/regexp.ts
+
+// 校验手机号格式
+export function isMob(phoneNumber: number | string) {
+  // ...
+}
+
+// 校验电子邮箱格式
+export function isEmail(email: string) {
+  // ...
+}
+
+// 校验网址格式
+export function isUrl(url: string) {
+  // ...
+}
+
+// 校验身份证号码格式
+export function isIdCard(idCardNumber: string) {
+  // ...
+}
+
+// 校验银行卡号码格式
+export function isBankCard(bankCardNumber: string) {
+  // ...
+}

统一使用命名导出,这样一个 TS 文件就像一个 npm 包一样,在使用的时候就可以从这个 “包” 里面导出各种要用到的方法直接使用,无需在组件里重复编写判断逻辑。

ts
import { isMob, isEmail } from '@libs/regexp'

其他诸如常用到的短信验证 sms.ts 、登录逻辑 login.ts 、数据格式转换 format.ts 都可以像这样单独抽出来封装,这种与业务解耦的封装方式非常灵活,以后不同项目如果也有类似的需求,就可以直接拿过去复用了!

types 文件

对于经常用到的 TypeScript 类型,也可以抽离成公共文件,笔者习惯在 src/types 目录管理公共类型,统一使用 .ts 作为扩展名并在里面导出 TS 类型,而不使用 .d.ts 这个类型声明文件。

这样做的好处是在使用到相应类型时,可以通过 import type 显式导入,在后期的项目维护过程中,可以很明确的知道类型来自于哪里,并且更接近从 npm 包里导入类型使用的开发方式。

例如上文配置路由的例子里,就是从 Vue Router 里导入了路由的类型:

ts
// src/router/routes.ts
+import type { RouteRecordRaw } from 'vue-router'
+
+const routes: RouteRecordRaw[] = [
+  // ...
+]
+
+export default routes

在 types 目录下,可以按照业务模块创建多个模块文件分别维护不同的 TS 类型,并统一在 index.ts 里导出:

bash
src
+└─types
+   # 入口文件
+  ├─index.ts
+   # 管理不同业务的公共类型
+  ├─user.ts
+  ├─game.ts
+  └─news.ts

例如 game.ts 可以维护经常用到的游戏业务相关类型,其中为了避免和其他模块命名冲突,以及一眼可以看出是来自哪个业务的类型,可以统一使用业务模块的名称作为前缀。

ts
// src/types/game.ts
+
+// 游戏公司信息
+export interface GameCompany {
+  // ...
+}
+
+// 游戏信息
+export interface GameInfo {
+  id: number
+  name: string
+  gameCompany: GameCompany
+  // ...
+}

将该模块的所有类型在 index.ts 里全部导出:

ts
// src/types/index.ts
+export * from './game'

在组件里就可以这样使用该类型:

ts
// 可以从 \`types\` 里统一导入,而不必明确到 \`types/game\`
+import type { GameInfo } from '@/types'
+
+const game: GameInfo = {
+  id: 1,
+  name: 'Contra',
+  gameCompany: {},
+}
+console.log(game)

TS 类型都遵循 PascalCase 命名风格,方便和声明的变量作为区分,大部分情况下一看到 GameInfo 就知道是类型,而 gameInfo 则是一个变量。

代码命名技巧

在编写 JavaScript / TypeScript 时,为变量和函数的命名也是新手开发者容易花费比较多时间的一件事情,笔者也分享自己常用的命名套路,可以大幅度降低命名的思考时间,而且可以体现一定的语义化。

变量的命名

首先笔者遵循变量只使用 camelCase 小驼峰风格的基本原则,并且根据不同的类型,搭配不同的命名前缀或后缀。

对于 string 字符串类型,使用相关的名词命名即可。

ts
import { ref } from 'vue'
+
+// 用户名
+const username = ref<string>('Petter')
+
+// 职业
+const profession = ref<string>('Front-end Engineer')

对于 number 数值类型,除了一些本身可以代表数字的名词,例如年龄 age 、秒数 seconds ,其他的情况可以搭配后缀命名,常用的后缀有 CountNumberSizeAmount 等和单位有关的名词。

ts
import { ref } from 'vue'
+
+// 最大数量
+const maxCount = ref<number>(100)
+
+// 页码
+const pageNumber = ref<number>(1)
+
+// 每页条数
+const pageSize = ref<number>(10)
+
+// 折扣金额
+const discountAmount = ref<number>(50)

对于 boolean 布尔值类型,可搭配 ishas 等 Be 动词或判断类的动词作为前缀命名,并视情况搭配行为动词和目标名词,或者直接使用一些状态形容词。

ts
import { ref } from 'vue'
+
+// 是否显示弹窗
+const isShowDialog = ref<boolean>(false)
+
+// 用户是否为 VIP 会员
+const isVIP = ref<boolean>(true)
+
+// 用户是否有头像
+const hasAvatar = ref<boolean>(true)
+
+// 是否被禁用
+const disabled = ref<boolean>(true)
+
+// 是否可见
+const visible = ref<boolean>(true)

之所以要搭配 is 开头,是为了和函数区分,例如 showDialog() 是显示弹窗的方法,而 isShowDialog 才是一个布尔值用于逻辑判断。

对于数组,通常使用名词的复数形式,或者名词加上 List 结尾作为命名,数组通常会有原始数据类型的数组,也有 JSON 对象数组,笔者习惯对前者使用名词复数,对后者使用 List 结尾。

ts
import { ref } from 'vue'
+
+// 每个 Item 都是字符串
+const tags = ref<string>(['食物', '粤菜', '卤水'])
+
+// 每个 Item 都是数值
+const tagIds = ref<number>([1, 2, 3])
+
+// 每个 Item 都是 JSON 对象
+const memberList = ref<Member[]>([
+  {
+    id: 1,
+    name: 'Petter',
+  },
+  {
+    id: 2,
+    name: 'Marry',
+  },
+])

如果是作为函数的入参,通常也遵循变量的命名规则。

除非是一些代码量很少的操作,可以使用 ij 等单个字母的变量名,例如提交接口参数时,经常只需要提交一个 ID 数组,从 JSON 数组里提取 ID 数组时就可以使用这种简短命名。

ts
// \`map\` 的参数命名就可以使用 \`i\` 这种简短命名
+const ids = dataList.map((i) => i.id)

函数的命名

函数的命名也是只使用 camelCase 小驼峰风格,通常根据该函数是同步操作还是异步操作,使用不同的动词前缀。

获取数据的函数,通常使用 getqueryread 等代表会返回数据的动词作为前缀,如果还是觉得很难确定使用哪一个,可以统一使用 get ,也可以根据函数的操作性质来决定:

  • 如果是同步操作,不涉及接口请求,使用 get 作为前缀
  • 如果是需要从 API 接口查询数据的异步操作,使用 query 作为前缀
  • 如果是 Node.js 程序这种需要进行文件内容读取的场景,就使用 read
ts
// 从本地存储读取数据
+// 因为是同步操作,所以使用 \`get\` 前缀
+function getLoginInfo() {
+  try {
+    const info = localStorage.getItem('loginInfo')
+    return info ? JSON.parse(info) : null
+  } catch (e) {
+    return null
+  }
+}
+
+// 从接口查询数据
+// 因为是异步操作,需要去数据库查数据,所以使用 \`query\` 前缀
+async function queryMemberInfo(id: number) {
+  try {
+    const res = await fetch(\`https://example.com/api/member/\${id}\`)
+    const json = await res.json()
+    return json
+  } catch (e) {
+    return null
+  }
+}

修改数据的函数,通常使用 saveupdatedelete 等会变更数据的动词作为前缀,一般情况下:

  • 数据存储可以统一使用 save
  • 如果要区分新建或者更新操作,可以对新建操作使用 create ,对更新操作使用 update
  • 删除使用 deleteremove
  • 如果是 Node.js 程序需要对文件写入内容,使用 write
  • 表单验证合法性等场景,可以使用 verifycheck
  • 切换可见性可以用 showhide ,如果是写在一个函数里,可以使用 toggle
  • 发送验证码、发送邮件等等可以使用 send
  • 打开路由、打开外部 URL 可以使用 open

当然以上只是一些常用到的命名技巧建议,对于简单的业务,例如一个 H5 活动页面,也可以在同步操作时使用 set 表示可以直接设置,在异步操作时使用 save 表示需要提交保存。

ts
// 将数据保存至本地存储
+// 因为是同步操作,所以使用 \`set\` 前缀
+function setLoginInfo(info: LoginInfo) {
+  try {
+    localStorage.setItem('loginInfo', JSON.stringify(info))
+    return true
+  } catch (e) {
+    return false
+  }
+}
+
+// 将数据通过接口保存到数据库
+// 因为是异步操作,所以使用 \`save\` 前缀
+async function saveMemberInfo(id: number, data: MemberDTO) {
+  try {
+    const res = await fetch(\`https://example.com/api/member/\${id}\`, {
+      method: 'POST',
+      body: JSON.stringify(data),
+    })
+    const json = await res.json()
+    return json.code === 200
+  } catch (e) {
+    return false
+  }
+}

Class 类上的方法和函数命名规则一样,但 Class 本身使用 PascalCase 命名法,代表这是一个类,在调用的时候需要 new

ts
// 类使用 PascalCase 命名法
+class Hello {
+  name: string
+
+  constructor(name: string) {
+    this.name = name
+  }
+
+  say() {
+    console.log(\`Hello \${this.name}\`)
+  }
+}
+
+const hello = new Hello('World')
+hello.say() // Hello World

希望曾经在命名上有过困扰的开发者,不再有此烦恼,编写代码更加高效率!

`,217);function D(F,i,C,A,d,u){const a=n("GitalkComment"),l=n("ClientOnly");return o(),e("div",null,[y,s(l,null,{default:t(()=>[s(a,{issueId:118})]),_:1})])}const h=p(r,[["render",D]]);export{g as __pageData,h as default}; diff --git a/assets/efficient.md.a41d0892.lean.js b/assets/efficient.md.a41d0892.lean.js new file mode 100644 index 00000000..cac3dc46 --- /dev/null +++ b/assets/efficient.md.a41d0892.lean.js @@ -0,0 +1,552 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as p,v as o,b as e,t as s,O as t,R as c,M as n}from"./chunks/framework.0d8bea05.js";const g=JSON.parse('{"title":"高效开发","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"efficient.md","filePath":"efficient.md"}'),r={name:"efficient.md"},y=c(`

高效开发

可能很多开发者(包括笔者)刚上手 Vue 3 的那段时间,都会觉得开发过程似乎变得更繁琐了, Vue 官方团队当然不会无视群众的呼声,如果基于脚手架和 .vue 文件开发,那么可以享受到更高效率的开发体验。

在阅读这一章之前,需要对 Vue 3 的单组件开发有一定的了解,如果还处于完全没有接触过的阶段,请先抽点时间阅读 单组件的编写 一章。

TIP

要体验以下新特性,请确保项目下 package.json 里的 vue 版本在 3.2.0 以上,最好同步 npm 上当前最新的 @latest 版本,否则可能出现 API 未定义等问题。

script-setup ~new

script-setup 是 Vue 3 组件的一个语法糖,旨在帮助开发者降低 setup 函数需要 return 的心智负担。

Vue 的 3.1.2 版本是针对 script-setup 的一个分水岭版本,自 3.1.4 开始 script-setup 进入定稿状态,部分旧的 API 已被舍弃,本章节内容将以最新的 API 为准进行整理说明,如果需要查阅旧版 API 的使用,请参阅笔者的 这篇开荒博文

script-setup 方案已在 Vue 3.2.0-beta.1 版本中脱离实验状态,正式进入 Vue 3 的队伍,此后所有的新版本均可以作为一个官方标准的开发方案使用。

新特性的产生背景

在了解 script-setup 怎么用之前,可以先了解一下推出该语法糖的一些开发背景,通过对比开发体验上的异同点,了解为什么会有这个新模式。

在 Vue 3 的组件标准写法里,如果数据和方法需要在 <template /> 里使用,都需要在 <script /> 的 setup 函数里 return 出来。

如果使用的是 TypeScript ,还需要借助 defineComponent 对 API 类型进行自动推导。

vue
<!-- 标准组件格式 -->
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // ...
+
+    return {
+      // ...
+    }
+  },
+})
+</script>

关于标准 setup 和 defineComponent 的说明和用法,可以查阅 全新的 setup 函数 一节。

script-setup 的推出是为了让熟悉 Vue 3 的开发者可以更高效率地开发组件,减少编码过程中的心智负担,只需要给 <script /> 标签添加一个 setup 属性,那么整个 <script /> 就直接会变成 setup 函数,所有顶级变量、函数,均会自动暴露给模板使用(无需再一个个 return 了)。

Vue 会通过单组件编译器,在编译的时候将其处理回标准组件,所以目前这个方案只适合用 .vue 文件写的工程化项目。

vue
<!-- 使用 script-setup 格式 -->
+<script setup lang="ts">
+// ...
+</script>

对,就是这样,代码量瞬间大幅度减少!

因为 script-setup 的大部分功能在书写上和标准版是一致的,因此下面的内容只提及有差异的写法。

全局编译器宏

在 script-setup 模式下,新增了 4 个全局编译器宏,他们无需 import 就可以直接使用。

但是默认的情况下直接使用,如果项目开启了 ESLint ,可能会提示 API 没有导入,但导入 API 后,控制台的 Vue 编译助手又会提示不需要导入,就很尴尬… 不过不用着急!可以通过配置 Lint 规则解决这个问题!

将这几个编译助手写进全局规则里,这样不导入也不会报错了。

js
// 项目根目录下的 .eslintrc.js
+module.exports = {
+  // ...
+  // 在原来的 Lint 规则后面,补充下面的 \`globals\` 选项
+  globals: {
+    defineProps: 'readonly',
+    defineEmits: 'readonly',
+    defineExpose: 'readonly',
+    withDefaults: 'readonly',
+  },
+}

关于几个宏的说明都在下面的文档部分有说明,也可以从这里导航过去直接查看。

说明
defineProps点击查看
defineEmits点击查看
defineExpose点击查看
withDefaults点击查看

下面继续了解 script-setup 的变化。

template 操作简化

如果使用 JSX / TSX 写法,这一点没有太大影响,但对于习惯使用 <template /> 的开发者来说,这是一个非常爽的体验。

主要体现在这两点:

  1. 变量无需进行 return
  2. 子组件无需手动注册

变量无需进行 return

标准组件模式下,变量和方法都需要在 setup 函数里 return 出去,才可以在 <template /> 部分拿到。

vue
<!-- 标准组件格式 -->
+<template>
+  <p>{{ msg }}</p>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const msg = 'Hello World!'
+
+    // 要给 \`<template />\` 用的数据需要 \`return\` 出去才可以
+    return {
+      msg,
+    }
+  },
+})
+</script>

在 script-setup 模式下,定义了就可以直接使用。

vue
<!-- 使用 script-setup 格式 -->
+<template>
+  <p>{{ msg }}</p>
+</template>
+
+<script setup lang="ts">
+const msg = 'Hello World!'
+</script>

子组件无需手动注册

子组件的挂载,在标准组件里的写法是需要 import 后再放到 components 里才能够启用:

vue
<!-- 标准组件格式 -->
+<template>
+  <Child />
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+// 导入子组件
+import Child from '@cp/Child.vue'
+
+export default defineComponent({
+  // 需要启用子组件作为模板
+  components: {
+    Child,
+  },
+
+  // 组件里的业务代码
+  setup() {
+    // ...
+  },
+})
+</script>

在 script-setup 模式下,只需要导入组件即可,编译器会自动识别并启用。

vue
<!-- 使用 script-setup 格式 -->
+<template>
+  <Child />
+</template>
+
+<script setup lang="ts">
+import Child from '@cp/Child.vue'
+</script>

props 的接收方式变化

由于整个 script 都变成了一个大的 setup 函数,没有了组件选项,也没有了 setup 的入参,所以没办法和标准写法一样去接收 props 了。

这里需要使用一个全新的 API :defineProps 。

defineProps 是一个方法,内部返回一个对象,也就是挂载到这个组件上的所有 props ,它和普通的 props 用法一样,如果不指定为 props ,则传下来的属性会被放到 attrs 那边去。

TIP

前置知识点:接收 props - 组件之间的通信

defineProps 的基础用法

如果只是单纯在 <template /> 里使用,那么这么简单定义就可以了:

ts
defineProps(['name', 'userInfo', 'tags'])

使用 string[] 数组作为入参,把 prop 的名称作为数组的 item 传给 defineProps 就可以了。

如果 <script /> 里的方法要拿到 props 的值,也可以使用字面量定义:

ts
const props = defineProps(['name', 'userInfo', 'tags'])
+console.log(props.name)

但作为 Vue 的老玩家,都清楚不显性的指定 props 的类型很容易在协作中引起程序报错,那么应该如何对每个 prop 进行类型检查呢?

有两种方式来处理类型定义:

  1. 通过构造函数检查 prop
  2. 使用类型注解检查 prop

通过构造函数检查 prop

这是第一种方式:使用 JavaScript 原生构造函数进行类型规定,也就是跟平时定义 prop 类型时一样, Vue 会通过 instanceof 来进行 类型检查

使用这种方法,需要通过一个 “对象” 入参来传递给 defineProps ,比如:

ts
defineProps({
+  name: String,
+  userInfo: Object,
+  tags: Array,
+})

所有原来 props 具备的校验机制,都可以适用,比如除了要限制类型外,还想指定 name 是可选,并且带有一个默认值:

ts
defineProps({
+  name: {
+    type: String,
+    required: false,
+    default: 'Petter',
+  },
+  userInfo: Object,
+  tags: Array,
+})

更多的 props 校验机制,可以点击 带有类型限制的 props可选以及带有默认值的 props 了解更多。

使用类型注解检查 prop

这是第二种方式:使用 TypeScript 的类型注解,和 ref 等 API 的用法一样,defineProps 也是可以使用尖括号 <> 来包裹类型定义,紧跟在 API 后面。

另外,由于 defineProps 返回的是一个对象(因为 props 本身是一个对象),所以尖括号里面的类型还要用大括号包裹,通过 key: value 的键值对形式表示,如:

ts
defineProps<{ name: string }>()

注意到了吗?这里使用的类型,和第一种方法提到的指定类型是不一样的。这里不再使用构造函数校验,而是需要遵循使用 TypeScript 的类型,比如字符串是 string,而不是 String

如果有多个 prop ,就跟写 interface 一样:

ts
defineProps<{
+  name: string
+  phoneNumber: number
+  userInfo: object
+  tags: string[]
+}>()

其中,举例里的 userInfo 是一个对象,可以简单的指定为 object,也可以先定义好它对应的类型,再进行指定:

ts
interface UserInfo {
+  id: number
+  age: number
+}
+
+defineProps<{
+  name: string
+  userInfo: UserInfo
+}>()

如果想对某个数据设置为可选,也是遵循 TS 规范,通过英文问号 ? 来允许可选:

ts
// name 是可选
+defineProps<{
+  name?: string
+  tags: string[]
+}>()

如果想设置可选参数的默认值,需要借助 withDefaults API。

WARNING

需要强调的一点是:在 构造函数类型注解 这两种校验方式只能二选一,不能同时使用,否则会引起程序报错。

withDefaults 的基础用法

这个新的 withDefaults API 可以让在使用 TS 类型系统时,也可以指定 props 的默认值。

它接收两个入参:

参数含义
props通过 defineProps 传入的 props
defaultValues根据 props 的 key 传入默认值

光看描述可能不容易理解,看看下面这段演示代码会更直观:

ts
withDefaults(
+  // 这是第一个参数,声明 props
+  defineProps<{
+    size?: number
+    labels?: string[]
+  }>(),
+  // 这是第二个参数,设置默认值
+  {
+    size: 3,
+    labels: () => ['default label'],
+  }
+)

也可以通过字面量获取 props :

ts
// 上面的写法可能比较复杂,存在阅读成本
+// 也可以跟平时一样先通过 interface 声明其类型
+interface Props {
+  size?: number
+  labels?: string[]
+}
+
+// 再作为 \`defineProps\` 的类型传入
+// 代码风格上会简洁很多
+const props = withDefaults(defineProps<Props>(), {
+  size: 3,
+  labels: () => ['default label'],
+})
+
+// 这样就可以通过 \`props\` 变量拿到需要的值
+console.log(props.size)

emits 的接收方式变化

和 props 一样,emits 的接收也是需要使用一个全新的 API 来操作,这个 API 就是 defineEmits 。

和 defineProps 一样, defineEmits 也是一个方法,它接受的入参格式和标准组件的要求是一致的。

TIP

前置知识点:接收 emits - 组件之间的通信

defineEmits 的基础用法

需要通过字面量来定义 emits ,最基础的用法也是传递一个 string[] 数组进来,把每个 emit 的名称作为数组的 item 。

ts
// 获取 emit
+const emit = defineEmits(['update-name'])
+
+// 调用 emit
+emit('update-name', 'Tom')

由于 defineEmits 的用法和原来的 emits 选项差别不大,这里也不重复说明更多的诸如校验之类的用法了,可以查看 接收 emits 一节了解更多。

attrs 的接收方式变化

attrs 和 props 很相似,也是基于父子通信的数据,如果父组件绑定下来的数据没有被指定为 props ,那么就会被 attrs 接收。

在标准组件里, attrs 的数据是通过 setup 的第二个入参 context 里的 attrs API 获取的。

ts
// 标准组件的写法
+export default defineComponent({
+  setup(props, { attrs }) {
+    // attrs 是个对象,每个 Attribute 都是它的 key
+    console.log(attrs.class)
+
+    // 如果传下来的 Attribute 带有短横线,需要通过这种方式获取
+    console.log(attrs['data-hash'])
+  },
+})

但和 props 一样,由于没有了 context 参数,需要使用一个新的 API 来拿到 attrs 数据,这个 API 就是 useAttrs 。

useAttrs 的基础用法

顾名思义, useAttrs 可以是用来获取 attrs 数据的,它的用法非常简单:

ts
import { useAttrs } from 'vue'
+
+// 获取 attrs
+const attrs = useAttrs()
+
+// attrs 是个对象,和 props 一样,需要通过 \`key\` 来得到对应的单个 attr
+console.log(attrs.msg)

对 attrs 不太了解的话,可以查阅 获取非 Prop 的 Attribute

slots 的接收方式变化

slots 是 Vue 组件的插槽数据,也是在父子通信里的一个重要成员。

对于使用 <template /> 的开发者来说,在 script-setup 里获取插槽数据并不困难,因为跟标准组件的写法是完全一样的,可以直接在 <template /> 里使用 <slot /> 标签渲染。

vue
<template>
+  <div>
+    <!-- 插槽数据 -->
+    <slot />
+    <!-- 插槽数据 -->
+  </div>
+</template>

但对使用 JSX / TSX 的开发者来说,就影响比较大了,在标准组件里,想在 script 里获取插槽数据,也是需要在 setup 的第二个入参里拿到 slots API 。

ts
// 标准组件的写法
+export default defineComponent({
+  // 这里的 slots 就是插槽
+  setup(props, { slots }) {
+    // ...
+  },
+})

新版本的 Vue 也提供了一个全新的 useSlots API 来帮助 script-setup 用户获取插槽。

useSlots 的基础用法

先来看看父组件,父组件先为子组件传入插槽数据,支持 “默认插槽” 和 “命名插槽” :

vue
<template>
+  <!-- 子组件 -->
+  <ChildTSX>
+    <!-- 默认插槽 -->
+    <p>Default slot for TSX.</p>
+    <!-- 默认插槽 -->
+
+    <!-- 命名插槽 -->
+    <template #msg>
+      <p>Named slot for TSX.</p>
+    </template>
+    <!-- 命名插槽 -->
+  </ChildTSX>
+  <!-- 子组件 -->
+</template>
+
+<script setup lang="ts">
+// 实际上是导入 ChildTSX.tsx 文件,扩展名默认可以省略
+import ChildTSX from '@cp/ChildTSX'
+</script>

在使用 JSX / TSX 编写的子组件里,就可以通过 useSlots 来获取父组件传进来的 slots 数据进行渲染:

tsx
// src/components/ChildTSX.tsx
+import { defineComponent, useSlots } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 获取插槽数据
+    const slots = useSlots()
+
+    // 渲染组件
+    return () => (
+      <div>
+        {/* 渲染默认插槽 */}
+        <p>{slots.default ? slots.default() : ''}</p>
+
+        {/* 渲染命名插槽 */}
+        <p>{slots.msg ? slots.msg() : ''}</p>
+      </div>
+    )
+  },
+})

请注意,这里的 TSX 组件代码需要使用 .tsx 作为文件扩展名,并且构建工具可能默认没有对 JSX / TSX 作支持,以 Vite 为例,需要安装官方提供的 JSX / TSX 支持插件才可以正常使用。

bash
# 该插件支持使用 JSX 或 TSX 作为 Vue 组件
+npm i -D @vitejs/plugin-vue-jsx

并在 vite.config.ts 里启用插件,添加对 JSX 和 TSX 的支持。

ts
// vite.config.ts
+import { defineConfig } from 'vite'
+import vueJsx from '@vitejs/plugin-vue-jsx'
+
+export default defineConfig({
+  // ...
+  plugins: [
+    // ...
+    // 启用插件
+    vueJsx(),
+  ],
+})

如果还存在报错的情况,可以检查项目下的 tsconfig.json 文件里,编译选项 jsx 是否设置为 preserve

json
{
+  "compilerOptions": {
+    "jsx": "preserve"
+  }
+}

ref 的通信方式变化

在标准组件写法里,子组件的数据和方法可以通过在 setup 里 return 出来给父组件调用,也就是父组件可以通过 childComponent.value.foo 这样的方式直接操作子组件的数据(参见:DOM 元素与子组件 - 响应式 API 之 ref)。

但在 script-setup 模式下,所有数据只是默认隐式 return 给 <template /> 使用,不会暴露到组件外,所以父组件是无法直接通过挂载 ref 变量获取子组件的数据。

在 script-setup 模式下,如果要调用子组件的数据,需要先在子组件显式的暴露出来,才能够正确的拿到,这个操作,就是由 defineExpose API 来完成。

defineExpose 的基础用法

defineExpose 的用法非常简单,它本身是一个函数,可以接受一个对象参数。

在子组件里,像这样把需要暴露出去的数据通过 key: value 的形式作为入参(下面的例子是用到了 ES6 的 属性的简洁表示法 ):

vue
<script setup lang="ts">
+const msg = 'Hello World!'
+
+// 通过该 API 显式暴露的数据,才可以在父组件拿到
+defineExpose({
+  msg,
+})
+</script>

然后在父组件就可以通过挂载在子组件上的 ref 变量,去拿到暴露出来的数据了。

顶级 await 的支持

在 script-setup 模式下,不必再配合 async 就可以直接使用 await 了,这种情况下,组件的 setup 会自动变成 async setup 。

vue
<script setup lang="ts">
+const res = await fetch(\`https://example.com/api/foo\`)
+const json = await res.json()
+console.log(json)
+</script>

它转换成标准组件的写法就是:

vue
<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  async setup() {
+    const res = await fetch(\`https://example.com/api/foo\`)
+    const json = await res.json()
+    console.log(json)
+
+    return {
+      json,
+    }
+  },
+})
+</script>

命名技巧

对于接触编程不久的开发者,在个人练习 demo 或者简单的代码片段里可能会经常看到 var avar b 这样的命名,因为本身是一段练习代码,因此都是 “能跑就行”,问题不大。

但在工作中,很多开发团队都会有语义化命名的规范要求,严格的团队会有 Code Review 环节,使用这种无意义命名的代码将无法通过审查,在这种背景下,开发者可能会在命名上花费很多时间,在这里也分享笔者的一些常用技巧,希望能够帮助开发者节约在命名上的时间开销。

文件命名技巧

在开始讲变量命名之前,先说说文件的命名,因为代码都是保存在文件里,并且可能会互相引用,如果后期再修改文件名或者保存位置而忘记更新代码里的引用路径,那么就会影响程序编译和运行。

Vue 组件

在 Vue 项目里,会有放在 views 下的路由组件,有放在 components 目录下的公共组件,虽然都是以 .vue 为扩展名的 Vue 组件文件,但根据用途,它们其实并不相同,因此命名上也有不同的技巧。

路由组件

路由组件组件通常存放在 src/views 目录下,在命名上容易困惑的点应该是风格问题,开发者容易陷入是使用 camelCase 小驼峰还是 kebab-case 短横线风格,或者是 snake_case 下划线风格的选择困难。

一般情况下路由组件都是以单个名词或动词进行命名,例如个人资料页使用 profile 命名路由,路由的访问路径使用 /profile ,对应的路由组件使用 profile.vue 命名,下面是几个常见的例子。

ts
// src/router/routes.ts
+import type { RouteRecordRaw } from 'vue-router'
+
+const routes: RouteRecordRaw[] = [
+  // 首页
+  // e.g. \`https://example.com/\`
+  {
+    path: '/',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+  },
+  // 个人资料页
+  // e.g. \`https://example.com/profile\`
+  {
+    path: '/profile',
+    name: 'profile',
+    component: () => import('@views/profile.vue'),
+  },
+  // 登录页
+  // e.g. \`https://example.com/login\`
+  {
+    path: '/login',
+    name: 'login',
+    component: () => import('@views/login.vue'),
+  },
+]
+
+export default routes

如果是一些数据列表类的页面,使用名词复数,或者名词单数加上 -list 结尾的 kebab-case 短横线风格写法,推荐短横线风格是因为在 URL 的风格设计里更为常见。

像文章列表可以使用 articles 或者 article-list ,但同一个项目建议只使用其中一种方式,以保持整个项目的风格统一,下面是几个常见的例子。

ts
// src/router/routes.ts
+import type { RouteRecordRaw } from 'vue-router'
+
+const routes: RouteRecordRaw[] = [
+  // 文章列表页
+  // 翻页逻辑是改变页码进行跳转,因此需要添加动态参数 \`:page\`
+  // 可以在组件内使用路由实例 \`route.params.page\` 拿到页码
+  // e.g. \`https://example.com/articles/1\`
+  {
+    path: '/articles/:page',
+    name: 'articles',
+    component: () => import('@views/articles.vue'),
+  },
+  // 通知列表页
+  // 翻页逻辑使用 AJAX 无刷翻页,这种情况则可以不配置页码参数
+  // e.g. \`https://example.com/notifications\`
+  {
+    path: '/notifications',
+    name: 'notifications',
+    component: () => import('@views/notifications.vue'),
+  },
+]
+
+export default routes

列表里的资源详情页,因为访问的时候通常会带上具体的 ID 以通过接口查询详情数据,这种情况下资源就继续使用单数,例如下面这个例子。

ts
// src/router/routes.ts
+import type { RouteRecordRaw } from 'vue-router'
+
+const routes: RouteRecordRaw[] = [
+  // 文章详情页
+  // 可以在组件内使用路由实例 \`route.params.id\` 拿到文章 ID
+  // e.g. \`https://example.com/article/1\`
+  {
+    path: '/article/:id',
+    name: 'article',
+    component: () => import('@views/article.vue'),
+  },
+]
+
+export default routes

如果项目路由比较多,通常会对同一业务的路由增加文件夹归类,因此上面的文章列表和文章详情页,可以统一放到 article 目录下,使用 listdetail 区分是列表还是详情。

ts
// src/router/routes.ts
+import type { RouteRecordRaw } from 'vue-router'
+
+const routes: RouteRecordRaw[] = [
+  // 文章相关的路由统一放在这里管理
+  {
+    path: '/article',
+    name: 'article',
+    // 这是一个配置了 \`<router-view />\` 标签的路由中转站组件
+    // 目的是使其可以渲染子路由
+    component: () => import('@cp/TransferStation.vue'),
+    // 由于父级路由没有内容,所以重定向至列表的第 1 页
+    // e.g. \`https://example.com/article\`
+    redirect: {
+      name: 'article-list',
+      params: {
+        page: 1,
+      },
+    },
+    children: [
+      // 文章列表页
+      // e.g. \`https://example.com/article/list/1\`
+      {
+        path: 'list/:page',
+        name: 'article-list',
+        component: () => import('@views/article/list.vue'),
+      },
+      // 文章详情页
+      // e.g. \`https://example.com/article/detail/1\`
+      {
+        path: 'detail/:id',
+        name: 'article-detail',
+        component: () => import('@views/article/detail.vue'),
+      },
+    ],
+  },
+]
+
+export default routes

对于一些需要用多个单词才能描述的资源,可以使用 kebab-case 短横线风格命名,例如很常见的 “策划面对面” 这种栏目,在设置路由时,比较难用一个单词在 URL 里体现其含义,就需要使用这种多个单词的连接。

ts
// src/router/routes.ts
+import type { RouteRecordRaw } from 'vue-router'
+
+const routes: RouteRecordRaw[] = [
+  // 面对面栏目
+  {
+    path: '/face-to-face',
+    name: 'face-to-face',
+    component: () => import('@views/face-to-face.vue'),
+  },
+]
+
+export default routes

这种情况如果需要使用文件夹管理多个路由,同样建议使用 kebab-case 短横线风格命名,例如上面这个 “策划面对面” 栏目,可能会归属于 “开发计划” 这个业务下,那么其父级文件夹就可以使用 development-plan 这样的短横线命名。

公共组件

公共组件组件通常存放在 src/components 目录下,也可以视不同的使用情况,在路由文件夹下创建属于当前路由的 components 目录,作为一个小范围共享的公共组件目录来管理,而 src/components 则只存放全局性质的公共组件。

本节最开始提到了路由组件和公共组件并不相同,虽然都是组件,但路由组件代表的是整个页面,而公共组件更多是作为一个页面上的某个可复用的部件,如果开发者写过 Flutter ,应该能够更深刻的理解到这里的公共组件更接近于 Widget 性质的小部件。

公共组件通常使用 PascalCase 帕斯卡命名法,也就是大驼峰,为什么不用小驼峰呢?

这是源于 Vue 官网的一个 组件名格式 命名推荐:

使用 PascalCase 作为组件名的注册格式,这是因为:
PascalCase 是合法的 JavaScript 标识符。这使得在 JavaScript 中导入和注册组件都很容易,同时 IDE 也能提供较好的自动补全。
><PascalCase /> 在模板中更明显地表明了这是一个 Vue 组件,而不是原生 HTML 元素。同时也能够将 Vue 组件和自定义元素( web components )区分开来。

而且实际使用 PascalCase 风格的编码过程中,在 VSCode 里可以得到不同颜色的高亮效果,这与 kebab-case 风格的 HTML 标签可以快速区分。

vue
<template>
+  <!-- 普通的 HTML 标签 -->
+  <!-- 在笔者的 VSCode 风格里呈现为桃红色 -->
+  <div></div>
+
+  <!-- 大驼峰组件 -->
+  <!-- 在笔者的 VSCode 风格里呈现为绿色 -->
+  <PascalCase />
+</template>

养成这种习惯还有一个好处,就是使用 UI 框架的时候,例如 Ant Design Vue 的 Select 组件 ,在其文档上演示的是全局安装的写法:

vue
<template>
+  <a-select>
+    <a-select-option value="Hello">Hello</a-select-option>
+  </a-select>
+</template>

而实际使用时,为了更好的配合构建工具进行 Tree Shaking 移除没有用到的组件,都是按需引入 UI 框架的组件,因此如果平时有养成习惯使用 PascalCase 命名,就可以很轻松的知道上面的 <a-select-option /> 组件应该对应的是 <SelectOption /> ,因此是这样按需导入:

ts
import { Select, SelectOption } from 'ant-design-vue'

可以说, PascalCase 这个命名方式也是目前流行 UI 框架都在使用的命名规范。

TypeScript 文件

在 Vue 项目里,虽然 TypeScript 代码可以写在组件里,但由于很多功能实现是可以解耦并复用,所以经常会有专门的目录管理公共方法,这样做也可以避免在一个组件里写出一两千行代码从而导致维护成本提高。

libs 文件

笔者习惯将这些方法统一放到 src/libs 目录下,按照业务模块或者功能的相似度,以一个名词或者动词作为文件命名。

例如常用的正则表达式,可以归类到 regexp.ts 里。

ts
// src/libs/regexp.ts
+
+// 校验手机号格式
+export function isMob(phoneNumber: number | string) {
+  // ...
+}
+
+// 校验电子邮箱格式
+export function isEmail(email: string) {
+  // ...
+}
+
+// 校验网址格式
+export function isUrl(url: string) {
+  // ...
+}
+
+// 校验身份证号码格式
+export function isIdCard(idCardNumber: string) {
+  // ...
+}
+
+// 校验银行卡号码格式
+export function isBankCard(bankCardNumber: string) {
+  // ...
+}

统一使用命名导出,这样一个 TS 文件就像一个 npm 包一样,在使用的时候就可以从这个 “包” 里面导出各种要用到的方法直接使用,无需在组件里重复编写判断逻辑。

ts
import { isMob, isEmail } from '@libs/regexp'

其他诸如常用到的短信验证 sms.ts 、登录逻辑 login.ts 、数据格式转换 format.ts 都可以像这样单独抽出来封装,这种与业务解耦的封装方式非常灵活,以后不同项目如果也有类似的需求,就可以直接拿过去复用了!

types 文件

对于经常用到的 TypeScript 类型,也可以抽离成公共文件,笔者习惯在 src/types 目录管理公共类型,统一使用 .ts 作为扩展名并在里面导出 TS 类型,而不使用 .d.ts 这个类型声明文件。

这样做的好处是在使用到相应类型时,可以通过 import type 显式导入,在后期的项目维护过程中,可以很明确的知道类型来自于哪里,并且更接近从 npm 包里导入类型使用的开发方式。

例如上文配置路由的例子里,就是从 Vue Router 里导入了路由的类型:

ts
// src/router/routes.ts
+import type { RouteRecordRaw } from 'vue-router'
+
+const routes: RouteRecordRaw[] = [
+  // ...
+]
+
+export default routes

在 types 目录下,可以按照业务模块创建多个模块文件分别维护不同的 TS 类型,并统一在 index.ts 里导出:

bash
src
+└─types
+   # 入口文件
+  ├─index.ts
+   # 管理不同业务的公共类型
+  ├─user.ts
+  ├─game.ts
+  └─news.ts

例如 game.ts 可以维护经常用到的游戏业务相关类型,其中为了避免和其他模块命名冲突,以及一眼可以看出是来自哪个业务的类型,可以统一使用业务模块的名称作为前缀。

ts
// src/types/game.ts
+
+// 游戏公司信息
+export interface GameCompany {
+  // ...
+}
+
+// 游戏信息
+export interface GameInfo {
+  id: number
+  name: string
+  gameCompany: GameCompany
+  // ...
+}

将该模块的所有类型在 index.ts 里全部导出:

ts
// src/types/index.ts
+export * from './game'

在组件里就可以这样使用该类型:

ts
// 可以从 \`types\` 里统一导入,而不必明确到 \`types/game\`
+import type { GameInfo } from '@/types'
+
+const game: GameInfo = {
+  id: 1,
+  name: 'Contra',
+  gameCompany: {},
+}
+console.log(game)

TS 类型都遵循 PascalCase 命名风格,方便和声明的变量作为区分,大部分情况下一看到 GameInfo 就知道是类型,而 gameInfo 则是一个变量。

代码命名技巧

在编写 JavaScript / TypeScript 时,为变量和函数的命名也是新手开发者容易花费比较多时间的一件事情,笔者也分享自己常用的命名套路,可以大幅度降低命名的思考时间,而且可以体现一定的语义化。

变量的命名

首先笔者遵循变量只使用 camelCase 小驼峰风格的基本原则,并且根据不同的类型,搭配不同的命名前缀或后缀。

对于 string 字符串类型,使用相关的名词命名即可。

ts
import { ref } from 'vue'
+
+// 用户名
+const username = ref<string>('Petter')
+
+// 职业
+const profession = ref<string>('Front-end Engineer')

对于 number 数值类型,除了一些本身可以代表数字的名词,例如年龄 age 、秒数 seconds ,其他的情况可以搭配后缀命名,常用的后缀有 CountNumberSizeAmount 等和单位有关的名词。

ts
import { ref } from 'vue'
+
+// 最大数量
+const maxCount = ref<number>(100)
+
+// 页码
+const pageNumber = ref<number>(1)
+
+// 每页条数
+const pageSize = ref<number>(10)
+
+// 折扣金额
+const discountAmount = ref<number>(50)

对于 boolean 布尔值类型,可搭配 ishas 等 Be 动词或判断类的动词作为前缀命名,并视情况搭配行为动词和目标名词,或者直接使用一些状态形容词。

ts
import { ref } from 'vue'
+
+// 是否显示弹窗
+const isShowDialog = ref<boolean>(false)
+
+// 用户是否为 VIP 会员
+const isVIP = ref<boolean>(true)
+
+// 用户是否有头像
+const hasAvatar = ref<boolean>(true)
+
+// 是否被禁用
+const disabled = ref<boolean>(true)
+
+// 是否可见
+const visible = ref<boolean>(true)

之所以要搭配 is 开头,是为了和函数区分,例如 showDialog() 是显示弹窗的方法,而 isShowDialog 才是一个布尔值用于逻辑判断。

对于数组,通常使用名词的复数形式,或者名词加上 List 结尾作为命名,数组通常会有原始数据类型的数组,也有 JSON 对象数组,笔者习惯对前者使用名词复数,对后者使用 List 结尾。

ts
import { ref } from 'vue'
+
+// 每个 Item 都是字符串
+const tags = ref<string>(['食物', '粤菜', '卤水'])
+
+// 每个 Item 都是数值
+const tagIds = ref<number>([1, 2, 3])
+
+// 每个 Item 都是 JSON 对象
+const memberList = ref<Member[]>([
+  {
+    id: 1,
+    name: 'Petter',
+  },
+  {
+    id: 2,
+    name: 'Marry',
+  },
+])

如果是作为函数的入参,通常也遵循变量的命名规则。

除非是一些代码量很少的操作,可以使用 ij 等单个字母的变量名,例如提交接口参数时,经常只需要提交一个 ID 数组,从 JSON 数组里提取 ID 数组时就可以使用这种简短命名。

ts
// \`map\` 的参数命名就可以使用 \`i\` 这种简短命名
+const ids = dataList.map((i) => i.id)

函数的命名

函数的命名也是只使用 camelCase 小驼峰风格,通常根据该函数是同步操作还是异步操作,使用不同的动词前缀。

获取数据的函数,通常使用 getqueryread 等代表会返回数据的动词作为前缀,如果还是觉得很难确定使用哪一个,可以统一使用 get ,也可以根据函数的操作性质来决定:

  • 如果是同步操作,不涉及接口请求,使用 get 作为前缀
  • 如果是需要从 API 接口查询数据的异步操作,使用 query 作为前缀
  • 如果是 Node.js 程序这种需要进行文件内容读取的场景,就使用 read
ts
// 从本地存储读取数据
+// 因为是同步操作,所以使用 \`get\` 前缀
+function getLoginInfo() {
+  try {
+    const info = localStorage.getItem('loginInfo')
+    return info ? JSON.parse(info) : null
+  } catch (e) {
+    return null
+  }
+}
+
+// 从接口查询数据
+// 因为是异步操作,需要去数据库查数据,所以使用 \`query\` 前缀
+async function queryMemberInfo(id: number) {
+  try {
+    const res = await fetch(\`https://example.com/api/member/\${id}\`)
+    const json = await res.json()
+    return json
+  } catch (e) {
+    return null
+  }
+}

修改数据的函数,通常使用 saveupdatedelete 等会变更数据的动词作为前缀,一般情况下:

  • 数据存储可以统一使用 save
  • 如果要区分新建或者更新操作,可以对新建操作使用 create ,对更新操作使用 update
  • 删除使用 deleteremove
  • 如果是 Node.js 程序需要对文件写入内容,使用 write
  • 表单验证合法性等场景,可以使用 verifycheck
  • 切换可见性可以用 showhide ,如果是写在一个函数里,可以使用 toggle
  • 发送验证码、发送邮件等等可以使用 send
  • 打开路由、打开外部 URL 可以使用 open

当然以上只是一些常用到的命名技巧建议,对于简单的业务,例如一个 H5 活动页面,也可以在同步操作时使用 set 表示可以直接设置,在异步操作时使用 save 表示需要提交保存。

ts
// 将数据保存至本地存储
+// 因为是同步操作,所以使用 \`set\` 前缀
+function setLoginInfo(info: LoginInfo) {
+  try {
+    localStorage.setItem('loginInfo', JSON.stringify(info))
+    return true
+  } catch (e) {
+    return false
+  }
+}
+
+// 将数据通过接口保存到数据库
+// 因为是异步操作,所以使用 \`save\` 前缀
+async function saveMemberInfo(id: number, data: MemberDTO) {
+  try {
+    const res = await fetch(\`https://example.com/api/member/\${id}\`, {
+      method: 'POST',
+      body: JSON.stringify(data),
+    })
+    const json = await res.json()
+    return json.code === 200
+  } catch (e) {
+    return false
+  }
+}

Class 类上的方法和函数命名规则一样,但 Class 本身使用 PascalCase 命名法,代表这是一个类,在调用的时候需要 new

ts
// 类使用 PascalCase 命名法
+class Hello {
+  name: string
+
+  constructor(name: string) {
+    this.name = name
+  }
+
+  say() {
+    console.log(\`Hello \${this.name}\`)
+  }
+}
+
+const hello = new Hello('World')
+hello.say() // Hello World

希望曾经在命名上有过困扰的开发者,不再有此烦恼,编写代码更加高效率!

`,217);function D(F,i,C,A,d,u){const a=n("GitalkComment"),l=n("ClientOnly");return o(),e("div",null,[y,s(l,null,{default:t(()=>[s(a,{issueId:118})]),_:1})])}const h=p(r,[["render",D]]);export{g as __pageData,h as default}; diff --git a/assets/engineering.md.43ce842b.js b/assets/engineering.md.43ce842b.js new file mode 100644 index 00000000..288e259c --- /dev/null +++ b/assets/engineering.md.43ce842b.js @@ -0,0 +1,291 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{v as y,b as D,t as s,O as n,F as o,L as e,X as F,R as p,M as t}from"./chunks/framework.0d8bea05.js";const i=p(`

了解前端工程化

现在前端的工作与以前的前端开发已经完全不同了。

刚接触前端的时候,做一个页面,是先创建 HTML 页面文件写页面结构,再在里面写 CSS 代码美化页面,再根据需要写一些 JavaScript 代码增加交互功能,需要几个页面就创建几个页面,相信大家的前端起步都是从这个模式开始的。

而实际上的前端开发工作,早已进入了前端工程化开发的时代,已经充满了各种现代化框架、预处理器、代码编译…

最终的产物也不再单纯是多个 HTML 页面,经常能看到 SPA / SSR / SSG 等词汇的身影。

TIP

现代化的开发概念 一节会介绍这些词汇的含义。

传统开发的弊端

在了解什么是前端工程化之前,先回顾一下传统开发存在的一些弊端,这样更能知道为什么需要它。

在传统的前端开发模式下,前端工程师大部分只需要单纯地写写页面,都是在 HTML 文件里直接编写代码,所需要的 JavaScript 代码是通过 script 标签以内联或者文件引用的形式放到 HTML 代码里的,当然 CSS 代码也是一样的处理方式。

例如这样:

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+  </head>
+  <body>
+    <!-- 引入 JS 文件 -->
+    <script src="./js/lib-1.js"></script>
+    <script src="./js/lib-2.js"></script>
+    <!-- 引入 JS 文件 -->
+  </body>
+</html>

如演示代码,虽然可以把代码分成多个文件来维护,这样可以有效降低代码维护成本,但在实际开发过程中,还是会存在代码运行时的一些问题。

一个常见的案例

继续用上面的演示代码,来看一个最简单的一个例子。

先在 lib-1.js 文件里,声明一个变量:

js
var foo = 1

再在 lib-2.js 文件里,也声明一个变量(没错,也是 foo ):

js
var foo = 2

然后在 HTML 代码里追加一个 script ,打印这个值:

html
<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Document</title>
+</head>
+<body>
+
+  <!-- 引入 JS 文件 -->
+  <script src="./js/lib-1.js"></script>
+  <script src="./js/lib-2.js"></script>
+  <!-- 引入 JS 文件 -->
+
+  <!-- 假设这里是实际的业务代码 -->
+  <script>
+    console.log(foo)
+  </script>
+  <!-- 假设这里是实际的业务代码 -->
+
+</body>
+</html>

先猜猜会输出什么? —— 答案是 2

如果在开发的过程中,不知道在 lib-2.js 文件里也声明了一个 foo 变量,一旦在后面的代码里预期了 foo + 2 === 3 ,那么这样就得不到想要的结果(因为 lib-1.js 里的 foo11 + 2 等于 3 ) 。

原因是 JavaScript 的加载顺序是从上到下,当使用 var 声明变量时,如果命名有重复,那么后加载的变量会覆盖掉先加载的变量。

这是使用 var 声明的情况,它允许使用相同的名称来重复声明,那么换成 let 或者 const 呢?

虽然不会出现重复声明的情况,但同样会收到一段报错:

bash
Uncaught SyntaxError: Identifier 'foo' has already been declared (at lib-2.js:1:1)

这次程序直接崩溃了,因为 letconst 无法重复声明,从而抛出这个错误,程序依然无法正确运行。

更多问题

以上只是一个最简单的案例,就暴露出了传统开发很大的弊端,然而并不止于此,实际上,存在诸如以下这些问题:

  1. 如本案例,可能存在同名的变量声明,引起变量冲突
  2. 引入多个资源文件时,比如有多个 JS 文件,在其中一个 JS 文件里面使用了在别处声明的变量,无法快速找到是在哪里声明的,大型项目难以维护
  3. 类似第 1 、 2 点提到的问题无法轻松预先感知,很依赖开发人员人工定位原因
  4. 大部分代码缺乏分割,比如一个工具函数库,很多时候需要整包引入到 HTML 里,文件很大,然而实际上只需要用到其中一两个方法
  5. 由第 4 点大文件延伸出的问题, script 的加载从上到下,容易阻塞页面渲染
  6. 不同页面的资源引用都需要手动管理,容易造成依赖混乱,难以维护
  7. 如果要压缩 CSS 、混淆 JS 代码,也是要人力操作使用工具去一个个处理后替换,容易出错

当然,实际上还会有更多的问题会遇到。

工程化带来的优势

为了解决传统开发的弊端,前端也开始引入工程化开发的概念,借助工具来解决人工层面的烦琐事情。

开发层面的优势

传统开发的弊端 里,主要列举的是开发层面的问题,工程化首要解决的当然也是在开发层面遇到的问题。

在开发层面,前端工程化有以下这些好处:

  1. 引入了模块化和包的概念,作用域隔离,解决了代码冲突的问题
  2. 按需导出和导入机制,让编码过程更容易定位问题
  3. 自动化的代码检测流程,有问题的代码在开发过程中就可以被发现
  4. 编译打包机制可以让使用开发效率更高的编码方式,比如 Vue 组件、 CSS 的各种预处理器
  5. 引入了代码兼容处理的方案( e.g. Babel ),可以让开发者自由使用更先进的 JavaScript 语句,而无需顾忌浏览器兼容性,因为最终会转换为浏览器兼容的实现版本
  6. 引入了 Tree Shaking 机制,清理没有用到的代码,减少项目构建后的体积

还有非常多的体验提升,列举不完。而对应的工具,根据用途也会有非常多的选择,在后面的学习过程中,会一步一步体验到工程化带来的好处。

团队协作的优势

除了对开发者有更好的开发体验和效率提升,对于团队协作,前端工程化也带来了更多的便利,例如下面这些场景:

统一的项目结构

以前的项目结构比较看写代码的人的喜好,虽然一般在研发部门里都有 “团队规范” 这种东西,但靠自觉性去配合的事情,还是比较难做到统一,特别是项目很赶的时候。

工程化后的项目结构非常清晰和统一,以 Vue 项目来说,通过脚手架创建一个新项目之后,它除了提供能直接运行 Hello World 的基础代码之外,还具备了如下的统一目录结构:

  • src 是源码目录
  • src/main.ts 是入口文件
  • src/views 是路由组件目录
  • src/components 是子组件目录
  • src/router 是路由目录

虽然也可以自行调整成别的结构,但根据笔者在多年的工作实际接触下来,以及从很多开源项目的代码里看到的,都是沿用脚手架创建的项目结构(不同脚手架创建的结构会有所不同,但基于同一技术栈的项目基本上都具备相同的结构)。

TIP

脚手架的升级与配置 一章可以学习如何使用脚手架创建 Vue 3 项目。

统一的代码风格

不管是接手其他人的代码或者是修改自己不同时期的代码,可能都会遇到这样的情况,例如一个模板语句,上面包含了很多属性,有的人喜欢写成一行,属性多了维护起来很麻烦,需要花费较多时间辨认:

vue
<template>
+  <div class="list">
+    <!-- 这个循环模板有很多属性 -->
+    <div class="item" :class="{ \`top-\${index + 1}\`: index < 3 }" v-for="(item, index)
+    in list" :key="item.id" @click="handleClick(item.id)">
+      <span>{{ item.text }}</span>
+    </div>
+    <!-- 这个循环模板有很多属性 -->
+  </div>
+</template>

而工程化配合统一的代码格式化规范,可以让不同人维护的代码,最终提交到 Git 上的时候,风格都保持一致,并且类似这种很多属性的地方,都会自动帮格式化为一个属性一行,维护起来就很方便:

vue
<template>
+  <div class="list">
+    <!-- 这个循环模板有很多属性 -->
+    <div
+      class="item"
+      :class="{ \`top-\${index + 1}\`: index < 3 }"
+      v-for="(item, index) in list"
+      :key="item.id"
+      @click="handleClick(item.id)"
+    >
+      <span>{{ item.text }}</span>
+    </div>
+    <!-- 这个循环模板有很多属性 -->
+  </div>
+</template>

同样的,写 JavaScript 时也会有诸如字符串用双引号还是单引号,缩进是 Tab 还是空格,如果用空格到底是要 4 个空格还是 2 个空格等一堆 “没有什么实际意义” 、但是不统一的话协作起来又很难受的问题……

在工程化项目这些问题都可以交给程序去处理,在书写代码的时候,开发者可以先按照自己的习惯书写,然后再执行命令进行格式化,或者是在提交代码的时候配合 Git Hooks 自动格式化,都可以做到统一风格。

TIP

添加协作规范 一节可以学习如何给项目添加统一的协作规范。

可复用的模块和组件

传统项目比较容易被复用的只有 JavaScript 代码和 CSS 代码,会抽离公共函数文件上传到 CDN ,然后在 HTML 页面里引入这些远程资源, HTML 代码部分通常只有由 JS 创建的比较小段的 DOM 结构。

并且通过 CDN 引入的资源,很多时候都是完整引入,可能有时候只需要用到里面的一两个功能,却要把很大的完整文件都引用进来。

这种情况下,在前端工程化里,就可以抽离成一个开箱即用的 npm 组件包,并且很多包都提供了模块化导出,配合构建工具的 Tree Shaking ,可以抽离用到的代码,没有用到的其他功能都会被抛弃,不会一起发布到生产环境。

TIP

依赖包和插件 一节可以学习如何查找和使用开箱即用的 npm 包。

代码健壮性有保障

传统的开发模式里,只能够写 JavaScript ,而在工程项目里,可以在开发环境编写带有类型系统的 TypeScript ,然后再编译为浏览器能认识的 JavaScript 。

在开发过程中,编译器会检查代码是否有问题,比如在 TypeScript 里声明了一个布尔值的变量,然后不小心将它赋值为数值:

ts
// 声明一个布尔值变量
+let bool: boolean = true
+
+// 在 TypeScript ,不允许随意改变类型,这里会报错
+bool = 3

编译器检测到这个行为的时候就会抛出错误:

bash
# ...
+return new TSError(diagnosticText, diagnosticCodes);
+           ^
+TSError:  Unable to compile TypeScript:
+src/index.ts:2:1 - error TS2322: Type 'number' is not assignable to type 'boolean'.
+
+2 bool = 3
+  ~~~~
+# ...

从而得以及时发现问题并修复,减少线上事故的发生。

团队开发效率高

在前后端合作环节,可以提前 Mock 接口与后端工程师同步开发,如果遇到跨域等安全限制,也可以进行本地代理,不受跨域困扰。

前端工程在开发过程中,还有很多可以交给程序处理的环节,像前面提到的代码格式化、代码检查,还有在部署上线的时候也可以配合 CI/CD 完成自动化流水线,不像以前改个字都要找服务端工程师去更新,可以把非常多的人力操作剥离出来交给程序。

求职竞争上的优势

近几年前端开发领域的相关岗位,都会在招聘详情里出现类似的描述:

熟悉 Vue / React 等主流框架,对前端组件化和模块化有深入的理解和实践
熟悉面向组件的开发模式,熟悉 Webpack / Vite 等构建工具
熟练掌握微信小程序开发,熟悉 Taro 框架或 uni-app 框架优先
熟悉 Scss / Less / Stylus 等预处理器的使用
熟练掌握 TypeScript 者优先
有良好的代码风格,结构设计与程序架构者优先
了解或熟悉后端开发者优先(如 Java / Go / Node.js )

知名企业对 1-3 年工作经验的初中级工程师,更是明确要求具备前端工程化开发的能力:

`,73),C=p('

组件化开发、模块化开发、 Webpack / Vite 构建工具、 Node.js 开发… 这些技能都属于前端工程化开发的知识范畴,不仅在面试的时候会提问,入职后新人接触的项目通常也是直接指派前端工程化项目,如果能够提前掌握相关的知识点,对求职也是非常有帮助的!

Vue.js 与工程化

在上一节提到了前端工程化 在求职竞争上的优势 ,里面列出的招聘要求例子都提及到了 Vue 和 React 这些主流的前端框架,前端框架是前端工程化开发里面不可或缺的成员。

框架能够充分的利用前端工程化相关的领先技术,不仅在开发层面降低开发者的上手难度、提升项目开发效率,在构建出来的项目成果上也有着远比传统开发更优秀的用户体验。

本书结合 Vue.js 框架 3.0 系列的全新版本,将从项目开发的角度,在帮助开发者入门前端工程化的同时,更快速的掌握一个流行框架的学习和使用。

了解 Vue.js 与全新的 3.0 版本

Vue.js(发音为 /vjuː/ ,类似 view )是一个易学易用,性能出色,适用场景丰富的 Web 前端框架,从 2015 年发布 1.0 版本以来,受到了全世界范围的前端开发者喜爱,已成为当下最受欢迎的前端框架之一。

',7),d=p(`

Vue 一直紧跟广大开发者的需求迭代发展,保持着它活跃的生命力。

2020 年 9 月 18 日, Vue.js 发布了 3.0 正式版,在大量开发者长达约一年半的使用和功能改进反馈之后, Vue 又于 2022 年 2 月 7 日发布了 3.2 版本,同一天, Vue 3 成为 Vue.js 框架全新的默认版本 (在此之前,通过 npm install vue 的默认版本还是 Vue 2 )。

也就是在未来的日子里, Vue 3 将随着时间的推移,逐步成为 Vue 生态的主流版本,是时候学习 Vue 3 了!

如果还没有体验过 Vue ,可以把以下代码复制到的代码编辑器,保存成一个 HTML 文件(例如: hello.html ),并在浏览器里打开访问,同时请唤起浏览器的控制台面板(例如 Chrome 浏览器是按 F12 或者鼠标右键点 “检查” ),在 Console 面板查看 Log 的打印。

html
<!-- 这是使用 Vue 实现的 demo -->
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Hello Vue</title>
+    <script src="https://unpkg.com/vue@3"></script>
+  </head>
+  <body>
+    <div id="app">
+      <!-- 通过 \`{{ 变量名 }}\` 语法渲染响应式变量 -->
+      <p>Hello {{ name }}!</p>
+
+      <!-- 通过 \`v-model\` 双向绑定响应式变量 -->
+      <!-- 通过 \`@input\` 给输入框绑定输入事件 -->
+      <input
+        type="text"
+        v-model="name"
+        placeholder="输入名称打招呼"
+        @input="printLog"
+      />
+
+      <!-- 通过 \`@click\` 给按钮绑定点击事件 -->
+      <button @click="reset">重置</button>
+    </div>
+
+    <script>
+      const { createApp, ref } = Vue
+      createApp({
+        // \`setup\` 是一个生命周期钩子
+        setup() {
+          // 默认值
+          const DEFAULT_NAME = 'World'
+
+          // 用于双向绑定的响应式变量
+          const name = ref(DEFAULT_NAME)
+
+          // 打印响应式变量的值到控制台
+          function printLog() {
+            // \`ref\` 变量需要通过 \`.value\` 操作值
+            console.log(name.value)
+          }
+
+          // 重置响应式变量为默认值
+          function reset() {
+            name.value = DEFAULT_NAME
+            printLog()
+          }
+
+          // 需要 \`return\` 出去才可以被模板使用
+          return { name, printLog, reset }
+        },
+      }).mount('#app')
+    </script>
+  </body>
+</html>

这是一个基于 Vue 3 组合式 API 语法的 demo ,它包含了两个主要功能:

  1. 可以在输入框修改输入内容,上方的 Hello World! 以及浏览器控制台的 Log 输出,都会随着输入框内容的变更而变化
  2. 可以点击 “重置” 按钮,响应式变量被重新赋值的时候,输入框的内容也会一起变化为新的值

这是 Vue 的特色之一:数据的双向绑定。

对比普通的 HTML 文件需要通过输入框的 oninput 事件手动编写视图的更新逻辑, Vue 的双向绑定功能大幅度减少了开发过程的编码量。

在未接触 Vue 这种编程方式之前,相信大部分人首先想到的是直接操作 DOM 来实现需求,为了更好的进行对比,接下来用原生 JavaScript 实现一次相同的功能:

html
<!-- 这是使用原生 JavaScript 实现的 demo -->
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Hello World</title>
+  </head>
+  <body>
+    <div id="app">
+      <!-- 通过一个 \`span\` 标签来指定要渲染数据的位置 -->
+      <p>Hello <span id="name"></span>!</p>
+
+      <!-- 通过 \`oninput\` 给输入框绑定输入事件 -->
+      <input
+        id="input"
+        type="text"
+        placeholder="输入名称打招呼"
+        oninput="handleInput()"
+      />
+
+      <!-- 通过 \`onclick\` 给按钮绑定点击事件 -->
+      <button onclick="reset()">重置</button>
+    </div>
+
+    <script>
+      // 默认值
+      const DEFAULT_NAME = 'World'
+
+      // 要操作的 DOM 元素
+      const nameElement = document.querySelector('#name')
+      const inputElement = document.querySelector('#input')
+
+      // 处理输入
+      function handleInput() {
+        const name = inputElement.value
+        nameElement.innerText = name
+        printLog()
+      }
+
+      // 打印输入框的值到控制台
+      function printLog() {
+        const name = inputElement.value
+        console.log(name)
+      }
+
+      // 重置 DOM 元素的文本和输入框的值
+      function reset() {
+        nameElement.innerText = DEFAULT_NAME
+        inputElement.value = DEFAULT_NAME
+        printLog()
+      }
+
+      // 执行一次初始化,赋予 DOM 元素默认文本和输入框的默认值
+      window.addEventListener('load', reset)
+    </script>
+  </body>
+</html>

虽然两个方案总的代码量相差不大,但可以看到两者的明显区别:

  1. Vue 只需要对一个 name 变量的进行赋值操作,就可以轻松实现视图的同步更新
  2. 使用原生 JavaScript 则需要频繁的操作 DOM 才能达到输入内容即时体现在文本 DOM 上面,并且还要考虑 DOM 是否已渲染完毕,否则操作会出错

Vue 的这种编程方式,称之为 “数据驱动” 编程。

如果在一个页面上频繁且大量的操作真实 DOM ,频繁的触发浏览器回流( Reflow )与重绘( Repaint ),会带来很大的性能开销,从而造成页面卡顿,在大型项目的性能上很是致命。

而 Vue 则是通过操作虚拟 DOM ( Virtual DOM ,简称 VDOM ),每一次数据更新都通过 Diff 算法找出需要更新的节点,只更新对应的虚拟 DOM ,再去映射到真实 DOM 上面渲染,以此避免频繁或大量的操作真实 DOM 。

TIP

虚拟 DOM 是一种编程概念,是指将原本应该是真实 DOM 元素的 UI 界面,用数据结构来组织起完整的 DOM 结构,再同步给真实 DOM 渲染,减少浏览器的回流与重绘。

在 JavaScript 里,虚拟 DOM 的表现是一个 Object 对象,其中需要包含指定的属性(例如 Vue 的虚拟 DOM 需要用 type 来指定当前标签是一个 <div /> 还是 <span /> ),然后框架会根据对象的属性去转换为 DOM 结构并最终完成内容的显示。

更多关于 Vue 虚拟 DOM 和性能优化可以查看官网的 渲染机制 一章进行了解。

Vue 3.0 版本还引入了组合式 API 的概念,更符合软件工程 “高内聚,低耦合” 的思想,让开发者可以更灵活的管理自己的逻辑代码,更方便的进行抽离封装再复用,不管是大型项目还是流水线业务,开箱即用的逻辑代码都是提升开发效率的利器。

Vue 与工程化之间的关联

在已经对 Vue 做了初步了解之后,可能有读者会问:“既然 Vue 的使用方式也非常简单,可以像 jQuery 这些经典类库一样在 HTML 引入使用,那么 Vue 和工程化有什么关联呢?”

Vue.js 是一个框架,框架除了简化编码过程中的复杂度之外,面对不同的业务需求还提供了通用的解决方案,而这些解决方案,通常是将前端工程化里的很多种技术栈组合起来串成一条条技术链,一环扣一环,串起来就是一个完整的工程化项目。

举一个常见的例子,比如上一节内容 了解 Vue.js 与全新的 3.0 版本 里的 demo 是一个简单的 HTML 页面,如果业务稍微复杂一点,比如区分了 “首页” 、 “列表页” 、 “内容页” 这样涉及到多个页面,传统的开发方案是通过 A 标签跳转到另外一个页面,在跳转期间会产生 “新页面需要重新加载资源、会有短暂白屏” 等情况,用户体验不太好。

Vue 提供了 Vue Router 实现路由功能,利用 History API 实现单页面模式(可在 现代化的开发概念 部分了解区别),在一个 HTML 页面里也可以体验 “页面跳转” 这样的体验,但如果页面很多,所有代码都堆积在一个 HTML 页面里,就很难维护。

借助前端工程化的构建工具,开发者可以编写 .vue 单组件文件,将多个页面的代码根据其功能模块进行划分,可拆分到多个单组件文件里维护并进行合理复用,最终通过构建工具编译再合并,最终生成浏览器能访问的 HTML / CSS / JS 文件,这样的开发过程,用户体验没有影响,但开发体验大大提升。

类似这样一个个业务场景会积少成多,把 Vue 和工程化结合起来,处理问题更高效更简单。

选择 Vue 入门工程化的理由

虽然前端的流行框架有主流的 Angular 、 React 和 Vue ,也有新兴的 Svelte 等等,每一个框架都有自己的特色,那为什么建议选择 Vue 来入门工程化呢?

最主要的两个原因是:

  1. 职场对 Vue 技术栈的需求量大,容易找工作
  2. 上手门槛低,会一些基础的 HTML / CSS / JavaScript 语法知识,就能够轻松上手 Vue 的组件开发

第一个原因在 求职竞争上的优势 已有进行过说明,掌握一门流行框架已经是前端岗位必备的技能,几乎所有公司在招聘前端工程师的时候都要求会 Vue 。

这里主要讲讲第二个原因,在 Vue 与工程化之间的关联 里提到了开发者可以编写 .vue 文件,这是一个 Vue 专属的文件扩展名,官方名称是 Single-File Component ,简称 SFC ,也就是单文件组件。

.vue 文件最大的特色就是支持像编写 .html 文件一样,在文件里写 HTML / CSS / JS 代码,不仅结构相似,在代码书写上,两者的语法也是十分接近:

.vue 文件.html 文件
<template /> 部分HTML 代码
<style /> 部分CSS 代码
<script /> 部分JavaScript 代码

下面就是一个最基础的 Vue 组件结构,可以看到和 HTML 文件是非常的相似:

vue
<!-- \`template\` 对应 HTML 代码 -->
+<template>
+  <div>
+    <!-- 一些 HTML  -->
+  </div>
+</template>
+
+<!-- \`script\` 部分对应 JavaScript 代码 -->
+<!-- 还支持其他语言,例如 \`lang="ts"\` 代表当前使用 TypeScript 编写 -->
+<script>
+export default {
+  // 这里是变量、函数等逻辑代码
+}
+</script>
+
+<!-- \`style\` 部分对应 CSS 代码 -->
+<!-- 还支持开启 \`scoped\` 标识,让 CSS 代码仅对当前组件生效,不会全局污染 -->
+<style scoped>
+/* 一些 CSS 代码 */
+</style>

Vue 组件不仅支持这些语言的所有基础用法,还增加了非常多更高效的功能,在后面 Vue3 教程的 单组件的编写 一章会有详细的介绍。

现代化的开发概念

在本章最开始的时候提到了 SPA / SSR / SSG 等词汇,这些词汇是一些现代前端工程化开发的概念名词缩写,代表着不同的开发模式和用户体验。

当下主流的前端框架都提供了这些开发模式的支持,因此在学习前端工程化和 Vue 开发的过程中,会不定期的看到这一类词汇,在实际工作业务的技术选型时,面对不同的业务场景也要考虑好需要使用什么样的开发模式,提前了解这些概念,对以后的工作也会很有帮助。

MPA 与 SPA

首先来看 MPA 与 SPA ,这代表着两个完全相反的开发模式和用户体验,它们的全称和中文含义如下:

名词全称中文
MPAMulti-Page Application多页面应用
SPASingle-Page Application单页面应用

多页面应用

MPA 多页面应用是最传统的网站体验,当一个网站有多个页面时,会对应有多个实际存在的 HTML 文件,访问每一个页面都需要经历一次完整的页面请求过程:

bash
# 传统的页面跳转过程
+
+从用户点击跳转开始:
+---> 浏览器打开新的页面
+---> 请求【所有】资源
+---> 加载 HTML 、CSS  JS  图片等资源
+---> 完成新页面的渲染
MPA 的优点

作为最传统也是最被广泛运用的模式,自然有它的优势存在:

  • 首屏加载速度快

因为 MPA 的页面源码都是实实在在的写在 HTML 文件里,所以当 HTML 文件被访问成功,内容也就随即呈现(在不考虑额外的 CSS 、 图片加载速度的情况下,这种模式的内容呈现速度是最快的)。

  • SEO 友好,容易被搜索引擎收录

如果读者有稍微了解过一些 SEO 知识,会知道除了网页的 TKD 三要素之外,网页的内容也影响收录的关键因素,传统的多页面应用,网页的内容都是直接位于 HTML 文件内,例如下面这个有很多内容的网页:

`,51),A=o("p",null,[e("右键查看该网页的源代码,可以看到网页内容对应的 HTML 结构也是包含在 "),o("code",null,".html"),e(" 文件里。")],-1),h=p(`

TIP

网页的 TKD 三要素是指一个网页的三个关键信息,含义如下:

T ,指 Title ,网站的标题,即网页的 <title>网站的标题</title> 标签。

K ,指 Keywords ,网站的关键词,即网页的 <meta name="Keywords" content="关键词1,关键词2,关键词3" /> 标签。

D ,指 Description ,网站的描述,即网页的 <meta name="description" content="网站的描述" /> 标签。

这三个要素标签都位于 HTML 文件的 <head /> 标签内。

  • 容易与服务端语言结合

由于传统的页面都是由服务端直出,所以可以使用 PHP 、 JSP 、 ASP 、 Python 等非前端语言或技术栈来编写页面模板,最终输出 HTML 页面到浏览器访问。

MPA 的缺点

说完 MPA 的优点,再来看看它的缺点,正因为有这些缺点的存在,才会催生出其他更优秀的开发模式出现。

  • 页面之间的跳转访问速度慢

正如它的访问流程,每一次页面访问都需要完整的经历一次渲染过程,哪怕从详情页 A 的 “相关阅读” 跳转到详情页 B ,这种网页结构一样,只有内容不同的两个页面,也需要经历这样的过程。

  • 用户体验不够友好

如果网页上的资源较多或者网速不好,这个过程就会有明显的卡顿或者布局错乱,影响用户体验。

  • 开发成本高

传统的多页面模式缺少前端工程化的很多优秀技术栈支持,前端开发者在刀耕火种的开发过程中效率低下。如果是基于 PHP 等非前端语言开发,工作量通常更是压在一名开发者身上,无法做到前后端分离来利用好跨岗位协作。

TIP

此处列举的多页面应用问题均指传统开发模式下的多页面,之所以特地说明,是因为后文还会有新的技术栈来实现多页面应用,但实现原理和体验并不一样。

单页面应用

正因为传统的多页面应用存在了很多无法解决的开发问题和用户体验问题,催生了现代化的 SPA 单页面应用技术的诞生。

SPA 单页面应用是现代化的网站体验,与 MPA 相反,不论站点内有多少个页面,在 SPA 项目实际上只有一个 HTML 文件,也就是 index.html 首页文件。

它只有第一次访问的时候才需要经历一次完整的页面请求过程,之后的每个内部跳转或者数据更新操作,都是通过 AJAX 技术来获取需要呈现的内容并只更新指定的网页位置。

TIP

AJAX 技术( Asynchronous JavaScript and XML )是指在不离开页面的情况下,通过 JavaScript 发出 HTTP 请求,让网页通过增量更新的方式呈现给用户界面,而不需要刷新整个页面来重新加载,是一种 “无刷体验” 。

SPA 在页面跳转的时候,地址栏也会发生变化,主要有以下两种方式:

  1. 通过修改 Location:hash 修改 URL 的 Hash 值(也就是 # 号后面部分),例如从 https://example.com/#/foo 变成 https://example.com/#/bar
  2. 通过 History API 的 pushState 方法更新 URL ,例如从 https://example.com/foo 变成 https://example.com/bar

这两个方式的共同特点是更新地址栏 URL 的时候,均不会刷新页面,只是单纯的变更地址栏的访问地址,而网页的内容则通过 AJAX 更新,配合起来就形成了一种网页的 “前进 / 后退” 等行为效果。

TIP

Vue Router 默认提供了这两种 URL 改变方式的支持,分别是 createWebHashHistory 的 Hash 模式和 createWebHistory 对应的 History 模式,在 路由的使用 一章可以学习更多 Vue 路由的使用。

理解了实现原理之后,可以把 SPA 的请求过程简化为如下步骤:

bash
# SPA 页面跳转过程
+
+从用户点击跳转开始:
+---> 浏览器通过 \`pushState\` 等方法更新 URL
+---> 请求接口数据(如果有涉及到前后端交互)
+---> 通过 JavaScript 处理数据,拼接 HTML 片段
+--->  HTML 片段渲染到指定位置,完成页面的 “刷新”
SPA 的优点

从上面的实现原理已经能总结出它的优势了:

  • 只有一次完全请求的等待时间(首屏加载)
  • 用户体验好,内部跳转的时候可以实现 “无刷切换”
  • 因为不需要重新请求整个页面,所以切换页面的时候速度更快
  • 因为没有脱离当前页面,所以 “页” 与 “页” 之间在切换过程中支持动画效果
  • 脱离了页面跳页面的框架,让整个网站形成一个 Web App ,更接近原生 App 的访问体验
  • 开发效率高,前后端分离,后端负责 API 接口,前端负责界面和联调,同步进行缩短工期

这也是为什么短短几年时间, SPA 的体验模式成为前端领域的主流。

SPA 的缺点

虽然 SPA 应用在使用过程中的用户体验非常好,但也有自身的缺点存在:

  • 首屏加载相对较慢

由于 SPA 应用的路由是由前端控制, SPA 在打开首页后,还要根据当前的路由再执行一次内容渲染,相对于 MPA 应用从服务端直出 HTML ,首屏渲染所花费的时间会更长。

  • 不利于 SEO 优化

由于 SPA 应用全程是由 JavaScript 控制内容的渲染,因此唯一的一个 HTML 页面 index.html 通常是一个空的页面,只有最基础的 HTML 结构,不仅无法设置每个路由页面的 TDK ,页面内容也无法呈现在 HTML 代码里,因此对搜索引擎来说,网站的内容再丰富,依然只是一个 “空壳” ,无法让搜索引擎进行内容爬取。

`,33),u=p('

为了减少用户等待过程中的焦虑感,可以通过增加 Loading 过程,或者 Skeleton 骨架屏等优化方案,但其实也是治标不治本,因此为了结合 SPA 和 MPA 的优点,又进一步催生出了更多实用的技术方案以适配更多的业务场景,在后面的小节将逐一介绍。

CSR 与 SSR

在了解了 MPA 与 SPA 之后,先了解另外两个有相关联的名词: CSR 与 SSR ,同样的,这一对也是代表着相反的开发模式和用户体验,它们的全称和中文含义如下:

名词全称中文
CSRClient-Side Rendering客户端渲染
SSRServer-Side Rendering服务端渲染

正如它们的名称,这两者代表的是渲染网页过程中使用到的技术栈。

客户端渲染

MPA 多页面应用与 SPA 单页面应用 部分的介绍过的 SPA 单页面应用,正是基于 CSR 客户端渲染实现的(因此大部分情况下, CSR 等同于 SPA ,包括实现原理和优势),这是一种利用 AJAX 技术,把渲染工作从服务端转移到客户端完成,不仅客户端的用户体验更好,前后端分离的开发模式更加高效。

但随之而来的首屏加载较慢、不利于 SEO 优化等缺点,而 SPA 的这几个缺点,却是传统 MPA 多页面应用所具备的优势,但同样的, MPA 也有着自己开发成本高、用户体验差等问题。

既然原来的技术方案无法完美满足项目需求,因此在结合 MPA 的优点和 SPA 的优点之后,一种新的技术随之诞生,这就是 SSR 服务端渲染。

服务端渲染

和传统的 MPA 使用 PHP / JSP 等技术栈做服务端渲染不同,现代前端工程化里的 SSR 通常是指使用 Node.js 作为服务端技术栈。

TIP

工程化神器 Node.js 一节会介绍 Node ,以及它对前端工程化带来的重大变化,现代前端工程化发展离不开它的存在。

传统的服务端渲染通常由后端开发者一起维护前后端代码,需要写后端语言支持的模板、 JavaScript 代码维护成本也比较高;而 SSR 服务端渲染则是交给前端开发者来维护,利用 Node 提供的能力进行同构渲染,由于本身前后端都使用 JavaScript 编写,维护成本也大大的降低。

SSR 技术利用的同构渲染方案( Isomorphic Rendering ),指的是一套代码不仅可以在客户端运行,也可以在服务端运行,在一些合适的时机先由服务端完成渲染( Server-Side Rendering )再直出给客户端激活( Client-Side Hydration ),这种开发模式带来了:

  • 更好的 SEO 支持,解决了 SPA 单页面应用的痛点
  • 更快的首屏加载速度,保持了 MPA 多页面应用的优点
  • 和 SPA 一样支持前后端分离,开发效率依然很高
  • 有更好的客户端体验,当用户完全打开页面后,本地访问过程中也可以保持 SPA 单页面应用的体验
  • 统一的心智模型,由于支持同构,因此没有额外的心智负担

那么,使用 Vue 开发项目时,应该如何实现 SSR 呢?

Vue 的 SSR 支持非常好, Vue 官方不仅提供了一个 Vue.js 服务器端渲染指南 介绍了基于 Vue 的 SSR 入门实践,还有基于 Vue 的 Nuxt.jsQuasar 框架帮助开发者更简单地落地 SSR 开发,构建工具 Vite 也有内置的 Vue SSR 支持。

Pre-Rendering 与 SSG

在介绍了 SSR 服务端渲染技术后,读者可能会想到一个问题,就是 SSR 的开发成本总归比较高,如果本身项目比较简单,例如一个静态博客,或者静态官网、落地页等内容不多,仅需要简单的 SEO 支持的项目需求,是否有更简便的方案呢?

以下两种方案正是用于满足这类需求的技术:

名词全称中文
Pre-RenderingPre-Rendering预渲染
SSGStatic-Site Generation静态站点生成

预渲染

预渲染也是一种可以让 SPA 单页面应用 解决 SEO 问题的技术手段。

预渲染的原理是在构建的时候启动无头浏览器( Headless Browser ),加载页面的路由并将访问结果按照路由的路径保存到静态 HTML 文件里,这样部署到服务端的页面,不再是一个空的 HTML 页面,而是有真实内容的存在,但由于只在构建时运行,因此用户每次访问的时候 HTML 里的内容不会产生变化,直到下一次构建。

TIP

无头浏览器( Headless Browser ),指没有 GUI 界面的浏览器,使用代码通过编程接口来控制浏览器的行为,常用于网络爬虫、自动化测试等场景,预渲染也使用它来完成页面的渲染,以获取渲染后的代码来填充 HTML 文件。

预渲染和 服务端渲染 最大的区别在于,预渲染只在构建的时候就完成了页面内容的输出(发生在用户请求前),因此构建后不论用户何时访问, HTML 文件里的内容都是构建的时候的那份内容,所以预渲染适合一些简单的、有一定的 SEO 要求但对内容更新频率没有太高要求、内容多为静态展示的页面。

例如企业用于宣传的官网页面、营销活动的推广落地页都非常适合使用预渲染技术,现代的构建工具都提供了预渲染的内置实现,例如这个教程: 用 Vite 更简单的解决 Vue3 项目的预渲染问题 ,就是通过 Vite 的内置功能来实现预渲染,最终也运用到了公司的业务上。

静态站点生成

SSG 静态站点生成是基于预渲染技术,通过开放简单的 API 和配置文件,就让开发者可以实现一个预渲染静态站点的技术方案。

它可以让开发者定制站点的个性化渲染方案,但更多情况下,通常是作为一些开箱即用的技术产品来简化开发过程中的繁琐步骤,这一类技术产品通常称之为静态站点生成器( Static-Site Generator ,也是简称 SSG )。

常见的 SSG 静态站点生成器有:基于 Vue 技术的 VuePressVitePress ,自带了 Vue 组件的支持,还有基于 React 的 Docusaurus ,以及很多各有特色的生成器,例如 JekyllHugo 等等。

如果有写技术文档或者博客等内容创作需求,使用静态站点生成器是一个非常方便的选择,通常这一类产品还有非常多的个性化主题可以使用。

ISR 与 DPR

在现代化的开发概念这一节,从 MPA 多页面应用到 SPA 单页面应用 ,再到 CSR 客户端渲染和 SSR 服务端渲染 ,以及 Pre-Rendering 预渲染与 SSG 静态站点生成 ,似乎已经把所有常见的开发场景覆盖完了。

那接下来要讲的 ISR 和 DPR 又是什么用途的技术方案呢?先看看它们的全称和中文含义:

名词全称中文
ISRIncremental Site Rendering增量式的网站渲染
DPRDistributed Persistent Rendering分布式的持续渲染

当网站的内容体量达到一定程度的时候,从头开始构建进行预渲染所花费的时间会非常久,而实际上并不是所有页面的内容都需要更新,这两项技术的推出是为了提升大型项目的渲染效率。

ISR 增量式的网站渲染,通过区分 “关键页面” 和 “非关键页面” 进行构建,优先预渲染 “关键页面” 以保证内容的最新和正确,同时缓存到 CDN ,而 “非关键页面” 则交给用户访问的时候再执行 CSR 客户端渲染,并触发异步的预渲染缓存到 CDN 。

这样做的好处是,大幅度的提升了每次构建的时间,但由于只保证部分 “关键页面” 的构建和内容正确,所以访问 “非关键页面” 的时候,有可能先看到旧的内容,再由 CSR 刷新为新的内容,会丢失一部分用户体验。

更多 ISR 技术细节可以阅读 Netlify 的开发者体验总监 Cassidy Williams 的一篇文章: Incremental Static Regeneration: Its Benefits and Its Flaws

DPR 分布式的持续渲染则是为了解决 ISR 方案下可能访问到旧内容的问题,这也是由 Cassidy Williams 发起的一个提案,详情可在 GitHub 查看:Distributed Persistent Rendering (DPR)

由于目前这两项技术还在发展初期,能够支持的框架和服务还比较少,在这里建议作为一种技术知识储备提前了解,在未来的某一天有业务需要的时候,也可以知道有这样的方案可以解决问题。

工程化不止于前端

现代化的开发概念 部分所讲述的都是关于网页开发的变化,当然,前端这个岗位本身就是从页面开发发展起来的,自然还是离不开网页这个老本行。

但随着前端工程化的发展,前端越来越不止于写前端,已经有很多前端工程师利用前端工程化带来的优势,不仅仅只是做一个 Web 前端,开始逐步发展为一个全栈工程师,在企业内部承担起了更多的岗位职责,包括作者笔者也是。

之所以能做这么多事情,得益于 Node.js 在前端开发带来的翻天覆地的变化,可以在保持原有的 JavaScript 和 TypeScript 基础上,几乎没有过多的学习成本就可以过度到其他端的开发。

在了解 Node.js 之前,先来看看现在的前端开发工程师除了写 Web 前端,还可以做到哪些岗位的工作。

服务端开发

在传统的认知里,如果一个前端工程师想自己搭建一个服务端项目,需要学习 Java 、 PHP 、 Go 等后端语言,还需要学习 Nginx 、 Apache 等 Web Server 程序的使用,并使用这些技术来开发并部署一个项目的服务端。

现在的前端工程师可以利用 Node.js ,单纯使用 JavaScript 或者 TypeScript 来开发一个基于 Node 的服务端项目。

Node 本身是一个 JavaScript 的运行时,还提供了 HTTP 模块 可以启动一个本地 HTTP 服务,如果把 Node 项目部署到服务器上,就可以运行一个可对外访问的公网服务。

但 Node 的原生服务端开发成本比较高,因此在 GitHub 开源社区也诞生了很多更方便的、开箱即用、功能全面的服务端框架,根据它们的特点,可以简单归类如下:

ExpressKoaFastify 为代表的轻量级服务端框架,这一类框架的特点是 “短平快” ,对于服务端需求不高,只是跑一些小项目的话,开箱即用非常地方便,比如 Build 了一个 Vue 项目,然后提供一个读取静态目录的服务来访问它。

但是 “短平快” 框架带来了一些团队协作上的弊端,如果缺少一些架构设计的能力,很容易把一个服务端搭的很乱以至于难以维护,比如项目的目录结构、代码的分层设计等等,每个创建项目的人都有自己的想法和个人喜好,就很难做到统一管理。

因此在这些框架的基础上,又诞生了以 Nest (底层基于 Express ,可切换为 Fastify )、 Egg (基于 Koa )为代表的基于 MVC 架构的企业级服务端框架,这一类框架的特点是基于底层服务进行了更进一步的架构设计并实现了代码分层,还自带了很多开箱即用的 Building Blocks ,例如 TypeORM 、WebSockets 、Swagger 等等,同样也是开箱即用,对大型项目的开发更加友好。

TIP

当然, Node.js 所做的事情是解决服务端程序部分的工作,如果涉及到数据存储的需求,学习 MySQL 和 Redis 的技术知识还是必不可少的!

App 开发

常规的 Native App 原生开发需要配备两条技术线的支持:使用 Java / Kotlin 语言开发 Android 版本,使用 Objective-C / Swift 语言开发 iOS 版本,这对于创业团队或者个人开发者来说都是一个比较高的开发成本。

前端开发者在项目组里对 App 的作用通常是做一些活动页面、工具页面内嵌到 App 的 WebView 里,如果是在一些产品比较少的团队里,例如只有一个 App 产品,那么前端的存在感会比较低。

而 Hybrid App 的出现,使得前端开发者也可以使用 JavaScript / TypeScript 来编写混合 App ,只需要了解简单的打包知识,就可以参与到一个 App 的开发工作中。

开发 Hybrid App 的过程通常称为混合开发,最大的特色就是一套代码可以运行到多个平台,这是因为整个 App 只有一个基座,里面的 App 页面都是使用 UI WebView 来渲染的 Web 界面,因此混合开发的开发成本相对于原生开发是非常低的,通常只需要一个人 / 一个小团队就可以输出双平台的 App ,并且整个 App 的开发周期也会更短。

在用户体验方面, Hybrid App 相对于 Native App ,一样可以做到:

  • 双平台的体验一致性
  • 支持热更新,无需用户重新下载整个 App
  • 内置的 WebView 在交互体验上也可以做到和系统交互,比如读取 / 存储照片、通讯录,获取定位等等
  • 支持 App Push 系统通知推送
  • 还有很多 Native App 具备的功能

基本上 Native App 的常见功能,在 Hybrid App 都能满足。

而且大部分情况下,在构建 Hybrid App 的时候还可以顺带输出一个 Web App 版本,也就是让这个 App 在被用户下载前,也有一模一样的网页版可以体验,这对于吸引新用户是非常有用的。

在混合开发的过程中,通常是由前端开发者来负责 App 项目从 “开发” 到 “打包” 再到 “发版” 的整个流程,在开发的过程中是使用常见的前端技术栈,例如目前主流的有基于 Vue 的 uni-app 、基于 React 的 React Native 等等,这些 Hybrid 框架都具备了 “学习成本低、开发成本低、一套代码编译多个平台” 的特点。

在 App 开发完毕后,使用 Hybrid 框架提供的 CLI 工具编译出 App 资源包,再根据框架提供的原生基座打包教程去完成 Android / iOS 的安装包构建,这个环节会涉及到原生开发的知识,例如 Android 包的构建会使用到 Android Studio ,但整个过程使用到原生开发的环节非常少,几乎没有太高的学习门槛。

桌面程序开发

放在以前要开发一个 Windows 桌面程序,需要用上 QT / WPF / WinForm 等技术栈,还要学习 C++ / C# 之类的语言,对于只想在业余写几个小工具的开发者来说,上手难度和学习成本都很高,但在前端工程化的时代里,使用 JavaScript 或 TypeScript 也可以满足程序开发的需要。

这得益于 Electron / Tauri 等技术栈的出现,其中 Electron 的成熟度最高、生态最完善、最被广泛使用,除了可以构建 Windows 平台支持的 .exe 文件之外,对 macOS 和 Linux 平台也提供了对应的文件构建支持。

广大前端开发者每天都在使用的 Visual Studio Code 以及知名的 HTTP 网络测试工具 Postman 都是使用 Electron 开发的。

',71),g=p('

笔者也通过 Electron 构建了多个给公司内部使用的界面化工具客户端,这一类技术栈对于前端开发者来说,真的非常方便!在这里以 Electron 为例,简单讲解下它的工作原理,以了解为什么程序开发可以如此简单。

Electron 的底层是基于 Chromium 和 Node.js ,它提供了两个进程供开发者使用:

  1. 主进程:它是整个应用的入口点,主进程运行在 Node 环境中,可以使用所有的 Node API ,程序也因此具备了和系统进行交互的能力,例如文件的读写操作。

  2. 渲染进程:负责与用户交互的 GUI 界面,基于 Chromium 运行,所以开发者得以使用 HTML / CSS / JavaScript 像编写网页一样来编写程序的 GUI 界面。

一个程序应用只会有一个主进程,而渲染进程则可以根据实际需求创建多个,渲染进程如果需要和系统交互,则必须与主进程通信,借助主进程的能力来实现。

在构建的时候, Electron 会把 Node 和 Chromium 一起打包为一个诸如 .exe 这样的安装文件(或者是包含了两者的免安装版本),这样用户不需要 Node 环境也可以运行桌面程序。

应用脚本开发

桌面程序开发 部分讲的是构建一种拥有可视化 GUI 界面的程序,但有时候并不需要复杂的 GUI ,可能只想提供一个双击运行的脚本类程序给用户,现在的前端工程化也支持使用 JavaScript 构建一个无界面的应用脚本。

假如某一天公司的运营小姐姐希望能做一个自动化的脚本减轻她们的机械操作,或者是自己工作过程中发现一些日常工作可以交付给脚本解决的情况,就可以使用这种方式来输出一个脚本程序,使用的时候双击运行非常方便。

笔者之前为了让团队的工程师减少写日报的心智负担,也是使用了这个方式编写了一个 git-commit-analytics 工具,部门里的工程师可以通过规范化 commit 来生成每天的工作日报,每天双击一下就可以生成一份报告,很受团队的喜欢。

',9),b=p(`

在这里推荐一个工具 Pkg ,它可以把 Node 项目打包为一个可执行文件,支持 Windows 、 macOS 、 Linux 等多个平台,它的打包机制和 Electron 打包的思路类似,也是通过把 Node 一起打包,让用户可以在不安装 Node 环境的情况下也可以直接运行脚本程序。

实践工程化的流程

基于 Vue 3 的项目,最主流的工程化组合拳有以下两种:

常用方案Runtime构建工具前端框架
方案一NodeWebpackVue
方案二NodeViteVue

方案一是比较传统并且过去项目使用最多的方案组合,但从 2021 年初随着 Vite 2.0 的发布,伴随着更快的开发体验和日渐丰富的社区生态,新项目很多都开始迁移到方案二,因此本书秉着面向当下与未来的原则,会侧重 Vite 的使用来开展讲解,包括一些 demo 的创建等等。

当技术成熟的时候,还可以选择更喜欢的方案自行组合,例如用 Deno 来代替 Node ,但前期还是按照主流的方案来进入工程化的学习。

下面的内容将根据 Vue 3 的工程化开发,逐一讲解涉及到常用的工具,了解它们的用途和用法。

工程化神器 Node.js

只要在近几年有接触过前端开发,哪怕没有实际使用过,也应该有听说过 Node.js ,那么它是一个什么样的存在?

什么是 Node.js

Node.js (简称 Node ) 是一个基于 Chrome V8 引擎构建的 JS 运行时( JavaScript Runtime )。

它让 JavaScript 代码不再局限于网页上,还可以跑在客户端、服务端等场景,极大的推动了前端开发的发展,现代的前端开发几乎都离不开 Node 。

什么是 Runtime

Runtime ,可以叫它 “运行时” 或者 “运行时环境” ,这个概念是指,项目的代码在哪里运行,哪里就是运行时。

传统的 JavaScript 只能跑在浏览器上,每个浏览器都为 JS 提供了一个运行时环境,可以简单地把浏览器当成一个 Runtime ,明白了这一点,相信就能明白什么是 Node 。

Node 就是一个让 JS 可以脱离浏览器运行的环境,当然,这里并不是说 Node 就是浏览器。

Node 和浏览器的区别

虽然 Node 也是基于 Chrome V8 引擎构建,但它并不是一个浏览器,它提供了一个完全不一样的运行时环境,没有 Window 、没有 Document 、没有 DOM 、没有 Web API ,没有 UI 界面…

但它提供了很多浏览器做不到的能力,比如和操作系统的交互,例如 “文件读写” 这样的操作在浏览器有诸多的限制,而在 Node 则轻轻松松。

对于前端开发者来说, Node 的巨大优势在于,使用一种语言就可以编写所有东西(前端和后端),不再花费很多精力去学习各种各样的开发语言。

哪怕仅仅只做 Web 开发,也不再需要顾虑新的语言特性在浏览器上的兼容性( e.g. ES6 、 ES7 、 ES8 、 ES9 …), Node 配合构建工具,以及诸如 Babel 这样的代码编译器,可以帮转换为浏览器兼容性最高的 ES5 。

当然还有很多工程化方面的好处,总之一句话,使用 Node 的开发体验会非常好。

工程化的入门准备 一章中,会对 Node 开发做进一步的讲解,下面先继续顺着 Node 的工具链,了解与日常开发息息相关的前端构建工具。

工程化的构建工具

在前端开发领域,构建工具已经成为现在必不可少的开发工具了,很多刚接触前端工程化的开发者可能会有疑惑,为什么以前的前端页面直接编写代码就可以在浏览器访问,现在却还要进行构建编译,是否 “多此一举” ?

要消除这些困惑,就需要了解一下为什么要使用构建工具,知道构建工具在开发上能够给带来什么好处。

为什么要使用构建工具

目前已经有很多流行的构建工具,例如: GruntGulpWebpackSnowpackParcelRollupVite … 每一个工具都有自己的特色。

如上面列举的构建工具,虽然具体到某一个工具的时候,是 “一个” 工具,但实际上可以理解为是 “一套” 工具链、工具集,构建工具通常集 “语言转换 / 编译” 、 “资源解析” 、 “代码分析” 、 “错误检查” 、 “任务队列” 等非常多的功能于一身。

构建工具可以帮解决很多问题,先看看最基础的一个功能支持: “语言转换 / 编译” 。

且不说构建工具让可以自由自在的在项目里使用 TypeScript 这些新兴的语言,单纯看历史悠久的 JavaScript ,从 2015 年开始,每年也都会有新的版本发布(例如 ES6 对应 ES2015 、 ES7 对应 ES2016 、 ES8 对应 ES2017 等等)。

虽然新版本的 JS API 更便捷更好用,但浏览器可能还没有完全支持,这种情况下可以通过构建工具去转换成兼容度更高的低版本 JS 代码。

举个很常用到的例子,现在判断一个数组是否包含某个值,通常会这么写:

js
// 声明一个数组
+const arr = ['foo', 'bar', 'baz']
+
+// 当数组包含 foo 这个值时,处理一些逻辑
+if (arr.includes('foo')) {
+  // do something…
+}

通过 Array.prototype.includes() 这个实例方法返回的布尔值,判断数组是否包含目标值,而这个方法是从 ES6 开始支持的,对于不支持 ES6 的古董浏览器,只能使用其他更早期的方法代替( e.g. indexOf ),或者手动引入它的 Polyfill 来保证这个方法可用。

TIP

Polyfill 是在浏览器不支持的情况下实现某个功能的代码,可以在概念发明者 Remy Sharp 的博文里了解到它的由来,是一个挺有意思的命名。

点击阅读: What is a Polyfill?

以下是摘选自 MDN 网站上关于 Array.prototype.includes() 的 Polyfill 实现:

js
// https://tc39.github.io/ecma262/#sec-array.prototype.includes
+if (!Array.prototype.includes) {
+  Object.defineProperty(Array.prototype, 'includes', {
+    value: function (valueToFind, fromIndex) {
+      if (this == null) {
+        throw new TypeError('"this" is null or not defined')
+      }
+
+      // 1. Let O be ? ToObject(this value).
+      var o = Object(this)
+
+      // 2. Let len be ? ToLength(? Get(O, "length")).
+      var len = o.length >>> 0
+
+      // 3. If len is 0, return false.
+      if (len === 0) {
+        return false
+      }
+
+      // 4. Let n be ? ToInteger(fromIndex).
+      //    (If fromIndex is undefined, this step produces the value 0.)
+      var n = fromIndex | 0
+
+      // 5. If n ≥ 0, then
+      //  a. Let k be n.
+      // 6. Else n < 0,
+      //  a. Let k be len + n.
+      //  b. If k < 0, let k be 0.
+      var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0)
+
+      function sameValueZero(x, y) {
+        return (
+          x === y ||
+          (typeof x === 'number' &&
+            typeof y === 'number' &&
+            isNaN(x) &&
+            isNaN(y))
+        )
+      }
+
+      // 7. Repeat, while k < len
+      while (k < len) {
+        // a. Let elementK be the result of ? Get(O, ! ToString(k)).
+        // b. If SameValueZero(valueToFind, elementK) is true, return true.
+        if (sameValueZero(o[k], valueToFind)) {
+          return true
+        }
+        // c. Increase k by 1.
+        k++
+      }
+
+      // 8. Return false
+      return false
+    },
+  })
+}

由于 JavaScript 允许更改 prototype ,所以 Polyfill 的原理就是先检查浏览器是否支持某个方法,当浏览器不支持的时候,会借助已经被广泛支持的方法来实现相同的功能,达到在旧浏览器上也可以使用新方法的目的。

下面是一个简单的 includes 方法实现,也借用浏览器支持的 indexOf 方法,让不支持 includes 的浏览器也可以使用 includes

js
// 借助 indexOf 来实现一个简单的 includes
+if (!Array.prototype.includes) {
+  Array.prototype.includes = function (v) {
+    return this.indexOf(v) > -1
+  }
+}

WARNING

请注意,上面这个实现方案很粗糙,没有 Polyfill 的方案考虑的足够周到,只是在这里做一个简单的实现演示。

Polyfill 会考虑到多种异常情况,最大幅度保证浏览器的兼容支持,当然一些复杂的方法实现起来会比较臃肿,全靠人工维护 Polyfill 很不现实。

而且实际的项目里,要用到的 JavaScript 原生方法非常多,不可能手动去维护每一个方法的兼容性,所以这部分工作,通常会让构建工具来自动化完成,常见的方案就有 Babel

除了 “语言转换 / 编译” 这个好处之外,在实际的开发中,构建工具可以更好地提高开发效率、提供自动化的代码检查、规避上线后的生产风险,例如:

  • 项目好多代码可以复用,可以直接抽离成 模块组件 ,交给构建工具去合并打包
  • TypeScript 的类型系统和代码检查真好用,也可以放心写,交给构建工具去编译
  • CSS 写起来很慢,可以使用 Sass 、 Less 等 CSS 预处理器 ,利用它们的变量支持、混合继承等功能提高开发效率,最终交给构建工具去编译回 CSS 代码
  • 海量的 npm 包 开箱即用,剩下的工作交给构建工具去按需抽离与合并
  • 项目上线前代码要混淆,人工处理太费劲,交给构建工具自动化处理
  • 还有很多列举不完的其他场景…

下面基于接下来要学习的 Vue3 技术栈,介绍两个流行且强相关的构建工具: WebpackVite

Webpack

Webpack 是一个老牌的构建工具,前些年可以说几乎所有的项目都是基于 Webpack 构建的,生态最庞大,各种各样的插件最全面,对旧版本的浏览器支持程度也最全面。

点击访问:Webpack 官网

在升级与配置一章里的 使用 @vue/cli 创建项目 会指导如何使用 Vue CLI 创建一个基于 Webpack 的 Vue 项目。

Vite

Vite 的作者也是熟悉的 Vue 作者尤雨溪,它是一个基于 ESM 实现的构建工具,主打更轻、更快的开发体验,主要面向现代浏览器,于 2021 年推出 2.x 版本之后,进入了一个飞速发展的时代,目前市场上的 npm 包基本都对 Vite 做了支持,用来做业务已经没有问题了。

毫秒级的开发服务启动和热重载,对 TypeScript 、 CSS 预处理器等常用开发工具都提供了开箱即用的支持,也兼容海量的 npm 包,如果是先用 Webpack 再用的 Vite ,会很快就喜欢上它!

点击访问:Vite 官网

在升级与配置一章里的 使用 Vite 创建项目 会指导如何使用流行脚手架创建一个基于 Vite 的 Vue 项目。

两者的区别

在开发流程上, Webpack 会先打包,再启动开发服务器,访问开发服务器时,会把打包好的结果直接给过去,下面是 Webpack 使用的 bundler 机制的工作流程。

`,58),m=o("p",null,"Vite 是基于浏览器原生的 ES Module ,所以不需要预先打包,而是直接启动开发服务器,请求到对应的模块的时候再进行编译,下面是 Vite 使用的 ESM 机制的工作流程。",-1),f=p('

所以当项目体积越大的时候,在开发启动速度上, Vite 和 Webpack 的差距会越来越大。

可以点击 Vite 官网的这篇文章: 为什么选 Vite 了解更多的技术细节。

构建方面,为了更好的加载体验,以及 Tree Shaking 按需打包 、懒加载和 Chunk 分割利于缓存,两者都需要进行打包;但由于 Vite 是面向现代浏览器,所以如果项目有兼容低版本浏览器的需求的话,建议还是用 Webpack 来打包,否则, Vite 是目前的更优解。

开发环境和生产环境

在使用构建工具的时候,需要了解一下 “环境” 的概念,对构建工具而言,会有 “开发环境( development )” 和 “生产环境( production )” 之分。

TIP

需要注意的是,这和业务上的 “测试 -> 预发 -> 生产” 那几个环境的概念是不一样的,业务上线流程的这几个环境,对于项目来说,都属于 “生产环境” ,因为需要打包部署。

开发环境

前面在编写 Hello TypeScript 这个 demo 的时候,使用了 npm run dev:ts 这样的命令来测试 TypeScript 代码的可运行性,可以把这个阶段认为是一个 “测试环境” ,这个时候代码不管怎么写,它都是 TypeScript 代码,不是最终要编译出来的 JavaScript 。

如果基于 Webpack 或者 Vite 这样的构建工具,测试环境提供了更多的功能,例如:

  • 可以使用 TypeScript 、 CSS 预处理器之类的需要编译的语言提高开发效率
  • 提供了热重载( Hot Module Replacement , 简称 HMR ),当修改了代码之后,无需重新运行或者刷新页面,构建工具会检测的修改自动帮更新
  • 代码不会压缩,并有 Source Mapping 源码映射,方便 BUG 调试
  • 默认提供局域网服务,无需自己做本地部署
  • 更多 …

生产环境

Hello TypeScript demo 最后配置的一个 npm run build 命令,将 TypeScript 代码编译成了 JavaScript ,这个时候 dist 文件夹下的代码文件就处于 “生产环境” 了,因为之后不论源代码怎么修改,都不会直接影响到它们,直到再次执行 build 编译。

可以看出生产环境和开发环境最大的区别就是稳定!除非再次打包发布,否则不会影响到已部署的代码。

  • 代码会编译为浏览器最兼容的版本,一些不兼容的新语法会进行 Polyfill
  • 稳定,除非重新发布,否则不会影响到已部署的代码
  • 打包的时候代码会进行压缩混淆,缩小项目的体积,也降低源码被直接曝光的风险

环境判断

',15),E=p('

在 Vite ,还可以通过判断 import.meta.env.DEVtrue 时是开发环境,判断 import.meta.env.PRODtrue 时是生产环境(这两个值永远相反)。

有关环境变量的问题可以查阅以下文档:

工具文档
Webpack模式
Vite环境变量和模式
',3),x=JSON.parse('{"title":"了解前端工程化","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"engineering.md","filePath":"engineering.md"}'),S={name:"engineering.md"},V=Object.assign(S,{setup(k){const c="process.env.NODE_ENV";return(v,q)=>{const l=t("ImgWrap"),a=t("ClientOnly"),r=t("GitalkComment");return y(),D("div",null,[i,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/job-details.png",dark:"/assets/img/job-details-dark.png",alt:"知名企业对 1-3 年经验的前端工程师招聘要求"})]),_:1}),C,s(a,null,{default:n(()=>[s(l,{src:"/logo.png",alt:"Vue.js Logo",maxWidth:240})]),_:1}),d,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/seo-page.jpg",dark:"/assets/img/seo-page-dark.jpg",alt:"网页呈现的内容"})]),_:1}),A,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/seo-page-code.jpg",dark:"/assets/img/seo-page-code-dark.jpg",alt:"网页内容对应的 HTML 源码"})]),_:1}),h,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/seo-spa-page-code.jpg",dark:"/assets/img/seo-spa-page-code-dark.jpg",alt:"单页面应用的网页内容只有一个空的 HTML 结构"})]),_:1}),u,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/screenshot-vscode.jpg",dark:"/assets/img/screenshot-vscode-dark.jpg",alt:"Visual Studio Code 界面截图"})]),_:1}),s(a,null,{default:n(()=>[s(l,{src:"/assets/img/screenshot-postman.jpg",dark:"/assets/img/screenshot-postman-dark.jpg",alt:"Postman 界面截图"})]),_:1}),g,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/screenshot-pkg.jpg",dark:"/assets/img/screenshot-pkg-dark.jpg",alt:"使用 Pkg 构建后的程序运行截图"})]),_:1}),b,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/bundler.png",alt:"Webpack 的工作原理(摘自 Vite 官网)"})]),_:1}),m,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/esm.png",alt:"Vite 的工作原理(摘自 Vite 官网)"})]),_:1}),f,o("p",null,[e("在 Webpack ,可以使用 "),o("code",null,F(c)),e(" 来区分开发环境( development )还是生产环境( production ),它会返回当前所处环境的名称。")]),E,s(a,null,{default:n(()=>[s(r,{issueId:194})]),_:1})])}}});export{x as __pageData,V as default}; diff --git a/assets/engineering.md.43ce842b.lean.js b/assets/engineering.md.43ce842b.lean.js new file mode 100644 index 00000000..288e259c --- /dev/null +++ b/assets/engineering.md.43ce842b.lean.js @@ -0,0 +1,291 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{v as y,b as D,t as s,O as n,F as o,L as e,X as F,R as p,M as t}from"./chunks/framework.0d8bea05.js";const i=p(`

了解前端工程化

现在前端的工作与以前的前端开发已经完全不同了。

刚接触前端的时候,做一个页面,是先创建 HTML 页面文件写页面结构,再在里面写 CSS 代码美化页面,再根据需要写一些 JavaScript 代码增加交互功能,需要几个页面就创建几个页面,相信大家的前端起步都是从这个模式开始的。

而实际上的前端开发工作,早已进入了前端工程化开发的时代,已经充满了各种现代化框架、预处理器、代码编译…

最终的产物也不再单纯是多个 HTML 页面,经常能看到 SPA / SSR / SSG 等词汇的身影。

TIP

现代化的开发概念 一节会介绍这些词汇的含义。

传统开发的弊端

在了解什么是前端工程化之前,先回顾一下传统开发存在的一些弊端,这样更能知道为什么需要它。

在传统的前端开发模式下,前端工程师大部分只需要单纯地写写页面,都是在 HTML 文件里直接编写代码,所需要的 JavaScript 代码是通过 script 标签以内联或者文件引用的形式放到 HTML 代码里的,当然 CSS 代码也是一样的处理方式。

例如这样:

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+  </head>
+  <body>
+    <!-- 引入 JS 文件 -->
+    <script src="./js/lib-1.js"></script>
+    <script src="./js/lib-2.js"></script>
+    <!-- 引入 JS 文件 -->
+  </body>
+</html>

如演示代码,虽然可以把代码分成多个文件来维护,这样可以有效降低代码维护成本,但在实际开发过程中,还是会存在代码运行时的一些问题。

一个常见的案例

继续用上面的演示代码,来看一个最简单的一个例子。

先在 lib-1.js 文件里,声明一个变量:

js
var foo = 1

再在 lib-2.js 文件里,也声明一个变量(没错,也是 foo ):

js
var foo = 2

然后在 HTML 代码里追加一个 script ,打印这个值:

html
<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Document</title>
+</head>
+<body>
+
+  <!-- 引入 JS 文件 -->
+  <script src="./js/lib-1.js"></script>
+  <script src="./js/lib-2.js"></script>
+  <!-- 引入 JS 文件 -->
+
+  <!-- 假设这里是实际的业务代码 -->
+  <script>
+    console.log(foo)
+  </script>
+  <!-- 假设这里是实际的业务代码 -->
+
+</body>
+</html>

先猜猜会输出什么? —— 答案是 2

如果在开发的过程中,不知道在 lib-2.js 文件里也声明了一个 foo 变量,一旦在后面的代码里预期了 foo + 2 === 3 ,那么这样就得不到想要的结果(因为 lib-1.js 里的 foo11 + 2 等于 3 ) 。

原因是 JavaScript 的加载顺序是从上到下,当使用 var 声明变量时,如果命名有重复,那么后加载的变量会覆盖掉先加载的变量。

这是使用 var 声明的情况,它允许使用相同的名称来重复声明,那么换成 let 或者 const 呢?

虽然不会出现重复声明的情况,但同样会收到一段报错:

bash
Uncaught SyntaxError: Identifier 'foo' has already been declared (at lib-2.js:1:1)

这次程序直接崩溃了,因为 letconst 无法重复声明,从而抛出这个错误,程序依然无法正确运行。

更多问题

以上只是一个最简单的案例,就暴露出了传统开发很大的弊端,然而并不止于此,实际上,存在诸如以下这些问题:

  1. 如本案例,可能存在同名的变量声明,引起变量冲突
  2. 引入多个资源文件时,比如有多个 JS 文件,在其中一个 JS 文件里面使用了在别处声明的变量,无法快速找到是在哪里声明的,大型项目难以维护
  3. 类似第 1 、 2 点提到的问题无法轻松预先感知,很依赖开发人员人工定位原因
  4. 大部分代码缺乏分割,比如一个工具函数库,很多时候需要整包引入到 HTML 里,文件很大,然而实际上只需要用到其中一两个方法
  5. 由第 4 点大文件延伸出的问题, script 的加载从上到下,容易阻塞页面渲染
  6. 不同页面的资源引用都需要手动管理,容易造成依赖混乱,难以维护
  7. 如果要压缩 CSS 、混淆 JS 代码,也是要人力操作使用工具去一个个处理后替换,容易出错

当然,实际上还会有更多的问题会遇到。

工程化带来的优势

为了解决传统开发的弊端,前端也开始引入工程化开发的概念,借助工具来解决人工层面的烦琐事情。

开发层面的优势

传统开发的弊端 里,主要列举的是开发层面的问题,工程化首要解决的当然也是在开发层面遇到的问题。

在开发层面,前端工程化有以下这些好处:

  1. 引入了模块化和包的概念,作用域隔离,解决了代码冲突的问题
  2. 按需导出和导入机制,让编码过程更容易定位问题
  3. 自动化的代码检测流程,有问题的代码在开发过程中就可以被发现
  4. 编译打包机制可以让使用开发效率更高的编码方式,比如 Vue 组件、 CSS 的各种预处理器
  5. 引入了代码兼容处理的方案( e.g. Babel ),可以让开发者自由使用更先进的 JavaScript 语句,而无需顾忌浏览器兼容性,因为最终会转换为浏览器兼容的实现版本
  6. 引入了 Tree Shaking 机制,清理没有用到的代码,减少项目构建后的体积

还有非常多的体验提升,列举不完。而对应的工具,根据用途也会有非常多的选择,在后面的学习过程中,会一步一步体验到工程化带来的好处。

团队协作的优势

除了对开发者有更好的开发体验和效率提升,对于团队协作,前端工程化也带来了更多的便利,例如下面这些场景:

统一的项目结构

以前的项目结构比较看写代码的人的喜好,虽然一般在研发部门里都有 “团队规范” 这种东西,但靠自觉性去配合的事情,还是比较难做到统一,特别是项目很赶的时候。

工程化后的项目结构非常清晰和统一,以 Vue 项目来说,通过脚手架创建一个新项目之后,它除了提供能直接运行 Hello World 的基础代码之外,还具备了如下的统一目录结构:

  • src 是源码目录
  • src/main.ts 是入口文件
  • src/views 是路由组件目录
  • src/components 是子组件目录
  • src/router 是路由目录

虽然也可以自行调整成别的结构,但根据笔者在多年的工作实际接触下来,以及从很多开源项目的代码里看到的,都是沿用脚手架创建的项目结构(不同脚手架创建的结构会有所不同,但基于同一技术栈的项目基本上都具备相同的结构)。

TIP

脚手架的升级与配置 一章可以学习如何使用脚手架创建 Vue 3 项目。

统一的代码风格

不管是接手其他人的代码或者是修改自己不同时期的代码,可能都会遇到这样的情况,例如一个模板语句,上面包含了很多属性,有的人喜欢写成一行,属性多了维护起来很麻烦,需要花费较多时间辨认:

vue
<template>
+  <div class="list">
+    <!-- 这个循环模板有很多属性 -->
+    <div class="item" :class="{ \`top-\${index + 1}\`: index < 3 }" v-for="(item, index)
+    in list" :key="item.id" @click="handleClick(item.id)">
+      <span>{{ item.text }}</span>
+    </div>
+    <!-- 这个循环模板有很多属性 -->
+  </div>
+</template>

而工程化配合统一的代码格式化规范,可以让不同人维护的代码,最终提交到 Git 上的时候,风格都保持一致,并且类似这种很多属性的地方,都会自动帮格式化为一个属性一行,维护起来就很方便:

vue
<template>
+  <div class="list">
+    <!-- 这个循环模板有很多属性 -->
+    <div
+      class="item"
+      :class="{ \`top-\${index + 1}\`: index < 3 }"
+      v-for="(item, index) in list"
+      :key="item.id"
+      @click="handleClick(item.id)"
+    >
+      <span>{{ item.text }}</span>
+    </div>
+    <!-- 这个循环模板有很多属性 -->
+  </div>
+</template>

同样的,写 JavaScript 时也会有诸如字符串用双引号还是单引号,缩进是 Tab 还是空格,如果用空格到底是要 4 个空格还是 2 个空格等一堆 “没有什么实际意义” 、但是不统一的话协作起来又很难受的问题……

在工程化项目这些问题都可以交给程序去处理,在书写代码的时候,开发者可以先按照自己的习惯书写,然后再执行命令进行格式化,或者是在提交代码的时候配合 Git Hooks 自动格式化,都可以做到统一风格。

TIP

添加协作规范 一节可以学习如何给项目添加统一的协作规范。

可复用的模块和组件

传统项目比较容易被复用的只有 JavaScript 代码和 CSS 代码,会抽离公共函数文件上传到 CDN ,然后在 HTML 页面里引入这些远程资源, HTML 代码部分通常只有由 JS 创建的比较小段的 DOM 结构。

并且通过 CDN 引入的资源,很多时候都是完整引入,可能有时候只需要用到里面的一两个功能,却要把很大的完整文件都引用进来。

这种情况下,在前端工程化里,就可以抽离成一个开箱即用的 npm 组件包,并且很多包都提供了模块化导出,配合构建工具的 Tree Shaking ,可以抽离用到的代码,没有用到的其他功能都会被抛弃,不会一起发布到生产环境。

TIP

依赖包和插件 一节可以学习如何查找和使用开箱即用的 npm 包。

代码健壮性有保障

传统的开发模式里,只能够写 JavaScript ,而在工程项目里,可以在开发环境编写带有类型系统的 TypeScript ,然后再编译为浏览器能认识的 JavaScript 。

在开发过程中,编译器会检查代码是否有问题,比如在 TypeScript 里声明了一个布尔值的变量,然后不小心将它赋值为数值:

ts
// 声明一个布尔值变量
+let bool: boolean = true
+
+// 在 TypeScript ,不允许随意改变类型,这里会报错
+bool = 3

编译器检测到这个行为的时候就会抛出错误:

bash
# ...
+return new TSError(diagnosticText, diagnosticCodes);
+           ^
+TSError:  Unable to compile TypeScript:
+src/index.ts:2:1 - error TS2322: Type 'number' is not assignable to type 'boolean'.
+
+2 bool = 3
+  ~~~~
+# ...

从而得以及时发现问题并修复,减少线上事故的发生。

团队开发效率高

在前后端合作环节,可以提前 Mock 接口与后端工程师同步开发,如果遇到跨域等安全限制,也可以进行本地代理,不受跨域困扰。

前端工程在开发过程中,还有很多可以交给程序处理的环节,像前面提到的代码格式化、代码检查,还有在部署上线的时候也可以配合 CI/CD 完成自动化流水线,不像以前改个字都要找服务端工程师去更新,可以把非常多的人力操作剥离出来交给程序。

求职竞争上的优势

近几年前端开发领域的相关岗位,都会在招聘详情里出现类似的描述:

熟悉 Vue / React 等主流框架,对前端组件化和模块化有深入的理解和实践
熟悉面向组件的开发模式,熟悉 Webpack / Vite 等构建工具
熟练掌握微信小程序开发,熟悉 Taro 框架或 uni-app 框架优先
熟悉 Scss / Less / Stylus 等预处理器的使用
熟练掌握 TypeScript 者优先
有良好的代码风格,结构设计与程序架构者优先
了解或熟悉后端开发者优先(如 Java / Go / Node.js )

知名企业对 1-3 年工作经验的初中级工程师,更是明确要求具备前端工程化开发的能力:

`,73),C=p('

组件化开发、模块化开发、 Webpack / Vite 构建工具、 Node.js 开发… 这些技能都属于前端工程化开发的知识范畴,不仅在面试的时候会提问,入职后新人接触的项目通常也是直接指派前端工程化项目,如果能够提前掌握相关的知识点,对求职也是非常有帮助的!

Vue.js 与工程化

在上一节提到了前端工程化 在求职竞争上的优势 ,里面列出的招聘要求例子都提及到了 Vue 和 React 这些主流的前端框架,前端框架是前端工程化开发里面不可或缺的成员。

框架能够充分的利用前端工程化相关的领先技术,不仅在开发层面降低开发者的上手难度、提升项目开发效率,在构建出来的项目成果上也有着远比传统开发更优秀的用户体验。

本书结合 Vue.js 框架 3.0 系列的全新版本,将从项目开发的角度,在帮助开发者入门前端工程化的同时,更快速的掌握一个流行框架的学习和使用。

了解 Vue.js 与全新的 3.0 版本

Vue.js(发音为 /vjuː/ ,类似 view )是一个易学易用,性能出色,适用场景丰富的 Web 前端框架,从 2015 年发布 1.0 版本以来,受到了全世界范围的前端开发者喜爱,已成为当下最受欢迎的前端框架之一。

',7),d=p(`

Vue 一直紧跟广大开发者的需求迭代发展,保持着它活跃的生命力。

2020 年 9 月 18 日, Vue.js 发布了 3.0 正式版,在大量开发者长达约一年半的使用和功能改进反馈之后, Vue 又于 2022 年 2 月 7 日发布了 3.2 版本,同一天, Vue 3 成为 Vue.js 框架全新的默认版本 (在此之前,通过 npm install vue 的默认版本还是 Vue 2 )。

也就是在未来的日子里, Vue 3 将随着时间的推移,逐步成为 Vue 生态的主流版本,是时候学习 Vue 3 了!

如果还没有体验过 Vue ,可以把以下代码复制到的代码编辑器,保存成一个 HTML 文件(例如: hello.html ),并在浏览器里打开访问,同时请唤起浏览器的控制台面板(例如 Chrome 浏览器是按 F12 或者鼠标右键点 “检查” ),在 Console 面板查看 Log 的打印。

html
<!-- 这是使用 Vue 实现的 demo -->
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Hello Vue</title>
+    <script src="https://unpkg.com/vue@3"></script>
+  </head>
+  <body>
+    <div id="app">
+      <!-- 通过 \`{{ 变量名 }}\` 语法渲染响应式变量 -->
+      <p>Hello {{ name }}!</p>
+
+      <!-- 通过 \`v-model\` 双向绑定响应式变量 -->
+      <!-- 通过 \`@input\` 给输入框绑定输入事件 -->
+      <input
+        type="text"
+        v-model="name"
+        placeholder="输入名称打招呼"
+        @input="printLog"
+      />
+
+      <!-- 通过 \`@click\` 给按钮绑定点击事件 -->
+      <button @click="reset">重置</button>
+    </div>
+
+    <script>
+      const { createApp, ref } = Vue
+      createApp({
+        // \`setup\` 是一个生命周期钩子
+        setup() {
+          // 默认值
+          const DEFAULT_NAME = 'World'
+
+          // 用于双向绑定的响应式变量
+          const name = ref(DEFAULT_NAME)
+
+          // 打印响应式变量的值到控制台
+          function printLog() {
+            // \`ref\` 变量需要通过 \`.value\` 操作值
+            console.log(name.value)
+          }
+
+          // 重置响应式变量为默认值
+          function reset() {
+            name.value = DEFAULT_NAME
+            printLog()
+          }
+
+          // 需要 \`return\` 出去才可以被模板使用
+          return { name, printLog, reset }
+        },
+      }).mount('#app')
+    </script>
+  </body>
+</html>

这是一个基于 Vue 3 组合式 API 语法的 demo ,它包含了两个主要功能:

  1. 可以在输入框修改输入内容,上方的 Hello World! 以及浏览器控制台的 Log 输出,都会随着输入框内容的变更而变化
  2. 可以点击 “重置” 按钮,响应式变量被重新赋值的时候,输入框的内容也会一起变化为新的值

这是 Vue 的特色之一:数据的双向绑定。

对比普通的 HTML 文件需要通过输入框的 oninput 事件手动编写视图的更新逻辑, Vue 的双向绑定功能大幅度减少了开发过程的编码量。

在未接触 Vue 这种编程方式之前,相信大部分人首先想到的是直接操作 DOM 来实现需求,为了更好的进行对比,接下来用原生 JavaScript 实现一次相同的功能:

html
<!-- 这是使用原生 JavaScript 实现的 demo -->
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Hello World</title>
+  </head>
+  <body>
+    <div id="app">
+      <!-- 通过一个 \`span\` 标签来指定要渲染数据的位置 -->
+      <p>Hello <span id="name"></span>!</p>
+
+      <!-- 通过 \`oninput\` 给输入框绑定输入事件 -->
+      <input
+        id="input"
+        type="text"
+        placeholder="输入名称打招呼"
+        oninput="handleInput()"
+      />
+
+      <!-- 通过 \`onclick\` 给按钮绑定点击事件 -->
+      <button onclick="reset()">重置</button>
+    </div>
+
+    <script>
+      // 默认值
+      const DEFAULT_NAME = 'World'
+
+      // 要操作的 DOM 元素
+      const nameElement = document.querySelector('#name')
+      const inputElement = document.querySelector('#input')
+
+      // 处理输入
+      function handleInput() {
+        const name = inputElement.value
+        nameElement.innerText = name
+        printLog()
+      }
+
+      // 打印输入框的值到控制台
+      function printLog() {
+        const name = inputElement.value
+        console.log(name)
+      }
+
+      // 重置 DOM 元素的文本和输入框的值
+      function reset() {
+        nameElement.innerText = DEFAULT_NAME
+        inputElement.value = DEFAULT_NAME
+        printLog()
+      }
+
+      // 执行一次初始化,赋予 DOM 元素默认文本和输入框的默认值
+      window.addEventListener('load', reset)
+    </script>
+  </body>
+</html>

虽然两个方案总的代码量相差不大,但可以看到两者的明显区别:

  1. Vue 只需要对一个 name 变量的进行赋值操作,就可以轻松实现视图的同步更新
  2. 使用原生 JavaScript 则需要频繁的操作 DOM 才能达到输入内容即时体现在文本 DOM 上面,并且还要考虑 DOM 是否已渲染完毕,否则操作会出错

Vue 的这种编程方式,称之为 “数据驱动” 编程。

如果在一个页面上频繁且大量的操作真实 DOM ,频繁的触发浏览器回流( Reflow )与重绘( Repaint ),会带来很大的性能开销,从而造成页面卡顿,在大型项目的性能上很是致命。

而 Vue 则是通过操作虚拟 DOM ( Virtual DOM ,简称 VDOM ),每一次数据更新都通过 Diff 算法找出需要更新的节点,只更新对应的虚拟 DOM ,再去映射到真实 DOM 上面渲染,以此避免频繁或大量的操作真实 DOM 。

TIP

虚拟 DOM 是一种编程概念,是指将原本应该是真实 DOM 元素的 UI 界面,用数据结构来组织起完整的 DOM 结构,再同步给真实 DOM 渲染,减少浏览器的回流与重绘。

在 JavaScript 里,虚拟 DOM 的表现是一个 Object 对象,其中需要包含指定的属性(例如 Vue 的虚拟 DOM 需要用 type 来指定当前标签是一个 <div /> 还是 <span /> ),然后框架会根据对象的属性去转换为 DOM 结构并最终完成内容的显示。

更多关于 Vue 虚拟 DOM 和性能优化可以查看官网的 渲染机制 一章进行了解。

Vue 3.0 版本还引入了组合式 API 的概念,更符合软件工程 “高内聚,低耦合” 的思想,让开发者可以更灵活的管理自己的逻辑代码,更方便的进行抽离封装再复用,不管是大型项目还是流水线业务,开箱即用的逻辑代码都是提升开发效率的利器。

Vue 与工程化之间的关联

在已经对 Vue 做了初步了解之后,可能有读者会问:“既然 Vue 的使用方式也非常简单,可以像 jQuery 这些经典类库一样在 HTML 引入使用,那么 Vue 和工程化有什么关联呢?”

Vue.js 是一个框架,框架除了简化编码过程中的复杂度之外,面对不同的业务需求还提供了通用的解决方案,而这些解决方案,通常是将前端工程化里的很多种技术栈组合起来串成一条条技术链,一环扣一环,串起来就是一个完整的工程化项目。

举一个常见的例子,比如上一节内容 了解 Vue.js 与全新的 3.0 版本 里的 demo 是一个简单的 HTML 页面,如果业务稍微复杂一点,比如区分了 “首页” 、 “列表页” 、 “内容页” 这样涉及到多个页面,传统的开发方案是通过 A 标签跳转到另外一个页面,在跳转期间会产生 “新页面需要重新加载资源、会有短暂白屏” 等情况,用户体验不太好。

Vue 提供了 Vue Router 实现路由功能,利用 History API 实现单页面模式(可在 现代化的开发概念 部分了解区别),在一个 HTML 页面里也可以体验 “页面跳转” 这样的体验,但如果页面很多,所有代码都堆积在一个 HTML 页面里,就很难维护。

借助前端工程化的构建工具,开发者可以编写 .vue 单组件文件,将多个页面的代码根据其功能模块进行划分,可拆分到多个单组件文件里维护并进行合理复用,最终通过构建工具编译再合并,最终生成浏览器能访问的 HTML / CSS / JS 文件,这样的开发过程,用户体验没有影响,但开发体验大大提升。

类似这样一个个业务场景会积少成多,把 Vue 和工程化结合起来,处理问题更高效更简单。

选择 Vue 入门工程化的理由

虽然前端的流行框架有主流的 Angular 、 React 和 Vue ,也有新兴的 Svelte 等等,每一个框架都有自己的特色,那为什么建议选择 Vue 来入门工程化呢?

最主要的两个原因是:

  1. 职场对 Vue 技术栈的需求量大,容易找工作
  2. 上手门槛低,会一些基础的 HTML / CSS / JavaScript 语法知识,就能够轻松上手 Vue 的组件开发

第一个原因在 求职竞争上的优势 已有进行过说明,掌握一门流行框架已经是前端岗位必备的技能,几乎所有公司在招聘前端工程师的时候都要求会 Vue 。

这里主要讲讲第二个原因,在 Vue 与工程化之间的关联 里提到了开发者可以编写 .vue 文件,这是一个 Vue 专属的文件扩展名,官方名称是 Single-File Component ,简称 SFC ,也就是单文件组件。

.vue 文件最大的特色就是支持像编写 .html 文件一样,在文件里写 HTML / CSS / JS 代码,不仅结构相似,在代码书写上,两者的语法也是十分接近:

.vue 文件.html 文件
<template /> 部分HTML 代码
<style /> 部分CSS 代码
<script /> 部分JavaScript 代码

下面就是一个最基础的 Vue 组件结构,可以看到和 HTML 文件是非常的相似:

vue
<!-- \`template\` 对应 HTML 代码 -->
+<template>
+  <div>
+    <!-- 一些 HTML  -->
+  </div>
+</template>
+
+<!-- \`script\` 部分对应 JavaScript 代码 -->
+<!-- 还支持其他语言,例如 \`lang="ts"\` 代表当前使用 TypeScript 编写 -->
+<script>
+export default {
+  // 这里是变量、函数等逻辑代码
+}
+</script>
+
+<!-- \`style\` 部分对应 CSS 代码 -->
+<!-- 还支持开启 \`scoped\` 标识,让 CSS 代码仅对当前组件生效,不会全局污染 -->
+<style scoped>
+/* 一些 CSS 代码 */
+</style>

Vue 组件不仅支持这些语言的所有基础用法,还增加了非常多更高效的功能,在后面 Vue3 教程的 单组件的编写 一章会有详细的介绍。

现代化的开发概念

在本章最开始的时候提到了 SPA / SSR / SSG 等词汇,这些词汇是一些现代前端工程化开发的概念名词缩写,代表着不同的开发模式和用户体验。

当下主流的前端框架都提供了这些开发模式的支持,因此在学习前端工程化和 Vue 开发的过程中,会不定期的看到这一类词汇,在实际工作业务的技术选型时,面对不同的业务场景也要考虑好需要使用什么样的开发模式,提前了解这些概念,对以后的工作也会很有帮助。

MPA 与 SPA

首先来看 MPA 与 SPA ,这代表着两个完全相反的开发模式和用户体验,它们的全称和中文含义如下:

名词全称中文
MPAMulti-Page Application多页面应用
SPASingle-Page Application单页面应用

多页面应用

MPA 多页面应用是最传统的网站体验,当一个网站有多个页面时,会对应有多个实际存在的 HTML 文件,访问每一个页面都需要经历一次完整的页面请求过程:

bash
# 传统的页面跳转过程
+
+从用户点击跳转开始:
+---> 浏览器打开新的页面
+---> 请求【所有】资源
+---> 加载 HTML 、CSS  JS  图片等资源
+---> 完成新页面的渲染
MPA 的优点

作为最传统也是最被广泛运用的模式,自然有它的优势存在:

  • 首屏加载速度快

因为 MPA 的页面源码都是实实在在的写在 HTML 文件里,所以当 HTML 文件被访问成功,内容也就随即呈现(在不考虑额外的 CSS 、 图片加载速度的情况下,这种模式的内容呈现速度是最快的)。

  • SEO 友好,容易被搜索引擎收录

如果读者有稍微了解过一些 SEO 知识,会知道除了网页的 TKD 三要素之外,网页的内容也影响收录的关键因素,传统的多页面应用,网页的内容都是直接位于 HTML 文件内,例如下面这个有很多内容的网页:

`,51),A=o("p",null,[e("右键查看该网页的源代码,可以看到网页内容对应的 HTML 结构也是包含在 "),o("code",null,".html"),e(" 文件里。")],-1),h=p(`

TIP

网页的 TKD 三要素是指一个网页的三个关键信息,含义如下:

T ,指 Title ,网站的标题,即网页的 <title>网站的标题</title> 标签。

K ,指 Keywords ,网站的关键词,即网页的 <meta name="Keywords" content="关键词1,关键词2,关键词3" /> 标签。

D ,指 Description ,网站的描述,即网页的 <meta name="description" content="网站的描述" /> 标签。

这三个要素标签都位于 HTML 文件的 <head /> 标签内。

  • 容易与服务端语言结合

由于传统的页面都是由服务端直出,所以可以使用 PHP 、 JSP 、 ASP 、 Python 等非前端语言或技术栈来编写页面模板,最终输出 HTML 页面到浏览器访问。

MPA 的缺点

说完 MPA 的优点,再来看看它的缺点,正因为有这些缺点的存在,才会催生出其他更优秀的开发模式出现。

  • 页面之间的跳转访问速度慢

正如它的访问流程,每一次页面访问都需要完整的经历一次渲染过程,哪怕从详情页 A 的 “相关阅读” 跳转到详情页 B ,这种网页结构一样,只有内容不同的两个页面,也需要经历这样的过程。

  • 用户体验不够友好

如果网页上的资源较多或者网速不好,这个过程就会有明显的卡顿或者布局错乱,影响用户体验。

  • 开发成本高

传统的多页面模式缺少前端工程化的很多优秀技术栈支持,前端开发者在刀耕火种的开发过程中效率低下。如果是基于 PHP 等非前端语言开发,工作量通常更是压在一名开发者身上,无法做到前后端分离来利用好跨岗位协作。

TIP

此处列举的多页面应用问题均指传统开发模式下的多页面,之所以特地说明,是因为后文还会有新的技术栈来实现多页面应用,但实现原理和体验并不一样。

单页面应用

正因为传统的多页面应用存在了很多无法解决的开发问题和用户体验问题,催生了现代化的 SPA 单页面应用技术的诞生。

SPA 单页面应用是现代化的网站体验,与 MPA 相反,不论站点内有多少个页面,在 SPA 项目实际上只有一个 HTML 文件,也就是 index.html 首页文件。

它只有第一次访问的时候才需要经历一次完整的页面请求过程,之后的每个内部跳转或者数据更新操作,都是通过 AJAX 技术来获取需要呈现的内容并只更新指定的网页位置。

TIP

AJAX 技术( Asynchronous JavaScript and XML )是指在不离开页面的情况下,通过 JavaScript 发出 HTTP 请求,让网页通过增量更新的方式呈现给用户界面,而不需要刷新整个页面来重新加载,是一种 “无刷体验” 。

SPA 在页面跳转的时候,地址栏也会发生变化,主要有以下两种方式:

  1. 通过修改 Location:hash 修改 URL 的 Hash 值(也就是 # 号后面部分),例如从 https://example.com/#/foo 变成 https://example.com/#/bar
  2. 通过 History API 的 pushState 方法更新 URL ,例如从 https://example.com/foo 变成 https://example.com/bar

这两个方式的共同特点是更新地址栏 URL 的时候,均不会刷新页面,只是单纯的变更地址栏的访问地址,而网页的内容则通过 AJAX 更新,配合起来就形成了一种网页的 “前进 / 后退” 等行为效果。

TIP

Vue Router 默认提供了这两种 URL 改变方式的支持,分别是 createWebHashHistory 的 Hash 模式和 createWebHistory 对应的 History 模式,在 路由的使用 一章可以学习更多 Vue 路由的使用。

理解了实现原理之后,可以把 SPA 的请求过程简化为如下步骤:

bash
# SPA 页面跳转过程
+
+从用户点击跳转开始:
+---> 浏览器通过 \`pushState\` 等方法更新 URL
+---> 请求接口数据(如果有涉及到前后端交互)
+---> 通过 JavaScript 处理数据,拼接 HTML 片段
+--->  HTML 片段渲染到指定位置,完成页面的 “刷新”
SPA 的优点

从上面的实现原理已经能总结出它的优势了:

  • 只有一次完全请求的等待时间(首屏加载)
  • 用户体验好,内部跳转的时候可以实现 “无刷切换”
  • 因为不需要重新请求整个页面,所以切换页面的时候速度更快
  • 因为没有脱离当前页面,所以 “页” 与 “页” 之间在切换过程中支持动画效果
  • 脱离了页面跳页面的框架,让整个网站形成一个 Web App ,更接近原生 App 的访问体验
  • 开发效率高,前后端分离,后端负责 API 接口,前端负责界面和联调,同步进行缩短工期

这也是为什么短短几年时间, SPA 的体验模式成为前端领域的主流。

SPA 的缺点

虽然 SPA 应用在使用过程中的用户体验非常好,但也有自身的缺点存在:

  • 首屏加载相对较慢

由于 SPA 应用的路由是由前端控制, SPA 在打开首页后,还要根据当前的路由再执行一次内容渲染,相对于 MPA 应用从服务端直出 HTML ,首屏渲染所花费的时间会更长。

  • 不利于 SEO 优化

由于 SPA 应用全程是由 JavaScript 控制内容的渲染,因此唯一的一个 HTML 页面 index.html 通常是一个空的页面,只有最基础的 HTML 结构,不仅无法设置每个路由页面的 TDK ,页面内容也无法呈现在 HTML 代码里,因此对搜索引擎来说,网站的内容再丰富,依然只是一个 “空壳” ,无法让搜索引擎进行内容爬取。

`,33),u=p('

为了减少用户等待过程中的焦虑感,可以通过增加 Loading 过程,或者 Skeleton 骨架屏等优化方案,但其实也是治标不治本,因此为了结合 SPA 和 MPA 的优点,又进一步催生出了更多实用的技术方案以适配更多的业务场景,在后面的小节将逐一介绍。

CSR 与 SSR

在了解了 MPA 与 SPA 之后,先了解另外两个有相关联的名词: CSR 与 SSR ,同样的,这一对也是代表着相反的开发模式和用户体验,它们的全称和中文含义如下:

名词全称中文
CSRClient-Side Rendering客户端渲染
SSRServer-Side Rendering服务端渲染

正如它们的名称,这两者代表的是渲染网页过程中使用到的技术栈。

客户端渲染

MPA 多页面应用与 SPA 单页面应用 部分的介绍过的 SPA 单页面应用,正是基于 CSR 客户端渲染实现的(因此大部分情况下, CSR 等同于 SPA ,包括实现原理和优势),这是一种利用 AJAX 技术,把渲染工作从服务端转移到客户端完成,不仅客户端的用户体验更好,前后端分离的开发模式更加高效。

但随之而来的首屏加载较慢、不利于 SEO 优化等缺点,而 SPA 的这几个缺点,却是传统 MPA 多页面应用所具备的优势,但同样的, MPA 也有着自己开发成本高、用户体验差等问题。

既然原来的技术方案无法完美满足项目需求,因此在结合 MPA 的优点和 SPA 的优点之后,一种新的技术随之诞生,这就是 SSR 服务端渲染。

服务端渲染

和传统的 MPA 使用 PHP / JSP 等技术栈做服务端渲染不同,现代前端工程化里的 SSR 通常是指使用 Node.js 作为服务端技术栈。

TIP

工程化神器 Node.js 一节会介绍 Node ,以及它对前端工程化带来的重大变化,现代前端工程化发展离不开它的存在。

传统的服务端渲染通常由后端开发者一起维护前后端代码,需要写后端语言支持的模板、 JavaScript 代码维护成本也比较高;而 SSR 服务端渲染则是交给前端开发者来维护,利用 Node 提供的能力进行同构渲染,由于本身前后端都使用 JavaScript 编写,维护成本也大大的降低。

SSR 技术利用的同构渲染方案( Isomorphic Rendering ),指的是一套代码不仅可以在客户端运行,也可以在服务端运行,在一些合适的时机先由服务端完成渲染( Server-Side Rendering )再直出给客户端激活( Client-Side Hydration ),这种开发模式带来了:

  • 更好的 SEO 支持,解决了 SPA 单页面应用的痛点
  • 更快的首屏加载速度,保持了 MPA 多页面应用的优点
  • 和 SPA 一样支持前后端分离,开发效率依然很高
  • 有更好的客户端体验,当用户完全打开页面后,本地访问过程中也可以保持 SPA 单页面应用的体验
  • 统一的心智模型,由于支持同构,因此没有额外的心智负担

那么,使用 Vue 开发项目时,应该如何实现 SSR 呢?

Vue 的 SSR 支持非常好, Vue 官方不仅提供了一个 Vue.js 服务器端渲染指南 介绍了基于 Vue 的 SSR 入门实践,还有基于 Vue 的 Nuxt.jsQuasar 框架帮助开发者更简单地落地 SSR 开发,构建工具 Vite 也有内置的 Vue SSR 支持。

Pre-Rendering 与 SSG

在介绍了 SSR 服务端渲染技术后,读者可能会想到一个问题,就是 SSR 的开发成本总归比较高,如果本身项目比较简单,例如一个静态博客,或者静态官网、落地页等内容不多,仅需要简单的 SEO 支持的项目需求,是否有更简便的方案呢?

以下两种方案正是用于满足这类需求的技术:

名词全称中文
Pre-RenderingPre-Rendering预渲染
SSGStatic-Site Generation静态站点生成

预渲染

预渲染也是一种可以让 SPA 单页面应用 解决 SEO 问题的技术手段。

预渲染的原理是在构建的时候启动无头浏览器( Headless Browser ),加载页面的路由并将访问结果按照路由的路径保存到静态 HTML 文件里,这样部署到服务端的页面,不再是一个空的 HTML 页面,而是有真实内容的存在,但由于只在构建时运行,因此用户每次访问的时候 HTML 里的内容不会产生变化,直到下一次构建。

TIP

无头浏览器( Headless Browser ),指没有 GUI 界面的浏览器,使用代码通过编程接口来控制浏览器的行为,常用于网络爬虫、自动化测试等场景,预渲染也使用它来完成页面的渲染,以获取渲染后的代码来填充 HTML 文件。

预渲染和 服务端渲染 最大的区别在于,预渲染只在构建的时候就完成了页面内容的输出(发生在用户请求前),因此构建后不论用户何时访问, HTML 文件里的内容都是构建的时候的那份内容,所以预渲染适合一些简单的、有一定的 SEO 要求但对内容更新频率没有太高要求、内容多为静态展示的页面。

例如企业用于宣传的官网页面、营销活动的推广落地页都非常适合使用预渲染技术,现代的构建工具都提供了预渲染的内置实现,例如这个教程: 用 Vite 更简单的解决 Vue3 项目的预渲染问题 ,就是通过 Vite 的内置功能来实现预渲染,最终也运用到了公司的业务上。

静态站点生成

SSG 静态站点生成是基于预渲染技术,通过开放简单的 API 和配置文件,就让开发者可以实现一个预渲染静态站点的技术方案。

它可以让开发者定制站点的个性化渲染方案,但更多情况下,通常是作为一些开箱即用的技术产品来简化开发过程中的繁琐步骤,这一类技术产品通常称之为静态站点生成器( Static-Site Generator ,也是简称 SSG )。

常见的 SSG 静态站点生成器有:基于 Vue 技术的 VuePressVitePress ,自带了 Vue 组件的支持,还有基于 React 的 Docusaurus ,以及很多各有特色的生成器,例如 JekyllHugo 等等。

如果有写技术文档或者博客等内容创作需求,使用静态站点生成器是一个非常方便的选择,通常这一类产品还有非常多的个性化主题可以使用。

ISR 与 DPR

在现代化的开发概念这一节,从 MPA 多页面应用到 SPA 单页面应用 ,再到 CSR 客户端渲染和 SSR 服务端渲染 ,以及 Pre-Rendering 预渲染与 SSG 静态站点生成 ,似乎已经把所有常见的开发场景覆盖完了。

那接下来要讲的 ISR 和 DPR 又是什么用途的技术方案呢?先看看它们的全称和中文含义:

名词全称中文
ISRIncremental Site Rendering增量式的网站渲染
DPRDistributed Persistent Rendering分布式的持续渲染

当网站的内容体量达到一定程度的时候,从头开始构建进行预渲染所花费的时间会非常久,而实际上并不是所有页面的内容都需要更新,这两项技术的推出是为了提升大型项目的渲染效率。

ISR 增量式的网站渲染,通过区分 “关键页面” 和 “非关键页面” 进行构建,优先预渲染 “关键页面” 以保证内容的最新和正确,同时缓存到 CDN ,而 “非关键页面” 则交给用户访问的时候再执行 CSR 客户端渲染,并触发异步的预渲染缓存到 CDN 。

这样做的好处是,大幅度的提升了每次构建的时间,但由于只保证部分 “关键页面” 的构建和内容正确,所以访问 “非关键页面” 的时候,有可能先看到旧的内容,再由 CSR 刷新为新的内容,会丢失一部分用户体验。

更多 ISR 技术细节可以阅读 Netlify 的开发者体验总监 Cassidy Williams 的一篇文章: Incremental Static Regeneration: Its Benefits and Its Flaws

DPR 分布式的持续渲染则是为了解决 ISR 方案下可能访问到旧内容的问题,这也是由 Cassidy Williams 发起的一个提案,详情可在 GitHub 查看:Distributed Persistent Rendering (DPR)

由于目前这两项技术还在发展初期,能够支持的框架和服务还比较少,在这里建议作为一种技术知识储备提前了解,在未来的某一天有业务需要的时候,也可以知道有这样的方案可以解决问题。

工程化不止于前端

现代化的开发概念 部分所讲述的都是关于网页开发的变化,当然,前端这个岗位本身就是从页面开发发展起来的,自然还是离不开网页这个老本行。

但随着前端工程化的发展,前端越来越不止于写前端,已经有很多前端工程师利用前端工程化带来的优势,不仅仅只是做一个 Web 前端,开始逐步发展为一个全栈工程师,在企业内部承担起了更多的岗位职责,包括作者笔者也是。

之所以能做这么多事情,得益于 Node.js 在前端开发带来的翻天覆地的变化,可以在保持原有的 JavaScript 和 TypeScript 基础上,几乎没有过多的学习成本就可以过度到其他端的开发。

在了解 Node.js 之前,先来看看现在的前端开发工程师除了写 Web 前端,还可以做到哪些岗位的工作。

服务端开发

在传统的认知里,如果一个前端工程师想自己搭建一个服务端项目,需要学习 Java 、 PHP 、 Go 等后端语言,还需要学习 Nginx 、 Apache 等 Web Server 程序的使用,并使用这些技术来开发并部署一个项目的服务端。

现在的前端工程师可以利用 Node.js ,单纯使用 JavaScript 或者 TypeScript 来开发一个基于 Node 的服务端项目。

Node 本身是一个 JavaScript 的运行时,还提供了 HTTP 模块 可以启动一个本地 HTTP 服务,如果把 Node 项目部署到服务器上,就可以运行一个可对外访问的公网服务。

但 Node 的原生服务端开发成本比较高,因此在 GitHub 开源社区也诞生了很多更方便的、开箱即用、功能全面的服务端框架,根据它们的特点,可以简单归类如下:

ExpressKoaFastify 为代表的轻量级服务端框架,这一类框架的特点是 “短平快” ,对于服务端需求不高,只是跑一些小项目的话,开箱即用非常地方便,比如 Build 了一个 Vue 项目,然后提供一个读取静态目录的服务来访问它。

但是 “短平快” 框架带来了一些团队协作上的弊端,如果缺少一些架构设计的能力,很容易把一个服务端搭的很乱以至于难以维护,比如项目的目录结构、代码的分层设计等等,每个创建项目的人都有自己的想法和个人喜好,就很难做到统一管理。

因此在这些框架的基础上,又诞生了以 Nest (底层基于 Express ,可切换为 Fastify )、 Egg (基于 Koa )为代表的基于 MVC 架构的企业级服务端框架,这一类框架的特点是基于底层服务进行了更进一步的架构设计并实现了代码分层,还自带了很多开箱即用的 Building Blocks ,例如 TypeORM 、WebSockets 、Swagger 等等,同样也是开箱即用,对大型项目的开发更加友好。

TIP

当然, Node.js 所做的事情是解决服务端程序部分的工作,如果涉及到数据存储的需求,学习 MySQL 和 Redis 的技术知识还是必不可少的!

App 开发

常规的 Native App 原生开发需要配备两条技术线的支持:使用 Java / Kotlin 语言开发 Android 版本,使用 Objective-C / Swift 语言开发 iOS 版本,这对于创业团队或者个人开发者来说都是一个比较高的开发成本。

前端开发者在项目组里对 App 的作用通常是做一些活动页面、工具页面内嵌到 App 的 WebView 里,如果是在一些产品比较少的团队里,例如只有一个 App 产品,那么前端的存在感会比较低。

而 Hybrid App 的出现,使得前端开发者也可以使用 JavaScript / TypeScript 来编写混合 App ,只需要了解简单的打包知识,就可以参与到一个 App 的开发工作中。

开发 Hybrid App 的过程通常称为混合开发,最大的特色就是一套代码可以运行到多个平台,这是因为整个 App 只有一个基座,里面的 App 页面都是使用 UI WebView 来渲染的 Web 界面,因此混合开发的开发成本相对于原生开发是非常低的,通常只需要一个人 / 一个小团队就可以输出双平台的 App ,并且整个 App 的开发周期也会更短。

在用户体验方面, Hybrid App 相对于 Native App ,一样可以做到:

  • 双平台的体验一致性
  • 支持热更新,无需用户重新下载整个 App
  • 内置的 WebView 在交互体验上也可以做到和系统交互,比如读取 / 存储照片、通讯录,获取定位等等
  • 支持 App Push 系统通知推送
  • 还有很多 Native App 具备的功能

基本上 Native App 的常见功能,在 Hybrid App 都能满足。

而且大部分情况下,在构建 Hybrid App 的时候还可以顺带输出一个 Web App 版本,也就是让这个 App 在被用户下载前,也有一模一样的网页版可以体验,这对于吸引新用户是非常有用的。

在混合开发的过程中,通常是由前端开发者来负责 App 项目从 “开发” 到 “打包” 再到 “发版” 的整个流程,在开发的过程中是使用常见的前端技术栈,例如目前主流的有基于 Vue 的 uni-app 、基于 React 的 React Native 等等,这些 Hybrid 框架都具备了 “学习成本低、开发成本低、一套代码编译多个平台” 的特点。

在 App 开发完毕后,使用 Hybrid 框架提供的 CLI 工具编译出 App 资源包,再根据框架提供的原生基座打包教程去完成 Android / iOS 的安装包构建,这个环节会涉及到原生开发的知识,例如 Android 包的构建会使用到 Android Studio ,但整个过程使用到原生开发的环节非常少,几乎没有太高的学习门槛。

桌面程序开发

放在以前要开发一个 Windows 桌面程序,需要用上 QT / WPF / WinForm 等技术栈,还要学习 C++ / C# 之类的语言,对于只想在业余写几个小工具的开发者来说,上手难度和学习成本都很高,但在前端工程化的时代里,使用 JavaScript 或 TypeScript 也可以满足程序开发的需要。

这得益于 Electron / Tauri 等技术栈的出现,其中 Electron 的成熟度最高、生态最完善、最被广泛使用,除了可以构建 Windows 平台支持的 .exe 文件之外,对 macOS 和 Linux 平台也提供了对应的文件构建支持。

广大前端开发者每天都在使用的 Visual Studio Code 以及知名的 HTTP 网络测试工具 Postman 都是使用 Electron 开发的。

',71),g=p('

笔者也通过 Electron 构建了多个给公司内部使用的界面化工具客户端,这一类技术栈对于前端开发者来说,真的非常方便!在这里以 Electron 为例,简单讲解下它的工作原理,以了解为什么程序开发可以如此简单。

Electron 的底层是基于 Chromium 和 Node.js ,它提供了两个进程供开发者使用:

  1. 主进程:它是整个应用的入口点,主进程运行在 Node 环境中,可以使用所有的 Node API ,程序也因此具备了和系统进行交互的能力,例如文件的读写操作。

  2. 渲染进程:负责与用户交互的 GUI 界面,基于 Chromium 运行,所以开发者得以使用 HTML / CSS / JavaScript 像编写网页一样来编写程序的 GUI 界面。

一个程序应用只会有一个主进程,而渲染进程则可以根据实际需求创建多个,渲染进程如果需要和系统交互,则必须与主进程通信,借助主进程的能力来实现。

在构建的时候, Electron 会把 Node 和 Chromium 一起打包为一个诸如 .exe 这样的安装文件(或者是包含了两者的免安装版本),这样用户不需要 Node 环境也可以运行桌面程序。

应用脚本开发

桌面程序开发 部分讲的是构建一种拥有可视化 GUI 界面的程序,但有时候并不需要复杂的 GUI ,可能只想提供一个双击运行的脚本类程序给用户,现在的前端工程化也支持使用 JavaScript 构建一个无界面的应用脚本。

假如某一天公司的运营小姐姐希望能做一个自动化的脚本减轻她们的机械操作,或者是自己工作过程中发现一些日常工作可以交付给脚本解决的情况,就可以使用这种方式来输出一个脚本程序,使用的时候双击运行非常方便。

笔者之前为了让团队的工程师减少写日报的心智负担,也是使用了这个方式编写了一个 git-commit-analytics 工具,部门里的工程师可以通过规范化 commit 来生成每天的工作日报,每天双击一下就可以生成一份报告,很受团队的喜欢。

',9),b=p(`

在这里推荐一个工具 Pkg ,它可以把 Node 项目打包为一个可执行文件,支持 Windows 、 macOS 、 Linux 等多个平台,它的打包机制和 Electron 打包的思路类似,也是通过把 Node 一起打包,让用户可以在不安装 Node 环境的情况下也可以直接运行脚本程序。

实践工程化的流程

基于 Vue 3 的项目,最主流的工程化组合拳有以下两种:

常用方案Runtime构建工具前端框架
方案一NodeWebpackVue
方案二NodeViteVue

方案一是比较传统并且过去项目使用最多的方案组合,但从 2021 年初随着 Vite 2.0 的发布,伴随着更快的开发体验和日渐丰富的社区生态,新项目很多都开始迁移到方案二,因此本书秉着面向当下与未来的原则,会侧重 Vite 的使用来开展讲解,包括一些 demo 的创建等等。

当技术成熟的时候,还可以选择更喜欢的方案自行组合,例如用 Deno 来代替 Node ,但前期还是按照主流的方案来进入工程化的学习。

下面的内容将根据 Vue 3 的工程化开发,逐一讲解涉及到常用的工具,了解它们的用途和用法。

工程化神器 Node.js

只要在近几年有接触过前端开发,哪怕没有实际使用过,也应该有听说过 Node.js ,那么它是一个什么样的存在?

什么是 Node.js

Node.js (简称 Node ) 是一个基于 Chrome V8 引擎构建的 JS 运行时( JavaScript Runtime )。

它让 JavaScript 代码不再局限于网页上,还可以跑在客户端、服务端等场景,极大的推动了前端开发的发展,现代的前端开发几乎都离不开 Node 。

什么是 Runtime

Runtime ,可以叫它 “运行时” 或者 “运行时环境” ,这个概念是指,项目的代码在哪里运行,哪里就是运行时。

传统的 JavaScript 只能跑在浏览器上,每个浏览器都为 JS 提供了一个运行时环境,可以简单地把浏览器当成一个 Runtime ,明白了这一点,相信就能明白什么是 Node 。

Node 就是一个让 JS 可以脱离浏览器运行的环境,当然,这里并不是说 Node 就是浏览器。

Node 和浏览器的区别

虽然 Node 也是基于 Chrome V8 引擎构建,但它并不是一个浏览器,它提供了一个完全不一样的运行时环境,没有 Window 、没有 Document 、没有 DOM 、没有 Web API ,没有 UI 界面…

但它提供了很多浏览器做不到的能力,比如和操作系统的交互,例如 “文件读写” 这样的操作在浏览器有诸多的限制,而在 Node 则轻轻松松。

对于前端开发者来说, Node 的巨大优势在于,使用一种语言就可以编写所有东西(前端和后端),不再花费很多精力去学习各种各样的开发语言。

哪怕仅仅只做 Web 开发,也不再需要顾虑新的语言特性在浏览器上的兼容性( e.g. ES6 、 ES7 、 ES8 、 ES9 …), Node 配合构建工具,以及诸如 Babel 这样的代码编译器,可以帮转换为浏览器兼容性最高的 ES5 。

当然还有很多工程化方面的好处,总之一句话,使用 Node 的开发体验会非常好。

工程化的入门准备 一章中,会对 Node 开发做进一步的讲解,下面先继续顺着 Node 的工具链,了解与日常开发息息相关的前端构建工具。

工程化的构建工具

在前端开发领域,构建工具已经成为现在必不可少的开发工具了,很多刚接触前端工程化的开发者可能会有疑惑,为什么以前的前端页面直接编写代码就可以在浏览器访问,现在却还要进行构建编译,是否 “多此一举” ?

要消除这些困惑,就需要了解一下为什么要使用构建工具,知道构建工具在开发上能够给带来什么好处。

为什么要使用构建工具

目前已经有很多流行的构建工具,例如: GruntGulpWebpackSnowpackParcelRollupVite … 每一个工具都有自己的特色。

如上面列举的构建工具,虽然具体到某一个工具的时候,是 “一个” 工具,但实际上可以理解为是 “一套” 工具链、工具集,构建工具通常集 “语言转换 / 编译” 、 “资源解析” 、 “代码分析” 、 “错误检查” 、 “任务队列” 等非常多的功能于一身。

构建工具可以帮解决很多问题,先看看最基础的一个功能支持: “语言转换 / 编译” 。

且不说构建工具让可以自由自在的在项目里使用 TypeScript 这些新兴的语言,单纯看历史悠久的 JavaScript ,从 2015 年开始,每年也都会有新的版本发布(例如 ES6 对应 ES2015 、 ES7 对应 ES2016 、 ES8 对应 ES2017 等等)。

虽然新版本的 JS API 更便捷更好用,但浏览器可能还没有完全支持,这种情况下可以通过构建工具去转换成兼容度更高的低版本 JS 代码。

举个很常用到的例子,现在判断一个数组是否包含某个值,通常会这么写:

js
// 声明一个数组
+const arr = ['foo', 'bar', 'baz']
+
+// 当数组包含 foo 这个值时,处理一些逻辑
+if (arr.includes('foo')) {
+  // do something…
+}

通过 Array.prototype.includes() 这个实例方法返回的布尔值,判断数组是否包含目标值,而这个方法是从 ES6 开始支持的,对于不支持 ES6 的古董浏览器,只能使用其他更早期的方法代替( e.g. indexOf ),或者手动引入它的 Polyfill 来保证这个方法可用。

TIP

Polyfill 是在浏览器不支持的情况下实现某个功能的代码,可以在概念发明者 Remy Sharp 的博文里了解到它的由来,是一个挺有意思的命名。

点击阅读: What is a Polyfill?

以下是摘选自 MDN 网站上关于 Array.prototype.includes() 的 Polyfill 实现:

js
// https://tc39.github.io/ecma262/#sec-array.prototype.includes
+if (!Array.prototype.includes) {
+  Object.defineProperty(Array.prototype, 'includes', {
+    value: function (valueToFind, fromIndex) {
+      if (this == null) {
+        throw new TypeError('"this" is null or not defined')
+      }
+
+      // 1. Let O be ? ToObject(this value).
+      var o = Object(this)
+
+      // 2. Let len be ? ToLength(? Get(O, "length")).
+      var len = o.length >>> 0
+
+      // 3. If len is 0, return false.
+      if (len === 0) {
+        return false
+      }
+
+      // 4. Let n be ? ToInteger(fromIndex).
+      //    (If fromIndex is undefined, this step produces the value 0.)
+      var n = fromIndex | 0
+
+      // 5. If n ≥ 0, then
+      //  a. Let k be n.
+      // 6. Else n < 0,
+      //  a. Let k be len + n.
+      //  b. If k < 0, let k be 0.
+      var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0)
+
+      function sameValueZero(x, y) {
+        return (
+          x === y ||
+          (typeof x === 'number' &&
+            typeof y === 'number' &&
+            isNaN(x) &&
+            isNaN(y))
+        )
+      }
+
+      // 7. Repeat, while k < len
+      while (k < len) {
+        // a. Let elementK be the result of ? Get(O, ! ToString(k)).
+        // b. If SameValueZero(valueToFind, elementK) is true, return true.
+        if (sameValueZero(o[k], valueToFind)) {
+          return true
+        }
+        // c. Increase k by 1.
+        k++
+      }
+
+      // 8. Return false
+      return false
+    },
+  })
+}

由于 JavaScript 允许更改 prototype ,所以 Polyfill 的原理就是先检查浏览器是否支持某个方法,当浏览器不支持的时候,会借助已经被广泛支持的方法来实现相同的功能,达到在旧浏览器上也可以使用新方法的目的。

下面是一个简单的 includes 方法实现,也借用浏览器支持的 indexOf 方法,让不支持 includes 的浏览器也可以使用 includes

js
// 借助 indexOf 来实现一个简单的 includes
+if (!Array.prototype.includes) {
+  Array.prototype.includes = function (v) {
+    return this.indexOf(v) > -1
+  }
+}

WARNING

请注意,上面这个实现方案很粗糙,没有 Polyfill 的方案考虑的足够周到,只是在这里做一个简单的实现演示。

Polyfill 会考虑到多种异常情况,最大幅度保证浏览器的兼容支持,当然一些复杂的方法实现起来会比较臃肿,全靠人工维护 Polyfill 很不现实。

而且实际的项目里,要用到的 JavaScript 原生方法非常多,不可能手动去维护每一个方法的兼容性,所以这部分工作,通常会让构建工具来自动化完成,常见的方案就有 Babel

除了 “语言转换 / 编译” 这个好处之外,在实际的开发中,构建工具可以更好地提高开发效率、提供自动化的代码检查、规避上线后的生产风险,例如:

  • 项目好多代码可以复用,可以直接抽离成 模块组件 ,交给构建工具去合并打包
  • TypeScript 的类型系统和代码检查真好用,也可以放心写,交给构建工具去编译
  • CSS 写起来很慢,可以使用 Sass 、 Less 等 CSS 预处理器 ,利用它们的变量支持、混合继承等功能提高开发效率,最终交给构建工具去编译回 CSS 代码
  • 海量的 npm 包 开箱即用,剩下的工作交给构建工具去按需抽离与合并
  • 项目上线前代码要混淆,人工处理太费劲,交给构建工具自动化处理
  • 还有很多列举不完的其他场景…

下面基于接下来要学习的 Vue3 技术栈,介绍两个流行且强相关的构建工具: WebpackVite

Webpack

Webpack 是一个老牌的构建工具,前些年可以说几乎所有的项目都是基于 Webpack 构建的,生态最庞大,各种各样的插件最全面,对旧版本的浏览器支持程度也最全面。

点击访问:Webpack 官网

在升级与配置一章里的 使用 @vue/cli 创建项目 会指导如何使用 Vue CLI 创建一个基于 Webpack 的 Vue 项目。

Vite

Vite 的作者也是熟悉的 Vue 作者尤雨溪,它是一个基于 ESM 实现的构建工具,主打更轻、更快的开发体验,主要面向现代浏览器,于 2021 年推出 2.x 版本之后,进入了一个飞速发展的时代,目前市场上的 npm 包基本都对 Vite 做了支持,用来做业务已经没有问题了。

毫秒级的开发服务启动和热重载,对 TypeScript 、 CSS 预处理器等常用开发工具都提供了开箱即用的支持,也兼容海量的 npm 包,如果是先用 Webpack 再用的 Vite ,会很快就喜欢上它!

点击访问:Vite 官网

在升级与配置一章里的 使用 Vite 创建项目 会指导如何使用流行脚手架创建一个基于 Vite 的 Vue 项目。

两者的区别

在开发流程上, Webpack 会先打包,再启动开发服务器,访问开发服务器时,会把打包好的结果直接给过去,下面是 Webpack 使用的 bundler 机制的工作流程。

`,58),m=o("p",null,"Vite 是基于浏览器原生的 ES Module ,所以不需要预先打包,而是直接启动开发服务器,请求到对应的模块的时候再进行编译,下面是 Vite 使用的 ESM 机制的工作流程。",-1),f=p('

所以当项目体积越大的时候,在开发启动速度上, Vite 和 Webpack 的差距会越来越大。

可以点击 Vite 官网的这篇文章: 为什么选 Vite 了解更多的技术细节。

构建方面,为了更好的加载体验,以及 Tree Shaking 按需打包 、懒加载和 Chunk 分割利于缓存,两者都需要进行打包;但由于 Vite 是面向现代浏览器,所以如果项目有兼容低版本浏览器的需求的话,建议还是用 Webpack 来打包,否则, Vite 是目前的更优解。

开发环境和生产环境

在使用构建工具的时候,需要了解一下 “环境” 的概念,对构建工具而言,会有 “开发环境( development )” 和 “生产环境( production )” 之分。

TIP

需要注意的是,这和业务上的 “测试 -> 预发 -> 生产” 那几个环境的概念是不一样的,业务上线流程的这几个环境,对于项目来说,都属于 “生产环境” ,因为需要打包部署。

开发环境

前面在编写 Hello TypeScript 这个 demo 的时候,使用了 npm run dev:ts 这样的命令来测试 TypeScript 代码的可运行性,可以把这个阶段认为是一个 “测试环境” ,这个时候代码不管怎么写,它都是 TypeScript 代码,不是最终要编译出来的 JavaScript 。

如果基于 Webpack 或者 Vite 这样的构建工具,测试环境提供了更多的功能,例如:

  • 可以使用 TypeScript 、 CSS 预处理器之类的需要编译的语言提高开发效率
  • 提供了热重载( Hot Module Replacement , 简称 HMR ),当修改了代码之后,无需重新运行或者刷新页面,构建工具会检测的修改自动帮更新
  • 代码不会压缩,并有 Source Mapping 源码映射,方便 BUG 调试
  • 默认提供局域网服务,无需自己做本地部署
  • 更多 …

生产环境

Hello TypeScript demo 最后配置的一个 npm run build 命令,将 TypeScript 代码编译成了 JavaScript ,这个时候 dist 文件夹下的代码文件就处于 “生产环境” 了,因为之后不论源代码怎么修改,都不会直接影响到它们,直到再次执行 build 编译。

可以看出生产环境和开发环境最大的区别就是稳定!除非再次打包发布,否则不会影响到已部署的代码。

  • 代码会编译为浏览器最兼容的版本,一些不兼容的新语法会进行 Polyfill
  • 稳定,除非重新发布,否则不会影响到已部署的代码
  • 打包的时候代码会进行压缩混淆,缩小项目的体积,也降低源码被直接曝光的风险

环境判断

',15),E=p('

在 Vite ,还可以通过判断 import.meta.env.DEVtrue 时是开发环境,判断 import.meta.env.PRODtrue 时是生产环境(这两个值永远相反)。

有关环境变量的问题可以查阅以下文档:

工具文档
Webpack模式
Vite环境变量和模式
',3),x=JSON.parse('{"title":"了解前端工程化","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"engineering.md","filePath":"engineering.md"}'),S={name:"engineering.md"},V=Object.assign(S,{setup(k){const c="process.env.NODE_ENV";return(v,q)=>{const l=t("ImgWrap"),a=t("ClientOnly"),r=t("GitalkComment");return y(),D("div",null,[i,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/job-details.png",dark:"/assets/img/job-details-dark.png",alt:"知名企业对 1-3 年经验的前端工程师招聘要求"})]),_:1}),C,s(a,null,{default:n(()=>[s(l,{src:"/logo.png",alt:"Vue.js Logo",maxWidth:240})]),_:1}),d,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/seo-page.jpg",dark:"/assets/img/seo-page-dark.jpg",alt:"网页呈现的内容"})]),_:1}),A,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/seo-page-code.jpg",dark:"/assets/img/seo-page-code-dark.jpg",alt:"网页内容对应的 HTML 源码"})]),_:1}),h,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/seo-spa-page-code.jpg",dark:"/assets/img/seo-spa-page-code-dark.jpg",alt:"单页面应用的网页内容只有一个空的 HTML 结构"})]),_:1}),u,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/screenshot-vscode.jpg",dark:"/assets/img/screenshot-vscode-dark.jpg",alt:"Visual Studio Code 界面截图"})]),_:1}),s(a,null,{default:n(()=>[s(l,{src:"/assets/img/screenshot-postman.jpg",dark:"/assets/img/screenshot-postman-dark.jpg",alt:"Postman 界面截图"})]),_:1}),g,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/screenshot-pkg.jpg",dark:"/assets/img/screenshot-pkg-dark.jpg",alt:"使用 Pkg 构建后的程序运行截图"})]),_:1}),b,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/bundler.png",alt:"Webpack 的工作原理(摘自 Vite 官网)"})]),_:1}),m,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/esm.png",alt:"Vite 的工作原理(摘自 Vite 官网)"})]),_:1}),f,o("p",null,[e("在 Webpack ,可以使用 "),o("code",null,F(c)),e(" 来区分开发环境( development )还是生产环境( production ),它会返回当前所处环境的名称。")]),E,s(a,null,{default:n(()=>[s(r,{issueId:194})]),_:1})])}}});export{x as __pageData,V as default}; diff --git a/assets/guide.md.c60fac0b.js b/assets/guide.md.c60fac0b.js new file mode 100644 index 00000000..d259ad8f --- /dev/null +++ b/assets/guide.md.c60fac0b.js @@ -0,0 +1,650 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as t,v as c,b as r,t as s,O as n,R as l,M as p}from"./chunks/framework.0d8bea05.js";const q=JSON.parse('{"title":"工程化的前期准备","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"guide.md","filePath":"guide.md"}'),y={name:"guide.md"},D=l(`

工程化的前期准备

对于刚刚迈入前端工程化、或者还没有接触过前端工程化的开发者,从传统的用 HTML + CSS + JS 手写页面的认知阶段走到工程化的世界,会面对翻天覆地的变化,需要先学习一些入门准备知识。

这一章会介绍一些前置的知识点科普,方便开始学习 Vue3 的时候,不会对一些基本的认知和操作存在太多疑惑。

TIP

本指南需要具备一定的 HTML 、 CSS 和 JavaScript 基础,如果完全不懂,请先对这三个知识点进行一些入门的学习。

命令行工具

在前端工程化开发过程中,已经离不开各种命令行操作,例如:管理项目依赖、本地服务启动、打包构建,还有拉取代码 / 提交代码这些 Git 操作等等。

命令行界面( Command-line Interface ,缩写 CLI ),是一种通过命令行来实现人机交互的工具,需要提前准备好命令行界面工具。

如果有所留意,会发现很多工具都可以实现命令行操作,比如:命令行界面( CLI )、终端( Terminal )、 Shell 、控制台( Console )等等。

从完整功能看,它们之间确实有许多区别,不过对于前端开发者来说,日常的命令行交互需要用到的功能不会特别多,所以后面会统一一些名词,减少理解上的偏差。

交互行为统一代替名词代替名词解释
输入命令行需要输入命令的时候,会统一用 “命令行” 来指代。
输出控制台鉴于前端开发者更多接触的是浏览器的 Console 控制台,
所以也是会用 “控制台” 来指代。

Windows

在 Windows 平台,可以使用自带的 CMD 或者 Windows PowerShell 工具。

但为了更好的开发体验,推荐使用以下工具(需要下载安装),可以根据自己的喜好选择其一:

名称简介下载
Windows Terminal由微软推出的强大且高效的 Windows 终端前往 GitHub 下载
CMDer一款体验非常好的 Windows 控制台模拟器前往 GitHub 下载

笔者在 Windows 台式机上是使用 Windows Terminal 比较多,在此之前是用 CMDer ,两者的设计和体验都非常优秀,当然,还有颜值。

macOS

如果使用的是 Mac 系统,可以直接使用系统自带的 “终端” 工具,笔者在 MacBook 上是使用自带的终端进行开发。

TIP

其实只要能正常使用命令行,对于前端工程师来说就可以满足日常需求,但选择更喜欢的工具,可以让自己的开发过程更为身心愉悦!

安装 Node.js 环境

安装好命令行工具之后,来安装 Node 的开发环境。

下载和安装 Node

在 Node.js 官网提供了安装包的下载,不论是使用 Windows 系统还是 MacOS 系统, Node 都提供了对应的安装包,直接下载安装包并运行即可安装到的电脑里,就可以用来开发的项目了。

点击访问:Node.js 官网下载

安装后,打开的 命令行工具 ,输入以下命令即可查看是否安装成功:

bash
node -v

如果已成功安装,会在控制台输出当前的 Node 版本号。

版本之间的区别

可以看到官网标注了 LTS 和 Current 两个系列,并且对应了不同的版本号。

Current 版本

Current 是最新发布版本,或者叫 “尝鲜版” ,可以在这个系列体验到最新的功能,但也可能会有一些意想不到的问题和兼容性要处理。

每六个月会发布一次 Current 大版本,新的偶数版本( e.g. v16.x.x )会在每年的 4 月份发布,奇数版本( e.g. v17.x.x )会在每年的 10 月份发布。

也就是说,所有版本都会有 Current 版本阶段,这个阶段会持续 6 个月的时间,期间会被活跃的维护和变更,在发布满 6 个月后,奇偶数版本会有不同的结果:

  • 大版本号是奇数的,将变为不支持状态,不会进入 LTS 版本。
  • 大版本号是偶数的,会按照发布节点进入 LTS ,并且作为活跃状态投入使用。

TIP

除非是狂热的 Node 开发探索者,否则不应该选择 Current 系列(特别是在生产环境),应该选择未被 EOL 的 LTS 系列作为的项目运行环境,详见下方的 LTS 版本 说明。

LTS 版本

LTS ,全称 Long Time Support ,长期维护版本,这个系列代表着稳定,建议首次下载以及后续的每次升级都选择 LTS 版本,减少开发过程中的未知问题出现。

每个 LTS 版本的大版本号都是偶数,并且会有 3 个阶段的生命周期:

生命周期含义说明
Active活跃阶段每个从 Current 进入 LTS 的偶数版本,都会有 18 个月的时间被积极维护和升级。
Maintenance维护阶段活跃阶段达到 18 个月后,会进入为期 12 个月的维护阶段,期间只会进行错误修复和安全补丁。
End of Life结束阶段简称 EOL ,在维护阶段达到期限之后,该版本进入 EOL 阶段,将不再维护,也就是说,每个 LTS 版本最长会有 30 个月的维护时间,之后将不再进行维护。

TIP

当然也会有一些例外情况,例如 Node.js 16 版本,为了配合 OpenSSL 1.1.1 的 EOL 时间,将提前 7 个月进入 EOL 阶段。

详见官方公告: Bringing forward the End-of-Life Date for Node.js 16

是否需要经常更新版本

不论是 LTS 还是 Current ,每个系列下面都还有不同的大版本和小版本,是不是每次都必须及时更新到最新版呢?

当然不是,完全可以依照的项目技术栈依赖的最低 Node 版本去决定是否需要升级,不过如果条件允许,还是建议至少要把大版本升级到最新的 LTS 版本。

TIP

关于 Node.js 的版本发布时间表可以在官方 GitHub 的 Release 仓库 查看。

基础的 Node 项目

在安装和配置完 Node.js 之后,接下来了解 Node 项目的一些基础组成,这有助于开启前端工程化开发大门。

TIP

当前文档所演示的 hello-node 项目已托管至 learning-vue3/hello-node 仓库,可使用 Git 克隆命令拉取至本地:

bash
# 从 GitHub 克隆
+git clone https://github.com/learning-vue3/hello-node.git
+
+# 如果 GitHub 访问失败,可以从 Gitee 克隆
+git clone https://gitee.com/learning-vue3/hello-node.git

成品项目可作为学习过程中的代码参考,但更建议按照教程的讲解步骤,从零开始亲手搭建一个新项目并完成 node 开发的体验,可以更有效的提升学习效果。

初始化一个项目

如果想让一个项目成为 Node 项目,只需要在命令行 cd 到项目所在的目录,执行初始化命令:

bash
npm init

之后命令行会输出一些提示,以及一些问题,可以根据的实际情况填写项目信息,例如:

bash
package name: (demo) hello-node

以上面这个问题为例:

冒号左边的 package name 是问题的题干,会询问要输入什么内容。

冒号右边的括号内容 (demo) 是 Node 为推荐的答案(不一定会出现这个推荐值),如果觉得 OK ,可以直接按回车确认,进入下一道题。

冒号右边的 hello-node 是输入的答案(如果选择了推荐的答案,则这里为空),这个答案会写入到项目信息文件里。

当回答完所有问题之后,会把填写的信息输出到控制台,确认无误后,回车完成初始化的工作。

bash
{
+  "name": "hello-node",
+  "version": "1.0.0",
+  "description": "A demo about Node.js.",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \\"Error: no test specified\\" && exit 1"
+  },
+  "author": "chengpeiquan",
+  "license": "MIT"
+}
+
+
+Is this OK? (yes)

如果觉得问题太多,太繁琐了,可以直接加上 -y 参数,这样会以 Node 推荐的答案帮快速生成项目信息。

bash
npm init -y

了解 package.json

在完成 项目的初始化 之后,会发现在项目的根目录下出现了一个名为 package.json 的 JSON 文件。

这是 Node 项目的清单,里面记录了这个项目的基础信息、依赖信息、开发过程的脚本行为、发布相关的信息等等,未来将在很多项目里看到它的身影。

TIP

它必须是 JSON 文件,不可以是存储了 JavaScript 对象字面量的 JS 文件。

如果是按照上面初始化一节的操作得到的这个文件,打开它之后,会发现里面存储了在初始化过程中,根据问题确认下来的那些答案,例如:

json
{
+  "name": "hello-node",
+  "version": "1.0.0",
+  "description": "A demo about Node.js.",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \\"Error: no test specified\\" && exit 1"
+  },
+  "author": "chengpeiquan",
+  "license": "MIT"
+}

package.json 的字段并非全部必填,唯一的要求就是,必须是一个 JSON 文件,所以也可以仅仅写入以下内容:

json
{}

但在实际的项目中,往往需要填写更完善的项目信息,除了手动维护这些信息之外,在安装 npm 包等操作时, Node 也会帮写入数据到这个文件里,来了解一些常用字段的含义:

字段名含义
name项目名称,如果打算发布成 npm 包,它将作为包的名称
version项目版本号,如果打算发布成 npm 包,这个字段是必须的,遵循 语义化版本号 的要求
description项目的描述
keywords关键词,用于在 npm 网站上进行搜索
homepage项目的官网 URL
main项目的入口文件
scripts指定运行脚本的命令缩写,常见的如 npm run build 等命令就在这里配置,详见 脚本命令的配置
author作者信息
license许可证信息,可以选择适当的许可证进行开源
dependencies记录当前项目的生产依赖,安装 npm 包时会自动生成,详见:依赖包和插件
devDependencies记录当前项目的开发依赖,安装 npm 包时会自动生成,详见:依赖包和插件
type配置 Node 对 CJS 和 ESM 的支持

其中最后的 type 字段是涉及到模块规范的支持,它有两个可选值: commonjsmodule ,其默认值为 commonjs

  • 当不设置或者设置为 commonjs 时,扩展名为 .js.cjs 的文件都是 CommonJS 规范的模块,如果要使用 ES Module 规范,需要使用 .mjs 扩展名
  • 当设置为 module 时,扩展名为 .js.mjs 的文件都是 ES Module 规范的模块,如果要使用 CommonJS 规范,需要使用 .cjs 扩展名

关于模块规范可以在 学习模块化设计 一节了解更多。

关于 package.json 的完整的选项可以在 npm Docs 上查阅。

项目名称规则

如果打算发布成 npm 包,它将作为包的名称,可以是普通包名,也可以是范围包的包名。

类型释义例子
范围包具备 @scope/project-name 格式,一般有一系列相关的开发依赖之间会以相同的 scope 进行命名@vue/cli@vue/cli-service 就是一系列相关的范围包
普通包其他命名都属于普通包vuevue-router

包名有一定的书写规则:

  • 名称必须保持在 1 ~ 214 个字符之间(包括范围包的 @scope/ 部分)
  • 只允许使用小写字母、下划线、短横线、数字、小数点(并且只有范围包可以以点或下划线开头)
  • 包名最终成为 URL 、命令行参数或者文件夹名称的一部分,所以名称不能包含任何非 URL 安全字符

TIP

了解这一点有助于在后续工作中,在需要查找技术栈相关包的时候,可以知道如何在 npmjs 上找到它们。

如果打算发布 npm 包,可以通过 npm view <package-name> 命令查询包名是否已存在,如果存在就会返回该包的相关信息。

比如查询 vue 这个包名,会返回它的版本号、许可证、描述等信息:

bash
npm view vue
+
+vue@3.2.33 | MIT | deps: 5 | versions: 372
+The progressive JavaScript framework for building modern web UI.
+https://github.com/vuejs/core/tree/main/packages/vue#readme
+
+keywords: vue
+
+# 后面太多信息这里就省略...

如果查询一个不存在的包名,则会返回 404 信息:

bash
npm view vue123456
+npm ERR! code E404
+npm ERR! 404 Not Found - GET https://registry.npmjs.org/vue123456 - Not found
+npm ERR! 404
+npm ERR! 404  'vue123456@latest' is not in this registry.
+npm ERR! 404 You should bug the author to publish it (or use the name yourself!)
+npm ERR! 404
+npm ERR! 404 Note that you can also install from a
+npm ERR! 404 tarball, folder, http url, or git url.
+
+# 后面太多信息这里就省略...

语义化版本号管理

Node 项目遵循 语义化版本号 的规则,例如 1.0.01.0.11.1.0 这样的版本号,本教材的主角 Vue 也是遵循了语义化版本号的发布规则。

建议开发者在入门前端工程化的时候就应该熟悉这套规则,后续的项目开发中,会使用到很多外部依赖,它们也是使用版本号控制来管理代码的发布,每个版本之间可能会有一些兼容性问题,如果不了解版本号的通用规则,很容易在的开发中带来困扰。

TIP

现在有很多 CI/CD 流水线作业具备了根据 Git 的 Commit 记录来自动升级版本号,它们也是遵循了语义化版本号规则,版本号的语义化在前端工程里有重大的意义。

基本格式与升级规则

版本号的格式为: Major.Minor.Patch (简称 X.Y.Z ),它们的含义和升级规则如下:

英文中文含义
Major主版本号当项目作了大量的变更,与旧版本存在一定的不兼容问题
Minor次版本号做了向下兼容的功能改动或者少量功能更新
Patch修订号修复上一个版本的少量 BUG

一般情况下,三者均为正整数,并且从 0 开始,遵循这三条注意事项:

  • 当主版本号升级时,次版本号和修订号归零
  • 当次版本号升级时,修订号归零,主版本号保持不变
  • 当修订号升级时,主版本号和次版本号保持不变

下面以一些常见的例子帮助快速理解版本号的升级规则:

  • 如果不打算发布,可以默认为 0.0.0 ,代表它并不是一个进入发布状态的包
  • 在正式发布之前,可以将其设置为 0.1.0 发布第一个测试版本,自此,代表已进入发布状态,但还处于初期开发阶段,这个阶段可能经常改变 API ,但不需要频繁的更新主版本号
  • 0.1.0 发布后,修复了 BUG ,下一个版本号将设置为 0.1.1 ,即更新了一个修订号
  • 0.1.1 发布后,有新的功能发布,下一个版本号可以升级为 0.2.0 ,即更新了一个次版本号
  • 当觉得这个项目已经功能稳定、没有什么 BUG 了,决定正式发布并给用户使用时,那么就可以进入了 1.0.0 正式版了

版本标识符

以上是一些常规的版本号升级规则,也可以通过添加 “标识符” 来修饰的版本更新:

格式为: Major.Minor.Patch-Identifier.1 ,其中的 Identifier 代表 “标识符” ,它和版本号之间使用 - 短横线来连接,后面的 .1 代表当前标识符的第几个版本,每发布一次,这个数字 +1 。

标识符含义
alpha内部版本,代表当前可能有很大的变动
beta测试版本,代表版本已开始稳定,但可能会有比较多的问题需要测试和修复
rc即将作为正式版本发布,只需做最后的验证即可发布正式版

脚本命令的配置

在工作中,会频繁接触到 npm run dev 启动开发环境、 npm run build 构建打包等操作,这些操作其实是对命令行的一种别名。

它在 package.json 里是存放于 scripts 字段,以 [key: string]: string 为格式的键值对存放数据( key: value )。

json
{
+  "scripts": {
+    // ...
+  }
+}

其中:

  • key 是命令的缩写,也就是 npm run xxx 里的 xxx ,如果一个单词不足以表达,可以用冒号 : 拼接多个单词,例如 mock:listmock:detail 等等

  • value 是完整的执行命令内容,多个命令操作用 && 连接,例如 git add . && git commit

以 Vue CLI 创建的项目为例,它的项目 package.json 文件里就会包括了这样的命令:

json
{
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build"
+  }
+}

这里的名字是可以自定义的,比如可以把 serve 改成更喜欢的 dev

json
{
+  "scripts": {
+    "dev": "vue-cli-service serve",
+    "build": "vue-cli-service build"
+  }
+}

这样运行 npm run dev 也可以相当于运行了 vue-cli-service serve

据笔者所了解,有不少开发者曾经对不同的 Vue CLI 版本提供的 npm run servenpm run dev 有什么区别有过疑问,看到这里应该都明白了吧,可以说没有区别,因为这取决于它对应的命令,而不是取决于它起什么名称。

TIP

如果 value 部分包含了双引号 " ,必须使用转义符 \\ 来避免格式问题,例如: \\"

可以阅读 npm 关于 scripts 的 完整文档 了解更多用法。

Hello Node

看到这里,对于 Node 项目的基本创建流程和关键信息都有所了解了吧!来写一个 demo ,实际体验一下如何从初始化项目到打印一个 Hello World 到控制台的过程。

请先启动的命令行工具,然后创建一个项目文件夹,这里使用 mkdir 命令:

bash
# 语法是 mkdir <dir-name>
+mkdir hello-node

使用 cd 命令进入刚刚创建好的项目目录:

bash
# 语法是 cd <dir-path>
+cd hello-node

执行项目初始化,可以回答问题,也可以添加 -y 参数来使用默认配置:

bash
npm init -y

来到这里就得到了一个具有 package.json 的 Node 项目了。

在项目下创建一个 index.js 的 JS 文件,可以像平时一样书写 JavaScript ,输入以下内容并保存:

js
console.log('Hello World')

然后打开 package.json 文件,修改 scripts 部分如下,也就是配置了一个 "dev": "node index" 命令:

json
{
+  "name": "hello-node",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "dev": "node index"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC"
+}

在命令行执行 npm run dev ,可以看到控制台打印出了 Hello World

bash
npm run dev
+
+> demo@1.0.0 dev
+> node index
+
+Hello World

这等价于直接在命令行执行 node index.js 命令,其中 node 是 Node.js 运行文件的命令, index 是文件名,相当于 index.js ,因为 JS 文件名后缀可以省略。

学习模块化设计

在了解 Node 项目之后,就要开始通过编码来加强对 Node.js 的熟悉程度了,但在开始使用之前,还需要了解一些概念。

在未来的日子里(不限于本教程,与前端工程化相关的工作内容息息相关),会频繁的接触到两个词:模块( Module )和包( Package )。

模块和包是 Node 开发最重要的组成部分,不管是全部自己实现一个项目,还是依赖各种第三方轮子来协助开发,项目的构成都离不开这两者。

模块化解决了什么问题

在软件工程的设计原则里,有一个原则叫 “单一职责” 。

假设一个代码块负责了多个职责的功能支持,在后续的迭代过程中,维护成本会极大的增加,虽然只需要修改这个代码块,但需要兼顾职责 1 、职责 2 、职责 3 … 等多个职责的兼容性,稍不注意就会引起工程运行的崩溃。

“单一职责” 的目的就是减少功能维护带来的风险,把代码块的职责单一化,让代码的可维护性更高。

一个完整业务的内部实现,不应该把各种代码都耦合在一起,而应该按照职责去划分好代码块,再进行组合,形成一个 “高内聚,低耦合” 的工程设计。

模块化就是由此而来,在前端工程里,每个单一职责的代码块,就叫做模块( Module ) ,模块有自己的作用域,功能与业务解耦,非常方便复用和移植。

TIP

模块化还可以解决本章开头所讲述的 传统开发的弊端 里提到的大部分问题,随着下面内容一步步深入,将一步步的理解它。

如何实现模块化

在前端工程的发展过程中,不同时期诞生了很多不同的模块化机制,最为主流的有以下几种:

模块化方案全称适用范围
CJSCommonJSNode 端
AMDAsync Module Definition浏览器
CMDCommon Module Definition浏览器
UMDUniversal Module DefinitionNode 端和浏览器
ESMES ModuleNode 端和浏览器

其中 AMD 、CMD 、 UMD 都已经属于偏过去式的模块化方案,在新的业务里,结合各种编译工具,可以直接用最新的 ESM 方案来实现模块化,所以可以在后续有接触的时候再了解。

ESM ( ES Module ) 是 JavaScript 在 ES6( ECMAScript 2015 )版本推出的模块化标准,旨在成为浏览器和服务端通用的模块解决方案。

CJS ( CommonJS ) 原本是服务端的模块化标准(设计之初也叫 ServerJS ),是为 JavaScript 设计的用于浏览器之外的一个模块化方案, Node 默认支持了该规范,在 Node 12 之前也只支持 CJS ,但从 Node 12 开始,已经同时支持 ES Module 的使用。

至此,不论是 Node 端还是浏览器端, ES Module 是统一的模块化标准了!

但由于历史原因, CJS 在 Node 端依然是非常主流的模块化写法,所以还是值得进行了解,因此下面的内容将主要介绍 CJS 和 ESM 这两种模块化规范是如何实际运用。

TIP

在开始体验模块化的编写之前,请先在电脑里 安装好 Node.js ,然后打开 命令行工具 ,通过 cd 命令进入平时管理项目的目录路径, 初始化一个 Node 项目

另外,在 CJS 和 ESM ,一个独立的文件就是一个模块,该文件内部的变量必须通过导出才能被外部访问到,而外部文件想访问这些变量,需要导入对应的模块才能生效。

用 CommonJS 设计模块

虽然现在推荐使用 ES Module 作为模块化标准,但是日后在实际工作的过程中,还是不免会遇到要维护一些老项目,因此了解 CommonJS 还是非常有必要的。

以下简称 CJS 代指 CommonJS 规范。

准备工作

延续在 Hello Node 部分创建的 Node.js demo 项目,先调整一下目录结构:

  1. 删掉 index.js 文件
  2. 创建一个 src 文件夹,在里面再创建一个 cjs 文件夹
  3. cjs 文件夹里面创建两个文件: index.cjsmodule.cjs

TIP

请注意这里使用了 .cjs 文件扩展名,其实它也是 JS 文件,但这个扩展名是 Node 专门为 CommonJS 规范设计的,可以在 了解 package.json 部分的内容了解更多。

此时目录结构应该如下:

bash
hello-node
+ # 源码文件夹
+├─src
+  # 业务文件夹
+ └─cjs
+    # 入口文件
+   ├─index.cjs
+    # 模块文件
+   └─module.cjs
+ # 项目清单
+└─package.json

这是一个常见的 Node 项目目录结构,通常源代码都会放在 src 文件夹里面统一管理。

接下来再修改一下 package.json 里面的 scripts 部分,改成如下:

json
{
+  "scripts": {
+    "dev:cjs": "node src/cjs/index.cjs"
+  }
+}

后面在命令行执行 npm run dev:cjs 命令,就可以测试刚刚添加的 CJS 模块了。

基本语法

CJS 使用 module.exports 语法导出模块,可以导出任意合法的 JavaScript 类型,例如:字符串、布尔值、对象、数组、函数等等。

使用 require 导入模块,在导入的时候,当文件扩展名是 .js 时,可以只写文件名,而此时使用的是 .cjs 扩展名,所以需要完整的书写。

默认导出和导入

默认导出的意思是,一个模块只包含一个值;而导入默认值则意味着,导入时声明的变量名就是对应模块的值。

src/cjs/module.cjs 文件里,写入以下代码,导出一句 Hello World 信息:

js
// src/cjs/module.cjs
+module.exports = 'Hello World'

TIP

自己在写入代码的时候,不需要包含文件路径那句注释,这句注释只是为了方便阅读时能够区分代码属于哪个文件,以下代码均如此。

src/cjs/index.cjs 文件里,写入以下代码,导入刚刚编写的模块。

js
// src/cjs/index.cjs
+const m = require('./module.cjs')
+console.log(m)

在命令行输入 npm run dev:cjs ,可以看到成功输出了 Hello World 信息:

bash
npm run dev:cjs
+
+> demo@1.0.0 dev:cjs
+> node src/cjs/index.cjs
+
+Hello World

可以看到,在导入模块时,声明的 m 变量拿到的值,就是整个模块的内容,可以直接使用,此例子中它是一个字符串。

再改动一下,把 src/cjs/module.cjs 改成如下,这次导出一个函数:

js
// src/cjs/module.cjs
+module.exports = function foo() {
+  console.log('Hello World')
+}

相应的,这次变成了导入一个函数,所以可以执行它:

js
// src/cjs/index.cjs
+const m = require('./module.cjs')
+m()

得到的结果也是打印一句 Hello World ,不同的是,这一次的打印行为是在模块里定义的,入口文件只是执行模块里的函数。

bash
npm run dev:cjs
+
+> demo@1.0.0 dev:cjs
+> node src/cjs/index.cjs
+
+Hello World

命名导出和导入

默认导出的时候,一个模块只包含一个值,有时候如果想把很多相同分类的函数进行模块化集中管理,例如想做一些 utils 类的工具函数文件、或者是维护项目的配置文件,全部使用默认导出的话,会有非常多的文件要维护。

那么就可以用到命名导出,这样既可以导出多个数据,又可以统一在一个文件里维护管理,命名导出是先声明多个变量,然后通过 {} 对象的形式导出。

再来修改一下 src/cjs/module.cjs 文件,这次改成如下:

js
// src/cjs/module.cjs
+function foo() {
+  console.log('Hello World from foo.')
+}
+
+const bar = 'Hello World from bar.'
+
+module.exports = {
+  foo,
+  bar,
+}

这个时候通过原来的方式去拿模块的值,会发现无法直接获取到函数体或者字符串的值,因为打印出来的也是一个对象。

js
// src/cjs/index.cjs
+const m = require('./module.cjs')
+console.log(m)

控制台输出:

bash
npm run dev:cjs
+
+> demo@1.0.0 dev:cjs
+> node src/cjs/index.cjs
+
+{ foo: [Function: foo], bar: 'Hello World from bar.' }

需要通过 m.foo()m.bar 的形式才可以拿到值。

此时可以用一种更方便的方式,利用 ES6 的对象解构来直接拿到变量:

js
// src/cjs/index.cjs
+const { foo, bar } = require('./module.cjs')
+foo()
+console.log(bar)

这样子才可以直接调用变量拿到对应的值。

导入时重命名

以上都是基于非常理想的情况下使用模块,有时候不同的模块之间也会存在相同命名导出的情况,来看看模块化是如何解决这个问题的。

src/cjs/module.cjs 文件保持不变,依然导出这两个变量:

js
// src/cjs/module.cjs
+function foo() {
+  console.log('Hello World from foo.')
+}
+
+const bar = 'Hello World from bar.'
+
+module.exports = {
+  foo,
+  bar,
+}

这次在入口文件里也声明一个 foo 变量,在导入的时候对模块里的 foo 进行了重命名操作。

js
// src/cjs/index.cjs
+const {
+  foo: foo2,  // 这里进行了重命名
+  bar,
+} = require('./module.cjs')
+
+// 就不会造成变量冲突
+const foo = 1
+console.log(foo)
+
+// 用新的命名来调用模块里的方法
+foo2()
+
+// 这个不冲突就可以不必处理
+console.log(bar)

再次运行 npm run dev:cjs ,可以看到打印出来的结果完全符合预期:

bash
npm run dev:cjs
+
+> demo@1.0.0 dev:cjs
+> node src/cjs/index.cjs
+
+1
+Hello World from foo.
+Hello World from bar.

这是利用了 ES6 解构对象的 给新的变量名赋值 技巧。

以上是针对命名导出时的重命名方案,如果是默认导出,那么在导入的时候用一个不冲突的变量名来声明就可以了。

用 ES Module 设计模块

ES Module 是新一代的模块化标准,它是在 ES6( ECMAScript 2015 )版本推出的,是原生 JavaScript 的一部分。

不过因为历史原因,如果要直接在浏览器里使用该方案,在不同的浏览器里会有一定的兼容问题,需要通过 Babel 等方案进行代码的版本转换(可在 控制编译代码的兼容性 一节了解如何使用 Babel )。

因此一般情况下都需要借助构建工具进行开发,工具通常会提供开箱即用的本地服务器用于开发调试,并且最终打包的时候还可以抹平不同浏览器之间的差异。

随着 ESM 的流行,很多新推出的构建工具都默认只支持该方案( e.g. Vite 、 Rollup ),如果需要兼容 CJS 反而需要另外引入插件单独配置。除了构建工具,很多语言也是默认支持 ESM ,例如 TypeScript ,因此了解 ESM 非常重要。

以下简称 ESM 代指 ES Module 规范。

TIP

在阅读本小节之前,建议先阅读 用 CommonJS 设计模块 以了解前置内容,本小节会在适当的内容前后与 CJS 的写法进行对比。

准备工作

继续使用在 用 CommonJS 设计模块 时使用的 hello-node 项目作为 demo ,当然也可以重新创建一个新的。

一样的,先调整一下目录结构:

  1. src 文件夹里面创建一个 esm 文件夹
  2. esm 文件夹里面创建两个 MJS 文件: index.mjsmodule.mjs

TIP

注意这里使用了 .mjs 文件扩展名,因为默认情况下, Node 需要使用该扩展名才会支持 ES Module 规范。

也可以在 package.json 里增加一个 "type": "module" 的字段来使 .js 文件支持 ESM ,但对应的,原来使用 CommonJS 规范的文件需要从 .js 扩展名改为 .cjs 才可以继续使用 CJS 。

为了减少理解上的门槛,这里选择了使用 .mjs 新扩展名便于入门,可以在 了解 package.json 部分的内容了解更多。

此时目录结构应该如下:

bash
hello-node
+ # 源码文件夹
+├─src
+  # 上次用来测试 CommonJS 的相关文件
+ ├─cjs
+  ├─index.cjs
+  └─module.cjs
+ 
+  # 这次要用的 ES Module 测试文件
+ └─esm
+    # 入口文件
+   ├─index.mjs
+    # 模块文件
+   └─module.mjs
+
+ # 项目清单
+└─package.json

同样的,源代码放在 src 文件夹里面管理。

然后再修改一下 package.json 里面的 scripts 部分,参照上次配置 CJS 的格式,增加一个 ESM 版本的 script ,改成如下:

json
{
+  "scripts": {
+    "dev:cjs": "node src/cjs/index.cjs",
+    "dev:esm": "node src/esm/index.mjs"
+  }
+}

后面在命令行执行 npm run dev:esm 就可以测试的 ESM 模块了。

TIP

注意, script 里的 .mjs 扩展名不能省略。

另外,在实际项目中,可能不需要做这些处理,因为很多工作脚手架已经帮处理过了,比如的 Vue3 项目。

基本语法

ESM 使用 export default (默认导出)和 export (命名导出)这两个语法导出模块,和 CJS 一样, ESM 也可以导出任意合法的 JavaScript 类型,例如:字符串、布尔值、对象、数组、函数等等。

使用 import ... from ... 导入模块,在导入的时候,如果文件扩展名是 .js 则可以省略文件名后缀,否则需要把扩展名也完整写出来。

默认导出和导入

ESM 的默认导出也是一个模块只包含一个值,导入时声明的变量名,它对应的数据就是对应模块的值。

src/esm/module.mjs 文件里,写入以下代码,导出一句 Hello World 信息:

js
// src/esm/module.mjs
+export default 'Hello World'

src/esm/index.mjs 文件里,写入以下代码,导入刚刚编写的模块。

js
// src/esm/index.mjs
+import m from './module.mjs'
+console.log(m)

在命令行输入 npm run dev:esm ,可以看到成功输出了 Hello World 信息:

bash
npm run dev:esm
+
+> demo@1.0.0 dev:esm
+> node src/esm/index.mjs
+
+Hello World

可以看到,在导入模块时,声明的 m 变量拿到的值,就是整个模块的内容,可以直接使用,此例子中它是一个字符串。

像在 CJS 的例子里一样,也来再改动一下,把 src/esm/module.mjs 改成导出一个函数:

js
// src/esm/module.mjs
+export default function foo() {
+  console.log('Hello World')
+}

同样的,这次也是变成了导入一个函数,可以执行它:

js
// src/esm/index.mjs
+import m from './module.mjs'
+m()

一样可以从模块里的函数得到一句 Hello World 的打印信息。

bash
npm run dev:esm
+
+> demo@1.0.0 dev:esm
+> node src/esm/index.mjs
+
+Hello World

TIP

可以看到, CJS 和 ESM 的默认导出是非常相似的,在未来如果有老项目需要从 CJS 往 ESM 迁移,大部分情况下只需要把 module.exports 改成 export default 即可。

命名导出和导入

虽然默认导出的时候, CJS 和 ESM 的写法非常相似,但命名导出却完全不同!

在 CJS 里,使用命名导出后的模块数据默认是一个对象,可以导入模块后通过 m.foo 这样的方式去调用对象的属性,或者在导入的时候直接解构拿到对象上的某个属性:

js
// CJS 支持导入的时候直接解构
+const { foo } = require('./module.cjs')

但 ES Module 的默认导出不能这样做,例如下面这个例子,虽然默认导出了一个对象:

js
// 在 ESM ,通过这样导出的数据也是属于默认导出
+export default {
+  foo: 1,
+}

但是无法和 CJS 一样通过大括号的方式导入其中的某个属性:

js
// ESM 无法通过这种方式对默认导出的数据进行 “解构”
+import { foo } from './module.mjs'

这样操作在运行过程中,控制台会抛出错误信息:

bash
import { foo } from './module.mjs'
+         ^^^
+SyntaxError:
+The requested module './module.mjs' does not provide an export named 'foo'

正确的方式应该是通过 export 对数据进行命名导出,先将 src/esm/module.mjs 文件修改成如下代码,请留意 export 关键字的使用:

js
// src/esm/module.mjs
+export function foo() {
+  console.log('Hello World from foo.')
+}
+
+export const bar = 'Hello World from bar.'

通过 export 命名导出的方式,现在才可以使用大括号将它们进行命名导入:

js
// src/esm/index.mjs
+import { foo, bar } from './module.mjs'
+
+foo()
+console.log(bar)

这一次程序可以顺利运行了:

bash
npm run dev:esm
+
+> demo@1.0.0 dev:esm
+> node src/esm/index.mjs
+
+Hello World from foo.
+Hello World from bar.

那么有没有办法像 CJS 一样使用 m.foo 调用对象属性的方式一样,去使用这些命名导出的模块呢?

答案是肯定的!命名导出支持使用 * as 变量名称 的方式将其所有命名挂在某个变量上,该变量是一个对象,每一个导出的命名都是其属性:

ts
// src/esm/index.mjs
+// 注意这里使用了另外一种方式,将所有的命名导出都挂在了 \`m\` 变量上
+import * as m from './module.mjs'
+
+console.log(typeof m)
+console.log(Object.keys(m))
+
+m.foo()
+console.log(m.bar)

运行 npm run dev:esm ,将输出:

bash
npm run dev:esm
+
+> demo@1.0.0 dev:esm
+> node src/esm/index.mjs
+
+object
+[ 'bar', 'foo' ]
+Hello World from foo.
+Hello World from bar.

导入时重命名

接下来看看 ESM 是如何处理相同命名导出的问题,项目下的模块文件依然保持不变,还是导出两个变量:

js
// src/esm/module.mjs
+export function foo() {
+  console.log('Hello World from foo.')
+}
+
+export const bar = 'Hello World from bar.'

入口文件里面,也声明一个 foo 变量,然后导入的时候对模块里的 foo 进行重命名操作:

js
// src/esm/index.mjs
+import {
+  foo as foo2,  // 这里进行了重命名
+  bar
+} from './module.mjs'
+
+// 就不会造成变量冲突
+const foo = 1
+console.log(foo)
+
+// 用新的命名来调用模块里的方法
+foo2()
+
+// 这个不冲突就可以不必处理
+console.log(bar)

可以看到,在 ESM 的重命名方式和 CJS 是完全不同的,它是使用 as 关键字来操作,语法为 <old-name> as <new-name>

现在再次运行 npm run dev:esm ,可以看到打印出来的结果也是完全符合预期了:

bash
npm run dev:esm
+
+> demo@1.0.0 dev:esm
+> node src/esm/index.mjs
+
+1
+Hello World from foo.
+Hello World from bar.

以上是针对命名导出时的重命名方案,如果是默认导出,和 CJS 一样,在导入的时候用一个不冲突的变量名来声明就可以了。

在浏览器里访问 ESM

ES Module 除了支持在 Node 环境使用,还可以和普通的 JavaScript 代码一样在浏览器里运行。

要在浏览器里体验 ESM ,需要使用现代的主流浏览器(如 Chrome ),并注意其访问限制,例如本地开发不能直接通过 file:// 协议在浏览器里访问本地 HTML 内引用的 JS 文件,这是因为浏览器对 JavaScript 的安全性要求,会触发 CORS 错误,因此需要启动本地服务并通过 http:// 协议访问。

TIP

CORS (全称 Cross-Origin Resource Sharing )是指跨源资源共享,可以决定浏览器是否需要阻止 JavaScript 获取跨域请求的响应。

现代浏览器默认使用 “同源安全策略” ,这里的 “源” 指 URL 的 origin 部分,例如网页可以通过 window.location.origin 获取到如 https://example.com 这样格式的数据,就是网页的 origin

默认情况下,非同源的请求会被浏览器拦截,最常见的场景是通过 XHR 或者 Fetch 请求 API 接口,需要网页和接口都部署在同一个域名才可以请求成功,否则就会触发跨域限制。

如果网页和接口不在同一个域名,例如网页部署在 https://web.example.com ,接口部署在 https://api.example.com ,此时需要在 https://api.example.com 的 API 服务端程序里,配置 Access-Control-Allow-Origin: * 允许跨域请求( * 代表允许任意外域访问,也可以指定具体的域名作为白名单列表)。

添加服务端程序

接下来搭建一个简单的本地服务,并通过 HTML 文件来引入 ESM 模块文件,体验浏览器端如何使用 ESM 模块。

在 hello-node 项目的根目录下创建名为 server 的文件夹(与 src 目录同级),并添加 index.js 文件,敲入以下代码:

js
// server/index.js
+const { readFileSync } = require('fs')
+const { resolve } = require('path')
+const { createServer } = require('http')
+
+/**
+ * 判断是否 ESM 文件
+ */
+function isESM(url) {
+  return String(url).endsWith('mjs')
+}
+
+/**
+ * 获取 MIME Type 信息
+ * @tips \`.mjs\` 和 \`.js\` 一样,都使用 JavaScript 的 MIME Type
+ */
+function mimeType(url) {
+  return isESM(url) ? 'application/javascript' : 'text/html'
+}
+
+/**
+ * 获取入口文件
+ * @returns 存放在本地的文件路径
+ */
+function entryFile(url) {
+  const file = isESM(url) ? \`../src/esm\${url}\` : './index.html'
+  return resolve(__dirname, file)
+}
+
+/**
+ * 创建 HTTP 服务
+ */
+const app = createServer((request, response) => {
+  // 获取请求时的相对路径,如网页路径、网页里的 JS 文件路径等
+  const { url } = request
+
+  // 转换成对应的本地文件路径并读取其内容
+  const entry = entryFile(url)
+  const data = readFileSync(entry, 'utf-8')
+
+  // 需要设置正确的响应头信息,浏览器才可以正确响应
+  response.writeHead(200, { 'Content-Type': mimeType(url) })
+  response.end(data)
+})
+
+/**
+ * 在指定的端口号启动本地服务
+ */
+const port = 8080
+app.listen(port, '0.0.0.0', () => {
+  console.log(\`Server running at:\`)
+  console.log()
+  console.log(\`  ➜  Local:  http://localhost:\${port}/\`)
+  console.log()
+})

这是一个基础的 Node.js 服务端程序,利用了 HTTP 模块启动本地服务,期间利用 FS 模块的 I/O 能力对本地文件进行读取,而 PATH 模块则简化了文件操作过程中的路径处理和兼容问题(例如众所周知的 Windows 与 macOS 的路径斜杆问题)。

TIP

在这段服务端程序代码里,请留意 mimeType 方法,要让浏览器能够正确解析 .mjs 文件,需要在服务端响应文件内容时,将其 MIME Type 设置为 和 JavaScript 文件一样,这一点非常重要。

并且需要注意传递给 readFileSync API 的文件路径是否与真实存在的文件路径匹配,如果启动服务时,在 Node 控制台报了 no such file or directory 的错误,请检查是否因为笔误写错了文件名称,或者文件路径多了空格等情况。

添加入口页面

继续在 server 目录下添加一个 index.html 并写入以下 HTML 代码,它将作为网站的首页文件:

TIP

可以在 VSCode 先新建一个空文件,文件语言设置为 HTML ,并写入英文感叹号 ! ,再按 Tab 键(或者鼠标选择第一个代码片段提示),可快速生成基础的 HTML 结构。

html
<!-- server/index.html -->
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>ESM run in browser</title>
+  </head>
+  <body>
+    <script type="module" src="./index.mjs"></script>
+  </body>
+</html>

请注意在 <script /> 标签这一句代码上,比平时多了一个 type="module" 属性,这代表这个 script 是使用了 ESM 模块,而 src 属性则对应指向了上文在 src/esm 目录下的入口文件名。

之所以无需使用 ../src/esm/index.mjs 显式的指向真实目录,是因为在 添加服务端程序 时,已通过服务端代码里的 entryFile 方法重新指向了文件所在的真实路径,所以在 HTML 文件里可以使用 ./ 简化文件路径。

启动服务并访问

打开 package.json 文件,在 scripts 字段追加一个 serve 命令如下:

json
{
+  "scripts": {
+    "dev:cjs": "node src/cjs/index.cjs",
+    "dev:esm": "node src/esm/index.mjs",
+    "serve": "node server/index.js"
+  }
+}

在命令行运行 npm run serve 即可启动本地服务:

bash
 npm run serve
+
+> demo@1.0.0 serve
+> node server/index.js
+
+Server running at:
+
+    Local:  http://localhost:8080/

根据命令行提示,在浏览器访问 http://localhost:8080/ 地址,即可访问本地服务。

TIP

如遭遇端口号冲突,可在 server/index.js 的 const port = 8080 代码处修改为其他端口号。

因为在编写 HTML 文件时没有写入内容,只引入了 ESM 模块文件,因此需要按 F12 唤起浏览器的控制台查看 Log ,可以看到控制台根据模块的文件内容,输出了这三句 Log (如果没有 Log ,可在控制台唤起的情况下按 F5 重新载入页面):

bash
1                                                   index.mjs:8
+Hello World from foo.                               module.mjs:2
+Hello World from bar.                               index.mjs:14

分别来自 src/esm/index.mjs 本身的 console.log 语句,以及 import 进来的 module.mjs 里的 console.log 语句。

如果未能出现这三句 Log ,请留意 .mjs 文件内容是否为上一小节最后的内容:

src/esm/index.mjs 文件内容为:

js
// src/esm/index.mjs
+import {
+  foo as foo2, // 这里进行了重命名
+  bar,
+} from './module.mjs'
+
+// 就不会造成变量冲突
+const foo = 1
+console.log(foo)
+
+// 用新的命名来调用模块里的方法
+foo2()
+
+// 这个不冲突就可以不必处理
+console.log(bar)

src/esm/module.mjs 文件内容为:

js
// src/esm/module.mjs
+export function foo() {
+  console.log('Hello World from foo.')
+}
+
+export const bar = 'Hello World from bar.'
内联的 ESM 代码

到目前为止, server/index.html 文件里始终是通过文件的形式引入 ESM 模块,其实 <script type="module" /> 也支持编写内联代码,和普通的 <script /> 标签用法相同:

html
<script type="module">
+  // ESM 模块的 JavaScript 代码
+</script>

请移除 <script /> 标签的 src 属性,并在标签内写入 src/esm/index.mjs 文件里的代码,现在该 HTML 文件的完整代码如下:

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>ESM run in browser</title>
+  </head>
+  <body>
+    <!-- 标签内的代码就是 src/esm/index.mjs 的代码 -->
+    <script type="module">
+      import {
+        foo as foo2, // 这里进行了重命名
+        bar,
+      } from './module.mjs'
+
+      // 就不会造成变量冲突
+      const foo = 1
+      console.log(foo)
+
+      // 用新的命名来调用模块里的方法
+      foo2()
+
+      // 这个不冲突就可以不必处理
+      console.log(bar)
+    </script>
+  </body>
+</html>

回到浏览器刷新 http://localhost:8080/ ,可以看到浏览器控制台依然输出了和引入 src="./index.mjs" 时一样的 Log 信息:

bash
1                                                   (index):21
+Hello World from foo.                               module.mjs:2
+Hello World from bar.                               (index):27
了解模块导入限制

虽然以上例子可以完美地在浏览器里引用现成的 ESM 模块代码并运行,但不代表工程化项目下所有的 ES Module 模块化方式都适合浏览器。

先做一个小尝试,将 src/esm/index.mjs 文件内容修改如下,导入项目已安装的 md5 工具包:

js
// src/esm/index.mjs
+import md5 from 'md5'
+console.log(md5('Hello World'))

回到浏览器刷新 http://localhost:8080/ ,观察控制台,可以发现出现了一个红色的错误信息:

bash
Uncaught TypeError: Failed to resolve module specifier "md5".
+Relative references must start with either "/", "./", or "../".

这是因为不论是通过 <script type="module" /> 标签还是通过 import 语句导入,模块的路径都必须是以 /./ 或者是 ../ 开头,因此无法直接通过 npm 包名进行导入。

这种情况下需要借助另外一个 script 类型: importmap ,在 server/index.html 里追加 <script type="importmap" /> 这一段代码:

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>ESM run in browser</title>
+  </head>
+  <body>
+    <!-- 注意需要先通过 \`importmap\` 引入 npm 包的 CDN -->
+    <script type="importmap">
+      {
+        "imports": {
+          "md5": "https://esm.run/md5"
+        }
+      }
+    </script>
+
+    <!-- 然后才能在 \`module\` 里 \`import xx from 'xx'\` -->
+    <script type="module" src="./index.mjs"></script>
+  </body>
+</html>

再次刷新页面,可以看到控制台成功输出了 b10a8db164e0754105b7a99be72e3fe5 这个字符串,也就是 Hello World 被 MD5 处理后的结果。

可以看到 importmap 的声明方式和 package.json 的 dependencies 字段非常相似, JSON 的 key 是包名称, value 则是支持 ESM 的远程地址。

TIP

Import Maps 的运行机制是通过 import 映射来控制模块说明符的解析,类似于构建工具常用的 alias 别名机制。

这是一个现代浏览器才能支持的新特性,建议使用 Chrome 最新版本体验完整功能,可以在其 GitHub 仓库 查看更多用法。

上方例子里, md5 对应的远程地址是使用了来自 esm.run 网站的 URL ,而不是 npm 包同步到 jsDelivr CDN 或者 UNPKG CDN 的地址,这是因为 md5 这个包本身不支持 ES Module ,需要通过 esm.run 这个网站进行在线转换才可以在 <script type="module" /> 上使用。

`,323),F=l(`

该网站的服务是 jsDelivr CDN 所属的服务商提供,因此也可以通过 jsDelivr CDN 的 URL 添加 /+esm 参数来达到转换效果,以 md5 包为例:

bash
# 默认是一个 CJS 包
+https://cdn.jsdelivr.net/npm/md5
+
+# 可添加 \`/+esm\` 参数变成 ESM 包
+https://cdn.jsdelivr.net/npm/md5/+esm

总的来说,现阶段在浏览器使用 ES Module 并不是一个很好的选择,建议开发者还是使用构建工具来开发,工具可以抹平这些浏览器差异化问题,降低开发成本。

认识组件化设计

学习完模块化设计之后,未来在 Vue 的工程化开发过程中,还会遇到一个新的概念,那就是 “组件” 。

什么是组件化

模块化属于 JavaScript 的概念,但作为一个页面,都知道它是由 HTML + CSS + JS 三部分组成的,既然 JS 代码可以按照不同的功能、需求划分成模块,那么页面是否也可以呢?

答案是肯定的!组件化就是由此而来。

在前端工程项目里,页面可以理解为一个积木作品,组件则是用来搭建这个作品的一块又一块积木。

`,9),i=l(`

解决了什么问题

模块化属于 JavaScript 的概念,把代码块的职责单一化,一个函数、一个类都可以独立成一个模块。

但这只解决了逻辑部分的问题,一个页面除了逻辑,还有骨架( HTML )和样式( CSS ),组件就是把一些可复用的 HTML 结构和 CSS 样式再做一层抽离,然后再放置到需要展示的位置。

常见的组件有:页头、页脚、导航栏、侧边栏… 甚至小到一个用户头像也可以抽离成组件,因为头像可能只是尺寸、圆角不同而已。

每个组件都有自己的 “作用域” , JavaScript 部分利用 模块化 来实现作用域隔离, HTML 和 CSS 代码则借助 Style Scoped 来生成独有的 hash ,避免全局污染,这些方案组合起来,使得组件与组件之间的代码不会互相影响。

如何实现组件化

在 Vue ,是通过 Single-File Component (简称 SFC , .vue 单文件组件)来实现组件化开发。

一个 Vue 组件是由三部分组成的:

vue
<template>
+  <!-- HTML 代码 -->
+</template>
+
+<script>
+// JavaScript 代码
+</script>
+
+<style scoped>
+/* CSS 代码 */
+</style>

在后面的 单组件的编写 一章中,会详细介绍如何编写一个 Vue 组件。

依赖包和插件

在实际业务中,经常会用到各种各样的插件,插件在 Node 项目里的体现是一个又一个的依赖包。

虽然也可以把插件的代码文件手动放到的源码文件夹里引入,但并不是一个最佳的选择,本节内容将带了解 Node 的依赖包。

什么是包

在 Node 项目里,包可以简单理解为模块的集合,一个包可以只提供一个模块的功能,也可以作为多个模块的集合集中管理。

包通常是发布在官方的包管理平台 npmjs 上面,开发者需要使用的时候,可以通过包管理器安装到项目里,并在的代码里引入,开箱即用(详见: 依赖包的管理 )。

使用 npm 包可以减少在项目中重复造轮子,提高项目的开发效率,也可以极大的缩小项目源码的体积(详见:什么是 node_modules)。

包管理平台官网:https://www.npmjs.com

什么是 node_modules

node_modules 是 Node 项目下用于存放已安装的依赖包的目录,如果不存在,会自动创建。

如果是本地依赖,会存在于项目根目录下,如果是全局依赖,会存在于环境变量关联的路径下,详见下方的管理依赖部分内容的讲解。

TIP

一般在提交项目代码到 Git 仓库或者的服务器上时,都需要排除 node_modules 文件夹的提交,因为它非常大。

如果托管在 Git 仓库,可以在 .gitignore 文件里添加 node_modules 作为要排除的文件夹名称。

什么是包管理器

包管理器( Package Manager )是用来管理依赖包的工具,比如:发布、安装、更新、卸载等等。

Node 默认提供了一个包管理器 npm ,在安装 Node.js 的时候,默认会一起安装 npm 包管理器,可以通过以下命令查看它是否正常。

bash
npm -v

如果正常,将会输出相应的版本号。

依赖包的管理

接下来会以 npm 作为默认的包管理器,来了解如何在项目里管理依赖包。

配置镜像源

在国内,直接使用 npm 会比较慢,可以通过绑定 npm Mirror 中国镜像站 的镜像源来提升依赖包的下载速度。

可以先在命令行输入以下命令查看当前的 npm 配置:

bash
npm config get registry
+# https://registry.npmjs.org/

默认情况下,会输出 npm 官方的资源注册表地址,接下来在命令行上输入以下命令,进行镜像源的绑定:

bash
npm config set registry https://registry.npmmirror.com

可以再次运行查询命令来查看是否设置成功:

bash
npm config get registry
+# https://registry.npmmirror.com/

可以看到已经成功更换为中国镜像站的地址了,之后在安装 npm 包的时候,速度会有很大的提升!

如果需要删除自己配置的镜像源,可以输入以下命令进行移除,移除后会恢复默认设置:

bash
npm config rm registry

TIP

如果之前已经绑定过 npm.taobao 系列域名,也请记得更换成 npmmirror 这个新的域名!

随着新的域名已经正式启用,老 npm.taobao.orgregistry.npm.taobao.org 域名在 2022 年 05 月 31 日零时后不再提供服务。

详见:【望周知】淘宝 npm 镜像站喊你切换新域名啦

本地安装

项目的依赖建议优先选择本地安装,这是因为本地安装可以把依赖列表记录到 package.json 里,多人协作的时候可以减少很多问题出现,特别是当本地依赖与全局依赖版本号不一致的时候。

生产依赖

执行 npm install 的时候,添加 --save 或者 -S 选项可以将依赖安装到本地,并列为生产依赖。

TIP

需要提前在命令行 cd 到的项目目录下再执行安装。

另外, --save 或者 -S 选项在实际使用的时候可以省略,因为它是默认选项。

bash
npm install --save <package-name>

可以在项目的 package.json 文件里的 dependencies 字段查看是否已安装成功,例如:

json
// package.json
+{
+  // 会安装到这里
+  "dependencies": {
+    // 以 "包名":"版本号" 的格式写入
+    "vue-router": "^4.0.14"
+  }
+}

生产依赖包会被安装到项目根目录下的 node_modules 目录里。

项目在上线后仍需用到的包,就需要安装到生产依赖里,比如 Vue 的路由 vue-router 就需要以这个方式安装。

开发依赖

执行 npm install 的时候,如果添加 --save-dev 或者 -D 选项,可以将依赖安装到本地,并写入开发依赖里。

TIP

需要提前在命令行 cd 到的项目目录下再执行安装。

bash
npm install --save-dev <package-name>

可以在项目的 package.json 文件里的 devDependencies 字段查看是否已安装成功,例如:

json
// package.json
+{
+  // 会安装到这里
+  "devDependencies": {
+    // 以 "包名":"版本号" 的格式写入
+    "eslint": "^8.6.0"
+  }
+}

开发依赖包也是会被安装到项目根目录下的 node_modules 目录里。

和生产依赖包不同的点在于,只在开发环境生效,构建部署到生产环境时可能会被抛弃,一些只在开发环境下使用的包,就可以安装到开发依赖里,比如检查代码是否正确的 ESLint 就可以用这个方式安装。

全局安装

执行 npm install 的时候,如果添加 --global 或者 -g 选项,可以将依赖安装到全局,它们将被安装在 配置环境变量 里配置的全局资源路径里。

bash
npm install --global <package-name>

TIP

Mac 用户需要使用 sudo 来提权才可以完成全局安装。

另外,可以通过 npm root -g 查看全局包的安装路径。

一般情况下,类似于 @vue/cli 之类的脚手架会提供全局安装的服务,安装后,就可以使用 vue create xxx 等命令直接创建 Vue 项目了。

但不是每个 npm 包在全局安装后都可以正常使用,请阅读 npm 包的主页介绍和使用说明。

版本控制

有时候一些包的新版本不一定适合的老项目,因此 npm 也提供了版本控制功能,支持通过指定的版本号或者 Tag 安装。

语法如下,在包名后面紧跟 @ 符号,再紧跟版本号或者 Tag 名称。

bash
npm install <package-name>@<version | tag>

例如:

现阶段 Vue 默认为 3.x 的版本了,如果想安装 Vue 2 ,可以通过指定版本号的方式安装:

bash
npm install vue@2.6.14

或者通过对应的 Tag 安装:

bash
npm install vue@legacy

TIP

版本号或者 Tag 名称可以在 npmjs 网站上的包详情页查询。

版本升级

一般来说,直接重新安装依赖包可以达到更新的目的,但也可以通过 npm update 命令来更新。

语法如下,可以更新全部的包:

bash
npm update

也可以更新指定的包:

bash
npm update <package-name>

npm 会检查是否有满足版本限制的更新版本。

卸载

可以通过 npm uninstall 命令来卸载指定的包,和安装一样,卸载也区分了卸载本地依赖包和卸载全局包,不过只有在卸载全局包的时候才需要添加选项,默认只卸载当前项目下的本地包。

本地卸载:

bash
npm uninstall <package-name>

全局卸载:

bash
npm uninstall --global <package-name>

TIP

Mac 用户需要使用 sudo 来提权才可以完成全局卸载。

如何使用包

在了解了 npm 包的常规操作之后,通过一个简单的例子来了解如何在项目里使用 npm 包。

继续使用的 Hello Node demo ,或者也可以重新创建一个 demo 。

首先在 命令行工具 通过 cd 命令进入项目所在的目录,用本地安装的方式来把 md5 包 添加到生产依赖,这是一个为提供开箱即用的哈希算法的包,在未来的实际工作中,可能也会用到它,在这里使用它是因为足够简单。

输入以下命令并回车执行:

bash
npm install md5

可以看到控制台提示一共安装了 4 个包,这是因为 md5 这个 npm 包还引用了其他的包作为依赖,需要同时安装才可以正常工作。

bash
# 这是安装 md5 之后控制台的信息返回
+added 4 packages, and audited 5 packages in 2s
+
+found 0 vulnerabilities

此时项目目录下会出现一个 node_modules 文件夹和一个 package-lock.json 文件:

bash
hello-node
+ # 依赖文件夹
+├─node_modules
+ # 源码文件夹
+├─src
+ # 锁定安装依赖的版本号
+├─package-lock.json
+ # 项目清单
+└─package.json

先打开 package.json ,可以看到已经多出了一个 dependencies 字段,这里记录了刚刚安装的 md5 包信息。

json
{
+  "name": "hello-node",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "dev:cjs": "node src/cjs/index.cjs",
+    "dev:esm": "node src/esm/index.mjs",
+    "serve": "node server/index.js"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "md5": "^2.3.0"
+  }
+}

来到这里可能会有一连串的疑问:

  1. 为什么只安装了一个 md5 ,但控制台提示安装了 4 个包?
  2. 为什么 package.json 又只记录了 1 个 md5 包信息?
  3. 为什么提示审核了 5 个包,哪里来的第 5 个包?

不要着急,请先打开 package-lock.json 文件,这个文件是记录了锁定安装依赖的版本号信息(由于篇幅原因,这里的展示省略了一些包的细节):

json
{
+  "name": "hello-node",
+  "version": "1.0.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "hello-node",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "md5": "^2.3.0"
+      }
+    },
+    "node_modules/charenc": {
+      "version": "0.0.2"
+      // ...
+    },
+    "node_modules/crypt": {
+      "version": "0.0.2"
+      // ...
+    },
+    "node_modules/is-buffer": {
+      "version": "1.1.6"
+      // ...
+    },
+    "node_modules/md5": {
+      "version": "2.3.0"
+      // ...
+    }
+  },
+  "dependencies": {
+    "charenc": {
+      "version": "0.0.2"
+      // ...
+    },
+    "crypt": {
+      "version": "0.0.2"
+      // ...
+    },
+    "is-buffer": {
+      "version": "1.1.6"
+      // ...
+    },
+    "md5": {
+      "version": "2.3.0",
+      // ...
+      "requires": {
+        "charenc": "0.0.2",
+        "crypt": "0.0.2",
+        "is-buffer": "~1.1.6"
+      }
+    }
+  }
+}

可以看到这个文件的 dependencies 字段除了 md5 之外,还有另外 3 个包信息,它们就是 md5 包所依赖的另外 3 个 npm 包了,这就解答了为什么一共安装了 4 个 npm 包。

在 node_modules 文件夹下也可以看到以这 4 个包名为命名的文件夹,这些文件夹存放的就是各个包项目发布在 npmjs 平台上的文件。

再看 packages 字段,这里除了罗列出 4 个 npm 包的信息之外,还把项目的信息也列了进来,这就是为什么是提示审核了 5 个包,原因是除了 4 个依赖包,该项目本身也是一个包。

TIP

package-lock.json 文件并不是一成不变的,假如以后 md5 又引用了更多的包,这里记录的信息也会随之增加。

并且不同的包管理器,它的 lock 文件也会不同,如果是使用 yarn 作为包管理器的话,它是生成一个 yarn.lock 文件,而不是 package-lock.json ,有关更多的包管理器,详见 插件的使用 一章。

现在已经安装好 md5 包了,接下来看看具体如何使用它。

通常在包的 npmjs 主页上会有 API 和用法的说明,只需要根据说明操作,打开 src/esm/index.mjs 文件,首先需要导入这个包。

包的导入和在 学习模块化设计 一节了解到的模块导入用法是一样的,只是把 from 后面的文件路径换成了包名。

js
// src/esm/index.mjs
+import md5 from 'md5'

然后根据 md5 的用法,来编写一个小例子,先声明一个原始字符串变量,然后再声明一个使用 md5 加密过的字符串变量,并打印它们:

js
// src/esm/index.mjs
+import md5 from 'md5'
+
+const before = 'Hello World'
+const after = md5(before)
+console.log({ before, after })

在命令行输入 npm run dev:esm ,可以在控制台看到输出了这些内容,成功获得了转换后的结果:

bash
npm run dev:esm
+
+> demo@1.0.0 dev:esm
+> node src/esm/index.mjs
+
+{ before: 'Hello World', after: 'b10a8db164e0754105b7a99be72e3fe5' }

是不是非常简单,其实包的用法和在导入模块的用法可以说是完全一样的,区别主要在于,包是需要安装了才能用,而模块是需要自己编写。

控制编译代码的兼容性

作为一名前端工程师,了解如何控制代码的兼容性是非常重要的能力。

在 “了解前端工程化” 的 为什么要使用构建工具 一节里,已简单介绍过 Polyfill 的作用,以及介绍了构建工具可以通过 Babel 等方案自动化处理代码的兼容问题,这一小节将讲解 Babel 的配置和使用,亲自体验如何控制代码的兼容性转换。

如何查询兼容性

在开始学习使用 Babel 之前,需要先掌握一个小技能:了解如何查询代码在不同浏览器上的兼容性。

说起浏览器兼容性,前端工程师应该都不陌生,特别是初学者很容易会遇到在自己的浏览器上布局正确、功能正常,而在其他人的电脑或者手机上访问就会有布局错位或者运行报错的问题出现,最常见的场景就是开发者使用的是功能强大的 Chrome 浏览器,而产品用户使用了 IE 浏览器。

这是因为网页开发使用的 HTML / CSS / JavaScript 每年都在更新新版本,推出更好用的新 API ,或者废弃部分过时的旧 API ,不同的浏览器在版本更新过程中,对这些新 API 的支持程度并不一致,如果使用了新 API 而没有做好兼容支持,很容易就会在低版本浏览器上出现问题。

为了保证程序可以正确的在不同版本浏览器之间运行,就需要根据产品要支持的目标浏览器范围,去选择兼容性最好的编程方案。

在 Web 开发有一个网站非常知名:Can I use ,只要搜索 API 的名称,它会以图表的形式展示该 API 在不同浏览器的不同版本之间的支持情况,支持 HTML 标签、 CSS 属性、 JavaScript API 等内容的查询。

以 JavaScript ES6 的 class 新特性为例:

`,128),C=l(`

可以看到在 Chrome 浏览器需要在 49 版本开始才被完全支持,而 IE 浏览器则全面不支持,如果不做特殊处理(例如引入 Polyfill 方案),那么就需要考虑在编程过程中,是否需要可以直接使用 class 来实现功能,还是寻找其他替代方案。

在工作中,工程师无需关注每一个 API 的具体支持范围,这些工作可以交给工具来处理,下面将介绍 Babel 的使用入门。

Babel 的使用和配置

Babel 是一个 JavaScript 编译器,它可以让开发者仅需维护一份简单的 JSON 配置文件,即可调动一系列工具链将源代码编译为目标浏览器指定版本所支持的语法。

安装 Babel

请打开 hello-node 项目,安装以下几个 Babel 依赖:

bash
npm i -D @babel/core @babel/cli @babel/preset-env

此时在 package.json 的 devDependencies 可以看到有了如下三个依赖:

json
{
+  "devDependencies": {
+    "@babel/cli": "^7.19.3",
+    "@babel/core": "^7.19.3",
+    "@babel/preset-env": "^7.19.3"
+  }
+}

它们的作用分别如下:

依赖作用文档
@babel/cli安装后可以从命令行使用 Babel 编译文件查看文档
@babel/coreBabel 的核心功能包查看文档
@babel/preset-env智能预设,可以通过它的选项控制代码要转换的支持版本查看文档

TIP

在使用 Babel 时,建议在项目下进行本地安装,尽量不选择全局安装,这是因为不同项目可能依赖于不同版本的 Babel ,全局依赖和可能会出现使用上的异常。

添加 Babel 配置

接下来在 hello-node 的根目录下创建一个名为 babel.config.json 的文件,这是 Babel 的配置文件,写入以下内容:

json
{
+  "presets": [
+    [
+      "@babel/preset-env",
+      {
+        "targets": {
+          "chrome": "41"
+        },
+        "modules": false,
+        "useBuiltIns": "usage",
+        "corejs": "3.6.5"
+      }
+    ]
+  ]
+}

这份配置将以 Chrome 浏览器作为目标浏览器,编译结果将保留 ES Module 规范,可以在 配置文件文档 查看更多配置选项。

这里的 targets.chrome 字段代表编译后要支持的目标浏览器版本号,在 caniuse 查询可知 ES6 的 class 语法 在 Chrome 49 版本之后才被完全支持,而 Chrome 41 或更低的版本是完全不支持该语法,因此先将其目标版本号设置为 41 ,下一步将开始测试 Babel 的编译结果。

使用 Babel 编译代码

在 hello-node 的 src 目录下添加一个 babel 文件夹,并在该文件夹下创建一个 index.js 文件,写入以下代码:

js
// src/babel/index.js
+export class Hello {
+  constructor(name) {
+    this.name = name
+  }
+
+  say() {
+    return \`Hello \${this.name}\`
+  }
+}

根据上一步的 Babel 配置,在这里使用 class 语法作为测试代码。

接下来再打开 package.json 文件,添加一个 compile script 如下:

json
{
+  "scripts": {
+    "dev:cjs": "node src/cjs/index.cjs",
+    "dev:esm": "node src/esm/index.mjs",
+    "compile": "babel src/babel --out-dir compiled",
+    "serve": "node server/index.js"
+  }
+}

这条命令的含义是:使用 Babel 处理 src/babel 目录下的文件,并输出到根目录下的 compiled 文件夹。

在命令行运行以下命令:

bash
npm run compile

可以看到 hello-node 的根目录下多了一个 compiled 文件夹,里面有一个和源码相同命名的 index.js 文件,它的文件内容如下:

js
// compiled/index.js
+function _classCallCheck(instance, Constructor) {
+  if (!(instance instanceof Constructor)) {
+    throw new TypeError('Cannot call a class as a function')
+  }
+}
+
+function _defineProperties(target, props) {
+  for (var i = 0; i < props.length; i++) {
+    var descriptor = props[i]
+    descriptor.enumerable = descriptor.enumerable || false
+    descriptor.configurable = true
+    if ('value' in descriptor) descriptor.writable = true
+    Object.defineProperty(target, descriptor.key, descriptor)
+  }
+}
+
+function _createClass(Constructor, protoProps, staticProps) {
+  if (protoProps) _defineProperties(Constructor.prototype, protoProps)
+  if (staticProps) _defineProperties(Constructor, staticProps)
+  Object.defineProperty(Constructor, 'prototype', { writable: false })
+  return Constructor
+}
+
+export var Hello = /*#__PURE__*/ (function () {
+  function Hello(name) {
+    _classCallCheck(this, Hello)
+
+    this.name = name
+  }
+
+  _createClass(Hello, [
+    {
+      key: 'say',
+      value: function say() {
+        return \`Hello \${this.name}\`
+      },
+    },
+  ])
+
+  return Hello
+})()

由于 Chrome 41 版本不支持 class 语法,因此 Babel 做了大量的工作对其进行转换兼容。

再次打开 babel.config.json ,将 targets.chrome 的版本号调整为支持 class 语法的 Chrome 49 版本:

diff
{
+  "presets": [
+    [
+      "@babel/preset-env",
+      {
+        "targets": {
+-          "chrome": "41"
++          "chrome": "49"
+        },
+        "modules": false,
+        "useBuiltIns": "usage",
+        "corejs": "3.6.5"
+      }
+    ]
+  ]
+}

再次执行编译,这一次编译后的代码和编译前完全一样:

js
// compiled/index.js
+export class Hello {
+  constructor(name) {
+    this.name = name
+  }
+
+  say() {
+    return \`Hello \${this.name}\`
+  }
+}

因为此时配置文件指定的目标浏览器版本已支持该语法,无需转换。

Babel 的使用其实非常简单,了解了这部分知识点之后,如果某一天需要自己控制代码的兼容性,只需要配合官方文档调整 Babel 的配置,处理起来就得心应手了!

`,35);function A(d,u,h,m,g,E){const o=p("ImgWrap"),a=p("ClientOnly"),e=p("GitalkComment");return c(),r("div",null,[D,s(a,null,{default:n(()=>[s(o,{src:"/assets/img/esm-run.jpg",alt:"esm.run 网站上的包转换操作界面"})]),_:1}),F,s(a,null,{default:n(()=>[s(o,{src:"/assets/img/components.png",alt:"把页面拆分成多个组件,降低维护成本(摘自 Vue 官网)"})]),_:1}),i,s(a,null,{default:n(()=>[s(o,{src:"/assets/img/caniuse-es6-classes.jpg",alt:"在 caniuse 网站上查询 ES6 `class` 的兼容情况"})]),_:1}),C,s(a,null,{default:n(()=>[s(e,{issueId:163})]),_:1})])}const f=t(y,[["render",A]]);export{q as __pageData,f as default}; diff --git a/assets/guide.md.c60fac0b.lean.js b/assets/guide.md.c60fac0b.lean.js new file mode 100644 index 00000000..d259ad8f --- /dev/null +++ b/assets/guide.md.c60fac0b.lean.js @@ -0,0 +1,650 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as t,v as c,b as r,t as s,O as n,R as l,M as p}from"./chunks/framework.0d8bea05.js";const q=JSON.parse('{"title":"工程化的前期准备","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"guide.md","filePath":"guide.md"}'),y={name:"guide.md"},D=l(`

工程化的前期准备

对于刚刚迈入前端工程化、或者还没有接触过前端工程化的开发者,从传统的用 HTML + CSS + JS 手写页面的认知阶段走到工程化的世界,会面对翻天覆地的变化,需要先学习一些入门准备知识。

这一章会介绍一些前置的知识点科普,方便开始学习 Vue3 的时候,不会对一些基本的认知和操作存在太多疑惑。

TIP

本指南需要具备一定的 HTML 、 CSS 和 JavaScript 基础,如果完全不懂,请先对这三个知识点进行一些入门的学习。

命令行工具

在前端工程化开发过程中,已经离不开各种命令行操作,例如:管理项目依赖、本地服务启动、打包构建,还有拉取代码 / 提交代码这些 Git 操作等等。

命令行界面( Command-line Interface ,缩写 CLI ),是一种通过命令行来实现人机交互的工具,需要提前准备好命令行界面工具。

如果有所留意,会发现很多工具都可以实现命令行操作,比如:命令行界面( CLI )、终端( Terminal )、 Shell 、控制台( Console )等等。

从完整功能看,它们之间确实有许多区别,不过对于前端开发者来说,日常的命令行交互需要用到的功能不会特别多,所以后面会统一一些名词,减少理解上的偏差。

交互行为统一代替名词代替名词解释
输入命令行需要输入命令的时候,会统一用 “命令行” 来指代。
输出控制台鉴于前端开发者更多接触的是浏览器的 Console 控制台,
所以也是会用 “控制台” 来指代。

Windows

在 Windows 平台,可以使用自带的 CMD 或者 Windows PowerShell 工具。

但为了更好的开发体验,推荐使用以下工具(需要下载安装),可以根据自己的喜好选择其一:

名称简介下载
Windows Terminal由微软推出的强大且高效的 Windows 终端前往 GitHub 下载
CMDer一款体验非常好的 Windows 控制台模拟器前往 GitHub 下载

笔者在 Windows 台式机上是使用 Windows Terminal 比较多,在此之前是用 CMDer ,两者的设计和体验都非常优秀,当然,还有颜值。

macOS

如果使用的是 Mac 系统,可以直接使用系统自带的 “终端” 工具,笔者在 MacBook 上是使用自带的终端进行开发。

TIP

其实只要能正常使用命令行,对于前端工程师来说就可以满足日常需求,但选择更喜欢的工具,可以让自己的开发过程更为身心愉悦!

安装 Node.js 环境

安装好命令行工具之后,来安装 Node 的开发环境。

下载和安装 Node

在 Node.js 官网提供了安装包的下载,不论是使用 Windows 系统还是 MacOS 系统, Node 都提供了对应的安装包,直接下载安装包并运行即可安装到的电脑里,就可以用来开发的项目了。

点击访问:Node.js 官网下载

安装后,打开的 命令行工具 ,输入以下命令即可查看是否安装成功:

bash
node -v

如果已成功安装,会在控制台输出当前的 Node 版本号。

版本之间的区别

可以看到官网标注了 LTS 和 Current 两个系列,并且对应了不同的版本号。

Current 版本

Current 是最新发布版本,或者叫 “尝鲜版” ,可以在这个系列体验到最新的功能,但也可能会有一些意想不到的问题和兼容性要处理。

每六个月会发布一次 Current 大版本,新的偶数版本( e.g. v16.x.x )会在每年的 4 月份发布,奇数版本( e.g. v17.x.x )会在每年的 10 月份发布。

也就是说,所有版本都会有 Current 版本阶段,这个阶段会持续 6 个月的时间,期间会被活跃的维护和变更,在发布满 6 个月后,奇偶数版本会有不同的结果:

  • 大版本号是奇数的,将变为不支持状态,不会进入 LTS 版本。
  • 大版本号是偶数的,会按照发布节点进入 LTS ,并且作为活跃状态投入使用。

TIP

除非是狂热的 Node 开发探索者,否则不应该选择 Current 系列(特别是在生产环境),应该选择未被 EOL 的 LTS 系列作为的项目运行环境,详见下方的 LTS 版本 说明。

LTS 版本

LTS ,全称 Long Time Support ,长期维护版本,这个系列代表着稳定,建议首次下载以及后续的每次升级都选择 LTS 版本,减少开发过程中的未知问题出现。

每个 LTS 版本的大版本号都是偶数,并且会有 3 个阶段的生命周期:

生命周期含义说明
Active活跃阶段每个从 Current 进入 LTS 的偶数版本,都会有 18 个月的时间被积极维护和升级。
Maintenance维护阶段活跃阶段达到 18 个月后,会进入为期 12 个月的维护阶段,期间只会进行错误修复和安全补丁。
End of Life结束阶段简称 EOL ,在维护阶段达到期限之后,该版本进入 EOL 阶段,将不再维护,也就是说,每个 LTS 版本最长会有 30 个月的维护时间,之后将不再进行维护。

TIP

当然也会有一些例外情况,例如 Node.js 16 版本,为了配合 OpenSSL 1.1.1 的 EOL 时间,将提前 7 个月进入 EOL 阶段。

详见官方公告: Bringing forward the End-of-Life Date for Node.js 16

是否需要经常更新版本

不论是 LTS 还是 Current ,每个系列下面都还有不同的大版本和小版本,是不是每次都必须及时更新到最新版呢?

当然不是,完全可以依照的项目技术栈依赖的最低 Node 版本去决定是否需要升级,不过如果条件允许,还是建议至少要把大版本升级到最新的 LTS 版本。

TIP

关于 Node.js 的版本发布时间表可以在官方 GitHub 的 Release 仓库 查看。

基础的 Node 项目

在安装和配置完 Node.js 之后,接下来了解 Node 项目的一些基础组成,这有助于开启前端工程化开发大门。

TIP

当前文档所演示的 hello-node 项目已托管至 learning-vue3/hello-node 仓库,可使用 Git 克隆命令拉取至本地:

bash
# 从 GitHub 克隆
+git clone https://github.com/learning-vue3/hello-node.git
+
+# 如果 GitHub 访问失败,可以从 Gitee 克隆
+git clone https://gitee.com/learning-vue3/hello-node.git

成品项目可作为学习过程中的代码参考,但更建议按照教程的讲解步骤,从零开始亲手搭建一个新项目并完成 node 开发的体验,可以更有效的提升学习效果。

初始化一个项目

如果想让一个项目成为 Node 项目,只需要在命令行 cd 到项目所在的目录,执行初始化命令:

bash
npm init

之后命令行会输出一些提示,以及一些问题,可以根据的实际情况填写项目信息,例如:

bash
package name: (demo) hello-node

以上面这个问题为例:

冒号左边的 package name 是问题的题干,会询问要输入什么内容。

冒号右边的括号内容 (demo) 是 Node 为推荐的答案(不一定会出现这个推荐值),如果觉得 OK ,可以直接按回车确认,进入下一道题。

冒号右边的 hello-node 是输入的答案(如果选择了推荐的答案,则这里为空),这个答案会写入到项目信息文件里。

当回答完所有问题之后,会把填写的信息输出到控制台,确认无误后,回车完成初始化的工作。

bash
{
+  "name": "hello-node",
+  "version": "1.0.0",
+  "description": "A demo about Node.js.",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \\"Error: no test specified\\" && exit 1"
+  },
+  "author": "chengpeiquan",
+  "license": "MIT"
+}
+
+
+Is this OK? (yes)

如果觉得问题太多,太繁琐了,可以直接加上 -y 参数,这样会以 Node 推荐的答案帮快速生成项目信息。

bash
npm init -y

了解 package.json

在完成 项目的初始化 之后,会发现在项目的根目录下出现了一个名为 package.json 的 JSON 文件。

这是 Node 项目的清单,里面记录了这个项目的基础信息、依赖信息、开发过程的脚本行为、发布相关的信息等等,未来将在很多项目里看到它的身影。

TIP

它必须是 JSON 文件,不可以是存储了 JavaScript 对象字面量的 JS 文件。

如果是按照上面初始化一节的操作得到的这个文件,打开它之后,会发现里面存储了在初始化过程中,根据问题确认下来的那些答案,例如:

json
{
+  "name": "hello-node",
+  "version": "1.0.0",
+  "description": "A demo about Node.js.",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \\"Error: no test specified\\" && exit 1"
+  },
+  "author": "chengpeiquan",
+  "license": "MIT"
+}

package.json 的字段并非全部必填,唯一的要求就是,必须是一个 JSON 文件,所以也可以仅仅写入以下内容:

json
{}

但在实际的项目中,往往需要填写更完善的项目信息,除了手动维护这些信息之外,在安装 npm 包等操作时, Node 也会帮写入数据到这个文件里,来了解一些常用字段的含义:

字段名含义
name项目名称,如果打算发布成 npm 包,它将作为包的名称
version项目版本号,如果打算发布成 npm 包,这个字段是必须的,遵循 语义化版本号 的要求
description项目的描述
keywords关键词,用于在 npm 网站上进行搜索
homepage项目的官网 URL
main项目的入口文件
scripts指定运行脚本的命令缩写,常见的如 npm run build 等命令就在这里配置,详见 脚本命令的配置
author作者信息
license许可证信息,可以选择适当的许可证进行开源
dependencies记录当前项目的生产依赖,安装 npm 包时会自动生成,详见:依赖包和插件
devDependencies记录当前项目的开发依赖,安装 npm 包时会自动生成,详见:依赖包和插件
type配置 Node 对 CJS 和 ESM 的支持

其中最后的 type 字段是涉及到模块规范的支持,它有两个可选值: commonjsmodule ,其默认值为 commonjs

  • 当不设置或者设置为 commonjs 时,扩展名为 .js.cjs 的文件都是 CommonJS 规范的模块,如果要使用 ES Module 规范,需要使用 .mjs 扩展名
  • 当设置为 module 时,扩展名为 .js.mjs 的文件都是 ES Module 规范的模块,如果要使用 CommonJS 规范,需要使用 .cjs 扩展名

关于模块规范可以在 学习模块化设计 一节了解更多。

关于 package.json 的完整的选项可以在 npm Docs 上查阅。

项目名称规则

如果打算发布成 npm 包,它将作为包的名称,可以是普通包名,也可以是范围包的包名。

类型释义例子
范围包具备 @scope/project-name 格式,一般有一系列相关的开发依赖之间会以相同的 scope 进行命名@vue/cli@vue/cli-service 就是一系列相关的范围包
普通包其他命名都属于普通包vuevue-router

包名有一定的书写规则:

  • 名称必须保持在 1 ~ 214 个字符之间(包括范围包的 @scope/ 部分)
  • 只允许使用小写字母、下划线、短横线、数字、小数点(并且只有范围包可以以点或下划线开头)
  • 包名最终成为 URL 、命令行参数或者文件夹名称的一部分,所以名称不能包含任何非 URL 安全字符

TIP

了解这一点有助于在后续工作中,在需要查找技术栈相关包的时候,可以知道如何在 npmjs 上找到它们。

如果打算发布 npm 包,可以通过 npm view <package-name> 命令查询包名是否已存在,如果存在就会返回该包的相关信息。

比如查询 vue 这个包名,会返回它的版本号、许可证、描述等信息:

bash
npm view vue
+
+vue@3.2.33 | MIT | deps: 5 | versions: 372
+The progressive JavaScript framework for building modern web UI.
+https://github.com/vuejs/core/tree/main/packages/vue#readme
+
+keywords: vue
+
+# 后面太多信息这里就省略...

如果查询一个不存在的包名,则会返回 404 信息:

bash
npm view vue123456
+npm ERR! code E404
+npm ERR! 404 Not Found - GET https://registry.npmjs.org/vue123456 - Not found
+npm ERR! 404
+npm ERR! 404  'vue123456@latest' is not in this registry.
+npm ERR! 404 You should bug the author to publish it (or use the name yourself!)
+npm ERR! 404
+npm ERR! 404 Note that you can also install from a
+npm ERR! 404 tarball, folder, http url, or git url.
+
+# 后面太多信息这里就省略...

语义化版本号管理

Node 项目遵循 语义化版本号 的规则,例如 1.0.01.0.11.1.0 这样的版本号,本教材的主角 Vue 也是遵循了语义化版本号的发布规则。

建议开发者在入门前端工程化的时候就应该熟悉这套规则,后续的项目开发中,会使用到很多外部依赖,它们也是使用版本号控制来管理代码的发布,每个版本之间可能会有一些兼容性问题,如果不了解版本号的通用规则,很容易在的开发中带来困扰。

TIP

现在有很多 CI/CD 流水线作业具备了根据 Git 的 Commit 记录来自动升级版本号,它们也是遵循了语义化版本号规则,版本号的语义化在前端工程里有重大的意义。

基本格式与升级规则

版本号的格式为: Major.Minor.Patch (简称 X.Y.Z ),它们的含义和升级规则如下:

英文中文含义
Major主版本号当项目作了大量的变更,与旧版本存在一定的不兼容问题
Minor次版本号做了向下兼容的功能改动或者少量功能更新
Patch修订号修复上一个版本的少量 BUG

一般情况下,三者均为正整数,并且从 0 开始,遵循这三条注意事项:

  • 当主版本号升级时,次版本号和修订号归零
  • 当次版本号升级时,修订号归零,主版本号保持不变
  • 当修订号升级时,主版本号和次版本号保持不变

下面以一些常见的例子帮助快速理解版本号的升级规则:

  • 如果不打算发布,可以默认为 0.0.0 ,代表它并不是一个进入发布状态的包
  • 在正式发布之前,可以将其设置为 0.1.0 发布第一个测试版本,自此,代表已进入发布状态,但还处于初期开发阶段,这个阶段可能经常改变 API ,但不需要频繁的更新主版本号
  • 0.1.0 发布后,修复了 BUG ,下一个版本号将设置为 0.1.1 ,即更新了一个修订号
  • 0.1.1 发布后,有新的功能发布,下一个版本号可以升级为 0.2.0 ,即更新了一个次版本号
  • 当觉得这个项目已经功能稳定、没有什么 BUG 了,决定正式发布并给用户使用时,那么就可以进入了 1.0.0 正式版了

版本标识符

以上是一些常规的版本号升级规则,也可以通过添加 “标识符” 来修饰的版本更新:

格式为: Major.Minor.Patch-Identifier.1 ,其中的 Identifier 代表 “标识符” ,它和版本号之间使用 - 短横线来连接,后面的 .1 代表当前标识符的第几个版本,每发布一次,这个数字 +1 。

标识符含义
alpha内部版本,代表当前可能有很大的变动
beta测试版本,代表版本已开始稳定,但可能会有比较多的问题需要测试和修复
rc即将作为正式版本发布,只需做最后的验证即可发布正式版

脚本命令的配置

在工作中,会频繁接触到 npm run dev 启动开发环境、 npm run build 构建打包等操作,这些操作其实是对命令行的一种别名。

它在 package.json 里是存放于 scripts 字段,以 [key: string]: string 为格式的键值对存放数据( key: value )。

json
{
+  "scripts": {
+    // ...
+  }
+}

其中:

  • key 是命令的缩写,也就是 npm run xxx 里的 xxx ,如果一个单词不足以表达,可以用冒号 : 拼接多个单词,例如 mock:listmock:detail 等等

  • value 是完整的执行命令内容,多个命令操作用 && 连接,例如 git add . && git commit

以 Vue CLI 创建的项目为例,它的项目 package.json 文件里就会包括了这样的命令:

json
{
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build"
+  }
+}

这里的名字是可以自定义的,比如可以把 serve 改成更喜欢的 dev

json
{
+  "scripts": {
+    "dev": "vue-cli-service serve",
+    "build": "vue-cli-service build"
+  }
+}

这样运行 npm run dev 也可以相当于运行了 vue-cli-service serve

据笔者所了解,有不少开发者曾经对不同的 Vue CLI 版本提供的 npm run servenpm run dev 有什么区别有过疑问,看到这里应该都明白了吧,可以说没有区别,因为这取决于它对应的命令,而不是取决于它起什么名称。

TIP

如果 value 部分包含了双引号 " ,必须使用转义符 \\ 来避免格式问题,例如: \\"

可以阅读 npm 关于 scripts 的 完整文档 了解更多用法。

Hello Node

看到这里,对于 Node 项目的基本创建流程和关键信息都有所了解了吧!来写一个 demo ,实际体验一下如何从初始化项目到打印一个 Hello World 到控制台的过程。

请先启动的命令行工具,然后创建一个项目文件夹,这里使用 mkdir 命令:

bash
# 语法是 mkdir <dir-name>
+mkdir hello-node

使用 cd 命令进入刚刚创建好的项目目录:

bash
# 语法是 cd <dir-path>
+cd hello-node

执行项目初始化,可以回答问题,也可以添加 -y 参数来使用默认配置:

bash
npm init -y

来到这里就得到了一个具有 package.json 的 Node 项目了。

在项目下创建一个 index.js 的 JS 文件,可以像平时一样书写 JavaScript ,输入以下内容并保存:

js
console.log('Hello World')

然后打开 package.json 文件,修改 scripts 部分如下,也就是配置了一个 "dev": "node index" 命令:

json
{
+  "name": "hello-node",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "dev": "node index"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC"
+}

在命令行执行 npm run dev ,可以看到控制台打印出了 Hello World

bash
npm run dev
+
+> demo@1.0.0 dev
+> node index
+
+Hello World

这等价于直接在命令行执行 node index.js 命令,其中 node 是 Node.js 运行文件的命令, index 是文件名,相当于 index.js ,因为 JS 文件名后缀可以省略。

学习模块化设计

在了解 Node 项目之后,就要开始通过编码来加强对 Node.js 的熟悉程度了,但在开始使用之前,还需要了解一些概念。

在未来的日子里(不限于本教程,与前端工程化相关的工作内容息息相关),会频繁的接触到两个词:模块( Module )和包( Package )。

模块和包是 Node 开发最重要的组成部分,不管是全部自己实现一个项目,还是依赖各种第三方轮子来协助开发,项目的构成都离不开这两者。

模块化解决了什么问题

在软件工程的设计原则里,有一个原则叫 “单一职责” 。

假设一个代码块负责了多个职责的功能支持,在后续的迭代过程中,维护成本会极大的增加,虽然只需要修改这个代码块,但需要兼顾职责 1 、职责 2 、职责 3 … 等多个职责的兼容性,稍不注意就会引起工程运行的崩溃。

“单一职责” 的目的就是减少功能维护带来的风险,把代码块的职责单一化,让代码的可维护性更高。

一个完整业务的内部实现,不应该把各种代码都耦合在一起,而应该按照职责去划分好代码块,再进行组合,形成一个 “高内聚,低耦合” 的工程设计。

模块化就是由此而来,在前端工程里,每个单一职责的代码块,就叫做模块( Module ) ,模块有自己的作用域,功能与业务解耦,非常方便复用和移植。

TIP

模块化还可以解决本章开头所讲述的 传统开发的弊端 里提到的大部分问题,随着下面内容一步步深入,将一步步的理解它。

如何实现模块化

在前端工程的发展过程中,不同时期诞生了很多不同的模块化机制,最为主流的有以下几种:

模块化方案全称适用范围
CJSCommonJSNode 端
AMDAsync Module Definition浏览器
CMDCommon Module Definition浏览器
UMDUniversal Module DefinitionNode 端和浏览器
ESMES ModuleNode 端和浏览器

其中 AMD 、CMD 、 UMD 都已经属于偏过去式的模块化方案,在新的业务里,结合各种编译工具,可以直接用最新的 ESM 方案来实现模块化,所以可以在后续有接触的时候再了解。

ESM ( ES Module ) 是 JavaScript 在 ES6( ECMAScript 2015 )版本推出的模块化标准,旨在成为浏览器和服务端通用的模块解决方案。

CJS ( CommonJS ) 原本是服务端的模块化标准(设计之初也叫 ServerJS ),是为 JavaScript 设计的用于浏览器之外的一个模块化方案, Node 默认支持了该规范,在 Node 12 之前也只支持 CJS ,但从 Node 12 开始,已经同时支持 ES Module 的使用。

至此,不论是 Node 端还是浏览器端, ES Module 是统一的模块化标准了!

但由于历史原因, CJS 在 Node 端依然是非常主流的模块化写法,所以还是值得进行了解,因此下面的内容将主要介绍 CJS 和 ESM 这两种模块化规范是如何实际运用。

TIP

在开始体验模块化的编写之前,请先在电脑里 安装好 Node.js ,然后打开 命令行工具 ,通过 cd 命令进入平时管理项目的目录路径, 初始化一个 Node 项目

另外,在 CJS 和 ESM ,一个独立的文件就是一个模块,该文件内部的变量必须通过导出才能被外部访问到,而外部文件想访问这些变量,需要导入对应的模块才能生效。

用 CommonJS 设计模块

虽然现在推荐使用 ES Module 作为模块化标准,但是日后在实际工作的过程中,还是不免会遇到要维护一些老项目,因此了解 CommonJS 还是非常有必要的。

以下简称 CJS 代指 CommonJS 规范。

准备工作

延续在 Hello Node 部分创建的 Node.js demo 项目,先调整一下目录结构:

  1. 删掉 index.js 文件
  2. 创建一个 src 文件夹,在里面再创建一个 cjs 文件夹
  3. cjs 文件夹里面创建两个文件: index.cjsmodule.cjs

TIP

请注意这里使用了 .cjs 文件扩展名,其实它也是 JS 文件,但这个扩展名是 Node 专门为 CommonJS 规范设计的,可以在 了解 package.json 部分的内容了解更多。

此时目录结构应该如下:

bash
hello-node
+ # 源码文件夹
+├─src
+  # 业务文件夹
+ └─cjs
+    # 入口文件
+   ├─index.cjs
+    # 模块文件
+   └─module.cjs
+ # 项目清单
+└─package.json

这是一个常见的 Node 项目目录结构,通常源代码都会放在 src 文件夹里面统一管理。

接下来再修改一下 package.json 里面的 scripts 部分,改成如下:

json
{
+  "scripts": {
+    "dev:cjs": "node src/cjs/index.cjs"
+  }
+}

后面在命令行执行 npm run dev:cjs 命令,就可以测试刚刚添加的 CJS 模块了。

基本语法

CJS 使用 module.exports 语法导出模块,可以导出任意合法的 JavaScript 类型,例如:字符串、布尔值、对象、数组、函数等等。

使用 require 导入模块,在导入的时候,当文件扩展名是 .js 时,可以只写文件名,而此时使用的是 .cjs 扩展名,所以需要完整的书写。

默认导出和导入

默认导出的意思是,一个模块只包含一个值;而导入默认值则意味着,导入时声明的变量名就是对应模块的值。

src/cjs/module.cjs 文件里,写入以下代码,导出一句 Hello World 信息:

js
// src/cjs/module.cjs
+module.exports = 'Hello World'

TIP

自己在写入代码的时候,不需要包含文件路径那句注释,这句注释只是为了方便阅读时能够区分代码属于哪个文件,以下代码均如此。

src/cjs/index.cjs 文件里,写入以下代码,导入刚刚编写的模块。

js
// src/cjs/index.cjs
+const m = require('./module.cjs')
+console.log(m)

在命令行输入 npm run dev:cjs ,可以看到成功输出了 Hello World 信息:

bash
npm run dev:cjs
+
+> demo@1.0.0 dev:cjs
+> node src/cjs/index.cjs
+
+Hello World

可以看到,在导入模块时,声明的 m 变量拿到的值,就是整个模块的内容,可以直接使用,此例子中它是一个字符串。

再改动一下,把 src/cjs/module.cjs 改成如下,这次导出一个函数:

js
// src/cjs/module.cjs
+module.exports = function foo() {
+  console.log('Hello World')
+}

相应的,这次变成了导入一个函数,所以可以执行它:

js
// src/cjs/index.cjs
+const m = require('./module.cjs')
+m()

得到的结果也是打印一句 Hello World ,不同的是,这一次的打印行为是在模块里定义的,入口文件只是执行模块里的函数。

bash
npm run dev:cjs
+
+> demo@1.0.0 dev:cjs
+> node src/cjs/index.cjs
+
+Hello World

命名导出和导入

默认导出的时候,一个模块只包含一个值,有时候如果想把很多相同分类的函数进行模块化集中管理,例如想做一些 utils 类的工具函数文件、或者是维护项目的配置文件,全部使用默认导出的话,会有非常多的文件要维护。

那么就可以用到命名导出,这样既可以导出多个数据,又可以统一在一个文件里维护管理,命名导出是先声明多个变量,然后通过 {} 对象的形式导出。

再来修改一下 src/cjs/module.cjs 文件,这次改成如下:

js
// src/cjs/module.cjs
+function foo() {
+  console.log('Hello World from foo.')
+}
+
+const bar = 'Hello World from bar.'
+
+module.exports = {
+  foo,
+  bar,
+}

这个时候通过原来的方式去拿模块的值,会发现无法直接获取到函数体或者字符串的值,因为打印出来的也是一个对象。

js
// src/cjs/index.cjs
+const m = require('./module.cjs')
+console.log(m)

控制台输出:

bash
npm run dev:cjs
+
+> demo@1.0.0 dev:cjs
+> node src/cjs/index.cjs
+
+{ foo: [Function: foo], bar: 'Hello World from bar.' }

需要通过 m.foo()m.bar 的形式才可以拿到值。

此时可以用一种更方便的方式,利用 ES6 的对象解构来直接拿到变量:

js
// src/cjs/index.cjs
+const { foo, bar } = require('./module.cjs')
+foo()
+console.log(bar)

这样子才可以直接调用变量拿到对应的值。

导入时重命名

以上都是基于非常理想的情况下使用模块,有时候不同的模块之间也会存在相同命名导出的情况,来看看模块化是如何解决这个问题的。

src/cjs/module.cjs 文件保持不变,依然导出这两个变量:

js
// src/cjs/module.cjs
+function foo() {
+  console.log('Hello World from foo.')
+}
+
+const bar = 'Hello World from bar.'
+
+module.exports = {
+  foo,
+  bar,
+}

这次在入口文件里也声明一个 foo 变量,在导入的时候对模块里的 foo 进行了重命名操作。

js
// src/cjs/index.cjs
+const {
+  foo: foo2,  // 这里进行了重命名
+  bar,
+} = require('./module.cjs')
+
+// 就不会造成变量冲突
+const foo = 1
+console.log(foo)
+
+// 用新的命名来调用模块里的方法
+foo2()
+
+// 这个不冲突就可以不必处理
+console.log(bar)

再次运行 npm run dev:cjs ,可以看到打印出来的结果完全符合预期:

bash
npm run dev:cjs
+
+> demo@1.0.0 dev:cjs
+> node src/cjs/index.cjs
+
+1
+Hello World from foo.
+Hello World from bar.

这是利用了 ES6 解构对象的 给新的变量名赋值 技巧。

以上是针对命名导出时的重命名方案,如果是默认导出,那么在导入的时候用一个不冲突的变量名来声明就可以了。

用 ES Module 设计模块

ES Module 是新一代的模块化标准,它是在 ES6( ECMAScript 2015 )版本推出的,是原生 JavaScript 的一部分。

不过因为历史原因,如果要直接在浏览器里使用该方案,在不同的浏览器里会有一定的兼容问题,需要通过 Babel 等方案进行代码的版本转换(可在 控制编译代码的兼容性 一节了解如何使用 Babel )。

因此一般情况下都需要借助构建工具进行开发,工具通常会提供开箱即用的本地服务器用于开发调试,并且最终打包的时候还可以抹平不同浏览器之间的差异。

随着 ESM 的流行,很多新推出的构建工具都默认只支持该方案( e.g. Vite 、 Rollup ),如果需要兼容 CJS 反而需要另外引入插件单独配置。除了构建工具,很多语言也是默认支持 ESM ,例如 TypeScript ,因此了解 ESM 非常重要。

以下简称 ESM 代指 ES Module 规范。

TIP

在阅读本小节之前,建议先阅读 用 CommonJS 设计模块 以了解前置内容,本小节会在适当的内容前后与 CJS 的写法进行对比。

准备工作

继续使用在 用 CommonJS 设计模块 时使用的 hello-node 项目作为 demo ,当然也可以重新创建一个新的。

一样的,先调整一下目录结构:

  1. src 文件夹里面创建一个 esm 文件夹
  2. esm 文件夹里面创建两个 MJS 文件: index.mjsmodule.mjs

TIP

注意这里使用了 .mjs 文件扩展名,因为默认情况下, Node 需要使用该扩展名才会支持 ES Module 规范。

也可以在 package.json 里增加一个 "type": "module" 的字段来使 .js 文件支持 ESM ,但对应的,原来使用 CommonJS 规范的文件需要从 .js 扩展名改为 .cjs 才可以继续使用 CJS 。

为了减少理解上的门槛,这里选择了使用 .mjs 新扩展名便于入门,可以在 了解 package.json 部分的内容了解更多。

此时目录结构应该如下:

bash
hello-node
+ # 源码文件夹
+├─src
+  # 上次用来测试 CommonJS 的相关文件
+ ├─cjs
+  ├─index.cjs
+  └─module.cjs
+ 
+  # 这次要用的 ES Module 测试文件
+ └─esm
+    # 入口文件
+   ├─index.mjs
+    # 模块文件
+   └─module.mjs
+
+ # 项目清单
+└─package.json

同样的,源代码放在 src 文件夹里面管理。

然后再修改一下 package.json 里面的 scripts 部分,参照上次配置 CJS 的格式,增加一个 ESM 版本的 script ,改成如下:

json
{
+  "scripts": {
+    "dev:cjs": "node src/cjs/index.cjs",
+    "dev:esm": "node src/esm/index.mjs"
+  }
+}

后面在命令行执行 npm run dev:esm 就可以测试的 ESM 模块了。

TIP

注意, script 里的 .mjs 扩展名不能省略。

另外,在实际项目中,可能不需要做这些处理,因为很多工作脚手架已经帮处理过了,比如的 Vue3 项目。

基本语法

ESM 使用 export default (默认导出)和 export (命名导出)这两个语法导出模块,和 CJS 一样, ESM 也可以导出任意合法的 JavaScript 类型,例如:字符串、布尔值、对象、数组、函数等等。

使用 import ... from ... 导入模块,在导入的时候,如果文件扩展名是 .js 则可以省略文件名后缀,否则需要把扩展名也完整写出来。

默认导出和导入

ESM 的默认导出也是一个模块只包含一个值,导入时声明的变量名,它对应的数据就是对应模块的值。

src/esm/module.mjs 文件里,写入以下代码,导出一句 Hello World 信息:

js
// src/esm/module.mjs
+export default 'Hello World'

src/esm/index.mjs 文件里,写入以下代码,导入刚刚编写的模块。

js
// src/esm/index.mjs
+import m from './module.mjs'
+console.log(m)

在命令行输入 npm run dev:esm ,可以看到成功输出了 Hello World 信息:

bash
npm run dev:esm
+
+> demo@1.0.0 dev:esm
+> node src/esm/index.mjs
+
+Hello World

可以看到,在导入模块时,声明的 m 变量拿到的值,就是整个模块的内容,可以直接使用,此例子中它是一个字符串。

像在 CJS 的例子里一样,也来再改动一下,把 src/esm/module.mjs 改成导出一个函数:

js
// src/esm/module.mjs
+export default function foo() {
+  console.log('Hello World')
+}

同样的,这次也是变成了导入一个函数,可以执行它:

js
// src/esm/index.mjs
+import m from './module.mjs'
+m()

一样可以从模块里的函数得到一句 Hello World 的打印信息。

bash
npm run dev:esm
+
+> demo@1.0.0 dev:esm
+> node src/esm/index.mjs
+
+Hello World

TIP

可以看到, CJS 和 ESM 的默认导出是非常相似的,在未来如果有老项目需要从 CJS 往 ESM 迁移,大部分情况下只需要把 module.exports 改成 export default 即可。

命名导出和导入

虽然默认导出的时候, CJS 和 ESM 的写法非常相似,但命名导出却完全不同!

在 CJS 里,使用命名导出后的模块数据默认是一个对象,可以导入模块后通过 m.foo 这样的方式去调用对象的属性,或者在导入的时候直接解构拿到对象上的某个属性:

js
// CJS 支持导入的时候直接解构
+const { foo } = require('./module.cjs')

但 ES Module 的默认导出不能这样做,例如下面这个例子,虽然默认导出了一个对象:

js
// 在 ESM ,通过这样导出的数据也是属于默认导出
+export default {
+  foo: 1,
+}

但是无法和 CJS 一样通过大括号的方式导入其中的某个属性:

js
// ESM 无法通过这种方式对默认导出的数据进行 “解构”
+import { foo } from './module.mjs'

这样操作在运行过程中,控制台会抛出错误信息:

bash
import { foo } from './module.mjs'
+         ^^^
+SyntaxError:
+The requested module './module.mjs' does not provide an export named 'foo'

正确的方式应该是通过 export 对数据进行命名导出,先将 src/esm/module.mjs 文件修改成如下代码,请留意 export 关键字的使用:

js
// src/esm/module.mjs
+export function foo() {
+  console.log('Hello World from foo.')
+}
+
+export const bar = 'Hello World from bar.'

通过 export 命名导出的方式,现在才可以使用大括号将它们进行命名导入:

js
// src/esm/index.mjs
+import { foo, bar } from './module.mjs'
+
+foo()
+console.log(bar)

这一次程序可以顺利运行了:

bash
npm run dev:esm
+
+> demo@1.0.0 dev:esm
+> node src/esm/index.mjs
+
+Hello World from foo.
+Hello World from bar.

那么有没有办法像 CJS 一样使用 m.foo 调用对象属性的方式一样,去使用这些命名导出的模块呢?

答案是肯定的!命名导出支持使用 * as 变量名称 的方式将其所有命名挂在某个变量上,该变量是一个对象,每一个导出的命名都是其属性:

ts
// src/esm/index.mjs
+// 注意这里使用了另外一种方式,将所有的命名导出都挂在了 \`m\` 变量上
+import * as m from './module.mjs'
+
+console.log(typeof m)
+console.log(Object.keys(m))
+
+m.foo()
+console.log(m.bar)

运行 npm run dev:esm ,将输出:

bash
npm run dev:esm
+
+> demo@1.0.0 dev:esm
+> node src/esm/index.mjs
+
+object
+[ 'bar', 'foo' ]
+Hello World from foo.
+Hello World from bar.

导入时重命名

接下来看看 ESM 是如何处理相同命名导出的问题,项目下的模块文件依然保持不变,还是导出两个变量:

js
// src/esm/module.mjs
+export function foo() {
+  console.log('Hello World from foo.')
+}
+
+export const bar = 'Hello World from bar.'

入口文件里面,也声明一个 foo 变量,然后导入的时候对模块里的 foo 进行重命名操作:

js
// src/esm/index.mjs
+import {
+  foo as foo2,  // 这里进行了重命名
+  bar
+} from './module.mjs'
+
+// 就不会造成变量冲突
+const foo = 1
+console.log(foo)
+
+// 用新的命名来调用模块里的方法
+foo2()
+
+// 这个不冲突就可以不必处理
+console.log(bar)

可以看到,在 ESM 的重命名方式和 CJS 是完全不同的,它是使用 as 关键字来操作,语法为 <old-name> as <new-name>

现在再次运行 npm run dev:esm ,可以看到打印出来的结果也是完全符合预期了:

bash
npm run dev:esm
+
+> demo@1.0.0 dev:esm
+> node src/esm/index.mjs
+
+1
+Hello World from foo.
+Hello World from bar.

以上是针对命名导出时的重命名方案,如果是默认导出,和 CJS 一样,在导入的时候用一个不冲突的变量名来声明就可以了。

在浏览器里访问 ESM

ES Module 除了支持在 Node 环境使用,还可以和普通的 JavaScript 代码一样在浏览器里运行。

要在浏览器里体验 ESM ,需要使用现代的主流浏览器(如 Chrome ),并注意其访问限制,例如本地开发不能直接通过 file:// 协议在浏览器里访问本地 HTML 内引用的 JS 文件,这是因为浏览器对 JavaScript 的安全性要求,会触发 CORS 错误,因此需要启动本地服务并通过 http:// 协议访问。

TIP

CORS (全称 Cross-Origin Resource Sharing )是指跨源资源共享,可以决定浏览器是否需要阻止 JavaScript 获取跨域请求的响应。

现代浏览器默认使用 “同源安全策略” ,这里的 “源” 指 URL 的 origin 部分,例如网页可以通过 window.location.origin 获取到如 https://example.com 这样格式的数据,就是网页的 origin

默认情况下,非同源的请求会被浏览器拦截,最常见的场景是通过 XHR 或者 Fetch 请求 API 接口,需要网页和接口都部署在同一个域名才可以请求成功,否则就会触发跨域限制。

如果网页和接口不在同一个域名,例如网页部署在 https://web.example.com ,接口部署在 https://api.example.com ,此时需要在 https://api.example.com 的 API 服务端程序里,配置 Access-Control-Allow-Origin: * 允许跨域请求( * 代表允许任意外域访问,也可以指定具体的域名作为白名单列表)。

添加服务端程序

接下来搭建一个简单的本地服务,并通过 HTML 文件来引入 ESM 模块文件,体验浏览器端如何使用 ESM 模块。

在 hello-node 项目的根目录下创建名为 server 的文件夹(与 src 目录同级),并添加 index.js 文件,敲入以下代码:

js
// server/index.js
+const { readFileSync } = require('fs')
+const { resolve } = require('path')
+const { createServer } = require('http')
+
+/**
+ * 判断是否 ESM 文件
+ */
+function isESM(url) {
+  return String(url).endsWith('mjs')
+}
+
+/**
+ * 获取 MIME Type 信息
+ * @tips \`.mjs\` 和 \`.js\` 一样,都使用 JavaScript 的 MIME Type
+ */
+function mimeType(url) {
+  return isESM(url) ? 'application/javascript' : 'text/html'
+}
+
+/**
+ * 获取入口文件
+ * @returns 存放在本地的文件路径
+ */
+function entryFile(url) {
+  const file = isESM(url) ? \`../src/esm\${url}\` : './index.html'
+  return resolve(__dirname, file)
+}
+
+/**
+ * 创建 HTTP 服务
+ */
+const app = createServer((request, response) => {
+  // 获取请求时的相对路径,如网页路径、网页里的 JS 文件路径等
+  const { url } = request
+
+  // 转换成对应的本地文件路径并读取其内容
+  const entry = entryFile(url)
+  const data = readFileSync(entry, 'utf-8')
+
+  // 需要设置正确的响应头信息,浏览器才可以正确响应
+  response.writeHead(200, { 'Content-Type': mimeType(url) })
+  response.end(data)
+})
+
+/**
+ * 在指定的端口号启动本地服务
+ */
+const port = 8080
+app.listen(port, '0.0.0.0', () => {
+  console.log(\`Server running at:\`)
+  console.log()
+  console.log(\`  ➜  Local:  http://localhost:\${port}/\`)
+  console.log()
+})

这是一个基础的 Node.js 服务端程序,利用了 HTTP 模块启动本地服务,期间利用 FS 模块的 I/O 能力对本地文件进行读取,而 PATH 模块则简化了文件操作过程中的路径处理和兼容问题(例如众所周知的 Windows 与 macOS 的路径斜杆问题)。

TIP

在这段服务端程序代码里,请留意 mimeType 方法,要让浏览器能够正确解析 .mjs 文件,需要在服务端响应文件内容时,将其 MIME Type 设置为 和 JavaScript 文件一样,这一点非常重要。

并且需要注意传递给 readFileSync API 的文件路径是否与真实存在的文件路径匹配,如果启动服务时,在 Node 控制台报了 no such file or directory 的错误,请检查是否因为笔误写错了文件名称,或者文件路径多了空格等情况。

添加入口页面

继续在 server 目录下添加一个 index.html 并写入以下 HTML 代码,它将作为网站的首页文件:

TIP

可以在 VSCode 先新建一个空文件,文件语言设置为 HTML ,并写入英文感叹号 ! ,再按 Tab 键(或者鼠标选择第一个代码片段提示),可快速生成基础的 HTML 结构。

html
<!-- server/index.html -->
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>ESM run in browser</title>
+  </head>
+  <body>
+    <script type="module" src="./index.mjs"></script>
+  </body>
+</html>

请注意在 <script /> 标签这一句代码上,比平时多了一个 type="module" 属性,这代表这个 script 是使用了 ESM 模块,而 src 属性则对应指向了上文在 src/esm 目录下的入口文件名。

之所以无需使用 ../src/esm/index.mjs 显式的指向真实目录,是因为在 添加服务端程序 时,已通过服务端代码里的 entryFile 方法重新指向了文件所在的真实路径,所以在 HTML 文件里可以使用 ./ 简化文件路径。

启动服务并访问

打开 package.json 文件,在 scripts 字段追加一个 serve 命令如下:

json
{
+  "scripts": {
+    "dev:cjs": "node src/cjs/index.cjs",
+    "dev:esm": "node src/esm/index.mjs",
+    "serve": "node server/index.js"
+  }
+}

在命令行运行 npm run serve 即可启动本地服务:

bash
 npm run serve
+
+> demo@1.0.0 serve
+> node server/index.js
+
+Server running at:
+
+    Local:  http://localhost:8080/

根据命令行提示,在浏览器访问 http://localhost:8080/ 地址,即可访问本地服务。

TIP

如遭遇端口号冲突,可在 server/index.js 的 const port = 8080 代码处修改为其他端口号。

因为在编写 HTML 文件时没有写入内容,只引入了 ESM 模块文件,因此需要按 F12 唤起浏览器的控制台查看 Log ,可以看到控制台根据模块的文件内容,输出了这三句 Log (如果没有 Log ,可在控制台唤起的情况下按 F5 重新载入页面):

bash
1                                                   index.mjs:8
+Hello World from foo.                               module.mjs:2
+Hello World from bar.                               index.mjs:14

分别来自 src/esm/index.mjs 本身的 console.log 语句,以及 import 进来的 module.mjs 里的 console.log 语句。

如果未能出现这三句 Log ,请留意 .mjs 文件内容是否为上一小节最后的内容:

src/esm/index.mjs 文件内容为:

js
// src/esm/index.mjs
+import {
+  foo as foo2, // 这里进行了重命名
+  bar,
+} from './module.mjs'
+
+// 就不会造成变量冲突
+const foo = 1
+console.log(foo)
+
+// 用新的命名来调用模块里的方法
+foo2()
+
+// 这个不冲突就可以不必处理
+console.log(bar)

src/esm/module.mjs 文件内容为:

js
// src/esm/module.mjs
+export function foo() {
+  console.log('Hello World from foo.')
+}
+
+export const bar = 'Hello World from bar.'
内联的 ESM 代码

到目前为止, server/index.html 文件里始终是通过文件的形式引入 ESM 模块,其实 <script type="module" /> 也支持编写内联代码,和普通的 <script /> 标签用法相同:

html
<script type="module">
+  // ESM 模块的 JavaScript 代码
+</script>

请移除 <script /> 标签的 src 属性,并在标签内写入 src/esm/index.mjs 文件里的代码,现在该 HTML 文件的完整代码如下:

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>ESM run in browser</title>
+  </head>
+  <body>
+    <!-- 标签内的代码就是 src/esm/index.mjs 的代码 -->
+    <script type="module">
+      import {
+        foo as foo2, // 这里进行了重命名
+        bar,
+      } from './module.mjs'
+
+      // 就不会造成变量冲突
+      const foo = 1
+      console.log(foo)
+
+      // 用新的命名来调用模块里的方法
+      foo2()
+
+      // 这个不冲突就可以不必处理
+      console.log(bar)
+    </script>
+  </body>
+</html>

回到浏览器刷新 http://localhost:8080/ ,可以看到浏览器控制台依然输出了和引入 src="./index.mjs" 时一样的 Log 信息:

bash
1                                                   (index):21
+Hello World from foo.                               module.mjs:2
+Hello World from bar.                               (index):27
了解模块导入限制

虽然以上例子可以完美地在浏览器里引用现成的 ESM 模块代码并运行,但不代表工程化项目下所有的 ES Module 模块化方式都适合浏览器。

先做一个小尝试,将 src/esm/index.mjs 文件内容修改如下,导入项目已安装的 md5 工具包:

js
// src/esm/index.mjs
+import md5 from 'md5'
+console.log(md5('Hello World'))

回到浏览器刷新 http://localhost:8080/ ,观察控制台,可以发现出现了一个红色的错误信息:

bash
Uncaught TypeError: Failed to resolve module specifier "md5".
+Relative references must start with either "/", "./", or "../".

这是因为不论是通过 <script type="module" /> 标签还是通过 import 语句导入,模块的路径都必须是以 /./ 或者是 ../ 开头,因此无法直接通过 npm 包名进行导入。

这种情况下需要借助另外一个 script 类型: importmap ,在 server/index.html 里追加 <script type="importmap" /> 这一段代码:

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>ESM run in browser</title>
+  </head>
+  <body>
+    <!-- 注意需要先通过 \`importmap\` 引入 npm 包的 CDN -->
+    <script type="importmap">
+      {
+        "imports": {
+          "md5": "https://esm.run/md5"
+        }
+      }
+    </script>
+
+    <!-- 然后才能在 \`module\` 里 \`import xx from 'xx'\` -->
+    <script type="module" src="./index.mjs"></script>
+  </body>
+</html>

再次刷新页面,可以看到控制台成功输出了 b10a8db164e0754105b7a99be72e3fe5 这个字符串,也就是 Hello World 被 MD5 处理后的结果。

可以看到 importmap 的声明方式和 package.json 的 dependencies 字段非常相似, JSON 的 key 是包名称, value 则是支持 ESM 的远程地址。

TIP

Import Maps 的运行机制是通过 import 映射来控制模块说明符的解析,类似于构建工具常用的 alias 别名机制。

这是一个现代浏览器才能支持的新特性,建议使用 Chrome 最新版本体验完整功能,可以在其 GitHub 仓库 查看更多用法。

上方例子里, md5 对应的远程地址是使用了来自 esm.run 网站的 URL ,而不是 npm 包同步到 jsDelivr CDN 或者 UNPKG CDN 的地址,这是因为 md5 这个包本身不支持 ES Module ,需要通过 esm.run 这个网站进行在线转换才可以在 <script type="module" /> 上使用。

`,323),F=l(`

该网站的服务是 jsDelivr CDN 所属的服务商提供,因此也可以通过 jsDelivr CDN 的 URL 添加 /+esm 参数来达到转换效果,以 md5 包为例:

bash
# 默认是一个 CJS 包
+https://cdn.jsdelivr.net/npm/md5
+
+# 可添加 \`/+esm\` 参数变成 ESM 包
+https://cdn.jsdelivr.net/npm/md5/+esm

总的来说,现阶段在浏览器使用 ES Module 并不是一个很好的选择,建议开发者还是使用构建工具来开发,工具可以抹平这些浏览器差异化问题,降低开发成本。

认识组件化设计

学习完模块化设计之后,未来在 Vue 的工程化开发过程中,还会遇到一个新的概念,那就是 “组件” 。

什么是组件化

模块化属于 JavaScript 的概念,但作为一个页面,都知道它是由 HTML + CSS + JS 三部分组成的,既然 JS 代码可以按照不同的功能、需求划分成模块,那么页面是否也可以呢?

答案是肯定的!组件化就是由此而来。

在前端工程项目里,页面可以理解为一个积木作品,组件则是用来搭建这个作品的一块又一块积木。

`,9),i=l(`

解决了什么问题

模块化属于 JavaScript 的概念,把代码块的职责单一化,一个函数、一个类都可以独立成一个模块。

但这只解决了逻辑部分的问题,一个页面除了逻辑,还有骨架( HTML )和样式( CSS ),组件就是把一些可复用的 HTML 结构和 CSS 样式再做一层抽离,然后再放置到需要展示的位置。

常见的组件有:页头、页脚、导航栏、侧边栏… 甚至小到一个用户头像也可以抽离成组件,因为头像可能只是尺寸、圆角不同而已。

每个组件都有自己的 “作用域” , JavaScript 部分利用 模块化 来实现作用域隔离, HTML 和 CSS 代码则借助 Style Scoped 来生成独有的 hash ,避免全局污染,这些方案组合起来,使得组件与组件之间的代码不会互相影响。

如何实现组件化

在 Vue ,是通过 Single-File Component (简称 SFC , .vue 单文件组件)来实现组件化开发。

一个 Vue 组件是由三部分组成的:

vue
<template>
+  <!-- HTML 代码 -->
+</template>
+
+<script>
+// JavaScript 代码
+</script>
+
+<style scoped>
+/* CSS 代码 */
+</style>

在后面的 单组件的编写 一章中,会详细介绍如何编写一个 Vue 组件。

依赖包和插件

在实际业务中,经常会用到各种各样的插件,插件在 Node 项目里的体现是一个又一个的依赖包。

虽然也可以把插件的代码文件手动放到的源码文件夹里引入,但并不是一个最佳的选择,本节内容将带了解 Node 的依赖包。

什么是包

在 Node 项目里,包可以简单理解为模块的集合,一个包可以只提供一个模块的功能,也可以作为多个模块的集合集中管理。

包通常是发布在官方的包管理平台 npmjs 上面,开发者需要使用的时候,可以通过包管理器安装到项目里,并在的代码里引入,开箱即用(详见: 依赖包的管理 )。

使用 npm 包可以减少在项目中重复造轮子,提高项目的开发效率,也可以极大的缩小项目源码的体积(详见:什么是 node_modules)。

包管理平台官网:https://www.npmjs.com

什么是 node_modules

node_modules 是 Node 项目下用于存放已安装的依赖包的目录,如果不存在,会自动创建。

如果是本地依赖,会存在于项目根目录下,如果是全局依赖,会存在于环境变量关联的路径下,详见下方的管理依赖部分内容的讲解。

TIP

一般在提交项目代码到 Git 仓库或者的服务器上时,都需要排除 node_modules 文件夹的提交,因为它非常大。

如果托管在 Git 仓库,可以在 .gitignore 文件里添加 node_modules 作为要排除的文件夹名称。

什么是包管理器

包管理器( Package Manager )是用来管理依赖包的工具,比如:发布、安装、更新、卸载等等。

Node 默认提供了一个包管理器 npm ,在安装 Node.js 的时候,默认会一起安装 npm 包管理器,可以通过以下命令查看它是否正常。

bash
npm -v

如果正常,将会输出相应的版本号。

依赖包的管理

接下来会以 npm 作为默认的包管理器,来了解如何在项目里管理依赖包。

配置镜像源

在国内,直接使用 npm 会比较慢,可以通过绑定 npm Mirror 中国镜像站 的镜像源来提升依赖包的下载速度。

可以先在命令行输入以下命令查看当前的 npm 配置:

bash
npm config get registry
+# https://registry.npmjs.org/

默认情况下,会输出 npm 官方的资源注册表地址,接下来在命令行上输入以下命令,进行镜像源的绑定:

bash
npm config set registry https://registry.npmmirror.com

可以再次运行查询命令来查看是否设置成功:

bash
npm config get registry
+# https://registry.npmmirror.com/

可以看到已经成功更换为中国镜像站的地址了,之后在安装 npm 包的时候,速度会有很大的提升!

如果需要删除自己配置的镜像源,可以输入以下命令进行移除,移除后会恢复默认设置:

bash
npm config rm registry

TIP

如果之前已经绑定过 npm.taobao 系列域名,也请记得更换成 npmmirror 这个新的域名!

随着新的域名已经正式启用,老 npm.taobao.orgregistry.npm.taobao.org 域名在 2022 年 05 月 31 日零时后不再提供服务。

详见:【望周知】淘宝 npm 镜像站喊你切换新域名啦

本地安装

项目的依赖建议优先选择本地安装,这是因为本地安装可以把依赖列表记录到 package.json 里,多人协作的时候可以减少很多问题出现,特别是当本地依赖与全局依赖版本号不一致的时候。

生产依赖

执行 npm install 的时候,添加 --save 或者 -S 选项可以将依赖安装到本地,并列为生产依赖。

TIP

需要提前在命令行 cd 到的项目目录下再执行安装。

另外, --save 或者 -S 选项在实际使用的时候可以省略,因为它是默认选项。

bash
npm install --save <package-name>

可以在项目的 package.json 文件里的 dependencies 字段查看是否已安装成功,例如:

json
// package.json
+{
+  // 会安装到这里
+  "dependencies": {
+    // 以 "包名":"版本号" 的格式写入
+    "vue-router": "^4.0.14"
+  }
+}

生产依赖包会被安装到项目根目录下的 node_modules 目录里。

项目在上线后仍需用到的包,就需要安装到生产依赖里,比如 Vue 的路由 vue-router 就需要以这个方式安装。

开发依赖

执行 npm install 的时候,如果添加 --save-dev 或者 -D 选项,可以将依赖安装到本地,并写入开发依赖里。

TIP

需要提前在命令行 cd 到的项目目录下再执行安装。

bash
npm install --save-dev <package-name>

可以在项目的 package.json 文件里的 devDependencies 字段查看是否已安装成功,例如:

json
// package.json
+{
+  // 会安装到这里
+  "devDependencies": {
+    // 以 "包名":"版本号" 的格式写入
+    "eslint": "^8.6.0"
+  }
+}

开发依赖包也是会被安装到项目根目录下的 node_modules 目录里。

和生产依赖包不同的点在于,只在开发环境生效,构建部署到生产环境时可能会被抛弃,一些只在开发环境下使用的包,就可以安装到开发依赖里,比如检查代码是否正确的 ESLint 就可以用这个方式安装。

全局安装

执行 npm install 的时候,如果添加 --global 或者 -g 选项,可以将依赖安装到全局,它们将被安装在 配置环境变量 里配置的全局资源路径里。

bash
npm install --global <package-name>

TIP

Mac 用户需要使用 sudo 来提权才可以完成全局安装。

另外,可以通过 npm root -g 查看全局包的安装路径。

一般情况下,类似于 @vue/cli 之类的脚手架会提供全局安装的服务,安装后,就可以使用 vue create xxx 等命令直接创建 Vue 项目了。

但不是每个 npm 包在全局安装后都可以正常使用,请阅读 npm 包的主页介绍和使用说明。

版本控制

有时候一些包的新版本不一定适合的老项目,因此 npm 也提供了版本控制功能,支持通过指定的版本号或者 Tag 安装。

语法如下,在包名后面紧跟 @ 符号,再紧跟版本号或者 Tag 名称。

bash
npm install <package-name>@<version | tag>

例如:

现阶段 Vue 默认为 3.x 的版本了,如果想安装 Vue 2 ,可以通过指定版本号的方式安装:

bash
npm install vue@2.6.14

或者通过对应的 Tag 安装:

bash
npm install vue@legacy

TIP

版本号或者 Tag 名称可以在 npmjs 网站上的包详情页查询。

版本升级

一般来说,直接重新安装依赖包可以达到更新的目的,但也可以通过 npm update 命令来更新。

语法如下,可以更新全部的包:

bash
npm update

也可以更新指定的包:

bash
npm update <package-name>

npm 会检查是否有满足版本限制的更新版本。

卸载

可以通过 npm uninstall 命令来卸载指定的包,和安装一样,卸载也区分了卸载本地依赖包和卸载全局包,不过只有在卸载全局包的时候才需要添加选项,默认只卸载当前项目下的本地包。

本地卸载:

bash
npm uninstall <package-name>

全局卸载:

bash
npm uninstall --global <package-name>

TIP

Mac 用户需要使用 sudo 来提权才可以完成全局卸载。

如何使用包

在了解了 npm 包的常规操作之后,通过一个简单的例子来了解如何在项目里使用 npm 包。

继续使用的 Hello Node demo ,或者也可以重新创建一个 demo 。

首先在 命令行工具 通过 cd 命令进入项目所在的目录,用本地安装的方式来把 md5 包 添加到生产依赖,这是一个为提供开箱即用的哈希算法的包,在未来的实际工作中,可能也会用到它,在这里使用它是因为足够简单。

输入以下命令并回车执行:

bash
npm install md5

可以看到控制台提示一共安装了 4 个包,这是因为 md5 这个 npm 包还引用了其他的包作为依赖,需要同时安装才可以正常工作。

bash
# 这是安装 md5 之后控制台的信息返回
+added 4 packages, and audited 5 packages in 2s
+
+found 0 vulnerabilities

此时项目目录下会出现一个 node_modules 文件夹和一个 package-lock.json 文件:

bash
hello-node
+ # 依赖文件夹
+├─node_modules
+ # 源码文件夹
+├─src
+ # 锁定安装依赖的版本号
+├─package-lock.json
+ # 项目清单
+└─package.json

先打开 package.json ,可以看到已经多出了一个 dependencies 字段,这里记录了刚刚安装的 md5 包信息。

json
{
+  "name": "hello-node",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "dev:cjs": "node src/cjs/index.cjs",
+    "dev:esm": "node src/esm/index.mjs",
+    "serve": "node server/index.js"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "md5": "^2.3.0"
+  }
+}

来到这里可能会有一连串的疑问:

  1. 为什么只安装了一个 md5 ,但控制台提示安装了 4 个包?
  2. 为什么 package.json 又只记录了 1 个 md5 包信息?
  3. 为什么提示审核了 5 个包,哪里来的第 5 个包?

不要着急,请先打开 package-lock.json 文件,这个文件是记录了锁定安装依赖的版本号信息(由于篇幅原因,这里的展示省略了一些包的细节):

json
{
+  "name": "hello-node",
+  "version": "1.0.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "hello-node",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "md5": "^2.3.0"
+      }
+    },
+    "node_modules/charenc": {
+      "version": "0.0.2"
+      // ...
+    },
+    "node_modules/crypt": {
+      "version": "0.0.2"
+      // ...
+    },
+    "node_modules/is-buffer": {
+      "version": "1.1.6"
+      // ...
+    },
+    "node_modules/md5": {
+      "version": "2.3.0"
+      // ...
+    }
+  },
+  "dependencies": {
+    "charenc": {
+      "version": "0.0.2"
+      // ...
+    },
+    "crypt": {
+      "version": "0.0.2"
+      // ...
+    },
+    "is-buffer": {
+      "version": "1.1.6"
+      // ...
+    },
+    "md5": {
+      "version": "2.3.0",
+      // ...
+      "requires": {
+        "charenc": "0.0.2",
+        "crypt": "0.0.2",
+        "is-buffer": "~1.1.6"
+      }
+    }
+  }
+}

可以看到这个文件的 dependencies 字段除了 md5 之外,还有另外 3 个包信息,它们就是 md5 包所依赖的另外 3 个 npm 包了,这就解答了为什么一共安装了 4 个 npm 包。

在 node_modules 文件夹下也可以看到以这 4 个包名为命名的文件夹,这些文件夹存放的就是各个包项目发布在 npmjs 平台上的文件。

再看 packages 字段,这里除了罗列出 4 个 npm 包的信息之外,还把项目的信息也列了进来,这就是为什么是提示审核了 5 个包,原因是除了 4 个依赖包,该项目本身也是一个包。

TIP

package-lock.json 文件并不是一成不变的,假如以后 md5 又引用了更多的包,这里记录的信息也会随之增加。

并且不同的包管理器,它的 lock 文件也会不同,如果是使用 yarn 作为包管理器的话,它是生成一个 yarn.lock 文件,而不是 package-lock.json ,有关更多的包管理器,详见 插件的使用 一章。

现在已经安装好 md5 包了,接下来看看具体如何使用它。

通常在包的 npmjs 主页上会有 API 和用法的说明,只需要根据说明操作,打开 src/esm/index.mjs 文件,首先需要导入这个包。

包的导入和在 学习模块化设计 一节了解到的模块导入用法是一样的,只是把 from 后面的文件路径换成了包名。

js
// src/esm/index.mjs
+import md5 from 'md5'

然后根据 md5 的用法,来编写一个小例子,先声明一个原始字符串变量,然后再声明一个使用 md5 加密过的字符串变量,并打印它们:

js
// src/esm/index.mjs
+import md5 from 'md5'
+
+const before = 'Hello World'
+const after = md5(before)
+console.log({ before, after })

在命令行输入 npm run dev:esm ,可以在控制台看到输出了这些内容,成功获得了转换后的结果:

bash
npm run dev:esm
+
+> demo@1.0.0 dev:esm
+> node src/esm/index.mjs
+
+{ before: 'Hello World', after: 'b10a8db164e0754105b7a99be72e3fe5' }

是不是非常简单,其实包的用法和在导入模块的用法可以说是完全一样的,区别主要在于,包是需要安装了才能用,而模块是需要自己编写。

控制编译代码的兼容性

作为一名前端工程师,了解如何控制代码的兼容性是非常重要的能力。

在 “了解前端工程化” 的 为什么要使用构建工具 一节里,已简单介绍过 Polyfill 的作用,以及介绍了构建工具可以通过 Babel 等方案自动化处理代码的兼容问题,这一小节将讲解 Babel 的配置和使用,亲自体验如何控制代码的兼容性转换。

如何查询兼容性

在开始学习使用 Babel 之前,需要先掌握一个小技能:了解如何查询代码在不同浏览器上的兼容性。

说起浏览器兼容性,前端工程师应该都不陌生,特别是初学者很容易会遇到在自己的浏览器上布局正确、功能正常,而在其他人的电脑或者手机上访问就会有布局错位或者运行报错的问题出现,最常见的场景就是开发者使用的是功能强大的 Chrome 浏览器,而产品用户使用了 IE 浏览器。

这是因为网页开发使用的 HTML / CSS / JavaScript 每年都在更新新版本,推出更好用的新 API ,或者废弃部分过时的旧 API ,不同的浏览器在版本更新过程中,对这些新 API 的支持程度并不一致,如果使用了新 API 而没有做好兼容支持,很容易就会在低版本浏览器上出现问题。

为了保证程序可以正确的在不同版本浏览器之间运行,就需要根据产品要支持的目标浏览器范围,去选择兼容性最好的编程方案。

在 Web 开发有一个网站非常知名:Can I use ,只要搜索 API 的名称,它会以图表的形式展示该 API 在不同浏览器的不同版本之间的支持情况,支持 HTML 标签、 CSS 属性、 JavaScript API 等内容的查询。

以 JavaScript ES6 的 class 新特性为例:

`,128),C=l(`

可以看到在 Chrome 浏览器需要在 49 版本开始才被完全支持,而 IE 浏览器则全面不支持,如果不做特殊处理(例如引入 Polyfill 方案),那么就需要考虑在编程过程中,是否需要可以直接使用 class 来实现功能,还是寻找其他替代方案。

在工作中,工程师无需关注每一个 API 的具体支持范围,这些工作可以交给工具来处理,下面将介绍 Babel 的使用入门。

Babel 的使用和配置

Babel 是一个 JavaScript 编译器,它可以让开发者仅需维护一份简单的 JSON 配置文件,即可调动一系列工具链将源代码编译为目标浏览器指定版本所支持的语法。

安装 Babel

请打开 hello-node 项目,安装以下几个 Babel 依赖:

bash
npm i -D @babel/core @babel/cli @babel/preset-env

此时在 package.json 的 devDependencies 可以看到有了如下三个依赖:

json
{
+  "devDependencies": {
+    "@babel/cli": "^7.19.3",
+    "@babel/core": "^7.19.3",
+    "@babel/preset-env": "^7.19.3"
+  }
+}

它们的作用分别如下:

依赖作用文档
@babel/cli安装后可以从命令行使用 Babel 编译文件查看文档
@babel/coreBabel 的核心功能包查看文档
@babel/preset-env智能预设,可以通过它的选项控制代码要转换的支持版本查看文档

TIP

在使用 Babel 时,建议在项目下进行本地安装,尽量不选择全局安装,这是因为不同项目可能依赖于不同版本的 Babel ,全局依赖和可能会出现使用上的异常。

添加 Babel 配置

接下来在 hello-node 的根目录下创建一个名为 babel.config.json 的文件,这是 Babel 的配置文件,写入以下内容:

json
{
+  "presets": [
+    [
+      "@babel/preset-env",
+      {
+        "targets": {
+          "chrome": "41"
+        },
+        "modules": false,
+        "useBuiltIns": "usage",
+        "corejs": "3.6.5"
+      }
+    ]
+  ]
+}

这份配置将以 Chrome 浏览器作为目标浏览器,编译结果将保留 ES Module 规范,可以在 配置文件文档 查看更多配置选项。

这里的 targets.chrome 字段代表编译后要支持的目标浏览器版本号,在 caniuse 查询可知 ES6 的 class 语法 在 Chrome 49 版本之后才被完全支持,而 Chrome 41 或更低的版本是完全不支持该语法,因此先将其目标版本号设置为 41 ,下一步将开始测试 Babel 的编译结果。

使用 Babel 编译代码

在 hello-node 的 src 目录下添加一个 babel 文件夹,并在该文件夹下创建一个 index.js 文件,写入以下代码:

js
// src/babel/index.js
+export class Hello {
+  constructor(name) {
+    this.name = name
+  }
+
+  say() {
+    return \`Hello \${this.name}\`
+  }
+}

根据上一步的 Babel 配置,在这里使用 class 语法作为测试代码。

接下来再打开 package.json 文件,添加一个 compile script 如下:

json
{
+  "scripts": {
+    "dev:cjs": "node src/cjs/index.cjs",
+    "dev:esm": "node src/esm/index.mjs",
+    "compile": "babel src/babel --out-dir compiled",
+    "serve": "node server/index.js"
+  }
+}

这条命令的含义是:使用 Babel 处理 src/babel 目录下的文件,并输出到根目录下的 compiled 文件夹。

在命令行运行以下命令:

bash
npm run compile

可以看到 hello-node 的根目录下多了一个 compiled 文件夹,里面有一个和源码相同命名的 index.js 文件,它的文件内容如下:

js
// compiled/index.js
+function _classCallCheck(instance, Constructor) {
+  if (!(instance instanceof Constructor)) {
+    throw new TypeError('Cannot call a class as a function')
+  }
+}
+
+function _defineProperties(target, props) {
+  for (var i = 0; i < props.length; i++) {
+    var descriptor = props[i]
+    descriptor.enumerable = descriptor.enumerable || false
+    descriptor.configurable = true
+    if ('value' in descriptor) descriptor.writable = true
+    Object.defineProperty(target, descriptor.key, descriptor)
+  }
+}
+
+function _createClass(Constructor, protoProps, staticProps) {
+  if (protoProps) _defineProperties(Constructor.prototype, protoProps)
+  if (staticProps) _defineProperties(Constructor, staticProps)
+  Object.defineProperty(Constructor, 'prototype', { writable: false })
+  return Constructor
+}
+
+export var Hello = /*#__PURE__*/ (function () {
+  function Hello(name) {
+    _classCallCheck(this, Hello)
+
+    this.name = name
+  }
+
+  _createClass(Hello, [
+    {
+      key: 'say',
+      value: function say() {
+        return \`Hello \${this.name}\`
+      },
+    },
+  ])
+
+  return Hello
+})()

由于 Chrome 41 版本不支持 class 语法,因此 Babel 做了大量的工作对其进行转换兼容。

再次打开 babel.config.json ,将 targets.chrome 的版本号调整为支持 class 语法的 Chrome 49 版本:

diff
{
+  "presets": [
+    [
+      "@babel/preset-env",
+      {
+        "targets": {
+-          "chrome": "41"
++          "chrome": "49"
+        },
+        "modules": false,
+        "useBuiltIns": "usage",
+        "corejs": "3.6.5"
+      }
+    ]
+  ]
+}

再次执行编译,这一次编译后的代码和编译前完全一样:

js
// compiled/index.js
+export class Hello {
+  constructor(name) {
+    this.name = name
+  }
+
+  say() {
+    return \`Hello \${this.name}\`
+  }
+}

因为此时配置文件指定的目标浏览器版本已支持该语法,无需转换。

Babel 的使用其实非常简单,了解了这部分知识点之后,如果某一天需要自己控制代码的兼容性,只需要配合官方文档调整 Babel 的配置,处理起来就得心应手了!

`,35);function A(d,u,h,m,g,E){const o=p("ImgWrap"),a=p("ClientOnly"),e=p("GitalkComment");return c(),r("div",null,[D,s(a,null,{default:n(()=>[s(o,{src:"/assets/img/esm-run.jpg",alt:"esm.run 网站上的包转换操作界面"})]),_:1}),F,s(a,null,{default:n(()=>[s(o,{src:"/assets/img/components.png",alt:"把页面拆分成多个组件,降低维护成本(摘自 Vue 官网)"})]),_:1}),i,s(a,null,{default:n(()=>[s(o,{src:"/assets/img/caniuse-es6-classes.jpg",alt:"在 caniuse 网站上查询 ES6 `class` 的兼容情况"})]),_:1}),C,s(a,null,{default:n(()=>[s(e,{issueId:163})]),_:1})])}const f=t(y,[["render",A]]);export{q as __pageData,f as default}; diff --git a/assets/img/book.png b/assets/img/book.png new file mode 100644 index 00000000..bb7473db Binary files /dev/null and b/assets/img/book.png differ diff --git a/assets/img/bundler.png b/assets/img/bundler.png new file mode 100644 index 00000000..fc339cf1 Binary files /dev/null and b/assets/img/bundler.png differ diff --git a/assets/img/caniuse-es6-classes.jpg b/assets/img/caniuse-es6-classes.jpg new file mode 100644 index 00000000..3ae62d67 Binary files /dev/null and b/assets/img/caniuse-es6-classes.jpg differ diff --git a/assets/img/cats.jpg b/assets/img/cats.jpg new file mode 100644 index 00000000..11592427 Binary files /dev/null and b/assets/img/cats.jpg differ diff --git a/assets/img/communication-prop-drilling.png b/assets/img/communication-prop-drilling.png new file mode 100644 index 00000000..47ff6dd1 Binary files /dev/null and b/assets/img/communication-prop-drilling.png differ diff --git a/assets/img/communication-provide-inject.png b/assets/img/communication-provide-inject.png new file mode 100644 index 00000000..92201eb7 Binary files /dev/null and b/assets/img/communication-provide-inject.png differ diff --git a/assets/img/components.png b/assets/img/components.png new file mode 100644 index 00000000..20d0435a Binary files /dev/null and b/assets/img/components.png differ diff --git a/assets/img/cookbook-miniprogram.jpg b/assets/img/cookbook-miniprogram.jpg new file mode 100644 index 00000000..1536ce07 Binary files /dev/null and b/assets/img/cookbook-miniprogram.jpg differ diff --git a/assets/img/donate-dark.jpg b/assets/img/donate-dark.jpg new file mode 100644 index 00000000..41883c74 Binary files /dev/null and b/assets/img/donate-dark.jpg differ diff --git a/assets/img/donate.jpg b/assets/img/donate.jpg new file mode 100644 index 00000000..795a01ce Binary files /dev/null and b/assets/img/donate.jpg differ diff --git a/assets/img/esm-run.jpg b/assets/img/esm-run.jpg new file mode 100644 index 00000000..541a2ed4 Binary files /dev/null and b/assets/img/esm-run.jpg differ diff --git a/assets/img/esm.png b/assets/img/esm.png new file mode 100644 index 00000000..22728998 Binary files /dev/null and b/assets/img/esm.png differ diff --git a/assets/img/github-top-languages-dark.jpg b/assets/img/github-top-languages-dark.jpg new file mode 100644 index 00000000..670622f7 Binary files /dev/null and b/assets/img/github-top-languages-dark.jpg differ diff --git a/assets/img/github-top-languages.jpg b/assets/img/github-top-languages.jpg new file mode 100644 index 00000000..ab976d61 Binary files /dev/null and b/assets/img/github-top-languages.jpg differ diff --git a/assets/img/job-details-dark.png b/assets/img/job-details-dark.png new file mode 100644 index 00000000..ae57b2eb Binary files /dev/null and b/assets/img/job-details-dark.png differ diff --git a/assets/img/job-details.png b/assets/img/job-details.png new file mode 100644 index 00000000..4e2a9713 Binary files /dev/null and b/assets/img/job-details.png differ diff --git a/assets/img/npm-detail-ts-icon.jpg b/assets/img/npm-detail-ts-icon.jpg new file mode 100644 index 00000000..9d80dd2d Binary files /dev/null and b/assets/img/npm-detail-ts-icon.jpg differ diff --git a/assets/img/screenshot-pkg-dark.jpg b/assets/img/screenshot-pkg-dark.jpg new file mode 100644 index 00000000..217ae054 Binary files /dev/null and b/assets/img/screenshot-pkg-dark.jpg differ diff --git a/assets/img/screenshot-pkg.jpg b/assets/img/screenshot-pkg.jpg new file mode 100644 index 00000000..bdedfd78 Binary files /dev/null and b/assets/img/screenshot-pkg.jpg differ diff --git a/assets/img/screenshot-postman-dark.jpg b/assets/img/screenshot-postman-dark.jpg new file mode 100644 index 00000000..627d1e56 Binary files /dev/null and b/assets/img/screenshot-postman-dark.jpg differ diff --git a/assets/img/screenshot-postman.jpg b/assets/img/screenshot-postman.jpg new file mode 100644 index 00000000..ff3c8037 Binary files /dev/null and b/assets/img/screenshot-postman.jpg differ diff --git a/assets/img/screenshot-vscode-dark.jpg b/assets/img/screenshot-vscode-dark.jpg new file mode 100644 index 00000000..952db0e0 Binary files /dev/null and b/assets/img/screenshot-vscode-dark.jpg differ diff --git a/assets/img/screenshot-vscode.jpg b/assets/img/screenshot-vscode.jpg new file mode 100644 index 00000000..1edcef24 Binary files /dev/null and b/assets/img/screenshot-vscode.jpg differ diff --git a/assets/img/seo-page-code-dark.jpg b/assets/img/seo-page-code-dark.jpg new file mode 100644 index 00000000..774ce5e7 Binary files /dev/null and b/assets/img/seo-page-code-dark.jpg differ diff --git a/assets/img/seo-page-code.jpg b/assets/img/seo-page-code.jpg new file mode 100644 index 00000000..6b1dd864 Binary files /dev/null and b/assets/img/seo-page-code.jpg differ diff --git a/assets/img/seo-page-dark.jpg b/assets/img/seo-page-dark.jpg new file mode 100644 index 00000000..245b5134 Binary files /dev/null and b/assets/img/seo-page-dark.jpg differ diff --git a/assets/img/seo-page.jpg b/assets/img/seo-page.jpg new file mode 100644 index 00000000..fcd7af1f Binary files /dev/null and b/assets/img/seo-page.jpg differ diff --git a/assets/img/seo-spa-page-code-dark.jpg b/assets/img/seo-spa-page-code-dark.jpg new file mode 100644 index 00000000..7a76c1a1 Binary files /dev/null and b/assets/img/seo-spa-page-code-dark.jpg differ diff --git a/assets/img/seo-spa-page-code.jpg b/assets/img/seo-spa-page-code.jpg new file mode 100644 index 00000000..d345d779 Binary files /dev/null and b/assets/img/seo-spa-page-code.jpg differ diff --git a/assets/img/vue-devtools-components-dark.jpg b/assets/img/vue-devtools-components-dark.jpg new file mode 100644 index 00000000..cf45527c Binary files /dev/null and b/assets/img/vue-devtools-components-dark.jpg differ diff --git a/assets/img/vue-devtools-components.jpg b/assets/img/vue-devtools-components.jpg new file mode 100644 index 00000000..95edc3b3 Binary files /dev/null and b/assets/img/vue-devtools-components.jpg differ diff --git a/assets/img/vue-devtools-pinia-dark.jpg b/assets/img/vue-devtools-pinia-dark.jpg new file mode 100644 index 00000000..8fd4b7be Binary files /dev/null and b/assets/img/vue-devtools-pinia-dark.jpg differ diff --git a/assets/img/vue-devtools-pinia.jpg b/assets/img/vue-devtools-pinia.jpg new file mode 100644 index 00000000..490c2573 Binary files /dev/null and b/assets/img/vue-devtools-pinia.jpg differ diff --git a/assets/img/vue-devtools-routes-dark.jpg b/assets/img/vue-devtools-routes-dark.jpg new file mode 100644 index 00000000..1719771d Binary files /dev/null and b/assets/img/vue-devtools-routes-dark.jpg differ diff --git a/assets/img/vue-devtools-routes.jpg b/assets/img/vue-devtools-routes.jpg new file mode 100644 index 00000000..1e362a60 Binary files /dev/null and b/assets/img/vue-devtools-routes.jpg differ diff --git a/assets/img/vue-devtools-timeline-dark.jpg b/assets/img/vue-devtools-timeline-dark.jpg new file mode 100644 index 00000000..cfdecee9 Binary files /dev/null and b/assets/img/vue-devtools-timeline-dark.jpg differ diff --git a/assets/img/vue-devtools-timeline.jpg b/assets/img/vue-devtools-timeline.jpg new file mode 100644 index 00000000..69ebd6a6 Binary files /dev/null and b/assets/img/vue-devtools-timeline.jpg differ diff --git a/assets/img/vue-versions-on-npmjs.jpg b/assets/img/vue-versions-on-npmjs.jpg new file mode 100644 index 00000000..0fa3e6a2 Binary files /dev/null and b/assets/img/vue-versions-on-npmjs.jpg differ diff --git a/assets/index.md.c8d0f3cd.js b/assets/index.md.c8d0f3cd.js new file mode 100644 index 00000000..1131332b --- /dev/null +++ b/assets/index.md.c8d0f3cd.js @@ -0,0 +1,8 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as n,v as l,b as r,t as a,O as d,F as e,L as i,R as p,M as t}from"./chunks/framework.0d8bea05.js";const b=JSON.parse('{"title":"出版说明","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"index.md","filePath":"index.md"}'),c={name:"index.md"},h=e("h1",{id:"出版说明",tabindex:"-1"},[i("出版说明 "),e("a",{class:"header-anchor",href:"#出版说明","aria-label":'Permalink to "出版说明"'},"​")],-1),f=e("p",null,"感谢各位读者长期以来的支持,经过长达一年时间的打磨和优化,本书已于 2023 年 5 月份正式出版上市,纸质版书籍的正式名称为《前端工程化:基于 Vue.js 3.0 的设计与实践》。",-1),m=p('

如果您对笔者的作品认可,建议购买纸质版,纸质书在电子书的基础上,经过机械工业出版社的编辑老师们的内容优化、校对勘误、排版美化,更成体系!

在此特别感谢李晓波编辑支持笔者将开源作品出版为纸质作品,李老师全程帮忙跟进了无数的大事小事,也给笔者科普了很多出版方面的知识,十分尽职!

购买地址

各位读者可以在各大电商平台购买纸质版书籍:

☞ 访问 京东商城 购买

☞ 访问 天猫商城 购买

TIP

可以在电商平台搜索纸质版的书名 “前端工程化:基于 Vue.js 3.0 的设计与实践” 或者笔者的名字 “程沛权” 查找更多的购买地址。

背后的故事

如果读者想了解这本书是如何写出来的,想了解笔者在日常生活中是个什么样的人,想知道笔者的表达能力是如何培养的,可以阅读 这本书背后的故事

',9);function u(X,_,V,g,k,F){const o=t("ImgWrap"),s=t("ClientOnly");return l(),r("div",null,[h,f,a(s,null,{default:d(()=>[a(o,{src:"/assets/img/book.png",alt:"前端工程化:基于 Vue.js 3.0 的设计与实践"})]),_:1}),m])}const B=n(c,[["render",u]]);export{b as __pageData,B as default}; diff --git a/assets/index.md.c8d0f3cd.lean.js b/assets/index.md.c8d0f3cd.lean.js new file mode 100644 index 00000000..1131332b --- /dev/null +++ b/assets/index.md.c8d0f3cd.lean.js @@ -0,0 +1,8 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as n,v as l,b as r,t as a,O as d,F as e,L as i,R as p,M as t}from"./chunks/framework.0d8bea05.js";const b=JSON.parse('{"title":"出版说明","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"index.md","filePath":"index.md"}'),c={name:"index.md"},h=e("h1",{id:"出版说明",tabindex:"-1"},[i("出版说明 "),e("a",{class:"header-anchor",href:"#出版说明","aria-label":'Permalink to "出版说明"'},"​")],-1),f=e("p",null,"感谢各位读者长期以来的支持,经过长达一年时间的打磨和优化,本书已于 2023 年 5 月份正式出版上市,纸质版书籍的正式名称为《前端工程化:基于 Vue.js 3.0 的设计与实践》。",-1),m=p('

如果您对笔者的作品认可,建议购买纸质版,纸质书在电子书的基础上,经过机械工业出版社的编辑老师们的内容优化、校对勘误、排版美化,更成体系!

在此特别感谢李晓波编辑支持笔者将开源作品出版为纸质作品,李老师全程帮忙跟进了无数的大事小事,也给笔者科普了很多出版方面的知识,十分尽职!

购买地址

各位读者可以在各大电商平台购买纸质版书籍:

☞ 访问 京东商城 购买

☞ 访问 天猫商城 购买

TIP

可以在电商平台搜索纸质版的书名 “前端工程化:基于 Vue.js 3.0 的设计与实践” 或者笔者的名字 “程沛权” 查找更多的购买地址。

背后的故事

如果读者想了解这本书是如何写出来的,想了解笔者在日常生活中是个什么样的人,想知道笔者的表达能力是如何培养的,可以阅读 这本书背后的故事

',9);function u(X,_,V,g,k,F){const o=t("ImgWrap"),s=t("ClientOnly");return l(),r("div",null,[h,f,a(s,null,{default:d(()=>[a(o,{src:"/assets/img/book.png",alt:"前端工程化:基于 Vue.js 3.0 的设计与实践"})]),_:1}),m])}const B=n(c,[["render",u]]);export{b as __pageData,B as default}; diff --git a/assets/inter-italic-cyrillic-ext.33bd5a8e.woff2 b/assets/inter-italic-cyrillic-ext.33bd5a8e.woff2 new file mode 100644 index 00000000..2a687296 Binary files /dev/null and b/assets/inter-italic-cyrillic-ext.33bd5a8e.woff2 differ diff --git a/assets/inter-italic-cyrillic.ea42a392.woff2 b/assets/inter-italic-cyrillic.ea42a392.woff2 new file mode 100644 index 00000000..f6403515 Binary files /dev/null and b/assets/inter-italic-cyrillic.ea42a392.woff2 differ diff --git a/assets/inter-italic-greek-ext.4fbe9427.woff2 b/assets/inter-italic-greek-ext.4fbe9427.woff2 new file mode 100644 index 00000000..00218960 Binary files /dev/null and b/assets/inter-italic-greek-ext.4fbe9427.woff2 differ diff --git a/assets/inter-italic-greek.8f4463c4.woff2 b/assets/inter-italic-greek.8f4463c4.woff2 new file mode 100644 index 00000000..71c265f8 Binary files /dev/null and b/assets/inter-italic-greek.8f4463c4.woff2 differ diff --git a/assets/inter-italic-latin-ext.bd8920cc.woff2 b/assets/inter-italic-latin-ext.bd8920cc.woff2 new file mode 100644 index 00000000..9c1b9440 Binary files /dev/null and b/assets/inter-italic-latin-ext.bd8920cc.woff2 differ diff --git a/assets/inter-italic-latin.bd3b6f56.woff2 b/assets/inter-italic-latin.bd3b6f56.woff2 new file mode 100644 index 00000000..01fcf207 Binary files /dev/null and b/assets/inter-italic-latin.bd3b6f56.woff2 differ diff --git a/assets/inter-italic-vietnamese.6ce511fb.woff2 b/assets/inter-italic-vietnamese.6ce511fb.woff2 new file mode 100644 index 00000000..e4f788ee Binary files /dev/null and b/assets/inter-italic-vietnamese.6ce511fb.woff2 differ diff --git a/assets/inter-roman-cyrillic-ext.e75737ce.woff2 b/assets/inter-roman-cyrillic-ext.e75737ce.woff2 new file mode 100644 index 00000000..28593ccb Binary files /dev/null and b/assets/inter-roman-cyrillic-ext.e75737ce.woff2 differ diff --git a/assets/inter-roman-cyrillic.5f2c6c8c.woff2 b/assets/inter-roman-cyrillic.5f2c6c8c.woff2 new file mode 100644 index 00000000..a20adc16 Binary files /dev/null and b/assets/inter-roman-cyrillic.5f2c6c8c.woff2 differ diff --git a/assets/inter-roman-greek-ext.ab0619bc.woff2 b/assets/inter-roman-greek-ext.ab0619bc.woff2 new file mode 100644 index 00000000..e3b0be76 Binary files /dev/null and b/assets/inter-roman-greek-ext.ab0619bc.woff2 differ diff --git a/assets/inter-roman-greek.d5a6d92a.woff2 b/assets/inter-roman-greek.d5a6d92a.woff2 new file mode 100644 index 00000000..f790e047 Binary files /dev/null and b/assets/inter-roman-greek.d5a6d92a.woff2 differ diff --git a/assets/inter-roman-latin-ext.0030eebd.woff2 b/assets/inter-roman-latin-ext.0030eebd.woff2 new file mode 100644 index 00000000..715bd903 Binary files /dev/null and b/assets/inter-roman-latin-ext.0030eebd.woff2 differ diff --git a/assets/inter-roman-latin.2ed14f66.woff2 b/assets/inter-roman-latin.2ed14f66.woff2 new file mode 100644 index 00000000..a540b7af Binary files /dev/null and b/assets/inter-roman-latin.2ed14f66.woff2 differ diff --git a/assets/inter-roman-vietnamese.14ce25a6.woff2 b/assets/inter-roman-vietnamese.14ce25a6.woff2 new file mode 100644 index 00000000..5a9f9cb9 Binary files /dev/null and b/assets/inter-roman-vietnamese.14ce25a6.woff2 differ diff --git a/assets/links.md.d1aafe6b.js b/assets/links.md.d1aafe6b.js new file mode 100644 index 00000000..523b7739 --- /dev/null +++ b/assets/links.md.d1aafe6b.js @@ -0,0 +1,8 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as n,v as l,b as o,t as e,O as i,R as s,M as t}from"./chunks/framework.0d8bea05.js";const y=JSON.parse('{"title":"常用文档","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"links.md","filePath":"links.md"}'),p={name:"links.md"},h=s('

常用文档

存档一些会用到的在线文档链接,方便读者进行扩展阅读。

官方文档

只罗列 Vue 3.0 相关的官网,有中文版的优先都放中文版。

名称官网文档
Vue 3点击访问
Vue Composition API点击访问
Vue Router点击访问
Vuex点击访问
Pinia点击访问
Vue CLI点击访问
Vite点击访问

教程工具

可能会用到的一些教程或者工具。

常用的 Web API 类型查询

DOM 元素与子组件 一节有提及到如何对 DOM 元素定义 TS 类型,拿出来放这里更方便查阅。

点击阅读:文档对象模型

当然其实不止于 DOM,所有的 Web API 都有自己的类型接口,可以通过 MDN 文档查阅。

点击阅读:Web API

TypeScript 入门教程

最近发现的一本好书,从 JavaScript 程序员的角度总结思考,循序渐进的理解 TypeScript ,可以帮更快熟悉 TS 开发。

点击阅读:TypeScript 入门教程

深入理解 TypeScript

一本挺不错的 TS 学习书籍,我自己是在京东打折的时候买的纸质书,不过也有个在线版,可以进行在线阅读,也可以去购买纸质版支持下作者 / 译者。

点击阅读:深入理解 TypeScript

程序员做菜教程

哈哈哈这个和编程没有太多关系,不过有兴趣也可以看看,也是我写的一些作品。

从 2021 年底陆续开始维护这个菜谱栏目,终于有一些像模像样的作品沉淀了,可以在这里了解一些关于如何下厨烹饪的菜谱和教程,这是来自一名从小就学做饭的程序员的原创经验。

详情查看:cooking-cookbook

也可以直接扫码访问小程序:

',23);function c(d,g,b,u,f,k){const r=t("ImgWrap"),a=t("ClientOnly");return l(),o("div",null,[h,e(a,null,{default:i(()=>[e(r,{src:"/assets/img/cookbook-miniprogram.jpg",alt:"请打开微信扫描二维码"})]),_:1})])}const _=n(p,[["render",c]]);export{y as __pageData,_ as default}; diff --git a/assets/links.md.d1aafe6b.lean.js b/assets/links.md.d1aafe6b.lean.js new file mode 100644 index 00000000..523b7739 --- /dev/null +++ b/assets/links.md.d1aafe6b.lean.js @@ -0,0 +1,8 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as n,v as l,b as o,t as e,O as i,R as s,M as t}from"./chunks/framework.0d8bea05.js";const y=JSON.parse('{"title":"常用文档","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"links.md","filePath":"links.md"}'),p={name:"links.md"},h=s('

常用文档

存档一些会用到的在线文档链接,方便读者进行扩展阅读。

官方文档

只罗列 Vue 3.0 相关的官网,有中文版的优先都放中文版。

名称官网文档
Vue 3点击访问
Vue Composition API点击访问
Vue Router点击访问
Vuex点击访问
Pinia点击访问
Vue CLI点击访问
Vite点击访问

教程工具

可能会用到的一些教程或者工具。

常用的 Web API 类型查询

DOM 元素与子组件 一节有提及到如何对 DOM 元素定义 TS 类型,拿出来放这里更方便查阅。

点击阅读:文档对象模型

当然其实不止于 DOM,所有的 Web API 都有自己的类型接口,可以通过 MDN 文档查阅。

点击阅读:Web API

TypeScript 入门教程

最近发现的一本好书,从 JavaScript 程序员的角度总结思考,循序渐进的理解 TypeScript ,可以帮更快熟悉 TS 开发。

点击阅读:TypeScript 入门教程

深入理解 TypeScript

一本挺不错的 TS 学习书籍,我自己是在京东打折的时候买的纸质书,不过也有个在线版,可以进行在线阅读,也可以去购买纸质版支持下作者 / 译者。

点击阅读:深入理解 TypeScript

程序员做菜教程

哈哈哈这个和编程没有太多关系,不过有兴趣也可以看看,也是我写的一些作品。

从 2021 年底陆续开始维护这个菜谱栏目,终于有一些像模像样的作品沉淀了,可以在这里了解一些关于如何下厨烹饪的菜谱和教程,这是来自一名从小就学做饭的程序员的原创经验。

详情查看:cooking-cookbook

也可以直接扫码访问小程序:

',23);function c(d,g,b,u,f,k){const r=t("ImgWrap"),a=t("ClientOnly");return l(),o("div",null,[h,e(a,null,{default:i(()=>[e(r,{src:"/assets/img/cookbook-miniprogram.jpg",alt:"请打开微信扫描二维码"})]),_:1})])}const _=n(p,[["render",c]]);export{y as __pageData,_ as default}; diff --git a/assets/pinia.md.c3d0acb5.js b/assets/pinia.md.c3d0acb5.js new file mode 100644 index 00000000..3e8691ad --- /dev/null +++ b/assets/pinia.md.c3d0acb5.js @@ -0,0 +1,465 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as e,v as t,b as c,t as s,O as r,R as n,M as a}from"./chunks/framework.0d8bea05.js";const m=JSON.parse('{"title":"全局状态管理","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"pinia.md","filePath":"pinia.md"}'),y={name:"pinia.md"},D=n('

全局状态管理

本来这部分打算放在 组件之间的通信 里,里面也简单介绍了一下 Vuex ,但 Pinia 作为被官方推荐在 Vue 3 项目里作为全局状态管理的新工具,写着写着笔者认为还是单独开一章来写会更方便阅读和理解。

官方推出的全局状态管理工具目前有 VuexPinia ,两者的作用和用法都比较相似,但 Pinia 的设计更贴近 Vue 3 组合式 API 的用法。

TIP

本章内的大部分内容都会和 Vuex 作对比,方便从 Vuex 项目向 Pinia 的迁移。

',4),F=n(`

关于 Pinia ~new

由于 Vuex 4.x 版本只是个过渡版,Vuex 4 对 TypeScript 和 Composition API 都不是很友好,虽然官方团队在 GitHub 已有讨论 Vuex 5 的开发提案,但从 2022-02-07 在 Vue 3 被设置为默认版本开始, Pinia 已正式被官方推荐作为全局状态管理的工具。

Pinia 支持 Vue 3 和 Vue 2 ,对 TypeScript 也有很完好的支持,延续本指南的宗旨,在这里只介绍基于 Vue 3 和 TypeScript 的用法。

点击访问:Pinia 官网

安装和启用 ~new

Pinia 目前还没有被广泛的默认集成在各种脚手架里,所以如果原来创建的项目没有 Pinia ,则需要手动安装它。

bash
# 需要 cd 到的项目目录下
+npm install pinia

查看的 package.json ,看看里面的 dependencies 是否成功加入了 Pinia 和它的版本号(下方是示例代码,以实际安装的最新版本号为准):

json
{
+  "dependencies": {
+    "pinia": "^2.0.11"
+  }
+}

然后打开 src/main.ts 文件,添加下面那两行有注释的新代码:

ts
import { createApp } from 'vue'
+import { createPinia } from 'pinia' // 导入 Pinia
+import App from '@/App.vue'
+
+createApp(App)
+  .use(createPinia()) // 启用 Pinia
+  .mount('#app')

到这里, Pinia 就集成到的项目里了。

TIP

也可以通过 Create Preset 创建新项目(选择 vue 技术栈进入,选择 vue3-ts-vite 模板),可以得到一个集成常用配置的项目启动模板,该模板现在使用 Pinia 作为全局状态管理工具。

状态树的结构 ~new

在开始写代码之前,先来看一个对比,直观的了解 Pinia 的状态树构成,才能在后面的环节更好的理解每个功能的用途。

鉴于可能有部分开发者之前没有用过 Vuex ,所以加入了 Vue 组件一起对比( Options API 写法)。

作用Vue ComponentVuexPinia
数据管理datastatestate
数据计算computedgettersgetters
行为方法methodsmutations / actionsactions

可以看到 Pinia 的结构和用途都和 Vuex 与 Component 非常相似,并且 Pinia 相对于 Vuex ,在行为方法部分去掉了 mutations (同步操作)和 actions (异步操作)的区分,更接近组件的结构,入门成本会更低一些。

下面来创建一个简单的 Store ,开始用 Pinia 来进行状态管理。

创建 Store ~new

和 Vuex 一样, Pinia 的核心也是称之为 Store 。

参照 Pinia 官网推荐的项目管理方案,也是先在 src 文件夹下创建一个 stores 文件夹,并在里面添加一个 index.ts 文件,然后就可以来添加一个最基础的 Store 。

Store 是通过 defineStore 方法来创建的,它有两种入参形式:

形式 1 :接收两个参数

接收两个参数,第一个参数是 Store 的唯一 ID ,第二个参数是 Store 的选项:

ts
// src/stores/index.ts
+import { defineStore } from 'pinia'
+
+export const useStore = defineStore('main', {
+  // Store 选项...
+})

形式 2 :接收一个参数

接收一个参数,直接传入 Store 的选项,但是需要把唯一 ID 作为选项的一部分一起传入:

ts
// src/stores/index.ts
+import { defineStore } from 'pinia'
+
+export const useStore = defineStore({
+  id: 'main',
+  // Store 选项...
+})

TIP

不论是哪种创建形式,都必须为 Store 指定一个唯一 ID 。

另外可以看到这里把导出的函数名命名为 useStore ,以 use 开头是 Vue 3 对可组合函数的一个命名约定。

并且使用的是 export const 而不是 export default (详见:命名导出和默认导出),这样在使用的时候可以和其他的 Vue 组合函数保持一致,都是通过 import { xxx } from 'xxx' 来导入。

如果有多个 Store ,可以分模块管理,并根据实际的功能用途进行命名( e.g. useMessageStoreuseUserStoreuseGameStore … )。

管理 state ~new

在上一小节的 状态树的结构 这里已经了解过, Pinia 是在 state 里面定义状态数据。

给 Store 添加 state

它是通过一个箭头函数的形式来返回数据,并且能够正确的帮推导 TypeScript 类型:

ts
// src/stores/index.ts
+import { defineStore } from 'pinia'
+
+export const useStore = defineStore('main', {
+  // 先定义一个最基本的 message 数据
+  state: () => ({
+    message: 'Hello World',
+  }),
+  // ...
+})

需要注意一点的是,如果不显式 return ,箭头函数的返回值需要用圆括号 () 套起来,这个是箭头函数的要求(详见:返回对象字面量)。

所以相当于这样写:

ts
// ...
+export const useStore = defineStore('main', {
+  state: () => {
+    return {
+      message: 'Hello World',
+    }
+  },
+  // ...
+})

笔者还是更喜欢加圆括号的简写方式。

TIP

可能有开发者会问: Vuex 可以用一个对象来定义 state 的数据, Pinia 可以吗?

答案是:不可以! state 的类型必须是 state?: (() => {}) | undefined ,要么不配置(就是 undefined ),要么只能是个箭头函数。

手动指定数据类型

虽然 Pinia 会帮推导 TypeScript 的数据类型,但有时候可能不太够用,比如下面这段代码,请留意代码注释的说明:

ts
// ...
+export const useStore = defineStore('main', {
+  state: () => {
+    return {
+      message: 'Hello World',
+      // 添加了一个随机消息数组
+      randomMessages: [],
+    }
+  },
+  // ...
+})

randomMessages 的预期应该是一个字符串数组 string[] ,但是这个时候 Pinia 会帮推导成 never[] ,那么类型就对不上了。

这种情况下就需要手动指定 randomMessages 的类型,可以通过 as 来指定:

ts
// ...
+export const useStore = defineStore('main', {
+  state: () => {
+    return {
+      message: 'Hello World',
+      // 通过 as 关键字指定 TS 类型
+      randomMessages: [] as string[],
+    }
+  },
+  // ...
+})

或者使用尖括号 <> 来指定:

ts
// ...
+export const useStore = defineStore('main', {
+  state: () => {
+    return {
+      message: 'Hello World',
+      // 通过尖括号指定 TS 类型
+      randomMessages: <string[]>[],
+    }
+  },
+  // ...
+})

这两种方式是等价的。

获取和更新 state

获取 state 有多种方法,略微有区别(详见下方各自的说明),但相同的是,他们都是响应性的。

WARNING

不能直接通过 ES6 解构的方式( e.g. const { message } = store ),那样会破坏数据的响应性。

使用 store 实例

用法上和 Vuex 很相似,但有一点区别是,数据直接是挂在 store 上的,而不是 store.state 上面!

TIP

e.g. Vuex 是 store.state.message , Pinia 是 store.message

所以,可以直接通过 store.message 直接调用 state 里的数据。

ts
import { defineComponent } from 'vue'
+import { useStore } from '@/stores'
+
+export default defineComponent({
+  setup() {
+    // 像 useRouter 那样定义一个变量拿到实例
+    const store = useStore()
+
+    // 直接通过实例来获取数据
+    console.log(store.message)
+
+    // 这种方式需要把整个 store 给到 template 去渲染数据
+    return {
+      store,
+    }
+  },
+})

但一些比较复杂的数据这样写会很长,所以有时候更推荐用下面介绍的 computed APIstoreToRefs API 等方式来获取。

在数据更新方面,在 Pinia 可以直接通过 Store 实例更新 state (这一点与 Vuex 有明显的不同,更改 Vuex 的 store 中的状态的唯一方法是提交 mutation),所以如果要更新 message ,只需要像下面这样,就可以更新 message 的值了!

ts
store.message = 'New Message.'

使用 computed API

现在 state 里已经有定义好的数据了,下面这段代码是在 Vue 组件里导入的 Store ,并通过计算数据 computed 拿到里面的 message 数据传给 template 使用。

vue
<script lang="ts">
+import { computed, defineComponent } from 'vue'
+import { useStore } from '@/stores'
+
+export default defineComponent({
+  setup() {
+    // 像 useRouter 那样定义一个变量拿到实例
+    const store = useStore()
+
+    // 通过计算拿到里面的数据
+    const message = computed(() => store.message)
+    console.log('message', message.value)
+
+    // 传给 template 使用
+    return {
+      message,
+    }
+  },
+})
+</script>

使用 store 实例 以及 使用 storeToRefs API 不同,这个方式默认情况下无法直接更新 state 的值。

TIP

这里的定义的 message 变量是一个只有 getter ,没有 setter 的 ComputedRef 数据,所以它是只读的。

如果要更新数据怎么办?

  1. 可以通过提前定义好的 Store Actions 方法进行更新。

  2. 在定义 computed 变量的时候,配置好 setter 的行为:

ts
// 其他代码和上一个例子一样,这里省略...
+
+// 修改:定义 computed 变量的时候配置 getter 和 setter
+const message = computed({
+  // getter 还是返回数据的值
+  get: () => store.message,
+  // 配置 setter 来定义赋值后的行为
+  set(newVal) {
+    store.message = newVal
+  },
+})
+
+// 此时不再抛出 Write operation failed: computed value is readonly 的警告
+message.value = 'New Message.'
+
+// store 上的数据已成功变成了 New Message.
+console.log(store.message)

使用 storeToRefs API

Pinia 还提供了一个 storeToRefs API 用于把 state 的数据转换为 ref 变量。

这是一个专门为 Pinia Stores 设计的 API ,类似于 toRefs ,区别在于,它会忽略掉 Store 上面的方法和非响应性的数据,只返回 state 上的响应性数据。

ts
import { defineComponent } from 'vue'
+import { useStore } from '@/stores'
+
+// 记得导入这个 API
+import { storeToRefs } from 'pinia'
+
+export default defineComponent({
+  setup() {
+    const store = useStore()
+
+    // 通过 storeToRefs 来拿到响应性的 message
+    const { message } = storeToRefs(store)
+    console.log('message', message.value)
+
+    return {
+      message,
+    }
+  },
+})

通过这个方式拿到的 message 变量是一个 Ref 类型的数据,所以可以像普通的 ref 变量一样进行读取和赋值。

ts
// 直接赋值即可
+message.value = 'New Message.'
+
+// store 上的数据已成功变成了 New Message.
+console.log(store.message)

使用 toRefs API

使用 storeToRefs API 部分所说,该 API 本身的设计就是类似于 toRefs ,所以也可以直接用 toRefs 把 state 上的数据转成 ref 变量。

ts
// 注意 toRefs 是 vue 的 API ,不是 Pinia
+import { defineComponent, toRefs } from 'vue'
+import { useStore } from '@/stores'
+
+export default defineComponent({
+  setup() {
+    const store = useStore()
+
+    // 跟 storeToRefs 操作都一样,只不过用 Vue 的这个 API 来处理
+    const { message } = toRefs(store)
+    console.log('message', message.value)
+
+    return {
+      message,
+    }
+  },
+})

详见 使用 toRefs 一节的说明,可以像普通的 ref 变量一样进行读取和赋值。

另外,像上面这样,对 store 执行 toRefs 会把 store 上面的 getters 、 actions 也一起提取,如果只需要提取 state 上的数据,可以这样做:

ts
// 只传入 store.$state
+const { message } = toRefs(store.$state)

使用 toRef API

toRef 是 toRefs 的兄弟 API ,一个是只转换一个字段,一个是转换所有字段,所以它也可以用来转换 state 数据变成 ref 变量。

ts
// 注意 toRef 是 vue 的 API ,不是 Pinia
+import { defineComponent, toRef } from 'vue'
+import { useStore } from '@/stores'
+
+export default defineComponent({
+  setup() {
+    const store = useStore()
+
+    // 遵循 toRef 的用法即可
+    const message = toRef(store, 'message')
+    console.log('message', message.value)
+
+    return {
+      message,
+    }
+  },
+})

详见 使用 toRef 一节的说明,可以像普通的 ref 变量一样进行读取和赋值。

使用 actions 方法

在 Vuex ,如果想通过方法来操作 state 的更新,必须通过 mutation 来提交;而异步操作需要更多一个步骤,必须先通过 action 来触发 mutation ,非常繁琐!

Pinia 所有操作都集合为 action ,无需区分同步和异步,按照平时的函数定义即可更新 state ,具体操作详见 管理 actions 一节。

批量更新 state

获取和更新 state 部分说的都是如何修改单个 state 数据,那么有时候要同时修改很多个,会显得比较繁琐。

如果写过 React 或者微信小程序,应该非常熟悉这些用法:

ts
// 下面不是 Vue 的代码,不要在的项目里使用
+
+// React
+this.setState({
+  foo: 'New Foo Value',
+  bar: 'New bar Value',
+})
+
+// 微信小程序
+this.setData({
+  foo: 'New Foo Value',
+  bar: 'New bar Value',
+})

Pinia 也提供了一个 $patch API 用于同时修改多个数据,它接收一个参数:

参数类型语法
partialState对象 / 函数store.$patch(partialState)

传入一个对象

当参数类型为对象时,key 是要修改的 state 数据名称, value 是新的值(支持嵌套传值),用法如下:

ts
// 继续用前面的数据,这里会打印出修改前的值
+console.log(JSON.stringify(store.$state))
+// 输出 {"message":"Hello World","randomMessages":[]}
+
+/**
+ * 注意这里,传入了一个对象
+ */
+store.$patch({
+  message: 'New Message',
+  randomMessages: ['msg1', 'msg2', 'msg3'],
+})
+
+// 这里会打印出修改后的值
+console.log(JSON.stringify(store.$state))
+// 输出 {"message":"New Message","randomMessages":["msg1","msg2","msg3"]}

对于简单的数据,直接修改成新值是非常好用的。

但有时候并不单单只是修改,而是要对数据进行拼接、补充、合并等操作,相对而言开销就会很大,这种情况下,更适合 传入一个函数 来处理。

TIP

使用这个方式时, key 只允许是实例上已有的数据,不可以提交未定义的数据进去。

强制提交的话,在 TypeScript 会抛出错误, JavaScript 虽然不会报错,但实际上, Store 实例上面依然不会有这个新增的非法数据。

传入一个函数

当参数类型为函数时,该函数会有一个入参 state ,是当前实例的 state ,等价于 store.$state ,用法如下:

ts
// 这里会打印出修改前的值
+console.log(JSON.stringify(store.$state))
+// 输出 {"message":"Hello World","randomMessages":[]}
+
+/**
+ * 注意这里,这次是传入了一个函数
+ */
+store.$patch((state) => {
+  state.message = 'New Message'
+
+  // 数组改成用追加的方式,而不是重新赋值
+  for (let i = 0; i < 3; i++) {
+    state.randomMessages.push(\`msg\${i + 1}\`)
+  }
+})
+
+// 这里会打印出修改后的值
+console.log(JSON.stringify(store.$state))
+// 输出 {"message":"New Message","randomMessages":["msg1","msg2","msg3"]}

传入一个对象 比,不一定说就是哪种方式更好,通常要结合业务场景合理使用。

TIP

使用这个方式时,和 传入一个对象 一样只能修改已定义的数据,并且另外需要注意,传进去的函数只能是同步函数,不可以是异步函数!

如果还不清楚什么是同步和异步,可以阅读 同步和异步 JavaScript - MDN 一文。

全量更新 state

批量更新 state 了解到可以用 store.$patch 方法对数据进行批量更新操作,不过如其命名,这种方式本质上是一种 “补丁更新” 。

虽然可以对所有数据都执行一次 “补丁更新” 来达到 “全量更新” 的目的,但 Pinia 也提供了一个更好的办法。

从前面多次提到 state 数据可以通过 store.$state 来拿到,而这个属性本身是可以直接赋值的。

还是继续用上面的例子, state 上现在有 messagerandomMessages 这两个数据,那么要全量更新为新的值,就这么操作:

ts
store.$state = {
+  message: 'New Message',
+  randomMessages: ['msg1', 'msg2', 'msg3'],
+}

同样的,必须遵循 state 原有的数据和对应的类型。

TIP

该操作不会使 state 失去响应性。

重置 state

Pinia 提供了一个 $reset API 挂在每个实例上面,用于重置整颗 state 树为初始数据:

ts
// 这个 store 是上面定义好的实例
+store.$reset()

具体例子:

ts
// 修改数据
+store.message = 'New Message'
+console.log(store.message) // 输出 New Message
+
+// 3s 后重置状态
+setTimeout(() => {
+  store.$reset()
+  console.log(store.message) // 输出最开始的 Hello World
+}, 3000)

订阅 state

和 Vuex 一样, Pinia 也提供了一个用于订阅 state 的 $subscribe API 。

订阅 API 的 TS 类型

在了解这个 API 的使用之前,先看一下它的 TS 类型定义:

ts
// $subscribe 部分的 TS 类型
+// ...
+$subscribe(
+  callback: SubscriptionCallback<S>,
+  options?: { detached?: boolean } & WatchOptions
+): () => void
+// ...

可以看到, $subscribe 可以接受两个参数:

  1. 第一个入参是 callback 函数,必传
  2. 第二个入参是一些选项,可选

同时还会返回一个函数,执行后可以用于移除当前订阅,下面来看看具体用法。

添加订阅

$subscribe API 的功能类似于 watch ,但它只会在 state 被更新的时候才触发一次,并且在组件被卸载时删除(参考:组件的生命周期)。

订阅 API 的 TS 类型 可以看到,它可以接受两个参数,第一个参数是必传的 callback 函数,一般情况下默认用这个方式即可,使用例子:

ts
// 可以在 state 出现变化时,更新本地持久化存储的数据
+store.$subscribe((mutation, state) => {
+  localStorage.setItem('store', JSON.stringify(state))
+})

这个 callback 里面有 2 个入参:

入参作用
mutation本次事件的一些信息
state当前实例的 state

其中 mutation 包含了以下数据:

字段
storeId发布本次订阅通知的 Pinia 实例的唯一 ID(由 创建 Store 时指定)
type有 3 个值:返回 direct 代表 直接更改 数据;返回 patch object 代表是通过 传入一个对象 更改;返回 patch function 则代表是通过 传入一个函数 更改
events触发本次订阅通知的事件列表
payload通过 传入一个函数 更改时,传递进来的荷载信息,只有 typepatch object 时才有

如果不希望组件被卸载时删除订阅,可以传递第二个参数 options 用以保留订阅状态,传入一个对象。

可以简单指定为 { detached: true }

ts
store.$subscribe(
+  (mutation, state) => {
+    // ...
+  },
+  { detached: true }
+)

也可以搭配 watch API 的选项一起用。

移除订阅

添加订阅 部分已了解过,默认情况下,组件被卸载时订阅也会被一并移除,但如果之前启用了 detached 选项,就需要手动取消了。

前面在 订阅 API 的 TS 类型 里提到,在启用 $subscribe API 之后,会有一个函数作为返回值,这个函数可以用来取消该订阅。

用法非常简单,做一下简单了解即可:

ts
// 定义一个退订变量,它是一个函数
+const unsubscribe = store.$subscribe(
+  (mutation, state) => {
+    // ...
+  },
+  { detached: true }
+)
+
+// 在合适的时期调用它,可以取消这个订阅
+unsubscribe()

跟 watch API 的机制非常相似, 它也是返回 一个取消侦听的函数 用于移除指定的 watch 。

管理 getters ~new

状态树的结构 了解过, Pinia 的 getters 是用来计算数据的。

给 Store 添加 getter

TIP

如果对 Vue 的计算数据不是很熟悉或者没接触过的话,可以先阅读 数据的计算 这一节,以便有个初步印象,不会云里雾里。

添加普通的 getter

继续用刚才的 message ,来定义一个 Getter ,用于返回一句拼接好的句子。

ts
// src/stores/index.ts
+import { defineStore } from 'pinia'
+
+export const useStore = defineStore('main', {
+  state: () => ({
+    message: 'Hello World',
+  }),
+  // 定义一个 fullMessage 的计算数据
+  getters: {
+    fullMessage: (state) => \`The message is "\${state.message}".\`,
+  },
+  // ...
+})

Options API 的 Computed 写法一样,也是通过函数来返回计算后的值, Getter 可以通过入参的 state 来拿到当前实例的数据(在 Pinia 里,官方更推荐使用箭头函数)。

添加引用 getter 的 getter

有时候可能要引用另外一个 getter 的值来返回数据,这个时候不能用箭头函数了,需要定义成普通函数而不是箭头函数,并在函数内部通过 this 来调用当前 Store 上的数据和方法。

继续在上面的例子里,添加多一个 emojiMessage 的 getter ,在返回 fullMessage 的结果的同时,拼接多一串 emoji 。

ts
export const useStore = defineStore('main', {
+  state: () => ({
+    message: 'Hello World',
+  }),
+  getters: {
+    fullMessage: (state) => \`The message is "\${state.message}".\`,
+    // 这个 getter 返回了另外一个 getter 的结果
+    emojiMessage(): string {
+      return \`🎉🎉🎉 \${this.fullMessage}\`
+    },
+  },
+})

如果只写 JavaScript ,可能对这一条所说的限制觉得很奇怪,事实上用 JS 写箭头函数来引用确实不会报错,但如果用的是 TypeScript ,不按照这个写法,在 VSCode 提示和执行 TSC 检查的时候都会给抛出一条错误:

bash
src/stores/index.ts:9:42 - error TS2339:
+Property 'fullMessage' does not exist on type '{ message: string; } & {}'.
+
+9     emojiMessage: (state) => \`🎉🎉🎉 \${state.fullMessage}\`,
+                                           ~~~~~~~~~~~
+
+
+Found 1 error in src/stores/index.ts:9

另外关于普通函数的 TS 返回类型,官方建议显式的进行标注,就像这个例子里的 emojiMessage(): string 里的 : string

给 getter 传递参数

getter 本身是不支持参数的,但和 Vuex 一样,支持返回一个具备入参的函数,用来满足需求。

ts
import { defineStore } from 'pinia'
+
+export const useStore = defineStore('main', {
+  state: () => ({
+    message: 'Hello World',
+  }),
+  getters: {
+    // 定义一个接收入参的函数作为返回值
+    signedMessage: (state) => {
+      return (name: string) => \`\${name} say: "The message is \${state.message}".\`
+    },
+  },
+})

调用的时候是这样:

ts
const signedMessage = store.signedMessage('Petter')
+console.log('signedMessage', signedMessage)
+// Petter say: "The message is Hello World".

这种情况下,这个 getter 只是调用的函数的作用,不再有缓存,如果通过变量定义了这个数据,那么这个变量也只是普通变量,不具备响应性。

ts
// 通过变量定义一个值
+const signedMessage = store.signedMessage('Petter')
+console.log('signedMessage', signedMessage)
+// Petter say: "The message is Hello World".
+
+// 2s 后改变 message
+setTimeout(() => {
+  store.message = 'New Message'
+
+  // signedMessage 不会变
+  console.log('signedMessage', signedMessage)
+  // Petter say: "The message is Hello World".
+
+  // 必须这样再次执行才能拿到更新后的值
+  console.log('signedMessage', store.signedMessage('Petter'))
+  // Petter say: "The message is New Message".
+}, 2000)

获取和更新 getter

getter 和 state 都属于数据管理,读取和赋值的方法是一样的,请参考上方 获取和更新 state 一节的内容。

管理 actions ~new

状态树的结构 提到了, Pinia 只需要用 actions 就可以解决各种数据操作,无需像 Vuex 一样区分为 mutations / actions 两大类。

给 Store 添加 action

可以为当前 Store 封装一些可以开箱即用的方法,支持同步和异步。

ts
// src/stores/index.ts
+import { defineStore } from 'pinia'
+
+export const useStore = defineStore('main', {
+  state: () => ({
+    message: 'Hello World',
+  }),
+  actions: {
+    // 异步更新 message
+    async updateMessage(newMessage: string): Promise<string> {
+      return new Promise((resolve) => {
+        setTimeout(() => {
+          // 这里的 this 是当前的 Store 实例
+          this.message = newMessage
+          resolve('Async done.')
+        }, 3000)
+      })
+    },
+    // 同步更新 message
+    updateMessageSync(newMessage: string): string {
+      // 这里的 this 是当前的 Store 实例
+      this.message = newMessage
+      return 'Sync done.'
+    },
+  },
+})

可以看到,在 action 里,如果要访问当前实例的 state 或者 getter ,只需要通过 this 即可操作,方法的入参完全不再受 Vuex 那样有固定形式的困扰。

TIP

在 action 里, this 是当前的 Store 实例,所以如果的 action 方法里有其他函数也要调用实例,请记得写成 箭头函数 来提升 this 。

调用 action

在 Pinia 中只要像普通的函数一样使用即可,不需要和 Vuex 一样调用 commit 或者 dispatch。

ts
export default defineComponent({
+  setup() {
+    const store = useStore()
+    const { message } = storeToRefs(store)
+
+    // 立即执行
+    console.log(store.updateMessageSync('New message by sync.'))
+
+    // 3s 后执行
+    store.updateMessage('New message by async.').then((res) => console.log(res))
+
+    return {
+      message,
+    }
+  },
+})

添加多个 Store ~new

到这里,对单个 Store 的配置和调用相信都已经清楚了,实际项目中会涉及到很多数据操作,还可以用多个 Store 来维护不同需求模块的数据状态。

这一点和 Vuex 的 Module 比较相似,目的都是为了避免状态树过于臃肿,但用起来会更为简单。

目录结构建议

建议统一存放在 src/stores 下面管理,根据业务需要进行命名,比如 user 就用来管理登录用户相关的状态数据。

bash
src
+└─stores
+   # 入口文件
+  ├─index.ts
+   # 多个 store
+  ├─user.ts
+  ├─game.ts
+  └─news.ts

里面暴露的方法就统一以 use 开头加上文件名,并以 Store 结尾,作为小驼峰写法,比如 user 这个 Store 文件里面导出的函数名就是:

ts
// src/stores/user.ts
+export const useUserStore = defineStore('user', {
+  // ...
+})

然后以 index.ts 里作为统一的入口文件, index.ts 里的代码写为:

ts
export * from './user'
+export * from './game'
+export * from './news'

这样在使用的时候,只需要从 @/stores 里导入即可,无需写完整的路径,例如,只需要这样:

ts
import { useUserStore } from '@/stores'

而无需这样:

ts
import { useUserStore } from '@/stores/user'

在 Vue 组件 / TS 文件里使用

这里以一个比较简单的业务场景举例,希望能够方便的理解如何同时使用多个 Store 。

假设目前有一个 userStore 是管理当前登录用户信息, gameStore 是管理游戏的信息,而 “个人中心” 这个页面需要展示 “用户信息” ,以及 “该用户绑定的游戏信息”,那么就可以这样:

ts
import { defineComponent, onMounted, ref } from 'vue'
+import { storeToRefs } from 'pinia'
+// 这里导入要用到的 Store
+import { useUserStore, useGameStore } from '@/stores'
+import type { GameItem } from '@/types'
+
+export default defineComponent({
+  setup() {
+    // 先从 userStore 获取用户信息(已经登录过,所以可以直接拿到)
+    const userStore = useUserStore()
+    const { userId, userName } = storeToRefs(userStore)
+
+    // 使用 gameStore 里的方法,传入用户 ID 去查询用户的游戏列表
+    const gameStore = useGameStore()
+    const gameList = ref<GameItem[]>([])
+    onMounted(async () => {
+      gameList.value = await gameStore.queryGameList(userId.value)
+    })
+
+    return {
+      userId,
+      userName,
+      gameList,
+    }
+  },
+})

再次提醒,切记每个 Store 的 ID 必须不同,如果 ID 重复,在同一个 Vue 组件 / TS 文件里定义 Store 实例变量的时候,会以先定义的为有效值,后续定义的会和前面一样。

如果先定义了 userStore :

ts
// 假设两个 Store 的 ID 一样
+const userStore = useUserStore() // 是想要的 Store
+const gameStore = useGameStore() // 得到的依然是 userStore 的那个 Store

如果先定义了 gameStore :

ts
// 假设两个 Store 的 ID 一样
+const gameStore = useGameStore() // 是想要的 Store
+const userStore = useUserStore() // 得到的依然是 gameStore 的那个 Store

Store 之间互相引用

如果在定义一个 Store 的时候,要引用另外一个 Store 的数据,也是很简单,回到那个 message 的例子,添加一个 getter ,它会返回一句问候语欢迎用户:

ts
// src/stores/message.ts
+import { defineStore } from 'pinia'
+
+// 导入用户信息的 Store 并启用它
+import { useUserStore } from './user'
+const userStore = useUserStore()
+
+export const useMessageStore = defineStore('message', {
+  state: () => ({
+    message: 'Hello World',
+  }),
+  getters: {
+    // 这里就可以直接引用 userStore 上面的数据了
+    greeting: () => \`Welcome, \${userStore.userName}!\`,
+  },
+})

假设现在 userName 是 Petter ,那么会得到一句对 Petter 的问候:

ts
const messageStore = useMessageStore()
+console.log(messageStore.greeting) // Welcome, Petter!

专属插件的使用 ~new

Pinia 拥有非常灵活的可扩展性,有专属插件可以开箱即用满足更多的需求场景。

如何查找插件

插件有统一的命名格式 pinia-plugin-* ,所以可以在 npmjs 上搜索这个关键词来查询目前有哪些插件已发布。

点击查询: pinia-plugin - npmjs

如何使用插件

这里以 pinia-plugin-persistedstate 为例,这是一个让数据持久化存储的 Pinia 插件。

TIP

数据持久化存储,指页面关闭后再打开,浏览器依然可以记录之前保存的本地数据,例如:浏览器原生的 localStorageIndexedDB ,或者是一些兼容多种原生方案并统一用法的第三方方案,例如: localForage

插件也是独立的 npm 包,需要先安装,再激活,然后才能使用。

激活方法会涉及到 Pinia 的初始化过程调整,这里不局限于某一个插件,通用的插件用法如下(请留意代码注释):

ts
// src/main.ts
+import { createApp } from 'vue'
+import App from '@/App.vue'
+import { createPinia } from 'pinia' // 导入 Pinia
+import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' // 导入 Pinia 插件
+
+const pinia = createPinia() // 初始化 Pinia
+pinia.use(piniaPluginPersistedstate) // 激活 Pinia 插件
+
+createApp(App)
+  .use(pinia) // 启用 Pinia ,这一次是包含了插件的 Pinia 实例
+  .mount('#app')

使用前

Pinia 默认在页面刷新时会丢失当前变更的数据,没有在本地做持久化记录:

ts
// 其他代码省略
+const store = useMessageStore()
+
+// 假设初始值是 Hello World
+setTimeout(() => {
+  // 2s 后变成 Hello World!
+  store.message = store.message + '!'
+}, 2000)
+
+// 页面刷新后又变回了 Hello World

使用后

按照 persistedstate 插件的文档说明,在其中一个 Store 启用它,只需要添加一个 persist: true 的选项即可开启:

ts
// src/stores/message.ts
+import { defineStore } from 'pinia'
+import { useUserStore } from './user'
+
+const userStore = useUserStore()
+
+export const useMessageStore = defineStore('message', {
+  state: () => ({
+    message: 'Hello World',
+  }),
+  getters: {
+    greeting: () => \`Welcome, \${userStore.userName}\`,
+  },
+  // 这是按照插件的文档,在实例上启用了该插件,这个选项是插件特有的
+  persist: true,
+})

回到的页面,现在这个 Store 具备了持久化记忆的功能了,它会从 localStorage 读取原来的数据作为初始值,每一次变化后也会将其写入 localStorage 进行记忆存储。

ts
// 其他代码省略
+const store = useMessageStore()
+
+// 假设初始值是 Hello World
+setTimeout(() => {
+  // 2s 后变成 Hello World!
+  store.message = store.message + '!'
+}, 2000)
+
+// 页面刷新后变成了 Hello World!!
+// 再次刷新后变成了 Hello World!!!
+// 再次刷新后变成了 Hello World!!!!

可以在浏览器查看到 localStorage 的存储变化,以 Chrome 浏览器为例,按 F12 ,打开 Application 面板,选择 Local Storage ,可以看到以当前 Store ID 为 Key 的存储数据。

这是其中一个插件使用的例子,更多的用法请根据自己选择的插件的 README 说明操作。

`,229);function i(C,A,d,g,u,h){const l=a("ReadingTips"),p=a("GitalkComment"),o=a("ClientOnly");return t(),c("div",null,[D,s(l),F,s(o,null,{default:r(()=>[s(p,{issueId:152})]),_:1})])}const E=e(y,[["render",i]]);export{m as __pageData,E as default}; diff --git a/assets/pinia.md.c3d0acb5.lean.js b/assets/pinia.md.c3d0acb5.lean.js new file mode 100644 index 00000000..3e8691ad --- /dev/null +++ b/assets/pinia.md.c3d0acb5.lean.js @@ -0,0 +1,465 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as e,v as t,b as c,t as s,O as r,R as n,M as a}from"./chunks/framework.0d8bea05.js";const m=JSON.parse('{"title":"全局状态管理","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"pinia.md","filePath":"pinia.md"}'),y={name:"pinia.md"},D=n('

全局状态管理

本来这部分打算放在 组件之间的通信 里,里面也简单介绍了一下 Vuex ,但 Pinia 作为被官方推荐在 Vue 3 项目里作为全局状态管理的新工具,写着写着笔者认为还是单独开一章来写会更方便阅读和理解。

官方推出的全局状态管理工具目前有 VuexPinia ,两者的作用和用法都比较相似,但 Pinia 的设计更贴近 Vue 3 组合式 API 的用法。

TIP

本章内的大部分内容都会和 Vuex 作对比,方便从 Vuex 项目向 Pinia 的迁移。

',4),F=n(`

关于 Pinia ~new

由于 Vuex 4.x 版本只是个过渡版,Vuex 4 对 TypeScript 和 Composition API 都不是很友好,虽然官方团队在 GitHub 已有讨论 Vuex 5 的开发提案,但从 2022-02-07 在 Vue 3 被设置为默认版本开始, Pinia 已正式被官方推荐作为全局状态管理的工具。

Pinia 支持 Vue 3 和 Vue 2 ,对 TypeScript 也有很完好的支持,延续本指南的宗旨,在这里只介绍基于 Vue 3 和 TypeScript 的用法。

点击访问:Pinia 官网

安装和启用 ~new

Pinia 目前还没有被广泛的默认集成在各种脚手架里,所以如果原来创建的项目没有 Pinia ,则需要手动安装它。

bash
# 需要 cd 到的项目目录下
+npm install pinia

查看的 package.json ,看看里面的 dependencies 是否成功加入了 Pinia 和它的版本号(下方是示例代码,以实际安装的最新版本号为准):

json
{
+  "dependencies": {
+    "pinia": "^2.0.11"
+  }
+}

然后打开 src/main.ts 文件,添加下面那两行有注释的新代码:

ts
import { createApp } from 'vue'
+import { createPinia } from 'pinia' // 导入 Pinia
+import App from '@/App.vue'
+
+createApp(App)
+  .use(createPinia()) // 启用 Pinia
+  .mount('#app')

到这里, Pinia 就集成到的项目里了。

TIP

也可以通过 Create Preset 创建新项目(选择 vue 技术栈进入,选择 vue3-ts-vite 模板),可以得到一个集成常用配置的项目启动模板,该模板现在使用 Pinia 作为全局状态管理工具。

状态树的结构 ~new

在开始写代码之前,先来看一个对比,直观的了解 Pinia 的状态树构成,才能在后面的环节更好的理解每个功能的用途。

鉴于可能有部分开发者之前没有用过 Vuex ,所以加入了 Vue 组件一起对比( Options API 写法)。

作用Vue ComponentVuexPinia
数据管理datastatestate
数据计算computedgettersgetters
行为方法methodsmutations / actionsactions

可以看到 Pinia 的结构和用途都和 Vuex 与 Component 非常相似,并且 Pinia 相对于 Vuex ,在行为方法部分去掉了 mutations (同步操作)和 actions (异步操作)的区分,更接近组件的结构,入门成本会更低一些。

下面来创建一个简单的 Store ,开始用 Pinia 来进行状态管理。

创建 Store ~new

和 Vuex 一样, Pinia 的核心也是称之为 Store 。

参照 Pinia 官网推荐的项目管理方案,也是先在 src 文件夹下创建一个 stores 文件夹,并在里面添加一个 index.ts 文件,然后就可以来添加一个最基础的 Store 。

Store 是通过 defineStore 方法来创建的,它有两种入参形式:

形式 1 :接收两个参数

接收两个参数,第一个参数是 Store 的唯一 ID ,第二个参数是 Store 的选项:

ts
// src/stores/index.ts
+import { defineStore } from 'pinia'
+
+export const useStore = defineStore('main', {
+  // Store 选项...
+})

形式 2 :接收一个参数

接收一个参数,直接传入 Store 的选项,但是需要把唯一 ID 作为选项的一部分一起传入:

ts
// src/stores/index.ts
+import { defineStore } from 'pinia'
+
+export const useStore = defineStore({
+  id: 'main',
+  // Store 选项...
+})

TIP

不论是哪种创建形式,都必须为 Store 指定一个唯一 ID 。

另外可以看到这里把导出的函数名命名为 useStore ,以 use 开头是 Vue 3 对可组合函数的一个命名约定。

并且使用的是 export const 而不是 export default (详见:命名导出和默认导出),这样在使用的时候可以和其他的 Vue 组合函数保持一致,都是通过 import { xxx } from 'xxx' 来导入。

如果有多个 Store ,可以分模块管理,并根据实际的功能用途进行命名( e.g. useMessageStoreuseUserStoreuseGameStore … )。

管理 state ~new

在上一小节的 状态树的结构 这里已经了解过, Pinia 是在 state 里面定义状态数据。

给 Store 添加 state

它是通过一个箭头函数的形式来返回数据,并且能够正确的帮推导 TypeScript 类型:

ts
// src/stores/index.ts
+import { defineStore } from 'pinia'
+
+export const useStore = defineStore('main', {
+  // 先定义一个最基本的 message 数据
+  state: () => ({
+    message: 'Hello World',
+  }),
+  // ...
+})

需要注意一点的是,如果不显式 return ,箭头函数的返回值需要用圆括号 () 套起来,这个是箭头函数的要求(详见:返回对象字面量)。

所以相当于这样写:

ts
// ...
+export const useStore = defineStore('main', {
+  state: () => {
+    return {
+      message: 'Hello World',
+    }
+  },
+  // ...
+})

笔者还是更喜欢加圆括号的简写方式。

TIP

可能有开发者会问: Vuex 可以用一个对象来定义 state 的数据, Pinia 可以吗?

答案是:不可以! state 的类型必须是 state?: (() => {}) | undefined ,要么不配置(就是 undefined ),要么只能是个箭头函数。

手动指定数据类型

虽然 Pinia 会帮推导 TypeScript 的数据类型,但有时候可能不太够用,比如下面这段代码,请留意代码注释的说明:

ts
// ...
+export const useStore = defineStore('main', {
+  state: () => {
+    return {
+      message: 'Hello World',
+      // 添加了一个随机消息数组
+      randomMessages: [],
+    }
+  },
+  // ...
+})

randomMessages 的预期应该是一个字符串数组 string[] ,但是这个时候 Pinia 会帮推导成 never[] ,那么类型就对不上了。

这种情况下就需要手动指定 randomMessages 的类型,可以通过 as 来指定:

ts
// ...
+export const useStore = defineStore('main', {
+  state: () => {
+    return {
+      message: 'Hello World',
+      // 通过 as 关键字指定 TS 类型
+      randomMessages: [] as string[],
+    }
+  },
+  // ...
+})

或者使用尖括号 <> 来指定:

ts
// ...
+export const useStore = defineStore('main', {
+  state: () => {
+    return {
+      message: 'Hello World',
+      // 通过尖括号指定 TS 类型
+      randomMessages: <string[]>[],
+    }
+  },
+  // ...
+})

这两种方式是等价的。

获取和更新 state

获取 state 有多种方法,略微有区别(详见下方各自的说明),但相同的是,他们都是响应性的。

WARNING

不能直接通过 ES6 解构的方式( e.g. const { message } = store ),那样会破坏数据的响应性。

使用 store 实例

用法上和 Vuex 很相似,但有一点区别是,数据直接是挂在 store 上的,而不是 store.state 上面!

TIP

e.g. Vuex 是 store.state.message , Pinia 是 store.message

所以,可以直接通过 store.message 直接调用 state 里的数据。

ts
import { defineComponent } from 'vue'
+import { useStore } from '@/stores'
+
+export default defineComponent({
+  setup() {
+    // 像 useRouter 那样定义一个变量拿到实例
+    const store = useStore()
+
+    // 直接通过实例来获取数据
+    console.log(store.message)
+
+    // 这种方式需要把整个 store 给到 template 去渲染数据
+    return {
+      store,
+    }
+  },
+})

但一些比较复杂的数据这样写会很长,所以有时候更推荐用下面介绍的 computed APIstoreToRefs API 等方式来获取。

在数据更新方面,在 Pinia 可以直接通过 Store 实例更新 state (这一点与 Vuex 有明显的不同,更改 Vuex 的 store 中的状态的唯一方法是提交 mutation),所以如果要更新 message ,只需要像下面这样,就可以更新 message 的值了!

ts
store.message = 'New Message.'

使用 computed API

现在 state 里已经有定义好的数据了,下面这段代码是在 Vue 组件里导入的 Store ,并通过计算数据 computed 拿到里面的 message 数据传给 template 使用。

vue
<script lang="ts">
+import { computed, defineComponent } from 'vue'
+import { useStore } from '@/stores'
+
+export default defineComponent({
+  setup() {
+    // 像 useRouter 那样定义一个变量拿到实例
+    const store = useStore()
+
+    // 通过计算拿到里面的数据
+    const message = computed(() => store.message)
+    console.log('message', message.value)
+
+    // 传给 template 使用
+    return {
+      message,
+    }
+  },
+})
+</script>

使用 store 实例 以及 使用 storeToRefs API 不同,这个方式默认情况下无法直接更新 state 的值。

TIP

这里的定义的 message 变量是一个只有 getter ,没有 setter 的 ComputedRef 数据,所以它是只读的。

如果要更新数据怎么办?

  1. 可以通过提前定义好的 Store Actions 方法进行更新。

  2. 在定义 computed 变量的时候,配置好 setter 的行为:

ts
// 其他代码和上一个例子一样,这里省略...
+
+// 修改:定义 computed 变量的时候配置 getter 和 setter
+const message = computed({
+  // getter 还是返回数据的值
+  get: () => store.message,
+  // 配置 setter 来定义赋值后的行为
+  set(newVal) {
+    store.message = newVal
+  },
+})
+
+// 此时不再抛出 Write operation failed: computed value is readonly 的警告
+message.value = 'New Message.'
+
+// store 上的数据已成功变成了 New Message.
+console.log(store.message)

使用 storeToRefs API

Pinia 还提供了一个 storeToRefs API 用于把 state 的数据转换为 ref 变量。

这是一个专门为 Pinia Stores 设计的 API ,类似于 toRefs ,区别在于,它会忽略掉 Store 上面的方法和非响应性的数据,只返回 state 上的响应性数据。

ts
import { defineComponent } from 'vue'
+import { useStore } from '@/stores'
+
+// 记得导入这个 API
+import { storeToRefs } from 'pinia'
+
+export default defineComponent({
+  setup() {
+    const store = useStore()
+
+    // 通过 storeToRefs 来拿到响应性的 message
+    const { message } = storeToRefs(store)
+    console.log('message', message.value)
+
+    return {
+      message,
+    }
+  },
+})

通过这个方式拿到的 message 变量是一个 Ref 类型的数据,所以可以像普通的 ref 变量一样进行读取和赋值。

ts
// 直接赋值即可
+message.value = 'New Message.'
+
+// store 上的数据已成功变成了 New Message.
+console.log(store.message)

使用 toRefs API

使用 storeToRefs API 部分所说,该 API 本身的设计就是类似于 toRefs ,所以也可以直接用 toRefs 把 state 上的数据转成 ref 变量。

ts
// 注意 toRefs 是 vue 的 API ,不是 Pinia
+import { defineComponent, toRefs } from 'vue'
+import { useStore } from '@/stores'
+
+export default defineComponent({
+  setup() {
+    const store = useStore()
+
+    // 跟 storeToRefs 操作都一样,只不过用 Vue 的这个 API 来处理
+    const { message } = toRefs(store)
+    console.log('message', message.value)
+
+    return {
+      message,
+    }
+  },
+})

详见 使用 toRefs 一节的说明,可以像普通的 ref 变量一样进行读取和赋值。

另外,像上面这样,对 store 执行 toRefs 会把 store 上面的 getters 、 actions 也一起提取,如果只需要提取 state 上的数据,可以这样做:

ts
// 只传入 store.$state
+const { message } = toRefs(store.$state)

使用 toRef API

toRef 是 toRefs 的兄弟 API ,一个是只转换一个字段,一个是转换所有字段,所以它也可以用来转换 state 数据变成 ref 变量。

ts
// 注意 toRef 是 vue 的 API ,不是 Pinia
+import { defineComponent, toRef } from 'vue'
+import { useStore } from '@/stores'
+
+export default defineComponent({
+  setup() {
+    const store = useStore()
+
+    // 遵循 toRef 的用法即可
+    const message = toRef(store, 'message')
+    console.log('message', message.value)
+
+    return {
+      message,
+    }
+  },
+})

详见 使用 toRef 一节的说明,可以像普通的 ref 变量一样进行读取和赋值。

使用 actions 方法

在 Vuex ,如果想通过方法来操作 state 的更新,必须通过 mutation 来提交;而异步操作需要更多一个步骤,必须先通过 action 来触发 mutation ,非常繁琐!

Pinia 所有操作都集合为 action ,无需区分同步和异步,按照平时的函数定义即可更新 state ,具体操作详见 管理 actions 一节。

批量更新 state

获取和更新 state 部分说的都是如何修改单个 state 数据,那么有时候要同时修改很多个,会显得比较繁琐。

如果写过 React 或者微信小程序,应该非常熟悉这些用法:

ts
// 下面不是 Vue 的代码,不要在的项目里使用
+
+// React
+this.setState({
+  foo: 'New Foo Value',
+  bar: 'New bar Value',
+})
+
+// 微信小程序
+this.setData({
+  foo: 'New Foo Value',
+  bar: 'New bar Value',
+})

Pinia 也提供了一个 $patch API 用于同时修改多个数据,它接收一个参数:

参数类型语法
partialState对象 / 函数store.$patch(partialState)

传入一个对象

当参数类型为对象时,key 是要修改的 state 数据名称, value 是新的值(支持嵌套传值),用法如下:

ts
// 继续用前面的数据,这里会打印出修改前的值
+console.log(JSON.stringify(store.$state))
+// 输出 {"message":"Hello World","randomMessages":[]}
+
+/**
+ * 注意这里,传入了一个对象
+ */
+store.$patch({
+  message: 'New Message',
+  randomMessages: ['msg1', 'msg2', 'msg3'],
+})
+
+// 这里会打印出修改后的值
+console.log(JSON.stringify(store.$state))
+// 输出 {"message":"New Message","randomMessages":["msg1","msg2","msg3"]}

对于简单的数据,直接修改成新值是非常好用的。

但有时候并不单单只是修改,而是要对数据进行拼接、补充、合并等操作,相对而言开销就会很大,这种情况下,更适合 传入一个函数 来处理。

TIP

使用这个方式时, key 只允许是实例上已有的数据,不可以提交未定义的数据进去。

强制提交的话,在 TypeScript 会抛出错误, JavaScript 虽然不会报错,但实际上, Store 实例上面依然不会有这个新增的非法数据。

传入一个函数

当参数类型为函数时,该函数会有一个入参 state ,是当前实例的 state ,等价于 store.$state ,用法如下:

ts
// 这里会打印出修改前的值
+console.log(JSON.stringify(store.$state))
+// 输出 {"message":"Hello World","randomMessages":[]}
+
+/**
+ * 注意这里,这次是传入了一个函数
+ */
+store.$patch((state) => {
+  state.message = 'New Message'
+
+  // 数组改成用追加的方式,而不是重新赋值
+  for (let i = 0; i < 3; i++) {
+    state.randomMessages.push(\`msg\${i + 1}\`)
+  }
+})
+
+// 这里会打印出修改后的值
+console.log(JSON.stringify(store.$state))
+// 输出 {"message":"New Message","randomMessages":["msg1","msg2","msg3"]}

传入一个对象 比,不一定说就是哪种方式更好,通常要结合业务场景合理使用。

TIP

使用这个方式时,和 传入一个对象 一样只能修改已定义的数据,并且另外需要注意,传进去的函数只能是同步函数,不可以是异步函数!

如果还不清楚什么是同步和异步,可以阅读 同步和异步 JavaScript - MDN 一文。

全量更新 state

批量更新 state 了解到可以用 store.$patch 方法对数据进行批量更新操作,不过如其命名,这种方式本质上是一种 “补丁更新” 。

虽然可以对所有数据都执行一次 “补丁更新” 来达到 “全量更新” 的目的,但 Pinia 也提供了一个更好的办法。

从前面多次提到 state 数据可以通过 store.$state 来拿到,而这个属性本身是可以直接赋值的。

还是继续用上面的例子, state 上现在有 messagerandomMessages 这两个数据,那么要全量更新为新的值,就这么操作:

ts
store.$state = {
+  message: 'New Message',
+  randomMessages: ['msg1', 'msg2', 'msg3'],
+}

同样的,必须遵循 state 原有的数据和对应的类型。

TIP

该操作不会使 state 失去响应性。

重置 state

Pinia 提供了一个 $reset API 挂在每个实例上面,用于重置整颗 state 树为初始数据:

ts
// 这个 store 是上面定义好的实例
+store.$reset()

具体例子:

ts
// 修改数据
+store.message = 'New Message'
+console.log(store.message) // 输出 New Message
+
+// 3s 后重置状态
+setTimeout(() => {
+  store.$reset()
+  console.log(store.message) // 输出最开始的 Hello World
+}, 3000)

订阅 state

和 Vuex 一样, Pinia 也提供了一个用于订阅 state 的 $subscribe API 。

订阅 API 的 TS 类型

在了解这个 API 的使用之前,先看一下它的 TS 类型定义:

ts
// $subscribe 部分的 TS 类型
+// ...
+$subscribe(
+  callback: SubscriptionCallback<S>,
+  options?: { detached?: boolean } & WatchOptions
+): () => void
+// ...

可以看到, $subscribe 可以接受两个参数:

  1. 第一个入参是 callback 函数,必传
  2. 第二个入参是一些选项,可选

同时还会返回一个函数,执行后可以用于移除当前订阅,下面来看看具体用法。

添加订阅

$subscribe API 的功能类似于 watch ,但它只会在 state 被更新的时候才触发一次,并且在组件被卸载时删除(参考:组件的生命周期)。

订阅 API 的 TS 类型 可以看到,它可以接受两个参数,第一个参数是必传的 callback 函数,一般情况下默认用这个方式即可,使用例子:

ts
// 可以在 state 出现变化时,更新本地持久化存储的数据
+store.$subscribe((mutation, state) => {
+  localStorage.setItem('store', JSON.stringify(state))
+})

这个 callback 里面有 2 个入参:

入参作用
mutation本次事件的一些信息
state当前实例的 state

其中 mutation 包含了以下数据:

字段
storeId发布本次订阅通知的 Pinia 实例的唯一 ID(由 创建 Store 时指定)
type有 3 个值:返回 direct 代表 直接更改 数据;返回 patch object 代表是通过 传入一个对象 更改;返回 patch function 则代表是通过 传入一个函数 更改
events触发本次订阅通知的事件列表
payload通过 传入一个函数 更改时,传递进来的荷载信息,只有 typepatch object 时才有

如果不希望组件被卸载时删除订阅,可以传递第二个参数 options 用以保留订阅状态,传入一个对象。

可以简单指定为 { detached: true }

ts
store.$subscribe(
+  (mutation, state) => {
+    // ...
+  },
+  { detached: true }
+)

也可以搭配 watch API 的选项一起用。

移除订阅

添加订阅 部分已了解过,默认情况下,组件被卸载时订阅也会被一并移除,但如果之前启用了 detached 选项,就需要手动取消了。

前面在 订阅 API 的 TS 类型 里提到,在启用 $subscribe API 之后,会有一个函数作为返回值,这个函数可以用来取消该订阅。

用法非常简单,做一下简单了解即可:

ts
// 定义一个退订变量,它是一个函数
+const unsubscribe = store.$subscribe(
+  (mutation, state) => {
+    // ...
+  },
+  { detached: true }
+)
+
+// 在合适的时期调用它,可以取消这个订阅
+unsubscribe()

跟 watch API 的机制非常相似, 它也是返回 一个取消侦听的函数 用于移除指定的 watch 。

管理 getters ~new

状态树的结构 了解过, Pinia 的 getters 是用来计算数据的。

给 Store 添加 getter

TIP

如果对 Vue 的计算数据不是很熟悉或者没接触过的话,可以先阅读 数据的计算 这一节,以便有个初步印象,不会云里雾里。

添加普通的 getter

继续用刚才的 message ,来定义一个 Getter ,用于返回一句拼接好的句子。

ts
// src/stores/index.ts
+import { defineStore } from 'pinia'
+
+export const useStore = defineStore('main', {
+  state: () => ({
+    message: 'Hello World',
+  }),
+  // 定义一个 fullMessage 的计算数据
+  getters: {
+    fullMessage: (state) => \`The message is "\${state.message}".\`,
+  },
+  // ...
+})

Options API 的 Computed 写法一样,也是通过函数来返回计算后的值, Getter 可以通过入参的 state 来拿到当前实例的数据(在 Pinia 里,官方更推荐使用箭头函数)。

添加引用 getter 的 getter

有时候可能要引用另外一个 getter 的值来返回数据,这个时候不能用箭头函数了,需要定义成普通函数而不是箭头函数,并在函数内部通过 this 来调用当前 Store 上的数据和方法。

继续在上面的例子里,添加多一个 emojiMessage 的 getter ,在返回 fullMessage 的结果的同时,拼接多一串 emoji 。

ts
export const useStore = defineStore('main', {
+  state: () => ({
+    message: 'Hello World',
+  }),
+  getters: {
+    fullMessage: (state) => \`The message is "\${state.message}".\`,
+    // 这个 getter 返回了另外一个 getter 的结果
+    emojiMessage(): string {
+      return \`🎉🎉🎉 \${this.fullMessage}\`
+    },
+  },
+})

如果只写 JavaScript ,可能对这一条所说的限制觉得很奇怪,事实上用 JS 写箭头函数来引用确实不会报错,但如果用的是 TypeScript ,不按照这个写法,在 VSCode 提示和执行 TSC 检查的时候都会给抛出一条错误:

bash
src/stores/index.ts:9:42 - error TS2339:
+Property 'fullMessage' does not exist on type '{ message: string; } & {}'.
+
+9     emojiMessage: (state) => \`🎉🎉🎉 \${state.fullMessage}\`,
+                                           ~~~~~~~~~~~
+
+
+Found 1 error in src/stores/index.ts:9

另外关于普通函数的 TS 返回类型,官方建议显式的进行标注,就像这个例子里的 emojiMessage(): string 里的 : string

给 getter 传递参数

getter 本身是不支持参数的,但和 Vuex 一样,支持返回一个具备入参的函数,用来满足需求。

ts
import { defineStore } from 'pinia'
+
+export const useStore = defineStore('main', {
+  state: () => ({
+    message: 'Hello World',
+  }),
+  getters: {
+    // 定义一个接收入参的函数作为返回值
+    signedMessage: (state) => {
+      return (name: string) => \`\${name} say: "The message is \${state.message}".\`
+    },
+  },
+})

调用的时候是这样:

ts
const signedMessage = store.signedMessage('Petter')
+console.log('signedMessage', signedMessage)
+// Petter say: "The message is Hello World".

这种情况下,这个 getter 只是调用的函数的作用,不再有缓存,如果通过变量定义了这个数据,那么这个变量也只是普通变量,不具备响应性。

ts
// 通过变量定义一个值
+const signedMessage = store.signedMessage('Petter')
+console.log('signedMessage', signedMessage)
+// Petter say: "The message is Hello World".
+
+// 2s 后改变 message
+setTimeout(() => {
+  store.message = 'New Message'
+
+  // signedMessage 不会变
+  console.log('signedMessage', signedMessage)
+  // Petter say: "The message is Hello World".
+
+  // 必须这样再次执行才能拿到更新后的值
+  console.log('signedMessage', store.signedMessage('Petter'))
+  // Petter say: "The message is New Message".
+}, 2000)

获取和更新 getter

getter 和 state 都属于数据管理,读取和赋值的方法是一样的,请参考上方 获取和更新 state 一节的内容。

管理 actions ~new

状态树的结构 提到了, Pinia 只需要用 actions 就可以解决各种数据操作,无需像 Vuex 一样区分为 mutations / actions 两大类。

给 Store 添加 action

可以为当前 Store 封装一些可以开箱即用的方法,支持同步和异步。

ts
// src/stores/index.ts
+import { defineStore } from 'pinia'
+
+export const useStore = defineStore('main', {
+  state: () => ({
+    message: 'Hello World',
+  }),
+  actions: {
+    // 异步更新 message
+    async updateMessage(newMessage: string): Promise<string> {
+      return new Promise((resolve) => {
+        setTimeout(() => {
+          // 这里的 this 是当前的 Store 实例
+          this.message = newMessage
+          resolve('Async done.')
+        }, 3000)
+      })
+    },
+    // 同步更新 message
+    updateMessageSync(newMessage: string): string {
+      // 这里的 this 是当前的 Store 实例
+      this.message = newMessage
+      return 'Sync done.'
+    },
+  },
+})

可以看到,在 action 里,如果要访问当前实例的 state 或者 getter ,只需要通过 this 即可操作,方法的入参完全不再受 Vuex 那样有固定形式的困扰。

TIP

在 action 里, this 是当前的 Store 实例,所以如果的 action 方法里有其他函数也要调用实例,请记得写成 箭头函数 来提升 this 。

调用 action

在 Pinia 中只要像普通的函数一样使用即可,不需要和 Vuex 一样调用 commit 或者 dispatch。

ts
export default defineComponent({
+  setup() {
+    const store = useStore()
+    const { message } = storeToRefs(store)
+
+    // 立即执行
+    console.log(store.updateMessageSync('New message by sync.'))
+
+    // 3s 后执行
+    store.updateMessage('New message by async.').then((res) => console.log(res))
+
+    return {
+      message,
+    }
+  },
+})

添加多个 Store ~new

到这里,对单个 Store 的配置和调用相信都已经清楚了,实际项目中会涉及到很多数据操作,还可以用多个 Store 来维护不同需求模块的数据状态。

这一点和 Vuex 的 Module 比较相似,目的都是为了避免状态树过于臃肿,但用起来会更为简单。

目录结构建议

建议统一存放在 src/stores 下面管理,根据业务需要进行命名,比如 user 就用来管理登录用户相关的状态数据。

bash
src
+└─stores
+   # 入口文件
+  ├─index.ts
+   # 多个 store
+  ├─user.ts
+  ├─game.ts
+  └─news.ts

里面暴露的方法就统一以 use 开头加上文件名,并以 Store 结尾,作为小驼峰写法,比如 user 这个 Store 文件里面导出的函数名就是:

ts
// src/stores/user.ts
+export const useUserStore = defineStore('user', {
+  // ...
+})

然后以 index.ts 里作为统一的入口文件, index.ts 里的代码写为:

ts
export * from './user'
+export * from './game'
+export * from './news'

这样在使用的时候,只需要从 @/stores 里导入即可,无需写完整的路径,例如,只需要这样:

ts
import { useUserStore } from '@/stores'

而无需这样:

ts
import { useUserStore } from '@/stores/user'

在 Vue 组件 / TS 文件里使用

这里以一个比较简单的业务场景举例,希望能够方便的理解如何同时使用多个 Store 。

假设目前有一个 userStore 是管理当前登录用户信息, gameStore 是管理游戏的信息,而 “个人中心” 这个页面需要展示 “用户信息” ,以及 “该用户绑定的游戏信息”,那么就可以这样:

ts
import { defineComponent, onMounted, ref } from 'vue'
+import { storeToRefs } from 'pinia'
+// 这里导入要用到的 Store
+import { useUserStore, useGameStore } from '@/stores'
+import type { GameItem } from '@/types'
+
+export default defineComponent({
+  setup() {
+    // 先从 userStore 获取用户信息(已经登录过,所以可以直接拿到)
+    const userStore = useUserStore()
+    const { userId, userName } = storeToRefs(userStore)
+
+    // 使用 gameStore 里的方法,传入用户 ID 去查询用户的游戏列表
+    const gameStore = useGameStore()
+    const gameList = ref<GameItem[]>([])
+    onMounted(async () => {
+      gameList.value = await gameStore.queryGameList(userId.value)
+    })
+
+    return {
+      userId,
+      userName,
+      gameList,
+    }
+  },
+})

再次提醒,切记每个 Store 的 ID 必须不同,如果 ID 重复,在同一个 Vue 组件 / TS 文件里定义 Store 实例变量的时候,会以先定义的为有效值,后续定义的会和前面一样。

如果先定义了 userStore :

ts
// 假设两个 Store 的 ID 一样
+const userStore = useUserStore() // 是想要的 Store
+const gameStore = useGameStore() // 得到的依然是 userStore 的那个 Store

如果先定义了 gameStore :

ts
// 假设两个 Store 的 ID 一样
+const gameStore = useGameStore() // 是想要的 Store
+const userStore = useUserStore() // 得到的依然是 gameStore 的那个 Store

Store 之间互相引用

如果在定义一个 Store 的时候,要引用另外一个 Store 的数据,也是很简单,回到那个 message 的例子,添加一个 getter ,它会返回一句问候语欢迎用户:

ts
// src/stores/message.ts
+import { defineStore } from 'pinia'
+
+// 导入用户信息的 Store 并启用它
+import { useUserStore } from './user'
+const userStore = useUserStore()
+
+export const useMessageStore = defineStore('message', {
+  state: () => ({
+    message: 'Hello World',
+  }),
+  getters: {
+    // 这里就可以直接引用 userStore 上面的数据了
+    greeting: () => \`Welcome, \${userStore.userName}!\`,
+  },
+})

假设现在 userName 是 Petter ,那么会得到一句对 Petter 的问候:

ts
const messageStore = useMessageStore()
+console.log(messageStore.greeting) // Welcome, Petter!

专属插件的使用 ~new

Pinia 拥有非常灵活的可扩展性,有专属插件可以开箱即用满足更多的需求场景。

如何查找插件

插件有统一的命名格式 pinia-plugin-* ,所以可以在 npmjs 上搜索这个关键词来查询目前有哪些插件已发布。

点击查询: pinia-plugin - npmjs

如何使用插件

这里以 pinia-plugin-persistedstate 为例,这是一个让数据持久化存储的 Pinia 插件。

TIP

数据持久化存储,指页面关闭后再打开,浏览器依然可以记录之前保存的本地数据,例如:浏览器原生的 localStorageIndexedDB ,或者是一些兼容多种原生方案并统一用法的第三方方案,例如: localForage

插件也是独立的 npm 包,需要先安装,再激活,然后才能使用。

激活方法会涉及到 Pinia 的初始化过程调整,这里不局限于某一个插件,通用的插件用法如下(请留意代码注释):

ts
// src/main.ts
+import { createApp } from 'vue'
+import App from '@/App.vue'
+import { createPinia } from 'pinia' // 导入 Pinia
+import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' // 导入 Pinia 插件
+
+const pinia = createPinia() // 初始化 Pinia
+pinia.use(piniaPluginPersistedstate) // 激活 Pinia 插件
+
+createApp(App)
+  .use(pinia) // 启用 Pinia ,这一次是包含了插件的 Pinia 实例
+  .mount('#app')

使用前

Pinia 默认在页面刷新时会丢失当前变更的数据,没有在本地做持久化记录:

ts
// 其他代码省略
+const store = useMessageStore()
+
+// 假设初始值是 Hello World
+setTimeout(() => {
+  // 2s 后变成 Hello World!
+  store.message = store.message + '!'
+}, 2000)
+
+// 页面刷新后又变回了 Hello World

使用后

按照 persistedstate 插件的文档说明,在其中一个 Store 启用它,只需要添加一个 persist: true 的选项即可开启:

ts
// src/stores/message.ts
+import { defineStore } from 'pinia'
+import { useUserStore } from './user'
+
+const userStore = useUserStore()
+
+export const useMessageStore = defineStore('message', {
+  state: () => ({
+    message: 'Hello World',
+  }),
+  getters: {
+    greeting: () => \`Welcome, \${userStore.userName}\`,
+  },
+  // 这是按照插件的文档,在实例上启用了该插件,这个选项是插件特有的
+  persist: true,
+})

回到的页面,现在这个 Store 具备了持久化记忆的功能了,它会从 localStorage 读取原来的数据作为初始值,每一次变化后也会将其写入 localStorage 进行记忆存储。

ts
// 其他代码省略
+const store = useMessageStore()
+
+// 假设初始值是 Hello World
+setTimeout(() => {
+  // 2s 后变成 Hello World!
+  store.message = store.message + '!'
+}, 2000)
+
+// 页面刷新后变成了 Hello World!!
+// 再次刷新后变成了 Hello World!!!
+// 再次刷新后变成了 Hello World!!!!

可以在浏览器查看到 localStorage 的存储变化,以 Chrome 浏览器为例,按 F12 ,打开 Application 面板,选择 Local Storage ,可以看到以当前 Store ID 为 Key 的存储数据。

这是其中一个插件使用的例子,更多的用法请根据自己选择的插件的 README 说明操作。

`,229);function i(C,A,d,g,u,h){const l=a("ReadingTips"),p=a("GitalkComment"),o=a("ClientOnly");return t(),c("div",null,[D,s(l),F,s(o,null,{default:r(()=>[s(p,{issueId:152})]),_:1})])}const E=e(y,[["render",i]]);export{m as __pageData,E as default}; diff --git a/assets/plugin.md.66b7dcdd.js b/assets/plugin.md.66b7dcdd.js new file mode 100644 index 00000000..0b02b364 --- /dev/null +++ b/assets/plugin.md.66b7dcdd.js @@ -0,0 +1,791 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as r,v as y,b as i,t as s,O as p,F as a,L as D,R as o,M as n}from"./chunks/framework.0d8bea05.js";const x=JSON.parse('{"title":"插件的开发和使用","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"plugin.md","filePath":"plugin.md"}'),F={name:"plugin.md"},C=a("h1",{id:"插件的开发和使用",tabindex:"-1"},[D("插件的开发和使用 "),a("a",{class:"header-anchor",href:"#插件的开发和使用","aria-label":'Permalink to "插件的开发和使用"'},"​")],-1),A=a("p",null,"在构建 Vue 项目的过程中,离不开各种开箱即用的插件支持,用以快速完成需求,避免自己造轮子。",-1),d=a("p",null,"在 Vue 项目里,可以使用针对 Vue 定制开发的专属插件,也可以使用无框架依赖的通用 JS 插件,插件的表现形式也是丰富多彩,既可以是功能的实现,也可以是组件的封装,本章将从插件的使用到亲自开发一个小插件的过程,逐一讲解。",-1),h=o(`

插件的安装和引入

前端工程化 十分普及的今天,可以说几乎所有要用到的插件,都可以在 npmjs 上搜到,除了官方提供的包管理器 npm ,也有很多种安装方式选择。

TIP

如果还不了解什么是包和包管理器,请先阅读 了解包和插件 一节的内容。

另外,每个包管理都可以配置镜像源,提升国内的下载速度,对此也可以先阅读 配置镜像源 一节了解。

虽然对于个人开发者来说,有一个用的顺手的包管理器就足够日常开发了,但是还是有必要多了解一下不同的包管理器,因为未来可能会面对团队协作开发、为开源项目贡献代码等情况,需要遵循团队要求的包管理机制(例如使用 Monorepo 架构的团队会更青睐于 yarn 或 pnpm 的 Workspace 功能)。

通过 npm 安装

npm 是 Node.js 自带的包管理器,平时通过 npm install 命令来安装各种 npm 包(比如 npm install vue-router ),就是通过这个包管理器来安装的。

如果包的下载速度太慢,可以通过以下命令管理镜像源:

bash
# 查看下载源
+npm config get registry
+
+# 绑定下载源
+npm config set registry https://registry.npmmirror.com
+
+# 删除下载源
+npm config rm registry

TIP

npm 的 lock 文件是 package-lock.json ,如果有管理多人协作仓库的需求,可以根据实际情况把它添加至 .gitignore 文件,便于统一团队的包管理。

通过 cnpm 安装

cnpm 是阿里巴巴推出的包管理工具,安装之后默认会使用 https://registry.npmmirror.com 这个镜像源。

它的安装命令和 npm 非常一致,通过 cnpm install 命令来安装(比如 cnpm install vue-router)。

在使用它之前,需要通过 npm 命令进行全局安装:

bash
npm install -g cnpm
+
+# 或者
+# npm install -g cnpm --registry=https://registry.npmmirror.com

TIP

cnpm 不生成 lock 文件,也不会识别项目下的 lock 文件,所以还是推荐使用 npm 或者其他包管理工具,通过绑定镜像源的方式来管理项目的包。

通过 yarn 安装

yarn 也是一个常用的包管理工具,和 npm 十分相似, npmjs 上的包,也会同步到 yarnpkg

也是需要全局安装才可以使用:

bash
npm install -g yarn

但是安装命令上会有点不同, yarn 是用 add 代替 install ,用 remove 代替 uninstall ,例如:

bash
# 安装单个包
+yarn add vue-router
+
+# 安装全局包
+yarn global add typescript
+
+# 卸载包
+yarn remove vue-router

而且在运行脚本的时候,可以直接用 yarn 来代替 npm run ,例如 yarn dev 相当于 npm run dev

yarn 默认绑定的是 https://registry.yarnpkg.com 的下载源,如果包的下载速度太慢,也可以配置镜像源,但是命令有所差异:

bash
# 查看镜像源
+yarn config get registry
+
+# 绑定镜像源
+yarn config set registry https://registry.npmmirror.com
+
+# 删除镜像源(注意这里是 delete )
+yarn config delete registry

TIP

yarn 的 lock 文件是 yarn.lock ,如果有管理多人协作仓库的需求,可以根据实际情况把它添加至 .gitignore 文件,便于统一团队的包管理。

通过 pnpm 安装

pnpm 是包管理工具的一个后起之秀,主打快速的、节省磁盘空间的特色,用法跟其他包管理器很相似,没有太多的学习成本, npm 和 yarn 的命令它都支持。

也是必须先全局安装它才可以使用:

bash
npm install -g pnpm

目前 pnpm 在开源社区的使用率越来越高,包括接触最多的 Vue / Vite 团队也在逐步迁移到 pnpm 来管理依赖。

pnpm 的下载源使用的是 npm ,所以如果要绑定镜像源,按照 npm 的方式 处理就可以了。

相关阅读:

TIP

pnpm 的 lock 文件是 pnpm-lock.yaml ,如果有管理多人协作仓库的需求,可以根据实际情况把它添加至 .gitignore 文件,便于统一团队的包管理。

通过 CDN 安装

大部分插件都会提供一个 CDN 版本,让可以在 .html 文件里通过 <script> 标签引入。

比如:

html
<script src="https://unpkg.com/vue-router"></script>

插件的引入

除了 CDN 版本是直接可用之外,其他通过 npm 、 yarn 等方式安装的插件,都需要在入口文件 main.ts 或者要用到的 .vue 文件里引入,比如:

ts
import { createRouter, createWebHistory } from 'vue-router'

因为本教程都是基于工程化开发,使用的 CLI 脚手架,所以这些内容暂时不谈及 CDN 的使用方式。

通常来说会有细微差别,但影响不大,插件作者也会在插件仓库的 README 或者使用文档里进行告知。

Vue 专属插件

这里特指 Vue 插件,通过 Vue Plugins 设计规范 开发出来的插件,在 npm 上通常是以 vue-xxx 这样带有 vue 关键字的格式命名(比如 vue-baidu-analytics)。

专属插件通常分为 全局插件单组件插件,区别在于,全局版本是在 main.ts 引入后 use,而单组件版本则通常是作为一个组件在 .vue 文件里引入使用。

全局插件的使用 ~new

在本教程最最前面的时候,特地说了一个内容就是 项目初始化 ,在这里有提到过就是需要通过 use 来初始化框架、插件。

全局插件的使用,就是在 main.ts 通过 import 引入,然后通过 use 来启动初始化。

在 Vue 2 ,全局插件是通过 Vue.use(xxx) 来启动,而现在,则需要通过 createAppuse,既可以单独一行一个 use ,也可以直接链式 use 下去。

参数

use 方法支持两个参数:

参数类型作用
pluginobject | function插件,一般是在 import 时使用的名称
optionsobject插件的参数,有些插件在初始化时可以配置一定的选项

基本的写法就是像下面这样:

ts
// main.ts
+import plugin1 from 'plugin1'
+import plugin2 from 'plugin2'
+import plugin3 from 'plugin3'
+import plugin4 from 'plugin4'
+
+createApp(App)
+  .use(plugin1)
+  .use(plugin2)
+  .use(plugin3, {
+    // plugin3's options
+  })
+  .use(plugin4)
+  .mount('#app')

大部分插件到这里就可以直接启动了,个别插件可能需要通过插件 API 去手动触发,在 npm 包的详情页或者 GitHub 仓库文档上,作者一般会告知使用方法,按照说明书操作即可。

单组件插件的使用 ~new

单组件的插件,通常自己本身也是一个 Vue 组件(大部分情况下都会打包为 JS 文件,但本质上是一个 Vue 的 Component )。

单组件的引入,一般都是在需要用到的 .vue 文件里单独 import ,然后挂到 <template /> 里去渲染,下面是一段模拟代码,理解起来会比较直观:

vue
<template>
+  <!-- 放置组件的渲染标签,用于显示组件 -->
+  <ComponentExample />
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, ref } from 'vue'
+import logo from '@/assets/logo.png'
+
+// 引入单组件插件
+import ComponentExample from 'a-component-example'
+
+export default defineComponent({
+  // 挂载组件模板
+  components: {
+    ComponentExample,
+  },
+})
+</script>

参考上面的代码还有注释,应该能大概了解如何使用单组件插件了吧!

通用 JS / TS 插件

也叫普通插件,这个 “普通” 不是指功能平平无奇,而是指它们无需任何框架依赖,可以应用在任意项目中,属于独立的 Library ,比如 axiosqrcodemd5 等等,在任何技术栈都可以单独引入使用,非 Vue 专属。

通用插件的使用非常灵活,既可以全局挂载,也可以在需要用到的组件里单独引入。

组件里单独引入方式:

ts
import { defineComponent } from 'vue'
+import md5 from '@withtypes/md5'
+
+export default defineComponent({
+  setup() {
+    const md5Msg = md5('message')
+  },
+})

全局挂载方法比较特殊,因为插件本身不是专属 Vue,没有 install 接口,无法通过 use 方法直接启动,下面有一小节内容单独讲这一块的操作,详见 全局 API 挂载

本地插件 ~new

插件也不全是来自于网上,有时候针对自己的业务,涉及到一些经常用到的功能模块,也可以抽离出来封装成项目专用的插件。

封装的目的

举个例子,比如在做一个具备用户系统的网站时,会涉及到手机短信验证码模块,在开始写代码之前,需要先要考虑到这些问题:

  1. 很多操作都涉及到下发验证码的请求,比如 “登录” 、 “注册” 、 “修改手机绑定” 、 “支付验证” 等等,代码雷同,只是接口 URL 或者参数不太一样

  2. 都是需要对手机号是否有传入、手机号的格式正确性验证等一些判断

  3. 需要对接口请求成功和失败的情况做一些不同的数据返回,但要处理的数据很相似,都是用于告知调用方当前是什么情况

  4. 返回一些 Toast 告知用户当前的交互结果

TIP

如果不把这一块的业务代码抽离出来,需要在每个用到的地方都写一次,不仅繁琐,而且以后一旦产品需求有改动,维护起来就惨了。

常用的封装类型

常用的本地封装方式有两种:一种是以 通用 JS / TS 插件 的形式,一种是以 Vue 专属插件 的形式。

关于这两者的区别已经在对应的小节有所介绍,接下来来看看如何封装它们。

开发本地通用 JS / TS 插件

一般情况下会以通用类型比较常见,因为大部分都是一些比较小的功能,而且可以很方便的在不同项目之间进行复用。

TIP

注:接下来会统一称之为 “通用插件” ,不论是基于 JavaScript 编写的还是 TypeScript 编写的。

项目结构

通常会把这一类文件都归类在 src 目录下的 libs 文件夹里,代表存放的是 Library 文件( JS 项目以 .js 文件存放, TS 项目以 .ts 文件存放)。

bash
vue-demo
+ # 源码文件夹
+├─src
+  # 本地通用插件
+ └─libs
+   ├─foo.ts
+   └─bar.ts
+
+ # 其他结构这里省略…
+
+└─package.json

这样在调用的时候,可以通过 @/libs/foo 来引入,或者配置了 alias 别名,也可以使用别名导入,例如 @libs/foo

设计规范与开发案例

在设计本地通用插件的时候,需要遵循 ES Module 模块设计规范 ,并且做好必要的代码注释(用途、入参、返回值等)。

TIP

如果还没有了解过 “模块” 的概念的话,可以先阅读 了解模块化设计 一节的内容。

一般来说,会有以下三种情况需要考虑。

只有一个默认功能

如果只有一个默认的功能,那么可以使用 export default 来默认导出一个函数。

例如需要封装一个打招呼的功能:

ts
// src/libs/greet.ts
+
+/**
+ * 向对方打招呼
+ * @param name - 打招呼的目标人名
+ * @returns 向传进来的人名返回一句欢迎语
+ */
+export default function greet(name: string): string {
+  return \`Welcome, \${name}!\`
+}

在 Vue 组件里就可以这样使用:

vue
<script lang="ts">
+import { defineComponent } from 'vue'
+// 导入本地插件
+import greet from '@libs/greet'
+
+export default defineComponent({
+  setup() {
+    // 导入的名称就是这个工具的方法名,可以直接调用
+    const message = greet('Petter')
+    console.log(message) // Welcome, Petter!
+  },
+})
+</script>
是一个小工具合集

如果有很多个作用相似的函数,那么建议放在一个文件里作为一个工具合集统一管理,使用 export 来导出里面的每个函数。

例如需要封装几个通过 正则表达式 判断表单的输入内容是否符合要求的函数:

ts
// src/libs/regexp.ts
+
+/**
+ * 手机号校验
+ * @param phoneNumber - 手机号
+ * @returns true=是手机号,false=不是手机号
+ */
+export function isMob(phoneNumber: number | string): boolean {
+  return /^1[3456789]\\d{9}$/.test(String(phoneNumber))
+}
+
+/**
+ * 邮箱校验
+ * @param email - 邮箱地址
+ * @returns true=是邮箱地址,false=不是邮箱地址
+ */
+export function isEmail(email: string): boolean {
+  return /^[A-Za-z\\d]+([-_.][A-Za-z\\d]+)*@([A-Za-z\\d]+[-.])+[A-Za-z\\d]{2,4}$/.test(
+    email
+  )
+}

在 Vue 组件里就可以这样使用:

vue
<script lang="ts">
+import { defineComponent } from 'vue'
+// 需要用花括号 {} 来按照命名导出时的名称导入
+import { isMob, isEmail } from '@libs/regexp'
+
+export default defineComponent({
+  setup() {
+    // 判断是否是手机号
+    console.log(isMob('13800138000')) // true
+    console.log(isMob('123456')) // false
+
+    // 判断是否是邮箱地址
+    console.log(isEmail('example@example.com')) // true
+    console.log(isEmail('example')) // false
+  },
+})
+</script>

TIP

类似这种情况,就没有必要为 isMobisEmail 每个方法都单独保存一个文件了,只需要统一放在 regexp.ts 正则文件里维护。

包含工具及辅助函数

如果主要提供一个独立功能,但还需要提供一些额外的变量或者辅助函数用于特殊的业务场景,那么可以用 export default 导出主功能,用 export 导出其他变量或者辅助函数。

只有一个默认功能 这个打招呼例子的基础上修改一下,默认提供的是 “打招呼” 的功能,偶尔需要更热情的赞美一下,那么这个 “赞美” 行为就可以用这个方式来放到这个文件里一起维护。

ts
// src/libs/greet.ts
+
+/**
+ * 称赞对方
+ * @param name - 要称赞的目标人名
+ * @returns 向传进来的人名发出一句赞美的话
+ */
+export function praise(name: string): string {
+  return \`Oh! \${name}! It's so kind of you!\`
+}
+
+/**
+ * 向对方打招呼
+ * @param name - 打招呼的目标人名
+ * @returns 向传进来的人名发出一句欢迎语
+ */
+export default function greet(name: string): string {
+  return \`Welcome, \${name}!\`
+}

在 Vue 组件里就可以这样使用:

vue
<script lang="ts">
+import { defineComponent } from 'vue'
+// 两者可以同时导入使用
+import greet, { praise } from '@libs/greet'
+
+export default defineComponent({
+  setup() {
+    // 使用默认的打招呼
+    const message = greet('Petter')
+    console.log(message) // Welcome, Petter!
+
+    // 使用命名导出的赞美
+    const praiseMessage = praise('Petter')
+    console.log(praiseMessage) // Oh! Petter! It's so kind of you!
+  },
+})
+</script>

开发本地 Vue 专属插件

Vue 专属插件 部分已介绍过,这一类的插件只能给 Vue 使用,有时候自己的业务比较特殊,无法找到完全适用的 npm 包,那么就可以自己写一个!

项目结构

通常会把这一类文件都归类在 src 目录下的 plugins 文件夹里,代表存放的是 Plugin 文件( JS 项目以 .js 文件存放, TS 项目以 .ts 文件存放)。

bash
vue-demo
+ # 源码文件夹
+├─src
+  # 本地 Vue 插件
+ └─plugins
+   ├─foo.ts
+   └─bar.ts
+
+ # 其他结构这里省略…
+
+└─package.json

这样在调用的时候,可以通过 @/plugins/foo 来引入,或者配置了 alias 别名,也可以使用别名导入,例如 @plugins/foo

设计规范

在设计本地 Vue 插件的时候,需要遵循 Vue 官方撰写的 Vue Plugins 设计规范 ,并且做好必要的代码注释,除了标明插件 API 的 “用途、入参、返回值” 之外,最好在注释内补充一个 Example 或者 Tips 说明,功能丰富的插件最好直接写个 README 文档。

开发案例

全局插件开发并启用后,只需要在 main.ts 里导入并 use 一次,即可在所有的组件内使用插件的功能。

下面对全局插件进行一个开发示范,希望能给大家以后需要的时候提供思路参考。

TIP

单组件插件一般作为 npm 包发布,会借助 Webpack 、 Vite 或者 Rollup 单独构建,本地直接放到 components 文件夹下作为组件管理即可。

基本结构

插件支持导出两种格式的:一种是函数,一种是对象。

  1. 当导出为一个函数时, Vue 会直接调用这个函数,此时插件内部是这样子:
ts
export default function (app, options) {
+  // 逻辑代码...
+}
  1. 当导出为一个对象时,对象上面需要有一个 install 方法给 Vue , Vue 通过调用这个方法来启用插件,此时插件内部是这样子:
ts
export default {
+  install: (app, options) => {
+    // 逻辑代码...
+  },
+}

不论哪种方式,入口函数都会接受两个入参:

参数作用类型
appcreateApp 生成的实例App (从 'vue' 里导入该类型),见下方的案例演示
options插件初始化时的选项undefined 或一个对象,对象的 TS 类型由插件的选项决定

如果需要在插件初始化时传入一些必要的选项,可以定义一个对象作为 options ,这样只要在 main.tsuse 插件时传入第二个参数,插件就可以拿到它们:

ts
// src/main.ts
+createApp(App)
+  // 注意这里的第二个参数就是插件选项
+  .use(customPlugin, {
+    foo: 1,
+    bar: 2,
+  })
+  .mount('#app')
编写插件

这里以一个 自定义指令 为例,写一个用于管理自定义指令的插件,其中包含两个自定义指令:一个是判断是否有权限,一个是给文本高亮,文本高亮还支持一个插件选项。

ts
// src/plugins/directive.ts
+import type { App } from 'vue'
+
+// 插件选项的类型
+interface Options {
+  // 文本高亮选项
+  highlight?: {
+    // 默认背景色
+    backgroundColor: string
+  }
+}
+
+/**
+ * 自定义指令
+ * @description 保证插件单一职责,当前插件只用于添加自定义指令
+ */
+export default {
+  install: (app: App, options?: Options) => {
+    /**
+     * 权限控制
+     * @description 用于在功能按钮上绑定权限,没权限时会销毁或隐藏对应 DOM 节点
+     * @tips 指令传入的值是管理员的组别 id
+     * @example <div v-permission="1" />
+     */
+    app.directive('permission', (el, binding) => {
+      // 假设 1 是管理员组别的 id ,则无需处理
+      if (binding.value === 1) return
+
+      // 其他情况认为没有权限,需要隐藏掉界面上的 DOM 元素
+      if (el.parentNode) {
+        el.parentNode.removeChild(el)
+      } else {
+        el.style.display = 'none'
+      }
+    })
+
+    /**
+     * 文本高亮
+     * @description 用于给指定的 DOM 节点添加背景色,搭配文本内容形成高亮效果
+     * @tips 指令传入的值需要是合法的 CSS 颜色名称或者 Hex 值
+     * @example <div v-highlight="\`cyan\`" />
+     */
+    app.directive('highlight', (el, binding) => {
+      // 获取默认颜色
+      let defaultColor = 'unset'
+      if (
+        Object.prototype.toString.call(options) === '[object Object]' &&
+        options?.highlight?.backgroundColor
+      ) {
+        defaultColor = options.highlight.backgroundColor
+      }
+
+      // 设置背景色
+      el.style.backgroundColor =
+        typeof binding.value === 'string' ? binding.value : defaultColor
+    })
+  },
+}
启用插件

main.ts 全局启用插件,在启用的时候传入了第二个参数 “插件的选项” ,这里配置了个高亮指令的默认背景颜色:

ts
// src/main.ts
+import { createApp } from 'vue'
+import App from '@/App.vue'
+import directive from '@/plugins/directive' // 导入插件
+
+createApp(App)
+   // 自定义插件
+  .use(directive, {
+    highlight: {
+      backgroundColor: '#ddd',
+    },
+  })
+  .mount('#app')
使用插件

在 Vue 组件里使用:

vue
<template>
+  <!-- 测试 permission 指令 -->
+  <div>根据 permission 指令的判断规则:</div>
+  <div v-permission="1">这个可以显示</div>
+  <div v-permission="2">这个没有权限,会被隐藏</div>
+
+  <!-- 测试 highlight 指令 -->
+  <div>根据 highlight 指令的判断规则:</div>
+  <div v-highlight="\`cyan\`">这个是青色高亮</div>
+  <div v-highlight="\`yellow\`">这个是黄色高亮</div>
+  <div v-highlight="\`red\`">这个是红色高亮</div>
+  <div v-highlight>这个是使用插件初始化时设置的灰色</div>
+</template>

全局 API 挂载

对于一些使用频率比较高的插件方法,如果觉得在每个组件里单独导入再用很麻烦,也可以考虑将其挂载到 Vue 上,使其成为 Vue 的全局变量。

注:接下来的全局变量,都是指 Vue 环境里的全局变量,非 Window 下的全局变量。

回顾 Vue 2

在 Vue 2 ,可以通过 prototype 的方式来挂载全局变量,然后通过 this 关键字来从 Vue 原型上调用该方法。

md5 插件为例,在 main.ts 里进行全局 import,然后通过 prototype 去挂到 Vue 上。

ts
import Vue from 'vue'
+import md5 from 'md5'
+
+Vue.prototype.$md5 = md5

之后在 .vue 文件里,就可以这样去使用 md5

ts
const md5Msg = this.$md5('message')

了解 Vue 3 ~new

在 Vue 3 ,已经不再支持 prototype 这样使用了,在 main.ts 里没有了 Vue,在组件的生命周期里也没有了 this

如果依然想要挂载全局变量,需要通过全新的 globalProperties 来实现,在使用该方式之前,可以把 createApp 定义为一个变量再执行挂载。

定义全局 API ~new

如上,在配置全局变量之前,可以把初始化时的 createApp 定义为一个变量(假设为 app ),然后把需要设置为全局可用的变量或方法,挂载到 appconfig.globalProperties 上面。

ts
import md5 from 'md5'
+
+// 创建 Vue 实例
+const app = createApp(App)
+
+// 把插件的 API 挂载全局变量到实例上
+app.config.globalProperties.$md5 = md5
+
+// 也可以自己写一些全局函数去挂载
+app.config.globalProperties.$log = (text: string): void => {
+  console.log(text)
+}
+
+app.mount('#app')

全局 API 的替代方案

在 Vue 3 实际上并不是特别推荐使用全局变量,Vue 3 比较推荐按需引入使用,这也是在构建过程中可以更好的做到代码优化。

特别是针对 TypeScript , Vue 作者尤雨溪先生对于全局 API 的相关 PR 说明: Global API updates ,也是不建议在 TS 里使用。

那么确实是需要用到一些全局 API 怎么办?

对于一般的数据和方法,建议采用 Provide / Inject 方案,在根组件(通常是 App.vue )把需要作为全局使用的数据或方法 Provide 下去,在需要用到的组件里通过 Inject 即可获取到,或者使用 EventBus / Vuex / Pinia 等全局通信方案来处理。

npm 包的开发与发布

相信很多开发者都想发布一个属于自己的 npm 包,在实际的工作中,也会有一些公司出于开发上的便利,也会将一些常用的业务功能抽离为独立的 npm 包,提前掌握包的开发也是非常重要的能力,接下来将介绍如何从 0 到 1 开发一个 npm 包,并将其发布到 npmjs 上可供其他项目安装使用。

TIP

在开始本节内容之前,请先阅读或回顾以下两部分内容:

  1. 阅读 了解 package.json 一节,了解或重温 npm 包清单文件的作用
  2. 阅读 学习模块化设计 一节,了解或重温模块化开发的知识

常用的构建工具

平时项目里用到的 npm 包,也可以理解为是一种项目插件,一些很简单的包,其实就和编写 本地插件 一样,假设包的入口文件是 index.js ,那么可以直接在 index.js 里编写代码,再进行模块化导出。

其他项目里安装这个包之后就可以直接使用里面的方法了,这种方式适合非常非常简单的包,很多独立的工具函数包就是使用这种方式来编写包的源代码。

例如 is-number 这个包,每周下载量超过 6800 万次,它的源代码非常少:

js
/**
+ * 摘自 is-number 的入口文件
+ * @see https://github.com/jonschlinkert/is-number/blob/master/index.js
+ */
+module.exports = function (num) {
+  if (typeof num === 'number') {
+    return num - num === 0
+  }
+  if (typeof num === 'string' && num.trim() !== '') {
+    return Number.isFinite ? Number.isFinite(+num) : isFinite(+num)
+  }
+  return false
+}

再如 slash 这个包,每周下载量超过 5200 万次,它的源代码也是只有几行:

js
/**
+ * 摘自 slash 的入口文件
+ * @see https://github.com/sindresorhus/slash/blob/main/index.js
+ */
+export default function slash(path) {
+  const isExtendedLengthPath = /^\\\\\\\\\\?\\\\/.test(path)
+
+  if (isExtendedLengthPath) {
+    return path
+  }
+
+  return path.replace(/\\\\/g, '/')
+}

但这一类包通常是提供很基础的功能实现,更多时候需要自己开发的包更倾向于和框架、和业务挂钩,涉及到非 JavaScript 代码,例如 Vue 组件的编译、 Less 等 CSS 预处理器编译、 TypeScript 的编译等等,如果不通过构建工具来处理,那么发布后这个包的使用就会有诸多限制,需要满足和开发这个包时一样的开发环境才能使用,这对于使用者来说非常不友好。

因此大部分 npm 包的开发也需要用到构建工具来转换项目源代码,统一输出为一个兼容性更好、适用性更广的 JavaScript 文件,配合 .d.ts 文件的类型声明,使用者可以不需要特地配置就可以开箱即用,非常方便,非常友好。

传统的 Webpack 可以用来构建 npm 包文件,但按照目前更主流的技术选项,编译结果更干净更迷的当属 Rollup ,但 Rollup 需要配置很多插件功能,这对于刚接触包开发的开发者来说学习成本比较高,而 Vite 的出现则解决了这个难题,因为 Vite 的底层是基于 Rollup 来完成构建,上层则简化了很多配置上的问题,因此接下来将使用 Vite 来带领开发者入门 npm 包的开发。

TIP

在开始使用构建工具之前,请先在命令行使用 node -v 命令检查当前的 Node.js 版本号是否在构建工具的支持范围内,避免无法正常使用构建工具。

通常可以在构建工具的官网查询到其支持的 Node 版本,以 Vite 为例,可以在 Vite 官网的 Node 支持 一节了解到当前只能在 Node 14.18+ / 16+ 版本上使用 Vite 。

当构建工具所支持的 Node 版本和常用的 Node 版本出现严重冲突时,推荐使用 nvm / nvm-windows 或者 n 等 Node 版本管理工具安装多个不同版本的 Node ,即可根据开发需求很方便的切换不同版本的 Node 进行开发。

项目结构与入口文件

在动手开发具体功能之前,先把项目框架搭起来,熟悉常用的项目结构,以及如何配置项目清单信息。

TIP

当前文档所演示的 hello-lib 项目已托管至 learning-vue3/hello-lib 仓库,可使用 Git 克隆命令拉取至本地:

bash
# 从 GitHub 克隆
+git clone https://github.com/learning-vue3/hello-lib.git
+
+# 如果 GitHub 访问失败,可以从 Gitee 克隆
+git clone https://gitee.com/learning-vue3/hello-lib.git

成品项目可作为学习过程中的代码参考,但更建议按照教程的讲解步骤,从零开始亲手搭建一个新项目并完成 npm 包的开发流程,可以更有效的提升学习效果。

初始化项目

首先需要初始化一个 Node 项目,打开命令行工具,先使用 cd 命令进入平时存放项目的目录,再通过 mkdir 命令创建一个项目文件夹,这里起名为 hello-lib

bash
# 创建一个项目文件夹
+mkdir hello-lib

创建了项目文件夹之后,使用 cd 命令进入项目,执行 Node 的项目初始化命令:

bash
# 进入项目文件夹
+cd hello-lib
+
+# 执行初始化,使其成为一个 Node 项目
+npm init -y

此时 hello-lib 目录下会生成一个 package.json 文件,由于后面还需要手动调整该文件的信息,所以初始化的时候可以添加 -y 参数使用默认的初始化数据直接生成该文件,跳过答题环节。

配置包信息

对一个 npm 包来说,最重要的文件莫过于 package.json 项目清单,其中有三个字段是必填的:

字段是否必填作用
name必填npm 包的名称,遵循 项目名称的规则
version必填npm 包的版本号,遵循 语义化版本号的规则
main必填项目的入口文件,通常指向构建产物所在目录的某个文件,该文件通常包含了所有模块的导出。

如果只指定了 main 字段,则使用 requireimport 以及浏览器访问 npm 包的 CDN 时,都将默认调用该字段指定的入口文件。

如果有指定 modulebrowser 字段,则通常对应 cjs 格式的文件,对应 CommonJS 规范。
module当项目使用 import 引入 npm 包时对应的入口文件,通常指向一个 es 格式的文件,对应 ES Module 规范。
browser当项目使用了 npm 包的 CDN 链接,在浏览器访问页面时的入口文件,通常指向一个 umd 格式的文件,对应 UMD 规范。
types一个 .d.ts 类型声明文件,包含了入口文件导出的方法 / 变量的类型声明,如果项目有自带类型文件,那么在使用者在使用 TypeScript 开发的项目里,可以得到友好的类型提示
files指定发布到 npm 上的文件范围,格式为 string[] 支持配置多个文件名或者文件夹名称。

通常可以只指定构建的输出目录,例如 dist 文件夹,如果不指定,则发布的时候会把所有源代码一同发布。

其中 mainmodulebrowser 三个入口文件对应的文件格式和规范,通常都是交给构建工具处理,无需手动编写,开发者只需要维护一份源码即可编译出不同规范的 JS 文件, types 对应的类型声明文件也是由工具来输出,无需手动维护。

而其他的字段可以根据项目的性质决定是否补充,以下是 hello-lib 的基础信息示例:

json
{
+  "name": "@learning-vue3/lib",
+  "version": "1.0.0",
+  "description": "A library demo for learning-vue3.",
+  "author": "chengpeiquan <chengpeiquan@chengpeiquan.com>",
+  "homepage": "https://github.com/learning-vue3/hello-lib",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/learning-vue3/hello-lib.git"
+  },
+  "license": "MIT",
+  "files": ["dist"],
+  "main": "dist/index.cjs",
+  "module": "dist/index.mjs",
+  "browser": "dist/index.min.js",
+  "types": "dist/index.d.ts",
+  "keywords": ["library", "demo", "example"],
+  "scripts": {
+    "build": "vite build"
+  }
+}

此时 mainmodulebrowsertypes 字段对应的文件还不存在,它们将在项目执行 npm run build 构建之后才会产生。

另外,入口文件使用了不同规范对应的文件扩展名,也可以统一使用 .js 扩展名,通过文件名来区分,例如 es 格式使用 index.es.js

scripts 字段则配置了一个 build 命令,这里使用了 Vite 的构建命令来打包项目,这个过程会读取 Vite 的配置文件 vite.config.ts ,关于该文件的配置内容将在下文继续介绍。

安装开发依赖

本次的 npm 包将使用 Vite 进行构建,使用 TypeScript 编写源代码,由于 Vite 本身对 TypeScript 进行了支持,因此只需要将 Vite 安装到开发依赖:

bash
# 添加 -D 选项将其安装到 devDependencies
+npm i -D vite

添加配置文件

配置包信息 的时候已提前配置了一个 npm run build 的命令,它将运行 Vite 来构建 npm 包的入口文件。

由于 Vite 默认是构建入口文件为 HTML 的网页应用,而开发 npm 包时入口文件是 JS / TS 文件,因此需要添加一份配置文件来指定构建的选项。

以下是本次的基础配置,可以完成最基本的打包,它将输出三个不同格式的入口文件,分别对应 CommonJS 、 ES Module 和 UMD 规范,分别对应 package.json 里 mainmodulebrowser 字段指定的文件。

ts
// vite.config.ts
+import { defineConfig } from 'vite'
+
+// https://cn.vitejs.dev/config/
+export default defineConfig({
+  build: {
+    // 输出目录
+    outDir: 'dist',
+    // 构建 npm 包时需要开启 “库模式”
+    lib: {
+      // 指定入口文件
+      entry: 'src/index.ts',
+      // 输出 UMD 格式时,需要指定一个全局变量的名称
+      name: 'hello',
+      // 最终输出的格式,这里指定了三种
+      formats: ['es', 'cjs', 'umd'],
+      // 针对不同输出格式对应的文件名
+      fileName: (format) => {
+        switch (format) {
+          // ES Module 格式的文件名
+          case 'es':
+            return 'index.mjs'
+          // CommonJS 格式的文件名
+          case 'cjs':
+            return 'index.cjs'
+          // UMD 格式的文件名
+          default:
+            return 'index.min.js'
+        }
+      },
+    },
+    // 压缩混淆构建后的文件代码
+    minify: true,
+  },
+})

添加入口文件

来到这里,最基础的准备工作已完成,接下来添加入口文件并尝试编译。

添加配置文件 时已指定了入口文件为 src/index.ts ,因此需要对应的创建该文件,并写入一个简单的方法,将用它来测试打包结果:

ts
// src/index.ts
+export default function hello(name: string) {
+  console.log(\`Hello \${name}\`)
+}

在命令行执行 npm run build 命令,可以看到项目下生成了 dist 文件夹,以及三个 JavaScript 文件,此时目录结构如下:

bash
hello-lib
+ # 构建产物的输出文件夹
+├─dist
+ ├─index.cjs
+ ├─index.min.js
+ └─index.mjs
+ # 依赖文件夹
+├─node_modules
+ # 源码文件夹
+├─src
+  # 入口文件
+ └─index.ts
+ # 项目清单信息
+├─package-lock.json
+├─package.json
+ # Vite 配置文件
+└─vite.config.ts

打开 dist 目录下的文件内容,可以看到虽然源码是使用 TypeScript 编写的,但最终输出的内容是按照指定的格式转换为 JavaScript 并且被执行了压缩和混淆,在这里将它们重新格式化,来看看转换后的结果。

这是 index.cjs 的文件内容,源码被转换为 CommonJS 风格的代码:

js
// dist/index.cjs
+'use strict'
+function l(o) {
+  console.log(\`Hello \${o}\`)
+}
+module.exports = l

这是 index.mjs 的内容,源码被转换为 ES Module 风格的代码:

js
// dist/index.mjs
+function o(l) {
+  console.log(\`Hello \${l}\`)
+}
+export { o as default }

这是 index.min.js 的内容,源码被转换为 UMD 风格的代码:

js
// dist/index.min.js
+;(function (e, n) {
+  typeof exports == 'object' && typeof module < 'u'
+    ? (module.exports = n())
+    : typeof define == 'function' && define.amd
+    ? define(n)
+    : ((e = typeof globalThis < 'u' ? globalThis : e || self), (e.hello = n()))
+})(this, function () {
+  'use strict'
+  function e(n) {
+    console.log(\`Hello \${n}\`)
+  }
+  return e
+})

来到这里,准备工作已就绪,下一步将开始进入工具包和组件包的开发。

开发 npm 包

这里先从最简单的函数库开始入门包的开发,为什么说它简单呢?因为只需要编写 JavaScript 或 TypeScript 就可以很好的完成开发工作。

在理解了包的开发流程之后,如果要涉及 Vue 组件包的开发,则安装相关的 Vue 的相关依赖、 Less 等 CSS 预处理器依赖,只要满足了编译条件,就可以正常构建和发布,它们的开发流程是一样的。

编写 npm 包代码

在开发的过程中,需要遵循模块化开发的要求,当前这个演示包使用 TypeScript 编码,就需要 使用 ES Module 来设计模块 ,如果对模块化设计还没有足够的了解,请先回顾相关的内容。

先在 src 目录下创建一个名为 utils.ts 的文件,写入以下内容:

ts
// src/utils.ts
+
+/**
+ * 生成随机数
+ * @param min - 最小值
+ * @param max - 最大值
+ * @param roundingType - 四舍五入类型
+ * @returns 范围内的随机数
+ */
+export function getRandomNumber(
+  min: number = 0,
+  max: number = 100,
+  roundingType: 'round' | 'ceil' | 'floor' = 'round'
+) {
+  return Math[roundingType](Math.random() * (max - min) + min)
+}
+
+/**
+ * 生成随机布尔值
+ */
+export function getRandomBoolean() {
+  const index = getRandomNumber(0, 1)
+  return [true, false][index]
+}

这里导出了两个随机方法,其中 getRandomNumber 提供了随机数值的返回,而 getRandomBoolean 提供了随机布尔值的返回,在源代码方面, getRandomBoolean 调用了 getRandomNumber 获取随机索引。

这是一个很常见的 npm 工具包的开发思路,包里的函数都使用了细粒度的编程设计,每一个函数都是独立的功能,在必要的情况下,函数 B 可以调用函数 A 来减少代码的重复编写。

在这里, utils.ts 文件已开发完毕,接下来需要将它导出的方法提供给包的使用者,请删除入口文件 src/index.ts 原来的测试内容,并输入以下新代码:

ts
// src/index.ts
+export * from './utils'

这代表将 utils.ts 文件里导出的所有方法或者变量,再次导出去,如果有很多个 utils.ts 这样的文件, index.ts 将作为一个统一的入口,统一的导出给构建工具去编译输出。

接下来在命令行执行 npm run build ,再分别看看 dist 目录下的文件变化:

此时的 index.cjs 文件,已经按照 CommonJS 规范转换了源代码:

js
// dist/index.cjs
+'use strict'
+Object.defineProperties(exports, {
+  __esModule: { value: !0 },
+  [Symbol.toStringTag]: { value: 'Module' },
+})
+function t(e = 0, o = 100, n = 'round') {
+  return Math[n](Math.random() * (o - e) + e)
+}
+function r() {
+  const e = t(0, 1)
+  return [!0, !1][e]
+}
+exports.getRandomBoolean = r
+exports.getRandomNumber = t

index.mjs 也按照 ES Module 规范进行了转换:

js
// dist/index.mjs
+function o(n = 0, t = 100, e = 'round') {
+  return Math[e](Math.random() * (t - n) + n)
+}
+function r() {
+  const n = o(0, 1)
+  return [!0, !1][n]
+}
+export { r as getRandomBoolean, o as getRandomNumber }

index.min.js 同样正常按照 UMD 风格转换成了 JavaScript 代码:

js
// dist/index.min.js
+;(function (e, n) {
+  typeof exports == 'object' && typeof module < 'u'
+    ? n(exports)
+    : typeof define == 'function' && define.amd
+    ? define(['exports'], n)
+    : ((e = typeof globalThis < 'u' ? globalThis : e || self),
+      n((e.hello = {})))
+})(this, function (e) {
+  'use strict'
+  function n(o = 0, u = 100, d = 'round') {
+    return Math[d](Math.random() * (u - o) + o)
+  }
+  function t() {
+    const o = n(0, 1)
+    return [!0, !1][o]
+  }
+  ;(e.getRandomBoolean = t),
+    (e.getRandomNumber = n),
+    Object.defineProperties(e, {
+      __esModule: { value: !0 },
+      [Symbol.toStringTag]: { value: 'Module' },
+    })
+})

对 npm 包进行本地调试

开发或者迭代了一个 npm 包之后,不建议直接发布,可以在本地进行测试,直到没有问题了再发布到 npmjs 上供其他人使用。

npm 提供了一个 npm link 命令供开发者本地联调,假设 path/to/my-library 是一个 npm 包的项目路径, path/to/my-project 是一个调试项目的所在路径,那么通过以下步骤可以在 my-project 里本地调试 my-library 包。

TIP

关于 path/to 的典故,可以查看 #294 了解。

创建本地软链接

先在 my-library npm 包项目里执行 npm link 命令,创建 npm 包的本地软链接:

bash
# 进入 npm 包项目所在的目录
+cd path/to/my-library
+
+# 创建 npm 包的本地软链接
+npm link

运行了以上命令之后,意味着刚刚开发好的 npm 包,已经被成功添加到了 Node 的全局安装目录下,可以在命令行运行以下命令查看全局安装目录的位置:

bash
npm prefix -g

假设 {prefix} 是全局安装目录,刚刚这个包在 package.json 里的包名称是 my-library ,那么在 {prefix}/node_modules/my-library 这个目录下可以看到被软链接了一份项目代码。

TIP

软链接( Symbolic Link / Symlink / Soft Link ),是指通过指定路径来指向文件或目录,操作系统会自动将其解释为另一个文件或目录的路径,因此软链接被删除或修改不会影响源文件,而源文件的移动或者删除,不会自动更新软链接,这一点和快捷方式的作用比较类似。

自此已经对这个 npm 包完成了一次 “本地发布” ,接下来就要在调试项目里进行本地关联。

关联本地软链接

my-project 调试项目里执行语法为 npm link [<package-spec>] 的 link 命令,关联 npm 包的本地软链接。

TIP

这里的 [<package-spec>] 参数,可以是包名称,也可以是 npm 包项目所在的路径。

bash
# 进入调试项目所在的目录
+cd path/to/my-project
+
+# 通过 npm 包的包名称关联本地软链接
+npm link my-library

如果通过 npm 包名称关联失败,例如返回了如下信息:

bash
 npm link my-library
+npm ERR! code E404
+npm ERR! 404 Not Found - GET https://registry.npmjs.org/my-library - Not found
+npm ERR! 404
+npm ERR! 404  'my-library@*' is not in this registry.
+npm ERR! 404
+npm ERR! 404 Note that you can also install from a
+npm ERR! 404 tarball, folder, http url, or git url.

这种情况通常出现于本地 npm 包还没有在 npmjs 上进行过任意版本的发布,而包管理器又找不到本地全局安装目录的软链接,就会去 npm 源找,都找不到就会返回 404 的报错,针对这种情况,也可以使用 npm 包项目的路径进行关联:

bash
# 进入调试项目所在的目录
+cd path/to/my-project
+
+# 通过 npm 包的项目路径关联本地软链接
+npm link path/to/my-library

至此,就完成了调试项目对该 npm 包在本地的 “安装” ,此时在 my-project 这个调试项目的 node_modules 目录下也会创建一个软链接,指向 my-library 所在的目录。

回归当前的演示包项目,先创建一个基于 TypeScript 的 Vue 新项目作为调试项目,在关联了本地 npm 包之后,就可以在调试项目里编写如下代码,测试 npm 包里的方法是否可以正常使用:

ts
// 请将 \`@learning-vue3/lib\` 更换为实际的包名称
+import { getRandomNumber } from '@learning-vue3/lib'
+
+const num = getRandomNumber()
+console.log(num)

启动 npm run dev 的调试命令并打开本地调试页面,就可以在浏览器控制台正确的打印出了随机结果。

因为本包还支持 UMD 规范,所以也可以在 HTML 页面通过普通的 <script /> 标签直接引入 dist 目录下的文件测试将来引入 CDN 时的效果,可以在 npm 包项目下创建一个 demo 目录,并添加一个 index.html 文件到该目录下,并写入以下内容:

html
<!-- demo/index.html -->
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Library Demo</title>
+  </head>
+  <body>
+    <!-- 这里引入的是 UMD 规范的文件 -->
+    <script src="../dist/index.min.js"></script>
+    <script>
+      /**
+       * UMD 规范的文件会有一个全局变量
+       * 由 vite.config.ts 的 \`build.lib.name\` 决定
+       */
+      console.log(hello)
+
+      /**
+       * 所有的方法会挂在这个全局变量上
+       * 类似于 jQuery 的 $.xxx() 那样使用
+       */
+      const num = hello.getRandomNumber()
+      console.log(num)
+    </script>
+  </body>
+</html>

在浏览器打开该 HTML 文件并唤起控制台,一样可以看到随机结果的打印记录。

添加版权注释

很多知名项目在 Library 文件的开头都会有一段版权注释,它的作用除了声明版权归属之外,还会告知使用者关于项目的主页地址、版本号、发布日期、 BUG 反馈渠道等信息。

例如很多开发者入门前端时使用过的经典类库 jQuery :

js
// https://cdn.jsdelivr.net/npm/jquery@3.6.1/dist/jquery.js
+
+/*!
+ * jQuery JavaScript Library v3.6.1
+ * https://jquery.com/
+ *
+ * Includes Sizzle.js
+ * https://sizzlejs.com/
+ *
+ * Copyright OpenJS Foundation and other contributors
+ * Released under the MIT license
+ * https://jquery.org/license
+ *
+ * Date: 2022-08-26T17:52Z
+ */
+( function( global, factory ) {
+// ...

又如流行的 JavaScript 工具库 Lodash :

js
// https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js
+
+/**
+ * @license
+ * Lodash <https://lodash.com/>
+ * Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
+ * Released under MIT license <https://lodash.com/license>
+ * Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
+ * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+ */
+(function(){
+// ...

还有每次做轮播图一定会想到它的 Swiper :

js
// https://cdn.jsdelivr.net/npm/swiper@8.4.3/swiper-bundle.js
+
+/**
+ * Swiper 8.4.3
+ * Most modern mobile touch slider and framework
+ * with hardware accelerated transitions
+ * https://swiperjs.com
+ *
+ * Copyright 2014-2022 Vladimir Kharlampidi
+ *
+ * Released under the MIT License
+ *
+ * Released on: October 6, 2022
+ */
+(function (global, factory) {
+// ...

聪明的开发者肯定已经猜到了,这些版权注释肯定不是手动添加的,那么它们是如何自动生成的呢?

npm 社区提供了非常多开箱即用的注入插件,通常可以通过 “当前使用的构建工具名称” 加上 “plugin banner” 这样的关键字,在 npmjs 网站上搜索是否有相关的插件,以当前使用的 Vite 为例,可以通过 vite-plugin-banner 实现版权注释的自动注入。

回到 hello-lib 项目,安装该插件到 devDependencies :

bash
npm i -D vite-plugin-banner

根据插件的文档建议,打开 vite.config.ts 文件,将其导入,并通过读取 package.json 的信息来生成常用的版权注释信息:

ts
// vite.config.ts
+import { defineConfig } from 'vite'
+// 导入版权注释插件
+import banner from 'vite-plugin-banner'
+// 导入 npm 包信息
+import pkg from './package.json'
+
+// https://cn.vitejs.dev/config/
+export default defineConfig({
+  // 其他选项保持不变
+  // ...
+  plugins: [
+    // 新增 banner 插件的启用,传入 package.json 的字段信息
+    banner(
+      \`/**\\n * name: \${pkg.name}\\n * version: v\${pkg.version}\\n * description: \${pkg.description}\\n * author: \${pkg.author}\\n * homepage: \${pkg.homepage}\\n */\`
+    ),
+  ],
+})

再次运行 npm run build 命令,打开 dist 目录下的 Library 文件,可以看到都成功添加了一段版权注释:

js
// dist/index.mjs
+
+/**
+ * name: @learning-vue3/lib
+ * version: v1.0.0
+ * description: A library demo for learning-vue3.
+ * author: chengpeiquan <chengpeiquan@chengpeiquan.com>
+ * homepage: https://github.com/learning-vue3/hello-lib
+ */
+function o(n = 0, t = 100, e = 'round') {
+  return Math[e](Math.random() * (t - n) + n)
+}
+function r() {
+  const n = o(0, 1)
+  return [!0, !1][n]
+}
+export { r as getRandomBoolean, o as getRandomNumber }

这样其他开发者如果在使用过程中遇到了问题,就可以轻松找到插件作者的联系方式了!

TIP

请根据实际的 package.json 存在的字段信息调整 banner 内容。

生成 npm 包的类型声明

虽然到这里已经得到一个可以运行的 JavaScript Library 文件,在 JavaScript 项目里使用是完全没有问题的,但还不建议直接发布到 npmjs 上,因为目前的情况下在 TypeScript 项目并不能完全兼容,还需要生成一份 npm 包的类型声明文件。

为什么需要类型声明

如果在上一小节 关联本地软链接 创建 Vue 调试项目时,也是使用了 TypeScript 版本的 Vue 项目,会遇到 VSCode 在下面这句代码上:

ts
import { getRandomNumber } from '@learning-vue3/lib'

在包名称 '@learning-vue3/lib' 的位置提示了一个红色波浪线,把鼠标移上去会显示这么一段话:

无法找到模块 “@learning-vue3/lib” 的声明文件。 “D:/Project/demo/hello-lib/dist/index.cjs” 隐式拥有 "any" 类型。
尝试使用 npm i --save-dev @types/learning-vue3__lib (如果存在),或者添加一个包含 declare module '@learning-vue3/lib'; 的新声明 (.d.ts) 文件 ts(7016)

此时在命令行运行 Vue 调试项目的打包命令 npm run build ,也会遇到打包失败的报错,控制台同样反馈了这个问题:缺少声明文件。

bash
 npm run build
+
+> hello-vue3@0.0.0 build
+> vue-tsc --noEmit && vite build
+
+src/App.vue:8:30 - error TS7016: Could not find a declaration file for module '@learning-vue3/lib'. 'D:/Project/demo/hello-lib/dist/index.cjs' implicitly has an 'any' type.
+  Try \`npm i --save-dev @types/learning-vue3__lib\` if it exists or add a new declaration (.d.ts) file containing \`declare module '@learning-vue3/lib';\`
+
+8 import { getRandomNumber } from '@learning-vue3/lib'
+                               ~~~~~~~~~~~~~~~~~~~~
+
+
+Found 1 error in src/App.vue:8

虽然使用者可以按照报错提示,在调试项目下创建一个 d.ts 文件并写入以下内容来声明该 npm 包:

ts
declare module '@learning-vue3/lib'

但这需要每个使用者,或者说每个使用到这个包的项目都声明一次,对于使用者来说非常不友好, declare module 之后虽然不会报错了,但也无法获得 VSCode 对 npm 包提供的 API 进行 TS 类型的自动推导与类型提示、代码补全等功能支持。

主流的做法

细心的开发者在 npmjs 网站上搜索 npm 包时,会发现很多 npm 包在详情页的包名后面,跟随有一个蓝色的 TS 图标,鼠标移上去时,还会显示一句提示语:

This package contains built-in TypeScript declarations

`,290),u=o(`

例如上图的 @vue/reactivity , Vue 3 的响应式 API 包,就带有这个图标。

这表示带有这个图标的 npm 包,已包含内置的 TypeScript 声明,可以获得完善的 TS 类型推导和提示支持,开发过程中也可以获得完善的代码补全功能支持,提高开发效率,在 TypeScript 项目执行 npm run build 的时候也能够被成功打包。

以 @vue/reactivity 这个包为例,如果项目下安装有这个 npm 包,可以在

bash
# 基于项目根目录
+./node_modules/@vue/reactivity/dist/reactivity.d.ts

这个文件里查看 Vue 3 响应式 API 的类型声明,也可以通过该文件的 CDN 地址访问到其内容:

bash
https://cdn.jsdelivr.net/npm/@vue/reactivity@3.2.40/dist/reactivity.d.ts

生成 DTS 文件

有在 “快速上手 TypeScript ” 一章阅读过 了解 tsconfig.json 这节内容的开发者,应该对该文件有了一定的了解,如果还没有阅读过也没关系,可以先按照下方的步骤操作,接下来将分布说明如何生成 npm 包的 DTS 类型声明文件(以 .d.ts 为扩展名的文件)。

请先全局安装 typescript 这个包:

bash
npm install -g typescript

依然是在在命令行界面,回到 hello-lib 这个 npm 包项目的根目录,执行以下命令生成 tsconfig.json 文件:

bash
tsc --init

打开 tsconfig.json 文件,生成的文件里会有很多默认被注释掉的选项,请将以下几个选项取消注释,同时在 compilerOptions 字段的同级新增 include 字段,这几个选项都修改为如下配置:

json
{
+  "compilerOptions": {
+    "declaration": true,
+    "emitDeclarationOnly": true,
+    "declarationDir": "./dist"
+  },
+  "include": ["./src"]
+}

其中 compilerOptions 三个选项的意思是: .ts 源文件不编译为 .js 文件,只生成 .d.ts 文件并输出到 dist 目录; include 选项则告诉 TypeScript 编译器,只处理 src 目录下的 TS 文件。

修改完毕后,在命令行执行以下命令,它将根据 tsconfig.json 的配置对项目进行编译:

bash
tsc

可以看到现在的 dist 目录下多了 2 份 .d.ts 文件: index.d.ts 和 utils.d.ts 。

bash
hello-lib
+└─dist
+  ├─index.cjs
+  ├─index.d.ts
+  ├─index.min.js
+  ├─index.mjs
+  └─utils.d.ts

打开 dist/index.d.ts ,可以看到它的内容和 src/index.ts 是一样的,因为作为入口文件,只提供了模块的导出:

ts
// dist/index.d.ts
+export * from './utils'

再打开 dist/utils.d.ts ,可以看到它的内容如下,对比 src/utils.ts 的文件内容,它去掉了具体的功能实现,并且根据代码逻辑,转换成了 TypeScript 的类型声明:

ts
// dist/utils.d.ts
+/**
+ * 生成随机数
+ * @param min - 最小值
+ * @param max - 最大值
+ * @param roundingType - 四舍五入类型
+ * @returns 范围内的随机数
+ */
+export declare function getRandomNumber(
+  min?: number,
+  max?: number,
+  roundingType?: 'round' | 'ceil' | 'floor'
+): number
+/**
+ * 生成随机布尔值
+ */
+export declare function getRandomBoolean(): boolean

由于 hello-lib 项目的 package.json 已提前指定了类型声明文件指向:

json
{
+  "types": "dist/index.d.ts"
+}

因此可以直接回到调试 npm 包的 Vue 项目,此时 VSCode 对那句 import 语句的红色波浪线报错信息已消失不见,鼠标移到 getRandomNumber 这个方法上,也可以看到 VSCode 出现了该方法的类型提示,非常方便。

TIP

如果 VSCode 未能及时更新该包的类型,依然提示红色波浪线,可以重启 VSCode 再次查看。

再次运行 npm run build 命令构建调试项目,这一次顺利通过编译:

bash
 npm run build
+
+> hello-vue3@0.0.0 build
+> vue-tsc --noEmit && vite build
+
+vite v2.9.15 building for production...
+ 42 modules transformed.
+dist/assets/logo.03d6d6da.png             6.69 KiB
+dist/index.html                           0.42 KiB
+dist/assets/home.9a123f29.js              2.01 KiB / gzip: 1.01 KiB
+dist/assets/logo.db8b6a93.js              0.12 KiB / gzip: 0.13 KiB
+dist/assets/TransferStation.25db7d3e.js   0.29 KiB / gzip: 0.22 KiB
+dist/assets/bar.0e9da4c4.js               0.53 KiB / gzip: 0.37 KiB
+dist/assets/bar.09e673fa.css              0.22 KiB / gzip: 0.18 KiB
+dist/assets/home.6bd02f2a.css             0.62 KiB / gzip: 0.33 KiB
+dist/assets/index.60726771.css            0.47 KiB / gzip: 0.29 KiB
+dist/assets/index.aebbe022.js             79.87 KiB / gzip: 31.80 KiB

生成 DTS Bundle

初始化项目生成 DTS 文件 ,其实已经走完一个 npm 包的完整开发流程了,是可以提交发布了,但在发布之前,先介绍另外一个生成 DTS 文件的方式,可以根据实际情况选择使用。

请注意这里使用了 DTS Bundle 来称呼类型声明文件,这是因为直接使用 tsc 命令生成的 DTS 文件,是和源码目录的文件数量挂钩的,可以留意到在上一小节使用 tsc 命令生成声明文件后,在 hello-lib 项目中:

  • src 源码目录有 index.ts 和 utils.ts 两个文件
  • dist 输出目录也对应生成了 index.d.ts 和 utils.d.ts 两个文件

在一个大型项目里,源码的目录和文件非常多,意味着 DTS 文件也是非常多,这样的输出结构并不是特别友好。

在讲 npm 包对类型声明 主流的做法 的时候,提到了 Vue 响应式 API 的 npm 包是提供了一个完整的 DTS 文件,它包含了所有 API 的类型声明信息:

bash
./node_modules/@vue/reactivity/dist/reactivity.d.ts

这种将多个模块的文件内容合并为一个完整文件的行为通常称之为 Bundle ,本小节将介绍如何生成这种 DTS Bundle 文件。

继续回到 hello-lib 这个 npm 包项目,由于 tsc 本身不提供类型文件的合并,所以需要借助第三方依赖来实现,比较流行的第三方包有: dts-bundle-generatornpm-dtsdts-bundledts-generator 等等。

之前笔者在为公司开发 npm 工具包的时候都对它们进行了一轮体验,鉴于实际开发过程中遇到的一些编译问题,在这里选用问题最少的 dts-bundle-generator 进行开发演示,请先安装到 hello-lib 项目的 devDependencies :

bash
npm i -D dts-bundle-generator

dts-bundle-generator 支持在 package.json 里配置一个 script ,通过命令的形式在命令行生成 DTS Bundle ,也支持通过 JavaScript / TypeScript 编写函数来执行文件的生成,鉴于实际开发过程中使用函数生成 DTS Bundle 的场景比较多(例如 Monorepo 会有生成多个 Bundle 的使用场景),因此这里以函数的方式进行演示。

TIP

在使用 Git 等版本控制系统时,如果多个独立项目之间有关联,会把这些项目的代码都存储在同一个代码仓库集中管理,此时这个大型代码仓库就被称之为 Monorepo (其中 Mono 表示单一, Repo 是存储库 Repository 的缩写),当下许多大型项目都基于这种方法管理代码, Vue 3 在 GitHub 的代码仓库也是一个 Monorepo 。

请在 hello-lib 的根目录下,创建一个与 src 源码目录同级的 scripts 目录,用来存储源码之外的脚本函数。

将以下代码保存到 scripts 目录下,命名为 buildTypes.mjs :

js
// scripts/buildTypes.mjs
+import { writeFileSync } from 'fs'
+import { dirname, resolve } from 'path'
+import { fileURLToPath } from 'url'
+import { generateDtsBundle } from 'dts-bundle-generator'
+
+async function run() {
+  // 默认情况下 \`.mjs\` 文件需要自己声明 __dirname 变量
+  const __filename = fileURLToPath(import.meta.url)
+  const __dirname = dirname(__filename)
+
+  // 获取项目的根目录路径
+  const rootPath = resolve(__dirname, '..')
+
+  // 添加构建选项
+  // 插件要求是一个数组选项,支持多个入口文件
+  const options = [
+    {
+      filePath: resolve(rootPath, \`./src/index.ts\`),
+      output: {
+        noBanner: true,
+      },
+    },
+  ]
+
+  // 生成 DTS 文件内容
+  // 插件返回一个数组,返回的文件内容顺序同选项顺序
+  const dtses = generateDtsBundle(options, {
+    preferredConfigPath: resolve(rootPath, \`./tsconfig.json\`),
+  })
+  if (!Array.isArray(dtses) || !dtses.length) return
+
+  // 将 DTS Bundle 的内容输出成 \`.d.ts\` 文件保存到 dist 目录下
+  // 当前只有一个文件要保存,所以只取第一个下标的数据
+  const dts = dtses[0]
+  const output = resolve(rootPath, \`./dist/index.d.ts\`)
+  writeFileSync(output, dts)
+}
+run().catch((e) => {
+  console.log(e)
+})

接下来打开 hello-lib 的 package.json 文件,添加一个 build:types 的 script ,并在 build 命令中通过 && 符号设置为继发执行任务,当前所有的 scripts 如下:

json
{
+  "scripts": {
+    "build": "vite build && npm run build:types",
+    "build:types": "node scripts/buildTypes.mjs"
+  }
+}

TIP

继发执行:只有前一个任务执行成功,才继续执行下一个任务,任务与任务之间使用 && 符号连接。

接下来再运行 npm run build 命令,将在执行完 Vite 的 build 任务之后,再继续执行 DTS Bundle 的文件生成,可以看到现在的 dist 目录变成了如下,只会生成一个 .d.ts 文件:

bash
hello-lib
+└─dist
+  ├─index.cjs
+  ├─index.d.ts
+  ├─index.min.js
+  └─index.mjs

现在 index.d.ts 文件已经集合了源码目录下所有的 TS 类型,变成了如下内容:

ts
// dist/index.d.ts
+/**
+ * 生成随机数
+ * @param min - 最小值
+ * @param max - 最大值
+ * @param roundingType - 四舍五入类型
+ * @returns 范围内的随机数
+ */
+export declare function getRandomNumber(
+  min?: number,
+  max?: number,
+  roundingType?: 'round' | 'ceil' | 'floor'
+): number
+/**
+ * 生成随机布尔值
+ */
+export declare function getRandomBoolean(): boolean
+
+export {}

对于大型项目,将 DTS 文件集合为 Bundle 输出是一种主流的管理方式,非常建议使用这种方式来为 npm 包生成类型文件。

添加说明文档

作为一个完整的 npm 包,应该配备一份操作说明给使用者阅读,复杂的文档可以使用 VitePress 等文档程序独立部署,而简单的项目则只需要完善一份 README 即可。

请创建一个名为 README.md 的 Markdown 文件在项目根目录下,与 src 源码目录同级,该文件的文件名 README 推荐使用全大写,这是开源社区主流的命名方式,全大写的原因是为了与代码文件进行直观的区分。

编写 README 使用的 Markdown 是一种轻量级标记语言,可以使用易读易写的纯文本格式编写文档,以 .md 作为文件扩展名,当代码托管到 GitHub 仓库或者发布到 npmjs 等平台时, README 文件会作为项目的主页内容呈现。

为了方便学习,这里将一些常用的 Markdown 语法与 HTML 代码对比如下,可以看到书写方面非常的简洁:

Markdown 代码HTML 代码
# 一级标题<h1>一级标题</h1>
## 二级标题<h2>二级标题</h2>
### 三级标题<h3>三级标题</h3>
**加粗文本**<span style="font-weight: bold;">加粗文本</span>
[链接文本](https://example.com)<a href="https://example.com">链接文本</a>

更多的 Markdown 语法建议在 Markdown 教程网站 上学习。

下面附上一份常用的 README 模板:

md
# 项目名称
+
+写上项目用途的一句话简介。
+
+## 功能介绍
+
+1. 功能 1 一句话介绍
+2. 功能 2 一句话介绍
+3. 功能 3 一句话介绍
+
+## 在线演示
+
+如果有部署在线 demo ,可放上 demo 的访问地址。
+
+## 安装方法
+
+使用 npm : \`npm install package-name\`
+
+使用 CDN : \`https://example.com/package-name\`
+
+## 用法
+
+告诉使用者如何使用 npm 包。
+
+## 插件选项
+
+如果 npm 包是一个插件,并支持传递插件选项,在这里可以使用表格介绍选项的作用。
+
+| 选项名称 |  类型  |    作用    |
+| :------: | :----: | :--------: |
+|   foo    | string | 一句话介绍 |
+|   bar    | number | 一句话介绍 |
+
+更多内容请根据实际情况补充。

拥有完善的使用说明文档,会让 npm 包更受欢迎!

发布 npm 包

一个 npm 包开发完毕后,就可以进入发布阶段了,这一小节将讲解如何注册 npm 账号并发布到 npmjs 平台上供其他开发者下载使用。

TIP

在操作 npm 包发布之前,请先运行 npm config rm registry 命令取消 npm 镜像源的绑定,否则会发布失败,在 npm 包发布后,可以再重新 配置镜像源

注册 npm 账号

在发布 npm 包之前,请先在 npm 官网上注册一个账号:点击注册

接下来需要在命令行上登录该账号以操作发布命令,打开命令行工具,输入以下命令进行登录:

bash
npm login

按照命令行的提示输入在 npmjs 网站上注册的账号和密码即可完成登录,可以通过以下命令查看当前登录的账号名称,验证是否登录成功:

bash
npm whoami

在登录成功之后,命令行会记住账号的登录状态,以后的操作就无需每次都执行登录命令了。

TIP

以上操作也可以实用 npm adduser 命令代替,直接在命令行完成注册和登录。

将包发布到 npmjs

在 npm 上发布私有包需要进行付费,因此这里只使用公共包的发布作为演示和讲解,如果开发的是公司内部使用的 npm 包,只要源代码是私有仓库,也可以使用这种方式来发布,当前在这样做之前请先获得公司的同意。

对于一个普通命名的包,要发布到 npmjs 上非常简单,只需要执行 npm 包管理器自带的一个命令即可:

bash
npm publish

它默认会将这个包作为一个公共包发布,如果包名称合法并且没有冲突,则发布成功,可以在 npmjs 查询到,否则会返回错误信息告知原因,如果因为包名冲突导致的失败,可以尝试修改别的名称再次发布。

如果打算使用像 @vue/cli@vue/compiler-sfc 这样带有 @scope 前缀的作用域包名,需要先在 npmjs 的 创建新组织 页面创建一个组织,或者确保自己拥有 @scope 对应的组织发布权限。

@scope 作用域包默认会作为私有包发布,因此在执行发布命令的时候还需要加上一个 --access 选项,将其指定为 public 允许公开访问才可以发布成功:

bash
npm publish --access public

当前的 hello-lib 项目已发布到 npmjs ,可以查看该包的主页 @learning-vue3/lib ,也可以通过 npm 安装到项目里使用了:

bash
npm i @learning-vue3/lib

并且发布到 npmjs 上的包,都同时获得热门 CDN 服务的自动同步,可以通过包名称获取到 CDN 链接并通过 <script /> 标签引入到 HTML 页面里:

bash
# 使用 jsDelivr CDN
+https://cdn.jsdelivr.net/npm/@learning-vue3/lib
+
+# 使用 UNPKG CDN
+https://unpkg.com/@learning-vue3/lib

此时 CDN 地址对应的 npm 包文件内容,就如前文所述,调用了 package.json 里 browser 字段指定的 UMD 规范文件 dist/index.min.js

给 npm 包打 Tag

细心的开发者还会留意到,例如像 Vue 这样的包,在 npmjs 上的 版本列表 里有 Current Tags 和 Version History 的版本分类,其中 Version History 是默认的版本发布历史列表,而 Current Tags 则是在发布 npm 包的时候指定打的标签。

`,89),g=o(`

标签的好处是可以让使用者无需记住对应的版本号,而是使用一些更具备语义化的单词来安装指定版本,例如:

bash
# 安装最新版的 Vue 3 ,即截图里对应的 3.2.40 版本
+npm i vue@latest
+
+# 安装最新版的 Vue 2 ,即截图里对应的 2.7.10 版本
+npm i vue@v2-latest
+
+# 如果后续有功能更新的测试版,也可以通过标签安装
+npm i vue@beta

除了减少寻找版本号的麻烦外,一旦后续有版本更新,再次使用相同的标签安装,可以重新安装到该标签对应的最新版本,例如从 1.0.0-beta.1 升级到 1.0.0-beta.2 ,可以使用 @beta 标签再次安装来达到升级的目的。

在标签列表里,有一个 latest 的标签是发布 npm 包时自带的,对应该包最新的正式版本,安装 npm 包时如果不指定标签,则默认使用 latest 标签,以下两个安装操作是等价的:

bash
# 隐式安装 latest 标签对应的版本
+npm i vue
+
+# 显式安装 latest 标签对应的版本
+npm i vue@latest

同样的,当发布 npm 包时不指定标签,则该版本也会在发布后作为 @latest 标签对应的版本号。

其他标签则需要在发布时配合发布命令,使用 --tag 作为选项手动指定,以下命令将为普通包打上名为 alpha 的 Tag :

bash
npm publish --tag alpha

同理,如果是 @scope 作用域包也是在使用 --access 选项的情况下,继续追加一条 --tag 选项指定包的标签。

bash
npm publish --access public --tag alpha

TIP

请注意,如果是 Alpha 或者 Beta 版本,通常会在版本号上增加 -alpha.0-alpha.1 这样的 版本标识符,以便在发布正式版本的时候可以使用无标识符的相同版本号,以保证版本号在遵循 升级规则 下的连续性。

`,11);function m(f,E,b,v,B,k){const t=n("ReadingTips"),e=n("ImgWrap"),l=n("ClientOnly"),c=n("GitalkComment");return y(),i("div",null,[C,A,d,s(t),h,s(l,null,{default:p(()=>[s(e,{src:"/assets/img/npm-detail-ts-icon.jpg",alt:"注意 npm 包名称后面的 TS 图标"})]),_:1}),u,s(l,null,{default:p(()=>[s(e,{src:"/assets/img/vue-versions-on-npmjs.jpg",alt:"Vue 在 npmjs 上的版本列表"})]),_:1}),g,s(l,null,{default:p(()=>[s(c,{issueId:48})]),_:1})])}const j=r(F,[["render",m]]);export{x as __pageData,j as default}; diff --git a/assets/plugin.md.66b7dcdd.lean.js b/assets/plugin.md.66b7dcdd.lean.js new file mode 100644 index 00000000..0b02b364 --- /dev/null +++ b/assets/plugin.md.66b7dcdd.lean.js @@ -0,0 +1,791 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as r,v as y,b as i,t as s,O as p,F as a,L as D,R as o,M as n}from"./chunks/framework.0d8bea05.js";const x=JSON.parse('{"title":"插件的开发和使用","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"plugin.md","filePath":"plugin.md"}'),F={name:"plugin.md"},C=a("h1",{id:"插件的开发和使用",tabindex:"-1"},[D("插件的开发和使用 "),a("a",{class:"header-anchor",href:"#插件的开发和使用","aria-label":'Permalink to "插件的开发和使用"'},"​")],-1),A=a("p",null,"在构建 Vue 项目的过程中,离不开各种开箱即用的插件支持,用以快速完成需求,避免自己造轮子。",-1),d=a("p",null,"在 Vue 项目里,可以使用针对 Vue 定制开发的专属插件,也可以使用无框架依赖的通用 JS 插件,插件的表现形式也是丰富多彩,既可以是功能的实现,也可以是组件的封装,本章将从插件的使用到亲自开发一个小插件的过程,逐一讲解。",-1),h=o(`

插件的安装和引入

前端工程化 十分普及的今天,可以说几乎所有要用到的插件,都可以在 npmjs 上搜到,除了官方提供的包管理器 npm ,也有很多种安装方式选择。

TIP

如果还不了解什么是包和包管理器,请先阅读 了解包和插件 一节的内容。

另外,每个包管理都可以配置镜像源,提升国内的下载速度,对此也可以先阅读 配置镜像源 一节了解。

虽然对于个人开发者来说,有一个用的顺手的包管理器就足够日常开发了,但是还是有必要多了解一下不同的包管理器,因为未来可能会面对团队协作开发、为开源项目贡献代码等情况,需要遵循团队要求的包管理机制(例如使用 Monorepo 架构的团队会更青睐于 yarn 或 pnpm 的 Workspace 功能)。

通过 npm 安装

npm 是 Node.js 自带的包管理器,平时通过 npm install 命令来安装各种 npm 包(比如 npm install vue-router ),就是通过这个包管理器来安装的。

如果包的下载速度太慢,可以通过以下命令管理镜像源:

bash
# 查看下载源
+npm config get registry
+
+# 绑定下载源
+npm config set registry https://registry.npmmirror.com
+
+# 删除下载源
+npm config rm registry

TIP

npm 的 lock 文件是 package-lock.json ,如果有管理多人协作仓库的需求,可以根据实际情况把它添加至 .gitignore 文件,便于统一团队的包管理。

通过 cnpm 安装

cnpm 是阿里巴巴推出的包管理工具,安装之后默认会使用 https://registry.npmmirror.com 这个镜像源。

它的安装命令和 npm 非常一致,通过 cnpm install 命令来安装(比如 cnpm install vue-router)。

在使用它之前,需要通过 npm 命令进行全局安装:

bash
npm install -g cnpm
+
+# 或者
+# npm install -g cnpm --registry=https://registry.npmmirror.com

TIP

cnpm 不生成 lock 文件,也不会识别项目下的 lock 文件,所以还是推荐使用 npm 或者其他包管理工具,通过绑定镜像源的方式来管理项目的包。

通过 yarn 安装

yarn 也是一个常用的包管理工具,和 npm 十分相似, npmjs 上的包,也会同步到 yarnpkg

也是需要全局安装才可以使用:

bash
npm install -g yarn

但是安装命令上会有点不同, yarn 是用 add 代替 install ,用 remove 代替 uninstall ,例如:

bash
# 安装单个包
+yarn add vue-router
+
+# 安装全局包
+yarn global add typescript
+
+# 卸载包
+yarn remove vue-router

而且在运行脚本的时候,可以直接用 yarn 来代替 npm run ,例如 yarn dev 相当于 npm run dev

yarn 默认绑定的是 https://registry.yarnpkg.com 的下载源,如果包的下载速度太慢,也可以配置镜像源,但是命令有所差异:

bash
# 查看镜像源
+yarn config get registry
+
+# 绑定镜像源
+yarn config set registry https://registry.npmmirror.com
+
+# 删除镜像源(注意这里是 delete )
+yarn config delete registry

TIP

yarn 的 lock 文件是 yarn.lock ,如果有管理多人协作仓库的需求,可以根据实际情况把它添加至 .gitignore 文件,便于统一团队的包管理。

通过 pnpm 安装

pnpm 是包管理工具的一个后起之秀,主打快速的、节省磁盘空间的特色,用法跟其他包管理器很相似,没有太多的学习成本, npm 和 yarn 的命令它都支持。

也是必须先全局安装它才可以使用:

bash
npm install -g pnpm

目前 pnpm 在开源社区的使用率越来越高,包括接触最多的 Vue / Vite 团队也在逐步迁移到 pnpm 来管理依赖。

pnpm 的下载源使用的是 npm ,所以如果要绑定镜像源,按照 npm 的方式 处理就可以了。

相关阅读:

TIP

pnpm 的 lock 文件是 pnpm-lock.yaml ,如果有管理多人协作仓库的需求,可以根据实际情况把它添加至 .gitignore 文件,便于统一团队的包管理。

通过 CDN 安装

大部分插件都会提供一个 CDN 版本,让可以在 .html 文件里通过 <script> 标签引入。

比如:

html
<script src="https://unpkg.com/vue-router"></script>

插件的引入

除了 CDN 版本是直接可用之外,其他通过 npm 、 yarn 等方式安装的插件,都需要在入口文件 main.ts 或者要用到的 .vue 文件里引入,比如:

ts
import { createRouter, createWebHistory } from 'vue-router'

因为本教程都是基于工程化开发,使用的 CLI 脚手架,所以这些内容暂时不谈及 CDN 的使用方式。

通常来说会有细微差别,但影响不大,插件作者也会在插件仓库的 README 或者使用文档里进行告知。

Vue 专属插件

这里特指 Vue 插件,通过 Vue Plugins 设计规范 开发出来的插件,在 npm 上通常是以 vue-xxx 这样带有 vue 关键字的格式命名(比如 vue-baidu-analytics)。

专属插件通常分为 全局插件单组件插件,区别在于,全局版本是在 main.ts 引入后 use,而单组件版本则通常是作为一个组件在 .vue 文件里引入使用。

全局插件的使用 ~new

在本教程最最前面的时候,特地说了一个内容就是 项目初始化 ,在这里有提到过就是需要通过 use 来初始化框架、插件。

全局插件的使用,就是在 main.ts 通过 import 引入,然后通过 use 来启动初始化。

在 Vue 2 ,全局插件是通过 Vue.use(xxx) 来启动,而现在,则需要通过 createAppuse,既可以单独一行一个 use ,也可以直接链式 use 下去。

参数

use 方法支持两个参数:

参数类型作用
pluginobject | function插件,一般是在 import 时使用的名称
optionsobject插件的参数,有些插件在初始化时可以配置一定的选项

基本的写法就是像下面这样:

ts
// main.ts
+import plugin1 from 'plugin1'
+import plugin2 from 'plugin2'
+import plugin3 from 'plugin3'
+import plugin4 from 'plugin4'
+
+createApp(App)
+  .use(plugin1)
+  .use(plugin2)
+  .use(plugin3, {
+    // plugin3's options
+  })
+  .use(plugin4)
+  .mount('#app')

大部分插件到这里就可以直接启动了,个别插件可能需要通过插件 API 去手动触发,在 npm 包的详情页或者 GitHub 仓库文档上,作者一般会告知使用方法,按照说明书操作即可。

单组件插件的使用 ~new

单组件的插件,通常自己本身也是一个 Vue 组件(大部分情况下都会打包为 JS 文件,但本质上是一个 Vue 的 Component )。

单组件的引入,一般都是在需要用到的 .vue 文件里单独 import ,然后挂到 <template /> 里去渲染,下面是一段模拟代码,理解起来会比较直观:

vue
<template>
+  <!-- 放置组件的渲染标签,用于显示组件 -->
+  <ComponentExample />
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, ref } from 'vue'
+import logo from '@/assets/logo.png'
+
+// 引入单组件插件
+import ComponentExample from 'a-component-example'
+
+export default defineComponent({
+  // 挂载组件模板
+  components: {
+    ComponentExample,
+  },
+})
+</script>

参考上面的代码还有注释,应该能大概了解如何使用单组件插件了吧!

通用 JS / TS 插件

也叫普通插件,这个 “普通” 不是指功能平平无奇,而是指它们无需任何框架依赖,可以应用在任意项目中,属于独立的 Library ,比如 axiosqrcodemd5 等等,在任何技术栈都可以单独引入使用,非 Vue 专属。

通用插件的使用非常灵活,既可以全局挂载,也可以在需要用到的组件里单独引入。

组件里单独引入方式:

ts
import { defineComponent } from 'vue'
+import md5 from '@withtypes/md5'
+
+export default defineComponent({
+  setup() {
+    const md5Msg = md5('message')
+  },
+})

全局挂载方法比较特殊,因为插件本身不是专属 Vue,没有 install 接口,无法通过 use 方法直接启动,下面有一小节内容单独讲这一块的操作,详见 全局 API 挂载

本地插件 ~new

插件也不全是来自于网上,有时候针对自己的业务,涉及到一些经常用到的功能模块,也可以抽离出来封装成项目专用的插件。

封装的目的

举个例子,比如在做一个具备用户系统的网站时,会涉及到手机短信验证码模块,在开始写代码之前,需要先要考虑到这些问题:

  1. 很多操作都涉及到下发验证码的请求,比如 “登录” 、 “注册” 、 “修改手机绑定” 、 “支付验证” 等等,代码雷同,只是接口 URL 或者参数不太一样

  2. 都是需要对手机号是否有传入、手机号的格式正确性验证等一些判断

  3. 需要对接口请求成功和失败的情况做一些不同的数据返回,但要处理的数据很相似,都是用于告知调用方当前是什么情况

  4. 返回一些 Toast 告知用户当前的交互结果

TIP

如果不把这一块的业务代码抽离出来,需要在每个用到的地方都写一次,不仅繁琐,而且以后一旦产品需求有改动,维护起来就惨了。

常用的封装类型

常用的本地封装方式有两种:一种是以 通用 JS / TS 插件 的形式,一种是以 Vue 专属插件 的形式。

关于这两者的区别已经在对应的小节有所介绍,接下来来看看如何封装它们。

开发本地通用 JS / TS 插件

一般情况下会以通用类型比较常见,因为大部分都是一些比较小的功能,而且可以很方便的在不同项目之间进行复用。

TIP

注:接下来会统一称之为 “通用插件” ,不论是基于 JavaScript 编写的还是 TypeScript 编写的。

项目结构

通常会把这一类文件都归类在 src 目录下的 libs 文件夹里,代表存放的是 Library 文件( JS 项目以 .js 文件存放, TS 项目以 .ts 文件存放)。

bash
vue-demo
+ # 源码文件夹
+├─src
+  # 本地通用插件
+ └─libs
+   ├─foo.ts
+   └─bar.ts
+
+ # 其他结构这里省略…
+
+└─package.json

这样在调用的时候,可以通过 @/libs/foo 来引入,或者配置了 alias 别名,也可以使用别名导入,例如 @libs/foo

设计规范与开发案例

在设计本地通用插件的时候,需要遵循 ES Module 模块设计规范 ,并且做好必要的代码注释(用途、入参、返回值等)。

TIP

如果还没有了解过 “模块” 的概念的话,可以先阅读 了解模块化设计 一节的内容。

一般来说,会有以下三种情况需要考虑。

只有一个默认功能

如果只有一个默认的功能,那么可以使用 export default 来默认导出一个函数。

例如需要封装一个打招呼的功能:

ts
// src/libs/greet.ts
+
+/**
+ * 向对方打招呼
+ * @param name - 打招呼的目标人名
+ * @returns 向传进来的人名返回一句欢迎语
+ */
+export default function greet(name: string): string {
+  return \`Welcome, \${name}!\`
+}

在 Vue 组件里就可以这样使用:

vue
<script lang="ts">
+import { defineComponent } from 'vue'
+// 导入本地插件
+import greet from '@libs/greet'
+
+export default defineComponent({
+  setup() {
+    // 导入的名称就是这个工具的方法名,可以直接调用
+    const message = greet('Petter')
+    console.log(message) // Welcome, Petter!
+  },
+})
+</script>
是一个小工具合集

如果有很多个作用相似的函数,那么建议放在一个文件里作为一个工具合集统一管理,使用 export 来导出里面的每个函数。

例如需要封装几个通过 正则表达式 判断表单的输入内容是否符合要求的函数:

ts
// src/libs/regexp.ts
+
+/**
+ * 手机号校验
+ * @param phoneNumber - 手机号
+ * @returns true=是手机号,false=不是手机号
+ */
+export function isMob(phoneNumber: number | string): boolean {
+  return /^1[3456789]\\d{9}$/.test(String(phoneNumber))
+}
+
+/**
+ * 邮箱校验
+ * @param email - 邮箱地址
+ * @returns true=是邮箱地址,false=不是邮箱地址
+ */
+export function isEmail(email: string): boolean {
+  return /^[A-Za-z\\d]+([-_.][A-Za-z\\d]+)*@([A-Za-z\\d]+[-.])+[A-Za-z\\d]{2,4}$/.test(
+    email
+  )
+}

在 Vue 组件里就可以这样使用:

vue
<script lang="ts">
+import { defineComponent } from 'vue'
+// 需要用花括号 {} 来按照命名导出时的名称导入
+import { isMob, isEmail } from '@libs/regexp'
+
+export default defineComponent({
+  setup() {
+    // 判断是否是手机号
+    console.log(isMob('13800138000')) // true
+    console.log(isMob('123456')) // false
+
+    // 判断是否是邮箱地址
+    console.log(isEmail('example@example.com')) // true
+    console.log(isEmail('example')) // false
+  },
+})
+</script>

TIP

类似这种情况,就没有必要为 isMobisEmail 每个方法都单独保存一个文件了,只需要统一放在 regexp.ts 正则文件里维护。

包含工具及辅助函数

如果主要提供一个独立功能,但还需要提供一些额外的变量或者辅助函数用于特殊的业务场景,那么可以用 export default 导出主功能,用 export 导出其他变量或者辅助函数。

只有一个默认功能 这个打招呼例子的基础上修改一下,默认提供的是 “打招呼” 的功能,偶尔需要更热情的赞美一下,那么这个 “赞美” 行为就可以用这个方式来放到这个文件里一起维护。

ts
// src/libs/greet.ts
+
+/**
+ * 称赞对方
+ * @param name - 要称赞的目标人名
+ * @returns 向传进来的人名发出一句赞美的话
+ */
+export function praise(name: string): string {
+  return \`Oh! \${name}! It's so kind of you!\`
+}
+
+/**
+ * 向对方打招呼
+ * @param name - 打招呼的目标人名
+ * @returns 向传进来的人名发出一句欢迎语
+ */
+export default function greet(name: string): string {
+  return \`Welcome, \${name}!\`
+}

在 Vue 组件里就可以这样使用:

vue
<script lang="ts">
+import { defineComponent } from 'vue'
+// 两者可以同时导入使用
+import greet, { praise } from '@libs/greet'
+
+export default defineComponent({
+  setup() {
+    // 使用默认的打招呼
+    const message = greet('Petter')
+    console.log(message) // Welcome, Petter!
+
+    // 使用命名导出的赞美
+    const praiseMessage = praise('Petter')
+    console.log(praiseMessage) // Oh! Petter! It's so kind of you!
+  },
+})
+</script>

开发本地 Vue 专属插件

Vue 专属插件 部分已介绍过,这一类的插件只能给 Vue 使用,有时候自己的业务比较特殊,无法找到完全适用的 npm 包,那么就可以自己写一个!

项目结构

通常会把这一类文件都归类在 src 目录下的 plugins 文件夹里,代表存放的是 Plugin 文件( JS 项目以 .js 文件存放, TS 项目以 .ts 文件存放)。

bash
vue-demo
+ # 源码文件夹
+├─src
+  # 本地 Vue 插件
+ └─plugins
+   ├─foo.ts
+   └─bar.ts
+
+ # 其他结构这里省略…
+
+└─package.json

这样在调用的时候,可以通过 @/plugins/foo 来引入,或者配置了 alias 别名,也可以使用别名导入,例如 @plugins/foo

设计规范

在设计本地 Vue 插件的时候,需要遵循 Vue 官方撰写的 Vue Plugins 设计规范 ,并且做好必要的代码注释,除了标明插件 API 的 “用途、入参、返回值” 之外,最好在注释内补充一个 Example 或者 Tips 说明,功能丰富的插件最好直接写个 README 文档。

开发案例

全局插件开发并启用后,只需要在 main.ts 里导入并 use 一次,即可在所有的组件内使用插件的功能。

下面对全局插件进行一个开发示范,希望能给大家以后需要的时候提供思路参考。

TIP

单组件插件一般作为 npm 包发布,会借助 Webpack 、 Vite 或者 Rollup 单独构建,本地直接放到 components 文件夹下作为组件管理即可。

基本结构

插件支持导出两种格式的:一种是函数,一种是对象。

  1. 当导出为一个函数时, Vue 会直接调用这个函数,此时插件内部是这样子:
ts
export default function (app, options) {
+  // 逻辑代码...
+}
  1. 当导出为一个对象时,对象上面需要有一个 install 方法给 Vue , Vue 通过调用这个方法来启用插件,此时插件内部是这样子:
ts
export default {
+  install: (app, options) => {
+    // 逻辑代码...
+  },
+}

不论哪种方式,入口函数都会接受两个入参:

参数作用类型
appcreateApp 生成的实例App (从 'vue' 里导入该类型),见下方的案例演示
options插件初始化时的选项undefined 或一个对象,对象的 TS 类型由插件的选项决定

如果需要在插件初始化时传入一些必要的选项,可以定义一个对象作为 options ,这样只要在 main.tsuse 插件时传入第二个参数,插件就可以拿到它们:

ts
// src/main.ts
+createApp(App)
+  // 注意这里的第二个参数就是插件选项
+  .use(customPlugin, {
+    foo: 1,
+    bar: 2,
+  })
+  .mount('#app')
编写插件

这里以一个 自定义指令 为例,写一个用于管理自定义指令的插件,其中包含两个自定义指令:一个是判断是否有权限,一个是给文本高亮,文本高亮还支持一个插件选项。

ts
// src/plugins/directive.ts
+import type { App } from 'vue'
+
+// 插件选项的类型
+interface Options {
+  // 文本高亮选项
+  highlight?: {
+    // 默认背景色
+    backgroundColor: string
+  }
+}
+
+/**
+ * 自定义指令
+ * @description 保证插件单一职责,当前插件只用于添加自定义指令
+ */
+export default {
+  install: (app: App, options?: Options) => {
+    /**
+     * 权限控制
+     * @description 用于在功能按钮上绑定权限,没权限时会销毁或隐藏对应 DOM 节点
+     * @tips 指令传入的值是管理员的组别 id
+     * @example <div v-permission="1" />
+     */
+    app.directive('permission', (el, binding) => {
+      // 假设 1 是管理员组别的 id ,则无需处理
+      if (binding.value === 1) return
+
+      // 其他情况认为没有权限,需要隐藏掉界面上的 DOM 元素
+      if (el.parentNode) {
+        el.parentNode.removeChild(el)
+      } else {
+        el.style.display = 'none'
+      }
+    })
+
+    /**
+     * 文本高亮
+     * @description 用于给指定的 DOM 节点添加背景色,搭配文本内容形成高亮效果
+     * @tips 指令传入的值需要是合法的 CSS 颜色名称或者 Hex 值
+     * @example <div v-highlight="\`cyan\`" />
+     */
+    app.directive('highlight', (el, binding) => {
+      // 获取默认颜色
+      let defaultColor = 'unset'
+      if (
+        Object.prototype.toString.call(options) === '[object Object]' &&
+        options?.highlight?.backgroundColor
+      ) {
+        defaultColor = options.highlight.backgroundColor
+      }
+
+      // 设置背景色
+      el.style.backgroundColor =
+        typeof binding.value === 'string' ? binding.value : defaultColor
+    })
+  },
+}
启用插件

main.ts 全局启用插件,在启用的时候传入了第二个参数 “插件的选项” ,这里配置了个高亮指令的默认背景颜色:

ts
// src/main.ts
+import { createApp } from 'vue'
+import App from '@/App.vue'
+import directive from '@/plugins/directive' // 导入插件
+
+createApp(App)
+   // 自定义插件
+  .use(directive, {
+    highlight: {
+      backgroundColor: '#ddd',
+    },
+  })
+  .mount('#app')
使用插件

在 Vue 组件里使用:

vue
<template>
+  <!-- 测试 permission 指令 -->
+  <div>根据 permission 指令的判断规则:</div>
+  <div v-permission="1">这个可以显示</div>
+  <div v-permission="2">这个没有权限,会被隐藏</div>
+
+  <!-- 测试 highlight 指令 -->
+  <div>根据 highlight 指令的判断规则:</div>
+  <div v-highlight="\`cyan\`">这个是青色高亮</div>
+  <div v-highlight="\`yellow\`">这个是黄色高亮</div>
+  <div v-highlight="\`red\`">这个是红色高亮</div>
+  <div v-highlight>这个是使用插件初始化时设置的灰色</div>
+</template>

全局 API 挂载

对于一些使用频率比较高的插件方法,如果觉得在每个组件里单独导入再用很麻烦,也可以考虑将其挂载到 Vue 上,使其成为 Vue 的全局变量。

注:接下来的全局变量,都是指 Vue 环境里的全局变量,非 Window 下的全局变量。

回顾 Vue 2

在 Vue 2 ,可以通过 prototype 的方式来挂载全局变量,然后通过 this 关键字来从 Vue 原型上调用该方法。

md5 插件为例,在 main.ts 里进行全局 import,然后通过 prototype 去挂到 Vue 上。

ts
import Vue from 'vue'
+import md5 from 'md5'
+
+Vue.prototype.$md5 = md5

之后在 .vue 文件里,就可以这样去使用 md5

ts
const md5Msg = this.$md5('message')

了解 Vue 3 ~new

在 Vue 3 ,已经不再支持 prototype 这样使用了,在 main.ts 里没有了 Vue,在组件的生命周期里也没有了 this

如果依然想要挂载全局变量,需要通过全新的 globalProperties 来实现,在使用该方式之前,可以把 createApp 定义为一个变量再执行挂载。

定义全局 API ~new

如上,在配置全局变量之前,可以把初始化时的 createApp 定义为一个变量(假设为 app ),然后把需要设置为全局可用的变量或方法,挂载到 appconfig.globalProperties 上面。

ts
import md5 from 'md5'
+
+// 创建 Vue 实例
+const app = createApp(App)
+
+// 把插件的 API 挂载全局变量到实例上
+app.config.globalProperties.$md5 = md5
+
+// 也可以自己写一些全局函数去挂载
+app.config.globalProperties.$log = (text: string): void => {
+  console.log(text)
+}
+
+app.mount('#app')

全局 API 的替代方案

在 Vue 3 实际上并不是特别推荐使用全局变量,Vue 3 比较推荐按需引入使用,这也是在构建过程中可以更好的做到代码优化。

特别是针对 TypeScript , Vue 作者尤雨溪先生对于全局 API 的相关 PR 说明: Global API updates ,也是不建议在 TS 里使用。

那么确实是需要用到一些全局 API 怎么办?

对于一般的数据和方法,建议采用 Provide / Inject 方案,在根组件(通常是 App.vue )把需要作为全局使用的数据或方法 Provide 下去,在需要用到的组件里通过 Inject 即可获取到,或者使用 EventBus / Vuex / Pinia 等全局通信方案来处理。

npm 包的开发与发布

相信很多开发者都想发布一个属于自己的 npm 包,在实际的工作中,也会有一些公司出于开发上的便利,也会将一些常用的业务功能抽离为独立的 npm 包,提前掌握包的开发也是非常重要的能力,接下来将介绍如何从 0 到 1 开发一个 npm 包,并将其发布到 npmjs 上可供其他项目安装使用。

TIP

在开始本节内容之前,请先阅读或回顾以下两部分内容:

  1. 阅读 了解 package.json 一节,了解或重温 npm 包清单文件的作用
  2. 阅读 学习模块化设计 一节,了解或重温模块化开发的知识

常用的构建工具

平时项目里用到的 npm 包,也可以理解为是一种项目插件,一些很简单的包,其实就和编写 本地插件 一样,假设包的入口文件是 index.js ,那么可以直接在 index.js 里编写代码,再进行模块化导出。

其他项目里安装这个包之后就可以直接使用里面的方法了,这种方式适合非常非常简单的包,很多独立的工具函数包就是使用这种方式来编写包的源代码。

例如 is-number 这个包,每周下载量超过 6800 万次,它的源代码非常少:

js
/**
+ * 摘自 is-number 的入口文件
+ * @see https://github.com/jonschlinkert/is-number/blob/master/index.js
+ */
+module.exports = function (num) {
+  if (typeof num === 'number') {
+    return num - num === 0
+  }
+  if (typeof num === 'string' && num.trim() !== '') {
+    return Number.isFinite ? Number.isFinite(+num) : isFinite(+num)
+  }
+  return false
+}

再如 slash 这个包,每周下载量超过 5200 万次,它的源代码也是只有几行:

js
/**
+ * 摘自 slash 的入口文件
+ * @see https://github.com/sindresorhus/slash/blob/main/index.js
+ */
+export default function slash(path) {
+  const isExtendedLengthPath = /^\\\\\\\\\\?\\\\/.test(path)
+
+  if (isExtendedLengthPath) {
+    return path
+  }
+
+  return path.replace(/\\\\/g, '/')
+}

但这一类包通常是提供很基础的功能实现,更多时候需要自己开发的包更倾向于和框架、和业务挂钩,涉及到非 JavaScript 代码,例如 Vue 组件的编译、 Less 等 CSS 预处理器编译、 TypeScript 的编译等等,如果不通过构建工具来处理,那么发布后这个包的使用就会有诸多限制,需要满足和开发这个包时一样的开发环境才能使用,这对于使用者来说非常不友好。

因此大部分 npm 包的开发也需要用到构建工具来转换项目源代码,统一输出为一个兼容性更好、适用性更广的 JavaScript 文件,配合 .d.ts 文件的类型声明,使用者可以不需要特地配置就可以开箱即用,非常方便,非常友好。

传统的 Webpack 可以用来构建 npm 包文件,但按照目前更主流的技术选项,编译结果更干净更迷的当属 Rollup ,但 Rollup 需要配置很多插件功能,这对于刚接触包开发的开发者来说学习成本比较高,而 Vite 的出现则解决了这个难题,因为 Vite 的底层是基于 Rollup 来完成构建,上层则简化了很多配置上的问题,因此接下来将使用 Vite 来带领开发者入门 npm 包的开发。

TIP

在开始使用构建工具之前,请先在命令行使用 node -v 命令检查当前的 Node.js 版本号是否在构建工具的支持范围内,避免无法正常使用构建工具。

通常可以在构建工具的官网查询到其支持的 Node 版本,以 Vite 为例,可以在 Vite 官网的 Node 支持 一节了解到当前只能在 Node 14.18+ / 16+ 版本上使用 Vite 。

当构建工具所支持的 Node 版本和常用的 Node 版本出现严重冲突时,推荐使用 nvm / nvm-windows 或者 n 等 Node 版本管理工具安装多个不同版本的 Node ,即可根据开发需求很方便的切换不同版本的 Node 进行开发。

项目结构与入口文件

在动手开发具体功能之前,先把项目框架搭起来,熟悉常用的项目结构,以及如何配置项目清单信息。

TIP

当前文档所演示的 hello-lib 项目已托管至 learning-vue3/hello-lib 仓库,可使用 Git 克隆命令拉取至本地:

bash
# 从 GitHub 克隆
+git clone https://github.com/learning-vue3/hello-lib.git
+
+# 如果 GitHub 访问失败,可以从 Gitee 克隆
+git clone https://gitee.com/learning-vue3/hello-lib.git

成品项目可作为学习过程中的代码参考,但更建议按照教程的讲解步骤,从零开始亲手搭建一个新项目并完成 npm 包的开发流程,可以更有效的提升学习效果。

初始化项目

首先需要初始化一个 Node 项目,打开命令行工具,先使用 cd 命令进入平时存放项目的目录,再通过 mkdir 命令创建一个项目文件夹,这里起名为 hello-lib

bash
# 创建一个项目文件夹
+mkdir hello-lib

创建了项目文件夹之后,使用 cd 命令进入项目,执行 Node 的项目初始化命令:

bash
# 进入项目文件夹
+cd hello-lib
+
+# 执行初始化,使其成为一个 Node 项目
+npm init -y

此时 hello-lib 目录下会生成一个 package.json 文件,由于后面还需要手动调整该文件的信息,所以初始化的时候可以添加 -y 参数使用默认的初始化数据直接生成该文件,跳过答题环节。

配置包信息

对一个 npm 包来说,最重要的文件莫过于 package.json 项目清单,其中有三个字段是必填的:

字段是否必填作用
name必填npm 包的名称,遵循 项目名称的规则
version必填npm 包的版本号,遵循 语义化版本号的规则
main必填项目的入口文件,通常指向构建产物所在目录的某个文件,该文件通常包含了所有模块的导出。

如果只指定了 main 字段,则使用 requireimport 以及浏览器访问 npm 包的 CDN 时,都将默认调用该字段指定的入口文件。

如果有指定 modulebrowser 字段,则通常对应 cjs 格式的文件,对应 CommonJS 规范。
module当项目使用 import 引入 npm 包时对应的入口文件,通常指向一个 es 格式的文件,对应 ES Module 规范。
browser当项目使用了 npm 包的 CDN 链接,在浏览器访问页面时的入口文件,通常指向一个 umd 格式的文件,对应 UMD 规范。
types一个 .d.ts 类型声明文件,包含了入口文件导出的方法 / 变量的类型声明,如果项目有自带类型文件,那么在使用者在使用 TypeScript 开发的项目里,可以得到友好的类型提示
files指定发布到 npm 上的文件范围,格式为 string[] 支持配置多个文件名或者文件夹名称。

通常可以只指定构建的输出目录,例如 dist 文件夹,如果不指定,则发布的时候会把所有源代码一同发布。

其中 mainmodulebrowser 三个入口文件对应的文件格式和规范,通常都是交给构建工具处理,无需手动编写,开发者只需要维护一份源码即可编译出不同规范的 JS 文件, types 对应的类型声明文件也是由工具来输出,无需手动维护。

而其他的字段可以根据项目的性质决定是否补充,以下是 hello-lib 的基础信息示例:

json
{
+  "name": "@learning-vue3/lib",
+  "version": "1.0.0",
+  "description": "A library demo for learning-vue3.",
+  "author": "chengpeiquan <chengpeiquan@chengpeiquan.com>",
+  "homepage": "https://github.com/learning-vue3/hello-lib",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/learning-vue3/hello-lib.git"
+  },
+  "license": "MIT",
+  "files": ["dist"],
+  "main": "dist/index.cjs",
+  "module": "dist/index.mjs",
+  "browser": "dist/index.min.js",
+  "types": "dist/index.d.ts",
+  "keywords": ["library", "demo", "example"],
+  "scripts": {
+    "build": "vite build"
+  }
+}

此时 mainmodulebrowsertypes 字段对应的文件还不存在,它们将在项目执行 npm run build 构建之后才会产生。

另外,入口文件使用了不同规范对应的文件扩展名,也可以统一使用 .js 扩展名,通过文件名来区分,例如 es 格式使用 index.es.js

scripts 字段则配置了一个 build 命令,这里使用了 Vite 的构建命令来打包项目,这个过程会读取 Vite 的配置文件 vite.config.ts ,关于该文件的配置内容将在下文继续介绍。

安装开发依赖

本次的 npm 包将使用 Vite 进行构建,使用 TypeScript 编写源代码,由于 Vite 本身对 TypeScript 进行了支持,因此只需要将 Vite 安装到开发依赖:

bash
# 添加 -D 选项将其安装到 devDependencies
+npm i -D vite

添加配置文件

配置包信息 的时候已提前配置了一个 npm run build 的命令,它将运行 Vite 来构建 npm 包的入口文件。

由于 Vite 默认是构建入口文件为 HTML 的网页应用,而开发 npm 包时入口文件是 JS / TS 文件,因此需要添加一份配置文件来指定构建的选项。

以下是本次的基础配置,可以完成最基本的打包,它将输出三个不同格式的入口文件,分别对应 CommonJS 、 ES Module 和 UMD 规范,分别对应 package.json 里 mainmodulebrowser 字段指定的文件。

ts
// vite.config.ts
+import { defineConfig } from 'vite'
+
+// https://cn.vitejs.dev/config/
+export default defineConfig({
+  build: {
+    // 输出目录
+    outDir: 'dist',
+    // 构建 npm 包时需要开启 “库模式”
+    lib: {
+      // 指定入口文件
+      entry: 'src/index.ts',
+      // 输出 UMD 格式时,需要指定一个全局变量的名称
+      name: 'hello',
+      // 最终输出的格式,这里指定了三种
+      formats: ['es', 'cjs', 'umd'],
+      // 针对不同输出格式对应的文件名
+      fileName: (format) => {
+        switch (format) {
+          // ES Module 格式的文件名
+          case 'es':
+            return 'index.mjs'
+          // CommonJS 格式的文件名
+          case 'cjs':
+            return 'index.cjs'
+          // UMD 格式的文件名
+          default:
+            return 'index.min.js'
+        }
+      },
+    },
+    // 压缩混淆构建后的文件代码
+    minify: true,
+  },
+})

添加入口文件

来到这里,最基础的准备工作已完成,接下来添加入口文件并尝试编译。

添加配置文件 时已指定了入口文件为 src/index.ts ,因此需要对应的创建该文件,并写入一个简单的方法,将用它来测试打包结果:

ts
// src/index.ts
+export default function hello(name: string) {
+  console.log(\`Hello \${name}\`)
+}

在命令行执行 npm run build 命令,可以看到项目下生成了 dist 文件夹,以及三个 JavaScript 文件,此时目录结构如下:

bash
hello-lib
+ # 构建产物的输出文件夹
+├─dist
+ ├─index.cjs
+ ├─index.min.js
+ └─index.mjs
+ # 依赖文件夹
+├─node_modules
+ # 源码文件夹
+├─src
+  # 入口文件
+ └─index.ts
+ # 项目清单信息
+├─package-lock.json
+├─package.json
+ # Vite 配置文件
+└─vite.config.ts

打开 dist 目录下的文件内容,可以看到虽然源码是使用 TypeScript 编写的,但最终输出的内容是按照指定的格式转换为 JavaScript 并且被执行了压缩和混淆,在这里将它们重新格式化,来看看转换后的结果。

这是 index.cjs 的文件内容,源码被转换为 CommonJS 风格的代码:

js
// dist/index.cjs
+'use strict'
+function l(o) {
+  console.log(\`Hello \${o}\`)
+}
+module.exports = l

这是 index.mjs 的内容,源码被转换为 ES Module 风格的代码:

js
// dist/index.mjs
+function o(l) {
+  console.log(\`Hello \${l}\`)
+}
+export { o as default }

这是 index.min.js 的内容,源码被转换为 UMD 风格的代码:

js
// dist/index.min.js
+;(function (e, n) {
+  typeof exports == 'object' && typeof module < 'u'
+    ? (module.exports = n())
+    : typeof define == 'function' && define.amd
+    ? define(n)
+    : ((e = typeof globalThis < 'u' ? globalThis : e || self), (e.hello = n()))
+})(this, function () {
+  'use strict'
+  function e(n) {
+    console.log(\`Hello \${n}\`)
+  }
+  return e
+})

来到这里,准备工作已就绪,下一步将开始进入工具包和组件包的开发。

开发 npm 包

这里先从最简单的函数库开始入门包的开发,为什么说它简单呢?因为只需要编写 JavaScript 或 TypeScript 就可以很好的完成开发工作。

在理解了包的开发流程之后,如果要涉及 Vue 组件包的开发,则安装相关的 Vue 的相关依赖、 Less 等 CSS 预处理器依赖,只要满足了编译条件,就可以正常构建和发布,它们的开发流程是一样的。

编写 npm 包代码

在开发的过程中,需要遵循模块化开发的要求,当前这个演示包使用 TypeScript 编码,就需要 使用 ES Module 来设计模块 ,如果对模块化设计还没有足够的了解,请先回顾相关的内容。

先在 src 目录下创建一个名为 utils.ts 的文件,写入以下内容:

ts
// src/utils.ts
+
+/**
+ * 生成随机数
+ * @param min - 最小值
+ * @param max - 最大值
+ * @param roundingType - 四舍五入类型
+ * @returns 范围内的随机数
+ */
+export function getRandomNumber(
+  min: number = 0,
+  max: number = 100,
+  roundingType: 'round' | 'ceil' | 'floor' = 'round'
+) {
+  return Math[roundingType](Math.random() * (max - min) + min)
+}
+
+/**
+ * 生成随机布尔值
+ */
+export function getRandomBoolean() {
+  const index = getRandomNumber(0, 1)
+  return [true, false][index]
+}

这里导出了两个随机方法,其中 getRandomNumber 提供了随机数值的返回,而 getRandomBoolean 提供了随机布尔值的返回,在源代码方面, getRandomBoolean 调用了 getRandomNumber 获取随机索引。

这是一个很常见的 npm 工具包的开发思路,包里的函数都使用了细粒度的编程设计,每一个函数都是独立的功能,在必要的情况下,函数 B 可以调用函数 A 来减少代码的重复编写。

在这里, utils.ts 文件已开发完毕,接下来需要将它导出的方法提供给包的使用者,请删除入口文件 src/index.ts 原来的测试内容,并输入以下新代码:

ts
// src/index.ts
+export * from './utils'

这代表将 utils.ts 文件里导出的所有方法或者变量,再次导出去,如果有很多个 utils.ts 这样的文件, index.ts 将作为一个统一的入口,统一的导出给构建工具去编译输出。

接下来在命令行执行 npm run build ,再分别看看 dist 目录下的文件变化:

此时的 index.cjs 文件,已经按照 CommonJS 规范转换了源代码:

js
// dist/index.cjs
+'use strict'
+Object.defineProperties(exports, {
+  __esModule: { value: !0 },
+  [Symbol.toStringTag]: { value: 'Module' },
+})
+function t(e = 0, o = 100, n = 'round') {
+  return Math[n](Math.random() * (o - e) + e)
+}
+function r() {
+  const e = t(0, 1)
+  return [!0, !1][e]
+}
+exports.getRandomBoolean = r
+exports.getRandomNumber = t

index.mjs 也按照 ES Module 规范进行了转换:

js
// dist/index.mjs
+function o(n = 0, t = 100, e = 'round') {
+  return Math[e](Math.random() * (t - n) + n)
+}
+function r() {
+  const n = o(0, 1)
+  return [!0, !1][n]
+}
+export { r as getRandomBoolean, o as getRandomNumber }

index.min.js 同样正常按照 UMD 风格转换成了 JavaScript 代码:

js
// dist/index.min.js
+;(function (e, n) {
+  typeof exports == 'object' && typeof module < 'u'
+    ? n(exports)
+    : typeof define == 'function' && define.amd
+    ? define(['exports'], n)
+    : ((e = typeof globalThis < 'u' ? globalThis : e || self),
+      n((e.hello = {})))
+})(this, function (e) {
+  'use strict'
+  function n(o = 0, u = 100, d = 'round') {
+    return Math[d](Math.random() * (u - o) + o)
+  }
+  function t() {
+    const o = n(0, 1)
+    return [!0, !1][o]
+  }
+  ;(e.getRandomBoolean = t),
+    (e.getRandomNumber = n),
+    Object.defineProperties(e, {
+      __esModule: { value: !0 },
+      [Symbol.toStringTag]: { value: 'Module' },
+    })
+})

对 npm 包进行本地调试

开发或者迭代了一个 npm 包之后,不建议直接发布,可以在本地进行测试,直到没有问题了再发布到 npmjs 上供其他人使用。

npm 提供了一个 npm link 命令供开发者本地联调,假设 path/to/my-library 是一个 npm 包的项目路径, path/to/my-project 是一个调试项目的所在路径,那么通过以下步骤可以在 my-project 里本地调试 my-library 包。

TIP

关于 path/to 的典故,可以查看 #294 了解。

创建本地软链接

先在 my-library npm 包项目里执行 npm link 命令,创建 npm 包的本地软链接:

bash
# 进入 npm 包项目所在的目录
+cd path/to/my-library
+
+# 创建 npm 包的本地软链接
+npm link

运行了以上命令之后,意味着刚刚开发好的 npm 包,已经被成功添加到了 Node 的全局安装目录下,可以在命令行运行以下命令查看全局安装目录的位置:

bash
npm prefix -g

假设 {prefix} 是全局安装目录,刚刚这个包在 package.json 里的包名称是 my-library ,那么在 {prefix}/node_modules/my-library 这个目录下可以看到被软链接了一份项目代码。

TIP

软链接( Symbolic Link / Symlink / Soft Link ),是指通过指定路径来指向文件或目录,操作系统会自动将其解释为另一个文件或目录的路径,因此软链接被删除或修改不会影响源文件,而源文件的移动或者删除,不会自动更新软链接,这一点和快捷方式的作用比较类似。

自此已经对这个 npm 包完成了一次 “本地发布” ,接下来就要在调试项目里进行本地关联。

关联本地软链接

my-project 调试项目里执行语法为 npm link [<package-spec>] 的 link 命令,关联 npm 包的本地软链接。

TIP

这里的 [<package-spec>] 参数,可以是包名称,也可以是 npm 包项目所在的路径。

bash
# 进入调试项目所在的目录
+cd path/to/my-project
+
+# 通过 npm 包的包名称关联本地软链接
+npm link my-library

如果通过 npm 包名称关联失败,例如返回了如下信息:

bash
 npm link my-library
+npm ERR! code E404
+npm ERR! 404 Not Found - GET https://registry.npmjs.org/my-library - Not found
+npm ERR! 404
+npm ERR! 404  'my-library@*' is not in this registry.
+npm ERR! 404
+npm ERR! 404 Note that you can also install from a
+npm ERR! 404 tarball, folder, http url, or git url.

这种情况通常出现于本地 npm 包还没有在 npmjs 上进行过任意版本的发布,而包管理器又找不到本地全局安装目录的软链接,就会去 npm 源找,都找不到就会返回 404 的报错,针对这种情况,也可以使用 npm 包项目的路径进行关联:

bash
# 进入调试项目所在的目录
+cd path/to/my-project
+
+# 通过 npm 包的项目路径关联本地软链接
+npm link path/to/my-library

至此,就完成了调试项目对该 npm 包在本地的 “安装” ,此时在 my-project 这个调试项目的 node_modules 目录下也会创建一个软链接,指向 my-library 所在的目录。

回归当前的演示包项目,先创建一个基于 TypeScript 的 Vue 新项目作为调试项目,在关联了本地 npm 包之后,就可以在调试项目里编写如下代码,测试 npm 包里的方法是否可以正常使用:

ts
// 请将 \`@learning-vue3/lib\` 更换为实际的包名称
+import { getRandomNumber } from '@learning-vue3/lib'
+
+const num = getRandomNumber()
+console.log(num)

启动 npm run dev 的调试命令并打开本地调试页面,就可以在浏览器控制台正确的打印出了随机结果。

因为本包还支持 UMD 规范,所以也可以在 HTML 页面通过普通的 <script /> 标签直接引入 dist 目录下的文件测试将来引入 CDN 时的效果,可以在 npm 包项目下创建一个 demo 目录,并添加一个 index.html 文件到该目录下,并写入以下内容:

html
<!-- demo/index.html -->
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Library Demo</title>
+  </head>
+  <body>
+    <!-- 这里引入的是 UMD 规范的文件 -->
+    <script src="../dist/index.min.js"></script>
+    <script>
+      /**
+       * UMD 规范的文件会有一个全局变量
+       * 由 vite.config.ts 的 \`build.lib.name\` 决定
+       */
+      console.log(hello)
+
+      /**
+       * 所有的方法会挂在这个全局变量上
+       * 类似于 jQuery 的 $.xxx() 那样使用
+       */
+      const num = hello.getRandomNumber()
+      console.log(num)
+    </script>
+  </body>
+</html>

在浏览器打开该 HTML 文件并唤起控制台,一样可以看到随机结果的打印记录。

添加版权注释

很多知名项目在 Library 文件的开头都会有一段版权注释,它的作用除了声明版权归属之外,还会告知使用者关于项目的主页地址、版本号、发布日期、 BUG 反馈渠道等信息。

例如很多开发者入门前端时使用过的经典类库 jQuery :

js
// https://cdn.jsdelivr.net/npm/jquery@3.6.1/dist/jquery.js
+
+/*!
+ * jQuery JavaScript Library v3.6.1
+ * https://jquery.com/
+ *
+ * Includes Sizzle.js
+ * https://sizzlejs.com/
+ *
+ * Copyright OpenJS Foundation and other contributors
+ * Released under the MIT license
+ * https://jquery.org/license
+ *
+ * Date: 2022-08-26T17:52Z
+ */
+( function( global, factory ) {
+// ...

又如流行的 JavaScript 工具库 Lodash :

js
// https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js
+
+/**
+ * @license
+ * Lodash <https://lodash.com/>
+ * Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
+ * Released under MIT license <https://lodash.com/license>
+ * Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
+ * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+ */
+(function(){
+// ...

还有每次做轮播图一定会想到它的 Swiper :

js
// https://cdn.jsdelivr.net/npm/swiper@8.4.3/swiper-bundle.js
+
+/**
+ * Swiper 8.4.3
+ * Most modern mobile touch slider and framework
+ * with hardware accelerated transitions
+ * https://swiperjs.com
+ *
+ * Copyright 2014-2022 Vladimir Kharlampidi
+ *
+ * Released under the MIT License
+ *
+ * Released on: October 6, 2022
+ */
+(function (global, factory) {
+// ...

聪明的开发者肯定已经猜到了,这些版权注释肯定不是手动添加的,那么它们是如何自动生成的呢?

npm 社区提供了非常多开箱即用的注入插件,通常可以通过 “当前使用的构建工具名称” 加上 “plugin banner” 这样的关键字,在 npmjs 网站上搜索是否有相关的插件,以当前使用的 Vite 为例,可以通过 vite-plugin-banner 实现版权注释的自动注入。

回到 hello-lib 项目,安装该插件到 devDependencies :

bash
npm i -D vite-plugin-banner

根据插件的文档建议,打开 vite.config.ts 文件,将其导入,并通过读取 package.json 的信息来生成常用的版权注释信息:

ts
// vite.config.ts
+import { defineConfig } from 'vite'
+// 导入版权注释插件
+import banner from 'vite-plugin-banner'
+// 导入 npm 包信息
+import pkg from './package.json'
+
+// https://cn.vitejs.dev/config/
+export default defineConfig({
+  // 其他选项保持不变
+  // ...
+  plugins: [
+    // 新增 banner 插件的启用,传入 package.json 的字段信息
+    banner(
+      \`/**\\n * name: \${pkg.name}\\n * version: v\${pkg.version}\\n * description: \${pkg.description}\\n * author: \${pkg.author}\\n * homepage: \${pkg.homepage}\\n */\`
+    ),
+  ],
+})

再次运行 npm run build 命令,打开 dist 目录下的 Library 文件,可以看到都成功添加了一段版权注释:

js
// dist/index.mjs
+
+/**
+ * name: @learning-vue3/lib
+ * version: v1.0.0
+ * description: A library demo for learning-vue3.
+ * author: chengpeiquan <chengpeiquan@chengpeiquan.com>
+ * homepage: https://github.com/learning-vue3/hello-lib
+ */
+function o(n = 0, t = 100, e = 'round') {
+  return Math[e](Math.random() * (t - n) + n)
+}
+function r() {
+  const n = o(0, 1)
+  return [!0, !1][n]
+}
+export { r as getRandomBoolean, o as getRandomNumber }

这样其他开发者如果在使用过程中遇到了问题,就可以轻松找到插件作者的联系方式了!

TIP

请根据实际的 package.json 存在的字段信息调整 banner 内容。

生成 npm 包的类型声明

虽然到这里已经得到一个可以运行的 JavaScript Library 文件,在 JavaScript 项目里使用是完全没有问题的,但还不建议直接发布到 npmjs 上,因为目前的情况下在 TypeScript 项目并不能完全兼容,还需要生成一份 npm 包的类型声明文件。

为什么需要类型声明

如果在上一小节 关联本地软链接 创建 Vue 调试项目时,也是使用了 TypeScript 版本的 Vue 项目,会遇到 VSCode 在下面这句代码上:

ts
import { getRandomNumber } from '@learning-vue3/lib'

在包名称 '@learning-vue3/lib' 的位置提示了一个红色波浪线,把鼠标移上去会显示这么一段话:

无法找到模块 “@learning-vue3/lib” 的声明文件。 “D:/Project/demo/hello-lib/dist/index.cjs” 隐式拥有 "any" 类型。
尝试使用 npm i --save-dev @types/learning-vue3__lib (如果存在),或者添加一个包含 declare module '@learning-vue3/lib'; 的新声明 (.d.ts) 文件 ts(7016)

此时在命令行运行 Vue 调试项目的打包命令 npm run build ,也会遇到打包失败的报错,控制台同样反馈了这个问题:缺少声明文件。

bash
 npm run build
+
+> hello-vue3@0.0.0 build
+> vue-tsc --noEmit && vite build
+
+src/App.vue:8:30 - error TS7016: Could not find a declaration file for module '@learning-vue3/lib'. 'D:/Project/demo/hello-lib/dist/index.cjs' implicitly has an 'any' type.
+  Try \`npm i --save-dev @types/learning-vue3__lib\` if it exists or add a new declaration (.d.ts) file containing \`declare module '@learning-vue3/lib';\`
+
+8 import { getRandomNumber } from '@learning-vue3/lib'
+                               ~~~~~~~~~~~~~~~~~~~~
+
+
+Found 1 error in src/App.vue:8

虽然使用者可以按照报错提示,在调试项目下创建一个 d.ts 文件并写入以下内容来声明该 npm 包:

ts
declare module '@learning-vue3/lib'

但这需要每个使用者,或者说每个使用到这个包的项目都声明一次,对于使用者来说非常不友好, declare module 之后虽然不会报错了,但也无法获得 VSCode 对 npm 包提供的 API 进行 TS 类型的自动推导与类型提示、代码补全等功能支持。

主流的做法

细心的开发者在 npmjs 网站上搜索 npm 包时,会发现很多 npm 包在详情页的包名后面,跟随有一个蓝色的 TS 图标,鼠标移上去时,还会显示一句提示语:

This package contains built-in TypeScript declarations

`,290),u=o(`

例如上图的 @vue/reactivity , Vue 3 的响应式 API 包,就带有这个图标。

这表示带有这个图标的 npm 包,已包含内置的 TypeScript 声明,可以获得完善的 TS 类型推导和提示支持,开发过程中也可以获得完善的代码补全功能支持,提高开发效率,在 TypeScript 项目执行 npm run build 的时候也能够被成功打包。

以 @vue/reactivity 这个包为例,如果项目下安装有这个 npm 包,可以在

bash
# 基于项目根目录
+./node_modules/@vue/reactivity/dist/reactivity.d.ts

这个文件里查看 Vue 3 响应式 API 的类型声明,也可以通过该文件的 CDN 地址访问到其内容:

bash
https://cdn.jsdelivr.net/npm/@vue/reactivity@3.2.40/dist/reactivity.d.ts

生成 DTS 文件

有在 “快速上手 TypeScript ” 一章阅读过 了解 tsconfig.json 这节内容的开发者,应该对该文件有了一定的了解,如果还没有阅读过也没关系,可以先按照下方的步骤操作,接下来将分布说明如何生成 npm 包的 DTS 类型声明文件(以 .d.ts 为扩展名的文件)。

请先全局安装 typescript 这个包:

bash
npm install -g typescript

依然是在在命令行界面,回到 hello-lib 这个 npm 包项目的根目录,执行以下命令生成 tsconfig.json 文件:

bash
tsc --init

打开 tsconfig.json 文件,生成的文件里会有很多默认被注释掉的选项,请将以下几个选项取消注释,同时在 compilerOptions 字段的同级新增 include 字段,这几个选项都修改为如下配置:

json
{
+  "compilerOptions": {
+    "declaration": true,
+    "emitDeclarationOnly": true,
+    "declarationDir": "./dist"
+  },
+  "include": ["./src"]
+}

其中 compilerOptions 三个选项的意思是: .ts 源文件不编译为 .js 文件,只生成 .d.ts 文件并输出到 dist 目录; include 选项则告诉 TypeScript 编译器,只处理 src 目录下的 TS 文件。

修改完毕后,在命令行执行以下命令,它将根据 tsconfig.json 的配置对项目进行编译:

bash
tsc

可以看到现在的 dist 目录下多了 2 份 .d.ts 文件: index.d.ts 和 utils.d.ts 。

bash
hello-lib
+└─dist
+  ├─index.cjs
+  ├─index.d.ts
+  ├─index.min.js
+  ├─index.mjs
+  └─utils.d.ts

打开 dist/index.d.ts ,可以看到它的内容和 src/index.ts 是一样的,因为作为入口文件,只提供了模块的导出:

ts
// dist/index.d.ts
+export * from './utils'

再打开 dist/utils.d.ts ,可以看到它的内容如下,对比 src/utils.ts 的文件内容,它去掉了具体的功能实现,并且根据代码逻辑,转换成了 TypeScript 的类型声明:

ts
// dist/utils.d.ts
+/**
+ * 生成随机数
+ * @param min - 最小值
+ * @param max - 最大值
+ * @param roundingType - 四舍五入类型
+ * @returns 范围内的随机数
+ */
+export declare function getRandomNumber(
+  min?: number,
+  max?: number,
+  roundingType?: 'round' | 'ceil' | 'floor'
+): number
+/**
+ * 生成随机布尔值
+ */
+export declare function getRandomBoolean(): boolean

由于 hello-lib 项目的 package.json 已提前指定了类型声明文件指向:

json
{
+  "types": "dist/index.d.ts"
+}

因此可以直接回到调试 npm 包的 Vue 项目,此时 VSCode 对那句 import 语句的红色波浪线报错信息已消失不见,鼠标移到 getRandomNumber 这个方法上,也可以看到 VSCode 出现了该方法的类型提示,非常方便。

TIP

如果 VSCode 未能及时更新该包的类型,依然提示红色波浪线,可以重启 VSCode 再次查看。

再次运行 npm run build 命令构建调试项目,这一次顺利通过编译:

bash
 npm run build
+
+> hello-vue3@0.0.0 build
+> vue-tsc --noEmit && vite build
+
+vite v2.9.15 building for production...
+ 42 modules transformed.
+dist/assets/logo.03d6d6da.png             6.69 KiB
+dist/index.html                           0.42 KiB
+dist/assets/home.9a123f29.js              2.01 KiB / gzip: 1.01 KiB
+dist/assets/logo.db8b6a93.js              0.12 KiB / gzip: 0.13 KiB
+dist/assets/TransferStation.25db7d3e.js   0.29 KiB / gzip: 0.22 KiB
+dist/assets/bar.0e9da4c4.js               0.53 KiB / gzip: 0.37 KiB
+dist/assets/bar.09e673fa.css              0.22 KiB / gzip: 0.18 KiB
+dist/assets/home.6bd02f2a.css             0.62 KiB / gzip: 0.33 KiB
+dist/assets/index.60726771.css            0.47 KiB / gzip: 0.29 KiB
+dist/assets/index.aebbe022.js             79.87 KiB / gzip: 31.80 KiB

生成 DTS Bundle

初始化项目生成 DTS 文件 ,其实已经走完一个 npm 包的完整开发流程了,是可以提交发布了,但在发布之前,先介绍另外一个生成 DTS 文件的方式,可以根据实际情况选择使用。

请注意这里使用了 DTS Bundle 来称呼类型声明文件,这是因为直接使用 tsc 命令生成的 DTS 文件,是和源码目录的文件数量挂钩的,可以留意到在上一小节使用 tsc 命令生成声明文件后,在 hello-lib 项目中:

  • src 源码目录有 index.ts 和 utils.ts 两个文件
  • dist 输出目录也对应生成了 index.d.ts 和 utils.d.ts 两个文件

在一个大型项目里,源码的目录和文件非常多,意味着 DTS 文件也是非常多,这样的输出结构并不是特别友好。

在讲 npm 包对类型声明 主流的做法 的时候,提到了 Vue 响应式 API 的 npm 包是提供了一个完整的 DTS 文件,它包含了所有 API 的类型声明信息:

bash
./node_modules/@vue/reactivity/dist/reactivity.d.ts

这种将多个模块的文件内容合并为一个完整文件的行为通常称之为 Bundle ,本小节将介绍如何生成这种 DTS Bundle 文件。

继续回到 hello-lib 这个 npm 包项目,由于 tsc 本身不提供类型文件的合并,所以需要借助第三方依赖来实现,比较流行的第三方包有: dts-bundle-generatornpm-dtsdts-bundledts-generator 等等。

之前笔者在为公司开发 npm 工具包的时候都对它们进行了一轮体验,鉴于实际开发过程中遇到的一些编译问题,在这里选用问题最少的 dts-bundle-generator 进行开发演示,请先安装到 hello-lib 项目的 devDependencies :

bash
npm i -D dts-bundle-generator

dts-bundle-generator 支持在 package.json 里配置一个 script ,通过命令的形式在命令行生成 DTS Bundle ,也支持通过 JavaScript / TypeScript 编写函数来执行文件的生成,鉴于实际开发过程中使用函数生成 DTS Bundle 的场景比较多(例如 Monorepo 会有生成多个 Bundle 的使用场景),因此这里以函数的方式进行演示。

TIP

在使用 Git 等版本控制系统时,如果多个独立项目之间有关联,会把这些项目的代码都存储在同一个代码仓库集中管理,此时这个大型代码仓库就被称之为 Monorepo (其中 Mono 表示单一, Repo 是存储库 Repository 的缩写),当下许多大型项目都基于这种方法管理代码, Vue 3 在 GitHub 的代码仓库也是一个 Monorepo 。

请在 hello-lib 的根目录下,创建一个与 src 源码目录同级的 scripts 目录,用来存储源码之外的脚本函数。

将以下代码保存到 scripts 目录下,命名为 buildTypes.mjs :

js
// scripts/buildTypes.mjs
+import { writeFileSync } from 'fs'
+import { dirname, resolve } from 'path'
+import { fileURLToPath } from 'url'
+import { generateDtsBundle } from 'dts-bundle-generator'
+
+async function run() {
+  // 默认情况下 \`.mjs\` 文件需要自己声明 __dirname 变量
+  const __filename = fileURLToPath(import.meta.url)
+  const __dirname = dirname(__filename)
+
+  // 获取项目的根目录路径
+  const rootPath = resolve(__dirname, '..')
+
+  // 添加构建选项
+  // 插件要求是一个数组选项,支持多个入口文件
+  const options = [
+    {
+      filePath: resolve(rootPath, \`./src/index.ts\`),
+      output: {
+        noBanner: true,
+      },
+    },
+  ]
+
+  // 生成 DTS 文件内容
+  // 插件返回一个数组,返回的文件内容顺序同选项顺序
+  const dtses = generateDtsBundle(options, {
+    preferredConfigPath: resolve(rootPath, \`./tsconfig.json\`),
+  })
+  if (!Array.isArray(dtses) || !dtses.length) return
+
+  // 将 DTS Bundle 的内容输出成 \`.d.ts\` 文件保存到 dist 目录下
+  // 当前只有一个文件要保存,所以只取第一个下标的数据
+  const dts = dtses[0]
+  const output = resolve(rootPath, \`./dist/index.d.ts\`)
+  writeFileSync(output, dts)
+}
+run().catch((e) => {
+  console.log(e)
+})

接下来打开 hello-lib 的 package.json 文件,添加一个 build:types 的 script ,并在 build 命令中通过 && 符号设置为继发执行任务,当前所有的 scripts 如下:

json
{
+  "scripts": {
+    "build": "vite build && npm run build:types",
+    "build:types": "node scripts/buildTypes.mjs"
+  }
+}

TIP

继发执行:只有前一个任务执行成功,才继续执行下一个任务,任务与任务之间使用 && 符号连接。

接下来再运行 npm run build 命令,将在执行完 Vite 的 build 任务之后,再继续执行 DTS Bundle 的文件生成,可以看到现在的 dist 目录变成了如下,只会生成一个 .d.ts 文件:

bash
hello-lib
+└─dist
+  ├─index.cjs
+  ├─index.d.ts
+  ├─index.min.js
+  └─index.mjs

现在 index.d.ts 文件已经集合了源码目录下所有的 TS 类型,变成了如下内容:

ts
// dist/index.d.ts
+/**
+ * 生成随机数
+ * @param min - 最小值
+ * @param max - 最大值
+ * @param roundingType - 四舍五入类型
+ * @returns 范围内的随机数
+ */
+export declare function getRandomNumber(
+  min?: number,
+  max?: number,
+  roundingType?: 'round' | 'ceil' | 'floor'
+): number
+/**
+ * 生成随机布尔值
+ */
+export declare function getRandomBoolean(): boolean
+
+export {}

对于大型项目,将 DTS 文件集合为 Bundle 输出是一种主流的管理方式,非常建议使用这种方式来为 npm 包生成类型文件。

添加说明文档

作为一个完整的 npm 包,应该配备一份操作说明给使用者阅读,复杂的文档可以使用 VitePress 等文档程序独立部署,而简单的项目则只需要完善一份 README 即可。

请创建一个名为 README.md 的 Markdown 文件在项目根目录下,与 src 源码目录同级,该文件的文件名 README 推荐使用全大写,这是开源社区主流的命名方式,全大写的原因是为了与代码文件进行直观的区分。

编写 README 使用的 Markdown 是一种轻量级标记语言,可以使用易读易写的纯文本格式编写文档,以 .md 作为文件扩展名,当代码托管到 GitHub 仓库或者发布到 npmjs 等平台时, README 文件会作为项目的主页内容呈现。

为了方便学习,这里将一些常用的 Markdown 语法与 HTML 代码对比如下,可以看到书写方面非常的简洁:

Markdown 代码HTML 代码
# 一级标题<h1>一级标题</h1>
## 二级标题<h2>二级标题</h2>
### 三级标题<h3>三级标题</h3>
**加粗文本**<span style="font-weight: bold;">加粗文本</span>
[链接文本](https://example.com)<a href="https://example.com">链接文本</a>

更多的 Markdown 语法建议在 Markdown 教程网站 上学习。

下面附上一份常用的 README 模板:

md
# 项目名称
+
+写上项目用途的一句话简介。
+
+## 功能介绍
+
+1. 功能 1 一句话介绍
+2. 功能 2 一句话介绍
+3. 功能 3 一句话介绍
+
+## 在线演示
+
+如果有部署在线 demo ,可放上 demo 的访问地址。
+
+## 安装方法
+
+使用 npm : \`npm install package-name\`
+
+使用 CDN : \`https://example.com/package-name\`
+
+## 用法
+
+告诉使用者如何使用 npm 包。
+
+## 插件选项
+
+如果 npm 包是一个插件,并支持传递插件选项,在这里可以使用表格介绍选项的作用。
+
+| 选项名称 |  类型  |    作用    |
+| :------: | :----: | :--------: |
+|   foo    | string | 一句话介绍 |
+|   bar    | number | 一句话介绍 |
+
+更多内容请根据实际情况补充。

拥有完善的使用说明文档,会让 npm 包更受欢迎!

发布 npm 包

一个 npm 包开发完毕后,就可以进入发布阶段了,这一小节将讲解如何注册 npm 账号并发布到 npmjs 平台上供其他开发者下载使用。

TIP

在操作 npm 包发布之前,请先运行 npm config rm registry 命令取消 npm 镜像源的绑定,否则会发布失败,在 npm 包发布后,可以再重新 配置镜像源

注册 npm 账号

在发布 npm 包之前,请先在 npm 官网上注册一个账号:点击注册

接下来需要在命令行上登录该账号以操作发布命令,打开命令行工具,输入以下命令进行登录:

bash
npm login

按照命令行的提示输入在 npmjs 网站上注册的账号和密码即可完成登录,可以通过以下命令查看当前登录的账号名称,验证是否登录成功:

bash
npm whoami

在登录成功之后,命令行会记住账号的登录状态,以后的操作就无需每次都执行登录命令了。

TIP

以上操作也可以实用 npm adduser 命令代替,直接在命令行完成注册和登录。

将包发布到 npmjs

在 npm 上发布私有包需要进行付费,因此这里只使用公共包的发布作为演示和讲解,如果开发的是公司内部使用的 npm 包,只要源代码是私有仓库,也可以使用这种方式来发布,当前在这样做之前请先获得公司的同意。

对于一个普通命名的包,要发布到 npmjs 上非常简单,只需要执行 npm 包管理器自带的一个命令即可:

bash
npm publish

它默认会将这个包作为一个公共包发布,如果包名称合法并且没有冲突,则发布成功,可以在 npmjs 查询到,否则会返回错误信息告知原因,如果因为包名冲突导致的失败,可以尝试修改别的名称再次发布。

如果打算使用像 @vue/cli@vue/compiler-sfc 这样带有 @scope 前缀的作用域包名,需要先在 npmjs 的 创建新组织 页面创建一个组织,或者确保自己拥有 @scope 对应的组织发布权限。

@scope 作用域包默认会作为私有包发布,因此在执行发布命令的时候还需要加上一个 --access 选项,将其指定为 public 允许公开访问才可以发布成功:

bash
npm publish --access public

当前的 hello-lib 项目已发布到 npmjs ,可以查看该包的主页 @learning-vue3/lib ,也可以通过 npm 安装到项目里使用了:

bash
npm i @learning-vue3/lib

并且发布到 npmjs 上的包,都同时获得热门 CDN 服务的自动同步,可以通过包名称获取到 CDN 链接并通过 <script /> 标签引入到 HTML 页面里:

bash
# 使用 jsDelivr CDN
+https://cdn.jsdelivr.net/npm/@learning-vue3/lib
+
+# 使用 UNPKG CDN
+https://unpkg.com/@learning-vue3/lib

此时 CDN 地址对应的 npm 包文件内容,就如前文所述,调用了 package.json 里 browser 字段指定的 UMD 规范文件 dist/index.min.js

给 npm 包打 Tag

细心的开发者还会留意到,例如像 Vue 这样的包,在 npmjs 上的 版本列表 里有 Current Tags 和 Version History 的版本分类,其中 Version History 是默认的版本发布历史列表,而 Current Tags 则是在发布 npm 包的时候指定打的标签。

`,89),g=o(`

标签的好处是可以让使用者无需记住对应的版本号,而是使用一些更具备语义化的单词来安装指定版本,例如:

bash
# 安装最新版的 Vue 3 ,即截图里对应的 3.2.40 版本
+npm i vue@latest
+
+# 安装最新版的 Vue 2 ,即截图里对应的 2.7.10 版本
+npm i vue@v2-latest
+
+# 如果后续有功能更新的测试版,也可以通过标签安装
+npm i vue@beta

除了减少寻找版本号的麻烦外,一旦后续有版本更新,再次使用相同的标签安装,可以重新安装到该标签对应的最新版本,例如从 1.0.0-beta.1 升级到 1.0.0-beta.2 ,可以使用 @beta 标签再次安装来达到升级的目的。

在标签列表里,有一个 latest 的标签是发布 npm 包时自带的,对应该包最新的正式版本,安装 npm 包时如果不指定标签,则默认使用 latest 标签,以下两个安装操作是等价的:

bash
# 隐式安装 latest 标签对应的版本
+npm i vue
+
+# 显式安装 latest 标签对应的版本
+npm i vue@latest

同样的,当发布 npm 包时不指定标签,则该版本也会在发布后作为 @latest 标签对应的版本号。

其他标签则需要在发布时配合发布命令,使用 --tag 作为选项手动指定,以下命令将为普通包打上名为 alpha 的 Tag :

bash
npm publish --tag alpha

同理,如果是 @scope 作用域包也是在使用 --access 选项的情况下,继续追加一条 --tag 选项指定包的标签。

bash
npm publish --access public --tag alpha

TIP

请注意,如果是 Alpha 或者 Beta 版本,通常会在版本号上增加 -alpha.0-alpha.1 这样的 版本标识符,以便在发布正式版本的时候可以使用无标识符的相同版本号,以保证版本号在遵循 升级规则 下的连续性。

`,11);function m(f,E,b,v,B,k){const t=n("ReadingTips"),e=n("ImgWrap"),l=n("ClientOnly"),c=n("GitalkComment");return y(),i("div",null,[C,A,d,s(t),h,s(l,null,{default:p(()=>[s(e,{src:"/assets/img/npm-detail-ts-icon.jpg",alt:"注意 npm 包名称后面的 TS 图标"})]),_:1}),u,s(l,null,{default:p(()=>[s(e,{src:"/assets/img/vue-versions-on-npmjs.jpg",alt:"Vue 在 npmjs 上的版本列表"})]),_:1}),g,s(l,null,{default:p(()=>[s(c,{issueId:48})]),_:1})])}const j=r(F,[["render",m]]);export{x as __pageData,j as default}; diff --git a/assets/preface.md.59b338cf.js b/assets/preface.md.59b338cf.js new file mode 100644 index 00000000..19703167 --- /dev/null +++ b/assets/preface.md.59b338cf.js @@ -0,0 +1,8 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as l,v as o,b as p,t as a,O as t,R as c,F as e,L as s,M as n}from"./chunks/framework.0d8bea05.js";const j=JSON.parse('{"title":"前言","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"preface.md","filePath":"preface.md"}'),u={name:"preface.md"},h=c('

前言

Vue 3.0 从 2020 年 9 月份中旬正式发布,到 2022 年 2 月份代替 Vue 2 成为 Vue 的默认版本,经过长达一年半的市场验证,已经证明了它可以完美地支持常见的企业需求,适合投产使用,未来会被越来越多的企业所采用,只掌握 Vue 2 将远远不能满足企业的技能要求。

本书以 Vue.js 的 3.0 版本为核心技术栈,围绕 “前端工程化” 和 TypeScript 的知识点展开讲解,读者可以收获到:

  1. 了解如何入门前端工程化开发,掌握 Node.js 和 npm 的使用
  2. 掌握前端领域多年来趋势走高、带有类型支持的 TypeScript 语言
  3. 上手主流前端框架 Vue.js 的全新版本,并且在遇到常见问题时知道如何解决

本书的每一个小节都融入了笔者多年的开发实践经验,大部分知识点都搭配了通俗易懂的讲解和可实现的代码案例,在阅读的过程中可以亲自敲代码加强学习印象,毕竟上手一个新技术栈最快的方法,就是一边 Reading 一边 Coding 。

适合人群

本书作为一本入门类教程,主要面向以下读者人群:

  1. 掌握了基础的 HTML 页面编写知识,想学习一个主流前端框架的新手前端工程师
  2. 已经学会了 Vue 2 ,面对 Vue 3 的大版本更新,想快速上手使用的前端工程师
  3. 非职业前端开发,但涉及前端的工作,需要掌握一个主流前端框架的全栈工程师

内容说明

本书根据笔者多年的前端开发经验和一线团队管理经验,将 Vue 3 的知识点按照工程师做项目的顺序梳理成章,一步一步帮助工程师入门前端工程化和 Vue 3 的开发。

书中包含了很多在构建项目过程中容易遇到的问题点和解决方案,对 Vue 3 和 Vue 2 差异化较大的功能点还进行了版本之间的写法对比。

这一点和各个技术栈的官方文档有比较大的不同,官方文档更适合作为一本工具书,方便在需要的时候对 API 进行检索查询。

目前笔者所带领的前端团队已经全员使用 TypeScript 和 Vue 3 进行日常的开发工作,团队成员的学习过程都非常顺利,各位开发者可放心阅读。

TIP

对于从 Vue 2 升级到 Vue 3 之后变化很大的内容,文档的侧边栏添加了图标,如果对 Vue 2 版本已经很熟悉,可以针对带有图标的地方单独查阅。

注:当出现在二级标题时,说明这一节都是新的,如果只出现在三级标题,说明只有那一小部分变化比较大。

学习顺序

推荐按照本书章节的顺序学习,从前端工程化开始上手,到 TypeScript 语言的学习,再到使用 TypeScript 开发 Vue 3 项目,这是一个循序渐进的学习过程。

Vue.js 是一个 “渐进式” 的框架,它可以只用最基础的组件来开发一个小项目,也可以把相关生态引入进来组合成一个大项目(例如 Vue Router 、 Vuex 、 Pinia 等等),本书和 Vue.js 框架一样,也是一个 “渐进式” 的教程,对于本身已经有一定基础的开发者,也可以在遇到具体问题的时候,只阅读对应的部分内容。

在学习的过程中,推荐将已有的其他技术栈项目,使用 Vue 3 重写,使其可以复刻原来的功能,比如一个使用 jQuery 或者是 Vue 2 编写的活动页面或小工具,用 Vue 3 重新实现一遍,看看能否还原出原来的功能,如果能够成功实现,那么说明已经能够掌握 Vue 3 的使用了。

疑惑解答

在学习的过程中如果遇到什么问题无法解决,可以通过以下途径反馈:

  1. 在仓库里提 Issue 或者 Discussions
  2. 发送邮件到笔者的邮箱 chengpeiquan@chengpeiquan.com

如果阅读本书的内容觉得有帮助,欢迎到仓库给个 Star 鼓励

赏个罐头

如果觉得本书的内容有帮助,欢迎 购买纸质版书籍 ,或者捐赠一个猫罐头给作者家的三只猫猫,感恩!

',24),d=e("p",null,"作者养了一只黑色英短、一只红虎斑缅因和一只棕虎斑美短,欢迎在线吸猫:",-1),_=e("h2",{id:"版权许可",tabindex:"-1"},[s("版权许可 "),e("a",{class:"header-anchor",href:"#版权许可","aria-label":'Permalink to "版权许可"'},"​")],-1),f=e("p",null,[s("MIT License © 2020 "),e("a",{href:"https://github.com/chengpeiquan",target:"_blank",rel:"noreferrer"},"chengpeiquan")],-1);function b(g,m,V,q,k,P){const i=n("ImgWrap"),r=n("ClientOnly");return o(),p("div",null,[h,a(r,null,{default:t(()=>[a(i,{src:"/assets/img/donate.jpg",dark:"/assets/img/donate-dark.jpg",alt:"请打开微信扫描二维码"})]),_:1}),d,a(r,null,{default:t(()=>[a(i,{src:"/assets/img/cats.jpg",alt:"从左到右:黑妹、小朋友、弟弟"})]),_:1}),_,f])}const v=l(u,[["render",b]]);export{j as __pageData,v as default}; diff --git a/assets/preface.md.59b338cf.lean.js b/assets/preface.md.59b338cf.lean.js new file mode 100644 index 00000000..19703167 --- /dev/null +++ b/assets/preface.md.59b338cf.lean.js @@ -0,0 +1,8 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as l,v as o,b as p,t as a,O as t,R as c,F as e,L as s,M as n}from"./chunks/framework.0d8bea05.js";const j=JSON.parse('{"title":"前言","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"preface.md","filePath":"preface.md"}'),u={name:"preface.md"},h=c('

前言

Vue 3.0 从 2020 年 9 月份中旬正式发布,到 2022 年 2 月份代替 Vue 2 成为 Vue 的默认版本,经过长达一年半的市场验证,已经证明了它可以完美地支持常见的企业需求,适合投产使用,未来会被越来越多的企业所采用,只掌握 Vue 2 将远远不能满足企业的技能要求。

本书以 Vue.js 的 3.0 版本为核心技术栈,围绕 “前端工程化” 和 TypeScript 的知识点展开讲解,读者可以收获到:

  1. 了解如何入门前端工程化开发,掌握 Node.js 和 npm 的使用
  2. 掌握前端领域多年来趋势走高、带有类型支持的 TypeScript 语言
  3. 上手主流前端框架 Vue.js 的全新版本,并且在遇到常见问题时知道如何解决

本书的每一个小节都融入了笔者多年的开发实践经验,大部分知识点都搭配了通俗易懂的讲解和可实现的代码案例,在阅读的过程中可以亲自敲代码加强学习印象,毕竟上手一个新技术栈最快的方法,就是一边 Reading 一边 Coding 。

适合人群

本书作为一本入门类教程,主要面向以下读者人群:

  1. 掌握了基础的 HTML 页面编写知识,想学习一个主流前端框架的新手前端工程师
  2. 已经学会了 Vue 2 ,面对 Vue 3 的大版本更新,想快速上手使用的前端工程师
  3. 非职业前端开发,但涉及前端的工作,需要掌握一个主流前端框架的全栈工程师

内容说明

本书根据笔者多年的前端开发经验和一线团队管理经验,将 Vue 3 的知识点按照工程师做项目的顺序梳理成章,一步一步帮助工程师入门前端工程化和 Vue 3 的开发。

书中包含了很多在构建项目过程中容易遇到的问题点和解决方案,对 Vue 3 和 Vue 2 差异化较大的功能点还进行了版本之间的写法对比。

这一点和各个技术栈的官方文档有比较大的不同,官方文档更适合作为一本工具书,方便在需要的时候对 API 进行检索查询。

目前笔者所带领的前端团队已经全员使用 TypeScript 和 Vue 3 进行日常的开发工作,团队成员的学习过程都非常顺利,各位开发者可放心阅读。

TIP

对于从 Vue 2 升级到 Vue 3 之后变化很大的内容,文档的侧边栏添加了图标,如果对 Vue 2 版本已经很熟悉,可以针对带有图标的地方单独查阅。

注:当出现在二级标题时,说明这一节都是新的,如果只出现在三级标题,说明只有那一小部分变化比较大。

学习顺序

推荐按照本书章节的顺序学习,从前端工程化开始上手,到 TypeScript 语言的学习,再到使用 TypeScript 开发 Vue 3 项目,这是一个循序渐进的学习过程。

Vue.js 是一个 “渐进式” 的框架,它可以只用最基础的组件来开发一个小项目,也可以把相关生态引入进来组合成一个大项目(例如 Vue Router 、 Vuex 、 Pinia 等等),本书和 Vue.js 框架一样,也是一个 “渐进式” 的教程,对于本身已经有一定基础的开发者,也可以在遇到具体问题的时候,只阅读对应的部分内容。

在学习的过程中,推荐将已有的其他技术栈项目,使用 Vue 3 重写,使其可以复刻原来的功能,比如一个使用 jQuery 或者是 Vue 2 编写的活动页面或小工具,用 Vue 3 重新实现一遍,看看能否还原出原来的功能,如果能够成功实现,那么说明已经能够掌握 Vue 3 的使用了。

疑惑解答

在学习的过程中如果遇到什么问题无法解决,可以通过以下途径反馈:

  1. 在仓库里提 Issue 或者 Discussions
  2. 发送邮件到笔者的邮箱 chengpeiquan@chengpeiquan.com

如果阅读本书的内容觉得有帮助,欢迎到仓库给个 Star 鼓励

赏个罐头

如果觉得本书的内容有帮助,欢迎 购买纸质版书籍 ,或者捐赠一个猫罐头给作者家的三只猫猫,感恩!

',24),d=e("p",null,"作者养了一只黑色英短、一只红虎斑缅因和一只棕虎斑美短,欢迎在线吸猫:",-1),_=e("h2",{id:"版权许可",tabindex:"-1"},[s("版权许可 "),e("a",{class:"header-anchor",href:"#版权许可","aria-label":'Permalink to "版权许可"'},"​")],-1),f=e("p",null,[s("MIT License © 2020 "),e("a",{href:"https://github.com/chengpeiquan",target:"_blank",rel:"noreferrer"},"chengpeiquan")],-1);function b(g,m,V,q,k,P){const i=n("ImgWrap"),r=n("ClientOnly");return o(),p("div",null,[h,a(r,null,{default:t(()=>[a(i,{src:"/assets/img/donate.jpg",dark:"/assets/img/donate-dark.jpg",alt:"请打开微信扫描二维码"})]),_:1}),d,a(r,null,{default:t(()=>[a(i,{src:"/assets/img/cats.jpg",alt:"从左到右:黑妹、小朋友、弟弟"})]),_:1}),_,f])}const v=l(u,[["render",b]]);export{j as __pageData,v as default}; diff --git a/assets/router.md.e17ca07e.js b/assets/router.md.e17ca07e.js new file mode 100644 index 00000000..2a4a36e6 --- /dev/null +++ b/assets/router.md.e17ca07e.js @@ -0,0 +1,503 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as e,v as t,b as c,t as s,O as r,R as n,M as a}from"./chunks/framework.0d8bea05.js";const m=JSON.parse('{"title":"路由的使用","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"router.md","filePath":"router.md"}'),y={name:"router.md"},D=n('

路由的使用

在传统的 Web 开发过程中,当需要实现多个站内页面时,以前需要写很多个 HTML 页面,然后通过 <a /> 标签来实现互相跳转。

在如今工程化模式下的前端开发,像 Vue 工程,可以轻松实现只用一个 HTML 文件,却能够完成多个站内页面渲染、跳转的功能,这就是路由。

TIP

从这里开始,所有包含到 .vue 文件引入的地方,可能会看到 @xx/xx.vue 这样的写法。

@viewssrc/views 的路径别名,@cpsrc/components 的路径别名。

路径别名可以在 vite.config.ts 等构建工具配置文件里添加 alias ,点击了解:添加项目配置

',4),F=n(`

路由的目录结构

Vue 3 引入路由的方式和 Vue 2 一样,路由的管理也是放在 src/router 这个目录下:

bash
src
+ # 路由目录
+├─router
+   # 路由入口文件
+├───index.ts
+   # 路由配置,如果路由很多,可以再拆分模块文件
+├───routes.ts
+ # 项目入口文件
+└─main.ts

其中 index.ts 是路由的入口文件,如果路由很少,那么可以只维护在这个文件里,但对复杂项目来说,往往需要配置上二级、三级路由,逻辑和配置都放到一个文件的话,太臃肿了。

所以如果项目稍微复杂一些,可以像上面这个结构一样拆分成两个文件: index.ts 和 routes.ts ,在 routes.ts 里维护路由树的结构,在 index.ts 导入路由树结构并激活路由,同时可以在该文件里配置路由钩子。

如果项目更加复杂,例如做一个 Admin 后台,可以按照业务模块,再把 routes 拆分得更细,例如 game.ts / member.ts / order.ts 等业务模块,再统一导入到 index.ts 文件里。

TIP

需要注意的是,与 Vue 3 配套的路由版本是 vue-router 4.x 以上才可以正确适配项目。

在项目里引入路由

不管是 Vue 2 还是 Vue 3 ,引入路由都是在 src/router/index.ts 文件里,但是版本升级带来的变化很大,由于本书关于 Vue 3 都是使用 TypeScript ,所以这里只做一个 TypeScript 的变化对比。

TIP

下文可能会出现多次 import.meta.env.BASE_URL 这个变量,它是由 Vite 提供的环境变量,详见 Vite 官网关于 环境变量 的说明。

使用其他构建工具请自行替换为对应构建工具提供的环境变量,例如使用 @vue/cli 创建的项目因为是基于 Webpack ,所以使用的是 process.env.BASE_URL

回顾 Vue 2

Vue 2 的引入方式如下(其中 RouteConfig 是路由项目的 TS 类型)。

ts
import Vue from 'vue'
+import VueRouter from 'vue-router'
+import type { RouteConfig } from 'vue-router'
+
+Vue.use(VueRouter)
+
+const routes: Array<RouteConfig> = [
+  // ...
+]
+
+const router = new VueRouter({
+  mode: 'history',
+  base: import.meta.env.BASE_URL,
+  routes,
+})
+
+export default router

里面一些选项的功能说明:

routes 是路由树的配置,当项目的路由很多的时候可以集中到 routes.ts 管理,然后再 import 进来(具体的配置请看后面的 路由配置部分 说明)。

mode 决定访问路径模式,可配置为 hash 或者 history , Hash 模式是这种 http://abc.com/#/home 这样带 # 号的地址,支持所有浏览器, History 模式是 http://abc.com/home 这样不带 # 号,不仅美观,而且体验更好,但需要服务端做一些配置支持(详见下文的 服务端配置方案 ),也只对主流浏览器支持。

base 是 History 模式在进行路由切换时的基础路径,默认是 / 根目录,如果的项目不是部署在根目录下,而是二级目录、三级目录等多级目录,就必须指定这个 base ,否则路由切换会有问题。

了解 Vue 3 ~new

Vue 3 的引入方式如下(其中 RouteRecordRaw 是路由项目的 TS 类型)。

ts
import { createRouter, createWebHistory } from 'vue-router'
+import type { RouteRecordRaw } from 'vue-router'
+
+const routes: Array<RouteRecordRaw> = [
+  // ...
+]
+
+const router = createRouter({
+  history: createWebHistory(import.meta.env.BASE_URL),
+  routes,
+})
+
+export default router

在 Vue 3 (也就是 vue-router 4.x) 里,路由简化了一些配置项,其中 routes 和 Vue 2 一样,是路由树的配置。

但是 history 和 Vue 2 有所不同,在 Vue 3 ,使用 history 来代替 Vue 2 的 mode ,但功能是一样的,也是决定访问路径模式是 Hash 模式 还是 History 模式,同时合并了 Vue 2 (也就是 vue-router 3.x) 的 base 选项作为模式函数的入参。

和在使用 Vue 2 的时候一样, Vue 3 也可以配置一些额外的路由选项,比如:指定 router-link 为当前激活的路由所匹配的 className :

ts
const router = createRouter({
+  history: createWebHistory(import.meta.env.BASE_URL),
+  linkActiveClass: 'cur',
+  linkExactActiveClass: 'cur',
+  routes,
+})

更多的配置项可以参考 Vue Router 官网的 API 参考 一章。

路由树的配置

引入路由 部分有说到,当项目的路由很多的时候,文件会变得非常长,难以维护,这个时候可以集中到 routes.ts 或者更多的模块化文件管理,然后再 import 到 index.ts 里。

暂且把 routes.ts 这个文件称为 “路由树” ,因为它像一棵大树一样,不仅可以以一级路由为树干去生长,还可以添加二级、三级等多级路由来开枝散叶,下面来看看 routes.ts 应该怎么写。

基础格式 ~new

在 TypeScript 里,路由文件的基础格式由三个部分组成:类型声明、数组结构、模块导出。

ts
// src/router/routes.ts
+
+// 使用 TypeScript 时需要导入路由项目的类型声明
+import type { RouteRecordRaw } from 'vue-router'
+
+// 使用路由项目类型声明一个路由数组
+const routes: Array<RouteRecordRaw> = [
+  // ...
+]
+
+// 将路由数组导出给其他模块使用
+export default routes

之后就可以在 index.ts 里导入使用了。

那么里面的路由数组又是怎么写呢?这里就涉及到了 一级路由多级路由 的编写。

公共基础路径

在配置路由之前,需要先了解 “公共基础路径” 的概念,在讲解使用 Vite 等工具创建项目时,都提到了一个 项目配置 的管理,以 Vite 项目的配置文件 vite.config.ts 为例,里面有一个选项 base ,其实就是用来控制路由的公共基础路径,那么它有什么用呢?

base 的默认值是 /,也就是说,如果不配置它,那么所有的资源文件都是从域名根目录读取,如果项目部署在域名根目录那当然好,但是如果不是呢?那么就必须来配置它了。

配置很简单,只要把项目要上线的最终地址,去掉域名,剩下的那部分就是 base 的值。假设项目是部署在 https://example.com/vue3/ ,那么 base 就可以设置为 /vue3/

TIP

如果路由只有一级,那么 base 也可以设置为相对路径 ./,这样可以把项目部署到任意地方。

如果路由不止一级,那么请准确的指定 base,并且确保是以 / 开头并以 / 结尾,例如 /foo/

一级路由

一级路由,顾名思义,就是在的项目地址后面,只有一级 Path ,比如 https://example.com/home 这里的 home 就是一级路由。

来看一下最基本的路由配置应该包含哪些字段:

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+  },
+]

其中 path 是路由的访问路径,像上面说的,如果的域名是 https://example.com, 配置为 /home,那么访问路径就是 https://example.com/home

TIP

一级路由的 path 都必须是以 / 开头,比如: /home/setting

如果的项目首页不想带上 home 之类的尾巴,只想要 https://example.com/ 这样的域名直达 ,其实也是配置一级路由,只需要把路由的 path 指定为 / 即可。

name 是路由的名称,非必填,但是一般都会配置上去,这样可以很方便的通过 name 来代替 path 实现路由的跳转,因为像有时候的开发环境和生产环境的路径不一致,或者说路径变更,通过 name 无需调整,但如果通过 path,可能就要修改很多文件里面的链接跳转目标了。

component 是路由的模板文件,指向一个 vue 组件,用于指定路由在浏览器端的视图渲染,这里有两种方式来指定使用哪个组件:

同步组件

字段 component 接收一个变量,变量的值就是对应的模板组件。

在打包的时候,会把组件的所有代码都打包到一个文件里,对于大项目来说,这种方式的首屏加载是个灾难,要面对文件过大带来等待时间变长的问题。

ts
import Home from '@views/home.vue'
+
+const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/',
+    name: 'home',
+    component: Home,
+  },
+]

所以现在都推荐使用第二种方式,可以实现 路由懒加载

异步组件

字段 component 接收一个函数,在 return 的时候返回模板组件,同时组件里的代码在打包的时候都会生成独立的文件,并在访问到对应路由的时候按需引入。

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+  },
+]

关于这部分的更多说明,可以查看 路由懒加载

多级路由

在 Vue 路由生态里,支持配置二级、三级、四级等多级路由,理论上没有上限,实际业务中用到的级数通常是三级到四级。

比如做一个美食类网站,打算在 “中餐” 大分类下配置一个 “饺子” 栏目,那么地址就是:

https://example.com/chinese-food/dumplings

这种情况下,中餐 chinese-food 就是一级路由,饺子 dumplings 就是二级路由。

如果想再细化一下,“饺子” 下面再增加一个 “韭菜” 、“白菜” 等不同馅料的子分类:

https://example.com/chinese-food/dumplings/chives

这里的韭菜 chives 就是饺子 dumplings 的子路由,也就是三级路由。

在了解了子路由的概念后,来看一下具体如何配置以及注意事项。

父子路由的关系,都是严格按照 JSON 的层级关系,子路由的信息配置到父级的 children 数组里面,孙路由也是按照一样的格式,配置到子路由的 children 里。

这是一个简单的子路由示范:

ts
const routes: Array<RouteRecordRaw> = [
+  // 注意:这里是一级路由
+  {
+    path: '/lv1',
+    name: 'lv1',
+    component: () => import('@views/lv1.vue'),
+    // 注意:这里是二级路由,在 \`path\` 的前面没有 \`/\`
+    children: [
+      {
+        path: 'lv2',
+        name: 'lv2',
+        component: () => import('@views/lv2.vue'),
+        // 注意:这里是三级路由,在 \`path\` 的前面没有 \`/\`
+        children: [
+          {
+            path: 'lv3',
+            name: 'lv3',
+            component: () => import('@views/lv3.vue'),
+          },
+        ],
+      },
+    ],
+  },
+]

上面这个配置,最终三级路由的访问地址如下:

https://example.com/lv1/lv2/lv3

可以看到在注释里提示了二级、三级路由的 path 字段前面没有 / ,这样路径前面才会有其父级路由的 path 以体现其层级关系,否则会从根目录开始。

路由懒加载

在上面提过,路由在配置 同步组件 的时候,构建出来的文件都集中在一起,大的项目的文件会变得非常大,影响页面加载。

所以 Vue 在 Webpack 的代码分割功能的基础上,推出了 异步组件,可以把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样按需载入,很方便的实现路由组件的懒加载。

在这一段配置里面:

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+  },
+]

起到懒加载配置作用的就是 component 接收的值 () => import('@views/home.vue') ,其中 @views/home.vue 就是路由的组件。

在命令行运行 npm run build 打包构建后,会看到控制台输出的打包结果:

bash
 npm run build
+
+> hello-vue3@0.0.0 build
+> vue-tsc --noEmit && vite build
+
+vite v2.9.15 building for production...
+ 42 modules transformed.
+dist/index.html                       0.42 KiB
+dist/assets/home.03ad1823.js          0.65 KiB / gzip: 0.42 KiB
+dist/assets/HelloWorld.1322d484.js    1.88 KiB / gzip: 0.96 KiB
+dist/assets/about.c2af6d65.js         0.64 KiB / gzip: 0.41 KiB
+dist/assets/login.e9d1d9f9.js         0.65 KiB / gzip: 0.42 KiB
+dist/assets/index.60726771.css        0.47 KiB / gzip: 0.29 KiB
+dist/assets/login.bef803dc.css        0.12 KiB / gzip: 0.10 KiB
+dist/assets/HelloWorld.b2638077.css   0.38 KiB / gzip: 0.19 KiB
+dist/assets/home.ea56cd55.css         0.12 KiB / gzip: 0.10 KiB
+dist/assets/about.a0917080.css        0.12 KiB / gzip: 0.10 KiB
+dist/assets/index.19d6fb3b.js         79.94 KiB / gzip: 31.71 KiB

可以看到路由文件都按照 views 目录下的路由组件和 components 目录下的组件命名,输出了对应的 JS 文件和 CSS 文件,项目部署后, Vue 只会根据当前路由加载需要的文件,其他文件只做预加载,对于大型项目的访问体验非常友好。

而如果不使用路由懒加载,打包出来的文件是这样的:

bash
 npm run build
+
+> hello-vue3@0.0.0 build
+> vue-tsc --noEmit && vite build
+
+vite v2.9.15 building for production...
+ 41 modules transformed.
+dist/index.html                  0.42 KiB
+dist/assets/index.67b1ee4f.css   1.22 KiB / gzip: 0.49 KiB
+dist/assets/index.f758ee53.js    78.85 KiB / gzip: 31.05 KiB

可以看到所有的组件都被打包成了一个很大的 JS 文件和 CSS 文件,没有进行代码分割,对大型项目来说,这种方式打包出来的文件可能会有好几兆,对首屏加载的速度可想而知。

路由的渲染

所有路由组件,要在访问后进行渲染,都必须在父级组件里带有 <router-view /> 标签。

<router-view /> 在哪里,路由组件的代码就渲染在哪个节点上,一级路由的父级组件,就是 src/App.vue 这个根组件。

其中最基础的配置就是 <template /> 里面直接就是写一个 <router-view /> ,整个页面就是路由组件。

vue
<template>
+  <router-view />
+</template>

如果站点带有全局公共组件,比如有全站统一的页头、页脚,只有中间区域才是路由,那么可以这样配置:

vue
<template>
+  <!-- 全局页头 -->
+  <Header />
+
+  <!-- 路由 -->
+  <router-view />
+
+  <!-- 全局页脚 -->
+  <Footer />
+</template>

如果有一部分路由带公共组件,一部分没有,比如大部分页面都需要有侧边栏,但登录页、注册页不需要,就可以这么处理:

vue
<template>
+  <!-- 登录 -->
+  <Login v-if="route.name === 'login'" />
+
+  <!-- 注册 -->
+  <Register v-else-if="route.name === 'register'" />
+
+  <!-- 带有侧边栏的其他路由 -->
+  <div v-else>
+    <!-- 固定在左侧的侧边栏 -->
+    <Sidebar />
+
+    <!-- 路由 -->
+    <router-view />
+  </div>
+</template>

也可以通过 路由元信息 来管理这些规则。

使用 route 获取路由信息 ~new

和 Vue 2 可以直接在组件里使用 this.$route 来获取当前路由信息不同,在 Vue 3 的组件里,Vue 实例既没有了 this,也没有了 $route

要牢记一个事情就是,Vue 3 用啥都要导入,所以获取当前路由信息的正确用法是先导入路由 API :

ts
import { useRoute } from 'vue-router'

再在 setup 里定义一个变量来获取当前路由:

ts
const route = useRoute()

接下来就可以通过定义好的变量 route 去获取当前路由信息了。

当然,如果要在 <template /> 里使用路由,记得把 routesetup 里 return 出去。

ts
// 获取路由名称
+console.log(route.name)
+
+// 获取路由参数
+console.log(route.params.id)

Vue 3 的 route 和 Vue 2 的用法基本一致,日常使用应该很快能上手。

WARNING

但是 Vue 3 的新路由也有一些小变化,有一些属性是被移除了,比如之前获取父级路由信息,很喜欢用的 parent 属性,现在已经没有了,可以在 Vue Router 官网的 从 Vue2 迁移 一章查看所有破坏性变化。

类似被移除的 parent ,如果要获取父级路由信息(比如在做面包屑功能的时候),可以改成下面这样,手动指定倒数第二个为父级信息:

ts
// 获取路由记录
+const matched = route.matched
+
+// 获取该记录的路由个数
+const max = matched.length
+
+// 获取倒数第二个路由(也就是当前路由的父级路由)
+const parentRoute = matched[max - 2]

如果有配置父级路由,那么 parentRoute 就是父级路由信息,否则会返回 undefined

使用 router 操作路由 ~new

route 一样,在 Vue 3 也不能再使用 this.$router ,也必须通过导入路由 API 来使用:

ts
import { useRouter } from 'vue-router'

useRoute 一样, useRouter 也是一个函数,需要在 setup 里定义一个变量来获取路由信息。

ts
const router = useRouter()

接下来就可以通过定义好的变量 router 去操作路由了:

ts
// 跳转首页
+router.push({
+  name: 'home',
+})
+
+// 返回上一页
+router.back()

router-link 是一个全局组件,可直接在 <template /> 里直接使用,无需导入,基础的用法在 Vue 2 和 Vue 3 里是一样。

默认会被转换为一个 a 标签,对比写死的 <a href="..."> ,使用 router-link 会更加灵活。

基础跳转

最基础的用法就是把它当成一个 target="_self"a 标签使用,但无需重新刷新页面,因为是路由跳转,它的体验和使用 router 去进行路由导航的效果完全一样。

vue
<template>
+  <router-link to="/home">首页</router-link>
+</template>

等价于 routerpush

ts
router.push({
+  name: 'home',
+})

可以写个 <div /> 标签绑定 Click 事件达到 router-link 的效果:

vue
<template>
+  <div
+    class="link"
+    @click="
+      router.push({
+        name: 'home',
+      })
+    "
+  >
+    <span>首页</span>
+  </div>
+</template>

了解这种使用对比,对下文其他跳转方式的学习会有帮助。

带参数的跳转

使用 router 的时候,可以轻松的带上参数去那些有 ID 的内容页、用户资料页、栏目列表页等等。

比如要访问一篇文章 https://example.com/article/123 ,用 push 的写法是:

ts
router.push({
+  name: 'article',
+  params: {
+    id: 123,
+  },
+})

同理,从基础跳转的写法,很容易就能猜到在 router-link 里应该怎么写:

vue
<template>
+  <router-link
+    class="link"
+    :to="{
+      name: 'article',
+      params: {
+        id: 123,
+      },
+    }"
+  >
+    这是文章的标题
+  </router-link>
+</template>

不生成 a 标签 ~new

router-link 默认是被转换为一个 a 标签,但根据业务场景,也可以把它指定为生成其他标签,比如 spandivli 等等,这些标签因为不具备 href 属性,所以在跳转时都是通过 Click 事件去执行。

在 Vue 2 ,指定为其他标签只需要一个 tag 属性即可:

vue
<template>
+  <router-link tag="span" to="/home">首页</router-link>
+</template>

但在 Vue 3 ,tag 属性已被移除,需要通过 customv-slot 的配合将其渲染为其他标签。

比如要渲染为一个带有路由导航功能的其他标签:

vue
<template>
+  <router-link to="/home" custom v-slot="{ navigate }">
+    <span class="link" @click="navigate"> 首页 </span>
+  </router-link>
+</template>

渲染后就是一个普通的 <span /> 标签,当该标签被点击的时候,会通过路由的导航跳转到指定的路由页:

html
<!-- 渲染后的标签 -->
+<span class="link">首页</span>

关于这两个属性的参数说明如下:

  1. custom ,一个布尔值,用于控制是否需要渲染为 a 标签,当不包含 custom 或者把 custom 设置为 false 时,则依然使用 a 标签渲染。

  2. v-slot 是一个对象,用来决定标签的行为,它包含了:

字段含义
href解析后的 URL,将会作为一个 a 元素的 href 属性
route解析后的规范化的地址
navigate触发导航的函数,会在必要时自动阻止事件,和 router-link 同理
isActive如果需要应用激活的 class 则为 true,允许应用一个任意的 class
isExactActive如果需要应用精确激活的 class 则为 true,允许应用一个任意的 class

一般来说,v-slot 必备的只有 navigate ,用来绑定元素的点击事件,否则元素点击后不会有任何反应,其他的可以根据实际需求来添加。

TIP

要渲染为非 a 标签,切记两个点:

  1. router-link 必须带上 customv-slot 属性

  2. 最终要渲染的标签,写在 router-link 里,包括对应的 className 和点击事件

在独立 TS/JS 文件里使用路由

除了可以在 .vue 文件里使用路由之外,也可以在单独的 .ts.js 里使用。

比如要做一个带有用户系统的站点,登录的相关代码除了在 login.vue 里运用外,在注册页面 register.vue,用户注册成功还要帮用户执行一次自动登录。

登录完成还要记录用户的登录信息、 Token 、过期时间等等,有不少数据要做处理,以及需要帮助用户自动切去登录前的页面等行为,这是两个不同的组件,如果写两次几乎一样的代码,会大大提高维护成本。

这种情况下就可以通过抽离核心代码,封装成一个 login.ts 文件,在这个独立的 ts 文件里去操作路由。

ts
// 导入路由
+import router from '@/router'
+
+// 执行路由跳转
+router.push({
+  name: 'home',
+})

路由元信息配置

有时候的项目需要一些个性化配置,比如:

  1. 每个路由给予独立的标题;

  2. 管理后台的路由,部分页面需要限制一些访问权限;

  3. 通过路由来自动生成侧边栏、面包屑;

  4. 部分路由的生命周期需要做缓存( Keep Alive );

  5. 其他更多业务场景…

无需维护很多套配置,在定义路由树的时候可以配置 meta 字段,比如下面就是包含了多种元信息的一个登录路由:

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/login',
+    name: 'login',
+    component: () => import('@views/login.vue'),
+    meta: {
+      title: '登录',
+      isDisableBreadcrumbLink: true,
+      isShowBreadcrumb: false,
+      addToSidebar: false,
+      sidebarIcon: '',
+      sidebarIconAlt: '',
+      isNoLogin: true,
+    },
+  },
+]

这个是笔者曾经在做后台项目时用过的一些配置,主要的功能是:

字段类型含义
titlestring用于在渲染的时候配置浏览器标题;
isDisableBreadcrumbLinkboolean是否禁用面包屑链接(对一些没有内容的路由可以屏蔽访问);
isShowBreadcrumbboolean是否显示面包屑(此处的登录页不需要面包屑);
addToSidebarboolean是否加入侧边栏(此处的登录页不需要加入侧边栏);
sidebarIconstring配置侧边栏的图标 className(默认);
sidebarIconAltstring配置侧边栏的图标 className(展开状态);
isNoLoginboolean是否免登录(后台默认强制登录,设置为 true 则可以免登录访问,此处的登录页不需要校验);

类似的,如果有其他需求,比如要增加对不同用户组的权限控制(比如有管理员、普通用户分组,部分页面只有管理员允许访问),都可以通过配置 Meta 里的字段,再配合 路由拦截 一起使用。

路由重定向

对一些已下线的页面,直接访问原来的地址会导致 404 ,为了避免这种情况出现,通常会配置重定向将其指向一个新的页面,或者跳转回首页。

基本用法

路由重定向是使用一个 redirect 字段进行配置到对应的路由里面去实现跳转:

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+    meta: {
+      title: 'Home',
+    },
+  },
+  // 访问这个路由会被重定向到首页
+  {
+    path: '/error',
+    redirect: '/',
+  },
+]

通常来说,配置了 redirect 的路由,只需要指定 2 个字段即可,1 个是 path 该路由本身的路径,1 个是 redirect 目标路由的路径,其他字段可以忽略。

redirect 字段可以接收三种类型的值:

类型填写的值
string另外一个路由的 path
route另外一个路由(类似 router.push
function可以判断不同情况的重定向目标,最终 return 一个 path 或者 route

业务场景

路由重定向可以避免用户访问到一些无效路由页面:

  1. 比如项目上线了一段时间后,有个路由需要改名,或者调整路径层级,可以把旧路由重定向到新的,避免原来的用户从收藏夹等地方进来后找不到

  2. 一些容易打错的地址,比如通常个人资料页都是用 /profile,但是业务网站是使用 /account,那也可以把 /profile 重定向到 /account

  3. 对于一些有会员体系的站点,可以根据用户权限进行重定向,分别指向他们具备访问权限的页面

  4. 官网首页在 PC 端、移动端、游戏内嵌横屏版分别有 3 套页面,但希望能通过主域名来识别不同设备,帮助用户自动切换访问

了解了业务场景,接下来就能比较清晰的了解应该如何配置重定向了。

配置为 path

最常用的场景,恐怕就是首页的指向了,比如首页地址是 https://example.com/home,但是想让主域名 https://example.com/ 也能跳转到 /home,可以这么配置:

这是最简单的配置方式,把目标路由的 path 配置进来就可以了:

ts
const routes: Array<RouteRecordRaw> = [
+  // 重定向到 \`/home\`
+  {
+    path: '/',
+    redirect: '/home',
+  },
+  // 真正的首页
+  {
+    path: '/home',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+  },
+]

但缺点也显而易见,只能针对那些不带参数的路由。

配置为 route

如果想要重定向后的路由地址带上一些参数,可以配置为 route

ts
const routes: Array<RouteRecordRaw> = [
+  // 重定向到 \`/home\` ,并带上一个 \`query\` 参数
+  {
+    path: '/',
+    redirect: {
+      name: 'home',
+      query: {
+        from: 'redirect',
+      },
+    },
+  },
+  // 真正的首页
+  {
+    path: '/home',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+  },
+]

最终访问的地址就是 https://example.com/home?from=redirect, 像这样带有来路参数的,就可以在 “百度统计” 或者 “ CNZZ 统计” 之类的统计站点查看来路的流量。

配置为 function

结合业务场景来解释是最直观的,比如的网站有 3 个用户组,一个是管理员,一个是普通用户,还有一个是游客(未登录),他们的网站首页是不一样的:

用户组访问首页时
管理员具备各种数据可视化图表、最新的网站数据、一些最新的用户消息等等
普通用户只有一些常用模块的入口链接
未登录用户直接跳转到登录页面

产品需要在访问网站主域名的时候,识别用户身份跳转不同的首页,那么就可以这样配置路由重定向:

ts
const routes: Array<RouteRecordRaw> = [
+  // 访问主域名时,根据用户的登录信息,重定向到不同的页面
+  {
+    path: '/',
+    redirect: () => {
+      // \`loginInfo\` 是当前用户的登录信息
+      // 可以从 \`localStorage\` 或者 \`Pinia\` 读取
+      const { groupId } = loginInfo
+
+      // 根据组别 ID 进行跳转
+      switch (groupId) {
+        // 管理员跳去仪表盘
+        case 1:
+          return '/dashboard'
+
+        // 普通用户跳去首页
+        case 2:
+          return '/home'
+
+        // 其他都认为未登录,跳去登录页
+        default:
+          return '/login'
+      }
+    },
+  },
+]

路由别名配置

根据的业务需求,也可以为路由指定一个别名,与上面的 路由重定向 功能相似,但又有不同:

配置了路由重定向,当用户访问 /a 时,URL 将会被替换成 /b,然后匹配的实际路由是 /b

配置了路由别名,/a 的别名是 /b,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样。

配置方法

添加一个 alias 字段即可轻松实现:

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/home',
+    alias: '/index',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+  },
+]

如上的配置,即可实现可以通过 /home 访问首页,也可以通过 /index 访问首页。

404 路由页面配置 ~new

可以配置一个 404 路由来代替站内的 404 页面。

配置方法

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/:pathMatch(.*)*',
+    name: '404',
+    component: () => import('@views/404.vue'),
+  },
+]

这样配置之后,只要访问到不存在的路由,就会显示为这个 404 模板。

WARNING

新版的路由不再支持直接配置通配符 * ,而是必须使用带有自定义正则表达式的参数进行定义,详见官网 删除了 *(星标或通配符)路由 的说明。

导航守卫

和 Vue 2 时使用的路由一样, Vue 3 也支持导航守卫,并且用法基本上是一样的。

导航守卫这个词对初次接触的开发者来说应该会有点云里雾里,其实就是几个专属的钩子函数,先来看一下使用场景,大致理解一下基本概念和作用。

钩子的应用场景

对于导航守卫还不熟悉的开发者,可以从一些实际使用场景来加强印象,比如:

  1. 前面说的,在渲染的时候配置浏览器标题,由于 Vue 项目只有一个 HTML 文件,所以默认只有一个标题,但想在访问 /home 的时候标题显示为 “首页”,访问 /about 的时候标题显示为 “关于” 。

  2. 部分页面需要管理员才能访问,普通用户不允许进入到该路由页面。

  3. Vue 单页面项目,传统的 CNZZ / 百度统计等网站统计代码只会在页面加载的时候统计一次,但需要每次切换路由都上报一次 PV 数据。

这样的场景还有很多,导航守卫支持全局使用,也可以在 .vue 文件里单独使用,接下来看看具体的用法。

路由里的全局钩子

顾名思义,是在创建 router 的时候进行全局的配置,也就是说,只要配置了钩子,那么所有的路由在被访问到的时候,都会触发这些钩子函数。

可用钩子含义触发时机
beforeEach全局前置守卫在路由跳转前触发
beforeResolve全局解析守卫在导航被确认前,同时在组件内守卫和异步路由组件被解析后
afterEach全局后置守卫在路由跳转完成后触发

全局配置非常简单,在 src/router/index.ts 里,在创建路由之后、在导出去之前使用:

ts
import { createRouter } from 'vue-router'
+
+// 创建路由
+const router = createRouter({ ... })
+
+// 在这里调用导航守卫的钩子函数
+router.beforeEach((to, from) => {
+  // ...
+})
+
+// 导出去
+export default router

beforeEach ~new

全局前置守卫,这是导航守卫里面运用的最多的一个钩子函数,通常将其称为 “路由拦截”。

拦截这个词,顾名思义,就是在 XXX 目的达到之前,把它拦下来,所以路由的目的就是渲染指定的组件,路由拦截就是在组件被渲染之前,做一些拦截操作。

参数

参数作用
to即将要进入的路由对象
from当前导航正要离开的路由

TIP

和 Vue 2 不同,Vue 2 的 beforeEach 是默认三个参数,第三个参数是 next,用来操作路由接下来的跳转。

但在新版本路由里,已经通过 RFC 将其删除,虽然目前还是作为可选参数使用,但以后不确定是否会移除,不建议继续使用,点击查看原因

新版本路由可以通过 return 来代替 next

用法

比如在进入路由之前,根据 Meta 路由元信息 的配置,设定路由的网页标题:

ts
router.beforeEach((to, from) => {
+  const { title } = to.meta
+  document.title = title || '默认标题'
+})

或者判断是否需要登录:

ts
router.beforeEach((to, from) => {
+  const { isNoLogin } = to.meta
+  if (!isNoLogin) return '/login'
+})

或者针对一些需要 ID 参数,但参数丢失的路由做拦截,比如:很多网站的文章详情页都是类似 https://example.com/article/123 这样格式的地址,是需要带有文章 ID 作为 URL 的一部分,如果只访问 https://example.com/article 则需要拦截掉。

这里是关于 article 路由的配置,是有要求 Params 要带上 ID 参数:

ts
const routes: Array<RouteRecordRaw> = [
+  // 这是一个配置了 \`params\` ,访问的时候必须带 \`id\` 的路由
+  {
+    path: '/article/:id',
+    name: 'article',
+    component: () => import('@views/article.vue'),
+  },
+  // ...
+]

当路由的 params 丢失的时候,路由记录 matched 是一个空数组,针对这样的情况,就可以配置一个拦截,丢失参数时返回首页:

ts
router.beforeEach((to) => {
+  if (to.name === 'article' && to.matched.length === 0) {
+    return '/'
+  }
+})

beforeResolve

全局解析守卫,它会在每次导航时触发,但是在所有组件内守卫和异步路由组件被解析之后,将在确认导航之前被调用。

这个钩子用的比较少,因为它和 beforeEach 非常相似,相信大部分开发者都是会用 beforeEach 来代替它。

那么它有什么用?

它通常会用在一些申请权限的环节,比如一些 H5 页面需要申请系统相机权限、一些微信活动需要申请微信的登录信息授权,获得权限之后才允许获取接口数据和给用户更多的操作,使用 beforeEach 时机太早,使用 afterEach 又有点晚,那么这个钩子的时机就刚刚好。

参数

参数作用
to即将要进入的路由对象
from当前导航正要离开的路由

用法

以以前 Vue Router 官网的申请照相机权限的例子来举例:

ts
// https://router.vuejs.org/zh/guide/advanced/navigation-guards.html
+
+router.beforeResolve(async (to) => {
+  // 如果路由配置了必须调用相机权限
+  if (to.meta.requiresCamera) {
+    // 正常流程,咨询是否允许使用照相机
+    try {
+      await askForCameraPermission()
+    } catch (error) {
+      // 容错
+      if (error instanceof NotAllowedError) {
+        // ... 处理错误,然后取消导航
+        return false
+      } else {
+        // 如果出现意外,则取消导航并抛出错误
+        throw error
+      }
+    }
+  }
+})

afterEach

全局后置守卫,这也是导航守卫里面用的比较多的一个钩子函数。

参数

参数作用
to即将要进入的路由对象
from当前导航正要离开的路由

用法

在刚刚的 钩子的应用场景 里面有个例子,就是每次切换路由都上报一次 PV 数据,类似这种每个路由都要执行一次,但又不必在渲染前操作的,都可以放到后置钩子里去执行。

笔者之前写过两个数据统计的插件:Vue 版 CNZZ 统计Vue 版百度统计,就是用的这个后置钩子来实现自动上报数据。

ts
router.afterEach((to, from) => {
+  // 上报流量的操作
+  // ...
+})

在组件内使用全局钩子

上面所讲的都是全局钩子,虽然一般都是在路由文件里使用,但如果有需要,也可以在 .vue 文件里操作。

TIP

和路由的渲染不同,渲染是父级路由组件必须带有 <router-view /> 标签才能渲染,但是使用全局钩子不受此限制。

建议只在一些入口文件里使用,比如 App.vue ,或者是一些全局的 Header.vueFooter.vue 里使用,方便后续维护。

setup 里,定义一个 router 变量获取路由之后,就可以操作了:

ts
import { defineComponent } from 'vue'
+import { useRouter } from 'vue-router'
+
+export default defineComponent({
+  setup() {
+    // 定义路由
+    const router = useRouter()
+
+    // 调用全局钩子
+    router.beforeEach((to, from) => {
+      // ...
+    })
+  },
+})

路由里的独享钩子

介绍完全局钩子,如果只是有个别路由要做处理,可以使用路由独享的守卫,用来针对个别路由定制一些特殊功能,可以减少在全局钩子里面写一堆判断。

可用钩子含义触发时机
beforeEnter路由独享前置守卫在路由跳转前触发

注:路由独享的钩子,必须配置在 routes 的 JSON 树里面,挂在对应的路由下面(与 pathnamemeta 这些字段同级)。

beforeEnter ~new

它和全局钩子 beforeEach 的作用相同,都是在进入路由之前触发,触发时机比 beforeResolve 要早。

顺序:beforeEach(全局) > beforeEnter(独享) > beforeResolve(全局)。

参数

参数作用
to即将要进入的路由对象
from当前导航正要离开的路由

TIP

beforeEach 一样,也是取消了 next,可以通过 return 来代替。

用法

比如:整个站点的默认标题都是以 “栏目标题” + “全站关键标题” 的格式作为网页的 Title ,例如 “项目经验 - 程沛权” ,但在首页的时候,想做一些不一样的定制。

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/home',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+    // 在这里添加单独的路由守卫
+    beforeEnter: (to, from) => {
+      document.title = '程沛权 - 养了三只猫'
+    },
+  },
+]

就可以通过 beforeEnter 来实现一些个别路由的单独定制。

TIP

需要注意的是,只有从不同的路由切换进来,才会触发该钩子。

针对同一个路由,但是不同的 paramsquery 或者 hash ,都不会重复触发该钩子。

比如从 https://example.com/article/123 切换到 https://example.com/article/234 是不会触发的。

其他的用法和 beforeEach 可以说是一样的。

组件内单独使用 ~new

组件里除了可以使用全局钩子外,还可以使用组件专属的路由钩子。

可用钩子含义触发时机
onBeforeRouteUpdate组件内的更新守卫在当前路由改变,但是该组件被复用时调用
onBeforeRouteLeave组件内的离开守卫导航离开该组件的对应路由时调用

TIP

1、组件内钩子的入参,也都是取消了 next,可以通过 return 来代替。

2、和其他 Composition API 一样,需要先 import 再操作。

和旧版路由不同,新版的 Composition API 移除了 beforeRouteEnter 这个钩子了。

onBeforeRouteUpdate

可以在当前路由改变,但是该组件被复用时,重新调用里面的一些函数用来更新模板数据的渲染。

参数

参数作用
to即将要进入的路由对象
from当前导航正要离开的路由

用法

比如一个内容网站,通常在文章详情页底部会有相关阅读推荐,这个时候就会有一个操作场景是,从文章 A 跳转到文章 B。

比如从 https://example.com/article/111 切去 https://example.com/article/222 ,这种情况就属于 “路由改变,但是组件被复用” 的情况了。

这种情况下,原本放在 onMounted 里执行数据请求的函数就不会被调用,可以借助该钩子来实现渲染新的文章内容。

ts
import { defineComponent, onMounted } from 'vue'
+import { useRoute, onBeforeRouteUpdate } from 'vue-router'
+
+export default defineComponent({
+  setup() {
+    // 其他代码略...
+
+    // 查询文章详情
+    async function queryArticleDetail(id: number) {
+      // 请求接口数据
+      const res = await axios({
+        url: \`/article/\${id}\`,
+      })
+      // ...
+    }
+
+    // 组件挂载完成后执行文章内容的请求
+    // 注意这里是获取 \`route\` 的 \`params\`
+    onMounted(async () => {
+      const id = Number(route.params.id) || 0
+      await queryArticleDetail(id)
+    })
+
+    // 组件被复用时重新请求新的文章内容
+    onBeforeRouteUpdate(async (to, from) => {
+      // ID 不变时减少重复请求
+      if (to.params.id === from.params.id) return
+
+      // 注意这里是获取 \`to\` 的 \`params\`
+      const id = Number(to.params.id) || 0
+      await queryArticleDetail(id)
+    })
+  },
+})

onBeforeRouteLeave

可以在离开当前路由之前,实现一些离开前的判断拦截。

参数

参数作用
to即将要进入的路由对象
from当前导航正要离开的路由

用法

这个离开守卫通常用来禁止用户在还未保存修改前突然离开,可以通过 return false 来取消用户离开当前路由。

ts
import { defineComponent } from 'vue'
+import { onBeforeRouteLeave } from 'vue-router'
+
+export default defineComponent({
+  setup() {
+    // 调用离开守卫
+    onBeforeRouteLeave((to, from) => {
+      // 弹出一个确认框
+      const confirmText = '确认要离开吗?您的更改尚未保存!'
+      const isConfirmLeave = window.confirm(confirmText)
+
+      // 当用户点取消时,不离开路由
+      if (!isConfirmLeave) {
+        return false
+      }
+    })
+  },
+})

路由侦听 ~new

路由的侦听,可以延续以往的 watch 大法,也可以用全新的 watchEffect

watch

在 Vue 2 的时候,侦听路由变化用的最多的就是 watch 了, Vue 3 的 watch API 使用更简单。

侦听整个路由

可以跟以前一样,直接侦听整个路由的变化:

ts
import { defineComponent, watch } from 'vue'
+import { useRoute } from 'vue-router'
+
+export default defineComponent({
+  setup() {
+    const route = useRoute()
+
+    // 侦听整个路由
+    watch(route, (to, from) => {
+      // 处理一些事情
+      // ...
+    })
+  },
+})

第一个参数传入整个路由;第二个参数是个 Callback ,可以获取 tofrom 来判断路由变化情况。

侦听路由的某个数据

如果只想侦听路由的某个数据变化,比如侦听一个 Query ,或者一个 Param ,可以采用这种方式:

ts
import { defineComponent, watch } from 'vue'
+import { useRoute } from 'vue-router'
+
+export default defineComponent({
+  setup() {
+    const route = useRoute()
+
+    // 侦听路由参数的变化
+    watch(
+      () => route.query.id,
+      () => {
+        console.log('侦听到 ID 变化')
+      }
+    )
+  },
+})

第一个参数传入一个 getter 函数, return 要侦听的值;第二个参数是个 Callback ,可以针对参数变化进行一些操作。

watchEffect

这是 Vue 3 新出的一个侦听函数,可以简化 watch 的行为。

比如定义了一个函数,通过路由的参数来获取文章 ID ,然后请求文章内容:

ts
import { defineComponent, watchEffect } from 'vue'
+import { useRoute } from 'vue-router'
+
+export default defineComponent({
+  setup() {
+    const route = useRoute()
+
+    // 从接口查询文章详情
+    async function queryArticleDetail() {
+      const id = Number(route.params.id) || 0
+      console.log('文章 ID 是:', id)
+
+      const res = await axios({
+        url: \`/article/\${id}\`,
+      })
+      // ...
+    }
+
+    // 直接侦听包含路由参数的那个函数
+    watchEffect(queryArticleDetail)
+  },
+})

对比 watch 的使用, watchEffect 在操作上更加简单,把包含要被侦听数据的函数,当成它的入参传进去即可。

部署问题与服务端配置

通常使用路由的 Hash 模式,部署后有问题的情况很少,但是如果使用 History 模式,可能会遇到这样那样的问题。

常见部署问题

这里整理一些常见部署问题的原因分析和解决方案,可作参考。

页面刷新就 404

页面部署到服务端之后,访问首页正常;通过导航上面的链接进行路由跳转,也正常;但是刷新页面就变成 404 了。

问题原因

一般这种情况是路由开启了 History 模式,但是服务端没有配置功能支持。

解决方案

请根据 服务端配置 部分的说明,与的运维同事沟通,让他帮忙修改服务端的配置。

部分路由白屏

如果在项目配置文件里,把里面的 publicPath (使用 Vue CLI ) 或者 base (使用 Vite ) 配置成相对路径 ./ ,但是路由配置了二级或以上,那么就会出现这个问题。

问题原因

原因是打包后的 JS 、 CSS 等静态资源都是存放在项目根目录下,一级路由的 ./ 就是根目录,所以访问正常;而二级路由的 ./ 则不是根目录了,是从当前目录载入的 ,这就导致无法正确载入 JS 文件,从而导致了白屏。

假设项目域名是 https://example.com ,那么:

  • 一级路由是 https://example.com/home
  • 二级路由是 https://example.com/foo/bar
  • 假设打包后的 JS 文件等静态资产存放于 https://example.com/assets/ 文件夹下

访问一级路由时, ./ 访问到的 JS 文件是 https://example.com/assets/home.js ,所以一级路由可以正常访问到。

访问二级路由时, ./ 访问到的 JS 文件是 https://example.com/foo/assets/bar.js ,但实际上文件是存放在 https://example.com/assets/bar.js ,访问到的 URL 资源不存在,所以白屏了。

解决方案

如果的项目开启了 History 模式,并且配置有二级或者二级以上的路由时,不要使用 ./ 这样的相对路径。

正确的方式应该是修改 publicPath (使用 Vue CLI ) 或者 base (使用 Vite ),如果是部署在域名根目录则写 / ,如果是子目录,则按照子目录的格式,将其以 / 开头,以 / 结尾的形式配置( e.g. /hello-world/

服务端配置方案

如果使用的是 HTML5 的 History 模式,那么服务端也需要配置对应的支持,否则会出现路由跳转正常,但页面一刷新就 404 的情况。

TIP

服务端配置后,就不再进入 404 了,需要在项目里手动配置 404 路由页面 的路由。

Nginx

现在大部分公司的服务程序都在使用 Nginx ,可以将以下代码发给运维工程师参考,调整 Nginx 的配置:

nginx
location / {
+  try_files $uri $uri/ /index.html;
+}

Express

如果是前端工程师使用 Node.js 作服务端,并且使用了 Express 服务端框架,那么操作将变得更简单:

  1. 仅需要安装一个中间件
bash
npm install connect-history-api-fallback
  1. 在服务启动入口文件里导入该中间件并激活
js
const express = require('express')
+const history = require('connect-history-api-fallback')
+
+// 创建 Express 实例
+const app = express()
+app
+  // 启用 History 中间件
+  .use(history())
+  // 这里是读取打包后的页面文件目录
+  .use('/', express.static(resolve('../dist')))

更多用法可以看: connect-history-api-fallback 的文档。

更多方案

其他的诸如 Apache 、 IIS 、或者原生 Node 等等配置方案, Vue 官方都提供了对应的演示代码,点击查看更多配置方案: 服务器配置示例

`,339);function i(C,A,d,h,u,g){const l=a("ReadingTips"),o=a("GitalkComment"),p=a("ClientOnly");return t(),c("div",null,[D,s(l),F,s(p,null,{default:r(()=>[s(o,{issueId:47})]),_:1})])}const b=e(y,[["render",i]]);export{m as __pageData,b as default}; diff --git a/assets/router.md.e17ca07e.lean.js b/assets/router.md.e17ca07e.lean.js new file mode 100644 index 00000000..2a4a36e6 --- /dev/null +++ b/assets/router.md.e17ca07e.lean.js @@ -0,0 +1,503 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as e,v as t,b as c,t as s,O as r,R as n,M as a}from"./chunks/framework.0d8bea05.js";const m=JSON.parse('{"title":"路由的使用","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"router.md","filePath":"router.md"}'),y={name:"router.md"},D=n('

路由的使用

在传统的 Web 开发过程中,当需要实现多个站内页面时,以前需要写很多个 HTML 页面,然后通过 <a /> 标签来实现互相跳转。

在如今工程化模式下的前端开发,像 Vue 工程,可以轻松实现只用一个 HTML 文件,却能够完成多个站内页面渲染、跳转的功能,这就是路由。

TIP

从这里开始,所有包含到 .vue 文件引入的地方,可能会看到 @xx/xx.vue 这样的写法。

@viewssrc/views 的路径别名,@cpsrc/components 的路径别名。

路径别名可以在 vite.config.ts 等构建工具配置文件里添加 alias ,点击了解:添加项目配置

',4),F=n(`

路由的目录结构

Vue 3 引入路由的方式和 Vue 2 一样,路由的管理也是放在 src/router 这个目录下:

bash
src
+ # 路由目录
+├─router
+   # 路由入口文件
+├───index.ts
+   # 路由配置,如果路由很多,可以再拆分模块文件
+├───routes.ts
+ # 项目入口文件
+└─main.ts

其中 index.ts 是路由的入口文件,如果路由很少,那么可以只维护在这个文件里,但对复杂项目来说,往往需要配置上二级、三级路由,逻辑和配置都放到一个文件的话,太臃肿了。

所以如果项目稍微复杂一些,可以像上面这个结构一样拆分成两个文件: index.ts 和 routes.ts ,在 routes.ts 里维护路由树的结构,在 index.ts 导入路由树结构并激活路由,同时可以在该文件里配置路由钩子。

如果项目更加复杂,例如做一个 Admin 后台,可以按照业务模块,再把 routes 拆分得更细,例如 game.ts / member.ts / order.ts 等业务模块,再统一导入到 index.ts 文件里。

TIP

需要注意的是,与 Vue 3 配套的路由版本是 vue-router 4.x 以上才可以正确适配项目。

在项目里引入路由

不管是 Vue 2 还是 Vue 3 ,引入路由都是在 src/router/index.ts 文件里,但是版本升级带来的变化很大,由于本书关于 Vue 3 都是使用 TypeScript ,所以这里只做一个 TypeScript 的变化对比。

TIP

下文可能会出现多次 import.meta.env.BASE_URL 这个变量,它是由 Vite 提供的环境变量,详见 Vite 官网关于 环境变量 的说明。

使用其他构建工具请自行替换为对应构建工具提供的环境变量,例如使用 @vue/cli 创建的项目因为是基于 Webpack ,所以使用的是 process.env.BASE_URL

回顾 Vue 2

Vue 2 的引入方式如下(其中 RouteConfig 是路由项目的 TS 类型)。

ts
import Vue from 'vue'
+import VueRouter from 'vue-router'
+import type { RouteConfig } from 'vue-router'
+
+Vue.use(VueRouter)
+
+const routes: Array<RouteConfig> = [
+  // ...
+]
+
+const router = new VueRouter({
+  mode: 'history',
+  base: import.meta.env.BASE_URL,
+  routes,
+})
+
+export default router

里面一些选项的功能说明:

routes 是路由树的配置,当项目的路由很多的时候可以集中到 routes.ts 管理,然后再 import 进来(具体的配置请看后面的 路由配置部分 说明)。

mode 决定访问路径模式,可配置为 hash 或者 history , Hash 模式是这种 http://abc.com/#/home 这样带 # 号的地址,支持所有浏览器, History 模式是 http://abc.com/home 这样不带 # 号,不仅美观,而且体验更好,但需要服务端做一些配置支持(详见下文的 服务端配置方案 ),也只对主流浏览器支持。

base 是 History 模式在进行路由切换时的基础路径,默认是 / 根目录,如果的项目不是部署在根目录下,而是二级目录、三级目录等多级目录,就必须指定这个 base ,否则路由切换会有问题。

了解 Vue 3 ~new

Vue 3 的引入方式如下(其中 RouteRecordRaw 是路由项目的 TS 类型)。

ts
import { createRouter, createWebHistory } from 'vue-router'
+import type { RouteRecordRaw } from 'vue-router'
+
+const routes: Array<RouteRecordRaw> = [
+  // ...
+]
+
+const router = createRouter({
+  history: createWebHistory(import.meta.env.BASE_URL),
+  routes,
+})
+
+export default router

在 Vue 3 (也就是 vue-router 4.x) 里,路由简化了一些配置项,其中 routes 和 Vue 2 一样,是路由树的配置。

但是 history 和 Vue 2 有所不同,在 Vue 3 ,使用 history 来代替 Vue 2 的 mode ,但功能是一样的,也是决定访问路径模式是 Hash 模式 还是 History 模式,同时合并了 Vue 2 (也就是 vue-router 3.x) 的 base 选项作为模式函数的入参。

和在使用 Vue 2 的时候一样, Vue 3 也可以配置一些额外的路由选项,比如:指定 router-link 为当前激活的路由所匹配的 className :

ts
const router = createRouter({
+  history: createWebHistory(import.meta.env.BASE_URL),
+  linkActiveClass: 'cur',
+  linkExactActiveClass: 'cur',
+  routes,
+})

更多的配置项可以参考 Vue Router 官网的 API 参考 一章。

路由树的配置

引入路由 部分有说到,当项目的路由很多的时候,文件会变得非常长,难以维护,这个时候可以集中到 routes.ts 或者更多的模块化文件管理,然后再 import 到 index.ts 里。

暂且把 routes.ts 这个文件称为 “路由树” ,因为它像一棵大树一样,不仅可以以一级路由为树干去生长,还可以添加二级、三级等多级路由来开枝散叶,下面来看看 routes.ts 应该怎么写。

基础格式 ~new

在 TypeScript 里,路由文件的基础格式由三个部分组成:类型声明、数组结构、模块导出。

ts
// src/router/routes.ts
+
+// 使用 TypeScript 时需要导入路由项目的类型声明
+import type { RouteRecordRaw } from 'vue-router'
+
+// 使用路由项目类型声明一个路由数组
+const routes: Array<RouteRecordRaw> = [
+  // ...
+]
+
+// 将路由数组导出给其他模块使用
+export default routes

之后就可以在 index.ts 里导入使用了。

那么里面的路由数组又是怎么写呢?这里就涉及到了 一级路由多级路由 的编写。

公共基础路径

在配置路由之前,需要先了解 “公共基础路径” 的概念,在讲解使用 Vite 等工具创建项目时,都提到了一个 项目配置 的管理,以 Vite 项目的配置文件 vite.config.ts 为例,里面有一个选项 base ,其实就是用来控制路由的公共基础路径,那么它有什么用呢?

base 的默认值是 /,也就是说,如果不配置它,那么所有的资源文件都是从域名根目录读取,如果项目部署在域名根目录那当然好,但是如果不是呢?那么就必须来配置它了。

配置很简单,只要把项目要上线的最终地址,去掉域名,剩下的那部分就是 base 的值。假设项目是部署在 https://example.com/vue3/ ,那么 base 就可以设置为 /vue3/

TIP

如果路由只有一级,那么 base 也可以设置为相对路径 ./,这样可以把项目部署到任意地方。

如果路由不止一级,那么请准确的指定 base,并且确保是以 / 开头并以 / 结尾,例如 /foo/

一级路由

一级路由,顾名思义,就是在的项目地址后面,只有一级 Path ,比如 https://example.com/home 这里的 home 就是一级路由。

来看一下最基本的路由配置应该包含哪些字段:

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+  },
+]

其中 path 是路由的访问路径,像上面说的,如果的域名是 https://example.com, 配置为 /home,那么访问路径就是 https://example.com/home

TIP

一级路由的 path 都必须是以 / 开头,比如: /home/setting

如果的项目首页不想带上 home 之类的尾巴,只想要 https://example.com/ 这样的域名直达 ,其实也是配置一级路由,只需要把路由的 path 指定为 / 即可。

name 是路由的名称,非必填,但是一般都会配置上去,这样可以很方便的通过 name 来代替 path 实现路由的跳转,因为像有时候的开发环境和生产环境的路径不一致,或者说路径变更,通过 name 无需调整,但如果通过 path,可能就要修改很多文件里面的链接跳转目标了。

component 是路由的模板文件,指向一个 vue 组件,用于指定路由在浏览器端的视图渲染,这里有两种方式来指定使用哪个组件:

同步组件

字段 component 接收一个变量,变量的值就是对应的模板组件。

在打包的时候,会把组件的所有代码都打包到一个文件里,对于大项目来说,这种方式的首屏加载是个灾难,要面对文件过大带来等待时间变长的问题。

ts
import Home from '@views/home.vue'
+
+const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/',
+    name: 'home',
+    component: Home,
+  },
+]

所以现在都推荐使用第二种方式,可以实现 路由懒加载

异步组件

字段 component 接收一个函数,在 return 的时候返回模板组件,同时组件里的代码在打包的时候都会生成独立的文件,并在访问到对应路由的时候按需引入。

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+  },
+]

关于这部分的更多说明,可以查看 路由懒加载

多级路由

在 Vue 路由生态里,支持配置二级、三级、四级等多级路由,理论上没有上限,实际业务中用到的级数通常是三级到四级。

比如做一个美食类网站,打算在 “中餐” 大分类下配置一个 “饺子” 栏目,那么地址就是:

https://example.com/chinese-food/dumplings

这种情况下,中餐 chinese-food 就是一级路由,饺子 dumplings 就是二级路由。

如果想再细化一下,“饺子” 下面再增加一个 “韭菜” 、“白菜” 等不同馅料的子分类:

https://example.com/chinese-food/dumplings/chives

这里的韭菜 chives 就是饺子 dumplings 的子路由,也就是三级路由。

在了解了子路由的概念后,来看一下具体如何配置以及注意事项。

父子路由的关系,都是严格按照 JSON 的层级关系,子路由的信息配置到父级的 children 数组里面,孙路由也是按照一样的格式,配置到子路由的 children 里。

这是一个简单的子路由示范:

ts
const routes: Array<RouteRecordRaw> = [
+  // 注意:这里是一级路由
+  {
+    path: '/lv1',
+    name: 'lv1',
+    component: () => import('@views/lv1.vue'),
+    // 注意:这里是二级路由,在 \`path\` 的前面没有 \`/\`
+    children: [
+      {
+        path: 'lv2',
+        name: 'lv2',
+        component: () => import('@views/lv2.vue'),
+        // 注意:这里是三级路由,在 \`path\` 的前面没有 \`/\`
+        children: [
+          {
+            path: 'lv3',
+            name: 'lv3',
+            component: () => import('@views/lv3.vue'),
+          },
+        ],
+      },
+    ],
+  },
+]

上面这个配置,最终三级路由的访问地址如下:

https://example.com/lv1/lv2/lv3

可以看到在注释里提示了二级、三级路由的 path 字段前面没有 / ,这样路径前面才会有其父级路由的 path 以体现其层级关系,否则会从根目录开始。

路由懒加载

在上面提过,路由在配置 同步组件 的时候,构建出来的文件都集中在一起,大的项目的文件会变得非常大,影响页面加载。

所以 Vue 在 Webpack 的代码分割功能的基础上,推出了 异步组件,可以把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样按需载入,很方便的实现路由组件的懒加载。

在这一段配置里面:

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+  },
+]

起到懒加载配置作用的就是 component 接收的值 () => import('@views/home.vue') ,其中 @views/home.vue 就是路由的组件。

在命令行运行 npm run build 打包构建后,会看到控制台输出的打包结果:

bash
 npm run build
+
+> hello-vue3@0.0.0 build
+> vue-tsc --noEmit && vite build
+
+vite v2.9.15 building for production...
+ 42 modules transformed.
+dist/index.html                       0.42 KiB
+dist/assets/home.03ad1823.js          0.65 KiB / gzip: 0.42 KiB
+dist/assets/HelloWorld.1322d484.js    1.88 KiB / gzip: 0.96 KiB
+dist/assets/about.c2af6d65.js         0.64 KiB / gzip: 0.41 KiB
+dist/assets/login.e9d1d9f9.js         0.65 KiB / gzip: 0.42 KiB
+dist/assets/index.60726771.css        0.47 KiB / gzip: 0.29 KiB
+dist/assets/login.bef803dc.css        0.12 KiB / gzip: 0.10 KiB
+dist/assets/HelloWorld.b2638077.css   0.38 KiB / gzip: 0.19 KiB
+dist/assets/home.ea56cd55.css         0.12 KiB / gzip: 0.10 KiB
+dist/assets/about.a0917080.css        0.12 KiB / gzip: 0.10 KiB
+dist/assets/index.19d6fb3b.js         79.94 KiB / gzip: 31.71 KiB

可以看到路由文件都按照 views 目录下的路由组件和 components 目录下的组件命名,输出了对应的 JS 文件和 CSS 文件,项目部署后, Vue 只会根据当前路由加载需要的文件,其他文件只做预加载,对于大型项目的访问体验非常友好。

而如果不使用路由懒加载,打包出来的文件是这样的:

bash
 npm run build
+
+> hello-vue3@0.0.0 build
+> vue-tsc --noEmit && vite build
+
+vite v2.9.15 building for production...
+ 41 modules transformed.
+dist/index.html                  0.42 KiB
+dist/assets/index.67b1ee4f.css   1.22 KiB / gzip: 0.49 KiB
+dist/assets/index.f758ee53.js    78.85 KiB / gzip: 31.05 KiB

可以看到所有的组件都被打包成了一个很大的 JS 文件和 CSS 文件,没有进行代码分割,对大型项目来说,这种方式打包出来的文件可能会有好几兆,对首屏加载的速度可想而知。

路由的渲染

所有路由组件,要在访问后进行渲染,都必须在父级组件里带有 <router-view /> 标签。

<router-view /> 在哪里,路由组件的代码就渲染在哪个节点上,一级路由的父级组件,就是 src/App.vue 这个根组件。

其中最基础的配置就是 <template /> 里面直接就是写一个 <router-view /> ,整个页面就是路由组件。

vue
<template>
+  <router-view />
+</template>

如果站点带有全局公共组件,比如有全站统一的页头、页脚,只有中间区域才是路由,那么可以这样配置:

vue
<template>
+  <!-- 全局页头 -->
+  <Header />
+
+  <!-- 路由 -->
+  <router-view />
+
+  <!-- 全局页脚 -->
+  <Footer />
+</template>

如果有一部分路由带公共组件,一部分没有,比如大部分页面都需要有侧边栏,但登录页、注册页不需要,就可以这么处理:

vue
<template>
+  <!-- 登录 -->
+  <Login v-if="route.name === 'login'" />
+
+  <!-- 注册 -->
+  <Register v-else-if="route.name === 'register'" />
+
+  <!-- 带有侧边栏的其他路由 -->
+  <div v-else>
+    <!-- 固定在左侧的侧边栏 -->
+    <Sidebar />
+
+    <!-- 路由 -->
+    <router-view />
+  </div>
+</template>

也可以通过 路由元信息 来管理这些规则。

使用 route 获取路由信息 ~new

和 Vue 2 可以直接在组件里使用 this.$route 来获取当前路由信息不同,在 Vue 3 的组件里,Vue 实例既没有了 this,也没有了 $route

要牢记一个事情就是,Vue 3 用啥都要导入,所以获取当前路由信息的正确用法是先导入路由 API :

ts
import { useRoute } from 'vue-router'

再在 setup 里定义一个变量来获取当前路由:

ts
const route = useRoute()

接下来就可以通过定义好的变量 route 去获取当前路由信息了。

当然,如果要在 <template /> 里使用路由,记得把 routesetup 里 return 出去。

ts
// 获取路由名称
+console.log(route.name)
+
+// 获取路由参数
+console.log(route.params.id)

Vue 3 的 route 和 Vue 2 的用法基本一致,日常使用应该很快能上手。

WARNING

但是 Vue 3 的新路由也有一些小变化,有一些属性是被移除了,比如之前获取父级路由信息,很喜欢用的 parent 属性,现在已经没有了,可以在 Vue Router 官网的 从 Vue2 迁移 一章查看所有破坏性变化。

类似被移除的 parent ,如果要获取父级路由信息(比如在做面包屑功能的时候),可以改成下面这样,手动指定倒数第二个为父级信息:

ts
// 获取路由记录
+const matched = route.matched
+
+// 获取该记录的路由个数
+const max = matched.length
+
+// 获取倒数第二个路由(也就是当前路由的父级路由)
+const parentRoute = matched[max - 2]

如果有配置父级路由,那么 parentRoute 就是父级路由信息,否则会返回 undefined

使用 router 操作路由 ~new

route 一样,在 Vue 3 也不能再使用 this.$router ,也必须通过导入路由 API 来使用:

ts
import { useRouter } from 'vue-router'

useRoute 一样, useRouter 也是一个函数,需要在 setup 里定义一个变量来获取路由信息。

ts
const router = useRouter()

接下来就可以通过定义好的变量 router 去操作路由了:

ts
// 跳转首页
+router.push({
+  name: 'home',
+})
+
+// 返回上一页
+router.back()

router-link 是一个全局组件,可直接在 <template /> 里直接使用,无需导入,基础的用法在 Vue 2 和 Vue 3 里是一样。

默认会被转换为一个 a 标签,对比写死的 <a href="..."> ,使用 router-link 会更加灵活。

基础跳转

最基础的用法就是把它当成一个 target="_self"a 标签使用,但无需重新刷新页面,因为是路由跳转,它的体验和使用 router 去进行路由导航的效果完全一样。

vue
<template>
+  <router-link to="/home">首页</router-link>
+</template>

等价于 routerpush

ts
router.push({
+  name: 'home',
+})

可以写个 <div /> 标签绑定 Click 事件达到 router-link 的效果:

vue
<template>
+  <div
+    class="link"
+    @click="
+      router.push({
+        name: 'home',
+      })
+    "
+  >
+    <span>首页</span>
+  </div>
+</template>

了解这种使用对比,对下文其他跳转方式的学习会有帮助。

带参数的跳转

使用 router 的时候,可以轻松的带上参数去那些有 ID 的内容页、用户资料页、栏目列表页等等。

比如要访问一篇文章 https://example.com/article/123 ,用 push 的写法是:

ts
router.push({
+  name: 'article',
+  params: {
+    id: 123,
+  },
+})

同理,从基础跳转的写法,很容易就能猜到在 router-link 里应该怎么写:

vue
<template>
+  <router-link
+    class="link"
+    :to="{
+      name: 'article',
+      params: {
+        id: 123,
+      },
+    }"
+  >
+    这是文章的标题
+  </router-link>
+</template>

不生成 a 标签 ~new

router-link 默认是被转换为一个 a 标签,但根据业务场景,也可以把它指定为生成其他标签,比如 spandivli 等等,这些标签因为不具备 href 属性,所以在跳转时都是通过 Click 事件去执行。

在 Vue 2 ,指定为其他标签只需要一个 tag 属性即可:

vue
<template>
+  <router-link tag="span" to="/home">首页</router-link>
+</template>

但在 Vue 3 ,tag 属性已被移除,需要通过 customv-slot 的配合将其渲染为其他标签。

比如要渲染为一个带有路由导航功能的其他标签:

vue
<template>
+  <router-link to="/home" custom v-slot="{ navigate }">
+    <span class="link" @click="navigate"> 首页 </span>
+  </router-link>
+</template>

渲染后就是一个普通的 <span /> 标签,当该标签被点击的时候,会通过路由的导航跳转到指定的路由页:

html
<!-- 渲染后的标签 -->
+<span class="link">首页</span>

关于这两个属性的参数说明如下:

  1. custom ,一个布尔值,用于控制是否需要渲染为 a 标签,当不包含 custom 或者把 custom 设置为 false 时,则依然使用 a 标签渲染。

  2. v-slot 是一个对象,用来决定标签的行为,它包含了:

字段含义
href解析后的 URL,将会作为一个 a 元素的 href 属性
route解析后的规范化的地址
navigate触发导航的函数,会在必要时自动阻止事件,和 router-link 同理
isActive如果需要应用激活的 class 则为 true,允许应用一个任意的 class
isExactActive如果需要应用精确激活的 class 则为 true,允许应用一个任意的 class

一般来说,v-slot 必备的只有 navigate ,用来绑定元素的点击事件,否则元素点击后不会有任何反应,其他的可以根据实际需求来添加。

TIP

要渲染为非 a 标签,切记两个点:

  1. router-link 必须带上 customv-slot 属性

  2. 最终要渲染的标签,写在 router-link 里,包括对应的 className 和点击事件

在独立 TS/JS 文件里使用路由

除了可以在 .vue 文件里使用路由之外,也可以在单独的 .ts.js 里使用。

比如要做一个带有用户系统的站点,登录的相关代码除了在 login.vue 里运用外,在注册页面 register.vue,用户注册成功还要帮用户执行一次自动登录。

登录完成还要记录用户的登录信息、 Token 、过期时间等等,有不少数据要做处理,以及需要帮助用户自动切去登录前的页面等行为,这是两个不同的组件,如果写两次几乎一样的代码,会大大提高维护成本。

这种情况下就可以通过抽离核心代码,封装成一个 login.ts 文件,在这个独立的 ts 文件里去操作路由。

ts
// 导入路由
+import router from '@/router'
+
+// 执行路由跳转
+router.push({
+  name: 'home',
+})

路由元信息配置

有时候的项目需要一些个性化配置,比如:

  1. 每个路由给予独立的标题;

  2. 管理后台的路由,部分页面需要限制一些访问权限;

  3. 通过路由来自动生成侧边栏、面包屑;

  4. 部分路由的生命周期需要做缓存( Keep Alive );

  5. 其他更多业务场景…

无需维护很多套配置,在定义路由树的时候可以配置 meta 字段,比如下面就是包含了多种元信息的一个登录路由:

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/login',
+    name: 'login',
+    component: () => import('@views/login.vue'),
+    meta: {
+      title: '登录',
+      isDisableBreadcrumbLink: true,
+      isShowBreadcrumb: false,
+      addToSidebar: false,
+      sidebarIcon: '',
+      sidebarIconAlt: '',
+      isNoLogin: true,
+    },
+  },
+]

这个是笔者曾经在做后台项目时用过的一些配置,主要的功能是:

字段类型含义
titlestring用于在渲染的时候配置浏览器标题;
isDisableBreadcrumbLinkboolean是否禁用面包屑链接(对一些没有内容的路由可以屏蔽访问);
isShowBreadcrumbboolean是否显示面包屑(此处的登录页不需要面包屑);
addToSidebarboolean是否加入侧边栏(此处的登录页不需要加入侧边栏);
sidebarIconstring配置侧边栏的图标 className(默认);
sidebarIconAltstring配置侧边栏的图标 className(展开状态);
isNoLoginboolean是否免登录(后台默认强制登录,设置为 true 则可以免登录访问,此处的登录页不需要校验);

类似的,如果有其他需求,比如要增加对不同用户组的权限控制(比如有管理员、普通用户分组,部分页面只有管理员允许访问),都可以通过配置 Meta 里的字段,再配合 路由拦截 一起使用。

路由重定向

对一些已下线的页面,直接访问原来的地址会导致 404 ,为了避免这种情况出现,通常会配置重定向将其指向一个新的页面,或者跳转回首页。

基本用法

路由重定向是使用一个 redirect 字段进行配置到对应的路由里面去实现跳转:

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+    meta: {
+      title: 'Home',
+    },
+  },
+  // 访问这个路由会被重定向到首页
+  {
+    path: '/error',
+    redirect: '/',
+  },
+]

通常来说,配置了 redirect 的路由,只需要指定 2 个字段即可,1 个是 path 该路由本身的路径,1 个是 redirect 目标路由的路径,其他字段可以忽略。

redirect 字段可以接收三种类型的值:

类型填写的值
string另外一个路由的 path
route另外一个路由(类似 router.push
function可以判断不同情况的重定向目标,最终 return 一个 path 或者 route

业务场景

路由重定向可以避免用户访问到一些无效路由页面:

  1. 比如项目上线了一段时间后,有个路由需要改名,或者调整路径层级,可以把旧路由重定向到新的,避免原来的用户从收藏夹等地方进来后找不到

  2. 一些容易打错的地址,比如通常个人资料页都是用 /profile,但是业务网站是使用 /account,那也可以把 /profile 重定向到 /account

  3. 对于一些有会员体系的站点,可以根据用户权限进行重定向,分别指向他们具备访问权限的页面

  4. 官网首页在 PC 端、移动端、游戏内嵌横屏版分别有 3 套页面,但希望能通过主域名来识别不同设备,帮助用户自动切换访问

了解了业务场景,接下来就能比较清晰的了解应该如何配置重定向了。

配置为 path

最常用的场景,恐怕就是首页的指向了,比如首页地址是 https://example.com/home,但是想让主域名 https://example.com/ 也能跳转到 /home,可以这么配置:

这是最简单的配置方式,把目标路由的 path 配置进来就可以了:

ts
const routes: Array<RouteRecordRaw> = [
+  // 重定向到 \`/home\`
+  {
+    path: '/',
+    redirect: '/home',
+  },
+  // 真正的首页
+  {
+    path: '/home',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+  },
+]

但缺点也显而易见,只能针对那些不带参数的路由。

配置为 route

如果想要重定向后的路由地址带上一些参数,可以配置为 route

ts
const routes: Array<RouteRecordRaw> = [
+  // 重定向到 \`/home\` ,并带上一个 \`query\` 参数
+  {
+    path: '/',
+    redirect: {
+      name: 'home',
+      query: {
+        from: 'redirect',
+      },
+    },
+  },
+  // 真正的首页
+  {
+    path: '/home',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+  },
+]

最终访问的地址就是 https://example.com/home?from=redirect, 像这样带有来路参数的,就可以在 “百度统计” 或者 “ CNZZ 统计” 之类的统计站点查看来路的流量。

配置为 function

结合业务场景来解释是最直观的,比如的网站有 3 个用户组,一个是管理员,一个是普通用户,还有一个是游客(未登录),他们的网站首页是不一样的:

用户组访问首页时
管理员具备各种数据可视化图表、最新的网站数据、一些最新的用户消息等等
普通用户只有一些常用模块的入口链接
未登录用户直接跳转到登录页面

产品需要在访问网站主域名的时候,识别用户身份跳转不同的首页,那么就可以这样配置路由重定向:

ts
const routes: Array<RouteRecordRaw> = [
+  // 访问主域名时,根据用户的登录信息,重定向到不同的页面
+  {
+    path: '/',
+    redirect: () => {
+      // \`loginInfo\` 是当前用户的登录信息
+      // 可以从 \`localStorage\` 或者 \`Pinia\` 读取
+      const { groupId } = loginInfo
+
+      // 根据组别 ID 进行跳转
+      switch (groupId) {
+        // 管理员跳去仪表盘
+        case 1:
+          return '/dashboard'
+
+        // 普通用户跳去首页
+        case 2:
+          return '/home'
+
+        // 其他都认为未登录,跳去登录页
+        default:
+          return '/login'
+      }
+    },
+  },
+]

路由别名配置

根据的业务需求,也可以为路由指定一个别名,与上面的 路由重定向 功能相似,但又有不同:

配置了路由重定向,当用户访问 /a 时,URL 将会被替换成 /b,然后匹配的实际路由是 /b

配置了路由别名,/a 的别名是 /b,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样。

配置方法

添加一个 alias 字段即可轻松实现:

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/home',
+    alias: '/index',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+  },
+]

如上的配置,即可实现可以通过 /home 访问首页,也可以通过 /index 访问首页。

404 路由页面配置 ~new

可以配置一个 404 路由来代替站内的 404 页面。

配置方法

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/:pathMatch(.*)*',
+    name: '404',
+    component: () => import('@views/404.vue'),
+  },
+]

这样配置之后,只要访问到不存在的路由,就会显示为这个 404 模板。

WARNING

新版的路由不再支持直接配置通配符 * ,而是必须使用带有自定义正则表达式的参数进行定义,详见官网 删除了 *(星标或通配符)路由 的说明。

导航守卫

和 Vue 2 时使用的路由一样, Vue 3 也支持导航守卫,并且用法基本上是一样的。

导航守卫这个词对初次接触的开发者来说应该会有点云里雾里,其实就是几个专属的钩子函数,先来看一下使用场景,大致理解一下基本概念和作用。

钩子的应用场景

对于导航守卫还不熟悉的开发者,可以从一些实际使用场景来加强印象,比如:

  1. 前面说的,在渲染的时候配置浏览器标题,由于 Vue 项目只有一个 HTML 文件,所以默认只有一个标题,但想在访问 /home 的时候标题显示为 “首页”,访问 /about 的时候标题显示为 “关于” 。

  2. 部分页面需要管理员才能访问,普通用户不允许进入到该路由页面。

  3. Vue 单页面项目,传统的 CNZZ / 百度统计等网站统计代码只会在页面加载的时候统计一次,但需要每次切换路由都上报一次 PV 数据。

这样的场景还有很多,导航守卫支持全局使用,也可以在 .vue 文件里单独使用,接下来看看具体的用法。

路由里的全局钩子

顾名思义,是在创建 router 的时候进行全局的配置,也就是说,只要配置了钩子,那么所有的路由在被访问到的时候,都会触发这些钩子函数。

可用钩子含义触发时机
beforeEach全局前置守卫在路由跳转前触发
beforeResolve全局解析守卫在导航被确认前,同时在组件内守卫和异步路由组件被解析后
afterEach全局后置守卫在路由跳转完成后触发

全局配置非常简单,在 src/router/index.ts 里,在创建路由之后、在导出去之前使用:

ts
import { createRouter } from 'vue-router'
+
+// 创建路由
+const router = createRouter({ ... })
+
+// 在这里调用导航守卫的钩子函数
+router.beforeEach((to, from) => {
+  // ...
+})
+
+// 导出去
+export default router

beforeEach ~new

全局前置守卫,这是导航守卫里面运用的最多的一个钩子函数,通常将其称为 “路由拦截”。

拦截这个词,顾名思义,就是在 XXX 目的达到之前,把它拦下来,所以路由的目的就是渲染指定的组件,路由拦截就是在组件被渲染之前,做一些拦截操作。

参数

参数作用
to即将要进入的路由对象
from当前导航正要离开的路由

TIP

和 Vue 2 不同,Vue 2 的 beforeEach 是默认三个参数,第三个参数是 next,用来操作路由接下来的跳转。

但在新版本路由里,已经通过 RFC 将其删除,虽然目前还是作为可选参数使用,但以后不确定是否会移除,不建议继续使用,点击查看原因

新版本路由可以通过 return 来代替 next

用法

比如在进入路由之前,根据 Meta 路由元信息 的配置,设定路由的网页标题:

ts
router.beforeEach((to, from) => {
+  const { title } = to.meta
+  document.title = title || '默认标题'
+})

或者判断是否需要登录:

ts
router.beforeEach((to, from) => {
+  const { isNoLogin } = to.meta
+  if (!isNoLogin) return '/login'
+})

或者针对一些需要 ID 参数,但参数丢失的路由做拦截,比如:很多网站的文章详情页都是类似 https://example.com/article/123 这样格式的地址,是需要带有文章 ID 作为 URL 的一部分,如果只访问 https://example.com/article 则需要拦截掉。

这里是关于 article 路由的配置,是有要求 Params 要带上 ID 参数:

ts
const routes: Array<RouteRecordRaw> = [
+  // 这是一个配置了 \`params\` ,访问的时候必须带 \`id\` 的路由
+  {
+    path: '/article/:id',
+    name: 'article',
+    component: () => import('@views/article.vue'),
+  },
+  // ...
+]

当路由的 params 丢失的时候,路由记录 matched 是一个空数组,针对这样的情况,就可以配置一个拦截,丢失参数时返回首页:

ts
router.beforeEach((to) => {
+  if (to.name === 'article' && to.matched.length === 0) {
+    return '/'
+  }
+})

beforeResolve

全局解析守卫,它会在每次导航时触发,但是在所有组件内守卫和异步路由组件被解析之后,将在确认导航之前被调用。

这个钩子用的比较少,因为它和 beforeEach 非常相似,相信大部分开发者都是会用 beforeEach 来代替它。

那么它有什么用?

它通常会用在一些申请权限的环节,比如一些 H5 页面需要申请系统相机权限、一些微信活动需要申请微信的登录信息授权,获得权限之后才允许获取接口数据和给用户更多的操作,使用 beforeEach 时机太早,使用 afterEach 又有点晚,那么这个钩子的时机就刚刚好。

参数

参数作用
to即将要进入的路由对象
from当前导航正要离开的路由

用法

以以前 Vue Router 官网的申请照相机权限的例子来举例:

ts
// https://router.vuejs.org/zh/guide/advanced/navigation-guards.html
+
+router.beforeResolve(async (to) => {
+  // 如果路由配置了必须调用相机权限
+  if (to.meta.requiresCamera) {
+    // 正常流程,咨询是否允许使用照相机
+    try {
+      await askForCameraPermission()
+    } catch (error) {
+      // 容错
+      if (error instanceof NotAllowedError) {
+        // ... 处理错误,然后取消导航
+        return false
+      } else {
+        // 如果出现意外,则取消导航并抛出错误
+        throw error
+      }
+    }
+  }
+})

afterEach

全局后置守卫,这也是导航守卫里面用的比较多的一个钩子函数。

参数

参数作用
to即将要进入的路由对象
from当前导航正要离开的路由

用法

在刚刚的 钩子的应用场景 里面有个例子,就是每次切换路由都上报一次 PV 数据,类似这种每个路由都要执行一次,但又不必在渲染前操作的,都可以放到后置钩子里去执行。

笔者之前写过两个数据统计的插件:Vue 版 CNZZ 统计Vue 版百度统计,就是用的这个后置钩子来实现自动上报数据。

ts
router.afterEach((to, from) => {
+  // 上报流量的操作
+  // ...
+})

在组件内使用全局钩子

上面所讲的都是全局钩子,虽然一般都是在路由文件里使用,但如果有需要,也可以在 .vue 文件里操作。

TIP

和路由的渲染不同,渲染是父级路由组件必须带有 <router-view /> 标签才能渲染,但是使用全局钩子不受此限制。

建议只在一些入口文件里使用,比如 App.vue ,或者是一些全局的 Header.vueFooter.vue 里使用,方便后续维护。

setup 里,定义一个 router 变量获取路由之后,就可以操作了:

ts
import { defineComponent } from 'vue'
+import { useRouter } from 'vue-router'
+
+export default defineComponent({
+  setup() {
+    // 定义路由
+    const router = useRouter()
+
+    // 调用全局钩子
+    router.beforeEach((to, from) => {
+      // ...
+    })
+  },
+})

路由里的独享钩子

介绍完全局钩子,如果只是有个别路由要做处理,可以使用路由独享的守卫,用来针对个别路由定制一些特殊功能,可以减少在全局钩子里面写一堆判断。

可用钩子含义触发时机
beforeEnter路由独享前置守卫在路由跳转前触发

注:路由独享的钩子,必须配置在 routes 的 JSON 树里面,挂在对应的路由下面(与 pathnamemeta 这些字段同级)。

beforeEnter ~new

它和全局钩子 beforeEach 的作用相同,都是在进入路由之前触发,触发时机比 beforeResolve 要早。

顺序:beforeEach(全局) > beforeEnter(独享) > beforeResolve(全局)。

参数

参数作用
to即将要进入的路由对象
from当前导航正要离开的路由

TIP

beforeEach 一样,也是取消了 next,可以通过 return 来代替。

用法

比如:整个站点的默认标题都是以 “栏目标题” + “全站关键标题” 的格式作为网页的 Title ,例如 “项目经验 - 程沛权” ,但在首页的时候,想做一些不一样的定制。

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/home',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+    // 在这里添加单独的路由守卫
+    beforeEnter: (to, from) => {
+      document.title = '程沛权 - 养了三只猫'
+    },
+  },
+]

就可以通过 beforeEnter 来实现一些个别路由的单独定制。

TIP

需要注意的是,只有从不同的路由切换进来,才会触发该钩子。

针对同一个路由,但是不同的 paramsquery 或者 hash ,都不会重复触发该钩子。

比如从 https://example.com/article/123 切换到 https://example.com/article/234 是不会触发的。

其他的用法和 beforeEach 可以说是一样的。

组件内单独使用 ~new

组件里除了可以使用全局钩子外,还可以使用组件专属的路由钩子。

可用钩子含义触发时机
onBeforeRouteUpdate组件内的更新守卫在当前路由改变,但是该组件被复用时调用
onBeforeRouteLeave组件内的离开守卫导航离开该组件的对应路由时调用

TIP

1、组件内钩子的入参,也都是取消了 next,可以通过 return 来代替。

2、和其他 Composition API 一样,需要先 import 再操作。

和旧版路由不同,新版的 Composition API 移除了 beforeRouteEnter 这个钩子了。

onBeforeRouteUpdate

可以在当前路由改变,但是该组件被复用时,重新调用里面的一些函数用来更新模板数据的渲染。

参数

参数作用
to即将要进入的路由对象
from当前导航正要离开的路由

用法

比如一个内容网站,通常在文章详情页底部会有相关阅读推荐,这个时候就会有一个操作场景是,从文章 A 跳转到文章 B。

比如从 https://example.com/article/111 切去 https://example.com/article/222 ,这种情况就属于 “路由改变,但是组件被复用” 的情况了。

这种情况下,原本放在 onMounted 里执行数据请求的函数就不会被调用,可以借助该钩子来实现渲染新的文章内容。

ts
import { defineComponent, onMounted } from 'vue'
+import { useRoute, onBeforeRouteUpdate } from 'vue-router'
+
+export default defineComponent({
+  setup() {
+    // 其他代码略...
+
+    // 查询文章详情
+    async function queryArticleDetail(id: number) {
+      // 请求接口数据
+      const res = await axios({
+        url: \`/article/\${id}\`,
+      })
+      // ...
+    }
+
+    // 组件挂载完成后执行文章内容的请求
+    // 注意这里是获取 \`route\` 的 \`params\`
+    onMounted(async () => {
+      const id = Number(route.params.id) || 0
+      await queryArticleDetail(id)
+    })
+
+    // 组件被复用时重新请求新的文章内容
+    onBeforeRouteUpdate(async (to, from) => {
+      // ID 不变时减少重复请求
+      if (to.params.id === from.params.id) return
+
+      // 注意这里是获取 \`to\` 的 \`params\`
+      const id = Number(to.params.id) || 0
+      await queryArticleDetail(id)
+    })
+  },
+})

onBeforeRouteLeave

可以在离开当前路由之前,实现一些离开前的判断拦截。

参数

参数作用
to即将要进入的路由对象
from当前导航正要离开的路由

用法

这个离开守卫通常用来禁止用户在还未保存修改前突然离开,可以通过 return false 来取消用户离开当前路由。

ts
import { defineComponent } from 'vue'
+import { onBeforeRouteLeave } from 'vue-router'
+
+export default defineComponent({
+  setup() {
+    // 调用离开守卫
+    onBeforeRouteLeave((to, from) => {
+      // 弹出一个确认框
+      const confirmText = '确认要离开吗?您的更改尚未保存!'
+      const isConfirmLeave = window.confirm(confirmText)
+
+      // 当用户点取消时,不离开路由
+      if (!isConfirmLeave) {
+        return false
+      }
+    })
+  },
+})

路由侦听 ~new

路由的侦听,可以延续以往的 watch 大法,也可以用全新的 watchEffect

watch

在 Vue 2 的时候,侦听路由变化用的最多的就是 watch 了, Vue 3 的 watch API 使用更简单。

侦听整个路由

可以跟以前一样,直接侦听整个路由的变化:

ts
import { defineComponent, watch } from 'vue'
+import { useRoute } from 'vue-router'
+
+export default defineComponent({
+  setup() {
+    const route = useRoute()
+
+    // 侦听整个路由
+    watch(route, (to, from) => {
+      // 处理一些事情
+      // ...
+    })
+  },
+})

第一个参数传入整个路由;第二个参数是个 Callback ,可以获取 tofrom 来判断路由变化情况。

侦听路由的某个数据

如果只想侦听路由的某个数据变化,比如侦听一个 Query ,或者一个 Param ,可以采用这种方式:

ts
import { defineComponent, watch } from 'vue'
+import { useRoute } from 'vue-router'
+
+export default defineComponent({
+  setup() {
+    const route = useRoute()
+
+    // 侦听路由参数的变化
+    watch(
+      () => route.query.id,
+      () => {
+        console.log('侦听到 ID 变化')
+      }
+    )
+  },
+})

第一个参数传入一个 getter 函数, return 要侦听的值;第二个参数是个 Callback ,可以针对参数变化进行一些操作。

watchEffect

这是 Vue 3 新出的一个侦听函数,可以简化 watch 的行为。

比如定义了一个函数,通过路由的参数来获取文章 ID ,然后请求文章内容:

ts
import { defineComponent, watchEffect } from 'vue'
+import { useRoute } from 'vue-router'
+
+export default defineComponent({
+  setup() {
+    const route = useRoute()
+
+    // 从接口查询文章详情
+    async function queryArticleDetail() {
+      const id = Number(route.params.id) || 0
+      console.log('文章 ID 是:', id)
+
+      const res = await axios({
+        url: \`/article/\${id}\`,
+      })
+      // ...
+    }
+
+    // 直接侦听包含路由参数的那个函数
+    watchEffect(queryArticleDetail)
+  },
+})

对比 watch 的使用, watchEffect 在操作上更加简单,把包含要被侦听数据的函数,当成它的入参传进去即可。

部署问题与服务端配置

通常使用路由的 Hash 模式,部署后有问题的情况很少,但是如果使用 History 模式,可能会遇到这样那样的问题。

常见部署问题

这里整理一些常见部署问题的原因分析和解决方案,可作参考。

页面刷新就 404

页面部署到服务端之后,访问首页正常;通过导航上面的链接进行路由跳转,也正常;但是刷新页面就变成 404 了。

问题原因

一般这种情况是路由开启了 History 模式,但是服务端没有配置功能支持。

解决方案

请根据 服务端配置 部分的说明,与的运维同事沟通,让他帮忙修改服务端的配置。

部分路由白屏

如果在项目配置文件里,把里面的 publicPath (使用 Vue CLI ) 或者 base (使用 Vite ) 配置成相对路径 ./ ,但是路由配置了二级或以上,那么就会出现这个问题。

问题原因

原因是打包后的 JS 、 CSS 等静态资源都是存放在项目根目录下,一级路由的 ./ 就是根目录,所以访问正常;而二级路由的 ./ 则不是根目录了,是从当前目录载入的 ,这就导致无法正确载入 JS 文件,从而导致了白屏。

假设项目域名是 https://example.com ,那么:

  • 一级路由是 https://example.com/home
  • 二级路由是 https://example.com/foo/bar
  • 假设打包后的 JS 文件等静态资产存放于 https://example.com/assets/ 文件夹下

访问一级路由时, ./ 访问到的 JS 文件是 https://example.com/assets/home.js ,所以一级路由可以正常访问到。

访问二级路由时, ./ 访问到的 JS 文件是 https://example.com/foo/assets/bar.js ,但实际上文件是存放在 https://example.com/assets/bar.js ,访问到的 URL 资源不存在,所以白屏了。

解决方案

如果的项目开启了 History 模式,并且配置有二级或者二级以上的路由时,不要使用 ./ 这样的相对路径。

正确的方式应该是修改 publicPath (使用 Vue CLI ) 或者 base (使用 Vite ),如果是部署在域名根目录则写 / ,如果是子目录,则按照子目录的格式,将其以 / 开头,以 / 结尾的形式配置( e.g. /hello-world/

服务端配置方案

如果使用的是 HTML5 的 History 模式,那么服务端也需要配置对应的支持,否则会出现路由跳转正常,但页面一刷新就 404 的情况。

TIP

服务端配置后,就不再进入 404 了,需要在项目里手动配置 404 路由页面 的路由。

Nginx

现在大部分公司的服务程序都在使用 Nginx ,可以将以下代码发给运维工程师参考,调整 Nginx 的配置:

nginx
location / {
+  try_files $uri $uri/ /index.html;
+}

Express

如果是前端工程师使用 Node.js 作服务端,并且使用了 Express 服务端框架,那么操作将变得更简单:

  1. 仅需要安装一个中间件
bash
npm install connect-history-api-fallback
  1. 在服务启动入口文件里导入该中间件并激活
js
const express = require('express')
+const history = require('connect-history-api-fallback')
+
+// 创建 Express 实例
+const app = express()
+app
+  // 启用 History 中间件
+  .use(history())
+  // 这里是读取打包后的页面文件目录
+  .use('/', express.static(resolve('../dist')))

更多用法可以看: connect-history-api-fallback 的文档。

更多方案

其他的诸如 Apache 、 IIS 、或者原生 Node 等等配置方案, Vue 官方都提供了对应的演示代码,点击查看更多配置方案: 服务器配置示例

`,339);function i(C,A,d,h,u,g){const l=a("ReadingTips"),o=a("GitalkComment"),p=a("ClientOnly");return t(),c("div",null,[D,s(l),F,s(p,null,{default:r(()=>[s(o,{issueId:47})]),_:1})])}const b=e(y,[["render",i]]);export{m as __pageData,b as default}; diff --git a/assets/style.dfe14009.css b/assets/style.dfe14009.css new file mode 100644 index 00000000..69b7e629 --- /dev/null +++ b/assets/style.dfe14009.css @@ -0,0 +1,8 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/assets/inter-roman-cyrillic.5f2c6c8c.woff2) format("woff2");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/assets/inter-roman-cyrillic-ext.e75737ce.woff2) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/assets/inter-roman-greek.d5a6d92a.woff2) format("woff2");unicode-range:U+0370-03FF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/assets/inter-roman-greek-ext.ab0619bc.woff2) format("woff2");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/assets/inter-roman-latin.2ed14f66.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/assets/inter-roman-latin-ext.0030eebd.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/assets/inter-roman-vietnamese.14ce25a6.woff2) format("woff2");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/assets/inter-italic-cyrillic.ea42a392.woff2) format("woff2");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/assets/inter-italic-cyrillic-ext.33bd5a8e.woff2) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/assets/inter-italic-greek.8f4463c4.woff2) format("woff2");unicode-range:U+0370-03FF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/assets/inter-italic-greek-ext.4fbe9427.woff2) format("woff2");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/assets/inter-italic-latin.bd3b6f56.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/assets/inter-italic-latin-ext.bd8920cc.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/assets/inter-italic-vietnamese.6ce511fb.woff2) format("woff2");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB}@font-face{font-family:Chinese Quotes;src:local("PingFang SC Regular"),local("PingFang SC"),local("SimHei"),local("Source Han Sans SC");unicode-range:U+2018,U+2019,U+201C,U+201D}:root{--vp-c-white: #ffffff;--vp-c-black: #000000;--vp-c-gray: #8e8e93;--vp-c-text-light-1: rgba(60, 60, 67);--vp-c-text-light-2: rgba(60, 60, 67, .75);--vp-c-text-light-3: rgba(60, 60, 67, .33);--vp-c-text-dark-1: rgba(255, 255, 245, .86);--vp-c-text-dark-2: rgba(235, 235, 245, .6);--vp-c-text-dark-3: rgba(235, 235, 245, .38);--vp-c-green: #10b981;--vp-c-green-light: #34d399;--vp-c-green-lighter: #6ee7b7;--vp-c-green-dark: #059669;--vp-c-green-darker: #047857;--vp-c-green-dimm-1: rgba(16, 185, 129, .05);--vp-c-green-dimm-2: rgba(16, 185, 129, .2);--vp-c-green-dimm-3: rgba(16, 185, 129, .5);--vp-c-yellow: #d97706;--vp-c-yellow-light: #f59e0b;--vp-c-yellow-lighter: #fbbf24;--vp-c-yellow-dark: #b45309;--vp-c-yellow-darker: #92400e;--vp-c-yellow-dimm-1: rgba(234, 179, 8, .05);--vp-c-yellow-dimm-2: rgba(234, 179, 8, .2);--vp-c-yellow-dimm-3: rgba(234, 179, 8, .5);--vp-c-red: #f43f5e;--vp-c-red-light: #fb7185;--vp-c-red-lighter: #fda4af;--vp-c-red-dark: #e11d48;--vp-c-red-darker: #be123c;--vp-c-red-dimm-1: rgba(244, 63, 94, .05);--vp-c-red-dimm-2: rgba(244, 63, 94, .2);--vp-c-red-dimm-3: rgba(244, 63, 94, .5);--vp-c-sponsor: #db2777}:root{--vp-c-bg: #ffffff;--vp-c-bg-elv: #ffffff;--vp-c-bg-elv-up: #ffffff;--vp-c-bg-elv-down: #f6f6f7;--vp-c-bg-elv-mute: #f6f6f7;--vp-c-bg-soft: #f6f6f7;--vp-c-bg-soft-up: #f9f9fa;--vp-c-bg-soft-down: #e3e3e5;--vp-c-bg-soft-mute: #e3e3e5;--vp-c-bg-alt: #f6f6f7;--vp-c-border: rgba(60, 60, 67, .29);--vp-c-divider: rgba(60, 60, 67, .12);--vp-c-gutter: rgba(60, 60, 67, .12);--vp-c-neutral: var(--vp-c-black);--vp-c-neutral-inverse: var(--vp-c-white);--vp-c-text-1: var(--vp-c-text-light-1);--vp-c-text-2: var(--vp-c-text-light-2);--vp-c-text-3: var(--vp-c-text-light-3);--vp-c-text-inverse-1: var(--vp-c-text-dark-1);--vp-c-text-inverse-2: var(--vp-c-text-dark-2);--vp-c-text-inverse-3: var(--vp-c-text-dark-3);--vp-c-text-code: #476582;--vp-c-brand: var(--vp-c-green);--vp-c-brand-light: var(--vp-c-green-light);--vp-c-brand-lighter: var(--vp-c-green-lighter);--vp-c-brand-dark: var(--vp-c-green-dark);--vp-c-brand-darker: var(--vp-c-green-darker);--vp-c-mute: #f6f6f7;--vp-c-mute-light: #f9f9fc;--vp-c-mute-lighter: #ffffff;--vp-c-mute-dark: #e3e3e5;--vp-c-mute-darker: #d7d7d9}.dark{--vp-c-bg: #1e1e20;--vp-c-bg-elv: #252529;--vp-c-bg-elv-up: #313136;--vp-c-bg-elv-down: #1e1e20;--vp-c-bg-elv-mute: #313136;--vp-c-bg-soft: #252529;--vp-c-bg-soft-up: #313136;--vp-c-bg-soft-down: #1e1e20;--vp-c-bg-soft-mute: #313136;--vp-c-bg-alt: #161618;--vp-c-border: rgba(82, 82, 89, .68);--vp-c-divider: rgba(82, 82, 89, .32);--vp-c-gutter: #000000;--vp-c-neutral: var(--vp-c-white);--vp-c-neutral-inverse: var(--vp-c-black);--vp-c-text-1: var(--vp-c-text-dark-1);--vp-c-text-2: var(--vp-c-text-dark-2);--vp-c-text-3: var(--vp-c-text-dark-3);--vp-c-text-inverse-1: var(--vp-c-text-light-1);--vp-c-text-inverse-2: var(--vp-c-text-light-2);--vp-c-text-inverse-3: var(--vp-c-text-light-3);--vp-c-text-code: #c9def1;--vp-c-mute: #313136;--vp-c-mute-light: #3a3a3c;--vp-c-mute-lighter: #505053;--vp-c-mute-dark: #2c2c30;--vp-c-mute-darker: #252529}:root{--vp-font-family-base: "Chinese Quotes", "Inter var", "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--vp-font-family-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}:root{--vp-shadow-1: 0 1px 2px rgba(0, 0, 0, .04), 0 1px 2px rgba(0, 0, 0, .06);--vp-shadow-2: 0 3px 12px rgba(0, 0, 0, .07), 0 1px 4px rgba(0, 0, 0, .07);--vp-shadow-3: 0 12px 32px rgba(0, 0, 0, .1), 0 2px 6px rgba(0, 0, 0, .08);--vp-shadow-4: 0 14px 44px rgba(0, 0, 0, .12), 0 3px 9px rgba(0, 0, 0, .12);--vp-shadow-5: 0 18px 56px rgba(0, 0, 0, .16), 0 4px 12px rgba(0, 0, 0, .16)}:root{--vp-z-index-local-nav: 10;--vp-z-index-nav: 20;--vp-z-index-layout-top: 30;--vp-z-index-backdrop: 40;--vp-z-index-sidebar: 50;--vp-z-index-footer: 60}:root{--vp-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' height='20' width='20' stroke='rgba(128,128,128,1)' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2'/%3E%3C/svg%3E");--vp-icon-copied: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' height='20' width='20' stroke='rgba(128,128,128,1)' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2m-6 9 2 2 4-4'/%3E%3C/svg%3E")}:root{--vp-layout-max-width: 1440px}:root{--vp-header-anchor-symbol: "#"}:root{--vp-code-line-height: 1.7;--vp-code-font-size: .875em;--vp-c-code-dimm: var(--vp-c-text-dark-3);--vp-code-block-color: var(--vp-c-text-dark-1);--vp-code-block-bg: #292b30;--vp-code-block-bg-light: #1e1e20;--vp-code-block-divider-color: #000000;--vp-code-line-highlight-color: rgba(0, 0, 0, .5);--vp-code-line-number-color: var(--vp-c-code-dimm);--vp-code-line-diff-add-color: var(--vp-c-green-dimm-2);--vp-code-line-diff-add-symbol-color: var(--vp-c-green);--vp-code-line-diff-remove-color: var(--vp-c-red-dimm-2);--vp-code-line-diff-remove-symbol-color: var(--vp-c-red);--vp-code-line-warning-color: var(--vp-c-yellow-dimm-2);--vp-code-line-error-color: var(--vp-c-red-dimm-2);--vp-code-copy-code-border-color: transparent;--vp-code-copy-code-bg: var(--vp-code-block-bg-light);--vp-code-copy-code-hover-border-color: var(--vp-c-divider);--vp-code-copy-code-hover-bg: var(--vp-code-block-bg-light);--vp-code-copy-code-active-text: var(--vp-c-text-dark-2);--vp-code-tab-divider: var(--vp-code-block-divider-color);--vp-code-tab-text-color: var(--vp-c-text-dark-2);--vp-code-tab-bg: var(--vp-code-block-bg);--vp-code-tab-hover-text-color: var(--vp-c-text-dark-1);--vp-code-tab-active-text-color: var(--vp-c-text-dark-1);--vp-code-tab-active-bar-color: var(--vp-c-brand)}.dark{--vp-code-block-bg: #161618}:root:not(.dark) .vp-adaptive-theme{--vp-c-code-dimm: var(--vp-c-text-2);--vp-code-block-color: var(--vp-c-text-1);--vp-code-block-bg: #f8f8f8;--vp-code-block-divider-color: var(--vp-c-divider);--vp-code-line-highlight-color: #ececec;--vp-code-line-number-color: var(--vp-c-code-dimm);--vp-code-copy-code-bg: #e2e2e2;--vp-code-copy-code-hover-bg: #dcdcdc;--vp-code-copy-code-active-text: var(--vp-c-text-2);--vp-code-tab-divider: var(--vp-c-divider);--vp-code-tab-text-color: var(--vp-c-text-2);--vp-code-tab-bg: var(--vp-code-block-bg);--vp-code-tab-hover-text-color: var(--vp-c-text-1);--vp-code-tab-active-text-color: var(--vp-c-text-1)}:root{--vp-button-brand-border: var(--vp-c-brand-lighter);--vp-button-brand-text: var(--vp-c-white);--vp-button-brand-bg: var(--vp-c-brand);--vp-button-brand-hover-border: var(--vp-c-brand-lighter);--vp-button-brand-hover-text: var(--vp-c-white);--vp-button-brand-hover-bg: var(--vp-c-brand-dark);--vp-button-brand-active-border: var(--vp-c-brand-lighter);--vp-button-brand-active-text: var(--vp-c-white);--vp-button-brand-active-bg: var(--vp-c-brand-darker);--vp-button-alt-border: var(--vp-c-border);--vp-button-alt-text: var(--vp-c-neutral);--vp-button-alt-bg: var(--vp-c-mute);--vp-button-alt-hover-border: var(--vp-c-border);--vp-button-alt-hover-text: var(--vp-c-neutral);--vp-button-alt-hover-bg: var(--vp-c-mute-dark);--vp-button-alt-active-border: var(--vp-c-border);--vp-button-alt-active-text: var(--vp-c-neutral);--vp-button-alt-active-bg: var(--vp-c-mute-darker);--vp-button-sponsor-border: var(--vp-c-gray-light-3);--vp-button-sponsor-text: var(--vp-c-text-light-2);--vp-button-sponsor-bg: transparent;--vp-button-sponsor-hover-border: var(--vp-c-sponsor);--vp-button-sponsor-hover-text: var(--vp-c-sponsor);--vp-button-sponsor-hover-bg: transparent;--vp-button-sponsor-active-border: var(--vp-c-sponsor);--vp-button-sponsor-active-text: var(--vp-c-sponsor);--vp-button-sponsor-active-bg: transparent}.dark{--vp-button-sponsor-border: var(--vp-c-gray-dark-1);--vp-button-sponsor-text: var(--vp-c-text-dark-2)}:root{--vp-custom-block-font-size: 14px;--vp-custom-block-code-font-size: 13px;--vp-custom-block-info-border: var(--vp-c-border);--vp-custom-block-info-text: var(--vp-c-text-2);--vp-custom-block-info-bg: var(--vp-c-bg-soft-up);--vp-custom-block-info-code-bg: var(--vp-c-bg-soft);--vp-custom-block-tip-border: var(--vp-c-green);--vp-custom-block-tip-text: var(--vp-c-green-dark);--vp-custom-block-tip-bg: var(--vp-c-bg-soft-up);--vp-custom-block-tip-code-bg: var(--vp-c-bg-soft);--vp-custom-block-warning-border: var(--vp-c-yellow);--vp-custom-block-warning-text: var(--vp-c-yellow);--vp-custom-block-warning-bg: var(--vp-c-bg-soft-up);--vp-custom-block-warning-code-bg: var(--vp-c-bg-soft);--vp-custom-block-danger-border: var(--vp-c-red);--vp-custom-block-danger-text: var(--vp-c-red);--vp-custom-block-danger-bg: var(--vp-c-bg-soft-up);--vp-custom-block-danger-code-bg: var(--vp-c-bg-soft);--vp-custom-block-details-border: var(--vp-custom-block-info-border);--vp-custom-block-details-text: var(--vp-custom-block-info-text);--vp-custom-block-details-bg: var(--vp-custom-block-info-bg);--vp-custom-block-details-code-bg: var(--vp-custom-block-details-bg)}:root{--vp-input-border-color: var(--vp-c-border);--vp-input-bg-color: var(--vp-c-bg-alt);--vp-input-hover-border-color: var(--vp-c-gray);--vp-input-switch-bg-color: var(--vp-c-mute)}:root{--vp-nav-height: 64px;--vp-nav-bg-color: var(--vp-c-bg);--vp-nav-screen-bg-color: var(--vp-c-bg);--vp-nav-logo-height: 24px}:root{--vp-local-nav-bg-color: var(--vp-c-bg)}:root{--vp-sidebar-width: 272px;--vp-sidebar-bg-color: var(--vp-c-bg-alt)}:root{--vp-backdrop-bg-color: rgba(0, 0, 0, .6)}:root{--vp-home-hero-name-color: var(--vp-c-brand);--vp-home-hero-name-background: transparent;--vp-home-hero-image-background-image: none;--vp-home-hero-image-filter: none}:root{--vp-badge-info-border: var(--vp-c-border);--vp-badge-info-text: var(--vp-c-text-2);--vp-badge-info-bg: var(--vp-c-bg-soft-up);--vp-badge-tip-border: var(--vp-c-green-dark);--vp-badge-tip-text: var(--vp-c-green);--vp-badge-tip-bg: var(--vp-c-green-dimm-1);--vp-badge-warning-border: var(--vp-c-yellow-dark);--vp-badge-warning-text: var(--vp-c-yellow);--vp-badge-warning-bg: var(--vp-c-yellow-dimm-1);--vp-badge-danger-border: var(--vp-c-red-dark);--vp-badge-danger-text: var(--vp-c-red);--vp-badge-danger-bg: var(--vp-c-red-dimm-1)}:root{--vp-carbon-ads-text-color: var(--vp-c-text-1);--vp-carbon-ads-poweredby-color: var(--vp-c-text-2);--vp-carbon-ads-bg-color: var(--vp-c-bg-soft);--vp-carbon-ads-hover-text-color: var(--vp-c-brand);--vp-carbon-ads-hover-poweredby-color: var(--vp-c-text-1)}:root{--vp-local-search-bg: var(--vp-c-bg);--vp-local-search-result-bg: var(--vp-c-bg);--vp-local-search-result-border: var(--vp-c-divider);--vp-local-search-result-selected-bg: var(--vp-c-bg);--vp-local-search-result-selected-border: var(--vp-c-brand);--vp-local-search-highlight-bg: var(--vp-c-green-lighter);--vp-local-search-highlight-text: var(--vp-c-black)}*,:before,:after{box-sizing:border-box}html{line-height:1.4;font-size:16px;-webkit-text-size-adjust:100%}html.dark{color-scheme:dark}body{margin:0;width:100%;min-width:320px;min-height:100vh;line-height:24px;font-family:var(--vp-font-family-base);font-size:16px;font-weight:400;color:var(--vp-c-text-1);background-color:var(--vp-c-bg);direction:ltr;font-synthesis:style;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}main{display:block}h1,h2,h3,h4,h5,h6{margin:0;line-height:24px;font-size:16px;font-weight:400}p{margin:0}strong,b{font-weight:600}a,area,button,[role=button],input,label,select,summary,textarea{touch-action:manipulation}a{color:inherit;text-decoration:inherit}ol,ul{list-style:none;margin:0;padding:0}blockquote{margin:0}pre,code,kbd,samp{font-family:var(--vp-font-family-mono)}img,svg,video,canvas,audio,iframe,embed,object{display:block}figure{margin:0}img,video{max-width:100%;height:auto}button,input,optgroup,select,textarea{border:0;padding:0;line-height:inherit;color:inherit}button{padding:0;font-family:inherit;background-color:transparent;background-image:none}button:enabled,[role=button]:enabled{cursor:pointer}button:focus,button:focus-visible{outline:1px dotted;outline:4px auto -webkit-focus-ring-color}button:focus:not(:focus-visible){outline:none!important}input:focus,textarea:focus,select:focus{outline:none}table{border-collapse:collapse}input{background-color:transparent}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:var(--vp-c-text-3)}input::-ms-input-placeholder,textarea::-ms-input-placeholder{color:var(--vp-c-text-3)}input::placeholder,textarea::placeholder{color:var(--vp-c-text-3)}input::-webkit-outer-spin-button,input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}input[type=number]{-moz-appearance:textfield}textarea{resize:vertical}select{-webkit-appearance:none}fieldset{margin:0;padding:0}h1,h2,h3,h4,h5,h6,li,p{overflow-wrap:break-word}vite-error-overlay{z-index:9999}.visually-hidden{position:absolute;width:1px;height:1px;white-space:nowrap;clip:rect(0 0 0 0);clip-path:inset(50%);overflow:hidden}.custom-block{border:1px solid transparent;border-radius:8px;padding:16px 16px 8px;line-height:24px;font-size:var(--vp-custom-block-font-size);color:var(--vp-c-text-2)}.custom-block.info{border-color:var(--vp-custom-block-info-border);color:var(--vp-custom-block-info-text);background-color:var(--vp-custom-block-info-bg)}.custom-block.custom-block th,.custom-block.custom-block blockquote>p{font-size:var(--vp-custom-block-font-size);color:inherit}.custom-block.info code{background-color:var(--vp-custom-block-info-code-bg)}.custom-block.tip{border-color:var(--vp-custom-block-tip-border);color:var(--vp-custom-block-tip-text);background-color:var(--vp-custom-block-tip-bg)}.custom-block.tip code{background-color:var(--vp-custom-block-tip-code-bg)}.custom-block.warning{border-color:var(--vp-custom-block-warning-border);color:var(--vp-custom-block-warning-text);background-color:var(--vp-custom-block-warning-bg)}.custom-block.warning code{background-color:var(--vp-custom-block-warning-code-bg)}.custom-block.danger{border-color:var(--vp-custom-block-danger-border);color:var(--vp-custom-block-danger-text);background-color:var(--vp-custom-block-danger-bg)}.custom-block.danger code{background-color:var(--vp-custom-block-danger-code-bg)}.custom-block.details{border-color:var(--vp-custom-block-details-border);color:var(--vp-custom-block-details-text);background-color:var(--vp-custom-block-details-bg)}.custom-block.details code{background-color:var(--vp-custom-block-details-code-bg)}.custom-block-title{font-weight:600}.custom-block p+p{margin:8px 0}.custom-block.details summary{margin:0 0 8px;font-weight:700;cursor:pointer}.custom-block.details summary+p{margin:8px 0}.custom-block a{color:inherit;font-weight:600}.custom-block a:hover{text-decoration:underline}.custom-block code{font-size:var(--vp-custom-block-code-font-size)}.dark .vp-code-light{display:none}html:not(.dark) .vp-code-dark{display:none}.vp-code-group{margin-top:16px}.vp-code-group .tabs{position:relative;display:flex;margin-right:-24px;margin-left:-24px;padding:0 12px;background-color:var(--vp-code-tab-bg);overflow-x:auto;overflow-y:hidden;box-shadow:inset 0 -1px var(--vp-code-tab-divider)}@media (min-width: 640px){.vp-code-group .tabs{margin-right:0;margin-left:0;border-radius:8px 8px 0 0}}.vp-code-group .tabs input{position:absolute;opacity:0;pointer-events:none}.vp-code-group .tabs label{position:relative;display:inline-block;border-bottom:1px solid transparent;padding:0 12px;line-height:48px;font-size:14px;font-weight:500;color:var(--vp-code-tab-text-color);white-space:nowrap;cursor:pointer;transition:color .25s}.vp-code-group .tabs label:after{position:absolute;right:8px;bottom:-1px;left:8px;z-index:1;height:1px;content:"";background-color:transparent;transition:background-color .25s}.vp-code-group label:hover{color:var(--vp-code-tab-hover-text-color)}.vp-code-group input:checked+label{color:var(--vp-code-tab-active-text-color)}.vp-code-group input:checked+label:after{background-color:var(--vp-code-tab-active-bar-color)}.vp-code-group div[class*=language-]{display:none;margin-top:0!important;border-top-left-radius:0!important;border-top-right-radius:0!important}.vp-code-group div[class*=language-].active{display:block}.vp-doc h1,.vp-doc h2,.vp-doc h3,.vp-doc h4,.vp-doc h5,.vp-doc h6{position:relative;font-weight:600;outline:none}.vp-doc h1{letter-spacing:-.02em;line-height:40px;font-size:28px}.vp-doc h2{margin:48px 0 16px;border-top:1px solid var(--vp-c-divider);padding-top:24px;letter-spacing:-.02em;line-height:32px;font-size:24px}.vp-doc h3{margin:32px 0 0;letter-spacing:-.01em;line-height:28px;font-size:20px}.vp-doc .header-anchor{position:absolute;top:0;left:0;margin-left:-.87em;font-weight:500;-webkit-user-select:none;user-select:none;opacity:0;transition:color .25s,opacity .25s}.vp-doc .header-anchor:before{content:var(--vp-header-anchor-symbol)}.vp-doc h1:hover .header-anchor,.vp-doc h1 .header-anchor:focus,.vp-doc h2:hover .header-anchor,.vp-doc h2 .header-anchor:focus,.vp-doc h3:hover .header-anchor,.vp-doc h3 .header-anchor:focus,.vp-doc h4:hover .header-anchor,.vp-doc h4 .header-anchor:focus,.vp-doc h5:hover .header-anchor,.vp-doc h5 .header-anchor:focus,.vp-doc h6:hover .header-anchor,.vp-doc h6 .header-anchor:focus{opacity:1}@media (min-width: 768px){.vp-doc h1{letter-spacing:-.02em;line-height:40px;font-size:32px}}.vp-doc h2 .header-anchor{top:24px}.vp-doc p,.vp-doc summary{margin:16px 0}.vp-doc p{line-height:28px}.vp-doc blockquote{margin:16px 0;border-left:2px solid var(--vp-c-divider);padding-left:16px;transition:border-color .5s}.vp-doc blockquote>p{margin:0;font-size:16px;color:var(--vp-c-text-2);transition:color .5s}.vp-doc a{font-weight:500;color:var(--vp-c-brand);text-decoration-style:dotted;transition:color .25s}.vp-doc a:hover{text-decoration:underline}.vp-doc strong{font-weight:600}.vp-doc ul,.vp-doc ol{padding-left:1.25rem;margin:16px 0}.vp-doc ul{list-style:disc}.vp-doc ol{list-style:decimal}.vp-doc li+li{margin-top:8px}.vp-doc li>ol,.vp-doc li>ul{margin:8px 0 0}.vp-doc table{display:block;border-collapse:collapse;margin:20px 0;overflow-x:auto}.vp-doc tr{border-top:1px solid var(--vp-c-divider);transition:background-color .5s}.vp-doc tr:nth-child(2n){background-color:var(--vp-c-bg-soft)}.vp-doc th,.vp-doc td{border:1px solid var(--vp-c-divider);padding:8px 16px}.vp-doc th{text-align:left;font-size:14px;font-weight:600;color:var(--vp-c-text-2);background-color:var(--vp-c-bg-soft)}.vp-doc td{font-size:14px}.vp-doc hr{margin:16px 0;border:none;border-top:1px solid var(--vp-c-divider)}.vp-doc .custom-block{margin:16px 0}.vp-doc .custom-block p{margin:8px 0;line-height:24px}.vp-doc .custom-block p:first-child{margin:0}.vp-doc .custom-block a{color:inherit;font-weight:600}.vp-doc .custom-block a:hover{text-decoration:underline}.vp-doc .custom-block code{font-size:var(--vp-custom-block-code-font-size);font-weight:700;color:inherit}.vp-doc .custom-block div[class*=language-]{margin:8px 0;border-radius:8px}.vp-doc .custom-block div[class*=language-] code{font-weight:400;background-color:transparent}.vp-doc .custom-block .vp-code-group .tabs{margin:0;border-radius:8px 8px 0 0}.vp-doc :not(pre,h1,h2,h3,h4,h5,h6)>code{font-size:var(--vp-code-font-size)}.vp-doc :not(pre)>code{border-radius:4px;padding:3px 6px;color:var(--vp-c-text-code);background-color:var(--vp-c-mute);transition:color .5s,background-color .5s}.vp-doc h1>code,.vp-doc h2>code,.vp-doc h3>code{font-size:.9em}.vp-doc a>code{color:var(--vp-c-brand);transition:color .25s}.vp-doc a:hover>code{color:var(--vp-c-brand-dark)}.vp-doc div[class*=language-]{position:relative;margin:16px -24px;background-color:var(--vp-code-block-bg);overflow-x:auto;transition:background-color .5s}@media (min-width: 640px){.vp-doc div[class*=language-]{border-radius:8px;margin:16px 0}}@media (max-width: 639px){.vp-doc li div[class*=language-]{border-radius:8px 0 0 8px}}.vp-doc div[class*=language-]+div[class*=language-],.vp-doc div[class$=-api]+div[class*=language-],.vp-doc div[class*=language-]+div[class$=-api]>div[class*=language-]{margin-top:-8px}.vp-doc [class*=language-] pre,.vp-doc [class*=language-] code{direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}.vp-doc [class*=language-] pre{position:relative;z-index:1;margin:0;padding:20px 0;background:transparent;overflow-x:auto}.vp-doc [class*=language-] code{display:block;padding:0 24px;width:fit-content;min-width:100%;line-height:var(--vp-code-line-height);font-size:var(--vp-code-font-size);color:var(--vp-code-block-color);transition:color .5s}.vp-doc [class*=language-] code .highlighted{background-color:var(--vp-code-line-highlight-color);transition:background-color .5s;margin:0 -24px;padding:0 24px;width:calc(100% + 48px);display:inline-block}.vp-doc [class*=language-] code .highlighted.error{background-color:var(--vp-code-line-error-color)}.vp-doc [class*=language-] code .highlighted.warning{background-color:var(--vp-code-line-warning-color)}.vp-doc [class*=language-] code .diff{transition:background-color .5s;margin:0 -24px;padding:0 24px;width:calc(100% + 48px);display:inline-block}.vp-doc [class*=language-] code .diff:before{position:absolute;left:10px}.vp-doc [class*=language-] .has-focused-lines .line:not(.has-focus){filter:blur(.095rem);opacity:.4;transition:filter .35s,opacity .35s}.vp-doc [class*=language-] .has-focused-lines .line:not(.has-focus){opacity:.7;transition:filter .35s,opacity .35s}.vp-doc [class*=language-]:hover .has-focused-lines .line:not(.has-focus){filter:blur(0);opacity:1}.vp-doc [class*=language-] code .diff.remove{background-color:var(--vp-code-line-diff-remove-color);opacity:.7}.vp-doc [class*=language-] code .diff.remove:before{content:"-";color:var(--vp-code-line-diff-remove-symbol-color)}.vp-doc [class*=language-] code .diff.add{background-color:var(--vp-code-line-diff-add-color)}.vp-doc [class*=language-] code .diff.add:before{content:"+";color:var(--vp-code-line-diff-add-symbol-color)}.vp-doc div[class*=language-].line-numbers-mode{padding-left:32px}.vp-doc .line-numbers-wrapper{position:absolute;top:0;bottom:0;left:0;z-index:3;border-right:1px solid var(--vp-code-block-divider-color);padding-top:20px;width:32px;text-align:center;font-family:var(--vp-font-family-mono);line-height:var(--vp-code-line-height);font-size:var(--vp-code-font-size);color:var(--vp-code-line-number-color);transition:border-color .5s,color .5s}.vp-doc [class*=language-]>button.copy{direction:ltr;position:absolute;top:12px;right:12px;z-index:3;border:1px solid var(--vp-code-copy-code-border-color);border-radius:4px;width:40px;height:40px;background-color:var(--vp-code-copy-code-bg);opacity:0;cursor:pointer;background-image:var(--vp-icon-copy);background-position:50%;background-size:20px;background-repeat:no-repeat;transition:border-color .25s,background-color .25s,opacity .25s}.vp-doc [class*=language-]:hover>button.copy,.vp-doc [class*=language-]>button.copy:focus{opacity:1}.vp-doc [class*=language-]>button.copy:hover,.vp-doc [class*=language-]>button.copy.copied{border-color:var(--vp-code-copy-code-hover-border-color);background-color:var(--vp-code-copy-code-hover-bg)}.vp-doc [class*=language-]>button.copy.copied,.vp-doc [class*=language-]>button.copy:hover.copied{border-radius:0 4px 4px 0;background-color:var(--vp-code-copy-code-hover-bg);background-image:var(--vp-icon-copied)}.vp-doc [class*=language-]>button.copy.copied:before,.vp-doc [class*=language-]>button.copy:hover.copied:before{position:relative;top:-1px;left:-65px;display:flex;justify-content:center;align-items:center;border:1px solid var(--vp-code-copy-code-hover-border-color);border-right:0;border-radius:4px 0 0 4px;width:64px;height:40px;text-align:center;font-size:12px;font-weight:500;color:var(--vp-code-copy-code-active-text);background-color:var(--vp-code-copy-code-hover-bg);white-space:nowrap;content:"Copied"}.vp-doc [class*=language-]>span.lang{position:absolute;top:2px;right:8px;z-index:2;font-size:12px;font-weight:500;color:var(--vp-c-code-dimm);transition:color .4s,opacity .4s}.vp-doc [class*=language-]:hover>button.copy+span.lang,.vp-doc [class*=language-]>button.copy:focus+span.lang{opacity:0}.vp-doc .VPTeamMembers{margin-top:24px}.vp-doc .VPTeamMembers.small.count-1 .container{margin:0!important;max-width:calc((100% - 24px)/2)!important}.vp-doc .VPTeamMembers.small.count-2 .container,.vp-doc .VPTeamMembers.small.count-3 .container{max-width:100%!important}.vp-doc .VPTeamMembers.medium.count-1 .container{margin:0!important;max-width:calc((100% - 24px)/2)!important}:is(.vp-external-link-icon,.vp-doc a[href*="://"],.vp-doc a[target=_blank]):not(.no-icon):after{display:inline-block;margin-top:-1px;margin-left:4px;width:11px;height:11px;background:currentColor;color:var(--vp-c-text-3);flex-shrink:0;--icon: url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M0 0h24v24H0V0z' fill='none' /%3E%3Cpath d='M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z' /%3E%3C/svg%3E");-webkit-mask-image:var(--icon);mask-image:var(--icon)}.vp-external-link-icon:after{content:""}.vp-sponsor{border-radius:16px;overflow:hidden}.vp-sponsor.aside{border-radius:12px}.vp-sponsor-section+.vp-sponsor-section{margin-top:4px}.vp-sponsor-tier{margin-bottom:4px;text-align:center;letter-spacing:1px;line-height:24px;width:100%;font-weight:600;color:var(--vp-c-text-2);background-color:var(--vp-c-bg-soft)}.vp-sponsor.normal .vp-sponsor-tier{padding:13px 0 11px;font-size:14px}.vp-sponsor.aside .vp-sponsor-tier{padding:9px 0 7px;font-size:12px}.vp-sponsor-grid+.vp-sponsor-tier{margin-top:4px}.vp-sponsor-grid{display:flex;flex-wrap:wrap;gap:4px}.vp-sponsor-grid.xmini .vp-sponsor-grid-link{height:64px}.vp-sponsor-grid.xmini .vp-sponsor-grid-image{max-width:64px;max-height:22px}.vp-sponsor-grid.mini .vp-sponsor-grid-link{height:72px}.vp-sponsor-grid.mini .vp-sponsor-grid-image{max-width:96px;max-height:24px}.vp-sponsor-grid.small .vp-sponsor-grid-link{height:96px}.vp-sponsor-grid.small .vp-sponsor-grid-image{max-width:96px;max-height:24px}.vp-sponsor-grid.medium .vp-sponsor-grid-link{height:112px}.vp-sponsor-grid.medium .vp-sponsor-grid-image{max-width:120px;max-height:36px}.vp-sponsor-grid.big .vp-sponsor-grid-link{height:184px}.vp-sponsor-grid.big .vp-sponsor-grid-image{max-width:192px;max-height:56px}.vp-sponsor-grid[data-vp-grid="2"] .vp-sponsor-grid-item{width:calc((100% - 4px)/2)}.vp-sponsor-grid[data-vp-grid="3"] .vp-sponsor-grid-item{width:calc((100% - 4px * 2) / 3)}.vp-sponsor-grid[data-vp-grid="4"] .vp-sponsor-grid-item{width:calc((100% - 12px)/4)}.vp-sponsor-grid[data-vp-grid="5"] .vp-sponsor-grid-item{width:calc((100% - 16px)/5)}.vp-sponsor-grid[data-vp-grid="6"] .vp-sponsor-grid-item{width:calc((100% - 4px * 5) / 6)}.vp-sponsor-grid-item{flex-shrink:0;width:100%;background-color:var(--vp-c-bg-soft);transition:background-color .25s}.vp-sponsor-grid-item:hover{background-color:var(--vp-c-bg-soft-down)}.vp-sponsor-grid-item:hover .vp-sponsor-grid-image{filter:grayscale(0) invert(0)}.vp-sponsor-grid-item.empty:hover{background-color:var(--vp-c-bg-soft)}.dark .vp-sponsor-grid-item:hover{background-color:var(--vp-c-white)}.dark .vp-sponsor-grid-item.empty:hover{background-color:var(--vp-c-bg-soft)}.vp-sponsor-grid-link{display:flex}.vp-sponsor-grid-box{display:flex;justify-content:center;align-items:center;width:100%}.vp-sponsor-grid-image{max-width:100%;filter:grayscale(1);transition:filter .25s}.dark .vp-sponsor-grid-image{filter:grayscale(1) invert(1)}.VPBadge[data-v-1aa52f9a]{display:inline-block;margin-left:2px;border:1px solid transparent;border-radius:10px;padding:0 8px;line-height:18px;font-size:12px;font-weight:600;transform:translateY(-2px)}h1 .VPBadge[data-v-1aa52f9a],h2 .VPBadge[data-v-1aa52f9a],h3 .VPBadge[data-v-1aa52f9a],h4 .VPBadge[data-v-1aa52f9a],h5 .VPBadge[data-v-1aa52f9a],h6 .VPBadge[data-v-1aa52f9a]{vertical-align:text-top}h2 .VPBadge[data-v-1aa52f9a]{border-radius:11px;line-height:20px}.VPBadge.info[data-v-1aa52f9a]{border-color:var(--vp-badge-info-border);color:var(--vp-badge-info-text);background-color:var(--vp-badge-info-bg)}.VPBadge.tip[data-v-1aa52f9a]{border-color:var(--vp-badge-tip-border);color:var(--vp-badge-tip-text);background-color:var(--vp-badge-tip-bg)}.VPBadge.warning[data-v-1aa52f9a]{border-color:var(--vp-badge-warning-border);color:var(--vp-badge-warning-text);background-color:var(--vp-badge-warning-bg)}.VPBadge.danger[data-v-1aa52f9a]{border-color:var(--vp-badge-danger-border);color:var(--vp-badge-danger-text);background-color:var(--vp-badge-danger-bg)}.VPBackdrop[data-v-c6055897]{position:fixed;top:0;right:0;bottom:0;left:0;z-index:var(--vp-z-index-backdrop);background:var(--vp-backdrop-bg-color);transition:opacity .5s}.VPBackdrop.fade-enter-from[data-v-c6055897],.VPBackdrop.fade-leave-to[data-v-c6055897]{opacity:0}.VPBackdrop.fade-leave-active[data-v-c6055897]{transition-duration:.25s}@media (min-width: 1280px){.VPBackdrop[data-v-c6055897]{display:none}}.NotFound[data-v-35559be6]{padding:64px 24px 96px;text-align:center}@media (min-width: 768px){.NotFound[data-v-35559be6]{padding:96px 32px 168px}}.code[data-v-35559be6]{line-height:64px;font-size:64px;font-weight:600}.title[data-v-35559be6]{padding-top:12px;letter-spacing:2px;line-height:20px;font-size:20px;font-weight:700}.divider[data-v-35559be6]{margin:24px auto 18px;width:64px;height:1px;background-color:var(--vp-c-divider)}.quote[data-v-35559be6]{margin:0 auto;max-width:256px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.action[data-v-35559be6]{padding-top:20px}.link[data-v-35559be6]{display:inline-block;border:1px solid var(--vp-c-brand);border-radius:16px;padding:3px 16px;font-size:14px;font-weight:500;color:var(--vp-c-brand);transition:border-color .25s,color .25s}.link[data-v-35559be6]:hover{border-color:var(--vp-c-brand-dark);color:var(--vp-c-brand-dark)}.root[data-v-748e15f2]{position:relative;z-index:1}.nested[data-v-748e15f2]{padding-left:13px}.outline-link[data-v-748e15f2]{display:block;line-height:28px;color:var(--vp-c-text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .5s;font-weight:500}.outline-link[data-v-748e15f2]:hover,.outline-link.active[data-v-748e15f2]{color:var(--vp-c-text-1);transition:color .25s}.outline-link.nested[data-v-748e15f2]{padding-left:13px}.VPDocAsideOutline[data-v-bd3a7332]{display:none}.VPDocAsideOutline.has-outline[data-v-bd3a7332]{display:block}.content[data-v-bd3a7332]{position:relative;border-left:1px solid var(--vp-c-divider);padding-left:16px;font-size:13px;font-weight:500}.outline-marker[data-v-bd3a7332]{position:absolute;top:32px;left:-1px;z-index:0;opacity:0;width:1px;height:18px;background-color:var(--vp-c-brand);transition:top .25s cubic-bezier(0,1,.5,1),background-color .5s,opacity .25s}.outline-title[data-v-bd3a7332]{letter-spacing:.4px;line-height:28px;font-size:13px;font-weight:600}.VPDocAside[data-v-61b1a3c5]{display:flex;flex-direction:column;flex-grow:1}.spacer[data-v-61b1a3c5]{flex-grow:1}.VPDocAside[data-v-61b1a3c5] .spacer+.VPDocAsideSponsors,.VPDocAside[data-v-61b1a3c5] .spacer+.VPDocAsideCarbonAds{margin-top:24px}.VPDocAside[data-v-61b1a3c5] .VPDocAsideSponsors+.VPDocAsideCarbonAds{margin-top:16px}.VPLastUpdated[data-v-06008453]{line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}@media (min-width: 640px){.VPLastUpdated[data-v-06008453]{line-height:32px;font-size:14px;font-weight:500}}.VPDocFooter[data-v-4a75099f]{margin-top:64px}.edit-info[data-v-4a75099f]{padding-bottom:18px}@media (min-width: 640px){.edit-info[data-v-4a75099f]{display:flex;justify-content:space-between;align-items:center;padding-bottom:14px}}.edit-link-button[data-v-4a75099f]{display:flex;align-items:center;border:0;line-height:32px;font-size:14px;font-weight:500;color:var(--vp-c-brand);transition:color .25s}.edit-link-button[data-v-4a75099f]:hover{color:var(--vp-c-brand-dark)}.edit-link-icon[data-v-4a75099f]{margin-right:8px;width:14px;height:14px;fill:currentColor}.prev-next[data-v-4a75099f]{border-top:1px solid var(--vp-c-divider);padding-top:24px;display:grid;grid-row-gap:8px}@media (min-width: 640px){.prev-next[data-v-4a75099f]{grid-template-columns:repeat(2,1fr);grid-column-gap:16px}}.pager-link[data-v-4a75099f]{display:block;border:1px solid var(--vp-c-divider);border-radius:8px;padding:11px 16px 13px;width:100%;height:100%;transition:border-color .25s}.pager-link[data-v-4a75099f]:hover{border-color:var(--vp-c-brand)}.pager-link.next[data-v-4a75099f]{margin-left:auto;text-align:right}.desc[data-v-4a75099f]{display:block;line-height:20px;font-size:12px;font-weight:500;color:var(--vp-c-text-2)}.title[data-v-4a75099f]{display:block;line-height:20px;font-size:14px;font-weight:500;color:var(--vp-c-brand);transition:color .25s}.VPDocOutlineDropdown[data-v-ff90d418]{margin-bottom:42px}.VPDocOutlineDropdown button[data-v-ff90d418]{display:block;font-size:14px;font-weight:500;line-height:24px;color:var(--vp-c-text-2);transition:color .5s;border:1px solid var(--vp-c-border);padding:4px 12px;border-radius:8px}.VPDocOutlineDropdown button[data-v-ff90d418]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPDocOutlineDropdown button.open[data-v-ff90d418]{color:var(--vp-c-text-1)}.icon[data-v-ff90d418]{display:inline-block;vertical-align:middle;margin-left:2px;width:14px;height:14px;fill:currentColor}[data-v-ff90d418] .outline-link{font-size:13px}.open>.icon[data-v-ff90d418]{transform:rotate(90deg)}.items[data-v-ff90d418]{margin-top:10px;border-left:1px solid var(--vp-c-divider)}.VPDoc[data-v-ff46f99c]{padding:32px 24px 96px;width:100%}.VPDoc .VPDocOutlineDropdown[data-v-ff46f99c]{display:none}@media (min-width: 960px) and (max-width: 1279px){.VPDoc .VPDocOutlineDropdown[data-v-ff46f99c]{display:block}}@media (min-width: 768px){.VPDoc[data-v-ff46f99c]{padding:48px 32px 128px}}@media (min-width: 960px){.VPDoc[data-v-ff46f99c]{padding:32px 32px 0}.VPDoc:not(.has-sidebar) .container[data-v-ff46f99c]{display:flex;justify-content:center;max-width:992px}.VPDoc:not(.has-sidebar) .content[data-v-ff46f99c]{max-width:752px}}@media (min-width: 1280px){.VPDoc .container[data-v-ff46f99c]{display:flex;justify-content:center}.VPDoc .aside[data-v-ff46f99c]{display:block}}@media (min-width: 1440px){.VPDoc:not(.has-sidebar) .content[data-v-ff46f99c]{max-width:784px}.VPDoc:not(.has-sidebar) .container[data-v-ff46f99c]{max-width:1104px}}.container[data-v-ff46f99c]{margin:0 auto;width:100%}.aside[data-v-ff46f99c]{position:relative;display:none;order:2;flex-grow:1;padding-left:32px;width:100%;max-width:256px}.left-aside[data-v-ff46f99c]{order:1;padding-left:unset;padding-right:32px}.aside-container[data-v-ff46f99c]{position:fixed;top:0;padding-top:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + var(--vp-doc-top-height, 0px) + 32px);width:224px;height:100vh;overflow-x:hidden;overflow-y:auto;scrollbar-width:none}.aside-container[data-v-ff46f99c]::-webkit-scrollbar{display:none}.aside-curtain[data-v-ff46f99c]{position:fixed;bottom:0;z-index:10;width:224px;height:32px;background:linear-gradient(transparent,var(--vp-c-bg) 70%)}.aside-content[data-v-ff46f99c]{display:flex;flex-direction:column;min-height:calc(100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 32px));padding-bottom:32px}.content[data-v-ff46f99c]{position:relative;margin:0 auto;width:100%}@media (min-width: 960px){.content[data-v-ff46f99c]{padding:0 32px 128px}}@media (min-width: 1280px){.content[data-v-ff46f99c]{order:1;margin:0;min-width:640px}}.content-container[data-v-ff46f99c]{margin:0 auto}.VPDoc.has-aside .content-container[data-v-ff46f99c]{max-width:688px}.external-link-icon-enabled[data-v-ff46f99c] :is(.vp-doc a[href*="://"],.vp-doc a[target=_blank]):after{content:"";color:currentColor}.VPButton[data-v-fceb97bb]{display:inline-block;border:1px solid transparent;text-align:center;font-weight:600;white-space:nowrap;transition:color .25s,border-color .25s,background-color .25s}.VPButton[data-v-fceb97bb]:active{transition:color .1s,border-color .1s,background-color .1s}.VPButton.medium[data-v-fceb97bb]{border-radius:20px;padding:0 20px;line-height:38px;font-size:14px}.VPButton.big[data-v-fceb97bb]{border-radius:24px;padding:0 24px;line-height:46px;font-size:16px}.VPButton.brand[data-v-fceb97bb]{border-color:var(--vp-button-brand-border);color:var(--vp-button-brand-text);background-color:var(--vp-button-brand-bg)}.VPButton.brand[data-v-fceb97bb]:hover{border-color:var(--vp-button-brand-hover-border);color:var(--vp-button-brand-hover-text);background-color:var(--vp-button-brand-hover-bg)}.VPButton.brand[data-v-fceb97bb]:active{border-color:var(--vp-button-brand-active-border);color:var(--vp-button-brand-active-text);background-color:var(--vp-button-brand-active-bg)}.VPButton.alt[data-v-fceb97bb]{border-color:var(--vp-button-alt-border);color:var(--vp-button-alt-text);background-color:var(--vp-button-alt-bg)}.VPButton.alt[data-v-fceb97bb]:hover{border-color:var(--vp-button-alt-hover-border);color:var(--vp-button-alt-hover-text);background-color:var(--vp-button-alt-hover-bg)}.VPButton.alt[data-v-fceb97bb]:active{border-color:var(--vp-button-alt-active-border);color:var(--vp-button-alt-active-text);background-color:var(--vp-button-alt-active-bg)}.VPButton.sponsor[data-v-fceb97bb]{border-color:var(--vp-button-sponsor-border);color:var(--vp-button-sponsor-text);background-color:var(--vp-button-sponsor-bg)}.VPButton.sponsor[data-v-fceb97bb]:hover{border-color:var(--vp-button-sponsor-hover-border);color:var(--vp-button-sponsor-hover-text);background-color:var(--vp-button-sponsor-hover-bg)}.VPButton.sponsor[data-v-fceb97bb]:active{border-color:var(--vp-button-sponsor-active-border);color:var(--vp-button-sponsor-active-text);background-color:var(--vp-button-sponsor-active-bg)}html:not(.dark) .VPImage.dark[data-v-d79cd251]{display:none}.dark .VPImage.light[data-v-d79cd251]{display:none}.VPHero[data-v-d29a3d56]{margin-top:calc((var(--vp-nav-height) + var(--vp-layout-top-height, 0px)) * -1);padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px) 24px 48px}@media (min-width: 640px){.VPHero[data-v-d29a3d56]{padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 48px 64px}}@media (min-width: 960px){.VPHero[data-v-d29a3d56]{padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 64px 64px}}.container[data-v-d29a3d56]{display:flex;flex-direction:column;margin:0 auto;max-width:1152px}@media (min-width: 960px){.container[data-v-d29a3d56]{flex-direction:row}}.main[data-v-d29a3d56]{position:relative;z-index:10;order:2;flex-grow:1;flex-shrink:0}.VPHero.has-image .container[data-v-d29a3d56]{text-align:center}@media (min-width: 960px){.VPHero.has-image .container[data-v-d29a3d56]{text-align:left}}@media (min-width: 960px){.main[data-v-d29a3d56]{order:1;width:calc((100% / 3) * 2)}.VPHero.has-image .main[data-v-d29a3d56]{max-width:592px}}.name[data-v-d29a3d56],.text[data-v-d29a3d56]{max-width:392px;letter-spacing:-.4px;line-height:40px;font-size:32px;font-weight:700;white-space:pre-wrap}.VPHero.has-image .name[data-v-d29a3d56],.VPHero.has-image .text[data-v-d29a3d56]{margin:0 auto}.name[data-v-d29a3d56]{color:var(--vp-home-hero-name-color)}.clip[data-v-d29a3d56]{background:var(--vp-home-hero-name-background);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:var(--vp-home-hero-name-color)}@media (min-width: 640px){.name[data-v-d29a3d56],.text[data-v-d29a3d56]{max-width:576px;line-height:56px;font-size:48px}}@media (min-width: 960px){.name[data-v-d29a3d56],.text[data-v-d29a3d56]{line-height:64px;font-size:56px}.VPHero.has-image .name[data-v-d29a3d56],.VPHero.has-image .text[data-v-d29a3d56]{margin:0}}.tagline[data-v-d29a3d56]{padding-top:8px;max-width:392px;line-height:28px;font-size:18px;font-weight:500;white-space:pre-wrap;color:var(--vp-c-text-2)}.VPHero.has-image .tagline[data-v-d29a3d56]{margin:0 auto}@media (min-width: 640px){.tagline[data-v-d29a3d56]{padding-top:12px;max-width:576px;line-height:32px;font-size:20px}}@media (min-width: 960px){.tagline[data-v-d29a3d56]{line-height:36px;font-size:24px}.VPHero.has-image .tagline[data-v-d29a3d56]{margin:0}}.actions[data-v-d29a3d56]{display:flex;flex-wrap:wrap;margin:-6px;padding-top:24px}.VPHero.has-image .actions[data-v-d29a3d56]{justify-content:center}@media (min-width: 640px){.actions[data-v-d29a3d56]{padding-top:32px}}@media (min-width: 960px){.VPHero.has-image .actions[data-v-d29a3d56]{justify-content:flex-start}}.action[data-v-d29a3d56]{flex-shrink:0;padding:6px}.image[data-v-d29a3d56]{order:1;margin:-76px -24px -48px}@media (min-width: 640px){.image[data-v-d29a3d56]{margin:-108px -24px -48px}}@media (min-width: 960px){.image[data-v-d29a3d56]{flex-grow:1;order:2;margin:0;min-height:100%}}.image-container[data-v-d29a3d56]{position:relative;margin:0 auto;width:320px;height:320px}@media (min-width: 640px){.image-container[data-v-d29a3d56]{width:392px;height:392px}}@media (min-width: 960px){.image-container[data-v-d29a3d56]{display:flex;justify-content:center;align-items:center;width:100%;height:100%;transform:translate(-32px,-32px)}}.image-bg[data-v-d29a3d56]{position:absolute;top:50%;left:50%;border-radius:50%;width:192px;height:192px;background-image:var(--vp-home-hero-image-background-image);filter:var(--vp-home-hero-image-filter);transform:translate(-50%,-50%)}@media (min-width: 640px){.image-bg[data-v-d29a3d56]{width:256px;height:256px}}@media (min-width: 960px){.image-bg[data-v-d29a3d56]{width:320px;height:320px}}[data-v-d29a3d56] .image-src{position:absolute;top:50%;left:50%;max-width:192px;max-height:192px;transform:translate(-50%,-50%)}@media (min-width: 640px){[data-v-d29a3d56] .image-src{max-width:256px;max-height:256px}}@media (min-width: 960px){[data-v-d29a3d56] .image-src{max-width:320px;max-height:320px}}.VPFeature[data-v-cf6e479c]{display:block;border:1px solid var(--vp-c-bg-soft);border-radius:12px;height:100%;background-color:var(--vp-c-bg-soft);transition:border-color .25s,background-color .25s}.VPFeature.link[data-v-cf6e479c]:hover{border-color:var(--vp-c-brand);background-color:var(--vp-c-bg-soft-up)}.box[data-v-cf6e479c]{display:flex;flex-direction:column;padding:24px;height:100%}.VPFeature[data-v-cf6e479c] .VPImage{width:48px;height:48px;margin-bottom:20px}.icon[data-v-cf6e479c]{display:flex;justify-content:center;align-items:center;margin-bottom:20px;border-radius:6px;background-color:var(--vp-c-bg-soft-down);width:48px;height:48px;font-size:24px;transition:background-color .25s}.title[data-v-cf6e479c]{line-height:24px;font-size:16px;font-weight:600}.details[data-v-cf6e479c]{flex-grow:1;padding-top:8px;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.link-text[data-v-cf6e479c]{padding-top:8px}.link-text-value[data-v-cf6e479c]{display:flex;align-items:center;font-size:14px;font-weight:500;color:var(--vp-c-brand)}.link-text-icon[data-v-cf6e479c]{display:inline-block;margin-left:6px;width:14px;height:14px;fill:currentColor}.VPFeatures[data-v-8ce6a87c]{position:relative;padding:0 24px}@media (min-width: 640px){.VPFeatures[data-v-8ce6a87c]{padding:0 48px}}@media (min-width: 960px){.VPFeatures[data-v-8ce6a87c]{padding:0 64px}}.container[data-v-8ce6a87c]{margin:0 auto;max-width:1152px}.items[data-v-8ce6a87c]{display:flex;flex-wrap:wrap;margin:-8px}.item[data-v-8ce6a87c]{padding:8px;width:100%}@media (min-width: 640px){.item.grid-2[data-v-8ce6a87c],.item.grid-4[data-v-8ce6a87c],.item.grid-6[data-v-8ce6a87c]{width:50%}}@media (min-width: 768px){.item.grid-2[data-v-8ce6a87c],.item.grid-4[data-v-8ce6a87c]{width:50%}.item.grid-3[data-v-8ce6a87c],.item.grid-6[data-v-8ce6a87c]{width:calc(100% / 3)}}@media (min-width: 960px){.item.grid-4[data-v-8ce6a87c]{width:25%}}.VPHome[data-v-5d1897af]{padding-bottom:96px}.VPHome[data-v-5d1897af] .VPHomeSponsors{margin-top:112px;margin-bottom:-128px}@media (min-width: 768px){.VPHome[data-v-5d1897af]{padding-bottom:128px}}.VPContent[data-v-35450758]{flex-grow:1;flex-shrink:0;margin:var(--vp-layout-top-height, 0px) auto 0;width:100%}.VPContent.is-home[data-v-35450758]{width:100%;max-width:100%}.VPContent.has-sidebar[data-v-35450758]{margin:0}@media (min-width: 960px){.VPContent[data-v-35450758]{padding-top:var(--vp-nav-height)}.VPContent.has-sidebar[data-v-35450758]{margin:var(--vp-layout-top-height, 0px) 0 0;padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPContent.has-sidebar[data-v-35450758]{padding-right:calc((100vw - var(--vp-layout-max-width)) / 2);padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.VPFooter[data-v-a51421e3]{position:relative;z-index:var(--vp-z-index-footer);border-top:1px solid var(--vp-c-gutter);padding:32px 24px;background-color:var(--vp-c-bg)}.VPFooter.has-sidebar[data-v-a51421e3]{display:none}@media (min-width: 768px){.VPFooter[data-v-a51421e3]{padding:32px}}.container[data-v-a51421e3]{margin:0 auto;max-width:var(--vp-layout-max-width);text-align:center}.message[data-v-a51421e3],.copyright[data-v-a51421e3]{line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.VPLocalNavOutlineDropdown[data-v-e7d97842]{padding:12px 20px 11px}.VPLocalNavOutlineDropdown button[data-v-e7d97842]{display:block;font-size:12px;font-weight:500;line-height:24px;color:var(--vp-c-text-2);transition:color .5s;position:relative}.VPLocalNavOutlineDropdown button[data-v-e7d97842]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPLocalNavOutlineDropdown button.open[data-v-e7d97842]{color:var(--vp-c-text-1)}.icon[data-v-e7d97842]{display:inline-block;vertical-align:middle;margin-left:2px;width:14px;height:14px;fill:currentColor}[data-v-e7d97842] .outline-link{font-size:14px;padding:2px 0}.open>.icon[data-v-e7d97842]{transform:rotate(90deg)}.items[data-v-e7d97842]{position:absolute;left:20px;right:20px;top:64px;background-color:var(--vp-local-nav-bg-color);padding:4px 10px 16px;border:1px solid var(--vp-c-divider);border-radius:8px;max-height:calc(var(--vp-vh, 100vh) - 86px);overflow:hidden auto;box-shadow:var(--vp-shadow-3)}.top-link[data-v-e7d97842]{display:block;color:var(--vp-c-brand);font-size:13px;font-weight:500;padding:6px 0;margin:0 13px 10px;border-bottom:1px solid var(--vp-c-divider)}.flyout-enter-active[data-v-e7d97842]{transition:all .2s ease-out}.flyout-leave-active[data-v-e7d97842]{transition:all .15s ease-in}.flyout-enter-from[data-v-e7d97842],.flyout-leave-to[data-v-e7d97842]{opacity:0;transform:translateY(-16px)}.VPLocalNav[data-v-341c3ee5]{position:sticky;top:0;left:0;z-index:var(--vp-z-index-local-nav);display:flex;justify-content:space-between;align-items:center;border-top:1px solid var(--vp-c-gutter);border-bottom:1px solid var(--vp-c-gutter);padding-top:var(--vp-layout-top-height, 0px);width:100%;background-color:var(--vp-local-nav-bg-color)}.VPLocalNav.fixed[data-v-341c3ee5]{position:fixed}.VPLocalNav.reached-top[data-v-341c3ee5]{border-top-color:transparent}@media (min-width: 960px){.VPLocalNav[data-v-341c3ee5]{display:none}}.menu[data-v-341c3ee5]{display:flex;align-items:center;padding:12px 24px 11px;line-height:24px;font-size:12px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.menu[data-v-341c3ee5]:hover{color:var(--vp-c-text-1);transition:color .25s}@media (min-width: 768px){.menu[data-v-341c3ee5]{padding:0 32px}}.menu-icon[data-v-341c3ee5]{margin-right:8px;width:16px;height:16px;fill:currentColor}.VPOutlineDropdown[data-v-341c3ee5]{padding:12px 24px 11px}@media (min-width: 768px){.VPOutlineDropdown[data-v-341c3ee5]{padding:12px 32px 11px}}.VPSwitch[data-v-40b19bac]{position:relative;border-radius:11px;display:block;width:40px;height:22px;flex-shrink:0;border:1px solid var(--vp-input-border-color);background-color:var(--vp-input-switch-bg-color);transition:border-color .25s}.VPSwitch[data-v-40b19bac]:hover{border-color:var(--vp-input-hover-border-color)}.check[data-v-40b19bac]{position:absolute;top:1px;left:1px;width:18px;height:18px;border-radius:50%;background-color:var(--vp-c-neutral-inverse);box-shadow:var(--vp-shadow-1);transition:transform .25s}.icon[data-v-40b19bac]{position:relative;display:block;width:18px;height:18px;border-radius:50%;overflow:hidden}.icon[data-v-40b19bac] svg{position:absolute;top:3px;left:3px;width:12px;height:12px;fill:var(--vp-c-text-2)}.dark .icon[data-v-40b19bac] svg{fill:var(--vp-c-text-1);transition:opacity .25s}.sun[data-v-535a86eb]{opacity:1}.moon[data-v-535a86eb],.dark .sun[data-v-535a86eb]{opacity:0}.dark .moon[data-v-535a86eb]{opacity:1}.dark .VPSwitchAppearance[data-v-535a86eb] .check{transform:translate(18px)}.VPNavBarAppearance[data-v-c4c830a7]{display:none}@media (min-width: 1280px){.VPNavBarAppearance[data-v-c4c830a7]{display:flex;align-items:center}}.VPMenuGroup+.VPMenuLink[data-v-2b1427af]{margin:12px -12px 0;border-top:1px solid var(--vp-c-divider);padding:12px 12px 0}.link[data-v-2b1427af]{display:block;border-radius:6px;padding:0 12px;line-height:32px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);white-space:nowrap;transition:background-color .25s,color .25s}.link[data-v-2b1427af]:hover{color:var(--vp-c-brand);background-color:var(--vp-c-bg-elv-mute)}.link.active[data-v-2b1427af]{color:var(--vp-c-brand)}.VPMenuGroup[data-v-f69155d8]{margin:12px -12px 0;border-top:1px solid var(--vp-c-divider);padding:12px 12px 0}.VPMenuGroup[data-v-f69155d8]:first-child{margin-top:0;border-top:0;padding-top:0}.VPMenuGroup+.VPMenuGroup[data-v-f69155d8]{margin-top:12px;border-top:1px solid var(--vp-c-divider)}.title[data-v-f69155d8]{padding:0 12px;line-height:32px;font-size:14px;font-weight:600;color:var(--vp-c-text-2);white-space:nowrap;transition:color .25s}.VPMenu[data-v-bcc9fed1]{border-radius:12px;padding:12px;min-width:128px;border:1px solid var(--vp-c-divider);background-color:var(--vp-c-bg-elv);box-shadow:var(--vp-shadow-3);transition:background-color .5s;max-height:calc(100vh - var(--vp-nav-height));overflow-y:auto}.VPMenu[data-v-bcc9fed1] .group{margin:0 -12px;padding:0 12px 12px}.VPMenu[data-v-bcc9fed1] .group+.group{border-top:1px solid var(--vp-c-divider);padding:11px 12px 12px}.VPMenu[data-v-bcc9fed1] .group:last-child{padding-bottom:0}.VPMenu[data-v-bcc9fed1] .group+.item{border-top:1px solid var(--vp-c-divider);padding:11px 16px 0}.VPMenu[data-v-bcc9fed1] .item{padding:0 16px;white-space:nowrap}.VPMenu[data-v-bcc9fed1] .label{flex-grow:1;line-height:28px;font-size:12px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.VPMenu[data-v-bcc9fed1] .action{padding-left:24px}.VPFlyout[data-v-87f2bbca]{position:relative}.VPFlyout[data-v-87f2bbca]:hover{color:var(--vp-c-brand);transition:color .25s}.VPFlyout:hover .text[data-v-87f2bbca]{color:var(--vp-c-text-2)}.VPFlyout:hover .icon[data-v-87f2bbca]{fill:var(--vp-c-text-2)}.VPFlyout.active .text[data-v-87f2bbca]{color:var(--vp-c-brand)}.VPFlyout.active:hover .text[data-v-87f2bbca]{color:var(--vp-c-brand-dark)}.VPFlyout:hover .menu[data-v-87f2bbca],.button[aria-expanded=true]+.menu[data-v-87f2bbca]{opacity:1;visibility:visible;transform:translateY(0)}.button[data-v-87f2bbca]{display:flex;align-items:center;padding:0 12px;height:var(--vp-nav-height);color:var(--vp-c-text-1);transition:color .5s}.text[data-v-87f2bbca]{display:flex;align-items:center;line-height:var(--vp-nav-height);font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.option-icon[data-v-87f2bbca]{margin-right:0;width:16px;height:16px;fill:currentColor}.text-icon[data-v-87f2bbca]{margin-left:4px;width:14px;height:14px;fill:currentColor}.icon[data-v-87f2bbca]{width:20px;height:20px;fill:currentColor;transition:fill .25s}.menu[data-v-87f2bbca]{position:absolute;top:calc(var(--vp-nav-height) / 2 + 20px);right:0;opacity:0;visibility:hidden;transition:opacity .25s,visibility .25s,transform .25s}.VPSocialLink[data-v-39e5dc3a]{display:flex;justify-content:center;align-items:center;width:36px;height:36px;color:var(--vp-c-text-2);transition:color .5s}.VPSocialLink[data-v-39e5dc3a]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPSocialLink[data-v-39e5dc3a]>svg{width:20px;height:20px;fill:currentColor}.VPSocialLinks[data-v-726a530e]{display:flex;justify-content:center}.VPNavBarExtra[data-v-4b777cbc]{display:none;margin-right:-12px}@media (min-width: 768px){.VPNavBarExtra[data-v-4b777cbc]{display:block}}@media (min-width: 1280px){.VPNavBarExtra[data-v-4b777cbc]{display:none}}.trans-title[data-v-4b777cbc]{padding:0 24px 0 12px;line-height:32px;font-size:14px;font-weight:700;color:var(--vp-c-text-1)}.item.appearance[data-v-4b777cbc],.item.social-links[data-v-4b777cbc]{display:flex;align-items:center;padding:0 12px}.item.appearance[data-v-4b777cbc]{min-width:176px}.appearance-action[data-v-4b777cbc]{margin-right:-2px}.social-links-list[data-v-4b777cbc]{margin:-4px -8px}.VPNavBarHamburger[data-v-1363b7e8]{display:flex;justify-content:center;align-items:center;width:48px;height:var(--vp-nav-height)}@media (min-width: 768px){.VPNavBarHamburger[data-v-1363b7e8]{display:none}}.container[data-v-1363b7e8]{position:relative;width:16px;height:14px;overflow:hidden}.VPNavBarHamburger:hover .top[data-v-1363b7e8]{top:0;left:0;transform:translate(4px)}.VPNavBarHamburger:hover .middle[data-v-1363b7e8]{top:6px;left:0;transform:translate(0)}.VPNavBarHamburger:hover .bottom[data-v-1363b7e8]{top:12px;left:0;transform:translate(8px)}.VPNavBarHamburger.active .top[data-v-1363b7e8]{top:6px;transform:translate(0) rotate(225deg)}.VPNavBarHamburger.active .middle[data-v-1363b7e8]{top:6px;transform:translate(16px)}.VPNavBarHamburger.active .bottom[data-v-1363b7e8]{top:6px;transform:translate(0) rotate(135deg)}.VPNavBarHamburger.active:hover .top[data-v-1363b7e8],.VPNavBarHamburger.active:hover .middle[data-v-1363b7e8],.VPNavBarHamburger.active:hover .bottom[data-v-1363b7e8]{background-color:var(--vp-c-text-2);transition:top .25s,background-color .25s,transform .25s}.top[data-v-1363b7e8],.middle[data-v-1363b7e8],.bottom[data-v-1363b7e8]{position:absolute;width:16px;height:2px;background-color:var(--vp-c-text-1);transition:top .25s,background-color .5s,transform .25s}.top[data-v-1363b7e8]{top:0;left:0;transform:translate(0)}.middle[data-v-1363b7e8]{top:6px;left:0;transform:translate(8px)}.bottom[data-v-1363b7e8]{top:12px;left:0;transform:translate(4px)}.VPNavBarMenuLink[data-v-6b3352fb]{display:flex;align-items:center;padding:0 12px;line-height:var(--vp-nav-height);font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.VPNavBarMenuLink.active[data-v-6b3352fb],.VPNavBarMenuLink[data-v-6b3352fb]:hover{color:var(--vp-c-brand)}.VPNavBarMenu[data-v-bc887b7f]{display:none}@media (min-width: 768px){.VPNavBarMenu[data-v-bc887b7f]{display:flex}}/*! @docsearch/css 3.5.1 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */:root{--docsearch-primary-color:#5468ff;--docsearch-text-color:#1c1e21;--docsearch-spacing:12px;--docsearch-icon-stroke-width:1.4;--docsearch-highlight-color:var(--docsearch-primary-color);--docsearch-muted-color:#969faf;--docsearch-container-background:rgba(101,108,133,.8);--docsearch-logo-color:#5468ff;--docsearch-modal-width:560px;--docsearch-modal-height:600px;--docsearch-modal-background:#f5f6f7;--docsearch-modal-shadow:inset 1px 1px 0 0 hsla(0,0%,100%,.5),0 3px 8px 0 #555a64;--docsearch-searchbox-height:56px;--docsearch-searchbox-background:#ebedf0;--docsearch-searchbox-focus-background:#fff;--docsearch-searchbox-shadow:inset 0 0 0 2px var(--docsearch-primary-color);--docsearch-hit-height:56px;--docsearch-hit-color:#444950;--docsearch-hit-active-color:#fff;--docsearch-hit-background:#fff;--docsearch-hit-shadow:0 1px 3px 0 #d4d9e1;--docsearch-key-gradient:linear-gradient(-225deg,#d5dbe4,#f8f8f8);--docsearch-key-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 2px 1px rgba(30,35,90,.4);--docsearch-footer-height:44px;--docsearch-footer-background:#fff;--docsearch-footer-shadow:0 -1px 0 0 #e0e3e8,0 -3px 6px 0 rgba(69,98,155,.12)}html[data-theme=dark]{--docsearch-text-color:#f5f6f7;--docsearch-container-background:rgba(9,10,17,.8);--docsearch-modal-background:#15172a;--docsearch-modal-shadow:inset 1px 1px 0 0 #2c2e40,0 3px 8px 0 #000309;--docsearch-searchbox-background:#090a11;--docsearch-searchbox-focus-background:#000;--docsearch-hit-color:#bec3c9;--docsearch-hit-shadow:none;--docsearch-hit-background:#090a11;--docsearch-key-gradient:linear-gradient(-26.5deg,#565872,#31355b);--docsearch-key-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 2px 2px 0 rgba(3,4,9,.3);--docsearch-footer-background:#1e2136;--docsearch-footer-shadow:inset 0 1px 0 0 rgba(73,76,106,.5),0 -4px 8px 0 rgba(0,0,0,.2);--docsearch-logo-color:#fff;--docsearch-muted-color:#7f8497}.DocSearch-Button{align-items:center;background:var(--docsearch-searchbox-background);border:0;border-radius:40px;color:var(--docsearch-muted-color);cursor:pointer;display:flex;font-weight:500;height:36px;justify-content:space-between;margin:0 0 0 16px;padding:0 8px;-webkit-user-select:none;user-select:none}.DocSearch-Button:active,.DocSearch-Button:focus,.DocSearch-Button:hover{background:var(--docsearch-searchbox-focus-background);box-shadow:var(--docsearch-searchbox-shadow);color:var(--docsearch-text-color);outline:none}.DocSearch-Button-Container{align-items:center;display:flex}.DocSearch-Search-Icon{stroke-width:1.6}.DocSearch-Button .DocSearch-Search-Icon{color:var(--docsearch-text-color)}.DocSearch-Button-Placeholder{font-size:1rem;padding:0 12px 0 6px}.DocSearch-Button-Keys{display:flex;min-width:calc(40px + .8em)}.DocSearch-Button-Key{align-items:center;background:var(--docsearch-key-gradient);border-radius:3px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;position:relative;padding:0 0 2px;border:0;top:-1px;width:20px}@media (max-width:768px){.DocSearch-Button-Keys,.DocSearch-Button-Placeholder{display:none}}.DocSearch--active{overflow:hidden!important}.DocSearch-Container,.DocSearch-Container *{box-sizing:border-box}.DocSearch-Container{background-color:var(--docsearch-container-background);height:100vh;left:0;position:fixed;top:0;width:100vw;z-index:200}.DocSearch-Container a{text-decoration:none}.DocSearch-Link{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;font:inherit;margin:0;padding:0}.DocSearch-Modal{background:var(--docsearch-modal-background);border-radius:6px;box-shadow:var(--docsearch-modal-shadow);flex-direction:column;margin:60px auto auto;max-width:var(--docsearch-modal-width);position:relative}.DocSearch-SearchBar{display:flex;padding:var(--docsearch-spacing) var(--docsearch-spacing) 0}.DocSearch-Form{align-items:center;background:var(--docsearch-searchbox-focus-background);border-radius:4px;box-shadow:var(--docsearch-searchbox-shadow);display:flex;height:var(--docsearch-searchbox-height);margin:0;padding:0 var(--docsearch-spacing);position:relative;width:100%}.DocSearch-Input{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:0;color:var(--docsearch-text-color);flex:1;font:inherit;font-size:1.2em;height:100%;outline:none;padding:0 0 0 8px;width:80%}.DocSearch-Input::placeholder{color:var(--docsearch-muted-color);opacity:1}.DocSearch-Input::-webkit-search-cancel-button,.DocSearch-Input::-webkit-search-decoration,.DocSearch-Input::-webkit-search-results-button,.DocSearch-Input::-webkit-search-results-decoration{display:none}.DocSearch-LoadingIndicator,.DocSearch-MagnifierLabel,.DocSearch-Reset{margin:0;padding:0}.DocSearch-MagnifierLabel,.DocSearch-Reset{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}.DocSearch-Container--Stalled .DocSearch-MagnifierLabel,.DocSearch-LoadingIndicator{display:none}.DocSearch-Container--Stalled .DocSearch-LoadingIndicator{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Reset{animation:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;right:0;stroke-width:var(--docsearch-icon-stroke-width)}}.DocSearch-Reset{animation:fade-in .1s ease-in forwards;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;padding:2px;right:0;stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Reset[hidden]{display:none}.DocSearch-Reset:hover{color:var(--docsearch-highlight-color)}.DocSearch-LoadingIndicator svg,.DocSearch-MagnifierLabel svg{height:24px;width:24px}.DocSearch-Cancel{display:none}.DocSearch-Dropdown{max-height:calc(var(--docsearch-modal-height) - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height));min-height:var(--docsearch-spacing);overflow-y:auto;overflow-y:overlay;padding:0 var(--docsearch-spacing);scrollbar-color:var(--docsearch-muted-color) var(--docsearch-modal-background);scrollbar-width:thin}.DocSearch-Dropdown::-webkit-scrollbar{width:12px}.DocSearch-Dropdown::-webkit-scrollbar-track{background:transparent}.DocSearch-Dropdown::-webkit-scrollbar-thumb{background-color:var(--docsearch-muted-color);border:3px solid var(--docsearch-modal-background);border-radius:20px}.DocSearch-Dropdown ul{list-style:none;margin:0;padding:0}.DocSearch-Label{font-size:.75em;line-height:1.6em}.DocSearch-Help,.DocSearch-Label{color:var(--docsearch-muted-color)}.DocSearch-Help{font-size:.9em;margin:0;-webkit-user-select:none;user-select:none}.DocSearch-Title{font-size:1.2em}.DocSearch-Logo a{display:flex}.DocSearch-Logo svg{color:var(--docsearch-logo-color);margin-left:8px}.DocSearch-Hits:last-of-type{margin-bottom:24px}.DocSearch-Hits mark{background:none;color:var(--docsearch-highlight-color)}.DocSearch-HitsFooter{color:var(--docsearch-muted-color);display:flex;font-size:.85em;justify-content:center;margin-bottom:var(--docsearch-spacing);padding:var(--docsearch-spacing)}.DocSearch-HitsFooter a{border-bottom:1px solid;color:inherit}.DocSearch-Hit{border-radius:4px;display:flex;padding-bottom:4px;position:relative}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--deleting{transition:none}}.DocSearch-Hit--deleting{opacity:0;transition:all .25s linear}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--favoriting{transition:none}}.DocSearch-Hit--favoriting{transform:scale(0);transform-origin:top center;transition:all .25s linear;transition-delay:.25s}.DocSearch-Hit a{background:var(--docsearch-hit-background);border-radius:4px;box-shadow:var(--docsearch-hit-shadow);display:block;padding-left:var(--docsearch-spacing);width:100%}.DocSearch-Hit-source{background:var(--docsearch-modal-background);color:var(--docsearch-highlight-color);font-size:.85em;font-weight:600;line-height:32px;margin:0 -4px;padding:8px 4px 0;position:sticky;top:0;z-index:10}.DocSearch-Hit-Tree{color:var(--docsearch-muted-color);height:var(--docsearch-hit-height);opacity:.5;stroke-width:var(--docsearch-icon-stroke-width);width:24px}.DocSearch-Hit[aria-selected=true] a{background-color:var(--docsearch-highlight-color)}.DocSearch-Hit[aria-selected=true] mark{text-decoration:underline}.DocSearch-Hit-Container{align-items:center;color:var(--docsearch-hit-color);display:flex;flex-direction:row;height:var(--docsearch-hit-height);padding:0 var(--docsearch-spacing) 0 0}.DocSearch-Hit-icon{height:20px;width:20px}.DocSearch-Hit-action,.DocSearch-Hit-icon{color:var(--docsearch-muted-color);stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Hit-action{align-items:center;display:flex;height:22px;width:22px}.DocSearch-Hit-action svg{display:block;height:18px;width:18px}.DocSearch-Hit-action+.DocSearch-Hit-action{margin-left:6px}.DocSearch-Hit-action-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:inherit;cursor:pointer;padding:2px}svg.DocSearch-Hit-Select-Icon{display:none}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Select-Icon{display:block}.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:rgba(0,0,0,.2);transition:background-color .1s ease-in}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{transition:none}}.DocSearch-Hit-action-button:focus path,.DocSearch-Hit-action-button:hover path{fill:#fff}.DocSearch-Hit-content-wrapper{display:flex;flex:1 1 auto;flex-direction:column;font-weight:500;justify-content:center;line-height:1.2em;margin:0 8px;overflow-x:hidden;position:relative;text-overflow:ellipsis;white-space:nowrap;width:80%}.DocSearch-Hit-title{font-size:.9em}.DocSearch-Hit-path{color:var(--docsearch-muted-color);font-size:.75em}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-action,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-icon,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-path,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-text,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-title,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Tree,.DocSearch-Hit[aria-selected=true] mark{color:var(--docsearch-hit-active-color)!important}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:rgba(0,0,0,.2);transition:none}}.DocSearch-ErrorScreen,.DocSearch-NoResults,.DocSearch-StartScreen{font-size:.9em;margin:0 auto;padding:36px 0;text-align:center;width:80%}.DocSearch-Screen-Icon{color:var(--docsearch-muted-color);padding-bottom:12px}.DocSearch-NoResults-Prefill-List{display:inline-block;padding-bottom:24px;text-align:left}.DocSearch-NoResults-Prefill-List ul{display:inline-block;padding:8px 0 0}.DocSearch-NoResults-Prefill-List li{list-style-position:inside;list-style-type:"» "}.DocSearch-Prefill{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:1em;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;font-size:1em;font-weight:700;padding:0}.DocSearch-Prefill:focus,.DocSearch-Prefill:hover{outline:none;text-decoration:underline}.DocSearch-Footer{align-items:center;background:var(--docsearch-footer-background);border-radius:0 0 8px 8px;box-shadow:var(--docsearch-footer-shadow);display:flex;flex-direction:row-reverse;flex-shrink:0;height:var(--docsearch-footer-height);justify-content:space-between;padding:0 var(--docsearch-spacing);position:relative;-webkit-user-select:none;user-select:none;width:100%;z-index:300}.DocSearch-Commands{color:var(--docsearch-muted-color);display:flex;list-style:none;margin:0;padding:0}.DocSearch-Commands li{align-items:center;display:flex}.DocSearch-Commands li:not(:last-of-type){margin-right:.8em}.DocSearch-Commands-Key{align-items:center;background:var(--docsearch-key-gradient);border-radius:2px;box-shadow:var(--docsearch-key-shadow);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 1px;color:var(--docsearch-muted-color);border:0;width:20px}@media (max-width:768px){:root{--docsearch-spacing:10px;--docsearch-footer-height:40px}.DocSearch-Dropdown{height:100%}.DocSearch-Container{height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);position:absolute}.DocSearch-Footer{border-radius:0;bottom:0;position:absolute}.DocSearch-Hit-content-wrapper{display:flex;position:relative;width:80%}.DocSearch-Modal{border-radius:0;box-shadow:none;height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);margin:0;max-width:100%;width:100%}.DocSearch-Dropdown{max-height:calc(var(--docsearch-vh, 1vh)*100 - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height))}.DocSearch-Cancel{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;flex:none;font:inherit;font-size:1em;font-weight:500;margin-left:var(--docsearch-spacing);outline:none;overflow:hidden;padding:0;-webkit-user-select:none;user-select:none;white-space:nowrap}.DocSearch-Commands,.DocSearch-Hit-Tree{display:none}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}.DocSearch{--docsearch-primary-color: var(--vp-c-brand);--docsearch-highlight-color: var(--docsearch-primary-color);--docsearch-text-color: var(--vp-c-text-1);--docsearch-muted-color: var(--vp-c-text-2);--docsearch-searchbox-shadow: none;--docsearch-searchbox-focus-background: transparent;--docsearch-key-gradient: transparent;--docsearch-key-shadow: none;--docsearch-modal-background: var(--vp-c-bg-soft);--docsearch-footer-background: var(--vp-c-bg)}.dark .DocSearch{--docsearch-modal-shadow: none;--docsearch-footer-shadow: none;--docsearch-logo-color: var(--vp-c-text-2);--docsearch-hit-background: var(--vp-c-bg-soft-mute);--docsearch-hit-color: var(--vp-c-text-2);--docsearch-hit-shadow: none}.DocSearch-Button{display:flex;justify-content:center;align-items:center;margin:0;padding:0;width:48px;height:55px;background:transparent;transition:border-color .25s}.DocSearch-Button:hover{background:transparent}.DocSearch-Button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}.DocSearch-Button:focus:not(:focus-visible){outline:none!important}@media (min-width: 768px){.DocSearch-Button{justify-content:flex-start;border:1px solid transparent;border-radius:8px;padding:0 10px 0 12px;width:100%;height:40px;background-color:var(--vp-c-bg-alt)}.DocSearch-Button:hover{border-color:var(--vp-c-brand);background:var(--vp-c-bg-alt)}}.DocSearch-Button .DocSearch-Button-Container{display:flex;align-items:center}.DocSearch-Button .DocSearch-Search-Icon{position:relative;width:16px;height:16px;color:var(--vp-c-text-1);fill:currentColor;transition:color .5s}.DocSearch-Button:hover .DocSearch-Search-Icon{color:var(--vp-c-text-1)}@media (min-width: 768px){.DocSearch-Button .DocSearch-Search-Icon{top:1px;margin-right:8px;width:14px;height:14px;color:var(--vp-c-text-2)}}.DocSearch-Button .DocSearch-Button-Placeholder{display:none;margin-top:2px;padding:0 16px 0 0;font-size:13px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.DocSearch-Button:hover .DocSearch-Button-Placeholder{color:var(--vp-c-text-1)}@media (min-width: 768px){.DocSearch-Button .DocSearch-Button-Placeholder{display:inline-block}}.DocSearch-Button .DocSearch-Button-Keys{direction:ltr;display:none;min-width:auto}@media (min-width: 768px){.DocSearch-Button .DocSearch-Button-Keys{display:flex;align-items:center}}.DocSearch-Button .DocSearch-Button-Key{display:block;margin:2px 0 0;border:1px solid var(--vp-c-divider);border-right:none;border-radius:4px 0 0 4px;padding-left:6px;min-width:0;width:auto;height:22px;line-height:22px;font-family:var(--vp-font-family-base);font-size:12px;font-weight:500;transition:color .5s,border-color .5s}.DocSearch-Button .DocSearch-Button-Key+.DocSearch-Button-Key{border-right:1px solid var(--vp-c-divider);border-left:none;border-radius:0 4px 4px 0;padding-left:2px;padding-right:6px}.DocSearch-Button .DocSearch-Button-Key:first-child{font-size:1px;letter-spacing:-12px;color:transparent}.DocSearch-Button .DocSearch-Button-Key:first-child:after{content:var(--vp-meta-key);font-size:12px;letter-spacing:normal;color:var(--docsearch-muted-color)}.DocSearch-Button .DocSearch-Button-Key:first-child>*{display:none}.VPNavBarSearch{display:flex;align-items:center}@media (min-width: 768px){.VPNavBarSearch{flex-grow:1;padding-left:24px}}@media (min-width: 960px){.VPNavBarSearch{padding-left:32px}}.dark .DocSearch-Footer{border-top:1px solid var(--vp-c-divider)}.DocSearch-Form{border:1px solid var(--vp-c-brand);background-color:var(--vp-c-white)}.dark .DocSearch-Form{background-color:var(--vp-c-bg-soft-mute)}.DocSearch-Screen-Icon>svg{margin:auto}.VPNavBarSocialLinks[data-v-ed745fe3]{display:none}@media (min-width: 1280px){.VPNavBarSocialLinks[data-v-ed745fe3]{display:flex;align-items:center}}.title[data-v-edfdb996]{display:flex;align-items:center;border-bottom:1px solid transparent;width:100%;height:var(--vp-nav-height);font-size:16px;font-weight:600;color:var(--vp-c-text-1);transition:opacity .25s}@media (min-width: 960px){.title[data-v-edfdb996]{flex-shrink:0}.VPNavBarTitle.has-sidebar .title[data-v-edfdb996]{border-bottom-color:var(--vp-c-divider)}}[data-v-edfdb996] .logo{margin-right:8px;height:var(--vp-nav-logo-height)}.VPNavBarTranslations[data-v-e2ffc50f]{display:none}@media (min-width: 1280px){.VPNavBarTranslations[data-v-e2ffc50f]{display:flex;align-items:center}}.title[data-v-e2ffc50f]{padding:0 24px 0 12px;line-height:32px;font-size:14px;font-weight:700;color:var(--vp-c-text-1)}.VPNavBar[data-v-cf1858b3]{position:relative;border-bottom:1px solid transparent;padding:0 8px 0 24px;height:var(--vp-nav-height);pointer-events:none;white-space:nowrap}@media (min-width: 768px){.VPNavBar[data-v-cf1858b3]{padding:0 32px}}@media (min-width: 960px){.VPNavBar.has-sidebar[data-v-cf1858b3]{padding:0}.VPNavBar.fill[data-v-cf1858b3]:not(.has-sidebar){border-bottom-color:var(--vp-c-gutter);background-color:var(--vp-nav-bg-color)}}.container[data-v-cf1858b3]{display:flex;justify-content:space-between;margin:0 auto;max-width:calc(var(--vp-layout-max-width) - 64px);height:var(--vp-nav-height);pointer-events:none}.container>.title[data-v-cf1858b3],.container>.content[data-v-cf1858b3]{pointer-events:none}.container[data-v-cf1858b3] *{pointer-events:auto}@media (min-width: 960px){.VPNavBar.has-sidebar .container[data-v-cf1858b3]{max-width:100%}}.title[data-v-cf1858b3]{flex-shrink:0;height:calc(var(--vp-nav-height) - 1px);transition:background-color .5s}@media (min-width: 960px){.VPNavBar.has-sidebar .title[data-v-cf1858b3]{position:absolute;top:0;left:0;z-index:2;padding:0 32px;width:var(--vp-sidebar-width);height:var(--vp-nav-height);background-color:transparent}}@media (min-width: 1440px){.VPNavBar.has-sidebar .title[data-v-cf1858b3]{padding-left:max(32px,calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));width:calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px)}}.content[data-v-cf1858b3]{flex-grow:1}@media (min-width: 960px){.VPNavBar.has-sidebar .content[data-v-cf1858b3]{position:relative;z-index:1;padding-right:32px;padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPNavBar.has-sidebar .content[data-v-cf1858b3]{padding-right:calc((100vw - var(--vp-layout-max-width)) / 2 + 32px);padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.content-body[data-v-cf1858b3]{display:flex;justify-content:flex-end;align-items:center;height:calc(var(--vp-nav-height) - 1px);transition:background-color .5s}@media (min-width: 960px){.VPNavBar.has-sidebar .content-body[data-v-cf1858b3],.VPNavBar.fill .content-body[data-v-cf1858b3]{position:relative;background-color:var(--vp-nav-bg-color)}}@media (max-width: 768px){.content-body[data-v-cf1858b3]{column-gap:.5rem}}.menu+.translations[data-v-cf1858b3]:before,.menu+.appearance[data-v-cf1858b3]:before,.menu+.social-links[data-v-cf1858b3]:before,.translations+.appearance[data-v-cf1858b3]:before,.appearance+.social-links[data-v-cf1858b3]:before{margin-right:8px;margin-left:8px;width:1px;height:24px;background-color:var(--vp-c-divider);content:""}.menu+.appearance[data-v-cf1858b3]:before,.translations+.appearance[data-v-cf1858b3]:before{margin-right:16px}.appearance+.social-links[data-v-cf1858b3]:before{margin-left:16px}.social-links[data-v-cf1858b3]{margin-right:-8px}@media (min-width: 960px){.VPNavBar.has-sidebar .curtain[data-v-cf1858b3]{position:absolute;right:0;bottom:-31px;width:calc(100% - var(--vp-sidebar-width));height:32px}.VPNavBar.has-sidebar .curtain[data-v-cf1858b3]:before{display:block;width:100%;height:32px;background:linear-gradient(var(--vp-c-bg),transparent 70%);content:""}}@media (min-width: 1440px){.VPNavBar.has-sidebar .curtain[data-v-cf1858b3]{width:calc(100% - ((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width)))}}.VPNavScreenMenuLink[data-v-067d5c32]{display:block;border-bottom:1px solid var(--vp-c-divider);padding:12px 0 11px;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:border-color .25s,color .25s}.VPNavScreenMenuLink[data-v-067d5c32]:hover{color:var(--vp-c-brand)}.VPNavScreenMenuGroupLink[data-v-7f48e5dc]{display:block;margin-left:12px;line-height:32px;font-size:14px;font-weight:400;color:var(--vp-c-text-1);transition:color .25s}.VPNavScreenMenuGroupLink[data-v-7f48e5dc]:hover{color:var(--vp-c-brand)}.VPNavScreenMenuGroupSection[data-v-5def7a83]{display:block}.title[data-v-5def7a83]{line-height:32px;font-size:13px;font-weight:700;color:var(--vp-c-text-2);transition:color .25s}.VPNavScreenMenuGroup[data-v-90941ff9]{border-bottom:1px solid var(--vp-c-divider);height:48px;overflow:hidden;transition:border-color .5s}.VPNavScreenMenuGroup .items[data-v-90941ff9]{visibility:hidden}.VPNavScreenMenuGroup.open .items[data-v-90941ff9]{visibility:visible}.VPNavScreenMenuGroup.open[data-v-90941ff9]{padding-bottom:10px;height:auto}.VPNavScreenMenuGroup.open .button[data-v-90941ff9]{padding-bottom:6px;color:var(--vp-c-brand)}.VPNavScreenMenuGroup.open .button-icon[data-v-90941ff9]{transform:rotate(45deg)}.button[data-v-90941ff9]{display:flex;justify-content:space-between;align-items:center;padding:12px 4px 11px 0;width:100%;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.button[data-v-90941ff9]:hover{color:var(--vp-c-brand)}.button-icon[data-v-90941ff9]{width:14px;height:14px;fill:var(--vp-c-text-2);transition:fill .5s,transform .25s}.group[data-v-90941ff9]:first-child{padding-top:0}.group+.group[data-v-90941ff9],.group+.item[data-v-90941ff9]{padding-top:4px}.VPNavScreenAppearance[data-v-a4ad84ce]{display:flex;justify-content:space-between;align-items:center;border-radius:8px;padding:12px 14px 12px 16px;background-color:var(--vp-c-bg-soft)}.text[data-v-a4ad84ce]{line-height:24px;font-size:12px;font-weight:500;color:var(--vp-c-text-2)}.VPNavScreenTranslations[data-v-bd8c50c0]{height:24px;overflow:hidden}.VPNavScreenTranslations.open[data-v-bd8c50c0]{height:auto}.title[data-v-bd8c50c0]{display:flex;align-items:center;font-size:14px;font-weight:500;color:var(--vp-c-text-1)}.icon[data-v-bd8c50c0]{width:16px;height:16px;fill:currentColor}.icon.lang[data-v-bd8c50c0]{margin-right:8px}.icon.chevron[data-v-bd8c50c0]{margin-left:4px}.list[data-v-bd8c50c0]{padding:4px 0 0 24px}.link[data-v-bd8c50c0]{line-height:32px;font-size:13px;color:var(--vp-c-text-1)}.VPNavScreen[data-v-b67951f6]{position:fixed;top:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 1px);right:0;bottom:0;left:0;padding:0 32px;width:100%;background-color:var(--vp-nav-screen-bg-color);overflow-y:auto;transition:background-color .5s;pointer-events:auto}.VPNavScreen.fade-enter-active[data-v-b67951f6],.VPNavScreen.fade-leave-active[data-v-b67951f6]{transition:opacity .25s}.VPNavScreen.fade-enter-active .container[data-v-b67951f6],.VPNavScreen.fade-leave-active .container[data-v-b67951f6]{transition:transform .25s ease}.VPNavScreen.fade-enter-from[data-v-b67951f6],.VPNavScreen.fade-leave-to[data-v-b67951f6]{opacity:0}.VPNavScreen.fade-enter-from .container[data-v-b67951f6],.VPNavScreen.fade-leave-to .container[data-v-b67951f6]{transform:translateY(-8px)}@media (min-width: 768px){.VPNavScreen[data-v-b67951f6]{display:none}}.container[data-v-b67951f6]{margin:0 auto;padding:24px 0 96px;max-width:288px}.menu+.translations[data-v-b67951f6],.menu+.appearance[data-v-b67951f6],.translations+.appearance[data-v-b67951f6]{margin-top:24px}.menu+.social-links[data-v-b67951f6]{margin-top:16px}.appearance+.social-links[data-v-b67951f6]{margin-top:16px}.VPNav[data-v-640c0ee4]{position:relative;top:var(--vp-layout-top-height, 0px);left:0;z-index:var(--vp-z-index-nav);width:100%;pointer-events:none;transition:background-color .5s}@media (min-width: 960px){.VPNav[data-v-640c0ee4]{position:fixed}}.VPSidebarItem.level-0[data-v-db75407b]{padding-bottom:24px}.VPSidebarItem.collapsed.level-0[data-v-db75407b]{padding-bottom:10px}.item[data-v-db75407b]{position:relative;display:flex;width:100%}.VPSidebarItem.collapsible>.item[data-v-db75407b]{cursor:pointer}.indicator[data-v-db75407b]{position:absolute;top:6px;bottom:6px;left:-17px;width:1px;transition:background-color .25s}.VPSidebarItem.level-2.is-active>.item>.indicator[data-v-db75407b],.VPSidebarItem.level-3.is-active>.item>.indicator[data-v-db75407b],.VPSidebarItem.level-4.is-active>.item>.indicator[data-v-db75407b],.VPSidebarItem.level-5.is-active>.item>.indicator[data-v-db75407b]{background-color:var(--vp-c-brand)}.link[data-v-db75407b]{display:flex;align-items:center;flex-grow:1}.text[data-v-db75407b]{flex-grow:1;padding:4px 0;line-height:24px;font-size:14px;transition:color .25s}.VPSidebarItem.level-0 .text[data-v-db75407b]{font-weight:700;color:var(--vp-c-text-1)}.VPSidebarItem.level-1 .text[data-v-db75407b],.VPSidebarItem.level-2 .text[data-v-db75407b],.VPSidebarItem.level-3 .text[data-v-db75407b],.VPSidebarItem.level-4 .text[data-v-db75407b],.VPSidebarItem.level-5 .text[data-v-db75407b]{font-weight:500;color:var(--vp-c-text-2)}.VPSidebarItem.level-0.is-link>.item>.link:hover .text[data-v-db75407b],.VPSidebarItem.level-1.is-link>.item>.link:hover .text[data-v-db75407b],.VPSidebarItem.level-2.is-link>.item>.link:hover .text[data-v-db75407b],.VPSidebarItem.level-3.is-link>.item>.link:hover .text[data-v-db75407b],.VPSidebarItem.level-4.is-link>.item>.link:hover .text[data-v-db75407b],.VPSidebarItem.level-5.is-link>.item>.link:hover .text[data-v-db75407b]{color:var(--vp-c-brand)}.VPSidebarItem.level-0.has-active>.item>.link>.text[data-v-db75407b],.VPSidebarItem.level-1.has-active>.item>.link>.text[data-v-db75407b],.VPSidebarItem.level-2.has-active>.item>.link>.text[data-v-db75407b],.VPSidebarItem.level-3.has-active>.item>.link>.text[data-v-db75407b],.VPSidebarItem.level-4.has-active>.item>.link>.text[data-v-db75407b],.VPSidebarItem.level-5.has-active>.item>.link>.text[data-v-db75407b]{color:var(--vp-c-text-1)}.VPSidebarItem.level-0.is-active>.item .link>.text[data-v-db75407b],.VPSidebarItem.level-1.is-active>.item .link>.text[data-v-db75407b],.VPSidebarItem.level-2.is-active>.item .link>.text[data-v-db75407b],.VPSidebarItem.level-3.is-active>.item .link>.text[data-v-db75407b],.VPSidebarItem.level-4.is-active>.item .link>.text[data-v-db75407b],.VPSidebarItem.level-5.is-active>.item .link>.text[data-v-db75407b]{color:var(--vp-c-brand)}.caret[data-v-db75407b]{display:flex;justify-content:center;align-items:center;margin-right:-7px;width:32px;height:32px;color:var(--vp-c-text-3);cursor:pointer;transition:color .25s;flex-shrink:0}.item:hover .caret[data-v-db75407b]{color:var(--vp-c-text-2)}.item:hover .caret[data-v-db75407b]:hover{color:var(--vp-c-text-1)}.caret-icon[data-v-db75407b]{width:18px;height:18px;fill:currentColor;transform:rotate(90deg);transition:transform .25s}.VPSidebarItem.collapsed .caret-icon[data-v-db75407b]{transform:rotate(0)}.VPSidebarItem.level-1 .items[data-v-db75407b],.VPSidebarItem.level-2 .items[data-v-db75407b],.VPSidebarItem.level-3 .items[data-v-db75407b],.VPSidebarItem.level-4 .items[data-v-db75407b],.VPSidebarItem.level-5 .items[data-v-db75407b]{border-left:1px solid var(--vp-c-divider);padding-left:16px}.VPSidebarItem.collapsed .items[data-v-db75407b]{display:none}.VPSidebar[data-v-4489ee23]{position:fixed;top:var(--vp-layout-top-height, 0px);bottom:0;left:0;z-index:var(--vp-z-index-sidebar);padding:32px 32px 96px;width:calc(100vw - 64px);max-width:320px;background-color:var(--vp-sidebar-bg-color);opacity:0;box-shadow:var(--vp-c-shadow-3);overflow-x:hidden;overflow-y:auto;transform:translate(-100%);transition:opacity .5s,transform .25s ease;overscroll-behavior:contain}.VPSidebar.open[data-v-4489ee23]{opacity:1;visibility:visible;transform:translate(0);transition:opacity .25s,transform .5s cubic-bezier(.19,1,.22,1)}.dark .VPSidebar[data-v-4489ee23]{box-shadow:var(--vp-shadow-1)}@media (min-width: 960px){.VPSidebar[data-v-4489ee23]{z-index:1;padding-top:var(--vp-nav-height);padding-bottom:128px;width:var(--vp-sidebar-width);max-width:100%;background-color:var(--vp-sidebar-bg-color);opacity:1;visibility:visible;box-shadow:none;transform:translate(0)}}@media (min-width: 1440px){.VPSidebar[data-v-4489ee23]{padding-left:max(32px,calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));width:calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px)}}@media (min-width: 960px){.curtain[data-v-4489ee23]{position:sticky;top:-64px;left:0;z-index:1;margin-top:calc(var(--vp-nav-height) * -1);margin-right:-32px;margin-left:-32px;height:var(--vp-nav-height);background-color:var(--vp-sidebar-bg-color)}}.nav[data-v-4489ee23]{outline:0}.group+.group[data-v-4489ee23]{border-top:1px solid var(--vp-c-divider);padding-top:10px}@media (min-width: 960px){.group[data-v-4489ee23]{padding-top:10px;width:calc(var(--vp-sidebar-width) - 64px)}}.VPSkipLink[data-v-9fa85018]{top:8px;left:8px;padding:8px 16px;z-index:999;border-radius:8px;font-size:12px;font-weight:700;text-decoration:none;color:var(--vp-c-brand);box-shadow:var(--vp-shadow-3);background-color:var(--vp-c-bg)}.VPSkipLink[data-v-9fa85018]:focus{height:auto;width:auto;clip:auto;clip-path:none}@media (min-width: 1280px){.VPSkipLink[data-v-9fa85018]{top:14px;left:16px}}.Layout[data-v-1f16208e]{display:flex;flex-direction:column;min-height:100vh}.VPHomeSponsors[data-v-6c7fa9e1]{border-top:1px solid var(--vp-c-gutter);padding:88px 24px 96px;background-color:var(--vp-c-bg)}.container[data-v-6c7fa9e1]{margin:0 auto;max-width:1152px}.love[data-v-6c7fa9e1]{margin:0 auto;width:28px;height:28px;color:var(--vp-c-text-3)}.icon[data-v-6c7fa9e1]{width:28px;height:28px;fill:currentColor}.message[data-v-6c7fa9e1]{margin:0 auto;padding-top:10px;max-width:320px;text-align:center;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}.sponsors[data-v-6c7fa9e1]{padding-top:32px}.action[data-v-6c7fa9e1]{padding-top:40px;text-align:center}.VPTeamPage[data-v-a1dbfdab]{padding-bottom:96px}@media (min-width: 768px){.VPTeamPage[data-v-a1dbfdab]{padding-bottom:128px}}.VPTeamPageSection+.VPTeamPageSection[data-v-a1dbfdab-s],.VPTeamMembers+.VPTeamPageSection[data-v-a1dbfdab-s]{margin-top:64px}.VPTeamMembers+.VPTeamMembers[data-v-a1dbfdab-s]{margin-top:24px}@media (min-width: 768px){.VPTeamPageTitle+.VPTeamPageSection[data-v-a1dbfdab-s]{margin-top:16px}.VPTeamPageSection+.VPTeamPageSection[data-v-a1dbfdab-s],.VPTeamMembers+.VPTeamPageSection[data-v-a1dbfdab-s]{margin-top:96px}}.VPTeamMembers[data-v-a1dbfdab-s]{padding:0 24px}@media (min-width: 768px){.VPTeamMembers[data-v-a1dbfdab-s]{padding:0 48px}}@media (min-width: 960px){.VPTeamMembers[data-v-a1dbfdab-s]{padding:0 64px}}.VPTeamPageTitle[data-v-4231b9dc]{padding:48px 32px;text-align:center}@media (min-width: 768px){.VPTeamPageTitle[data-v-4231b9dc]{padding:64px 48px 48px}}@media (min-width: 960px){.VPTeamPageTitle[data-v-4231b9dc]{padding:80px 64px 48px}}.title[data-v-4231b9dc]{letter-spacing:0;line-height:44px;font-size:36px;font-weight:500}@media (min-width: 768px){.title[data-v-4231b9dc]{letter-spacing:-.5px;line-height:56px;font-size:48px}}.lead[data-v-4231b9dc]{margin:0 auto;max-width:512px;padding-top:12px;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}@media (min-width: 768px){.lead[data-v-4231b9dc]{max-width:592px;letter-spacing:.15px;line-height:28px;font-size:20px}}.VPTeamPageSection[data-v-b1face36]{padding:0 32px}@media (min-width: 768px){.VPTeamPageSection[data-v-b1face36]{padding:0 48px}}@media (min-width: 960px){.VPTeamPageSection[data-v-b1face36]{padding:0 64px}}.title[data-v-b1face36]{position:relative;margin:0 auto;max-width:1152px;text-align:center;color:var(--vp-c-text-2)}.title-line[data-v-b1face36]{position:absolute;top:16px;left:0;width:100%;height:1px;background-color:var(--vp-c-divider)}.title-text[data-v-b1face36]{position:relative;display:inline-block;padding:0 24px;letter-spacing:0;line-height:32px;font-size:20px;font-weight:500;background-color:var(--vp-c-bg)}.lead[data-v-b1face36]{margin:0 auto;max-width:480px;padding-top:12px;text-align:center;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}.members[data-v-b1face36]{padding-top:40px}.VPTeamMembersItem[data-v-125054c1]{display:flex;flex-direction:column;gap:2px;border-radius:12px;width:100%;height:100%;overflow:hidden}.VPTeamMembersItem.small .profile[data-v-125054c1]{padding:32px}.VPTeamMembersItem.small .data[data-v-125054c1]{padding-top:20px}.VPTeamMembersItem.small .avatar[data-v-125054c1]{width:64px;height:64px}.VPTeamMembersItem.small .name[data-v-125054c1]{line-height:24px;font-size:16px}.VPTeamMembersItem.small .affiliation[data-v-125054c1]{padding-top:4px;line-height:20px;font-size:14px}.VPTeamMembersItem.small .desc[data-v-125054c1]{padding-top:12px;line-height:20px;font-size:14px}.VPTeamMembersItem.small .links[data-v-125054c1]{margin:0 -16px -20px;padding:10px 0 0}.VPTeamMembersItem.medium .profile[data-v-125054c1]{padding:48px 32px}.VPTeamMembersItem.medium .data[data-v-125054c1]{padding-top:24px;text-align:center}.VPTeamMembersItem.medium .avatar[data-v-125054c1]{width:96px;height:96px}.VPTeamMembersItem.medium .name[data-v-125054c1]{letter-spacing:.15px;line-height:28px;font-size:20px}.VPTeamMembersItem.medium .affiliation[data-v-125054c1]{padding-top:4px;font-size:16px}.VPTeamMembersItem.medium .desc[data-v-125054c1]{padding-top:16px;max-width:288px;font-size:16px}.VPTeamMembersItem.medium .links[data-v-125054c1]{margin:0 -16px -12px;padding:16px 12px 0}.profile[data-v-125054c1]{flex-grow:1;background-color:var(--vp-c-bg-soft)}.data[data-v-125054c1]{text-align:center}.avatar[data-v-125054c1]{position:relative;flex-shrink:0;margin:0 auto;border-radius:50%;box-shadow:var(--vp-shadow-3)}.avatar-img[data-v-125054c1]{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:50%;object-fit:cover}.name[data-v-125054c1]{margin:0;font-weight:600}.affiliation[data-v-125054c1]{margin:0;font-weight:500;color:var(--vp-c-text-2)}.org.link[data-v-125054c1]{color:var(--vp-c-text-2);transition:color .25s}.org.link[data-v-125054c1]:hover{color:var(--vp-c-brand)}.desc[data-v-125054c1]{margin:0 auto}.desc[data-v-125054c1] a{font-weight:500;color:var(--vp-c-brand);text-decoration-style:dotted;transition:color .25s}.links[data-v-125054c1]{display:flex;justify-content:center;height:56px}.sp-link[data-v-125054c1]{display:flex;justify-content:center;align-items:center;text-align:center;padding:16px;font-size:14px;font-weight:500;color:var(--vp-c-sponsor);background-color:var(--vp-c-bg-soft);transition:color .25s,background-color .25s}.sp .sp-link.link[data-v-125054c1]:hover,.sp .sp-link.link[data-v-125054c1]:focus{outline:none;color:var(--vp-c-white);background-color:var(--vp-c-sponsor)}.sp-icon[data-v-125054c1]{margin-right:8px;width:16px;height:16px;fill:currentColor}.VPTeamMembers.small .container[data-v-d11b4b54]{grid-template-columns:repeat(auto-fit,minmax(224px,1fr))}.VPTeamMembers.small.count-1 .container[data-v-d11b4b54]{max-width:276px}.VPTeamMembers.small.count-2 .container[data-v-d11b4b54]{max-width:576px}.VPTeamMembers.small.count-3 .container[data-v-d11b4b54]{max-width:876px}.VPTeamMembers.medium .container[data-v-d11b4b54]{grid-template-columns:repeat(auto-fit,minmax(256px,1fr))}@media (min-width: 375px){.VPTeamMembers.medium .container[data-v-d11b4b54]{grid-template-columns:repeat(auto-fit,minmax(288px,1fr))}}.VPTeamMembers.medium.count-1 .container[data-v-d11b4b54]{max-width:368px}.VPTeamMembers.medium.count-2 .container[data-v-d11b4b54]{max-width:760px}.container[data-v-d11b4b54]{display:grid;gap:24px;margin:0 auto;max-width:1152px}@font-face{font-family:octicons-link;src:url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format("woff")}.markdown-body{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;color:#24292e;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol;font-size:16px;line-height:1.5;word-wrap:break-word}.markdown-body .pl-c{color:#6a737d}.markdown-body .pl-c1,.markdown-body .pl-s .pl-v{color:#005cc5}.markdown-body .pl-e,.markdown-body .pl-en{color:#6f42c1}.markdown-body .pl-smi,.markdown-body .pl-s .pl-s1{color:#24292e}.markdown-body .pl-ent{color:#22863a}.markdown-body .pl-k{color:#d73a49}.markdown-body .pl-s,.markdown-body .pl-pds,.markdown-body .pl-s .pl-pse .pl-s1,.markdown-body .pl-sr,.markdown-body .pl-sr .pl-cce,.markdown-body .pl-sr .pl-sre,.markdown-body .pl-sr .pl-sra{color:#032f62}.markdown-body .pl-v,.markdown-body .pl-smw{color:#e36209}.markdown-body .pl-bu{color:#b31d28}.markdown-body .pl-ii{color:#fafbfc;background-color:#b31d28}.markdown-body .pl-c2{color:#fafbfc;background-color:#d73a49}.markdown-body .pl-c2:before{content:"^M"}.markdown-body .pl-sr .pl-cce{font-weight:700;color:#22863a}.markdown-body .pl-ml{color:#735c0f}.markdown-body .pl-mh,.markdown-body .pl-mh .pl-en,.markdown-body .pl-ms{font-weight:700;color:#005cc5}.markdown-body .pl-mi{font-style:italic;color:#24292e}.markdown-body .pl-mb{font-weight:700;color:#24292e}.markdown-body .pl-md{color:#b31d28;background-color:#ffeef0}.markdown-body .pl-mi1{color:#22863a;background-color:#f0fff4}.markdown-body .pl-mc{color:#e36209;background-color:#ffebda}.markdown-body .pl-mi2{color:#f6f8fa;background-color:#005cc5}.markdown-body .pl-mdr{font-weight:700;color:#6f42c1}.markdown-body .pl-ba{color:#586069}.markdown-body .pl-sg{color:#959da5}.markdown-body .pl-corl{text-decoration:underline;color:#032f62}.markdown-body .octicon{display:inline-block;vertical-align:text-top;fill:currentColor}.markdown-body a{background-color:transparent;-webkit-text-decoration-skip:objects}.markdown-body a:active,.markdown-body a:hover{outline-width:0}.markdown-body strong{font-weight:inherit}.markdown-body strong{font-weight:bolder}.markdown-body h1{font-size:2em;margin:.67em 0}.markdown-body img{border-style:none}.markdown-body svg:not(:root){overflow:hidden}.markdown-body code,.markdown-body kbd,.markdown-body pre{font-family:monospace,monospace;font-size:1em}.markdown-body hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}.markdown-body input{font:inherit;margin:0}.markdown-body input{overflow:visible}.markdown-body [type=checkbox]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}.markdown-body *{-webkit-box-sizing:border-box;box-sizing:border-box}.markdown-body input{font-family:inherit;font-size:inherit;line-height:inherit}.markdown-body a{color:#0366d6;text-decoration:none}.markdown-body a:hover{text-decoration:underline}.markdown-body strong{font-weight:600}.markdown-body hr{height:0;margin:15px 0;overflow:hidden;background:transparent;border:0;border-bottom:1px solid #dfe2e5}.markdown-body hr:before{display:table;content:""}.markdown-body hr:after{display:table;clear:both;content:""}.markdown-body table{border-spacing:0;border-collapse:collapse}.markdown-body td,.markdown-body th{padding:0}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:0;margin-bottom:0}.markdown-body h1{font-size:32px;font-weight:600}.markdown-body h2{font-size:24px;font-weight:600}.markdown-body h3{font-size:20px;font-weight:600}.markdown-body h4{font-size:16px;font-weight:600}.markdown-body h5{font-size:14px;font-weight:600}.markdown-body h6{font-size:12px;font-weight:600}.markdown-body p{margin-top:0;margin-bottom:10px}.markdown-body blockquote{margin:0}.markdown-body ul,.markdown-body ol{padding-left:0;margin-top:0;margin-bottom:0}.markdown-body ol ol,.markdown-body ul ol{list-style-type:lower-roman}.markdown-body ul ul ol,.markdown-body ul ol ol,.markdown-body ol ul ol,.markdown-body ol ol ol{list-style-type:lower-alpha}.markdown-body dd{margin-left:0}.markdown-body code{font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;font-size:12px}.markdown-body pre{margin-top:0;margin-bottom:0;font:12px SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}.markdown-body .octicon{vertical-align:text-bottom}.markdown-body .pl-0{padding-left:0!important}.markdown-body .pl-1{padding-left:4px!important}.markdown-body .pl-2{padding-left:8px!important}.markdown-body .pl-3{padding-left:16px!important}.markdown-body .pl-4{padding-left:24px!important}.markdown-body .pl-5{padding-left:32px!important}.markdown-body .pl-6{padding-left:40px!important}.markdown-body:before{display:table;content:""}.markdown-body:after{display:table;clear:both;content:""}.markdown-body>*:first-child{margin-top:0!important}.markdown-body>*:last-child{margin-bottom:0!important}.markdown-body a:not([href]){color:inherit;text-decoration:none}.markdown-body .anchor{float:left;padding-right:4px;margin-left:-20px;line-height:1}.markdown-body .anchor:focus{outline:none}.markdown-body p,.markdown-body blockquote,.markdown-body ul,.markdown-body ol,.markdown-body dl,.markdown-body table,.markdown-body pre{margin-top:0;margin-bottom:16px}.markdown-body hr{height:.25em;padding:0;margin:24px 0;background-color:#e1e4e8;border:0}.markdown-body blockquote{padding:0 1em;color:#6a737d;border-left:.25em solid #dfe2e5}.markdown-body blockquote>:first-child{margin-top:0}.markdown-body blockquote>:last-child{margin-bottom:0}.markdown-body kbd{display:inline-block;padding:3px 5px;font-size:11px;line-height:10px;color:#444d56;vertical-align:middle;background-color:#fafbfc;border:solid 1px #c6cbd1;border-bottom-color:#959da5;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 #959da5;box-shadow:inset 0 -1px #959da5}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:24px;margin-bottom:16px;font-weight:600;line-height:1.25}.markdown-body h1 .octicon-link,.markdown-body h2 .octicon-link,.markdown-body h3 .octicon-link,.markdown-body h4 .octicon-link,.markdown-body h5 .octicon-link,.markdown-body h6 .octicon-link{color:#1b1f23;vertical-align:middle;visibility:hidden}.markdown-body h1:hover .anchor,.markdown-body h2:hover .anchor,.markdown-body h3:hover .anchor,.markdown-body h4:hover .anchor,.markdown-body h5:hover .anchor,.markdown-body h6:hover .anchor{text-decoration:none}.markdown-body h1:hover .anchor .octicon-link,.markdown-body h2:hover .anchor .octicon-link,.markdown-body h3:hover .anchor .octicon-link,.markdown-body h4:hover .anchor .octicon-link,.markdown-body h5:hover .anchor .octicon-link,.markdown-body h6:hover .anchor .octicon-link{visibility:visible}.markdown-body h1{padding-bottom:.3em;font-size:2em;border-bottom:1px solid #eaecef}.markdown-body h2{padding-bottom:.3em;font-size:1.5em;border-bottom:1px solid #eaecef}.markdown-body h3{font-size:1.25em}.markdown-body h4{font-size:1em}.markdown-body h5{font-size:.875em}.markdown-body h6{font-size:.85em;color:#6a737d}.markdown-body ul,.markdown-body ol{padding-left:2em}.markdown-body ul ul,.markdown-body ul ol,.markdown-body ol ol,.markdown-body ol ul{margin-top:0;margin-bottom:0}.markdown-body li>p{margin-top:16px}.markdown-body li+li{margin-top:.25em}.markdown-body dl{padding:0}.markdown-body dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:600}.markdown-body dl dd{padding:0 16px;margin-bottom:16px}.markdown-body table{display:block;width:100%;overflow:auto}.markdown-body table th{font-weight:600}.markdown-body table th,.markdown-body table td{padding:6px 13px;border:1px solid #dfe2e5}.markdown-body table tr{background-color:#fff;border-top:1px solid #c6cbd1}.markdown-body table tr:nth-child(2n){background-color:#f6f8fa}.markdown-body img{max-width:100%;-webkit-box-sizing:content-box;box-sizing:content-box;background-color:#fff}.markdown-body code{padding:.2em 0;margin:0;font-size:85%;background-color:#1b1f230d;border-radius:3px}.markdown-body code:before,.markdown-body code:after{letter-spacing:-.2em;content:" "}.markdown-body pre{word-wrap:normal}.markdown-body pre>code{padding:0;margin:0;font-size:100%;word-break:normal;white-space:pre;background:transparent;border:0}.markdown-body .highlight{margin-bottom:16px}.markdown-body .highlight pre{margin-bottom:0;word-break:normal}.markdown-body .highlight pre,.markdown-body pre{padding:16px;overflow:auto;font-size:85%;line-height:1.45;background-color:#f6f8fa;border-radius:3px}.markdown-body pre code{display:inline;max-width:auto;padding:0;margin:0;overflow:visible;line-height:inherit;word-wrap:normal;background-color:transparent;border:0}.markdown-body pre code:before,.markdown-body pre code:after{content:normal}.markdown-body .full-commit .btn-outline:not(:disabled):hover{color:#005cc5;border-color:#005cc5}.markdown-body kbd{display:inline-block;padding:3px 5px;font:11px SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;line-height:10px;color:#444d56;vertical-align:middle;background-color:#fafbfc;border:solid 1px #d1d5da;border-bottom-color:#c6cbd1;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 #c6cbd1;box-shadow:inset 0 -1px #c6cbd1}.markdown-body :checked+.radio-label{position:relative;z-index:1;border-color:#0366d6}.markdown-body .task-list-item{list-style-type:none}.markdown-body .task-list-item+.task-list-item{margin-top:3px}.markdown-body .task-list-item input{margin:0 .2em .25em -1.6em;vertical-align:middle}.markdown-body hr{border-bottom-color:#eee}.gt-container{-webkit-box-sizing:border-box;box-sizing:border-box;font-size:16px}.gt-container *{-webkit-box-sizing:border-box;box-sizing:border-box}.gt-container a{color:#6190e8}.gt-container a:hover{color:#81a6ed;border-color:#81a6ed}.gt-container a.is--active{color:#333;cursor:default!important}.gt-container a.is--active:hover{color:#333}.gt-container .hide{display:none!important}.gt-container .gt-svg{display:inline-block;width:1em;height:1em;vertical-align:sub}.gt-container .gt-svg svg{width:100%;height:100%;fill:#6190e8}.gt-container .gt-ico{display:inline-block}.gt-container .gt-ico-text{margin-left:.3125em}.gt-container .gt-ico-github,.gt-container .gt-ico-github .gt-svg{width:100%;height:100%}.gt-container .gt-ico-github svg{fill:inherit}.gt-container .gt-spinner{position:relative}.gt-container .gt-spinner:before{content:"";-webkit-box-sizing:border-box;box-sizing:border-box;position:absolute;top:3px;width:.75em;height:.75em;margin-top:-.1875em;margin-left:-.375em;border-radius:50%;border:1px solid #fff;border-top-color:#6190e8;-webkit-animation:gt-kf-rotate .6s linear infinite;animation:gt-kf-rotate .6s linear infinite}.gt-container .gt-loader{position:relative;border:1px solid #999;-webkit-animation:ease gt-kf-rotate 1.5s infinite;animation:ease gt-kf-rotate 1.5s infinite;display:inline-block;font-style:normal;width:1.75em;height:1.75em;line-height:1.75em;border-radius:50%}.gt-container .gt-loader:before{content:"";position:absolute;display:block;top:0;left:50%;margin-top:-.1875em;margin-left:-.1875em;width:.375em;height:.375em;background-color:#999;border-radius:50%}.gt-container .gt-avatar{display:inline-block;width:3.125em;height:3.125em}@media (max-width: 479px){.gt-container .gt-avatar{width:2em;height:2em}}.gt-container .gt-avatar img{width:100%;height:auto;border-radius:3px}.gt-container .gt-avatar-github{width:3em;height:3em;cursor:pointer}@media (max-width: 479px){.gt-container .gt-avatar-github{width:1.875em;height:1.875em}}.gt-container .gt-btn{padding:.75em 1.25em;display:inline-block;line-height:1;text-decoration:none;white-space:nowrap;cursor:pointer;border:1px solid #6190e8;border-radius:5px;background-color:#6190e8;color:#fff;outline:none;font-size:.75em}.gt-container .gt-btn-text{font-weight:400}.gt-container .gt-btn-loading{position:relative;margin-left:.5em;display:inline-block;width:.75em;height:1em;vertical-align:top}.gt-container .gt-btn.is--disable{cursor:not-allowed;opacity:.5}.gt-container .gt-btn-login{margin-right:0}.gt-container .gt-btn-preview{background-color:#fff;color:#6190e8}.gt-container .gt-btn-preview:hover{background-color:#f2f2f2;border-color:#81a6ed}.gt-container .gt-btn-public:hover{background-color:#81a6ed;border-color:#81a6ed}.gt-container .gt-error{text-align:center;margin:.625em;color:#ff3860}.gt-container .gt-initing{padding:1.25em 0;text-align:center}.gt-container .gt-initing-text{margin:.625em auto;font-size:92%}.gt-container .gt-no-init{padding:1.25em 0;text-align:center}.gt-container .gt-link{border-bottom:1px dotted #6190e8}.gt-container .gt-link-counts,.gt-container .gt-link-project{text-decoration:none}.gt-container .gt-meta{margin:1.25em 0;padding:1em 0;border-bottom:1px solid #e9e9e9;font-size:1em;position:relative;z-index:10}.gt-container .gt-meta:before,.gt-container .gt-meta:after{content:" ";display:table}.gt-container .gt-meta:after{clear:both}.gt-container .gt-counts{margin:0 .625em 0 0}.gt-container .gt-user{float:right;margin:0;font-size:92%}.gt-container .gt-user-pic{width:16px;height:16px;vertical-align:top;margin-right:.5em}.gt-container .gt-user-inner{display:inline-block;cursor:pointer}.gt-container .gt-user .gt-ico{margin:0 0 0 .3125em}.gt-container .gt-user .gt-ico svg{fill:inherit}.gt-container .gt-user .is--poping .gt-ico svg{fill:#6190e8}.gt-container .gt-version{color:#a1a1a1;margin-left:.375em}.gt-container .gt-copyright{margin:0 .9375em .5em;border-top:1px solid #e9e9e9;padding-top:.5em}.gt-container .gt-popup{position:absolute;right:0;top:2.375em;background:#fff;display:inline-block;border:1px solid #e9e9e9;padding:.625em 0;font-size:.875em;letter-spacing:.5px}.gt-container .gt-popup .gt-action{cursor:pointer;display:block;margin:.5em 0;padding:0 1.125em;position:relative;text-decoration:none}.gt-container .gt-popup .gt-action.is--active:before{content:"";width:.25em;height:.25em;background:#6190e8;position:absolute;left:.5em;top:.4375em}.gt-container .gt-header{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex}.gt-container .gt-header-comment{-webkit-box-flex:1;-ms-flex:1;flex:1;margin-left:1.25em}@media (max-width: 479px){.gt-container .gt-header-comment{margin-left:.875em}}.gt-container .gt-header-textarea{padding:.75em;display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;min-height:5.125em;max-height:15em;border-radius:5px;border:1px solid rgba(0,0,0,.1);font-size:.875em;word-wrap:break-word;resize:vertical;background-color:#f6f6f6;outline:none;-webkit-transition:all .25s ease;transition:all .25s ease}.gt-container .gt-header-textarea:hover{background-color:#fbfbfb}.gt-container .gt-header-preview{padding:.75em;border-radius:5px;border:1px solid rgba(0,0,0,.1);background-color:#f6f6f6}.gt-container .gt-header-controls{position:relative;margin:.75em 0 0}.gt-container .gt-header-controls:before,.gt-container .gt-header-controls:after{content:" ";display:table}.gt-container .gt-header-controls:after{clear:both}@media (max-width: 479px){.gt-container .gt-header-controls{margin:0}}.gt-container .gt-header-controls-tip{font-size:.875em;color:#6190e8;text-decoration:none;vertical-align:sub}@media (max-width: 479px){.gt-container .gt-header-controls-tip{display:none}}.gt-container .gt-header-controls .gt-btn{float:right;margin-left:1.25em}@media (max-width: 479px){.gt-container .gt-header-controls .gt-btn{float:none;width:100%;margin:.75em 0 0}}.gt-container:after{content:"";position:fixed;bottom:100%;left:0;right:0;top:0;opacity:0}.gt-container.gt-input-focused{position:relative}.gt-container.gt-input-focused:after{content:"";position:fixed;bottom:0%;left:0;right:0;top:0;background:#000;opacity:.6;-webkit-transition:opacity .3s,bottom 0s;transition:opacity .3s,bottom 0s;z-index:9999}.gt-container.gt-input-focused .gt-header-comment{z-index:10000}.gt-container .gt-comments{padding-top:1.25em}.gt-container .gt-comments-null{text-align:center}.gt-container .gt-comments-controls{margin:1.25em 0;text-align:center}.gt-container .gt-comment{position:relative;padding:.625em 0;display:-webkit-box;display:-ms-flexbox;display:flex}.gt-container .gt-comment-content{-webkit-box-flex:1;-ms-flex:1;flex:1;margin-left:1.25em;padding:.75em 1em;background-color:#f9f9f9;overflow:auto;-webkit-transition:all ease .25s;transition:all ease .25s}.gt-container .gt-comment-content:hover{-webkit-box-shadow:0 .625em 3.75em 0 #f4f4f4;box-shadow:0 .625em 3.75em #f4f4f4}@media (max-width: 479px){.gt-container .gt-comment-content{margin-left:.875em;padding:.625em .75em}}.gt-container .gt-comment-header{margin-bottom:.5em;font-size:.875em;position:relative}.gt-container .gt-comment-block-1{float:right;height:1.375em;width:2em}.gt-container .gt-comment-block-2{float:right;height:1.375em;width:4em}.gt-container .gt-comment-username{font-weight:500;color:#6190e8;text-decoration:none}.gt-container .gt-comment-username:hover{text-decoration:underline}.gt-container .gt-comment-text,.gt-container .gt-comment-date{margin-left:.5em;color:#a1a1a1}.gt-container .gt-comment-like,.gt-container .gt-comment-edit,.gt-container .gt-comment-reply{position:absolute;height:1.375em}.gt-container .gt-comment-like:hover,.gt-container .gt-comment-edit:hover,.gt-container .gt-comment-reply:hover{cursor:pointer}.gt-container .gt-comment-like{top:0;right:2em}.gt-container .gt-comment-edit,.gt-container .gt-comment-reply{top:0;right:0}.gt-container .gt-comment-body{color:#333!important}.gt-container .gt-comment-body .email-hidden-toggle a{display:inline-block;height:12px;padding:0 9px;font-size:12px;font-weight:600;line-height:6px;color:#444d56;text-decoration:none;vertical-align:middle;background:#dfe2e5;border-radius:1px}.gt-container .gt-comment-body .email-hidden-toggle a:hover{background-color:#c6cbd1}.gt-container .gt-comment-body .email-hidden-reply{display:none;white-space:pre-wrap}.gt-container .gt-comment-body .email-hidden-reply .email-signature-reply{padding:0 15px;margin:15px 0;color:#586069;border-left:4px solid #dfe2e5}.gt-container .gt-comment-body .email-hidden-reply.expanded{display:block}.gt-container .gt-comment-admin .gt-comment-content{background-color:#f6f9fe}@-webkit-keyframes gt-kf-rotate{0%{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes gt-kf-rotate{0%{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.gitalk-container[data-v-c2acb4b7] .gt-container a{color:var(--c-brand)!important}.gitalk-container[data-v-c2acb4b7] .gt-container .gt-header-textarea{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;background-color:var(--c-bg-light)}.gitalk-container[data-v-c2acb4b7] .gt-container .gt-svg svg{fill:var(--c-brand)}.gitalk-container[data-v-c2acb4b7] .gt-container .gt-link{border-color:var(--c-brand)}.gitalk-container[data-v-c2acb4b7] .gt-container .gt-btn{background-color:var(--c-brand);border-color:var(--c-brand)}.gitalk-container[data-v-c2acb4b7] .gt-container .gt-btn.gt-btn-preview{color:var(--c-brand);background-color:var(--c-bg)}.gitalk-container[data-v-c2acb4b7] .gt-container .gt-comments .gt-comments-null{color:var(--c-text)}.gitalk-container[data-v-c2acb4b7] .gt-container .gt-comment-content{background-color:var(--c-bg-light);box-shadow:none}.gitalk-container[data-v-c2acb4b7] .gt-container .gt-comment-content .gt-comment-body{color:var(--c-text)!important}.gitalk-container[data-v-c2acb4b7] .gt-container .gt-comment-content blockquote{color:var(--c-text-quote)!important;border:0;background-color:var(--c-bg-lighter);padding:1em;margin-top:1em!important}.gitalk-container[data-v-c2acb4b7] .gt-container .gt-avatar{border-radius:50%;overflow:hidden}.gitalk-container[data-v-c2acb4b7] .gt-container .gt-avatar img{height:100%;object-fit:cover}.img-wrap[data-v-04203bbc]{display:flex;flex-direction:column;flex-shrink:0;align-items:center;width:100%;margin:20px 0}.img-wrap .img[data-v-04203bbc]{width:auto;height:auto;max-width:100%;border-radius:4px}.img-wrap .desc[data-v-04203bbc]{font-size:14px;color:#999;margin:10px 0 0}:root{scroll-behavior:smooth}body{overflow-x:hidden}.vp-doc p,.vp-doc li+li{text-align:justify} diff --git a/assets/typescript.md.e6e7bfb1.js b/assets/typescript.md.e6e7bfb1.js new file mode 100644 index 00000000..c1506400 --- /dev/null +++ b/assets/typescript.md.e6e7bfb1.js @@ -0,0 +1,677 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{v as y,b as D,t as a,O as e,F as s,L as n,X as F,R as p,M as l}from"./chunks/framework.0d8bea05.js";const C=p('

快速上手 TypeScript

如果已经看完 工程化的起步准备 一章,相信此时的已经对 Node 工程项目有了足够的认识了,在此之前的所有代码都是使用 JavaScript 编写的,接下来这一节,将开始介绍 TypeScript ,这是一门新的语言,但是上手非常简单。

TypeScript 简称 TS ,既是一门新语言,也是 JS 的一个超集,它是在 JavaScript 的基础上增加了一套类型系统,它支持所有的 JS 语句,为工程化开发而生,最终在编译的时候去掉类型和特有的语法,生成 JS 代码。

虽然带有类型系统的前端语言不止 TypeScript (例如 Facebook 推出的 Flow.js ),但从目前整个 开源社区的流行趋势 看, TypeScript 无疑是更好的选择。

',4),A=p(`

而且只要本身已经学会了 JS ,并且经历过很多协作类的项目,那么使用 TS 编程是一个很自然而然的过程。

为什么需要类型系统

要想知道自己为什么要用 TypeScript ,得先从 JavaScript 有什么不足说起,举一个非常小的例子:

js
function getFirstWord(msg) {
+  console.log(msg.split(' ')[0])
+}
+
+getFirstWord('Hello World') // 输出 Hello
+
+getFirstWord(123) // TypeError: msg.split is not a function

这里定义了一个用空格切割字符串的方法,并打印出第一个单词:

  1. 第一次执行时,字符串支持 split 方法,所以成功获取到了第一个单词 Hello
  2. 第二次执行时,由于数值不存在 split 方法,所以传入 123 引起了程序崩溃

这就是 JavaScript 的弊端,过于灵活,没有类型的约束,很容易因为类型的变化导致一些本可避免的 BUG 出现,而且这些 BUG 通常需要在程序运行的时候才会被发现,很容易引发生产事故。

虽然可以在执行 split 方法之前执行一层判断或者转换,但很明显增加了很多工作量。

TypeScript 的出现,在编译的时候就可以执行检查来避免掉这些问题,而且配合 VSCode 等编辑器的智能提示,可以很方便的知道每个变量对应的类型。

Hello TypeScript

将继续使用 Hello Node 这个 demo ,或者可以再建一个新 demo ,依然是在 src 文件夹下,创建一个 ts 文件夹归类本次的测试文件,然后创建一个 index.ts 文件在 ts 文件夹下。

TIP

TypeScript 语言对应的文件扩展名是 .ts

然后在命令行通过 cd 命令进入项目所在的目录路径,安装 TypeScript 开发的两个主要依赖包:

  1. typescript 这个包是用 TypeScript 编程的语言依赖包

  2. ts-node 是让 Node 可以运行 TypeScript 的执行环境

bash
npm install -D typescript ts-node

这次添加了一个 -D 参数,因为 TypeScript 和 TS-Node 是开发过程中使用的依赖,所以将其添加到 package.json 的 devDependencies 字段里。

然后修改 scripts 字段,增加一个 dev:ts 的 script :

json
{
+  "name": "hello-node",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "dev:cjs": "node src/cjs/index.cjs",
+    "dev:esm": "node src/esm/index.mjs",
+    "dev:ts": "ts-node src/ts/index.ts",
+    "compile": "babel src/babel --out-dir compiled",
+    "serve": "node server/index.js"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "md5": "^2.3.0"
+  },
+  "devDependencies": {
+    "ts-node": "^10.7.0",
+    "typescript": "^4.6.3"
+  }
+}

准备工作完毕!

TIP

请注意, dev:ts 这个 script 是用了 ts-node 来代替原来在用的 node ,因为使用 node 无法识别 TypeScript 语言。

为什么需要类型系统 里面提到的例子放到 src/ts/index.ts 里:

ts
// src/ts/index.ts
+function getFirstWord(msg) {
+  console.log(msg.split(' ')[0])
+}
+
+getFirstWord('Hello World')
+
+getFirstWord(123)

然后在命令行运行 npm run dev:ts 来看看这次的结果:

bash
TSError:  Unable to compile TypeScript:
+src/ts/index.ts:1:23 - error TS7006: Parameter 'msg' implicitly has an 'any' type.
+
+1 function getFirstWord(msg) {
+                        ~~~

这是告知 getFirstWord 的入参 msg 带有隐式 any 类型,这个时候可能还不了解 any 代表什么意思,没关系,来看下如何修正这段代码:

ts
// src/ts/index.ts
+function getFirstWord(msg: string) {
+  console.log(msg.split(' ')[0])
+}
+
+getFirstWord('Hello World')
+
+getFirstWord(123)

留意到没有,现在函数的入参 msg 已经变成了 msg: string ,这是 TypeScript 指定参数为字符串类型的一个写法。

现在再运行 npm run dev:ts ,上一个错误提示已经不再出现,取而代之的是一个新的报错:

bash
TSError:  Unable to compile TypeScript:
+src/ts/index.ts:7:14 - error TS2345:
+Argument of type 'number' is not assignable to parameter of type 'string'.
+
+7 getFirstWord(123)
+               ~~~

这次的报错代码是在 getFirstWord(123) 这里,告诉 number 类型的数据不能分配给 string 类型的参数,也就是故意传入一个会报错的数值进去,被 TypeScript 检查出来了!

可以再仔细留意一下控制台的信息,会发现没有报错的 getFirstWord('Hello World') 也没有打印出结果,这是因为 TypeScript 需要先被编译成 JavaScript ,然后再执行。

这个机制让有问题的代码能够被及早发现,一旦代码出现问题,编译阶段就会失败。

移除会报错的那行代码,只保留如下:

ts
// src/ts/index.ts
+function getFirstWord(msg: string) {
+  console.log(msg.split(' ')[0])
+}
+
+getFirstWord('Hello World')

再次运行 npm run dev:ts ,这次完美运行!

ts
npm run dev:ts
+
+> demo@1.0.0 dev:ts
+> ts-node src/ts/index.ts
+
+Hello

在这个例子里,相信已经感受到 TypeScript 的魅力了!接下来来认识一下不同的 JavaScript 类型,在 TypeScript 里面应该如何定义。

常用的 TS 类型定义

Hello TypeScript 的体验中,相信能够感受到 TypeScript 编程带来的好处了,代码的健壮性得到了大大的提升!

并且应该也能够深刻理解,TypeScript 的类型系统并不会给编程带来极高的门槛或者说开发障碍,这是以非常小的成本换取巨大收益的举措。

TIP

如果还没有体验这个 demo ,建议先按教程跑一下,然后来讲解不同的 JavaScript 类型应该如何在 TypeScript 里定义,接下来的时间里,可以一边看,一边在 demo 里实践。

原始数据类型

原始数据类型 是一种既非对象也无方法的数据,刚才演示代码里,函数的入参使用的字符串 String 就是原始数据类型之一。

除了 String ,另外还有数值 Number 、布尔值 Boolean 等等,它们在 TypeScript 都有统一的表达方式,列个表格对比,能够更直观的了解:

原始数据类型JavaScriptTypeScript
字符串Stringstring
数值Numbernumber
布尔值Booleanboolean
大整数BigIntbigint
符号Symbolsymbol
不存在Nullnull
未定义Undefinedundefined

有没有发现窍门?对! TypeScript 对原始数据的类型定义真的是超级简单,就是转为全小写即可!

举几个例子:

ts
// 字符串
+const str: string = 'Hello World'
+
+// 数值
+const num: number = 1
+
+// 布尔值
+const bool: boolean = true

不过在实际的编程过程中,原始数据类型的类型定义是可以省略的,因为 TypeScript 会根据声明变量时赋值的类型,自动推导变量类型,也就是可以跟平时写 JavaScript 一样:

ts
// 这样也不会报错,因为 TS 会推导它们的类型
+const str = 'Hello World'
+const num = 1
+const bool = true

数组

除了原始数据类型之外, JavaScript 还有引用类型,数组 Array 就是其中的一种。

之所以先讲数组,是因为它在 TS 类型定义的写法上面,可能是最接近原始数据的一个类型了,为什么这么说?还是列个表格,来看一下如何定义数组:

数组里的数据类型写法 1类型写法 2
字符串string[]Array<string>
数值number[]Array<number>
布尔值boolean[]Array<boolean>
大整数bigint[]Array<bigint>
符号symbol[]Array<symbol>
不存在null[]Array<null>
未定义undefined[]Array<undefined>

是吧!就只是在原始数据类型的基础上变化了一下书写格式,就成为了数组的定义!

笔者最常用的就是 string[] 这样的格式,只需要追加一个方括号 [] ,另外一种写法是基于 TS 的泛型 Array<T> ,两种方式定义出来的类型其实是一样的。

举几个例子:

ts
// 字符串数组
+const strs: string[] = ['Hello World', 'Hi World']
+
+// 数值数组
+const nums: number[] = [1, 2, 3]
+
+// 布尔值数组
+const bools: boolean[] = [true, true, false]

在实际的编程过程中,如果数组一开始就有初始数据(数组长度不为 0 ),那么 TypeScript 也会根据数组里面的项目类型,正确自动帮推导这个数组的类型,这种情况下也可以省略类型定义:

ts
// 这种有初始项目的数组, TS 也会推导它们的类型
+const strs = ['Hello World', 'Hi World']
+const nums = [1, 2, 3]
+const bools = [true, true, false]

但是!如果一开始是 [] ,那么就必须显式的指定数组类型(取决于当前项目的 tsconfig.json 配置,可能会引起报错):

ts
// 这个时候会认为是 any[] 或者 never[] 类型
+const nums = []
+
+// 这个时候再 push 一个 number 数据进去,也不会使其成为 number[]
+nums.push(1)

而对于复杂的数组,比如数组里面的 item 都是对象,其实格式也是一样,只不过把原始数据类型换成 对象的类型 即可,例如 UserItem[] 表示这是一个关于用户的数组列表。

对象(接口)

看完了数组,接下来看看对象的用法,对象也是引用类型,在 数组 的最后提到了一个 UserItem[] 的写法,这里的 UserItem 就是一个对象的类型定义。

如果熟悉 JavaScript ,那么就知道对象的 “键值对” 里面的值,可能是由原始数据、数组、对象组成的,所以在 TypeScript ,类型定义也是需要根据值的类型来确定它的类型,因此定义对象的类型应该是第一个比较有门槛的地方。

如何定义对象的类型

对象的类型定义有两个语法支持: typeinterface

先看看 type 的写法:

ts
type UserItem = {
+  // ...
+}

再看看 interface 的写法:

ts
interface UserItem {
+  // ...
+}

可以看到它们表面上的区别是一个有 = 号,一个没有,事实上在一般的情况下也确实如此,两者非常接近,但是在特殊的时候也有一定的区别。

了解接口的使用

为了降低学习门槛,统一使用 interface 来做入门教学,它的写法与 Object 更为接近,事实上它也被用的更多。

对象的类型 interface 也叫做接口,用来描述对象的结构。

TIP

对象的类型定义通常采用 Upper Camel Case 大驼峰命名法,也就是每个单词的首字母大写,例如 UserItemGameDetail ,这是为了跟普通变量进行区分(变量通常使用 Lower Camel Case 小驼峰写法,也就是第一个单词的首字母小写,其他首字母大写,例如 userItem )。

这里通过一些举例来带举一反三,随时可以在 demo 里进行代码实践。

以这个用户信息为例子,比如要描述 Petter 这个用户,他的最基础信息就是姓名和年龄,那么定义为接口就是这么写:

ts
// 定义用户对象的类型
+interface UserItem {
+  name: string
+  age: number
+}
+
+// 在声明变量的时候将其关联到类型上
+const petter: UserItem = {
+  name: 'Petter',
+  age: 20,
+}

如果需要添加数组、对象等类型到属性里,按照这样继续追加即可。

可选的接口属性

注意,上面这样定义的接口类型,表示 nameage 都是必选的属性,不可以缺少,一旦缺少,代码运行起来就会报错!

src/ts/index.ts 里敲入以下代码,也就是在声明变量的时候故意缺少了 age 属性,来看看会发生什么:

ts
// 注意!这是一段会报错的代码
+
+interface UserItem {
+  name: string
+  age: number
+}
+
+const petter: UserItem = {
+  name: 'Petter',
+}

运行 npm run dev:ts ,会看到控制台给的报错信息,缺少了必选的属性 age

bash
src/ts/index.ts:6:7 - error TS2741:
+Property 'age' is missing in type '{ name: string; }'
+but required in type 'UserItem'.
+
+6 const petter: UserItem = {
+        ~~~~~~
+
+  src/ts/index.ts:3:3
+    3   age: number
+        ~~~
+    'age' is declared here.

在实际的业务中,有可能会出现一些属性并不是必须的,就像这个年龄,可以将其设置为可选属性,通过添加 ? 来定义。

请注意下面代码的第三行, age 后面紧跟了一个 ? 号再接 : 号,这是 TypeScript 对象对于可选属性的一个定义方式,这一次这段代码是可以成功运行的!

ts
interface UserItem {
+  name: string
+  // 这个属性变成了可选
+  age?: number
+}
+
+const petter: UserItem = {
+  name: 'Petter',
+}

调用自身接口的属性

如果一些属性的结构跟本身一致,也可以直接引用,比如下面例子里的 friendList 属性,用户的好友列表,它就可以继续使用 UserItem 这个接口作为数组的类型:

ts
interface UserItem {
+  name: string
+  age: number
+  enjoyFoods: string[]
+  // 这个属性引用了本身的类型
+  friendList: UserItem[]
+}
+
+const petter: UserItem = {
+  name: 'Petter',
+  age: 18,
+  enjoyFoods: ['rice', 'noodle', 'pizza'],
+  friendList: [
+    {
+      name: 'Marry',
+      age: 16,
+      enjoyFoods: ['pizza', 'ice cream'],
+      friendList: [],
+    },
+    {
+      name: 'Tom',
+      age: 20,
+      enjoyFoods: ['chicken', 'cake'],
+      friendList: [],
+    }
+  ],
+}

接口的继承

接口还可以继承,比如要对用户设置管理员,管理员信息也是一个对象,但要比普通用户多一个权限级别的属性,那么就可以使用继承,它通过 extends 来实现:

ts
interface UserItem {
+  name: string
+  age: number
+  enjoyFoods: string[]
+  friendList: UserItem[]
+}
+
+// 这里继承了 UserItem 的所有属性类型,并追加了一个权限等级属性
+interface Admin extends UserItem {
+  permissionLevel: number
+}
+
+const admin: Admin = {
+  name: 'Petter',
+  age: 18,
+  enjoyFoods: ['rice', 'noodle', 'pizza'],
+  friendList: [
+    {
+      name: 'Marry',
+      age: 16,
+      enjoyFoods: ['pizza', 'ice cream'],
+      friendList: [],
+    },
+    {
+      name: 'Tom',
+      age: 20,
+      enjoyFoods: ['chicken', 'cake'],
+      friendList: [],
+    }
+  ],
+  permissionLevel: 1,
+}

如果觉得这个 Admin 类型不需要记录这么多属性,也可以在继承的过程中舍弃某些属性,通过 Omit 帮助类型来实现,Omit 的类型如下:

ts
type Omit<T, K extends string | number | symbol>

其中 T 代表已有的一个对象类型, K 代表要删除的属性名,如果只有一个属性就直接是一个字符串,如果有多个属性,用 | 来分隔开,下面的例子就是删除了两个不需要的属性:

ts
interface UserItem {
+  name: string
+  age: number
+  enjoyFoods: string[]
+  friendList?: UserItem[]
+}
+
+// 这里在继承 UserItem 类型的时候,删除了两个多余的属性
+interface Admin extends Omit<UserItem, 'enjoyFoods' | 'friendList'> {
+  permissionLevel: number
+}
+
+// 现在的 admin 就非常精简了
+const admin: Admin = {
+  name: 'Petter',
+  age: 18,
+  permissionLevel: 1,
+}

看到这里并实际体验过的话,在业务中常见的类型定义已经难不倒了!

类是 JavaScript ES6 推出的一个概念,通过 class 关键字,可以定义一个对象的模板,如果对类还比较陌生的话,可以先阅读一下阮一峰老师的 ES6 文章:Class 的基本语法

在 TypeScript ,通过类得到的变量,它的类型就是这个类,可能这句话看起来有点难以理解,来看个例子,可以在 demo 里运行它:

ts
// 定义一个类
+class User {
+  // constructor 上的数据需要先这样定好类型
+  name: string
+
+  // 入参也要定义类型
+  constructor(userName: string) {
+    this.name = userName
+  }
+
+  getName() {
+    console.log(this.name)
+  }
+}
+
+// 通过 new 这个类得到的变量,它的类型就是这个类
+const petter: User = new User('Petter')
+petter.getName() // Petter

类与类之间可以继承:

ts
// 这是一个基础类
+class UserBase {
+  name: string
+  constructor(userName: string) {
+    this.name = userName
+  }
+}
+
+// 这是另外一个类,继承自基础类
+class User extends UserBase {
+  getName() {
+    console.log(this.name)
+  }
+}
+
+// 这个变量拥有上面两个类的所有属性和方法
+const petter: User = new User('Petter')
+petter.getName()

类也可以提供给接口去继承:

ts
// 这是一个类
+class UserBase {
+  name: string
+  constructor(userName: string) {
+    this.name = userName
+  }
+}
+
+// 这是一个接口,可以继承自类
+interface User extends UserBase {
+  age: number
+}
+
+// 这样这个变量就必须同时存在两个属性
+const petter: User = {
+  name: 'Petter',
+  age: 18,
+}

如果类上面本身有方法存在,接口在继承的时候也要相应的实现,当然也可以借助在 对象(接口) 提到的 Omit 帮助类型来去掉这些方法。

ts
class UserBase {
+  name: string
+  constructor(userName: string) {
+    this.name = userName
+  }
+  // 这是一个方法
+  getName() {
+    console.log(this.name)
+  }
+}
+
+// 接口继承类的时候也可以去掉类上面的方法
+interface User extends Omit<UserBase, 'getName'> {
+  age: number
+}
+
+// 最终只保留数据属性,不带有方法
+const petter: User = {
+  name: 'Petter',
+  age: 18,
+}

联合类型

阅读到这里,对 JavaScript 的数据和对象如何在 TypeScript 定义类型相信没有太大问题了吧!

所以这里先插入一个知识点,在介绍 对象(接口) 的类型定义时,提到 Omit 的帮助类型,它的类型里面有一个写法是 string | number | symbol ,这其实是 TypeScript 的一个联合类型。

当一个变量可能出现多种类型的值的时候,可以使用联合类型来定义它,类型之间用 | 符号分隔。

举一个简单的例子,下面这个函数接收一个代表 “计数” 的入参,并拼接成一句话打印到控制台,因为最终打印出来的句子是字符串,所以参数没有必要非得是数值,传字符串也是可以的,所以就可以使用联合类型:

ts
// 可以在 demo 里运行这段代码
+function counter(count: number | string) {
+  console.log(\`The current count is: \${count}.\`)
+}
+
+// 不论传数值还是字符串,都可以达到的目的
+counter(1)  // The current count is: 1.
+counter('2')  // The current count is: 2.
`,117),i=s("p",{class:"custom-block-title"},"TIP",-1),d=s("code",null,"counter",-1),g=s("code",null,"console.log",-1),h=s("a",{href:"https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Template_literals",target:"_blank",rel:"noreferrer"},"模板字符串",-1),u=s("p",null,[n("对 JavaScript 后面推出的新语法不太熟悉的话,很容易和单引号混淆,在学名上,它也被称之为 “反引号” ( Backquote ) ,可以使用标准键盘的 "),s("code",null,"ESC"),n(" 键下方、也就是 "),s("code",null,"1"),n(" 左边的那个按键打出来。")],-1),m=p(`

在实际的业务场景中,例如 Vue 的路由在不同的数据结构里也有不同的类型,有时候需要通过路由实例来判断是否符合要求的页面,也需要用到这种联合类型:

ts
// 注意:这不是完整的代码,只是一个使用场景示例
+import type { RouteRecordRaw, RouteLocationNormalizedLoaded } from 'vue-router'
+
+function isArticle(
+  route: RouteRecordRaw | RouteLocationNormalizedLoaded
+): boolean {
+  // ...
+}

再举个例子,是用 Vue 做页面,会涉及到子组件或者 DOM 的操作,当它们还没有渲染出来时,获取到的是 null ,渲染后才能拿到组件或者 DOM 结构,这种场景也可以使用联合类型:

ts
// querySelector 拿不到 DOM 的时候返回 null
+const ele: HTMLElement | null = document.querySelector('.main')

最后这个使用场景在 Vue 单组件的 DOM 元素与子组件 一节里也有相关的讲解。

当决定使用联合类型的时候,大部分情况下可能需要对变量做一些类型判断再写逻辑,当然有时候也可以无所谓,就像第一个例子拼接字符串那样。

这一小节在这里做简单了解即可,因为下面会继续配合不同的知识点把这个联合类型再次拿出来讲,比如 函数的重载 部分。

函数

函数是 JavaScript 里最重要的成员之一,所有的功能实现都是基于函数。

函数的基本的写法

在 JavaScript ,函数有很多种写法:

js
// 注意:这是 JavaScript 代码
+
+// 写法一:函数声明
+function sum1(x, y) {
+  return x + y
+}
+
+// 写法二:函数表达式
+const sum2 = function (x, y) {
+  return x + y
+}
+
+// 写法三:箭头函数
+const sum3 = (x, y) => x + y
+
+// 写法四:对象上的方法
+const obj = {
+  sum4(x, y) {
+    return x + y
+  },
+}
+
+// 还有很多……

但其实离不开两个最核心的操作:输入与输出,也就是对应函数的 “入参” 和 “返回值” ,在 TypeScript ,函数本身和 TS 类型有关系的也是在这两个地方。

函数的入参是把类型写在参数后面,返回值是写在圆括号后面,把上面在 JavaScript 的这几个写法,转换成 TypeScript 看看区别在哪里:

ts
// 注意:这是 TypeScript 代码
+
+// 写法一:函数声明
+function sum1(x: number, y: number): number {
+  return x + y
+}
+
+// 写法二:函数表达式
+const sum2 = function(x: number, y: number): number {
+  return x + y
+}
+
+// 写法三:箭头函数
+const sum3 = (x: number, y: number): number => x + y
+
+// 写法四:对象上的方法
+const obj = {
+  sum4(x: number, y: number): number {
+    return x + y
+  }
+}
+
+// 还有很多……

是不是一下子 Get 到了技巧!函数的类型定义也是非常的简单,掌握这个技巧可以让解决大部分常见的函数。

函数的可选参数

实际业务中会遇到有一些函数入参是可选,可以用和 对象(接口) 一样,用 ? 来定义:

ts
// 注意 isDouble 这个入参后面有个 ? 号,表示可选
+function sum(x: number, y: number, isDouble?: boolean): number {
+  return isDouble ? (x + y) * 2 : x + y
+}
+
+// 这样传参都不会报错,因为第三个参数是可选的
+sum(1, 2) // 3
+sum(1, 2, true) // 6

TIP

需要注意的是,可选参数必须排在必传参数的后面。

无返回值的函数

除了有返回值的函数,更多时候是不带返回值的,例如下面这个例子,这种函数用 void 来定义它的返回,也就是空。

ts
// 注意这里的返回值类型
+function sayHi(name: string): void {
+  console.log(\`Hi, \${name}!\`)
+}
+
+sayHi('Petter') // Hi, Petter!

需要注意的是, voidnullundefined 不可以混用,如果的函数返回值类型是 null ,那么是真的需要 return 一个 null 值:

ts
// 只有返回 null 值才能定义返回类型为 null
+function sayHi(name: string): null {
+  console.log(\`Hi, \${name}!\`)
+  return null
+}

有时候要判断参数是否合法,不符合要求时需要提前终止执行(比如在做一些表单校验的时候),这种情况下也可以用 void

ts
function sayHi(name: string): void {
+  // 这里判断参数不符合要求则提前终止运行,但它没有返回值
+  if (!name) return
+
+  // 否则正常运行
+  console.log(\`Hi, \${name}!\`)
+}

异步函数的返回值

对于异步函数,需要用 Promise<T> 类型来定义它的返回值,这里的 T 是泛型,取决于该函数最终返回一个什么样的值( async / await 也适用这个类型)。

例如这个例子,这是一个异步函数,会 resolve 一个字符串,所以它的返回类型是 Promise<string> (假如没有 resolve 数据,那么就是 Promise<void> )。

ts
// 注意这里的返回值类型
+function queryData(): Promise<string> {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve('Hello World')
+    }, 3000)
+  })
+}
+
+queryData().then((data) => console.log(data))

函数本身的类型

细心的开发者可能会有个疑问,通过函数表达式或者箭头函数声明的函数,这样写好像只对函数体的类型进行了定义,而左边的变量并没有指定。

没错,确实是没有为这个变量指定类型:

ts
// 这里的 sum ,确实是没有指定类型
+const sum = (x: number, y: number): number => x + y

这是因为,通常 TypeScript 会根据函数体自动推导,所以可以省略这里的定义。

如果确实有必要,可以这样来定义等号左边的类型:

ts
const sum: (x: number, y: number) => number = (x: number, y: number): number =>
+  x + y

这里出现了 2 个箭头 => ,注意第一个箭头是 TypeScript 的,第二个箭头是 JavaScript ES6 的。

实际上上面这句代码是分成了三部分:

  1. const sum: (x: number, y: number) => number 是这个函数的名称和类型
  2. = (x: number, y: number) 这里是指明了函数的入参和类型
  3. : number => x + y 这里是函数的返回值和类型

第 2 和 3 点相信从上面的例子已经能够理解了,所以注意力放在第一点:

TypeScript 的函数类型是以 () => void 这样的形式来写的:左侧圆括号是函数的入参类型,如果没有参数,就只有一个圆括号,如果有参数,就按照参数的类型写进去;右侧则是函数的返回值。

事实上由于 TypeScript 会推导函数类型,所以很少会显式的去写出来,除非在给对象定义方法:

ts
// 对象的接口
+interface Obj {
+  // 上面的方法就需要显式的定义出来
+  sum: (x: number, y: number) => number
+}
+
+// 声明一个对象
+const obj: Obj = {
+  sum(x: number, y: number): number {
+    return x + y
+  }
+}

函数的重载

在未来的实际开发中,可能会接触到一个 API 有多个 TS 类型的情况,比如 Vue 的 watch API

Vue 的这个 watch API 在被调用时,需要接收一个数据源参数,当侦听单个数据源时,它匹配了类型 1 ,当传入一个数组侦听多个数据源时,它匹配了类型 2 。

这个知识点其实就是 TypeScript 里的函数重载。

先来看一下在不使用函数重载时应该如何编写代码:

ts
// 对单人或者多人打招呼
+function greet(name: string | string[]): string | string[] {
+  if (Array.isArray(name)) {
+    return name.map((n) => \`Welcome, \${n}!\`)
+  }
+  return \`Welcome, \${name}!\`
+}
+
+// 单个问候语
+const greeting = greet('Petter')
+console.log(greeting) // Welcome, Petter!
+
+// 多个问候语
+const greetings = greet(['Petter', 'Tom', 'Jimmy'])
+console.log(greetings)
+// [ 'Welcome, Petter!', 'Welcome, Tom!', 'Welcome, Jimmy!' ]

TIP

注意这里的入参和返回值使用了 TypeScript 的 联合类型 ,不了解的话请先重温知识点。

虽然代码逻辑部分还是比较清晰的,区分了入参的数组类型和字符串类型,返回不同的结果,但是,在入参和返回值的类型这里,却显得非常乱。

并且这样子写,下面在调用函数时,定义的变量也无法准确的获得它们的类型:

ts
// 此时这个变量依然可能有多个类型
+const greeting: string | string[]

如果要强制确认类型,需要使用 TS 的 类型断言 (留意后面的 as 关键字):

ts
const greeting = greet('Petter') as string
+const greetings = greet(['Petter', 'Tom', 'Jimmy']) as string[]

这无形的增加了编码时的心智负担。

此时,利用 TypeScript 的函数重载就非常有用!来看一下具体如何实现:

ts
// 这一次用了函数重载
+function greet(name: string): string  // TS 类型
+function greet(name: string[]): string[]  // TS 类型
+function greet(name: string | string[]) {
+  if (Array.isArray(name)) {
+    return name.map((n) => \`Welcome, \${n}!\`)
+  }
+  return \`Welcome, \${name}!\`
+}
+
+// 单个问候语,此时只有一个类型 string
+const greeting = greet('Petter')
+console.log(greeting) // Welcome, Petter!
+
+// 多个问候语,此时只有一个类型 string[]
+const greetings = greet(['Petter', 'Tom', 'Jimmy'])
+console.log(greetings)
+// [ 'Welcome, Petter!', 'Welcome, Tom!', 'Welcome, Jimmy!' ]

上面是利用函数重载优化后的代码,可以看到一共写了 3 行 function greet … ,区别如下:

第 1 行是函数的 TS 类型,告知 TypeScript ,当入参为 string 类型时,返回值也是 string ;

第 2 行也是函数的 TS 类型,告知 TypeScript ,当入参为 string[] 类型时,返回值也是 string[] ;

第 3 行开始才是真正的函数体,这里的函数入参需要把可能涉及到的类型都写出来,用以匹配前两行的类型,并且这种情况下,函数的返回值类型可以省略,因为在第 1 、 2 行里已经定义过返回类型了。

任意值

如果实在不知道应该如何定义一个变量的类型, TypeScript 也允许使用任意值。

还记得在 为什么需要类型系统 的用的那个例子吗?再次放到 src/ts/index.ts 里:

ts
// 这段代码在 TS 里运行会报错
+function getFirstWord(msg) {
+  console.log(msg.split(' ')[0])
+}
+
+getFirstWord('Hello World')
+
+getFirstWord(123)

运行 npm run dev:ts 的时候,会得到一句报错 Parameter 'msg' implicitly has an 'any' type. ,意思是这个参数带有隐式 any 类型。

这里的 any 类型,就是 TypeScript 任意值。

既然报错是 “隐式” ,那 “显式” 的指定就可以了,当然,为了程序能够正常运行,还提高一下函数体内的代码健壮性:

ts
// 这里的入参显式指定了 any
+function getFirstWord(msg: any) {
+  // 这里使用了 String 来避免程序报错
+  console.log(String(msg).split(' ')[0])
+}
+
+getFirstWord('Hello World')
+
+getFirstWord(123)

这次就不会报错了,不论是传 string 还是 number 还是其他类型,都可以正常运行。

TIP

使用 any 的目的是让在开发的过程中,可以不必在无法确认类型的地方消耗太多时间,不代表不需要注意代码的健壮性。

一旦使用了 any ,代码里的逻辑请务必考虑多种情况进行判断或者处理兼容。

npm 包

虽然现在从 npm 安装的包都基本自带 TS 类型了,不过也存在一些包没有默认支持 TypeScript ,比如前面提到的 md5

在 TS 文件里导入并使用这个包的时候,会编译失败,比如在前面的 Hello TypeScript demo 里敲入以下代码:

ts
// src/ts/index.ts
+import md5 from 'md5'
+console.log(md5('Hello World'))

在命令行执行 npm run dev:ts 之后,会得到一段报错信息:

bash
src/ts/index.ts:1:17 - error TS7016:
+Could not find a declaration file for module 'md5'.
+'D:/Project/demo/hello-node/node_modules/md5/md5.js' implicitly has an 'any' type.
+  Try \`npm i --save-dev @types/md5\` if it exists
+  or add a new declaration (.d.ts) file
+  containing \`declare module 'md5';\`
+
+1 import md5 from 'md5'
+                  ~~~~~

这是因为缺少 md5 这个包的类型定义,根据命令行的提示,安装 @types/md5 这个包。

这是因为这些包是很早期用 JavaScript 编写的,因为功能够用作者也没有进行维护更新,所以缺少相应的 TS 类型,因此开源社区推出了一套 @types 类型包,专门处理这样的情况。

@types 类型包的命名格式为 @types/<package-name> ,也就是在原有的包名前面拼接 @types ,日常开发要用到的知名 npm 包都会有相应的类型包,只需要将其安装到 package.json 的 devDependencies 里即可解决该问题。

来安装一下 md5 的类型包:

bash
npm install -D @types/md5

再次运行就不会报错了!

bash
npm run dev:ts
+
+> demo@1.0.0 dev:ts
+> ts-node src/ts/index.ts
+
+b10a8db164e0754105b7a99be72e3fe5

类型断言

在讲解 函数的重载 的时候,提到了一个用法:

ts
const greeting = greet('Petter') as string

这里的 值 as 类型 就是 TypeScript 类型断言的语法,它还有另外一个语法是 <类型>值

当一个变量应用了 联合类型 时,在某些时候如果不显式的指明其中的一种类型,可能会导致后续的代码运行报错。

这个时候就可以通过类型断言强制指定其中一种类型,以便程序顺利运行下去。

常见的使用场景

把函数重载时最开始用到的那个例子,也就是下面的代码放到 src/ts/index.ts 里:

ts
// 对单人或者多人打招呼
+function greet(name: string | string[]): string | string[] {
+  if (Array.isArray(name)) {
+    return name.map((n) => \`Welcome, \${n}!\`)
+  }
+  return \`Welcome, \${name}!\`
+}
+
+// 虽然已知此时应该是 string[]
+// 但 TypeScript 还是会认为这是 string | string[]
+const greetings = greet(['Petter', 'Tom', 'Jimmy'])
+
+// 会导致无法使用 join 方法
+const greetingSentence = greetings.join(' ')
+console.log(greetingSentence)

执行 npm run dev:ts ,可以清楚的看到报错原因,因为 string 类型不具备 join 方法。

bash
src/ts/index.ts:11:31 - error TS2339:
+Property 'join' does not exist on type 'string | string[]'.
+  Property 'join' does not exist on type 'string'.
+
+11 const greetingStr = greetings.join(' ')
+                                 ~~~~

此时利用类型断言就可以达到目的:

ts
// 对单人或者多人打招呼
+function greet(name: string | string[]): string | string[] {
+  if (Array.isArray(name)) {
+    return name.map((n) => \`Welcome, \${n}!\`)
+  }
+  return \`Welcome, \${name}!\`
+}
+
+// 已知此时应该是 string[] ,所以用类型断言将其指定为 string[]
+const greetings = greet(['Petter', 'Tom', 'Jimmy']) as string[]
+
+// 现在可以正常使用 join 方法
+const greetingSentence = greetings.join(' ')
+console.log(greetingSentence)

需要注意的事情

但是,请不要滥用类型断言,只在能够确保代码正确的情况下去使用它,来看一个反例:

ts
// 原本要求 age 也是必须的属性之一
+interface User {
+  name: string
+  age: number
+}
+
+// 但是类型断言过程中,遗漏了
+const petter = {} as User
+petter.name = 'Petter'
+
+// TypeScript 依然可以运行下去,但实际上的数据是不完整的
+console.log(petter) // { name: 'Petter' }

TIP

使用类型断言可以让 TypeScript 不再检查该代码,默认是正确无误的。

所以,请务必保证这段代码真的是正确的!

类型推论

还记得在讲 原始数据类型 的时候,最后提到的:

不过在实际的编程过程中,原始数据类型的类型定义是可以省略的,因为 TypeScript 会根据声明变量时赋值的类型,自动帮推导变量类型

这其实是 TypeScript 的类型推论功能,当在声明变量的时候可以确认它的值,那么 TypeScript 也可以在这个时候帮推导它的类型,这种情况下就可以省略一些代码量。

下面这个变量这样声明是 OK 的,因为 TypeScript 会推导 msgstring 类型。

ts
// 相当于 msg: string
+let msg = 'Hello World'
+
+// 所以要赋值为 number 类型时会报错
+msg = 3 // Type 'number' is not assignable to type 'string'

下面这段代码也是可以正常运行的,因为 TypeScript 会根据 return 的结果推导 getRandomNumber 的返回值是 number 类型,从而推导变量 num 也是 number 类型。

ts
// 相当于 getRandomNumber(): number
+function getRandomNumber() {
+  return Math.round(Math.random() * 10)
+}
+
+// 相当于 num: number
+const num = getRandomNumber()

类型推论的前提是变量在声明时有明确的值,如果一开始没有赋值,那么会被默认为 any 类型。

ts
// 此时相当于 foo: any
+let foo
+
+// 所以可以任意改变类型
+foo = 1 // 1
+foo = true // true

类型推论可以节约很多书写工作量,在确保变量初始化有明确的值的时候,可以省略其类型,但必要的时候,该写上的还是要写上。

如何编译为 JavaScript 代码

学习到这里,对于 TypeScript 的入门知识已经学到了吧!

前面学习的时候,一直是基于 dev:ts 命令,它调用的是 ts-node 来运行的 TS 文件:

json
{
+  // ...
+  "scripts": {
+    // ...
+    "dev:ts": "ts-node src/ts/index.ts"
+  }
+  // ...
+}

但最终可能需要的是一个 JS 文件,比如要通过 <script src> 来放到 HTML 页面里,这就涉及到对 TypeScript 的编译。

来看看如何把一个 TS 文件编译成 JS 文件,让其从 TypeScript 变成 JavaScript 代码。

编译单个文件

先在 package.json 里增加一个 build script :

json
{
+  "name": "hello-node",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "dev:cjs": "node src/cjs/index.cjs",
+    "dev:esm": "node src/esm/index.mjs",
+    "dev:ts": "ts-node src/ts/index.ts",
+    "build": "tsc src/ts/index.ts --outDir dist",
+    "compile": "babel src/babel --out-dir compiled",
+    "serve": "node server/index.js"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "md5": "^2.3.0"
+  },
+  "devDependencies": {
+    "@types/md5": "^2.3.2",
+    "ts-node": "^10.7.0",
+    "typescript": "^4.6.3"
+  }
+}

这样在命令行运行 npm run build 的时候,就会把 src/ts/index.ts 这个 TS 文件编译,并输出到项目下与 src 文件夹同级的 dist 目录下。

其中 tsc 是 TypeScript 用来编译文件的命令, --outDir 是它的一个选项,用来指定输出目录,如果不指定,则默认生成到源文件所在的目录下面。

把之前在 函数的重载 用过的这个例子放到 src/ts/index.ts 文件里,因为它是一段比较典型的、包含了多个知识点的 TypeScript 代码:

ts
// 对单人或者多人打招呼
+function greet(name: string): string
+function greet(name: string[]): string[]
+function greet(name: string | string[]) {
+  if (Array.isArray(name)) {
+    return name.map((n) => \`Welcome, \${n}!\`)
+  }
+  return \`Welcome, \${name}!\`
+}
+
+// 单个问候语
+const greeting = greet('Petter')
+console.log(greeting)
+
+// 多个问候语
+const greetings = greet(['Petter', 'Tom', 'Jimmy'])
+console.log(greetings)

可以先执行 npm run dev:ts 测试它的可运行性,当然,如果期间的代码运行有问题,在编译阶段也会给报错。

现在来编译它,现在在命令行输入 npm run build 并回车执行。

可以看到多了一个 dist 文件夹,里面多了一个 index.js 文件。

bash
hello-node
+ # 构建产物
+├─dist
+  # 编译后的 JS 文件
+ └─index.js
+ # 依赖文件夹
+├─node_modules
+ # 源码文件夹
+├─src
+ # 锁定安装依赖的版本号
+├─package-lock.json
+ # 项目清单
+└─package.json

index.js 文件里面的代码如下:

js
function greet(name) {
+  if (Array.isArray(name)) {
+    return name.map(function (n) {
+      return 'Welcome, '.concat(n, '!')
+    })
+  }
+  return 'Welcome, '.concat(name, '!')
+}
+// 单个问候语
+var greeting = greet('Petter')
+console.log(greeting)
+// 多个问候语
+var greetings = greet(['Petter', 'Tom', 'Jimmy'])
+console.log(greetings)

可以看到已经成功把 TypeScript 代码编译成 JavaScript 代码了。

在命令行执行 node dist/index.js ,像之前测试 JS 文件一样使用 node 命令,运行 dist 目录下的 index.js 文件,它可以正确运行:

bash
node dist/index.js
+Welcome, Petter!
+[ 'Welcome, Petter!', 'Welcome, Tom!', 'Welcome, Jimmy!' ]

编译多个模块

刚才只是编译一个 index.ts 文件,如果 index.ts 里引入了其他模块,此时 index.ts 是作为入口文件,入口文件 import 进来使用的模块也会被 TypeScript 一并编译。

拆分一下模块,把 greet 函数单独抽离成一个模块文件 src/ts/greet.ts

ts
// src/ts/greet.ts
+function greet(name: string): string
+function greet(name: string[]): string[]
+function greet(name: string | string[]) {
+  if (Array.isArray(name)) {
+    return name.map((n) => \`Welcome, \${n}!\`)
+  }
+  return \`Welcome, \${name}!\`
+}
+
+export default greet

src/ts/index.ts 这边,把这个模块导进来:

ts
// src/ts/index.ts
+import greet from './greet'
+
+// 单个问候语
+const greeting = greet('Petter')
+console.log(greeting)
+
+// 多个问候语
+const greetings = greet(['Petter', 'Tom', 'Jimmy'])
+console.log(greetings)

package.json 里的 build script 无需修改,依然只编译 index.ts ,但因为导入了 greet.ts ,所以 TypeScript 也会一并编译,来试一下运行 npm run build , 现在 dist 目录下有两个文件了:

bash
hello-node
+ # 构建产物
+├─dist
+ ├─greet.js  # 多了这个文件
+ └─index.js
+
+ # 其他文件这里省略...
+└─package.json

来看看这一次的编译结果:

先看看 greet.js

js
// dist/greet.js
+'use strict'
+exports.__esModule = true
+function greet(name) {
+  if (Array.isArray(name)) {
+    return name.map(function (n) {
+      return 'Welcome, '.concat(n, '!')
+    })
+  }
+  return 'Welcome, '.concat(name, '!')
+}
+exports['default'] = greet

再看看 index.js

js
// dist/index.js
+'use strict'
+exports.__esModule = true
+var greet_1 = require('./greet')
+// 单个问候语
+var greeting = (0, greet_1['default'])('Petter')
+console.log(greeting)
+// 多个问候语
+var greetings = (0, greet_1['default'])(['Petter', 'Tom', 'Jimmy'])
+console.log(greetings)

这个代码风格有没有觉得似曾相识?是的,就是前面提到的 CommonJS 模块代码。

其实在 编译单个文件 代码的时候,它也是 CommonJS ,只不过因为只有一个文件,没有涉及到模块化,所以第一眼看不出来。

还是在命令行执行 node dist/index.js ,虽然也是运行 dist 目录下的 index.js 文件,但这次它的作用是充当一个入口文件了,引用到的 greet.js 模块文件也会被调用。

这次一样可以得到正确的结果:

bash
node dist/index.js
+Welcome, Petter!
+[ 'Welcome, Petter!', 'Welcome, Tom!', 'Welcome, Jimmy!' ]

修改编译后的 JS 版本

还可以修改编译配置,让 TypeScript 编译成不同的 JavaScript 版本。

修改 package.json 里的 build script ,在原有的命令后面增加一个 --target 选项:

json
{
+  // ...
+  "scripts": {
+    // ...
+    "build": "tsc src/ts/index.ts --outDir dist --target es6"
+  }
+  // ...
+}

--target 选项的作用是控制编译后的 JavaScript 版本,可选的值目前有: es3es5es6es2015es2016es2017es2018es2019es2020es2021es2022esnext ,分别对应不同的 JS 规范(所以未来的可选值会根据 JS 规范一起增加)。

之前编译出来的 JavaScript 是 CommonJS 规范 ,本次配置的是 es6 ,这是支持 ES Module 规范 的版本。

TIP

通常还需要配置一个 --module 选项,用于决定编译后是 CJS 规范还是 ESM 规范,但如果缺省,会根据 --target 来决定。

再次在命令行运行 npm run build ,这次看看变成了什么:

先看看 greet.js

js
// dist/greet.js
+function greet(name) {
+  if (Array.isArray(name)) {
+    return name.map((n) => \`Welcome, \${n}!\`)
+  }
+  return \`Welcome, \${name}!\`
+}
+export default greet

再看看 index.js

js
// dist/index.js
+import greet from './greet'
+// 单个问候语
+const greeting = greet('Petter')
+console.log(greeting)
+// 多个问候语
+const greetings = greet(['Petter', 'Tom', 'Jimmy'])
+console.log(greetings)

这次编译出来的都是基于 ES6 的 JavaScript 代码,因为涉及到 ESM 模块,所以不能直接在 node 运行它了,可以手动改一下扩展名,改成 .mjs (包括 index 文件里 import 的 greet 文件名也要改),然后再运行 node dist/index.mjs

其他事项

在尝试 编译单个文件编译多个模块 的时候,相信各位开发者应该没有太大的疑问,但是来到 修改编译后的 JS 版本 这里,事情就开始变得复杂了起来,应该能感觉到编译的选项和测试成本都相应的增加了很多。

事实上刚才编译的 JS 文件,因为涉及到 ESM 模块化,是无法通过普通的 <script /> 标签在 HTML 页面里使用的(单个文件可以,因为没有涉及模块),不仅需要加上 ESM 模块所需的 <script type="module" /> 属性,本地开发还需要启动本地服务器通过 HTTP 协议访问页面,才允许在浏览器里使用 ESM 模块(详见: 在浏览器里访问 ESM 一节)。

因此在实际的项目开发中,需要借助 构建工具 来处理很多编译过程中的兼容性问题,降低开发成本。

而刚才用到的诸如 --target 这样的选项,可以用一个更简单的方式来管理,类似于 package.json 项目清单, TypeScript 也有一份适用于项目的配置清单,请看 了解 tsconfig.json 部分。

了解 tsconfig.json

TypeScript 项目一般都会有一个 tsconfig.json 文件,放置于项目的根目录下,这个文件的作用是用来管理 TypeScript 在编译过程中的一些选项配置。

在开始之前,需要全局安装一下 TypeScript :

bash
npm install -g typescript

这样就可以使用 TypeScript 提供的全局功能,可以直接在命令行里使用 tsc 命令了(之前本地安装的时候,需要封装成 package.json 的 script 才能调用它)。

依然是用的 Hello TypeScript demo ,记得先通过 cd 命令进入项目所在的目录。

在命令行输入 tsc --init ,这是 TypeScript 提供的初始化功能,会生成一个默认的 tsconfig.json 文件。

bash
tsc --init
+
+Created a new tsconfig.json with:
+                                                                               TS
+  target: es2016
+  module: commonjs
+  strict: true
+  esModuleInterop: true
+  skipLibCheck: true
+  forceConsistentCasingInFileNames: true
+
+
+You can learn more at https://aka.ms/tsconfig.json

现在的目录结构是这样子,多了一个 tsconfig.json 文件:

bash
hello-node
+ # 构建产物
+├─dist
+ # 依赖文件夹
+├─node_modules
+ # 源码文件夹
+├─src
+ # 锁定安装依赖的版本号
+├─package-lock.json
+ # 项目清单
+├─package.json
+ # TypeScript 配置
+└─tsconfig.json

每一个 tsc 的命令行的选项,都可以作为这个 JSON 的一个字段来管理,例如刚才的 --outDir--target 选项,在这个 JSON 文件里对应的就是:

json
{
+  "compilerOptions": {
+    "target": "es6",
+    "module": "es6",
+    "outDir": "./dist"
+  }
+}

可以直接在生成的 tsconfig.json 上面修改。

来试试效果,这一次不需要用到 package.json 里的 build script 了,直接在命令行运行 tsc ,它现在会根据配置的 tsconfig.json 文件,按照的要求来编译。

可以看到它依然按照要求在 dist 目录下生成编译后的 JS 文件,而且这一次的编译结果,和在 build script 里使用的 tsc src/ts/index.ts --outDir dist --target es6 这一长串命令是一样的。

所以正常工作中,都是使用 tsconfig.json 来管理 TypeScript 的配置的。

完整的选项可以查看 TypeScript 官网: tsconfig - typescriptlang

不过实际工作中的项目都是通过一些脚手架创建的,例如 Vue CLI ,或者现在的 Create Vue 或者 Create Preset ,都会在创建项目模板的时候,提前配置好通用的选项,只需要在不满足条件的情况下去调整。

`,191),q=JSON.parse('{"title":"快速上手 TypeScript","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"typescript.md","filePath":"typescript.md"}'),E={name:"typescript.md"},S=Object.assign(E,{setup(B){const t="``";return(f,b)=>{const c=l("ImgWrap"),o=l("ClientOnly"),r=l("GitalkComment");return y(),D("div",null,[C,a(o,null,{default:e(()=>[a(c,{src:"/assets/img/github-top-languages.jpg",dark:"/assets/img/github-top-languages-dark.jpg",alt:"TypeScript 的流行程度(来自 GitHub 年度统计报告)"})]),_:1}),A,s("div",{class:"tip custom-block"},[i,s("p",null,[n("注意在上面 "),d,n(" 函数的 "),g,n(" 语句里,使用了一个 "),s("code",null,F(t)),n(" 符号来定义字符串,这是 ES6 语法里的 "),h,n(" ,它和传统的单引号 / 双引号相比更为灵活,特别是遇到字符串需要配合多变量拼接和换行的情况。")]),u]),m,a(o,null,{default:e(()=>[a(r,{issueId:193})]),_:1})])}}});export{q as __pageData,S as default}; diff --git a/assets/typescript.md.e6e7bfb1.lean.js b/assets/typescript.md.e6e7bfb1.lean.js new file mode 100644 index 00000000..c1506400 --- /dev/null +++ b/assets/typescript.md.e6e7bfb1.lean.js @@ -0,0 +1,677 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{v as y,b as D,t as a,O as e,F as s,L as n,X as F,R as p,M as l}from"./chunks/framework.0d8bea05.js";const C=p('

快速上手 TypeScript

如果已经看完 工程化的起步准备 一章,相信此时的已经对 Node 工程项目有了足够的认识了,在此之前的所有代码都是使用 JavaScript 编写的,接下来这一节,将开始介绍 TypeScript ,这是一门新的语言,但是上手非常简单。

TypeScript 简称 TS ,既是一门新语言,也是 JS 的一个超集,它是在 JavaScript 的基础上增加了一套类型系统,它支持所有的 JS 语句,为工程化开发而生,最终在编译的时候去掉类型和特有的语法,生成 JS 代码。

虽然带有类型系统的前端语言不止 TypeScript (例如 Facebook 推出的 Flow.js ),但从目前整个 开源社区的流行趋势 看, TypeScript 无疑是更好的选择。

',4),A=p(`

而且只要本身已经学会了 JS ,并且经历过很多协作类的项目,那么使用 TS 编程是一个很自然而然的过程。

为什么需要类型系统

要想知道自己为什么要用 TypeScript ,得先从 JavaScript 有什么不足说起,举一个非常小的例子:

js
function getFirstWord(msg) {
+  console.log(msg.split(' ')[0])
+}
+
+getFirstWord('Hello World') // 输出 Hello
+
+getFirstWord(123) // TypeError: msg.split is not a function

这里定义了一个用空格切割字符串的方法,并打印出第一个单词:

  1. 第一次执行时,字符串支持 split 方法,所以成功获取到了第一个单词 Hello
  2. 第二次执行时,由于数值不存在 split 方法,所以传入 123 引起了程序崩溃

这就是 JavaScript 的弊端,过于灵活,没有类型的约束,很容易因为类型的变化导致一些本可避免的 BUG 出现,而且这些 BUG 通常需要在程序运行的时候才会被发现,很容易引发生产事故。

虽然可以在执行 split 方法之前执行一层判断或者转换,但很明显增加了很多工作量。

TypeScript 的出现,在编译的时候就可以执行检查来避免掉这些问题,而且配合 VSCode 等编辑器的智能提示,可以很方便的知道每个变量对应的类型。

Hello TypeScript

将继续使用 Hello Node 这个 demo ,或者可以再建一个新 demo ,依然是在 src 文件夹下,创建一个 ts 文件夹归类本次的测试文件,然后创建一个 index.ts 文件在 ts 文件夹下。

TIP

TypeScript 语言对应的文件扩展名是 .ts

然后在命令行通过 cd 命令进入项目所在的目录路径,安装 TypeScript 开发的两个主要依赖包:

  1. typescript 这个包是用 TypeScript 编程的语言依赖包

  2. ts-node 是让 Node 可以运行 TypeScript 的执行环境

bash
npm install -D typescript ts-node

这次添加了一个 -D 参数,因为 TypeScript 和 TS-Node 是开发过程中使用的依赖,所以将其添加到 package.json 的 devDependencies 字段里。

然后修改 scripts 字段,增加一个 dev:ts 的 script :

json
{
+  "name": "hello-node",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "dev:cjs": "node src/cjs/index.cjs",
+    "dev:esm": "node src/esm/index.mjs",
+    "dev:ts": "ts-node src/ts/index.ts",
+    "compile": "babel src/babel --out-dir compiled",
+    "serve": "node server/index.js"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "md5": "^2.3.0"
+  },
+  "devDependencies": {
+    "ts-node": "^10.7.0",
+    "typescript": "^4.6.3"
+  }
+}

准备工作完毕!

TIP

请注意, dev:ts 这个 script 是用了 ts-node 来代替原来在用的 node ,因为使用 node 无法识别 TypeScript 语言。

为什么需要类型系统 里面提到的例子放到 src/ts/index.ts 里:

ts
// src/ts/index.ts
+function getFirstWord(msg) {
+  console.log(msg.split(' ')[0])
+}
+
+getFirstWord('Hello World')
+
+getFirstWord(123)

然后在命令行运行 npm run dev:ts 来看看这次的结果:

bash
TSError:  Unable to compile TypeScript:
+src/ts/index.ts:1:23 - error TS7006: Parameter 'msg' implicitly has an 'any' type.
+
+1 function getFirstWord(msg) {
+                        ~~~

这是告知 getFirstWord 的入参 msg 带有隐式 any 类型,这个时候可能还不了解 any 代表什么意思,没关系,来看下如何修正这段代码:

ts
// src/ts/index.ts
+function getFirstWord(msg: string) {
+  console.log(msg.split(' ')[0])
+}
+
+getFirstWord('Hello World')
+
+getFirstWord(123)

留意到没有,现在函数的入参 msg 已经变成了 msg: string ,这是 TypeScript 指定参数为字符串类型的一个写法。

现在再运行 npm run dev:ts ,上一个错误提示已经不再出现,取而代之的是一个新的报错:

bash
TSError:  Unable to compile TypeScript:
+src/ts/index.ts:7:14 - error TS2345:
+Argument of type 'number' is not assignable to parameter of type 'string'.
+
+7 getFirstWord(123)
+               ~~~

这次的报错代码是在 getFirstWord(123) 这里,告诉 number 类型的数据不能分配给 string 类型的参数,也就是故意传入一个会报错的数值进去,被 TypeScript 检查出来了!

可以再仔细留意一下控制台的信息,会发现没有报错的 getFirstWord('Hello World') 也没有打印出结果,这是因为 TypeScript 需要先被编译成 JavaScript ,然后再执行。

这个机制让有问题的代码能够被及早发现,一旦代码出现问题,编译阶段就会失败。

移除会报错的那行代码,只保留如下:

ts
// src/ts/index.ts
+function getFirstWord(msg: string) {
+  console.log(msg.split(' ')[0])
+}
+
+getFirstWord('Hello World')

再次运行 npm run dev:ts ,这次完美运行!

ts
npm run dev:ts
+
+> demo@1.0.0 dev:ts
+> ts-node src/ts/index.ts
+
+Hello

在这个例子里,相信已经感受到 TypeScript 的魅力了!接下来来认识一下不同的 JavaScript 类型,在 TypeScript 里面应该如何定义。

常用的 TS 类型定义

Hello TypeScript 的体验中,相信能够感受到 TypeScript 编程带来的好处了,代码的健壮性得到了大大的提升!

并且应该也能够深刻理解,TypeScript 的类型系统并不会给编程带来极高的门槛或者说开发障碍,这是以非常小的成本换取巨大收益的举措。

TIP

如果还没有体验这个 demo ,建议先按教程跑一下,然后来讲解不同的 JavaScript 类型应该如何在 TypeScript 里定义,接下来的时间里,可以一边看,一边在 demo 里实践。

原始数据类型

原始数据类型 是一种既非对象也无方法的数据,刚才演示代码里,函数的入参使用的字符串 String 就是原始数据类型之一。

除了 String ,另外还有数值 Number 、布尔值 Boolean 等等,它们在 TypeScript 都有统一的表达方式,列个表格对比,能够更直观的了解:

原始数据类型JavaScriptTypeScript
字符串Stringstring
数值Numbernumber
布尔值Booleanboolean
大整数BigIntbigint
符号Symbolsymbol
不存在Nullnull
未定义Undefinedundefined

有没有发现窍门?对! TypeScript 对原始数据的类型定义真的是超级简单,就是转为全小写即可!

举几个例子:

ts
// 字符串
+const str: string = 'Hello World'
+
+// 数值
+const num: number = 1
+
+// 布尔值
+const bool: boolean = true

不过在实际的编程过程中,原始数据类型的类型定义是可以省略的,因为 TypeScript 会根据声明变量时赋值的类型,自动推导变量类型,也就是可以跟平时写 JavaScript 一样:

ts
// 这样也不会报错,因为 TS 会推导它们的类型
+const str = 'Hello World'
+const num = 1
+const bool = true

数组

除了原始数据类型之外, JavaScript 还有引用类型,数组 Array 就是其中的一种。

之所以先讲数组,是因为它在 TS 类型定义的写法上面,可能是最接近原始数据的一个类型了,为什么这么说?还是列个表格,来看一下如何定义数组:

数组里的数据类型写法 1类型写法 2
字符串string[]Array<string>
数值number[]Array<number>
布尔值boolean[]Array<boolean>
大整数bigint[]Array<bigint>
符号symbol[]Array<symbol>
不存在null[]Array<null>
未定义undefined[]Array<undefined>

是吧!就只是在原始数据类型的基础上变化了一下书写格式,就成为了数组的定义!

笔者最常用的就是 string[] 这样的格式,只需要追加一个方括号 [] ,另外一种写法是基于 TS 的泛型 Array<T> ,两种方式定义出来的类型其实是一样的。

举几个例子:

ts
// 字符串数组
+const strs: string[] = ['Hello World', 'Hi World']
+
+// 数值数组
+const nums: number[] = [1, 2, 3]
+
+// 布尔值数组
+const bools: boolean[] = [true, true, false]

在实际的编程过程中,如果数组一开始就有初始数据(数组长度不为 0 ),那么 TypeScript 也会根据数组里面的项目类型,正确自动帮推导这个数组的类型,这种情况下也可以省略类型定义:

ts
// 这种有初始项目的数组, TS 也会推导它们的类型
+const strs = ['Hello World', 'Hi World']
+const nums = [1, 2, 3]
+const bools = [true, true, false]

但是!如果一开始是 [] ,那么就必须显式的指定数组类型(取决于当前项目的 tsconfig.json 配置,可能会引起报错):

ts
// 这个时候会认为是 any[] 或者 never[] 类型
+const nums = []
+
+// 这个时候再 push 一个 number 数据进去,也不会使其成为 number[]
+nums.push(1)

而对于复杂的数组,比如数组里面的 item 都是对象,其实格式也是一样,只不过把原始数据类型换成 对象的类型 即可,例如 UserItem[] 表示这是一个关于用户的数组列表。

对象(接口)

看完了数组,接下来看看对象的用法,对象也是引用类型,在 数组 的最后提到了一个 UserItem[] 的写法,这里的 UserItem 就是一个对象的类型定义。

如果熟悉 JavaScript ,那么就知道对象的 “键值对” 里面的值,可能是由原始数据、数组、对象组成的,所以在 TypeScript ,类型定义也是需要根据值的类型来确定它的类型,因此定义对象的类型应该是第一个比较有门槛的地方。

如何定义对象的类型

对象的类型定义有两个语法支持: typeinterface

先看看 type 的写法:

ts
type UserItem = {
+  // ...
+}

再看看 interface 的写法:

ts
interface UserItem {
+  // ...
+}

可以看到它们表面上的区别是一个有 = 号,一个没有,事实上在一般的情况下也确实如此,两者非常接近,但是在特殊的时候也有一定的区别。

了解接口的使用

为了降低学习门槛,统一使用 interface 来做入门教学,它的写法与 Object 更为接近,事实上它也被用的更多。

对象的类型 interface 也叫做接口,用来描述对象的结构。

TIP

对象的类型定义通常采用 Upper Camel Case 大驼峰命名法,也就是每个单词的首字母大写,例如 UserItemGameDetail ,这是为了跟普通变量进行区分(变量通常使用 Lower Camel Case 小驼峰写法,也就是第一个单词的首字母小写,其他首字母大写,例如 userItem )。

这里通过一些举例来带举一反三,随时可以在 demo 里进行代码实践。

以这个用户信息为例子,比如要描述 Petter 这个用户,他的最基础信息就是姓名和年龄,那么定义为接口就是这么写:

ts
// 定义用户对象的类型
+interface UserItem {
+  name: string
+  age: number
+}
+
+// 在声明变量的时候将其关联到类型上
+const petter: UserItem = {
+  name: 'Petter',
+  age: 20,
+}

如果需要添加数组、对象等类型到属性里,按照这样继续追加即可。

可选的接口属性

注意,上面这样定义的接口类型,表示 nameage 都是必选的属性,不可以缺少,一旦缺少,代码运行起来就会报错!

src/ts/index.ts 里敲入以下代码,也就是在声明变量的时候故意缺少了 age 属性,来看看会发生什么:

ts
// 注意!这是一段会报错的代码
+
+interface UserItem {
+  name: string
+  age: number
+}
+
+const petter: UserItem = {
+  name: 'Petter',
+}

运行 npm run dev:ts ,会看到控制台给的报错信息,缺少了必选的属性 age

bash
src/ts/index.ts:6:7 - error TS2741:
+Property 'age' is missing in type '{ name: string; }'
+but required in type 'UserItem'.
+
+6 const petter: UserItem = {
+        ~~~~~~
+
+  src/ts/index.ts:3:3
+    3   age: number
+        ~~~
+    'age' is declared here.

在实际的业务中,有可能会出现一些属性并不是必须的,就像这个年龄,可以将其设置为可选属性,通过添加 ? 来定义。

请注意下面代码的第三行, age 后面紧跟了一个 ? 号再接 : 号,这是 TypeScript 对象对于可选属性的一个定义方式,这一次这段代码是可以成功运行的!

ts
interface UserItem {
+  name: string
+  // 这个属性变成了可选
+  age?: number
+}
+
+const petter: UserItem = {
+  name: 'Petter',
+}

调用自身接口的属性

如果一些属性的结构跟本身一致,也可以直接引用,比如下面例子里的 friendList 属性,用户的好友列表,它就可以继续使用 UserItem 这个接口作为数组的类型:

ts
interface UserItem {
+  name: string
+  age: number
+  enjoyFoods: string[]
+  // 这个属性引用了本身的类型
+  friendList: UserItem[]
+}
+
+const petter: UserItem = {
+  name: 'Petter',
+  age: 18,
+  enjoyFoods: ['rice', 'noodle', 'pizza'],
+  friendList: [
+    {
+      name: 'Marry',
+      age: 16,
+      enjoyFoods: ['pizza', 'ice cream'],
+      friendList: [],
+    },
+    {
+      name: 'Tom',
+      age: 20,
+      enjoyFoods: ['chicken', 'cake'],
+      friendList: [],
+    }
+  ],
+}

接口的继承

接口还可以继承,比如要对用户设置管理员,管理员信息也是一个对象,但要比普通用户多一个权限级别的属性,那么就可以使用继承,它通过 extends 来实现:

ts
interface UserItem {
+  name: string
+  age: number
+  enjoyFoods: string[]
+  friendList: UserItem[]
+}
+
+// 这里继承了 UserItem 的所有属性类型,并追加了一个权限等级属性
+interface Admin extends UserItem {
+  permissionLevel: number
+}
+
+const admin: Admin = {
+  name: 'Petter',
+  age: 18,
+  enjoyFoods: ['rice', 'noodle', 'pizza'],
+  friendList: [
+    {
+      name: 'Marry',
+      age: 16,
+      enjoyFoods: ['pizza', 'ice cream'],
+      friendList: [],
+    },
+    {
+      name: 'Tom',
+      age: 20,
+      enjoyFoods: ['chicken', 'cake'],
+      friendList: [],
+    }
+  ],
+  permissionLevel: 1,
+}

如果觉得这个 Admin 类型不需要记录这么多属性,也可以在继承的过程中舍弃某些属性,通过 Omit 帮助类型来实现,Omit 的类型如下:

ts
type Omit<T, K extends string | number | symbol>

其中 T 代表已有的一个对象类型, K 代表要删除的属性名,如果只有一个属性就直接是一个字符串,如果有多个属性,用 | 来分隔开,下面的例子就是删除了两个不需要的属性:

ts
interface UserItem {
+  name: string
+  age: number
+  enjoyFoods: string[]
+  friendList?: UserItem[]
+}
+
+// 这里在继承 UserItem 类型的时候,删除了两个多余的属性
+interface Admin extends Omit<UserItem, 'enjoyFoods' | 'friendList'> {
+  permissionLevel: number
+}
+
+// 现在的 admin 就非常精简了
+const admin: Admin = {
+  name: 'Petter',
+  age: 18,
+  permissionLevel: 1,
+}

看到这里并实际体验过的话,在业务中常见的类型定义已经难不倒了!

类是 JavaScript ES6 推出的一个概念,通过 class 关键字,可以定义一个对象的模板,如果对类还比较陌生的话,可以先阅读一下阮一峰老师的 ES6 文章:Class 的基本语法

在 TypeScript ,通过类得到的变量,它的类型就是这个类,可能这句话看起来有点难以理解,来看个例子,可以在 demo 里运行它:

ts
// 定义一个类
+class User {
+  // constructor 上的数据需要先这样定好类型
+  name: string
+
+  // 入参也要定义类型
+  constructor(userName: string) {
+    this.name = userName
+  }
+
+  getName() {
+    console.log(this.name)
+  }
+}
+
+// 通过 new 这个类得到的变量,它的类型就是这个类
+const petter: User = new User('Petter')
+petter.getName() // Petter

类与类之间可以继承:

ts
// 这是一个基础类
+class UserBase {
+  name: string
+  constructor(userName: string) {
+    this.name = userName
+  }
+}
+
+// 这是另外一个类,继承自基础类
+class User extends UserBase {
+  getName() {
+    console.log(this.name)
+  }
+}
+
+// 这个变量拥有上面两个类的所有属性和方法
+const petter: User = new User('Petter')
+petter.getName()

类也可以提供给接口去继承:

ts
// 这是一个类
+class UserBase {
+  name: string
+  constructor(userName: string) {
+    this.name = userName
+  }
+}
+
+// 这是一个接口,可以继承自类
+interface User extends UserBase {
+  age: number
+}
+
+// 这样这个变量就必须同时存在两个属性
+const petter: User = {
+  name: 'Petter',
+  age: 18,
+}

如果类上面本身有方法存在,接口在继承的时候也要相应的实现,当然也可以借助在 对象(接口) 提到的 Omit 帮助类型来去掉这些方法。

ts
class UserBase {
+  name: string
+  constructor(userName: string) {
+    this.name = userName
+  }
+  // 这是一个方法
+  getName() {
+    console.log(this.name)
+  }
+}
+
+// 接口继承类的时候也可以去掉类上面的方法
+interface User extends Omit<UserBase, 'getName'> {
+  age: number
+}
+
+// 最终只保留数据属性,不带有方法
+const petter: User = {
+  name: 'Petter',
+  age: 18,
+}

联合类型

阅读到这里,对 JavaScript 的数据和对象如何在 TypeScript 定义类型相信没有太大问题了吧!

所以这里先插入一个知识点,在介绍 对象(接口) 的类型定义时,提到 Omit 的帮助类型,它的类型里面有一个写法是 string | number | symbol ,这其实是 TypeScript 的一个联合类型。

当一个变量可能出现多种类型的值的时候,可以使用联合类型来定义它,类型之间用 | 符号分隔。

举一个简单的例子,下面这个函数接收一个代表 “计数” 的入参,并拼接成一句话打印到控制台,因为最终打印出来的句子是字符串,所以参数没有必要非得是数值,传字符串也是可以的,所以就可以使用联合类型:

ts
// 可以在 demo 里运行这段代码
+function counter(count: number | string) {
+  console.log(\`The current count is: \${count}.\`)
+}
+
+// 不论传数值还是字符串,都可以达到的目的
+counter(1)  // The current count is: 1.
+counter('2')  // The current count is: 2.
`,117),i=s("p",{class:"custom-block-title"},"TIP",-1),d=s("code",null,"counter",-1),g=s("code",null,"console.log",-1),h=s("a",{href:"https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Template_literals",target:"_blank",rel:"noreferrer"},"模板字符串",-1),u=s("p",null,[n("对 JavaScript 后面推出的新语法不太熟悉的话,很容易和单引号混淆,在学名上,它也被称之为 “反引号” ( Backquote ) ,可以使用标准键盘的 "),s("code",null,"ESC"),n(" 键下方、也就是 "),s("code",null,"1"),n(" 左边的那个按键打出来。")],-1),m=p(`

在实际的业务场景中,例如 Vue 的路由在不同的数据结构里也有不同的类型,有时候需要通过路由实例来判断是否符合要求的页面,也需要用到这种联合类型:

ts
// 注意:这不是完整的代码,只是一个使用场景示例
+import type { RouteRecordRaw, RouteLocationNormalizedLoaded } from 'vue-router'
+
+function isArticle(
+  route: RouteRecordRaw | RouteLocationNormalizedLoaded
+): boolean {
+  // ...
+}

再举个例子,是用 Vue 做页面,会涉及到子组件或者 DOM 的操作,当它们还没有渲染出来时,获取到的是 null ,渲染后才能拿到组件或者 DOM 结构,这种场景也可以使用联合类型:

ts
// querySelector 拿不到 DOM 的时候返回 null
+const ele: HTMLElement | null = document.querySelector('.main')

最后这个使用场景在 Vue 单组件的 DOM 元素与子组件 一节里也有相关的讲解。

当决定使用联合类型的时候,大部分情况下可能需要对变量做一些类型判断再写逻辑,当然有时候也可以无所谓,就像第一个例子拼接字符串那样。

这一小节在这里做简单了解即可,因为下面会继续配合不同的知识点把这个联合类型再次拿出来讲,比如 函数的重载 部分。

函数

函数是 JavaScript 里最重要的成员之一,所有的功能实现都是基于函数。

函数的基本的写法

在 JavaScript ,函数有很多种写法:

js
// 注意:这是 JavaScript 代码
+
+// 写法一:函数声明
+function sum1(x, y) {
+  return x + y
+}
+
+// 写法二:函数表达式
+const sum2 = function (x, y) {
+  return x + y
+}
+
+// 写法三:箭头函数
+const sum3 = (x, y) => x + y
+
+// 写法四:对象上的方法
+const obj = {
+  sum4(x, y) {
+    return x + y
+  },
+}
+
+// 还有很多……

但其实离不开两个最核心的操作:输入与输出,也就是对应函数的 “入参” 和 “返回值” ,在 TypeScript ,函数本身和 TS 类型有关系的也是在这两个地方。

函数的入参是把类型写在参数后面,返回值是写在圆括号后面,把上面在 JavaScript 的这几个写法,转换成 TypeScript 看看区别在哪里:

ts
// 注意:这是 TypeScript 代码
+
+// 写法一:函数声明
+function sum1(x: number, y: number): number {
+  return x + y
+}
+
+// 写法二:函数表达式
+const sum2 = function(x: number, y: number): number {
+  return x + y
+}
+
+// 写法三:箭头函数
+const sum3 = (x: number, y: number): number => x + y
+
+// 写法四:对象上的方法
+const obj = {
+  sum4(x: number, y: number): number {
+    return x + y
+  }
+}
+
+// 还有很多……

是不是一下子 Get 到了技巧!函数的类型定义也是非常的简单,掌握这个技巧可以让解决大部分常见的函数。

函数的可选参数

实际业务中会遇到有一些函数入参是可选,可以用和 对象(接口) 一样,用 ? 来定义:

ts
// 注意 isDouble 这个入参后面有个 ? 号,表示可选
+function sum(x: number, y: number, isDouble?: boolean): number {
+  return isDouble ? (x + y) * 2 : x + y
+}
+
+// 这样传参都不会报错,因为第三个参数是可选的
+sum(1, 2) // 3
+sum(1, 2, true) // 6

TIP

需要注意的是,可选参数必须排在必传参数的后面。

无返回值的函数

除了有返回值的函数,更多时候是不带返回值的,例如下面这个例子,这种函数用 void 来定义它的返回,也就是空。

ts
// 注意这里的返回值类型
+function sayHi(name: string): void {
+  console.log(\`Hi, \${name}!\`)
+}
+
+sayHi('Petter') // Hi, Petter!

需要注意的是, voidnullundefined 不可以混用,如果的函数返回值类型是 null ,那么是真的需要 return 一个 null 值:

ts
// 只有返回 null 值才能定义返回类型为 null
+function sayHi(name: string): null {
+  console.log(\`Hi, \${name}!\`)
+  return null
+}

有时候要判断参数是否合法,不符合要求时需要提前终止执行(比如在做一些表单校验的时候),这种情况下也可以用 void

ts
function sayHi(name: string): void {
+  // 这里判断参数不符合要求则提前终止运行,但它没有返回值
+  if (!name) return
+
+  // 否则正常运行
+  console.log(\`Hi, \${name}!\`)
+}

异步函数的返回值

对于异步函数,需要用 Promise<T> 类型来定义它的返回值,这里的 T 是泛型,取决于该函数最终返回一个什么样的值( async / await 也适用这个类型)。

例如这个例子,这是一个异步函数,会 resolve 一个字符串,所以它的返回类型是 Promise<string> (假如没有 resolve 数据,那么就是 Promise<void> )。

ts
// 注意这里的返回值类型
+function queryData(): Promise<string> {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve('Hello World')
+    }, 3000)
+  })
+}
+
+queryData().then((data) => console.log(data))

函数本身的类型

细心的开发者可能会有个疑问,通过函数表达式或者箭头函数声明的函数,这样写好像只对函数体的类型进行了定义,而左边的变量并没有指定。

没错,确实是没有为这个变量指定类型:

ts
// 这里的 sum ,确实是没有指定类型
+const sum = (x: number, y: number): number => x + y

这是因为,通常 TypeScript 会根据函数体自动推导,所以可以省略这里的定义。

如果确实有必要,可以这样来定义等号左边的类型:

ts
const sum: (x: number, y: number) => number = (x: number, y: number): number =>
+  x + y

这里出现了 2 个箭头 => ,注意第一个箭头是 TypeScript 的,第二个箭头是 JavaScript ES6 的。

实际上上面这句代码是分成了三部分:

  1. const sum: (x: number, y: number) => number 是这个函数的名称和类型
  2. = (x: number, y: number) 这里是指明了函数的入参和类型
  3. : number => x + y 这里是函数的返回值和类型

第 2 和 3 点相信从上面的例子已经能够理解了,所以注意力放在第一点:

TypeScript 的函数类型是以 () => void 这样的形式来写的:左侧圆括号是函数的入参类型,如果没有参数,就只有一个圆括号,如果有参数,就按照参数的类型写进去;右侧则是函数的返回值。

事实上由于 TypeScript 会推导函数类型,所以很少会显式的去写出来,除非在给对象定义方法:

ts
// 对象的接口
+interface Obj {
+  // 上面的方法就需要显式的定义出来
+  sum: (x: number, y: number) => number
+}
+
+// 声明一个对象
+const obj: Obj = {
+  sum(x: number, y: number): number {
+    return x + y
+  }
+}

函数的重载

在未来的实际开发中,可能会接触到一个 API 有多个 TS 类型的情况,比如 Vue 的 watch API

Vue 的这个 watch API 在被调用时,需要接收一个数据源参数,当侦听单个数据源时,它匹配了类型 1 ,当传入一个数组侦听多个数据源时,它匹配了类型 2 。

这个知识点其实就是 TypeScript 里的函数重载。

先来看一下在不使用函数重载时应该如何编写代码:

ts
// 对单人或者多人打招呼
+function greet(name: string | string[]): string | string[] {
+  if (Array.isArray(name)) {
+    return name.map((n) => \`Welcome, \${n}!\`)
+  }
+  return \`Welcome, \${name}!\`
+}
+
+// 单个问候语
+const greeting = greet('Petter')
+console.log(greeting) // Welcome, Petter!
+
+// 多个问候语
+const greetings = greet(['Petter', 'Tom', 'Jimmy'])
+console.log(greetings)
+// [ 'Welcome, Petter!', 'Welcome, Tom!', 'Welcome, Jimmy!' ]

TIP

注意这里的入参和返回值使用了 TypeScript 的 联合类型 ,不了解的话请先重温知识点。

虽然代码逻辑部分还是比较清晰的,区分了入参的数组类型和字符串类型,返回不同的结果,但是,在入参和返回值的类型这里,却显得非常乱。

并且这样子写,下面在调用函数时,定义的变量也无法准确的获得它们的类型:

ts
// 此时这个变量依然可能有多个类型
+const greeting: string | string[]

如果要强制确认类型,需要使用 TS 的 类型断言 (留意后面的 as 关键字):

ts
const greeting = greet('Petter') as string
+const greetings = greet(['Petter', 'Tom', 'Jimmy']) as string[]

这无形的增加了编码时的心智负担。

此时,利用 TypeScript 的函数重载就非常有用!来看一下具体如何实现:

ts
// 这一次用了函数重载
+function greet(name: string): string  // TS 类型
+function greet(name: string[]): string[]  // TS 类型
+function greet(name: string | string[]) {
+  if (Array.isArray(name)) {
+    return name.map((n) => \`Welcome, \${n}!\`)
+  }
+  return \`Welcome, \${name}!\`
+}
+
+// 单个问候语,此时只有一个类型 string
+const greeting = greet('Petter')
+console.log(greeting) // Welcome, Petter!
+
+// 多个问候语,此时只有一个类型 string[]
+const greetings = greet(['Petter', 'Tom', 'Jimmy'])
+console.log(greetings)
+// [ 'Welcome, Petter!', 'Welcome, Tom!', 'Welcome, Jimmy!' ]

上面是利用函数重载优化后的代码,可以看到一共写了 3 行 function greet … ,区别如下:

第 1 行是函数的 TS 类型,告知 TypeScript ,当入参为 string 类型时,返回值也是 string ;

第 2 行也是函数的 TS 类型,告知 TypeScript ,当入参为 string[] 类型时,返回值也是 string[] ;

第 3 行开始才是真正的函数体,这里的函数入参需要把可能涉及到的类型都写出来,用以匹配前两行的类型,并且这种情况下,函数的返回值类型可以省略,因为在第 1 、 2 行里已经定义过返回类型了。

任意值

如果实在不知道应该如何定义一个变量的类型, TypeScript 也允许使用任意值。

还记得在 为什么需要类型系统 的用的那个例子吗?再次放到 src/ts/index.ts 里:

ts
// 这段代码在 TS 里运行会报错
+function getFirstWord(msg) {
+  console.log(msg.split(' ')[0])
+}
+
+getFirstWord('Hello World')
+
+getFirstWord(123)

运行 npm run dev:ts 的时候,会得到一句报错 Parameter 'msg' implicitly has an 'any' type. ,意思是这个参数带有隐式 any 类型。

这里的 any 类型,就是 TypeScript 任意值。

既然报错是 “隐式” ,那 “显式” 的指定就可以了,当然,为了程序能够正常运行,还提高一下函数体内的代码健壮性:

ts
// 这里的入参显式指定了 any
+function getFirstWord(msg: any) {
+  // 这里使用了 String 来避免程序报错
+  console.log(String(msg).split(' ')[0])
+}
+
+getFirstWord('Hello World')
+
+getFirstWord(123)

这次就不会报错了,不论是传 string 还是 number 还是其他类型,都可以正常运行。

TIP

使用 any 的目的是让在开发的过程中,可以不必在无法确认类型的地方消耗太多时间,不代表不需要注意代码的健壮性。

一旦使用了 any ,代码里的逻辑请务必考虑多种情况进行判断或者处理兼容。

npm 包

虽然现在从 npm 安装的包都基本自带 TS 类型了,不过也存在一些包没有默认支持 TypeScript ,比如前面提到的 md5

在 TS 文件里导入并使用这个包的时候,会编译失败,比如在前面的 Hello TypeScript demo 里敲入以下代码:

ts
// src/ts/index.ts
+import md5 from 'md5'
+console.log(md5('Hello World'))

在命令行执行 npm run dev:ts 之后,会得到一段报错信息:

bash
src/ts/index.ts:1:17 - error TS7016:
+Could not find a declaration file for module 'md5'.
+'D:/Project/demo/hello-node/node_modules/md5/md5.js' implicitly has an 'any' type.
+  Try \`npm i --save-dev @types/md5\` if it exists
+  or add a new declaration (.d.ts) file
+  containing \`declare module 'md5';\`
+
+1 import md5 from 'md5'
+                  ~~~~~

这是因为缺少 md5 这个包的类型定义,根据命令行的提示,安装 @types/md5 这个包。

这是因为这些包是很早期用 JavaScript 编写的,因为功能够用作者也没有进行维护更新,所以缺少相应的 TS 类型,因此开源社区推出了一套 @types 类型包,专门处理这样的情况。

@types 类型包的命名格式为 @types/<package-name> ,也就是在原有的包名前面拼接 @types ,日常开发要用到的知名 npm 包都会有相应的类型包,只需要将其安装到 package.json 的 devDependencies 里即可解决该问题。

来安装一下 md5 的类型包:

bash
npm install -D @types/md5

再次运行就不会报错了!

bash
npm run dev:ts
+
+> demo@1.0.0 dev:ts
+> ts-node src/ts/index.ts
+
+b10a8db164e0754105b7a99be72e3fe5

类型断言

在讲解 函数的重载 的时候,提到了一个用法:

ts
const greeting = greet('Petter') as string

这里的 值 as 类型 就是 TypeScript 类型断言的语法,它还有另外一个语法是 <类型>值

当一个变量应用了 联合类型 时,在某些时候如果不显式的指明其中的一种类型,可能会导致后续的代码运行报错。

这个时候就可以通过类型断言强制指定其中一种类型,以便程序顺利运行下去。

常见的使用场景

把函数重载时最开始用到的那个例子,也就是下面的代码放到 src/ts/index.ts 里:

ts
// 对单人或者多人打招呼
+function greet(name: string | string[]): string | string[] {
+  if (Array.isArray(name)) {
+    return name.map((n) => \`Welcome, \${n}!\`)
+  }
+  return \`Welcome, \${name}!\`
+}
+
+// 虽然已知此时应该是 string[]
+// 但 TypeScript 还是会认为这是 string | string[]
+const greetings = greet(['Petter', 'Tom', 'Jimmy'])
+
+// 会导致无法使用 join 方法
+const greetingSentence = greetings.join(' ')
+console.log(greetingSentence)

执行 npm run dev:ts ,可以清楚的看到报错原因,因为 string 类型不具备 join 方法。

bash
src/ts/index.ts:11:31 - error TS2339:
+Property 'join' does not exist on type 'string | string[]'.
+  Property 'join' does not exist on type 'string'.
+
+11 const greetingStr = greetings.join(' ')
+                                 ~~~~

此时利用类型断言就可以达到目的:

ts
// 对单人或者多人打招呼
+function greet(name: string | string[]): string | string[] {
+  if (Array.isArray(name)) {
+    return name.map((n) => \`Welcome, \${n}!\`)
+  }
+  return \`Welcome, \${name}!\`
+}
+
+// 已知此时应该是 string[] ,所以用类型断言将其指定为 string[]
+const greetings = greet(['Petter', 'Tom', 'Jimmy']) as string[]
+
+// 现在可以正常使用 join 方法
+const greetingSentence = greetings.join(' ')
+console.log(greetingSentence)

需要注意的事情

但是,请不要滥用类型断言,只在能够确保代码正确的情况下去使用它,来看一个反例:

ts
// 原本要求 age 也是必须的属性之一
+interface User {
+  name: string
+  age: number
+}
+
+// 但是类型断言过程中,遗漏了
+const petter = {} as User
+petter.name = 'Petter'
+
+// TypeScript 依然可以运行下去,但实际上的数据是不完整的
+console.log(petter) // { name: 'Petter' }

TIP

使用类型断言可以让 TypeScript 不再检查该代码,默认是正确无误的。

所以,请务必保证这段代码真的是正确的!

类型推论

还记得在讲 原始数据类型 的时候,最后提到的:

不过在实际的编程过程中,原始数据类型的类型定义是可以省略的,因为 TypeScript 会根据声明变量时赋值的类型,自动帮推导变量类型

这其实是 TypeScript 的类型推论功能,当在声明变量的时候可以确认它的值,那么 TypeScript 也可以在这个时候帮推导它的类型,这种情况下就可以省略一些代码量。

下面这个变量这样声明是 OK 的,因为 TypeScript 会推导 msgstring 类型。

ts
// 相当于 msg: string
+let msg = 'Hello World'
+
+// 所以要赋值为 number 类型时会报错
+msg = 3 // Type 'number' is not assignable to type 'string'

下面这段代码也是可以正常运行的,因为 TypeScript 会根据 return 的结果推导 getRandomNumber 的返回值是 number 类型,从而推导变量 num 也是 number 类型。

ts
// 相当于 getRandomNumber(): number
+function getRandomNumber() {
+  return Math.round(Math.random() * 10)
+}
+
+// 相当于 num: number
+const num = getRandomNumber()

类型推论的前提是变量在声明时有明确的值,如果一开始没有赋值,那么会被默认为 any 类型。

ts
// 此时相当于 foo: any
+let foo
+
+// 所以可以任意改变类型
+foo = 1 // 1
+foo = true // true

类型推论可以节约很多书写工作量,在确保变量初始化有明确的值的时候,可以省略其类型,但必要的时候,该写上的还是要写上。

如何编译为 JavaScript 代码

学习到这里,对于 TypeScript 的入门知识已经学到了吧!

前面学习的时候,一直是基于 dev:ts 命令,它调用的是 ts-node 来运行的 TS 文件:

json
{
+  // ...
+  "scripts": {
+    // ...
+    "dev:ts": "ts-node src/ts/index.ts"
+  }
+  // ...
+}

但最终可能需要的是一个 JS 文件,比如要通过 <script src> 来放到 HTML 页面里,这就涉及到对 TypeScript 的编译。

来看看如何把一个 TS 文件编译成 JS 文件,让其从 TypeScript 变成 JavaScript 代码。

编译单个文件

先在 package.json 里增加一个 build script :

json
{
+  "name": "hello-node",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "dev:cjs": "node src/cjs/index.cjs",
+    "dev:esm": "node src/esm/index.mjs",
+    "dev:ts": "ts-node src/ts/index.ts",
+    "build": "tsc src/ts/index.ts --outDir dist",
+    "compile": "babel src/babel --out-dir compiled",
+    "serve": "node server/index.js"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "md5": "^2.3.0"
+  },
+  "devDependencies": {
+    "@types/md5": "^2.3.2",
+    "ts-node": "^10.7.0",
+    "typescript": "^4.6.3"
+  }
+}

这样在命令行运行 npm run build 的时候,就会把 src/ts/index.ts 这个 TS 文件编译,并输出到项目下与 src 文件夹同级的 dist 目录下。

其中 tsc 是 TypeScript 用来编译文件的命令, --outDir 是它的一个选项,用来指定输出目录,如果不指定,则默认生成到源文件所在的目录下面。

把之前在 函数的重载 用过的这个例子放到 src/ts/index.ts 文件里,因为它是一段比较典型的、包含了多个知识点的 TypeScript 代码:

ts
// 对单人或者多人打招呼
+function greet(name: string): string
+function greet(name: string[]): string[]
+function greet(name: string | string[]) {
+  if (Array.isArray(name)) {
+    return name.map((n) => \`Welcome, \${n}!\`)
+  }
+  return \`Welcome, \${name}!\`
+}
+
+// 单个问候语
+const greeting = greet('Petter')
+console.log(greeting)
+
+// 多个问候语
+const greetings = greet(['Petter', 'Tom', 'Jimmy'])
+console.log(greetings)

可以先执行 npm run dev:ts 测试它的可运行性,当然,如果期间的代码运行有问题,在编译阶段也会给报错。

现在来编译它,现在在命令行输入 npm run build 并回车执行。

可以看到多了一个 dist 文件夹,里面多了一个 index.js 文件。

bash
hello-node
+ # 构建产物
+├─dist
+  # 编译后的 JS 文件
+ └─index.js
+ # 依赖文件夹
+├─node_modules
+ # 源码文件夹
+├─src
+ # 锁定安装依赖的版本号
+├─package-lock.json
+ # 项目清单
+└─package.json

index.js 文件里面的代码如下:

js
function greet(name) {
+  if (Array.isArray(name)) {
+    return name.map(function (n) {
+      return 'Welcome, '.concat(n, '!')
+    })
+  }
+  return 'Welcome, '.concat(name, '!')
+}
+// 单个问候语
+var greeting = greet('Petter')
+console.log(greeting)
+// 多个问候语
+var greetings = greet(['Petter', 'Tom', 'Jimmy'])
+console.log(greetings)

可以看到已经成功把 TypeScript 代码编译成 JavaScript 代码了。

在命令行执行 node dist/index.js ,像之前测试 JS 文件一样使用 node 命令,运行 dist 目录下的 index.js 文件,它可以正确运行:

bash
node dist/index.js
+Welcome, Petter!
+[ 'Welcome, Petter!', 'Welcome, Tom!', 'Welcome, Jimmy!' ]

编译多个模块

刚才只是编译一个 index.ts 文件,如果 index.ts 里引入了其他模块,此时 index.ts 是作为入口文件,入口文件 import 进来使用的模块也会被 TypeScript 一并编译。

拆分一下模块,把 greet 函数单独抽离成一个模块文件 src/ts/greet.ts

ts
// src/ts/greet.ts
+function greet(name: string): string
+function greet(name: string[]): string[]
+function greet(name: string | string[]) {
+  if (Array.isArray(name)) {
+    return name.map((n) => \`Welcome, \${n}!\`)
+  }
+  return \`Welcome, \${name}!\`
+}
+
+export default greet

src/ts/index.ts 这边,把这个模块导进来:

ts
// src/ts/index.ts
+import greet from './greet'
+
+// 单个问候语
+const greeting = greet('Petter')
+console.log(greeting)
+
+// 多个问候语
+const greetings = greet(['Petter', 'Tom', 'Jimmy'])
+console.log(greetings)

package.json 里的 build script 无需修改,依然只编译 index.ts ,但因为导入了 greet.ts ,所以 TypeScript 也会一并编译,来试一下运行 npm run build , 现在 dist 目录下有两个文件了:

bash
hello-node
+ # 构建产物
+├─dist
+ ├─greet.js  # 多了这个文件
+ └─index.js
+
+ # 其他文件这里省略...
+└─package.json

来看看这一次的编译结果:

先看看 greet.js

js
// dist/greet.js
+'use strict'
+exports.__esModule = true
+function greet(name) {
+  if (Array.isArray(name)) {
+    return name.map(function (n) {
+      return 'Welcome, '.concat(n, '!')
+    })
+  }
+  return 'Welcome, '.concat(name, '!')
+}
+exports['default'] = greet

再看看 index.js

js
// dist/index.js
+'use strict'
+exports.__esModule = true
+var greet_1 = require('./greet')
+// 单个问候语
+var greeting = (0, greet_1['default'])('Petter')
+console.log(greeting)
+// 多个问候语
+var greetings = (0, greet_1['default'])(['Petter', 'Tom', 'Jimmy'])
+console.log(greetings)

这个代码风格有没有觉得似曾相识?是的,就是前面提到的 CommonJS 模块代码。

其实在 编译单个文件 代码的时候,它也是 CommonJS ,只不过因为只有一个文件,没有涉及到模块化,所以第一眼看不出来。

还是在命令行执行 node dist/index.js ,虽然也是运行 dist 目录下的 index.js 文件,但这次它的作用是充当一个入口文件了,引用到的 greet.js 模块文件也会被调用。

这次一样可以得到正确的结果:

bash
node dist/index.js
+Welcome, Petter!
+[ 'Welcome, Petter!', 'Welcome, Tom!', 'Welcome, Jimmy!' ]

修改编译后的 JS 版本

还可以修改编译配置,让 TypeScript 编译成不同的 JavaScript 版本。

修改 package.json 里的 build script ,在原有的命令后面增加一个 --target 选项:

json
{
+  // ...
+  "scripts": {
+    // ...
+    "build": "tsc src/ts/index.ts --outDir dist --target es6"
+  }
+  // ...
+}

--target 选项的作用是控制编译后的 JavaScript 版本,可选的值目前有: es3es5es6es2015es2016es2017es2018es2019es2020es2021es2022esnext ,分别对应不同的 JS 规范(所以未来的可选值会根据 JS 规范一起增加)。

之前编译出来的 JavaScript 是 CommonJS 规范 ,本次配置的是 es6 ,这是支持 ES Module 规范 的版本。

TIP

通常还需要配置一个 --module 选项,用于决定编译后是 CJS 规范还是 ESM 规范,但如果缺省,会根据 --target 来决定。

再次在命令行运行 npm run build ,这次看看变成了什么:

先看看 greet.js

js
// dist/greet.js
+function greet(name) {
+  if (Array.isArray(name)) {
+    return name.map((n) => \`Welcome, \${n}!\`)
+  }
+  return \`Welcome, \${name}!\`
+}
+export default greet

再看看 index.js

js
// dist/index.js
+import greet from './greet'
+// 单个问候语
+const greeting = greet('Petter')
+console.log(greeting)
+// 多个问候语
+const greetings = greet(['Petter', 'Tom', 'Jimmy'])
+console.log(greetings)

这次编译出来的都是基于 ES6 的 JavaScript 代码,因为涉及到 ESM 模块,所以不能直接在 node 运行它了,可以手动改一下扩展名,改成 .mjs (包括 index 文件里 import 的 greet 文件名也要改),然后再运行 node dist/index.mjs

其他事项

在尝试 编译单个文件编译多个模块 的时候,相信各位开发者应该没有太大的疑问,但是来到 修改编译后的 JS 版本 这里,事情就开始变得复杂了起来,应该能感觉到编译的选项和测试成本都相应的增加了很多。

事实上刚才编译的 JS 文件,因为涉及到 ESM 模块化,是无法通过普通的 <script /> 标签在 HTML 页面里使用的(单个文件可以,因为没有涉及模块),不仅需要加上 ESM 模块所需的 <script type="module" /> 属性,本地开发还需要启动本地服务器通过 HTTP 协议访问页面,才允许在浏览器里使用 ESM 模块(详见: 在浏览器里访问 ESM 一节)。

因此在实际的项目开发中,需要借助 构建工具 来处理很多编译过程中的兼容性问题,降低开发成本。

而刚才用到的诸如 --target 这样的选项,可以用一个更简单的方式来管理,类似于 package.json 项目清单, TypeScript 也有一份适用于项目的配置清单,请看 了解 tsconfig.json 部分。

了解 tsconfig.json

TypeScript 项目一般都会有一个 tsconfig.json 文件,放置于项目的根目录下,这个文件的作用是用来管理 TypeScript 在编译过程中的一些选项配置。

在开始之前,需要全局安装一下 TypeScript :

bash
npm install -g typescript

这样就可以使用 TypeScript 提供的全局功能,可以直接在命令行里使用 tsc 命令了(之前本地安装的时候,需要封装成 package.json 的 script 才能调用它)。

依然是用的 Hello TypeScript demo ,记得先通过 cd 命令进入项目所在的目录。

在命令行输入 tsc --init ,这是 TypeScript 提供的初始化功能,会生成一个默认的 tsconfig.json 文件。

bash
tsc --init
+
+Created a new tsconfig.json with:
+                                                                               TS
+  target: es2016
+  module: commonjs
+  strict: true
+  esModuleInterop: true
+  skipLibCheck: true
+  forceConsistentCasingInFileNames: true
+
+
+You can learn more at https://aka.ms/tsconfig.json

现在的目录结构是这样子,多了一个 tsconfig.json 文件:

bash
hello-node
+ # 构建产物
+├─dist
+ # 依赖文件夹
+├─node_modules
+ # 源码文件夹
+├─src
+ # 锁定安装依赖的版本号
+├─package-lock.json
+ # 项目清单
+├─package.json
+ # TypeScript 配置
+└─tsconfig.json

每一个 tsc 的命令行的选项,都可以作为这个 JSON 的一个字段来管理,例如刚才的 --outDir--target 选项,在这个 JSON 文件里对应的就是:

json
{
+  "compilerOptions": {
+    "target": "es6",
+    "module": "es6",
+    "outDir": "./dist"
+  }
+}

可以直接在生成的 tsconfig.json 上面修改。

来试试效果,这一次不需要用到 package.json 里的 build script 了,直接在命令行运行 tsc ,它现在会根据配置的 tsconfig.json 文件,按照的要求来编译。

可以看到它依然按照要求在 dist 目录下生成编译后的 JS 文件,而且这一次的编译结果,和在 build script 里使用的 tsc src/ts/index.ts --outDir dist --target es6 这一长串命令是一样的。

所以正常工作中,都是使用 tsconfig.json 来管理 TypeScript 的配置的。

完整的选项可以查看 TypeScript 官网: tsconfig - typescriptlang

不过实际工作中的项目都是通过一些脚手架创建的,例如 Vue CLI ,或者现在的 Create Vue 或者 Create Preset ,都会在创建项目模板的时候,提前配置好通用的选项,只需要在不满足条件的情况下去调整。

`,191),q=JSON.parse('{"title":"快速上手 TypeScript","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"typescript.md","filePath":"typescript.md"}'),E={name:"typescript.md"},S=Object.assign(E,{setup(B){const t="``";return(f,b)=>{const c=l("ImgWrap"),o=l("ClientOnly"),r=l("GitalkComment");return y(),D("div",null,[C,a(o,null,{default:e(()=>[a(c,{src:"/assets/img/github-top-languages.jpg",dark:"/assets/img/github-top-languages-dark.jpg",alt:"TypeScript 的流行程度(来自 GitHub 年度统计报告)"})]),_:1}),A,s("div",{class:"tip custom-block"},[i,s("p",null,[n("注意在上面 "),d,n(" 函数的 "),g,n(" 语句里,使用了一个 "),s("code",null,F(t)),n(" 符号来定义字符串,这是 ES6 语法里的 "),h,n(" ,它和传统的单引号 / 双引号相比更为灵活,特别是遇到字符串需要配合多变量拼接和换行的情况。")]),u]),m,a(o,null,{default:e(()=>[a(r,{issueId:193})]),_:1})])}}});export{q as __pageData,S as default}; diff --git a/assets/upgrade.md.25cf659f.js b/assets/upgrade.md.25cf659f.js new file mode 100644 index 00000000..5d8981ad --- /dev/null +++ b/assets/upgrade.md.25cf659f.js @@ -0,0 +1,190 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as c,v as i,b as D,t as s,O as n,R as t,F as e,L as o,M as p}from"./chunks/framework.0d8bea05.js";const q=JSON.parse('{"title":"脚手架的升级与配置","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"upgrade.md","filePath":"upgrade.md"}'),y={name:"upgrade.md"},C=t('

脚手架的升级与配置

相信在 “了解前端工程化” 一章里阅读过 Vue.js 与工程化 一节内容的开发者,可以轻松猜到本书接下来关于 Vue 3 的学习都将基于前端工程化展开,本章将介绍如何配置 Vue 3 的开发环境,并创建基于前端工程化的 Vue 3 项目。

TIP

如果还不熟悉 Node.js 、 npm 依赖管理等前端工程化工具链的使用,请先阅读 工程化的前期准备 一章。

全新的 Vue 版本 ~new

在 2022 年 2 月 7 日, Vue 3 代替了 Vue 2 成为 Vue 的默认版本,有一些注意事项需要留意:

使用 Vue 3

在 npmjs 网站 Vue 主页的版本列表 上面,可以看到当前已使用 3.x.x 这样的版本号作为 latest 这个 Tag 对应的版本,也就是运行 npm i vue 默认会安装 Vue 3 了,无需再和以前一样,需要指定 vue@next 才可以安装到 Vue 3 。

',7),F=t(`

包括 vue-routervuexvue-loader@vue/test-utils 等相关的生态,同样不需要指定 next 版本了,都配合 Vue 3 指定了新的 latest 默认版本。

同时 Vue 生态的所有官方文档也都默认切换到 Vue 3 版本,可在 官方文档 一节了解最新的官方资源站点。

使用 Vue 2

如果还需要使用 Vue 2 ,则在安装的时候需要手动指定 Tag 为 legacy 或者 v2-latest 才能安装到 Vue 2 :

bash
# 安装 2.6.x 的最新版本
+npm i vue@legacy
+
+# 安装 2.7.x 的最新版本
+npm i vue@v2-latest

注意到 Vue 2 配对了两个不同的 Tag ,分别对应 2.7 系列和 2.6 系列。

TIP

Vue 2.7 系列是在 Vue 2 的基础上,对标 Vue 3 的功能支持所作的升级,主要是面向想使用 Vue 3 的新特性、但顾虑于产品对旧浏览器的支持而无法贸然升级的开发者。

Vue 2.7 与 Vue 2.6 之前的旧版本在使用上略有不同,具体可以查看 Vue 2 的 更新记录 了解具体的差异化。

对于一些没有打 Tag 的 Vue 2 相关生态(如 vuex 截止到撰写本文时还没有为旧版本打 Tag ),则需要显式的指定版本号才可以安装到配套的程序:

bash
# 显式的指定具体版本号安装
+npm i vuex@3.6.2

如果之前使用了 latest 标签或 * 从 npm 安装 Vue 或其他官方库,请确保项目下的 package.json 文件能够明确使用兼容 Vue 2 的版本。

diff
{
+  "dependencies": {
+-   "vue": "latest",
++   "vue": "^2.6.14",
+-   "vue-router": "latest",
++   "vue-router": "^3.5.3",
+-   "vuex": "latest"
++   "vuex": "^3.6.2"
+  },
+  "devDependencies": {
+-   "vue-loader": "latest",
++   "vue-loader": "^15.9.8",
+-   "@vue/test-utils": "latest"
++   "@vue/test-utils": "^1.3.0"
+  }
+}

TIP

上方代码块里的 - 号代表移除, + 号代表新增,这是一种 Diff 风格的排版,表明修改前后的变化,后文如有类似的代码风格同理。

Hello Vue3

如果想早点开始 Vue 3 的世界,可以通过以下命令直接创建一个启动项目:

bash
# 全局安装脚手架
+npm install -g create-preset
+
+# 使用 \`vue3-ts-vite\` 模板创建一个名为 \`hello-vue3\` 的项目
+preset init hello-vue3 --template vue3-ts-vite

这是一个基于 Vite + TypeScript + Vue 3 + Pinia 的项目启动模板,可以使用这个项目来练习后面的案例代码,创建完毕后可以直接跳到 安装 VSCode添加 VSCode 插件 继续学习。

当然有时间还是希望继续阅读 使用 Vite 创建项目使用 @vue/cli 创建项目 这两部分内容,了解 Vue 3 更主流的项目创建方案。

TIP

如果网络问题下载失败,可以先执行 preset proxy on 开启加速镜像代理下载。

使用 Vite 创建项目 ~new

Vite 是由 Vue 作者尤雨溪先生带领团队开发的一个构建工具,它利用浏览器原生支持 ES 模块的特点,极大提升了开发体验,自 2021 年 1 月份发布 2.0 版本以来,发展非常快,笔者也在第一时间参与贡献了一些文档和插件,并且在 2021 年期间,个人项目已经全面切换到 Vite ,公司业务也在 2021 年底开始使用 Vite 创建新项目,整体情况非常稳定,前景非常乐观。

关于是否使用 Vite 和安利团队使用 Vue 3 ,可以看笔者在 2022 年春节前写的 Markdown 工程师的一周 一文,记录了一次关于团队技术栈升级的总结,笔者是非常推荐升级技术栈的!

在这里推荐以下这几种创建 Vite 项目的方式:Create ViteCreate VueCreate Preset

Create Vite

create-vite 是 Vite 官方推荐的一个脚手架工具,可以创建基于 Vite 的不同技术栈基础模板。

运行以下命令创建模板项目,再按照命令行的提示操作(选择 vue 技术栈进入),即可创建一个基于 Vite 的基础空项目。

bash
npm create vite

不过这个方式创建的项目非常基础,如果需要用到 Router 、 Vuex 、 ESLint 等程序,都需要再单独安装和配置,所以推荐使用 Create Preset

Create Vue

create-vue 是 Vue 官方推出的一个新脚手架,用以代替基于 Webpack 的 Vue CLI ,它可以创建基于 Vite 的 Vue 基础模板。

运行以下命令创建模板项目,然后根据命令行的提示操作即可。

bash
npm init vue@3

Create Preset

create-preset 是 Awesome Starter 的 CLI 脚手架,提供快速创建预设项目的能力,可以创建一些有趣实用的项目启动模板,也可以用来管理的常用项目配置。

简单使用

可以通过包管理器直接创建配置,然后按照命令行的提示操作,即可创建开箱即用的模板项目。

bash
npm create preset

在这里选择 vue 技术栈进入,选择 vue3-ts-vite 创建一个基于 Vite + Vue 3 + TypeScript 的项目启动模板。

TIP

如果下载失败,可以通过 npm create preset proxy on 开启加速镜像代理下载。

点击查看:代理选项 - Create Preset

全局安装

也可以像使用 @vue/cli 一样,全局安装到本地,通过 preset init 命令来创建项目。

推荐全局安装它,用起来更方便,请先全局安装:

bash
npm install -g create-preset

可以通过下面这个命令来检查安装是否成功,如果成功,将会得到一个版本号:

bash
preset -v

然后可以通过 --template 选项直接指定一个模板创建项目,在这里使用 vue3-ts-vite 模板创建一个名为 hello-vue3 的项目:

bash
preset init hello-vue3 --template vue3-ts-vite

常用的项目模板也可以绑定为本地配置,点击 Create Preset 官方文档 查看完整使用教程。

管理项目配置

不论使用上方哪种方式创建项目,在项目的根目录下都会有一个名为 vite.config.jsvite.config.ts 的项目配置文件(其扩展名由项目使用 JavaScript 还是 TypeScript 决定)。

里面会有一些预设好的配置,可以在 Vite 官网的配置文档 查阅更多的可配置选项。

使用 @vue/cli 创建项目

如果不习惯 Vite ,依然可以使用 Vue CLI 作为开发脚手架。

和 Vite 的区别

Vue CLI 使用的构建工具是基于 Webpack ,可以在 了解构建工具 一节了解 Webpack 和 Vite 这两个构建工具的区别。

更新 CLI 脚手架

请先全局安装,把脚手架更新到最新版本(最低版本要求在 4.5.6 以上才能支持 Vue 3 项目的创建)。

js
npm install -g @vue/cli

使用 CLI 创建 3.x 项目 ~new

Vue CLI 全局安装后,可以在命令行输入 vue 进行操作,创建项目使用的是 create 命令:

js
vue create hello-vue3

由于要使用 TypeScript ,所以需要选择最后一个选项来进行自定义搭配,通过键盘的上下箭头进行切换选择:

js
Vue CLI v5.0.4
+? Please pick a preset:
+  Default ([Vue 3] babel, eslint)
+  Default ([Vue 2] babel, eslint)
+> Manually select features

多选菜单可以按空格选中需要的依赖,总共选择了下面这些选项:

js
Vue CLI v5.0.4
+? Please pick a preset: Manually select features
+? Check the features needed for your project: (Press <space> to select,
+<a> to toggle all, <i> to invert selection, and <enter> to proceed)
+ (*) Babel
+ (*) TypeScript
+ ( ) Progressive Web App (PWA) Support
+ (*) Router
+ (*) Vuex
+ (*) CSS Pre-processors
+>(*) Linter / Formatter
+ ( ) Unit Testing
+ ( ) E2E Testing

选择 Vue 版本,要用 Vue 3 所以需要选择 3.x :

js
? Choose a version of Vue.js that you want to start the project with
+  (Use arrow keys)
+> 3.x
+  2.x

是否选择 Class 语法的模板,在 Vue 2 版本为了更好的支持 TypeScript ,通常需要使用 Class 语法,由于 Vue 3 有了对 TypeScript 支持度更高的 Composition API ,因此选择 n ,也就是 “否” :

js
? Use class-style component syntax? (y/N) n

Babel 可以把新版本的 JavaScript 语句转换为兼容性更好的低版本 Polyfill 写法,所以选 y 确认使用:

js
? Use Babel alongside TypeScript
+  (required for modern mode, auto-detected polyfills, transpiling JSX)?
+  (Y/n) y

接下来是选择路由模式,选 y 启用 History 模式,选 n 使用 Hash 模式,可根据项目情况选择。

建议先选 y 确认,如果遇到部署的问题可以在 “路由” 一章的 部署问题与服务端配置 小节查看如何处理。

js
? Use history mode for router?
+  (Requires proper server setup for index fallback in production)
+  (Y/n) y

选择一个 CSS 预处理器,可以根据自己的喜好选择,不过鉴于目前开源社区组件常用的都是 Less ,所以也建议选择 Less 作为入门的预处理器工具。

js
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported
+ by default):
+  Sass/SCSS (with dart-sass)
+> Less
+  Stylus

Lint 规则,用来代码检查,写 TypeScript 离不开 Lint ,可以根据自己喜好选择,也可以先选择默认,后面在 添加协作规范 一节也有说明如何配置规则,这里先默认第一个:

js
? Pick a linter / formatter config: (Use arrow keys)
+> ESLint with error prevention only
+  ESLint + Airbnb config
+  ESLint + Standard config
+  ESLint + Prettier

Lint 的校验时机,一个是在保存时校验,一个是在提交 commit 的时候才校验,这里也选默认:

js
? Pick additional lint features: (Press <space> to select,
+ <a> to toggle all, <i> to invert selection, and <enter> to proceed)
+>(*) Lint on save
+ ( ) Lint and fix on commit

项目配置文件,笔者更习惯保存为独立文件:

js
? Where do you prefer placing config for Babel, ESLint, etc.?
+  (Use arrow keys)
+> In dedicated config files
+  In package.json

是否保存为未来的项目配置,存起来方便以后快速创建:

js
? Save this as a preset for future projects? Yes
+? Save preset as: vue-3-ts-config

至此,项目创建完成!可以通过 npm run serve 开启热更进行开发调试,通过 npm run build 构建打包上线。

管理项目配置

Vue CLI 的配置文件是 vue.config.js ,可以参考官网的说明文档调整各个选项配置:配置参考 - Vue CLI

调整 TS Config

如果在 Vite 的配置文件 vite.config.ts ,或者是在 Vue CLI 的配置文件 vue.config.js 里设置了 alias 的话,因为 TypeScript 不认识里面配置的 alias 别名,所以需要再对 tsconfig.json 做一点调整,增加对应的 paths ,否则在 VSCode 里可能会路径报红,提示找不到模块或其相应的类型声明。

比如在 Vue 组件里引入路径为 @cp/HelloWorld.vue 的时候,可以避免写出 ../../../../components/HelloWorld.vue 这样的非常多层级的相对路径,但是默认情况下 TypeScript 并不知道这个 alias 等价于 src/components/HelloWorld.vue 这个文件路径,从而会报错找不到该模块并导致无法正确编译。

假设在 vite.config.ts 里配置了这些 alias :

ts
export default defineConfig({
+  // ...
+  resolve: {
+    alias: {
+      '@': resolve('src'), // 源码根目录
+      '@img': resolve('src/assets/img'), // 图片
+      '@less': resolve('src/assets/less'), // 预处理器
+      '@libs': resolve('src/libs'), // 本地库
+      '@plugins': resolve('src/plugins'), // 本地插件
+      '@cp': resolve('src/components'), // 公共组件
+      '@views': resolve('src/views'), // 路由组件
+    },
+  },
+  // ...
+})

那么在该项目的 tsconfig.json 文件里就需要相应的加上这些 paths :

json
{
+  "compilerOptions": {
+    // ...
+    "paths": {
+      "@/*": ["src/*"],
+      "@img/*": ["src/assets/img/*"],
+      "@less/*": ["src/assets/less/*"],
+      "@libs/*": ["src/libs/*"],
+      "@plugins/*": ["src/plugins/*"],
+      "@cp/*": ["src/components/*"],
+      "@views/*": ["src/views/*"]
+    },
+    // ...
+  },
+  // ...
+}

TIP

注意 paths 的配置全部要以 /* 结尾,代表该目录下的文件都可以被匹配,而不是指向某一个文件。

添加协作规范

考虑到后续可能会有团队协作,最好是能够统一编码风格。

Editor Config

在项目根目录下再增加一个名为 .editorconfig 的文件。

这个文件的作用是强制编辑器以该配置来进行编码,比如缩进统一为空格而不是 Tab ,每次缩进都是 2 个空格而不是 4 个等等。

文件内容如下:

js
# http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+max_line_length = 80
+trim_trailing_whitespace = true
+
+[*.md]
+max_line_length = 0
+trim_trailing_whitespace = false

具体的参数说明可参考:项目代码风格统一神器 editorconfig 的作用与配置说明

TIP

部分编辑器可能需要安装对应的插件才可以支持该配置。

例如 VSCode 需要安装 EditorConfig for VSCode 扩展

Prettier

Prettier 是目前最流行的代码格式化工具,可以约束的代码风格不会乱七八糟,目前所知道的知名项目(如 Vue 、 Vite 、 React 等)和大厂团队(谷歌、微软、阿里、腾讯等)都在使用 Prettier 格式化代码。

通过脚手架创建的项目很多都内置了 Prettier 功能集成(例如 Create Preset ,参考了主流的格式化规范,比如 2 个空格的缩进、无需写分号结尾、数组 / 对象每一项都带有尾逗号等等)。

如果需要手动增加功能支持,请在项目根目录下创建一个名为 .prettierrc 的文件,写入以下内容:

json
{
+  "semi": false,
+  "singleQuote": true
+}

这代表 JavaScript / TypeScript 代码一般情况下不需要加 ; 分号结尾,然后使用 '' 单引号来定义字符串等变量。

这里只需要写入与默认配置不同的选项即可,如果和默认配置一致,可以省略,完整的配置选项以及默认值可以在 Prettier 官网的 Options Docs 查看。

配合 VSCode 的 VSCode Prettier 扩展,可以在编辑器里使用该规则格式化文件(此时无需在项目下安装 Prettier 依赖)。

如果开启了 ESLint ,配合 ESLint 的代码提示,可以更方便的体验格式化排版,详见 ESLint 一节的说明。

TIP

配合 VSCode Prettier 扩展 ,这份配置直接在 VSCode 里生效,如果配合 ESLint 使用,需要安装 Prettier 依赖。

ESLint

ESLint 是一个查找 JavaScript / TypeScript 代码问题并提供修复建议的工具,换句话说就是可以约束的代码不会写出一堆 BUG ,它是代码健壮性的重要保障。

虽然大部分前端开发者都不愿意接受这些约束(当年笔者入坑的时候也是),但说实话,经过 ESLint 检查过的代码质量真的高了很多,如果不愿意总是做一个游兵散勇,建议努力让自己习惯被 ESLint 检查,大厂和大项目都是有 ESLint 检查的。

特别是写 TypeScript ,配合 ESLint 的检查实在太爽了(字面意思,真的很舒服)。

通过脚手架创建的项目通常都会帮配置好 ESLint 规则,如果有一些项目是一开始没有,后面想增加 ESLint 检查,也可以手动配置具体规则。

这里以一个 Vite + TypeScript + Prettier 的 Vue 3 项目为例,在项目根目录下创建一个名为 .eslintrc.js 文件,写入以下内容:

js
module.exports = {
+  root: true,
+  env: {
+    node: true,
+    browser: true,
+  },
+  extends: ['plugin:vue/vue3-essential', 'eslint:recommended', 'prettier'],
+  parser: 'vue-eslint-parser',
+  parserOptions: {
+    parser: '@typescript-eslint/parser',
+    ecmaVersion: 2020,
+    sourceType: 'module',
+  },
+  plugins: ['@typescript-eslint', 'prettier'],
+  rules: {
+    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+    'prettier/prettier': 'warn',
+    'vue/multi-word-component-names': 'off',
+  },
+  globals: {
+    defineProps: 'readonly',
+    defineEmits: 'readonly',
+    defineExpose: 'readonly',
+    withDefaults: 'readonly',
+  },
+}

然后安装对应的依赖(记得添加 -D 参数添加到 devDependencies ,因为都是开发环境下使用的):

这样就可以在项目中生效了(如果 VSCode 未能立即生效,重启编辑器即可),一旦代码有问题, ESLint 就会帮检查出来并反馈具体的报错原因,久而久之的代码就会越写越规范。

更多的选项可以在 ESLint 官网的 Configuring ESLint 查阅。

如果有一些文件需要排除检查,可以再创建一个 .eslintignore 文件在项目根目录下,里面添加要排除的文件或者文件夹名称:

txt
dist/*

更多的排除规则可以在 ESLint 官网的 The .eslintignore File 一文查阅。

安装 VSCode

要问现在前端工程师用的最多的代码编辑器是哪个,肯定是 Visual Studio Code 了!

与其他的编辑器相比,有这些优点:

  • 背靠 Microsoft ,完全免费并且开源,开箱即用
  • 可以通过简单的配置调整来满足之前在其他编辑器上的习惯( e.g. Sublime Text )
  • 轻量级但功能强大,内置了对 JavaScript、TypeScript 和 Node.js 的支持,
  • 丰富的插件生态,可以根据的需要,安装提高编码效率的功能支持,以及其他的语言扩展
  • 智能的代码补全、类型推导、代码检查提示、批量编辑、引用跳转、比对文件等功能支持
  • 登录的 GitHub 账号即可实现配置自动同步,在其他电脑上直接使用的最习惯配置和插件

当然,还有非常多优点可自行体验!

点击下载:Visual Studio Code

一般情况下开箱即用,无门槛,也可以阅读官方文档了解一些个性化的配置。

点击查看:VSCode 操作文档

添加 VSCode 插件

VSCode 本身是轻量级的,也就是只提供最基础的功能,更优秀的体验或者个性化体验,是需要通过插件来启用的。

这里推荐几个非常舒服的 VSCode 插件,可以通过插件中心安装,也可以通过官方应用市场下载。

Chinese (Simplified)

VSCode 安装后默认是英文本,需要自己进行汉化配置, VSCode 的特色就是插件化处理各种功能,语言方面也一样。

安装该插件并启用,即可让 VSCode 显示为简体中文。

点击下载:Chinese (Simplified)

Volar

Vue 官方推荐的 VSCode 扩展,用以代替 Vue 2 时代的 Vetur ,提供了 Vue 3 的语言支持、 TypeScript 支持、基于 vue-tsc 的类型检查等功能。

点击下载:Volar

TIP

Volar 取代了 Vetur 作为 Vue 3 的官方扩展,如果之前已经安装了 Vetur ,请确保在 Vue 3 的项目中禁用它。

Vue VSCode Snippets

从实际使用 Vue 的角度提供 Vue 代码片段的生成,可以通过简单的命令,在 .vue 文件里实现大篇幅的代码片段生成,例如:

  1. 输入 ts 可以快速创建一个包含了 template + script + style 的 Vue 组件模板(可选 2.x 、3.x 以及 class 风格的模板)

  2. 也可以通过输入带有 v3 开头的指令来快速生成 Vue 3 的 API 。

下面是输入了 ts 两个字母之后,用箭头选择 vbase-3-ts 自动生成的一个模板片段,在开发过程中非常省事:

vue
<template>
+  <div></div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup() {
+    return {}
+  },
+})
+</script>
+
+<style scoped></style>

点击下载:Vue VSCode Snippets

Auto Close Tag

可以快速完成 HTML 标签的闭合,除非通过 .jsx / .tsx 文件编写 Vue 组件,否则在 .vue 文件里写 template 的时候肯定用得上。

点击下载:Auto Close Tag

Auto Rename Tag

假如要把 div 修改为 section,不需要再把 <div> 然后找到代码尾部的 </div> 才能修改,只需要选中前面或后面的半个标签直接修改,插件会自动把闭合部分也同步修改,对于篇幅比较长的代码调整非常有帮助。

点击下载:Auto Rename Tag

EditorConfig for VSCode

一个可以让编辑器遵守协作规范的插件,详见 添加协作规范

点击下载:EditorConfig for VSCode

Prettier for VSCode

这是 Prettier 在 VSCode 的一个扩展,不论项目有没有安装 Pretter 依赖,安装该扩展之后,单纯在 VSCode 也可以使用 Pretter 进行代码格式化。

点击下载:Prettier - Code formatter

点击访问:Prettier 官网 了解更多配置。

ESLint for VSCode

这是 ESLint 在 VSCode 的一个扩展, TypeScript 项目基本都开了 ESLint ,编辑器也建议安装该扩展支持以便获得更好的代码提示。

点击下载:VSCode ESLint

点击访问:ESLint 官网 了解更多配置。

其他插件

其他的比如预处理器相关的,Git 相关的,可以根据自己的需求在 VSCode 的插件市场 里搜索安装。

项目初始化

至此,通过脚手架已经搭好了一个可直接运行的基础项目,已经可以正常的 npm run devnpm run build 了(具体命令取决于的项目 脚本命令的配置 ),项目配置和编辑器也都弄好了,是不是可以开始写代码了呢?

请不要着急,还需要了解一点东西,就是如何初始化一个 Vue 3 项目。

因为在实际开发过程中,还会用到各种 npm 包,像很多 UI 框架、功能插件的引入都是需要在 Vue 初始化阶段处理。

甚至有时候还要脱离脚手架,采用 CDN 引入的方式来开发,所以开始写组件之前,还需要了解一下在 Vue 3 项目中,初始化阶段对比 Vue 2 的一些变化。

入口文件

项目的初始化都是在入口文件集中处理,Vue 3 的目录结构对比 Vue 2 没有变化,入口文件依然还是 main.ts 这个文件。

但是 Vue 3 在初始化的时候,做了不少的调整,代码写法和 Vue 2 是完全不同,但是对于这次大改动,笔者认为是好的,因为统一了相关生态的启用方式,不再像 Vue 2 时期那样多方式共存,显得比较杂乱。

回顾 Vue 2

Vue 2 在导入各种依赖之后,通过 new Vue() 执行 Vue 的初始化,相关的 Vue 生态和插件,有的是使用 Vue.use() 来进行初始化,有的是作为 new Vue() 的入参:

ts
import Vue from 'vue'
+import App from './App.vue'
+import router from './router'
+import store from './store'
+import pluginA from 'pluginA'
+import pluginB from 'pluginB'
+import pluginC from 'pluginC'
+
+// 使用了 \`use\` 方法激活
+Vue.use(pluginA)
+Vue.use(pluginB)
+Vue.use(pluginC)
+
+Vue.config.productionTip = false
+
+// 作为 \`new Vue()\` 的入参激活
+new Vue({
+  router,
+  store,
+  render: (h) => h(App),
+}).$mount('#app')

了解 Vue 3 ~new

在 Vue 3 ,使用 createApp 执行 Vue 的初始化,另外不管是 Vue 生态里的东西,还是外部插件、 UI 框架,统一都是由 use() 进行激活,非常统一和简洁:

ts
import { createApp } from 'vue'
+import App from './App.vue'
+import router from './router'
+import store from './store'
+import pluginA from 'pluginA'
+import pluginB from 'pluginB'
+import pluginC from 'pluginC'
+
+createApp(App)
+  .use(store)
+  .use(router)
+  .use(pluginA)
+  .use(pluginB)
+  .use(pluginC)
+  .mount('#app')

Vue Devtools

Vue Devtools 是一个浏览器扩展,支持 Chrome 、 Firefox 等浏览器,需要先安装才能使用。

点击安装:Vue Devtools 的浏览器扩展

当在 Vue 项目通过 npm run dev 等命令启动开发环境服务后,访问本地页面(如: http://localhost:3000/ ),在页面上按 F12 唤起浏览器的控制台,会发现多了一个名为 vue 的面板。

面板的顶部有一个菜单可以切换不同的选项卡,菜单数量会根据不同项目有所不同,例如没有安装 Pinia 则不会出现 Pinia 选项卡,这里以其中一部分选项卡作为举例。

Components 是以结构化的方式显示组件的调试信息,可以查看组件的父子关系,并检查组件的各种内部状态:

`,191),A=e("p",null,"Routes 可以查看当前所在路由的配置信息:",-1),d=e("p",null,"Timeline 是以时间线的方式追踪不同类型的数据,例如事件:",-1),u=e("p",null,"Pinia 是可以查看当前组件引入的全局状态情况:",-1),h=e("p",null,[o("可以在 "),e("a",{href:"https://devtools.vuejs.org/",target:"_blank",rel:"noreferrer"},"Vue Devtools 官网"),o(" 了解更多的用法。")],-1);function g(m,f,v,b,V,k){const l=p("ImgWrap"),a=p("ClientOnly"),r=p("GitalkComment");return i(),D("div",null,[C,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/vue-versions-on-npmjs.jpg",alt:"Vue 在 npmjs 上的版本列表"})]),_:1}),F,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/vue-devtools-components.jpg",dark:"/assets/img/vue-devtools-components-dark.jpg",alt:"Vue Devtools 的 Components 界面"})]),_:1}),A,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/vue-devtools-routes.jpg",dark:"/assets/img/vue-devtools-routes-dark.jpg",alt:"Vue Devtools 的 Routes 界面"})]),_:1}),d,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/vue-devtools-timeline.jpg",dark:"/assets/img/vue-devtools-timeline-dark.jpg",alt:"Vue Devtools 的 Timeline 界面"})]),_:1}),u,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/vue-devtools-pinia.jpg",dark:"/assets/img/vue-devtools-pinia-dark.jpg",alt:"Vue Devtools 的 Pinia 界面"})]),_:1}),h,s(a,null,{default:n(()=>[s(r,{issueId:45})]),_:1})])}const S=c(y,[["render",g]]);export{q as __pageData,S as default}; diff --git a/assets/upgrade.md.25cf659f.lean.js b/assets/upgrade.md.25cf659f.lean.js new file mode 100644 index 00000000..5d8981ad --- /dev/null +++ b/assets/upgrade.md.25cf659f.lean.js @@ -0,0 +1,190 @@ +/** + * name: learning-vue3 + * version: v2.0.0 + * description: A starting learning tutorial on Vue 3.0 + TypeScript, suitable for complete Vue novices and Vue 2.0 veterans, incorporating some of my own practical experience on the basis of official documents. + * author: chengpeiquan + * homepage: https://vue3.chengpeiquan.com + */ +import{_ as c,v as i,b as D,t as s,O as n,R as t,F as e,L as o,M as p}from"./chunks/framework.0d8bea05.js";const q=JSON.parse('{"title":"脚手架的升级与配置","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"upgrade.md","filePath":"upgrade.md"}'),y={name:"upgrade.md"},C=t('

脚手架的升级与配置

相信在 “了解前端工程化” 一章里阅读过 Vue.js 与工程化 一节内容的开发者,可以轻松猜到本书接下来关于 Vue 3 的学习都将基于前端工程化展开,本章将介绍如何配置 Vue 3 的开发环境,并创建基于前端工程化的 Vue 3 项目。

TIP

如果还不熟悉 Node.js 、 npm 依赖管理等前端工程化工具链的使用,请先阅读 工程化的前期准备 一章。

全新的 Vue 版本 ~new

在 2022 年 2 月 7 日, Vue 3 代替了 Vue 2 成为 Vue 的默认版本,有一些注意事项需要留意:

使用 Vue 3

在 npmjs 网站 Vue 主页的版本列表 上面,可以看到当前已使用 3.x.x 这样的版本号作为 latest 这个 Tag 对应的版本,也就是运行 npm i vue 默认会安装 Vue 3 了,无需再和以前一样,需要指定 vue@next 才可以安装到 Vue 3 。

',7),F=t(`

包括 vue-routervuexvue-loader@vue/test-utils 等相关的生态,同样不需要指定 next 版本了,都配合 Vue 3 指定了新的 latest 默认版本。

同时 Vue 生态的所有官方文档也都默认切换到 Vue 3 版本,可在 官方文档 一节了解最新的官方资源站点。

使用 Vue 2

如果还需要使用 Vue 2 ,则在安装的时候需要手动指定 Tag 为 legacy 或者 v2-latest 才能安装到 Vue 2 :

bash
# 安装 2.6.x 的最新版本
+npm i vue@legacy
+
+# 安装 2.7.x 的最新版本
+npm i vue@v2-latest

注意到 Vue 2 配对了两个不同的 Tag ,分别对应 2.7 系列和 2.6 系列。

TIP

Vue 2.7 系列是在 Vue 2 的基础上,对标 Vue 3 的功能支持所作的升级,主要是面向想使用 Vue 3 的新特性、但顾虑于产品对旧浏览器的支持而无法贸然升级的开发者。

Vue 2.7 与 Vue 2.6 之前的旧版本在使用上略有不同,具体可以查看 Vue 2 的 更新记录 了解具体的差异化。

对于一些没有打 Tag 的 Vue 2 相关生态(如 vuex 截止到撰写本文时还没有为旧版本打 Tag ),则需要显式的指定版本号才可以安装到配套的程序:

bash
# 显式的指定具体版本号安装
+npm i vuex@3.6.2

如果之前使用了 latest 标签或 * 从 npm 安装 Vue 或其他官方库,请确保项目下的 package.json 文件能够明确使用兼容 Vue 2 的版本。

diff
{
+  "dependencies": {
+-   "vue": "latest",
++   "vue": "^2.6.14",
+-   "vue-router": "latest",
++   "vue-router": "^3.5.3",
+-   "vuex": "latest"
++   "vuex": "^3.6.2"
+  },
+  "devDependencies": {
+-   "vue-loader": "latest",
++   "vue-loader": "^15.9.8",
+-   "@vue/test-utils": "latest"
++   "@vue/test-utils": "^1.3.0"
+  }
+}

TIP

上方代码块里的 - 号代表移除, + 号代表新增,这是一种 Diff 风格的排版,表明修改前后的变化,后文如有类似的代码风格同理。

Hello Vue3

如果想早点开始 Vue 3 的世界,可以通过以下命令直接创建一个启动项目:

bash
# 全局安装脚手架
+npm install -g create-preset
+
+# 使用 \`vue3-ts-vite\` 模板创建一个名为 \`hello-vue3\` 的项目
+preset init hello-vue3 --template vue3-ts-vite

这是一个基于 Vite + TypeScript + Vue 3 + Pinia 的项目启动模板,可以使用这个项目来练习后面的案例代码,创建完毕后可以直接跳到 安装 VSCode添加 VSCode 插件 继续学习。

当然有时间还是希望继续阅读 使用 Vite 创建项目使用 @vue/cli 创建项目 这两部分内容,了解 Vue 3 更主流的项目创建方案。

TIP

如果网络问题下载失败,可以先执行 preset proxy on 开启加速镜像代理下载。

使用 Vite 创建项目 ~new

Vite 是由 Vue 作者尤雨溪先生带领团队开发的一个构建工具,它利用浏览器原生支持 ES 模块的特点,极大提升了开发体验,自 2021 年 1 月份发布 2.0 版本以来,发展非常快,笔者也在第一时间参与贡献了一些文档和插件,并且在 2021 年期间,个人项目已经全面切换到 Vite ,公司业务也在 2021 年底开始使用 Vite 创建新项目,整体情况非常稳定,前景非常乐观。

关于是否使用 Vite 和安利团队使用 Vue 3 ,可以看笔者在 2022 年春节前写的 Markdown 工程师的一周 一文,记录了一次关于团队技术栈升级的总结,笔者是非常推荐升级技术栈的!

在这里推荐以下这几种创建 Vite 项目的方式:Create ViteCreate VueCreate Preset

Create Vite

create-vite 是 Vite 官方推荐的一个脚手架工具,可以创建基于 Vite 的不同技术栈基础模板。

运行以下命令创建模板项目,再按照命令行的提示操作(选择 vue 技术栈进入),即可创建一个基于 Vite 的基础空项目。

bash
npm create vite

不过这个方式创建的项目非常基础,如果需要用到 Router 、 Vuex 、 ESLint 等程序,都需要再单独安装和配置,所以推荐使用 Create Preset

Create Vue

create-vue 是 Vue 官方推出的一个新脚手架,用以代替基于 Webpack 的 Vue CLI ,它可以创建基于 Vite 的 Vue 基础模板。

运行以下命令创建模板项目,然后根据命令行的提示操作即可。

bash
npm init vue@3

Create Preset

create-preset 是 Awesome Starter 的 CLI 脚手架,提供快速创建预设项目的能力,可以创建一些有趣实用的项目启动模板,也可以用来管理的常用项目配置。

简单使用

可以通过包管理器直接创建配置,然后按照命令行的提示操作,即可创建开箱即用的模板项目。

bash
npm create preset

在这里选择 vue 技术栈进入,选择 vue3-ts-vite 创建一个基于 Vite + Vue 3 + TypeScript 的项目启动模板。

TIP

如果下载失败,可以通过 npm create preset proxy on 开启加速镜像代理下载。

点击查看:代理选项 - Create Preset

全局安装

也可以像使用 @vue/cli 一样,全局安装到本地,通过 preset init 命令来创建项目。

推荐全局安装它,用起来更方便,请先全局安装:

bash
npm install -g create-preset

可以通过下面这个命令来检查安装是否成功,如果成功,将会得到一个版本号:

bash
preset -v

然后可以通过 --template 选项直接指定一个模板创建项目,在这里使用 vue3-ts-vite 模板创建一个名为 hello-vue3 的项目:

bash
preset init hello-vue3 --template vue3-ts-vite

常用的项目模板也可以绑定为本地配置,点击 Create Preset 官方文档 查看完整使用教程。

管理项目配置

不论使用上方哪种方式创建项目,在项目的根目录下都会有一个名为 vite.config.jsvite.config.ts 的项目配置文件(其扩展名由项目使用 JavaScript 还是 TypeScript 决定)。

里面会有一些预设好的配置,可以在 Vite 官网的配置文档 查阅更多的可配置选项。

使用 @vue/cli 创建项目

如果不习惯 Vite ,依然可以使用 Vue CLI 作为开发脚手架。

和 Vite 的区别

Vue CLI 使用的构建工具是基于 Webpack ,可以在 了解构建工具 一节了解 Webpack 和 Vite 这两个构建工具的区别。

更新 CLI 脚手架

请先全局安装,把脚手架更新到最新版本(最低版本要求在 4.5.6 以上才能支持 Vue 3 项目的创建)。

js
npm install -g @vue/cli

使用 CLI 创建 3.x 项目 ~new

Vue CLI 全局安装后,可以在命令行输入 vue 进行操作,创建项目使用的是 create 命令:

js
vue create hello-vue3

由于要使用 TypeScript ,所以需要选择最后一个选项来进行自定义搭配,通过键盘的上下箭头进行切换选择:

js
Vue CLI v5.0.4
+? Please pick a preset:
+  Default ([Vue 3] babel, eslint)
+  Default ([Vue 2] babel, eslint)
+> Manually select features

多选菜单可以按空格选中需要的依赖,总共选择了下面这些选项:

js
Vue CLI v5.0.4
+? Please pick a preset: Manually select features
+? Check the features needed for your project: (Press <space> to select,
+<a> to toggle all, <i> to invert selection, and <enter> to proceed)
+ (*) Babel
+ (*) TypeScript
+ ( ) Progressive Web App (PWA) Support
+ (*) Router
+ (*) Vuex
+ (*) CSS Pre-processors
+>(*) Linter / Formatter
+ ( ) Unit Testing
+ ( ) E2E Testing

选择 Vue 版本,要用 Vue 3 所以需要选择 3.x :

js
? Choose a version of Vue.js that you want to start the project with
+  (Use arrow keys)
+> 3.x
+  2.x

是否选择 Class 语法的模板,在 Vue 2 版本为了更好的支持 TypeScript ,通常需要使用 Class 语法,由于 Vue 3 有了对 TypeScript 支持度更高的 Composition API ,因此选择 n ,也就是 “否” :

js
? Use class-style component syntax? (y/N) n

Babel 可以把新版本的 JavaScript 语句转换为兼容性更好的低版本 Polyfill 写法,所以选 y 确认使用:

js
? Use Babel alongside TypeScript
+  (required for modern mode, auto-detected polyfills, transpiling JSX)?
+  (Y/n) y

接下来是选择路由模式,选 y 启用 History 模式,选 n 使用 Hash 模式,可根据项目情况选择。

建议先选 y 确认,如果遇到部署的问题可以在 “路由” 一章的 部署问题与服务端配置 小节查看如何处理。

js
? Use history mode for router?
+  (Requires proper server setup for index fallback in production)
+  (Y/n) y

选择一个 CSS 预处理器,可以根据自己的喜好选择,不过鉴于目前开源社区组件常用的都是 Less ,所以也建议选择 Less 作为入门的预处理器工具。

js
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported
+ by default):
+  Sass/SCSS (with dart-sass)
+> Less
+  Stylus

Lint 规则,用来代码检查,写 TypeScript 离不开 Lint ,可以根据自己喜好选择,也可以先选择默认,后面在 添加协作规范 一节也有说明如何配置规则,这里先默认第一个:

js
? Pick a linter / formatter config: (Use arrow keys)
+> ESLint with error prevention only
+  ESLint + Airbnb config
+  ESLint + Standard config
+  ESLint + Prettier

Lint 的校验时机,一个是在保存时校验,一个是在提交 commit 的时候才校验,这里也选默认:

js
? Pick additional lint features: (Press <space> to select,
+ <a> to toggle all, <i> to invert selection, and <enter> to proceed)
+>(*) Lint on save
+ ( ) Lint and fix on commit

项目配置文件,笔者更习惯保存为独立文件:

js
? Where do you prefer placing config for Babel, ESLint, etc.?
+  (Use arrow keys)
+> In dedicated config files
+  In package.json

是否保存为未来的项目配置,存起来方便以后快速创建:

js
? Save this as a preset for future projects? Yes
+? Save preset as: vue-3-ts-config

至此,项目创建完成!可以通过 npm run serve 开启热更进行开发调试,通过 npm run build 构建打包上线。

管理项目配置

Vue CLI 的配置文件是 vue.config.js ,可以参考官网的说明文档调整各个选项配置:配置参考 - Vue CLI

调整 TS Config

如果在 Vite 的配置文件 vite.config.ts ,或者是在 Vue CLI 的配置文件 vue.config.js 里设置了 alias 的话,因为 TypeScript 不认识里面配置的 alias 别名,所以需要再对 tsconfig.json 做一点调整,增加对应的 paths ,否则在 VSCode 里可能会路径报红,提示找不到模块或其相应的类型声明。

比如在 Vue 组件里引入路径为 @cp/HelloWorld.vue 的时候,可以避免写出 ../../../../components/HelloWorld.vue 这样的非常多层级的相对路径,但是默认情况下 TypeScript 并不知道这个 alias 等价于 src/components/HelloWorld.vue 这个文件路径,从而会报错找不到该模块并导致无法正确编译。

假设在 vite.config.ts 里配置了这些 alias :

ts
export default defineConfig({
+  // ...
+  resolve: {
+    alias: {
+      '@': resolve('src'), // 源码根目录
+      '@img': resolve('src/assets/img'), // 图片
+      '@less': resolve('src/assets/less'), // 预处理器
+      '@libs': resolve('src/libs'), // 本地库
+      '@plugins': resolve('src/plugins'), // 本地插件
+      '@cp': resolve('src/components'), // 公共组件
+      '@views': resolve('src/views'), // 路由组件
+    },
+  },
+  // ...
+})

那么在该项目的 tsconfig.json 文件里就需要相应的加上这些 paths :

json
{
+  "compilerOptions": {
+    // ...
+    "paths": {
+      "@/*": ["src/*"],
+      "@img/*": ["src/assets/img/*"],
+      "@less/*": ["src/assets/less/*"],
+      "@libs/*": ["src/libs/*"],
+      "@plugins/*": ["src/plugins/*"],
+      "@cp/*": ["src/components/*"],
+      "@views/*": ["src/views/*"]
+    },
+    // ...
+  },
+  // ...
+}

TIP

注意 paths 的配置全部要以 /* 结尾,代表该目录下的文件都可以被匹配,而不是指向某一个文件。

添加协作规范

考虑到后续可能会有团队协作,最好是能够统一编码风格。

Editor Config

在项目根目录下再增加一个名为 .editorconfig 的文件。

这个文件的作用是强制编辑器以该配置来进行编码,比如缩进统一为空格而不是 Tab ,每次缩进都是 2 个空格而不是 4 个等等。

文件内容如下:

js
# http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+max_line_length = 80
+trim_trailing_whitespace = true
+
+[*.md]
+max_line_length = 0
+trim_trailing_whitespace = false

具体的参数说明可参考:项目代码风格统一神器 editorconfig 的作用与配置说明

TIP

部分编辑器可能需要安装对应的插件才可以支持该配置。

例如 VSCode 需要安装 EditorConfig for VSCode 扩展

Prettier

Prettier 是目前最流行的代码格式化工具,可以约束的代码风格不会乱七八糟,目前所知道的知名项目(如 Vue 、 Vite 、 React 等)和大厂团队(谷歌、微软、阿里、腾讯等)都在使用 Prettier 格式化代码。

通过脚手架创建的项目很多都内置了 Prettier 功能集成(例如 Create Preset ,参考了主流的格式化规范,比如 2 个空格的缩进、无需写分号结尾、数组 / 对象每一项都带有尾逗号等等)。

如果需要手动增加功能支持,请在项目根目录下创建一个名为 .prettierrc 的文件,写入以下内容:

json
{
+  "semi": false,
+  "singleQuote": true
+}

这代表 JavaScript / TypeScript 代码一般情况下不需要加 ; 分号结尾,然后使用 '' 单引号来定义字符串等变量。

这里只需要写入与默认配置不同的选项即可,如果和默认配置一致,可以省略,完整的配置选项以及默认值可以在 Prettier 官网的 Options Docs 查看。

配合 VSCode 的 VSCode Prettier 扩展,可以在编辑器里使用该规则格式化文件(此时无需在项目下安装 Prettier 依赖)。

如果开启了 ESLint ,配合 ESLint 的代码提示,可以更方便的体验格式化排版,详见 ESLint 一节的说明。

TIP

配合 VSCode Prettier 扩展 ,这份配置直接在 VSCode 里生效,如果配合 ESLint 使用,需要安装 Prettier 依赖。

ESLint

ESLint 是一个查找 JavaScript / TypeScript 代码问题并提供修复建议的工具,换句话说就是可以约束的代码不会写出一堆 BUG ,它是代码健壮性的重要保障。

虽然大部分前端开发者都不愿意接受这些约束(当年笔者入坑的时候也是),但说实话,经过 ESLint 检查过的代码质量真的高了很多,如果不愿意总是做一个游兵散勇,建议努力让自己习惯被 ESLint 检查,大厂和大项目都是有 ESLint 检查的。

特别是写 TypeScript ,配合 ESLint 的检查实在太爽了(字面意思,真的很舒服)。

通过脚手架创建的项目通常都会帮配置好 ESLint 规则,如果有一些项目是一开始没有,后面想增加 ESLint 检查,也可以手动配置具体规则。

这里以一个 Vite + TypeScript + Prettier 的 Vue 3 项目为例,在项目根目录下创建一个名为 .eslintrc.js 文件,写入以下内容:

js
module.exports = {
+  root: true,
+  env: {
+    node: true,
+    browser: true,
+  },
+  extends: ['plugin:vue/vue3-essential', 'eslint:recommended', 'prettier'],
+  parser: 'vue-eslint-parser',
+  parserOptions: {
+    parser: '@typescript-eslint/parser',
+    ecmaVersion: 2020,
+    sourceType: 'module',
+  },
+  plugins: ['@typescript-eslint', 'prettier'],
+  rules: {
+    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+    'prettier/prettier': 'warn',
+    'vue/multi-word-component-names': 'off',
+  },
+  globals: {
+    defineProps: 'readonly',
+    defineEmits: 'readonly',
+    defineExpose: 'readonly',
+    withDefaults: 'readonly',
+  },
+}

然后安装对应的依赖(记得添加 -D 参数添加到 devDependencies ,因为都是开发环境下使用的):

这样就可以在项目中生效了(如果 VSCode 未能立即生效,重启编辑器即可),一旦代码有问题, ESLint 就会帮检查出来并反馈具体的报错原因,久而久之的代码就会越写越规范。

更多的选项可以在 ESLint 官网的 Configuring ESLint 查阅。

如果有一些文件需要排除检查,可以再创建一个 .eslintignore 文件在项目根目录下,里面添加要排除的文件或者文件夹名称:

txt
dist/*

更多的排除规则可以在 ESLint 官网的 The .eslintignore File 一文查阅。

安装 VSCode

要问现在前端工程师用的最多的代码编辑器是哪个,肯定是 Visual Studio Code 了!

与其他的编辑器相比,有这些优点:

  • 背靠 Microsoft ,完全免费并且开源,开箱即用
  • 可以通过简单的配置调整来满足之前在其他编辑器上的习惯( e.g. Sublime Text )
  • 轻量级但功能强大,内置了对 JavaScript、TypeScript 和 Node.js 的支持,
  • 丰富的插件生态,可以根据的需要,安装提高编码效率的功能支持,以及其他的语言扩展
  • 智能的代码补全、类型推导、代码检查提示、批量编辑、引用跳转、比对文件等功能支持
  • 登录的 GitHub 账号即可实现配置自动同步,在其他电脑上直接使用的最习惯配置和插件

当然,还有非常多优点可自行体验!

点击下载:Visual Studio Code

一般情况下开箱即用,无门槛,也可以阅读官方文档了解一些个性化的配置。

点击查看:VSCode 操作文档

添加 VSCode 插件

VSCode 本身是轻量级的,也就是只提供最基础的功能,更优秀的体验或者个性化体验,是需要通过插件来启用的。

这里推荐几个非常舒服的 VSCode 插件,可以通过插件中心安装,也可以通过官方应用市场下载。

Chinese (Simplified)

VSCode 安装后默认是英文本,需要自己进行汉化配置, VSCode 的特色就是插件化处理各种功能,语言方面也一样。

安装该插件并启用,即可让 VSCode 显示为简体中文。

点击下载:Chinese (Simplified)

Volar

Vue 官方推荐的 VSCode 扩展,用以代替 Vue 2 时代的 Vetur ,提供了 Vue 3 的语言支持、 TypeScript 支持、基于 vue-tsc 的类型检查等功能。

点击下载:Volar

TIP

Volar 取代了 Vetur 作为 Vue 3 的官方扩展,如果之前已经安装了 Vetur ,请确保在 Vue 3 的项目中禁用它。

Vue VSCode Snippets

从实际使用 Vue 的角度提供 Vue 代码片段的生成,可以通过简单的命令,在 .vue 文件里实现大篇幅的代码片段生成,例如:

  1. 输入 ts 可以快速创建一个包含了 template + script + style 的 Vue 组件模板(可选 2.x 、3.x 以及 class 风格的模板)

  2. 也可以通过输入带有 v3 开头的指令来快速生成 Vue 3 的 API 。

下面是输入了 ts 两个字母之后,用箭头选择 vbase-3-ts 自动生成的一个模板片段,在开发过程中非常省事:

vue
<template>
+  <div></div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup() {
+    return {}
+  },
+})
+</script>
+
+<style scoped></style>

点击下载:Vue VSCode Snippets

Auto Close Tag

可以快速完成 HTML 标签的闭合,除非通过 .jsx / .tsx 文件编写 Vue 组件,否则在 .vue 文件里写 template 的时候肯定用得上。

点击下载:Auto Close Tag

Auto Rename Tag

假如要把 div 修改为 section,不需要再把 <div> 然后找到代码尾部的 </div> 才能修改,只需要选中前面或后面的半个标签直接修改,插件会自动把闭合部分也同步修改,对于篇幅比较长的代码调整非常有帮助。

点击下载:Auto Rename Tag

EditorConfig for VSCode

一个可以让编辑器遵守协作规范的插件,详见 添加协作规范

点击下载:EditorConfig for VSCode

Prettier for VSCode

这是 Prettier 在 VSCode 的一个扩展,不论项目有没有安装 Pretter 依赖,安装该扩展之后,单纯在 VSCode 也可以使用 Pretter 进行代码格式化。

点击下载:Prettier - Code formatter

点击访问:Prettier 官网 了解更多配置。

ESLint for VSCode

这是 ESLint 在 VSCode 的一个扩展, TypeScript 项目基本都开了 ESLint ,编辑器也建议安装该扩展支持以便获得更好的代码提示。

点击下载:VSCode ESLint

点击访问:ESLint 官网 了解更多配置。

其他插件

其他的比如预处理器相关的,Git 相关的,可以根据自己的需求在 VSCode 的插件市场 里搜索安装。

项目初始化

至此,通过脚手架已经搭好了一个可直接运行的基础项目,已经可以正常的 npm run devnpm run build 了(具体命令取决于的项目 脚本命令的配置 ),项目配置和编辑器也都弄好了,是不是可以开始写代码了呢?

请不要着急,还需要了解一点东西,就是如何初始化一个 Vue 3 项目。

因为在实际开发过程中,还会用到各种 npm 包,像很多 UI 框架、功能插件的引入都是需要在 Vue 初始化阶段处理。

甚至有时候还要脱离脚手架,采用 CDN 引入的方式来开发,所以开始写组件之前,还需要了解一下在 Vue 3 项目中,初始化阶段对比 Vue 2 的一些变化。

入口文件

项目的初始化都是在入口文件集中处理,Vue 3 的目录结构对比 Vue 2 没有变化,入口文件依然还是 main.ts 这个文件。

但是 Vue 3 在初始化的时候,做了不少的调整,代码写法和 Vue 2 是完全不同,但是对于这次大改动,笔者认为是好的,因为统一了相关生态的启用方式,不再像 Vue 2 时期那样多方式共存,显得比较杂乱。

回顾 Vue 2

Vue 2 在导入各种依赖之后,通过 new Vue() 执行 Vue 的初始化,相关的 Vue 生态和插件,有的是使用 Vue.use() 来进行初始化,有的是作为 new Vue() 的入参:

ts
import Vue from 'vue'
+import App from './App.vue'
+import router from './router'
+import store from './store'
+import pluginA from 'pluginA'
+import pluginB from 'pluginB'
+import pluginC from 'pluginC'
+
+// 使用了 \`use\` 方法激活
+Vue.use(pluginA)
+Vue.use(pluginB)
+Vue.use(pluginC)
+
+Vue.config.productionTip = false
+
+// 作为 \`new Vue()\` 的入参激活
+new Vue({
+  router,
+  store,
+  render: (h) => h(App),
+}).$mount('#app')

了解 Vue 3 ~new

在 Vue 3 ,使用 createApp 执行 Vue 的初始化,另外不管是 Vue 生态里的东西,还是外部插件、 UI 框架,统一都是由 use() 进行激活,非常统一和简洁:

ts
import { createApp } from 'vue'
+import App from './App.vue'
+import router from './router'
+import store from './store'
+import pluginA from 'pluginA'
+import pluginB from 'pluginB'
+import pluginC from 'pluginC'
+
+createApp(App)
+  .use(store)
+  .use(router)
+  .use(pluginA)
+  .use(pluginB)
+  .use(pluginC)
+  .mount('#app')

Vue Devtools

Vue Devtools 是一个浏览器扩展,支持 Chrome 、 Firefox 等浏览器,需要先安装才能使用。

点击安装:Vue Devtools 的浏览器扩展

当在 Vue 项目通过 npm run dev 等命令启动开发环境服务后,访问本地页面(如: http://localhost:3000/ ),在页面上按 F12 唤起浏览器的控制台,会发现多了一个名为 vue 的面板。

面板的顶部有一个菜单可以切换不同的选项卡,菜单数量会根据不同项目有所不同,例如没有安装 Pinia 则不会出现 Pinia 选项卡,这里以其中一部分选项卡作为举例。

Components 是以结构化的方式显示组件的调试信息,可以查看组件的父子关系,并检查组件的各种内部状态:

`,191),A=e("p",null,"Routes 可以查看当前所在路由的配置信息:",-1),d=e("p",null,"Timeline 是以时间线的方式追踪不同类型的数据,例如事件:",-1),u=e("p",null,"Pinia 是可以查看当前组件引入的全局状态情况:",-1),h=e("p",null,[o("可以在 "),e("a",{href:"https://devtools.vuejs.org/",target:"_blank",rel:"noreferrer"},"Vue Devtools 官网"),o(" 了解更多的用法。")],-1);function g(m,f,v,b,V,k){const l=p("ImgWrap"),a=p("ClientOnly"),r=p("GitalkComment");return i(),D("div",null,[C,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/vue-versions-on-npmjs.jpg",alt:"Vue 在 npmjs 上的版本列表"})]),_:1}),F,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/vue-devtools-components.jpg",dark:"/assets/img/vue-devtools-components-dark.jpg",alt:"Vue Devtools 的 Components 界面"})]),_:1}),A,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/vue-devtools-routes.jpg",dark:"/assets/img/vue-devtools-routes-dark.jpg",alt:"Vue Devtools 的 Routes 界面"})]),_:1}),d,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/vue-devtools-timeline.jpg",dark:"/assets/img/vue-devtools-timeline-dark.jpg",alt:"Vue Devtools 的 Timeline 界面"})]),_:1}),u,s(a,null,{default:n(()=>[s(l,{src:"/assets/img/vue-devtools-pinia.jpg",dark:"/assets/img/vue-devtools-pinia-dark.jpg",alt:"Vue Devtools 的 Pinia 界面"})]),_:1}),h,s(a,null,{default:n(()=>[s(r,{issueId:45})]),_:1})])}const S=c(y,[["render",g]]);export{q as __pageData,S as default}; diff --git a/changelog.html b/changelog.html new file mode 100644 index 00000000..daa9e99b --- /dev/null +++ b/changelog.html @@ -0,0 +1,28 @@ + + + + + + 更新记录 | Vue3 入门指南与实战案例 + + + + + + + + + + + + + +
Skip to content
目录

更新记录

由于平时也比较忙,都是利用碎片时间整理的文档,习惯勤备份,因此 commit 记录比较多,而且有些提交记录跟内容也无关,比如文档程序的调整和优化等代码提交是和内容无关的。

考虑到后面还会不定期更新内容,所以翻了一下之前跟朋友的微信聊天记录整理了前期的更新记录,之后当文档有再次更新的时候也会继续整理更新记录,方便读者们查阅。

2024-02-20

单组件的编写路由的使用插件的开发和使用组件之间的通信全局状态管理 这几张内容的最前面,补充了一个阅读提示,解答近期收到的读者反馈的一些疑惑。

2024-01-16

插件部分增加一个 path/to 的典故 扩展阅读。

2023-05-07

感谢各位读者长期以来的支持,经过长达一年时间的打磨和优化,本书已于 2023 年 5 月份正式出版上市,纸质版书籍的正式名称为《前端工程化:基于 Vue.js 3.0 的设计与实践》。

如果您对笔者的作品认可,建议购买纸质版,纸质书在电子书的基础上,经过机械工业出版社的编辑老师们的内容优化、校对勘误、排版美化,更成体系!

各位读者可以在各大电商平台购买纸质版书籍:

☞ 访问 京东商城 购买

☞ 访问 天猫商城 购买

可点击 出版说明 了解更多信息。

2023-03-01

在 “单组件的编写” 的 DOM 元素与子组件 一节里,添加了 InstanceType<T> 帮助类型的使用说明,感谢 @rayadaschn#62 (comment) 的反馈。

2022-11-20

在 “高效开发” 新增了 命名技巧 一节,希望能够帮助有命名困扰的开发者解决选择困难的问题。

2022-11-17

更新了 “用 ES Module 设计模块” 中关于 命名导出和导入 的讲解,减少在理解上的歧义,感谢 @Yeshan-Taoist#163 (commont) 的反馈。

2022-11-12

优化了 “高效开发” 一章,考虑到现在的 Vue 3 项目基本都是最新版本的 Vue ,因此移除了过渡时期的一些过时的使用提示信息,同时优化了演示代码使其更容易理解。

2022-11-11

优化了 “组件之间的通信” 一章:

  • 把一些原来没有特地注明是父组件的代码还是子组件的代码,进行了明确的标明,并优化了一部分示范代码
  • 修正了之前一部分示范代码的 TypeScript 类型(之前有一些地方用了 any ,现在更新为明确的类型)
  • 优化了 provide / inject 的相关内容,大幅度缩短了篇幅,但通过新的例子使其更容易看懂
  • 增加了 Reactive State 创建一个小型的状态中心案例
  • Vuex 添加了一些使用提示,建议使用 Pinia 代替 Vuex 作为状态管理库使用

2022-11-06

优化了 “路由的使用” 一章:

  • 涉及 Webpack 的环境变量和配置,统一调整为 Vite 的配置,并标注了这些配置在使用时需要留意当前所使用的构建工具是否匹配
  • 将原本指向 Vue Router 英文官网的参考内容地址修改为指向其中文官网(因为当时编写的时候还没有中文官网)
  • 优化了里面的一些举例,使其更容易理解

优化了 “插件的开发和使用” 一章:

  • 简化了一些例子

2022-11-04

比较大幅度的优化了 “单组件的编写” 一章的 响应式 API 之 toRef 与 toRefs 的内容,使其更容易理解。

2022-11-02

优化了 “单组件的编写” 一章的 “响应式 API 之 ref ” 一节,对其 类型声明 作了一个更容易理解和更详细的讲解。

2022-10-25

  • 把 hello-node 和 hello-lib 这两个演示仓库同步到了 Gitee ,如果访问 GitHub 失败,也可以从 Gitee 拉取。
  • 在 “脚手架的升级与配置” 一章更新了 Vue Devtools 的介绍,因为插件更新后变化较大,重新截图进行了说明,并且对图片适配了文档的 Light / Dark 皮肤切换

2022-10-23

在 “工程化的前期准备” 一章的 “用 ES Module 设计模块” 一节,增加了 在浏览器里访问 ESM 小节,讲解了浏览器对原生 ESM 的访问支持和限制,以及在 HTML 页面里使用 ESM 的代码演示。

另外贯穿 “前端工程化入门” 所演示的 hello-node 项目已托管至 learning-vue3/hello-node 仓库,可使用 Git 克隆命令拉取至本地:

bash
git clone https://github.com/learning-vue3/hello-node.git

成品项目可作为学习过程中的代码参考,但更建议按照教程的讲解步骤,从零开始亲手搭建一个新项目并完成 node 开发的体验,可以更有效的提升学习效果。

2022-10-13

  1. 本书目前已不再仅限于 Vue 3 的学习,因此重写了 前言 的内容
  2. 不同时期写的内容存在一定程度的上下文不统一(例如早期使用了 @vue/cli 创建的项目做演示,后期因为 Vite 的出现并且被 Vue 官方作为其主推的构建工具,因此新内容使用的是 Vite 项目),因此对内容进行了检查和统一修正
  3. 减少了全文的人称代词,剩下不可删除的则进行了上下文统一,减少前后不一致可能会引起一些理解困惑
  4. 优化了不同时期的内容之间的章节衔接

其中第 2 、 3 、 4 点可能还有遗漏,近期持续检查修正。

2022-10-11

在 “开发 npm 包” 部分 常用的构建工具 一节增加 Node 版本检查的提示,在开发之前请确保当前的 Node 版本是否在构建工具的支持范围内,感谢 @rayadaschn#204 的建议。

2022-10-10

发布 npm 包 小节增加一个取消镜像源的提示,否则可能引起发布失败。

2022-10-09

在 “开发 npm 包” 部分所演示的 hello-lib 项目已托管至 learning-vue3/hello-lib 仓库,可使用 Git 克隆命令拉取至本地:

bash
git clone https://github.com/learning-vue3/hello-lib.git

成品项目可作为学习过程中的代码参考,但更建议按照教程的讲解步骤,从零开始亲手搭建一个新项目并完成 npm 包的开发流程,可以更有效的提升学习效果。

2022-10-08

在 “开发 npm 包” 的相关内容处增加了 添加版权注释 的构建说明,它的作用除了声明版权归属之外,还会告知使用者关于项目的主页地址、版本号、发布日期、 BUG 反馈渠道等信息,例如 jQuery / Lodash / Swiper 等库都有这样的版权注释,因此补充了这部分的内容,供有需要的开发者了解和使用。

2022-10-07

  1. 文档程序从 VuePress 2.x 迁移到 VitePress 1.x ,并做了简单的主题适配
  2. 原来的 “脚手架的升级与配置” 一章的访问地址从 update 调整为 upgrade ,访问地址已配置重定向
  3. 由于原来的 “起步准备” 一章内容有点太长,这一次续写拆分了一下内容,重新划分为单独的三章,分别是:
    • 独立出来了 了解前端工程化 一章,用于科普现代前端开发所要求的工程化概念,属于讲故事性质的一章内容
    • 工程化的前期准备 是原来 “起步准备” 的核心内容部分,主要是科普前端工程化的入门实践,属于动手实操章节
    • 另外考虑到本书是直接使用 TypeScript 来入门 Vue 3 ,语言部分也是内容的重点之一,所以把 TypeScript 部分也独立成了 快速上手 TypeScript 一章
  4. 在 “了解前端工程化” 一章里,增加以下内容:
  5. 在 “工程化的前期准备” 一章里,增加以下内容:
  6. 在 “插件的开发和使用” 一章里,增加了以下内容:

2022-09-01

更新了引用到的官方文档链接,同时修复了部分官方文档未做好重定向的无效链接,感谢 @alleluya-young#191 的反馈!

2022-07-03

补充了关于 Node.js 版本之间的区别 的一些说明。

2022-06-23

根据 #163 (comment) 的反馈,对一些 ES6+ 可能容易混淆的新语法进行了一些 Tips 补充,减少学习过程中的理解成本。

2022-05-03

更新了 “升级与配置” 一章的部分内容:

  1. 更新了使用 Create Preset 创建项目的说明,现在可以通过 --template 选项来指定模板创建,例如:
bash
# 全局安装
+npm install -g create-preset
+
+# 将使用 `vue3-ts-vite` 模板创建一个名为 `hello-vue3` 的项目
+preset init hello-vue3 --template vue3-ts-vite
  1. 配合 1 的更新,相应的增加了 Hello Vue3 一节
  2. 配合 @vue/cli 5.x 版本的命令行交互,更新了 使用 @vue/cli 创建项目 的内容
  3. 更新了 调整 TS Config 部分的内容,由于现在通过各类脚手架创建的 TypeScript 项目都自带了该文件,所以这部分内容移除了之前的配置参考,仅保留 paths 的配置提醒

2022-04-30

最近有时间,所以对内容做了一大波更新,主要是补充了在开始开发 Vue 项目之前的一些基础知识点,当然 Vue 相关的内容也做了一些新增或者完善。

  1. 增加了全新的一章 “起步准备” ,面向对前端工程化开发不太熟悉的开发者,包含了以下内容:

  2. 在 “升级与配置” 一章里,增加了以下内容:

  3. 在 “单组件的编写” 一章里,增加了以下内容:

    • 响应式数据 设计上的变化
    • 增加了 指令 一节的内容,主要讲解如何在 Vue 3 开发自定义指令
    • 增加了 插槽 一节的内容
  4. 在 “路由的使用” 一章里,增加了以下内容,整理了一些常见的部署问题原因以及解决方案:

  5. 在 “插件的使用” 一章里,对 “本地插件” 作了进一步的细化讲解:

2022-04-20

更正了 watch API 在 侦听选项之 deep 部分内容的错误,并增加了如何检测特例的用法,感谢 @zcc0329 的反馈!

2022-04-10

重写了单组件关于 数据的侦听 这一节的内容,因为在写 Pinia 的订阅功能的时候,有很多跟 watch 相关联的知识点,发现之前这部分内容写的比较简单,所以完善了一下。

2022-04-07

补充了昨天新增的关于 Pinia 订阅 state 部分的一些内容。

2022-04-06

  1. 创建基于 Vite 的 Vue 3 项目增加了一些内容:

    • 增加了使用 Create Vue 创建 Vite 项目的说明
    • 使用 Create Preset 创建的 vue3-ts-vite 预设项目,使用 Pinia 代替 Vuex
  2. 补充了 Pinia 的部分内容:

2022-04-04

先跟大家说声抱歉,在 2022-02-09 的更新里说要写这部分内容,结果因为现实里的一些事情还有工作比较忙,现在借着假期才得以继续更新。

增加了 全局状态的管理 一章,主要是面向 Vue 3 全新的状态管理工具 Pinia 展开,里面的大部分内容也加入了和 Vuex 的对比。

虽然可以归入 组件之间的通信 里,但 Pinia 作为被官方推荐在 Vue 3 项目里作为全局状态管理的新工具,写着写着我觉得还是单独开一章来写会更方便阅读和理解。

2022-03-04

把 Algolia 的搜素功能申请下来了,看看文档的搜索体验会不会比较好(搜索框在页面的右上角)。

2022-02-28

升级了文档的程序版本,加入了 Dark Mode 暗黑模式的支持,默认跟随系统模式进行适配,可以在右上角的 “主题切换” 按钮手动切换主题。

2022-02-09

  1. 优化了 升级与配置 一章,配合 2022-02-07 Vue 3 成为默认版本带来的调整变化,主要改动如下:

  2. 高效开发 一章开头的 WARNING 改成了 TIP ,里面提到的功能在最新版的 Vue 下发挥稳定,后续如果有实验性的 API 再单独标记。

  3. 把原先演示命令里的 @next 版本都改成了 @latest ,现在不需要指定 next 就能使用 Vue 3 了。

  4. 更新了 官方文档 里面的链接说明,因为各个官网在 2022-02-07 都上线了新版本,更换了新地址。

  5. 组件之间的通信 里加入了 Pinia 的介绍,不过暂时没有写实践记录(还没有时间哈哈哈),欢迎先自行体验,这是一个官方推荐的适合 Vue 3 的状态管理工具,因为它更加面向 Composition API 的编程方式(至少在 Vuex 5 推出之前,建议用来代替 Vuex 4)。

  6. 改了一下文档名称:《Vue3.0 学习教程与实战案例》 --> 《Vue 3 入门指南与实战案例》,其他原先使用 Vue 3.0 叫法的地方,后面也会陆续改成 Vue 3 了。

2022-01-20

  1. 更新了 cnpm 的用法,原因是旧的 cnpm 源即将下线,需要使用新的域名啦!(感谢 @alleluya-young#135 的反馈!)

  2. 添加了 pnpm 的用法说明,目前开源社区越来越流行使用 pnpm ,对大型项目管理也有好处,值得了解和体验。

2021-12-05

由于离第一次执笔到现在有了差不多一年的时间,当时的前言信息有些陈旧,重新写了一下 前言

另外经过 2021 年的开发实践,很多地方我又有了新的认知,打算在接下来的时间里,我会再继续抽空把各部分的内容再精细化一下,把以前可能写的比较简单的部分,或者没有写进来的一些功能点,都再做一次完善。

2021-11-16

重写了 数据的计算 一节,之前对于计算数据的介绍,描述上确实有点难以理解,而且有点简单带过,所以重新写了这部分内容,感谢读者 @superficial 的反馈!

2021-10-07

补充了 全局编译器宏 的配置说明,本来很久前就想写的,一直偷懒,国庆结束前补充进来啦!

2021-08-17

更新了 3.2.0 版本新增的一些知识点:

  1. 增加了 使用 v-bind 动态修改 style 功能说明,同时结合上下文知识点补充了 动态绑定 CSS 一节

  2. 增加了 CSS Modules 一节的内容

  3. 增加了 useCssModule 一节的内容

2021-08-16

  1. 更新了 深度操作符 的内容,原有的 API 已被废弃,请注意使用新写法

  2. 补充了几个拓展阅读链接到 教程工具

2021-07-28

迎接 Vue 3.2.0 的新版本内容,对 script-setup 做了较多的调整。

目前 3.2 还处于 Beta 阶段,不过从这个版本开始,script-setup 脱离实验性阶段,正式进入 Vue 3.0 的队伍,本次的更新内容较多,很多测试阶段的 API 被移除或者修改,如果之前有用过 script-setup ,建议先简单看我之前的博客,了解本次的一些变更:

Vue3.0 最新动态:script-setup 定稿 部分实验性 API 将弃用

2021-03-31

  1. 调整了 使用全局 API 一节的内容

原因是原来推荐的 const { proxy } = getCurrentInstance() 的方案,在 TS 新版本校验下,使用成本非常高,原来的代码不做多层判断的情况下已经无法正常运行,所以按照目前的 “最佳实践” 重新更新了这一节。

  1. 新增了一节 全局 API 的替代方案

由于 Vue 3.0 的有意为之,这个 “最佳实践” 实际上用起来也挺繁琐(在官网和 GitHub 对此都有说明,不推荐使用),所以补充了这一节内容。

2021-03-23

DOM 元素与子组件 一节增加了 TS 类型的定义说明,以及编译失败的原因说明及解决方案补充,非常感谢 @aierong 的反馈和支持。

2021-03-21

增加了 高效开发 一章,将记录一些提高 Vue 3.0 开发效率的内容,面像对 Vue 3.0 已上手,想提高开发效率的开发者。

本次更新了 script-setup 一节的内容。

2021-01-21

增加了 组件之间的通信 一章的内容。

2020-12-18

本文档的第一个版本上线,完成了 升级与配置单组件的编写路由的使用插件的使用 等章节的编写。

2020-10-01

国庆期间开始开荒 Vue 3.0,陆陆续续打了不少笔记,业余时间着手整理成文档。

Released under the MIT License.

+ + + + \ No newline at end of file diff --git a/communication.html b/communication.html new file mode 100644 index 00000000..b6606261 --- /dev/null +++ b/communication.html @@ -0,0 +1,688 @@ + + + + + + 组件之间的通信 | Vue3 入门指南与实战案例 + + + + + + + + + + + + + +
Skip to content
目录

组件之间的通信

经过前面几章的阅读,相信开发者已经可以搭建一个基础的 Vue 3 项目了!

但实际业务开发过程中,还会遇到一些组件之间的通信问题,父子组件通信、兄弟组件通信、爷孙组件通信,还有一些全局通信的场景。

TIP

这一章节的内容,Vue 3 对比 Vue 2 的变化都比较大!

这一章就按使用场景来划分对应的章节吧,在什么场景下遇到问题,也方便快速找到对应的处理办法。

通信场景快速定位
父子组件通信点击查看
爷孙组件通信点击查看
兄弟组件通信点击查看
全局组件通信点击查看

温馨提示

这是笔者写于 2024.02.20 的补充提示,为读者解答为何前面的章节都未提及 script-setup 组件语法的信息。

笔者在执笔之初,考虑到降低初学过程中的各种心智负担,以及当时处于 script-setup 组件语法尚未完全稳定的背景下,所以在最后一章 高效开发 之前的章节,都只提供了组件的标准写法的讲解。

除非遇到框架进行了 Breaking Change (例如从 Vue 2 升级到了 Vue 3 这种大版本更新),否则标准写法是始终有效的,在笔者多年的学习经历和实际收获里,也是比较相信一点:不论学习什么语言或者框架,只要基础打的好,未来总是可以轻松地举一反三去解决遇到的各类问题。

因此如果读者通过各类脚手架创建的 Vue 3 项目,在组件看到 <script setup> 这种风格的组件,以及诸如 defineXxx 风格的 API ,这是 .vue 组件所支持的一种语法糖,可以随时在 高效开发 一章了解相关的 API 用法。

另外从 Vue 3.4 版本之后所新增的 API 未在本书的介绍范围内,请读者在阅读的过程中也可以查阅官方的 API 一起学习,不论是本书还是其他计算机书籍,官方文档永远是最好的查询手册。

TIP

语法糖(英语:Syntactic sugar)是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。

父子组件通信

父子组件通信是指,B 组件引入到 A 组件里渲染,此时 A 是 B 的父级;B 组件的一些数据需要从 A 组件拿,B 组件有时也要告知 A 组件一些数据变化情况。

他们之间的关系如下, Child.vue 是直接挂载在 Father.vue 下面:

bash
# 父组件
+Father.vue
+ # 子组件
+└─Child.vue

常用的方法有:

方案父组件向子组件子组件向父组件对应章节传送门
props / emitspropsemits点击查看
v-model / emitsv-modelemits点击查看
ref / emitsrefemits点击查看
provide / injectprovideinject点击查看
EventBusemit / onemit / on点击查看
Reactive State--点击查看
Vuex--点击查看
Pinia--点击查看

为了方便阅读,下面的父组件统一叫 Father.vue ,子组件统一叫 Child.vue 。

WARNING

在 Vue 2 ,有的开发者可能喜欢用 $attrs / $listeners 来进行通信,但该方案在 Vue 3 已经移除了,详见 移除 $listeners

props / emits

这是 Vue 跨组件通信最常用,也是基础的一个方案,它的通信过程是:

  1. 父组件 Father.vue 通过 props 向子组件 Child.vue 传值
  2. 子组件 Child.vue 则可以通过 emits 向父组件 Father.vue 发起事件通知

最常见的场景就是统一在父组件发起 AJAX 请求,拿到数据后,再根据子组件的渲染需要传递不同的 props 给不同的子组件使用。

下发 props

注:这一小节的步骤是在 Father.vue 里操作。

下发的过程是在 Father.vue 里完成的,父组件在向子组件下发 props 之前,需要导入子组件并启用它作为自身的模板,然后在 setup 里处理好数据并 return 给 <template /> 用。

在 Father.vue 的 <script /> 里:

ts
// Father.vue
+import { defineComponent } from 'vue'
+import Child from '@cp/Child.vue'
+
+interface Member {
+  id: number
+  name: string
+}
+
+export default defineComponent({
+  // 需要启用子组件作为模板
+  components: {
+    Child,
+  },
+
+  // 定义一些数据并 `return` 给 `<template />` 用
+  setup() {
+    const userInfo: Member = {
+      id: 1,
+      name: 'Petter',
+    }
+
+    // 不要忘记 `return` ,否则 `<template />` 拿不到数据
+    return {
+      userInfo,
+    }
+  },
+})

然后在 Father.vue 的 <template /> 这边拿到 return 出来的数据,把要传递的数据通过属性的方式绑定在组件标签上。

vue
<!-- Father.vue -->
+<template>
+  <Child
+    title="用户信息"
+    :index="1"
+    :uid="userInfo.id"
+    :user-name="userInfo.name"
+  />
+</template>

这样就完成了 props 数据的下发。

<template /> 绑定属性这里,如果是普通的字符串,比如上面的 title,则直接给属性名赋值就可以。

如果是变量名,或者其他类型如 numberboolean 等,比如上面的 index,则需要通过属性动态绑定的方式来添加,使用 v-bind: 或者 : 符号进行绑定。

另外官方文档推荐对 camelCase 风格(小驼峰)命名的 props ,在绑定时使用和 HTML attribute 一样的 kebab-case 风格(短横线),例如使用 user-name 代替 userName 传递,详见官网的 传递 prop 的细节 一节。

接收 props

注:这一小节的步骤是在 Child.vue 里操作。

接收的过程是在 Child.vue 里完成的,在 <script /> 部分,子组件通过与 setup 同级的 props 来接收数据。

它可以是一个 string[] 数组,把要接受的变量名放到这个数组里,直接放进来作为数组的 item

ts
// Child.vue
+export default defineComponent({
+  props: ['title', 'index', 'userName', 'uid'],
+})

但这种情况下,使用者不知道这些属性的使用限制,例如是什么类型的值、是否必传等等。

带有类型限制的 props

注:这一小节的步骤是在 Child.vue 里操作。

和 TypeScript 一样,类型限制可以为程序带来更好的健壮性, Vue 的 props 也支持增加类型限制。

相对于传递一个 string[] 类型的数组,更推荐的方式是把 props 定义为一个对象,以对象形式列出,每个 Property 的名称和值分别是各自的名称和类型,只有合法的类型才允许传入。

TIP

注意,和 TS 的类型定义不同, props 这里的类型,首字母需要大写,也就是 JavaScript 的基本类型。

支持的类型有:

类型含义
String字符串
Number数值
Boolean布尔值
Array数组
Object对象
Date日期数据,e.g. new Date()
Function函数,e.g. 普通函数、箭头函数、构造函数
PromisePromise 类型的函数
SymbolSymbol 类型的值

了解了基本的类型限制用法之后,接下来给 props 加上类型限制:

ts
// Child.vue
+export default defineComponent({
+  props: {
+    title: String,
+    index: Number,
+    userName: String,
+    uid: Number,
+  },
+})

现在如果传入不正确的类型,程序就会抛出警告信息,告知开发者必须正确传值。

如果需要对某个 Prop 允许多类型,比如这个 uid 字段,可能是数值,也可能是字符串,那么可以在类型这里,使用一个数组,把允许的类型都加进去。

ts
// Child.vue
+export default defineComponent({
+  props: {
+    // 单类型
+    title: String,
+    index: Number,
+    userName: String,
+
+    // 这里使用了多种类型
+    uid: [Number, String],
+  },
+})

可选以及带有默认值的 props

注:这一小节的步骤是在 Child.vue 里操作。

所有 props 默认都是可选的,如果不传递具体的值,则默认值都是 undefined ,可能引起程序运行崩溃, Vue 支持对可选的 props 设置默认值,也是通过对象的形式配置 props 的选项。

其中支持配置的选项有:

选项类型含义
typestring类型
requiredboolean是否必传, true 代表必传, false 代表可选
defaultanytype 选项的类型相对应的默认值,如果 required 选项是 false ,但这里不设置默认值,则会默认为 undefined
validatorfunction自定义验证函数,需要 return 一个布尔值, true 代表校验通过, false 代表校验不通过,当校验不通过时,控制台会抛出警告信息

了解了配置选项后,接下来再对 props 进行改造,将其中部分选项设置为可选,并提供默认值:

ts
// Child.vue
+export default defineComponent({
+  props: {
+    // 可选,并提供默认值
+    title: {
+      type: String,
+      required: false,
+      default: '默认标题',
+    },
+
+    // 默认可选,单类型
+    index: Number,
+
+    // 添加一些自定义校验
+    userName: {
+      type: String,
+
+      // 在这里校验用户名必须至少 3 个字
+      validator: (v) => v.length >= 3,
+    },
+
+    // 默认可选,但允许多种类型
+    uid: [Number, String],
+  },
+})

使用 props ~new

注:这一小节的步骤是在 Child.vue 里操作。

<template /> 部分, Vue 3 的使用方法和 Vue 2 是一样的,比如要渲染父组件传入的 props :

vue
<!-- Child.vue -->
+<template>
+  <p>标题:{{ title }}</p>
+  <p>索引:{{ index }}</p>
+  <p>用户id:{{ uid }}</p>
+  <p>用户名:{{ userName }}</p>
+</template>

但是 <script /> 部分,变化非常大!

在 Vue 2 里,只需要通过 this.uidthis.userName 就可以使用父组件传下来的 Prop ,但是 Vue 3 没有了 this ,所以是通过 setup 的入参进行操作。

ts
// Child.vue
+export default defineComponent({
+  props: {
+    title: String,
+    index: Number,
+    userName: String,
+    uid: Number,
+  },
+
+  // 在这里需要添加一个入参
+  setup(props) {
+    // 该入参包含了当前组件定义的所有 props
+    console.log(props)
+  },
+})

关于 Setup 函数的第一个入参 props

  1. 该入参包含了当前组件定义的所有 props (如果父组件 Father.vue 传进来的数据在 Child.vue 里未定义,不仅不会拿到,并且在控制台会有警告信息)。
  2. 该入参可以随意命名,比如可以写成一个下划线 _ ,通过 _.uid 也可以拿到数据,但是语义化命名是一个良好的编程习惯。
  3. 该入参具备响应性,父组件修改了传递下来的值,子组件也会同步得到更新,因此请不要直接解构,可以通过 toRef 或 toRefs API 转换为响应式变量

传递非 props 的属性

上一小节最后有一句提示是:

如果父组件 Father.vue 传进来的数据在 Child.vue 里未定义,不仅不会拿到,并且在控制台会有警告信息。

这种情况虽然无法从 props 里拿到对应的数据,但也不意味着不能传递任何未定义的属性数据,在父组件,除了可以给子组件绑定 props ,还可以根据实际需要去绑定一些特殊的属性。

比如给子组件设置 classid,或者 data-xxx 之类的一些自定义属性,如果子组件 Child.vue 的 <template /> 里只有一个根节点,那么这些属性默认会自动继承并渲染在 Node 节点上。

假设当前在子组件 Child.vue 是如下这样只有一个根节点,并且未接收任何 props :

TIP

如果已安装 Vue VSCode Snippets 这个 VSCode 插件,可以在空的 .vue 文件里输入 v3 ,在出现的代码片段菜单里选择 vbase-3-ts 生成一个 Vue 组件的基础代码片段。

vue
<!-- Child.vue -->
+<template>
+  <div class="child">子组件</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup() {
+    return {}
+  },
+})
+</script>
+
+<style scoped>
+.child {
+  width: 100%;
+}
+</style>

在 Father.vue 里对 Child.vue 传递了多个属性:

vue
<!-- Father.vue -->
+<template>
+  <Child
+    id="child-component"
+    class="class-name-from-father"
+    :keys="['foo', 'bar']"
+    :obj="{ foo: 'bar' }"
+    data-hash="b10a8db164e0754105b7a99be72e3fe5"
+  />
+</template>

回到浏览器,通过 Chrome 的审查元素可以看到子组件 Child.vue 在渲染后,按照 HTML 属性的渲染规则生成了多个属性:

html
<!-- Child.vue 在浏览器里渲染后的 HTML DOM 结构 -->
+<div
+  class="child class-name-from-father"
+  id="child-component"
+  keys="foo,bar"
+  obj="[object Object]"
+  data-hash="b10a8db164e0754105b7a99be72e3fe5"
+  data-v-2dcc19c8=""
+  data-v-7eb2bc79=""
+>
+  子组件
+</div>

TIP

其中有两个以 data-v- 开头的属性是 <style /> 标签开启了 Style Scoped 功能自动生成的 Hash 值。

可以在 Child.vue 配置 inheritAttrsfalse 来屏蔽这些非 props 属性的渲染。

ts
// Child.vue
+export default defineComponent({
+  inheritAttrs: false,
+  setup() {
+    // ...
+  },
+})

关闭了 之后,现在的 DOM 结构如下,只保留了两个由 Style Scoped 生成的 Hash 值:

html
<!-- Child.vue 在浏览器里渲染后的 HTML DOM 解构 -->
+<div class="child" data-v-2dcc19c8="" data-v-7eb2bc79="">子组件</div>

这一类非 props 属性通常称之为 attrs 。

刚接触 Vue 的开发者可能容易混淆这两者,确实是非常接近,都是由父组件传递,由子组件接收,支持传递的数据类型也一样,但为什么一部分是在 props 获取,一部分在 attrs 获取呢?笔者给出一个比较容易记忆的方式,不一定特别准确,但相信可以帮助开发者加深两者的区别理解。

根据它们的缩写,其实是可以知道 Prop 是指 Property ,而 Attr 是指 Attribute ,虽然都是 “属性” ,但 Property 更接近于事物本身的属性,因此需要在组件里声明,而 Attribute 更偏向于赋予的属性,因此用于指代父组件传递的其他未被声明为 Property 的属性。

获取非 props 的属性 ~new

注:这一小节的步骤是在 Child.vue 里操作。

在上一小节 传递非 props 的属性 已经在父组件 Father.vue 里向子组件 Child.vue 传递了一些 attrs 自定义属性,在子组件里想要拿到这些属性,使用原生 JavaScript 操作是需要通过 Element.getAttribute() 方法,但 Vue 提供了更简单的操作方式。

在 Child.vue 里,可以通过 setup 的第二个参数 context 里的 attrs 来获取到这些属性,并且父组件传递了什么类型的值,获取到的也是一样的类型,这一点和使用 Element.getAttribute() 完全不同。

ts
// Child.vue
+export default defineComponent({
+  setup(props, { attrs }) {
+    // `attrs` 是个对象,每个 Attribute 都是它的 `key`
+    console.log(attrs.id) // child-component
+    console.log(attrs.class) // class-name-from-father
+
+    // 传递数组会被保留类型,不会被转换为 `key1,key2` 这样的字符串
+    // 这一点与 `Element.getAttribute()` 完全不同
+    console.log(attrs.keys) // ['foo', 'bar']
+
+    // 传递对象也可以正常获取
+    console.log(attrs.obj) // {foo: 'bar'}
+
+    // 如果传下来的 Attribute 带有短横线,需要通过这种方式获取
+    console.log(attrs['data-hash']) // b10a8db164e0754105b7a99be72e3fe5
+  },
+})

TIP

子组件不论是否设置 inheritAttrs 属性,都可以通过 attrs 拿到父组件传递下来的数据,但是如果要使用 Element.getAttribute() 则只有当 inheritAttrstrue 的时候才可以,因为此时在 DOM 上才会渲染这些属性。

与 Vue 2 的 <template /> 只能有一个根节点不同, Vue 3 允许多个根节点,多个根节点的情况下,无法直接继承这些 attrs 属性(在 inheritAttrs: true 的情况也下无法默认继承),需要在子组件 Child.vue 里通过 v-bind 绑定到要继承在节点上。

可以通过 Vue 实例属性 $attrs 或者从 setup 函数里把 attrs return 出来使用。

vue
<!-- Child.vue -->
+<template>
+  <!-- 默认不会继承属性 -->
+  <div class="child">不会继承</div>
+
+  <!-- 绑定后可继承, `$attrs` 是一个 Vue 提供的实例属性 -->
+  <div class="child" v-bind="$attrs">使用 $attrs 继承</div>
+
+  <!-- 绑定后可继承, `attrs` 是从 `setup` 里 `return` 出来的变量 -->
+  <div class="child" v-bind="attrs">使用 attrs 继承</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup(props, { attrs }) {
+
+    return {
+      attrs,
+    }
+  },
+})
+</script>

绑定 emits ~new

注:这一小节的步骤是在 Father.vue 里操作。

如果父组件 Father.vue 需要获取子组件 Child.vue 的数据更新情况,可以由子组件通过 emits 进行通知,下面这个更新用户年龄的例子可以学习如何给子组件绑定 emit 事件。

事件的逻辑是由父组件决定的,因此需要在父组件 Father.vue 的 <script /> 里先声明数据变量和一个更新函数,并且这个更新函数通常会有一个入参作为数据的新值接收。

在本例子里,父组件声明了一个 updateAge 方法,它接受一个入参 newAge ,代表新的年龄数据,这个入参的值将由子组件 Child.vue 在触发 emits 时传入。

因为还需要在 <template /> 部分绑定给子组件,所以请记得 return 出来。

ts
// Father.vue
+import { defineComponent, reactive } from 'vue'
+import Child from '@cp/Child.vue'
+
+interface Member {
+  id: number
+  name: string
+  age: number
+}
+
+export default defineComponent({
+  components: {
+    Child,
+  },
+  setup() {
+    const userInfo: Member = reactive({
+      id: 1,
+      name: 'Petter',
+      age: 0,
+    })
+
+    /**
+     * 声明一个更新年龄的方法
+     * @param newAge - 新的年龄,由子组件触发 emits 时传递
+     */
+    function updateAge(newAge: number) {
+      userInfo.age = newAge
+    }
+
+    return {
+      userInfo,
+      updateAge,
+    }
+  },
+})

再看 Father.vue 的 <template /> 部分,和 Click 事件使用 @click 一样,自定义的 emits 事件也是通过 v-on 或者是 @ 来绑定:

vue
<!-- Father.vue -->
+<template>
+  <Child @update-age="updateAge" />
+</template>

和 props 一样,官方文档也推荐将 camelCase 风格(小驼峰)命名的函数,在绑定时使用 kebab-case 风格(短横线),例如使用 update-age 代替 updateAge 传递。

接收并调用 emits ~new

注:这一小节的步骤是在 Child.vue 里操作。

和 props 一样,可以指定是一个数组,把要接收的 emit 事件名称写进去:

ts
// Child.vue
+export default defineComponent({
+  emits: ['update-age'],
+})

和 props 不同,通常情况下 emits 这样配置就足够使用了。

接下来如果子组件需要更新数据并通知父组件,可以使用 setup 第二个参数 context 里的 emit 方法触发:

ts
// Child.vue
+export default defineComponent({
+  emits: ['update-age'],
+  setup(props, { emit }) {
+    // 通知父组件将年龄设置为 `2`
+    emit('update-age', 2)
+  },
+})

emit 方法最少要传递一个参数:事件名称。

事件名称是指父组件 Father.vue 绑定事件时 @update-age="updateAge" 里的 update-age ,如果改成 @hello="updateAge" ,那么事件名称就需要使用 hello ,一般情况下事件名称和更新函数的名称会保持一致,方便维护。

对于需要更新数据的情况, emit 还支持传递更多的参数,对应更新函数里的入参,所以可以看到上面例子里的 emit('update-age', 2) 有第二个参数,传递了一个 2 的数值,就是作为父组件 updateAge 的入参 newAge 传递。

如果需要通信的数据很多,建议第二个入参使用一个对象来管理数据,例如父组件调整为:

ts
// Father.vue
+function updateInfo({ name, age }: Member) {
+  // 当 `name` 变化时更新 `name` 的值
+  if (name && name !== userInfo.name) {
+    userInfo.name = name
+  }
+
+  // 当 `age` 变化并且新值在正确的范围内时,更新 `age` 的值
+  if (age > 0 && age !== userInfo.age) {
+    userInfo.age = age
+  }
+}

子组件在传递新数据时,就应该使用对象的形式传递:

ts
// Child.vue
+emit('update-info', {
+  name: 'Tom',
+  age: 18,
+})

这对于更新表单等数据量较多的场景非常好用。

接收 emits 时做一些校验

注:这一小节的步骤是在 Child.vue 里操作。

和 props 一样,子组件在接收 emits 时也可以对这些事件做一些验证,这个时候就需要将 emits 配置为对象,然后把事件名称作为 keyvalue 则对应为一个用来校验的方法。

还是用回上文那个更新年龄的方法,如果需要增加一个条件:当达到成年人的年龄时才会更新父组件的数据,那么就可以将 emits 调整为:

ts
// Child.vue
+export default defineComponent({
+  emits: {
+    // 需要校验
+    'update-age': (age: number) => {
+      // 写一些条件拦截,返回 `false` 表示验证不通过
+      if (age < 18) {
+        console.log('未成年人不允许参与')
+        return false
+      }
+
+      // 通过则返回 `true`
+      return true
+    },
+
+    // 一些无需校验的,设置为 `null` 即可
+    'update-name': null,
+  },
+})

接下来如果提交 emit('update-age', 2) ,因为不满足验证条件,浏览器控制台将会出现一段 [Vue warn]: Invalid event arguments: event validation failed for event "update-age". 这样的警告信息。

v-model / emits

相对于 props / emits 这一对通信方案,使用 v-model 的方式更为简单:

  1. 在 Father.vue ,通过 v-model 向 Child.vue 传值

  2. Child.vue 通过自身设定的 emits 向 Father.vue 通知数据更新

v-model 的用法和 props 非常相似,但是很多操作上更为简化,但操作简单带来的 “副作用” ,就是功能上也没有 props 那么多。

绑定 v-model ~new

注:这一小节的步骤是在 Father.vue 里操作。

和下发 props 的方式类似,都是在子组件上绑定 Father.vue 定义好的数据,这是绑定一个数据的例子:

vue
<!-- Father.vue -->
+<template>
+  <Child v-model:username="userInfo.name" />
+</template>

和 Vue 2 不同, Vue 3 可以直接绑定 v-model ,而无需在子组件指定 model 选项 ,并且 Vue 3 的 v-model 需要使用英文冒号 : 指定要绑定的属性名,同时也支持绑定多个 v-model 。

如果要绑定多个数据,写多个 v-model 即可:

vue
<!-- Father.vue -->
+<template>
+  <Child
+    v-model:uid="userInfo.id"
+    v-model:username="userInfo.name"
+    v-model:age="userInfo.age"
+  />
+</template>

看到这里应该能明白了,一个 v-model 其实就是一个 prop ,它支持的数据类型和 prop 是一样的,所以子组件在接收数据的时候,完全按照 props 去定义就可以了。

点击回顾:接收 props ,了解在 Child.vue 如何接收 props,以及相关的 props 类型限制等部分内容。

配置 emits ~new

注:这一小节的步骤是在 Child.vue 里操作。

虽然 v-model 的配置和 props 相似,但是为什么出这么两个相似的东西?自然是为了简化一些开发上的操作。

使用 props / emits ,如果要更新父组件的数据,还需要在父组件声明一个更新函数并绑定事件给子组件,才能够更新。

而使用 v-model / emits ,无需在父组件声明更新函数,只需要在子组件 Child.vue 里通过 update: 前缀加上 v-model 的属性名这样的格式,即可直接定义一个更新事件。

ts
// Child.vue
+export default defineComponent({
+  props: {
+    uid: Number,
+    username: String,
+    age: Number,
+  },
+  // 注意这里的 `update:` 前缀
+  emits: ['update:uid', 'update:username', 'update:age'],
+})

这里的 update 后面的属性名,支持驼峰写法,这一部分和 Vue 2 的使用是相同的。

在配置 emits 时,也可以对数据更新做一些校验,配置方式和讲解 props / emits 时 接收 emits 时做一些校验 这一小节的操作是一样的。

在 Child.vue 配置好 emits 之后,就可以在 setup 里直接操作数据的更新了:

ts
// Child.vue
+export default defineComponent({
+  setup(props, { emit }) {
+    // 2s 后更新用户名
+    setTimeout(() => {
+      emit('update:username', 'Tom')
+    }, 2000)
+  },
+})

子组件通过调用 emit('update:xxx') 即可让父组件更新对应的数据。

ref / emits

在学习 响应式 API 之 ref 的时候,已讲解过 ref 是可以用在 DOM 元素与子组件 上面,所以也可以使用 ref 配合 emits 完成父子组件的通信。

父组件操作子组件 ~new

注:这一小节的步骤是在 Father.vue 里操作。

父组件可以给子组件绑定 ref 属性,然后通过 Ref 变量操作子组件的数据或者调用子组件里面的方法。

先在 <template /> 处给子组件标签绑定 ref 属性:

vue
<!-- Father.vue -->
+<template>
+  <Child ref="child" />
+</template>

然后在 <script /> 部分定义好对应的变量名称 child (记得要 return 出来哦),即可通过该变量操作子组件上的变量或方法:

ts
// Father.vue
+import { defineComponent, onMounted, ref } from 'vue'
+import Child from '@cp/Child.vue'
+
+export default defineComponent({
+  components: {
+    Child,
+  },
+  setup() {
+    // 给子组件定义一个 `ref` 变量
+    const child = ref<InstanceType<typeof Child>>()
+
+    // 请保证视图渲染完毕后再执行操作
+    onMounted(async () => {
+      // 执行子组件里面的 AJAX 请求函数
+      await child.value!.queryList()
+
+      // 显示子组件里面的弹窗
+      child.value!.isShowDialog = true
+    })
+
+    // 必须 `return` 出去才可以给到 `<template />` 使用
+    return {
+      child,
+    }
+  },
+})

需要注意的是,在子组件 Child.vue 里,变量和方法也需要在 setup 里 return 出来才可以被父组件调用到。

子组件通知父组件

子组件如果想主动向父组件通讯,也需要使用 emits ,详细的配置方法可见:绑定 emits

爷孙组件通信

顾名思义,爷孙组件是比 父子组件通信 要更深层次的引用关系(也有称之为 “隔代组件” )。

C 组件被引入到 B 组件里, B 组件又被引入到 A 组件里渲染,此时 A 是 C 的爷爷级别(可能还有更多层级关系),它们之间的关系可以假设如下:

Grandfather.vue
+└─Son.vue
+  └─Grandson.vue

可以看到 Grandson.vue 并非直接挂载在 Grandfather.vue 下面,他们之间还隔着至少一个 Son.vue (在实际业务中可能存在更多层级),如果使用 props ,只能一级组件一级组件传递下去,就太繁琐了。

因此需要更直接的通信方式来解决这种问题,这一 Part 就是讲一讲 C 和 A 之间的数据传递,常用的方法有:

方案爷组件向孙组件孙组件向爷组件对应章节传送门
provide / injectprovideinject点击查看
EventBusemit / onemit / on点击查看
Reactive State--点击查看
Vuex--点击查看
Pinia--点击查看

因为上下级的关系的一致性,爷孙组件通信的方案也适用于 父子组件通信 ,只需要把爷孙关系换成父子关系即可,为了方便阅读,下面的爷组件统一叫 Grandfather.vue,子组件统一叫 Grandson.vue 。

provide / inject

这个通信方式也是有两部分:

  1. Grandfather.vue 通过 provide 向孙组件 Grandson.vue 提供数据和方法
  2. Grandson.vue 通过 inject 注入爷爷组件 Grandfather.vue 的数据和方法

无论组件层次结构有多深,发起 provide 的组件都可以作为其所有下级组件的依赖提供者。

Vue 3 的这一部分内容对比 Vue 2 来说变化很大,但使用起来其实也很简单,开发者学到这里不用慌,它们之间也有相同的地方:

  1. 爷组件不需要知道哪些子组件使用它 provide 的数据
  2. 子组件不需要知道 inject 的数据来自哪里

另外要切记一点就是: provide 和 inject 绑定并不是可响应的,这是刻意为之的,除非传入了一个可侦听的对象。

发起 provide ~new

注:这一小节的步骤是在 Grandfather.vue 里操作。

先来回顾一下 Vue 2 的用法:

ts
export default {
+  // 在 `data` 选项里定义好数据
+  data() {
+    return {
+      tags: ['中餐', '粤菜', '烧腊'],
+    }
+  },
+  // 在 `provide` 选项里添加要提供的数据
+  provide() {
+    return {
+      tags: this.tags,
+    }
+  },
+}

旧版的 provide 用法和 data 类似,都是配置为一个返回对象的函数,而 Vue 3 的新版 provide ,和 Vue 2 的用法区别比较大。

在 Vue 3 , provide 需要导入并在 setup 里启用,并且现在是一个全新的方法,每次要 provide 一个数据的时候,就要单独调用一次。

provide 的 TS 类型如下:

ts
// `provide` API 本身的类型
+function provide<T>(key: InjectionKey<T> | string, value: T): void
+
+// 入参 `key` 的其中一种类型
+interface InjectionKey<T> extends Symbol {}

每次调用 provide 的时候都需要传入两个参数:

参数说明
key数据的名称
value数据的值

其中 key 一般使用 string 类型就可以满足大部分业务场景,如果有特殊的需要(例如开发插件时可以避免和用户的业务冲突),可以使用 InjectionKey<T> 类型,这是一个继承自 Symbol 的泛型:

ts
import type { InjectionKey } from 'vue'
+const key = Symbol() as InjectionKey<string>

还需要注意的是, provide 不是响应式的,如果要使其具备响应性,需要传入响应式数据,详见:响应性数据的传递与接收

下面来试试在爷组件 Grandfather.vue 里创建数据 provide 下去:

ts
// Grandfather.vue
+import { defineComponent, provide, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 声明一个响应性变量并 provide 其自身
+    // 孙组件获取后可以保持响应性
+    const msg = ref('Hello World!')
+    provide('msg', msg)
+
+    // 只 provide 响应式变量的值
+    // 孙组件获取后只会得到当前的值
+    provide('msgValue', msg.value)
+
+    // 声明一个方法并 provide
+    function printMsg() {
+      console.log(msg.value)
+    }
+    provide('printMsg', printMsg)
+  },
+})

接收 inject ~new

注:这一小节的步骤是在 Grandson.vue 里操作。

也是先回顾一下在 Vue 2 里的用法,和接收 props 类似:

ts
export default {
+  // 通过 `inject` 选项获取
+  inject: ['tags'],
+  mounted() {
+    console.log(this.tags)
+  },
+}

Vue 3 的新版 inject 和 Vue 2 的用法区别也是比较大,在 Vue 3 , inject 和 provide 一样,也是需要先导入然后在 setup 里启用,也是一个全新的方法,每次要 inject 一个数据的时候,也是要单独调用一次。

另外还有一个特殊情况需要注意,当 Grandson.vue 的父级、爷级组件都 provide 了相同名字的数据下来,那么在 inject 的时候,会优先选择离它更近的组件的数据。

根据不同的场景, inject 可以接受不同数量的入参,入参类型也各不相同。

默认用法

默认情况下, inject API 的 TS 类型如下:

ts
function inject<T>(key: InjectionKey<T> | string): T | undefined

每次调用时只需要传入一个参数:

参数类型说明
keystring与 provide 相对应的数据名称

接下来看看如何在孙组件里 inject 爷组件 provide 下来的数据:

ts
// Grandson.vue
+import { defineComponent, inject } from 'vue'
+import type { Ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 获取响应式变量
+    const msg = inject<Ref<string>>('msg')
+    console.log(msg!.value)
+
+    // 获取普通的字符串
+    const msgValue = inject<string>('msgValue')
+    console.log(msgValue)
+
+    // 获取函数
+    const printMsg = inject<() => void>('printMsg')
+    if (typeof printMsg === 'function') {
+      printMsg()
+    }
+  },
+})

可以看到在每个 inject 都使用尖括号 <> 添加了相应的 TS 类型,并且在调用变量的时候都进行了判断,这是因为默认的情况下, inject 除了返回指定类型的数据之外,还默认带上 undefined 作为可能的值。

如果明确数据不会是 undefined ,也可以在后面添加 as 关键字指定其 TS 类型,这样 TypeScript 就不再因为可能出现 undefined 而提示代码有问题。

ts
// Grandson.vue
+import { defineComponent, inject } from 'vue'
+import type { Ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 获取响应式变量
+    const msg = inject('msg') as Ref<string>
+    console.log(msg.value)
+
+    // 获取普通的字符串
+    const msgValue = inject('msgValue') as string
+    console.log(msgValue)
+
+    // 获取函数
+    const printMsg = inject('printMsg') as () => void
+    printMsg()
+  },
+})

设置默认值

inject API 还支持设置默认值,可以接受更多的参数。

默认情况下,只需要传入第二个参数指定默认值即可,此时它的 TS 类型如下,

ts
function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T

对于不可控的情况,建议在 inject 时添加一个兜底的默认值,防止程序报错:

ts
// Grandson.vue
+import { defineComponent, inject, ref } from 'vue'
+import type { Ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 获取响应式变量
+    const msg = inject<Ref<string>>('msg', ref('Hello'))
+    console.log(msg.value)
+
+    // 获取普通的字符串
+    const msgValue = inject<string>('msgValue', 'Hello')
+    console.log(msgValue)
+
+    // 获取函数
+    const printMsg = inject<() => void>('printMsg', () => {
+      console.log('Hello')
+    })
+    printMsg()
+  },
+})

需要注意的是, inject 的什么类型的数据,其默认值也需要保持相同的类型。

工厂函数选项

inject API 在第二个 TS 类型的基础上,还有第三个 TS 类型,可以传入第三个参数:

ts
function inject<T>(
+  key: InjectionKey<T> | string,
+  defaultValue: () => T,
+  treatDefaultAsFactory?: false
+): T

当第二个参数是一个工厂函数,那么可以添加第三个值,将其设置为 true ,此时默认值一定会是其 return 的值。

在 Grandson.vue 里新增一个 inject ,接收一个不存在的函数名,并提供一个工厂函数作为默认值:

ts
// Grandson.vue
+import { defineComponent, inject } from 'vue'
+
+interface Food {
+  name: string
+  count: number
+}
+
+export default defineComponent({
+  setup() {
+    // 获取工厂函数
+    const getFood = inject<() => Food>('nonexistentFunction', () => {
+      return {
+        name: 'Pizza',
+        count: 1,
+      }
+    })
+    console.log(typeof getFood) // function
+
+    const food = getFood()
+    console.log(food) // {name: 'Pizza', count: 1}
+  },
+})

此时因为第三个参数默认为 Falsy 值 ,所以可以得到一个函数作为默认值,并可以调用该函数获得一个 Food 对象。

如果将第三个参数传入为 true ,再运行程序则会在 const food = getFood() 这一行报错:

ts
// Grandson.vue
+import { defineComponent, inject } from 'vue'
+
+interface Food {
+  name: string
+  count: number
+}
+
+export default defineComponent({
+  setup() {
+    // 获取工厂函数
+    const getFood = inject<() => Food>(
+      'nonexistentFunction',
+      () => {
+        return {
+          name: 'Pizza',
+          count: 1,
+        }
+      },
+      true
+    )
+    console.log(typeof getFood) // object
+
+    // 此时下面的代码无法运行
+    // 报错 Uncaught (in promise) TypeError: getMsg is not a function
+    const food = getFood()
+    console.log(food)
+  },
+})

因为此时第三个入参告知 inject ,默认值是一个工厂函数,因此默认值不再是函数本身,而是函数的返回值,所以 typeof getFood 得到的不再是一个 function 而是一个 object

这个参数对于需要通过工厂函数返回数据的情况非常有用!

兄弟组件通信

兄弟组件是指两个组件都挂载在同一个 Father.vue 下,但两个组件之间并没有什么直接的关联,先看看它们的关系:

Father.vue
+├─Brother.vue
+└─LittleBrother.vue

这种层级关系下,如果组件之间要进行通信,目前通常有这两类选择:

  1. 【不推荐】先把数据传给 Father.vue ,再使用 父子组件通信 方案处理
  2. 【推荐】借助 全局组件通信 的方案达到目的

下面的内容将进入全局通信的讲解。

全局组件通信

全局组件通信是指项目下两个任意组件,不管是否有直接关联(例如父子关系、爷孙关系)都可以直接进行交流的通信方案。

举个例子,像下面这种项目结构, B2.vue 可以采用全局通信方案直接向 D2.vue 发起交流,而无需经过它们各自的父组件。

bash
A.vue
+├─B1.vue
+├───C1.vue
+├─────D1.vue
+├─────D2.vue
+├───C2.vue
+├─────D3.vue
+└─B2.vue

常用的方法有:

方案发起方接收方对应章节传送门
EventBusemiton点击查看
Reactive State--点击查看
Vuex--点击查看
Pinia--点击查看

EventBus ~new

EventBus 通常被称之为 “全局事件总线” ,是用在全局范围内通信的一个常用方案,在 Vue 2 时期该方案非常流行,其特点就是 “简单” 、 “灵活” 、 “轻量级” 。

回顾 Vue 2

在 Vue 2 ,使用 EventBus 无需导入第三方插件,可以在项目下的 libs 文件夹里,创建一个名为 eventBus.ts 的文件,导出一个新的 Vue 实例即可。

ts
// src/libs/eventBus.ts
+import Vue from 'vue'
+export default new Vue()

上面短短两句代码已完成了一个 EventBus 的创建,接下来就可以开始进行通信。

先在负责接收事件的组件里,利用 Vue 的生命周期,通过 eventBus.$on 添加事件侦听,通过 eventBus.$off 移除事件侦听。

ts
import eventBus from '@libs/eventBus'
+
+export default {
+  mounted() {
+    // 在组件创建时,添加一个名为 `hello` 的事件侦听
+    eventBus.$on('hello', () => {
+      console.log('Hello World')
+    })
+  },
+  beforeDestroy() {
+    // 在组件销毁前,通过 `hello` 这个名称移除该事件侦听
+    eventBus.$off('hello')
+  },
+}

然后在另外一个组件里通过 eventBus.$emit 触发事件侦听。

ts
import eventBus from './eventBus'
+
+export default {
+  methods: {
+    sayHello() {
+      // 触发名为 `hello` 的事件
+      eventBus.$emit('hello')
+    },
+  },
+}

这样一个简单的全局方案就完成了。

了解 Vue 3 ~new

Vue 3 应用实例不再实现事件触发接口,因此移除了 $on$off$once 这几个事件 API ,无法像 Vue 2 一样利用 Vue 实例创建 EventBus 。

根据官方文档在 事件 API 迁移策略 的推荐,可以使用 mitt 或者 tiny-emitter 等第三方插件实现 EventBus 。

创建 Vue 3 的 EventBus ~new

这里以 mitt 为例,示范如何创建一个 Vue 3 的 EventBus ,首先需要安装它。

npm i mitt

然后在 src/libs 文件夹下,创建一个名为 eventBus.ts 的文件,文件内容和 Vue 2 的写法其实是一样的,只不过是把 Vue 实例换成了 mitt 实例。

ts
// src/libs/eventBus.ts
+import mitt from 'mitt'
+export default mitt()

接下来就可以定义通信的相关事件了,常用的 API 和参数如下:

方法名称作用
on注册一个侦听事件,用于接收数据
emit调用方法发起数据传递
off用来移除侦听事件

on 的参数:

参数类型作用
typestring | symbol方法名
handlerfunction接收到数据之后要做什么处理的回调函数

这里的 handler 建议使用具名函数,因为匿名函数无法销毁。

emit 的参数:

参数类型作用
typestring | symbol与 on 对应的方法名
dataany与 on 对应的,允许接收的数据

off 的参数:

参数类型作用
typestring | symbol与 on 对应的方法名
handlerfunction要被删除的,与 on 对应的 handler 函数名

更多的 API 可以查阅 插件的官方文档 ,在了解了最基本的用法之后,来开始配置一对组件通信。

创建和移除侦听事件 ~new

在需要暴露交流事件的组件里,通过 on 配置好接收方法,同时为了避免路由切换过程中造成事件多次被绑定,从而引起多次触发,需要在适当的时机 off 掉:

ts
import { defineComponent, onBeforeUnmount } from 'vue'
+import eventBus from '@libs/eventBus'
+
+export default defineComponent({
+  setup() {
+    // 声明一个打招呼的方法
+    function sayHi(msg = 'Hello World!') {
+      console.log(msg)
+    }
+
+    // 启用侦听
+    eventBus.on('sayHi', sayHi)
+
+    // 在组件卸载之前移除侦听
+    onBeforeUnmount(() => {
+      eventBus.off('sayHi', sayHi)
+    })
+  },
+})

关于销毁的时机,可以参考 组件的生命周期

调用侦听事件 ~new

在需要调用交流事件的组件里,通过 emit 进行调用:

ts
import { defineComponent } from 'vue'
+import eventBus from '@libs/eventBus'
+
+export default defineComponent({
+  setup() {
+    // 调用打招呼事件,传入消息内容
+    eventBus.emit('sayHi', 'Hello')
+  },
+})

旧项目升级 EventBus

在 Vue 3 的 EventBus 里,可以看到它的 API 和旧版是非常接近的,只是去掉了 $ 符号。

如果要对旧的项目进行升级改造,由于原来都是使用了 $on$emit 等旧的 API ,一个一个组件去修改成新的 API 容易遗漏或者全局替换出错。

因此可以在创建 eventBus.ts 的时候,通过自定义一个 eventBus 对象来挂载 mitt 的 API 。

在 eventBus.ts 里,改成以下代码:

ts
// src/libs/eventBus.ts
+import mitt from 'mitt'
+
+// 初始化一个 mitt 实例
+const emitter = mitt()
+
+// 在导出时使用旧的 API 名称去调用 mitt 的 API
+export default {
+  $on: (...args) => emitter.on(...args),
+  $emit: (...args) => emitter.emit(...args),
+  $off: (...args) => emitter.off(...args),
+}

这样在组件里就可以继续使用 eventBus.$oneventBus.$emit 等旧 API ,不会影响旧项目的升级使用。

Reactive State ~new

在 Vue 3 里,使用响应式的 reactive API 也可以实现一个小型的状态共享库,如果运用在一个简单的 H5 活动页面这样小需求里,完全可以满足使用。

创建状态中心

首先在 src 目录下创建一个 state 文件夹,并添加一个 index.ts 文件,写入以下代码:

ts
// src/state/index.ts
+import { reactive } from 'vue'
+
+// 如果有多个不同业务的内部状态共享
+// 使用具名导出更容易维护
+export const state = reactive({
+  // 设置一个属性并赋予初始值
+  message: 'Hello World',
+
+  // 添加一个更新数据的方法
+  setMessage(msg: string) {
+    this.message = msg
+  },
+})

这就完成了一个简单的 Reactive State 响应式状态中心的创建。

设定状态更新逻辑

接下来在一个组件 Child.vue 的 <script /> 里添加以下代码,分别进行了以下操作:

  1. 打印初始值
  2. 对 state 里的数据启用侦听器
  3. 使用 state 里的方法更新数据
  4. 直接更新 state 的数据
ts
// Child.vue
+import { defineComponent, watch } from 'vue'
+import { state } from '@/state'
+
+export default defineComponent({
+  setup() {
+    console.log(state.message)
+    // Hello World
+
+    // 因为是响应式数据,所以可以侦听数据变化
+    watch(
+      () => state.message,
+      (val) => {
+        console.log('Message 发生变化:', val)
+      }
+    )
+
+    setTimeout(() => {
+      state.setMessage('Hello Hello')
+      // Message 发生变化: Hello Hello
+    }, 1000)
+
+    setTimeout(() => {
+      state.message = 'Hi Hi'
+      // Message 发生变化: Hi Hi
+    }, 2000)
+  },
+})

观察全局状态变化

继续在另外一个组件 Father.vue 里写入以下代码,导入 state 并在 <template /> 渲染其中的数据:

vue
<!-- Father.vue -->
+<template>
+  <div>{{ state.message }}</div>
+  <Child />
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+import Child from '@cp/Child.vue'
+import { state } from '@/state'
+
+export default defineComponent({
+  components: {
+    Child,
+  },
+  setup() {
+    return {
+      state,
+    }
+  },
+})
+</script>

可以观察到当 Child.vue 里的定时器执行时, Father.vue 的视图也会同步得到更新。

一个无需额外插件即可实现的状态中心就这么完成了!

Vuex ~new

Vuex 是 Vue 生态里面非常重要的一个成员,运用于状态管理模式。

它也是一个全局的通信方案,对比 EventBus,Vuex 的功能更多,更灵活,但对应的学习成本和体积也相对较大,通常大型项目才会用上 Vuex 。

在了解之前

摘自 Vuex 仓库 README 文档的一段官方提示:

Pinia is now the new default
The official state management library for Vue has changed to Pinia. Pinia has almost the exact same or enhanced API as Vuex 5, described in Vuex 5 RFC. You could simply consider Pinia as Vuex 5 with a different name. Pinia also works with Vue 2.x as well.
Vuex 3 and 4 will still be maintained. However, it's unlikely to add new functionalities to it. Vuex and Pinia can be installed in the same project. If you're migrating existing Vuex app to Pinia, it might be a suitable option. However, if you're planning to start a new project, we highly recommend using Pinia instead.

意思是 Pinia 已经成为 Vue 生态最新的官方状态管理库,不仅适用于 Vue 3 ,也支持 Vue 2 ,而 Vuex 将进入维护状态,不再增加新功能, Vue 官方强烈建议在新项目中使用 Pinia 。

TIP

笔者建议:如果是全新的项目,建议直接使用 Pinia ,不仅更加适配 Vue 3 组合式 API 的使用,对 TypeScript 的支持也更完善,上手难度和使用舒适度均比 Vuex 更好, Vuex 正在逐渐退出舞台,请根据实际需求决定是否需要启用它。

Vuex 的目录结构

在 Vue 3 里使用 Vuex ,需要选择 4.x 版本,也是当前的 @latest 标签对应的版本,请先安装它。

bash
npm i vuex

接下来按照下面的目录结构创建对应的目录与文件:

bash
src
+ # Vuex 的目录
+├─store
+├───index.ts
+└─main.ts

一般情况下一个 index.ts 足矣,它是 Vuex 的入口文件,如果的项目比较庞大,可以在 store 目录下创建一个命名为 modules 的文件夹,使用 Vuex Modules 的方式导入到 index.ts 里去注册。

回顾 Vue 2

在 Vue 2 ,需要先分别导入 vuevuex,使用 use 方法启用 Vuex 后,通过 new Vuex.Store(...) 的方式进行初始化。

ts
// src/store/index.ts
+import Vue from 'vue'
+import Vuex from 'vuex'
+
+Vue.use(Vuex)
+
+export default new Vuex.Store({
+  state: {},
+  mutations: {},
+  actions: {},
+  modules: {},
+})

之后在组件里就可以通过 this.$store 操作 Vuex 上的方法了。

ts
export default {
+  mounted() {
+    // 通过 `this.$store` 操作 Vuex
+    this.$store.commit('increment')
+    console.log(this.$store.state.count)
+  },
+}

了解 Vue 3 ~new

Vue 3 需要从 Vuex 里导入 createStore 创建实例:

ts
// src/store/index.ts
+import { createStore } from 'vuex'
+
+export default createStore({
+  state: {},
+  mutations: {},
+  actions: {},
+  modules: {},
+})

之后在 src/main.ts 里启用 Vuex :

ts
// src/main.ts
+import { createApp } from 'vue'
+import App from './App.vue'
+import store from './store'
+
+createApp(App)
+  .use(store) // 启用 Vuex
+  .mount('#app')

Vue 3 在组件里使用 Vuex 的方式和 Vue 2 有所不同,需要像使用路由那样通过一个组合式 API useStore 启用。

ts
import { defineComponent } from 'vue'
+import { useStore } from 'vuex'
+
+export default defineComponent({
+  setup() {
+    // 需要创建一个 store 变量
+    const store = useStore()
+
+    // 再使用 store 去操作 Vuex 的 API
+    // ...
+  },
+})

Vuex 的配置

除了初始化方式有一定的改变, Vuex 在 Vue 3 的其他配置和 Vue 2 是一样的。

由于现在在 Vue 3 里已经更推荐使用 Pinia , Vuex 已处于维护状态,因此关于 Vuex 的使用将不展开更多的介绍,有需要的开发者可以查看 Vuex 官网的 使用指南 了解更多。

Pinia ~new

Pinia 和 Vuex 一样,也是 Vue 生态里面非常重要的一个成员,也都是运用于全局的状态管理。

但面向 Componsition API 而生的 Pinia ,更受 Vue 3 喜爱,已被钦定为官方推荐的新状态管理工具。

为了阅读上的方便,对 Pinia 单独开了一章,请在 全局状态的管理 一章阅读。

Released under the MIT License.

+ + + + \ No newline at end of file diff --git a/component.html b/component.html new file mode 100644 index 00000000..5d1dfb44 --- /dev/null +++ b/component.html @@ -0,0 +1,1965 @@ + + + + + + 单组件的编写 | Vue3 入门指南与实战案例 + + + + + + + + + + + + + +
Skip to content
目录

单组件的编写

项目搭好了,第一个需要了解的是 Vue 组件的变化,由于这部分篇幅会非常大,所以会分成很多个小节,一部分一部分按照开发顺序来逐步了解。

因为 Vue 3 对 TypeScript 的支持真的是太完善了,并且 TypeScript 的发展趋势和市场需求度越来越高,所以接下来都将直接使用 TypeScript 进行编程。

TIP

对 TypeScript 不太熟悉的开发者,建议先阅读 快速上手 TypeScript 一章,有了一定的语言基础之后,再一边写代码一边加深印象。

温馨提示

这是笔者写于 2024.02.20 的补充提示,为读者解答为何前面的章节都未提及 script-setup 组件语法的信息。

笔者在执笔之初,考虑到降低初学过程中的各种心智负担,以及当时处于 script-setup 组件语法尚未完全稳定的背景下,所以在最后一章 高效开发 之前的章节,都只提供了组件的标准写法的讲解。

除非遇到框架进行了 Breaking Change (例如从 Vue 2 升级到了 Vue 3 这种大版本更新),否则标准写法是始终有效的,在笔者多年的学习经历和实际收获里,也是比较相信一点:不论学习什么语言或者框架,只要基础打的好,未来总是可以轻松地举一反三去解决遇到的各类问题。

因此如果读者通过各类脚手架创建的 Vue 3 项目,在组件看到 <script setup> 这种风格的组件,以及诸如 defineXxx 风格的 API ,这是 .vue 组件所支持的一种语法糖,可以随时在 高效开发 一章了解相关的 API 用法。

另外从 Vue 3.4 版本之后所新增的 API 未在本书的介绍范围内,请读者在阅读的过程中也可以查阅官方的 API 一起学习,不论是本书还是其他计算机书籍,官方文档永远是最好的查询手册。

TIP

语法糖(英语:Syntactic sugar)是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。

全新的 setup 函数 ~new

在开始编写 Vue 组件之前,需要了解两个全新的前置知识点:

  • 全新的 setup 函数,关系到组件的生命周期和渲染等问题
  • 写 TypeScript 组件离不开的 defineComponent API

setup 的含义

Vue 3 的 Composition API 系列里,推出了一个全新的 setup 函数,它是一个组件选项,在创建组件之前执行,一旦 props 被解析,并作为组合式 API 的入口点。

TIP

说的通俗一点,就是在使用 Vue 3 生命周期的情况下,整个组件相关的业务代码,都可以放在 setup 里执行。

因为在 setup 之后,其他的生命周期才会被启用(点击了解:组件的生命周期)。

基本语法:

ts
// 这是一个基于 TypeScript 的 Vue 组件
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup(props, context) {
+    // 在这里声明数据,或者编写函数并在这里执行它
+
+    return {
+      // 需要给 `<template />` 用的数据或函数,在这里 `return` 出去
+    }
+  },
+})

可以发现在这段代码里还导入了一个 defineComponent API ,也是 Vue 3 带来的新功能,下文的 defineComponent 的作用 将介绍其用法。

在使用 setup 的情况下,请牢记一点:不能再用 this 来获取 Vue 实例,也就是无法和 Vue 2 一样,通过 this.foothis.bar() 这样来获取实例上的数据,或者执行实例上的方法。

关于全新的 Vue 3 组件编写,笔者将在下文一步步说明。

setup 的参数使用

setup 函数包含了两个入参:

参数类型含义是否必传
propsobject由父组件传递下来的数据
contextobject组件的执行上下文

第一个参数 props

它是响应式的,当父组件传入新的数据时,它将被更新。

TIP

请不要解构它,这样会让数据失去响应性,一旦父组件发生数据变化,解构后的变量将无法同步更新为最新的值。

可以使用 Vue 3 全新的响应式 API toRef / toRefs 进行响应式数据转换,下文将会介绍全新的响应式 API 的用法。

第二个参数 context

context 只是一个普通的对象,它暴露三个组件的 Property :

属性类型作用
attrs非响应式对象未在 Props 里定义的属性都将变成 Attrs
slots非响应式对象组件插槽,用于接收父组件传递进来的模板内容
emit方法触发父组件绑定下来的事件

因为 context 只是一个普通对象,所以可以直接使用 ES6 解构。

平时使用可以通过直接传入 { emit } ,即可用 emit('xxx') 来代替使用 context.emit('xxx'),另外两个功能也是如此。

但是 attrsslots 请保持 attrs.xxxslots.xxx 的方式来使用其数据,不要进行解构,虽然这两个属性不是响应式对象,但对应的数据会随组件本身的更新而更新。

两个参数的具体使用,可查阅 组件之间的通信 一章详细了解。

defineComponent 的作用

defineComponent 是 Vue 3 推出的一个全新 API ,可用于对 TypeScript 代码的类型推导,帮助开发者简化掉很多编码过程中的类型声明。

比如,原本需要这样才可以使用 setup 函数:

ts
import { Slots } from 'vue'
+
+// 声明 `props` 和 `return` 的数据类型
+interface Data {
+  [key: string]: unknown
+}
+
+// 声明 `context` 的类型
+interface SetupContext {
+  attrs: Data
+  slots: Slots
+  emit: (event: string, ...args: unknown[]) => void
+}
+
+// 使用的时候入参要加上声明, `return` 也要加上声明
+export default {
+  setup(props: Data, context: SetupContext): Data {
+    // ...
+
+    return {
+      // ...
+    }
+  },
+}

每个组件都这样进行类型声明,会非常繁琐,如果使用了 defineComponent ,就可以省略这些类型声明:

ts
import { defineComponent } from 'vue'
+
+// 使用 `defineComponent` 包裹组件的内部逻辑
+export default defineComponent({
+  setup(props, context) {
+    // ...
+
+    return {
+      // ...
+    }
+  },
+})

代码量瞬间大幅度减少,只要是 Vue 本身的 API , defineComponent 都可以自动推导其类型,这样开发者在编写组件的过程中,只需要维护自己定义的数据类型就可以了,可专注于业务。

组件的生命周期 ~new

在了解了 Vue 3 组件的两个前置知识点后,不着急写组件,还需要先了解组件的生命周期,这个知识点非常重要,只有理解并记住组件的生命周期,才能够灵活的把控好每一处代码的执行,使程序的运行结果可以达到预期。

升级变化

从 Vue 2 升级到 Vue 3 ,在保留对 Vue 2 的生命周期支持的同时,Vue 3 也带来了一定的调整。

Vue 2 的生命周期写法名称是 Options API (选项式 API ), Vue 3 新的生命周期写法名称是 Composition API (组合式 API )。

Vue 3 组件默认支持 Options API ,而 Vue 2 可以通过 @vue/composition-api 插件获得 Composition API 的功能支持(其中 Vue 2.7 版本内置了该插件, 2.6 及以下的版本需要单独安装)。

为了减少理解成本,笔者将从读者的使用习惯上,使用 “ Vue 2 的生命周期” 代指 Options API 写法,用 “ Vue 3 的生命周期” 代指 Composition API 写法。

关于 Vue 生命周期的变化,可以从下表直观地了解:

Vue 2 生命周期Vue 3 生命周期执行时间说明
beforeCreatesetup组件创建前执行
createdsetup组件创建后执行
beforeMountonBeforeMount组件挂载到节点上之前执行
mountedonMounted组件挂载完成后执行
beforeUpdateonBeforeUpdate组件更新之前执行
updatedonUpdated组件更新完成之后执行
beforeDestroyonBeforeUnmount组件卸载之前执行
destroyedonUnmounted组件卸载完成后执行
errorCapturedonErrorCaptured当捕获一个来自子孙组件的异常时激活钩子函数

可以看到 Vue 2 生命周期里的 beforeCreatecreated ,在 Vue 3 里已被 setup 替代。

熟悉 Vue 2 的开发者应该都知道 Vue 有一个全局组件 <KeepAlive /> ,用于在多个组件间动态切换时缓存被移除的组件实例,当组件被包含在 <KeepAlive /> 组件里时,会多出两个生命周期钩子函数:

Vue 2 生命周期Vue 3 生命周期执行时间说明
activatedonActivated被激活时执行
deactivatedonDeactivated切换组件后,原组件消失前执行

TIP

虽然 Vue 3 依然支持 Vue 2 的生命周期,但是不建议混搭使用,前期可以继续使用 Vue 2 的生命周期作为过度阶段慢慢适应,但还是建议尽快熟悉并完全使用 Vue 3 的生命周期编写组件。

使用 3.x 的生命周期

在 Vue 3 的 Composition API 写法里,每个生命周期函数都要先导入才可以使用,并且所有生命周期函数统一放在 setup 里运行。

如果需要达到 Vue 2 的 beforeCreatecreated 生命周期的执行时机,直接在 setup 里执行函数即可。

以下是几个生命周期的执行顺序对比:

ts
import { defineComponent, onBeforeMount, onMounted } from 'vue'
+
+export default defineComponent({
+  setup() {
+    console.log(1)
+
+    onBeforeMount(() => {
+      console.log(2)
+    })
+
+    onMounted(() => {
+      console.log(3)
+    })
+
+    console.log(4)
+  },
+})

最终将按照生命周期的顺序输出:

js
// 1
+// 4
+// 2
+// 3

组件的基本写法

如果想在 Vue 2 里使用 TypeScript 编写组件,需要通过 Options API 的 Vue.extend 语法,或者是另外一种风格 Class Component 的语法声明组件,其中为了更好的进行类型推导, Class Component 语法更受开发者欢迎。

但是 Class Component 语法和默认的组件语法相差较大,带来了一定的学习成本,对于平时编写 JavaScript 代码很少使用 Class 的开发者,适应时间应该也会比较长。

因此 Vue 3 在保留对 Class Component 支持的同时,推出了全新的 Function-based Component ,更贴合 JavaScript 的函数式编程风格,这也是接下来要讲解并贯穿全文使用的 Composition API 新写法。

Composition API 虽然也是一个步伐迈得比较大的改动,但其组件结构并没有特别大的变化,区别比较大的地方在于组件生命周期和响应式 API 的使用,只要掌握了这些核心功能,上手 Vue 3 非常容易!

看到这里可能有开发者心里在想:

“这几种组件写法,加上视图部分又有 Template 和 TSX 的写法之分,生命周期方面 Vue 3 对 Vue 2 的写法又保持了兼容,在 Vue 里写 TypeScript 的组合方式一只手数不过来,在入门时选择合适的编程风格就遇到了困难,可怎么办?”

不用担心!笔者将九种常见的组合方式以表格的形式进行对比, Vue 3 组件最好的写法一目了然!

回顾 Vue 2

在 Vue 2 ,常用以下三种写法声明 TypeScript 组件:

适用版本基本写法视图写法
Vue 2Vue.extendTemplate
Vue 2Class ComponentTemplate
Vue 2Class ComponentTSX

其中最接近 Options API 的写法是使用 Vue.extend API 声明组件:

ts
// 这是一段摘选自 Vue 2 官网的代码演示
+import Vue from 'vue'
+
+// 推荐使用 Vue.extend 声明组件
+const Component = Vue.extend({
+  // 类型推断已启用
+})
+
+// 不推荐这种方式声明
+const Component = {
+  // 这里不会有类型推断,
+  // 因为 TypeScript 不能确认这是 Vue 组件的选项
+}

而为了更好地获得 TypeScript 类型推导支持,通常使用 Class Component 的写法,这是 Vue 官方推出的一个装饰器插件(需要单独安装):

ts
// 这是一段摘选自 Vue 2 官网的代码演示
+import Vue from 'vue'
+import Component from 'vue-class-component'
+
+// @Component 修饰符注明了此类为一个 Vue 组件
+@Component({
+  // 所有的组件选项都可以放在这里
+  template: '<button @click="onClick">Click!</button>',
+})
+
+// 使用 Class 声明一个组件
+export default class MyComponent extends Vue {
+  // 初始数据可以直接声明为实例的 property
+  message: string = 'Hello!'
+
+  // 组件方法也可以直接声明为实例的方法
+  onClick(): void {
+    window.alert(this.message)
+  }
+}

可在 Vue 2 官网的 TypeScript 支持 一章了解更多配置说明。

了解 Vue 3 ~new

Vue 3 从设计初期就考虑了 TypeScript 的支持,其中 defineComponent 这个 API 就是为了解决 Vue 2 对 TypeScript 类型推导不完善等问题而推出的。

在 Vue 3 ,至少有以下六种写法可以声明 TypeScript 组件:

适用版本基本写法视图写法生命周期版本官方是否推荐
Vue 3Class ComponentTemplateVue 2×
Vue 3defineComponentTemplateVue 2×
Vue 3defineComponentTemplateVue 3
Vue 3Class ComponentTSXVue 2×
Vue 3defineComponentTSXVue 2×
Vue 3defineComponentTSXVue 3

其中 defineComponent + Composition API + Template 的组合是 Vue 官方最为推荐的组件声明方式,本书接下来的内容都会以这种写法作为示范案例,也推荐开发者在学习的过程中,使用该组合进行入门。

下面看看如何使用 Composition API 编写一个最简单的 Hello World 组件:

vue
<!-- Template 代码和 Vue 2 一样 -->
+<template>
+  <p class="msg">{{ msg }}</p>
+</template>
+
+<!-- Script 代码需要使用 Vue 3 的新写法-->
+<script lang="ts">
+// Vue 3 的 API 需要导入才能使用
+import { defineComponent } from 'vue'
+
+// 使用 `defineComponent` 包裹组件代码
+// 即可获得完善的 TypeScript 类型推导支持
+export default defineComponent({
+  setup() {
+    // 在 `setup` 方法里声明变量
+    const msg = 'Hello World!'
+
+    // 将需要在 `<template />` 里使用的变量 `return` 出去
+    return {
+      msg,
+    }
+  },
+})
+</script>
+
+<!-- CSS 代码和 Vue 2 一样 -->
+<style scoped>
+.msg {
+  font-size: 14px;
+}
+</style>

可以看到 Vue 3 的组件也是 <template /> + <script /> + <style /> 的三段式组合,上手非常简单。

其中 Template 沿用了 Vue 2 时期类似 HTML 风格的模板写法, Style 则是使用原生 CSS 语法或者 Less 等 CSS 预处理器编写。

但需要注意的是,在 Vue 3 的 Composition API 写法里,数据或函数如果需要在 <template /> 中使用,就必须在 setup 里将其 return 出去,而仅在 <script /> 里被调用的函数或变量,不需要渲染到模板则无需 return

响应式数据的变化 ~new

响应式数据是 MVVM 数据驱动编程的特色, Vue 的设计也是受 MVVM 模型的启发,相信大部分开发者选择 MVVM 框架都是因为数据驱动编程比传统的事件驱动编程要来得方便,而选择 Vue ,则是方便中的方便。

TIP

Model-View-ViewModel (简称 MVVM ) 是一种软件架构模式,将视图 UI 和业务逻辑分开,通过对逻辑数据的修改即可驱动视图 UI 的更新,因此常将这种编程方式称为 “数据驱动” ,与之对应的需要操作 DOM 才能完成视图更新的编程方式则称为 “事件驱动” 。

设计上的变化

作为最重要的一个亮点, Vue 3 的响应式数据在设计上和 Vue 2 有着很大的不同。

回顾 Vue 2

Vue 2 是使用了 Object.defineProperty API 的 getter/setter 来实现数据的响应性,这个方法的具体用法可以参考 MDN 的文档: Object.defineProperty - MDN

下面使用 Object.defineProperty 实现一个简单的双向绑定 demo ,亲自敲代码试一下可以有更多的理解:

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>DefineProperty Demo</title>
+  </head>
+  <body>
+    <!-- 输入框和按钮 -->
+    <div>
+      <input type="text" id="input" />
+      <button onclick="vm.text = 'Hello World'">设置为 Hello World</button>
+    </div>
+    <!-- 输入框和按钮 -->
+
+    <!-- 文本展示 -->
+    <div id="output"></div>
+    <!-- 文本展示 -->
+
+    <script>
+      // 声明一个响应式数据
+      const vm = {}
+      Object.defineProperty(vm, 'text', {
+        set(value) {
+          document.querySelector('#input').value = value
+          document.querySelector('#output').innerText = value
+        },
+      })
+
+      // 处理输入行为
+      document.querySelector('#input').oninput = function (e) {
+        vm.text = e.target.value
+      }
+    </script>
+  </body>
+</html>

这个小 demo 实现了这两个功能:

  1. 输入框的输入行为只修改 vm.text 的数据,但会同时更新 output 标签的文本内容
  2. 点击按钮修改 vm.text 的数据,也会触发输入框和 output 文本的更新

当然 Vue 做了非常多的工作,而非只是简单的调用了 Object.defineProperty ,可以在官网 深入 Vue 2 的响应式原理 一章了解更多 Vue 2 的响应式原理。

了解 Vue 3

Vue 3 是使用了 Proxy API 的 getter/setter 来实现数据的响应性,这个方法的具体用法可以参考 MDN 的文档: Proxy - MDN

同样的,也来实现一个简单的双向绑定 demo ,这次使用 Proxy 来实现:

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Proxy Demo</title>
+  </head>
+  <body>
+    <!-- 输入框和按钮 -->
+    <div>
+      <input type="text" id="input" />
+      <button onclick="vm.text = 'Hello World'">设置为 Hello World</button>
+    </div>
+    <!-- 输入框和按钮 -->
+
+    <!-- 文本展示 -->
+    <div id="output"></div>
+    <!-- 文本展示 -->
+
+    <script>
+      // 声明一个响应式数据
+      const vm = new Proxy(
+        {},
+        {
+          set(obj, key, value) {
+            document.querySelector('#input').value = value
+            document.querySelector('#output').innerText = value
+          },
+        }
+      )
+
+      // 处理输入行为
+      document.querySelector('#input').oninput = function (e) {
+        vm.text = e.target.value
+      }
+    </script>
+  </body>
+</html>

这个 demo 实现的功能和使用 Object.defineProperty 的 demo 是完全一样的,也都是基于 setter 的行为完成数据更新的实现,那么为什么 Vue 3 要舍弃 Object.defineProperty ,换成 Proxy 呢?

主要原因在于 Object.defineProperty 有以下的不足:

  1. 无法侦听数组下标的变化,通过 arr[i] = newValue 这样的操作无法实时响应
  2. 无法侦听数组长度的变化,例如通过 arr.length = 10 去修改数组长度,无法响应
  3. 只能侦听对象的属性,对于整个对象需要遍历,特别是多级对象更是要通过嵌套来深度侦听
  4. 使用 Object.assign() 等方法给对象添加新属性时,也不会触发更新
  5. 更多细节上的问题 …

这也是为什么 Vue 2 要提供一个 Vue.set API 的原因,可以在官网 Vue 2 中检测变化的注意事项 一章了解更多说明。

而这些问题在 Proxy 都可以得到解决,可以在官网 深入 Vue 3 的响应式原理 一章了解更多这部分的内容。

用法上的变化

本书只使用 Composition API 编写组件,这是使用 Vue 3 的最大优势。

TIP

虽然官方文档在各个 API 的使用上都做了一定的举例,但在实际使用过程中可能会遇到一些问题,常见的情况就是有些数据用着用着就失去了响应,或者是在 TypeScript 里出现类型不匹配的报错等等。

当然,一般遇到这种情况并不一定是框架的 BUG ,而可能是使用方式不对,本章节将结合笔者最初入门 Vue 3 时遇到的问题和解决问题的心得,复盘这些响应式 API 的使用。

相对于 Vue 2 在 data 里声明后即可通过 this.xxx 调用响应式数据,在 Vue 3 的生命周期里没有了 Vue 实例的 this 指向,需要导入 refreactive 等响应式 API 才能声明并使用响应式数据。

ts
// 这里导入的 `ref` 是一个响应式 API
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 通过响应式 API 创建的变量具备了响应性
+    const msg = ref<string>('Hello World!')
+  },
+})

由于 Vue 3 新的 API 非常多,但有些 API 的使用场景却不多,因此本书当前只对常用的部分 API 的使用和常见问题进行说明,更多的 API 可以在官方文档的 响应性 API 一章查阅。

响应式 API 之 ref ~new

ref 是最常用的一个响应式 API,它可以用来定义所有类型的数据,包括 Node 节点和组件。

没错,在 Vue 2 常用的 this.$refs.xxx 来取代 document.querySelector('.xxx') 获取 Node 节点的方式,也是使用这个 API 来取代。

类型声明

在开始使用 API 之前,需要先了解在 TypeScript 中如何声明 Ref 变量的类型。

API 本身的类型

先看 API 本身, ref API 是一个函数,通过接受一个泛型入参,返回一个响应式对象,所有的值都通过 .value 属性获取,这是 API 本身的 TS 类型:

ts
// `ref` API 的 TS 类型
+function ref<T>(value: T): Ref<UnwrapRef<T>>
+
+// `ref` API 的返回值的 TS 类型
+interface Ref<T> {
+  value: T
+}

因此在声明变量时,是使用尖括号 <> 包裹其 TS 类型,紧跟在 ref API 之后:

ts
// 显式指定 `msg.value` 是 `string` 类型
+const msg = ref<string>('Hello World!')

再回看该 API 本身的类型,其中使用了 T 泛型,这表示在传入函数的入参时,可以不需要手动指定其 TS 类型, TypeScript 会根据这个 API 所返回的响应式对象的 .value 属性的类型,确定当前变量的类型。

因此也可以省略显式的类型指定,像下面这样声明变量,其类型交给 TypeScript 去自动推导:

ts
// TypeScript 会推导 `msg.value` 是 `string` 类型
+const msg = ref('Hello World')

对于声明时会赋予初始值,并且在使用过程中不会改变其类型的变量,是可以省略类型的显式指定的。

而如果有显式的指定的类型,那么在一些特殊情况下,初始化时可以不必赋值,这样 TypeScript 会自动添加 undefined 类型:

ts
const msg = ref<string>()
+console.log(msg.value) // undefined
+
+msg.value = 'Hello World!'
+console.log(msg.value) // Hello World!

因为入参留空时,虽然指定了 string 类型,但实际上此时的值是 undefined ,因此实际上这个时候的 msg.value 是一个 string | undefined 的联合类型。

对于声明时不知道是什么值,在某种条件下才进行初始化的情况,就可以省略其初始值,但是切记在调用该变量的时候对 .value 值进行有效性判断。

而如果既不显式指定类型,也不赋予初始值,那么会被默认为 any 类型,除非真的无法确认类型,否则不建议这么做。

API 返回值的类型

细心的开发者还会留意到 ref API 类型里面还标注了一个返回值的 TS 类型:

ts
interface Ref<T> {
+  value: T
+}

它是代表整个 Ref 变量的完整类型:

  • 上文声明 Ref 变量时,提到的 string 类型都是指 msg.value 这个 .value 属性的类型
  • msg 这个响应式变量,其本身是 Ref<string> 类型

如果在开发过程中需要在函数里返回一个 Ref 变量,那么其 TypeScript 类型就可以这样写(请留意 Calculator 里的 num 变量的类型):

ts
// 导入 `ref` API
+import { ref } from 'vue'
+// 导入 `ref` API 的返回值类型
+import type { Ref } from 'vue'
+
+// 声明 `useCalculator` 函数的返回值类型
+interface Calculator {
+  // 这里包含了一个 Ref 变量
+  num: Ref<number>
+  add: () => void
+}
+
+// 声明一个 “使用计算器” 的函数
+function useCalculator(): Calculator {
+  const num = ref<number>(0)
+
+  function add() {
+    num.value++
+  }
+
+  return {
+    num,
+    add,
+  }
+}
+
+// 在执行使用计算器函数时,可以获取到一个 Ref 变量和其他方法
+const { num, add } = useCalculator()
+add()
+console.log(num.value) // 1

上面这个简单的例子演示了如何手动指定 Ref 变量的类型,对于逻辑复用时的函数代码抽离、插件开发等场景非常有用!当然大部分情况下可以交给 TypeScript 自动推导,但掌握其用法,在必要的时候就派得上用场了!

变量的定义

在了解了如何对 Ref 变量进行类型声明之后,面对不同的数据类型,相信都得心应手了!但不同类型的值之间还是有少许差异和注意事项,例如上文提及到该 API 可以用来定义所有类型的数据,包括 Node 节点和组件,具体可以参考下文的示例。

基本类型

对字符串、布尔值等基本类型的定义方式,比较简单:

ts
// 字符串
+const msg = ref<string>('Hello World!')
+
+// 数值
+const count = ref<number>(1)
+
+// 布尔值
+const isVip = ref<boolean>(false)

引用类型

对于对象、数组等引用类型也适用,比如要定义一个对象:

ts
// 先声明对象的格式
+interface Member {
+  id: number
+  name: string
+}
+
+// 在定义对象时指定该类型
+const userInfo = ref<Member>({
+  id: 1,
+  name: 'Tom',
+})

定义一个普通数组:

ts
// 数值数组
+const uids = ref<number[]>([1, 2, 3])
+
+// 字符串数组
+const names = ref<string[]>(['Tom', 'Petter', 'Andy'])

定义一个对象数组:

ts
// 声明对象的格式
+interface Member {
+  id: number
+  name: string
+}
+
+// 定义一个对象数组
+const memberList = ref<Member[]>([
+  {
+    id: 1,
+    name: 'Tom',
+  },
+  {
+    id: 2,
+    name: 'Petter',
+  },
+])

DOM 元素与子组件

除了可以定义数据,ref 也有熟悉的用途,就是用来挂载节点,也可以挂在子组件上,也就是对应在 Vue 2 时常用的 this.$refs.xxx 获取 DOM 元素信息的作用。

模板部分依然是熟悉的用法,在要引用的 DOM 上添加一个 ref 属性:

vue
<template>
+  <!-- 给 DOM 元素添加 `ref` 属性 -->
+  <p ref="msg">请留意该节点,有一个 ref 属性</p>
+
+  <!-- 子组件也是同样的方式添加 -->
+  <Child ref="child" />
+</template>

<script /> 部分有三个最基本的注意事项:

  1. <template /> 代码里添加的 ref 属性的值,是对应 <script /> 里使用 ref API 声明的变量的名称;

  2. 请保证视图渲染完毕后再执行 DOM 或组件的相关操作(需要放到生命周期的 onMounted 或者 nextTick 函数里,这一点在 Vue 2 也是一样);

  3. 该 Ref 变量必须 return 出去才可以给到 <template /> 使用,这一点是 Vue 3 生命周期的硬性要求,子组件的数据和方法如果要给父组件操作,也要 return 出来才可以。

配合上面的 <template /> ,来看看 <script /> 部分的具体例子:

ts
import { defineComponent, onMounted, ref } from 'vue'
+import Child from '@cp/Child.vue'
+
+export default defineComponent({
+  components: {
+    Child,
+  },
+  setup() {
+    // 定义挂载节点,声明的类型详见下方附表
+    const msg = ref<HTMLElement>()
+    const child = ref<InstanceType<typeof Child>>()
+
+    // 请保证视图渲染完毕后再执行节点操作 e.g. `onMounted` / `nextTick`
+    onMounted(() => {
+      // 比如获取 DOM 的文本
+      console.log(msg.value.innerText)
+
+      // 或者操作子组件里的数据
+      child.value.isShowDialog = true
+    })
+
+    // 必须 `return` 出去才可以给到 `<template />` 使用
+    return {
+      msg,
+      child,
+    }
+  },
+})

关于 DOM 和子组件的 TS 类型声明,可参考以下规则:

节点类型声明类型参考文档
DOM 元素使用 HTML 元素接口HTML 元素接口
子组件使用 InstanceType 配合 typeof 获取子组件的类型typeof 操作符

TIP

单纯使用 typeof Child 虽然可以获得 Child.vue 组件的 Props 和方法等提示,但在 VSCode 的类型推导还不够智能,缺乏更有效的代码补全支持。

上文使用的 InstanceType<T> 是 TypeScript 提供的一个工具类型,可以获取构造函数类型的实例类型,因此将组件的类型声明为 InstanceType<typeof Child> ,不仅可以得到更完善的类型提示,在编程过程中还可以让编辑器提供更完善的代码补全功能。

另外,关于这一小节,有一个可能会引起 TS 编译报错的情况是,一些脚手架创建出来的项目会默认启用 --strictNullChecks 选项,会导致案例中的代码无法正常编译,出现如下报错:

bash
 npm run build
+
+> hello-vue3@0.0.0 build
+> vue-tsc --noEmit && vite build
+
+src/views/home.vue:27:7 - error TS2532: Object is possibly 'undefined'.
+
+27       child.value.isShowDialog = true
+         ~~~~~~~~~~~
+
+
+Found 1 error in src/views/home.vue:27

这是因为在默认情况下 nullundefined 是所有类型的子类型,但开启了 strictNullChecks 选项之后,会使 nullundefined 只能赋值给 void 和它们各自,这是一个更为严谨的选项,可以保障程序代码的健壮性,但对于刚接触 TypeScript 不久的开发者可能不太友好。

有以下几种解决方案可以参考:

  1. 在涉及到相关操作的时候,对节点变量增加一个判断:
ts
// 添加 `if` 分支,判断 `.value` 存在时才执行相关代码
+if (child.value) {
+  // 读取子组件的数据
+  console.log(child.value.num)
+
+  // 执行子组件的方法
+  child.value.sayHi('Use `if` in `onMounted` API.')
+}
  1. 通过 TS 的可选符 ? 将目标设置为可选,避免出现错误(这个方式不能直接修改子组件数据的值):
ts
// 读取子组件的数据(留意 `.num` 前面有一个 `?` 问号)
+console.log(child.value?.num)
+
+// 执行子组件的方法(留意 `.sayHi` 前面有一个 `?` 问号)
+child.value?.sayHi('use ? in onMounted')
  1. 在项目根目录下的 tsconfig.json 文件里,显式的关闭 strictNullChecks 选项,关闭后,需要开发者在写代码的时候,自行把控好是否需要对 nullundefined 进行判断:
json
{
+  "compilerOptions": {
+    // ...
+    "strictNullChecks": false
+  }
+  // ...
+}
  1. 使用 any 类型代替,但是写 TypeScript 还是尽量不要使用 any ,满屏的 AnyScript 不如直接使用 JavaScript

变量的读取与赋值

前面在介绍 API 类型的时候已经了解,通过 ref 声明的变量会全部变成对象,不管定义的是什么类型的值,都会转化为一个 Ref 对象,其中 Ref 对象具有指向内部值的单个 Property .value

也就是说,任何 Ref 对象的值都必须通过 xxx.value 才可以正确获取。

请牢记上面这句话,初拥 Vue 3 的开发者很多 BUG 都是由于这个问题引起的(包括笔者刚开始使用 Vue 3 的那段时间,嘿嘿)。

读取变量

平时对于普通变量的值,读取的时候都是直接调用其变量名即可:

ts
// 读取一个字符串
+const msg: string = 'Hello World!'
+console.log(msg)
+
+// 读取一个数组
+const uids: number[] = [1, 2, 3]
+console.log(uids[1])

而 Ref 对象的值的读取,切记!必须通过 .value

ts
// 读取一个字符串
+const msg = ref<string>('Hello World!')
+console.log(msg.value)
+
+// 读取一个数组
+const uids = ref<number[]>([1, 2, 3])
+console.log(uids.value[1])

为变量赋值

普通变量需要使用 let 声明才可以修改其值,由于 Ref 对象是个引用类型,所以可以使用 const 声明,直接通过 .value 修改。

ts
// 声明一个字符串变量
+const msg = ref<string>('Hi!')
+
+// 等待 1s 后修改它的值
+setTimeout(() => {
+  msg.value = 'Hello!'
+}, 1000)

因此日常业务中,像在对接服务端 API 的接口数据时,可以自由的使用 forEachmapfilter 等方法操作 Ref 数组,或者直接重置它,而不必担心数据失去响应性。

ts
const data = ref<string[]>([])
+
+// 提取接口的数据
+data.value = api.data.map((item: any) => item.text)
+
+// 重置数组
+data.value = []

为什么突然要说这个呢?因为涉及到下一部分的知识,关于 reactive API 在使用上的注意事项。

响应式 API 之 reactive ~new

reactive 是继 ref 之后最常用的一个响应式 API 了,相对于 ref ,它的局限性在于只适合对象、数组。

TIP

使用 reactive 的好处就是写法跟平时的对象、数组几乎一模一样,但它也带来了一些特殊注意点,请留意赋值部分的特殊说明。

类型声明与定义

reactive 变量的声明方式没有 ref 的变化那么大,基本上和普通变量一样,它的 TS 类型如下:

ts
function reactive<T extends object>(target: T): UnwrapNestedRefs<T>

可以看到其用法还是比较简单的,下面是一个 Reactive 对象的声明方式:

ts
// 声明对象的类型
+interface Member {
+  id: number
+  name: string
+}
+
+// 定义一个对象
+const userInfo: Member = reactive({
+  id: 1,
+  name: 'Tom',
+})

下面是 Reactive 数组的声明方式:

ts
const uids: number[] = reactive([1, 2, 3])

还可以声明一个 Reactive 对象数组:

ts
// 对象数组也是先声明其中的对象类型
+interface Member {
+  id: number
+  name: string
+}
+
+// 再定义一个为对象数组
+const userList: Member[] = reactive([
+  {
+    id: 1,
+    name: 'Tom',
+  },
+  {
+    id: 2,
+    name: 'Petter',
+  },
+  {
+    id: 3,
+    name: 'Andy',
+  },
+])

变量的读取与赋值

虽然 reactive API 在使用上没有像 ref API 一样有 .value 的心智负担,但也有一些注意事项要留意。

处理对象

Reactive 对象在读取字段的值,或者修改值的时候,与普通对象是一样的,这部分没有太多问题。

ts
// 声明对象的类型
+interface Member {
+  id: number
+  name: string
+}
+
+// 定义一个对象
+const userInfo: Member = reactive({
+  id: 1,
+  name: 'Tom',
+})
+
+// 读取用户名
+console.log(userInfo.name)
+
+// 修改用户名
+userInfo.name = 'Petter'

处理数组

但是对于 Reactive 数组,和普通数组会有一些区别。

普通数组在 “重置” 或者 “修改值” 时都是可以直接操作:

ts
// 定义一个普通数组
+let uids: number[] = [1, 2, 3]
+
+// 从另外一个对象数组里提取数据过来
+uids = api.data.map((item: any) => item.id)
+
+// 合并另外一个数组
+let newUids: number[] = [4, 5, 6]
+uids = [...uids, ...newUids]
+
+// 重置数组
+uids = []

Vue 2 在操作数组的时候,也可以和普通数组这样处理数据的变化,依然能够保持响应性,但在 Vue 3 ,如果使用 reactive 定义数组,则不能这么处理,必须只使用那些不会改变引用地址的操作。

笔者刚开始接触时,按照原来的思维去处理 reactive 数组,于是遇到了 “数据变了,但模板不会更新的问题” ,如果开发者在学习的过程中也遇到了类似的情况,可以从这里去入手排查问题所在。

举个例子,比如要从服务端 API 接口获取翻页数据时,通常要先重置数组,再异步添加数据,如果使用常规的重置,会导致这个变量失去响应性:

ts
let uids: number[] = reactive([1, 2, 3])
+
+/**
+ * 不推荐使用这种方式,会丢失响应性
+ * 异步添加数据后,模板不会响应更新
+ */
+uids = []
+
+// 异步获取数据后,模板依然是空数组
+setTimeout(() => {
+  uids.push(1)
+}, 1000)

要让数据依然保持响应性,则必须在关键操作时,不破坏响应性 API ,以下是推荐的操作方式,通过重置数组的 length 长度来实现数据的重置:

ts
const uids: number[] = reactive([1, 2, 3])
+
+/**
+ * 推荐使用这种方式,不会破坏响应性
+ */
+uids.length = 0
+
+// 异步获取数据后,模板可以正确的展示
+setTimeout(() => {
+  uids.push(1)
+}, 1000)

特别注意

不要对 Reactive 数据进行 ES6 的解构 操作,因为解构后得到的变量会失去响应性。

比如这些情况,在 2s 后都得不到新的 name 信息:

ts
import { defineComponent, reactive } from 'vue'
+
+interface Member {
+  id: number
+  name: string
+}
+
+export default defineComponent({
+  setup() {
+    // 定义一个带有响应性的对象
+    const userInfo: Member = reactive({
+      id: 1,
+      name: 'Petter',
+    })
+
+    // 在 2s 后更新 `userInfo`
+    setTimeout(() => {
+      userInfo.name = 'Tom'
+    }, 2000)
+
+    // 这个变量在 2s 后不会同步更新
+    const newUserInfo: Member = { ...userInfo }
+
+    // 这个变量在 2s 后不会再同步更新
+    const { name } = userInfo
+
+    // 这样 `return` 出去给模板用,在 2s 后也不会同步更新
+    return {
+      ...userInfo,
+    }
+  },
+})

响应式 API 之 toRef 与 toRefs ~new

相信各位开发者看到这里时,应该已经对 refreactive API 都有所了解了,为了方便开发者使用, Vue 3 还推出了两个与之相关的 API : toReftoRefs ,都是用于 reactiveref 转换。

各自的作用

这两个 API 在拼写上非常接近,顾名思义,一个是只转换一个字段,一个是转换所有字段,转换后将得到新的变量,并且新变量和原来的变量可以保持同步更新。

API作用
toRef创建一个新的 Ref 变量,转换 Reactive 对象的某个字段为 Ref 变量
toRefs创建一个新的对象,它的每个字段都是 Reactive 对象各个字段的 Ref 变量

光看概念可能不容易理解,来看下面的例子,先声明一个 reactive 变量:

ts
interface Member {
+  id: number
+  name: string
+}
+
+const userInfo: Member = reactive({
+  id: 1,
+  name: 'Petter',
+})

然后分别看看这两个 API 应该怎么使用。

使用 toRef

先看这个转换单个字段的 toRef API ,了解了它的用法之后,再去看 toRefs 就很容易理解了。

API 类型和基本用法

toRef API 的 TS 类型如下:

ts
// `toRef` API 的 TS 类型
+function toRef<T extends object, K extends keyof T>(
+  object: T,
+  key: K,
+  defaultValue?: T[K]
+): ToRef<T[K]>
+
+// `toRef` API 的返回值的 TS 类型
+type ToRef<T> = T extends Ref ? T : Ref<T>

通过接收两个必传的参数(第一个是 reactive 对象, 第二个是要转换的 key ),返回一个 Ref 变量,在适当的时候也可以传递第三个参数,为该变量设置默认值。

以上文声明好的 userInfo 为例,如果想转换 name 这个字段为 Ref 变量,只需要这样操作:

ts
const name = toRef(userInfo, 'name')
+console.log(name.value) // Petter

等号左侧的 name 变量此时是一个 Ref 变量,这里因为 TypeScript 可以对其自动推导,因此声明时可以省略 TS 类型的显式指定,实际上该变量的类型是 Ref<string>

所以之后在读取和赋值时,就需要使用 name.value 来操作,在重新赋值时会同时更新 nameuserInfo.name 的值:

ts
// 修改前先查看初始值
+const name = toRef(userInfo, 'name')
+console.log(name.value) // Petter
+console.log(userInfo.name) // Petter
+
+// 修改 Ref 变量的值,两者同步更新
+name.value = 'Tom'
+console.log(name.value) // Tom
+console.log(userInfo.name) // Tom
+
+// 修改 Reactive 对象上该属性的值,两者也是同步更新
+userInfo.name = 'Jerry'
+console.log(name.value) // Jerry
+console.log(userInfo.name) // Jerry

这个 API 也可以接收一个 Reactive 数组,此时第二个参数应该传入数组的下标:

ts
// 这一次声明的是数组
+const words = reactive(['a', 'b', 'c'])
+
+// 通过下标 `0` 转换第一个 item
+const a = toRef(words, 0)
+console.log(a.value) // a
+console.log(words[0]) // a
+
+// 通过下标 `2` 转换第三个 item
+const c = toRef(words, 2)
+console.log(c.value) // c
+console.log(words[2]) // c

设置默认值

如果 Reactive 对象上有一个属性本身没有初始值,也可以传递第三个参数进行设置(默认值仅对 Ref 变量有效):

ts
interface Member {
+  id: number
+  name: string
+  // 类型上新增一个属性,因为是可选的,因此默认值会是 `undefined`
+  age?: number
+}
+
+// 声明变量时省略 `age` 属性
+const userInfo: Member = reactive({
+  id: 1,
+  name: 'Petter',
+})
+
+// 此时为了避免程序运行错误,可以指定一个初始值
+// 但初始值仅对 Ref 变量有效,不会影响 Reactive 字段的值
+const age = toRef(userInfo, 'age', 18)
+console.log(age.value)  // 18
+console.log(userInfo.age) // undefined
+
+// 除非重新赋值,才会使两者同时更新
+age.value = 25
+console.log(age.value)  // 25
+console.log(userInfo.age) // 25

数组也是同理,对于可能不存在的下标,可以传入默认值避免项目的逻辑代码出现问题:

ts
const words = reactive(['a', 'b', 'c'])
+
+// 当下标对应的值不存在时,也是返回 `undefined`
+const d = toRef(words, 3)
+console.log(d.value) // undefined
+console.log(words[3]) // undefined
+
+// 设置了默认值之后,就会对 Ref 变量使用默认值, Reactive 数组此时不影响
+const e = toRef(words, 4, 'e')
+console.log(e.value) // e
+console.log(words[4]) // undefined

其他用法

这个 API 还有一个特殊用法,但不建议在 TypeScript 里使用。

toRef 的过程中,如果使用了原对象上面不存在的 key ,那么定义出来的 Ref 变量的 .value 值将会是 undefined

ts
// 众所周知, Petter 是没有女朋友的
+const girlfriend = toRef(userInfo, 'girlfriend')
+console.log(girlfriend.value) // undefined
+console.log(userInfo.girlfriend) // undefined
+
+// 此时 Reactive 对象上只有两个 Key
+console.log(Object.keys(userInfo)) // ['id', 'name']

如果对这个不存在的 key 的 Ref 变量进行赋值,那么原来的 Reactive 对象也会同步增加这个 key,其值也会同步更新。

ts
// 赋值后,不仅 Ref 变量得到了 `Marry` , Reactive 对象也得到了 `Marry`
+girlfriend.value = 'Marry'
+console.log(girlfriend.value) // 'Marry'
+console.log(userInfo.girlfriend) // 'Marry'
+
+// 此时 Reactive 对象上有了三个 Key
+console.log(Object.keys(userInfo)) // ['id', 'name', 'girlfriend']

为什么强调不要在 TypeScript 里使用呢?因为在编译时,无法通过 TypeScript 的类型检查:

bash
 npm run build
+
+> hello-vue3@0.0.0 build
+> vue-tsc --noEmit && vite build
+
+src/views/home.vue:37:40 - error TS2345: Argument of type '"girlfriend"'
+is not assignable to parameter of type 'keyof Member'.
+
+37     const girlfriend = toRef(userInfo, 'girlfriend')
+                                          ~~~~~~~~~~~~
+
+src/views/home.vue:39:26 - error TS2339: Property 'girlfriend' does not exist
+on type 'Member'.
+
+39     console.log(userInfo.girlfriend) // undefined
+                            ~~~~~~~~~~
+
+src/views/home.vue:45:26 - error TS2339: Property 'girlfriend' does not exist
+on type 'Member'.
+
+45     console.log(userInfo.girlfriend) // 'Marry'
+                            ~~~~~~~~~~
+
+
+Found 3 errors in the same file, starting at: src/views/home.vue:37

如果不得不使用这种情况,可以考虑使用 any 类型:

ts
// 将该类型直接指定为 `any`
+type Member = any
+// 当然一般都是 `const userInfo: any`
+
+// 或者保持接口类型的情况下,允许任意键值
+interface Member {
+  [key: string]: any
+}
+
+// 使用 `Record` 也是同理
+type Member = Record<string, any>

但笔者还是更推荐保持良好的类型声明习惯,尽量避免这种用法。

使用 toRefs

在了解了 toRef API 之后,来看看 toRefs 的用法。

API 类型和基本用法

先看看它的 TS 类型:

ts
function toRefs<T extends object>(
+  object: T
+): {
+  [K in keyof T]: ToRef<T[K]>
+}
+
+type ToRef = T extends Ref ? T : Ref<T>

toRef 不同, toRefs 只接收了一个参数,是一个 reactive 变量。

ts
interface Member {
+  id: number
+  name: string
+}
+
+// 声明一个 Reactive 变量
+const userInfo: Member = reactive({
+  id: 1,
+  name: 'Petter',
+})
+
+// 传给 `toRefs` 作为入参
+const userInfoRefs = toRefs(userInfo)

此时这个新的 userInfoRefs 变量,它的 TS 类型就不再是 Member 了,而应该是:

ts
// 导入 `toRefs` API 的类型
+import type { ToRefs } from 'vue'
+
+// 上下文代码省略...
+
+// 将原来的类型传给 API 的类型
+const userInfoRefs: ToRefs<Member> = toRefs(userInfo)

也可以重新编写一个新的类型来指定它,因为每个字段都是与原来关联的 Ref 变量,所以也可以这样声明:

ts
// 导入 `ref` API 的类型
+import type { Ref } from 'vue'
+
+// 上下文代码省略...
+
+// 新声明的类型每个字段都是一个 Ref 变量的类型
+interface MemberRefs {
+  id: Ref<number>
+  name: Ref<string>
+}
+
+// 使用新的类型进行声明
+const userInfoRefs: MemberRefs = toRefs(userInfo)

当然实际上日常使用时并不需要手动指定其类型, TypeScript 会自动推导,可以节约非常多的开发工作量。

toRef API 一样,这个 API 也是可以对数组进行转换:

ts
const words = reactive(['a', 'b', 'c'])
+const wordsRefs = toRefs(words)

此时新数组的类型是 Ref<string>[] ,不再是原来的 string[] 类型。

解构与赋值

转换后的 Reactive 对象或数组支持 ES6 的解构,并且不会失去响应性,因为解构后的每一个变量都具备响应性。

ts
// 为了提高开发效率,可以直接将 Ref 变量直接解构出来使用
+const { name } = toRefs(userInfo)
+console.log(name.value) // Petter
+
+// 此时对解构出来的变量重新赋值,原来的变量也可以同步更新
+name.value = 'Tom'
+console.log(name.value) // Tom
+console.log(userInfo.name) // Tom

这一点和直接解构 Reactive 变量有非常大的不同,直接解构 Reactive 变量,得到的是一个普通的变量,不再具备响应性。

这个功能在使用 Hooks 函数非常好用(在 Vue 3 里也叫可组合函数, Composable Functions ),还是以一个计算器函数为例,这一次将其修改为内部有一个 Reactive 的数据状态中心,在函数返回时解构为多个 Ref 变量:

ts
import { reactive, toRefs } from 'vue'
+
+// 声明 `useCalculator` 数据状态类型
+interface CalculatorState {
+  // 这是要用来计算操作的数据
+  num: number
+  // 这是每次计算时要增加的幅度
+  step: number
+}
+
+// 声明一个 “使用计算器” 的函数
+function useCalculator() {
+  // 通过数据状态中心的形式,集中管理内部变量
+  const state: CalculatorState = reactive({
+    num: 0,
+    step: 10,
+  })
+
+  // 功能函数也是通过数据中心变量去调用
+  function add() {
+    state.num += state.step
+  }
+
+  return {
+    ...toRefs(state),
+    add,
+  }
+}

这样在调用 useCalculator 函数时,可以通过解构直接获取到 Ref 变量,不需要再进行额外的转换工作。

ts
// 解构出来的 `num` 和 `step` 都是 Ref 变量
+const { num, step, add } = useCalculator()
+console.log(num.value) // 0
+console.log(step.value) // 10
+
+// 调用计算器的方法,数据也是会得到响应式更新
+add()
+console.log(num.value) // 10

为什么要进行转换

关于为什么要出这么两个 API ,官方文档没有特别说明,不过经过笔者在业务中的一些实际使用感受,以及在写上一节 reactive特别注意,可能知道一些使用理由。

关于 refreactive 这两个 API 的好处就不重复了,但是在使用的过程中,各自都有不方便的地方:

ref API 虽然在 <template /> 里使用起来方便,但是在 <script /> 里进行读取 / 赋值的时候,要一直记得加上 .value ,否则 BUG 就来了。

reactive API 虽然在使用的时候,因为知道它本身是一个对象,所以不会忘记通过 foo.bar 这样的格式去操作,但是在 <template /> 渲染的时候,又因此不得不每次都使用 foo.bar 的格式去渲染。

那么有没有办法,既可以在编写 <script /> 的时候不容易出错,在写 <template /> 的时候又比较简单呢?

于是, toReftoRefs 因此诞生。

什么场景下比较适合使用它们

从便利性和可维护性来说,最好只在功能单一、代码量少的组件里使用,比如一个表单组件,通常表单的数据都放在一个对象里。

当然也可以把所有的数据都定义到一个 data 里,再去 data 里面取值,但是没有必要为了转换而转换,否则不如使用 Options API 风格。

在业务中的具体运用

继续使用上文一直在使用的 userInfo 来当案例,以一个用户信息表的小 demo 做个演示。

<script /> 部分:

  1. 先用 reactive 定义一个源数据,所有的数据更新,都是修改这个对象对应的值,按照对象的写法维护数据

  2. 再通过 toRefs 定义一个给 <template /> 使用的对象,这样可以得到一个每个字段都是 Ref 变量的新对象

  3. return 的时候,对步骤 2 里的 toRefs 对象进行解构,这样导出去就是各个字段对应的 Ref 变量,而不是一整个对象

ts
import { defineComponent, reactive, toRefs } from 'vue'
+
+interface Member {
+  id: number
+  name: string
+  age: number
+  gender: string
+}
+
+export default defineComponent({
+  setup() {
+    // 定义一个 reactive 对象
+    const userInfo = reactive({
+      id: 1,
+      name: 'Petter',
+      age: 18,
+      gender: 'male',
+    })
+
+    // 定义一个新的对象,它本身不具备响应性,但是它的字段全部是 Ref 变量
+    const userInfoRefs = toRefs(userInfo)
+
+    // 在 2s 后更新 `userInfo`
+    setTimeout(() => {
+      userInfo.id = 2
+      userInfo.name = 'Tom'
+      userInfo.age = 20
+    }, 2000)
+
+    // 在这里解构 `toRefs` 对象才能继续保持响应性
+    return {
+      ...userInfoRefs,
+    }
+  },
+})

<template /> 部分:

由于 return 出来的都是 Ref 变量,所以在模板里可以直接使用 userInfo 各个字段的 key ,不再需要写很长的 userInfo.name 了。

vue
<template>
+  <ul class="user-info">
+    <li class="item">
+      <span class="key">ID:</span>
+      <span class="value">{{ id }}</span>
+    </li>
+
+    <li class="item">
+      <span class="key">name:</span>
+      <span class="value">{{ name }}</span>
+    </li>
+
+    <li class="item">
+      <span class="key">age:</span>
+      <span class="value">{{ age }}</span>
+    </li>
+
+    <li class="item">
+      <span class="key">gender:</span>
+      <span class="value">{{ gender }}</span>
+    </li>
+  </ul>
+</template>

需要注意的问题

请注意是否有相同命名的变量存在,比如上面在 return<template /> 使用时,在解构 userInfoRefs 的时候已经包含了一个 name 字段,此时如果还有一个单独的变量也叫 name ,就会出现渲染上的数据显示问题。

此时它们在 <template /> 里哪个会生效,取决于谁排在后面,因为 return 出去的其实是一个对象,在对象里,如果存在相同的 key ,则后面的会覆盖前面的。

下面这种情况,会以单独的 name 为渲染数据:

ts
return {
+  ...userInfoRefs,
+  name,
+}

而下面这种情况,则是以 userInfoRefs 里的 name 为渲染数据:

ts
return {
+  name,
+  ...userInfoRefs,
+}

所以当决定使用 toReftoRefs API 的时候,请注意这个特殊情况!

函数的声明和使用 ~new

在了解了响应式数据如何使用之后,接下来就要开始了解函数了。

在 Vue 2 ,函数通常是作为当前组件实例上的方法在 methods 里声明,然后再在 mounted 等生命周期里调用,或者是在模板里通过 Click 等行为触发,由于组件内部经常需要使用 this 获取组件实例,因此不能使用箭头函数。

js
export default {
+  data: () => {
+    return {
+      num: 0,
+    }
+  },
+  mounted: function () {
+    this.add()
+  },
+  methods: {
+    // 不可以使用 `add: () => this.num++`
+    add: function () {
+      this.num++
+    },
+  },
+}

在 Vue 3 则灵活了很多,可以使用普通函数、 Class 类、箭头函数、匿名函数等等进行声明,可以将其写在 setup 里直接使用,也可以抽离在独立的 .js / .ts 文件里再导入使用。

需要在组件创建时自动执行的函数,其执行时机需要遵循 Vue 3 的生命周期,需要在模板里通过 @click@change 等行为触发,和变量一样,需要把函数名在 setup 里进行 return 出去。

下面是一个简单的例子,方便开发者更直观地了解:

vue
<template>
+  <p>{{ msg }}</p>
+
+  <!-- 在这里点击执行 `return` 出来的方法 -->
+  <button @click="updateMsg">修改MSG</button>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const msg = ref<string>('Hello World!')
+
+    // 这个要暴露给模板使用,必须 `return` 才可以使用
+    function updateMsg() {
+      msg.value = 'Hi World!'
+    }
+
+    // 这个要在页面载入时执行,无需 `return` 出去
+    const init = () => {
+      console.log('init')
+    }
+
+    onMounted(() => {
+      init()
+    })
+
+    return {
+      msg,
+      updateMsg,
+    }
+  },
+})
+</script>

数据的侦听 ~new

侦听数据变化也是组件里的一项重要工作,比如侦听路由变化、侦听参数变化等等。

Vue 3 在保留原来的 watch 功能之外,还新增了一个 watchEffect 帮助更简单的进行侦听。

watch

在 Vue 3 ,新版的 watch 和 Vue 2 的旧版写法对比,在使用方式上变化非常大!

回顾 Vue 2

在 Vue 2 是这样用的,和 datamethods 都在同级配置:

ts
export default {
+  data() {
+    return {
+      // ...
+    }
+  },
+  // 注意这里,放在 `data` 、 `methods` 同个级别
+  watch: {
+    // ...
+  },
+  methods: {
+    // ...
+  },
+}

并且类型繁多,选项式 API 的类型如下:

ts
watch: { [key: string]: string | Function | Object | Array}

联合类型过多,意味着用法复杂,下面是个很好的例子,虽然出自 官网 的用法介绍,但过于繁多的用法也反映出来对初学者不太友好,初次接触可能会觉得一头雾水:

ts
export default {
+  data() {
+    return {
+      a: 1,
+      b: 2,
+      c: {
+        d: 4,
+      },
+      e: 5,
+      f: 6,
+    }
+  },
+  watch: {
+    // 侦听顶级 Property
+    a(val, oldVal) {
+      console.log(`new: ${val}, old: ${oldVal}`)
+    },
+    // 字符串方法名
+    b: 'someMethod',
+    // 该回调会在任何被侦听的对象的 Property 改变时被调用,不论其被嵌套多深
+    c: {
+      handler(val, oldVal) {
+        console.log('c changed')
+      },
+      deep: true,
+    },
+    // 侦听单个嵌套 Property
+    'c.d': function (val, oldVal) {
+      // do something
+    },
+    // 该回调将会在侦听开始之后被立即调用
+    e: {
+      handler(val, oldVal) {
+        console.log('e changed')
+      },
+      immediate: true,
+    },
+    // 可以传入回调数组,它们会被逐一调用
+    f: [
+      'handle1',
+      function handle2(val, oldVal) {
+        console.log('handle2 triggered')
+      },
+      {
+        handler: function handle3(val, oldVal) {
+          console.log('handle3 triggered')
+        },
+        /* ... */
+      },
+    ],
+  },
+  methods: {
+    someMethod() {
+      console.log('b changed')
+    },
+    handle1() {
+      console.log('handle 1 triggered')
+    },
+  },
+}

当然肯定也会有开发者会觉得这样选择多是个好事,选择适合自己的就好,但笔者还是认为这种写法对于初学者来说不是那么友好,有些过于复杂化,如果一个用法可以适应各种各样的场景,岂不是更妙?

TIP

另外需要注意的是,不能使用箭头函数来定义 Watcher 函数 (例如 searchQuery: newValue => this.updateAutocomplete(newValue) )。

因为箭头函数绑定了父级作用域的上下文,所以 this 将不会按照期望指向组件实例, this.updateAutocomplete 将是 undefined

Vue 2 也可以通过 this.$watch() 这个 API 的用法来实现对某个数据的侦听,它接受三个参数: sourcecallbackoptions

ts
export default {
+  data() {
+    return {
+      a: 1,
+    }
+  },
+  // 生命周期钩子
+  mounted() {
+    this.$watch('a', (newVal, oldVal) => {
+      // ...
+    })
+  },
+}

由于 this.$watch 的用法和 Vue 3 比较接近,所以这里不做过多的回顾,请直接看 了解 Vue 3 部分。

了解 Vue 3

在 Vue 3 的组合式 API 写法, watch 是一个可以接受 3 个参数的函数(保留了 Vue 2 的 this.$watch 这种用法),在使用层面上简单了很多。

ts
import { watch } from 'vue'
+
+// 一个用法走天下
+watch(
+  source, // 必传,要侦听的数据源
+  callback // 必传,侦听到变化后要执行的回调函数
+  // options // 可选,一些侦听选项
+)

下面的内容都基于 Vue 3 的组合式 API 用法展开讲解。

API 的 TS 类型

在了解用法之前,先对它的 TS 类型声明做一个简单的了解, watch 作为组合式 API ,根据使用方式有两种类型声明:

  1. 基础用法的 TS 类型,详见 基础用法 部分
ts
// watch 部分的 TS 类型
+// ...
+export declare function watch<T, Immediate extends Readonly<boolean> = false>(
+  source: WatchSource<T>,
+  cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
+  options?: WatchOptions<Immediate>
+): WatchStopHandle
+// ...
  1. 批量侦听的 TS 类型,详见 批量侦听 部分
ts
// watch 部分的 TS 类型
+// ...
+export declare function watch<
+  T extends MultiWatchSources,
+  Immediate extends Readonly<boolean> = false
+>(
+  sources: [...T],
+  cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
+  options?: WatchOptions<Immediate>
+): WatchStopHandle
+
+// MultiWatchSources 是一个数组
+declare type MultiWatchSources = (WatchSource<unknown> | object)[]
+// ...

但是不管是基础用法还是批量侦听,可以看到这个 API 都是接受三个入参:

参数是否可选含义
source必传数据源(详见:要侦听的数据源
callback必传侦听到变化后要执行的回调函数(详见:侦听后的回调函数
options可选一些侦听选项(详见:侦听的选项

并返回一个可以用来停止侦听的函数(详见:停止侦听)。

要侦听的数据源

在上面 API 的 TS 类型 已经对 watch API 的组成有一定的了解了,这里先对数据源的类型和使用限制做下说明。

TIP

如果不提前了解,在使用的过程中可能会遇到 “侦听了但没有反应” 的情况出现。

另外,这部分内容会先围绕基础用法展开说明,批量侦听会在 批量侦听 部分单独说明。

watch API 的第 1 个参数 source 是要侦听的数据源,它的 TS 类型如下:

ts
// watch 第 1 个入参的 TS 类型
+// ...
+export declare type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
+// ...

可以看到能够用于侦听的数据,是通过 响应式 API 定义的变量( Ref<T> ),或者是一个 计算数据ComputedRef<T> ),或者是一个 getter 函数() => T )。

所以要想定义的 watch 能够做出预期的行为,数据源必须具备响应性或者是一个 getter ,如果只是通过 let 定义一个普通变量,然后去改变这个变量的值,这样是无法侦听的。

TIP

如果要侦听响应式对象里面的某个值(这种情况下对象本身是响应式,但它的 property 不是),需要写成 getter 函数,简单的说就是需要写成有返回值的函数,这个函数 return 要侦听的数据, e.g. () => foo.bar ,可以结合下方 基础用法 的例子一起理解。

侦听后的回调函数

在上面 API 的 TS 类型 介绍了 watch API 的组成,和数据源一样,先了解一下回调函数的定义。

TIP

和数据源部分一样,回调函数的内容也是会先围绕基础用法展开说明,批量侦听会在 批量侦听 部分单独说明。

watch API 的第 2 个参数 callback 是侦听到数据变化时要做出的行为,它的 TS 类型如下:

ts
// watch 第 2 个入参的 TS 类型
+// ...
+export declare type WatchCallback<V = any, OV = any> = (
+  value: V,
+  oldValue: OV,
+  onCleanup: OnCleanup
+) => any
+// ...

乍一看它有三个参数,但实际上这些参数不是自己定义的,而是 watch API 传给的,所以不管用或者不用,它们都在那里:

参数作用
value变化后的新值,类型和数据源保持一致
oldValue变化前的旧值,类型和数据源保持一致
onCleanup注册一个清理函数,详见 侦听效果清理 部分

注意:第一个参数是新值,第二个才是原来的旧值!

如同其他 JS 函数,在使用 watch 的回调函数时,可以对这三个参数任意命名,比如把 value 命名为觉得更容易理解的 newValue

TIP

如果侦听的数据源是一个 引用类型 时( e.g. ObjectArrayDate … ), valueoldValue 是完全相同的,因为指向同一个对象。

另外,默认情况下,watch 是惰性的,也就是只有当被侦听的数据源发生变化时才执行回调。

基础用法

来到这里,对 2 个必传的参数都有一定的了解了,先看看基础的用法,也就是日常最常编写的方案,只需要先关注前 2 个必传的参数。

ts
// 不要忘了导入要用的 API
+import { defineComponent, reactive, watch } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 定义一个响应式数据
+    const userInfo = reactive({
+      name: 'Petter',
+      age: 18,
+    })
+
+    // 2s后改变数据
+    setTimeout(() => {
+      userInfo.name = 'Tom'
+    }, 2000)
+
+    /**
+     * 可以直接侦听这个响应式对象
+     * callback 的参数如果不用可以不写
+     */
+    watch(userInfo, () => {
+      console.log('侦听整个 userInfo ', userInfo.name)
+    })
+
+    /**
+     * 也可以侦听对象里面的某个值
+     * 此时数据源需要写成 getter 函数
+     */
+    watch(
+      // 数据源,getter 形式
+      () => userInfo.name,
+      // 回调函数 callback
+      (newValue, oldValue) => {
+        console.log('只侦听 name 的变化 ', userInfo.name)
+        console.log('打印变化前后的值', { oldValue, newValue })
+      }
+    )
+  },
+})

一般的业务场景,基础用法足以面对。

如果有多个数据源要侦听,并且侦听到变化后要执行的行为一样,那么可以使用 批量侦听

特殊的情况下,可以搭配 侦听的选项 做一些特殊的用法,详见下面部分的内容。

批量侦听

如果有多个数据源要侦听,并且侦听到变化后要执行的行为一样,第一反应可能是这样来写:

  1. 抽离相同的处理行为为公共函数
  2. 然后定义多个侦听操作,传入这个公共函数
ts
import { defineComponent, ref, watch } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const message = ref<string>('')
+    const index = ref<number>(0)
+
+    // 2s后改变数据
+    setTimeout(() => {
+      // 来到这里才会触发 watch 的回调
+      message.value = 'Hello World!'
+      index.value++
+    }, 2000)
+
+    // 抽离相同的处理行为为公共函数
+    const handleWatch = (
+      newValue: string | number,
+      oldValue: string | number
+    ): void => {
+      console.log({ newValue, oldValue })
+    }
+
+    // 然后定义多个侦听操作,传入这个公共函数
+    watch(message, handleWatch)
+    watch(index, handleWatch)
+  },
+})

这样写其实没什么问题,不过除了抽离公共代码的写法之外, watch API 还提供了一个批量侦听的用法,和 基础用法 的区别在于,数据源和回调参数都变成了数组的形式。

数据源:以数组的形式传入,里面每一项都是一个响应式数据。

回调参数:原来的 valuenewValue 也都变成了数组,每个数组里面的顺序和数据源数组排序一致。

可以看下面的这个例子更为直观:

ts
import { defineComponent, ref, watch } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 定义多个数据源
+    const message = ref<string>('')
+    const index = ref<number>(0)
+
+    // 2s后改变数据
+    setTimeout(() => {
+      message.value = 'Hello World!'
+      index.value++
+    }, 2000)
+
+    watch(
+      // 数据源改成了数组
+      [message, index],
+      // 回调的入参也变成了数组,每个数组里面的顺序和数据源数组排序一致
+      ([newMessage, newIndex], [oldMessage, oldIndex]) => {
+        console.log('message 的变化', { newMessage, oldMessage })
+        console.log('index 的变化', { newIndex, oldIndex })
+      }
+    )
+  },
+})

什么情况下可能会用到批量侦听呢?比如一个子组件有多个 props ,当有任意一个 prop 发生变化时,都需要执行初始化函数重置组件的状态,那么这个时候就可以用上这个功能啦!

TIP

在适当的业务场景,也可以使用 watchEffect 来完成批量侦听,但请留意 功能区别 部分的说明。

侦听的选项

API 的 TS 类型 里提到, watch API 还接受第 3 个参数 options ,可选的一些侦听选项。

它的 TS 类型如下:

ts
// watch 第 3 个入参的 TS 类型
+// ...
+export declare interface WatchOptions<Immediate = boolean>
+  extends WatchOptionsBase {
+  immediate?: Immediate
+  deep?: boolean
+}
+// ...
+
+// 继承的 base 类型
+export declare interface WatchOptionsBase extends DebuggerOptions {
+  flush?: 'pre' | 'post' | 'sync'
+}
+// ...
+
+// 继承的 debugger 选项类型
+export declare interface DebuggerOptions {
+  onTrack?: (event: DebuggerEvent) => void
+  onTrigger?: (event: DebuggerEvent) => void
+}
+// ...

options 是一个对象的形式传入,有以下几个选项:

选项类型默认值可选值作用
deepbooleanfalsetrue | false是否进行深度侦听
immediatebooleanfalsetrue | false是否立即执行侦听回调
flushstring'pre''pre' | 'post' | 'sync'控制侦听回调的调用时机
onTrack(e) => void在数据源被追踪时调用
onTrigger(e) => void在侦听回调被触发时调用

其中 onTrackonTriggere 是 debugger 事件,建议在回调内放置一个 debugger 语句 以调试依赖,这两个选项仅在开发模式下生效。

TIP

deep 默认是 false ,但是在侦听 reactive 对象或数组时,会默认为 true ,详见 侦听选项之 deep

侦听选项之 deep

deep 选项接受一个布尔值,可以设置为 true 开启深度侦听,或者是 false 关闭深度侦听,默认情况下这个选项是 false 关闭深度侦听的,但也存在特例。

设置为 false 的情况下,如果直接侦听一个响应式的 引用类型 数据(e.g. ObjectArray … ),虽然它的属性的值有变化,但对其本身来说是不变的,所以不会触发 watch 的 callback 。

下面是一个关闭了深度侦听的例子:

ts
import { defineComponent, ref, watch } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 定义一个响应式数据,注意用的是 ref 来定义
+    const nums = ref<number[]>([])
+
+    // 2s后给这个数组添加项目
+    setTimeout(() => {
+      nums.value.push(1)
+
+      // 可以打印一下,确保数据确实变化了
+      console.log('修改后', nums.value)
+    }, 2000)
+
+    // 但是这个 watch 不会按预期执行
+    watch(
+      nums,
+      // 这里的 callback 不会被触发
+      () => {
+        console.log('触发侦听', nums.value)
+      },
+      // 因为关闭了 deep
+      {
+        deep: false,
+      }
+    )
+  },
+})

类似这种情况,需要把 deep 设置为 true 才可以触发侦听。

可以看到上面的例子特地用了 ref API ,这是因为通过 reactive API 定义的对象无法将 deep 成功设置为 false (这一点在目前的官网文档未找到说明,最终是在 watch API 的源码 上找到了答案)。

ts
// ...
+if (isReactive(source)) {
+  getter = () => source
+  deep = true // 被强制开启了
+}
+// ...

这个情况就是上面所说的 “特例” ,可以通过 isReactive API 来判断是否需要手动开启深度侦听。

ts
// 导入 isReactive API
+import { defineComponent, isReactive, reactive, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 侦听这个数据时,会默认开启深度侦听
+    const foo = reactive({
+      name: 'Petter',
+      age: 18,
+    })
+    console.log(isReactive(foo)) // true
+
+    // 侦听这个数据时,不会默认开启深度侦听
+    const bar = ref({
+      name: 'Petter',
+      age: 18,
+    })
+    console.log(isReactive(bar)) // false
+  },
+})

侦听选项之 immediate

侦听后的回调函数 部分有了解过, watch 默认是惰性的,也就是只有当被侦听的数据源发生变化时才执行回调。

这句话是什么意思呢?来看一下这段代码,为了减少 deep 选项的干扰,换一个类型,换成 string 数据来演示,请留意注释:

ts
import { defineComponent, ref, watch } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 这个时候不会触发 watch 的回调
+    const message = ref<string>('')
+
+    // 2s后改变数据
+    setTimeout(() => {
+      // 来到这里才会触发 watch 的回调
+      message.value = 'Hello World!'
+    }, 2000)
+
+    watch(message, () => {
+      console.log('触发侦听', message.value)
+    })
+  },
+})

可以看到,数据在初始化的时候并不会触发侦听回调,如果有需要的话,通过 immediate 选项来让它直接触发。

immediate 选项接受一个布尔值,默认是 false ,可以设置为 true 让回调立即执行。

改成这样,请留意高亮的代码部分和新的注释:

ts
import { defineComponent, ref, watch } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 这一次在这里可以会触发 watch 的回调了
+    const message = ref<string>('')
+
+    // 2s后改变数据
+    setTimeout(() => {
+      // 这一次,这里是第二次触发 watch 的回调,不再是第一次
+      message.value = 'Hello World!'
+    }, 2000)
+
+    watch(
+      message,
+      () => {
+        console.log('触发侦听', message.value)
+      },
+      // 设置 immediate 选项
+      {
+        immediate: true,
+      }
+    )
+  },
+})

注意,在带有 immediate 选项时,不能在第一次回调时取消该数据源的侦听,详见 停止侦听 部分。

侦听选项之 flush

flush 选项是用来控制 侦听回调 的调用时机,接受指定的字符串,可选值如下,默认是 'pre'

可选值回调的调用时机使用场景
'pre'将在渲染前被调用允许回调在模板运行前更新了其他值
'sync'在渲染时被同步调用目前来说没什么好处,可以了解但不建议用…
'post'被推迟到渲染之后调用如果要通过 ref 操作 DOM 元素与子组件 ,需要使用这个值来启用该选项,以达到预期的执行效果

对于 'pre''post' ,回调使用队列进行缓冲。回调只被添加到队列中一次。

即使观察值变化了多次,值的中间变化将被跳过,不会传递给回调,这样做不仅可以提高性能,还有助于保证数据的一致性。

更多关于 flush 的信息,请参阅 回调的触发时机

停止侦听

如果在 setup 或者 script-setup 里使用 watch 的话, 组件被卸载 的时候也会一起被停止,一般情况下不太需要关心如何停止侦听。

不过有时候可能想要手动取消, Vue 3 也提供了方法。

TIP

随着组件被卸载一起停止的前提是,侦听器必须是 同步语句 创建的,这种情况下侦听器会绑定在当前组件上。

如果放在 setTimeout异步函数 里面创建,则不会绑定到当前组件,因此组件卸载的时候不会一起停止该侦听器,这种时候就需要手动停止侦听。

API 的 TS 类型 有提到,当在定义一个 watch 行为的时候,它会返回一个用来停止侦听的函数。

这个函数的 TS 类型如下:

ts
export declare type WatchStopHandle = () => void

用法很简单,做一下简单了解即可:

ts
// 定义一个取消观察的变量,它是一个函数
+const unwatch = watch(message, () => {
+  // ...
+})
+
+// 在合适的时期调用它,可以取消这个侦听
+unwatch()

但是也有一点需要注意的是,如果启用了 immediate 选项 ,不能在第一次触发侦听回调时执行它。

ts
// 注意:这是一段错误的代码,运行会报错
+const unwatch = watch(
+  message,
+  // 侦听的回调
+  () => {
+    // ...
+    // 在这里调用会有问题 ❌
+    unwatch()
+  },
+  // 启用 immediate 选项
+  {
+    immediate: true,
+  }
+)

会收获一段报错,告诉 unwatch 这个变量在初始化前无法被访问:

bash
Uncaught ReferenceError: Cannot access 'unwatch' before initialization

目前有两种方案可以让实现这个操作:

方案一:使用 var 并判断变量类型,利用 var 的变量提升 来实现目的。

ts
// 这里改成 var ,不要用 const 或 let
+var unwatch = watch(
+  message,
+  // 侦听回调
+  () => {
+    // 这里加一个判断,是函数才执行它
+    if (typeof unwatch === 'function') {
+      unwatch()
+    }
+  },
+  // 侦听选项
+  {
+    immediate: true,
+  }
+)

不过 var 已经属于过时的语句了,建议用方案二的 let

方案二:使用 let 并判断变量类型。

ts
// 如果不想用 any ,可以导入 TS 类型
+import type { WatchStopHandle } from 'vue'
+
+// 这里改成 let ,但是要另起一行,先定义,再赋值
+let unwatch: WatchStopHandle
+unwatch = watch(
+  message,
+  // 侦听回调
+  () => {
+    // 这里加一个判断,是函数才执行它
+    if (typeof unwatch === 'function') {
+      unwatch()
+    }
+  },
+  // 侦听选项
+  {
+    immediate: true,
+  }
+)

侦听效果清理

侦听后的回调函数 部分提及到一个参数 onCleanup ,它可以帮注册一个清理函数。

有时 watch 的回调会执行异步操作,当 watch 到数据变更的时候,需要取消这些操作,这个函数的作用就用于此,会在以下情况调用这个清理函数:

  • watcher 即将重新运行的时候
  • watcher 被停止(组件被卸载或者被手动 停止侦听

TS 类型:

ts
declare type OnCleanup = (cleanupFn: () => void) => void

用法方面比较简单,传入一个回调函数运行即可,不过需要注意的是,需要在停止侦听之前注册好清理行为,否则不会生效。

停止侦听 里的最后一个 immediate 例子的基础上继续添加代码,请注意注册的时机:

ts
let unwatch: WatchStopHandle
+unwatch = watch(
+  message,
+  (newValue, oldValue, onCleanup) => {
+    // 需要在停止侦听之前注册好清理行为
+    onCleanup(() => {
+      console.log('侦听清理ing')
+      // 根据实际的业务情况定义一些清理操作 ...
+    })
+    // 然后再停止侦听
+    if (typeof unwatch === 'function') {
+      unwatch()
+    }
+  },
+  {
+    immediate: true,
+  }
+)

watchEffect

如果一个函数里包含了多个需要侦听的数据,一个一个数据去侦听太麻烦了,在 Vue 3 ,可以直接使用 watchEffect API 来简化的操作。

API 的 TS 类型

这个 API 的类型如下,使用的时候需要传入一个副作用函数(相当于 watch 的 侦听后的回调函数 ),也可以根据的实际情况传入一些可选的 侦听选项

和 watch API 一样,它也会返回一个用于 停止侦听 的函数。

ts
// watchEffect 部分的 TS 类型
+// ...
+export declare type WatchEffect = (onCleanup: OnCleanup) => void
+
+export declare function watchEffect(
+  effect: WatchEffect,
+  options?: WatchOptionsBase
+): WatchStopHandle
+// ...

副作用函数也会传入一个清理回调作为参数,和 watch 的 侦听效果清理 一样的用法。

可以理解为它是一个简化版的 watch ,具体简化在哪里呢?请看下面的用法示例。

用法示例

它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

ts
import { defineComponent, ref, watchEffect } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 单独定义两个数据,后面用来分开改变数值
+    const name = ref<string>('Petter')
+    const age = ref<number>(18)
+
+    // 定义一个调用这两个数据的函数
+    const getUserInfo = (): void => {
+      console.log({
+        name: name.value,
+        age: age.value,
+      })
+    }
+
+    // 2s后改变第一个数据
+    setTimeout(() => {
+      name.value = 'Tom'
+    }, 2000)
+
+    // 4s后改变第二个数据
+    setTimeout(() => {
+      age.value = 20
+    }, 4000)
+
+    // 直接侦听调用函数,在每个数据产生变化的时候,它都会自动执行
+    watchEffect(getUserInfo)
+  },
+})

和 watch 的区别

虽然理论上 watchEffectwatch 的一个简化操作,可以用来代替 批量侦听 ,但它们也有一定的区别:

  1. watch 可以访问侦听状态变化前后的值,而 watchEffect 没有。

  2. watch 是在属性改变的时候才执行,而 watchEffect 则默认会执行一次,然后在属性改变的时候也会执行。

第二点的意思,看下面这段代码可以有更直观的理解:

使用 watch :

ts
export default defineComponent({
+  setup() {
+    const foo = ref<string>('')
+
+    setTimeout(() => {
+      foo.value = 'Hello World!'
+    }, 2000)
+
+    function bar() {
+      console.log(foo.value)
+    }
+
+    // 使用 watch 需要先手动执行一次
+    bar()
+
+    // 然后当 foo 有变动时,才会通过 watch 来执行 bar()
+    watch(foo, bar)
+  },
+})

使用 watchEffect :

ts
export default defineComponent({
+  setup() {
+    const foo = ref<string>('')
+
+    setTimeout(() => {
+      foo.value = 'Hello World!'
+    }, 2000)
+
+    function bar() {
+      console.log(foo.value)
+    }
+
+    // 可以通过 watchEffect 实现 bar() + watch(foo, bar) 的效果
+    watchEffect(bar)
+  },
+})

可用的侦听选项

虽然用法和 watch 类似,但也简化了一些选项,它的侦听选项 TS 类型如下:

ts
// 只支持 base 类型
+export declare interface WatchOptionsBase extends DebuggerOptions {
+  flush?: 'pre' | 'post' | 'sync'
+}
+// ...
+
+// 继承的 debugger 选项类型
+export declare interface DebuggerOptions {
+  onTrack?: (event: DebuggerEvent) => void
+  onTrigger?: (event: DebuggerEvent) => void
+}
+// ...

对比 watch API ,它不支持 deepimmediate ,请记住这一点,其他的用法是一样的。

flush 选项的使用详见 侦听选项之 flushonTrackonTrigger 详见 侦听的选项 部分内容。

watchPostEffect

watchEffect API 使用 flush: 'post' 选项时的别名,具体区别详见 侦听选项之 flush 部分。

TIP

Vue v3.2.0 及以上版本才支持该 API 。

watchSyncEffect

watchEffect API 使用 flush: 'sync' 选项时的别名,具体区别详见 侦听选项之 flush 部分。

TIP

Vue v3.2.0 及以上版本才支持该 API 。

数据的计算 ~new

和 Vue 2.0 一样,数据的计算也是使用 computed API ,它可以通过现有的响应式数据,去通过计算得到新的响应式变量,用过 Vue 2.0 的开发者应该不会太陌生,但是在 Vue 3.0 ,在使用方式上也是变化非常大!

TIP

这里的响应式数据,可以简单理解为通过 ref API 、 reactive API 定义出来的数据,当然 Vuex 、Vue Router 等 Vue 数据也都具备响应式,可以在 响应式数据的变化 了解。

用法变化

先从一个简单的用例来看看在 Vue 新旧版本的用法区别:

假设定义了两个分开的数据 firstName 名字和 lastName 姓氏,但是在 template 展示时,需要展示完整的姓名,那么就可以通过 computed 来计算一个新的数据:

回顾 Vue 2

在 Vue 2.0 ,computeddata 在同级配置,并且不可以和 data 里的数据同名重复定义:

ts
// 在 Vue 2 的写法:
+export default {
+  data() {
+    return {
+      firstName: 'Bill',
+      lastName: 'Gates',
+    }
+  },
+  // 注意这里定义的变量,都要通过函数的形式来返回它的值
+  computed: {
+    // 普通函数可以直接通过熟悉的 this 来拿到 data 里的数据
+    fullName() {
+      return `${this.firstName} ${this.lastName}`
+    },
+    // 箭头函数则需要通过参数来拿到实例上的数据
+    fullName2: (vm) => `${vm.firstName} ${vm.lastName}`,
+  },
+}

这样在需要用到全名的地方,只需要通过 this.fullName 就可以得到 Bill Gates

了解 Vue 3

在 Vue 3.0 ,跟其他 API 的用法一样,需要先导入 computed 才能使用:

ts
// 在 Vue 3 的写法:
+import { defineComponent, ref, computed } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 定义基本的数据
+    const firstName = ref<string>('Bill')
+    const lastName = ref<string>('Gates')
+
+    // 定义需要计算拼接结果的数据
+    const fullName = computed(() => `${firstName.value} ${lastName.value}`)
+
+    // 2s 后改变某个数据的值
+    setTimeout(() => {
+      firstName.value = 'Petter'
+    }, 2000)
+
+    // template 那边在 2s 后也会显示为 Petter Gates
+    return {
+      fullName,
+    }
+  },
+})

可以把这个用法简单的理解为,传入一个回调函数,并 return 一个值,对,它需要有明确的返回值。

TIP

需要注意的是:

  1. 定义出来的 computed 变量,和 Ref 变量的用法一样,也是需要通过 .value 才能拿到它的值

  2. 但是区别在于,默认情况下 computedvalue 是只读的

原因详见下方的 类型声明

类型声明

之前说过,在 defineComponent 里,会自动帮推导 Vue API 的类型,所以一般情况下,是不需要显式的去定义 computed 出来的变量类型的。

在确实需要手动指定的情况下,也可以导入它的类型然后定义:

ts
import { computed } from 'vue'
+import type { ComputedRef } from 'vue'
+
+// 注意这里添加了类型声明
+const fullName: ComputedRef<string> = computed(
+  () => `${firstName.value} ${lastName.value}`
+)

要返回一个字符串,就写 ComputedRef<string> ;返回布尔值,就写 ComputedRef<boolean> ;返回一些复杂对象信息,可以先定义好的类型,再诸如 ComputedRef<UserInfo> 去写。

ts
// 这是 ComputedRef 的类型声明:
+export declare interface ComputedRef<T = any> extends WritableComputedRef<T> {
+  readonly value: T
+  [ComoutedRefSymbol]: true
+}

优势对比和注意事项

在继续往下看之前,先来了解一下这个 API 的一些优势和注意事项(如果在 Vue 2 已经有接触过的话,可以跳过这一段,因为优势和需要注意的东西比较一致)。

优势对比

看到这里,相信刚接触的开发者可能会有疑问,既然 computed 也是通过一个函数来返回值,那么和普通的 function 有什么区别,或者说优势?

  1. 性能优势

这一点在 官网文档 其实是有提到的:

数据的计算是基于它们的响应依赖关系缓存的,只在相关响应式依赖发生改变时它们才会重新求值。

也就是说,只要原始数据没有发生改变,多次访问 computed ,都是会立即返回之前的计算结果,而不是再次执行函数;而普通的 function 调用多少次就执行多少次,每调用一次就计算一次。

至于为何要如此设计,官网文档也给出了原因:

为什么需要缓存?假设有一个性能开销比较大的计算数据 list,它需要遍历一个巨大的数组并做大量的计算。然后可能有其他的计算数据依赖于 list。如果没有缓存,将不可避免的多次执行 list 的 getter!如果不希望有缓存,请用 function 来替代。

TIP

在这部分内容里,将官方文档的一些用词做了更换,比如把 method 都替换成了 function ,也把 “计算属性” 都换成了 “计算数据”,原因在于官网很多地方是基于 Options API 的写法去描述,而本文档是基于 Composition API 。

点击了解: 如何理解 JavaScript 中方法(method)和函数(function)的区别?

  1. 书写统一

假定 foo1 是 Ref 变量, foo2 是 computed 变量, foo3 是普通函数返回值

看到这里的开发者应该都已经清楚 Vue 3 的 Ref 变量是通过 foo1.value 来拿到值的,而 computed 也是通过 foo2.value ,并且在 template 里都可以省略 .value ,在读取方面,他们是有一致的风格和简洁性。

而 foo3 不管是在 script 还是 template ,都需要通过 foo3() 才能拿到结果,相对来说会有那么一丢丢别扭。

当然,关于这一点,如果涉及到的数据不是响应式数据,那么还是老老实实的用函数返回值吧,原因请见下面的 注意事项

注意事项

有优势当然也就有一定的 “劣势” ,当然这也是 Vue 框架的有意为之,所以在使用上也需要注意一些问题:

  1. 只会更新响应式数据的计算

假设要获取当前的时间信息,因为不是响应式数据,所以这种情况下就需要用普通的函数去获取返回值,才能拿到最新的时间。

ts
const nowTime = computed(() => new Date())
+console.log(nowTime.value)
+// 输出 Sun Nov 14 2021 21:07:00 GMT+0800 (GMT+08:00)
+
+// 2s 后依然是跟上面一样的结果
+setTimeout(() => {
+  console.log(nowTime.value)
+  // 还是输出 Sun Nov 14 2021 21:07:00 GMT+0800 (GMT+08:00)
+}, 2000)
  1. 数据是只读的

通过 computed 定义的数据,它是只读的,这一点在 类型声明 已经有所了解。

如果直接赋值,不仅无法变更数据,而且会收获一个报错。

bash
TS2540: Cannot assign to 'value' because it is a read-only property.

虽然无法直接赋值,但是在必要的情况下,依然可以通过 computedsetter 来更新数据。

点击了解:computed 的 setter 用法

setter 的使用

通过 computed 定义的变量默认都是只读的形式(只有一个 getter ),但是在必要的情况下,也可以使用其 setter 属性来更新数据。

基本格式

当需要用到 setter 的时候, computed 就不再是一个传入 callback 的形式了,而是传入一个带有 2 个方法的对象。

ts
// 注意这里computed接收的入参已经不再是函数
+const foo = computed({
+  // 这里需要明确的返回一个值
+  get() {
+    // ...
+  },
+  // 这里接收一个参数,代表修改 foo 时,赋值下来的新值
+  set(newValue) {
+    // ...
+  },
+})

这里的 get 就是 computed 的 getter ,跟原来传入 callback 的形式一样,是用于 foo.value 的读取,所以这里必须有明确的返回值。

这里的 set 就是 computed 的 setter ,它会接收一个参数,代表新的值,当通过 foo.value = xxx 赋值的时候,赋入的这个值,就会通过这个入参来传递进来,可以根据的业务需要,把这个值,赋给相关的数据源。

TIP

请注意,必须使用 getset 这 2 个方法名,也只接受这 2 个方法。

在了解了基本格式后,可以查看下面的例子来了解具体的用法。

使用示范

官网的 例子 是一个 Options API 的案例,这里改成 Composition API 的写法来演示:

ts
// 还是这2个数据源
+const firstName = ref<string>('Bill')
+const lastName = ref<string>('Gates')
+
+// 这里配合setter的需要,改成了另外一种写法
+const fullName = computed({
+  // getter还是返回一个拼接起来的全名
+  get() {
+    return `${firstName.value} ${lastName.value}`
+  },
+  // setter这里改成只更新firstName,注意参数也定义TS类型
+  set(newFirstName: string) {
+    firstName.value = newFirstName
+  },
+})
+console.log(fullName.value) // 输出 Bill Gates
+
+// 2s后更新一下数据
+setTimeout(() => {
+  // 对fullName的赋值,其实更新的是firstName
+  fullName.value = 'Petter'
+
+  // 此时firstName已经得到了更新
+  console.log(firstName.value) // 会输出 Petter
+
+  // 当然,由于firstName变化了,所以fullName的getter也会得到更新
+  console.log(fullName.value) // 会输出 Petter Gates
+}, 2000)

应用场景

计算 API 的作用,官网文档只举了一个非常简单的例子,那么在实际项目中,什么情况下用它会让更方便呢?

简单举几个比较常见的例子吧,加深一下对 computed 的理解。

数据的拼接和计算

如上面的案例,与其每个用到的地方都要用到 firstName + ' ' + lastName 这样的多变量拼接,不如用一个 fullName 来的简单。

当然,不止是字符串拼接,数据的求和等操作更是合适,比如说做一个购物车,购物车里有商品列表,同时还要显示购物车内的商品总金额,这种情况就非常适合用计算数据。

复用组件的动态数据

在一个项目里,很多时候组件会涉及到复用,比如说:“首页的文章列表 vs 列表页的文章列表 vs 作者详情页的文章列表” ,特别常见于新闻网站等内容资讯站点,这种情况下,往往并不需要每次都重新写 UI 、数据渲染等代码,仅仅是接口 URL 的区别。

这种情况就可以通过路由名称来动态获取要调用哪个列表接口:

ts
const route = useRoute()
+
+// 定义一个根据路由名称来获取接口URL的计算数据
+const apiUrl = computed(() => {
+  switch (route.name) {
+    // 首页
+    case 'home':
+      return '/api/list1'
+    // 列表页
+    case 'list':
+      return '/api/list2'
+    // 作者页
+    case 'author':
+      return '/api/list3'
+    // 默认是随机列表
+    default:
+      return '/api/random'
+  }
+})
+
+// 请求列表
+const getArticleList = async (): Promise<void> => {
+  // ...
+  articleList.value = await axios({
+    method: 'get',
+    url: apiUrl.value,
+    // ...
+  })
+  // ...
+}

当然,这种情况也可以在父组件通过 props 传递接口 URL ,如果已经学到了 组件通讯 一章的话。

获取多级对象的值

应该很经常的遇到要在 template 显示一些多级对象的字段,但是有时候又可能存在某些字段不一定有,需要做一些判断的情况,虽然有 v-if ,但是嵌套层级一多,模板代码会难以维护。

如果把这些工作量转移给计算数据,结合 try / catch ,这样就无需在 template 里处理很多判断了。

ts
// 例子比较极端,但在 Vuex 这种大型数据树上,也不是完全不可能存在
+const foo = computed(() => {
+  // 正常情况下返回需要的数据
+  try {
+    return store.state.foo3.foo2.foo1.foo
+  } catch (e) {
+    // 处理失败则返回一个默认值
+    return ''
+  }
+})

这样在 template 里要拿到 foo 的值,完全不需要关心中间一级又一级的字段是否存在,只需要区分是不是默认值。

不同类型的数据转换

有时候会遇到一些需求类似于,让用户在输入框里,按一定的格式填写文本,比如用英文逗号 , 隔开每个词,然后保存的时候,是用数组的格式提交给接口。

这个时候 computed 的 setter 就可以妙用了,只需要一个简单的 computed ,就可以代替 inputchange 事件或者 watch 侦听,可以减少很多业务代码的编写。

vue
<template>
+  <input
+    type="text"
+    v-model="tagsStr"
+    placeholder="请输入标签,多个标签用英文逗号隔开"
+  />
+</template>
+
+<script lang="ts">
+import { defineComponent, computed, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 这个是最终要用到的数组
+    const tags = ref<string[]>([])
+
+    // 因为input必须绑定一个字符串
+    const tagsStr = computed({
+      // 所以通过getter来转成字符串
+      get() {
+        return tags.value.join(',')
+      },
+      // 然后在用户输入的时候,切割字符串转换回数组
+      set(newValue: string) {
+        tags.value = newValue.split(',')
+      },
+    })
+
+    return {
+      tagsStr,
+    }
+  },
+})
+</script>

所以在实际业务开发中,开发者可以多考虑一下是否可以使用 computed 代替 watch ,避免过多的数组侦听带来项目性能的下降。

指令

指令是 Vue 模板语法里的特殊标记,在使用上和 HTML 的 data-* 属性十分相似,统一以 v- 开头( e.g. v-html )。

它以简单的方式实现了常用的 JavaScript 表达式功能,当表达式的值改变的时候,响应式地作用到 DOM 上。

内置指令

Vue 提供了一些内置指令可以直接使用,例如:

vue
<template>
+  <!-- 渲染一段文本 -->
+  <span v-text="msg"></span>
+
+  <!-- 渲染一段 HTML -->
+  <div v-html="html"></div>
+
+  <!-- 循环创建一个列表 -->
+  <ul v-if="items.length">
+    <li v-for="(item, index) in items" :key="index">
+      <span>{{ item }}</span>
+    </li>
+  </ul>
+
+  <!-- 一些事件( `@` 等价于 `v-on` ) -->
+  <button @click="hello">Hello</button>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const msg = ref<string>('Hello World!')
+    const html = ref<string>('<p>Hello World!</p>')
+    const items = ref<string[]>(['a', 'b', 'c', 'd'])
+
+    function hello() {
+      console.log(msg.value)
+    }
+
+    return {
+      msg,
+      html,
+      items,
+      hello,
+    }
+  },
+})
+</script>

内置指令在使用上都非常的简单,可以在官方文档的 内置指令 一章查询完整的指令列表和用法,在模板上使用时,请了解 指令的模板语法

TIP

有两个指令可以使用别名:

  • v-on 的别名是 @ ,使用 @click 等价于 v-on:click
  • v-bind 的别名是 : ,使用 :src 等价于 v-bind:src

自定义指令 ~new

如果 Vue 的内置指令不能满足业务需求,还可以开发自定义指令。

相关的 TS 类型

在开始编写代码之前,先了解一下自定义指令相关的 TypeScript 类型。

自定义指令有两种实现形式,一种是作为一个对象,其中的写法比较接近于 Vue 组件,除了 getSSRPropsdeep 选项 外,其他的每一个属性都是一个 钩子函数 ,下一小节会介绍钩子函数的内容。

ts
// 对象式写法的 TS 类型
+// ...
+export declare interface ObjectDirective<T = any, V = any> {
+  created?: DirectiveHook<T, null, V>
+  beforeMount?: DirectiveHook<T, null, V>
+  mounted?: DirectiveHook<T, null, V>
+  beforeUpdate?: DirectiveHook<T, VNode<any, T>, V>
+  updated?: DirectiveHook<T, VNode<any, T>, V>
+  beforeUnmount?: DirectiveHook<T, null, V>
+  unmounted?: DirectiveHook<T, null, V>
+  getSSRProps?: SSRDirectiveHook
+  deep?: boolean
+}
+// ...

另外一种是函数式写法,只需要定义成一个函数,但这种写法只在 mountedupdated 这两个钩子生效,并且触发一样的行为。

ts
// 函数式写法的 TS 类型
+// ...
+export declare type FunctionDirective<T = any, V = any> = DirectiveHook<
+  T,
+  any,
+  V
+>
+// ...

这是每个钩子函数对应的类型,它有 4 个入参:

ts
// 钩子函数的 TS 类型
+// ...
+export declare type DirectiveHook<
+  T = any,
+  Prev = VNode<any, T> | null,
+  V = any
+> = (
+  el: T,
+  binding: DirectiveBinding<V>,
+  vnode: VNode<any, T>,
+  prevVNode: Prev
+) => void
+// ...

钩子函数第二个参数的类型:

ts
// 钩子函数第二个参数的 TS 类型
+// ...
+export declare interface DirectiveBinding<V = any> {
+  instance: ComponentPublicInstance | null
+  value: V
+  oldValue: V | null
+  arg?: string
+  modifiers: DirectiveModifiers
+  dir: ObjectDirective<any, V>
+}
+// ...

可以看到自定义指令最核心的就是 “钩子函数” 了,接下来来了解这部分的知识点。

钩子函数

组件的生命周期 类似,自定义指令里的逻辑代码也有一些特殊的调用时机,在这里称之为钩子函数:

钩子函数调用时机
created在绑定元素的 attribute 或事件侦听器被应用之前调用
beforeMount当指令第一次绑定到元素并且在挂载父组件之前调用
mounted在绑定元素的父组件被挂载后调用
beforeUpdate在更新包含组件的 VNode 之前调用
updated在包含组件的 VNode 及其子组件的 VNode 更新后调用
beforeUnmount在卸载绑定元素的父组件之前调用
unmounted当指令与元素解除绑定且父组件已卸载时,只调用一次

TIP

因为自定义指令的默认写法是一个对象,所以在代码风格上是遵循 Options API 的生命周期命名,而非 Vue 3 的 Composition API 风格。

钩子函数在用法上就是这样子:

ts
const myDirective = {
+  created(el, binding, vnode, prevVnode) {
+    // ...
+  },
+  mounted(el, binding, vnode, prevVnode) {
+    // ...
+  },
+  // 其他钩子...
+}

相关的 TS 类型 已了解,每个钩子函数都有 4 个入参:

参数作用
el指令绑定的 DOM 元素,可以直接操作它
binding一个对象数据,见下方的单独说明
vnodeel 对应在 Vue 里的虚拟节点信息
prevVNodeUpdate 时的上一个虚拟节点信息,仅在 beforeUpdateupdated 可用

其中用的最多是 elbinding 了。

  • el 的值就是通过 document.querySelector 拿到的那个 DOM 元素。

  • binding 是一个对象,里面包含了以下属性:

属性作用
value传递给指令的值,例如 v-foo="bar" 里的 bar ,支持任意有效的 JS 表达式
oldValue指令的上一个值,仅对 beforeUpdateupdated 可用
arg传给指令的参数,例如 v-foo:bar 里的 bar
modifiers传给指令的修饰符,例如 v-foo.bar 里的 bar
instance使用指令的组件实例
dir指令定义的对象(就是上面的 const myDirective = { /* ... */ } 这个对象)

在了解了指令的写法和参数作用之后,来看看如何注册一个自定义指令。

局部注册

自定义指令可以在单个组件内定义并使用,通过和 setup 函数 同级别的 directives 选项进行定义,可以参考下面的例子和注释:

vue
<template>
+  <!-- 这个使用默认值 `unset` -->
+  <div v-highlight>{{ msg }}</div>
+
+  <!-- 这个使用传进去的黄色 -->
+  <div v-highlight="`yellow`">{{ msg }}</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  // 自定义指令在这里编写,和 `setup` 同级别
+  directives: {
+    // `directives` 下的每个字段名就是指令名称
+    highlight: {
+      // 钩子函数
+      mounted(el, binding) {
+        el.style.backgroundColor =
+          typeof binding.value === 'string' ? binding.value : 'unset'
+      },
+    },
+  },
+  setup() {
+    const msg = ref<string>('Hello World!')
+
+    return {
+      msg,
+    }
+  },
+})
+</script>

上面是对象式的写法,也可以写成函数式:

ts
export default defineComponent({
+  directives: {
+    highlight(el, binding) {
+      el.style.backgroundColor =
+        typeof binding.value === 'string' ? binding.value : 'unset'
+    },
+  },
+})

TIP

局部注册的自定义指令,默认在子组件内生效,子组件内无需重新注册即可使用父组件的自定义指令。

全局注册

自定义指令也可以注册成全局,这样就无需在每个组件里定义了,只要在入口文件 main.ts 里启用它,任意组件里都可以使用自定义指令。

请查看 开发本地 Vue 专属插件 一节的内容了解如何注册一个全局的自定义指令插件。

deep 选项

除了 钩子函数 ,在 相关的 TS 类型 里还可以看到有一个 deep 选项,它是一个布尔值,作用是:

如果自定义指令用于一个有嵌套属性的对象,并且需要在嵌套属性更新的时候触发 beforeUpdateupdated 钩子,那么需要将这个选项设置为 true 才能够生效。

vue
<template>
+  <div v-foo="foo"></div>
+</template>
+
+<script lang="ts">
+import { defineComponent, reactive } from 'vue'
+
+export default defineComponent({
+  directives: {
+    foo: {
+      beforeUpdate(el, binding) {
+        console.log('beforeUpdate', binding)
+      },
+      updated(el, binding) {
+        console.log('updated', binding)
+      },
+      mounted(el, binding) {
+        console.log('mounted', binding)
+      },
+      // 需要设置为 `true` ,如果是 `false` 则不会触发
+      deep: true,
+    },
+  },
+  setup() {
+    // 定义一个有嵌套属性的对象
+    const foo = reactive({
+      bar: {
+        baz: 1,
+      },
+    })
+
+    // 2s 后修改其中一个值,会触发 `beforeUpdate` 和 `updated`
+    setTimeout(() => {
+      foo.bar.baz = 2
+      console.log(foo)
+    }, 2000)
+
+    return {
+      foo,
+    }
+  },
+})
+</script>

插槽

Vue 在使用子组件的时候,子组件在 template 里类似一个 HTML 标签,可以在这个子组件标签里传入任意模板代码以及 HTML 代码,这个功能就叫做 “插槽” 。

默认插槽

默认情况下,子组件使用 <slot /> 标签即可渲染父组件传下来的插槽内容,例如:

在父组件这边:

vue
<template>
+  <Child>
+    <!-- 注意这里,子组件标签里面传入了 HTML 代码 -->
+    <p>这是插槽内容</p>
+  </Child>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+import Child from '@cp/Child.vue'
+
+export default defineComponent({
+  components: {
+    Child,
+  },
+})
+</script>

在子组件这边:

vue
<template>
+  <slot />
+</template>

默认插槽非常简单,一个 <slot /> 就可以了。

具名插槽

有时候可能需要指定多个插槽,例如一个子组件里有 “标题” 、 “作者”、 “内容” 等预留区域可以显示对应的内容,这时候就需要用到具名插槽来指定不同的插槽位。

子组件通过 name 属性来指定插槽名称:

vue
<template>
+  <!-- 显示标题的插槽内容 -->
+  <div class="title">
+    <slot name="title" />
+  </div>
+
+  <!-- 显示作者的插槽内容 -->
+  <div class="author">
+    <slot name="author" />
+  </div>
+
+  <!-- 其他插槽内容放到这里 -->
+  <div class="content">
+    <slot />
+  </div>
+</template>

父组件通过 template 标签绑定 v-slot:name 格式的属性,来指定传入哪个插槽里:

vue
<template>
+  <Child>
+    <!-- 传给标题插槽 -->
+    <template v-slot:title>
+      <h1>这是标题</h1>
+    </template>
+
+    <!-- 传给作者插槽 -->
+    <template v-slot:author>
+      <h1>这是作者信息</h1>
+    </template>
+
+    <!-- 传给默认插槽 -->
+    <p>这是插槽内容</p>
+  </Child>
+</template>

v-slot:name 有一个别名 #name 语法,上面父组件的代码也相当于:

vue
<template>
+  <Child>
+    <!-- 传给标题插槽 -->
+    <template #title>
+      <h1>这是标题</h1>
+    </template>
+
+    <!-- 传给作者插槽 -->
+    <template #author>
+      <h1>这是作者信息</h1>
+    </template>
+
+    <!-- 传给默认插槽 -->
+    <p>这是插槽内容</p>
+  </Child>
+</template>

TIP

在使用具名插槽的时候,子组件如果不指定默认插槽,那么在具名插槽之外的内容将不会被渲染。

默认内容

可以给 slot 标签添加内容,例如 <slot>默认内容</slot> ,当父组件没有传入插槽内容时,会使用默认内容来显示,默认插槽和具名插槽均支持该功能。

注意事项

有一条规则需要记住:

  • 父组件里的所有内容都是在父级作用域中编译的
  • 子组件里的所有内容都是在子作用域中编译的

CSS 样式与预处理器

Vue 组件的 CSS 样式部分,Vue 3 保留着和 Vue 2 完全一样的写法。

编写组件样式表

最基础的写法,就是在 .vue 文件里添加一个 <style /> 标签,即可在里面写 CSS 代码了。

vue
<template>
+  <div>
+    <!-- HTML 代码 -->
+  </div>
+</template>
+
+<script lang="ts">
+  // TypeScript 代码
+</script>
+
+<style>
+/* CSS 代码 */
+.msg {
+  width: 100%;
+}
+.msg p {
+  color: #333;
+  font-size: 14px;
+}
+</style>

动态绑定 CSS

动态绑定 CSS ,在 Vue 2 就已经存在了,在此之前常用的是 :class:style ,现在在 Vue 3 ,还可以通过 v-bind 来动态修改了。

使用 :class 动态修改样式名

它是绑定在 DOM 元素上面的一个属性,跟 class="class-name" 这样的属性同级别,它非常灵活!

TIP

使用 :class 是用来动态修改样式名,也就意味着必须提前把样式名对应的样式表先写好!

假设已经提前定义好了这几个变量:

vue
<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const activeClass = 'active-class'
+    const activeClass1 = 'active-class1'
+    const activeClass2 = 'active-class2'
+    const isActive = true
+
+    return {
+      activeClass,
+      activeClass1,
+      activeClass2,
+      isActive,
+    }
+  },
+})
+</script>

如果只想绑定一个单独的动态样式,可以传入一个字符串:

vue
<template>
+  <p :class="activeClass">Hello World!</p>
+</template>

如果有多个动态样式,也可以传入一个数组:

vue
<template>
+  <p :class="[activeClass1, activeClass2]">Hello World!</p>
+</template>

还可以对动态样式做一些判断,这个时候传入一个对象:

vue
<template>
+  <p :class="{ 'active-class': isActive }">Hello World!</p>
+</template>

多个判断的情况下,记得也用数组套起来:

vue
<template>
+  <p :class="[{ activeClass1: isActive }, { activeClass2: !isActive }]">
+    Hello World!
+  </p>
+</template>

那么什么情况下会用到 :class 呢?

最常见的场景,应该就是导航、选项卡了,比如要给一个当前选中的选项卡做一个突出高亮的状态,那么就可以使用 :class 来动态绑定一个样式。

vue
<template>
+  <ul class="list">
+    <li
+      class="item"
+      :class="{ cur: index === curIndex }"
+      v-for="(item, index) in 5"
+      :key="index"
+      @click="curIndex = index"
+    >
+      {{ item }}
+    </li>
+  </ul>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const curIndex = ref<number>(0)
+
+    return {
+      curIndex,
+    }
+  },
+})
+</script>
+
+<style scoped>
+.cur {
+  color: red;
+}
+</style>

这样就简单实现了一个点击切换选项卡高亮的功能。

使用 :style 动态修改内联样式

如果觉得使用 :class 需要提前先写样式,再去绑定样式名有点繁琐,有时候只想简简单单的修改几个样式,那么可以通过 :style 来处理。

默认的情况下,都是传入一个对象去绑定:

  • key 是符合 CSS 属性名的 “小驼峰式” 写法,或者套上引号的短横线分隔写法(原写法),例如在 CSS 里,定义字号是 font-size ,那么需要写成 fontSize 或者 'font-size' 作为它的键。

  • value 是 CSS 属性对应的 “合法值”,比如要修改字号大小,可以传入 13px0.4rem 这种带合法单位字符串值,但不可以是 13 这样的缺少单位的值,无效的 CSS 值会被过滤不渲染。

vue
<template>
+  <p
+    :style="{
+      fontSize: '13px',
+      'line-height': 2,
+      color: '#ff0000',
+      textAlign: 'center',
+    }"
+  >
+    Hello World!
+  </p>
+</template>

如果有些特殊场景需要绑定多套 style,需要在 script 先定义好各自的样式变量(也是符合上面说到的那几个要求的对象),然后通过数组来传入:

vue
<template>
+  <p :style="[style1, style2]">Hello World!</p>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const style1 = {
+      fontSize: '13px',
+      'line-height': 2,
+    }
+    const style2 = {
+      color: '#ff0000',
+      textAlign: 'center',
+    }
+
+    return {
+      style1,
+      style2,
+    }
+  },
+})
+</script>

使用 v-bind 动态修改 style ~new

当然,以上两种形式都是关于 <script /><template /> 部分的操作,如果觉得会给模板带来一定的维护成本的话,不妨考虑这个新方案,将变量绑定到 <style /> 部分去。

TIP

请注意这是一个在 3.2.0 版本之后才被归入正式队列的新功能!如果需要使用它,请确保的 vue 的版本号在 3.2.0 以上,最好是保持最新版本。

先来看看基本的用法:

vue
<template>
+  <p class="msg">Hello World!</p>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const fontColor = ref<string>('#ff0000')
+
+    return {
+      fontColor,
+    }
+  },
+})
+</script>
+
+<style scoped>
+.msg {
+  color: v-bind(fontColor);
+}
+</style>

如上面的代码,将渲染出一句红色文本的 Hello World!

这其实是利用了现代浏览器支持的 CSS 变量来实现的一个功能(所以如果打算用它的话,需要提前注意一下兼容性噢,点击查看:CSS Variables 兼容情况 )。

它渲染到 DOM 上,其实也是通过绑定 style 来实现,可以看到渲染出来的样式是:

html
<p class="msg" data-v-7eb2bc79="" style="--7eb2bc79-fontColor:#ff0000;">
+  Hello World!
+</p>

对应的 CSS 变成了:

css
.msg[data-v-7eb2bc79] {
+  color: var(--7eb2bc79-fontColor);
+}

理论上 v-bind 函数可以在 Vue 内部支持任意的 JavaScript 表达式,但由于可能包含在 CSS 标识符中无效的字符,因此官方是建议在大多数情况下,用引号括起来,如:

css
.text {
+  font-size: v-bind('theme.font.size');
+}

由于 CSS 变量的特性,因此对 CSS 响应式属性的更改不会触发模板的重新渲染(这也是和 :class:style 的最大不同)。

TIP

不管有没有开启 <style scoped> ,使用 v-bind 渲染出来的 CSS 变量,都会带上 scoped 的随机 hash 前缀,避免样式污染(永远不会意外泄漏到子组件中),所以请放心使用!

如果对 CSS 变量的使用还不是很了解的话,可以先阅读一下相关的基础知识点。

相关阅读:使用 CSS 自定义属性(变量) - MDN

样式表的组件作用域

CSS 不像 JS ,是没有作用域的概念的,一旦写了某个样式,直接就是全局污染。所以 BEM 命名法 等规范才应运而生。

但在 Vue 组件里,有两种方案可以避免出现这种污染问题:一个是 Vue 2 就有的 <style scoped> ,一个是 Vue 3 新推出的 <style module>

Style Scoped

Vue 组件在设计的时候,就想到了一个很优秀的解决方案,通过 scoped 来支持创建一个 CSS 作用域,使这部分代码只运行在这个组件渲染出来的虚拟 DOM 上。

使用方式很简单,只需要在 <style /> 上添加 scoped 属性:

vue
<!-- 注意这里多了一个 `scoped` -->
+<style scoped>
+.msg {
+  width: 100%;
+}
+.msg p {
+  color: #333;
+  font-size: 14px;
+}
+</style>

编译后,虚拟 DOM 都会带有一个 data-v-xxxxx 这样的属性,其中 xxxxx 是一个随机生成的 Hash ,同一个组件的 Hash 是相同并且唯一的:

html
<div class="msg" data-v-7eb2bc79>
+  <p data-v-7eb2bc79>Hello World!</p>
+</div>

而 CSS 则也会带上与 HTML 相同的属性,从而达到样式作用域的目的。

css
.msg[data-v-7eb2bc79] {
+  width: 100%;
+}
+.msg p[data-v-7eb2bc79] {
+  color: #333;
+  font-size: 14px;
+}

使用 scoped 可以有效的避免全局样式污染,可以在不同的组件里面都使用相同的 className,而不必担心会相互覆盖,不必再定义很长很长的样式名来防止冲突了。

TIP

添加 scoped 生成的样式,只作用于当前组件中的元素,并且权重高于全局 CSS ,可以覆盖全局样式

Style Module ~new

这是在 Vue 3 才推出的一个新方案,和 <style scoped> 不同,scoped 是通过给 DOM 元素添加自定义属性的方式来避免冲突,而 <style module> 则更为激进,将会编译成 CSS Modules

对于 CSS Modules 的处理方式,也可以通过一个小例子来更直观的了解它:

css
/* 案例来自阮一峰老师的博文《CSS Modules 用法教程》 */
+/* https://www.ruanyifeng.com/blog/2016/06/css_modules.html */
+
+/* 编译前 */
+.title {
+  color: red;
+}
+
+/* 编译后 */
+._3zyde4l1yATCOkgn-DBWEL {
+  color: red;
+}

可以看出,是通过比较 “暴力” 的方式,把编写的 “好看的” 样式名,直接改写成一个随机 Hash 样式名,来避免样式互相污染。

所以回到 Vue 这边,看看 <style module> 是怎么操作的。

vue
<template>
+  <p :class="$style.msg">Hello World!</p>
+</template>
+
+<style module>
+.msg {
+  color: #ff0000;
+}
+</style>

于是,将渲染出一句红色文本的 Hello World!

TIP

  1. 使用这个方案,需要了解如何 使用 :class 动态修改样式名

  2. 如果单纯只使用 <style module> ,那么在绑定样式的时候,是默认使用 $style 对象来操作的

  3. 必须显示的指定绑定到某个样式,比如 $style.msg ,才能生效

  4. 如果单纯的绑定 $style ,并不能得到 “把全部样式名直接绑定” 的期望结果

  5. 如果指定的 className 是短横杆命名,比如 .user-name ,那么需要通过 $style['user-name'] 去绑定

也可以给 module 进行命名,然后就可以通过命名的 “变量名” 来操作:

vue
<template>
+  <p :class="classes.msg">Hello World!</p>
+</template>
+
+<style module="classes">
+.msg {
+  color: #ff0000;
+}
+</style>

TIP

需要注意的一点是,一旦开启 <style module> ,那么在 <style module> 里所编写的样式都必须手动绑定才能生效,没有被绑定的样式虽然也会被编译,但不会主动生效到 DOM 上。

原因是编译出来的样式名已经变化,而原来的 DOM 未指定对应的样式名,或者指定的是编译前的命名,所以并不能匹配到正确的样式。

useCssModule ~new

这是一个全新的 API ,面向在 script 部分操作 CSS Modules 。

在上面的 CSS Modules 部分可以知道,可以在 style 定义好样式,然后在 template 部分通过变量名来绑定样式。

那么如果有一天有个需求,需要通过 v-html 来渲染 HTML 代码,那这里的样式岂不是凉凉了?当然不会!

Vue 3 提供了一个 Composition API useCssModule 来帮助在 setup 函数里操作的 CSS Modules (对,只能在 setup 或者 script setup 里使用)。

基本用法:

绑定多几个样式,再来操作:

vue
<template>
+  <p :class="$style.msg">
+    <span :class="$style.text">Hello World!</span>
+  </p>
+</template>
+
+<script lang="ts">
+import { defineComponent, useCssModule } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const style = useCssModule()
+    console.log(style)
+  },
+})
+</script>
+
+<style module>
+.msg {
+  color: #ff0000;
+}
+.text {
+  font-size: 14px;
+}
+</style>

可以看到打印出来的 style 是一个对象:

  • key 是在 <style modules> 里定义的原始样式名

  • value 则是编译后的新样式名

js
{
+  msg: 'home_msg_37Xmr',
+  text: 'home_text_2woQJ'
+}

所以来配合 模板字符串 的使用,看看刚刚说的,要通过 v-html 渲染出来的内容应该如何绑定样式:

vue
<template>
+  <div v-html="content"></div>
+</template>
+
+<script lang="ts">
+import { defineComponent, useCssModule } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 获取样式
+    const style = useCssModule()
+
+    // 编写模板内容
+    const content = `<p class="${style.msg}">
+      <span class="${style.text}">Hello World! —— from v-html</span>
+    </p>`
+
+    return {
+      content,
+    }
+  },
+})
+</script>
+
+<style module>
+.msg {
+  color: #ff0000;
+}
+.text {
+  font-size: 14px;
+}
+</style>

是不是也非常简单?可能刚开始不太习惯,但写多几次其实也蛮好玩的这个功能!

另外,需要注意的是,如果是指定了 modules 的名称,那么必须传入对应的名称作为入参才可以正确拿到这些样式:

比如指定了一个 classes 作为名称:

vue
<style module="classes">
+/* ... */
+</style>

那么需要通过传入 classes 这个名称才能拿到样式,否则会是一个空对象:

ts
const style = useCssModule('classes')

TIP

const style = useCssModule() 的时候,命名是随意的,跟在 <style module="classes"> 这里指定的命名没有关系。

深度操作符 ~new

样式表的组件作用域 部分了解到,使用 scoped 后,父组件的样式将不会渗透到子组件中,但也不能直接修改子组件的样式。

如果确实需要进行修改子组件的样式,必须通过 ::v-deep(完整写法) 或者 :deep(快捷写法) 操作符来实现。

TIP

  1. 旧版的深度操作符是 >>>/deep/::v-deep,现在 >>>/deep/ 已进入弃用阶段(虽然暂时还没完全移除)

  2. 同时需要注意的是,旧版 ::v-deep 的写法是作为组合器的方式,写在样式或者元素前面,如:::v-deep .class-name { /* ... */ },现在这种写法也废弃了。

现在不论是 ::v-deep 还是 :deep ,使用方法非常统一,来假设 .b 是子组件的样式名:

vue
<style scoped>
+.a :deep(.b) {
+  /* ... */
+}
+</style>

编译后:

css
.a[data-v-f3f3eg9] .b {
+  /* ... */
+}

TIP

可以看到,新的 deep 写法是作为一个类似 JS “函数” 那样去使用,需要深度操作的样式或者元素名,作为 “入参” 去传入。

同理,如果使用 Less 或者 Stylus 这种支持嵌套写法的预处理器,也是可以这样去深度操作的:

less
.a {
+  :deep(.b) {
+    /* ... */
+  }
+}

另外,除了操作子组件的样式,那些通过 v-html 创建的 DOM 内容,也不受作用域内的样式影响,也可以通过深度操作符来实现样式修改。

使用 CSS 预处理器

在工程化的现在,可以说前端都几乎不写 CSS 了,都是通过 sasslessstylus 等 CSS 预处理器来完成样式的编写。

为什么要用 CSS 预处理器?放一篇关于三大预处理器的点评,新开发者可以做个简单了解,具体的用法在对应的官网上有非常详细的说明。

可以查看了解:浅谈 CSS 预处理器,Sass、Less 和 Stylus

在 Vue 组件里使用预处理器非常简单,像 Vite 已内置了对预处理器文件的支持(可处理 .less.scss 之类的预处理器扩展名文件),因此只需要安装对应的依赖到项目里。

这里以 Less 为例,先安装该预处理器:

bash
# 因为是在开发阶段使用,所以添加到 `devDependencies`
+npm i -D less

接下来在 Vue 组件里,只需要在 <style /> 标签上,通过 lang="less" 属性指定使用哪个预处理器,即可直接编写对应的代码:

vue
<style lang="less" scoped>
+// 定义颜色变量
+@color-black: #333;
+@color-red: #ff0000;
+
+// 父级标签
+.msg {
+  width: 100%;
+  // 其子标签可以使用嵌套写法
+  p {
+    color: @color-black;
+    font-size: 14px;
+    // 支持多级嵌套
+    span {
+      color: @color-red;
+    }
+  }
+}
+</style>

编译后的 css 代码:

css
.msg {
+  width: 100%;
+}
+.msg p {
+  color: #333333;
+  font-size: 14px;
+}
+.msg p span {
+  color: #ff0000;
+}

预处理器也支持 scoped,用法请查阅 样式表的组件作用域 部分。

Released under the MIT License.

+ + + + \ No newline at end of file diff --git a/efficient.html b/efficient.html new file mode 100644 index 00000000..e47a96ca --- /dev/null +++ b/efficient.html @@ -0,0 +1,568 @@ + + + + + + 高效开发 | Vue3 入门指南与实战案例 + + + + + + + + + + + + + +
Skip to content
目录

高效开发

可能很多开发者(包括笔者)刚上手 Vue 3 的那段时间,都会觉得开发过程似乎变得更繁琐了, Vue 官方团队当然不会无视群众的呼声,如果基于脚手架和 .vue 文件开发,那么可以享受到更高效率的开发体验。

在阅读这一章之前,需要对 Vue 3 的单组件开发有一定的了解,如果还处于完全没有接触过的阶段,请先抽点时间阅读 单组件的编写 一章。

TIP

要体验以下新特性,请确保项目下 package.json 里的 vue 版本在 3.2.0 以上,最好同步 npm 上当前最新的 @latest 版本,否则可能出现 API 未定义等问题。

script-setup ~new

script-setup 是 Vue 3 组件的一个语法糖,旨在帮助开发者降低 setup 函数需要 return 的心智负担。

Vue 的 3.1.2 版本是针对 script-setup 的一个分水岭版本,自 3.1.4 开始 script-setup 进入定稿状态,部分旧的 API 已被舍弃,本章节内容将以最新的 API 为准进行整理说明,如果需要查阅旧版 API 的使用,请参阅笔者的 这篇开荒博文

script-setup 方案已在 Vue 3.2.0-beta.1 版本中脱离实验状态,正式进入 Vue 3 的队伍,此后所有的新版本均可以作为一个官方标准的开发方案使用。

新特性的产生背景

在了解 script-setup 怎么用之前,可以先了解一下推出该语法糖的一些开发背景,通过对比开发体验上的异同点,了解为什么会有这个新模式。

在 Vue 3 的组件标准写法里,如果数据和方法需要在 <template /> 里使用,都需要在 <script /> 的 setup 函数里 return 出来。

如果使用的是 TypeScript ,还需要借助 defineComponent 对 API 类型进行自动推导。

vue
<!-- 标准组件格式 -->
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // ...
+
+    return {
+      // ...
+    }
+  },
+})
+</script>

关于标准 setup 和 defineComponent 的说明和用法,可以查阅 全新的 setup 函数 一节。

script-setup 的推出是为了让熟悉 Vue 3 的开发者可以更高效率地开发组件,减少编码过程中的心智负担,只需要给 <script /> 标签添加一个 setup 属性,那么整个 <script /> 就直接会变成 setup 函数,所有顶级变量、函数,均会自动暴露给模板使用(无需再一个个 return 了)。

Vue 会通过单组件编译器,在编译的时候将其处理回标准组件,所以目前这个方案只适合用 .vue 文件写的工程化项目。

vue
<!-- 使用 script-setup 格式 -->
+<script setup lang="ts">
+// ...
+</script>

对,就是这样,代码量瞬间大幅度减少!

因为 script-setup 的大部分功能在书写上和标准版是一致的,因此下面的内容只提及有差异的写法。

全局编译器宏

在 script-setup 模式下,新增了 4 个全局编译器宏,他们无需 import 就可以直接使用。

但是默认的情况下直接使用,如果项目开启了 ESLint ,可能会提示 API 没有导入,但导入 API 后,控制台的 Vue 编译助手又会提示不需要导入,就很尴尬… 不过不用着急!可以通过配置 Lint 规则解决这个问题!

将这几个编译助手写进全局规则里,这样不导入也不会报错了。

js
// 项目根目录下的 .eslintrc.js
+module.exports = {
+  // ...
+  // 在原来的 Lint 规则后面,补充下面的 `globals` 选项
+  globals: {
+    defineProps: 'readonly',
+    defineEmits: 'readonly',
+    defineExpose: 'readonly',
+    withDefaults: 'readonly',
+  },
+}

关于几个宏的说明都在下面的文档部分有说明,也可以从这里导航过去直接查看。

说明
defineProps点击查看
defineEmits点击查看
defineExpose点击查看
withDefaults点击查看

下面继续了解 script-setup 的变化。

template 操作简化

如果使用 JSX / TSX 写法,这一点没有太大影响,但对于习惯使用 <template /> 的开发者来说,这是一个非常爽的体验。

主要体现在这两点:

  1. 变量无需进行 return
  2. 子组件无需手动注册

变量无需进行 return

标准组件模式下,变量和方法都需要在 setup 函数里 return 出去,才可以在 <template /> 部分拿到。

vue
<!-- 标准组件格式 -->
+<template>
+  <p>{{ msg }}</p>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const msg = 'Hello World!'
+
+    // 要给 `<template />` 用的数据需要 `return` 出去才可以
+    return {
+      msg,
+    }
+  },
+})
+</script>

在 script-setup 模式下,定义了就可以直接使用。

vue
<!-- 使用 script-setup 格式 -->
+<template>
+  <p>{{ msg }}</p>
+</template>
+
+<script setup lang="ts">
+const msg = 'Hello World!'
+</script>

子组件无需手动注册

子组件的挂载,在标准组件里的写法是需要 import 后再放到 components 里才能够启用:

vue
<!-- 标准组件格式 -->
+<template>
+  <Child />
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+// 导入子组件
+import Child from '@cp/Child.vue'
+
+export default defineComponent({
+  // 需要启用子组件作为模板
+  components: {
+    Child,
+  },
+
+  // 组件里的业务代码
+  setup() {
+    // ...
+  },
+})
+</script>

在 script-setup 模式下,只需要导入组件即可,编译器会自动识别并启用。

vue
<!-- 使用 script-setup 格式 -->
+<template>
+  <Child />
+</template>
+
+<script setup lang="ts">
+import Child from '@cp/Child.vue'
+</script>

props 的接收方式变化

由于整个 script 都变成了一个大的 setup 函数,没有了组件选项,也没有了 setup 的入参,所以没办法和标准写法一样去接收 props 了。

这里需要使用一个全新的 API :defineProps 。

defineProps 是一个方法,内部返回一个对象,也就是挂载到这个组件上的所有 props ,它和普通的 props 用法一样,如果不指定为 props ,则传下来的属性会被放到 attrs 那边去。

TIP

前置知识点:接收 props - 组件之间的通信

defineProps 的基础用法

如果只是单纯在 <template /> 里使用,那么这么简单定义就可以了:

ts
defineProps(['name', 'userInfo', 'tags'])

使用 string[] 数组作为入参,把 prop 的名称作为数组的 item 传给 defineProps 就可以了。

如果 <script /> 里的方法要拿到 props 的值,也可以使用字面量定义:

ts
const props = defineProps(['name', 'userInfo', 'tags'])
+console.log(props.name)

但作为 Vue 的老玩家,都清楚不显性的指定 props 的类型很容易在协作中引起程序报错,那么应该如何对每个 prop 进行类型检查呢?

有两种方式来处理类型定义:

  1. 通过构造函数检查 prop
  2. 使用类型注解检查 prop

通过构造函数检查 prop

这是第一种方式:使用 JavaScript 原生构造函数进行类型规定,也就是跟平时定义 prop 类型时一样, Vue 会通过 instanceof 来进行 类型检查

使用这种方法,需要通过一个 “对象” 入参来传递给 defineProps ,比如:

ts
defineProps({
+  name: String,
+  userInfo: Object,
+  tags: Array,
+})

所有原来 props 具备的校验机制,都可以适用,比如除了要限制类型外,还想指定 name 是可选,并且带有一个默认值:

ts
defineProps({
+  name: {
+    type: String,
+    required: false,
+    default: 'Petter',
+  },
+  userInfo: Object,
+  tags: Array,
+})

更多的 props 校验机制,可以点击 带有类型限制的 props可选以及带有默认值的 props 了解更多。

使用类型注解检查 prop

这是第二种方式:使用 TypeScript 的类型注解,和 ref 等 API 的用法一样,defineProps 也是可以使用尖括号 <> 来包裹类型定义,紧跟在 API 后面。

另外,由于 defineProps 返回的是一个对象(因为 props 本身是一个对象),所以尖括号里面的类型还要用大括号包裹,通过 key: value 的键值对形式表示,如:

ts
defineProps<{ name: string }>()

注意到了吗?这里使用的类型,和第一种方法提到的指定类型是不一样的。这里不再使用构造函数校验,而是需要遵循使用 TypeScript 的类型,比如字符串是 string,而不是 String

如果有多个 prop ,就跟写 interface 一样:

ts
defineProps<{
+  name: string
+  phoneNumber: number
+  userInfo: object
+  tags: string[]
+}>()

其中,举例里的 userInfo 是一个对象,可以简单的指定为 object,也可以先定义好它对应的类型,再进行指定:

ts
interface UserInfo {
+  id: number
+  age: number
+}
+
+defineProps<{
+  name: string
+  userInfo: UserInfo
+}>()

如果想对某个数据设置为可选,也是遵循 TS 规范,通过英文问号 ? 来允许可选:

ts
// name 是可选
+defineProps<{
+  name?: string
+  tags: string[]
+}>()

如果想设置可选参数的默认值,需要借助 withDefaults API。

WARNING

需要强调的一点是:在 构造函数类型注解 这两种校验方式只能二选一,不能同时使用,否则会引起程序报错。

withDefaults 的基础用法

这个新的 withDefaults API 可以让在使用 TS 类型系统时,也可以指定 props 的默认值。

它接收两个入参:

参数含义
props通过 defineProps 传入的 props
defaultValues根据 props 的 key 传入默认值

光看描述可能不容易理解,看看下面这段演示代码会更直观:

ts
withDefaults(
+  // 这是第一个参数,声明 props
+  defineProps<{
+    size?: number
+    labels?: string[]
+  }>(),
+  // 这是第二个参数,设置默认值
+  {
+    size: 3,
+    labels: () => ['default label'],
+  }
+)

也可以通过字面量获取 props :

ts
// 上面的写法可能比较复杂,存在阅读成本
+// 也可以跟平时一样先通过 interface 声明其类型
+interface Props {
+  size?: number
+  labels?: string[]
+}
+
+// 再作为 `defineProps` 的类型传入
+// 代码风格上会简洁很多
+const props = withDefaults(defineProps<Props>(), {
+  size: 3,
+  labels: () => ['default label'],
+})
+
+// 这样就可以通过 `props` 变量拿到需要的值
+console.log(props.size)

emits 的接收方式变化

和 props 一样,emits 的接收也是需要使用一个全新的 API 来操作,这个 API 就是 defineEmits 。

和 defineProps 一样, defineEmits 也是一个方法,它接受的入参格式和标准组件的要求是一致的。

TIP

前置知识点:接收 emits - 组件之间的通信

defineEmits 的基础用法

需要通过字面量来定义 emits ,最基础的用法也是传递一个 string[] 数组进来,把每个 emit 的名称作为数组的 item 。

ts
// 获取 emit
+const emit = defineEmits(['update-name'])
+
+// 调用 emit
+emit('update-name', 'Tom')

由于 defineEmits 的用法和原来的 emits 选项差别不大,这里也不重复说明更多的诸如校验之类的用法了,可以查看 接收 emits 一节了解更多。

attrs 的接收方式变化

attrs 和 props 很相似,也是基于父子通信的数据,如果父组件绑定下来的数据没有被指定为 props ,那么就会被 attrs 接收。

在标准组件里, attrs 的数据是通过 setup 的第二个入参 context 里的 attrs API 获取的。

ts
// 标准组件的写法
+export default defineComponent({
+  setup(props, { attrs }) {
+    // attrs 是个对象,每个 Attribute 都是它的 key
+    console.log(attrs.class)
+
+    // 如果传下来的 Attribute 带有短横线,需要通过这种方式获取
+    console.log(attrs['data-hash'])
+  },
+})

但和 props 一样,由于没有了 context 参数,需要使用一个新的 API 来拿到 attrs 数据,这个 API 就是 useAttrs 。

useAttrs 的基础用法

顾名思义, useAttrs 可以是用来获取 attrs 数据的,它的用法非常简单:

ts
import { useAttrs } from 'vue'
+
+// 获取 attrs
+const attrs = useAttrs()
+
+// attrs 是个对象,和 props 一样,需要通过 `key` 来得到对应的单个 attr
+console.log(attrs.msg)

对 attrs 不太了解的话,可以查阅 获取非 Prop 的 Attribute

slots 的接收方式变化

slots 是 Vue 组件的插槽数据,也是在父子通信里的一个重要成员。

对于使用 <template /> 的开发者来说,在 script-setup 里获取插槽数据并不困难,因为跟标准组件的写法是完全一样的,可以直接在 <template /> 里使用 <slot /> 标签渲染。

vue
<template>
+  <div>
+    <!-- 插槽数据 -->
+    <slot />
+    <!-- 插槽数据 -->
+  </div>
+</template>

但对使用 JSX / TSX 的开发者来说,就影响比较大了,在标准组件里,想在 script 里获取插槽数据,也是需要在 setup 的第二个入参里拿到 slots API 。

ts
// 标准组件的写法
+export default defineComponent({
+  // 这里的 slots 就是插槽
+  setup(props, { slots }) {
+    // ...
+  },
+})

新版本的 Vue 也提供了一个全新的 useSlots API 来帮助 script-setup 用户获取插槽。

useSlots 的基础用法

先来看看父组件,父组件先为子组件传入插槽数据,支持 “默认插槽” 和 “命名插槽” :

vue
<template>
+  <!-- 子组件 -->
+  <ChildTSX>
+    <!-- 默认插槽 -->
+    <p>Default slot for TSX.</p>
+    <!-- 默认插槽 -->
+
+    <!-- 命名插槽 -->
+    <template #msg>
+      <p>Named slot for TSX.</p>
+    </template>
+    <!-- 命名插槽 -->
+  </ChildTSX>
+  <!-- 子组件 -->
+</template>
+
+<script setup lang="ts">
+// 实际上是导入 ChildTSX.tsx 文件,扩展名默认可以省略
+import ChildTSX from '@cp/ChildTSX'
+</script>

在使用 JSX / TSX 编写的子组件里,就可以通过 useSlots 来获取父组件传进来的 slots 数据进行渲染:

tsx
// src/components/ChildTSX.tsx
+import { defineComponent, useSlots } from 'vue'
+
+export default defineComponent({
+  setup() {
+    // 获取插槽数据
+    const slots = useSlots()
+
+    // 渲染组件
+    return () => (
+      <div>
+        {/* 渲染默认插槽 */}
+        <p>{slots.default ? slots.default() : ''}</p>
+
+        {/* 渲染命名插槽 */}
+        <p>{slots.msg ? slots.msg() : ''}</p>
+      </div>
+    )
+  },
+})

请注意,这里的 TSX 组件代码需要使用 .tsx 作为文件扩展名,并且构建工具可能默认没有对 JSX / TSX 作支持,以 Vite 为例,需要安装官方提供的 JSX / TSX 支持插件才可以正常使用。

bash
# 该插件支持使用 JSX 或 TSX 作为 Vue 组件
+npm i -D @vitejs/plugin-vue-jsx

并在 vite.config.ts 里启用插件,添加对 JSX 和 TSX 的支持。

ts
// vite.config.ts
+import { defineConfig } from 'vite'
+import vueJsx from '@vitejs/plugin-vue-jsx'
+
+export default defineConfig({
+  // ...
+  plugins: [
+    // ...
+    // 启用插件
+    vueJsx(),
+  ],
+})

如果还存在报错的情况,可以检查项目下的 tsconfig.json 文件里,编译选项 jsx 是否设置为 preserve

json
{
+  "compilerOptions": {
+    "jsx": "preserve"
+  }
+}

ref 的通信方式变化

在标准组件写法里,子组件的数据和方法可以通过在 setup 里 return 出来给父组件调用,也就是父组件可以通过 childComponent.value.foo 这样的方式直接操作子组件的数据(参见:DOM 元素与子组件 - 响应式 API 之 ref)。

但在 script-setup 模式下,所有数据只是默认隐式 return 给 <template /> 使用,不会暴露到组件外,所以父组件是无法直接通过挂载 ref 变量获取子组件的数据。

在 script-setup 模式下,如果要调用子组件的数据,需要先在子组件显式的暴露出来,才能够正确的拿到,这个操作,就是由 defineExpose API 来完成。

defineExpose 的基础用法

defineExpose 的用法非常简单,它本身是一个函数,可以接受一个对象参数。

在子组件里,像这样把需要暴露出去的数据通过 key: value 的形式作为入参(下面的例子是用到了 ES6 的 属性的简洁表示法 ):

vue
<script setup lang="ts">
+const msg = 'Hello World!'
+
+// 通过该 API 显式暴露的数据,才可以在父组件拿到
+defineExpose({
+  msg,
+})
+</script>

然后在父组件就可以通过挂载在子组件上的 ref 变量,去拿到暴露出来的数据了。

顶级 await 的支持

在 script-setup 模式下,不必再配合 async 就可以直接使用 await 了,这种情况下,组件的 setup 会自动变成 async setup 。

vue
<script setup lang="ts">
+const res = await fetch(`https://example.com/api/foo`)
+const json = await res.json()
+console.log(json)
+</script>

它转换成标准组件的写法就是:

vue
<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  async setup() {
+    const res = await fetch(`https://example.com/api/foo`)
+    const json = await res.json()
+    console.log(json)
+
+    return {
+      json,
+    }
+  },
+})
+</script>

命名技巧

对于接触编程不久的开发者,在个人练习 demo 或者简单的代码片段里可能会经常看到 var avar b 这样的命名,因为本身是一段练习代码,因此都是 “能跑就行”,问题不大。

但在工作中,很多开发团队都会有语义化命名的规范要求,严格的团队会有 Code Review 环节,使用这种无意义命名的代码将无法通过审查,在这种背景下,开发者可能会在命名上花费很多时间,在这里也分享笔者的一些常用技巧,希望能够帮助开发者节约在命名上的时间开销。

文件命名技巧

在开始讲变量命名之前,先说说文件的命名,因为代码都是保存在文件里,并且可能会互相引用,如果后期再修改文件名或者保存位置而忘记更新代码里的引用路径,那么就会影响程序编译和运行。

Vue 组件

在 Vue 项目里,会有放在 views 下的路由组件,有放在 components 目录下的公共组件,虽然都是以 .vue 为扩展名的 Vue 组件文件,但根据用途,它们其实并不相同,因此命名上也有不同的技巧。

路由组件

路由组件组件通常存放在 src/views 目录下,在命名上容易困惑的点应该是风格问题,开发者容易陷入是使用 camelCase 小驼峰还是 kebab-case 短横线风格,或者是 snake_case 下划线风格的选择困难。

一般情况下路由组件都是以单个名词或动词进行命名,例如个人资料页使用 profile 命名路由,路由的访问路径使用 /profile ,对应的路由组件使用 profile.vue 命名,下面是几个常见的例子。

ts
// src/router/routes.ts
+import type { RouteRecordRaw } from 'vue-router'
+
+const routes: RouteRecordRaw[] = [
+  // 首页
+  // e.g. `https://example.com/`
+  {
+    path: '/',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+  },
+  // 个人资料页
+  // e.g. `https://example.com/profile`
+  {
+    path: '/profile',
+    name: 'profile',
+    component: () => import('@views/profile.vue'),
+  },
+  // 登录页
+  // e.g. `https://example.com/login`
+  {
+    path: '/login',
+    name: 'login',
+    component: () => import('@views/login.vue'),
+  },
+]
+
+export default routes

如果是一些数据列表类的页面,使用名词复数,或者名词单数加上 -list 结尾的 kebab-case 短横线风格写法,推荐短横线风格是因为在 URL 的风格设计里更为常见。

像文章列表可以使用 articles 或者 article-list ,但同一个项目建议只使用其中一种方式,以保持整个项目的风格统一,下面是几个常见的例子。

ts
// src/router/routes.ts
+import type { RouteRecordRaw } from 'vue-router'
+
+const routes: RouteRecordRaw[] = [
+  // 文章列表页
+  // 翻页逻辑是改变页码进行跳转,因此需要添加动态参数 `:page`
+  // 可以在组件内使用路由实例 `route.params.page` 拿到页码
+  // e.g. `https://example.com/articles/1`
+  {
+    path: '/articles/:page',
+    name: 'articles',
+    component: () => import('@views/articles.vue'),
+  },
+  // 通知列表页
+  // 翻页逻辑使用 AJAX 无刷翻页,这种情况则可以不配置页码参数
+  // e.g. `https://example.com/notifications`
+  {
+    path: '/notifications',
+    name: 'notifications',
+    component: () => import('@views/notifications.vue'),
+  },
+]
+
+export default routes

列表里的资源详情页,因为访问的时候通常会带上具体的 ID 以通过接口查询详情数据,这种情况下资源就继续使用单数,例如下面这个例子。

ts
// src/router/routes.ts
+import type { RouteRecordRaw } from 'vue-router'
+
+const routes: RouteRecordRaw[] = [
+  // 文章详情页
+  // 可以在组件内使用路由实例 `route.params.id` 拿到文章 ID
+  // e.g. `https://example.com/article/1`
+  {
+    path: '/article/:id',
+    name: 'article',
+    component: () => import('@views/article.vue'),
+  },
+]
+
+export default routes

如果项目路由比较多,通常会对同一业务的路由增加文件夹归类,因此上面的文章列表和文章详情页,可以统一放到 article 目录下,使用 listdetail 区分是列表还是详情。

ts
// src/router/routes.ts
+import type { RouteRecordRaw } from 'vue-router'
+
+const routes: RouteRecordRaw[] = [
+  // 文章相关的路由统一放在这里管理
+  {
+    path: '/article',
+    name: 'article',
+    // 这是一个配置了 `<router-view />` 标签的路由中转站组件
+    // 目的是使其可以渲染子路由
+    component: () => import('@cp/TransferStation.vue'),
+    // 由于父级路由没有内容,所以重定向至列表的第 1 页
+    // e.g. `https://example.com/article`
+    redirect: {
+      name: 'article-list',
+      params: {
+        page: 1,
+      },
+    },
+    children: [
+      // 文章列表页
+      // e.g. `https://example.com/article/list/1`
+      {
+        path: 'list/:page',
+        name: 'article-list',
+        component: () => import('@views/article/list.vue'),
+      },
+      // 文章详情页
+      // e.g. `https://example.com/article/detail/1`
+      {
+        path: 'detail/:id',
+        name: 'article-detail',
+        component: () => import('@views/article/detail.vue'),
+      },
+    ],
+  },
+]
+
+export default routes

对于一些需要用多个单词才能描述的资源,可以使用 kebab-case 短横线风格命名,例如很常见的 “策划面对面” 这种栏目,在设置路由时,比较难用一个单词在 URL 里体现其含义,就需要使用这种多个单词的连接。

ts
// src/router/routes.ts
+import type { RouteRecordRaw } from 'vue-router'
+
+const routes: RouteRecordRaw[] = [
+  // 面对面栏目
+  {
+    path: '/face-to-face',
+    name: 'face-to-face',
+    component: () => import('@views/face-to-face.vue'),
+  },
+]
+
+export default routes

这种情况如果需要使用文件夹管理多个路由,同样建议使用 kebab-case 短横线风格命名,例如上面这个 “策划面对面” 栏目,可能会归属于 “开发计划” 这个业务下,那么其父级文件夹就可以使用 development-plan 这样的短横线命名。

公共组件

公共组件组件通常存放在 src/components 目录下,也可以视不同的使用情况,在路由文件夹下创建属于当前路由的 components 目录,作为一个小范围共享的公共组件目录来管理,而 src/components 则只存放全局性质的公共组件。

本节最开始提到了路由组件和公共组件并不相同,虽然都是组件,但路由组件代表的是整个页面,而公共组件更多是作为一个页面上的某个可复用的部件,如果开发者写过 Flutter ,应该能够更深刻的理解到这里的公共组件更接近于 Widget 性质的小部件。

公共组件通常使用 PascalCase 帕斯卡命名法,也就是大驼峰,为什么不用小驼峰呢?

这是源于 Vue 官网的一个 组件名格式 命名推荐:

使用 PascalCase 作为组件名的注册格式,这是因为:
PascalCase 是合法的 JavaScript 标识符。这使得在 JavaScript 中导入和注册组件都很容易,同时 IDE 也能提供较好的自动补全。
><PascalCase /> 在模板中更明显地表明了这是一个 Vue 组件,而不是原生 HTML 元素。同时也能够将 Vue 组件和自定义元素( web components )区分开来。

而且实际使用 PascalCase 风格的编码过程中,在 VSCode 里可以得到不同颜色的高亮效果,这与 kebab-case 风格的 HTML 标签可以快速区分。

vue
<template>
+  <!-- 普通的 HTML 标签 -->
+  <!-- 在笔者的 VSCode 风格里呈现为桃红色 -->
+  <div></div>
+
+  <!-- 大驼峰组件 -->
+  <!-- 在笔者的 VSCode 风格里呈现为绿色 -->
+  <PascalCase />
+</template>

养成这种习惯还有一个好处,就是使用 UI 框架的时候,例如 Ant Design Vue 的 Select 组件 ,在其文档上演示的是全局安装的写法:

vue
<template>
+  <a-select>
+    <a-select-option value="Hello">Hello</a-select-option>
+  </a-select>
+</template>

而实际使用时,为了更好的配合构建工具进行 Tree Shaking 移除没有用到的组件,都是按需引入 UI 框架的组件,因此如果平时有养成习惯使用 PascalCase 命名,就可以很轻松的知道上面的 <a-select-option /> 组件应该对应的是 <SelectOption /> ,因此是这样按需导入:

ts
import { Select, SelectOption } from 'ant-design-vue'

可以说, PascalCase 这个命名方式也是目前流行 UI 框架都在使用的命名规范。

TypeScript 文件

在 Vue 项目里,虽然 TypeScript 代码可以写在组件里,但由于很多功能实现是可以解耦并复用,所以经常会有专门的目录管理公共方法,这样做也可以避免在一个组件里写出一两千行代码从而导致维护成本提高。

libs 文件

笔者习惯将这些方法统一放到 src/libs 目录下,按照业务模块或者功能的相似度,以一个名词或者动词作为文件命名。

例如常用的正则表达式,可以归类到 regexp.ts 里。

ts
// src/libs/regexp.ts
+
+// 校验手机号格式
+export function isMob(phoneNumber: number | string) {
+  // ...
+}
+
+// 校验电子邮箱格式
+export function isEmail(email: string) {
+  // ...
+}
+
+// 校验网址格式
+export function isUrl(url: string) {
+  // ...
+}
+
+// 校验身份证号码格式
+export function isIdCard(idCardNumber: string) {
+  // ...
+}
+
+// 校验银行卡号码格式
+export function isBankCard(bankCardNumber: string) {
+  // ...
+}

统一使用命名导出,这样一个 TS 文件就像一个 npm 包一样,在使用的时候就可以从这个 “包” 里面导出各种要用到的方法直接使用,无需在组件里重复编写判断逻辑。

ts
import { isMob, isEmail } from '@libs/regexp'

其他诸如常用到的短信验证 sms.ts 、登录逻辑 login.ts 、数据格式转换 format.ts 都可以像这样单独抽出来封装,这种与业务解耦的封装方式非常灵活,以后不同项目如果也有类似的需求,就可以直接拿过去复用了!

types 文件

对于经常用到的 TypeScript 类型,也可以抽离成公共文件,笔者习惯在 src/types 目录管理公共类型,统一使用 .ts 作为扩展名并在里面导出 TS 类型,而不使用 .d.ts 这个类型声明文件。

这样做的好处是在使用到相应类型时,可以通过 import type 显式导入,在后期的项目维护过程中,可以很明确的知道类型来自于哪里,并且更接近从 npm 包里导入类型使用的开发方式。

例如上文配置路由的例子里,就是从 Vue Router 里导入了路由的类型:

ts
// src/router/routes.ts
+import type { RouteRecordRaw } from 'vue-router'
+
+const routes: RouteRecordRaw[] = [
+  // ...
+]
+
+export default routes

在 types 目录下,可以按照业务模块创建多个模块文件分别维护不同的 TS 类型,并统一在 index.ts 里导出:

bash
src
+└─types
+   # 入口文件
+  ├─index.ts
+   # 管理不同业务的公共类型
+  ├─user.ts
+  ├─game.ts
+  └─news.ts

例如 game.ts 可以维护经常用到的游戏业务相关类型,其中为了避免和其他模块命名冲突,以及一眼可以看出是来自哪个业务的类型,可以统一使用业务模块的名称作为前缀。

ts
// src/types/game.ts
+
+// 游戏公司信息
+export interface GameCompany {
+  // ...
+}
+
+// 游戏信息
+export interface GameInfo {
+  id: number
+  name: string
+  gameCompany: GameCompany
+  // ...
+}

将该模块的所有类型在 index.ts 里全部导出:

ts
// src/types/index.ts
+export * from './game'

在组件里就可以这样使用该类型:

ts
// 可以从 `types` 里统一导入,而不必明确到 `types/game`
+import type { GameInfo } from '@/types'
+
+const game: GameInfo = {
+  id: 1,
+  name: 'Contra',
+  gameCompany: {},
+}
+console.log(game)

TS 类型都遵循 PascalCase 命名风格,方便和声明的变量作为区分,大部分情况下一看到 GameInfo 就知道是类型,而 gameInfo 则是一个变量。

代码命名技巧

在编写 JavaScript / TypeScript 时,为变量和函数的命名也是新手开发者容易花费比较多时间的一件事情,笔者也分享自己常用的命名套路,可以大幅度降低命名的思考时间,而且可以体现一定的语义化。

变量的命名

首先笔者遵循变量只使用 camelCase 小驼峰风格的基本原则,并且根据不同的类型,搭配不同的命名前缀或后缀。

对于 string 字符串类型,使用相关的名词命名即可。

ts
import { ref } from 'vue'
+
+// 用户名
+const username = ref<string>('Petter')
+
+// 职业
+const profession = ref<string>('Front-end Engineer')

对于 number 数值类型,除了一些本身可以代表数字的名词,例如年龄 age 、秒数 seconds ,其他的情况可以搭配后缀命名,常用的后缀有 CountNumberSizeAmount 等和单位有关的名词。

ts
import { ref } from 'vue'
+
+// 最大数量
+const maxCount = ref<number>(100)
+
+// 页码
+const pageNumber = ref<number>(1)
+
+// 每页条数
+const pageSize = ref<number>(10)
+
+// 折扣金额
+const discountAmount = ref<number>(50)

对于 boolean 布尔值类型,可搭配 ishas 等 Be 动词或判断类的动词作为前缀命名,并视情况搭配行为动词和目标名词,或者直接使用一些状态形容词。

ts
import { ref } from 'vue'
+
+// 是否显示弹窗
+const isShowDialog = ref<boolean>(false)
+
+// 用户是否为 VIP 会员
+const isVIP = ref<boolean>(true)
+
+// 用户是否有头像
+const hasAvatar = ref<boolean>(true)
+
+// 是否被禁用
+const disabled = ref<boolean>(true)
+
+// 是否可见
+const visible = ref<boolean>(true)

之所以要搭配 is 开头,是为了和函数区分,例如 showDialog() 是显示弹窗的方法,而 isShowDialog 才是一个布尔值用于逻辑判断。

对于数组,通常使用名词的复数形式,或者名词加上 List 结尾作为命名,数组通常会有原始数据类型的数组,也有 JSON 对象数组,笔者习惯对前者使用名词复数,对后者使用 List 结尾。

ts
import { ref } from 'vue'
+
+// 每个 Item 都是字符串
+const tags = ref<string>(['食物', '粤菜', '卤水'])
+
+// 每个 Item 都是数值
+const tagIds = ref<number>([1, 2, 3])
+
+// 每个 Item 都是 JSON 对象
+const memberList = ref<Member[]>([
+  {
+    id: 1,
+    name: 'Petter',
+  },
+  {
+    id: 2,
+    name: 'Marry',
+  },
+])

如果是作为函数的入参,通常也遵循变量的命名规则。

除非是一些代码量很少的操作,可以使用 ij 等单个字母的变量名,例如提交接口参数时,经常只需要提交一个 ID 数组,从 JSON 数组里提取 ID 数组时就可以使用这种简短命名。

ts
// `map` 的参数命名就可以使用 `i` 这种简短命名
+const ids = dataList.map((i) => i.id)

函数的命名

函数的命名也是只使用 camelCase 小驼峰风格,通常根据该函数是同步操作还是异步操作,使用不同的动词前缀。

获取数据的函数,通常使用 getqueryread 等代表会返回数据的动词作为前缀,如果还是觉得很难确定使用哪一个,可以统一使用 get ,也可以根据函数的操作性质来决定:

  • 如果是同步操作,不涉及接口请求,使用 get 作为前缀
  • 如果是需要从 API 接口查询数据的异步操作,使用 query 作为前缀
  • 如果是 Node.js 程序这种需要进行文件内容读取的场景,就使用 read
ts
// 从本地存储读取数据
+// 因为是同步操作,所以使用 `get` 前缀
+function getLoginInfo() {
+  try {
+    const info = localStorage.getItem('loginInfo')
+    return info ? JSON.parse(info) : null
+  } catch (e) {
+    return null
+  }
+}
+
+// 从接口查询数据
+// 因为是异步操作,需要去数据库查数据,所以使用 `query` 前缀
+async function queryMemberInfo(id: number) {
+  try {
+    const res = await fetch(`https://example.com/api/member/${id}`)
+    const json = await res.json()
+    return json
+  } catch (e) {
+    return null
+  }
+}

修改数据的函数,通常使用 saveupdatedelete 等会变更数据的动词作为前缀,一般情况下:

  • 数据存储可以统一使用 save
  • 如果要区分新建或者更新操作,可以对新建操作使用 create ,对更新操作使用 update
  • 删除使用 deleteremove
  • 如果是 Node.js 程序需要对文件写入内容,使用 write
  • 表单验证合法性等场景,可以使用 verifycheck
  • 切换可见性可以用 showhide ,如果是写在一个函数里,可以使用 toggle
  • 发送验证码、发送邮件等等可以使用 send
  • 打开路由、打开外部 URL 可以使用 open

当然以上只是一些常用到的命名技巧建议,对于简单的业务,例如一个 H5 活动页面,也可以在同步操作时使用 set 表示可以直接设置,在异步操作时使用 save 表示需要提交保存。

ts
// 将数据保存至本地存储
+// 因为是同步操作,所以使用 `set` 前缀
+function setLoginInfo(info: LoginInfo) {
+  try {
+    localStorage.setItem('loginInfo', JSON.stringify(info))
+    return true
+  } catch (e) {
+    return false
+  }
+}
+
+// 将数据通过接口保存到数据库
+// 因为是异步操作,所以使用 `save` 前缀
+async function saveMemberInfo(id: number, data: MemberDTO) {
+  try {
+    const res = await fetch(`https://example.com/api/member/${id}`, {
+      method: 'POST',
+      body: JSON.stringify(data),
+    })
+    const json = await res.json()
+    return json.code === 200
+  } catch (e) {
+    return false
+  }
+}

Class 类上的方法和函数命名规则一样,但 Class 本身使用 PascalCase 命名法,代表这是一个类,在调用的时候需要 new

ts
// 类使用 PascalCase 命名法
+class Hello {
+  name: string
+
+  constructor(name: string) {
+    this.name = name
+  }
+
+  say() {
+    console.log(`Hello ${this.name}`)
+  }
+}
+
+const hello = new Hello('World')
+hello.say() // Hello World

希望曾经在命名上有过困扰的开发者,不再有此烦恼,编写代码更加高效率!

Released under the MIT License.

+ + + + \ No newline at end of file diff --git a/engineering.html b/engineering.html new file mode 100644 index 00000000..5c0599fd --- /dev/null +++ b/engineering.html @@ -0,0 +1,307 @@ + + + + + + 了解前端工程化 | Vue3 入门指南与实战案例 + + + + + + + + + + + + + +
Skip to content
目录

了解前端工程化

现在前端的工作与以前的前端开发已经完全不同了。

刚接触前端的时候,做一个页面,是先创建 HTML 页面文件写页面结构,再在里面写 CSS 代码美化页面,再根据需要写一些 JavaScript 代码增加交互功能,需要几个页面就创建几个页面,相信大家的前端起步都是从这个模式开始的。

而实际上的前端开发工作,早已进入了前端工程化开发的时代,已经充满了各种现代化框架、预处理器、代码编译…

最终的产物也不再单纯是多个 HTML 页面,经常能看到 SPA / SSR / SSG 等词汇的身影。

TIP

现代化的开发概念 一节会介绍这些词汇的含义。

传统开发的弊端

在了解什么是前端工程化之前,先回顾一下传统开发存在的一些弊端,这样更能知道为什么需要它。

在传统的前端开发模式下,前端工程师大部分只需要单纯地写写页面,都是在 HTML 文件里直接编写代码,所需要的 JavaScript 代码是通过 script 标签以内联或者文件引用的形式放到 HTML 代码里的,当然 CSS 代码也是一样的处理方式。

例如这样:

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+  </head>
+  <body>
+    <!-- 引入 JS 文件 -->
+    <script src="./js/lib-1.js"></script>
+    <script src="./js/lib-2.js"></script>
+    <!-- 引入 JS 文件 -->
+  </body>
+</html>

如演示代码,虽然可以把代码分成多个文件来维护,这样可以有效降低代码维护成本,但在实际开发过程中,还是会存在代码运行时的一些问题。

一个常见的案例

继续用上面的演示代码,来看一个最简单的一个例子。

先在 lib-1.js 文件里,声明一个变量:

js
var foo = 1

再在 lib-2.js 文件里,也声明一个变量(没错,也是 foo ):

js
var foo = 2

然后在 HTML 代码里追加一个 script ,打印这个值:

html
<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Document</title>
+</head>
+<body>
+
+  <!-- 引入 JS 文件 -->
+  <script src="./js/lib-1.js"></script>
+  <script src="./js/lib-2.js"></script>
+  <!-- 引入 JS 文件 -->
+
+  <!-- 假设这里是实际的业务代码 -->
+  <script>
+    console.log(foo)
+  </script>
+  <!-- 假设这里是实际的业务代码 -->
+
+</body>
+</html>

先猜猜会输出什么? —— 答案是 2

如果在开发的过程中,不知道在 lib-2.js 文件里也声明了一个 foo 变量,一旦在后面的代码里预期了 foo + 2 === 3 ,那么这样就得不到想要的结果(因为 lib-1.js 里的 foo11 + 2 等于 3 ) 。

原因是 JavaScript 的加载顺序是从上到下,当使用 var 声明变量时,如果命名有重复,那么后加载的变量会覆盖掉先加载的变量。

这是使用 var 声明的情况,它允许使用相同的名称来重复声明,那么换成 let 或者 const 呢?

虽然不会出现重复声明的情况,但同样会收到一段报错:

bash
Uncaught SyntaxError: Identifier 'foo' has already been declared (at lib-2.js:1:1)

这次程序直接崩溃了,因为 letconst 无法重复声明,从而抛出这个错误,程序依然无法正确运行。

更多问题

以上只是一个最简单的案例,就暴露出了传统开发很大的弊端,然而并不止于此,实际上,存在诸如以下这些问题:

  1. 如本案例,可能存在同名的变量声明,引起变量冲突
  2. 引入多个资源文件时,比如有多个 JS 文件,在其中一个 JS 文件里面使用了在别处声明的变量,无法快速找到是在哪里声明的,大型项目难以维护
  3. 类似第 1 、 2 点提到的问题无法轻松预先感知,很依赖开发人员人工定位原因
  4. 大部分代码缺乏分割,比如一个工具函数库,很多时候需要整包引入到 HTML 里,文件很大,然而实际上只需要用到其中一两个方法
  5. 由第 4 点大文件延伸出的问题, script 的加载从上到下,容易阻塞页面渲染
  6. 不同页面的资源引用都需要手动管理,容易造成依赖混乱,难以维护
  7. 如果要压缩 CSS 、混淆 JS 代码,也是要人力操作使用工具去一个个处理后替换,容易出错

当然,实际上还会有更多的问题会遇到。

工程化带来的优势

为了解决传统开发的弊端,前端也开始引入工程化开发的概念,借助工具来解决人工层面的烦琐事情。

开发层面的优势

传统开发的弊端 里,主要列举的是开发层面的问题,工程化首要解决的当然也是在开发层面遇到的问题。

在开发层面,前端工程化有以下这些好处:

  1. 引入了模块化和包的概念,作用域隔离,解决了代码冲突的问题
  2. 按需导出和导入机制,让编码过程更容易定位问题
  3. 自动化的代码检测流程,有问题的代码在开发过程中就可以被发现
  4. 编译打包机制可以让使用开发效率更高的编码方式,比如 Vue 组件、 CSS 的各种预处理器
  5. 引入了代码兼容处理的方案( e.g. Babel ),可以让开发者自由使用更先进的 JavaScript 语句,而无需顾忌浏览器兼容性,因为最终会转换为浏览器兼容的实现版本
  6. 引入了 Tree Shaking 机制,清理没有用到的代码,减少项目构建后的体积

还有非常多的体验提升,列举不完。而对应的工具,根据用途也会有非常多的选择,在后面的学习过程中,会一步一步体验到工程化带来的好处。

团队协作的优势

除了对开发者有更好的开发体验和效率提升,对于团队协作,前端工程化也带来了更多的便利,例如下面这些场景:

统一的项目结构

以前的项目结构比较看写代码的人的喜好,虽然一般在研发部门里都有 “团队规范” 这种东西,但靠自觉性去配合的事情,还是比较难做到统一,特别是项目很赶的时候。

工程化后的项目结构非常清晰和统一,以 Vue 项目来说,通过脚手架创建一个新项目之后,它除了提供能直接运行 Hello World 的基础代码之外,还具备了如下的统一目录结构:

  • src 是源码目录
  • src/main.ts 是入口文件
  • src/views 是路由组件目录
  • src/components 是子组件目录
  • src/router 是路由目录

虽然也可以自行调整成别的结构,但根据笔者在多年的工作实际接触下来,以及从很多开源项目的代码里看到的,都是沿用脚手架创建的项目结构(不同脚手架创建的结构会有所不同,但基于同一技术栈的项目基本上都具备相同的结构)。

TIP

脚手架的升级与配置 一章可以学习如何使用脚手架创建 Vue 3 项目。

统一的代码风格

不管是接手其他人的代码或者是修改自己不同时期的代码,可能都会遇到这样的情况,例如一个模板语句,上面包含了很多属性,有的人喜欢写成一行,属性多了维护起来很麻烦,需要花费较多时间辨认:

vue
<template>
+  <div class="list">
+    <!-- 这个循环模板有很多属性 -->
+    <div class="item" :class="{ `top-${index + 1}`: index < 3 }" v-for="(item, index)
+    in list" :key="item.id" @click="handleClick(item.id)">
+      <span>{{ item.text }}</span>
+    </div>
+    <!-- 这个循环模板有很多属性 -->
+  </div>
+</template>

而工程化配合统一的代码格式化规范,可以让不同人维护的代码,最终提交到 Git 上的时候,风格都保持一致,并且类似这种很多属性的地方,都会自动帮格式化为一个属性一行,维护起来就很方便:

vue
<template>
+  <div class="list">
+    <!-- 这个循环模板有很多属性 -->
+    <div
+      class="item"
+      :class="{ `top-${index + 1}`: index < 3 }"
+      v-for="(item, index) in list"
+      :key="item.id"
+      @click="handleClick(item.id)"
+    >
+      <span>{{ item.text }}</span>
+    </div>
+    <!-- 这个循环模板有很多属性 -->
+  </div>
+</template>

同样的,写 JavaScript 时也会有诸如字符串用双引号还是单引号,缩进是 Tab 还是空格,如果用空格到底是要 4 个空格还是 2 个空格等一堆 “没有什么实际意义” 、但是不统一的话协作起来又很难受的问题……

在工程化项目这些问题都可以交给程序去处理,在书写代码的时候,开发者可以先按照自己的习惯书写,然后再执行命令进行格式化,或者是在提交代码的时候配合 Git Hooks 自动格式化,都可以做到统一风格。

TIP

添加协作规范 一节可以学习如何给项目添加统一的协作规范。

可复用的模块和组件

传统项目比较容易被复用的只有 JavaScript 代码和 CSS 代码,会抽离公共函数文件上传到 CDN ,然后在 HTML 页面里引入这些远程资源, HTML 代码部分通常只有由 JS 创建的比较小段的 DOM 结构。

并且通过 CDN 引入的资源,很多时候都是完整引入,可能有时候只需要用到里面的一两个功能,却要把很大的完整文件都引用进来。

这种情况下,在前端工程化里,就可以抽离成一个开箱即用的 npm 组件包,并且很多包都提供了模块化导出,配合构建工具的 Tree Shaking ,可以抽离用到的代码,没有用到的其他功能都会被抛弃,不会一起发布到生产环境。

TIP

依赖包和插件 一节可以学习如何查找和使用开箱即用的 npm 包。

代码健壮性有保障

传统的开发模式里,只能够写 JavaScript ,而在工程项目里,可以在开发环境编写带有类型系统的 TypeScript ,然后再编译为浏览器能认识的 JavaScript 。

在开发过程中,编译器会检查代码是否有问题,比如在 TypeScript 里声明了一个布尔值的变量,然后不小心将它赋值为数值:

ts
// 声明一个布尔值变量
+let bool: boolean = true
+
+// 在 TypeScript ,不允许随意改变类型,这里会报错
+bool = 3

编译器检测到这个行为的时候就会抛出错误:

bash
# ...
+return new TSError(diagnosticText, diagnosticCodes);
+           ^
+TSError:  Unable to compile TypeScript:
+src/index.ts:2:1 - error TS2322: Type 'number' is not assignable to type 'boolean'.
+
+2 bool = 3
+  ~~~~
+# ...

从而得以及时发现问题并修复,减少线上事故的发生。

团队开发效率高

在前后端合作环节,可以提前 Mock 接口与后端工程师同步开发,如果遇到跨域等安全限制,也可以进行本地代理,不受跨域困扰。

前端工程在开发过程中,还有很多可以交给程序处理的环节,像前面提到的代码格式化、代码检查,还有在部署上线的时候也可以配合 CI/CD 完成自动化流水线,不像以前改个字都要找服务端工程师去更新,可以把非常多的人力操作剥离出来交给程序。

求职竞争上的优势

近几年前端开发领域的相关岗位,都会在招聘详情里出现类似的描述:

熟悉 Vue / React 等主流框架,对前端组件化和模块化有深入的理解和实践
熟悉面向组件的开发模式,熟悉 Webpack / Vite 等构建工具
熟练掌握微信小程序开发,熟悉 Taro 框架或 uni-app 框架优先
熟悉 Scss / Less / Stylus 等预处理器的使用
熟练掌握 TypeScript 者优先
有良好的代码风格,结构设计与程序架构者优先
了解或熟悉后端开发者优先(如 Java / Go / Node.js )

知名企业对 1-3 年工作经验的初中级工程师,更是明确要求具备前端工程化开发的能力:

组件化开发、模块化开发、 Webpack / Vite 构建工具、 Node.js 开发… 这些技能都属于前端工程化开发的知识范畴,不仅在面试的时候会提问,入职后新人接触的项目通常也是直接指派前端工程化项目,如果能够提前掌握相关的知识点,对求职也是非常有帮助的!

Vue.js 与工程化

在上一节提到了前端工程化 在求职竞争上的优势 ,里面列出的招聘要求例子都提及到了 Vue 和 React 这些主流的前端框架,前端框架是前端工程化开发里面不可或缺的成员。

框架能够充分的利用前端工程化相关的领先技术,不仅在开发层面降低开发者的上手难度、提升项目开发效率,在构建出来的项目成果上也有着远比传统开发更优秀的用户体验。

本书结合 Vue.js 框架 3.0 系列的全新版本,将从项目开发的角度,在帮助开发者入门前端工程化的同时,更快速的掌握一个流行框架的学习和使用。

了解 Vue.js 与全新的 3.0 版本

Vue.js(发音为 /vjuː/ ,类似 view )是一个易学易用,性能出色,适用场景丰富的 Web 前端框架,从 2015 年发布 1.0 版本以来,受到了全世界范围的前端开发者喜爱,已成为当下最受欢迎的前端框架之一。

Vue 一直紧跟广大开发者的需求迭代发展,保持着它活跃的生命力。

2020 年 9 月 18 日, Vue.js 发布了 3.0 正式版,在大量开发者长达约一年半的使用和功能改进反馈之后, Vue 又于 2022 年 2 月 7 日发布了 3.2 版本,同一天, Vue 3 成为 Vue.js 框架全新的默认版本 (在此之前,通过 npm install vue 的默认版本还是 Vue 2 )。

也就是在未来的日子里, Vue 3 将随着时间的推移,逐步成为 Vue 生态的主流版本,是时候学习 Vue 3 了!

如果还没有体验过 Vue ,可以把以下代码复制到的代码编辑器,保存成一个 HTML 文件(例如: hello.html ),并在浏览器里打开访问,同时请唤起浏览器的控制台面板(例如 Chrome 浏览器是按 F12 或者鼠标右键点 “检查” ),在 Console 面板查看 Log 的打印。

html
<!-- 这是使用 Vue 实现的 demo -->
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Hello Vue</title>
+    <script src="https://unpkg.com/vue@3"></script>
+  </head>
+  <body>
+    <div id="app">
+      <!-- 通过 `{{ 变量名 }}` 语法渲染响应式变量 -->
+      <p>Hello {{ name }}!</p>
+
+      <!-- 通过 `v-model` 双向绑定响应式变量 -->
+      <!-- 通过 `@input` 给输入框绑定输入事件 -->
+      <input
+        type="text"
+        v-model="name"
+        placeholder="输入名称打招呼"
+        @input="printLog"
+      />
+
+      <!-- 通过 `@click` 给按钮绑定点击事件 -->
+      <button @click="reset">重置</button>
+    </div>
+
+    <script>
+      const { createApp, ref } = Vue
+      createApp({
+        // `setup` 是一个生命周期钩子
+        setup() {
+          // 默认值
+          const DEFAULT_NAME = 'World'
+
+          // 用于双向绑定的响应式变量
+          const name = ref(DEFAULT_NAME)
+
+          // 打印响应式变量的值到控制台
+          function printLog() {
+            // `ref` 变量需要通过 `.value` 操作值
+            console.log(name.value)
+          }
+
+          // 重置响应式变量为默认值
+          function reset() {
+            name.value = DEFAULT_NAME
+            printLog()
+          }
+
+          // 需要 `return` 出去才可以被模板使用
+          return { name, printLog, reset }
+        },
+      }).mount('#app')
+    </script>
+  </body>
+</html>

这是一个基于 Vue 3 组合式 API 语法的 demo ,它包含了两个主要功能:

  1. 可以在输入框修改输入内容,上方的 Hello World! 以及浏览器控制台的 Log 输出,都会随着输入框内容的变更而变化
  2. 可以点击 “重置” 按钮,响应式变量被重新赋值的时候,输入框的内容也会一起变化为新的值

这是 Vue 的特色之一:数据的双向绑定。

对比普通的 HTML 文件需要通过输入框的 oninput 事件手动编写视图的更新逻辑, Vue 的双向绑定功能大幅度减少了开发过程的编码量。

在未接触 Vue 这种编程方式之前,相信大部分人首先想到的是直接操作 DOM 来实现需求,为了更好的进行对比,接下来用原生 JavaScript 实现一次相同的功能:

html
<!-- 这是使用原生 JavaScript 实现的 demo -->
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Hello World</title>
+  </head>
+  <body>
+    <div id="app">
+      <!-- 通过一个 `span` 标签来指定要渲染数据的位置 -->
+      <p>Hello <span id="name"></span>!</p>
+
+      <!-- 通过 `oninput` 给输入框绑定输入事件 -->
+      <input
+        id="input"
+        type="text"
+        placeholder="输入名称打招呼"
+        oninput="handleInput()"
+      />
+
+      <!-- 通过 `onclick` 给按钮绑定点击事件 -->
+      <button onclick="reset()">重置</button>
+    </div>
+
+    <script>
+      // 默认值
+      const DEFAULT_NAME = 'World'
+
+      // 要操作的 DOM 元素
+      const nameElement = document.querySelector('#name')
+      const inputElement = document.querySelector('#input')
+
+      // 处理输入
+      function handleInput() {
+        const name = inputElement.value
+        nameElement.innerText = name
+        printLog()
+      }
+
+      // 打印输入框的值到控制台
+      function printLog() {
+        const name = inputElement.value
+        console.log(name)
+      }
+
+      // 重置 DOM 元素的文本和输入框的值
+      function reset() {
+        nameElement.innerText = DEFAULT_NAME
+        inputElement.value = DEFAULT_NAME
+        printLog()
+      }
+
+      // 执行一次初始化,赋予 DOM 元素默认文本和输入框的默认值
+      window.addEventListener('load', reset)
+    </script>
+  </body>
+</html>

虽然两个方案总的代码量相差不大,但可以看到两者的明显区别:

  1. Vue 只需要对一个 name 变量的进行赋值操作,就可以轻松实现视图的同步更新
  2. 使用原生 JavaScript 则需要频繁的操作 DOM 才能达到输入内容即时体现在文本 DOM 上面,并且还要考虑 DOM 是否已渲染完毕,否则操作会出错

Vue 的这种编程方式,称之为 “数据驱动” 编程。

如果在一个页面上频繁且大量的操作真实 DOM ,频繁的触发浏览器回流( Reflow )与重绘( Repaint ),会带来很大的性能开销,从而造成页面卡顿,在大型项目的性能上很是致命。

而 Vue 则是通过操作虚拟 DOM ( Virtual DOM ,简称 VDOM ),每一次数据更新都通过 Diff 算法找出需要更新的节点,只更新对应的虚拟 DOM ,再去映射到真实 DOM 上面渲染,以此避免频繁或大量的操作真实 DOM 。

TIP

虚拟 DOM 是一种编程概念,是指将原本应该是真实 DOM 元素的 UI 界面,用数据结构来组织起完整的 DOM 结构,再同步给真实 DOM 渲染,减少浏览器的回流与重绘。

在 JavaScript 里,虚拟 DOM 的表现是一个 Object 对象,其中需要包含指定的属性(例如 Vue 的虚拟 DOM 需要用 type 来指定当前标签是一个 <div /> 还是 <span /> ),然后框架会根据对象的属性去转换为 DOM 结构并最终完成内容的显示。

更多关于 Vue 虚拟 DOM 和性能优化可以查看官网的 渲染机制 一章进行了解。

Vue 3.0 版本还引入了组合式 API 的概念,更符合软件工程 “高内聚,低耦合” 的思想,让开发者可以更灵活的管理自己的逻辑代码,更方便的进行抽离封装再复用,不管是大型项目还是流水线业务,开箱即用的逻辑代码都是提升开发效率的利器。

Vue 与工程化之间的关联

在已经对 Vue 做了初步了解之后,可能有读者会问:“既然 Vue 的使用方式也非常简单,可以像 jQuery 这些经典类库一样在 HTML 引入使用,那么 Vue 和工程化有什么关联呢?”

Vue.js 是一个框架,框架除了简化编码过程中的复杂度之外,面对不同的业务需求还提供了通用的解决方案,而这些解决方案,通常是将前端工程化里的很多种技术栈组合起来串成一条条技术链,一环扣一环,串起来就是一个完整的工程化项目。

举一个常见的例子,比如上一节内容 了解 Vue.js 与全新的 3.0 版本 里的 demo 是一个简单的 HTML 页面,如果业务稍微复杂一点,比如区分了 “首页” 、 “列表页” 、 “内容页” 这样涉及到多个页面,传统的开发方案是通过 A 标签跳转到另外一个页面,在跳转期间会产生 “新页面需要重新加载资源、会有短暂白屏” 等情况,用户体验不太好。

Vue 提供了 Vue Router 实现路由功能,利用 History API 实现单页面模式(可在 现代化的开发概念 部分了解区别),在一个 HTML 页面里也可以体验 “页面跳转” 这样的体验,但如果页面很多,所有代码都堆积在一个 HTML 页面里,就很难维护。

借助前端工程化的构建工具,开发者可以编写 .vue 单组件文件,将多个页面的代码根据其功能模块进行划分,可拆分到多个单组件文件里维护并进行合理复用,最终通过构建工具编译再合并,最终生成浏览器能访问的 HTML / CSS / JS 文件,这样的开发过程,用户体验没有影响,但开发体验大大提升。

类似这样一个个业务场景会积少成多,把 Vue 和工程化结合起来,处理问题更高效更简单。

选择 Vue 入门工程化的理由

虽然前端的流行框架有主流的 Angular 、 React 和 Vue ,也有新兴的 Svelte 等等,每一个框架都有自己的特色,那为什么建议选择 Vue 来入门工程化呢?

最主要的两个原因是:

  1. 职场对 Vue 技术栈的需求量大,容易找工作
  2. 上手门槛低,会一些基础的 HTML / CSS / JavaScript 语法知识,就能够轻松上手 Vue 的组件开发

第一个原因在 求职竞争上的优势 已有进行过说明,掌握一门流行框架已经是前端岗位必备的技能,几乎所有公司在招聘前端工程师的时候都要求会 Vue 。

这里主要讲讲第二个原因,在 Vue 与工程化之间的关联 里提到了开发者可以编写 .vue 文件,这是一个 Vue 专属的文件扩展名,官方名称是 Single-File Component ,简称 SFC ,也就是单文件组件。

.vue 文件最大的特色就是支持像编写 .html 文件一样,在文件里写 HTML / CSS / JS 代码,不仅结构相似,在代码书写上,两者的语法也是十分接近:

.vue 文件.html 文件
<template /> 部分HTML 代码
<style /> 部分CSS 代码
<script /> 部分JavaScript 代码

下面就是一个最基础的 Vue 组件结构,可以看到和 HTML 文件是非常的相似:

vue
<!-- `template` 对应 HTML 代码 -->
+<template>
+  <div>
+    <!-- 一些 HTML  -->
+  </div>
+</template>
+
+<!-- `script` 部分对应 JavaScript 代码 -->
+<!-- 还支持其他语言,例如 `lang="ts"` 代表当前使用 TypeScript 编写 -->
+<script>
+export default {
+  // 这里是变量、函数等逻辑代码
+}
+</script>
+
+<!-- `style` 部分对应 CSS 代码 -->
+<!-- 还支持开启 `scoped` 标识,让 CSS 代码仅对当前组件生效,不会全局污染 -->
+<style scoped>
+/* 一些 CSS 代码 */
+</style>

Vue 组件不仅支持这些语言的所有基础用法,还增加了非常多更高效的功能,在后面 Vue3 教程的 单组件的编写 一章会有详细的介绍。

现代化的开发概念

在本章最开始的时候提到了 SPA / SSR / SSG 等词汇,这些词汇是一些现代前端工程化开发的概念名词缩写,代表着不同的开发模式和用户体验。

当下主流的前端框架都提供了这些开发模式的支持,因此在学习前端工程化和 Vue 开发的过程中,会不定期的看到这一类词汇,在实际工作业务的技术选型时,面对不同的业务场景也要考虑好需要使用什么样的开发模式,提前了解这些概念,对以后的工作也会很有帮助。

MPA 与 SPA

首先来看 MPA 与 SPA ,这代表着两个完全相反的开发模式和用户体验,它们的全称和中文含义如下:

名词全称中文
MPAMulti-Page Application多页面应用
SPASingle-Page Application单页面应用

多页面应用

MPA 多页面应用是最传统的网站体验,当一个网站有多个页面时,会对应有多个实际存在的 HTML 文件,访问每一个页面都需要经历一次完整的页面请求过程:

bash
# 传统的页面跳转过程
+
+从用户点击跳转开始:
+---> 浏览器打开新的页面
+---> 请求【所有】资源
+---> 加载 HTML 、CSS  JS  图片等资源
+---> 完成新页面的渲染
MPA 的优点

作为最传统也是最被广泛运用的模式,自然有它的优势存在:

  • 首屏加载速度快

因为 MPA 的页面源码都是实实在在的写在 HTML 文件里,所以当 HTML 文件被访问成功,内容也就随即呈现(在不考虑额外的 CSS 、 图片加载速度的情况下,这种模式的内容呈现速度是最快的)。

  • SEO 友好,容易被搜索引擎收录

如果读者有稍微了解过一些 SEO 知识,会知道除了网页的 TKD 三要素之外,网页的内容也影响收录的关键因素,传统的多页面应用,网页的内容都是直接位于 HTML 文件内,例如下面这个有很多内容的网页:

右键查看该网页的源代码,可以看到网页内容对应的 HTML 结构也是包含在 .html 文件里。

TIP

网页的 TKD 三要素是指一个网页的三个关键信息,含义如下:

T ,指 Title ,网站的标题,即网页的 <title>网站的标题</title> 标签。

K ,指 Keywords ,网站的关键词,即网页的 <meta name="Keywords" content="关键词1,关键词2,关键词3" /> 标签。

D ,指 Description ,网站的描述,即网页的 <meta name="description" content="网站的描述" /> 标签。

这三个要素标签都位于 HTML 文件的 <head /> 标签内。

  • 容易与服务端语言结合

由于传统的页面都是由服务端直出,所以可以使用 PHP 、 JSP 、 ASP 、 Python 等非前端语言或技术栈来编写页面模板,最终输出 HTML 页面到浏览器访问。

MPA 的缺点

说完 MPA 的优点,再来看看它的缺点,正因为有这些缺点的存在,才会催生出其他更优秀的开发模式出现。

  • 页面之间的跳转访问速度慢

正如它的访问流程,每一次页面访问都需要完整的经历一次渲染过程,哪怕从详情页 A 的 “相关阅读” 跳转到详情页 B ,这种网页结构一样,只有内容不同的两个页面,也需要经历这样的过程。

  • 用户体验不够友好

如果网页上的资源较多或者网速不好,这个过程就会有明显的卡顿或者布局错乱,影响用户体验。

  • 开发成本高

传统的多页面模式缺少前端工程化的很多优秀技术栈支持,前端开发者在刀耕火种的开发过程中效率低下。如果是基于 PHP 等非前端语言开发,工作量通常更是压在一名开发者身上,无法做到前后端分离来利用好跨岗位协作。

TIP

此处列举的多页面应用问题均指传统开发模式下的多页面,之所以特地说明,是因为后文还会有新的技术栈来实现多页面应用,但实现原理和体验并不一样。

单页面应用

正因为传统的多页面应用存在了很多无法解决的开发问题和用户体验问题,催生了现代化的 SPA 单页面应用技术的诞生。

SPA 单页面应用是现代化的网站体验,与 MPA 相反,不论站点内有多少个页面,在 SPA 项目实际上只有一个 HTML 文件,也就是 index.html 首页文件。

它只有第一次访问的时候才需要经历一次完整的页面请求过程,之后的每个内部跳转或者数据更新操作,都是通过 AJAX 技术来获取需要呈现的内容并只更新指定的网页位置。

TIP

AJAX 技术( Asynchronous JavaScript and XML )是指在不离开页面的情况下,通过 JavaScript 发出 HTTP 请求,让网页通过增量更新的方式呈现给用户界面,而不需要刷新整个页面来重新加载,是一种 “无刷体验” 。

SPA 在页面跳转的时候,地址栏也会发生变化,主要有以下两种方式:

  1. 通过修改 Location:hash 修改 URL 的 Hash 值(也就是 # 号后面部分),例如从 https://example.com/#/foo 变成 https://example.com/#/bar
  2. 通过 History API 的 pushState 方法更新 URL ,例如从 https://example.com/foo 变成 https://example.com/bar

这两个方式的共同特点是更新地址栏 URL 的时候,均不会刷新页面,只是单纯的变更地址栏的访问地址,而网页的内容则通过 AJAX 更新,配合起来就形成了一种网页的 “前进 / 后退” 等行为效果。

TIP

Vue Router 默认提供了这两种 URL 改变方式的支持,分别是 createWebHashHistory 的 Hash 模式和 createWebHistory 对应的 History 模式,在 路由的使用 一章可以学习更多 Vue 路由的使用。

理解了实现原理之后,可以把 SPA 的请求过程简化为如下步骤:

bash
# SPA 页面跳转过程
+
+从用户点击跳转开始:
+---> 浏览器通过 `pushState` 等方法更新 URL
+---> 请求接口数据(如果有涉及到前后端交互)
+---> 通过 JavaScript 处理数据,拼接 HTML 片段
+--->  HTML 片段渲染到指定位置,完成页面的 “刷新”
SPA 的优点

从上面的实现原理已经能总结出它的优势了:

  • 只有一次完全请求的等待时间(首屏加载)
  • 用户体验好,内部跳转的时候可以实现 “无刷切换”
  • 因为不需要重新请求整个页面,所以切换页面的时候速度更快
  • 因为没有脱离当前页面,所以 “页” 与 “页” 之间在切换过程中支持动画效果
  • 脱离了页面跳页面的框架,让整个网站形成一个 Web App ,更接近原生 App 的访问体验
  • 开发效率高,前后端分离,后端负责 API 接口,前端负责界面和联调,同步进行缩短工期

这也是为什么短短几年时间, SPA 的体验模式成为前端领域的主流。

SPA 的缺点

虽然 SPA 应用在使用过程中的用户体验非常好,但也有自身的缺点存在:

  • 首屏加载相对较慢

由于 SPA 应用的路由是由前端控制, SPA 在打开首页后,还要根据当前的路由再执行一次内容渲染,相对于 MPA 应用从服务端直出 HTML ,首屏渲染所花费的时间会更长。

  • 不利于 SEO 优化

由于 SPA 应用全程是由 JavaScript 控制内容的渲染,因此唯一的一个 HTML 页面 index.html 通常是一个空的页面,只有最基础的 HTML 结构,不仅无法设置每个路由页面的 TDK ,页面内容也无法呈现在 HTML 代码里,因此对搜索引擎来说,网站的内容再丰富,依然只是一个 “空壳” ,无法让搜索引擎进行内容爬取。

为了减少用户等待过程中的焦虑感,可以通过增加 Loading 过程,或者 Skeleton 骨架屏等优化方案,但其实也是治标不治本,因此为了结合 SPA 和 MPA 的优点,又进一步催生出了更多实用的技术方案以适配更多的业务场景,在后面的小节将逐一介绍。

CSR 与 SSR

在了解了 MPA 与 SPA 之后,先了解另外两个有相关联的名词: CSR 与 SSR ,同样的,这一对也是代表着相反的开发模式和用户体验,它们的全称和中文含义如下:

名词全称中文
CSRClient-Side Rendering客户端渲染
SSRServer-Side Rendering服务端渲染

正如它们的名称,这两者代表的是渲染网页过程中使用到的技术栈。

客户端渲染

MPA 多页面应用与 SPA 单页面应用 部分的介绍过的 SPA 单页面应用,正是基于 CSR 客户端渲染实现的(因此大部分情况下, CSR 等同于 SPA ,包括实现原理和优势),这是一种利用 AJAX 技术,把渲染工作从服务端转移到客户端完成,不仅客户端的用户体验更好,前后端分离的开发模式更加高效。

但随之而来的首屏加载较慢、不利于 SEO 优化等缺点,而 SPA 的这几个缺点,却是传统 MPA 多页面应用所具备的优势,但同样的, MPA 也有着自己开发成本高、用户体验差等问题。

既然原来的技术方案无法完美满足项目需求,因此在结合 MPA 的优点和 SPA 的优点之后,一种新的技术随之诞生,这就是 SSR 服务端渲染。

服务端渲染

和传统的 MPA 使用 PHP / JSP 等技术栈做服务端渲染不同,现代前端工程化里的 SSR 通常是指使用 Node.js 作为服务端技术栈。

TIP

工程化神器 Node.js 一节会介绍 Node ,以及它对前端工程化带来的重大变化,现代前端工程化发展离不开它的存在。

传统的服务端渲染通常由后端开发者一起维护前后端代码,需要写后端语言支持的模板、 JavaScript 代码维护成本也比较高;而 SSR 服务端渲染则是交给前端开发者来维护,利用 Node 提供的能力进行同构渲染,由于本身前后端都使用 JavaScript 编写,维护成本也大大的降低。

SSR 技术利用的同构渲染方案( Isomorphic Rendering ),指的是一套代码不仅可以在客户端运行,也可以在服务端运行,在一些合适的时机先由服务端完成渲染( Server-Side Rendering )再直出给客户端激活( Client-Side Hydration ),这种开发模式带来了:

  • 更好的 SEO 支持,解决了 SPA 单页面应用的痛点
  • 更快的首屏加载速度,保持了 MPA 多页面应用的优点
  • 和 SPA 一样支持前后端分离,开发效率依然很高
  • 有更好的客户端体验,当用户完全打开页面后,本地访问过程中也可以保持 SPA 单页面应用的体验
  • 统一的心智模型,由于支持同构,因此没有额外的心智负担

那么,使用 Vue 开发项目时,应该如何实现 SSR 呢?

Vue 的 SSR 支持非常好, Vue 官方不仅提供了一个 Vue.js 服务器端渲染指南 介绍了基于 Vue 的 SSR 入门实践,还有基于 Vue 的 Nuxt.jsQuasar 框架帮助开发者更简单地落地 SSR 开发,构建工具 Vite 也有内置的 Vue SSR 支持。

Pre-Rendering 与 SSG

在介绍了 SSR 服务端渲染技术后,读者可能会想到一个问题,就是 SSR 的开发成本总归比较高,如果本身项目比较简单,例如一个静态博客,或者静态官网、落地页等内容不多,仅需要简单的 SEO 支持的项目需求,是否有更简便的方案呢?

以下两种方案正是用于满足这类需求的技术:

名词全称中文
Pre-RenderingPre-Rendering预渲染
SSGStatic-Site Generation静态站点生成

预渲染

预渲染也是一种可以让 SPA 单页面应用 解决 SEO 问题的技术手段。

预渲染的原理是在构建的时候启动无头浏览器( Headless Browser ),加载页面的路由并将访问结果按照路由的路径保存到静态 HTML 文件里,这样部署到服务端的页面,不再是一个空的 HTML 页面,而是有真实内容的存在,但由于只在构建时运行,因此用户每次访问的时候 HTML 里的内容不会产生变化,直到下一次构建。

TIP

无头浏览器( Headless Browser ),指没有 GUI 界面的浏览器,使用代码通过编程接口来控制浏览器的行为,常用于网络爬虫、自动化测试等场景,预渲染也使用它来完成页面的渲染,以获取渲染后的代码来填充 HTML 文件。

预渲染和 服务端渲染 最大的区别在于,预渲染只在构建的时候就完成了页面内容的输出(发生在用户请求前),因此构建后不论用户何时访问, HTML 文件里的内容都是构建的时候的那份内容,所以预渲染适合一些简单的、有一定的 SEO 要求但对内容更新频率没有太高要求、内容多为静态展示的页面。

例如企业用于宣传的官网页面、营销活动的推广落地页都非常适合使用预渲染技术,现代的构建工具都提供了预渲染的内置实现,例如这个教程: 用 Vite 更简单的解决 Vue3 项目的预渲染问题 ,就是通过 Vite 的内置功能来实现预渲染,最终也运用到了公司的业务上。

静态站点生成

SSG 静态站点生成是基于预渲染技术,通过开放简单的 API 和配置文件,就让开发者可以实现一个预渲染静态站点的技术方案。

它可以让开发者定制站点的个性化渲染方案,但更多情况下,通常是作为一些开箱即用的技术产品来简化开发过程中的繁琐步骤,这一类技术产品通常称之为静态站点生成器( Static-Site Generator ,也是简称 SSG )。

常见的 SSG 静态站点生成器有:基于 Vue 技术的 VuePressVitePress ,自带了 Vue 组件的支持,还有基于 React 的 Docusaurus ,以及很多各有特色的生成器,例如 JekyllHugo 等等。

如果有写技术文档或者博客等内容创作需求,使用静态站点生成器是一个非常方便的选择,通常这一类产品还有非常多的个性化主题可以使用。

ISR 与 DPR

在现代化的开发概念这一节,从 MPA 多页面应用到 SPA 单页面应用 ,再到 CSR 客户端渲染和 SSR 服务端渲染 ,以及 Pre-Rendering 预渲染与 SSG 静态站点生成 ,似乎已经把所有常见的开发场景覆盖完了。

那接下来要讲的 ISR 和 DPR 又是什么用途的技术方案呢?先看看它们的全称和中文含义:

名词全称中文
ISRIncremental Site Rendering增量式的网站渲染
DPRDistributed Persistent Rendering分布式的持续渲染

当网站的内容体量达到一定程度的时候,从头开始构建进行预渲染所花费的时间会非常久,而实际上并不是所有页面的内容都需要更新,这两项技术的推出是为了提升大型项目的渲染效率。

ISR 增量式的网站渲染,通过区分 “关键页面” 和 “非关键页面” 进行构建,优先预渲染 “关键页面” 以保证内容的最新和正确,同时缓存到 CDN ,而 “非关键页面” 则交给用户访问的时候再执行 CSR 客户端渲染,并触发异步的预渲染缓存到 CDN 。

这样做的好处是,大幅度的提升了每次构建的时间,但由于只保证部分 “关键页面” 的构建和内容正确,所以访问 “非关键页面” 的时候,有可能先看到旧的内容,再由 CSR 刷新为新的内容,会丢失一部分用户体验。

更多 ISR 技术细节可以阅读 Netlify 的开发者体验总监 Cassidy Williams 的一篇文章: Incremental Static Regeneration: Its Benefits and Its Flaws

DPR 分布式的持续渲染则是为了解决 ISR 方案下可能访问到旧内容的问题,这也是由 Cassidy Williams 发起的一个提案,详情可在 GitHub 查看:Distributed Persistent Rendering (DPR)

由于目前这两项技术还在发展初期,能够支持的框架和服务还比较少,在这里建议作为一种技术知识储备提前了解,在未来的某一天有业务需要的时候,也可以知道有这样的方案可以解决问题。

工程化不止于前端

现代化的开发概念 部分所讲述的都是关于网页开发的变化,当然,前端这个岗位本身就是从页面开发发展起来的,自然还是离不开网页这个老本行。

但随着前端工程化的发展,前端越来越不止于写前端,已经有很多前端工程师利用前端工程化带来的优势,不仅仅只是做一个 Web 前端,开始逐步发展为一个全栈工程师,在企业内部承担起了更多的岗位职责,包括作者笔者也是。

之所以能做这么多事情,得益于 Node.js 在前端开发带来的翻天覆地的变化,可以在保持原有的 JavaScript 和 TypeScript 基础上,几乎没有过多的学习成本就可以过度到其他端的开发。

在了解 Node.js 之前,先来看看现在的前端开发工程师除了写 Web 前端,还可以做到哪些岗位的工作。

服务端开发

在传统的认知里,如果一个前端工程师想自己搭建一个服务端项目,需要学习 Java 、 PHP 、 Go 等后端语言,还需要学习 Nginx 、 Apache 等 Web Server 程序的使用,并使用这些技术来开发并部署一个项目的服务端。

现在的前端工程师可以利用 Node.js ,单纯使用 JavaScript 或者 TypeScript 来开发一个基于 Node 的服务端项目。

Node 本身是一个 JavaScript 的运行时,还提供了 HTTP 模块 可以启动一个本地 HTTP 服务,如果把 Node 项目部署到服务器上,就可以运行一个可对外访问的公网服务。

但 Node 的原生服务端开发成本比较高,因此在 GitHub 开源社区也诞生了很多更方便的、开箱即用、功能全面的服务端框架,根据它们的特点,可以简单归类如下:

ExpressKoaFastify 为代表的轻量级服务端框架,这一类框架的特点是 “短平快” ,对于服务端需求不高,只是跑一些小项目的话,开箱即用非常地方便,比如 Build 了一个 Vue 项目,然后提供一个读取静态目录的服务来访问它。

但是 “短平快” 框架带来了一些团队协作上的弊端,如果缺少一些架构设计的能力,很容易把一个服务端搭的很乱以至于难以维护,比如项目的目录结构、代码的分层设计等等,每个创建项目的人都有自己的想法和个人喜好,就很难做到统一管理。

因此在这些框架的基础上,又诞生了以 Nest (底层基于 Express ,可切换为 Fastify )、 Egg (基于 Koa )为代表的基于 MVC 架构的企业级服务端框架,这一类框架的特点是基于底层服务进行了更进一步的架构设计并实现了代码分层,还自带了很多开箱即用的 Building Blocks ,例如 TypeORM 、WebSockets 、Swagger 等等,同样也是开箱即用,对大型项目的开发更加友好。

TIP

当然, Node.js 所做的事情是解决服务端程序部分的工作,如果涉及到数据存储的需求,学习 MySQL 和 Redis 的技术知识还是必不可少的!

App 开发

常规的 Native App 原生开发需要配备两条技术线的支持:使用 Java / Kotlin 语言开发 Android 版本,使用 Objective-C / Swift 语言开发 iOS 版本,这对于创业团队或者个人开发者来说都是一个比较高的开发成本。

前端开发者在项目组里对 App 的作用通常是做一些活动页面、工具页面内嵌到 App 的 WebView 里,如果是在一些产品比较少的团队里,例如只有一个 App 产品,那么前端的存在感会比较低。

而 Hybrid App 的出现,使得前端开发者也可以使用 JavaScript / TypeScript 来编写混合 App ,只需要了解简单的打包知识,就可以参与到一个 App 的开发工作中。

开发 Hybrid App 的过程通常称为混合开发,最大的特色就是一套代码可以运行到多个平台,这是因为整个 App 只有一个基座,里面的 App 页面都是使用 UI WebView 来渲染的 Web 界面,因此混合开发的开发成本相对于原生开发是非常低的,通常只需要一个人 / 一个小团队就可以输出双平台的 App ,并且整个 App 的开发周期也会更短。

在用户体验方面, Hybrid App 相对于 Native App ,一样可以做到:

  • 双平台的体验一致性
  • 支持热更新,无需用户重新下载整个 App
  • 内置的 WebView 在交互体验上也可以做到和系统交互,比如读取 / 存储照片、通讯录,获取定位等等
  • 支持 App Push 系统通知推送
  • 还有很多 Native App 具备的功能

基本上 Native App 的常见功能,在 Hybrid App 都能满足。

而且大部分情况下,在构建 Hybrid App 的时候还可以顺带输出一个 Web App 版本,也就是让这个 App 在被用户下载前,也有一模一样的网页版可以体验,这对于吸引新用户是非常有用的。

在混合开发的过程中,通常是由前端开发者来负责 App 项目从 “开发” 到 “打包” 再到 “发版” 的整个流程,在开发的过程中是使用常见的前端技术栈,例如目前主流的有基于 Vue 的 uni-app 、基于 React 的 React Native 等等,这些 Hybrid 框架都具备了 “学习成本低、开发成本低、一套代码编译多个平台” 的特点。

在 App 开发完毕后,使用 Hybrid 框架提供的 CLI 工具编译出 App 资源包,再根据框架提供的原生基座打包教程去完成 Android / iOS 的安装包构建,这个环节会涉及到原生开发的知识,例如 Android 包的构建会使用到 Android Studio ,但整个过程使用到原生开发的环节非常少,几乎没有太高的学习门槛。

桌面程序开发

放在以前要开发一个 Windows 桌面程序,需要用上 QT / WPF / WinForm 等技术栈,还要学习 C++ / C# 之类的语言,对于只想在业余写几个小工具的开发者来说,上手难度和学习成本都很高,但在前端工程化的时代里,使用 JavaScript 或 TypeScript 也可以满足程序开发的需要。

这得益于 Electron / Tauri 等技术栈的出现,其中 Electron 的成熟度最高、生态最完善、最被广泛使用,除了可以构建 Windows 平台支持的 .exe 文件之外,对 macOS 和 Linux 平台也提供了对应的文件构建支持。

广大前端开发者每天都在使用的 Visual Studio Code 以及知名的 HTTP 网络测试工具 Postman 都是使用 Electron 开发的。

笔者也通过 Electron 构建了多个给公司内部使用的界面化工具客户端,这一类技术栈对于前端开发者来说,真的非常方便!在这里以 Electron 为例,简单讲解下它的工作原理,以了解为什么程序开发可以如此简单。

Electron 的底层是基于 Chromium 和 Node.js ,它提供了两个进程供开发者使用:

  1. 主进程:它是整个应用的入口点,主进程运行在 Node 环境中,可以使用所有的 Node API ,程序也因此具备了和系统进行交互的能力,例如文件的读写操作。

  2. 渲染进程:负责与用户交互的 GUI 界面,基于 Chromium 运行,所以开发者得以使用 HTML / CSS / JavaScript 像编写网页一样来编写程序的 GUI 界面。

一个程序应用只会有一个主进程,而渲染进程则可以根据实际需求创建多个,渲染进程如果需要和系统交互,则必须与主进程通信,借助主进程的能力来实现。

在构建的时候, Electron 会把 Node 和 Chromium 一起打包为一个诸如 .exe 这样的安装文件(或者是包含了两者的免安装版本),这样用户不需要 Node 环境也可以运行桌面程序。

应用脚本开发

桌面程序开发 部分讲的是构建一种拥有可视化 GUI 界面的程序,但有时候并不需要复杂的 GUI ,可能只想提供一个双击运行的脚本类程序给用户,现在的前端工程化也支持使用 JavaScript 构建一个无界面的应用脚本。

假如某一天公司的运营小姐姐希望能做一个自动化的脚本减轻她们的机械操作,或者是自己工作过程中发现一些日常工作可以交付给脚本解决的情况,就可以使用这种方式来输出一个脚本程序,使用的时候双击运行非常方便。

笔者之前为了让团队的工程师减少写日报的心智负担,也是使用了这个方式编写了一个 git-commit-analytics 工具,部门里的工程师可以通过规范化 commit 来生成每天的工作日报,每天双击一下就可以生成一份报告,很受团队的喜欢。

在这里推荐一个工具 Pkg ,它可以把 Node 项目打包为一个可执行文件,支持 Windows 、 macOS 、 Linux 等多个平台,它的打包机制和 Electron 打包的思路类似,也是通过把 Node 一起打包,让用户可以在不安装 Node 环境的情况下也可以直接运行脚本程序。

实践工程化的流程

基于 Vue 3 的项目,最主流的工程化组合拳有以下两种:

常用方案Runtime构建工具前端框架
方案一NodeWebpackVue
方案二NodeViteVue

方案一是比较传统并且过去项目使用最多的方案组合,但从 2021 年初随着 Vite 2.0 的发布,伴随着更快的开发体验和日渐丰富的社区生态,新项目很多都开始迁移到方案二,因此本书秉着面向当下与未来的原则,会侧重 Vite 的使用来开展讲解,包括一些 demo 的创建等等。

当技术成熟的时候,还可以选择更喜欢的方案自行组合,例如用 Deno 来代替 Node ,但前期还是按照主流的方案来进入工程化的学习。

下面的内容将根据 Vue 3 的工程化开发,逐一讲解涉及到常用的工具,了解它们的用途和用法。

工程化神器 Node.js

只要在近几年有接触过前端开发,哪怕没有实际使用过,也应该有听说过 Node.js ,那么它是一个什么样的存在?

什么是 Node.js

Node.js (简称 Node ) 是一个基于 Chrome V8 引擎构建的 JS 运行时( JavaScript Runtime )。

它让 JavaScript 代码不再局限于网页上,还可以跑在客户端、服务端等场景,极大的推动了前端开发的发展,现代的前端开发几乎都离不开 Node 。

什么是 Runtime

Runtime ,可以叫它 “运行时” 或者 “运行时环境” ,这个概念是指,项目的代码在哪里运行,哪里就是运行时。

传统的 JavaScript 只能跑在浏览器上,每个浏览器都为 JS 提供了一个运行时环境,可以简单地把浏览器当成一个 Runtime ,明白了这一点,相信就能明白什么是 Node 。

Node 就是一个让 JS 可以脱离浏览器运行的环境,当然,这里并不是说 Node 就是浏览器。

Node 和浏览器的区别

虽然 Node 也是基于 Chrome V8 引擎构建,但它并不是一个浏览器,它提供了一个完全不一样的运行时环境,没有 Window 、没有 Document 、没有 DOM 、没有 Web API ,没有 UI 界面…

但它提供了很多浏览器做不到的能力,比如和操作系统的交互,例如 “文件读写” 这样的操作在浏览器有诸多的限制,而在 Node 则轻轻松松。

对于前端开发者来说, Node 的巨大优势在于,使用一种语言就可以编写所有东西(前端和后端),不再花费很多精力去学习各种各样的开发语言。

哪怕仅仅只做 Web 开发,也不再需要顾虑新的语言特性在浏览器上的兼容性( e.g. ES6 、 ES7 、 ES8 、 ES9 …), Node 配合构建工具,以及诸如 Babel 这样的代码编译器,可以帮转换为浏览器兼容性最高的 ES5 。

当然还有很多工程化方面的好处,总之一句话,使用 Node 的开发体验会非常好。

工程化的入门准备 一章中,会对 Node 开发做进一步的讲解,下面先继续顺着 Node 的工具链,了解与日常开发息息相关的前端构建工具。

工程化的构建工具

在前端开发领域,构建工具已经成为现在必不可少的开发工具了,很多刚接触前端工程化的开发者可能会有疑惑,为什么以前的前端页面直接编写代码就可以在浏览器访问,现在却还要进行构建编译,是否 “多此一举” ?

要消除这些困惑,就需要了解一下为什么要使用构建工具,知道构建工具在开发上能够给带来什么好处。

为什么要使用构建工具

目前已经有很多流行的构建工具,例如: GruntGulpWebpackSnowpackParcelRollupVite … 每一个工具都有自己的特色。

如上面列举的构建工具,虽然具体到某一个工具的时候,是 “一个” 工具,但实际上可以理解为是 “一套” 工具链、工具集,构建工具通常集 “语言转换 / 编译” 、 “资源解析” 、 “代码分析” 、 “错误检查” 、 “任务队列” 等非常多的功能于一身。

构建工具可以帮解决很多问题,先看看最基础的一个功能支持: “语言转换 / 编译” 。

且不说构建工具让可以自由自在的在项目里使用 TypeScript 这些新兴的语言,单纯看历史悠久的 JavaScript ,从 2015 年开始,每年也都会有新的版本发布(例如 ES6 对应 ES2015 、 ES7 对应 ES2016 、 ES8 对应 ES2017 等等)。

虽然新版本的 JS API 更便捷更好用,但浏览器可能还没有完全支持,这种情况下可以通过构建工具去转换成兼容度更高的低版本 JS 代码。

举个很常用到的例子,现在判断一个数组是否包含某个值,通常会这么写:

js
// 声明一个数组
+const arr = ['foo', 'bar', 'baz']
+
+// 当数组包含 foo 这个值时,处理一些逻辑
+if (arr.includes('foo')) {
+  // do something…
+}

通过 Array.prototype.includes() 这个实例方法返回的布尔值,判断数组是否包含目标值,而这个方法是从 ES6 开始支持的,对于不支持 ES6 的古董浏览器,只能使用其他更早期的方法代替( e.g. indexOf ),或者手动引入它的 Polyfill 来保证这个方法可用。

TIP

Polyfill 是在浏览器不支持的情况下实现某个功能的代码,可以在概念发明者 Remy Sharp 的博文里了解到它的由来,是一个挺有意思的命名。

点击阅读: What is a Polyfill?

以下是摘选自 MDN 网站上关于 Array.prototype.includes() 的 Polyfill 实现:

js
// https://tc39.github.io/ecma262/#sec-array.prototype.includes
+if (!Array.prototype.includes) {
+  Object.defineProperty(Array.prototype, 'includes', {
+    value: function (valueToFind, fromIndex) {
+      if (this == null) {
+        throw new TypeError('"this" is null or not defined')
+      }
+
+      // 1. Let O be ? ToObject(this value).
+      var o = Object(this)
+
+      // 2. Let len be ? ToLength(? Get(O, "length")).
+      var len = o.length >>> 0
+
+      // 3. If len is 0, return false.
+      if (len === 0) {
+        return false
+      }
+
+      // 4. Let n be ? ToInteger(fromIndex).
+      //    (If fromIndex is undefined, this step produces the value 0.)
+      var n = fromIndex | 0
+
+      // 5. If n ≥ 0, then
+      //  a. Let k be n.
+      // 6. Else n < 0,
+      //  a. Let k be len + n.
+      //  b. If k < 0, let k be 0.
+      var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0)
+
+      function sameValueZero(x, y) {
+        return (
+          x === y ||
+          (typeof x === 'number' &&
+            typeof y === 'number' &&
+            isNaN(x) &&
+            isNaN(y))
+        )
+      }
+
+      // 7. Repeat, while k < len
+      while (k < len) {
+        // a. Let elementK be the result of ? Get(O, ! ToString(k)).
+        // b. If SameValueZero(valueToFind, elementK) is true, return true.
+        if (sameValueZero(o[k], valueToFind)) {
+          return true
+        }
+        // c. Increase k by 1.
+        k++
+      }
+
+      // 8. Return false
+      return false
+    },
+  })
+}

由于 JavaScript 允许更改 prototype ,所以 Polyfill 的原理就是先检查浏览器是否支持某个方法,当浏览器不支持的时候,会借助已经被广泛支持的方法来实现相同的功能,达到在旧浏览器上也可以使用新方法的目的。

下面是一个简单的 includes 方法实现,也借用浏览器支持的 indexOf 方法,让不支持 includes 的浏览器也可以使用 includes

js
// 借助 indexOf 来实现一个简单的 includes
+if (!Array.prototype.includes) {
+  Array.prototype.includes = function (v) {
+    return this.indexOf(v) > -1
+  }
+}

WARNING

请注意,上面这个实现方案很粗糙,没有 Polyfill 的方案考虑的足够周到,只是在这里做一个简单的实现演示。

Polyfill 会考虑到多种异常情况,最大幅度保证浏览器的兼容支持,当然一些复杂的方法实现起来会比较臃肿,全靠人工维护 Polyfill 很不现实。

而且实际的项目里,要用到的 JavaScript 原生方法非常多,不可能手动去维护每一个方法的兼容性,所以这部分工作,通常会让构建工具来自动化完成,常见的方案就有 Babel

除了 “语言转换 / 编译” 这个好处之外,在实际的开发中,构建工具可以更好地提高开发效率、提供自动化的代码检查、规避上线后的生产风险,例如:

  • 项目好多代码可以复用,可以直接抽离成 模块组件 ,交给构建工具去合并打包
  • TypeScript 的类型系统和代码检查真好用,也可以放心写,交给构建工具去编译
  • CSS 写起来很慢,可以使用 Sass 、 Less 等 CSS 预处理器 ,利用它们的变量支持、混合继承等功能提高开发效率,最终交给构建工具去编译回 CSS 代码
  • 海量的 npm 包 开箱即用,剩下的工作交给构建工具去按需抽离与合并
  • 项目上线前代码要混淆,人工处理太费劲,交给构建工具自动化处理
  • 还有很多列举不完的其他场景…

下面基于接下来要学习的 Vue3 技术栈,介绍两个流行且强相关的构建工具: WebpackVite

Webpack

Webpack 是一个老牌的构建工具,前些年可以说几乎所有的项目都是基于 Webpack 构建的,生态最庞大,各种各样的插件最全面,对旧版本的浏览器支持程度也最全面。

点击访问:Webpack 官网

在升级与配置一章里的 使用 @vue/cli 创建项目 会指导如何使用 Vue CLI 创建一个基于 Webpack 的 Vue 项目。

Vite

Vite 的作者也是熟悉的 Vue 作者尤雨溪,它是一个基于 ESM 实现的构建工具,主打更轻、更快的开发体验,主要面向现代浏览器,于 2021 年推出 2.x 版本之后,进入了一个飞速发展的时代,目前市场上的 npm 包基本都对 Vite 做了支持,用来做业务已经没有问题了。

毫秒级的开发服务启动和热重载,对 TypeScript 、 CSS 预处理器等常用开发工具都提供了开箱即用的支持,也兼容海量的 npm 包,如果是先用 Webpack 再用的 Vite ,会很快就喜欢上它!

点击访问:Vite 官网

在升级与配置一章里的 使用 Vite 创建项目 会指导如何使用流行脚手架创建一个基于 Vite 的 Vue 项目。

两者的区别

在开发流程上, Webpack 会先打包,再启动开发服务器,访问开发服务器时,会把打包好的结果直接给过去,下面是 Webpack 使用的 bundler 机制的工作流程。

Vite 是基于浏览器原生的 ES Module ,所以不需要预先打包,而是直接启动开发服务器,请求到对应的模块的时候再进行编译,下面是 Vite 使用的 ESM 机制的工作流程。

所以当项目体积越大的时候,在开发启动速度上, Vite 和 Webpack 的差距会越来越大。

可以点击 Vite 官网的这篇文章: 为什么选 Vite 了解更多的技术细节。

构建方面,为了更好的加载体验,以及 Tree Shaking 按需打包 、懒加载和 Chunk 分割利于缓存,两者都需要进行打包;但由于 Vite 是面向现代浏览器,所以如果项目有兼容低版本浏览器的需求的话,建议还是用 Webpack 来打包,否则, Vite 是目前的更优解。

开发环境和生产环境

在使用构建工具的时候,需要了解一下 “环境” 的概念,对构建工具而言,会有 “开发环境( development )” 和 “生产环境( production )” 之分。

TIP

需要注意的是,这和业务上的 “测试 -> 预发 -> 生产” 那几个环境的概念是不一样的,业务上线流程的这几个环境,对于项目来说,都属于 “生产环境” ,因为需要打包部署。

开发环境

前面在编写 Hello TypeScript 这个 demo 的时候,使用了 npm run dev:ts 这样的命令来测试 TypeScript 代码的可运行性,可以把这个阶段认为是一个 “测试环境” ,这个时候代码不管怎么写,它都是 TypeScript 代码,不是最终要编译出来的 JavaScript 。

如果基于 Webpack 或者 Vite 这样的构建工具,测试环境提供了更多的功能,例如:

  • 可以使用 TypeScript 、 CSS 预处理器之类的需要编译的语言提高开发效率
  • 提供了热重载( Hot Module Replacement , 简称 HMR ),当修改了代码之后,无需重新运行或者刷新页面,构建工具会检测的修改自动帮更新
  • 代码不会压缩,并有 Source Mapping 源码映射,方便 BUG 调试
  • 默认提供局域网服务,无需自己做本地部署
  • 更多 …

生产环境

Hello TypeScript demo 最后配置的一个 npm run build 命令,将 TypeScript 代码编译成了 JavaScript ,这个时候 dist 文件夹下的代码文件就处于 “生产环境” 了,因为之后不论源代码怎么修改,都不会直接影响到它们,直到再次执行 build 编译。

可以看出生产环境和开发环境最大的区别就是稳定!除非再次打包发布,否则不会影响到已部署的代码。

  • 代码会编译为浏览器最兼容的版本,一些不兼容的新语法会进行 Polyfill
  • 稳定,除非重新发布,否则不会影响到已部署的代码
  • 打包的时候代码会进行压缩混淆,缩小项目的体积,也降低源码被直接曝光的风险

环境判断

在 Webpack ,可以使用 process.env.NODE_ENV 来区分开发环境( development )还是生产环境( production ),它会返回当前所处环境的名称。

在 Vite ,还可以通过判断 import.meta.env.DEVtrue 时是开发环境,判断 import.meta.env.PRODtrue 时是生产环境(这两个值永远相反)。

有关环境变量的问题可以查阅以下文档:

工具文档
Webpack模式
Vite环境变量和模式

Released under the MIT License.

+ + + + \ No newline at end of file diff --git a/guide.html b/guide.html new file mode 100644 index 00000000..1da50cee --- /dev/null +++ b/guide.html @@ -0,0 +1,666 @@ + + + + + + 工程化的前期准备 | Vue3 入门指南与实战案例 + + + + + + + + + + + + + +
Skip to content
目录

工程化的前期准备

对于刚刚迈入前端工程化、或者还没有接触过前端工程化的开发者,从传统的用 HTML + CSS + JS 手写页面的认知阶段走到工程化的世界,会面对翻天覆地的变化,需要先学习一些入门准备知识。

这一章会介绍一些前置的知识点科普,方便开始学习 Vue3 的时候,不会对一些基本的认知和操作存在太多疑惑。

TIP

本指南需要具备一定的 HTML 、 CSS 和 JavaScript 基础,如果完全不懂,请先对这三个知识点进行一些入门的学习。

命令行工具

在前端工程化开发过程中,已经离不开各种命令行操作,例如:管理项目依赖、本地服务启动、打包构建,还有拉取代码 / 提交代码这些 Git 操作等等。

命令行界面( Command-line Interface ,缩写 CLI ),是一种通过命令行来实现人机交互的工具,需要提前准备好命令行界面工具。

如果有所留意,会发现很多工具都可以实现命令行操作,比如:命令行界面( CLI )、终端( Terminal )、 Shell 、控制台( Console )等等。

从完整功能看,它们之间确实有许多区别,不过对于前端开发者来说,日常的命令行交互需要用到的功能不会特别多,所以后面会统一一些名词,减少理解上的偏差。

交互行为统一代替名词代替名词解释
输入命令行需要输入命令的时候,会统一用 “命令行” 来指代。
输出控制台鉴于前端开发者更多接触的是浏览器的 Console 控制台,
所以也是会用 “控制台” 来指代。

Windows

在 Windows 平台,可以使用自带的 CMD 或者 Windows PowerShell 工具。

但为了更好的开发体验,推荐使用以下工具(需要下载安装),可以根据自己的喜好选择其一:

名称简介下载
Windows Terminal由微软推出的强大且高效的 Windows 终端前往 GitHub 下载
CMDer一款体验非常好的 Windows 控制台模拟器前往 GitHub 下载

笔者在 Windows 台式机上是使用 Windows Terminal 比较多,在此之前是用 CMDer ,两者的设计和体验都非常优秀,当然,还有颜值。

macOS

如果使用的是 Mac 系统,可以直接使用系统自带的 “终端” 工具,笔者在 MacBook 上是使用自带的终端进行开发。

TIP

其实只要能正常使用命令行,对于前端工程师来说就可以满足日常需求,但选择更喜欢的工具,可以让自己的开发过程更为身心愉悦!

安装 Node.js 环境

安装好命令行工具之后,来安装 Node 的开发环境。

下载和安装 Node

在 Node.js 官网提供了安装包的下载,不论是使用 Windows 系统还是 MacOS 系统, Node 都提供了对应的安装包,直接下载安装包并运行即可安装到的电脑里,就可以用来开发的项目了。

点击访问:Node.js 官网下载

安装后,打开的 命令行工具 ,输入以下命令即可查看是否安装成功:

bash
node -v

如果已成功安装,会在控制台输出当前的 Node 版本号。

版本之间的区别

可以看到官网标注了 LTS 和 Current 两个系列,并且对应了不同的版本号。

Current 版本

Current 是最新发布版本,或者叫 “尝鲜版” ,可以在这个系列体验到最新的功能,但也可能会有一些意想不到的问题和兼容性要处理。

每六个月会发布一次 Current 大版本,新的偶数版本( e.g. v16.x.x )会在每年的 4 月份发布,奇数版本( e.g. v17.x.x )会在每年的 10 月份发布。

也就是说,所有版本都会有 Current 版本阶段,这个阶段会持续 6 个月的时间,期间会被活跃的维护和变更,在发布满 6 个月后,奇偶数版本会有不同的结果:

  • 大版本号是奇数的,将变为不支持状态,不会进入 LTS 版本。
  • 大版本号是偶数的,会按照发布节点进入 LTS ,并且作为活跃状态投入使用。

TIP

除非是狂热的 Node 开发探索者,否则不应该选择 Current 系列(特别是在生产环境),应该选择未被 EOL 的 LTS 系列作为的项目运行环境,详见下方的 LTS 版本 说明。

LTS 版本

LTS ,全称 Long Time Support ,长期维护版本,这个系列代表着稳定,建议首次下载以及后续的每次升级都选择 LTS 版本,减少开发过程中的未知问题出现。

每个 LTS 版本的大版本号都是偶数,并且会有 3 个阶段的生命周期:

生命周期含义说明
Active活跃阶段每个从 Current 进入 LTS 的偶数版本,都会有 18 个月的时间被积极维护和升级。
Maintenance维护阶段活跃阶段达到 18 个月后,会进入为期 12 个月的维护阶段,期间只会进行错误修复和安全补丁。
End of Life结束阶段简称 EOL ,在维护阶段达到期限之后,该版本进入 EOL 阶段,将不再维护,也就是说,每个 LTS 版本最长会有 30 个月的维护时间,之后将不再进行维护。

TIP

当然也会有一些例外情况,例如 Node.js 16 版本,为了配合 OpenSSL 1.1.1 的 EOL 时间,将提前 7 个月进入 EOL 阶段。

详见官方公告: Bringing forward the End-of-Life Date for Node.js 16

是否需要经常更新版本

不论是 LTS 还是 Current ,每个系列下面都还有不同的大版本和小版本,是不是每次都必须及时更新到最新版呢?

当然不是,完全可以依照的项目技术栈依赖的最低 Node 版本去决定是否需要升级,不过如果条件允许,还是建议至少要把大版本升级到最新的 LTS 版本。

TIP

关于 Node.js 的版本发布时间表可以在官方 GitHub 的 Release 仓库 查看。

基础的 Node 项目

在安装和配置完 Node.js 之后,接下来了解 Node 项目的一些基础组成,这有助于开启前端工程化开发大门。

TIP

当前文档所演示的 hello-node 项目已托管至 learning-vue3/hello-node 仓库,可使用 Git 克隆命令拉取至本地:

bash
# 从 GitHub 克隆
+git clone https://github.com/learning-vue3/hello-node.git
+
+# 如果 GitHub 访问失败,可以从 Gitee 克隆
+git clone https://gitee.com/learning-vue3/hello-node.git

成品项目可作为学习过程中的代码参考,但更建议按照教程的讲解步骤,从零开始亲手搭建一个新项目并完成 node 开发的体验,可以更有效的提升学习效果。

初始化一个项目

如果想让一个项目成为 Node 项目,只需要在命令行 cd 到项目所在的目录,执行初始化命令:

bash
npm init

之后命令行会输出一些提示,以及一些问题,可以根据的实际情况填写项目信息,例如:

bash
package name: (demo) hello-node

以上面这个问题为例:

冒号左边的 package name 是问题的题干,会询问要输入什么内容。

冒号右边的括号内容 (demo) 是 Node 为推荐的答案(不一定会出现这个推荐值),如果觉得 OK ,可以直接按回车确认,进入下一道题。

冒号右边的 hello-node 是输入的答案(如果选择了推荐的答案,则这里为空),这个答案会写入到项目信息文件里。

当回答完所有问题之后,会把填写的信息输出到控制台,确认无误后,回车完成初始化的工作。

bash
{
+  "name": "hello-node",
+  "version": "1.0.0",
+  "description": "A demo about Node.js.",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "chengpeiquan",
+  "license": "MIT"
+}
+
+
+Is this OK? (yes)

如果觉得问题太多,太繁琐了,可以直接加上 -y 参数,这样会以 Node 推荐的答案帮快速生成项目信息。

bash
npm init -y

了解 package.json

在完成 项目的初始化 之后,会发现在项目的根目录下出现了一个名为 package.json 的 JSON 文件。

这是 Node 项目的清单,里面记录了这个项目的基础信息、依赖信息、开发过程的脚本行为、发布相关的信息等等,未来将在很多项目里看到它的身影。

TIP

它必须是 JSON 文件,不可以是存储了 JavaScript 对象字面量的 JS 文件。

如果是按照上面初始化一节的操作得到的这个文件,打开它之后,会发现里面存储了在初始化过程中,根据问题确认下来的那些答案,例如:

json
{
+  "name": "hello-node",
+  "version": "1.0.0",
+  "description": "A demo about Node.js.",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "chengpeiquan",
+  "license": "MIT"
+}

package.json 的字段并非全部必填,唯一的要求就是,必须是一个 JSON 文件,所以也可以仅仅写入以下内容:

json
{}

但在实际的项目中,往往需要填写更完善的项目信息,除了手动维护这些信息之外,在安装 npm 包等操作时, Node 也会帮写入数据到这个文件里,来了解一些常用字段的含义:

字段名含义
name项目名称,如果打算发布成 npm 包,它将作为包的名称
version项目版本号,如果打算发布成 npm 包,这个字段是必须的,遵循 语义化版本号 的要求
description项目的描述
keywords关键词,用于在 npm 网站上进行搜索
homepage项目的官网 URL
main项目的入口文件
scripts指定运行脚本的命令缩写,常见的如 npm run build 等命令就在这里配置,详见 脚本命令的配置
author作者信息
license许可证信息,可以选择适当的许可证进行开源
dependencies记录当前项目的生产依赖,安装 npm 包时会自动生成,详见:依赖包和插件
devDependencies记录当前项目的开发依赖,安装 npm 包时会自动生成,详见:依赖包和插件
type配置 Node 对 CJS 和 ESM 的支持

其中最后的 type 字段是涉及到模块规范的支持,它有两个可选值: commonjsmodule ,其默认值为 commonjs

  • 当不设置或者设置为 commonjs 时,扩展名为 .js.cjs 的文件都是 CommonJS 规范的模块,如果要使用 ES Module 规范,需要使用 .mjs 扩展名
  • 当设置为 module 时,扩展名为 .js.mjs 的文件都是 ES Module 规范的模块,如果要使用 CommonJS 规范,需要使用 .cjs 扩展名

关于模块规范可以在 学习模块化设计 一节了解更多。

关于 package.json 的完整的选项可以在 npm Docs 上查阅。

项目名称规则

如果打算发布成 npm 包,它将作为包的名称,可以是普通包名,也可以是范围包的包名。

类型释义例子
范围包具备 @scope/project-name 格式,一般有一系列相关的开发依赖之间会以相同的 scope 进行命名@vue/cli@vue/cli-service 就是一系列相关的范围包
普通包其他命名都属于普通包vuevue-router

包名有一定的书写规则:

  • 名称必须保持在 1 ~ 214 个字符之间(包括范围包的 @scope/ 部分)
  • 只允许使用小写字母、下划线、短横线、数字、小数点(并且只有范围包可以以点或下划线开头)
  • 包名最终成为 URL 、命令行参数或者文件夹名称的一部分,所以名称不能包含任何非 URL 安全字符

TIP

了解这一点有助于在后续工作中,在需要查找技术栈相关包的时候,可以知道如何在 npmjs 上找到它们。

如果打算发布 npm 包,可以通过 npm view <package-name> 命令查询包名是否已存在,如果存在就会返回该包的相关信息。

比如查询 vue 这个包名,会返回它的版本号、许可证、描述等信息:

bash
npm view vue
+
+vue@3.2.33 | MIT | deps: 5 | versions: 372
+The progressive JavaScript framework for building modern web UI.
+https://github.com/vuejs/core/tree/main/packages/vue#readme
+
+keywords: vue
+
+# 后面太多信息这里就省略...

如果查询一个不存在的包名,则会返回 404 信息:

bash
npm view vue123456
+npm ERR! code E404
+npm ERR! 404 Not Found - GET https://registry.npmjs.org/vue123456 - Not found
+npm ERR! 404
+npm ERR! 404  'vue123456@latest' is not in this registry.
+npm ERR! 404 You should bug the author to publish it (or use the name yourself!)
+npm ERR! 404
+npm ERR! 404 Note that you can also install from a
+npm ERR! 404 tarball, folder, http url, or git url.
+
+# 后面太多信息这里就省略...

语义化版本号管理

Node 项目遵循 语义化版本号 的规则,例如 1.0.01.0.11.1.0 这样的版本号,本教材的主角 Vue 也是遵循了语义化版本号的发布规则。

建议开发者在入门前端工程化的时候就应该熟悉这套规则,后续的项目开发中,会使用到很多外部依赖,它们也是使用版本号控制来管理代码的发布,每个版本之间可能会有一些兼容性问题,如果不了解版本号的通用规则,很容易在的开发中带来困扰。

TIP

现在有很多 CI/CD 流水线作业具备了根据 Git 的 Commit 记录来自动升级版本号,它们也是遵循了语义化版本号规则,版本号的语义化在前端工程里有重大的意义。

基本格式与升级规则

版本号的格式为: Major.Minor.Patch (简称 X.Y.Z ),它们的含义和升级规则如下:

英文中文含义
Major主版本号当项目作了大量的变更,与旧版本存在一定的不兼容问题
Minor次版本号做了向下兼容的功能改动或者少量功能更新
Patch修订号修复上一个版本的少量 BUG

一般情况下,三者均为正整数,并且从 0 开始,遵循这三条注意事项:

  • 当主版本号升级时,次版本号和修订号归零
  • 当次版本号升级时,修订号归零,主版本号保持不变
  • 当修订号升级时,主版本号和次版本号保持不变

下面以一些常见的例子帮助快速理解版本号的升级规则:

  • 如果不打算发布,可以默认为 0.0.0 ,代表它并不是一个进入发布状态的包
  • 在正式发布之前,可以将其设置为 0.1.0 发布第一个测试版本,自此,代表已进入发布状态,但还处于初期开发阶段,这个阶段可能经常改变 API ,但不需要频繁的更新主版本号
  • 0.1.0 发布后,修复了 BUG ,下一个版本号将设置为 0.1.1 ,即更新了一个修订号
  • 0.1.1 发布后,有新的功能发布,下一个版本号可以升级为 0.2.0 ,即更新了一个次版本号
  • 当觉得这个项目已经功能稳定、没有什么 BUG 了,决定正式发布并给用户使用时,那么就可以进入了 1.0.0 正式版了

版本标识符

以上是一些常规的版本号升级规则,也可以通过添加 “标识符” 来修饰的版本更新:

格式为: Major.Minor.Patch-Identifier.1 ,其中的 Identifier 代表 “标识符” ,它和版本号之间使用 - 短横线来连接,后面的 .1 代表当前标识符的第几个版本,每发布一次,这个数字 +1 。

标识符含义
alpha内部版本,代表当前可能有很大的变动
beta测试版本,代表版本已开始稳定,但可能会有比较多的问题需要测试和修复
rc即将作为正式版本发布,只需做最后的验证即可发布正式版

脚本命令的配置

在工作中,会频繁接触到 npm run dev 启动开发环境、 npm run build 构建打包等操作,这些操作其实是对命令行的一种别名。

它在 package.json 里是存放于 scripts 字段,以 [key: string]: string 为格式的键值对存放数据( key: value )。

json
{
+  "scripts": {
+    // ...
+  }
+}

其中:

  • key 是命令的缩写,也就是 npm run xxx 里的 xxx ,如果一个单词不足以表达,可以用冒号 : 拼接多个单词,例如 mock:listmock:detail 等等

  • value 是完整的执行命令内容,多个命令操作用 && 连接,例如 git add . && git commit

以 Vue CLI 创建的项目为例,它的项目 package.json 文件里就会包括了这样的命令:

json
{
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build"
+  }
+}

这里的名字是可以自定义的,比如可以把 serve 改成更喜欢的 dev

json
{
+  "scripts": {
+    "dev": "vue-cli-service serve",
+    "build": "vue-cli-service build"
+  }
+}

这样运行 npm run dev 也可以相当于运行了 vue-cli-service serve

据笔者所了解,有不少开发者曾经对不同的 Vue CLI 版本提供的 npm run servenpm run dev 有什么区别有过疑问,看到这里应该都明白了吧,可以说没有区别,因为这取决于它对应的命令,而不是取决于它起什么名称。

TIP

如果 value 部分包含了双引号 " ,必须使用转义符 \ 来避免格式问题,例如: \"

可以阅读 npm 关于 scripts 的 完整文档 了解更多用法。

Hello Node

看到这里,对于 Node 项目的基本创建流程和关键信息都有所了解了吧!来写一个 demo ,实际体验一下如何从初始化项目到打印一个 Hello World 到控制台的过程。

请先启动的命令行工具,然后创建一个项目文件夹,这里使用 mkdir 命令:

bash
# 语法是 mkdir <dir-name>
+mkdir hello-node

使用 cd 命令进入刚刚创建好的项目目录:

bash
# 语法是 cd <dir-path>
+cd hello-node

执行项目初始化,可以回答问题,也可以添加 -y 参数来使用默认配置:

bash
npm init -y

来到这里就得到了一个具有 package.json 的 Node 项目了。

在项目下创建一个 index.js 的 JS 文件,可以像平时一样书写 JavaScript ,输入以下内容并保存:

js
console.log('Hello World')

然后打开 package.json 文件,修改 scripts 部分如下,也就是配置了一个 "dev": "node index" 命令:

json
{
+  "name": "hello-node",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "dev": "node index"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC"
+}

在命令行执行 npm run dev ,可以看到控制台打印出了 Hello World

bash
npm run dev
+
+> demo@1.0.0 dev
+> node index
+
+Hello World

这等价于直接在命令行执行 node index.js 命令,其中 node 是 Node.js 运行文件的命令, index 是文件名,相当于 index.js ,因为 JS 文件名后缀可以省略。

学习模块化设计

在了解 Node 项目之后,就要开始通过编码来加强对 Node.js 的熟悉程度了,但在开始使用之前,还需要了解一些概念。

在未来的日子里(不限于本教程,与前端工程化相关的工作内容息息相关),会频繁的接触到两个词:模块( Module )和包( Package )。

模块和包是 Node 开发最重要的组成部分,不管是全部自己实现一个项目,还是依赖各种第三方轮子来协助开发,项目的构成都离不开这两者。

模块化解决了什么问题

在软件工程的设计原则里,有一个原则叫 “单一职责” 。

假设一个代码块负责了多个职责的功能支持,在后续的迭代过程中,维护成本会极大的增加,虽然只需要修改这个代码块,但需要兼顾职责 1 、职责 2 、职责 3 … 等多个职责的兼容性,稍不注意就会引起工程运行的崩溃。

“单一职责” 的目的就是减少功能维护带来的风险,把代码块的职责单一化,让代码的可维护性更高。

一个完整业务的内部实现,不应该把各种代码都耦合在一起,而应该按照职责去划分好代码块,再进行组合,形成一个 “高内聚,低耦合” 的工程设计。

模块化就是由此而来,在前端工程里,每个单一职责的代码块,就叫做模块( Module ) ,模块有自己的作用域,功能与业务解耦,非常方便复用和移植。

TIP

模块化还可以解决本章开头所讲述的 传统开发的弊端 里提到的大部分问题,随着下面内容一步步深入,将一步步的理解它。

如何实现模块化

在前端工程的发展过程中,不同时期诞生了很多不同的模块化机制,最为主流的有以下几种:

模块化方案全称适用范围
CJSCommonJSNode 端
AMDAsync Module Definition浏览器
CMDCommon Module Definition浏览器
UMDUniversal Module DefinitionNode 端和浏览器
ESMES ModuleNode 端和浏览器

其中 AMD 、CMD 、 UMD 都已经属于偏过去式的模块化方案,在新的业务里,结合各种编译工具,可以直接用最新的 ESM 方案来实现模块化,所以可以在后续有接触的时候再了解。

ESM ( ES Module ) 是 JavaScript 在 ES6( ECMAScript 2015 )版本推出的模块化标准,旨在成为浏览器和服务端通用的模块解决方案。

CJS ( CommonJS ) 原本是服务端的模块化标准(设计之初也叫 ServerJS ),是为 JavaScript 设计的用于浏览器之外的一个模块化方案, Node 默认支持了该规范,在 Node 12 之前也只支持 CJS ,但从 Node 12 开始,已经同时支持 ES Module 的使用。

至此,不论是 Node 端还是浏览器端, ES Module 是统一的模块化标准了!

但由于历史原因, CJS 在 Node 端依然是非常主流的模块化写法,所以还是值得进行了解,因此下面的内容将主要介绍 CJS 和 ESM 这两种模块化规范是如何实际运用。

TIP

在开始体验模块化的编写之前,请先在电脑里 安装好 Node.js ,然后打开 命令行工具 ,通过 cd 命令进入平时管理项目的目录路径, 初始化一个 Node 项目

另外,在 CJS 和 ESM ,一个独立的文件就是一个模块,该文件内部的变量必须通过导出才能被外部访问到,而外部文件想访问这些变量,需要导入对应的模块才能生效。

用 CommonJS 设计模块

虽然现在推荐使用 ES Module 作为模块化标准,但是日后在实际工作的过程中,还是不免会遇到要维护一些老项目,因此了解 CommonJS 还是非常有必要的。

以下简称 CJS 代指 CommonJS 规范。

准备工作

延续在 Hello Node 部分创建的 Node.js demo 项目,先调整一下目录结构:

  1. 删掉 index.js 文件
  2. 创建一个 src 文件夹,在里面再创建一个 cjs 文件夹
  3. cjs 文件夹里面创建两个文件: index.cjsmodule.cjs

TIP

请注意这里使用了 .cjs 文件扩展名,其实它也是 JS 文件,但这个扩展名是 Node 专门为 CommonJS 规范设计的,可以在 了解 package.json 部分的内容了解更多。

此时目录结构应该如下:

bash
hello-node
+ # 源码文件夹
+├─src
+  # 业务文件夹
+ └─cjs
+    # 入口文件
+   ├─index.cjs
+    # 模块文件
+   └─module.cjs
+ # 项目清单
+└─package.json

这是一个常见的 Node 项目目录结构,通常源代码都会放在 src 文件夹里面统一管理。

接下来再修改一下 package.json 里面的 scripts 部分,改成如下:

json
{
+  "scripts": {
+    "dev:cjs": "node src/cjs/index.cjs"
+  }
+}

后面在命令行执行 npm run dev:cjs 命令,就可以测试刚刚添加的 CJS 模块了。

基本语法

CJS 使用 module.exports 语法导出模块,可以导出任意合法的 JavaScript 类型,例如:字符串、布尔值、对象、数组、函数等等。

使用 require 导入模块,在导入的时候,当文件扩展名是 .js 时,可以只写文件名,而此时使用的是 .cjs 扩展名,所以需要完整的书写。

默认导出和导入

默认导出的意思是,一个模块只包含一个值;而导入默认值则意味着,导入时声明的变量名就是对应模块的值。

src/cjs/module.cjs 文件里,写入以下代码,导出一句 Hello World 信息:

js
// src/cjs/module.cjs
+module.exports = 'Hello World'

TIP

自己在写入代码的时候,不需要包含文件路径那句注释,这句注释只是为了方便阅读时能够区分代码属于哪个文件,以下代码均如此。

src/cjs/index.cjs 文件里,写入以下代码,导入刚刚编写的模块。

js
// src/cjs/index.cjs
+const m = require('./module.cjs')
+console.log(m)

在命令行输入 npm run dev:cjs ,可以看到成功输出了 Hello World 信息:

bash
npm run dev:cjs
+
+> demo@1.0.0 dev:cjs
+> node src/cjs/index.cjs
+
+Hello World

可以看到,在导入模块时,声明的 m 变量拿到的值,就是整个模块的内容,可以直接使用,此例子中它是一个字符串。

再改动一下,把 src/cjs/module.cjs 改成如下,这次导出一个函数:

js
// src/cjs/module.cjs
+module.exports = function foo() {
+  console.log('Hello World')
+}

相应的,这次变成了导入一个函数,所以可以执行它:

js
// src/cjs/index.cjs
+const m = require('./module.cjs')
+m()

得到的结果也是打印一句 Hello World ,不同的是,这一次的打印行为是在模块里定义的,入口文件只是执行模块里的函数。

bash
npm run dev:cjs
+
+> demo@1.0.0 dev:cjs
+> node src/cjs/index.cjs
+
+Hello World

命名导出和导入

默认导出的时候,一个模块只包含一个值,有时候如果想把很多相同分类的函数进行模块化集中管理,例如想做一些 utils 类的工具函数文件、或者是维护项目的配置文件,全部使用默认导出的话,会有非常多的文件要维护。

那么就可以用到命名导出,这样既可以导出多个数据,又可以统一在一个文件里维护管理,命名导出是先声明多个变量,然后通过 {} 对象的形式导出。

再来修改一下 src/cjs/module.cjs 文件,这次改成如下:

js
// src/cjs/module.cjs
+function foo() {
+  console.log('Hello World from foo.')
+}
+
+const bar = 'Hello World from bar.'
+
+module.exports = {
+  foo,
+  bar,
+}

这个时候通过原来的方式去拿模块的值,会发现无法直接获取到函数体或者字符串的值,因为打印出来的也是一个对象。

js
// src/cjs/index.cjs
+const m = require('./module.cjs')
+console.log(m)

控制台输出:

bash
npm run dev:cjs
+
+> demo@1.0.0 dev:cjs
+> node src/cjs/index.cjs
+
+{ foo: [Function: foo], bar: 'Hello World from bar.' }

需要通过 m.foo()m.bar 的形式才可以拿到值。

此时可以用一种更方便的方式,利用 ES6 的对象解构来直接拿到变量:

js
// src/cjs/index.cjs
+const { foo, bar } = require('./module.cjs')
+foo()
+console.log(bar)

这样子才可以直接调用变量拿到对应的值。

导入时重命名

以上都是基于非常理想的情况下使用模块,有时候不同的模块之间也会存在相同命名导出的情况,来看看模块化是如何解决这个问题的。

src/cjs/module.cjs 文件保持不变,依然导出这两个变量:

js
// src/cjs/module.cjs
+function foo() {
+  console.log('Hello World from foo.')
+}
+
+const bar = 'Hello World from bar.'
+
+module.exports = {
+  foo,
+  bar,
+}

这次在入口文件里也声明一个 foo 变量,在导入的时候对模块里的 foo 进行了重命名操作。

js
// src/cjs/index.cjs
+const {
+  foo: foo2,  // 这里进行了重命名
+  bar,
+} = require('./module.cjs')
+
+// 就不会造成变量冲突
+const foo = 1
+console.log(foo)
+
+// 用新的命名来调用模块里的方法
+foo2()
+
+// 这个不冲突就可以不必处理
+console.log(bar)

再次运行 npm run dev:cjs ,可以看到打印出来的结果完全符合预期:

bash
npm run dev:cjs
+
+> demo@1.0.0 dev:cjs
+> node src/cjs/index.cjs
+
+1
+Hello World from foo.
+Hello World from bar.

这是利用了 ES6 解构对象的 给新的变量名赋值 技巧。

以上是针对命名导出时的重命名方案,如果是默认导出,那么在导入的时候用一个不冲突的变量名来声明就可以了。

用 ES Module 设计模块

ES Module 是新一代的模块化标准,它是在 ES6( ECMAScript 2015 )版本推出的,是原生 JavaScript 的一部分。

不过因为历史原因,如果要直接在浏览器里使用该方案,在不同的浏览器里会有一定的兼容问题,需要通过 Babel 等方案进行代码的版本转换(可在 控制编译代码的兼容性 一节了解如何使用 Babel )。

因此一般情况下都需要借助构建工具进行开发,工具通常会提供开箱即用的本地服务器用于开发调试,并且最终打包的时候还可以抹平不同浏览器之间的差异。

随着 ESM 的流行,很多新推出的构建工具都默认只支持该方案( e.g. Vite 、 Rollup ),如果需要兼容 CJS 反而需要另外引入插件单独配置。除了构建工具,很多语言也是默认支持 ESM ,例如 TypeScript ,因此了解 ESM 非常重要。

以下简称 ESM 代指 ES Module 规范。

TIP

在阅读本小节之前,建议先阅读 用 CommonJS 设计模块 以了解前置内容,本小节会在适当的内容前后与 CJS 的写法进行对比。

准备工作

继续使用在 用 CommonJS 设计模块 时使用的 hello-node 项目作为 demo ,当然也可以重新创建一个新的。

一样的,先调整一下目录结构:

  1. src 文件夹里面创建一个 esm 文件夹
  2. esm 文件夹里面创建两个 MJS 文件: index.mjsmodule.mjs

TIP

注意这里使用了 .mjs 文件扩展名,因为默认情况下, Node 需要使用该扩展名才会支持 ES Module 规范。

也可以在 package.json 里增加一个 "type": "module" 的字段来使 .js 文件支持 ESM ,但对应的,原来使用 CommonJS 规范的文件需要从 .js 扩展名改为 .cjs 才可以继续使用 CJS 。

为了减少理解上的门槛,这里选择了使用 .mjs 新扩展名便于入门,可以在 了解 package.json 部分的内容了解更多。

此时目录结构应该如下:

bash
hello-node
+ # 源码文件夹
+├─src
+  # 上次用来测试 CommonJS 的相关文件
+ ├─cjs
+  ├─index.cjs
+  └─module.cjs
+ 
+  # 这次要用的 ES Module 测试文件
+ └─esm
+    # 入口文件
+   ├─index.mjs
+    # 模块文件
+   └─module.mjs
+
+ # 项目清单
+└─package.json

同样的,源代码放在 src 文件夹里面管理。

然后再修改一下 package.json 里面的 scripts 部分,参照上次配置 CJS 的格式,增加一个 ESM 版本的 script ,改成如下:

json
{
+  "scripts": {
+    "dev:cjs": "node src/cjs/index.cjs",
+    "dev:esm": "node src/esm/index.mjs"
+  }
+}

后面在命令行执行 npm run dev:esm 就可以测试的 ESM 模块了。

TIP

注意, script 里的 .mjs 扩展名不能省略。

另外,在实际项目中,可能不需要做这些处理,因为很多工作脚手架已经帮处理过了,比如的 Vue3 项目。

基本语法

ESM 使用 export default (默认导出)和 export (命名导出)这两个语法导出模块,和 CJS 一样, ESM 也可以导出任意合法的 JavaScript 类型,例如:字符串、布尔值、对象、数组、函数等等。

使用 import ... from ... 导入模块,在导入的时候,如果文件扩展名是 .js 则可以省略文件名后缀,否则需要把扩展名也完整写出来。

默认导出和导入

ESM 的默认导出也是一个模块只包含一个值,导入时声明的变量名,它对应的数据就是对应模块的值。

src/esm/module.mjs 文件里,写入以下代码,导出一句 Hello World 信息:

js
// src/esm/module.mjs
+export default 'Hello World'

src/esm/index.mjs 文件里,写入以下代码,导入刚刚编写的模块。

js
// src/esm/index.mjs
+import m from './module.mjs'
+console.log(m)

在命令行输入 npm run dev:esm ,可以看到成功输出了 Hello World 信息:

bash
npm run dev:esm
+
+> demo@1.0.0 dev:esm
+> node src/esm/index.mjs
+
+Hello World

可以看到,在导入模块时,声明的 m 变量拿到的值,就是整个模块的内容,可以直接使用,此例子中它是一个字符串。

像在 CJS 的例子里一样,也来再改动一下,把 src/esm/module.mjs 改成导出一个函数:

js
// src/esm/module.mjs
+export default function foo() {
+  console.log('Hello World')
+}

同样的,这次也是变成了导入一个函数,可以执行它:

js
// src/esm/index.mjs
+import m from './module.mjs'
+m()

一样可以从模块里的函数得到一句 Hello World 的打印信息。

bash
npm run dev:esm
+
+> demo@1.0.0 dev:esm
+> node src/esm/index.mjs
+
+Hello World

TIP

可以看到, CJS 和 ESM 的默认导出是非常相似的,在未来如果有老项目需要从 CJS 往 ESM 迁移,大部分情况下只需要把 module.exports 改成 export default 即可。

命名导出和导入

虽然默认导出的时候, CJS 和 ESM 的写法非常相似,但命名导出却完全不同!

在 CJS 里,使用命名导出后的模块数据默认是一个对象,可以导入模块后通过 m.foo 这样的方式去调用对象的属性,或者在导入的时候直接解构拿到对象上的某个属性:

js
// CJS 支持导入的时候直接解构
+const { foo } = require('./module.cjs')

但 ES Module 的默认导出不能这样做,例如下面这个例子,虽然默认导出了一个对象:

js
// 在 ESM ,通过这样导出的数据也是属于默认导出
+export default {
+  foo: 1,
+}

但是无法和 CJS 一样通过大括号的方式导入其中的某个属性:

js
// ESM 无法通过这种方式对默认导出的数据进行 “解构”
+import { foo } from './module.mjs'

这样操作在运行过程中,控制台会抛出错误信息:

bash
import { foo } from './module.mjs'
+         ^^^
+SyntaxError:
+The requested module './module.mjs' does not provide an export named 'foo'

正确的方式应该是通过 export 对数据进行命名导出,先将 src/esm/module.mjs 文件修改成如下代码,请留意 export 关键字的使用:

js
// src/esm/module.mjs
+export function foo() {
+  console.log('Hello World from foo.')
+}
+
+export const bar = 'Hello World from bar.'

通过 export 命名导出的方式,现在才可以使用大括号将它们进行命名导入:

js
// src/esm/index.mjs
+import { foo, bar } from './module.mjs'
+
+foo()
+console.log(bar)

这一次程序可以顺利运行了:

bash
npm run dev:esm
+
+> demo@1.0.0 dev:esm
+> node src/esm/index.mjs
+
+Hello World from foo.
+Hello World from bar.

那么有没有办法像 CJS 一样使用 m.foo 调用对象属性的方式一样,去使用这些命名导出的模块呢?

答案是肯定的!命名导出支持使用 * as 变量名称 的方式将其所有命名挂在某个变量上,该变量是一个对象,每一个导出的命名都是其属性:

ts
// src/esm/index.mjs
+// 注意这里使用了另外一种方式,将所有的命名导出都挂在了 `m` 变量上
+import * as m from './module.mjs'
+
+console.log(typeof m)
+console.log(Object.keys(m))
+
+m.foo()
+console.log(m.bar)

运行 npm run dev:esm ,将输出:

bash
npm run dev:esm
+
+> demo@1.0.0 dev:esm
+> node src/esm/index.mjs
+
+object
+[ 'bar', 'foo' ]
+Hello World from foo.
+Hello World from bar.

导入时重命名

接下来看看 ESM 是如何处理相同命名导出的问题,项目下的模块文件依然保持不变,还是导出两个变量:

js
// src/esm/module.mjs
+export function foo() {
+  console.log('Hello World from foo.')
+}
+
+export const bar = 'Hello World from bar.'

入口文件里面,也声明一个 foo 变量,然后导入的时候对模块里的 foo 进行重命名操作:

js
// src/esm/index.mjs
+import {
+  foo as foo2,  // 这里进行了重命名
+  bar
+} from './module.mjs'
+
+// 就不会造成变量冲突
+const foo = 1
+console.log(foo)
+
+// 用新的命名来调用模块里的方法
+foo2()
+
+// 这个不冲突就可以不必处理
+console.log(bar)

可以看到,在 ESM 的重命名方式和 CJS 是完全不同的,它是使用 as 关键字来操作,语法为 <old-name> as <new-name>

现在再次运行 npm run dev:esm ,可以看到打印出来的结果也是完全符合预期了:

bash
npm run dev:esm
+
+> demo@1.0.0 dev:esm
+> node src/esm/index.mjs
+
+1
+Hello World from foo.
+Hello World from bar.

以上是针对命名导出时的重命名方案,如果是默认导出,和 CJS 一样,在导入的时候用一个不冲突的变量名来声明就可以了。

在浏览器里访问 ESM

ES Module 除了支持在 Node 环境使用,还可以和普通的 JavaScript 代码一样在浏览器里运行。

要在浏览器里体验 ESM ,需要使用现代的主流浏览器(如 Chrome ),并注意其访问限制,例如本地开发不能直接通过 file:// 协议在浏览器里访问本地 HTML 内引用的 JS 文件,这是因为浏览器对 JavaScript 的安全性要求,会触发 CORS 错误,因此需要启动本地服务并通过 http:// 协议访问。

TIP

CORS (全称 Cross-Origin Resource Sharing )是指跨源资源共享,可以决定浏览器是否需要阻止 JavaScript 获取跨域请求的响应。

现代浏览器默认使用 “同源安全策略” ,这里的 “源” 指 URL 的 origin 部分,例如网页可以通过 window.location.origin 获取到如 https://example.com 这样格式的数据,就是网页的 origin

默认情况下,非同源的请求会被浏览器拦截,最常见的场景是通过 XHR 或者 Fetch 请求 API 接口,需要网页和接口都部署在同一个域名才可以请求成功,否则就会触发跨域限制。

如果网页和接口不在同一个域名,例如网页部署在 https://web.example.com ,接口部署在 https://api.example.com ,此时需要在 https://api.example.com 的 API 服务端程序里,配置 Access-Control-Allow-Origin: * 允许跨域请求( * 代表允许任意外域访问,也可以指定具体的域名作为白名单列表)。

添加服务端程序

接下来搭建一个简单的本地服务,并通过 HTML 文件来引入 ESM 模块文件,体验浏览器端如何使用 ESM 模块。

在 hello-node 项目的根目录下创建名为 server 的文件夹(与 src 目录同级),并添加 index.js 文件,敲入以下代码:

js
// server/index.js
+const { readFileSync } = require('fs')
+const { resolve } = require('path')
+const { createServer } = require('http')
+
+/**
+ * 判断是否 ESM 文件
+ */
+function isESM(url) {
+  return String(url).endsWith('mjs')
+}
+
+/**
+ * 获取 MIME Type 信息
+ * @tips `.mjs` 和 `.js` 一样,都使用 JavaScript 的 MIME Type
+ */
+function mimeType(url) {
+  return isESM(url) ? 'application/javascript' : 'text/html'
+}
+
+/**
+ * 获取入口文件
+ * @returns 存放在本地的文件路径
+ */
+function entryFile(url) {
+  const file = isESM(url) ? `../src/esm${url}` : './index.html'
+  return resolve(__dirname, file)
+}
+
+/**
+ * 创建 HTTP 服务
+ */
+const app = createServer((request, response) => {
+  // 获取请求时的相对路径,如网页路径、网页里的 JS 文件路径等
+  const { url } = request
+
+  // 转换成对应的本地文件路径并读取其内容
+  const entry = entryFile(url)
+  const data = readFileSync(entry, 'utf-8')
+
+  // 需要设置正确的响应头信息,浏览器才可以正确响应
+  response.writeHead(200, { 'Content-Type': mimeType(url) })
+  response.end(data)
+})
+
+/**
+ * 在指定的端口号启动本地服务
+ */
+const port = 8080
+app.listen(port, '0.0.0.0', () => {
+  console.log(`Server running at:`)
+  console.log()
+  console.log(`  ➜  Local:  http://localhost:${port}/`)
+  console.log()
+})

这是一个基础的 Node.js 服务端程序,利用了 HTTP 模块启动本地服务,期间利用 FS 模块的 I/O 能力对本地文件进行读取,而 PATH 模块则简化了文件操作过程中的路径处理和兼容问题(例如众所周知的 Windows 与 macOS 的路径斜杆问题)。

TIP

在这段服务端程序代码里,请留意 mimeType 方法,要让浏览器能够正确解析 .mjs 文件,需要在服务端响应文件内容时,将其 MIME Type 设置为 和 JavaScript 文件一样,这一点非常重要。

并且需要注意传递给 readFileSync API 的文件路径是否与真实存在的文件路径匹配,如果启动服务时,在 Node 控制台报了 no such file or directory 的错误,请检查是否因为笔误写错了文件名称,或者文件路径多了空格等情况。

添加入口页面

继续在 server 目录下添加一个 index.html 并写入以下 HTML 代码,它将作为网站的首页文件:

TIP

可以在 VSCode 先新建一个空文件,文件语言设置为 HTML ,并写入英文感叹号 ! ,再按 Tab 键(或者鼠标选择第一个代码片段提示),可快速生成基础的 HTML 结构。

html
<!-- server/index.html -->
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>ESM run in browser</title>
+  </head>
+  <body>
+    <script type="module" src="./index.mjs"></script>
+  </body>
+</html>

请注意在 <script /> 标签这一句代码上,比平时多了一个 type="module" 属性,这代表这个 script 是使用了 ESM 模块,而 src 属性则对应指向了上文在 src/esm 目录下的入口文件名。

之所以无需使用 ../src/esm/index.mjs 显式的指向真实目录,是因为在 添加服务端程序 时,已通过服务端代码里的 entryFile 方法重新指向了文件所在的真实路径,所以在 HTML 文件里可以使用 ./ 简化文件路径。

启动服务并访问

打开 package.json 文件,在 scripts 字段追加一个 serve 命令如下:

json
{
+  "scripts": {
+    "dev:cjs": "node src/cjs/index.cjs",
+    "dev:esm": "node src/esm/index.mjs",
+    "serve": "node server/index.js"
+  }
+}

在命令行运行 npm run serve 即可启动本地服务:

bash
 npm run serve
+
+> demo@1.0.0 serve
+> node server/index.js
+
+Server running at:
+
+    Local:  http://localhost:8080/

根据命令行提示,在浏览器访问 http://localhost:8080/ 地址,即可访问本地服务。

TIP

如遭遇端口号冲突,可在 server/index.js 的 const port = 8080 代码处修改为其他端口号。

因为在编写 HTML 文件时没有写入内容,只引入了 ESM 模块文件,因此需要按 F12 唤起浏览器的控制台查看 Log ,可以看到控制台根据模块的文件内容,输出了这三句 Log (如果没有 Log ,可在控制台唤起的情况下按 F5 重新载入页面):

bash
1                                                   index.mjs:8
+Hello World from foo.                               module.mjs:2
+Hello World from bar.                               index.mjs:14

分别来自 src/esm/index.mjs 本身的 console.log 语句,以及 import 进来的 module.mjs 里的 console.log 语句。

如果未能出现这三句 Log ,请留意 .mjs 文件内容是否为上一小节最后的内容:

src/esm/index.mjs 文件内容为:

js
// src/esm/index.mjs
+import {
+  foo as foo2, // 这里进行了重命名
+  bar,
+} from './module.mjs'
+
+// 就不会造成变量冲突
+const foo = 1
+console.log(foo)
+
+// 用新的命名来调用模块里的方法
+foo2()
+
+// 这个不冲突就可以不必处理
+console.log(bar)

src/esm/module.mjs 文件内容为:

js
// src/esm/module.mjs
+export function foo() {
+  console.log('Hello World from foo.')
+}
+
+export const bar = 'Hello World from bar.'
内联的 ESM 代码

到目前为止, server/index.html 文件里始终是通过文件的形式引入 ESM 模块,其实 <script type="module" /> 也支持编写内联代码,和普通的 <script /> 标签用法相同:

html
<script type="module">
+  // ESM 模块的 JavaScript 代码
+</script>

请移除 <script /> 标签的 src 属性,并在标签内写入 src/esm/index.mjs 文件里的代码,现在该 HTML 文件的完整代码如下:

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>ESM run in browser</title>
+  </head>
+  <body>
+    <!-- 标签内的代码就是 src/esm/index.mjs 的代码 -->
+    <script type="module">
+      import {
+        foo as foo2, // 这里进行了重命名
+        bar,
+      } from './module.mjs'
+
+      // 就不会造成变量冲突
+      const foo = 1
+      console.log(foo)
+
+      // 用新的命名来调用模块里的方法
+      foo2()
+
+      // 这个不冲突就可以不必处理
+      console.log(bar)
+    </script>
+  </body>
+</html>

回到浏览器刷新 http://localhost:8080/ ,可以看到浏览器控制台依然输出了和引入 src="./index.mjs" 时一样的 Log 信息:

bash
1                                                   (index):21
+Hello World from foo.                               module.mjs:2
+Hello World from bar.                               (index):27
了解模块导入限制

虽然以上例子可以完美地在浏览器里引用现成的 ESM 模块代码并运行,但不代表工程化项目下所有的 ES Module 模块化方式都适合浏览器。

先做一个小尝试,将 src/esm/index.mjs 文件内容修改如下,导入项目已安装的 md5 工具包:

js
// src/esm/index.mjs
+import md5 from 'md5'
+console.log(md5('Hello World'))

回到浏览器刷新 http://localhost:8080/ ,观察控制台,可以发现出现了一个红色的错误信息:

bash
Uncaught TypeError: Failed to resolve module specifier "md5".
+Relative references must start with either "/", "./", or "../".

这是因为不论是通过 <script type="module" /> 标签还是通过 import 语句导入,模块的路径都必须是以 /./ 或者是 ../ 开头,因此无法直接通过 npm 包名进行导入。

这种情况下需要借助另外一个 script 类型: importmap ,在 server/index.html 里追加 <script type="importmap" /> 这一段代码:

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>ESM run in browser</title>
+  </head>
+  <body>
+    <!-- 注意需要先通过 `importmap` 引入 npm 包的 CDN -->
+    <script type="importmap">
+      {
+        "imports": {
+          "md5": "https://esm.run/md5"
+        }
+      }
+    </script>
+
+    <!-- 然后才能在 `module` 里 `import xx from 'xx'` -->
+    <script type="module" src="./index.mjs"></script>
+  </body>
+</html>

再次刷新页面,可以看到控制台成功输出了 b10a8db164e0754105b7a99be72e3fe5 这个字符串,也就是 Hello World 被 MD5 处理后的结果。

可以看到 importmap 的声明方式和 package.json 的 dependencies 字段非常相似, JSON 的 key 是包名称, value 则是支持 ESM 的远程地址。

TIP

Import Maps 的运行机制是通过 import 映射来控制模块说明符的解析,类似于构建工具常用的 alias 别名机制。

这是一个现代浏览器才能支持的新特性,建议使用 Chrome 最新版本体验完整功能,可以在其 GitHub 仓库 查看更多用法。

上方例子里, md5 对应的远程地址是使用了来自 esm.run 网站的 URL ,而不是 npm 包同步到 jsDelivr CDN 或者 UNPKG CDN 的地址,这是因为 md5 这个包本身不支持 ES Module ,需要通过 esm.run 这个网站进行在线转换才可以在 <script type="module" /> 上使用。

该网站的服务是 jsDelivr CDN 所属的服务商提供,因此也可以通过 jsDelivr CDN 的 URL 添加 /+esm 参数来达到转换效果,以 md5 包为例:

bash
# 默认是一个 CJS 包
+https://cdn.jsdelivr.net/npm/md5
+
+# 可添加 `/+esm` 参数变成 ESM 包
+https://cdn.jsdelivr.net/npm/md5/+esm

总的来说,现阶段在浏览器使用 ES Module 并不是一个很好的选择,建议开发者还是使用构建工具来开发,工具可以抹平这些浏览器差异化问题,降低开发成本。

认识组件化设计

学习完模块化设计之后,未来在 Vue 的工程化开发过程中,还会遇到一个新的概念,那就是 “组件” 。

什么是组件化

模块化属于 JavaScript 的概念,但作为一个页面,都知道它是由 HTML + CSS + JS 三部分组成的,既然 JS 代码可以按照不同的功能、需求划分成模块,那么页面是否也可以呢?

答案是肯定的!组件化就是由此而来。

在前端工程项目里,页面可以理解为一个积木作品,组件则是用来搭建这个作品的一块又一块积木。

解决了什么问题

模块化属于 JavaScript 的概念,把代码块的职责单一化,一个函数、一个类都可以独立成一个模块。

但这只解决了逻辑部分的问题,一个页面除了逻辑,还有骨架( HTML )和样式( CSS ),组件就是把一些可复用的 HTML 结构和 CSS 样式再做一层抽离,然后再放置到需要展示的位置。

常见的组件有:页头、页脚、导航栏、侧边栏… 甚至小到一个用户头像也可以抽离成组件,因为头像可能只是尺寸、圆角不同而已。

每个组件都有自己的 “作用域” , JavaScript 部分利用 模块化 来实现作用域隔离, HTML 和 CSS 代码则借助 Style Scoped 来生成独有的 hash ,避免全局污染,这些方案组合起来,使得组件与组件之间的代码不会互相影响。

如何实现组件化

在 Vue ,是通过 Single-File Component (简称 SFC , .vue 单文件组件)来实现组件化开发。

一个 Vue 组件是由三部分组成的:

vue
<template>
+  <!-- HTML 代码 -->
+</template>
+
+<script>
+// JavaScript 代码
+</script>
+
+<style scoped>
+/* CSS 代码 */
+</style>

在后面的 单组件的编写 一章中,会详细介绍如何编写一个 Vue 组件。

依赖包和插件

在实际业务中,经常会用到各种各样的插件,插件在 Node 项目里的体现是一个又一个的依赖包。

虽然也可以把插件的代码文件手动放到的源码文件夹里引入,但并不是一个最佳的选择,本节内容将带了解 Node 的依赖包。

什么是包

在 Node 项目里,包可以简单理解为模块的集合,一个包可以只提供一个模块的功能,也可以作为多个模块的集合集中管理。

包通常是发布在官方的包管理平台 npmjs 上面,开发者需要使用的时候,可以通过包管理器安装到项目里,并在的代码里引入,开箱即用(详见: 依赖包的管理 )。

使用 npm 包可以减少在项目中重复造轮子,提高项目的开发效率,也可以极大的缩小项目源码的体积(详见:什么是 node_modules)。

包管理平台官网:https://www.npmjs.com

什么是 node_modules

node_modules 是 Node 项目下用于存放已安装的依赖包的目录,如果不存在,会自动创建。

如果是本地依赖,会存在于项目根目录下,如果是全局依赖,会存在于环境变量关联的路径下,详见下方的管理依赖部分内容的讲解。

TIP

一般在提交项目代码到 Git 仓库或者的服务器上时,都需要排除 node_modules 文件夹的提交,因为它非常大。

如果托管在 Git 仓库,可以在 .gitignore 文件里添加 node_modules 作为要排除的文件夹名称。

什么是包管理器

包管理器( Package Manager )是用来管理依赖包的工具,比如:发布、安装、更新、卸载等等。

Node 默认提供了一个包管理器 npm ,在安装 Node.js 的时候,默认会一起安装 npm 包管理器,可以通过以下命令查看它是否正常。

bash
npm -v

如果正常,将会输出相应的版本号。

依赖包的管理

接下来会以 npm 作为默认的包管理器,来了解如何在项目里管理依赖包。

配置镜像源

在国内,直接使用 npm 会比较慢,可以通过绑定 npm Mirror 中国镜像站 的镜像源来提升依赖包的下载速度。

可以先在命令行输入以下命令查看当前的 npm 配置:

bash
npm config get registry
+# https://registry.npmjs.org/

默认情况下,会输出 npm 官方的资源注册表地址,接下来在命令行上输入以下命令,进行镜像源的绑定:

bash
npm config set registry https://registry.npmmirror.com

可以再次运行查询命令来查看是否设置成功:

bash
npm config get registry
+# https://registry.npmmirror.com/

可以看到已经成功更换为中国镜像站的地址了,之后在安装 npm 包的时候,速度会有很大的提升!

如果需要删除自己配置的镜像源,可以输入以下命令进行移除,移除后会恢复默认设置:

bash
npm config rm registry

TIP

如果之前已经绑定过 npm.taobao 系列域名,也请记得更换成 npmmirror 这个新的域名!

随着新的域名已经正式启用,老 npm.taobao.orgregistry.npm.taobao.org 域名在 2022 年 05 月 31 日零时后不再提供服务。

详见:【望周知】淘宝 npm 镜像站喊你切换新域名啦

本地安装

项目的依赖建议优先选择本地安装,这是因为本地安装可以把依赖列表记录到 package.json 里,多人协作的时候可以减少很多问题出现,特别是当本地依赖与全局依赖版本号不一致的时候。

生产依赖

执行 npm install 的时候,添加 --save 或者 -S 选项可以将依赖安装到本地,并列为生产依赖。

TIP

需要提前在命令行 cd 到的项目目录下再执行安装。

另外, --save 或者 -S 选项在实际使用的时候可以省略,因为它是默认选项。

bash
npm install --save <package-name>

可以在项目的 package.json 文件里的 dependencies 字段查看是否已安装成功,例如:

json
// package.json
+{
+  // 会安装到这里
+  "dependencies": {
+    // 以 "包名":"版本号" 的格式写入
+    "vue-router": "^4.0.14"
+  }
+}

生产依赖包会被安装到项目根目录下的 node_modules 目录里。

项目在上线后仍需用到的包,就需要安装到生产依赖里,比如 Vue 的路由 vue-router 就需要以这个方式安装。

开发依赖

执行 npm install 的时候,如果添加 --save-dev 或者 -D 选项,可以将依赖安装到本地,并写入开发依赖里。

TIP

需要提前在命令行 cd 到的项目目录下再执行安装。

bash
npm install --save-dev <package-name>

可以在项目的 package.json 文件里的 devDependencies 字段查看是否已安装成功,例如:

json
// package.json
+{
+  // 会安装到这里
+  "devDependencies": {
+    // 以 "包名":"版本号" 的格式写入
+    "eslint": "^8.6.0"
+  }
+}

开发依赖包也是会被安装到项目根目录下的 node_modules 目录里。

和生产依赖包不同的点在于,只在开发环境生效,构建部署到生产环境时可能会被抛弃,一些只在开发环境下使用的包,就可以安装到开发依赖里,比如检查代码是否正确的 ESLint 就可以用这个方式安装。

全局安装

执行 npm install 的时候,如果添加 --global 或者 -g 选项,可以将依赖安装到全局,它们将被安装在 配置环境变量 里配置的全局资源路径里。

bash
npm install --global <package-name>

TIP

Mac 用户需要使用 sudo 来提权才可以完成全局安装。

另外,可以通过 npm root -g 查看全局包的安装路径。

一般情况下,类似于 @vue/cli 之类的脚手架会提供全局安装的服务,安装后,就可以使用 vue create xxx 等命令直接创建 Vue 项目了。

但不是每个 npm 包在全局安装后都可以正常使用,请阅读 npm 包的主页介绍和使用说明。

版本控制

有时候一些包的新版本不一定适合的老项目,因此 npm 也提供了版本控制功能,支持通过指定的版本号或者 Tag 安装。

语法如下,在包名后面紧跟 @ 符号,再紧跟版本号或者 Tag 名称。

bash
npm install <package-name>@<version | tag>

例如:

现阶段 Vue 默认为 3.x 的版本了,如果想安装 Vue 2 ,可以通过指定版本号的方式安装:

bash
npm install vue@2.6.14

或者通过对应的 Tag 安装:

bash
npm install vue@legacy

TIP

版本号或者 Tag 名称可以在 npmjs 网站上的包详情页查询。

版本升级

一般来说,直接重新安装依赖包可以达到更新的目的,但也可以通过 npm update 命令来更新。

语法如下,可以更新全部的包:

bash
npm update

也可以更新指定的包:

bash
npm update <package-name>

npm 会检查是否有满足版本限制的更新版本。

卸载

可以通过 npm uninstall 命令来卸载指定的包,和安装一样,卸载也区分了卸载本地依赖包和卸载全局包,不过只有在卸载全局包的时候才需要添加选项,默认只卸载当前项目下的本地包。

本地卸载:

bash
npm uninstall <package-name>

全局卸载:

bash
npm uninstall --global <package-name>

TIP

Mac 用户需要使用 sudo 来提权才可以完成全局卸载。

如何使用包

在了解了 npm 包的常规操作之后,通过一个简单的例子来了解如何在项目里使用 npm 包。

继续使用的 Hello Node demo ,或者也可以重新创建一个 demo 。

首先在 命令行工具 通过 cd 命令进入项目所在的目录,用本地安装的方式来把 md5 包 添加到生产依赖,这是一个为提供开箱即用的哈希算法的包,在未来的实际工作中,可能也会用到它,在这里使用它是因为足够简单。

输入以下命令并回车执行:

bash
npm install md5

可以看到控制台提示一共安装了 4 个包,这是因为 md5 这个 npm 包还引用了其他的包作为依赖,需要同时安装才可以正常工作。

bash
# 这是安装 md5 之后控制台的信息返回
+added 4 packages, and audited 5 packages in 2s
+
+found 0 vulnerabilities

此时项目目录下会出现一个 node_modules 文件夹和一个 package-lock.json 文件:

bash
hello-node
+ # 依赖文件夹
+├─node_modules
+ # 源码文件夹
+├─src
+ # 锁定安装依赖的版本号
+├─package-lock.json
+ # 项目清单
+└─package.json

先打开 package.json ,可以看到已经多出了一个 dependencies 字段,这里记录了刚刚安装的 md5 包信息。

json
{
+  "name": "hello-node",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "dev:cjs": "node src/cjs/index.cjs",
+    "dev:esm": "node src/esm/index.mjs",
+    "serve": "node server/index.js"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "md5": "^2.3.0"
+  }
+}

来到这里可能会有一连串的疑问:

  1. 为什么只安装了一个 md5 ,但控制台提示安装了 4 个包?
  2. 为什么 package.json 又只记录了 1 个 md5 包信息?
  3. 为什么提示审核了 5 个包,哪里来的第 5 个包?

不要着急,请先打开 package-lock.json 文件,这个文件是记录了锁定安装依赖的版本号信息(由于篇幅原因,这里的展示省略了一些包的细节):

json
{
+  "name": "hello-node",
+  "version": "1.0.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "hello-node",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "md5": "^2.3.0"
+      }
+    },
+    "node_modules/charenc": {
+      "version": "0.0.2"
+      // ...
+    },
+    "node_modules/crypt": {
+      "version": "0.0.2"
+      // ...
+    },
+    "node_modules/is-buffer": {
+      "version": "1.1.6"
+      // ...
+    },
+    "node_modules/md5": {
+      "version": "2.3.0"
+      // ...
+    }
+  },
+  "dependencies": {
+    "charenc": {
+      "version": "0.0.2"
+      // ...
+    },
+    "crypt": {
+      "version": "0.0.2"
+      // ...
+    },
+    "is-buffer": {
+      "version": "1.1.6"
+      // ...
+    },
+    "md5": {
+      "version": "2.3.0",
+      // ...
+      "requires": {
+        "charenc": "0.0.2",
+        "crypt": "0.0.2",
+        "is-buffer": "~1.1.6"
+      }
+    }
+  }
+}

可以看到这个文件的 dependencies 字段除了 md5 之外,还有另外 3 个包信息,它们就是 md5 包所依赖的另外 3 个 npm 包了,这就解答了为什么一共安装了 4 个 npm 包。

在 node_modules 文件夹下也可以看到以这 4 个包名为命名的文件夹,这些文件夹存放的就是各个包项目发布在 npmjs 平台上的文件。

再看 packages 字段,这里除了罗列出 4 个 npm 包的信息之外,还把项目的信息也列了进来,这就是为什么是提示审核了 5 个包,原因是除了 4 个依赖包,该项目本身也是一个包。

TIP

package-lock.json 文件并不是一成不变的,假如以后 md5 又引用了更多的包,这里记录的信息也会随之增加。

并且不同的包管理器,它的 lock 文件也会不同,如果是使用 yarn 作为包管理器的话,它是生成一个 yarn.lock 文件,而不是 package-lock.json ,有关更多的包管理器,详见 插件的使用 一章。

现在已经安装好 md5 包了,接下来看看具体如何使用它。

通常在包的 npmjs 主页上会有 API 和用法的说明,只需要根据说明操作,打开 src/esm/index.mjs 文件,首先需要导入这个包。

包的导入和在 学习模块化设计 一节了解到的模块导入用法是一样的,只是把 from 后面的文件路径换成了包名。

js
// src/esm/index.mjs
+import md5 from 'md5'

然后根据 md5 的用法,来编写一个小例子,先声明一个原始字符串变量,然后再声明一个使用 md5 加密过的字符串变量,并打印它们:

js
// src/esm/index.mjs
+import md5 from 'md5'
+
+const before = 'Hello World'
+const after = md5(before)
+console.log({ before, after })

在命令行输入 npm run dev:esm ,可以在控制台看到输出了这些内容,成功获得了转换后的结果:

bash
npm run dev:esm
+
+> demo@1.0.0 dev:esm
+> node src/esm/index.mjs
+
+{ before: 'Hello World', after: 'b10a8db164e0754105b7a99be72e3fe5' }

是不是非常简单,其实包的用法和在导入模块的用法可以说是完全一样的,区别主要在于,包是需要安装了才能用,而模块是需要自己编写。

控制编译代码的兼容性

作为一名前端工程师,了解如何控制代码的兼容性是非常重要的能力。

在 “了解前端工程化” 的 为什么要使用构建工具 一节里,已简单介绍过 Polyfill 的作用,以及介绍了构建工具可以通过 Babel 等方案自动化处理代码的兼容问题,这一小节将讲解 Babel 的配置和使用,亲自体验如何控制代码的兼容性转换。

如何查询兼容性

在开始学习使用 Babel 之前,需要先掌握一个小技能:了解如何查询代码在不同浏览器上的兼容性。

说起浏览器兼容性,前端工程师应该都不陌生,特别是初学者很容易会遇到在自己的浏览器上布局正确、功能正常,而在其他人的电脑或者手机上访问就会有布局错位或者运行报错的问题出现,最常见的场景就是开发者使用的是功能强大的 Chrome 浏览器,而产品用户使用了 IE 浏览器。

这是因为网页开发使用的 HTML / CSS / JavaScript 每年都在更新新版本,推出更好用的新 API ,或者废弃部分过时的旧 API ,不同的浏览器在版本更新过程中,对这些新 API 的支持程度并不一致,如果使用了新 API 而没有做好兼容支持,很容易就会在低版本浏览器上出现问题。

为了保证程序可以正确的在不同版本浏览器之间运行,就需要根据产品要支持的目标浏览器范围,去选择兼容性最好的编程方案。

在 Web 开发有一个网站非常知名:Can I use ,只要搜索 API 的名称,它会以图表的形式展示该 API 在不同浏览器的不同版本之间的支持情况,支持 HTML 标签、 CSS 属性、 JavaScript API 等内容的查询。

以 JavaScript ES6 的 class 新特性为例:

可以看到在 Chrome 浏览器需要在 49 版本开始才被完全支持,而 IE 浏览器则全面不支持,如果不做特殊处理(例如引入 Polyfill 方案),那么就需要考虑在编程过程中,是否需要可以直接使用 class 来实现功能,还是寻找其他替代方案。

在工作中,工程师无需关注每一个 API 的具体支持范围,这些工作可以交给工具来处理,下面将介绍 Babel 的使用入门。

Babel 的使用和配置

Babel 是一个 JavaScript 编译器,它可以让开发者仅需维护一份简单的 JSON 配置文件,即可调动一系列工具链将源代码编译为目标浏览器指定版本所支持的语法。

安装 Babel

请打开 hello-node 项目,安装以下几个 Babel 依赖:

bash
npm i -D @babel/core @babel/cli @babel/preset-env

此时在 package.json 的 devDependencies 可以看到有了如下三个依赖:

json
{
+  "devDependencies": {
+    "@babel/cli": "^7.19.3",
+    "@babel/core": "^7.19.3",
+    "@babel/preset-env": "^7.19.3"
+  }
+}

它们的作用分别如下:

依赖作用文档
@babel/cli安装后可以从命令行使用 Babel 编译文件查看文档
@babel/coreBabel 的核心功能包查看文档
@babel/preset-env智能预设,可以通过它的选项控制代码要转换的支持版本查看文档

TIP

在使用 Babel 时,建议在项目下进行本地安装,尽量不选择全局安装,这是因为不同项目可能依赖于不同版本的 Babel ,全局依赖和可能会出现使用上的异常。

添加 Babel 配置

接下来在 hello-node 的根目录下创建一个名为 babel.config.json 的文件,这是 Babel 的配置文件,写入以下内容:

json
{
+  "presets": [
+    [
+      "@babel/preset-env",
+      {
+        "targets": {
+          "chrome": "41"
+        },
+        "modules": false,
+        "useBuiltIns": "usage",
+        "corejs": "3.6.5"
+      }
+    ]
+  ]
+}

这份配置将以 Chrome 浏览器作为目标浏览器,编译结果将保留 ES Module 规范,可以在 配置文件文档 查看更多配置选项。

这里的 targets.chrome 字段代表编译后要支持的目标浏览器版本号,在 caniuse 查询可知 ES6 的 class 语法 在 Chrome 49 版本之后才被完全支持,而 Chrome 41 或更低的版本是完全不支持该语法,因此先将其目标版本号设置为 41 ,下一步将开始测试 Babel 的编译结果。

使用 Babel 编译代码

在 hello-node 的 src 目录下添加一个 babel 文件夹,并在该文件夹下创建一个 index.js 文件,写入以下代码:

js
// src/babel/index.js
+export class Hello {
+  constructor(name) {
+    this.name = name
+  }
+
+  say() {
+    return `Hello ${this.name}`
+  }
+}

根据上一步的 Babel 配置,在这里使用 class 语法作为测试代码。

接下来再打开 package.json 文件,添加一个 compile script 如下:

json
{
+  "scripts": {
+    "dev:cjs": "node src/cjs/index.cjs",
+    "dev:esm": "node src/esm/index.mjs",
+    "compile": "babel src/babel --out-dir compiled",
+    "serve": "node server/index.js"
+  }
+}

这条命令的含义是:使用 Babel 处理 src/babel 目录下的文件,并输出到根目录下的 compiled 文件夹。

在命令行运行以下命令:

bash
npm run compile

可以看到 hello-node 的根目录下多了一个 compiled 文件夹,里面有一个和源码相同命名的 index.js 文件,它的文件内容如下:

js
// compiled/index.js
+function _classCallCheck(instance, Constructor) {
+  if (!(instance instanceof Constructor)) {
+    throw new TypeError('Cannot call a class as a function')
+  }
+}
+
+function _defineProperties(target, props) {
+  for (var i = 0; i < props.length; i++) {
+    var descriptor = props[i]
+    descriptor.enumerable = descriptor.enumerable || false
+    descriptor.configurable = true
+    if ('value' in descriptor) descriptor.writable = true
+    Object.defineProperty(target, descriptor.key, descriptor)
+  }
+}
+
+function _createClass(Constructor, protoProps, staticProps) {
+  if (protoProps) _defineProperties(Constructor.prototype, protoProps)
+  if (staticProps) _defineProperties(Constructor, staticProps)
+  Object.defineProperty(Constructor, 'prototype', { writable: false })
+  return Constructor
+}
+
+export var Hello = /*#__PURE__*/ (function () {
+  function Hello(name) {
+    _classCallCheck(this, Hello)
+
+    this.name = name
+  }
+
+  _createClass(Hello, [
+    {
+      key: 'say',
+      value: function say() {
+        return `Hello ${this.name}`
+      },
+    },
+  ])
+
+  return Hello
+})()

由于 Chrome 41 版本不支持 class 语法,因此 Babel 做了大量的工作对其进行转换兼容。

再次打开 babel.config.json ,将 targets.chrome 的版本号调整为支持 class 语法的 Chrome 49 版本:

diff
{
+  "presets": [
+    [
+      "@babel/preset-env",
+      {
+        "targets": {
+-          "chrome": "41"
++          "chrome": "49"
+        },
+        "modules": false,
+        "useBuiltIns": "usage",
+        "corejs": "3.6.5"
+      }
+    ]
+  ]
+}

再次执行编译,这一次编译后的代码和编译前完全一样:

js
// compiled/index.js
+export class Hello {
+  constructor(name) {
+    this.name = name
+  }
+
+  say() {
+    return `Hello ${this.name}`
+  }
+}

因为此时配置文件指定的目标浏览器版本已支持该语法,无需转换。

Babel 的使用其实非常简单,了解了这部分知识点之后,如果某一天需要自己控制代码的兼容性,只需要配合官方文档调整 Babel 的配置,处理起来就得心应手了!

Released under the MIT License.

+ + + + \ No newline at end of file diff --git a/hashmap.json b/hashmap.json new file mode 100644 index 00000000..1a7ee675 --- /dev/null +++ b/hashmap.json @@ -0,0 +1 @@ +{"changelog.md":"1e825a6d","index.md":"c8d0f3cd","links.md":"d1aafe6b","preface.md":"59b338cf","engineering.md":"43ce842b","pinia.md":"c3d0acb5","router.md":"e17ca07e","upgrade.md":"25cf659f","typescript.md":"e6e7bfb1","plugin.md":"66b7dcdd","efficient.md":"a41d0892","component.md":"b59f8845","guide.md":"c60fac0b","communication.md":"03c12ebf"} diff --git a/icons/apple-touch-icon-152x152.png b/icons/apple-touch-icon-152x152.png new file mode 100644 index 00000000..397ad2be Binary files /dev/null and b/icons/apple-touch-icon-152x152.png differ diff --git a/icons/icon-128x128.png b/icons/icon-128x128.png new file mode 100644 index 00000000..d99e792d Binary files /dev/null and b/icons/icon-128x128.png differ diff --git a/icons/icon-256x256.png b/icons/icon-256x256.png new file mode 100644 index 00000000..60da39b5 Binary files /dev/null and b/icons/icon-256x256.png differ diff --git a/icons/icon-32x32.png b/icons/icon-32x32.png new file mode 100644 index 00000000..2a1785cc Binary files /dev/null and b/icons/icon-32x32.png differ diff --git a/icons/icon-512x512.png b/icons/icon-512x512.png new file mode 100644 index 00000000..0cc474c9 Binary files /dev/null and b/icons/icon-512x512.png differ diff --git a/icons/msapplication-icon-144x144.png b/icons/msapplication-icon-144x144.png new file mode 100644 index 00000000..90f0f97c Binary files /dev/null and b/icons/msapplication-icon-144x144.png differ diff --git a/icons/safari-pinned-tab.svg b/icons/safari-pinned-tab.svg new file mode 100644 index 00000000..36c53a2c --- /dev/null +++ b/icons/safari-pinned-tab.svg @@ -0,0 +1,862 @@ + + + + diff --git a/index.html b/index.html new file mode 100644 index 00000000..00b4b282 --- /dev/null +++ b/index.html @@ -0,0 +1,24 @@ + + + + + + 出版说明 | Vue3 入门指南与实战案例 + + + + + + + + + + + + + +
Skip to content
目录

出版说明

感谢各位读者长期以来的支持,经过长达一年时间的打磨和优化,本书已于 2023 年 5 月份正式出版上市,纸质版书籍的正式名称为《前端工程化:基于 Vue.js 3.0 的设计与实践》。

如果您对笔者的作品认可,建议购买纸质版,纸质书在电子书的基础上,经过机械工业出版社的编辑老师们的内容优化、校对勘误、排版美化,更成体系!

在此特别感谢李晓波编辑支持笔者将开源作品出版为纸质作品,李老师全程帮忙跟进了无数的大事小事,也给笔者科普了很多出版方面的知识,十分尽职!

购买地址

各位读者可以在各大电商平台购买纸质版书籍:

☞ 访问 京东商城 购买

☞ 访问 天猫商城 购买

TIP

可以在电商平台搜索纸质版的书名 “前端工程化:基于 Vue.js 3.0 的设计与实践” 或者笔者的名字 “程沛权” 查找更多的购买地址。

背后的故事

如果读者想了解这本书是如何写出来的,想了解笔者在日常生活中是个什么样的人,想知道笔者的表达能力是如何培养的,可以阅读 这本书背后的故事

Released under the MIT License.

+ + + + \ No newline at end of file diff --git a/links.html b/links.html new file mode 100644 index 00000000..fe655ac1 --- /dev/null +++ b/links.html @@ -0,0 +1,24 @@ + + + + + + 常用文档 | Vue3 入门指南与实战案例 + + + + + + + + + + + + + +
Skip to content
目录

Released under the MIT License.

+ + + + \ No newline at end of file diff --git a/logo.png b/logo.png new file mode 100644 index 00000000..60e17006 Binary files /dev/null and b/logo.png differ diff --git a/logo.svg b/logo.svg new file mode 100644 index 00000000..d4d5f0bd --- /dev/null +++ b/logo.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/manifest.json b/manifest.json new file mode 100644 index 00000000..de620f75 --- /dev/null +++ b/manifest.json @@ -0,0 +1,24 @@ +{ + "icons": [ + { + "src": "icons/icon-32x32.png", + "type": "image/png", + "sizes": "32x32" + }, + { + "src": "icons/icon-128x128.png", + "type": "image/png", + "sizes": "128x128" + }, + { + "src": "icons/icon-256x256.png", + "type": "image/png", + "sizes": "256x256" + }, + { + "src": "icons/icon-512x512.png", + "type": "image/png", + "sizes": "512x512" + } + ] +} \ No newline at end of file diff --git a/pinia.html b/pinia.html new file mode 100644 index 00000000..c4005341 --- /dev/null +++ b/pinia.html @@ -0,0 +1,481 @@ + + + + + + 全局状态管理 | Vue3 入门指南与实战案例 + + + + + + + + + + + + + +
Skip to content
目录

全局状态管理

本来这部分打算放在 组件之间的通信 里,里面也简单介绍了一下 Vuex ,但 Pinia 作为被官方推荐在 Vue 3 项目里作为全局状态管理的新工具,写着写着笔者认为还是单独开一章来写会更方便阅读和理解。

官方推出的全局状态管理工具目前有 VuexPinia ,两者的作用和用法都比较相似,但 Pinia 的设计更贴近 Vue 3 组合式 API 的用法。

TIP

本章内的大部分内容都会和 Vuex 作对比,方便从 Vuex 项目向 Pinia 的迁移。

温馨提示

这是笔者写于 2024.02.20 的补充提示,为读者解答为何前面的章节都未提及 script-setup 组件语法的信息。

笔者在执笔之初,考虑到降低初学过程中的各种心智负担,以及当时处于 script-setup 组件语法尚未完全稳定的背景下,所以在最后一章 高效开发 之前的章节,都只提供了组件的标准写法的讲解。

除非遇到框架进行了 Breaking Change (例如从 Vue 2 升级到了 Vue 3 这种大版本更新),否则标准写法是始终有效的,在笔者多年的学习经历和实际收获里,也是比较相信一点:不论学习什么语言或者框架,只要基础打的好,未来总是可以轻松地举一反三去解决遇到的各类问题。

因此如果读者通过各类脚手架创建的 Vue 3 项目,在组件看到 <script setup> 这种风格的组件,以及诸如 defineXxx 风格的 API ,这是 .vue 组件所支持的一种语法糖,可以随时在 高效开发 一章了解相关的 API 用法。

另外从 Vue 3.4 版本之后所新增的 API 未在本书的介绍范围内,请读者在阅读的过程中也可以查阅官方的 API 一起学习,不论是本书还是其他计算机书籍,官方文档永远是最好的查询手册。

TIP

语法糖(英语:Syntactic sugar)是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。

关于 Pinia ~new

由于 Vuex 4.x 版本只是个过渡版,Vuex 4 对 TypeScript 和 Composition API 都不是很友好,虽然官方团队在 GitHub 已有讨论 Vuex 5 的开发提案,但从 2022-02-07 在 Vue 3 被设置为默认版本开始, Pinia 已正式被官方推荐作为全局状态管理的工具。

Pinia 支持 Vue 3 和 Vue 2 ,对 TypeScript 也有很完好的支持,延续本指南的宗旨,在这里只介绍基于 Vue 3 和 TypeScript 的用法。

点击访问:Pinia 官网

安装和启用 ~new

Pinia 目前还没有被广泛的默认集成在各种脚手架里,所以如果原来创建的项目没有 Pinia ,则需要手动安装它。

bash
# 需要 cd 到的项目目录下
+npm install pinia

查看的 package.json ,看看里面的 dependencies 是否成功加入了 Pinia 和它的版本号(下方是示例代码,以实际安装的最新版本号为准):

json
{
+  "dependencies": {
+    "pinia": "^2.0.11"
+  }
+}

然后打开 src/main.ts 文件,添加下面那两行有注释的新代码:

ts
import { createApp } from 'vue'
+import { createPinia } from 'pinia' // 导入 Pinia
+import App from '@/App.vue'
+
+createApp(App)
+  .use(createPinia()) // 启用 Pinia
+  .mount('#app')

到这里, Pinia 就集成到的项目里了。

TIP

也可以通过 Create Preset 创建新项目(选择 vue 技术栈进入,选择 vue3-ts-vite 模板),可以得到一个集成常用配置的项目启动模板,该模板现在使用 Pinia 作为全局状态管理工具。

状态树的结构 ~new

在开始写代码之前,先来看一个对比,直观的了解 Pinia 的状态树构成,才能在后面的环节更好的理解每个功能的用途。

鉴于可能有部分开发者之前没有用过 Vuex ,所以加入了 Vue 组件一起对比( Options API 写法)。

作用Vue ComponentVuexPinia
数据管理datastatestate
数据计算computedgettersgetters
行为方法methodsmutations / actionsactions

可以看到 Pinia 的结构和用途都和 Vuex 与 Component 非常相似,并且 Pinia 相对于 Vuex ,在行为方法部分去掉了 mutations (同步操作)和 actions (异步操作)的区分,更接近组件的结构,入门成本会更低一些。

下面来创建一个简单的 Store ,开始用 Pinia 来进行状态管理。

创建 Store ~new

和 Vuex 一样, Pinia 的核心也是称之为 Store 。

参照 Pinia 官网推荐的项目管理方案,也是先在 src 文件夹下创建一个 stores 文件夹,并在里面添加一个 index.ts 文件,然后就可以来添加一个最基础的 Store 。

Store 是通过 defineStore 方法来创建的,它有两种入参形式:

形式 1 :接收两个参数

接收两个参数,第一个参数是 Store 的唯一 ID ,第二个参数是 Store 的选项:

ts
// src/stores/index.ts
+import { defineStore } from 'pinia'
+
+export const useStore = defineStore('main', {
+  // Store 选项...
+})

形式 2 :接收一个参数

接收一个参数,直接传入 Store 的选项,但是需要把唯一 ID 作为选项的一部分一起传入:

ts
// src/stores/index.ts
+import { defineStore } from 'pinia'
+
+export const useStore = defineStore({
+  id: 'main',
+  // Store 选项...
+})

TIP

不论是哪种创建形式,都必须为 Store 指定一个唯一 ID 。

另外可以看到这里把导出的函数名命名为 useStore ,以 use 开头是 Vue 3 对可组合函数的一个命名约定。

并且使用的是 export const 而不是 export default (详见:命名导出和默认导出),这样在使用的时候可以和其他的 Vue 组合函数保持一致,都是通过 import { xxx } from 'xxx' 来导入。

如果有多个 Store ,可以分模块管理,并根据实际的功能用途进行命名( e.g. useMessageStoreuseUserStoreuseGameStore … )。

管理 state ~new

在上一小节的 状态树的结构 这里已经了解过, Pinia 是在 state 里面定义状态数据。

给 Store 添加 state

它是通过一个箭头函数的形式来返回数据,并且能够正确的帮推导 TypeScript 类型:

ts
// src/stores/index.ts
+import { defineStore } from 'pinia'
+
+export const useStore = defineStore('main', {
+  // 先定义一个最基本的 message 数据
+  state: () => ({
+    message: 'Hello World',
+  }),
+  // ...
+})

需要注意一点的是,如果不显式 return ,箭头函数的返回值需要用圆括号 () 套起来,这个是箭头函数的要求(详见:返回对象字面量)。

所以相当于这样写:

ts
// ...
+export const useStore = defineStore('main', {
+  state: () => {
+    return {
+      message: 'Hello World',
+    }
+  },
+  // ...
+})

笔者还是更喜欢加圆括号的简写方式。

TIP

可能有开发者会问: Vuex 可以用一个对象来定义 state 的数据, Pinia 可以吗?

答案是:不可以! state 的类型必须是 state?: (() => {}) | undefined ,要么不配置(就是 undefined ),要么只能是个箭头函数。

手动指定数据类型

虽然 Pinia 会帮推导 TypeScript 的数据类型,但有时候可能不太够用,比如下面这段代码,请留意代码注释的说明:

ts
// ...
+export const useStore = defineStore('main', {
+  state: () => {
+    return {
+      message: 'Hello World',
+      // 添加了一个随机消息数组
+      randomMessages: [],
+    }
+  },
+  // ...
+})

randomMessages 的预期应该是一个字符串数组 string[] ,但是这个时候 Pinia 会帮推导成 never[] ,那么类型就对不上了。

这种情况下就需要手动指定 randomMessages 的类型,可以通过 as 来指定:

ts
// ...
+export const useStore = defineStore('main', {
+  state: () => {
+    return {
+      message: 'Hello World',
+      // 通过 as 关键字指定 TS 类型
+      randomMessages: [] as string[],
+    }
+  },
+  // ...
+})

或者使用尖括号 <> 来指定:

ts
// ...
+export const useStore = defineStore('main', {
+  state: () => {
+    return {
+      message: 'Hello World',
+      // 通过尖括号指定 TS 类型
+      randomMessages: <string[]>[],
+    }
+  },
+  // ...
+})

这两种方式是等价的。

获取和更新 state

获取 state 有多种方法,略微有区别(详见下方各自的说明),但相同的是,他们都是响应性的。

WARNING

不能直接通过 ES6 解构的方式( e.g. const { message } = store ),那样会破坏数据的响应性。

使用 store 实例

用法上和 Vuex 很相似,但有一点区别是,数据直接是挂在 store 上的,而不是 store.state 上面!

TIP

e.g. Vuex 是 store.state.message , Pinia 是 store.message

所以,可以直接通过 store.message 直接调用 state 里的数据。

ts
import { defineComponent } from 'vue'
+import { useStore } from '@/stores'
+
+export default defineComponent({
+  setup() {
+    // 像 useRouter 那样定义一个变量拿到实例
+    const store = useStore()
+
+    // 直接通过实例来获取数据
+    console.log(store.message)
+
+    // 这种方式需要把整个 store 给到 template 去渲染数据
+    return {
+      store,
+    }
+  },
+})

但一些比较复杂的数据这样写会很长,所以有时候更推荐用下面介绍的 computed APIstoreToRefs API 等方式来获取。

在数据更新方面,在 Pinia 可以直接通过 Store 实例更新 state (这一点与 Vuex 有明显的不同,更改 Vuex 的 store 中的状态的唯一方法是提交 mutation),所以如果要更新 message ,只需要像下面这样,就可以更新 message 的值了!

ts
store.message = 'New Message.'

使用 computed API

现在 state 里已经有定义好的数据了,下面这段代码是在 Vue 组件里导入的 Store ,并通过计算数据 computed 拿到里面的 message 数据传给 template 使用。

vue
<script lang="ts">
+import { computed, defineComponent } from 'vue'
+import { useStore } from '@/stores'
+
+export default defineComponent({
+  setup() {
+    // 像 useRouter 那样定义一个变量拿到实例
+    const store = useStore()
+
+    // 通过计算拿到里面的数据
+    const message = computed(() => store.message)
+    console.log('message', message.value)
+
+    // 传给 template 使用
+    return {
+      message,
+    }
+  },
+})
+</script>

使用 store 实例 以及 使用 storeToRefs API 不同,这个方式默认情况下无法直接更新 state 的值。

TIP

这里的定义的 message 变量是一个只有 getter ,没有 setter 的 ComputedRef 数据,所以它是只读的。

如果要更新数据怎么办?

  1. 可以通过提前定义好的 Store Actions 方法进行更新。

  2. 在定义 computed 变量的时候,配置好 setter 的行为:

ts
// 其他代码和上一个例子一样,这里省略...
+
+// 修改:定义 computed 变量的时候配置 getter 和 setter
+const message = computed({
+  // getter 还是返回数据的值
+  get: () => store.message,
+  // 配置 setter 来定义赋值后的行为
+  set(newVal) {
+    store.message = newVal
+  },
+})
+
+// 此时不再抛出 Write operation failed: computed value is readonly 的警告
+message.value = 'New Message.'
+
+// store 上的数据已成功变成了 New Message.
+console.log(store.message)

使用 storeToRefs API

Pinia 还提供了一个 storeToRefs API 用于把 state 的数据转换为 ref 变量。

这是一个专门为 Pinia Stores 设计的 API ,类似于 toRefs ,区别在于,它会忽略掉 Store 上面的方法和非响应性的数据,只返回 state 上的响应性数据。

ts
import { defineComponent } from 'vue'
+import { useStore } from '@/stores'
+
+// 记得导入这个 API
+import { storeToRefs } from 'pinia'
+
+export default defineComponent({
+  setup() {
+    const store = useStore()
+
+    // 通过 storeToRefs 来拿到响应性的 message
+    const { message } = storeToRefs(store)
+    console.log('message', message.value)
+
+    return {
+      message,
+    }
+  },
+})

通过这个方式拿到的 message 变量是一个 Ref 类型的数据,所以可以像普通的 ref 变量一样进行读取和赋值。

ts
// 直接赋值即可
+message.value = 'New Message.'
+
+// store 上的数据已成功变成了 New Message.
+console.log(store.message)

使用 toRefs API

使用 storeToRefs API 部分所说,该 API 本身的设计就是类似于 toRefs ,所以也可以直接用 toRefs 把 state 上的数据转成 ref 变量。

ts
// 注意 toRefs 是 vue 的 API ,不是 Pinia
+import { defineComponent, toRefs } from 'vue'
+import { useStore } from '@/stores'
+
+export default defineComponent({
+  setup() {
+    const store = useStore()
+
+    // 跟 storeToRefs 操作都一样,只不过用 Vue 的这个 API 来处理
+    const { message } = toRefs(store)
+    console.log('message', message.value)
+
+    return {
+      message,
+    }
+  },
+})

详见 使用 toRefs 一节的说明,可以像普通的 ref 变量一样进行读取和赋值。

另外,像上面这样,对 store 执行 toRefs 会把 store 上面的 getters 、 actions 也一起提取,如果只需要提取 state 上的数据,可以这样做:

ts
// 只传入 store.$state
+const { message } = toRefs(store.$state)

使用 toRef API

toRef 是 toRefs 的兄弟 API ,一个是只转换一个字段,一个是转换所有字段,所以它也可以用来转换 state 数据变成 ref 变量。

ts
// 注意 toRef 是 vue 的 API ,不是 Pinia
+import { defineComponent, toRef } from 'vue'
+import { useStore } from '@/stores'
+
+export default defineComponent({
+  setup() {
+    const store = useStore()
+
+    // 遵循 toRef 的用法即可
+    const message = toRef(store, 'message')
+    console.log('message', message.value)
+
+    return {
+      message,
+    }
+  },
+})

详见 使用 toRef 一节的说明,可以像普通的 ref 变量一样进行读取和赋值。

使用 actions 方法

在 Vuex ,如果想通过方法来操作 state 的更新,必须通过 mutation 来提交;而异步操作需要更多一个步骤,必须先通过 action 来触发 mutation ,非常繁琐!

Pinia 所有操作都集合为 action ,无需区分同步和异步,按照平时的函数定义即可更新 state ,具体操作详见 管理 actions 一节。

批量更新 state

获取和更新 state 部分说的都是如何修改单个 state 数据,那么有时候要同时修改很多个,会显得比较繁琐。

如果写过 React 或者微信小程序,应该非常熟悉这些用法:

ts
// 下面不是 Vue 的代码,不要在的项目里使用
+
+// React
+this.setState({
+  foo: 'New Foo Value',
+  bar: 'New bar Value',
+})
+
+// 微信小程序
+this.setData({
+  foo: 'New Foo Value',
+  bar: 'New bar Value',
+})

Pinia 也提供了一个 $patch API 用于同时修改多个数据,它接收一个参数:

参数类型语法
partialState对象 / 函数store.$patch(partialState)

传入一个对象

当参数类型为对象时,key 是要修改的 state 数据名称, value 是新的值(支持嵌套传值),用法如下:

ts
// 继续用前面的数据,这里会打印出修改前的值
+console.log(JSON.stringify(store.$state))
+// 输出 {"message":"Hello World","randomMessages":[]}
+
+/**
+ * 注意这里,传入了一个对象
+ */
+store.$patch({
+  message: 'New Message',
+  randomMessages: ['msg1', 'msg2', 'msg3'],
+})
+
+// 这里会打印出修改后的值
+console.log(JSON.stringify(store.$state))
+// 输出 {"message":"New Message","randomMessages":["msg1","msg2","msg3"]}

对于简单的数据,直接修改成新值是非常好用的。

但有时候并不单单只是修改,而是要对数据进行拼接、补充、合并等操作,相对而言开销就会很大,这种情况下,更适合 传入一个函数 来处理。

TIP

使用这个方式时, key 只允许是实例上已有的数据,不可以提交未定义的数据进去。

强制提交的话,在 TypeScript 会抛出错误, JavaScript 虽然不会报错,但实际上, Store 实例上面依然不会有这个新增的非法数据。

传入一个函数

当参数类型为函数时,该函数会有一个入参 state ,是当前实例的 state ,等价于 store.$state ,用法如下:

ts
// 这里会打印出修改前的值
+console.log(JSON.stringify(store.$state))
+// 输出 {"message":"Hello World","randomMessages":[]}
+
+/**
+ * 注意这里,这次是传入了一个函数
+ */
+store.$patch((state) => {
+  state.message = 'New Message'
+
+  // 数组改成用追加的方式,而不是重新赋值
+  for (let i = 0; i < 3; i++) {
+    state.randomMessages.push(`msg${i + 1}`)
+  }
+})
+
+// 这里会打印出修改后的值
+console.log(JSON.stringify(store.$state))
+// 输出 {"message":"New Message","randomMessages":["msg1","msg2","msg3"]}

传入一个对象 比,不一定说就是哪种方式更好,通常要结合业务场景合理使用。

TIP

使用这个方式时,和 传入一个对象 一样只能修改已定义的数据,并且另外需要注意,传进去的函数只能是同步函数,不可以是异步函数!

如果还不清楚什么是同步和异步,可以阅读 同步和异步 JavaScript - MDN 一文。

全量更新 state

批量更新 state 了解到可以用 store.$patch 方法对数据进行批量更新操作,不过如其命名,这种方式本质上是一种 “补丁更新” 。

虽然可以对所有数据都执行一次 “补丁更新” 来达到 “全量更新” 的目的,但 Pinia 也提供了一个更好的办法。

从前面多次提到 state 数据可以通过 store.$state 来拿到,而这个属性本身是可以直接赋值的。

还是继续用上面的例子, state 上现在有 messagerandomMessages 这两个数据,那么要全量更新为新的值,就这么操作:

ts
store.$state = {
+  message: 'New Message',
+  randomMessages: ['msg1', 'msg2', 'msg3'],
+}

同样的,必须遵循 state 原有的数据和对应的类型。

TIP

该操作不会使 state 失去响应性。

重置 state

Pinia 提供了一个 $reset API 挂在每个实例上面,用于重置整颗 state 树为初始数据:

ts
// 这个 store 是上面定义好的实例
+store.$reset()

具体例子:

ts
// 修改数据
+store.message = 'New Message'
+console.log(store.message) // 输出 New Message
+
+// 3s 后重置状态
+setTimeout(() => {
+  store.$reset()
+  console.log(store.message) // 输出最开始的 Hello World
+}, 3000)

订阅 state

和 Vuex 一样, Pinia 也提供了一个用于订阅 state 的 $subscribe API 。

订阅 API 的 TS 类型

在了解这个 API 的使用之前,先看一下它的 TS 类型定义:

ts
// $subscribe 部分的 TS 类型
+// ...
+$subscribe(
+  callback: SubscriptionCallback<S>,
+  options?: { detached?: boolean } & WatchOptions
+): () => void
+// ...

可以看到, $subscribe 可以接受两个参数:

  1. 第一个入参是 callback 函数,必传
  2. 第二个入参是一些选项,可选

同时还会返回一个函数,执行后可以用于移除当前订阅,下面来看看具体用法。

添加订阅

$subscribe API 的功能类似于 watch ,但它只会在 state 被更新的时候才触发一次,并且在组件被卸载时删除(参考:组件的生命周期)。

订阅 API 的 TS 类型 可以看到,它可以接受两个参数,第一个参数是必传的 callback 函数,一般情况下默认用这个方式即可,使用例子:

ts
// 可以在 state 出现变化时,更新本地持久化存储的数据
+store.$subscribe((mutation, state) => {
+  localStorage.setItem('store', JSON.stringify(state))
+})

这个 callback 里面有 2 个入参:

入参作用
mutation本次事件的一些信息
state当前实例的 state

其中 mutation 包含了以下数据:

字段
storeId发布本次订阅通知的 Pinia 实例的唯一 ID(由 创建 Store 时指定)
type有 3 个值:返回 direct 代表 直接更改 数据;返回 patch object 代表是通过 传入一个对象 更改;返回 patch function 则代表是通过 传入一个函数 更改
events触发本次订阅通知的事件列表
payload通过 传入一个函数 更改时,传递进来的荷载信息,只有 typepatch object 时才有

如果不希望组件被卸载时删除订阅,可以传递第二个参数 options 用以保留订阅状态,传入一个对象。

可以简单指定为 { detached: true }

ts
store.$subscribe(
+  (mutation, state) => {
+    // ...
+  },
+  { detached: true }
+)

也可以搭配 watch API 的选项一起用。

移除订阅

添加订阅 部分已了解过,默认情况下,组件被卸载时订阅也会被一并移除,但如果之前启用了 detached 选项,就需要手动取消了。

前面在 订阅 API 的 TS 类型 里提到,在启用 $subscribe API 之后,会有一个函数作为返回值,这个函数可以用来取消该订阅。

用法非常简单,做一下简单了解即可:

ts
// 定义一个退订变量,它是一个函数
+const unsubscribe = store.$subscribe(
+  (mutation, state) => {
+    // ...
+  },
+  { detached: true }
+)
+
+// 在合适的时期调用它,可以取消这个订阅
+unsubscribe()

跟 watch API 的机制非常相似, 它也是返回 一个取消侦听的函数 用于移除指定的 watch 。

管理 getters ~new

状态树的结构 了解过, Pinia 的 getters 是用来计算数据的。

给 Store 添加 getter

TIP

如果对 Vue 的计算数据不是很熟悉或者没接触过的话,可以先阅读 数据的计算 这一节,以便有个初步印象,不会云里雾里。

添加普通的 getter

继续用刚才的 message ,来定义一个 Getter ,用于返回一句拼接好的句子。

ts
// src/stores/index.ts
+import { defineStore } from 'pinia'
+
+export const useStore = defineStore('main', {
+  state: () => ({
+    message: 'Hello World',
+  }),
+  // 定义一个 fullMessage 的计算数据
+  getters: {
+    fullMessage: (state) => `The message is "${state.message}".`,
+  },
+  // ...
+})

Options API 的 Computed 写法一样,也是通过函数来返回计算后的值, Getter 可以通过入参的 state 来拿到当前实例的数据(在 Pinia 里,官方更推荐使用箭头函数)。

添加引用 getter 的 getter

有时候可能要引用另外一个 getter 的值来返回数据,这个时候不能用箭头函数了,需要定义成普通函数而不是箭头函数,并在函数内部通过 this 来调用当前 Store 上的数据和方法。

继续在上面的例子里,添加多一个 emojiMessage 的 getter ,在返回 fullMessage 的结果的同时,拼接多一串 emoji 。

ts
export const useStore = defineStore('main', {
+  state: () => ({
+    message: 'Hello World',
+  }),
+  getters: {
+    fullMessage: (state) => `The message is "${state.message}".`,
+    // 这个 getter 返回了另外一个 getter 的结果
+    emojiMessage(): string {
+      return `🎉🎉🎉 ${this.fullMessage}`
+    },
+  },
+})

如果只写 JavaScript ,可能对这一条所说的限制觉得很奇怪,事实上用 JS 写箭头函数来引用确实不会报错,但如果用的是 TypeScript ,不按照这个写法,在 VSCode 提示和执行 TSC 检查的时候都会给抛出一条错误:

bash
src/stores/index.ts:9:42 - error TS2339:
+Property 'fullMessage' does not exist on type '{ message: string; } & {}'.
+
+9     emojiMessage: (state) => `🎉🎉🎉 ${state.fullMessage}`,
+                                           ~~~~~~~~~~~
+
+
+Found 1 error in src/stores/index.ts:9

另外关于普通函数的 TS 返回类型,官方建议显式的进行标注,就像这个例子里的 emojiMessage(): string 里的 : string

给 getter 传递参数

getter 本身是不支持参数的,但和 Vuex 一样,支持返回一个具备入参的函数,用来满足需求。

ts
import { defineStore } from 'pinia'
+
+export const useStore = defineStore('main', {
+  state: () => ({
+    message: 'Hello World',
+  }),
+  getters: {
+    // 定义一个接收入参的函数作为返回值
+    signedMessage: (state) => {
+      return (name: string) => `${name} say: "The message is ${state.message}".`
+    },
+  },
+})

调用的时候是这样:

ts
const signedMessage = store.signedMessage('Petter')
+console.log('signedMessage', signedMessage)
+// Petter say: "The message is Hello World".

这种情况下,这个 getter 只是调用的函数的作用,不再有缓存,如果通过变量定义了这个数据,那么这个变量也只是普通变量,不具备响应性。

ts
// 通过变量定义一个值
+const signedMessage = store.signedMessage('Petter')
+console.log('signedMessage', signedMessage)
+// Petter say: "The message is Hello World".
+
+// 2s 后改变 message
+setTimeout(() => {
+  store.message = 'New Message'
+
+  // signedMessage 不会变
+  console.log('signedMessage', signedMessage)
+  // Petter say: "The message is Hello World".
+
+  // 必须这样再次执行才能拿到更新后的值
+  console.log('signedMessage', store.signedMessage('Petter'))
+  // Petter say: "The message is New Message".
+}, 2000)

获取和更新 getter

getter 和 state 都属于数据管理,读取和赋值的方法是一样的,请参考上方 获取和更新 state 一节的内容。

管理 actions ~new

状态树的结构 提到了, Pinia 只需要用 actions 就可以解决各种数据操作,无需像 Vuex 一样区分为 mutations / actions 两大类。

给 Store 添加 action

可以为当前 Store 封装一些可以开箱即用的方法,支持同步和异步。

ts
// src/stores/index.ts
+import { defineStore } from 'pinia'
+
+export const useStore = defineStore('main', {
+  state: () => ({
+    message: 'Hello World',
+  }),
+  actions: {
+    // 异步更新 message
+    async updateMessage(newMessage: string): Promise<string> {
+      return new Promise((resolve) => {
+        setTimeout(() => {
+          // 这里的 this 是当前的 Store 实例
+          this.message = newMessage
+          resolve('Async done.')
+        }, 3000)
+      })
+    },
+    // 同步更新 message
+    updateMessageSync(newMessage: string): string {
+      // 这里的 this 是当前的 Store 实例
+      this.message = newMessage
+      return 'Sync done.'
+    },
+  },
+})

可以看到,在 action 里,如果要访问当前实例的 state 或者 getter ,只需要通过 this 即可操作,方法的入参完全不再受 Vuex 那样有固定形式的困扰。

TIP

在 action 里, this 是当前的 Store 实例,所以如果的 action 方法里有其他函数也要调用实例,请记得写成 箭头函数 来提升 this 。

调用 action

在 Pinia 中只要像普通的函数一样使用即可,不需要和 Vuex 一样调用 commit 或者 dispatch。

ts
export default defineComponent({
+  setup() {
+    const store = useStore()
+    const { message } = storeToRefs(store)
+
+    // 立即执行
+    console.log(store.updateMessageSync('New message by sync.'))
+
+    // 3s 后执行
+    store.updateMessage('New message by async.').then((res) => console.log(res))
+
+    return {
+      message,
+    }
+  },
+})

添加多个 Store ~new

到这里,对单个 Store 的配置和调用相信都已经清楚了,实际项目中会涉及到很多数据操作,还可以用多个 Store 来维护不同需求模块的数据状态。

这一点和 Vuex 的 Module 比较相似,目的都是为了避免状态树过于臃肿,但用起来会更为简单。

目录结构建议

建议统一存放在 src/stores 下面管理,根据业务需要进行命名,比如 user 就用来管理登录用户相关的状态数据。

bash
src
+└─stores
+   # 入口文件
+  ├─index.ts
+   # 多个 store
+  ├─user.ts
+  ├─game.ts
+  └─news.ts

里面暴露的方法就统一以 use 开头加上文件名,并以 Store 结尾,作为小驼峰写法,比如 user 这个 Store 文件里面导出的函数名就是:

ts
// src/stores/user.ts
+export const useUserStore = defineStore('user', {
+  // ...
+})

然后以 index.ts 里作为统一的入口文件, index.ts 里的代码写为:

ts
export * from './user'
+export * from './game'
+export * from './news'

这样在使用的时候,只需要从 @/stores 里导入即可,无需写完整的路径,例如,只需要这样:

ts
import { useUserStore } from '@/stores'

而无需这样:

ts
import { useUserStore } from '@/stores/user'

在 Vue 组件 / TS 文件里使用

这里以一个比较简单的业务场景举例,希望能够方便的理解如何同时使用多个 Store 。

假设目前有一个 userStore 是管理当前登录用户信息, gameStore 是管理游戏的信息,而 “个人中心” 这个页面需要展示 “用户信息” ,以及 “该用户绑定的游戏信息”,那么就可以这样:

ts
import { defineComponent, onMounted, ref } from 'vue'
+import { storeToRefs } from 'pinia'
+// 这里导入要用到的 Store
+import { useUserStore, useGameStore } from '@/stores'
+import type { GameItem } from '@/types'
+
+export default defineComponent({
+  setup() {
+    // 先从 userStore 获取用户信息(已经登录过,所以可以直接拿到)
+    const userStore = useUserStore()
+    const { userId, userName } = storeToRefs(userStore)
+
+    // 使用 gameStore 里的方法,传入用户 ID 去查询用户的游戏列表
+    const gameStore = useGameStore()
+    const gameList = ref<GameItem[]>([])
+    onMounted(async () => {
+      gameList.value = await gameStore.queryGameList(userId.value)
+    })
+
+    return {
+      userId,
+      userName,
+      gameList,
+    }
+  },
+})

再次提醒,切记每个 Store 的 ID 必须不同,如果 ID 重复,在同一个 Vue 组件 / TS 文件里定义 Store 实例变量的时候,会以先定义的为有效值,后续定义的会和前面一样。

如果先定义了 userStore :

ts
// 假设两个 Store 的 ID 一样
+const userStore = useUserStore() // 是想要的 Store
+const gameStore = useGameStore() // 得到的依然是 userStore 的那个 Store

如果先定义了 gameStore :

ts
// 假设两个 Store 的 ID 一样
+const gameStore = useGameStore() // 是想要的 Store
+const userStore = useUserStore() // 得到的依然是 gameStore 的那个 Store

Store 之间互相引用

如果在定义一个 Store 的时候,要引用另外一个 Store 的数据,也是很简单,回到那个 message 的例子,添加一个 getter ,它会返回一句问候语欢迎用户:

ts
// src/stores/message.ts
+import { defineStore } from 'pinia'
+
+// 导入用户信息的 Store 并启用它
+import { useUserStore } from './user'
+const userStore = useUserStore()
+
+export const useMessageStore = defineStore('message', {
+  state: () => ({
+    message: 'Hello World',
+  }),
+  getters: {
+    // 这里就可以直接引用 userStore 上面的数据了
+    greeting: () => `Welcome, ${userStore.userName}!`,
+  },
+})

假设现在 userName 是 Petter ,那么会得到一句对 Petter 的问候:

ts
const messageStore = useMessageStore()
+console.log(messageStore.greeting) // Welcome, Petter!

专属插件的使用 ~new

Pinia 拥有非常灵活的可扩展性,有专属插件可以开箱即用满足更多的需求场景。

如何查找插件

插件有统一的命名格式 pinia-plugin-* ,所以可以在 npmjs 上搜索这个关键词来查询目前有哪些插件已发布。

点击查询: pinia-plugin - npmjs

如何使用插件

这里以 pinia-plugin-persistedstate 为例,这是一个让数据持久化存储的 Pinia 插件。

TIP

数据持久化存储,指页面关闭后再打开,浏览器依然可以记录之前保存的本地数据,例如:浏览器原生的 localStorageIndexedDB ,或者是一些兼容多种原生方案并统一用法的第三方方案,例如: localForage

插件也是独立的 npm 包,需要先安装,再激活,然后才能使用。

激活方法会涉及到 Pinia 的初始化过程调整,这里不局限于某一个插件,通用的插件用法如下(请留意代码注释):

ts
// src/main.ts
+import { createApp } from 'vue'
+import App from '@/App.vue'
+import { createPinia } from 'pinia' // 导入 Pinia
+import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' // 导入 Pinia 插件
+
+const pinia = createPinia() // 初始化 Pinia
+pinia.use(piniaPluginPersistedstate) // 激活 Pinia 插件
+
+createApp(App)
+  .use(pinia) // 启用 Pinia ,这一次是包含了插件的 Pinia 实例
+  .mount('#app')

使用前

Pinia 默认在页面刷新时会丢失当前变更的数据,没有在本地做持久化记录:

ts
// 其他代码省略
+const store = useMessageStore()
+
+// 假设初始值是 Hello World
+setTimeout(() => {
+  // 2s 后变成 Hello World!
+  store.message = store.message + '!'
+}, 2000)
+
+// 页面刷新后又变回了 Hello World

使用后

按照 persistedstate 插件的文档说明,在其中一个 Store 启用它,只需要添加一个 persist: true 的选项即可开启:

ts
// src/stores/message.ts
+import { defineStore } from 'pinia'
+import { useUserStore } from './user'
+
+const userStore = useUserStore()
+
+export const useMessageStore = defineStore('message', {
+  state: () => ({
+    message: 'Hello World',
+  }),
+  getters: {
+    greeting: () => `Welcome, ${userStore.userName}`,
+  },
+  // 这是按照插件的文档,在实例上启用了该插件,这个选项是插件特有的
+  persist: true,
+})

回到的页面,现在这个 Store 具备了持久化记忆的功能了,它会从 localStorage 读取原来的数据作为初始值,每一次变化后也会将其写入 localStorage 进行记忆存储。

ts
// 其他代码省略
+const store = useMessageStore()
+
+// 假设初始值是 Hello World
+setTimeout(() => {
+  // 2s 后变成 Hello World!
+  store.message = store.message + '!'
+}, 2000)
+
+// 页面刷新后变成了 Hello World!!
+// 再次刷新后变成了 Hello World!!!
+// 再次刷新后变成了 Hello World!!!!

可以在浏览器查看到 localStorage 的存储变化,以 Chrome 浏览器为例,按 F12 ,打开 Application 面板,选择 Local Storage ,可以看到以当前 Store ID 为 Key 的存储数据。

这是其中一个插件使用的例子,更多的用法请根据自己选择的插件的 README 说明操作。

Released under the MIT License.

+ + + + \ No newline at end of file diff --git a/plugin.html b/plugin.html new file mode 100644 index 00000000..271d8a2e --- /dev/null +++ b/plugin.html @@ -0,0 +1,807 @@ + + + + + + 插件的开发和使用 | Vue3 入门指南与实战案例 + + + + + + + + + + + + + +
Skip to content
目录

插件的开发和使用

在构建 Vue 项目的过程中,离不开各种开箱即用的插件支持,用以快速完成需求,避免自己造轮子。

在 Vue 项目里,可以使用针对 Vue 定制开发的专属插件,也可以使用无框架依赖的通用 JS 插件,插件的表现形式也是丰富多彩,既可以是功能的实现,也可以是组件的封装,本章将从插件的使用到亲自开发一个小插件的过程,逐一讲解。

温馨提示

这是笔者写于 2024.02.20 的补充提示,为读者解答为何前面的章节都未提及 script-setup 组件语法的信息。

笔者在执笔之初,考虑到降低初学过程中的各种心智负担,以及当时处于 script-setup 组件语法尚未完全稳定的背景下,所以在最后一章 高效开发 之前的章节,都只提供了组件的标准写法的讲解。

除非遇到框架进行了 Breaking Change (例如从 Vue 2 升级到了 Vue 3 这种大版本更新),否则标准写法是始终有效的,在笔者多年的学习经历和实际收获里,也是比较相信一点:不论学习什么语言或者框架,只要基础打的好,未来总是可以轻松地举一反三去解决遇到的各类问题。

因此如果读者通过各类脚手架创建的 Vue 3 项目,在组件看到 <script setup> 这种风格的组件,以及诸如 defineXxx 风格的 API ,这是 .vue 组件所支持的一种语法糖,可以随时在 高效开发 一章了解相关的 API 用法。

另外从 Vue 3.4 版本之后所新增的 API 未在本书的介绍范围内,请读者在阅读的过程中也可以查阅官方的 API 一起学习,不论是本书还是其他计算机书籍,官方文档永远是最好的查询手册。

TIP

语法糖(英语:Syntactic sugar)是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。

插件的安装和引入

前端工程化 十分普及的今天,可以说几乎所有要用到的插件,都可以在 npmjs 上搜到,除了官方提供的包管理器 npm ,也有很多种安装方式选择。

TIP

如果还不了解什么是包和包管理器,请先阅读 了解包和插件 一节的内容。

另外,每个包管理都可以配置镜像源,提升国内的下载速度,对此也可以先阅读 配置镜像源 一节了解。

虽然对于个人开发者来说,有一个用的顺手的包管理器就足够日常开发了,但是还是有必要多了解一下不同的包管理器,因为未来可能会面对团队协作开发、为开源项目贡献代码等情况,需要遵循团队要求的包管理机制(例如使用 Monorepo 架构的团队会更青睐于 yarn 或 pnpm 的 Workspace 功能)。

通过 npm 安装

npm 是 Node.js 自带的包管理器,平时通过 npm install 命令来安装各种 npm 包(比如 npm install vue-router ),就是通过这个包管理器来安装的。

如果包的下载速度太慢,可以通过以下命令管理镜像源:

bash
# 查看下载源
+npm config get registry
+
+# 绑定下载源
+npm config set registry https://registry.npmmirror.com
+
+# 删除下载源
+npm config rm registry

TIP

npm 的 lock 文件是 package-lock.json ,如果有管理多人协作仓库的需求,可以根据实际情况把它添加至 .gitignore 文件,便于统一团队的包管理。

通过 cnpm 安装

cnpm 是阿里巴巴推出的包管理工具,安装之后默认会使用 https://registry.npmmirror.com 这个镜像源。

它的安装命令和 npm 非常一致,通过 cnpm install 命令来安装(比如 cnpm install vue-router)。

在使用它之前,需要通过 npm 命令进行全局安装:

bash
npm install -g cnpm
+
+# 或者
+# npm install -g cnpm --registry=https://registry.npmmirror.com

TIP

cnpm 不生成 lock 文件,也不会识别项目下的 lock 文件,所以还是推荐使用 npm 或者其他包管理工具,通过绑定镜像源的方式来管理项目的包。

通过 yarn 安装

yarn 也是一个常用的包管理工具,和 npm 十分相似, npmjs 上的包,也会同步到 yarnpkg

也是需要全局安装才可以使用:

bash
npm install -g yarn

但是安装命令上会有点不同, yarn 是用 add 代替 install ,用 remove 代替 uninstall ,例如:

bash
# 安装单个包
+yarn add vue-router
+
+# 安装全局包
+yarn global add typescript
+
+# 卸载包
+yarn remove vue-router

而且在运行脚本的时候,可以直接用 yarn 来代替 npm run ,例如 yarn dev 相当于 npm run dev

yarn 默认绑定的是 https://registry.yarnpkg.com 的下载源,如果包的下载速度太慢,也可以配置镜像源,但是命令有所差异:

bash
# 查看镜像源
+yarn config get registry
+
+# 绑定镜像源
+yarn config set registry https://registry.npmmirror.com
+
+# 删除镜像源(注意这里是 delete )
+yarn config delete registry

TIP

yarn 的 lock 文件是 yarn.lock ,如果有管理多人协作仓库的需求,可以根据实际情况把它添加至 .gitignore 文件,便于统一团队的包管理。

通过 pnpm 安装

pnpm 是包管理工具的一个后起之秀,主打快速的、节省磁盘空间的特色,用法跟其他包管理器很相似,没有太多的学习成本, npm 和 yarn 的命令它都支持。

也是必须先全局安装它才可以使用:

bash
npm install -g pnpm

目前 pnpm 在开源社区的使用率越来越高,包括接触最多的 Vue / Vite 团队也在逐步迁移到 pnpm 来管理依赖。

pnpm 的下载源使用的是 npm ,所以如果要绑定镜像源,按照 npm 的方式 处理就可以了。

相关阅读:

TIP

pnpm 的 lock 文件是 pnpm-lock.yaml ,如果有管理多人协作仓库的需求,可以根据实际情况把它添加至 .gitignore 文件,便于统一团队的包管理。

通过 CDN 安装

大部分插件都会提供一个 CDN 版本,让可以在 .html 文件里通过 <script> 标签引入。

比如:

html
<script src="https://unpkg.com/vue-router"></script>

插件的引入

除了 CDN 版本是直接可用之外,其他通过 npm 、 yarn 等方式安装的插件,都需要在入口文件 main.ts 或者要用到的 .vue 文件里引入,比如:

ts
import { createRouter, createWebHistory } from 'vue-router'

因为本教程都是基于工程化开发,使用的 CLI 脚手架,所以这些内容暂时不谈及 CDN 的使用方式。

通常来说会有细微差别,但影响不大,插件作者也会在插件仓库的 README 或者使用文档里进行告知。

Vue 专属插件

这里特指 Vue 插件,通过 Vue Plugins 设计规范 开发出来的插件,在 npm 上通常是以 vue-xxx 这样带有 vue 关键字的格式命名(比如 vue-baidu-analytics)。

专属插件通常分为 全局插件单组件插件,区别在于,全局版本是在 main.ts 引入后 use,而单组件版本则通常是作为一个组件在 .vue 文件里引入使用。

全局插件的使用 ~new

在本教程最最前面的时候,特地说了一个内容就是 项目初始化 ,在这里有提到过就是需要通过 use 来初始化框架、插件。

全局插件的使用,就是在 main.ts 通过 import 引入,然后通过 use 来启动初始化。

在 Vue 2 ,全局插件是通过 Vue.use(xxx) 来启动,而现在,则需要通过 createAppuse,既可以单独一行一个 use ,也可以直接链式 use 下去。

参数

use 方法支持两个参数:

参数类型作用
pluginobject | function插件,一般是在 import 时使用的名称
optionsobject插件的参数,有些插件在初始化时可以配置一定的选项

基本的写法就是像下面这样:

ts
// main.ts
+import plugin1 from 'plugin1'
+import plugin2 from 'plugin2'
+import plugin3 from 'plugin3'
+import plugin4 from 'plugin4'
+
+createApp(App)
+  .use(plugin1)
+  .use(plugin2)
+  .use(plugin3, {
+    // plugin3's options
+  })
+  .use(plugin4)
+  .mount('#app')

大部分插件到这里就可以直接启动了,个别插件可能需要通过插件 API 去手动触发,在 npm 包的详情页或者 GitHub 仓库文档上,作者一般会告知使用方法,按照说明书操作即可。

单组件插件的使用 ~new

单组件的插件,通常自己本身也是一个 Vue 组件(大部分情况下都会打包为 JS 文件,但本质上是一个 Vue 的 Component )。

单组件的引入,一般都是在需要用到的 .vue 文件里单独 import ,然后挂到 <template /> 里去渲染,下面是一段模拟代码,理解起来会比较直观:

vue
<template>
+  <!-- 放置组件的渲染标签,用于显示组件 -->
+  <ComponentExample />
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, ref } from 'vue'
+import logo from '@/assets/logo.png'
+
+// 引入单组件插件
+import ComponentExample from 'a-component-example'
+
+export default defineComponent({
+  // 挂载组件模板
+  components: {
+    ComponentExample,
+  },
+})
+</script>

参考上面的代码还有注释,应该能大概了解如何使用单组件插件了吧!

通用 JS / TS 插件

也叫普通插件,这个 “普通” 不是指功能平平无奇,而是指它们无需任何框架依赖,可以应用在任意项目中,属于独立的 Library ,比如 axiosqrcodemd5 等等,在任何技术栈都可以单独引入使用,非 Vue 专属。

通用插件的使用非常灵活,既可以全局挂载,也可以在需要用到的组件里单独引入。

组件里单独引入方式:

ts
import { defineComponent } from 'vue'
+import md5 from '@withtypes/md5'
+
+export default defineComponent({
+  setup() {
+    const md5Msg = md5('message')
+  },
+})

全局挂载方法比较特殊,因为插件本身不是专属 Vue,没有 install 接口,无法通过 use 方法直接启动,下面有一小节内容单独讲这一块的操作,详见 全局 API 挂载

本地插件 ~new

插件也不全是来自于网上,有时候针对自己的业务,涉及到一些经常用到的功能模块,也可以抽离出来封装成项目专用的插件。

封装的目的

举个例子,比如在做一个具备用户系统的网站时,会涉及到手机短信验证码模块,在开始写代码之前,需要先要考虑到这些问题:

  1. 很多操作都涉及到下发验证码的请求,比如 “登录” 、 “注册” 、 “修改手机绑定” 、 “支付验证” 等等,代码雷同,只是接口 URL 或者参数不太一样

  2. 都是需要对手机号是否有传入、手机号的格式正确性验证等一些判断

  3. 需要对接口请求成功和失败的情况做一些不同的数据返回,但要处理的数据很相似,都是用于告知调用方当前是什么情况

  4. 返回一些 Toast 告知用户当前的交互结果

TIP

如果不把这一块的业务代码抽离出来,需要在每个用到的地方都写一次,不仅繁琐,而且以后一旦产品需求有改动,维护起来就惨了。

常用的封装类型

常用的本地封装方式有两种:一种是以 通用 JS / TS 插件 的形式,一种是以 Vue 专属插件 的形式。

关于这两者的区别已经在对应的小节有所介绍,接下来来看看如何封装它们。

开发本地通用 JS / TS 插件

一般情况下会以通用类型比较常见,因为大部分都是一些比较小的功能,而且可以很方便的在不同项目之间进行复用。

TIP

注:接下来会统一称之为 “通用插件” ,不论是基于 JavaScript 编写的还是 TypeScript 编写的。

项目结构

通常会把这一类文件都归类在 src 目录下的 libs 文件夹里,代表存放的是 Library 文件( JS 项目以 .js 文件存放, TS 项目以 .ts 文件存放)。

bash
vue-demo
+ # 源码文件夹
+├─src
+  # 本地通用插件
+ └─libs
+   ├─foo.ts
+   └─bar.ts
+
+ # 其他结构这里省略…
+
+└─package.json

这样在调用的时候,可以通过 @/libs/foo 来引入,或者配置了 alias 别名,也可以使用别名导入,例如 @libs/foo

设计规范与开发案例

在设计本地通用插件的时候,需要遵循 ES Module 模块设计规范 ,并且做好必要的代码注释(用途、入参、返回值等)。

TIP

如果还没有了解过 “模块” 的概念的话,可以先阅读 了解模块化设计 一节的内容。

一般来说,会有以下三种情况需要考虑。

只有一个默认功能

如果只有一个默认的功能,那么可以使用 export default 来默认导出一个函数。

例如需要封装一个打招呼的功能:

ts
// src/libs/greet.ts
+
+/**
+ * 向对方打招呼
+ * @param name - 打招呼的目标人名
+ * @returns 向传进来的人名返回一句欢迎语
+ */
+export default function greet(name: string): string {
+  return `Welcome, ${name}!`
+}

在 Vue 组件里就可以这样使用:

vue
<script lang="ts">
+import { defineComponent } from 'vue'
+// 导入本地插件
+import greet from '@libs/greet'
+
+export default defineComponent({
+  setup() {
+    // 导入的名称就是这个工具的方法名,可以直接调用
+    const message = greet('Petter')
+    console.log(message) // Welcome, Petter!
+  },
+})
+</script>
是一个小工具合集

如果有很多个作用相似的函数,那么建议放在一个文件里作为一个工具合集统一管理,使用 export 来导出里面的每个函数。

例如需要封装几个通过 正则表达式 判断表单的输入内容是否符合要求的函数:

ts
// src/libs/regexp.ts
+
+/**
+ * 手机号校验
+ * @param phoneNumber - 手机号
+ * @returns true=是手机号,false=不是手机号
+ */
+export function isMob(phoneNumber: number | string): boolean {
+  return /^1[3456789]\d{9}$/.test(String(phoneNumber))
+}
+
+/**
+ * 邮箱校验
+ * @param email - 邮箱地址
+ * @returns true=是邮箱地址,false=不是邮箱地址
+ */
+export function isEmail(email: string): boolean {
+  return /^[A-Za-z\d]+([-_.][A-Za-z\d]+)*@([A-Za-z\d]+[-.])+[A-Za-z\d]{2,4}$/.test(
+    email
+  )
+}

在 Vue 组件里就可以这样使用:

vue
<script lang="ts">
+import { defineComponent } from 'vue'
+// 需要用花括号 {} 来按照命名导出时的名称导入
+import { isMob, isEmail } from '@libs/regexp'
+
+export default defineComponent({
+  setup() {
+    // 判断是否是手机号
+    console.log(isMob('13800138000')) // true
+    console.log(isMob('123456')) // false
+
+    // 判断是否是邮箱地址
+    console.log(isEmail('example@example.com')) // true
+    console.log(isEmail('example')) // false
+  },
+})
+</script>

TIP

类似这种情况,就没有必要为 isMobisEmail 每个方法都单独保存一个文件了,只需要统一放在 regexp.ts 正则文件里维护。

包含工具及辅助函数

如果主要提供一个独立功能,但还需要提供一些额外的变量或者辅助函数用于特殊的业务场景,那么可以用 export default 导出主功能,用 export 导出其他变量或者辅助函数。

只有一个默认功能 这个打招呼例子的基础上修改一下,默认提供的是 “打招呼” 的功能,偶尔需要更热情的赞美一下,那么这个 “赞美” 行为就可以用这个方式来放到这个文件里一起维护。

ts
// src/libs/greet.ts
+
+/**
+ * 称赞对方
+ * @param name - 要称赞的目标人名
+ * @returns 向传进来的人名发出一句赞美的话
+ */
+export function praise(name: string): string {
+  return `Oh! ${name}! It's so kind of you!`
+}
+
+/**
+ * 向对方打招呼
+ * @param name - 打招呼的目标人名
+ * @returns 向传进来的人名发出一句欢迎语
+ */
+export default function greet(name: string): string {
+  return `Welcome, ${name}!`
+}

在 Vue 组件里就可以这样使用:

vue
<script lang="ts">
+import { defineComponent } from 'vue'
+// 两者可以同时导入使用
+import greet, { praise } from '@libs/greet'
+
+export default defineComponent({
+  setup() {
+    // 使用默认的打招呼
+    const message = greet('Petter')
+    console.log(message) // Welcome, Petter!
+
+    // 使用命名导出的赞美
+    const praiseMessage = praise('Petter')
+    console.log(praiseMessage) // Oh! Petter! It's so kind of you!
+  },
+})
+</script>

开发本地 Vue 专属插件

Vue 专属插件 部分已介绍过,这一类的插件只能给 Vue 使用,有时候自己的业务比较特殊,无法找到完全适用的 npm 包,那么就可以自己写一个!

项目结构

通常会把这一类文件都归类在 src 目录下的 plugins 文件夹里,代表存放的是 Plugin 文件( JS 项目以 .js 文件存放, TS 项目以 .ts 文件存放)。

bash
vue-demo
+ # 源码文件夹
+├─src
+  # 本地 Vue 插件
+ └─plugins
+   ├─foo.ts
+   └─bar.ts
+
+ # 其他结构这里省略…
+
+└─package.json

这样在调用的时候,可以通过 @/plugins/foo 来引入,或者配置了 alias 别名,也可以使用别名导入,例如 @plugins/foo

设计规范

在设计本地 Vue 插件的时候,需要遵循 Vue 官方撰写的 Vue Plugins 设计规范 ,并且做好必要的代码注释,除了标明插件 API 的 “用途、入参、返回值” 之外,最好在注释内补充一个 Example 或者 Tips 说明,功能丰富的插件最好直接写个 README 文档。

开发案例

全局插件开发并启用后,只需要在 main.ts 里导入并 use 一次,即可在所有的组件内使用插件的功能。

下面对全局插件进行一个开发示范,希望能给大家以后需要的时候提供思路参考。

TIP

单组件插件一般作为 npm 包发布,会借助 Webpack 、 Vite 或者 Rollup 单独构建,本地直接放到 components 文件夹下作为组件管理即可。

基本结构

插件支持导出两种格式的:一种是函数,一种是对象。

  1. 当导出为一个函数时, Vue 会直接调用这个函数,此时插件内部是这样子:
ts
export default function (app, options) {
+  // 逻辑代码...
+}
  1. 当导出为一个对象时,对象上面需要有一个 install 方法给 Vue , Vue 通过调用这个方法来启用插件,此时插件内部是这样子:
ts
export default {
+  install: (app, options) => {
+    // 逻辑代码...
+  },
+}

不论哪种方式,入口函数都会接受两个入参:

参数作用类型
appcreateApp 生成的实例App (从 'vue' 里导入该类型),见下方的案例演示
options插件初始化时的选项undefined 或一个对象,对象的 TS 类型由插件的选项决定

如果需要在插件初始化时传入一些必要的选项,可以定义一个对象作为 options ,这样只要在 main.tsuse 插件时传入第二个参数,插件就可以拿到它们:

ts
// src/main.ts
+createApp(App)
+  // 注意这里的第二个参数就是插件选项
+  .use(customPlugin, {
+    foo: 1,
+    bar: 2,
+  })
+  .mount('#app')
编写插件

这里以一个 自定义指令 为例,写一个用于管理自定义指令的插件,其中包含两个自定义指令:一个是判断是否有权限,一个是给文本高亮,文本高亮还支持一个插件选项。

ts
// src/plugins/directive.ts
+import type { App } from 'vue'
+
+// 插件选项的类型
+interface Options {
+  // 文本高亮选项
+  highlight?: {
+    // 默认背景色
+    backgroundColor: string
+  }
+}
+
+/**
+ * 自定义指令
+ * @description 保证插件单一职责,当前插件只用于添加自定义指令
+ */
+export default {
+  install: (app: App, options?: Options) => {
+    /**
+     * 权限控制
+     * @description 用于在功能按钮上绑定权限,没权限时会销毁或隐藏对应 DOM 节点
+     * @tips 指令传入的值是管理员的组别 id
+     * @example <div v-permission="1" />
+     */
+    app.directive('permission', (el, binding) => {
+      // 假设 1 是管理员组别的 id ,则无需处理
+      if (binding.value === 1) return
+
+      // 其他情况认为没有权限,需要隐藏掉界面上的 DOM 元素
+      if (el.parentNode) {
+        el.parentNode.removeChild(el)
+      } else {
+        el.style.display = 'none'
+      }
+    })
+
+    /**
+     * 文本高亮
+     * @description 用于给指定的 DOM 节点添加背景色,搭配文本内容形成高亮效果
+     * @tips 指令传入的值需要是合法的 CSS 颜色名称或者 Hex 值
+     * @example <div v-highlight="`cyan`" />
+     */
+    app.directive('highlight', (el, binding) => {
+      // 获取默认颜色
+      let defaultColor = 'unset'
+      if (
+        Object.prototype.toString.call(options) === '[object Object]' &&
+        options?.highlight?.backgroundColor
+      ) {
+        defaultColor = options.highlight.backgroundColor
+      }
+
+      // 设置背景色
+      el.style.backgroundColor =
+        typeof binding.value === 'string' ? binding.value : defaultColor
+    })
+  },
+}
启用插件

main.ts 全局启用插件,在启用的时候传入了第二个参数 “插件的选项” ,这里配置了个高亮指令的默认背景颜色:

ts
// src/main.ts
+import { createApp } from 'vue'
+import App from '@/App.vue'
+import directive from '@/plugins/directive' // 导入插件
+
+createApp(App)
+   // 自定义插件
+  .use(directive, {
+    highlight: {
+      backgroundColor: '#ddd',
+    },
+  })
+  .mount('#app')
使用插件

在 Vue 组件里使用:

vue
<template>
+  <!-- 测试 permission 指令 -->
+  <div>根据 permission 指令的判断规则:</div>
+  <div v-permission="1">这个可以显示</div>
+  <div v-permission="2">这个没有权限,会被隐藏</div>
+
+  <!-- 测试 highlight 指令 -->
+  <div>根据 highlight 指令的判断规则:</div>
+  <div v-highlight="`cyan`">这个是青色高亮</div>
+  <div v-highlight="`yellow`">这个是黄色高亮</div>
+  <div v-highlight="`red`">这个是红色高亮</div>
+  <div v-highlight>这个是使用插件初始化时设置的灰色</div>
+</template>

全局 API 挂载

对于一些使用频率比较高的插件方法,如果觉得在每个组件里单独导入再用很麻烦,也可以考虑将其挂载到 Vue 上,使其成为 Vue 的全局变量。

注:接下来的全局变量,都是指 Vue 环境里的全局变量,非 Window 下的全局变量。

回顾 Vue 2

在 Vue 2 ,可以通过 prototype 的方式来挂载全局变量,然后通过 this 关键字来从 Vue 原型上调用该方法。

md5 插件为例,在 main.ts 里进行全局 import,然后通过 prototype 去挂到 Vue 上。

ts
import Vue from 'vue'
+import md5 from 'md5'
+
+Vue.prototype.$md5 = md5

之后在 .vue 文件里,就可以这样去使用 md5

ts
const md5Msg = this.$md5('message')

了解 Vue 3 ~new

在 Vue 3 ,已经不再支持 prototype 这样使用了,在 main.ts 里没有了 Vue,在组件的生命周期里也没有了 this

如果依然想要挂载全局变量,需要通过全新的 globalProperties 来实现,在使用该方式之前,可以把 createApp 定义为一个变量再执行挂载。

定义全局 API ~new

如上,在配置全局变量之前,可以把初始化时的 createApp 定义为一个变量(假设为 app ),然后把需要设置为全局可用的变量或方法,挂载到 appconfig.globalProperties 上面。

ts
import md5 from 'md5'
+
+// 创建 Vue 实例
+const app = createApp(App)
+
+// 把插件的 API 挂载全局变量到实例上
+app.config.globalProperties.$md5 = md5
+
+// 也可以自己写一些全局函数去挂载
+app.config.globalProperties.$log = (text: string): void => {
+  console.log(text)
+}
+
+app.mount('#app')

全局 API 的替代方案

在 Vue 3 实际上并不是特别推荐使用全局变量,Vue 3 比较推荐按需引入使用,这也是在构建过程中可以更好的做到代码优化。

特别是针对 TypeScript , Vue 作者尤雨溪先生对于全局 API 的相关 PR 说明: Global API updates ,也是不建议在 TS 里使用。

那么确实是需要用到一些全局 API 怎么办?

对于一般的数据和方法,建议采用 Provide / Inject 方案,在根组件(通常是 App.vue )把需要作为全局使用的数据或方法 Provide 下去,在需要用到的组件里通过 Inject 即可获取到,或者使用 EventBus / Vuex / Pinia 等全局通信方案来处理。

npm 包的开发与发布

相信很多开发者都想发布一个属于自己的 npm 包,在实际的工作中,也会有一些公司出于开发上的便利,也会将一些常用的业务功能抽离为独立的 npm 包,提前掌握包的开发也是非常重要的能力,接下来将介绍如何从 0 到 1 开发一个 npm 包,并将其发布到 npmjs 上可供其他项目安装使用。

TIP

在开始本节内容之前,请先阅读或回顾以下两部分内容:

  1. 阅读 了解 package.json 一节,了解或重温 npm 包清单文件的作用
  2. 阅读 学习模块化设计 一节,了解或重温模块化开发的知识

常用的构建工具

平时项目里用到的 npm 包,也可以理解为是一种项目插件,一些很简单的包,其实就和编写 本地插件 一样,假设包的入口文件是 index.js ,那么可以直接在 index.js 里编写代码,再进行模块化导出。

其他项目里安装这个包之后就可以直接使用里面的方法了,这种方式适合非常非常简单的包,很多独立的工具函数包就是使用这种方式来编写包的源代码。

例如 is-number 这个包,每周下载量超过 6800 万次,它的源代码非常少:

js
/**
+ * 摘自 is-number 的入口文件
+ * @see https://github.com/jonschlinkert/is-number/blob/master/index.js
+ */
+module.exports = function (num) {
+  if (typeof num === 'number') {
+    return num - num === 0
+  }
+  if (typeof num === 'string' && num.trim() !== '') {
+    return Number.isFinite ? Number.isFinite(+num) : isFinite(+num)
+  }
+  return false
+}

再如 slash 这个包,每周下载量超过 5200 万次,它的源代码也是只有几行:

js
/**
+ * 摘自 slash 的入口文件
+ * @see https://github.com/sindresorhus/slash/blob/main/index.js
+ */
+export default function slash(path) {
+  const isExtendedLengthPath = /^\\\\\?\\/.test(path)
+
+  if (isExtendedLengthPath) {
+    return path
+  }
+
+  return path.replace(/\\/g, '/')
+}

但这一类包通常是提供很基础的功能实现,更多时候需要自己开发的包更倾向于和框架、和业务挂钩,涉及到非 JavaScript 代码,例如 Vue 组件的编译、 Less 等 CSS 预处理器编译、 TypeScript 的编译等等,如果不通过构建工具来处理,那么发布后这个包的使用就会有诸多限制,需要满足和开发这个包时一样的开发环境才能使用,这对于使用者来说非常不友好。

因此大部分 npm 包的开发也需要用到构建工具来转换项目源代码,统一输出为一个兼容性更好、适用性更广的 JavaScript 文件,配合 .d.ts 文件的类型声明,使用者可以不需要特地配置就可以开箱即用,非常方便,非常友好。

传统的 Webpack 可以用来构建 npm 包文件,但按照目前更主流的技术选项,编译结果更干净更迷的当属 Rollup ,但 Rollup 需要配置很多插件功能,这对于刚接触包开发的开发者来说学习成本比较高,而 Vite 的出现则解决了这个难题,因为 Vite 的底层是基于 Rollup 来完成构建,上层则简化了很多配置上的问题,因此接下来将使用 Vite 来带领开发者入门 npm 包的开发。

TIP

在开始使用构建工具之前,请先在命令行使用 node -v 命令检查当前的 Node.js 版本号是否在构建工具的支持范围内,避免无法正常使用构建工具。

通常可以在构建工具的官网查询到其支持的 Node 版本,以 Vite 为例,可以在 Vite 官网的 Node 支持 一节了解到当前只能在 Node 14.18+ / 16+ 版本上使用 Vite 。

当构建工具所支持的 Node 版本和常用的 Node 版本出现严重冲突时,推荐使用 nvm / nvm-windows 或者 n 等 Node 版本管理工具安装多个不同版本的 Node ,即可根据开发需求很方便的切换不同版本的 Node 进行开发。

项目结构与入口文件

在动手开发具体功能之前,先把项目框架搭起来,熟悉常用的项目结构,以及如何配置项目清单信息。

TIP

当前文档所演示的 hello-lib 项目已托管至 learning-vue3/hello-lib 仓库,可使用 Git 克隆命令拉取至本地:

bash
# 从 GitHub 克隆
+git clone https://github.com/learning-vue3/hello-lib.git
+
+# 如果 GitHub 访问失败,可以从 Gitee 克隆
+git clone https://gitee.com/learning-vue3/hello-lib.git

成品项目可作为学习过程中的代码参考,但更建议按照教程的讲解步骤,从零开始亲手搭建一个新项目并完成 npm 包的开发流程,可以更有效的提升学习效果。

初始化项目

首先需要初始化一个 Node 项目,打开命令行工具,先使用 cd 命令进入平时存放项目的目录,再通过 mkdir 命令创建一个项目文件夹,这里起名为 hello-lib

bash
# 创建一个项目文件夹
+mkdir hello-lib

创建了项目文件夹之后,使用 cd 命令进入项目,执行 Node 的项目初始化命令:

bash
# 进入项目文件夹
+cd hello-lib
+
+# 执行初始化,使其成为一个 Node 项目
+npm init -y

此时 hello-lib 目录下会生成一个 package.json 文件,由于后面还需要手动调整该文件的信息,所以初始化的时候可以添加 -y 参数使用默认的初始化数据直接生成该文件,跳过答题环节。

配置包信息

对一个 npm 包来说,最重要的文件莫过于 package.json 项目清单,其中有三个字段是必填的:

字段是否必填作用
name必填npm 包的名称,遵循 项目名称的规则
version必填npm 包的版本号,遵循 语义化版本号的规则
main必填项目的入口文件,通常指向构建产物所在目录的某个文件,该文件通常包含了所有模块的导出。

如果只指定了 main 字段,则使用 requireimport 以及浏览器访问 npm 包的 CDN 时,都将默认调用该字段指定的入口文件。

如果有指定 modulebrowser 字段,则通常对应 cjs 格式的文件,对应 CommonJS 规范。
module当项目使用 import 引入 npm 包时对应的入口文件,通常指向一个 es 格式的文件,对应 ES Module 规范。
browser当项目使用了 npm 包的 CDN 链接,在浏览器访问页面时的入口文件,通常指向一个 umd 格式的文件,对应 UMD 规范。
types一个 .d.ts 类型声明文件,包含了入口文件导出的方法 / 变量的类型声明,如果项目有自带类型文件,那么在使用者在使用 TypeScript 开发的项目里,可以得到友好的类型提示
files指定发布到 npm 上的文件范围,格式为 string[] 支持配置多个文件名或者文件夹名称。

通常可以只指定构建的输出目录,例如 dist 文件夹,如果不指定,则发布的时候会把所有源代码一同发布。

其中 mainmodulebrowser 三个入口文件对应的文件格式和规范,通常都是交给构建工具处理,无需手动编写,开发者只需要维护一份源码即可编译出不同规范的 JS 文件, types 对应的类型声明文件也是由工具来输出,无需手动维护。

而其他的字段可以根据项目的性质决定是否补充,以下是 hello-lib 的基础信息示例:

json
{
+  "name": "@learning-vue3/lib",
+  "version": "1.0.0",
+  "description": "A library demo for learning-vue3.",
+  "author": "chengpeiquan <chengpeiquan@chengpeiquan.com>",
+  "homepage": "https://github.com/learning-vue3/hello-lib",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/learning-vue3/hello-lib.git"
+  },
+  "license": "MIT",
+  "files": ["dist"],
+  "main": "dist/index.cjs",
+  "module": "dist/index.mjs",
+  "browser": "dist/index.min.js",
+  "types": "dist/index.d.ts",
+  "keywords": ["library", "demo", "example"],
+  "scripts": {
+    "build": "vite build"
+  }
+}

此时 mainmodulebrowsertypes 字段对应的文件还不存在,它们将在项目执行 npm run build 构建之后才会产生。

另外,入口文件使用了不同规范对应的文件扩展名,也可以统一使用 .js 扩展名,通过文件名来区分,例如 es 格式使用 index.es.js

scripts 字段则配置了一个 build 命令,这里使用了 Vite 的构建命令来打包项目,这个过程会读取 Vite 的配置文件 vite.config.ts ,关于该文件的配置内容将在下文继续介绍。

安装开发依赖

本次的 npm 包将使用 Vite 进行构建,使用 TypeScript 编写源代码,由于 Vite 本身对 TypeScript 进行了支持,因此只需要将 Vite 安装到开发依赖:

bash
# 添加 -D 选项将其安装到 devDependencies
+npm i -D vite

添加配置文件

配置包信息 的时候已提前配置了一个 npm run build 的命令,它将运行 Vite 来构建 npm 包的入口文件。

由于 Vite 默认是构建入口文件为 HTML 的网页应用,而开发 npm 包时入口文件是 JS / TS 文件,因此需要添加一份配置文件来指定构建的选项。

以下是本次的基础配置,可以完成最基本的打包,它将输出三个不同格式的入口文件,分别对应 CommonJS 、 ES Module 和 UMD 规范,分别对应 package.json 里 mainmodulebrowser 字段指定的文件。

ts
// vite.config.ts
+import { defineConfig } from 'vite'
+
+// https://cn.vitejs.dev/config/
+export default defineConfig({
+  build: {
+    // 输出目录
+    outDir: 'dist',
+    // 构建 npm 包时需要开启 “库模式”
+    lib: {
+      // 指定入口文件
+      entry: 'src/index.ts',
+      // 输出 UMD 格式时,需要指定一个全局变量的名称
+      name: 'hello',
+      // 最终输出的格式,这里指定了三种
+      formats: ['es', 'cjs', 'umd'],
+      // 针对不同输出格式对应的文件名
+      fileName: (format) => {
+        switch (format) {
+          // ES Module 格式的文件名
+          case 'es':
+            return 'index.mjs'
+          // CommonJS 格式的文件名
+          case 'cjs':
+            return 'index.cjs'
+          // UMD 格式的文件名
+          default:
+            return 'index.min.js'
+        }
+      },
+    },
+    // 压缩混淆构建后的文件代码
+    minify: true,
+  },
+})

添加入口文件

来到这里,最基础的准备工作已完成,接下来添加入口文件并尝试编译。

添加配置文件 时已指定了入口文件为 src/index.ts ,因此需要对应的创建该文件,并写入一个简单的方法,将用它来测试打包结果:

ts
// src/index.ts
+export default function hello(name: string) {
+  console.log(`Hello ${name}`)
+}

在命令行执行 npm run build 命令,可以看到项目下生成了 dist 文件夹,以及三个 JavaScript 文件,此时目录结构如下:

bash
hello-lib
+ # 构建产物的输出文件夹
+├─dist
+ ├─index.cjs
+ ├─index.min.js
+ └─index.mjs
+ # 依赖文件夹
+├─node_modules
+ # 源码文件夹
+├─src
+  # 入口文件
+ └─index.ts
+ # 项目清单信息
+├─package-lock.json
+├─package.json
+ # Vite 配置文件
+└─vite.config.ts

打开 dist 目录下的文件内容,可以看到虽然源码是使用 TypeScript 编写的,但最终输出的内容是按照指定的格式转换为 JavaScript 并且被执行了压缩和混淆,在这里将它们重新格式化,来看看转换后的结果。

这是 index.cjs 的文件内容,源码被转换为 CommonJS 风格的代码:

js
// dist/index.cjs
+'use strict'
+function l(o) {
+  console.log(`Hello ${o}`)
+}
+module.exports = l

这是 index.mjs 的内容,源码被转换为 ES Module 风格的代码:

js
// dist/index.mjs
+function o(l) {
+  console.log(`Hello ${l}`)
+}
+export { o as default }

这是 index.min.js 的内容,源码被转换为 UMD 风格的代码:

js
// dist/index.min.js
+;(function (e, n) {
+  typeof exports == 'object' && typeof module < 'u'
+    ? (module.exports = n())
+    : typeof define == 'function' && define.amd
+    ? define(n)
+    : ((e = typeof globalThis < 'u' ? globalThis : e || self), (e.hello = n()))
+})(this, function () {
+  'use strict'
+  function e(n) {
+    console.log(`Hello ${n}`)
+  }
+  return e
+})

来到这里,准备工作已就绪,下一步将开始进入工具包和组件包的开发。

开发 npm 包

这里先从最简单的函数库开始入门包的开发,为什么说它简单呢?因为只需要编写 JavaScript 或 TypeScript 就可以很好的完成开发工作。

在理解了包的开发流程之后,如果要涉及 Vue 组件包的开发,则安装相关的 Vue 的相关依赖、 Less 等 CSS 预处理器依赖,只要满足了编译条件,就可以正常构建和发布,它们的开发流程是一样的。

编写 npm 包代码

在开发的过程中,需要遵循模块化开发的要求,当前这个演示包使用 TypeScript 编码,就需要 使用 ES Module 来设计模块 ,如果对模块化设计还没有足够的了解,请先回顾相关的内容。

先在 src 目录下创建一个名为 utils.ts 的文件,写入以下内容:

ts
// src/utils.ts
+
+/**
+ * 生成随机数
+ * @param min - 最小值
+ * @param max - 最大值
+ * @param roundingType - 四舍五入类型
+ * @returns 范围内的随机数
+ */
+export function getRandomNumber(
+  min: number = 0,
+  max: number = 100,
+  roundingType: 'round' | 'ceil' | 'floor' = 'round'
+) {
+  return Math[roundingType](Math.random() * (max - min) + min)
+}
+
+/**
+ * 生成随机布尔值
+ */
+export function getRandomBoolean() {
+  const index = getRandomNumber(0, 1)
+  return [true, false][index]
+}

这里导出了两个随机方法,其中 getRandomNumber 提供了随机数值的返回,而 getRandomBoolean 提供了随机布尔值的返回,在源代码方面, getRandomBoolean 调用了 getRandomNumber 获取随机索引。

这是一个很常见的 npm 工具包的开发思路,包里的函数都使用了细粒度的编程设计,每一个函数都是独立的功能,在必要的情况下,函数 B 可以调用函数 A 来减少代码的重复编写。

在这里, utils.ts 文件已开发完毕,接下来需要将它导出的方法提供给包的使用者,请删除入口文件 src/index.ts 原来的测试内容,并输入以下新代码:

ts
// src/index.ts
+export * from './utils'

这代表将 utils.ts 文件里导出的所有方法或者变量,再次导出去,如果有很多个 utils.ts 这样的文件, index.ts 将作为一个统一的入口,统一的导出给构建工具去编译输出。

接下来在命令行执行 npm run build ,再分别看看 dist 目录下的文件变化:

此时的 index.cjs 文件,已经按照 CommonJS 规范转换了源代码:

js
// dist/index.cjs
+'use strict'
+Object.defineProperties(exports, {
+  __esModule: { value: !0 },
+  [Symbol.toStringTag]: { value: 'Module' },
+})
+function t(e = 0, o = 100, n = 'round') {
+  return Math[n](Math.random() * (o - e) + e)
+}
+function r() {
+  const e = t(0, 1)
+  return [!0, !1][e]
+}
+exports.getRandomBoolean = r
+exports.getRandomNumber = t

index.mjs 也按照 ES Module 规范进行了转换:

js
// dist/index.mjs
+function o(n = 0, t = 100, e = 'round') {
+  return Math[e](Math.random() * (t - n) + n)
+}
+function r() {
+  const n = o(0, 1)
+  return [!0, !1][n]
+}
+export { r as getRandomBoolean, o as getRandomNumber }

index.min.js 同样正常按照 UMD 风格转换成了 JavaScript 代码:

js
// dist/index.min.js
+;(function (e, n) {
+  typeof exports == 'object' && typeof module < 'u'
+    ? n(exports)
+    : typeof define == 'function' && define.amd
+    ? define(['exports'], n)
+    : ((e = typeof globalThis < 'u' ? globalThis : e || self),
+      n((e.hello = {})))
+})(this, function (e) {
+  'use strict'
+  function n(o = 0, u = 100, d = 'round') {
+    return Math[d](Math.random() * (u - o) + o)
+  }
+  function t() {
+    const o = n(0, 1)
+    return [!0, !1][o]
+  }
+  ;(e.getRandomBoolean = t),
+    (e.getRandomNumber = n),
+    Object.defineProperties(e, {
+      __esModule: { value: !0 },
+      [Symbol.toStringTag]: { value: 'Module' },
+    })
+})

对 npm 包进行本地调试

开发或者迭代了一个 npm 包之后,不建议直接发布,可以在本地进行测试,直到没有问题了再发布到 npmjs 上供其他人使用。

npm 提供了一个 npm link 命令供开发者本地联调,假设 path/to/my-library 是一个 npm 包的项目路径, path/to/my-project 是一个调试项目的所在路径,那么通过以下步骤可以在 my-project 里本地调试 my-library 包。

TIP

关于 path/to 的典故,可以查看 #294 了解。

创建本地软链接

先在 my-library npm 包项目里执行 npm link 命令,创建 npm 包的本地软链接:

bash
# 进入 npm 包项目所在的目录
+cd path/to/my-library
+
+# 创建 npm 包的本地软链接
+npm link

运行了以上命令之后,意味着刚刚开发好的 npm 包,已经被成功添加到了 Node 的全局安装目录下,可以在命令行运行以下命令查看全局安装目录的位置:

bash
npm prefix -g

假设 {prefix} 是全局安装目录,刚刚这个包在 package.json 里的包名称是 my-library ,那么在 {prefix}/node_modules/my-library 这个目录下可以看到被软链接了一份项目代码。

TIP

软链接( Symbolic Link / Symlink / Soft Link ),是指通过指定路径来指向文件或目录,操作系统会自动将其解释为另一个文件或目录的路径,因此软链接被删除或修改不会影响源文件,而源文件的移动或者删除,不会自动更新软链接,这一点和快捷方式的作用比较类似。

自此已经对这个 npm 包完成了一次 “本地发布” ,接下来就要在调试项目里进行本地关联。

关联本地软链接

my-project 调试项目里执行语法为 npm link [<package-spec>] 的 link 命令,关联 npm 包的本地软链接。

TIP

这里的 [<package-spec>] 参数,可以是包名称,也可以是 npm 包项目所在的路径。

bash
# 进入调试项目所在的目录
+cd path/to/my-project
+
+# 通过 npm 包的包名称关联本地软链接
+npm link my-library

如果通过 npm 包名称关联失败,例如返回了如下信息:

bash
 npm link my-library
+npm ERR! code E404
+npm ERR! 404 Not Found - GET https://registry.npmjs.org/my-library - Not found
+npm ERR! 404
+npm ERR! 404  'my-library@*' is not in this registry.
+npm ERR! 404
+npm ERR! 404 Note that you can also install from a
+npm ERR! 404 tarball, folder, http url, or git url.

这种情况通常出现于本地 npm 包还没有在 npmjs 上进行过任意版本的发布,而包管理器又找不到本地全局安装目录的软链接,就会去 npm 源找,都找不到就会返回 404 的报错,针对这种情况,也可以使用 npm 包项目的路径进行关联:

bash
# 进入调试项目所在的目录
+cd path/to/my-project
+
+# 通过 npm 包的项目路径关联本地软链接
+npm link path/to/my-library

至此,就完成了调试项目对该 npm 包在本地的 “安装” ,此时在 my-project 这个调试项目的 node_modules 目录下也会创建一个软链接,指向 my-library 所在的目录。

回归当前的演示包项目,先创建一个基于 TypeScript 的 Vue 新项目作为调试项目,在关联了本地 npm 包之后,就可以在调试项目里编写如下代码,测试 npm 包里的方法是否可以正常使用:

ts
// 请将 `@learning-vue3/lib` 更换为实际的包名称
+import { getRandomNumber } from '@learning-vue3/lib'
+
+const num = getRandomNumber()
+console.log(num)

启动 npm run dev 的调试命令并打开本地调试页面,就可以在浏览器控制台正确的打印出了随机结果。

因为本包还支持 UMD 规范,所以也可以在 HTML 页面通过普通的 <script /> 标签直接引入 dist 目录下的文件测试将来引入 CDN 时的效果,可以在 npm 包项目下创建一个 demo 目录,并添加一个 index.html 文件到该目录下,并写入以下内容:

html
<!-- demo/index.html -->
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Library Demo</title>
+  </head>
+  <body>
+    <!-- 这里引入的是 UMD 规范的文件 -->
+    <script src="../dist/index.min.js"></script>
+    <script>
+      /**
+       * UMD 规范的文件会有一个全局变量
+       * 由 vite.config.ts 的 `build.lib.name` 决定
+       */
+      console.log(hello)
+
+      /**
+       * 所有的方法会挂在这个全局变量上
+       * 类似于 jQuery 的 $.xxx() 那样使用
+       */
+      const num = hello.getRandomNumber()
+      console.log(num)
+    </script>
+  </body>
+</html>

在浏览器打开该 HTML 文件并唤起控制台,一样可以看到随机结果的打印记录。

添加版权注释

很多知名项目在 Library 文件的开头都会有一段版权注释,它的作用除了声明版权归属之外,还会告知使用者关于项目的主页地址、版本号、发布日期、 BUG 反馈渠道等信息。

例如很多开发者入门前端时使用过的经典类库 jQuery :

js
// https://cdn.jsdelivr.net/npm/jquery@3.6.1/dist/jquery.js
+
+/*!
+ * jQuery JavaScript Library v3.6.1
+ * https://jquery.com/
+ *
+ * Includes Sizzle.js
+ * https://sizzlejs.com/
+ *
+ * Copyright OpenJS Foundation and other contributors
+ * Released under the MIT license
+ * https://jquery.org/license
+ *
+ * Date: 2022-08-26T17:52Z
+ */
+( function( global, factory ) {
+// ...

又如流行的 JavaScript 工具库 Lodash :

js
// https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js
+
+/**
+ * @license
+ * Lodash <https://lodash.com/>
+ * Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
+ * Released under MIT license <https://lodash.com/license>
+ * Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
+ * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+ */
+(function(){
+// ...

还有每次做轮播图一定会想到它的 Swiper :

js
// https://cdn.jsdelivr.net/npm/swiper@8.4.3/swiper-bundle.js
+
+/**
+ * Swiper 8.4.3
+ * Most modern mobile touch slider and framework
+ * with hardware accelerated transitions
+ * https://swiperjs.com
+ *
+ * Copyright 2014-2022 Vladimir Kharlampidi
+ *
+ * Released under the MIT License
+ *
+ * Released on: October 6, 2022
+ */
+(function (global, factory) {
+// ...

聪明的开发者肯定已经猜到了,这些版权注释肯定不是手动添加的,那么它们是如何自动生成的呢?

npm 社区提供了非常多开箱即用的注入插件,通常可以通过 “当前使用的构建工具名称” 加上 “plugin banner” 这样的关键字,在 npmjs 网站上搜索是否有相关的插件,以当前使用的 Vite 为例,可以通过 vite-plugin-banner 实现版权注释的自动注入。

回到 hello-lib 项目,安装该插件到 devDependencies :

bash
npm i -D vite-plugin-banner

根据插件的文档建议,打开 vite.config.ts 文件,将其导入,并通过读取 package.json 的信息来生成常用的版权注释信息:

ts
// vite.config.ts
+import { defineConfig } from 'vite'
+// 导入版权注释插件
+import banner from 'vite-plugin-banner'
+// 导入 npm 包信息
+import pkg from './package.json'
+
+// https://cn.vitejs.dev/config/
+export default defineConfig({
+  // 其他选项保持不变
+  // ...
+  plugins: [
+    // 新增 banner 插件的启用,传入 package.json 的字段信息
+    banner(
+      `/**\n * name: ${pkg.name}\n * version: v${pkg.version}\n * description: ${pkg.description}\n * author: ${pkg.author}\n * homepage: ${pkg.homepage}\n */`
+    ),
+  ],
+})

再次运行 npm run build 命令,打开 dist 目录下的 Library 文件,可以看到都成功添加了一段版权注释:

js
// dist/index.mjs
+
+/**
+ * name: @learning-vue3/lib
+ * version: v1.0.0
+ * description: A library demo for learning-vue3.
+ * author: chengpeiquan <chengpeiquan@chengpeiquan.com>
+ * homepage: https://github.com/learning-vue3/hello-lib
+ */
+function o(n = 0, t = 100, e = 'round') {
+  return Math[e](Math.random() * (t - n) + n)
+}
+function r() {
+  const n = o(0, 1)
+  return [!0, !1][n]
+}
+export { r as getRandomBoolean, o as getRandomNumber }

这样其他开发者如果在使用过程中遇到了问题,就可以轻松找到插件作者的联系方式了!

TIP

请根据实际的 package.json 存在的字段信息调整 banner 内容。

生成 npm 包的类型声明

虽然到这里已经得到一个可以运行的 JavaScript Library 文件,在 JavaScript 项目里使用是完全没有问题的,但还不建议直接发布到 npmjs 上,因为目前的情况下在 TypeScript 项目并不能完全兼容,还需要生成一份 npm 包的类型声明文件。

为什么需要类型声明

如果在上一小节 关联本地软链接 创建 Vue 调试项目时,也是使用了 TypeScript 版本的 Vue 项目,会遇到 VSCode 在下面这句代码上:

ts
import { getRandomNumber } from '@learning-vue3/lib'

在包名称 '@learning-vue3/lib' 的位置提示了一个红色波浪线,把鼠标移上去会显示这么一段话:

无法找到模块 “@learning-vue3/lib” 的声明文件。 “D:/Project/demo/hello-lib/dist/index.cjs” 隐式拥有 "any" 类型。
尝试使用 npm i --save-dev @types/learning-vue3__lib (如果存在),或者添加一个包含 declare module '@learning-vue3/lib'; 的新声明 (.d.ts) 文件 ts(7016)

此时在命令行运行 Vue 调试项目的打包命令 npm run build ,也会遇到打包失败的报错,控制台同样反馈了这个问题:缺少声明文件。

bash
 npm run build
+
+> hello-vue3@0.0.0 build
+> vue-tsc --noEmit && vite build
+
+src/App.vue:8:30 - error TS7016: Could not find a declaration file for module '@learning-vue3/lib'. 'D:/Project/demo/hello-lib/dist/index.cjs' implicitly has an 'any' type.
+  Try `npm i --save-dev @types/learning-vue3__lib` if it exists or add a new declaration (.d.ts) file containing `declare module '@learning-vue3/lib';`
+
+8 import { getRandomNumber } from '@learning-vue3/lib'
+                               ~~~~~~~~~~~~~~~~~~~~
+
+
+Found 1 error in src/App.vue:8

虽然使用者可以按照报错提示,在调试项目下创建一个 d.ts 文件并写入以下内容来声明该 npm 包:

ts
declare module '@learning-vue3/lib'

但这需要每个使用者,或者说每个使用到这个包的项目都声明一次,对于使用者来说非常不友好, declare module 之后虽然不会报错了,但也无法获得 VSCode 对 npm 包提供的 API 进行 TS 类型的自动推导与类型提示、代码补全等功能支持。

主流的做法

细心的开发者在 npmjs 网站上搜索 npm 包时,会发现很多 npm 包在详情页的包名后面,跟随有一个蓝色的 TS 图标,鼠标移上去时,还会显示一句提示语:

This package contains built-in TypeScript declarations

例如上图的 @vue/reactivity , Vue 3 的响应式 API 包,就带有这个图标。

这表示带有这个图标的 npm 包,已包含内置的 TypeScript 声明,可以获得完善的 TS 类型推导和提示支持,开发过程中也可以获得完善的代码补全功能支持,提高开发效率,在 TypeScript 项目执行 npm run build 的时候也能够被成功打包。

以 @vue/reactivity 这个包为例,如果项目下安装有这个 npm 包,可以在

bash
# 基于项目根目录
+./node_modules/@vue/reactivity/dist/reactivity.d.ts

这个文件里查看 Vue 3 响应式 API 的类型声明,也可以通过该文件的 CDN 地址访问到其内容:

bash
https://cdn.jsdelivr.net/npm/@vue/reactivity@3.2.40/dist/reactivity.d.ts

生成 DTS 文件

有在 “快速上手 TypeScript ” 一章阅读过 了解 tsconfig.json 这节内容的开发者,应该对该文件有了一定的了解,如果还没有阅读过也没关系,可以先按照下方的步骤操作,接下来将分布说明如何生成 npm 包的 DTS 类型声明文件(以 .d.ts 为扩展名的文件)。

请先全局安装 typescript 这个包:

bash
npm install -g typescript

依然是在在命令行界面,回到 hello-lib 这个 npm 包项目的根目录,执行以下命令生成 tsconfig.json 文件:

bash
tsc --init

打开 tsconfig.json 文件,生成的文件里会有很多默认被注释掉的选项,请将以下几个选项取消注释,同时在 compilerOptions 字段的同级新增 include 字段,这几个选项都修改为如下配置:

json
{
+  "compilerOptions": {
+    "declaration": true,
+    "emitDeclarationOnly": true,
+    "declarationDir": "./dist"
+  },
+  "include": ["./src"]
+}

其中 compilerOptions 三个选项的意思是: .ts 源文件不编译为 .js 文件,只生成 .d.ts 文件并输出到 dist 目录; include 选项则告诉 TypeScript 编译器,只处理 src 目录下的 TS 文件。

修改完毕后,在命令行执行以下命令,它将根据 tsconfig.json 的配置对项目进行编译:

bash
tsc

可以看到现在的 dist 目录下多了 2 份 .d.ts 文件: index.d.ts 和 utils.d.ts 。

bash
hello-lib
+└─dist
+  ├─index.cjs
+  ├─index.d.ts
+  ├─index.min.js
+  ├─index.mjs
+  └─utils.d.ts

打开 dist/index.d.ts ,可以看到它的内容和 src/index.ts 是一样的,因为作为入口文件,只提供了模块的导出:

ts
// dist/index.d.ts
+export * from './utils'

再打开 dist/utils.d.ts ,可以看到它的内容如下,对比 src/utils.ts 的文件内容,它去掉了具体的功能实现,并且根据代码逻辑,转换成了 TypeScript 的类型声明:

ts
// dist/utils.d.ts
+/**
+ * 生成随机数
+ * @param min - 最小值
+ * @param max - 最大值
+ * @param roundingType - 四舍五入类型
+ * @returns 范围内的随机数
+ */
+export declare function getRandomNumber(
+  min?: number,
+  max?: number,
+  roundingType?: 'round' | 'ceil' | 'floor'
+): number
+/**
+ * 生成随机布尔值
+ */
+export declare function getRandomBoolean(): boolean

由于 hello-lib 项目的 package.json 已提前指定了类型声明文件指向:

json
{
+  "types": "dist/index.d.ts"
+}

因此可以直接回到调试 npm 包的 Vue 项目,此时 VSCode 对那句 import 语句的红色波浪线报错信息已消失不见,鼠标移到 getRandomNumber 这个方法上,也可以看到 VSCode 出现了该方法的类型提示,非常方便。

TIP

如果 VSCode 未能及时更新该包的类型,依然提示红色波浪线,可以重启 VSCode 再次查看。

再次运行 npm run build 命令构建调试项目,这一次顺利通过编译:

bash
 npm run build
+
+> hello-vue3@0.0.0 build
+> vue-tsc --noEmit && vite build
+
+vite v2.9.15 building for production...
+ 42 modules transformed.
+dist/assets/logo.03d6d6da.png             6.69 KiB
+dist/index.html                           0.42 KiB
+dist/assets/home.9a123f29.js              2.01 KiB / gzip: 1.01 KiB
+dist/assets/logo.db8b6a93.js              0.12 KiB / gzip: 0.13 KiB
+dist/assets/TransferStation.25db7d3e.js   0.29 KiB / gzip: 0.22 KiB
+dist/assets/bar.0e9da4c4.js               0.53 KiB / gzip: 0.37 KiB
+dist/assets/bar.09e673fa.css              0.22 KiB / gzip: 0.18 KiB
+dist/assets/home.6bd02f2a.css             0.62 KiB / gzip: 0.33 KiB
+dist/assets/index.60726771.css            0.47 KiB / gzip: 0.29 KiB
+dist/assets/index.aebbe022.js             79.87 KiB / gzip: 31.80 KiB

生成 DTS Bundle

初始化项目生成 DTS 文件 ,其实已经走完一个 npm 包的完整开发流程了,是可以提交发布了,但在发布之前,先介绍另外一个生成 DTS 文件的方式,可以根据实际情况选择使用。

请注意这里使用了 DTS Bundle 来称呼类型声明文件,这是因为直接使用 tsc 命令生成的 DTS 文件,是和源码目录的文件数量挂钩的,可以留意到在上一小节使用 tsc 命令生成声明文件后,在 hello-lib 项目中:

  • src 源码目录有 index.ts 和 utils.ts 两个文件
  • dist 输出目录也对应生成了 index.d.ts 和 utils.d.ts 两个文件

在一个大型项目里,源码的目录和文件非常多,意味着 DTS 文件也是非常多,这样的输出结构并不是特别友好。

在讲 npm 包对类型声明 主流的做法 的时候,提到了 Vue 响应式 API 的 npm 包是提供了一个完整的 DTS 文件,它包含了所有 API 的类型声明信息:

bash
./node_modules/@vue/reactivity/dist/reactivity.d.ts

这种将多个模块的文件内容合并为一个完整文件的行为通常称之为 Bundle ,本小节将介绍如何生成这种 DTS Bundle 文件。

继续回到 hello-lib 这个 npm 包项目,由于 tsc 本身不提供类型文件的合并,所以需要借助第三方依赖来实现,比较流行的第三方包有: dts-bundle-generatornpm-dtsdts-bundledts-generator 等等。

之前笔者在为公司开发 npm 工具包的时候都对它们进行了一轮体验,鉴于实际开发过程中遇到的一些编译问题,在这里选用问题最少的 dts-bundle-generator 进行开发演示,请先安装到 hello-lib 项目的 devDependencies :

bash
npm i -D dts-bundle-generator

dts-bundle-generator 支持在 package.json 里配置一个 script ,通过命令的形式在命令行生成 DTS Bundle ,也支持通过 JavaScript / TypeScript 编写函数来执行文件的生成,鉴于实际开发过程中使用函数生成 DTS Bundle 的场景比较多(例如 Monorepo 会有生成多个 Bundle 的使用场景),因此这里以函数的方式进行演示。

TIP

在使用 Git 等版本控制系统时,如果多个独立项目之间有关联,会把这些项目的代码都存储在同一个代码仓库集中管理,此时这个大型代码仓库就被称之为 Monorepo (其中 Mono 表示单一, Repo 是存储库 Repository 的缩写),当下许多大型项目都基于这种方法管理代码, Vue 3 在 GitHub 的代码仓库也是一个 Monorepo 。

请在 hello-lib 的根目录下,创建一个与 src 源码目录同级的 scripts 目录,用来存储源码之外的脚本函数。

将以下代码保存到 scripts 目录下,命名为 buildTypes.mjs :

js
// scripts/buildTypes.mjs
+import { writeFileSync } from 'fs'
+import { dirname, resolve } from 'path'
+import { fileURLToPath } from 'url'
+import { generateDtsBundle } from 'dts-bundle-generator'
+
+async function run() {
+  // 默认情况下 `.mjs` 文件需要自己声明 __dirname 变量
+  const __filename = fileURLToPath(import.meta.url)
+  const __dirname = dirname(__filename)
+
+  // 获取项目的根目录路径
+  const rootPath = resolve(__dirname, '..')
+
+  // 添加构建选项
+  // 插件要求是一个数组选项,支持多个入口文件
+  const options = [
+    {
+      filePath: resolve(rootPath, `./src/index.ts`),
+      output: {
+        noBanner: true,
+      },
+    },
+  ]
+
+  // 生成 DTS 文件内容
+  // 插件返回一个数组,返回的文件内容顺序同选项顺序
+  const dtses = generateDtsBundle(options, {
+    preferredConfigPath: resolve(rootPath, `./tsconfig.json`),
+  })
+  if (!Array.isArray(dtses) || !dtses.length) return
+
+  // 将 DTS Bundle 的内容输出成 `.d.ts` 文件保存到 dist 目录下
+  // 当前只有一个文件要保存,所以只取第一个下标的数据
+  const dts = dtses[0]
+  const output = resolve(rootPath, `./dist/index.d.ts`)
+  writeFileSync(output, dts)
+}
+run().catch((e) => {
+  console.log(e)
+})

接下来打开 hello-lib 的 package.json 文件,添加一个 build:types 的 script ,并在 build 命令中通过 && 符号设置为继发执行任务,当前所有的 scripts 如下:

json
{
+  "scripts": {
+    "build": "vite build && npm run build:types",
+    "build:types": "node scripts/buildTypes.mjs"
+  }
+}

TIP

继发执行:只有前一个任务执行成功,才继续执行下一个任务,任务与任务之间使用 && 符号连接。

接下来再运行 npm run build 命令,将在执行完 Vite 的 build 任务之后,再继续执行 DTS Bundle 的文件生成,可以看到现在的 dist 目录变成了如下,只会生成一个 .d.ts 文件:

bash
hello-lib
+└─dist
+  ├─index.cjs
+  ├─index.d.ts
+  ├─index.min.js
+  └─index.mjs

现在 index.d.ts 文件已经集合了源码目录下所有的 TS 类型,变成了如下内容:

ts
// dist/index.d.ts
+/**
+ * 生成随机数
+ * @param min - 最小值
+ * @param max - 最大值
+ * @param roundingType - 四舍五入类型
+ * @returns 范围内的随机数
+ */
+export declare function getRandomNumber(
+  min?: number,
+  max?: number,
+  roundingType?: 'round' | 'ceil' | 'floor'
+): number
+/**
+ * 生成随机布尔值
+ */
+export declare function getRandomBoolean(): boolean
+
+export {}

对于大型项目,将 DTS 文件集合为 Bundle 输出是一种主流的管理方式,非常建议使用这种方式来为 npm 包生成类型文件。

添加说明文档

作为一个完整的 npm 包,应该配备一份操作说明给使用者阅读,复杂的文档可以使用 VitePress 等文档程序独立部署,而简单的项目则只需要完善一份 README 即可。

请创建一个名为 README.md 的 Markdown 文件在项目根目录下,与 src 源码目录同级,该文件的文件名 README 推荐使用全大写,这是开源社区主流的命名方式,全大写的原因是为了与代码文件进行直观的区分。

编写 README 使用的 Markdown 是一种轻量级标记语言,可以使用易读易写的纯文本格式编写文档,以 .md 作为文件扩展名,当代码托管到 GitHub 仓库或者发布到 npmjs 等平台时, README 文件会作为项目的主页内容呈现。

为了方便学习,这里将一些常用的 Markdown 语法与 HTML 代码对比如下,可以看到书写方面非常的简洁:

Markdown 代码HTML 代码
# 一级标题<h1>一级标题</h1>
## 二级标题<h2>二级标题</h2>
### 三级标题<h3>三级标题</h3>
**加粗文本**<span style="font-weight: bold;">加粗文本</span>
[链接文本](https://example.com)<a href="https://example.com">链接文本</a>

更多的 Markdown 语法建议在 Markdown 教程网站 上学习。

下面附上一份常用的 README 模板:

md
# 项目名称
+
+写上项目用途的一句话简介。
+
+## 功能介绍
+
+1. 功能 1 一句话介绍
+2. 功能 2 一句话介绍
+3. 功能 3 一句话介绍
+
+## 在线演示
+
+如果有部署在线 demo ,可放上 demo 的访问地址。
+
+## 安装方法
+
+使用 npm : `npm install package-name`
+
+使用 CDN : `https://example.com/package-name`
+
+## 用法
+
+告诉使用者如何使用 npm 包。
+
+## 插件选项
+
+如果 npm 包是一个插件,并支持传递插件选项,在这里可以使用表格介绍选项的作用。
+
+| 选项名称 |  类型  |    作用    |
+| :------: | :----: | :--------: |
+|   foo    | string | 一句话介绍 |
+|   bar    | number | 一句话介绍 |
+
+更多内容请根据实际情况补充。

拥有完善的使用说明文档,会让 npm 包更受欢迎!

发布 npm 包

一个 npm 包开发完毕后,就可以进入发布阶段了,这一小节将讲解如何注册 npm 账号并发布到 npmjs 平台上供其他开发者下载使用。

TIP

在操作 npm 包发布之前,请先运行 npm config rm registry 命令取消 npm 镜像源的绑定,否则会发布失败,在 npm 包发布后,可以再重新 配置镜像源

注册 npm 账号

在发布 npm 包之前,请先在 npm 官网上注册一个账号:点击注册

接下来需要在命令行上登录该账号以操作发布命令,打开命令行工具,输入以下命令进行登录:

bash
npm login

按照命令行的提示输入在 npmjs 网站上注册的账号和密码即可完成登录,可以通过以下命令查看当前登录的账号名称,验证是否登录成功:

bash
npm whoami

在登录成功之后,命令行会记住账号的登录状态,以后的操作就无需每次都执行登录命令了。

TIP

以上操作也可以实用 npm adduser 命令代替,直接在命令行完成注册和登录。

将包发布到 npmjs

在 npm 上发布私有包需要进行付费,因此这里只使用公共包的发布作为演示和讲解,如果开发的是公司内部使用的 npm 包,只要源代码是私有仓库,也可以使用这种方式来发布,当前在这样做之前请先获得公司的同意。

对于一个普通命名的包,要发布到 npmjs 上非常简单,只需要执行 npm 包管理器自带的一个命令即可:

bash
npm publish

它默认会将这个包作为一个公共包发布,如果包名称合法并且没有冲突,则发布成功,可以在 npmjs 查询到,否则会返回错误信息告知原因,如果因为包名冲突导致的失败,可以尝试修改别的名称再次发布。

如果打算使用像 @vue/cli@vue/compiler-sfc 这样带有 @scope 前缀的作用域包名,需要先在 npmjs 的 创建新组织 页面创建一个组织,或者确保自己拥有 @scope 对应的组织发布权限。

@scope 作用域包默认会作为私有包发布,因此在执行发布命令的时候还需要加上一个 --access 选项,将其指定为 public 允许公开访问才可以发布成功:

bash
npm publish --access public

当前的 hello-lib 项目已发布到 npmjs ,可以查看该包的主页 @learning-vue3/lib ,也可以通过 npm 安装到项目里使用了:

bash
npm i @learning-vue3/lib

并且发布到 npmjs 上的包,都同时获得热门 CDN 服务的自动同步,可以通过包名称获取到 CDN 链接并通过 <script /> 标签引入到 HTML 页面里:

bash
# 使用 jsDelivr CDN
+https://cdn.jsdelivr.net/npm/@learning-vue3/lib
+
+# 使用 UNPKG CDN
+https://unpkg.com/@learning-vue3/lib

此时 CDN 地址对应的 npm 包文件内容,就如前文所述,调用了 package.json 里 browser 字段指定的 UMD 规范文件 dist/index.min.js

给 npm 包打 Tag

细心的开发者还会留意到,例如像 Vue 这样的包,在 npmjs 上的 版本列表 里有 Current Tags 和 Version History 的版本分类,其中 Version History 是默认的版本发布历史列表,而 Current Tags 则是在发布 npm 包的时候指定打的标签。

标签的好处是可以让使用者无需记住对应的版本号,而是使用一些更具备语义化的单词来安装指定版本,例如:

bash
# 安装最新版的 Vue 3 ,即截图里对应的 3.2.40 版本
+npm i vue@latest
+
+# 安装最新版的 Vue 2 ,即截图里对应的 2.7.10 版本
+npm i vue@v2-latest
+
+# 如果后续有功能更新的测试版,也可以通过标签安装
+npm i vue@beta

除了减少寻找版本号的麻烦外,一旦后续有版本更新,再次使用相同的标签安装,可以重新安装到该标签对应的最新版本,例如从 1.0.0-beta.1 升级到 1.0.0-beta.2 ,可以使用 @beta 标签再次安装来达到升级的目的。

在标签列表里,有一个 latest 的标签是发布 npm 包时自带的,对应该包最新的正式版本,安装 npm 包时如果不指定标签,则默认使用 latest 标签,以下两个安装操作是等价的:

bash
# 隐式安装 latest 标签对应的版本
+npm i vue
+
+# 显式安装 latest 标签对应的版本
+npm i vue@latest

同样的,当发布 npm 包时不指定标签,则该版本也会在发布后作为 @latest 标签对应的版本号。

其他标签则需要在发布时配合发布命令,使用 --tag 作为选项手动指定,以下命令将为普通包打上名为 alpha 的 Tag :

bash
npm publish --tag alpha

同理,如果是 @scope 作用域包也是在使用 --access 选项的情况下,继续追加一条 --tag 选项指定包的标签。

bash
npm publish --access public --tag alpha

TIP

请注意,如果是 Alpha 或者 Beta 版本,通常会在版本号上增加 -alpha.0-alpha.1 这样的 版本标识符,以便在发布正式版本的时候可以使用无标识符的相同版本号,以保证版本号在遵循 升级规则 下的连续性。

Released under the MIT License.

+ + + + \ No newline at end of file diff --git a/preface.html b/preface.html new file mode 100644 index 00000000..c6e37125 --- /dev/null +++ b/preface.html @@ -0,0 +1,24 @@ + + + + + + 前言 | Vue3 入门指南与实战案例 + + + + + + + + + + + + + +
Skip to content
目录

前言

Vue 3.0 从 2020 年 9 月份中旬正式发布,到 2022 年 2 月份代替 Vue 2 成为 Vue 的默认版本,经过长达一年半的市场验证,已经证明了它可以完美地支持常见的企业需求,适合投产使用,未来会被越来越多的企业所采用,只掌握 Vue 2 将远远不能满足企业的技能要求。

本书以 Vue.js 的 3.0 版本为核心技术栈,围绕 “前端工程化” 和 TypeScript 的知识点展开讲解,读者可以收获到:

  1. 了解如何入门前端工程化开发,掌握 Node.js 和 npm 的使用
  2. 掌握前端领域多年来趋势走高、带有类型支持的 TypeScript 语言
  3. 上手主流前端框架 Vue.js 的全新版本,并且在遇到常见问题时知道如何解决

本书的每一个小节都融入了笔者多年的开发实践经验,大部分知识点都搭配了通俗易懂的讲解和可实现的代码案例,在阅读的过程中可以亲自敲代码加强学习印象,毕竟上手一个新技术栈最快的方法,就是一边 Reading 一边 Coding 。

适合人群

本书作为一本入门类教程,主要面向以下读者人群:

  1. 掌握了基础的 HTML 页面编写知识,想学习一个主流前端框架的新手前端工程师
  2. 已经学会了 Vue 2 ,面对 Vue 3 的大版本更新,想快速上手使用的前端工程师
  3. 非职业前端开发,但涉及前端的工作,需要掌握一个主流前端框架的全栈工程师

内容说明

本书根据笔者多年的前端开发经验和一线团队管理经验,将 Vue 3 的知识点按照工程师做项目的顺序梳理成章,一步一步帮助工程师入门前端工程化和 Vue 3 的开发。

书中包含了很多在构建项目过程中容易遇到的问题点和解决方案,对 Vue 3 和 Vue 2 差异化较大的功能点还进行了版本之间的写法对比。

这一点和各个技术栈的官方文档有比较大的不同,官方文档更适合作为一本工具书,方便在需要的时候对 API 进行检索查询。

目前笔者所带领的前端团队已经全员使用 TypeScript 和 Vue 3 进行日常的开发工作,团队成员的学习过程都非常顺利,各位开发者可放心阅读。

TIP

对于从 Vue 2 升级到 Vue 3 之后变化很大的内容,文档的侧边栏添加了图标,如果对 Vue 2 版本已经很熟悉,可以针对带有图标的地方单独查阅。

注:当出现在二级标题时,说明这一节都是新的,如果只出现在三级标题,说明只有那一小部分变化比较大。

学习顺序

推荐按照本书章节的顺序学习,从前端工程化开始上手,到 TypeScript 语言的学习,再到使用 TypeScript 开发 Vue 3 项目,这是一个循序渐进的学习过程。

Vue.js 是一个 “渐进式” 的框架,它可以只用最基础的组件来开发一个小项目,也可以把相关生态引入进来组合成一个大项目(例如 Vue Router 、 Vuex 、 Pinia 等等),本书和 Vue.js 框架一样,也是一个 “渐进式” 的教程,对于本身已经有一定基础的开发者,也可以在遇到具体问题的时候,只阅读对应的部分内容。

在学习的过程中,推荐将已有的其他技术栈项目,使用 Vue 3 重写,使其可以复刻原来的功能,比如一个使用 jQuery 或者是 Vue 2 编写的活动页面或小工具,用 Vue 3 重新实现一遍,看看能否还原出原来的功能,如果能够成功实现,那么说明已经能够掌握 Vue 3 的使用了。

疑惑解答

在学习的过程中如果遇到什么问题无法解决,可以通过以下途径反馈:

  1. 在仓库里提 Issue 或者 Discussions
  2. 发送邮件到笔者的邮箱 chengpeiquan@chengpeiquan.com

如果阅读本书的内容觉得有帮助,欢迎到仓库给个 Star 鼓励

赏个罐头

如果觉得本书的内容有帮助,欢迎 购买纸质版书籍 ,或者捐赠一个猫罐头给作者家的三只猫猫,感恩!

作者养了一只黑色英短、一只红虎斑缅因和一只棕虎斑美短,欢迎在线吸猫:

版权许可

MIT License © 2020 chengpeiquan

Released under the MIT License.

+ + + + \ No newline at end of file diff --git a/router.html b/router.html new file mode 100644 index 00000000..71046f3c --- /dev/null +++ b/router.html @@ -0,0 +1,519 @@ + + + + + + 路由的使用 | Vue3 入门指南与实战案例 + + + + + + + + + + + + + +
Skip to content
目录

路由的使用

在传统的 Web 开发过程中,当需要实现多个站内页面时,以前需要写很多个 HTML 页面,然后通过 <a /> 标签来实现互相跳转。

在如今工程化模式下的前端开发,像 Vue 工程,可以轻松实现只用一个 HTML 文件,却能够完成多个站内页面渲染、跳转的功能,这就是路由。

TIP

从这里开始,所有包含到 .vue 文件引入的地方,可能会看到 @xx/xx.vue 这样的写法。

@viewssrc/views 的路径别名,@cpsrc/components 的路径别名。

路径别名可以在 vite.config.ts 等构建工具配置文件里添加 alias ,点击了解:添加项目配置

温馨提示

这是笔者写于 2024.02.20 的补充提示,为读者解答为何前面的章节都未提及 script-setup 组件语法的信息。

笔者在执笔之初,考虑到降低初学过程中的各种心智负担,以及当时处于 script-setup 组件语法尚未完全稳定的背景下,所以在最后一章 高效开发 之前的章节,都只提供了组件的标准写法的讲解。

除非遇到框架进行了 Breaking Change (例如从 Vue 2 升级到了 Vue 3 这种大版本更新),否则标准写法是始终有效的,在笔者多年的学习经历和实际收获里,也是比较相信一点:不论学习什么语言或者框架,只要基础打的好,未来总是可以轻松地举一反三去解决遇到的各类问题。

因此如果读者通过各类脚手架创建的 Vue 3 项目,在组件看到 <script setup> 这种风格的组件,以及诸如 defineXxx 风格的 API ,这是 .vue 组件所支持的一种语法糖,可以随时在 高效开发 一章了解相关的 API 用法。

另外从 Vue 3.4 版本之后所新增的 API 未在本书的介绍范围内,请读者在阅读的过程中也可以查阅官方的 API 一起学习,不论是本书还是其他计算机书籍,官方文档永远是最好的查询手册。

TIP

语法糖(英语:Syntactic sugar)是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。

路由的目录结构

Vue 3 引入路由的方式和 Vue 2 一样,路由的管理也是放在 src/router 这个目录下:

bash
src
+ # 路由目录
+├─router
+   # 路由入口文件
+├───index.ts
+   # 路由配置,如果路由很多,可以再拆分模块文件
+├───routes.ts
+ # 项目入口文件
+└─main.ts

其中 index.ts 是路由的入口文件,如果路由很少,那么可以只维护在这个文件里,但对复杂项目来说,往往需要配置上二级、三级路由,逻辑和配置都放到一个文件的话,太臃肿了。

所以如果项目稍微复杂一些,可以像上面这个结构一样拆分成两个文件: index.ts 和 routes.ts ,在 routes.ts 里维护路由树的结构,在 index.ts 导入路由树结构并激活路由,同时可以在该文件里配置路由钩子。

如果项目更加复杂,例如做一个 Admin 后台,可以按照业务模块,再把 routes 拆分得更细,例如 game.ts / member.ts / order.ts 等业务模块,再统一导入到 index.ts 文件里。

TIP

需要注意的是,与 Vue 3 配套的路由版本是 vue-router 4.x 以上才可以正确适配项目。

在项目里引入路由

不管是 Vue 2 还是 Vue 3 ,引入路由都是在 src/router/index.ts 文件里,但是版本升级带来的变化很大,由于本书关于 Vue 3 都是使用 TypeScript ,所以这里只做一个 TypeScript 的变化对比。

TIP

下文可能会出现多次 import.meta.env.BASE_URL 这个变量,它是由 Vite 提供的环境变量,详见 Vite 官网关于 环境变量 的说明。

使用其他构建工具请自行替换为对应构建工具提供的环境变量,例如使用 @vue/cli 创建的项目因为是基于 Webpack ,所以使用的是 process.env.BASE_URL

回顾 Vue 2

Vue 2 的引入方式如下(其中 RouteConfig 是路由项目的 TS 类型)。

ts
import Vue from 'vue'
+import VueRouter from 'vue-router'
+import type { RouteConfig } from 'vue-router'
+
+Vue.use(VueRouter)
+
+const routes: Array<RouteConfig> = [
+  // ...
+]
+
+const router = new VueRouter({
+  mode: 'history',
+  base: import.meta.env.BASE_URL,
+  routes,
+})
+
+export default router

里面一些选项的功能说明:

routes 是路由树的配置,当项目的路由很多的时候可以集中到 routes.ts 管理,然后再 import 进来(具体的配置请看后面的 路由配置部分 说明)。

mode 决定访问路径模式,可配置为 hash 或者 history , Hash 模式是这种 http://abc.com/#/home 这样带 # 号的地址,支持所有浏览器, History 模式是 http://abc.com/home 这样不带 # 号,不仅美观,而且体验更好,但需要服务端做一些配置支持(详见下文的 服务端配置方案 ),也只对主流浏览器支持。

base 是 History 模式在进行路由切换时的基础路径,默认是 / 根目录,如果的项目不是部署在根目录下,而是二级目录、三级目录等多级目录,就必须指定这个 base ,否则路由切换会有问题。

了解 Vue 3 ~new

Vue 3 的引入方式如下(其中 RouteRecordRaw 是路由项目的 TS 类型)。

ts
import { createRouter, createWebHistory } from 'vue-router'
+import type { RouteRecordRaw } from 'vue-router'
+
+const routes: Array<RouteRecordRaw> = [
+  // ...
+]
+
+const router = createRouter({
+  history: createWebHistory(import.meta.env.BASE_URL),
+  routes,
+})
+
+export default router

在 Vue 3 (也就是 vue-router 4.x) 里,路由简化了一些配置项,其中 routes 和 Vue 2 一样,是路由树的配置。

但是 history 和 Vue 2 有所不同,在 Vue 3 ,使用 history 来代替 Vue 2 的 mode ,但功能是一样的,也是决定访问路径模式是 Hash 模式 还是 History 模式,同时合并了 Vue 2 (也就是 vue-router 3.x) 的 base 选项作为模式函数的入参。

和在使用 Vue 2 的时候一样, Vue 3 也可以配置一些额外的路由选项,比如:指定 router-link 为当前激活的路由所匹配的 className :

ts
const router = createRouter({
+  history: createWebHistory(import.meta.env.BASE_URL),
+  linkActiveClass: 'cur',
+  linkExactActiveClass: 'cur',
+  routes,
+})

更多的配置项可以参考 Vue Router 官网的 API 参考 一章。

路由树的配置

引入路由 部分有说到,当项目的路由很多的时候,文件会变得非常长,难以维护,这个时候可以集中到 routes.ts 或者更多的模块化文件管理,然后再 import 到 index.ts 里。

暂且把 routes.ts 这个文件称为 “路由树” ,因为它像一棵大树一样,不仅可以以一级路由为树干去生长,还可以添加二级、三级等多级路由来开枝散叶,下面来看看 routes.ts 应该怎么写。

基础格式 ~new

在 TypeScript 里,路由文件的基础格式由三个部分组成:类型声明、数组结构、模块导出。

ts
// src/router/routes.ts
+
+// 使用 TypeScript 时需要导入路由项目的类型声明
+import type { RouteRecordRaw } from 'vue-router'
+
+// 使用路由项目类型声明一个路由数组
+const routes: Array<RouteRecordRaw> = [
+  // ...
+]
+
+// 将路由数组导出给其他模块使用
+export default routes

之后就可以在 index.ts 里导入使用了。

那么里面的路由数组又是怎么写呢?这里就涉及到了 一级路由多级路由 的编写。

公共基础路径

在配置路由之前,需要先了解 “公共基础路径” 的概念,在讲解使用 Vite 等工具创建项目时,都提到了一个 项目配置 的管理,以 Vite 项目的配置文件 vite.config.ts 为例,里面有一个选项 base ,其实就是用来控制路由的公共基础路径,那么它有什么用呢?

base 的默认值是 /,也就是说,如果不配置它,那么所有的资源文件都是从域名根目录读取,如果项目部署在域名根目录那当然好,但是如果不是呢?那么就必须来配置它了。

配置很简单,只要把项目要上线的最终地址,去掉域名,剩下的那部分就是 base 的值。假设项目是部署在 https://example.com/vue3/ ,那么 base 就可以设置为 /vue3/

TIP

如果路由只有一级,那么 base 也可以设置为相对路径 ./,这样可以把项目部署到任意地方。

如果路由不止一级,那么请准确的指定 base,并且确保是以 / 开头并以 / 结尾,例如 /foo/

一级路由

一级路由,顾名思义,就是在的项目地址后面,只有一级 Path ,比如 https://example.com/home 这里的 home 就是一级路由。

来看一下最基本的路由配置应该包含哪些字段:

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+  },
+]

其中 path 是路由的访问路径,像上面说的,如果的域名是 https://example.com, 配置为 /home,那么访问路径就是 https://example.com/home

TIP

一级路由的 path 都必须是以 / 开头,比如: /home/setting

如果的项目首页不想带上 home 之类的尾巴,只想要 https://example.com/ 这样的域名直达 ,其实也是配置一级路由,只需要把路由的 path 指定为 / 即可。

name 是路由的名称,非必填,但是一般都会配置上去,这样可以很方便的通过 name 来代替 path 实现路由的跳转,因为像有时候的开发环境和生产环境的路径不一致,或者说路径变更,通过 name 无需调整,但如果通过 path,可能就要修改很多文件里面的链接跳转目标了。

component 是路由的模板文件,指向一个 vue 组件,用于指定路由在浏览器端的视图渲染,这里有两种方式来指定使用哪个组件:

同步组件

字段 component 接收一个变量,变量的值就是对应的模板组件。

在打包的时候,会把组件的所有代码都打包到一个文件里,对于大项目来说,这种方式的首屏加载是个灾难,要面对文件过大带来等待时间变长的问题。

ts
import Home from '@views/home.vue'
+
+const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/',
+    name: 'home',
+    component: Home,
+  },
+]

所以现在都推荐使用第二种方式,可以实现 路由懒加载

异步组件

字段 component 接收一个函数,在 return 的时候返回模板组件,同时组件里的代码在打包的时候都会生成独立的文件,并在访问到对应路由的时候按需引入。

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+  },
+]

关于这部分的更多说明,可以查看 路由懒加载

多级路由

在 Vue 路由生态里,支持配置二级、三级、四级等多级路由,理论上没有上限,实际业务中用到的级数通常是三级到四级。

比如做一个美食类网站,打算在 “中餐” 大分类下配置一个 “饺子” 栏目,那么地址就是:

https://example.com/chinese-food/dumplings

这种情况下,中餐 chinese-food 就是一级路由,饺子 dumplings 就是二级路由。

如果想再细化一下,“饺子” 下面再增加一个 “韭菜” 、“白菜” 等不同馅料的子分类:

https://example.com/chinese-food/dumplings/chives

这里的韭菜 chives 就是饺子 dumplings 的子路由,也就是三级路由。

在了解了子路由的概念后,来看一下具体如何配置以及注意事项。

父子路由的关系,都是严格按照 JSON 的层级关系,子路由的信息配置到父级的 children 数组里面,孙路由也是按照一样的格式,配置到子路由的 children 里。

这是一个简单的子路由示范:

ts
const routes: Array<RouteRecordRaw> = [
+  // 注意:这里是一级路由
+  {
+    path: '/lv1',
+    name: 'lv1',
+    component: () => import('@views/lv1.vue'),
+    // 注意:这里是二级路由,在 `path` 的前面没有 `/`
+    children: [
+      {
+        path: 'lv2',
+        name: 'lv2',
+        component: () => import('@views/lv2.vue'),
+        // 注意:这里是三级路由,在 `path` 的前面没有 `/`
+        children: [
+          {
+            path: 'lv3',
+            name: 'lv3',
+            component: () => import('@views/lv3.vue'),
+          },
+        ],
+      },
+    ],
+  },
+]

上面这个配置,最终三级路由的访问地址如下:

https://example.com/lv1/lv2/lv3

可以看到在注释里提示了二级、三级路由的 path 字段前面没有 / ,这样路径前面才会有其父级路由的 path 以体现其层级关系,否则会从根目录开始。

路由懒加载

在上面提过,路由在配置 同步组件 的时候,构建出来的文件都集中在一起,大的项目的文件会变得非常大,影响页面加载。

所以 Vue 在 Webpack 的代码分割功能的基础上,推出了 异步组件,可以把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样按需载入,很方便的实现路由组件的懒加载。

在这一段配置里面:

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+  },
+]

起到懒加载配置作用的就是 component 接收的值 () => import('@views/home.vue') ,其中 @views/home.vue 就是路由的组件。

在命令行运行 npm run build 打包构建后,会看到控制台输出的打包结果:

bash
 npm run build
+
+> hello-vue3@0.0.0 build
+> vue-tsc --noEmit && vite build
+
+vite v2.9.15 building for production...
+ 42 modules transformed.
+dist/index.html                       0.42 KiB
+dist/assets/home.03ad1823.js          0.65 KiB / gzip: 0.42 KiB
+dist/assets/HelloWorld.1322d484.js    1.88 KiB / gzip: 0.96 KiB
+dist/assets/about.c2af6d65.js         0.64 KiB / gzip: 0.41 KiB
+dist/assets/login.e9d1d9f9.js         0.65 KiB / gzip: 0.42 KiB
+dist/assets/index.60726771.css        0.47 KiB / gzip: 0.29 KiB
+dist/assets/login.bef803dc.css        0.12 KiB / gzip: 0.10 KiB
+dist/assets/HelloWorld.b2638077.css   0.38 KiB / gzip: 0.19 KiB
+dist/assets/home.ea56cd55.css         0.12 KiB / gzip: 0.10 KiB
+dist/assets/about.a0917080.css        0.12 KiB / gzip: 0.10 KiB
+dist/assets/index.19d6fb3b.js         79.94 KiB / gzip: 31.71 KiB

可以看到路由文件都按照 views 目录下的路由组件和 components 目录下的组件命名,输出了对应的 JS 文件和 CSS 文件,项目部署后, Vue 只会根据当前路由加载需要的文件,其他文件只做预加载,对于大型项目的访问体验非常友好。

而如果不使用路由懒加载,打包出来的文件是这样的:

bash
 npm run build
+
+> hello-vue3@0.0.0 build
+> vue-tsc --noEmit && vite build
+
+vite v2.9.15 building for production...
+ 41 modules transformed.
+dist/index.html                  0.42 KiB
+dist/assets/index.67b1ee4f.css   1.22 KiB / gzip: 0.49 KiB
+dist/assets/index.f758ee53.js    78.85 KiB / gzip: 31.05 KiB

可以看到所有的组件都被打包成了一个很大的 JS 文件和 CSS 文件,没有进行代码分割,对大型项目来说,这种方式打包出来的文件可能会有好几兆,对首屏加载的速度可想而知。

路由的渲染

所有路由组件,要在访问后进行渲染,都必须在父级组件里带有 <router-view /> 标签。

<router-view /> 在哪里,路由组件的代码就渲染在哪个节点上,一级路由的父级组件,就是 src/App.vue 这个根组件。

其中最基础的配置就是 <template /> 里面直接就是写一个 <router-view /> ,整个页面就是路由组件。

vue
<template>
+  <router-view />
+</template>

如果站点带有全局公共组件,比如有全站统一的页头、页脚,只有中间区域才是路由,那么可以这样配置:

vue
<template>
+  <!-- 全局页头 -->
+  <Header />
+
+  <!-- 路由 -->
+  <router-view />
+
+  <!-- 全局页脚 -->
+  <Footer />
+</template>

如果有一部分路由带公共组件,一部分没有,比如大部分页面都需要有侧边栏,但登录页、注册页不需要,就可以这么处理:

vue
<template>
+  <!-- 登录 -->
+  <Login v-if="route.name === 'login'" />
+
+  <!-- 注册 -->
+  <Register v-else-if="route.name === 'register'" />
+
+  <!-- 带有侧边栏的其他路由 -->
+  <div v-else>
+    <!-- 固定在左侧的侧边栏 -->
+    <Sidebar />
+
+    <!-- 路由 -->
+    <router-view />
+  </div>
+</template>

也可以通过 路由元信息 来管理这些规则。

使用 route 获取路由信息 ~new

和 Vue 2 可以直接在组件里使用 this.$route 来获取当前路由信息不同,在 Vue 3 的组件里,Vue 实例既没有了 this,也没有了 $route

要牢记一个事情就是,Vue 3 用啥都要导入,所以获取当前路由信息的正确用法是先导入路由 API :

ts
import { useRoute } from 'vue-router'

再在 setup 里定义一个变量来获取当前路由:

ts
const route = useRoute()

接下来就可以通过定义好的变量 route 去获取当前路由信息了。

当然,如果要在 <template /> 里使用路由,记得把 routesetup 里 return 出去。

ts
// 获取路由名称
+console.log(route.name)
+
+// 获取路由参数
+console.log(route.params.id)

Vue 3 的 route 和 Vue 2 的用法基本一致,日常使用应该很快能上手。

WARNING

但是 Vue 3 的新路由也有一些小变化,有一些属性是被移除了,比如之前获取父级路由信息,很喜欢用的 parent 属性,现在已经没有了,可以在 Vue Router 官网的 从 Vue2 迁移 一章查看所有破坏性变化。

类似被移除的 parent ,如果要获取父级路由信息(比如在做面包屑功能的时候),可以改成下面这样,手动指定倒数第二个为父级信息:

ts
// 获取路由记录
+const matched = route.matched
+
+// 获取该记录的路由个数
+const max = matched.length
+
+// 获取倒数第二个路由(也就是当前路由的父级路由)
+const parentRoute = matched[max - 2]

如果有配置父级路由,那么 parentRoute 就是父级路由信息,否则会返回 undefined

使用 router 操作路由 ~new

route 一样,在 Vue 3 也不能再使用 this.$router ,也必须通过导入路由 API 来使用:

ts
import { useRouter } from 'vue-router'

useRoute 一样, useRouter 也是一个函数,需要在 setup 里定义一个变量来获取路由信息。

ts
const router = useRouter()

接下来就可以通过定义好的变量 router 去操作路由了:

ts
// 跳转首页
+router.push({
+  name: 'home',
+})
+
+// 返回上一页
+router.back()

router-link 是一个全局组件,可直接在 <template /> 里直接使用,无需导入,基础的用法在 Vue 2 和 Vue 3 里是一样。

默认会被转换为一个 a 标签,对比写死的 <a href="..."> ,使用 router-link 会更加灵活。

基础跳转

最基础的用法就是把它当成一个 target="_self"a 标签使用,但无需重新刷新页面,因为是路由跳转,它的体验和使用 router 去进行路由导航的效果完全一样。

vue
<template>
+  <router-link to="/home">首页</router-link>
+</template>

等价于 routerpush

ts
router.push({
+  name: 'home',
+})

可以写个 <div /> 标签绑定 Click 事件达到 router-link 的效果:

vue
<template>
+  <div
+    class="link"
+    @click="
+      router.push({
+        name: 'home',
+      })
+    "
+  >
+    <span>首页</span>
+  </div>
+</template>

了解这种使用对比,对下文其他跳转方式的学习会有帮助。

带参数的跳转

使用 router 的时候,可以轻松的带上参数去那些有 ID 的内容页、用户资料页、栏目列表页等等。

比如要访问一篇文章 https://example.com/article/123 ,用 push 的写法是:

ts
router.push({
+  name: 'article',
+  params: {
+    id: 123,
+  },
+})

同理,从基础跳转的写法,很容易就能猜到在 router-link 里应该怎么写:

vue
<template>
+  <router-link
+    class="link"
+    :to="{
+      name: 'article',
+      params: {
+        id: 123,
+      },
+    }"
+  >
+    这是文章的标题
+  </router-link>
+</template>

不生成 a 标签 ~new

router-link 默认是被转换为一个 a 标签,但根据业务场景,也可以把它指定为生成其他标签,比如 spandivli 等等,这些标签因为不具备 href 属性,所以在跳转时都是通过 Click 事件去执行。

在 Vue 2 ,指定为其他标签只需要一个 tag 属性即可:

vue
<template>
+  <router-link tag="span" to="/home">首页</router-link>
+</template>

但在 Vue 3 ,tag 属性已被移除,需要通过 customv-slot 的配合将其渲染为其他标签。

比如要渲染为一个带有路由导航功能的其他标签:

vue
<template>
+  <router-link to="/home" custom v-slot="{ navigate }">
+    <span class="link" @click="navigate"> 首页 </span>
+  </router-link>
+</template>

渲染后就是一个普通的 <span /> 标签,当该标签被点击的时候,会通过路由的导航跳转到指定的路由页:

html
<!-- 渲染后的标签 -->
+<span class="link">首页</span>

关于这两个属性的参数说明如下:

  1. custom ,一个布尔值,用于控制是否需要渲染为 a 标签,当不包含 custom 或者把 custom 设置为 false 时,则依然使用 a 标签渲染。

  2. v-slot 是一个对象,用来决定标签的行为,它包含了:

字段含义
href解析后的 URL,将会作为一个 a 元素的 href 属性
route解析后的规范化的地址
navigate触发导航的函数,会在必要时自动阻止事件,和 router-link 同理
isActive如果需要应用激活的 class 则为 true,允许应用一个任意的 class
isExactActive如果需要应用精确激活的 class 则为 true,允许应用一个任意的 class

一般来说,v-slot 必备的只有 navigate ,用来绑定元素的点击事件,否则元素点击后不会有任何反应,其他的可以根据实际需求来添加。

TIP

要渲染为非 a 标签,切记两个点:

  1. router-link 必须带上 customv-slot 属性

  2. 最终要渲染的标签,写在 router-link 里,包括对应的 className 和点击事件

在独立 TS/JS 文件里使用路由

除了可以在 .vue 文件里使用路由之外,也可以在单独的 .ts.js 里使用。

比如要做一个带有用户系统的站点,登录的相关代码除了在 login.vue 里运用外,在注册页面 register.vue,用户注册成功还要帮用户执行一次自动登录。

登录完成还要记录用户的登录信息、 Token 、过期时间等等,有不少数据要做处理,以及需要帮助用户自动切去登录前的页面等行为,这是两个不同的组件,如果写两次几乎一样的代码,会大大提高维护成本。

这种情况下就可以通过抽离核心代码,封装成一个 login.ts 文件,在这个独立的 ts 文件里去操作路由。

ts
// 导入路由
+import router from '@/router'
+
+// 执行路由跳转
+router.push({
+  name: 'home',
+})

路由元信息配置

有时候的项目需要一些个性化配置,比如:

  1. 每个路由给予独立的标题;

  2. 管理后台的路由,部分页面需要限制一些访问权限;

  3. 通过路由来自动生成侧边栏、面包屑;

  4. 部分路由的生命周期需要做缓存( Keep Alive );

  5. 其他更多业务场景…

无需维护很多套配置,在定义路由树的时候可以配置 meta 字段,比如下面就是包含了多种元信息的一个登录路由:

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/login',
+    name: 'login',
+    component: () => import('@views/login.vue'),
+    meta: {
+      title: '登录',
+      isDisableBreadcrumbLink: true,
+      isShowBreadcrumb: false,
+      addToSidebar: false,
+      sidebarIcon: '',
+      sidebarIconAlt: '',
+      isNoLogin: true,
+    },
+  },
+]

这个是笔者曾经在做后台项目时用过的一些配置,主要的功能是:

字段类型含义
titlestring用于在渲染的时候配置浏览器标题;
isDisableBreadcrumbLinkboolean是否禁用面包屑链接(对一些没有内容的路由可以屏蔽访问);
isShowBreadcrumbboolean是否显示面包屑(此处的登录页不需要面包屑);
addToSidebarboolean是否加入侧边栏(此处的登录页不需要加入侧边栏);
sidebarIconstring配置侧边栏的图标 className(默认);
sidebarIconAltstring配置侧边栏的图标 className(展开状态);
isNoLoginboolean是否免登录(后台默认强制登录,设置为 true 则可以免登录访问,此处的登录页不需要校验);

类似的,如果有其他需求,比如要增加对不同用户组的权限控制(比如有管理员、普通用户分组,部分页面只有管理员允许访问),都可以通过配置 Meta 里的字段,再配合 路由拦截 一起使用。

路由重定向

对一些已下线的页面,直接访问原来的地址会导致 404 ,为了避免这种情况出现,通常会配置重定向将其指向一个新的页面,或者跳转回首页。

基本用法

路由重定向是使用一个 redirect 字段进行配置到对应的路由里面去实现跳转:

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+    meta: {
+      title: 'Home',
+    },
+  },
+  // 访问这个路由会被重定向到首页
+  {
+    path: '/error',
+    redirect: '/',
+  },
+]

通常来说,配置了 redirect 的路由,只需要指定 2 个字段即可,1 个是 path 该路由本身的路径,1 个是 redirect 目标路由的路径,其他字段可以忽略。

redirect 字段可以接收三种类型的值:

类型填写的值
string另外一个路由的 path
route另外一个路由(类似 router.push
function可以判断不同情况的重定向目标,最终 return 一个 path 或者 route

业务场景

路由重定向可以避免用户访问到一些无效路由页面:

  1. 比如项目上线了一段时间后,有个路由需要改名,或者调整路径层级,可以把旧路由重定向到新的,避免原来的用户从收藏夹等地方进来后找不到

  2. 一些容易打错的地址,比如通常个人资料页都是用 /profile,但是业务网站是使用 /account,那也可以把 /profile 重定向到 /account

  3. 对于一些有会员体系的站点,可以根据用户权限进行重定向,分别指向他们具备访问权限的页面

  4. 官网首页在 PC 端、移动端、游戏内嵌横屏版分别有 3 套页面,但希望能通过主域名来识别不同设备,帮助用户自动切换访问

了解了业务场景,接下来就能比较清晰的了解应该如何配置重定向了。

配置为 path

最常用的场景,恐怕就是首页的指向了,比如首页地址是 https://example.com/home,但是想让主域名 https://example.com/ 也能跳转到 /home,可以这么配置:

这是最简单的配置方式,把目标路由的 path 配置进来就可以了:

ts
const routes: Array<RouteRecordRaw> = [
+  // 重定向到 `/home`
+  {
+    path: '/',
+    redirect: '/home',
+  },
+  // 真正的首页
+  {
+    path: '/home',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+  },
+]

但缺点也显而易见,只能针对那些不带参数的路由。

配置为 route

如果想要重定向后的路由地址带上一些参数,可以配置为 route

ts
const routes: Array<RouteRecordRaw> = [
+  // 重定向到 `/home` ,并带上一个 `query` 参数
+  {
+    path: '/',
+    redirect: {
+      name: 'home',
+      query: {
+        from: 'redirect',
+      },
+    },
+  },
+  // 真正的首页
+  {
+    path: '/home',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+  },
+]

最终访问的地址就是 https://example.com/home?from=redirect, 像这样带有来路参数的,就可以在 “百度统计” 或者 “ CNZZ 统计” 之类的统计站点查看来路的流量。

配置为 function

结合业务场景来解释是最直观的,比如的网站有 3 个用户组,一个是管理员,一个是普通用户,还有一个是游客(未登录),他们的网站首页是不一样的:

用户组访问首页时
管理员具备各种数据可视化图表、最新的网站数据、一些最新的用户消息等等
普通用户只有一些常用模块的入口链接
未登录用户直接跳转到登录页面

产品需要在访问网站主域名的时候,识别用户身份跳转不同的首页,那么就可以这样配置路由重定向:

ts
const routes: Array<RouteRecordRaw> = [
+  // 访问主域名时,根据用户的登录信息,重定向到不同的页面
+  {
+    path: '/',
+    redirect: () => {
+      // `loginInfo` 是当前用户的登录信息
+      // 可以从 `localStorage` 或者 `Pinia` 读取
+      const { groupId } = loginInfo
+
+      // 根据组别 ID 进行跳转
+      switch (groupId) {
+        // 管理员跳去仪表盘
+        case 1:
+          return '/dashboard'
+
+        // 普通用户跳去首页
+        case 2:
+          return '/home'
+
+        // 其他都认为未登录,跳去登录页
+        default:
+          return '/login'
+      }
+    },
+  },
+]

路由别名配置

根据的业务需求,也可以为路由指定一个别名,与上面的 路由重定向 功能相似,但又有不同:

配置了路由重定向,当用户访问 /a 时,URL 将会被替换成 /b,然后匹配的实际路由是 /b

配置了路由别名,/a 的别名是 /b,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样。

配置方法

添加一个 alias 字段即可轻松实现:

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/home',
+    alias: '/index',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+  },
+]

如上的配置,即可实现可以通过 /home 访问首页,也可以通过 /index 访问首页。

404 路由页面配置 ~new

可以配置一个 404 路由来代替站内的 404 页面。

配置方法

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/:pathMatch(.*)*',
+    name: '404',
+    component: () => import('@views/404.vue'),
+  },
+]

这样配置之后,只要访问到不存在的路由,就会显示为这个 404 模板。

WARNING

新版的路由不再支持直接配置通配符 * ,而是必须使用带有自定义正则表达式的参数进行定义,详见官网 删除了 *(星标或通配符)路由 的说明。

导航守卫

和 Vue 2 时使用的路由一样, Vue 3 也支持导航守卫,并且用法基本上是一样的。

导航守卫这个词对初次接触的开发者来说应该会有点云里雾里,其实就是几个专属的钩子函数,先来看一下使用场景,大致理解一下基本概念和作用。

钩子的应用场景

对于导航守卫还不熟悉的开发者,可以从一些实际使用场景来加强印象,比如:

  1. 前面说的,在渲染的时候配置浏览器标题,由于 Vue 项目只有一个 HTML 文件,所以默认只有一个标题,但想在访问 /home 的时候标题显示为 “首页”,访问 /about 的时候标题显示为 “关于” 。

  2. 部分页面需要管理员才能访问,普通用户不允许进入到该路由页面。

  3. Vue 单页面项目,传统的 CNZZ / 百度统计等网站统计代码只会在页面加载的时候统计一次,但需要每次切换路由都上报一次 PV 数据。

这样的场景还有很多,导航守卫支持全局使用,也可以在 .vue 文件里单独使用,接下来看看具体的用法。

路由里的全局钩子

顾名思义,是在创建 router 的时候进行全局的配置,也就是说,只要配置了钩子,那么所有的路由在被访问到的时候,都会触发这些钩子函数。

可用钩子含义触发时机
beforeEach全局前置守卫在路由跳转前触发
beforeResolve全局解析守卫在导航被确认前,同时在组件内守卫和异步路由组件被解析后
afterEach全局后置守卫在路由跳转完成后触发

全局配置非常简单,在 src/router/index.ts 里,在创建路由之后、在导出去之前使用:

ts
import { createRouter } from 'vue-router'
+
+// 创建路由
+const router = createRouter({ ... })
+
+// 在这里调用导航守卫的钩子函数
+router.beforeEach((to, from) => {
+  // ...
+})
+
+// 导出去
+export default router

beforeEach ~new

全局前置守卫,这是导航守卫里面运用的最多的一个钩子函数,通常将其称为 “路由拦截”。

拦截这个词,顾名思义,就是在 XXX 目的达到之前,把它拦下来,所以路由的目的就是渲染指定的组件,路由拦截就是在组件被渲染之前,做一些拦截操作。

参数

参数作用
to即将要进入的路由对象
from当前导航正要离开的路由

TIP

和 Vue 2 不同,Vue 2 的 beforeEach 是默认三个参数,第三个参数是 next,用来操作路由接下来的跳转。

但在新版本路由里,已经通过 RFC 将其删除,虽然目前还是作为可选参数使用,但以后不确定是否会移除,不建议继续使用,点击查看原因

新版本路由可以通过 return 来代替 next

用法

比如在进入路由之前,根据 Meta 路由元信息 的配置,设定路由的网页标题:

ts
router.beforeEach((to, from) => {
+  const { title } = to.meta
+  document.title = title || '默认标题'
+})

或者判断是否需要登录:

ts
router.beforeEach((to, from) => {
+  const { isNoLogin } = to.meta
+  if (!isNoLogin) return '/login'
+})

或者针对一些需要 ID 参数,但参数丢失的路由做拦截,比如:很多网站的文章详情页都是类似 https://example.com/article/123 这样格式的地址,是需要带有文章 ID 作为 URL 的一部分,如果只访问 https://example.com/article 则需要拦截掉。

这里是关于 article 路由的配置,是有要求 Params 要带上 ID 参数:

ts
const routes: Array<RouteRecordRaw> = [
+  // 这是一个配置了 `params` ,访问的时候必须带 `id` 的路由
+  {
+    path: '/article/:id',
+    name: 'article',
+    component: () => import('@views/article.vue'),
+  },
+  // ...
+]

当路由的 params 丢失的时候,路由记录 matched 是一个空数组,针对这样的情况,就可以配置一个拦截,丢失参数时返回首页:

ts
router.beforeEach((to) => {
+  if (to.name === 'article' && to.matched.length === 0) {
+    return '/'
+  }
+})

beforeResolve

全局解析守卫,它会在每次导航时触发,但是在所有组件内守卫和异步路由组件被解析之后,将在确认导航之前被调用。

这个钩子用的比较少,因为它和 beforeEach 非常相似,相信大部分开发者都是会用 beforeEach 来代替它。

那么它有什么用?

它通常会用在一些申请权限的环节,比如一些 H5 页面需要申请系统相机权限、一些微信活动需要申请微信的登录信息授权,获得权限之后才允许获取接口数据和给用户更多的操作,使用 beforeEach 时机太早,使用 afterEach 又有点晚,那么这个钩子的时机就刚刚好。

参数

参数作用
to即将要进入的路由对象
from当前导航正要离开的路由

用法

以以前 Vue Router 官网的申请照相机权限的例子来举例:

ts
// https://router.vuejs.org/zh/guide/advanced/navigation-guards.html
+
+router.beforeResolve(async (to) => {
+  // 如果路由配置了必须调用相机权限
+  if (to.meta.requiresCamera) {
+    // 正常流程,咨询是否允许使用照相机
+    try {
+      await askForCameraPermission()
+    } catch (error) {
+      // 容错
+      if (error instanceof NotAllowedError) {
+        // ... 处理错误,然后取消导航
+        return false
+      } else {
+        // 如果出现意外,则取消导航并抛出错误
+        throw error
+      }
+    }
+  }
+})

afterEach

全局后置守卫,这也是导航守卫里面用的比较多的一个钩子函数。

参数

参数作用
to即将要进入的路由对象
from当前导航正要离开的路由

用法

在刚刚的 钩子的应用场景 里面有个例子,就是每次切换路由都上报一次 PV 数据,类似这种每个路由都要执行一次,但又不必在渲染前操作的,都可以放到后置钩子里去执行。

笔者之前写过两个数据统计的插件:Vue 版 CNZZ 统计Vue 版百度统计,就是用的这个后置钩子来实现自动上报数据。

ts
router.afterEach((to, from) => {
+  // 上报流量的操作
+  // ...
+})

在组件内使用全局钩子

上面所讲的都是全局钩子,虽然一般都是在路由文件里使用,但如果有需要,也可以在 .vue 文件里操作。

TIP

和路由的渲染不同,渲染是父级路由组件必须带有 <router-view /> 标签才能渲染,但是使用全局钩子不受此限制。

建议只在一些入口文件里使用,比如 App.vue ,或者是一些全局的 Header.vueFooter.vue 里使用,方便后续维护。

setup 里,定义一个 router 变量获取路由之后,就可以操作了:

ts
import { defineComponent } from 'vue'
+import { useRouter } from 'vue-router'
+
+export default defineComponent({
+  setup() {
+    // 定义路由
+    const router = useRouter()
+
+    // 调用全局钩子
+    router.beforeEach((to, from) => {
+      // ...
+    })
+  },
+})

路由里的独享钩子

介绍完全局钩子,如果只是有个别路由要做处理,可以使用路由独享的守卫,用来针对个别路由定制一些特殊功能,可以减少在全局钩子里面写一堆判断。

可用钩子含义触发时机
beforeEnter路由独享前置守卫在路由跳转前触发

注:路由独享的钩子,必须配置在 routes 的 JSON 树里面,挂在对应的路由下面(与 pathnamemeta 这些字段同级)。

beforeEnter ~new

它和全局钩子 beforeEach 的作用相同,都是在进入路由之前触发,触发时机比 beforeResolve 要早。

顺序:beforeEach(全局) > beforeEnter(独享) > beforeResolve(全局)。

参数

参数作用
to即将要进入的路由对象
from当前导航正要离开的路由

TIP

beforeEach 一样,也是取消了 next,可以通过 return 来代替。

用法

比如:整个站点的默认标题都是以 “栏目标题” + “全站关键标题” 的格式作为网页的 Title ,例如 “项目经验 - 程沛权” ,但在首页的时候,想做一些不一样的定制。

ts
const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/home',
+    name: 'home',
+    component: () => import('@views/home.vue'),
+    // 在这里添加单独的路由守卫
+    beforeEnter: (to, from) => {
+      document.title = '程沛权 - 养了三只猫'
+    },
+  },
+]

就可以通过 beforeEnter 来实现一些个别路由的单独定制。

TIP

需要注意的是,只有从不同的路由切换进来,才会触发该钩子。

针对同一个路由,但是不同的 paramsquery 或者 hash ,都不会重复触发该钩子。

比如从 https://example.com/article/123 切换到 https://example.com/article/234 是不会触发的。

其他的用法和 beforeEach 可以说是一样的。

组件内单独使用 ~new

组件里除了可以使用全局钩子外,还可以使用组件专属的路由钩子。

可用钩子含义触发时机
onBeforeRouteUpdate组件内的更新守卫在当前路由改变,但是该组件被复用时调用
onBeforeRouteLeave组件内的离开守卫导航离开该组件的对应路由时调用

TIP

1、组件内钩子的入参,也都是取消了 next,可以通过 return 来代替。

2、和其他 Composition API 一样,需要先 import 再操作。

和旧版路由不同,新版的 Composition API 移除了 beforeRouteEnter 这个钩子了。

onBeforeRouteUpdate

可以在当前路由改变,但是该组件被复用时,重新调用里面的一些函数用来更新模板数据的渲染。

参数

参数作用
to即将要进入的路由对象
from当前导航正要离开的路由

用法

比如一个内容网站,通常在文章详情页底部会有相关阅读推荐,这个时候就会有一个操作场景是,从文章 A 跳转到文章 B。

比如从 https://example.com/article/111 切去 https://example.com/article/222 ,这种情况就属于 “路由改变,但是组件被复用” 的情况了。

这种情况下,原本放在 onMounted 里执行数据请求的函数就不会被调用,可以借助该钩子来实现渲染新的文章内容。

ts
import { defineComponent, onMounted } from 'vue'
+import { useRoute, onBeforeRouteUpdate } from 'vue-router'
+
+export default defineComponent({
+  setup() {
+    // 其他代码略...
+
+    // 查询文章详情
+    async function queryArticleDetail(id: number) {
+      // 请求接口数据
+      const res = await axios({
+        url: `/article/${id}`,
+      })
+      // ...
+    }
+
+    // 组件挂载完成后执行文章内容的请求
+    // 注意这里是获取 `route` 的 `params`
+    onMounted(async () => {
+      const id = Number(route.params.id) || 0
+      await queryArticleDetail(id)
+    })
+
+    // 组件被复用时重新请求新的文章内容
+    onBeforeRouteUpdate(async (to, from) => {
+      // ID 不变时减少重复请求
+      if (to.params.id === from.params.id) return
+
+      // 注意这里是获取 `to` 的 `params`
+      const id = Number(to.params.id) || 0
+      await queryArticleDetail(id)
+    })
+  },
+})

onBeforeRouteLeave

可以在离开当前路由之前,实现一些离开前的判断拦截。

参数

参数作用
to即将要进入的路由对象
from当前导航正要离开的路由

用法

这个离开守卫通常用来禁止用户在还未保存修改前突然离开,可以通过 return false 来取消用户离开当前路由。

ts
import { defineComponent } from 'vue'
+import { onBeforeRouteLeave } from 'vue-router'
+
+export default defineComponent({
+  setup() {
+    // 调用离开守卫
+    onBeforeRouteLeave((to, from) => {
+      // 弹出一个确认框
+      const confirmText = '确认要离开吗?您的更改尚未保存!'
+      const isConfirmLeave = window.confirm(confirmText)
+
+      // 当用户点取消时,不离开路由
+      if (!isConfirmLeave) {
+        return false
+      }
+    })
+  },
+})

路由侦听 ~new

路由的侦听,可以延续以往的 watch 大法,也可以用全新的 watchEffect

watch

在 Vue 2 的时候,侦听路由变化用的最多的就是 watch 了, Vue 3 的 watch API 使用更简单。

侦听整个路由

可以跟以前一样,直接侦听整个路由的变化:

ts
import { defineComponent, watch } from 'vue'
+import { useRoute } from 'vue-router'
+
+export default defineComponent({
+  setup() {
+    const route = useRoute()
+
+    // 侦听整个路由
+    watch(route, (to, from) => {
+      // 处理一些事情
+      // ...
+    })
+  },
+})

第一个参数传入整个路由;第二个参数是个 Callback ,可以获取 tofrom 来判断路由变化情况。

侦听路由的某个数据

如果只想侦听路由的某个数据变化,比如侦听一个 Query ,或者一个 Param ,可以采用这种方式:

ts
import { defineComponent, watch } from 'vue'
+import { useRoute } from 'vue-router'
+
+export default defineComponent({
+  setup() {
+    const route = useRoute()
+
+    // 侦听路由参数的变化
+    watch(
+      () => route.query.id,
+      () => {
+        console.log('侦听到 ID 变化')
+      }
+    )
+  },
+})

第一个参数传入一个 getter 函数, return 要侦听的值;第二个参数是个 Callback ,可以针对参数变化进行一些操作。

watchEffect

这是 Vue 3 新出的一个侦听函数,可以简化 watch 的行为。

比如定义了一个函数,通过路由的参数来获取文章 ID ,然后请求文章内容:

ts
import { defineComponent, watchEffect } from 'vue'
+import { useRoute } from 'vue-router'
+
+export default defineComponent({
+  setup() {
+    const route = useRoute()
+
+    // 从接口查询文章详情
+    async function queryArticleDetail() {
+      const id = Number(route.params.id) || 0
+      console.log('文章 ID 是:', id)
+
+      const res = await axios({
+        url: `/article/${id}`,
+      })
+      // ...
+    }
+
+    // 直接侦听包含路由参数的那个函数
+    watchEffect(queryArticleDetail)
+  },
+})

对比 watch 的使用, watchEffect 在操作上更加简单,把包含要被侦听数据的函数,当成它的入参传进去即可。

部署问题与服务端配置

通常使用路由的 Hash 模式,部署后有问题的情况很少,但是如果使用 History 模式,可能会遇到这样那样的问题。

常见部署问题

这里整理一些常见部署问题的原因分析和解决方案,可作参考。

页面刷新就 404

页面部署到服务端之后,访问首页正常;通过导航上面的链接进行路由跳转,也正常;但是刷新页面就变成 404 了。

问题原因

一般这种情况是路由开启了 History 模式,但是服务端没有配置功能支持。

解决方案

请根据 服务端配置 部分的说明,与的运维同事沟通,让他帮忙修改服务端的配置。

部分路由白屏

如果在项目配置文件里,把里面的 publicPath (使用 Vue CLI ) 或者 base (使用 Vite ) 配置成相对路径 ./ ,但是路由配置了二级或以上,那么就会出现这个问题。

问题原因

原因是打包后的 JS 、 CSS 等静态资源都是存放在项目根目录下,一级路由的 ./ 就是根目录,所以访问正常;而二级路由的 ./ 则不是根目录了,是从当前目录载入的 ,这就导致无法正确载入 JS 文件,从而导致了白屏。

假设项目域名是 https://example.com ,那么:

  • 一级路由是 https://example.com/home
  • 二级路由是 https://example.com/foo/bar
  • 假设打包后的 JS 文件等静态资产存放于 https://example.com/assets/ 文件夹下

访问一级路由时, ./ 访问到的 JS 文件是 https://example.com/assets/home.js ,所以一级路由可以正常访问到。

访问二级路由时, ./ 访问到的 JS 文件是 https://example.com/foo/assets/bar.js ,但实际上文件是存放在 https://example.com/assets/bar.js ,访问到的 URL 资源不存在,所以白屏了。

解决方案

如果的项目开启了 History 模式,并且配置有二级或者二级以上的路由时,不要使用 ./ 这样的相对路径。

正确的方式应该是修改 publicPath (使用 Vue CLI ) 或者 base (使用 Vite ),如果是部署在域名根目录则写 / ,如果是子目录,则按照子目录的格式,将其以 / 开头,以 / 结尾的形式配置( e.g. /hello-world/

服务端配置方案

如果使用的是 HTML5 的 History 模式,那么服务端也需要配置对应的支持,否则会出现路由跳转正常,但页面一刷新就 404 的情况。

TIP

服务端配置后,就不再进入 404 了,需要在项目里手动配置 404 路由页面 的路由。

Nginx

现在大部分公司的服务程序都在使用 Nginx ,可以将以下代码发给运维工程师参考,调整 Nginx 的配置:

nginx
location / {
+  try_files $uri $uri/ /index.html;
+}

Express

如果是前端工程师使用 Node.js 作服务端,并且使用了 Express 服务端框架,那么操作将变得更简单:

  1. 仅需要安装一个中间件
bash
npm install connect-history-api-fallback
  1. 在服务启动入口文件里导入该中间件并激活
js
const express = require('express')
+const history = require('connect-history-api-fallback')
+
+// 创建 Express 实例
+const app = express()
+app
+  // 启用 History 中间件
+  .use(history())
+  // 这里是读取打包后的页面文件目录
+  .use('/', express.static(resolve('../dist')))

更多用法可以看: connect-history-api-fallback 的文档。

更多方案

其他的诸如 Apache 、 IIS 、或者原生 Node 等等配置方案, Vue 官方都提供了对应的演示代码,点击查看更多配置方案: 服务器配置示例

Released under the MIT License.

+ + + + \ No newline at end of file diff --git a/typescript.html b/typescript.html new file mode 100644 index 00000000..04e7f463 --- /dev/null +++ b/typescript.html @@ -0,0 +1,693 @@ + + + + + + 快速上手 TypeScript | Vue3 入门指南与实战案例 + + + + + + + + + + + + + +
Skip to content
目录

快速上手 TypeScript

如果已经看完 工程化的起步准备 一章,相信此时的已经对 Node 工程项目有了足够的认识了,在此之前的所有代码都是使用 JavaScript 编写的,接下来这一节,将开始介绍 TypeScript ,这是一门新的语言,但是上手非常简单。

TypeScript 简称 TS ,既是一门新语言,也是 JS 的一个超集,它是在 JavaScript 的基础上增加了一套类型系统,它支持所有的 JS 语句,为工程化开发而生,最终在编译的时候去掉类型和特有的语法,生成 JS 代码。

虽然带有类型系统的前端语言不止 TypeScript (例如 Facebook 推出的 Flow.js ),但从目前整个 开源社区的流行趋势 看, TypeScript 无疑是更好的选择。

而且只要本身已经学会了 JS ,并且经历过很多协作类的项目,那么使用 TS 编程是一个很自然而然的过程。

为什么需要类型系统

要想知道自己为什么要用 TypeScript ,得先从 JavaScript 有什么不足说起,举一个非常小的例子:

js
function getFirstWord(msg) {
+  console.log(msg.split(' ')[0])
+}
+
+getFirstWord('Hello World') // 输出 Hello
+
+getFirstWord(123) // TypeError: msg.split is not a function

这里定义了一个用空格切割字符串的方法,并打印出第一个单词:

  1. 第一次执行时,字符串支持 split 方法,所以成功获取到了第一个单词 Hello
  2. 第二次执行时,由于数值不存在 split 方法,所以传入 123 引起了程序崩溃

这就是 JavaScript 的弊端,过于灵活,没有类型的约束,很容易因为类型的变化导致一些本可避免的 BUG 出现,而且这些 BUG 通常需要在程序运行的时候才会被发现,很容易引发生产事故。

虽然可以在执行 split 方法之前执行一层判断或者转换,但很明显增加了很多工作量。

TypeScript 的出现,在编译的时候就可以执行检查来避免掉这些问题,而且配合 VSCode 等编辑器的智能提示,可以很方便的知道每个变量对应的类型。

Hello TypeScript

将继续使用 Hello Node 这个 demo ,或者可以再建一个新 demo ,依然是在 src 文件夹下,创建一个 ts 文件夹归类本次的测试文件,然后创建一个 index.ts 文件在 ts 文件夹下。

TIP

TypeScript 语言对应的文件扩展名是 .ts

然后在命令行通过 cd 命令进入项目所在的目录路径,安装 TypeScript 开发的两个主要依赖包:

  1. typescript 这个包是用 TypeScript 编程的语言依赖包

  2. ts-node 是让 Node 可以运行 TypeScript 的执行环境

bash
npm install -D typescript ts-node

这次添加了一个 -D 参数,因为 TypeScript 和 TS-Node 是开发过程中使用的依赖,所以将其添加到 package.json 的 devDependencies 字段里。

然后修改 scripts 字段,增加一个 dev:ts 的 script :

json
{
+  "name": "hello-node",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "dev:cjs": "node src/cjs/index.cjs",
+    "dev:esm": "node src/esm/index.mjs",
+    "dev:ts": "ts-node src/ts/index.ts",
+    "compile": "babel src/babel --out-dir compiled",
+    "serve": "node server/index.js"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "md5": "^2.3.0"
+  },
+  "devDependencies": {
+    "ts-node": "^10.7.0",
+    "typescript": "^4.6.3"
+  }
+}

准备工作完毕!

TIP

请注意, dev:ts 这个 script 是用了 ts-node 来代替原来在用的 node ,因为使用 node 无法识别 TypeScript 语言。

为什么需要类型系统 里面提到的例子放到 src/ts/index.ts 里:

ts
// src/ts/index.ts
+function getFirstWord(msg) {
+  console.log(msg.split(' ')[0])
+}
+
+getFirstWord('Hello World')
+
+getFirstWord(123)

然后在命令行运行 npm run dev:ts 来看看这次的结果:

bash
TSError:  Unable to compile TypeScript:
+src/ts/index.ts:1:23 - error TS7006: Parameter 'msg' implicitly has an 'any' type.
+
+1 function getFirstWord(msg) {
+                        ~~~

这是告知 getFirstWord 的入参 msg 带有隐式 any 类型,这个时候可能还不了解 any 代表什么意思,没关系,来看下如何修正这段代码:

ts
// src/ts/index.ts
+function getFirstWord(msg: string) {
+  console.log(msg.split(' ')[0])
+}
+
+getFirstWord('Hello World')
+
+getFirstWord(123)

留意到没有,现在函数的入参 msg 已经变成了 msg: string ,这是 TypeScript 指定参数为字符串类型的一个写法。

现在再运行 npm run dev:ts ,上一个错误提示已经不再出现,取而代之的是一个新的报错:

bash
TSError:  Unable to compile TypeScript:
+src/ts/index.ts:7:14 - error TS2345:
+Argument of type 'number' is not assignable to parameter of type 'string'.
+
+7 getFirstWord(123)
+               ~~~

这次的报错代码是在 getFirstWord(123) 这里,告诉 number 类型的数据不能分配给 string 类型的参数,也就是故意传入一个会报错的数值进去,被 TypeScript 检查出来了!

可以再仔细留意一下控制台的信息,会发现没有报错的 getFirstWord('Hello World') 也没有打印出结果,这是因为 TypeScript 需要先被编译成 JavaScript ,然后再执行。

这个机制让有问题的代码能够被及早发现,一旦代码出现问题,编译阶段就会失败。

移除会报错的那行代码,只保留如下:

ts
// src/ts/index.ts
+function getFirstWord(msg: string) {
+  console.log(msg.split(' ')[0])
+}
+
+getFirstWord('Hello World')

再次运行 npm run dev:ts ,这次完美运行!

ts
npm run dev:ts
+
+> demo@1.0.0 dev:ts
+> ts-node src/ts/index.ts
+
+Hello

在这个例子里,相信已经感受到 TypeScript 的魅力了!接下来来认识一下不同的 JavaScript 类型,在 TypeScript 里面应该如何定义。

常用的 TS 类型定义

Hello TypeScript 的体验中,相信能够感受到 TypeScript 编程带来的好处了,代码的健壮性得到了大大的提升!

并且应该也能够深刻理解,TypeScript 的类型系统并不会给编程带来极高的门槛或者说开发障碍,这是以非常小的成本换取巨大收益的举措。

TIP

如果还没有体验这个 demo ,建议先按教程跑一下,然后来讲解不同的 JavaScript 类型应该如何在 TypeScript 里定义,接下来的时间里,可以一边看,一边在 demo 里实践。

原始数据类型

原始数据类型 是一种既非对象也无方法的数据,刚才演示代码里,函数的入参使用的字符串 String 就是原始数据类型之一。

除了 String ,另外还有数值 Number 、布尔值 Boolean 等等,它们在 TypeScript 都有统一的表达方式,列个表格对比,能够更直观的了解:

原始数据类型JavaScriptTypeScript
字符串Stringstring
数值Numbernumber
布尔值Booleanboolean
大整数BigIntbigint
符号Symbolsymbol
不存在Nullnull
未定义Undefinedundefined

有没有发现窍门?对! TypeScript 对原始数据的类型定义真的是超级简单,就是转为全小写即可!

举几个例子:

ts
// 字符串
+const str: string = 'Hello World'
+
+// 数值
+const num: number = 1
+
+// 布尔值
+const bool: boolean = true

不过在实际的编程过程中,原始数据类型的类型定义是可以省略的,因为 TypeScript 会根据声明变量时赋值的类型,自动推导变量类型,也就是可以跟平时写 JavaScript 一样:

ts
// 这样也不会报错,因为 TS 会推导它们的类型
+const str = 'Hello World'
+const num = 1
+const bool = true

数组

除了原始数据类型之外, JavaScript 还有引用类型,数组 Array 就是其中的一种。

之所以先讲数组,是因为它在 TS 类型定义的写法上面,可能是最接近原始数据的一个类型了,为什么这么说?还是列个表格,来看一下如何定义数组:

数组里的数据类型写法 1类型写法 2
字符串string[]Array<string>
数值number[]Array<number>
布尔值boolean[]Array<boolean>
大整数bigint[]Array<bigint>
符号symbol[]Array<symbol>
不存在null[]Array<null>
未定义undefined[]Array<undefined>

是吧!就只是在原始数据类型的基础上变化了一下书写格式,就成为了数组的定义!

笔者最常用的就是 string[] 这样的格式,只需要追加一个方括号 [] ,另外一种写法是基于 TS 的泛型 Array<T> ,两种方式定义出来的类型其实是一样的。

举几个例子:

ts
// 字符串数组
+const strs: string[] = ['Hello World', 'Hi World']
+
+// 数值数组
+const nums: number[] = [1, 2, 3]
+
+// 布尔值数组
+const bools: boolean[] = [true, true, false]

在实际的编程过程中,如果数组一开始就有初始数据(数组长度不为 0 ),那么 TypeScript 也会根据数组里面的项目类型,正确自动帮推导这个数组的类型,这种情况下也可以省略类型定义:

ts
// 这种有初始项目的数组, TS 也会推导它们的类型
+const strs = ['Hello World', 'Hi World']
+const nums = [1, 2, 3]
+const bools = [true, true, false]

但是!如果一开始是 [] ,那么就必须显式的指定数组类型(取决于当前项目的 tsconfig.json 配置,可能会引起报错):

ts
// 这个时候会认为是 any[] 或者 never[] 类型
+const nums = []
+
+// 这个时候再 push 一个 number 数据进去,也不会使其成为 number[]
+nums.push(1)

而对于复杂的数组,比如数组里面的 item 都是对象,其实格式也是一样,只不过把原始数据类型换成 对象的类型 即可,例如 UserItem[] 表示这是一个关于用户的数组列表。

对象(接口)

看完了数组,接下来看看对象的用法,对象也是引用类型,在 数组 的最后提到了一个 UserItem[] 的写法,这里的 UserItem 就是一个对象的类型定义。

如果熟悉 JavaScript ,那么就知道对象的 “键值对” 里面的值,可能是由原始数据、数组、对象组成的,所以在 TypeScript ,类型定义也是需要根据值的类型来确定它的类型,因此定义对象的类型应该是第一个比较有门槛的地方。

如何定义对象的类型

对象的类型定义有两个语法支持: typeinterface

先看看 type 的写法:

ts
type UserItem = {
+  // ...
+}

再看看 interface 的写法:

ts
interface UserItem {
+  // ...
+}

可以看到它们表面上的区别是一个有 = 号,一个没有,事实上在一般的情况下也确实如此,两者非常接近,但是在特殊的时候也有一定的区别。

了解接口的使用

为了降低学习门槛,统一使用 interface 来做入门教学,它的写法与 Object 更为接近,事实上它也被用的更多。

对象的类型 interface 也叫做接口,用来描述对象的结构。

TIP

对象的类型定义通常采用 Upper Camel Case 大驼峰命名法,也就是每个单词的首字母大写,例如 UserItemGameDetail ,这是为了跟普通变量进行区分(变量通常使用 Lower Camel Case 小驼峰写法,也就是第一个单词的首字母小写,其他首字母大写,例如 userItem )。

这里通过一些举例来带举一反三,随时可以在 demo 里进行代码实践。

以这个用户信息为例子,比如要描述 Petter 这个用户,他的最基础信息就是姓名和年龄,那么定义为接口就是这么写:

ts
// 定义用户对象的类型
+interface UserItem {
+  name: string
+  age: number
+}
+
+// 在声明变量的时候将其关联到类型上
+const petter: UserItem = {
+  name: 'Petter',
+  age: 20,
+}

如果需要添加数组、对象等类型到属性里,按照这样继续追加即可。

可选的接口属性

注意,上面这样定义的接口类型,表示 nameage 都是必选的属性,不可以缺少,一旦缺少,代码运行起来就会报错!

src/ts/index.ts 里敲入以下代码,也就是在声明变量的时候故意缺少了 age 属性,来看看会发生什么:

ts
// 注意!这是一段会报错的代码
+
+interface UserItem {
+  name: string
+  age: number
+}
+
+const petter: UserItem = {
+  name: 'Petter',
+}

运行 npm run dev:ts ,会看到控制台给的报错信息,缺少了必选的属性 age

bash
src/ts/index.ts:6:7 - error TS2741:
+Property 'age' is missing in type '{ name: string; }'
+but required in type 'UserItem'.
+
+6 const petter: UserItem = {
+        ~~~~~~
+
+  src/ts/index.ts:3:3
+    3   age: number
+        ~~~
+    'age' is declared here.

在实际的业务中,有可能会出现一些属性并不是必须的,就像这个年龄,可以将其设置为可选属性,通过添加 ? 来定义。

请注意下面代码的第三行, age 后面紧跟了一个 ? 号再接 : 号,这是 TypeScript 对象对于可选属性的一个定义方式,这一次这段代码是可以成功运行的!

ts
interface UserItem {
+  name: string
+  // 这个属性变成了可选
+  age?: number
+}
+
+const petter: UserItem = {
+  name: 'Petter',
+}

调用自身接口的属性

如果一些属性的结构跟本身一致,也可以直接引用,比如下面例子里的 friendList 属性,用户的好友列表,它就可以继续使用 UserItem 这个接口作为数组的类型:

ts
interface UserItem {
+  name: string
+  age: number
+  enjoyFoods: string[]
+  // 这个属性引用了本身的类型
+  friendList: UserItem[]
+}
+
+const petter: UserItem = {
+  name: 'Petter',
+  age: 18,
+  enjoyFoods: ['rice', 'noodle', 'pizza'],
+  friendList: [
+    {
+      name: 'Marry',
+      age: 16,
+      enjoyFoods: ['pizza', 'ice cream'],
+      friendList: [],
+    },
+    {
+      name: 'Tom',
+      age: 20,
+      enjoyFoods: ['chicken', 'cake'],
+      friendList: [],
+    }
+  ],
+}

接口的继承

接口还可以继承,比如要对用户设置管理员,管理员信息也是一个对象,但要比普通用户多一个权限级别的属性,那么就可以使用继承,它通过 extends 来实现:

ts
interface UserItem {
+  name: string
+  age: number
+  enjoyFoods: string[]
+  friendList: UserItem[]
+}
+
+// 这里继承了 UserItem 的所有属性类型,并追加了一个权限等级属性
+interface Admin extends UserItem {
+  permissionLevel: number
+}
+
+const admin: Admin = {
+  name: 'Petter',
+  age: 18,
+  enjoyFoods: ['rice', 'noodle', 'pizza'],
+  friendList: [
+    {
+      name: 'Marry',
+      age: 16,
+      enjoyFoods: ['pizza', 'ice cream'],
+      friendList: [],
+    },
+    {
+      name: 'Tom',
+      age: 20,
+      enjoyFoods: ['chicken', 'cake'],
+      friendList: [],
+    }
+  ],
+  permissionLevel: 1,
+}

如果觉得这个 Admin 类型不需要记录这么多属性,也可以在继承的过程中舍弃某些属性,通过 Omit 帮助类型来实现,Omit 的类型如下:

ts
type Omit<T, K extends string | number | symbol>

其中 T 代表已有的一个对象类型, K 代表要删除的属性名,如果只有一个属性就直接是一个字符串,如果有多个属性,用 | 来分隔开,下面的例子就是删除了两个不需要的属性:

ts
interface UserItem {
+  name: string
+  age: number
+  enjoyFoods: string[]
+  friendList?: UserItem[]
+}
+
+// 这里在继承 UserItem 类型的时候,删除了两个多余的属性
+interface Admin extends Omit<UserItem, 'enjoyFoods' | 'friendList'> {
+  permissionLevel: number
+}
+
+// 现在的 admin 就非常精简了
+const admin: Admin = {
+  name: 'Petter',
+  age: 18,
+  permissionLevel: 1,
+}

看到这里并实际体验过的话,在业务中常见的类型定义已经难不倒了!

类是 JavaScript ES6 推出的一个概念,通过 class 关键字,可以定义一个对象的模板,如果对类还比较陌生的话,可以先阅读一下阮一峰老师的 ES6 文章:Class 的基本语法

在 TypeScript ,通过类得到的变量,它的类型就是这个类,可能这句话看起来有点难以理解,来看个例子,可以在 demo 里运行它:

ts
// 定义一个类
+class User {
+  // constructor 上的数据需要先这样定好类型
+  name: string
+
+  // 入参也要定义类型
+  constructor(userName: string) {
+    this.name = userName
+  }
+
+  getName() {
+    console.log(this.name)
+  }
+}
+
+// 通过 new 这个类得到的变量,它的类型就是这个类
+const petter: User = new User('Petter')
+petter.getName() // Petter

类与类之间可以继承:

ts
// 这是一个基础类
+class UserBase {
+  name: string
+  constructor(userName: string) {
+    this.name = userName
+  }
+}
+
+// 这是另外一个类,继承自基础类
+class User extends UserBase {
+  getName() {
+    console.log(this.name)
+  }
+}
+
+// 这个变量拥有上面两个类的所有属性和方法
+const petter: User = new User('Petter')
+petter.getName()

类也可以提供给接口去继承:

ts
// 这是一个类
+class UserBase {
+  name: string
+  constructor(userName: string) {
+    this.name = userName
+  }
+}
+
+// 这是一个接口,可以继承自类
+interface User extends UserBase {
+  age: number
+}
+
+// 这样这个变量就必须同时存在两个属性
+const petter: User = {
+  name: 'Petter',
+  age: 18,
+}

如果类上面本身有方法存在,接口在继承的时候也要相应的实现,当然也可以借助在 对象(接口) 提到的 Omit 帮助类型来去掉这些方法。

ts
class UserBase {
+  name: string
+  constructor(userName: string) {
+    this.name = userName
+  }
+  // 这是一个方法
+  getName() {
+    console.log(this.name)
+  }
+}
+
+// 接口继承类的时候也可以去掉类上面的方法
+interface User extends Omit<UserBase, 'getName'> {
+  age: number
+}
+
+// 最终只保留数据属性,不带有方法
+const petter: User = {
+  name: 'Petter',
+  age: 18,
+}

联合类型

阅读到这里,对 JavaScript 的数据和对象如何在 TypeScript 定义类型相信没有太大问题了吧!

所以这里先插入一个知识点,在介绍 对象(接口) 的类型定义时,提到 Omit 的帮助类型,它的类型里面有一个写法是 string | number | symbol ,这其实是 TypeScript 的一个联合类型。

当一个变量可能出现多种类型的值的时候,可以使用联合类型来定义它,类型之间用 | 符号分隔。

举一个简单的例子,下面这个函数接收一个代表 “计数” 的入参,并拼接成一句话打印到控制台,因为最终打印出来的句子是字符串,所以参数没有必要非得是数值,传字符串也是可以的,所以就可以使用联合类型:

ts
// 可以在 demo 里运行这段代码
+function counter(count: number | string) {
+  console.log(`The current count is: ${count}.`)
+}
+
+// 不论传数值还是字符串,都可以达到的目的
+counter(1)  // The current count is: 1.
+counter('2')  // The current count is: 2.

TIP

注意在上面 counter 函数的 console.log 语句里,使用了一个 `` 符号来定义字符串,这是 ES6 语法里的 模板字符串 ,它和传统的单引号 / 双引号相比更为灵活,特别是遇到字符串需要配合多变量拼接和换行的情况。

对 JavaScript 后面推出的新语法不太熟悉的话,很容易和单引号混淆,在学名上,它也被称之为 “反引号” ( Backquote ) ,可以使用标准键盘的 ESC 键下方、也就是 1 左边的那个按键打出来。

在实际的业务场景中,例如 Vue 的路由在不同的数据结构里也有不同的类型,有时候需要通过路由实例来判断是否符合要求的页面,也需要用到这种联合类型:

ts
// 注意:这不是完整的代码,只是一个使用场景示例
+import type { RouteRecordRaw, RouteLocationNormalizedLoaded } from 'vue-router'
+
+function isArticle(
+  route: RouteRecordRaw | RouteLocationNormalizedLoaded
+): boolean {
+  // ...
+}

再举个例子,是用 Vue 做页面,会涉及到子组件或者 DOM 的操作,当它们还没有渲染出来时,获取到的是 null ,渲染后才能拿到组件或者 DOM 结构,这种场景也可以使用联合类型:

ts
// querySelector 拿不到 DOM 的时候返回 null
+const ele: HTMLElement | null = document.querySelector('.main')

最后这个使用场景在 Vue 单组件的 DOM 元素与子组件 一节里也有相关的讲解。

当决定使用联合类型的时候,大部分情况下可能需要对变量做一些类型判断再写逻辑,当然有时候也可以无所谓,就像第一个例子拼接字符串那样。

这一小节在这里做简单了解即可,因为下面会继续配合不同的知识点把这个联合类型再次拿出来讲,比如 函数的重载 部分。

函数

函数是 JavaScript 里最重要的成员之一,所有的功能实现都是基于函数。

函数的基本的写法

在 JavaScript ,函数有很多种写法:

js
// 注意:这是 JavaScript 代码
+
+// 写法一:函数声明
+function sum1(x, y) {
+  return x + y
+}
+
+// 写法二:函数表达式
+const sum2 = function (x, y) {
+  return x + y
+}
+
+// 写法三:箭头函数
+const sum3 = (x, y) => x + y
+
+// 写法四:对象上的方法
+const obj = {
+  sum4(x, y) {
+    return x + y
+  },
+}
+
+// 还有很多……

但其实离不开两个最核心的操作:输入与输出,也就是对应函数的 “入参” 和 “返回值” ,在 TypeScript ,函数本身和 TS 类型有关系的也是在这两个地方。

函数的入参是把类型写在参数后面,返回值是写在圆括号后面,把上面在 JavaScript 的这几个写法,转换成 TypeScript 看看区别在哪里:

ts
// 注意:这是 TypeScript 代码
+
+// 写法一:函数声明
+function sum1(x: number, y: number): number {
+  return x + y
+}
+
+// 写法二:函数表达式
+const sum2 = function(x: number, y: number): number {
+  return x + y
+}
+
+// 写法三:箭头函数
+const sum3 = (x: number, y: number): number => x + y
+
+// 写法四:对象上的方法
+const obj = {
+  sum4(x: number, y: number): number {
+    return x + y
+  }
+}
+
+// 还有很多……

是不是一下子 Get 到了技巧!函数的类型定义也是非常的简单,掌握这个技巧可以让解决大部分常见的函数。

函数的可选参数

实际业务中会遇到有一些函数入参是可选,可以用和 对象(接口) 一样,用 ? 来定义:

ts
// 注意 isDouble 这个入参后面有个 ? 号,表示可选
+function sum(x: number, y: number, isDouble?: boolean): number {
+  return isDouble ? (x + y) * 2 : x + y
+}
+
+// 这样传参都不会报错,因为第三个参数是可选的
+sum(1, 2) // 3
+sum(1, 2, true) // 6

TIP

需要注意的是,可选参数必须排在必传参数的后面。

无返回值的函数

除了有返回值的函数,更多时候是不带返回值的,例如下面这个例子,这种函数用 void 来定义它的返回,也就是空。

ts
// 注意这里的返回值类型
+function sayHi(name: string): void {
+  console.log(`Hi, ${name}!`)
+}
+
+sayHi('Petter') // Hi, Petter!

需要注意的是, voidnullundefined 不可以混用,如果的函数返回值类型是 null ,那么是真的需要 return 一个 null 值:

ts
// 只有返回 null 值才能定义返回类型为 null
+function sayHi(name: string): null {
+  console.log(`Hi, ${name}!`)
+  return null
+}

有时候要判断参数是否合法,不符合要求时需要提前终止执行(比如在做一些表单校验的时候),这种情况下也可以用 void

ts
function sayHi(name: string): void {
+  // 这里判断参数不符合要求则提前终止运行,但它没有返回值
+  if (!name) return
+
+  // 否则正常运行
+  console.log(`Hi, ${name}!`)
+}

异步函数的返回值

对于异步函数,需要用 Promise<T> 类型来定义它的返回值,这里的 T 是泛型,取决于该函数最终返回一个什么样的值( async / await 也适用这个类型)。

例如这个例子,这是一个异步函数,会 resolve 一个字符串,所以它的返回类型是 Promise<string> (假如没有 resolve 数据,那么就是 Promise<void> )。

ts
// 注意这里的返回值类型
+function queryData(): Promise<string> {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve('Hello World')
+    }, 3000)
+  })
+}
+
+queryData().then((data) => console.log(data))

函数本身的类型

细心的开发者可能会有个疑问,通过函数表达式或者箭头函数声明的函数,这样写好像只对函数体的类型进行了定义,而左边的变量并没有指定。

没错,确实是没有为这个变量指定类型:

ts
// 这里的 sum ,确实是没有指定类型
+const sum = (x: number, y: number): number => x + y

这是因为,通常 TypeScript 会根据函数体自动推导,所以可以省略这里的定义。

如果确实有必要,可以这样来定义等号左边的类型:

ts
const sum: (x: number, y: number) => number = (x: number, y: number): number =>
+  x + y

这里出现了 2 个箭头 => ,注意第一个箭头是 TypeScript 的,第二个箭头是 JavaScript ES6 的。

实际上上面这句代码是分成了三部分:

  1. const sum: (x: number, y: number) => number 是这个函数的名称和类型
  2. = (x: number, y: number) 这里是指明了函数的入参和类型
  3. : number => x + y 这里是函数的返回值和类型

第 2 和 3 点相信从上面的例子已经能够理解了,所以注意力放在第一点:

TypeScript 的函数类型是以 () => void 这样的形式来写的:左侧圆括号是函数的入参类型,如果没有参数,就只有一个圆括号,如果有参数,就按照参数的类型写进去;右侧则是函数的返回值。

事实上由于 TypeScript 会推导函数类型,所以很少会显式的去写出来,除非在给对象定义方法:

ts
// 对象的接口
+interface Obj {
+  // 上面的方法就需要显式的定义出来
+  sum: (x: number, y: number) => number
+}
+
+// 声明一个对象
+const obj: Obj = {
+  sum(x: number, y: number): number {
+    return x + y
+  }
+}

函数的重载

在未来的实际开发中,可能会接触到一个 API 有多个 TS 类型的情况,比如 Vue 的 watch API

Vue 的这个 watch API 在被调用时,需要接收一个数据源参数,当侦听单个数据源时,它匹配了类型 1 ,当传入一个数组侦听多个数据源时,它匹配了类型 2 。

这个知识点其实就是 TypeScript 里的函数重载。

先来看一下在不使用函数重载时应该如何编写代码:

ts
// 对单人或者多人打招呼
+function greet(name: string | string[]): string | string[] {
+  if (Array.isArray(name)) {
+    return name.map((n) => `Welcome, ${n}!`)
+  }
+  return `Welcome, ${name}!`
+}
+
+// 单个问候语
+const greeting = greet('Petter')
+console.log(greeting) // Welcome, Petter!
+
+// 多个问候语
+const greetings = greet(['Petter', 'Tom', 'Jimmy'])
+console.log(greetings)
+// [ 'Welcome, Petter!', 'Welcome, Tom!', 'Welcome, Jimmy!' ]

TIP

注意这里的入参和返回值使用了 TypeScript 的 联合类型 ,不了解的话请先重温知识点。

虽然代码逻辑部分还是比较清晰的,区分了入参的数组类型和字符串类型,返回不同的结果,但是,在入参和返回值的类型这里,却显得非常乱。

并且这样子写,下面在调用函数时,定义的变量也无法准确的获得它们的类型:

ts
// 此时这个变量依然可能有多个类型
+const greeting: string | string[]

如果要强制确认类型,需要使用 TS 的 类型断言 (留意后面的 as 关键字):

ts
const greeting = greet('Petter') as string
+const greetings = greet(['Petter', 'Tom', 'Jimmy']) as string[]

这无形的增加了编码时的心智负担。

此时,利用 TypeScript 的函数重载就非常有用!来看一下具体如何实现:

ts
// 这一次用了函数重载
+function greet(name: string): string  // TS 类型
+function greet(name: string[]): string[]  // TS 类型
+function greet(name: string | string[]) {
+  if (Array.isArray(name)) {
+    return name.map((n) => `Welcome, ${n}!`)
+  }
+  return `Welcome, ${name}!`
+}
+
+// 单个问候语,此时只有一个类型 string
+const greeting = greet('Petter')
+console.log(greeting) // Welcome, Petter!
+
+// 多个问候语,此时只有一个类型 string[]
+const greetings = greet(['Petter', 'Tom', 'Jimmy'])
+console.log(greetings)
+// [ 'Welcome, Petter!', 'Welcome, Tom!', 'Welcome, Jimmy!' ]

上面是利用函数重载优化后的代码,可以看到一共写了 3 行 function greet … ,区别如下:

第 1 行是函数的 TS 类型,告知 TypeScript ,当入参为 string 类型时,返回值也是 string ;

第 2 行也是函数的 TS 类型,告知 TypeScript ,当入参为 string[] 类型时,返回值也是 string[] ;

第 3 行开始才是真正的函数体,这里的函数入参需要把可能涉及到的类型都写出来,用以匹配前两行的类型,并且这种情况下,函数的返回值类型可以省略,因为在第 1 、 2 行里已经定义过返回类型了。

任意值

如果实在不知道应该如何定义一个变量的类型, TypeScript 也允许使用任意值。

还记得在 为什么需要类型系统 的用的那个例子吗?再次放到 src/ts/index.ts 里:

ts
// 这段代码在 TS 里运行会报错
+function getFirstWord(msg) {
+  console.log(msg.split(' ')[0])
+}
+
+getFirstWord('Hello World')
+
+getFirstWord(123)

运行 npm run dev:ts 的时候,会得到一句报错 Parameter 'msg' implicitly has an 'any' type. ,意思是这个参数带有隐式 any 类型。

这里的 any 类型,就是 TypeScript 任意值。

既然报错是 “隐式” ,那 “显式” 的指定就可以了,当然,为了程序能够正常运行,还提高一下函数体内的代码健壮性:

ts
// 这里的入参显式指定了 any
+function getFirstWord(msg: any) {
+  // 这里使用了 String 来避免程序报错
+  console.log(String(msg).split(' ')[0])
+}
+
+getFirstWord('Hello World')
+
+getFirstWord(123)

这次就不会报错了,不论是传 string 还是 number 还是其他类型,都可以正常运行。

TIP

使用 any 的目的是让在开发的过程中,可以不必在无法确认类型的地方消耗太多时间,不代表不需要注意代码的健壮性。

一旦使用了 any ,代码里的逻辑请务必考虑多种情况进行判断或者处理兼容。

npm 包

虽然现在从 npm 安装的包都基本自带 TS 类型了,不过也存在一些包没有默认支持 TypeScript ,比如前面提到的 md5

在 TS 文件里导入并使用这个包的时候,会编译失败,比如在前面的 Hello TypeScript demo 里敲入以下代码:

ts
// src/ts/index.ts
+import md5 from 'md5'
+console.log(md5('Hello World'))

在命令行执行 npm run dev:ts 之后,会得到一段报错信息:

bash
src/ts/index.ts:1:17 - error TS7016:
+Could not find a declaration file for module 'md5'.
+'D:/Project/demo/hello-node/node_modules/md5/md5.js' implicitly has an 'any' type.
+  Try `npm i --save-dev @types/md5` if it exists
+  or add a new declaration (.d.ts) file
+  containing `declare module 'md5';`
+
+1 import md5 from 'md5'
+                  ~~~~~

这是因为缺少 md5 这个包的类型定义,根据命令行的提示,安装 @types/md5 这个包。

这是因为这些包是很早期用 JavaScript 编写的,因为功能够用作者也没有进行维护更新,所以缺少相应的 TS 类型,因此开源社区推出了一套 @types 类型包,专门处理这样的情况。

@types 类型包的命名格式为 @types/<package-name> ,也就是在原有的包名前面拼接 @types ,日常开发要用到的知名 npm 包都会有相应的类型包,只需要将其安装到 package.json 的 devDependencies 里即可解决该问题。

来安装一下 md5 的类型包:

bash
npm install -D @types/md5

再次运行就不会报错了!

bash
npm run dev:ts
+
+> demo@1.0.0 dev:ts
+> ts-node src/ts/index.ts
+
+b10a8db164e0754105b7a99be72e3fe5

类型断言

在讲解 函数的重载 的时候,提到了一个用法:

ts
const greeting = greet('Petter') as string

这里的 值 as 类型 就是 TypeScript 类型断言的语法,它还有另外一个语法是 <类型>值

当一个变量应用了 联合类型 时,在某些时候如果不显式的指明其中的一种类型,可能会导致后续的代码运行报错。

这个时候就可以通过类型断言强制指定其中一种类型,以便程序顺利运行下去。

常见的使用场景

把函数重载时最开始用到的那个例子,也就是下面的代码放到 src/ts/index.ts 里:

ts
// 对单人或者多人打招呼
+function greet(name: string | string[]): string | string[] {
+  if (Array.isArray(name)) {
+    return name.map((n) => `Welcome, ${n}!`)
+  }
+  return `Welcome, ${name}!`
+}
+
+// 虽然已知此时应该是 string[]
+// 但 TypeScript 还是会认为这是 string | string[]
+const greetings = greet(['Petter', 'Tom', 'Jimmy'])
+
+// 会导致无法使用 join 方法
+const greetingSentence = greetings.join(' ')
+console.log(greetingSentence)

执行 npm run dev:ts ,可以清楚的看到报错原因,因为 string 类型不具备 join 方法。

bash
src/ts/index.ts:11:31 - error TS2339:
+Property 'join' does not exist on type 'string | string[]'.
+  Property 'join' does not exist on type 'string'.
+
+11 const greetingStr = greetings.join(' ')
+                                 ~~~~

此时利用类型断言就可以达到目的:

ts
// 对单人或者多人打招呼
+function greet(name: string | string[]): string | string[] {
+  if (Array.isArray(name)) {
+    return name.map((n) => `Welcome, ${n}!`)
+  }
+  return `Welcome, ${name}!`
+}
+
+// 已知此时应该是 string[] ,所以用类型断言将其指定为 string[]
+const greetings = greet(['Petter', 'Tom', 'Jimmy']) as string[]
+
+// 现在可以正常使用 join 方法
+const greetingSentence = greetings.join(' ')
+console.log(greetingSentence)

需要注意的事情

但是,请不要滥用类型断言,只在能够确保代码正确的情况下去使用它,来看一个反例:

ts
// 原本要求 age 也是必须的属性之一
+interface User {
+  name: string
+  age: number
+}
+
+// 但是类型断言过程中,遗漏了
+const petter = {} as User
+petter.name = 'Petter'
+
+// TypeScript 依然可以运行下去,但实际上的数据是不完整的
+console.log(petter) // { name: 'Petter' }

TIP

使用类型断言可以让 TypeScript 不再检查该代码,默认是正确无误的。

所以,请务必保证这段代码真的是正确的!

类型推论

还记得在讲 原始数据类型 的时候,最后提到的:

不过在实际的编程过程中,原始数据类型的类型定义是可以省略的,因为 TypeScript 会根据声明变量时赋值的类型,自动帮推导变量类型

这其实是 TypeScript 的类型推论功能,当在声明变量的时候可以确认它的值,那么 TypeScript 也可以在这个时候帮推导它的类型,这种情况下就可以省略一些代码量。

下面这个变量这样声明是 OK 的,因为 TypeScript 会推导 msgstring 类型。

ts
// 相当于 msg: string
+let msg = 'Hello World'
+
+// 所以要赋值为 number 类型时会报错
+msg = 3 // Type 'number' is not assignable to type 'string'

下面这段代码也是可以正常运行的,因为 TypeScript 会根据 return 的结果推导 getRandomNumber 的返回值是 number 类型,从而推导变量 num 也是 number 类型。

ts
// 相当于 getRandomNumber(): number
+function getRandomNumber() {
+  return Math.round(Math.random() * 10)
+}
+
+// 相当于 num: number
+const num = getRandomNumber()

类型推论的前提是变量在声明时有明确的值,如果一开始没有赋值,那么会被默认为 any 类型。

ts
// 此时相当于 foo: any
+let foo
+
+// 所以可以任意改变类型
+foo = 1 // 1
+foo = true // true

类型推论可以节约很多书写工作量,在确保变量初始化有明确的值的时候,可以省略其类型,但必要的时候,该写上的还是要写上。

如何编译为 JavaScript 代码

学习到这里,对于 TypeScript 的入门知识已经学到了吧!

前面学习的时候,一直是基于 dev:ts 命令,它调用的是 ts-node 来运行的 TS 文件:

json
{
+  // ...
+  "scripts": {
+    // ...
+    "dev:ts": "ts-node src/ts/index.ts"
+  }
+  // ...
+}

但最终可能需要的是一个 JS 文件,比如要通过 <script src> 来放到 HTML 页面里,这就涉及到对 TypeScript 的编译。

来看看如何把一个 TS 文件编译成 JS 文件,让其从 TypeScript 变成 JavaScript 代码。

编译单个文件

先在 package.json 里增加一个 build script :

json
{
+  "name": "hello-node",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "dev:cjs": "node src/cjs/index.cjs",
+    "dev:esm": "node src/esm/index.mjs",
+    "dev:ts": "ts-node src/ts/index.ts",
+    "build": "tsc src/ts/index.ts --outDir dist",
+    "compile": "babel src/babel --out-dir compiled",
+    "serve": "node server/index.js"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "md5": "^2.3.0"
+  },
+  "devDependencies": {
+    "@types/md5": "^2.3.2",
+    "ts-node": "^10.7.0",
+    "typescript": "^4.6.3"
+  }
+}

这样在命令行运行 npm run build 的时候,就会把 src/ts/index.ts 这个 TS 文件编译,并输出到项目下与 src 文件夹同级的 dist 目录下。

其中 tsc 是 TypeScript 用来编译文件的命令, --outDir 是它的一个选项,用来指定输出目录,如果不指定,则默认生成到源文件所在的目录下面。

把之前在 函数的重载 用过的这个例子放到 src/ts/index.ts 文件里,因为它是一段比较典型的、包含了多个知识点的 TypeScript 代码:

ts
// 对单人或者多人打招呼
+function greet(name: string): string
+function greet(name: string[]): string[]
+function greet(name: string | string[]) {
+  if (Array.isArray(name)) {
+    return name.map((n) => `Welcome, ${n}!`)
+  }
+  return `Welcome, ${name}!`
+}
+
+// 单个问候语
+const greeting = greet('Petter')
+console.log(greeting)
+
+// 多个问候语
+const greetings = greet(['Petter', 'Tom', 'Jimmy'])
+console.log(greetings)

可以先执行 npm run dev:ts 测试它的可运行性,当然,如果期间的代码运行有问题,在编译阶段也会给报错。

现在来编译它,现在在命令行输入 npm run build 并回车执行。

可以看到多了一个 dist 文件夹,里面多了一个 index.js 文件。

bash
hello-node
+ # 构建产物
+├─dist
+  # 编译后的 JS 文件
+ └─index.js
+ # 依赖文件夹
+├─node_modules
+ # 源码文件夹
+├─src
+ # 锁定安装依赖的版本号
+├─package-lock.json
+ # 项目清单
+└─package.json

index.js 文件里面的代码如下:

js
function greet(name) {
+  if (Array.isArray(name)) {
+    return name.map(function (n) {
+      return 'Welcome, '.concat(n, '!')
+    })
+  }
+  return 'Welcome, '.concat(name, '!')
+}
+// 单个问候语
+var greeting = greet('Petter')
+console.log(greeting)
+// 多个问候语
+var greetings = greet(['Petter', 'Tom', 'Jimmy'])
+console.log(greetings)

可以看到已经成功把 TypeScript 代码编译成 JavaScript 代码了。

在命令行执行 node dist/index.js ,像之前测试 JS 文件一样使用 node 命令,运行 dist 目录下的 index.js 文件,它可以正确运行:

bash
node dist/index.js
+Welcome, Petter!
+[ 'Welcome, Petter!', 'Welcome, Tom!', 'Welcome, Jimmy!' ]

编译多个模块

刚才只是编译一个 index.ts 文件,如果 index.ts 里引入了其他模块,此时 index.ts 是作为入口文件,入口文件 import 进来使用的模块也会被 TypeScript 一并编译。

拆分一下模块,把 greet 函数单独抽离成一个模块文件 src/ts/greet.ts

ts
// src/ts/greet.ts
+function greet(name: string): string
+function greet(name: string[]): string[]
+function greet(name: string | string[]) {
+  if (Array.isArray(name)) {
+    return name.map((n) => `Welcome, ${n}!`)
+  }
+  return `Welcome, ${name}!`
+}
+
+export default greet

src/ts/index.ts 这边,把这个模块导进来:

ts
// src/ts/index.ts
+import greet from './greet'
+
+// 单个问候语
+const greeting = greet('Petter')
+console.log(greeting)
+
+// 多个问候语
+const greetings = greet(['Petter', 'Tom', 'Jimmy'])
+console.log(greetings)

package.json 里的 build script 无需修改,依然只编译 index.ts ,但因为导入了 greet.ts ,所以 TypeScript 也会一并编译,来试一下运行 npm run build , 现在 dist 目录下有两个文件了:

bash
hello-node
+ # 构建产物
+├─dist
+ ├─greet.js  # 多了这个文件
+ └─index.js
+
+ # 其他文件这里省略...
+└─package.json

来看看这一次的编译结果:

先看看 greet.js

js
// dist/greet.js
+'use strict'
+exports.__esModule = true
+function greet(name) {
+  if (Array.isArray(name)) {
+    return name.map(function (n) {
+      return 'Welcome, '.concat(n, '!')
+    })
+  }
+  return 'Welcome, '.concat(name, '!')
+}
+exports['default'] = greet

再看看 index.js

js
// dist/index.js
+'use strict'
+exports.__esModule = true
+var greet_1 = require('./greet')
+// 单个问候语
+var greeting = (0, greet_1['default'])('Petter')
+console.log(greeting)
+// 多个问候语
+var greetings = (0, greet_1['default'])(['Petter', 'Tom', 'Jimmy'])
+console.log(greetings)

这个代码风格有没有觉得似曾相识?是的,就是前面提到的 CommonJS 模块代码。

其实在 编译单个文件 代码的时候,它也是 CommonJS ,只不过因为只有一个文件,没有涉及到模块化,所以第一眼看不出来。

还是在命令行执行 node dist/index.js ,虽然也是运行 dist 目录下的 index.js 文件,但这次它的作用是充当一个入口文件了,引用到的 greet.js 模块文件也会被调用。

这次一样可以得到正确的结果:

bash
node dist/index.js
+Welcome, Petter!
+[ 'Welcome, Petter!', 'Welcome, Tom!', 'Welcome, Jimmy!' ]

修改编译后的 JS 版本

还可以修改编译配置,让 TypeScript 编译成不同的 JavaScript 版本。

修改 package.json 里的 build script ,在原有的命令后面增加一个 --target 选项:

json
{
+  // ...
+  "scripts": {
+    // ...
+    "build": "tsc src/ts/index.ts --outDir dist --target es6"
+  }
+  // ...
+}

--target 选项的作用是控制编译后的 JavaScript 版本,可选的值目前有: es3es5es6es2015es2016es2017es2018es2019es2020es2021es2022esnext ,分别对应不同的 JS 规范(所以未来的可选值会根据 JS 规范一起增加)。

之前编译出来的 JavaScript 是 CommonJS 规范 ,本次配置的是 es6 ,这是支持 ES Module 规范 的版本。

TIP

通常还需要配置一个 --module 选项,用于决定编译后是 CJS 规范还是 ESM 规范,但如果缺省,会根据 --target 来决定。

再次在命令行运行 npm run build ,这次看看变成了什么:

先看看 greet.js

js
// dist/greet.js
+function greet(name) {
+  if (Array.isArray(name)) {
+    return name.map((n) => `Welcome, ${n}!`)
+  }
+  return `Welcome, ${name}!`
+}
+export default greet

再看看 index.js

js
// dist/index.js
+import greet from './greet'
+// 单个问候语
+const greeting = greet('Petter')
+console.log(greeting)
+// 多个问候语
+const greetings = greet(['Petter', 'Tom', 'Jimmy'])
+console.log(greetings)

这次编译出来的都是基于 ES6 的 JavaScript 代码,因为涉及到 ESM 模块,所以不能直接在 node 运行它了,可以手动改一下扩展名,改成 .mjs (包括 index 文件里 import 的 greet 文件名也要改),然后再运行 node dist/index.mjs

其他事项

在尝试 编译单个文件编译多个模块 的时候,相信各位开发者应该没有太大的疑问,但是来到 修改编译后的 JS 版本 这里,事情就开始变得复杂了起来,应该能感觉到编译的选项和测试成本都相应的增加了很多。

事实上刚才编译的 JS 文件,因为涉及到 ESM 模块化,是无法通过普通的 <script /> 标签在 HTML 页面里使用的(单个文件可以,因为没有涉及模块),不仅需要加上 ESM 模块所需的 <script type="module" /> 属性,本地开发还需要启动本地服务器通过 HTTP 协议访问页面,才允许在浏览器里使用 ESM 模块(详见: 在浏览器里访问 ESM 一节)。

因此在实际的项目开发中,需要借助 构建工具 来处理很多编译过程中的兼容性问题,降低开发成本。

而刚才用到的诸如 --target 这样的选项,可以用一个更简单的方式来管理,类似于 package.json 项目清单, TypeScript 也有一份适用于项目的配置清单,请看 了解 tsconfig.json 部分。

了解 tsconfig.json

TypeScript 项目一般都会有一个 tsconfig.json 文件,放置于项目的根目录下,这个文件的作用是用来管理 TypeScript 在编译过程中的一些选项配置。

在开始之前,需要全局安装一下 TypeScript :

bash
npm install -g typescript

这样就可以使用 TypeScript 提供的全局功能,可以直接在命令行里使用 tsc 命令了(之前本地安装的时候,需要封装成 package.json 的 script 才能调用它)。

依然是用的 Hello TypeScript demo ,记得先通过 cd 命令进入项目所在的目录。

在命令行输入 tsc --init ,这是 TypeScript 提供的初始化功能,会生成一个默认的 tsconfig.json 文件。

bash
tsc --init
+
+Created a new tsconfig.json with:
+                                                                               TS
+  target: es2016
+  module: commonjs
+  strict: true
+  esModuleInterop: true
+  skipLibCheck: true
+  forceConsistentCasingInFileNames: true
+
+
+You can learn more at https://aka.ms/tsconfig.json

现在的目录结构是这样子,多了一个 tsconfig.json 文件:

bash
hello-node
+ # 构建产物
+├─dist
+ # 依赖文件夹
+├─node_modules
+ # 源码文件夹
+├─src
+ # 锁定安装依赖的版本号
+├─package-lock.json
+ # 项目清单
+├─package.json
+ # TypeScript 配置
+└─tsconfig.json

每一个 tsc 的命令行的选项,都可以作为这个 JSON 的一个字段来管理,例如刚才的 --outDir--target 选项,在这个 JSON 文件里对应的就是:

json
{
+  "compilerOptions": {
+    "target": "es6",
+    "module": "es6",
+    "outDir": "./dist"
+  }
+}

可以直接在生成的 tsconfig.json 上面修改。

来试试效果,这一次不需要用到 package.json 里的 build script 了,直接在命令行运行 tsc ,它现在会根据配置的 tsconfig.json 文件,按照的要求来编译。

可以看到它依然按照要求在 dist 目录下生成编译后的 JS 文件,而且这一次的编译结果,和在 build script 里使用的 tsc src/ts/index.ts --outDir dist --target es6 这一长串命令是一样的。

所以正常工作中,都是使用 tsconfig.json 来管理 TypeScript 的配置的。

完整的选项可以查看 TypeScript 官网: tsconfig - typescriptlang

不过实际工作中的项目都是通过一些脚手架创建的,例如 Vue CLI ,或者现在的 Create Vue 或者 Create Preset ,都会在创建项目模板的时候,提前配置好通用的选项,只需要在不满足条件的情况下去调整。

Released under the MIT License.

+ + + + \ No newline at end of file diff --git a/upgrade.html b/upgrade.html new file mode 100644 index 00000000..09b7edb1 --- /dev/null +++ b/upgrade.html @@ -0,0 +1,206 @@ + + + + + + 脚手架的升级与配置 | Vue3 入门指南与实战案例 + + + + + + + + + + + + + +
Skip to content
目录

脚手架的升级与配置

相信在 “了解前端工程化” 一章里阅读过 Vue.js 与工程化 一节内容的开发者,可以轻松猜到本书接下来关于 Vue 3 的学习都将基于前端工程化展开,本章将介绍如何配置 Vue 3 的开发环境,并创建基于前端工程化的 Vue 3 项目。

TIP

如果还不熟悉 Node.js 、 npm 依赖管理等前端工程化工具链的使用,请先阅读 工程化的前期准备 一章。

全新的 Vue 版本 ~new

在 2022 年 2 月 7 日, Vue 3 代替了 Vue 2 成为 Vue 的默认版本,有一些注意事项需要留意:

使用 Vue 3

在 npmjs 网站 Vue 主页的版本列表 上面,可以看到当前已使用 3.x.x 这样的版本号作为 latest 这个 Tag 对应的版本,也就是运行 npm i vue 默认会安装 Vue 3 了,无需再和以前一样,需要指定 vue@next 才可以安装到 Vue 3 。

包括 vue-routervuexvue-loader@vue/test-utils 等相关的生态,同样不需要指定 next 版本了,都配合 Vue 3 指定了新的 latest 默认版本。

同时 Vue 生态的所有官方文档也都默认切换到 Vue 3 版本,可在 官方文档 一节了解最新的官方资源站点。

使用 Vue 2

如果还需要使用 Vue 2 ,则在安装的时候需要手动指定 Tag 为 legacy 或者 v2-latest 才能安装到 Vue 2 :

bash
# 安装 2.6.x 的最新版本
+npm i vue@legacy
+
+# 安装 2.7.x 的最新版本
+npm i vue@v2-latest

注意到 Vue 2 配对了两个不同的 Tag ,分别对应 2.7 系列和 2.6 系列。

TIP

Vue 2.7 系列是在 Vue 2 的基础上,对标 Vue 3 的功能支持所作的升级,主要是面向想使用 Vue 3 的新特性、但顾虑于产品对旧浏览器的支持而无法贸然升级的开发者。

Vue 2.7 与 Vue 2.6 之前的旧版本在使用上略有不同,具体可以查看 Vue 2 的 更新记录 了解具体的差异化。

对于一些没有打 Tag 的 Vue 2 相关生态(如 vuex 截止到撰写本文时还没有为旧版本打 Tag ),则需要显式的指定版本号才可以安装到配套的程序:

bash
# 显式的指定具体版本号安装
+npm i vuex@3.6.2

如果之前使用了 latest 标签或 * 从 npm 安装 Vue 或其他官方库,请确保项目下的 package.json 文件能够明确使用兼容 Vue 2 的版本。

diff
{
+  "dependencies": {
+-   "vue": "latest",
++   "vue": "^2.6.14",
+-   "vue-router": "latest",
++   "vue-router": "^3.5.3",
+-   "vuex": "latest"
++   "vuex": "^3.6.2"
+  },
+  "devDependencies": {
+-   "vue-loader": "latest",
++   "vue-loader": "^15.9.8",
+-   "@vue/test-utils": "latest"
++   "@vue/test-utils": "^1.3.0"
+  }
+}

TIP

上方代码块里的 - 号代表移除, + 号代表新增,这是一种 Diff 风格的排版,表明修改前后的变化,后文如有类似的代码风格同理。

Hello Vue3

如果想早点开始 Vue 3 的世界,可以通过以下命令直接创建一个启动项目:

bash
# 全局安装脚手架
+npm install -g create-preset
+
+# 使用 `vue3-ts-vite` 模板创建一个名为 `hello-vue3` 的项目
+preset init hello-vue3 --template vue3-ts-vite

这是一个基于 Vite + TypeScript + Vue 3 + Pinia 的项目启动模板,可以使用这个项目来练习后面的案例代码,创建完毕后可以直接跳到 安装 VSCode添加 VSCode 插件 继续学习。

当然有时间还是希望继续阅读 使用 Vite 创建项目使用 @vue/cli 创建项目 这两部分内容,了解 Vue 3 更主流的项目创建方案。

TIP

如果网络问题下载失败,可以先执行 preset proxy on 开启加速镜像代理下载。

使用 Vite 创建项目 ~new

Vite 是由 Vue 作者尤雨溪先生带领团队开发的一个构建工具,它利用浏览器原生支持 ES 模块的特点,极大提升了开发体验,自 2021 年 1 月份发布 2.0 版本以来,发展非常快,笔者也在第一时间参与贡献了一些文档和插件,并且在 2021 年期间,个人项目已经全面切换到 Vite ,公司业务也在 2021 年底开始使用 Vite 创建新项目,整体情况非常稳定,前景非常乐观。

关于是否使用 Vite 和安利团队使用 Vue 3 ,可以看笔者在 2022 年春节前写的 Markdown 工程师的一周 一文,记录了一次关于团队技术栈升级的总结,笔者是非常推荐升级技术栈的!

在这里推荐以下这几种创建 Vite 项目的方式:Create ViteCreate VueCreate Preset

Create Vite

create-vite 是 Vite 官方推荐的一个脚手架工具,可以创建基于 Vite 的不同技术栈基础模板。

运行以下命令创建模板项目,再按照命令行的提示操作(选择 vue 技术栈进入),即可创建一个基于 Vite 的基础空项目。

bash
npm create vite

不过这个方式创建的项目非常基础,如果需要用到 Router 、 Vuex 、 ESLint 等程序,都需要再单独安装和配置,所以推荐使用 Create Preset

Create Vue

create-vue 是 Vue 官方推出的一个新脚手架,用以代替基于 Webpack 的 Vue CLI ,它可以创建基于 Vite 的 Vue 基础模板。

运行以下命令创建模板项目,然后根据命令行的提示操作即可。

bash
npm init vue@3

Create Preset

create-preset 是 Awesome Starter 的 CLI 脚手架,提供快速创建预设项目的能力,可以创建一些有趣实用的项目启动模板,也可以用来管理的常用项目配置。

简单使用

可以通过包管理器直接创建配置,然后按照命令行的提示操作,即可创建开箱即用的模板项目。

bash
npm create preset

在这里选择 vue 技术栈进入,选择 vue3-ts-vite 创建一个基于 Vite + Vue 3 + TypeScript 的项目启动模板。

TIP

如果下载失败,可以通过 npm create preset proxy on 开启加速镜像代理下载。

点击查看:代理选项 - Create Preset

全局安装

也可以像使用 @vue/cli 一样,全局安装到本地,通过 preset init 命令来创建项目。

推荐全局安装它,用起来更方便,请先全局安装:

bash
npm install -g create-preset

可以通过下面这个命令来检查安装是否成功,如果成功,将会得到一个版本号:

bash
preset -v

然后可以通过 --template 选项直接指定一个模板创建项目,在这里使用 vue3-ts-vite 模板创建一个名为 hello-vue3 的项目:

bash
preset init hello-vue3 --template vue3-ts-vite

常用的项目模板也可以绑定为本地配置,点击 Create Preset 官方文档 查看完整使用教程。

管理项目配置

不论使用上方哪种方式创建项目,在项目的根目录下都会有一个名为 vite.config.jsvite.config.ts 的项目配置文件(其扩展名由项目使用 JavaScript 还是 TypeScript 决定)。

里面会有一些预设好的配置,可以在 Vite 官网的配置文档 查阅更多的可配置选项。

使用 @vue/cli 创建项目

如果不习惯 Vite ,依然可以使用 Vue CLI 作为开发脚手架。

和 Vite 的区别

Vue CLI 使用的构建工具是基于 Webpack ,可以在 了解构建工具 一节了解 Webpack 和 Vite 这两个构建工具的区别。

更新 CLI 脚手架

请先全局安装,把脚手架更新到最新版本(最低版本要求在 4.5.6 以上才能支持 Vue 3 项目的创建)。

js
npm install -g @vue/cli

使用 CLI 创建 3.x 项目 ~new

Vue CLI 全局安装后,可以在命令行输入 vue 进行操作,创建项目使用的是 create 命令:

js
vue create hello-vue3

由于要使用 TypeScript ,所以需要选择最后一个选项来进行自定义搭配,通过键盘的上下箭头进行切换选择:

js
Vue CLI v5.0.4
+? Please pick a preset:
+  Default ([Vue 3] babel, eslint)
+  Default ([Vue 2] babel, eslint)
+> Manually select features

多选菜单可以按空格选中需要的依赖,总共选择了下面这些选项:

js
Vue CLI v5.0.4
+? Please pick a preset: Manually select features
+? Check the features needed for your project: (Press <space> to select,
+<a> to toggle all, <i> to invert selection, and <enter> to proceed)
+ (*) Babel
+ (*) TypeScript
+ ( ) Progressive Web App (PWA) Support
+ (*) Router
+ (*) Vuex
+ (*) CSS Pre-processors
+>(*) Linter / Formatter
+ ( ) Unit Testing
+ ( ) E2E Testing

选择 Vue 版本,要用 Vue 3 所以需要选择 3.x :

js
? Choose a version of Vue.js that you want to start the project with
+  (Use arrow keys)
+> 3.x
+  2.x

是否选择 Class 语法的模板,在 Vue 2 版本为了更好的支持 TypeScript ,通常需要使用 Class 语法,由于 Vue 3 有了对 TypeScript 支持度更高的 Composition API ,因此选择 n ,也就是 “否” :

js
? Use class-style component syntax? (y/N) n

Babel 可以把新版本的 JavaScript 语句转换为兼容性更好的低版本 Polyfill 写法,所以选 y 确认使用:

js
? Use Babel alongside TypeScript
+  (required for modern mode, auto-detected polyfills, transpiling JSX)?
+  (Y/n) y

接下来是选择路由模式,选 y 启用 History 模式,选 n 使用 Hash 模式,可根据项目情况选择。

建议先选 y 确认,如果遇到部署的问题可以在 “路由” 一章的 部署问题与服务端配置 小节查看如何处理。

js
? Use history mode for router?
+  (Requires proper server setup for index fallback in production)
+  (Y/n) y

选择一个 CSS 预处理器,可以根据自己的喜好选择,不过鉴于目前开源社区组件常用的都是 Less ,所以也建议选择 Less 作为入门的预处理器工具。

js
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported
+ by default):
+  Sass/SCSS (with dart-sass)
+> Less
+  Stylus

Lint 规则,用来代码检查,写 TypeScript 离不开 Lint ,可以根据自己喜好选择,也可以先选择默认,后面在 添加协作规范 一节也有说明如何配置规则,这里先默认第一个:

js
? Pick a linter / formatter config: (Use arrow keys)
+> ESLint with error prevention only
+  ESLint + Airbnb config
+  ESLint + Standard config
+  ESLint + Prettier

Lint 的校验时机,一个是在保存时校验,一个是在提交 commit 的时候才校验,这里也选默认:

js
? Pick additional lint features: (Press <space> to select,
+ <a> to toggle all, <i> to invert selection, and <enter> to proceed)
+>(*) Lint on save
+ ( ) Lint and fix on commit

项目配置文件,笔者更习惯保存为独立文件:

js
? Where do you prefer placing config for Babel, ESLint, etc.?
+  (Use arrow keys)
+> In dedicated config files
+  In package.json

是否保存为未来的项目配置,存起来方便以后快速创建:

js
? Save this as a preset for future projects? Yes
+? Save preset as: vue-3-ts-config

至此,项目创建完成!可以通过 npm run serve 开启热更进行开发调试,通过 npm run build 构建打包上线。

管理项目配置

Vue CLI 的配置文件是 vue.config.js ,可以参考官网的说明文档调整各个选项配置:配置参考 - Vue CLI

调整 TS Config

如果在 Vite 的配置文件 vite.config.ts ,或者是在 Vue CLI 的配置文件 vue.config.js 里设置了 alias 的话,因为 TypeScript 不认识里面配置的 alias 别名,所以需要再对 tsconfig.json 做一点调整,增加对应的 paths ,否则在 VSCode 里可能会路径报红,提示找不到模块或其相应的类型声明。

比如在 Vue 组件里引入路径为 @cp/HelloWorld.vue 的时候,可以避免写出 ../../../../components/HelloWorld.vue 这样的非常多层级的相对路径,但是默认情况下 TypeScript 并不知道这个 alias 等价于 src/components/HelloWorld.vue 这个文件路径,从而会报错找不到该模块并导致无法正确编译。

假设在 vite.config.ts 里配置了这些 alias :

ts
export default defineConfig({
+  // ...
+  resolve: {
+    alias: {
+      '@': resolve('src'), // 源码根目录
+      '@img': resolve('src/assets/img'), // 图片
+      '@less': resolve('src/assets/less'), // 预处理器
+      '@libs': resolve('src/libs'), // 本地库
+      '@plugins': resolve('src/plugins'), // 本地插件
+      '@cp': resolve('src/components'), // 公共组件
+      '@views': resolve('src/views'), // 路由组件
+    },
+  },
+  // ...
+})

那么在该项目的 tsconfig.json 文件里就需要相应的加上这些 paths :

json
{
+  "compilerOptions": {
+    // ...
+    "paths": {
+      "@/*": ["src/*"],
+      "@img/*": ["src/assets/img/*"],
+      "@less/*": ["src/assets/less/*"],
+      "@libs/*": ["src/libs/*"],
+      "@plugins/*": ["src/plugins/*"],
+      "@cp/*": ["src/components/*"],
+      "@views/*": ["src/views/*"]
+    },
+    // ...
+  },
+  // ...
+}

TIP

注意 paths 的配置全部要以 /* 结尾,代表该目录下的文件都可以被匹配,而不是指向某一个文件。

添加协作规范

考虑到后续可能会有团队协作,最好是能够统一编码风格。

Editor Config

在项目根目录下再增加一个名为 .editorconfig 的文件。

这个文件的作用是强制编辑器以该配置来进行编码,比如缩进统一为空格而不是 Tab ,每次缩进都是 2 个空格而不是 4 个等等。

文件内容如下:

js
# http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+max_line_length = 80
+trim_trailing_whitespace = true
+
+[*.md]
+max_line_length = 0
+trim_trailing_whitespace = false

具体的参数说明可参考:项目代码风格统一神器 editorconfig 的作用与配置说明

TIP

部分编辑器可能需要安装对应的插件才可以支持该配置。

例如 VSCode 需要安装 EditorConfig for VSCode 扩展

Prettier

Prettier 是目前最流行的代码格式化工具,可以约束的代码风格不会乱七八糟,目前所知道的知名项目(如 Vue 、 Vite 、 React 等)和大厂团队(谷歌、微软、阿里、腾讯等)都在使用 Prettier 格式化代码。

通过脚手架创建的项目很多都内置了 Prettier 功能集成(例如 Create Preset ,参考了主流的格式化规范,比如 2 个空格的缩进、无需写分号结尾、数组 / 对象每一项都带有尾逗号等等)。

如果需要手动增加功能支持,请在项目根目录下创建一个名为 .prettierrc 的文件,写入以下内容:

json
{
+  "semi": false,
+  "singleQuote": true
+}

这代表 JavaScript / TypeScript 代码一般情况下不需要加 ; 分号结尾,然后使用 '' 单引号来定义字符串等变量。

这里只需要写入与默认配置不同的选项即可,如果和默认配置一致,可以省略,完整的配置选项以及默认值可以在 Prettier 官网的 Options Docs 查看。

配合 VSCode 的 VSCode Prettier 扩展,可以在编辑器里使用该规则格式化文件(此时无需在项目下安装 Prettier 依赖)。

如果开启了 ESLint ,配合 ESLint 的代码提示,可以更方便的体验格式化排版,详见 ESLint 一节的说明。

TIP

配合 VSCode Prettier 扩展 ,这份配置直接在 VSCode 里生效,如果配合 ESLint 使用,需要安装 Prettier 依赖。

ESLint

ESLint 是一个查找 JavaScript / TypeScript 代码问题并提供修复建议的工具,换句话说就是可以约束的代码不会写出一堆 BUG ,它是代码健壮性的重要保障。

虽然大部分前端开发者都不愿意接受这些约束(当年笔者入坑的时候也是),但说实话,经过 ESLint 检查过的代码质量真的高了很多,如果不愿意总是做一个游兵散勇,建议努力让自己习惯被 ESLint 检查,大厂和大项目都是有 ESLint 检查的。

特别是写 TypeScript ,配合 ESLint 的检查实在太爽了(字面意思,真的很舒服)。

通过脚手架创建的项目通常都会帮配置好 ESLint 规则,如果有一些项目是一开始没有,后面想增加 ESLint 检查,也可以手动配置具体规则。

这里以一个 Vite + TypeScript + Prettier 的 Vue 3 项目为例,在项目根目录下创建一个名为 .eslintrc.js 文件,写入以下内容:

js
module.exports = {
+  root: true,
+  env: {
+    node: true,
+    browser: true,
+  },
+  extends: ['plugin:vue/vue3-essential', 'eslint:recommended', 'prettier'],
+  parser: 'vue-eslint-parser',
+  parserOptions: {
+    parser: '@typescript-eslint/parser',
+    ecmaVersion: 2020,
+    sourceType: 'module',
+  },
+  plugins: ['@typescript-eslint', 'prettier'],
+  rules: {
+    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+    'prettier/prettier': 'warn',
+    'vue/multi-word-component-names': 'off',
+  },
+  globals: {
+    defineProps: 'readonly',
+    defineEmits: 'readonly',
+    defineExpose: 'readonly',
+    withDefaults: 'readonly',
+  },
+}

然后安装对应的依赖(记得添加 -D 参数添加到 devDependencies ,因为都是开发环境下使用的):

这样就可以在项目中生效了(如果 VSCode 未能立即生效,重启编辑器即可),一旦代码有问题, ESLint 就会帮检查出来并反馈具体的报错原因,久而久之的代码就会越写越规范。

更多的选项可以在 ESLint 官网的 Configuring ESLint 查阅。

如果有一些文件需要排除检查,可以再创建一个 .eslintignore 文件在项目根目录下,里面添加要排除的文件或者文件夹名称:

txt
dist/*

更多的排除规则可以在 ESLint 官网的 The .eslintignore File 一文查阅。

安装 VSCode

要问现在前端工程师用的最多的代码编辑器是哪个,肯定是 Visual Studio Code 了!

与其他的编辑器相比,有这些优点:

  • 背靠 Microsoft ,完全免费并且开源,开箱即用
  • 可以通过简单的配置调整来满足之前在其他编辑器上的习惯( e.g. Sublime Text )
  • 轻量级但功能强大,内置了对 JavaScript、TypeScript 和 Node.js 的支持,
  • 丰富的插件生态,可以根据的需要,安装提高编码效率的功能支持,以及其他的语言扩展
  • 智能的代码补全、类型推导、代码检查提示、批量编辑、引用跳转、比对文件等功能支持
  • 登录的 GitHub 账号即可实现配置自动同步,在其他电脑上直接使用的最习惯配置和插件

当然,还有非常多优点可自行体验!

点击下载:Visual Studio Code

一般情况下开箱即用,无门槛,也可以阅读官方文档了解一些个性化的配置。

点击查看:VSCode 操作文档

添加 VSCode 插件

VSCode 本身是轻量级的,也就是只提供最基础的功能,更优秀的体验或者个性化体验,是需要通过插件来启用的。

这里推荐几个非常舒服的 VSCode 插件,可以通过插件中心安装,也可以通过官方应用市场下载。

Chinese (Simplified)

VSCode 安装后默认是英文本,需要自己进行汉化配置, VSCode 的特色就是插件化处理各种功能,语言方面也一样。

安装该插件并启用,即可让 VSCode 显示为简体中文。

点击下载:Chinese (Simplified)

Volar

Vue 官方推荐的 VSCode 扩展,用以代替 Vue 2 时代的 Vetur ,提供了 Vue 3 的语言支持、 TypeScript 支持、基于 vue-tsc 的类型检查等功能。

点击下载:Volar

TIP

Volar 取代了 Vetur 作为 Vue 3 的官方扩展,如果之前已经安装了 Vetur ,请确保在 Vue 3 的项目中禁用它。

Vue VSCode Snippets

从实际使用 Vue 的角度提供 Vue 代码片段的生成,可以通过简单的命令,在 .vue 文件里实现大篇幅的代码片段生成,例如:

  1. 输入 ts 可以快速创建一个包含了 template + script + style 的 Vue 组件模板(可选 2.x 、3.x 以及 class 风格的模板)

  2. 也可以通过输入带有 v3 开头的指令来快速生成 Vue 3 的 API 。

下面是输入了 ts 两个字母之后,用箭头选择 vbase-3-ts 自动生成的一个模板片段,在开发过程中非常省事:

vue
<template>
+  <div></div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  setup() {
+    return {}
+  },
+})
+</script>
+
+<style scoped></style>

点击下载:Vue VSCode Snippets

Auto Close Tag

可以快速完成 HTML 标签的闭合,除非通过 .jsx / .tsx 文件编写 Vue 组件,否则在 .vue 文件里写 template 的时候肯定用得上。

点击下载:Auto Close Tag

Auto Rename Tag

假如要把 div 修改为 section,不需要再把 <div> 然后找到代码尾部的 </div> 才能修改,只需要选中前面或后面的半个标签直接修改,插件会自动把闭合部分也同步修改,对于篇幅比较长的代码调整非常有帮助。

点击下载:Auto Rename Tag

EditorConfig for VSCode

一个可以让编辑器遵守协作规范的插件,详见 添加协作规范

点击下载:EditorConfig for VSCode

Prettier for VSCode

这是 Prettier 在 VSCode 的一个扩展,不论项目有没有安装 Pretter 依赖,安装该扩展之后,单纯在 VSCode 也可以使用 Pretter 进行代码格式化。

点击下载:Prettier - Code formatter

点击访问:Prettier 官网 了解更多配置。

ESLint for VSCode

这是 ESLint 在 VSCode 的一个扩展, TypeScript 项目基本都开了 ESLint ,编辑器也建议安装该扩展支持以便获得更好的代码提示。

点击下载:VSCode ESLint

点击访问:ESLint 官网 了解更多配置。

其他插件

其他的比如预处理器相关的,Git 相关的,可以根据自己的需求在 VSCode 的插件市场 里搜索安装。

项目初始化

至此,通过脚手架已经搭好了一个可直接运行的基础项目,已经可以正常的 npm run devnpm run build 了(具体命令取决于的项目 脚本命令的配置 ),项目配置和编辑器也都弄好了,是不是可以开始写代码了呢?

请不要着急,还需要了解一点东西,就是如何初始化一个 Vue 3 项目。

因为在实际开发过程中,还会用到各种 npm 包,像很多 UI 框架、功能插件的引入都是需要在 Vue 初始化阶段处理。

甚至有时候还要脱离脚手架,采用 CDN 引入的方式来开发,所以开始写组件之前,还需要了解一下在 Vue 3 项目中,初始化阶段对比 Vue 2 的一些变化。

入口文件

项目的初始化都是在入口文件集中处理,Vue 3 的目录结构对比 Vue 2 没有变化,入口文件依然还是 main.ts 这个文件。

但是 Vue 3 在初始化的时候,做了不少的调整,代码写法和 Vue 2 是完全不同,但是对于这次大改动,笔者认为是好的,因为统一了相关生态的启用方式,不再像 Vue 2 时期那样多方式共存,显得比较杂乱。

回顾 Vue 2

Vue 2 在导入各种依赖之后,通过 new Vue() 执行 Vue 的初始化,相关的 Vue 生态和插件,有的是使用 Vue.use() 来进行初始化,有的是作为 new Vue() 的入参:

ts
import Vue from 'vue'
+import App from './App.vue'
+import router from './router'
+import store from './store'
+import pluginA from 'pluginA'
+import pluginB from 'pluginB'
+import pluginC from 'pluginC'
+
+// 使用了 `use` 方法激活
+Vue.use(pluginA)
+Vue.use(pluginB)
+Vue.use(pluginC)
+
+Vue.config.productionTip = false
+
+// 作为 `new Vue()` 的入参激活
+new Vue({
+  router,
+  store,
+  render: (h) => h(App),
+}).$mount('#app')

了解 Vue 3 ~new

在 Vue 3 ,使用 createApp 执行 Vue 的初始化,另外不管是 Vue 生态里的东西,还是外部插件、 UI 框架,统一都是由 use() 进行激活,非常统一和简洁:

ts
import { createApp } from 'vue'
+import App from './App.vue'
+import router from './router'
+import store from './store'
+import pluginA from 'pluginA'
+import pluginB from 'pluginB'
+import pluginC from 'pluginC'
+
+createApp(App)
+  .use(store)
+  .use(router)
+  .use(pluginA)
+  .use(pluginB)
+  .use(pluginC)
+  .mount('#app')

Vue Devtools

Vue Devtools 是一个浏览器扩展,支持 Chrome 、 Firefox 等浏览器,需要先安装才能使用。

点击安装:Vue Devtools 的浏览器扩展

当在 Vue 项目通过 npm run dev 等命令启动开发环境服务后,访问本地页面(如: http://localhost:3000/ ),在页面上按 F12 唤起浏览器的控制台,会发现多了一个名为 vue 的面板。

面板的顶部有一个菜单可以切换不同的选项卡,菜单数量会根据不同项目有所不同,例如没有安装 Pinia 则不会出现 Pinia 选项卡,这里以其中一部分选项卡作为举例。

Components 是以结构化的方式显示组件的调试信息,可以查看组件的父子关系,并检查组件的各种内部状态:

Routes 可以查看当前所在路由的配置信息:

Timeline 是以时间线的方式追踪不同类型的数据,例如事件:

Pinia 是可以查看当前组件引入的全局状态情况:

可以在 Vue Devtools 官网 了解更多的用法。

Released under the MIT License.

+ + + + \ No newline at end of file