为什么要打包
因为要解决两个关键问题:
- 因为某些浏览器不支持: import和export关键字。
- 防止文件过多。
import b from './b.js'
const a = {
value: 'a',
getB: () => b.value + ' from a.js'
}
export default a
1、浏览器不支持 import和export关键字
虽然谷歌浏览器支持 <script type="module"></script> 写法,可以支持import和export关键字,但是IE8~15不支持type=module的写法。所以要将import和export关键字转化成所有浏览器能识别的语法:
- import 关键字变成 require 函数
- export 关键字变成 exports 对象
import和export关键字属于ES Module 模块,require函数和exports对象属于CommonJS模块。
它们之间的区别:
- ES Module 是 ES 标准里的模块定义语法,CommonJS2 则是 Node.js 社区的民间约定
- ES Module 使用 import 和 export 关键字,CommonJS2 则使用 require 函数和 exports 对象
- ES Modules 对模块采用了「动态只读引用」,而 CommonJS2 则是简单的浅复制
2、文件过多,需要打包成一个文件
虽然有的浏览器能支持type='module'写法,但是当我们真正在浏览器中开始使用一个项目的时候,整个项目可能会有成千上万个通过import、export关键字引用的文件,这会导致浏览器请求文件时间特别长。
所以我们需要将关键字转译为普通代码,并把所有文件打包成一个。
打包成什么样的文件?
肯定包含了所以模块,然后能执行所有模块。
打包过程
我们结合我另一篇文章的内容——《Webpack之AST、Babel、依赖》。将a.js 和 b.js打包。得出如下代码:
a.js 的变化:import 关键字不见了,变成了 require()。export 关键字不见了,变成了 exports['default']。
a.js 变成 ES5 之后的代码详解:
"use strict"; //严格模式
// 等于 exports[__esModule] = true。__esModule为true则表示为ES module,与之CommonJS区分开
Object.defineProperty(exports, "__esModule", {value: true});
//老JS技巧,清空值,将值变为undefined
exports["default"] = void 0;
var _b = _interopRequireDefault(require("./b.js"));
// _interopRequireDefault函数意图给模块添加'default'
// 如果有__esModule则输出obj,没有则加上default默认值
// 其他 _interop 开头的函数大多都是为了兼容旧代码
// function _interopRequireDefault(obj) {
// return obj && obj.__esModule ? obj : { "default": obj };
// }
var a = {
value: 'a',
getB: function getB() {
return _b["default"].value + ' from a.js';
}
};
// 等于 exports["default"] = a
var _default = a;
exports["default"] = _default;
打包后的文件
简易的打包器:
import { parse } from "@babel/parser"
import traverse from "@babel/traverse"
import { writeFileSync, readFileSync } from 'fs'
import { resolve, relative, dirname } from 'path';
import * as babel from '@babel/core'
// 设置根目录
const projectRoot = resolve(__dirname, 'project_1')
// 类型声明
type DepRelation = { key: string, deps: string[], code: string }[]
// 初始化一个空的 depRelation,用于收集依赖
const depRelation: DepRelation = [] // 数组!
// 将入口文件的绝对路径传入函数,如 D:\demo\fixture_1\index.js
collectCodeAndDeps(resolve(projectRoot, 'index.js'))
writeFileSync('dist_2.js', generateCode())
console.log('done')
function generateCode() {
// 在 code 字符串外面包一个 function(require, module, exports){ ... } *
// 把 code 写到文件里,引号不会出现在文件中
let code = ''
code += 'var depRelation = [' + depRelation.map(item => {
const { key, deps, code } = item
return `{
key: ${JSON.stringify(key)},
deps: ${JSON.stringify(deps)},
code: function(require, module, exports){
${code}
}
}`
}).join(',') + '];\n'
code += 'var modules = {};\n'
code += `execute(depRelation[0].key)\n`
code += `
function execute(key) {
if (modules[key]) { return modules[key] }
var item = depRelation.find(i => i.key === key)
if (!item) { throw new Error(\`\${item} is not found\`) }
var pathToKey = (path) => {
var dirname = key.substring(0, key.lastIndexOf('/') + 1)
var projectPath = (dirname + path).replace(\/\\.\\\/\/g, '').replace(\/\\\/\\\/\/, '/')
return projectPath
}
var require = (path) => {
return execute(pathToKey(path))
}
modules[key] = { __esModule: true }
var module = { exports: modules[key] }
item.code(require, module, module.exports)
return modules[key]
}
`
return code
}
function collectCodeAndDeps(filepath: string) {
const key = getProjectPath(filepath) // 文件的项目路径,如 index.js
if (depRelation.find(i => i.key === key)) {
// 注意,重复依赖不一定是循环依赖
return
}
// 获取文件内容,将内容放至 depRelation
const code = readFileSync(filepath).toString()
const { code: es5Code } = babel.transform(code, {
presets: ['@babel/preset-env']
})
// 初始化 depRelation[key]
// depRelation[key] = { deps: [], code: es5Code }
// 改为了
// const item = { key, deps: [], code: es5Code }
// depRelation.push(item)
const item = { key, deps: [], code: es5Code }
depRelation.push(item)
// 将代码转为 AST
const ast = parse(code, { sourceType: 'module' })
// 分析文件依赖,将内容放至 depRelation
traverse(ast, {
enter: path => {
if (path.node.type === 'ImportDeclaration') {
// path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径
const depAbsolutePath = resolve(dirname(filepath), path.node.source.value)
// 然后转为项目路径
const depProjectPath = getProjectPath(depAbsolutePath)
// 把依赖写进 depRelation
item.deps.push(depProjectPath)
collectCodeAndDeps(depAbsolutePath)
}
}
})
}
// 获取文件相对于根目录的相对路径
function getProjectPath(path: string) {
return relative(projectRoot, path).replace(/\\/g, '/')
}
简化版打包后的文件:
// dist_2.js
var depRelation = [
{key: 'index.js', deps: ['a.js', 'b.js'], code: function(require, module, exports) {...} },
{key: 'a.js', deps: ['b.js'], code: function(require, module, exports) {...} },
{key: 'b.js', deps: ['a.js'], code: function(require, module, exports) {...} }
]
execute(depRelation[0].key)
var modules = {}
// 执行入口文件,把depRelation从对象改为数组。因为数组的第一项就是入口,而对象没有第一项的概念。
execute(depRelation[0].key)
function execute(key) {
// 如果已经 require 过,就直接返回上次的结果
if (modules[key]) { return modules[key] }
// 找到要执行的项目
var item = depRelation.find(i => i.key === key)
// 找不到就报错,中断执行
if (!item) { throw new Error(`${item} is not found`) }
// 把相对路径变成项目路径
var pathToKey = (path) => {
var dirname = key.substring(0, key.lastIndexOf('/') + 1)
var projectPath = (dirname + path).replace(/\.\//g, '').replace(/\/\//, '/')
return projectPath
}
// 创建 require 函数
var require = (path) => {
return execute(pathToKey(path))
}
// 初始化当前模块
modules[key] = { __esModule: true }
// 初始化 module 方便 code 往 module.exports 上添加属性
var module = { exports: modules[key] }
// 调用 code 函数,往 module.exports 上添加导出属性
// 第二个参数 module 大部分时候是无用的,主要用于兼容旧代码
item.code(require, module, module.exports)
// 返回当前模块
return modules[key]
}
最终运行dist_2.js打印出来的的文件与转译前的内容一致:
总结
- webpack核心原理就是转译和打包。
- 把 code 由字符串改为函数。
- depRelation 从对象改为数组,因为数组的第一项就是入口,而对象没有第一项的概念。
- 一些转译后的源码无需花太多精力去理解,因为并没有多大实际意义,如:item.code(require, module, module.exports) 中的module,或者是var _default = a; exports["default"] = _default。