如何构建自己的免费无服务器评论框

by Shaun Persad

通过Shaun Persad

如何构建自己的免费无服务器评论框 (How you can build your own free, serverless comment box)

Contentful’s flexible content modeling goes far beyond blog posts. Here’s how you can leverage Contentful and Netlify to create a nested commenting system that’s easy to moderate and deploy.

Contentful灵活的内容建模远远超出了博客文章。 这是如何利用Contentful和Netlify创建易于审核和部署的嵌套评论系统。

动机 (The motivation)

I find most commenting systems out there to be…lacking. Disqus can often be slow to render, and their user tracking behavior doesn’t have the best reputation. Meanwhile, Facebook’s comments plugin is quite nice, but of course is limited to Facebook users.

我发现那里的大多数评论系统都……不足。 Disqus的呈现速度通常很慢,并且它们的用户跟踪行为也不是最好的声誉。 同时,Facebook的评论插件相当不错,但当然仅限Facebook用户。

What I really wanted was the native speed and approach to nested commenting and moderation taken by sites like Hacker News and Indie Hackers, but I needed a solution that would be portable to multiple projects.

我真正想要的是像Hacker News和Indie Hackers这样的网站采用固有的速度和方法来进行嵌套评论和主持人,但是我需要一个可移植到多个项目的解决方案。

There just didn’t seem to be a great fit out there, so I decided to build my own, with my wish list of features:

似乎没有什么合适的选择,所以我决定建立我自己的功能清单,并提供以下功能:

  • Free

    自由

  • Low barrier to entry — minimal steps required to submit a comment

    进入门槛低 -提交评论所需的最少步骤

  • Low maintenance — serverless, to not worry about hosting or scaling

    低维护 —无服务器,不用担心托管或扩展

  • Easy moderation — use a dashboard to perform CRUD on comments

    轻松审核 -使用仪表板对评论执行CRUD

  • Peformant — super-fast to appear on the page

    Peformant-超快速出现在页面上

  • Flexible — users should be able to log in via multiple platforms

    灵活 -用户应该能够通过多个平台登录

  • Powerful — comments should have smart formatting features

    强大 -注释应具有智能格式设置功能

  • High comment quality — users can upvote and downvote comments

    高评论质量 -用户可以对评论进行投票和降级

  • Subscriptions — users can receive notifications when their comments are replied to

    订阅 -回复他们的评论时,用户可以收到通知

Over the course of this series, we will build out a commenting system that incorporates each of the above aspects.

在本系列的整个过程中,我们将构建一个包含上述各个方面的评论系统。

计划 (The plan)

Our stack will initially include:

我们的堆栈最初将包括:

We will create a React component to serve as our comment box, and supply it with the ability to make an API call to Contentful to fetch comments as necessary. It will also be able to make an API call to our Lambda function to post a comment to Contentful.

我们将创建一个React组件作为我们的注释框,并为它提供对Contentful进行API调用以根据需要获取注释的功能。 它还将能够对我们的Lambda函数进行API调用,以对Contentful发表评论。

Project-wise, our Lambda function will live along-side our front-end code. Both the front-end and back-end will be set up to be continuously deployed via Netlify.

在项目方面,我们的Lambda函数将与我们的前端代码一起使用。 前端和后端都将设置为通过Netlify进行连续部署。

By the way, the above stack is all free! Well, mostly. Unless you’re going to be doing over 10,000 comments, it’s free. Also, I’m not affiliated with any of these companies…I just love their stuff :)

顺便说一下,以上堆栈都是免费的! 好吧,主要是。 除非您要进行超过10,000条评论,否则它是免费的。 另外,我与这些公司都不隶属...我只是喜欢他们的东西:)

在10秒内满意 (Contentful in 10 seconds)

If you’re not already familiar with Contentful and how it works, it’s a “headless” (API-driven) CMS. You’re able to model your content with different fields and field types, and then you create content based on those models. You can build your front-end however you like, and query for your data using their API. It’s super flexible, and their dashboard is quite nice to use. It’s basically the best thing to happen to CMS’s since, well, ever?

如果您还不熟悉Contentful及其工作原理 ,那么它就是“无头”(由API驱动)的CMS。 您可以使用不同的字段和字段类型对内容建模,然后根据这些模型创建内容。 您可以根据需要构建自己的前端,并使用其API查询数据。 它非常灵活,他们的仪表板非常好用。 从根本上说,这是CMS发生的最好的事情了?

I was already using Contentful for my blog posts, so I wondered, could it be viable to host comments as well? I’m happy to report that the answer is yes! However, a few of the items on my wishlist don’t quite work out using just Contentful. But don’t worry, we’ll get there…in the subsequent posts of this series.

我已经在博客文章中使用了Contentful,所以我想知道是否也可以托管评论? 我很高兴地报告答案是肯定的! 但是,我的愿望清单上的某些项目使用Contentful不太可行。 但是请放心,我们将会到达……在本系列的后续文章中。

We’ll be using Contentful because:

我们将使用Contentful是因为:

  • flexible data modeling

    灵活的数据建模
  • convenient API

    方便的API
  • moderation via a dashboard

    通过仪表板进行审核
  • you may already be using it for your website/blog that needs comments

    您可能已经将其用于需要评论的网站/博客

在10秒内完成Netlify (Netlify in 10 seconds)

I think Netlify has by far the most enjoyable deployment experience for front-end apps. It links to your GitHub repo and sets you up to continuously deploy a static site to CDN-backed hosting. They also have Netlify Functions, which let you deploy to AWS Lambda without any of the pain of messing around in AWS.

我认为Netlify迄今为止具有最令人满意的前端应用程序部署体验。 它链接到您的GitHub存储库,并设置为将静态站点连续部署到CDN支持的托管。 它们还具有Netlify Functions ,可让您部署到AWS Lambda,而不会在AWS上乱成一团

You can get started at their docs, but honestly, their dashboard is so easy to use and understand, I recommend just logging in and poking around.

您可以从他们的文档开始,但老实说,他们的仪表板非常易于使用和理解,我建议您先登录并四处浏览。

We’ll be using Netlify because:

我们将使用Netlify是因为:

  • painless AWS Lambda integration

    无痛的AWS Lambda集成
  • you may already be using it for your website/blog that needs comments

    您可能已经将其用于需要评论的网站/博客
  • If you’re not already using it, you can still deploy the Lambda functions we create to AWS itself

    如果尚未使用它,您仍然可以将我们创建的Lambda函数部署到AWS本身

等等,没有“ 10秒钟后React”吗? (Wait, no “React in 10 seconds”?)

I don’t know if 10 seconds is enough to do React justice. If you haven’t yet learned it, you should! But skip the Redux and Flux stuff. Chances are you don’t need any of that (but that’s another topic for another time).

我不知道10秒钟是否足以完成React正义。 如果您还没有学过,那应该! 但是跳过Redux和Flux的内容。 您可能不需要其中任何一个(但这是另一回事了)。

Contentful中的内容建模 (Content modeling in Contentful)

Now down to business.

现在正事。

There are two different approaches we could take regarding how we handle our users: authless and logged-in commenting:

关于我们如何处理用户,我们可以采取两种不同的方法: authless登录评论:

  • Authless — anyone can leave a comment simply by supplying their name

    无需身份验证-任何人都可以通过提供姓名来发表评论
  • Logged-in — only users who are authenticated in some auth system can comment

    登录-只有在某些身份验证系统中经过身份验证的用户才能评论

I prefer logged-in commenting, because in my opinion, the conversations tend to be more civilized. Plus, you tend to avoid spam altogether. On the flipside, the barrier to create a comment is slightly higher.

我更喜欢登录评论,因为我认为对话通常更加文明。 另外,您倾向于完全避免垃圾邮件。 另一方面,创建评论的障碍稍高。

However, we will start off with authless commenting, because it’s simpler to implement. Once we get our feet wet, we’ll jump into logged-in commenting in Part 2.

但是,我们将从无认证注释开始,因为它更易于实现。 一旦弄湿了,我们将在第2部分中进入登录评论。

Regardless, we’re going to first need to create a content model to represent our comments.

无论如何,我们首先需要创建一个内容模型来表示我们的评论。

For both authless and logged-in approaches, our Comment content model will remain mostly the same as well, though there will be some later changes to the Author field, as noted below.

对于无认证方法和登录方法,我们的评论内容模型也将基本保持不变,尽管稍后会在“ 作者”字段中进行一些更改,如下所述。

评论内容模型 (The Comment content model)

This is the model at the heart of our commenting system. Comments should have four fields:

这是我们评论系统的核心模型。 注释应包含四个字段:

Body

身体

  • The actual body of the comment

    评论的实际内容
  • Mark this one as the entry title

    将此标记为条目标题
  • Feel free to also set a maximum and/or minimum value on its length

    还可以设置其长度的最大值和/或最小值

Author

作者

  • A unique identifier representing the user who posted this comment.

    代表发布此评论的用户的唯一标识符。
  • For authless commenting, you’d use short text and fill in the author’s name in this field

    对于无认证评论,您将使用短文本并在此字段中填写作者的姓名
  • For logged-in commenting, this field will become a reference to the upcoming CommentAuthor model

    对于已登录的评论,此字段将成为对即将到来的CommentAuthor模型的引用

Subject

学科

  • The unique ID of the blog post (or equivalent) that these comments belong to

    这些评论所属的博客文章的唯一ID(或等效名称)
  • It can also be the URL of the page

    也可以是页面的URL
  • For maximum flexibility, I chose not to assume that you’re storing your blog posts in Contentful, or else this would be a reference field instead of short text

    为了获得最大的灵活性,我选择不假定您将博客文章存储在Contentful中,否则这将是一个参考字段,而不是短文本

ParentComment

家长评论

  • If this comment is a reply to another comment, we’ll reference that comment here

    如果此评论是对其他评论的回复,我们将在此处引用该评论
  • This field is what enables us to create nested comments

    该字段使我们能够创建嵌套注释

实施无认证评论 (Implementing authless commenting)

For this implementation, we want the user to enter their name before they are able to post a comment. I recommend doing an initial read-through of the following steps, and then check out the final demo project at the end to see how it all comes together.

对于此实现,我们希望用户在发表评论之前输入他们的姓名。 我建议对以下步骤进行初步阅读,然后在最后查看最终的演示项目,以了解所有内容如何组合在一起。

前端 (Front-end)

Now that our Comment model is done, it’s time to create our comment box. The good news is that I’ve already made a generic “comment box” React component. It’s designed as a low-order component, where you wrap a higher-order component around it to handle fetching and creating Contentful comments, and other application-specific business logic.

现在,我们的评论模型已经完成,是时候创建我们的评论框了。 好消息是我已经制作了一个通用的“注释框” React组件。 它被设计为一个低阶组件,你环绕它高阶组件来处理获取和创造Contentful意见,以及其他应用程序特定的业务逻辑。

You can install it and the other required packages via npm:

您可以通过npm安装它和其他必需的软件包:

npm install react-commentbox contentful contentful-management --save

The GitHub repo has a list of every prop you can pass to it, but minimally, we’ll be implementing and passing these:

GitHub存储库列出了您可以传递给它的每个道具,但是最少,我们将实现并传递这些道具:

  • getComments: a function that returns a promise that resolves to an array of comments, ordered from oldest to newest

    getComments :一个函数,该函数返回一个promise,该promise解析为从最旧到最新的注释数组

  • normalizeComment: a function that maps your array of comments to objects that the component understands

    normalizeComment :将注释数组映射到组件可以理解的对象的函数

  • comment: a function that makes an API call to create a comment, and returns a promise

    comment :进行API调用以创建评论并返回promise的函数

  • disabled: set to true when commenting should be disabled

    disabled :应禁用评论时设置为true

  • disabledComponent: the component to show when commenting is disabled

    disabledComponent :禁用评论时显示的组件

Let’s create our higher-level component:

让我们创建更高级别的组件:

import React from 'react';import CommentBox from 'react-commentbox';
class MyCommentBox extends React.Component {
state = { authorName: '', authorNameIsSet: false };
onChangeAuthorName = (e) => this.setState({         authorName: e.currentTarget.value     });
onSubmitAuthorName = (e) => {
e.preventDefault();        this.setState({ authorNameIsSet: true });    };}

Notice that the component is in charge of setting the author’s name.

注意,该组件负责设置作者的姓名。

By the way, we’re using the transform-class-properties Babel plugin to avoid tedious constructor setup and function bindings. You don’t need to use it, but it’s quite handy.

顺便说一句,我们正在使用transform-class-properties Babel插件来避免繁琐的构造函数设置和函数绑定。 您不需要使用它,但是非常方便。

Now we need to implement the business-logic props that react-commentbox needs.

现在,我们需要实现react-commentbox所需的业务逻辑道具。

We’ll start off by fetching comments from Contentful, and normalizing them:

我们将从获取Contentful的注释并对其进行规范化开始:

// fetch our comments from ContentfulgetComments = () => {
return this.props.contentfulClient.getEntries({        'order': 'sys.createdAt',        'content_type': 'comment',        'fields.subject': this.props.subjectId,    }).then( response => {
return response.items;
}).catch(console.error);};
// turn Contentful entries to objects that react-commentbox expects.normalizeComment = (comment) => {
const { id, createdAt } = comment.sys;    const { body, author, parentComment } = comment.fields;
return {        id,        bodyDisplay: body,        userNameDisplay: author,        timestampDisplay: createdAt.split('T')[0],        belongsToAuthor: false,        parentCommentId: parentComment ? parentComment.sys.id : null    };};

Next, we need to make the API call to create comments:

接下来,我们需要进行API调用以创建注释:

// make an API call to post a commentcomment = (body, parentCommentId = null) => {
return this.props.postData('/create-comment', {        body,        parentCommentId,        authorName: this.state.authorName,        subjectId: this.props.subjectId    });};

We also need to ask the user for their name before they can comment:

我们还需要先询问用户的姓名,然后他们才能发表评论:

// will be shown when the comment box is initially disableddisabledComponent = (props) => {
return (        <form             className="author-name"             onSubmit{ this.onSubmitAuthorName }        >            <input                type="text"                placeholder="Enter your name to post a comment"                value={ this.state.authorName }                onChange={ this.onChangeAuthorName }            />            <button type="submit">Submit</button>        </form>    );};

Then, bring it all together in render, by passing the appropriate props to react-commentbox:

然后,通过将适当的道具传递给react-commentbox ,将其全部合并到render

render() {
return (        <div>            <h4>Comments</h4>            <CommentBox                disabled={ !this.state.authorNameIsSet }                getComments={ this.getComments }                normalizeComment={ this.normalizeComment }                comment={ this.comment }                disabledComponent={ this.disabledComponent }            />        </div>    );};

We’ve also set the disabled prop to true while the author's name is not set. This disables the textarea, and shows the disabledComponent form we made to get the author's name.

在未设置作者姓名的情况下,我们还将disabled prop设置为true 。 这将禁用textarea ,并显示我们为获得作者姓名而创建的disabledComponent形式。

You can view the complete component here.

您可以在此处查看完整的组件。

You may have noticed that our newly created MyCommentBox also expects a few props itself: subjectId, postData, and contentfulClient.

您可能已经注意到,我们的新创建的MyCommentBox还预计一些道具本身: subjectIdpostDatacontentfulClient

The subjectId is simply some unique ID or URL of the blog post (or equivalent entity) that these comments are for.

subjectId只是这些评论所针对的博客文章(或等效实体)的唯一ID或URL。

postData is a function that makes POST ajax calls. Using fetch, it could look like this:

postData是进行POST ajax调用的函数。 使用fetch ,它看起来可能像这样:

function postData(url, data) {
return fetch(`.netlify/functions${url}`, {        body: JSON.stringify(data),        headers: {            'content-type': 'application/json'        },        method: 'POST',        mode: 'cors' // if your endpoints are on a different domain    }).then(response => response.json());}

contentfulClient is an instance of the client you get when using the contentful npm package (so make sure you've installed it):

contentfulClient是使用有内容的 npm软件包时获得的客户端的实例(因此请确保已安装它):

import { createClient } from 'contentful';const contentfulClient = createClient({    space: 'my-space-id',    accessToken: 'my-access-token'});

You can get your space ID in the Contentful dashboard under “Space settings” > “General settings”.

您可以在内容丰富的信息中心的“空间设置”>“常规设置”下获取空间ID。

You can get your access token from “Space settings” > “API keys” > “Content delivery/preview tokens” > “Add API Key”.

您可以从“空间设置”>“ API密钥”>“内容交付/预览令牌”>“添加API密钥”获取访问令牌。

You can then pass in your props when creating MyCommentBox, as shown here.

然后,您可以通过在你的道具制作时MyCommentBox ,如图所示这里

后端 (Back-end)

We will implement our /create-comment endpoint as an AWS Lambda function.

我们将/create-comment端点实现为AWS Lambda函数。

先决条件 (Prerequisites)

To be able to build, preview, and eventually deploy these functions, we’re going to use the handy netlify-lambda npm package. It lets you write your Lambda functions as regular ES6 functions in a particular source directory, and then it builds them in a Lambda-friendly way and puts them in a destination directory, ready for deployment. Even better, it also allows us to preview these functions by deploying them locally.

为了能够构建,预览并最终部署这些功能,我们将使用方便的netlify-lambda npm软件包。 它允许您在特定的源目录中将Lambda函数编写为常规的ES6函数,然后以对Lambda友好的方式构建它们,并将它们放置在目标目录中,以备部署。 更好的是,它还允许我们通过在本地部署这些功能来预览这些功能。

So, you’ll need to create a particular source directory to store your function (e.g. src/lambda), then create a netlify.toml file in your root directory. Minimally, that file should look like this:

因此,您需要创建一个特定的源目录来存储您的函数(例如src/lambda ),然后在根目录中创建一个netlify.toml文件。 至少,该文件应如下所示:

[build] Functions = "lambda"

The above tells netlify-lambda which directory to put your built functions, meaning it will build the functions in src/lambda and store them in ./lambda. Also, when it comes time to deploy, Netlify will look in the ./lambda directory to deploy to AWS.

上面的内容告诉netlify-lambda将构建的函数放在哪个目录中,这意味着它将在src/lambda构建函数并将它们存储在./lambda 。 另外,当需要进行部署时,Netlify将在./lambda目录中查找以部署到AWS。

To run your Lambda functions locally, use the following command:

要在本地运行Lambda函数,请使用以下命令:

netlify-lambda serve <source directory>

This will allow you to run your functions on http://localhost:9000/{function-name}.

这将允许您在http://localhost:9000/{function-name}上运行函数

This is the default behavior, but it does not quite match what will happen in production, because it’s running our functions on a different domain from our front-end. In production, our functions will be available on the same domain as our front-end, via the URL {domain}/.netlify/functions/{function-name}.

这是默认行为,但与生产中发生的情况完全不匹配,因为它在我们前端的不同域上运行我们的功能。 在生产中,我们的功能将通过URL {domain}/.netlify/functions/{function-name}在与前端相同的域中提供。

To replicate this behavior locally, we need to proxy front-end calls from /.netlify/functions/{function-name} to http://localhost:9000/{function-name}.

要在本地复制此行为,我们需要将前端调用从/.netlify/functions/{function-name}代理到http://localhost:9000/{function-name}

Accomplishing this differs based on your project setup. I will cover two popular setups:

完成此操作因您的项目设置而异。 我将介绍两种流行的设置:

For create-react-app projects, add the following to your package.json:

对于create-react-app项目,将以下内容添加到package.json

"proxy": {        "/.netlify/functions": {        "target": "http://localhost:9000",        "pathRewrite": {            "^/\\.netlify/functions": ""        }    }}

For Gatsby.js projects, add the following to your gatsby-config.js:

对于Gatsby.js项目,将以下内容添加到您的gatsby-config.js

const proxy = require('http-proxy-middleware');...developMiddleware: app => {    app.use(        '/.netlify/functions/',        proxy({            target: 'http://lambda:9000',            pathRewrite: {                '/.netlify/functions/': '',            }        })    );},

For most other projects, you can leverage webpack’s dev server, which has proxy support.

对于大多数其他项目,您可以利用webpack的具有代理支持的dev服务器。

编写我们的功能 (Writing our function)

Before we get to writing Lambda-specific code, we will first create a generic function to handle most of our logic. This way, our code remains portable beyond Lambda.

在编写特定于Lambda的代码之前,我们将首先创建一个通用函数来处理大多数逻辑。 这样,我们的代码可移植到Lambda之外。

Let’s create a createComment function:

让我们创建一个createComment函数:

const contentful = require('contentful-management');const client = contentful.createClient({    accessToken: process.env.CONTENTFUL_CONTENT_MANAGEMENT_ACCESS_TOKEN});
module.exports = function createComment(    body,     authorName,     subjectId,     parentCommentId = null) {
return client.getSpace('my-space-id')        .then(space => space.getEnvironment('master'))        .then(environment => environment.createEntry('comment', {            fields: {                body: {                    'en-US': body                },                author: {                    'en-US': authorName                },                subject: {                    'en-US': subjectId                },                parentComment: {                    'en-US': {                        sys: {                            type: 'Link',                            linkType: 'Entry',                            id: parentCommentId                        }                    }                }            }        }))        .then(entry => entry.publish());};

You can put the above function someplace like a utils directory. It uses the contentful-management npm package to create and publish a new comment entry, and returns a promise. Notice we've specified our management API key as an environment variable. You definitely do not want to hard-code that one. When deploying to Netlify or anywhere else, be sure to check that your environment variables are set.

您可以将上述函数放在utils目录中。 它使用contentful-management npm包来创建和发布新的评论条目,并返回承诺。 请注意,我们已将管理API密钥指定为环境变量。 您绝对不想硬编码那个。 部署到Netlify或其他任何地方时,请确保检查是否已设置环境变量。

You can get your management access token from the Contentful dashboard at “Space settings” > “API keys” > “Content management tokens” > “Generate personal token”.

您可以从“内容”仪表板的“空间设置”>“ API密钥”>“内容管理令牌”>“生成个人令牌”获取管理访问令牌。

Now, let’s create our Lambda-specific function:

现在,让我们创建特定于Lambda的函数:

const createComment = require('../utils/createComment');
exports.handler = function (event, context, callback) {
const { body, authorName, subjectId, parentCommentId } = JSON.parse(event.body);
createComment(body, authorName, subjectId, parentCommentId)        .then(entry => callback(null, {            headers: {                'Content-Type': 'application/json'            },            statusCode: 200,            body: JSON.stringify({ message: 'OK' })        }))        .catch(callback);};

Put this function in your Lambda source directory, and name the file with the path you’d want the URL to be, e.g. create-comment.js . This will make your function available at the URL /.netlify/functions/create-comment.

将此函数放在Lambda源目录中,并用您想要URL的路径命名文件,例如create-comment.js 。 这将使您的函数可以在URL /.netlify/functions/create-comment

大图 (The big picture)

To illustrate our complete front-end and back-end setup thus far, I’ve created a create-react-app project that functions as a readily-deployable, fully-functional example.

为了说明到目前为止我们完整的前端和后端设置,我创建了一个create-react-app项目 ,该项目充当易于部署且功能齐全的示例。

Notice that in the example project’s netlify.toml file, there’s a few more lines that you should add to your own file. Command tells Netlify what commands to run to build the project. Publish tells Netlify where to find the static assets ready for deployment once the build is complete. You can read more about this file in Netlify's documentation.

请注意,在示例项目的netlify.toml文件中,应在自己的文件中添加几行。 Command告诉Netlify运行哪些命令来构建项目。 Publish告诉Netlify,一旦构建完成,就可以在哪里找到准备部署的静态资产。 您可以在Netlify的文档中阅读有关此文件的更多信息。

The example project is also easily cloneable and deployable to your own Netlify account via the convenient deploy button in the README.

通过自述文件中的便捷部署按钮,示例项目也可以轻松克隆并部署到您自己的Netlify帐户。

If you’ve been implementing this in your own project instead, head over to the Netlify dashboard and follow their straightforward instructions to set up your repo to deploy.

如果您一直在自己的项目中实施此操作,请转到Netlify仪表板,并按照其简单的说明设置要部署的存储库。

Once it’s up and running, you’ll be able to start commenting like a boss.

一旦启动并运行,您就可以像老板一样发表评论。

(Note: this is just a screenshot, so don’t try clicking on it ^_^)

(注意:这只是一个屏幕截图,因此请勿尝试单击它^ _ ^)

直到下一次 (Until next time)

In Part 2, we’ll cover implementing logged-in commenting, as well as giving our comment box some super-cool text formatting functionality.

在第2部分中,我们将介绍如何实现登录评论,以及为评论框提供一些超酷的文本格式设置功能。

Thanks for reading! — Shaun

谢谢阅读! —肖恩

Originally published at shaunasaservice.com.

最初发布在shaunasaservice.com上

翻译自: https://www.freecodecamp.org/news/how-you-can-build-your-own-free-serverless-comment-box-dc9d4f366d12/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值