I have a problem implementing server side rendering. I'm using react + typescript frontend and backend written in node.js with typescript as well. I cannot find a way to include styles in my page when it's send from the server for the first time (before js loads). What I've tried is bundling all my styles into single main.css file and then using link tag in html file send from server, but in this scenario it takes as much time to load css, as it takes to load js (what is the purpose of SSR here ?). Also i'm trying to avoid styling my component inline. Any help would be apprecieted. Here I'm sharing some of my current code in case it might be helpful in some way.
App Component <- optimally i would like to see styled app as soon as browser reads .html file (i guess that's
the purpose of SSR)
export default class App extends React.Component<State, Props> {
state: State = {
}
render = () => {
return (
<div className="app">
APP COMPONENT !!!
</div>
)
}
}
Client code entry (ClientEntry.tsx)
import React from "react";
import ReactDOM from "react-dom";
import App from "../shared/containers/App";
import { BrowserRouter } from 'react-router-dom';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("app")
);
Webpack config
const webpack = require("webpack");
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const autoprefixer = require("autoprefixer");
const WebpackShellPlugin = require('webpack-shell-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const clientConfig = {
stats:'errors-only',
entry: "./src/client/ClientEntry.tsx",
output: {
path: __dirname,
filename: "./build/bundle.js"
},
devtool: "cheap-module-source-map",
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx']
},
module: {
rules: [
{
test: [/\.svg$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
loader: "file-loader",
options: {
name: "build/media/[name].[ext]",
publicPath: url => url.replace(/build/, "")
}
},
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
use: [
{
loader: "css-loader",
options: { importLoaders: 1 }
},
{
loader: "postcss-loader",
options: { plugins: [autoprefixer()] }
}
]
})
},
{
test: [/tsx?$/],
exclude: /(node_modules)/,
loader: "ts-loader",
}
]
},
plugins: [
new ExtractTextPlugin({
filename: "build/css/[name].css"
}),
new WebpackShellPlugin({
onBuildEnd: ['nodemon build/server.js']
})
]
};
const serverConfig = {
optimization: {
minimize: true,
minimizer: [new TerserPlugin({ terserOptions: { mangle: false } })],
},
stats:'errors-only',
entry: "./src/server/index.tsx",
target: "node",
output: {
path: __dirname,
filename: "./build/server.js",
libraryTarget: "commonjs2"
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx']
},
devtool: "cheap-module-source-map",
module: {
rules: [
{
test: [/\.svg$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
loader: "file-loader",
options: {
name: "build/media/[name].[ext]",
publicPath: url => url.replace(/build/, ""),
emit: false
}
},
{
test: /\.css$/,
use: [
{
loader: "css-loader/locals"
}
]
},
{
test: [/tsx?$/],
exclude: /(node_modules)/,
loader: "ts-loader",
}
]
}
};
module.exports = [clientConfig, serverConfig];
Part of index.tsx (express main file)
server.get('/*', (req, res) => {
const body = renderToString(
<StaticRouter location={req.url}>
<App />
</StaticRouter>
);
res.send(
html({
body
})
);
})
And function that converts to html
const html = ({ body }: { body: string }) => `
<!DOCTYPE html>
<title> my app</title>
<html>
<head>
<link rel="stylesheet" href="/css/main.css">
</head>
<body style="margin:0">
<div id="app">${body}</div>
</body>
<script src="/bundle.js" defer></script>
</html>
`;
export default html;
I'm assuming you're using express, which has a built in method to serve static files (such as images, css, etc...) you just need to show it the path as follow
server.use('static', express.static('css'))
which basically serves all files inside the css folder under the link
http://${host}/static/
if you want to serve static images you can do the following.
server.use('images', express.static('images'))
and then you can refer to any image file inside your code using the following.
<img src="/images/<image_name>" />
You can read more about it here.
express static files
Related
Hi I'm building a React app with SSR. Server and client part both written in typescript and transpiled separately.
Here the app structure for the better understanding:
Here the simplified webpack configs for the server and client bundles:
// webpack.client.js
module.exports = {
mode: "development",
resolve: {
modules: ["src", "static", "node_modules"],
extensions: [".ts", ".tsx", ".js", ".jsx"],
},
entry: [
"./src/client/index.tsx"
],
output: {
filename: "bundle.js",
path: PUBLIC_PATH,
},
module: {
rules: [
{
test: /\.tsx?$/,
use: [
{
loader: "ts-loader",
}
],
},
]
},
plugins: [
new webpack.DefinePlugin({ IS_SERVER: false })
]
};
Server config looks pretty much the except for the target and externals
//webpack.server.js
const config = {
mode: "development",
resolve: {
modules: ["src", "static", "node_modules"],
extensions: [".ts", ".tsx", ".js", ".jsx"],
},
externals: [webpackNodeExternals()],
target: 'node',
entry: [
"./src/server/index.ts"
],
output: {
filename: "bundle.js",
path: SERVER_BUILD_PATH
},
module: {
rules: [
{
test: /\.tsx?$/,
use: [
{
loader: "ts-loader",
}
],
},
]
},
plugins: [
new webpack.DefinePlugin({ IS_SERVER: true })
]
};
In the server code I got a renderer function which renders React application to string.
// renderer.tsx
import React from "react";
import { renderToString } from "react-dom/server";
import { App } from "client/App";
const html = (app) => `
<html>
<head>
</head>
<body>
<div id="root">${app}</div>
<script src="/public/bundle.js"></script>
</body>
</html>
`;
export async function renderer(req) {
const app = renderToString(<App />);
return html(app);
}
Which then returns to the client by the express server.
//index.ts
app.get("*", async (req, res) => {
const content = await renderer(req);
res.send(content);
});
As you see both parts need to transpile React app. The question is - how can I reuse transpiled client code in the server bundle so that server config only need to transpile index.ts and renderer.tsx?
You can use webpack-merge package to do that
Here is my example
const merge = require('webpack-merge');
const baseConfig = require('./webpack.config.js');
const webpack = require("webpack");
module.exports = merge(baseConfig, {
plugins: [
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify('development'),
'BASE_URL': JSON.stringify('http://localhost:5000/')
}
})
],
watch: true
});
This can be easily done using resolve.alias:
https://webpack.js.org/configuration/resolve/#resolvealias
Simply move the helpers directory up in the app's directory hierarchy and import from it.
In your case, I would also like to redesign the app using a single webpack.config.js file at the root level of your app in which you can combine the client and server configurations using the multi-compiler feature and respect the principle "Do not repeat yourself":
https://stackoverflow.com/a/43689505/2569746
I have a variable in my vars.scss that I want to access from Javascript in root/app/app.vue.
root/app/scss/vars.scss
:export {
cursor: #fff;
}
root/app/app.vue
<template>
<div id="yes">
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import colors from '#/scss/vars.scss';
export default Vue.extend({
mounted() {
console.log(colors.cursor);
},
});
</script>
<style >
</style>
I have read approximately 30 different stackoverflow questions that appear to be dealing with the similar problem of importing variables into the style block of the .vue file, as well as the identical problem of importing the variables directly into the Javascript code. As a result, my webpack.config.js looks like the following:
root/webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const env = process.env.NODE_ENV
module.exports = {
entry: './app/index.ts',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'staticfiles')
},
resolve: {
extensions: [ '.ts', '.js', '.vue', '.scss', '.sass'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'#': path.resolve(__dirname, '/app/')
}
},
plugins: [
new HtmlWebpackPlugin(),
new CleanWebpackPlugin(),
new webpack.HotModuleReplacementPlugin(),
new VueLoaderPlugin()
],
module: {
rules: [
{
enforce: 'pre',
test: /\.(js|vue|ts)$/,
loader: 'eslint-loader',
exclude: /node_modules/
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
// Since sass-loader (weirdly) has SCSS as its default parse mode, we map
// the "scss" and "sass" values for the lang attribute to the right configs here.
// other preprocessors should work out of the box, no loader config like this necessary.
'scss': 'vue-style-loader!css-loader!sass-loader',
'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax',
}
// other vue-loader options go here
}
},
{
test: /\.css$/,
use: [
'vue-style-loader',
'css-loader'
]
},
{
test: /\.tsx?$/,
loader: 'ts-loader',
exclude: /node_modules/,
options: {
appendTsSuffixTo: [/\.vue$/],
}
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: 'file-loader',
options: {
name: '[name].[ext]?[hash]'
}
},
{
test: /\.s(a|c)ss$/,
use: [ {
loader: "style-loader",
options: {
sourceMap: env === 'development',
}
}, {
loader: "css-loader",
options: {
sourceMap: env === 'development',
}
}, {
loader: "sass-loader",
options: {
sourceMap: env === 'development',
}
},
'vue-style-loader'],
}]
}
};
I have also tried, in the test: /\.s(a|c)ss$/ section, to put vue-style-loader at the beginning of the array.
I have tried many combinations of filenames when attempting to import the .scss file, such as relative (../scss/vars.scss), removing the extension, using .css as an extension, etc.
The error I get is:
ERROR in /home/Documents/application/app/app.vue.ts
[tsl] ERROR in /home/Documents/application/app/app.vue.ts(10,28)
TS2307: Cannot find module '#/scss/vars.scss'.
My question:
In a project that uses vue-style-loader and vue-loader to build .vue files with webpack, how can I import .scss variables into the <script> portion of a .vue file? (please note - I am NOT attempting to import them into the <style> section of the .vue file)
An example based on my comment:
SCSS fragment:
$foo: #333;
body {
--variable-foo: $foo;
}
And then anywhere in the JavaScript
const value = document.body.style.getPropertyValue("--variable-foo");
console.log(value); // outputs "#333"
I am trying to use react router to have dynamic profile pages, preferably having it in slashes rather than like a ? parameter wildcard in the URL. I'm looking for like /profile/{username}/ rather than /profile?user={username} if that makes sense.
Here's the route that I'm using to try and achieve this;
<Route path="/profile/:name/" component={Profile}/>
But when I try and go to this route as in `/profile/jeff/' or anything it returns a bundle.js (webpack'd) that is a blank HTML template, which is unexpected to be in the bundle.js and throws an error. Any idea's how I can fix this? Thanks.
Here is the bundle.js that gets returned;
<html>
<body style="margin:0px; padding: 0px;">
<link rel="stylesheet" href="styles.css">
<link href="https://fonts.googleapis.com/css?family=Ubuntu" rel="stylesheet">
<div id = "root" style="margin:0px; padding: 0px;"></div>
<script src="bundle.js"></script>
</body>
Profile component;
import React from 'react';
import styles from './profile.scss';
import astyles from './allpages.scss';
export default class Profile extends React.Component{
render(){
console.log("hello!");
const { match, location, history } = this.props
console.log(location);
console.log(match);
console.log(history);
return(
<div className = {styles.borderContainer}>
<p> {this.props.param.name} </p>
</div>
)
}
}
Webpack config;
var webpack = require('webpack');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var path = require('path');
require('style-loader');
require('css-loader');
const loaders = {
css: {
loader: 'css-loader'
},
postcss: {
loader: 'postcss-loader',
options: {
plugins: (loader) => [
autoprefixer({
browsers: ['last 2 versions']
})
]
}
},
sass: {
loader: 'sass-loader',
options: {
indentedSyntax: true,
includePaths: [path.resolve(__dirname, './src/app')]
}
}
}
module.exports = {
context: __dirname,
entry: './src/app/index.js',
output: {
path: __dirname + '/dist/',
filename: 'bundle.js',
libraryTarget: 'umd'
},
devtool: "sourceMap",
module: {
loaders: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
exclude: /node_modules/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
// Could also be write as follow:
// use: 'css-loader?modules&localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader'
use: [
{
loader: 'css-loader',
query: {
modules: true,
localIdentName: '[name]__[local]___[hash:base64:5]'
}
},
'postcss-loader'
]
}),
},
{
test: /\.scss$/,
exclude: /node_modules/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
// Could also be write as follow:
// use: 'css-loader?modules&importLoader=2&sourceMap&localIdentName=[name]__[local]___[hash:base64:5]!sass-loader'
use: [
{
loader: 'css-loader',
query: {
modules: true,
sourceMap: true,
importLoaders: 2,
localIdentName: '[name]__[local]___[hash:base64:5]'
}
},
'sass-loader'
]
}),
},
],
},
plugins: [
new ExtractTextPlugin('styles.css'),
],
}
When you request /profile/jeff/ you serve the index.html you posted, and presumably that is done for every resource that doesn't exist on your server (as a fallback). In the index.html you have the following script tag:
<script src="bundle.js"></script>
This is a relative path. You are actually requesting /profile/jeff/bundle.js at this point and because that doesn't exist, you end up serving the index.html as the bundle, which is problematic because it's not valid JavaScript.
You should always use /bundle.js regardless of the current URL. Similarly you'd always want /styles.css for your CSS.
<link rel="stylesheet" href="/styles.css">
<script src="/bundle.js"></script>
I don't have a better source for this, but the development server needs to be configured to handle React Routers dynamic routes because it should always serve the same html file (index.html) because it's an SPA.
https://redux.js.org/docs/advanced/UsageWithReactRouter.html
Edit:
Particularly, I think you are missing this in your webpack config
devServer: {
historyApiFallback: true
}
Edit 2:
For ExpressJS you would need something like this,
app.get('/*', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'))
})
It looks like it's your stylesheet inclusion. My understanding is that you should be using a babel import statement in your component, not link tags, or they will 404 causing the error you are seeing.
I need to render React component in my view (html file). I'm using webpack 1 and getting error of component undefined. I tried to use window.component, but it didn't work too. If I use RenderDOM inside my component, all works well.
My component:
export class SmallCart extends BaseComponent {
...
render() {...}
}
Webpack 1 config:
var ExtractTextPlugin = require("extract-text-webpack-plugin");
var BowerWebpackPlugin = require('bower-webpack-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin');
var webpack = require('webpack');
module.exports = {
devtool: 'source-map',
entry: {
...
myComponent: "./wwwroot/js/es6/SmallCart/SmallCart.jsx",
...
},
output: {
path: __dirname + "/dist",
filename: '[name].js',
chunkFilename: '[id].chunk.js'
},
module: {
loaders: [
{
test: /\.(png|jpg|gif|ico)$/,
loader: "file-loader?name=assets/[name]-[hash:6].[ext]"
}, {
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
loader: "url-loader?limit=10000&minetype=application/font-woff&name=assets/[name]-[hash:6" +
"].[ext]"
}, {
test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
loader: "file-loader?name=assets/[name]-[hash:6].[ext]"
}, {
test: /\.scss$/i,
loader: ExtractTextPlugin.extract(['css-loader?-autoprefixer!postcss-loader', 'sass'])
}, {
test: /\.css$/i,
loader: ExtractTextPlugin.extract(['css-loader?-autoprefixer!postcss-loader'])
}, {
test: /\.(js|jsx)$/,
exclude: /(node_modules|bower_components)/,
loader: 'babel-loader',
query: {
presets: ['es2015', 'react']
}
}
]
},
progress: true,
resolve: {
modulesDirectories: ['node_modules'],
extensions: ['', '.json', '.js']
},
externals: {
jquery: "jQuery",
react: "React",
reactDOM: "ReactDOM"
},
plugins: [
new webpack.ProvidePlugin({'window.Nette': 'nette-forms', 'window.jQuery': 'jquery', jQuery: 'jquery', $: 'jquery'}),
new webpack
.optimize
.CommonsChunkPlugin({
filename: "commons.js", name: "commons",
}),
new ExtractTextPlugin("frontend.css"),
]
}
And in my view I have this:
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.5.4/react.js" charset="utf-8"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.5.4/react-dom.min.js" charset="utf-8"></script>
<script type="text/javascript" src="/dist/commons.js" charset="utf-8"></script>
<script type="text/javascript" src="/dist/SmallCart.js" charset="utf-8"></script>
<script type="text/javascript">
window.data_for_react = {};
ReactDOM.render(React.createElement(SmallCart, { dataSource : data_for_react}, document.getElementById('box-to-rendering')));
</script>
But in render method is component undefined.
What is wrong? Is possible render component in view?
Thank you for your time.
EDIT
Ok, now I try use window.SmallBasket and webpack config update to:
...
new webpack
.optimize
.CommonsChunkPlugin({
names: [
"commons"
],
minChunks: Infinity
}),
...
And it works. But I still cannot solve it without window property.
Try putting all your react code from your view in something like this:
document.addEventListener('DOMContentLoaded', () => {
//... your ReactDOM.render stuff here
};
You need to wait for the DOM elements to load before you can getElementById.
I have a very simple React component, that is supposed to display an image.
I am also using Webpack for bundling.
It's probably worth noting that I am using ReactJS.NET.
Although the webpack bundle builds properly, and the .jpg generated by webpack is viewable (using Windows Photo Viewer, for example), the image does not display in my View.
When I take a peek into inspector, the html structure is built properly, but I am getting:
"Could not load the image" - when I hover over the image path.
I made sure that the image path is correct.
Below is my react component:
var React = require('react');
var BackgroundImg = require('./Images/img_fjords.jpg');
class Login extends React.Component {
render() {
return (
<img src={BackgroundImg} />
);
}
componentDidMount() {
console.log("Mounted");
}
}
module.exports = Login;
Webpack config:
var path = require('path');
var WebpackNotifierPlugin = require('webpack-notifier');
module.exports = {
context: path.join(__dirname, 'App'),
entry: {
server: './server',
client: './client'
},
output: {
path: path.join(__dirname, 'Built/'),
publicPath: path.join(__dirname, 'Built/'),
filename: '[name].bundle.js'
},
plugins: [
new WebpackNotifierPlugin()
],
module: {
loaders: [
{ test: /\.css$/, loader: "style-loader!css-loader" },
{
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
loader: "url-loader?limit=10000&mimetype=application/font-woff"
},
{ test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "file-loader" },
{ test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192' },
{ test: /\.jsx$/, loader: 'jsx-loader?harmony' }
]
},
resolve: {
// Allow require('./blah') to require blah.jsx
extensions: ['.js', '.jsx']
},
externals: {
// Use external version of React (from CDN for client-side, or
// bundled with ReactJS.NET for server-side)
react: 'React'
}
};
The problem was solved thanks to help from #Luggage.
The webpack.config was wrong, it should have been:
output: {
path: path.join(__dirname, 'Built/'),
publicPath: 'Built/',
filename: '[name].bundle.js'
},
Well, I can't see your webpack config, but I'm assuming your using all the correct loaders (file-loader, extract-loader, html-loader, url-loader)? The way I handle it is using the webpack-copy-plugin to copy over my images folder so relative paths still work.