Всем привет, уважаемые хабражители.

Многие из вас так или иначе имели дело с Node.js. Наверное, не имеет смысла рассказывать о том, какие преимущества есть у JavaScript и у его серверной реализации в частности. В настоящий момент я много всего делаю на JS, начиная от простых консольных скриптов и заканчивая API, сервисами и сайтами. Современный стандарт EcmaScript принес значительные изменения в язык: он не только исправил некоторые древние «косяки» JS, но и добавил новые возможности, позволив, в частности, красиво избавиться от Callback Hell.

Когда передо мной в очередной раз встала задача развернуть простой сайт, состоящий из нескольких десятков страничек, мне захотелось сделать это при помощи какой-нибудь легковесной, но современной CMS, основанной на Node. Оценив обстановку, я понял, что ничего подходящего до сих пор нет. Мой старый и добрый Taracot оказался для этой задачи слишком тяжелым, к тому же, он не работает с современными версиями Node и перегружен функционалом.

Что я хотел получить в итоге?

  • Систему регистрации, авторизации, управления пользователями и группами, чтобы об этом не нужно было каждый раз думать
  • Удобный шаблонизатор с возможностью использования асинхронных функций
  • Модуль для быстрой отправки e-mail пользователям
  • Captcha, желательно без сторонних библиотек
  • Валидацию форм и полей
  • Быструю и удобную AJAX-driven таблицу для отображения данных
  • Код с использованием возможностей ES6
  • Многоязычность из коробки
  • Модульную структуру с возможностью быстро и комфортно написать новый модуль

Автоматическую систему обновлений
Простой модуль для создания и редактирования контента с удобной загрузкой изображений на сервер

Zoia.js

Работал я в свободное время и исключительно ради удовольствия, поэтому проект немного затянулся, но с первого коммита в конце мая до середины октября 2017 года получилось сделать многое по перечисленным выше пунктам:


image

Лицензия — MIT.

В качестве базового фреймворка используется Express.js, база данных — MongoDB, шаблонизатор — Nunjucks от Mozilla, а для UI используется UIkit.

Почему Web Framework, а не просто CMS? Прежде всего потому, что на базе системы можно сделать API, используя, например, только возможности, связанные с авторизацией. То есть из Zoia можно спокойно «выкинуть» модули, связанные с отображением контента для пользователя, и возвращать только JSON/XML.

Динамические таблицы и формы

Для динамического отображения данных в табличной форме я написал jQuery плагин zoiaTable. Он позволяет превратить любую HTML-таблицу в «динамическую», с возможностью фильтрации данных, разбивкой на страницы и сортировкой. Как это выглядит «вживую», можно посмотреть здесь.

Удобное построение форм — ещё одна задача, для которой я написал другой плагин: zoiaFormBuilder. У него две основные задачи: динамическое постороение форм с возможностью сериализации/десериализации данных и валидация данных по заданным правилам.

Оба плагина доступны по лицензии MIT, и их можно использовать отдельно от Zoia, хотя в данный момент они мне нужны только там. Отображение не привязано к конкретному фреймворку (можно в параметрах вызова задавать нужный HTML и стили), по умолчанию используется UIkit.

Что ещё реализовано на данный момент?

Прежде всего, доступная система управления пользователями и группами. В перспективе это позволит сделать разграничение прав для модулей и их отдельных компонентов (например, определенная группа может редактировать странички только в определенной папке). На данный момент существует одна системная группа — admin, которая позволяет пользователям заходить в backend.

Система регистрации пользователей сделана достаточно стандартно — с валидацией по e-mail. Существует возможность восстановить забытый пароль (также через e-mail). В перспективе здесь нужно будет прикрутить авторизацию через различные социальные сервисы (по Oauth), двухфакторную авторизацию (например, через Google Authenticator или SMS), а также сделать простой личный кабинет. Что из этого необходимо в системе, позиционирующей себя как «лековесная» — отдельный вопрос.

Самописная Captcha не использует сторонних библиотек вроде GraphicMagick, вместо используется JIMP — бибилотека, не использующая внешних зависимостей.

Редактор контента (Pages) использует CKEditor как WISIWYG редактор. Есть возможность вставлять в страницы «хлебные крошки» (breadcrumbs). Также написан простой бразуер с возможностью загрузки файлов и автоматическим созданием thumbnail'ов:



Он умеет создавать папки, копировать-перемещать файлы, в общем, имеет в наличии весь базовый функционал.

Модуль навигации (Navigation) позволяет создавать многоуровневые навигационные меню для сайта. Работа с навигацией осуществляется в виде дерева, в котором можно создавать, редактировать и перетаскивать элементы.

Система автоматических обновлений позволяет загружать новую версию системы и смотреть список изменений.

Как установить Zoia на своём сервере?

Потребуется установленный Node 7+ и MongoDB. Разработку я веду под Windows, поэтому там это тоже работает, но в продакшене, конечно, лучше использовать Linux-based системы. Если у Вас Debian-совместимая система, то есть простой способ установить всё одной командой:

wget -q https://xtremespb.github.io/zoia/zoia_install && sudo bash zoia_install

Также можно установить всё через Docker:

docker pull mongo:latest
docker pull xtremespb/zoia:latest
docker run -d --name mongo mongo
docker run -p 3000:3000 -d --name zoia --link=mongo:mongo xtremespb/zoia
docker exec -it zoia node /usr/local/zoia/bin/install.js


Подробнее об установке можно почитать в документации.

Что будет реализовано

В настоящий момент фреймворк находится в состоянии beta (думаю, в таком состоянии он будет ещё достаточно долго). Как я уже написал выше, хочется доработать до определенной стадии модуль авторизации (Auth), чтобы сделать двухфакторную авторизацию и Oauth — но это ещё не точно.

Также, отдельно от базовой системы, я хочу сделать модуль для ведения блога (личного или коллективного), а также простой модуль интернет-магазина.

Документация — над ней я ещё работаю. Вообще, я работаю над всем, включая архитектуру, дизайн, разработку и тестирование, а времени очень мало. Именно поэтому перевода на русский язык (и другие языки) пока нет, но я буду рад, если кто-то сможет мне с этим помочь. Также буду рад всем разработчикам, кто захочет совместно работать над проектом — код открыт.

Давно ничего не публиковал на Хабре.
Если что-то сделал не так, пожалуйста, пишите в личку.

Комментарии (37)


  1. asdf404
    12.10.2017 13:53
    +2

    Есть пара вопросов:


    1. где можно посмотреть Dockerfile?
    2. зачем вы делаете вот так require(path.join(__dirname, '..', 'etc', 'config.js')), когда можно писать просто require('../etc/config.js')?
    3. почему не используете стрелочные функции, а вместо этого let that = this, как, например, тут?
    4. почему версия хранится в файле version.js, а не в package.json?
    5. если это, конфиг приложения, то почему он находится в системе контроля версий?
    6. почему Express, а не стильный-модный-молодёжный Koa? (хотя это дело вкуса, признаю)

    И ради всего святого, не делайте так никогда (тем более с sudo):


    wget -q https://xtremespb.github.io/zoia/zoia_install && sudo bash zoia_install


    1. xtremespb Автор
      12.10.2017 14:06

      1. Dockerfile здесь: github.com/xtremespb/xtremespb.github.io/blob/master/zoia/Dockerfile
      2. Я использую path для нормализации пути с учетом различных нотаций в различных ОС, насколько я понимаю, это best practice
      3. Стрелочные функции используются не везде, поскольку местами есть копипаст с моего старого ES5 кода. Я стараюсь использовать их везде, где это возможно.
      4. С package.json хорошая идея, брать версию оттуда. Сделал отдельный файл, т.к. не хотел при обновлении трогать package.json, но, похоже, это всё-таки хорошая идея. Спасибо!
      5. Это дефотлный конфиг. Он находится в системе контроля версий, т.к. с ним можно сразу стартануть приложение, и в бета-версии его структура ещё может меняться.
      6. Я привык работать с Express и неплохо его знаю. После одного маленького напильника он позволяет использовать асинхронные функции в routes, поэтому не вижу причин, почему не использовать его дальше ;-)

      По поводу кода для установки: почему нет? Это стандартная практика, например, вот здесь такой мануал по установке официального Node:

      curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
      sudo apt-get install -y nodejs


      1. asdf404
        12.10.2017 14:50

        Спасибо за быстрый ответ.


        1. тут будет несколько подпунктов:
          1. есть готовый официальный образ, основанный как раз на Debian, можно использовать его как основу и не придётся возиться с установкой ноды самому;
          2. не стоит внутри докера делать apt-get upgrade, лучше использовать новую версию родительского образа (в данном случае Debian);
          3. каждый вызов RUN кешируется и сохраняется как отдельный слой, обычно этого стараются избегать;
          4. на мой взгляд не стоит использовать forever внутри докер-контейнера (разве что при разработке, если нужен автоматический перезапуск сервера при изменении файлов), лучше при старте указать --restart always, тогда упавший процесс будет перезапускаться самим докером (см. docker run --help);
        2. под рукой не windows-машины, но я уверен, что там это будет работать нормально.
        3. нашел, где есть стрелочные функции, но там те же let that = this. Стрелочные функции не имеют своего контекста, они наследуют родительскийй, поэтому в этом трюке нет смысла.
        4. это очень удобно на самом деле, вы можете использовать npm version ..., который сам изменит версию в package.json и поставит тег в git; важно, чтобы все текущие изменения были закоммичены, иначе он упадёт с ошибкой. Рекомендую в вопросе версионирования следовать semver.
        5. всё же рекомендуется в гите хранить только пример конфига, а реальные создавать локально и не версионировать, потому что если я захочу изменить конфиг для себя, то я могу столкнуться с конфликтами при pull'е новой версии кода; к тому же, ваш конфиг содержит некоторые секреты.
        6. на вкус и цвет… :)
        7. только заметил, а почему вот здесь вы не используете, например, mongoose?


        1. asdf404
          12.10.2017 14:57

          8. ну и по поводу установки через curl ... | sudo bash ... — это ужасная практика, которая учит пользователей запускать непроверенные скрипты, полученные через интернет (не говоря уже про sudo). Не каждый полезет внутрь читать и разбираться в bash'евских закорючках, чтобы понять что же делает этот скрипт.


          1. xtremespb Автор
            12.10.2017 15:16
            +1

            А как бы Вы рекомендовали делать инсталляцию?


            1. asdf404
              12.10.2017 16:21

              Общие принципы изложил в другом комментарии. Здесь напишу подробнее, хотя сдаётся мне, что лучше оформить это уже отдельной статьёй, возможно позже.


              Итак, как я делаю это у себя:


              Есть 2 Dockerfile (в др. комментарии), в production версию копируется весь код приложения, и для него устанавливаются зависимости (npm install и т.п.), полученный образ тегается и пушится в реестр, он готов к использованию. Стоит учесть, что вся конфигурация задаётся через переменные окружения, согласно 12factor, это позволяет не влазить каждый раз в код для изменения настроек, а просто перезапустить контейнер с новым окружением.


              Для разработки используется другой Dockerfile (также в др. комментарии). В нём устанавливаюстя лишь те компоненты, которые я не могу пробросить извне (imagemagick в моём случае), остальное: код и node_modules, через volume монтируется внутрь контейнера и оттуда запускается. При этом (у меня Linux, не знаю как будет на Windows), благодаря nodemon, при изменении файлов автоматически перезапускается приложение.


              Вот как-то так. Возможно, не совсем понятно объяснил, не стесняйтесь спрашивать и уточнять.


              1. xtremespb Автор
                12.10.2017 16:29

                Спасибо, про Docker идея ясна.
                А как быть, если Docker не используется? Мой скрипт загружает необходимые зависимости и производит установку последнего релиза с Github. В принципе, sudo здесь необходим только для установки через apt-get, всё остальное загружается в локальную директорию.


                1. asdf404
                  12.10.2017 16:41

                  Для apt-get лучше указать зависимости в README.md, и дать пользователю самому это сделать. Да, это несколько сложнее для пользователя (целое лишнее действие), но зато более понятно, что именно произойдёт в итоге.


                  По поводу если Docker не используется, то можно оформить вашу CMS в виде NPM пакета, который можно глобально (npm install -g ...) установить. Либо можно подсмотреть как делают другие, например упомянутый ниже KeystoneJS.


                  Вообще, глядя на всякие CMS на NodeJS, видно, что они используют подход с генератором шаблона, для которого сама CMS устанавливается как зависимость после.


                  1. xtremespb Автор
                    12.10.2017 17:14

                    Да, спасибо, нужно подумать в этом направлении.
                    Идею со скриптом я, конечно, «украл» у установщика Node.js. Но Ваши аргументы выглядят более разумно.


                1. justboris
                  12.10.2017 17:08

                  А если у меня не apt-get а, например, yum?


                  Будет удобнее написать, какие зависимости вам нужны, а пользователи пусть сами ставят.


                  1. xtremespb Автор
                    12.10.2017 17:14

                    Поэтому я написал Debian-based systems. Для разных дистрибутивов не только менеджеры пакетов разные, но и сами названия пакетов тоже?


                    1. justboris
                      12.10.2017 17:18

                      То есть если у меня не Debian-based, то я в пролете? Печально :(


                      Почитал скрипт установки, а там из debian-специфичного только apt-get. Убрать его, и можно будет заменить Debian-based на "Linux и Mac".


                      1. xtremespb Автор
                        12.10.2017 17:37

                        Конечно не в пролете ;-) Просто зависимости придётся ставить руками.
                        Я постараюсь сделать инструкцию под разные дистрибутивы.


        1. xtremespb Автор
          12.10.2017 15:16

          Спасибо за дельные комментарии!

          1. С Docker'ом я имею дело ровно два дня, так что всё, что Вы написали — очень ценная для меня информация. 1) по поводу образа Node учту, спасибо 2) Я использовал :latest версию, и всё равно там была не обновлена часть пакетов, отсюда и apt-get upgrade 3) есть другие альтернативы, когда нужно выполнить команду в контейнере? 4) будет работать как monit, т.е. перезапускать при недоступности порта (или процесса)?
          2. Да, под Windows вряд ли запуститься, надо будет попробовать.
          3. Стрелочные функции, например, вот здесь. Но я в любом случае буду делать рефакторинг кода с that = this.
          4. Отдельное спасибо за npm version.
          5. Да, с конфигом Вы правы. Учту это.
          7. Mongoose не использую, т.к. API, предоставляемое драйвером MongoDB-Node, нравится мне больше.


          1. MikailBag
            12.10.2017 16:10
            +2

            1. Юниксовые слеши отлично работают под виндой, в т.ч. в require() и import.
              Из мест, где не работает — указание пути к бинарнику в cmd.exe (в PowerShell работает)


          1. asdf404
            12.10.2017 16:11

            1. рад, что оказался полезным;
              2. вы не должны обновлять их сами, в документации написано:


              You should avoid RUN apt-get upgrade or dist-upgrade, as many of the “essential” packages from the parent images won’t upgrade inside an unprivileged container. If a package contained in the parent image is out-of-date, you should contact its maintainers.

              3. если я правильно понял вопрос: когда вы собираете докер-образ, то нет другого способа выполнять сборочные команды, кроме как через RUN. Но рекомендуется объединять вызов множества команд в один RUN, т.е.:


              RUN apt update && apt install ... && apt ...

              вместо


              RUN apt update
              RUN apt install
              RUN apt ...

              Docker sees the initial and modified instructions as identical and reuses the cache from previous steps. As a result the apt-get update is NOT executed because the build uses the cached version. Because the apt-get update is not run, your build can potentially get an outdated version of the curl and nginx packages.

              А вот уже после того как контейнер запущен, вы можете выполнить в нём команду с помощью exec. Но нужно учитывать, что любые изменения в работающем контейнере потеряются, после его удаления (или остановки, если запускали с флагом --rm).


              4. если приложение падает внутри контейнера, то контейнер останавливается вместе с ним с кодом ошибки, докер это видит и перезапускает (если указано --restart always). При разработке вам всё ещё может быть удобно использовать forever или nodemon, для перезапуска процесса при изменении исходных файлов, например. Для такого случая я у себя использую 2 Dockerfile: prod и dev, для продакшена и разработки, соответственно:



            Dockerfile
            # production версия
            FROM node:8.4-alpine
            
            WORKDIR /service
            # устанавливаются все необходимые пакеты
            RUN apk update && apk add imagemagick
            # копируется код
            COPY . /service
            ENV NODE_ENV production
            # устанавливаются зависимости самого приложения, только после того, как удостоверимся, что код приложения находится внутри образа
            RUN npm install --production
            
            EXPOSE 8080
            CMD ["node", "--harmony", "./index.js"]


            1. xtremespb Автор
              12.10.2017 16:32

              Спасибо, вопросов по Docker'у пока больше нет.
              Займусь более детальным изучением и пересоберу образ в соответствии с Вашими рекомендациями.


              1. antirek
                13.10.2017 05:50

                плюсую за использование docker-compose — быстро развернуть и попробовать


          1. Akuma
            12.10.2017 16:57
            +1

            Позвольте дополнить :)

            1. Указывайте версию в FROM. Думаю не нужно объяснять зачем.
            2. Пути через / под Windows прекрасно работают. Вообще ни разу не встречал ОС, где они бы не работали.


            1. xtremespb Автор
              13.10.2017 09:13

              Спасибо! Мне показалось, что решение с path более универсально, но возможно, Вы и правы, поскольку фреймворк в любом случае ориентируется на *NIX системы.


        1. serf
          12.10.2017 17:14

          нашел, где есть стрелочные функции, но там те же let that = this. Стрелочные функции не имеют своего контекста, они наследуют родительскийй, поэтому в этом трюке нет смысла.


          кстати в промис заворочивать коллбэк вызовы вручную не обязательно, в 8й ноде есть util.promisify (а ранее то же самое делалось с Bluebird)


  1. MooooM
    12.10.2017 13:56

    А можно подробнее чем не устроил, например, KeystoneJS?


    1. xtremespb Автор
      12.10.2017 14:10

      Лично мне он не понравился некоторой нелогичностью интерфейса админки и отсутствием «из коробки» некоторых вещей, которые я перечислил в списке «Что я хотел получить в итоге?». Ну и «леговесным» при всём желании Keystone назвать нельзя, ИМХО.


  1. cane
    12.10.2017 14:07

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


    1. xtremespb Автор
      12.10.2017 14:11

      Идея как раз в том, что для каждого языка можно делать абсолютно разное дерево навигации. Возможно, имеет смысл сделать функцию «скопировать структуру из языка n»?


      1. cane
        12.10.2017 14:13

        А для чего разные деревья для разных языков? Переключения между языками так и не нашел.


        1. xtremespb Автор
          12.10.2017 14:18

          Допустим, версия для одного языка готова, а для другого есть только пара переведенных страниц. Нужно будет либо показывать пользователю каждый раз что-то вроде «Страница ещё не переведена на Ваш язык», либо просто показывать ему другое меню навигации.
          Переключение между языками сейчас работает через куки и через поддомены. Т.е. можно открыть что-то вроде ru.example.com и попасть на русскоязычную версию. Ещё я хочу сделать постоянное переключение на другой язык через GET, как-то так: example.com?lang=de


          1. cane
            12.10.2017 14:28

            Идея понятна. Но с пользовательской точки зрения, в меню навигация я предпочел бы видеть оба дерева. Так более наглядно. А то страница навигации не несет должной нагрузки.


            1. xtremespb Автор
              12.10.2017 14:29

              ОК, спасибо за идею!


  1. ingumsky
    12.10.2017 16:18
    +1

    Любопытный проект. Как раз что-то подобное ищу, поэтому будет интересно попробовать.
    Если вам нужна помощь в переводе, я могу поучаствовать (я как раз переводчик и редактор).


    1. xtremespb Автор
      12.10.2017 17:12
      +1

      Спасибо, буду очень рад! В репозитории на GitHub есть директории lang, в которых содержатся языковые файлы в формате JSON. Можно их переводить и делать Pull Request'ы.


      1. ingumsky
        12.10.2017 21:12

        Отлично, сделал первый PR :)


  1. Reon
    12.10.2017 23:48

    Вы пишете, что сделали упор на легковесность и минимум зависимостей, а в итоге подтянули
    почти весь набор зависимостей, который используют современные проекты.
    Ожидал что-то простое, получил монстра на 746 пакетов.


    1. xtremespb Автор
      13.10.2017 09:11

      Говоря об отсутствии зависимостей, я подразумеваю, прежде всего, системные пакеты (вроде GraphicsMagick). Всего система использует 24 библиотеки из NPM, среди которых express и mongodb, они, разумеется, тоже подтягивают свои зависимости. Если говорить о легковесности, то речь идёт прежде всего об общей простоте архитектуры всей системы.


  1. yarommax
    13.10.2017 09:07

    Выглядит привлекательно


  1. sutarmin
    13.10.2017 12:30

    Самый главный вопрос про любой фреймворк: А почему Зоя? :) Просто броское название или есть вложенный смысл?

    Вообще начинание хорошее. Несколько месяцев назад потребовался такого рода фреймворк. Удивился тому, что единственный живой — KeystoneJS. Учитывая, как стремительно сейчас развивается Node.js-сообщество, такое наблюдать было странно. Так что начинание отличное, не останавливайтесь! Сам постараюсь присоединиться по мере возникновения свободного времени.


    1. xtremespb Автор
      13.10.2017 13:42

      Люблю называть проекты женскими именами. У меня нет ни одной знакомой Зои, а звучит красиво :-)