Skip to content

Commit bf32fda

Browse files
author
徐远翔
committed
feat: metro dev server
1 parent b8b7e6d commit bf32fda

File tree

5 files changed

+151
-74
lines changed

5 files changed

+151
-74
lines changed

README.md

+6-29
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,12 @@
1212
- [ ] umi-plugin-react-native-bundler-metro: developing
1313
- [ ] umi-plugin-react-native-bundler-haul
1414

15-
## 必备条件
15+
## 必备
1616

1717
- RN 工程(已有,或使用`react-native init`新建);
1818
- [RN 开发环境](https://reactnative.dev/docs/environment-setup)
1919

20-
## 使用步骤
21-
22-
### 安装
20+
## 安装
2321

2422
选用官方[metro](https://facebook.github.io/metro/)打包:
2523

@@ -33,36 +31,15 @@ yarn add umi-plugin-react-native umi-plugin-react-native-bundler-metro --dev
3331
yarn add umi-plugin-react-native umi-plugin-react-native-bundler-haul --dev
3432
```
3533

36-
### 配置
37-
38-
```javascript
39-
// .umirc.js
40-
export default {
41-
history: {
42-
type: 'memory',
43-
},
44-
'react-native': {
45-
appKey: 'RNExample',
46-
},
47-
plugins: ['umi-plugin-react-native', 'umi-plugin-react-native-bundler-metro'],
48-
// plugins: ['umi-plugin-react-native', 'umi-plugin-react-native-bundler-haul'],
49-
};
50-
```
34+
### 注意
5135

52-
**注意:**
36+
`umi-plugin-react-native-bundler-metro``umi-plugin-react-native-bundler-haul` 只能二选一,同时安装会导致 umi 报错(`dev-rn``build-rn`命令行工具冲突)。
5337

54-
- `history`: **必须**设置为`"memory"`,因为 RN 中没有 DOM,使用<s>"browser"</s>或者<s>"hash"</s>时会报错。
55-
- `react-native``umi-plugin-react-native`配置项。
56-
- `appKey`: 选填,缺省值:RN 工程 app.json 文件中的 "name" 字段。
57-
- 在 RN JS 代码域中命名为:`appKey`,即`AppRegistry.registerComponent(appKey, componentProvider);`的第一个参数;
58-
- 在 iOS/Android 代码域中命名为:`moduleName`,是原生层加载 js bundle 文件的必填参数。
59-
- `plugins`:
60-
- `umi-plugin-react-native`**必须**;
61-
- `umi-plugin-react-native-bundler-metro``umi-plugin-react-native-bundler-haul`: 二选一,同时添加时会导致 umi 报错(`dev-rn``dev-build`命令行工具冲突)。
38+
## 使用
6239

6340
### 开发
6441

65-
可以修改`package.json`文件,使用`umi`取代原本的`react-native`
42+
修改`package.json`文件,使用`umi`取代原本的`react-native`
6643

6744
```diff
6845
{

packages/bundler-metro/src/index.ts

+132-31
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,172 @@
11
import { IApi } from '@umijs/types';
22
import { join } from 'path';
33
import { existsSync } from 'fs';
4+
import { constants } from 'os';
5+
import http, { Server as HttpServer } from 'http';
6+
import { Server as HttpsServer } from 'https';
7+
import assert from 'assert';
48
import generateFiles from '@umijs/preset-built-in/lib/plugins/commands/generateFiles';
59
import { cleanTmpPathExceptCache } from '@umijs/preset-built-in/lib/plugins/commands/buildDevUtils';
610
import { watchPkg } from '@umijs/preset-built-in/lib/plugins/commands/dev/watchPkg';
711
import loadMetroConfig from '@react-native-community/cli/build/tools/loadMetroConfig';
812
import loadConfig from '@react-native-community/cli/build/tools/config';
13+
import eventsSocketModule from '@react-native-community/cli/build/commands/server/eventsSocket';
14+
import messageSocket from '@react-native-community/cli/build/commands/server/messageSocket';
15+
import webSocketProxy from '@react-native-community/cli/build/commands/server/webSocketProxy';
16+
import MiddlewareManager from '@react-native-community/cli/build/commands/server/middleware/MiddlewareManager';
17+
import releaseChecker from '@react-native-community/cli/build/tools/releaseChecker';
18+
import enableWatchMode from '@react-native-community/cli/build/commands/server/watchMode';
919
import getIPAddress from './getIPAddress';
1020

1121
export default (api: IApi) => {
1222
const {
1323
logger,
14-
env,
1524
paths,
1625
utils: { chalk, portfinder },
1726
} = api;
18-
const METRO_PATH = join(api.paths.absNodeModulesPath || process.cwd(), 'metro');
27+
const METRO_PATH = join(paths.absNodeModulesPath || '', 'metro');
28+
const METRO_CORE_PATH = join(paths.absNodeModulesPath || '', 'metro-core');
1929

20-
if (!existsSync(METRO_PATH)) {
21-
throw new TypeError('未找到 "metro",请执行 `yarn install` 安装所有依赖。');
22-
}
30+
assert(existsSync(METRO_PATH), '未找到 "metro",请执行 `yarn install` 安装所有依赖。');
31+
assert(existsSync(METRO_CORE_PATH), '未找到 "metro-core",请执行 `yarn install` 安装所有依赖。');
2332

24-
// eslint-disable-next-line @typescript-eslint/no-var-requires
25-
const MetroServer = require(join(METRO_PATH, 'src', 'Server.js'));
2633
let port: number;
2734
let hostname: string;
28-
let server: typeof MetroServer;
35+
let server: HttpServer | HttpsServer;
2936
const unwatchs: (() => void)[] = [];
3037

31-
function destroy() {
32-
for (const unwatch of unwatchs) {
33-
unwatch();
34-
}
38+
function destroy(): Promise<void> {
39+
return new Promise<void>((resolve, reject) => {
40+
for (const unwatch of unwatchs) {
41+
try {
42+
unwatch();
43+
} catch (ignored) {}
44+
}
45+
if (server) {
46+
server.close((err) => (err ? reject(err) : resolve()));
47+
} else {
48+
resolve();
49+
}
50+
});
3551
}
3652

3753
api.registerCommand({
3854
name: 'dev-rn',
3955
description: 'starts react-native dev webserver',
4056
fn: async ({ args }) => {
41-
logger.info('rn-dev:', args);
4257
const defaultPort = process.env.PORT || args?.port || api.config.devServer?.port;
4358
port = await portfinder.getPortPromise({
4459
port: defaultPort ? parseInt(String(defaultPort), 10) : 8081,
4560
});
4661
hostname = process.env.HOST || api.config.devServer?.host || getIPAddress();
47-
console.log(chalk.cyan('Starting the development server...'));
48-
// eslint-disable-next-line no-unused-expressions
49-
process.send?.({ type: 'UPDATE_PORT', port });
62+
63+
logger.info('rn-dev:', args, 'hostname=', hostname, 'port=', port);
5064

5165
cleanTmpPathExceptCache({
5266
absTmpPath: paths.absTmpPath!,
5367
});
5468
const watch = process.env.WATCH !== 'none';
5569

56-
async function runMetroServer(): Promise<void> {
57-
const ctx = loadConfig(api.paths.cwd);
70+
async function startMetroServer(): Promise<void> {
71+
// eslint-disable-next-line @typescript-eslint/no-var-requires
72+
const Metro = require(METRO_PATH);
73+
// eslint-disable-next-line @typescript-eslint/no-var-requires
74+
const { Terminal } = require(METRO_CORE_PATH);
75+
// eslint-disable-next-line @typescript-eslint/no-var-requires
76+
const TerminalReporter = require(join(METRO_PATH, 'src', 'lib', 'TerminalReporter'));
77+
const terminal = new Terminal(process.stdout);
78+
const terminalReporter = new TerminalReporter(terminal);
79+
// eslint-disable-next-line
80+
let eventsSocket: ReturnType<typeof eventsSocketModule.attachToServer> | undefined;
81+
82+
const reporter = {
83+
update(event: any) {
84+
terminalReporter.update(event);
85+
if (eventsSocket) {
86+
eventsSocket.reportEvent(event);
87+
}
88+
},
89+
};
90+
const ctx = loadConfig(paths.cwd);
91+
logger.info('ctx:', ctx);
5892
const metroConfig = await loadMetroConfig(ctx, {
5993
...args,
60-
watchFolders: [api.paths.absTmpPath || ''],
61-
projectRoot: api.paths.cwd,
62-
// reporter,
94+
config: join(paths.cwd || '', 'metro.config.js'),
95+
projectRoot: paths.absTmpPath,
96+
reporter,
6397
});
64-
}
98+
if (Array.isArray(args.assetPlugins)) {
99+
metroConfig.transformer.assetPlugins = args.assetPlugins.map((plugin) => require.resolve(plugin));
100+
}
101+
102+
const middlewareManager = new MiddlewareManager({
103+
host: hostname,
104+
port: metroConfig.server.port || port,
105+
watchFolders: metroConfig.watchFolders,
106+
});
107+
108+
metroConfig.watchFolders.forEach(middlewareManager.serveStatic.bind(middlewareManager));
109+
110+
const customEnhanceMiddleware = metroConfig.server.enhanceMiddleware;
65111

66-
function restartMetroServer(): void {
67-
if (server && typeof server.close === 'function') {
68-
server.close(() => {
69-
runMetroServer().then();
112+
metroConfig.server.enhanceMiddleware = (middleware: any, server: unknown) => {
113+
if (customEnhanceMiddleware) {
114+
middleware = customEnhanceMiddleware(middleware, server);
115+
}
116+
117+
return middlewareManager.getConnectInstance().use(middleware);
118+
};
119+
120+
logger.info('metroConfig:', metroConfig);
121+
server = await Metro.runServer(metroConfig, {
122+
host: args.host,
123+
secure: args.https,
124+
secureCert: args.cert,
125+
secureKey: args.key,
126+
hmrEnabled: true,
127+
});
128+
129+
const wsProxy = webSocketProxy.attachToServer(server, '/debugger-proxy');
130+
const ms = messageSocket.attachToServer(server, '/message');
131+
eventsSocket = eventsSocketModule.attachToServer(server, '/events', ms);
132+
133+
middlewareManager.attachDevToolsSocket(wsProxy);
134+
middlewareManager.attachDevToolsSocket(ms);
135+
136+
if (args.interactive) {
137+
enableWatchMode(ms);
138+
}
139+
140+
middlewareManager
141+
.getConnectInstance()
142+
.use('/reload', (_req: http.IncomingMessage, res: http.ServerResponse) => {
143+
ms.broadcast('reload');
144+
res.end('OK');
70145
});
146+
147+
// In Node 8, the default keep-alive for an HTTP connection is 5 seconds. In
148+
// early versions of Node 8, this was implemented in a buggy way which caused
149+
// some HTTP responses (like those containing large JS bundles) to be
150+
// terminated early.
151+
//
152+
// As a workaround, arbitrarily increase the keep-alive from 5 to 30 seconds,
153+
// which should be enough to send even the largest of JS bundles.
154+
//
155+
// For more info: https://github.com/nodejs/node/issues/13391
156+
//
157+
server.keepAliveTimeout = 30000;
158+
159+
await releaseChecker(ctx.root);
160+
}
161+
162+
async function restartServer(): Promise<void> {
163+
console.log(chalk.gray(`Try to restart dev server...`));
164+
try {
165+
await destroy();
166+
await startMetroServer();
167+
} catch (err) {
168+
console.log(chalk.red('Dev server restarting failed'), err);
169+
process.exit(constants.signals.SIGHUP);
71170
}
72171
}
73172

@@ -81,19 +180,19 @@ export default (api: IApi) => {
81180
onChange() {
82181
console.log();
83182
api.logger.info(`Plugins in package.json changed.`);
84-
api.restartServer();
183+
restartServer();
85184
},
86185
});
87186
unwatchs.push(unwatchPkg);
88187

89188
// watch config change
90189
const unwatchConfig = api.service.configInstance.watch({
91190
userConfig: api.service.userConfig,
92-
onChange: async ({ pluginChanged, userConfig, valueChanged }) => {
191+
onChange: async ({ pluginChanged, valueChanged }) => {
93192
if (pluginChanged.length) {
94193
console.log();
95194
api.logger.info(`Plugins of ${pluginChanged.map((p) => p.key).join(', ')} changed.`);
96-
api.restartServer();
195+
restartServer();
97196
}
98197
if (valueChanged.length) {
99198
let reload = false;
@@ -117,7 +216,7 @@ export default (api: IApi) => {
117216
if (reload) {
118217
console.log();
119218
api.logger.info(`Config ${reloadConfigs.join(', ')} changed.`);
120-
api.restartServer();
219+
restartServer();
121220
} else {
122221
api.service.userConfig = api.service.configInstance.getUserConfig();
123222

@@ -147,6 +246,8 @@ export default (api: IApi) => {
147246
});
148247
unwatchs.push(unwatchConfig);
149248
}
249+
250+
await startMetroServer();
150251
},
151252
});
152253

@@ -155,7 +256,7 @@ export default (api: IApi) => {
155256
description: 'builds react-native javascript bundle for offline use',
156257
fn: async ({ args: { platform } }) => {
157258
logger.info('rn-build:', platform);
158-
// await generateFiles({ api, watch: false });
259+
await generateFiles({ api });
159260
},
160261
});
161262
};

packages/bundler-metro/tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"extends": "../../tsconfig",
33
"compilerOptions": {
44
"rootDir": "src",
5-
"outDir": "lib"
5+
"outDir": "lib",
6+
"skipLibCheck": true
67
},
78
"include": ["src", "../../types"]
89
}

packages/plugin/src/entryFileTpl.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,11 @@ const getClientRender = (args: { hot?: boolean } = {}) => plugin.applyPlugins({
2525
const clientRender = getClientRender();
2626
export default clientRender();
2727
28-
{{{ entryCode }}}
29-
3028
// hot module replacement
3129
// @ts-ignore
3230
if (module.hot) {
3331
// @ts-ignore
34-
module.hot.accept('./core/routes', () => {
32+
module.hot.accept(() => {
3533
getClientRender({ hot: true })();
3634
});
3735
}

packages/renderer/src/renderClient/renderClient.tsx

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect } from 'react';
1+
import React, { useEffect, ReactElement } from 'react';
22
import { AppRegistry } from 'react-native';
33
import { ApplyPluginsType, Plugin } from '@umijs/runtime';
44
import { IRoute, renderRoutes } from '@umijs/renderer-react';
@@ -18,7 +18,7 @@ interface IOpts extends IRouterComponentProps {
1818
appKey: string;
1919
}
2020

21-
function RouterComponent(props: IRouterComponentProps) {
21+
function RouterComponent(props: IRouterComponentProps): JSX.Element {
2222
const { history, ...renderRoutesProps } = props;
2323

2424
useEffect(() => {
@@ -44,17 +44,17 @@ function RouterComponent(props: IRouterComponentProps) {
4444
return <Router history={history}>{renderRoutes(renderRoutesProps)}</Router>;
4545
}
4646

47-
export default function renderClient(opts: IOpts) {
48-
const rootContainer = opts.plugin.applyPlugins({
47+
export default function renderClient({ plugin, appKey, history, routes }: IOpts) {
48+
const rootContainer = plugin.applyPlugins({
4949
type: ApplyPluginsType.modify,
5050
key: 'rootContainer',
51-
initialValue: <RouterComponent history={opts.history} routes={opts.routes} plugin={opts.plugin} />,
51+
initialValue: <RouterComponent history={history} routes={routes} plugin={plugin} />,
5252
args: {
53-
history: opts.history,
54-
routes: opts.routes,
55-
plugin: opts.plugin,
53+
history,
54+
routes,
55+
plugin,
5656
},
57-
});
58-
AppRegistry.registerComponent(opts.appKey, rootContainer);
57+
}) as ReactElement;
58+
AppRegistry.registerComponent(appKey, () => () => rootContainer);
5959
return rootContainer;
6060
}

0 commit comments

Comments
 (0)