使用 Prisma GraphQL API

概览

Prisma API 基于 HTTP 协议。这意味着您可以使用任何您喜欢的 HTTP 工具/库与 Prisma API 进行通信。

以下是 createUser mutation 的 HTTP POST 请求的结构:

Header

  • Authorization:携带用于验证请求的 service token(前缀 Bearer); 仅在使用 service secret 部署服务时才需要。

  • Content-Type:指定请求正文(JSON)的格式,通常为 application/json

Body(JSON)

  • query:要发送到 API 的 GraphQL 操作; 请注意,尽管该字段被称为 query,但它也用于 mutations!

  • variables:一个 JSON 对象,包含在提交的 GraphQL 操作中定义的 query 中的变量。

此页面上的所有示例均基于具有以下 service 配置的 Prisma service:

prisma.yml

datamodel: datamodel.prisma
secret: my-secret-42

datamodel.prisma

type User {
  id: ID! @unique
  name: String!
}

curl

curl 是一个命令行工具,除此之外,它允许您向 URL 发送 HTTP 请求。

以下是使用 curl 发送 createUser mutation 到 Prisma API 的方法:

curl '__YOUR_PRISMA_ENDPOINT__' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer __YOUR_SERVICE_TOKEN__' \
--data-binary '{"query":"mutation ($name: String!) { createUser(data: { name: $name }) { id } }","variables":{"name":"Sarah"}}'

要试用此示例,请使用 Prisma service 中的相应值替换 __YOUR_PRISMA_ENDPOINT____YOUR_SERVICE_TOKEN__ 占位符,并将相应的代码段粘贴到终端中。

这相对于向 Prisma API 发送了以下的 mutation 请求:

mutation {
  createUser(data: {
    name: "Sarah"
  }) {
    id
  }
}

试试这个例子:

  1. 用 Prisma service 中的相应值替换 __YOUR_PRISMA_ENDPOINT____YOUR_SERVICE_TOKEN__占位符

  2. 将相应代码段粘贴到终端中,然后按Enter键

fetch(Node.JS)

fetch 允许您使用 JavaScrip t向 URL 发送 HTTP 请求。

以下是在 node 脚本中,使用 fetch 向 Prisma API 发送 createUser mutation 的方法:

const fetch = require('node-fetch')

const endpoint = '__YOUR_PRISMA_ENDPOINT__'

const query = `
mutation($name: String!) {
  createUser(data: {
    name: $name
  }) {
    id
  }
}
`

const variables = { name: 'Sarah' }

fetch(endpoint, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization:
      'Bearer __YOUR_SERVICE_TOKEN__',
  },
  body: JSON.stringify({ query: query, variables: variables }),
})
  .then(response => response.json())
  .then(data => console.log(JSON.stringify(data)))

这相当于向 Prisma API 发送了如下 mutation:

mutation {
  createUser(data: {
    name: "Sarah"
  }) {
    id
  }
}

试试这个例子:

  1. 用 Prisma service 中的相应值替换__YOUR_PRISMA_ENDPOINT____YOUR_SERVICE_TOKEN__占位符

  2. 将相应代码段存储在名为的 script.js 的 JavaScript 文件中

  3. script.js 同一目录下,运行 yarn add node-fetch 安装 node-fetch

  4. 使用以下 terminal 命令行执行脚本: node script.js

graphql-request

graphql-request 库是 fetch 上包装的一个轻量级库,您可以使用它来保存和编写样板代码。它主要用于需要与 GraphQL API 通信的脚本和较小的应用程序。

使用 request

request 不支持将 headers 传递给请求(尚未)。因此,此特定示例假定您的 Prisma 服务已部署,并且没有 service secret。

const { request } = require('graphql-request')

const query = `
mutation($name: String!) {
  createUser(data: {
    name: $name
  }) {
    id
  }
}
`

const variables = { name: 'Sarah' }

request('__YOUR_PRISMA_ENDPOINT__', query,  variables)
  .then(data => console.log(data))

试试这个例子:

  1. 用 Prisma service 中的相应值替换__YOUR_PRISMA_ENDPOINT____YOUR_SERVICE_TOKEN__占位符

  2. 将相应代码段存储在名为的 script.js 的 JavaScript 文件中

  3. script.js 同一目录下,运行库 yarn add graphql-request 命令安装 graphql-request

  4. 使用以下 terminal 命令行执行脚本: node script.js

使用 GraphQLClient

const { GraphQLClient } = require('graphql-request')

const client = new GraphQLClient('__YOUR_PRISMA_ENDPOINT__', {
  headers: {
    Authorization: 'Bearer __YOUR_SERVICE_TOKEN__',
  },
})

const query = `
mutation($name: String!) {
  createUser(data: {
    name: $name
  }) {
    id
  }
}
`

const variables = { name: 'Sarah' }

client.request(query, variables)
  .then(data => console.log(data))

试试这个例子:

  1. 用 Prisma service 中的相应值替换__YOUR_PRISMA_ENDPOINT____YOUR_SERVICE_TOKEN__占位符

  2. 将相应代码段存储在名为的 script.js 的 JavaScript 文件中

  3. script.js 同一目录下,运行库 yarn add graphql-request 命令安装 graphql-request

  4. 使用以下 terminal 命令行执行脚本:node script.js

GraphQL Playground

GraphQL Playground 是一个 GraphQL IDE,允许您向 GraphQL API 发送 queries,mutations 和 subscriptions。

您可以通过将终端,导航到 service 的 prisma.yml 所在目录,然后运行以下命令,来打开 Prisma API 的 Playground:

prisma playground

Apollo Client

Apollo Client 是一个复杂的 GraphQL client 库,常用于大型前端应用程序。虽然所有前面的例子中都使用的类似工具来发送 queries 和 mutations 一样,Apollo Client 暴露了用于发送 queries 和 mutations 的专用方法:querymutate

query 查询

const { ApolloClient } = require('apollo-boost')
const gql = require('graphql-tag')

const endpoint = 'https://eu1.prisma.sh/nikolas-burk/demodofin/dev'

const client = new ApolloClient({
  uri: endpoint
});

const query = gql`
  query {
    users {
      id
      name
    }
  }
`

client.query({
  query: query,
})
  .then(data => console.log(data))

mutate 变换

const { ApolloClient } = require('apollo-boost')
const gql = require('graphql-tag')

const endpoint = 'https://eu1.prisma.sh/nikolas-burk/demodofin/dev'

const client = new ApolloClient({
  uri: endpoint
});

const mutation = gql`
  mutation($name: String!) {
    createUser(data: {
      name: $name
    }) {
      id
    }
  }
  `

const variables = { name: 'Sarah' }

client.mutate({
  mutation: mutation,
  variables: variables
})
  .then(data => console.log(data))

参考链接:https://github.com/graphql/graphql-js

Prisma 服务及CLI

前言:本文主要讲述 Primsa 的场景用例,主要优点以及如何将它适配到您的技术栈中。

Prisma 和 GraphQL

Prisma 适用 GraphQL 作为通用数据库抽象,这意味着它将您的数据库转换为GraphQL API,使您能够:

  • 使用 GraphQL queries 和 mutations 来读写数据库

  • 使用 GraphQL subscriptions 来接收数据库事件的实时更新信息

  • 使用 GraphQL SDL 执行迁移和数据建模

当一个 Prisma client 发送请求到 Prisma server,它实际上会生成 GraphQL 操作,这些操作会被发送到 Prisma 的 GraphQL API。然后,client 会将 GraphQL 响应转换成所期望的数据结构,并从调用的方法返回它。

Prisma 服务

数据库的 GraphQL 映射由 Prisma 服务提供,每个服务都为数据库提供相应的 GraphQL CRUD 映射。GraphQL API 是自动生成的,并为所服务的 datamodel 中的每个 model 提供 CRUD 操作。

Prisma 服务在 Prisma server 上运行。Prisma server 可以配置为托管多个 Prisma 服务。

img

Prisma 服务使用两个组件配置:

  • prisma.yml : Prisma 服务的根配置文件(包括服务的端点、服务机密,数据模型文件的路径,……)

  • Datamodel: 在 datamodel 中,您会定义数据模型,Prisma 会使用该模型来为您的数据库生成 GraphQL API。它使用声明性 GraphQL SDL 语法,通常存储在一个名为 datamodel.prisma 的文件中。

一个极简的 prisma.yml 看起来像这样:

endpoint: http://localhost:4466
datamodel: datamodel.prisma
secret: mysecret42

其中:

  • endpoint : 应将服务部署到的 Prisma server 的HTTP endpoint。此端点对外暴露 service 的Prisma API。

  • datamodel : datamodel 文件(生成 GraphQL CRUD / realtime API 的基础)的路径配置

  • secret : 用于确保 service GraphQL API 端点的安全而设置的基于 JWT-based 签名校验的 service 秘钥。如果未设置 secret ,那么这个service 不需要权限校验就可以访问

Prisma CLI安装

可以从 NPM 仓库安装 Prisma CLI 。

NPM

npm install -g prisma

Yarn

yarn global add prisma

概要

$ prisma

GraphQL Database Gateway (https://www.prisma.io)

Usage: prisma COMMAND

Service:
  init            Initialize a new service
  deploy          Deploy service changes (or new service)
  introspect      Introspect database schema(s) of service
  info            Display service information (endpoints, cluster, ...)
  token           Create a new service token
  list            List all deployed services
  delete          Delete an existing service

Data workflows:
  playground      Open service endpoints in GraphQL Playground
  seed            Seed a service with data specified in the prisma.yml
  import          Import data into a service
  export          Export service data to local file
  reset           Reset the stage data

Cloud:
  login           Login or signup to the Prisma Cloud
  logout          Logout from Prisma Cloud
  console         Open Prisma Console in browser
  account         Display account information

Use prisma help [command] for more information about a command.
Docs can be found here: https://bit.ly/prisma-cli-commands

Examples:

- Initialize files for a new Prisma service
  $ prisma init

- Deploy service changes (or new service)
  $ prisma deploy

快速开始

安装完成后,执行以下命令让 Prisma API 启动并运行,然后便可以开始向其发送 queries 和 mutations :

prisma init hello-world
# Select a *demo server* from the interactive prompt
cd hello-world
prisma deploy
prisma playground

您现在可以开始向 Prisma API 发送 queries 和 mutations。有关更全面的演示,请查看“入门”部分。

graphql-config 的使用

Prisma CLI 集成了 graphql-config。如果您的项目使用 .graphqlconfig-file ,您可以使用 prisma 扩展并将其指向您的prisma.yml:

projects:
  prisma:
    schemaPath: prisma.graphql
    extensions:
      prisma: prisma.yml

为 CLI 命令行添加 HTTP 代理

Prisma CLI 支持自定义HTTP代理。当在公司防火墙后面时,这尤其重要。

要激活代理,请提供环境变量 HTTP_PROXYHTTPS_PROXY。该行为与 npm CLI 对该行为的处理非常相似。

可以提供以下环境变量:

  • HTTP_PROXY 或者 http_proxy:http 代理 URL,例如 http://localhost:8080

  • HTTPS_PROXY 或者 https_proxy:https 代理 URL,例如 https://localhost:8080

  • NO_PROXY 或者 no_proxy:要禁用某些 URLs 的代理,请为 NO_PROXY 提供一个 glob 通配符,如 *。

要获得简单的本地代理,您可以使用该 proxy 模块:

npm install -g proxy
DEBUG="*" proxy -p 8080
HTTP_PROXY=http://localhost:8080 HTTPS_PROXY=https://localhost:8080 prisma deploy

参考链接:https://github.com/graphql/graphql-js

GraphQL Prisma

本文将介绍 Prisma 的适用场景,拥有的优点以及如何将它融入到你的技术栈中。

Prisma 是什么 ?

Prisma 是在你的应用架构中用来替代传统 ORM 框架的数据层框架(data layer)。

该 data layer 由以下几个组件组成:

  • 扮演数据库代理角色的 Prisma server

  • 运行在 Prisma server 上的高性能的查询引擎(query engine),用于生成真实的数据库查询请求

  • 连接到 Prisma server 的客户端 Prisma client

  • 实时的事件系统,让你可以订阅相关的数据库事件

适用场景

在你处理各类数据库操作的场景中,在任何上下文里,Prisma 都是一个非常有用的工具。

创建 GraphQL 服务器

Prisma 是创建 GraphQL 服务器的完美工具。Prisma client 能够很好的兼容 Apollo 框架生态,拥有默认的对 GraphQL subscriptions 的支持,以及 Relay 风格的分页支持,同时提供端对端类型安全的以及内置的dataloader来解决 N+1 问题。

创建 REST APIs

Prisma 非常适合用来创建 REST APIs,主要用于取代传统的 ORM 框架。它拥有类型安全、先进API以及自由读写关系型数据等诸多优点。

CLIs 命令行, Scripts 脚本,Serverless Functions 及其它

Prisma 拥有极其自由的 API,让其非常适合处理各类使用场景。当你需要同一个或多分数据库进行会话时,Prisma 将会在数据库workflows简化方面提供巨大的帮助。

为什么用 Prisma?

简单的数据库工作流 database workflows

Prisma 的最终目标是去除你的应用中的复杂的通用数据库工作流同时简化数据库访问。

  • 类型安全的数据库访问,得益于已配置的自动生成的 Prisma client

  • 处理关系型数据以及事务的简单而强大的API

  • Prisma 同时对多个数据库的统一的访问,因此大大降低了跨数据库工作流程的复杂性

  • 数据库实时数据流以及事件系统,能够确保你能够获取到数据库中发生的所有重要事件的更新

  • 基于使用 GraphQL schema definition language(SDL)表示的声明式数据类型datamodel 的自动数据库迁移方案

  • 其它数据库工作流,如数据导入/导出等

实时的数据库层

一些数据库,例如 RethinkDB 或者 DynamoDB 提供了开箱即用的实时API。这样的API允许客户端订阅数据库中发生的任何更改。然而,绝大多数传统数据库不提供这样的实时API,并且手动实现它非常复杂。Prisma为每个受支持的数据库提供实时API,允许您订阅任何数据库事件,例如创建,更新或删除数据。

端到端安全性

以类型安全的方式编程是现代应用程序开发的默认设置。以下是安全类型的一些核心优势:

  • 信心:由于静态分析和编译时错误检查,开发人员可以对代码充满信心。

  • 开发体验:在明确定义数据类型时,开发人员开发体验更好。类型定义是IDE功能的基础,如智能自动补全或定义跳转。

  • 代码生成:在开发工作流程中利用代码生成很容易,以避免编写样板。

  • 跨系统协定:类型定义可以跨系统共享(例如,在客户端和服务器之间),并用作定义相应接口/ API的协定。

端到端类型安全是指从客户端到数据库在整个堆栈中具有类型安全性。端到端类型安全体系结构可能如下所示:

  • 数据库:Prisma提供强类型数据库层,datamodel定义了存储在数据库中的数据类型

  • 应用程序服务器:应用程序服务器定义自己的schema(例如,使用GraphQL或OpenAPI / Swagger),它可以重用或转换数据库中的数据类型。应用程序服务器需要使用类型安全的语言(例如TypeScript,Scala,Go)编写。

  • 客户端:了解应用程序服务器架构的客户端可以在构建时验证API请求和潜在响应。

整洁分层式架构

在开发应用程序服务器时,最复杂的是在同步,查询优化 / 性能和安全性等方面,实现安全且组织良好的数据库访问。当涉及多个数据库时,这变得更加复杂。

解决这个问题的一个常见解决方案是引入专用数据访问层(DAL),它将数据库访问的复杂性抽象出来。DAL的API由应用程序服务器使用,允许API开发人员只需简单地思考他们需要什么数据,而不必担心如何从数据库安全地和高效地检索它。

img

使用 DAL 架构可以确保关注点分离,从而提高代码的可维护性和复用性。具有某种数据库抽象(无论是简单的 ORM 库还是独立的基础架构组件)是小型应用以及大规模运行的应用的最佳实践。它确保应用服务器能够以安全而高效的方式与您的数据库进行通信。

Prisma 是一个自动生成的 DAL,它遵循了其它行业领先的DAL(例如 Twitter 的 Strato 或 Facebook 的 TAO)相同的原则,同时可以轻易的在小型的应用中使用。

Prisma 让你在一开始就使用干净的体系结构启动项目,并让你免于编写大量应用服务器和数据库通信的模块。

如何让 Prisma 融入到您的技术栈中?

Prisma 是一个位于数据库之上的独立基础架构组件。您可以在应用服务器中使用 Prisma client(各种类型的语言支持)来连接到 Prisma。

这使您可以通过简单而流行的API与数据库通信,并确保高性能和安全的数据库访问。

GraphQL 浅析

官方定义:GraphQL 是一种可以与任何后端服务相关联的查询语言以及对应的执行引擎。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,这也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。

GraphQL 能做什么?有什么优势

GraphQL 可以让服务调用者通过发送 GraphQL 查询语句(get 或 post 请求),来定制服务 API 接口的返回内容,而不会产生冗余数据。同时,多个数据查询请求可以被组合成一条 GraphQL 查询语句,最终只发送一次请求就可以获取页面需要的所有数据。

相比于 Restful 的优势:

  • 定制接口的返回内容,不产生冗余数据

  • 合并多个查询请求,减少请求次数

  • 通过上述两个特性,可以大幅减少接口访问所产生的流量

  • GraphQL 的接口支持嵌套(类似于数据库关系查询),可以方便的进行数据的聚合操作

GraphQL 基本概念

GraphQL type system 是在 GraphQL Schema 文件中定义的类型系统,它描述了返回的对象的类型与结构,是 GraphQL 实现的核心组成部分。

以博客系统为例,基础的 type 有 User 和 Post,分别代表用户和帖子。那么对于 User,一般会有一个字段叫 name,Post 可能会有两个字段 title 和 content。那么对于这样一个场景,使用 GraphQL Schema 的语法来描述它:

# 用户信息
type User {
    name: String
}

# 帖子信息
type Post {
    title: String
    content: String
}

通过这种方式我们可以描述类型系统的基本结构,同时它会在后台数据和 type system 之间建立映射关系。例如,我们调用了某个后台服务接口后返回了JSON数据 {“name”: “June”},我们指定了使用 Type User 来解析该数据,那么对应的数据和 Type 之间会建立映射关系,当数据不满足指定的格式时,如 {“title”: “Hello World!”},GraphQL 会提示报错信息。

上述 Schema 还可以利用相关特性继续扩展:

# 用户信息
type User {
    id: String!              # !表示该字段不能为空
    name: String
    posts: [Post]            # 可以通过 [Type] 来定义元素类型为某种 Type 的数组
    role: Role               # 可以使用枚举 enum
}

# 帖子信息
type Post {
    id: String!
    title: String
    content: String
    auther: User             # 可以通过 Type 来指定字段类型为某种 Type
}

# 枚举
enum Role { customer, master, admin }

至此,我们要完成这个 Schema type 定义文件的内容还差最后一步:定义进入 type system 的切入点。

我们需要定义一个 type,它是所有 query 查询的基础。该 type 的名称一般为 Query,它描述了我们公共的顶级 API。对应于上述场景,它的定义可能是这样的:

type Query {
    me: User
    user(id: String!): User
    post(id: String!): Post
}

上述代码中,我们描述了我们的三个顶级的 API 操作:

  • me:返回当前登录的用户的信息

  • user:根据传入的 id 参数,查询该 id 对应的用户的信息

  • post:根据传入的 id 参数,查询该 id 对应的帖子信息

上述代码也展示了 type system 的另一个功能,可以为字段配置参数来指定其行为。

当我们将整个 type system 打包到一起时,将 Query 上的 type 定义为查询的入口点,就创建了一个 GraphQL Schema。

更多

请查看相关文档

GraphQL Demo

此处,我们将按照上面的数据模型,来一步步构造一个 GraphQL Demo 项目,帮助开发者了解如何使用 GraphQL 技术来开发项目。

首先创建一个文件夹,并在其内部执行 npm init,初始化一个 node 项目:

mkdir graphql-demo
cd graphql-demo
npm init

安装 graphql-yoga 库,该库是一个 GraphQL server 库(基于 express-graphql),支持非常多的特性,此处重点讲述如何使用它起 demo 项目,就不作赘述。

npm install graphql-yoga -S

创建 src 文件夹,并在其内部新建我们的服务器代码入口 js 文件 index.js,将以下代码复制到 index.js 中:

const { GraphQLServer } = require('graphql-yoga');

const typeDefs = `
type Query {
    me: User
    user(id: String!): User
    post(id: String!): Post
}

type User {
    id: String!
    name: String
    posts: [Post]
    role: Role
}

type Post {
    id: String!
    title: String
    content: String
    auther: User
}

enum Role { customer, master, admin }
`;

const usersList = [
    {
        id: '1',
        name: 'June',
        role: 'admin'
    },
    {
        id: '2',
        name: 'Jim',
        role: 'customer'
    }
];

const postsList = [
    {
        id: "1001",
        title: "Hello World!",
        content: "Hello world, June~"
    },
    {
        id: "1002",
        title: "GraphQL 入门",
        content: "GraphQL 入门知识点"
    }
];

const resolvers = {
    Query: {
        me: (_, args) => Object.assign(usersList[0], {posts: postsList}),

        user: (_, { id }) => usersList.find(item => item.id === id) || null,

        post: (_, { id }) => postsList.find(item => item.id === id),

    },
};

const server = new GraphQLServer({
    typeDefs,
    resolvers
});

server.start(() => console.log('Server is running on localhost:4000'));

在这段代码中:

  • typeDefs 变量对应了上述内容中的 Schema 定义,内部定义了 type Query\User\Post,在 Query 中定义了 me、user、post 三个顶级接口

  • usersList 以及 postsList 变量保存了我们的应用数据,此处我们使用了写死的数据,实际开发的时候,这些数据往往是从数据库中读取的

  • resolvers 变量中,我们定义了 Query 中三个接口的解析器函数,用于处理这些接口调用的业务逻辑与返回值

  • 然后我们将 typeDefs 变量和 resolvers 变量传入到 GraphQLServer 实例中,并在 4000 端口启动对应的 Server 以及 GraphQL Playground 面板

执行 node ./src/index.js 命令,便可启动对应的 node GraphQL 服务器,然后在浏览器中打开 localhost:4000 页面,会显示 GraphQL Playground 面板,在此处便可以测试前文中叙述的各类 query 请求:

Playground 面板中可以做如下操作:

  • 在左侧的 Query 面板中执行查询语句,点击中间的运行按钮执行查询,在右侧的结果栏中获得得到的结果数据

  • 点击右侧的 SCHEMA 绿色按钮,可以查看对应的后台服务的接口以及 type 类型信息,调试非常方便

  • 下方的 QUERY VARIABLES,可以向 query 的传参,具体用法见上文

  • 下发的 HTTP HEADERS 可以向查询请求的 headers 中添加相应的属性,格式为 JSON 格式

  • 左侧的 Query 面板中输入查询语句时,会作格式校验,若不符合 GraphQL 规范,则会有红线提示

前端开发中的聚合

什么是聚合

所谓条条大路通罗马,但如果让我来设计通向罗马的各种大路,我至少会做两件事情:

① 让罗马只有一个入口

② 让罗马只有一个出口

这样做的好处是,无论你路从哪来,我可以统一在入口处给你打上各种标志,我也可以在你离开罗马时给你留点纪念。当然罗马自然不只一个出口入口,但是每个出口入口一定有一套相同的规定,否则就会出问题。

具体到当今的工作场景,高速公路又是一个汇总聚合的好例子,进入高速公路时候得经过收费站做点标志,离开时候也会做点操作,如果没有这种聚合,不论是缴费工作,流量统计或者其他都是无法统计的。

正常生活中有各种聚合的行为,我们会发现,聚合虽然会让效率变低,但却可以更好的管理,同样的道理是可以应用到前端乃至整个程序开发的,今天我们就来聊一聊前端中的各种聚合。

一个聚合的例子

前面我们说了工作中聚合带来的各种好处(坏处是面向用户会增加成本),而我们前端开发中会有哪些聚合呢?

一般来说,对于前端,请求聚合即是ajax的聚合,而经常有朋友会问我一些问题:重复的请求如何让他第二次不请求呢?

其实解决这个问题很简单的一个方案就是对ajax进行聚合处理:

1 let ajaxProxy = (params) => {
2     //做一些额外的工作,比如处理params参数
3     httpPost(params)
4 };

这里的处理办法就是统一在底层篡改ajax success回调,对数据做一层处理,然而对请求接口进行聚合的好处远远不止于此。

封装请求参数

首先,我们可以对每个请求的请求参数在底层加入额外参数,比如我们与server端约定,每次请求我们都会额外带一个head参数,会携带一些非业务公共数据:

1 head: {
2     channel: 'webapp', //渠道标志
3     version: '2.2.0', //版本信息
4     ct: 3, //平台信息
5     extend: null//可能需要的扩展信息
6 

每一个请求如果额外带这些信息的话,可以解决很多问题:

① server端知道当前请求来源于哪个渠道(SEM渠道、微信流量入口、搜索流量入口……)、哪一个版本、哪一个平台(iOS、Android、H5),可能Server就能对这个请求做定制化处理了

② 协助KPI考核,比如市场人员要推广自己的产品,而后台要统计他今天成功推广多少单,就会为这个用户生成一个二维码,具体的url是这样的:

http://domain.com?channel=xxx

那么,我的没一个请求(包括生成订单)都将把channel字段发给Server端,Server如果存于数据库,每天就能很简单生成所有用户的订单完成量

③ SEM渠道是一大流量来源(买搜索关键词),如果我们想拿到每一个关键词对我们系统每一个页面的访问量的话,也可以在这种公共请参数做处理

④ 根据以上功能,我们甚至可以根据这些特性配合通用的统计平台建立初略的前端漏斗模型

统一数据处理

一般来说,每个请求接口,server端返回的数据有一固定格式:

{
2   data: {},//真实数据
3   errno: 0,//错误码
4   msg: "success"//信息
5 }

正常的逻辑我们只需要处理data数据即可,而错误码不为0的情况,我们多是弹一个toast提示msg错误信息,所以我们会统一修改请求的回调,当然也会对一些错误码做特殊处理(未登陆、未授权),比如这样:

//统一处理请求返回数据
 2 const commonDataHandler = function (data) {
 3     //记录请求返回
 4     if (!data) {
 5         Toast('服务器出错,请稍候再试');
 6         return;
 7     }
 8     if (_.isString(data)) data = JSON.parse(data);
 9     //正常情况,不执行其它逻辑
10     if (data.errcode === 0) return true;
11 
12     //处理请求未登陆的特殊情况
13     if (data.errcode == ERROR_CODE['NOT_LOGIN']) {
14         showToast(data.errmsg, function () {
15           //执行统一逻辑,跳到登陆页面,要求登陆成功后跳回来
16         });
17         return false;
18     }
19     //处理其它需要特殊处理的错误码,需要业务开发对接口做定制化,将处理逻辑写到具体页面
20     if(this.errCodeCallback[data.errcode]) {
21         this.errCodeCallback[data.errcode](data.errcode, data.errmsg, data);
22         return false;
23     }
24     //通用错误处理,直接弹出toast
25     if (window.APP && data && data.errmsg) window.APP.showToast(data.errmsg, this.errorCallback);
26     return false;
27 };

当然,最开始说的重复请求不再请求也可以做到这个地方,但是具体操作会有很多细节点需要考虑。

不使用ajax

因为hybrid模式的出现,前端除了ajax外,可能会有更多的选择,比如在Native容器中,前端便不使用ajax发出请求,直接由Native代理发出,如果请求没做封口的话,便需要改动所有的业务代码,这个是十分不科学的。

结语

这里仅仅是提一个小小的点想向各位说明程序聚合的重要性,其实我们程序中很多细小的点皆需要做聚合处理。

localstorage使用聚合

我们前面说过如果不想重复请求便需要使用缓存技术,对应到前端是localstorage,无论何时,我们使用缓存都必须考虑如何更新和缓存过期问题,这个时候我们需要对齐聚合。

跳转聚合

在做单页应用时,我们为了不破坏路由,需要对跳转做聚合,我们甚至需要对window.location这种跳转做聚合处理,得封装为一个函数。

关于实际工作中的聚合的例子太多了,细小入setTimeout的聚合,事件机制的聚合,大到账号体系、钱包体系等的聚合处理,我们在实际工作中应该具备这种聚合的思想。

前端构建工具—dawn

介绍使用

参考:Dawn

原理

dawn 是一个采用中间件技术实现的轻量的任务流协调器,类似于gulp, grunt。dawn 本身并不处理任务,转而交由中间件承担这一职能,如同 gulp, grunt 插件。在实现上,dawn 是 webpack 出台后的产物,就不需要像 gulp, grunt 那样关注任务流的始点 —— 文件位置,而更容易聚焦于任务的分解,将编译、压缩作业交给 dn-middleware-webpack 中间件。dawn 的中间件实现机制如同 koa,使用 next 引用下一个中间件串联任务流,以继承事件模型的 ctx 实例作为上下文。其核心代码如下:

class Context extends EventEmitter {
  async _execQueue(middlewares, args, onFail) {
    const middleware = middlewares.shift();
    if (!middleware) return;

    // this.load 安装中间件,并执行中间件的外层函数,获得实际的任务逻辑 handler
    const handler = await this.load(middleware);

    const next = (args) => {
      // 若返回真值,在 watch 状态下,也只执行一次
      if (next.__result) return next.__result;

      next.__result = this._execQueue(middlewares, args, onFail)
        .catch(err => onFail(err));
      return next.__result;
    };
    return handler.call(this, next, this, args);
  }
}

常用中间件介绍

1. dn-middleware-webpack

概述:基于 webpack3 实现的中间件,打包模块。本地开发模式也将打包模块,而不是读取 webpack 缓存数据。
– dn-middleware-webpack 在回调中执行后续中间件的处理逻辑。
– 通过 vmodule-webpack-plugin 插件将 config.yml 类配置文件注入为可以 import 引入的虚拟模块。
– ctx 中添加 webpack 属性,即 webpack 类库。
事件:
– ‘webpack.opts’,可用于修改 opts 配置项,参数 opts。
– ‘webpack.config’,可用于修改注入 webpack 的 config 配置,参数 config, webpack, opts。
– ‘webpack.compiler’,操纵 webpack 的编译器,参数 compiler。
– ‘webpack.stats’,可用于监控编译状态,参数 stats。

2. dn-middleware-server

概述:基于 nokit,启动本地服务。首次执行时将在项目空间创建 server.yml 配置文件。
– 在 nokit 服务器中设置拦截器,通过 httpProxy 转发请求。
– ctx 中添加 server 属性,即 nokit.Server 实例;以及 httpServer 属性,即 server.httpServer。

事件:
– ‘server.init’,服务未启动时事件,参数 server 实例。
– ‘server.start’,服务启动成功时事件,参数 server 实例。

3. dn-middleware-dll

概述:独立构建项目依赖,节省打包时间。

  • 借助 ctx.exec 方法执行 webpack 中间件,打包项目的依赖,默认存放在工程目录 .cache 文件夹内。子文件夹名基于项目所使用的依赖通过 md5 生成散列,以便在依赖更新时重新打包。再借助 ctx.exec 方法执行 copy 中间件,将打包文件拷贝到 build/js 文件夹内。
  • 通过 ‘webpack.config’ 事件,在 webpackConfig 插件中注入 webpack.DllReferencePlugin 插件。
4. dn-middleware-faked

概述:基于 faked 提供数据模拟服务。

  • 基于 faked 创建 gui server 服务器,配置的模拟数据将输出到工程目录 mock 文件夹中 index.js, gui.data.json。
  • 模拟数据文件最终将作为 webpack 入口文件,以此实现远程请求的拦截,并实现热更新。
  • 执行逻辑被封装为 ctx.faked.apply 方法,在 dn-middleware-webpack 中执行,两个中间件耦合度较高。
5. dn-middleware-i18n

概述:将工程目录中 locales 语言包加载为 $locales 模块,通过 $i18n 获取指定模块,实现国际化。

  • 使用 confman.webpackPlugin 方法将工程目录中的语言包输出为 $locales 虚拟模块。
  • 使用 vmodule-webpack-plugin 类库输出 $i18n 虚拟模块,以获取指定文案。
6. 其他
  • dn-middleware-clean,清理文件或目录,可用 opts.target 加以配置,默认清理 ‘./build//.’。
  • dn-middleware-copy,复制文件。选项 from 查询源文件的文件夹路径,默认 ‘./from’; to 目标文件夹路径; log 是否打印日志; dot 源文件匹配规则是否支持 ‘.’ 起始; direction 影响映射 key 键指代源文件还是目标文件; files 源文件和目标文件映射,目标文件路径支持占位符 {index} 替换(index 自右而左),或使用源文件路径(映射中,目标文件以 ‘/’ 结尾)。
  • dn-middleware-browser-sync 基于 ‘browser-sync’ 监听打包文件变更,借助 ‘connect-browser-sync’ express中间件实现热更新。选项 files 配置监听的文件,默认 [‘./build//.’];port 为 ‘browser-sync’ 服务启动端口。
  • dn-middleware-git-sync,git 操作,包含 commit, push 动作(push 又区分日常和预发环境)。
  • dn-middleware-jcs,在 babel-loader 中添加 ‘jsx-control-statements’ 插件,以使 jsx 可使用结构控制语句,同时 lint 阶段也会作代码检查,参考 通过 JSX Control Statements 编写 JSX。
  • dn-middleware-lint,使用 eslint 命令作语法检查。
  • dn-middleware-tslint,对 typescript 进行语法检查。
  • dn-middleware-typedoc,使用 typedoc 为 typescript 项目生成文档。
  • dn-middleware-typescript,在 webpackConfig 中添加 awesome-typescript-loader,支持编译 tsx 模块。选项 declaration 是否分离 ts, js 文件,默认分离,false 时不分离。
  • dn-middleware-pkginfo,更新项目 package.json 中的 name, version, description 信息。
  • dn-middleware-prepush,在 .git/hooks/pre-push 添加 shell 命令,推送前执行 dn build 命令。
  • dn-middleware-sensitive-path,在 webpackConfig 中添加 ‘case-sensitive-paths-webpack-plugin’ 插件,使 mac, windows 引入模块时严格区分模块的大小写。
  • dn-middleware-shell,调用 ctx.utils.exec 执行 shell 命令。选项 script 命令内容;wscript 为 windows 系统下命令内容;async 是否异步执行,默认同步。
  • dn-middleware-watch,基于 ‘chokidar’ 监听执行文件变更。选项 match 匹配的文件,默认为 ‘./src//.’;event 监听的事件类型;script 事件发生后执行的脚本;onChange 事件发生后执行的动作。
  • dn-middleware-unit,基于 ‘mocha’ 对 ./test/unit 文件夹中内容作单元测试。

简单、可扩展的状态管理—Mobx

Mobx和Redux相似,是状态管理工具。Mobx本身和React无关,但也提供了React绑定库。与Redux相比,Mobx更像是为React而生。

什么时候会更适合用到mobx

  1. Domain分层清晰
  2. 简单清晰的page级组件通信场景
  3. 厌倦redux繁琐的reacter

使用Mobx最大的不同就是将状态从组件中抽离出来单独管理,这也正是状态管理库的初衷。

首先导入模块:

import { observer } from 'mobx-react'
import { observable, computed, action } from 'mobx'

创建状态类,再由类生成对象:

class Data {
  @observable num1 = 0
  @observable num2 = 0
  @computed get sum() {
    return this.num1 + this.num2
  }
}

const store = new Data()

因为装饰器用于类,所以首先定义类,再实例化出对象。这里的store不像Redux中的store,拥有一些内置方法。Mobx中的action不需要dispatch。

@observable装饰器标识一个可监听的对象,@computed装饰器会自动计算监听对象的动作,与Vue.js的计算属性有相似之处。

在 React 中使用 Mobx 需要用到 mobx-react。其提供了 Provider 组件用来包裹最外层组件节点,并且传入 store(通过)context 传递给后代组件:

ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'))

使用 @inject 给组件注入其需要的 store(利用 React context 机制);
通过 @observer 将 React 组件转化成响应式组件,它用 mobx.autorun 包装了组件的 render 函数以确保任何组件渲染中使用的数据变化时都可以强制刷新组件:

@observer class App extends React.Component {
    render() {
        return (<span>count:{this.props.store.sum()}</span> )
    }
};

autorun 接受一个函数作为参数,在使用 autorun 的时候,该函数会被立即调用一次,之后当该函数中依赖的可观察状态属性(或者计算属性)发生变化的时候,该函数会被调用,注意,该函数的调用取决的函数中使用了哪些可观察状态属性(或者计算属性)。

个人目录写法:

-Container
    -Main
        -index.jsx
        -store.js
    -PageA
        -index.jsx
        -store.js
    -PageB
        -index.jsx
        -store.js
-Pages
    -Home
        -index
-Store
    -index.js
example Main store
//   Container Main store.js
import { observable, action, computed } from 'mobx'

class MainStore {
    @observable todos = []

    @action
    insertTodo = (text) => {
        this.todos.push(text)
    }

    @action
    deleteTodo = () => {
        this.todos.shift()
    }

    @computed
    get filterTodo() {
        return this.todos.filter(item !== 'banyu')
    }
}

const mainStore = new MainStore()

export default mainStore
example stores
//    Store index.js
import mainStore from '../Container/Main/store'
import pageAStore from '../Container/PageA/store'
import pageBStore from '../Container/PageB/store'

const stores = {
    mainStore,
    pageAStore,
    pageBStore
}
export default stores
example Home Page
//    home page
import { Provider } from 'mobx-react'
import stores from '../../Store/index'

class HomePage extends Component {
    render() {
        return(
            <Provider {...stores} >
                // ... children
            </Provider>
        )
    }
}
example pageB Container
import { inject } from 'mobx-react'

@inject('pageBStore')
class PageB extends Component {
    // ... code
}

Chrome前端调试—代理插件

Chrome前端调试—代理插件

前端开发过程中,经常会有需要对远程环境调试或者本地mock数据的需求。比如,修改线上bug,开发环境不在本地,后端只提供了接口文档等等。

我理想中的请求映射工具应该是这样的:简单,打开浏览器就能用、支持目录映射和文件映射、跨平台。 ReRes以及偷天换日就是居于这个目标写出来的,您可以把请求映射到其他的url,也可以映射到你本机的文件或者目录。ReRes支持单个url映射,也支持目录映射。

  1. 开始使用ReRes

    首先从chrome商店安装ReRes:地址

    安装完毕后,在地址栏输入chrome://extensions/进入扩展页,找到ReRes,勾选“允许访问文件网址”,这样才能让ReRes支持本地映射,如下图:

    至此,ReRes就可以使用了。下面是一些基本功能的使用操作方法:

    添加规则

    点击“添加规则”按钮,输入以下信息,然后保存:

  • If URL match: 一个正则表达式,当请求的URL与之匹配时,规则生效。注意:不要填开头的/和结束的/g,如/.*/g请写成.*

  • Response: 映射的响应地址,线上地址以http://开头,本地地址以file:///开头,比如http://cssha.comfile:///D:/a.js

    启动/禁用

    勾选/取消对应规则前面的勾选框即可。

    编辑规则

    鼠标移到响应规则上,点击“编辑”。

    删除规则

    鼠标移到响应规则上,点击“删除”。

    批量导入规则

    点击“管理规则”按钮进入管理页,点击顶部“导入”按钮,即可导入规则列表文件。规则列表文件是一个json文件,其格式如下:

    [
    {
        "req": ".*auth",
        "res": "https://www.ibanyu.com:30000/auth",
        "checked": false
    },
    {
        "req": ".*/opapi",
        "res": "https://test.ipalfish.com:30000/opapi",
        "checked": true
    },
    {
        "req": ".*/ugc",
        "res": "http://rap2api.taobao.org/app/mock/118135/ugc",
        "checked": false
    },
    {
        "req": ".*/wechatcourse/",
        "res": "http://rap2api.taobao.org/app/mock/118135/wechatcourse/",
        "checked": false
    },
    {
        "req": ".*/thirdparty/wechat",
        "res": "https://www.ipalfish.com/klian/thirdparty/",
        "checked": false
    },
    {
        "req": ".*/wechat",
        "res": "http://rap2api.taobao.org/app/mock/118135/wechat",
        "checked": false
    },
    {
        "req": ".*/base",
        "res": "https://www.ipalfish.com:30000/klian/base",
        "checked": false
    }
    ]
    

    其中相关字段含义如下:

    • req:请求所匹配的正则表达式(对应于If URL match输入框的内容)
    • res:映射的响应地址(对应Response输入框的内容)
    • checked:是否启用
  • 在日常开发中经常会有后端同学定义了接口,但是没有环境的同步开发流程。这个时候就需要借助一些mock数据平台来模拟数据。常用平台有:http://rap2.taobao.org/

    类似下面模拟操作:

    [
    {
        "req": ".*/ugc",
        "res": "http://rap2api.taobao.org/app/mock/118135/ugc",
        "checked": false
    }
    ]
    

  1. 偷天换日(开发者为前项目组同事—见智)

    首先从chrome商店安装偷天换日:

    地址

React Hooks 探索

React Hooks 浅析

在React开发的项目中肯定是充斥着大量的 React 生命周期函数(即使使用了状态管理库也避免不了),每个生命周期里几乎都承担着某个业务逻辑的一部分,或者说某个业务逻辑是分散在各个生命周期里的。

一般情况下,我们都是通过组件和自上而下传递的数据流将我们页面上的大型UI组织成为独立的小型UI,实现组件的重用。但是我们经常遇到很难侵入一个复杂的组件中实现重用,因为组件的逻辑是有状态的,无法提取到函数组件当中。这在处理动画和表单的时候,尤其常见,当我们在组件中连接外部的数据源,然后希望在组件中执行更多其他的操作的时候,我们就会把组件搞得特别糟糕:

- 难以重用和共享组件中的与状态相关的逻辑,造成产生很多巨大的组件
- 逻辑复杂的组件难以开发与维护,当我们的组件需要处理多个互不相关的 localstate 时,每个生命周期函数中可能会包含着各种互不相关的逻辑在里面。
- 复杂的模式,如渲染道具和高阶组件。
- 由于业务变动,函数组件不得不改为类组件。

而 Hooks 的出现本质是你不用再去关心本不该关心的生命周期,达到面向生命周期编程变成面向业务逻辑编程的目的。

一个 Hooks 演变

我们先假想一个常见的需求,一个 Modal 里需要展示一些信息,这些信息需要通过 API 获取且跟 Modal 强业务相关,要求我们:

  • 因为业务简单,没有引入额外状态管理库
  • 因为业务强相关,并不想把数据跟组件分开放
  • API 数据会随机变动,因此需要每次打开 Modal 才获取最新数据
  • 为了后期优化,不可以有额外的组件创建和销毁

我们可能的实现如下:

class RandomUserModal extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      user: {},
      loading: false,
    };
    this.fetchData = this.fetchData.bind(this);
  }

  componentDidMount() {
    if (this.props.visible) {
      this.fetchData();
    }
  }

  componentDidUpdate(prevProps) {
    if (!prevProps.visible &amp;&amp; this.props.visible) {
      this.fetchData();
    }
  }

  fetchData() {
    this.setState({ loading: true });
    fetch('../data.json')
      .then(res =&gt; res.json())
      .then(json =&gt; this.setState({
        user: json.results[0],
        loading: false,
      }));
  }

  render() {
    const user = this.state.user;
    return (

        <button>Close Modal</button>
        {this.state.loading ?
          <div>loading...</div>
          :
          <ul>
            <li>Name: {`${(user.name || {}).first} ${(user.name || {}).last}`}</li>
            <li>Gender: {user.gender}</li>
            <li>Phone: {user.phone}</li>
          </ul>
        }

    )
  }
}


class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      visible: false,
    };
    this.handleOpenModal = this.handleOpenModal.bind(this);
    this.handleCloseModal = this.handleCloseModal.bind(this);
  }

  handleOpenModal() {
    this.setState({ visible: true });
  }

  handleCloseModal() {
    this.setState({ visible: false });
  }

  render() {
    return (
      <div>
        <button>Open Modal</button>

      </div>
    );
  }
}

ReactDOM.render(
  ,
  document.getElementById('root')
);

我们抽象了一个包含业务逻辑的 RandomUserModal,该 Modal 的展示与否由父组件控制,因此会传入参数 visiblehandleCloseModal(用于 Modal 关闭自己)。

为了实现在 Modal 打开的时候才进行数据获取,我们需要同时在 componentDidMountcomponentDidUpdate 两个生命周期里实现数据获取的逻辑,而且 constructor 里的一些初始化操作也少不了。

其实我们的要求很简单:在合适的时候通过 API 获取新的信息,这就是我们抽象出来的一个业务逻辑,为了这个业务逻辑能在 React 里正确工作,我们需要将其按照 React 组件生命周期进行拆解。这种拆解除了代码冗余,还很难复用

下面我们看看采用 Hooks 改造后会是什么样:

function RandomUserModal(props) {
  const [user, setUser] = React.useState({});
  const [loading, setLoading] = React.useState(false);

  React.useEffect(() =&gt; {
    if (!props.visible) return;
    setLoading(true);
    fetch('../data.json').then(res =&gt; res.json()).then(json =&gt; {
      setUser(json.results[0]);
      setLoading(false);
    });
  }, [props.visible]);

  return (

      <button>Close Modal</button>
      {loading ?
        <div>loading...</div>
        :
        <ul>
          <li>Name: {`${(user.name || {}).first} ${(user.name || {}).last}`}</li>
          <li>Gender: {user.gender}</li>
          <li>Phone: {user.phone}</li>
        </ul>
      }

  );
}


class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      visible: false,
    };
    this.handleOpenModal = this.handleOpenModal.bind(this);
    this.handleCloseModal = this.handleCloseModal.bind(this);
  }

  handleOpenModal() {
    this.setState({ visible: true });
  }

  handleCloseModal() {
    this.setState({ visible: false });
  }

  render() {
    return (
      <div>
        <button>Open Modal</button>

      </div>
    );
  }
}

ReactDOM.render(
  ,
  document.getElementById('root')
);

很明显地可以看到我们把 Class 形式变成了 Function 形式,使用了两个 State Hook 进行数据管理(类比 constructor),之前 cDMcDU 两个生命周期里干的事我们直接在一个 Effect Hook 里做了。做了这些,最大的优势是代码精简,业务逻辑变的紧凑,代码行数也从 50+ 行减少到 30+ 行。

Hooks 的强大之处还不仅仅是这个,最重要的是这些业务逻辑可以随意地的的抽离出去,跟普通的函数没什么区别(仅仅是看起来没区别),于是就变成了可以复用的自定义 Hook。具体可以看下面的进一步改造:

// 自定义 Hook
function useFetchUser(visible) {
  const [user, setUser] = React.useState({});
  const [loading, setLoading] = React.useState(false);

  React.useEffect(() =&gt; {
    if (!visible) return;
    setLoading(true);
    fetch('../data.json').then(res =&gt; res.json()).then(json =&gt; {
      setUser(json.results[0]);
      setLoading(false);
    });
  }, [visible]);
  return { user, loading };
}

function RandomUserModal(props) {
  const { user, loading } = useFetchUser(props.visible);

  return (

      <button>Close Modal</button>
      {loading ?
        <div>loading...</div>
        :
        <ul>
          <li>Name: {`${(user.name || {}).first} ${(user.name || {}).last}`}</li>
          <li>Gender: {user.gender}</li>
          <li>Phone: {user.phone}</li>
        </ul>
      }

  );
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      visible: false,
    };
    this.handleOpenModal = this.handleOpenModal.bind(this);
    this.handleCloseModal = this.handleCloseModal.bind(this);
  }

  handleOpenModal() {
    this.setState({ visible: true });
  }

  handleCloseModal() {
    this.setState({ visible: false });
  }

  render() {
    return (
      <div>
        <button>Open Modal</button>

      </div>
    );
  }
}

ReactDOM.render(
  ,
  document.getElementById('root')
);

这里的 useFetchUser 为自定义 Hook,它的地位跟自带的 useState 等比也没什么区别,你可以在其它组件里使用,甚至在这个组件里使用两次,它们会天然地隔离开。

Hooks

Hooks 本质上面说了,是把面向生命周期编程变成了面向业务逻辑编程,写法上带来的优化只是顺带的。

这里,做一个类比,await/async 本质是把 JS 里异步编程思维变成了同步思维,写法上表现出来的特点就是原来的·回调地狱·被打平了。

总结对比:
* await/async 把 ·回调地狱· 干掉了,异步编程思维变成了同步编程思维
* Hooks 把 ·嵌套地狱· 干掉了,面向生命周期编程变成了面向业务逻辑编程

参考

  • react官网 es2049