3

I'm working on a WordPress website that uses the Swiper library, on several pages, as an npm dependency. Included in the website is an embedded Vue CLI 4 app, that I'd also like to add the Swiper library to. Currently, the website has its own webpack config, while the app uses a slightly modified version of its default build config.

There are Vue Swiper components that I could use but, as I'm already using the original library, I'd like to avoid duplication. I assume I'd need to somehow combine my two build processes (unless there's another solution), but I've not attempted anything like that before.

The main webpack config is as follows:

const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = (env, options) => {
    return {
        entry: {
            main: ["./src/js/index.js", "./src/scss/index.scss"],
            admin: ["./src/scss/admin.scss", "./src/js/admin/index.js"]
        },
        output: {
            path: path.resolve(__dirname, "dist"),
            filename: "[name].js",
            publicPath: "/dist"
        },
        module: {
            rules: [
                {
                    test: /\.js$/,
                    use: {
                        loader: "babel-loader",
                        options: { presets: ["@babel/preset-env"] }
                    }
                },
                {
                    test: /\.scss$/,
                    use: [
                        MiniCssExtractPlugin.loader,
                        "css-loader",
                        "sass-loader"
                    ]
                },
                {
                    test: /\.svg$/,
                    use: {
                        loader: 'file-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: '/images',
                            publicPath(url) {
                                return `/wp-content/themes/themename/dist/images/${url}`
                            },
                        },
                    }
                },
            ]
        },
        plugins: [
            new MiniCssExtractPlugin({
                filename: "[name].css"
            })
        ]
    }
};

And the directory structure:

├── app
|   ├── src
|   ├── dist
|   ├── ...
|   └── vue.config.js
├── src
├── dist
├── ...
└── webpack.config.js

Ideally, I'd like to keep the directories as they are, so how would I be able to use Swiper in both projects, without having it duplicated across both bundles?

verism
  • 1,106
  • 1
  • 12
  • 35
  • So are you saying that you have the swiper library on many pages but are only using Vue CLI 4 app on one page? So in your header you are including the script that contains the swiper library but when a specific page loads you then have a script that includes the vue app? – Hides May 01 '20 at 11:11
  • @Hides Yes to your first question - that's exactly right. Currently, however, Swiper is imported via `index.js` and webpack bundled into one single file. The Vue app is (currently) on a single page though. – verism May 01 '20 at 11:53
  • Sorry, are you saying that index.js is being imported into the site but the vue app is in another file which is only imported on that single page? – Hides May 01 '20 at 12:56
  • Yeah that's right. If you look at the directory structure above - the _app_ directory contains all the Vue logic which is included on the homepage. The Vue app has its own build process that's separate from the outer files. – verism May 01 '20 at 13:04
  • Are you wanting to avoid bundling both index and your vue app together to into one single file and calling that on every page? To combine the processes then you could edit your package scripts to first build the vue app then in the upper directory to build your index.js file. something like `scripts: { 'build': 'cd app && npm run build && cd ../ && npm run build'} – Hides May 01 '20 at 13:13
  • That's right, they should be bundled entirely separately. I don't necessarily need to run both builds at the same time though - they're ostensibly two totally independent codebases albeit with (hopefully) shared vendor libraries. – verism May 01 '20 at 14:56
  • You could do something like [this](https://stackoverflow.com/questions/35464020/checking-if-external-js-library-is-loaded-or-not). Check to see if the swiper library has been loaded and if not call it in. This way you don't have duplicate code and your vue app can still be a stand alone however it will need to make an additional http request once the javascript has loaded which will impact load time, not huge amount, but something to consider if you want to use your vue app outside of the rest of the build – Hides May 01 '20 at 21:43

2 Answers2

4

Just add all dependencies via NPM to be included in the build vendor bundle.

You could then use vanilla JS like referenced here:

// Either Vanilla JavaScript
window.addEventListener('load', function() {
  console.debug('All assets are loaded');
})
// or jQuery
$(document).ready(function() {
  // After App class declared in global namespace
  App.init();
});
// Loaded in document head
<script type="text/javascript">
  document.addEventListener("DOMContentLoaded", function(event) {
    // After App class declared in global namespace
    App.init();
  }); 
</script>

I'd advise using or learning about Vue CLI bootstrapped project template with CLI plugins added via webpack and NPM. Then in vue.config.js file you can configure your webpack plugin settings.

For instance, using Webpack Bundle Analyzer plugin to be run as your build via a npm script command. With -o parameter it opens a visualized map of your vendor scripts bundle size on a block map.

EDIT: So in your vue.config.js you could add separate entrypoints:

module.exports = {
  pages: {
    app: {
      index: 'client/src/index.ts',
      template: 'client/public/index.html',
    },
    main: {
      entry: 'client/src/main.ts',
      template: 'client/public/main.html',
    },
  },
};

Then index.html you can for example dynamically check if some rule, i.e. active session id in local storage, and based on that intel execute init method for your app.

Then you'd have main.ts which directly starts the app, when user browsing to /main path in your domain.

ux.engineer
  • 10,082
  • 13
  • 67
  • 112
  • I've modified `vue.config.js` a little already - I assumed this problem was outside the scope of Vue's build process though, as the library in question exists in the outer dependency tree. Am I also right in thinking the vendor libraries are exposed as global variables to all other scripts on the page? – verism May 01 '20 at 14:51
  • Vendor packages installed via NPM *are loaded* as any script asset you would manually add as a script url in your document head. If using TypeScript, then you would need to add at least interfaces for those scope names. Also note that Vues default build setup creates another file "vendors" in the bundle, and one for each of your apps entrypoints. For example you can have main.ts as your main entrypoint, then have another oidc-callback.ts and another html base file in your public/oidc-callback.html – ux.engineer May 02 '20 at 09:47
  • Please also note that when install i.e. with npm, `npm install -D` install as devDependency (not included when building with production environment value), and `npm i -S` saves as dependency (to be included in production bundle). – ux.engineer May 02 '20 at 09:50
  • @ux.engineer If I understand correctly, I should add Swiper lib to NPM dependencies of the Wordpress project and call `App.init()` in Vue.js app code? (Given that `App.init()` contains swiper initialization.) It took me a while to figure this out... – user14967413 May 22 '22 at 11:41
2

If you want Vue to use a library that's already loaded in the window object, specify it in vue.config.js:

module.exports = {
  chainWebpack: config => {
    config.externals({
      swiper: 'Swiper'
    })
  }
}

Now anywhere you use import Swiper from 'swiper' (or import * as Swiper from 'swiper' - depending on your tsconfig and on how the lib you're using is exported) you're actually getting whatever window.Swiper is at the moment whatever imports it is instantiated (might be the app or might be at load time of a lazy loaded component).


As far as reputable sources go, I believe the highest authorities on the matter are:

Externals

Prevent bundling of certain imported packages and instead retrieve these external dependencies at runtime.


Also note you have to import the module in public/index.html using a <script> (pointing to wherever you load that lib from in production), if you want your serve to work, as you now have told webpack it's going to find that dependency in the window object. Otherwise your build will work just fine (assuming window.Swiper exists in production), but serve won't.


Using externals with typescript:

Note you will still need @types/${moduleName} in devDependencies if you want ${moduleName} to be typed in your application, if @types/${moduleName} exists.

If it doesn't, you can import the types from the package (in devDependecies, just for the types, it won't be included in the bundle). If none of these options is valid for your particular package, you probably want to declare a mock module in your shims-vue.d.ts:

declare module '${moduleName}';

...which will effectively disable typescript checking for that module. Do note declaring a mock can be detrimental, as it will override any existing typings for that module. So only use it when nothing else works.

Obviously, in your case ${moduleName} is swiper.

tao
  • 82,996
  • 16
  • 114
  • 150