Connecting static resources from templates

    Having worked on a number of web projects as a frontend / backend developer / layout designer in different companies, I constantly came across an ineffective and ugly approach to the task of connecting the necessary static resources (for now, consider this as .css and .js files) to display on the page .

    The main problem of all the approaches I came across is the close relationship between the structure of the frontend code, the logic of the deploy and backend code (mainly templates), as well as the lack of semantics. Further, the term frontend-code will mean the whole collection .js , .cssand any other files or resources that are given to the browser. Typically, these files are handled by frontend developers (sick!).

    First, I will give a couple of real examples (in pseudo-code, since different frameworks and languages ​​were used everywhere, and the real code will only confuse us), I will consider the shortcomings and problems associated with the approaches used, and in the end I will describe my vision of this problem.

    First example

    On one large 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:
    

    We will assume that the methods ViewHelpers.appendStylesheet and ViewHelpers.appendJsFile guarantee us the connection of the files transferred to them in the corresponding tag on the final page. The line PROJECT_STATIC_VERSION was used to add a key to the url, updating which would force the browser to download a new version of this file.
    In addition to this, files were often connected outside 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 if the js code included from the template relied on ExtJS . Unfortunately, in 95% of cases this was done by copy-paste type:

    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 of course, of course):
    • The backend code and template code are aware of the structure of the frontend code. Changing (adding, moving, merging, splitting) the frontend code will make it necessary to change the backend code. What can be a rather 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 fellow. What is not ice! (In the above example, it saved that there was no separation of frontend / backend developers on the project, so the person implementing this or that component wrote both backend and frontend code.)
    • There is no single point for connecting static resources. Because 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.
    • Lack of explicit dependencies. Since the list of files was simply connected, it was impossible to identify the relationship between them. For example, a developer making a new component based on someone else’s could copy part of the resource connections (in his opinion, responsible for some kind of isolated part), and then puzzle why JavaScript does not work. But the fact is that he forgot to include the file /lib/some-cool-plugin.js
    • Adding deploy logic to templates. I consider this the most wrong move. (In the following example, it will be even sadder!). Concatenation of the static version to the resource url is one of the deployment techniques for the application or environment, and has nothing to do with the logic of the template and frontend code. Plus, this is another opportunity to make a mistake, forgetting to add this key (too lazy!) Or forgetting to change this key (this often happened).
    • Duplication. Both directly the connection code ( ViewHelpers.blaBlaBla () ), and the same files in different templates. In general, DRY .
    • Lack of semantics. Just the above list of resources says little. We cannot isolate dependencies, determine the nature of these resources, understand where this code is still used, etc.
    • A commonplace typo opportunity. Long file paths and names are often prone to typos. He often spent extra time on a stale head to determine 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, because often routing mixed with nginx 'a. Anyway, no one was doing this.

    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:
    

    Plus, at the project level, there was a config for describing the files needed to merge and minify js and css code of the form:

    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 disadvantages of the previous one, only in a more terrifying form:
    • Deployment logic in the template. And so now the frontend programmer is fully aware of all (as many as 2!) Environments on which the application can be deployed. Plus, this adds responsibility for maintaining the config for merging and minifying files, which can only be tested on the production environment. It’s scary to imagine what will happen if a new environment is added, such as stage or testing. In fact, no statics will connect ( if-else-if ). I consider this the most nightmarish option of connecting 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 under different environments.
    • No dependencies. At the moments when some components started using the common code, I had to shaman. All minified versions were divided into two parts (the list for Dev wasn’t changing at all), the config was getting longer, and the worst thing is now to check that we correctly connected all the files in the min version, I had to add lists from two sections in my 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 1.js and 2.js. Block B uses 2.js and 3.js files. We can no longer connect both of these blocks, because 2.js file will be processed 2 times.

    Task

    As a result, after analyzing the shortcomings of these and other approaches, I gathered a number of requirements for a system for connecting static resources:

    • One place to connect resources
    • Independence of the structure and ease of modification of the front-end code
    • No deploy logic in templates
    • Easy dependency management, duplicate minimization
    • Explicit error message in case of typos
    • The presence of semantics

    Decision

    • A single place to connect resources. It is necessary to strictly determine the place in the project where you can connect static resources. I think the only decent place is the template. Why? As a rule, one or another markup block is associated with the corresponding styles and Java script. It would be logical to define this connection in the same template. In the end, I propose to ban the connection of files outside the template code.
    • The presence of semantics. It’s 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 a 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 of duplicates. Because we refer to the names of the blocks, we need a place where we will describe these blocks. I suggest using an easy-to-read configuration format (for example, in the YAML language) 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 (e.g. lib / jquery ).

    • No deploy logic. How the static resources will be deployed should be decided by the backend application / framework code. There you can apply any strategy (merger, minification, return from CDN, etc.). To manage this, you can expand the config format.
    • In one template - one "includ". If the template needs to connect a static resource, it is advisable to do this in one include with a self-explanatory name. Do not be lazy to start a block for connections like “library + my file” or “general_module + modification”. When using 2 or more connections in one template, we add a description of the dependencies in the template itself, thereby returning to the problems of the first examples.
    • Independence and ease of modification of frontend code . Now you can easily add new files to blocks, split, move, etc. However, you do not need to make any changes to the templates.
    • Error in case of typos. Yes. If a block is connected in the template or the block uses another block, which was not defined in the map of static resources, as a dependency, we display an explicit error message. So that we are always sure of the correctness of a particular connection.

    Useful 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 map into entities like libs.yaml, framework, yaml, my-module.yaml, my-component.yaml, etc.
    • Expand the “map” config format. Add various features like .less files, loading of 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 introduce it into the current project at work.

    Thanks to everyone who read it. I will be glad to any comments and suggestions!

    Also popular now: