What is the best Vue-Router practice for very large webapplications? - laravel

I have to make a webapplication with many different modules (like a todo-module, document-modules, and a big usermanagement-module for admin users). The total number of pages is > 100. And the module access is different for each user.
I am working with Laravel and Vue-router.
But what is the best practice to do it?
Create a SPA-application, with 1 large vue-router for everything?
For every module a own single "SPA" (with and own vue-router)?
Or another suggestion...?

Little late but I will try to answer the question. This is more an architectural question than just routing level question.
TLDR: You will need a mix of approaches. One approach won't fit.
1. Routing Mode
First, you should determine if you are going with HTML 5 history mode or hash mode
It is 2018, and I definitely recommend that you use HTML5 history mode.
If you use history mode, then it means your client-side router will need to work in sync with your server-side router.
2. Micro-frontends
I am not sure if you know this, but micro-frontends is the term you are looking for. Basically, it is your first line of segregation. You should split your app into multiple smaller apps. Each app will have its root component, router, models, services, etc. You will share many of the components (Of course, word very large application is important. And I literally mean it.)
3. Mono-repo considerations
If you have chosen to go ahead with Micro-frontends, then you might consider mono-repo setup
using Lerna or Builder.
4. Routing Module - Initialization
Irrespective of micro-apps, your app should have one starting point - main.js or index.js. In this file, you should initialize all your singleton things. Main singleton entities in a typical Vue.js app are Root Component, Data Store, Router, etc.
Your routing module will be separate from any component. Import routing module in this entry file and initialize it here.
5. Routing Module - Implementation
Routing module should be further split into smaller modules. Use simple functions and ES modules to do that. Each function will be responsible for returning a RouteConfig object. This is how it will look:
const route: RouteConfig = {
path: '/some-path',
component: AppComponent,
children: [
getWelcomeRoute(),
getDashboardRoute()
]
};
function getWelcomeRoute(): RouteConfig {
return {
name: ROUTE_WELCOME,
path: '',
component: WelcomeComponent
};
}
At route level, you should consider doing lazy loading of the modules. This will save many bytes from loading upfront:
function getLazyWelcomeRoute(): RouteConfig {
// IMPORTANT for lazy loading
const LazyWelcomeComponent = () => import('views/WelcomeComponent.vue');
return {
name: ROUTE_WELCOME,
path: '',
component: LazyWelcomeComponent
};
}
You cannot do this unless you use bundler like Webpack or Rollup.
5. Routing Module - Guard
This is very important
Guards are where you should handle your authorization. With Vue.js, it is possible to write component level route guard. But my suggestion is to refrain from doing so. Do it only when absolutely necessary. It is basically a separation of concern. Your routing module should possess the knowledge of authorization of your app. And technically, authorization exists/applies to a route than a component. That is the reason, why we created routing as a separate module altogether.
I am assuming that you are using some sort of data store like Redux or Vuex for the very large application. If you are going to write route level guards, then you will need to consult with data from Redux/Vuex store to take authorization decisions. It means you need to inject store into routing module. The simplest way to do that is to wrap your router initialization into a function like this:
export function makeRouter(store: Store<AppState>): VueRouter {
// Now you can access store here
return new VueRouter({
mode: 'history',
routes: [
getWelcomeRoute(store),
]
});
}
Now you can simply call this function from your entry-point file.
6. Routing Module - Default route
Remember to define a default catch-all route to show generic/intelligent 404 message to your users.
7. Routing Module - Routing data
Since we are really talking about very large application, it is better to avoid direct access to a router within your component. Instead, keep your router data in sync with your data store like vuex-router-sync
. You will save the painful amount of bugs by doing this.
8. Routing Module - Side effects
You will often use $router.replace() or $router.push() within your components. From a component perspective, it is a side effect. Instead, handle programmatic router navigation outside of your component. Create a central place for all router navigation. Dispatch a request/action to this external entity to handle these side effects for you. TLDR; Don't do routing side effect directly within your components. It will make your components SOLID and easy to test. In our case, we use redux-observable to handle routing side effects.
I hope this covers all the aspects of routing for a very large scale application.

If you go with SPA app, please make sure you are using these practices:
Lazy loading/async components. We usually do lazy loading for static assets. In the same spirit, we only need to load the components when the routes are visited. Vue will only trigger the factory function when the component needs to be rendered and will cache the result for future re-renders.
import MainPage from './routes/MainPage.vue'
const Page1 = r => require.ensure([], () => r(require('./routes/Page1.vue')))
const routes = [
{ path: '/main', component: MainPage },
{ path: '/page1', component: Page1 }
]
Combining routes: (related to above) For example, in the following case, 2 routes are output in the same chunk and bundle, causing that bundle to be lazy-loaded when either route is accessed.
const Page1 = r => require.ensure([], () => r(require('./routes/Page1.vue')), 'big-pages')
const Page2 = r => require.ensure([], () => r(require('./routes/Page2.vue')), 'big-pages')

Nuxt can help you with that. It generates your folder Structur dynamically to a router config file. Have a look at https://nuxtjs.org/guide/routing
It has even more help functions than routing. But especially for large applications in general an idea to set on nuxt

Related

GatsbyJS Service Worker configuration doesn't respect "networkFirst", keeps serving stale data

I have a GatsbyJS website that I'm deploying to Netlify. Whenever I navigate to my website, the Service Worker serves me a stale version of the website and sends a request in the background so that I get a fresher version next time. I don't think it's acceptable that a user who goes to my site sees a version that may be several days old. I want the Service Worker to fetch a fresh version as long as network is available, and serve the stale version only in offline mode. I'm unable to find any documentation on how to make that happen.
Based on this GatsbyJS doc and this Workbox doc I figured that it should work to change the strategy from staleWhileRevalidate to networkFirst. They don't provide a complete example anywhere, so I had to guess the syntax, and it looks like my guess was not correct. Can anyone provide a complete example of how to configure gatsby-plugin-offline to achieve reasonable behavior?
This is a quite a long answer so I've split it into 3 parts.
The problem
Gatsby's offline plugin uses Google's workbox under the hood to do both pre-caching and runtime caching. Workbox has different runtime caching strategies provided by the workbox-strategies package. These include stale-while-revalidate, cache first (cache falling back to network) and network first (network falling back to cache).
gatsby-plugin-offline sets up appropriate caching strategies for different URLs. For example:
CSS, JS and files in the static directory use CacheFirst as they have hashed, unique URLS and can be safely served from cache.
page-data.json files use StaleWhileRevalidate because they don't have hashed URLS.
Note: the Gatsby docs for gatsby-plugin-offline (as of May 2020) says that page-data.json files use NetworkFirst but they actually use StaleWhileRevalidate.
Files that don't have specific handlers set up in gatsby-plugin-offline (e.g. HTML files for first load) use a cache first policy. The plugin uses generateSW from workbox-build under the hood. Note: I couldn't actually find reference to this in the docs, just the comment I linked to but I did experiment and HTML pages are definitely cached by the service worker, despite not being in the the runtimeCaching list.
So I think your problem of users seeing stale pages is because the HTML on first load and the page-data.json files are both served from the cache by the service worker.
Should you fix it
There's a trade off between using NetworkFirst and CacheFirst (or StaleWhileRevalidate). NetworkFirst is optimised for accurate data and the slight expense of speed. Whereas CacheFirst and StaleWhileRevalidate serve straight from cache so are optimised for performance at the expense of having the most up-to-date data. Both have the benefit of resilience in case of a network outage.
So it probably depends per use-case based on things like type of website, content and audience. For example:
a blog with content that doesn't get updated often could probably safely use StaleWhileRevalidate, or even CacheFirst for individual posts.
A website where you know the majority of your uses are using desktop computers over fast, wired, reliable internet connections, means using NetworkFirst is probably appropriate.
A website showing something time-sensitive where you'd prefer to see the latest content would make sense to use NetworkFirst.
How to fix it
I think there are 2 main ways to solve this: refresh the page when updates are available, or change the caching policy to be NetworkFirst.
Refreshing the page
Gatsby provides a onServiceWorkerUpdateReady hook for this purpose. So you could leave the default cache first behaviour as-is but use this hook to refresh the page.
The simplest way is to reload the page when there's an update in the service worker:
// gatsby-browser.js
export const onServiceWorkerUpdateReady = () => window.location.reload(true);
However, this can be intrusive and un-prompted so isn't necessarily a great user experience. The alternative is to prompt the user to update. This is what the Gatsby docs suggests:
// gatsby-browser.js
export const onServiceWorkerUpdateReady = () => {
const answer = window.confirm(
`This application has been updated. ` +
`Reload to display the latest version?`
)
if (answer === true) {
window.location.reload()
}
}
If you're not a fan of the native browser prompt (I'm not!) then a third option is to prompt the user via some custom UI. Something like:
// gatsby-browser.js
import React from "react";
import ReactDOM from "react-dom";
export const onServiceWorkerUpdateReady = () => {
const root = document.body.appendChild(document.createElement("div"));
ReactDOM.render(
<div>
<p>
Acme has been updated in the background.
<br />
Refresh to see the latest version.
</p>
<button onClick={() => window.location.reload(true)}>
Refresh
</button>
<button onClick={() => document.body.removeChild(root)}>
Close
</button>
</div>,
root
);
};
Note: if you're going down this route and implementing as a dialog then please make sure it's accessible.
Use NetworkFirst
I think this is what you're after as it answers your "I want the Service Worker to fetch a fresh version as long as network is available".
Override the workbox config with gatsby-plugin-offline to change the runtime caching policies to use NetworkFirst appropriately. Use the runtimeCaching property to do something like:
// gatsby-config.js
module.exports = {
plugins: [
{
resolve: `gatsby-plugin-offline`,
options: {
workboxConfig: {
runtimeCaching: [
{
urlPattern: /(\.js$|\.css$|static\/)/,
handler: `CacheFirst`,
},
{
urlPattern: /^https?:.*\/page-data\/.*\/(page-data|app-data)\.json$/,
handler: `NetworkFirst`,
options: {
networkTimeoutSeconds: 1,
},
},
{
urlPattern: /^https?:.*\.(png|jpg|jpeg|webp|svg|gif|tiff|js|woff|woff2|json|css)$/,
handler: `StaleWhileRevalidate`,
},
{
urlPattern: /^https?:\/\/fonts\.googleapis\.com\/css/,
handler: `StaleWhileRevalidate`,
},
{
urlPattern: /\/$/,
handler: `NetworkFirst`,
options: {
networkTimeoutSeconds: 1,
},
},
],
},
},
},
]
};
This is the default runtimeCaching that gatsby-plugin-offline uses with 2 key changes: the following rules are both using NetworkFirst:
page-data.json files
HTML routes (that end in a /) - I've assumed your HTML pages all end with a / and if they don't you can use something like gatsby-plugin-force-trailing-slashes.

Is there a name for this UI deign pattern? A group of views that are related by state and hierarchical?

Example: I have an SPA Angular app; but this might apply to everything from MVC to Winforms..
There is a sale process(could be another related process, just an example) which includes UI views for:
Choose OR Create Customer
Add Items to Work Order
Cash Out Sale
Traits are:
These are conceptually related.
You have to access them in some order. Cannot start at 2+ for example.
State is persisted in this "flow(don't know better word)"
I am confronting things like, state between UI views, etc and would like to know how to describe the pattern.
Edit: 2
I have significantly edited the title and contents to not seem to apply to Angular Framework.
Do you know about 'Auth Guards' in angular 2?
You can easily handle user directly inputting URL.
Detailed explanation of 'Auth Guard' is here.
Above explanation only works with angular 2+.
You could migrate these three components into their own Module which you could import into your AppModule. Also, for this new Module you could create a custom routing module ng g m sales --routing true where you could do modify the routes array to do something like this.
const routes: Routes = [{
path: 'sales',
children: [{
path: 'step-1',
component: ComponentOne
}, {
path: 'step-2',
component: ComponentTwo
}, {
path: 'step-3',
component: ComponentThree
}]
}];
#NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SalesRoutingModule { }
I wouldn't say that there is a name to this pattern per se, however with the application of a concept called LazyLoading this is considered a requirement. And the concept of putting related components/routes into their own module is described in the Angular Style Guide (if you would like to look into that)
The style guide does advise a methodology called LIFT which to me seems fairy generic to any web development. It's just a slew of best practices for your coding structure.

Routing Conventions in Can.js

So I’m looking to make some routes within my super cool can.js application. Aiming for something like this…
#!claims ClaimsController - lists claims
#!claims/:id ClaimController - views a single claim
#!claims/new ClaimController - creates a new claim
#!claims/:id/pdf - do nothing, the ClaimController will handle it
#!admin AdminController - loads my Administrative panel with menu
#!admin/users - do nothing, the AdminController will handle it
#!admin/settings - do nothing, the AdminController will handle it
So how might we do this?
“claims route”: function() { load('ClaimsController'); },
“claims/:id route”: function() { load('ClaimController'); },
“admin”: function() { load(‘AdminController’); },
Cool beans, we’re off. So what if someone sends a link to someone like...
http://myapp#!claims/1/pdf
Nothing happens! Ok, well let’s add the route.
“claims/:id/pdf route”: function() { load('ClaimController'); },
Great. Now that link works. Here, the router’s job is only to load the controller. The controller will recognize that the pdf action is wanted, and show the correct view.
So pretend I’ve loaded up a claim claims/:id and I edit one or two things. Then I click the Print Preview button to view the PDF and change my route to claims/:id/pdf.
What should happen… the Claim Controller is watching the route and shows the pdf view.
What actually happens… the router sees the change, matches the claims/:id/pdf route we added, and reloads the Claim Controller, displaying a fresh version of the claim pulled from the server/cache, losing my changes.
To try and define the problem, I need the router to identify when the route changes, what controller the route belongs to, and if the controller is already loaded, ignore it. But this is hard!
claims //
claims/:id // different controllers!
claims/:id //
claims/:id/pdf // same controller!
We could just bind on the "controller" change. So defining routes like can.route(':controller') and binding on :controller.
{can.route} controller
// or
can.route.bind('controller', function() {...})
But clicking on a claim (changing from ClaimsController to ClaimController) won't trigger, as the first token claim is the same in both cases.
Is there a convention I can lean on? Should I be specifying every single route in the app and checking if the controller is loaded? Are my preferred route urls just not working?
The following is how I setup routing in complex CanJS applications. You can see an example of this here.
First, do not use can.Control routes. It's an anti-pattern and will be removed in 3.0 for something like the ideas in this issue.
Instead you setup a routing app module that imports and sets up modules by convention similar to this which is used here.
I will explain how to setup a routing app module in a moment. But first, it's important to understand how can.route is different from how you are probably used to thinking of routing. Its difference makes it difficult to understand at first, but once you get it; you'll hopefully see how powerful and perfect it is for client-side routing.
Instead of thinking of urls, think of can.route's data. What is in can.route.attr(). For example, your URLs seem to have data like:
page - the primary area someone is dealing with
subpage - an optional secondary area within the page
id - the id of a type
For example, admin/users might want can.route.attr() to return:
{page: "admin", subpage: "users"}
And, claims/5 might translate into:
{page: "claims", id: "5"}
When I start building an application, I only use urls that look like #!page=admin&subpage=users and ignore the pretty routing until later. I build an application around state first and foremost.
Once I have the mental picture of the can.route.attr() data that encapsulates my application's state, I build a routing app module that listens to changes in can.route and sets up the right controls or components. Yours might look like:
can.route.bind("change", throttle(function(){
if( can.route.attr("page") == "admin" ) {
load("AdminController")
} else if(can.route.attr("page") === "claims" && can.route.attr("id") {
load("ClaimController")
} else if ( ... ) {
...
} else {
// by convention, load a controller for whatever page is
load(can.capitalize(can.route.attr("page")+"Controller")
}
}) );
Finally, after setting all of that up, I make my pretty routes map to my expected can.route.attr() values:
can.route(":page"); // for #!claims, #!admin
can.route("claims/new", {page: "claims", subpage: "new"});
can.route("claims/:id", {page: "claims"});
can.route("admin/:subpage",{page: "admin"});
By doing it this way, you keep your routes independent of rest of the application. Everything simply listens to changes in can.route's attributes. All your routing rules are maintained in one place.

Node.js, restify and proper routing

I'm still wrapping my head around Node, but I have a very simple question. I see a lot of node examples where people are declaring their routes and all their logic in a single app.js file (or sometimes splitting them off into subfiles).
My question is basically: is it better to keep all your route declarations in the app or bootstrap a generic route that maps to your file structure. This may seem like a primitive question but my goal is to grasp what's most efficient within node.
I'm currently building an API handler with Restify but I have another app that uses Express (so this question will likely answer both questions).
In my route I can either declare a single route bootstrap like so:
app.all('/api/:package/:controller', function(request, response) {
var controller = require(
'../' + request.params.package + '/api/' + request.params.controller
);
controller.index(request, response);
response.end();
});
This basically accepts all calls from the API and targets the proper api controller. Alternatively I can declare each route individually or perhaps even write an loop that goes through each of my controllers and declares them on init. So:
for (var i in packages.controllers) {
app.all('api/' + package + '/' + controllers[i].name, function(request, response) {
var controller = require(
'../' + request.params.package + '/api/' + request.params.controller
);
controller.index(request, response);
}
}
packages.controllers being an array of all possible controllers. Note the above code is not accurate, I have an HMVC folder structure so the code is a bit more complicated than the above. But you get the point.
I'm wondering what the consequences of either are and if it really matters at all?
Thanks!
I would not recommend a single app.js at all. You will end up with a 5,000+ line file which is a nightmare to maintain.
The largest issue I see with your snippet is that even though require() gets cached, it has to perform a synchronous IO request. It's just a bad habit to get into.
Similar to what Don recommends, I have had the best luck splitting out routes into modules which export a single function which accept an instance of the app. You can think of it as "decorating" the app instance:
// app.js
var app = express.createServer();
app.configure(function(){ //... });
require('./foo')(app);
// foo.js
exports = module.exports = function(app){
app.get('/whatever', function(req, res){});
};
The exploding app.js file prompted a couple of us to create a small reference app to codify a standard Express app structure. It's not rocket science, but rather a set of conventions that makes things more organized.
You can find it here: https://github.com/EAAppFoundry/tableau
We'd love suggestions/pull requests if there something we got wrong or is missing.
I don't think there should be any real problem with looping through the directory tree and generating the routes. However, it'll be hard to define route based middleware and other routing features (such as variables in the routes) in a nice way.
I wrote a library that I use to define my routes declaratively and with minimal repetition which you might be interested in. It's inspired by Rails resourceful routing and is pretty flexible - the idea is to build a hash of routes and subroutes; there are also facilities to define groups of routes, middleware and variables.
https://github.com/cheesun/express-declarative-routing
Although it doesn't automatically generate the routes based on your directory structure, I think that'd be a cool feature and welcome you add it to the library.

Separate layouts and namespaces for frontend/backend in Zend application

I had to develop a CMS using Zend Framework and I used the default namespace defined in my boostrap for my backend:
autoloaderNamespaces[] = "Application_"
Now I want to develop the frontend, but I don't know how to do it, since I have access to my backend from the /public/ directory.
Then I would like to use a different layout for my frontend than the one I use for the backend access. So I found this post but I don't know if I have to change/add (and then how to change) the module of my backend, or if I have to create a second module that I will use for my frontend
my file tree is like this :
So if I create a frontend module, shall I create a frontend directory next to the applicationdirectory ?
EDIT : I created 2directories pub and frontend next to the application directory. In pub/index.php I instanciated the bootstrap with the application/configs/application.ini file with a different APPLICATION_FRONT_ENV :
[frontprod : production]
bootstrap.path = APPLICATION_FRONT_PATH "/bootstrap.php"
bootstrap.class = "Bootstrap"
resources.frontController.controllerDirectory = APPLICATION_FRONT_PATH "/controllers"
autoloaderNamespaces[] = "Frontend_"
resources.layout.layout = "layout"
resources.layout.layoutPath = APPLICATION_FRONT_PATH "/layouts/scripts"
[frontdev: frontprod]
phpSettings.display_startup_errors = 1
phpSettings.display_errors = 1
resources.frontController.params.displayExceptions = 1
and in the frontend/bootstrap.php I loaded models from applicationdirectory :
public function _initAutoloader(){
$baseAutoload = new Zend_Loader_Autoloader_Resource(array(
'namespace' => 'Application',
'basePath' => realpath(dirname(__FILE__).'/../application')
)
);
}
And it seems to be working fine =)
thank you !
In Zend Framework you can organise your app in modules, wich suits very well to your needs. Unfortunately the doc doesn't emphasize enough the importance of this concept, and how you should implement it from day one.
Modules allows you to regroup under a same module folder everything that is related to this module only, and this way to isolate "parts" of your app in logical groups.
In your case it would be "back" and "front", but you could also have a "forum" module or let's say a "shop" module.
In the urls point of view, the default routing of a modular structure is example.com/module/controller/action, but using hostname routes you can also have www.example.com pointing to your "front" module and admin.example.com pointing to your backend.
Have a look at the poor documentation section about modules, and don't panic, you won't have to rename everything if you move your current controllers, views and models in the "default" module.
There is an other alternative that could suit well for a backend/frontend logic, but not if you want to split your code in more logical parts (forum, blog, shop,...). You just create a second application folder (you would name 'frontend') next to the 'application' folder, and a second public directory (where you can symlink your assets folder if you use the sames), and a different namespace.
To be able to autoload your 'Application_' classes in your frontend code, just add and configure a Module Autoloader in your frontend bootstrap. The code is quite simple :
//in your frontend/Bootstrap.php
public function _initAutoloader(){
new Zend_Loader_Autoloader_Resource( array(
'namespace' => 'Application_',
'path' => realpath(dirname(__FILE__).'/../application'
)
);
}
For the application.ini config file i would recommend, instead of duplicating it, that you create a section [frontprod : production] section where you override your backend settings (and a matching [frontdev: frontprod] for your local settings).
I hope this helped. There is so much to say about all the topics introduced here that you should first have a look at this, then comment this answer with more specific questions about the problems you may encounter, and i'll extend the answer.

Resources