You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

9 lines
177 KiB

<!doctypehtml><html lang=zh-CN><meta charset=UTF-8><meta content=width=device-width name=viewport><meta content=#222 name=theme-color><meta content="Hexo 7.1.0" name=generator><link href=https://fonts.googleapis.com rel=preconnect><link href=https://cdnjs.cloudflare.com rel=preconnect><link href=/images/apple-touch-icon-next.png rel=apple-touch-icon sizes=180x180><link href=/images/favicon-32x32-next.png rel=icon sizes=32x32 type=image/png><link href=/images/favicon-16x16-next.png rel=icon sizes=16x16 type=image/png><link color=#222 href=/images/logo.svg rel=mask-icon><meta content=2X6S9P7CAjXjVvw8YyQR8pCu-B0oEu7O9quLgxXuWyA name=google-site-verification><meta content=dV8JGNzi0c name=baidu-site-verification><script>setTimeout(function(){var d=document.createElement("script");d.setAttribute("data-ad-client","ca-pub-7480618470784058"),d.async=!0,d.src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js",document.body.appendChild(d)},5e3),window.addEventListener?window.addEventListener("load",downloadJsAtOnload,!1):window.attachEvent?window.attachEvent("onload",downloadJsAtOnload):window.onload=downloadJsAtOnload</script><style>:root{--body-bg-color:#f5f7f9;--content-bg-color:#fff;--card-bg-color:#f5f5f5;--text-color:#555;--blockquote-color:#666;--link-color:#555;--link-hover-color:#222;--brand-color:#fff;--brand-hover-color:#fff;--table-row-odd-bg-color:#f9f9f9;--table-row-hover-bg-color:#f5f5f5;--menu-item-bg-color:#f5f5f5;--theme-color:#222;--btn-default-bg:#fff;--btn-default-color:#555;--btn-default-border-color:#555;--btn-default-hover-bg:#222;--btn-default-hover-color:#fff;--btn-default-hover-border-color:#222;--highlight-background:#f3f3f3;--highlight-foreground:#444;--highlight-gutter-background:#e1e1e1;--highlight-gutter-foreground:#555;color-scheme:light}html{line-height:1.15;-webkit-text-size-adjust:100%}details,main{display:block}pre{font-size:1em;overflow:auto;padding:10px}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:ButtonText dotted 1px}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}.table-container,textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}summary{display:list-item}[hidden],template{display:none}::selection{background:#262a30;color:#eee}body,html{height:100%}body{margin:0;background:var(--body-bg-color);box-sizing:border-box;color:var(--text-color);font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;font-size:1.2em;line-height:2;min-height:100%;position:relative;transition:padding .2s ease-in-out}h1,h2,h3,h4,h5,h6{font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;font-weight:700;line-height:1.5;margin:30px 0 15px}h1{font-size:1.5em}h2{font-size:1.375em}h3{font-size:1.25em}h4{font-size:1.125em}h5{font-size:1em}h6{font-size:.875em}p{margin:0 0 20px}a{background:0 0;border-bottom:1px solid #999;color:var(--link-color);cursor:pointer;outline:0;text-decoration:none;overflow-wrap:break-word}a:hover{border-bottom-color:var(--link-hover-color);color:var(--link-hover-color)}embed,iframe,img,video{display:block;margin-left:auto;margin-right:auto;max-width:100%}hr{box-sizing:content-box;overflow:visible;background-image:repeating-linear-gradient(-45deg,#ddd,#ddd 4px,transparent 4px,transparent 8px);border:0;height:3px;margin:40px 0}blockquote{border-left:4px solid #ddd;color:var(--blockquote-color);margin:0;padding:0 15px}blockquote cite::before{content:'-';padding:0 5px}dt{font-weight:700}dd{margin:0;padding:0}table{border-collapse:collapse;border-spacing:0;font-size:.875em;margin:0 0 20px;width:100%}tbody tr:nth-of-type(odd){background:var(--table-row-odd-bg-color)}tbody tr:hover{background:var(--table-row-hover-bg-color)}caption,td,th{padding:8px}td,th{border:1px solid #ddd;border-bottom:3px solid #ddd}th{font-weight:700;padding-bottom:10px}td{border-bottom-width:1px}.btn{background:var(--btn-default-bg);border:2px solid var(--btn-default-border-color);border-radius:2px;color:var(--btn-default-color);display:inline-block;font-size:.875em;line-height:2;padding:0 20px;transition:background-color .2s ease-in-out}.btn:hover{background:var(--btn-default-hover-bg);border-color:var(--btn-default-hover-border-color);color:var(--btn-default-hover-color)}.btn+.btn{margin:0 0 8px 8px}.btn .fa-fw{text-align:left;width:1.285714285714286em}.toggle{line-height:0}.toggle .toggle-line{background:#fff;display:block;height:2px;left:0;position:relative;top:0;transition:.4s;width:100%}.toggle .toggle-line:first-child{margin-top:1px}.toggle .toggle-line:not(:first-child){margin-top:4px}.toggle.toggle-arrow :first-child{left:50%;top:2px;transform:rotate(45deg);width:50%}.toggle.toggle-arrow :last-child{left:50%;top:-2px;transform:rotate(-45deg);width:50%}.toggle.toggle-close :nth-child(2){opacity:0}.toggle.toggle-close :first-child{top:6px;transform:rotate(45deg)}.toggle.toggle-close :last-child{top:-6px;transform:rotate(-45deg)}/*!
Theme: Default
Description: Original highlight.js style
Author: (c) Ivan Sagalaev <maniac@softwaremaniacs.org>
Maintainer: @highlightjs/core-team
Website: https://highlightjs.org/
License: see project LICENSE
Touched: 2021
*/pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#f3f3f3;color:#444}.hljs-comment{color:#697070}.hljs-punctuation,.hljs-tag{color:#444a}.hljs-tag .hljs-attr,.hljs-tag .hljs-name{color:#444}.hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-name,.hljs-selector-tag{font-weight:700}.hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type{color:#800}.hljs-section,.hljs-title{color:#800;font-weight:700}.hljs-link,.hljs-operator,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#ab5656}.hljs-literal{color:#695}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta .hljs-string{color:#38a}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}.code-container:hover .copy-btn,.highlight:hover .copy-btn{opacity:1}.code-container{position:relative}.copy-btn{color:#333;cursor:pointer;line-height:1.6;opacity:0;padding:2px 6px;position:absolute;transition:opacity .2s ease-in-out;background-color:#eee;background-image:linear-gradient(#fcfcfc,#eee);border:1px solid #d5d5d5;border-radius:3px;font-size:.8125em;right:4px;top:8px}code,figure.highlight,kbd,pre{background:var(--highlight-background);color:var(--highlight-foreground)}figure.highlight,pre{line-height:1.6;margin:0 auto 20px}figure.highlight figcaption,pre .caption,pre figcaption{background:var(--highlight-gutter-background);color:var(--highlight-foreground);display:flow-root;font-size:.875em;line-height:1.2;padding:.5em}figure.highlight figcaption a,pre .caption a,pre figcaption a{color:var(--highlight-foreground);float:right}figure.highlight figcaption a:hover,pre .caption a:hover,pre figcaption a:hover{border-bottom-color:var(--highlight-foreground)}code,pre{font-family:consolas,Menlo,monospace,'PingFang SC','Microsoft YaHei'}code{border-radius:3px;font-size:.875em;padding:2px 4px;overflow-wrap:break-word}kbd{border:2px solid #ccc;border-radius:.2em;box-shadow:.1em .1em .2em rgba(0,0,0,.1);font-family:inherit;padding:.1em .3em;white-space:nowrap}figure.highlight{overflow:auto;position:relative}figure.highlight pre{border:0;margin:0;padding:10px 0}figure.highlight table{border:0;margin:0;width:auto}figure.highlight td{border:0;padding:0}figure.highlight .gutter{-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}figure.highlight .gutter pre{background:var(--highlight-gutter-background);color:var(--highlight-gutter-foreground);padding-left:10px;padding-right:10px;text-align:right}figure.highlight .code pre{padding-left:10px;width:100%}figure.highlight .marked{background:rgba(0,0,0,.3)}pre .caption,pre figcaption{margin-bottom:10px}.gist table{width:auto}.gist table td{border:0}pre code{background:0 0;padding:0;text-shadow:none}.blockquote-center{border-left:0;margin:40px 0;padding:0;position:relative;text-align:center}.blockquote-center::after,.blockquote-center::before{left:0;line-height:1;opacity:.6;position:absolute;width:100%}.blockquote-center::before{border-top:1px solid #ccc;text-align:left;top:-20px;content:'\f10d';font-family:'Font Awesome 6 Free';font-weight:900}.blockquote-center::after{border-bottom:1px solid #ccc;bottom:-20px;text-align:right;content:'\f10e';font-family:'Font Awesome 6 Free';font-weight:900}.blockquote-center div,.blockquote-center p{text-align:center}.group-picture{margin-bottom:20px}.group-picture .group-picture-row{display:flex;gap:3px;margin-bottom:3px}.group-picture .group-picture-column{flex:1}.group-picture .group-picture-column img{height:100%;margin:0;object-fit:cover;width:100%}.post-body .label{color:#555;padding:0 2px}.post-body .label.default{background:#f0f0f0}.post-body .label.primary{background:#efe6f7}.post-body .label.info{background:#e5f2f8}.post-body .label.success{background:#e7f4e9}.post-body .label.warning{background:#fcf6e1}.post-body .label.danger{background:#fae8eb}.post-body .link-grid{display:grid;grid-gap:1.5rem;gap:1.5rem;grid-template-columns:1fr 1fr;margin-bottom:20px;padding:1rem}.post-body .link-grid .link-grid-container{border:solid #ddd;box-shadow:1rem 1rem .5rem rgba(0,0,0,.5);min-height:5rem;min-width:0;padding:.5rem;position:relative;transition:background .3s}.post-body .link-grid .link-grid-container:hover{animation:.5s next-shake;background:var(--card-bg-color)}.post-body .link-grid .link-grid-container:active{box-shadow:.5rem .5rem .25rem rgba(0,0,0,.5);transform:translate(.2rem,.2rem)}.post-body .link-grid .link-grid-container .link-grid-image{border:1px solid #ddd;border-radius:50%;box-sizing:border-box;height:5rem;padding:3px;position:absolute;width:5rem}.post-body .link-grid .link-grid-container p{margin:0 1rem 0 6rem}.post-body .link-grid .link-grid-container p:first-of-type{font-size:1.2em}.post-body .link-grid .link-grid-container p:last-of-type{font-size:.8em;line-height:1.3rem;opacity:.7}.post-body .link-grid .link-grid-container a{border:0;height:100%;left:0;position:absolute;top:0;width:100%}@keyframes next-shake{0%{transform:translate(1pt,1pt) rotate(0)}10%{transform:translate(-1pt,-2pt) rotate(-1deg)}20%{transform:translate(-3pt,0) rotate(1deg)}30%{transform:translate(3pt,2pt) rotate(0)}40%{transform:translate(1pt,-1pt) rotate(1deg)}50%{transform:translate(-1pt,2pt) rotate(-1deg)}60%{transform:translate(-3pt,1pt) rotate(0)}70%{transform:translate(3pt,1pt) rotate(-1deg)}80%{transform:translate(-1pt,-1pt) rotate(1deg)}90%{transform:translate(1pt,2pt) rotate(0)}100%{transform:translate(1pt,-2pt) rotate(-1deg)}}.post-body .note{border-radius:3px;margin-bottom:20px;padding:1em;position:relative;border:1px solid #eee;border-left-width:5px}.post-body .note summary{cursor:pointer;outline:0}.post-body .note summary p{display:inline}.post-body .note h2,.post-body .note h3,.post-body .note h4,.post-body .note h5,.post-body .note h6{border-bottom:initial;margin:0;padding-top:0}.post-body .note :first-child{margin-top:0}.post-body .note :last-child{margin-bottom:0}.post-body .note.default{border-left-color:#777}.post-body .note.default h2,.post-body .note.default h3,.post-body .note.default h4,.post-body .note.default h5,.post-body .note.default h6{color:#777}.post-body .note.primary{border-left-color:#6f42c1}.post-body .note.primary h2,.post-body .note.primary h3,.post-body .note.primary h4,.post-body .note.primary h5,.post-body .note.primary h6{color:#6f42c1}.post-body .note.info{border-left-color:#428bca}.post-body .note.info h2,.post-body .note.info h3,.post-body .note.info h4,.post-body .note.info h5,.post-body .note.info h6{color:#428bca}.post-body .note.success{border-left-color:#5cb85c}.post-body .note.success h2,.post-body .note.success h3,.post-body .note.success h4,.post-body .note.success h5,.post-body .note.success h6{color:#5cb85c}.post-body .note.warning{border-left-color:#f0ad4e}.post-body .note.warning h2,.post-body .note.warning h3,.post-body .note.warning h4,.post-body .note.warning h5,.post-body .note.warning h6{color:#f0ad4e}.post-body .note.danger{border-left-color:#d9534f}.post-body .note.danger h2,.post-body .note.danger h3,.post-body .note.danger h4,.post-body .note.danger h5,.post-body .note.danger h6{color:#d9534f}.post-body .tabs{margin-bottom:20px}.post-body .tabs,.tabs-comment{padding-top:10px}.post-body .tabs ul.nav-tabs,.tabs-comment ul.nav-tabs{background:var(--content-bg-color);display:flex;display:flex;flex-wrap:wrap;justify-content:center;margin:0;padding:0;position:-webkit-sticky;position:sticky;top:0;z-index:5}.post-body .tabs ul.nav-tabs li.tab,.tabs-comment ul.nav-tabs li.tab{border-bottom:1px solid #ddd;border-left:1px solid transparent;border-right:1px solid transparent;border-radius:0;border-top:3px solid transparent;flex-grow:1;list-style-type:none}@media (max-width:413px){.post-body .tabs ul.nav-tabs,.tabs-comment ul.nav-tabs{display:block;margin-bottom:5px}.post-body .tabs ul.nav-tabs li.tab,.tabs-comment ul.nav-tabs li.tab{border-bottom:1px solid transparent;border-left:3px solid transparent;border-right:1px solid transparent;border-top:1px solid transparent;border-radius:0}}.post-body .tabs ul.nav-tabs li.tab a,.tabs-comment ul.nav-tabs li.tab a{border-bottom:initial;display:block;line-height:1.8;padding:.25em .75em;text-align:center;transition:.2s ease-out}.post-body .tabs ul.nav-tabs li.tab a i[class^=fa],.tabs-comment ul.nav-tabs li.tab a i[class^=fa]{width:1.285714285714286em}.post-body .tabs ul.nav-tabs li.tab.active,.tabs-comment ul.nav-tabs li.tab.active{border-color:#fc6423 #ddd transparent}@media (max-width:413px){.post-body .tabs ul.nav-tabs li.tab.active,.tabs-comment ul.nav-tabs li.tab.active{border-color:#ddd #ddd #ddd #fc6423}}.post-body .tabs ul.nav-tabs li.tab.active a,.tabs-comment ul.nav-tabs li.tab.active a{cursor:default}.post-body .tabs .tab-content,.tabs-comment .tab-content{border:1px solid #ddd;border-radius:0;border-top-color:transparent}@media (max-width:413px){.post-body .tabs .tab-content,.tabs-comment .tab-content{border-radius:0;border-top-color:#ddd}}.post-body .tabs .tab-content .tab-pane,.tabs-comment .tab-content .tab-pane{padding:20px 20px 0}.post-body .tabs .tab-content .tab-pane:not(.active),.tabs-comment .tab-content .tab-pane:not(.active){display:none}.pagination .next,.pagination .page-number,.pagination .prev,.pagination .space{display:inline-block;margin:-1px 10px 0;padding:0 10px}.algolia-pagination .current .page-number,.pagination .page-number.current{background:#ccc;border-color:#ccc;color:var(--content-bg-color)}.pagination{border-top:1px solid #eee;margin:120px 0 0;text-align:center}.pagination .next,.pagination .page-number,.pagination .prev{border-bottom:0;border-top:1px solid #eee;transition:border-color .2s ease-in-out}.pagination .next:hover,.pagination .page-number:hover,.pagination .prev:hover{border-top-color:var(--link-hover-color)}@media (max-width:767px){.post-body .link-grid{grid-template-columns:1fr}.pagination .next,.pagination .page-number,.pagination .prev,.pagination .space{margin:0 5px}.pagination{border-top:0}.pagination .next,.pagination .page-number,.pagination .prev{border-bottom:1px solid #eee;border-top:0}.pagination .next:hover,.pagination .page-number:hover,.pagination .prev:hover{border-bottom-color:var(--link-hover-color)}.site-meta{text-align:center}}.pagination .space{margin:0;padding:0}.comments{margin-top:60px;overflow:hidden}.comment-button-group{display:flex;display:flex;flex-wrap:wrap;justify-content:center;justify-content:center;margin:1em 0}.comment-button-group .comment-button{margin:.1em .2em}.comment-button-group .comment-button.active{background:var(--btn-default-hover-bg);border-color:var(--btn-default-hover-border-color);color:var(--btn-default-hover-color)}.comment-position{display:none}.comment-position.active{display:block}.tabs-comment{margin-top:4em;padding-top:0}.tabs-comment .comments{margin-top:0;padding-top:0}.headband{background:var(--theme-color);height:3px}@media (max-width:991px){.headband{display:none}}.site-brand-container{display:flex;flex-shrink:0;padding:0 10px}.use-motion .column,.use-motion .site-brand-container .toggle{opacity:0}.site-meta{flex-grow:1;text-align:center}.custom-logo-image{margin-top:20px}@media (max-width:991px){.custom-logo-image{display:none}}.brand{border-bottom:0;color:var(--brand-color);display:inline-block;padding:0}.brand:hover{color:var(--brand-hover-color)}.site-title{font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;font-size:1.375em;font-weight:400;line-height:1.5;margin:0}.site-subtitle{color:#ddd;font-size:.8125em;margin:10px 10px 0}.use-motion .custom-logo-image,.use-motion .site-subtitle,.use-motion .site-title{opacity:0;position:relative;top:-10px}.site-nav-right,.site-nav-toggle{display:none}.site-nav-right .toggle,.site-nav-toggle .toggle{color:var(--text-color);padding:10px;width:22px}.site-nav-right .toggle .toggle-line,.site-nav-toggle .toggle .toggle-line{background:var(--text-color);border-radius:1px}@media (max-width:767px){.site-nav-right,.site-nav-toggle{display:flex;flex-direction:column;justify-content:center}.site-nav{--scroll-height:0;height:0;overflow:hidden;transition:height .2s ease-in-out,visibility .2s ease-in-out;visibility:hidden}body:not(.site-nav-on) .site-nav .animated{animation:none}body.site-nav-on .site-nav{height:var(--scroll-height);visibility:unset}}.menu{margin:0;padding:1em 0;text-align:center}.menu-item{display:inline-block;list-style:none;margin:0 10px}@media (max-width:767px){.menu-item{display:block;margin-top:10px}.menu-item.menu-item-search{display:none}}.menu-item a{border-bottom:0;display:block;font-size:.8125em;transition:border-color .2s ease-in-out}.menu-item a.menu-item-active,.menu-item a:hover{background:var(--menu-item-bg-color)}.menu-item i[class^=fa]{margin-right:8px}.menu-item .badge{display:inline-block;font-weight:700;line-height:1;margin-left:.35em;margin-top:.35em;text-align:center;white-space:nowrap}.use-motion .menu-item{visibility:hidden}.github-corner :hover .octo-arm{animation:560ms ease-in-out octocat-wave}.github-corner svg{color:#fff;fill:var(--theme-color);position:absolute;right:0;top:0;z-index:5}@media (max-width:991px){.github-corner{display:none}.github-corner svg{color:var(--theme-color);fill:#fff}.github-corner .github-corner:hover .octo-arm{animation:none}.github-corner .github-corner .octo-arm{animation:560ms ease-in-out octocat-wave}}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}.sidebar-inner{color:#999;padding:18px 10px;text-align:center;display:flex;flex-direction:column;justify-content:center}.cc-license .cc-opacity{border-bottom:0;opacity:.7}.cc-license .cc-opacity:hover{opacity:.9}.cc-license img{display:inline-block}.site-author-image{border:1px solid #eee;max-width:120px;padding:2px}.site-author-name{color:var(--text-color);font-weight:600;margin:0}.site-description{color:#999;font-size:.8125em;margin-top:0}.links-of-author a{font-size:.8125em}.links-of-author i[class^=fa]{margin-right:2px}.sidebar .sidebar-button:not(:first-child){margin-top:15px}.sidebar .sidebar-button button{background:0 0;cursor:pointer;line-height:2;padding:0 15px;border-radius:4px}.sidebar .sidebar-button button i[class^=fa]{margin-right:5px}.links-of-blogroll{font-size:.8125em}.links-of-blogroll-title{font-size:.875em;font-weight:600}.links-of-blogroll-list{list-style:none;margin:0;padding:0}.sidebar-nav{font-size:.875em;height:0;margin:0;overflow:hidden;padding-left:0;pointer-events:none;transition:height .2s ease-in-out,visibility .2s ease-in-out;visibility:hidden}.sidebar-nav-active .sidebar-nav{height:calc(2em + 1px);pointer-events:unset;visibility:unset}.sidebar-nav li{border-bottom:1px solid transparent;color:var(--text-color);cursor:pointer;display:inline-block;transition:border-bottom-color .2s ease-in-out,color .2s ease-in-out}.sidebar-nav li.sidebar-nav-overview{margin-left:10px}.sidebar-nav li:hover{color:#fc6423}.sidebar-overview-active .sidebar-nav-overview,.sidebar-toc-active .sidebar-nav-toc{border-bottom-color:#fc6423;color:#fc6423;transition-delay:0.2s}.sidebar-overview-active .sidebar-nav-overview:hover,.sidebar-toc-active .sidebar-nav-toc:hover{color:#fc6423}.sidebar-panel-container{align-items:start;display:grid;flex:1;overflow-x:hidden;overflow-y:auto;padding-top:0;transition:padding-top .2s ease-in-out}.sidebar-nav-active .sidebar-panel-container{padding-top:20px}.sidebar-panel{animation:.2s ease-in-out deactivate-sidebar-panel;grid-area:1/1;height:0;opacity:0;overflow:hidden;pointer-events:none;transform:translateY(0);transition:.2s ease-in-out;transition-property:opacity,transform,visibility;visibility:hidden}.sidebar-nav-active .sidebar-panel,.sidebar-overview-active .sidebar-panel.post-toc-wrap{transform:translateY(-20px)}.sidebar-overview-active:not(.sidebar-nav-active) .sidebar-panel.post-toc-wrap{transition-delay:0s,0.2s,0s}.sidebar-overview-active .sidebar-panel.site-overview-wrap,.sidebar-toc-active .sidebar-panel.post-toc-wrap{animation-name:activate-sidebar-panel;height:auto;opacity:1;pointer-events:unset;transform:translateY(0);transition-delay:0.2s,0.2s,0s;visibility:unset}.sidebar-panel.site-overview-wrap{display:flex;flex-direction:column;justify-content:center;gap:10px;justify-content:flex-start}@keyframes deactivate-sidebar-panel{from{height:var(--inactive-panel-height,0)}to{height:var(--active-panel-height,0)}}@keyframes activate-sidebar-panel{from{height:var(--inactive-panel-height,auto)}to{height:var(--active-panel-height,auto)}}.sidebar-toggle{bottom:61px;height:16px;padding:5px;width:16px;background:#222;cursor:pointer;opacity:.6;position:fixed;z-index:30;right:30px}.sidebar-toggle:hover{opacity:.8}@media (max-width:991px){.sidebar-toggle{right:20px;opacity:.8}}.sidebar-toggle:hover .toggle-line{background:#fc6423}@media (any-hover:hover){body:not(.sidebar-active) .sidebar-toggle:hover :first-child{left:50%;top:2px;transform:rotate(45deg);width:50%}body:not(.sidebar-active) .sidebar-toggle:hover :last-child{left:50%;top:-2px;transform:rotate(-45deg);width:50%}}.sidebar-active .sidebar-toggle :nth-child(2){opacity:0}.sidebar-active .sidebar-toggle :first-child{top:6px;transform:rotate(45deg)}.sidebar-active .sidebar-toggle :last-child{top:-6px;transform:rotate(-45deg)}.post-toc{font-size:.875em}.post-toc ol{list-style:none;margin:0;padding:0 2px 0 10px;text-align:left}.post-toc ol>:last-child{margin-bottom:5px}.post-toc ol>ol{padding-left:0}.post-toc ol a{transition:.2s ease-in-out}.post-toc .nav-item{line-height:1.8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.post-toc .nav .nav-child{--height:0;height:0;opacity:0;overflow:hidden;transition:.2s ease-in-out;visibility:hidden}.post-toc .nav .active>.nav-child{height:var(--height,auto);opacity:1;visibility:unset}.post-toc .nav .active>a{border-bottom-color:#fc6423;color:#fc6423}.post-toc .nav .active-current>a,.post-toc .nav .active-current>a:hover{color:#fc6423}.site-state{display:flex;flex-wrap:wrap;justify-content:center;line-height:1.4}.site-state-item a{border-bottom:0;display:block}.site-state-item-count{display:block;font-size:1em;font-weight:600}.site-state-item-name{color:#999;font-size:.8125em}.sidebar-post-related{font-size:.8125em;padding:18px 0 0}.popular-posts{margin:0;padding:1em 0;text-align:left}.popular-posts .popular-posts-item{display:block}.popular-posts .popular-posts-item .popular-posts-link{border-bottom:0;display:block;padding:5px 20px;transition:background .2s ease-in-out}.popular-posts .popular-posts-item .popular-posts-link:hover{background:var(--menu-item-bg-color)}.popular-posts .popular-posts-item .popular-posts-time{color:#999}.footer{color:#999;font-size:.875em;padding:20px 0;transition:left .2s ease-in-out,right .2s ease-in-out}.footer.footer-fixed{bottom:0;left:0;position:absolute;right:0}.footer-inner{box-sizing:border-box;text-align:center;display:flex;flex-direction:column;justify-content:center;margin:0 auto;width:calc(100% - 20px)}@media (max-width:767px){.menu-item .badge{float:right;margin-left:0}.footer-inner{width:auto}}@media (min-width:1200px){.footer-inner{width:1160px}}@media (min-width:1600px){.footer-inner{width:73%}}.use-motion .footer{opacity:0}.languages{display:inline-block;font-size:1.125em;position:relative}.languages .lang-select-label span{margin:0 .5em}.languages .lang-select{height:100%;left:0;opacity:0;position:absolute;top:0;width:100%}.with-love{color:red;display:inline-block;margin:0 5px}.beian img{display:inline-block;margin:0 3px;vertical-align:middle}.busuanzi-count #busuanzi_container_site_pv,.busuanzi-count #busuanzi_container_site_uv{display:none}@keyframes icon-animate{0%,100%{transform:scale(1)}10%,30%{transform:scale(.9)}20%,40%,50%,60%,70%,80%{transform:scale(1.1)}}@media (max-width:567px){.main-inner{padding:initial!important}.posts-expand .post-header{margin-bottom:10px!important}.post-block{margin-top:initial!important;padding:8px 18px!important}.post-body h1,.post-body h2,.post-body h3,.post-body h4,.post-body h5,.post-body h6{margin:20px 0 8px}.post-body .note h1,.post-body .note h2,.post-body .note h3,.post-body .note h4,.post-body .note h5,.post-body .note h6,.post-body .tabs .tab-content .tab-pane h1,.post-body .tabs .tab-content .tab-pane h2,.post-body .tabs .tab-content .tab-pane h3,.post-body .tabs .tab-content .tab-pane h4,.post-body .tabs .tab-content .tab-pane h5,.post-body .tabs .tab-content .tab-pane h6{margin:0 5px}.post-body>p{margin:0 0 10px}.post-body .note>p,.post-body .tabs .tab-content .tab-pane>p{padding:0 5px}.post-body img,.post-body video{margin-bottom:10px!important}.post-body .fancybox+figcaption,.post-body img+figcaption{margin:-5px auto 15px!important}.post-body .note{margin-bottom:10px!important;padding:10px!important}.post-body .tabs .tab-content .tab-pane{padding:10px 10px 0!important}.post-eof{margin:40px auto 20px!important}.pagination{margin-top:40px}}.back-to-top{font-size:12px;align-items:center;bottom:-100px;color:#fff;display:flex;height:26px;transition:bottom .2s ease-in-out;background:#222;cursor:pointer;opacity:.6;position:fixed;z-index:30;right:30px}.back-to-top span{margin-right:8px;display:none}.back-to-top .fa{text-align:center;width:26px}.back-to-top:hover{opacity:.8;color:#fc6423}.back-to-top.back-to-top-on{bottom:30px}.rtl.post-body a,.rtl.post-body h1,.rtl.post-body h2,.rtl.post-body h3,.rtl.post-body h4,.rtl.post-body h5,.rtl.post-body h6,.rtl.post-body li,.rtl.post-body ol,.rtl.post-body p,.rtl.post-body ul{direction:rtl;font-family:UKIJ Ekran}.rtl.post-title{font-family:UKIJ Ekran}.post-button{margin-top:40px;text-align:center}.use-motion .collection-header,.use-motion .comments,.use-motion .pagination,.use-motion .post-block,.use-motion .post-body,.use-motion .post-header{visibility:hidden}.posts-collapse .post-content{margin-bottom:35px;margin-left:35px;position:relative}@media (max-width:767px){.posts-collapse .post-content{margin-left:0;margin-right:0}}.posts-collapse .post-content .collection-title{font-size:1.125em;position:relative}.posts-collapse .post-content .collection-title::before{background:#999;border:1px solid #fff;margin-left:-6px;margin-top:-4px;position:absolute;top:50%;border-radius:50%;content:' ';height:10px;width:10px}.posts-collapse .post-content .collection-year{font-size:1.5em;font-weight:700;margin:60px 0;position:relative}.posts-collapse .post-content .collection-year::before{background:#bbb;margin-left:-4px;margin-top:-4px;position:absolute;top:50%;border-radius:50%;content:' ';height:8px;width:8px}.posts-collapse .post-content .collection-header{display:block;margin-left:20px}.posts-collapse .post-content .collection-header small{color:#bbb;margin-left:5px}.posts-collapse .post-content .post-header{border-bottom:1px dashed #ccc;margin:30px 2px 0;padding-left:15px;position:relative;transition:border .2s ease-in-out}.posts-collapse .post-content .post-header::before{background:#bbb;border:1px solid #fff;left:-6px;position:absolute;top:.75em;transition:background .2s ease-in-out;border-radius:50%;content:' ';height:6px;width:6px}.posts-collapse .post-content .post-header:hover{border-bottom-color:#666}.posts-collapse .post-content .post-header:hover::before{background:#222}.posts-collapse .post-content .post-meta-container{display:inline;font-size:.75em;margin-right:10px}.posts-collapse .post-content .post-title{display:inline}.posts-collapse .post-content .post-title a{border-bottom:0;color:var(--link-color)}.posts-collapse .post-content::before{background:#f5f5f5;content:' ';height:100%;margin-left:-2px;position:absolute;top:1.25em;width:4px}.post-body{font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;overflow-wrap:break-word}@media (min-width:1200px){.post-body{font-size:1.125em}}@media (min-width:992px){.post-body{text-align:justify}}.post-body h1 .header-anchor,.post-body h1 .headerlink,.post-body h2 .header-anchor,.post-body h2 .headerlink,.post-body h3 .header-anchor,.post-body h3 .headerlink,.post-body h4 .header-anchor,.post-body h4 .headerlink,.post-body h5 .header-anchor,.post-body h5 .headerlink,.post-body h6 .header-anchor,.post-body h6 .headerlink{border-bottom-style:none;color:inherit;float:right;font-size:.875em;margin-left:10px;opacity:0}.post-body h1 .header-anchor::before,.post-body h1 .headerlink::before,.post-body h2 .header-anchor::before,.post-body h2 .headerlink::before,.post-body h3 .header-anchor::before,.post-body h3 .headerlink::before,.post-body h4 .header-anchor::before,.post-body h4 .headerlink::before,.post-body h5 .header-anchor::before,.post-body h5 .headerlink::before,.post-body h6 .header-anchor::before,.post-body h6 .headerlink::before{content:'\f0c1';font-family:'Font Awesome 6 Free';font-weight:900}.post-body h1:hover .header-anchor,.post-body h1:hover .headerlink,.post-body h2:hover .header-anchor,.post-body h2:hover .headerlink,.post-body h3:hover .header-anchor,.post-body h3:hover .headerlink,.post-body h4:hover .header-anchor,.post-body h4:hover .headerlink,.post-body h5:hover .header-anchor,.post-body h5:hover .headerlink,.post-body h6:hover .header-anchor,.post-body h6:hover .headerlink{opacity:.5}.post-body h1:hover .header-anchor:hover,.post-body h1:hover .headerlink:hover,.post-body h2:hover .header-anchor:hover,.post-body h2:hover .headerlink:hover,.post-body h3:hover .header-anchor:hover,.post-body h3:hover .headerlink:hover,.post-body h4:hover .header-anchor:hover,.post-body h4:hover .headerlink:hover,.post-body h5:hover .header-anchor:hover,.post-body h5:hover .headerlink:hover,.post-body h6:hover .header-anchor:hover,.post-body h6:hover .headerlink:hover{opacity:1}.post-body .exturl .fa{font-size:.875em;margin-left:4px}.post-body .fancybox+figcaption,.post-body img+figcaption{color:#999;font-size:.875em;font-weight:700;line-height:1;margin:-15px auto 15px;text-align:center}.post-body embed,.post-body iframe,.post-body img,.post-body video{margin-bottom:20px}.post-body .video-container{height:0;margin-bottom:20px;overflow:hidden;padding-top:75%;position:relative;width:100%}.post-body .video-container embed,.post-body .video-container iframe,.post-body .video-container object{height:100%;left:0;margin:0;position:absolute;top:0;width:100%}.post-gallery{display:flex;min-height:200px}.post-gallery .post-gallery-image{flex:1}.post-gallery .post-gallery-image:not(:first-child){clip-path:polygon(40px 0,100% 0,100% 100%,0 100%);margin-left:-20px}.post-gallery .post-gallery-image:not(:last-child){margin-right:-20px}.post-gallery .post-gallery-image img{height:100%;object-fit:cover;opacity:1;width:100%}.posts-expand .post-gallery{margin-bottom:60px}.posts-collapse .post-gallery{margin:15px 0}.posts-expand .post-header{font-size:1.125em;margin-bottom:60px;text-align:center}.posts-expand .post-title{font-size:1.5em;font-weight:400;margin:initial;overflow-wrap:break-word}.posts-expand .post-title-link{border-bottom:0;color:var(--link-color);display:inline-block;position:relative}.posts-expand .post-title-link::before{background:var(--link-color);bottom:0;content:'';height:2px;left:0;position:absolute;transform:scaleX(0);transition:transform .2s ease-in-out;width:100%}.posts-expand .post-title-link:hover::before{transform:scaleX(1)}.posts-expand .post-title-link .fa{font-size:.875em;margin-left:5px}.post-sticky-flag{display:inline-block;margin-right:8px;transform:rotate(30deg)}.posts-expand .post-meta-container{color:#999;font-family:Lato,'PingFang SC','Microsoft YaHei',sans-serif;font-size:.75em;margin-top:3px}.posts-expand .post-meta-container .post-description{font-size:.875em;margin-top:2px}.posts-expand .post-meta-container time{border-bottom:1px dashed #999}.post-meta{display:flex;flex-wrap:wrap;justify-content:center}:not(.post-meta-break)+.post-meta-item::before{content:'|';margin:0 .5em}.post-meta-item-icon{margin-right:3px}@media (max-width:991px){.back-to-top{right:20px;opacity:.8}.post-body{text-align:justify}.post-meta-item-text{display:none}}.post-meta-break{flex-basis:100%;height:0}#busuanzi_container_page_pv{display:none}.post-nav{border-top:1px solid #eee;display:flex;gap:30px;justify-content:space-between;margin-top:1em;padding:10px 5px 0}.post-nav-item{flex:1}.post-nav-item a{border-bottom:0;display:block;font-size:.875em;line-height:1.6}.post-nav-item a:active{top:2px}.post-nav-item .fa{font-size:.75em}.post-nav-item:first-child .fa{margin-right:5px}.post-nav-item:last-child{text-align:right}.post-nav-item:last-child .fa{margin-left:5px}.post-footer{display:flex;flex-direction:column;justify-content:center}.post-eof{background:#ccc;height:1px;margin:80px auto 60px;width:8%}.post-block:last-of-type .post-eof{display:none}.post-copyright ul{list-style:none;overflow:hidden;padding:.5em 1em;position:relative;background:var(--card-bg-color);border-left:3px solid #ff2a2a;margin:1em 0 0}.post-copyright ul::after{content:'\f25e';font-family:'Font Awesome 6 Brands';font-size:200px;opacity:.1;position:absolute;right:-50px;top:-150px}.post-tags{margin-top:40px;text-align:center}.post-tags a{display:inline-block;font-size:.8125em}.post-tags a:not(:last-child){margin-right:10px}.social-like{border-top:1px solid #eee;font-size:.875em;margin-top:1em;padding-top:1em;display:flex;flex-wrap:wrap;justify-content:center}.social-like a{border-bottom:none}.reward-container{margin:1em 0 0;padding:1em 0;text-align:center}.reward-container button{background:0 0;color:#fc6423;cursor:pointer;line-height:2;padding:0 15px;border:2px solid #fc6423;border-radius:2px;outline:0;transition:.2s ease-in-out;vertical-align:text-top}.reward-container button:hover{background:#fc6423;color:#fff}.post-reward{display:none;padding-top:20px}.post-reward.active{display:block}.post-reward div{display:inline-block}.post-reward div span{display:block}.post-reward img{display:inline-block;margin:.8em 2em 0;max-width:100%;width:180px}@keyframes next-roll{from{transform:rotateZ(30deg)}to{transform:rotateZ(-30deg)}}.category-all-page .category-all-title{text-align:center}.category-all-page .category-all{margin-top:20px}.category-all-page .category-list{list-style:none;margin:0;padding:0}.category-all-page .category-list-item{margin:5px 10px}.category-all-page .category-list-count{color:#bbb}.category-all-page .category-list-count::before{content:' ('}.category-all-page .category-list-count::after{content:') '}.category-all-page .category-list-child{padding-left:10px}.event-list hr{background:#222;margin:20px 0 45px}.event-list hr::after{background:#222;color:#fff;content:'NOW';display:inline-block;font-weight:700;padding:0 5px}.event-list .event{--event-background:#222;--event-foreground:#bbb;--event-title:#fff;background:var(--event-background);padding:15px}.event-list .event .event-summary{border-bottom:0;color:var(--event-title);margin:0;padding:0 0 0 35px;position:relative}.event-list .event .event-summary::before{animation:1s ease-in-out infinite alternate dot-flash;background:var(--event-title);left:0;margin-top:-6px;position:absolute;top:50%;border-radius:50%;content:' ';height:12px;width:12px}.event-list .event:nth-of-type(odd) .event-summary::before{animation-delay:.5s}.event-list .event:not(:last-child){margin-bottom:20px}.event-list .event .event-relative-time{color:var(--event-foreground);display:inline-block;font-size:12px;font-weight:400;padding-left:12px}.event-list .event .event-details{color:var(--event-foreground);display:block;line-height:18px;padding:6px 0 6px 35px}.event-list .event .event-details::before{color:var(--event-foreground);display:inline-block;margin-right:9px;width:14px;font-family:'Font Awesome 6 Free';font-weight:900}.event-list .event .event-details.event-location::before{content:'\f041'}.event-list .event .event-details.event-duration::before{content:'\f017'}.event-list .event .event-details.event-description::before{content:'\f024'}.event-list .event-past{--event-background:#f5f5f5;--event-foreground:#999;--event-title:#222}@keyframes dot-flash{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.8)}}ul.breadcrumb{font-size:.75em;list-style:none;margin:1em 0;padding:0 2em;text-align:center}ul.breadcrumb li{display:inline}ul.breadcrumb li:not(:first-child)::before{content:'/\00a0';font-weight:400;padding:.5em}ul.breadcrumb li:last-child{font-weight:700}.tag-cloud{text-align:center}.tag-cloud a{display:inline-block;margin:10px}.tag-cloud-0{border-bottom-color:#aaa;color:#aaa}.tag-cloud-1{border-bottom-color:#9a9a9a;color:#9a9a9a}.tag-cloud-2{border-bottom-color:#8b8b8b;color:#8b8b8b}.tag-cloud-3{border-bottom-color:#7c7c7c;color:#7c7c7c}.tag-cloud-4{border-bottom-color:#6c6c6c;color:#6c6c6c}.tag-cloud-5{border-bottom-color:#5d5d5d;color:#5d5d5d}.tag-cloud-6{border-bottom-color:#4e4e4e;color:#4e4e4e}.tag-cloud-7{border-bottom-color:#3e3e3e;color:#3e3e3e}.tag-cloud-8{border-bottom-color:#2f2f2f;color:#2f2f2f}.tag-cloud-9{border-bottom-color:#202020;color:#202020}.tag-cloud-10{border-bottom-color:#111;color:#111}.search-active{overflow:hidden}.search-pop-overlay{background:rgba(0,0,0,0);display:flex;height:100%;left:0;position:fixed;top:0;transition:visibility .4s,background .4s;visibility:hidden;width:100%;z-index:40}.search-active .search-pop-overlay{background:rgba(0,0,0,.3);visibility:visible}.search-popup{background:var(--card-bg-color);border-radius:5px;height:80%;margin:auto;transform:scale(0);transition:transform .4s;width:700px}.search-active .search-popup{transform:scale(1)}@media (max-width:767px){.search-popup{border-radius:0;height:100%;width:100%}}.search-popup .popup-btn-close,.search-popup .search-icon{color:#999;font-size:18px;padding:0 10px}.search-popup .popup-btn-close{cursor:pointer}.search-popup .popup-btn-close:hover .fa{color:#222}.search-popup .search-header{background:#eee;border-top-left-radius:5px;border-top-right-radius:5px;display:flex;padding:5px}.search-popup input.search-input{background:0 0;border:0;outline:0;width:100%}.search-popup input.search-input::-webkit-search-cancel-button{display:none}.search-popup .search-result-container{height:calc(100% - 55px);overflow:auto;padding:5px 25px}.search-popup .search-result-container hr{margin:5px 0 10px}.search-popup .search-result-container hr:first-child{display:none}.search-popup .search-result-list{margin:0 5px;padding:0}.search-popup a.search-result-title{font-weight:700}.search-popup p.search-result{border-bottom:1px dashed #ccc;padding:5px 0}.search-input-container{flex-grow:1}.search-input-container form{padding:2px}.search-stats{align-items:center;display:flex;justify-content:space-between}.search-stats img{height:1em;margin:0}.algolia-pagination{margin:40px 0;opacity:1;padding:0}.algolia-pagination .pagination-item{display:inline-block}.algolia-pagination .current .page-number{cursor:default}.algolia-pagination .disabled-item{visibility:hidden}.use-motion .animated{animation-fill-mode:none;visibility:inherit}.use-motion .sidebar .animated{animation-fill-mode:both}header.header{background:var(--content-bg-color);border-radius:initial;box-shadow:initial}.main{align-items:stretch;display:flex;justify-content:space-between;margin:0 auto;width:calc(100% - 20px)}@media (max-width:767px){.main{width:auto}}@media (min-width:1200px){.main{width:1160px}}@media (min-width:1600px){.main{width:73%}}@media (max-width:991px){header.header{border-radius:initial}.main{display:block;width:auto}}.main-inner{border-radius:initial;box-sizing:border-box;width:calc(100% - 252px)}.footer-inner{padding-left:252px}@media (max-width:991px){.main-inner{border-radius:initial;width:100%}.footer-inner{padding-left:0;padding-right:0;width:auto}}.column{width:240px}.site-brand-container{background:var(--theme-color)}.site-meta{padding:20px 0}.site-nav-right .toggle,.site-nav-toggle .toggle{color:#fff}.site-nav-right .toggle .toggle-line,.site-nav-toggle .toggle .toggle-line{background:#fff}@media (min-width:768px) and (max-width:991px){.site-nav-right,.site-nav-toggle{display:flex;flex-direction:column;justify-content:center}.site-nav{--scroll-height:0;height:0;overflow:hidden;transition:height .2s ease-in-out,visibility .2s ease-in-out;visibility:hidden}body:not(.site-nav-on) .site-nav .animated{animation:none}body.site-nav-on .site-nav{height:var(--scroll-height);visibility:unset}}.menu .menu-item{display:block;margin:0}.menu .menu-item a{padding:5px 20px;position:relative;text-align:left;transition-property:background-color}.menu .menu-item .badge{background:#ccc;border-radius:10px;color:var(--content-bg-color);float:right;padding:2px 5px;text-shadow:1px 1px 0 rgba(0,0,0,.1)}.main-menu .menu-item-active::after{background:#bbb;border-radius:50%;content:' ';height:6px;margin-top:-3px;position:absolute;right:15px;top:50%;width:6px}.sub-menu{margin:0;padding:6px 0}.sub-menu .menu-item{display:inline-block}.sub-menu .menu-item a{background:0 0;margin:5px 10px;padding:initial}.sub-menu .menu-item a:hover{background:0 0;color:#fc6423}.sub-menu .menu-item-active{border-bottom-color:#fc6423;color:#fc6423}.sub-menu .menu-item-active:hover{border-bottom-color:#fc6423}.sidebar{position:-webkit-sticky;position:sticky;top:12px}@media (max-width:991px){.column{width:auto}.site-nav-on .site-brand-container{box-shadow:0 0 16px rgba(0,0,0,.5)}.menu .menu-item.menu-item-search,.sidebar{display:none}}.sidebar-inner{background:var(--content-bg-color);border-radius:initial;box-shadow:initial;box-sizing:border-box;color:var(--text-color);margin-top:12px;max-height:calc(100vh - 24px);visibility:hidden}.site-state-item{padding:0 10px}.sidebar .sidebar-button{border-bottom:1px dotted #ccc;border-top:1px dotted #ccc}.sidebar .sidebar-button button{border:0;color:#fc6423;display:block;width:100%}.sidebar .sidebar-button button:hover{background:0 0;border:0;color:#e34603}.links-of-author{display:flex;flex-wrap:wrap;justify-content:center}.links-of-author-item{margin:5px 0 0;width:50%}.links-of-author-item a{box-sizing:border-box;max-width:100%;overflow:hidden;padding:0 5px;text-overflow:ellipsis;white-space:nowrap;border-bottom:0;border-radius:4px;display:block}.links-of-author-item a:hover{background:var(--body-bg-color)}.main-inner{background:var(--content-bg-color);box-shadow:initial;padding:40px}@media (max-width:991px){.main-inner{padding:20px}}.sub-menu{border-bottom:1px solid #ddd}.post-block:first-of-type{padding-top:40px}@media (max-width:767px){.pagination{margin-bottom:10px}}</style><link as=style href=https://fonts.googleapis.com/css?family=Lato:300,300italic,400,400italic,700,700italic&display=swap&subset=latin,latin-ext onload=this.rel='stylesheet' rel=preload><link crossorigin=anonymous href=https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css integrity=sha256-yIDrPSXHZdOZhAqiBP7CKzIwMQmRCJ8UeB8Jo17YC4o= rel=stylesheet><link crossorigin=anonymous href=https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.1.1/animate.min.css integrity=sha256-PR7ttpcvz8qrF57fur/yAx1qXMFJeJFiA6pSzWi0OIE= rel=stylesheet><link crossorigin=anonymous href=https://cdnjs.cloudflare.com/ajax/libs/fancyapps-ui/5.0.28/fancybox/fancybox.css integrity=sha256-6cQIC71/iBIYXFK+0RHAvwmjwWzkWd+r7v/BX3/vZDc= rel=stylesheet><script class=next-config data-name=main type=application/json>{"hostname":"nicksxs.me","root":"/","images":"/images","scheme":"Pisces","darkmode":false,"version":"8.19.1","exturl":false,"sidebar":{"position":"left","display":"post","padding":18,"offset":12},"copycode":{"enable":true,"style":"default"},"fold":{"enable":false,"height":500},"bookmark":{"enable":false,"color":"#222","save":"auto"},"mediumzoom":false,"lazyload":true,"pangu":false,"comments":{"style":"tabs","active":null,"storage":true,"lazyload":false,"nav":null},"stickytabs":false,"motion":{"enable":true,"async":false,"transition":{"menu_item":"fadeInDown","post_block":"fadeIn","post_header":"fadeInDown","post_body":"fadeInDown","coll_header":"fadeInLeft","sidebar":"fadeInUp"}},"prism":false,"i18n":{"placeholder":"搜索...","empty":"没有找到任何搜索结果:${query}","hits_time":"找到 ${hits} 个搜索结果(用时 ${time} 毫秒)","hits":"找到 ${hits} 个搜索结果"},"algolia":{"appID":"663Q99OQQD","apiKey":"e461a1371d12cec1491c1153b288a9ed","indexName":"nicksxs","hits":{"per_page":10}}}</script><script src=/js/config.js></script><meta content="learn from zero,技术博客,Nicksxs,史学森" name=description><meta content=website property=og:type><meta content="Nicksxs's Blog" property=og:title><meta content=https://nicksxs.me/index.html property=og:url><meta content="Nicksxs's Blog" property=og:site_name><meta content="learn from zero,技术博客,Nicksxs,史学森" property=og:description><meta content=zh_CN property=og:locale><meta content=Nicksxs property=article:author><meta content=Nicksxs,史学森,米方方,米方方的男朋友,森哥 property=article:tag><meta content=summary name=twitter:card><link href=https://nicksxs.me/ rel=canonical><script class=next-config data-name=page type=application/json>{"sidebar":"","isHome":true,"isPost":false,"lang":"zh-CN","comments":"","permalink":"","path":"index.html","title":""}</script><script class=next-config data-name=calendar type=application/json>""</script><title>Nicksxs's Blog - What hurts more, the pain of hard work or the pain of regret?</title><script async src=https://www.googletagmanager.com/gtag/js?id=UA-61358619-1></script><script class=next-config data-name=google_analytics type=application/json>{"tracking_id":"UA-61358619-1","only_pageview":false,"measure_protocol_api_secret":null}</script><script src=/js/third-party/analytics/google-analytics.js></script><script src=/js/third-party/analytics/baidu-analytics.js></script><script async src=https://hm.baidu.com/hm.js?20f33b3c0c0eff9b1522999c0015646d></script><noscript><link href=/css/noscript.css rel=stylesheet></noscript><link title="Nicksxs's Blog" href=/atom.xml rel=alternate type=application/atom+xml><body class=use-motion itemscope itemtype=http://schema.org/WebPage><div class=headband></div><main class=main><div class=column><header class=header itemscope itemtype=http://schema.org/WPHeader><div class=site-brand-container><div class=site-nav-toggle><div aria-label=切换导航栏 class=toggle role=button><span class=toggle-line></span><span class=toggle-line></span><span class=toggle-line></span></div></div><div class=site-meta><a class=brand href=/ rel=start><i class=logo-line></i><h1 class=site-title>Nicksxs's Blog</h1><i class=logo-line></i></a><p class=site-subtitle itemprop=description>What hurts more, the pain of hard work or the pain of regret?</div><div class=site-nav-right><div class="toggle popup-trigger" aria-label=搜索 role=button><i class="fa fa-search fa-fw fa-lg"></i></div></div></div><nav class=site-nav><ul class="main-menu menu"><li class="menu-item menu-item-home"><a href=/ rel=section><i class="fa fa-home fa-fw"></i>首页</a><li class="menu-item menu-item-about"><a href=/about/ rel=section><i class="fa fa-user fa-fw"></i>关于我</a><li class="menu-item menu-item-mirror"><a href=https://nicksxs.com/ rel=section target=_blank><i class="fa fa-user fa-fw"></i>国内镜像</a><li class="menu-item menu-item-tags"><a href=/tags/ rel=section><i class="fa fa-tags fa-fw"></i>标签</a><li class="menu-item menu-item-categories"><a href=/categories/ rel=section><i class="fa fa-th fa-fw"></i>分类</a><li class="menu-item menu-item-archives"><a href=/archives/ rel=section><i class="fa fa-archive fa-fw"></i>归档</a><li class="menu-item menu-item-top"><a href=/top/ rel=section><i class="fa fa-th fa-fw"></i>热度</a><li class="menu-item menu-item-sitemap"><a href=/sitemap.xml rel=section><i class="fa fa-sitemap fa-fw"></i>站点地图</a><li class="menu-item menu-item-commonweal"><a href=/404/ rel=section><i class="fa fa-heartbeat fa-fw"></i>公益 404</a><li class="menu-item menu-item-search"><a class=popup-trigger role=button><i class="fa fa-search fa-fw"></i>搜索</a></ul></nav><div class=search-pop-overlay><div class="popup search-popup"><div class=search-header><span class=search-icon><i class="fa fa-search"></i></span><div class=search-input-container></div><span class=popup-btn-close role=button><i class="fa fa-times-circle"></i></span></div><div class=search-result-container><div class=algolia-stats><hr></div><div class=algolia-hits></div><div class=algolia-pagination></div></div></div></div></header><aside class=sidebar><div class="sidebar-inner sidebar-overview-active"><ul class=sidebar-nav><li class=sidebar-nav-toc>文章目录<li class=sidebar-nav-overview>站点概览</ul><div class=sidebar-panel-container><div class="post-toc-wrap sidebar-panel"></div><div class="site-overview-wrap sidebar-panel"><div class="site-author animated" itemprop=author itemscope itemtype=http://schema.org/Person><img alt=Nicksxs class=site-author-image itemprop=image src=/uploads/avatar.jpg><p class=site-author-name itemprop=name>Nicksxs<div class=site-description itemprop=description>learn from zero,技术博客,Nicksxs,史学森</div></div><div class="site-state-wrap animated"><nav class=site-state><div class="site-state-item site-state-posts"><a href=/archives/><span class=site-state-item-count>380</span> <span class=site-state-item-name>日志</span></a></div><div class="site-state-item site-state-categories"><a href=/categories/><span class=site-state-item-count>181</span> <span class=site-state-item-name>分类</span></a></div><div class="site-state-item site-state-tags"><a href=/tags/><span class=site-state-item-count>319</span> <span class=site-state-item-name>标签</span></a></div></nav></div><div class="links-of-author animated"><span class=links-of-author-item><a rel="noopener me" title="GitHub → https://github.com/nicksxs" href=https://github.com/nicksxs target=_blank><i class="fab fa-github fa-fw"></i>GitHub</a> </span><span class=links-of-author-item><a rel="noopener me" title="E-Mail → mailto:nicksxs1202@gmail.com" href=mailto:nicksxs1202@gmail.com target=_blank><i class="fa fa-envelope fa-fw"></i>E-Mail</a></span></div><div class="cc-license animated" itemprop=license><a class=cc-opacity href=https://creativecommons.org/licenses/by-nc-sa/4.0/ rel=noopener target=_blank><img alt="Creative Commons" src=https://cdnjs.cloudflare.com/ajax/libs/creativecommons-vocabulary/2020.11.3/assets/license_badges/small/by_nc_sa.svg></a></div><div class=post-gallery itemscope itemtype=http://schema.org/ImageGallery style=height:120px;min-height:120px><div class=post-gallery-row><a class="post-gallery-img fancybox" href=https://url.cn/LLWrL7gx itemprop=url itemscope itemtype=http://schema.org/ImageObject rel=gallery_ target=_blank><img alt=腾讯云推广 itemprop=contentUrl src=https://img.nicksxs.com/blog/345X200.jpg style=padding:12px></a></div></div><div data-text-font="Trebuchet MS, Helvetica" class=widget-wrap data-canvas-id=sidebar-tag-cloud-canvas data-freeze-active=true data-max-speed=0.1 data-outline-colour=#E2E1D1 data-script-src=/js/tagcanvas.js data-tag-cloud data-text-colour=#333 data-text-height=20><div class="widget tagcloud" data-tag-cloud-container id=myCanvasContainer><canvas height=250 id=sidebar-tag-cloud-canvas style=width:100% width=250><ul class=tag-list itemprop=keywords><li class=tag-list-item><a class=tag-list-link href=/tags/Java/ rel=tag>Java</a><li class=tag-list-item><a class=tag-list-link href=/tags/java/ rel=tag>java</a><li class=tag-list-item><a class=tag-list-link href=/tags/leetcode/ rel=tag>leetcode</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E7%94%9F%E6%B4%BB/ rel=tag>生活</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E9%A2%98%E8%A7%A3/ rel=tag>题解</a><li class=tag-list-item><a class=tag-list-link href=/tags/LLM/ rel=tag>LLM</a><li class=tag-list-item><a class=tag-list-link href=/tags/SpringBoot/ rel=tag>SpringBoot</a><li class=tag-list-item><a class=tag-list-link href=/tags/c/ rel=tag>c++</a><li class=tag-list-item><a class=tag-list-link href=/tags/Tomcat/ rel=tag>Tomcat</a><li class=tag-list-item><a class=tag-list-link href=/tags/Mysql/ rel=tag>Mysql</a><li class=tag-list-item><a class=tag-list-link href=/tags/Mybatis/ rel=tag>Mybatis</a><li class=tag-list-item><a class=tag-list-link href=/tags/redis/ rel=tag>redis</a><li class=tag-list-item><a class=tag-list-link href=/tags/mysql/ rel=tag>mysql</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ rel=tag>数据结构</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E6%BA%90%E7%A0%81/ rel=tag>源码</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E5%B0%8F%E6%8A%80%E5%B7%A7/ rel=tag>小技巧</a><li class=tag-list-item><a class=tag-list-link href=/tags/MQ/ rel=tag>MQ</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/ rel=tag>消息队列</a><li class=tag-list-item><a class=tag-list-link href=/tags/RocketMQ/ rel=tag>RocketMQ</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E8%BF%90%E5%8A%A8/ rel=tag>运动</a><li class=tag-list-item><a class=tag-list-link href=/tags/php/ rel=tag>php</a><li class=tag-list-item><a class=tag-list-link href=/tags/Spring/ rel=tag>Spring</a><li class=tag-list-item><a class=tag-list-link href=/tags/linux/ rel=tag>linux</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E5%87%8F%E8%82%A5/ rel=tag>减肥</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E8%B7%91%E6%AD%A5/ rel=tag>跑步</a><li class=tag-list-item><a class=tag-list-link href=/tags/Dubbo/ rel=tag>Dubbo</a><li class=tag-list-item><a class=tag-list-link href=/tags/headscale/ rel=tag>headscale</a><li class=tag-list-item><a class=tag-list-link href=/tags/Docker/ rel=tag>Docker</a><li class=tag-list-item><a class=tag-list-link href=/tags/git/ rel=tag>git</a><li class=tag-list-item><a class=tag-list-link href=/tags/nas/ rel=tag>nas</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E5%B9%B2%E6%B4%BB/ rel=tag>干活</a><li class=tag-list-item><a class=tag-list-link href=/tags/Windows/ rel=tag>Windows</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E7%AE%97%E6%B3%95/ rel=tag>算法</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E5%B7%A5%E5%85%B7/ rel=tag>工具</a><li class=tag-list-item><a class=tag-list-link href=/tags/llm/ rel=tag>llm</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E7%B3%9F%E5%BF%83%E4%BA%8B/ rel=tag>糟心事</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/ rel=tag>年终总结</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E8%AF%BB%E5%90%8E%E6%84%9F/ rel=tag>读后感</a><li class=tag-list-item><a class=tag-list-link href=/tags/Apollo/ rel=tag>Apollo</a><li class=tag-list-item><a class=tag-list-link href=/tags/RPC/ rel=tag>RPC</a><li class=tag-list-item><a class=tag-list-link href=/tags/nginx/ rel=tag>nginx</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E5%89%8A%E5%B3%B0%E5%A1%AB%E8%B0%B7/ rel=tag>削峰填谷</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/ rel=tag>中间件</a><li class=tag-list-item><a class=tag-list-link href=/tags/markdown/ rel=tag>markdown</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E8%A7%84%E5%88%99/ rel=tag>规则</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E6%9D%AD%E5%B7%9E/ rel=tag>杭州</a><li class=tag-list-item><a class=tag-list-link href=/tags/2021/ rel=tag>2021</a><li class=tag-list-item><a class=tag-list-link href=/tags/aqs/ rel=tag>aqs</a><li class=tag-list-item><a class=tag-list-link href=/tags/C/ rel=tag>C++</a><li class=tag-list-item><a class=tag-list-link href=/tags/Disruptor/ rel=tag>Disruptor</a><li class=tag-list-item><a class=tag-list-link href=/tags/Binary-Tree/ rel=tag>Binary Tree</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E4%BA%8C%E5%8F%89%E6%A0%91/ rel=tag>二叉树</a><li class=tag-list-item><a class=tag-list-link href=/tags/grep/ rel=tag>grep</a><li class=tag-list-item><a class=tag-list-link href=/tags/namespace/ rel=tag>namespace</a><li class=tag-list-item><a class=tag-list-link href=/tags/ssh/ rel=tag>ssh</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91/ rel=tag>端口转发</a><li class=tag-list-item><a class=tag-list-link href=/tags/Rust/ rel=tag>Rust</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E6%89%80%E6%9C%89%E6%9D%83/ rel=tag>所有权</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E5%86%85%E5%AD%98%E5%88%86%E5%B8%83/ rel=tag>内存分布</a><li class=tag-list-item><a class=tag-list-link href=/tags/%E6%96%B0%E8%AF%AD%E8%A8%80/ rel=tag>新语言</a></ul></canvas></div></div><script defer src=/js/tagcloud-loader.js></script></div></div></div><div class="sidebar-inner sidebar-blogroll"><div class="links-of-blogroll animated"><div class=links-of-blogroll-title><i class="fa fa-globe fa-fw"></i> 链接</div><ul class=links-of-blogroll-list><li class=links-of-blogroll-item><a href=https://covermusic.cn/ rel=noopener target=_blank title=https://covermusic.cn>69伙伴</a></ul></div></div></aside></div><div class="main-inner index posts-expand"><div class=post-block><article class=post-content itemscope itemtype=http://schema.org/Article><link href=https://nicksxs.me/2026/07/19/%E8%AE%B0%E4%B8%80%E4%B8%AAgiea%E6%8E%A8%E9%80%81%E5%A4%B1%E8%B4%A5%E7%9A%84%E9%97%AE%E9%A2%98/ itemprop=mainEntityOfPage><span hidden itemprop=author itemscope itemtype=http://schema.org/Person><meta content=/uploads/avatar.jpg itemprop=image><meta content=Nicksxs itemprop=name></span><span hidden itemprop=publisher itemscope itemtype=http://schema.org/Organization><meta content="Nicksxs's Blog" itemprop=name><meta content="learn from zero,技术博客,Nicksxs,史学森" itemprop=description></span><span hidden itemprop=post itemscope itemtype=http://schema.org/CreativeWork><meta content="undefined | Nicksxs's Blog" itemprop=name><meta itemprop=description></span><header class=post-header><h2 itemprop="name headline" class=post-title><a class=post-title-link href=/2026/07/19/%E8%AE%B0%E4%B8%80%E4%B8%AAgiea%E6%8E%A8%E9%80%81%E5%A4%B1%E8%B4%A5%E7%9A%84%E9%97%AE%E9%A2%98/ itemprop=url>记一个gitea推送失败的问题</a></h2><div class=post-meta-container><div class=post-meta><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-calendar"></i> </span><span class=post-meta-item-text>发表于</span> <time itemprop="dateCreated datePublished" title="创建时间:2026-07-19 20:53:14" datetime=2026-07-19T20:53:14+08:00>2026-07-19</time> </span><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-folder"></i> </span><span class=post-meta-item-text>分类于</span> <span itemprop=about itemscope itemtype=http://schema.org/Thing><a href=/categories/git/ itemprop=url rel=index><span itemprop=name>git</span></a> </span></span><span class="post-meta-item cloudflare-page-counter" data-increment=false data-path=/2026/07/19/%E8%AE%B0%E4%B8%80%E4%B8%AAgiea%E6%8E%A8%E9%80%81%E5%A4%B1%E8%B4%A5%E7%9A%84%E9%97%AE%E9%A2%98/ data-title=记一个gitea推送失败的问题 title=阅读次数><span class=post-meta-item-icon><i class="far fa-eye"></i> </span><span class=post-meta-item-text>阅读次数:</span> <span class=cloudflare-page-counter-count></span> </span><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-comment"></i> </span><span class=post-meta-item-text>Disqus:</span> <a href=/2026/07/19/%E8%AE%B0%E4%B8%80%E4%B8%AAgiea%E6%8E%A8%E9%80%81%E5%A4%B1%E8%B4%A5%E7%9A%84%E9%97%AE%E9%A2%98/#disqus_thread itemprop=discussionUrl title=disqus><span class="post-comments-count disqus-comment-count" data-disqus-identifier=2026/07/19/记一个giea推送失败的问题/ itemprop=commentCount></span></a></span></div></div></header><div class=post-body itemprop=articleBody><p>我的博客的推送一份是在github<br>作为github page,另一份是在自建的gitea上面<br>上周突然就出现推送失败了<br>在gpt5.5老师的指导下一顿查gitea服务器的日志也没查出来原因<br>一开始说是可以尝试从https切换到ssh形式也没起作用<br>服务端日志也没有用的<br>然后这次升级了gpt5.6老师以后,就再问了下<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>send-pack: unexpected disconnect while reading sideband packet</span><br><span class=line>Writing objects: 100% (6455/6455), 2.95 MiB | 1.53 MiB/s, done.</span><br><span class=line>Total 6455 (delta 2752), reused 6281 (delta 2580), pack-reused 0</span><br><span class=line>fatal: the remote end hung up unexpectedly</span><br></pre></table></figure><p>这次的日志比较有价值可能<br>另外还有个信息是之前<code>git gc</code>因为仓库的小文件太多了可能<br>然后这次gpt老师给出的方法是<figure class="highlight shell"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>http.postBuffer = 16777216</span><br><span class=line>http.version = HTTP/1.1</span><br></pre></table></figure><p>把postBuffer给调大了,然后直接在仓库里执行<figure class="highlight shell"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>git -C .deploy_git push --verbose</span><br></pre></table></figure><p>发现直接可以了<br>具体原因分析下来是: 推送产生的请求体约为 2.95 MiB,超过了 Git 默认 1 MiB 的 http.postBuffer。<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br></pre><td class=code><pre><span class=line>推送数据 2.95 MiB</span><br><span class=line> ↓ 超过默认缓冲区 1 MiB</span><br><span class=line>Git 改用 chunked 分块传输</span><br><span class=line> ↓</span><br><span class=line>Traefik / Gitea 链路没有正确处理该请求</span><br><span class=line> ↓</span><br><span class=line>返回 HTTP 200,但响应体为 0</span><br><span class=line> ↓</span><br><span class=line>Git 收不到 receive-pack 的 sideband 结果</span><br><span class=line> ↓</span><br><span class=line>unexpected disconnect</span><br></pre></table></figure><p>因为默认的postBuffer不够大,然后使用了chunked传输,是指一块块传,直到最后一块大小为0<br>然后因为我的gitea可能经过了traefik和gitea中间有不支持的<br>关于postBuffer在git官方文档也有解释<h3 id=http-postBuffer><a class=headerlink href=#http-postBuffer title=http.postBuffer></a>http.postBuffer</h3><figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>Maximum size in bytes of the buffer used by smart HTTP transports when POSTing data to the remote system. For requests larger than this buffer size, HTTP/1.1 and Transfer-Encoding: chunked is used to avoid creating a massive pack file locally. Default is 1 MiB, which is sufficient for most requests.</span><br><span class=line></span><br><span class=line>Note that raising this limit is only effective for disabling chunked transfer encoding and therefore should be used only where the remote server or a proxy only supports HTTP/1.0 or is noncompliant with the HTTP standard. Raising this is not, in general, an effective solution for most push problems, but can increase memory consumption significantly since the entire buffer is allocated even for small pushes.</span><br></pre></table></figure><p>翻译下<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>Git 使用智能 HTTP 传输协议向远程服务器发送 POST 请求时,所使用缓冲区的最大大小,单位为字节。</span><br><span class=line>当请求大小超过这个缓冲区时,Git 会使用 HTTP/1.1 的 Transfer-Encoding: chunked,也就是分块传输编码,以避免在本地创建一个非常大的 pack 文件。</span><br><span class=line>默认值是 1 MiB,对于大多数请求来说已经足够。</span><br><span class=line>需要注意的是,增大这个值的作用仅仅是停用分块传输编码。因此,只有在远程服务器或中间代理仅支持 HTTP/1.0,或者没有正确遵循 HTTP 标准时,才应该提高这个限制。</span><br><span class=line>一般来说,增大这个值并不能解决大多数 Git 推送问题。同时它还可能显著增加内存占用,因为即使实际推送的数据很小,Git 也会分配完整大小的缓冲区。</span><br></pre></table></figure><p>对于我这样的hexo是在.deploy_git目录里的.git/config文件<br>普通git仓库如果要配置就是在.git/config里进行配置<figure class="highlight shell"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>[http]</span><br><span class=line> postBuffer = 16777216</span><br><span class=line> version = HTTP/1.1</span><br></pre></table></figure><p>可以直接改这个文件,也可以用git配置命令<figure class="highlight shell"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>git config http.postBuffer 16777216</span><br><span class=line>git config http.version HTTP/1.1</span><br></pre></table></figure><p>这样都可以,在现在这个时代<br>发现这样一个小技巧或者小问题的答案<br>我还是想记录下<br>因为很多的东西感觉已经被GPT这样的颠覆掉了<br>有这些小的问题被解决<br>感觉有种久违的愉悦感<br>排查问题,找到原因是个很有意思的过程<br>并把它解决也是很有意思<br>这个可能就算是初心了吧<br>就能把解决问题的思路方法分享下<br>也是一种比较开始的事情</div><footer class=post-footer><div class=post-eof></div></footer></article></div><div class=post-block><article class=post-content itemscope itemtype=http://schema.org/Article><link href=https://nicksxs.me/2026/07/12/%E5%AD%A6%E4%B8%80%E4%B8%8Bchrome%E7%9A%84%E6%89%A9%E5%B1%95%E5%BC%80%E5%8F%91/ itemprop=mainEntityOfPage><span hidden itemprop=author itemscope itemtype=http://schema.org/Person><meta content=/uploads/avatar.jpg itemprop=image><meta content=Nicksxs itemprop=name></span><span hidden itemprop=publisher itemscope itemtype=http://schema.org/Organization><meta content="Nicksxs's Blog" itemprop=name><meta content="learn from zero,技术博客,Nicksxs,史学森" itemprop=description></span><span hidden itemprop=post itemscope itemtype=http://schema.org/CreativeWork><meta content="undefined | Nicksxs's Blog" itemprop=name><meta itemprop=description></span><header class=post-header><h2 itemprop="name headline" class=post-title><a class=post-title-link href=/2026/07/12/%E5%AD%A6%E4%B8%80%E4%B8%8Bchrome%E7%9A%84%E6%89%A9%E5%B1%95%E5%BC%80%E5%8F%91/ itemprop=url>学一下chrome的扩展开发</a></h2><div class=post-meta-container><div class=post-meta><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-calendar"></i> </span><span class=post-meta-item-text>发表于</span> <time itemprop="dateCreated datePublished" title="创建时间:2026-07-12 22:18:19" datetime=2026-07-12T22:18:19+08:00>2026-07-12</time> </span><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-folder"></i> </span><span class=post-meta-item-text>分类于</span> <span itemprop=about itemscope itemtype=http://schema.org/Thing><a href=/categories/%E5%89%8D%E7%AB%AF/ itemprop=url rel=index><span itemprop=name>前端</span></a> </span></span><span class="post-meta-item cloudflare-page-counter" data-increment=false data-path=/2026/07/12/%E5%AD%A6%E4%B8%80%E4%B8%8Bchrome%E7%9A%84%E6%89%A9%E5%B1%95%E5%BC%80%E5%8F%91/ data-title=学一下chrome的扩展开发 title=阅读次数><span class=post-meta-item-icon><i class="far fa-eye"></i> </span><span class=post-meta-item-text>阅读次数:</span> <span class=cloudflare-page-counter-count></span> </span><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-comment"></i> </span><span class=post-meta-item-text>Disqus:</span> <a href=/2026/07/12/%E5%AD%A6%E4%B8%80%E4%B8%8Bchrome%E7%9A%84%E6%89%A9%E5%B1%95%E5%BC%80%E5%8F%91/#disqus_thread itemprop=discussionUrl title=disqus><span class="post-comments-count disqus-comment-count" data-disqus-identifier=2026/07/12/学一下chrome的扩展开发/ itemprop=commentCount></span></a></span></div></div></header><div class=post-body itemprop=articleBody><p>最近因为看了下 Chrome 里面置入的小模型,顺手又看了下 Chrome 扩展。<p>之前其实也接触过一些浏览器扩展,但是一直没有特别系统地看过它的结构,感觉好像就是写一个 <code>html</code>,然后再配几个 <code>js</code> 文件就可以了。<p>但是真正写起来以后,会发现 Chrome 扩展里面有几个不同的运行环境,尤其是 <code>popup</code> 和 <code>content script</code>,如果没有分清楚,后面调用网页内容或者操作页面 DOM 的时候就容易有点混乱。<p>这次先做一个比较简单的例子,点击扩展按钮以后,给当前页面加一个背景色,再在右下角显示一个提示。<div class=post-button><a class=btn href=/2026/07/12/%E5%AD%A6%E4%B8%80%E4%B8%8Bchrome%E7%9A%84%E6%89%A9%E5%B1%95%E5%BC%80%E5%8F%91/#more rel=contents>阅读全文 »</a></div></div><footer class=post-footer><div class=post-eof></div></footer></article></div><div class=post-block><article class=post-content itemscope itemtype=http://schema.org/Article><link href=https://nicksxs.me/2026/07/05/%E7%9C%8B%E4%B8%8Bchrome%E7%9A%84%E5%86%85%E7%BD%AE%E6%A8%A1%E5%9E%8B/ itemprop=mainEntityOfPage><span hidden itemprop=author itemscope itemtype=http://schema.org/Person><meta content=/uploads/avatar.jpg itemprop=image><meta content=Nicksxs itemprop=name></span><span hidden itemprop=publisher itemscope itemtype=http://schema.org/Organization><meta content="Nicksxs's Blog" itemprop=name><meta content="learn from zero,技术博客,Nicksxs,史学森" itemprop=description></span><span hidden itemprop=post itemscope itemtype=http://schema.org/CreativeWork><meta content="undefined | Nicksxs's Blog" itemprop=name><meta itemprop=description></span><header class=post-header><h2 itemprop="name headline" class=post-title><a class=post-title-link href=/2026/07/05/%E7%9C%8B%E4%B8%8Bchrome%E7%9A%84%E5%86%85%E7%BD%AE%E6%A8%A1%E5%9E%8B/ itemprop=url>看下chrome的内置模型</a></h2><div class=post-meta-container><div class=post-meta><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-calendar"></i> </span><span class=post-meta-item-text>发表于</span> <time itemprop="dateCreated datePublished" title="创建时间:2026-07-05 20:52:32 / 修改时间:20:53:17" datetime=2026-07-05T20:52:32+08:00>2026-07-05</time> </span><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-folder"></i> </span><span class=post-meta-item-text>分类于</span> <span itemprop=about itemscope itemtype=http://schema.org/Thing><a href=/categories/LLM/ itemprop=url rel=index><span itemprop=name>LLM</span></a> </span></span><span class="post-meta-item cloudflare-page-counter" data-increment=false data-path=/2026/07/05/%E7%9C%8B%E4%B8%8Bchrome%E7%9A%84%E5%86%85%E7%BD%AE%E6%A8%A1%E5%9E%8B/ data-title=看下chrome的内置模型 title=阅读次数><span class=post-meta-item-icon><i class="far fa-eye"></i> </span><span class=post-meta-item-text>阅读次数:</span> <span class=cloudflare-page-counter-count></span> </span><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-comment"></i> </span><span class=post-meta-item-text>Disqus:</span> <a href=/2026/07/05/%E7%9C%8B%E4%B8%8Bchrome%E7%9A%84%E5%86%85%E7%BD%AE%E6%A8%A1%E5%9E%8B/#disqus_thread itemprop=discussionUrl title=disqus><span class="post-comments-count disqus-comment-count" data-disqus-identifier=2026/07/05/看下chrome的内置模型/ itemprop=commentCount></span></a></span></div></div></header><div class=post-body itemprop=articleBody><p>之前听说chrome在你的电脑里塞了一个小模型,相比于说这个比较容易引起公愤的,可能是因为模型有4g大小,但是感觉也提供了挺大的想象空间<br>比如在浏览器内就可以直接调用模型进行对话处理,最简单的比如做个翻译啥的,<br>首先也有具体的硬件条件,<blockquote><ol><li>系统:Windows 10/11、macOS 13+、Linux、ChromeOS<li>显存:4GB 以上 VRAM,或者 16GB 内存加 4 核 CPU<li>Chrome 版本:138 以上</ol></blockquote><p>然后可以用这个地址看下怎么来开启这个内置模型<br><code>https://prompt-api.com/</code><figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>Open chrome://flags/#optimization-guide-on-device-model and set to Enabled BypassPerfRequirement</span><br><span class=line>Open chrome://flags/#prompt-api-for-gemini-nano and set to Enabled</span><br><span class=line>Open chrome://flags/#prompt-api-for-gemini-nano-multimodal-input and set to Enabled (for image/audio input)</span><br><span class=line>Click Relaunch to restart Chrome</span><br><span class=line>After relaunch, visit chrome://on-device-internals to check model status</span><br></pre></table></figure><p>主要是这些配置项,开启之后可以通过 <code>chrome://on-device-internals</code>当前的状态,比如硬件条件是否符合,我一开始是本地的磁盘空间不够,<br>默认需要有大概22g空间以上才可以下载使用,<table><thead><tr><th>Property<th>Value<tbody><tr><td>device capable<td>true<tr><td>disk space available<td>18671 MiB available, 20480 MiB required<tr><td>enabled by enterprise policy<td>true<tr><td>enabled by feature<td>true<tr><td>enabled by user setting<td>true<tr><td>is already installing<td>true<tr><td>on device feature recently used<td>true<tr><td>out of retention<td>false<tr><td>Detected VRAM (MiB)<td>12288<tr><td>Minimum VRAM required (MiB)<td>0</table><p>符合之后可以在console查看当前的状态,<figure class="highlight js"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line><span class=keyword>await</span> <span class="title class_">LanguageModel</span>.<span class="title function_">availability</span>({</span><br><span class=line> <span class=attr>expectedInputs</span>: [{ <span class=attr>type</span>: <span class=string>'text'</span>, <span class=attr>languages</span>: [<span class=string>'en'</span>] }],</span><br><span class=line> <span class=attr>expectedOutputs</span>: [{ <span class=attr>type</span>: <span class=string>'text'</span>, <span class=attr>languages</span>: [<span class=string>'en'</span>] }]</span><br><span class=line> })</span><br></pre></table></figure><p>比如正在下载会显示<br><code>downloading</code>,可以使用的话就是<code>'available'</code><br>如果想对话尝试下可以在上面的 <code>https://prompt-api.com/</code> 里进行对话体验<br>还有就是这个模型能用来干嘛呢<br>chrome其实提供了一系列的api<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br></pre><td class=code><pre><span class=line>Translator API</span><br><span class=line>Translator API 可在 Chrome 138 稳定版中使用。根据要求翻译用户生成的内容和动态内容。</span><br><span class=line></span><br><span class=line>使用场景</span><br><span class=line>用户可以使用自己的第一语言输入请求,您可以使用 Language Detector API 识别该语言。然后,使用 Translator API 将请求转换为您的业务运营语言,并将其发送给支持代理。</span><br><span class=line>在社交网络应用中,当用户的时间轴上显示他们不熟悉的语言的帖子时,他们可以按需请求翻译。</span><br><span class=line>Language Detector API</span><br><span class=line>Language Detector API 自 Chrome 138 稳定版开始提供。您可以使用此 API 检测输入文本的语言。 这是翻译流程的关键部分,因为您可能并不总是知道翻译的输入语言。</span><br><span class=line></span><br><span class=line>使用场景</span><br><span class=line>语言检测有多种应用场景:</span><br><span class=line></span><br><span class=line>确定未知源语言,以便将内容翻译为已知目标语言,这样用户就不必同时指定这两种语言。</span><br><span class=line>标记文本,例如,为了改进在线社交网站中的屏幕阅读器发音。</span><br></pre></table></figure><p>比如翻译的,语言检测的等等<br>那比如我们可以自己做个翻译的扩展小工具,既不用联网,也不用科学上网,还是很方便的<br>感觉端侧的小模型还是挺有前景的,随着模型能力越来越强<br>顶尖的模型都很贵,但是很多活其实可以拆分着来<br>最复杂核心的架构分析重构设计需要最好的模型<br>但是常规的一些简单的重复性的工作,一般的小模型也能完成<br>这样就chrome本身这么大的一个入口,带上这么个模型<br>还是非常有想象力的<br>当然这个也是我作为一个软件开发从业者的角度<br>而不是商业思维上来看<br>因为这些看起来都是没啥商业空间<br>毕竟免费的好用的都已经一大堆了,没有啥必要再付费或者订阅啥的<br>只是对于自己使用来说,有时候还是比较方便的<br>就比如这个翻译的场景,能够有个不需要联网,不用科学上网的<br>不过还一点是它现在好像不支持中文<br>所以还可以在期待下后续的发展<br>一个是模型能力的再提升<br>另一个是支持的范围,可以由中文支持这种</div><footer class=post-footer><div class=post-eof></div></footer></article></div><div class=post-block><article class=post-content itemscope itemtype=http://schema.org/Article><link href=https://nicksxs.me/2026/06/28/%E4%BB%8E-app-test-%E5%88%B0%E5%B0%8F%E9%94%81%EF%BC%9A%E6%9C%AC%E5%9C%B0-HTTPS-%E7%9A%84%E5%AE%8C%E6%95%B4%E9%93%BE%E8%B7%AF/ itemprop=mainEntityOfPage><span hidden itemprop=author itemscope itemtype=http://schema.org/Person><meta content=/uploads/avatar.jpg itemprop=image><meta content=Nicksxs itemprop=name></span><span hidden itemprop=publisher itemscope itemtype=http://schema.org/Organization><meta content="Nicksxs's Blog" itemprop=name><meta content="learn from zero,技术博客,Nicksxs,史学森" itemprop=description></span><span hidden itemprop=post itemscope itemtype=http://schema.org/CreativeWork><meta content="undefined | Nicksxs's Blog" itemprop=name><meta itemprop=description></span><header class=post-header><h2 itemprop="name headline" class=post-title><a class=post-title-link href=/2026/06/28/%E4%BB%8E-app-test-%E5%88%B0%E5%B0%8F%E9%94%81%EF%BC%9A%E6%9C%AC%E5%9C%B0-HTTPS-%E7%9A%84%E5%AE%8C%E6%95%B4%E9%93%BE%E8%B7%AF/ itemprop=url>从 app.test 到小锁:valet 本地 HTTPS 的完整链路</a></h2><div class=post-meta-container><div class=post-meta><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-calendar"></i> </span><span class=post-meta-item-text>发表于</span> <time itemprop="dateCreated datePublished" title="创建时间:2026-06-28 22:18:00" datetime=2026-06-28T22:18:00+08:00>2026-06-28</time> </span><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-folder"></i> </span><span class=post-meta-item-text>分类于</span> <span itemprop=about itemscope itemtype=http://schema.org/Thing><a href=/categories/php/ itemprop=url rel=index><span itemprop=name>php</span></a> </span></span><span class="post-meta-item cloudflare-page-counter" data-title="从 app.test 到小锁:valet 本地 HTTPS 的完整链路" data-increment=false data-path=/2026/06/28/%E4%BB%8E-app-test-%E5%88%B0%E5%B0%8F%E9%94%81%EF%BC%9A%E6%9C%AC%E5%9C%B0-HTTPS-%E7%9A%84%E5%AE%8C%E6%95%B4%E9%93%BE%E8%B7%AF/ title=阅读次数><span class=post-meta-item-icon><i class="far fa-eye"></i> </span><span class=post-meta-item-text>阅读次数:</span> <span class=cloudflare-page-counter-count></span> </span><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-comment"></i> </span><span class=post-meta-item-text>Disqus:</span> <a href=/2026/06/28/%E4%BB%8E-app-test-%E5%88%B0%E5%B0%8F%E9%94%81%EF%BC%9A%E6%9C%AC%E5%9C%B0-HTTPS-%E7%9A%84%E5%AE%8C%E6%95%B4%E9%93%BE%E8%B7%AF/#disqus_thread itemprop=discussionUrl title=disqus><span class="post-comments-count disqus-comment-count" data-disqus-identifier=2026/06/28/从-app-test-到小锁:本地-HTTPS-的完整链路/ itemprop=commentCount></span></a></span></div></div></header><div class=post-body itemprop=articleBody><p>之前在用到valet的时候就觉得这个工具很厉害,因为本地部署很多时候都是比较费劲的,也比较简陋,就直接localhost启动下,但是有时候需要验一下回调的,就需要有域名跟https,当然之前也没想明白的是在本地其实用本地的地址也是可以的,意思就是如果回调地址没限制必须要域名,也可以用127.0.0.1或者localhost,当然如果需要域名和https那就正好是valet的用武之地,所以想把 Valet 这类工具背后的逻辑捋一下。<p>平时用 Valet 的时候体验是非常顺的:<figure class="highlight shell"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>valet park</span><br><span class=line>valet secure demo</span><br></pre></table></figure><p>然后浏览器里访问:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>https://demo.test</span><br></pre></table></figure><p>就能看到本地项目,而且还有 HTTPS 的小锁。<p>这个体验乍一看很像魔法。一个本地域名,既不用手动写 hosts,也不用自己配 Nginx,还不用去公网 CA 申请证书,怎么就能跑起来了呢?<p>其实拆开之后,Valet 并没有发明什么新协议,它只是把本地开发里几件比较麻烦的事情串起来了:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line>本地域名解析</span><br><span class=line>Nginx 入口</span><br><span class=line>PHP-FPM 执行 PHP</span><br><span class=line>项目目录映射</span><br><span class=line>本地 CA 和站点证书</span><br><span class=line>框架 driver 判断入口文件</span><br></pre></table></figure><h2 id=先看完整链路><a class=headerlink href=#先看完整链路 title=先看完整链路></a>先看完整链路</h2><p>先放一个整体流程,后面再一段一段拆。<p>假设我们访问:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>https://demo.test/users/1</span><br></pre></table></figure><p>整个过程大概是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br><span class=line>28</span><br></pre><td class=code><pre><span class=line>浏览器访问 https://demo.test/users/1</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>macOS 发现 .test 域名要走 /etc/resolver/test</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>本地 dnsmasq 把 demo.test 解析到 127.0.0.1</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>请求到达本机 443 端口</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>Nginx 接收请求,并使用 demo.test 的证书完成 HTTPS</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>Nginx 把请求交给 Valet 的 server.php</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>server.php 根据 Host 找到 demo 对应的本地目录</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>Valet driver 判断这是 Laravel 项目</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>静态文件直接返回,非静态请求交给 public/index.php</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>Laravel 应用开始处理路由</span><br></pre></table></figure><p>所以可以先得出一个粗略结论:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line>DNS 负责把域名带到本机</span><br><span class=line>Nginx 负责接住 HTTP/HTTPS 请求</span><br><span class=line>server.php 负责把请求分发到具体项目</span><br><span class=line>driver 负责判断不同框架的入口规则</span><br><span class=line>PHP-FPM 负责真正执行 PHP</span><br><span class=line>本地 CA 负责让浏览器信任本地证书</span><br></pre></table></figure><p>这个结论很重要,因为很多时候我们排查 Valet 问题,就是在判断链路断在哪一层。<p>比如:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>域名打不开,可能是 DNS/dnsmasq 问题</span><br><span class=line>80/443 端口不通,可能是 Nginx 问题</span><br><span class=line>项目找不到,可能是 park/link 映射问题</span><br><span class=line>PHP 报错,可能是 PHP-FPM 或项目本身问题</span><br><span class=line>HTTPS 不被信任,可能是证书或系统信任问题</span><br></pre></table></figure><h2 id=Valet-的配置中心><a title="Valet 的配置中心" class=headerlink href=#Valet-的配置中心></a>Valet 的配置中心</h2><p>源码里可以先看 <code>Configuration.php</code>。<p>Valet 会在用户目录下维护一个自己的配置目录:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>~/.config/valet</span><br></pre></table></figure><p>这个目录下面会有几类东西:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br></pre><td class=code><pre><span class=line>~/.config/valet/config.json</span><br><span class=line>~/.config/valet/Sites</span><br><span class=line>~/.config/valet/Drivers</span><br><span class=line>~/.config/valet/Nginx</span><br><span class=line>~/.config/valet/Log</span><br><span class=line>~/.config/valet/Certificates</span><br><span class=line>~/.config/valet/CA</span><br></pre></table></figure><p>从源码看,<code>Configuration::install()</code> 做的事情就是创建这些基础目录,并确保基础配置存在。<p>其中最核心的是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>config.json</span><br></pre></table></figure><p>它大概长这样:<figure class="highlight json"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line><span class=punctuation>{</span></span><br><span class=line> <span class=attr>"tld"</span><span class=punctuation>:</span> <span class=string>"test"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"loopback"</span><span class=punctuation>:</span> <span class=string>"127.0.0.1"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"paths"</span><span class=punctuation>:</span> <span class=punctuation>[</span><span class=punctuation>]</span></span><br><span class=line><span class=punctuation>}</span></span><br></pre></table></figure><p>这几个字段分别代表:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>tld 本地域名后缀,默认是 test</span><br><span class=line>loopback 本地回环地址,默认是 127.0.0.1</span><br><span class=line>paths 被 park 的目录列表</span><br></pre></table></figure><p>所以 Valet 的状态不是凭空来的,它有一个非常明确的落盘位置。<p>比如 <code>valet park</code> 之后,当前目录会被写到 <code>paths</code> 里。<p>比如 <code>valet link demo</code> 之后,<code>~/.config/valet/Sites/demo</code> 里会出现一个指向真实项目目录的符号链接。<p>比如 <code>valet secure demo</code> 之后,证书会进入:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>~/.config/valet/Certificates</span><br></pre></table></figure><p>对应的 Nginx 站点配置会进入:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>~/.config/valet/Nginx</span><br></pre></table></figure><p>理解这个目录很有用,因为后面很多问题都可以直接去这里看。<h2 id=valet-install-大概做了什么><a title="valet install 大概做了什么" class=headerlink href=#valet-install-大概做了什么></a>valet install 大概做了什么</h2><p>我们平时执行:<figure class="highlight shell"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>valet install</span><br></pre></table></figure><p>它背后不是单纯装一个命令,而是在本机准备一整套本地 Web 开发环境。<p>从源码结构上看,大体可以分成几部分:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>Configuration.php 创建 ~/.config/valet 相关目录和 config.json</span><br><span class=line>DnsMasq.php 安装和配置 dnsmasq</span><br><span class=line>Nginx.php 安装和配置 Nginx</span><br><span class=line>PhpFpm.php 配置 PHP-FPM 和 valet.sock</span><br><span class=line>Valet.php 处理 valet 命令本身的链接和 sudoers 等辅助事情</span><br></pre></table></figure><p>这里先不展开 PHP 版本管理和 sudoers 等细节,只看主链路。<p>Valet 安装完成后,本机基本会形成这么一个结构:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br></pre><td class=code><pre><span class=line>macOS DNS</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>dnsmasq</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>127.0.0.1:80 / 127.0.0.1:443</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>Nginx</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>Valet server.php</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>具体项目</span><br></pre></table></figure><p>也就是说,Valet 的轻量不是说它什么服务都没有,而是它复用了本机的 Nginx、PHP-FPM、dnsmasq,不需要为每个项目单独开一个虚拟机或容器。<h2 id=demo-test-为什么会到本机><a title="demo.test 为什么会到本机" class=headerlink href=#demo-test-为什么会到本机></a>demo.test 为什么会到本机</h2><p>先看第一个问题:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>demo.test 怎么就指向 127.0.0.1 了?</span><br></pre></table></figure><p>如果不用 Valet,我们最容易想到的方法是改 <code>/etc/hosts</code>:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>127.0.0.1 demo.test</span><br></pre></table></figure><p>但这样有个问题:每加一个项目,就要写一行。<p>Valet 不这么做。它用的是 <code>dnsmasq</code> 加 macOS resolver。<p>源码里对应的是 <code>DnsMasq.php</code>。<p><code>DnsMasq::install()</code> 的流程大概是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>确保 dnsmasq 已安装</span><br><span class=line>让 dnsmasq 加载额外配置目录</span><br><span class=line>创建 .test 后缀对应的 dnsmasq 配置</span><br><span class=line>创建 /etc/resolver/test</span><br><span class=line>重启 dnsmasq</span><br></pre></table></figure><p>这里有两个关键文件。<p>第一个是类似这样的 dnsmasq 配置:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>address=/.test/127.0.0.1</span><br><span class=line>address=/.test/::1</span><br><span class=line>listen-address=127.0.0.1</span><br></pre></table></figure><p>它的意思是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>所有 .test 结尾的域名,都解析到本机</span><br></pre></table></figure><p>所以这些域名都可以成立:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>demo.test</span><br><span class=line>blog.test</span><br><span class=line>api.test</span><br><span class=line>anything.test</span><br></pre></table></figure><p>第二个是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>/etc/resolver/test</span><br></pre></table></figure><p>内容大概是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>nameserver 127.0.0.1</span><br></pre></table></figure><p>这个文件告诉 macOS:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>遇到 .test 这个后缀,不要走普通 DNS,交给 127.0.0.1 上的 DNS 服务处理</span><br></pre></table></figure><p>然后本地的 dnsmasq 再把它解析回 127.0.0.1。<p>这里要注意一件事:DNS 这一层只解决了“域名到 IP”的问题。<p>也就是说:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>demo.test -> 127.0.0.1</span><br><span class=line>blog.test -> 127.0.0.1</span><br><span class=line>api.test -> 127.0.0.1</span><br></pre></table></figure><p>但是 DNS 并不知道:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>demo.test 对应哪个项目目录</span><br><span class=line>blog.test 对应哪个项目目录</span><br></pre></table></figure><p>这个问题要留给后面的 Nginx 和 Valet 自己处理。<h2 id=请求到本机后谁来接><a class=headerlink href=#请求到本机后谁来接 title=请求到本机后谁来接></a>请求到本机后谁来接</h2><p>域名解析到 127.0.0.1 之后,浏览器会根据协议和端口发请求。<p>如果是 HTTP:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>http://demo.test -> 127.0.0.1:80</span><br></pre></table></figure><p>如果是 HTTPS:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>https://demo.test -> 127.0.0.1:443</span><br></pre></table></figure><p>这时接请求的是 Nginx。<p>源码里对应的是 <code>Nginx.php</code>。<p><code>Nginx::install()</code> 大致做几件事:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>确保 Nginx 已安装</span><br><span class=line>写入 nginx.conf</span><br><span class=line>写入 Valet 的 valet.conf</span><br><span class=line>创建 ~/.config/valet/Nginx 目录</span><br></pre></table></figure><p>这里比较关键的是 Valet 的 Nginx 不是简单地给某一个项目写死 root。<p>它会准备一个统一入口,把请求转给 Valet 自己的 <code>server.php</code>。<p>可以粗略理解为:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br></pre><td class=code><pre><span class=line>Nginx 接住所有本地 .test 请求</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>统一转给 Valet server.php</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>server.php 再决定这个请求属于哪个项目</span><br></pre></table></figure><p>这一点是 Valet 设计里比较关键的地方。<p>如果完全靠 Nginx,每个项目都要生成一个完整的 server block:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>demo.test -> /Users/me/code/demo/public</span><br><span class=line>blog.test -> /Users/me/code/blog/public</span><br></pre></table></figure><p>Valet 当然也会为 HTTPS、proxy、PHP 隔离这类特殊场景生成站点配置,但普通场景下,它更核心的思路是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>Nginx 做入口</span><br><span class=line>PHP 层的 server.php 做动态分发</span><br></pre></table></figure><p>这样它才能做到 <code>valet park</code> 之后,目录下面新增一个项目,不需要手工改 Nginx 配置也能访问。<h2 id=park-和-link-到底有什么区别><a title="park 和 link 到底有什么区别" class=headerlink href=#park-和-link-到底有什么区别></a>park 和 link 到底有什么区别</h2><p>接着看项目目录映射。<p>Valet 常用两个命令:<figure class="highlight shell"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>valet park</span><br><span class=line>valet link</span><br></pre></table></figure><p>它们都能让本地域名对应到项目,但思路不一样。<h3 id=park-是登记一个父目录><a title="park 是登记一个父目录" class=headerlink href=#park-是登记一个父目录></a>park 是登记一个父目录</h3><p>假设我们在这个目录执行:<figure class="highlight shell"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>cd ~/Sites</span><br><span class=line>valet park</span><br></pre></table></figure><p>然后目录结构是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>~/Sites/demo</span><br><span class=line>~/Sites/blog</span><br><span class=line>~/Sites/shop</span><br></pre></table></figure><p>Valet 就可以让这些域名工作:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>demo.test -> ~/Sites/demo</span><br><span class=line>blog.test -> ~/Sites/blog</span><br><span class=line>shop.test -> ~/Sites/shop</span><br></pre></table></figure><p>从配置角度看,<code>park</code> 主要就是把 <code>~/Sites</code> 这个路径写入 <code>config.json</code> 的 <code>paths</code>。<p>之后 Valet 查找站点时,会扫描这些 parked path 下面的子目录。<p>所以 <code>park</code> 的心智模型是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>我把一个工作区交给 Valet</span><br><span class=line>工作区下每个子目录都是一个站点</span><br></pre></table></figure><h3 id=link-是创建一个明确映射><a title="link 是创建一个明确映射" class=headerlink href=#link-是创建一个明确映射></a>link 是创建一个明确映射</h3><p><code>link</code> 更像是给当前目录起一个名字。<p>比如:<figure class="highlight shell"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>cd ~/code/some-long-project-name</span><br><span class=line>valet link demo</span><br></pre></table></figure><p>它会创建类似这样的符号链接:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>~/.config/valet/Sites/demo -> ~/code/some-long-project-name</span><br></pre></table></figure><p>之后:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>demo.test</span><br></pre></table></figure><p>就对应这个真实项目目录。<p>源码里 <code>Site::link()</code> 做的事情也很直接:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>确保 ~/.config/valet/Sites 存在</span><br><span class=line>把 Sites 目录加入配置路径</span><br><span class=line>创建一个符号链接</span><br></pre></table></figure><p>所以可以这样理解:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>park 是目录扫描</span><br><span class=line>link 是显式别名</span><br></pre></table></figure><p>这也是排查时很好用的判断。<p>如果是 <code>park</code> 出来的站点,要看父目录有没有在 <code>config.json</code> 的 <code>paths</code> 里。<p>如果是 <code>link</code> 出来的站点,要看 <code>~/.config/valet/Sites</code> 下的符号链接是否存在、是否指向正确目录。<h2 id=每次请求真正进入的是-server-php><a title="每次请求真正进入的是 server.php" class=headerlink href=#每次请求真正进入的是-server-php></a>每次请求真正进入的是 server.php</h2><p>现在 DNS 和 Nginx 都说完了,请求已经进入 Valet。<p>真正处理请求分发的是仓库根目录下的:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>server.php</span><br></pre></table></figure><p>这段源码非常值得看,因为它基本把 Valet 的请求生命周期写清楚了。<p>它做的事情可以简化成这样:<figure class="highlight php"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br></pre><td class=code><pre><span class=line><span class=variable>$config</span> = <span class="title function_ invoke__">read_valet_config</span>();</span><br><span class=line></span><br><span class=line><span class=variable>$uri</span> = <span class="title function_ invoke__">parse_request_uri</span>();</span><br><span class=line><span class=variable>$siteName</span> = <span class="title function_ invoke__">site_name_from_http_host</span>(<span class=variable>$_SERVER</span>[<span class=string>'HTTP_HOST'</span>]);</span><br><span class=line><span class=variable>$sitePath</span> = <span class="title function_ invoke__">find_site_path</span>(<span class=variable>$siteName</span>);</span><br><span class=line></span><br><span class=line><span class=variable>$driver</span> = <span class="title class_">ValetDriver</span>::<span class="title function_ invoke__">assign</span>(<span class=variable>$sitePath</span>, <span class=variable>$siteName</span>, <span class=variable>$uri</span>);</span><br><span class=line></span><br><span class=line><span class=keyword>if</span> (<span class=variable>$driver</span>-><span class="title function_ invoke__">isStaticFile</span>(...)) {</span><br><span class=line> <span class=keyword>return</span> <span class=variable>$driver</span>-><span class="title function_ invoke__">serveStaticFile</span>(...);</span><br><span class=line>}</span><br><span class=line></span><br><span class=line><span class=variable>$frontController</span> = <span class=variable>$driver</span>-><span class="title function_ invoke__">frontControllerPath</span>(...);</span><br><span class=line></span><br><span class=line><span class=keyword>require</span> <span class=variable>$frontController</span>;</span><br></pre></table></figure><p>这里面有几个关键点。<p>第一个,<code>siteName</code> 来自 HTTP Host。<p>访问:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>demo.test</span><br></pre></table></figure><p>Valet 会从 Host 里解析出:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>demo</span><br></pre></table></figure><p>第二个,<code>sitePath</code> 来自 Valet 的站点映射。<p>也就是前面说的:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>parked paths</span><br><span class=line>linked sites</span><br></pre></table></figure><p>第三个,Valet 不会一上来就假设这是 Laravel 项目,而是会通过 driver 选择。<p>这一层是 Valet 能支持很多框架的关键。<h2 id=driver-是-Valet-的框架适配层><a title="driver 是 Valet 的框架适配层" class=headerlink href=#driver-是-Valet-的框架适配层></a>driver 是 Valet 的框架适配层</h2><p>源码里 <code>ValetDriver.php</code> 定义了几个关键方法:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>serves()</span><br><span class=line>isStaticFile()</span><br><span class=line>frontControllerPath()</span><br></pre></table></figure><p>这三个方法基本就回答了三个问题:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>这个 driver 能不能处理当前项目?</span><br><span class=line>当前请求是不是静态文件?</span><br><span class=line>如果不是静态文件,入口文件在哪里?</span><br></pre></table></figure><p>比如 Laravel 项目的 driver,也就是 <code>LaravelValetDriver.php</code>,判断逻辑可以概括成:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>项目目录下有 public/index.php</span><br><span class=line>并且项目目录下有 artisan</span><br><span class=line>那就认为这是 Laravel 项目</span><br></pre></table></figure><p>这就很符合 Laravel 项目的基本结构。<p>对于静态文件,它会优先找:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>项目目录/public/当前 URI</span><br><span class=line>项目目录/storage/app/public/当前 URI</span><br></pre></table></figure><p>如果请求的是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>/css/app.css</span><br></pre></table></figure><p>并且真实文件存在:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>public/css/app.css</span><br></pre></table></figure><p>那就直接返回静态文件。<p>如果请求的是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>/users/1</span><br></pre></table></figure><p>这显然不是一个真实静态文件,那就交给:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>public/index.php</span><br></pre></table></figure><p>然后 Laravel 自己的路由系统再接着处理。<p>这就是 Valet driver 的意义。<p>Nginx 并不知道 Laravel、WordPress、Statamic、普通 PHP 项目的入口规则有什么区别。Valet 通过 driver 把这些规则封装起来。<p>所以 Valet 支持多框架的本质不是 Nginx 有多聪明,而是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>server.php 统一接管请求</span><br><span class=line>driver 负责识别项目类型和入口文件</span><br></pre></table></figure><h2 id=静态文件为什么不是-PHP-直接读出来><a title="静态文件为什么不是 PHP 直接读出来" class=headerlink href=#静态文件为什么不是-PHP-直接读出来></a>静态文件为什么不是 PHP 直接读出来</h2><p>Valet 处理静态文件还有一个有意思的细节。<p>在 driver 判断某个请求是静态文件后,它不是简单地在 PHP 里 <code>readfile()</code>。<p>Valet driver 会通过一个内部跳转机制,让 Nginx 去返回真实文件。<p>也就是类似:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br></pre><td class=code><pre><span class=line>PHP 判断这是静态文件</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>告诉 Nginx 这个文件在哪里</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>Nginx 直接返回文件</span><br></pre></table></figure><p>这么做的好处是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>PHP 只负责判断逻辑</span><br><span class=line>真正传输文件这种事情交给 Nginx</span><br></pre></table></figure><p>这也符合 Web Server 和应用层的分工。<p>Nginx 擅长处理静态文件,PHP 擅长处理动态逻辑。<h2 id=PHP-FPM-在链路里的位置><a title="PHP-FPM 在链路里的位置" class=headerlink href=#PHP-FPM-在链路里的位置></a>PHP-FPM 在链路里的位置</h2><p>再说一下 PHP-FPM。<p>Nginx 自己不会执行 PHP。<p>当请求需要执行 <code>server.php</code> 或项目的 <code>public/index.php</code> 时,Nginx 会把请求交给 PHP-FPM。<p>Valet 里常见的通信方式是 Unix socket,比如:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>~/.config/valet/valet.sock</span><br></pre></table></figure><p>可以粗略理解成:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>Nginx 是前台</span><br><span class=line>PHP-FPM 是后厨</span><br><span class=line>server.php 和 Laravel index.php 是菜单上的具体菜</span><br></pre></table></figure><p>Nginx 接到请求后,如果需要执行 PHP,就把请求交给 PHP-FPM。<p>PHP-FPM 执行完 PHP 脚本,再把响应交回给 Nginx,最后返回给浏览器。<p>这也解释了为什么有时候 Valet 域名能解析、Nginx 也启动了,但页面仍然打不开,可能是 PHP-FPM 没起来,或者 socket 路径不对。<h2 id=本地-HTTPS-是怎么来的><a title="本地 HTTPS 是怎么来的" class=headerlink href=#本地-HTTPS-是怎么来的></a>本地 HTTPS 是怎么来的</h2><p>现在再看 HTTPS。<p>问题是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>为什么 https://demo.test 能有小锁?</span><br></pre></table></figure><p>公网 HTTPS 的常见逻辑是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>浏览器内置信任一批根 CA</span><br><span class=line>网站证书由这些 CA 或它们的中间 CA 签发</span><br><span class=line>浏览器验证证书链</span><br><span class=line>验证通过后显示可信</span><br></pre></table></figure><p>本地 HTTPS 没法直接去公网 CA 申请 <code>demo.test</code> 的证书,因为它不是公网真实站点。<p>Valet 的思路是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>自己创建一个本地 CA</span><br><span class=line>把这个本地 CA 加到 macOS 系统信任</span><br><span class=line>再用这个本地 CA 给 demo.test 签发证书</span><br></pre></table></figure><p>也就是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br></pre><td class=code><pre><span class=line>Laravel Valet 本地 CA</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>demo.test 站点证书</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>浏览器验证到系统信任的本地 CA,于是显示可信</span><br></pre></table></figure><p>这里最关键的一句话是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>浏览器信任的不是 demo.test 这个域名本身,而是它的证书链能追溯到系统信任的 CA。</span><br></pre></table></figure><p>所以 Valet 本地 HTTPS 的本质是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>本地生成证书</span><br><span class=line>本地信任证书颁发者</span><br><span class=line>本地 Nginx 使用这张证书</span><br></pre></table></figure><p>这个信任只在你的机器上成立。<p>换一台电脑,如果没有信任你这台机器上的 Valet CA,<code>demo.test</code> 的证书就不会被认为可信。<h2 id=valet-secure-在源码里做了什么><a title="valet secure 在源码里做了什么" class=headerlink href=#valet-secure-在源码里做了什么></a>valet secure 在源码里做了什么</h2><p>源码里 HTTPS 的主线在 <code>Site.php</code>。<p><code>valet secure demo</code> 最终会走到类似 <code>Site::secure()</code> 的逻辑。<p>它的主流程可以整理成:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line>保留当前站点可能存在的 PHP 版本隔离配置</span><br><span class=line>确保本地 CA 存在</span><br><span class=line>删除旧的站点证书和旧 Nginx 配置</span><br><span class=line>创建 demo.test 的站点证书</span><br><span class=line>生成 HTTPS Nginx server 配置</span><br><span class=line>写入 ~/.config/valet/Nginx/demo.test</span><br></pre></table></figure><p>里面又可以拆成两个层次。<h3 id=第一层:创建本地-CA><a title="第一层:创建本地 CA" class=headerlink href=#第一层:创建本地-CA></a>第一层:创建本地 CA</h3><p><code>createCa()</code> 负责创建本地 CA。<p>它会生成类似这些文件:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>~/.config/valet/CA/LaravelValetCASelfSigned.pem</span><br><span class=line>~/.config/valet/CA/LaravelValetCASelfSigned.key</span><br></pre></table></figure><p>然后通过 macOS 的 <code>security</code> 命令把这个 CA 加入系统钥匙串。<p>也就是告诉系统:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>这个本地 CA 我信任</span><br></pre></table></figure><p>这是小锁能出现的根本原因。<p>如果这一步失败,比如钥匙串没有信任成功,证书就算生成了,浏览器也可能不认。<h3 id=第二层:给具体站点签证书><a class=headerlink href=#第二层:给具体站点签证书 title=第二层:给具体站点签证书></a>第二层:给具体站点签证书</h3><p><code>createCertificate()</code> 负责给具体域名生成证书。<p>比如:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>demo.test.key</span><br><span class=line>demo.test.csr</span><br><span class=line>demo.test.crt</span><br><span class=line>demo.test.conf</span><br></pre></table></figure><p>这些文件放在:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>~/.config/valet/Certificates</span><br></pre></table></figure><p>流程大概是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>生成私钥</span><br><span class=line>生成证书签名请求</span><br><span class=line>用本地 CA 签出站点证书</span><br></pre></table></figure><p>所以证书关系是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>CA 私钥 + CA 证书</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>签发 demo.test 证书</span><br></pre></table></figure><p>这和公网证书的逻辑是类似的,只是 CA 从公网机构换成了你本机的本地 CA。<h2 id=secure-之后-Nginx-多了什么><a title="secure 之后 Nginx 多了什么" class=headerlink href=#secure-之后-Nginx-多了什么></a>secure 之后 Nginx 多了什么</h2><p>只生成证书还不够。<p>Nginx 还得知道:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>demo.test 用哪张证书</span><br><span class=line>demo.test 用哪把私钥</span><br><span class=line>443 端口怎么监听</span><br><span class=line>HTTP 要不要跳 HTTPS</span><br><span class=line>请求最终仍然转给哪个入口</span><br></pre></table></figure><p><code>Site::buildSecureNginxServer()</code> 会读取 Valet 的 HTTPS Nginx 模板,然后把占位符替换成真实值。<p>大概会替换这些东西:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>VALET_SITE -> demo.test</span><br><span class=line>VALET_CERT -> ~/.config/valet/Certificates/demo.test.crt</span><br><span class=line>VALET_KEY -> ~/.config/valet/Certificates/demo.test.key</span><br><span class=line>VALET_HOME_PATH</span><br><span class=line>VALET_SERVER_PATH</span><br></pre></table></figure><p>所以 <code>valet secure demo</code> 之后,Nginx 站点配置就会知道:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>server_name demo.test</span><br><span class=line>ssl_certificate demo.test.crt</span><br><span class=line>ssl_certificate_key demo.test.key</span><br></pre></table></figure><p>同时它还会保留 Valet 的基本请求分发逻辑:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br></pre><td class=code><pre><span class=line>HTTPS 请求进来</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>Nginx 完成 TLS</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>请求继续交给 Valet server.php</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>server.php 找项目和 driver</span><br></pre></table></figure><p>也就是说,HTTPS 只是入口层多了一次证书和 TLS 处理,后面的项目分发逻辑并没有变。<h2 id=SNI-在这里扮演什么角色><a title="SNI 在这里扮演什么角色" class=headerlink href=#SNI-在这里扮演什么角色></a>SNI 在这里扮演什么角色</h2><p>这里稍微提一下 SNI,但不展开太远。<p>本机可能同时有很多 HTTPS 站点:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>demo.test</span><br><span class=line>blog.test</span><br><span class=line>api.test</span><br></pre></table></figure><p>它们都走:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>127.0.0.1:443</span><br></pre></table></figure><p>那 Nginx 怎么知道应该拿哪张证书?<p>客户端在 TLS 握手时会带上要访问的域名,这就是 SNI。<p>Nginx 根据这个域名匹配对应的 server block,然后选择对应证书。<p>所以访问:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>https://demo.test</span><br></pre></table></figure><p>Nginx 会用 <code>demo.test</code> 的证书。<p>访问:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>https://blog.test</span><br></pre></table></figure><p>Nginx 会用 <code>blog.test</code> 的证书。<p>这也是为什么 <code>server_name</code> 和证书配置要对应起来。<h2 id=proxy-又是怎么回事><a title="proxy 又是怎么回事" class=headerlink href=#proxy-又是怎么回事></a>proxy 又是怎么回事</h2><p>Valet 还有一个很实用的功能:<figure class="highlight shell"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>valet proxy api http://127.0.0.1:3000</span><br></pre></table></figure><p>这个功能不是 PHP 项目映射,而是 Nginx 反向代理。<p>它的意思是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br></pre><td class=code><pre><span class=line>api.test</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>Nginx</span><br><span class=line> |</span><br><span class=line> v</span><br><span class=line>http://127.0.0.1:3000</span><br></pre></table></figure><p>源码里也在 <code>Site.php</code>,对应 <code>proxyCreate()</code> 这类逻辑。<p>它会生成一份 Nginx proxy 配置,把 <code>api.test</code> 的流量转发到指定地址。<p>如果加上 secure,本质就是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>给 api.test 生成本地证书</span><br><span class=line>Nginx 负责 HTTPS</span><br><span class=line>后端仍然代理到 127.0.0.1:3000</span><br></pre></table></figure><p>所以 Valet 不只能服务 Laravel 或 PHP 项目。对于 Node、Go、Docker 暴露出来的本地端口,Valet 也可以提供一个统一的本地域名和本地 HTTPS 入口。<h2 id=从源码角度再串一次><a class=headerlink href=#从源码角度再串一次 title=从源码角度再串一次></a>从源码角度再串一次</h2><p>现在把源码文件和职责再汇总一下。<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>Configuration.php</span><br></pre></table></figure><p>负责 Valet 自己的配置目录和 <code>config.json</code>:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>tld</span><br><span class=line>loopback</span><br><span class=line>paths</span><br></pre></table></figure><figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>DnsMasq.php</span><br></pre></table></figure><p>负责 <code>.test</code> 这类本地域名后缀:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>/etc/resolver/test</span><br><span class=line>dnsmasq address=/.test/127.0.0.1</span><br></pre></table></figure><figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>Nginx.php</span><br></pre></table></figure><p>负责安装和写入 Valet 的 Nginx 配置:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>nginx.conf</span><br><span class=line>valet.conf</span><br><span class=line>~/.config/valet/Nginx</span><br></pre></table></figure><figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>Site.php</span><br></pre></table></figure><p>负责站点层面的事情:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line>link</span><br><span class=line>secure</span><br><span class=line>unsecure</span><br><span class=line>proxy</span><br><span class=line>证书路径</span><br><span class=line>Nginx 站点配置生成</span><br></pre></table></figure><figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>server.php</span><br></pre></table></figure><p>是每次请求真正进入的 Valet 分发入口:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>解析 Host</span><br><span class=line>找到 site path</span><br><span class=line>选择 driver</span><br><span class=line>判断静态文件</span><br><span class=line>加载 front controller</span><br></pre></table></figure><figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>ValetDriver.php</span><br></pre></table></figure><p>定义项目识别和入口判断的抽象:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>serves()</span><br><span class=line>isStaticFile()</span><br><span class=line>frontControllerPath()</span><br></pre></table></figure><figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>LaravelValetDriver.php</span><br></pre></table></figure><p>是 Laravel 项目的具体规则:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>有 artisan</span><br><span class=line>有 public/index.php</span><br><span class=line>静态文件从 public 或 storage/app/public 找</span><br><span class=line>动态请求交给 public/index.php</span><br></pre></table></figure><p>这几个文件串起来,就能看到 Valet 的整体设计。<h2 id=排查问题时可以顺着这条链路看><a class=headerlink href=#排查问题时可以顺着这条链路看 title=排查问题时可以顺着这条链路看></a>排查问题时可以顺着这条链路看</h2><p>理解了链路之后,排查也会清楚很多。<h3 id=1-域名有没有解析到本机><a title="1. 域名有没有解析到本机" class=headerlink href=#1-域名有没有解析到本机></a>1. 域名有没有解析到本机</h3><p>可以看:<figure class="highlight shell"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>cat /etc/resolver/test</span><br></pre></table></figure><p>以及:<figure class="highlight shell"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>cat ~/.config/valet/dnsmasq.d/tld-test.conf</span><br></pre></table></figure><p>如果这里不对,问题通常还没到 Nginx。<h3 id=2-Valet-配置里有没有这个路径><a title="2. Valet 配置里有没有这个路径" class=headerlink href=#2-Valet-配置里有没有这个路径></a>2. Valet 配置里有没有这个路径</h3><p>可以看:<figure class="highlight shell"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>cat ~/.config/valet/config.json</span><br></pre></table></figure><p>如果是 <code>park</code>,看 <code>paths</code> 里有没有父目录。<p>如果是 <code>link</code>,看:<figure class="highlight shell"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>ls -l ~/.config/valet/Sites</span><br></pre></table></figure><h3 id=3-Nginx-配置有没有生成><a title="3. Nginx 配置有没有生成" class=headerlink href=#3-Nginx-配置有没有生成></a>3. Nginx 配置有没有生成</h3><p>可以看:<figure class="highlight shell"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>ls ~/.config/valet/Nginx</span><br></pre></table></figure><p>如果是 secure 或 proxy 站点,这里一般会有对应配置。<h3 id=4-证书是否存在><a title="4. 证书是否存在" class=headerlink href=#4-证书是否存在></a>4. 证书是否存在</h3><p>可以看:<figure class="highlight shell"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>ls ~/.config/valet/Certificates</span><br></pre></table></figure><p>也可以检查本地 CA:<figure class="highlight shell"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>ls ~/.config/valet/CA</span><br></pre></table></figure><h3 id=5-PHP-FPM-是否正常><a title="5. PHP-FPM 是否正常" class=headerlink href=#5-PHP-FPM-是否正常></a>5. PHP-FPM 是否正常</h3><p>如果 Nginx 能接请求,但 PHP 执行异常,就要看 PHP-FPM 和 socket。<p>Valet 的 Nginx 配置会把 PHP 请求交给类似:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>~/.config/valet/valet.sock</span><br></pre></table></figure><p>如果这个 socket 不存在,或者 PHP-FPM 没启动,就会出问题。<h2 id=最后总结一下><a class=headerlink href=#最后总结一下 title=最后总结一下></a>最后总结一下</h2><p>Valet 的原理可以压缩成一句话:<blockquote><p>Valet 用 dnsmasq 解决本地域名,用 Nginx 接住请求,用 server.php 和 driver 找到真正项目入口,用 PHP-FPM 执行 PHP,再用本地 CA 和 Nginx 配置补上 HTTPS。</blockquote><p>它的好用之处不是某个单点技术特别神奇,而是把一堆本来要手工处理的事情自动化了:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>不用每个项目手写 hosts</span><br><span class=line>不用每个项目手写 Nginx 配置</span><br><span class=line>不用自己维护 PHP-FPM socket</span><br><span class=line>不用自己生成和信任本地证书</span><br><span class=line>不用每个项目单独起一套容器或虚拟机</span><br></pre></table></figure><p>如果只看表面,Valet 好像只是让 <code>demo.test</code> 能访问。<p>但从源码和链路看,它其实是给本地开发搭了一套很轻量的“入口网关”:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line>所有本地域名都先进本机</span><br><span class=line>所有请求都先进 Nginx</span><br><span class=line>所有动态判断都进 Valet server.php</span><br><span class=line>所有框架差异都交给 driver</span><br><span class=line>所有 PHP 执行都交给 PHP-FPM</span><br><span class=line>所有 HTTPS 信任都交给本地 CA</span><br></pre></table></figure><p>理解这一点之后,再看 <code>valet park</code>、<code>valet link</code>、<code>valet secure</code>、<code>valet proxy</code>,就不太像魔法了。<p>它们只是分别在这条链路上改了一小段配置。<h2 id=参考><a class=headerlink href=#参考 title=参考></a>参考</h2><ul><li><a href=https://laravel.com/docs/12.x/valet rel=noopener target=_blank>Laravel Valet 官方文档</a><li><a href=https://github.com/laravel/valet rel=noopener target=_blank>laravel/valet GitHub 仓库</a><li><a href=https://github.com/laravel/valet/blob/master/server.php rel=noopener target=_blank>server.php</a><li><a href=https://github.com/laravel/valet/blob/master/cli/Valet/Site.php rel=noopener target=_blank>Site.php</a><li><a href=https://github.com/laravel/valet/blob/master/cli/Valet/DnsMasq.php rel=noopener target=_blank>DnsMasq.php</a><li><a href=https://github.com/laravel/valet/blob/master/cli/Valet/Nginx.php rel=noopener target=_blank>Nginx.php</a><li><a href=https://github.com/laravel/valet/blob/master/cli/Valet/Drivers/ValetDriver.php rel=noopener target=_blank>ValetDriver.php</a></ul></div><footer class=post-footer><div class=post-eof></div></footer></article></div><div class=post-block><article class=post-content itemscope itemtype=http://schema.org/Article><link href=https://nicksxs.me/2026/06/21/%E6%B5%85%E6%9E%90%E4%B8%80%E4%B8%8B%E5%90%84%E7%B1%BB%E5%9B%BE%E7%89%87%E6%A0%BC%E5%BC%8F%E5%8F%8A%E5%85%B6%E6%9D%A5%E6%BA%90/ itemprop=mainEntityOfPage><span hidden itemprop=author itemscope itemtype=http://schema.org/Person><meta content=/uploads/avatar.jpg itemprop=image><meta content=Nicksxs itemprop=name></span><span hidden itemprop=publisher itemscope itemtype=http://schema.org/Organization><meta content="Nicksxs's Blog" itemprop=name><meta content="learn from zero,技术博客,Nicksxs,史学森" itemprop=description></span><span hidden itemprop=post itemscope itemtype=http://schema.org/CreativeWork><meta content="undefined | Nicksxs's Blog" itemprop=name><meta itemprop=description></span><header class=post-header><h2 itemprop="name headline" class=post-title><a class=post-title-link href=/2026/06/21/%E6%B5%85%E6%9E%90%E4%B8%80%E4%B8%8B%E5%90%84%E7%B1%BB%E5%9B%BE%E7%89%87%E6%A0%BC%E5%BC%8F%E5%8F%8A%E5%85%B6%E6%9D%A5%E6%BA%90/ itemprop=url>浅析一下jpeg图片格式及其来源</a></h2><div class=post-meta-container><div class=post-meta><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-calendar"></i> </span><span class=post-meta-item-text>发表于</span> <time itemprop="dateCreated datePublished" title="创建时间:2026-06-21 20:42:27" datetime=2026-06-21T20:42:27+08:00>2026-06-21</time> </span><span class="post-meta-item cloudflare-page-counter" data-increment=false data-path=/2026/06/21/%E6%B5%85%E6%9E%90%E4%B8%80%E4%B8%8B%E5%90%84%E7%B1%BB%E5%9B%BE%E7%89%87%E6%A0%BC%E5%BC%8F%E5%8F%8A%E5%85%B6%E6%9D%A5%E6%BA%90/ data-title=浅析一下jpeg图片格式及其来源 title=阅读次数><span class=post-meta-item-icon><i class="far fa-eye"></i> </span><span class=post-meta-item-text>阅读次数:</span> <span class=cloudflare-page-counter-count></span> </span><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-comment"></i> </span><span class=post-meta-item-text>Disqus:</span> <a href=/2026/06/21/%E6%B5%85%E6%9E%90%E4%B8%80%E4%B8%8B%E5%90%84%E7%B1%BB%E5%9B%BE%E7%89%87%E6%A0%BC%E5%BC%8F%E5%8F%8A%E5%85%B6%E6%9D%A5%E6%BA%90/#disqus_thread itemprop=discussionUrl title=disqus><span class="post-comments-count disqus-comment-count" data-disqus-identifier=2026/06/21/浅析一下各类图片格式及其来源/ itemprop=commentCount></span></a></span></div></div></header><div class=post-body itemprop=articleBody><h1 id=JPEG-是怎么压缩图片的?><a title="JPEG 是怎么压缩图片的?" class=headerlink href=#JPEG-是怎么压缩图片的?></a>JPEG 是怎么压缩图片的?</h1><p>JPEG 的核心目标是:<blockquote><p>在人眼不太容易察觉的地方丢掉一些信息,从而大幅减小照片体积。</blockquote><p>所以 JPEG 通常是 <strong>有损压缩</strong>。它不是简单地把文件“打包变小”,而是会真的改变图像数据。<p>可以把 JPEG 压缩理解成几个步骤:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br></pre><td class=code><pre><span class=line>RGB 图像</span><br><span class=line>↓</span><br><span class=line>转换成 YCbCr</span><br><span class=line>↓</span><br><span class=line>色度抽样</span><br><span class=line>↓</span><br><span class=line>切成 8×8 小块</span><br><span class=line>↓</span><br><span class=line>DCT 变换</span><br><span class=line>↓</span><br><span class=line>量化</span><br><span class=line>↓</span><br><span class=line>Zigzag 扫描</span><br><span class=line>↓</span><br><span class=line>游程编码</span><br><span class=line>↓</span><br><span class=line>熵编码</span><br><span class=line>↓</span><br><span class=line>生成 JPEG 文件</span><br></pre></table></figure><hr><h2 id=1-RGB-转成-YCbCr><a title="1. RGB 转成 YCbCr" class=headerlink href=#1-RGB-转成-YCbCr></a>1. RGB 转成 YCbCr</h2><p>我们平时看到的图片通常可以理解成 RGB:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>R = 红色</span><br><span class=line>G = 绿色</span><br><span class=line>B = 蓝色</span><br></pre></table></figure><p>但 JPEG 不直接用 RGB 压缩,而是会先转换成 YCbCr:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>Y = 亮度信息</span><br><span class=line>Cb = 蓝色色度信息</span><br><span class=line>Cr = 红色色度信息</span><br></pre></table></figure><p>这么做是因为人眼对 <strong>亮度变化</strong> 很敏感,但对 <strong>颜色细节</strong> 没那么敏感。<p>比如一张人脸照片:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>轮廓、明暗、边缘变化很重要</span><br><span class=line>颜色细节稍微模糊一点,人眼不一定明显察觉</span><br></pre></table></figure><p>所以 JPEG 会把图像拆成:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>更重要的亮度通道 Y</span><br><span class=line>没那么重要的颜色通道 Cb / Cr</span><br></pre></table></figure><p>后面压缩时,颜色通道可以压得更狠一些。<hr><h2 id=2-色度抽样><a title="2. 色度抽样" class=headerlink href=#2-色度抽样></a>2. 色度抽样</h2><p>既然人眼对颜色细节没那么敏感,JPEG 就可以减少颜色信息的采样数量。<p>常见色度抽样有:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>4:4:4 不减少颜色信息</span><br><span class=line>4:2:2 横向减少一半颜色信息</span><br><span class=line>4:2:0 横向和纵向都减少颜色信息</span><br></pre></table></figure><p>最常见的是 <strong>4:2:0</strong>。<p>它大概意思是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>亮度信息完整保留</span><br><span class=line>颜色信息减少到原来的一部分</span><br></pre></table></figure><p>举个简化例子:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line>原始:</span><br><span class=line>每个像素都有 Y、Cb、Cr</span><br><span class=line></span><br><span class=line>4:2:0 后:</span><br><span class=line>每个像素仍然有 Y</span><br><span class=line>但多个像素共用一组 Cb、Cr</span><br></pre></table></figure><p>这一步对照片很有效,因为自然照片里颜色通常是连续变化的。<br>但如果是文字截图、UI 图标、红蓝边缘特别明显的图,色度抽样可能会让边缘发糊、出现彩边。<p>所以:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>照片适合 JPEG</span><br><span class=line>截图、文字、Logo 不太适合 JPEG</span><br></pre></table></figure><p>这里再补充一个,就是4:2:0究竟是啥意思<h3 id=YCbCr-是啥意思><a title="YCbCr 是啥意思" class=headerlink href=#YCbCr-是啥意思></a>YCbCr 是啥意思</h3><p>YCbCr 会把一个颜色拆成三部分:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>Y = 亮度,也就是明暗</span><br><span class=line>Cb = 蓝色色度差,大概表示“偏蓝还是不偏蓝”</span><br><span class=line>Cr = 红色色度差,大概表示“偏红还是不偏红”</span><br></pre></table></figure><p>注意:Cb / Cr 不是直接等于蓝色和红色,而是 <strong>颜色差异信息</strong>。<p>可以粗略理解为:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>Y 管黑白明暗</span><br><span class=line>Cb 管蓝黄方向的颜色变化</span><br><span class=line>Cr 管红绿方向的颜色变化</span><br></pre></table></figure><p>所以一张 RGB 图片转成 YCbCr 后,可以想象成三张灰度图:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>原图</span><br><span class=line>↓</span><br><span class=line>Y 图:黑白版,保留轮廓、明暗、细节</span><br><span class=line>Cb 图:颜色偏蓝 / 偏黄的信息</span><br><span class=line>Cr 图:颜色偏红 / 偏绿的信息</span><br></pre></table></figure><p>其中最重要的是 <strong>Y 亮度图</strong>。<hr><h3 id=为什么可以把亮度和颜色分开?><a class=headerlink href=#为什么可以把亮度和颜色分开? title=为什么可以把亮度和颜色分开?></a>为什么可以把亮度和颜色分开?</h3><p>因为人眼看图时,最敏感的是亮度边缘,而不是颜色边缘。<p>比如一张白底黑字图片:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>白底 + 黑字</span><br></pre></table></figure><p>你能很清楚看见文字,是因为亮度差异很大。<p>再比如一张人像照片:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>脸部轮廓</span><br><span class=line>鼻梁阴影</span><br><span class=line>眼睛边缘</span><br><span class=line>头发明暗</span><br><span class=line>衣服褶皱</span><br></pre></table></figure><p>这些主要靠 <strong>Y 亮度信息</strong> 表达。<p>但颜色细节稍微粗糙一点,比如皮肤颜色、天空蓝色、墙面颜色,人的感知没那么敏锐。<p>所以 JPEG 的策略是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>Y 亮度:尽量保留清楚</span><br><span class=line>Cb / Cr 颜色:可以减少一些</span><br></pre></table></figure><p>这就是色度抽样的基础。<hr><h3 id=4-RGB-转-YCbCr-是怎么转><a title="4. RGB 转 YCbCr 是怎么转" class=headerlink href=#4-RGB-转-YCbCr-是怎么转></a>4. RGB 转 YCbCr 是怎么转</h3><p>近似公式是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>Y = 0.299R + 0.587G + 0.114B</span><br><span class=line>Cb = B - Y 的某种缩放</span><br><span class=line>Cr = R - Y 的某种缩放</span><br></pre></table></figure><p>这里可以看到G是权重最大的,因为人眼对绿色最敏感,所以绿色对亮度感知贡献最大。<p>例如:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>R = 200</span><br><span class=line>G = 120</span><br><span class=line>B = 80</span><br></pre></table></figure><p>那么亮度 Y 大概是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>Y = 0.299 × 200 + 0.587 × 120 + 0.114 × 80</span><br><span class=line> ≈ 59.8 + 70.4 + 9.1</span><br><span class=line> ≈ 139.3</span><br></pre></table></figure><p>所以这个像素的亮度大概是 139。<p>然后 Cb / Cr 负责记录它相对于这个亮度来说,颜色偏向哪里。<p>简单理解:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>Y 记录:这个点有多亮</span><br><span class=line>Cb/Cr 记录:在这个亮度基础上,它是什么颜色</span><br></pre></table></figure><hr><h3 id=刚才说的-4-4-4、4-2-2、4-2-0-是啥意思><a title="刚才说的 4:4:4、4:2:2、4:2:0 是啥意思" class=headerlink href=#刚才说的-4-4-4、4-2-2、4-2-0-是啥意思></a>刚才说的 4:4:4、4:2:2、4:2:0 是啥意思</h3><blockquote><p>色度通道 Cb / Cr 相对于亮度通道 Y,被保留了多少采样点。</blockquote><p>先看一个 2×2 像素块。<p>原始情况下,每个像素都有:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>Y + Cb + Cr</span><br></pre></table></figure><p>也就是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>像素 A: Y1 Cb1 Cr1</span><br><span class=line>像素 B: Y2 Cb2 Cr2</span><br><span class=line>像素 C: Y3 Cb3 Cr3</span><br><span class=line>像素 D: Y4 Cb4 Cr4</span><br></pre></table></figure><hr><h2 id=6-4-4-4:完全不减少颜色信息><a title="6. 4:4:4:完全不减少颜色信息" class=headerlink href=#6-4-4-4:完全不减少颜色信息></a>6. 4:4:4:完全不减少颜色信息</h2><p>4:4:4 表示:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>每个像素都有自己的 Y</span><br><span class=line>每个像素也都有自己的 Cb / Cr</span><br></pre></table></figure><p>用 2×2 看:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br></pre><td class=code><pre><span class=line>Y:</span><br><span class=line>Y1 Y2</span><br><span class=line>Y3 Y4</span><br><span class=line></span><br><span class=line>Cb:</span><br><span class=line>Cb1 Cb2</span><br><span class=line>Cb3 Cb4</span><br><span class=line></span><br><span class=line>Cr:</span><br><span class=line>Cr1 Cr2</span><br><span class=line>Cr3 Cr4</span><br></pre></table></figure><p>也就是亮度和颜色分辨率一样高。<p>所以:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>4:4:4 = 不做色度抽样</span><br></pre></table></figure><p>质量最好,体积也更大。<hr><h2 id=7-4-2-2:横向减少颜色信息><a title="7. 4:2:2:横向减少颜色信息" class=headerlink href=#7-4-2-2:横向减少颜色信息></a>7. 4:2:2:横向减少颜色信息</h2><p>4:2:2 表示:<blockquote><p>水平方向的颜色采样减半,垂直方向不减。</blockquote><p>可以理解成:横向相邻两个像素共用一组 Cb / Cr。<p>比如 2×2 像素:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br></pre><td class=code><pre><span class=line>Y:</span><br><span class=line>Y1 Y2</span><br><span class=line>Y3 Y4</span><br><span class=line></span><br><span class=line>Cb:</span><br><span class=line>Cb1 Cb1</span><br><span class=line>Cb2 Cb2</span><br><span class=line></span><br><span class=line>Cr:</span><br><span class=line>Cr1 Cr1</span><br><span class=line>Cr2 Cr2</span><br></pre></table></figure><p>也就是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>第一行:两个像素共用一组颜色</span><br><span class=line>第二行:两个像素共用一组颜色</span><br></pre></table></figure><p>亮度 Y 仍然每个像素都有。<p>所以:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>4:2:2 = 横向颜色分辨率减半,纵向颜色分辨率不变</span><br></pre></table></figure><p>如果原来颜色采样是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>宽 1000,高 1000</span><br></pre></table></figure><p>4:2:2 的颜色通道大概变成:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>宽 500,高 1000</span><br></pre></table></figure><hr><h2 id=8-4-2-0:横向和纵向都减少颜色信息><a title="8. 4:2:0:横向和纵向都减少颜色信息" class=headerlink href=#8-4-2-0:横向和纵向都减少颜色信息></a>8. 4:2:0:横向和纵向都减少颜色信息</h2><p>4:2:0 最容易误解。它不是说“颜色没有了”,也不是说“第二行没有颜色”。<p>它的意思是:<blockquote><p>色度信息在横向减半,纵向也减半。</blockquote><p>也就是一个 2×2 像素块共用一组 Cb / Cr。<p>看 2×2:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br></pre><td class=code><pre><span class=line>Y:</span><br><span class=line>Y1 Y2</span><br><span class=line>Y3 Y4</span><br><span class=line></span><br><span class=line>Cb:</span><br><span class=line>Cb1 Cb1</span><br><span class=line>Cb1 Cb1</span><br><span class=line></span><br><span class=line>Cr:</span><br><span class=line>Cr1 Cr1</span><br><span class=line>Cr1 Cr1</span><br></pre></table></figure><p>也就是说:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>4 个像素各自有自己的亮度 Y</span><br><span class=line>但 4 个像素共用同一组颜色 Cb / Cr</span><br></pre></table></figure><p>所以颜色通道的分辨率变成原来的:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>宽度 1/2</span><br><span class=line>高度 1/2</span><br><span class=line>总采样点 = 1/4</span><br></pre></table></figure><p>假设原图是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>1920 × 1080</span><br></pre></table></figure><p>Y 通道仍然是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>1920 × 1080</span><br></pre></table></figure><p>但 Cb 通道变成:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>960 × 540</span><br></pre></table></figure><p>Cr 通道也变成:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>960 × 540</span><br></pre></table></figure><p>这就是“横向纵向都减少颜色信息”。<hr><p>这是一个前置的理解,那么关于图片格式,也想聊下怎么识别这些格式,光是后缀好像是可以被随意改动,比如这个jpeg的格式,那么我们可以从文件头来看<figure class="highlight plaintext"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>FF D8 FF E0 ... 4A 46 49 46</span><br></pre></table></figure><p>后面就代表的是 <code>JFIF</code> 这是文件头<p>我们也可以用一个小工具来识别<figure class="highlight php"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br><span class=line>28</span><br><span class=line>29</span><br><span class=line>30</span><br><span class=line>31</span><br><span class=line>32</span><br><span class=line>33</span><br><span class=line>34</span><br><span class=line>35</span><br><span class=line>36</span><br><span class=line>37</span><br><span class=line>38</span><br><span class=line>39</span><br><span class=line>40</span><br><span class=line>41</span><br><span class=line>42</span><br><span class=line>43</span><br><span class=line>44</span><br><span class=line>45</span><br><span class=line>46</span><br></pre><td class=code><pre><span class=line><span class=meta>&LT?php</span></span><br><span class=line></span><br><span class=line><span class=function><span class=keyword>function</span> <span class=title>detectImageType</span>(<span class=params><span class=keyword>string</span> <span class=variable>$path</span></span>): <span class=title>string</span></span></span><br><span class=line><span class=function></span>{</span><br><span class=line> <span class=variable>$h</span> = <span class="title function_ invoke__">file_get_contents</span>(<span class=variable>$path</span>, <span class=literal>false</span>, <span class=literal>null</span>, <span class=number>0</span>, <span class=number>32</span>);</span><br><span class=line></span><br><span class=line> <span class=keyword>if</span> (<span class=variable>$h</span> === <span class=literal>false</span> || <span class=variable>$h</span> === <span class=string>''</span>) {</span><br><span class=line> <span class=keyword>return</span> <span class=string>'unknown'</span>;</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=keyword>if</span> (<span class="title function_ invoke__">strncmp</span>(<span class=variable>$h</span>, <span class=string>"\xFF\xD8\xFF"</span>, <span class=number>3</span>) === <span class=number>0</span>) {</span><br><span class=line> <span class=keyword>return</span> <span class=string>'image/jpeg'</span>;</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=keyword>if</span> (<span class="title function_ invoke__">strncmp</span>(<span class=variable>$h</span>, <span class=string>"\x89PNG\x0D\x0A\x1A\x0A"</span>, <span class=number>8</span>) === <span class=number>0</span>) {</span><br><span class=line> <span class=keyword>return</span> <span class=string>'image/png'</span>;</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=keyword>if</span> (<span class="title function_ invoke__">strncmp</span>(<span class=variable>$h</span>, <span class=string>"GIF87a"</span>, <span class=number>6</span>) === <span class=number>0</span> || <span class="title function_ invoke__">strncmp</span>(<span class=variable>$h</span>, <span class=string>"GIF89a"</span>, <span class=number>6</span>) === <span class=number>0</span>) {</span><br><span class=line> <span class=keyword>return</span> <span class=string>'image/gif'</span>;</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=keyword>if</span> (<span class="title function_ invoke__">strlen</span>(<span class=variable>$h</span>) >= <span class=number>12</span> && <span class="title function_ invoke__">substr</span>(<span class=variable>$h</span>, <span class=number>0</span>, <span class=number>4</span>) === <span class=string>"RIFF"</span> && <span class="title function_ invoke__">substr</span>(<span class=variable>$h</span>, <span class=number>8</span>, <span class=number>4</span>) === <span class=string>"WEBP"</span>) {</span><br><span class=line> <span class=keyword>return</span> <span class=string>'image/webp'</span>;</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=keyword>if</span> (<span class="title function_ invoke__">strlen</span>(<span class=variable>$h</span>) >= <span class=number>12</span> && <span class="title function_ invoke__">substr</span>(<span class=variable>$h</span>, <span class=number>4</span>, <span class=number>4</span>) === <span class=string>"ftyp"</span>) {</span><br><span class=line> <span class=variable>$brands</span> = <span class="title function_ invoke__">substr</span>(<span class=variable>$h</span>, <span class=number>8</span>);</span><br><span class=line></span><br><span class=line> <span class=keyword>if</span> (<span class="title function_ invoke__">str_contains</span>(<span class=variable>$brands</span>, <span class=string>"avif"</span>)) {</span><br><span class=line> <span class=keyword>return</span> <span class=string>'image/avif'</span>;</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=keyword>if</span> (</span><br><span class=line> <span class="title function_ invoke__">str_contains</span>(<span class=variable>$brands</span>, <span class=string>"heic"</span>) ||</span><br><span class=line> <span class="title function_ invoke__">str_contains</span>(<span class=variable>$brands</span>, <span class=string>"heix"</span>) ||</span><br><span class=line> <span class="title function_ invoke__">str_contains</span>(<span class=variable>$brands</span>, <span class=string>"mif1"</span>)</span><br><span class=line> ) {</span><br><span class=line> <span class=keyword>return</span> <span class=string>'image/heif'</span>;</span><br><span class=line> }</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=keyword>return</span> <span class=string>'unknown'</span>;</span><br><span class=line>}</span><br><span class=line></span><br><span class=line><span class=keyword>echo</span> <span class="title function_ invoke__">detectImageType</span>(<span class=variable>$argv</span>[<span class=number>1</span>] ?? <span class=string>''</span>) . PHP_EOL;</span><br></pre></table></figure><p>这是通过文件头和brand来一起识别,brand是针对那些跟视频格式一致的可能包含容器的格式<br>比如<code>HEIC / AVIF</code>,对于<code>JPEG、PNG、GIF、WebP</code>这类常规格式,是一般从文件头识别<br>而后面的容器型则需要结合容器的brand标识,<br>比如一个<code>AVIF</code>的文件头是<figure class="highlight plaintext"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>00 00 00 20 66 74 79 70 61 76 69 66 00 00 00 00 61 76 69 66 6D 69 66 31</span><br></pre></table></figure><p>拆开以后就是<figure class="highlight plaintext"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line>00 00 00 20 = box size</span><br><span class=line>66 74 79 70 = ftyp</span><br><span class=line>61 76 69 66 = avif</span><br><span class=line>00 00 00 00 = minor_version</span><br><span class=line>61 76 69 66 = avif</span><br><span class=line>6D 69 66 31 = mif1</span><br></pre></table></figure><p>也就是<figure class="highlight plaintext"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>box type: ftyp</span><br><span class=line>major_brand: avif</span><br><span class=line>compatible_brands: avif, mif1</span><br></pre></table></figure><p>这样就大致知道了图片格式的识别</div><footer class=post-footer><div class=post-eof></div></footer></article></div><nav class=pagination><span class="page-number current">1</span><a class=page-number href=/page/2/>2</a><span class=space>…</span><a class=page-number href=/page/76/>76</a><a class="extend next" aria-label=下一页 href=/page/2/ rel=next title=下一页><i class="fa fa-angle-right"></i></a></nav></div></main><footer class=footer><div class=footer-inner><div class=beian><a href=https://beian.miit.gov.cn/ rel=noopener target=_blank>浙ICP备16002580号-2 </a><img alt src=/uploads/beian.png><a href=https://beian.mps.gov.cn/#/query/webSearch?code=33010602011094 rel=noopener target=_blank>浙公网安备 33010602011094号</a></div><div class=copyright>© <span itemprop=copyrightYear>2026</span><span class=with-love><i class="fa fa-heart"></i> </span><span class=author itemprop=copyrightHolder>Nicksxs</span></div><div class=busuanzi-count><span class=post-meta-item id=busuanzi_container_site_uv><span class=post-meta-item-icon><i class="fa fa-user"></i> </span><span class=site-uv title=总访客量><span id=busuanzi_value_site_uv></span> </span></span><span class=post-meta-item id=busuanzi_container_site_pv><span class=post-meta-item-icon><i class="fa fa-eye"></i> </span><span class=site-pv title=总访问量><span id=busuanzi_value_site_pv></span></span></span></div></div></footer><div aria-label=返回顶部 class=back-to-top role=button><i class="fa fa-arrow-up fa-lg"></i><span>0%</span></div><a aria-label="在 GitHub 上关注我" title="在 GitHub 上关注我" class=github-corner href=https://github.com/nicksxs rel=noopener target=_blank><svg viewbox="0 0 250 250" aria-hidden=true height=80 width=80><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" style="transform-origin:130px 106px" class=octo-arm fill=currentColor></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" class=octo-body fill=currentColor></path></svg></a><noscript><div class=noscript-warning>Theme NexT works best with JavaScript enabled</div></noscript><script crossorigin=anonymous integrity=sha256-XL2inqUJaslATFnHdJOi9GfQ60on8Wx1C2H8DYiN1xY= src=https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js></script><script crossorigin=anonymous integrity=sha256-ytMJGN3toR+a84u7g7NuHm91VIR06Q41kMWDr2pq7Zo= src=https://cdnjs.cloudflare.com/ajax/libs/fancyapps-ui/5.0.28/fancybox/fancybox.umd.js></script><script crossorigin=anonymous integrity=sha256-mOFREFhqmHeQbXpK2lp4nA3qooVgACfh88fpJftLBbc= src=https://cdnjs.cloudflare.com/ajax/libs/lozad.js/1.16.0/lozad.min.js></script><script src=/js/comments.js></script><script src=/js/utils.js></script><script src=/js/motion.js></script><script src=/js/next-boot.js></script><script crossorigin=anonymous integrity=sha256-DABVk+hYj0mdUzo+7ViJC6cwLahQIejFvC+my2M/wfM= src=https://cdnjs.cloudflare.com/ajax/libs/algoliasearch/4.20.0/algoliasearch-lite.umd.js></script><script crossorigin=anonymous integrity=sha256-9242vN47QUX50UG5Gf5XDO1YREWCEJRyXHofh5fsl24= src=https://cdnjs.cloudflare.com/ajax/libs/instantsearch.js/4.60.0/instantsearch.production.min.js></script><script src=/js/third-party/search/algolia-search.js></script><script src=/js/third-party/fancybox.js></script><script async src=https://busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js></script><script crossorigin=anonymous integrity=sha256-yvJQOINiH9fWemHn0vCA5lsHWJaHs6/ZmO+1Ft04SvM= src=https://cdnjs.cloudflare.com/ajax/libs/quicklink/2.3.0/quicklink.umd.js></script><script class=next-config data-name=quicklink type=application/json>{"enable":false,"home":false,"archive":false,"delay":true,"timeout":3000,"priority":true,"url":"https://nicksxs.me/"}</script><script src=/js/third-party/quicklink.js></script><script data-cloudflare-page-counter-config type=application/json>{"enable":true,"endpoint":"https://counter.nicksxs.me","site":"nicksxs","local_cache":true,"timeout":5000,"icon":"far fa-eye","item_text":true}</script><script src=/js/cloudflare-page-counter.js></script><script class=next-config data-name=disqus type=application/json>{"enable":true,"shortname":"nicksxs","count":true,"i18n":{"disqus":"disqus"}}</script><script src=/js/third-party/comments/disqus.js></script>