chore: add an example for SSR (#8298)

<img width="740" alt="Screen Shot 2020-05-20 at 16 31 30" src="https://user-images.githubusercontent.com/1174278/82458770-673d8880-9ab7-11ea-81d3-8ac0c1e52705.png">
This commit is contained in:
Satyajit Sahoo
2020-05-26 16:07:47 +02:00
committed by GitHub
parent af1722d1e9
commit 52d5cb4179
18 changed files with 777 additions and 59 deletions

View File

@@ -1,6 +1,10 @@
import 'react-native-gesture-handler';
import { registerRootComponent } from 'expo';
import { Asset } from 'expo-asset';
import { Assets as StackAssets } from '@react-navigation/stack';
import App from './src/index';
Asset.loadAsync(StackAssets);
registerRootComponent(App);

View File

@@ -0,0 +1,13 @@
import fetch from 'node-fetch';
import cheerio from 'cheerio';
const server = 'http://localhost:3275';
it('renders the home page', async () => {
const res = await fetch(server);
const html = await res.text();
const $ = cheerio.load(html);
expect($('title').text()).toBe('Examples');
});

View File

@@ -1,8 +1,16 @@
import { setup } from 'jest-dev-server';
export default async function () {
await setup({
command: 'yarn serve -l 3579 web-build',
port: 3579,
});
await setup([
{
command: 'yarn serve -l 3579 web-build',
launchTimeout: 50000,
port: 3579,
},
{
command: 'yarn server',
launchTimeout: 50000,
port: 3275,
},
]);
}

View File

@@ -9,6 +9,7 @@
"native": "react-native start",
"android": "react-native run-android",
"ios": "react-native run-ios",
"server": "nodemon -e '.js,.ts,.tsx' --exec \"babel-node -i '/node_modules[/\\](?react-native)/' -x '.web.tsx,.web.ts,.web.js,.tsx,.ts,.js' --config-file ./server/babel.config.js server\"",
"test": "jest"
},
"dependencies": {
@@ -18,6 +19,7 @@
"expo": "^37.0.8",
"expo-asset": "~8.1.3",
"expo-blur": "~8.1.0",
"koa": "^2.12.0",
"react": "~16.9.0",
"react-dom": "~16.9.0",
"react-native": "~0.61.5",
@@ -29,17 +31,28 @@
"react-native-screens": "^2.7.0",
"react-native-tab-view": "2.14.0",
"react-native-unimodules": "~0.9.1",
"react-native-vector-icons": "^6.6.0",
"react-native-web": "^0.11.7"
},
"devDependencies": {
"@babel/node": "^7.8.7",
"@expo/webpack-config": "^0.11.19",
"@types/cheerio": "^0.22.18",
"@types/jest-dev-server": "^4.2.0",
"@types/koa": "^2.11.3",
"@types/node-fetch": "^2.5.7",
"@types/react": "^16.9.34",
"@types/react-dom": "^16.9.8",
"@types/react-native": "^0.62.7",
"babel-plugin-module-resolver": "^4.0.0",
"babel-preset-expo": "^8.1.0",
"cheerio": "^1.0.0-rc.3",
"expo-cli": "^3.20.1",
"jest": "^26.0.1",
"jest-dev-server": "^4.4.0",
"mock-require-assets": "^0.0.1",
"node-fetch": "^2.6.0",
"nodemon": "^2.0.4",
"playwright": "^0.14.0",
"serve": "^11.3.0",
"typescript": "^3.8.3"

View File

@@ -0,0 +1,40 @@
const path = require('path');
const fs = require('fs');
const packages = path.resolve(__dirname, '..', '..', 'packages');
const alias = Object.fromEntries(
fs
.readdirSync(packages)
.filter((name) => !name.startsWith('.'))
.map((name) => [
`@react-navigation/${name}`,
path.resolve(
packages,
name,
require(`../../packages/${name}/package.json`).source
),
])
);
module.exports = {
presets: [
'@babel/preset-env',
'@babel/preset-flow',
'@babel/preset-typescript',
'@babel/preset-react',
],
plugins: [
'@babel/plugin-proposal-class-properties',
[
'module-resolver',
{
root: ['..'],
alias: {
'react-native': 'react-native-web',
...alias,
},
},
],
],
};

54
example/server/index.tsx Normal file
View File

@@ -0,0 +1,54 @@
import './resolve-hooks';
import Koa from 'koa';
import * as React from 'react';
import ReactDOMServer from 'react-dom/server';
import { AppRegistry } from 'react-native-web';
import { ServerContainer, ServerContainerRef } from '@react-navigation/native';
import App from '../src/index';
AppRegistry.registerComponent('App', () => App);
const PORT = process.env.PORT || 3275;
const app = new Koa();
app.use(async (ctx) => {
const { element, getStyleElement } = AppRegistry.getApplication('App');
const ref = React.createRef<ServerContainerRef>();
const html = ReactDOMServer.renderToString(
<ServerContainer
ref={ref}
location={{ pathname: ctx.path, search: ctx.search }}
>
{element}
</ServerContainer>
);
const css = ReactDOMServer.renderToStaticMarkup(getStyleElement());
const document = `
<!DOCTYPE html>
<html style="height: 100%">
<meta charset="utf-8">
<meta httpEquiv="X-UA-Compatible" content="IE=edge">
<meta
name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover"
>
${css}
<title>${ref.current?.getCurrentOptions()?.title}</title>
<body style="height: 100%">
<div id="root" style="display: flex; height: 100%">
${html}
</div>
`;
ctx.body = document;
});
app.listen(PORT, () => {
console.log(`Running at http://localhost:${PORT}`);
});

View File

@@ -0,0 +1,12 @@
import 'mock-require-assets';
import Module from 'module';
// We need to make sure that .web.xx extensions are resolved before .xx
// @ts-ignore
Module._extensions = Object.fromEntries(
// @ts-ignore
Object.entries(Module._extensions).sort((a, b) => {
return b[0].split('.').length - a[0].split('.').length;
})
);

View File

@@ -0,0 +1,11 @@
import RNRestart from 'react-native-restart';
import { Updates } from 'expo';
export function restartApp() {
// @ts-ignore
if (global.Expo) {
Updates.reloadFromCache();
} else {
RNRestart.Restart();
}
}

1
example/src/Restart.tsx Normal file
View File

@@ -0,0 +1 @@
export function restartApp() {}

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { View, ScrollView, StyleSheet, Platform } from 'react-native';
import { Button } from 'react-native-paper';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import {
createBottomTabNavigator,
BottomTabNavigationProp,

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { View, StyleSheet } from 'react-native';
import { Title, Button } from 'react-native-paper';
import { Feather } from '@expo/vector-icons';
import Feather from 'react-native-vector-icons/Feather';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
type BottomTabParams = {

View File

@@ -1,8 +1,7 @@
import * as React from 'react';
import { View, StyleSheet, ScrollView, Alert, Platform } from 'react-native';
import { Button, Appbar } from 'react-native-paper';
import { BlurView } from 'expo-blur';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { RouteProp, ParamListBase } from '@react-navigation/native';
import {
createStackNavigator,
@@ -10,6 +9,7 @@ import {
HeaderBackground,
useHeaderHeight,
} from '@react-navigation/stack';
import BlurView from '../Shared/BlurView';
import Article from '../Shared/Article';
import Albums from '../Shared/Albums';

View File

@@ -0,0 +1,3 @@
import { BlurView } from 'expo-blur';
export default BlurView;

View File

@@ -0,0 +1,12 @@
import * as React from 'react';
import { View, ViewProps } from 'react-native';
type Props = ViewProps & {
tint: 'light' | 'dark';
intensity: number;
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default function BlurView({ tint, intensity, ...rest }: Props) {
return <View {...rest} />;
}

View File

@@ -11,10 +11,7 @@ import {
} from 'react-native';
// eslint-disable-next-line import/no-unresolved
import { enableScreens } from 'react-native-screens';
import RNRestart from 'react-native-restart';
import { Updates } from 'expo';
import { Asset } from 'expo-asset';
import { MaterialIcons } from '@expo/vector-icons';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import {
Provider as PaperProvider,
DefaultTheme as PaperLightTheme,
@@ -36,11 +33,11 @@ import {
} from '@react-navigation/drawer';
import {
createStackNavigator,
Assets as StackAssets,
StackNavigationProp,
HeaderStyleInterpolators,
} from '@react-navigation/stack';
import { restartApp } from './Restart';
import AsyncStorage from './AsyncStorage';
import LinkingPrefixes from './LinkingPrefixes';
import SettingsItem from './Shared/SettingsItem';
@@ -126,12 +123,10 @@ const Stack = createStackNavigator<RootStackParamList>();
const NAVIGATION_PERSISTENCE_KEY = 'NAVIGATION_STATE';
const THEME_PERSISTENCE_KEY = 'THEME_TYPE';
Asset.loadAsync(StackAssets);
export default function App() {
const [theme, setTheme] = React.useState(DefaultTheme);
const [isReady, setIsReady] = React.useState(false);
const [isReady, setIsReady] = React.useState(Platform.OS === 'web');
const [initialState, setInitialState] = React.useState<
InitialState | undefined
>();
@@ -307,12 +302,7 @@ export default function App() {
value={I18nManager.isRTL}
onValueChange={() => {
I18nManager.forceRTL(!I18nManager.isRTL);
// @ts-ignore
if (global.Expo) {
Updates.reloadFromCache();
} else {
RNRestart.Restart();
}
restartApp();
}}
/>
<Divider />

16
example/types/react-native-web.d.ts vendored Normal file
View File

@@ -0,0 +1,16 @@
declare module 'react-native-web' {
export const AppRegistry: {
registerComponent(
name: string,
callback: () => React.ComponentType<any>
): void;
getApplication(
name: string,
options?: { initialProps: object }
): {
element: React.ReactElement;
getStyleElement(): React.ReactElement;
};
};
}

View File

@@ -53,5 +53,5 @@ export type LinkingOptions = {
};
export type ServerContainerRef = {
getCurrentOptions(): object | undefined;
getCurrentOptions(): Record<string, any> | undefined;
};

613
yarn.lock

File diff suppressed because it is too large Load Diff