React Native && Expo框架整理
React Native项目支持的项目配置插件
一、配置文件(如 .babelrc 或 babel.config.js)
presets: [“module:metro-react-native-babel-preset”]
- @babel/plugin-proposal-export-namespace-from: Babel 插件,用于将 ES6 模块中的星号导出(export * from ‘module’)转换为 ES5 代码
- @babel/plugin-proposal-decorators: 开启装饰者模式
- @babel/plugin-proposal-optional-catch-binding: 在进行异常捕获时,有时我们并不想强制要求捕获到的异常必须被绑定到一个变量中,而是希望在有异常时只执行一些操作,而不必在意异常本身的信息
- transform-inline-environment-variables: RN配置环境变量
- @babel/plugin-proposal-export-namespace-from: 用于将 ES6 模块中的星号导出(export * from ‘module’)转换为 ES5 代码
- react-native-reanimated/plugin: 使用react-native-reanimated库需要引用这个babel库
- transform-remove-console: 优化性能,删除掉代码中的console,可以配置: [‘transform-remove-console’, { exclude: [‘tron’, ‘info’, ‘error’] }]
- module-resolver: 配置alias
[
'module-resolver',
{
extensions: ['.ios.js', '.android.js', '.js', '.jsx', '.ts', '.tsx', '.json'],
alias: {
'@app': './app/',
'@@': './',
},
},
]
二、拆包分包打包
metro.business.config.js
const fs = require('fs')
const { argv } = require('process')
const platformIndex = argv.indexOf('--platform')
let platformVal = platformIndex !== -1 && argv[platformIndex + 1] ? argv[platformIndex + 1] : null
console.log('[metro.business.config] platformVal:', platformVal)
// 读取 moduleIdList.txt,转换为数组
const moduleIdList = fs
.readFileSync(`./build-dist/common/${platformVal}/moduleIdList.txt`, 'utf8')
.toString()
.split('\n')
function getModuleId(path) {
// 根据文件的相对路径构建 moduleId
const projectRootPath = __dirname
let moduleId = path.substr(projectRootPath.length + 1)
return moduleId
}
function createModuleIdFactory() {
return getModuleId
}
function processModuleFilter(module) {
//const modulePath = getProjectPath(module['path'])
const moduleId = getModuleId(module['path'])
if (moduleIdList.indexOf(moduleId) >= 0) {
// 过滤掉上一步记录的基础模块的moduleId
return false
}
return true
}
module.exports = {
serializer: {
createModuleIdFactory,
processModuleFilter,
},
}
metro.common.config.js
通常将版本稳定不变的包放到common包中(react、react-native)
// 为了避免 moduleId 重复,目前业内主流的做法是把模块的文件路径当作 moduleId。metro 暴露了 createModuleIdFactory() 这个函数,可以在这个函数里自定义moduleId 的生成逻辑。
const fs = require('fs')
const { argv } = require('process')
const platformIndex = argv.indexOf('--platform')
let platformVal = platformIndex !== -1 && argv[platformIndex + 1] ? argv[platformIndex + 1] : null
console.log('[metro.common.config] platformVal:', platformVal)
function getModuleId(path) {
// 根据文件的相对路径构建 moduleId
const projectRootPath = __dirname
let moduleId = path.substr(projectRootPath.length + 1)
return moduleId
}
function createModuleIdFactory() {
return getModuleId
}
function processModuleFilter(module) {
const moduleId = getModuleId(module['path'])
// 把 moduleId 写入 moduleIdList.txt 文件,记录基础模块的moduleId
fs.appendFileSync(`./build-dist/common/${platformVal}/moduleIdList.txt`, `${moduleId}\n`)
return true
}
module.exports = {
serializer: {
createModuleIdFactory,
processModuleFilter,
},
}
三、打包命令
“build”: “node build.js --hbc-mode --linux --env=fat --business-name myAppName --common --business-entry index.js”
build.js
/**
* 构建包
* @param --common 构建公共包
* @param --business-name {String} 业务名称
* @param --business-entry {String} 业务主入口,默认:index.js
* @example 仅打业务包: node build.js --business-name fund-index --business-entry index.js
* @example 打公共包+业务包: node build.js --business-name fund-index --common --business-entry index.js
* @example 仅打公共包: node build.js --common
*/
const AdmZip = require('adm-zip')
var child_process = require('child_process')
const path = require('path')
const fs = require('fs')
const { argv } = require('process')
const cwdPath = process.cwd()
console.log('[build] cwdPath:', cwdPath)
const commonIndex = argv.indexOf('--common')
const busNameIndex = argv.indexOf('--business-name')
const isLinux = argv.indexOf('--linux') > -1
const isHbcMode = argv.indexOf('--hbc-mode') > -1
const isFat = argv.indexOf('--env=fat') > -1
const bundleFilePrefix = isHbcMode ? '-hbc' : ''
let busNameVal = busNameIndex !== -1 && argv[busNameIndex + 1] ? argv[busNameIndex + 1] : null
console.log('[build] busNameVal:', busNameVal)
const busEntryIndex = argv.indexOf('--business-entry')
let busEntryVal =
busEntryIndex !== -1 && argv[busEntryIndex + 1] ? argv[busEntryIndex + 1] : 'index.js'
console.log('[build] busEntryVal:', busEntryVal)
const buildDist = path.join(cwdPath, 'build-dist')
// 清空影响缓存的文件
if (fs.existsSync(buildDist)) {
fs.rmdirSync(buildDist, { recursive: true, force: true })
}
fs.mkdirSync(buildDist)
if (commonIndex !== -1) {
console.log('[build] start build [common] ...')
fs.mkdirSync(path.join(buildDist, 'common'))
fs.mkdirSync(path.join(buildDist, 'common', 'ios'))
fs.mkdirSync(path.join(buildDist, 'common', 'android'))
child_process.execSync(
'npx react-native bundle --platform ios --config metro.common.config.js --dev false --entry-file metro.common.js --bundle-output build-dist/common/ios/index.bundle --assets-dest build-dist/common/ios',
{ cwd: cwdPath },
)
child_process.execSync(
'npx react-native bundle --platform android --config metro.common.config.js --dev false --entry-file metro.common.js --bundle-output build-dist/common/android/index.bundle --assets-dest build-dist/common/android',
{ cwd: cwdPath },
)
if (isHbcMode) {
fs.mkdirSync(path.join(buildDist, 'common', 'ios' + bundleFilePrefix))
fs.mkdirSync(path.join(buildDist, 'common', 'android' + bundleFilePrefix))
if (isLinux) {
console.log('[build] build [common:hbc] by linux system')
child_process.execSync(
'./node_modules/hermes-engine/linux64-bin/hermesc -emit-binary -out ./build-dist/common/android-hbc/index.bundle -output-source-map ./build-dist/common/android/index.bundle',
{ cwd: cwdPath },
)
child_process.execSync(
'./node_modules/hermes-engine/linux64-bin/hermesc -emit-binary -out ./build-dist/common/ios-hbc/index.bundle -output-source-map ./build-dist/common/ios/index.bundle',
{ cwd: cwdPath },
)
} else {
child_process.execSync(
'./node_modules/hermes-engine/osx-bin/hermesc -emit-binary -out ./build-dist/common/android-hbc/index.bundle -output-source-map ./build-dist/common/android/index.bundle',
{ cwd: cwdPath },
)
child_process.execSync(
'./node_modules/hermes-engine/osx-bin/hermesc -emit-binary -out ./build-dist/common/ios-hbc/index.bundle -output-source-map ./build-dist/common/ios/index.bundle',
{ cwd: cwdPath },
)
}
console.log('[build] build [common:hbc] complete')
}
const rnCommonZip = new AdmZip()
const iosCommonZip = new AdmZip()
const androidCommonZip = new AdmZip()
const webCommonZip = new AdmZip()
androidCommonZip.addLocalFile(
path.join(buildDist, 'common', 'android' + bundleFilePrefix, 'index.bundle'),
)
iosCommonZip.addLocalFile(
path.join(buildDist, 'common', 'ios' + bundleFilePrefix, 'index.bundle'),
)
rnCommonZip.addFile('ios.zip', iosCommonZip.toBuffer())
rnCommonZip.addFile('android.zip', androidCommonZip.toBuffer())
rnCommonZip.addFile('web.zip', webCommonZip.toBuffer())
rnCommonZip.writeZip(path.join(buildDist, 'rn-common.zip'))
console.log('[build] build [common] complete')
}
if (busNameVal) {
console.log('[build] start build [business] ...')
fs.mkdirSync(path.join(buildDist, 'business'))
fs.mkdirSync(path.join(buildDist, 'business', 'ios'))
fs.mkdirSync(path.join(buildDist, 'business', 'android'))
// 修改入口文件的注册模块名称为业务包名称
let entryFilePath = path.join(cwdPath, busEntryVal)
// 读取入口文件内容,并替换注册的模块名称
let newEntryFileContent = fs
.readFileSync(entryFilePath)
.toString()
.replace(
/AppRegistry\.registerComponent\((['"])[^'"]+\1/,
`AppRegistry.registerComponent($1${busNameVal}$1`,
)
let newEntryFilePath = path.join(cwdPath, busEntryVal.replace(/\.js$/, '.tmp.js'))
// 创建临时主入口文件,并写入新的文件内容(替换模块名称后的内容)
fs.writeFileSync(newEntryFilePath, newEntryFileContent)
child_process.execSync(
`ENV=${
isFat ? 'fat' : 'prd'
} npx react-native bundle --platform ios --config metro.business.config.js --dev false --entry-file ${newEntryFilePath} --bundle-output build-dist/business/ios/index.bundle --assets-dest build-dist/business/ios`,
{ cwd: cwdPath },
)
child_process.execSync(
`ENV=${
isFat ? 'fat' : 'prd'
} npx react-native bundle --platform android --config metro.business.config.js --dev false --entry-file ${newEntryFilePath} --bundle-output build-dist/business/android/index.bundle --assets-dest build-dist/business/android`,
{ cwd: cwdPath },
)
if (isHbcMode) {
fs.mkdirSync(path.join(buildDist, 'business', 'ios' + bundleFilePrefix))
fs.mkdirSync(path.join(buildDist, 'business', 'android' + bundleFilePrefix))
if (isLinux) {
console.log('[build] build [business:hbc] by linux system')
child_process.execSync(
'./node_modules/hermes-engine/linux64-bin/hermesc -emit-binary -out ./build-dist/business/ios-hbc/index.bundle -output-source-map ./build-dist/business/ios/index.bundle',
{ cwd: cwdPath, maxBuffer: 2048 * 2048 },
)
child_process.execSync(
'./node_modules/hermes-engine/linux64-bin/hermesc -emit-binary -out ./build-dist/business/android-hbc/index.bundle -output-source-map ./build-dist/business/android/index.bundle',
{ cwd: cwdPath, maxBuffer: 2048 * 2048 },
)
} else {
child_process.execSync(
'./node_modules/hermes-engine/osx-bin/hermesc -emit-binary -out ./build-dist/business/android-hbc/index.bundle -output-source-map ./build-dist/business/android/index.bundle',
{ cwd: cwdPath },
)
child_process.execSync(
'./node_modules/hermes-engine/osx-bin/hermesc -emit-binary -out ./build-dist/business/ios-hbc/index.bundle -output-source-map ./build-dist/business/ios/index.bundle',
{ cwd: cwdPath },
)
}
console.log('[build] build [business:hbc] complete')
}
// 删除临时主入口文件
fs.rmSync(newEntryFilePath, { recursive: true, force: true })
console.log('[build] start build [web] ...')
child_process.execSync(`ENV=${isFat ? 'fat' : 'prd'} npx expo build:web --no-pwa`, {
cwd: cwdPath,
})
console.log('[build] build [web] complete')
const webBuild = path.join(cwdPath, 'web-build')
console.log('[build] webBuild:', webBuild)
const rnBusZip = new AdmZip()
const webBusZip = new AdmZip()
const iosBusZip = new AdmZip()
const androidBusZip = new AdmZip()
androidBusZip.addLocalFile(
path.join(buildDist, 'business', 'android' + bundleFilePrefix, 'index.bundle'),
)
iosBusZip.addLocalFile(path.join(buildDist, 'business', 'ios' + bundleFilePrefix, 'index.bundle'))
webBusZip.addLocalFolder(webBuild)
webBusZip.writeZip('web.zip')
rnBusZip.addFile('ios.zip', iosBusZip.toBuffer())
rnBusZip.addFile('android.zip', androidBusZip.toBuffer())
rnBusZip.addFile('web.zip', webBusZip.toBuffer())
rnBusZip.writeZip(path.join(buildDist, `rn-${busNameVal}.zip`))
webBusZip.writeZip(path.join(buildDist, 'web.zip'))
iosBusZip.writeZip(path.join(buildDist, 'ios.zip'))
androidBusZip.writeZip(path.join(buildDist, 'android.zip'))
// 删除临时web包
fs.rmSync(path.join(cwdPath, 'web.zip'), { recursive: true, force: true })
console.log('[build] build [business] complete')
} else {
console.log('[build] no business')
}