使用 IBM Bluemix 在云中构建和部署一个投资跟踪应用程序,第 2 部分

栏目: 数据库 · 发布时间: 6年前

内容简介:使用 IBM Bluemix 在云中构建和部署一个投资跟踪应用程序,第 2 部分

在在本系列 的第 1 部分 中,您了解了如何使用一个开放 API 连接并检索财务数据。在第 2 部分中,您将了解如何使用第 1 部分中开发的 PHP 应用程序中的财务数据,提供投资组合的即时、准确的估值。一个顺应移动趋势的接口框架使应用程序能够在桌面计算机和移动设备上同样良好地运行。将它托管在 IBM Bluemix® 上可确保可靠性和可扩展性。

本系列中介绍的示例应用程序允许用户搜索股票并将一支或多支股票添加到他们的在线投资组合中。

完成您的应用程序的前提条件

参见第 1 部分 了解预备知识和软件,以及 API 需求。

下载所有代码和试验!

学习、开发和联系

在新的 developerWorks Premium 会员计划中一站式访问强大的开发 工具 和活动。除了为期 12 个月的 Bluemix 订阅和 240 美元额度,还包含 Safari Books Online。浏览 500 多篇优秀的技术文章(100 多篇是专门面向云开发人员的),观看最新的 O'Reilly 大会的视频回放,并且参加重要的开发人员活动可享有大额折扣。

立即注册

您可从 GitHub 存储库 下载本系列中实现的所有代码,以及这里使用的 PHP buildpack 的配置文件。推荐您获取该代码,使用它,并尝试添加一些新特性。我保证您不会造成任何破坏,而且它肯定对您的学习有所帮助。祝开发愉快!

Bluemix 平台和它的各种各样的服务与 Cloud Foundry PHP buildpack 相结合,为您提供了专为可扩展性和可靠性而设计的尖端技术基础架构。

第 1 步. 初始化和配置 Cloudant

在第 1 部分 中,我介绍了如何设置一个页面模板,该模板包含一个链接到 /add URL 路由的 Add 按钮。下一个逻辑步骤是充实该功能,允许用户向他或她的投资组合添加股票。这需要一个持久数据存储:在本例中,使用了一个 Cloudant(或 CouchDB)数据库。

阅读: Cloudant 文档

阅读: CouchDB 基础

  1. 开始使用 Cloudant 的最简单的方式是登录到您的 Bluemix 帐户并添加一个新的 Cloudant 服务实例。最初,这个 Cloudant 服务实例在一种未绑定状态下运行;这允许在本地开发应用程序,将数据库本身托管在 Bluemix 云中。一旦应用程序随后开发完成,您将把它部署到 Bluemix 并将 Cloudant 服务实例绑定到部署的应用程序实例。
  2. 要创建一个新 Cloudant 数据库,可登录到您的 Bluemix 帐户,从仪表板单击 USE SERVICES OR APIS 按钮。 使用 IBM Bluemix 在云中构建和部署一个投资跟踪应用程序,第 2 部分
  3. 从结果服务列表中,选择 Cloudant NoSQL DB使用 IBM Bluemix 在云中构建和部署一个投资跟踪应用程序,第 2 部分
  4. 检查该服务的描述并单击 USE 来启动它。确保 "App" 字段设置为 " Leave unbound "。 使用 IBM Bluemix 在云中构建和部署一个投资跟踪应用程序,第 2 部分

    Cloudant 数据库服务实例现在将被初始化,您将看到一个服务信息页面。单击页面上的 Launch 来启动 Cloudant 仪表板。这是您应在您的实例的 Cloudant 仪表板上看到的界面:

    使用 IBM Bluemix 在云中构建和部署一个投资跟踪应用程序,第 2 部分
  5. 您在这里的第一个任务是创建一个新数据库。单击顶部菜单栏中的 Add New Database 按钮,并输入新数据库的名称 portfolios 。单击 Create 来创建该数据库。 使用 IBM Bluemix 在云中构建和部署一个投资跟踪应用程序,第 2 部分

    您的数据库现在已创建。

  6. 因为每个应用程序用户可拥有一个单独的投资组合,所以该应用程序存储的每个文档必须包含一个用户标识符。得益于 HybridAuth,这个现成的标识符是用户的电子邮件地址。所以,下一步是向 Cloudant 添加一个搜索索引,它可用于查找和返回所有与指定的电子邮件地址匹配的文档。

    为此,在可用数据库列表中选择您新创建的数据库,然后在结果页面上选择创建一个 新搜索索引 的选项。

    使用 IBM Bluemix 在云中构建和部署一个投资跟踪应用程序,第 2 部分
  7. 将设计文档命名为 users ,将搜索索引命名为 searchByUID 。在搜索索引函数中使用以下代码:
    function (doc) { 
     index("default", doc.uid);
    }
    使用 IBM Bluemix 在云中构建和部署一个投资跟踪应用程序,第 2 部分
  8. 单击 Save and Build Index 将索引保存到系统中。您现在可使用这个 searchByUID() 函数在文档的 'uid' 属性中检索所有与一个指定电子邮件地址匹配的文档。稍后将更详细地介绍此函数。
  9. 接下来,导航回到您的 Bluemix 仪表板,在可用服务列表中选择新 Cloudant 服务实例。在结果页面上,单击左侧导航栏中的箭头并从结果菜单中选择 Service Credentials使用 IBM Bluemix 在云中构建和部署一个投资跟踪应用程序,第 2 部分
  10. 复制 JSON 凭据块中的 URL 并将其粘贴到您应用程序的配置文件中的 'db_uri' 键中。与此同时,更新 'db_name' 键以反映您刚创建的新数据库的名称(在本例中为 "portfolios")。这是它的可能内容:
    <?php
    // config.php
    $config = [
      'quandl_key'    => 'YOUR-QUANDL-API-KEY',
      'oauth_id'      => 'YOUR-OAUTH-ID',
      'oauth_secret'  => 'YOUR-OAUTH-SECRET',
      'db_uri'        => 'https://username:password@instancexyz-bluemix.cloudant.com',
      'db_name'       => 'portfolios'
    ];
  11. 这是在 $APP_ROOT/index.php 中更新您的应用程序代码,以及配置 Guzzle 客户端来为所有请求使用此数据库 URI 作为其基础 URI 的不错时机。
    <?php
    // application initialization and other routes – snipped!
    
    // initialize HTTP client
    $guzzle = new GuzzleHttp\Client([
      'base_uri' => $app->config['db_uri'] . '/'
    ]);

您的 Cloudant 数据库实例现在已经激活,而且您的应用程序经过配置可使用它。您可使用此数据库存储一个用户的投资组合的细节,这是本部分的剩余内容处理的重点。

第 2 步. 将股票添加到投资组合

您现在已有一个持久数据存储。下一步是添加一个表单和表单处理器,以便用户可开始从搜索屏幕中将股票添加到其投资组合中。这是一个简单的表单,它应保存为 $APP_ROOT/views/add.twig:

点击查看代码清单

关闭 [x]

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jquerymobile/1.4.5/jquery.mobile.min.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquerymobile/1.4.5/jquery.mobile.min.js"></script>    
  </head>
  <body>

    <div data-role="dialog">

      <div data-role="content">	
        <div>
          <h2 class="ui-bar ui-bar-a">Add to Portfolio: {{symbol}}</h2>
          <div class="ui-body">
            <form method="post" action="{{ app.request.basepath }}/add" data-ajax="false">
              <input type="hidden" name="symbol" value="{{symbol}}" />
              <label for="units">Number of shares:</label>
              <input type="text" id="units" name="units" />
              <input type="submit" name="submit" value="Add" />
            </form>
          </div>      
        </div>        
      </div>
      
    </div>
      
  </body>
</html>

这是一个将由 jQuery Mobile 借助顶级容器中的 data-role="dialog" 属性设置为对话框样式的 Web 表单。该对话框本身非常简单:它包含一个表单,表单中有一个字段和一个 Submit 按钮,用户可在该字段中输入在一个指定公司内持有的股数。公司的股票代号(您要记住,此代码作为 URL 参数传递到页面)作为隐含的输入包含在表单中。

/add 路由的 Silex 回调也同样简单:它呈现表单,传递股票代号作为模板变量,以便其可插入到表单中的这个隐含字段中。以下是回调代码,它应添加到 $APP_ROOT/index.php 中:

<?php
// application initialization and other routes – snipped!

$app->get('/add/{symbol}', function ($symbol) use ($app) {
  return $app['twig']->render('add.twig', array('symbol' => $symbol));
})
->before($authenticate);

用户提交表单后,一个单独的回调会收到用户输入,验证它并通过 REST 将它保存到 Cloudant 数据库。以下是必要的回调,用于处理表单 POST 提交:

<?php
// application initialization and other routes – snipped!

// form submission handler
$app->post('/add', function (Request $request) use ($app, $guzzle) {
  $symbol = strip_tags($request->get('symbol'));
  $units = (int)$request->get('units');
  if ($units <= 0) {
    throw new Exception('Invalid input');
  }
  $doc = [
    'uid' => $_SESSION['uid'],
    'symbol' => $symbol,
    'units' => $units
  ];
  $guzzle->post($app->config['db_name'], [ 'json' => $doc ]);
  return $app->redirect($app["url_generator"]->generate('index') . '#manage');
})
->before($authenticate);

在这里,一个 Silex 回调接收表单提交作为 POST 请求,并提取用户提交的各个值。执行基本的输入验证和清理(例如检查输入的数量是否为大于 0 的数),然后将结果值格式化为一个适合插入到 Cloudant 中的 JSON 文档。然后使用 Guzzle HTTP 客户端制定一个包含该 JSON 文档的 POST 请求,并将它发送到 Cloudant REST API;这会创建一个新文档并保存在 Cloudant 数据库中。最后,客户端浏览器被重定向回应用程序的索引页面。

要查看实际运行情况,可尝试通过该应用程序选择并添加一支股票。您现在应看到类似下图的界面:

使用 IBM Bluemix 在云中构建和部署一个投资跟踪应用程序,第 2 部分

输入一个值并单击 Add 后,您会被重定向回索引页面。接下来如果浏览回 Bluemix 控制台,启动 Cloudant 仪表板并查看您数据库中的 所有文档 ,您应看到一条包含新添加的投资组合股票的细节的新记录。

使用 IBM Bluemix 在云中构建和部署一个投资跟踪应用程序,第 2 部分

第 3 步. 更新投资组合价值

现在,应用程序能够支持输入股票组合的数据。接下来的工作就是通过将数量乘以当前价格来计算投资组合中每支股票的价值。对于当前价格,我们将再次访问 Quandl API 的 WIKI 数据库,它(回想第 1 部分中的第 4 步)提供了美国股票的盘后价格。

为此,该应用程序需要连接到 Cloudant 数据库,检索登录的用户的所有可用文档(每个文档表示一支股票),然后对于每个这样的文档,连接到 Quandl API 来检索相应股票代号的当前价格。此信息然后可与最终的价值计算结果一起提供给模板来显示。

这是更新后的 /index 路由的代码,它执行了上述所有步骤:

点击查看代码清单

关闭 [x]

<?php
// application initialization and other routes – snipped!

$app->get('/index', function () use ($app, $guzzle) {
  $uid = $_SESSION['uid'];
  // get all stocks in user's portfolio
  $response = $guzzle->get($app->config['db_name'] . '/_design/users/_search/searchByUID?include_docs=true&q='.urlencode($uid));
  $list = json_decode((string)$response->getBody());
  $symbols = [];
  $data = [];
  // extract unique list of stocks
  foreach ($list->rows as $row) {
    $symbol = $row->doc->symbol;
    $symbols[$symbol] = 0;
  }
  // get closing price for each stock
  foreach ($symbols as $key => &$value) {
    $url = "https://www.quandl.com/api/v1/datasets/WIKI/$key.json?api_key=" .$app->config['quandl_key'] . "&rows=1";
    $response = $guzzle->get($url);
    $eod = json_decode((string)$response->getBody());
    $idx = array_search('Close', $eod->column_names);
    $price = $eod->data[0][$idx];
    $value = $price;
  }
  // create structured array of stocks, prices and valuations
  foreach ($list->rows as $row) {
    $id = $row->doc->_id;
    $rev = $row->doc->_rev;
    $rid = "$id.$rev";
    $symbol = $row->doc->symbol;
    $units = $row->doc->units;
    $price = $symbols[$symbol];
    $data[$rid] = [
      'symbol' => $symbol,
      'units' => $units,
      'price' => $price,
      'value' => $units * $price,
    ];
  }
  return $app['twig']->render('index.twig', array('data' => $data, 'uid' => $uid));
})
->before($authenticate)
->bind('index');

/index 回调使用之前创建的 searchByUID() 方法来检索所有属于当前登录的用户的文档。然后它迭代这些文档来提取出股票列表,对于每支股票,生成一个对 Quandl API 的请求来检索最新价格。API 响应和价值计算得出的股票价格然后组合到一个数组中,被传递到模板中来显示。

您现在也可更新 $APP_ROOT/views/index.twig 上的索引页面模板,如下所示:

点击查看代码清单

关闭 [x]

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jquerymobile/1.4.5/jquery.mobile.min.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquerymobile/1.4.5/jquery.mobile.min.js"></script>    
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js"></script>
    <script>      
      var myApp = angular.module('myApp', []);
      
      myApp.controller("myAppController", function ($scope, $http) {
        $scope.items = {};
        $scope.items.results = [];
        $scope.items.query = '';
        
        $scope.search = function() {
          if ($scope.items.query != '') {
            $http({
                method: 'GET',
                url: '{{ app.request.basepath }}/search/' + $scope.items.query,
              }).
              success(function(data) {
                $scope.items.results = data.docs;
              });
          } else {
            $scope.items.results = [];
          }
        };
      });
    </script>    
  </head>
  <body>

    <div data-role="page">

      <div data-role="header">
        <h1>Portfolio Tracker</h1>
        <a data-ajax="false" href="{{ app.request.basepath }}/logout" data-role="button" class="ui-btn-right">Sign out</a>
        <div class="ui-bar ui-bar-a" style="text-align: center">
          {{ uid }}
        </div>
      </div>

      <div data-role="content">
        <div data-role="tabs">
        
          <div id="navbar" data-role="navbar">
            <ul>
              <li><a id="tab-search" href="#search" data-theme="a" class="ui-btn-active">Search</a></li>
              <li><a id="tab-manage" href="#manage" data-theme="a" class="">Manage</a></li>
            </ul>
          </div>
          
          <div id="search" ng-app="myApp" ng-controller="myAppController">
            <h2 class="ui-bar ui-bar-a">Stock Search</h2>
            <div class="ui-body">
                <input type="search" name="query" ng-model="items.query" />
                <button ng-click="search()">Search</button>
            </div>      
            <h2 class="ui-bar ui-bar-a">Search Results</h2>   
            <div class="ui-body">
              <ul data-role="listview" data-split-theme="d">
                <li ng-repeat="r in items.results">
                {% verbatim %}
                  <a>{{r.name}}</a>
                {% endverbatim %}
                  <a href="{{ app.request.basepath }}{% verbatim %}/add/{{r.code}}{% endverbatim %}" data-ajax="false" data-inline="true" data-role="button" data-icon="plus" data-theme="a">Add</a>                
                </li>
              </ul>                    
            </div>          
          </div>

          <div id="manage">
            <h2 class="ui-bar ui-bar-a">Portfolio Summary</h2>
            <div class="ui-body">
              <ul data-role="listview">          
              {% for id, item in data %}
                <li>
                  <a href="#">
                    <div class="ui-grid-a">
                      <div class="ui-block-a">
                        {{ item.symbol }}
                      </div>
                      <div class="ui-block-b">
                         <span class="ui-li-count">${{ item.value }}</span>                
                      </div>
                    </div>
                    <div>
                      <p>{{ item.units }} units at ${{ item.price}}</p>
                    </div>
                  </a>
                  <a href="{{ app.request.basepath }}/delete/{{ id }}" data-ajax="false" data-inline="true" data-role="button" data-icon="minus" data-theme="a">Remove</a>                 
                </li>
              {% endfor %}
              </ul>
            </div>          
          </div>

        </div>        
      </div>

      <div data-role="footer">
      </div>

    </div>
      
  </body>
</html>

这个清单向索引页面的 “Manage” 选项卡添加了一个新的 “Portfolio Summary” 部分。该部分列出投资组合中的每支股票,以及股数和当前价值。这是该界面的外观:

使用 IBM Bluemix 在云中构建和部署一个投资跟踪应用程序,第 2 部分

第 4 步. 从投资组合中删除股票

从前面的图像和代码清单中,可以清楚地看到投资组合摘要页面中列出的每支股票包含一个 Delete 按钮,该按钮链接到 /delete 路由并将 Cloudant 文档和修订标识符作为 URL 参数传递给路由回调。有了目前学到的所有知识,您现在应能轻松地为此路由编写一个 Silex 回调,它使用所提供的信息来向 Cloudant REST API 发送一个 DELETE 请求,并从数据库中删除该文档。

这是附加的代码:

<?php
// application initialization and other routes – snipped!

$app->get('/delete/{rid}', function ($rid) use ($app, $guzzle) {
  $arr = explode('.', $rid);
  $id = $arr[0];
  $rev = $arr[1];
  $guzzle->delete($app->config['db_name'] . '/' . $id . '?rev=' . $rev);
  return $app->redirect($app["url_generator"]->generate('index') . '#manage');
})
->before($authenticate);

这也是向应用程序添加错误处理函数的不错时机:

<?php
// application initialization and other routes – snipped!

// error page handler
$app->error(function (\Exception $e, $code) use ($app) {
  return $app['twig']->render('error.twig', array('error' => $e->getMessage()));
});

这个简单的错误处理函数拦截所有异常并呈现一个包含异常消息的错误模板。您可在应用程序的源代码存储库中找到错误模板的代码。

第 5 步. 部署到 IBM Bluemix

现在该应用程序已编码完成,最后一步是部署它。当然,如果部署在本地,那么您已完成 -- 您应能够正常使用该应用程序。但是,如果部署在 Bluemix 上,您需要一个 Bluemix 帐户Cloud Foundry 命令行工具 。按照以下步骤完成部署过程。

  1. 更新 Bluemix 环境的应用程序代码。

    部署到 Bluemix 后,您将需要将一个 Cloudant 服务实例附加或绑定到您的应用程序。您已在上面的第 1 步中通过 Bluemix 配备了此实例,但需要手动检索该数据库实例的凭据才能使用它们。但是,同样的凭据已在 Bluemix 环境中通过特殊的 VCAP_SERVICES 环境变量公开。因此,最好更新应用程序代码,以在此变量可用时读取它并从中检索凭据,因为这样会使您的应用程序更可移植,使您能够在需要时轻松地 “换入” 一个新的 Cloudant 服务实例。

    向 $APP_ROOT/index.php 文件中添加以下代码:

    <?php
    // use Composer autoloader
    require 'vendor/autoload.php';
    require 'config.php';
    
    // load classes – snipped!
    
    // initialization – snipped!
    
    // load configuration from file – snipped!
    
    // add configuration for HybridAuth – snipped!
    
    // register various providers – snipped!
    
    // if Bluemix VCAP_SERVICES environment available
    // overwrite local database credentials with Bluemix credentials
    if ($services = getenv("VCAP_SERVICES")) {
      $services_json = json_decode($services, true);
      $app->config['db_uri'] = $services_json['cloudantNoSQLDB'][0]['credentials']['url'];
    }
    
    // start session
    session_start();
    
    // application route handlers – snipped!
    
    $app->run();
  2. 创建应用程序清单文件。

    应用程序清单文件告诉 Bluemix 如何部署应用程序。具体来讲,它指定应该使用的 PHP 运行时环境(“build pack”)。在 $APP_ROOT/manifest.yml 上创建此文件,在其中填入以下信息。

    ---
    applications:
    - name: portfolio-tracker-[your-initials]
    memory: 256M
    instances: 1
    host: portfolio-tracker-[your-initials]
    buildpack: https://github.com/cloudfoundry/php-buildpack.git
    stack: cflinuxfs2

    请记住更新主机和应用程序名称,使其保持唯一,这可通过更改它或向它附加一个随机字符串或编号(比如您的首字母)来完成。我使用了 Cloud Foundry PHP buildpack ,但也可采用其他替代方法。

  3. 连接到 Bluemix 并部署应用程序。

    使用 cf 命令行工具,使用您 IBM 用户名和密码登录到 Bluemix。

    shell> cf api https://api.ng.bluemix.net
    shell> cf login

    更改到 $APP_ROOT 目录并将应用程序推送到 Bluemix。

    shell> cf push

    以下是您在此过程中将看到的输出的示例。

    使用 IBM Bluemix 在云中构建和部署一个投资跟踪应用程序,第 2 部分
  4. 将 Cloudant 服务绑定到您的应用程序。

    您的应用程序现在已经部署,但仍然需要将它连接到一个 Cloudant 数据库实例。因为您已在上面的第 1 步中配备了这个实例,所以这一步很容易完成。只需转到 Bluemix 仪表板并使用您的 IBM 用户名和密码登录。您应看到您的应用程序在页面上列出。单击应用程序名称以到达应用程序细节页面。

    使用 IBM Bluemix 在云中构建和部署一个投资跟踪应用程序,第 2 部分

    点击查看大图

    关闭 [x]

    使用 IBM Bluemix 在云中构建和部署一个投资跟踪应用程序,第 2 部分

    单击 BIND A SERVICE OR API 按钮,在显示的页面上选择您在上面的第 1 步中配备的 Cloudant 数据库,单击 ADD 将它绑定到您的应用程序。

    使用 IBM Bluemix 在云中构建和部署一个投资跟踪应用程序,第 2 部分 您会被提示重新暂存您的应用程序。重新暂存,您现在应在 Bluemix 仪表板中看到 Cloudant 服务实例已绑定到您的应用程序。

  5. 在 Google 开发人员控制台中更新您的重定向 URI。

    最后一步是再次访问 Google 开发人员控制台,更新授权的重定向 URL 列表以包含您的 Bluemix 应用程序的主机名。没有此更改,您的 OAuth 重定向将失败,用户将无法登录到该应用程序。

  6. 开始使用您的应用程序。

    应用程序部署后,即可浏览到您在应用程序清单文件中指定的主机(例如 http://portfolio-tracker-[your-initials].mybluemix.net)来开始使用它。也可以单击本教程顶部的 运行应用程序 来试着实时演示该应用程序。如果看到一个空白页面或其他错误,请参阅 “ 在 IBM Bluemix 上调试 PHP 错误 ” 来调试您的 PHP 代码,找到哪里出错了。

有关为 Bluemix 上的 PHP 应用程序使用 manifest.yml 文件 的更多细节,请参阅 Carl Osipov 的文章 “ 将 Hello World PHP 应用程序部署到 IBM Bluemix 上的 Zend Server 中 。”

在我的文章 “ 在 IBM Bluemix 上为基于框架的 PHP 应用程序配置 URL 重写 ” 中学习如何在 Bluemix 上设置 URL 重写 。(对于我的示例应用程序,URL 重写已在 GitHub 代码库中的 .htaccess 文件 中完成。)

结束语

借助通过开放 API 广泛提供财务数据的能力,很容易便可创建一个利用该数据帮助用户了解其财务生活的移动 Web 应用程序。Bluemix 平台和它的各种各样的服务与 Cloud Foundry PHP buildpack 相结合,为您提供了专为可扩展性和可靠性而设计的尖端技术基础架构。再辅以 Silex、jQuery Mobile 和 HybridAuth 等开源组件,您已拥有一个全功能的工具包来在云中设计、构建和测试您的应用程序。

BLUEMIX SERVICE USED IN THIS TUTORIAL: Cloudant NoSQL 服务 提供了访问一个始终在线、全面托管的 NoSQL JSON 数据层的能力。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

刷屏

刷屏

[美] 凯文•阿洛卡 / 侯奕茜、何语涵 / 中信出版社 / 2018-10-1 / 68.00

1. YouTube流行趋势经理,解密如何打造爆款视频 在视频时代,制造互动,才能创造潮流 用户不再是被动的观众,而是主动的传播者 2. 《刷屏》以行内人视角解读: 病毒视频 粉丝经济 网红产业 平台如何为内容创作者赋能 3. 你是否常常被病毒视频刷屏?你是否觉得很多网红火爆到“无法用常理解释”? 视频时代已经到来,我们每天观看网络......一起来看看 《刷屏》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具