Zoia.js: ещё один веб-фреймворк на Node

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

    Многие из вас так или иначе имели дело с 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 системы. Для установки Zoia необходимо установить в системе пакеты nginx и mongo, после чего выполнить следующие команды:

    mkdir /var/www/zoia && chdir /var/www/zoia
    wget -q -O - https://xtremespb.github.io/zoia/zoia_download | bash
    npm run config && npm run install && npm run webserver


    Также можно установить всё через 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 — но это ещё не точно.

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

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

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


    UPD: всем большое спасибо за комментарии! С момента публикации статьи я получил большое число дельных комментариев, и в прошедшие выходные реализовал следующие изменения: 1) конфигурационные файлы системы теперь в формате JSON и исключены из системы версионирования; 2) версия системы хранится непосредственно в package.json; 3) обновленный образ Docker базируется на node:slim и использует только одну команду RUN; 4) обновленный инсталлятор не требует наличия root и не ставит за пользователя системные пакеты.
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 39
    • +3

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


      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
        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
        • 0

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


          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

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

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

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


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


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


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


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

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

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


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


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

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

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


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

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

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


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

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

                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, нравится мне больше.
                • +2
                  1. Юниксовые слеши отлично работают под виндой, в т.ч. в require() и import.
                    Из мест, где не работает — указание пути к бинарнику в cmd.exe (в PowerShell работает)
                  • 0
                    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"]

                    и для разработки:


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

                    Соответственно, при разработке, и все остальные части приложения (БД, например) тоже крутятся внутри докера. Я использую Docker Compose, который позволяет декларативно описать все опции и зависимости контейнеров:


                    docker-compose.yml
                    version: '3'
                    
                    services:
                      mongodb:
                        image: mongo:3.4
                        volumes:
                          - mongodb-data:/data/db
                      backend:
                        image: registry.gitlab.com/my-team/my-repo:latest # образ, который хотим использовать
                        build: # если указана опция build, то image не будет скачиваться, но соберётся локально с этим именем и тегом
                          context: . # директория, в которой лежит Dockerfile
                          dockerfile: Dockerfile.dev # указываем какой Dockerfile хотим использовать
                        restart: always
                        volumes: # указываем
                          - .:/service # монтируем приложение на время разработки
                          - static-data:/static
                        depends_on: # опция, которая позволяет указать зависимости этого сервиса
                          - mongodb
                    
                    volumes:
                      static-data:
                      mongodb-data:

                    В реальности у меня всё несколько сложнее, но как пример приложения с одной БД, он вполне подходит.


                    7. Mongoose (да и любые другие ORM/ODM) значительно более удобен, по сравнению с прямым обращением к БД. Он позволяет хранить структуру, правила валидации и различне методы моделей в одном месте и не дублировать логику (ту же валидацию) по всему приложению. Думаю, со временем вы к этому сами придёте.

                    • 0
                      Спасибо, вопросов по Docker'у пока больше нет.
                      Займусь более детальным изучением и пересоберу образ в соответствии с Вашими рекомендациями.
                      • 0
                        плюсую за использование docker-compose — быстро развернуть и попробовать
                    • +1
                      Позвольте дополнить :)

                      1. Указывайте версию в FROM. Думаю не нужно объяснять зачем.
                      2. Пути через / под Windows прекрасно работают. Вообще ни разу не встречал ОС, где они бы не работали.
                      • –1
                        Спасибо! Мне показалось, что решение с path более универсально, но возможно, Вы и правы, поскольку фреймворк в любом случае ориентируется на *NIX системы.
                    • +1
                      нашел, где есть стрелочные функции, но там те же let that = this. Стрелочные функции не имеют своего контекста, они наследуют родительскийй, поэтому в этом трюке нет смысла.


                      кстати в промис заворочивать коллбэк вызовы вручную не обязательно, в 8й ноде есть util.promisify (а ранее то же самое делалось с Bluebird)
                • 0
                  А можно подробнее чем не устроил, например, KeystoneJS?
                  • 0
                    Лично мне он не понравился некоторой нелогичностью интерфейса админки и отсутствием «из коробки» некоторых вещей, которые я перечислил в списке «Что я хотел получить в итоге?». Ну и «леговесным» при всём желании Keystone назвать нельзя, ИМХО.
                  • 0
                    В меню навигация реализован функционал интернализации. Для одних и тех же записей на разных языках можно указать разные страницы. При большом количестве страниц, такие ошибки возможны. Лучше реализовать навигацию в виде дерева, при клике на элемент которого открывать форму с полями для доступных языков.
                    • 0
                      Идея как раз в том, что для каждого языка можно делать абсолютно разное дерево навигации. Возможно, имеет смысл сделать функцию «скопировать структуру из языка n»?
                      • 0
                        А для чего разные деревья для разных языков? Переключения между языками так и не нашел.
                        • 0
                          Допустим, версия для одного языка готова, а для другого есть только пара переведенных страниц. Нужно будет либо показывать пользователю каждый раз что-то вроде «Страница ещё не переведена на Ваш язык», либо просто показывать ему другое меню навигации.
                          Переключение между языками сейчас работает через куки и через поддомены. Т.е. можно открыть что-то вроде ru.example.com и попасть на русскоязычную версию. Ещё я хочу сделать постоянное переключение на другой язык через GET, как-то так: example.com?lang=de
                          • 0
                            Идея понятна. Но с пользовательской точки зрения, в меню навигация я предпочел бы видеть оба дерева. Так более наглядно. А то страница навигации не несет должной нагрузки.
                  • +1
                    Любопытный проект. Как раз что-то подобное ищу, поэтому будет интересно попробовать.
                    Если вам нужна помощь в переводе, я могу поучаствовать (я как раз переводчик и редактор).
                    • 0
                      Спасибо, буду очень рад! В репозитории на GitHub есть директории lang, в которых содержатся языковые файлы в формате JSON. Можно их переводить и делать Pull Request'ы.
                      • +1
                        Отлично, сделал первый PR :)
                    • +1
                      Вы пишете, что сделали упор на легковесность и минимум зависимостей, а в итоге подтянули
                      почти весь набор зависимостей, который используют современные проекты.
                      Ожидал что-то простое, получил монстра на 746 пакетов.
                      • –1
                        Говоря об отсутствии зависимостей, я подразумеваю, прежде всего, системные пакеты (вроде GraphicsMagick). Всего система использует 24 библиотеки из NPM, среди которых express и mongodb, они, разумеется, тоже подтягивают свои зависимости. Если говорить о легковесности, то речь идёт прежде всего об общей простоте архитектуры всей системы.
                      • 0
                        Выглядит привлекательно
                        • 0
                          Самый главный вопрос про любой фреймворк: А почему Зоя? :) Просто броское название или есть вложенный смысл?

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

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.