How to achieve code splitting + SSR with #loadable/component ? Is webpack needed for the server code? - node.js

I'm trying to add code splitting + SSR to my React web app using #loadable/component.
My current situation: This is how I've implemented SSR for robots on my website. Since it's just for robots, I'm not using hydrate. Basically, I send either the index.html with the JS bundle's script tags for a regular user, or I send a fully rendered HTML page for the robots, without the need to hydrate.
My goal: I'd like to use #loadable/component to always return SSR pages from my server, and use hydrate to attach my JS bundle. And also achieve code splitting with that.
Here is how I currently build my app (pseudo code):
1. webpack BUILD FOR entry { app: "src/index.tsx" } AND OUTPUT BUNDLES TO MY /public FOLDER
2. babel TRANSPILE WHOLE `/src` FOLDER AND OUTPUT FILES TO MY /dist_app FOLDER
It's basically 2 builds, one of them is using webpack to bundle, and the other one basically transpiles the files from src to distApp.
And this is what my server does (pseudo code)
1. CHECK IF USER IS ROBOT (FROM THE USER AGENT STRING)
2. IF REGULAR USER
res.send("public/index.html"); // SEND REGULAR index.html WITH JS BUNDLES GENERATED BY WEBPACK
IF ROBOT
const App = require("./dist_app/App"); // THIS IS THE src/App COMPONENT TRANSPILED BY BABEL
const ssrHtml = ReactDOM.renderToString(<App/>);
// ADD <head> <helmet> <styles> ETC
res.send(ssrHtml);
The steps described above works just fine for my initial requirements (ssr just for robots).
But after I added #loadable/component to achieve code splitting + SSR, the set up above does not work anymore.
Because now I have dynamic imports on some of my routes. For example:
const AsyncPage = loadable(() => import("#app/pages/PageContainer"));
So my renderToString(<App/>) call comes back mostly empty, because it does not load those AsyncPages.
Over on the docs for Loadable components: server side rendering they have an example repo on how to achieve this.
But their example is kind of complex and it seems they are using webpack inside the server. I'll post what they do on their server below.
QUESTION
Do I really have to use webpack to bundle my app's server code in order to use #loadable/component for SSR like they are showing in their example? Can't I just use some kind of babel plugin to convert the dynamic imports into regular require calls? So that I'll be able to render it the way I was doing before?
It's weird, that they use webpack-dev-middleware. It's like this example should be used just for development. Does anybody know a repo with a production example of this?
import path from 'path'
import express from 'express'
import React from 'react'
import { renderToString } from 'react-dom/server'
import { ChunkExtractor } from '#loadable/server'
const app = express()
app.use(express.static(path.join(__dirname, '../../public')))
if (process.env.NODE_ENV !== 'production') {
/* eslint-disable global-require, import/no-extraneous-dependencies */
const { default: webpackConfig } = require('../../webpack.config.babel')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpack = require('webpack')
/* eslint-enable global-require, import/no-extraneous-dependencies */
const compiler = webpack(webpackConfig)
app.use(
webpackDevMiddleware(compiler, {
logLevel: 'silent',
publicPath: '/dist/web',
writeToDisk(filePath) {
return /dist\/node\//.test(filePath) || /loadable-stats/.test(filePath)
},
}),
)
}
const nodeStats = path.resolve(
__dirname,
'../../public/dist/node/loadable-stats.json',
)
const webStats = path.resolve(
__dirname,
'../../public/dist/web/loadable-stats.json',
)
app.get('*', (req, res) => {
const nodeExtractor = new ChunkExtractor({ statsFile: nodeStats })
const { default: App } = nodeExtractor.requireEntrypoint()
const webExtractor = new ChunkExtractor({ statsFile: webStats })
const jsx = webExtractor.collectChunks(<App />)
const html = renderToString(jsx)
res.set('content-type', 'text/html')
res.send(`
<!DOCTYPE html>
<html>
<head>
${webExtractor.getLinkTags()}
${webExtractor.getStyleTags()}
</head>
<body>
<div id="main">${html}</div>
${webExtractor.getScriptTags()}
</body>
</html>
`)
})
// eslint-disable-next-line no-console
app.listen(9000, () => console.log('Server started http://localhost:9000'))

Related

Node Express i18next - How to send locale text to client side js file?

I have i18next setup in Node.js server side. What is the best practice to use i18next.t() in client side javascript files? I've already set up Express to render server side variables to be used in .ejs files. However, I can't transfer them to the js files that are imported in ejs.
Some possible ways I've thought of:
Export and load i18next that was initialized in the server to the client.
I know the <%= %> variables for ejs work in inline scripts. However, I'm avoiding to have them for content security policy. Perhaps there is a way to send this over to the js file?
Load and initialize i18next on the client-side again. I've tried this and it works, but then there are duplicated locale files for both server and client.
Export the locale files by specifying path. EX
app.use('/locale', express.static(path.join(__dirname, 'locale', {{lng}}.json)));
// app.js
const i18next = require('i18next');
const Backend = require('i18next-fs-backend');
const middleware = require('i18next-http-middleware');
i18next.use(Backend)
.use(middleware.LanguageDetector)
.init({
detection: detection_options,
fallbackLng: 'en',
backend: {
loadPath(lng, ns) {
if (lng === 'zh' || lng === 'zh-HK' || lng === 'zh-TW') {
return path.join(__dirname, 'locales/zh-hant.json');
} else if (lng === 'en-US') {
return path.join(__dirname, 'locales/en.json');
}
return path.join(__dirname, 'locales/{{lng}}.json');
}
}
})
app.use(middleware.handle(i18next));
// index.ejs
<%- include('../includes/head.ejs') %>
<link rel="stylesheet" href="/css/index.css">
</head>
<body>
<h1>
<%= t('locale-text-from-en.json-goes-here') %> // this works well
</h1>
<script src="/js/index.js"></script>
</body>
// index.js
console.log(t('locale-text-from-en.json-goes-here')) // how to use i18next.t() here?
To use i18next on the client you need to install the i18next package via npm or yarn on the client. Or download i18next library via their CDN https://unpkg.com/i18next/dist/umd/i18next.js.
i18next has many extensions for your project, like react-i18next for react project, jquery-i18next for jquery project.

Set "basedir" option for Pug in NestJS

I'm trying to use pug layouts in NestJS, however when extending a layout from an absolute path, pug requires the basedir option to be set.
In ExpressJS you would use app.locals.basedir = ..., what would be the equivalent in NestJS?
const server = await NestFactory.create<NestExpressApplication>(AppModule);
server.setViewEngine('pug');
server.setBaseViewsDir(join(__dirname, 'templates', 'views'));
await server.listen(config.server.port);
Using extends /layouts/index in a view would throw the following; the "basedir" option is required to use includes and extends with "absolute" paths.
I'm not looking to use relative paths, since this quickly becomes very messy. E.g. extends ../../../layouts/index
From what I can tell, you can achieve the same functionality as /layouts/index with just using layout/index so long as layout is a folder in your templates/views directory.
I've set up a git repo as a working example so you can test it out yourself and see if I need to go in more depth about anything.
EDIT 6/27/2019:
Thank you, I misunderstood your initial question.
With creating and express based application, you can send an express server to the NestFactory to use that server instance instead of having Nest create a plain instance for you. From here you can set up the express server as you normally would and get the desired functionality. I've modified the git repo to be able to test the scenario better and believe this is what you are looking for.
My main.ts
import { NestFactory } from '#nestjs/core';
import { NestExpressApplication, ExpressAdapter } from '#nestjs/platform-express';
import * as express from 'express';
import { AppModule } from './app.module';
import { join } from 'path';
async function bootstrap() {
// Creating and setting up the express instanced server
const server = express();
server.locals.basedir = join(__dirname, '..', 'views');
// Using the express server instance in the nest factory
const app = await NestFactory.create<NestExpressApplication>(AppModule, new ExpressAdapter(server));
app.useStaticAssets(join(__dirname, '..', 'public'));
app.setBaseViewsDir(join(__dirname, '..', 'views'));
app.setViewEngine('pug');
await app.listen(3000);
}
bootstrap();
Overall the folder set up is like so
src
|-app.controller.ts
|-app.module.ts
|-app.service.ts
|-main.ts
views
|-hello
|-home.pug
|-message
|-message.pug
|-templates
|-layout.pug
And the beginning of my home.pug and message.pug files is extends /templates/layout
After looking around through the documentation, NestJS uses an express under the hood, and gives you access to the underlying instance with getHttpAdapter().getInstance().
Keeping that in mind, we can set the basedir as follows;
const express = server.getHttpAdapter().getInstance();
express.locals.basedir = join(__dirname, 'templates');

How can my client get application configuration from the server when using Webpack?

I'm adding Webpack to a Node/Express app that previously used RequireJS. When the client needed some configuration from the server, we previously used a custom Express route that retrieved specific configs as JSON:
server/index.js - Set up Express routes for config files
const app = express();
const configRouter = express.Router();
configRouter.get('/some-config.json', (req, res) => {
const someConfig = {
prop1: getProp1(),
prop2: getProp2()
}
res.json(someConfig);
}
app.use('/config', configRouter);
client/controller.js - Use/config/some-config.json during initialization
define(['text!/config/some-config.json'], function(SomeConfig) {
// do something with SomeConfig
});
But removing RequireJS means I can no longer retrieve the JSON this way as a dependency. And it's not static JSON either, so it's not as simple as just placing it alongside client code and importing it.
So what is the best way to do this with Webpack? Any help greatly appreciated. Thanks!

How to import a default export of webpack entry file from outside?

I think I can best explain it with code. I have a file in webpack like the following:
import ReactDOMServer from 'react-dom/server';
import Server from './server';
import templateFn from './template';
export default (req, res) => {
const reactString = ReactDOMServer.renderToString(<Server />);
const template = templateFn(html);
res.send(template);
};
I also have an express application where I want to have access to the default exported function. If it makes any difference, this file is the webpack entry file. Here is what I tried in my express app:
const handleRequest = require(path.resolve(webpackConfig.output.path, webpackConfig.output.filename));
app.get('*', (req, res) => {
console.log(handleRequest);
});
I was trying to import the webpack generated file with the hope that I will be able to access the entry file's default export. Well, I was wrong as the output of the import was {}.
Is there a webpack plugin or some kind of a technique to do what I am trying to build? I don't want the express application to be part of the webpack build. That was the main reason I separated the code in this way.
I was able to access contents of webpack using library parameter (webpack.config.js):
output: {
path: ...,
filename: ...,
library: 'myapp',
libraryTarget: 'commonjs'
}
Then access it in the code:
const output = require(path.resolve(webpackConfig.output.path, webpackConfig.output.filename));
const defaultExportFunction = output.myapp.default;

How to embed ECTJS views in LocomotiveJS

I have typical installation of locomotive JS. I want to use ECTJS for views. I have successfully installed ECTJS by using following command:
npm install ect --save
I have following controller:
var locomotive = require('locomotive')
, Controller = locomotive.Controller;
var roseController = new Controller();
roseController.thorne = function(){
this.render();
}
module.exports = roseController;
I have created following 'thorne.html.ect' file in directory '/app/views/rose'
<!DOCTYPE html>
<html>
<head>
<title>Title</title>
<link rel="stylesheet" href="/stylesheets/screen.css" />
</head>
<body>
<h1>Rose Controller - Thorne Action from ECT</h1>
<p></p>
</body>
</html>
I have made changes in '02_views.js' file in 'initializers' folder. File is as below:
module.exports = function() {
// Configure view-related settings. Consult the Express API Reference for a
// list of the available [settings](http://expressjs.com/api.html#app-settings).
var ECT = require('ect');
var renderer = ECT({ root : __dirname + '/app/views', ext : '.ect' });
//var renderer = ECT({ watch: true, root: __dirname + '/app/views'});
this.set('view engine', 'ect');
this.engine('ect', renderer.render);
// // this.set('views', __dirname + '/../../app/views');
// // this.set('view engine', 'ejs');
// Register EJS as a template engine.
// //this.engine('ejs', require('ejs').__express);
// Override default template extension. By default, Locomotive finds
// templates using the `name.format.engine` convention, for example
// `index.html.ejs` For some template engines, such as Jade, that find
// layouts using a `layout.engine` notation, this results in mixed conventions
// that can cause confusion. If this occurs, you can map an explicit
// extension to a format.
/* this.format('html', { extension: '.jade' }) */
// Register formats for content negotiation. Using content negotiation,
// different formats can be served as needed by different clients. For
// example, a browser is sent an HTML response, while an API client is sent a
// JSON or XML response.
/* this.format('xml', { engine: 'xmlb' }); */
}
When I run
http://localhost:3000/rose/thorne
I get following error :
500 Error: Failed to lookup view "rose/thorne.html.ect"
If I use:
this.res.send('Test');
in rose/thorne action, it shows without any problem.
Can some one guide me how can I embed ECTJS in locomotiveJS.
Thanks
It seems that you configuration in 02_views.js is broken.
Try the following configuration:
// Creating ECT renderer
var ectRenderer = ECT({
watch: false,
gzip: true,
root: __dirname + '/../../app/views'
});
// Register ECT as a template engine.
this.engine('.ect', ectRenderer.render);
// Configure application settings. Consult the Express API Reference for a
// list of the available [settings](http://expressjs.com/api.html#app-settings).
this.set('views', __dirname + '/../../app/views');
this.set('view engine', 'ect');
// Override default template extension. By default, Locomotive finds
// templates using the `name.format.engine` convention, for example
// `index.html.ect` For some template engines, such as ECT, that find
// layouts using a `layout.engine` notation, this results in mixed conventions
// that can cuase confusion. If this occurs, you can map an explicit
// extension to a format.
this.format('html', {
extension: '.ect'
});
With this configuration my view files are named filename.ect. So if filename.html.ect doesnt work you can rename it to filename.ect

Resources