A mostly unopinionated MVC framework for Node.js and Vue.js designed for portability.
Note: This is still ES5 to maximize compatibility, I will port it in a few months.
You must have a firebase account, as well as an Auth0 account for managing secured areas.
Stateless utilizes Firebase to manage data related to states, settings, sessions and user specific information. This gives it an element of portability. It is setup with Webpack and Vue for real-time data updates and single file components. Auth0 is also required for secured areas.
On top of being a server library that does most of the heavy lifting, Stateless requires developing in a specific way to hook up controllers, enable webpack, allow for customization. The goal is to make an NPM library that does most of the work but there will always be a template for development.
You might notice the code isn't here yet :) it is coming soon.
var Stateless = require('./stateless'),
site = new Stateless();
You can specify an object, or a path to a JSON file with the site structure.
site.pages(__dirname + '/pages.json');
JSON file example:
[
{
"index": "home",
"title": "Home",
"path": "/",
"nav": true
},
{
"index": "settings",
"title": "Settings",
"path": "/settings",
"nav": true
}
]
This maps all the pages to Vue components, and takes care of Vue-router and express routing. If you have a page with children, a 'pages' property will be available in the page components data object, allowing you to draw navigation for child pages.
Note: this is designed to be a website micro service, so entire sites will use authentication or not.
You must create a service account for your Firebase database. This is for storing session data and updating users in real-time.
Obtain a service account credentials JSON under Permissions -> Service Account and the other values under "project settings" (click "add Firebase to your web app).
var options = {
serviceAccountCredentials: __dirname + '/firebase-credentials.json',
apiKey: 'api-key',
authDomain: 'myaccount-#####.firebaseapp.com',
databaseURL: 'https://myaccount-#####.firebaseio.com'
};
site.firebase(options);
These Firebase rules are required for security:
{
"rules": {
"sessions": {
".read": "false",
".write": "false"
},
"states": {
".read": "auth != null",
".write": "false"
},
"users": {
"$uid": {
"client": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
},
"server": {
".read": "$uid === auth.uid",
".write": "false"
}
}
}
}
}
Auth0 is a great authentication service that is extremely fair priced. You can make it work social network connections, internal database users, and apply things like two-factor authentication or logging in as a user easily. When you setup the client for this site, also add a Firebase addon. Pull out the information from the Firebase JSON credentials file (including the email address) and enter it into Auth0.
var options = {
domain: 'mydomain.auth0.com',
clientId: 'XXXXXXXXXXXXXXXXXXXXXX',
clientSecret: 'XXXXXXXXXXXXXXXXXXXXXXXX_-XXXXXXXXXXXXXXXXXX'
};
site.auth0(options);
If you use Vue reusable components, You must describe them so they get included by webpack.
site.vue('component', ['form', 'line', 'markdown', 'table', 'tabs']);
site.vue('field', ['boolean', 'checkbox', 'email', 'list', 'markdown', 'number', 'password', 'radio', 'select', 'text', 'textarea', 'toggle', 'url', 'date']);
site.vue('cell', ['hidden', 'template', 'field', 'currency', 'boolean', 'number']);
Note: Currently They must be prefixed/categorized. The .vue single file components will be found in the named folders, as well as referenced like this:
<component-line></component-line>
var options = {
host: '127.0.0.1',
port: 8080,
jsPort: 9201, //also package.json "npm run frontend"
sessionSecret: 'chicken',
urlLogout: '/'
};
site.http(options);
The bulk of your business logic and data handling should be contained to simple controllers. They are meant to be accessed on request from the client or used inside the application. Controllers are categorized by a name. Under each name you can chose to have subfolders if you wish. Recommended: Name your controllers after the data model the data belongs to (or the primary query table). This will ensure a good natural organization.
The parameters of a controller is always a linear name->value object. This ensures the best access by code, http/ajax requests and socket requests.
;(function(){
module.exports = function(cb, params){
cb(null, 'hello world');
}
}());
Note: The first variable is always an error object (if applicable).
You can call controllers from others. The controller method is available in every controllers scope:
;(function(){
module.exports = function(cb, params){
this.controller('message', 'hello-world', function(err, message){
cb(null, message + ' ' + params.name);
});
}
}());
You can call controllers from others. The controller method is available in every controllers scope:
site.controller('message');
Stateless strives to be as unopinionated as possible. One way is allowing you to use any libraries you want.
function date(){
var y=new Date().getFullYear().toString();
var m=(new Date().getMonth()+1).toString();
var d=new Date().getDate().toString();
return y+'-'+(m[1]?m:'0'+m[0])+'-'+(d[1]?d:'0'+d[0]);
}
site.scope('date', date);
var ymd = this.date();
Note: This is optional you can also require dependencies inside a controller.
Stateless weirdly is very good at managing states. Everything from site-wide variables to user specific information.
When a server starts it must know how to populate state information to send to Firebase (and every user). You must provide a name of a state, name of a controller, and the path. Optionally you can specify parameters for the controller.
site.stateMap('countries', 'country', 'list');
Now the users will have a list of countries always available to them.
Accessing the (real-time) information in a Vue component is easy. In the <template>
:
<select>
<option v-for="(iso, country) in countries" track-by="$index" :value="iso">{{ country }}</option>
</select>
And in the Vue component <script>
:
export default {
data: function(){return{
countries: Store.countries //reactive
}}
};
Sometimes you might not want to map a state name to a controller. This is how you manually specify the value:
//inside a controller:
this.stateSet('restart', this.datetime());
//in the main app code:
site.stateSet('restart', site.datetime());
You can hook into events for logging, error handling purposes.
- start when the HTTP server starts running
- request when a user requests a path (there should only be 1 per user)
- authenticated when a user logs in or signs up
- error when stateless encounters an error
site.on('err', function(err){
console.log('darn it all ', err.message);
})
Better documentation coming soon
You may want to trigger an event yourself so you have one source for events:
//inside a controller:
this.trigger('error', err);
//in the main app code:
site.trigger('error', err);
Both a backend node express server, and a frontend Webpack server are used while in development. This enables hot-loading Vue components which enables a lot faster development.
For convenience the package.json has two scripts for starting both servers. You can modify then with environment variables or node flags if needed. Note: you will need to use the same jsPort
in both the frontend
script and used when configuring the http server (in the main app.js file).
Running Backend Server
npm run backend
Running Frontend Server
npm run frontend
Now when you save an individual .vue
component file it will quickly load in the browser.
Note: When you add, remove or move files you will need to re-run the frontend server.
Stateless uses Webpack to create a single javascript file (build.js) for everything. Every time you change any .vue file you need to rebuild. Feel free to modify webpack.config.js for your needs (included image sizes, loaders for different parsers, file types). The build file gets gzip compressed when served to the user.
npm run build
A simple logging system has been setup, prefixing the output with date/time and log type information.
//inside a controller:
this.log('message', 'error', data);
//in the main app code:
site.log('message', 'success', data);
###Types of logs
The second parameter can be one of the following:
- default: (=)
- error: (-)
- success: (+)
- unknown: (?)
- event: (*)
- redirect: (~)
- other: (#)
When used the log message will be prefixed with the character in brackets.
- Support for state lists (chat messages)
- Custom events
- User specific states/data
- Supressing logging