How can I programmatically render a react app in gulp and node 12?
I taking over and upgrading an old react (0.12.0) app to latest. This also involved upgrading to ES6. The react code itself is done, but we also need to prerender the application (The app is an interactive documentation and must be crawled by search engines).
Previously, the gulp build process ran browserify on the code and then ran it with vm.runInContext:
// source code for the bundle
const component = path.resolve(SRC_DIR + subDir, relComponent);
vm.runInNewContext(
fs.readFileSync(BUILD_DIR + 'bundle.js') + // ugly
'\nrequire("react").renderToString(' +
'require("react").createElement(require(component)))',
{
global: {
React: React,
Immutable: Immutable,
},
window: {},
component: component,
console: console,
}
);
I am suprised it worked before, but it really did. But now it fails, because the source uses ES6.
I looked for pre-made solutions, but they seem all targeting old react versions, where react-tools was still around.
I packaged the special server-side script below with browserify & babel and then ran it using runInNewContext. It does not fail but also not output any code, it just logs an empty object
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './index';
const content = renderToString(<App />);
I found tons of articles about "server-side rendering", but they all seem to be about rendering with express and use the same lines as the script above. I can't run that code directly in gulp, as it does not play well with ES6 imports, which are only available after node 14 (and are experimental).
I failed to show the gulp-browserify task, which was rendering the app component directly, instead of the server-side entrypoint script above. In case anyone ever needs to do this, here is a working solution.
Using vm.runInNewContext allows us to define a synthetic browser context, which require does not. This is important if you access window anywhere in the app.
src/server.js:
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './index';
const content = renderToString(<App />);
global.output = content;
above script serves as entry point to browserify. Gulp task to compile:
function gulpJS() {
const sourcePath = path.join(SRC_DIR, 'src/server.js');
return browserify(sourcePath, { debug:true })
.transform('babelify', {
presets: [
["#babel/preset-env", { targets: "> 0.25%, not dead" }],
"#babel/preset-react",
],
})
.bundle()
.pipe(source('server_output.js'))
.pipe(buffer())
.pipe(sourcemaps.init({loadMaps: true}))
.pipe(sourcemaps.write('.'))
.pipe(dest(BUILD_DIR));
}
The generated file can now be used by later tasks, e.g. to insert the rendered content into a HTML file.
const componentContent = fs.readFileSync(path.join(BUILD_DIR, 'server.js'));
const context = {
global: {
React: React,
Immutable: Immutable,
data: {
Immutable
},
},
window: {
addEventListener() { /* fake */ },
removeEventListener() { /* fake */ },
},
console,
};
vm.runInNewContext(componentContent, context);
const result = context.global.output;
Related
Is there any way of building my svelte or react application in a way, that the three.js module (which I usually import using npm) will be declared as a script tag which will call the module from a CDN? I would like to keep the advantages of a framework but also be able to reduce my final bundle size, since most of my bundle contains three code.
Thank you for your wisdom
There are two ways to go about your goal of reducing bundle size:
Importing from a CDN (your suggestion)
Code-splitting
Importing from a CDN
To keep semantics of ESModules, you may simply replace your current three.js imports with a URL from an npm CDN, like unpkg:
Pros
Cons
No extra configuration needed
Slower to load, as browser needs to spin up new connections to access third-party CDN
Asynchronously
<script>
// App.svelte
import('https://unpkg.com/three#0.133.1/build/three.min.js').then(({ default: THREE }) => {
// your code here
});
</script>
Synchronously
Note: Importing like this blocks the rest of your script from loading while three.js is downloading, which defeats the purpose of the whole shebang. It's just here for completeness
<script>
// App.svelte
import { default as THREE } from 'https://unpkg.com/three#0.133.1/build/three.min.js';
// your code here
</script>
Code-splitting
This method takes advantage of the fact that you're already using a bundler (probably rollup, vite, or webpack). This answer will focus on rollup as it's the default used in svelte's examples.
Pros
Cons
Faster to load, as browser can use existing connections to access first-party resources
More complicated to get set up
Asynchronously
In your rollup.config.js file, ensure output.format is set to 'esm' & output.dir is set instead of output.file
// rollup.config.js
import svelte from 'rollup-plugin-svelte';
import resolve from '#rollup/plugin-node-resolve';
import commonjs from '#rollup/plugin-commonjs';
import postcss from 'rollup-plugin-postcss';
const production = !process.env.ROLLUP_WATCH;
export default {
input: 'src/index.js',
output: {
sourcemap: !production,
format: 'esm',
name: 'app',
dir: 'public',
},
plugins: {
// your plugins
svelte({
compilerOptions: {
dev: !production,
},
}),
postcss({
extract: 'bundle.css',
}),
resolve({
browser: true,
dedupe: ['svelte'],
}),
commonjs(),
}
}
<script>
// App.svelte
import('three').then(({ default: THREE }) => {
// your code here
});
</script>
Note: There is no synchronous way due to how code-splitting is evaluated at compile time. Plus it doesn't make much sense to do it like that anyways.
Yes, you can do the following:
In your "index.html" file, you can import the js file from a CDN as follow:
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
Then, in the file where you want to use it, which could be a React component for instance, you can do the following:
const THREE = window.THREE;
Which would replace your import statement, which would have been import * as THREE from "three"; or import THREE from "three";
I'm developing a plugin feature in my next.js react app in which I have to dynamically import components from a bunch of given modules.
component.tsx :
const MyComponent = (props) => {
useEffect(() => {
const pluginNames = ['test-component'];
pluginNames.forEach(async (name) => {
try {
const plugin = await import(name);
} catch(err) {
// I don't want an unvalid plugin to crash my app
console.warn(err);
}
});
}, []);
// returns any html template
}
but when I run my code, I get the following error:
it seem to clearly indicate that the plugin is not found despite it's installed.
From what I understood, it happens because webpack doesn't pack up the dynamically imported plugins in my webpack config. Is there a way to tell webpack to include the specified modules (considering they are fixed since the startup of the app).
Possible solutions:
create a js file importing all modules and tell webpack to inject it into the bundle page
configure webpack so it adds the required modules
This is what the react-hot-loader DOCs says:
https://www.npmjs.com/package/react-hot-loader
Note: You can safely install react-hot-loader as a regular dependency instead of a dev dependency as it automatically ensures it is not executed in production and the footprint is minimal.
Even though it says that. My goals are:
I want to remove react-hot-loader from my production bundle.
And I also want a single App.js file. That should work for DEV and PROD.
The only command that I have related to react-hot-loader is inside of my App.js file:
App.js
import { hot } from 'react-hot-loader/root';
import React from 'react';
import Layout from './Layout/Layout';
function App() {
console.log('Rendering App...');
return(
<Layout/>
);
}
export default process.env = hot(App);
If I run it just like this, I end up with the following line on my app.js transpiled and bundled file:
/* WEBPACK VAR INJECTION /(function(process) {/ harmony import / var react_hot_loader_root__WEBPACK_IMPORTED_MODULE_0__ = webpack_require(/! react-hot-loader/root */ "wSuE");
That's expected.
But if I change my App.js file to:
AppV2.js
import { hot } from 'react-hot-loader/root'; // KEEPING THE IMPORT
import React from 'react';
import Layout from './Layout/Layout';
function App() {
console.log('Rendering App...');
console.log(window);
return(
<Layout/>
);
}
// export default hot(App); <--- COMMENTED OUT THE hot() LINE
export default App;
And I add this line to my webpack.config.js
webpack.config.js
plugins:[
new webpack.IgnorePlugin(/react-hot-loader/)
]
I'll end up with a new transpiled app.js file with this line:
*** !(function webpackMissingModule() { var e = new Error("Cannot find module 'react-hot-loader/root'"); e.code = 'MODULE_NOT_FOUND'; throw e; }());
Note: The first '***' chars in the line above don't really exist. I had to add them in order to the ! exclation mark to be shown in the quote. Don't know why but you can't start a quote with an exclamation mark.
QUESTION
Isn't the IgnorePlugin supposed to completely ignore the react-hot-loader package? Why is it being marked as missing? See that it's not even being used on the code (since I've commented out the hot() call).
Ignore Plugin only excludes that particular module in bundle generation. However it will not remove the references to the module from your source code. Hence your webpack output is throwing that error.
One way of bypassing this error is to use the DefinePlugin to create a dummy stub for react-hot-loader. More on that here.
That said react-hot-loader itself proxies the children without any changes if the NODE_ENV is production. Check here and here. So in production mode apart from the hot() function call which directly returns your component, there is no other stuff that happens.
Another option could be:
// App.js
export default function AppFactory() {
if (process.env.NODE_ENV === "development") {
return hot(App);
} else {
return App;
}
}
// index.js:
import AppFactory from './App';
const App = AppFactory();
// ...
<App />
Now since webpack is creating bundles at build time, it knows if the mode is development or production (more on build modes) and should be able to eliminate the dead code with tree shaking and UglifyjsWebpackPlugin.
Make sure that if you are using Babel it's not transpiling your code to CommonJS - see Conclusion section, point 2 of the tree shaking page.
Pass the ambient mode to babel.
"scripts": {
"build-dev": "webpack --node-env development",
"build-prod": "webpack --node-env production",
},
I have a Gatsby site with a React component called ArticleBody that uses react-markdown to convert an article written in Markdown to a React tree.
As this is a bit of an expensive operation and a somewhat large component — and for SEO reasons — I'd like to pre-render ArticleBody at build time. However, I'd also like to load ArticleBody asynchronously in the client. Since the article body will already be included in the HTML, there's no rush to load and render the Markdown component in the client, so async should be fine.
How would I accomplish this? It's almost as if I want to have two different JS bundles — one bundle that loads ArticleBody synchronously, for the build, and one that loads it asynchronously, for the client. Is this possible in Gatsby?
Thanks!
Instead of React.lazy which is not supported, you can use loadable components. There is a Gatsby plugin to handle SSR correctly gatsby-plugin-loadable-components-ssr
Currently there is an issue with it since Gatsby 3.x, but there is a way to implement it yourself without the extra plugin. See the comment in the issue here. Also add the changes mentioned in the comment below of it.
I haven't tried this specific implementation yet, but it should work with the following steps:
npm install --save-dev #loadable/babel-plugin #loadable/server #loadable/webpack-plugin #loadable/component
gatsby-browser.js
import { loadableReady } from '#loadable/component'
import { hydrate } from 'react-dom'
export const replaceHydrateFunction = () => (element, container, callback) => {
loadableReady(() => {
hydrate(element, container, callback)
})
}
gatsby-node.js
exports.onCreateWebpackConfig = ({ actions, stage }) => {
if (
stage === "build-javascript" ||
stage === "develop" ||
stage === "develop-html"
) {
actions.setWebpackConfig({
plugins: [
new LoadablePlugin({
filename:
stage === "develop"
? `public/loadable-stats.json`
: "loadable-stats.json",
writeToDisk: true
})
]
});
}
};
gatsby-ssr.js
import { ChunkExtractor } from '#loadable/server'
import path from 'path'
const extractor = new ChunkExtractor({
// Read the stats file generated by webpack loadable plugin.
statsFile: path.resolve('./public/loadable-stats.json'),
entrypoints: [],
})
// extractor.collectChunks() will wrap the application in a ChunkExtractorManager
export const wrapRootElement = ({ element }) =>
extractor.collectChunks(element)
export const onRenderBody = ({ setHeadComponents, setPostBodyComponents }) => {
// Set link rel="preload" tags in the head to start the request asap. This will NOT parse the assets fetched
setHeadComponents(extractor.getLinkElements())
// Set script and style tags at the end of the document to parse the assets.
setPostBodyComponents([...extractor.getScriptElements(), ...extractor.getStyleElements()])
// Reset collected chunks after each page is rendered
extractor.chunks = []
}
If you have DEV_SSR enabled, you should not add stage === "develop-html". Otherwise, you are good.
Hope this summary of the issue's comments help to get you started.
everyone.
I have a trivial doubt on making vue components.
I don't want to use browserify or webpack , cause I am working in django and it has most of it's templates in static files , although I read this , which does describe how to take in account both ( but that's for some other day ).
Problem :
I am making a single file component which I have to import and use, using my router but I can't, as the import just doesn't happen.
My Hello.vue
<template>
Some HTML code here.
</template>
<script>
module.exports = {
data() {
return {
coin : []
}
},
beforeRouteEnter (to, from, next) {
axios.get('my-django-rest-api-url')
.then(response => {
next(vm => {
vm.data = response.data
})
})
}
}
</script>
I have it in the index.html file itself , no other .js file,
<script>
import Hello from '#/components/Hello.vue'
Vue.use(VueRouter);
const dashboard = {template:'<p>This is the base template</p>'};
const profile = {
template: '#profile_template',
data () {
return {
profile_details: []
}
},
beforeRouteEnter (to, from, next) {
axios.get('my-api-url')
.then(response => {
next(vm => {
vm.profile_details = response.data
})
})
}
}
const router = new VueRouter({
routes: [
{ path: '/', component: dashboard },
{ path: '/profile', component: profile },
{ path: '/hello', component: Hello }
]
});
new Vue({
router : router,
}).$mount('#app');
</script>
What all I've tried :
1.<script src="../components/Hello.js" type="module"></script> and removing the import statement as suggested here
Replacing my Hello.js's code with this : export const Hello = { ...
Making a Hello.js file and importing it like this import Hello from '../components/Hello.js';
Error :
**Mozilla ( Quantum 57.0.4 64 bit ) ** : SyntaxError: import declarations may only appear at top level of a module
**Chrome ( 63.0.3239.108 (Official Build) (64-bit) ) ** :Uncaught SyntaxError: Unexpected identifier
P.S. : I have tried these in various combinations
Not a Vue.js guru, but here are a few perspectives that might help you.
Module loading is still not supported on modern browsers by default, and you'd need to set special flags in order to enable it (which the users of your app probably won't do).
If you insist on using import and export, you'd need Webpack. And most certainly Babel (or any other ES6 transpiler, e.g. Buble) as well.
If you prefer module.exports, then you'd need Browserify. It enables support for CommonJS in browser environments.
If neither is doable, then your best bet is defining Vue components in global scope. You can split them across separate files, and import each with a <script> individually. Definitely not the cleanest approach.
Single file components typically go inside of .vue files, but either way they require vue-loader which can be added and configured (again) with a bundler.
Last option is to just use an existing setup in place, if there is any (is there?). If you already have RequireJS, UMD, or something similar in place, adjust your components to fit that. Otherwise, use <script>s.
You are trying to do something which is not possible. Vue Single file components are not supported as raw component file by web browsers. The single file component is supposed to be compiled.
Please see this for more:
https://v2.vuejs.org/v2/guide/single-file-components.html
In Webpack, each file can be transformed by a “loader” before being included in the bundle, and Vue offers the vue-loader plugin to translate single-file (.vue) components.
A vue single file component is first "translated" (compiled) to pure javascript code which is use-able by browsers.