Skip to content

externals

什么是 externals

外部扩展(Externals),如果你熟悉 webpackviterollup 中任何一个打包器,那么你对这个应该不陌生。以下是 webpack 的解释:

防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。

简单说就是:项目中引用了很多外部的包,比如 react、jquery 等不经常改变的包,配置了 externals 之后,可以把这些外部包从主逻辑代码中提取出来,存放到一个单独的文件中。

举例理解

比如我们有一个 index.entry.ts 文件,里面使用了 reactantd

tsx
//  index.entry.ts
import { useState } from "react";
import { Modal, Space, Button } from "antd";
export function App() {
  const [open, setOpen] = useState(false);
  const [name, setName] = useState("");
  return (
    <div>
      <Modal
        open={open}
        title={"请输入你的名称"}
        onOk={() => {
          handleSave();
        }}
        okText={"保存"}
        onCancel={() => setOpen(false)}
      >
        <Space size={12} direction={"vertical"}>
          <Input value={name} onChange={(ev) => setName(ev.target.value)} />
        </Space>
      </Modal>
      <Button onClick={() => setOpen(true)}>Show</Button>
    </div>
  );
}
//  index.entry.ts
import { useState } from "react";
import { Modal, Space, Button } from "antd";
export function App() {
  const [open, setOpen] = useState(false);
  const [name, setName] = useState("");
  return (
    <div>
      <Modal
        open={open}
        title={"请输入你的名称"}
        onOk={() => {
          handleSave();
        }}
        okText={"保存"}
        onCancel={() => setOpen(false)}
      >
        <Space size={12} direction={"vertical"}>
          <Input value={name} onChange={(ev) => setName(ev.target.value)} />
        </Space>
      </Modal>
      <Button onClick={() => setOpen(true)}>Show</Button>
    </div>
  );
}

如果未配置 externals 打包后的文件是长这样的

js
//  dist/injects/index.js
const i = /* react runtime 的源代码 6.91kb */ react;
const o = /* antd 源代码 3m */ antd;

function App() {
  return i.jsxs("div", {
    children: [
      i.jsx(o.Modal, {
        open: s,
        title: "请输入你的名称",
        onOk: () => {
          d();
        },
        okText: "保存",
        onCancel: () => e(!1),
        children: i.jsxs(o.Space, {
          size: 12,
          direction: "vertical",
          children: [
            i.jsx(o.Input, { value: n, onChange: (l) => t(l.target.value) }),
          ],
        }),
      }),
      i.jsx(o.Button, { onClick: () => e(!0), children: "Show" }),
    ],
  });
}
//  dist/injects/index.js
const i = /* react runtime 的源代码 6.91kb */ react;
const o = /* antd 源代码 3m */ antd;

function App() {
  return i.jsxs("div", {
    children: [
      i.jsx(o.Modal, {
        open: s,
        title: "请输入你的名称",
        onOk: () => {
          d();
        },
        okText: "保存",
        onCancel: () => e(!1),
        children: i.jsxs(o.Space, {
          size: 12,
          direction: "vertical",
          children: [
            i.jsx(o.Input, { value: n, onChange: (l) => t(l.target.value) }),
          ],
        }),
      }),
      i.jsx(o.Button, { onClick: () => e(!0), children: "Show" }),
    ],
  });
}

我们实际的逻辑代码才不到 30 行,开发时这些外部的包压根不会去修改,但每次改代码都要重新打包 reactantd 这两个大家伙,明显不合理。

我们配置了 externals 之后,打包后的文件是长这样的

js
//  dist/injects/index.js
((i, o) => {
  function App() {
    return i.jsxs("div", {
      children: [
        i.jsx(o.Modal, {
          open: s,
          title: "请输入你的名称",
          onOk: () => {
            d();
          },
          okText: "保存",
          onCancel: () => e(!1),
          children: i.jsxs(o.Space, {
            size: 12,
            direction: "vertical",
            children: [
              i.jsx(o.Input, { value: n, onChange: (l) => t(l.target.value) }),
            ],
          }),
        }),
        i.jsx(o.Button, { onClick: () => e(!0), children: "Show" }),
      ],
    });
  }
})(
  window["extextenion_externals"]["react"],
  window["extextenion_externals"]["antd"],
);
//  dist/injects/index.js
((i, o) => {
  function App() {
    return i.jsxs("div", {
      children: [
        i.jsx(o.Modal, {
          open: s,
          title: "请输入你的名称",
          onOk: () => {
            d();
          },
          okText: "保存",
          onCancel: () => e(!1),
          children: i.jsxs(o.Space, {
            size: 12,
            direction: "vertical",
            children: [
              i.jsx(o.Input, { value: n, onChange: (l) => t(l.target.value) }),
            ],
          }),
        }),
        i.jsx(o.Button, { onClick: () => e(!0), children: "Show" }),
      ],
    });
  }
})(
  window["extextenion_externals"]["react"],
  window["extextenion_externals"]["antd"],
);

和一个 externals.js 文件

js
//  dist/injects/externals.js
window["extextenion_externals"]["react"] =
  /* react runtime 的源代码 6.91kb */ react;
window["extextenion_externals"]["antd"] = /* antd 源代码 3m */ antd;
//  dist/injects/externals.js
window["extextenion_externals"]["react"] =
  /* react runtime 的源代码 6.91kb */ react;
window["extextenion_externals"]["antd"] = /* antd 源代码 3m */ antd;

这样我们只需要在注入 index.js 之前先注入 externals.js 即可

typescript
browser.scripting.registerContentScripts([
  {
    id: `inject-module`,
    js: ["injects/externals.js", "injects/index.js"],
    matches: ["https://github.com/*"],
    runAt: "document_end",
    world: "MAIN",
  },
]);
browser.scripting.registerContentScripts([
  {
    id: `inject-module`,
    js: ["injects/externals.js", "injects/index.js"],
    matches: ["https://github.com/*"],
    runAt: "document_end",
    world: "MAIN",
  },
]);

可以看到,reactantd 现在是从 window 变量上获取的。这样每次修改逻辑代码,只会编译 index.js,不会再重新编译外部包,大幅加快了编译速度。

为什么要使用 externals

如上面这个例子,没配置 externals 之前,每次修改 injectScript 的代码都要重新编译 reactantd,最快也得 3s;配置了 externals 之后最快只需要 10ms,差了 300 倍。

如何配置

默认已经把一些常见的包加到了 externals 配置中,它们会在 injectScript 编译时自动提取到 externals.js 文件中:

typescript
const externals = Array.from(
  new Set([
    "react",
    "react-dom",
    "react-dom/client",
    "react/jsx-runtime",
    "antd",
    "lodash-es",
    "dayjs",
    "react-use",
    "@ant-design/icons",
    ..._externals,
  ]),
);
const externals = Array.from(
  new Set([
    "react",
    "react-dom",
    "react-dom/client",
    "react/jsx-runtime",
    "antd",
    "lodash-es",
    "dayjs",
    "react-use",
    "@ant-design/icons",
    ..._externals,
  ]),
);

如果你需要添加额外的包,比如 jquery,可以这样配置:

typescript
import { defineConfig } from "vite";

export default defineConfig({
  //  ...
  plugins: [
    //    ...
    //    在这里你可以配置所有 webextkits 提供的 vite 插件
    ViteWebExtKits({
      extensionId: extId,
      externals: ["jquery"],
    }),
  ],
});
import { defineConfig } from "vite";

export default defineConfig({
  //  ...
  plugins: [
    //    ...
    //    在这里你可以配置所有 webextkits 提供的 vite 插件
    ViteWebExtKits({
      extensionId: extId,
      externals: ["jquery"],
    }),
  ],
});

在 externals.ts 中导出外部包

定义 externals 配置后,还需要在 /src/scopes/injects/externals.ts 中导出对应的包,这样它才能正确加载到 window 变量上:

typescript
import * as antd from "antd";
import React from "react";
import ReactDOM from "react-dom/client";
import JSXRuntime from "react/jsx-runtime";
import jquery from "jquery";

export default {
  antd: antd,
  react: React,
  "react-dom/client": ReactDOM,
  "react/jsx-runtime": JSXRuntime,
  jquery: jquery,
};
import * as antd from "antd";
import React from "react";
import ReactDOM from "react-dom/client";
import JSXRuntime from "react/jsx-runtime";
import jquery from "jquery";

export default {
  antd: antd,
  react: React,
  "react-dom/client": ReactDOM,
  "react/jsx-runtime": JSXRuntime,
  jquery: jquery,
};

WARNING

导出的默认对象中的 key 需要保证它与包的名称是一致的,这样才能正确建立依赖关系

如何在 background 中配置 externals

配置好 externals 并在 externals.ts 中导出了对应的包之后,就可以在 background 中注册:

typescript
browser.scripting.registerContentScripts([
  {
    id: `inject-module`,
    //    注意一定要放在入口文件之前执行
    js: ["injects/externals.js", "injects/index.js"],
    matches: ["https://github.com/*"],
    runAt: "document_end",
    world: "MAIN",
  },
]);
browser.scripting.registerContentScripts([
  {
    id: `inject-module`,
    //    注意一定要放在入口文件之前执行
    js: ["injects/externals.js", "injects/index.js"],
    matches: ["https://github.com/*"],
    runAt: "document_end",
    world: "MAIN",
  },
]);

tree shaking

对于 antd 这种大包,如果你只用几个组件,却把整个包都导出就有点浪费了。externals.ts 支持 tree shaking,用到什么组件就导出什么组件。比如只用到了 antd 中的 Button 组件,那么可以只导出 Button

typescript
import { Button } from "antd";

export default {
  antd: { Button },
};
import { Button } from "antd";

export default {
  antd: { Button },
};

WARNING

如果用户不关心文件大小,或者你觉得无所谓,可以不用 tree shaking。因为它确实有点麻烦,比如后来在 injectScript 中又用到了 Modal 组件,你还得跑回来把 Modal 也导出

Powered by Vitepress