如何让 React Native 项目支持 WEB 网页端
前言
不同于expo-cli
脚手架创建的项目,以前使用react-native
脚手架创建的 React Native 项目并不能直接运行在网页端。幸运的是,社区中已经有大佬提供了解决方案react-native-web,他实现了一套支持网页端的react-native
组件。我们可以通过打包工具将react-native
库的引用替换为react-native-web
然后再解决一些小问题,就可以将我们的项目打包到网页端运行了。
安装依赖
npm i react-dom react-native-web
或
yarn add react-dom react-native-web
。
再安装webpack
打包工具和一些插件:
npm i -D webpack webpack-cli webpack-dev-server babel-loader babel-plugin-react-native-web file-loader react-native-web-image-loader mini-css-extract-plugin css-minimizer-webpack-plugin clean-webpack-plugin html-webpack-plugin
同样的,你可以使用
yarn
。
依赖简要说明
webpack
、webpack-cli
是打包工具所需依赖,webpack-dev-server
是启动开发环境(在一个端口上运行网页应用)所需依赖。babel-loader
是为了解析JavaScript
不同 ECMAScript 语法和语法糖,让浏览器可以识别并运行的关键。babel-plugin-react-native-web
是react-native-web
的一个插件,可以自动设置别名,把react-native
的引用换为react-native-web
。file-loader
是为了让webpack
解析项目中引入的除.js
外的一些文件,比如图片.jpg
音频.mp3
视频.mp4
等等。react-native-web-image-loader
是为了让webpack
解析react-native
中使用的@2x
@3x
的图片资源,并且能解决项目代码中使用Image
组件图片没有设置宽高属性导致运行在网页端后图片不显示的问题。这个加载器很好的将项目中用到的图片解析成一个对象:AdaptiveImage { "data": { "uri": "static/media/pic1.abcd1234.png", "uri@2x": "static/media/pic1@2x.4321dcba.png", "uri@3x": "static/media/pic1-3x.efgh5678.png", "width": 128, "height": 64 }, get uri(), // returns uri based on pixel ratio get width(), // returns this.data.width get height(), // returns this.data.height }
项目运行在网页端时,会根据
window.devicePixelRatio
的值拿到不同@2x
@3x
的图片。mini-css-extract-plugin
css-minimizer-webpack-plugin
是用来解析.css
文件的,并压缩优化样式。clean-webpack-plugin
插件会在每次运行webpack
打包前删除之前打包产生的文件。html-webpack-plugin
是为了生成网页的index.html
供应用访问。
添加 Webpack 配置
创建一个web
文件夹,和ios
android
文件夹平级,这样方便进行项目管理。
在web
文件夹下创建webpack.config.js
文件,进行webpack
配置。
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const resolvePath = (relativePath) => path.resolve(__dirname, relativePath);
module.exports = {
mode: 'development',
entry: resolvePath('../index.js'),
output: {
filename: '[name].[contenthash:8].js',
path: resolvePath('build'),
publicPath: '/',
},
// 也可以用 source-map,但是在启动时如果项目很大会比较耗时,好处是显示的错误信息更加充分
// 可以参考:https://webpack.js.org/configuration/devtool/
devtool: 'cheap-module-source-map',
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
inject: 'body',
template: resolvePath('index.html'),
}),
new MiniCssExtractPlugin({
filename: '[name].[contenthash:8].css',
}),
],
module: {
rules: [
// 解析 js 文件
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
presets: ['module:metro-react-native-babel-preset'],
// 如果项目用到了装饰器等语法糖,可能需要添加相应的插件进行解析
plugins: ['react-native-web'],
configFile: false,
},
},
},
// 解析项目用到的音频等素材
{
test: /\.(mp3|mp4)$/,
use: {
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'sounds',
esModule: false,
},
},
},
// 解析项目使用的图片资源
{
test: /\.(png|jpe?g|gif)$/,
options: {
name: '[name].[hash:8].[ext]',
outputPath: 'images',
scalings: { '@2x': 2, '@3x': 3 },
esModule: false,
},
loader: 'react-native-web-image-loader',
},
// 解析项目用到的css样式
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
resolve: {
alias: {
'react-native{{ content }}#x27;: 'react-native-web'
},
// 优先使用 .web.js 后缀的文件
extensions: ['.web.js', '.js'],
},
};
添加 index.html
在web
文件夹下添加index.html
网页端访问时的入口文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
添加快捷脚本
在package.json
的scripts
中添加start:web
,方便之后启动网页端项目开发。
{
"scripts": {
"这只是注释": "在存在的其他项后面添加下面的内容",
"start:web": "webpack serve --config web/webpack.config.js"
}
}
然后就可以通过npm run start:web
或者yarn start:web
命令启动网页端开发环境了。
运行网页端应用
在项目根目录index.js
入口文件调用运行应用的方法AppRegistry.runApplication
:
import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
AppRegistry.registerComponent(appName, () => App);
AppRegistry.runApplication(appName, { rootTag: document.getElementById('root') });
如果项目index.js
配置了很多原生端才有的东西,可以新建一个index.web.js
文件针对网页端进行特殊处理。
然后使用npm run start:web
或yarn start:web
启动应用。
调试
如果你的项目安装了很多三方组件库,可能存在一些依赖库是只适用于原生平台,调用了NativeModule
原生模块,并不适用于网页端,那么我们可以先使用一个空壳组件代替它。
先确定是哪一个依赖库报错
找到每个报错末尾出模块来源,分析依赖关系,比如上图
react-native-sound
库存在报错。在
web
文件夹下创建一个polyfills
文件夹,再在其中创建Sound.js
文件,方便对这类文件进行管理。export default {}
然后在
webpack
配置中添加alias
配置:// webpack.config.js 文件 // .... 省略大部分配置,可看上文 module.exports = { mode: 'development', entry: resolvePath('../index.js'), module: { // 省略 }, resolve: { alias: { 'react-native{{ content }}#x27;: 'react-native-web', 'react-native-sound': resolvePath('polyfills/Sound.js') }, // 省略 }, };
也就是将
react-native-sound
组件先用一个空壳代替,让项目先运行起来,之后再去实现可用于网页端的特定代码。
生产环境优化
实际情况下,我们需要将项目打包为静态文件,也需要对项目文件进行必要的压缩和处理。
添加
build:web
命令打包项目为静态文件{ "scripts": { "start:web": "webpack serve --config web/webpack.config.js", "build:web": "webpack --env production --config web/webpack.config.js" } }
配置
webpack
生产做一些优化const TerserPlugin = require('terser-webpack-plugin'); const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); // 从上文导出一个对象修改为导出一个函数 module.exports = (env) => { const isProduction = !!env.production; return { mode: isProduction ? 'production' : 'development', entry: resolvePath('../index.js'), // 省略其他没有区分生产的配置,可见上文 devtool: isProduction ? false : 'cheap-module-source-map', optimization: { // 开发环境不需要进行压缩 minimize: isProduction, minimizer: [ // CSS 样式压缩 new CssMinimizerPlugin(), // JS 文件优化 new TerserPlugin({ extractComments: 'all', terserOptions: { compress: { // 生产上去掉日志输出 drop_console: true, }, }, }), ], }, } }
代码分割、拆包(可选)
有时项目太大会导致打包生产的bundle
文件较大,我们可以通过代码分割进行一个拆包。详细配置可见Code Splitting。
return {
// 省略
optimization: {
// 省略
minimizer: [
// 省略
],
splitChunks: {
chunks: 'all',
cacheGroups: {
default: {
name: 'common',
// 模块被引用2次以上的才拆分
minChunks: 2,
priority: -10,
},
// 拆分第三方库(node_modules 中的模块都会拆到一起)
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
priority: -9,
},
// 把 node_modules 中的一个库单独拆出来
checkbox_module: {
// 有的库小于 20kb 不会被单独拆出来,因为默认 minSize 值为 20000 (bytes)
// 所以如果你也想拆出来就需要降低 miniSize
minSize: 0,
test: /[\\/]node_modules[\\/]react-native-check-box[\\/]/,
name: 'checkbox_module',
priority: -8,
},
},
},
// 运行时模块
runtimeChunk: {
name: (entrypoint) => `runtime-${entrypoint.name}`,
},
},
}
补充
可参考案例项目react-native-web-example,实际情况下,我们需要添加网页端支持的项目都较为复杂,大概率会出现一些小问题,就需要我们耐心的一个一个解决。我自己在使用中遇到的很多问题汇总在React Native Web 常见问题解决方案大家可以参考。
版权声明:
Anand's Blog文章皆为站长Anand Zhang原创内容,转载请注明出处。
包括商业转载在内,注明下方要求的文章出处信息即可,无需联系站长授权。
请尊重他人劳动成果,用爱发电十分不易,谢谢!
请注明出处:
本文出自:Anand's Blog