分类 软件开发 下的文章

刚开始学习软件工程的时候,我们经常会碰到像这样的事情:

软件应该符合 SOLID 原则。

但这句话实际是什么意思?让我们看看 SOLID 中每个字母在架构里所代表的重要含义,例如:

简单来说,我们需要提供一个类,这个类有它所需要的所有对象,以便实现其功能。

概述

依赖注入听起来像是描述非常复杂的东西的一个术语,但实际上它很简单,看下面这个例子你就明白了:

class NoDependencyInjection {
  private Dependency d;

  public NoDependencyInjection() {
    d = new Dependency();
  }
}

class DependencyInjection {
  private Dependency d;

  public DependencyInjection(Dependency d) {
    this.d = d;
  }
}

正如我们所见,第一种情况是我们在构造器里创建了依赖对象,但在第二种情况下,它作为参数被传递给构造器,这就是我们所说的 依赖注入 dependency injection 。这样做是为了让我们所写的类不依靠特定依赖关系的实现,却能直接使用它。

参数传递的目标是构造器,我们就称之为构造器依赖注入;或者是某个方法,就称之为方法依赖注入:

class Example {
  private ConstructorDependency cd;
  private MethodDependency md;
  Example(ConstructorDependency cd) {
    this.cd = cd; //Constructor Dependency Injection
  }

  public setMethodDependency(MethodDependency md) {
    this.md = md; //Method Dependency Injection
  }
}

要是你想总体深入地了解依赖注入,可以看看由 Dan Lew 发表的精彩的演讲,事实上是这个演讲启迪了这篇概述。

在 Android 平台,当需要框架来处理依赖注入这个特殊的问题时,我们有不同的选择,其中最有名的框架就是 Dagger 2。它最开始是由 Square 公司(LCTT 译注:Square 是美国一家移动支付公司)的一些很棒的开发者开发出来的,然后慢慢发展成由 Google 自己开发。首先开发出来的是 Dagger 1,然后 Big G 接手这个项目发布了第二个版本,做了很多改动,比如以 注解 annotation 为基础,在编译的时候完成其任务。

导入框架

安装 Dagger 并不难,但需要导入 android-apt 插件,通过向项目的根目录下的 build.gradle 文件中添加它的依赖关系:

buildscript{
  ...
  dependencies{
    ...
    classpath ‘com.neenbedankt.gradle.plugins:android-apt:1.8’
  }
}

然后,我们需要将 android-apt 插件应用到项目 build.gradle 文件,放在文件顶部 Android application 那一句的下一行:

apply plugin: ‘com.neenbedankt.android-apt’

这个时候,我们只用添加依赖关系,然后就能使用库及其 注解 annotation 了:

dependencies{
    ...
    compile ‘com.google.dagger:dagger:2.6’ 
    apt ‘com.google.dagger:dagger-compiler:2.6’
    provided ‘javax.annotation:jsr250-api:1.0’
}
需要加上最后一个依赖关系是因为 @Generated 注解在 Android 里还不可用,但它是原生的 Java 注解

Dagger 模块

要注入依赖,首先需要告诉框架我们能提供什么(比如说上下文)以及特定的对象应该怎样创建。为了完成注入,我们用 @Module 注释对一个特殊的类进行了注解(这样 Dagger 就能识别它了),寻找 @Provide 注解的方法,生成图表,能够返回我们所请求的对象。

看下面的例子,这里我们创建了一个模块,它会返回给我们 ConnectivityManager,所以我们要把 Context 对象传给这个模块的构造器。

@Module
public class ApplicationModule {
  private final Context context;

  public ApplicationModule(Context context) {
    this.context = context;
  }

  @Provides @Singleton
  public Context providesContext() {
    return context;
  }

  @Provides @Singleton
  public ConnectivityManager providesConnectivityManager(Context context) {
    return (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
  }
}
Dagger 中十分有意思的一点是简单地注解一个方法来提供一个单例(Singleton),就能处理所有从 Java 中继承过来的问题。

组件

当我们有一个模块的时候,我们需要告诉 Dagger 想把依赖注入到哪里:我们在一个 组件 Component 里完成依赖注入,这是一个我们特别创建的特殊注解接口。我们在这个接口里创造不同的方法,而接口的参数是我们想注入依赖关系的类。

下面给出一个例子并告诉 Dagger 我们想要 MainActivity 类能够接受 ConnectivityManager(或者在图表里的其它依赖对象)。我们只要做类似以下的事:

@Singleton
@Component(modules = {ApplicationModule.class})
public interface ApplicationComponent {

  void inject(MainActivity activity);
}
正如我们所见,@Component 注解有几个参数,一个是所支持的模块的数组,代表它能提供的依赖。这里既可以是 Context 也可以是 ConnectivityManager,因为它们在 ApplicationModule 类中有声明。

用法

这时,我们要做的是尽快创建组件(比如在应用的 onCreate 阶段)并返回它,那么类就能用它来注入依赖了:

为了让框架自动生成 DaggerApplicationComponent,我们需要构建项目以便 Dagger 能够扫描我们的代码,并生成我们需要的部分。

MainActivity 里,我们要做的两件事是用 @Inject 注解符对想要注入的属性进行注解,调用我们在 ApplicationComponent 接口中声明的方法(请注意后面一部分会因我们使用的注入类型的不同而变化,但这里简单起见我们不去管它),然后依赖就被注入了,我们就能自由使用他们:

public class MainActivity extends AppCompatActivity {
  @Inject
  ConnectivityManager manager;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    ...
    ((App) getApplication()).getComponent().inject(this);
  }
}

总结

当然了,我们可以手动注入依赖,管理所有不同的对象,但 Dagger 消除了很多比如模板这样的“噪声”,给我们提供有用的附加品(比如 Singleton),而仅用 Java 处理将会很糟糕。


via: https://medium.com/di-101/di-101-part-1-81896c2858a0#.3hg0jj14o

作者:Roberto Orgiu 译者:GitFuture 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Webpack 2 一旦文档完成,就将结束 Beta 测试期。不过这并不意味着你现在不能开始使用第 2 版,前提是你知道怎么配置它。(LCTT 译注:Webpack 2.1 已经发布。)

Webpack 是什么

简单来说,Webpack 是一个 JavaScript 模块打包器。然而,自从它发布以来,它发展成为了你所有的前端代码的管理工具(或许是有意的,或许是社区的意愿)。

老式的任务运行器的方式:你的标记、样式和 JavaScript 是分离的。你必须分别管理它们每一个,并且你需要确保每一样都达到产品级

任务运行器 task runner ,例如 Gulp,可以处理许多不同的 预处理器 preprocesser 转换器 transpiler ,但是在所有的情景下,它都需要一个输入源并将其压缩到一个编译好的输出文件中。然而,它是在每个部分的基础上这样做的,而没有考虑到整个系统。这就造成了开发者的负担:找到任务运行器所不能处理的地方,并找到适当的方式将所有这些模块在生产环境中联合在一起。

Webpack 试图通过提出一个大胆的想法来减轻开发者的负担:如果有一部分开发过程可以自动处理依赖关系会怎样?如果我们可以简单地写代码,让构建过程最终只根据需求管理自己会怎样?

Webpack 的方式:如果 Webpack 了解依赖关系,它会仅捆绑我们在生产环境中实际需要的部分

如果你过去几年一直参与 web 社区,你已经知道解决问题的首选方法:使用 JavaScript 来构建。而且 Webpack 尝试通过 JavaScript 传递依赖关系使得构建过程更加容易。不过这个设计真正的亮点不是简化代码管理部分,而是管理层由 100% 有效的 JavaScript 实现(具有 Nodejs 特性)。Webpack 能够让你编写有效的 JavaScript,更好更全面地了解系统。

换句话来说:你不需要为 Webpack 写代码。你只需要写项目代码。而且 Webpack 就会持续工作(当然需要一些配置)。

简而言之,如果你曾经遇到过以下任何一种情况:

  • 载入有问题的依赖项
  • 意外引入一些你不需要在生产中用上的 CSS 样式表和 JS 库,使项目膨胀
  • 意外的两次载入(或三次)库
  • 遇到作用域的问题 —— CSS 和 JavaScript 都会有
  • 寻找一个让你在 JavaScript 中使用 Node/Bower 模块的构建系统,要么就得依靠一个令人发狂的后端配置才能正确地使用这些模块
  • 需要优化 资产 asset 交付,但你担心会弄坏一些东西

等等……

那么你可以从 Webpack 中收益了。它通过让 JavaScript 轻松处理你的依赖关系和加载顺序,而不是通过开发者的大脑。最好的部分是,Webpack 甚至可以纯粹在服务器端运行,这意味着你还可以使用 Webpack 构建渐进增强式网站。

第一步

我们将在本教程中使用 Yarn(运行命令 brew install yarn) 代替 npm,不过这完全取决于你的喜好,它们做同样的事情。在我们的项目文件夹中,我们将在终端窗口中运行以下代码,将 Webpack 2 添加到我们的全局软件包以及本地项目中:

yarn global add [email protected] [email protected]
yarn add --dev [email protected] [email protected]

我们接着会通过项目根目录的一个 webpack.config.js 文件来声明 webpack 的配置:

'use strict';

const webpack = require('webpack');

module.exports = {
  context: __dirname + '/src',
  entry: {
    app: './app.js',
  },
  output: {
    path: __dirname + '/dist',
    filename: '[name].bundle.js',
  },
};

注意:此处 __dirname 是指你的项目根目录

记住,Webpack “知道”你的项目发生了什么。它通过阅读你的代码来实现(别担心,它签署了保密协议 :D )。Webpack 基本上执行以下操作:

  1. context 文件夹开始……
  2. ……它查找 entry 下的文件名……
  3. ……并读取其内容。每一个 importES6)或 require()(Nodejs)的依赖会在它解析代码的时候找到,它会在最终构建的时候打包这些依赖项。然后,它会搜索那些依赖项以及那些依赖项所依赖的依赖项,直到它到达“树”的最底端 —— 只打包它所需要的,没有其它东西。
  4. Webpack 从 context 文件夹打包所有东西到 output.path 文件夹,使用 output.filename 命名模板来为其命名(其中 [name] 被替换成来自 entry 的对象的键)。

所以如果我们的 src/app.js 文件看起来像这样(假设我们事先运行了 yarn add --dev moment):

'use strict';

import moment from 'moment';
var rightNow = moment().format('MMMM Do YYYY, h:mm:ss a');
console.log( rightNow );

// "October 23rd 2016, 9:30:24 pm"

我们应该运行:

webpack -p

注意:p 标志表示“生产”模式,这会压缩输出文件。

它会输出一个 dist/app.bundle.js,并将当前日期和时间打印到控制台。要注意 Webpack 会自动识别 上面的 'moment' 指代的是什么(比如说,虽然如果你有一个 moment.js 文件在你的目录,默认情况下 Webpack 会优先考虑你的 moment Node 模块)。

使用多个文件

你可以通过仅仅修改 entry 对象来指定任意数量的 入口 entry / 输出点 output

打包多个文件

'use strict';

const webpack = require("webpack");

module.exports = {
  context: __dirname + "/src",
  entry: {
    app: ["./home.js", "./events.js", "./vendor.js"],
  },
  output: {
    path: __dirname + "/dist",
    filename: "[name].bundle.js",
  },
};

所有文件都会按照数组的顺序一起被打包成一个 dist/app.bundle.js 文件。

输出多个文件

const webpack = require("webpack");

module.exports = {
  context: __dirname + "/src",
  entry: {
    home: "./home.js",
    events: "./events.js",
    contact: "./contact.js",
  },
  output: {
    path: __dirname + "/dist",
    filename: "[name].bundle.js",
  },
};

或者,你可以选择打包成多个 JS 文件以便于分割应用的某些模块。这将被打包成 3 个文件:dist/home.bundle.jsdist/events.bundle.jsdist/contact.bundle.js

高级打包自动化

如果你将你的应用分割成多个 output 输出项(如果你的应用的一部分有大量你不需要预加载的 JS,这会很有用),你可能会重用这些文件的代码,因为它将分别解析每个依赖关系。幸运的是,Webpack 有一个内置的 CommonsChunk 插件来处理这个:

module.exports = {
  // …

  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: "commons",
      filename: "commons.bundle.js",
      minChunks: 2,
    }),
  ],

  // …
};

现在,在你的 output 文件中,如果你有任何模块被加载 2 次以上(通过 minChunks 设置),它会把那个模块打包到 common.js 文件中,然后你可以将其缓存在客户端。这将生成一个额外的请求头,但是你防止了客户端多次下载同一个库。因此,在很多情景下,这会大大提升速度。

开发

Webpack 实际上有自己的开发服务器,所以无论你是开发一个静态网站还是只是你的网站前端原型,它都是无可挑剔的。要运行那个服务器,只需要添加一个 devServer 对象到 webpack.config.js

module.exports = {
  context: __dirname + "/src",
  entry: {
    app: "./app.js",
  },
  output: {
    filename: "[name].bundle.js",
    path: __dirname + "/dist/assets",
    publicPath: "/assets",            // New
  },
  devServer: {
    contentBase: __dirname + "/src",  // New
  },
};

现在创建一个包含以下代码的 src/index.html 文件:

<script src="/assets/app.bundle.js"></script>

……在你的终端中运行:

webpack-dev-server

你的服务器现在运行在 localhost:8080。注意 script 标签里面的 /assets 是怎么匹配到 output.publicPath 的 —— 你可以随意更改它的名称(如果你需要一个 CDN 的时候这会很有用)。

Webpack 会热加载所有 JavaScript 更改,而不需要刷新你的浏览器。但是,所有 webpack.config.js 文件里面的更改都需要重新启动服务器才能生效。

全局访问方法

需要在全局空间使用你的函数?在 webpack.config.js 里面简单地设置 output.library

module.exports = {
  output: {
    library: 'myClassName',
  }
};

……这会将你打包好的文件附加到一个 window.myClassName 实例。因此,使用该命名空间,你可以调用入口文件的可用方法(可以在该文档中阅读有关此设置的更多信息)。

加载器

到目前为止,我们所做的一切只涉及 JavaScript。从一开始就使用 JavaScript 是重要的,因为它是 Webpack 唯一支持的语言。事实上我们可以处理几乎所有文件类型,只要我们将其转换成 JavaScript。我们用 加载器 loader 来实现这个功能。

加载器可以是 Sass 这样的预处理器,或者是 Babel 这样的转译器。在 NPM 上,它们通常被命名为 *-loader,例如 sass-loaderbabel-loader

Babel 和 ES6

如果我们想在项目中通过 Babel 来使用 ES6,我们首先需要在本地安装合适的加载器:

yarn add --dev babel-loader babel-core babel-preset-es2015

然后将它添加到 webpack.config.js,让 Webpack 知道在哪里使用它。

module.exports = {
  // …

  module: {
    rules: [
      {
        test: /\.js$/,
        use: [{
          loader: "babel-loader",
          options: { presets: ["es2015"] }
        }],
      },

      // Loaders for other file types can go here
    ],
  },

  // …
};

Webpack 1 的用户注意:加载器的核心概念没有任何改变,但是语法改进了。直到官方文档完成之前,这可能不是确切的首选语法。

/\.js$/ 这个正则表达式查找所有以 .js 结尾的待通过 Babel 加载的文件。Webpack 依靠正则检查给予你完全的控制权 —— 它不限制你的文件扩展名或者假定你的代码必须以某种方式组织。例如:也许你的 /my_legacy_code/ 文件夹下的内容不是用 ES6 写的,所以你可以修改上述的 test/^((?!my_legacy_folder).)\.js$/,这将会排除那个特定的文件夹,不过会用 Babel 处理其余的文件。

CSS 和 Style 加载器

如果我们只想为我们的应用所需加载 CSS,我们也可以这样做。假设我们有一个 index.js 文件,我们将从那里引入:

import styles from './assets/stylesheets/application.css';

我们会得到以下错误:你可能需要一个合适的加载器来处理这种类型的文件。记住,Webpack 只能识别 JavaScript,所以我们必须安装合适的加载器:

yarn add --dev css-loader style-loader

然后添加一条规则到 webpack.config.js

module.exports = {
  // …

  module: {
    rules: [
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
      },

      // …
    ],
  },
};

加载器以数组的逆序处理。这意味着 css-loader 会比 style-loader 先执行。

你可能会注意到,即使在生产版本中,这实际上是将你的 CSS 和 JavaScript 打包在一起,style-loader 手动将你的样式写到 <head>。乍一看,它可能看起来有点怪异,但你仔细想想就会发现这就慢慢开始变得更加有意义了。你已经节省了一个头部请求 —— 节省了一些连接上的时间。如果你用 JavaScript 来加载你的 DOM,无论如何,这从本质上消除了 FOUC

你还会注意到一个开箱即用的特性 —— Webpack 已经通过将这些文件打包在一起以自动解决你所有的 @import 查询(而不是依靠 CSS 默认的 import 方式,这会导致无谓的头部请求以及资源加载缓慢)。

从你的 JS 加载 CSS 是非常惊人的,因为你现在可以用一种新的强大的方式将你的 CSS 模块化。比如说你要只通过 button.js 来加载 button.css,这将意味着如果 button.js 从来没有真正使用过的话,它的 CSS 就不会膨胀我们的生产版本。如果你坚持面向组件的 CSS 实践,如 SMACSS 或 BEM,你会看到更紧密地结合你的 CSS 和你的标记和 JavaScript 的价值。

CSS 和 Node 模块

我们可以使用 Webpack 来利用 Node.js 使用 ~ 前缀导入 Node 模块的优势。如果我们运行 yarn add normalize.css,我们可以使用:

@import "~normalize.css";

……并且充分利用 NPM 来管理我们的第三方样式 —— 版本控制、没有任何副本和粘贴的部分。此外,让 Webpack 为我们打包 CSS 比起使用 CSS 的默认导入方式有明显的优势 —— 节省无谓的头部请求和加载时间。

更新:这一节和下面一节已经更新为准确的用法,不再使用 CSS 模块简单地导入 Node 的模块。感谢 Albert Fernández 的帮助!

CSS 模块

你可能听说过 CSS 模块,它把 CSS 变成了 SS,消除了 CSS 的 层叠性 Cascading 。通常它的最适用场景是只有当你使用 JavaScript 构建 DOM 的时候,但实质上,它神奇地将你的 CSS 类放置到加载它的 JavaScript 文件里(在这里了解更多)。如果你打算使用它,CSS 模块已经与 css-loader 封装在一起(yarn add --dev css-loader):

module.exports = {
  // …

  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          "style-loader",
          { loader: "css-loader", options: { modules: true } }
        ],
      },

      // …
    ],
  },
};

注意:对于 css-loader,我们现在使用 扩展对象语法 expanded object syntax 来给它传递一个选项。你可以使用一个更为精简的字符串来取代默认选项,正如我们仍然使用了 style-loader


值得注意的是,当允许导入 CSS 模块的时候(例如:@import 'normalize.css';),你完全可以删除掉 ~。但是,当你 @import 你自己的 CSS 的时候,你可能会遇到构建错误。如果你遇到“无法找到 \_\_\_\_”的错误,尝试添加一个 resolve 对象到 webpack.config.js,让 Webpack 更好地理解你的模块加载顺序。

const path = require("path");

module.exports = {
  //…

  resolve: {
    modules: [path.resolve(__dirname, "src"), "node_modules"]
  },
};

我们首先指定源目录,然后指定 node_modules。这样,Webpack 会更好地处理解析,按照既定的顺序(分别用你的源目录和 Node 模块的目录替换 "src""node_modules"),首先查找我们的源目录,然后再查找已安装的 Node 模块。

Sass

需要使用 Sass?没问题。安装:

yarn add --dev sass-loader node-sass

并添加新的规则:

module.exports = {
  // …

  module: {
    rules: [
      {
        test: /\.(sass|scss)$/,
        use: [
          "style-loader",
          "css-loader",
          "sass-loader",
        ]
      }

      // …
    ],
  },
};

然后当你的 Javascript 对一个 .scss.sass 文件调用 import 方法的时候,Webpack 会处理的。

CSS 独立打包

或许你在处理渐进增强的问题;或许你因为其它原因需要一个单独的 CSS 文件。我们可以通过在我们的配置中用 extract-text-webpack-plugin 替换 style-loader 而轻易地做到这一点,这不需要更改任何代码。以我们的 app.js 文件为例:

import styles from './assets/stylesheets/application.css';

让我们安装这个插件到本地(我们需要 2016 年 10 月的测试版本):

yarn add --dev [email protected]

并且添加到 webpack.config.js

const ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = {
  // …

  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          ExtractTextPlugin.extract("css"),
          { loader: "css-loader", options: { modules: true } },
        ],
      },

      // …
    ]
  },
  plugins: [
    new ExtractTextPlugin({
      filename: "[name].bundle.css",
      allChunks: true,
    }),
  ],
};

现在当运行 webpack -p 的时候,你的 output 目录还会有一个 app.bundle.css 文件。只需要像往常一样简单地在你的 HTML 中向该文件添加一个 <link> 标签即可。

HTML

正如你可能已经猜到,Webpack 还有一个 [html-loader][6] 插件。但是,当我们用 JavaScript 加载 HTML 时,我们针对不同的场景分成了不同的方法,我无法想出一个单一的例子来为你计划下一步做什么。通常,你需要加载 HTML 以便于在更大的系统(如 ReactAngularVueEmber)中使用 JavaScript 风格的标记,如 JSXMustacheHandlebars。或者你可以使用类似 Pug (以前叫 Jade)或 Haml 这样的 HTML 预处理器,抑或你可以直接把同样的 HTML 从你的源代码目录推送到你的构建目录。你怎么做都行。

教程到此为止了:你可以用 Webpack 加载标记,但是进展到这一步的时候,关于你的架构,你将做出自己的决定,我和 Webpack 都无法左右你。不过参考以上的例子以及搜索 NPM 上适用的加载器应该足够你发展下去了。

从模块的角度思考

为了充分使用 Webpack,你必须从模块的角度来思考:细粒度的、可复用的、用于高效处理每一件事的独立的处理程序。这意味着采取这样的方式:

└── js/
    └── application.js   // 300KB of spaghetti code

将其转变成这样:

└── js/
    ├── components/
    │   ├── button.js
    │   ├── calendar.js
    │   ├── comment.js
    │   ├── modal.js
    │   ├── tab.js
    │   ├── timer.js
    │   ├── video.js
    │   └── wysiwyg.js
    │
    └── application.js  // ~ 1KB of code; imports from ./components/

结果呈现了整洁的、可复用的代码。每一个独立的组件通过 import 来引入自身的依赖,并 export 它想要暴露给其它模块的部分。结合 Babel 和 ES6,你可以利用 JavaScript 类 来实现更强大的模块化,而不用考虑它的工作原理。

有关模块的更多信息,请参阅 Preethi Kasreddy 这篇优秀的文章


延伸阅读


via: https://blog.madewithenvy.com/getting-started-with-webpack-2-ed2b86c68783#.oozfpppao

作者:Drew Powers 译者:OneNewLife 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

这篇文章针对开发和部署基于 JavaScript 的应用(服务器端的 Node.js 或者客户端)的各种经验水平的开发者,通过本文,你将看到如何把 AWS SDK, Amazon Cognito Identity SDK 嵌入到 JavaScript 中,以及如何使用流行的 webpack 模块打包器。

2016 年 7 月, AWS 推出了 Amazon Cognito 用户库 user pool ,这个新特性极大的方便了开发者在移动和 Web 应用程序上添加注册和登录功能。为了让开发者更容易在自己的应用程序中实现用户库功能,我们也发布了用于 JavaScript 的 Amazon Cognito Identity SDK

Amazon Cognito 用户库可以让你在移动和 Web 应用程序上添加用户注册和登录功能更加容易。全托管用户库可以扩展到数以百万计的用户,你可以在每个 AWS 账户下有多重目录。创建一个用户库只需要几分钟的时间,并且你可以决定当一个新用户在你的应用程序或服务上注册时哪些属性(包括地址、邮箱、电话号码以及自定义属性)是强制的,哪些是可选择的。你的应用程序也可以指定所需的密码强度,指定用户需要进行多因素认证(MFA),以及通过电话号码或者邮件地址来验证新用户,从而进一步加强应用程序的安全性。

如果你是首次接触用于 JavaScript 的 Amazon Cognito Identity SDK,那么请先阅读这篇 AWS 文章作为开始。

为什么在 JavaScript 上使用 Asset 及模块捆绑的 Amazon Cogtito Identity SDK

今天,针对移动和桌面的现代 Web 应用程序都能为用户提供安全、快捷、灵敏以及类本地应用的体验。毫无疑问,现代的浏览器功能足够强大,能够肩负起大量可能的功能实现。许多流行的实现很大程度上依赖于 JavaScript 应用程序通过某种形式的 asset 封装和/或模块捆绑进行部署。这使得许多开发人员能够很好的维护自己的 JavaScript 应用程序,并且可以通过使用 script 标签创建一个或多个可以加载到客户端浏览器上的文件。

关于如何实现打包有许多争鸣,包括 GruntGulp 这样的 task runner,以及 Browserity 这样的打包器。然而,一个普遍的共识是, asset 打包不仅可以改善加载时间 - 它可以在确保可测试性和健壮性的前提下使你的应用程序模块化。

使用带有 Amazon Cognito Identity SDK 的 webpack 打包 JavaScript

我们接到了许多请求,要求我们提供如何在 webpack 环境下整合用于 JavaScript 的 Amazon Cognito Identity SDK 的更多细节。我们特地要求确保 webpack 正确管理一下第三方的依赖包:

通过这些例子,可以看到,下面这些 bower 库都被 bower.json 使用

"aws-cognito-sdk": "https://raw.githubusercontent.com/aws/amazon-cognito-identity-js/master/dist/aws-cognito-sdk.js",
"amazon-cognito-identity": "https://raw.githubusercontent.com/aws/amazon-cognito-identity-js/master/dist/amazon-cognito-identity.min.js",
"sjcl": "https://raw.githubusercontent.com/bitwiseshiftleft/sjcl/master/sjcl.js",
"jsbn": "https://raw.githubusercontent.com/andyperlitch/jsbn/master/index.js",

出于我们前面给出的关于 asset 打包对于开发过程的重要性的原因,除非你的应用程序非常小,否则使用像 webpack 这样的 asset 打包工具几乎总是必须的。当然,还有一个方法是可以通过使用标签简单的处理所有依赖关系。然而,这会污染全局命名空间,而且不能够提供最优的资源管理和加载方式。许多开发者首次使用的是具有标准 babel 加载器的标准 webpack.config.js 文件,像下面展示的这样。

{
  /** test for file ending in js or jsx 
   * exclude node_module and bower_components - we dont want to babel these 
   * use the babel loader 
   * apply the react and es2015 (es6) transformations **/

  test: /\.jsx?$/,
  exclude: /(node_modules|bower_components)/,
  loader: 'babel',
  query: {
    presets: ['react', 'es2015']
  }
}

需要重点记住的是,这个配置没有考虑一些第三方依赖关系,这些被用于 JavaScript 的 Amazon Cognito Identity SDK 所使用的第三方依赖目前没有使用 JavaScript 通用模块定义(UMD)

UMD 模式试图提供与 RequireJSCommonJS 这些当前最流行的脚本加载器的异步模块定义 [AMD] 的基本兼容。

这是 webpack 所依赖的模式,为了让 webpack 能够工作,我们必须进行一些改变。不做这些改变,你可能会遇到下面这些错误。

amazon-cognito-identity.min.js:19 Uncaught ReferenceError: BigInteger is not defined

这样一个错误可能会在调用 AWSCognito.CognitoIdentityServiceProvider.CognitoUser property authenticateUser 的时候出现。在这个错误例子中,我们可以利用 webpack 的 import 和 export 加载器的能力来解决这个错误。

使用 webpack 加载器

根据 [webpack 文档],“加载器允许你通过 require() 或 load 来预处理文件。加载器是一种类似其它构建工具中 “tasks” 的工具,它可以提供一个处理前端构建步骤的强大方法。加载器可以把一个文件从一种语言转化成另一种语言,比如把 CoffeeScript 转化成 JavaScript ,或者把图像转换为 data URL。“

为了解决 UMD 的兼容性缺乏的问题,你必须依赖两个具体的加载器, import 和 export 加载器。

使用 export 加载器

在使用用于 JavaScript 的 Amazon Cognito Identity SDK 的情况下,我们需要确保把 theAWSCognito 变量导出到 require /import 它们的模块的变量范围内(针对 ES6)。

{
  test: /aws-cognito-sdk\/index\.js/,
  loader: 'exports?AWSCognito'
}

在由 webpack 创建的捆绑中,使用 export 加载器会有导出模块方法的效果。因此, 在 require 和 import 后,AWSCognito 和 AWS 是可访问的(针对 ES6)。

var AWSCognito = require('aws-cognito-sdk')

/*** EXPORTS from export-loader ***/ 
module.exports = AWSCongito

这儿可以找到更多关于 export 加载器的信息。

使用 import 加载器

import 加载器主要用于把变量注入(import)到另一个模块的变量范围内。当第三方模块需要依赖全局变量的时候, import 加载器非常有用,比如针对 JavaScript 的 Amazon Cognito Identity SDK 需要依赖 BigInteger 或者 sjcl 的时候。

如果你不使用 webpack 加载器,下面这些内容会在一个捆绑中生成。

__webpack_require__(431);       // refers to jsbin
__webpack_require__(432);       // refers to sjcl

因为 jsbin 和 sjcl 都不能 export 任何东西,因此任何依赖于这些模块的调用都会导致一个错误。

为了解决这个问题,我们需要使用下面的 webpack 加载器配置:

{
  test: /amazon-cognito-identity\/index\.js/,
  loader: 'imports?jsbn,BigInteger=>jsbn.BigInteger,sjcl'
},
{
  test: /sjcl\/index\.js/,
  loader: 'imports?sjcl'
}

这个配置把下面的这些内容嵌入到了由 webpack 创建的捆绑中(此时是 bundle.js)。

/*** IMPORTS FROM imports-loader ***/
var jsbn = __webpack_require__(431);
var BigInteger = jsbn.BigInteger;
var sjcl = __webpack_require__(432);

结果,jsbn、BigInteger 和 sjcl 都被从它们各自的模块中导入到了用于 JavaScript 的 Amazon Cognito Identity SDK 中。

有关 import 加载器的更多信息可以在这儿找到。

下一步

我们鼓励你下载用于 JavaScript 的 Amazon Cognito Identity SDK 并开始构建你的应用,并在这篇文章的指导下通过 webpack 能够迅速掌握。

如果你有任何意见或问题,请在下面自由评论,也可以发邮件到 [email protected] 或者在这儿提出问题。

引用

这篇文章引用了下面这些第三方资源:


via: https://mobile.awsblog.com/post/Tx1A84CLMDJ744T/Using-webpack-with-the-Amazon-Cognito-Identity-SDK-for-JavaScript

作者:Marc Teichtahl 译者:ucasFL 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

最近我开始发力钻研 Python 的新 asyncio 模块。原因是我需要做一些事情,使用事件 IO 会使这些事情工作得更好,炙手可热的 asynio 正好可以用来牛刀小试。从试用的经历来看,该模块比我预想的复杂许多,我现在可以非常肯定地说,我不知道该如何恰当地使用 asyncio。

从 Twisted 框架借鉴一些经验来理解 asynio 并非难事,但是,asyncio 包含众多的元素,我开始动摇,不知道如何将这些孤立的零碎拼图组合成一副完整的图画。我已没有足够的智力提出任何更好的建议,在这里,只想分享我的困惑,求大神指点。

原语

asyncio 通过 协程 coroutines 的帮助来实现异步 IO。最初它是通过 yieldyield from 表达式实现的一个库,因为 Python 语言本身演进的缘故,现在它已经变成一个更复杂的怪兽。所以,为了在同一个频道讨论下去,你需要了解如下一些术语:

  • 事件循环
  • 事件循环策略
  • awaitable
  • 协程函数
  • 老式协程函数
  • 协程
  • 协程封装
  • 生成器 generator
  • future
  • 并发的future
  • 任务 task
  • 句柄
  • 执行器 executor
  • 传输 transport
  • 协议

此外,Python 还新增了一些新的特殊方法:

  • __aenter____aenter__,用于异步块操作
  • __aiter____anext__,用于异步迭代器(异步循环和异步推导)。为了更强大些,协议已经改变过一次了。 在 Python 3.5 它返回一个 awaitable(这是个协程);在 3.6它返回一个新的异步生成器。
  • __await__,用于自定义的 awaitable

你还需要了解相当多的内容,文档涵盖了那些部分。尽管如此,我做了一些额外说明以便对其有更好的理解:

事件循环

asyncio 事件循环和你第一眼看上去的略有不同。表面看,每个线程都有一个事件循环,然而事实并非如此。我认为它们应该按照如下的方式工作:

  • 如果是主线程,当调用 asyncio.get_event_loop() 时创建一个事件循环。
  • 如果是其它线程,当调用 asyncio.get_event_loop() 时返回运行时错误。
  • 当前线程可以使用 asyncio.set_event_loop() 在任何时间节点绑定事件循环。该事件循环可由 asyncio.new_evet_loop() 函数创建。
  • 事件循环可以在不绑定到当前线程的情况下使用。
  • asyncio.get_event_loop() 返回绑定线程的事件循环,而非当前运行的事件循环。

这些行为的组合是超混淆的,主要有以下几个原因。 首先,你需要知道这些函数被委托到全局设置的底层事件循环策略。 默认是将事件循环绑定到线程。 或者,如果需要的话,可以在理论上将事件循环绑定到一个 greenlet 或类似的。 然而,重要的是要知道库代码不控制策略,因此不能推断 asyncio 将适用于线程。

其次,asyncio 不需要通过策略将事件循环绑定到上下文。 事件循环可以单独工作。 但是这正是库代码的第一个问题,因为协同程序或类似的东西并不知道哪个事件循环负责调度它。 这意味着,如果从协程中调用 asyncio.get_event_loop(),你可能没有机会取得事件循环。 这也是所有 API 均采用可选的显式事件循环参数的原因。 举例来说,要弄清楚当前哪个协程正在运行,不能使用如下调用:

def get_task():
    loop = asyncio.get_event_loop()
    try:
        return asyncio.Task.get_current(loop)
    except RuntimeError:
        return None

相反,必须显式地传递事件循环。 这进一步要求你在库代码中显式地遍历事件循环,否则可能发生很奇怪的事情。 我不知道这种设计的思想是什么,但如果不解决这个问题(例如 get_event_loop() 返回实际运行的事件循环),那么唯一有意义的其它方案是明确禁止显式事件循环传递,并要求它绑定到当前上下文(线程等)。

由于事件循环策略不提供当前上下文的标识符,因此库也不可能以任何方式“索引”到当前上下文。 也没有回调函数用来监视这样的上下文的拆除,这进一步限制了实际可以开展的操作。

awaitable 与 协程 coroutine

以我的愚见,Python 最大的设计错误是过度重载迭代器。它们现在不仅用于迭代,而且用于各种类型的协程。 Python 中迭代器最大的设计错误之一是如果 StopIteration 没有被捕获形成的空泡。 这可能导致非常令人沮丧的问题,其中某处的异常可能导致其它地方的生成器或协同程序中止。 这是一个长期存在的问题,基于 Python 的模板引擎如 Jinja 经常面临这种问题。 该模板引擎在内部渲染为生成器,并且当由于某种原因的模板引起 StopIteration 时,渲染就停止在那里。

Python 慢慢认识到了过度重载的教训。 首先在 3.x 版本加入 asyncio 模块,并没有语言级支持。 所以自始至终它不过仅仅是装饰器和生成器而已。 为了实现 yield from 以及其它东西,StopIteration 再次重载。 这导致了令人困惑的行为,像这样:

>>> def foo(n):
...  if n in (0, 1):
...   return [1]
...  for item in range(n):
...   yield item * 2
...
>>> list(foo(0))
[]
>>> list(foo(1))
[]
>>> list(foo(2))
[0, 2]

没有错误,没有警告。只是不是你所期望的行为。 这是因为从一个作为生成器的函数中 return 的值实际上引发了一个带有单个参数的 StopIteration,它不是由迭代器协议捕获的,而只是在协程代码中处理。

在 3.5 和 3.6 有很多改变,因为现在除了生成器我们还有协程对象。除了通过封装生成器来生成协程,没有其它可以直接生成协程的单独对象。它是通过用给函数加 async 前缀来实现。 例如 async def x() 会产生这样的协程。 现在在 3.6,将有单独的异步生成器,它通过触发 AsyncStopIteration 保持其独立性。 此外,对于Python 3.5 和更高版本,导入新的 future 对象(generator_stop),如果代码在迭代步骤中触发 StopIteration,它将引发 RuntimeError

为什么我提到这一切? 因为老的实现方式并未真的消失。 生成器仍然具有 sendthrow 方法以及协程仍然在很大程度上表现为生成器。你需要知道这些东西,它们将在未来伴随你相当长的时间。

为了统一很多这样的重复,现在我们在 Python 中有更多的概念了:

  • awaitable:具有__await__方法的对象。 由本地协同程序和旧式协同程序以及一些其它程序实现。
  • 协程函数 coroutinefunction :返回原生协程的函数。 不要与返回协程的函数混淆。
  • 协程 coroutine : 原生的协程程序。 注意,目前为止,当前文档不认为老式 asyncio 协程是协程程序。 至少 inspect.iscoroutine 不认为它是协程。 尽管它被 future/awaitable 分支接纳。

特别令人困惑的是 asyncio.iscoroutinefunctioninspect.iscoroutinefunction 正在做不同的事情,这与 inspect.iscoroutineinspect.iscoroutinefunction 情况相同。 值得注意的是,尽管 inspect 在类型检查中不知道有关 asycnio 旧式协程函数的任何信息,但是当您检查 awaitable 状态时它显然知道它们,即使它与 **await** 不一致。

协程封装器 coroutine wrapper

每当你运行 async def ,Python 就会调用一个线程局部的协程封装器。它由 sys.set_coroutine_wrapper 设置,并且它是可以包装这些东西的一个函数。 看起来有点像如下代码:

>>> import sys
>>> sys.set_coroutine_wrapper(lambda x: 42)
>>> async def foo():
...  pass
...
>>> foo()
__main__:1: RuntimeWarning: coroutine 'foo' was never awaited
42

在这种情况下,我从来没有实际调用原始的函数,只是给你一个提示,说明这个函数可以做什么。 目前我只能说它总是线程局部有效,所以,如果替换事件循环策略,你需要搞清楚如何让协程封装器在相同的上下文同步更新。创建的新线程不会从父线程继承那些标识。

这不要与 asyncio 协程封装代码混淆。

awaitable 和 future

有些东西是 awaitable 的。 据我所见,以下概念被认为是 awaitable:

  • 原生的协程
  • 配置了假的 CO_ITERABLE_COROUTINE 标识的生成器(文中有涉及)
  • 具有 __await__ 方法的对象

除了生成器由于历史遗留的原因不使用之外,其它的对象都使用 __await__ 方法。 CO_ITERABLE_COROUTINE 标志来自哪里?它来自一个协程封装器(现在与 sys.set_coroutine_wrapper 有些混淆),即 @asyncio.coroutine。 通过一些间接方法,它使用 types.coroutine(现在与 types.CoroutineTypeasyncio.coroutine 有些混淆)封装生成器,并通过另外一个标志 CO_ITERABLE_COROUTINE 重新创建内部代码对象。

所以既然我们知道这些东西是什么,那么什么是 future? 首先,我们需要澄清一件事情:在 Python 3 中,实际上有两种(完全不兼容)的 future 类型:asyncio.futures.Futureconcurrent.futures.Future。 其中一个出现在另一个之前,但它们都仍然在 asyncio 中使用。 例如,asyncio.run_coroutine_threadsafe() 将调度一个协程到在另一个线程中运行的事件循环,但它返回一个 concurrent.futures.Future 对象,而不是 asyncio.futures.Future 对象。 这是有道理的,因为只有 concurrent.futures.Future 对象是线程安全的。

所以现在我们知道有两个不兼容的 future,我们应该澄清哪个 future 在 asyncio 中。 老实说,我不完全确定差异在哪里,但我打算暂时称之为“最终”。它是一个最终将持有一个值的对象,当还在计算时你可以对最终结果做一些处理。 future 对象的一些变种称为 deferred,还有一些叫做 promise。 我实在难以理解它们真正的区别。

你能用一个 future 对象做什么? 你可以关联一个准备就绪时将被调用的回调函数,或者你可以关联一个 future 失败时将被触发的回调函数。 此外,你可以 await 它(它实现__await__,因此可等待),此外,future 也可以取消。

那么你怎样才能得到这样的 future 对象? 通过在 awaitable 对象上调用 asyncio.ensure_future。它会把一个旧版的生成器转变为 future 对象。 然而,如果你阅读文档,你会读到 asyncio.ensure_future 实际上返回一个task(任务)。 那么问题来了,什么是任务?

任务

任务 task 某种意义上是一个封装了协程的 futur 对象。它的工作方式和 future 类似,但它也有一些额外的方法来提取所包含的协程的当前堆栈。 我们已经见过了在前面提到过的任务,因为它是通过 Task.get_current 确定事件循环当前正在做什么的主要方式。

在如何取消工作方面,任务和 future 也有区别,但这超出了本文的范围。“取消”是它们自己最大的问题。 如果你处于一个协程中,并且知道自己正在运行,你可以通过前面提到的 Task.get_current 获取自己的任务,但这需要你知道自己被派遣在哪个事件循环,该事件循环可能是、也可能不是已绑定的那个线程。

协程不可能知道它与哪个循环一起使用。task 也没有提供该信息的公共 API。 然而,如果你确实可以获得一个任务,你可以访问 task._loop,通过它反指到事件循环。

句柄

除了上面提到的所有一切还有句柄。 句柄是等待执行的不透明对象,不可等待,但可以被取消。 特别是如果你使用 call_soon 或者 call_soon_threadsafe(还有其它一些)调度执行一个调用,你可以获得句柄,然后使用它尽力尝试取消执行,但不能等待实际调用生效。

执行器 Executor

因为你可以有多个事件循环,但这并不意味着每个线程理所当然地应用多个事件循环,最常见的情形还是一个线程一个事件循环。 那么你如何通知另一个事件循环做一些工作? 你不能到另一个线程的事件循环中执行回调函数并获取结果。 这种情况下,你需要使用执行器。

执行器 Executor 来自 concurrent.futures,它允许你将工作安排到本身未发生事件的线程中。 例如,如果在事件循环中使用 run_in_executor 来调度将在另一个线程中调用的函数。 其返回结果是 asyncio 协程,而不是像 run_coroutine_threadsafe 这样的并发协程。 我还没有足够的心智来弄清楚为什么设计这样的 API,应该如何使用,以及什么时候使用。 文档中建议执行器可以用于构建多进程。

传输和协议

我总是认为传输与协议也凌乱不堪,实际这部分内容基本上是对 Twisted 的逐字拷贝。详情毋庸赘述,请直接阅读相关文档。

如何使用 asyncio

现在我们已经大致了解 asyncio,我发现了一些模式,人们似乎在写 asyncio 代码时使用:

  • 将事件循环传递给所有协程。 这似乎是社区中一部分人的做法。 把事件循环信息提供给协程为协程获取自己运行的任务提供了可能性。
  • 或者你要求事件循环绑定到线程,这也能达到同样的目的。 理想情况下两者都支持。 可悲的是,社区已经分化。
  • 如果想使用上下文数据(如线程本地数据),你可谓是运气不佳。 最流行的变通方法显然是 atlassian 的 aiolocals,它基本上需要你手动传递上下文信息到协程,因为解释器不为此提供支持。 这意味着如果你用一个工具类库生成协程,你将失去上下文。
  • 忽略 Python 中的旧式协程。 只使用 3.5 版本中 async def 关键字和协程。 你总可能要用到它们,因为在老版本中,没有异步上下文管理器,这是非常必要的资源管理。
  • 学习重新启动事件循环进行善后清理。 这部分功能和我预想的不同,我花了比较长的时间来厘清它的实现。清理操作的最好方式是不断重启事件循环直到没有等待事件。 遗憾的是没有什么通用的模式来处理清理操作,你只能用一些丑陋的临时方案糊口度日。 例如 aiohttp 的 web 支持也做这个模式,所以如果你想要结合两个清理逻辑,你可能需要重新实现它提供的工具助手,因为该助手功能实现后,它彻底破坏了事件循环的设计。 当然,它不是我见过的第一个干这种坏事的库 :(。
  • 使用子进程是不明显的。 你需要一个事件循环在主线程中运行,我想它是在监听信号事件,然后分派到其它事件循环。 这需要通过 asyncio.get_child_watcher().attach_loop(...) 通知循环。
  • 编写同时支持异步和同步的代码在某种程度上注定要失败。 尝试在同一个对象上支持 withasync with 是危险的事情。
  • 如果你想给一个协程起个更好的名字,弄清楚为什么它没有被等待,设置 __name__没有帮助。 你需要设置 __qualname__ 而不是打印出错误消息来。
  • 有时内部类型交换会使你麻痹。 特别是 asyncio.wait() 函数将确保所有的事情都是 future,这意味着如果你传递协程,你将很难发现你的协程是否已经完成或者正在等待,因为输入对象不再匹配输出对象。 在这种情况下,唯一真正理智的做法是确保前期一切都是 future。

上下文数据

除了疯狂的复杂性和对如何更好地编写 API 缺乏理解,我最大的问题是完全缺乏对上下文本地数据的考虑。这是 Node 社区现在学习的东西。continuation-local-storage 存在,但该实现被接受的太晚。持续本地存储和类似的概念常用于在并发环境中实施安全策略,并且该信息的损坏可能导致严重的安全问题。

事实上,Python 甚至没有任何存储,这令人失望至极。我正在研究这个内容,因为我正在调查如何最好地支持 Sentry's breadcrumbs 的 asyncio,然而我并没有看到一个合理的方式做到这一点。在 asyncio 中没有上下文的概念,没有办法从通用代码中找出您正在使用的事件循环,并且如果没有 monkeypatching(运行环境下的补丁),也无法获取这些信息。

Node 当前正在经历如何找到这个问题的长期解决方案的过程。这个问题不容忽视,因为它在所有生态系统中反复出现过,如 JavaScript、Python 和 .NET 环境。该问题被命名为异步上下文传播,其解决方案有许多名称。在 Go 中,需要使用上下文包,并明确地传递给所有 goroutine(不是一个完美的解决方案,但至少有一个)。.NET 具有本地调用上下文形式的最佳解决方案。它可以是线程上下文,Web 请求上下文或类似的东西,除非被抑制,否则它会自动传播。微软的解决方案是我们的黄金标准。我现在相信,微软在 15 年前已经解决了该问题。

我不知道该生态系统是否还够年轻,还可以添加逻辑调用上下文,可能现在仍然为时未晚。

个人感想

复杂的东西变得越来越复杂。 我没有随意使用 asyncio 的心智。它需要不断地更新所有 Python 语言的变化的知识,这很大程度上使语言本身变得复杂。 令人鼓舞的是,围绕着它的生态系统正在不断发展,只是不知道还需要几年的时间,才能带给开发者愉快和稳定的开发体验。

3.5 版本引入的东西(新的协程对象)非常棒。 特别是这些变化包括引入了一个合理的基础,这些都是我在早期的版本中一直期盼的。在我心中, 通过重载生成器实现协程是一个错误。 关于什么是 asyncio,我难以置喙。 这是一个非常复杂的事情,内部令人眼花缭乱。 我很难理解它工作的所有细节。你什么时候可以传递一个生成器,什么时候它必须是一个真正的协程,future 是什么,任务是什么,事件循环如何工作,这甚至还没有触碰到真正的 IO 部分。

最糟糕的是,asyncio 甚至不是特别快。 David Beazley 演示的它设计的 asyncio 的替代品是原生版本速度的两倍。 asyncio 巨复杂,很难理解,也无法兑现自己在主要特性上的承诺,对于它,我只想说我想静静。我知道,至少我对 asyncio 理解的不够透彻,没有足够的信心对人们如何用它构建代码给出建议。


作者:

Armin Ronacher

软件开发者和开源骨灰, Flask 框架的创造者。


via: http://lucumr.pocoo.org/2016/10/30/i-dont-understand-asyncio/

作者:Armin Ronacher 译者:firstadream 校对:jasminepeng

本文由 LCTT 原创编译,Linux中国 荣誉推出

编者注:本文是 2016 年 4 月 Nicole Whilte 在欧洲 GraphConnect 时所作。这儿我们快速回顾一下她所涉及的内容:

  • 图数据库推荐基础
  • 社会化推荐
  • 相似性推荐
  • 集群推荐

今天我们将要讨论的内容是数据科学和 图推荐 graph recommendations

我在 Neo4j 任职已经两年了,但实际上我已经使用 Neo4j 和 Cypher 工作三年了。当我首次发现这个特别的 图数据库 graph database 的时候,我还是一个研究生,那时候我在奥斯丁的德克萨斯大学攻读关于社交网络的统计学硕士学位。

实时推荐引擎是 Neo4j 中最广泛的用途之一,也是使它如此强大并且容易使用的原因之一。为了探索这个东西,我将通过使用示例数据集来阐述如何将统计学方法并入这些引擎中。

第一个很简单 - 将 Cypher 用于社交推荐。接下来,我们将看一看相似性推荐,这涉及到可被计算的相似性度量,最后探索的是集群推荐。

图数据库推荐基础

下面的数据集包含所有达拉斯 Fort Worth 国际机场的餐饮场所,达拉斯 Fort Worth 国际机场是美国主要的机场枢纽之一:

我们把节点标记成黄色并按照出入口和航站楼给它们的位置建模。同时我们也按照食物和饮料的主类别将地点分类,其中一些包括墨西哥食物、三明治、酒吧和烤肉。

让我们做一个简单的推荐。我们想要在机场的某一确定地点找到一种特定食物,大括号中的内容表示是的用户输入,它将进入我们的假想应用程序中。

这个英文句子表示成 Cypher 查询:

这将提取出该类别中用户所请求的所有地点、航站楼和出入口。然后我们可以计算出用户所在位置到出入口的准确距离,并以升序返回结果。再次说明,这个非常简单的 Cypher 推荐仅仅依据的是用户在机场中的位置。

社交推荐 Social Recommendations

让我们来看一下社交推荐。在我们的假想应用程序中,用户可以登录并且可以用和 Facebook 类似的方式标记自己“喜好”的地点,也可以在某地签到。

考虑位于我们所研究的第一个模型之上的数据模型,现在让我们在下面的分类中找到用户的朋友喜好的航站楼里面离出入口最近的餐饮场所:

MATCH 子句和我们第一次 Cypher 查询的 MATCH 子句相似,只是现在我们依据喜好和朋友来匹配:

前三行是完全一样的,但是现在要考虑的是那些登录的用户,我们想要通过 :FRIENDS_WITH 这一关系来找到他们的朋友。仅需通过在 Cypher 中增加一些行内容,我们现在已经把社交层面考虑到了我们的推荐引擎中。

再次说明,我们仅仅显示了用户明确请求的类别,并且这些类别中的地点与用户进入的地方是相同的航站楼。当然,我们希望按照登录并做出请求的用户来滤过这些目录,然后返回地点的名字、位置以及所在目录。我们也要显示出有多少朋友已经“喜好”那个地点以及那个地点到出入口的确切距离,然后在 RETURN 子句中同时返回所有这些内容。

相似性推荐 Similarity Recommendations

现在,让我们看一看相似性推荐引擎:

和前面的数据模型相似,用户可以标记“喜好”的地点,但是这一次他们可以用 1 到 10 的整数给地点评分。这是通过前期在 Neo4j 中增加一些属性到关系中建模实现的。

这将允许我们找到其他相似的用户,比如以上面的 Greta 和 Alice 为例,我们已经查询了他们共同喜好的地点,并且对于每一个地点,我们可以看到他们所设定的权重。大概地,我们可以通过他们的评分来确定他们之间的相似性大小。

现在我们有两个向量:

现在让我们按照 欧几里得距离 Euclidean distance 的定义来计算这两个向量之间的距离:

我们把所有的数字带入公式中计算,然后得到下面的相似度,这就是两个用户之间的“距离”:

你可以很容易地在 Cypher 中计算两个特定用户的“距离”,特别是如果他们仅仅同时“喜好”一个很小的地点子集。再次说明,这儿我们依据两个用户 Alice 和 Greta 来进行匹配,并尝试去找到他们同时“喜好”的地点:

他们都有对最后找到的地点的 :LIKES 关系,然后我们可以在 Cypher 中很容易的计算出他们之间的欧几里得距离,计算方法为他们对各个地点评分差的平方求和再开平方根。

在两个特定用户的例子中上面这个方法或许能够工作。但是,在实时情况下,当你想要通过和实时数据库中的其他用户比较,从而由一架飞机上的一个用户推断相似用户时,这个方法就不一定能够工作。不用说,至少它不能够很好的工作。

为了找到解决这个问题的好方法,我们可以预先计算好距离并存入实际关系中:

当遇到一个很大的数据集时,我们需要成批处理这件事,在这个很小的示例数据集中,我们可以按照所有用户的 迪卡尔乘积 Cartesian product 和他们共同“喜好”的地点来进行匹配。当我们使用 WHERE id(u1) < id(u2) 作为 Cypher 询问的一部分时,它只是来确定我们在左边和右边没有找到相同的对的一个技巧。

通过用户之间的欧几里得距离,我们创建了他们之间的一种关系,叫做 :DISTANCE,并且设置了一个叫做 euclidean 的欧几里得属性。理论上,我们可以也通过用户间的一些关系来存储其他相似度从而获取不同的相似度,因为在确定的环境下某些相似度可能比其他相似度更有用。

在 Neo4j 中,的确是对关系属性建模的能力使得完成像这样的事情无比简单。然而,实际上,你不会希望存储每一个可能存在的单一关系,因为你仅仅希望返回离他们“最近”的一些人。

因此你可以根据一些临界值来存入前几个,从而你不需要构建完整的连通图。这允许你完成一些像下面这样的实时的数据库查询,因为我们已经预先计算好了“距离”并存储在了关系中,在 Cypher 中,我们能够很快的攫取出数据。

在这个查询中,我们依据地点和类别来进行匹配:

再次说明,前三行是相同的,除了登录用户以外,我们找出了和他们有 :DISTANCE 关系的用户。这是我们前面查看的关系产生的作用 - 实际上,你只需要存储处于前几位的相似用户 :DISTANCE 关系,因此你不需要在 MATCH 子句中攫取大量用户。相反,我们只攫取和那些用户“喜好”的地方有 :DISTANCE 关系的用户。

这允许我们用少许几行内容表达较为复杂的模型。我们也可以攫取 :LIKES 关系并把它放入到变量中,因为后面我们将使用这些权重来评分。

在这儿重要的是,我们可以依据“距离”大小将用户按照升序进行排序,因为这是一个距离测度。同时,我们想要找到用户间的最小距离因为距离越小表明他们的相似度最大。

通过其他按照欧几里得距离大小排序好的用户,我们得到用户评分最高的三个地点并按照用户的平均评分高低来推荐这些地点。换句话说,我们先找出一个活跃用户,然后依据其他用户“喜好”的地点找出和他最相似的其他用户,接下来按照这些相似用户的平均评分把那些地点排序在结果的集合中。

本质上,我们通过把所有评分相加然后除以收集的用户数目来计算出平均分,然后按照平均评分的升序进行排序。其次,我们按照出入口距离排序。假想地,我猜测应该会有交接点,因此你可以按照出入口距离排序然后再返回名字、类别、出入口和航站楼。

集群推荐 Cluster Recommendations

我们最后要讲的一个例子是集群推荐,在 Cypher 中,这可以被想像成一个作为临时解决方案的离线计算工作流。这可能完全基于在欧洲 GraphConnect 上宣布的新方法,但是有时你必须进行一些 Cypher 2.3 版本所没有的算法逼近。

在这儿你可以使用一些统计软件,把数据从 Neo4j 取出然后放入像 Apache Spark、R 或者 Python 这样的软件中。下面是一段把数据从 Neo4j 中取出的 R 代码,运行该程序,如果正确,写下程序返回结果的给 Neo4j,可以是一个属性、节点、关系或者一个新的标签。

通过持续把程序运行结果放入到图表中,你可以在一个和我们刚刚看到的查询相似的实时查询中使用它:

下面是用 R 来完成这件事的一些示例代码,但是你可以使用任何你最喜欢的软件来做这件事,比如 Python 或 Spark。你需要做的只是登录并连接到图表。

在下面的例子中,我基于用户的相似性把他们聚合起来。每个用户作为一个观察点,然后得到他们对每一个目录评分的平均值。

假定用户对酒吧类评分的方式和一般的评分方式相似。然后我攫取出喜欢相同类别中的地点的用户名、类别名、“喜好”关系的平均权重,比如平均权重这些信息,从而我可以得到下面这样一个表格:

因为我们把每一个用户都作为一个观察点,所以我们必须巧妙的处理每一个类别中的数据,这些数据的每一个特性都是用户对该类中餐厅评分的平均权重。接下来,我们将使用这些数据来确定用户的相似性,然后我将使用 聚类 clustering 算法来确定在不同集群中的用户。

在 R 中这很直接:

在这个示例中我们使用 K-均值 k-means 聚类算法,这将使你很容易攫取集群分配。总之,我通过运行聚类算法然后分别得到每一个用户的集群分配。

Bob 和 David 在一个相同的集群中 - 他们在集群二中 - 现在我可以实时查看哪些用户被放在了相同的集群中。

接下来我把集群分配写入 CSV 文件中,然后存入图数据库:

我们只有用户和集群分配,因此 CSV 文件只有两列。 LOAD CSV 是 Cypher 中的内建语法,它允许你从一些其他文件路径或者 URL 调用 CSV ,并给它一个别名。接下来,我们将匹配图数据库中存在的用户,从 CSV 文件中攫取用户列然后合并到集群中。

我们在图表中创建了一个新的标签节点:Cluster ID, 这是由 K-平均聚类算法给出的。接下来我们创建用户和集群间的关系,通过创建这个关系,当我们想要找到在相同集群中的实际推荐用户时,就会很容易进行查询。

我们现在有了一个新的集群标签,在相同集群中的用户和那个集群存在关系。新的数据模型看起来像下面这样,它比我们前面探索的其他数据模型要更好:

现在让我们考虑下面的查询:

通过这个 Cypher 查询,我们在更远处找到了在同一个集群中的相似用户。由于这个原因,我们删除了“距离”关系:

在这个查询中,我们取出已经登录的用户,根据用户-集群关系找到他们所在的集群,找到他们附近和他们在相同集群中的用户。

我们把这些用户分配到变量 c1 中,然后我们得到其他被我取别名为 neighbor 变量的用户,这些用户和那个相同集群存在着用户-集群关系,最后我们得到这些附近用户“喜好”的地点。再次说明,我把“喜好”放入了变量 r 中,因为我们需要从关系中攫取权重来对结果进行排序。

在这个查询中,我们所做的改变是,不使用相似性距离,而是攫取在相同集群中的用户,然后对类别、航站楼以及我们所攫取的登录用户进行声明。我们收集所有的权重:来自附近用户“喜好”地点的“喜好”关系,得到的类别,确定的距离值,然后把它们按升序进行排序并返回结果。

在这些例子中,我们可以进行一个相当复杂的处理并且将其放到图数据库中,然后我们就可以使用实时算法结果-聚类算法和集群分配的结果。

我们更喜欢的工作流程是更新这些集群分配,更新频率适合你自己就可以,比如每晚一次或每小时一次。当然,你可以根据直觉来决定多久更新一次这些集群分配是可接受的。


via: https://neo4j.com/blog/real-time-recommendation-engine-data-science/

作者:Nicole White 译者:ucasFL 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

gcc 编译器提供了几乎数不清的命令行选项列表。当然,没有人会使用过或者精通它所有的命令行选项,但是有一些命令行选项是每一个 gcc 用户都应该知道的 - 即使不是必须知道。它们中有一些很常用,其他一些不太常用,但不常用并不意味着它们的用处没前者大。

在这个系列的文章中,我们集中于一些不常用但是很有用的 gcc 命令行选项,在第一节已经讲到几个这样的命令行选项。

不知道你是否能够回想起,在这个系列教程的第一部分的开始,我简要的提到了开发者们通常用来生成警告的 -Wall 选项,并不包括一些特殊的警告。如果你不了解这些特殊警告,并且不知道如何生成它们,不用担心,我将在这篇文章中详细讲解关于它们所有的细节。

除此以外,这篇文章也将涉及与浮点值相关的 gcc 警告选项,以及在 gcc 命令行选项列表变得很大的时候如何更好的管理它们。

在继续之前,请记住,这个教程中的所有例子、命令和指令都已在 Ubuntu 16.04 LTS 操作系统和 gcc 5.4.0 上测试过。

生成 -Wall 选项不包括的警告

尽管 gcc 编译器的 -Wall 选项涵盖了绝大多数警告标记,依然有一些警告不能生成。为了生成它们,请使用 -Wextra 选项。

比如,下面的代码:

#include <stdio.h>
#include <stdlib.h>
int main()
{
    int i=0;
    /* ...
       some code here 
       ...
    */

    if(i);
        return 1;
     return 0; 
}

我不小心在 if 条件后面多打了一个分号。现在,如果使用下面的 gcc 命令来进行编译,不会生成任何警告。

gcc -Wall test.c -o test

但是如果同时使用 -Wextra 选项来进行编译:

gcc -Wall -Wextra test.c -o test

会生成下面这样一个警告:

test.c: In function ‘main’:
test.c:10:8: warning: suggest braces around empty body in an ‘if’ statement [-Wempty-body]
 if(i);

从上面的警告清楚的看到, -Wextra 选项从内部启用了 -Wempty-body 选项,从而可以检测可疑代码并生成警告。下面是这个选项启用的全部警告标记。

  • -Wclobbered
  • -Wempty-body
  • -Wignored-qualifiers
  • -Wmissing-field-initializers
  • -Wmissing-parameter-type (仅针对 C 语言)
  • -Wold-style-declaration (仅针对 C 语言)
  • -Woverride-init
  • -Wsign-compare
  • -Wtype-limits
  • -Wuninitialized
  • -Wunused-parameter (只有和 -Wunused-Wall 选项使用时才会启用)
  • -Wunused-but-set-parameter (只有和-Wunused-Wall` 选项使用时才会生成)

如果想对上面所提到的标记有更进一步的了解,请查看 gcc 手册

此外,遇到下面这些情况, -Wextra 选项也会生成警告:

  • 一个指针和整数 0 进行 <<=>, 或 >= 比较
  • (仅 C++)一个枚举类型和一个非枚举类型同时出现在一个条件表达式中
  • (仅 C++)有歧义的虚拟基底
  • (仅 C++)寄存器类型的数组加下标
  • (仅 C++)对寄存器类型的变量进行取址
  • (仅 C++)基类没有在派生类的复制构建函数中进行初始化

浮点值的等值比较时生成警告

你可能已经知道,浮点值不能进行确切的相等比较(如果不知道,请阅读与浮点值比较相关的 FAQ)。但是如果你不小心这样做了, gcc 编译器是否会报出错误或警告?让我们来测试一下:

下面是一段使用 == 运算符进行浮点值比较的代码:

#include<stdio.h>

void compare(float x, float y)
{
    if(x == y)
    {
        printf("\n EQUAL \n");
    }
}

int main(void)
{
    compare(1.234, 1.56789);

    return 0; 
}

使用下面的 gcc 命令(包含 -Wall-Wextra 选项)来编译这段代码:

gcc -Wall -Wextra test.c -o test

遗憾的是,上面的命令没有生成任何与浮点值比较相关的警告。快速看一下 gcc 手册,在这种情形下可以使用一个专用的 -Wfloat-equal 选项。

下面是包含这个选项的命令:

gcc -Wall -Wextra -Wfloat-equal test.c -o test

下面是这条命令产生的输出:

test.c: In function ‘compare’:
test.c:5:10: warning: comparing floating point with == or != is unsafe [-Wfloat-equal]
 if(x == y)

正如上面你所看到的输出那样, -Wfloat-equal 选项会强制 gcc 编译器生成一个与浮点值比较相关的警告。

这儿是gcc 手册关于这一选项的说明:

这背后的想法是,有时,对程序员来说,把浮点值考虑成近似无限精确的实数是方便的。如果你这样做,那么你需要通过分析代码,或者其他方式,算出这种计算方式引入的最大或可能的最大误差,然后进行比较时(以及产生输出时,不过这是一个不同的问题)允许这个误差。特别要指出,不应该检查是否相等,而应该检查两个值是否可能出现范围重叠;这是用关系运算符来做的,所以等值比较可能是搞错了。

如何更好的管理 gcc 命令行选项

如果在你使用的 gcc 命令中,命令行选项列表变得很大而且很难管理,那么你可以把它放在一个文本文件中,然后把文件名作为 gcc 命令的一个参数。之后,你必须使用 @file 命令行选项。

比如,下面这行是你的 gcc 命令:

gcc -Wall -Wextra -Wfloat-equal test.c -o test

然后你可以把这三个和警告相关的选项放到一个文件里,文件名叫做 gcc-options

$ cat gcc-options&nbsp;
-Wall -Wextra -Wfloat-equal

这样,你的 gcc 命令会变得更加简洁并且易于管理:

gcc @gcc-options test.c -o test

下面是 gcc 手册关于 @file 的说明:

从文件中读取命令行选项。读取到的选项随之被插入到原始 @file 选项所在的位置。如果文件不存在或者无法读取,那么这个选项就会被当成文字处理,而不会被删除。

文件中的选项以空格分隔。选项中包含空白字符的话,可以用一个由单引号或双引号包围完整选项。任何字符(包括反斜杠: '\')均可能通过一个 '\' 前缀而包含在一个选项中。如果该文件本身包含额外的 @file 选项,那么它将会被递归处理。

结论

在这个系列的教程中,我们一共讲解了 5 个不常见但是很有用的 gcc 命令行选项: -Save-temps-g-Wextra-Wfloat-equal 以及 @file。记得花时间练习使用每一个选项,同时不要忘了浏览 gcc 手册上面所提供的关于它们的全部细节。

你是否知道或使用其他像这样有用的 gcc 命令行选项,并希望把它们在全世界范围内分享?请在下面的评论区留下所有的细节。


via: https://www.howtoforge.com/tutorial/uncommon-but-useful-gcc-command-line-options-2/

作者:Ansh 译者:ucasFL 校对:jasminepeng

本文由 LCTT 原创编译,Linux中国 荣誉推出