Frontend development of portals on open source software: share experience

In the first part of the article on how we create portal solutions for the largest employers in Russia, the architecture was described by the backend. In this article we will pass to frontend.



As already noted in the first part, our main goal was to develop a platform that can be easily scaled and maintained.

The reusability of the

Front is mostly written in Vue.js, and since the entire portal is divided into portlets, each of them is a separate Vue instance with its own side (Vuex), route (Vue Router) and components. Each such instance is moved to its own repository.

Since the portlet is in its repository, the question arises of how not to write a lot of the same type of code for different portlets. To solve this problem, we transfer everything that can be reused to a separate repository, which is then connected via .gitmodules. At the moment there are two such submodules.

One stores common functionality: these are common components, services, constants, etc. We have this module called Vue-common.

The second submodule contains settings for assembly, it stores configs for webpack, as well as loaders and helpers needed during assembly. This module is called Vue-bundler.

For the convenience of working with the API, REST methods were also divided into general and local. In Vue-common, methods were introduced for obtaining a list of portal users, administration methods, access to the portal file system, and others. All API endpoints were moved to separate services that registered at the entry point and connected to the Vue instance. Then they could be used anywhere in the application.

Each individual service is registered inside the plugin. Vue has a built-in use function for connecting plugins. Read more about plugins in Vue here .

The plugin itself is initialized like this:

class Api {
    constructor () {
        //  http ,   
        this.instance = instance

        //     
       //     this,      http 
        Object.keys(commonServices).forEach(name => commonServices[name](this))
        
        //     
	requireService.keys().forEach(filename => requireService(filename).default(this))
    }

    install () {
	Vue.prototype.$api= this
    }
}

export default new Api()

In addition to initialization:

  1. The http client instance is created. Which sets the baseURL of our backend and the headers

    const instance = axios.create({
    	baseURL: '/example/api',
    	responseType: 'json',
    	headers: {
    		'Content-Type': 'application/json',
    		'Cache-Control': 'no-cache',
    		'Pragma': 'no-cache',
    	}
    })
                    backend   ,   axios.

  2. Services are created that store the requests themselves

    // api -   http 
    export default api => {
    	api.exampleService= {
    		exampleGetRequest(params) {
    			return api.instance.request({
    				method: 'get',
    				url: `example/get`,
    				params
    			})
    		},
    		examplePostRequest(data) {
    			return api.instance.request({
    				method: 'post',
    				url: `example/post`,
    				data
    			})
    		},
    	}
    }
    

    In vue-common it’s enough to create only such a service, and it is already registered for each portlet in the Api class
  3. General and local services are registered

         const requireService = require.context('./service', false, /.service.js$/)
    

In components, they are used very simply. For instance:

export default {
	methods: {
		someMethod() {
    			this.$api.exampleService.exampleGetRequest()
}
}
}

If you need to make requests outside the application, you can do this:

//    (@ -     )
import api from ‘@/api’

//        
api.exampleService.exampleGetRequest()

Scaling

As noted above, a separate bundle is collected for each portal, and each bundle has its own entry points. In each of them, components and assets are registered, authorization is configured for local development, and plug-ins are connected.

Components are registered globally for each application, both local and general.

Component registration looks like this:

import _ from “lodash”

const requireComponent = require.context('@/components', true, /^[^_].+\.vue$/i)

requireComponent.keys().forEach(filename => {
    const componentConfig = requireComponent(filename)

    // Get PascalCase name of component
    const componentName = _.upperFirst(
        _.camelCase(/\/\w+\.vue/.exec(filename)[0].replace(/^\.\//, '').replace(/\.\w+$/, ''))
    )

    Vue.component(componentName, componentConfig.default || componentConfig)
})

Sometimes it becomes necessary for the portal we are developing to add unique functionality, and for this you have to write components that are unique to it, or simply implement a component in a different way. It is enough to create a component in a separate folder, for example / components-portal / * portal name * / *. Vue, and register it in the desired entry point, adding require.context not for one folder, but for several.

const contexts = [
    require.context('@/components', true, /^[^_].+\.vue$/i),
   require.context('@/components-portal/example', true, /^[^_].+\.vue$/i)
]

contexts.forEach(requireComponent => {
    requireComponent.keys().forEach(filename => {
        const componentConfig = requireComponent(filename)

        // Get PascalCase name of component
        const componentName = _.upperFirst(
            _.camelCase(/\/\w+\.vue/.exec(filename)[0].replace(/^\.\//, '').replace(/\.\w+$/, ''))
        )

        Vue.component(componentName, componentConfig.default || componentConfig)
    })
})

If you set the same name for the component for a specific portal as from the general library of components, then it will simply be rewritten as an object property and will be used as a component for this portal.

Assets such as svg icons are also globally registered. We use svg-sprite-loader to create a sprite from svg icons and then use them via <use: xlink: href = "# * icon name *" />

They are registered like this:

const requireAll = (r) => r.keys().forEach(r)

const requireContext = require.context('@/assets/icons/', true, /\.svg$/)

requireAll(requireContext)

In order to scale not only functionality, but also component styles, we have implemented a mechanism for changing styles for a particular portal. Single-file components specify styles in the <style> tag and are used by default. In order to implement styles for a specific portal, you need to register them in another tag

All Articles