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中引用類型的存儲問題,這裏再也不贅述。