原文地址:Rome, a new JavaScript Toolchain
作者:Jason Miller

Sebastian McKenzie是Yarn和Babel的创建者,也是Facebook中React Native团队的成员,其一直致力于JavaScript和TypeScript开发中集大成的解决方案。

Rome项目的取名,源自“条条大路通罗马”,于2020年2月26号公开。

什么是Rome?

Rome是从零构建的一个具有JavaScript完整工具链的项目。其提供了编译构建JavaScript项目、lint和类型检查、执行测试用例以及格式化代码的功能。

雏形

虽然Rome处于非常初期的阶段,但CLI能为我们提供一些有关其用法的有用信息:

命令 描述
rome bundle 为项目构建独立的JS包
rome compile 编译一个文件
rome develop 启动网络服务
rome parse 解析当个文件为AST
rome resolve 解析文件
rome analyzeDependencies 分析并转储文件的依赖

有关完整的使用细节,请参考CLI Usage

为什么这可能是个好主意?

Rome采用了一种不同于现有开源工具的维护方式,其可能与大型公司内部基于单仓库的工具更为类似。Rome中所有的构建和编译都由其本身完成,而不会通过现有的开源工具来完成。

这有助于解决目前如Webpack和Rollup这类流行的构建工具所面临的一个问题,即分析优化整个项目非常困难且耗时,因为每个工具都必须解析并构造出自己的AST。

打包

Rome的结构比较独特:所有的编译工作都是在每个模块的基础上进行的,这允许每个模块都能在一个工作线程池中进行处理。这对于每个模块来说能达到很好的转换效果,但对于打包来说是一个挑战:为了避免重新解析其它模块,就需要对模块进行预命名,以便它们能够共享同一个作用域。

尽管Rome的编译是针对每个文件的,为了实现打包的功能,Rome会给所有模块作用域下的变量添加基于模块文件名生成的标识符前缀。如:在一个名为text.js文件中有变量foo,最终会解析为test_js_foo。

这也同样会应用在每个模块的导入导出标识符上,这意味着任何模块的导出都可以通过使用模块文件名和导出名来解决。如:

文件名 内容 输出
test.js export const foo = 1; const ____R$test_js$foo = 1;
index.js import { foo } from './test.js'; console.log(foo); console.log(___R$test_js$foo);

输出质量

对于现代Web开发来说,工具通常决定了我们开发应用的效率以及应用的大小。这意味着去分析程序包的构成是非常有意义的,即我们需要去关注Rome是如何对应用进行打包的。尤其是,我总是非常感兴趣于工具在打包过程中是否会将模块合并到一个共享的闭包中(如Rollup),还是通过闭包和运行时加载来分离不同的模块(如Webpack)。

我对Rome的产物进行了初步的调查,其似乎将模块进行了合并并生成了单闭包,这与通过Rollup进行构建的结果非常相似。如:

  1. 输入(模块)
function hello() {  
  return 'Hello World';
}

console.log(hello()); 
  1. 输出(产物)
(function(global) {
  'use strict';
  // input.ts

  const ___R$rome$input_ts = {};
  function ___R$$priv$rome$input_ts$hello() {
    return 'Hello World';
  }

  console.log(___R$$priv$rome$input_ts$hello());

  return ___R$rome$input_ts;
})(typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : this);

考虑到项目还处于初期迭代预览的过程中,Rome目前还不支持压缩打包产物的能力是可以接受的。然而,将产物在通过Terser进行处理后,就能得到非常满意的输出产物。

!function(o) {
    "use strict";
    console.log("Hello World");
}("undefined" != typeof global ? global : "undefined" != typeof window && window);

正如我们所见,即使上述只是一段很简单的代码,打包后也存在一定的优化空间。理想情况下,构建工具是能够知道预设模式的。譬如,已知正在针对ES模块进行编译,则将省略闭包和严格模式。其还可以将global声明提升到模块的作用域下,在上述情况下就能够通过Terser对产物中的无效代码进行优化。

大项目应用

让我们看一个稍微复杂一点的示例,其中涉及两个公共依赖的模块:

entry.tsx:

import React from './react';  
import title from './other';  
// Rome还不支持动态引入
// const title = import('./other').then(m => m.default);

async function App(props: any) {  
  return <div id="app">{await title()}</div>
}

App({}).then(console.log); 

other.tsx:

import React from './react';  
export default () => <h1>Hello World</h1>;

react.tsx:

type VNode = {  
  type: string;
  props: any;
  children: Array<VNode|string>
};
function createElement(  
  type: string,
  props: any,
  ...children: Array<VNode|string>
): VNode {
  return { type, props, children };
}
export default { createElement };  

执行rome bundle entry.tsx out进行打包构建,会生成一个带有index.js文件的文件目录

(function(global) {
  'use strict';
  // rome/react.tsx

  function ___R$$priv$rome$react_tsx$createElement(
    type, props, ...children
  ) {
    return {type: type, props: props, children: children};
  }
  const ___R$rome$react_tsx$default = {
    createElement: ___R$$priv$rome$react_tsx$createElement
  };

  // rome/other.tsx

  const ___R$rome$other_tsx$default = () =>
    ___R$rome$react_tsx$default.createElement(
      'h1', null, 'Hello World'
    );

  // rome/test.tsx

  const ___R$rome$test_tsx = {};
  async function ___R$$priv$rome$test_tsx$App(props) {
    return ___R$rome$react_tsx$default.createElement(
      'div', { id: 'app'},
      (await ___R$rome$other_tsx$default())
    );
  }

  ___R$$priv$rome$test_tsx$App({}).then(console.log);

  return ___R$rome$test_tsx;
})(typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : this);

虽然上述JavaScript文件看起来有点难以理解,但我们能够看到与单模块示例相同的结构。

除去Rome构建产物中的模块实现和CommonJS相关的代码,会发现三个模块都被内联到单个闭包中:

(function(global) {
  'use strict';
  // rome/react.tsx
  const ___R$rome$react_tsx$default = /* snip */;

  // rome/other.tsx
  const ___R$rome$other_tsx$default = /* snip */;

  // rome/entry.tsx
  ___R$$priv$rome$entry_tsx$App({}).then(console.log);
})(window);

压缩产物

正如我所提到的,Rome目前还不支持压缩功能,尽管Rome的设计很适合在打包阶段执行该功能。我们可以通过运行Terser来看一下上述的产物代码。为了便于阅读,这里对其进行了格式化:

! function(e) {
    const n = {
        createElement: function(e, n, ...t) {
            return {
                type: e,
                props: n,
                children: t
            }
        }
    };
    (async function(e) {
        return n.createElement("div", {
            id: "app"
        }, await n.createElement("h1", null, "Hello World"))
    })().then(console.log)
}("undefined" != typeof global ? global : "undefined" != typeof window && window);

压缩后的结果看起来还不错。但这还仅是一个很简单的示例,因此我们还无法看到Rome是如何打包一个完整的应用程序。

进一步优化

在过去半年中,我一直在从事一个项目,该项目用于自动优化JS打包文件。作为测试,我尝试将Rome打包出来的产物在执行Terser之前,先通过该项目进行优化编译。很高兴的说,其结果十分接近理想的输出:没有包装函数、没有无效代码,并且还利用了现代语法的优势:

const e = {  
  createElement: (e, n, ...t) =>
    ({ type: e, props: n, children: t })
};
(async () =>
  e.createElement("div", { id: "app" },
    await e.createElement("h1", null, "Hello World")
  )
)().then(console.log);

代码拆分

Rome似乎还不支持动态引入或代码拆分。在代码中使用import()会发现其像静态引入一样内联到产物中。原始的import()语句在打包的产物中保持不变,这就导致了语法错误。

代码拆分和分块是如何影响打包产物的还有待观察,因为两者都是从一个包中访问另一个包中的变量。

CLI Usage

如果仅想看一看Rome提供的命令,可以无需自行构建项目通过--help获得。(Rome目前还未用于生产环境,需要通过下载源码自行构建,尽管构建速度很快)

$ rome --help
  Usage: rome [command] [flags]

  Options

    --benchmark
    --benchmark-iterations <num>
    --collect-markers
    --cwd <input>
    --focus <input>
    --grep <input>
    --inverse-grep
    --log-path <input>
    --logs
    --log-workers
    --markers-path <input>
    --max-diagnostics <num>
    --no-profile-workers
    --no-show-all-diagnostics
    --profile
    --profile-path <input>
    --profile-sampling <num>
    --profile-timeout <num>
    --rage
    --rage-path <input>
    --resolver-mocks
    --resolver-scale <num>
    --silent
    --temporary-daemon
    --verbose
    --verbose-diagnostics
    --watch

  Code Quality Commands

    ci    install dependencies, run lint and tests
    lint  run lint against a set of files
    test  run tests
      --no-coverage
      --show-all-coverage
      --update-snapshots

  Internal Commands

    evict  evict a file from the memory cache
    logs   
    rage   

  Process Management Commands

    restart  restart daemon
    start    start daemon (if none running)
    status   get the current daemon status
    stop     stop a running daemon if one exists
    web      

  Project Management Commands

    config   
    publish  TODO
    run      TODO

  Source Code Commands

    analyzeDependencies  analyze and dump the dependencies of a file
      --compact
      --focus-source <input>
    bundle               build a standalone js bundle for a package
    compile              compile a single file
      --bundle
    develop              start a web server
      --port <num>
    parse                parse a single file and dump its ast
      --no-compact
      --show-despite-diagnostics
    resolve              resolve a file

文章来源于腾讯云开发者社区,点击查看原文