Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

简单了解 webpack 打包原理 #36

Open
liangbus opened this issue Apr 6, 2020 · 0 comments
Open

简单了解 webpack 打包原理 #36

liangbus opened this issue Apr 6, 2020 · 0 comments

Comments

@liangbus
Copy link
Owner

liangbus commented Apr 6, 2020

每次提起 webpack,都会被它一大堆配置搞得头都大了,本身就不算很熟,如果面试被问起来,就更难了。

相信 webpack 大家都有使用过,但是多少人知道它的原理呢?有没有去看过它打包后的代码是怎么样的?本文讨论的是 webpack 4,也会忽略掉 webpack 的一些基本的配置(当作你是会用的啦~),这次就通过打包一些简单的文件,看看 webpack 的打包过程是怎么样的。

下面是我们当前的目录结构(tree 生成)

├── dist
├── package.json
├── src
│   ├── index.js
├── webpack.config.js
└── yarn.lock

我在 src 新建一个叫 index.js 的文件,并且将其作为 webpack 的打包入口,index.js 我只写了一行代码,主要是看

// webpack.config.js
module.exports = {
  // mode: 'production', // 默认是 production
  mode: 'development', // 这里要注意改成 development,否则打包出来的是经压缩后的代码
  entry: {
    bundle: './src/index.js'
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: '[name].js'
  }
}
// index.js

console.log('hahaha!!!')

此时,我们来 webpack 一下,然后我们的 dist 就会有我们的 bundle.js 文件了( bundle 这个名字是来自 entry 的 key,因为我们写了 [name] )

大概会有 100 行代码,这里我省去一些暂时不需要了解的代码(一些对 webpack_require 函数拓展的方法),把一些关键的代码贴出来

(function(modules) { // webpackBootstrap
	// The module cache
	var installedModules = {};

	// The require function
	function __webpack_require__(moduleId) {

		// Check if module is in cache
		if(installedModules[moduleId]) {
			return installedModules[moduleId].exports;
		}
		// Create a new module (and put it into the cache)
		var module = installedModules[moduleId] = {
			i: moduleId,
			l: false,
			exports: {}
		};

		// Execute the module function
		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

		// Flag the module as loaded
		module.l = true;

		// Return the exports of the module
		return module.exports;
	}

	// Load entry module and return exports
	return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
({

/***/ "./src/index.js":
/*!**********************!*\
 !*** ./src/index.js ***!
 \**********************/
/*! no static exports found */
/***/ (function(module, exports) {

eval("console.log('hahaha!!!')\n\n\n//# sourceURL=webpack:///./src/index.js?");
 })

 });

删完之后,大概就剩下40多行了。这时看就很方便了

先看整体,整个 js 文件是一个自执行函数,Like this

((options) => { /**do something **/ } )({ foo: 'foo' })

再来看下入参,入参是一个对象,里面的 key 是文件路径,就是我们在 webpack.config.js 里 entry 配置的文件入口,上面我只配置了一个 index.js 的入口,所以这里入参对象只有一项,而对应的 value 是一个函数对象,函数体是执行一个 eval 函数,然后其执行的内容,就是这个文件的内容!

然后再来看下自执行函数里面是做了什么

一个对象,表面上去像是缓存已经加载过的 module(事实也是如此)

// The module cache
 var installedModules = {};

紧接着是,可以说 webpack 最关键的一个的一个函数 webpack_require ,它正是我们平常 require 文件的关键

// The require function
 function __webpack_require__(moduleId) {

 	// Check if module is in cache
 	if(installedModules[moduleId]) {
 		return installedModules[moduleId].exports;
 	}
 	// Create a new module (and put it into the cache)
 	var module = installedModules[moduleId] = {
 		i: moduleId,
 		l: false,
 		exports: {}
 	};
 	// Execute the module function
 	modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

 	// Flag the module as loaded
 	module.l = true;

 	// Return the exports of the module
 	return module.exports;
 }

函数体做的事情不复杂,首先是检查之前外部声明的 installedModules 对象(没错吧,就是用来缓存模块的,一个文件一个模块,也就是一个 k-v 对),假如存在,直接读取其 export 出来的值。

否则在 installedModules 以其当前模块的 id (也就是文件的路径)创建一个值。

接着,会将该文件 export 出的内容通过 call 方法进行调用,调用完毕设置其标志位,并将其 export 值返回,假如是当前文件中有 require, webpack_require 将会递归地调用,Like this,index.js 及自执行函数参数部分如下

var search = require('./search.js')
console.log('hahaha!!!')
console.log(search)
// bundle.js
({
 "./src/index.js":
 (function(module, exports, __webpack_require__) {

eval("\nvar search = __webpack_require__(/*! ./search.js */ \"./src/search.js\")\n\n\nconsole.log('hahaha!!!')\n\nconsole.log(search)\n\n//# sourceURL=webpack:///./src/index.js?");

 }),

 "./src/search.js":
 (function(module, exports) {
eval("module.exports = {\n  moduleName: 'searchModule',\n  foo: function() {\n    console.log('I am foo!!!')\n  }\n}\n\n//# sourceURL=webpack:///./src/search.js?");
 })

require 被编译成 webpack_require 方法,同时匿名函数多了个参数,See? 我们的文件就是这样递归地进行调用,然后被打包成一个文件。

除了 require 方法,对于 import,webpack 又是怎么打包的呢?
我来新建一个 util 文件,并引用其中的一个方法

import { isObject } from './utils/util'

var arr = [1,2,3,4]
console.log('isObject -> ', isObject(arr))

这时我们再打包一下,会发现我们原本 "./src/index.js" 这个模块的 eval 内容有点不一样了,最顶处多了这些代码

__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _utils_util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils/util */ "./src/utils/util.js");

这里使用了 __webpack_require__.r

        // define __esModule on exports
 	__webpack_require__.r = function(exports) {
 		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
 			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
 		}
 		Object.defineProperty(exports, '__esModule', { value: true });
 	};

检查当前环境是否支持 Symbol,若支持则将其[[Class]]值设为 Module(这个值可以通过Object.prototype.toString 方法获得),然后设置一个 __esModule 属性,值为 true.

这个函数看起来就是给 export 增加一个标记

再来看下我们的 utils 文件打包后的样子

__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isObject", function() { return isObject; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "generateRandomArray", function() { return generateRandomArray; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "shuffleArray", function() { return shuffleArray; });
// 检查对象类型
function isObject(target) {
    const t = typeof target
  return target !== null && (t === 'object' || t === 'function')
}

// 随机不重复数组(排序后是连续的)
function generateRandomArray (len){
  let i = 0;
  const arr = []
  while(i++ < len) {
    arr.push(i)
  }
  return arr.sort(() => 0.5 - Math.random())
}

/**
 * 打乱数组顺序
  * @param arr
  */
function shuffleArray(arr) {
    let i = arr.length;
    while(i) {
      let r = Math.floor(Math.random() * i);
      [arr[i - 1], arr[r]] = [arr[r], arr[i - 1]]
      i--
  }
  return arr
}

//# sourceURL=webpack:///./src/utils/util.js?

这里主要是用到了 __webpack_require__.d 这个函数

         // define getter function for harmony exports
 	__webpack_require__.d = function(exports, name, getter) {
 		if(!__webpack_require__.o(exports, name)) {
 			Object.defineProperty(exports, name, { enumerable: true, get: getter });
 		}
 	};

可以看到,这里主要是将 utils 定义的方法定义到 exports 里面,getter 就是返回相关函数的引用,然后引用的页面就读到这些 export 出来的引用了,这个 export 就是开始定义的

// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
 	i: moduleId,
 	l: false,
 	exports: {}
 };

大致的流程就是这样子,是不是还挺容易理解的?

延伸

有时候我们也可以在 JS 引入 css,由于 webpack 只认识 js 文件,如果需要打包 css 文件,那就需要额外加入能让webpack 认识的 loader,css-loader(如果是 sass, less 那些,还需要特定的 loader),尝试一下

({

 "./node_modules/css-loader/dist/runtime/api.js":
 (function(module, exports, __webpack_require__) { /****/}),

 "./src/css/index.css":
 (function(module, exports, __webpack_require__) {

eval("// Imports\nvar ___CSS_LOADER_API_IMPORT___ = __webpack_require__(/*! ../../node_modules/css-loader/dist/runtime/api.js */ \"./node_modules/css-loader/dist/runtime/api.js\");\nexports = ___CSS_LOADER_API_IMPORT___(false);\n// Module\nexports.push([module.i, \".box { width: 100px; height: 100px; background-color: aqua;}\", \"\"]);\n// Exports\nmodule.exports = exports;\n\n\n//# sourceURL=webpack:///./src/css/index.css?");
 }),

 "./src/index.js":
 (function(module, exports, __webpack_require__) {
eval("\nvar search = __webpack_require__(/*! ./search.js */ \"./src/search.js\")\n__webpack_require__(/*! ./css/index.css */ \"./src/css/index.css\")\n\nconsole.log('hahaha!!!')\n\nlet a = 'a'\nlet b = 'b'\nlet c = 'c'\n\nconsole.log(a + b + c)\n\nlet n1 = 2\nlet n2 = 4\nlet n3 = 6\n\nconsole.log(n1+n2+n3)\n\n//# sourceURL=webpack:///./src/index.js?");
 }),

 "./src/search.js":
 (function(module, exports) {
eval("module.exports = {\n  moduleName: 'searchModule',\n  foo: function() {\n    console.log('I am foo!!!')\n  }\n}\n\n//# sourceURL=webpack:///./src/search.js?");
 })

可以看到,入参多了两个 k-v 对,其中一个是我们之前理解的以我们的文件路径为 key 的值,另外一个则是一个 api

./node_modules/css-loader/dist/runtime/api.js

看到 css 文件执行内容如下

// Imports
var ___CSS_LOADER_API_IMPORT___ = __webpack_require__(
  /*! ../../node_modules/css-loader/dist/runtime/api.js */
  "./node_modules/css-loader/dist/runtime/api.js"
  );
exports = ___CSS_LOADER_API_IMPORT___(false);
// Module
exports.push([module.i, ".box { width: 100px; height: 100px; background-color: aqua;}", ""]);
// Exports
module.exports = exports;
//# sourceURL=webpack:///./src/css/index.css?

通过上面可以看到,我们会先通过加载一个 loader ,然后再通过 loader 去获取我们的 css 内容,具体实现可以查看 css-loader/dist/runtime/api.js 的源码,这里就不再展开

要注意的是,这里 css-loader 只是获取到了样式内容,并没有在文档上应用里面的样式,如果需要把样式应用到文档中,可以使用 style-loader,这个 loader 会把样式内容以 style 标签插入到 head 标签末尾,下面是 style-loader 的一些关键代码

// node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js
// 避免贴过多代码,以下有删减
function insertStyleElement(options) {
  var style = document.createElement('style');

  if (typeof options.insert === 'function') {
    options.insert(style);
  } else {
    var target = getTarget(options.insert || 'head');

    if (!target) {
      throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.");
    }

    target.appendChild(style);
  }

  return style;
}
function applyToTag(style, options, obj) {
  var css = obj.css;
  var media = obj.media;

  /* istanbul ignore if  */

  if (style.styleSheet) {
    style.styleSheet.cssText = css;
  } else {
    while (style.firstChild) {
      style.removeChild(style.firstChild);
    }

    style.appendChild(document.createTextNode(css));
  }
}

webpack 中,loader 的加载顺序是从右到左

所以正确使用 style-loader 和 css-loader 的姿势是

{
    test: /\.css$/,
    use: [
       'style-loader',
       'css-loader'
    ]
}

这里会先使用 css-loader 处理,然后将处理完的结果递交给 style-loader 处理,以此类推

优化

webpack 有一些自带的优化项,比如

// src/index.js
var a = 1
var b = 2
var c = 3

console.log(a+b+c)

打包之后,生成的文件如下(我们的代码部分)

console.log(6)

在编译阶段 webpack 会自动把一些常量计算结果,以结果的形式输出

DllPlugin 和 DllReferencePlugin

这两个是 webpack 提供的插件,可以用来拆分业务代码,提取一些外部库,使其不会打包进我们的业务代码中,利用 CDN 缓存起来,同时也降低业务代码包的体积,加快构建速度
使用:

需新建一个 webpack 配置文件,专门用于生成抽取出来的独立文件,以及映射表 manifest.json

// webpack.dll.config.js
const path = require('path');
const webpack = require('webpack')

module.exports = {
  mode: 'production',
  // mode: 'development',
  entry: {
    react: ['react', 'react-dom']
  },
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: '[name].dll.js',
    libraryTarget: 'var', // (默认值)当 library 加载完成,入口起点的返回值将分配给一个变量:
    library: '_dll_[name]_[hash]' // libraryTarget 输出后赋值的变量名
  },
  plugins:[
    new webpack.DllPlugin({
      path: path.join(__dirname, '../dist/dll', '[name].manifest.json'),
      name: '_dll_[name]_[hash]',
      context: __dirname // 必填 且与 webpack.config.js 中的 DllReferencePlugin context 保持一致
    })
  ]
}
// webpack.config.js 主配置文件中(关键代码)
new webpack.DllReferencePlugin({
      context: __dirname,
      manifest: require(
        path.join(__dirname, '../dist/dll/react.manifest.json')
      )
    }),

以我自己个人的项目来说,优化前 app.js 为 153kb,使用 dll 技术后 app.js 仅剩 23kb

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant