简介
接上一篇《JSBundle拆包之原理篇》
快速入门
安装
安装metro-core依赖主要有两种方式:npm和yarn。npm安装的命令如下:
代码语言:javascript复制npm install --save-dev metro metro-core
yarn方式的安装命令如下:
代码语言:javascript复制yarn add --dev metro metro-core
运行
metro bundle支持使用CLI脚手架方式运行和通过程序的编程调用它来运行。
在程序中使用metro需要先导入它,导入的方式如下:
代码语言:javascript复制const Metro = require('metro');
方法
metro提供了很多有用的函数,这些函数包括:
runMetro(config)
此方法用于给定配置,请求特定的服务。此时,您可以使用processRequest方法来hook HTTP(S)请求。例如:
代码语言:javascript复制'use strict';
const http = require('http');
const Metro = require('metro');
// We first load the config from the file system
Metro.loadConfig().then(config => {
const metroBundlerServer = Metro.runMetro(config);
const httpServer = http.createServer(
metroBundlerServer.processRequest.bind(metroBundlerServer),
);
httpServer.listen(8081);
});
为了与Express apps兼容,当请求无法被Metro bundler处理时,processRequest也会调用它的第三个参数。这允许您将服务与现有服务集成,或者扩展一个新的服务。
代码语言:javascript复制const httpServer = http.createServer((req, res) => {
metroBundlerServer.processRequest(req, res, () => {
// Metro does not know how to handle the request.
});
});
如果您正在使用Express,可以将processRequest作为中间件进行使用。
代码语言:javascript复制const express = require('express');
const app = express();
app.use(
metroBundlerServer.processRequest.bind(metroBundlerServer),
);
app.listen(8081);
runServer(Config, Options)
runServer基于给定的配置和选项启动开发服务,并返回服务。我们建议使用runMetro而不是runServer。runServer可选的参数有:
- host (string):服务的host驻留地址。
- onReady (Function):在服务已经准备好时服务请求时被调用。
- secure (boolean):服务是否需要运行在https上,而不是http上。
- secureKey (string):使用https访问时secureKey。
- secureCert (string):用于https访问的安全证书。
- hmrEnabled (boolean):是否开启热更新功能。
runBuild(Config, Options)
此函数用于,给定一个配置和一组通常传递给服务器的选项,以及一组特定于包本身的选项,并用于构建一个包。runBuild支持的选项有:
- dev (boolean):构建一个开发版本。例如,process.env.NODE_ENV = ‘development’。
- entry (string):指向要绑定的条目文件。
- onBegin (Function):绑定开始时被调用。
- onComplete (Function): 绑定完成后调用。
- onProgress (Function):在包期间调用,每次有关于模块计数/进度的新信息时被调用。
- minify (boolean): 是否缩小bundle。
- out (string):输出包的路径。
- platform (‘web’ | ‘android’ | ‘ios’): 指定打包的平台。
- sourceMap (boolean):是否生成源映射。
- sourceMapUrl (string): 源映射的URL匹配,它默认为与包相同的URL,只是将扩展名从.bundle更改为.map。
可用选项
有关配置选项的详细信息,可用参考下面的连接:Configuring Metro
URL与 bundle 请求
Assets
为了获取Assets资源,您可以使用require方法来获取一个js文件,服务器将根据特定的require请求返回js文件的路径。当请求Assets资源时通常会原样返回。
除此之外,服务器还可以根据平台和请求的大小返回特定的Assets资源。指定平台的方法是通过点后缀(例如.ios)和at后缀(例如@2x)方式来进行的。
Bundle
任何js文件都可以作为bundle来请求根文件,这个文件将被看作是项目的根目录,根目录将包含所有递归在内的文件。为了请求bundle包,只需将扩展名从.js更改为.bundle即可。构建包的选项有:
- dev: 是否以开发模式来构建包。
- platform: 平台请求包,可以是ios或android。
- minify: 代码是否应该缩小。
- excludeSource: 源码是否应该包含在源映射中。
例如,请求http://localhost:8081/foo/bar/baz.bundle?dev=true&platform=ios将创建一个foo/bar/baz包,js为iOS开发模式。
Source maps
通过使用与包相同的URL为每个包构建源映射,只有当inlineSourceMap设置为false时才会工作。您传递给包的所有选项将被添加到源映射URL;否则,它们就不匹配。
JavaScript transformer
JavaScript transformer被用来进行JS代码转换,适用于访问Babel。这个transformer可以导出两种方法:
transform(module)
此方法主要用于转换代码。接收到的对象将会被转换为包含一个ast键代码。默认的转换器仅能完成将代码解析为AST,以此来完成最低限度的工作:
代码语言:javascript复制const babylon = require('@babel/parser');
module.exports.transform = (file: {filename: string, src: string}) => {
const ast = babylon.parse(code, {sourceType: 'module'});
return {ast};
};
如果您想要使用babel插件,您可以通过将代码传递给它来实现:
代码语言:javascript复制const {transformSync} = require('@babel/core');
module.exports.transform = file => {
return transformSync(file.src, {
// Babel options...
});
};
getCacheKey()
此方法用于返回转换器缓存。当使用不同的转换器时,这允许正确地将转换后的文件绑定到转换它的转换器,且方法的结果必须是一个字符串。
概念
Metro是一个JavaScript的打包工具。它接收选项、一个条目文件,返回一个包含所有JavaScript的文件。Metro绑定程序主要涉及三个阶段:
- Resolution
- Transformation
- Serialization
Resolution
Metro需要从入口点构建所需的所有模块的图,要从另一个文件中找到所需的文件,需要使用Metro解析器。在现实开发中,这个阶段与Transformation阶段是并行的。
Transformation
所有模块都要经过Transformation阶段,Transformation负责将模块转换成目标平台可以理解的格式(如React Naitve)。模块的转换是基于拥有的核心数量来进行的。
Serialization
所有模块一经转换就会被序列化,Serialization会组合这些模块来生成一个或多个包,包就是将模块组合成一个JavaScript文件的包。
Modules
Metro被划分为多个模块,每个模块对应于流程中的每个步骤,每个模块都有自己的职责。这意味着我们每个模块可以根据您的需要进行交换。
构建
绑定时,每个模块都会被分配一个数字id,这意味着不支持动态需求。require通过数字版本更改、模块以不同的格式存储。支持三种不同的捆绑形式:
Plain bundle
这是一种标准的打包方式,在这种方式中,所有文件都用函数调用包装,然后添加到全局文件中,这对于只需要JS包(例如浏览器)的环境非常有用。只需要具有.bundle扩展名的入口点就可以完成它的构建。
Indexed RAM bundle
这种打包方式会将包打包成二进制文件,其格式包括以下部分:
- 一组数字:用于验证文件。uint32必须位于文件的开头,值为0xFB0BD1E5。
- 偏移表:该表是一个由32对uint32对组成的序列,带有一个表头。
- 其他子模块,由一个空字节( )完成。例如:
` 0 1 2 3 4 5 6
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
| Magic number | Header size |
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
| Startup code size | Module 0 offset |
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
| Module 0 length | |
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
| |
...
| |
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
| | Module n offset |
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
| Module n length | Module 0 code | Module 0 code | ... | |
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
| Module 1 code | Module 1 code | ... | | |
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
| |
...
| |
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
| | Module n code | Module n code | ... | |
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - `
这种结构对于同时加载内存中所有代码的环境来说是最优的:
- 通过使用偏移表,可以在固定的时间内加载任何模块,其中模块x的代码位于文件[(x 3) * sizeof(uint32)]。由于有一个空字符( )分隔所有模块,通常不需要使用长度,模块可以直接作为ASCIIZ字符串加载。
- 启动代码总是可以在文件[sizeof(uint32)]中找到。
Indexed RAM bundle通常被用于iOS分包。
File RAM bundle
每个模块都会被存储为一个文件,例如,名称为js-modules/${id},创建了一个名为UNBUNDLE的额外文件,它唯一的内容是一个数字0xFB0BD1E5。注意,解包文件是在根目录下创建的。 Android通常使用这种方式分包,因为包内容是压缩的,而且访问压缩文件要快得多。如果使用索引方式(Indexed RAM bundle),则应立即解压缩所有绑定,以获取对应模块的代码。
缓存
Metro具有多层缓存,您可以设置多个缓存供Metro使用,而不是一个缓存。下面来看看Motro的多层缓存是如何工作的。
为什么要缓存
缓存提供了很大的性能优势,它们可以将打包的速度提高十倍以上。然而,许多系统使用的是非持久缓存。对于Metro来说,我们有一种更复杂的层系统缓存方式。例如,我们可以在服务器上存储缓存,这样,连接到同一服务器的所有打包都可以使用共享缓存。因此,CI服务器和本地开发的初始构建时间显著降低。
我们希望将缓存存储在多个位置,以便缓存可以执行回退操作。这就是为什么有一个多层缓存系统。
缓存的请求与缓存
在Metro中,系统使用了一个排序机制来决定使用哪个缓存。为了检索缓存,我们从上到下遍历缓存,直到找到结果;为了保存缓存,我们同样遍历缓存,直到找到具有缓存的存储。
假设您有两个缓存存储:一个在服务器上,另一个在本地文件系统上。那么,你可以这样指定:
代码语言:javascript复制const config = {
cacheStores: [
new FileStore({/*opts*/}),
new NetworkStore({/*opts*/})
]
}
当我们检索缓存时,Metro将首先查看本地文件存储,如果不能找到缓存,它将检查NetworkStore。最后,如果没有缓存,它将生成一个新的缓存。一旦缓存生成,Metro将再次从上到下在所有存储中存储缓存。如果找到缓存,也会进行存储。例如,如果Metro在NetworkStore中找到缓存,它也会将其存储在FileStore中。
API
API
Methods
metro提供了如下一些方法:
- loadConfig()
- async runMetro(config)
- async runBuild(config, )
- async runServer(config, )
- createConnectMiddleware(config, )
使用
编译文件:
代码语言:javascript复制const config = await Metro.loadConfig();
await Metro.runBuild(config, {
entry: 'index.js',
out: 'bundle.js',
});
运行服务并监视文件系统的更改:
代码语言:javascript复制const config = await Metro.loadConfig();
await Metro.runServer(config, {
port: 8080,
});
Reference
下面公开的所有函数都会接受一个附加的配置选项,即metro.config.js,如意要使用它,你可以使用Metro.loadConfig来获得它。
loadConfig()
Basic options: config, cwd 加载Metro配置,如果指定,可以从选项中的config加载,也可以从cwd到根目录遍历直到找到一个文件(默认metro.config.js)。返回的配置将与Metro的默认值合并。
async runMetro(config)
基于配置创建一个Metro服务器并返回它,您可以将其用作现有服务器中的中间件。
async runBuild(config, )
绑定给定平台的条目,并将其保存到外部位置。如果设置了sourceMap,还会生成一个源映射。源映射将被内联,除非还定义了sourceMapUrl。在后一种情况下,将使用sourceMapUrl参数的basename生成一个新文件。
async runServer(config, )
启动一个完整的Metro HTTP服务,它将侦听指定的host:port,然后可以查询它以检索各种入口点的包。如果提供了安全系列选项,服务器将通过HTTPS公开。如果设置了hmrEnabled,服务器还将公开websocket服务器内容,并将HMR客户机注入生成的包中。
createConnectMiddleware(config, )
与其创建完整的服务器不同,此函数用于创建一个连接中间件来响应包请求。然后可以将此中间件插入您自己的服务器,端口参数是可选的,仅用于日志记录。
Metro配置
Metro配置可以通过以下三种方式创建:
- metro.config.js
- metro.config.json
- The metro field in package.json
您还可以通过调用CLI时指定自定义文件,文件的格式为:
代码语言:javascript复制--config <path/to/config>
结构
每个模块都有一个单独的配置选项,Metro中常见的配置结构如下:
代码语言:javascript复制module.exports = {
resolver: {
/* resolver options */
},
transformer: {
/* transformer options */
},
serializer: {
/* serializer options */
},
server: {
/* server options */
}
/* general options */
};
可用的选项参数可以参考下面的链接:General Options
Option | Type | Description |
---|---|---|
cacheStores | Array<CacheStore<TransformResult<>> | 列出存储缓存的位置 |
cacheVersion | string | 将使整个metro缓存生成一个键 |
projectRoot | string | 项目的根文件 |
watchFolders | Array < string> | 指定任何额外的监视文件夹 |
transformerPath | string | 转换器路径 |
watch | boolean | 是否监视所有文件 |
reporter | {update: () => void} | 是否监视打包过程中的状态 |
resetCache | boolean | 是否在启动构建时重置缓存 |
stickyWorkers | boolean | 创建的worker是否应该基于文件名 |
maxWorkers | number | 把序列化的包串联起来 |
服务器选项可以参考下面的链接:Server Options
Option | Type | Description |
---|---|---|
port | number | 监听的端口 |
useGlobalHotkey | boolean | 是否打开热更新快捷键,快捷键为CMD R |
enhanceMiddleware | (Middleware, Server) => Middleware | 添加自定义中间件 |
enableVisualizer | boolean | 启用metro-visualizer中间件 |
Metro的转化器选项如下:Transformer Options
Option | Type | Description |
---|---|---|
asyncRequireModulePath | string | 处理异步请求模块 |
babelTransformerPath | string | 使用自定义babel转换器 |
dynamicDepsInPackages | string (throwAtRuntime or reject) | 发现动态依赖的处理动作 |
enableBabelRCLookup | boolean (default: true) | 是否使用.babelrc配置文件 |
enableBabelRuntime | boolean (default: true) | 是否使用@babel/transform/runtime插件 |
enableBabelRuntime | boolean (default: true) | 是否使用.babelrc配置文件 |
未完待续!!!!!