diff --git a/dist/archives/2023/12/index.html b/dist/archives/2023/12/index.html new file mode 100644 index 0000000..210ad21 --- /dev/null +++ b/dist/archives/2023/12/index.html @@ -0,0 +1,240 @@ + + + + + + + Archiv: 2023/12 | TaoLiuJun's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+ 2023 +
+
+ + + +
+ + + +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/dist/archives/2023/index.html b/dist/archives/2023/index.html new file mode 100644 index 0000000..f8490f4 --- /dev/null +++ b/dist/archives/2023/index.html @@ -0,0 +1,240 @@ + + + + + + + Archiv: 2023 | TaoLiuJun's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+ 2023 +
+
+ + + +
+ + + +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/dist/archives/index.html b/dist/archives/index.html new file mode 100644 index 0000000..14c9412 --- /dev/null +++ b/dist/archives/index.html @@ -0,0 +1,240 @@ + + + + + + + Archiv | TaoLiuJun's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+ 2023 +
+
+ + + +
+ + + +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/dist/categories/React/index.html b/dist/categories/React/index.html new file mode 100644 index 0000000..1345a61 --- /dev/null +++ b/dist/categories/React/index.html @@ -0,0 +1,221 @@ + + + + + + + Kategorie: React | TaoLiuJun's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+ 2023 +
+
+ + + + +
+ + + +
+ + + +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git "a/dist/categories/\345\267\245\347\250\213\345\214\226/index.html" "b/dist/categories/\345\267\245\347\250\213\345\214\226/index.html" new file mode 100644 index 0000000..5dee3b2 --- /dev/null +++ "b/dist/categories/\345\267\245\347\250\213\345\214\226/index.html" @@ -0,0 +1,221 @@ + + + + + + + Kategorie: 工程化 | TaoLiuJun's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+ 2023 +
+
+ + + +
+ + + +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/dist/css/images/banner.jpg b/dist/css/images/banner.jpg new file mode 100644 index 0000000..b963e06 Binary files /dev/null and b/dist/css/images/banner.jpg differ diff --git a/dist/css/style.css b/dist/css/style.css new file mode 100644 index 0000000..2fead12 --- /dev/null +++ b/dist/css/style.css @@ -0,0 +1,1346 @@ +body { + width: 100%; +} +body:before, +body:after { + content: ""; + display: table; +} +body:after { + clear: both; +} +html, +body, +div, +span, +applet, +object, +iframe, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +a, +abbr, +acronym, +address, +big, +cite, +code, +del, +dfn, +em, +img, +ins, +kbd, +q, +s, +samp, +small, +strike, +strong, +sub, +sup, +tt, +var, +dl, +dt, +dd, +ol, +ul, +li, +fieldset, +form, +label, +legend, +table, +caption, +tbody, +tfoot, +thead, +tr, +th, +td { + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-weight: inherit; + font-style: inherit; + font-family: inherit; + font-size: 100%; + vertical-align: baseline; +} +body { + line-height: 1; + color: #000; + background: #fff; +} +ol, +ul { + list-style: none; +} +table { + border-collapse: separate; + border-spacing: 0; + vertical-align: middle; +} +caption, +th, +td { + text-align: left; + font-weight: normal; + vertical-align: middle; +} +a img { + border: none; +} +input, +button { + margin: 0; + padding: 0; +} +input::-moz-focus-inner, +button::-moz-focus-inner { + border: 0; + padding: 0; +} +html, +body, +#container { + height: 100%; +} +body { + background: #eee; + font: 14px -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + -webkit-text-size-adjust: 100%; +} +.outer { + max-width: 1220px; + margin: 0 auto; + padding: 0 20px; +} +.outer:before, +.outer:after { + content: ""; + display: table; +} +.outer:after { + clear: both; +} +.inner { + display: inline; + float: left; + width: 98.33333333333333%; + margin: 0 0.833333333333333%; +} +.left, +.alignleft { + float: left; +} +.right, +.alignright { + float: right; +} +.clear { + clear: both; +} +#container { + position: relative; +} +.mobile-nav-on { + overflow: hidden; +} +#wrap { + height: 100%; + width: 100%; + position: absolute; + top: 0; + left: 0; + -webkit-transition: 0.2s ease-out; + -moz-transition: 0.2s ease-out; + -ms-transition: 0.2s ease-out; + transition: 0.2s ease-out; + z-index: 1; + background: #eee; +} +.mobile-nav-on #wrap { + left: 280px; +} +@media screen and (min-width: 768px) { + #main { + display: inline; + float: left; + width: 73.33333333333333%; + margin: 0 0.833333333333333%; + } +} +.article-date, +.article-category-link, +.archive-year, +.widget-title { + text-decoration: none; + text-transform: uppercase; + letter-spacing: 2px; + color: #999; + margin-bottom: 1em; + margin-left: 5px; + line-height: 1em; + text-shadow: 0 1px #fff; + font-weight: bold; +} +.article-inner, +.archive-article-inner { + background: #fff; + -webkit-box-shadow: 1px 2px 3px #ddd; + box-shadow: 1px 2px 3px #ddd; + border: 1px solid #ddd; + border-radius: 3px; +} +.article-entry h1, +.widget h1 { + font-size: 2em; +} +.article-entry h2, +.widget h2 { + font-size: 1.5em; +} +.article-entry h3, +.widget h3 { + font-size: 1.3em; +} +.article-entry h4, +.widget h4 { + font-size: 1.2em; +} +.article-entry h5, +.widget h5 { + font-size: 1em; +} +.article-entry h6, +.widget h6 { + font-size: 1em; + color: #999; +} +.article-entry hr, +.widget hr { + border: 1px dashed #ddd; +} +.article-entry strong, +.widget strong { + font-weight: bold; +} +.article-entry em, +.widget em, +.article-entry cite, +.widget cite { + font-style: italic; +} +.article-entry sup, +.widget sup, +.article-entry sub, +.widget sub { + font-size: 0.75em; + line-height: 0; + position: relative; + vertical-align: baseline; +} +.article-entry sup, +.widget sup { + top: -0.5em; +} +.article-entry sub, +.widget sub { + bottom: -0.2em; +} +.article-entry small, +.widget small { + font-size: 0.85em; +} +.article-entry acronym, +.widget acronym, +.article-entry abbr, +.widget abbr { + border-bottom: 1px dotted; +} +.article-entry ul, +.widget ul, +.article-entry ol, +.widget ol, +.article-entry dl, +.widget dl { + margin: 0 20px; + line-height: 1.6em; +} +.article-entry ul ul, +.widget ul ul, +.article-entry ol ul, +.widget ol ul, +.article-entry ul ol, +.widget ul ol, +.article-entry ol ol, +.widget ol ol { + margin-top: 0; + margin-bottom: 0; +} +.article-entry ul, +.widget ul { + list-style: disc; +} +.article-entry ol, +.widget ol { + list-style: decimal; +} +.article-entry dt, +.widget dt { + font-weight: bold; +} +#header { + height: 300px; + position: relative; + border-bottom: 1px solid #ddd; +} +#header:before, +#header:after { + content: ""; + position: absolute; + left: 0; + right: 0; + height: 40px; +} +#header:before { + top: 0; + background: -webkit-linear-gradient(rgba(0,0,0,0.2), transparent); + background: -moz-linear-gradient(rgba(0,0,0,0.2), transparent); + background: -ms-linear-gradient(rgba(0,0,0,0.2), transparent); + background: linear-gradient(rgba(0,0,0,0.2), transparent); +} +#header:after { + bottom: 0; + background: -webkit-linear-gradient(transparent, rgba(0,0,0,0.2)); + background: -moz-linear-gradient(transparent, rgba(0,0,0,0.2)); + background: -ms-linear-gradient(transparent, rgba(0,0,0,0.2)); + background: linear-gradient(transparent, rgba(0,0,0,0.2)); +} +#header-outer { + height: 100%; + position: relative; +} +#header-inner { + position: relative; + overflow: hidden; +} +#banner { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: url("images/banner.jpg") center #000; + background-size: cover; + z-index: -1; +} +#header-title { + text-align: center; + height: 40px; + position: absolute; + top: 50%; + left: 0; + margin-top: -20px; +} +#logo, +#subtitle { + text-decoration: none; + color: #fff; + font-weight: 300; + text-shadow: 0 1px 4px rgba(0,0,0,0.3); +} +#logo { + font-size: 40px; + line-height: 40px; + letter-spacing: 2px; +} +#subtitle { + font-size: 16px; + line-height: 16px; + letter-spacing: 1px; +} +#subtitle-wrap { + margin-top: 16px; +} +#main-nav { + float: left; + margin-left: -15px; +} +.nav-icon, +.main-nav-link { + float: left; + color: #fff; + opacity: 0.6; + text-decoration: none; + text-shadow: 0 1px rgba(0,0,0,0.2); + -webkit-transition: opacity 0.2s; + -moz-transition: opacity 0.2s; + -ms-transition: opacity 0.2s; + transition: opacity 0.2s; + display: block; + padding: 20px 15px; +} +.nav-icon:hover, +.main-nav-link:hover { + opacity: 1; +} +.nav-icon { + text-align: center; + font-size: 14px; + width: 14px; + height: 14px; + padding: 20px 15px; + position: relative; + cursor: pointer; +} +.main-nav-link { + font-weight: 300; + letter-spacing: 1px; +} +@media screen and (max-width: 479px) { + .main-nav-link { + display: none; + } +} +#main-nav-toggle { + display: none; +} +@media screen and (max-width: 479px) { + #main-nav-toggle { + display: block; + } +} +#sub-nav { + float: right; + margin-right: -15px; +} +#search-form-wrap { + position: absolute; + top: 15px; + width: 150px; + height: 30px; + right: -150px; + opacity: 0; + -webkit-transition: 0.2s ease-out; + -moz-transition: 0.2s ease-out; + -ms-transition: 0.2s ease-out; + transition: 0.2s ease-out; +} +#search-form-wrap.on { + opacity: 1; + right: 0; +} +@media screen and (max-width: 479px) { + #search-form-wrap { + width: 100%; + right: -100%; + } +} +.search-form { + position: absolute; + top: 0; + left: 0; + right: 0; + background: #fff; + padding: 5px 15px; + border-radius: 15px; + -webkit-box-shadow: 0 0 10px rgba(0,0,0,0.3); + box-shadow: 0 0 10px rgba(0,0,0,0.3); +} +.search-form-input { + border: none; + background: none; + color: #555; + width: 100%; + font: 13px -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + outline: none; +} +.search-form-input::-webkit-search-results-decoration, +.search-form-input::-webkit-search-cancel-button { + -webkit-appearance: none; +} +.search-form-submit { + position: absolute; + top: 50%; + right: 10px; + margin-top: -7px; + font: 13px ForkAwesome; + border: none; + background: none; + color: #bbb; + cursor: pointer; +} +.search-form-submit:hover, +.search-form-submit:focus { + color: #777; +} +.article { + margin: 50px 0; +} +.article-inner { + overflow: hidden; +} +.article-meta:before, +.article-meta:after { + content: ""; + display: table; +} +.article-meta:after { + clear: both; +} +.article-date { + float: left; +} +.article-category { + float: left; + line-height: 1em; + color: #ccc; + text-shadow: 0 1px #fff; + margin-left: 8px; +} +.article-category:before { + content: "\2022"; +} +.article-category-link { + margin: 0 12px 1em; +} +.article-header { + padding: 20px 20px 0; +} +.article-title { + text-decoration: none; + font-size: 2em; + font-weight: bold; + color: #555; + line-height: 1.1em; + -webkit-transition: color 0.2s; + -moz-transition: color 0.2s; + -ms-transition: color 0.2s; + transition: color 0.2s; +} +a.article-title:hover { + color: #258fb8; +} +.article-entry { + color: #555; + padding: 0 20px; +} +.article-entry:before, +.article-entry:after { + content: ""; + display: table; +} +.article-entry:after { + clear: both; +} +.article-entry p, +.article-entry table { + line-height: 1.6em; + margin: 1.6em 0; +} +.article-entry h1, +.article-entry h2, +.article-entry h3, +.article-entry h4, +.article-entry h5, +.article-entry h6 { + font-weight: bold; +} +.article-entry h1, +.article-entry h2, +.article-entry h3, +.article-entry h4, +.article-entry h5, +.article-entry h6 { + line-height: 1.1em; + margin: 1.1em 0; +} +.article-entry a { + color: #258fb8; + text-decoration: none; +} +.article-entry a:hover { + text-decoration: underline; +} +.article-entry ul, +.article-entry ol, +.article-entry dl { + margin-top: 1.6em; + margin-bottom: 1.6em; +} +.article-entry img, +.article-entry video { + max-width: 100%; + height: auto; + display: block; + margin: auto; +} +.article-entry iframe { + border: none; +} +.article-entry table { + width: 100%; + border-collapse: collapse; + border-spacing: 0; +} +.article-entry th { + font-weight: bold; + border-bottom: 3px solid #ddd; + padding-bottom: 0.5em; +} +.article-entry td { + border-bottom: 1px solid #ddd; + padding: 10px 0; +} +.article-entry blockquote { + font-family: Georgia, "Times New Roman", serif; + margin: 1.6em 20px; + text-align: center; +} +.article-entry blockquote footer { + font-size: 14px; + margin: 1.6em 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; +} +.article-entry blockquote footer cite:before { + content: "—"; + padding: 0 0.5em; +} +.article-entry .pullquote { + text-align: left; + width: 45%; + margin: 0; +} +.article-entry .pullquote.left { + margin-left: 0.5em; + margin-right: 1em; +} +.article-entry .pullquote.right { + margin-right: 0.5em; + margin-left: 1em; +} +.article-entry .caption { + color: #999; + display: block; + font-size: 0.9em; + margin-top: 0.5em; + position: relative; + text-align: center; +} +.article-entry .video-container { + position: relative; + padding-top: 56.25%; + height: 0; + overflow: hidden; +} +.article-entry .video-container iframe, +.article-entry .video-container object, +.article-entry .video-container embed { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + margin-top: 0; +} +.article-more-link a { + display: inline-block; + line-height: 1em; + padding: 6px 15px; + border-radius: 15px; + background: #eee; + color: #999; + text-shadow: 0 1px #fff; + text-decoration: none; +} +.article-more-link a:hover { + background: #258fb8; + color: #fff; + text-decoration: none; + text-shadow: 0 1px #1e7293; +} +.article-footer { + font-size: 0.85em; + line-height: 1.6em; + border-top: 1px solid #ddd; + padding-top: 1.6em; + margin: 0 20px 20px; +} +.article-footer:before, +.article-footer:after { + content: ""; + display: table; +} +.article-footer:after { + clear: both; +} +.article-footer a { + color: #999; + text-decoration: none; +} +.article-footer a:hover { + color: #555; +} +.article-tag-list-item { + float: left; + margin-right: 10px; +} +.article-tag-list-link:before { + content: "#"; +} +.article-comment-link { + float: right; +} +.article-comment-link:before { + padding-right: 8px; +} +.article-share-link { + cursor: pointer; + float: right; + margin-left: 20px; +} +.article-share-link:before { + padding-right: 6px; +} +#article-nav { + position: relative; +} +#article-nav:before, +#article-nav:after { + content: ""; + display: table; +} +#article-nav:after { + clear: both; +} +@media screen and (min-width: 768px) { + #article-nav { + margin: 50px 0; + } + #article-nav:before { + width: 8px; + height: 8px; + position: absolute; + top: 50%; + left: 50%; + margin-top: -4px; + margin-left: -4px; + content: ""; + border-radius: 50%; + background: #ddd; + -webkit-box-shadow: 0 1px 2px #fff; + box-shadow: 0 1px 2px #fff; + } +} +.article-nav-link-wrap { + text-decoration: none; + text-shadow: 0 1px #fff; + color: #999; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + margin-top: 50px; + text-align: center; + display: block; +} +.article-nav-link-wrap:hover { + color: #555; +} +@media screen and (min-width: 768px) { + .article-nav-link-wrap { + width: 50%; + margin-top: 0; + } +} +@media screen and (min-width: 768px) { + #article-nav-newer { + float: left; + text-align: right; + padding-right: 20px; + } +} +@media screen and (min-width: 768px) { + #article-nav-older { + float: right; + text-align: left; + padding-left: 20px; + } +} +.article-nav-caption { + text-transform: uppercase; + letter-spacing: 2px; + color: #ddd; + line-height: 1em; + font-weight: bold; +} +#article-nav-newer .article-nav-caption { + margin-right: -2px; +} +.article-nav-title { + font-size: 0.85em; + line-height: 1.6em; + margin-top: 0.5em; +} +.article-share-box { + position: absolute; + display: none; + background: #fff; + -webkit-box-shadow: 1px 2px 10px rgba(0,0,0,0.2); + box-shadow: 1px 2px 10px rgba(0,0,0,0.2); + border-radius: 3px; + margin-left: -145px; + overflow: hidden; + z-index: 1; +} +.article-share-box.on { + display: block; +} +.article-share-input { + width: 100%; + background: none; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + font: 14px -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + padding: 0 15px; + color: #555; + outline: none; + border: 1px solid #ddd; + border-radius: 3px 3px 0 0; + height: 36px; + line-height: 36px; +} +.article-share-links { + background: #eee; +} +.article-share-links:before, +.article-share-links:after { + content: ""; + display: table; +} +.article-share-links:after { + clear: both; +} +.article-share-twitter, +.article-share-facebook, +.article-share-pinterest, +.article-share-linkedin { + width: 50px; + height: 36px; + display: block; + float: left; + position: relative; + color: #999; + text-shadow: 0 1px #fff; +} +.article-share-twitter:before, +.article-share-facebook:before, +.article-share-pinterest:before, +.article-share-linkedin:before { + font-size: 20px; + width: 20px; + height: 20px; + position: absolute; + top: 50%; + left: 50%; + margin-top: -10px; + margin-left: -10px; + text-align: center; +} +.article-share-twitter:hover, +.article-share-facebook:hover, +.article-share-pinterest:hover, +.article-share-linkedin:hover { + color: #fff; +} +.article-share-twitter:hover { + background: #00aced; + text-shadow: 0 1px #008abe; +} +.article-share-facebook:hover { + background: #3b5998; + text-shadow: 0 1px #2f477a; +} +.article-share-pinterest:hover { + background: #cb2027; + text-shadow: 0 1px #a21a1f; +} +.article-share-linkedin:hover { + background: #0077b5; + text-shadow: 0 1px #005f91; +} +.article-gallery { + background: #000; + position: relative; +} +.article-gallery-photos { + position: relative; + overflow: hidden; +} +.article-gallery-img { + display: none; + max-width: 100%; +} +.article-gallery-img:first-child { + display: block; +} +.article-gallery-img.loaded { + position: absolute; + display: block; +} +.article-gallery-img img { + display: block; + max-width: 100%; + margin: 0 auto; +} +#comments { + background: #fff; + -webkit-box-shadow: 1px 2px 3px #ddd; + box-shadow: 1px 2px 3px #ddd; + padding: 20px; + border: 1px solid #ddd; + border-radius: 3px; + margin: 50px 0; +} +#comments a { + color: #258fb8; +} +.archives-wrap { + margin: 50px 0; +} +.archives:before, +.archives:after { + content: ""; + display: table; +} +.archives:after { + clear: both; +} +.archive-year-wrap { + margin-bottom: 1em; +} +.archives { + -webkit-column-gap: 10px; + -moz-column-gap: 10px; + column-gap: 10px; +} +@media screen and (min-width: 480px) and (max-width: 767px) { + .archives { + -webkit-column-count: 2; + -moz-column-count: 2; + column-count: 2; + } +} +@media screen and (min-width: 768px) { + .archives { + -webkit-column-count: 3; + -moz-column-count: 3; + column-count: 3; + } +} +.archive-article { + -webkit-column-break-inside: avoid; + page-break-inside: avoid; + overflow: hidden; + break-inside: avoid-column; +} +.archive-article-inner { + padding: 10px; + margin-bottom: 15px; +} +.archive-article-title { + text-decoration: none; + font-weight: bold; + color: #555; + -webkit-transition: color 0.2s; + -moz-transition: color 0.2s; + -ms-transition: color 0.2s; + transition: color 0.2s; + line-height: 1.6em; +} +.archive-article-title:hover { + color: #258fb8; +} +.archive-article-footer { + margin-top: 1em; +} +.archive-article-date { + color: #999; + text-decoration: none; + font-size: 0.85em; + line-height: 1em; + margin-bottom: 0.5em; + display: block; +} +#page-nav { + margin: 50px auto; + background: #fff; + -webkit-box-shadow: 1px 2px 3px #ddd; + box-shadow: 1px 2px 3px #ddd; + border: 1px solid #ddd; + border-radius: 3px; + text-align: center; + color: #999; + overflow: hidden; +} +#page-nav:before, +#page-nav:after { + content: ""; + display: table; +} +#page-nav:after { + clear: both; +} +#page-nav a, +#page-nav span { + padding: 10px 20px; + line-height: 1; + height: 2ex; +} +#page-nav a { + color: #999; + text-decoration: none; +} +#page-nav a:hover { + background: #999; + color: #fff; +} +#page-nav .prev { + float: left; +} +#page-nav .next { + float: right; +} +#page-nav .page-number { + display: inline-block; +} +@media screen and (max-width: 479px) { + #page-nav .page-number { + display: none; + } +} +#page-nav .current { + color: #555; + font-weight: bold; +} +#page-nav .space { + color: #ddd; +} +#footer { + background: #262a30; + padding: 50px 0; + border-top: 1px solid #ddd; + color: #999; +} +#footer a { + color: #258fb8; + text-decoration: none; +} +#footer a:hover { + text-decoration: underline; +} +#footer-info { + line-height: 1.6em; + font-size: 0.85em; +} +.article-entry pre, +.article-entry .highlight { + background: #2d2d2d; + margin: 0 -20px; + padding: 15px 20px; + border-style: solid; + border-color: #ddd; + border-width: 1px 0; + overflow: auto; + color: #ccc; + line-height: 22.400000000000002px; +} +.article-entry .highlight .gutter pre, +.article-entry .gist .gist-file .gist-data .line-numbers { + color: #666; + font-size: 0.85em; +} +.article-entry pre, +.article-entry code { + font-family: "Source Code Pro", Consolas, Monaco, Menlo, Consolas, monospace; +} +.article-entry code { + background: #eee; + text-shadow: 0 1px #fff; + padding: 0 0.3em; +} +.article-entry pre code { + background: none; + text-shadow: none; + padding: 0; +} +.article-entry .highlight pre { + border: none; + margin: 0; + padding: 0; +} +.article-entry .highlight table { + margin: 0; + width: auto; +} +.article-entry .highlight td { + border: none; + padding: 0; +} +.article-entry .highlight figcaption { + font-size: 0.85em; + color: #999; + line-height: 1em; + margin-bottom: 1em; +} +.article-entry .highlight figcaption:before, +.article-entry .highlight figcaption:after { + content: ""; + display: table; +} +.article-entry .highlight figcaption:after { + clear: both; +} +.article-entry .highlight figcaption a { + float: right; +} +.article-entry .highlight .gutter { + -moz-user-select: none; + -ms-user-select: none; + -webkit-user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.article-entry .highlight .gutter pre { + text-align: right; + padding-right: 20px; +} +.article-entry .highlight .line { + height: 22.400000000000002px; +} +.article-entry .highlight .line.marked { + background: #515151; +} +.article-entry .gist { + margin: 0 -20px; + border-style: solid; + border-color: #ddd; + border-width: 1px 0; + background: #2d2d2d; + padding: 15px 20px 15px 0; +} +.article-entry .gist .gist-file { + border: none; + font-family: "Source Code Pro", Consolas, Monaco, Menlo, Consolas, monospace; + margin: 0; +} +.article-entry .gist .gist-file .gist-data { + background: none; + border: none; +} +.article-entry .gist .gist-file .gist-data .line-numbers { + background: none; + border: none; + padding: 0 20px 0 0; +} +.article-entry .gist .gist-file .gist-data .line-data { + padding: 0 !important; +} +.article-entry .gist .gist-file .highlight { + margin: 0; + padding: 0; + border: none; +} +.article-entry .gist .gist-file .gist-meta { + background: #2d2d2d; + color: #999; + font: 0.85em -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + text-shadow: 0 0; + padding: 0; + margin-top: 1em; + margin-left: 20px; +} +.article-entry .gist .gist-file .gist-meta a { + color: #258fb8; + font-weight: normal; +} +.article-entry .gist .gist-file .gist-meta a:hover { + text-decoration: underline; +} +pre .comment, +pre .title { + color: #999; +} +pre .variable, +pre .attribute, +pre .tag, +pre .regexp, +pre .ruby .constant, +pre .xml .tag .title, +pre .xml .pi, +pre .xml .doctype, +pre .html .doctype, +pre .css .id, +pre .css .class, +pre .css .pseudo { + color: #f2777a; +} +pre .number, +pre .preprocessor, +pre .built_in, +pre .literal, +pre .params, +pre .constant { + color: #f99157; +} +pre .class, +pre .ruby .class .title, +pre .css .rules .attribute { + color: #9c9; +} +pre .string, +pre .value, +pre .inheritance, +pre .header, +pre .ruby .symbol, +pre .xml .cdata { + color: #9c9; +} +pre .css .hexcolor { + color: #6cc; +} +pre .function, +pre .python .decorator, +pre .python .title, +pre .ruby .function .title, +pre .ruby .title .keyword, +pre .perl .sub, +pre .javascript .title, +pre .coffeescript .title { + color: #69c; +} +pre .keyword, +pre .javascript .function { + color: #c9c; +} +@media screen and (max-width: 479px) { + #mobile-nav { + position: absolute; + top: 0; + left: 0; + width: 280px; + height: 100%; + background: #191919; + border-right: 1px solid #fff; + } +} +@media screen and (max-width: 479px) { + .mobile-nav-link { + display: block; + color: #999; + text-decoration: none; + padding: 15px 20px; + font-weight: bold; + } + .mobile-nav-link:hover { + color: #fff; + } +} +@media screen and (min-width: 768px) { + #sidebar { + display: inline; + float: left; + width: 23.333333333333332%; + margin: 0 0.833333333333333%; + } +} +.widget-wrap { + margin: 50px 0; +} +.widget { + color: #777; + text-shadow: 0 1px #fff; + background: #ddd; + -webkit-box-shadow: 0 -1px 4px #ccc inset; + box-shadow: 0 -1px 4px #ccc inset; + border: 1px solid #ccc; + padding: 15px; + border-radius: 3px; +} +.widget a { + color: #258fb8; + text-decoration: none; +} +.widget a:hover { + text-decoration: underline; +} +.widget ul ul, +.widget ol ul, +.widget dl ul, +.widget ul ol, +.widget ol ol, +.widget dl ol, +.widget ul dl, +.widget ol dl, +.widget dl dl { + margin-left: 15px; + list-style: disc; +} +.widget { + line-height: 1.6em; + word-wrap: break-word; + font-size: 0.9em; +} +.widget ul, +.widget ol { + list-style: none; + margin: 0; +} +.widget ul ul, +.widget ol ul, +.widget ul ol, +.widget ol ol { + margin: 0 20px; +} +.widget ul ul, +.widget ol ul { + list-style: disc; +} +.widget ul ol, +.widget ol ol { + list-style: decimal; +} +.category-list-count, +.tag-list-count, +.archive-list-count { + padding-left: 5px; + color: #999; + font-size: 0.85em; +} +.category-list-count:before, +.tag-list-count:before, +.archive-list-count:before { + content: "("; +} +.category-list-count:after, +.tag-list-count:after, +.archive-list-count:after { + content: ")"; +} +.tagcloud a { + margin-right: 5px; + display: inline-block; +} diff --git a/dist/fancybox/jquery.fancybox.min.css b/dist/fancybox/jquery.fancybox.min.css new file mode 100644 index 0000000..7cc60b2 --- /dev/null +++ b/dist/fancybox/jquery.fancybox.min.css @@ -0,0 +1 @@ +body.compensate-for-scrollbar{overflow:hidden}.fancybox-active{height:auto}.fancybox-is-hidden{left:-9999px;margin:0;position:absolute!important;top:-9999px;visibility:hidden}.fancybox-container{-webkit-backface-visibility:hidden;height:100%;left:0;outline:none;position:fixed;-webkit-tap-highlight-color:transparent;top:0;-ms-touch-action:manipulation;touch-action:manipulation;transform:translateZ(0);width:100%;z-index:99992}.fancybox-container *{box-sizing:border-box}.fancybox-bg,.fancybox-inner,.fancybox-outer,.fancybox-stage{bottom:0;left:0;position:absolute;right:0;top:0}.fancybox-outer{-webkit-overflow-scrolling:touch;overflow-y:auto}.fancybox-bg{background:#1e1e1e;opacity:0;transition-duration:inherit;transition-property:opacity;transition-timing-function:cubic-bezier(.47,0,.74,.71)}.fancybox-is-open .fancybox-bg{opacity:.9;transition-timing-function:cubic-bezier(.22,.61,.36,1)}.fancybox-caption,.fancybox-infobar,.fancybox-navigation .fancybox-button,.fancybox-toolbar{direction:ltr;opacity:0;position:absolute;transition:opacity .25s ease,visibility 0s ease .25s;visibility:hidden;z-index:99997}.fancybox-show-caption .fancybox-caption,.fancybox-show-infobar .fancybox-infobar,.fancybox-show-nav .fancybox-navigation .fancybox-button,.fancybox-show-toolbar .fancybox-toolbar{opacity:1;transition:opacity .25s ease 0s,visibility 0s ease 0s;visibility:visible}.fancybox-infobar{color:#ccc;font-size:13px;-webkit-font-smoothing:subpixel-antialiased;height:44px;left:0;line-height:44px;min-width:44px;mix-blend-mode:difference;padding:0 10px;pointer-events:none;top:0;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.fancybox-toolbar{right:0;top:0}.fancybox-stage{direction:ltr;overflow:visible;transform:translateZ(0);z-index:99994}.fancybox-is-open .fancybox-stage{overflow:hidden}.fancybox-slide{-webkit-backface-visibility:hidden;display:none;height:100%;left:0;outline:none;overflow:auto;-webkit-overflow-scrolling:touch;padding:44px;position:absolute;text-align:center;top:0;transition-property:transform,opacity;white-space:normal;width:100%;z-index:99994}.fancybox-slide:before{content:"";display:inline-block;font-size:0;height:100%;vertical-align:middle;width:0}.fancybox-is-sliding .fancybox-slide,.fancybox-slide--current,.fancybox-slide--next,.fancybox-slide--previous{display:block}.fancybox-slide--image{overflow:hidden;padding:44px 0}.fancybox-slide--image:before{display:none}.fancybox-slide--html{padding:6px}.fancybox-content{background:#fff;display:inline-block;margin:0;max-width:100%;overflow:auto;-webkit-overflow-scrolling:touch;padding:44px;position:relative;text-align:left;vertical-align:middle}.fancybox-slide--image .fancybox-content{animation-timing-function:cubic-bezier(.5,0,.14,1);-webkit-backface-visibility:hidden;background:transparent;background-repeat:no-repeat;background-size:100% 100%;left:0;max-width:none;overflow:visible;padding:0;position:absolute;top:0;transform-origin:top left;transition-property:transform,opacity;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;z-index:99995}.fancybox-can-zoomOut .fancybox-content{cursor:zoom-out}.fancybox-can-zoomIn .fancybox-content{cursor:zoom-in}.fancybox-can-pan .fancybox-content,.fancybox-can-swipe .fancybox-content{cursor:grab}.fancybox-is-grabbing .fancybox-content{cursor:grabbing}.fancybox-container [data-selectable=true]{cursor:text}.fancybox-image,.fancybox-spaceball{background:transparent;border:0;height:100%;left:0;margin:0;max-height:none;max-width:none;padding:0;position:absolute;top:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;width:100%}.fancybox-spaceball{z-index:1}.fancybox-slide--iframe .fancybox-content,.fancybox-slide--map .fancybox-content,.fancybox-slide--pdf .fancybox-content,.fancybox-slide--video .fancybox-content{height:100%;overflow:visible;padding:0;width:100%}.fancybox-slide--video .fancybox-content{background:#000}.fancybox-slide--map .fancybox-content{background:#e5e3df}.fancybox-slide--iframe .fancybox-content{background:#fff}.fancybox-iframe,.fancybox-video{background:transparent;border:0;display:block;height:100%;margin:0;overflow:hidden;padding:0;width:100%}.fancybox-iframe{left:0;position:absolute;top:0}.fancybox-error{background:#fff;cursor:default;max-width:400px;padding:40px;width:100%}.fancybox-error p{color:#444;font-size:16px;line-height:20px;margin:0;padding:0}.fancybox-button{background:rgba(30,30,30,.6);border:0;border-radius:0;box-shadow:none;cursor:pointer;display:inline-block;height:44px;margin:0;padding:10px;position:relative;transition:color .2s;vertical-align:top;visibility:inherit;width:44px}.fancybox-button,.fancybox-button:link,.fancybox-button:visited{color:#ccc}.fancybox-button:hover{color:#fff}.fancybox-button:focus{outline:none}.fancybox-button.fancybox-focus{outline:1px dotted}.fancybox-button[disabled],.fancybox-button[disabled]:hover{color:#888;cursor:default;outline:none}.fancybox-button div{height:100%}.fancybox-button svg{display:block;height:100%;overflow:visible;position:relative;width:100%}.fancybox-button svg path{fill:currentColor;stroke-width:0}.fancybox-button--fsenter svg:nth-child(2),.fancybox-button--fsexit svg:first-child,.fancybox-button--pause svg:first-child,.fancybox-button--play svg:nth-child(2){display:none}.fancybox-progress{background:#ff5268;height:2px;left:0;position:absolute;right:0;top:0;transform:scaleX(0);transform-origin:0;transition-property:transform;transition-timing-function:linear;z-index:99998}.fancybox-close-small{background:transparent;border:0;border-radius:0;color:#ccc;cursor:pointer;opacity:.8;padding:8px;position:absolute;right:-12px;top:-44px;z-index:401}.fancybox-close-small:hover{color:#fff;opacity:1}.fancybox-slide--html .fancybox-close-small{color:currentColor;padding:10px;right:0;top:0}.fancybox-slide--image.fancybox-is-scaling .fancybox-content{overflow:hidden}.fancybox-is-scaling .fancybox-close-small,.fancybox-is-zoomable.fancybox-can-pan .fancybox-close-small{display:none}.fancybox-navigation .fancybox-button{background-clip:content-box;height:100px;opacity:0;position:absolute;top:calc(50% - 50px);width:70px}.fancybox-navigation .fancybox-button div{padding:7px}.fancybox-navigation .fancybox-button--arrow_left{left:0;left:env(safe-area-inset-left);padding:31px 26px 31px 6px}.fancybox-navigation .fancybox-button--arrow_right{padding:31px 6px 31px 26px;right:0;right:env(safe-area-inset-right)}.fancybox-caption{background:linear-gradient(0deg,rgba(0,0,0,.85) 0,rgba(0,0,0,.3) 50%,rgba(0,0,0,.15) 65%,rgba(0,0,0,.075) 75.5%,rgba(0,0,0,.037) 82.85%,rgba(0,0,0,.019) 88%,transparent);bottom:0;color:#eee;font-size:14px;font-weight:400;left:0;line-height:1.5;padding:75px 44px 25px;pointer-events:none;right:0;text-align:center;z-index:99996}@supports (padding:max(0px)){.fancybox-caption{padding:75px max(44px,env(safe-area-inset-right)) max(25px,env(safe-area-inset-bottom)) max(44px,env(safe-area-inset-left))}}.fancybox-caption--separate{margin-top:-50px}.fancybox-caption__body{max-height:50vh;overflow:auto;pointer-events:all}.fancybox-caption a,.fancybox-caption a:link,.fancybox-caption a:visited{color:#ccc;text-decoration:none}.fancybox-caption a:hover{color:#fff;text-decoration:underline}.fancybox-loading{animation:a 1s linear infinite;background:transparent;border:4px solid #888;border-bottom-color:#fff;border-radius:50%;height:50px;left:50%;margin:-25px 0 0 -25px;opacity:.7;padding:0;position:absolute;top:50%;width:50px;z-index:99999}@keyframes a{to{transform:rotate(1turn)}}.fancybox-animated{transition-timing-function:cubic-bezier(0,0,.25,1)}.fancybox-fx-slide.fancybox-slide--previous{opacity:0;transform:translate3d(-100%,0,0)}.fancybox-fx-slide.fancybox-slide--next{opacity:0;transform:translate3d(100%,0,0)}.fancybox-fx-slide.fancybox-slide--current{opacity:1;transform:translateZ(0)}.fancybox-fx-fade.fancybox-slide--next,.fancybox-fx-fade.fancybox-slide--previous{opacity:0;transition-timing-function:cubic-bezier(.19,1,.22,1)}.fancybox-fx-fade.fancybox-slide--current{opacity:1}.fancybox-fx-zoom-in-out.fancybox-slide--previous{opacity:0;transform:scale3d(1.5,1.5,1.5)}.fancybox-fx-zoom-in-out.fancybox-slide--next{opacity:0;transform:scale3d(.5,.5,.5)}.fancybox-fx-zoom-in-out.fancybox-slide--current{opacity:1;transform:scaleX(1)}.fancybox-fx-rotate.fancybox-slide--previous{opacity:0;transform:rotate(-1turn)}.fancybox-fx-rotate.fancybox-slide--next{opacity:0;transform:rotate(1turn)}.fancybox-fx-rotate.fancybox-slide--current{opacity:1;transform:rotate(0deg)}.fancybox-fx-circular.fancybox-slide--previous{opacity:0;transform:scale3d(0,0,0) translate3d(-100%,0,0)}.fancybox-fx-circular.fancybox-slide--next{opacity:0;transform:scale3d(0,0,0) translate3d(100%,0,0)}.fancybox-fx-circular.fancybox-slide--current{opacity:1;transform:scaleX(1) translateZ(0)}.fancybox-fx-tube.fancybox-slide--previous{transform:translate3d(-100%,0,0) scale(.1) skew(-10deg)}.fancybox-fx-tube.fancybox-slide--next{transform:translate3d(100%,0,0) scale(.1) skew(10deg)}.fancybox-fx-tube.fancybox-slide--current{transform:translateZ(0) scale(1)}@media (max-height:576px){.fancybox-slide{padding-left:6px;padding-right:6px}.fancybox-slide--image{padding:6px 0}.fancybox-close-small{right:-6px}.fancybox-slide--image .fancybox-close-small{background:#4e4e4e;color:#f2f4f6;height:36px;opacity:1;padding:6px;right:0;top:0;width:36px}.fancybox-caption{padding-left:12px;padding-right:12px}@supports (padding:max(0px)){.fancybox-caption{padding-left:max(12px,env(safe-area-inset-left));padding-right:max(12px,env(safe-area-inset-right))}}}.fancybox-share{background:#f4f4f4;border-radius:3px;max-width:90%;padding:30px;text-align:center}.fancybox-share h1{color:#222;font-size:35px;font-weight:700;margin:0 0 20px}.fancybox-share p{margin:0;padding:0}.fancybox-share__button{border:0;border-radius:3px;display:inline-block;font-size:14px;font-weight:700;line-height:40px;margin:0 5px 10px;min-width:130px;padding:0 15px;text-decoration:none;transition:all .2s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;white-space:nowrap}.fancybox-share__button:link,.fancybox-share__button:visited{color:#fff}.fancybox-share__button:hover{text-decoration:none}.fancybox-share__button--fb{background:#3b5998}.fancybox-share__button--fb:hover{background:#344e86}.fancybox-share__button--pt{background:#bd081d}.fancybox-share__button--pt:hover{background:#aa0719}.fancybox-share__button--tw{background:#1da1f2}.fancybox-share__button--tw:hover{background:#0d95e8}.fancybox-share__button svg{height:25px;margin-right:7px;position:relative;top:-1px;vertical-align:middle;width:25px}.fancybox-share__button svg path{fill:#fff}.fancybox-share__input{background:transparent;border:0;border-bottom:1px solid #d7d7d7;border-radius:0;color:#5d5b5b;font-size:14px;margin:10px 0 0;outline:none;padding:10px 15px;width:100%}.fancybox-thumbs{background:#ddd;bottom:0;display:none;margin:0;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar;padding:2px 2px 4px;position:absolute;right:0;-webkit-tap-highlight-color:rgba(0,0,0,0);top:0;width:212px;z-index:99995}.fancybox-thumbs-x{overflow-x:auto;overflow-y:hidden}.fancybox-show-thumbs .fancybox-thumbs{display:block}.fancybox-show-thumbs .fancybox-inner{right:212px}.fancybox-thumbs__list{font-size:0;height:100%;list-style:none;margin:0;overflow-x:hidden;overflow-y:auto;padding:0;position:absolute;position:relative;white-space:nowrap;width:100%}.fancybox-thumbs-x .fancybox-thumbs__list{overflow:hidden}.fancybox-thumbs-y .fancybox-thumbs__list::-webkit-scrollbar{width:7px}.fancybox-thumbs-y .fancybox-thumbs__list::-webkit-scrollbar-track{background:#fff;border-radius:10px;box-shadow:inset 0 0 6px rgba(0,0,0,.3)}.fancybox-thumbs-y .fancybox-thumbs__list::-webkit-scrollbar-thumb{background:#2a2a2a;border-radius:10px}.fancybox-thumbs__list a{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:rgba(0,0,0,.1);background-position:50%;background-repeat:no-repeat;background-size:cover;cursor:pointer;float:left;height:75px;margin:2px;max-height:calc(100% - 8px);max-width:calc(50% - 4px);outline:none;overflow:hidden;padding:0;position:relative;-webkit-tap-highlight-color:transparent;width:100px}.fancybox-thumbs__list a:before{border:6px solid #ff5268;bottom:0;content:"";left:0;opacity:0;position:absolute;right:0;top:0;transition:all .2s cubic-bezier(.25,.46,.45,.94);z-index:99991}.fancybox-thumbs__list a:focus:before{opacity:.5}.fancybox-thumbs__list a.fancybox-thumbs-active:before{opacity:1}@media (max-width:576px){.fancybox-thumbs{width:110px}.fancybox-show-thumbs .fancybox-inner{right:110px}.fancybox-thumbs__list a{max-width:calc(100% - 10px)}} \ No newline at end of file diff --git a/dist/fancybox/jquery.fancybox.min.js b/dist/fancybox/jquery.fancybox.min.js new file mode 100644 index 0000000..d5d10f6 --- /dev/null +++ b/dist/fancybox/jquery.fancybox.min.js @@ -0,0 +1,13 @@ +// ================================================== +// fancyBox v3.5.7 +// +// Licensed GPLv3 for open source use +// or fancyBox Commercial License for commercial use +// +// http://fancyapps.com/fancybox/ +// Copyright 2019 fancyApps +// +// ================================================== +!function(t,e,n,o){"use strict";function i(t,e){var o,i,a,s=[],r=0;t&&t.isDefaultPrevented()||(t.preventDefault(),e=e||{},t&&t.data&&(e=h(t.data.options,e)),o=e.$target||n(t.currentTarget).trigger("blur"),(a=n.fancybox.getInstance())&&a.$trigger&&a.$trigger.is(o)||(e.selector?s=n(e.selector):(i=o.attr("data-fancybox")||"",i?(s=t.data?t.data.items:[],s=s.length?s.filter('[data-fancybox="'+i+'"]'):n('[data-fancybox="'+i+'"]')):s=[o]),r=n(s).index(o),r<0&&(r=0),a=n.fancybox.open(s,e,r),a.$trigger=o))}if(t.console=t.console||{info:function(t){}},n){if(n.fn.fancybox)return void console.info("fancyBox already initialized");var a={closeExisting:!1,loop:!1,gutter:50,keyboard:!0,preventCaptionOverlap:!0,arrows:!0,infobar:!0,smallBtn:"auto",toolbar:"auto",buttons:["zoom","slideShow","thumbs","close"],idleTime:3,protect:!1,modal:!1,image:{preload:!1},ajax:{settings:{data:{fancybox:!0}}},iframe:{tpl:'',preload:!0,css:{},attr:{scrolling:"auto"}},video:{tpl:'',format:"",autoStart:!0},defaultType:"image",animationEffect:"zoom",animationDuration:366,zoomOpacity:"auto",transitionEffect:"fade",transitionDuration:366,slideClass:"",baseClass:"",baseTpl:'',spinnerTpl:'
',errorTpl:'

{{ERROR}}

',btnTpl:{download:'',zoom:'',close:'',arrowLeft:'',arrowRight:'',smallBtn:''},parentEl:"body",hideScrollbar:!0,autoFocus:!0,backFocus:!0,trapFocus:!0,fullScreen:{autoStart:!1},touch:{vertical:!0,momentum:!0},hash:null,media:{},slideShow:{autoStart:!1,speed:3e3},thumbs:{autoStart:!1,hideOnClose:!0,parentEl:".fancybox-container",axis:"y"},wheel:"auto",onInit:n.noop,beforeLoad:n.noop,afterLoad:n.noop,beforeShow:n.noop,afterShow:n.noop,beforeClose:n.noop,afterClose:n.noop,onActivate:n.noop,onDeactivate:n.noop,clickContent:function(t,e){return"image"===t.type&&"zoom"},clickSlide:"close",clickOutside:"close",dblclickContent:!1,dblclickSlide:!1,dblclickOutside:!1,mobile:{preventCaptionOverlap:!1,idleTime:!1,clickContent:function(t,e){return"image"===t.type&&"toggleControls"},clickSlide:function(t,e){return"image"===t.type?"toggleControls":"close"},dblclickContent:function(t,e){return"image"===t.type&&"zoom"},dblclickSlide:function(t,e){return"image"===t.type&&"zoom"}},lang:"en",i18n:{en:{CLOSE:"Close",NEXT:"Next",PREV:"Previous",ERROR:"The requested content cannot be loaded.
Please try again later.",PLAY_START:"Start slideshow",PLAY_STOP:"Pause slideshow",FULL_SCREEN:"Full screen",THUMBS:"Thumbnails",DOWNLOAD:"Download",SHARE:"Share",ZOOM:"Zoom"},de:{CLOSE:"Schließen",NEXT:"Weiter",PREV:"Zurück",ERROR:"Die angeforderten Daten konnten nicht geladen werden.
Bitte versuchen Sie es später nochmal.",PLAY_START:"Diaschau starten",PLAY_STOP:"Diaschau beenden",FULL_SCREEN:"Vollbild",THUMBS:"Vorschaubilder",DOWNLOAD:"Herunterladen",SHARE:"Teilen",ZOOM:"Vergrößern"}}},s=n(t),r=n(e),c=0,l=function(t){return t&&t.hasOwnProperty&&t instanceof n},d=function(){return t.requestAnimationFrame||t.webkitRequestAnimationFrame||t.mozRequestAnimationFrame||t.oRequestAnimationFrame||function(e){return t.setTimeout(e,1e3/60)}}(),u=function(){return t.cancelAnimationFrame||t.webkitCancelAnimationFrame||t.mozCancelAnimationFrame||t.oCancelAnimationFrame||function(e){t.clearTimeout(e)}}(),f=function(){var t,n=e.createElement("fakeelement"),o={transition:"transitionend",OTransition:"oTransitionEnd",MozTransition:"transitionend",WebkitTransition:"webkitTransitionEnd"};for(t in o)if(void 0!==n.style[t])return o[t];return"transitionend"}(),p=function(t){return t&&t.length&&t[0].offsetHeight},h=function(t,e){var o=n.extend(!0,{},t,e);return n.each(e,function(t,e){n.isArray(e)&&(o[t]=e)}),o},g=function(t){var o,i;return!(!t||t.ownerDocument!==e)&&(n(".fancybox-container").css("pointer-events","none"),o={x:t.getBoundingClientRect().left+t.offsetWidth/2,y:t.getBoundingClientRect().top+t.offsetHeight/2},i=e.elementFromPoint(o.x,o.y)===t,n(".fancybox-container").css("pointer-events",""),i)},b=function(t,e,o){var i=this;i.opts=h({index:o},n.fancybox.defaults),n.isPlainObject(e)&&(i.opts=h(i.opts,e)),n.fancybox.isMobile&&(i.opts=h(i.opts,i.opts.mobile)),i.id=i.opts.id||++c,i.currIndex=parseInt(i.opts.index,10)||0,i.prevIndex=null,i.prevPos=null,i.currPos=0,i.firstRun=!0,i.group=[],i.slides={},i.addContent(t),i.group.length&&i.init()};n.extend(b.prototype,{init:function(){var o,i,a=this,s=a.group[a.currIndex],r=s.opts;r.closeExisting&&n.fancybox.close(!0),n("body").addClass("fancybox-active"),!n.fancybox.getInstance()&&!1!==r.hideScrollbar&&!n.fancybox.isMobile&&e.body.scrollHeight>t.innerHeight&&(n("head").append('"),n("body").addClass("compensate-for-scrollbar")),i="",n.each(r.buttons,function(t,e){i+=r.btnTpl[e]||""}),o=n(a.translate(a,r.baseTpl.replace("{{buttons}}",i).replace("{{arrows}}",r.btnTpl.arrowLeft+r.btnTpl.arrowRight))).attr("id","fancybox-container-"+a.id).addClass(r.baseClass).data("FancyBox",a).appendTo(r.parentEl),a.$refs={container:o},["bg","inner","infobar","toolbar","stage","caption","navigation"].forEach(function(t){a.$refs[t]=o.find(".fancybox-"+t)}),a.trigger("onInit"),a.activate(),a.jumpTo(a.currIndex)},translate:function(t,e){var n=t.opts.i18n[t.opts.lang]||t.opts.i18n.en;return e.replace(/\{\{(\w+)\}\}/g,function(t,e){return void 0===n[e]?t:n[e]})},addContent:function(t){var e,o=this,i=n.makeArray(t);n.each(i,function(t,e){var i,a,s,r,c,l={},d={};n.isPlainObject(e)?(l=e,d=e.opts||e):"object"===n.type(e)&&n(e).length?(i=n(e),d=i.data()||{},d=n.extend(!0,{},d,d.options),d.$orig=i,l.src=o.opts.src||d.src||i.attr("href"),l.type||l.src||(l.type="inline",l.src=e)):l={type:"html",src:e+""},l.opts=n.extend(!0,{},o.opts,d),n.isArray(d.buttons)&&(l.opts.buttons=d.buttons),n.fancybox.isMobile&&l.opts.mobile&&(l.opts=h(l.opts,l.opts.mobile)),a=l.type||l.opts.type,r=l.src||"",!a&&r&&((s=r.match(/\.(mp4|mov|ogv|webm)((\?|#).*)?$/i))?(a="video",l.opts.video.format||(l.opts.video.format="video/"+("ogv"===s[1]?"ogg":s[1]))):r.match(/(^data:image\/[a-z0-9+\/=]*,)|(\.(jp(e|g|eg)|gif|png|bmp|webp|svg|ico)((\?|#).*)?$)/i)?a="image":r.match(/\.(pdf)((\?|#).*)?$/i)?(a="iframe",l=n.extend(!0,l,{contentType:"pdf",opts:{iframe:{preload:!1}}})):"#"===r.charAt(0)&&(a="inline")),a?l.type=a:o.trigger("objectNeedsType",l),l.contentType||(l.contentType=n.inArray(l.type,["html","inline","ajax"])>-1?"html":l.type),l.index=o.group.length,"auto"==l.opts.smallBtn&&(l.opts.smallBtn=n.inArray(l.type,["html","inline","ajax"])>-1),"auto"===l.opts.toolbar&&(l.opts.toolbar=!l.opts.smallBtn),l.$thumb=l.opts.$thumb||null,l.opts.$trigger&&l.index===o.opts.index&&(l.$thumb=l.opts.$trigger.find("img:first"),l.$thumb.length&&(l.opts.$orig=l.opts.$trigger)),l.$thumb&&l.$thumb.length||!l.opts.$orig||(l.$thumb=l.opts.$orig.find("img:first")),l.$thumb&&!l.$thumb.length&&(l.$thumb=null),l.thumb=l.opts.thumb||(l.$thumb?l.$thumb[0].src:null),"function"===n.type(l.opts.caption)&&(l.opts.caption=l.opts.caption.apply(e,[o,l])),"function"===n.type(o.opts.caption)&&(l.opts.caption=o.opts.caption.apply(e,[o,l])),l.opts.caption instanceof n||(l.opts.caption=void 0===l.opts.caption?"":l.opts.caption+""),"ajax"===l.type&&(c=r.split(/\s+/,2),c.length>1&&(l.src=c.shift(),l.opts.filter=c.shift())),l.opts.modal&&(l.opts=n.extend(!0,l.opts,{trapFocus:!0,infobar:0,toolbar:0,smallBtn:0,keyboard:0,slideShow:0,fullScreen:0,thumbs:0,touch:0,clickContent:!1,clickSlide:!1,clickOutside:!1,dblclickContent:!1,dblclickSlide:!1,dblclickOutside:!1})),o.group.push(l)}),Object.keys(o.slides).length&&(o.updateControls(),(e=o.Thumbs)&&e.isActive&&(e.create(),e.focus()))},addEvents:function(){var e=this;e.removeEvents(),e.$refs.container.on("click.fb-close","[data-fancybox-close]",function(t){t.stopPropagation(),t.preventDefault(),e.close(t)}).on("touchstart.fb-prev click.fb-prev","[data-fancybox-prev]",function(t){t.stopPropagation(),t.preventDefault(),e.previous()}).on("touchstart.fb-next click.fb-next","[data-fancybox-next]",function(t){t.stopPropagation(),t.preventDefault(),e.next()}).on("click.fb","[data-fancybox-zoom]",function(t){e[e.isScaledDown()?"scaleToActual":"scaleToFit"]()}),s.on("orientationchange.fb resize.fb",function(t){t&&t.originalEvent&&"resize"===t.originalEvent.type?(e.requestId&&u(e.requestId),e.requestId=d(function(){e.update(t)})):(e.current&&"iframe"===e.current.type&&e.$refs.stage.hide(),setTimeout(function(){e.$refs.stage.show(),e.update(t)},n.fancybox.isMobile?600:250))}),r.on("keydown.fb",function(t){var o=n.fancybox?n.fancybox.getInstance():null,i=o.current,a=t.keyCode||t.which;if(9==a)return void(i.opts.trapFocus&&e.focus(t));if(!(!i.opts.keyboard||t.ctrlKey||t.altKey||t.shiftKey||n(t.target).is("input,textarea,video,audio,select")))return 8===a||27===a?(t.preventDefault(),void e.close(t)):37===a||38===a?(t.preventDefault(),void e.previous()):39===a||40===a?(t.preventDefault(),void e.next()):void e.trigger("afterKeydown",t,a)}),e.group[e.currIndex].opts.idleTime&&(e.idleSecondsCounter=0,r.on("mousemove.fb-idle mouseleave.fb-idle mousedown.fb-idle touchstart.fb-idle touchmove.fb-idle scroll.fb-idle keydown.fb-idle",function(t){e.idleSecondsCounter=0,e.isIdle&&e.showControls(),e.isIdle=!1}),e.idleInterval=t.setInterval(function(){++e.idleSecondsCounter>=e.group[e.currIndex].opts.idleTime&&!e.isDragging&&(e.isIdle=!0,e.idleSecondsCounter=0,e.hideControls())},1e3))},removeEvents:function(){var e=this;s.off("orientationchange.fb resize.fb"),r.off("keydown.fb .fb-idle"),this.$refs.container.off(".fb-close .fb-prev .fb-next"),e.idleInterval&&(t.clearInterval(e.idleInterval),e.idleInterval=null)},previous:function(t){return this.jumpTo(this.currPos-1,t)},next:function(t){return this.jumpTo(this.currPos+1,t)},jumpTo:function(t,e){var o,i,a,s,r,c,l,d,u,f=this,h=f.group.length;if(!(f.isDragging||f.isClosing||f.isAnimating&&f.firstRun)){if(t=parseInt(t,10),!(a=f.current?f.current.opts.loop:f.opts.loop)&&(t<0||t>=h))return!1;if(o=f.firstRun=!Object.keys(f.slides).length,r=f.current,f.prevIndex=f.currIndex,f.prevPos=f.currPos,s=f.createSlide(t),h>1&&((a||s.index0)&&f.createSlide(t-1)),f.current=s,f.currIndex=s.index,f.currPos=s.pos,f.trigger("beforeShow",o),f.updateControls(),s.forcedDuration=void 0,n.isNumeric(e)?s.forcedDuration=e:e=s.opts[o?"animationDuration":"transitionDuration"],e=parseInt(e,10),i=f.isMoved(s),s.$slide.addClass("fancybox-slide--current"),o)return s.opts.animationEffect&&e&&f.$refs.container.css("transition-duration",e+"ms"),f.$refs.container.addClass("fancybox-is-open").trigger("focus"),f.loadSlide(s),void f.preload("image");c=n.fancybox.getTranslate(r.$slide),l=n.fancybox.getTranslate(f.$refs.stage),n.each(f.slides,function(t,e){n.fancybox.stop(e.$slide,!0)}),r.pos!==s.pos&&(r.isComplete=!1),r.$slide.removeClass("fancybox-slide--complete fancybox-slide--current"),i?(u=c.left-(r.pos*c.width+r.pos*r.opts.gutter),n.each(f.slides,function(t,o){o.$slide.removeClass("fancybox-animated").removeClass(function(t,e){return(e.match(/(^|\s)fancybox-fx-\S+/g)||[]).join(" ")});var i=o.pos*c.width+o.pos*o.opts.gutter;n.fancybox.setTranslate(o.$slide,{top:0,left:i-l.left+u}),o.pos!==s.pos&&o.$slide.addClass("fancybox-slide--"+(o.pos>s.pos?"next":"previous")),p(o.$slide),n.fancybox.animate(o.$slide,{top:0,left:(o.pos-s.pos)*c.width+(o.pos-s.pos)*o.opts.gutter},e,function(){o.$slide.css({transform:"",opacity:""}).removeClass("fancybox-slide--next fancybox-slide--previous"),o.pos===f.currPos&&f.complete()})})):e&&s.opts.transitionEffect&&(d="fancybox-animated fancybox-fx-"+s.opts.transitionEffect,r.$slide.addClass("fancybox-slide--"+(r.pos>s.pos?"next":"previous")),n.fancybox.animate(r.$slide,d,e,function(){r.$slide.removeClass(d).removeClass("fancybox-slide--next fancybox-slide--previous")},!1)),s.isLoaded?f.revealContent(s):f.loadSlide(s),f.preload("image")}},createSlide:function(t){var e,o,i=this;return o=t%i.group.length,o=o<0?i.group.length+o:o,!i.slides[t]&&i.group[o]&&(e=n('
').appendTo(i.$refs.stage),i.slides[t]=n.extend(!0,{},i.group[o],{pos:t,$slide:e,isLoaded:!1}),i.updateSlide(i.slides[t])),i.slides[t]},scaleToActual:function(t,e,o){var i,a,s,r,c,l=this,d=l.current,u=d.$content,f=n.fancybox.getTranslate(d.$slide).width,p=n.fancybox.getTranslate(d.$slide).height,h=d.width,g=d.height;l.isAnimating||l.isMoved()||!u||"image"!=d.type||!d.isLoaded||d.hasError||(l.isAnimating=!0,n.fancybox.stop(u),t=void 0===t?.5*f:t,e=void 0===e?.5*p:e,i=n.fancybox.getTranslate(u),i.top-=n.fancybox.getTranslate(d.$slide).top,i.left-=n.fancybox.getTranslate(d.$slide).left,r=h/i.width,c=g/i.height,a=.5*f-.5*h,s=.5*p-.5*g,h>f&&(a=i.left*r-(t*r-t),a>0&&(a=0),ap&&(s=i.top*c-(e*c-e),s>0&&(s=0),se-.5&&(l=e),d>o-.5&&(d=o),"image"===t.type?(u.top=Math.floor(.5*(o-d))+parseFloat(c.css("paddingTop")),u.left=Math.floor(.5*(e-l))+parseFloat(c.css("paddingLeft"))):"video"===t.contentType&&(a=t.opts.width&&t.opts.height?l/d:t.opts.ratio||16/9,d>l/a?d=l/a:l>d*a&&(l=d*a)),u.width=l,u.height=d,u)},update:function(t){var e=this;n.each(e.slides,function(n,o){e.updateSlide(o,t)})},updateSlide:function(t,e){var o=this,i=t&&t.$content,a=t.width||t.opts.width,s=t.height||t.opts.height,r=t.$slide;o.adjustCaption(t),i&&(a||s||"video"===t.contentType)&&!t.hasError&&(n.fancybox.stop(i),n.fancybox.setTranslate(i,o.getFitPos(t)),t.pos===o.currPos&&(o.isAnimating=!1,o.updateCursor())),o.adjustLayout(t),r.length&&(r.trigger("refresh"),t.pos===o.currPos&&o.$refs.toolbar.add(o.$refs.navigation.find(".fancybox-button--arrow_right")).toggleClass("compensate-for-scrollbar",r.get(0).scrollHeight>r.get(0).clientHeight)),o.trigger("onUpdate",t,e)},centerSlide:function(t){var e=this,o=e.current,i=o.$slide;!e.isClosing&&o&&(i.siblings().css({transform:"",opacity:""}),i.parent().children().removeClass("fancybox-slide--previous fancybox-slide--next"),n.fancybox.animate(i,{top:0,left:0,opacity:1},void 0===t?0:t,function(){i.css({transform:"",opacity:""}),o.isComplete||e.complete()},!1))},isMoved:function(t){var e,o,i=t||this.current;return!!i&&(o=n.fancybox.getTranslate(this.$refs.stage),e=n.fancybox.getTranslate(i.$slide),!i.$slide.hasClass("fancybox-animated")&&(Math.abs(e.top-o.top)>.5||Math.abs(e.left-o.left)>.5))},updateCursor:function(t,e){var o,i,a=this,s=a.current,r=a.$refs.container;s&&!a.isClosing&&a.Guestures&&(r.removeClass("fancybox-is-zoomable fancybox-can-zoomIn fancybox-can-zoomOut fancybox-can-swipe fancybox-can-pan"),o=a.canPan(t,e),i=!!o||a.isZoomable(),r.toggleClass("fancybox-is-zoomable",i),n("[data-fancybox-zoom]").prop("disabled",!i),o?r.addClass("fancybox-can-pan"):i&&("zoom"===s.opts.clickContent||n.isFunction(s.opts.clickContent)&&"zoom"==s.opts.clickContent(s))?r.addClass("fancybox-can-zoomIn"):s.opts.touch&&(s.opts.touch.vertical||a.group.length>1)&&"video"!==s.contentType&&r.addClass("fancybox-can-swipe"))},isZoomable:function(){var t,e=this,n=e.current;if(n&&!e.isClosing&&"image"===n.type&&!n.hasError){if(!n.isLoaded)return!0;if((t=e.getFitPos(n))&&(n.width>t.width||n.height>t.height))return!0}return!1},isScaledDown:function(t,e){var o=this,i=!1,a=o.current,s=a.$content;return void 0!==t&&void 0!==e?i=t1.5||Math.abs(a.height-s.height)>1.5)),s},loadSlide:function(t){var e,o,i,a=this;if(!t.isLoading&&!t.isLoaded){if(t.isLoading=!0,!1===a.trigger("beforeLoad",t))return t.isLoading=!1,!1;switch(e=t.type,o=t.$slide,o.off("refresh").trigger("onReset").addClass(t.opts.slideClass),e){case"image":a.setImage(t);break;case"iframe":a.setIframe(t);break;case"html":a.setContent(t,t.src||t.content);break;case"video":a.setContent(t,t.opts.video.tpl.replace(/\{\{src\}\}/gi,t.src).replace("{{format}}",t.opts.videoFormat||t.opts.video.format||"").replace("{{poster}}",t.thumb||""));break;case"inline":n(t.src).length?a.setContent(t,n(t.src)):a.setError(t);break;case"ajax":a.showLoading(t),i=n.ajax(n.extend({},t.opts.ajax.settings,{url:t.src,success:function(e,n){"success"===n&&a.setContent(t,e)},error:function(e,n){e&&"abort"!==n&&a.setError(t)}})),o.one("onReset",function(){i.abort()});break;default:a.setError(t)}return!0}},setImage:function(t){var o,i=this;setTimeout(function(){var e=t.$image;i.isClosing||!t.isLoading||e&&e.length&&e[0].complete||t.hasError||i.showLoading(t)},50),i.checkSrcset(t),t.$content=n('
').addClass("fancybox-is-hidden").appendTo(t.$slide.addClass("fancybox-slide--image")),!1!==t.opts.preload&&t.opts.width&&t.opts.height&&t.thumb&&(t.width=t.opts.width,t.height=t.opts.height,o=e.createElement("img"),o.onerror=function(){n(this).remove(),t.$ghost=null},o.onload=function(){i.afterLoad(t)},t.$ghost=n(o).addClass("fancybox-image").appendTo(t.$content).attr("src",t.thumb)),i.setBigImage(t)},checkSrcset:function(e){var n,o,i,a,s=e.opts.srcset||e.opts.image.srcset;if(s){i=t.devicePixelRatio||1,a=t.innerWidth*i,o=s.split(",").map(function(t){var e={};return t.trim().split(/\s+/).forEach(function(t,n){var o=parseInt(t.substring(0,t.length-1),10);if(0===n)return e.url=t;o&&(e.value=o,e.postfix=t[t.length-1])}),e}),o.sort(function(t,e){return t.value-e.value});for(var r=0;r=a||"x"===c.postfix&&c.value>=i){n=c;break}}!n&&o.length&&(n=o[o.length-1]),n&&(e.src=n.url,e.width&&e.height&&"w"==n.postfix&&(e.height=e.width/e.height*n.value,e.width=n.value),e.opts.srcset=s)}},setBigImage:function(t){var o=this,i=e.createElement("img"),a=n(i);t.$image=a.one("error",function(){o.setError(t)}).one("load",function(){var e;t.$ghost||(o.resolveImageSlideSize(t,this.naturalWidth,this.naturalHeight),o.afterLoad(t)),o.isClosing||(t.opts.srcset&&(e=t.opts.sizes,e&&"auto"!==e||(e=(t.width/t.height>1&&s.width()/s.height()>1?"100":Math.round(t.width/t.height*100))+"vw"),a.attr("sizes",e).attr("srcset",t.opts.srcset)),t.$ghost&&setTimeout(function(){t.$ghost&&!o.isClosing&&t.$ghost.hide()},Math.min(300,Math.max(1e3,t.height/1600))),o.hideLoading(t))}).addClass("fancybox-image").attr("src",t.src).appendTo(t.$content),(i.complete||"complete"==i.readyState)&&a.naturalWidth&&a.naturalHeight?a.trigger("load"):i.error&&a.trigger("error")},resolveImageSlideSize:function(t,e,n){var o=parseInt(t.opts.width,10),i=parseInt(t.opts.height,10);t.width=e,t.height=n,o>0&&(t.width=o,t.height=Math.floor(o*n/e)),i>0&&(t.width=Math.floor(i*e/n),t.height=i)},setIframe:function(t){var e,o=this,i=t.opts.iframe,a=t.$slide;t.$content=n('
').css(i.css).appendTo(a),a.addClass("fancybox-slide--"+t.contentType),t.$iframe=e=n(i.tpl.replace(/\{rnd\}/g,(new Date).getTime())).attr(i.attr).appendTo(t.$content),i.preload?(o.showLoading(t),e.on("load.fb error.fb",function(e){this.isReady=1,t.$slide.trigger("refresh"),o.afterLoad(t)}),a.on("refresh.fb",function(){var n,o,s=t.$content,r=i.css.width,c=i.css.height;if(1===e[0].isReady){try{n=e.contents(),o=n.find("body")}catch(t){}o&&o.length&&o.children().length&&(a.css("overflow","visible"),s.css({width:"100%","max-width":"100%",height:"9999px"}),void 0===r&&(r=Math.ceil(Math.max(o[0].clientWidth,o.outerWidth(!0)))),s.css("width",r||"").css("max-width",""),void 0===c&&(c=Math.ceil(Math.max(o[0].clientHeight,o.outerHeight(!0)))),s.css("height",c||""),a.css("overflow","auto")),s.removeClass("fancybox-is-hidden")}})):o.afterLoad(t),e.attr("src",t.src),a.one("onReset",function(){try{n(this).find("iframe").hide().unbind().attr("src","//about:blank")}catch(t){}n(this).off("refresh.fb").empty(),t.isLoaded=!1,t.isRevealed=!1})},setContent:function(t,e){var o=this;o.isClosing||(o.hideLoading(t),t.$content&&n.fancybox.stop(t.$content),t.$slide.empty(),l(e)&&e.parent().length?((e.hasClass("fancybox-content")||e.parent().hasClass("fancybox-content"))&&e.parents(".fancybox-slide").trigger("onReset"),t.$placeholder=n("
").hide().insertAfter(e),e.css("display","inline-block")):t.hasError||("string"===n.type(e)&&(e=n("
").append(n.trim(e)).contents()),t.opts.filter&&(e=n("
").html(e).find(t.opts.filter))),t.$slide.one("onReset",function(){n(this).find("video,audio").trigger("pause"),t.$placeholder&&(t.$placeholder.after(e.removeClass("fancybox-content").hide()).remove(),t.$placeholder=null),t.$smallBtn&&(t.$smallBtn.remove(),t.$smallBtn=null),t.hasError||(n(this).empty(),t.isLoaded=!1,t.isRevealed=!1)}),n(e).appendTo(t.$slide),n(e).is("video,audio")&&(n(e).addClass("fancybox-video"),n(e).wrap("
"),t.contentType="video",t.opts.width=t.opts.width||n(e).attr("width"),t.opts.height=t.opts.height||n(e).attr("height")),t.$content=t.$slide.children().filter("div,form,main,video,audio,article,.fancybox-content").first(),t.$content.siblings().hide(),t.$content.length||(t.$content=t.$slide.wrapInner("
").children().first()),t.$content.addClass("fancybox-content"),t.$slide.addClass("fancybox-slide--"+t.contentType),o.afterLoad(t))},setError:function(t){t.hasError=!0,t.$slide.trigger("onReset").removeClass("fancybox-slide--"+t.contentType).addClass("fancybox-slide--error"),t.contentType="html",this.setContent(t,this.translate(t,t.opts.errorTpl)),t.pos===this.currPos&&(this.isAnimating=!1)},showLoading:function(t){var e=this;(t=t||e.current)&&!t.$spinner&&(t.$spinner=n(e.translate(e,e.opts.spinnerTpl)).appendTo(t.$slide).hide().fadeIn("fast"))},hideLoading:function(t){var e=this;(t=t||e.current)&&t.$spinner&&(t.$spinner.stop().remove(),delete t.$spinner)},afterLoad:function(t){var e=this;e.isClosing||(t.isLoading=!1,t.isLoaded=!0,e.trigger("afterLoad",t),e.hideLoading(t),!t.opts.smallBtn||t.$smallBtn&&t.$smallBtn.length||(t.$smallBtn=n(e.translate(t,t.opts.btnTpl.smallBtn)).appendTo(t.$content)),t.opts.protect&&t.$content&&!t.hasError&&(t.$content.on("contextmenu.fb",function(t){return 2==t.button&&t.preventDefault(),!0}),"image"===t.type&&n('
').appendTo(t.$content)),e.adjustCaption(t),e.adjustLayout(t),t.pos===e.currPos&&e.updateCursor(),e.revealContent(t))},adjustCaption:function(t){var e,n=this,o=t||n.current,i=o.opts.caption,a=o.opts.preventCaptionOverlap,s=n.$refs.caption,r=!1;s.toggleClass("fancybox-caption--separate",a),a&&i&&i.length&&(o.pos!==n.currPos?(e=s.clone().appendTo(s.parent()),e.children().eq(0).empty().html(i),r=e.outerHeight(!0),e.empty().remove()):n.$caption&&(r=n.$caption.outerHeight(!0)),o.$slide.css("padding-bottom",r||""))},adjustLayout:function(t){var e,n,o,i,a=this,s=t||a.current;s.isLoaded&&!0!==s.opts.disableLayoutFix&&(s.$content.css("margin-bottom",""),s.$content.outerHeight()>s.$slide.height()+.5&&(o=s.$slide[0].style["padding-bottom"],i=s.$slide.css("padding-bottom"),parseFloat(i)>0&&(e=s.$slide[0].scrollHeight,s.$slide.css("padding-bottom",0),Math.abs(e-s.$slide[0].scrollHeight)<1&&(n=i),s.$slide.css("padding-bottom",o))),s.$content.css("margin-bottom",n))},revealContent:function(t){var e,o,i,a,s=this,r=t.$slide,c=!1,l=!1,d=s.isMoved(t),u=t.isRevealed;return t.isRevealed=!0,e=t.opts[s.firstRun?"animationEffect":"transitionEffect"],i=t.opts[s.firstRun?"animationDuration":"transitionDuration"],i=parseInt(void 0===t.forcedDuration?i:t.forcedDuration,10),!d&&t.pos===s.currPos&&i||(e=!1),"zoom"===e&&(t.pos===s.currPos&&i&&"image"===t.type&&!t.hasError&&(l=s.getThumbPos(t))?c=s.getFitPos(t):e="fade"),"zoom"===e?(s.isAnimating=!0,c.scaleX=c.width/l.width,c.scaleY=c.height/l.height,a=t.opts.zoomOpacity,"auto"==a&&(a=Math.abs(t.width/t.height-l.width/l.height)>.1),a&&(l.opacity=.1,c.opacity=1),n.fancybox.setTranslate(t.$content.removeClass("fancybox-is-hidden"),l),p(t.$content),void n.fancybox.animate(t.$content,c,i,function(){s.isAnimating=!1,s.complete()})):(s.updateSlide(t),e?(n.fancybox.stop(r),o="fancybox-slide--"+(t.pos>=s.prevPos?"next":"previous")+" fancybox-animated fancybox-fx-"+e,r.addClass(o).removeClass("fancybox-slide--current"),t.$content.removeClass("fancybox-is-hidden"),p(r),"image"!==t.type&&t.$content.hide().show(0),void n.fancybox.animate(r,"fancybox-slide--current",i,function(){r.removeClass(o).css({transform:"",opacity:""}),t.pos===s.currPos&&s.complete()},!0)):(t.$content.removeClass("fancybox-is-hidden"),u||!d||"image"!==t.type||t.hasError||t.$content.hide().fadeIn("fast"),void(t.pos===s.currPos&&s.complete())))},getThumbPos:function(t){var e,o,i,a,s,r=!1,c=t.$thumb;return!(!c||!g(c[0]))&&(e=n.fancybox.getTranslate(c),o=parseFloat(c.css("border-top-width")||0),i=parseFloat(c.css("border-right-width")||0),a=parseFloat(c.css("border-bottom-width")||0),s=parseFloat(c.css("border-left-width")||0),r={top:e.top+o,left:e.left+s,width:e.width-i-s,height:e.height-o-a,scaleX:1,scaleY:1},e.width>0&&e.height>0&&r)},complete:function(){var t,e=this,o=e.current,i={};!e.isMoved()&&o.isLoaded&&(o.isComplete||(o.isComplete=!0,o.$slide.siblings().trigger("onReset"),e.preload("inline"),p(o.$slide),o.$slide.addClass("fancybox-slide--complete"),n.each(e.slides,function(t,o){o.pos>=e.currPos-1&&o.pos<=e.currPos+1?i[o.pos]=o:o&&(n.fancybox.stop(o.$slide),o.$slide.off().remove())}),e.slides=i),e.isAnimating=!1,e.updateCursor(),e.trigger("afterShow"),o.opts.video.autoStart&&o.$slide.find("video,audio").filter(":visible:first").trigger("play").one("ended",function(){Document.exitFullscreen?Document.exitFullscreen():this.webkitExitFullscreen&&this.webkitExitFullscreen(),e.next()}),o.opts.autoFocus&&"html"===o.contentType&&(t=o.$content.find("input[autofocus]:enabled:visible:first"),t.length?t.trigger("focus"):e.focus(null,!0)),o.$slide.scrollTop(0).scrollLeft(0))},preload:function(t){var e,n,o=this;o.group.length<2||(n=o.slides[o.currPos+1],e=o.slides[o.currPos-1],e&&e.type===t&&o.loadSlide(e),n&&n.type===t&&o.loadSlide(n))},focus:function(t,o){var i,a,s=this,r=["a[href]","area[href]",'input:not([disabled]):not([type="hidden"]):not([aria-hidden])',"select:not([disabled]):not([aria-hidden])","textarea:not([disabled]):not([aria-hidden])","button:not([disabled]):not([aria-hidden])","iframe","object","embed","video","audio","[contenteditable]",'[tabindex]:not([tabindex^="-"])'].join(",");s.isClosing||(i=!t&&s.current&&s.current.isComplete?s.current.$slide.find("*:visible"+(o?":not(.fancybox-close-small)":"")):s.$refs.container.find("*:visible"),i=i.filter(r).filter(function(){return"hidden"!==n(this).css("visibility")&&!n(this).hasClass("disabled")}),i.length?(a=i.index(e.activeElement),t&&t.shiftKey?(a<0||0==a)&&(t.preventDefault(),i.eq(i.length-1).trigger("focus")):(a<0||a==i.length-1)&&(t&&t.preventDefault(),i.eq(0).trigger("focus"))):s.$refs.container.trigger("focus"))},activate:function(){var t=this;n(".fancybox-container").each(function(){var e=n(this).data("FancyBox");e&&e.id!==t.id&&!e.isClosing&&(e.trigger("onDeactivate"),e.removeEvents(),e.isVisible=!1)}),t.isVisible=!0,(t.current||t.isIdle)&&(t.update(),t.updateControls()),t.trigger("onActivate"),t.addEvents()},close:function(t,e){var o,i,a,s,r,c,l,u=this,f=u.current,h=function(){u.cleanUp(t)};return!u.isClosing&&(u.isClosing=!0,!1===u.trigger("beforeClose",t)?(u.isClosing=!1,d(function(){u.update()}),!1):(u.removeEvents(),a=f.$content,o=f.opts.animationEffect,i=n.isNumeric(e)?e:o?f.opts.animationDuration:0,f.$slide.removeClass("fancybox-slide--complete fancybox-slide--next fancybox-slide--previous fancybox-animated"),!0!==t?n.fancybox.stop(f.$slide):o=!1,f.$slide.siblings().trigger("onReset").remove(),i&&u.$refs.container.removeClass("fancybox-is-open").addClass("fancybox-is-closing").css("transition-duration",i+"ms"),u.hideLoading(f),u.hideControls(!0),u.updateCursor(),"zoom"!==o||a&&i&&"image"===f.type&&!u.isMoved()&&!f.hasError&&(l=u.getThumbPos(f))||(o="fade"),"zoom"===o?(n.fancybox.stop(a),s=n.fancybox.getTranslate(a),c={top:s.top,left:s.left,scaleX:s.width/l.width,scaleY:s.height/l.height,width:l.width,height:l.height},r=f.opts.zoomOpacity, +"auto"==r&&(r=Math.abs(f.width/f.height-l.width/l.height)>.1),r&&(l.opacity=0),n.fancybox.setTranslate(a,c),p(a),n.fancybox.animate(a,l,i,h),!0):(o&&i?n.fancybox.animate(f.$slide.addClass("fancybox-slide--previous").removeClass("fancybox-slide--current"),"fancybox-animated fancybox-fx-"+o,i,h):!0===t?setTimeout(h,i):h(),!0)))},cleanUp:function(e){var o,i,a,s=this,r=s.current.opts.$orig;s.current.$slide.trigger("onReset"),s.$refs.container.empty().remove(),s.trigger("afterClose",e),s.current.opts.backFocus&&(r&&r.length&&r.is(":visible")||(r=s.$trigger),r&&r.length&&(i=t.scrollX,a=t.scrollY,r.trigger("focus"),n("html, body").scrollTop(a).scrollLeft(i))),s.current=null,o=n.fancybox.getInstance(),o?o.activate():(n("body").removeClass("fancybox-active compensate-for-scrollbar"),n("#fancybox-style-noscroll").remove())},trigger:function(t,e){var o,i=Array.prototype.slice.call(arguments,1),a=this,s=e&&e.opts?e:a.current;if(s?i.unshift(s):s=a,i.unshift(a),n.isFunction(s.opts[t])&&(o=s.opts[t].apply(s,i)),!1===o)return o;"afterClose"!==t&&a.$refs?a.$refs.container.trigger(t+".fb",i):r.trigger(t+".fb",i)},updateControls:function(){var t=this,o=t.current,i=o.index,a=t.$refs.container,s=t.$refs.caption,r=o.opts.caption;o.$slide.trigger("refresh"),r&&r.length?(t.$caption=s,s.children().eq(0).html(r)):t.$caption=null,t.hasHiddenControls||t.isIdle||t.showControls(),a.find("[data-fancybox-count]").html(t.group.length),a.find("[data-fancybox-index]").html(i+1),a.find("[data-fancybox-prev]").prop("disabled",!o.opts.loop&&i<=0),a.find("[data-fancybox-next]").prop("disabled",!o.opts.loop&&i>=t.group.length-1),"image"===o.type?a.find("[data-fancybox-zoom]").show().end().find("[data-fancybox-download]").attr("href",o.opts.image.src||o.src).show():o.opts.toolbar&&a.find("[data-fancybox-download],[data-fancybox-zoom]").hide(),n(e.activeElement).is(":hidden,[disabled]")&&t.$refs.container.trigger("focus")},hideControls:function(t){var e=this,n=["infobar","toolbar","nav"];!t&&e.current.opts.preventCaptionOverlap||n.push("caption"),this.$refs.container.removeClass(n.map(function(t){return"fancybox-show-"+t}).join(" ")),this.hasHiddenControls=!0},showControls:function(){var t=this,e=t.current?t.current.opts:t.opts,n=t.$refs.container;t.hasHiddenControls=!1,t.idleSecondsCounter=0,n.toggleClass("fancybox-show-toolbar",!(!e.toolbar||!e.buttons)).toggleClass("fancybox-show-infobar",!!(e.infobar&&t.group.length>1)).toggleClass("fancybox-show-caption",!!t.$caption).toggleClass("fancybox-show-nav",!!(e.arrows&&t.group.length>1)).toggleClass("fancybox-is-modal",!!e.modal)},toggleControls:function(){this.hasHiddenControls?this.showControls():this.hideControls()}}),n.fancybox={version:"3.5.7",defaults:a,getInstance:function(t){var e=n('.fancybox-container:not(".fancybox-is-closing"):last').data("FancyBox"),o=Array.prototype.slice.call(arguments,1);return e instanceof b&&("string"===n.type(t)?e[t].apply(e,o):"function"===n.type(t)&&t.apply(e,o),e)},open:function(t,e,n){return new b(t,e,n)},close:function(t){var e=this.getInstance();e&&(e.close(),!0===t&&this.close(t))},destroy:function(){this.close(!0),r.add("body").off("click.fb-start","**")},isMobile:/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),use3d:function(){var n=e.createElement("div");return t.getComputedStyle&&t.getComputedStyle(n)&&t.getComputedStyle(n).getPropertyValue("transform")&&!(e.documentMode&&e.documentMode<11)}(),getTranslate:function(t){var e;return!(!t||!t.length)&&(e=t[0].getBoundingClientRect(),{top:e.top||0,left:e.left||0,width:e.width,height:e.height,opacity:parseFloat(t.css("opacity"))})},setTranslate:function(t,e){var n="",o={};if(t&&e)return void 0===e.left&&void 0===e.top||(n=(void 0===e.left?t.position().left:e.left)+"px, "+(void 0===e.top?t.position().top:e.top)+"px",n=this.use3d?"translate3d("+n+", 0px)":"translate("+n+")"),void 0!==e.scaleX&&void 0!==e.scaleY?n+=" scale("+e.scaleX+", "+e.scaleY+")":void 0!==e.scaleX&&(n+=" scaleX("+e.scaleX+")"),n.length&&(o.transform=n),void 0!==e.opacity&&(o.opacity=e.opacity),void 0!==e.width&&(o.width=e.width),void 0!==e.height&&(o.height=e.height),t.css(o)},animate:function(t,e,o,i,a){var s,r=this;n.isFunction(o)&&(i=o,o=null),r.stop(t),s=r.getTranslate(t),t.on(f,function(c){(!c||!c.originalEvent||t.is(c.originalEvent.target)&&"z-index"!=c.originalEvent.propertyName)&&(r.stop(t),n.isNumeric(o)&&t.css("transition-duration",""),n.isPlainObject(e)?void 0!==e.scaleX&&void 0!==e.scaleY&&r.setTranslate(t,{top:e.top,left:e.left,width:s.width*e.scaleX,height:s.height*e.scaleY,scaleX:1,scaleY:1}):!0!==a&&t.removeClass(e),n.isFunction(i)&&i(c))}),n.isNumeric(o)&&t.css("transition-duration",o+"ms"),n.isPlainObject(e)?(void 0!==e.scaleX&&void 0!==e.scaleY&&(delete e.width,delete e.height,t.parent().hasClass("fancybox-slide--image")&&t.parent().addClass("fancybox-is-scaling")),n.fancybox.setTranslate(t,e)):t.addClass(e),t.data("timer",setTimeout(function(){t.trigger(f)},o+33))},stop:function(t,e){t&&t.length&&(clearTimeout(t.data("timer")),e&&t.trigger(f),t.off(f).css("transition-duration",""),t.parent().removeClass("fancybox-is-scaling"))}},n.fn.fancybox=function(t){var e;return t=t||{},e=t.selector||!1,e?n("body").off("click.fb-start",e).on("click.fb-start",e,{options:t},i):this.off("click.fb-start").on("click.fb-start",{items:this,options:t},i),this},r.on("click.fb-start","[data-fancybox]",i),r.on("click.fb-start","[data-fancybox-trigger]",function(t){n('[data-fancybox="'+n(this).attr("data-fancybox-trigger")+'"]').eq(n(this).attr("data-fancybox-index")||0).trigger("click.fb-start",{$trigger:n(this)})}),function(){var t=null;r.on("mousedown mouseup focus blur",".fancybox-button",function(e){switch(e.type){case"mousedown":t=n(this);break;case"mouseup":t=null;break;case"focusin":n(".fancybox-button").removeClass("fancybox-focus"),n(this).is(t)||n(this).is("[disabled]")||n(this).addClass("fancybox-focus");break;case"focusout":n(".fancybox-button").removeClass("fancybox-focus")}})}()}}(window,document,jQuery),function(t){"use strict";var e={youtube:{matcher:/(youtube\.com|youtu\.be|youtube\-nocookie\.com)\/(watch\?(.*&)?v=|v\/|u\/|embed\/?)?(videoseries\?list=(.*)|[\w-]{11}|\?listType=(.*)&list=(.*))(.*)/i,params:{autoplay:1,autohide:1,fs:1,rel:0,hd:1,wmode:"transparent",enablejsapi:1,html5:1},paramPlace:8,type:"iframe",url:"https://www.youtube-nocookie.com/embed/$4",thumb:"https://img.youtube.com/vi/$4/hqdefault.jpg"},vimeo:{matcher:/^.+vimeo.com\/(.*\/)?([\d]+)(.*)?/,params:{autoplay:1,hd:1,show_title:1,show_byline:1,show_portrait:0,fullscreen:1},paramPlace:3,type:"iframe",url:"//player.vimeo.com/video/$2"},instagram:{matcher:/(instagr\.am|instagram\.com)\/p\/([a-zA-Z0-9_\-]+)\/?/i,type:"image",url:"//$1/p/$2/media/?size=l"},gmap_place:{matcher:/(maps\.)?google\.([a-z]{2,3}(\.[a-z]{2})?)\/(((maps\/(place\/(.*)\/)?\@(.*),(\d+.?\d+?)z))|(\?ll=))(.*)?/i,type:"iframe",url:function(t){return"//maps.google."+t[2]+"/?ll="+(t[9]?t[9]+"&z="+Math.floor(t[10])+(t[12]?t[12].replace(/^\//,"&"):""):t[12]+"").replace(/\?/,"&")+"&output="+(t[12]&&t[12].indexOf("layer=c")>0?"svembed":"embed")}},gmap_search:{matcher:/(maps\.)?google\.([a-z]{2,3}(\.[a-z]{2})?)\/(maps\/search\/)(.*)/i,type:"iframe",url:function(t){return"//maps.google."+t[2]+"/maps?q="+t[5].replace("query=","q=").replace("api=1","")+"&output=embed"}}},n=function(e,n,o){if(e)return o=o||"","object"===t.type(o)&&(o=t.param(o,!0)),t.each(n,function(t,n){e=e.replace("$"+t,n||"")}),o.length&&(e+=(e.indexOf("?")>0?"&":"?")+o),e};t(document).on("objectNeedsType.fb",function(o,i,a){var s,r,c,l,d,u,f,p=a.src||"",h=!1;s=t.extend(!0,{},e,a.opts.media),t.each(s,function(e,o){if(c=p.match(o.matcher)){if(h=o.type,f=e,u={},o.paramPlace&&c[o.paramPlace]){d=c[o.paramPlace],"?"==d[0]&&(d=d.substring(1)),d=d.split("&");for(var i=0;i1&&("youtube"===n.contentSource||"vimeo"===n.contentSource)&&o.load(n.contentSource)}})}(jQuery),function(t,e,n){"use strict";var o=function(){return t.requestAnimationFrame||t.webkitRequestAnimationFrame||t.mozRequestAnimationFrame||t.oRequestAnimationFrame||function(e){return t.setTimeout(e,1e3/60)}}(),i=function(){return t.cancelAnimationFrame||t.webkitCancelAnimationFrame||t.mozCancelAnimationFrame||t.oCancelAnimationFrame||function(e){t.clearTimeout(e)}}(),a=function(e){var n=[];e=e.originalEvent||e||t.e,e=e.touches&&e.touches.length?e.touches:e.changedTouches&&e.changedTouches.length?e.changedTouches:[e];for(var o in e)e[o].pageX?n.push({x:e[o].pageX,y:e[o].pageY}):e[o].clientX&&n.push({x:e[o].clientX,y:e[o].clientY});return n},s=function(t,e,n){return e&&t?"x"===n?t.x-e.x:"y"===n?t.y-e.y:Math.sqrt(Math.pow(t.x-e.x,2)+Math.pow(t.y-e.y,2)):0},r=function(t){if(t.is('a,area,button,[role="button"],input,label,select,summary,textarea,video,audio,iframe')||n.isFunction(t.get(0).onclick)||t.data("selectable"))return!0;for(var e=0,o=t[0].attributes,i=o.length;ee.clientHeight,a=("scroll"===o||"auto"===o)&&e.scrollWidth>e.clientWidth;return i||a},l=function(t){for(var e=!1;;){if(e=c(t.get(0)))break;if(t=t.parent(),!t.length||t.hasClass("fancybox-stage")||t.is("body"))break}return e},d=function(t){var e=this;e.instance=t,e.$bg=t.$refs.bg,e.$stage=t.$refs.stage,e.$container=t.$refs.container,e.destroy(),e.$container.on("touchstart.fb.touch mousedown.fb.touch",n.proxy(e,"ontouchstart"))};d.prototype.destroy=function(){var t=this;t.$container.off(".fb.touch"),n(e).off(".fb.touch"),t.requestId&&(i(t.requestId),t.requestId=null),t.tapped&&(clearTimeout(t.tapped),t.tapped=null)},d.prototype.ontouchstart=function(o){var i=this,c=n(o.target),d=i.instance,u=d.current,f=u.$slide,p=u.$content,h="touchstart"==o.type;if(h&&i.$container.off("mousedown.fb.touch"),(!o.originalEvent||2!=o.originalEvent.button)&&f.length&&c.length&&!r(c)&&!r(c.parent())&&(c.is("img")||!(o.originalEvent.clientX>c[0].clientWidth+c.offset().left))){if(!u||d.isAnimating||u.$slide.hasClass("fancybox-animated"))return o.stopPropagation(),void o.preventDefault();i.realPoints=i.startPoints=a(o),i.startPoints.length&&(u.touch&&o.stopPropagation(),i.startEvent=o,i.canTap=!0,i.$target=c,i.$content=p,i.opts=u.opts.touch,i.isPanning=!1,i.isSwiping=!1,i.isZooming=!1,i.isScrolling=!1,i.canPan=d.canPan(),i.startTime=(new Date).getTime(),i.distanceX=i.distanceY=i.distance=0,i.canvasWidth=Math.round(f[0].clientWidth),i.canvasHeight=Math.round(f[0].clientHeight),i.contentLastPos=null,i.contentStartPos=n.fancybox.getTranslate(i.$content)||{top:0,left:0},i.sliderStartPos=n.fancybox.getTranslate(f),i.stagePos=n.fancybox.getTranslate(d.$refs.stage),i.sliderStartPos.top-=i.stagePos.top,i.sliderStartPos.left-=i.stagePos.left,i.contentStartPos.top-=i.stagePos.top,i.contentStartPos.left-=i.stagePos.left,n(e).off(".fb.touch").on(h?"touchend.fb.touch touchcancel.fb.touch":"mouseup.fb.touch mouseleave.fb.touch",n.proxy(i,"ontouchend")).on(h?"touchmove.fb.touch":"mousemove.fb.touch",n.proxy(i,"ontouchmove")),n.fancybox.isMobile&&e.addEventListener("scroll",i.onscroll,!0),((i.opts||i.canPan)&&(c.is(i.$stage)||i.$stage.find(c).length)||(c.is(".fancybox-image")&&o.preventDefault(),n.fancybox.isMobile&&c.parents(".fancybox-caption").length))&&(i.isScrollable=l(c)||l(c.parent()),n.fancybox.isMobile&&i.isScrollable||o.preventDefault(),(1===i.startPoints.length||u.hasError)&&(i.canPan?(n.fancybox.stop(i.$content),i.isPanning=!0):i.isSwiping=!0,i.$container.addClass("fancybox-is-grabbing")),2===i.startPoints.length&&"image"===u.type&&(u.isLoaded||u.$ghost)&&(i.canTap=!1,i.isSwiping=!1,i.isPanning=!1,i.isZooming=!0,n.fancybox.stop(i.$content),i.centerPointStartX=.5*(i.startPoints[0].x+i.startPoints[1].x)-n(t).scrollLeft(),i.centerPointStartY=.5*(i.startPoints[0].y+i.startPoints[1].y)-n(t).scrollTop(),i.percentageOfImageAtPinchPointX=(i.centerPointStartX-i.contentStartPos.left)/i.contentStartPos.width,i.percentageOfImageAtPinchPointY=(i.centerPointStartY-i.contentStartPos.top)/i.contentStartPos.height,i.startDistanceBetweenFingers=s(i.startPoints[0],i.startPoints[1]))))}},d.prototype.onscroll=function(t){var n=this;n.isScrolling=!0,e.removeEventListener("scroll",n.onscroll,!0)},d.prototype.ontouchmove=function(t){var e=this;return void 0!==t.originalEvent.buttons&&0===t.originalEvent.buttons?void e.ontouchend(t):e.isScrolling?void(e.canTap=!1):(e.newPoints=a(t),void((e.opts||e.canPan)&&e.newPoints.length&&e.newPoints.length&&(e.isSwiping&&!0===e.isSwiping||t.preventDefault(),e.distanceX=s(e.newPoints[0],e.startPoints[0],"x"),e.distanceY=s(e.newPoints[0],e.startPoints[0],"y"),e.distance=s(e.newPoints[0],e.startPoints[0]),e.distance>0&&(e.isSwiping?e.onSwipe(t):e.isPanning?e.onPan():e.isZooming&&e.onZoom()))))},d.prototype.onSwipe=function(e){var a,s=this,r=s.instance,c=s.isSwiping,l=s.sliderStartPos.left||0;if(!0!==c)"x"==c&&(s.distanceX>0&&(s.instance.group.length<2||0===s.instance.current.index&&!s.instance.current.opts.loop)?l+=Math.pow(s.distanceX,.8):s.distanceX<0&&(s.instance.group.length<2||s.instance.current.index===s.instance.group.length-1&&!s.instance.current.opts.loop)?l-=Math.pow(-s.distanceX,.8):l+=s.distanceX),s.sliderLastPos={top:"x"==c?0:s.sliderStartPos.top+s.distanceY,left:l},s.requestId&&(i(s.requestId),s.requestId=null),s.requestId=o(function(){s.sliderLastPos&&(n.each(s.instance.slides,function(t,e){var o=e.pos-s.instance.currPos;n.fancybox.setTranslate(e.$slide,{top:s.sliderLastPos.top,left:s.sliderLastPos.left+o*s.canvasWidth+o*e.opts.gutter})}),s.$container.addClass("fancybox-is-sliding"))});else if(Math.abs(s.distance)>10){if(s.canTap=!1,r.group.length<2&&s.opts.vertical?s.isSwiping="y":r.isDragging||!1===s.opts.vertical||"auto"===s.opts.vertical&&n(t).width()>800?s.isSwiping="x":(a=Math.abs(180*Math.atan2(s.distanceY,s.distanceX)/Math.PI),s.isSwiping=a>45&&a<135?"y":"x"),"y"===s.isSwiping&&n.fancybox.isMobile&&s.isScrollable)return void(s.isScrolling=!0);r.isDragging=s.isSwiping,s.startPoints=s.newPoints,n.each(r.slides,function(t,e){var o,i;n.fancybox.stop(e.$slide),o=n.fancybox.getTranslate(e.$slide),i=n.fancybox.getTranslate(r.$refs.stage),e.$slide.css({transform:"",opacity:"","transition-duration":""}).removeClass("fancybox-animated").removeClass(function(t,e){return(e.match(/(^|\s)fancybox-fx-\S+/g)||[]).join(" ")}),e.pos===r.current.pos&&(s.sliderStartPos.top=o.top-i.top,s.sliderStartPos.left=o.left-i.left),n.fancybox.setTranslate(e.$slide,{top:o.top-i.top,left:o.left-i.left})}),r.SlideShow&&r.SlideShow.isActive&&r.SlideShow.stop()}},d.prototype.onPan=function(){var t=this;if(s(t.newPoints[0],t.realPoints[0])<(n.fancybox.isMobile?10:5))return void(t.startPoints=t.newPoints);t.canTap=!1,t.contentLastPos=t.limitMovement(),t.requestId&&i(t.requestId),t.requestId=o(function(){n.fancybox.setTranslate(t.$content,t.contentLastPos)})},d.prototype.limitMovement=function(){var t,e,n,o,i,a,s=this,r=s.canvasWidth,c=s.canvasHeight,l=s.distanceX,d=s.distanceY,u=s.contentStartPos,f=u.left,p=u.top,h=u.width,g=u.height;return i=h>r?f+l:f,a=p+d,t=Math.max(0,.5*r-.5*h),e=Math.max(0,.5*c-.5*g),n=Math.min(r-h,.5*r-.5*h),o=Math.min(c-g,.5*c-.5*g),l>0&&i>t&&(i=t-1+Math.pow(-t+f+l,.8)||0),l<0&&i0&&a>e&&(a=e-1+Math.pow(-e+p+d,.8)||0),d<0&&aa?(t=t>0?0:t,t=ts?(e=e>0?0:e,e=e1&&(o.dMs>130&&s>10||s>50);o.sliderLastPos=null,"y"==t&&!e&&Math.abs(o.distanceY)>50?(n.fancybox.animate(o.instance.current.$slide,{top:o.sliderStartPos.top+o.distanceY+150*o.velocityY,opacity:0},200),i=o.instance.close(!0,250)):r&&o.distanceX>0?i=o.instance.previous(300):r&&o.distanceX<0&&(i=o.instance.next(300)),!1!==i||"x"!=t&&"y"!=t||o.instance.centerSlide(200),o.$container.removeClass("fancybox-is-sliding")},d.prototype.endPanning=function(){var t,e,o,i=this;i.contentLastPos&&(!1===i.opts.momentum||i.dMs>350?(t=i.contentLastPos.left,e=i.contentLastPos.top):(t=i.contentLastPos.left+500*i.velocityX,e=i.contentLastPos.top+500*i.velocityY),o=i.limitPosition(t,e,i.contentStartPos.width,i.contentStartPos.height),o.width=i.contentStartPos.width,o.height=i.contentStartPos.height,n.fancybox.animate(i.$content,o,366))},d.prototype.endZooming=function(){var t,e,o,i,a=this,s=a.instance.current,r=a.newWidth,c=a.newHeight;a.contentLastPos&&(t=a.contentLastPos.left,e=a.contentLastPos.top,i={top:e,left:t,width:r,height:c,scaleX:1,scaleY:1},n.fancybox.setTranslate(a.$content,i),rs.width||c>s.height?a.instance.scaleToActual(a.centerPointStartX,a.centerPointStartY,150):(o=a.limitPosition(t,e,r,c),n.fancybox.animate(a.$content,o,150)))},d.prototype.onTap=function(e){var o,i=this,s=n(e.target),r=i.instance,c=r.current,l=e&&a(e)||i.startPoints,d=l[0]?l[0].x-n(t).scrollLeft()-i.stagePos.left:0,u=l[0]?l[0].y-n(t).scrollTop()-i.stagePos.top:0,f=function(t){var o=c.opts[t];if(n.isFunction(o)&&(o=o.apply(r,[c,e])),o)switch(o){case"close":r.close(i.startEvent);break;case"toggleControls":r.toggleControls();break;case"next":r.next();break;case"nextOrClose":r.group.length>1?r.next():r.close(i.startEvent);break;case"zoom":"image"==c.type&&(c.isLoaded||c.$ghost)&&(r.canPan()?r.scaleToFit():r.isScaledDown()?r.scaleToActual(d,u):r.group.length<2&&r.close(i.startEvent))}};if((!e.originalEvent||2!=e.originalEvent.button)&&(s.is("img")||!(d>s[0].clientWidth+s.offset().left))){if(s.is(".fancybox-bg,.fancybox-inner,.fancybox-outer,.fancybox-container"))o="Outside";else if(s.is(".fancybox-slide"))o="Slide";else{if(!r.current.$content||!r.current.$content.find(s).addBack().filter(s).length)return;o="Content"}if(i.tapped){if(clearTimeout(i.tapped),i.tapped=null,Math.abs(d-i.tapX)>50||Math.abs(u-i.tapY)>50)return this;f("dblclick"+o)}else i.tapX=d,i.tapY=u,c.opts["dblclick"+o]&&c.opts["dblclick"+o]!==c.opts["click"+o]?i.tapped=setTimeout(function(){i.tapped=null,r.isAnimating||f("click"+o)},500):f("click"+o);return this}},n(e).on("onActivate.fb",function(t,e){e&&!e.Guestures&&(e.Guestures=new d(e))}).on("beforeClose.fb",function(t,e){e&&e.Guestures&&e.Guestures.destroy()})}(window,document,jQuery),function(t,e){"use strict";e.extend(!0,e.fancybox.defaults,{btnTpl:{slideShow:''},slideShow:{autoStart:!1,speed:3e3,progress:!0}});var n=function(t){this.instance=t,this.init()};e.extend(n.prototype,{timer:null,isActive:!1,$button:null,init:function(){var t=this,n=t.instance,o=n.group[n.currIndex].opts.slideShow;t.$button=n.$refs.toolbar.find("[data-fancybox-play]").on("click",function(){t.toggle()}),n.group.length<2||!o?t.$button.hide():o.progress&&(t.$progress=e('
').appendTo(n.$refs.inner))},set:function(t){var n=this,o=n.instance,i=o.current;i&&(!0===t||i.opts.loop||o.currIndex'},fullScreen:{autoStart:!1}}),e(t).on(n.fullscreenchange,function(){var t=o.isFullscreen(),n=e.fancybox.getInstance();n&&(n.current&&"image"===n.current.type&&n.isAnimating&&(n.isAnimating=!1,n.update(!0,!0,0),n.isComplete||n.complete()),n.trigger("onFullscreenChange",t),n.$refs.container.toggleClass("fancybox-is-fullscreen",t),n.$refs.toolbar.find("[data-fancybox-fullscreen]").toggleClass("fancybox-button--fsenter",!t).toggleClass("fancybox-button--fsexit",t))})}e(t).on({"onInit.fb":function(t,e){var i;if(!n)return void e.$refs.toolbar.find("[data-fancybox-fullscreen]").remove();e&&e.group[e.currIndex].opts.fullScreen?(i=e.$refs.container,i.on("click.fb-fullscreen","[data-fancybox-fullscreen]",function(t){t.stopPropagation(),t.preventDefault(),o.toggle()}),e.opts.fullScreen&&!0===e.opts.fullScreen.autoStart&&o.request(),e.FullScreen=o):e&&e.$refs.toolbar.find("[data-fancybox-fullscreen]").hide()},"afterKeydown.fb":function(t,e,n,o,i){e&&e.FullScreen&&70===i&&(o.preventDefault(),e.FullScreen.toggle())},"beforeClose.fb":function(t,e){e&&e.FullScreen&&e.$refs.container.hasClass("fancybox-is-fullscreen")&&o.exit()}})}(document,jQuery),function(t,e){"use strict";var n="fancybox-thumbs";e.fancybox.defaults=e.extend(!0,{btnTpl:{thumbs:''},thumbs:{autoStart:!1,hideOnClose:!0,parentEl:".fancybox-container",axis:"y"}},e.fancybox.defaults);var o=function(t){this.init(t)};e.extend(o.prototype,{$button:null,$grid:null,$list:null,isVisible:!1,isActive:!1,init:function(t){var e=this,n=t.group,o=0;e.instance=t,e.opts=n[t.currIndex].opts.thumbs,t.Thumbs=e,e.$button=t.$refs.toolbar.find("[data-fancybox-thumbs]");for(var i=0,a=n.length;i1));i++);o>1&&e.opts?(e.$button.removeAttr("style").on("click",function(){e.toggle()}),e.isActive=!0):e.$button.hide()},create:function(){var t,o=this,i=o.instance,a=o.opts.parentEl,s=[];o.$grid||(o.$grid=e('
').appendTo(i.$refs.container.find(a).addBack().filter(a)),o.$grid.on("click","a",function(){i.jumpTo(e(this).attr("data-index"))})),o.$list||(o.$list=e('
').appendTo(o.$grid)),e.each(i.group,function(e,n){t=n.thumb,t||"image"!==n.type||(t=n.src),s.push('")}),o.$list[0].innerHTML=s.join(""),"x"===o.opts.axis&&o.$list.width(parseInt(o.$grid.css("padding-right"),10)+i.group.length*o.$list.children().eq(0).outerWidth(!0))},focus:function(t){var e,n,o=this,i=o.$list,a=o.$grid;o.instance.current&&(e=i.children().removeClass("fancybox-thumbs-active").filter('[data-index="'+o.instance.current.index+'"]').addClass("fancybox-thumbs-active"),n=e.position(),"y"===o.opts.axis&&(n.top<0||n.top>i.height()-e.outerHeight())?i.stop().animate({scrollTop:i.scrollTop()+n.top},t):"x"===o.opts.axis&&(n.lefta.scrollLeft()+(a.width()-e.outerWidth()))&&i.parent().stop().animate({scrollLeft:n.left},t))},update:function(){var t=this;t.instance.$refs.container.toggleClass("fancybox-show-thumbs",this.isVisible),t.isVisible?(t.$grid||t.create(),t.instance.trigger("onThumbsShow"),t.focus(0)):t.$grid&&t.instance.trigger("onThumbsHide"),t.instance.update()},hide:function(){this.isVisible=!1,this.update()},show:function(){this.isVisible=!0,this.update()},toggle:function(){this.isVisible=!this.isVisible,this.update()}}),e(t).on({"onInit.fb":function(t,e){var n;e&&!e.Thumbs&&(n=new o(e),n.isActive&&!0===n.opts.autoStart&&n.show())},"beforeShow.fb":function(t,e,n,o){var i=e&&e.Thumbs;i&&i.isVisible&&i.focus(o?0:250)},"afterKeydown.fb":function(t,e,n,o,i){var a=e&&e.Thumbs;a&&a.isActive&&71===i&&(o.preventDefault(),a.toggle())},"beforeClose.fb":function(t,e){var n=e&&e.Thumbs;n&&n.isVisible&&!1!==n.opts.hideOnClose&&n.$grid.hide()}})}(document,jQuery),function(t,e){"use strict";function n(t){var e={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};return String(t).replace(/[&<>"'`=\/]/g,function(t){return e[t]})}e.extend(!0,e.fancybox.defaults,{btnTpl:{share:''},share:{url:function(t,e){return!t.currentHash&&"inline"!==e.type&&"html"!==e.type&&(e.origSrc||e.src)||window.location}, +tpl:''}}),e(t).on("click","[data-fancybox-share]",function(){var t,o,i=e.fancybox.getInstance(),a=i.current||null;a&&("function"===e.type(a.opts.share.url)&&(t=a.opts.share.url.apply(a,[i,a])),o=a.opts.share.tpl.replace(/\{\{media\}\}/g,"image"===a.type?encodeURIComponent(a.src):"").replace(/\{\{url\}\}/g,encodeURIComponent(t)).replace(/\{\{url_raw\}\}/g,n(t)).replace(/\{\{descr\}\}/g,i.$caption?encodeURIComponent(i.$caption.text()):""),e.fancybox.open({src:i.translate(i,o),type:"html",opts:{touch:!1,animationEffect:!1,afterLoad:function(t,e){i.$refs.container.one("beforeClose.fb",function(){t.close(null,0)}),e.$content.find(".fancybox-share__button").click(function(){return window.open(this.href,"Share","width=550, height=450"),!1})},mobile:{autoFocus:!1}}}))})}(document,jQuery),function(t,e,n){"use strict";function o(){var e=t.location.hash.substr(1),n=e.split("-"),o=n.length>1&&/^\+?\d+$/.test(n[n.length-1])?parseInt(n.pop(-1),10)||1:1,i=n.join("-");return{hash:e,index:o<1?1:o,gallery:i}}function i(t){""!==t.gallery&&n("[data-fancybox='"+n.escapeSelector(t.gallery)+"']").eq(t.index-1).focus().trigger("click.fb-start")}function a(t){var e,n;return!!t&&(e=t.current?t.current.opts:t.opts,""!==(n=e.hash||(e.$orig?e.$orig.data("fancybox")||e.$orig.data("fancybox-trigger"):""))&&n)}n.escapeSelector||(n.escapeSelector=function(t){return(t+"").replace(/([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g,function(t,e){return e?"\0"===t?"�":t.slice(0,-1)+"\\"+t.charCodeAt(t.length-1).toString(16)+" ":"\\"+t})}),n(function(){!1!==n.fancybox.defaults.hash&&(n(e).on({"onInit.fb":function(t,e){var n,i;!1!==e.group[e.currIndex].opts.hash&&(n=o(),(i=a(e))&&n.gallery&&i==n.gallery&&(e.currIndex=n.index-1))},"beforeShow.fb":function(n,o,i,s){var r;i&&!1!==i.opts.hash&&(r=a(o))&&(o.currentHash=r+(o.group.length>1?"-"+(i.index+1):""),t.location.hash!=="#"+o.currentHash&&(s&&!o.origHash&&(o.origHash=t.location.hash),o.hashTimer&&clearTimeout(o.hashTimer),o.hashTimer=setTimeout(function(){"replaceState"in t.history?(t.history[s?"pushState":"replaceState"]({},e.title,t.location.pathname+t.location.search+"#"+o.currentHash),s&&(o.hasCreatedHistory=!0)):t.location.hash=o.currentHash,o.hashTimer=null},300)))},"beforeClose.fb":function(n,o,i){i&&!1!==i.opts.hash&&(clearTimeout(o.hashTimer),o.currentHash&&o.hasCreatedHistory?t.history.back():o.currentHash&&("replaceState"in t.history?t.history.replaceState({},e.title,t.location.pathname+t.location.search+(o.origHash||"")):t.location.hash=o.origHash),o.currentHash=null)}}),n(t).on("hashchange.fb",function(){var t=o(),e=null;n.each(n(".fancybox-container").get().reverse(),function(t,o){var i=n(o).data("FancyBox");if(i&&i.currentHash)return e=i,!1}),e?e.currentHash===t.gallery+"-"+t.index||1===t.index&&e.currentHash==t.gallery||(e.currentHash=null,e.close()):""!==t.gallery&&i(t)}),setTimeout(function(){n.fancybox.getInstance()||i(o())},50))})}(window,document,jQuery),function(t,e){"use strict";var n=(new Date).getTime();e(t).on({"onInit.fb":function(t,e,o){e.$refs.stage.on("mousewheel DOMMouseScroll wheel MozMousePixelScroll",function(t){var o=e.current,i=(new Date).getTime();e.group.length<2||!1===o.opts.wheel||"auto"===o.opts.wheel&&"image"!==o.type||(t.preventDefault(),t.stopPropagation(),o.$slide.hasClass("fancybox-animated")||(t=t.originalEvent||t,i-n<250||(n=i,e[(-t.deltaY||-t.deltaX||t.wheelDelta||-t.detail)<0?"next":"previous"]())))})}})}(document,jQuery); \ No newline at end of file diff --git a/dist/github-actions-sample-eslint-in-pull-request/index.html b/dist/github-actions-sample-eslint-in-pull-request/index.html new file mode 100644 index 0000000..878e179 --- /dev/null +++ b/dist/github-actions-sample-eslint-in-pull-request/index.html @@ -0,0 +1,417 @@ + + + + + + + 7. GitHub Actions - 在pull request中执行eslint检测的工作流例子 | TaoLiuJun's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ +
+ + +
+ + +

+ 7. GitHub Actions - 在pull request中执行eslint检测的工作流例子 +

+ + +
+ +
+ +

原文链接:https://github.com/taoliujun/blog/issues/36

+ + +

一个在pull request发起的时候执行eslint检测的workflow,点此查看完整代码,它实现的功能如下:

+
    +
  • 在pull request创建、更新的时候执行。
  • +
  • 先回复一个评论,告诉用户正在运行。
  • +
  • 初始化仓库,并安装依赖,产生依赖缓存。
  • +
  • 运行eslint增量检查。
  • +
  • 运行typescript检查。
  • +
  • 运行jest检查。
  • +
  • 更新之前的评论,回复检查的结果。
  • +
+

运行截图:

+

Alt text

+

为避免歧义,涉及到github action的术语都是英文的。术语介绍如下:

+
    +
  • workflow,工作流,可以理解为yml文件。
  • +
  • jobs,工作,一个workflow可以包含多个job,并行执行。
  • +
  • steps,作业,一个job可以包含多个step,串行执行。
  • +
  • action,操作,作业中具体的执行。
  • +
+

步骤

+ + +

初始化workflow

在项目中新建文件.github/workflows/check-pull-request.yml,内容如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
name: test check pull request
run-name: 'check pull request #${{ github.event.pull_request.number }}'
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
replyChecking:
runs-on: ubuntu-latest
steps:
- run: echo 'replyChecking'

init:
runs-on: ubuntu-latest
steps:
- run: echo 'init'

eslint:
runs-on: ubuntu-latest
needs: [init]
steps:
- run: echo 'eslint'

typescript:
runs-on: ubuntu-latest
needs: [init]
steps:
- run: echo 'typescript'

unitTest:
runs-on: ubuntu-latest
needs: [init]
steps:
- run: echo 'unitTest'

replyResult:
runs-on: ubuntu-latest
needs: [replyChecking, eslint, typescript, unitTest]
steps:
- run: echo 'replyResult'
+ +

name和run-name

给workflow命名为check pull request,它会出现在Actions页面的左侧菜单中。运行实例名为check pull request #44,出现在右侧的运行列表中。如图:

+

+

run-name中的${{ github.event.pull_request.number }}是workflow的上下文,这里读取了上下文中的pr编号。

+

on

on指定了workflow的触发条件,这里配置了在pr创建、同步、重新打开的时候,触发该workflow。

+

jobs

按照设想,需要定义几个job,分别是:

+
    +
  • replyChecking:回复用户正在检查中
  • +
  • init:初始化仓库,缓存依赖项
  • +
  • eslint:运行eslint检查
  • +
  • typescript:运行typescript检查
  • +
  • unitTest:运行单元测试
  • +
  • replyResult:回复用户检查结果
  • +
+

jobs是并行运行的,聪明如你肯定发现了,eslint、typescript、unitTest这三个job会涉及到安装npm依赖,所以它们最好在init后执行,确保依赖已经缓存了。

+

其次,replyResult肯定要拿到eslint等job的结果才能执行,所以使用了needs管理它们的执行依赖关系。

+

runs-on

每个job都运行在独立的容器中,github官方提供了windows、macos、linux多种容器,这里使用了ubuntu容器。

+

测试

发起一个pr,看到Actions页面出现了新的运行实例,点击进去,可以看到各个job的运行情况和依赖关系:

+

+ + +

replyChecking

在进行eslint检测之前,先在pr里回复checking,并且带上拽酷炫的话。将replyChecking改成如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
replyChecking:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}
- name: Get date time
id: getDateTime
run: echo "result=$(TZ=Asia/Shanghai date)" >> "$GITHUB_OUTPUT"
- name: Create or update a comment
uses: ./.github/actions/unique-comment
with:
uniqueIdentifier: ${{ github.workflow }}
body: |
**Checking...**

---

Commented by Action [${{github.workflow}}](${{github.event.repository.html_url}}/actions/runs/${{github.run_id}}), last updated on ${{steps.getDateTime.outputs.result}}.
+ +

steps每一步里nameid是可选的,name在Actions详情页面里会显示,更直观的看到step的名称,推荐写上。

+

Checkout

uses表示使用一个action,名为actions/checkout@v4,它用来拉取仓库。

+
+

同其他编程语言一样,重复的action可以封装起来。action市场提供了很多。

+
+

with属性指定了该action的输入参数,每个action的参数不尽相同。

+

ref参数表示要拉取的分支,${{github.head_ref}}也是一个上下文,表示当前pr的源分支。

+

Get Date time

这step还写了id,表示该step在该job中的唯一标识,为什么要写呢?是为了下一步step能根据id读取到它的output

+
+

output是workflow中非常重要的概念,它用于在step之间、job之间分享简单的数据。

+
+

run就是在容器中跑一个命令,这里跑了一个unix bash命令,将当前时间写入到$GITHUB_OUTPUT中,键名为result

+
+

$GITHUB_OUTPUT是workflow注入到容器中的一个路径,用于存放output。

+
+

Create or update a comment

uses使用了本地的action,这个action用于创建或更新一个唯一回复,下一节说。

+
+

有时候,官方或市场的action并不能满足你的需要,就得自己写一个了。

+
+

同理,该action也有with属性,uniqueIdentifier是回复评论的唯一标识,body是回复的内容,内容使用了markdown语法,里面还涉及到上下文不一一细讲了。只说${{steps.getDateTime.outputs.result}}这个上下文表示获取getDateTime这个step中,键名为result的值。

+

如果你不需要在内容里插入时间,那么上面的Get Date time就可以省略了。

+

测试

因为我已经有完整的代码了,所以运行后,pr中会有一个回复,如图:

+

+ + +

./.github/actions/unique-comment

这是一个封装的javascript action,用于对issue创建、更新唯一评论。

+

目录结构

创建目录./.github/actions/unique-comment,最终目录结构如下:

+
1
2
3
4
5
6
7
8
9
10
.
├── action.yml
├── config
│   └── webpack.config.js
├── dist
│   ├── index.js
│   └── index.js.LICENSE.txt
├── package.json
└── src
└── index.js
+ +

action.yml

这是action的配置文件,必须存在,内容如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
name: unique-comment
description: create or update a unique comment

runs:
using: 'node20'
main: './dist/index.js'

inputs:
token:
description: 'GitHub token'
required: false
default: ${{ github.token }}
owner:
description: 'Repository owner'
required: false
default: ${{ github.event.repository.owner.login }}
repo:
description: 'Repository name'
required: false
default: ${{ github.event.repository.name }}
issue_number:
description: 'Issue number'
required: false
default: ${{ github.event.number }}
body:
description: 'Comment body'
required: false
uniqueIdentifier:
description: 'Unique identifier for comment'
required: false
default: 'unique-comment'
+ +

大部分属性不一一细讲了,都是简单的英文望文生义即可。

+

runs表示运行在node20环境下,入口文件为./dist/index.js

+

inputs表示接受的参数,也就是之前提到的with属性里要输入的参数。用required表示是否必须传入,default表示默认值。

+

src/index.js

为什么入口文件是dist/index.js,而不是src/index.js呢?因为要引用一些github官方提供的快捷操作github REST API的js包去操作issue评论(pull request也是一种issue),最终打包后的文件才能在工作流中稳妥的运行。所以,写好src/index.js,再打包就行。

+

该文件代码如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const core = require('@actions/core');
const github = require('@actions/github');

const main = async () => {
const token = core.getInput('token');
const owner = core.getInput('owner');
const repo = core.getInput('repo');
const issueNumber = core.getInput('issue_number');
const uniqueIdentifier = `[^uniqueIdentifier]: ${core.getInput('uniqueIdentifier')}`;
const body = `${core.getInput('body')}\n\n${uniqueIdentifier}`;

core.debug(`uniqueIdentifier is ${uniqueIdentifier}`);

const octokit = github.getOctokit(token);

const comments = await octokit.rest.issues.listComments({
owner,
repo,
issue_number: issueNumber,
});

const botComment = comments.data.find((v) => v.body.includes(uniqueIdentifier));

if (botComment) {
core.info('update comment successfully.');
await octokit.rest.issues.updateComment({
owner,
repo,
comment_id: botComment.id,
body,
});
} else {
core.info('create comment successfully.');
await octokit.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body,
});
}
};

try {
main();
} catch (err) {
core.setFailed(err.message);
}
+ +

@actions/core@actions/github是github官方提供的js包,前者可以方便的读取入参等,后者可以方便的操作github REST API。

+

main函数的代码就是原生javascript,不一一解释了,主要通过uniqueIdentifier来判断是否发布过评论,如果是,就更新评论,否则就创建评论。

+
+

markdown语法[^uniqueIdentifier]表示脚注,不会被渲染。

+
+

core.setFailed(err.message);表示抛出退出代码。

+

config/webpack.config.js

打包用的,配置简单可用即可:

+
1
2
3
4
5
6
7
8
9
module.exports = {
mode: 'production',
target: 'node20',
entry: './src/index.js',
output: {
filename: 'index.js',
clean: true,
},
};
+ +

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "unique-comment",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "webpack --config ./config/webpack.config.js"
},
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/github": "^6.0.0"
},
"devDependencies": {
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4"
}
}
+ +

没啥好说的,列出了依赖项。和一个打包脚本。

+

测试

修改了src/index.jsbuild,然后push到github仓库。

+

记得将dist目录也提交到github仓库。

+ + +

init

现在,开始搞正经的了。

+

先初始化项目,这个job的目的仅仅是为了缓存pnpm依赖项,如果你的项目的依赖项不经常更新,可以省略这个job,后续也不要needs这个job。

+

将init改成如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
init:
runs-on: ubuntu-latest
steps:
- name: Init repo
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}

- name: Init pnpm
uses: pnpm/action-setup@v2
with:
version: 8

- name: Init node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'

- name: Install dependencies
run: pnpm install
+ +

相信经过对之前的job的了解,这里的配置就看起来很简单了。

+

Init pnpm

使用第三方action,安装pnpm@^8。

+

Init node

cache: 'pnpm'指定缓存机制,它内部是利用了workflow的cache机制。

+

Install dependencies

安装依赖项,触发缓存。

+ + +

eslint

将eslint改成如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
eslint:
runs-on: ubuntu-latest
needs: [init]
outputs:
result: ${{ steps.lint.outputs.result }}
steps:
- name: Init repo
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}
fetch-depth: 0

- name: Init pnpm
uses: pnpm/action-setup@v2
with:
version: 8

- name: Init node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'

- name: Install dependencies
run: pnpm install

- name: Run eslint
id: lint
uses: actions/github-script@v7
with:
result-encoding: string
script: |
let output = '';
let outerr = '';
let diffFiles = '';

await exec.exec(
`git diff --name-only origin/${{github.base_ref}}`,
[],
{
// silent: true,
// ignoreReturnCode: true,
listeners: {
stdout: (data) => {
diffFiles += data.toString();
},
},
}
);

const lintFiles = diffFiles.split(`\n`).filter((file) => {
return file.endsWith('.js') || file.endsWith('.ts') || file.endsWith('.tsx')
}).join(' ');

await exec.exec(
// "pnpm run lint --format stylish",
`pnpm eslint ${lintFiles}`,
[],
{
// silent: true,
ignoreReturnCode: true,
listeners: {
stdout: (data) => {
output += data.toString();
},
stderr: (data) => {
outerr += data.toString();
},
},
}
);

if (outerr) {
return `:x: Some command execution errors, non-eslint business errors.`;
}

const errorMatch = output.match(/(\d+) errors?/);
const warnMatch = output.match(/(\d+) warnings?/);

if (errorMatch && errorMatch?.[1] !== '0') {
return `:x: ${errorMatch?.[0]} ${warnMatch?.[0]}`;
}

return `:white_check_mark: ${errorMatch?.[0] || '0 error'} ${warnMatch?.[0] || '0 warning'}`;
+ +

needs

使用needs依赖init,可以使用到pnpm的缓存项,防止install太慢。

+
+

因为eslint、typescript、unitTest都需要pnpm install,所以一个前置的init去缓存pnpm依赖项,可以加快后续的install速度。

+
+

outputs

job里的outputs,可以在依赖它的其他job中访问到。这里使用${{ steps.lint.outputs.result }}去获取该job中lint这个step里的output里的result。

+
+

output有job和step两个维度,注意区分。

+
+

Run eslint

它uses了actions/github-script@v7,这是github官方提供的一个action,可以在with.script里写js代码去执行,同时它会注入一些变量到script中去,见它的官方文档

+
+

对于简单的js代码,可以使用这个action去完成,不用再去写一个js文件。

+
+

result-encoding是指定script返回的数据格式的,默认是json,这指定为string。

+
+

为什么script里return了string,还要指定为string呢?
因为return 'hello'在json encode后是'"hello"',而string encode后为'hello'

+
+

script里是原生的js代码了,里面的exec是该action注入的变量,用来执行shell命令。

+

这段js代码做了两个事情,一是git diff获取pr中改动的文件列表,二是eslint检查这些增量文件,最后返回处理的结果。

+

fetch-depth

Init repo这个step里设置了fetch-depth: 0,不然获取不到完整的git分支,具体看actions/checkout的解释,涉及到git的知识不展开细说了。

+

steps.lint.outputs.result

steps.lint.outputs.result为什么能拿到lint step里的output.result呢?因为actions/github-script这个action内部将script的返回值,设置到$GITHUB_OUTPUT里了,且键名为result

+ + +

typescript

和eslint的配置大同小异,只是改了对检测结果的判断。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
typescript:
runs-on: ubuntu-latest
needs: [init]
outputs:
result: ${{ steps.lint.outputs.result }}
steps:
- name: Init repo
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}

- name: Init pnpm
uses: pnpm/action-setup@v2
with:
version: 8

- name: Init node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'

- name: Install dependencies
run: pnpm install

- name: Run lint
id: lint
uses: actions/github-script@v7
with:
result-encoding: string
script: |
let output = '';
let outerr = '';

await exec.exec(
`pnpm run -r lint:ts`,
[],
{
// silent: true,
ignoreReturnCode: true,
listeners: {
stdout: (data) => {
output += data.toString();
},
stderr: (data) => {
outerr += data.toString();
},
},
}
);

if (outerr) {
return `:x: Some command execution errors, no business errors.`;
}

const errorMatch = output.match(/error TS/g);

if (errorMatch) {
return `:x: ${errorMatch?.length} errors`;
}

return `:white_check_mark: ${'0 error'}`;
+ + +

unitTest

和eslint的配置大同小异,只是改了对检测结果的判断。唯一的区别是jest的检测结果是输出到stderr,见https://github.com/jestjs/jest/issues/5064。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
unitTest:
runs-on: ubuntu-latest
needs: [init]
outputs:
result: ${{ steps.lint.outputs.result }}
steps:
- name: Init repo
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}

- name: Init pnpm
uses: pnpm/action-setup@v2
with:
version: 8

- name: Init node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'

- name: Install dependencies
run: |
pnpm remove @nike/eslint-multi-formatter || true
pnpm remove @nike/svg-packer || true
pnpm install

- name: Run lint
id: lint
uses: actions/github-script@v7
with:
result-encoding: string
script: |
let output = '';
let outerr = '';

await exec.exec(
`pnpm run test`,
[],
{
// silent: true,
ignoreReturnCode: true,
listeners: {
stdout: (data) => {
output += data.toString();
},
stderr: (data) => {
outerr += data.toString();
},
},
}
);

// why use outerr? https://github.com/jestjs/jest/issues/5064

const failMatch = outerr.match(/Test Suites: \d+ failed/);

if (failMatch) {
return `:x: ${failMatch?.[0]}`;
}

const errorMatch = outerr.match(/Jest: "global" coverage threshold for lines \([0-9\.]+%\) not met: [0-9\.]+%/);

if (errorMatch) {
return `:x: ${errorMatch?.[0]}`;
}

return `:white_check_mark: passed`;
+ + +

replyResult

最后,将几个检测的结果进行汇总,回复到pr里就行了。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
replyResult:
runs-on: ubuntu-latest
needs: [replyChecking, eslint, typescript, unitTest]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}
- name: Get date time
id: getDateTime
run: echo "result=$(TZ=Asia/Shanghai date)" >> "$GITHUB_OUTPUT"
- name: Create or update a comment
uses: ./.github/actions/unique-comment
with:
uniqueIdentifier: ${{ github.workflow }}
body: |
## Eslint Check Result

${{needs.eslint.outputs.result}}

## Typescript Check Result

${{needs.typescript.outputs.result}}

## UnitTest Check Result

${{needs.unitTest.outputs.result}}

---

Commented by Action [${{github.workflow}}](${{github.event.repository.html_url}}/actions/runs/${{github.run_id}}), last updated on ${{steps.getDateTime.outputs.result}}.
+ +

和replyChecking差不多,在body里使用${{needs.eslint.outputs.result}}去读取了eslint job的outputs。

+

测试

去发起新的pr,故意提交一个有eslint error的js/ts文件,看看表现吧~

+ + +
+ +
+ + + + + +
+ + +
+ + + +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/dist/index.html b/dist/index.html new file mode 100644 index 0000000..0d1e32c --- /dev/null +++ b/dist/index.html @@ -0,0 +1,476 @@ + + + + + + + TaoLiuJun's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ +
+ +
+ + +
+ + +

+ 7. GitHub Actions - 在pull request中执行eslint检测的工作流例子 +

+ + +
+ +
+ +

原文链接:https://github.com/taoliujun/blog/issues/36

+ + +

一个在pull request发起的时候执行eslint检测的workflow,点此查看完整代码,它实现的功能如下:

+
    +
  • 在pull request创建、更新的时候执行。
  • +
  • 先回复一个评论,告诉用户正在运行。
  • +
  • 初始化仓库,并安装依赖,产生依赖缓存。
  • +
  • 运行eslint增量检查。
  • +
  • 运行typescript检查。
  • +
  • 运行jest检查。
  • +
  • 更新之前的评论,回复检查的结果。
  • +
+

运行截图:

+

Alt text

+

为避免歧义,涉及到github action的术语都是英文的。术语介绍如下:

+
    +
  • workflow,工作流,可以理解为yml文件。
  • +
  • jobs,工作,一个workflow可以包含多个job,并行执行。
  • +
  • steps,作业,一个job可以包含多个step,串行执行。
  • +
  • action,操作,作业中具体的执行。
  • +
+

步骤

+ + +

初始化workflow

在项目中新建文件.github/workflows/check-pull-request.yml,内容如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
name: test check pull request
run-name: 'check pull request #${{ github.event.pull_request.number }}'
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
replyChecking:
runs-on: ubuntu-latest
steps:
- run: echo 'replyChecking'

init:
runs-on: ubuntu-latest
steps:
- run: echo 'init'

eslint:
runs-on: ubuntu-latest
needs: [init]
steps:
- run: echo 'eslint'

typescript:
runs-on: ubuntu-latest
needs: [init]
steps:
- run: echo 'typescript'

unitTest:
runs-on: ubuntu-latest
needs: [init]
steps:
- run: echo 'unitTest'

replyResult:
runs-on: ubuntu-latest
needs: [replyChecking, eslint, typescript, unitTest]
steps:
- run: echo 'replyResult'
+ +

name和run-name

给workflow命名为check pull request,它会出现在Actions页面的左侧菜单中。运行实例名为check pull request #44,出现在右侧的运行列表中。如图:

+

+

run-name中的${{ github.event.pull_request.number }}是workflow的上下文,这里读取了上下文中的pr编号。

+

on

on指定了workflow的触发条件,这里配置了在pr创建、同步、重新打开的时候,触发该workflow。

+

jobs

按照设想,需要定义几个job,分别是:

+
    +
  • replyChecking:回复用户正在检查中
  • +
  • init:初始化仓库,缓存依赖项
  • +
  • eslint:运行eslint检查
  • +
  • typescript:运行typescript检查
  • +
  • unitTest:运行单元测试
  • +
  • replyResult:回复用户检查结果
  • +
+

jobs是并行运行的,聪明如你肯定发现了,eslint、typescript、unitTest这三个job会涉及到安装npm依赖,所以它们最好在init后执行,确保依赖已经缓存了。

+

其次,replyResult肯定要拿到eslint等job的结果才能执行,所以使用了needs管理它们的执行依赖关系。

+

runs-on

每个job都运行在独立的容器中,github官方提供了windows、macos、linux多种容器,这里使用了ubuntu容器。

+

测试

发起一个pr,看到Actions页面出现了新的运行实例,点击进去,可以看到各个job的运行情况和依赖关系:

+

+ + +

replyChecking

在进行eslint检测之前,先在pr里回复checking,并且带上拽酷炫的话。将replyChecking改成如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
replyChecking:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}
- name: Get date time
id: getDateTime
run: echo "result=$(TZ=Asia/Shanghai date)" >> "$GITHUB_OUTPUT"
- name: Create or update a comment
uses: ./.github/actions/unique-comment
with:
uniqueIdentifier: ${{ github.workflow }}
body: |
**Checking...**

---

Commented by Action [${{github.workflow}}](${{github.event.repository.html_url}}/actions/runs/${{github.run_id}}), last updated on ${{steps.getDateTime.outputs.result}}.
+ +

steps每一步里nameid是可选的,name在Actions详情页面里会显示,更直观的看到step的名称,推荐写上。

+

Checkout

uses表示使用一个action,名为actions/checkout@v4,它用来拉取仓库。

+
+

同其他编程语言一样,重复的action可以封装起来。action市场提供了很多。

+
+

with属性指定了该action的输入参数,每个action的参数不尽相同。

+

ref参数表示要拉取的分支,${{github.head_ref}}也是一个上下文,表示当前pr的源分支。

+

Get Date time

这step还写了id,表示该step在该job中的唯一标识,为什么要写呢?是为了下一步step能根据id读取到它的output

+
+

output是workflow中非常重要的概念,它用于在step之间、job之间分享简单的数据。

+
+

run就是在容器中跑一个命令,这里跑了一个unix bash命令,将当前时间写入到$GITHUB_OUTPUT中,键名为result

+
+

$GITHUB_OUTPUT是workflow注入到容器中的一个路径,用于存放output。

+
+

Create or update a comment

uses使用了本地的action,这个action用于创建或更新一个唯一回复,下一节说。

+
+

有时候,官方或市场的action并不能满足你的需要,就得自己写一个了。

+
+

同理,该action也有with属性,uniqueIdentifier是回复评论的唯一标识,body是回复的内容,内容使用了markdown语法,里面还涉及到上下文不一一细讲了。只说${{steps.getDateTime.outputs.result}}这个上下文表示获取getDateTime这个step中,键名为result的值。

+

如果你不需要在内容里插入时间,那么上面的Get Date time就可以省略了。

+

测试

因为我已经有完整的代码了,所以运行后,pr中会有一个回复,如图:

+

+ + +

./.github/actions/unique-comment

这是一个封装的javascript action,用于对issue创建、更新唯一评论。

+

目录结构

创建目录./.github/actions/unique-comment,最终目录结构如下:

+
1
2
3
4
5
6
7
8
9
10
.
├── action.yml
├── config
│   └── webpack.config.js
├── dist
│   ├── index.js
│   └── index.js.LICENSE.txt
├── package.json
└── src
└── index.js
+ +

action.yml

这是action的配置文件,必须存在,内容如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
name: unique-comment
description: create or update a unique comment

runs:
using: 'node20'
main: './dist/index.js'

inputs:
token:
description: 'GitHub token'
required: false
default: ${{ github.token }}
owner:
description: 'Repository owner'
required: false
default: ${{ github.event.repository.owner.login }}
repo:
description: 'Repository name'
required: false
default: ${{ github.event.repository.name }}
issue_number:
description: 'Issue number'
required: false
default: ${{ github.event.number }}
body:
description: 'Comment body'
required: false
uniqueIdentifier:
description: 'Unique identifier for comment'
required: false
default: 'unique-comment'
+ +

大部分属性不一一细讲了,都是简单的英文望文生义即可。

+

runs表示运行在node20环境下,入口文件为./dist/index.js

+

inputs表示接受的参数,也就是之前提到的with属性里要输入的参数。用required表示是否必须传入,default表示默认值。

+

src/index.js

为什么入口文件是dist/index.js,而不是src/index.js呢?因为要引用一些github官方提供的快捷操作github REST API的js包去操作issue评论(pull request也是一种issue),最终打包后的文件才能在工作流中稳妥的运行。所以,写好src/index.js,再打包就行。

+

该文件代码如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const core = require('@actions/core');
const github = require('@actions/github');

const main = async () => {
const token = core.getInput('token');
const owner = core.getInput('owner');
const repo = core.getInput('repo');
const issueNumber = core.getInput('issue_number');
const uniqueIdentifier = `[^uniqueIdentifier]: ${core.getInput('uniqueIdentifier')}`;
const body = `${core.getInput('body')}\n\n${uniqueIdentifier}`;

core.debug(`uniqueIdentifier is ${uniqueIdentifier}`);

const octokit = github.getOctokit(token);

const comments = await octokit.rest.issues.listComments({
owner,
repo,
issue_number: issueNumber,
});

const botComment = comments.data.find((v) => v.body.includes(uniqueIdentifier));

if (botComment) {
core.info('update comment successfully.');
await octokit.rest.issues.updateComment({
owner,
repo,
comment_id: botComment.id,
body,
});
} else {
core.info('create comment successfully.');
await octokit.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body,
});
}
};

try {
main();
} catch (err) {
core.setFailed(err.message);
}
+ +

@actions/core@actions/github是github官方提供的js包,前者可以方便的读取入参等,后者可以方便的操作github REST API。

+

main函数的代码就是原生javascript,不一一解释了,主要通过uniqueIdentifier来判断是否发布过评论,如果是,就更新评论,否则就创建评论。

+
+

markdown语法[^uniqueIdentifier]表示脚注,不会被渲染。

+
+

core.setFailed(err.message);表示抛出退出代码。

+

config/webpack.config.js

打包用的,配置简单可用即可:

+
1
2
3
4
5
6
7
8
9
module.exports = {
mode: 'production',
target: 'node20',
entry: './src/index.js',
output: {
filename: 'index.js',
clean: true,
},
};
+ +

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "unique-comment",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "webpack --config ./config/webpack.config.js"
},
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/github": "^6.0.0"
},
"devDependencies": {
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4"
}
}
+ +

没啥好说的,列出了依赖项。和一个打包脚本。

+

测试

修改了src/index.jsbuild,然后push到github仓库。

+

记得将dist目录也提交到github仓库。

+ + +

init

现在,开始搞正经的了。

+

先初始化项目,这个job的目的仅仅是为了缓存pnpm依赖项,如果你的项目的依赖项不经常更新,可以省略这个job,后续也不要needs这个job。

+

将init改成如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
init:
runs-on: ubuntu-latest
steps:
- name: Init repo
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}

- name: Init pnpm
uses: pnpm/action-setup@v2
with:
version: 8

- name: Init node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'

- name: Install dependencies
run: pnpm install
+ +

相信经过对之前的job的了解,这里的配置就看起来很简单了。

+

Init pnpm

使用第三方action,安装pnpm@^8。

+

Init node

cache: 'pnpm'指定缓存机制,它内部是利用了workflow的cache机制。

+

Install dependencies

安装依赖项,触发缓存。

+ + +

eslint

将eslint改成如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
eslint:
runs-on: ubuntu-latest
needs: [init]
outputs:
result: ${{ steps.lint.outputs.result }}
steps:
- name: Init repo
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}
fetch-depth: 0

- name: Init pnpm
uses: pnpm/action-setup@v2
with:
version: 8

- name: Init node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'

- name: Install dependencies
run: pnpm install

- name: Run eslint
id: lint
uses: actions/github-script@v7
with:
result-encoding: string
script: |
let output = '';
let outerr = '';
let diffFiles = '';

await exec.exec(
`git diff --name-only origin/${{github.base_ref}}`,
[],
{
// silent: true,
// ignoreReturnCode: true,
listeners: {
stdout: (data) => {
diffFiles += data.toString();
},
},
}
);

const lintFiles = diffFiles.split(`\n`).filter((file) => {
return file.endsWith('.js') || file.endsWith('.ts') || file.endsWith('.tsx')
}).join(' ');

await exec.exec(
// "pnpm run lint --format stylish",
`pnpm eslint ${lintFiles}`,
[],
{
// silent: true,
ignoreReturnCode: true,
listeners: {
stdout: (data) => {
output += data.toString();
},
stderr: (data) => {
outerr += data.toString();
},
},
}
);

if (outerr) {
return `:x: Some command execution errors, non-eslint business errors.`;
}

const errorMatch = output.match(/(\d+) errors?/);
const warnMatch = output.match(/(\d+) warnings?/);

if (errorMatch && errorMatch?.[1] !== '0') {
return `:x: ${errorMatch?.[0]} ${warnMatch?.[0]}`;
}

return `:white_check_mark: ${errorMatch?.[0] || '0 error'} ${warnMatch?.[0] || '0 warning'}`;
+ +

needs

使用needs依赖init,可以使用到pnpm的缓存项,防止install太慢。

+
+

因为eslint、typescript、unitTest都需要pnpm install,所以一个前置的init去缓存pnpm依赖项,可以加快后续的install速度。

+
+

outputs

job里的outputs,可以在依赖它的其他job中访问到。这里使用${{ steps.lint.outputs.result }}去获取该job中lint这个step里的output里的result。

+
+

output有job和step两个维度,注意区分。

+
+

Run eslint

它uses了actions/github-script@v7,这是github官方提供的一个action,可以在with.script里写js代码去执行,同时它会注入一些变量到script中去,见它的官方文档

+
+

对于简单的js代码,可以使用这个action去完成,不用再去写一个js文件。

+
+

result-encoding是指定script返回的数据格式的,默认是json,这指定为string。

+
+

为什么script里return了string,还要指定为string呢?
因为return 'hello'在json encode后是'"hello"',而string encode后为'hello'

+
+

script里是原生的js代码了,里面的exec是该action注入的变量,用来执行shell命令。

+

这段js代码做了两个事情,一是git diff获取pr中改动的文件列表,二是eslint检查这些增量文件,最后返回处理的结果。

+

fetch-depth

Init repo这个step里设置了fetch-depth: 0,不然获取不到完整的git分支,具体看actions/checkout的解释,涉及到git的知识不展开细说了。

+

steps.lint.outputs.result

steps.lint.outputs.result为什么能拿到lint step里的output.result呢?因为actions/github-script这个action内部将script的返回值,设置到$GITHUB_OUTPUT里了,且键名为result

+ + +

typescript

和eslint的配置大同小异,只是改了对检测结果的判断。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
typescript:
runs-on: ubuntu-latest
needs: [init]
outputs:
result: ${{ steps.lint.outputs.result }}
steps:
- name: Init repo
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}

- name: Init pnpm
uses: pnpm/action-setup@v2
with:
version: 8

- name: Init node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'

- name: Install dependencies
run: pnpm install

- name: Run lint
id: lint
uses: actions/github-script@v7
with:
result-encoding: string
script: |
let output = '';
let outerr = '';

await exec.exec(
`pnpm run -r lint:ts`,
[],
{
// silent: true,
ignoreReturnCode: true,
listeners: {
stdout: (data) => {
output += data.toString();
},
stderr: (data) => {
outerr += data.toString();
},
},
}
);

if (outerr) {
return `:x: Some command execution errors, no business errors.`;
}

const errorMatch = output.match(/error TS/g);

if (errorMatch) {
return `:x: ${errorMatch?.length} errors`;
}

return `:white_check_mark: ${'0 error'}`;
+ + +

unitTest

和eslint的配置大同小异,只是改了对检测结果的判断。唯一的区别是jest的检测结果是输出到stderr,见https://github.com/jestjs/jest/issues/5064。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
unitTest:
runs-on: ubuntu-latest
needs: [init]
outputs:
result: ${{ steps.lint.outputs.result }}
steps:
- name: Init repo
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}

- name: Init pnpm
uses: pnpm/action-setup@v2
with:
version: 8

- name: Init node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'

- name: Install dependencies
run: |
pnpm remove @nike/eslint-multi-formatter || true
pnpm remove @nike/svg-packer || true
pnpm install

- name: Run lint
id: lint
uses: actions/github-script@v7
with:
result-encoding: string
script: |
let output = '';
let outerr = '';

await exec.exec(
`pnpm run test`,
[],
{
// silent: true,
ignoreReturnCode: true,
listeners: {
stdout: (data) => {
output += data.toString();
},
stderr: (data) => {
outerr += data.toString();
},
},
}
);

// why use outerr? https://github.com/jestjs/jest/issues/5064

const failMatch = outerr.match(/Test Suites: \d+ failed/);

if (failMatch) {
return `:x: ${failMatch?.[0]}`;
}

const errorMatch = outerr.match(/Jest: "global" coverage threshold for lines \([0-9\.]+%\) not met: [0-9\.]+%/);

if (errorMatch) {
return `:x: ${errorMatch?.[0]}`;
}

return `:white_check_mark: passed`;
+ + +

replyResult

最后,将几个检测的结果进行汇总,回复到pr里就行了。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
replyResult:
runs-on: ubuntu-latest
needs: [replyChecking, eslint, typescript, unitTest]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}
- name: Get date time
id: getDateTime
run: echo "result=$(TZ=Asia/Shanghai date)" >> "$GITHUB_OUTPUT"
- name: Create or update a comment
uses: ./.github/actions/unique-comment
with:
uniqueIdentifier: ${{ github.workflow }}
body: |
## Eslint Check Result

${{needs.eslint.outputs.result}}

## Typescript Check Result

${{needs.typescript.outputs.result}}

## UnitTest Check Result

${{needs.unitTest.outputs.result}}

---

Commented by Action [${{github.workflow}}](${{github.event.repository.html_url}}/actions/runs/${{github.run_id}}), last updated on ${{steps.getDateTime.outputs.result}}.
+ +

和replyChecking差不多,在body里使用${{needs.eslint.outputs.result}}去读取了eslint job的outputs。

+

测试

去发起新的pr,故意提交一个有eslint error的js/ts文件,看看表现吧~

+ + +
+ +
+ +
+ + + + +
+ +
+ + +
+ + +

+ React公共状态利器 - Zustand +

+ + +
+ +
+ +

原文链接:https://github.com/taoliujun/blog/issues/35

+ + +

官方文档:https://docs.pmnd.rs/zustand

+

如何使用

Zustand 是一个非常简单粗暴的全局状态管理库,它的使用有多简单呢?如下:

+
1
> pnpm add zustand
+ +
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// useFormStateStore.ts
import { create } from 'zustand';

interface State {
loading: boolean;
disabled: boolean;
setLoadingByAge: (value: number) => void;
}

export const useFormStateStore = create<State>((set) => ({
loading: false,
disabled: false,
setLoadingByAge: (value) => {
set({ loading: value > 10 });
},
}));
+ +
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// app.tsx
import { useState, type FC } from 'react';
import { useFormStateStore } from './useFormStateStore';
import { Button } from '@/components/Button';

const Loading: FC = () => {
const { loading } = useFormStateStore();
return <div>loading: {String(loading)}</div>;
};

const Disabled: FC = () => {
const { disabled } = useFormStateStore();
return <div>disabled: {String(disabled)}</div>;
};

const Main: FC = () => {
const { setLoadingByAge } = useFormStateStore();
const [age, setAge] = useState(0);

return (
<div>
<Loading />
<br />
<Disabled />
<br />
<Button
onClick={() => {
useFormStateStore.setState({
loading: true,
});
}}
>
set loading true
</Button>
<Button
onClick={() => {
useFormStateStore.setState({
loading: false,
});
}}
>
set loading false
</Button>
<Button
onClick={() => {
useFormStateStore.setState({
disabled: true,
});
}}
>
set disabled true
</Button>
<Button
onClick={() => {
useFormStateStore.setState({
disabled: false,
});
}}
>
set disabled false
</Button>
<br />
<input
type="number"
value={age}
onChange={(e) => {
setAge(Number(e.target.value));
}}
/>
<br />
<Button
onClick={() => {
setLoadingByAge(age);
}}
>
set loading by age
</Button>
</div>
);
};

export default Main;
+ +

useFormStateStore.ts中定义了状态,然后在app.tsx中使用,就是这么简单粗暴!这里有几点介绍下:

+
    +
  • 对于简单的状态更新,使用setState方法就可以,它的参数是一个对象,这个对象就是你要更新的状态,它会和之前的状态进行合并,然后返回一个新的状态,从而触发组件更新。

    +
  • +
  • 对于需要通用的逻辑处理的状态更新,参照useFormStateStore.ts中的setLoadingByAge方法,将它作为状态里的一个方法就行了。

    +
  • +
+

Zustand

使用非常简单,API也很少,它的原理是使用了Proxy,所以它的性能非常好。

+

相比Redux

相比Redux,Zustand的代码非常简单明了,不需要使用connectmapStateToPropsmapDispatchToProps这些方法。

+

相比React Context

React Context需要一个Provider包裹组件以传递状态,需要一个useContext使用状态,光从层级上就让人绕起来了。而Zustand只需要一个create方法,就可以使用了,且状态是全局的,不需要传递。

+ + +
+ +
+ +
+ + + + + + +
+ + + +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/dist/js/jquery-3.6.4.min.js b/dist/js/jquery-3.6.4.min.js new file mode 100644 index 0000000..0de648e --- /dev/null +++ b/dist/js/jquery-3.6.4.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.4 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,y=n.hasOwnProperty,a=y.toString,l=a.call(Object),v={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.4",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&v(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!y||!y.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ve(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ye(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ve(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.cssHas=ce(function(){try{return C.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],y=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||y.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||y.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||y.push(".#.+[+~]"),e.querySelectorAll("\\\f"),y.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),d.cssHas||y.push(":has"),y=y.length&&new RegExp(y.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),v=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType&&e.documentElement||e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&v(p,e)?-1:t==C||t.ownerDocument==p&&v(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!y||!y.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),v.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",v.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",v.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),v.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(v.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return B(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=_e(v.pixelPosition,function(e,t){if(t)return t=Be(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return B(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0', + '', + '
', + '', + '', + '', + '', + '
', + '
' + ].join(''); + + var box = $(html); + + $('body').append(box); + } + + $('.article-share-box.on').hide(); + + box.css({ + top: offset.top + 25, + left: offset.left + }).addClass('on'); + }).on('click', '.article-share-box', function(e){ + e.stopPropagation(); + }).on('click', '.article-share-box-input', function(){ + $(this).select(); + }).on('click', '.article-share-box-link', function(e){ + e.preventDefault(); + e.stopPropagation(); + + window.open(this.href, 'article-share-box-window-' + Date.now(), 'width=500,height=450'); + }); + + // Caption + $('.article-entry').each(function(i){ + $(this).find('img').each(function(){ + if ($(this).parent().hasClass('fancybox') || $(this).parent().is('a')) return; + + var alt = this.alt; + + if (alt) $(this).after('' + alt + ''); + + $(this).wrap('') + }); + + $(this).find('.fancybox').each(function(){ + $(this).attr('rel', 'article' + i); + }); + }); + + if ($.fancybox){ + $('.fancybox').fancybox(); + } + + // Mobile nav + var $container = $('#container'), + isMobileNavAnim = false, + mobileNavAnimDuration = 200; + + var startMobileNavAnim = function(){ + isMobileNavAnim = true; + }; + + var stopMobileNavAnim = function(){ + setTimeout(function(){ + isMobileNavAnim = false; + }, mobileNavAnimDuration); + } + + $('#main-nav-toggle').on('click', function(){ + if (isMobileNavAnim) return; + + startMobileNavAnim(); + $container.toggleClass('mobile-nav-on'); + stopMobileNavAnim(); + }); + + $('#wrap').on('click', function(){ + if (isMobileNavAnim || !$container.hasClass('mobile-nav-on')) return; + + $container.removeClass('mobile-nav-on'); + }); +})(jQuery); \ No newline at end of file diff --git a/dist/react-zustand/index.html b/dist/react-zustand/index.html new file mode 100644 index 0000000..3f06139 --- /dev/null +++ b/dist/react-zustand/index.html @@ -0,0 +1,275 @@ + + + + + + + React公共状态利器 - Zustand | TaoLiuJun's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ +
+ + +
+ + +

+ React公共状态利器 - Zustand +

+ + +
+ +
+ +

原文链接:https://github.com/taoliujun/blog/issues/35

+ + +

官方文档:https://docs.pmnd.rs/zustand

+

如何使用

Zustand 是一个非常简单粗暴的全局状态管理库,它的使用有多简单呢?如下:

+
1
> pnpm add zustand
+ +
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// useFormStateStore.ts
import { create } from 'zustand';

interface State {
loading: boolean;
disabled: boolean;
setLoadingByAge: (value: number) => void;
}

export const useFormStateStore = create<State>((set) => ({
loading: false,
disabled: false,
setLoadingByAge: (value) => {
set({ loading: value > 10 });
},
}));
+ +
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// app.tsx
import { useState, type FC } from 'react';
import { useFormStateStore } from './useFormStateStore';
import { Button } from '@/components/Button';

const Loading: FC = () => {
const { loading } = useFormStateStore();
return <div>loading: {String(loading)}</div>;
};

const Disabled: FC = () => {
const { disabled } = useFormStateStore();
return <div>disabled: {String(disabled)}</div>;
};

const Main: FC = () => {
const { setLoadingByAge } = useFormStateStore();
const [age, setAge] = useState(0);

return (
<div>
<Loading />
<br />
<Disabled />
<br />
<Button
onClick={() => {
useFormStateStore.setState({
loading: true,
});
}}
>
set loading true
</Button>
<Button
onClick={() => {
useFormStateStore.setState({
loading: false,
});
}}
>
set loading false
</Button>
<Button
onClick={() => {
useFormStateStore.setState({
disabled: true,
});
}}
>
set disabled true
</Button>
<Button
onClick={() => {
useFormStateStore.setState({
disabled: false,
});
}}
>
set disabled false
</Button>
<br />
<input
type="number"
value={age}
onChange={(e) => {
setAge(Number(e.target.value));
}}
/>
<br />
<Button
onClick={() => {
setLoadingByAge(age);
}}
>
set loading by age
</Button>
</div>
);
};

export default Main;
+ +

useFormStateStore.ts中定义了状态,然后在app.tsx中使用,就是这么简单粗暴!这里有几点介绍下:

+
    +
  • 对于简单的状态更新,使用setState方法就可以,它的参数是一个对象,这个对象就是你要更新的状态,它会和之前的状态进行合并,然后返回一个新的状态,从而触发组件更新。

    +
  • +
  • 对于需要通用的逻辑处理的状态更新,参照useFormStateStore.ts中的setLoadingByAge方法,将它作为状态里的一个方法就行了。

    +
  • +
+

Zustand

使用非常简单,API也很少,它的原理是使用了Proxy,所以它的性能非常好。

+

相比Redux

相比Redux,Zustand的代码非常简单明了,不需要使用connectmapStateToPropsmapDispatchToProps这些方法。

+

相比React Context

React Context需要一个Provider包裹组件以传递状态,需要一个useContext使用状态,光从层级上就让人绕起来了。而Zustand只需要一个create方法,就可以使用了,且状态是全局的,不需要传递。

+ + +
+ +
+ + + + + +
+ + +
+ + + +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/dist/tags/github-actions/index.html b/dist/tags/github-actions/index.html new file mode 100644 index 0000000..22df6aa --- /dev/null +++ b/dist/tags/github-actions/index.html @@ -0,0 +1,221 @@ + + + + + + + Tag: github actions | TaoLiuJun's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+ 2023 +
+
+ + + +
+ + + +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/dist/tags/react-store/index.html b/dist/tags/react-store/index.html new file mode 100644 index 0000000..25e9a7f --- /dev/null +++ b/dist/tags/react-store/index.html @@ -0,0 +1,221 @@ + + + + + + + Tag: react store | TaoLiuJun's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+ 2023 +
+
+ + + + +
+ + + +
+ + + +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/dist/tags/zustand/index.html b/dist/tags/zustand/index.html new file mode 100644 index 0000000..79191ab --- /dev/null +++ b/dist/tags/zustand/index.html @@ -0,0 +1,221 @@ + + + + + + + Tag: zustand | TaoLiuJun's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+ 2023 +
+
+ + + + +
+ + + +
+ + + +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/src/db.json b/src/db.json new file mode 100644 index 0000000..33060ce --- /dev/null +++ b/src/db.json @@ -0,0 +1 @@ +{"meta":{"version":1,"warehouse":"4.0.2"},"models":{"Asset":[{"_id":"node_modules/hexo-theme-landscape/source/css/style.styl","path":"css/style.styl","modified":1,"renderable":1},{"_id":"node_modules/hexo-theme-landscape/source/fancybox/jquery.fancybox.min.css","path":"fancybox/jquery.fancybox.min.css","modified":1,"renderable":1},{"_id":"node_modules/hexo-theme-landscape/source/fancybox/jquery.fancybox.min.js","path":"fancybox/jquery.fancybox.min.js","modified":1,"renderable":1},{"_id":"node_modules/hexo-theme-landscape/source/js/jquery-3.6.4.min.js","path":"js/jquery-3.6.4.min.js","modified":1,"renderable":1},{"_id":"node_modules/hexo-theme-landscape/source/js/script.js","path":"js/script.js","modified":1,"renderable":1},{"_id":"node_modules/hexo-theme-landscape/source/css/images/banner.jpg","path":"css/images/banner.jpg","modified":1,"renderable":1}],"Cache":[{"_id":"source/_posts/github-actions-sample-eslint-in-pull-request.md","hash":"d8d26a081af1360062c354f884a2c6de84872710","modified":1704273216691},{"_id":"source/_posts/react-zustand.md","hash":"346576eb884e3157eb3e630225a314d9f0cd36fe","modified":1704273216695},{"_id":"node_modules/hexo-theme-landscape/LICENSE","hash":"c480fce396b23997ee23cc535518ffaaf7f458f8","modified":1704270132837},{"_id":"node_modules/hexo-theme-landscape/README.md","hash":"1a9b279e6dd29fd19245f913f0c4a316ffaa62db","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/_config.yml","hash":"b608c1f1322760dce9805285a602a95832730a2e","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/package.json","hash":"4bf95d52f77edf811f23f6d264a7493311a8d078","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/languages/de-DE.yml","hash":"d29d1c4256b7ed9df42f511c2ff0a23ad5fd6c1f","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/de.yml","hash":"3ebf0775abbee928c8d7bda943c191d166ded0d3","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/default.yml","hash":"ea5e6aee4cb14510793ac4593a3bddffe23e530c","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/en-GB.yml","hash":"ea5e6aee4cb14510793ac4593a3bddffe23e530c","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/en-US.yml","hash":"ea5e6aee4cb14510793ac4593a3bddffe23e530c","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/en.yml","hash":"3083f319b352d21d80fc5e20113ddf27889c9d11","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/es-ES.yml","hash":"7008a8fc91f18d2a735864817b8ebda30c7a2c66","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/es.yml","hash":"76edb1171b86532ef12cfd15f5f2c1ac3949f061","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/fr-FR.yml","hash":"8d09dbdab00a30a2870b56f7c0a7ca7deafa7b88","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/fr.yml","hash":"415e1c580ced8e4ce20b3b0aeedc3610341c76fb","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/hu-HU.yml","hash":"712d18664898fa21ba38d4973e90ef41a324ea25","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/hu.yml","hash":"284d557130bf54a74e7dcef9d42096130e4d9550","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/it-IT.yml","hash":"2cb6dc2fab9bd2dbe1c8bb869a9e8bf85a564fdd","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/it.yml","hash":"89b7d91306b2c1a0f3ac023b657bf974f798a1e8","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/ja-JP.yml","hash":"08481267e0c112e1f6855620f2837ec4c4a98bbd","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/ja.yml","hash":"a73e1b9c80fd6e930e2628b393bfe3fb716a21a9","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/ko-KR.yml","hash":"19209ad8f9d4057e8df808937f950eb265e1db69","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/ko.yml","hash":"881d6a0a101706e0452af81c580218e0bfddd9cf","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/mn-MN.yml","hash":"b9e5f3e7c0c2f779cf2cfded6db847b5941637ca","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/mn.yml","hash":"2e7523951072a9403ead3840ad823edd1084c116","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/nl-NL.yml","hash":"5ebbc30021f05d99938f96dfff280392df7f91f0","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/nl.yml","hash":"12ed59faba1fc4e8cdd1d42ab55ef518dde8039c","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/no.yml","hash":"965a171e70347215ec726952e63f5b47930931ef","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/pt-PT.yml","hash":"0f852b6b228e6ea59aa3540574bb89b233f2a098","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/pt.yml","hash":"57d07b75d434fbfc33b0ddb543021cb5f53318a8","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/ru-RU.yml","hash":"360d11a28bb768afb1dd15f63fa7fd3a8cc547ee","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/ru.yml","hash":"4fda301bbd8b39f2c714e2c934eccc4b27c0a2b0","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/th-TH.yml","hash":"ebfdba9bc4842c829473c1e6e4544344f182724d","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/th.yml","hash":"84a55b00aa01f03982be294e43c33a20e6d32862","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/tr.yml","hash":"a1cdbfa17682d7a971de8ab8588bf57c74224b5b","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/zh-CN.yml","hash":"1efd95774f401c80193eac6ee3f1794bfe93dc5a","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/languages/zh-TW.yml","hash":"53ce3000c5f767759c7d2c4efcaa9049788599c3","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/layout/archive.ejs","hash":"2703b07cc8ac64ae46d1d263f4653013c7e1666b","modified":1704270133201},{"_id":"node_modules/hexo-theme-landscape/layout/category.ejs","hash":"765426a9c8236828dc34759e604cc2c52292835a","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/layout/index.ejs","hash":"aa1b4456907bdb43e629be3931547e2d29ac58c8","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/layout/layout.ejs","hash":"0d1765036e4874500e68256fedb7470e96eeb6ee","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/layout/page.ejs","hash":"7d80e4e36b14d30a7cd2ac1f61376d9ebf264e8b","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/layout/post.ejs","hash":"7d80e4e36b14d30a7cd2ac1f61376d9ebf264e8b","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/layout/tag.ejs","hash":"eaa7b4ccb2ca7befb90142e4e68995fb1ea68b2e","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/scripts/fancybox.js","hash":"c857d7a5e4a5d71c743a009c5932bf84229db428","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/layout/_partial/after-footer.ejs","hash":"377d257d5d16e0158a4405c72401517b074fd7ff","modified":1704270133201},{"_id":"node_modules/hexo-theme-landscape/layout/_partial/archive-post.ejs","hash":"c7a71425a946d05414c069ec91811b5c09a92c47","modified":1704270133201},{"_id":"node_modules/hexo-theme-landscape/layout/_partial/archive.ejs","hash":"7cb70a7a54f8c7ae49b10d1f37c0a9b74eab8826","modified":1704270133201},{"_id":"node_modules/hexo-theme-landscape/layout/_partial/article.ejs","hash":"56597e951203dd662a6d2c817c7c4f1c920d4a25","modified":1704270133201},{"_id":"node_modules/hexo-theme-landscape/layout/_partial/footer.ejs","hash":"3656eb692254346671abc03cb3ba1459829e0dce","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/layout/_partial/gauges-analytics.ejs","hash":"21a1e2a3907d1a3dad1cd0ab855fe6735f233c74","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/layout/_partial/google-analytics.ejs","hash":"2ea7442ea1e1a8ab4e41e26c563f58413b59a3d0","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/layout/_partial/head.ejs","hash":"f05bced793b0314d4f2ef0c993b3a51d0b7d203a","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/layout/_partial/header.ejs","hash":"6a5033d189554c9a6d42e2ef7952ae5c9742648e","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/layout/_partial/mobile-nav.ejs","hash":"e952a532dfc583930a666b9d4479c32d4a84b44e","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/layout/_partial/sidebar.ejs","hash":"930da35cc2d447a92e5ee8f835735e6fd2232469","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/layout/_widget/archive.ejs","hash":"beb4a86fcc82a9bdda9289b59db5a1988918bec3","modified":1704270133201},{"_id":"node_modules/hexo-theme-landscape/layout/_widget/category.ejs","hash":"dd1e5af3c6af3f5d6c85dfd5ca1766faed6a0b05","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/layout/_widget/recent_posts.ejs","hash":"60c4b012dcc656438ff59997e60367e5a21ab746","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/layout/_widget/tag.ejs","hash":"2de380865df9ab5f577f7d3bcadf44261eb5faae","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/layout/_widget/tagcloud.ejs","hash":"b4a2079101643f63993dcdb32925c9b071763b46","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/source/css/_extend.styl","hash":"222fbe6d222531d61c1ef0f868c90f747b1c2ced","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/source/css/_variables.styl","hash":"ca28281423ae57d76b6c1eb91cd845fd4e518bd6","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/source/css/style.styl","hash":"e55a1d92954ed20f6887f92dc727bb995a010a43","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/source/fancybox/jquery.fancybox.min.css","hash":"1be9b79be02a1cfc5d96c4a5e0feb8f472babd95","modified":1704270133201},{"_id":"node_modules/hexo-theme-landscape/source/js/script.js","hash":"49773efcb2221bbdf2d86f3f5c5ff2d841b528cc","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/layout/_partial/post/category.ejs","hash":"c6bcd0e04271ffca81da25bcff5adf3d46f02fc0","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/layout/_partial/post/date.ejs","hash":"f1458584b679545830b75bef2526e2f3eb931045","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/layout/_partial/post/gallery.ejs","hash":"3d9d81a3c693ff2378ef06ddb6810254e509de5b","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/layout/_partial/post/nav.ejs","hash":"16a904de7bceccbb36b4267565f2215704db2880","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/layout/_partial/post/tag.ejs","hash":"2fcb0bf9c8847a644167a27824c9bb19ac74dd14","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/layout/_partial/post/title.ejs","hash":"4d7e62574ddf46de9b41605fe3140d77b5ddb26d","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/source/css/_partial/archive.styl","hash":"db15f5677dc68f1730e82190bab69c24611ca292","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/source/css/_partial/article.styl","hash":"2d1f6f79ebf9cb55ebdb3865a2474437eb2b37c6","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/source/css/_partial/comment.styl","hash":"79d280d8d203abb3bd933ca9b8e38c78ec684987","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/source/css/_partial/footer.styl","hash":"e35a060b8512031048919709a8e7b1ec0e40bc1b","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/source/css/_partial/header.styl","hash":"268d2989acb06e2ddd06cc36a6918c6cd865476b","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/source/css/_partial/highlight.styl","hash":"9cc3b2927d814f2f6e8e188f9d3657b94f4c6ef3","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/source/css/_partial/mobile.styl","hash":"a399cf9e1e1cec3e4269066e2948d7ae5854d745","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/source/css/_partial/sidebar-aside.styl","hash":"890349df5145abf46ce7712010c89237900b3713","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/source/css/_partial/sidebar-bottom.styl","hash":"8fd4f30d319542babfd31f087ddbac550f000a8a","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/source/css/_partial/sidebar.styl","hash":"404ec059dc674a48b9ab89cd83f258dec4dcb24d","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/source/css/_util/grid.styl","hash":"0bf55ee5d09f193e249083602ac5fcdb1e571aed","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/source/css/_util/mixin.styl","hash":"44f32767d9fd3c1c08a60d91f181ee53c8f0dbb3","modified":1704270133209},{"_id":"node_modules/hexo-theme-landscape/source/fancybox/jquery.fancybox.min.js","hash":"6181412e73966696d08e1e5b1243a572d0f22ba6","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/source/js/jquery-3.6.4.min.js","hash":"eda46747c71d38a880bee44f9a439c3858bb8f99","modified":1704270133205},{"_id":"node_modules/hexo-theme-landscape/source/css/images/banner.jpg","hash":"f44aa591089fcb3ec79770a1e102fd3289a7c6a6","modified":1704270133205},{"_id":"public/github-actions-sample-eslint-in-pull-request/index.html","hash":"c127049c6f7716fd05343ec91d13791ececa161a","modified":1704273217808},{"_id":"public/react-zustand/index.html","hash":"0865fe2a8dcebbe8380ac5894b63ce618a7ce525","modified":1704273217808},{"_id":"public/archives/index.html","hash":"a3e5ef891425cf7f9a74cc481391db0cef6df832","modified":1704273217808},{"_id":"public/archives/2023/index.html","hash":"af6009a0e1abd053eca5b1825440974d94e3c0dc","modified":1704273217808},{"_id":"public/archives/2023/12/index.html","hash":"4942d8918efe645a56c8a18d35ac700a090708ad","modified":1704273217808},{"_id":"public/categories/工程化/index.html","hash":"9b6fc65ca27999579f2879a6e2d6781bd182f126","modified":1704273217808},{"_id":"public/categories/React/index.html","hash":"7bbde442712865bf309ed38411f8c4d8a3059329","modified":1704273217808},{"_id":"public/index.html","hash":"9bd1ab1740439ca15fa6443dc054be964fb00166","modified":1704273217808},{"_id":"public/tags/github-actions/index.html","hash":"6d466024facde7a014b9a68f733dc9b10bb3e1e0","modified":1704273217808},{"_id":"public/tags/zustand/index.html","hash":"4f999b262ec480f48c4129a3d6281685629f0cdf","modified":1704273217808},{"_id":"public/tags/react-store/index.html","hash":"46b2eb630e9d320ef78f339d44623736ac8cc72b","modified":1704273217808},{"_id":"public/css/style.css","hash":"ddb3792605d744ab3d9f0a649c82b62e9b16daa6","modified":1704273217808},{"_id":"public/fancybox/jquery.fancybox.min.css","hash":"1be9b79be02a1cfc5d96c4a5e0feb8f472babd95","modified":1704273217808},{"_id":"public/fancybox/jquery.fancybox.min.js","hash":"6181412e73966696d08e1e5b1243a572d0f22ba6","modified":1704273217808},{"_id":"public/js/jquery-3.6.4.min.js","hash":"eda46747c71d38a880bee44f9a439c3858bb8f99","modified":1704273217808},{"_id":"public/js/script.js","hash":"49773efcb2221bbdf2d86f3f5c5ff2d841b528cc","modified":1704273217808},{"_id":"public/css/images/banner.jpg","hash":"f44aa591089fcb3ec79770a1e102fd3289a7c6a6","modified":1704273217808}],"Category":[{"name":"工程化","_id":"clqxk9r670003joorffjb03fe"},{"name":"React","_id":"clqxk9r680005joor13xp87sb"}],"Data":[],"Page":[],"Post":[{"title":"7. GitHub Actions - 在pull request中执行eslint检测的工作流例子","date":"2023-12-28T19:56:49.000Z","url":"github-actions-sample-eslint-in-pull-request","_content":"\n\n原文链接:[https://github.com/taoliujun/blog/issues/36](https://github.com/taoliujun/blog/issues/36)\n\n\n\n一个在pull request发起的时候执行eslint检测的workflow,[点此查看完整代码](https://github.com/taoliujun/npm-packages/blob/master/.github/workflows/check-pull-request.yml),它实现的功能如下:\n\n- 在pull request创建、更新的时候执行。\n- 先回复一个评论,告诉用户正在运行。\n- 初始化仓库,并安装依赖,产生依赖缓存。\n- 运行eslint增量检查。\n- 运行typescript检查。\n- 运行jest检查。\n- 更新之前的评论,回复检查的结果。\n\n运行截图:\n\n![Alt text](https://github.com/taoliujun/blog/assets/5689134/09c86bc1-ada1-41c3-9f8f-7e6c46f8204e)\n\n为避免歧义,涉及到github action的术语都是英文的。术语介绍如下:\n\n* workflow,工作流,可以理解为yml文件。\n* jobs,工作,一个workflow可以包含多个job,并行执行。\n* steps,作业,一个job可以包含多个step,串行执行。\n* action,操作,作业中具体的执行。\n\n## 步骤\n\n- [初始化workflow](https://github.com/taoliujun/blog/issues/36#issuecomment-1871790603)\n- [reply checking](https://github.com/taoliujun/blog/issues/36#issuecomment-1871806576)\n- [./.github/actions/unique-comment](https://github.com/taoliujun/blog/issues/36#issuecomment-1871818126)\n- [init](https://github.com/taoliujun/blog/issues/36#issuecomment-1871862632)\n- [eslint](https://github.com/taoliujun/blog/issues/36#issuecomment-1871862779)\n- [typescript](https://github.com/taoliujun/blog/issues/36#issuecomment-1871862850)\n- [unit test](https://github.com/taoliujun/blog/issues/36#issuecomment-1871863037)\n- [reply result](https://github.com/taoliujun/blog/issues/36#issuecomment-1871863117)\n\n\n\n# 初始化workflow\n\n在项目中新建文件`.github/workflows/check-pull-request.yml`,内容如下:\n\n```yaml\nname: test check pull request\nrun-name: 'check pull request #${{ github.event.pull_request.number }}'\non:\n pull_request:\n types: [opened, synchronize, reopened]\njobs:\n replyChecking:\n runs-on: ubuntu-latest\n steps:\n - run: echo 'replyChecking'\n\n init:\n runs-on: ubuntu-latest\n steps:\n - run: echo 'init'\n\n eslint:\n runs-on: ubuntu-latest\n needs: [init]\n steps:\n - run: echo 'eslint'\n\n typescript:\n runs-on: ubuntu-latest\n needs: [init]\n steps:\n - run: echo 'typescript'\n\n unitTest:\n runs-on: ubuntu-latest\n needs: [init]\n steps:\n - run: echo 'unitTest'\n\n replyResult:\n runs-on: ubuntu-latest\n needs: [replyChecking, eslint, typescript, unitTest]\n steps:\n - run: echo 'replyResult'\n```\n\n## name和run-name\n\n给workflow命名为`check pull request`,它会出现在Actions页面的左侧菜单中。运行实例名为`check pull request #44`,出现在右侧的运行列表中。如图:\n\n![](https://github.com/taoliujun/blog/assets/5689134/c1371ff2-8fc3-4e5b-8b60-3c572419938b)\n\n`run-name`中的`${{ github.event.pull_request.number }}`是workflow的上下文,这里读取了上下文中的pr编号。\n\n## on\n\n`on`指定了workflow的触发条件,这里配置了在pr创建、同步、重新打开的时候,触发该workflow。\n\n## jobs\n\n按照设想,需要定义几个job,分别是:\n\n- replyChecking:回复用户正在检查中\n- init:初始化仓库,缓存依赖项\n- eslint:运行eslint检查\n- typescript:运行typescript检查\n- unitTest:运行单元测试\n- replyResult:回复用户检查结果\n\n`jobs`是并行运行的,聪明如你肯定发现了,eslint、typescript、unitTest这三个job会涉及到安装npm依赖,所以它们最好在init后执行,确保依赖已经缓存了。\n\n其次,replyResult肯定要拿到eslint等job的结果才能执行,所以使用了`needs`管理它们的执行依赖关系。\n\n### runs-on\n\n每个job都运行在独立的容器中,github官方提供了windows、macos、linux多种容器,这里使用了ubuntu容器。\n\n## 测试\n\n发起一个pr,看到Actions页面出现了新的运行实例,点击进去,可以看到各个job的运行情况和依赖关系:\n\n![](https://github.com/taoliujun/blog/assets/5689134/09c86bc1-ada1-41c3-9f8f-7e6c46f8204e)\n\n\n\n# replyChecking\n\n在进行eslint检测之前,先在pr里回复`checking`,并且带上拽酷炫的话。将replyChecking改成如下:\n\n```yaml\nreplyChecking:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n with:\n ref: ${{github.head_ref}}\n - name: Get date time\n id: getDateTime\n run: echo \"result=$(TZ=Asia/Shanghai date)\" >> \"$GITHUB_OUTPUT\"\n - name: Create or update a comment\n uses: ./.github/actions/unique-comment\n with:\n uniqueIdentifier: ${{ github.workflow }}\n body: |\n **Checking...**\n\n ---\n\n Commented by Action [${{github.workflow}}](${{github.event.repository.html_url}}/actions/runs/${{github.run_id}}), last updated on ${{steps.getDateTime.outputs.result}}.\n```\n\n`steps`每一步里`name`、`id`是可选的,`name`在Actions详情页面里会显示,更直观的看到step的名称,推荐写上。\n\n## Checkout\n\n`uses`表示使用一个action,名为`actions/checkout@v4`,它用来拉取仓库。\n\n> 同其他编程语言一样,重复的action可以封装起来。[action市场](https://github.com/marketplace?type=actions)提供了很多。\n\n`with`属性指定了该action的输入参数,每个action的参数不尽相同。\n\n`ref`参数表示要拉取的分支,`${{github.head_ref}}`也是一个上下文,表示当前pr的源分支。\n\n\n## Get Date time\n\n这step还写了`id`,表示该step在该job中的唯一标识,为什么要写呢?是为了下一步step能根据`id`读取到它的`output`。\n\n> **output**是workflow中非常重要的概念,它用于在step之间、job之间分享简单的数据。\n\n`run`就是在容器中跑一个命令,这里跑了一个unix bash命令,将当前时间写入到`$GITHUB_OUTPUT`中,键名为`result`。\n\n> `$GITHUB_OUTPUT`是workflow注入到容器中的一个路径,用于存放output。\n\n## Create or update a comment\n\n`uses`使用了本地的action,这个action用于创建或更新一个唯一回复,下一节说。\n\n> 有时候,官方或市场的action并不能满足你的需要,就得自己写一个了。\n\n同理,该action也有`with`属性,`uniqueIdentifier`是回复评论的唯一标识,`body`是回复的内容,内容使用了markdown语法,里面还涉及到上下文不一一细讲了。只说`${{steps.getDateTime.outputs.result}}`这个上下文表示获取getDateTime这个step中,键名为`result`的值。\n\n如果你不需要在内容里插入时间,那么上面的`Get Date time`就可以省略了。\n\n## 测试\n\n因为我已经有完整的代码了,所以运行后,pr中会有一个回复,如图:\n\n![](https://github.com/taoliujun/blog/assets/5689134/42396a84-b798-4f4e-9f39-5bf92a8acb15)\n\n\n# ./.github/actions/unique-comment\n\n这是一个封装的javascript action,用于对issue创建、更新唯一评论。\n\n## 目录结构\n\n创建目录`./.github/actions/unique-comment`,最终目录结构如下:\n\n```bash\n.\n├── action.yml\n├── config\n│   └── webpack.config.js\n├── dist\n│   ├── index.js\n│   └── index.js.LICENSE.txt\n├── package.json\n└── src\n └── index.js\n```\n\n## action.yml\n\n这是action的配置文件,必须存在,内容如下:\n\n```yaml\nname: unique-comment\ndescription: create or update a unique comment\n\nruns:\n using: 'node20'\n main: './dist/index.js'\n\ninputs:\n token:\n description: 'GitHub token'\n required: false\n default: ${{ github.token }}\n owner:\n description: 'Repository owner'\n required: false\n default: ${{ github.event.repository.owner.login }}\n repo:\n description: 'Repository name'\n required: false\n default: ${{ github.event.repository.name }}\n issue_number:\n description: 'Issue number'\n required: false\n default: ${{ github.event.number }}\n body:\n description: 'Comment body'\n required: false\n uniqueIdentifier:\n description: 'Unique identifier for comment'\n required: false\n default: 'unique-comment'\n```\n\n大部分属性不一一细讲了,都是简单的英文望文生义即可。\n\n`runs`表示运行在`node20`环境下,入口文件为`./dist/index.js`。\n\n`inputs`表示接受的参数,也就是之前提到的`with`属性里要输入的参数。用`required`表示是否必须传入,`default`表示默认值。\n\n## src/index.js\n\n为什么入口文件是`dist/index.js`,而不是`src/index.js`呢?因为要引用一些github官方提供的快捷操作github REST API的js包去操作issue评论(pull request也是一种issue),最终打包后的文件才能在工作流中稳妥的运行。所以,写好`src/index.js`,再打包就行。\n\n该文件代码如下:\n\n```javascript\nconst core = require('@actions/core');\nconst github = require('@actions/github');\n\nconst main = async () => {\n const token = core.getInput('token');\n const owner = core.getInput('owner');\n const repo = core.getInput('repo');\n const issueNumber = core.getInput('issue_number');\n const uniqueIdentifier = `[^uniqueIdentifier]: ${core.getInput('uniqueIdentifier')}`;\n const body = `${core.getInput('body')}\\n\\n${uniqueIdentifier}`;\n\n core.debug(`uniqueIdentifier is ${uniqueIdentifier}`);\n\n const octokit = github.getOctokit(token);\n\n const comments = await octokit.rest.issues.listComments({\n owner,\n repo,\n issue_number: issueNumber,\n });\n\n const botComment = comments.data.find((v) => v.body.includes(uniqueIdentifier));\n\n if (botComment) {\n core.info('update comment successfully.');\n await octokit.rest.issues.updateComment({\n owner,\n repo,\n comment_id: botComment.id,\n body,\n });\n } else {\n core.info('create comment successfully.');\n await octokit.rest.issues.createComment({\n owner,\n repo,\n issue_number: issueNumber,\n body,\n });\n }\n};\n\ntry {\n main();\n} catch (err) {\n core.setFailed(err.message);\n}\n```\n\n`@actions/core`和`@actions/github`是github官方提供的js包,前者可以方便的读取入参等,后者可以方便的操作github REST API。\n\n`main`函数的代码就是原生javascript,不一一解释了,主要通过`uniqueIdentifier`来判断是否发布过评论,如果是,就更新评论,否则就创建评论。\n\n> markdown语法`[^uniqueIdentifier]`表示脚注,不会被渲染。\n\n`core.setFailed(err.message);`表示抛出退出代码。\n\n## config/webpack.config.js\n\n打包用的,配置简单可用即可:\n\n```javascript\nmodule.exports = {\n mode: 'production',\n target: 'node20',\n entry: './src/index.js',\n output: {\n filename: 'index.js',\n clean: true,\n },\n};\n```\n\n## package.json\n\n```json\n{\n \"name\": \"unique-comment\",\n \"version\": \"1.0.0\",\n \"private\": true,\n \"scripts\": {\n \"build\": \"webpack --config ./config/webpack.config.js\"\n },\n \"dependencies\": {\n \"@actions/core\": \"^1.10.1\",\n \"@actions/github\": \"^6.0.0\"\n },\n \"devDependencies\": {\n \"webpack\": \"^5.89.0\",\n \"webpack-cli\": \"^5.1.4\"\n }\n}\n```\n\n没啥好说的,列出了依赖项。和一个打包脚本。\n\n## 测试\n\n修改了`src/index.js`得`build`,然后push到github仓库。\n\n记得将**dist**目录也提交到github仓库。\n\n\n\n# init\n\n现在,开始搞正经的了。\n\n先初始化项目,这个job的目的仅仅是为了缓存pnpm依赖项,如果你的项目的依赖项不经常更新,可以省略这个job,后续也不要`needs`这个job。\n\n将init改成如下:\n\n```yaml\ninit:\n runs-on: ubuntu-latest\n steps:\n - name: Init repo\n uses: actions/checkout@v4\n with:\n ref: ${{github.head_ref}}\n\n - name: Init pnpm\n uses: pnpm/action-setup@v2\n with:\n version: 8\n\n - name: Init node\n uses: actions/setup-node@v4\n with:\n node-version: 20\n cache: 'pnpm'\n\n - name: Install dependencies\n run: pnpm install\n```\n\n相信经过对之前的job的了解,这里的配置就看起来很简单了。\n\n## Init pnpm\n\n使用第三方action,安装pnpm@^8。\n\n## Init node\n\n`cache: 'pnpm'`指定缓存机制,它内部是利用了workflow的cache机制。\n\n## Install dependencies\n\n安装依赖项,触发缓存。\n\n\n# eslint\n\n将eslint改成如下:\n\n```yaml\neslint:\n runs-on: ubuntu-latest\n needs: [init]\n outputs:\n result: ${{ steps.lint.outputs.result }}\n steps:\n - name: Init repo\n uses: actions/checkout@v4\n with:\n ref: ${{github.head_ref}}\n fetch-depth: 0\n\n - name: Init pnpm\n uses: pnpm/action-setup@v2\n with:\n version: 8\n\n - name: Init node\n uses: actions/setup-node@v4\n with:\n node-version: 20\n cache: 'pnpm'\n\n - name: Install dependencies\n run: pnpm install\n\n - name: Run eslint\n id: lint\n uses: actions/github-script@v7\n with:\n result-encoding: string\n script: |\n let output = '';\n let outerr = '';\n let diffFiles = '';\n\n await exec.exec(\n `git diff --name-only origin/${{github.base_ref}}`,\n [],\n {\n // silent: true,\n // ignoreReturnCode: true,\n listeners: {\n stdout: (data) => {\n diffFiles += data.toString();\n },\n },\n }\n );\n\n const lintFiles = diffFiles.split(`\\n`).filter((file) => {\n return file.endsWith('.js') || file.endsWith('.ts') || file.endsWith('.tsx')\n }).join(' ');\n\n await exec.exec(\n // \"pnpm run lint --format stylish\",\n `pnpm eslint ${lintFiles}`,\n [],\n {\n // silent: true,\n ignoreReturnCode: true,\n listeners: {\n stdout: (data) => {\n output += data.toString();\n },\n stderr: (data) => {\n outerr += data.toString();\n },\n },\n }\n );\n\n if (outerr) {\n return `:x: Some command execution errors, non-eslint business errors.`;\n }\n\n const errorMatch = output.match(/(\\d+) errors?/);\n const warnMatch = output.match(/(\\d+) warnings?/);\n\n if (errorMatch && errorMatch?.[1] !== '0') {\n return `:x: ${errorMatch?.[0]} ${warnMatch?.[0]}`;\n }\n\n return `:white_check_mark: ${errorMatch?.[0] || '0 error'} ${warnMatch?.[0] || '0 warning'}`;\n```\n\n## needs\n\n使用`needs`依赖init,可以使用到pnpm的缓存项,防止install太慢。\n\n> 因为eslint、typescript、unitTest都需要pnpm install,所以一个前置的init去缓存pnpm依赖项,可以加快后续的install速度。\n\n## outputs\n\njob里的outputs,可以在依赖它的其他job中访问到。这里使用`${{ steps.lint.outputs.result }}`去获取该job中lint这个step里的output里的result。\n\n> output有job和step两个维度,注意区分。\n\n\n## Run eslint\n\n它uses了`actions/github-script@v7`,这是github官方提供的一个action,可以在`with.script`里写js代码去执行,同时它会注入一些变量到script中去,见它的[官方文档](https://github.com/actions/github-script/tree/v7/)。\n\n> 对于简单的js代码,可以使用这个action去完成,不用再去写一个js文件。\n\n`result-encoding`是指定script返回的数据格式的,默认是json,这指定为string。\n\n> 为什么script里return了string,还要指定为string呢?\n> 因为`return 'hello'`在json encode后是`'\"hello\"'`,而string encode后为`'hello'`。\n\nscript里是原生的js代码了,里面的`exec`是该action注入的变量,用来执行shell命令。\n\n这段js代码做了两个事情,一是`git diff`获取pr中改动的文件列表,二是`eslint`检查这些增量文件,最后返回处理的结果。\n\n## fetch-depth\n\nInit repo这个step里设置了`fetch-depth: 0`,不然获取不到完整的git分支,具体看`actions/checkout`的解释,涉及到git的知识不展开细说了。\n\n## steps.lint.outputs.result\n\n`steps.lint.outputs.result`为什么能拿到lint step里的output.result呢?因为`actions/github-script`这个action内部将script的返回值,设置到`$GITHUB_OUTPUT`里了,且键名为`result`。\n\n\n# typescript\n\n和eslint的配置大同小异,只是改了对检测结果的判断。\n\n```yaml\ntypescript:\n runs-on: ubuntu-latest\n needs: [init]\n outputs:\n result: ${{ steps.lint.outputs.result }}\n steps:\n - name: Init repo\n uses: actions/checkout@v4\n with:\n ref: ${{github.head_ref}}\n\n - name: Init pnpm\n uses: pnpm/action-setup@v2\n with:\n version: 8\n\n - name: Init node\n uses: actions/setup-node@v4\n with:\n node-version: 20\n cache: 'pnpm'\n\n - name: Install dependencies\n run: pnpm install\n\n - name: Run lint\n id: lint\n uses: actions/github-script@v7\n with:\n result-encoding: string\n script: |\n let output = '';\n let outerr = '';\n\n await exec.exec(\n `pnpm run -r lint:ts`,\n [],\n {\n // silent: true,\n ignoreReturnCode: true,\n listeners: {\n stdout: (data) => {\n output += data.toString();\n },\n stderr: (data) => {\n outerr += data.toString();\n },\n },\n }\n );\n\n if (outerr) {\n return `:x: Some command execution errors, no business errors.`;\n }\n\n const errorMatch = output.match(/error TS/g);\n\n if (errorMatch) {\n return `:x: ${errorMatch?.length} errors`;\n }\n\n return `:white_check_mark: ${'0 error'}`;\n```\n\n\n# unitTest\n\n和eslint的配置大同小异,只是改了对检测结果的判断。唯一的区别是jest的检测结果是输出到stderr,见https://github.com/jestjs/jest/issues/5064。\n\n```yaml\nunitTest:\n runs-on: ubuntu-latest\n needs: [init]\n outputs:\n result: ${{ steps.lint.outputs.result }}\n steps:\n - name: Init repo\n uses: actions/checkout@v4\n with:\n ref: ${{github.head_ref}}\n\n - name: Init pnpm\n uses: pnpm/action-setup@v2\n with:\n version: 8\n\n - name: Init node\n uses: actions/setup-node@v4\n with:\n node-version: 20\n cache: 'pnpm'\n\n - name: Install dependencies\n run: |\n pnpm remove @nike/eslint-multi-formatter || true\n pnpm remove @nike/svg-packer || true\n pnpm install\n\n - name: Run lint\n id: lint\n uses: actions/github-script@v7\n with:\n result-encoding: string\n script: |\n let output = '';\n let outerr = '';\n\n await exec.exec(\n `pnpm run test`,\n [],\n {\n // silent: true,\n ignoreReturnCode: true,\n listeners: {\n stdout: (data) => {\n output += data.toString();\n },\n stderr: (data) => {\n outerr += data.toString();\n },\n },\n }\n );\n\n // why use outerr? https://github.com/jestjs/jest/issues/5064\n\n const failMatch = outerr.match(/Test Suites: \\d+ failed/);\n\n if (failMatch) {\n return `:x: ${failMatch?.[0]}`;\n }\n\n const errorMatch = outerr.match(/Jest: \"global\" coverage threshold for lines \\([0-9\\.]+%\\) not met: [0-9\\.]+%/);\n\n if (errorMatch) {\n return `:x: ${errorMatch?.[0]}`;\n }\n\n return `:white_check_mark: passed`;\n```\n\n\n# replyResult\n\n最后,将几个检测的结果进行汇总,回复到pr里就行了。\n\n```yaml\nreplyResult:\n runs-on: ubuntu-latest\n needs: [replyChecking, eslint, typescript, unitTest]\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n with:\n ref: ${{github.head_ref}}\n - name: Get date time\n id: getDateTime\n run: echo \"result=$(TZ=Asia/Shanghai date)\" >> \"$GITHUB_OUTPUT\"\n - name: Create or update a comment\n uses: ./.github/actions/unique-comment\n with:\n uniqueIdentifier: ${{ github.workflow }}\n body: |\n ## Eslint Check Result\n\n ${{needs.eslint.outputs.result}}\n\n ## Typescript Check Result\n\n ${{needs.typescript.outputs.result}}\n\n ## UnitTest Check Result\n\n ${{needs.unitTest.outputs.result}}\n\n ---\n\n Commented by Action [${{github.workflow}}](${{github.event.repository.html_url}}/actions/runs/${{github.run_id}}), last updated on ${{steps.getDateTime.outputs.result}}.\n```\n\n和replyChecking差不多,在body里使用`${{needs.eslint.outputs.result}}`去读取了eslint job的outputs。\n\n## 测试\n\n去发起新的pr,故意提交一个有eslint error的js/ts文件,看看表现吧~\n\n","source":"_posts/github-actions-sample-eslint-in-pull-request.md","raw":"---\ntitle: \"7. GitHub Actions - 在pull request中执行eslint检测的工作流例子\"\ndate: \"2023-12-29T03:56:49Z\"\ncategories:\n - [工程化]\n\nurl: github-actions-sample-eslint-in-pull-request\ntags:\n - github actions\n\n---\n\n\n原文链接:[https://github.com/taoliujun/blog/issues/36](https://github.com/taoliujun/blog/issues/36)\n\n\n\n一个在pull request发起的时候执行eslint检测的workflow,[点此查看完整代码](https://github.com/taoliujun/npm-packages/blob/master/.github/workflows/check-pull-request.yml),它实现的功能如下:\n\n- 在pull request创建、更新的时候执行。\n- 先回复一个评论,告诉用户正在运行。\n- 初始化仓库,并安装依赖,产生依赖缓存。\n- 运行eslint增量检查。\n- 运行typescript检查。\n- 运行jest检查。\n- 更新之前的评论,回复检查的结果。\n\n运行截图:\n\n![Alt text](https://github.com/taoliujun/blog/assets/5689134/09c86bc1-ada1-41c3-9f8f-7e6c46f8204e)\n\n为避免歧义,涉及到github action的术语都是英文的。术语介绍如下:\n\n* workflow,工作流,可以理解为yml文件。\n* jobs,工作,一个workflow可以包含多个job,并行执行。\n* steps,作业,一个job可以包含多个step,串行执行。\n* action,操作,作业中具体的执行。\n\n## 步骤\n\n- [初始化workflow](https://github.com/taoliujun/blog/issues/36#issuecomment-1871790603)\n- [reply checking](https://github.com/taoliujun/blog/issues/36#issuecomment-1871806576)\n- [./.github/actions/unique-comment](https://github.com/taoliujun/blog/issues/36#issuecomment-1871818126)\n- [init](https://github.com/taoliujun/blog/issues/36#issuecomment-1871862632)\n- [eslint](https://github.com/taoliujun/blog/issues/36#issuecomment-1871862779)\n- [typescript](https://github.com/taoliujun/blog/issues/36#issuecomment-1871862850)\n- [unit test](https://github.com/taoliujun/blog/issues/36#issuecomment-1871863037)\n- [reply result](https://github.com/taoliujun/blog/issues/36#issuecomment-1871863117)\n\n\n\n# 初始化workflow\n\n在项目中新建文件`.github/workflows/check-pull-request.yml`,内容如下:\n\n```yaml\nname: test check pull request\nrun-name: 'check pull request #${{ github.event.pull_request.number }}'\non:\n pull_request:\n types: [opened, synchronize, reopened]\njobs:\n replyChecking:\n runs-on: ubuntu-latest\n steps:\n - run: echo 'replyChecking'\n\n init:\n runs-on: ubuntu-latest\n steps:\n - run: echo 'init'\n\n eslint:\n runs-on: ubuntu-latest\n needs: [init]\n steps:\n - run: echo 'eslint'\n\n typescript:\n runs-on: ubuntu-latest\n needs: [init]\n steps:\n - run: echo 'typescript'\n\n unitTest:\n runs-on: ubuntu-latest\n needs: [init]\n steps:\n - run: echo 'unitTest'\n\n replyResult:\n runs-on: ubuntu-latest\n needs: [replyChecking, eslint, typescript, unitTest]\n steps:\n - run: echo 'replyResult'\n```\n\n## name和run-name\n\n给workflow命名为`check pull request`,它会出现在Actions页面的左侧菜单中。运行实例名为`check pull request #44`,出现在右侧的运行列表中。如图:\n\n![](https://github.com/taoliujun/blog/assets/5689134/c1371ff2-8fc3-4e5b-8b60-3c572419938b)\n\n`run-name`中的`${{ github.event.pull_request.number }}`是workflow的上下文,这里读取了上下文中的pr编号。\n\n## on\n\n`on`指定了workflow的触发条件,这里配置了在pr创建、同步、重新打开的时候,触发该workflow。\n\n## jobs\n\n按照设想,需要定义几个job,分别是:\n\n- replyChecking:回复用户正在检查中\n- init:初始化仓库,缓存依赖项\n- eslint:运行eslint检查\n- typescript:运行typescript检查\n- unitTest:运行单元测试\n- replyResult:回复用户检查结果\n\n`jobs`是并行运行的,聪明如你肯定发现了,eslint、typescript、unitTest这三个job会涉及到安装npm依赖,所以它们最好在init后执行,确保依赖已经缓存了。\n\n其次,replyResult肯定要拿到eslint等job的结果才能执行,所以使用了`needs`管理它们的执行依赖关系。\n\n### runs-on\n\n每个job都运行在独立的容器中,github官方提供了windows、macos、linux多种容器,这里使用了ubuntu容器。\n\n## 测试\n\n发起一个pr,看到Actions页面出现了新的运行实例,点击进去,可以看到各个job的运行情况和依赖关系:\n\n![](https://github.com/taoliujun/blog/assets/5689134/09c86bc1-ada1-41c3-9f8f-7e6c46f8204e)\n\n\n\n# replyChecking\n\n在进行eslint检测之前,先在pr里回复`checking`,并且带上拽酷炫的话。将replyChecking改成如下:\n\n```yaml\nreplyChecking:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n with:\n ref: ${{github.head_ref}}\n - name: Get date time\n id: getDateTime\n run: echo \"result=$(TZ=Asia/Shanghai date)\" >> \"$GITHUB_OUTPUT\"\n - name: Create or update a comment\n uses: ./.github/actions/unique-comment\n with:\n uniqueIdentifier: ${{ github.workflow }}\n body: |\n **Checking...**\n\n ---\n\n Commented by Action [${{github.workflow}}](${{github.event.repository.html_url}}/actions/runs/${{github.run_id}}), last updated on ${{steps.getDateTime.outputs.result}}.\n```\n\n`steps`每一步里`name`、`id`是可选的,`name`在Actions详情页面里会显示,更直观的看到step的名称,推荐写上。\n\n## Checkout\n\n`uses`表示使用一个action,名为`actions/checkout@v4`,它用来拉取仓库。\n\n> 同其他编程语言一样,重复的action可以封装起来。[action市场](https://github.com/marketplace?type=actions)提供了很多。\n\n`with`属性指定了该action的输入参数,每个action的参数不尽相同。\n\n`ref`参数表示要拉取的分支,`${{github.head_ref}}`也是一个上下文,表示当前pr的源分支。\n\n\n## Get Date time\n\n这step还写了`id`,表示该step在该job中的唯一标识,为什么要写呢?是为了下一步step能根据`id`读取到它的`output`。\n\n> **output**是workflow中非常重要的概念,它用于在step之间、job之间分享简单的数据。\n\n`run`就是在容器中跑一个命令,这里跑了一个unix bash命令,将当前时间写入到`$GITHUB_OUTPUT`中,键名为`result`。\n\n> `$GITHUB_OUTPUT`是workflow注入到容器中的一个路径,用于存放output。\n\n## Create or update a comment\n\n`uses`使用了本地的action,这个action用于创建或更新一个唯一回复,下一节说。\n\n> 有时候,官方或市场的action并不能满足你的需要,就得自己写一个了。\n\n同理,该action也有`with`属性,`uniqueIdentifier`是回复评论的唯一标识,`body`是回复的内容,内容使用了markdown语法,里面还涉及到上下文不一一细讲了。只说`${{steps.getDateTime.outputs.result}}`这个上下文表示获取getDateTime这个step中,键名为`result`的值。\n\n如果你不需要在内容里插入时间,那么上面的`Get Date time`就可以省略了。\n\n## 测试\n\n因为我已经有完整的代码了,所以运行后,pr中会有一个回复,如图:\n\n![](https://github.com/taoliujun/blog/assets/5689134/42396a84-b798-4f4e-9f39-5bf92a8acb15)\n\n\n# ./.github/actions/unique-comment\n\n这是一个封装的javascript action,用于对issue创建、更新唯一评论。\n\n## 目录结构\n\n创建目录`./.github/actions/unique-comment`,最终目录结构如下:\n\n```bash\n.\n├── action.yml\n├── config\n│   └── webpack.config.js\n├── dist\n│   ├── index.js\n│   └── index.js.LICENSE.txt\n├── package.json\n└── src\n └── index.js\n```\n\n## action.yml\n\n这是action的配置文件,必须存在,内容如下:\n\n```yaml\nname: unique-comment\ndescription: create or update a unique comment\n\nruns:\n using: 'node20'\n main: './dist/index.js'\n\ninputs:\n token:\n description: 'GitHub token'\n required: false\n default: ${{ github.token }}\n owner:\n description: 'Repository owner'\n required: false\n default: ${{ github.event.repository.owner.login }}\n repo:\n description: 'Repository name'\n required: false\n default: ${{ github.event.repository.name }}\n issue_number:\n description: 'Issue number'\n required: false\n default: ${{ github.event.number }}\n body:\n description: 'Comment body'\n required: false\n uniqueIdentifier:\n description: 'Unique identifier for comment'\n required: false\n default: 'unique-comment'\n```\n\n大部分属性不一一细讲了,都是简单的英文望文生义即可。\n\n`runs`表示运行在`node20`环境下,入口文件为`./dist/index.js`。\n\n`inputs`表示接受的参数,也就是之前提到的`with`属性里要输入的参数。用`required`表示是否必须传入,`default`表示默认值。\n\n## src/index.js\n\n为什么入口文件是`dist/index.js`,而不是`src/index.js`呢?因为要引用一些github官方提供的快捷操作github REST API的js包去操作issue评论(pull request也是一种issue),最终打包后的文件才能在工作流中稳妥的运行。所以,写好`src/index.js`,再打包就行。\n\n该文件代码如下:\n\n```javascript\nconst core = require('@actions/core');\nconst github = require('@actions/github');\n\nconst main = async () => {\n const token = core.getInput('token');\n const owner = core.getInput('owner');\n const repo = core.getInput('repo');\n const issueNumber = core.getInput('issue_number');\n const uniqueIdentifier = `[^uniqueIdentifier]: ${core.getInput('uniqueIdentifier')}`;\n const body = `${core.getInput('body')}\\n\\n${uniqueIdentifier}`;\n\n core.debug(`uniqueIdentifier is ${uniqueIdentifier}`);\n\n const octokit = github.getOctokit(token);\n\n const comments = await octokit.rest.issues.listComments({\n owner,\n repo,\n issue_number: issueNumber,\n });\n\n const botComment = comments.data.find((v) => v.body.includes(uniqueIdentifier));\n\n if (botComment) {\n core.info('update comment successfully.');\n await octokit.rest.issues.updateComment({\n owner,\n repo,\n comment_id: botComment.id,\n body,\n });\n } else {\n core.info('create comment successfully.');\n await octokit.rest.issues.createComment({\n owner,\n repo,\n issue_number: issueNumber,\n body,\n });\n }\n};\n\ntry {\n main();\n} catch (err) {\n core.setFailed(err.message);\n}\n```\n\n`@actions/core`和`@actions/github`是github官方提供的js包,前者可以方便的读取入参等,后者可以方便的操作github REST API。\n\n`main`函数的代码就是原生javascript,不一一解释了,主要通过`uniqueIdentifier`来判断是否发布过评论,如果是,就更新评论,否则就创建评论。\n\n> markdown语法`[^uniqueIdentifier]`表示脚注,不会被渲染。\n\n`core.setFailed(err.message);`表示抛出退出代码。\n\n## config/webpack.config.js\n\n打包用的,配置简单可用即可:\n\n```javascript\nmodule.exports = {\n mode: 'production',\n target: 'node20',\n entry: './src/index.js',\n output: {\n filename: 'index.js',\n clean: true,\n },\n};\n```\n\n## package.json\n\n```json\n{\n \"name\": \"unique-comment\",\n \"version\": \"1.0.0\",\n \"private\": true,\n \"scripts\": {\n \"build\": \"webpack --config ./config/webpack.config.js\"\n },\n \"dependencies\": {\n \"@actions/core\": \"^1.10.1\",\n \"@actions/github\": \"^6.0.0\"\n },\n \"devDependencies\": {\n \"webpack\": \"^5.89.0\",\n \"webpack-cli\": \"^5.1.4\"\n }\n}\n```\n\n没啥好说的,列出了依赖项。和一个打包脚本。\n\n## 测试\n\n修改了`src/index.js`得`build`,然后push到github仓库。\n\n记得将**dist**目录也提交到github仓库。\n\n\n\n# init\n\n现在,开始搞正经的了。\n\n先初始化项目,这个job的目的仅仅是为了缓存pnpm依赖项,如果你的项目的依赖项不经常更新,可以省略这个job,后续也不要`needs`这个job。\n\n将init改成如下:\n\n```yaml\ninit:\n runs-on: ubuntu-latest\n steps:\n - name: Init repo\n uses: actions/checkout@v4\n with:\n ref: ${{github.head_ref}}\n\n - name: Init pnpm\n uses: pnpm/action-setup@v2\n with:\n version: 8\n\n - name: Init node\n uses: actions/setup-node@v4\n with:\n node-version: 20\n cache: 'pnpm'\n\n - name: Install dependencies\n run: pnpm install\n```\n\n相信经过对之前的job的了解,这里的配置就看起来很简单了。\n\n## Init pnpm\n\n使用第三方action,安装pnpm@^8。\n\n## Init node\n\n`cache: 'pnpm'`指定缓存机制,它内部是利用了workflow的cache机制。\n\n## Install dependencies\n\n安装依赖项,触发缓存。\n\n\n# eslint\n\n将eslint改成如下:\n\n```yaml\neslint:\n runs-on: ubuntu-latest\n needs: [init]\n outputs:\n result: ${{ steps.lint.outputs.result }}\n steps:\n - name: Init repo\n uses: actions/checkout@v4\n with:\n ref: ${{github.head_ref}}\n fetch-depth: 0\n\n - name: Init pnpm\n uses: pnpm/action-setup@v2\n with:\n version: 8\n\n - name: Init node\n uses: actions/setup-node@v4\n with:\n node-version: 20\n cache: 'pnpm'\n\n - name: Install dependencies\n run: pnpm install\n\n - name: Run eslint\n id: lint\n uses: actions/github-script@v7\n with:\n result-encoding: string\n script: |\n let output = '';\n let outerr = '';\n let diffFiles = '';\n\n await exec.exec(\n `git diff --name-only origin/${{github.base_ref}}`,\n [],\n {\n // silent: true,\n // ignoreReturnCode: true,\n listeners: {\n stdout: (data) => {\n diffFiles += data.toString();\n },\n },\n }\n );\n\n const lintFiles = diffFiles.split(`\\n`).filter((file) => {\n return file.endsWith('.js') || file.endsWith('.ts') || file.endsWith('.tsx')\n }).join(' ');\n\n await exec.exec(\n // \"pnpm run lint --format stylish\",\n `pnpm eslint ${lintFiles}`,\n [],\n {\n // silent: true,\n ignoreReturnCode: true,\n listeners: {\n stdout: (data) => {\n output += data.toString();\n },\n stderr: (data) => {\n outerr += data.toString();\n },\n },\n }\n );\n\n if (outerr) {\n return `:x: Some command execution errors, non-eslint business errors.`;\n }\n\n const errorMatch = output.match(/(\\d+) errors?/);\n const warnMatch = output.match(/(\\d+) warnings?/);\n\n if (errorMatch && errorMatch?.[1] !== '0') {\n return `:x: ${errorMatch?.[0]} ${warnMatch?.[0]}`;\n }\n\n return `:white_check_mark: ${errorMatch?.[0] || '0 error'} ${warnMatch?.[0] || '0 warning'}`;\n```\n\n## needs\n\n使用`needs`依赖init,可以使用到pnpm的缓存项,防止install太慢。\n\n> 因为eslint、typescript、unitTest都需要pnpm install,所以一个前置的init去缓存pnpm依赖项,可以加快后续的install速度。\n\n## outputs\n\njob里的outputs,可以在依赖它的其他job中访问到。这里使用`${{ steps.lint.outputs.result }}`去获取该job中lint这个step里的output里的result。\n\n> output有job和step两个维度,注意区分。\n\n\n## Run eslint\n\n它uses了`actions/github-script@v7`,这是github官方提供的一个action,可以在`with.script`里写js代码去执行,同时它会注入一些变量到script中去,见它的[官方文档](https://github.com/actions/github-script/tree/v7/)。\n\n> 对于简单的js代码,可以使用这个action去完成,不用再去写一个js文件。\n\n`result-encoding`是指定script返回的数据格式的,默认是json,这指定为string。\n\n> 为什么script里return了string,还要指定为string呢?\n> 因为`return 'hello'`在json encode后是`'\"hello\"'`,而string encode后为`'hello'`。\n\nscript里是原生的js代码了,里面的`exec`是该action注入的变量,用来执行shell命令。\n\n这段js代码做了两个事情,一是`git diff`获取pr中改动的文件列表,二是`eslint`检查这些增量文件,最后返回处理的结果。\n\n## fetch-depth\n\nInit repo这个step里设置了`fetch-depth: 0`,不然获取不到完整的git分支,具体看`actions/checkout`的解释,涉及到git的知识不展开细说了。\n\n## steps.lint.outputs.result\n\n`steps.lint.outputs.result`为什么能拿到lint step里的output.result呢?因为`actions/github-script`这个action内部将script的返回值,设置到`$GITHUB_OUTPUT`里了,且键名为`result`。\n\n\n# typescript\n\n和eslint的配置大同小异,只是改了对检测结果的判断。\n\n```yaml\ntypescript:\n runs-on: ubuntu-latest\n needs: [init]\n outputs:\n result: ${{ steps.lint.outputs.result }}\n steps:\n - name: Init repo\n uses: actions/checkout@v4\n with:\n ref: ${{github.head_ref}}\n\n - name: Init pnpm\n uses: pnpm/action-setup@v2\n with:\n version: 8\n\n - name: Init node\n uses: actions/setup-node@v4\n with:\n node-version: 20\n cache: 'pnpm'\n\n - name: Install dependencies\n run: pnpm install\n\n - name: Run lint\n id: lint\n uses: actions/github-script@v7\n with:\n result-encoding: string\n script: |\n let output = '';\n let outerr = '';\n\n await exec.exec(\n `pnpm run -r lint:ts`,\n [],\n {\n // silent: true,\n ignoreReturnCode: true,\n listeners: {\n stdout: (data) => {\n output += data.toString();\n },\n stderr: (data) => {\n outerr += data.toString();\n },\n },\n }\n );\n\n if (outerr) {\n return `:x: Some command execution errors, no business errors.`;\n }\n\n const errorMatch = output.match(/error TS/g);\n\n if (errorMatch) {\n return `:x: ${errorMatch?.length} errors`;\n }\n\n return `:white_check_mark: ${'0 error'}`;\n```\n\n\n# unitTest\n\n和eslint的配置大同小异,只是改了对检测结果的判断。唯一的区别是jest的检测结果是输出到stderr,见https://github.com/jestjs/jest/issues/5064。\n\n```yaml\nunitTest:\n runs-on: ubuntu-latest\n needs: [init]\n outputs:\n result: ${{ steps.lint.outputs.result }}\n steps:\n - name: Init repo\n uses: actions/checkout@v4\n with:\n ref: ${{github.head_ref}}\n\n - name: Init pnpm\n uses: pnpm/action-setup@v2\n with:\n version: 8\n\n - name: Init node\n uses: actions/setup-node@v4\n with:\n node-version: 20\n cache: 'pnpm'\n\n - name: Install dependencies\n run: |\n pnpm remove @nike/eslint-multi-formatter || true\n pnpm remove @nike/svg-packer || true\n pnpm install\n\n - name: Run lint\n id: lint\n uses: actions/github-script@v7\n with:\n result-encoding: string\n script: |\n let output = '';\n let outerr = '';\n\n await exec.exec(\n `pnpm run test`,\n [],\n {\n // silent: true,\n ignoreReturnCode: true,\n listeners: {\n stdout: (data) => {\n output += data.toString();\n },\n stderr: (data) => {\n outerr += data.toString();\n },\n },\n }\n );\n\n // why use outerr? https://github.com/jestjs/jest/issues/5064\n\n const failMatch = outerr.match(/Test Suites: \\d+ failed/);\n\n if (failMatch) {\n return `:x: ${failMatch?.[0]}`;\n }\n\n const errorMatch = outerr.match(/Jest: \"global\" coverage threshold for lines \\([0-9\\.]+%\\) not met: [0-9\\.]+%/);\n\n if (errorMatch) {\n return `:x: ${errorMatch?.[0]}`;\n }\n\n return `:white_check_mark: passed`;\n```\n\n\n# replyResult\n\n最后,将几个检测的结果进行汇总,回复到pr里就行了。\n\n```yaml\nreplyResult:\n runs-on: ubuntu-latest\n needs: [replyChecking, eslint, typescript, unitTest]\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n with:\n ref: ${{github.head_ref}}\n - name: Get date time\n id: getDateTime\n run: echo \"result=$(TZ=Asia/Shanghai date)\" >> \"$GITHUB_OUTPUT\"\n - name: Create or update a comment\n uses: ./.github/actions/unique-comment\n with:\n uniqueIdentifier: ${{ github.workflow }}\n body: |\n ## Eslint Check Result\n\n ${{needs.eslint.outputs.result}}\n\n ## Typescript Check Result\n\n ${{needs.typescript.outputs.result}}\n\n ## UnitTest Check Result\n\n ${{needs.unitTest.outputs.result}}\n\n ---\n\n Commented by Action [${{github.workflow}}](${{github.event.repository.html_url}}/actions/runs/${{github.run_id}}), last updated on ${{steps.getDateTime.outputs.result}}.\n```\n\n和replyChecking差不多,在body里使用`${{needs.eslint.outputs.result}}`去读取了eslint job的outputs。\n\n## 测试\n\n去发起新的pr,故意提交一个有eslint error的js/ts文件,看看表现吧~\n\n","slug":"github-actions-sample-eslint-in-pull-request","published":1,"updated":"2024-01-03T09:13:36.691Z","comments":1,"layout":"post","photos":[],"link":"","_id":"clqxk9r600000joor84vf3yg8","content":"

原文链接:https://github.com/taoliujun/blog/issues/36

\n\n\n

一个在pull request发起的时候执行eslint检测的workflow,点此查看完整代码,它实现的功能如下:

\n
    \n
  • 在pull request创建、更新的时候执行。
  • \n
  • 先回复一个评论,告诉用户正在运行。
  • \n
  • 初始化仓库,并安装依赖,产生依赖缓存。
  • \n
  • 运行eslint增量检查。
  • \n
  • 运行typescript检查。
  • \n
  • 运行jest检查。
  • \n
  • 更新之前的评论,回复检查的结果。
  • \n
\n

运行截图:

\n

\"Alt

\n

为避免歧义,涉及到github action的术语都是英文的。术语介绍如下:

\n
    \n
  • workflow,工作流,可以理解为yml文件。
  • \n
  • jobs,工作,一个workflow可以包含多个job,并行执行。
  • \n
  • steps,作业,一个job可以包含多个step,串行执行。
  • \n
  • action,操作,作业中具体的执行。
  • \n
\n

步骤

\n\n\n

初始化workflow

在项目中新建文件.github/workflows/check-pull-request.yml,内容如下:

\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
name: test check pull request
run-name: 'check pull request #${{ github.event.pull_request.number }}'
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
replyChecking:
runs-on: ubuntu-latest
steps:
- run: echo 'replyChecking'

init:
runs-on: ubuntu-latest
steps:
- run: echo 'init'

eslint:
runs-on: ubuntu-latest
needs: [init]
steps:
- run: echo 'eslint'

typescript:
runs-on: ubuntu-latest
needs: [init]
steps:
- run: echo 'typescript'

unitTest:
runs-on: ubuntu-latest
needs: [init]
steps:
- run: echo 'unitTest'

replyResult:
runs-on: ubuntu-latest
needs: [replyChecking, eslint, typescript, unitTest]
steps:
- run: echo 'replyResult'
\n\n

name和run-name

给workflow命名为check pull request,它会出现在Actions页面的左侧菜单中。运行实例名为check pull request #44,出现在右侧的运行列表中。如图:

\n

\n

run-name中的${{ github.event.pull_request.number }}是workflow的上下文,这里读取了上下文中的pr编号。

\n

on

on指定了workflow的触发条件,这里配置了在pr创建、同步、重新打开的时候,触发该workflow。

\n

jobs

按照设想,需要定义几个job,分别是:

\n
    \n
  • replyChecking:回复用户正在检查中
  • \n
  • init:初始化仓库,缓存依赖项
  • \n
  • eslint:运行eslint检查
  • \n
  • typescript:运行typescript检查
  • \n
  • unitTest:运行单元测试
  • \n
  • replyResult:回复用户检查结果
  • \n
\n

jobs是并行运行的,聪明如你肯定发现了,eslint、typescript、unitTest这三个job会涉及到安装npm依赖,所以它们最好在init后执行,确保依赖已经缓存了。

\n

其次,replyResult肯定要拿到eslint等job的结果才能执行,所以使用了needs管理它们的执行依赖关系。

\n

runs-on

每个job都运行在独立的容器中,github官方提供了windows、macos、linux多种容器,这里使用了ubuntu容器。

\n

测试

发起一个pr,看到Actions页面出现了新的运行实例,点击进去,可以看到各个job的运行情况和依赖关系:

\n

\n\n\n

replyChecking

在进行eslint检测之前,先在pr里回复checking,并且带上拽酷炫的话。将replyChecking改成如下:

\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
replyChecking:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}
- name: Get date time
id: getDateTime
run: echo "result=$(TZ=Asia/Shanghai date)" >> "$GITHUB_OUTPUT"
- name: Create or update a comment
uses: ./.github/actions/unique-comment
with:
uniqueIdentifier: ${{ github.workflow }}
body: |
**Checking...**

---

Commented by Action [${{github.workflow}}](${{github.event.repository.html_url}}/actions/runs/${{github.run_id}}), last updated on ${{steps.getDateTime.outputs.result}}.
\n\n

steps每一步里nameid是可选的,name在Actions详情页面里会显示,更直观的看到step的名称,推荐写上。

\n

Checkout

uses表示使用一个action,名为actions/checkout@v4,它用来拉取仓库。

\n
\n

同其他编程语言一样,重复的action可以封装起来。action市场提供了很多。

\n
\n

with属性指定了该action的输入参数,每个action的参数不尽相同。

\n

ref参数表示要拉取的分支,${{github.head_ref}}也是一个上下文,表示当前pr的源分支。

\n

Get Date time

这step还写了id,表示该step在该job中的唯一标识,为什么要写呢?是为了下一步step能根据id读取到它的output

\n
\n

output是workflow中非常重要的概念,它用于在step之间、job之间分享简单的数据。

\n
\n

run就是在容器中跑一个命令,这里跑了一个unix bash命令,将当前时间写入到$GITHUB_OUTPUT中,键名为result

\n
\n

$GITHUB_OUTPUT是workflow注入到容器中的一个路径,用于存放output。

\n
\n

Create or update a comment

uses使用了本地的action,这个action用于创建或更新一个唯一回复,下一节说。

\n
\n

有时候,官方或市场的action并不能满足你的需要,就得自己写一个了。

\n
\n

同理,该action也有with属性,uniqueIdentifier是回复评论的唯一标识,body是回复的内容,内容使用了markdown语法,里面还涉及到上下文不一一细讲了。只说${{steps.getDateTime.outputs.result}}这个上下文表示获取getDateTime这个step中,键名为result的值。

\n

如果你不需要在内容里插入时间,那么上面的Get Date time就可以省略了。

\n

测试

因为我已经有完整的代码了,所以运行后,pr中会有一个回复,如图:

\n

\n\n\n

./.github/actions/unique-comment

这是一个封装的javascript action,用于对issue创建、更新唯一评论。

\n

目录结构

创建目录./.github/actions/unique-comment,最终目录结构如下:

\n
1
2
3
4
5
6
7
8
9
10
.
├── action.yml
├── config
│   └── webpack.config.js
├── dist
│   ├── index.js
│   └── index.js.LICENSE.txt
├── package.json
└── src
└── index.js
\n\n

action.yml

这是action的配置文件,必须存在,内容如下:

\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
name: unique-comment
description: create or update a unique comment

runs:
using: 'node20'
main: './dist/index.js'

inputs:
token:
description: 'GitHub token'
required: false
default: ${{ github.token }}
owner:
description: 'Repository owner'
required: false
default: ${{ github.event.repository.owner.login }}
repo:
description: 'Repository name'
required: false
default: ${{ github.event.repository.name }}
issue_number:
description: 'Issue number'
required: false
default: ${{ github.event.number }}
body:
description: 'Comment body'
required: false
uniqueIdentifier:
description: 'Unique identifier for comment'
required: false
default: 'unique-comment'
\n\n

大部分属性不一一细讲了,都是简单的英文望文生义即可。

\n

runs表示运行在node20环境下,入口文件为./dist/index.js

\n

inputs表示接受的参数,也就是之前提到的with属性里要输入的参数。用required表示是否必须传入,default表示默认值。

\n

src/index.js

为什么入口文件是dist/index.js,而不是src/index.js呢?因为要引用一些github官方提供的快捷操作github REST API的js包去操作issue评论(pull request也是一种issue),最终打包后的文件才能在工作流中稳妥的运行。所以,写好src/index.js,再打包就行。

\n

该文件代码如下:

\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const core = require('@actions/core');
const github = require('@actions/github');

const main = async () => {
const token = core.getInput('token');
const owner = core.getInput('owner');
const repo = core.getInput('repo');
const issueNumber = core.getInput('issue_number');
const uniqueIdentifier = `[^uniqueIdentifier]: ${core.getInput('uniqueIdentifier')}`;
const body = `${core.getInput('body')}\\n\\n${uniqueIdentifier}`;

core.debug(`uniqueIdentifier is ${uniqueIdentifier}`);

const octokit = github.getOctokit(token);

const comments = await octokit.rest.issues.listComments({
owner,
repo,
issue_number: issueNumber,
});

const botComment = comments.data.find((v) => v.body.includes(uniqueIdentifier));

if (botComment) {
core.info('update comment successfully.');
await octokit.rest.issues.updateComment({
owner,
repo,
comment_id: botComment.id,
body,
});
} else {
core.info('create comment successfully.');
await octokit.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body,
});
}
};

try {
main();
} catch (err) {
core.setFailed(err.message);
}
\n\n

@actions/core@actions/github是github官方提供的js包,前者可以方便的读取入参等,后者可以方便的操作github REST API。

\n

main函数的代码就是原生javascript,不一一解释了,主要通过uniqueIdentifier来判断是否发布过评论,如果是,就更新评论,否则就创建评论。

\n
\n

markdown语法[^uniqueIdentifier]表示脚注,不会被渲染。

\n
\n

core.setFailed(err.message);表示抛出退出代码。

\n

config/webpack.config.js

打包用的,配置简单可用即可:

\n
1
2
3
4
5
6
7
8
9
module.exports = {
mode: 'production',
target: 'node20',
entry: './src/index.js',
output: {
filename: 'index.js',
clean: true,
},
};
\n\n

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "unique-comment",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "webpack --config ./config/webpack.config.js"
},
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/github": "^6.0.0"
},
"devDependencies": {
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4"
}
}
\n\n

没啥好说的,列出了依赖项。和一个打包脚本。

\n

测试

修改了src/index.jsbuild,然后push到github仓库。

\n

记得将dist目录也提交到github仓库。

\n\n\n

init

现在,开始搞正经的了。

\n

先初始化项目,这个job的目的仅仅是为了缓存pnpm依赖项,如果你的项目的依赖项不经常更新,可以省略这个job,后续也不要needs这个job。

\n

将init改成如下:

\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
init:
runs-on: ubuntu-latest
steps:
- name: Init repo
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}

- name: Init pnpm
uses: pnpm/action-setup@v2
with:
version: 8

- name: Init node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'

- name: Install dependencies
run: pnpm install
\n\n

相信经过对之前的job的了解,这里的配置就看起来很简单了。

\n

Init pnpm

使用第三方action,安装pnpm@^8。

\n

Init node

cache: 'pnpm'指定缓存机制,它内部是利用了workflow的cache机制。

\n

Install dependencies

安装依赖项,触发缓存。

\n\n\n

eslint

将eslint改成如下:

\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
eslint:
runs-on: ubuntu-latest
needs: [init]
outputs:
result: ${{ steps.lint.outputs.result }}
steps:
- name: Init repo
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}
fetch-depth: 0

- name: Init pnpm
uses: pnpm/action-setup@v2
with:
version: 8

- name: Init node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'

- name: Install dependencies
run: pnpm install

- name: Run eslint
id: lint
uses: actions/github-script@v7
with:
result-encoding: string
script: |
let output = '';
let outerr = '';
let diffFiles = '';

await exec.exec(
`git diff --name-only origin/${{github.base_ref}}`,
[],
{
// silent: true,
// ignoreReturnCode: true,
listeners: {
stdout: (data) => {
diffFiles += data.toString();
},
},
}
);

const lintFiles = diffFiles.split(`\\n`).filter((file) => {
return file.endsWith('.js') || file.endsWith('.ts') || file.endsWith('.tsx')
}).join(' ');

await exec.exec(
// "pnpm run lint --format stylish",
`pnpm eslint ${lintFiles}`,
[],
{
// silent: true,
ignoreReturnCode: true,
listeners: {
stdout: (data) => {
output += data.toString();
},
stderr: (data) => {
outerr += data.toString();
},
},
}
);

if (outerr) {
return `:x: Some command execution errors, non-eslint business errors.`;
}

const errorMatch = output.match(/(\\d+) errors?/);
const warnMatch = output.match(/(\\d+) warnings?/);

if (errorMatch && errorMatch?.[1] !== '0') {
return `:x: ${errorMatch?.[0]} ${warnMatch?.[0]}`;
}

return `:white_check_mark: ${errorMatch?.[0] || '0 error'} ${warnMatch?.[0] || '0 warning'}`;
\n\n

needs

使用needs依赖init,可以使用到pnpm的缓存项,防止install太慢。

\n
\n

因为eslint、typescript、unitTest都需要pnpm install,所以一个前置的init去缓存pnpm依赖项,可以加快后续的install速度。

\n
\n

outputs

job里的outputs,可以在依赖它的其他job中访问到。这里使用${{ steps.lint.outputs.result }}去获取该job中lint这个step里的output里的result。

\n
\n

output有job和step两个维度,注意区分。

\n
\n

Run eslint

它uses了actions/github-script@v7,这是github官方提供的一个action,可以在with.script里写js代码去执行,同时它会注入一些变量到script中去,见它的官方文档

\n
\n

对于简单的js代码,可以使用这个action去完成,不用再去写一个js文件。

\n
\n

result-encoding是指定script返回的数据格式的,默认是json,这指定为string。

\n
\n

为什么script里return了string,还要指定为string呢?
因为return 'hello'在json encode后是'"hello"',而string encode后为'hello'

\n
\n

script里是原生的js代码了,里面的exec是该action注入的变量,用来执行shell命令。

\n

这段js代码做了两个事情,一是git diff获取pr中改动的文件列表,二是eslint检查这些增量文件,最后返回处理的结果。

\n

fetch-depth

Init repo这个step里设置了fetch-depth: 0,不然获取不到完整的git分支,具体看actions/checkout的解释,涉及到git的知识不展开细说了。

\n

steps.lint.outputs.result

steps.lint.outputs.result为什么能拿到lint step里的output.result呢?因为actions/github-script这个action内部将script的返回值,设置到$GITHUB_OUTPUT里了,且键名为result

\n\n\n

typescript

和eslint的配置大同小异,只是改了对检测结果的判断。

\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
typescript:
runs-on: ubuntu-latest
needs: [init]
outputs:
result: ${{ steps.lint.outputs.result }}
steps:
- name: Init repo
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}

- name: Init pnpm
uses: pnpm/action-setup@v2
with:
version: 8

- name: Init node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'

- name: Install dependencies
run: pnpm install

- name: Run lint
id: lint
uses: actions/github-script@v7
with:
result-encoding: string
script: |
let output = '';
let outerr = '';

await exec.exec(
`pnpm run -r lint:ts`,
[],
{
// silent: true,
ignoreReturnCode: true,
listeners: {
stdout: (data) => {
output += data.toString();
},
stderr: (data) => {
outerr += data.toString();
},
},
}
);

if (outerr) {
return `:x: Some command execution errors, no business errors.`;
}

const errorMatch = output.match(/error TS/g);

if (errorMatch) {
return `:x: ${errorMatch?.length} errors`;
}

return `:white_check_mark: ${'0 error'}`;
\n\n\n

unitTest

和eslint的配置大同小异,只是改了对检测结果的判断。唯一的区别是jest的检测结果是输出到stderr,见https://github.com/jestjs/jest/issues/5064。

\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
unitTest:
runs-on: ubuntu-latest
needs: [init]
outputs:
result: ${{ steps.lint.outputs.result }}
steps:
- name: Init repo
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}

- name: Init pnpm
uses: pnpm/action-setup@v2
with:
version: 8

- name: Init node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'

- name: Install dependencies
run: |
pnpm remove @nike/eslint-multi-formatter || true
pnpm remove @nike/svg-packer || true
pnpm install

- name: Run lint
id: lint
uses: actions/github-script@v7
with:
result-encoding: string
script: |
let output = '';
let outerr = '';

await exec.exec(
`pnpm run test`,
[],
{
// silent: true,
ignoreReturnCode: true,
listeners: {
stdout: (data) => {
output += data.toString();
},
stderr: (data) => {
outerr += data.toString();
},
},
}
);

// why use outerr? https://github.com/jestjs/jest/issues/5064

const failMatch = outerr.match(/Test Suites: \\d+ failed/);

if (failMatch) {
return `:x: ${failMatch?.[0]}`;
}

const errorMatch = outerr.match(/Jest: "global" coverage threshold for lines \\([0-9\\.]+%\\) not met: [0-9\\.]+%/);

if (errorMatch) {
return `:x: ${errorMatch?.[0]}`;
}

return `:white_check_mark: passed`;
\n\n\n

replyResult

最后,将几个检测的结果进行汇总,回复到pr里就行了。

\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
replyResult:
runs-on: ubuntu-latest
needs: [replyChecking, eslint, typescript, unitTest]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}
- name: Get date time
id: getDateTime
run: echo "result=$(TZ=Asia/Shanghai date)" >> "$GITHUB_OUTPUT"
- name: Create or update a comment
uses: ./.github/actions/unique-comment
with:
uniqueIdentifier: ${{ github.workflow }}
body: |
## Eslint Check Result

${{needs.eslint.outputs.result}}

## Typescript Check Result

${{needs.typescript.outputs.result}}

## UnitTest Check Result

${{needs.unitTest.outputs.result}}

---

Commented by Action [${{github.workflow}}](${{github.event.repository.html_url}}/actions/runs/${{github.run_id}}), last updated on ${{steps.getDateTime.outputs.result}}.
\n\n

和replyChecking差不多,在body里使用${{needs.eslint.outputs.result}}去读取了eslint job的outputs。

\n

测试

去发起新的pr,故意提交一个有eslint error的js/ts文件,看看表现吧~

\n","site":{"data":{}},"excerpt":"","more":"

原文链接:https://github.com/taoliujun/blog/issues/36

\n\n\n

一个在pull request发起的时候执行eslint检测的workflow,点此查看完整代码,它实现的功能如下:

\n
    \n
  • 在pull request创建、更新的时候执行。
  • \n
  • 先回复一个评论,告诉用户正在运行。
  • \n
  • 初始化仓库,并安装依赖,产生依赖缓存。
  • \n
  • 运行eslint增量检查。
  • \n
  • 运行typescript检查。
  • \n
  • 运行jest检查。
  • \n
  • 更新之前的评论,回复检查的结果。
  • \n
\n

运行截图:

\n

\"Alt

\n

为避免歧义,涉及到github action的术语都是英文的。术语介绍如下:

\n
    \n
  • workflow,工作流,可以理解为yml文件。
  • \n
  • jobs,工作,一个workflow可以包含多个job,并行执行。
  • \n
  • steps,作业,一个job可以包含多个step,串行执行。
  • \n
  • action,操作,作业中具体的执行。
  • \n
\n

步骤

\n\n\n

初始化workflow

在项目中新建文件.github/workflows/check-pull-request.yml,内容如下:

\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
name: test check pull request
run-name: 'check pull request #${{ github.event.pull_request.number }}'
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
replyChecking:
runs-on: ubuntu-latest
steps:
- run: echo 'replyChecking'

init:
runs-on: ubuntu-latest
steps:
- run: echo 'init'

eslint:
runs-on: ubuntu-latest
needs: [init]
steps:
- run: echo 'eslint'

typescript:
runs-on: ubuntu-latest
needs: [init]
steps:
- run: echo 'typescript'

unitTest:
runs-on: ubuntu-latest
needs: [init]
steps:
- run: echo 'unitTest'

replyResult:
runs-on: ubuntu-latest
needs: [replyChecking, eslint, typescript, unitTest]
steps:
- run: echo 'replyResult'
\n\n

name和run-name

给workflow命名为check pull request,它会出现在Actions页面的左侧菜单中。运行实例名为check pull request #44,出现在右侧的运行列表中。如图:

\n

\n

run-name中的${{ github.event.pull_request.number }}是workflow的上下文,这里读取了上下文中的pr编号。

\n

on

on指定了workflow的触发条件,这里配置了在pr创建、同步、重新打开的时候,触发该workflow。

\n

jobs

按照设想,需要定义几个job,分别是:

\n
    \n
  • replyChecking:回复用户正在检查中
  • \n
  • init:初始化仓库,缓存依赖项
  • \n
  • eslint:运行eslint检查
  • \n
  • typescript:运行typescript检查
  • \n
  • unitTest:运行单元测试
  • \n
  • replyResult:回复用户检查结果
  • \n
\n

jobs是并行运行的,聪明如你肯定发现了,eslint、typescript、unitTest这三个job会涉及到安装npm依赖,所以它们最好在init后执行,确保依赖已经缓存了。

\n

其次,replyResult肯定要拿到eslint等job的结果才能执行,所以使用了needs管理它们的执行依赖关系。

\n

runs-on

每个job都运行在独立的容器中,github官方提供了windows、macos、linux多种容器,这里使用了ubuntu容器。

\n

测试

发起一个pr,看到Actions页面出现了新的运行实例,点击进去,可以看到各个job的运行情况和依赖关系:

\n

\n\n\n

replyChecking

在进行eslint检测之前,先在pr里回复checking,并且带上拽酷炫的话。将replyChecking改成如下:

\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
replyChecking:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}
- name: Get date time
id: getDateTime
run: echo "result=$(TZ=Asia/Shanghai date)" >> "$GITHUB_OUTPUT"
- name: Create or update a comment
uses: ./.github/actions/unique-comment
with:
uniqueIdentifier: ${{ github.workflow }}
body: |
**Checking...**

---

Commented by Action [${{github.workflow}}](${{github.event.repository.html_url}}/actions/runs/${{github.run_id}}), last updated on ${{steps.getDateTime.outputs.result}}.
\n\n

steps每一步里nameid是可选的,name在Actions详情页面里会显示,更直观的看到step的名称,推荐写上。

\n

Checkout

uses表示使用一个action,名为actions/checkout@v4,它用来拉取仓库。

\n
\n

同其他编程语言一样,重复的action可以封装起来。action市场提供了很多。

\n
\n

with属性指定了该action的输入参数,每个action的参数不尽相同。

\n

ref参数表示要拉取的分支,${{github.head_ref}}也是一个上下文,表示当前pr的源分支。

\n

Get Date time

这step还写了id,表示该step在该job中的唯一标识,为什么要写呢?是为了下一步step能根据id读取到它的output

\n
\n

output是workflow中非常重要的概念,它用于在step之间、job之间分享简单的数据。

\n
\n

run就是在容器中跑一个命令,这里跑了一个unix bash命令,将当前时间写入到$GITHUB_OUTPUT中,键名为result

\n
\n

$GITHUB_OUTPUT是workflow注入到容器中的一个路径,用于存放output。

\n
\n

Create or update a comment

uses使用了本地的action,这个action用于创建或更新一个唯一回复,下一节说。

\n
\n

有时候,官方或市场的action并不能满足你的需要,就得自己写一个了。

\n
\n

同理,该action也有with属性,uniqueIdentifier是回复评论的唯一标识,body是回复的内容,内容使用了markdown语法,里面还涉及到上下文不一一细讲了。只说${{steps.getDateTime.outputs.result}}这个上下文表示获取getDateTime这个step中,键名为result的值。

\n

如果你不需要在内容里插入时间,那么上面的Get Date time就可以省略了。

\n

测试

因为我已经有完整的代码了,所以运行后,pr中会有一个回复,如图:

\n

\n\n\n

./.github/actions/unique-comment

这是一个封装的javascript action,用于对issue创建、更新唯一评论。

\n

目录结构

创建目录./.github/actions/unique-comment,最终目录结构如下:

\n
1
2
3
4
5
6
7
8
9
10
.
├── action.yml
├── config
│   └── webpack.config.js
├── dist
│   ├── index.js
│   └── index.js.LICENSE.txt
├── package.json
└── src
└── index.js
\n\n

action.yml

这是action的配置文件,必须存在,内容如下:

\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
name: unique-comment
description: create or update a unique comment

runs:
using: 'node20'
main: './dist/index.js'

inputs:
token:
description: 'GitHub token'
required: false
default: ${{ github.token }}
owner:
description: 'Repository owner'
required: false
default: ${{ github.event.repository.owner.login }}
repo:
description: 'Repository name'
required: false
default: ${{ github.event.repository.name }}
issue_number:
description: 'Issue number'
required: false
default: ${{ github.event.number }}
body:
description: 'Comment body'
required: false
uniqueIdentifier:
description: 'Unique identifier for comment'
required: false
default: 'unique-comment'
\n\n

大部分属性不一一细讲了,都是简单的英文望文生义即可。

\n

runs表示运行在node20环境下,入口文件为./dist/index.js

\n

inputs表示接受的参数,也就是之前提到的with属性里要输入的参数。用required表示是否必须传入,default表示默认值。

\n

src/index.js

为什么入口文件是dist/index.js,而不是src/index.js呢?因为要引用一些github官方提供的快捷操作github REST API的js包去操作issue评论(pull request也是一种issue),最终打包后的文件才能在工作流中稳妥的运行。所以,写好src/index.js,再打包就行。

\n

该文件代码如下:

\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const core = require('@actions/core');
const github = require('@actions/github');

const main = async () => {
const token = core.getInput('token');
const owner = core.getInput('owner');
const repo = core.getInput('repo');
const issueNumber = core.getInput('issue_number');
const uniqueIdentifier = `[^uniqueIdentifier]: ${core.getInput('uniqueIdentifier')}`;
const body = `${core.getInput('body')}\\n\\n${uniqueIdentifier}`;

core.debug(`uniqueIdentifier is ${uniqueIdentifier}`);

const octokit = github.getOctokit(token);

const comments = await octokit.rest.issues.listComments({
owner,
repo,
issue_number: issueNumber,
});

const botComment = comments.data.find((v) => v.body.includes(uniqueIdentifier));

if (botComment) {
core.info('update comment successfully.');
await octokit.rest.issues.updateComment({
owner,
repo,
comment_id: botComment.id,
body,
});
} else {
core.info('create comment successfully.');
await octokit.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body,
});
}
};

try {
main();
} catch (err) {
core.setFailed(err.message);
}
\n\n

@actions/core@actions/github是github官方提供的js包,前者可以方便的读取入参等,后者可以方便的操作github REST API。

\n

main函数的代码就是原生javascript,不一一解释了,主要通过uniqueIdentifier来判断是否发布过评论,如果是,就更新评论,否则就创建评论。

\n
\n

markdown语法[^uniqueIdentifier]表示脚注,不会被渲染。

\n
\n

core.setFailed(err.message);表示抛出退出代码。

\n

config/webpack.config.js

打包用的,配置简单可用即可:

\n
1
2
3
4
5
6
7
8
9
module.exports = {
mode: 'production',
target: 'node20',
entry: './src/index.js',
output: {
filename: 'index.js',
clean: true,
},
};
\n\n

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "unique-comment",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "webpack --config ./config/webpack.config.js"
},
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/github": "^6.0.0"
},
"devDependencies": {
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4"
}
}
\n\n

没啥好说的,列出了依赖项。和一个打包脚本。

\n

测试

修改了src/index.jsbuild,然后push到github仓库。

\n

记得将dist目录也提交到github仓库。

\n\n\n

init

现在,开始搞正经的了。

\n

先初始化项目,这个job的目的仅仅是为了缓存pnpm依赖项,如果你的项目的依赖项不经常更新,可以省略这个job,后续也不要needs这个job。

\n

将init改成如下:

\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
init:
runs-on: ubuntu-latest
steps:
- name: Init repo
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}

- name: Init pnpm
uses: pnpm/action-setup@v2
with:
version: 8

- name: Init node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'

- name: Install dependencies
run: pnpm install
\n\n

相信经过对之前的job的了解,这里的配置就看起来很简单了。

\n

Init pnpm

使用第三方action,安装pnpm@^8。

\n

Init node

cache: 'pnpm'指定缓存机制,它内部是利用了workflow的cache机制。

\n

Install dependencies

安装依赖项,触发缓存。

\n\n\n

eslint

将eslint改成如下:

\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
eslint:
runs-on: ubuntu-latest
needs: [init]
outputs:
result: ${{ steps.lint.outputs.result }}
steps:
- name: Init repo
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}
fetch-depth: 0

- name: Init pnpm
uses: pnpm/action-setup@v2
with:
version: 8

- name: Init node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'

- name: Install dependencies
run: pnpm install

- name: Run eslint
id: lint
uses: actions/github-script@v7
with:
result-encoding: string
script: |
let output = '';
let outerr = '';
let diffFiles = '';

await exec.exec(
`git diff --name-only origin/${{github.base_ref}}`,
[],
{
// silent: true,
// ignoreReturnCode: true,
listeners: {
stdout: (data) => {
diffFiles += data.toString();
},
},
}
);

const lintFiles = diffFiles.split(`\\n`).filter((file) => {
return file.endsWith('.js') || file.endsWith('.ts') || file.endsWith('.tsx')
}).join(' ');

await exec.exec(
// "pnpm run lint --format stylish",
`pnpm eslint ${lintFiles}`,
[],
{
// silent: true,
ignoreReturnCode: true,
listeners: {
stdout: (data) => {
output += data.toString();
},
stderr: (data) => {
outerr += data.toString();
},
},
}
);

if (outerr) {
return `:x: Some command execution errors, non-eslint business errors.`;
}

const errorMatch = output.match(/(\\d+) errors?/);
const warnMatch = output.match(/(\\d+) warnings?/);

if (errorMatch && errorMatch?.[1] !== '0') {
return `:x: ${errorMatch?.[0]} ${warnMatch?.[0]}`;
}

return `:white_check_mark: ${errorMatch?.[0] || '0 error'} ${warnMatch?.[0] || '0 warning'}`;
\n\n

needs

使用needs依赖init,可以使用到pnpm的缓存项,防止install太慢。

\n
\n

因为eslint、typescript、unitTest都需要pnpm install,所以一个前置的init去缓存pnpm依赖项,可以加快后续的install速度。

\n
\n

outputs

job里的outputs,可以在依赖它的其他job中访问到。这里使用${{ steps.lint.outputs.result }}去获取该job中lint这个step里的output里的result。

\n
\n

output有job和step两个维度,注意区分。

\n
\n

Run eslint

它uses了actions/github-script@v7,这是github官方提供的一个action,可以在with.script里写js代码去执行,同时它会注入一些变量到script中去,见它的官方文档

\n
\n

对于简单的js代码,可以使用这个action去完成,不用再去写一个js文件。

\n
\n

result-encoding是指定script返回的数据格式的,默认是json,这指定为string。

\n
\n

为什么script里return了string,还要指定为string呢?
因为return 'hello'在json encode后是'"hello"',而string encode后为'hello'

\n
\n

script里是原生的js代码了,里面的exec是该action注入的变量,用来执行shell命令。

\n

这段js代码做了两个事情,一是git diff获取pr中改动的文件列表,二是eslint检查这些增量文件,最后返回处理的结果。

\n

fetch-depth

Init repo这个step里设置了fetch-depth: 0,不然获取不到完整的git分支,具体看actions/checkout的解释,涉及到git的知识不展开细说了。

\n

steps.lint.outputs.result

steps.lint.outputs.result为什么能拿到lint step里的output.result呢?因为actions/github-script这个action内部将script的返回值,设置到$GITHUB_OUTPUT里了,且键名为result

\n\n\n

typescript

和eslint的配置大同小异,只是改了对检测结果的判断。

\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
typescript:
runs-on: ubuntu-latest
needs: [init]
outputs:
result: ${{ steps.lint.outputs.result }}
steps:
- name: Init repo
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}

- name: Init pnpm
uses: pnpm/action-setup@v2
with:
version: 8

- name: Init node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'

- name: Install dependencies
run: pnpm install

- name: Run lint
id: lint
uses: actions/github-script@v7
with:
result-encoding: string
script: |
let output = '';
let outerr = '';

await exec.exec(
`pnpm run -r lint:ts`,
[],
{
// silent: true,
ignoreReturnCode: true,
listeners: {
stdout: (data) => {
output += data.toString();
},
stderr: (data) => {
outerr += data.toString();
},
},
}
);

if (outerr) {
return `:x: Some command execution errors, no business errors.`;
}

const errorMatch = output.match(/error TS/g);

if (errorMatch) {
return `:x: ${errorMatch?.length} errors`;
}

return `:white_check_mark: ${'0 error'}`;
\n\n\n

unitTest

和eslint的配置大同小异,只是改了对检测结果的判断。唯一的区别是jest的检测结果是输出到stderr,见https://github.com/jestjs/jest/issues/5064。

\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
unitTest:
runs-on: ubuntu-latest
needs: [init]
outputs:
result: ${{ steps.lint.outputs.result }}
steps:
- name: Init repo
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}

- name: Init pnpm
uses: pnpm/action-setup@v2
with:
version: 8

- name: Init node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'

- name: Install dependencies
run: |
pnpm remove @nike/eslint-multi-formatter || true
pnpm remove @nike/svg-packer || true
pnpm install

- name: Run lint
id: lint
uses: actions/github-script@v7
with:
result-encoding: string
script: |
let output = '';
let outerr = '';

await exec.exec(
`pnpm run test`,
[],
{
// silent: true,
ignoreReturnCode: true,
listeners: {
stdout: (data) => {
output += data.toString();
},
stderr: (data) => {
outerr += data.toString();
},
},
}
);

// why use outerr? https://github.com/jestjs/jest/issues/5064

const failMatch = outerr.match(/Test Suites: \\d+ failed/);

if (failMatch) {
return `:x: ${failMatch?.[0]}`;
}

const errorMatch = outerr.match(/Jest: "global" coverage threshold for lines \\([0-9\\.]+%\\) not met: [0-9\\.]+%/);

if (errorMatch) {
return `:x: ${errorMatch?.[0]}`;
}

return `:white_check_mark: passed`;
\n\n\n

replyResult

最后,将几个检测的结果进行汇总,回复到pr里就行了。

\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
replyResult:
runs-on: ubuntu-latest
needs: [replyChecking, eslint, typescript, unitTest]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{github.head_ref}}
- name: Get date time
id: getDateTime
run: echo "result=$(TZ=Asia/Shanghai date)" >> "$GITHUB_OUTPUT"
- name: Create or update a comment
uses: ./.github/actions/unique-comment
with:
uniqueIdentifier: ${{ github.workflow }}
body: |
## Eslint Check Result

${{needs.eslint.outputs.result}}

## Typescript Check Result

${{needs.typescript.outputs.result}}

## UnitTest Check Result

${{needs.unitTest.outputs.result}}

---

Commented by Action [${{github.workflow}}](${{github.event.repository.html_url}}/actions/runs/${{github.run_id}}), last updated on ${{steps.getDateTime.outputs.result}}.
\n\n

和replyChecking差不多,在body里使用${{needs.eslint.outputs.result}}去读取了eslint job的outputs。

\n

测试

去发起新的pr,故意提交一个有eslint error的js/ts文件,看看表现吧~

\n"},{"title":"React公共状态利器 - Zustand","date":"2023-12-12T22:46:26.000Z","url":"react-zustand","_content":"\n\n原文链接:[https://github.com/taoliujun/blog/issues/35](https://github.com/taoliujun/blog/issues/35)\n\n\n\n官方文档:https://docs.pmnd.rs/zustand\n\n# 如何使用\n\n**Zustand** 是一个非常简单粗暴的全局状态管理库,它的使用有多简单呢?如下:\n\n```bash\n> pnpm add zustand\n```\n\n```ts\n// useFormStateStore.ts\nimport { create } from 'zustand';\n\ninterface State {\n loading: boolean;\n disabled: boolean;\n setLoadingByAge: (value: number) => void;\n}\n\nexport const useFormStateStore = create((set) => ({\n loading: false,\n disabled: false,\n setLoadingByAge: (value) => {\n set({ loading: value > 10 });\n },\n}));\n```\n\n```tsx\n// app.tsx\nimport { useState, type FC } from 'react';\nimport { useFormStateStore } from './useFormStateStore';\nimport { Button } from '@/components/Button';\n\nconst Loading: FC = () => {\n const { loading } = useFormStateStore();\n return
loading: {String(loading)}
;\n};\n\nconst Disabled: FC = () => {\n const { disabled } = useFormStateStore();\n return
disabled: {String(disabled)}
;\n};\n\nconst Main: FC = () => {\n const { setLoadingByAge } = useFormStateStore();\n const [age, setAge] = useState(0);\n\n return (\n
\n \n
\n \n
\n {\n useFormStateStore.setState({\n loading: true,\n });\n }}\n >\n set loading true\n \n {\n useFormStateStore.setState({\n loading: false,\n });\n }}\n >\n set loading false\n \n {\n useFormStateStore.setState({\n disabled: true,\n });\n }}\n >\n set disabled true\n \n {\n useFormStateStore.setState({\n disabled: false,\n });\n }}\n >\n set disabled false\n \n
\n {\n setAge(Number(e.target.value));\n }}\n />\n
\n {\n setLoadingByAge(age);\n }}\n >\n set loading by age\n \n
\n );\n};\n\nexport default Main;\n```\n\n在`useFormStateStore.ts`中定义了状态,然后在`app.tsx`中使用,就是这么简单粗暴!这里有几点介绍下:\n\n- 对于简单的状态更新,使用`setState`方法就可以,它的参数是一个对象,这个对象就是你要更新的状态,它会和之前的状态进行合并,然后返回一个新的状态,从而触发组件更新。\n\n- 对于需要通用的逻辑处理的状态更新,参照`useFormStateStore.ts`中的`setLoadingByAge`方法,将它作为状态里的一个方法就行了。\n\n# Zustand\n\n使用非常简单,API也很少,它的原理是使用了`Proxy`,所以它的性能非常好。\n\n# 相比Redux\n\n相比Redux,Zustand的代码非常简单明了,不需要使用`connect`、`mapStateToProps`、`mapDispatchToProps`这些方法。\n\n# 相比React Context\n\nReact Context需要一个`Provider`包裹组件以传递状态,需要一个`useContext`使用状态,光从层级上就让人绕起来了。而Zustand只需要一个`create`方法,就可以使用了,且状态是全局的,不需要传递。\n\n\n\n\n","source":"_posts/react-zustand.md","raw":"---\ntitle: \"React公共状态利器 - Zustand\"\ndate: \"2023-12-13T06:46:26Z\"\ncategories:\n - [React]\n\nurl: react-zustand\ntags:\n - zustand\n - react store\n\n---\n\n\n原文链接:[https://github.com/taoliujun/blog/issues/35](https://github.com/taoliujun/blog/issues/35)\n\n\n\n官方文档:https://docs.pmnd.rs/zustand\n\n# 如何使用\n\n**Zustand** 是一个非常简单粗暴的全局状态管理库,它的使用有多简单呢?如下:\n\n```bash\n> pnpm add zustand\n```\n\n```ts\n// useFormStateStore.ts\nimport { create } from 'zustand';\n\ninterface State {\n loading: boolean;\n disabled: boolean;\n setLoadingByAge: (value: number) => void;\n}\n\nexport const useFormStateStore = create((set) => ({\n loading: false,\n disabled: false,\n setLoadingByAge: (value) => {\n set({ loading: value > 10 });\n },\n}));\n```\n\n```tsx\n// app.tsx\nimport { useState, type FC } from 'react';\nimport { useFormStateStore } from './useFormStateStore';\nimport { Button } from '@/components/Button';\n\nconst Loading: FC = () => {\n const { loading } = useFormStateStore();\n return
loading: {String(loading)}
;\n};\n\nconst Disabled: FC = () => {\n const { disabled } = useFormStateStore();\n return
disabled: {String(disabled)}
;\n};\n\nconst Main: FC = () => {\n const { setLoadingByAge } = useFormStateStore();\n const [age, setAge] = useState(0);\n\n return (\n
\n \n
\n \n
\n {\n useFormStateStore.setState({\n loading: true,\n });\n }}\n >\n set loading true\n \n {\n useFormStateStore.setState({\n loading: false,\n });\n }}\n >\n set loading false\n \n {\n useFormStateStore.setState({\n disabled: true,\n });\n }}\n >\n set disabled true\n \n {\n useFormStateStore.setState({\n disabled: false,\n });\n }}\n >\n set disabled false\n \n
\n {\n setAge(Number(e.target.value));\n }}\n />\n
\n {\n setLoadingByAge(age);\n }}\n >\n set loading by age\n \n
\n );\n};\n\nexport default Main;\n```\n\n在`useFormStateStore.ts`中定义了状态,然后在`app.tsx`中使用,就是这么简单粗暴!这里有几点介绍下:\n\n- 对于简单的状态更新,使用`setState`方法就可以,它的参数是一个对象,这个对象就是你要更新的状态,它会和之前的状态进行合并,然后返回一个新的状态,从而触发组件更新。\n\n- 对于需要通用的逻辑处理的状态更新,参照`useFormStateStore.ts`中的`setLoadingByAge`方法,将它作为状态里的一个方法就行了。\n\n# Zustand\n\n使用非常简单,API也很少,它的原理是使用了`Proxy`,所以它的性能非常好。\n\n# 相比Redux\n\n相比Redux,Zustand的代码非常简单明了,不需要使用`connect`、`mapStateToProps`、`mapDispatchToProps`这些方法。\n\n# 相比React Context\n\nReact Context需要一个`Provider`包裹组件以传递状态,需要一个`useContext`使用状态,光从层级上就让人绕起来了。而Zustand只需要一个`create`方法,就可以使用了,且状态是全局的,不需要传递。\n\n\n\n\n","slug":"react-zustand","published":1,"updated":"2024-01-03T09:13:36.695Z","comments":1,"layout":"post","photos":[],"link":"","_id":"clqxk9r640001joorghsabmh1","content":"

原文链接:https://github.com/taoliujun/blog/issues/35

\n\n\n

官方文档:https://docs.pmnd.rs/zustand

\n

如何使用

Zustand 是一个非常简单粗暴的全局状态管理库,它的使用有多简单呢?如下:

\n
1
> pnpm add zustand
\n\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// useFormStateStore.ts
import { create } from 'zustand';

interface State {
loading: boolean;
disabled: boolean;
setLoadingByAge: (value: number) => void;
}

export const useFormStateStore = create<State>((set) => ({
loading: false,
disabled: false,
setLoadingByAge: (value) => {
set({ loading: value > 10 });
},
}));
\n\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// app.tsx
import { useState, type FC } from 'react';
import { useFormStateStore } from './useFormStateStore';
import { Button } from '@/components/Button';

const Loading: FC = () => {
const { loading } = useFormStateStore();
return <div>loading: {String(loading)}</div>;
};

const Disabled: FC = () => {
const { disabled } = useFormStateStore();
return <div>disabled: {String(disabled)}</div>;
};

const Main: FC = () => {
const { setLoadingByAge } = useFormStateStore();
const [age, setAge] = useState(0);

return (
<div>
<Loading />
<br />
<Disabled />
<br />
<Button
onClick={() => {
useFormStateStore.setState({
loading: true,
});
}}
>
set loading true
</Button>
<Button
onClick={() => {
useFormStateStore.setState({
loading: false,
});
}}
>
set loading false
</Button>
<Button
onClick={() => {
useFormStateStore.setState({
disabled: true,
});
}}
>
set disabled true
</Button>
<Button
onClick={() => {
useFormStateStore.setState({
disabled: false,
});
}}
>
set disabled false
</Button>
<br />
<input
type="number"
value={age}
onChange={(e) => {
setAge(Number(e.target.value));
}}
/>
<br />
<Button
onClick={() => {
setLoadingByAge(age);
}}
>
set loading by age
</Button>
</div>
);
};

export default Main;
\n\n

useFormStateStore.ts中定义了状态,然后在app.tsx中使用,就是这么简单粗暴!这里有几点介绍下:

\n
    \n
  • 对于简单的状态更新,使用setState方法就可以,它的参数是一个对象,这个对象就是你要更新的状态,它会和之前的状态进行合并,然后返回一个新的状态,从而触发组件更新。

    \n
  • \n
  • 对于需要通用的逻辑处理的状态更新,参照useFormStateStore.ts中的setLoadingByAge方法,将它作为状态里的一个方法就行了。

    \n
  • \n
\n

Zustand

使用非常简单,API也很少,它的原理是使用了Proxy,所以它的性能非常好。

\n

相比Redux

相比Redux,Zustand的代码非常简单明了,不需要使用connectmapStateToPropsmapDispatchToProps这些方法。

\n

相比React Context

React Context需要一个Provider包裹组件以传递状态,需要一个useContext使用状态,光从层级上就让人绕起来了。而Zustand只需要一个create方法,就可以使用了,且状态是全局的,不需要传递。

\n","site":{"data":{}},"excerpt":"","more":"

原文链接:https://github.com/taoliujun/blog/issues/35

\n\n\n

官方文档:https://docs.pmnd.rs/zustand

\n

如何使用

Zustand 是一个非常简单粗暴的全局状态管理库,它的使用有多简单呢?如下:

\n
1
> pnpm add zustand
\n\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// useFormStateStore.ts
import { create } from 'zustand';

interface State {
loading: boolean;
disabled: boolean;
setLoadingByAge: (value: number) => void;
}

export const useFormStateStore = create<State>((set) => ({
loading: false,
disabled: false,
setLoadingByAge: (value) => {
set({ loading: value > 10 });
},
}));
\n\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// app.tsx
import { useState, type FC } from 'react';
import { useFormStateStore } from './useFormStateStore';
import { Button } from '@/components/Button';

const Loading: FC = () => {
const { loading } = useFormStateStore();
return <div>loading: {String(loading)}</div>;
};

const Disabled: FC = () => {
const { disabled } = useFormStateStore();
return <div>disabled: {String(disabled)}</div>;
};

const Main: FC = () => {
const { setLoadingByAge } = useFormStateStore();
const [age, setAge] = useState(0);

return (
<div>
<Loading />
<br />
<Disabled />
<br />
<Button
onClick={() => {
useFormStateStore.setState({
loading: true,
});
}}
>
set loading true
</Button>
<Button
onClick={() => {
useFormStateStore.setState({
loading: false,
});
}}
>
set loading false
</Button>
<Button
onClick={() => {
useFormStateStore.setState({
disabled: true,
});
}}
>
set disabled true
</Button>
<Button
onClick={() => {
useFormStateStore.setState({
disabled: false,
});
}}
>
set disabled false
</Button>
<br />
<input
type="number"
value={age}
onChange={(e) => {
setAge(Number(e.target.value));
}}
/>
<br />
<Button
onClick={() => {
setLoadingByAge(age);
}}
>
set loading by age
</Button>
</div>
);
};

export default Main;
\n\n

useFormStateStore.ts中定义了状态,然后在app.tsx中使用,就是这么简单粗暴!这里有几点介绍下:

\n
    \n
  • 对于简单的状态更新,使用setState方法就可以,它的参数是一个对象,这个对象就是你要更新的状态,它会和之前的状态进行合并,然后返回一个新的状态,从而触发组件更新。

    \n
  • \n
  • 对于需要通用的逻辑处理的状态更新,参照useFormStateStore.ts中的setLoadingByAge方法,将它作为状态里的一个方法就行了。

    \n
  • \n
\n

Zustand

使用非常简单,API也很少,它的原理是使用了Proxy,所以它的性能非常好。

\n

相比Redux

相比Redux,Zustand的代码非常简单明了,不需要使用connectmapStateToPropsmapDispatchToProps这些方法。

\n

相比React Context

React Context需要一个Provider包裹组件以传递状态,需要一个useContext使用状态,光从层级上就让人绕起来了。而Zustand只需要一个create方法,就可以使用了,且状态是全局的,不需要传递。

\n"}],"PostAsset":[],"PostCategory":[{"post_id":"clqxk9r600000joor84vf3yg8","category_id":"clqxk9r670003joorffjb03fe","_id":"clqxk9r690008joor4imo859j"},{"post_id":"clqxk9r640001joorghsabmh1","category_id":"clqxk9r680005joor13xp87sb","_id":"clqxk9r6a0009joor69azc42l"}],"PostTag":[{"post_id":"clqxk9r600000joor84vf3yg8","tag_id":"clqxk9r660002joorb3zc5fqo","_id":"clqxk9r690006joorhst22hj6"},{"post_id":"clqxk9r640001joorghsabmh1","tag_id":"clqxk9r680004joor9f4327js","_id":"clqxk9r6a000ajoorb4fqgt2j"},{"post_id":"clqxk9r640001joorghsabmh1","tag_id":"clqxk9r690007joor0mwh9cfx","_id":"clqxk9r6a000bjoor5eui8kh4"}],"Tag":[{"name":"github actions","_id":"clqxk9r660002joorb3zc5fqo"},{"name":"zustand","_id":"clqxk9r680004joor9f4327js"},{"name":"react store","_id":"clqxk9r690007joor0mwh9cfx"}]}} \ No newline at end of file diff --git a/src/source/_posts/github-actions-sample-eslint-in-pull-request.md b/src/source/_posts/github-actions-sample-eslint-in-pull-request.md new file mode 100644 index 0000000..52d34b8 --- /dev/null +++ b/src/source/_posts/github-actions-sample-eslint-in-pull-request.md @@ -0,0 +1,751 @@ +--- +title: "7. GitHub Actions - 在pull request中执行eslint检测的工作流例子" +date: "2023-12-29T03:56:49Z" +categories: + - [工程化] + +url: github-actions-sample-eslint-in-pull-request +tags: + - github actions + +--- + + +原文链接:[https://github.com/taoliujun/blog/issues/36](https://github.com/taoliujun/blog/issues/36) + + + +一个在pull request发起的时候执行eslint检测的workflow,[点此查看完整代码](https://github.com/taoliujun/npm-packages/blob/master/.github/workflows/check-pull-request.yml),它实现的功能如下: + +- 在pull request创建、更新的时候执行。 +- 先回复一个评论,告诉用户正在运行。 +- 初始化仓库,并安装依赖,产生依赖缓存。 +- 运行eslint增量检查。 +- 运行typescript检查。 +- 运行jest检查。 +- 更新之前的评论,回复检查的结果。 + +运行截图: + +![Alt text](https://github.com/taoliujun/blog/assets/5689134/09c86bc1-ada1-41c3-9f8f-7e6c46f8204e) + +为避免歧义,涉及到github action的术语都是英文的。术语介绍如下: + +* workflow,工作流,可以理解为yml文件。 +* jobs,工作,一个workflow可以包含多个job,并行执行。 +* steps,作业,一个job可以包含多个step,串行执行。 +* action,操作,作业中具体的执行。 + +## 步骤 + +- [初始化workflow](https://github.com/taoliujun/blog/issues/36#issuecomment-1871790603) +- [reply checking](https://github.com/taoliujun/blog/issues/36#issuecomment-1871806576) +- [./.github/actions/unique-comment](https://github.com/taoliujun/blog/issues/36#issuecomment-1871818126) +- [init](https://github.com/taoliujun/blog/issues/36#issuecomment-1871862632) +- [eslint](https://github.com/taoliujun/blog/issues/36#issuecomment-1871862779) +- [typescript](https://github.com/taoliujun/blog/issues/36#issuecomment-1871862850) +- [unit test](https://github.com/taoliujun/blog/issues/36#issuecomment-1871863037) +- [reply result](https://github.com/taoliujun/blog/issues/36#issuecomment-1871863117) + + + +# 初始化workflow + +在项目中新建文件`.github/workflows/check-pull-request.yml`,内容如下: + +```yaml +name: test check pull request +run-name: 'check pull request #${{ github.event.pull_request.number }}' +on: + pull_request: + types: [opened, synchronize, reopened] +jobs: + replyChecking: + runs-on: ubuntu-latest + steps: + - run: echo 'replyChecking' + + init: + runs-on: ubuntu-latest + steps: + - run: echo 'init' + + eslint: + runs-on: ubuntu-latest + needs: [init] + steps: + - run: echo 'eslint' + + typescript: + runs-on: ubuntu-latest + needs: [init] + steps: + - run: echo 'typescript' + + unitTest: + runs-on: ubuntu-latest + needs: [init] + steps: + - run: echo 'unitTest' + + replyResult: + runs-on: ubuntu-latest + needs: [replyChecking, eslint, typescript, unitTest] + steps: + - run: echo 'replyResult' +``` + +## name和run-name + +给workflow命名为`check pull request`,它会出现在Actions页面的左侧菜单中。运行实例名为`check pull request #44`,出现在右侧的运行列表中。如图: + +![](https://github.com/taoliujun/blog/assets/5689134/c1371ff2-8fc3-4e5b-8b60-3c572419938b) + +`run-name`中的`${{ github.event.pull_request.number }}`是workflow的上下文,这里读取了上下文中的pr编号。 + +## on + +`on`指定了workflow的触发条件,这里配置了在pr创建、同步、重新打开的时候,触发该workflow。 + +## jobs + +按照设想,需要定义几个job,分别是: + +- replyChecking:回复用户正在检查中 +- init:初始化仓库,缓存依赖项 +- eslint:运行eslint检查 +- typescript:运行typescript检查 +- unitTest:运行单元测试 +- replyResult:回复用户检查结果 + +`jobs`是并行运行的,聪明如你肯定发现了,eslint、typescript、unitTest这三个job会涉及到安装npm依赖,所以它们最好在init后执行,确保依赖已经缓存了。 + +其次,replyResult肯定要拿到eslint等job的结果才能执行,所以使用了`needs`管理它们的执行依赖关系。 + +### runs-on + +每个job都运行在独立的容器中,github官方提供了windows、macos、linux多种容器,这里使用了ubuntu容器。 + +## 测试 + +发起一个pr,看到Actions页面出现了新的运行实例,点击进去,可以看到各个job的运行情况和依赖关系: + +![](https://github.com/taoliujun/blog/assets/5689134/09c86bc1-ada1-41c3-9f8f-7e6c46f8204e) + + + +# replyChecking + +在进行eslint检测之前,先在pr里回复`checking`,并且带上拽酷炫的话。将replyChecking改成如下: + +```yaml +replyChecking: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{github.head_ref}} + - name: Get date time + id: getDateTime + run: echo "result=$(TZ=Asia/Shanghai date)" >> "$GITHUB_OUTPUT" + - name: Create or update a comment + uses: ./.github/actions/unique-comment + with: + uniqueIdentifier: ${{ github.workflow }} + body: | + **Checking...** + + --- + + Commented by Action [${{github.workflow}}](${{github.event.repository.html_url}}/actions/runs/${{github.run_id}}), last updated on ${{steps.getDateTime.outputs.result}}. +``` + +`steps`每一步里`name`、`id`是可选的,`name`在Actions详情页面里会显示,更直观的看到step的名称,推荐写上。 + +## Checkout + +`uses`表示使用一个action,名为`actions/checkout@v4`,它用来拉取仓库。 + +> 同其他编程语言一样,重复的action可以封装起来。[action市场](https://github.com/marketplace?type=actions)提供了很多。 + +`with`属性指定了该action的输入参数,每个action的参数不尽相同。 + +`ref`参数表示要拉取的分支,`${{github.head_ref}}`也是一个上下文,表示当前pr的源分支。 + + +## Get Date time + +这step还写了`id`,表示该step在该job中的唯一标识,为什么要写呢?是为了下一步step能根据`id`读取到它的`output`。 + +> **output**是workflow中非常重要的概念,它用于在step之间、job之间分享简单的数据。 + +`run`就是在容器中跑一个命令,这里跑了一个unix bash命令,将当前时间写入到`$GITHUB_OUTPUT`中,键名为`result`。 + +> `$GITHUB_OUTPUT`是workflow注入到容器中的一个路径,用于存放output。 + +## Create or update a comment + +`uses`使用了本地的action,这个action用于创建或更新一个唯一回复,下一节说。 + +> 有时候,官方或市场的action并不能满足你的需要,就得自己写一个了。 + +同理,该action也有`with`属性,`uniqueIdentifier`是回复评论的唯一标识,`body`是回复的内容,内容使用了markdown语法,里面还涉及到上下文不一一细讲了。只说`${{steps.getDateTime.outputs.result}}`这个上下文表示获取getDateTime这个step中,键名为`result`的值。 + +如果你不需要在内容里插入时间,那么上面的`Get Date time`就可以省略了。 + +## 测试 + +因为我已经有完整的代码了,所以运行后,pr中会有一个回复,如图: + +![](https://github.com/taoliujun/blog/assets/5689134/42396a84-b798-4f4e-9f39-5bf92a8acb15) + + +# ./.github/actions/unique-comment + +这是一个封装的javascript action,用于对issue创建、更新唯一评论。 + +## 目录结构 + +创建目录`./.github/actions/unique-comment`,最终目录结构如下: + +```bash +. +├── action.yml +├── config +│   └── webpack.config.js +├── dist +│   ├── index.js +│   └── index.js.LICENSE.txt +├── package.json +└── src + └── index.js +``` + +## action.yml + +这是action的配置文件,必须存在,内容如下: + +```yaml +name: unique-comment +description: create or update a unique comment + +runs: + using: 'node20' + main: './dist/index.js' + +inputs: + token: + description: 'GitHub token' + required: false + default: ${{ github.token }} + owner: + description: 'Repository owner' + required: false + default: ${{ github.event.repository.owner.login }} + repo: + description: 'Repository name' + required: false + default: ${{ github.event.repository.name }} + issue_number: + description: 'Issue number' + required: false + default: ${{ github.event.number }} + body: + description: 'Comment body' + required: false + uniqueIdentifier: + description: 'Unique identifier for comment' + required: false + default: 'unique-comment' +``` + +大部分属性不一一细讲了,都是简单的英文望文生义即可。 + +`runs`表示运行在`node20`环境下,入口文件为`./dist/index.js`。 + +`inputs`表示接受的参数,也就是之前提到的`with`属性里要输入的参数。用`required`表示是否必须传入,`default`表示默认值。 + +## src/index.js + +为什么入口文件是`dist/index.js`,而不是`src/index.js`呢?因为要引用一些github官方提供的快捷操作github REST API的js包去操作issue评论(pull request也是一种issue),最终打包后的文件才能在工作流中稳妥的运行。所以,写好`src/index.js`,再打包就行。 + +该文件代码如下: + +```javascript +const core = require('@actions/core'); +const github = require('@actions/github'); + +const main = async () => { + const token = core.getInput('token'); + const owner = core.getInput('owner'); + const repo = core.getInput('repo'); + const issueNumber = core.getInput('issue_number'); + const uniqueIdentifier = `[^uniqueIdentifier]: ${core.getInput('uniqueIdentifier')}`; + const body = `${core.getInput('body')}\n\n${uniqueIdentifier}`; + + core.debug(`uniqueIdentifier is ${uniqueIdentifier}`); + + const octokit = github.getOctokit(token); + + const comments = await octokit.rest.issues.listComments({ + owner, + repo, + issue_number: issueNumber, + }); + + const botComment = comments.data.find((v) => v.body.includes(uniqueIdentifier)); + + if (botComment) { + core.info('update comment successfully.'); + await octokit.rest.issues.updateComment({ + owner, + repo, + comment_id: botComment.id, + body, + }); + } else { + core.info('create comment successfully.'); + await octokit.rest.issues.createComment({ + owner, + repo, + issue_number: issueNumber, + body, + }); + } +}; + +try { + main(); +} catch (err) { + core.setFailed(err.message); +} +``` + +`@actions/core`和`@actions/github`是github官方提供的js包,前者可以方便的读取入参等,后者可以方便的操作github REST API。 + +`main`函数的代码就是原生javascript,不一一解释了,主要通过`uniqueIdentifier`来判断是否发布过评论,如果是,就更新评论,否则就创建评论。 + +> markdown语法`[^uniqueIdentifier]`表示脚注,不会被渲染。 + +`core.setFailed(err.message);`表示抛出退出代码。 + +## config/webpack.config.js + +打包用的,配置简单可用即可: + +```javascript +module.exports = { + mode: 'production', + target: 'node20', + entry: './src/index.js', + output: { + filename: 'index.js', + clean: true, + }, +}; +``` + +## package.json + +```json +{ + "name": "unique-comment", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "webpack --config ./config/webpack.config.js" + }, + "dependencies": { + "@actions/core": "^1.10.1", + "@actions/github": "^6.0.0" + }, + "devDependencies": { + "webpack": "^5.89.0", + "webpack-cli": "^5.1.4" + } +} +``` + +没啥好说的,列出了依赖项。和一个打包脚本。 + +## 测试 + +修改了`src/index.js`得`build`,然后push到github仓库。 + +记得将**dist**目录也提交到github仓库。 + + + +# init + +现在,开始搞正经的了。 + +先初始化项目,这个job的目的仅仅是为了缓存pnpm依赖项,如果你的项目的依赖项不经常更新,可以省略这个job,后续也不要`needs`这个job。 + +将init改成如下: + +```yaml +init: + runs-on: ubuntu-latest + steps: + - name: Init repo + uses: actions/checkout@v4 + with: + ref: ${{github.head_ref}} + + - name: Init pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Init node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install +``` + +相信经过对之前的job的了解,这里的配置就看起来很简单了。 + +## Init pnpm + +使用第三方action,安装pnpm@^8。 + +## Init node + +`cache: 'pnpm'`指定缓存机制,它内部是利用了workflow的cache机制。 + +## Install dependencies + +安装依赖项,触发缓存。 + + +# eslint + +将eslint改成如下: + +```yaml +eslint: + runs-on: ubuntu-latest + needs: [init] + outputs: + result: ${{ steps.lint.outputs.result }} + steps: + - name: Init repo + uses: actions/checkout@v4 + with: + ref: ${{github.head_ref}} + fetch-depth: 0 + + - name: Init pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Init node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install + + - name: Run eslint + id: lint + uses: actions/github-script@v7 + with: + result-encoding: string + script: | + let output = ''; + let outerr = ''; + let diffFiles = ''; + + await exec.exec( + `git diff --name-only origin/${{github.base_ref}}`, + [], + { + // silent: true, + // ignoreReturnCode: true, + listeners: { + stdout: (data) => { + diffFiles += data.toString(); + }, + }, + } + ); + + const lintFiles = diffFiles.split(`\n`).filter((file) => { + return file.endsWith('.js') || file.endsWith('.ts') || file.endsWith('.tsx') + }).join(' '); + + await exec.exec( + // "pnpm run lint --format stylish", + `pnpm eslint ${lintFiles}`, + [], + { + // silent: true, + ignoreReturnCode: true, + listeners: { + stdout: (data) => { + output += data.toString(); + }, + stderr: (data) => { + outerr += data.toString(); + }, + }, + } + ); + + if (outerr) { + return `:x: Some command execution errors, non-eslint business errors.`; + } + + const errorMatch = output.match(/(\d+) errors?/); + const warnMatch = output.match(/(\d+) warnings?/); + + if (errorMatch && errorMatch?.[1] !== '0') { + return `:x: ${errorMatch?.[0]} ${warnMatch?.[0]}`; + } + + return `:white_check_mark: ${errorMatch?.[0] || '0 error'} ${warnMatch?.[0] || '0 warning'}`; +``` + +## needs + +使用`needs`依赖init,可以使用到pnpm的缓存项,防止install太慢。 + +> 因为eslint、typescript、unitTest都需要pnpm install,所以一个前置的init去缓存pnpm依赖项,可以加快后续的install速度。 + +## outputs + +job里的outputs,可以在依赖它的其他job中访问到。这里使用`${{ steps.lint.outputs.result }}`去获取该job中lint这个step里的output里的result。 + +> output有job和step两个维度,注意区分。 + + +## Run eslint + +它uses了`actions/github-script@v7`,这是github官方提供的一个action,可以在`with.script`里写js代码去执行,同时它会注入一些变量到script中去,见它的[官方文档](https://github.com/actions/github-script/tree/v7/)。 + +> 对于简单的js代码,可以使用这个action去完成,不用再去写一个js文件。 + +`result-encoding`是指定script返回的数据格式的,默认是json,这指定为string。 + +> 为什么script里return了string,还要指定为string呢? +> 因为`return 'hello'`在json encode后是`'"hello"'`,而string encode后为`'hello'`。 + +script里是原生的js代码了,里面的`exec`是该action注入的变量,用来执行shell命令。 + +这段js代码做了两个事情,一是`git diff`获取pr中改动的文件列表,二是`eslint`检查这些增量文件,最后返回处理的结果。 + +## fetch-depth + +Init repo这个step里设置了`fetch-depth: 0`,不然获取不到完整的git分支,具体看`actions/checkout`的解释,涉及到git的知识不展开细说了。 + +## steps.lint.outputs.result + +`steps.lint.outputs.result`为什么能拿到lint step里的output.result呢?因为`actions/github-script`这个action内部将script的返回值,设置到`$GITHUB_OUTPUT`里了,且键名为`result`。 + + +# typescript + +和eslint的配置大同小异,只是改了对检测结果的判断。 + +```yaml +typescript: + runs-on: ubuntu-latest + needs: [init] + outputs: + result: ${{ steps.lint.outputs.result }} + steps: + - name: Init repo + uses: actions/checkout@v4 + with: + ref: ${{github.head_ref}} + + - name: Init pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Init node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install + + - name: Run lint + id: lint + uses: actions/github-script@v7 + with: + result-encoding: string + script: | + let output = ''; + let outerr = ''; + + await exec.exec( + `pnpm run -r lint:ts`, + [], + { + // silent: true, + ignoreReturnCode: true, + listeners: { + stdout: (data) => { + output += data.toString(); + }, + stderr: (data) => { + outerr += data.toString(); + }, + }, + } + ); + + if (outerr) { + return `:x: Some command execution errors, no business errors.`; + } + + const errorMatch = output.match(/error TS/g); + + if (errorMatch) { + return `:x: ${errorMatch?.length} errors`; + } + + return `:white_check_mark: ${'0 error'}`; +``` + + +# unitTest + +和eslint的配置大同小异,只是改了对检测结果的判断。唯一的区别是jest的检测结果是输出到stderr,见https://github.com/jestjs/jest/issues/5064。 + +```yaml +unitTest: + runs-on: ubuntu-latest + needs: [init] + outputs: + result: ${{ steps.lint.outputs.result }} + steps: + - name: Init repo + uses: actions/checkout@v4 + with: + ref: ${{github.head_ref}} + + - name: Init pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Init node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + + - name: Install dependencies + run: | + pnpm remove @nike/eslint-multi-formatter || true + pnpm remove @nike/svg-packer || true + pnpm install + + - name: Run lint + id: lint + uses: actions/github-script@v7 + with: + result-encoding: string + script: | + let output = ''; + let outerr = ''; + + await exec.exec( + `pnpm run test`, + [], + { + // silent: true, + ignoreReturnCode: true, + listeners: { + stdout: (data) => { + output += data.toString(); + }, + stderr: (data) => { + outerr += data.toString(); + }, + }, + } + ); + + // why use outerr? https://github.com/jestjs/jest/issues/5064 + + const failMatch = outerr.match(/Test Suites: \d+ failed/); + + if (failMatch) { + return `:x: ${failMatch?.[0]}`; + } + + const errorMatch = outerr.match(/Jest: "global" coverage threshold for lines \([0-9\.]+%\) not met: [0-9\.]+%/); + + if (errorMatch) { + return `:x: ${errorMatch?.[0]}`; + } + + return `:white_check_mark: passed`; +``` + + +# replyResult + +最后,将几个检测的结果进行汇总,回复到pr里就行了。 + +```yaml +replyResult: + runs-on: ubuntu-latest + needs: [replyChecking, eslint, typescript, unitTest] + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{github.head_ref}} + - name: Get date time + id: getDateTime + run: echo "result=$(TZ=Asia/Shanghai date)" >> "$GITHUB_OUTPUT" + - name: Create or update a comment + uses: ./.github/actions/unique-comment + with: + uniqueIdentifier: ${{ github.workflow }} + body: | + ## Eslint Check Result + + ${{needs.eslint.outputs.result}} + + ## Typescript Check Result + + ${{needs.typescript.outputs.result}} + + ## UnitTest Check Result + + ${{needs.unitTest.outputs.result}} + + --- + + Commented by Action [${{github.workflow}}](${{github.event.repository.html_url}}/actions/runs/${{github.run_id}}), last updated on ${{steps.getDateTime.outputs.result}}. +``` + +和replyChecking差不多,在body里使用`${{needs.eslint.outputs.result}}`去读取了eslint job的outputs。 + +## 测试 + +去发起新的pr,故意提交一个有eslint error的js/ts文件,看看表现吧~ + diff --git a/src/source/_posts/react-zustand.md b/src/source/_posts/react-zustand.md new file mode 100644 index 0000000..7edecaa --- /dev/null +++ b/src/source/_posts/react-zustand.md @@ -0,0 +1,160 @@ +--- +title: "React公共状态利器 - Zustand" +date: "2023-12-13T06:46:26Z" +categories: + - [React] + +url: react-zustand +tags: + - zustand + - react store + +--- + + +原文链接:[https://github.com/taoliujun/blog/issues/35](https://github.com/taoliujun/blog/issues/35) + + + +官方文档:https://docs.pmnd.rs/zustand + +# 如何使用 + +**Zustand** 是一个非常简单粗暴的全局状态管理库,它的使用有多简单呢?如下: + +```bash +> pnpm add zustand +``` + +```ts +// useFormStateStore.ts +import { create } from 'zustand'; + +interface State { + loading: boolean; + disabled: boolean; + setLoadingByAge: (value: number) => void; +} + +export const useFormStateStore = create((set) => ({ + loading: false, + disabled: false, + setLoadingByAge: (value) => { + set({ loading: value > 10 }); + }, +})); +``` + +```tsx +// app.tsx +import { useState, type FC } from 'react'; +import { useFormStateStore } from './useFormStateStore'; +import { Button } from '@/components/Button'; + +const Loading: FC = () => { + const { loading } = useFormStateStore(); + return
loading: {String(loading)}
; +}; + +const Disabled: FC = () => { + const { disabled } = useFormStateStore(); + return
disabled: {String(disabled)}
; +}; + +const Main: FC = () => { + const { setLoadingByAge } = useFormStateStore(); + const [age, setAge] = useState(0); + + return ( +
+ +
+ +
+ + + + +
+ { + setAge(Number(e.target.value)); + }} + /> +
+ +
+ ); +}; + +export default Main; +``` + +在`useFormStateStore.ts`中定义了状态,然后在`app.tsx`中使用,就是这么简单粗暴!这里有几点介绍下: + +- 对于简单的状态更新,使用`setState`方法就可以,它的参数是一个对象,这个对象就是你要更新的状态,它会和之前的状态进行合并,然后返回一个新的状态,从而触发组件更新。 + +- 对于需要通用的逻辑处理的状态更新,参照`useFormStateStore.ts`中的`setLoadingByAge`方法,将它作为状态里的一个方法就行了。 + +# Zustand + +使用非常简单,API也很少,它的原理是使用了`Proxy`,所以它的性能非常好。 + +# 相比Redux + +相比Redux,Zustand的代码非常简单明了,不需要使用`connect`、`mapStateToProps`、`mapDispatchToProps`这些方法。 + +# 相比React Context + +React Context需要一个`Provider`包裹组件以传递状态,需要一个`useContext`使用状态,光从层级上就让人绕起来了。而Zustand只需要一个`create`方法,就可以使用了,且状态是全局的,不需要传递。 + + + +