cascornelissen / svg-spritemap-webpack-plugin

SVG spritemap plugin for webpack
MIT License
207 stars 50 forks source link

How to require generated sprite in JS file #147

Closed czhDavid closed 3 years ago

czhDavid commented 3 years ago

Hi i have a quiestion and can't find solution anywhere. We want to add hash to the generated sprite, so when we add or change an icon, the browser will download newer spritefile. This can be easily achieved with .addPlugin(createSvgSpritemapPluginInstance('images/icons.[contenthash].svg')) And this will generate the file and line in manifest.json "dist/images/icons.svg": "/dist/images/icons.68e91515460ebaae.svg",. The problem is how to use this file in a React component or in any JS file? I am trying to do an import but can't seem to find the right combination: /import icons from '/dist/images/icons.svg'; The problem is that the file physicaly doesn't exist and is generated during compilation. Webpack will exit with `This dependency was not found:

is there a way how to use file with hash in JS file?

cascornelissen commented 3 years ago

You probably don't want to import the SVG spritemap file as that will download the entire spritemap every time you try to import/use a single sprite. There's a couple of approaches:

All approaches have pros and cons and the "best" option depends on your setup. If you have any further questions feel free to comment ✌🏼

HassanZahirnia commented 3 years ago

@cascornelissen Sorry for pinging on an old closed issue. Didn't want to create a new issue for asking this question. I'm using laravel mix and in my vue files I'm doing something like :

<template>
    <svg>
        <use :xlink:href="'/svg/sprite.svg#' + icon"/>
    </svg>
</template>

Which directly references the generated file at the public path ( public/svg/sprite.svg ). As you may have guessed, this causes caching issue in the browser because the filename has no unique hash attached to it and I don't want to manually add a random string to it everytime I add or remove a svg file.

Do you know anyway I can solve this issue ? You mentioned 3 solutions, and I don't quite know how to do the second and the third ones. Using a sprite file not only causes caching issues in production for the clients, but also it's really hard to develop locally because everytime I make change to the sprite file, I have to stop my npm run hot and run npm run dev to recompile the svg sprite then I have to run npm run hot again which takes lot of time.

Any helps would be appreciated! Thanks in advance!

cascornelissen commented 3 years ago

No problem, the main issue is that you want to bust the cache when the contents of the spritemap changes. The best way to do this is to use [contenthash] in the filename, this will insert a hash of the content in the filename which only changes when the content of the file changes. For example: sprite.95f18a53.svg where 95f18a53 is this contenthash that is injected by the plugin.

Then the next, and probably harder-to-solve issue is that you now have a filename with a dynamic value. The solution to this really depends on your stack, which is why it's hard to answer this question as I haven't worked with Laravel (mix) in a few years. I expect that both the second and third option in https://github.com/cascornelissen/svg-spritemap-webpack-plugin/issues/147#issuecomment-769412404 are possible in your case.

The second option describes a solution where you use the power of the templating engine that's used by the project. The templating engine probably has access to the filesystem and should be able to figure out what the filename of the spritemap is. Then this templating engine injects the SVG directly into the HTML that it's generating, this way you don't have to reference an external file but you can reference an element that's available in the DOM already. The main downside of this approach is that you're forcing users to download the entire spritemap because it's part of the HTML document, kind of defeating one of the main points on why you'd want to use a spritemap in the first place.

The third option describes a solution that makes use of another webpack plugin. This plugin generates a manifest JSON file with original filenames as keys and hashed filenames as values, allowing you to basically do manifest['sprite.svg'] to get to the value you need; sprite.95f18a53.svg. You can do this approach both on the server and on the client. Since you're using Laravel I'm guessing doing it on the server is the best way forward in your case, especially since doing it client-side also has some issues (e.g. https://github.com/cascornelissen/svg-spritemap-webpack-plugin/issues/170). In your template (I'm guessing at this point), you can import this generated manifest JSON file, and do something like:

<use :xlink:href="'/svg/' + manifest['sprite.svg'] + '#' + icon"/>

I hope this clears up the approaches you can take, I'm sure there are other ways to accomplish this but this should at least give you some insights.

HassanZahirnia commented 3 years ago

@cascornelissen Thanks a ton for the quick answer and detailed explanation 😇 You're right about using the [contenthash] in the filename is a bit hard to figure out later cause the generated hash is not stored anywhere, plus you have to clean up the previously generated files and it seems lots of work. The third option ( using manifest file ) seems to be the way to go. I've already implemented using the method you mentioned :

// main layout file
<script>
    window.sprite_path = "{!! mix('svg/sprite.svg') !!}";
</script>

and then

<use :xlink:href="'/svg/' + sprite_path + '#' + icon"/>

...

data(){
     return {
         sprite_path: window.sprite_path.split('/').pop(),
     }
},

The only ugly side of this approach was the mix webpack configuration because webpack doesn't pick up the sprite file generated by this plugin for some reason 🤔 So to tackle this issue I ended up generating the file at some unnecessary path like '../resources/pre/sprite.svg' and then use mix.copy to copy it over to the public directory. Doing all that just to let webpack know: "this is one of my assets you got to add to the manifest file and version it later in the production".

Thanks a lot again anyway, great stuff 😄👍

philippdieter commented 2 years ago

@cascornelissen

The third option describes a solution that makes use of another webpack plugin. This plugin generates a manifest JSON file with original filenames as keys and hashed filenames as values, allowing you to basically do manifest['sprite.svg'] to get to the value you need; sprite.95f18a53.svg. You can do this approach both on the server and on the client. Since you're using Laravel I'm guessing doing it on the server is the best way forward in your case, especially since doing it client-side also has some issues (e.g. #170). In your template (I'm guessing at this point), you can import this generated manifest JSON file, and do something like:

<use :xlink:href="'/svg/' + manifest['sprite.svg'] + '#' + icon"/>

This would be the exact way I want to go in a current project. But after spending time trying to figure out which plugin you may have in mind I still have no idea. I have found plugins which generate manifest files but none of them made a variable available in template files or allowed me to import the manifest. Can you give some more details on this?

cascornelissen commented 2 years ago

That comment seems to refer back to the comment I made earlier in this very same thread: https://github.com/cascornelissen/svg-spritemap-webpack-plugin/issues/147#issuecomment-769412404. That earlier comment mentions webpack-manifest-plugin which will create a JSON file that looks like this, according to its documentation:

{
  "dist/batman.js": "dist/batman.1234567890.js",
  "dist/joker.js": "dist/joker.0987654321.js"
}

That's just the first step though, you'll need to import this JSON file and use the correct key to reach the hashed filename value.

none of them made a variable available in template files or allowed me to import the manifest

Most plug-ins aren't going to provide this as it fully depends on your setup. If you're using a templating engine you'll have to look into the documentation for that tool on how to import and interpret JSON files. If, for example, you're using Laravel's Blade something like this will probably work. If you're rendering things client-side it might be as "easy" as using a dynamic import to fetch the file.

philippdieter commented 2 years ago

In my projects I use the html-loader so the hashed paths are written into the resulting html files when linking images. My knowledge is a little limited here, but afaik the image paths are parsed into require functions.

I was hoping for a solution which works alike for spritemaps generated by this plugin, without having to use a template engine or client side scripts, just a static html file with the hashed filename for the sprite. But if I get you right now, the manifest will not help me here. Thank you for the explanation.

cascornelissen commented 2 years ago

I don't have much experience with html-loader specifically either but I quickly also wanted to highlight that there's an example using html-webpack-plugin that might be similar to what you're looking for: https://github.com/cascornelissen/svg-spritemap-webpack-plugin/tree/master/examples/inline-html.