Skip to content

externals

什么是 externals

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

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

你可以理解为,项目中引用了很多外部的包,比如 react, jquery 等等这些不经常改变的包,进行对 externals 的配置之后,能将这些引用的外部包从主逻辑代码中提取出来,存放到一个指定文件中。

举例理解

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

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: [
    //    ...
    //    在这里你可以配置所有 webextkit 提供的 vite 插件
    ViteWebExtKits({
      extensionId: extId,
      externals: ["jquery"],
    }),
  ],
});
import { defineConfig } from "vite";

export default defineConfig({
  //  ...
  plugins: [
    //    ...
    //    在这里你可以配置所有 webextkit 提供的 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 shark

对于一些大家伙比如 antd,如果你只需要使用几个组件,却将整个 antd 包都导出的话,这有点不理想。我们希望的应该是用到什么组件,导出什么组件,externals.ts 支持 tree shark,比如我们只用到了 antd 中的 Button 组件,那么你可以只导出 Button

typescript
import { Button } from "antd";

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

export default {
  antd: { Button },
};

WARNING

如果使用你扩展的用户并不关心文件大小,或者你也觉得无所谓,那么你可以不用 tree shark,因为它确实有些麻烦,比如此时我们又在 injectScript 中使用到了 Modal 组件,那么你还必须再跑到这个文件夹中导出 Modal

Powered by Vitepress