logo
Published on

如何在一个JavaScript文件中引入另一个JavaScript文件

Authors
  • Name
    Twitter

旧版本的JavaScript没有import、include或require,因此开发者们采用了许多不同的方法来解决这个问题。但自从2015年(ES6)以来,JavaScript已经有了ES6模块标准,可以在Node.js中导入模块,并且大多数现代浏览器也支持这一标准。

为了兼容老版本浏览器,可以使用诸如WebpackRollup这样构建工具,以及Babel等转译工具。

ES6 模块

自Node.js v8.5(需要--experimental-modules标志)以来,Node.js支持ECMAScript(ES6)模块。从Node.js v13.8.0起,无需任何标志即可使用ES6模块。要启用"ESM"(与Node.js之前的CommonJS模块系统"CJS"相比),你可以在package.json中使用"type": "module",或者将文件拓展名改为.mjs。同样,使用Node.js之前的CJS模块可以命名为.cjs,如果你的默认模块类型是ESM。

使用package.json

{
  "type": "module"
}

然后在module.js中:

export function hello() {
  return 'Hello'
}

最后在main.js中:

import { hello } from './module.js'
let val = hello() // val是"Hello"

使用.mjs文件:

export function hello() {
  return 'Hello'
}

然后在main.mjs中:

import { hello } from './module.mjs'
let val = hello() // val是"Hello"

浏览器中的ECMAScript模块

浏览器从Safari 10.1、Chrome 61、Firefox 60和Edge 16开始支持直接加载ECMAScript模块(无需Webpack等工具)。检查当前支持情况可以访问caniuse。这里不需要使用Node.js的.mjs扩展名;浏览器会完全忽略模块/脚本的文件扩展名。

<script type="module">
  import { hello } from './hello.mjs' // 或者扩展名可以是`.js`
  hello('world')
</script>
// hello.mjs 或者扩展名可以是`.js`
export function hello(text) {
  const div = document.createElement('div')
  div.textContent = `Hello ${text}`
  document.body.appendChild(div)
}

更多内容请参见:https://jakearchibald.com/2017/es-modules-in-browsers/

浏览器中的动态导入

动态导入允许脚本根据需要加载其他脚本:

<script type="module">
  import('hello.mjs').then((module) => {
    module.hello('world')
  })
</script>

更多内容请参见:https://developers.google.com/web/updates/2017/11/dynamic-import

Node.js的require

旧的CJS模块风格仍然在Node.js中被广泛使用,即module.exports/require系统。

// mymodule.js
module.exports = {
  hello: function () {
    return 'Hello'
  },
}
// server.js
const myModule = require('./mymodule')
let val = myModule.hello() // val是"Hello"

在浏览器中加载外部JavaScript的其他方式

AJAX加载

可以使用AJAX调用加载附加脚本,然后使用eval运行它。这是最简单的方式,但由于JavaScript沙盒安全模型的限制,它仅限于当前域。使用eval也会带来bug、黑客和安全问题。

Fetch加载

类似于动态导入,可以使用fetch调用并使用Promise控制脚本依赖的执行顺序。可以使用Fetch Inject库:

fetchInject(['https://cdn.jsdelivr.net/momentjs/2.17.1/moment.min.js']).then(() => {
  console.log(`Finish in less than ${moment().endOf('year').fromNow(true)}`)
})

jQuery加载

jQuery库提供了一行代码的加载功能:jQuery.getScript

$.getScript('my_lovely_script.js', function () {
  alert('Script loaded but not necessarily executed.')
})

动态脚本加载

可以将带有脚本URL的script标签添加到HTML中。为了避免jQuery的开销,这是一种理想的解决方案。脚本甚至可以驻留在不同的服务器上。此外,浏览器将评估代码。<script>标签可以注入网页的<head>部分,或插入在关闭的</body>标签之前。

样例如下:

function dynamicallyLoadScript(url) {
  var script = document.createElement('script') // 创建一个script DOM节点
  script.src = url // 将其src设置为提供的URL

  document.head.appendChild(script) // 将它添加到页面head部分的末尾(可以将'head'改为'body'以将其添加到body部分的末尾)
}

此函数将一个新的<script>标签添加到页面head部分的末尾,其中src属性设置为传递给函数的URL。这种和其他方法在JavaScript Madness: Dynamic Script Loading中有详细讨论和示例。

检测脚本执行完成

现代浏览器会异步加载文件以提升性能(这适用于jQuery方法和手动动态脚本加载方法)。因此,如果直接使用这些方法,你无法在请求加载脚本后的下一行使用新加载的代码,因为其可能仍在加载中。

例如:my_lovely_script.js包含MySuperObject

var js = document.createElement("script");

js.type = "text/javascript";
js.src = jsFilePath;

document.body.appendChild(js);

var s = new MySuperObject();

Error : MySuperObject is undefined

然后刷新页面(F5),它却工作了!令人困惑...

所以该怎么解决这个问题呢?

你可以使用作者在上面提供的链接中提出的解决方案。简而言之,他使用事件在脚本加载时运行回调函数。将所有使用远程库的代码放在回调函数中。例如:

function loadScript(url, callback) {
  var head = document.head
  var script = document.createElement('script')
  script.type = 'text/javascript'
  script.src = url

  script.onreadystatechange = callback
  script.onload = callback

  head.appendChild(script)
}

然后你可以在一个匿名函数中编写你希望在脚本加载后使用的代码:

var myPrettyCode = function () {
  // 在这里做任何事
}

然后运行这些代码:

loadScript('my_lovely_script.js', myPrettyCode)

需要注意的是,脚本可能在DOM加载后执行,也可能在此之前,具体取决于浏览器以及你是否包含了script.async = false;这一行代码。关于JavaScript加载的更多内容,可以参见这篇绝佳的文章,它全面讨论了JavaScript的加载问题。

代码合并/预处理

如本文开头所述,许多开发者在其项目中使用Parcel、Webpack或Babel等构建/转译工具,这使他们能够使用即将发布的JavaScript语法,提供对旧浏览器的兼容性,合并文件,压缩代码,执行代码分割等。