Skip to content

Instantly share code, notes, and snippets.

@Skylarity
Last active October 3, 2020 18:08
Show Gist options
  • Save Skylarity/07792aea4ab012a6a55fdaa09bb2cc84 to your computer and use it in GitHub Desktop.
Save Skylarity/07792aea4ab012a6a55fdaa09bb2cc84 to your computer and use it in GitHub Desktop.
Vue project initialization

General Plan

Project spin-up

Vue CLI

We use npx to initialize our project, because it downloads the latest version of the package you're using in order to ensure you're always up-to-date. This process may take several minutes.

npx @vue/cli create my-project

Then, we choose our project settings like so:

Pick a preset

  • Manually select features - Babel, TypeScript, Router, Vuex, CSS Pre-processors, Linter / Formatter, Unit Testing, E2E Testing

Use class-style component syntax?

  • Yes

Use Babel alongside TypeScript

  • Yes

Use history mode for router?

  • Yes

Pick a CSS pre-processor

  • Sass/SCSS (with dart-sass)

Pick a linter / formatter config

  • ESLint + Prettier

Pick additional lint features

  • Lint on save

Pick a unit testing solution

  • Jest

Pick an E2E testing solution

  • Cypress

Where do you prefer placing config for Babel, ESLint, etc.?

  • In dedicated config files

After this, the project will initialize itself.

Then, we install Vuetify.

cd my-project
npx @vue/cli add vuetify

When prompted to choose a preset, select the default.

Configuration Files

Environment Variables

All environment variables must be preceded by "VUE_APP" if you need to access them in your application, like so: VUE_APP_MAPBOX_ACCESS_TOKEN=XXXX-XXXX-XXXX-XXXX

A minimum of three files are necessary:

  • .env - This file contains all variables that do not change across dev/qa/prod - An example could be a Mapbox access token (or other API access token) unless you are using multiple accounts, one for dev, one for prod, etc.
  • .env.development - Should contain only variables needed for development - i.e. VUE_APP_API_ENDPOINT=https://dev.example.com/api
  • .env.production - Should contain only variables needed for production - i.e. VUE_APP_API_ENDPOINT=https://example.com/api

Note in the dev and prod examples I have used the same variable name (VUE_APP_API_ENDPOINT), but changed the value to point to a dev or prod server.

Prettier

The actual configuration is talked about in the next section (Coding Standards).

However it is worth noting that once you have initialized a project you should manually run prettier on the source and test directories in order to reduce merge conflicts from the get-go, like so:

npx prettier --config .prettierrc --write "src/**/*"

Coding Standards

Components

We will use the TypeScript class-component syntax for our Vue components.

All components must be initialized in this way (make sure the language is set to lang="ts"):

import Vue from "vue";
import {Component} from "vue-property-decorator";

@Component
export default class MyComponent extends Vue {}

Types/Interfaces/Enums

Types, interfaces, and enums must live in @/types/MyName.ts and be imported/exported in @/types/index.ts like so:

export {MyName} from "@/types/MyName";

Prettier

Install the Prettier plugin for your editor/IDE of choice.

Prettier will be configured as follows (in a file called .prettierrc):

{
	"tabWidth": 4,
	"useTabs": true,
	"bracketSpacing": false,
	"semi": true,
	"trailingComma": "none",
	"singleQuote": false,
	"arrowParens": "avoid"
}

Proposed Folder Structure

Components

Components will be split into two categories as usual: Views and Components.

Views

Views represent stateful components that act as an independent segment of a site. These can be pages or independent subsections within a page (i.e. a map and a sidebar).

IMPORTANT: These components will live in @/views, and can be categorized into sub-directories if necessary.

Components

Components represent stateless components that are composed together to form Views.

IMPORTANT: These components will live in @/components, and can be categorized into sub-directories if necessary.

Vuex Store

Everything related to the Vuex Store will live inside @/store.

The main store will be composed and exported from @/store/index.ts, and all modules must live in @/store/modules/my-module/ (IMPORTANT: note that this is a directory), and modules shall be broken into multiple files, detailed below in the Vuex Store section.

SCSS

SCSS files will be broken into two categories: global, and local to a component.

Global

Global SCSS files (including the index.scss) shall be located in @/scss.

Partials shall be located in @/scss/partials/_my-partial.scss (IMPORTANT: note the underscore preceding the filename, this tells the SCSS compiler that it is a partial).

Partials shall be imported into @/scss/index.scss, and the index shall not contain any style definitions of its own.

Local to a component

Styles relevant to a single component should be contained in the <style> tag of a single-file component. Make sure that language is set to lang="scss".

Importing variables

In order to globally import all variables into every component, the following shall be added to vue.config.js:

module.exports = {
	// ...
	css: {
		loaderOptions: {
			scss: {
				prependData: `@import "~@/scss/partials/_variables.scss";`
			}
		}
	}
};

Data

All data files shall live in @/assets/data. These files may be broken into sub-directories if necessary.

Other assets (images, videos, audio, etc.)

All media files shall live in @/assets. These files may be broken into sub-directories if necessary.

Helper libraries/functions

All user-written helper libraries and function shall live in @/helpers. These files may be broken into sub-directories if necessary.

All third-party libraries should be installed via npm if possible, or stored in @/helpers/third-party if necessary.

Router organization

All router code shall live in @/router/index.ts.

All routes shall follow this example:

const routes = [
	// ...
	{
		path: "/route-name",
		name: "route-name",
		component: () => import("../views/RouteName.vue")
	}
	// ...
];

Vuex Store Organization

General @/store/index.ts setup

The index serves two purposes:

  1. Importing modules and creating the store.
  2. Exporting namespaces for each module to be used in components.

Several things are going on here (to be explained in the next couple sections), but here is a snapshot of the entire index file:

import Vue from "vue";
import Vuex, {StoreOptions} from "vuex";
import {namespace} from "vuex-class";

import {RootState} from "./types";
import {myModule as moduleMyModule} from "./modules/my-module";

Vue.use(Vuex);

const store: StoreOptions<RootState> = {
	modules: {myModule: moduleMyModule}
};

export const myModule = namespace("myModule");

const vuexStore = new Vuex.Store<RootState>(store);

export default vuexStore;

Importing modules and creating the store

We must import our modules and add them to the global store.

IMPORTANT: When adding a new module to the store you must import and add it here.

Here are the relevant lines:

// ...
import {myModule as moduleMyModule} from "./modules/my-module";

// ...
const store: StoreOptions<RootState> = {
	modules: {myModule: moduleMyModule}
};

// ...
const vuexStore = new Vuex.Store<RootState>(store);

export default vuexStore;

IMPORTANT: The reason we import myModule as moduleMyModule is so that we can still export myModule later as a namespace and not run into any name collisions. We must always import our modules in this way!


Exporting namespaces for each module to be used in components.

We export namespaces in order to easily use our store Getters/Mutations/Actions inside our components. This drastically reduces the amount of boilerplate we need to import and set up within each component.

IMPORTANT: You must export a namespace when adding a new module to the store.

By exporting namespaces like so:

import {namespace} from "vuex-class";

// ...
export const myModule = namespace("myModule");

We can use our getters, mutations, and actions in our components like so:

// ...
import {myModule} from "@/store";
import {MyType} from "@/types";

// ...
@Component
export default class MyComponent extends Vue {
	@myModule.Getter myProperty!: MyType;
	@myModule.Mutation setMyProperty!: (newProp: MyType) => {};
	@myModule.Action loadMyProperty!: () => Promise<MyType>;

	// ...

	// You can then use these like this:
	this.myProperty;  // Getter

	this.setMyProperty("something");  // Mutation

	this.loadMyProperty()  // Action
		.then((response: MyType) => {
			// ...
		});
}

IMPORTANT: The names of our getters, mutations, and actions must reflect their names in the store.

Modules

Modules will be split into several files both for organizational sake as well as to avoid git merge conflicts.

They will be implemented as follows:

index.ts

This file collects the various parts of the module and exports them:

import {Module} from "vuex";

import {RootState} from "@/store/types";
import {MyModuleState} from "./types";
import {getters} from "./getters";
import {mutations} from "./mutations";
import {actions} from "./actions";

const namespaced = true; // Always namespace modules

export const state: MyModuleState = {
	myProperty: null
};

export const myModule: Module<MyModuleState, RootState> = {
	namespaced,
	state,
	getters,
	mutations,
	actions
};

getters.ts

This file contains the getters for the module.

import {GetterTree} from "vuex";
import {RootState} from "@/store/types";
import {MyModuleState} from "./types";
import {MyType} from "@/types";

export const getters: GetterTree<MyModuleState, RootState> = {
	myProperty: (state): MyType | null => state.myProperty
	// ...
};

mutations.ts

This file contains the mutations for the module.

import {MutationTree} from "vuex";
import {MyModuleState} from "./types";
import {MyType} from "@/types";

export const mutations: MutationTree<MyModuleState> = {
	setMyProperty(state, newMyProperty: MyType | null) {
		state.myProperty = newMyProperty;
	}
	// ...
};

actions.ts

This file contains the actions for the module.

This example also contains code for making an API call with Axios.

import {ActionTree} from "vuex";

import {axios} from "@/helpers/axios";
import {AxiosResponse, AxiosError} from "axios";

import {RootState} from "@/store/types";
import {MyModuleState} from "./types";
import {MyType, MyPropertyCallConfig} from "@/types";

export const actions: ActionTree<MyModuleState, RootState> = {
	loadMyProperty({commit}: {commit: any}, options?: MyPropertyCallConfig) {
		return new Promise((resolve, reject) => {
			axios
				.get("/some-api", {data: config})
				.then((response: AxiosResponse<MyType>) => {
					const myProperty: MyType = response.data;

					commit("setMyProperty", myProperty);
					resolve(myProperty);
				})
				.catch((err: AxiosError) => {
					reject(err);
				});
		});
	}
	// ...
};

types.ts

This includes the types necessary for the module's state:

import {MyType} from "@/types";

export interface MyModuleState {
	myProperty: MyType | null;
}

Testing

Unit Testing

For unit testing we will be using the Jest library.

Jest Configuration

The Jest config file lives at jest.config.js, but should typically not need to be changed.

File structure and naming

All unit tests will live in tests/unit.

This contest of this directory should roughly mirror the contents of @/views and @/components. Each file should live in tests/unit/views|components/component-name/ComponentName.spec.ts.

IMPORTANT: Note the .spec portion of the filename — this is necessary for Jest to locate each testing file.

End-to-end (UI) Testing

For e2e testing we will be using the Cypress framework.

Cypress Configuration

The Cypress config file lives at cypress.json, but should typically not need to be changed.

File structure and naming

All e2e tests will live in tests/e2e/spec.

Each test should be clearly named as to its function, e.g. tests/e2e/spec/add-facility-to-map.ts.

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