目录

1.贡献

1.1.核心开发

1.1.1.basePath 注意事项

1.1.2.管理依赖

1.1.3.模块和自动加载

1.1.4.与 Elasticsearch 通信

1.1.5.功能测试

1.2.插件开发

1.2.1.插件资源

1.2.2.UI Exports

1.2.3.插件功能测试

2.限制

嵌套字段


1.贡献

我们非常欢迎您为 Kibana 做贡献,如果您想提交 pull request 到 Kibana 仓库,您可以参考核心开发。

如果您想使用 Kibana 内部的插件 API,您可以查看插件开发。

 

1.1.核心开发

  • basePath 注意事项
  • 管理依赖
  • 模块和自动加载
  • 与 Elasticsearch 通信
  • 功能测试

 

1.1.1.basePath 注意事项

所有从 Kibana UI 到服务端的通信,都要遵守 server.basePath 。下面是一些基于上下文处理该问题的建议:

 

<img> 和 <a> 元素

如您所愿,可以不写基本路径而直接写上 src 或者 href urls,然后用 kbn-src 或 kbn-href 替换 src或 href 。

<img kbn-src="plugins/kibana/images/logo.png">

 

获取一个静态的 asset url

用 webpack 导入 asset 到 build 中。这会给您一个 JavaScript 的 URL,并给 webpack 一个机会去执行优化及中断缓冲。

// in plugin/public/main.js
import uiChrome from 'ui/chrome';
import logoUrl from 'plugins/facechimp/assets/banner.png';

uiChrome.setBrand({
  logo: `url(${logoUrl}) center no-repeat`
});

前端 API 请求

使用 chrome.addBasePath() 将 basePath 添加到 url 的前面。

import chrome from 'ui/chrome';
$http.get(chrome.addBasePath('/api/plugin/things'));

服务器端

在每个绝对 URL 路径上添加 config.get('server.basePath')

const basePath = server.config().get('server.basePath');
server.route({
  path: '/redirect',
  handler(req, reply) {
    reply.redirect(`${basePath}/otherLocation`);
  }
});

开发模式中的 BasePathProxy

Kibana 开发服务器在一个有随机 server.basePath 的代理之后自动运行。这样,当开发者编写代码时,他们会不断地确认他们的代码是否与 basePath 一起工作。

为了达到这个效果, serve 任务做了如下一些事情:

  1. 根据 dev.basePathProxyTarget 配置服务器的端口(默认 5603 )
  2. 在 server.port 启动一个 BasePathProxy
  • 为 randomBasePath 挑选一个三位数的值
  • 从 /{any}/app/{appName} 重定向到 /{randomBasePath}/app/{appName} ,从而使刷新起作用
  • 将以 /{randomBasePath}/ 开头的所有请求代理到 Kibana 服务器

有时候,在开发环境中代理会产生意想不到的副作用。因此,当需要的时候您可以通过传递 --no-base-path 选项到 serve 任务或者 npm start 以退出代理。

npm start -- --no-base-path

 

1.1.2.管理依赖

在开发 Kibana 前端环境中使用的插件时,您可能想要包含(至少)一个或两个库。虽然这在90%的情况下都是很简单的,但总会有一些异常值,而其中一些异常值还是在非常受欢迎的项目中。

在 Kibana 中使用外部库之前,您要先安装它。您可以通过如下方式安装:

 

npm (首选方法)

一旦您发现想要添加的依赖,您可以像这样安装它:

npm install --save some-neat-library

在 javascript 文件的顶部,只需要使用它的名称来导入库:

import someNeatLibrary from 'some-neat-library';

就像在 node.js 中一样,前端代码无需进行扩展配置就能请求 npm 节点模块安装。

 

webpackShims

当您想要用的库使用 es6 或 common.js 模块,但在 npm 上不可用时,您可以复制库的源文件到 webpackShim。

# 为新的库创建一个目录
mkdir -p webpackShims/some-neat-library
# 将您想使用的库下载到该目录
curl https://cdnjs.com/some-neat-library/library.js > webpackShims/some-neat-library/index.js

然后,像往常一样在您的 JavaScript 代码中包含该库:

import someNeatLibrary from 'some-neat-library';

 

桥接第三方代码

一些 JavaScript 库声明它们依赖的方式并不会被诸如 webpack 的工具所理解。通常情况下,库不会导出它们提供的值,而只是将它们写入到全局变量里(或者某种意义上的变量)。

当将这样的代码拉入到 Kibana 中时,我们要编写 “桥接代码(shims)” 来整合第三方代码,以便与我们的应用、其他库和模块系统一起工作。我们可以使用 webpackShims 目录来做到这一点。

示例是解释如何编写桥接代码的最简单方式。下面是我们用于 jQuery 的 webpack 桥接代码。

// webpackShims/jquery.js

module.exports = window.jQuery = window.$ = require('node_modules/jquery/dist/jquery');
require('ui/jquery/findTestSubject')(window.$);

由于 webpackShims 的行为类似于 node_modules , 一旦 webpack 发现 import 'jquery'; 语句,桥接代码会被加载。当它发生时,桥接代码会做两件事:

  1. 将实际 jQuery 模块的导出值分配给 $ 和 jQuery ,允许像 angular 这样的库检测 jQuery 是否可用,并将其作为模块的导出变量使用。
  2. 最终,我们编写的 jQuery 插件会被引入,因此每次有文件引入 jQuery 时,该文件会同时得到 jQuery 和 $.findTestSubject 的帮助模块。

下面是我们用于 angular 的 webpack 桥接代码:

// webpackShims/angular.js

require('jquery');
require('node_modules/angular/angular');
require('node_modules/angular-elastic/elastic');
require('ui/modules').get('kibana', ['monospaced.elastic']);
module.exports = window.angular;

如果您一行一行的看,会发现桥接代码做的很简单:

  1. 确保 jQuery 在 angular(实际运行上述的 shim) 之前加载。
  2. 从 npm 安装中加载 angular.js 。
  3. 加载 angular-elastic 插件,每次导入 angular 都需要包含 angular-elastic 插件。
  4. 使用 ui/modules 模块添加被 angular-elastic 导入进来的模块作为 kibana angular 模块的依赖。
  5. 最后,导出 window.angular 变量。这意味着当写下 import angular from 'angular'; 时会恰当地设置 angular 库中的 angular 变量,而不是未定义的默认配置。

 

1.1.3.模块和自动加载

自动加载

由于 JS 模块与指令、过滤器和服务的脱节,很难知道要导入哪些内容。更难的是,您不知道自己移除的看似无用的模块是否会对其他地方产生影响。

为了防止其成为一个问题,ui 模块提供了 “自动加载(autoloading)” 模块。这些模块的唯一目的是使用某些组件拓展环境。以下是这些模块的分解:

  • import 'ui/autoload/styles' 在 src/ui/public/styles 导入所有样式。
  • import 'ui/autoload/directives' 在 src/ui/public/directives 导入所有指令。
  • import 'ui/autoload/filters' 在 src/ui/public/filters 导入所有过滤器。
  • import 'ui/autoload/modules'
  • import 'ui/autoload/all'

解析所需路径

Kibana 用 Webpack 管理依赖。

下面是如何将 import/require 语句解析为一个文件:

  1. 检查模块路径的开头
  • 如果路径以 . 开头
  • 添加当前文件的目录
  • 转到 3
  • 如果路径以 / 开头
  • 搜索这条精确路径
  • 转到 3
  • 转到 2
  1. 搜索已命名模块
  • moduleName = dirname(require path)
  • 如果 moduleName 是下面别名中的一个,或者以下面中的别名开头,则匹配
  • 用匹配替换别名,继续 3
  • 当满足这些条件时,匹配:
  • ./webpackShims/${moduleName} 是一个目录
  • ./node_modules/${moduleName} 是一个目录
  • 如果没有找到匹配
  • 转到上层目录
  • 从 2.iii 重新开始,直至找到匹配或者到达根目录
  • 如果找到匹配
  • 以匹配的全路径替换 requite 语句中的 moduleName 前缀,然后转到 3
  1. 搜索文件
  • 下列路径中第一个解析为一个 file 的是我们的匹配
  • path + .js
  • path + .json
  • path + .jsx
  • path + .less
  • path
  • path/${basename(path)} + .js
  • path/${basename(path)} + .json
  • path/${basename(path)} + .jsx
  • path/${basename(path)} + .less
  • path/${basename(path)}
  • path/index + .js
  • path/index + .json
  • path/index + .jsx
  • path/index + .less
  • path/index
  • 如果以上路径均不匹配则抛出错误

 

1.1.4.与 Elasticsearch 通信

Kibana 在服务器和浏览器上暴露了两个客户端用于和 elasticsearch 通信。其中一个为管理客户端,用于管理 Kibana 的状态;另外一个为数据客户端,用于处理其它所有的请求。客户端使用 elasticsearch.js 库。

 

服务器客户端

服务器客户端通过 elasticsearch 插件暴露。

const adminCluster = server.plugins.elasticsearch.getCluster('admin);
  const dataCluster = server.plugins.elasticsearch.getCluster('data);

  //ping as the configured elasticsearch.user in kibana.yml
  adminCluster.callWithInternalUser('ping');

  //ping as the user specified in the current requests header
  adminCluster.callWithRequest(req, 'ping');

 

浏览器客户端

浏览器客户端通过 AngularJS 服务暴露。

uiModules.get('kibana')
.run(function (esAdmin, es) {
  es.ping()
  .then(() => esAdmin.ping())
  .catch(err => {
    console.log('error pinging servers');
  });
});

 

1.1.5.功能测试

我们通过功能测试确保 Kibana UI 按照预期方式运行。该功能测试通过自动化用户交互,取代了长达数小时的人工测试。Kibana 使用的工具叫做 FunctionalTestRunner ,能够更好的控制功能测试环境,更便于插件作者使用。

 

运行功能测试

FunctionalTestRunner 结构非常简单,大部分功能主要源自位于 test/functional/config.js 的配置文件。如果您正在编写一个插件,就会拥有自己的配置文件。有关更多信息,请参见 插件功能测试 。

使用 node.js 执行 FunctionalTestRunner 脚本运行测试,使用 Kibana 的默认配置:

node scripts/functional_test_runner

在没有任何参数的情况下运行时, FunctionalTestRunner 会自动加载位于标准位置的配置,但是您可以用 --config 标记来覆盖这个行为。 --bail 和 --grep 也有命令行标记,功能与 mocha 类似。 日志也可以通过使用 --quiet 、 --debug 或 --verbose 标记进行自定义设置。

使用 --help 标记获得更多选项。

 

编写功能测试

环境

测试位于 mocha ,使用 expect 作断言。

我们使用 chromedriver、 leadfoot 和 digdug 在 Chrome 上做自动化测试。 FunctionalTestRunner启动后,digdug 会打开一个启动 chromedriver 的 Tunnel 和一个 Chrome 的精简用例。digdug 还会创建一个 Leadfoot’s Command 类的用例,该用例可以通过 remote 服务器获取。 remote 通过 digdug Tunnel 与 Chrome 进行通讯。 remote 涉及的所有命令详见 leadfoot/Command API

FunctionalTestRunner 使用 babel 语言自动编译功能测试,因此测试可以使用 Kibana 源代码使用的 ECMAScript 特性。详见 style_guides/js_style_guide.md

 

术语

Provider(提供者):

   FunctionalTestRunner 运行的代码会被打包进一个函数中,这样它就可以通过配置文件传递,并且能被参数化。这些提供者函数中的任何一个都有可能是异步的,并需要返回或重新获取他们想要的  。提供者函数总通过单一参数:API Provider(参见 Provider API 章节)来调用。

提供者配置示例:

// config and test files use `export default`
export default function (/* { providerAPI } */) {
  return {
    // ...
  }
}

Services(服务)

服务根据服务提供者产生的单一值命名。测试和其他服务能够通过请求服务的名称来检索服务实例。除 mocha API 以外的所有功能都是通过服务公开的。

Page objects(页对象)

页对象是一种将通常行为封装进特定页面或插件的特殊服务。当您编写自己的插件时,您可能想要添加一个(或多个)用于描述测试所需执行的常见交互的页面对象。

Test Files(测试文件)

   FunctionalTestRunner 的主要目的是执行测试文件。这些文件导出一个提供者 API 调用的测试提供者(Test Provider),但并不会返回数值。相反,测试提供者用 mocha’s BDD interface 定义一个程序组。

Test Suite(测试套件)

测试套件是调用 describe() 的测试集,然后通过调用 it() 、 before() 、 beforeEach() 等填充测试和 setup/teardown hooks。每个测试文件都必须定义唯一一个顶级测试套件,但测试套件可以拥有任意多个嵌套测试套件。

 

测试文件剖析

下列带注释的示例文件展示了每个测试套件所使用的基本结构。它首先导入 expect.js,然后定义其默认输出:一个匿名的测试提供者。该测试提供者为 getService() 和 getPageObjects() 函数拆解提供者 API。它使用这些函数来收集本套件的依赖。对于 mocha.js 用户,其他测试文件看起来就很普通了。

import expect from 'expect.js';
// test files must `export default` a function that defines a test suite
export default function ({ getService, getPageObject }) {

  // most test files will start off by loading some services
  const retry = getService('retry');
  const testSubjects = getService('testSubjects');
  const esArchiver = getService('esArchiver');

  // for historical reasons, PageObjects are loaded in a single API call
  // and returned on an object with a key/value for each requested PageObject
  const PageObjects = getPageObjects(['common', 'visualize']);

  // every file must define a top-level suite before defining hooks/tests
  describe('My Test Suite', () => {

    // most suites start with a before hook that navigates to a specific
    // app/page and restores some archives into elasticsearch with esArchiver
    before(async () => {
  await Promise.all([
        // start with an empty .kibana index
        esArchiver.load('empty_kibana'),
        // load some basic log data only if the index doesn't exist
        esArchiver.loadIfNeeded('makelogs')
      ]);
      // go to the page described by `apps.visualize` in the config
      await PageObjects.common.navigateTo('visualize');
    });

    // right after the before() hook definition, add the teardown steps
    // that will tidy up elasticsearch for other test suites
    after(async () => {
      // we unload the empty_kibana archive but not the makelogs
      // archive because we don't make any changes to it, and subsequent
      // suites could use it if they call `.loadIfNeeded()`.
      await esArchiver.unload('empty_kibana');
    });

    // This series of tests illustrate how tests generally verify
    // one step of a larger process and then move on to the next in
    // a new test, each step building on top of the previous
    it('Vis Listing Page is empty');
    it('Create a new vis');
    it('Shows new vis in listing page');
    it('Opens the saved vis');
    it('Respects time filter changes');
    it(...
  });

}

 

提供者 API

提供者 API 对象(Provider API Object)是所有提供者的第一个也是唯一一个参数。这个对象可以用于加载服务、页面对象和配置、测试文件。

在配置文件中,API具有以下属性

 

log

ToolingLog 的一个准备使用的实例

readConfigFile(path)

返回一个解析为配置实例的承诺,提供 path 路径下的配置文件值

在服务和 PageObject 提供者中,API 是:

getService(name)

根据名称,加载并返回服务的一个单例实例

getPageObjects(names)

加载 PageObject 的单例实例,收集它们到一个对象,名字是 PageObject 中每个对象的 key

测试提供者中的 API 与服务提供者 API 相同,但是具有附加方法:

loadTestFile(path)

加载路径上的测试文件。使用此方法将其他文件中的套件嵌套到更高级的套件中。

 

服务指标

内置服务

FunctionalTestRunner 自带三种内置 service:

config:

log:

  • 源码: src/utils/tooling_log/tooling_log.js
  • ToolingLog 实例是可读流。此服务提供的实例由 FunctionalTestRunner CLI 自动传输到 stdout
  • log.verbose() 、 log.debug() 、 log.info() 、 log.warning() 像 console.log 那样工作,只不过产生结构化更好的输出

lifecycle:

  • 源码: src/functional_test_runner/lib/lifecycle.js
  • 设计主要用于 service 中
  • 公开生命周期事件以进行基本协调。处理程序可以返回承诺并异步地解析、失败
  • 包括 beforeLoadTests 、 beforeTests 、 beforeEachTest 、 cleanup 、 phaseStart 、 phaseEnd 阶段

 

Kibana 服务

Kibana 功能测试定义了绝大部分测试会使用的实际功能。

retry:

  • retry.try(fn) - 在 loop 中执行 fn 直至成功或超过默认重试时间
  • retry.tryForTime(ms, fn) 在 loop 中执行,直至成功或超过 ms 毫秒

testSubjects:

  • 用 data-test-subj 属性标记您的测试对象: <div id="container”> <button id="clickMe” data-test-subj=”containerButton” /> </div>
  • 使用 testSubjects 帮助器点击这个按钮  await testSubjects.click(‘containerButton’);
  • 常用方法:
  •   testSubjects.find(testSubjectSelector) - 在页面中寻找一个测试对象;如果过一段时间没有找到,抛出异常
  •   testSubjects.click(testSubjectSelector) - 在页面中点击一个测试主题;如果过一段时间没有找到,抛出异常

find:

  • find.byCssSelector()
  • find.allByCssSelector()

kibanaServer:

  • kibanaServer.uiSettings.update()
  • kibanaServer.version.get()
  • kibanaServer.status.getOverallState()

esArchiver:

  • esArchiver.load(name)
  • esArchiver.loadIfNeeded(name)
  • esArchiver.unload(name)

docTable:

pointSeriesVis:

Low-level utilities:

  • es
  • remote

 

自定义服务

服务是有意通用的。它们可以是任何东西(甚至什么都不是)。有些服务有助于与特定类型的 UI 元素(如 PooSosieServices )交互,而其他服务则更为基础,如日志或配置。每当您想在可重用包中提供一些功能时,请考虑制作自定义服务。

为了创建一个自定义的 somethingUseful service:

1.创建一个如下的 test/functional/services/something_useful.js 文件:

// Services are defined by Provider functions that receive the ServiceProviderAPI
export function SomethingUsefulProvider({ getService }) {
  const log = getService('log');

  class SomethingUseful {
    doSomething() {
    }
  }
  return new SomethingUseful();
}

2.从 services/index.js 重新导出您的 provider

3.将它导入到 src/functional/config.js 并添加到服务配置中:

import { SomethingUsefulProvider } from './services';

export default function () {
  return {
    // … truncated ...
    services: {
      somethingUseful: SomethingUsefulProvider
    }
  }
}

 

PageObjects

PageObject 的目的只是自我解释。可视化的 PageObject 提供与可视化 app 交互的助手,相当于仪表板对于仪表板 app。

"common" PageObject 是一个例外。作为一个延缓的实验性的实现,common PageObject 是有用的跨页面的帮助器集合。现在我们有了共享服务,并且这些服务可以与其他的 FunctionalTestRunner 共享,我们会继续将功能从 common PageObject 转移到服务中。

请在已有或新服务中添加新的方法,而不是进一步扩展 CommonPage 类。

 

Gotchas

记住您不能运行文件( it 块)中一个单独的测试,因为整个 describe 需要按顺序执行。在一个文件中应该只有一个顶级的 describe 。

 

功能测试计时

另一个重要的 gotcha 是通过注意时间来编写稳定的测试。所有 remote 方法异步运行。最好在进入下一步之前,在 UI 上添加等待变化的交互。

例如,与其简单的编写点击按钮的交互,不如在头脑中编写更高级目的的交互:

不好的例子: PageObjects.app.clickButton()

class AppPage {
  // what can people who call this method expect from the
  // UI after the promise resolves? Since the reaction to most
  // clicks is asynchronous the behavior is dependant on timing
  // and likely to cause test that fail unexpectedly
  async clickButton () {
    await testSubjects.click(‘menuButton’);
  }
}

好的例子: PageObjects.app.openMenu()

class AppPage {
  // unlike `clickButton()`, callers of `openMenu()` know
  // the state that the UI will be in before they move on to
  // the next step
  async openMenu () {
    await testSubjects.click(‘menuButton’);
    await testSubjects.exists(‘menu’);
  }
}

这样写将确保您的测试时间不是片状的,或者基于交互后UI更新的假设。

 

调试

在命令行运行:

node --debug-brk --inspect scripts/functional_test_runner

该命令会输出一个URL,通过在 Chrome 浏览器中访问该URL,您可以调试您的功能测试用例。

您也可以在运行 FunctionalTestRunner 时增加 --debug 或 --verbose 参数,从而在命令行看额外的日志信息。您可以像下面这样,在您的测试用例中增加日志:

// load the log service
const log = getService(‘log’);

// log.debug only writes when using the `--debug` or `--verbose` flag.
log.debug(‘done clicking menu’);

 

1.2.插件开发

Kibana 插件接口在不断的发展变化。由于插件更新很快,因此很难向后兼容。
Kibana 强制要求安装的插件版本必须和 Kibana 版本一致。插件开发者必须为每个新的 Kibana 版本发布新的插件版本。
  • 插件资源
  • UI Exports
  • 插件功能测试

 

1.2.1.插件资源

这里有一些资源可以帮助您开发插件

我们的 IRC 频道

很多 Kibana 开发者都在 #kibana 频道上的 irc.freenode.net 上玩。我们  帮助您开发插件。更重要的,我们 想要您的帮助 来理解您使用插件的目标,借此我们可以为您创建一个更好的插件系统。如果您从来没有用过 IRC,欢迎来玩。您可以从 Freenode 网页客户端开始。

一些有用的文章

视频

插件生成器

下载 插件生成器 以快速生成您的插件。

代码中的引用

 

1.2.2.UI Exports

可用的 UiExport 类型列表:

类型

用途

hacks

每个应用都需要包含的模块。

visTypes

注册器通过 ui/registry/vis_types 提供的模块。

fieldFormats

注册器通过 ui/registry/field_formats 提供的模块。

chromeNavControls

注册器通过 ui/registry/chrome_nav_controls 提供的模块。

navbarExtensions

注册器通过 ui/registry/navbar_extensions 提供的模块。

docViews

注册器通过 ui/registry/doc_views 提供的模块。

app

向系统添加一个应用。这个 uiExport 类型被定义为元数据的一个对象,而不仅仅是一个模块 id。

 

1.2.3.插件功能测试

插件在 Kibana repo 外部运行并使用 FunctionalTestRunner 。在继续之前,请确认您的 Kibana 开发环境配置正确。

编写您自己的配置

每个工程或插件应该有自己的 FunctionalTestRunner 配置文件。就像 Kibana,配置文件会定义所有要加载的测试文件、服务提供者和 PageObjects,还有某些服务的配置选项。

通过复制下面的例子到 test/functional/config.js 文件开始测试:

import { resolve } from 'path';
import { MyServiceProvider } from './services/my_service';
import { MyAppPageProvider } from './services/my_app_page;

// 允许重写默认的 kibana 目录
// 使用 KIBANA_DIR 环境变量
const KIBANA_CONFIG_PATH = resolve(process.env.KIBANA_DIR || '../kibana', 'test/functional/config.js');

// 配置文件的默认导出必须是一个配置提供程序(config provider)
// 它返回带有项目配置值的对象
export default async function ({ readConfigFile }) {

  // 读取 Kibana 配置文件,这样我们可以使用它的一些服务和 PageObjects
  const kibanaConfig = await readConfigFile(KIBANA_CONFIG_PATH);

  return {
    // 列出包含您插件测试用例的文件路径
    testFiles: [
      resolve(__dirname, './my_test_file.js'),
    ],

    // 定义在您的测试中可用的服务和提供程序,
    // 否则,只有内置的服务可用
    services: {
      ...kibanaConfig.get('services'),
      myService: MyServiceProvider,
    },

    // 就像服务那样,PageObjects 定义为提供者的名称映射
    // 在 Kibana 中合并或选择指定一个
    pageObjects: {
      management: kibanaConfig.get('pageObjects.management'),
      myApp: MyAppPageProvider,
    },

    // apps 部分定义 PageObjects.common.navigateTo(appKey) 使用的 urls
    // 为您的插件合并 Kibana 配置中定义的 url,以便使用帮助
    apps: {
      ...kibanaConfig.get('apps'),
      myApp: {
        pathname: '/app/my_app',
      }
    },

    // 选择 esArchiver 从哪里加载
    esArchiver: {
      directory: resolve(__dirname, './es_archives'),
    },

    // 选择 screenshots 保存到哪里
    screenshots: {
      directory: resolve(__dirname, './tmp/screenshots'),
    }

    // 更多设置,例如 timeouts、 mochaOpts 等定义在配置模式。参见 {blob}src/functional_test_runner/lib/config/schema.js[src/functional_test_runner/lib/config/schema.js]
  };
}

现在可以在 repo 的根目录运行您插件工程的 FunctionalTestRunner 脚本。

node ../kibana/scripts/functional_test_runner

使用 esArchiver

pull request

 

2.限制

Kibana 目前有以下限制:

  • 嵌套字段

嵌套字段

Kibana 不能执行带嵌套字段(nested objects)的聚合操作。再查询框中执行 Lucene 查询语句的时候也不能有嵌套对象。

使用 include_in_parent 和 copy_to 属性作为临时解决方案已经不再支持了。