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.
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.
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/**/*"
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, and enums must live in @/types/MyName.ts
and be imported/exported in @/types/index.ts
like so:
export {MyName} from "@/types/MyName";
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"
}
Components will be split into two categories as usual: Views and Components.
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 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.
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 files will be broken into two categories: global, and local to a component.
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.
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"
.
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";`
}
}
}
};
All data files shall live in @/assets/data
. These files may be broken into sub-directories if necessary.
All media files shall live in @/assets
. These files may be broken into sub-directories if necessary.
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.
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")
}
// ...
];
The index serves two purposes:
- Importing modules and creating the store.
- 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;
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!
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 will be split into several files both for organizational sake as well as to avoid git merge conflicts.
They will be implemented as follows:
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
};
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
// ...
};
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;
}
// ...
};
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);
});
});
}
// ...
};
This includes the types necessary for the module's state:
import {MyType} from "@/types";
export interface MyModuleState {
myProperty: MyType | null;
}
For unit testing we will be using the Jest library.
The Jest config file lives at jest.config.js
, but should typically not need to be changed.
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.
For e2e testing we will be using the Cypress framework.
The Cypress config file lives at cypress.json
, but should typically not need to be changed.
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
.