【问题标题】:React Native Web implement SSRReact Native Web implement SSR
【发布时间】:2022-12-02 02:18:22
【问题描述】:

https://necolas.github.io/react-native-web/docs/rendering/

After reading the SSR example from the document, I still don't know how to implement SSR

And I don't want to apply SSR with other framework like NextJS

Can anyone show me an example or give me some advice

【问题讨论】:

  • Do you even know how to setup SSR yet with React and Node and Webpack alone? Because if you don't know how to setup from basic, it's worth nothing to add React Native and React Native Web complexity in addition to what you're not experienced with yet. If not, you might just want to use NextJS or Gatsby, or just learn with basic React + Node + Webpack.
  • I did combine perfectly React Native Web with NextJS SSR, and it works like a charm. If you're still on it, I'll add an answer soon. If not, I'll add it later for anyone else.
  • @KeitelDOG would you mind sharing your answer? thanks!
  • Ok I'll put how I handle them with some explanation. And from that you'll arrange it to adapt to your project.

标签: react-native-web


【解决方案1】:

I'm posting this, not as a direct answer to the original question, because it's targeted directly SSR with NextJS, and the OP needed SSR independently from frameworks like NextJS. However, understanding it with NextJS can get anyone closer with things, because they key relies in Webpack config that NextJS also use as SSR in its encapsulation config.

First thing to know is that, once a Package has been written for React Native, it need to be transpiled first to be able to be used in Web, with webpack config.externals.

let modulesToTranspile = [
  'react-native',
  'react-native-dotenv',
  'react-native-linear-gradient',
  'react-native-media-query',
  'react-native-paper',
  'react-native-view-more-text',
  // 'react-native-vector-icons',
];

Then you need to alias some react-native packages to react-native-web equivalent to let package use web version of modules like:

config.resolve.alias = {
  ...(config.resolve.alias || {}),
  // Transform all direct `react-native` imports to `react-native-web`
  'react-native$': 'react-native-web',
  'react-native-linear-gradient': 'react-native-web-linear-gradient',
};

At this point, you almost get the essential. The rest is normal Webpack config for the normal Application. Also, it needs some additional config in native config file too. I will post all configs content.

For NextJS: next.config.js :

const path = require('path');

let modulesToTranspile = [
  'react-native',
  'react-native-dotenv',
  'react-native-linear-gradient',
  'react-native-media-query',
  'react-native-paper',
  'react-native-view-more-text',
  // 'react-native-vector-icons',
];

// console.log('modules to transpile', modulesToTranspile);

// import ntm = from 'next-transpile-modules';
// const withTM = ntm(modulesToTranspile);
// logic below for externals has been extracted from 'next-transpile-modules'
// we won't use this modules as they don't allow package without 'main' field...
// https://github.com/martpie/next-transpile-modules/issues/170
const getPackageRootDirectory = m =>
  path.resolve(path.join(__dirname, 'node_modules', m));

const modulesPaths = modulesToTranspile.map(getPackageRootDirectory);

const hasInclude = (context, request) => {
  return modulesPaths.some(mod => {
    // If we the code requires/import an absolute path
    if (!request.startsWith('.')) {
      try {
        const moduleDirectory = getPackageRootDirectory(request);
        if (!moduleDirectory) {
          return false;
        }
        return moduleDirectory.includes(mod);
      } catch (err) {
        return false;
      }
    }
    // Otherwise, for relative imports
    return path.resolve(context, request).includes(mod);
  });
};

const configuration = {
  node: {
    global: true,
  },
  env: {
    ENV: process.env.NODE_ENV,
  },
  // optimizeFonts: false,
  // target: 'serverless',

  // bs-platform
  // pageExtensions: ['jsx', 'js', 'bs.js'],

  // options: { buildId, dev, isServer, defaultLoaders, webpack }
  webpack: (config, options) => {
    // config.experimental.forceSwcTransforms = true;

    // console.log('fallback', config.resolve.fallback);
    if (!options.isServer) {
      // We shim fs for things like the blog slugs component
      // where we need fs access in the server-side part
      config.resolve.fallback.fs = false;
    } else {
      // SSR
      // provide plugin
      config.plugins.push(
        new options.webpack.ProvidePlugin({
          requestAnimationFrame: path.resolve(__dirname, './polyfills/raf.js'),
        }),
      );
    }

    // react-native-web
    config.resolve.alias = {
      ...(config.resolve.alias || {}),
      // Transform all direct `react-native` imports to `react-native-web`
      'react-native$': 'react-native-web',
      'react-native-linear-gradient': 'react-native-web-linear-gradient',
    };
    config.resolve.extensions = [
      '.web.js',
      '.web.ts',
      '.web.tsx',
      ...config.resolve.extensions,
    ];

    config.externals = config.externals.map(external => {
      if (typeof external !== 'function') {
        return external;
      }
      return async ({ context, request, getResolve }) => {
        if (hasInclude(context, request)) {
          return;
        }
        return external({ context, request, getResolve });
      };
    });

    const babelLoaderConfiguration = {
      test: /.jsx?$/,
      use: options.defaultLoaders.babel,
      include: modulesPaths,
      // exclude: /node_modules[/\](?!react-native-vector-icons)/,
    };

    babelLoaderConfiguration.use.options = {
      ...babelLoaderConfiguration.use.options,
      cacheDirectory: false,
      // For Next JS transpile
      presets: ['next/babel'],
      plugins: [
        ['react-native-web', { commonjs: true }],
        ['@babel/plugin-proposal-class-properties'],
        // ['@babel/plugin-proposal-object-rest-spread'],
      ],
    };

    config.module.rules.push(babelLoaderConfiguration);

    return config;
  },
};

// module.exports = withTM(config);
module.exports = configuration;

SSR will fail to build when missing some functions at server side. The most popular with React Native is requestAnimationFrame. I added it add a Webpack Plugin to mimic it. It can be an empty function or Polyfill:

The file 'polyfills/raf.js(I just put it assetImmediate`):

const polys = { requestAnimationFrame: setImmediate };

module.exports = polys.requestAnimationFrame;

The Babel config is necessary for the last part of it, couldn't work directly in next config. babel.config.js :

module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
  plugins: [['module:react-native-dotenv'], 'react-native-reanimated/plugin'],
};

And finally, my list of packages in package.json:

{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "android": "react-native run-android",
    "android:dev": "adb reverse tcp:8081 tcp:8081 && react-native run-android",
    "ios": "react-native run-ios",
    "start": "react-native start",
    "test": "jest",
    "lint": "eslint .",
    "web": "webpack serve -d source-map --mode development --config "./web/webpack.config.js" --inline --color --hot",
    "build:web": "webpack --mode production --config "./web/webpack.config.js" --hot",
    "next:dev": "next",
    "next:build": "next build",
    "next:start": "next start",
    "next:analyze": "ANALYZE=true next build"
  },
  "dependencies": {
    "@material-ui/core": "^4.12.4",
    "@react-native-async-storage/async-storage": "^1.17.3",
    "@react-navigation/drawer": "^6.4.1",
    "@react-navigation/native": "^6.0.10",
    "@react-navigation/stack": "^6.2.1",
    "@reduxjs/toolkit": "^1.8.1",
    "axios": "^0.21.1",
    "local-storage": "^2.0.0",
    "lottie-ios": "^3.2.3",
    "lottie-react-native": "^5.1.3",
    "lottie-web": "^5.9.4",
    "moment": "^2.29.1",
    "next": "^12.1.6",
    "nookies": "^2.5.2",
    "numeral": "^2.0.6",
    "raf": "^3.4.1",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-native": "0.68.1",
    "react-native-dotenv": "^2.5.5",
    "react-native-gesture-handler": "^2.4.2",
    "react-native-keyboard-aware-scroll-view": "^0.9.5",
    "react-native-linear-gradient": "^2.5.6",
    "react-native-media-query": "^1.0.9",
    "react-native-paper": "^4.12.1",
    "react-native-progress": "^5.0.0",
    "react-native-read-more-text": "^1.1.2",
    "react-native-reanimated": "^2.8.0",
    "react-native-safe-area-context": "^4.2.5",
    "react-native-screens": "^3.13.1",
    "react-native-share-menu": "^6.0.0",
    "react-native-svg": "^12.3.0",
    "react-native-svg-transformer": "^1.0.0",
    "react-native-vector-icons": "^9.1.0",
    "react-native-view-more-text": "^2.1.0",
    "react-native-web": "^0.17.7",
    "react-native-web-linear-gradient": "^1.1.2",
    "react-redux": "^8.0.1"
  },
  "devDependencies": {
    "@babel/plugin-proposal-class-properties": "^7.14.5",
    "@next/bundle-analyzer": "^12.2.2",
    "@react-native-community/eslint-config": "^2.0.0",
    "@swc/cli": "^0.1.57",
    "@swc/core": "^1.2.179",
    "eslint": "^7.28.0",
    "metro-react-native-babel-preset": "^0.66.0",
    "url-loader": "^4.1.1",
    "webpack": "^5.39.1",
    "webpack-cli": "^4.7.2"
  },
  "jest": {
    "preset": "react-native-web"
  },
  "sideEffects": false
}

NB: only React-Native packages used also in Web has to be transpiled. Some React-Native packages can be used ONLY in Native, so transpiling them for Web will add up unnecessary chunks of heavy codes in the Web, which is not good. React-Native-Web/React-Native is already more heavy for Web than normal packages made directly for Web.

TIPS to keep it cool with NextJS

  • Avoid writing conditional Platform.OS === 'web' on small components where you plan to use either a React-Native module or a Web module, which can cause all of them to load unnecessary Native-Only package on web codes. If size is not important, then you can ignore it. Add extension .web.js and .native.js at the end and separate the small codes. For example I write separate Functions and Components for : Storage.web.js, Storage.native.js, CustomLink.web.js, CustomLink.native.js, and hooks useCustomNavigation.web.js, useCustomNavigation.native.js, so that I call CustomLink in place of NextJS Link/router and React-Navigation Link/navigation.
  • I use react-native-media-query package as life saver for advanced media queries for all SSR/CSR Web and Native responsive display. The App can be restructured on big screen like normal Desktop Web, and be shrunk to Mobile View on the go, EXACTLY LIKE Material-UI on NextJS.

【讨论】:

    猜你喜欢
    • 2022-10-25
    • 2021-04-05
    • 2018-07-02
    • 2022-12-01
    • 1970-01-01
    • 2022-10-05
    • 2022-09-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多