React Native 使用官方Metro进行Android 分包

一、分包原理

(1) Bundle文件结构及内容说明

React Native打包形成的Bundle文件的内容从上到下依次是:
Polyfills:定义基本的JS环境(如:__d()函数、__r()函数、DEV 变量等)
Module定义:使用__d()函数定义所有用到的模块,该函数为每个模块赋予了一个模块ID,模块之间的依赖关系都是通过这个ID进行关联的。
Require调用:使用__r()函数引用根模块。

var __BUNDLE_START_TIME__=this.nativePerformanceNow?nativePerformanceNow():Date.now(),__DEV__=false,process=this.process||{};process.env=process.env||{};process.env.NODE_ENV=process.env.NODE_ENV||"production";
!(function(r){"use strict";r.__r=o,r.__d=function(r,i,n){if(null!=e[i])return;var o={dependencyMap:n,factory:r,hasError:!1,importedAll:t,importedDefault:t,isInitialized:!1,publicModule:{exports:{}}};e[i]=o},r.__c=n,r.__registerSegment=function(r,e){s[r]=e};var e=n(),t={},i={}.hasOwnProperty;function n(){return e=Object.create(null)}function o(r){var t=r,i=e[t];return i&&i.isInitialized?i.publicModule.exports:d(t,i)}function l(r){var i=r;if(e[i]&&e[i].importedDefault!==t)return e[i].importedDefault;var n=o(i),l=n&&n.__esModule?n.default:n;return e[i].importedDefault=l}function u(r){var n=r;if(e[n]&&e[n].importedAll!==t)return e[n].importedAll;var l,u=o(n);if(u&&u.__esModule)l=u;else{if(l={},u)for(var a in u)i.call(u,a)&&(l[a]=u[a]);l.default=u}return e[n].importedAll=l}o.importDefault=l,o.importAll=u;var a=!1;function d(e,t){if(!a&&r.ErrorUtils){var i;a=!0;try{i=v(e,t)}catch(e){r.ErrorUtils.reportFatalError(e)}return a=!1,i}return v(e,t)}var c=16,f=65535;function p(r){return{segmentId:r>>>c,localId:r&f}}o.unpackModuleId=p,o.packModuleId=function(r){return(r.segmentId<<c)+r.localId};var s=[];function v(t,i){if(!i&&s.length>0){var n=p(t),a=n.segmentId,d=n.localId,c=s[a];null!=c&&(c(d),i=e[t])}var f=r.nativeRequire;if(!i&&f){var v=p(t),h=v.segmentId;f(v.localId,h),i=e[t]}if(!i)throw Error('Requiring unknown module "'+t+'".');if(i.hasError)throw m(t,i.error);i.isInitialized=!0;var I=i,g=I.factory,y=I.dependencyMap;try{var _=i.publicModule;return _.id=t,g(r,o,l,u,_,_.exports,y),i.factory=void 0,i.dependencyMap=void 0,_.exports}catch(r){throw i.hasError=!0,i.error=r,i.isInitialized=!1,i.publicModule.exports=void 0,r}}function m(r,e){return Error('Requiring module "'+r+'", which threw an exception: '+e)}})('undefined'!=typeof globalThis?globalThis:'undefined'!=typeof global?global:'undefined'!=typeof window?window:this);
!(function(n){var e=(function(){function n(n,e){return n}function e(n){var e={};return n.forEach(function(n,r){e[n]=!0}),e}function r(n,r,l){if(n.formatValueCalls++,n.formatValueCalls>200)return"[TOO BIG formatValueCalls "+n.formatValueCalls+" exceeded limit of 200]";var f=t(n,r);if(f)return f;var c=Object.keys(r),g=e(c);if(d(r)&&(c.indexOf('message')>=0||c.indexOf('description')>=0))return o(r);if(0===c.length){if(h(r)){var p=r.name?': '+r.name:'';return n.stylize('[Function'+p+']','special')}if(s(r))return n.stylize(RegExp.prototype.toString.call(r),'regexp');if(y(r))return n.stylize(Date.prototype.toString.call(r),'date');if(d(r))return o(r)}var v,b,m='',j=!1,z=['{','}'];(v=r,Array.isArray(v)&&(j=!0,z=['[',']']),h(r))&&(m=' [Function'+(r.name?': '+r.name:'')+']');return s(r)&&(m=' '+RegExp.prototype.toString.call(r)),y(r)&&(m=' '+Date.prototype.toUTCString.call(r)),d(r)&&(m=' '+o(r)),0!==c.length||j&&0!=r.length?l<0?s(r)?n.stylize(RegExp.prototype.toString.call(r),'regexp'):n.stylize('[Object]','special'):(n.seen.push(r),b=j?i(n,r,l,g,c):c.map(function(e){return a(n,r,l,g,e,j)}),n.seen.pop(),u(b,m,z)):z[0]+m+z[1]}function t(n,e){if(g(e))return n.stylize('undefined','undefined');if('string'==typeof e){var r="'"+JSON.stringify(e).replace(/^"|"$/g,'').replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return n.stylize(r,'string')}return c(e)?n.stylize(''+e,'number'):l(e)?n.stylize(''+e,'boolean'):f(e)?n.stylize('null','null'):void 0}function o(n){return'['+Error.prototype.toString.call(n)+']'}function i(n,e,r,t,o){for(var i=[],u=0,l=e.length;u<l;++u)b(e,String(u))?i.push(a(n,e,r,t,String(u),!0)):i.push('');return o.forEach(function(o){o.match(/^\d+$/)||i.push(a(n,e,r,t,o,!0))}),i}function a(n,e,t,o,i,a){var u,l,c;if((c=Object.getOwnPropertyDescriptor(e,i)||{value:e[i]}).get?l=c.set?n.stylize('[Getter/Setter]','special'):n.stylize('[Getter]','special'):c.set&&(l=n.stylize('[Setter]','special')),b(o,i)||(u='['+i+']'),l||(n.seen.indexOf(c.value)<0?(l=f(t)?r(n,c.value,null):r(n,c.value,t-1)).indexOf('\n')>-1&&(l=a?l.split('\n').map(function(n){return'  '+n}).join('\n').substr(2):'\n'+l.split('\n').map(function(n){return'   '+n}).join('\n')):l=n.stylize('[Circular]','special')),g(u)){if(a&&i.match(/^\d+$/))return l;(u=JSON.stringify(''+i)).match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)?(u=u.substr(1,u.length-2),u=n.stylize(u,'name')):(u=u.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'"),u=n.stylize(u,'string'))}return u+': '+l}function u(n,e,r){return n.reduce(function(n,e){return 0,e.indexOf('\n')>=0&&0,n+e.replace(/\u001b\[\d\d?m/g,'').length+1},0)>60?r[0]+(''===e?'':e+'\n ')+' '+n.join(',\n  ')+' '+r[1]:r[0]+e+' '+n.join(', ')+' '+r[1]}function l(n){return'boolean'==typeof n}function f(n){return null===n}function c(n){return'number'==typeof n}function g(n){return void 0===n}function s(n){return p(n)&&'[object RegExp]'===v(n)}function p(n){return'object'==typeof n&&null!==n}function y(n){return p(n)&&'[object Date]'===v(n)}function d(n){return p(n)&&('[object Error]'===v(n)||n instanceof Error)}function h(n){return'function'==typeof n}function v(n){return Object.prototype.toString.call(n)}function b(n,e){return Object.prototype.hasOwnProperty.call(n,e)}return function(e,t){return r({seen:[],formatValueCalls:0,stylize:n},e,t.depth)}})(),r='(index)',t={trace:0,info:1,warn:2,error:3},o=[];o[t.trace]='debug',o[t.info]='log',o[t.warn]='warning',o[t.error]='error';var i=1;function a(r){return function(){var a;a=1===arguments.length&&'string'==typeof arguments[0]?arguments[0]:Array.prototype.map.call(arguments,function(n){return e(n,{depth:10})}).join(', ');var u=r;'Warning: '===a.slice(0,9)&&u>=t.error&&(u=t.warn),n.__inspectorLog&&n.__inspectorLog(o[u],a,[].slice.call(arguments),i),g.length&&(a=s('',a)),n.nativeLoggingHook(a,u)}}function u(n,e){return Array.apply(null,Array(e)).map(function(){return n})}var l="\u2502",f="\u2510",c="\u2518",g=[];function s(n,e){return g.join('')+n+' '+(e||'')}if(n.nativeLoggingHook){n.console;n.console={error:a(t.error),info:a(t.info),log:a(t.info),warn:a(t.warn),trace:a(t.trace),debug:a(t.trace),table:function(e){if(!Array.isArray(e)){var o=e;for(var i in e=[],o)if(o.hasOwnProperty(i)){var a=o[i];a[r]=i,e.push(a)}}if(0!==e.length){var l=Object.keys(e[0]).sort(),f=[],c=[];l.forEach(function(n,r){c[r]=n.length;for(var t=0;t<e.length;t++){var o=(e[t][n]||'?').toString();f[t]=f[t]||[],f[t][r]=o,c[r]=Math.max(c[r],o.length)}});for(var g=y(c.map(function(n){return u('-',n).join('')}),'-'),s=[y(l),g],p=0;p<e.length;p++)s.push(y(f[p]));n.nativeLoggingHook('\n'+s.join('\n'),t.info)}else n.nativeLoggingHook('',t.info);function y(n,e){var r=n.map(function(n,e){return n+u(' ',c[e]-n.length).join('')});return e=e||' ',r.join(e+'|'+e)}},group:function(e){n.nativeLoggingHook(s(f,e),t.info),g.push(l)},groupEnd:function(){g.pop(),n.nativeLoggingHook(s(c),t.info)},groupCollapsed:function(e){n.nativeLoggingHook(s(c,e),t.info),g.push(l)},assert:function(e,r){e||n.nativeLoggingHook('Assertion failed: '+r,t.error)}}}else if(!n.console){var p=n.print||function(){};n.console={error:p,info:p,log:p,warn:p,trace:p,debug:p,table:p}}})('undefined'!=typeof globalThis?globalThis:'undefined'!=typeof global?global:'undefined'!=typeof window?window:this);
!(function(n){var r=0,t=function(n,r){throw n},l={setGlobalHandler:function(n){t=n},getGlobalHandler:function(){return t},reportError:function(n){t&&t(n,!1)},reportFatalError:function(n){t&&t(n,!0)},applyWithGuard:function(n,t,u,o,e){try{return r++,n.apply(t,u)}catch(n){l.reportError(n)}finally{r--}return null},applyWithGuardIfNeeded:function(n,r,t){return l.inGuard()?n.apply(r,t):(l.applyWithGuard(n,r,t),null)},inGuard:function(){return!!r},guard:function(n,r,t){var u;if('function'!=typeof n)return console.warn('A function must be passed to ErrorUtils.guard, got ',n),null;var o=null!=(u=null!=r?r:n.name)?u:'<generated guard>';return function(){for(var r=arguments.length,u=new Array(r),e=0;e<r;e++)u[e]=arguments[e];return l.applyWithGuard(n,null!=t?t:this,u,null,o)}}};n.ErrorUtils=l})('undefined'!=typeof globalThis?globalThis:'undefined'!=typeof global?global:'undefined'!=typeof window?window:this);
'undefined'!=typeof globalThis?globalThis:'undefined'!=typeof global?global:'undefined'!=typeof window&&window,(function(){'use strict';var e=Object.prototype.hasOwnProperty;'function'!=typeof Object.entries&&(Object.entries=function(n){if(null==n)throw new TypeError('Object.entries called on non-object');var o=[];for(var t in n)e.call(n,t)&&o.push([t,n[t]]);return o}),'function'!=typeof Object.values&&(Object.values=function(n){if(null==n)throw new TypeError('Object.values called on non-object');var o=[];for(var t in n)e.call(n,t)&&o.push(n[t]);return o})})();
__d(function(g,r,i,a,m,e,d){r(d[0]),r(d[1])},1,[2,55]);
__d(function(g,r,i,a,m,e,d){'use strict';r(d[0]);var t=r(d[1]);m.exports={get AccessibilityInfo(){return r(d[2])},get ActivityIndicator(){return r(d[3])},get ART(){return t('art-moved',"React Native ART has been extracted from react-native core and will be removed in a future release. It can now be installed and imported from '@react-native-community/art' instead of 'react-native'. See https://github.com/react-native-community/art"),r(d[4])},get Button(){return r(d[5])},get CheckBox(){return t('checkBox-moved',"CheckBox has been extracted from react-native core and will be removed in a future release. It can now be installed and imported from '@react-native-community/checkbox' instead of 'react-native'. See https://github.com/react-native-community/react-native-checkbox"),r(d[6])},get DatePickerIOS(){return t('DatePickerIOS-merged',"DatePickerIOS has been merged with DatePickerAndroid and will be removed in a future release. It can now be installed and imported from '@react-native-community/datetimepicker' instead of 'react-native'. See https://github.com/react-native-community/react-native-datetimepicker"),r(d[7])},get DrawerLayoutAndroid(){return r(d[8])},get FlatList(){return r(d[9])},get Image(){return r(d[10])},get ImageBackground(){return r(d[11])},get InputAccessoryView(){return r(d[12])},get KeyboardAvoidingView(){return r(d[13])},get MaskedViewIOS(){return t('maskedviewios-moved',"MaskedViewIOS has been extracted from react-native core and will be removed in a future release. It can now be installed and imported from '@react-native-community/masked-view' instead of 'react-native'. See https://github.com/react-native-community/react-native-masked-view"),r(d[14])},get Modal(){return r(d[15])},get Picker(){return r(d[16])},get PickerIOS(){return r(d[17])},get ProgressBarAndroid(){return r(d[18])},get ProgressViewIOS(){return r(d[19])},get SafeAreaView(){return r(d[20])},get ScrollView(){return r(d[21])},get SectionList(){return r(d[22])},get SegmentedControlIOS(){return r(d[23])},get Slider(){return t('slider-moved',"Slider has been extracted from react-native core and will be removed in a future release. It can now be installed and imported from '@react-native-community/slider' instead of 'react-native'. See https://github.com/react-native-community/react-native-slider"),r(d[24])},get Switch(){return r(d[25])},get RefreshControl(){return r(d[26])},get StatusBar(){return r(d[27])},get Text(){return r(d[28])},get TextInput(){return r(d[29])},get Touchable(){return r(d[30])},get TouchableHighlight(){return r(d[31])},get TouchableNativeFeedback(){return r(d[32])},get TouchableOpacity(){return r(d[33])},get TouchableWithoutFeedback(){return r(d[34])},get View(){return r(d[35])},get VirtualizedList(){return r(d[36])},get VirtualizedSectionList(){return r(d[37])},get ActionSheetIOS(){return r(d[38])},get Alert(){return r(d[39])},get Animated(){return r(d[40])},get AppRegistry(){return r(d[41])},get AppState(){return r(d[42])},get AsyncStorage(){return t('async-storage-moved',"AsyncStorage has been extracted from react-native core and will be removed in a future release. It can now be installed and imported from '@react-native-community/async-storage' instead of 'react-native'. See https://github.com/react-native-community/react-native-async-storage"),r(d[43])},get BackHandler(){return r(d[44])},get Clipboard(){return r(d[45])},get DatePickerAndroid(){return t('DatePickerAndroid-merged',"DatePickerAndroid has been merged with DatePickerIOS and will be removed in a future release. It can now be installed and imported from '@react-native-community/datetimepicker' instead of 'react-native'. See https://github.com/react-native-community/react-native-datetimepicker"),r(d[46])},get DeviceInfo(){return r(d[47])},get Dimensions(){return r(d[48])},get Easing(){return r(d[49])},get findNodeHandle(){return r(d[50]).findNodeHandle},get I18nManager(){return r(d[51])},get ImagePickerIOS(){return t('imagePickerIOS-moved',"ImagePickerIOS has been extracted from react-native core and will be removed in a future release. Please upgrade to use either '@react-native-community/react-native-image-picker' or 'expo-image-picker'. If you cannot upgrade to a different library, please install the deprecated '@react-native-community/image-picker-ios' package. See https://github.com/react-native-community/react-native-image-picker-ios"),r(d[52])},get InteractionManager(){return r(d[53])},get Keyboard(){return r(d[54])},get LayoutAnimation(){return r(d[55])},get Linking(){return r(d[56])},get NativeDialogManagerAndroid(){return r(d[57]).default},get NativeEventEmitter(){return r(d[58])},get PanResponder(){return r(d[59])},get PermissionsAndroid(){return r(d[60])},get PixelRatio(){return r(d[61])},get PushNotificationIOS(){return t('pushNotificationIOS-moved',"PushNotificationIOS has been extracted from react-native core and will be removed in a future release. It can now be installed and imported from '@react-native-community/push-notification-ios' instead of 'react-native'. See https://github.com/react-native-community/react-native-push-notification-ios"),r(d[62])},get Settings(){return r(d[63])},get Share(){return r(d[64])},get StatusBarIOS(){return t('StatusBarIOS-merged','StatusBarIOS has been merged with StatusBar and will be removed in a future release. Use StatusBar for mutating the status bar'),r(d[65])},get StyleSheet(){return r(d[66])},get Systrace(){return r(d[67])},get TimePickerAndroid(){return t('TimePickerAndroid-merged',"TimePickerAndroid has been merged with DatePickerIOS and DatePickerAndroid and will be removed in a future release. It can now be installed and imported from '@react-native-community/datetimepicker' instead of 'react-native'. See https://github.com/react-native-community/react-native-datetimepicker"),r(d[68])},get ToastAndroid(){return r(d[69])},get TurboModuleRegistry(){return r(d[70])},get TVEventHandler(){return r(d[71])},get UIManager(){return r(d[72])},get unstable_batchedUpdates(){return r(d[50]).unstable_batchedUpdates},get useWindowDimensions(){return r(d[73]).default},get UTFSequence(){return r(d[74])},get Vibration(){return r(d[75])},get YellowBox(){return r(d[76])},get DeviceEventEmitter(){return r(d[77])},get NativeAppEventEmitter(){return r(d[78])},get NativeModules(){return r(d[79])},get Platform(){return r(d[80])},get processColor(){return r(d[81])},get requireNativeComponent(){return r(d[82])},get unstable_RootTagContext(){return r(d[83])},get ColorPropType(){return r(d[84])},get EdgeInsetsPropType(){return r(d[85])},get PointPropType(){return r(d[86])},get ViewPropTypes(){return r(d[87])}}},2,[3,4,7,52,183,193,284,287,288,250,271,293,294,296,297,299,306,310,180,311,312,257,279,313,314,316,254,291,194,319,200,326,209,217,210,81,251,280,327,137,218,329,344,347,340,349,351,353,60,246,83,304,354,227,261,263,356,138,121,358,360,59,362,364,365,367,58,28,368,370,12,204,44,372,373,374,376,32,147,13,49,75,178,303,64,196,377,273]);
__d(function(g,r,i,a,m,e,d){'use strict';m.exports=function(n,o,t,f,s,u,c,l){if(!n){var v;if(void 0===o)v=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var p=[t,f,s,u,c,l],h=0;(v=new Error(o.replace(/%s/g,function(){return p[h++]}))).name='Invariant Violation'}throw v.framesToPop=1,v}}},3,[]);
__d(function(g,r,i,a,m,e,d){'use strict';var t=r(d[0]),n={};m.exports=function(c,o){n[c]||(t(!1,o),n[c]=!0)}},4,[5]);
__d(function(g,r,i,a,m,e,d){'use strict';var t=r(d[0]);m.exports=t},5,[6]);
__d(function(g,r,i,a,m,e,d){"use strict";function t(t){return function(){return t}}var n=function(){};n.thatReturns=t,n.thatReturnsFalse=t(!1),n.thatReturnsTrue=t(!0),n.thatReturnsNull=t(null),n.thatReturnsThis=function(){return this},n.thatReturnsArgument=function(t){return t},m.exports=n},6,[]);
__d(function(g,r,i,a,m,e,d){'use strict';var n=r(d[0])(r(d[1])),t=r(d[2]),o=r(d[3]),s=new Map,c={isBoldTextEnabled:function(){return Promise.resolve(!1)},isGrayscaleEnabled:function(){return Promise.resolve(!1)},isInvertColorsEnabled:function(){return Promise.resolve(!1)},isReduceMotionEnabled:function(){return new Promise(function(t,o){n.default?n.default.isReduceMotionEnabled(t):o(!1)})},isReduceTransparencyEnabled:function(){return Promise.resolve(!1)},isScreenReaderEnabled:function(){return new Promise(function(t,o){n.default?n.default.isTouchExplorationEnabled(t):o(!1)})},get fetch(){return this.isScreenReaderEnabled},addEventListener:function(n,o){var c;'change'===n||'screenReaderChanged'===n?c=t.addListener("touchExplorationDidChange",function(n){o(n)}):'reduceMotionChanged'===n&&(c=t.addListener("reduceMotionDidChange",function(n){o(n)})),s.set(o,c)},removeEventListener:function(n,t){var o=s.get(t);o&&(o.remove(),s.delete(t))},setAccessibilityFocus:function(n){o.sendAccessibilityEvent(n,o.getConstants().AccessibilityEventTypes.typeViewFocused)},announceForAccessibility:function(t){n.default&&n.default.announceForAccessibility(t)}};m.exports=c},7,[8,9,32,44]);
__d(function(g,r,i,a,m,e,d){m.exports=function(n){return n&&n.__esModule?n:{default:n}}},8,[]);
__d(function(g,r,i,a,m,e,d){'use strict';var t=r(d[0]);Object.defineProperty(e,"__esModule",{value:!0}),e.default=void 0;var u=t(r(d[1])).get('AccessibilityInfo');e.default=u},9,[10,12]);
__d(function(g,r,i,a,m,e,d){var t=r(d[0]);function n(){if("function"!=typeof WeakMap)return null;var t=new WeakMap;return n=function(){return t},t}m.exports=function(o){if(o&&o.__esModule)return o;if(null===o||"object"!==t(o)&&"function"!=typeof o)return{default:o};var u=n();if(u&&u.has(o))return u.get(o);var f={},c=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var p in o)if(Object.prototype.hasOwnProperty.call(o,p)){var l=c?Object.getOwnPropertyDescriptor(o,p):null;l&&(l.get||l.set)?Object.defineProperty(f,p,l):f[p]=o[p]}return f.default=o,u&&u.set(o,f),f}},10,[11]);
__d(function(g,r,i,a,m,e,d){function o(t){"@babel/helpers - typeof";return"function"==typeof Symbol&&"symbol"==typeof("function"==typeof Symbol?Symbol.iterator:"@@iterator")?m.exports=o=function(o){return typeof o}:m.exports=o=function(o){return o&&"function"==typeof Symbol&&o.constructor===Symbol&&o!==("function"==typeof Symbol?Symbol.prototype:"@@prototype")?"symbol":typeof o},o(t)}m.exports=o},11,[]);
...
...
__r(86);
__r(1);

(2) Metro工具

随着React Native 版本迭代,官方已经逐步将bundle文件生成流程规范化,并为此设计了独立的打包模块 – Metro。Metro 通过输入一个需要打包的JS文件及几个配置参数,返回一个包含了所有依赖内容的JS文件。
Metro将打包的过程分为了3个依次执行的阶段:
解析(Resolution):计算得到所有的依赖模块,形成依赖树,该过程是多线程并行执行。
转义(Transformation):将模块内容转义为React Native可识别的格式,该过程是多线程并行执行。
序列化(Serialization):将所有的模块合并到一个文件中输出。
Metro工具提供了配置功能,开发人员可以通过配置RN项目中的metro.config.js文件修改bundle文件的生成流程。

新拆包方法主要关注的是Metro工具在“序列化”阶段时调用的 createModuleIdFactory(path)方法和processModuleFilter(module)
createModuleIdFactory(path)是传入的模块绝对路径path,并为该模块返回一个唯一的Id。processModuleFilter(module)则可以实现对模块进行过滤,使其不被写入到最后的bundle文件中。

官方的createModuleIdFactory(path)方法是返回个数字。(如前所述,该数字在 require 方法中进行被调用,以此来实现模块的导入和初始化)

"use strict";
function createModuleIdFactory() {
  const fileToIdMap = new Map();
  let nextId = 0;
  return path => {
    let id = fileToIdMap.get(path);
    if (typeof id !== "number") {
      id = nextId++;
      fileToIdMap.set(path, id);
    }
    return id;
  };
}

官方的实现存在的问题是Id值从0开始分配,所以任意改动业务代码可能引起模块构建的顺序变动,致使同一个模块在两次构建分配了有2个不同的Id值。

针对官方实现的问题,我们重新声明一个createModuleIdFactory(path)方法,该方法使用当前模块文件的路径的哈希值作为分配模块的Id的依据,并建立哈希值与模块Id对应关系的存在本地文件module_id.json中,每次编译Bundle文件前先读取本地关系文件来初始化内部缓存,当需要分配Id时,先从内部缓存中查找,查找不到则新分配Id并存储变化。

由上述步骤可以到达同一个模块,无论编译顺序如何,返回的Id是同一个。关键代码如下:

/**
 * Get the key, which used to find the Id in local storage
 * @param {get} path
 */
function getFindKey(path) {
  let md5 = crypto.createHash('md5');
  md5.update(path);
  let findKey = md5.digest('hex');
  return findKey;
}

var moduleIdsJsonObj = {};

const moduleIdsMapFilePath = './module_id.json';

/**
 * 注释: 创建模块ID
 * 时间: 2020/6/12 0012 15:40
 * @author 郭翰林
 */
buildCreateModuleIdFactoryWithLocalStorage = function(buildConfig) {
  let currentModuleId = 0;
  // init moduleIdsJsonObj from file;
  moduleIdsJsonObj = getOrCreateModuleIdsJsonObj(moduleIdsMapFilePath);
  // init currentModuleId;
  for (var key in moduleIdsJsonObj) {
    currentModuleId = currentModuleId > moduleIdsJsonObj[key].id ? currentModuleId : moduleIdsJsonObj[key].id;
  }
  console.log('currentModuleId = ' + currentModuleId);
  return () => {
    return path => {
      // console.log(`buildType: ${buildType}`);
      let findKey = getFindKey(path);
      if (moduleIdsJsonObj[findKey] == null) {
        moduleIdsJsonObj[findKey] = {
          id: ++currentModuleId,
          type: buildConfig.type,
        };
        saveModuleIdsJsonObj(moduleIdsMapFilePath, moduleIdsJsonObj);
      }
      let id = moduleIdsJsonObj[findKey].id;
      // console.log(`createModuleIdFactory id = ${id} for ${path}`);
      return id;
    };
  };
};

同时,为了能够在processModuleFilter(module)方法中对模块进行过滤,需要在构建Common文件时,标记某个模块是否已包含在Common文件中。为此,我们在保存模块id对应关系时,额外加上了type字段,该字段的值来源于构建脚本执行时传入的参数。当构建Common文件时,该值为common,当构建Diff文件时,该值为diff。

生成的module_id.json文件如下:

{
    "f8f41b41b631cbda0ab96da29ca046e8": {
        "id": 1,
        "type": "common"
    },
    "6c5b9eb9940d40c69dee43d88f1beb4c": {
        "id": 2,
        "type": "common"
    },
    "dd51f8177f72708a90b7826e9e370fc9": {
        "id": 3,
        "type": "common"
    },
   ...
   ...
    "95e6c1577450804611cbd475e01dc540": {
        "id": 1191,
        "type": "common"
    },
    "d5339447c8123c7c23ce432f3b3c3671": {
        "id": 1192,
        "type": "common"
    }
}

processModuleFilter(module)方法实现如下:

/**
 * 注释: 过滤模块ID
 * 时间: 2020/6/12 0012 15:41
 * @author 郭翰林
 * @param buildConfig
 * @returns {function(...[*]=)}
 */
buildProcessModuleFilter = function(buildConfig) {
  return moduleObj => {
    let path = moduleObj.path;
    if (!fs.existsSync(path)) {
      return true;
    }
    if (buildConfig.type === BUILD_TYPE_DIFF) {
      //当前打包是否是diff打包
      let findKey = getFindKey(path);
      let storeObj = moduleIdsJsonObj[findKey];
      if (storeObj != null && storeObj.type === BUILD_TYPE_COMMON) {
        //如果diff包中存在的模块,common中已经存在则过滤掉,不打进最后的bundle中
        return false;
      }
      return true;
    }
    return true;
  };
};

通过上述步骤构建出的Diff文件中,还保留了Pollyfills部分内容,需要进行删除。删除脚步位于removePolyfill.js中,代码如下:

const fs = require('fs');
const readline = require('readline');
// Functions

// Main
let argvs = process.argv.splice(2);
let filePath = argvs[0];

var fRead = fs.createReadStream(filePath);
var objReadline = readline.createInterface({
  input: fRead,
});
let diff = new Array();
objReadline.on('line', function(line) {
  if (line.startsWith('__d') || line.startsWith('__r')) {
    diff.push(line);
  }
});
objReadline.on('close', function() {
  let data = diff.join('\n');
  fs.writeFileSync(filePath, data);
});

//删除module_id.json
fs.access('module_id.json', fs.constants.F_OK, error => {
  if (!error) {
    fs.unlink('module_id.json', err => {
      if (err) {
        console.log(err);
      }
    });
  }
});

二、建立分包脚本进行分包操作

Android中React Native打包主要是通过react.gradle文件去执行官方打包命令,我们可以到react.gradle中查看打包关键代码:

if (bundleConfig) {
    extraArgs = extraArgs.clone()
    extraArgs.add("--config");
    extraArgs.add(bundleConfig);
}

if (Os.isFamily(Os.FAMILY_WINDOWS)) {
    commandLine("cmd", "/c", *nodeExecutableAndArgs, cliPath, bundleCommand, "--platform", "android", "--dev", "${devEnabled}",
        "--reset-cache", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir,
        "--sourcemap-output", enableHermes ? jsPackagerSourceMapFile : jsOutputSourceMapFile, *extraArgs)
} else {
    commandLine(*nodeExecutableAndArgs, cliPath, bundleCommand, "--platform", "android", "--dev", "${devEnabled}",
        "--reset-cache", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir,
        "--sourcemap-output", enableHermes ? jsPackagerSourceMapFile : jsOutputSourceMapFile, *extraArgs)
}

打包代码中有一个--config参数,这边就是配置我们之前书写的Metro打包脚本,我把Metro打包脚本分为基础包脚本、diff包脚本、公用打包脚本,代码如下:

公用打包脚本 metro.config.base.js

/**
 * Metro configuration for React Native
 * https://github.com/facebook/react-native
 *
 * @format
 */
const fs = require('fs');
const crypto = require('crypto');

const BUILD_TYPE_COMMON = 'common';
const BUILD_TYPE_DEFAULT = 'default';
const BUILD_TYPE_DIFF = 'diff';

const moduleIdsMapFilePath = './module_id.json';

/**
 *
 * @param {*} filepath
 */
function getOrCreateModuleIdsJsonObj(filepath) {
  if (fs.existsSync(filepath)) {
    console.log(`init map from file : ${filepath}`);
    let data = fs.readFileSync(filepath, 'utf-8');
    return JSON.parse(data);
  } else {
    return {};
  }
}
/**
 *
 * @param {*} filepath
 * @param {*} jsonObj
 */
function saveModuleIdsJsonObj(filepath, jsonObj) {
  let data = JSON.stringify(jsonObj);
  fs.writeFileSync(filepath, data, err => {
    if (err) throw err;
    console.log(`Save ${filepath} SUCCESS.`);
  });
}
/**
 * Get the key, which used to find the Id in local storage
 * @param {get} path
 */
function getFindKey(path) {
  let md5 = crypto.createHash('md5');
  md5.update(path);
  let findKey = md5.digest('hex');
  return findKey;
}

var moduleIdsJsonObj = {};

/**
 * 注释: 创建模块ID
 * 时间: 2020/6/12 0012 15:40
 * @author 郭翰林
 */
buildCreateModuleIdFactoryWithLocalStorage = function(buildConfig) {
  let currentModuleId = 0;
  // init moduleIdsJsonObj from file;
  moduleIdsJsonObj = getOrCreateModuleIdsJsonObj(moduleIdsMapFilePath);
  // init currentModuleId;
  for (var key in moduleIdsJsonObj) {
    currentModuleId = currentModuleId > moduleIdsJsonObj[key].id ? currentModuleId : moduleIdsJsonObj[key].id;
  }
  console.log('currentModuleId = ' + currentModuleId);
  return () => {
    return path => {
      // console.log(`buildType: ${buildType}`);
      let findKey = getFindKey(path);
      if (moduleIdsJsonObj[findKey] == null) {
        moduleIdsJsonObj[findKey] = {
          id: ++currentModuleId,
          type: buildConfig.type,
        };
        saveModuleIdsJsonObj(moduleIdsMapFilePath, moduleIdsJsonObj);
      }
      let id = moduleIdsJsonObj[findKey].id;
      // console.log(`createModuleIdFactory id = ${id} for ${path}`);
      return id;
    };
  };
};

/**
 * 注释: 过滤模块ID
 * 时间: 2020/6/12 0012 15:41
 * @author 郭翰林
 * @param buildConfig
 * @returns {function(...[*]=)}
 */
buildProcessModuleFilter = function(buildConfig) {
  return moduleObj => {
    let path = moduleObj.path;
    if (!fs.existsSync(path)) {
      return true;
    }
    if (buildConfig.type === BUILD_TYPE_DIFF) {
      let findKey = getFindKey(path);
      let storeObj = moduleIdsJsonObj[findKey];
      if (storeObj != null && storeObj.type === BUILD_TYPE_COMMON) {
        return false;
      }
      return true;
    }
    return true;
  };
};

module.exports = {
  BuildType: {
    COMMON: BUILD_TYPE_COMMON,
    DEFAULT: BUILD_TYPE_DEFAULT,
    DIFF: BUILD_TYPE_DIFF,
  },
  buildCreateModuleIdFactory: buildCreateModuleIdFactoryWithLocalStorage,
  buildProcessModuleFilter: buildProcessModuleFilter,
};

基础包打包脚本 metro.config.common.js:

/**
 * Metro configuration for React Native
 * https://github.com/facebook/react-native
 *
 * @format
 */

const baseMetroConfig = require('./metro.config.base.js');
const buildConfig = {
  type: baseMetroConfig.BuildType.COMMON,
};

module.exports = {
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: false,
      },
    }),
  },
  serializer: {
    createModuleIdFactory: baseMetroConfig.buildCreateModuleIdFactory(buildConfig),
    processModuleFilter: baseMetroConfig.buildProcessModuleFilter(buildConfig),
  },
};

diff包打包脚本 metro.config.diff.js:

/**
 * Metro configuration for React Native
 * https://github.com/facebook/react-native
 *
 * @format
 */

const baseMetroConfig = require('./metro.config.base.js');
const buildConfig = {
  type: baseMetroConfig.BuildType.DIFF,
};
module.exports = {
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: false,
      },
    }),
  },
  serializer: {
    createModuleIdFactory: baseMetroConfig.buildCreateModuleIdFactory(buildConfig),
    processModuleFilter: baseMetroConfig.buildProcessModuleFilter(buildConfig),
  },
};

下面我们就可以在工程package.json中配置基础包和diff包打包命令:

  "scripts": {
    .....
    "prettier": "prettier --write app/**/*.js app/**/*.jsx app/**/*.ts app/**/*.tsx",
    "build_android_common_bundle": "node scripts/bundle/createAndroidAssets.js && react-native bundle --platform android --dev false --entry-file app/entry/Common.js  --bundle-output ./android/app/build/generated/assets/react/release/index.android.bundle --assets-dest ./android/app/build/generated/res/react/release/ --sourcemap-output ./android/app/build/generated/sourcemaps/react/release/index.android.bundle.map --config metro.config.common.js",
    "build_android_diff_bundle": "react-native bundle --platform android --dev false --entry-file index.android.js  --bundle-output ./android/app/build/generated/assets/react/release/diff.android.bundle --assets-dest ./android/app/build/generated/res/react/release/ --sourcemap-output ./android/app/build/generated/sourcemaps/react/release/diff.android.bundle.map --config metro.config.diff.js && node scripts/bundle/removePolyfill.js ./android/app/build/generated/assets/react/release/diff.android.bundle"
  },

这里注意到在生成基础包之前,我执行了一次node命令,这个命令是用来生成bundle输出文件夹和最终的sourcemaps文件夹的,要不然执行打包脚本会报错createAndroidAssets.js脚本如下:

const fs = require('fs');
const path = require('path');
//删除module_id.json
fs.access('module_id.json', fs.constants.F_OK, error => {
  if (!error) {
    fs.unlink('module_id.json', err => {
      if (err) {
        console.log(err);
      }
    });
  }
});
//Android app模块下创建相应文件夹
fs.access(path.join(__dirname, '../../android/app/'), fs.constants.F_OK, error => {
  if (!error) {
    fs.mkdir(
      path.join(__dirname, '../../android/app/build/generated/assets/react/release'),
      {recursive: true},
      error => {
        if (error) {
          console.log(error);
        }
      },
    );
  }
});
fs.access(path.join(__dirname, '../../android/app/'), fs.constants.F_OK, error => {
  if (!error) {
    fs.mkdir(path.join(__dirname, '../../android/app/build/generated/res/react/release'), {recursive: true}, error => {
      if (error) {
        console.log(error);
      }
    });
  }
});
fs.access(path.join(__dirname, '../../android/app/'), fs.constants.F_OK, error => {
  if (!error) {
    fs.mkdir(
      path.join(__dirname, '../../android/app/build/generated/sourcemaps/react/release/'),
      {recursive: true},
      error => {
        if (error) {
          console.log(error);
        }
      },
    );
  }
});

此时!我们已经可以通过下面的2条命令去生成基础包和diff包了:

npm run build_android_common_bundle
npm run build_android_diff_bundle

三、基础包和diff包的划分,以及Android中bundle的异步加载

(1)基础包、diff包划分

以我们工程为例,我把App引导页和主页作为基础模块,放到Common.js中,代码如下,下面的代码我们无需过多的关心,我们只需关心基础包会把哪些内容打进包内:

/**
 * 注释: RN 基础模块
 * 时间: 2020/6/15 0015 10:05
 * @author 郭翰林
 */
import React, {PureComponent} from 'react';
import {
  ActivityIndicator,
  AppRegistry,
  DeviceEventEmitter,
  FlatList,
  Image,
  NativeModules,
  Platform,
  ScrollView,
  StatusBar,
  Text,
  TextInput,
  View,
  YellowBox,
} from 'react-native';
import KeyboardManager from 'react-native-keyboard-manager';
import {CommonBridge, PageBridge} from '../bridge';
import moment from 'moment-timezone';
import {Provider} from '@ant-design/react-native';
import {enableScreens} from 'react-native-screens';
import AsyncStorage from '@react-native-community/async-storage';
import Sentry from '../dependence/sentry';
import Config from '../config';
import {createStackNavigator} from 'react-navigation-stack';
import {injectEventLog} from '../commons/event';
import {ComponentStyles, StyleConfig} from '../resources/style';
import Button from 'react-native-button';
import {TransitionIOSSpec} from 'react-navigation-stack/src/vendor/TransitionConfigs/TransitionSpecs';
import {forHorizontalIOS} from 'react-navigation-stack/src/vendor/TransitionConfigs/CardStyleInterpolators';
import {createAppContainer, NavigationActions, StackActions} from 'react-navigation';
import IntroducePage from '../modules/introduce/IntroducePage';
import MainTabBar from '../modules/main/MainTabBar';
import ProductSearchPage from '../modules/product/list/ProductSearchPage';
import AnXin from '../modules/anXin/router';

moment.tz.setDefault('Asia/Shanghai');

Platform.OS !== 'ios' && enableScreens();

let shouldLoadNaviState = true;

/**
 * 注释: 以欢迎页、首页以及相关依赖页面作为基础包
 * 时间: 2020/6/15 0015 10:23
 * @author 郭翰林
 * @returns {{new(*=): {onNavigationStateChange: function(*, *=): void, render: {(): *, (): React.ReactNode}, componentDidMount?(): void, shouldComponentUpdate?(nextProps: Readonly<{}>, nextState: Readonly<{}>, nextContext: any): boolean, componentWillUnmount?(): void, componentDidCatch?(error: Error, errorInfo: React.ErrorInfo): void, getSnapshotBeforeUpdate?(prevProps: Readonly<{}>, prevState: Readonly<{}>): (any | null), componentDidUpdate?(prevProps: Readonly<{}>, prevState: Readonly<{}>, snapshot?: any): void, componentWillMount?(): void, UNSAFE_componentWillMount?(): void, componentWillReceiveProps?(nextProps: Readonly<{}>, nextContext: any): void, UNSAFE_componentWillReceiveProps?(nextProps: Readonly<{}>, nextContext: any): void, componentWillUpdate?(nextProps: Readonly<{}>, nextState: Readonly<{}>, nextContext: any): void, UNSAFE_componentWillUpdate?(nextProps: Readonly<{}>, nextState: Readonly<{}>, nextContext: any): void, context: any, setState<K extends never>(state: (((prevState: Readonly<{}>, props: Readonly<{}>) => (Pick<{}, K> | {} | null)) | Pick<{}, K> | {} | null), callback?: () => void): void, forceUpdate(callback?: () => void): void, readonly props: Readonly<{}> & Readonly<{children?: React.ReactNode}>, state: Readonly<{}>, refs: {[p: string]: React.ReactInstance}}, contextType?: React.Context<any>, new<P, S>(props: Readonly<{}>): {onNavigationStateChange: function(*, *=): void, render: {(): *, (): React.ReactNode}, componentDidMount?(): void, shouldComponentUpdate?(nextProps: Readonly<{}>, nextState: Readonly<{}>, nextContext: any): boolean, componentWillUnmount?(): void, componentDidCatch?(error: Error, errorInfo: React.ErrorInfo): void, getSnapshotBeforeUpdate?(prevProps: Readonly<{}>, prevState: Readonly<{}>): (any | null), componentDidUpdate?(prevProps: Readonly<{}>, prevState: Readonly<{}>, snapshot?: any): void, componentWillMount?(): void, UNSAFE_componentWillMount?(): void, componentWillReceiveProps?(nextProps: Readonly<{}>, nextContext: any): void, UNSAFE_componentWillReceiveProps?(nextProps: Readonly<{}>, nextContext: any): void, componentWillUpdate?(nextProps: Readonly<{}>, nextState: Readonly<{}>, nextContext: any): void, UNSAFE_componentWillUpdate?(nextProps: Readonly<{}>, nextState: Readonly<{}>, nextContext: any): void, context: any, setState<K extends never>(state: (((prevState: Readonly<{}>, props: Readonly<{}>) => (Pick<{}, K> | {} | null)) | Pick<{}, K> | {} | null), callback?: () => void): void, forceUpdate(callback?: () => void): void, readonly props: Readonly<{}> & Readonly<{children?: React.ReactNode}>, state: Readonly<{}>, refs: {[p: string]: React.ReactInstance}}, new<P, S>(props: {}, context?: any): {onNavigationStateChange: function(*, *=): void, render: {(): *, (): React.ReactNode}, componentDidMount?(): void, shouldComponentUpdate?(nextProps: Readonly<{}>, nextState: Readonly<{}>, nextContext: any): boolean, componentWillUnmount?(): void, componentDidCatch?(error: Error, errorInfo: React.ErrorInfo): void, getSnapshotBeforeUpdate?(prevProps: Readonly<{}>, prevState: Readonly<{}>): (any | null), componentDidUpdate?(prevProps: Readonly<{}>, prevState: Readonly<{}>, snapshot?: any): void, componentWillMount?(): void, UNSAFE_componentWillMount?(): void, componentWillReceiveProps?(nextProps: Readonly<{}>, nextContext: any): void, UNSAFE_componentWillReceiveProps?(nextProps: Readonly<{}>, nextContext: any): void, componentWillUpdate?(nextProps: Readonly<{}>, nextState: Readonly<{}>, nextContext: any): void, UNSAFE_componentWillUpdate?(nextProps: Readonly<{}>, nextState: Readonly<{}>, nextContext: any): void, context: any, setState<K extends never>(state: (((prevState: Readonly<{}>, props: Readonly<{}>) => (Pick<{}, K> | {} | null)) | Pick<{}, K> | {} | null), callback?: () => void): void, forceUpdate(callback?: () => void): void, readonly props: Readonly<{}> & Readonly<{children?: React.ReactNode}>, state: Readonly<{}>, refs: {[p: string]: React.ReactInstance}}, prototype: {onNavigationStateChange: function(*, *=): void, render: {(): *, (): React.ReactNode}, componentDidMount?(): void, shouldComponentUpdate?(nextProps: Readonly<{}>, nextState: Readonly<{}>, nextContext: any): boolean, componentWillUnmount?(): void, componentDidCatch?(error: Error, errorInfo: React.ErrorInfo): void, getSnapshotBeforeUpdate?(prevProps: Readonly<{}>, prevState: Readonly<{}>): (any | null), componentDidUpdate?(prevProps: Readonly<{}>, prevState: Readonly<{}>, snapshot?: any): void, componentWillMount?(): void, UNSAFE_componentWillMount?(): void, componentWillReceiveProps?(nextProps: Readonly<{}>, nextContext: any): void, UNSAFE_componentWillReceiveProps?(nextProps: Readonly<{}>, nextContext: any): void, componentWillUpdate?(nextProps: Readonly<{}>, nextState: Readonly<{}>, nextContext: any): void, UNSAFE_componentWillUpdate?(nextProps: Readonly<{}>, nextState: Readonly<{}>, nextContext: any): void, context: any, setState<K extends never>(state: (((prevState: Readonly<{}>, props: Readonly<{}>) => (Pick<{}, K> | {} | null)) | Pick<{}, K> | {} | null), callback?: () => void): void, forceUpdate(callback?: () => void): void, readonly props: Readonly<{}> & Readonly<{children?: React.ReactNode}>, state: Readonly<{}>, refs: {[p: string]: React.ReactInstance}}}}
 */
function createEntry(pageName) {
  return class extends PureComponent {
    onNavigationStateChange = (prevState, currentState) => {
      processSlideGesture(currentState, this.props.pageId);
    };

    constructor(props) {
      super(props);
      this.state = {
        barStyle: 'dark-content',
      };
      if (pageName === 'MainTabBar' && !this.props.lastAppVersion) {
        this.Router = initialRoute('IntroducePage');
      } else {
        this.Router = initialRoute(pageName);
      }
      if (Platform.OS === 'ios') {
        KeyboardManager.setToolbarPreviousNextButtonEnable(true);
      }
      if (__DEV__) {
        YellowBox.ignoreWarnings([
          'Remote debugger',
          'Warning: isMounted(...) is deprecated',
          'Module RCTImageLoader',
          'You should only render one navigator explicitly',
        ]);
        console.ignoredYellowBox = ['Remote debugger'];
      }
    }

    render() {
      const Router = this.Router;
      return (
        <View style={{flex: 1}}>
          <Provider>
            {Platform.OS === 'ios' ? <StatusBar barStyle={this.state.barStyle} /> : null}
            <Router
              screenProps={this.props}
              {...getPersistenceFunctions()}
              renderLoadingExperimental={() => <ActivityIndicator />}
              onNavigationStateChange={this.onNavigationStateChange.bind(this)}
            />
          </Provider>
        </View>
      );
    }
  };
}

/**
 * 尽量以路由的跳转方式跳转,以减少这边的页面依赖关系
 * @type {{IntroducePage: {screen: IntroducePage}, ProductSearchPage: {screen: ProductSearchPage}, AddFamilyMemberPage: {screen}, MainTabBar: {screen: import("react-navigation").NavigationNavigator<any, import("react-navigation").NavigationProp<import("react-navigation").NavigationState>>}}}
 */
const allPages = {
  IntroducePage: {screen: IntroducePage},
  MainTabBar: {screen: MainTabBar},
  ProductSearchPage: {screen: ProductSearchPage},
  ...AnXin,
};

let initialRoute;
export default (initialRoute = rootName => {
  const navigator = createStackNavigator(injectEventLog(allPages, rootName), {
    initialRouteName: rootName,
    defaultNavigationOptions: props => ({
      headerBackTitle: null,
      headerStyle: ComponentStyles.navigationBar,
      headerLeft: () => (
        <Button
          containerStyle={{marginLeft: 16, width: 24, height: 24}}
          onPress={() => {
            const {params = {}} = props.navigation.state;
            if (params.hud) {
              params.hud.dismiss();
            }
            if (params.onBack) {
              params.onBack();
            } else {
              if (!props.navigation.isFirstRouteInParent()) {
                props.navigation.goBack();
              } else {
                NativeModules.CommonUtilBridge.pop(props.screenProps.pageId);
              }
            }
          }}>
          <Image
            style={{tintColor: '#1D2023', width: 24, height: 24}}
            source={require('../resources/images/common/icon_back.png')}
          />
        </Button>
      ),
      headerRight: () => <View />,
      headerTitleStyle: ComponentStyles.headerTitleStyle,
      headerTitleAlign: 'left',
      headerTintColor: StyleConfig.color_button_normal,
      cardStyle: {backgroundColor: '#fff'},
      headerTitleAllowFontScaling: false,
      cardOverlayEnabled: true,
      cardShadowEnabled: true,
      gestureEnabled: true,
      gestureResponseDistance: {
        horizontal: 10,
      },
      gestureDirection: 'horizontal',
      transitionSpec: {
        open: TransitionIOSSpec,
        close: TransitionIOSSpec,
      },
      cardStyleInterpolator: forHorizontalIOS,
    }),
    headerMode: 'screen',
    mode: 'card',
  });
  navigator.router.getStateForAction = navigateOnce(navigator.router.getStateForAction);
  return createAppContainer(navigator);
});

const navigateOnce = getStateForAction => (action, state) => {
  const {type, routeName, params = {}} = action;
  return state &&
    (type === NavigationActions.NAVIGATE || type === StackActions.PUSH) &&
    !params.canPush &&
    routeName === state.routes[state.routes.length - 1].routeName
    ? null
    : getStateForAction(action, state);
  // you might want to replace 'null' with 'state' if you're using redux (see comments below)
};

AppRegistry.registerComponent('MainEntry', () => createEntry('MainTabBar'));

这里的打包会把Common.js所有相关的引用以及React-Navigation导航配置的路由页面都会被打进包内!
此时,我们再去执行diff包命令,则会把App RN部分非这部分包内的引用和相关页面打进diff包内

(2)Android中异步加载Bundle

由于我们工程使用的是React-Navigation去跳转页面,如果其他的页面在diff包内,则如果在没有加载diff包的时候,是无法通过this.props.navigation.push("XXXX")进行跳转的,此时就需要我们去改造基础包中页面的页面跳转方式,改为使用原生路由的方式的去跳转页面,跳转时新建一个RN容器。另外使用路由的跳转方式也可以减少基础包中页面的依赖,使得基础变得更小!

this.props.navigation.navigate('ProductDetailPage', {planId: item.planId});
//改为
RouterPageBridge.gotoRouterSkipSystem(RouterUri.ProductDetailPage, {planId: item.planId});

基础包加载完成之后设置监听立即去加载diff包,这样做的好处是可以减少首页的加载时间更快的进入首页(但是由于React Native在0.56版本之后,把RN JS代码和资源文件分开之后,这部分时间提升有限!因为大头都是资源文件,而资源文件都是直接被打进APK包assets文件内的)。另一个好处就是首页进行跳转diff包页面时不会出现白屏现象,因为diff包已经被异步加载完成!关键代码如下:

//RN初始化入口页
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ......
    ......
    LoadAnimationView.show(this);
    if(!BuildConfig.DEBUG){
        //设置RN加载监听
        getReactInstanceManager().addReactInstanceEventListener(context -> {
            //异步加载Diff模块
            getReactInstanceManager().getCurrentReactContext().getCatalystInstance().loadScriptFromAssets(getAssets(), "assets://diff.android.bundle", false);
        });
    }
}

四、Sentry编译问题解决

我们工程改写了react.gradle脚本,在打Release包时,去除原有的打包脚本,从而执行前面我们自己定义的npm 分包脚本,关键代码如下:

// Copyright (c) Facebook, Inc. and its affiliates.

// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.

import org.apache.tools.ant.taskdefs.condition.Os

def config = project.hasProperty("react") ? project.react : [];

def cliPath = config.cliPath ?: "node_modules/react-native/cli.js"
def composeSourceMapsPath = config.composeSourceMapsPath ?: "node_modules/react-native/scripts/compose-source-maps.js"
def bundleAssetName = config.bundleAssetName ?: "index.android.bundle"
def entryFile = config.entryFile ?: "index.android.js"
def bundleCommand = config.bundleCommand ?: "bundle"
def reactRoot = file(config.root ?: "../../")
def inputExcludes = config.inputExcludes ?: ["android/**", "ios/**"]
def bundleConfig = config.bundleConfig ? "${reactRoot}/${config.bundleConfig}" : null;
def enableVmCleanup = config.enableVmCleanup == null ? true : config.enableVmCleanup
def hermesCommand = config.hermesCommand ?: "../../node_modules/hermes-engine/%OS-BIN%/hermes"

def reactNativeDevServerPort() {
    def value = project.getProperties().get("reactNativeDevServerPort")
    return value != null ? value : "8081"
}

def reactNativeInspectorProxyPort() {
    def value = project.getProperties().get("reactNativeInspectorProxyPort")
    return value != null ? value : reactNativeDevServerPort()
}

/**
 * 注释:是否开启编译JsBundle任务
 * 时间:2019/4/18 0018 10:18
 * 作者:郭翰林
 * @return
 */
boolean isEnableBuildJsBundle(String targetName) {
    if (targetName.toLowerCase().contains("release")) {
        return false
    }
    File jsBundle = file("$buildDir/intermediates/assets/debug/index.android.bundle")
    if (!jsBundle.exists()) {
        return true
    } else {
        println("【跳过编译JsBundle】JsBundle已存在,无需再次编译")
        return false
    }
}

def getHermesOSBin() {
    if (Os.isFamily(Os.FAMILY_WINDOWS)) return "win64-bin";
    if (Os.isFamily(Os.FAMILY_MAC)) return "osx-bin";
    if (Os.isOs(null, "linux", "amd64", null)) return "linux64-bin";
    throw new Exception("OS not recognized. Please set project.ext.react.hermesCommand " +
            "to the path of a working Hermes compiler.");
}

// Make sure not to inspect the Hermes config unless we need it,
// to avoid breaking any JSC-only setups.
def getHermesCommand = {
    // If the project specifies a Hermes command, don't second guess it.
    if (!hermesCommand.contains("%OS-BIN%")) {
        return hermesCommand
    }

    // Execution on Windows fails with / as separator
    return hermesCommand
            .replaceAll("%OS-BIN%", getHermesOSBin())
            .replace('/' as char, File.separatorChar);
}

// Set enableHermesForVariant to a function to configure per variant,
// or set `enableHermes` to True/False to set all of them
def enableHermesForVariant = config.enableHermesForVariant ?: {
    def variant -> config.enableHermes ?: false
}

android {
    buildTypes.all {
        resValue "integer", "react_native_dev_server_port", reactNativeDevServerPort()
        resValue "integer", "react_native_inspector_proxy_port", reactNativeInspectorProxyPort()
    }
}

afterEvaluate {
    def isAndroidLibrary = plugins.hasPlugin("com.android.library")
    def variants = isAndroidLibrary ? android.libraryVariants : android.applicationVariants
    variants.all { def variant ->
        ....
        ....
        ....
        
        def enableHermes = enableHermesForVariant(variant)
        def currentBundleTask = tasks.create(
                name: "bundle${targetName}JsAndAssets",
                type: Exec) {
            group = "react"
            description = "bundle JS and assets for ${targetName}."

            // Create dirs if they are not there (e.g. the "clean" task just ran)
            doFirst {
                jsBundleDir.mkdirs()
                resourcesDir.mkdirs()
                jsIntermediateSourceMapsDir.mkdirs()
                jsSourceMapsDir.mkdirs()
            }

            .....

            if (isEnableBuildJsBundle(targetName)) {
                if (Os.isFamily(Os.FAMILY_WINDOWS)) {
                    commandLine("cmd", "/c", *nodeExecutableAndArgs, cliPath, bundleCommand, "--platform", "android", "--dev", "${devEnabled}",
                            "--reset-cache", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir,
                            "--sourcemap-output", enableHermes ? jsPackagerSourceMapFile : jsOutputSourceMapFile, *extraArgs)
                } else {
                    commandLine(*nodeExecutableAndArgs, cliPath, bundleCommand, "--platform", "android", "--dev", "${devEnabled}",
                            "--reset-cache", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir,
                            "--sourcemap-output", enableHermes ? jsPackagerSourceMapFile : jsOutputSourceMapFile, *extraArgs)
                }

                if (enableHermes) {
                   ......
                   ......
                }
            } else if (targetName.toLowerCase().contains("release")) {
                //Release打包执行分包脚本
                if (Os.isFamily(Os.FAMILY_WINDOWS)) {
                    commandLine("cmd", "/c", "java", "-version")
                } else {
                    commandLine "bash", "-c", "java -version"
                }
                doLast {
                    exec {
                        if (Os.isFamily(Os.FAMILY_WINDOWS)) {
                            commandLine("cmd", "/c", "npm", "run", "build_android_common_bundle")
                        } else {
                            commandLine "bash", "-c", "npm run build_android_common_bundle"
                        }
                    }
                    exec {
                        if (Os.isFamily(Os.FAMILY_WINDOWS)) {
                            commandLine("cmd", "/c", "npm", "run", "build_android_diff_bundle")
                        } else {
                            commandLine "bash", "-c", "npm run build_android_diff_bundle"
                        }
                    }
                }
            } else {
                if (Os.isFamily(Os.FAMILY_WINDOWS)) {
                    commandLine("cmd", "/c", "java", "-version")
                } else {
                    commandLine "bash", "-c", "java -version"
                }
            }

            enabled config."bundleIn${targetName}" != null
                    ? config."bundleIn${targetName}"
                    : config."bundleIn${variant.buildType.name.capitalize()}" != null
                    ? config."bundleIn${variant.buildType.name.capitalize()}"
                    : targetName.toLowerCase().contains("release")
        }
        .....
        .....  
        .....
    }
}

但是如果你的工程集成Sentry之后,你会发现Sentry在打包时执行脚本会编译报错,下面我们来看下为什么会报错,../../node_modules/@sentry/react-native/sentry.gradle代码如下:

import org.apache.tools.ant.taskdefs.condition.Os

import java.util.regex.Matcher
import java.util.regex.Pattern

def config = project.hasProperty("sentryCli") ? project.sentryCli : [];

gradle.projectsEvaluated {
    def releases = extractReleasesInfo()

    if (config.flavorAware && config.sentryProperties) {
        throw new GradleException("Incompatible sentry configuration. " +
                "You cannot use both `flavorAware` and `sentryProperties`. " +
                "Please remove one of these from the project.ext.sentryCli configuration.")
    }

    if (config.sentryProperties instanceof String) {
        config.sentryProperties = file(config.sentryProperties)
    }

    if (config.sentryProperties) {
        if (!config.sentryProperties.exists()) {
            throw new GradleException("project.ext.sentryCli configuration defines a non-existant 'sentryProperties' file: " + config.sentryProperties.getAbsolutePath())
        }
        logger.info("Using 'sentry.properties' at: " + config.sentryProperties.getAbsolutePath())
    }

    if (config.flavorAware) {
        println "**********************************"
        println "* Flavor aware sentry properties *"
        println "**********************************"
    }

    // separately we then hook into the bundle task of react native to inject
    // sourcemap generation parameters.  In case for whatever reason no release
    // was found for the asset folder we just bail.
    def bundleTasks = tasks.findAll { task -> task.name.startsWith("bundle") && task.name.endsWith("JsAndAssets") && !task.name.contains("Debug") }
    bundleTasks.each { bundleTask ->
        def shouldCleanUp
        def sourcemapOutput
        def bundleOutput
        def props = bundleTask.getProperties()
        def reactRoot = props.get("workingDir")

        (shouldCleanUp, bundleOutput, sourcemapOutput) = forceSourceMapOutputFromBundleTask(bundleTask)

        // Lets leave this here if we need to debug
        // println bundleTask.properties
        //     .sort{it.key}
        //     .collect{it}
        //     .findAll{!['class', 'active'].contains(it.key)}
        //     .join('\n')

        def currentVariants = extractCurrentVariants(bundleTask, releases)
        if (currentVariants == null) return

        def variant = null
        def releaseName = null
        def versionCodes = new ArrayList<Integer>(currentVariants.size())

        currentVariants.each { key, currentVariant ->
            variant = currentVariant[0]
            releaseName = currentVariant[1]
            versionCodes.push(currentVariant[2])
        }

        def nameCliTask = "${bundleTask.name}_SentryUpload"
        def nameCleanup = "${bundleTask.name}_SentryUploadCleanUp"

        /** Upload source map file to the sentry server via CLI call. */
        def cliTask = tasks.create(name: nameCliTask, type: Exec) {
            description = "upload debug symbols to sentry"
            group = 'sentry.io'

            workingDir reactRoot

            def propertiesFile = config.sentryProperties
                    ? config.sentryProperties
                    : "$reactRoot/android/sentry.properties"

            if (config.flavorAware) {
                propertiesFile = "$reactRoot/android/sentry-${variant}.properties"
                project.logger.info("For $variant using: $propertiesFile")
            } else {
                environment("SENTRY_PROPERTIES", propertiesFile)
            }

            Properties sentryProps = new Properties()
            try {
                sentryProps.load(new FileInputStream(propertiesFile))
            } catch (FileNotFoundException e) {
                project.logger.info("file not found '$propertiesFile' for '$variant'")
            }
            def cliExecutable = sentryProps.get("cli.executable", "$reactRoot/node_modules/@sentry/cli/bin/sentry-cli")

            // fix path separator for Windows
            if (Os.isFamily(Os.FAMILY_WINDOWS)) {
                cliExecutable = cliExecutable.replaceAll("/", "\\\\")
            }

            //
            // based on:
            //   https://github.com/getsentry/sentry-cli/blob/master/src/commands/react_native_gradle.rs
            //
            def args = [cliExecutable]

            args.addAll(!config.logLevel ? [] : [
                    "--log-level", config.logLevel      // control verbosity of the output
            ])
            args.addAll(!config.flavorAware ? [] : [
                    "--url", sentryProps.get("defaults.url"),
                    "--auth-token", sentryProps.get("auth.token")
            ])
            args.addAll(["react-native", "gradle",
                         "--bundle", bundleOutput,           // The path to a bundle that should be uploaded.
                         "--sourcemap", sourcemapOutput,     // The path to a sourcemap that should be uploaded.
                         "--release", releaseName            // The name of the release to publish.
            ])
            args.addAll(!config.flavorAware ? [] : [
                    "--org", sentryProps.get("defaults.org"),
                    "--project", sentryProps.get("defaults.project")
            ])

            // The names of the distributions to publish. Can be supplied multiple times.
            versionCodes.each { versionCode -> args.addAll(["--dist", versionCode]) }

            project.logger.info("Sentry-CLI arguments: ${args}")

            def osCompatibility = Os.isFamily(Os.FAMILY_WINDOWS) ? ['cmd', '/c', 'node'] : []
            commandLine(*osCompatibility, *args)

            enabled true
        }

        /** Delete sourcemap files */
        def cliCleanUpTask = tasks.create(name: nameCleanup, type: Delete) {
            description = "clean up extra sourcemap"
            group = 'sentry.io'

            delete sourcemapOutput
            delete "$buildDir/intermediates/assets/release/index.android.bundle.map" // react native default bundle dir
        }

        // dependsOn, mustRunAfter, shouldRunAfter, doFirst, doLast, finalizedBy
        // bundleTask --> cliTask
        bundleTask.finalizedBy cliTask

        // register clean task extension
        cliCleanUpTask.onlyIf { shouldCleanUp }
        cliTask.finalizedBy cliCleanUpTask
    }
}

/** Compose lookup map of build variants - to - outputs. */
def extractReleasesInfo() {
    def releases = [:]

    android.applicationVariants.each { variant ->

        variant.outputs.each { output ->
            def versionCode = output.getVersionCode()
            def releaseName = "${variant.getApplicationId()}@${variant.getVersionName()}+${versionCode}"
            def variantName = variant.getName()
            def outputName = output.getName()
            if (releases[variantName] == null) {
                releases[variantName] = [:]
            }
            releases[variantName][outputName] = [outputName, releaseName, versionCode]
        }
    }

    return releases
}

/** Extract from arguments collection bundle and sourcemap files output names. */
static extractBundleTaskArguments(cmdArgs, Project project) {
    def bundleOutput = null
    def sourcemapOutput = null

    cmdArgs.eachWithIndex { String arg, int i ->
        if (arg == "--bundle-output") {
            bundleOutput = cmdArgs[i + 1]
            project.logger.info("--bundle-output: `${bundleOutput}`")
        } else if (arg == "--sourcemap-output") {
            sourcemapOutput = cmdArgs[i + 1]
            project.logger.info("--sourcemap-output param: `${sourcemapOutput}`")
        }
    }

    // Best thing would be if we just had access to the local gradle variables here:
    // https://github.com/facebook/react-native/blob/ff3b839e9a5a6c9e398a1327cde6dd49a3593092/react.gradle#L89-L97
    // Now, the issue is that hermes builds have a different pipeline:
    // `metro -> hermes -> compose-source-maps`, which then combines both intermediate sourcemaps into the final one.
    // In this function here, we only grep through the first `metro` step, which only generates an intermediate sourcemap,
    // which is wrong. We need the final one. Luckily, we can just generate the path from the `bundleOutput`, since
    // the paths seem to be well defined.

    // if sourcemapOutput is null, it means there's no source maps at all
    // if hermes is enabled and has intermediates folder, we need to fix paths
    // if hermes is disabled, sourcemapOutput is already ok
    def enableHermes = project.ext.react.get("enableHermes", false);
    project.logger.info("enableHermes: `${enableHermes}`")

    if (bundleOutput != null && sourcemapOutput != null && enableHermes) {
        // react-native < 0.60.1
        def pattern = Pattern.compile("(/|\\\\)intermediates\\1sourcemaps\\1react\\1")
        Matcher matcher = pattern.matcher(sourcemapOutput)
        // if its intermediates/sourcemaps/react then it should be generated/sourcemaps/react
        if (matcher.find()) {
            project.logger.info("sourcemapOutput has the wrong path, let's fix it.")
            // replacing from bundleOutput which is more reliable
            sourcemapOutput = bundleOutput.replaceAll("(/|\\\\)generated\\1assets\\1react\\1", "\$1generated\$1sourcemaps\$1react\$1") + ".map"
            project.logger.info("sourcemapOutput new path: `${sourcemapOutput}`")
        }
    }

    return [bundleOutput, sourcemapOutput]
}

/** Force Bundle task to produce sourcemap files if they are not pre-configured by user yet. */
def forceSourceMapOutputFromBundleTask(bundleTask) {
    def props = bundleTask.getProperties()
    def cmd = props.get("commandLine") as List<String>
    def cmdArgs = props.get("args") as List<String>
    def shouldCleanUp = false
    def bundleOutput = null
    def sourcemapOutput = null

    (bundleOutput, sourcemapOutput) = extractBundleTaskArguments(cmdArgs, project)

    if (sourcemapOutput == null) {
        sourcemapOutput = bundleOutput + ".map"

        cmd.addAll(["--sourcemap-output", sourcemapOutput])
        cmdArgs.addAll(["--sourcemap-output", sourcemapOutput])

        shouldCleanUp = true

        bundleTask.setProperty("commandLine", cmd)
        bundleTask.setProperty("args", cmdArgs)

        project.logger.info("forced sourcemap file output for `${bundleTask.name}` task")
    } else {
        project.logger.info("Info: used pre-configured source map files: ${sourcemapOutput}")
    }

    return [shouldCleanUp, bundleOutput, sourcemapOutput]
}

/** compose array with one item - current build flavor name */
static extractCurrentVariants(bundleTask, releases) {
    // examples: bundleLocalReleaseJsAndAssets, bundleYellowDebugJsAndAssets
    def pattern = Pattern.compile("bundle([A-Z][A-Za-z0-9_]+)JsAndAssets")

    def currentRelease = ""

    Matcher matcher = pattern.matcher(bundleTask.name)
    if (matcher.find()) {
        def match = matcher.group(1)
        currentRelease = match.substring(0, 1).toLowerCase() + match.substring(1)
    }

    def currentVariants = null
    releases.each { key, release ->
        if (key.equalsIgnoreCase(currentRelease)) {
            currentVariants = release
        }
    }

    return currentVariants
}

我们可以看到在执行上传Task之前,执行了forceSourceMapOutputFromBundleTask(bundleTask)方法,继续追踪这个方法,可以看到以下关键代码:

/** Extract from arguments collection bundle and sourcemap files output names. */
static extractBundleTaskArguments(cmdArgs, Project project) {
    def bundleOutput = null
    def sourcemapOutput = null

    cmdArgs.eachWithIndex { String arg, int i ->
        if (arg == "--bundle-output") {
            bundleOutput = cmdArgs[i + 1]
            project.logger.info("--bundle-output: `${bundleOutput}`")
        } else if (arg == "--sourcemap-output") {
            sourcemapOutput = cmdArgs[i + 1]
            project.logger.info("--sourcemap-output param: `${sourcemapOutput}`")
        }
    }

    if (bundleOutput != null && sourcemapOutput != null && enableHermes) {
        // react-native < 0.60.1
        def pattern = Pattern.compile("(/|\\\\)intermediates\\1sourcemaps\\1react\\1")
        Matcher matcher = pattern.matcher(sourcemapOutput)
        // if its intermediates/sourcemaps/react then it should be generated/sourcemaps/react
        if (matcher.find()) {
            project.logger.info("sourcemapOutput has the wrong path, let's fix it.")
            // replacing from bundleOutput which is more reliable
            sourcemapOutput = bundleOutput.replaceAll("(/|\\\\)generated\\1assets\\1react\\1", "\$1generated\$1sourcemaps\$1react\$1") + ".map"
            project.logger.info("sourcemapOutput new path: `${sourcemapOutput}`")
        }
    }

    return [bundleOutput, sourcemapOutput]
}

这段代码的意思是从上个依赖Task中取--bundle-output和--sourcemap-output的入参作为之前上传Task中使用到的bundleOutput和sourcemapOutput命令入参

args.addAll(["react-native", "gradle",
             "--bundle", bundleOutput,           // The path to a bundle that should be uploaded.
             "--sourcemap", sourcemapOutput,     // The path to a sourcemap that should be uploaded.
             "--release", releaseName            // The name of the release to publish.
])

由于我们之前改写了react.gralde中的打包脚本,所以这段逻辑就无法取到--bundle-output和--sourcemap-output的入参,致使打包编译失败,那么我们就去改写这段脚本来去适配Sentry上传脚本:

/** Extract from arguments collection bundle and sourcemap files output names. */
static extractBundleTaskArguments(Project project) {
    def bundleOutput = "${project.buildDir}/generated/assets/react/release/index.android.bundle"
    def sourcemapOutput = "${project.buildDir}/generated/sourcemaps/react/release/index.android.bundle.map"
    return [bundleOutput, sourcemapOutput]
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335