Плагин – это независимый элемент общего назначения. При подключении его к приложению не требуется лишних усилий и конфигураций. Фронтенд плагин – это календарь, слайдер, буферклавиатуры. Фронтенд плагином может быть все, что увеличивает функциональность доступных элементов в приложении. Этот плагин может представлять собой отдельное крупное приложение – например, фотогалерею с увеличением для превью.
Мне кажется, плагин должен быть прост в использовании. Мне не нравятся плагины, для которых требуется много действий: установка зависимостей вроде jQuery, jQuery UI, инициализация приложения и т. д. Также я не хочу, чтобы плагин импортировал много вещей (библиотеку иконок, главный файл стилей, стилевой файл для темы, дистрибутив JavaScript и др.). Сейчас в тренде изоморфные плагины. Это значит, что они импортируются прямо в тег script либо через сервер с помощью синтаксиса require
или import
. Благодаря этому люди могут воспользоваться средствами запуска задач или упаковщиками – grunt
, gulp
или webpack
.
Я считаю, что в 2018 плагин должен быть UMD, npm-модулем, который имеет лишь один объект для работы.
Самая главная проблема – не «Как люди будут использовать мой плагин?», а «Как я вообще его разработаю?»
Разработка JavaScript plugin должна быть простой, а код должен быть удобным в поддержке приложения. Он должен быть разделен на несколько фрагментов, чтобы другим разработчикам и сопроводителям было ясно, что к чему. Именно здесь в игру вступают ES6, SASS и Webpack.
ES6+ или ES2015+ - то, как мы должны писать JavaScript в 2018. Так разработка будет проходить интереснее и быстрее. SASS касается то же самое. Так как ни синтаксис ES6, ни SASS не поддерживаются в браузерах (некоторые из них все же поддерживают ES6), нужно компилировать их в ES5 и CSS. Тут-то и пригодится Webpack.
Webpack – это модульный упаковщик. Мы даем Webpack одну точку входа (файл JavaScript), а он затем поднимается по дереву зависимостей (в него входят все файлы, импортированные в наш файл) и объединяет их в один файл. Webpack умеет и другие вещи: компилирование SASS в CSS, создание кода JavaScript, который динамически вставит CSS в HTML. Это значит, что клиенту не придется импортировать файлы .css
по отдельности и по порядку; плагин и без этого будет отлично работать.
В этой статье мы поговорим о процессе разработки простого JavaScript плагина UserList
, который просто берет массив гостей и вставляет их в div. Этот JavaScript plugin способен динамически добавлять новых гостей с помощью метода .addUser
.
Начнем с создания папки проекта. Я назову ее user-list
. Это название также будет названием пакета модуля npm. Если у вас есть репозиторий GIT, вы можете клонировать его или инициализировать git в этой папке. Это важный шаг, ведь плагин должен иметь открытый код.
Следующий шаг – npm init
, который создает файл package.json
для хранения зависимостей и отправки модуля в репозиторий npm.
Теперь вы должны понять, какие пакеты вам нужно установить для начала разработки. Так как мы говорили о ES6 и SASS, вам нужен будет компилятор для того, чтобы перевести их в ES5 и CSS. Вам понадобится система сборки кода, чтобы меньше использовать CSS и JavaScript и объединить их в один файл на JavaScript. Существует много таких инструментов – Grunt, Gulp, Webpack. Я предпочитаю Webpack, вы можете выбрать что-то иное.
Чтобы установить Webpack, нужно провести следующую команду:
npm install --save-dev webpack webpack-dev-server
Здесь нужно помнить 2 вещи. --save-dev
сохраняет пакеты в devDependencies
файла package.json
. Это значит, что это инструментальные пакеты. Пользователь может не устанавливать их и при этом пользоваться плагином. webpack-dev-server
– локальный сервер, который вы будете использовать, чтобы увидеть, как выглядит плагин, во время разработки. Этот сервер будет автоматически отправлять события перезагрузки в браузер. Страницы будут перезагружаться, когда вы будете что-либо изменять в коде. Вам не придется перезагружать страницу вручную, чтобы увидеть изменения.
Webpack ничего не компилирует самостоятельно. Он просто объединяет несколько вещей. Чтобы компилировать ES6 в ES5 и SASS в CSS, нам понадобятся загрузчики.
- Чтобы загрузить модули ES6 в node.js с использованием синтаксиса import (node.js ATM не поддерживает синтаксис import), нам нужно установить babel-core и babel-loader;
- Babel нужны дополнительные пресеты, такие как babel-preset-env, которые будут выполнять фактическую компиляцию ES6+ в ES5. Если бы вы также использовали React.js, вы бы установили babel-preset-Reaction для компиляции JSX в ES5;
- babel-preset-transform-object-rest-spread — это плагин для поддержки синтаксиса Object rest/spread, если ваш пресет babel не поддерживает его;
- Чтобы загрузить файлы .scss в файл JavaScript с использованием синтаксиса import на ES6 и скомпилировать их в CSS, нам нужно установить sass-loader. Вам также понадобится node-sass, который будет выполнять непосредственно компиляцию;
- Чтобы минимизировать (для уменьшения размера) использование CSS и добавить префиксы (для совместимости с браузерами), вам нужно использовать postcss-loader. Postcss также зависит от других плагинов, таких как cssnano и autoprefixer, которые выполняют саму работу;
- Вам нужны css-loader и style-loader для импорта файла .css в JavaScript (который генерирует sass-loader) и, наконец, для внедрения в DOM в теге style;
- uglifyjs-webpack-plugin используется для минимизации (сжатия) пакета JavaScript, созданного Webpack;
- html-webpack-plugin необходим webpack-dev-server для запуска предварительного просмотра вашего плагина и внедрения в него дистрибутивных файлов .js.
Итак, теперь наш <code rel="CODE">package.json</code> выглядит следующим образом:
"name": "user-list",
"version": "1.0.0",
"main": "dist/index.js",
"devDependencies": {
"autoprefixer": "^8.5.0",
"babel-core": "^6.26.3",
"babel-loader": "^7.1.4",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-preset-env": "^1.7.0",
"css-loader": "^0.28.11",
"cssnano": "^3.10.0",
"html-webpack-plugin": "^3.2.0",
"node-sass": "^4.9.0",
"postcss-loader": "^2.1.5",
"sass-loader": "^7.0.1",
"style-loader": "^0.21.0",
"uglifyjs-webpack-plugin": "^1.2.5",
"webpack": "^3.12.0",
"webpack-dev-server": "^2.11.2"
}
Поскольку мы создаем плагин VanillaJS, мы не зависим от каких-либо других зависимостей, таких как React или jQuery. Следовательно, вы видите только devDependencies
в package.json
. Поле main указывает файл, который пользователь будет импортировать, когда он использует синтаксис import UserList from 'user-list'
.
Давайте перейдем к структуре проекта. Нужно отделить исходную папку от дистрибутивной папки. dist
или build
обычно является папкой дистрибутива, а src
- исходной папкой.
Вот как должна выглядеть наша структура каталогов:
user-list
├── dist
| ├── index.html
| └── index.js
├── index.html
├── .babelrc
├── package.json
├── postcss.config.js
├── src
| ├── index.js
| ├── lib
| | ├── user-list.js
| ├── scss
| | └── styles.scss
| └── util
| ├── string.js
└── webpack.config.js
Пока просто игнорируйте содержимое папки dist
, потому что оно будет сгенерировано Webpack, и мы поговорим об этом позже.
Взгляните на папку src
, где находятся наши исходные файлы. index.js
— это точка входа для Webpack, куда мы импортируем все необходимые вещи и экспортируем что-то для конечного пользователя. У нас есть src/lib
для файлов функций, src/util
для функций утилит и src/scss
для файлов .scss
.
Webpack не важен ни один из этих файлов, кроме index.js
в каталоге src
. index.js
— это файл входа для Webpack. Мы сами так настроили Webpack.
В webpack.config.js
мы пишем конфигурацию для Webpack. Этот файл сообщает веб-пакету, где находится файл входа и куда выводить окончательный файл дистрибутива.
const path = require('path');
const webpack = require('webpack');
const HTMLWebpackPlugin = require('html-webpack-plugin');
const uglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
library: 'UserList',
libraryTarget: 'umd',
libraryExport: 'default',
path: path.resolve(__dirname, 'dist'),
filename: 'index.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: ['babel-loader']
},
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
'postcss-loader',
'sass-loader'
]
}
]
},
plugins: [
new uglifyJsPlugin(),
new HTMLWebpackPlugin({
template: path.resolve(__dirname, 'index.html')
}),
new webpack.HotModuleReplacementPlugin(),
]
};
Чтобы узнать о конфигурации Webpack, посетите webpack.js.org. Мы используем uglifyJsPlugin для минимизации JavaScript. Тем не менее, вы обнаружите, что код трудно отлаживать, когда он минифицирован, поэтому отключите плагин при разработке или используйте исходные карты.
Для нашего JavaScript plugin крайне важна работа с ключами library, libraryTarget
и libraryExport
в выходном объекте.
Ключ libraryTarget
указывает, как пользователь будет импортировать этот плагин: с использованием синтаксиса ES6 import
, require.js require
или чего-то еще. Мы хотим, чтобы пользователь реализовал этот плагин так, как он хочет, поэтому мы использовали umd, что означает определение универсального модуля.
libraryExport: 'default'
будет использовать синтаксис export по умолчанию, чтобы получить значение для экспорта в виде библиотеки из src/index.js
. Это VALUE будет возвращено, когда пользователь использует синтаксис import VALUE from 'user-list'
.
Наконец, ключ library указывает имя переменной, которая будет доступна в браузере и будет указывать на это VALUE, что означает window.UserList === UserList === export default VALUE
.
Файл postcss.config.js
сообщает postcss-loader
, как обрабатывать код CSS.
module.exports = {
plugins: [
require('autoprefixer'),
require('cssnano'),
]
};
В Webpack плагины запускаются в обратном направлении. Сначала будет минимизирован CSS с помощью cssnano
, а затем будут добавлены префиксы с помощью autoprefixer
.
Файл .babelrc
сообщает babel-loader
, как обрабатывать файлы JavaScript (в зависимости от версии ES).
{
"presets": [
"env"
],
"plugins": [
"transform-object-rest-spread"
]
}
Посмотрим, как выглядят исходные файлы index.js
:
// import `.scss` files
import './scss/styles.scss';
// import UserList class
import { UserList as defaultExport } from './lib/user-list';
// export default UserList class
// I used `defaultExport` to state that variable name doesn't matter
export default defaultExport;
Синтаксис import file.scss
сбивает с толку многих людей. Мы пытаемся импортировать файл SASS в JavaScript, и это не совсем законно. Webpack использует sass-loader
в сочетании с другими загрузчиками, которые мы только что установили, чтобы все работало.
Затем мы импортировали класс UserList из src/lib/user-list.js
, который предоставил API нашего плагина. После этого мы просто экспортировали все как плагин. С этого момента наш плагин - не что иное, как класс UserList. Таким образом, всякий раз, когда пользователь использует import x from 'user-list'
, он получает класс UserList в x. Когда этот плагин используется в браузере, он получает UserList в контексте window (определен в webpack.config.js
).
Далее следует написать класс UserList и экспортировать его из файла user-list.js
:
// import dependencies
import { concat } from '../util/string';
// return UserList class
export class UserList{
constructor(elem, users){
this.elem = elem;
this.users = users;
this.initialized = false;
}
// initialize plugin
init() {
let ul = document.createElement( 'ul' );
ul.classList.add('users-list');
// store element reference
this.ul = this.elem.appendChild( ul );
// render initial list of users
this.renderList();
// set initialized to `true`
this.initialized = true;
}
// get fullname of the user
getUserFullName( user ) {
return concat( user.firstname, user.lastname );
}
// get list of users with fullname
getUsers() {
return this.users.map(
user => this.getUserFullName( user )
);
}
// return `li` element with user fullname
getUserLi( fullname ) {
let li = document.createElement( 'li' );
li.innerText = fullname;
return li;
}
// append `li` element to the users `ul` element
appendLi( li ) {
this.ul.appendChild( li );
}
// render entire users list
renderList() {
let users = this.getUsers();
let liElements = users.map(
fullname => this.getUserLi( fullname )
);
for( let li of liElements ){
this.appendLi( li );
}
}
// add new user
addUser( user ) {
let fullname = this.getUserFullName( user );
let li = this.getUserLi( fullname );
this.appendLi( li );
}
}
В представленном выше коде мы импортировали файл string.js
из папки util, которая экспортирует функцию concat.
// export `concat` function which joins strings by space
export function concat(...strings){
return strings.join(' ');
}
Наконец, пишем код SASS-файла styles.scss
, чтобы он помог визуально:
// plugin level css
.users-list{
width: 400px;
min-height: 200px;
border-radius: 3px;
background-color: #fff;
padding: 15px 30px;
list-style-type: none;
>li{
line-height: 30px;
}
}
Мы закончили работать с файлами. Следующим шагом будет использование Webpack для компиляции этого кода в код, понятный браузерам.
Внутри package.json
вам понадобятся следующие команды:
"scripts": {
"build": "webpack",
"start": "webpack-dev-server --open"
},
npm run build
вызывает Webpack для поиска webpack.config.js
и выполняет работу в соответствии с задачами, указанными в файле. npm run start
запустит сервер разработки Webpack, который также будет использовать webpack.config.js
и откроет файл index.html
для предварительного просмотра.
--open flag откроет вкладку браузера автоматически.
Давайте поговорим о файле index.html и о том, почему он там есть. Во время разработки плагина вы должны проверить, работает ли ваш плагин, и выяснить, есть ли баги. Этот файл этим и занимается. Вам просто нужно написать простой шаблон HTML и протестировать свой плагин.
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>User Application</title>
<style>
html,body{
margin: 0;
padding: 0;
font-size: 14px;
font-family: sans-serif;
color: #111;
background-color: #eee;
display: flex;
justify-content: center;
align-items: center;
}
</style>
</head>
<body>
<div id="app"></div>
<script>
document.addEventListener('DOMContentLoaded', function(){
var elem = document.getElementById('app');
let instance = new UserList(elem, [
{firstname: 'Ross', lastname: 'Geller'},
{firstname: 'Monica', lastname: 'Geller'},
{firstname: 'Chandler', lastname: 'Bing'},
{firstname: 'Joey', lastname: 'Tribbiani'},
]);
// initialize
instance.init();
// add new users
setTimeout(function(){
instance.addUser({firstname: 'Rachel', lastname: 'Green'});
}, 2000);
setTimeout(function(){
instance.addUser({firstname: 'Phoebe', lastname: 'Buffay'});
}, 4000);
});
</script>
</body>
</html>
Первый вопрос, который приходит вам в голову: где находится конструктор UserList? Не беспокойтесь, он содержится в dist/index.js
, потому что именно там Webpack разместил ваш плагин, а HTMLWebpackPlugin внедрил его в index.html
. Вы не сможете увидеть эти изменения в папке dist, пока не запустите npm run build
, потому что webpack-dev-server
работает в памяти.
Когда вы запускаете npm run build
, Webpack просматривает src/index.js
(потому что webpack.config.js
имеет этот файл в качестве точки входа), обрабатывает импорт различных файлов и пишет в них код в соответствии с этим файлом конфигурации. Наконец, когда все сделано, он выводит index.js в папку dist вместе с файлом index.html.
Когда вы будете удовлетворены плагином, вам нужно подготовить его к работе. Это означает, что вам нужно поместить окончательные выходные файлы в папку dist, потому что именно там находится основной файл package.json
.
Для этого у нас есть задача npm run build
. Она создает не только файл дистрибутива index.js, но и выходной файл index.html, который содержит index.js внутри тега script. Можно использовать live-server с командой live-server dist для запуска локального сервера, чтобы проверить, работает ли ваш плагин.
Теперь у вас есть возможность поделиться своим плагином на npm с помощью одной простой команды npm publish
. Я сделал небольшой репозиторий с примером выше на GitHub.