八股文不用背-webpack打包的原理
浏览器未支持模块化时
背景
浏览器解析html时,是不识别import关键字的,也就是说浏览器是不支持模块引入的,而我们开发项目需要模块化开发,怎么办呢?
比如我们现在创建了index.js、add.js、minus.js三个文件 文件目录是:
// index.js
import add from "./add.js"
import minus from './minus.js'
const sum = add(1, 2)
const division = minus(2, 1)
console.log(sum)
console.log(division)
// add.js
export default (a, b) => {
return a + b
}
// minus.js
export default (a, b) => {
return a - b
}
当我在html中引入index.js,然你在浏览器打开html后,控制台会报一下错误:
这是浏览器不识别import关键词
如果我能把这三个文件合并到一个文件bundle.js
,那么就完全不需要import啦~~~ 。开干!
- 如果
bundle.js
中有一个depsGraph(就是一个map),存着这三个文件的代码,我们就能通过eval()执行对应文件的代码。 - 但是
import关键词
我们没什么办法,这是es6的的语法,我们先将它转换成es5的语法,import add from './add.js'
=>require(\"./add.js\")
,当我们eval(index.js的代码),浏览器无法识别require函数,那也简单,我们自己写一个require函数,入参是文件名,出参是代码执行完的exports对象 - 同样,
exports关键词
,我们也没什么办法,我们将其转化成es5语法,exports default
=>exports[\"default\"]
,那感情好啊,我直接定义一个对象exports
第一步:构造depsGraph
这是我们想要的depsGraph
{
// 文件的绝对路径
"./src/index.js": {
// 该文件代码里import的所有依赖文件的绝对路径
"deps": {
"./add.js": "./src\\add.js",
"./minus.js": "./src\\minus.js"
},
// 该文件的代码
"code": "\"use strict\";\n\nvar _add = _interopRequireDefault(require(\"./add.js\"));\n\nvar _minus = _interopRequireDefault(require(\"./minus.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nvar sum = (0, _add[\"default\"])(1, 2);\nvar division = (0, _minus[\"default\"])(2, 1);\nconsole.log(sum);\nconsole.log(division);"
},
"./src\\add.js": {
"deps": {},
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _default = function _default(a, b) {\n return a + b;\n};\n\nexports[\"default\"] = _default;"
},
"./src\\minus.js": {
"deps": {},
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _default = function _default(a, b) {\n return a - b;\n};\n\nexports[\"default\"] = _default;"
}
}
为了得到上图的depsGraph,我们需要定义一个函数getModuleInfo
,入参是入口文件(index.js)的绝对路径,功能是将入口文件及其所有依赖文件
和代码
做对应关系
// 文件读取模块
const fs = require('fs')
// 文件路径模块
const path = require('path')
// babel的parser插件
const parser = require('@babel/parser')
// babel的traverse插件能遍历parser解析出来的抽象语法树
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
let depsGraph = {}
// ./src/index.js是项目的入口文
getModuleInfo('./src/index.js')
function getModuleInfo(file){
// 先读取文件内容
const body = fs.readFileSync(file, 'utf-8')
// 通过parser.parse将文件内容解析成一个抽象语法树
const ast = parser.parse(body, {
sourceType: 'module' //表示我们要解析的是ES模块
});
const deps = {}
// 通过traverse插件遍历抽象语法树
traverse(ast, {
// 对树节点是importDeclaration的模块进行处理
// 因为这种节点就是import语句,比如import add from './add.js'就会被解析成这种节点
ImportDeclaration({
node
}) {
// 这个node就是当前文件的依赖文件,比如index.js的代码中有import add from './add.js',那么这个node就是依赖的文件add.js
// 获取依赖文件的绝对路径
const dirname = path.dirname(file)
const abspath = "./" + path.join(dirname, node.source.value)
// 将依赖的 相对路径 和其 绝对路径 形成对应关系
// 比如 './add.js' : './src/add.js'
deps[node.source.value] = abspath
}
})
// 这个大家都懂,将代码转化成es5语法
const {
code
} = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
// 形成 文件路径-{依赖文件的绝对路径,文件的代码} 对应关系
depsGraph[file] = {deps,
code}
// 遍历依赖文件,然后重复递归调用
for (const key in deps) {
if (Object.hasOwnProperty.call(deps, key)) {
const el = deps[key];
getModuleInfo(el)
}
}
}
第二步:构造require函数
定义一个require函数,入参是文件的路径,然后从depsGraph中获取文件路径对应的代码,然后执行。
- 入口文件
./src/index.js
,执行require('./src/index.js')
depsGraph中index.js是这样的:
"./src/index.js": {
// 该文件代码里import的所有依赖文件的绝对路径
"deps": {
"./add.js": "./src\\add.js",
"./minus.js": "./src\\minus.js"
},
// 该文件的代码
"code": "\"use strict\";\n\nvar _add = _interopRequireDefault(require(\"./add.js\"));\n\nvar _minus = _interopRequireDefault(require(\"./minus.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nvar sum = (0, _add[\"default\"])(1, 2);\nvar division = (0, _minus[\"default\"])(2, 1);\nconsole.log(sum);\nconsole.log(division);"
},
定义require函数
function require(path){
let {code} = depsGraph[path]
eval(code)
}
require('./src/index.js')
如果是上图这样的话,index.js代码中的require函数是require(\"./add.js\")
和require(\"./minus.js\")
,./add.js和./minus.js都是相对路径,而depsGraph中存的是key是文件的绝对路径,明显取不到。
然而我们在depsGraph中保存着index.js的所有依赖的绝对路径
"deps": {
"./add.js": "./src\\add.js",
"./minus.js": "./src\\minus.js"
},
显然,对于index.js文件里的require函数,他们的入参是相对路径,而我们写的require必须传入的是绝对路径,那我们得重写一个require函数->absRequire函数,入参是相对路径,通过deps找到绝对路径,然后作为require函数的入参去执行
function require(path){
let {code, deps} = depsGraph[path]
function absRequire(relPath){
return require(deps[replPath])
}
(function(require,code){
eval(code)
})(absRequire,code)
return
}
require('./src/index.js')
构造export
node环境下,每个js文件默认都有一个export导出对象。
上图的代码中,我们没有显性的返回东西,而我们对require的功能定义:入参是文件绝对路径,出参是代码的执行后的exports对象
,那我们只要定义一个exports对象,然后eval(code),然后代码执行的结果就会被赋值在exports对象上,然后我们就可以返回它。
function require(path){
let {code, deps} = depsGraph[path]
function absRequire(relPath){
return require(deps[replPath])
}
let exports = {}
;(function(require,code,exports){
eval(code)
})(absRequire,code,exports)
return exports
}
require('./src/index.js')
全部代码
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
let depsGraph = {}
const getModuleInfo = (file) => {
const body = fs.readFileSync(file, 'utf-8')
const ast = parser.parse(body, {
sourceType: 'module' //表示我们要解析的是ES模块
});
const deps = {}
traverse(ast, {
ImportDeclaration({
node
}) {
const dirname = path.dirname(file)
const abspath = "./" + path.join(dirname, node.source.value)
deps[node.source.value] = abspath
}
})
const {
code
} = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
depsGraph[file] = {deps,
code}
for (const key in deps) {
if (Object.hasOwnProperty.call(deps, key)) {
const el = deps[key];
getModuleInfo(el)
}
}
}
// 新增代码
const bundle = (file) => {
getModuleInfo(file)
return `(function (graph) {
function require(file) {
function absRequire(relPath) {
return require(graph[file].deps[relPath])
}
var exports = {}
;(function (require,exports,code) {
eval(code)
})(absRequire,exports,graph[file].code)
return exports
}
require('${file}')
})(${JSON.stringify(depsGraph)})`
}
fs.writeFileSync('./dist/bundle.js', bundle('./src/index.js'))
然后运行node bundle.js
,就会在dist文件夹下生成合并和后的bundle.js,然后在index.html中
引入./dist/bundle.js,用浏览器打开html,就能在控制台看到输出
看到这里的看官,麻烦点个赞赞吧!
转载自:https://juejin.cn/post/7137122886971555847