xiaokeqi / i-learned

1 stars 0 forks source link

代码分割之动态import和webpack #47

Open xiaokeqi opened 4 years ago

xiaokeqi commented 4 years ago

代码分割之动态import和webpack

​ 无论在vue里,还是在react中,我们都知道通过动态import()可以按需加载组件、页面路由。那么,动态import是什么?用于哪里,怎么用,如何实现?跟着我一起揭开其面纱吧!

介绍

​ import()是一个“function-like”的动态模块引入,其现在处于TC39的提案中,且在4个月前,也就是2019年6月份,移到stage 4中,而chrome 61、edge16、ios10.3、firefox60、opera48、safari10.1版本以上就开始支持了,预计不久的将来会成为正式的语法标准。

​ 现有的语法形式是静态声明的,他们只接受字符串字面量,不能是变量,因为其是在编译阶段对模块进行静态分析、打包、tree shaking,而非运行时。这对于90%的设计来说是great!然而,动态加载js模块的场景也存在很多,如多个模块择优选择加载、静态模块加载失败后通过动态引入来增加程序健壮性等等。但是现有的语法不支持动态加载js模块。

​ 因此 Domenic Denicola 和module-loading社区产生了增加动态导入想法,并被tc39列入提案,提案中,建议增加import(specifier)语法形式,其用法类似于函数,却没有函数的某些特性。它返回一个Promise。

用法

<!DOCTYPE html>
<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>Content will load here!</main>

<script>
  const main = document.querySelector("main");
  for (const link of document.querySelectorAll("nav > a")) {
    link.addEventListener("click", e => {
      e.preventDefault();

      import(`./section-modules/${link.dataset.entryModule}.js`)
        .then(module => {
          module.loadPageInto(main);
        })
        .catch(err => {
          main.textContent = err.message;
        });
    });
  }
</script>

我们可以看到

再举个例子

a.js

console.log('from static import')
let number = 0;
export default function count() {
  number ++;
  return number
}

main.js

import count from './a.js'

console.log('start----')
console.log(count())
import('./a.js').then(_module => {
  console.log('a.js----')
  console.log(_module.default())
})

import('./a-dynamic.js').then(_module => {
  console.log('dy 1----')
  console.log(_module.default())
})
import('./a-dynamic.js').then(_module => {
  console.log('dy 2----')
  console.log(_module.default())
})

console.log('end')

a-dynamic.js

console.log('from dynatic import')
let number = 0
export default function count() {
  number ++;
  return number
}

运行 main.js,在node中输出结果如下:

from static import
start----
1
end
a.js----
2
from dynatic import
dy 2----
1
dy 1----
2

我们更改main.js为如下

import('./a.js').then(_module => {
  console.log('a.js----')
  console.log(_module.default())
})

import('./a-dynamic.js').then(_module => {
  console.log('dy 1----')
  console.log(_module.default())
})
import('./a-dynamic.js').then(_module => {
  console.log('dy 2----')
  console.log(_module.default())
})

console.log('end')

import count from './a.js'
console.log('start----')
console.log(count())

其输出结果如下:

from static import
end
start----
1
a.js----
2
from dynatic import
dy 2----
1
dy 1----
2

通过以上例子我们可以看到

从上所示,输出顺序,不如我们期望看到的那样,那通过babel转换后的呢?我们看下,通过babel转换后的代码,如下:

"use strict";

require("core-js/modules/web.dom.iterable");

require("core-js/modules/es6.array.iterator");

require("core-js/modules/es6.string.iterator");

require("core-js/modules/es6.weak-map");

require("core-js/modules/es6.promise");

require("core-js/modules/es6.object.to-string");

var _a = _interopRequireDefault(require("./a.js"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; }

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; if (obj != null) { var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }

Promise.resolve().then(function () {
  return _interopRequireWildcard(require('./a.js'));
}).then(function (_module) {
  console.log('a.js----');
  console.log(_module.default());
});
Promise.resolve().then(function () {
  return _interopRequireWildcard(require('./a-dynamic.js'));
}).then(function (_module) {
  console.log('dy 1----');
  console.log(_module.default());
});
Promise.resolve().then(function () {
  return _interopRequireWildcard(require('./a-dynamic.js'));
}).then(function (_module) {
  console.log('dy 2----');
  console.log(_module.default());
});
Promise.resolve().then(function () {
  return _interopRequireWildcard(require('./a-dynamic.js'));
}).then(function (_module) {
  console.log('dy 3----');
  console.log(_module.default());
});
console.log('end');
console.log('start----');
console.log((0, _a.default)());

babel将其转换成了Promise的写法,从上图我们可看出,编译后的代码,运行结果如下:

image

其执行顺序与转换前完全不一致。前者由于我们没有看node 中import()的实现,不知其具体原因,而后者,完全采用promise及通过weakMap 弱引用的方式设置缓存、防止内存消耗与泄露实现一次加载、多次执行。

好了,到这里,我们已经知道了,import()的用法,大致实现过程、以及import()用法上的注意点。接下来说下webpack如何通过import()实现代码分割

webpack import()代码分割

这期间,webpack充当什么角色呢?由于webpack实在babel对代码转换之前运行的,因此webpack会对代码进行解析,将调用import()之处作为分离的模块起点,并打包它和它引用的子模块分离到单独的chunk中。再通过babel转换器,进行转换。

完!

水平有限

如有理解错误之处,请指正~