В процессе использования webpack вам интересно, почему код, упакованный webpack, может работать прямо в браузере? Почему webpack может поддерживать различные последние версии синтаксиса ES6? Почему модули импорта ES6 могут быть написаны в веб-пакете и требуют, чтобы модули CommonJS также поддерживались?
Спецификация модуля
Что касается модулей, давайте сначала разберемся с текущими спецификациями основных модулей (поскольку существуют такие инструменты, как ES6 Module и Webpack, спецификация AMD/CMD имеет очень маленькое жизненное пространство):
- CommonJS
- UMD
- ES6 Module
CommonJS
До ES6 у js не было собственной спецификации модуля, поэтому сообщество разработало спецификацию CommonJS. Модульная система, используемая NodeJS, основана на спецификации CommonJS.
// CommonJS 导出
module.exports = { age: 1, a: 'hello', foo:function(){} }
// CommonJS 导入
const foo = require('./foo.js')
UMD
В соответствии с оценкой текущей рабочей среды, если это среда Node, следует использовать спецификацию CommonJS, если нет, определяется, является ли это средой AMD, и, наконец, экспортируются глобальные переменные. Таким образом, код может работать как в среде Node, так и в среде браузера. В настоящее время большинство библиотек упакованы в спецификации UMD. Webpack также поддерживает упаковку UMD.output.libraryTarget. Подробный пример см. в наборе инструментов npm, упакованном автором:cache-manage-js
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.libName = factory());
}(this, (function () { 'use strict';})));
ES6 Module
Идея дизайна модулей ES6 состоит в том, чтобы быть как можно более статичными, чтобы зависимости модулей, а также входные и выходные переменные можно было определить во время компиляции. Конкретные идеи и грамматику см. в другой статье автора:Детали модуля ES6
// es6模块 导出
export default { age: 1, a: 'hello', foo:function(){} }
// es6模块 导入
import foo from './foo'
Упаковка модуля Webpack
Поскольку существует так много спецификаций модулей, как Webpack анализирует разные модули?
Согласно входному файлу в webpack.config.js, webpack идентифицирует зависимости модуля в входном файле.Независимо от того, написаны ли здесь зависимости модуля в спецификации модуля CommonJS или ES6, webpack автоматически проанализирует, преобразует и скомпилирует код. в окончательный файл.最终文件中的模块实现是基于webpack自己实现的webpack_require(es5代码)
, чтобы упакованный файл можно было запустить в браузере.
В то же время вышеизложенное означает, что в среде webapck вы можете использовать только синтаксис модуля ES6 для написания кода (обычно мы так и делаем), вы также можете использовать синтаксис модуля CommonJS или даже их смесь. Потому что, начиная с webpack2, есть встроенная поддержка ES6, CommonJS, модульных операторов AMD,webpack会对各种模块进行语法分析,并做转换编译
.
Давайте возьмем пример для анализа упакованного файла с исходным кодом.Исходный код примера находится вwebpack-module-example
// webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
// JavaScript 执行入口文件
entry: './src/main.js',
output: {
// 把所有依赖的模块合并输出到一个 bundle.js 文件
filename: 'bundle.js',
// 输出文件都放到 dist 目录下
path: path.resolve(__dirname, './dist'),
}
};
// src/add
export default function(a, b) {
let { name } = { name: 'hello world,'} // 这里特意使用了ES6语法
return name + a + b
}
// src/main.js
import Add from './add'
console.log(Add, Add(1, 2))
Упрощенный файл bundle.js после упаковки выглядит следующим образом:
// modules是存放所有模块的数组,数组中每个元素存储{ 模块路径: 模块导出代码函数 }
(function(modules) {
// 模块缓存作用,已加载的模块可以不用再重新读取,提升性能
var installedModules = {};
// 关键函数,加载模块代码
// 形式有点像Node的CommonJS模块,但这里是可跑在浏览器上的es5代码
function __webpack_require__(moduleId) {
// 缓存检查,有则直接从缓存中取得
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 先创建一个空模块,塞入缓存中
var module = installedModules[moduleId] = {
i: moduleId,
l: false, // 标记是否已经加载
exports: {} // 初始模块为空
};
// 把要加载的模块内容,挂载到module.exports上
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.l = true; // 标记为已加载
// 返回加载的模块,调用方直接调用即可
return module.exports;
}
// __webpack_require__对象下的r函数
// 在module.exports上定义__esModule为true,表明是一个模块对象
__webpack_require__.r = function(exports) {
Object.defineProperty(exports, '__esModule', { value: true });
};
// 启动入口模块main.js
return __webpack_require__(__webpack_require__.s = "./src/main.js");
})
({
// add模块
"./src/add.js": (function(module, __webpack_exports__, __webpack_require__) {
// 在module.exports上定义__esModule为true
__webpack_require__.r(__webpack_exports__);
// 直接把add模块内容,赋给module.exports.default对象上
__webpack_exports__["default"] = (function(a, b) {
let { name } = { name: 'hello world,'}
return name + a + b
});
}),
// 入口模块
"./src/main.js": (function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__)
// 拿到add模块的定义
// _add__WEBPACK_IMPORTED_MODULE_0__ = module.exports,有点类似require
var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/add.js");
// add模块内容: _add__WEBPACK_IMPORTED_MODULE_0__["default"]
console.log(_add__WEBPACK_IMPORTED_MODULE_0__["default"], Object(_add__WEBPACK_IMPORTED_MODULE_0__["default"])(1, 2))
})
});
В приведенном выше основном коде упакованный код можно запустить непосредственно в браузере, поскольку webpack имитирует загрузку модуля с помощью функции __webpack_require__ (аналогично синтаксису require в узле) и монтирует определенное содержимое модуля в модуль. . В то же время кэш модуля также оптимизирован в функции __webpack_require__, чтобы предотвратить повторную загрузку модуля и оптимизировать производительность.
Давайте посмотрим на исходный код webpack:
// webpack/lib/MainTemplate.js
// 主文件模板
// webpack生成的最终文件叫chunk,chunk包含若干的逻辑模块,即为module
this.hooks.render.tap( "MainTemplate",
(bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) => {
const source = new ConcatSource();
source.add("/******/ (function(modules) { // webpackBootstrap\n");
// 入口内容,__webpack_require__就在bootstrapSource中
source.add(new PrefixSource("/******/", bootstrapSource));
source.add("/******/ })\n");
source.add(
"/************************************************************************/\n"
);
source.add("/******/ (");
source.add(
// 依赖的module都会写入对应数组
this.hooks.modules.call(
new RawSource(""),
chunk,
hash,
moduleTemplate,
dependencyTemplates
)
);
source.add(")");
return source;
}
Поддержка синтаксиса Webpack ES6
Внимательные читатели могут заметить, что приведенный выше код упакованного модуля добавления по-прежнему является синтаксисом ES6, который не поддерживается в младших браузерах. Это связано с тем, что нет соответствующего загрузчика для разбора кода js, webpack рассматривает все ресурсы как модули, а разные ресурсы используют разные загрузчики для конвертации.
Здесь вам нужно использовать babel-loader и его плагин @babel/preset-env для обработки, чтобы преобразовать код ES6 в код es5, который можно запустить в браузере.
// webpack.config.js
module.exports = {
...,
module: {
rules: [
{
// 对以js后缀的文件资源,用babel进行处理
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
};
// 经过babel处理es6语法后的代码
__webpack_exports__["default"] = (function (a, b) {
var _name = { name: 'hello world,' }, name = _name.name;
return name + a + b;
});
Модули Webpack загружаются асинхронно
Приведенный выше веб-пакет упаковывает все модули в основной файл, поэтому метод загрузки модулей является синхронным. Но в процессе разработки приложений загрузка по запросу (также называемая отложенной загрузкой) также является одним из часто используемых методов оптимизации. Загрузка по требованию, вообще говоря, означает, что код выполняется в асинхронный модуль (содержимое модуля находится в другом js-файле), и соответствующий код асинхронного модуля загружается сразу через сетевой запрос, а затем следующий процесс продолжается. Как webpack определяет, какие коды являются асинхронными модулями при выполнении кода? Как webpack загружает асинхронные модули?
в вебпаке естьrequire.ensureAPI для пометки модулей асинхронной загрузки, последний webpack4 рекомендует использовать новыйimport()api (требуется плагин @babel/plugin-syntax-dynamic-import). Поскольку require.ensure выполняет следующий процесс через функцию обратного вызова, а import() возвращает обещание, это означает, что можно использовать новейший синтаксис async/await ES8, позволяющий выполнять асинхронные процессы точно так же, как при написании синхронного кода.
Теперь давайте посмотрим, как webpack реализует асинхронную загрузку модулей из исходного кода, упакованного webpack. Измените входной файл main.js и добавьте асинхронный модуль async.js:
// main.js
import Add from './add'
console.log(Add, Add(1, 2), 123)
// 按需加载
// 方式1: require.ensure
// require.ensure([], function(require){
// var asyncModule = require('./async')
// console.log(asyncModule.default, 234)
// })
// 方式2: webpack4新的import语法
// 需要加@babel/plugin-syntax-dynamic-import插件
let asyncModuleWarp = async () => await import('./async')
console.log(asyncModuleWarp().default, 234)
// async.js
export default function() {
return 'hello, aysnc module'
}
Приведенная выше упаковка кода сгенерирует два файла фрагментов, один из которых является основным файлом.main.bundle.js
и файлы асинхронного модуля0.bundle.js
. Точно так же, для удобства быстрого понимания читателями, код основного процесса упрощен и сохранен.
// 0.bundle.js
// 异步模块
// window["webpackJsonp"]是连接多个chunk文件的桥梁
// window["webpackJsonp"].push = 主chunk文件.webpackJsonpCallback
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
[0], // 异步模块标识chunkId,可判断异步代码是否加载成功
// 跟同步模块一样,存放了{模块路径:模块内容}
{
"./src/async.js": (function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
__webpack_exports__["default"] = (function () {
return 'hello, aysnc module';
});
})
}
]);
Как известно из вышесказанного, в запакованном файле асинхронного модуляСохраняет исходный код асинхронного модуля., и чтобы различать разные асинхронные модули,Он также сохраняет идентификатор, соответствующий асинхронному модулю: chunkId.. Приведенный выше код активно вызывает функцию window["webpackJsonp"].push, которая является ключевой функцией для соединения асинхронного модуля и основного модуля. Эта функция фактически определена в основном файле.window["webpackJsonp"].push = webpackJsonpCallback
, чтобы получить подробный исходный код, давайте взглянем на упакованный код основного файла:
// main.bundle.js
(function(modules) {
// 获取到异步chunk代码后的回调函数
// 连接两个模块文件的关键函数
function webpackJsonpCallback(data) {
var chunkIds = data[0]; //data[0]存放了异步模块对应的chunkId
var moreModules = data[1]; // data[1]存放了异步模块代码
// 标记异步模块已加载成功
var moduleId, chunkId, i = 0, resolves = [];
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
// 把异步模块代码都存放到modules中
// 此时万事俱备,异步代码都已经同步加载到主模块中
for(moduleId in moreModules) {
modules[moduleId] = moreModules[moduleId];
}
// 重点:执行resolve() = installedChunks[chunkId][0]()返回promise
while(resolves.length) {
resolves.shift()();
}
};
// 记录哪些chunk已加载完成
var installedChunks = {
"main": 0
};
// __webpack_require__依然是同步读取模块代码作用
function __webpack_require__(moduleId) {
...
}
// 加载异步模块
__webpack_require__.e = function requireEnsure(chunkId) {
// 创建promise
// 把resolve保存到installedChunks[chunkId]中,等待代码加载好再执行resolve()以返回promise
var promise = new Promise(function(resolve, reject) {
installedChunks[chunkId] = [resolve, reject];
});
// 通过往head头部插入script标签异步加载到chunk代码
var script = document.createElement('script');
script.charset = 'utf-8';
script.timeout = 120;
script.src = __webpack_require__.p + "" + ({}[chunkId]||chunkId) + ".bundle.js"
var onScriptComplete = function (event) {
var chunk = installedChunks[chunkId];
};
script.onerror = script.onload = onScriptComplete;
document.head.appendChild(script);
return promise;
};
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
// 关键代码: window["webpackJsonp"].push = webpackJsonpCallback
jsonpArray.push = webpackJsonpCallback;
// 入口执行
return __webpack_require__(__webpack_require__.s = "./src/main.js");
})
({
"./src/add.js": (function(module, __webpack_exports__, __webpack_require__) {...}),
"./src/main.js": (function(module, exports, __webpack_require__) {
// 同步方式
var Add = __webpack_require__("./src/add.js").default;
console.log(Add, Add(1, 2), 123);
// 异步方式
var asyncModuleWarp =function () {
var _ref = _asyncToGenerator( regeneratorRuntime.mark(function _callee() {
return regeneratorRuntime.wrap(function _callee$(_context) {
// 执行到异步代码时,会去执行__webpack_require__.e方法
// __webpack_require__.e其返回promise,表示异步代码都已经加载到主模块了
// 接下来像同步一样,直接加载模块
return __webpack_require__.e(0)
.then(__webpack_require__.bind(null, "./src/async.js"))
}, _callee);
}));
return function asyncModuleWarp() {
return _ref.apply(this, arguments);
};
}();
console.log(asyncModuleWarp().default, 234)
})
});
Как видно из приведенного выше исходного кода,Асинхронная загрузка модулей с помощью webpack немного похожа на процесс jsonp.. В основном файле js путем построения тега скрипта в голове информация о модуле загружается асинхронно, затем функция обратного вызова webpackJsonpCallback используется для синхронизации исходного кода асинхронного модуля с основным файлом, поэтому последующая работа асинхронного модуля может быть таким же, как синхронный модуль. Конкретный процесс реализации исходного кода:
- При встрече с асинхронными модулями используйте
__webpack_require__.e
функция для загрузки в нее асинхронного кода. Эта функция будет динамически добавлять тег скрипта в заголовок html, а src указывает на файл, хранящийся в указанном асинхронном модуле. - Загруженный файл асинхронного модуля будет выполнен
webpackJsonpCallback
функция для загрузки асинхронного модуля в основной файл. - Таким образом, последующие действия можно использовать непосредственно как модуль синхронизации.
__webpack_require__("./src/async.js")
Загрузите асинхронные модули.
Обратите внимание, что использование primose в исходном коде очень тонкое, и асинхронный модуль будет разрешать() только после загрузки основного модуля.
Суммировать
- Реализация Webpack модулей ES/CommonJS основана на собственной реализации webpack_require, поэтому код может работать в браузере.
- Начиная с webpack2, была встроена поддержка модульных операторов ES6, CommonJS, AMD. Но за исключением нового синтаксиса ES6 для кода ES5, эта часть работы по-прежнему остается за Babel и его плагинами.
- В webpack можно использовать как модули ES6, так и модули CommonJS. Поскольку module.exports очень похож на export default, модули ES6 могут быть легко совместимы с CommonJS: импортируйте XXX из 'commonjs-module'. В свою очередь, CommonJS совместим с модулями ES6, и вам нужно добавить default: require('es-module').default.
- Процесс реализации модуля асинхронной загрузки webpack в основном такой же, как и у jsonp.