Пишем собственную библиотеку при помощи 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"
}

Как написать Virtual Dom

Есть 2 вещи, которые необходимо знать. Вам не нужно погружаться в исходный код React’а или других библиотек, они довольно большие и сложные, на самом деле Virtual DOM может быть написан в меньше чем 50 строк кода.

  1. Virtual DOM это аналог настоящего DOM
  2. Когда мы меняем что-то в дереве Virtual DOM , мы создаем новый Virtual DOM. Алгоритм сравнения двух деревьев вычисляет разницу и вносит только необходимые, минимальные правки в  настоящий DOM.

Читать далее Как написать Virtual Dom

html5 notification VS web push api

В чем разница между html5 notification и web push API?

html5 notification используется для отображения уведомлений с веб-страницы, в то время как Push API, используется для отправки уведомлений удаленно, даже если веб-страница неактивна.

Koa vs Express

Философия koa направлена на «исправление и замену узлов», в то время как express — «расширяет узел». Koa использует co, чтобы избавить приложение от callback-hell’а и упростить обработку ошибок. Он выставляет собственные this.request и this.response вместо аргументов функции, объектов req и res.

С другой стороны, express расширяет объекты req и res дополнительными методами и свойствами, включает в себя много других «фишек» таких как: маршрутизация и шаблонизация. В koa этого нет.

Таким образом, koa можно рассматривать как абстрактный http модуль для node.js, а express это полноценный фреймворк.

Redis API — основные возможности

Для взаимодействия между node.js и nosql базой данных — redis, можно воспользоваться модулем node_redis (npm install redis).

Структуры данных поддерживаемые redis’ом:

  • Хеш-таблицы
  • Списки
  • Пары ключ/значение
  • Множества (set)

Читать далее Redis API — основные возможности

Сниппет для require

Если вы пользуетесь текстовым редактором — sublime text и вам надоело постоянно писать require(…), я предлагаю воспользоваться данным снипетом.
Снипет попытается предложить вам автоматическую подстановку и установит курсор в кавычки, между скобочками — require(‘cursor will be here‘), после повторного нажатия на таб, переведет курсор на новую строку.




req
requre
source.js

Модули для node.JS

В node.js модуль — это файл, который можно подключить при помощи require();
Переменные:
var — обычная переменная,
export — переменная, которая передает объект из модуля в то место где его вызывают,
global — глобальный объект, аналог браузерного window, не рекомендуется использовать его в своих скриптах.

Модуль можно объявить в директории, создав в ней файл index.js.

Promise

Promise (обычно их так и называют «промис») это объект позволяющий удобно организовать работу с асинхронным кодом в JavaScript.

Тело promise — это функция, из которой можно выйти, вызвав второй или первый ее аргумент:


let promise = new Promise((resolve, reject)=> {
setTimeout(()=>{
console.log(1000);
// Вызываем resolve, отвечающий за успех.
resolve();
}, 1000)
})

// Навешиваем обработчики, первый для вызова resolve, отвечающего за успех, второй для второго, вызывается в случае если произошла ошибка и был
// вызван reject.

promise.then(() => {
console.log('resolve')
}, () => {
console.log('reject')
})

ECMASCRIPT-2015: Объекты

Короткие свойства.

Свойства можно задать используя переменную.


let x = 1;
let y = 2;

var o = {
x,
y
}

console.log(o.x + o.y) // 3

Вычисляемые имена свойств

let o = {
['x'+'y']: 1
}

console.log(o.xy) // 1

Object.assign

Получает список объектов и записывает их свойства в первый.


let x = {
x:1
}
let y = {
y:1
}
let z = {
z:1
}

var o = Object.assign(x,y,z);
console.log(o); // {"x":1,"y":1,"z":1}

Методы

Теперь методы можно записывать сразу:

var o = {
meth() {
return 10
}
}
console.log(o.meth())

JavaScript шаблоны: Объекты

  • Чтобы избежать засорения глобального объекта используйте пространство имён.
  • Объявляйте зависимости в начале модуля как переменные. (Это уменьшит размер файла при минификации)
  • Вы можете скрывать данные используя замыкания а так же создавать методы доступные только самому объекту (недоступные из вне).
  • Все вышеперечисленное является строительными блоками шаблона «модуль»
  • В JavaScript отсутствует возможность создавать константы (до версии ECMASCRIPT 2015). Но вы  можете определить объект «const» в котором будет реализована возможность добавлять константы.
  • Конструкторы  могут иметь статические методы, которые не меняются от экземпляра к экземпляру.
  • Старайтесь возвращать из методов ссылку на объект. Это позволит организовывать удобные цепочки вызовов, которые, однако, сложно отлаживать.