Webpack很强大,做为前端开发人员咱们必须熟练掌握。但它的原理其实并不难理解,甚至很简单。毕竟全部复杂的事物都是由简单的事物组合造成的。不光是Webpack,像Vue,React这样成熟的前端框架亦是如此。css
读完本文,你会认识到:html
另外,但愿你能跟着本身实现一遍,代码量真的不大。前端
源码,能够clone下来写一写node
先看一个例子,也许你还不知道,node其实还有这样一个彩蛋:webpack
新建test.js输入一行代码:git
/* test.js */
console.log(arguments.callee.toString())
复制代码
在命令行中输入node test.js
运行结果以下:es6
function (exports, require, module, __filename, __dirname) {
console.log(arguments.callee.toString())
}
复制代码
注意这是控制台输出的代码,也就是console.log()的输出结果。github
因为arguments.callee
这个属性指向函数的调用者,咱们使用toString()转化后发现这竟然是一个函数,由此说明,node的代码实际上是运行在一个函数中的。咱们写的代码最终会被这样一个函数包裹,经常使用的require,module,exports,__dirname, __filename
都是这个函数的参数,因此咱们才随处可用。web
export const message = 'qin'
export const weather = 'sunny day'
复制代码
export default function (name) {
console.log(`hello ${name}`)
}
复制代码
#app {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
animation: breath 2s ease infinite;
}
@keyframes breath {
from, to {
width: 100px;
height: 100px;
background-color: black;
}
50% {
width: 200px;
height: 200px;
background-color: red;
}
}
复制代码
import hello from './js/say.js'
import { message, weather } from './js/message.js'
import './css/main.css'
hello(message)
hello(`今天的天气是:${weather}`)
复制代码
main.js
开始,递归解析依并读取文件内容。可使用@babel/parser
来实现。CSS in JS
这个概念。(function (参数) {
/* 函数体 */
})(传参)
复制代码
聪明的你应该想到了,开篇提到的例子就是为了解决这个问题。npm
配置webpack进行打包,具体配置很是简单这里就不贴代码了。若是你还不会配置的话,或许须要先学习webpack的基础知识。
我删剪了部分代码,那不属于咱们讨论的范畴,最后生成的bundle.js内容以下:
(function(modules) {
// webpack中用来模拟node环境下的require函数
function __webpack_require__(path) {
// 构造一个模块
var module = { exports: {} };
// 执行模块对应的函数
modules[path].call(module.exports, module, module.exports,__webpack_require__);
// 返回模块加载的的结果
return module.exports;
}
__webpack_require__("./src/main.js");
}) ({
"./src/css/main.css": (function(module, exports, __webpack_require__) {
eval("var api = __webpack_require__(/*! ../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js */ \"./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\");\n var content = __webpack_require__(/*! !../../node_modules/css-loader/dist/cjs.js!./main.css */ \"./node_modules/css-loader/dist/cjs.js!./src/css/main.css\");\n\n content = content.__esModule ? content.default : content;\n\n if (typeof content === 'string') {\n content = [[module.i, content, '']];\n }\n\nvar options = {};\n\noptions.insert = \"head\";\noptions.singleton = false;\n\nvar update = api(content, options);\n\nvar exported = content.locals ? content.locals : {};\n\n\n\nmodule.exports = exported;\n\n//# sourceURL=webpack:///./src/css/main.css?");
}),
"./src/js/message.js": (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"message\", function() { return message; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"weather\", function() { return weather; });\nconst message = 'qin';\nconst weather = 'sunny day';\n\n//# sourceURL=webpack:///./src/js/message.js?");
}),
"./src/js/say.js": (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (function (name) {\n console.log(`hello ${name}`);\n});\n\n//# sourceURL=webpack:///./src/js/say.js?");
}),
"./src/main.js": (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _js_say_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./js/say.js */ \"./src/js/say.js\");\n/* harmony import */ var _js_message_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./js/message.js */ \"./src/js/message.js\");\n/* harmony import */ var _css_main_css__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./css/main.css */ \"./src/css/main.css\");\n/* harmony import */ var _css_main_css__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_css_main_css__WEBPACK_IMPORTED_MODULE_2__);\n\n\n\nObject(_js_say_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(_js_message_js__WEBPACK_IMPORTED_MODULE_1__[\"message\"]);\nObject(_js_say_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(`今天的天气是:${_js_message_js__WEBPACK_IMPORTED_MODULE_1__[\"weather\"]}`);\n\n//# sourceURL=webpack:///./src/main.js?");
})
});
复制代码
能够看到,总体是一个闭包函数,传递的参数为已加载的全部的模块组成的对象。这里能够看到,模块就是一个个的函数,即开篇提到的例子。闭包函数的主体是,经过模拟的require函数找到对应模块并调用。至于eavl,不用多说了吧?传入代码内容字符串就会执行了。
须要用到的工具以下
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser') // 生成抽象语法树
const { transformFromAst } = require('@babel/core') // 转换es6语法
const { default: traverse } = require('@babel/traverse') // 抽象语法树分析
复制代码
注意,traverse模块是ES6 Module,因此使用CommonJs引入时须要加上default
快速安装:
npm install @babel/parser @babel/core @babel/traverse @babel/parser -D
复制代码
好的,如今在根目录下新建bundler.js用来打包,咱们的打包流程将写在这里。首先实现analyze函数:
/**
* 经过路径读取文件并解析
* @param {String} filePath
* @return {Object} 解析结果
*/
const analyze = function (filePath) {
const content = fs.readFileSync(filePath, 'utf-8')
const ast = parser.parse(content, { sourceType: 'module' })
const dependencies = []
// 转换es6语法,并获得转换后的源代码
const { code } = transformFromAst(ast, null, {
presets: ['@babel/env']
})
// 分析依赖
traverse(ast, {
// 分析依赖的钩子
ImportDeclaration ({ node }) {
dependencies.push(node.source.value) // 得到全部依赖
}
})
return {
filePath,
dependencies,
code
}
}
复制代码
这里解释下traverse函数的做用。咱们使用@babel/parser
生成抽象语法树AST,就是一个描述代码结构的JSON对象,这个对象中包含了语法信息。咱们能够打印下
console.log(ast.program.body)
复制代码
结果是一个数组,我截取了数组中的一个元素,以下:
Node {
type: 'ImportDeclaration',
start: 0,
end: 31,
loc: SourceLocation { start: [Position], end: [Position] },
specifiers: [ [Node] ],
source: Node {
type: 'StringLiteral',
start: 18,
end: 31,
loc: [SourceLocation],
extra: [Object],
value: './js/say.js'
}
}
复制代码
能够看到type: 'ImportDeclaration'
说明这是一个import引入语法,如此一来,咱们就能够轻松的拿到对应的依赖,如上例是./js/say.js
traverse中的ImportDeclaration钩子,参数中包含node属性,这就是咱们须要找的依赖文件,咱们将它保存起来用于下面的分析。
经过对代码的依赖分析,获取全部资源,用于最后的打包
/**
* 经过入口文件递归解析依赖,并返回全部的依赖
* @param {String} entryFile 入口文件
* @return 依赖的全部代码
*/
const getAssets = function (entryFile) {
const entry = analyze(entryFile)
const dependencies = [entry] // 起初依赖只包含入口,随着遍历不断加入
for (const asset of dependencies) {
// 获取目录名
const dirname = path.dirname(asset.filePath)
asset.dependencies.forEach(relPath => {
// 将相对路径转换为绝对路径,相对路径是基于dirname的
const absolutePath = path.join(dirname, relPath)
// 处理css文件
if (/\.css$/.test(absolutePath)) {
const content = fs.readFileSync(absolutePath, 'utf-8')
// 使用js插入style节点
const cssInsertCode = `
const stylesheet = document.createElement('style');
stylesheet.innerText = ${JSON.stringify(content)};
document.head.appendChild(stylesheet);
`
dependencies.push({
filePath: absolutePath,
relPath, // 记得保存相对路径,由于require的时候须要用到
dependencies: [],
code: cssInsertCode
})
} else {
const child = analyze(absolutePath)
child.relPath = relPath // 同上
dependencies.push(child) // 递归解析
}
})
}
return dependencies
}
复制代码
打包的目的是将文件合并,因为浏览器环境限制,咱们须要构造闭包,还要模拟node的环境变量。
/**
* 打包流程主函数
* @param {String} entry 入口文件
* @return void
*/
const bundle = function (entry) {
const dependencies = getAssets(entry)
// 将依赖构建成对象
const deps = dependencies.map(dep => {
const filePath = dep.relPath || entry
// 路径和模块造成映射
return `'${filePath}':function (exports, require, module) { ${dep.code} }`
})
// 构造require函数,babel解析后的代码是node环境下的,咱们须要构造相应的函数
// 来模拟原生require,从咱们构建的deps对象中获取相应模块函数
const result = `(function(deps){
function require(path){
// 构造一个模块,表示当前模块
const module = { exports: {} }
// 执行对应的模块,并传入参数
deps[path](module.exports, require, module)
// 返回模块导出的内容,也就是require函数获取到的内容
return module.exports
}
require('${entry}') // 从入口文件开始执行
})({${deps.join(',')}})`
// 若是你想压缩成一行能够加上这个,可是相应的要安装babel-preset-minify
// const ast = parser.parse(result, { sourceType: 'script' })
// const { code } = transformFromAst(ast, null, {
// presets: ['minify']
// })
// 写入文件
fs.writeFileSync('./public/vendors.js', result) // 若是你压缩了,这里填code
}
// 运行打包
bundle('./src/main.js')
复制代码
须要注意的是,咱们要将代码觉得本的形式拼接在一块儿,不然代码将会直接运行生成结果,这不是咱们想要的。牢记,咱们是在拼接代码。
${deps.join(',')}
获得的内容是一个字符串,咱们用一个大括号括起来,在运行时就至关因而一个对象了,即{${deps.join(',')}}
。
也许你会想直接构造一个对象而后使用JSON.stringify不就行了吗。实际上不行,由于咱们的这个对象的键值对中,key能够是字符串,可是value不行,value是咱们模拟的一个node模块,是一个函数,JSON.stringify会致使咱们最终获取到的是函数的字符串,而不是函数。
打包后的vendors.js内容以下:
(function (deps) {
function require(path) {
const module = {
exports: {}
}
deps[path](module.exports, require, module)
return module.exports
}
require('./src/main.js')
})({
'./src/main.js': function (exports, require, module) {
"use strict";
var _say = _interopRequireDefault(require("./js/say.js"));
var _message = require("./js/message.js");
require("./css/main.css");
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {
"default": obj
};
}
(0, _say["default"])(_message.message);
(0, _say["default"])("\u4ECA\u5929\u7684\u5929\u6C14\u662F\uFF1A".concat(_message.weather));
},
'./js/say.js': function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = _default;
function _default(name) {
console.log("hello ".concat(name));
}
},
'./js/message.js': function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.weather = exports.message = void 0;
var message = 'qin';
exports.message = message;
var weather = 'sunny day';
exports.weather = weather;
},
'./css/main.css': function (exports, require, module) {
const stylesheet = document.createElement('style');
stylesheet.innerText = "#app {\r\n position: absolute;\r\n top: 50%;\r\n left: 50%;\r\n transform: translate(-50%, -50%);\r\n animation: breath 2s ease infinite;\r\n}\r\n@keyframes breath {\r\n from, to {\r\n width: 100px;\r\n height: 100px;\r\n background-color: black;\r\n }\r\n 50% {\r\n width: 200px;\r\n height: 200px;\r\n background-color: red;\r\n }\r\n}";
document.head.appendChild(stylesheet);
}
})
复制代码
对比webpack的结果,是否是很类似?只不过咱们没有使用eval函数,而是将代码直接写在函数体中。
新建html文件并引入vendors.js
<div id="app"></div>
<script src="./vendors.js"></script>
复制代码
结果以下:
生效了,没问题。
这就是咱们自制的一个小型打包工具啦~喜欢点个赞哈😊
在webpack打包代码结果展现那里,我删除的代码是关于webpack的一些更高级的功能的。例如webpack内置了缓存机制,一个模块加载事后就会缓存起来,并赋予id值,而后标记为已加载。之后再加载这个模块的时候经过标记判断,加载过的话就直接读缓存。
咱们构建的module是这样的:
const module = { exports: {} }
复制代码
而__webpack_require__
中构建的module是这样的:
// installedModules就是缓存
var module = installedModules[moduleId] = {
i: moduleId, // 经过id来获取
l: false, // loaded:标识是否加载过
exports: {}
};
复制代码
咱们再去node中打印一下module
的值:
console.log(module, module.exports === exports)
// 结果以下:
Module {
id: '.',
path: 'c:\\Users\\Administrator\\Desktop',
exports: {},
parent: null,
filename: 'c:\\Users\\Administrator\\Desktop\\a.js',
loaded: false,
children: [],
paths: [
'c:\\Users\\Administrator\\Desktop\\node_modules',
'c:\\Users\\Administrator\\node_modules',
'c:\\Users\\node_modules',
'c:\\node_modules'
]
} true
复制代码
看这结构模,是这么的类似~文件名,模块id,路径等。是否是有种尽在掌握的感受?😄
相比之下咱们构建的module就很简陋了,不过仍是能说明问题的,至少证实,node的模块化机制也没有那么难理解嘛。
咱们还能够看到,module.exports和exports
是同一个对象,指向同一块内存,所以咱们既能够经过exports.a = 1
这种属性的方式导出,也能够经过module.exports = {a:1}
这种字面量的方式导出。
可是使用exports时,不能直接赋值,如:exports = {a:1}
,这是没法正常导出的,涉及js中引用类型的存储问题,这里再也不赘述。