有马

chrome63支持动态 import()

原文链接: developers.google.com

作者: Mathias Bynens. V8 开发者.

Note: Chrome 63和 Safari Technology Preview24表示已经支持动态 import()加载ES Module。

动态import()提案介绍了一个类似import的函数,和静态import相比,它解锁了一些新的能力,本文将比较这两者,并做出概述。

Static import

早在9月份,Chrome 61就提供了对ES2015 import 语句的支持,实现模块加载

util.mjs代码如下:

// Default export
export default () => {
  console.log('Hi from the default export!');
};

// Named export `doStuff`
export const doStuff = () => {
  console.log('Doing stuff…');
};

下面的代码通过静态import使用./util.mjs模块:

<script type="module">
  import * as module from './utils.mjs';
  module.default();
  // → logs 'Hi from the default export!'
  module.doStuff();
  // → logs 'Doing stuff…'
</script>

Note: 上面的例子使用.mjs扩展符表示这是一个模块而不是普通的脚本。在web上文件扩展名并不重要,只要在HTTP头部Content-Type上加上正确的MIME类型就可以,例如JavaScript 文件的MIME类型是text/javascript.mjs文件扩展名在像Node这样的平台上非常有用,Node没有MIME类型的概念,也没有像type=module这样的属性表示它是一个模块而不是普通脚本。我们在这里使用相同的扩展名来保证跨平台的一致性,并明确区分模块和常规脚本。

这个导入模块的语法形式是一种 静态 声明:它只接受一个字符串作为模块标识符,通过预运行时将绑定引入本地作用域的“连接”过程。 静态import语法只能在文件的顶层使用,它可以开启一些重要功能,如静态分析、捆绑工具和tree-shaking。

下面这些有用的场景,是用静态import解决不了的:

  • 根据需要导入模块(或有条件)

  • 在运行时计算模块标识符

  • 从一个普通的脚本中导入一个模块(而不是一个模块)

Dynamic import() ?

动态import()提案介绍了一个类似import的函数,import(moduleSpecifier) 在获取,实例化并评估所有模块的依赖关系后(包扩这个模块本身),返回这个请求模块的命名空间的Promise对象。

下面的代码展示了如何通过动态import()使用./util.mjs模块:

<script type="module">
  const moduleSpecifier = './utils.mjs';
  import(moduleSpecifier)
    .then((module) => {
      module.default();
      // → logs 'Hi from the default export!'
      module.doStuff();
      // → logs 'Doing stuff…'
    });
</script>

Note: 尽管import()像函数一样调用, 它被指定为恰好使用括号的语法 (例如 super()). 这就意味着import并没有继承Function.prototype,所以你不能调用callapply调用它,而且const importAlias = import这样的语句也不会生效,import甚至都不是对象,但是这在实践中并不重要。

下面这个例子,通过动态import()在单页应用中实现导航功能的模块懒加载:

<!DOCTYPE html>
<meta charset="utf-8">
<title>My library</title>
<nav>
  <a href="books.html" data-entry-module="books">Books</a>
  <a href="movies.html" data-entry-module="movies">Movies</a>
  <a href="video-games.html" data-entry-module="video-games">Video Games</a>
</nav>
<main>This is a placeholder for the content that will be loaded on-demand.</main>
<script>
  const main = document.querySelector('main');
  const links = document.querySelectorAll('nav > a');
  for (const link of links) {
    link.addEventListener('click', async (event) => {
      event.preventDefault();
      try {
        const module = await import(`/${link.dataset.entryModule}.mjs`);
        // The module exports a function named `loadPageInto`.
        module.loadPageInto(main);
      } catch (error) {
        main.textContent = error.message;
      }
    });
  }
</script>

如果想运行上面的例子,需要返回正确的MIME类型,在此译者贴上自己的代码。文件结构如下:

├── books.mjs
├── index.html
├── mime.js
├── movies.mjs
└── video-games.mjs

在终端运行node mime.js,然后访问localhost:3000

// mime.js
const http = require('http')
const url = require('url')
const fs = require('fs')
const path = require('path')

function getMimeType(res) {
  const EXT_MIME_TYPES = {
    default: 'text/html',
    '.mjs': 'text/javascript',
    '.js': 'text/javascript',
    '.css': 'text/css',
    '.json': 'text/json',
    '.jpeg': 'image/jpeg',
    '.jpg': 'image/jpg',
    '.png': 'image/png'
  }
  let mime_type = EXT_MIME_TYPES[path.extname(res)] || EXT_MIME_TYPES['default']
  return mime_type
}

const server = http.createServer((req, res) => {
  let srvUrl = url.parse(`http://${req.url}`)
  let path = srvUrl.path
  if (path === '/') path = '/index.html'

  let resPath = '.' + path

  if (!fs.existsSync(resPath)) {
    res.writeHead(404, {
      'Content-Type': 'text/html'
    })
    return res.end('<h1>404 Not Found</h1>')
  }

  let resStream = fs.createReadStream(resPath)
  res.writeHead(200, {
    'Content-Type': getMimeType(resPath)
  })
  resStream.pipe(res)
})

server.on('clientError', (err, socket) => {
  socket.end('HTTP/1.1 400 Bad Request\r\n\r\n')
})

server.listen(3000)

如果使用得当,动态import()启用的懒加载功能可以非常强大。Addy修改了一个Hacker News PWA的例子,它在第一次加载时静态地导入了所有的依赖关系,包括注释。更新的版本使用动态import()来懒加载注释,到用户确实需要它们之前避免加载,解析和编译代码。

建议

静态import和动态import()都非常有用,都有自己独特的用处。使用静态的import来初始绘制依赖关系,特别是对于上面的内容。 在其他情况下,请考虑使用动态的import()加载依赖关系。