VueJs 2: Unable to render view when using history mode and route params - express

I am trying to set up SPA routes using history mode as follows:
{
mode: 'history',
routes: [
{
path: '/',
component: Home
},
{
path: '/articles',
component: ArticleList,
children: [
{
path: ':title',
component: ArticleView
}
]
}
]
}
As I am using the history mode routing on vue and on express application I've set up "express-history-api-fallback" as the last middleware in the pipeline:
const app = express();
const root = path.join(__dirname, '../client/dist');
app.use(express.static(root));
/* other middlewares */
app.use(fallback('index.html', {root: root}));
At the moment of a page reload, everything works fine. I.e. loading a url http://application/articles, opens correctly the view, BUT when I try to access the view that takes in a parameter, no matter what, the view does not get loaded and two requests are made to the express.
I.E. a request to http://application/articles/test will resolve into two requests. One to http://application/articles/test and another one to http://application/articles/app.[calculated-hash].js
As far as I understand, the first request fetches the index.html the other request fetches the bundled js script.
Also, on the express app, all routes to api start with 'api' prefix.
Question:
What is wrong with my setup/code using history mode and route params because no routes with parameters are loaded when trying to access them when entering url manually or hitting refresh?
Update:
Using connect-history-api-fallback produces the same results

The problem was with the script tag the webpack injects into the index.html
<script type="text/javascript" src="app.06503cbc260646a996d9.js"></script>
The src attribute value was missing a '/' prefix thus the resolution of files failed.
So to make it work, I've linked the src file like this:
<script type="text/javascript" src="/app.06503cbc260646a996d9.js"></script>
As I am using the webpack to bundle js files, on the webpack config I've added output.publicPath '/'
More info: connect-history-api-fallback bugtracker

Related

Catch-all route for production build of Vue Router/Vite-based SPA

I have a Vue3-based SPA using Vue Router and Vite as dev server. The app has three valid URL paths:
/
/first
/second
For production, it will be deployed on Apache under a prefix, i.e. the production URL paths will be:
/prefix
/prefix/first
/prefix/second
What I want to achieve is that clients should be redirected to a valid URL path (e.g. /prefix) of the application even when they initially request an invalid URL path, such as /prefix/invalid.
For this purpose, I've defined the following router.js:
import { createWebHistory, createRouter } from "vue-router";
const routes = [
{
path: "/first",
component: () => import("./components/First.vue")
},
{
path: "/second",
component: () => import("./components/Second.vue")
},
{
path: "/:anything+",
redirect: "/"
},
];
const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL), routes});
export default router;
The third route entry defines a catch-all route that will match whenever clients request an invalid URL path. This works fine in development on Vite, i.e. when serving the app via npm run dev -- --base='/prefix/'. Even when the browser initially requests /prefix/invalid, the app's /prefix route is loaded and displayed.
When deploying with npm run build -- --base='/prefix/' for production, however, the catch-all route starts working only after clients have initially requested /prefix or /prefix/index.html and the SPA has been loaded. When they initially request /prefix/invalid or even /prefix/first or /prefix/second, Apache responds with 404 Not Found. This is of course because the whole routing is implemented client-side in JavaScript, so my catch-all route and everything else routing-related will only work once the SPA itself has been loaded and its JavaScript is executing in the browser.
My question is: is there a way to make initial requests for invalid URL paths work in production on Apache like in dev on Vite? I tried adding a file public/.htaccess like this:
ErrorDocument 404 import.meta.env.BASE_URL
But the expression import.meta.env.BASE_URL, which Vite statically replaces with the base config option value in router.js, is not replaced in this file - which is consistent with the documentation of the public directory. Hence this approach doesn't work.
Not sure whether I'm following the right path with .htaccess or whether my business problem has any better -possibly simpler- solution?
The problem can be solved by generating .htaccess dynamically using a simple Vite plugin, which I define inline in vite.config.js for simplicity.
// vite.config.js
import { defineConfig } from 'vite'
import vue from '#vitejs/plugin-vue'
// A private Vite plugin for generating .htaccess dynamically
function generateHtaccess () {
// Resolved Vite configuration, including "base" option
let viteConfig;
return {
name: 'generate-htaccess',
// Rollup Plugin API output generation hook
// See https://rollupjs.org/guide/en/#generatebundle
generateBundle() {
this.emitFile({
type: 'asset',
fileName: '.htaccess',
source: `ErrorDocument 404 ${viteConfig.base}\n`
});
},
// Vite Plugin API specific hook
// See https://vitejs.dev/guide/api-plugin.html#configresolved
configResolved(resolvedConfig) {
viteConfig = resolvedConfig;
},
};
}
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), generateHtaccess()],
});
A non-prefixed production build, such as npx vite build, will hence generate dist/.htaccess with the following content:
ErrorDocument 404 /
while specifying a prefix, e.g. npx vite build --base='/~user/' will generate:
ErrorDocument 404 /~user/
With this .htaccess, clients initially requesting any invalid URL path in production on Apache will be properly redirected to the same catch-all route -the app's root directory- as defined in router.js. This behavior will work consistently for any --base value supplied at build time.

Static page within dynamic nested route not generated on NuxtJS

My goal is to have the following page structure
pages
--/users/
--/users/:id/
--/users/:id/edit/
edit should be generated for every :id. Right now my setup works only in development mode. On nuxt generate only the first /users/:id/edit/ is generated. All other routes get a 404 error on production!
Current setup:
- /users
- index.vue
/_id
- index.vue
- edit.vue
I also tried to add another _id.vue which i found in the documentation and here
/users
- _id.vue
- /id
[...]
I checked this as well.
In nuxt.config.js I have
ssr: true,
target: 'static',
Do I need to add something in router property in nuxt.config.js? I am not sure why it is not generated in production.
If you are using static site generation you have to define your static routes, because Nuxt does not know how many users do you have in your data base.
You can set a generate method into your nuxt.config.js file:
generate: {
fallback: true,
async routes() {
const routes = await _getRoutes()
return routes
}
}
And then declare a function called _getRoutes to generate a list of dynamic routes:
async function _getRoutes($content) {
const usersList = [1, 2, 3] // get from your DB
const paths = []
usersList.forEach((userId) => {
paths.push(`/users/${userId}`)
paths.push(`/users/${userId}/edit`)
})
return paths
}
this is a bug with nuxt 2 in vue 2.
for dynamically generated routes, and also not-english nested routes ,
(or maybe just generally non-english routes) when vue hydrates the component , it will face a problem and restarts it . so the data that you cached in data(){} with fetch(), will be empty again .
currently you should use static and/or english-only routes .

Cannot figure out how vue-router works

I have a Vue.js project with the following router:
import Vue from 'vue';
import Router from 'vue-router';
import Overview from '#/components/Overview';
import Experiment from '#/components/ForExperiment';
Vue.use(Router);
export default new Router({
routes: [
{
path: '/',
redirect: 'test',
},
{
path: '/overview',
component: Overview,
},
{
path: '/overview/from/:from/to/:to',
name: 'overview',
component: Overview,
},
//... some other urls goes here.
{
path: '/test',
name: 'test',
component: Experiment,
},
],
});
If I open http://localhost:8080 in a browser I am redirected to http://localhost:8080/#/test. Why not just http://localhost:8080/test? Where does the '#' symbol come from?
And why if I open http://localhost:8080/test am I redirected to http://localhost:8080/test#/test?
And what is even more strange, if I open http://localhost:8080/overview I am redirected to http://localhost:8080/overview#/test, so the Overview component is not displayed.
What can cause these strange effects?
Vue router has different modes. The default mode when a browser is detected is hash. The current route is determined by the hash part of the url. The upside of this approach is that no serverside configuration is required. All urls point at the same resource (e.g. the route), which you can make your index.html file.
You can change this mode to history. The history mode uses the history api of the browser. It will only work in recent browsers, but support should not be an issue at this point. It will also require server side configuration in that you need to internally rewrite your router urls to the same file. If you would not do that, refreshing the page will show a 404 page instead of the page you want to see.
vue-router default mode is hash mode, that is why you see a # on your URL. It uses the URL hash to simulate a full URL without reloading the page if it changes.
To get rid of the hash, we can use vue-router history mode. Change the mode like so:
const router = new VueRouter({
mode: 'history',
routes: [...]
})
This leverages the History API.
If you want to use the history mode, you'll need to change your server configuration. Vue Router docs has some examples here.
The vue router defaults to hash mode. For your url to go to http://localhost:8080/test you need to go into history mode. This is done because by default web servers are not setup to redirect all requests to one html file. hash mode is used to per the docs:
The default mode for vue-router is hash mode - it uses the URL hash to simulate a full URL so that the page won't be reloaded when the URL changes.
Change your router to this to get history mode. But you will need to configure NGINX or Apache2 to redirect all requests to your vue code
const router = new VueRouter({
mode: 'history', // Add this to your router
routes: [...]
})

Vue Routes does not work properly in production

I am new to vuejs. For my Vuejs application, I cannot access url like '/foo/apple' in the web hosting server, after I run the "npm run build". It shows error 404 I am using the HTML5 History Mode,(https://router.vuejs.org/guide/essentials/history-mode.html#example-server-configurations) and I implemented the "connect-history-api-fallback" like below in dev-server.js:
const express = require('express')
const app = express()
app.use(require('connect-history-api-fallback')())
My router/index.js
export default new Router({
mode: 'history',
{
path: '/Product',
name: 'Product',
component: Product,
},
{
path: '/Product/kraftbag',
name: 'Kraftbag',
component: Kraftbag
},
});
my website http://americandunnage.n55lwi.info/.
I have looked for a lot of posts regarding to this problem, but I still can find the solution.
you are likely missing the hashbang (#)
try #/foo/apple instead of /foo/apple
The default setting of the vue router is to use the hashbang method. This relies on using the index.html (or whatever defaults to / url) page, because everything after it is not taken as part of the url location but passed into the application. For example, in an anchor 1st heading this will go to a part of the page, and if you're already on that page, you will not be redirected.

express server for serving Vue.js html file

I wrote a Vue application using the CLI. I’ve run the build command and got the dist folder with html file and the associated js/css file etc. Now I want to host the app on heroku with node/express sever, what I’ve written only half works.
For the record I’m using the vue router to render different components at different routes.
const routes = [
{path: "/", redirect: "/home"},
{path: "/home", component: body},
{path: "/home/new", component: create},
{path: "/home/entry/:id", component: blog},
{path: "/register", component: register},
{path: "/logout", component: logout},
{path: "/home/user/:userprofile", component: userprofile}
]
My vue application calls a separate backend API for all its data so the express server I’m requesting is only to server the html file across all routes.
This is what I have found from google:
var express = require('express');
var path = require('path');
var serveStatic = require('serve-static');
var port = process.env.PORT || 5000;
app = express();
app.use(serveStatic(__dirname + "/dist"));
app.listen(port);
It works when I’m going to “/” route but when I go to “/home” or similar it returns an error “cannot GET /home” or similar.
Can someone write me a short and simple express sever app for deploying vue on Heroku please?
Most likely you are missing configuration on your web server to redirect all requests to "/". VueJs router takes responsibility after that but it doesn't receive requests unless you explicitly configure your web server to do so.
Here is a Vue Router documentation page with short code snippets for different web servers that you will need to add to your web server configuration.