Skip to content

Instantly share code, notes, and snippets.

@alexeybondarenko
Created April 12, 2018 14:26
Show Gist options
  • Save alexeybondarenko/4f416494bf4471a2762a4bac858a8a1a to your computer and use it in GitHub Desktop.
Save alexeybondarenko/4f416494bf4471a2762a4bac858a8a1a to your computer and use it in GitHub Desktop.
Example of Vue Frontend architecture readme

Example of Vue Frontend architecture readme

Technologies

  • VueJS
  • Vuex
  • Webpack
  • ESLint

Build Setup

# install dependencies
npm install

# serve with hot reload at localhost:8080
yarn dev

# build for production with minification
yarn build

# build for production and view the bundle analyzer report
yarn build --report

# run storybook
yarn storybook

For detailed explanation on how things work, checkout the guide and docs for vue-loader.

Frontend Architecture

Introduction

This document describes the essentials of the principles that we are following during the development of this application. If you feel that we can improve some of them feel free to create the PR and we will discuss the changes.

We always have to hold the balance between the speed of the feature delivery and the technical quality.

Project structure

  • src/components - (generic) UI components. Re-usable, mostly stateless elements.
  • src/containers - (not-generic) UI element, like pages, blocks (e.g. footer, navigation, user card and etc), that are integrated with store and are not generic. Try to avoid custom styles in the containers.
  • src/containers/pages - pages
  • src/containers/layouts - layouts
  • src/containers/blocks - not-generic components and data-specific, like UserInfoCard, navigation or footer.
  • src/filters - Vue filters
  • src/services - configuration of 3rd party services/libraries
  • src/store - Vuex store files
  • src/router - Vue-router files

Important: try to avoid custom styles in the containers. They usually can be just the composition of the components. In this case in long-term we would have consistent UI, because the most of our style will be encapsulated inside the components.

Component structure

  • ComponentName/index.vue - entry point of a component. Container template and loaders for scripts and styles' files
  • ComponentName/scripts.js - JS scripts of a component
  • ComponentName/index.story.js - story of a component
  • ComponentName/styles.scss - styles of a component

Code format

Project contains ESLint config. We recommend you to configure your favorite code-editor to use it to check you code during the development. This will save the time in PR reviews.

Dependency injection

Application has @ alias, that is related to src folder. Use it, instead of the relative paths.

Bad example

import Page from '../../../../components/Page'

Good example

import Page from '@/components/Page'

Trello workflow

We have the several columns in out trello board:

  • Backlog - entry point for the tickets/ideas or bug reports
  • Blocked - tickets, that are blocked by another tickets of by other reasons. If it's a frontend tickets it usually has the corresponing API change in the ticket description and these ticket is not ready yet.
  • To do - tickets, that are ready to be taked for development. <- Take these ticket for development
  • In progress - tickets, that are currently in development. We need this column to understand that you are working on.
  • In review - tickets, that are development and they have the corresponging PR and you've requested for the review.
  • Released - tickets after review. This column is using by project manager to check the implemented feature

Git flow

  • Stable branch - master
  • Don't push directly to the stable branch. Use PRs instead

Workflow:

  1. Start a ticket with a new branch
  2. Write code
  3. Create Pull Request
  4. Get an approve from one of your coworkers
  5. Merge PR's branch with the stable branch

Name of the branches

We are not following some strict rule on this stage of branch naming. So we have a single rule for the branch names:

  1. Make you branch names meaningful.

Bad example

fix-1
redesign

Good example

fix-signals-table
new-user-profile-page

Releases

We have the release script ./bin/release.sh. You will need Github Token to be able to create releases. Create new Github Personal Access Token with scopes (repo and admin:org)

./bin/release.sh --github-token=$GITHUB_TOKEN [--increment]

Use --increment flag, if you want to patch version of the project.

Script contains the steps:

  1. Increment version (if --increment flag is passed): update version in package.json
  2. Create Github release and git tag
  3. Build project
  4. Create tag archive and attach it to the Github release

Deployment

Deployment scripts are in deployment folder.

To configure new environment

We will need NGINX installed on the server.

  1. Go to project folder
mkdir -p /var/www/project-name
cd /var/www/project-name
  1. Copy deployment folder to the project folder using you favorite client.
  2. Change folder rights
chmod -R 755 deployment
  1. Configure NGINX

Example of NGINX config can be found here. You have to specify SSL parameters, server_name and paths to the sources.

Default NGINX configuration is looking /var/www/project-name/current. Let's create symlink to current symlink in deployment folder.

ln -s deployment/current current
  1. Create environment configuration file

Default NGINX config is looking into /var/www/project-name/configs/config.js.

To create this file, execute this code.

mkdir -p configs
echo "window.__CONFIG__ = {};" > configs/config.js

Configure environment

Available configuration parameters can be found here - BUILT_IN_VALUES. For example, you can specify API_URL parameter, like this.

window.__CONFIG__ = {
  API_URL: 'https://example.com',
};

Deploy new version

  1. Run deploy-version.sh
./deployment/deploy-version.sh --version=0.0.5 --github-token=$GITHUB_TOKEN

It will download build assets from Github Release, unpack it and change current symlink.

- deployment/downloads/ - downloaded release archives
- deployment/versions/ - unpacked folders of the versions
- deployment/current - symlink to current version

How to rollback to previous version*

If you want to rollback to 0.0.5 version and it exists in versions folder, use this command.

ln -s versions/0.0.5 deployment/current

TODO: write specific script, that will check availability of the version in the versions folder

Nginx configuration

Key points:

  • all assets are caching and compressing with gzip
  • service worker is not caching
  • config.js is not caching

Styling

We're using scoped styles, so you don't need to use BEM or other methodology to avoid conflicts in the styles. In BEM terminology, you don't have to use elements. Use only block and modificators. If you feel that you also need an element - think, probably you have to extract a new component from this template.

Bad example

.page {
  &__title {}
  &__content {
    &_active {}
  }
}

Good example

.root {} // root element
.title {}
.content {
  &.is-active {}
}

Use is- prefix for the modificators.

Components

We decompose our UI into the components. Component is an independance element, that can be reusable and doesn't relate to the parents' styles and scripts. This mean, that the styles of the component is only related to the its own styles and don't related to the styles of the parent components (the same for logic scripts).

Props

  1. Boolean props are true by default if they are passed. e.g <Component hidden /> equals <Component :hidden="true" />

Bad example

<Component :hidden="false" />

Good example

<Component /> // hidden = false
<Component hidden /> // hidden = true
  1. Use (emiting of the events)[https://vuejs.org/v2/guide/components.html#Custom-Events]) for the event callbacks
<Component @click="clickHandler" />
  1. Inject components with t- prefix
<template>
  <t-alert>Text</t-alert>
</template>
<script>
  import tAlert from '@/components/Alert'

  export default {
    components: {
      tAlert,
    }
  }
</script>

Storybook

We are using Storybook as our UI library. Add to storybook all the "dummy UI element" - components, that don't have the fetching of the data or vuex connections.

The goal of using storybook is to help development team to know about the existing UI components and find them.

To add storybook, add index.story.js file with the story's definition to the component folder. See example here.

Run storybook

yarn storybook

Routes

  1. You have to be able to load page information based only on the URL params.
  2. Use the same URL as a main API endpoint of the page.
Page URL: /users
API request: get:/users

Page URL: /users/:id
API request: get:/users/:id

Page URL: /users/new
API request: post:/users
  1. If you need to save the state of the page - use the URL query params. e.g /users?page=1, /users?q=John Doe
  2. Make URLs meaningful

Data

Numbers

API can return tiny numbers with big precision. Default JS math methods can be not enough for the number manipulation.

Use bignumber.js, when you have to manipulate with numbers.

Normalization

We're making normalization of the data. We need it for 2 reasons:

  • to have a single version of the entity in the store and be able to update it and be sure, these changes will be made in the rest of the app. Caution. If you don't need to update the entity globally - don't use/update the instance in the store.
  • to have a cache of the entities

Key points:

  • For the normalization of the data we'are using normalizr
  • The schemas are defined here
  • Normalization is performing in actions
  • Denormalization is performing in getters

Example: If you have to show the list of the entities in the page, you have to store the list of ids on the page component and then use getter to denormalize data by these ids.

import { mapActions } from 'vuex';

import Page from '@/components/Page';
import SignalsTable from '@/containers/blocks/SignalsTable';

export default {
  components: {
    SignalsTable,
    Page
  },

  data: () => ({
    signalsIds: []
  }),

  computed: {
    signals() {
      return this.$store.getters.signals(this.signalsIds);
    }
  },
  methods: {
    ...mapActions([
      'getSignals'
    ])
  },

  async mounted() {
    this.signalsIds = await this.getSignals();
  }
};

Vuex modules

We are using vuex for the state-management. Vuex folder structure:

store/
store/index.js - entry point
store/mutations.js
store/initialState.js
store/actions.js
store/getters.js
store/modules/
store/modules/moduleA/index.js
store/modules/moduleA/mutations.js
store/modules/moduleA/getters.js
store/modules/moduleA/actions.js
store/modules/moduleA/initialState.js

References:

  1. https://medium.com/3yourmind/large-scale-vuex-application-structures-651e44863e2f
  2. https://github.com/bstavroulakis/vue-wordpress-pwa/tree/master/src/vuex

Vuex architecture

If you are not familliar with the Vuex - read the documentation first. https://vuex.vuejs.org

Data in containers

Fetch the data

– TODO

Read data from store

See the example in the normalization section

Authorization

API uses devise_token_auth gem. Here is the description: https://github.com/lynndylanhurley/ng-token-auth.

At the start of the application, we send a request to validate_token to validate token from storage and fetch user's info. See the logic in here

Next, we're checking authorization on the router-level. We also handle 401 Unauthorized API errors and terminate user session if it is caught.

The application has to store this data in the localStorage. These parameters are required in the validate_token request, that is used to fetch user's data at a boot time.

  • uid
  • client
  • token

They can be also passed via URL query params. Query params are using, for example, in a reset password confirmation page.

  • uid
  • client_id
  • token

Internationalization (i18n)

To be ready for i18n - avoid the strings in the logic (JS scripts) - only in the view-layer (templates). In this case we will be ready to extract them and replace with the loader for the translates.

Forms

We're using model-based validation with vuelidate.

Server side validations

To show the server side validation errors you can simply get errors object ({ field_name: ["errors"] }) and pass them to a form-control component, or use error-messages component directly. e.g.

<t-form-control :error-messages="errors.field_name">
<error-messages :messages="errors.field_name">

Example.

async onSubmit() {
  this.$v.$touch();
  if (this.$v.$error) return;

  try {
    await this.signUp({
      password: this.password,
      email: this.email,
      account_name: this.account_name
    });
    this.$router.push('/');
  } catch (error) {
    this.errors = get(error, 'response.data.errors');
  }
}

Custom validation rules

You can easily write custom validators and combine them with builtin ones, as those are just a simple predicate functions. e.g.

Example.

validations: {
  password: {
    required,
    password: minLength(6)
  },
  password_confirm: {
    required,
    password: minLength(6),
    equal: (value) => value === this.password // just example, there is sameAs method in buildIn validations
  }
},

P.S. If you need check for equality, there is sameAs method in buildIn validations sameAs.

i18n

Services

Application uses several 3rd party services. They are configured here.

Google Tag Manager

We collect all analytics events thought Google Tag Manager. The configuration contains 2 part: in index.html we are loading a gtm script, in main.js we configure an integration GTM with Vue JS app.

GTM - Vue integration includes:

  • handling router changes
  • passing GTM to the Vue component

You can configure GTM using these configuration parameters:

  • GTM_TRACKING_ID
  • GTM_APP_NAME

Rollbar

Rollbar is used to collect the errors.

You can configure Rollbar instance with:

  • ROLLBAR_ACCESS_TOKEN
  • ROLLBAR_ACCESS_ENVIRONMENT
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment