Skip to content

Instantly share code, notes, and snippets.

@alexeybondarenko
Created February 16, 2018 09:15
Show Gist options
  • Save alexeybondarenko/2975df0e53c8b166537ac504971a590c to your computer and use it in GitHub Desktop.
Save alexeybondarenko/2975df0e53c8b166537ac504971a590c to your computer and use it in GitHub Desktop.
Frontend Architecture. Example of Readme

Readme example

Technologies

  • VueJS
  • Vuex
  • Webpack
  • ESLint

Build Setup

# install dependencies
npm install

# serve with hot reload at localhost:8080
npm run dev

# build for production with minification
npm run build

# build for production and view the bundle analyzer report
npm run build --report

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/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.

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 per-field validation with vee-validate.

Server side validations

To show the server side validation errors use this helper. It will map server side errors to the client format and will add them to a vee-validate instance. These errors message will be displayed according to your rules in the views. e.g.

<div v-show="errors.has('username')" class="help-block alert alert-danger">
  {{ errors.first('username') }}
</div>

Example.

async onSubmit() {
  const result = await this.$validator.validateAll();
  if (!result) return;

  try {
    await this.signUp({
      password: this.password,
      email: this.email,
      account_name: this.account_name
    });
    this.$router.push('/');
  } catch (e) {
    showServerValidationErrors(e, this.$validator);
  }
}

Custom validation rules

You can define new validation rules here. There are also the example of the custom validation rule

s18n

Example of i18n for the validation messages.

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