让我们一起创建一个 DEV.to CLI...
设置
创建 src/api.mjs
JavaScript 互操作
读取缓存
解析 HTML
格式化数据
查看文章
查看帮助
文章来自Cache
解析文章
阅读文章
显示文章
HTML 到文本
最后的润色
所以你决定直接跳到最后?
关于 CLI 的警告
Hacktoberfest 快乐!
为了 hacktoberfest,我将为 DEV.to 制作一个 CLI...让我们一起做吧!
这是一个跟随式的教程……所以请跟着学习。但如果你觉得自己已经很优秀了,学不到什么新东西,可以直接跳到最后。
如果我跳过某些内容太快而您想要更多解释,请在评论中询问我!
设置
因为是我开车,所以语言由我来选择。我当然会用MojiScript 。
git clone https://github.com/joelnet/mojiscript-starter-app.git devto-cli
cd devto-cli
npm ci
DEV.to 没有 API。所有没有 API 的网站会怎么样?它们会被抓取!
# install axios
npm install --save-prod axios
添加 axios 依赖项index.mjs
import log from 'mojiscript/console/log'
import run from 'mojiscript/core/run'
import axios from 'mojiscript/net/axios'
import main from './main'
const dependencies = {
axios,
log
}
run ({ dependencies, main })
创建 src/api.mjs
创建一个新文件src/api.mjs
来包含我们的抓取 API。我们使用mojiscript/net/axios
,它是 的柯里化版本axios
。
import pipe from 'mojiscript/core/pipe'
const getData = response => response.data
export const getUrl = axios => pipe ([
url => axios.get (url) ({}),
getData
])
export const getDevToHtml = axios => pipe ([
() => getUrl (axios) ('https://dev.to')
])
导入getDevToHtml
main.mjs
import pipe from 'mojiscript/core/pipe'
import { getDevToHtml } from './api'
const main = ({ axios, log }) => pipe ([
getDevToHtml (axios),
log
])
export default main
现在运行代码:
npm start
如果一切顺利,您应该会看到一堆 HTML 充斥着控制台。
JavaScript 互操作
现在我不想在每次调试代码时都用 HTTP 调用来攻击 DEV.to,所以让我们将该输出缓存到文件中。
# this will get you the same version in this tutorial
curl -Lo devto.html https://raw.githubusercontent.com/joelnet/devto-cli/master/devto.html
接下来我将创建一个文件interop/fs.mjs
,它将fs.readFile
被放置在这里。我将它放在一个interop
文件夹中,因为 MojiScript 要求 JavaScript 互操作文件也必须放在这个文件夹中。JavaScript 的编写方式与 MojiScript 不同,有时两者不兼容(除非在互操作目录中)。
为了fs.readFile
与 MojiScript 兼容,我需要先promisify
这样做。
promisify (fs.readFile)
既然已经承诺了,我也需要对其进行柯里化。
export const readFile = curry (2) (promisify (fs.readFile))
我也在处理 UTF8,所以让我们添加一个助手来让生活变得更轻松。
export const readUtf8File = file => readFile (file) ('utf8')
完整内容如下interop/fs.mjs
:
import fs from 'fs'
import curry from 'mojiscript/function/curry'
import { promisify } from 'util'
export const readFile = curry (2) (promisify (fs.readFile))
export const readUtf8File = file => readFile (file) ('utf8')
读取缓存
在 里面src/mocks/axios.mock.mjs
,我将创建。当被调用mockAxios
时,它将返回我们文件的内容。get
import pipe from 'mojiscript/core/pipe'
import { readUtf8File } from '../interop/fs'
const mockAxios = {
get: () => pipe ([
() => readUtf8File ('devto.html'),
data => ({ data })
])
}
export default mockAxios
使用模拟很简单。我只需要更改dependencies
。 中的所有内容都不main.mjs
需要更改!
// don't forget to add the import!
import mockAxios from './mocks/axios.mock'
const dependencies = {
axios: mockAxios,
log
}
现在运行时npm start
不会再有 HTTP 请求了。这很好,因为我可能还要运行npm start
一堆程序才能完成这个任务!
解析 HTML
我喜欢cheerio
用它来解析。我敢肯定那些酷酷的小子们都在用这个。
npm install --save-prod cheerio
创建另一个互操作interop/cheerio.mjs
。
import cheerio from 'cheerio';
import pipe from 'mojiscript/core/pipe';
import map from 'mojiscript/list/map';
export const getElements = selector => pipe ([
cheerio.load,
$ => $ (selector),
$articles => $articles.toArray (),
map (cheerio)
])
注意:调用 cheerio 时toArray
,元素会丢失所有有用的 cheerio 方法。所以我们必须map
cheerio
将其返回到所有元素。
接下来添加getElements
到main
。
import { getElements } from './interop/cheerio'
const main = ({ axios, log }) => pipe ([
getDevToHtml (axios),
getElements ('.single-article:not(.feed-cta)'),
log
])
再次运行npm start
即可查看元素数组。
npm install --save-prod reselect nothis
创建interop/parser.mjs
。我将使用它reselect
从 HTML 中选择我需要的属性。我不会详细讲解它。它基本上就是对元素进行一系列的获取操作。代码很容易阅读,你也可以跳过它,因为它并不重要。
import reselect from 'reselect'
import nothis from 'nothis'
const { createSelector } = reselect
const isTextNode = nothis(({ nodeType }) => nodeType === 3)
const parseUrl = element => `http://dev.to${element.find('a.index-article-link').attr('href')}`
const parseTitle = element => element.find('h3').contents().filter(isTextNode).text().trim()
const parseUserName = element => element.find('.featured-user-name,h4').text().trim().split('・')[0]
const parseTags = element => element.find('.featured-tags a,.tags a').text().substr(1).split('#')
const parseComments = element => element.find('.comments-count .engagement-count-number').text().trim() || '0'
const parseReactions = element => element.find('.reactions-count .engagement-count-number').text().trim() || '0'
export const parseElement = createSelector(
parseUrl,
parseTitle,
parseUserName,
parseTags,
parseComments,
parseReactions,
(url, title, username, tags, comments, reactions) => ({
url,
title,
username,
tags,
comments,
reactions
})
)
添加。parseElement
main
import map from 'mojiscript/list/map'
import { parseElement } from './interop/parser'
const main = ({ axios, log }) => pipe ([
getDevToHtml (axios),
getElements ('.single-article:not(.feed-cta)'),
map (parseElement),
log,
])
现在当你运行时npm start
你应该会看到类似这样的内容:
[
{ url:
'http://dev.to/ccleary00/how-to-find-the-best-open-source-nodejs-projects-to-study-for-leveling-up-your-skills-1c28',
title:
'How to find the best open source Node.js projects to study for leveling up your skills',
username: 'Corey Cleary',
tags: [ 'node', 'javascript', 'hacktoberfest' ],
comments: '0',
reactions: '33' } ]
格式化数据
添加import
,formatPost
并添加formatPost
至main
并更改log
为map (log)
。
import $ from 'mojiscript/string/template'
const formatPost = $`${'title'}
${'url'}\n#${'tags'}
${'username'} ・ 💖 ${'comments'} 💬 ${'reactions'}
`
const main = ({ axios, log }) => pipe ([
getDevToHtml (axios),
getElements ('.single-article:not(.feed-cta)'),
map (parseElement),
map (formatPost),
map (log)
])
再次运行npm start
,您应该会看到一些如下所示的记录:
The Introvert's Guide to Professional Development
http://dev.to/geekgalgroks/the-introverts-guide-to-professional-development-3408
#introvert,tips,development,professional
Jenn ・ 💖 1 💬 50
最后,这开始看起来像某种东西了!
我还将添加一个条件,以便仅在设置时main.mjs
使用。axios
production
NODE_ENV
import ifElse from 'mojiscript/logic/ifElse'
const isProd = env => env === 'production'
const getAxios = () => axios
const getMockAxios = () => mockAxios
const dependencies = {
axios: ifElse (isProd) (getAxios) (getMockAxios) (process.env.NODE_ENV),
log
}
运行它和不运行它production
以确保两者都正常工作。
# dev mode
npm start
# production mode
NODE_ENV=production npm start
查看文章
这份清单很不错,我本来打算就此打住,但如果我也能读到这篇文章,那就太酷了。
我希望能够输入类似以下内容:
devto read 3408
我注意到 URL 末尾有一个我可以使用的 ID:http://dev.to/geekgalgroks/the-introverts-guide-to-professional-development-3408
<-- 就在那里。
因此我将进行修改parser.mjs
以包含一个新的解析器来获取该 ID。
const parseId = createSelector(
parseUrl,
url => url.match(/-(\w+)$/, 'i')[1]
)
然后只需按照模式parseId
进入parseElement
即可。
现在 CLI 将有两个分支,一个用于显示 feed,另一个用于显示文章。因此,让我们将 feed 逻辑从main.mjs
和 拆分到 中src/showFeed.mjs
。
import pipe from 'mojiscript/core/pipe'
import map from 'mojiscript/list/map'
import $ from 'mojiscript/string/template'
import { getDevToHtml } from './api'
import { getElements } from './interop/cheerio'
import { parseElement } from './interop/parser'
const formatPost = $`${'title'}
${'url'}\n#${'tags'}
${'username'} ・ 💖 ${'comments'} 💬 ${'reactions'}
`
export const shouldShowFeed = args => args.length < 1
export const showFeed = ({ axios, log }) => pipe ([
getDevToHtml (axios),
getElements ('.single-article:not(.feed-cta)'),
map (parseElement),
map (formatPost),
map (log)
])
接下来,我要绕一圈cond
。CLIshowFeed
中可能会有更多分支(也许有帮助?),但目前我们只有 1 条路径。
main.mjs
现在看起来应该是这样的。
import pipe from 'mojiscript/core/pipe'
import cond from 'mojiscript/logic/cond'
import { showFeed } from './showFeed'
const main = dependencies => pipe ([
cond ([
[ () => true, showFeed (dependencies) ]
])
])
export default main
我们需要访问节点的参数。因此,请进行以下更改main.mjs
。我之所以slice
对其进行更改,是因为前两个参数是垃圾参数,我不需要它们。
// add this line
const state = process.argv.slice (2)
// add state to run
run ({ dependencies, state, main })
好的,在我们真正查看文章之前,还有很多工作要做。所以我们先添加帮助。这很简单。
查看帮助
创造src/showHelp.mjs
。
import pipe from 'mojiscript/core/pipe'
const helpText = `usage: devto [<command>] [<args>]
<default>
Show article feed
read <id> Read an article
`
export const showHelp = ({ log }) => pipe ([
() => log (helpText)
])
现在我们可以简化main.mjs
并将新案例添加到cond
。
import pipe from 'mojiscript/core/pipe'
import cond from 'mojiscript/logic/cond'
import { shouldShowFeed, showFeed } from './showFeed'
import { showHelp } from './showHelp'
const main = dependencies => pipe ([
cond ([
[ shouldShowFeed, showFeed (dependencies) ],
[ () => true, showHelp (dependencies) ]
])
])
export default main
现在如果我们运行npm start -- help
,我们应该会看到帮助:
usage: devto [<command>] [<args>]
<default> Show article feed
read <id> Read an article
如果我们跑步的话npm start
我们仍然可以看到我们的反馈!
文章来自Cache
就像我从缓存中读取主信息一样,我也想从缓存中读取文章。
curl -Lo article.html https://raw.githubusercontent.com/joelnet/devto-cli/master/article.html
修改一下axios.mock.mjs
也读一下文章。
import pipe from 'mojiscript/core/pipe'
import ifElse from 'mojiscript/logic/ifElse'
import { readUtf8File } from '../interop/fs'
const feedOrArticle = ifElse (url => url === 'https://dev.to') (() => 'devto.html') (() => 'article.html')
const mockAxios = {
get: url => pipe ([
() => feedOrArticle (url),
readUtf8File,
data => ({ data })
])
}
export default mockAxios
解析文章
解析文章 HTML 要容易得多,因为我打算将整个article-body
区块格式化为文本。所以我只需要标题和正文。
创造interop/articleParser.mjs
。
import reselect from 'reselect'
const { createSelector } = reselect
const parseTitle = $ => $('h1').first().text().trim()
const parseBody = $ => $('#article-body').html()
export const parseArticle = createSelector(
parseTitle,
parseBody,
(title, body) => ({
title,
body
})
)
阅读文章
因为没有状态,当我发出命令时,CLI 不知道要拉取哪个 URL read
。因为我比较懒,所以我会再次查询 feed,然后从 feed 中拉取 URL。
因此我将跳回去showFeed.mjs
并展示该功能。
我只是从中提取函数showFeed
并将它们放入其中getArticles
。我没有在这里添加任何新代码。
export const getArticles = axios => pipe ([
getDevToHtml (axios),
getElements ('.single-article:not(.feed-cta)'),
map (parseElement)
])
export const showFeed = ({ axios, log }) => pipe ([
getArticles (axios),
map (formatPost),
map (log)
])
显示文章
现在我想编写一个类似下面的函数,但会报错id
:未定义。id
是 的参数pipe
,但在这里无法访问。 的输入filter
是文章数组,而不是id
。
const getArticle = ({ axios }) => pipe ([
getArticles (axios),
filter (article => article.id === id), // 'id' is not defined
articles => articles[0]
])
但有一个技巧。使用W 组合器,我可以创建一个闭包,这样它就id
暴露出来了。
const getArticle = ({ axios }) => W (id => pipe ([
getArticles (axios),
filter (article => article.id === id),
articles => articles[0]
]))
把这个代码块和上面的代码块比较一下,区别不大,只是添加了W (id =>
一个结束符)
。W 组合器真是个好工具。更多关于函数组合器的内容,以后再写一篇文章吧 :) 现在,我们继续。
总体src/showArticle.mjs
看起来应该是这样的:
import W from 'mojiscript/combinators/W'
import pipe from 'mojiscript/core/pipe'
import filter from 'mojiscript/list/filter'
import { getArticles } from './showFeed'
export const shouldShowArticle = args => args.length === 2 && args[0] === 'read'
const getArticle = ({ axios }) => W (id => pipe ([
getArticles (axios),
filter (article => article.id === id),
articles => articles[0]
]))
export const showArticle = ({ axios, log }) => pipe ([
getArticle ({ axios }),
log
])
修改main.mjs
以cond
包含新功能:
import { shouldShowArticle, showArticle } from './showArticle'
const main = dependencies => pipe ([
cond ([
[ shouldShowArticle, args => showArticle (dependencies) (args[1]) ],
[ shouldShowFeed, showFeed (dependencies) ],
[ () => true, showHelp (dependencies) ]
])
])
运行npm run start -- 1i0a
(替换 id)你应该会看到类似这样的内容:
{ id: '1i0a',
url:
'http://dev.to/ppshobi/-email-sending-in-django-2-part--1--1i0a',
title: 'Email Sending in Django 2, Part -1',
username: 'Shobi',
tags: [ 'django', 'emails', 'consoleemailbackend' ],
comments: '0',
reactions: '13' }
HTML 到文本
我发现了一个很棒的 npm 包,看起来它可以帮我处理这个问题。
npm install --save-prod html-to-text
我们已经完成了大部分基础工作,因此,要发出 HTTP 请求、解析 HTML 并将其格式化为文本,就这么简单。打开showArticle.mjs
。
const getArticleTextFromUrl = axios => pipe ([
({ url }) => getUrl (axios) (url),
cheerio.load,
parseArticle,
article => `${article.title}\n\n${htmlToText.fromString (article.body)}`
])
我还想为id
未找到的情况创建一个视图。
const showArticleNotFound = $`Article ${0} not found.\n`
我还将创建一个isArticleFound
条件以使代码更具可读性。
const isArticleFound = article => article != null
我将使用相同的 W Combinator 技术来创建闭包并公开id
和修改showArticle
。
export const showArticle = ({ axios, log }) => W (id => pipe ([
getArticle ({ axios }),
ifElse (isArticleFound) (getArticleTextFromUrl (axios)) (() => showArticleNotFound (id)),
log
]))
总体showArticle.mjs
看起来是这样的:
import cheerio from 'cheerio'
import htmlToText from 'html-to-text'
import W from 'mojiscript/combinators/W'
import pipe from 'mojiscript/core/pipe'
import filter from 'mojiscript/list/filter'
import ifElse from 'mojiscript/logic/ifElse'
import $ from 'mojiscript/string/template'
import { getUrl } from './api'
import { parseArticle } from './interop/articleParser'
import { getArticles } from './showFeed'
const isArticleFound = article => article != null
const showArticleNotFound = $`Article ${0} not found.\n`
const getArticleTextFromUrl = axios => pipe ([
({ url }) => getUrl (axios) (url),
cheerio.load,
parseArticle,
article => `${article.title}\n\n${htmlToText.fromString (article.body)}`
])
export const shouldShowArticle = args => args.length === 2 && args[0] === 'read'
const getArticle = ({ axios }) => W (id => pipe ([
getArticles (axios),
filter (article => article.id === id),
articles => articles[0]
]))
export const showArticle = ({ axios, log }) => W (id => pipe ([
getArticle ({ axios }),
ifElse (isArticleFound) (getArticleTextFromUrl (axios)) (() => showArticleNotFound (id)),
log
]))
npm start -- read 1i0a
再次运行,您就应该看到该文章!
最后的润色
我想id
在 feed 中将其说得更清楚一些。
const formatPost = $`${'id'}・${'title'}
${'url'}\n#${'tags'}
${'username'} ・ 💖 ${'comments'} 💬 ${'reactions'}
`
将其添加到package.json
,我将命名该命令devto
。
"bin": {
"devto": "./src/index.mjs"
}
在src/index.mjs
顶部添加这个神秘的魔法:
#!/bin/sh
':' //# comment; exec /usr/bin/env NODE_ENV=production node --experimental-modules --no-warnings "$0" "$@"
运行此命令来创建该命令的全局链接。
npm link
如果一切顺利,您现在应该能够运行以下命令:
# get the feed
devto
# read the article
devto read <id>
所以你决定直接跳到最后?
你可以把马牵到水边……或者做其他事情。
要赶上我们其他人的步伐,请按照以下步骤操作:
# clone the repo
git clone https://github.com/joelnet/devto-cli
cd devto-cli
# install
npm ci
npm run build
npm link
# run
devto
关于 CLI 的警告
抓取网站数据是个坏主意。网站一旦发生变化(这种情况肯定会发生),你的代码就会崩溃。
这只是#hacktoberfest的一个趣味演示,并非可维护的项目。如果您发现任何 Bug,请提交 PR 并附上 Bug 报告进行修复。我本人不负责维护此项目。
如果这是一个真实的项目,那么有些事情会很酷:
- 登录,这样您就可以阅读您的订阅源。
- 更多互动、评论、点赞、标签。或许可以发篇文章?
Hacktoberfest 快乐!
对于那些读完整篇文章的朋友们,感谢你们抽出时间。我知道这篇文章很长。我希望它很有趣,希望你们学到了一些东西,最重要的是,希望你们玩得开心。
对于那些实际上一步一步跟随并自己创建 CLI 的人来说:你们使我变得完整💖。
请在评论或推特中告诉我您学到了什么、发现了什么有趣的东西或任何其他评论或批评。
我的文章非常注重功能性 JavaScript,如果您需要更多,请在这里关注我,或在 Twitter 上关注我@joelnet!
更多文章
问我一些关于函数式编程的愚蠢问题
让我们来讨论一下 JavaScript 的自动生成文档工具