Summary: Long question so here's the quick summary. I can run the commands ng build && ng run app-name:server and npm run dev:server successfully and see no errors on console whenever I try to connect to server. But page is loading infinitely. I have tried to apply NgxSsrTimeoutModule.forRoot({ timeout: 500 }) nothing changed. Made other changes but saw no progress. Really stuck at this point having headaches.
Hello,
I'm trying to convert a SPA Angular application to an SSR-compatible one. Application is a little bit chunky, so I couldn't create a new SSR rendered application and move the modules to that one, one by one. First, my versions are:
#angular-devkit/architect 0.1301.2
#angular-devkit/build-angular 13.1.2
#angular-devkit/core 13.1.2
#angular-devkit/schematics 13.1.2
#angular/cli 13.1.2
#angular/platform-server 13.1.3
#nguniversal/builders 13.1.1
#nguniversal/express-engine 13.0.2
#schematics/angular 13.1.2
rxjs 6.6.7
typescript 4.5.4
I have made some changes to get development server working. First change was on angular.json. I had error Angular Universal Configuration 'development' is not set in the workspace:
"serve-ssr": {
"builder": "#nguniversal/builders:ssr-dev-server",
"configurations": {
"production": {
"browserTarget": "appName:build:production",
"serverTarget": "appName:server:production"
},
"development": ...
},
"defaultConfiguration": "development"
},
First change was on angular.json. Referring to this issue
I have changed as this:
"serve-ssr": {
"builder": "#nguniversal/builders:ssr-dev-server",
"options": { "browserTarget": "appName:build", "serverTarget": "appName:server"},
"configurations": {
"production": {
"browserTarget": "appName:build:production",
"serverTarget": "appName:server:production"
}
}
},
Second change was on ServerModule.
We had an application starter basically an Angular service requesting an appConfig.json to make application configurations. I had Could not load file '../../assets/config/appConfig.json' which is an error thrown from that service. So basic ServerModule changed as this;
import { NgModule, Inject } from '#angular/core';
import { INITIAL_CONFIG, PlatformConfig, ServerModule } from '#angular/platform-server';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { NgxSsrTimeoutModule } from '#ngx-ssr/timeout';
#NgModule({
imports: [
AppModule,
ServerModule,
NgxSsrTimeoutModule.forRoot({ timeout: 500 })
],
bootstrap: [AppComponent],
})
export class AppServerModule {
constructor(#Inject(INITIAL_CONFIG) private config: PlatformConfig) {
this.config.useAbsoluteUrl = true;
}
}
Also solved the "window not defined" error by adding domino library
to the server code.
After changing this, I could successfully run the comman npm run dev:ssr. Take note that, before doing this, I could run the command ng build && ng run app-name:server but not npm run dev:ssr. And after that, I had a running Node server on my localhost:4200 but page is always in loading state, I could see nothing on network tab (Chrome Dev Tools), no client side application taking action and nothing on console (just says Messages received, value changed...). After all these, I implemented *appShellRender and *appShellNoRender directives, and just decided to render a loading animation in the server, nothing changed;
app.component.ts:
<div *appShellRender>
<app-loading-animation></app-loading-animation>
</div>
<div *appShellNoRender>
<ng-container appCardQuery appSystemIdTracker>
<mat-sidenav-container>
<mat-sidenav #sidenav>
<app-side-nav (closeSideNav)="sidenav.close()"></app-side-nav>
</mat-sidenav>
<mat-sidenav-content (scroll) = "onScroll($event)">
<ng-container *ngIf="isToolbarOn">
<app-toolbar (SideNavToggle)="sidenav.toggle()"></app-toolbar>
</ng-container>
<main>
<!-- <div style="background-color: black; width: 250px;height: 250px;" (click)="testClick()"></div> -->
<router-outlet></router-outlet>
</main>
<div class="spacer30"></div>
<ng-container *ngIf="isAppAboutOn">
<app-about></app-about>
</ng-container>
</mat-sidenav-content>
</mat-sidenav-container>
</ng-container>
</div>
server.ts:
import 'zone.js/dist/zone-node';
import { ngExpressEngine } from '#nguniversal/express-engine';
import * as express from 'express';
import { join } from 'path';
import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '#angular/common';
import { existsSync } from 'fs';
const domino = require('domino');
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const distFolder = join(process.cwd(), 'dist/appName/browser');
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
const win = domino.createWindow(indexHtml);
global['window'] = win;
global['document'] = win.document;
global['navigator'] = win.navigator;
global['screen'] = win.screen;
// Our Universal express-engine (found # https://github.com/angular/universal/tree/master/modules/express-engine)
server.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));
server.set('view engine', 'html');
server.set('views', distFolder);
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));
// All regular routes use the Universal engine
server.get('*', (req, res) => {
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
});
return server;
}
function run(): void {
const port = process.env['PORT'] || 4000;
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}
export * from './src/main.server';
Error Possibilities
I see no errors on console but there's obviously something wrong.
We're using AgmCoreModule which is an Angular component to use Maps on application. Also leaflet.
index.js:
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin="" />
<script src="https://unpkg.com/leaflet#1.7.1/dist/leaflet.js" integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" crossorigin=""></script>
These might be causing errors?
Related
I am using react as the client side and node.js as the server side, and using webpack to transpile. When I start the website on local host it is missing all of its styling and css. Any Ideas?
This is my frontend webpack that handles the transpiling of my frontend code
const path = require('path');
const nodeExternals = require('webpack-node-externals');
module.exports = {
target: 'node',
externals: [nodeExternals()],
entry: './frontend/src/App',
output: {
path: path.resolve(__dirname, 'backend/build'),
filename: 'bundle.js',
libraryTarget: 'commonjs2',
libraryExport: 'default'
},
module: {
rules: [
{
test: /\.jsx$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['#babel/preset-env', '#babel/preset-react']
}
}
},
{
test: /\.(png|jpe?g|gif)$/i,
use: [
{
loader: 'file-loader',
},
],
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['#babel/preset-env', '#babel/preset-react']
}
}
},
{
test: /\.css$/,
use: [ 'css-loader'],
sideEffects: true
}
]
}
};
This is my backend/server.js that handles server rendering
const express = require('express');
const React = require('react');
const { renderToString } = require('react-dom/server');
const App = require('../backend/build/bundle');
const { matchPath } = require('react-router-dom');
const app = express();
// This is the middleware that will handle all routes
app.get('*', (req, res) => {
// We create a context object that will be passed to the StaticRouter
const context = {};
// Render the app component to a string
const html = renderToString(
React.createElement(App, {
location: req.url,
context,
})
);
// If the app component set a context.url value, it means the user
// tried to access a protected route that they are not authenticated for.
// In this case, we redirect them to the login page.
if (context.url) {
res.redirect(context.url);
} else {
// Send the rendered HTML as the response
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="root">${html}</div>
<script src="/build/bundle.js"></script>
</body>
</html>
`);
}
});
app.listen(3000, () =\> {
console.log('Server is listening on port 3000');
});
This is my frontend App.js
import React from "react";
import { StaticRouter } from "react-router-dom/server";
import { Footer, Contact, Who, Header } from "./containers";
import { Navbar, Background } from "./components";
import { Route, Routes } from "react-router-dom";
import "./App.css";
const App = (props) =\> {
return (
\<StaticRouter location={props.location} context={props.context}\>
\<Navbar /\>
\<Routes\>
\<Route exact path="/" element={\<Header /\>} /\>
\<Route path="/about" element={\<Who /\>} /\>
\<Route path="/contact" element={\<Contact /\>} /\>
\</Routes\>
\<Footer /\>
\</StaticRouter\>
);
};
export default App;
And this is my index.js
import React from 'react';
import ReactDom from 'react-dom';
import './index.css';
import App from './App';
ReactDom.render(
\<BrowserRouter\>
\<App /\>
\</BrowserRouter\>,
document.getElementById('root')
)
I have tried to add the styling-loader to the webpack config for the frontend but I end up getting an error where the document is not defined. The routing works fine, just no css or images, just basic html is being rendered`
I have been trying to learn how to SSR with React(without using Next.js) I've gotten to the point that I'm seeing the HTML rendered on the page but once I add the css imports to style the component babel start throwing errors (
C:\Users\Frozen\Desktop\ssr stuff\ssr4\src\App.css:1
body {
^
SyntaxError: Unexpected token '{'
)
.babelrc
{
"presets": ["#babel/preset-env", "#babel/preset-react"],
}
webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './build'),
filename: "bundle.js",
publicPath: "/"
},
devServer: {
port: 3010,
static: true,
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ['#babel/preset-env', "#babel/preset-react"]
}
},
},
{
test: /\.(css|scss)$/,
use: ["style-loader", "css-loader", "sass-loader"],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
publicPath: "/"
})
]
};
server.js
import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import App from './src/App.js';
import path from 'path';
import fs from 'fs';
const app = express();
app.get('/', (req, res) => {
console.log('in /');
fs.readFile(
path.resolve('./build/index.html'),
'utf8',
(err, data) => {
if (err) {
console.log(err);
return res.status(500).send('Internal Server Error');
}
const html = data.replace(
'<div id="root"></div>',
`<div id="root">${ReactDOMServer.renderToString(<App />)}</div>`
)
console.log(html)
return res.send(html);
}
);
});
app.use(express.static(path.resolve(__dirname,'build')));
app.listen(3000, () => {
console.log('server listening on port 3000')
})
I also fail to understand why do I have to put the app.use(static) below the app.get('/') aren't middlewares supposed to go one after another in order top to bottom (if I move the app.use(static) above the app.get('/') where I return the component I don't see the console.log('in /');
index.js
import React from "react";
import ReactDOM from 'react-dom'
import App from "./App";
const appElement = document.getElementById('root');
ReactDOM.hydrate(<App/>, appElement)
I run
npx webpack
npx babel-node server.js
And I get the css error (if I remove the css import I don't get it but I don't have css in the component)
Also is there a way to rebuild and run server.js once I change something ( like when you use create-react-app ) Also any other suggestions on what I can improve in the current setup will be much appreciated.
Install babel-plugin-css-modules-transform npm install --save-dev babel-plugin-css-modules-transform.Then include it in .babelrc "plugins": ["css-modules-transform"]
I have launched my first node.js app using express.js for the backend and react.js for the frontend. I have it deployed as a background service on a VM with Ubuntu 20.04. The app itself is pretty simple. It makes an API call, gets the latest data and displays it to the enduser in a web browser interface. I initially deployed it on a free Heroku account, where it works fine and always display the latest info from the API call. But when I deployed it on a private VM with Ubuntu 20.04, I noticed that it does not show the latest API info anymore, i.e. it makes the initial API call during npm start and then probably caches it on the server. So, if I come to the app the next day, all the data is from yesterday. To see the latest data, I have to kill the background service on Ubuntu and then do "npm start", which starts the server and makes the API call. I then see the latest data in the browser. This is my first time working with all this technology, so if you have had a similar issue with deploying on Ubuntu, please let me know how I can fix it.
Here is my package.json "scripts":
"scripts": {
"start:dev": "nodemon --exec babel-node server/server.js --ignore public/",
"build": "babel server --out-dir dist",
"start": "node dist/server.js &",
"dev": "webpack -w --mode=development",
"prod": "webpack --mode=production"
}
Here is my server.js code:
import config, { nodeEnv, logStars } from "./config";
import apiRouter from "./api";
import sassMiddleware from "node-sass-middleware";
import path from "path";
import AMdataPullDone from "./AMdataPullDone";
import express from "express";
const server = express();
server.use(
sassMiddleware({
src: path.join(__dirname, "sass"),
dest: path.join(__dirname, "public"),
})
);
server.set("view engine", "ejs");
server.get("/", (req, res) => {
AMdataPullDone.then(({ initialMarkup, initialData }) => {
res.render("index", {
initialMarkup,
initialData,
});
}).catch(console.error);
});
server.use("/api", apiRouter);
server.use(express.static("public"));
server.listen(config.port, config.host, () => {
console.info("Express listening on port ", config.serverUrl);
});
And here is my index.js file with the reach App:
import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";
ReactDOM.hydrate(
<App initialData={window.initialData} />,
document.getElementById("root")
);
AMdataPullDone.js code:
import React from "react";
import ReactDOMServer from "react-dom/server";
import App from "./src/components/App";
import generateToken from "./utils/generateToken";
import serverGetAMData from "./serverGetAMData";
const AMdataPullDone = new Promise((resolve, reject) => {
generateToken()
.then(({ access_token }) => {
serverGetAMData(
access_token,
"URL_to_external_API"
)
.then((segResult) => {
const allData = {
initialMarkup: ReactDOMServer.renderToString(
<App initialData={segResult} />
),
initialData: segResult,
};
return resolve(allData);
})
.catch((err) => {
return reject(err.response);
});
})
.catch(console.error);
});
export default AMdataPullDone;
serverGetAMData.js code:
import axios from "axios";
import config from "./config";
const serverGetAMData = async (access_token, apiUrl) => {
const folders = [];
await axios
.get(apiUrl, {
headers: {
Authorization: `Bearer ${access_token}`,
"x-api-key": config.AM_IMS_client_id,
"x-gw-ims-org-id": config.org_id,
}
})
.then((resp) => {
folders.push(...resp.data);
//return resp.data;
})
.catch(console.error);
return folders;
};
export default serverGetAMData;
I have a react app, which uses a router and works fine if the URL is not entered directly into the address bar, but rather is set by react router when navigating the app. I have added ExpressJS to the app, by running the react app via the Express proxy. The goal is to re-direct any dynamic requests to the react app, so that it can handle it, instead of Express. I have looked at many examples and tried using them, but I had no luck in making the re-direct work.
The folder structure and key file locations are as follows:
[Root directory]
---- [client]
--------- [node_modules]
--------- [public]
--------- [scss]
--------- [src]
--------- package.json
--------- webpack.config.js
---- [server]
--------- index.js
---- [node_modules]
---- package.json
The "client" directory contains the react app. The "server" directory contains the Express app. The Express app's package.json is in the root directory, while react app's package.json is in the "client" directory.
server/index.js
const express = require('express');
const path = require('path');
const PORT = process.env.PORT || 5000;
const app = express();
// Serve static files
app.use(express.static(path.resolve(__dirname, '../client/public')));
app.get('*', function(request, response) {
response.sendFile(path.resolve(__dirname, '../client/public', 'index.html'));
});
app.listen(PORT, function () {
console.error(`Listening on port ${PORT}`);
});
client/package.json
Contains "proxy": "http://localhost:5000"
client/webpack.config.js
Contains the following for devServer, entry, and output (I excluded various loaders as irrelevant for URL processing):
const BUILD_DIR = path.resolve(__dirname, 'build');
const SRC_DIR = path.resolve(__dirname, 'src');
module.exports = (env = {}) => {
return {
entry: {
index: [SRC_DIR + '/index.js']
},
output: {
path: BUILD_DIR,
filename: '[name].bundle.js'
},
devtool: env.prod ? 'source-map' : 'cheap-module-eval-source-map',
devServer: {
contentBase: BUILD_DIR,
compress: true,
hot: true,
open: true,
historyApiFallback: true
}
}
};
client/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, combineReducers, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import createHistory from 'history/createBrowserHistory';
import { Route } from 'react-router';
import { ConnectedRouter, routerReducer, routerMiddleware, push } from 'react-router-redux';
import rootReducer from './reducers';
// Wrapper
import SiteWrapper from './wrappers/SiteWrapper/'
// Pages
import Error404 from './ErrorPages/Error404/'
const history = createHistory();
const middleware = routerMiddleware(history);
const store = createStore(
combineReducers({
rootReducer,
router: routerReducer
}),
applyMiddleware(middleware)
)
ReactDOM.render((
<Provider store={store}>
<ConnectedRouter history={history}>
<div>
<Route exact path="/404" name="Error 404" component={Error404}/>
<Route path="/" name="Home" component={SiteWrapper}/>
</div>
</ConnectedRouter>
</Provider>
), document.getElementById('root'));
I first run the Express app, which starts listening on port 5000, and then I run the react app. When entering a dynamic URL http://localhost:7500/dynamicurl/test-dynamic-url, the app no longer shows 404, but then it doesn't show anything at all, and this is what I see in the developer console:
Refused to apply style from 'http://localhost:7500/dynamicurl/index.fonts.css' because its MIME type ('text/html') is not a supported stylesheet MIME type, and strict MIME checking is enabled.
test-dynamic-url:1 Refused to apply style from 'http://localhost:7500/dynamicurl/index.styles.css' because its MIME type ('text/html') is not a supported stylesheet MIME type, and strict MIME checking is enabled.
test-dynamic-url:54 GET http://localhost:7500/dynamicurl/index.bundle.js net::ERR_ABORTED
What am I doing wrong here? Did I miss something?
Thanks in advance
I'm having a problem using the Angular2 router and express, but none of the previous questions seem to have the solution I need. I'm using the latest version of the angular2-cli.
When I direct my browser to localhost:3000, I get the generic "app works" message that the angular2-cli generates. My problem arises when I try to navigate to one of my child routes, such as localhost:3000/auth/login - it redirects me back to the same "app works" page, instead of showing me a page that says "login works".
Looking at other questions on stack exchange, I figure that the issue is here:
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist/index.html'));
});
When I replace the * with a /, instead of redirecting me back to my target page, it gives me the cannot GET error.
In summary, my problem is that url navigation of my angular2 routes integrated into my express app is not working.
My express code in server.js is as follows:
var express = require("express");
var app = express();
var bodyParser = require("body-parser");
var mongoose = require("mongoose");
var path = require("path");
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended:true}));
app.use(express.static(path.join(__dirname, 'dist')));
// Catch all other routes and return the index file
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist/index.html'));
});
app.listen(3000, () => {
console.log("Server has started");
});
My application's routing module is as follows (app-routing.module.ts):
import {Route, RouterModule} from '#angular/router';
import {NgModule} from '#angular/core';
import {LoginComponent} from './login/login.component';
import {RegisterComponent} from './register/register.component';
import {FeedComponent} from './feed/feed.component';
import {FindComponent} from './find/find.component';
import {SubmitComponent} from './submit/submit.component';
import {ProfileComponent} from './profile/profile.component';
export const routeConfig = [
{
path: "auth",
children: [
{
path: "login",
component: LoginComponent
},
{
path: "register",
component: RegisterComponent
},
{
path: "",
redirectTo: "/login",
pathMatch: "full"
}
]
},
{
path: "app",
children: [
{
path: "feed",
component: FeedComponent
},
{
path: "find",
component: FindComponent
},
{
path: "submit",
component: SubmitComponent
},
{
path: "profile",
component: ProfileComponent
},
{
path: "",
redirectTo: "/feed",
pathMatch: "full"
}
]
}
];
#NgModule({
imports: [RouterModule.forRoot(routeConfig)],
exports: [RouterModule]
})
export class AppRoutingModule{
}
My application module is as follows:
import { BrowserModule } from '#angular/platform-browser';
import { NgModule } from '#angular/core';
import { FormsModule } from '#angular/forms';
import { HttpModule } from '#angular/http';
import {AppRoutingModule} from './app-routing.module';
import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { FeedComponent } from './feed/feed.component';
import { FindComponent } from './find/find.component';
import { SubmitComponent } from './submit/submit.component';
import { ProfileComponent } from './profile/profile.component';
#NgModule({
declarations: [
AppComponent,
LoginComponent,
RegisterComponent,
FeedComponent,
FindComponent,
SubmitComponent,
ProfileComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
All the components have been defined in their respective files and are the barebones default templates generated by the angular2-cli.
I have no idea what to do at this point. I've even tried restarting and rewriting this project multiple times to see if I went wrong somewhere (this is my 3rd attempt).
For angular2 and nodejs integration you can either put this in server.js:
var cors = required('cors');
app.use(cors());
or you can make use of proxy in your web server. For example in apache2 inside virtualhost tag you can do
ProxyPass /api/ http://localhost:3000/
ProxyReverse /api/ http://localhost:3000/
Now instead of using "http://localhost:3000/" in your http request function (although you are not using it) you can use just "api/".