Skip to content
On this page

Webpack 部分原理

浏览器怎样才能运行 import / export ?

不同浏览器功能不同:现代浏览器可以通过 <script type=module> 来支持 import export ;IE 8~15 不支持 import export,所以不可能运行。 兼容策略:把代码全放在 <script type=module> 里(比较激进),但是不被 IE 8~15,而且会导致文件请求过多;把关键字转移为普通代码,并把所有文件打包成一个文件(比较平稳),但是需要很复杂的代码去完成这件事。

编译 import / export

使用 @babel/core 、@babel/preset-env 编译成 ES5 代码。 import 关键字变成 require(),export 关键字变成 export['default']

js
// a.js
import b from './b.js'
const a = {
  value: 'a',
  getB: () => b.value + ' from a.js'
}
export default a

// b.js
import a from './a.js'
const b = {
  value: 'b',
  getA: () => a.value + ' from b.js'
}
export default b

// index.js
import a from './a.js'
import b from './b.js'
console.log(a.getB())
console.log(b.getA())

// a.js 变成 ES5 代码
"use strict";
Object.defineProperty(exports, "__esModule", {value: true}); // 疑惑 1
exports["default"] = void 0; // 疑惑 2
var _b = _interopRequireDefault(require("./b.js")); // 细节 1
function _interopRequireDefault(obj) { // 细节 1
  return obj && obj.__esModule?obj: { "default": obj }; //细节 1
}
var a = {
  value: 'a',  
  getB: function getB() {
    return _b["default"].value + ' from a.js'; // 细节 1
  }
};
var _default = a; // 细节 2
exports["default"] = _default; // 细节 2

解答疑惑和细节: Object.defineProperty(exports, "__esModule", {value: true}); 给当前模块添加 __esModule: true 属性,方便跟 CommonJS 模块区分开;那为什么不直接用 exports.__esModule = true 非要装个逼? 我看了下源码,发现是可以通过选项来切换的,所以两种区别不大,上面写法功能更强,exports.__esModule兼容性更好。

exports["default"] = void 0;void 0 等价于 undefined,老 JSer 的常见过时技巧;这句话是为了强制清空 exports['default'] 的值;为什么要清空?可能有些特殊情况的使用。

_interopRequireDefault(module) : _ 下划线前缀是为了避免与其他变量重名; 该函数的意图是给模块添加 'default'; 为什么要加 default:CommonJS 模块没有默认导出,加上方便兼容; 内部实现:return m && m.__esModule ? m : { "default": m } 其他 _interop 开头的函数大多都是为了兼容旧代码

转译本质就是把 ESModule 语法变成了 CommonJS 规则,并把所有文件打包成一个文件。

打包成一个什么样的文件

js
var depRelation = [ 
  {key: 'index.js', deps: ['a.js', 'b.js'], code: function... },
  {key: 'a.js', deps: ['b.js'], code: function... },
  {key: 'b.js', deps: ['a.js'], code: function... }
] // 为什么把 depRelation 从对象改为数组?
// 因为数组的第一项就是入口,而对象没有第一项的概念
execute(depRelation[0].key) // 执行入口文件
function execute(key){
  var item = depRelation.find(i => i.key === key)
  item.code(???) // 执行 item 的代码,因此 code 最好是个函数,方便执行
  // 但是目前还不知道要传什么参数给 code 
  // ...
}

有三个问题: depRelation 原来是对象,变成一个数组; code 原来是字符串,变成一个函数; execute 函数完善。 1、 把 code 由字符串改为函数

js
code = `
  var b = require('./b.js')
  exports.default = 'a'
`

code2 = `function(require, module, exports){
	${code}
}`  // 注意此时 code2 还是字符串

// 然后把 `{code: ${code2}}` 写入最终文件里,最终文件里的 code 就是函数了

注:require, module, exports 这三个参数是 CommonJS 2 规范规定的。

2、完善 execute 函数(主体思路)

js
const modules = {} // modules 用于缓存所有模块
function execute(key) { 
  if (modules[key]) { return modules[key] }
  var item = depRelation.find(i => i.key === key)
  
  var require = (path) => {
    return execute(pathToKey(path))
  }
  
  modules[key] = { __esModule: true } // modules['a.js']
  var module = { exports: modules[key] }
  item.code(require, module, module.exports) // 细节 1
  
  return modules.exports // 细节 2
}

细节 1 其实就是 item.code(require, module, modules[key]) ,code 方法执行把一些数据挂载到 modules[key] (即 modules.exports )上,所以细节 2 很容易理解了。

最终打包生成的文件内容:

js
var depRelation = [ 
  {key: 'index.js', deps: ['a.js', 'b.js'], code: function... },
  {key: 'a.js', deps: ['b.js'], code: function... },
  {key: 'b.js', deps: ['a.js'], code: function... }
] 
var modules = {} // modules 用于缓存所有模块
execute(depRelation[0].key)
function execute(key){
  var require = ...
  var module = ...
  item.code(require, module, module.exports)
  ...
}
// 详见 dist.js

loader 的本质

loader 其实就是个函数,可以是普通函数,也可以是异步函数。

js
function transform(code){
  const code2 = doSomething(code)
  return code2
}
module.exports = transform // 用 module 是为了兼容 Node.js

async function transform(code){
  const code2 = await doSomething(code)
  return code2
}
module.exports = transform // 旧版本 Node.js 不支持 export 关键字

// css loader 核心代码
let code = readFileSync(filePath).toString()
if(/.\css$/.test(filePath)) {
	code = require('./css-loader.js')(code)
	// 为什么不用 import 
	// 为了方便动态加载
}

问题:看过哪些 loader 源码? 1、style-loader 原理: style-loader 在 pitch 钩子里通过 css-loader 来 require 文件内容,然后在文件内容后面添加 injectStylesIntoStyleTag(content, ...) 代码。

2、raw-loader :用来加载任意文件,不做任何处理。

js
// https://github.com/webpack-contrib/raw-loader/blob/master/src/index.js

import { getOptions } from 'loader-utils';
import { validate } from 'schema-utils';
import schema from './options.json';
export default function rawLoader(source) {
	const options = getOptions(this);
	validate(schema, options, {
		name: 'Raw Loader',
		baseDataPath: 'options',
	});
	
	const json = JSON.stringify(source)
		.replace(/\u2028/g, '\\u2028')
		.replace(/\u2029/g, '\\u2029');
	const esModule = typeof options.esModule !== 'undefined' 
						? options.esModule : true;
	return `${esModule ? 'export default' : 'module.exports ='} ${json};`;
}

Webpack 提供 loader-utils 和 schema-utils 作为辅助工具; Webpack 通过 this 来传递上下文; getOptions(this) 可以获取 options ; validate 可以验证 options 是否合法。 JSON 的 2028 和 2029 问题

3、css-loader

问题: Webpack 中的 loader 是什么? webpack 自带的打包器只能支持 JS 文件;当我们想要加载 css/less/scss/stylus/ts/md 文件时,就需要用 loader;loader 的原理就是把文件内容包装成能运行的 JS,比如加载 css 需要用到 style-loader 和 css-loader 。 css-loader 把代码从 CSS 代码变成 export default str 形式的 JS 代码 style-loader 把代码挂载到 head 里的 style 标签里。 这里可以深入讲一下 style-loader 用到了 pitch 钩子和 request 对象。

另外一种回答:写过一个简单的 loader 放在 GitHub 上,可以看一下, 如果问原理,就把代码思路说一遍,然后说说自己的 loader 和 webpack 推荐的 loader 区别在哪里。

Webpack 执行流程

doBuild -> runLoaders -> processResult -> createSource -> _ast -> parser -> parse -> acron

1、Webpack 是如何知道 index.js 依赖了哪些文件? Webpack 会对 index.js 进行 parse 得到 ast ; 接下来 traverse 这个 ast , 寻找 import 语句; 在源码 JavaScriptParser.js 的 3231 行得到 ast ,3260~3264 行 traverse 了 ast ; 其中 blockPreWalkStatement() 对 ImportDeclaration 进行了检查; 一旦发现 import 'xxx',就会触发 import 钩子,对应的监听函数会处理依赖; 其中 walkStatemants() 对 ImportExpression 进行了检查; 一旦发现 import('xxx'),就会触发 importCall 钩子,对应的监听也会。

2、怎么把 modules 合并成一个文件? 看 compilation.seal(),该函数会创建 chunks、 为每个 chunk 进行 codeGeneration,然后为每个 chunk 创建 asset ; seal() 之后,emitAssets()、emitFiles() 会创建文件(emit 就是射); 最终得到 dist/main.js 和其他 chunk 文件。

总结一下:Webpack 怎么分析依赖和打包的? 使用 JavascriptParser 对 index.js 进行 parse 得到 ast,然后遍历 ast; 发现依赖声明就将其添加到 module 的 dependencies 或 blocks 中; seal 阶段,webpack 将 module 转为 chunk,可能会把多个 module 通过 codeGeneration 合并为一个 chunk; seal 之后,为每个 chunk 创建文件,并写到硬盘上。

Webpack 的 plugin 原理

等待填充...

loader 和 plugin 区别

执行阶段不一样:loader 是在 make 阶段执行,plugin 在任何阶段都可以。 loader 是一个转换器,将A文件进行编译成B文件,比如:将 A.less 转换为 A.css ,单纯的文件转换过程。 plugin 是一个扩展器,它丰富了 Webpack 本身,针对是 loader 结束后,Webpack 打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听 Webpack 打包过程中的某些节点,执行广泛的任务。

官网原话:相对于loader转换指定类型的模块功能,plugins能够被用于执行更广泛的任务比如打包优化、文件管理、环境注入等……

Released under the MIT License.