<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Ultramarine]]></title><description><![CDATA[前端开发，音游狗]]></description><link>https://gerhut.me/</link><image><url>https://gerhut.me/favicon.png</url><title>Ultramarine</title><link>https://gerhut.me/</link></image><generator>Ghost 3.36</generator><lastBuildDate>Fri, 10 Oct 2025 08:57:23 GMT</lastBuildDate><atom:link href="https://gerhut.me/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Welcome to Ghost]]></title><description><![CDATA[Welcome, it's great to have you here.
We know that first impressions are important, so we've populated your new site with some initial getting started posts that will help you get familiar with everything in no time.]]></description><link>https://gerhut.me/welcome/</link><guid isPermaLink="false">5fa11306356f4100015596e5</guid><category><![CDATA[Getting Started]]></category><dc:creator><![CDATA[Ghost]]></dc:creator><pubDate>Tue, 03 Nov 2020 08:21:32 GMT</pubDate><media:content url="https://static.ghost.org/v3.0.0/images/welcome-to-ghost.png" medium="image"/><content:encoded><![CDATA[<h2 id="a-few-things-you-should-know"><strong>A few things you should know</strong></h2><ol><li>Ghost is designed for ambitious, professional publishers who want to actively build a business around their content. That's who it works best for. </li><li>The entire platform can be modified and customised to suit your needs. It's very powerful, but does require some knowledge of code. Ghost is not necessarily a good platform for beginners or people who just want a simple personal blog. </li><li>It's possible to work with all your favourite tools and apps with hundreds of <a href="https://ghost.org/integrations/">integrations</a> to speed up your workflows, connect email lists, build communities and much more.</li></ol><h2 id="behind-the-scenes">Behind the scenes</h2><img src="https://static.ghost.org/v3.0.0/images/welcome-to-ghost.png" alt="Welcome to Ghost"><p>Ghost is made by an independent non-profit organisation called the Ghost Foundation. We are 100% self funded by revenue from our <a href="https://ghost.org/pricing">Ghost(Pro)</a> service, and every penny we make is re-invested into funding further development of free, open source technology for modern publishing.</p><p>The version of Ghost you are looking at right now would not have been made possible without generous contributions from the open source <a href="https://github.com/TryGhost">community</a>.</p><h2 id="next-up-the-editor">Next up, the editor</h2><p>The main thing you'll want to read about next is probably: <a href="https://gerhut.me/the-editor/">the Ghost editor</a>. This is where the good stuff happens.</p><blockquote>By the way, once you're done reading, you can simply delete the default Ghost user from your team to remove all of these introductory posts! </blockquote>]]></content:encoded></item><item><title><![CDATA[Writing posts with Ghost ✍️]]></title><description><![CDATA[Discover familiar formatting options in a functional toolbar and the ability to add dynamic content seamlessly.]]></description><link>https://gerhut.me/the-editor/</link><guid isPermaLink="false">5fa11306356f4100015596e3</guid><category><![CDATA[Getting Started]]></category><dc:creator><![CDATA[Ghost]]></dc:creator><pubDate>Tue, 03 Nov 2020 08:21:31 GMT</pubDate><media:content url="https://static.ghost.org/v3.0.0/images/writing-posts-with-ghost.png" medium="image"/><content:encoded><![CDATA[<h2 id="just-start-writing">Just start writing</h2><img src="https://static.ghost.org/v3.0.0/images/writing-posts-with-ghost.png" alt="Writing posts with Ghost ✍️"><p>Ghost has a powerful visual editor with familiar formatting options, as well as the ability to add dynamic content.</p><p>Select your text to add formatting such as headers or to create links. Or use Markdown shortcuts to do the work for you - if that's your thing. </p><figure class="kg-card kg-image-card"><img src="https://static.ghost.org/v2.0.0/images/formatting-editor-demo.gif" class="kg-image" alt="Writing posts with Ghost ✍️"></figure><h2 id="rich-editing-at-your-fingertips">Rich editing at your fingertips</h2><p>The editor can also handle rich media objects, called <strong>cards</strong>, which can be organised and re-ordered using drag and drop. </p><p>You can insert a card either by clicking the  <code>+</code>  button, or typing  <code>/</code>  on a new line to search for a particular card. This allows you to efficiently insert<strong> images</strong>, <strong>markdown</strong>, <strong>html, embeds </strong>and more.</p><p><strong>For example</strong>:</p><ul><li>Insert a video from YouTube directly by pasting the URL</li><li>Create unique content like buttons or forms using the HTML card</li><li>Need to share some code? Embed code blocks directly </li></ul><pre><code>&lt;header class="site-header outer"&gt;
    &lt;div class="inner"&gt;
        {{&gt; "site-nav"}}
    &lt;/div&gt;
&lt;/header&gt;</code></pre><p>It's also possible to share links from across the web in a visual way using bookmark cards that automatically render information from a websites meta data. Paste any URL to try it out: </p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://ghost.org/"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Ghost: The #1 open source headless Node.js CMS</div><div class="kg-bookmark-description">The world’s most popular modern open source publishing platform. A headless Node.js CMS used by Apple, Sky News, Tinder and thousands more. MIT licensed, with 30k+ stars on Github.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://ghost.org/icons/icon-512x512.png?v&#x3D;188b8b6d743c6338ba2eab2e35bab4f5" alt="Writing posts with Ghost ✍️"><span class="kg-bookmark-publisher">Ghost</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://ghost.org/images/meta/Ghost.png" alt="Writing posts with Ghost ✍️"></div></a></figure><h2 id="working-with-images-in-posts">Working with images in posts</h2><p>You can add images to your posts in many ways:</p><ul><li>Upload from your computer</li><li>Click and drag an image into the browser</li><li>Paste directly into the editor from your clipboard</li><li>Insert using a URL</li></ul><h3 id="image-sizes">Image sizes</h3><p>Once inserted you can blend images beautifully into your content at different sizes and add captions and alt tags wherever needed.</p><figure class="kg-card kg-image-card"><img src="https://static.ghost.org/v3.0.0/images/image-sizes-ghost-editor.png" class="kg-image" alt="Writing posts with Ghost ✍️"></figure><h3 id="image-galleries">Image galleries</h3><p>Tell visual stories using the gallery card to add up to 9 images that will display as a responsive image gallery: </p><figure class="kg-card kg-gallery-card kg-width-wide"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://static.ghost.org/v3.0.0/images/gallery-sample-1.jpg" width="6000" height="4000" alt="Writing posts with Ghost ✍️"></div><div class="kg-gallery-image"><img src="https://static.ghost.org/v3.0.0/images/gallery-sample-2.jpg" width="5746" height="3831" alt="Writing posts with Ghost ✍️"></div><div class="kg-gallery-image"><img src="https://static.ghost.org/v3.0.0/images/gallery-sample-3.jpg" width="5872" height="3915" alt="Writing posts with Ghost ✍️"></div></div></div></figure><h3 id="image-optimisation">Image optimisation</h3><p>Ghost will automatically resize and optimise your images with lossless compression. Your posts will be fully optimised for the web without any extra effort on your part.</p><h2 id="next-publishing-options">Next: Publishing Options</h2><p>Once your post is looking good, you'll want to use the <a href="https://gerhut.me/publishing-options/">publishing options</a> to ensure it gets distributed in the right places, with custom meta data, feature images and more.</p>]]></content:encoded></item><item><title><![CDATA[Publishing options]]></title><description><![CDATA[The Ghost editor post settings menu has everything you need to fully optimise and distribute your content effectively.]]></description><link>https://gerhut.me/publishing-options/</link><guid isPermaLink="false">5fa11306356f4100015596e1</guid><category><![CDATA[Getting Started]]></category><dc:creator><![CDATA[Ghost]]></dc:creator><pubDate>Tue, 03 Nov 2020 08:21:30 GMT</pubDate><media:content url="https://static.ghost.org/v3.0.0/images/publishing-options.png" medium="image"/><content:encoded><![CDATA[<h2 id="distribute-your-content">Distribute your content</h2><img src="https://static.ghost.org/v3.0.0/images/publishing-options.png" alt="Publishing options"><p>Access the post settings menu by clicking the settings icon in the top right hand corner of the editor and discover everything you need to get your content ready for publishing. This is where you can edit things like tags, post URL, publish date and custom meta data.</p><h2 id="feature-images-url-excerpts">Feature images, URL &amp; excerpts</h2><p>Insert your post feature image from the very top of the post settings menu. Consider resizing or optimising your image first to ensure it's an appropriate size. Below this, you can set your post URL, publish date and add a custom excerpt.</p><h2 id="tags-authors">Tags &amp; authors</h2><p>You can easily add multiple tags and authors to any post to filter and organise the relationships between your content in Ghost.</p><h2 id="structured-data-seo">Structured data &amp; SEO</h2><p>There's no need to hard code your meta data. In fact, Ghost will generate default meta data automatically using the content in your post.</p><p>Alternatively, you can override this by adding a custom meta title and description, as well as unique information for social media sharing cards on Facebook and Twitter.</p><p>It's also possible to set custom canonicals, which is useful for guest posts or curated lists of external links.</p><p>Ghost will automatically implement <strong>structured data</strong> for your publication using JSON-LD to further optimise your content.</p><pre><code>{
    "@context": "https://schema.org",
    "@type": "Article",
    "publisher": {
        "@type": "Organization",
        "name": "Publishing options",
        "logo": "https://static.ghost.org/ghost-logo.svg"
    },
    "author": {
        "@type": "Person",
        "name": "Ghost",
        "url": "http://demo.ghost.io/author/ghost/",
        "sameAs": []
    },
    "headline": "Publishing options",
    "url": "http://demo.ghost.io/publishing-options",
    "datePublished": "2018-08-08T11:44:00.000Z",
    "dateModified": "2018-08-09T12:06:21.000Z",
    "keywords": "Getting Started",
    "description": "The Ghost editor has everything you need to fully optimise your content. This is where you can add tags and authors, feature a post, or turn a post into a page."
}
    </code></pre><p>You can test that the structured data <a href="https://schema.org/">schema</a> on your site is working as it should using <a href="https://search.google.com/structured-data/testing-tool" rel="noreferrer nofollow noopener">Google’s structured data tool</a>. </p><h2 id="code-injection">Code injection</h2><p>This tool allows you to inject code on a per post or page basis, or across your entire site. This means you can modify CSS, add unique tracking codes, or add other scripts to the head or foot of your publication without making edits to your theme files. </p><p><strong>To add code site-wide</strong>, use the code injection tool in the main admin menu. This is useful for adding a Google Analytics tracking code, or to start tracking with any other analytics tool.</p><p><strong>To add code to a post or page</strong>, use the code injection tool within the post settings menu. This is useful if you want to add art direction, scripts or styles that are only applicable to one post or page.</p><h2 id="next-admin-settings">Next: Admin settings</h2><p>Now you understand how to create and optimise content, let's explore some <a href="https://gerhut.me/admin-settings/">admin settings</a> so you can invite your team and start collaborating.</p>]]></content:encoded></item><item><title><![CDATA[Managing admin settings]]></title><description><![CDATA[There are a couple of things to do next while you're getting set up: making your site private and inviting your team.]]></description><link>https://gerhut.me/admin-settings/</link><guid isPermaLink="false">5fa11306356f4100015596df</guid><category><![CDATA[Getting Started]]></category><dc:creator><![CDATA[Ghost]]></dc:creator><pubDate>Tue, 03 Nov 2020 08:21:29 GMT</pubDate><media:content url="https://static.ghost.org/v3.0.0/images/admin-settings.png" medium="image"/><content:encoded><![CDATA[<h2 id="make-your-site-private">Make your site private</h2><img src="https://static.ghost.org/v3.0.0/images/admin-settings.png" alt="Managing admin settings"><p>If you've got a publication that you don't want the world to see yet because it's not ready to launch, you can hide your Ghost site behind a basic shared pass-phrase.</p><p>You can toggle this preference on at the bottom of Ghost's General Settings:</p><figure class="kg-card kg-image-card"><img src="https://static.ghost.org/v1.0.0/images/private.png" class="kg-image" alt="Managing admin settings"></figure><p>Ghost will give you a short, randomly generated pass-phrase which you can share with anyone who needs access to the site while you're working on it. While this setting is enabled, all search engine optimisation features will be switched off to help keep your site under the radar.</p><p>Do remember though, this is <em>not</em> secure authentication. You shouldn't rely on this feature for protecting important private data. It's just a simple, shared pass-phrase for some very basic privacy.</p><h2 id="invite-your-team">Invite your team </h2><p>Ghost has a number of different user roles for your team:</p><p><strong>Contributors</strong><br>This is the base user level in Ghost. Contributors can create and edit their own draft posts, but they are unable to edit drafts of others or publish posts. Contributors are <strong>untrusted</strong> users with the most basic access to your publication.</p><p><strong>Authors</strong><br>Authors are the 2nd user level in Ghost. Authors can write, edit  and publish their own posts. Authors are <strong>trusted</strong> users. If you don't trust users to be allowed to publish their own posts, they should be set as Contributors.</p><p><strong>Editors</strong><br>Editors are the 3rd user level in Ghost. Editors can do everything that an Author can do, but they can also edit and publish the posts of others - as well as their own. Editors can also invite new Contributors &amp; Authors to the site.</p><p><strong>Administrators</strong><br>The top user level in Ghost is Administrator. Again, administrators can do everything that Authors and Editors can do, but they can also edit all site settings and data, not just content. Additionally, administrators have full access to invite, manage or remove any other user of the site.<br><br><strong>The Owner</strong><br>There is only ever one owner of a Ghost site. The owner is a special user which has all the same permissions as an Administrator, but with two exceptions: The Owner can never be deleted. And in some circumstances the owner will have access to additional special settings if applicable. For example: billing details, if using <a href="https://ghost.org/pricing/"><strong>Ghost(Pro)</strong></a>.</p><blockquote><em>It's a good idea to ask all of your users to fill out their user profiles, including bio and social links. These will populate rich structured data for posts and generally create more opportunities for themes to fully populate their design.</em></blockquote><h2 id="next-organising-content">Next: Organising content</h2><p>Find out how to <a href="https://gerhut.me/organising-content/">organise your content</a> with sensible tags and authors, or for more advanced configurations, how to create custom content structures using dynamic routing.</p>]]></content:encoded></item><item><title><![CDATA[Organising your content]]></title><description><![CDATA[Ghost has a flexible organisational taxonomy called tags and the ability to create custom site structures using dynamic routes.]]></description><link>https://gerhut.me/organising-content/</link><guid isPermaLink="false">5fa11306356f4100015596dd</guid><category><![CDATA[Getting Started]]></category><dc:creator><![CDATA[Ghost]]></dc:creator><pubDate>Tue, 03 Nov 2020 08:21:28 GMT</pubDate><media:content url="https://static.ghost.org/v3.0.0/images/organising-your-content.png" medium="image"/><content:encoded><![CDATA[<h2 id="sensible-tagging">Sensible tagging</h2><img src="https://static.ghost.org/v3.0.0/images/organising-your-content.png" alt="Organising your content"><p>You can think of tags like Gmail labels. By tagging posts with one or more keyword, you can organise articles into buckets of related content.</p><p>When you create content for your publication you can assign tags to help differentiate between categories of content. </p><p>For example you may tag some content with News and other content with Podcast, which would create two distinct categories of content listed on <code>/tag/news/</code> and <code>/tag/podcast/</code>, respectively.</p><p>If you tag a post with both <code>News</code> <em>and</em> <code>Podcast</code> - then it appears in both sections. Tag archives are like dedicated home-pages for each category of content that you have. They have their own pages, their own RSS feeds, and can support their own cover images and meta data.</p><h3 id="the-primary-tag">The primary tag</h3><p>Inside the Ghost editor, you can drag and drop tags into a specific order. The first tag in the list is always given the most importance, and some themes will only display the primary tag (the first tag in the list) by default. </p><blockquote><em><strong>News</strong>, Technology, Startup</em></blockquote><p>So you can add the most important tag which you want to show up in your theme, but also add related tags which are less important.</p><h3 id="private-tags">Private tags</h3><p>Sometimes you may want to assign a post a specific tag, but you don't necessarily want that tag appearing in the theme or creating an archive page. In Ghost, hashtags are private and can be used for special styling.</p><p>For example, if you sometimes publish posts with video content - you might want your theme to adapt and get rid of the sidebar for these posts, to give more space for an embedded video to fill the screen. In this case, you could use private tags to tell your theme what to do.</p><blockquote><em><strong>News</strong>, #video</em></blockquote><p>Here, the theme would assign the post publicly displayed tags of News - but it would also keep a private record of the post being tagged with #video. In your theme, you could then look for private tags conditionally and give them special formatting. </p><blockquote><em>You can find documentation for theme development techniques like this and many more over on Ghost's extensive <a href="https://ghost.org/docs/api/handlebars-themes/">theme docs</a>.</em></blockquote><h2 id="dynamic-routing">Dynamic routing</h2><p>Dynamic routing gives you the ultimate freedom to build a custom publication to suit your needs. Routes are rules that map URL patterns to your content and templates. </p><p>You may not want content tagged with <code>News</code> to exist on: <code>example.com/tag/news</code>. Instead, you want it to exist on <code>example.com/news</code> .</p><p>In this case you can use dynamic routes to create customised collections of content on your site. It's also possible to use multiple templates in your theme to render each content type differently.</p><p>There are lots of use cases for dynamic routing with Ghost, here are a few common examples: </p><ul><li>Setting a custom home page with its own template</li><li>Having separate content hubs for blog and podcast, that render differently, and have custom RSS feeds to support two types of content</li><li>Creating a founders column as a unique view, by filtering content created by specific authors</li><li>Including dates in permalinks for your posts</li><li>Setting posts to have a URL relative to their primary tag like <code>example.com/europe/story-title/</code><br></li></ul><blockquote><em>Dynamic routing can be configured in Ghost using <a href="http://yaml.org/spec/1.2/spec.html" rel="noreferrer nofollow noopener">YAML</a> files. Read our dynamic routing <a href="https://ghost.org/docs/api/handlebars-themes/routing/">documentation</a> for further details.</em></blockquote><h2 id="next-apps-integrations">Next: Apps &amp; Integrations</h2><p>Work with all your favourite apps and tools using our <a href="https://gerhut.me/apps-integrations/">integrations</a>, or create your own custom integrations with webhooks.</p>]]></content:encoded></item><item><title><![CDATA[Apps & integrations]]></title><description><![CDATA[Work with all your favourite apps and tools or create your own custom integrations using the Ghost API.]]></description><link>https://gerhut.me/apps-integrations/</link><guid isPermaLink="false">5fa11306356f4100015596db</guid><category><![CDATA[Getting Started]]></category><dc:creator><![CDATA[Ghost]]></dc:creator><pubDate>Tue, 03 Nov 2020 08:21:27 GMT</pubDate><media:content url="https://static.ghost.org/v3.0.0/images/app-integrations.png" medium="image"/><content:encoded><![CDATA[<h2 id="work-with-your-existing-tools">Work with your existing tools</h2><img src="https://static.ghost.org/v3.0.0/images/app-integrations.png" alt="Apps & integrations"><p>It's possible to connect your Ghost site to hundreds of the most popular apps and tools using integrations that take no more than a few minutes to setup.</p><p>Whether you need to automate workflows, connect your email list, build a community or embed products from your ecommerce store, our <a href="https://ghost.org/integrations/">integrations library</a> has got it all covered with hundreds of tutorials.</p><figure class="kg-card kg-image-card kg-width-full"><img src="https://static.ghost.org/v3.0.0/images/integrations-icons.png" class="kg-image" alt="Apps & integrations"></figure><h2 id="zapier">Zapier</h2><p>On top of this, you can connect your Ghost site to more than 1,000 external services using the official integration with <a href="https://zapier.com">Zapier</a>.</p><p>Zapier sets up automations with Triggers and Actions, which allows you to create and customise a wide range of connected applications.</p><blockquote><strong>Example</strong>: When someone new subscribes to a newsletter on a Ghost site (Trigger) then the contact information is automatically pushed into MailChimp (Action).</blockquote><p><strong>Here are the most popular Ghost&lt;&gt;Zapier automation templates:</strong> </p><!--kg-card-begin: markdown--><script src="https://zapier.com/apps/embed/widget.js?services=Ghost&container=true&limit=8"></script>
<!--kg-card-end: markdown--><h2 id="custom-integrations">Custom integrations</h2><p>At the heart of Ghost sits a robust JSON API – designed to create, manage and retrieve content with ease. </p><p>It's possible to create custom Ghost integrations with dedicated API keys and webhooks from the Integrations page within Ghost Admin. </p><figure class="kg-card kg-image-card"><img src="https://static.ghost.org/v3.0.0/images/integrations-and-webhooks-in-ghost.png" class="kg-image" alt="Apps & integrations"></figure><p>Beyond that, the API allows you to build entirely custom publishing apps. You can send content from your favourite desktop editor, build a custom interface for handling editorial workflow or use Ghost as a full headless CMS with a custom front-end.</p><p>The Ghost API is <a href="https://ghost.org/docs/api/">thoroughly documented</a> and straightforward to work with for developers of almost any level. </p><h2 id="final-step-themes">Final step: Themes</h2><p>Alright, on to the last post in our welcome-series! If you're curious about creating your own Ghost theme from scratch, <a href="https://gerhut.me/themes/">find out how that works</a>.</p>]]></content:encoded></item><item><title><![CDATA[Creating a custom theme]]></title><description><![CDATA[Ghost comes with a beautiful default theme designed for publishers which can easily be adapted for most purposes, or you can build a custom theme to suit your needs.]]></description><link>https://gerhut.me/themes/</link><guid isPermaLink="false">5fa11306356f4100015596d9</guid><category><![CDATA[Getting Started]]></category><dc:creator><![CDATA[Ghost]]></dc:creator><pubDate>Tue, 03 Nov 2020 08:21:26 GMT</pubDate><media:content url="https://static.ghost.org/v3.0.0/images/creating-a-custom-theme.png" medium="image"/><content:encoded><![CDATA[<h2 id="ghost-themes">Ghost themes</h2><img src="https://static.ghost.org/v3.0.0/images/creating-a-custom-theme.png" alt="Creating a custom theme"><p>Ghost comes with a default theme called Casper, which is designed to be a clean, readable publication layout and can be easily adapted for most purposes.</p><p>If you need something a little more customised, it's entirely possible to build on top of existing open source themes, or to build your own from scratch. Rather than giving you a few basic settings which act as a poor proxy for code, we just let you write code.</p><h2 id="marketplace">Marketplace</h2><p>There are a huge range of both free and premium pre-built themes which you can download from the <a href="https://ghost.org/marketplace/">Ghost Theme Marketplace</a>:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://static.ghost.org/v3.0.0/images/theme-marketplace.png" class="kg-image" alt="Creating a custom theme"><figcaption>Anyone can write a completely custom Ghost theme with some solid knowledge of HTML and CSS</figcaption></figure><h2 id="theme-development">Theme development</h2><p>Ghost themes are written with a templating language called handlebars, which has a set of dynamic helpers to insert your data into template files. For example: <code>{{author.name}}</code> outputs the name of the current author.</p><p>The best way to learn how to write your own Ghost theme is to have a look at <a href="https://github.com/TryGhost/Casper">the source code for Casper</a>, which is heavily commented and should give you a sense of how everything fits together.<br></p><ul><li><code>default.hbs</code> is the main template file, all contexts will load inside this file unless specifically told to use a different template.</li><li><code>post.hbs</code> is the file used in the context of viewing a post.</li><li><code>index.hbs</code> is the file used in the context of viewing the home page.</li><li>and so on</li></ul><p>We've got <a href="https://ghost.org/docs/api/handlebars-themes/">full and extensive theme documentation</a> which outlines every template file, context and helper that you can use. You can also get started with our useful <a href="https://github.com/TryGhost/Starter/">starter theme</a>, which includes the most common foundations and components required to build your own theme.</p><blockquote>If you want to chat with other people making Ghost themes to get any advice or help, there's also a <strong>themes</strong> section on our <a href="https://forum.ghost.org/c/themes">public Ghost forum</a>.</blockquote>]]></content:encoded></item><item><title><![CDATA[Summary of decorators in TypeScript]]></title><description><![CDATA[<pre><code>declare classDecorator =
  (ClassConstructor) =&gt; DecoratedClassConstructor | undefined

declare memberPropertyDecorator =
  (prototype, name, descriptor?) =&gt; decoratedDescriptor | undefined

declare memberAccessorDecorator =
  (prototype, name, descriptor) =&gt; decoratedDescriptor | undefined

declare memberMethodDecorator =
  (prototype, name, descriptor) =&gt; decoratedDescriptor | undefined

declare memberMethodParameterDecorator =
  (prototype, name, index) =&gt; void

declare staticPropertyDecorator =
  (ClassConstructor, name, descriptor?) =&gt; decoratedDescriptor | undefined

declare staticAccessorDecorator =
  (ClassConstructor, name, descriptor)</code></pre>]]></description><link>https://gerhut.me/summary-of-decorator/</link><guid isPermaLink="false">5fa11a5ceb2f540001853c9b</guid><dc:creator><![CDATA[George Cheng]]></dc:creator><pubDate>Wed, 27 Nov 2019 13:01:09 GMT</pubDate><content:encoded><![CDATA[<pre><code>declare classDecorator =
  (ClassConstructor) =&gt; DecoratedClassConstructor | undefined

declare memberPropertyDecorator =
  (prototype, name, descriptor?) =&gt; decoratedDescriptor | undefined

declare memberAccessorDecorator =
  (prototype, name, descriptor) =&gt; decoratedDescriptor | undefined

declare memberMethodDecorator =
  (prototype, name, descriptor) =&gt; decoratedDescriptor | undefined

declare memberMethodParameterDecorator =
  (prototype, name, index) =&gt; void

declare staticPropertyDecorator =
  (ClassConstructor, name, descriptor?) =&gt; decoratedDescriptor | undefined

declare staticAccessorDecorator =
  (ClassConstructor, name, descriptor) =&gt; decoratedDescriptor | undefined

declare staticMethodDecorator =
  (ClassConstructor, name, descriptor) =&gt; decoratedDescriptor | undefined</code></pre>]]></content:encoded></item><item><title><![CDATA[Automatically Pass Export Compliance Test in Testflight]]></title><description><![CDATA[<p>These days I am configuring CI/CD for an iOS project. <a href="http://fastlane.tools/">Fastlane</a> is a fantastic tool for  automatically building, archiving, as well as uploading iOS app to Testflight.</p><p>But I found that each time a new build of iOS app is uploaded to Testflight, it always notices me "This build</p>]]></description><link>https://gerhut.me/auto-pass-export-complicance-in-testflight/</link><guid isPermaLink="false">5fa11a5ceb2f540001853c9a</guid><dc:creator><![CDATA[George Cheng]]></dc:creator><pubDate>Sat, 09 Mar 2019 14:51:06 GMT</pubDate><content:encoded><![CDATA[<p>These days I am configuring CI/CD for an iOS project. <a href="http://fastlane.tools/">Fastlane</a> is a fantastic tool for  automatically building, archiving, as well as uploading iOS app to Testflight.</p><p>But I found that each time a new build of iOS app is uploaded to Testflight, it always notices me "This build is missing export compliance information", and I have to do some more operations on that page to make it able to test. </p><p>To make it automatically, I set <code>ITSAppUsesNonExemptEncryption</code> to <code>NO</code> in the XCode project. After this the new builds upload to Testflight will be automatically able to test.</p>]]></content:encoded></item><item><title><![CDATA[明确一下 XMLHttpRequest#withCredentials 的作用]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>先重复一下两个启用 withCredentials 之后 CORS 响应的要点：</p>
<ol>
<li>响应头 <code>Access-Control-Allow-Origin</code> 不能为 <code>*</code>，必须明确指定为包含 <code>Origin</code> 的值。</li>
<li>响应头 <code>Access-Control-Allow-Credentials</code> 必须为 <code>true</code></li>
</ol>
<p>再重复一个 cookie origin 的问题：</p>
<blockquote>
<p>请求发到哪个 origin，带的就是哪个 origin 的 cookie。不可能出现 <em>a.com 携带 a.com 的 cookie 向 b.com 发 ajax 请求</em> 的情况发生。</p>
</blockquote>
<h2 id="case">几个 case</h2>
<ol>
<li><code>withCredentials</code> 请求的响应操作 cookie，浏览器打开目标域网页输出 cookie：<strong>能获取到 cookie</strong></li>
<li>非</li></ol>]]></description><link>https://gerhut.me/xmlhttprequest-withcredentials/</link><guid isPermaLink="false">5fa11a5ceb2f540001853c99</guid><dc:creator><![CDATA[George Cheng]]></dc:creator><pubDate>Fri, 01 Feb 2019 10:38:52 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><p>先重复一下两个启用 withCredentials 之后 CORS 响应的要点：</p>
<ol>
<li>响应头 <code>Access-Control-Allow-Origin</code> 不能为 <code>*</code>，必须明确指定为包含 <code>Origin</code> 的值。</li>
<li>响应头 <code>Access-Control-Allow-Credentials</code> 必须为 <code>true</code></li>
</ol>
<p>再重复一个 cookie origin 的问题：</p>
<blockquote>
<p>请求发到哪个 origin，带的就是哪个 origin 的 cookie。不可能出现 <em>a.com 携带 a.com 的 cookie 向 b.com 发 ajax 请求</em> 的情况发生。</p>
</blockquote>
<h2 id="case">几个 case</h2>
<ol>
<li><code>withCredentials</code> 请求的响应操作 cookie，浏览器打开目标域网页输出 cookie：<strong>能获取到 cookie</strong></li>
<li>非 <code>withCredentials</code> 请求的响应操作 cookie，浏览器打开目标域网页输出 cookie：<strong>不能获取到 cookie</strong></li>
<li><code>withCredentials</code> 请求的响应操作 cookie，<code>withCredentials</code> 请求读取 cookie：<strong>能获取到 cookie</strong></li>
<li><code>withCredentials</code> 请求的响应操作 cookie，非 <code>withCredentials</code> 请求读取 cookie：<strong>不能获取到 cookie</strong></li>
</ol>
<h2 id>结论</h2>
<ul>
<li><code>withCredentials</code> 未启用时的 CORS 请求
<ol>
<li>请求不会携带任何 cookie。</li>
<li>响应不会影响任何本地 cookie 存储。</li>
</ol>
</li>
<li><code>withCredentials</code> 启用时的 CORS 请求
<ol>
<li>请求会携带目标 origin 的 cookie。</li>
<li>响应可以影响本地对应目标 origin 的 cookie 存储。</li>
</ol>
</li>
</ul>
<p>BTW，非 CORS 请求和响应都会正常携带和操作当前 origin 的 cookie。</p>
<p>其实很简单，总是被误导</p>
<p>提供一个 <a href="https://github.com/Gerhut/withCredentials">GitHub repo</a> 用来验证和测试，可以使用 <code>localhost:3000</code> 打开网页，<code>127.0.0.1:3000</code> 发 Ajax。</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[如何测试手撸的 TypeScript 类型描述文件]]></title><description><![CDATA[TypeScript 现在已经可以编译 JavaScript 文件了，能不能把 JavaScript 的单元测试文件直接利用起来呢？]]></description><link>https://gerhut.me/how-to-test-typescript-declaration-files/</link><guid isPermaLink="false">5fa11a5ceb2f540001853c98</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[George Cheng]]></dc:creator><pubDate>Wed, 27 Jun 2018 02:34:37 GMT</pubDate><media:content url="https://gerhut.me/content/images/2018/06/1_i0qclSPNcjj8cWOPr3wLxg.png" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://gerhut.me/content/images/2018/06/1_i0qclSPNcjj8cWOPr3wLxg.png" alt="如何测试手撸的 TypeScript 类型描述文件"><p>最近在用我的 <a href="https://npm.im/axiosist">axiosist</a> 做单元测试的时候，发现在 VSCode 下用这个库写代码，自动提示不是特别友好。想把它做的友好的最简单方法就是用 TypeScript 重写一下，但是因为目前采用 commonjs 的模块直接 <code>module.exports = axiosist</code> 的方式导出的默认模块，在 TypeScript 下就要用官方很不推荐的 <code>export = axiosist</code> 的语法来替代。再加上一大堆的编译发布流程，感觉改动太大了。所以打算参照 index.js 先手撸一个 index.d.ts 出来用。那么遇到的问题就是，如何验证我这个手撸的 index.d.ts 是符合 index.js 的定义？看了下大家的普遍做法，是再写一个 TypeScript 的单元测试，但是我觉得既然我已经有一个 JavaScript 写的单元测试了，而 TypeScript 现在已经可以编译 JavaScript 文件了，能不能把 JavaScript 的单元测试文件直接利用起来呢？于是我开始了尝试：</p>
<p>首先我先去掉我的 index.d.ts 文件，用 tsc 跑一下 test.js</p>
<pre><code>$ npx tsc test.js
</code></pre>
<p>报错不支持 .js 这个后缀，我们加上 <code>--allowJs</code> 启用 JavaScript 文件的支持</p>
<pre><code>$ npx tsc --allowJs test.js
</code></pre>
<p>报错不能覆盖现有文件，我们加上 <code>--noEmit</code> 去掉输出</p>
<pre><code>$ npx tsc --allowJs --noEmit test.js
</code></pre>
<p>运行成功了，但是这并不是我们想要的，因为它并没有检查类型。因为没有写任何类型的情况下，所有的类型都是 any，所以我们加上 <code>--strict</code> 参数，启用严格模式，禁止使用 any 类型</p>
<pre><code>$ npx tsc --allowJs --noEmit --strict test.js
</code></pre>
<p>仍然没有报错，说明 JavaScript 文件虽然参与编译，但是并没有参与类型检查，于是我们加上 <code>--checkJs</code> 参数，使 JavaScript 文件参与到类型检查中。</p>
<pre><code>$ npx tsc --allowJs --noEmit --strict --checkJs test.js
</code></pre>
<p>成功了，所有的 any 报错都出来了。这个时候把 index.d.ts 启用，我们就可以测试我们写的 index.d.ts 是否符合库代码了。</p>
<p>把刚才的参数整理成 tsconfig.json 文件，就是这样</p>
<pre><code>{
    &quot;compilerOptions&quot;: {
        &quot;allowJs&quot;: true,
        &quot;checkJs&quot;: true,
        &quot;noEmit&quot;: true,
        &quot;strict&quot;: true,
    },
    &quot;files&quot;: [
        &quot;test.js&quot;
    ]
}
</code></pre>
<p>放到项目根目录，我们就有了一个不用维护任何 TypeScript 逻辑代码，只用现有的 JavaScript 文件就可以验证 d.ts 文件是否符合要求的方法，当然，前提是 JavaScript 的测试要尽量全面，调用了所有接口。这样才能尽可能保证 TypeScript 在验证类型时不会发生漏验的情况。</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[从 Nginx​ 启动时自动解析域名讲起]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>目前赛亚测试服务器的现状就是一台服务器上跑的服务非常多，我们把所有服务都 Dockerize 化之后通过统一的一个 Nginx 反向代理容器对外开放，其中下层的容器名作为反向代理的域名。</p>
<pre><code>server {
    listen 80;
    server_name project.domain;

    location / {
        proxy_set_header        Host $http_host;
        proxy_set_header        X-Real-IP $remote_addr;
        proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_pass      http://container:80;
        proxy_redirect  off;
    }
}
</code></pre>
<p>我们遇到的第一个问题就是当其中某个下层容器因为各种原因启动失败的时候，Nginx 就会因为该容器名对应的域名解析失败导致无法重启，解决的方法就是把 <code>proxy_pass</code></p>]]></description><link>https://gerhut.me/cong-nginx-jie-xi/</link><guid isPermaLink="false">5fa11a5ceb2f540001853c97</guid><dc:creator><![CDATA[George Cheng]]></dc:creator><pubDate>Mon, 21 May 2018 16:10:12 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><p>目前赛亚测试服务器的现状就是一台服务器上跑的服务非常多，我们把所有服务都 Dockerize 化之后通过统一的一个 Nginx 反向代理容器对外开放，其中下层的容器名作为反向代理的域名。</p>
<pre><code>server {
    listen 80;
    server_name project.domain;

    location / {
        proxy_set_header        Host $http_host;
        proxy_set_header        X-Real-IP $remote_addr;
        proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_pass      http://container:80;
        proxy_redirect  off;
    }
}
</code></pre>
<p>我们遇到的第一个问题就是当其中某个下层容器因为各种原因启动失败的时候，Nginx 就会因为该容器名对应的域名解析失败导致无法重启，解决的方法就是把 <code>proxy_pass</code> 的值传入变量，此时 Nginx 就不会在启动的时候解析这个域名了。</p>
<pre><code>server {
    listen 80;
    server_name project.domain;

    location / {
        proxy_set_header        Host $http_host;
        proxy_set_header        X-Real-IP $remote_addr;
        proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;

        set             $remote container;
        proxy_pass      http://$remote:80;
        proxy_redirect  off;
    }
}
</code></pre>
<p>我们遇到的第二个问题是要在测试服务器上做个简单的滚动部署，让测试人员无缝切换，大概就是每个服务搞两个容器，用 Nginx 负载均衡一下，部署的时候先更新第一个容器再更新第二个容器。这个时候我们遇到的问题是在 <code>upstream</code> 这个上下文中是不能像 <code>location</code> 上下文这样用变量代替域名的，直接写域名又会导致 Nginx 启动依赖下层容器的问题。研究了一个晚上，发现用 unix socket 多转一层可以 workaround。</p>
<pre><code>server {
    listen unix:/var/run/socket-1.sock;

    location / {
        proxy_set_header        Host $http_host;
        proxy_set_header        X-Real-IP $remote_addr;
        proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_pass      http://container-1:80;
        proxy_redirect  off;
    }
}

server {
    listen unix:/var/run/socket-2.sock;

    location / {
        proxy_set_header        Host $http_host;
        proxy_set_header        X-Real-IP $remote_addr;
        proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_pass      http://container-2:80;
        proxy_redirect  off;
    }
}

upstream upstream {
    server unix:/var/run/socket-1.sock;
    server unix:/var/run/socket-2.sock;
}

server {
    listen 80;
    server_name project.domain;

    location / {
        proxy_set_header        Host $http_host;
        proxy_set_header        X-Real-IP $remote_addr;
        proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_pass      http://upstream:80;
        proxy_redirect  off;
    }
}</code></pre>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[使用 CI 构建 React Native 项目]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>网上已经有很多教程了，我这边也整理一份我跑通的流程。</p>
<h2 id="ios">iOS 构建部分</h2>
<p>首先参照 <a href="http://facebook.github.io/react-native/docs/running-on-device.html#building-your-app-for-production">官方文档</a> 调整代码</p>
<p>其中文档中需要在 Xcode 下构建的部分，使用命令行来操作就是：</p>
<pre><code>xcodebuild archive -project ios/$PROJECT.xcodeproj
                   -scheme $PROJECT
                   -archivePath ios/build/$PROJECT.xcarchive
xcodebuild -exportArchive
           -archivePath ios/build/$PROJECT.xcarchive
           -exportPath ios/build/
           -exportOptionsPlist ios/ExportOptions.plist
           -allowProvisioningUpdates
</code></pre>
<p>要点：</p>
<ol>
<li>绝大多数构建参数都可以用 Xcode 打开项目调整。</li>
<li><code>ExportOptions.plist</code> 在使用 Xcode 手动 Export 一次之后会自动生成。</li></ol>]]></description><link>https://gerhut.me/ci/</link><guid isPermaLink="false">5fa11a5ceb2f540001853c96</guid><dc:creator><![CDATA[George Cheng]]></dc:creator><pubDate>Tue, 03 Apr 2018 14:59:28 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><p>网上已经有很多教程了，我这边也整理一份我跑通的流程。</p>
<h2 id="ios">iOS 构建部分</h2>
<p>首先参照 <a href="http://facebook.github.io/react-native/docs/running-on-device.html#building-your-app-for-production">官方文档</a> 调整代码</p>
<p>其中文档中需要在 Xcode 下构建的部分，使用命令行来操作就是：</p>
<pre><code>xcodebuild archive -project ios/$PROJECT.xcodeproj
                   -scheme $PROJECT
                   -archivePath ios/build/$PROJECT.xcarchive
xcodebuild -exportArchive
           -archivePath ios/build/$PROJECT.xcarchive
           -exportPath ios/build/
           -exportOptionsPlist ios/ExportOptions.plist
           -allowProvisioningUpdates
</code></pre>
<p>要点：</p>
<ol>
<li>绝大多数构建参数都可以用 Xcode 打开项目调整。</li>
<li><code>ExportOptions.plist</code> 在使用 Xcode 手动 Export 一次之后会自动生成。</li>
<li>在 archive 阶段时配置环境变量 <code>RCT_NO_LAUNCH_PACKAGER=1</code> 会跳过调用构建服务器的步骤。</li>
<li>因为 <code>project.xcworkspace</code> 目录是会被 React Native 脚手架 gitignore 的，所以网上很多基于 <code>-workspace ios/$PROJECT.xcodeproj/project.xcworkspace</code> 都是不能直接用 CI runner 跑的。使用 <code>-project ios/$PROJECT.xcodeproj</code> 可以解决。</li>
<li>注意不要丢失 <code>ios/$PROJECT.xcodeproj/xcshareddata/xcschemes/$PROJECT.xcscheme</code> 文件，该文件很容易在 Xcode 配置时丢失或转入被 gitignore 的 xcuserdata 目录。</li>
</ol>
<h2 id="android">Android 构建部分</h2>
<p>Android 构建方面相对简单，参照 <a href="http://facebook.github.io/react-native/docs/signed-apk-android.html">官方文档</a> 签名即可</p>
<p>我不愿意在 CI 中改变当前目录，所以我的命令行是：</p>
<pre><code>android/gradlew assembleRelease --project-dir android
</code></pre>
<p>要点：</p>
<ol>
<li>手机跑不了未签名的 Release 包，而 Debug 包一定要连接构建服务器。</li>
<li>GitLab Runner 在使用 shell 模式下不会加载我的 <code>.zshrc</code>/<code>.zprofile</code>，所以 <code>ANDROID_HOME</code> 这种环境变量请存在 <code>~/.profile</code> 中。</li>
<li>keystore 文件是会被 gitignore 的，存在仓库里也不安全，在部署 CI Runner 的时候记得把 keystore 文件拷过去。</li>
</ol>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[axios + 5h4d0w50ck5 越过 GFW]]></title><description><![CDATA[使用 tunnel-agent 配合 axios 实现对 5h4d0w50ck5 的支持。]]></description><link>https://gerhut.me/axios-5h4d0w50ck5/</link><guid isPermaLink="false">5fa11a5ceb2f540001853c95</guid><category><![CDATA[node]]></category><category><![CDATA[HTTP]]></category><dc:creator><![CDATA[George Cheng]]></dc:creator><pubDate>Sat, 24 Mar 2018 18:03:02 GMT</pubDate><media:content url="https://gerhut.me/content/images/2018/03/China-VPN-for-Mac.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://gerhut.me/content/images/2018/03/China-VPN-for-Mac.jpg" alt="axios + 5h4d0w50ck5 越过 GFW"><p>Updates: 写了个 axios interceptor <a href="https://www.npmjs.com/package/axios-https-proxy">axios-https-proxy</a> 可以解决 HTTPS 请求 HTTP 代理的问题。</p>
<p>这两天在做 <a href="https://www.npmjs.com/package/googleapis">googleapis</a> 的相关开发。</p>
<p><a href="https://www.npmjs.com/package/googleapis">googleapis</a> 是一个前后端同构库，请求部分基于 axios 实现。</p>
<p>因为众所周知的原因，我们的电脑要翻墙才能访问 www.googleapis.com 的服务器。</p>
<p>我这边是采用 5h4d0w50ck5 翻的墙，在命令行下大部分情况可以配置 <code>HTTP_PROXY</code> 和 <code>HTTPS_PROXY</code> 环境变量实现通过 HTTP 协议代理访问墙外的内容。</p>
<p>axios 也同样支持了这种配置方式。</p>
<p>然而，axios 仅仅支持 HTTP 代理协议中<em>直接代理</em>这一种方式，不支持<em>隧道代理</em>的方式。</p>
<p>同时，axios 无视了环境变量中的协议头，默认代理服务器的协议与目标地址协议相同。</p>
<p>也就是说：如果我们请求 http 地址，axios 就使用 http 协议连接 <code>HTTP_PROXY</code> 中配置的服务器来访问目标地址；如果我们请求 https 地址，axios 就使用 https 协议连接 <code>HTTPS_PROXY</code> 中配置的服务器来访问目标地址。两种情况下都是采用直接代理方式与代理服务器沟通。</p>
<p>同样地，在 axios 中配置 <code>proxy</code> 参数也是无法配置代理服务器协议的，协议以请求的地址为准。</p>
<p>但是，我们 5h4d0w50ck5 提供的 HTTP 代理服务器仅支持 HTTP 协议，不支持 HTTPS 协议。</p>
<p>所以说，当我们透过 5h4d0w50ck5 提供的 HTTP 代理服务器访问 HTTPS 地址时，axios 会采用 HTTPS 协议访问 5h4d0w50ck5 提供的 HTTP 代理服务器，从而访问失败。</p>
<p>相关 issue：<a href="https://github.com/axios/axios/issues/925">https://github.com/axios/axios/issues/925</a></p>
<p>为了解决这个问题，我首先尝试了一种不标准的方法：使用直接代理方式连接 HTTP 服务器访问 HTTPS 目标。这种方法非常不安全，因为将 HTTPS 传输的加密内容暴露在了 HTTP 的未加密环境下。从而使可信内容变得不可信。</p>
<p>我修改了 axios 源码中对于 isHttps 的判断逻辑，使用 HTTP 协议连接 HTTPS 代理。</p>
<p>结果是，拿不到响应内容，在终端下用 nc 模拟，仍然拿不到内容。</p>
<pre><code>➜ nc -v 127.0.0.1 1087
found 0 associations
found 1 connections:
     1:	flags=82&lt;CONNECTED,PREFERRED&gt;
    outif lo0
    src 127.0.0.1 port 49499
    dst 127.0.0.1 port 1087
    rank info not available
    TCP aux info available

Connection to 127.0.0.1 port 1087 [tcp/cplscrambler-in] succeeded!
GET https://www.google.com/ HTTP/1.1

HTTP/1.1 200 Connection established
</code></pre>
<p>说明 5h4d0w50ck5 提供的 HTTP 代理服务器不支持这种不标准不安全的访问模式。</p>
<p>所以说，我们无法使用直接代理的方式达到目标，只能转而使用隧道代理的方式。</p>
<p>这个时候我们就要请出 node 界 HTTP 客户端老大哥 <a href="https://www.npmjs.com/package/request">request</a>，看看它是怎么实现隧道代理的。</p>
<p>于是我们就发现了它的一个支撑库 <a href="https://www.npmjs.com/package/tunnel-agent">tunnel-agent</a>，<a href="https://www.npmjs.com/package/request">request</a> 是通过 <a href="https://www.npmjs.com/package/tunnel-agent">tunnel-agent</a>生成 <a href="https://nodejs.org/api/http.html#http_class_http_agent">HTTP Agent</a> 的方式实现隧道代理的。</p>
<p>幸运的是，axios 也支持配置 <a href="https://nodejs.org/api/http.html#http_class_http_agent">HTTP Agent</a>，它有两个配置参数 <code>httpAgent</code> 和 <code>httpsAgent</code>，分别配置不同协议下请求的 <a href="https://nodejs.org/api/http.html#http_class_http_agent">HTTP Agent</a>。所以我们就可以把 <a href="https://www.npmjs.com/package/tunnel-agent">tunnel-agent</a> 借过来用在 axios 上了。</p>
<p>首先我们是这样配置的：</p>
<pre><code>const axios = require('axios')
const { httpsOverHttp, httpOverHttp } = require('tunnel-agent')

const TUNNEL_OPTIONS = { proxy: { port: 1087 } }
axios.defaults.proxy = false // 强制禁用环境变量中的代理配置
axios.defaults.httpAgent = httpOverHttp(TUNNEL_OPTIONS)
axios.defaults.httpsAgent = httpsOverHttp(TUNNEL_OPTIONS)
</code></pre>
<p>然后我们发现请求报错</p>
<pre><code>TypeError: Agent option must be an Agent-like object, undefined, or false.
    at new ClientRequest (_http_client.js:104:11)
</code></pre>
<p>查 node 源码之后我们发现，原因是我们传入的 agent 没有 <code>addRequest</code> 方法。</p>
<p>但是我们打印了 <code>tunnelAgent.addRequest</code>，它的确是存在的。</p>
<p>这里的问题出现在 axios 中的 <code>utils.merge</code> 函数：它不会自动合并默认参数中挂在原型上的成员。</p>
<p>相关 issue：<a href="https://github.com/axios/axios/issues/1077">https://github.com/axios/axios/issues/1077</a></p>
<p>查 <a href="https://www.npmjs.com/package/tunnel-agent">tunnel-agent</a> 源码后发现，我们的 <code>tunnelAgent.addRequest</code> 恰好是挂在 <code>TunnelAgent</code> 构造函数原型上的，所以没有被合并到真正的请求配置中来。</p>
<p>所以我们不能采用配置默认参数的方式配置我们的 Agent，转而使用 interceptors 的方式配置。</p>
<pre><code>const axios = require('axios')
const { httpsOverHttp, httpOverHttp } = require('tunnel-agent')

const TUNNEL_OPTIONS = { proxy: { port: 1087 } }

axios.interceptors.request.use(function (config) {
  config.proxy = false // 强制禁用环境变量中的代理配置
  config.httpAgent = httpOverHttp(TUNNEL_OPTIONS)
  config.httpsAgent = httpsOverHttp(TUNNEL_OPTIONS)
  return config
})
</code></pre>
<p>这种方式使得 proxy 与 agent 配置参数无法被业务更改，但是的确是一种可行的方法。</p>
<p>把它存成了 5h4d0w50ck5.js 文件，当需要翻墙时 执行</p>
<pre><code>$ node --require ./5h4d0w50ck5 app.js
</code></pre>
<p>即可使用 axios 访问墙外的内容。</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Welcome to Ghost]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>Hey! Welcome to Ghost, it's great to have you :)</p>
<p>We know that first impressions are important, so we've populated your new site with some initial <strong>Getting Started</strong> posts that will help you get familiar with everything in no time. This is the first one!</p>
<h3 id="thereareafewthingsthatyoushouldknowupfront">There are a few things that</h3>]]></description><link>https://gerhut.me/welcome-2/</link><guid isPermaLink="false">5fa11a5ceb2f540001853c23</guid><category><![CDATA[Getting Started]]></category><dc:creator><![CDATA[Ghost]]></dc:creator><pubDate>Tue, 06 Mar 2018 10:19:41 GMT</pubDate><media:content url="https://casper.ghost.org/v1.0.0/images/welcome.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://casper.ghost.org/v1.0.0/images/welcome.jpg" alt="Welcome to Ghost"><p>Hey! Welcome to Ghost, it's great to have you :)</p>
<p>We know that first impressions are important, so we've populated your new site with some initial <strong>Getting Started</strong> posts that will help you get familiar with everything in no time. This is the first one!</p>
<h3 id="thereareafewthingsthatyoushouldknowupfront">There are a few things that you should know up-front:</h3>
<ol>
<li>
<p>Ghost is designed for ambitious, professional publishers who want to actively build a business around their content. That's who it works best for. If you're using Ghost for some other purpose, that's fine too - but it might not be the best choice for you.</p>
</li>
<li>
<p>The entire platform can be modified and customized to suit your needs, which is very powerful, but doing so <strong>does</strong> require some knowledge of code. Ghost is not necessarily a good platform for beginners or people who just want a simple personal blog.</p>
</li>
<li>
<p>For the best experience we recommend downloading the <a href="https://ghost.org/downloads/">Ghost Desktop App</a> for your computer, which is the best way to access your Ghost site on a desktop device.</p>
</li>
</ol>
<p>Ghost is made by an independent non-profit organisation called the Ghost Foundation. We are 100% self funded by revenue from our <a href="https://ghost.org/pricing">Ghost(Pro)</a> service, and every penny we make is re-invested into funding further development of free, open source technology for modern journalism.</p>
<p>The main thing you'll want to read about next is probably: <a href="https://gerhut.me/the-editor/">the Ghost editor</a>.</p>
<p>Once you're done reading, you can simply delete the default <strong>Ghost</strong> user from your team to remove all of these introductory posts!</p>
<!--kg-card-end: markdown-->]]></content:encoded></item></channel></rss>