User:Saul/Realtime app

From Organic Design wiki

This is the documentation for the stack I use for creating a real-time application. You can find the repository to download a simple general boilerplate here. The stack consists of node.js and feathers.js for the server, the client uses: Vue, vue-router, vuex and setup using the vue webpack. The repository for the boilerplate with this stack can be found here: vue-boilerplate

Setup

Install Cli Libs

This globally installs the feathers and vue cli's onto the system so if you have already done this before you can skip this step.

vue

sudo npm install -g vue

feathers

sudo npm i -g @feathersjs/cli

Generate Files

The following commands will setup the app:

mkdir APPNAME APPNAME/server APPNAME/client && cd APPNAME # prepare file structure
sudo vue init webpack-simple APPNAME # create a new project using the "webpack-simple" template
cd server
feathers generate app # generate the feathers app

Modify Files

A few changes are need to finish connecting the app:

Copy all dependencies from server/package.json to the package.json located in the root dir. Do the same for request and request-promise from devDependencies.

Add to the scripts section in package.json (this allows the back end to be started via "npm run server"):

"server": "node server/src/",

Add to package.json:

"directories": {
	"lib": "server/src"
},

Modify the dev script in package.json to:

"cross-env NODE_ENV=development webpack-dev-server --content-base server/public --open --hot"

Modify the config/default.json file like so:

"public": "../server/public/",

Modify the webpack.config.js by changing the path and publicPath like so:

path: path.resolve(__dirname, "server", "public"),
publicPath: "/",

Remove the line in webpack.config.js which makes babel-loader exclude the node module folders.
This appears to make build crash for some reason, perhaps feathers.js is written in es6, when it should be written in es5?

// modify this section like so:
test: /\.js$/,
loader: 'babel-loader'/*,
exclude: /node_modules/*/

Modify the index.html so that the script build source is:

<script src="/build.js"></script>

Fix Structure, Install, And Cleanup

These commands will finish arranging the files (make sure you are still in the server dir):

mv config ../config && mv .editorconfig ../.editorconfig && mv ../index.html public/index.html # move then files to the proper dirs
rm package.json LICENSE .gitignore .npmignore README.md .eslintrc.json package-lock.json -R test node_modules # delete the unnecessary files

And to install the dependencies and run:

cd .. # go back to the project root
npm i @feathersjs/client vuex vue-router # install additional dependencies
npm i # install dependencies

Run App

To run in development:

npm run dev # this launches the front-end
npm run server # this launches the back-end

To run in production:

npm build # build the front-end into the backend
npm run server # run the backend

See Also

Client

File Structure

To generate the recommended file structure, navigate to the project root directory and run the following command:

mkdir src/components src/store src/libs src/pages # generate the client end folders: components (for storing vue components), store (for storing vuex store files), and libs (for storing client side js libs)

Auto

To automatically edit the files run this one-liner or skip to manual if you would like to manually edit them. The code does the following:

  • generates the src/store/index.js file with an example included.
  • generates the src/router.js file with examples
  • generates the src/libs/conf.js to use the configuration settings
  • generates the src/libs/client.js to setup feathers with socket.io
  • rewrites App.vue to use the vue-router
  • rewrites main.js to use the vue-router and vuex
echo -e "import Vue from \"vue\"\nimport Vuex from \"vuex\"\n//import chat from \"./SOTRENAMEONE\"\n//import login from \"./SOTRENAMETWO\"\n\nVue.use(Vuex)\n\nexport default new Vuex.Store({\n\tmodules: {\n\t\t/*\n\t\t\tchat,\n\t\t\tlogin\n\t\t*/\n\t}\n})" > src/store/index.js && echo -e "import Vue from \"vue\"\nimport Router from \"vue-router\"\n//import Login from \"./pages/Login.vue\"\n//import Register from \"./pages/Register.vue\"\n//import Chat from \"./pages/Chat.vue\"\n//import auth from \"./libs/auth\"\n\nVue.use(Router)\n\nconst router = new Router({\n\tmode: \"history\",\n\troutes: [\n\t\t/*\n\t\t\t{\n\t\t\t\tpath: \"/\",\n\t\t\t\tname: \"Chat\",\n\t\t\t\tcomponent: Chat,\n\t\t\t\tbeforeEnter: auth.isLoggedIn\n\t\t\t},\n\t\t\t{\n\t\t\t\tpath: \"/login\",\n\t\t\t\tname: \"Login\",\n\t\t\t\tcomponent: Login\n\t\t\t},\n\t\t\t{\n\t\t\t\tpath: \"/register\",\n\t\t\t\tname: \"Register\",\n\t\t\t\tcomponent: Register\n\t\t\t},\n\t\t*/\n\t\t// If access a non-existing route, redirect to root route\n\t\t{\n\t\t\tpath: \"*\",\n\t\t\tredirect: \"/\"\n\t\t}\n\t]\n})\n\nrouter.afterEach(function (to, from){\n\t//Set the title for the pages\n\tdocument.title = to.name;\n});\n\nexport default router;" > src/router.js && echo -e "<template>\n\t<div id=\"app\">\n\t\t<router-view></router-view>\n\t</div>\n</template>\n\n<script>\n\texport default {\n\t\tname: \"app\"\n\t}\n</script>\n\n<style>\n\t#app, body, html{\n\t\tbox-sizing: border-box;\n\t\tpadding: 0px;\n\t\tmargin: 0px;\n\t}\n</style>" > src/App.vue && echo -e "import Vue from \"vue\"\nimport App from \"./App.vue\"\nimport router from \"./router\"\nimport store from \"./store\"\n\nnew Vue({\n\tel: \"#app\",\n\tstore,\n\trouter,\n\trender: h => h(App)\n})" > src/main.js && echo -e "import defaultConf from \"../../config/default.json\"\nimport productionConf from \"../../config/production.json\"\nimport merge from \"lodash.merge\"\n\nlet conf = merge({}, defaultConf)\n\n// When we build for production, merge production conf\nif (process.env.NODE_ENV === \"production\") {\n\tconf = merge(conf, productionConf)\n}\n\nexport default conf" > src/libs/conf.js && echo -e "import io from \"socket.io-client\";\nimport feathers from \"@feathersjs/feathers\";\nimport socketio from \"@feathersjs/socketio-client\";\nimport auth from \"@feathersjs/authentication-client\";\n\nimport conf from \"./conf\";\n\nconst client = feathers();\n\nconst socket = io(\`http://\${conf.host}:\${conf.port}\`, {\n\ttransports: [\"websocket\"],\n\tforceNew: true\n});\n\nclient.configure(auth({ storage: localStorage }));\n// Create the Feathers application with a \`socketio\` connection\nclient.configure(socketio(socket));\n\nexport default client;" > src/libs/client.js

Manual

This section does the same thing manually as Auto does.

First create the src/store/index.js file like this:

import Vue from "vue"
import Vuex from "vuex"
//import chat from "./SOTRENAMEONE"
//import login from "./SOTRENAMETWO"

Vue.use(Vuex)

export default new Vuex.Store({
	modules: {
		/*
			chat,
			login
		*/
	}
})

Then create the src/router.js file with the following:

import Vue from "vue"
import Router from "vue-router"
//import Login from "./pages/Login.vue"
//import Register from "./pages/Register.vue"
//import Chat from "./pages/Chat.vue"
//import auth from "./libs/auth"

Vue.use(Router)

const router = new Router({
	mode: "history",
	routes: [
		/*
			{
				path: "/",
				name: "Chat",
				component: Chat,
				beforeEnter: auth.isLoggedIn
			},
			{
				path: "/login",
				name: "Login",
				component: Login
			},
			{
				path: "/register",
				name: "Register",
				component: Register
			},
		*/
		// If access a non-existing route, redirect to root route
		{
			path: "*",
			redirect: "/"
		}
	]
})

router.afterEach(function (to, from){
	//Set the title for the pages
	document.title = to.name;
});

export default router;

Then change the contents of App.vue like so:

<template>
	<div id="app">
		<router-view></router-view>
	</div>
</template>

<script>
	export default {
		name: "app"
	}
</script>

<style>
	#app, body, html{
		box-sizing: border-box;
		padding: 0px;
		margin: 0px;
	}
</style>

Change the contents main.js to use the vue-router and vuex like this:

import Vue from "vue"
import App from "./App.vue"
import router from "./router"
import store from "./store"

new Vue({
	el: "#app",
	store,
	router,
	render: h => h(App)
})

Create the client conf data lib as src/libs/conf.js:

import defaultConf from "../../config/default.json"
import productionConf from "../../config/production.json"
import merge from "lodash.merge"

let conf = merge({}, defaultConf)

// When we build for production, merge production conf
if (process.env.NODE_ENV === "production") {
  conf = merge(conf, productionConf)
}

export default conf

Finally create the feathers lib as src/libs/client.js:

import io from "socket.io-client";
import feathers from "@feathersjs/feathers";
import socketio from "@feathersjs/socketio-client";
import auth from "@feathersjs/authentication-client";

import conf from "./conf";

const client = feathers();

const socket = io(`http://${conf.host}:${conf.port}`, {
	transports: ["websocket"],
	forceNew: true
});

client.configure(auth({ storage: localStorage }));
// Create the Feathers application with a `socketio` connection
client.configure(socketio(socket));

export default client;

Creating Pages

For every page you would like to make create a file in src/pages/PAGENAME.vue and then add it in the src/router.js file.
Each page should have the standard vue template:

<template>
</template>

<script>
</script>

<style>
</style>

Creating Components

Components are independent pieces of code to be embedded into a page.
For every component you would like to make create a file in src/component/COMPONENTNAME.vue
Each component should have the standard vue template (like pages do).
To embed the components in a page you need to import them in the page (in the script section):

import Chat from './components/chat.vue'
import Channels from './components/channels.vue'

export default{
	components: {
		Channels,
		Chat
	}
}

Then you can use them in the template section like so:

<channels></channels>
<chat></chat>

Creating Vuex Stores

Vuex stores are ways of preserving state.
To create a vuex store for your application create the folder src/store/STORENAME
Create a index.js file in the src/store/STORENAME folder. The index.js file should contain something like:

export const state = {
	// This should contain variables that you would like to preserve
}

export const mutations = {
	// This should contain methods for modifying the state section but should not be called directly
}

export const actions = {
	// This should contain methods that do not modify the state directly
	// They may however modify the stat by calling a method in the mutations
	// This may be done via: commit("mutation_name", channel)
}

export const getters = {
	// This should contain methods for retrieving the data from the state
}

export default {
  state,
  mutations,
  actions,
  getters
}

Then you need to modify the src/store/index.js to reference it.
Then you can import it into your pages and use the data.

Using JS Librarys

To add a JS library just create the file src/libs/LIBNAME.js
Add the code into that file and import it into the relevant files.
You may like to add an auth.js file to handle authentication and loggin in and out.
Another file that you probably will need is a services file for handling the service calls to the server eg. creating a message.

Listening To Server events

The server by default will publish all events to all authenticated users by adding them to the "authenticated" channel and publishing all events across that channel.
So to listen to an event eg. when a message is created, make sure to import the client and create the listener like so:

import client from '../libs/client'

export default {
	created(){
		client.service("messages").on("created", message => {
			// Do something
		});
	}
}

Server

Routing

Some modifications need to be made so the server understands how to deliver content.
If you try to go to localhost:3030/somepagename you will get a 404 error.
So to fix this add below the public folder host route the following code:

// This line already exists:
app.use('/', express.static(app.get('public')));
// Add these 3 lines:
app.get('/*',  function(req,res){
	res.sendFile(path.join(app.get('public'), 'index.html'));
});

This will make all routes return the index.html file but keep the route for vue to handle.

Services

Services are used as a system of communication between the client server and database.
Services set most of the code automatically so you won't need to do much
To generate a feathers js service run:

feathers generate service # Creates the feathers.js service
# Example: Mongoose, messages, /messages, mongodb://localhost:27017/chat

If you have issues with a service check what the service name is.
Most of what will need editing is in the service's hooks.js file, and and the corresponding model file.
The model file may need changing to add or remove fields to require the data you want the database to store.
The hooks file may need some hook functions created to modify data eg. change the date and times into human readable format
To generate the authentication service run:

feathers generate authentication # Creates the feathers.js authentication service

Authentication automatically creates the necessary hooks for protecting users passwords.
The authentication will also create the channels.js which sets up publishing for channels.