前置知識
現在最多見的模塊化構建工具 應該是webpack,rollup,fis,parcel等等各類各樣。html
可是如今可謂是webpack社區較爲龐大。前端
其實呢,模塊化開發很大的一點是爲了程序可維護性node
那麼其實咱們是否是能夠理解爲打包工具是將咱們一塊塊模塊化的代碼進行智能拼湊。使得咱們程序正常運行。webpack
// 1. 全局函數 function module1 () { // do somethings } function module2 () { // do somethings } // 2. 以對象作單個命名空間 var module = {} module.addpath = function() {} // 3. IIFE保護私有成員 var module1 = (function () { var test = function (){} var dosomething = function () { test(); } return { dosomething: dosomething } })(); // 4. 複用模塊 var module1 = (function (module) { module.moduledosomething = function() {} return module })(modules2); // 再到後來的COMMONJS、AMD、CMD // node module是COMMONJS的典型 (function(exports, require, module, __filename, __dirname) { // 模塊的代碼實際上在這裏 function test() { // dosomethings } modules.exports = { test: test } }); // AMD 異步加載 依賴前置 // requireJS示例 define('mymodule', ['module depes'], function () { function dosomethings() {} return { dosomethings: dosomethings } }) require('mymodule', function (mymodule) { mymodule.dosomethings() }) // CMD 依賴後置 // seajs 示例 // mymodule.js define(function(require, exports, module) { var module1 = require('module1') module.exports = { dosomethings: module1.dosomethings } }) seajs.use(['mymodule.js'], function (mymodule) { mymodule.dosomethings(); }) // 還有如今流行的esModule // mymodule export default { dosomething: function() {} } import mymodule from './mymodule.js' mymodule.dosomething()
能夠分紅兩大部分
具體步驟git
const fs = require('fs'); const path = require('path'); const babylon = require('babylon');//AST 解析器 const traverse = require('babel-traverse').default; //遍歷工具 const {transformFromAst} = require('babel-core'); // babel-core let ID = 0; function createAsset(filename) { const content = fs.readFileSync(filename, 'utf-8'); // 得到文件內容, 從而在下面作語法樹分析 const ast = babylon.parse(content, { sourceType: 'module', }); // 解析內容至AST // This array will hold the relative paths of modules this module depends on. const dependencies = []; // 初始化依賴集 // 使用babel-traverse基礎知識,須要找到一個statement而後定義進去的方法。 // 這裏進ImportDeclaration 這個statement內。而後對節點import的依賴值進行push進依賴集 traverse(ast, { ImportDeclaration: ({node}) => { // We push the value that we import into the dependencies array. dependencies.push(node.source.value); }, }); // id自增 const id = ID++; const {code} = transformFromAst(ast, null, { presets: ['env'], }); // 返回這麼模塊的全部信息 // 咱們設置的id filename 依賴集 代碼 return { id, filename, dependencies, code, }; } function createGraph(entry) { // 從一個入口進行解析依賴圖譜 // Start by parsing the entry file. const mainAsset = createAsset(entry); // 最初的依賴集 const queue = [mainAsset]; // 一張圖常見的遍歷算法有廣度遍歷與深度遍歷 // 這裏採用的是廣度遍歷 for (const asset of queue) { // 給當前依賴作mapping記錄 asset.mapping = {}; // 得到依賴模塊地址 const dirname = path.dirname(asset.filename); // 剛開始只有一個asset 可是dependencies可能多個 asset.dependencies.forEach(relativePath => { // 這邊得到絕對路徑 const absolutePath = path.join(dirname, relativePath); // 這裏作解析 // 至關於這層作的解析擴散到下一層,從而遍歷整個圖 const child = createAsset(absolutePath); // 至關於當前模塊與子模塊作關聯 asset.mapping[relativePath] = child.id; // 廣度遍歷藉助隊列 queue.push(child); }); } // 返回遍歷完依賴的隊列 return queue; } function bundle(graph) { let modules = ''; graph.forEach(mod => { modules += `${mod.id}: [ function (require, module, exports) { ${mod.code} }, ${JSON.stringify(mod.mapping)}, ],`; }); // CommonJS風格 const result = ` (function(modules) { function require(id) { const [fn, mapping] = modules[id]; function localRequire(name) { return require(mapping[name]); } const module = { exports : {} }; fn(localRequire, module, module.exports); return module.exports; } require(0); })({${modules}}) `; return result; }
// doing.js import t from './hahaha.js' document.body.onclick = function (){ console.log(t.name) } // hahaha.js export default { name: 'ZWkang' } const graph = createGraph('../example/doing.js'); const result = bundle(graph);
// 打包出的代碼相似 (function(modules) { function require(id) { const [fn, mapping] = modules[id]; function localRequire(name) { return require(mapping[name]); } const module = { exports : {} }; fn(localRequire, module, module.exports); return module.exports; } require(0); })({0: [ function (require, module, exports) { "use strict"; var _hahaha = require("./hahaha.js"); var _hahaha2 = _interopRequireDefault(_hahaha); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } document.body.onclick = function () { console.log(_hahaha2.default.name); }; }, {"./hahaha.js":1}, ],1: [ function (require, module, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = { name: 'ZWkang' }; }, {}, ],})
modules = { 0: [function code , {deps} ], 1: [function code , {deps} ] }
而require則是模擬了一個很簡單的COMMONJS模塊module的操做github
function require(id) { const [fn, mapping] = modules[id]; function localRequire(name) { return require(mapping[name]); } const module = { exports : {} }; fn(localRequire, module, module.exports); return module.exports; } require(0);
咱們模塊代碼會被執行。而且執行的結果會存儲在module.exports中web
並接受三個參數 require module module.exports算法
相似COMMONJS module會在模塊閉包內注入exports, require, module, __filename, __dirname
會在入口處對其代碼進行require執行一遍。後端
經過上述分析,咱們能夠了解數組
既然bundle都已經實現了,咱們可不能夠基於minipack實現一個簡單的HMR用於熱替換模塊內容
能夠簡單的實現一下
能夠分爲如下幾步
固然還有更多仔細的處理。
例如,模塊細分的hotload 處理,HMR的顆粒度等等
主要仍是在設置module bundle時須要考慮。
咱們能夠設想一下須要作什麼。
watch module asset的變化
利用ws進行先後端update通知。
改變前端的modules[變化id]
// 創建一個文件夾目錄格式爲 - test.js - base.js - bundle.js - wsserver.js - index.js - temp.html
// temp.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <button class="click"> click me </button> <% script %> <!-- 替換用佔位符 --> </body> </html>
// base.js與test.js則是測試用的模塊 // base.js var result = { name: 'ZWKas' } export default result // test.js import t from './base.js' console.log(t, '1'); document.body.innerHTML = t.name
// 首先是實現第一步 // watch asset file function createGraph(entry) { // Start by parsing the entry file. const mainAsset = createAsset(entry); const queue = [mainAsset]; for (const asset of queue) { asset.mapping = {}; const dirname = path.dirname(asset.filename); fs.watch(path.join(__dirname,asset.filename), (event, filename) => { console.log('watch ',event, filename) const assetSource = createAsset(path.join(__dirname,asset.filename)) wss.emitmessage(assetSource) }) asset.dependencies.forEach(relativePath => { const absolutePath = path.join(dirname, relativePath); const child = createAsset(absolutePath); asset.mapping[relativePath] = child.id; queue.push(child); }); } return queue; }
簡單改造了createGraphl 添加了fs.watch方法做爲觸發點。
(根據操做系統觸發底層實現的不一樣,watch的事件可能觸發幾回)
在建立資源圖的同時對資源進行了watch操做。
這邊還有一點要補充的。當咱們使用creareAsset的時候,若是沒有對id與path作關聯的話,那再次觸發得到的id也會發生改動。
能夠直接將絕對地址作module id關聯。從而複用了module的id
// createasset一些代碼的改動 關鍵代碼 let mapWithPath = new Map() if(!mapWithPath.has(path.resolve(__dirname, filename))) { mapWithPath.set(path.resolve(__dirname, filename), id) } const afterid = mapWithPath.get(path.resolve(__dirname, filename)) return { id: afterid, filename, dependencies, code, };
// wsserver.js file 則是實現第二步。利用websocket與前端進行交互,提示update const EventEmitter = require('events').EventEmitter const WebSocket = require('ws') class wsServer extends EventEmitter { constructor(port) { super() this.wss = new WebSocket.Server({ port }); this.wss.on('connection', function connection(ws) { ws.on('message', function incoming(message) { console.log('received: %s', message); }); }); } emitmessage(assetSource) { this.wss.clients.forEach(ws => { ws.send(JSON.stringify({ type: 'update', ...assetSource })) }) } } const wsserver = new wsServer(8080) module.exports = wsserver // 簡單地export一個帶對客戶端傳輸update信息的websocket實例
在fs.watch觸發點觸發
const assetSource = createAsset(path.join(__dirname,asset.filename)) wss.emitmessage(assetSource)
這裏就是作這個操做。將資源圖進行從新的建立。包括id,code等
bundle.js則是作咱們的打包操做
const minipack = require('./index') const fs = require('fs') const makeEntry = (entryHtml, outputhtml ) => { const temp = fs.readFileSync(entryHtml).toString() // console.log(temp)caches.c const graph = minipack.createGraph('./add.js') const result = minipack.bundle(graph) const data = temp.replace('<% script %>', `<script>${result}</script><script> const ws = new WebSocket('ws://127.0.0.1:8080') ws.onmessage = function(data) { console.log(data) let parseData try { parseData = JSON.parse(data.data) }catch(e) { throw e; } if(parseData.type === 'update') { const [fn,mapping] = modules[parseData.id] modules[parseData.id] = [ new Function('require', 'module', 'exports', parseData.code), mapping ] require(0) } } </script>`) fs.writeFileSync(outputhtml, data) } makeEntry('./temp.html', './index.html')
操做則是獲取temp.html 將依賴圖打包注入script到temp.html中
而且創建了ws連接。以獲取數據
const [fn,mapping] = modules[parseData.id] modules[parseData.id] = [ new Function('require', 'module', 'exports', parseData.code), mapping ] // 這裏是刷新對應module的內容 require(0) // 從入口重新運行一次
固然一些細緻操做可能replace只會對引用的模塊parent進行replace,可是這裏簡化版能夠先不作吧
這時候咱們去run bundle.js的file咱們會發現watch模式開啓了。此時
訪問生成的index.html文件
當咱們改動base.js的內容時
就這樣 一個簡單的基於minipack的HMR就完成了。
不過顯然易見,存在的問題不少。純當拋磚引玉。
(例如module的反作用,資源只有js資源等等,仔細剖析還有不少有趣的點)