externals
什么是 externals
外部扩展(Externals),如果你熟悉 webpack
,vite
, rollup
中任何一个打包器,那么你对这个应该不陌生. 以下是 webpack 的解释
防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。
你可以理解为,项目中引用了很多外部的包,比如 react, jquery 等等这些不经常改变的包,进行对 externals 的配置之后,能将这些引用的外部包从主逻辑代码中提取出来,存放到一个指定文件中。
举例理解
比如我们有一个 index.entry.ts
文件,里面使用了 react
, 和 antd
// 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
打包后的文件是长这样的
// 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 行,们开发项目时,像这些外部的包,压根是不会进行修改的,但是每次修改都需要重新引用 react
和 antd
这俩大家伙,这很明显是不理想,不合理更不应该的.
我们配置了 externals
之后,打包后的文件是长这样的
// 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
文件
// 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
即可
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",
},
]);
可以看到,对于 react
和 antd
现在是通过 window 变量上获取,这样我们每次修改逻辑代码,只会编译逻辑代码(index.js
),而不会再编译外部包的引用了,这样极大的加快了编译速度和减少用电量.
为什么要使用 externals
如上面这个例子,在没有配置 externals 之前,每次修改 injectScript 的代码都要重新编译 react
和 antd
最快也得 3s,配置完 externals 之后,最快只需要 10ms,差了 300倍的编译速度,你不考虑下吗?
如何配置
默认情况下一些常见的包已经添加到 externals 配置中,它们会在 injectScript 编译时自动提取出来放到 externals.js 文件中
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
可以通过以下方式进行配置
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 变量上
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 中注册它
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
import { Button } from "antd";
export default {
antd: { Button },
};
import { Button } from "antd";
export default {
antd: { Button },
};
WARNING
如果使用你扩展的用户并不关心文件大小,或者你也觉得无所谓,那么你可以不用 tree shark,因为它确实有些麻烦,比如此时我们又在 injectScript 中使用到了 Modal
组件,那么你还必须再跑到这个文件夹中导出 Modal