0%

显示图片的组件.

提供了以下几个不同用途的构造方法:

  • new Image: 从 ImageProvider 中获取图片.
  • new Image.asset: 使用 key 从 AssetBundle 中获取图片
  • new Image.network: 从 URL 中获取图片
  • new Image.file: 从 File 中获取图片
  • new Image.memory: 从 Uint8List 中获取图片

支持以下图片格式: JPEG,PNG,GIF,Animated GIF,WebP,Animated WebP,BMP,WBMP.

为了自动实现像素密度级的资产管理, 确保在 MaterialApp,WidgetsApp,MediaQuery组件树中使用 AssetImage 指定的 Image 组件.

图片是用 paintImage 画出来的,它包含了 Image 中的不同属性详情描述.

类似组件

  • Icon
  • new Ink.Image: 在 material app 中推荐使用(特别是图片在 Material 中,而且上面有 InkWell)
  • Image: dart:ui 提供

继承 🌲

Object > Diagnosticable > DiagnosticableTree > Widget > StatefulWidget > Image

属性

  • alignment -> AlignmentGeometry
  • centerSlice -> Rect: .9 图
  • color -> Color
  • colorBlendMode -> BlendMode: 混合颜色
  • excludeFromSemantics -> bool
  • filterQuality -> FilterQuality
  • fit -> BoxFit
  • gaplessPlayback -> bool: 当 image provider 改变时是否显示旧的图片.
  • heihgt -> double
  • image -> ImageProvider
  • matchTextDirection -> bool
  • repeat -> ImageRepeat
  • semanticLabel -> String
  • width -> double

以垂直数组方式显示子组件的组件.

想要某个子组件扩展填充垂直空间,使用 Expanded 组件包装即可.

Column 组件不会滑动(通常如果 Column 中的多个子组件超出了可用空间,则会报错).如果有一排组件,而且想要在空间不足时能滑动,考虑使用 ListView.

横向排列考虑使用 Row.

如果只有一个子组件,考虑使用 AlignCenter 定位组件.

如果传入的垂直约束是无边界的

如果一个 Column 组件有一个或多个 ExpandedFlexible 组件,并且被放在另一个 ColumnListView 或其他不提供最大高度约束上下文的组件中,那么将会收到一个运行时异常,表明有非 0 flex 的子组件,但是其垂直约束是无边界的.

正如异常表现出来的问题,使用 FlexibleExpanded 意味着接下来布局其他的子组件时必须把剩余的空间平均分配,而如果传入的垂直约束是无边界的话,剩余的空间就变成无限的.

解决该问题的关键在于为什么 Column 会接收到无边界的垂直约束.

一个可能的原因是 Column 被放在了另一个 Column (内部的 Column 没有使用 ExpandedFlexible 包装)中.当一个 Column 布局它的非 flex 子组件(没有使用 ExpandedFlexible 包装)时,该 Column 就给了其子组件无边界的约束,这样子组件可以自己决定他们的维度(传递无边界的约束意味着该子组件可能需要收缩以包装其内容).对应的解决方案是使用 Expanded 包装内部的 Column,表明内部的 Column 应该填充外部 Column 的剩余空间,而不是去获取它想要的空间大小.

另一个可能的原因是一个 Column 可能嵌套在 ListView 或其他可滑动的垂直布局组件中.在这种场景下,确实存在无限的垂直空间(垂直滑动列表的重点在于允许无限垂直滑动).通常应该检查为什么内部的 Column 会有一个 ExpandedFlexible 子组件:它的大小真的是内部子组件的大小吗?解决方案是从包装内部子组件的父组件中移除 ExpandedFlexible 组件.

查看 BoxConstraints 获取更多关于约束的信息.

黄黑相间条

当一个 Column 的内容超过了可用空间,即 Column 溢出,那么内容将被裁剪.在 debug 模式下,在溢出边角上会显示一个黄黑相间条指出该问题,在 Column 下会打印一个溢出多少的信息.

最普遍的解决方案是当垂直空间受限时确保内容滑动使用 ListView 而不是 Column.

布局算法

接下来是 framework 如何渲染 Column,查看 BoxConstraints 获取盒布局模型信息.

布局 Column 需要 6 步.
1、使用无边界的水平约束和传入的垂直约束设置每个子组件 flex 因子为 null 或 0.如果 crossAxisAlignment 值为 CrossAxisAlignment.stetch,使用满足传入的垂直约束的最大高度而不是使用准确的垂直约束.
2、对非 0 flex 因子的子组件,按照其 flex 因子将剩下的水平空间分割.如一个 flex 因子为 2.0 的子组件在水平空间上将比 flex 因子是 1.0 的子组件宽 2 倍.
3、使用相同的垂直约束按照步骤 1 布局剩下的子组件,使用基于步骤 2 申请到的空间作为水平约束而不是无边界水平约束布局.Flexible.fit 属性值为 FlexFit.tight 的子组件受到严格约束(如强制填充申请到的空间),而 Flexible.fit 属性值为 FlexFit.tight 的子组件约束宽松(如不强制填充申请到的空间).
4、Row 的高度总是最大子组件的高度(一般满足传入的垂直约束).
5、Row 的宽度由 mainAxisSize 属性决定.如果 mainAxisSize 属性值为 MainAxisSize.max,那么 Row 的宽度是传入约束的最大宽度.如果 mainAxisSize 的值为 MainAxisSize.min,那么 Row 的宽度是所有子组件的宽度之和(受传入约束).
6、根据 mainAxisAlignmentcrossAxisAlignment 确定每个子组件的位置.例如,如果 mainAxisAlignmentMainAxisAlignment.spaceBetween,
没有被分配给子组件的空间将会平均分配到各个子组件之间.

继承 🌲

Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > MultiChildRenderObjectWidget > Flex > Column

以横向数组的方式显示子组件的组件.

为了使其一个组件填充剩余可用空间,可以使用 Expanded对其进行包装.

Row 组件不会滑动(通常一个Row组件中的子组件过多已经超出了可用空间,则会导致错误).如果在不足的空间中想要一排组件能够滑动,可以考虑使用 ListView.

Column 竖向排列.

如果仅有一个子组件,那么可以考虑使用 AlignCenter 定位该子组件.

为什么我的 row 有一条黄黑相间的条纹?

如果 Row的内容是不可伸缩扩展的(没有使用 ExpandedFlexible 组件包装),连在一起比 row 本身更宽,那么我们就称 row 溢出了.
如果 row 溢出了,那么 row 就没有多余的空间去分配给它的FlexibleExpanded 子组件. row 会在边上显示一条黄黑相间的条表示溢出了.如果 row 外有空间,那么溢出亮将会以红色字体打印出来.

布局算法

接下来介绍 framework 是如何渲染 Row 的.查看 BoxConstraints 了解盒布局模型.

Row 的布局分以下 6 步:
1、使用无边界的水平约束和传入的垂直约束设置每个子组件 flex 因子为 null 或 0.如果 crossAxisAlignment 值为 CrossAxisAlignment.stetch,使用满足传入的垂直约束的最大高度而不是使用准确的垂直约束.
2、对非 0 flex 因子的子组件,按照其 flex 因子将剩下的水平空间分割.如一个 flex 因子为 2.0 的子组件在水平空间上将比 flex 因子是 1.0 的子组件宽 2 倍.
3、使用相同的垂直约束按照步骤 1 布局剩下的子组件,使用基于步骤 2 申请到的空间作为水平约束而不是无边界水平约束布局.Flexible.fit 属性值为 FlexFit.tight 的子组件受到严格约束(如强制填充申请到的空间),而 Flexible.fit 属性值为 FlexFit.tight 的子组件约束宽松(如不强制填充申请到的空间).
4、Row 的高度总是最大子组件的高度(一般满足传入的垂直约束).
5、Row 的宽度由 mainAxisSize 属性决定.如果 mainAxisSize 属性值为 MainAxisSize.max,那么 Row 的宽度是传入约束的最大宽度.如果 mainAxisSize 的值为 MainAxisSize.min,那么 Row 的宽度是所有子组件的宽度之和(受传入约束).
6、根据 mainAxisAlignmentcrossAxisAlignment 确定每个子组件的位置.例如,如果 mainAxisAlignmentMainAxisAlignment.spaceBetween,
没有被分配给子组件的空间将会平均分配到各个子组件之间.

继承 🌲

Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > MultiChildRenderObjectWidget > Flex > Row

flutter 的核心设计是将整个应用的各个部分各个层级都看作 Widget 来渲染,所以按照 Widget 的分类来学习会比较全面。

基础组件

Container

  • Row
  • Column
  • Image
  • Text
  • Icon
  • RaisedButton
  • Scaffold
  • AppBar
  • FlutterLogo
  • Placeholder

Material 组件

结构和导航

  • Scaffold
  • AppBar
  • BottomNavigationBar
  • TabBar
  • TabBarView
  • MaterialApp
  • WidgetsApp
  • Drawer

按钮

  • RaisedButton
  • FloatingActionButton
  • FlatButton
  • IconButton
  • PopupMenuButton
  • ButtonBar

输入框和选择框

  • TextField
  • Checkbox
  • Radio
  • Switch
  • Slider
  • Date & Time Pickers

对话框,Alert Panel

  • SimpleDialog
  • AlertDialog
  • BottomSheet
  • ExpansionPanel
  • SnackBar

信息展示

  • Image
  • Icon
  • Chip
  • Tooltip
  • DataTable
  • Card
  • LinearProgressIndicator

布局

  • ListTile
  • Stepper
  • Divider

Cupertino

  • CupertinoActivityIndicator
  • CupertinoAlertDialog
  • CupertinoButton
  • CupertinoDialog
  • CupertinoDialogAction
  • CupertinoSlider
  • CupertinoSwitch
  • CupertinoPageTransition
  • CupertinoFullScreenDialogTransition
  • CupertinoNavigationBar
  • CupertinoTabBar
  • CupertinoPageScaffold
  • CupertinoTabScaffold
  • CupertinoTabView

Layout

单个元素

  • Container
  • Padding
  • Center
  • Align
  • FittedBox
  • AspectRatio
  • ConstrainedBox
  • Baseline
  • FractionallySizedBox
  • IntrinsicHeight
  • IntrinsicWidth
  • LimitedBox
  • Offstage
  • OverflowBox
  • SizedBox
  • SizedOverflowBox
  • Transform
  • CustomSingleChildLayout

多个元素

  • Row
  • Column
  • Stack
  • IndexedStack
  • Flow
  • Table
  • Wrap
  • ListBody
  • ListView
  • CustomMultiChildLayout

LayoutBuilder

Text

  • Text
  • RichText
  • DefaultTextStyle

Assets

  • Image
  • Icon
  • RawImage
  • AssetBundle

Input

  • Form
  • FormField
  • RawKeyboardListener

Animation

  • AnimatedContainer
  • AnimatedCrossFade
  • Hero
  • AnimatedBuilder
  • DecoratedBoxTransition
  • FadeTransition
  • PositionedTransition
  • RotationTransition
  • ScaleTransition
  • SizeTransition
  • SlideTransition
  • AnimatedDefaultTextStyle
  • AnimatedListState
  • AnimatedModalBarrier
  • AnimatedOpacity
  • AnimatedPhysicalModal
  • AnimatedPositioned
  • AnimatedSize
  • AnimatedWidget
  • AnimatedWidgetBaseState

交互

  • LongPressDraggable
  • GestureDetector
  • DragTarget
  • Dismissible
  • IgnorePointer
  • AbsorbPointer
  • Navigator
  • Scrollable

样式

  • Padding
  • Theme
  • MediaQuery

Draw

  • Opacity
  • Transform
  • DecoratedBox
  • FractionalTransition
  • RotatedBox
  • ClipOval
  • ClipPath
  • ClipRect
  • CustomPaint
  • BackdropFilter

Async

  • FutureBuilder
  • StreamBuilder

滚动

  • ListView
  • NestedScrollView
  • GridView
  • SingleChildScrollView
  • Scrollable
  • Scrollbar
  • CustomScrollView
  • NotificationListener
  • ScrollConfiguration
  • RefreshIndicator

辅助

  • Semantics
  • MergeSemantics
  • ExcludeSemantics

批处理

使用 Dataloader

如果你正在使用 graphql,那么你可能遇到数据图查询.这可以通过本地数据图加载轻松实现.
使用 java-dataloader 将帮助你更高效的处理数据图条目的缓存和批量请求.如果 dataloader 已经发现了一个之前的数据条目,它将会缓存数据并且直接返回不再发起请求.
假设我们需要查询一个英雄和他们朋友的名字及他们朋友的朋友的名字.

1
2
3
4
5
6
7
8
9
10
11
12
13
{
hero {
name {
friends {
name {
friends {
name
}
}
}
}
}
}

这个查询的结果如下.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker",
"friends": [
{
"name": "HanSolo"
},
{
"name": "Leia Organa"
},
{
"name": "C-3P0"
},
{
"name": "R2-D2"
}
]
},
{
"name": "Han Solo",
"friends": [
{ "name": "Luke Skywalker" },
{ "name": "Leia Organa" },
{ "name": "R2-D2" }
]
},
{
"name": "Leia Organa",
"friends": [
{ "name": "Luke Skywalker" },
{ "name": "Han Solo" },
{ "name": "C-3PO" },
{ "name": "R2-D2" }
]
}
]
}
}

最差的办法是每次调用 DataFetcher获取 person 对象.
本例中将发起 15 次网络请求.即使很多人有共同的朋友.使用 dataloader你可以使 graphql 查询变得更高效.
当 graphql 降序查询每个层级时(hero -> friends -> friends),dataloader 调用 promise 传递 person 对象.在每个层级中调用 dataloader.dispatch() 批量发起部分查询请求.加上缓存(默认使用),之前的 person 将被返回.
上例中只涉及到 5 个独立的 people,合理的使用缓存和批量请求将只有 3 个批量加载函数被调用,3 个网络请求或数据库查询总比 15 个要好.
如果你使用了 java.util.concurrent.CompletableFuture.supplyAsync(),那么你可以通过异步调用使查询变得更高效.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//  batch loader 可能被调用多次,因为它的无状态性,所以适合作为单例使用
BatchLoader<String,Object> characterBatchLoader = new BatchLoader<String,Object>() {
@Override
public CompletableStage<List<Object>> load(List<String> keys) {
// 使用 supplyAsync() 最大化并行执行
return CompletableFuture.supplyAsync(() -> getCharacterDataViaBatchHTTPApi(keys));
}
};

// 这个 data laoder 获取关联的人物,把他们放入 graphql schema
DataFetcher heroDataFetcher = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment environment) {
DataLoader<String,Object> dataloader = environment.getDataLoader("character");
return dataloader.load("2001");
}
};

DataFetcher friendsDataFetcher = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment environment) {
StarWarsCharacter starWarsCharacter = environment.getSource();
List<String> friendsIds = starWarsCharacter.getFriendIds();
DataLoader<String,Object> dataloader = environment.getDataLoader("character");
return dataloader.loadMany(friendsIds);
}
}
DataLoaderDispatcherInstrumentationOptions options = DataLoaderDispatcherInstrumentationOptions.newOptions().includeStatistics(true);
DataLoaderDispatcherInstrumentation dispatcherInstrumentation = new DataLoaderDispatcherInstrumentation(options);
GraphQL graphql = GraphQL.newGraphQL(buildSchema())
.instrumentation(dispatcherInstrumentation)
.build();
// 因为 data loader 是有状态的,所以每次请求都会被执行
DataLoader<String,Object> characterDataLoader = DataLoader.newDataLoader(characterBatchLoader);
DataLoaderRegistry registry = new DataLoaderRegistry();
registry.register("character",characterDataLoader);

ExecutionInput executionInput = newExecutionInput()
.query(getQuery())
.dataLoaderRegistry(registry)
.build();
ExecutionResult executionResult = graphql.execute(executionInput);

本例中因为我们需要微调 DataLoaderDispatcherInstrumentation选项,所以手动添加.如果不要的话,默认会自动添加的.

仅适用于 AsyncExecutionStrategy 的 Data Loader

这是因为此执行策略知道在最佳时机分发你的 load 调用.它通过深度追踪你有多少个突出的属性及他们是否是列表值等实现.
其他策略如 ExecutorServiceExecutionStrategy无法实现这个功能,因为如果 data loader 代码检测到你没有使用 AsyncExecitionStrategy,那么当碰到每个属性时,它将简单的分发 data loader.你可能会得到值的 caching,但你绝对拿不到他们的 batching.

Data Loader 的每一个请求

如果你正在为 web 请求提供服务,那么可以为用户请求指定数据.如果你有用户指定的数据,你可能不会缓存用户 a 的数据,然后在后续的请求中把它传递给用户 b.
你的 DataLoader 实例的范围是很重要的.你可能想每个 web 请求创建一个 dataloader 以确保数据只对特定的 web 请求缓存.同时确保 dispatch调用不影响其他的 graphql 执行.
DataLoader 默认行为类似缓存.如果发现之前存在某个 key 对应的值,那么会自动返回它.
如果你的数据可以跨 web 请求分享,那么你可能需要改变你的 data loader 缓存实现,这样他们就能通过如 memcached 或 redis 这样的缓存层进行数据分享.
下例中仍然每个请求创建一个 data loader,然而缓存层允许数据分享.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
CacheMap<String,Object> crossRequestCacheMap = new CacheMap<String,Object>() {
@Override
public boolean containsKey(String key) {
return redisIntegration.containsKey(key);
}

@Override
public Object get(String key) {
return redisIntegration.getValue(key);
}

@Override
public CacheMap<String,Object> set(String key,Object value) {
redisIntegration.setValue(key,value);
return this;
}

@Override
public CacheMap<String,Object> delete(String key) {
redisIntegration.clearKey(key);
return this;
}

@Override
public CacheMap<String,Object> clear() {
redisIntegration.clearAll();
return this;
}
};

DataLoaderOptions options = DataLoaderOptions.newOptions().setCacheMap(crossRequestCacheMap);
DataLoader<String,Object> dataloader = DataLoader.newDataLoader(batchLoader,options);

只能异步调用的批量加载功能

此 dataloader 代码模式整合所有明显的 data loader 调用到一个更有效的批量加载调用.
graphql-java 追踪已发起的明显的 data loader 调用,然后在最合适的时机(即所有的 graphql 属性已经校验成功并分发)在后台调用dispatch.
然而有些情况下将导致你的 data loader 调用永不会完成,这中情况必须避免.这种情况包括在异步线程调用 DataLoader.
下面的 🌰 不会成功(将永远无法完成).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
BatchLoader<String,Object> batchLoader = new BatchLoader<String,Object>() {
@Override
public CompletionStage<List<Object>> load(List<String> keys) {
return CompletableFuture.completedFuture(getTheseCharacters(keys));
}
};

DataLoader<String,Object> characterDataLoader = DataLoader.newDataLoader(batchLoader);

DataFetcher dataFetcherThatCallsTheDataLoader = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment environment) {
// 千万要避免这样做
return CompletableFuture.supplyAsync(() -> {
String argId = environment.getArgument("id");
DataLoader<String,Object> characterLoader = environment.getDataLoader("characterLoader");
return characterLoader.load(argId);
})
}
}

上面的 🌰 中,characterDataLoader.load(argId) 可以在另外一个线程的未来某个时刻被调用. graphql-java 引擎不知道何时是最佳时机去分发明显的 DataLoader 调用,因此这个 data loader 可能永远不会如期执行,也不会有结果返回.
请记住,data loader 调用仅仅是一个保证,后面会将明显的调用批量调用在合适的时机获取结果.最佳时机是 graphql 属性树已经校验过,且所有的属性值已经被分发.
下面的 🌰 依然是异步代码,但是把它放在 BatchLoader 里.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
BatchLoader<String,Object> batchLoader = new BatchLoader<String,Object>() {
@Override
public CompletionStage<List<Object>> load(List<String> keys) {
return CompletableFuture.supplyAsync(() -> getThreseCharacters(keys));
}
};

DataLoader<String,Object> characterDataLoader = DataLoader.newDataLoader(batchLoader);

DataFetcher dataFetcherThatCallsTheDataLoader = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment environment) {
// 这是阔以滴
String argId = environment.getArgument("id");
DataLoader<String,Object> characterLoader = environment.getDataLoader("characterLoader");
return characterLoader.load(argId);
}
}

上面的 🌰 characterDataLoader.load(argId) 会立即返回.这将会把 data 请求入队列,z 当所有的 graphql 属性都分发后再执行.
然后当 DataLoader 被分发后,他的 BatchLoader 函数被调用.这个代码可以异步执行,所以你可以有多个批量加载函数,他们可以同时执行.在上例中 CompletableFuture.supplyAsync(() -> getTheseCharacters(keys)); 将再另一个线程中返回 getTheseCharacters() 方法.

向你的 data loader 传递 context

data load 库支持传递两个类型的 context 到 batch loader.第一个是每个 dataloader 一个全局的 context 对象,第二个是一个 loaded key 一个 context 对象的 map.
这允许你传递下游需要的额外信息.dataloader key 用在缓存结果,而 context 对象可以用在调用中.
在下面的 🌰 中,我们有一个全局的安全 context 对象,提供了一个调用 token,同时可以传递 graphql 原对象到每个 dataLoader.load() 调用中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
BatchLoaderWithContext<String,Object> batchLoaderWithCtx = new BatchLoaderWithContext<String,Object>() {
@Override
public CompletionStage<List<Object>> load(List<String> keys,BatchLoaderEnvironment loaderContext) {
// 获取全局 context 对象
SecurityContext sercurityCtx = loaderContext.getContext();
// 每个键都有一个 context 对象集
Map<Object,Object> keysToSOurceObjects = loaderContext.getKeyContexts();
return CompletableFuture.supplyAsync(() -> getTheseCharacters(securityCtx.getToken(),keys,keysToSourceObjects));
}
};
SecurityContext securityCtx = SecurityContext.newSecurityContext();
BatchLoaderContextProvider contextProvider = new BatchLoaderContextProvider() {
@Override
public Object getÇontext() {
return securityCtx;
}
};
DataLoaderOptions loaderOptions = DataLoaderOptions.newOptions().setBatchLoaderContextProvider(contextProvider);
DataLoader<String,Object> characterDataLoader = DataLoader.newDataLoader(batchLoaderWithCtx,loaderOptions);

DataFetcher dataFetcherCallsTheDataLoader = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment environment) {
String argId = environment.getArgument("id");
Object source = environment.getSource();
return characterDataLoader.load(argId,source);
}
}

graphql 中的常量

scalars

graphql 类型系统的叶子节点被称为 scalars.一旦到达了 scalar 类型则无法在沿着类型结构继续向下了.scalar 类型是指不可再分割的值.
graphql 规范明确要求所有的语言实现必须具有以下 scalar 类型.

  • String 即 GraphQLString- 一个 UTF-8 字符串序列.
  • Boolean 即 GraphQLBoolean- true or false.
  • Int 即 GraphQLInt- 有符号的 32 整型数.
  • Float 即 GraphQLFloat- 有符号的双精度浮点数.
  • ID 即 GraphQLID- 类似于 String 的唯一标识符.定义一个 ID 标识符即表示该属性不是人类可识别的用途.
    graphql-java 为 java 系统添加了以下有用的 scalar 类型.
  • Long 即 GraphQLLong- 基于 java.lang.Long 的 scalar.
  • Short 即 GraphQLShort- 基于 java.lang.Short 的 scalar.
  • Byte 即 GraphQLByte- 基于 java.lang.Byte 的 scalar.
  • BigDecimal 即 GraphQLBigDicimal 基于 java.math.BigDecimal 的 scalar.
  • BigInteger 即 GrapQLBigInteger 基于 java.math.BigInteger 的 scalar.

graphql.Scalars类包含了提供 scalar 类型的单例实例.

自定义 scalars

你可以实现自定义 scalar.在运行时你需要完成类型强制转换,后面会解释.假设我们需要一个 email 的 scalar 类型.它把 email 地址作为输入输出.
我们将创建一个如下的 graphql.schema.GraphQLScalarType 单例实例.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static final GraphQLScalarType EMAIL = new GraphQLScalarType("email","A cusom scalar that handles emails",new Coercing() {
@Override
public Object serialize(Object dataFetcehrResult) {
return serilizeEmail(dataFetcherResult);
}

@Override
public Object parseValue(Object input) {
return parseEmailFromVariable(input);
}

@Override
public Object parseLiteral(Object input) {
return parseEmailFromAstLiteral(input);
}
});

强制类型转换

自定义 scalar 实现的真正作用点在 graphql.schema.Coercing实现.有 3 个函数需要实现

  • parseValue- 接收一个输入变量,转换为 java 运行时实现.
  • parseLiteral-接收一个 AST 字符 graphql.language.Value 作为输入,转换为 java 运行时实现.
  • serialize-接收一个 Java 对象,最终转为 scalar 输出类型.
    所以你自定义的 scalar 实现需要处理 2 中类型的输入(parseValue/parseLiteral)和 1 中输出(serialize).
    如下查询,使用了变量,AST 字符然后输出我们需要的 scalar 类型 email.
1
2
3
4
5
6
7
8
9
mutation Contact($mainContact: Email!) {
makeContact(
mainContactEmail: $mainContact
backupContactEmial: "backup@company.com"
) {
id
mainContactEmail
}
}

我们的自定义 email scalar 类型将

  • 调用 parseValue$mainContact变量转为运行时对象.
  • 调用 parseLiteral 将 AST graphql.language.StringValuebackup@company.com“ 转换为运行时对象.
  • 调用 serialize 将 mainContactEmial 运行时实现转为输出对象形式.

输入输出校验

例如我们的 email scalar 将会校验输入输出是否是真是的 email 地址.
graphql.schema.Coercing 协议如下:

  • serialize 只允许抛出 graphql.schema.CoercingSerializeException.这表明值无法被序列化为合适的形式.决不允许指定其他运行时异常以取得普通的 graphql 校验行为.必须返回一个非 null 值.
  • parseValue 只允许抛出 graphql.schema.CoercingParseValueException.这表明值无法被作为输入解析为合适的形式..决不允许指定其他运行时异常以取得普通的 graphql 校验行为.必须返回一个非 null 值.
  • parseLiteral 只允许抛出 graphql.schema.CoercingParseLiterialException.这表明 AST 值无法被作为输入解析为合适的形式..决不允许指定其他运行时异常以取得普通的 graphql 校验行为.
    有的人尝试依赖运行时异常校验以期获取普通的 graphql 错误.这是行不通的.必须遵照 Coercing 方法协议使 graphql-java 引擎按照 grapqhl 的 scalar 类型规范运行.

示例实现

下面是一个简单的 email scalar 类型实现,展示了如何通过继承 Coercing 实现一个 scalar.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public static class EmailScalar {
public static final GraphQLScalarType EMAIL = new GraphQLScalarType("email","A custom scalar that handles emails",new Coercing() {
@Override
public Object serialize(Object dataFetcherResult) {
return serializeEmail(dataFetcherResult);
}
@Override
public Object parseValue(Object input) {
return parseEmailFromVariable(input);
}
@Override
public Object parseLiterial(Object input) {
return parseEmialFromAstLiterial(input);
}
});

private static boolean looksLikeAnEmailAddress(String possibleEmailValue) {
return Pattern.matches("[A-Za-z0-9]@[.*]",possibleEmailValue);
}
private static Object serialzeEmail(Object dataFetcherResult) {
String possibleEmailValue = String.valueOf(dataFetcherResult);
if (looksLikeAnEmailAddress(possibleEmailValue)) {
return possibleEmailValue;
} else {
throw new CoercingSerializeException("Unable to serialize" + possibleEmailValue + " as an email address");
}
}
private static Object parseEmailFromVariable(Object input) {
if (input instanceof String) {
String possibleEmailValue = input.toString();
if (looksLikeAnEmailAddress(possibleEmailValue)) {
return possibleEmailValue;
}
}
throw new CoercingParseValueException("Unable to parse variable value " + input + " as an email address");
}
private static Object parseEmailFromAstLiterial(Object input) {
if (input instanceof StringValue) {
String possibleEmailValue = ((StringValue)input).getValue();
if (looksLikeAnEmailAddress(possibleEmailValue)) {
return possibleEmailValue;
}
}
throw new CoercingParseLiterialException("Unable to parse variable value " + input + " as an email address");
}
}
}

Mapping data

graphql 是如何把对象数据匹配到类型的

graphql 内部全部是关于声明类型 schema,然后在运行匹配到数据.
作为类型 schema 的设计者,你应该在处理这些元素.

1
2
3
4
5
6
7
8
9
10
type Query {
products(match: String): [Product] # a list of products
}
type Product {
id: ID
name: String
description: String
cost: Float
tax: Float
}

然后可以执行查询

1
2
3
4
5
6
7
8
query ProductQuery {
products(match: "Paper*") {
id
name
cost
tax
}
}

对于 Query.products属性有一个绑定的 DataFetcher负责查找匹配输入参数的 product s 列表.
假设我们有 3 个下游服务.一个获取产品信息,一个获取产品价格信息,一个计算查新税收信息.
graphql-java 使用这些对象运行 data fetcher,获取信息然后将其匹配到 schema 指定的类型中.
我们的目标是获取到这 3 个源的信息,然后把它们作为一个 unified 类型展示.
我们可以对 cost 和 tax 需要计算的属性指定 data fetcher,但这需要更多的维护精力,可能导致 N+1 性能问题.
我们最好在 Query.products data fetcher 中获取所有的信息,同时创建一个 unified 数据视图.

1
2
3
4
5
6
7
8
9
10
DataFetcher produtctsDataFetcher = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment env) {
String matchArg = env.getArgument("match");
List<ProductInfo> productInfos = getMatchingProducts(matchArg);
List<ProductCostInfo> productConstInfo = getProdutConsts(productInfo);
List<ProductTaxInfo> productTaxInfo = getProductTax(productInfo);
return mapDataTogether(productInfo,productCostInfo,productTaxInfo);
}
}

上面有 3 个类型的信息需要被整合为一个以便 graphql 查询可以访问 id,name,cost,tax 属性.
有 2 中方法可以创建这个映射.一个是使用类型不安全的 List<Map> 结构,另一个是使用类型安全的 List<ProductDTO>封装这些数据.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private List<Map> mapDataTogetherViaMap(List<ProductInfo> productInfo,List<ProductCostInfo> productCostInfo,List<ProductTaxInfo> productTaxInfo) {
List<Map> unifiedView = new ArrayList<>();
for (int i = 0;i < productInfo.size();i++) {
ProductInfo info = productInfo.get(i);
ProductCostInfo cost = productCostInfo.get(i);
ProductTaxInfo tax = productTaxInfo.get(i);

Map<String,Object> objectMap = new HashMap<>();
objectMap.put("id",info.getId());
objectMap.put("name",info.getName());
objectMap.put("descriptioin",info.getDescription());
objectMap.put("cost",cost.getCost());
objectMap.put("tax",tax.getTax());

unifiedView.add(objectMap);
}
return unifiedView;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class ProductDTO {
private final String id;
private final String name;
private final String description;
private final Float cost;
private final Float tax;

public ProductDTO(String id, String name, String description, Float cost, Float tax) {
this.id = id;
this.name = name;
this.description = description;
this.cost = cost;
this.tax = tax;
}

public String getId() {
return id;
}

public String getName() {
return name;
}

public String getDescription() {
return description;
}

public Float getCost() {
return cost;
}

public Float getTax() {
return tax;
}
}

private List<ProductDTO> mapDataTogetherViaDTO(List<ProductInfo> productInfo, List<ProductCostInfo> productCostInfo, List<ProductTaxInfo> productTaxInfo) {
List<ProductDTO> unifiedView = new ArrayList<>();
for (int i = 0; i < productInfo.size(); i++) {
ProductInfo info = productInfo.get(i);
ProductCostInfo cost = productCostInfo.get(i);
ProductTaxInfo tax = productTaxInfo.get(i);

ProductDTO productDTO = new ProductDTO(
info.getId(),
info.getName(),
info.getDescription(),
cost.getCost(),
tax.getTax()
);
unifiedView.add(productDTO);
}
return unifiedView;
}

graphql 引擎现在可以使用 object 列表然后运行查询获取 id,name,cost,tax 属性.
graphql-java 默认的 data fetcher graphql.schema.PropertyDataFetcher 同时支持 map 和 POJO.
对于列表的每一个对象都会通过 id 属性,或使用 name 在 map 里查找,或通过 getId() 方法获取,然后返回给 graphql response.对于查询中的每个类型都会执行这样的操作.
通过在高级 data fetcher 中创建一个 unified view,你就可以在运行时数据和 graphql schema 之间建立一个映射.

Execution

Queries

对 schema 执行 query,使用合适的参数构建一个新的 GraphQL 对象,然后调用execute().
query 的结果是包含查询数据或者(并且)一系列错误的ExecutionResult 对象.

1
2
3
4
5
6
GraphQLSchema schema = GraphQLSchema.newSchema().query(queryType).build();
GraphQL graphQl = GraphQL.newGraphQL(schema).build();
ExecutionInput executionInput = ExecutionInput.newExecutionInput().query("query { hero { name }}");
ExectionResult executionResult = graphQl.execute(executionInput);
Object data = executionResult.getData();
List<GraphQLError> errors = executionResult.getErrors();

更多复杂的查询示例请参考StarWars query tests;

Data Fetchers

每个 graphql 属性类型都有一个 graphql.schema.DataFetcher 与之关联.其他 graphql 实现通常把这个类型成为 resolvers.
通常可以使用graphql.schema.PropertyDataFetcher来检查 提供属性值的 Java POJO 对象.如果某个属性未指定 data fetcher,默认会使用这个.
然而你可能需要使用自定义的 data fetcehr 获取你的顶级域对象.可能涉及到数据库调用或通过 HTTP 请求其他系统.
graphql-java不关心你是如何获取你的域对象,这是你需要关心的地方.同时也不关心用户访问数据授权.这些都应该放到你自己的逻辑处理层.
data fetcher 示例如下:

1
2
3
4
5
6
DataFetcher userDataFetcher = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment environment) {
return fetchUserFromDatabase(environment.getArgument("userId"));
}
};

每个 DataFetcher都会传递一个 graphql.schema.DataFetchingEnvironment 对象(包含了将要获取的属性,获取该属性所需提供的参数和其他信息如属性的父对象,query 根对象或 query 上下文对象).
上例中,execution将会在 data fetcher 返回结果后才继续执行.可以通过返回CompletionStage 对象使 DataFetcher 异步执行,详情请继续阅读.

当获取数据时发生异常

如果在 data fetcher 调用中发生异常,那么默认执行策略将生成graphql.ExceptionWhileDataFetching 错误,然后添加到结果中的错误集中.切记 graphql 允许带错误的部分结果.
下面是标准的行为.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SimpleDataFetcherExceptionHandler implements DataFetcherExceptionhandler {
private static final Logger log = LoggerFactory.getLogger(SimpleDataFetcherExceptionHandler.class);

@Override
public void accept(DataFetcherExceptionHandlerParameters handlerParameters) {
Throwable exception = handlerParameters.getException();
SourceLocation sourceLocation = handlerParameters.getField().getSourceLocation();
ExecutionPath path = handlerParameters.getPath();

ExceptionWhileDataFetching error = new ExceptionWhileDataFetching(path,exception,sourceLocation);
handlerParameters.getExecutionContext().addError(error);
log.warn(error.getMessage(),exception);
}
}

如果你抛出的是GraphqlError,那么它会从 exception 中转换 message 和自定义扩展属性到 ExceptionWhileDataFetching对象.此处允许你向调用者返回自定义的属性到 graphql error.
例如想象你的 data fetcher 将抛出这个异常.foofizz 属性将被添加到返回的 graphql error 中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class CustomRuntimeException extends RuntimeException implements GrapQLError {
@Override
public Map<String,Object> getExtension() {
Map<String,Object> customAttributes = new LinkedHashMap<>();
customAttributes.put("foo","bar");
cutomAttributes.put("fizz","whizz");
return customAttributes;
}

@Override
public List<SourceLocation> getLocation() {
return null;
}

@Override
public ErrorType getErrorType() {
return ErrorType.DataFetchingException;
}
}

你可以通过创建自己的 graphql.execution.DataFetcherExceptionHandler异常处理代码改变此默认行为,给出你自己的执行策略.
例如上面的代码记录了基础异常和堆栈跟踪.有的人可能不喜欢在输出错误列表中看到这些.所以你可以使用这个机制改变这个行为.

1
2
3
4
5
6
7
DataFetcherExceptionHandler handler = new DataFetcherExceptionHandler() {
@Override
public void accept(DataFetcherExceptionHandlerParameters handlerParameters) {

}
};
ExecutionStrategy executionStrategy = new AsyncExecutionStrategy(handler);

返回值和错误

DataFetcher实现中通过直接或者使用 CompletableFuture 实例包装异步执行返回 graphql.execution.DataFetcherResult 来实现同时返回数据和多个错误.当你的DataFetcher 需要从多个数据源或其他 GraphQL 资源获取数据时特别有用.
在这个 🌰 中,DataFetcher 从另一个 GraphQL 资源中获取 user 同时返回数据和错误.

1
2
3
4
5
6
7
8
9
10
11
DataFetcher userDataFetcher = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment environment) {
Map response = fetchUserFromRemoteGraphQLResource(environment.getArgument("userID"));
List<GralhQLError> errors = response.get("errors)
.stream()
.map(MyMapGraphQLError::new)
.collect(Collections.toList());
return new DataFetcherResult(response.get("data"),errors);
}
}

将结果序列化为 JSON

调用 graphql 最常见的方法是通过 HTTP,返回 JSON 响应.所以你需要将 graphql.ExecutionResult 转为 JSON.
最常用的实现是使用 Jackson 或 GSON 这样的 JSON 序列化库.然而它们解析数据的方式有它们自己的一套方式.例如 nulls对 graphql 结果是很重要的,所以你必须在设置 json mapper 时包含它.
为了保证你获取的 JSON 结果 100% 符合 graphql 的需求,你应该对结果调用toSpecification,然后将其作为 JSON 返回.
这将会确保返回的结果符合规范.

1
2
3
ExecutionResult executionResult = graphQL.execute(executinInput);
Map<String,Object> toSpecificationResult = executionResult.toSpecification();
sendAsJson(toSpecificationResult);

Mutations

在这儿学习 mutations.
本质上你需要定义一个接收参数作为输入的 GraphQLObjectType .这些参数你可以通过 data fetcher 调用修改你的数据存储.

1
2
3
4
5
6
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}

在执行 mutation 操作中需要传递参数,本例中是 $ep$review 参数.
你可以像这样创建类型处理 mutation 操作.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
GraphQLInputObjectType episodeType = newInputObject()
.name("Episode")
.field(newInputObjectField()
.name("episodeNumber")
.type(Scalars.GraphQLInt))
.build();

GraphQLInputObjectType reviewInputType = newInputObject()
.name("ReviewInput")
.field(newInputObjectField()
.name("stars")
.type(Scalars.GraphQLString)
.name("commentary")
.type(Scalars.GraphQLString))
.build();

GraphQLObjectType reviewType = newObject()
.name("Review")
.field(newFieldDefinition()
.name("stars")
.type(GraphQLString))
.field(newFieldDefinition()
.name("commentary")
.type(GraphQLString))
.build();

GraphQLObjectType createReviewForEpisodeMutation = newObject()
.name("CreateReviewForEpisodeMutation")
.field(newFieldDefinition()
.name("createReview")
.type(reviewType)
.argument(newArgument()
.name("episode")
.type(episodeType)
)
.argument(newArgument()
.name("review")
.type(reviewInputType)
)
)
.build();

GraphQLCodeRegistry codeRegistry = newCodeRegistry()
.dataFetcher(
coordinates("CreateReviewForEpisodeMutation", "createReview"),
mutationDataFetcher()
)
.build();


GraphQLSchema schema = GraphQLSchema.newSchema()
.query(queryType)
.mutation(createReviewForEpisodeMutation)
.codeRegistry(codeRegistry)
.build();

请注意输入参数类型是 GraphQLInputObjectType.这是很重要的.输入类型只能是这种类型,绝不能使用输出类型如 GraphQLObjectType.标量类型既可以是输入类型也可以是输出类型.
这个 data fetcher 执行 mutation,返回一些有意义的输出值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private DataFetcher mutationDataFetcher() {
return new DataFetcher() {
@Override
public Review get(DataFetchingEnvironment environment) {
Map<String,Object> episodeInputMap = environemnt.getArugment("episode");
Map<String,Object> reviewInputMap = environment.getArugment("review");

EpisodeInput episodeInput = EpisodeInput.fromMap(episodeInputMap);
ReviewInput reviewInput = ReviewInput.fromMap(reviewInputMap);
Review updatedReview = reviewStore().update(episodeInput, reviewInput);
return updatedReview;
}
}
}

异步执行

graphql-java 执行查询时可以完全支持异步执行.你可以通过调用 executeAsync() 获取 CompletableFuture的结果.

1
2
3
4
5
6
7
GraphQL graphql = buildSchema();
ExecutionInput executionInput = ExecutionInput.newExecutionInput().query("query { hero { name }}").build();
CompletableFuture<ExecutionResult> promise = graphql.executeAsync(executionInput);
promise.thenAccept(executinoResult -> {
encodeResultToJsonAndSendResponse(executionResult);
});
promise.join();

使用 CompletableFuture 可以在执行完成时组合 action 和 function.最终调用 join() 等待执行完成.
graphql-java 使用异步执行的原理是通过 join 调用通过方法execute().所以下面的代码效果是一样的.

1
2
3
ExecutionResult executionResult = graphql.execute(executionInput);
CompletableFuture<ExecutionResult> promise = graphql.executeAsync(executionInput);
ExecutionResult executionResult = promise.join();

如果 graphql.schema.DataFetcher 返回的是 CompletableFuture<T> 对象,那么这个结果将被组合进整个异步查询执行中.这意味着你可以并行发起多个属性查询请求.你使用的线程池策略取决于你的 data fetcher 代码.
下面的代码采用了标准的 java.util.concurrent.ForkJoinPool.commonPool() 线程执行器在另外一个线程提供数据.

1
2
3
4
5
6
7
8
9
DataFetcher userDataFetcher = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment environment) {
CompletableFuture<User> userPromise = CompletableFuture.supplyAsync(() -> {
return fetchUserViaHttp(environment.getArgument("userId"));
});
return userPromise;
}
}

上面的代码用 Java8 Lambdas 可以简略为:

1
DataFetcher userDataFetcher = environment -> CompletableFuture.supplyAsync(() -> fetchUserViaHttp(environment.getArgument("userId"));

graphql-java 引擎确保所有的 CompletableFuture 对象遵照 graphql 规范组合在一起提供执行结果.
这是 graphql-java 创建异步 data fetcher 的快捷方式.使用 graphql.schema.AsyncDataFetcher.async(DataFetcher<T>) 包装一个 DataFetcher.可以使用静态导入创建更易读的代码.

1
DataFetcher userDataFetcher = async(environment -> fetchUserViaHttp(environment.getArgument("userId")));

执行策略

继承 graphql.execution.ExecutionStrategy 的类可以用于运行一个查询或修改. graphql-java 提供了大量不同的策略,如果你非常迫切,也可以使用自定义的.
当你创建 Graphql 对象时可以确定执行策略.

1
2
3
4
GraphQL.newGraphQL(schema)
.queryExecutionStrategy(new AsyncExecutionStrategy())
.mutationExecutionStrategy(new AsyncSerialExecutionStrategy())
.build();

实际上上面的代码和默认设置一致,大多数情况下是一个明智的策略选择.

异步执行策略

默认的查询执行策略是 graphql.execution.AsyncExecutionStrategy,会把每一个属性作为 CompletableFuture 对象分发,并且不关心哪个最先完成.此策略是性能最佳的执行策略.
data fetchers 本身会返回 CompletionStage 值,这将导致完全异步的行为.

1
2
3
4
5
6
7
8
9
10
query {
hero {
enemies {
name
}
friends {
name
}
}
}

AsyncExecutionStrategy 自由分发 enemies 属性和 friends属性.enemies 属性不必等待 friends属性返回.这是非常低效的.
无论如何,最终会将结果按顺序排列.查询结果将遵照 graphql 规范,返回结果对应 query 属性顺序.只有 data fetcher 的执行是随机顺序.

异步序列化执行策

graphql 规范要求 mutation 必须按照 query 属性的顺序序列化执行.
所以 mutation 默认使用 graphql.execution.AsyncSerialExecutionStrategy 策略.它会保证在执行下一个和后面前当前的每个属性执行完毕.也可以在 mutation data fetcher 中返回 CompletionStage 对象,并且会按顺序在下一个 mutation 属性 data fetcher 被分发之前执行完毕.

订阅执行策略

graphql 允许对 graphql data 创建有状态的订阅.可以使用 SubscriptionExecutionStrategy实现,同时支持 reactive-stream API.
查看了解更多基于 graphql 服务的订阅支持.

查询缓存

在 graphql-java 引擎执行查询之前必须被解析和检验,并且这个处理过程可能有些耗时.
为了避免重复解析/校验GraphQL.Builder允许PreparsedDocumentProvider实例复用Document实例.
注意 ⚠️,这只缓存解析的 Document,不缓存查询结果

1
2
3
4
Cache<String,PreparsedDocumentEntry> cache = Caffeine.newBuilder().maximumSize(10_000).build();
GraphQL graphql = GraphQL.newGraphQL(StarWarsSchema.starWarsSchema)
.preparsedDocumentProvider(cache::get)
.build();
  • 这个缓存实例应该是线程安全共享的.
  • PreparsedDocumentProvider 是一个只有一个 get 方法的函数接口,我们可以传递一个方法引用到里面以匹配 builder 的签名.

为了实现高缓存覆盖率,推荐属性参数通过变量传递而不是直接在 query 中定义.
下面的查询:

1
2
3
4
5
query HelloTo {
sayHello(to: "ME") {
greeting
}
}

应该这样写:

1
2
3
4
5
query HelloTo($to: String!) {
sayHello(to: $to) {
greeting
}
}

和变量

1
2
3
{
"to": "Me"
}

现在就可以不管提供的变量是什么而重用查询.

获取数据

graphql 如何获取数据

graphql 中的每个属性都关联了一个 graphql.schema.DataFetcher.
一些属性会使用专用的 data fetcher 从数据库获取该属性的相关信息.而大多数简单的使用属性名和 Plain Old Java Object(POJO)模式 从内存中获取数据.
在其他 graphql 实现中,Data fetcher 被称为 resolver
现在声明一个类型定义:

1
2
3
4
5
6
7
8
9
10
11
12
type Query {
products(match: String): [Product] # a list of products
}

type Product {
id: ID
name: String
description: String
cost: Float
tax: Float
launchDate(dateFormat: String = "dd,MM,yyyy"): String
}

原文

简介

本文将探索如何创建移动优先的网页设计体验。

  • 为什么我们需要创建移动优先,响应式,适应式网页设计体验?
  • 如何针对适应式站点组织 HTML 结构以便优化性能,优先考虑灵活性?
  • 如何书写优先定义共享样式、针对大屏和媒体查询构建、使用相对单位的 CSS?
  • 如何编写不引人注目的 Javascript 来有条件地加载内容片段,利用触摸事件和地理定位
  • 我们可以做些什么来进一步增强我们的适应性体验

适应性的必要性

随着网络环境越来越复杂,为越来越多的环境提供可靠的网络体验变得极为重要.幸运的是,响应式 web 设计给 web 开发者提供了一些可以匹配任意大小屏幕的工具来组织布局.