在使用 webpack 的过程中, 你是否好奇 webpack 打包的代码为什么可以直接在浏览器中跑? 为什么 webpack 可以支持各种 ES6 最新语法? 为什么在 webpack 中可以书写 import ES6 模块, 也支持 require CommonJS 模块?
模块规范
关于模块, 我们先来认识下目前主流的模块规范 (自从有了 ES6 Module 及 Webpack 等工具, AMD/CMD 规范生存空间已经很小了):
- CommonJS
- UMD
- ES6 Module
- CommonJS
ES6 前, JS 没有属于自己的模块规范, 所以社区制定了 CommonJS 规范. 而 Node.JS 所使用的模块系统就是基于 CommonJS 规范实现的.
- // CommonJS 导出
- module.exports = {
- age: 1, a: 'hello', foo:function(){
- }
- }
- // CommonJS 导入
- const foo = require('./foo.js')
UMD
根据当前运行环境的判断, 如果是 Node 环境 就是使用 CommonJS 规范, 如果不是就判断是否为 AMD 环境, 最后导出全局变量. 这样代码可以同时运行在 Node 和浏览器环境中. 目前大部分库都是打包成 UMD 规范, Webpack 也支持 UMD 打包, 配置 API 是 output.libraryTarget. 详细案例可以看笔者封装的 NPM 工具包: https://github.com/lq782655835/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 根据 webpack.config.JS 中的入口文件, 在入口文件里识别模块依赖, 不管这里的模块依赖是用 CommonJS 写的, 还是 ES6 Module 规范写的, webpack 会自动进行分析, 并通过转换, 编译代码, 打包成最终的文件. 最终文件中的模块实现是基于 webpack 自己实现的 webpack_require(es5 代码), 所以打包后的文件可以跑在浏览器上.
同时以上意味着在 webapck 环境下, 你可以只使用 ES6 模块语法书写代码 (通常我们都是这么做的), 也可以使用 CommonJS 模块语法, 甚至可以两者混合使用. 因为从 webpack2 开始, 内置了对 ES6,CommonJS,AMD 模块化语句的支持, webpack 会对各种模块进行语法分析, 并做转换编译.
我们举个例子来分析下打包后的源码文件, 例子源代码在
- // 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__ 函数模拟了模块的加载 (类似于 node 中的 require 语法), 把定义的模块内容挂载到 module.exports 上. 同时__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 语法支持
可能细心的读者看到, 以上打包后的 add 模块代码中依然还是 ES6 语法, 在低端的浏览器中不支持. 这是因为没有对应的 loader 去解析 JS 代码, webpack 把所有的资源都视作模块, 不同的资源使用不同的 loader 进行转换.
这里需要使用 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 对于 ES 模块 / CommonJS 模块的实现, 是基于自己实现的 webpack_require, 所以代码能跑在浏览器中.
从 webpack2 开始, 已经内置了对 ES6,CommonJS,AMD 模块化语句的支持. 但不包括新的 ES6 语法转为 ES5 代码, 这部分工作还是留给了 babel 及其插件.
在 webpack 中可以同时使用 ES6 模块和 CommonJS 模块. 因为 module.exports 很像 export default, 所以 ES6 模块可以很方便兼容 CommonJS:import XXX from 'commonjs-module'. 反过来 CommonJS 兼容 ES6 模块, 需要额外加上 default:require('es-module').default.
参考文章
前端模块化: CommonJS,AMD,CMD,ES6
深入 CommonJs 与 ES6 Module https://segmentfault.com/a/1190000017878394
Webpack 将代码打包成什么样子 https://github.com/Pines-Cheng/blog/issues/45
Webpack 源码分析 https://zhuanlan.zhihu.com/p/29551683
来源: https://juejin.im/post/5c94a2f36fb9a070fc623df4