React-native-web: Image: new babel/webpack loader

Created on 24 Oct 2016  路  23Comments  路  Source: necolas/react-native-web

We need a new webpack image loader that can generate RN source objects from assets on the file system:

require('./image.png');
// => { uri: '...', width: 40, height: 40 }

It should also support RN's syntax for resolution-specific images:

image.png
[email protected]
[email protected]

...by adding extra data to the object so that Image can choose the most appropriate asset:

require('./image.png');
// => { uri: '...', uri@2x: '...', uri@3x: '...', width: 40, height: 40 }
enhancement help

Most helpful comment

I have created a webpack loader, which takes care of appropriate scaling usage too(based on device pixel ratio). Check it out, and feel free to contribute. This is my first npm package so please if something is wrong you can tell me :)
https://www.npmjs.com/package/react-native-web-image-loader

I have plans to release a webpack plugin too which is a resolver. For me, image suffixes were pain in the *ss since some module required its asset (./assets/back.png) but even the 1x scaling had a suffix(./assets/[email protected]) and webpack didn't seem to find it.

All 23 comments

Going to try this https://www.npmjs.com/package/react-native-image-loader
(can't find the repo, the only reference I found is https://github.com/willpiers/react-native-image-loader from https://github.com/mjohnston/react-native-webpack-server/issues/6 but leads to a 404)

Ok false alarm, this module is just 5 lines of code and totally don't work as expected :(
Will try to do one then.

I did this pretty quickly and it seems to work for now

const fs = require("fs")

const ImageFile = require("image-file")

module.exports = function(content) {
  const cleanPublicPath = content
    .replace("module.exports = ", "")
    .replace(/;$/, "")

  const path = this.resourcePath
  const image = new ImageFile(fs.readFileSync(path).buffer)

  return `module.exports = {
    uri: ${ cleanPublicPath },
    width: "${ image.width }",
    height: "${ image.height }"
  }`
}

Usage in webpack config:

        {
          test: /\.(jpg|png|gif)$/,
          loader: "./src/loaders/react-native-web-image-loader.js!file-loader",
        },

Nice start. How do we get webpack to extract any other resolution-specific images?

Not handled yet in my case, but I guess the next step would be:

const fs = require("fs")

const ImageFile = require("image-file")

module.exports = function(content) {

  //
  // @todo instead of using original file path, we should just reuse some
  // file-loader logic directly to support hash for file paths etc
  //

  const cleanPublicPath = content
    .replace("module.exports = ", "")
    .replace(/;$/, "")

  const absoluteFile = this.resourcePath
  const image = new ImageFile(fs.readFileSync(absoluteFile).buffer)

  const path = require("path")

  const absoluteData = path.parse(absoluteFile)
  const base = absoluteData.dir + absoluteData.name

  const fileData = path.parse(cleanPublicPath)
  const cleanBase = fileData.dir + fileData.name

  let cleanPublicPath2x
  let cleanPublicPath3x
  try {
    const x2 = fs.readFileSync(base + "@2x" + absoluteData.ext)
    cleanPublicPath2x = cleanBase + "@2x" + fileData.ext
    this.emitFile(cleanPublicPath2x, x2)
  }
  catch (e) {
    // do nothing, just ignore @2x file
  }
  try {
    const x3 = fs.readFileSync(base + "@3x" + absoluteData.ext)
    cleanPublicPath3x = cleanBase + "@3x" + fileData.ext
    this.emitFile(cleanPublicPath3x, x3)
  }
  catch (e) {
    // do nothing, just ignore @3x file
  }

  return `module.exports = {
    uri: ${ cleanPublicPath },
    ${ cleanPublicPath2x ? `uri@2x: ${ cleanPublicPath2x }` : "" },
    ${ cleanPublicPath3x ? `uri@3x: ${ cleanPublicPath3x }` : "" },
    width: "${ image.width }",
    height: "${ image.height }"
  }`
}

This code is currently pretty dumb and does not take in consideration the fact that path emitted by file-loader can be hashes like 0dcbbaa701328a3c262cfd45869e351f.png and will emit [email protected] for 2x version (which is stupid, and should just be another hash, see comment at the top of this snippet)

You may want to fork file-loader which internally uses loader-utils and takes care of hashing etc. Additional resolution info could be passed along with other configuration options like so: res: [1, 2, 3] maybe.

Will this also support absolute source uri in production? Scenario: using create-react-app to build the site, and all static assets get hosted on a cdn while the core app is deployed to heroku. Right now a build results in relative paths like

/static/media/icon.79190308.png

when we need paths like

https://img.s3-us-east-1.amazonaws.com/static/media/icon.79190308.png

EDIT: Found this issue on create-react-app that discusses the above use case: https://github.com/facebookincubator/create-react-app/issues/936 . We'll have to see if the corresponding PR addresses it.

@sjmueller FYI the related PR is this one https://github.com/facebookincubator/create-react-app/pull/937, but I can't see how that will help this issue.
Probably you can just use an environment variable and call it a day, if my understanding of your needs are not wrong.

I have created a webpack loader, which takes care of appropriate scaling usage too(based on device pixel ratio). Check it out, and feel free to contribute. This is my first npm package so please if something is wrong you can tell me :)
https://www.npmjs.com/package/react-native-web-image-loader

I have plans to release a webpack plugin too which is a resolver. For me, image suffixes were pain in the *ss since some module required its asset (./assets/back.png) but even the 1x scaling had a suffix(./assets/[email protected]) and webpack didn't seem to find it.

Cool. This library already has modules that provide the pixel ratio so the wrapper module the plugin injects seems redundant. Interested in moving development of the plugin into this repo?

I did not notice that you had already take care of pixel ratios. I think we could move it here since an out-of-box solution is better than installing separate packages. How would you like to impement it here?

well I did not get any response from necolas, and currently I'm too busy with office work.

I've used @psychoo118 's package and it works well. Since using react-native-web requires editing your webpack config already why don't we just update the documentation to refer to the new webpack loader? I know @necolas mentioned that there is some redundancy in getting the pixel ratio but its only a couple of lines.

FYI, Haul includes an assetLoader and AssetResolver that _mostly_ works with react-native-web. There are caveats, like it expects an AssetRegistry module which react-native-web doesn't have, but you can fake with your own implementation through some webpack trickery.

This issue somewhat relates to callstack-io/haul#132, where they are discussing making Haul compatible with react-native-web.

I wonder if some of webpack-specific parts of Haul (like the image loader with scale support) could be published as npm packages for use outside of Haul. But if we can get Haul working with RNW too that would be great.

Relevant parts of React Native to work into RNW:

I think this functionality is possibly best introduced as a babel-plugin, that way it could be used with Metro, Haul, or Webpack rather than being tied to webpack. Not sure what Metro does to support this in React Native

@psychoo118 you save our lives :D!

glad to hear that :)

Hei! How does this work? It doesn't seem automatic yet and this issue is 2 years old so I'm curious what I can do to get them crisp images 馃槑

Personally I think this feature is better suited to a universal <picture /> element or props like srcset="..." sizes="..." being added to the upstream Image component. Adding new bundler functionality to every bundler seems like a far more complex task and makes it harder to keep RNW bundler agnostic.

I understand the reasoning, I get it, but I disagree. It sucks that I can't load images that contain an @ in the name. Now I renamed all images and I always have to load the correct one myself, even on mobile.

I think that at least having a clear guide on this would be much better than that.

Hello, it's me again. I still don't have a satisfying approach for this. What I am doing now is renaming all images to be image.png, image_2x.png, image_3x.png and then using the PixelRation.get() to use the correct image. The problem with this is that the expo service worker downloads all the images for all densities and it makes things quite heavy.

With this issue still being open, I am curious what the next steps should be. I don't mind doing the work, I just need some pointers

Closing this because no one has really worked on the bundler side for 4 years and the teams working on RN packagers should figure this out

Was this page helpful?
0 / 5 - 0 ratings