preflower / blog

preflower's blog
0 stars 0 forks source link

为RN项目添加Web支持 #23

Open preflower opened 2 years ago

preflower commented 2 years ago

构建

基于 Webpack5

Webpack 基本配置

// base.conf.js
const appDirectory = path.resolve(__dirname, '../')
const { presets } = require(`${appDirectory}/babel.config.js`)

const compileNodeModules = [
  // Add every react-native package that needs compiling
].map((moduleName) => path.resolve(appDirectory, `node_modules/${moduleName}`))

module.exports = {
  // 配置入口文件
  entry: {
    app: path.join(appDirectory, 'index.web.js')
  },
  output: {
    // 配置出口文件目录
    path: path.resolve(appDirectory, 'dist'),
    // 设置打包后的文件名,基于contenthash确保内容不变的情况下,hash值不变
    filename: 'js/[name].[contenthash].js',
    // 每次构建前清空构建目录
    clean: true
  },
  resolve: {
    // 新增扩展名以 .web 优先
    extensions: ['.web.tsx', '.web.ts', '.web.js', '.tsx', '.ts', '.js'],
    alias: {
      // 使用 react-native-web 替代 react-native
      'react-native$': 'react-native-web',
    }
  },
  modules: {
    rules: [
      // Babel配置
      {
        test: /\.js$|tsx?$/,
        // Add every directory that needs to be compiled by Babel during the build.
        include: [
          path.resolve(appDirectory, 'index.web.js'), // Entry to your application
          path.resolve(appDirectory, 'App.web.tsx'), // Change this to your main App file
          path.resolve(appDirectory, 'src'),
          ...compileNodeModules
        ],
        use: {
          loader: 'babel-loader',
          options: {
            cacheDirectory: true,
            presets,
            plugins: ['react-native-web']
          }
        }
      },
      // 图片 和 文字处理采用 Webpack5 提供的 asset 资源模块处理
      {
        test: /\.(gif|jpe?g|png)$/,
        type: 'asset/resource',
        generator: {
          filename: 'static/images/[name][ext][query]'
        }
      },
      {
        test: /\.ttf$/,
        type: 'asset/resource',
        generator: {
          filename: 'static/fonts/[name][ext][query]'
        }
      }
    ]
  },
  plugins: [
    // 将资源文件导入到index.html中
    new HtmlWebpackPlugin({
      template: path.join(appDirectory, 'index.html')
    }),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.DefinePlugin({
      // See: https://github.com/necolas/react-native-web/issues/349
      __DEV__: JSON.stringify(true)
    }),
    // 配置静态文件路径
    new CopyWebpackPlugin({
      patterns: [
        {
          from: path.resolve(appDirectory, './static'),
          to: 'static',
          globOptions: {
            ignore: ['.*']
          }
        }
      ]
    })
  ]
}

Webpack Dev 环境配置

// dev.conf.js
const baseWebpackConfig = require('./base.conf')

const appDirectory = path.resolve(__dirname, '../')

const webpackConfig = merge(baseWebpackConfig, {
  mode: 'development',
  devServer: {
    // dev-server 支持 history 模式
    historyApiFallback: true
  },
  module: {
    rules: [
      // 解析 css 文件
      // 测试环境与生产环境不同
      // 测试环境采用inline方式将 css 注入到 html 中
      // 生产环境采用 MiniCssExtractPlugin 将 css 打成单独包
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader']
      },
      // 获取库中已有的sourcemap,便于开发调试
      {
        test: /\.js$/,
        enforce: 'pre',
        use: ['source-map-loader']
      }
    ]
  }
})

module.exports = webpackConfig

Webpack Prod 环境配置

// prod.conf.js
const path = require('path')
const { merge } = require('webpack-merge')
const baseWebpackConfig = require('./base.conf')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

const appDirectory = path.resolve(__dirname, '../')

const webpackConfig = merge(baseWebpackConfig, {
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash].css'
    })
  ],
  optimization: {
    minimize: true,
    // 提取出 运行时 文件(模块解析,加载 和 模块信息清单)供所有包共享
    // 提取的目的是防止包含模块信息的模块会因模块hash改变而导致缓存失效
    runtimeChunk: 'single',
    // 将体积大于20kb的三方依赖包都做分包处理,最大化确保缓存的有效性
    splitChunks: {
      chunks: 'all',
      maxInitialRequests: Infinity,
      minSize: 20000,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name (module) {
            // get the name. E.g. node_modules/packageName/not/this/part.js
            // or node_modules/packageName
            const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)?.[1]
            if (packageName) {
              // npm package names are URL-safe, but some servers don't like @ symbols
              return `npm.${packageName.replace('@', '')}`
            } else {
              return false
            }
          },
          reuseExistingChunk: true
        }
      }
    }
  }
})

module.exports = (env) => {
  // 使用 --env report 生成 Bundle Analyzer 报告
  if (env.report) {
    webpackConfig.plugins.push(
      new BundleAnalyzerPlugin()
    )
  }

  return webpackConfig
}

Babel配置

我们已经在Webpack里配置了babel-loader 以启用babel,并且把preset指向了我们的babel.config.js 文件

通过引入babel-preset-expo 替代module:metro-react-native-babel-preset 来解决其导致Webpack Tree Shaking 失效的问题

module.exports = {
    ...
    presets: [
-          'module:metro-react-native-babel-preset',
+          [
+            'babel-preset-expo',
+            {
+              jsxRuntime: 'classic'
+            }
+          ]
    ]
        ...
}

添加 PWA 功能

使用workbox 自动生成SW相关文件,达到最基本的PWA功能

// base.conf.js
const WorkboxPlugin = require('workbox-webpack-plugin')

module.exports = {
  ...
  plugins: [
    new WorkboxPlugin.GenerateSW({
      // 获取到新资源后立刻执行
      skipWaiting: true
    }), 
  ]
  ...
}

配置 manifest.json 文件

// /static/manifest.json
{
  "name": "Porject Name",
  "short_name": "Porject Name",
  "icons": [
    {
      "src": "/static/pwa/logos/192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/static/pwa/logos/512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "start_url": "/index.html",
  "display": "standalone",
  "background_color": "#000000",
  "theme_color": "#4DBA87"
}

配置入口文件

<!-- index.html -->
<script>
  // Check that service workers are supported
  if ('serviceWorker' in navigator) {
    // Use the window load event to keep the page load performant
    window.addEventListener('load', () => {
      navigator.serviceWorker.register('/service-worker.js');
    });
  }
</script>
<link rel="manifest" href="/static/manifest.json">

环境配置

当前项目需要支持 开发环境,测试环境,生产环境,并且需沿用项目内已有的环境配置;基于此引入react-native-web-config 插件配置

// base.conf.js
modul.exports = {
  ...
  resolve: {
    alias: {
      ...
      'react-native-config': 'react-native-web-config',
      ...
    }
  }
  ...
}

// dev.conf.js
const ReactNativeWebConfig = require('react-native-web-config/plugin')

module.exports = {
  ...
  plugins: [
    ReactNativeWebConfig(path.join(appDirectory, '.env.beta'))
  ]
  ...
}

// prod.conf.js
module.exports = (env) => {
  ...
  webpackConfig.plugins.push(
    ReactNativeWebConfig(path.join(
      appDirectory,
      // 通过 --env goal 是否为 prod 来判断当前环境是测试/生产
      env.goal === 'prod' ? '.env.prod' : '.env.beta'
    ))
  )
  ...
}