zalando / tailor

A streaming layout service for front-end microservices
http://www.mosaic9.org
MIT License
1.73k stars 140 forks source link

Fragment-common, Vue 3 & RequireJS #343

Open Oskar-Nilsen-Roos opened 3 years ago

Oskar-Nilsen-Roos commented 3 years ago

Hi! I've been trying to create a "fragment-common" for resources that are to be shared between our micro frontends, and I haven't been able to find an example of someone bundling Vue as a shared resource.

The only way I got it working was through a script tag, pointing to a bundle.js with Vue. However, what we actually want is a regular fragment tag so we can point our shared JS resources through a Link header instead, but I haven't been able to get this working.

My current solution looks something like this:

common.js

exports.Vue = require('vue/dist/vue.esm-bundler')

webpack.config.js

var webpack = require('webpack')

module.exports = {
  entry: './common.js',
  output: {
    path: __dirname + '/public',
    filename: 'bundle.js',
    libraryTarget: 'umd',
  },
}

base-template.html

<head>
    <script src="http://localhost:6006/fragment-common/public/bundle.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.22/require.min.js" crossorigin=""></script>
</head>
<body>

  <p id="app">{{user}}</p>

  <script>
      const RootComponent = {
        setup() {
          return {user: 'Test'};
        }
      };
      const app = Vue.createApp(RootComponent);
      app.mount('#app');
  </script>
</body>

And this also comes with a caveat, I noticed that if RequireJS (automatically imported by tailor) comes before the bundle.js script, Vue won't load. I don't know why this is. As far as I know there's no nice way to make sure RequireJS loads after, but I solved it by passing an empty string to the amdLoaderURL option in the Tailor config like so:

const tailor = new Tailor({
  fetchTemplate: fetchTemplateFs(
    path.join(__dirname, 'templates'),
    baseTemplateHeaderFn
  ),
  fetchContext: (req) => {
      ...
  },
  filterRequestHeaders: filterHeadersFn,
  amdLoaderUrl: ""
)}

Which results in the following when Tailor tries to set it:

<script src="" crossorigin=""></script>

Where it is later imported manually by setting it in the template instead.

I have tried different ways of solving this, with different webpack builds and also this solution, but to no avail.

TLDR: How do I share Vue in a common fragment, passing it with a Link header to Tailor on fragment request?

stevoPerisic commented 3 years ago

I would discourage you from loading "common" scripts. This comes from experience.

The problems you are trying to solve like repeating the code, loading commonly used files across fragments/URLs and reducing duplication of code in the code-base requires a different approach.

In our solution we have fallen into the common script trap and now find ourselves too coupled and unable to release smaller changes without always releasing the common fragment. I think that using bundler's tree-shaking capabilities can be helpful and potentially a better approach. Check this out: https://webpack.js.org/guides/tree-shaking/ https://parceljs.org/features/code-splitting/#tree-shaking

This does mean you will have to do a bit of extra work to make sure any legacy code is written to be tree-shaken (in proper module syntax). But, this will pay off in the long run. Hope this helps!

On Mon, Oct 18, 2021 at 9:28 AM Oskar Nilsen Roos @.***> wrote:

Hi! I've been trying to create a "fragment-common" for resources that are to be shared between our micro frontends, and I haven't been able to find an example of someone bundling Vue as a shared resource.

The only way I got it working was through a script tag, pointing to a bundle.js with Vue. However, what we actually want is a regular fragment tag so we can point our shared JS resources through a Link header instead, but I haven't been able to get this working.

My current solution looks something like this:

common.js

exports.Vue = require('vue/dist/vue.esm-bundler')

webpack.config.js

var webpack = require('webpack') module.exports = { entry: './common.js', output: { path: __dirname + '/public', publicPath: 'http://localhost:6006/public/', filename: 'bundle.js', libraryTarget: 'umd', },}

base-template.html

{{user}}

And this also comes with a caveat, I noticed that if RequireJS (automatically imported by tailor) comes before the bundle.js script, Vue won't load. I don't know why this is. As far as I know there's no nice way to make sure RequireJS loads after, but I solved it by passing an empty string to the amdLoader option in the Tailor config like so: const tailor = new Tailor({ fetchTemplate: fetchTemplateFs( path.join(__dirname, 'templates'), baseTemplateHeaderFn ), fetchContext: (req) => { ... }, filterRequestHeaders: filterHeadersFn, amdLoaderUrl: "" )} Which results in the following when Tailor tries to set it: Where it is later imported manually by setting it in the template instead. I have tried different ways of solving this, with different webpack builds and also this solution , but to no avail. *TLDR:* How do I share Vue in a common fragment, passing it with a Link header to Tailor on fragment request? — You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub , or unsubscribe . Triage notifications on the go with GitHub Mobile for iOS or Android .
Oskar-Nilsen-Roos commented 3 years ago

Thank you for your input, that does make sense. We have thought some about what it's going to look like when we, for example, eventually transition from all apps running Vue 3, to all except one which is running Vue 4. In this case we'd have to bundle two different versions of Vue into our common fragment until all have transitioned properly. Is this similar to how you've had to solve it too?

I'm not too familiar with tree shaking, is it possible for it to solve my Vue issue, as in at runtime? I read the article but as far as I can understand it only applies to reducing dead code in the same project, not across several different apps running on the same page, potentially with different build configs. Do correct me if I'm wrong!

stevoPerisic commented 3 years ago

I don't think tree-shaking will solve the problem of having two separate versions of Vue in fragments within one URL. You will have to live with the initial page load time slowness. Potentially you can load the resources async, and use caching for the Vue lib code.

On Tue, Oct 19, 2021 at 10:55 AM Oskar Nilsen Roos @.***> wrote:

Thank you for your input, that does make sense. We have thought some about what it's going to look like when we, for example, eventually transition from all apps running Vue 3, to all except one which is running Vue 4. In this case we'd have to bundle two different versions of Vue into our common fragment until all have transitioned properly. Is this similar to how you've had to solve it too?

I'm not too familiar with tree shaking, is it possible for it to solve my Vue issue, as in at runtime? I read the article but as far as I can understand it only applies to reducing dead code in the same project, not across several different apps running on the same page, potentially with different build configs. Do correct me if I'm wrong!

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/zalando/tailor/issues/343#issuecomment-946808229, or unsubscribe https://github.com/notifications/unsubscribe-auth/AANQN27RHI5CM5BU7GVCPRDUHWBFPANCNFSM5GGVAQXQ . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.

Oskar-Nilsen-Roos commented 3 years ago

Okay, then our current solution doesn't seem too bad to share Vue between a few fragments 😄 But I will keep in mind your tip of not getting stuck in the script trap. Tree-shaking still seems like it would be a great addition to reduce our load times though!

If people don't mind I'd like to keep the issue open for anyone that might have solved this with the RequireJS define script, or ran into the weird Vue + RequireJS load order thing.

aviranbergic commented 2 years ago

@Oskar-Nilsen-Roos not sure it helps you now but what we do is as follows: we have a common-fragment that brings all shared dependencies to the page

<script>
                     (function (d) {
                       require(d);
                       var arr =  [
                         'react','react-dom',
                         'redux','react-redux','redux-thunk',
                         'redux-dynamic-modules',
                         'redux-dynamic-modules-thunk',
                         'redux-dynamic-modules-react',
                         'redux-dynamic-modules-core',
                         'styled-components',
                         'i18n-react'
                        ];
                       while (i = arr.pop()) (function (dep) {
                           define(dep, d, function (b) {
                               return b()[dep];
                           })
                       })(i);
                      }(['/res/fgmt/common/v/1.1.103/bundle.js']));
                   </script>

on each one of the fragments we define these packages as externals

xternals: {
    react: 'react',
    'react-dom': 'react-dom',
    'react-redux': 'react-redux',
    redux: 'redux',
    'redux-thunk': 'redux-thunk',
    'redux-dynamic-modules': 'redux-dynamic-modules',
    'redux-dynamic-modules-thunk': 'redux-dynamic-modules-thunk',
    'redux-dynamic-modules-react': 'redux-dynamic-modules-react',
    'redux-dynamic-modules-core': 'redux-dynamic-modules-core',
    'styled-components': 'styled-components',
    'i18n-react': 'i18n-react',
  }

we use tree shaking and each fragment is about 90kb.

our common fragments do not only bring libraries but we also initialize redux store and populate cross-application data in the store so all fragments can access if they need. (we still maintain a clear separation of concerns between fragments, common is the exception)

we use redux dynamic modules to be able to append reducers to store post initialization.

hops thats help somehow.