为不同的组件加载不同的JS库文件

Load different JS library files for different components

我有一个用 ReactJS 制作的网站。在public/index.html,我有

<head>
  <script src="/lib/analyzejs-v1.js"></script>
  <script src="/lib/analyzejs-v2.js"></script>
</head>
<body>
  <div id="root"></div>
</body>

其中analyzejs-v1.js有6Mo,analyzejs-v2.js有3Mo;它们都是固定文件,我不能修改太多。

这两个文件不是模块;它们的功能已声明(例如,src/defines/analyzejs-v1.d.ts 中的 declare function f1(address: string): string;)。所以一些组件调用analyzejs-v1.js的函数是直接使用f1(...)这样的函数名,没有任何命名空间,没有import,没有export。其余组件直接使用f2(...)这样的函数名调用analyzejs-v2.js的函数,无需任何命名空间、导入或导出。

加载这两个js库文件需要时间。所以我正在寻找一种方法来根据组件(或 URL)加载 analyzejs-v1.jsanalyzejs-v2.js

那么有没有人知道一个常规的方法,为不同的组件加载不同的JS库文件?

当您使用 <script> 标签导入脚本时,该库只能在客户端使用,因此不能与节点一起使用。但是,如果您将其标记为模块,则另一个脚本可以像这样使用它:

index.html:

<script src="test.mjs" type="module"></script>
<script type="module">
      import {hello} from "./test.mjs"
      hello()
</script>

test.mjs:

export function hello(text) {
    console.log("hello from test")
}

唯一的问题是你的反应脚本和内联脚本之间的通信。我想通了,唯一的方法是使用 window.

免责声明

我真的不确定,是否有人应该这样使用它。我只测试过一次,它很可能会坏掉……也许其他人可以告诉我他们对我的方法的看法。

index.tsx

... // imports

(window as any).importStuff = (a: any) => {
    a.hello()
}

...

index.html

<script src="test.mjs" type="module"></script>
<script type="module">
      import {hello} from "./test.mjs"
      window.importStuff({
            hello: hello
      })
</script>

如果您不需要同时使用两个脚本,您可以在必要时在运行时添加脚本标签。 我可以为您提供一个钩子,我用它来动态加载脚本。

export function useScript(url: string, clean: boolean = false, cleanJob: () => void = () => undefined): boolean {
  const [loaded, setLoaded] = useState(false);
  useEffect(() => {
    let create = false;
    let script = document.querySelector(`script[src="${url}"]`) as HTMLScriptElement | null;
    if (!script) {
      script = document.createElement('script');
      script.src = url;
      script.async = true;
      if (type (document as any).attachEvent === 'object') {
        (script as any).onreadystatechange = () => {
          if ((script as any).readyState === 'loaded') {
            setLoaded(true);
          }
        }
      } else {
        script.onload = () => {
          setLoaded(true);
        }
      }
      document.body.appendChild(script);
      create = true;
    } else {
      setLoaded(true);
    }
    // For a special library, you can do the clean work by deleting the variable it exports.
    return () => {
      if (create && script && clean) {
        setLoaded(false);
        document.body.removeChild(script);
        cleanJob && cleanJob();
      }
    }
  }, [url]);
  return loaded;
}

使用方法:

export const Comp = (props: ICompProps) => {
 const loaded = useScript('https://path/to/the/script.js');
 // if you want to do some clean work, Suppose the external script introduces the variable A, And A can be reasigned.
 // const loaded = useScript('https://path/to/the/script.js', true, () -> { A = undefined; });
 useEffect(() -> {
   if (loaded) {
     // Suppose the external script introduces the variable A. Now it is available.
     const a = new A();
     // do something with a.
   }
 }, [loaded]);
 if (loaded) {
   return XXX;  
 } else {
   return null;
 }
}

如果脚本不是模块,只需添加一个不带导入语句的typescript声明文件,并声明脚本导出的全局变量。如:

declare interface XXX {
  YYY
}
declare const ScriptValue: XXX;

如果您正在寻找更好的页面性能并且不让脚本阻止 DOM 内容加载,那么您可能需要考虑向脚本添加延迟 https://javascript.info/script-async-defer#defer

如果您正在寻找脚本的动态加载,并且仅在首次使用时加载脚本,请查看此文档 https://javascript.info/script-async-defer#dynamic-scripts

您可以在加载组件时创建 <script> 标记。

首先,我们创建一个函数来创建脚本标签

const scriptGenerator = (options = {}) => {
    const s = document.createElement("script");
    for (const option in options) {
        s[option] = options[option]
    }

    document.querySelector("head").appendChild(s);
}

我们可以使用两个属性来加载脚本

  • 推迟
  • 异步

defer: defer 属性告诉浏览器不要等待脚本。相反,浏览器将继续处理 HTML,构建 DOM。该脚本“在后台”加载,然后在 DOM 完全构建时运行。

async: async属性有点像defer。它还使脚本成为非阻塞的。但它在行为上有重要差异。

async & defer Docs

完成这些步骤后,我们就可以定义我们的脚本。您可以使用 useEffect

useEffect(() => {
    // V1
    scriptGenerator({
        src: "...",
        async: 1,
    });

    // V2
    scriptGenerator({
        src: "...",
        async: 1,
    });
}, []);

退出组件后不要忘记delete它们

useEffect(() => {
    // Generate script

    return () => {
        document
            .querySelector("head")
            .querySelectorAll('script[src="..."]')
            .remove();
    }
});

如果要得出一个结论,就是如下

import { useEffect } from 'react';

const Component = () => {
    const scriptGenerator = (options = {}) => {
        const s = document.createElement("script");
        for (const option in options) {
            s[option] = options[option]
        }

        document.querySelector("head").appendChild(s);
    }

    useEffect(() => {
        // V1
        scriptGenerator({
            src: "...",
            async: 1,
        });

        // V2
        scriptGenerator({
            src: "...",
            async: 1,
        });

        return () => {
            document
                .querySelector("head")
                .querySelectorAll('script[src="..."]')
                .remove();
        }
    });
}