<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          TypeScript 項(xiàng)目實(shí)踐——自動(dòng)化發(fā)布博客文章

          共 11624字,需瀏覽 24分鐘

           ·

          2020-08-22 12:39


          我最近嘗試養(yǎng)成寫(xiě)作習(xí)慣,寫(xiě)的文章比較多。我常用 ?Medium, dev.to ?和 ?Hashnode ?發(fā)布博客,同時(shí)也想在我的個(gè)人站點(diǎn)發(fā)一份。

          我的個(gè)人站點(diǎn)很簡(jiǎn)單,只采用了基本的 HTML、CSS,以及少量 JavaScript。但是我考慮改進(jìn)發(fā)布過(guò)程。

          從何入手呢?

          我在 Notion 上做了這個(gè)博客寫(xiě)作路線(xiàn)圖:



          這是一種簡(jiǎn)單的看板,我喜歡用它把所有想法呈現(xiàn)出來(lái)(或者說(shuō)把想法數(shù)字化)。我還使用它來(lái)創(chuàng)建草稿,不斷修改,然后發(fā)布。

          所以我用 Notion 寫(xiě)草稿,完成后,復(fù)制 Notion 的內(nèi)容并將其粘貼到在線(xiàn)工具中,將 Markdown 轉(zhuǎn)換為 HTML 格式,然后使用 HTML 來(lái)創(chuàng)建實(shí)際發(fā)布的文章。

          但這只是正文,即頁(yè)面的內(nèi)容。我需要?jiǎng)?chuàng)建整個(gè) HTML,包括頭部?jī)?nèi)容,正文和頁(yè)腳。

          這個(gè)過(guò)程乏味無(wú)聊,但好消息是它可以自動(dòng)化操作。這篇文章會(huì)闡述如何自動(dòng)化。我想向你展示我創(chuàng)建的這個(gè)新工具的幕后花絮,以及我從這個(gè)過(guò)程中學(xué)到的知識(shí)。

          功能

          我的主要想法是準(zhǔn)備發(fā)布整個(gè) HTML 文章。正如我之前提到的, ? ?和 ?

          ?部分的變化不大。所以我可以將它們用作“模板”。

          使用此模板,我提供的數(shù)據(jù)可以隨著我撰寫(xiě)和發(fā)布的每篇文章而改變。數(shù)據(jù)是模板中包含 ?{{ variableName }} ?的變量,舉個(gè)例子:

          <h1>{{?title?}}h1>

          現(xiàn)在,我可以使用模板并將變量替換為實(shí)際數(shù)據(jù),即每篇文章的特定信息。

          第二部分是博客正文。在模板中,用 ?{{ article }} ?表示。該變量將被從 Notion Markdown 生成的 HTML 代替。

          當(dāng)我們從 Notion 復(fù)制和粘貼筆記時(shí),我們得到了帶有 Markdown 樣式的內(nèi)容。本項(xiàng)目會(huì)將 Markdown 轉(zhuǎn)換為 HTML,并將其用作模板中的 ?article ?變量。

          為了創(chuàng)建理想的模板,我總結(jié)了所需創(chuàng)建的所有變量:

          • title
          • description
          • date
          • tags
          • imageAlt
          • imageCover
          • photographerUrl
          • photographerName
          • article
          • keywords

          使用這些變量,我創(chuàng)建了 ?template。

          要通過(guò)一些信息來(lái)構(gòu)建 HTML,我創(chuàng)建了一個(gè) ?json ?的文章配置文件 ?article.config.json,文件內(nèi)容如下:

          {
          ??"title":?"React?Hooks,?Context?API,?and?Pokemons",
          ??"description":?"Understanding?how?hooks?and?the?context?api?work",
          ??"date":?"2020-04-21",
          ??"tags":?[
          ????"javascript",
          ????"react"
          ??],
          ??"imageAlt":?"The?Ash?from?Pokemon",
          ??"photographerUrl":?"",
          ??"photographerName":?"kazuh.yasiro",
          ??"articleFile":?"article.md",
          ??"keywords":?"javascript,react"
          }

          第一步是項(xiàng)目應(yīng)該知道如何打開(kāi)和讀取模板以及文章配置。我使用此數(shù)據(jù)填充模板。

          首先是模板:

          const?templateContent:?string?=?await?getTemplateContent();

          因此,我們首先需要實(shí)現(xiàn) ?getTemplateContent ?功能。

          import?fs,?{?promises?}?from?'fs';
          import?{?resolve?}?from?'path';

          const?{?readFile?}?=?promises;

          const?getTemplateContent?=?async?():?Promise?=>?{
          ??const?contentTemplatePath?=?resolve(__dirname,?'../examples/template.html');
          ??return?await?readFile(contentTemplatePath,?'utf8');
          };

          使用 ?dirname ?參數(shù)的 ?resolve ?將獲得正在運(yùn)行的源文件的絕對(duì)路徑目錄,然后訪(fǎng)問(wèn) ?examples/template.html ?文件, ?readFile ?將從模板路徑異步讀取并返回內(nèi)容。

          現(xiàn)在我們有了模板內(nèi)容,需要對(duì)文章配置執(zhí)行相同的操作。

          const?getArticleConfig?=?async?():?Promise?=>?{
          ??const?articleConfigPath?=?resolve(__dirname,?'../examples/article.config.json');
          ??const?articleConfigContent?=?await?readFile(articleConfigPath,?'utf8');
          ??return?JSON.parse(articleConfigContent);
          };

          這里發(fā)生兩件事:

          • 由于 ?article.config.json ?具有 JSON 格式,因此我們需要在讀取文件后將此 JSON 字符串轉(zhuǎn)換為 JavaScript 對(duì)象。
          • articleConfigContent ?的返回將是我在函數(shù)返回類(lèi)型中定義的 ?ArticleConfig,細(xì)節(jié)如下:

          • type?ArticleConfig?=?{
            ??title:?string;
            ??description:?string;
            ??date:?string;
            ??tags:?string[];
            ??imageCover:?string;
            ??imageAlt:?string;
            ??photographerUrl:?string;
            ??photographerName:?string;
            ??articleFile:?string;
            ??keywords:?string;
            };

          獲得相關(guān)內(nèi)容后,我們還將使用這一新類(lèi)型。

          const?articleConfig:?ArticleConfig?=?await?getArticleConfig();


          現(xiàn)在,我們可以使用 ?replace ?方法在模板內(nèi)容中填充配置數(shù)據(jù),示例如下:

          templateContent.replace('title',?articleConfig.title)

          但是某些變量在模板中出現(xiàn)了不止一次,正則表達(dá)式會(huì)有助于解決這一問(wèn)題:

          new?RegExp('\\{\\{(?:\\\\s+)?(title)(?:\\\\s+)?\\}\\}',?'g');

          得到所有匹配 ?{{ title }} ?的字符串。我可以構(gòu)建一個(gè)接收目標(biāo)參數(shù)的函數(shù),并使用它替換 ?title。

          const?getPattern?=?(find:?string):?RegExp?=>
          ??new?RegExp('\\{\\{(?:\\\\s+)?('?+?find?+?')(?:\\\\s+)?\\}\\}',?'g');

          現(xiàn)在我們可以替換所有匹配項(xiàng)。title變量的示例:

          templateContent.replace(getPattern('title'),?articleConfig.title)

          但是我們并不想只替換 ?title變量,而是要替換文章配置中的所有變量,全部替換!

          const?buildArticle?=?(templateContent:?string)?=>?({
          ??with:?(articleConfig:?ArticleAttributes)?=>
          ????templateContent
          ??????.replace(getPattern('title'),?articleConfig.title)
          ??????.replace(getPattern('description'),?articleConfig.description)
          ??????.replace(getPattern('date'),?articleConfig.date)
          ??????.replace(getPattern('tags'),?articleConfig.articleTags)
          ??????.replace(getPattern('imageCover'),?articleConfig.imageCover)
          ??????.replace(getPattern('imageAlt'),?articleConfig.imageAlt)
          ??????.replace(getPattern('photographerUrl'),?articleConfig.photographerUrl)
          ??????.replace(getPattern('photographerName'),?articleConfig.photographerName)
          ??????.replace(getPattern('article'),?articleConfig.articleBody)
          ??????.replace(getPattern('keywords'),?articleConfig.keywords)
          });

          現(xiàn)在全部替換!我們這樣使用它:

          const?article:?string?=?buildArticle(templateContent).with(articleConfig);

          但是我們?cè)谶@里缺少兩部分:

          • tags
          • article

          在 JSON 配置文件中,tags ?是一個(gè)列表。對(duì)于列表:

          ['javascript',?'react'];

          最終的 HTML 將是:

          class="tag-link"?href="../../../tags/javascript.html">javascript
          class="tag-link"?href="../../../tags/react.html">react

          因此,我使用 ?{{ tag }} ?變量創(chuàng)建了另一個(gè)模板:?tag_template.html。我們只需要遍歷 ?tags ?列表并使用模板為每一項(xiàng)創(chuàng)建 HTML Tag。

          const?getArticleTags?=?async?({?tags?}:?{?tags:?string[]?}):?Promise?=>?{
          ??const?tagTemplatePath?=?resolve(__dirname,?'../examples/tag_template.html');
          ??const?tagContent?=?await?readFile(tagTemplatePath,?'utf8');
          ??return?tags.map(buildTag(tagContent)).join('');
          };

          在這里,我們:

          • 獲取標(biāo)簽?zāi)0迓窂?/section>
          • 獲取標(biāo)簽?zāi)0宓膬?nèi)容
          • 遍歷 ?tags ?并根據(jù)標(biāo)簽?zāi)0鍢?gòu)建最終的 HTML Tag

          buildTag ?是返回另一個(gè)函數(shù)的函數(shù)。

          const?buildTag?=?(tagContent:?string)?=>?(tag:?string):?string?=>
          ??tagContent.replace(getPattern('tag'),?tag);

          它接收參數(shù) ?tagContent ?- 這是標(biāo)簽?zāi)0宓膬?nèi)容 - 并返回一個(gè)接收 tag 參數(shù)并構(gòu)建最終標(biāo)簽 HTML 的函數(shù)。現(xiàn)在我們調(diào)用它以獲得標(biāo)簽。

          const?articleTags:?string?=?await?getArticleTags(articleConfig);

          現(xiàn)在這篇文章看起來(lái)像這樣:

          const?getArticleBody?=?async?({?articleFile?}:?{?articleFile:?string?}):?Promise?=>?{
          ??const?articleMarkdownPath?=?resolve(__dirname,?`../examples/${articleFile}`);
          ??const?articleMarkdown?=?await?readFile(articleMarkdownPath,?'utf8');
          ??return?fromMarkdownToHTML(articleMarkdown);
          };

          它收到 ?articleFile,我們嘗試獲取路徑,讀取文件并獲取 Markdown 內(nèi)容。然后將此內(nèi)容傳遞給 ?fromMarkdownToHTML ?函數(shù),以將 Markdown 轉(zhuǎn)換為 HTML。

          對(duì)于這一部分,我將使用一個(gè)外部庫(kù) ?showdown。它處理所有邊角案例,以將 Markdown 轉(zhuǎn)換為 HTML。

          import?showdown?from?'showdown';

          const?fromMarkdownToHTML?=?(articleMarkdown:?string):?string?=>?{
          ??const?converter?=?new?showdown.Converter()
          ??return?converter.makeHtml(articleMarkdown);
          };

          現(xiàn)在,我有了 tag 和文章的 HTML:

          const?templateContent:?string?=?await?getTemplateContent();
          const?articleConfig:?ArticleConfig?=?await?getArticleConfig();
          const?articleTags:?string?=?await?getArticleTags(articleConfig);
          const?articleBody:?string?=?await?getArticleBody(articleConfig);

          const?article:?string?=?buildArticle(templateContent).with({
          ??...articleConfig,
          ??articleTags,
          ??articleBody
          });

          我漏掉了一件事!以前,我總是需要將圖像封面路徑添加到文章配置文件中,像這樣:

          {
          ??"imageCover":?"an-image.png",
          }

          但是我們可以假設(shè)圖像名稱(chēng)為 ?cover。主要問(wèn)題是擴(kuò)展名,它可以是 ?.png,.jpg,.jpeg,或 ?.gif。

          因此,我建立了一個(gè)函數(shù)來(lái)獲取正確的圖像擴(kuò)展名。這個(gè)想法是在文件夾中搜索圖像。如果它存在于文件夾中,則返回?cái)U(kuò)展名。

          我從 existing 部分開(kāi)始。

          fs.existsSync(`${folder}/${fileName}.${extension}`);

          在這里,我正在使用 ?existsSync ?方法來(lái)查找文件。如果它存在于文件夾中,則返回true,否則為 false。

          我將此代碼添加到一個(gè)函數(shù)中:

          const?existsFile?=?(folder:?string,?fileName:?string)?=>?(extension:?string):?boolean?=>
          ??fs.existsSync(`${folder}/${fileName}.${extension}`);

          為什么要這樣做?

          使用這個(gè)功能,我需要傳遞參數(shù) ?folder ?,filename,extension。?folder ?和 ?filename ?總是相同的,區(qū)別在于 ?extension。

          因此,我可以構(gòu)建柯里化函數(shù)。這樣,我可以為相同的 ?folder ?和 ?filename ?建立不同的函數(shù),像這樣:

          const?hasFileWithExtension?=?existsFile(examplesFolder,?imageName);

          hasFileWithExtension('jpeg');?//?true?or?false
          hasFileWithExtension('jpg');?//?true?or?false
          hasFileWithExtension('png');?//?true?or?false
          hasFileWithExtension('gif');?//?true?or?false

          完整函數(shù)如下:

          const?getImageExtension?=?():?string?=>?{
          ??const?examplesFolder:?string?=?resolve(__dirname,?`../examples`);
          ??const?imageName:?string?=?'cover';
          ??const?hasFileWithExtension?=?existsFile(examplesFolder,?imageName);

          ??if?(hasFileWithExtension('jpeg'))?{
          ????return?'jpeg';
          ??}

          ??if?(hasFileWithExtension('jpg'))?{
          ????return?'jpg';
          ??}

          ??if?(hasFileWithExtension('png'))?{
          ????return?'png';
          ??}

          ??return?'gif';
          };

          但我不喜歡用硬編碼的字符串來(lái)表示圖像擴(kuò)展名。enum ?真的很酷!

          enum?ImageExtension?{
          ??JPEG?=?'jpeg',
          ??JPG?=?'jpg',
          ??PNG?=?'png',
          ??GIF?=?'gif'
          };

          現(xiàn)在使用我們的新的枚舉類(lèi)型 ?ImageExtension ?的函數(shù):

          const?getImageExtension?=?():?string?=>?{
          ??const?examplesFolder:?string?=?resolve(__dirname,?`../examples`);
          ??const?imageName:?string?=?'cover';
          ??const?hasFileWithExtension?=?existsFile(examplesFolder,?imageName);

          ??if?(hasFileWithExtension(ImageExtension.JPEG))?{
          ????return?ImageExtension.JPEG;
          ??}

          ??if?(hasFileWithExtension(ImageExtension.JPG))?{
          ????return?ImageExtension.JPG;
          ??}

          ??if?(hasFileWithExtension(ImageExtension.PNG))?{
          ????return?ImageExtension.PNG;
          ??}

          ??return?ImageExtension.GIF;
          };

          現(xiàn)在,我獲得了用以填充模板的所有數(shù)據(jù)。

          HTML 完成后,我想使用此數(shù)據(jù)創(chuàng)建實(shí)際的 HTML 文件。我大致需要獲取正確的路徑,HTML,并使用該 ?writeFile ?函數(shù)創(chuàng)建此文件。

          要獲取路徑,我需要確定我的博客的形式。它使用年、月、標(biāo)題組織文件夾,文件名為 ?index.html。

          一個(gè)例子是:

          2020/04/publisher-a-tooling-to-blog-post-publishing/index.html

          最初,我考慮過(guò)將這些數(shù)據(jù)添加到文章配置文件中。因此,我需要在每次更新時(shí)文章配置中的此屬性以獲取正確的路徑。

          但是另一個(gè)有趣的想法是根據(jù)文章配置文件中已有的一些數(shù)據(jù)來(lái)推斷路徑。我們有 ?date(例如 ?"2020-04-21")和 ?title(例如 ?"Publisher: tooling to automate blog post publishing")。從 ?date ?中,我可以得到年和月。從標(biāo)題中,我可以生成文章所在文件夾。?index.html ?文件始終是不變的。

          最終字符串如下所示:

          `${year}/${month}/${slugifiedTitle}`

          對(duì)于日期,這真的很簡(jiǎn)單,我可以拆分 ?-

          const?[year,?month]:?string[]?=?date.split('-');

          對(duì)于 ?slugifiedTitle,我構(gòu)建了一個(gè)函數(shù):

          const?slugify?=?(title:?string):?string?=>
          ??title
          ????.trim()
          ????.toLowerCase()
          ????.replace(/[^\\w\\s]/gi,?'')
          ????.replace(/[\\s]/g,?'-');

          它從字符串的開(kāi)頭和結(jié)尾刪除空格,然后將字符串小寫(xiě),然后刪除所有特殊字符(僅保留單詞和空格字符),最后,將所有空白替換為 ?-。

          整個(gè)函數(shù)如下所示:

          const?buildNewArticleFolderPath?=?({?title,?date?}:?{?title:?string,?date:?string?}):?string?=>?{
          ??const?[year,?month]:?string[]?=?date.split('-');
          ??const?slugifiedTitle:?string?=?slugify(title);

          ??return?resolve(__dirname,?`../../${year}/${month}/${slugifiedTitle}`);
          };

          這一函數(shù)嘗試獲取文章文件夾,它不會(huì)生成新文件,這就是為什么我沒(méi)有在最終字符串的末尾添加 ?/index.html ?的原因。

          為什么這樣做呢?因?yàn)樵趯?xiě)入新文件之前,我們始終需要?jiǎng)?chuàng)建文件夾。我使用 ?mkdir ?此文件夾路徑來(lái)創(chuàng)建它。

          const?newArticleFolderPath:?string?=?buildNewArticleFolderPath(articleConfig);
          await?mkdir(newArticleFolderPath,?{?recursive:?true?});

          現(xiàn)在,我可以使用新建的文件夾,在其中創(chuàng)建新的文章文件。

          const?newArticlePath:?string?=?`${newArticleFolderPath}/index.html`;
          await?writeFile(newArticlePath,?article);

          我們漏了一件事:當(dāng)我將圖像封面添加到文章配置文件夾中時(shí),我需要將其復(fù)制粘貼到正確的位置。

          對(duì)于 ?2020/04/publisher-a-tooling-to-blog-post-publishing/index.html ?示例, 圖像封面位于 assets 文件夾中:

          2020/04/publisher-a-tooling-to-blog-post-publishing/assets/cover.png

          為此,我需要做兩件事:

          • 使用 ?mkdir ?創(chuàng)建一個(gè)新的 ?assets ?文件夾
          • 使用 ?copyFile ?復(fù)制圖像文件并將其粘貼到新文件夾中

          要?jiǎng)?chuàng)建新文件夾,我只需要文件夾路徑。要復(fù)制和粘貼圖像文件,我需要當(dāng)前圖像路徑和文章圖像路徑。

          對(duì)于文件夾,因?yàn)槲椰F(xiàn)有 ?newArticleFolderPath,我只需要將此路徑連接到 asset 文件夾。

          const?assetsFolder:?string?=?`${newArticleFolderPath}/assets`;

          對(duì)于當(dāng)前的圖像路徑,已有帶正確擴(kuò)展名的 ?imageCoverFileName,我只需要獲取圖像封面路徑:

          const?imageCoverExamplePath:?string?=?resolve(__dirname,?`../examples/${imageCoverFileName}`);

          為了獲得將來(lái)的圖像路徑,我需要將圖像封面路徑和圖像文件名連接起來(lái):

          const?imageCoverPath:?string?=?`${assetsFolder}/${imageCoverFileName}`;

          使用所有這些數(shù)據(jù),我可以創(chuàng)建新文件夾:

          await?mkdir(assetsFolder,?{?recursive:?true?});

          并復(fù)制并粘貼圖像封面文件:

          await?copyFile(imageCoverExamplePath,?imageCoverPath);

          在實(shí)現(xiàn)這一 paths ?部分時(shí),我看到可以將它們?nèi)拷M合成一個(gè)函數(shù) ?buildPaths。

          const?buildPaths?=?(newArticleFolderPath:?string):?ArticlePaths?=>?{
          ??const?imageExtension:?string?=?getImageExtension();
          ??const?imageCoverFileName:?string?=?`cover.${imageExtension}`;
          ??const?newArticlePath:?string?=?`${newArticleFolderPath}/index.html`;
          ??const?imageCoverExamplePath:?string?=?resolve(__dirname,?`../examples/${imageCoverFileName}`);
          ??const?assetsFolder:?string?=?`${newArticleFolderPath}/assets`;
          ??const?imageCoverPath:?string?=?`${assetsFolder}/${imageCoverFileName}`;

          ??return?{
          ????newArticlePath,
          ????imageCoverExamplePath,
          ????imageCoverPath,
          ????assetsFolder,
          ????imageCoverFileName
          ??};
          };

          我還創(chuàng)建了 ?ArticlePaths ?類(lèi)型:

          type?ArticlePaths?=?{
          ??newArticlePath:?string;
          ??imageCoverExamplePath:?string;
          ??imageCoverPath:?string;
          ??assetsFolder:?string;
          ??imageCoverFileName:?string;
          };

          而且我可以使用該函數(shù)來(lái)獲取所需的所有路徑數(shù)據(jù):

          const?{
          ??newArticlePath,
          ??imageCoverExamplePath,
          ??imageCoverPath,
          ??assetsFolder,
          ??imageCoverFileName
          }:?ArticlePaths?=?buildPaths(newArticleFolderPath);

          現(xiàn)在是算法的最后一部分!我想快速驗(yàn)證創(chuàng)建的帖子。那么,如果可以在瀏覽器選項(xiàng)卡中打開(kāi)創(chuàng)建的帖子怎么樣?

          所以我做到了:

          await?open(newArticlePath);

          在這里,我使用 ?open ?庫(kù)來(lái)模擬終端打開(kāi)命令。

          就是這樣!

          結(jié)語(yǔ)

          這個(gè)項(xiàng)目很有趣!通過(guò)這個(gè)過(guò)程,我學(xué)到了一些很酷的東西:

          • 在學(xué)習(xí) TypeScript 時(shí),我想快速驗(yàn)證我正在編寫(xiě)的代碼。因此,我配置 ?nodemon為在每次保存文件時(shí)編譯并運(yùn)行代碼,使開(kāi)發(fā)過(guò)程如此動(dòng)態(tài)是很酷的。
          • 嘗試用新 nodejs 的 ?fs ?的 ?promises ?API:readFile,mkdirwriteFile,和 ?copyFile,它目前在規(guī)范 ?Stability: 2
          • 對(duì)某些函數(shù)進(jìn)行柯里化,使其可重復(fù)使用。
          • 枚舉和類(lèi)型是使?fàn)顟B(tài)在 TypeScript 中保持一致的好方法,也能很好地展示和記錄項(xiàng)目數(shù)據(jù)。數(shù)據(jù)契約確實(shí)很棒。
          • 工具化思維,這是我喜歡編程的方面,構(gòu)建工具有助于自動(dòng)執(zhí)行重復(fù)性任務(wù)并簡(jiǎn)化工作。

          希望你喜歡這篇文章。



          原文鏈接:https://www.freecodecamp.org/news/automating-my-blog-posts-publishing-process-with-typescript/

          作者:TK

          譯者:長(zhǎng)河漸落曉星沉

          掃碼關(guān)注公眾號(hào),訂閱更多精彩內(nèi)容。


          給個(gè)[在看],是對(duì)達(dá)達(dá)最大的支持!
          瀏覽 57
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  AAA级国产免费 | 亚洲精品韩国 | 国产成人综合欧美精品久久 | 日皮视频免费在线观看 | 白丝自慰喷水 |