Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose history api fallback options #7095

Closed
4 tasks done
otakustay opened this issue Feb 26, 2022 · 12 comments
Closed
4 tasks done

Expose history api fallback options #7095

otakustay opened this issue Feb 26, 2022 · 12 comments

Comments

@otakustay
Copy link
Contributor

Clear and concise description of the problem

We're setting up a project simulating micro-frontend like qiankun in development, for this to work we need to respond index as a special "wrapper" html with qiankun runtime and a startup script pulling actual project, just like:

<script src="/__local_qiankun_bundle__.js"></script>
<script>
  qiankun.registerMicroApps([
    {
      name: 'xxx',
      entry: '/index.html', // The actual index html
      container: '#root',
      activeRule: '/',
    }
  ]);
  qiankun.start();
</script>

(This is only an example, actually we bundle qiankun runtime and entry script with esbuild.)

We then need to modify dev server's route into:

  1. history api fallback to /__qiankun_wrapper__.html which responds content described above
  2. on /__local_qiankun_bundle__.js responds a bundled version of qiankun
  3. /index.html to serve as what vite did not

Without the ability to customize historyApiFallback middleware, it is not possible to setup a server exactly do these staffs, and historyApiFallback is a special middleware that we cannot add another one before or after vite's middleware array.

Suggested solution

Expose server.apiHistoryFallbackOptions to allow user customize this behavior.

Alternative

Another solution is to allow a server.historyApiFallback: false option to intentionally disable history api fallback behavior, users can then add this middleware on demand.

Additional context

No response

Validations

@bluwy
Copy link
Member

bluwy commented Feb 26, 2022

The alternative seems to be similar to #5720, but I'm curious if transformIndexHtml would work for your usecase?

That should allow you to inject the extra scripts into index.html. For /__local_qiankun_bundle__.js, maybe you can do <script type="module" src="./path/to/__local_qiankun_bundle__.js"></script> instead? A Vite plugin could be used if you want the src to point to a virtual path, and the plugin dynamically resolve the virtual path.

3. /index.html to serve as what vite did not

I don't understand what you mean here. Do you mean preventing index.html from serving?

@otakustay
Copy link
Contributor Author

I don't understand what you mean here. Do you mean preventing index.html from serving?

We need index.html to be served as usual, but index.html is not the entry, it is loaded by qiankun's wrapper html. This is why we can't make use of transformIndexHtml because we still need index.html and all of its functionality (including original transformIndexHtml)

The qiankun's wrapper html will execute logic like:

// In this fetch we need vite's dev server to serve original index.html
const html = await fetch('/index.html').then(r => r.text());
const subAppDocument = parsetHtml(html);
subAppDocument.head.children.forEach(v => document.head.appendChild(v));
const container = document.getElementById('root');
container.empty();
container.appendChild(subAPpDocument.body.firstChild);

@bluwy
Copy link
Member

bluwy commented Feb 26, 2022

I see. Thanks for the explanation! Besides the alternative mentioned in #5720, I wonder if a approach like:

  1. Use transformIndexHtml to overwrite index.html to qiankun's script, but import _internal_index.html instead.
  2. Have a vite plugin to resolve _internal_index.html to the actual index.html content.

Would work? 🤔 Hoping to reduce the need to introduce new options as Vite tries to avoid that as much as possible.

@otakustay
Copy link
Contributor Author

I didn't find a way to resolve _internal_index.html, I tried a vite config as this:

import fs from 'fs/promises';
import { defineConfig, Plugin } from 'vite'
import react from '@vitejs/plugin-react'

function setup(): Plugin {
  return {
    name: 'custom',
    transformIndexHtml(html) {
      return 'Hello WOrld';
    },
    resolveId(id) {
      console.log('resolve', id);
      if (id === '/custom.html') {
        return id;
      }
    },
    load(id) {
      console.log('load', id);
      return fs.readFile('index.html', 'utf-8');
    }
  };
}

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [setup(), react()]
})

When visiting http://localhost:3000/custom.html I get 404 instead of vite resolving and loading it as a virtual module, no console output there.

Also I tried to add import('./custom.html') inside transformIndexHtml:

transformIndexHtml(html) {
  return `
    <!DOCTYPE html>
    <html
      <body>
        <script type="module">
          import('./custom.html');
        </script>
      </body>
    </html>
  `
},
async load(id) {
  console.log('load', id);
  const loaded = await this.load({id: '/index.html'});
  return loaded;
},

This time vite complains about this.load is not a function, also import in qiankun's wrapper html doesn't make any effect, we must have /custom.html accessible via fetch

@otakustay
Copy link
Contributor Author

After some deeper investigation, I believe qiankun won't be able to support vite's dev mode at all, it's core import-html-entry can't work with script[type=module] and mostly can't be upgraded to support module script because it requires new Function to eval code, which doesn't support top level import() at all

@bluwy
Copy link
Member

bluwy commented Feb 28, 2022

FWIW I've been testing out the plugin too and it doesn't seem possible unfortunately. Looks like loading .html files isn't possible with Vite plugins as the .html files are handled directly by the dev server directly.

@otakustay
Copy link
Contributor Author

@bluwy You can refer to my custom plugin as an implement of hijacking .html load for vite in dev mode, and this plugin for build mode, I've tested them and works at least in a simple TodoMVC app

@bluwy
Copy link
Member

bluwy commented Mar 1, 2022

Neat! Thanks for figuring out the plugin flow, that would be really helpful in the future. Do you think this feature request is still needed given the plugin solution?

@chriscalo
Copy link

chriscalo commented Mar 1, 2022

I for one would love to see a built-in solution for this.

I came up with a very different plugin approach (see below), which seems to mostly work, though not perfectly[1].

Rationale for why this seems important to build into vite core:

Vite seems to take the stance that:

  • it's a dev and build tool only, not to be used in production
  • built files are meant to be served statically, therefore, it doesn't come with a production server

However, for static file servers:

  • in many static file servers it's pretty easy to configure it to return index files when a directory is requested
  • however, they generally don't fallback to serving /index.html when a file is not found and instead return a 404 in those situations

Therefore, it doesn't make much sense that Vite's dev server has this fallback behavior when it targets production environments that don't have it. It would be nice if there were a "correct" (that is, well tested) way to turn off the fallback behavior while keeping the rest of the serving behavior (HTML transformation, serving the right headers, etc).


Update: I published the package vite-plugin-no-fallback to address this issue.

[1] The plugin above seems to have broken the ability to use a <script> tag with content as the app entry point instead of linking to the entry point via the src attribute:

<html><script type="module">
    import Vue from 'vue';
    import App from './App.vue';
    
    new Vue({
      render: (h) => h(App),
    }).$mount('#app');
  </script></html>

@michaelalhilly
Copy link

@sapphi-red Thanks for linking here.

I'm having a similar issue. I'd like to nest all my entry points including root index.html in /src/pages. I'm having trouble with both; getting rid of the trailing slash for nested pages and getting the root index.html to work in /src/pages. Tried everything in the docs and still no luck.

@bluwy
Copy link
Member

bluwy commented Jun 26, 2022

Vite 3 can now officially support this with appType: 'mpa', which would disable the spa fallback, so you can add a custom one if needed via configureServer. appType is also used for the preview server too so it should emulate your production setup. Otherwise there's also configurePreviewServer hook for plugins.

@bluwy bluwy closed this as completed Jun 26, 2022
@chriscalo
Copy link

Amazing, thanks for sharing appType: 'mpa'. I haven't tried yet, but this feels like it should make my issue go away.

@github-actions github-actions bot locked and limited conversation to collaborators Jul 18, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

4 participants