react.js做小程序_使用用户登录和身份验证构建React.js应用程序

react.js做小程序

真实的用户身份验证

本文最初发表在Stormpath上 感谢您支持使SitePoint成为可能的合作伙伴。

React(有时称为React.js)是一种构建Web UI的绝妙方式。 Stormpath React SDK通过路由和组件扩展了React和React Router,使您能够使用Stormpath解决常见的用户管理任务,例如身份验证授权

最近,React得到了很多关注,并且很容易理解为什么。 React使您可以将复杂的UI变成可以轻松组合在一起的简单且可重用的组件。

这篇文章将向您展示如何使用Stormpath React SDK从零开始构建React应用程序,并添加允许人们注册,登录甚至查看自己的用户个人资料的功能。

让我们开始吧!

React + Express.js应用程序堆栈

由于我们是从头开始构建应用程序,因此我们将使用ES6JSX编写尽可能少的代码,以及用于用户功能的Stormpath React SDK。

为了让您对我们将要使用的内容有一个很好的了解:

  • React –允许我们编写简单而强大的UI。
  • ReactRouter –在我们的React应用程序中组织URL导航。
  • ES6 – JavaScript的下一版本。 允许我们编写真正JavaScript类。
  • JSX –允许我们将HTML放置在JavaScript中而无需连接字符串。
  • Stormpath –允许我们存储和验证用户,而无需为其创建自己的后端。
  • Stormpath React SDK –只需很少的工作即可将注册表单,登录页面和身份验证集成到我们的React应用程序中。
  • Express –允许我们提供HTML和JavaScript文件。
  • Express Stormpath –允许我们通过Express来提供Stormpath的API。
  • Webpack –允许我们将所有JavaScript文件打包到一个捆绑包中。
  • Babel –允许我们将ES6和JSX转换为ES5。
  • Bootstrap –因为我们希望事情变得漂亮。

设置我们的React + Express.js项目

首先创建一个新的项目目录和一个package.json文件。

$ mkdir my-react-app
$ cd my-react-app
$ npm init --yes

现在安装Express,Express的Stormpath模块和Body Parser:

$ npm install --save express express-stormpath body-parser

我们需要一个服务器来承载我们的应用程序,因此创建一个名为server.js的新文件,并将下面的代码放入其中:

var express = require('express');
var stormpath = require('express-stormpath');
var bodyParser = require('body-parser');

var app = express();

app.use(stormpath.init(app, {
  web: {
    produces: ['application/json']
  }
}));

app.on('stormpath.ready', function () {
  app.listen(3000, 'localhost', function (err) {
    if (err) {
      return console.error(err);
    }
    console.log('Listening at http://localhost:3000');
  });
});

太棒了 现在,我们可以通过使用以下代码创建一个名为stormpath.yml的新文件,将其连接到Stormpath应用程序。 是的,您必须用自己的值替换其中的那些值。

client:
  apiKey:
    id: YOUR_API_KEY_ID
    secret: YOUR_API_KEY_SECRET
application:
  href: https://api.stormpath.com/v1/applications/XXXX <-- YOUR APP HREF

到目前为止,一切都很好。 现在,通过运行$ node server.js尝试服务器。 如果一切设置正确,则应该看到:

Listening at http://localhost:3000

如果看到该消息,则说明您已成功配置服务器以与Stormpath进行通信,并公开了REST API供我们的React应用程序使用。

配置Webpack

在您太兴奋之前,请终止服务器并安装Webpack,以便我们可以打包所有客户端脚本(我们很快将需要此组织)。

$ npm install --save webpack
$ npm install --save-dev webpack-dev-middleware

通过创建一个名为webpack.config.js的新文件来配置Webpack,并将以下代码放入其中:

var path = require('path');
var webpack = require('webpack');

module.exports = {
  entry: [
    './src/app'
  ],
  devtool: 'eval-source-map',
  output: {
    path: __dirname,
    filename: 'app.js',
    publicPath: '/js/'
  },
  module: {
    loaders: []
  }
};

要做的就是查看我们的/src/目录(我们将很快创建),并将所有脚本及其依赖项打包到该目录下,作为一个模块。 然后,将文件/src/app.js及其导出用作该模块的导出。 最后,当它生成该模块包时,它将通过Express在/js/app.js端点下提供该/js/app.js

但是为了使Express服务Webpack文件,我们必须打开server.js并将这些行添加到它的顶部:

var webpack = require('webpack');
var config = require('./webpack.config');

然后紧接在行var app = express(); 加:

var compiler = webpack(config);

app.use(require('webpack-dev-middleware')(compiler, {
  noInfo: true,
  publicPath: config.output.publicPath
}));

免费学习PHP!

全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。

原价$ 11.95 您的完全免费

如前所述,这将允许Webpack拦截请求并提供打包的/js/app.js文件。

配置Babel

由于我们将使用ES6和JSX,因此我们需要将这些文件转换为ES5(以与非现代浏览器向后兼容)。 这就是Babel的用处。Babel可以将我们的ES6 / JSX文件作为输入,并将其转换为ES5。

要使用Babel ,请先安装一些依赖项:

$ npm install --save babel-core babel-runtime babel-loader babel-plugin-react-transform \
  babel-preset-es2015 babel-preset-react babel-preset-stage-0

现在,我们将指导Babel如何编译我们的文件,因此创建一个名为`.babelrc`的新文件并添加以下代码:

{
  "presets": ["stage-0", "es2015", "react"]
}

最后,为了使Babel能够与Webpack一起使用,我们需要编辑`webpack.config.js`并将一个条目添加到`module.loaders`数组中,如下所示:

module: {
  loaders: [{
    test: /\.js$/,
    loaders: ['babel'],
    include: path.join(__dirname, 'src')
  }]
}

Index.html和Bootstrap

现在,在开始使用React之前,我们将为我们的应用程序准备输入页面。 该页面将告诉浏览器在初始化React和应用程序之前必须加载什么。 因此,创建一个名为build的新目录,然后在其中放入名为index.html的文件。 我们的服务器将提供此文件夹中的所有静态文件。

$ mkdir build
$ cd build
$ touch index.html

然后在index.html ,放入以下内容:

<!doctype html>
<!--[if lt IE 7]><html class="no-js lt-ie9 lt-ie8 lt-ie7"><![endif]-->
<!--[if IE 7]><html class="no-js lt-ie9 lt-ie8"><![endif]-->
<!--[if IE 8]><html class="no-js lt-ie9"><![endif]-->
<!--[if gt IE 8]><!-->
<html class="no-js"><!--<![endif]-->
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <base href="/">
    <title></title>
    <meta name="description" content="">
    <meta name="viewport" content="width=device-width">
    <link rel="stylesheet" href="/css/bootstrap.min.css" />
  </head>
  <body>
    <div id="app-container"></div>
    <script src="/js/app.js"></script>
  </body>
</html>

另外,在build目录下,创建一个名为css的新目录,并将Bootstrap下载到该目录。 将文件命名为bootstrap.min.css

$ mkdir css
$ cd css
$ curl -O https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css
$ cd ../.. # return to /my-react-app

现在,为了使我们的浏览器能够访问这些文件,我们需要对其进行配置,以便通过Express进行提供。 因此,打开server.js并在文件顶部添加:

var path = require('path');

然后在app.use(stormpath.init(app, ...)); 加:

app.get('/css/bootstrap.min.css', function (req, res) {
  res.sendFile(path.join(__dirname, 'build/css/bootstrap.min.css'));
});

app.get('*', function (req, res) {
  res.sendFile(path.join(__dirname, 'build/index.html'));
});

React如何工作?

现在我们已经完成了应用程序的框架,我们可以专注于构建React应用程序。 但是在编写任何代码之前,让我们看一下React是什么以及它对我们有什么作用。

组件

在React中,一切都建立在组件上。 您可以将组件视为呈现DOM节点的组件。 一个简单的React组件如下所示:

class HelloWorld extends React.Component {
  render() {
    return <span>Hello World!</span>;
  }
}

那很简单。 现在,如果您想将此组件呈现到页面上,那么您要做的就是导入React,然后调用:

ReactDOM.render(
  <HelloWorld />,
  document.getElementById('hello-world-element')
);

然后React将组件渲染到该元素。

当然,React组件还有更多的东西,例如状态。 下面是一个计数器组件的示例,该组件在添加到DOM时开始计数,而在删除时停止。

class Counter extends React.Component {
  state = {
    current: 0
  }

  constructor() {
    super(arguments...);
    this.intervalId = null;
  }

  updateCounter() {
    this.setState({ counter: this.state.current + 1 });
  }

  componentWillMount() {
    this.setState({ counter: this.props.from || 0 });
    this.intervalId = setInterval(this.updateCounter.bind(this), 1000);
  }

  componentWillUnmount() {
    clearInterval(this.intervalId);
  }

  render() {
    return <span>{ this.state.current }</span>;
  }
}

注意方法componentWillMount()componentWillUnmount() 。 这些是组件生命周期方法,将在组件生命周期的各个点(在这种情况下为安装和卸载)执行。 这些方法通常用于设置和拆卸组件,并且是必须使用的,因为如果在尚未安装组件时尝试设置组件的状态,React将会出错。

另请注意this.props.from 。 成员this.props是传递给组件的所有属性(输入)的集合。 可以如下设置组件的属性:

<Counter from="50" />
<Counter from={ myVariable } />

JSX变量

可以使用{ nameOfVariable }轻松将变量插入到您的JSX DOM中,例如,如下所示:

render() {
  var myVariable = 123;
  return <span>{ myVariable }</span>;
}

JSX和保留JavaScript标识符

由于JSX是JavaScript,因此在使用React时需要了解一些注意事项。 即设置的属性时作出ReactDOM组件,你不能既使用forclass ,因为这些被认为是预留JavaScript标识符。 为了解决这个问题,React提供了htmlForclassName ,您应该使用它。

为了说明问题,这将不起作用:

<label for="my-input" class="my-label">My Input</label>

但这将:

<label htmlFor="my-input" className="my-label">My Input</label>

虚拟DOM

在React中,不是直接针对DOM,而是将所有组件都保存在自己的虚拟DOM中。 您可以将虚拟DOM视为JavaScript中的DOM实现(因为实际上是这样)。 然后将此虚拟DOM映射到实际DOM元素。 因此,当您渲染React组件时,React将查看该组件的DOM输出,将其与虚拟DOM中的表示形式进行比较,然后为实际DOM生成补丁。

这意味着您不必再考虑手动操作DOM元素。 您所要做的就是告诉React您希望组件的外观如何,它将负责以必要的方式(最小的工作量)来转换DOM。

安装React依赖

现在,当我们熟悉了React时,我们将通过安装一些React依赖项来开始:

$ npm install --save react react-dom react-router react-stormpath react-document-title history

在开始编码之前,我们需要一个放置React文件的地方,因此创建一个名为src的新目录,然后将其用作您的工作目录。

$ mkdir src
$ cd src

现在,让我们从应用程序的入口开始。 这将是我们设置React应用程序及其路由的地方。 因此,创建一个名为app.js的新文件并输入以下代码:

import React from 'react';
import ReactDOM from 'react-dom';
import { Router, IndexRoute, Route, browserHistory } from 'react-router';

ReactDOM.render(
  <Router history={browserHistory}>
  </Router>,
  document.getElementById('app-container')
);

因此,现在我们为应用程序奠定了基础。 让我们继续并导入Stormpath SDK和其中需要的一些东西。 在app.js文件的顶部,添加导入语句:

import ReactStormpath, { Router, HomeRoute, LoginRoute, AuthenticatedRoute } from 'react-stormpath';

如您在app.js看到的,现在有两个相互冲突的Router导入。 由于ReactStormpath.Router从延伸ReactRouter.Router我们将不会需要再这样了。 因此,继续并从react-router删除Router导入。 重要提示: 剩下其他的ReactRouter导入,稍后我们将需要它们。

现在,我们将初始化Stormpath SDK。 在ReactDOM.render()上方添加以下行。

ReactStormpath.init();

那很简单! 现在,我们准备开始构建页面。

母版页

在创建页面之前,我们必须设置路由器。 路由器决定了我们如何在React应用程序中导航。 我们将从创建共享根路由开始。 这将充当我们的“母版页”。 即,该路由下的所有路由都将共享相同的主组件(标头)。 因此,将下面的代码放在app.js<Router>标记内,使其看起来像这样:

<Router history={browserHistory}>
  <Route path='/' component={MasterPage}>
  </Route>
</Router>

如您所见,我们引用了MasterPage 。 尚不存在的东西。 因此,让我们继续在新目录中创建该目录,并将其命名为src文件夹中的pages

$ mkdir pages
$ cd pages

现在创建一个名为MasterPage.js的新文件,并将以下代码添加到其中:

import React from 'react';
import { Link } from 'react-router';
import { LoginLink } from 'react-stormpath';
import DocumentTitle from 'react-document-title';

import Header from './Header';

export default class is extends React.Component {
  render() {
    return (
      <DocumentTitle title='My React App'>
        <div className='MasterPage'>
          <Header />
          { this.props.children }
        </div>
      </DocumentTitle>
    );
  }
}

如您所见,我们还没有Header组件,让我们在具有以下内容的同一目录中创建一个名为Header.js的新文件:

import React from 'react';
import { Link } from 'react-router';
import { LoginLink, LogoutLink, Authenticated, NotAuthenticated } from 'react-stormpath';

export default class Header extends React.Component {
  render() {
    return (
      <nav className="navbar navbar-default navbar-static-top">
        <div className="container">
          <div id="navbar-collapse" className="collapse navbar-collapse">
            <ul className="nav navbar-nav">
              <li><Link to="/">Home</Link></li>
            </ul>
            <ul className="nav navbar-nav navbar-right">
            </ul>
          </div>
        </div>
      </nav>
    );
  }
}

索引页

在我们的MasterPage注意属性this.props.children 。 这将包含我们路由器匹配的子路由的组成部分。 因此,如果我们有一条看起来像的路线:

<Route path='/' component={MasterPage}>
  <Route path='/hello' component={HelloPage} />
</Route>

并且我们尝试访问/hello 。 将使用HelloPage组件填充this.props.children数组,因此该组件将在我们的母版页中呈现。

现在想象一下您尝试访问/的场景。 没有任何this.props.children ,这只会呈现您的母版页,但内容为空。 这就是IndexRoute发挥作用的地方。 使用IndexRoute您可以指定当您点击母版页路线的路径时所应呈现的组件(在本例中为“ /”)。

但是,我们添加我们之前IndexRoute我们的路由器,让我们创建我们的一个新的文件pages指定的目录IndexPage.js及以下添加到它:

import { Link } from 'react-router';
import React, { PropTypes } from 'react';
import { LoginLink } from 'react-stormpath';

export default class IndexPage extends React.Component {
  render() {
    return (
      <div className="container">
        <h2 className="text-center">Welcome!</h2>
        <hr />
        <div className="jumbotron">
          <p>
            <strong>To my React application!</strong>
          </p>
          <p>Ready to begin? Try these Stormpath features that are included in this example:</p>
          <ol className="lead">
            <li><Link to="/register">Registration</Link></li>
            <li><LoginLink /></li>
            <li><Link to="/profile">Custom Profile Data</Link></li>
          </ol>
        </div>
      </div>
    );
  }
}

现在,让我们添加IndexRoute 。 打开app.js并在标签<Route path='/' component={MasterPage}>添加您的IndexRoute ,使其类似于以下内容:

<Route path='/' component={MasterPage}>
  <IndexRoute component={IndexPage} />
</Route>

登录页面

现在,我们有了一个React应用程序,其中显示了带有默认页面的标题。 但是我们还没有登录页面。 因此,让我们创建一个名为`LoginPage.js`的新文件,并向其中添加一些内容:

import React from 'react';
import DocumentTitle from 'react-document-title';
import { LoginForm } from 'react-stormpath';

export default class LoginPage extends React.Component {
  render() {
    return (
      <DocumentTitle title={`Login`}>
        <div className="container">
          <div className="row">
            <div className="col-xs-12">
              <h3>Login</h3>
              <hr />
            </div>
          </div>
          <LoginForm />
        </div>
      </DocumentTitle>
    );
  }
}

注意LoginForm组件。 这是我们要添加的全部内容,以便我们拥有可以供人们注册的完整工作表格。

但是在使用它之前,我们需要打开app.js并在路由器中为该页面添加路由。 因此,在标记<Route path='/' component={MasterPage}>添加以下内容:

<LoginRoute path='/login' component={LoginPage} />

为了能够访问登录页面,我们需要将其添加到菜单中。 因此,继续打开Header.js并在元素<ul className="nav navbar-nav navbar-right">添加以下内容:

<NotAuthenticated>
  <li>
    <LoginLink />
  </li>
</NotAuthenticated>

如您所见,我们正在使用NotAuthenticated组件。 这样,我们将仅在用户尚未登录时显示LoginLink

注册页面

现在,让我们添加一个页面,供人们注册。 我们将其称为RegistrationPage 。 因此,创建一个名为RegistrationPage.js的新文件,并将以下内容放入其中:

import React from 'react';
import DocumentTitle from 'react-document-title';
import { RegistrationForm } from 'react-stormpath';

export default class RegistrationPage extends React.Component {
  render() {
    return (
      <DocumentTitle title={`Registration`}>
        <div className="container">
          <div className="row">
            <div className="col-xs-12">
              <h3>Registration</h3>
              <hr />
            </div>
          </div>
          <RegistrationForm />
        </div>
      </DocumentTitle>
    );
  }
}

注意,我们使用了RegistrationForm组件。 您可能已经猜到了,这将呈现一个Stormpath注册表格。 注册后,它将使用户指向登录页面,他们将可以登录。

为了访问此页面。 我们需要添加一条路线。 因此,继续打开app.js并在<Route path='/' component={MasterPage}>标记内添加:

<Route path='/register' component={RegistrationPage} />

现在,我们有了一条路线,但是除非我们链接到该页面,否则人们将无法找到该页面,因此请打开Header.js并在<ul className="nav navbar-nav navbar-right">的结束标记( </ul> )之前添加以下内容<ul className="nav navbar-nav navbar-right">

<NotAuthenticated>
  <li>
    <Link to="/register">Create Account</Link>
  </li>
</NotAuthenticated>

请注意,使用了NotAuthenticated组件。 这样,我们只会在用户未登录时显示/register链接。

个人资料页

用户登录后,我们希望能够向他们显示一些个性化内容(他们的用户数据)。 因此,创建一个名为ProfilePage.js的新文件,并将以下代码放入其中:

import React from 'react';
import DocumentTitle from 'react-document-title';
import { UserProfileForm } from 'react-stormpath';

export default class ProfilePage extends React.Component {
  render() {
    return (
      <DocumentTitle title={`My Profile`}>
      <div className="container">
          <div className="row">
            <div className="col-xs-12">
              <h3>My Profile</h3>
              <hr />
            </div>
          </div>
          <div className="row">
            <div className="col-xs-12">
              <UserProfileForm />
            </div>
          </div>
        </div>
      </DocumentTitle>
    );
  }
}

注意,我们使用了UserProfileForm 。 这是一个简单的帮助程序表单,允许您编辑最基本的用户字段。

但是,为了实际修改用户个人资料,我们需要在服务器中进行一些更改。 因此,打开server.js并在app.use(stormpath.init(app, ...));下添加以下路由app.use(stormpath.init(app, ...));

app.post('/me', bodyParser.json(), stormpath.loginRequired, function (req, res) {
  function writeError(message) {
    res.status(400);
    res.json({ message: message, status: 400 });
    res.end();
  }

  function saveAccount () {
    req.user.givenName = req.body.givenName;
    req.user.surname = req.body.surname;
    req.user.email = req.body.email;

    req.user.save(function (err) {
      if (err) {
        return writeError(err.userMessage || err.message);
      }
      res.end();
    });
  }

  if (req.body.password) {
    var application = req.app.get('stormpathApplication');

    application.authenticateAccount({
      username: req.user.username,
      password: req.body.existingPassword
    }, function (err) {
      if (err) {
        return writeError('The existing password that you entered was incorrect.');
      }

      req.user.password = req.body.password;

      saveAccount();
    });
  } else {
    saveAccount();
  }
});

这将允许表单更改用户的给定名称,姓氏,电子邮件和密码。

如果您还有其他要编辑的字段,则只需自定义UserProfileForm表单,然后在上面的路由中添加要编辑的字段。

现在,为了使我们能够从菜单访问此页面,请打开Header.js然后在<li><Link to="/">Home</Link></li>下方添加:

<Authenticated>
  <li>
    <Link to="/profile">Profile</Link>
  </li>
</Authenticated>

这样,使用Authenticated组件,当我们有一个用户会话时,我们将呈现一个指向/profile页面的链接,并允许我们的用户查看其用户个人资料。

为了使我们能够访问该页面,我们必须像其他页面一样将其添加到路由器中。 打开app.js并在<Route path='/' component={MasterPage}>标记内添加:

<AuthenticatedRoute path='/profile' component={ProfilePage} />

注意,我们正在使用AuthenticatedRoute 。 这是只有经过身份验证的用户会话才能访问的路由。 如果没有会话,则用户将自动重定向到LoginLink的路径。

本国路线

现在,当我们设置完大部分路由之后。 让我们看一下称为HomeRoute的特殊路由。 这条路线本身没有任何作用。 但是充当“标记”,以指示登录和注销时重定向到的位置。

因此,为了指定退出时要在哪里结束,请打开app.js并更改:

<Route path='/' component={MasterPage}>
  ...
</Route>

进入:

<HomeRoute path='/' component={MasterPage}>
  ...
</HomeRoute>

现在注销时,Stormpath SDK将知道它应该重定向到“ /”路径。 现在,要指定注销时重定向到的位置,请更改我们在上一步中创建的AuthenticatedRoute

<AuthenticatedRoute path='/profile' component={ProfilePage} />

这样看起来像:

<AuthenticatedRoute>
  <HomeRoute path='/profile' component={ProfilePage} />
</AuthenticatedRoute>

注意AuthenticatedRoute如何包装HomeRoute 。 这用于指示登录后我们要重定向到的经过身份验证的路由。

登出

最后,一旦我们的用户注册并登录。我们希望为他们提供注销选项。 幸运的是,添加它确实很简单。

因此,打开Header.js并在<ul className="nav navbar-nav navbar-right">添加以下代码:

<Authenticated>
  <li>
    <LogoutLink />
  </li>
</Authenticated>

注意LogoutLink组件。 单击此选项后,用户会话将被自动破坏,并且用户将被重定向到未经HomeRoute验证的HomeRoute

组件中的用户状态

通过请求身份验证用户上下文类型来访问组件中的用户状态:

class ContextExample extends React.Component {
  static contextTypes = {
    authenticated: React.PropTypes.bool,
    user: React.PropTypes.object
  };

  render() {
    if (!this.context.authenticated) {
      return (
        <div>
          You need to <LoginLink />.
        </div>
      );
    }

    return (
      <div>
        Welcome {this.context.user.username}!
      </div>
    );
  }
}

导入元件

为了能够引用我们的页面,我们需要导入它们。 为了使导入变得容易,我们将它们全部放入一个index.js文件中,因此我们只需要导入一次即可。 因此,让我们在pages目录中创建一个名为index.js的新文件,并从中导出所有页面,如下所示:

export MasterPage from './MasterPage'
export IndexPage from './IndexPage'
export LoginPage from './LoginPage'
export RegistrationPage from './RegistrationPage'
export ProfilePage from './ProfilePage'

这样,我们只需要做一次导入就可以访问我们的所有页面。

因此,让我们这样做。 打开app.js文件,并在文件顶部添加以下导入语句:

import { MasterPage, IndexPage, LoginPage, RegistrationPage, ProfilePage } from './pages';

运行项目

现在,我们有了一个应用程序,用户可以在其中注册,登录和显示其用户数据。 因此,让我们尝试一下!

和以前一样,通过运行以下命令来启动我们的服务器:

$ node server.js

如果一切运行顺利,您应该可以看到以下消息:

Listening at http://localhost:3000

因此,在浏览器中打开http:// localhost:3000并尝试一下!

摘要

如您在本文中所见,React是一个非常强大的工具,当与ES6,JSX和Stormpath结合使用时,构建应用程序突然变得很有趣。

如果您不确定任何部分,请随时检查示例项目并将其用作参考实现。 我也很喜欢有关React设计的文章 –它详细说明了React为什么很棒。

并且,如果您对Stormpath React SDK有疑问,请务必查看其API文档

翻译自: https://www.sitepoint.com/tutorial-build-a-react-js-application-with-user-login-and-authentication/

react.js做小程序

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值