📜 ⬆️ ⬇️

Well.js is another approach to modular JavaScript development.

According to the title of the publication, some might think: “What again ?! Another bike? ”I hasten to please - no. Well.js ( Github ) is a wrapper for existing AMD solutions (default for Require.js), the main idea of ​​which is to make working with modules and their dependencies more attractive to the author.

For example, take the module Require.js:

define(['views/common/basic-page', 'views/partials/sidebar', 'utils/helper', 'models/user' ], function (BasicView,SidebarView, Helper, UserModel) { //  }); 

And replace it with a flick of the wrist:
')
 wellDefine('Views:Pages:Overview', function(app, modules) { this.use('Views:Common:BasicPage') .use('Views:Partials:Sidebar') .use('Utils:Helper', {as: 'MyHelper', autoInit: false}) .use('Models:User', {as: 'UserModel'}) .exports(function(options){ /*       : this.BasicPage this.Sidebar this.MyHelper this.UserModel */ }); }); 


To whom it is interesting, for what all this is necessary, I ask under cat.

Introduction


I work in a company that has quite a lot of different projects and often new ideas emerge that generate new projects. Some projects have duplicate components, or they use the same set of libraries, which are logical not to copy-paste, but to bring to the repository accessible to all projects and request from there as necessary.

I did not want to use Require.js in its pure form for these purposes for two reasons: one is aesthetic - I really do not like the way the declaration of the paths and the enumeration of all the function arguments look like. The second reason is technological - I could not quickly figure out how, when building a project, quickly minify and glue the files in the sequence I need. These two reasons pushed me to write a wrapper that allowed me to solve at least the second question.

Work on the project is conducted in the time free from the main work, so I decided to share it with the public. I also want to note that this article is not a textbook, but rather familiarity with Well.js and code examples that I will give fictional, but I will try to convey my thought through them.

Ideology


The ideology of Well.js is that different developers can write independent application components, use them both within the framework of one project, and share them through package managers or in other ways. By components, I mean not only * .js files, but also templates, styles, etc.

Another ideological feature of Well is the naming convention for modules - the names of the modules correspond to their paths. Those. Views: Common: BasicPage corresponds to views / common / basic-page.js .

Application


Since there is no community yet, I will give an example from my own activities. In order to organize the work of N applications, a directory structure was developed, which looks something like this:

 apps - project_one - project_two - project_three- project_n build plugins vendor require.js well.js 


The apps folder contains projects and all their individual files: templates, styles, images, scripts, etc.

 project_one - styles - images - js -- views -- models -- collections -- utils -- index.html 

In the build folder there are scripts for building. I use Gulp to build

The plugins folder contains plugins that can be connected to via nginx, all applications have access to.

The vendor folder is also used by all applications via nginx. Libraries, frameworks, jquery, backbone, underscore and so on are stored in this folder.

 vendor -src --backbone.min.js --handlebars.min.js --handlebars-runtime.min.js --jquery.min.js --underscore.min.js -backbone-well.js -handlebars-well.js -jquery-well.js -underscore-well.js 


By default, the library cannot just be taken and used; it should be wrapped in a well module:

 wellDefine('Vendor:JqueryWell', function(app){ //  ,   ,     window this.set({context: window}); this.exports(function(){ //          }); }); 


Naturally, all this can be done not manually, but, for example, with the help of Gulp. For convenience, all source files of libraries are stored in vendor / src, and the modules themselves are directly in vendor. They also get the -well suffix in order to be able to understand that this is a module.

For example, as the component used in several projects, I took the User entity. To create a user plugin, you need everything related to the user, i.e. registration form, authorization, authorization through social networks, put in the folder plugins / user. The result is the following structure:

 plugins -- user --- main.js --- model.js --- form.html --- login-view.js --- style.css 


So, before you start creating project files, you need to configure well. The configuration is described in index.html, before the well.js file is connected.

index.html
 <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>Well-example(development)</title> <script> window.WellConfig = { appRoot: '/js', pluginsRoot: '/plugins', vendorRoot: '/vendor', strategy: 'Strategy', appName: 'PluginsExample', //isProduction: true, }; </script> <script src="require.js"></script> <script src="/well/well.js"></script> </head> <body> <div id="site-container"></div> </body> </html> 


appRoot - application root folder. Regarding it, the paths and names of the application modules will be calculated.

pluginsRoot is the root folder of the plugins. Regarding it, the paths and names of plug-in modules will be calculated. I recall that in my case, this folder is shared and is two levels above the root of the application, so access to it is done via nginx.

vendorRoot - similar to plugins, only a library repository.

strategy - strategy is the module that launches the application. In this case, the module is called Strategy, because it corresponds to the name of the js / strategy.js file.

appName is an optional parameter that sets the name of the application, which will eventually be available in the window object.

isProduction is an optional parameter indicating that modules are minified and loaded. For production, it is enough to glue and minify all modules into one file. The only condition that must be met - the strategy must be glued to the very last.

And finally javascript

The starting point for launching an application is always the strategy module, the path to which is specified in the Well configuration. In strategy, I usually connect libraries, plug-ins and other modules that are used by all application components.

In this example, in addition to the libraries, the User plugin is connected. As a rule, a plug-in should have one main module that connects to an application. Here is how this connection will look in our example:

strategy.js
 wellDefine('Strategy', function (app, modules) { this.use('Vendor:JqueryWell'); //     ,  // underscore       jquery this.use('Vendor:UnderscoreWell'); // -      this //  ,      //  as.       //    this.Main   this.User // autoInit -   ,    // ,     . // -    true this.use('Plugins:User:Main', {as: 'User', autoInit: false}); this.use('Helpers:Utils'); this.exports(function () { var user = app.User = new this.User({ onLoginSuccess: function () { $('#site-container').html('<h3>Hello, ' + user.model.get('name') + '</h3>') }, onLoginError: function (err) { alert(err); } }); }); }); 


Dependencies are activated in the order they are declared. Ie, if Plugin: User: Main uses jQuery or Underscore, then both of these libraries will already be available, but Helpers: Utils will not.

By activation is meant the execution of the exports () function.

So, such a plugin architecture is not mandatory, but is recommended for use. In this case, the Main module is the gateway between the application and the rest of the plug-in modules that do not have to be opened.

plugins / user / main.js
 wellDefine('Plugins:User:Main', function (app, modules) { // well.js     //    ,    //         this.use(':Model', {as: 'UserModel'}); this.use(':LoginView'); // well.js     , //        this.get(<option>) this.set({ template: 'form' }); this.exports(function () { var mod = this; var User = function (opts) { this.options = opts || {}; this.appendCss(); this.loadTemplate(function (err, html) { if (err) throw err; this.onTemplateLoaded(html); }.bind(this)); this.model = new mod.UserModel(); }; User.prototype.appendCss = function () { var link = document.createElement("link"); link.type = "text/css"; link.rel = "stylesheet"; link.href = 'plugins/user/style.css'; document.getElementsByTagName("head")[0].appendChild(link); }; User.prototype.loadTemplate = function (next) { $.get('plugins/user/' + mod.get('template') + '.html', function (html) { next(null, html); }).fail(function (err) { next(err.statusText) }); }; User.prototype.onTemplateLoaded = function (html) { this.render(html); new mod.LoginView({ el: $('.login-form'), model: this.model, onLoginSuccess: this.options.onLoginSuccess, onLoginError: this.options.onLoginError }); }; User.prototype.render = function (html) { $('#site-container').html(html); }; return User; }); }); 


The next two modules are the Model and View of our plugin. They were connected higher in the main module.

plugins / user / model.js
 wellDefine('Plugins:User:Model', function (app, modules) { this.exports(function (options) { var M = function () { this.attrs = {}; }; M.prototype.set = function (key, value) { this.attrs[key] = value; }; M.prototype.get = function (attr) { return this.attrs[attr]; }; return M; }); }); 


plugins / user / login-view.js
 wellDefine('Plugins:User:LoginView', function (app, modules) { this.exports(function (options) { var L = function (opts) { _.extend(this, { model: opts.model, $el: opts.el, options: opts }); this.$('.submit').on('click', this.submit.bind(this)); }; L.prototype.$ = function (selector) { return this.$el.find(selector); }; L.prototype.auth = function (login, pass) { var err = 'bad username or password'; if (login === 'demo' && pass === '1234') this.onLoginSuccess(); else this.options.onLoginError ? this.options.onLoginError(err) : alert(err); }; L.prototype.onLoginSuccess = function () { this.model.set('name', 'John Doe'); if (this.options.onLoginSuccess) this.options.onLoginSuccess(); }; L.prototype.submit = function () { var login = this.$('input[name=name]').val(); var pass = this.$('input[name=pass]').val(); if (login && pass) this.auth(login, pass); else alert('Error: fill in all necessary fields!'); }; return L; }); }); 


Due to the fact that my goal is to talk about how to use Well.js, I did not begin to describe the methods of the plugin. The working version of the example can be viewed in the project repository .

Perhaps this acquaintance can be finished.

If you have any comments on the publication, please write about them in a personal, I will fix everything. Sound criticism, ideas and suggestions for the development of the project are welcome.

Future plans include support for Well.js modules in Node.js so that you can use common modules on both the client and the server.

I would like to thank my szcheh perlovik colleague , some of whose recommendations have found application in this project.

Also, if you thought my idea was useful, then for me it would be a good reason to continue to talk about it.

Source: https://habr.com/ru/post/243397/


All Articles