Created by: gaearon
I’ve been thinking about this for a few weeks and I have a solution for static assets that satisfies me. It is not exactly what we discussed before so please bear with me and give it five minutes.
Problem
Currently, the only way to use an image or another resource is to import
it from JavaScript.
This works really well for most use cases because Webpack takes care of:
- Raising a compilation error if the file is missing.
- Adding a content hash to the filename so if file changes, browser cache is busted.
However there are a few problems with this approach. Let me group them into a few buckets:
-
[ROOT-NAMES]
Some files (e.g.favicon.ico
ormanifest.json
) must exist at top level. Default approach of putting them intostatic/
and adding a content hash doesn’t work for them and we have to add exceptions. (https://github.com/facebookincubator/create-react-app/issues/558, https://github.com/facebookincubator/create-react-app/issues/697) -
[ANY-ATTRIBUTES]
HTML needs to be able to refer to asset paths inside any attributes. For example, some<meta>
tags may want to refer to an image URL in theircontent
, but we can’t be sure which ones. (https://github.com/facebookincubator/create-react-app/issues/618) -
[DYNAMIC-FILES]
Sometimes you have hundreds of image files that rarely change, and you want to do something dynamic instead of making hundreds of imports. (https://github.com/facebookincubator/create-react-app/issues/585#issuecomment-244809341) -
[SKIP-BUNDLER]
Sometimes you really want to load some JS or CSS independently of the bundler, for example, to show a spinner, or to use a script that somehow breaks Webpack. (https://github.com/facebookincubator/create-react-app/issues/625, https://github.com/facebookincubator/create-react-app/issues/573#issuecomment-245934473)
This means we need an alternative way of including some assets into the build.
Constraints
-
[PROD-WORKS]
If it works in development, it must work in production too. -
[HOMEPAGE]
User may specify a customhomepage
inpackage.json
, and we should respect that. -
[ROUTING]
User may use client-side routing, and app may be served from an arbitrary subpath. -
[SECRETS]
It should be obvious to the user which files or folders will end up in the build output. -
[PIT-OF-SUCCESS]
There should be one relatively obvious way to do it.
Prior Solutions
Implicitly Serve Everything in Development
This is how it worked prior to 0.4.0. It wasn’t intentional, but WebpackDevServer defaults to this behavior.
This means <img src="/src/logo.svg">
or <link rel="shortcut icon" href="/favicon.ico">
happened to work in development. However once you compile them with npm run build
, the links would be broken because module system knew nothing about those files.
Verdict: solves all problems but violates [PROD-WORKS]
.
Auto-Detect Assets in HTML
This is the current solution that was added as a stop-gap measure in 0.4.0. Any <link href>
attribute is being parsed, and relative URLs like ./favicon.ico
get processed through Webpack.
Verdict: does not violate the constraints but also does not solve [ROOT-NAMES]
unless we keep an extensive whitelist. JSON files make it nearly impossible to keep a whitelist like this because they should be treated differently depending on whether they’re imported from HTML or JS (and Webpack doesn’t support this). Similarly, [ANY-ATTRIBUTES]
, [DYNAMIC-FILES]
, and [SKIP-BUNDLER]
are unsolved.
Introduce a Static Folder and Serve It
This is the approach described in https://github.com/facebookincubator/create-react-app/pull/226. If an additional /static
folder exists, it is served under /static
and is merged with /build/static
on build.
Verdict: it does not solve [ROOT-NAMES]
unless we merge static/*
directly into build/*
output. However, in that case, for something to be served from /static/
, you’d have to create static/static
which is confusing and violates [PIT-OF-SUCCESS]
. It also violates [PIT-OF-SUCCESS]
because to refer to something, you’d have to use /static/*
paths in your HTML, but to many people, this would imply that /src/*
or /img/*
would work the same way. Most importantly, it violates [HOMEPAGE]
because there is no way for us to fix /static/
path to have the right absolute prefix without parsing HTML and applying some brittle heuristics. It would also be confusing that /static/
paths get “auto-fixed” in HTML while other paths are kept intact. Finally, if we proposed that user types relative static/*
in HTML instead, it would violate [ROUTING]
.
Proposed Solution
There is a new top-level folder called public
. We move index.html
and favicon.ico
there.
We introduce a new concept called “public URL”. It roughly corresponds to “public path” in Webpack.
It is already configurable via homepage
field in package.json
, but now we also expose it to the user.
In HTML:
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
In JavaScript:
render() {
// Escape hatch! Normally you’d just import them.
const src = `${process.env.PUBLIC_URL}/img/team_/${this.props.teamId}.png`;
return <img src={src} alt='Team' />;
}
Only files inside public
folder are accessible by public URL. You can’t refer to things from node_modules
or src
. The public
folder contents gets merged with the build
output.
Let’s see how this proposal addresses each of the problems and constraints.
Addressed Problems
[ROOT-NAMES]
Some files (e.g.favicon.ico
ormanifest.json
) must exist at top level. Default approach of putting them intostatic/
and adding a content hash doesn’t work for them and we have to add exceptions. (https://github.com/facebookincubator/create-react-app/issues/558, https://github.com/facebookincubator/create-react-app/issues/697)
With public
folder, you have full control over how to call your files.
[ANY-ATTRIBUTES]
HTML needs to be able to refer to asset paths inside any attributes. For example, some<meta>
tags may want to refer to an image URL in theircontent
, but we can’t be sure which ones. (https://github.com/facebookincubator/create-react-app/issues/618)
With %PUBLIC_URL%
, you have full control over attributes.
[DYNAMIC-FILES]
Sometimes you have hundreds of image files that rarely change, and you want to do something dynamic instead of making hundreds of imports. (https://github.com/facebookincubator/create-react-app/issues/585#issuecomment-244809341)
We expose process.env.PUBLIC_URL
so you can do that now.
[SKIP-BUNDLER]
Sometimes you really want to load some JS or CSS independently of the bundler, for example, to show a spinner, or to use a script that somehow breaks Webpack. (https://github.com/facebookincubator/create-react-app/issues/625, https://github.com/facebookincubator/create-react-app/issues/573#issuecomment-245934473)
Again, since we don’t process files in public
folder with webpack, this will work.
Addressed Constraints
[PROD-WORKS]
If it works in development, it must work in production too.
The public
folder gets merged with the build output, so references both from HTML and JS will keep working in production.
[HOMEPAGE]
User may specify a customhomepage
inpackage.json
, and we should respect that.
Both %PUBLIC_URL%
and process.env.PUBLIC_URL
take homepage
into account, as they directly correspond to publicPath
used by Webpack.
[ROUTING]
User may use client-side routing, and app may be served from an arbitrary subpath.
Both %PUBLIC_URL%
and process.env.PUBLIC_URL
provide safety against that, as they are absolute and know about homepage
.
[SECRETS]
It should be obvious to the user which files or folders will end up in the build output.
The mental model is simple: only public
files are merged with build output.
[PIT-OF-SUCCESS]
There should be one relatively obvious way to do it.
You can’t accidentally refer to a file in src
or node_modules
because it will not work. We show two preferred methods out of the box: import
ing images in App.js
, and using %PUBLIC_URL%
in index.html
. Using process.env.PUBLIC_URL
is an escape hatch so we will only use mention in documentation for people who need it.
Overall I feel like it’s a solid, explicit solution, and I intend to release it as part of 0.5.0.