Skip to content

Documentation

Hendrik-Jan de Harder edited this page Sep 28, 2018 · 65 revisions

Table of Contents

  1. Prerequisite
  2. Installation
  3. Code/Folder conventions
  4. Yarn Commands
  5. Seng generator templates
  6. Configuration
    1. Webpack configuration
    2. Project configuration
    3. Site configuration
  7. SCSS
  8. Sharing SCSS variables with Java/TypeScript
  9. Using the seng-device-state-tracker
  10. Component Structure
  11. Vuex store modules
  12. VueExposePlugin
  13. Dependency Injection
  14. Axios
  15. Gateway
  16. Using SVGs
  17. Autoprefixer
  18. Modernizr
  19. Asset management
    1. Types of assets
    2. Referencing assets processed by webpack
    3. Referencing static assets
  20. Previewing a build
  21. Polyfill configuration
  22. Localization support
  23. Application startup
  24. Pre-push hooks
  25. Code splitting
  26. Setting up a (api)proxy
  27. Runtime public path

Prerequisite

Installation

git clone https://github.com/hjeti/vue-skeleton.git

After cloning run yarn in the project root to get started.

Code/Folder conventions

  • Every component folder is formatted in PascalCase
  • Every component contains an index.js to integrate vuex-connect and for easy import import HomePage from 'page/HomePage'
  • Every page name is appended with Page
  • Always use the PascalCase formatting for components in templates <ScrollBar/>

Yarn Commands

  • yarn dev: Starts the development server
  • yarn build: Creates a build. To override the publicPath use: yarn build -- --publicPath=/v/vue-skeleton/. To override the version use: yarn build -- --versionNumber=v1
  • yarn preview: Preview the latest build in the browser
  • yarn lint:js: Runs eslint
  • yarn lint:ts: Runs tslint
  • yarn lint:scss: Runs stylelint
  • yarn analyze: Analyze webpack bundle using Webpack Bundle Analyzer
  • yarn prettify: Runs Prettier

Seng generator templates

Vue skeleton comes with seng-generator predefined templates and configuration which is used to scaffold components, pages and store modules.

Global installation of seng-generator is mandatory. To install it globally run the following command:

npm i seng-generator -g

After installation the following scaffolding commands are available:

  • component (sg component <name>) : Creates a component

Optional variables:

  1. props: List of props
  2. components: List of components
  3. lifecycle: Do you want to add empty lifecycle methods?
  • page (sg page <name>): Creates a page

Optional variables:

  1. props: List of props
  2. components: List of components
  3. lifecycle: Do you want to add empty lifecycle methods?
  • store (sg store <name>): Creates a store module

Check the seng-generator documentation for detailed information about modifying or adding templates.

Configuration

There are 3 configurations in Vue skeleton.

Webpack configuration

The webpack configuration is located in the build-tools/config/webpack folder. It consists of a base (webpack.base.conf.js) that contains all the configuration that is shared between development (webpack.dev.conf.js) and production (webpack.prod.conf.js). To avoid config duplication there is a webpackHelpers file with some helpers that return the right config context for development and production. Webpack is completely configured out of the box. But there is always room for customization.

Project configuration

The project config is located in build-tools/config/config.js. The project config contains variables that are used by webpack like the environment variables, location of the index file and the version path. If the site is running in a subfolder on the server it's possible to change the publicpath to avoid problems.

This file contains some other non webpack related settings. These additional settings are:

  • generate favicons and other icons
  • enable image optimization
  • proxy configuration
  • configuration of lint-staged tasks
  • enable/disable https during development

Site configuration

In development there needs to be a place to store urls of APIs like the facebook app id etc. Vue skeleton uses seng-config because it has a straightforward API and comes packed with a lot of features. It has support for properties, urls and variables and environments. The latter is very important because most of the config is environment based. Seng-config environments can extend each other for easy configuration.

All the app configuration related files are stored in src/config:

  • config.js: Contains the config and the environment logic. The environment is set based on the host. This configuration already has some common configuration set like:
    • API path
    • default locale
    • enable locale routing
    • enable localization
  • configManager: Is available using Dependency Injection reference data/Injectables.js.Check the documentation for all available methods.
  • localeConfig.js: Contains the locale config.

A ConfigManager instance is exposed to all Vue components as $config. In other places the injector needs to be used to get the ConfigManager instance.

SCSS

Vue skeleton uses SCSS for styling. It uses CSS modules to local scope the styling of Vue components. Check CSS Modules for more information.

There are two main SCSS files:

  • screen.scss Application global styling goes here. By default it only imports the normalize.css module.
  • utils.scss Application wide available mixins and variables. By default it imports seng-scss.

utils.scss Automatically imported in every component SCSS file.

Note: Make sure that utils.scss NEVER outputs CSS. Outputting CSS to utils.scss will add this CSS to every component.

Sharing variables between SCSS and Java/TypeScript

Vue skeleton comes with the node-sass-json-importer support. The skeleton imports by default mediaQueries.json which contains a standard set of media queries. This setup plays nicely with the respond-to mixin provided by seng-scss.

The Device State Tracker is implemented by default in the vue-skeleton. The configuration can be found in two places:

  • src/control/startUp.js: The Device State Tracker is created in the startup.
  • src/data/mediaQueries.json: The JSON file where the MediaQueries are defined.

Usage in Vue component

.js

import { mapState } from 'vuex';

// @vue/component
export default {
  name: 'HomePage',
  computed: {
    ...mapState({
      deviceState: state => state.app.deviceState,
    }),
  },
};

.vue

<template>
  <div>
    <p v-if="deviceState >= DeviceState.MEDIUM">This is only visible on MEDIUM or larger</p>
  </div>
</template>

.scss

.header {
    display: none;

    @include respond-to(MEDIUM) {
      display: block;
    }
  }

More information on usage can be found here seng-device-state-tracker.

Component structure

A component consists of 4 files:

  • {Name}.vue: This is the main file it contains the imports of the style and logic of the component. Besides the imports it also contains the template(html) of the component.
  • {Name}.js: This is the javascript file that contains all component logic.
  • {Name}.scss: This is the SCSS file that contains all the styling of a component. Vue skeleton uses css modules so all the styling is scoped to the component.
  • index.js: This javascript file is in every component for two reasons:
  1. The index.js makes your component imports shorter import HomePage from 'page/HomePage' instead of import Homepage from 'page/HomePage/HomePage'.

Vuex store modules

It's a best practice to split your data modules in vuex. The store seng-generator templates makes it easy to work with modules as the store templates already contain a static for the namespace.

A module has the following properties:

  • state: The state of the module. Don't use statics for the state because you can nest the state. Use mapState to get values from the state in your component.
  • getters: The getters of the module. Only use getters when you want to transform a value from the state. Use mapGetters to access the getters in your component. Don't create a getter for every value in the state.
  • mutations: The mutations of the module. In a mutation you can change the state. Don't use one god mutation (SET) that can change everything in the state. Use mapMutations to use your mutations in a component.
  • actions: The actions of a module. An action is async in most cases and results in triggering a state change when the action is completed. You should use actions to do a gateway calls instead of doing the gateway call in the component. Don't use actions to just simply trigger mutations use mutations directly instead. Use mapActions to use actions in your components.

Example of setting up a module and using it in a component:

store/module/app/app.js:

// The namespace used to prefix actions, getters and mutations
const namespace = 'app';

// Statics are not grouped in objects. Just export a single string for each static you define
export const SET_DEVICE_STATE = `${namespace}/setDeviceState`;

export default {
  state: {
    deviceState: null,
  },
  getters: {},
  mutations: {
    [SET_DEVICE_STATE](state, deviceState) {
      state.deviceState = deviceState;
    },
  },
  actions: {},
};

Component:

import { mapState, mapGetters } from 'vuex';
import {GET_FULL_NAME, SAVE_USER, SET_FIRST_NAME} from '../../store/module/app/app.js';

// @vue/component
export default {
  name: 'HomePage',
  computed: {
    ...mapState({
      deviceState: state => state.app.deviceState,
    }),
    ...mapGetters({
      fullName: GET_FULL_NAME,
    }),
  },
  methods: {
    ...mapMutations({
      setFirstName: SET_FIRST_NAME
    }),
    ...mapActions({
      saveUser: SAVE_USER
    })
  }
};

VueExposePlugin

Vue skeleton contains a little plugin that makes development faster and easier.

The VueExposePlugin exposes code(enums, functions, classes etc.) in Vue components.

By default it's impossible to use imported code in the templates of components. The VueExposePlugin provides a workaround.

Without the plugin:

<router-link :to="{ name: 'contact', params: {id: 5}}">Contact</router-link>

With the plugin:

<router-link :to="{ name: RouteNames.CONTACT, params: {[Params.ID]: 5}}">Contact</router-link>
<a :href="$config.getURL(URLNames.TERMS)" target="_blank">terms</a>

The VueExposePlugin is registered in the startUp.js. By default it exposes page and config enums and the configmanager instance.

NOTE: VueExposePlugin adds everything on the Vue prototype so watch out for naming conflicts and expose only what is really needed.

Dependency Injection

Some instances of classes need to accessed in multiple places across an application. Using singletons to fix this problem is not a great solution, because it prevents creating multiple instances of the same class. Vue skeleton has a simple solution to fix this problem. Instances are registered with a name in the injector. Using the registered name makes it possible to get the instance from the injector. Before Vue is initialized the setupInjects function is called from the bootstrap.js where injects can be setup. This makes sure that the injects are available before the app/website is initialized.

Example:

// setupInjects.js
import { CONFIG_MANAGER } from 'data/Injectables';
import config from 'config/config';
import ConfigManager from 'seng-config';
import { setValue } from './injector';

const setupInjects = () => {
  const configManager = new ConfigManager();
  configManager.init(config.config, config.environment);

  setValue(CONFIG_MANAGER, configManager);
};

// somewhere else in the application
import { getValue} from 'util/injector';
import { CONFIG_MANAGER } from 'data/Injectables';

const configManager = getValue(CONFIG_MANAGER);
const apiURL = configManager.getURL('api');

It's also possible to setup different injects based on environment, build type etc. This makes it possible to use a stub gateway in development but a real gateway in a production build without changing code.

Axios

The skeleton uses Axios for http requests.

Axios is exposed to all Vue components as $http. In other places axios needs to be imported first before use:

import axios from 'axios';

axios.get('en-gb.json').then((response) => console.log(response.data));

Gateway

The skeleton also comes with an preconfigured Axios based gateway to communicate with the backend.

The gateway is setup in util/setupInjects.js. The api url can be changed in the config. It also has an interceptor setup for easy retrieval of data returned by the gateway. All data that is returned from the gateway is added directly to the response instead of the data property of the response:

// default axios
response.data.data
response.data.pagination
response.data.error.message

// vue skeleton
response.data
response.pagination
response.error.message

The gateway is exposed to all Vue components as $gateway. In other places the injector needs to be used to get the gateway instance:

import { getValue} from 'util/injector';
import { GATEWAY } from 'data/Injectables';

const gateway = getValue(GATEWAY);
gateway.get('/init').then(response => console.log(response.data));

Read the Gateway API spec for more information about gateway response types.

Using SVGs

Webpack automatically parses and optimizes SVGs using SVGo. Vue skeleton comes with predefined SVGo settings which can be found in build/webpack.base.conf.js.

Implementing a icon can be done using the Icon component. The SVG file is referenced using the name property of the Icon component. The value of this property is the SVG filename without the SVG file extension.

<Icon name="zoom-in" class="icon-check" />

The Icon component is globally registered in Vue allowing it to be used directly without importing and registering within Vue components.

Autoprefixer

Autoprefixer is enabled by default. To configure which browser(s) need prefixing adjust the browser list in the /package.json file.

Modernizr

Modernizr is built-in the Vue skeleton. The Modernizr configuration is located in the /.modernizrrc file. Reference the Modernizr Configuration for all options and feature-detects.

Asset management

Managing and working with assets is important. The Vue skeleton comes with a hassle-free solution for managing assets.

Types of assets

  • Static assets
  • Assets that are processed by webpack

Assets that need to be processed by webpack are stored in the src/asset folder. Examples of those assets are fonts, images, SVGs and SCSS files. Referencing these files in .vue templates can be done by prepending ~.

Referencing assets processed by webpack

*.vue template example A image is located in src/asset/image/example.png to reference this image so it can be picked up by webpack use the following syntax <img src="~asset/image/example.png" />

*.scss example A image is located in src/asset/image/example.png to reference this image so it can be picked up by webpack use the following syntax background-image: url(asset/image/example.png);

Referencing static assets

There are two folders for static assets:

  • static This folder is for assets that need to be versioned. Examples: locale JSONs, data JSONs, videos and images. After a build this folder will end up in the root of the versioned folder (by default: version/${timestamp}/static.
  • staticRoot This folder is for assets that don't need to be versioned. Examples: favicon and share images. After a build the content is copied over in a static folder in the root of the build next to the index .html.

static assets won't be processed by webpack (e.g. manually file optimization). It is mandatory to prefix static paths in code using a variable. As stated above the versioned static folder is placed in a versioned folder with a timestamp in the path. It's impossible to know the timestamp during development the only option is to prefix assets.

Luckily it's super easy to prefix paths because Vue skeleton provides all the necessary variables:

process.env.VERSIONED_STATIC_ROOT
process.env.STATIC_ROOT

Prefixing paths using these variables is important not using them can result in unresolvable assets during development/build.

import { getValue } from 'util/injector';
import { CONFIG_MANAGER } from 'data/Injectables';
import { VariableNames } from 'data/enum/configNames';

const configManager = getValue(CONFIG_MANAGER);
const backgroundImage = `${configManager.getVariable(VariableNames.VERSIONED_STATIC_ROOT)}image/background.png`;
const shareImage = `${configManager.getVariable(VariableNames.STATIC_ROOT)}image/share.png`;

The VueExposePlugin sets $versionRoot and $staticRoot variables which allows easy access to version/static root. These variables are only available in a Vue component or Vue template.

Within a component

const video = `${this.$versionRoot}video/intro.mp4`;
const video = `${this.$staticRoot}intro.mp4`;

Within a .vue template

<img :src="`${$versionRoot}image/example.jpg`" />
<img :src="`${$staticRoot}example.jpg`" />

Reference the configuration chapter for more information.

Previewing a build

After creating a new build it is possible to preview it by running the yarn preview command. Due to config differences between development and production it may occur that it runs perfectly fine on development but not in a production build. It is good to test builds on a regular basis to avoid issues when deploying to an environment.

Polyfill configuration

All required polyfills are imported in the src/polyfill/index.js file. Vue skeleton uses babel polyfill in combination with the env babel preset so only required polyfills are included.

By default it includes polyfills for the following features

  • Array.includes
  • Classlist

Localization support

The Vue skeleton is packaged with vue-i18n-manager for localization.

Configuration can be changed in the project config (src/config/config.js) and in the locale config (src/config/localeConfig.js).

In most cases the standard config should be sufficient. The config has the following variables that determine how and if localization is used:

  • VariableNames.LOCALE_ENABLED: Enable/Disable localization
  • VariableNames.LOCALE_ROUTING_ENABLED: Enable/Disable localized routing (/en/home)
  • PropertyNames.DEFAULT_LOCALE: The default locale
  • PropertyNames.AVAILABLE_LOCALES: An array with all available locales

The value of the locales can be an object that i18n-manager accepts or a string. A string value (eg. en-gb) in the config has to match the JSON filename and will also be present in the url if localized routing is enabled.

localeConfig.js contains the config of the i18n manager.

The locales are loaded by the localeLoader (util/localeLoader.js). The i18n-manager uses the localeLoader as a proxy to load the locales. The locale files are loaded from src/data/locale. Change the localeLoader if a custom load implementation is required.

Check the i18nManager documentation for usage within Vue components.

Application startup

Add methods to control/startUp that need to be run before app initialisation. The startUp returns a promise allowing to chain startup tasks.

Examples of startup tasks:

  • Registering/Initialisation of Vue plugins
  • Requesting async initialisation data

Lint staged

Before every commit lint-staged runs to format the staged code with prettier and lints (eslint, tslint, stylelint) it. If lint-staged finds an error the commit will abort. This makes sure that the code in the repository is correctly formatted and lint error free.

It is possible to disable the linting of lint-staged in the config (build-tools/config/index.js).

lintStaged: {
  eslintEnabled: true,
  tslintEnabled: true,
  stylelintEnabled: true,
},

Note: Only disable linting for a valid reason.

Code splitting

It is also possible to use code splitting in Vue Skeleton. This can improve load times if an app consists of a lot of pages for example.

Splitting happens in src\router\routes.js where all the routes are defined. In a normal situation without code splitting all pages are imported at the top of the file. Instead of importing, a page needs to be imported using import().

const HomePage = () => import(/* webpackChunkName: 'HomePage' */ 'page/HomePage').then(page => page.default);

The route definition where the page component is used stays exactly the same.

It's also possible to group multiple pages in a seperate bundle by giving them the same chunk name. In the example above the chunkname is set to HomePage.

More info

Setting up a (api)proxy

The development server runs on node. This can cause problems when a project also has a backend. The backend runs on a different domain/port. Vue skeleton has a proxy functionality built-in this way the backend can be reached in the same way as on the production environment.

Proxy setup is done in the config (config/index.js). The development config contains a proxyTable property where proxies can be added. The development server always needs to be restarted when changes are made.

Example:

proxyTable: {'/api': {target: 'https://localhost/project', changeOrigin: true}},

When a call is made to /api it will be proxied to https://localhost/project/api. The source url (/api in this case) has to exist on the target, because the source url will be added to the target when a call is made.

More info and configuration options

Runtime public path

It's possible to set the public path during runtime. This is handy if a build needs to run on multiple servers with different public paths.

The public path needs to be set in the index.html before the other js files are loaded.

var webpackPublicPath = '/v/vue-skeleton/';

Note: If the public path is updated in the index.html the paths to the css and js also needs to be prepended with the same public path.