webpack是一款前端項目構建工具,隨着如今前端生態的發展,webpack已經成爲前端開發人員必備的技能之一,不少開發人員開始使用react和vue的時候,都會使用默認的單頁應該建立指令來建立一個工程化項目,實際上,這些工程化的項目都是基於webpack來搭建的;
當咱們熟悉使用這些工程話文件的時候,咱們就會開始思考,爲何咱們寫的代碼直接在瀏覽器運行不了,通過webpack打包之後就能在瀏覽器上運行,打包的過程發生了什麼?前端
實際上,webpack 是基於node實現的,打包的過程包括了讀取文件流進行處理和模塊依賴的引入解析和導出等過程,下面來簡單的實現這麼一個過程。github源碼地址:https://github.com/wzd-front-...vue
首先,咱們新建一個文件夾,能夠命名爲bundler,並在命令行工具(黑窗口)中使用npm init進行初始化,初始化的過程當中,會要求咱們輸入項目相關的一些信息,以下node
Press ^C at any time to quit. package name: (bundler) version: (1.0.0) description: entry point: (index.js) test command: git repository: (https://github.com/wzd-front-end/bundler.git) keywords: author: license: (ISC)
若是咱們想跳過這一個環節,能夠使用npm init -y,加上-y後,自動生成默認配置,不會再詢問;python
接下來,在建立測試用例以前,咱們先來構建咱們的項目,下面是咱們的目錄結構,src文件夾下面的文件爲咱們的測試例子:react
--bundler --src index.js message.js word.js --node_modules --bundler.js --package.json --README.md
word.js代碼webpack
export const word = 'hello';
message.js代碼git
import { word } from './word.js'; const message = `say ${word}`; export default message;
index.js代碼github
import message from './message.js'; console.log(message);
經過觀察上面簡單的三個文件的代碼,咱們會發現,這幾段代碼的主要功能模塊的導入和導出解析,這也是打包工具的主要功能,那這些代碼是如何轉換爲瀏覽器可識別代碼的,接下來,咱們來經過代碼演示實現這個過程;web
首先,咱們在bundler文件下建立bundler.js文件,做爲咱們打包過程的執行文件,而後咱們去執行node bundler.js來執行打包的過程;咱們先建立一個名爲moduleAnalyser的函數來解析模塊,該函數接收一個filename地址字符串,獲取到對應地址的文件,並經過
@babel/parser模塊的parser方法將對應的文件字符串轉化爲抽象節點樹,不清楚抽象節點樹的小夥伴能夠經過把下面代碼中的ast在控制檯中打印出來,觀察其結構;在咱們生成節點樹後,咱們須要獲取其中的import節點,不少人能夠想着,那經過字符串截取出import字符不就u能夠嗎?
當只有一個import的時候,確實能夠,但多個的時候,咱們經過截取來實現就比較複雜了,這個時候,咱們能夠藉助
@babel/traverse來幫咱們實現,具體實現能夠查看babel官網,引入該模塊後,咱們能夠將parser獲取到ast做爲參數傳入;經過前面輸出的節點樹咱們能夠發現,import 節點的type類型爲ImportDeclaration,咱們能夠在traverse()的第二個參數中傳入一個對象,以節點的type類型做爲名稱,能夠幫咱們獲取到對應的節點,最後咱們再將處理後的ast從新轉化爲代碼字符串返回,具體實現以下:npm
const fs = require('fs') const path = require('path') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default; const babel = require('@babel/core'); const moduleAnalyser = (filename) => { // 經過fs模塊的異步讀取文件api獲取傳入路徑的文件,編碼格式爲'utf-8' const content = fs.readFileSync(filename, 'utf-8'); // 經過parser.parse方法將讀取到的代碼轉化爲抽象節點樹,其中sourceType類型是指定導入文件的方式 const ast = parser.parse(content, { sourceType: "module" }); const dependencies = {} // 經過traverse獲取節點樹中類型爲ImportDeclaration的節點,並將其映射關係保存到dependencies對象中 traverse(ast, { ImportDeclaration({ node }) { // 獲取傳入路勁的根路徑 const dirname = path.dirname(filename) // 拼接文件中實際引入文件的路徑 const newFile = dirname + node.source.value // 將映射關係存入dependencies對象中 dependencies[node.source.value] = newFile } }) // 利用presets將ast轉化爲對應的es5代碼,第一個參數是抽象節點樹,第二個參數是源碼,第三個參數是配置 const { code } = babel.transformFromAst(ast, null, { presets: ["@babel/preset-env"] }) return { filename, dependencies, code } } console.log(moduleAnalyser('./src/index.js'))
經過上面代碼,咱們能夠獲得一個模塊入口文件的分析,包括模塊的名稱,依賴以及代碼,但咱們只是獲得一個入口文件的解析,入口模塊裏面有本身的依賴,依賴裏面又有本身的依賴,所以,咱們須要去對每個模塊進行深度分析;
.... // 用於循環調用多個模塊 const makeDependenciesGraph = (entry) => { // 首先獲取入口模塊的分析對象 const entryModule = moduleAnalyser(entry) // 保存所有模塊的分析對象 const graphArray = [entryModule] // 對graphArray 中的每一項進行分析,分析每一項中的dependencies,若是存在,咱們就把新的依賴模塊進行分析,直到所有查找完爲止 for (let i = 0; i < graphArray.length; i++) { const item = graphArray[i] const {dependencies} = item // 若是dependencies不爲空對象,就利用for..in枚舉對象中每一個依賴模塊,將依賴模塊的路徑存入,分析生成新的分析結果對象,存入到graphArray數組中 if (JSON.stringify(dependencies) !== '{}') { for (let j in dependencies) { graphArray.push(moduleAnalyser(dependencies[j])) } } } // 咱們把最後的結果經過每一個分析結果對象的filename做爲key值,存入graph對象中,目的是爲了方便後續經過模塊路徑進行取值 const graph = {} graphArray.forEach(item => { graph[item.filename] = { dependencies: item.dependencies, code: item.code } }) return graph } console.log(makeDependenciesGraph('./src/index.js'))
執行完上面的操做後,咱們經過入口文件進入後全部相關的模塊已經所有解析完畢,接下來,咱們須要把這些模塊,轉化爲瀏覽器能夠執行的代碼,轉化後生成的代碼中,咱們會發現,包含了require方法和export對象,這都是咱們瀏覽器不具有的,咱們須要進一步聲明對應的方法,讓瀏覽器能找到對應的方法去執行,接下來咱們執行最後一步的生成代碼操做
.... const generateCode = (entry) => { // 由於咱們須要返回對應的可執行字符串,因此咱們須要把對象先轉化爲字符串,否則會出現'[object, object]' const graph = JSON.stringify(makeDependenciesGraph(entry)); // 返回字符串使用模板字符串,且使用到閉包,防止污染全局 return ` (function(graph){ function require(module) { function localRequire(relativePath) { return require(graph[module].dependencies[relativePath]); } var exports = {}; (function(require, exports, code){ eval(code) })(localRequire, exports, graph[module].code); return exports; }; require('${entry}') })(${graph}); `; } const code = generateCode('./src/index.js') console.log(code)
最後咱們在控制檯輸出的代碼,複製到瀏覽器的控制擡中執行,按照預約的結果運行打印出結果,運行代碼以下:
(function(graph){ function require(module) { function localRequire(relativePath) { return require(graph[module].dependencies[relativePath]); } var exports = {}; (function(require, exports, code){ eval(code) })(localRequire, exports, graph[module].code); return exports; }; require('./src/index.js') })({"./src/index.js":{"dependencies":{"./message.js":"./src\\message.js"},"code":"\"use strict\";\n\nvar _message = _interopRequireDefault(require(\"./message.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_message[\"default\"]);"},"./src\\message.js":{"dependencies":{"./word.js":"./src\\word.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _word = require(\"./word.js\");\n\nvar message = \"say \".concat(_word.word);\nvar _default = message;\nexports[\"default\"] = _default;"},"./src\\word.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.word = void 0;\nvar word = 'hello';\nexports.word = word;"}});
以上的代碼就是咱們打包後的代碼,咱們會發現,在咱們打包後,須要用到其餘的模塊的時候,會調用require 方法,require方法又會經過傳入的地址路徑參數去查詢咱們生成的以filename爲key值的對象,找到對應的code,利用eval()方法去執行,這就是打包工具的一個基本原理。