Пишем собственную библиотеку при помощи webpack и es6


Два месяца назад я опубликовал starter-pack для react на основе webpack. Сегодня я узнал, что мне нужно почти то же самое, но без recat. Это упрощает установку, но есть еще некоторые сложные детали. Итак, я сделал новый репозиторий webpack-library-starter и собрал все то, что необходимо для создания JavaScript библиотеки.

Прежде всего, что я имел в виду под словами “ библиотека”

Мое определение библиотеки в контексте JavaScript — это фрагмент кода, обеспечивающий определенную функциональность. Он делает одну вещь и делает это хорошо. В идеале она не должна зависеть от других библиотек или фреймворков. Хороший пример — библиотека jQuery. React и vue.js также можно считать библиотекой.

Библиотека должна:

  • Быть доступна для использования в браузере, через тег <script>
  • Быть доступна через npm
  • Быть совместимой с системой модулей ES6(ES2015), commonjs, AMD.

Структура директорий

Я собираюсь создать следующую структуру каталогов:

+-- lib
|   +-- library.js
|   +-- library.min.js
+-- src
|   +-- index.js
+-- test

Где в src лежат исходные файлы, а в lib скомпилированная версия библиотеки. Это означает то, что точкой входа библиотеки будет файл в lib, а не в src.

The starter

Мне очень нравится новая спецификация ES6. Плохо то, что вокруг нее есть какая-то дополнительная обвязка. Когда-нибудь мы, вероятно, напишем такой JavaScript без необходимости транспилировать, но сегодня это не так. Обычно нам нужна интеграция babel. Babel может конвертировать наши файлы из ES6 в ES5, но он не предназначен для создания пакетов. Другими словами, если у нас есть следующие файлы:

+-- lib
+-- src
    +-- index.js (es6)
    +-- helpers.js (es6)

То используя babel мы получим:

+-- lib
|   +-- index.js (es5)
|   +-- helpers.js (es5)
+-- src
    +-- index.js (es6)
    +-- helpers.js (es6)

К сожалению, babel не разрешает imports/requires. Так что нам нужен bundler и, как вы можете догадаться, мой выбор для этого — webpack. То, чего я хочу достичь в итоге:

+-- lib
|   +-- library.js (es5)
|   +-- library.min.js (es5)
+-- src
    +-- index.js (es6)
    +-- helpers.js (es6)

npm команды

npm предоставляет хороший механизм для выполнения команд  — scripts. Мы должны иметь как минимум три:

"scripts": {
  "build": "...",
  "dev": "...",
  "test": "..."
}
  • npm run build — эта должна делать финальную минифицированную версию нашей библиотеки
  • npm run dev — аналогично build но не делает минификцию и продолжает работать в режиме наблюдения
  • npm run test — запуск тестов

Версии для разработки

npm run dev должна запускать webpack и делать файл — lib/library.js. Мы начинаем с файла конфигурации webpack:

// webpack.config.js

var webpack = require('webpack');
var path = require('path');
var libraryName = 'library';
var outputFile = libraryName + '.js';

var config = {
  entry: __dirname + '/src/index.js',
  devtool: 'source-map',
  output: {
    path: __dirname + '/lib',
    filename: outputFile,
    library: libraryName,
    libraryTarget: 'umd',
    umdNamedDefine: true
  },
  module: {
    loaders: [
      {
        test: /(\.jsx|\.js)$/,
        loader: 'babel',
        exclude: /(node_modules|bower_components)/
      },
      {
        test: /(\.jsx|\.js)$/,
        loader: "eslint-loader",
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    root: path.resolve('./src'),
    extensions: ['', '.js']
  }
};

module.exports = config;

Даже если у вас нет опыта работы с webpack, вы сможете понять, что делает этот конфигурационный файл. Определяем вход (entery) и выход (output) компиляции. Поле module говорит, что нужно применять для каждого файла во время обработки. В нашем случае это babel и ESLint где ESLint используется для проверки синтаксиса и правильности кода.

Тут небольшая уловка — несколько свойств, которые я пропустил. Речь идет обlibrary, libraryTarget и umdNamedDefine. Сначала я попробовал без них и на выходе из библиотеки я получил что-то вроде этого:

(function(modules) {
  var installedModules = {};

  function __webpack_require__(moduleId) {
    if(installedModules[moduleId]) return installedModules[moduleId].exports;

    var module = installedModules[moduleId] = {
      exports: {},
      id: moduleId,
      loaded: false
    };
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    module.loaded = true;
    return module.exports;
  }

  __webpack_require__.m = modules;
  __webpack_require__.c = installedModules;
  __webpack_require__.p = "";

  return __webpack_require__(0);
})([
  function(module, exports) {
    // ... my code here
  }

Так выглядит каждый скомпилированный webpack’ом пакет. Он использует подход аналогичный browserify. Есть self-invoking функция, которая получает все модули, используемые в нашем приложении. Каждый из них остается позади индекса массива modules. В приведенном выше коде у нас есть только один модуль и __webpack_require__(0) эффективно запускает код в нашем src/index.js файле.

Наличие такого пакета не соответствует всем требованиям, упомянутым в начале этой статьи, поскольку мы ничего не экспортируем. Код будет скрыт для веб-страницы. Тем не менее, добавление library, libraryTarget и umdNamedDefine делают так, что webpack добавляет в начале файла такой кусок кода:

(function webpackUniversalModuleDefinition(root, factory) {
  if(typeof exports === 'object' && typeof module === 'object')
    module.exports = factory();
  else if(typeof define === 'function' && define.amd)
    define("library", [], factory);
  else if(typeof exports === 'object')
    exports["library"] = factory();
  else
    root["library"] = factory();
})(this, function() {
return (function(modules) {
 ...
 ...

Установка libraryTarget в UMD означает что для конечного результата будет использоваться universal module definition. И действительно, этот фрагмент кода распознает среду и предоставляет надлежащий загрузочный механизм для нашей библиотеки.

Построение версии для разработки

Единственное различие между разработкой и production режимом для webpack — это минификация. Запуск npm run build должен собирать уменьшенную версию — library.min.js. webpack имеет хороший встроенный плагин для этого:

// webpack.config.js

...
var UglifyJsPlugin = webpack.optimize.UglifyJsPlugin;
var env = process.env.WEBPACK_ENV;

var libraryName = 'library';
var plugins = [], outputFile;

if (env === 'build') {
  plugins.push(new UglifyJsPlugin({ minimize: true }));
  outputFile = libraryName + '.min.js';
} else {
  outputFile = libraryName + '.js';
}

var config = {
  entry: __dirname + '/src/index.js',
  devtool: 'source-map',
  output: { ... },
  module: { ... },
  resolve: { ... },
  plugins: plugins
};

module.exports = config;

UglifyJsPlugin сработает, если добавить его в массив плагинов. Тут еще кое что надо уточнить. Нам нужно условие, где мы даем указания webpack, какой пакет собирать (production или development). Одним из популярных подходов является определение переменной окружения и ее передача из командной строки. Например:

// package.json

"scripts": {
  "build": "WEBPACK_ENV=build webpack",
  "dev": "WEBPACK_ENV=dev webpack --progress --colors --watch"
}

(Обратите внимание на опцию --watch. Она заставляте webpack постоянно работать и наблюдать за изменениями)

Тестирование

Я обычно использую Mocha и Chai для тестирования, и это то, что я добавил в начале. Снова возникают сложности, Mocha не понимает ЕС6 файлы, но, к счастью babel решил эту проблему.

// package.json
"scripts": {
  ...
  "test": "mocha --compilers js:babel-core/register --colors -w ./test/*.spec.js"
}

Важным элементом является опция --compilers. Она позволяет нам обрабатывать входящий файл перед его запуском.

Несколько других конфигурационных файлов

Babel получил некоторые серьезные изменения в новейшей версии 6. Теперь применяются preset для тех мест, для которых мы хотим получить преобразования. Один из самых простых способов — настроить это — .babelrc файл:

// .babelrc
{
  "presets": ["es2015"],
  "plugins": ["babel-plugin-add-module-exports"]
}

ESLint предоставляет то же самое, вот наш .eslintrc:

// .eslintrc
{
  "ecmaFeatures": {
    "globalReturn": true,
    "jsx": true,
    "modules": true
  },
  "env": {
    "browser": true,
    "es6": true,
    "node": true
  },
  "globals": {
    "document": false,
    "escape": false,
    "navigator": false,
    "unescape": false,
    "window": false,
    "describe": true,
    "before": true,
    "it": true,
    "expect": true,
    "sinon": true
  },
  "parser": "babel-eslint",
  "plugins": [],
  "rules": {
    // ... lots of lots of rules here
  }
}

Ссылки

Стартер доступен в GitHub здесь github.com/krasimir/webpack-library-starter.

Использованные инструменты:

  • webpack
  • Babel
  • ESLint
  • Mocha, Chai
  • UMD

Зависимости:

// package.json
"devDependencies": {
  "babel": "6.3.13",
  "babel-core": "6.1.18",
  "babel-eslint": "4.1.3",
  "babel-loader": "6.1.0",
  "babel-plugin-add-module-exports": "0.1.2",
  "babel-preset-es2015": "6.3.13",
  "chai": "3.4.1",
  "eslint": "1.7.2",
  "eslint-loader": "1.1.0",
  "mocha": "2.3.4",
  "webpack": "1.12.9"
}

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *