Практика с вами, чтобы начать работу с плагином Webpack

внешний интерфейс Webpack
Практика с вами, чтобы начать работу с плагином Webpack

Это 101-я оригинальная статья без воды. Если вы хотите получить больше оригинальных статей, выполните поиск в публичном аккаунте и подпишитесь на нас~ Эта статья была впервые опубликована в блоге Zhengcaiyun:Практика с вами, чтобы начать работу с плагином Webpack

句号.png

О вебпаке

Прежде чем говорить о плагине, давайте сначала разберемсяWebpack. По сути,Webpackэто сборщик статических модулей для современных приложений JavaScript. Он анализирует наш код, генерирует соответствующие зависимости, а затем объединяет разные модули в один или несколько пакетов.

WebpackК основным понятиям относятся следующие:

  1. Запись: входной файл WebPack указывает, какой модуль следует использовать в качестве записи для построения внутренней зависимой фигуры.
  2. Вывод: Сообщите Webpack, куда выводить созданные файлы пакетов, а также правила того, как должны называться файлы выходных пакетов и где их выводить.
  3. Загрузчик: конвертер кода модуля, который позволяет Webpack обрабатывать другие типы файлов, помимо JS и JSON.
  4. Плагин: Плагин предоставляет функции для выполнения более широкого круга задач, в том числе: оптимизация упаковки, управление ресурсами, внедрение переменных среды и т. д.
  5. Режим: необходимые параметры для выполнения различных параметров оптимизации в соответствии с различными операционными средами.
  6. Совместимость с браузерами: браузеры, поддерживающие все стандарты ES5 (IE8 и выше).

После понимания основных концепций Webpack давайте посмотрим, зачем нам нужен плагин.

Роль плагинов

Позвольте мне привести вам случай из нашего политического облака:

В проекте React наши файлы маршрутизатора обычно пишутся в проекте.Если проект содержит много страниц, все маршрутизаторы бизнес-модулей неизбежно будут связаны, поэтому мы разработали плагин.При сборке и упаковке плагин будет читать index.js файлы во всех папках, а затем объединить их вместе, чтобы сформировать единый файл маршрутизатора, который может легко решить проблему бизнес-связывания. Это применение плагина (конкретная реализация будет объяснена в последнем разделе).

Давайте взглянем на структуру кода нашего проекта до синтеза:

├── package.json
├── README.md
├── zoo.config.js
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .stylelintrc
├── build (Webpack 配置目录)
│   └── webpack.dev.conf.js
├── src
│   ├── index.hbs
│   ├── main.js (入口文件)
│   ├── common (通用模块,包权限,统一报错拦截等)
│       └── ...
│   ├── components (项目公共组件)
│       └── ...
│   ├── layouts (项目顶通)
│       └── ...
│   ├── utils (公共类)
│       └── ...
│   ├── routes (页面路由)
│   │   ├── Hello (对应 Hello 页面的代码)
│   │   │   ├── config (页面配置信息)
│   │   │       └── ...
│   │   │   ├── models (dva数据中心)
│   │   │       └── ...
│   │   │   ├── services (请求相关接口定义)
│   │   │       └── ...
│   │   │   ├── views (请求相关接口定义)
│   │   │       └── ...
│   │   │   └── index.js (router定义的路由信息)
├── .eslintignore
├── .eslintrc
├── .gitignore
└── .stylelintrc

Давайте посмотрим на структуру после того, как плагин синтезирует маршрутизатор:

├── package.json
├── README.md
├── zoo.config.js
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .stylelintrc
├── build (Webpack 配置目录)
│   └── webpack.dev.conf.js
├── src
│   ├── index.hbs
│   ├── main.js (入口文件)
│   ├── router-config.js (合成后的router文件)
│   ├── common (通用模块,包权限,统一报错拦截等)
│       └── ...
│   ├── components (项目公共组件)
│       └── ...
│   ├── layouts (项目顶通)
│       └── ...
│   ├── utils (公共类)
│       └── ...
│   ├── routes (页面路由)
│   │   ├── Hello (对应 Hello 页面的代码)
│   │   │   ├── config (页面配置信息)
│   │   │       └── ...
│   │   │   ├── models (dva数据中心)
│   │   │       └── ...
│   │   │   ├── services (请求相关接口定义)
│   │   │       └── ...
│   │   │   ├── views (请求相关接口定义)
│   │   │       └── ...
├── .eslintignore
├── .eslintrc
├── .gitignore
└── .stylelintrc

Таким образом, роль плагина заключается в следующем:

  1. Предоставляет некоторые другие вещи, которые загрузчик не может решить
  2. Предоставляет мощные методы расширения для выполнения более широкого круга задач.

После понимания общей функции плагина, давайте поговорим о том, как создать плагин.

Создать плагин

Hook

Прежде чем говорить о создании плагина, давайте поговорим о том, что такое хук.

Webpack запускает серию процессов в процессе компиляции.В такой серии процессов Webpack предоставляет некоторые ключевые узлы процесса для использования разработчиками.Это Hook, который можно сравнить с крючком жизненного цикла React.

Плагин предоставляет методы этих хуков для разработчиков, чтобы они могли выполнять некоторые дополнительные операции.При написании плагина нам также нужно понять, с каким хуком мы должны работать.

Как создать плагин

Давайте посмотрим на официальный случай, предоставленный Webpack:

const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
    apply(compiler) {
        // 代表开始读取 records 之前执行
        compiler.hooks.run.tap(pluginName, compilation => {
            console.log("webpack 构建过程开始!");
        });
    }
}

Из приведенного выше кода мы можем резюмировать следующее:

  • Плагин на самом деле класс.
  • Классу нужен метод применения, который выполняет конкретный метод плагина.
  • Одна вещь, которую делает метод плагина, — это регистрация метода журнала синхронной печати в хуке запуска.
  • Входной параметр метода apply вводит экземпляр компилятора.Экземпляр компилятора представляет собой основной механизм Webpack и представляет все элементы конфигурации, передаваемые интерфейсом командной строки и Node API.
  • Способ обратного вызова в экземпляре компиляции, модуль компиляции и доступ к соответствующему временно-зависимую ток.
Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;

Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。
                                              —— 摘自「深入浅出 Webpack」
  • Многие хуки определены в экземпляре CompiLer и экземпляре компиляции, которые можно передать实例.hooks.具体HookAccess, Hook также предоставляет три метода для использования, а именно tap, tapAsync и tapPromise. Эти три метода используются для определения того, как выполнять хуки. Например, tap означает регистрацию синхронных хуков, tapAsync означает регистрацию асинхронных хуков в режиме обратного вызова, а tapPromise означает регистрацию асинхронных хуков в режиме Promise. Вы можете увидеть исходный код этих трех типов. реализаций в Webpack.Для удобства чтения я добавил несколько комментариев.
// tap方法的type是sync,tapAsync方法的type是async,tapPromise方法的type是promise
// 源码取自Hook工厂方法:lib/HookCodeFactory.js
create(options) {
  this.init(options);
  let fn;
  // Webpack 通过new Function 生成函数
  switch (this.options.type) {
    case "sync":
      fn = new Function(
        this.args(), // 生成函数入参
        '"use strict";\n' +
        this.header() + // 公共方法,生成一些需要定义的变量
        this.contentWithInterceptors({ // 生成实际执行的代码的方法
          onError: err => `throw ${err};\n`, // 错误回调
          onResult: result => `return ${result};\n`, // 得到值的时候的回调
          resultReturns: true,
          onDone: () => "",
          rethrowIfPossible: true
        })
      );
      break;
    case "async":
      fn = new Function(
        this.args({
          after: "_callback"
        }),
        '"use strict";\n' +
        this.header() + // 公共方法,生成一些需要定义的变量
        this.contentWithInterceptors({ 
          onError: err => `_callback(${err});\n`, // 错误时执行回调方法
          onResult: result => `_callback(null, ${result});\n`, // 得到结果时执行回调方法
          onDone: () => "_callback();\n" // 无结果,执行完成时
        })
      );
      break;
    case "promise":
      let errorHelperUsed = false;
      const content = this.contentWithInterceptors({
        onError: err => {
          errorHelperUsed = true;
          return `_error(${err});\n`;
        },
        onResult: result => `_resolve(${result});\n`,
        onDone: () => "_resolve();\n"
      });
      let code = "";
      code += '"use strict";\n';
      code += this.header(); // 公共方法,生成一些需要定义的变量
      code += "return new Promise((function(_resolve, _reject) {\n"; // 返回的是 Promise
      if (errorHelperUsed) {
        code += "var _sync = true;\n";
        code += "function _error(_err) {\n";
        code += "if(_sync)\n";
        code +=
          "_resolve(Promise.resolve().then((function() { throw _err; })));\n";
        code += "else\n";
        code += "_reject(_err);\n";
        code += "};\n";
      }
      code += content; // 判断具体执行_resolve方法还是执行_error方法
      if (errorHelperUsed) {
        code += "_sync = false;\n";
      }
      code += "}));\n";
      fn = new Function(this.args(), code);
      break;
  }
  this.deinit(); // 清空 options 和 _args
  return fn;
}

Webpack предоставляет следующие десять видов хуков, и все конкретные хуки в коде относятся к одному из следующих десяти типов.

// 源码取自:lib/index.js
"use strict";

exports.__esModule = true;
// 同步执行的钩子,不能处理异步任务
exports.SyncHook = require("./SyncHook");
// 同步执行的钩子,返回非空时,阻止向下执行
exports.SyncBailHook = require("./SyncBailHook");
// 同步执行的钩子,支持将返回值透传到下一个钩子中
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
// 同步执行的钩子,支持将返回值透传到下一个钩子中,返回非空时,重复执行
exports.SyncLoopHook = require("./SyncLoopHook");
// 异步并行的钩子
exports.AsyncParallelHook = require("./AsyncParallelHook");
// 异步并行的钩子,返回非空时,阻止向下执行,直接执行回调
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
// 异步串行的钩子
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
// 异步串行的钩子,返回非空时,阻止向下执行,直接执行回调
exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook");
// 支持异步串行 && 并行的钩子,返回非空时,重复执行
exports.AsyncSeriesLoopHook = require("./AsyncSeriesLoopHook");
// 异步串行的钩子,下一步依赖上一步返回的值
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");
// 以下 2 个是 hook 工具类,分别用于 hooks 映射以及 hooks 重定向
exports.HookMap = require("./HookMap");
exports.MultiHook = require("./MultiHook");

Вот несколько простых примеров:

  • Run Hook в официальном случае выше будет выполняться перед началом чтения записей, его тип AsyncSeriesHook, глядя на исходный код, можно обнаружить, что run Hook может выполнять как синхронный метод tap, так и асинхронные методы tapAsync и tapPromise, поэтому можно также написать:
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
    apply(compiler) {
        compiler.hooks.run.tapAsync(pluginName, (compilation, callback) => {
            setTimeout(() => {
              console.log("webpack 构建过程开始!");
              callback(); // callback 方法为了让构建继续执行下去,必须要调用
            }, 1000);
        });
    }
}
  • Другой пример, такой как неудачный хук, будет выполняться после сбоя компиляции. Его тип — SyncHook. Глядя на исходный код, вы можете обнаружить, что при вызове методов tapAsync и tapPromise сразу выдается ошибка.

Для некоторых синхронных методов рекомендуется использовать tap напрямую для регистрации метода.Для асинхронных решений tapAsync реализует обратный вызов, выполняя метод обратного вызова.Если выполняемый метод возвращает обещание, рекомендуется использовать tapPromise для регистрации метода.

Тип хука можно запросить через официальный API,адресный портал

// 源码取自:lib/SyncHook.js
const TAP_ASYNC = () => {
  throw new Error("tapAsync is not supported on a SyncHook");
};

const TAP_PROMISE = () => {
  throw new Error("tapPromise is not supported on a SyncHook");
};

function SyncHook(args = [], name = undefined) {
  const hook = new Hook(args, name);
  hook.constructor = SyncHook;
  hook.tapAsync = TAP_ASYNC;
  hook.tapPromise = TAP_PROMISE;
  hook.compile = COMPILE;
  return hook;
}

После объяснения конкретного метода выполнения давайте поговорим о процессе Webpack и о том, что такое Tapable.

Webpack && Tapable

Среда выполнения веб-пакета

Чтобы понять плагин, мы сначала должны иметь общее представление о процессе упаковки Webpack.

  1. Когда мы упаковываем, мы сначала объединим файл конфигурации Webpack и параметры командной строки в опции.
  2. Передайте параметры конструктору компилятора, сгенерируйте экземпляр компилятора и создайте экземпляр хуков в компиляторе.
  3. Объект компилятора выполняет метод run и автоматически запускает ключевые хуки, такие как beforeRun, run, beforeCompile и compile.
  4. Вызов конструктора Compilation для создания объекта компиляции.Компиляция отвечает за управление всеми модулями и соответствующими зависимостями, а триггеры делают Hook после создания.
  5. Выполните методcompile.addEntry(), addEntry используется для анализа всех входных файлов, рекурсивного анализа уровня за уровнем, вызова метода NormalModuleFactory, создания экземпляра модуля для каждой зависимости и запуска ключевых хуков, таких как beforeResolve, resolver, afterResolve и модуль в процессе выполнения.
  6. Используя экземпляр модуля, сгенерированный на шаге 5, в качестве входного параметра, выполните методы Compilation.addModule() и Compilation.buildModule() для рекурсивного создания объектов модуля и зависимых объектов модуля.
  7. Вызовите метод seal для генерации кода, организации и вывода основного файла и фрагмента и, наконец, вывода.

Tapable

Tapable — это основная библиотека инструментов Webpack. Она предоставляет определения абстрактных классов для всех хуков. Многие объекты Webpack наследуются от класса Tapable. Например, tap, tapAsync и tapPromise, упомянутые выше, доступны через Tapable. Исходный код выглядит следующим образом (часть кода перехвачена):

// 第二节 “创建一个 Plugin” 中说的 10 种 Hooks 都是继承了这两个类
// 源码取自:tapable.d.ts
declare class Hook<T, R, AdditionalOptions = UnsetAdditionalOptions> {
  tap(options: string | Tap & IfSet<AdditionalOptions>, fn: (...args: AsArray<T>) => R): void;
}

declare class AsyncHook<T, R, AdditionalOptions = UnsetAdditionalOptions> extends Hook<T, R, AdditionalOptions> {
  tapAsync(
    options: string | Tap & IfSet<AdditionalOptions>,
    fn: (...args: Append<AsArray<T>, InnerCallback<Error, R>>) => void
  ): void;
  tapPromise(
    options: string | Tap & IfSet<AdditionalOptions>,
    fn: (...args: AsArray<T>) => Promise<R>
  ): void;
}

API общих хуков

может относиться кWebpack

В этой статье перечислены некоторые часто используемые хуки и соответствующие им типы:

Compiler Hooks

Hook type передача
run AsyncSeriesHook Прежде чем приступить к чтению записей
compile SyncHook После создания новой компиляции
emit AsyncSeriesHook Перед генерацией ресурсов в выходной каталог
done SyncHook Компиляция завершена

Compilation Hooks

Hook type передача
buildModule SyncHook Запускается перед началом сборки модуля
finishModules SyncHook Все модули собраны
optimize SyncHook Запускается, когда начинается фаза оптимизации

Применение плагина в проекте

После разговора о таком большом количестве теоретических знаний давайте взглянем на реальную борьбу плагина в проекте: как объединить файлы маршрутизатора в каждом подмодуле в router-config.js.

задний план:

В проекте React наши файлы маршрутизатора обычно пишутся в проекте. Если проект содержит много страниц, все маршрутизаторы бизнес-модулей неизбежно будут связаны, поэтому мы разработали плагин. При сборке и упаковке плагин будет читать файлы маршрутизатора в все папки, а затем объединить их вместе, чтобы сформировать единый файл конфигурации маршрутизатора, который может легко решить проблему связанности бизнеса. Это приложение плагина.

выполнить:

const fs = require('fs');
const path = require('path');
const _ = require('lodash');

function resolve(dir) {
  return path.join(__dirname, '..', dir);
}

function MegerRouterPlugin(options) {
  // options是配置文件,你可以在这里进行一些与options相关的工作
}

MegerRouterPlugin.prototype.apply = function (compiler) {
  // 注册 before-compile 钩子,触发文件合并
  compiler.plugin('before-compile', (compilation, callback) => {
    // 最终生成的文件数据
    const data = {};
    const routesPath = resolve('src/routes');
    const targetFile = resolve('src/router-config.js');
    // 获取路径下所有的文件和文件夹
    const dirs = fs.readdirSync(routesPath);
    try {
      dirs.forEach((dir) => {
        const routePath = resolve(`src/routes/${dir}`);
        // 判断是否是文件夹
        if (!fs.statSync(routePath).isDirectory()) {
          return true;
        }
        delete require.cache[`${routePath}/index.js`];
        const routeInfo = require(routePath);
        // 多个 view 的情况下,遍历生成router信息
        if (!_.isArray(routeInfo)) {
          generate(routeInfo, dir, data);
        // 单个 view 的情况下,直接生成
        } else {
          routeInfo.map((config) => {
            generate(config, dir, data);
          });
        }
      });
    } catch (e) {
      console.log(e);
    }

    // 如果 router-config.js 存在,判断文件数据是否相同,不同删除文件后再生成
    if (fs.existsSync(targetFile)) {
      delete require.cache[targetFile];
      const targetData = require(targetFile);
      if (!_.isEqual(targetData, data)) {
        writeFile(targetFile, data);
      }
    // 如果 router-config.js 不存在,直接生成文件
    } else {
      writeFile(targetFile, data);
    }

    // 最后调用 callback,继续执行 webpack 打包
    callback();
  });
};
// 合并当前文件夹下的router数据,并输出到 data 对象中
function generate(config, dir, data) {
  // 合并 router
  mergeConfig(config, dir, data);
  // 合并子 router
  getChildRoutes(config.childRoutes, dir, data, config.url);
}
// 合并 router 数据到 targetData 中
function mergeConfig(config, dir, targetData) {
  const { view, models, extraModels, url, childRoutes, ...rest } = config;
  // 获取 models,并去除 src 字段
  const dirModels = getModels(`src/routes/${dir}/models`, models);
  const data = {
    ...rest,
  };
  // view 拼接到 path 字段
  data.path = `${dir}/views${view ? `/${view}` : ''}`;
  // 如果有 extraModels,就拼接到 models 对象上
  if (dirModels.length || (extraModels && extraModels.length)) {
    data.models = mergerExtraModels(config, dirModels);
  }
  Object.assign(targetData, {
    [url]: data,
  });
}
// 拼接 dva models
function getModels(modelsDir, models) {
  if (!fs.existsSync(modelsDir)) {
    return [];
  }
  let files = fs.readdirSync(modelsDir);
  // 必须要以 js 或者 jsx 结尾
  files = files.filter((item) => {
    return /\.jsx?$/.test(item);
  });
  // 如果没有定义 models ,默认取 index.js
  if (!models || !models.length) {
    if (files.indexOf('index.js') > -1) {
      // 去除 src
      return [`${modelsDir.replace('src/', '')}/index.js`];
    }
    return [];
  }
  return models.map((item) => {
    if (files.indexOf(`${item}.js`) > -1) {
      // 去除 src
      return `${modelsDir.replace('src/', '')}/${item}.js`;
    }
  });
}
// 合并 extra models
function mergerExtraModels(config, models) {
  return models.concat(config.extraModels ? config.extraModels : []);
}
// 合并子 router
function getChildRoutes(childRoutes, dir, targetData, oUrl) {
  if (!childRoutes) {
    return;
  }
  childRoutes.map((option) => {
    option.url = oUrl + option.url;
    if (option.childRoutes) {
      // 递归合并子 router
      getChildRoutes(option.childRoutes, dir, targetData, option.url);
    }
    mergeConfig(option, dir, targetData);
  });
}

// 写文件
function writeFile(targetFile, data) {
  fs.writeFileSync(targetFile, `module.exports = ${JSON.stringify(data, null, 2)}`, 'utf-8');
}

module.exports = MegerRouterPlugin;

результат:

Файлы перед объединением:

module.exports = [
  {
    url: '/category/protocol',
    view: 'protocol',
  },
  {
    url: '/category/sync',
    models: ['sync'],
    view: 'sync',
  },
  {
    url: '/category/list',
    models: ['category', 'config', 'attributes', 'group', 'otherSet', 'collaboration'],
    view: 'categoryRefactor',
  },
  {
    url: '/category/conversion',
    models: ['conversion'],
    view: 'conversion',
  },
];

Комбинированный файл:

module.exports = {
  "/category/protocol": {
    "path": "Category/views/protocol"
  },
  "/category/sync": {
    "path": "Category/views/sync",
    "models": [
      "routes/Category/models/sync.js"
    ]
  },
  "/category/list": {
    "path": "Category/views/categoryRefactor",
    "models": [
      "routes/Category/models/category.js",
      "routes/Category/models/config.js",
      "routes/Category/models/attributes.js",
      "routes/Category/models/group.js",
      "routes/Category/models/otherSet.js",
      "routes/Category/models/collaboration.js"
    ]
  },
  "/category/conversion": {
    "path": "Category/views/conversion",
    "models": [
      "routes/Category/models/conversion.js"
    ]
  },
}

Окончательный проект создаст файл router-config.js.

конец

Я надеюсь, что после прочтения этой главы у вас будет предварительное представление о подключаемом модуле Webpack и вы сможете написать свой собственный подключаемый модуль для применения в своих проектах.

Если в статье что-то не так, поправьте меня.

Рекомендуемое чтение

Реализуйте экспозицию переднего плана и скрытых точек с помощью пользовательских инструкций Vue.

Схема кэширования списка страниц H5

работы с открытым исходным кодом

  • Zhengcaiyun интерфейсный таблоид

адрес с открытым исходным кодомwww.zoo.team/openweekly/(На главной странице официального сайта таблоида есть группа обмена WeChat)

Карьера

ZooTeam, молодая, увлеченная и творческая команда, связанная с отделом исследований и разработок продукции Zhengcaiyun, базируется в живописном Ханчжоу. В настоящее время в команде более 40 фронтенд-партнеров, средний возраст которых составляет 27 лет, и почти 30% из них — инженеры полного стека, настоящая молодежная штурмовая группа. В состав членов входят «ветераны» солдат из Ali и NetEase, а также первокурсники из Чжэцзянского университета, Университета науки и технологий Китая, Университета Хандянь и других школ. В дополнение к ежедневным деловым связям, команда также проводит технические исследования и фактические боевые действия в области системы материалов, инженерной платформы, строительной платформы, производительности, облачных приложений, анализа и визуализации данных, а также продвигает и внедряет ряд внутренних технологий. Откройте для себя новые горизонты передовых технологических систем.

Если вы хотите измениться, вас забрасывают вещами, и вы надеетесь начать их бросать; если вы хотите измениться, вам сказали, что вам нужно больше идей, но вы не можете сломать игру; если вы хотите изменить , у вас есть возможность добиться этого результата, но вы не нужны; если вы хотите изменить то, чего хотите достичь, вам нужна команда для поддержки, но вам некуда вести людей; если вы хотите изменить установившийся ритм, это будет "5 лет рабочего времени и 3 года стажа работы"; если вы хотите изменить исходный Понимание хорошее, но всегда есть размытие того слоя оконной бумаги.. , Если вы верите в силу веры, верьте, что обычные люди могут достичь необыкновенных вещей, и верьте, что они могут встретить лучшего себя. Если вы хотите участвовать в процессе становления бизнеса и лично способствовать росту фронтенд-команды с глубоким пониманием бизнеса, надежной технической системой, технологиями, создающими ценность, и побочным влиянием, я думаю, что мы должны говорить. В любое время, ожидая, пока вы что-нибудь напишете, отправьте это наZooTeam@cai-inc.com