Having worked on a number of web projects as a frontend / backend developer / maker-up in different companies, I constantly encountered an inefficient and ugly approach to the task of connecting the necessary static resources (for the time being, we consider this
.css and
.js files) to display on the page .
The main problem of all the approaches I have encountered is the close connection between the structure of the frontend code, the logic of the deployment and the backend code (mainly templates), as well as the absence of semantics. Further, the term
frontend-code will be understood as the entire set of
.js ,
.css and any other files or resources that are given to the browser. As a rule, frontend developers (sick!) Deal with these files.
First, I will give a couple of real-life examples (in pseudocode, since different frameworks and languages ​​were used everywhere, and the real code will only confuse us), consider the shortcomings and problems associated with the approaches used, and at the end I will describe my vision of this problem.
First example
On one extensive project (based on the
Zend Framework ), static files were connected in approximately the following way:
')
// someWidget.tpl // PROJECT_STATIC_VERSION — ( ) ViewHelpers.appendStylesheet("css/some-path/sub-path/some-widget.css?" + PROJECT_STATIC_VERSION); ViewHelpers.appendJsFile ("js/some-path/sub-path/some-widget.js?" + PROJECT_STATIC_VERSION); // Layout: <div class="some-widget"> </div>
We assume that the methods
ViewHelpers.appendStylesheet and
ViewHelpers.appendJsFile guarantee us the
inclusion of the files transferred to them in the corresponding tag on the final page. The
PROJECT_STATIC_VERSION line was used to add some key to the url, the update of which would force the browser to download the new version of this file.
In addition to this, files are often connected outside of templates, for example, in the controller code or in the element decorator code (
Zend_Form_Decorator ). Especially frequent was the connection of
ExtJS js-framework files in case the js-code connected from the template was based on
ExtJS . Unfortunately, in 95% of cases this was done by a copy-paste of the form:
ViewHelpers.appendJsFile("js/libs/ext-js/ext-js.js?" + PROJECT_STATIC_VERSION); ViewHelpers.appendCss ("css/libs/ext-js/ext-js.css?" + PROJECT_STATIC_VERSION); ViewHelpers.appendJsFile("js/libs/ext-js/locale-ru.js?" + PROJECT_STATIC_VERSION);
So, the disadvantages of this approach (most certainly is obvious):
- Backend-code and template code know about the structure of the frontend-code. Changing (adding, moving, merging, splitting) the frontend-code will lead to the need to change the backend-code. What can be quite a lengthy and painful process, if some files were connected in a considerable number of templates (and often it is). Those. The frontend developer essentially depends on the backend sibling. What is not ice! (In the above example, it was saved that there was no separation of frontend / backend developers on the project, so the person implementing this or that component wrote both the backend and frontend code.)
- There is no single point of connection of static resources. Since Since files can be connected from different places: controller code, element decorator code, view helper code and template code itself, it is no longer possible to simply determine which files a particular component needs.
- No explicit dependencies. Since the file list was simply connected, it was impossible to identify the dependency between them. For example, a developer making a new component on the basis of someone else, could copy a part of the resource connections (in his opinion, responsible for some separate part), and then puzzle over why JavaScript does not work. And the fact is that he forgot to connect the file /lib/some-cool-plugin.js
- The mixing of deploy logic into templates. I think this is the wrong move. (In the following example, it will be even sadder!). The concatenation of the static version of a resource's url is one of the application deployment techniques for one environment or another, and is in no way connected with the logic of the template and the frontend-code. Plus, this is another opportunity to make a mistake, forgetting to add this key (laziness!) Or forgetting to change this key (this often happened).
- Duplication. As directly connection code ( ViewHelpers.blaBlaBla () ), and the same files in different templates. In general, DRY .
- Lack of semantics. Just a listed list of resources says little. We cannot isolate dependencies, determine the nature of these resources, understand where this code is still used, etc.
- The banal possibility of typos. Long file paths and names are often prone to typing errors. On a stale head, I often spent too much time determining that the specified file was not connected (404 Not Found). Of course, you could write code that checks for the presence of certain files, but this was not always possible, since Often, rules with nginx 'were mixed into the routing. And in general, nobody did it.
The second example from a symfony project
// SomeConroller.SomeAction If (Config.Env == "Production") { includeCss("styles/feature.min.css"); includeJs("js/feature.min.js"); } Else If(Config.Env == "Dev") { includeCss("styles/feature/global.css"); includeCss("styles/feature/sub-feature.css"); includeJs("js/classes/Core.js"); includeJs("js/classes/Event.js"); includeJs("js/classes/CoolPlugin.js"); includeJs("js/classes/Feature.js"); includeJs("js/classes/FeatureSubFeature.js"); } Layout: <div class="feature"> <!
Plus, at the project level, there was a config for describing the files necessary for merging and minifying
js and
css of the following code:
styles/feature.min.css: styles/feature/main.css styles/feature/sub-feature.css js/feature.min.js: js/classes/Core.js js/classes/Event.js js/classes/CoolPlugin.js js/classes/Feature.js js/classes/FeatureSubFeature.js
This example has all the drawbacks of the previous one, only in a more terrifying form:- Deploy logic in the template. And so now the frontend programmer fully knows about everything (as much as 2!) Environments to which the application can be deployed. Plus, this adds the responsibility for maintaining the config file for merging and minifying files, which can only be tested in the production environment. It is terrible to imagine what will happen if a new environment is added, for example, stage or testing. In fact, no static will connect ( if-else-if ). I think this is the most nightmarish option to connect static resources.
- A bunch of duplication. Changing the structure of the front-end code turns into a nightmare. It is necessary to change the config, all places of inclusion for different environments.
- No dependencies. At the moments when some components began to use a common code, we had to shaman. All the minified versions were divided into two parts (the list for Dev simply did not change), the config became longer, and worst of all, now to check that we correctly connected all the files in the min version, we had to add lists from two sections in the mind.
At the same time, errors also appeared from places where the non-crushed part was still used, and some code worked twice in different places. For simplicity, imagine an example: Block A uses the files 1.js and 2.js. Block B uses the 2.js and 3.js files. We can no longer connect both of these units, since 2.js file will be processed 2 times.
Task
As a result, after analyzing the shortcomings of these and other approaches, I collected a number of requirements for the system of connecting static resources:
- A single place to connect resources
- The independence of the structure and ease of modification frontend code
- No deploy logic in the templates
- Easy dependency management, duplicate minimization
- Explicit error message in case of typos
- Presence of semantics
Decision
- A single place to connect resources. It is necessary to strictly define in the project a place where you can connect static resources. I think the only decent place is a template. Why? As a rule, this or that block markup is associated with the appropriate styles and java script. It would be logical to define this link in the same template. As a result, I propose to prohibit the connection of files outside the template code.
- The presence of semantics. It is easier for a person to operate with certain entities than with a list of files or resources. Therefore, the unit of connection will be the name of some block defined outside the template. This name should reflect the essence of the connection, not its composition or physical location. Example names: lib / jquery, lib / twitter-bootstrap, reset, blog-module / main, blog-module / photos, plugin / cool-one, etc.
- Description of dependencies and minimization duplicates. Since we refer to the names of the blocks, we need a place where we will describe these blocks. I propose to use an easy-to-read configuration format (for example, in YAML) to describe the so-called “static resource map”:
reset: - fw/css/reset.css lib/underscore: - libs/underscore/underscore.js options: - useCdn lib/jquery: - libs/jquery/jquery-1.7.2.min.js options: - useCdn lib/twitter-bootstrap: - libs/bootstrap/css/bootstrap.css - libs/bootstrap/js/bootstrap.js - css/bootstrap-override.css depends: - lib/jquery framework/core: - fw/js/Tiks.js - fw/js/Classes.js - fw/js/EventsManager.js - fw/js/Core.js - fw/js/CorePublic.js - fw/js/ModulesManager.js - fw/js/Module.js - fw/js/ModuleSandbox.js depends: - lib/underscore - lib/jquery options: - merge module/blog: - js/modules/blog.js - css/modules/blog.css depends: - framework/core - lib/twitter-bootstrap
Now in our blog template you just need to connect:
StaticInclude("module/blog")
All dependencies will pull themselves up and in the correct order. Duplicates connect only once (for example lib / jquery ).
- No deploy logic. How static resources will be deployed should be solved by the backend code of the application / framework. There you can apply any strategy (merger, minification, return with CDN, etc.). To manage this, you can expand the format of the config.
- In one pattern - one "incLud". If a template needs to connect a static resource, it is advisable to do it with a single connection with a self-explanatory name. Do not be lazy to start a block for connections like "library + my file" or "common module + modification". When using 2 or more connections in one template, we enter a description of dependencies in the template itself, thereby returning to the problems of the first examples.
- Independence and ease of modifying frontend code . Now you can easily add new files to blocks, split, move, etc. However, no changes to the templates are necessary.
- Error in case of typo. Yes. If a block is connected in a template or the block uses another block as a dependency that was not defined in the static resource map, then we output an explicit error message. So that we are always confident in the correctness of a particular connection.
Good Practices
- It is not necessary to store the entire "map" in one file. When there are a lot of blocks, it makes sense to split the card into entities like libs.yaml, framework, yaml, my-module.yaml, my-component.yaml, etc.
- Expand the format of the "card" config. Add different features like .less files, uploading some generated resources (for example, JS module descriptors, localization files via JSONP) to the map capabilities. Very convenient.
In conclusion, I want to say that I successfully use this approach in personal projects and gradually integrate it into the current project at work.
Thanks to everyone who read it. I will welcome any comments and suggestions!