Meteor: Как собрать приложение из пакетов

Дмитрий Манжасов, Мера

Meteor: Как собрать приложение из пакетов

Дмитрий Манжасов, Мера

https://habrahabr.ru/company/yandex/blog/265569/

Чтобы собрать приложение из отдельных пакетов (packages), надо сначала разбить его на отдельные пакеты.

Но зачем?

Если можно так

        meteor create simple-todos
      

И весь код разместить в 3-х файлах

        simple-todos.js   # JavaScript loaded on both client & server
        simple-todos.html # HTML file that defines view templates
        simple-todos.css  # CSS file to define your app's styles
      

https://www.meteor.com/tutorials/blaze/creating-an-app

Или разбить по каталогам так

        client/           # everything for client-side only
          compatibility/  # dinosaurs
          css/            # some styles
          lib/            # client libraries
          view/           # template HTML & JavaScript
            layout/       # page layouts
            template/     # page templates
              page1/      # page1 HTML template and JavaScript
        public/           # static resources:
          assets/          # local fonts and images
      

.. и так

        common/          # for the both client and server
          controller/    # Iron Router controllers
          lib/           # common libraries
          collections.js # some collections
          routes.js      # Iron Router routes
        server/          # server-side code
          methods.js     # remote functions
          publish.js     # collection publishing rules
        private/         # top secret files
      

Зачем разбивать приложение на пакеты

Плюсы

  1. Пакет можно использовать в нескольких приложениях
  2. Можно забыть магию имён client, server, public, private и т.д.
  3. Лучшее управление зависимостями
  4. Изоляция кода и явный экспорт переменных
  5. Легко запускать юнит-тесты

Минусы

  1. Надо создавать дескриптор пакета package.js
  2. Можно забыть прописать новый файл в package.js и он не будет загружен

Пакетная система Meteor

  1. Изоморфная
  2. Система сборки в комплекте
  3. Система сборки расширяется плагинами
  4. Платформа Meteor это тоже набор пакетов

Создание нового пакета

meteor create --package username:packagename
        packagename/
          package.js
          packagename.js
          packagename-tests.js
          README.md
      

package.js: Package.describe

        // All properties are optional
        Package.describe({
          // Default is package directory name.
          name: "bundle:namespace",
          version: "0.0.1",
          summary: "What this does",
          // Github URL to your source repository.
          git: "https://github.com/something/something.git",
          // Package documentation
          documentation: 'README.md'
        });
      

package.js: Package.onUse

        // This defines your actual package
        Package.onUse(function(api) {
          // Use core Meteor packages from the release specified
          api.versionsFrom('1.2.1');
          // Dependencies
          api.use('ecmascript');
          api.use(['bundle:namespace', 'bundle:imports']);
          api.use('tap:i18n@1.3.0');
          api.use('underscore', 'client');
          // to be continued
      

package.js: Package.onUse

          // Imply = use for including package or app
          api.imply('iron:router@1.0.12');
          // Package files
          api.addFiles('route.js');
          api.addFiles(['fruits.html', 'fruits.js'], 'client');
          // Package assets
          api.addAssets(['fonts/bundle.eot', ...
            'img/default-avatar.jpg'], 'client');
          // Export
          api.export('Bundle');
        });
      

Imply

use imply use Layer 1 Пакет A Пакет B Пакет C

Assets

/code/bundle/layout/icons.css

        src:url("fonts/bundle.eot");
        src:url("fonts/bundle.eot?#iefix") format("embedded-opentype"),
          url("fonts/bundle.woff") format("woff"),
          url("fonts/bundle.ttf") format("truetype"),
          url("fonts/bundle.svg#bundle") format("svg");
      

/code/bundle/layout/layout.html

        <img src="/packages/bundle_layout/img/default-avatar.jpg">
      

package.js: документация

http://docs.meteor.com/#/full/packagejs

Публикация пакета

https://atmospherejs.com/

        meteor publish
      

Ограничения

  1. Версия обязательна
  2. username должен совпадать с аккаунтом разработчика Meteor

Использование пакетов в приложении

        meteor add <package> [package..]
      

Где Meteor ищет пакеты:

  1. Каталог packages/ приложения
  2. Переменная среды PACKAGE_DIRS
  3. Пакеты из Meteor release
  4. https://atmospherejs.com/

PACKAGE_DIRS

Linux

        PACKAGE_DIRS=../bundle:../i18n
      

Windows

        set PACKAGE_DIRS=../bundle:../i18n
      

Приложение состоящее из одних пакетов

Создание

        meteor create myapp
        cd myapp
        del myapp.*
        meteor remove autopublish
        mkdir packages
        cd packages
        meteor create --package myapp
        cd ..
        meteor add myapp
      

Пакет приложения

        Package.onUse(function(api) {
          api.versionsFrom('1.2.1');
          api.use('coffeescript');
          api.use(['bundle:imports',
            'bundle:namespace',
            'bundle:layout',
            'bundle:fruits',
            'bundle:sweets']);
          api.addFiles('myapp.coffee');
        });
      

Пакет приложения

/code/myapp/packages/myapp/myapp.coffee

        # Default route
        Router.route '/', -> Router.go 'fruits'
        if Meteor.isClient
          # Navigation menu
          Bundle.Navigation.set [
            {template: 'NavFruits'}
            {template: 'NavSweets'}
          ]
      

Пакет пакетов

        Package.onUse(function(api) {
          api.versionsFrom('1.2.1');
          api.imply([
            'iron:router@1.0.12',
            'zimme:active-route@2.0.1'
          ]);
        });
      

Пакет пакетов

https://github.com/meteor/meteor/blob/devel/packages/meteor-base/package.js

Пакет namespace

        Package.onUse(function(api) {
          api.versionsFrom('1.2.1');
          api.addFiles('namespace.js');
          api.export('Bundle');
        });
      

/code/bundle/namespace/namespace.js

        Bundle = {};
      

Пакет namespace

/code/bundle/layout/

        api.use('bundle:namespace'); // package.js
      
        Bundle.Navigation = new ReactiveVar([]); // layout.js
      

/code/myapp/packages/myapp/

        api.use(['bundle:namespace', 'bundle:layout']); // package.js
      
        Bundle.Navigation.set [template: 'NavFruits'] # myapp.coffee
      

Пакет collections

        Package.onUse(function(api) {
          api.versionsFrom('1.2.1');
          api.use(['ecmascript', 'bundle:namespace']);
          api.addFiles('collections.js');
          api.addFiles('publish.js', 'server');
        });
      

Пакет collections

/code/bundle/collections/collections.js

        Bundle.Collection = {
          Fruits: new Meteor.Collection('fruits'),
          Sweets: new Meteor.Collection('sweets')
        };
        for(let name in Bundle.Collection) {
          Bundle.Collection[name].allow({
            // set insert, update and remove rules
          });
        }
      

Пакет collections

/code/bundle/collections/publish.js

        Meteor.publish('fruits', function() {
          // TODO apply filter and check user access rights
          return Bundle.Collection.Fruits.find();
        });
        Meteor.publish('sweets', function() {
          // TODO apply filter and check user access rights
          return Bundle.Collection.Sweets.find();
        });
      

Пакет layout

        Package.onUse(function(api) {
          api.versionsFrom('1.2.1');
          api.use(['ecmascript', 'templating', 'reactive-var']);
          api.use(['bundle:namespace', 'bundle:imports']);
          api.addFiles('route-controller.js');
          api.addFiles(['icons.css', 'layout.css', 
            'layout.html', 'layout.js'], 'client');
          api.addAssets(['fonts/bundle.eot', 'fonts/bundle.svg',
            'fonts/bundle.ttf', 'fonts/bundle.woff', 
            'img/default-avatar.jpg'], 'client');
        });
      

Пакет layout

/code/bundle/layout/layout.html

        <template name="Layout">
          <div>
            <aside>{{> yield region="navigation"}}</aside>
            <div>
              <header>{{> yield region="header"}}</header>
              <main>{{> yield}}</main>
            </div>
          </div>
          <footer></footer>
        </template>
      

Пакет layout

/code/bundle/layout/route-controller.js

        Bundle.RouteController = RouteController.extend({
          layoutTemplate: 'Layout',
          title: '', // default title
          context: undefined, // default context
          action: function() {
             // implementation
          }
        });
      

Пакет layout

/code/bundle/layout/route-controller.js

          this.render('Navigation', { to: 'navigation',
            data: function() { return Bundle.Navigation.get(); }
          });
          this.render('Header', { to: 'header',
            data: this.title
          });
          this.render(this.lookupTemplate(), {
            data: this.context
          });
      

Пакет page

        Package.onUse(function(api) {
          api.versionsFrom('1.2.1');
          api.use(['ecmascript', 'templating']);
          api.use(['bundle:imports', 'bundle:namespace',
            'bundle:collections']);
          api.addFiles('route.js');
          api.addFiles(['fruits.html', 'fruits.js'], 'client');
        });
      

Пакет page

/code/bundle/fruits/route.js

        Router.route('/fruits', {
          controller: Bundle.RouteController.extend({
            title: 'Свежие Фрукты',
            subscriptions: function() {
              return Meteor.subscribe('fruits');
            },
            context: function() {
              return Bundle.Collection.Fruits.find();
            }})
        });
      

Запуск

        set PACKAGE_DIRS=../bundle
        meteor
      

Сборка бандла

        set PACKAGE_DIRS=../bundle
        meteor build ..\ --architecture os.linux.x86_64
      

package.js: Package.onTest

        /* This defines the tests for the package */
        Package.onTest(function(api) {
          api.use('ecmascript');
          // Allows you to use the 'tinytest' framework
          api.use('tinytest');
          // Sets up a dependency on this package
          api.use('username:packagename');
          // Specify the source code for the package tests
          api.addFiles('packagename-tests.js');
        });
      

https://github.com/meteor/meteor/tree/devel/packages/tinytest

Tinytest.add

/code/bundle/namespace/namespace-tests.js

        Tinytest.add('namespace', function (test) {
          test.instanceOf(Bundle, Object);
        });
      

Tinytest.addAsync

/code/bundle/collections/collections-tests.js

        Tinytest.addAsync('collections - fruits', (test, next) => {
          Meteor.subscribe('fruits', {
            onError: (e) => {
              test.fail(e);
              next();
            }, onReady: () => {
              test.ok();
              next();
            }});
        });
      

Запуск юнит тестов

        meteor test-packages
      

Вопросы?

available online

Fork me on GitHub