摘要:的變化利用進行前后端通知。例如的副作用,資源只有資源等等,仔細(xì)剖析還有很多有趣的點擴展閱讀創(chuàng)建熱更新流程本文示例代碼聯(lián)系我
前置知識
首先可能你需要知道打包工具是什么存在
基本的模塊化演變進程
對模塊化bundle有一定了解
了解babel的一些常識
對node有一定常識
常見的一些打包工具如今最常見的模塊化構(gòu)建工具 應(yīng)該是webpack,rollup,fis,parcel等等各種各樣。
但是現(xiàn)在可謂是webpack社區(qū)較為龐大。
其實呢,模塊化開發(fā)很大的一點是為了程序可維護性
那么其實我們是不是可以理解為打包工具是將我們一塊塊模塊化的代碼進行智能拼湊。使得我們程序正常運行。
基本的模塊化演變// 1. 全局函數(shù) 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. 復(fù)用模塊 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(); }) // 還有現(xiàn)在流行的esModule // mymodule export default { dosomething: function() {} } import mymodule from "./mymodule.js" mymodule.dosomething()minipack的打包流程
可以分成兩大部分
生成模塊依賴(循環(huán)引用等問題沒有解決的~)
根據(jù)處理依賴進行打包
模塊依賴生成具體步驟
給定入口文件
根據(jù)入口文件分析依賴(借助bable獲取)
廣度遍歷依賴圖獲取依賴
根據(jù)依賴圖生成(模塊id)key:(數(shù)組)value的對象表示
建立require機制實現(xiàn)模塊加載運行
源碼的分析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"); // 獲得文件內(nèi)容, 從而在下面做語法樹分析 const ast = babylon.parse(content, { sourceType: "module", }); // 解析內(nèi)容至AST // This array will hold the relative paths of modules this module depends on. const dependencies = []; // 初始化依賴集 // 使用babel-traverse基礎(chǔ)知識,需要找到一個statement然后定義進去的方法。 // 這里進ImportDeclaration 這個statement內(nèi)。然后對節(jié)點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"], }); // 返回這么模塊的所有信息 // 我們設(shè)置的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) { // 給當(dāng)前依賴做mapping記錄 asset.mapping = {}; // 獲得依賴模塊地址 const dirname = path.dirname(asset.filename); // 剛開始只有一個asset 但是dependencies可能多個 asset.dependencies.forEach(relativePath => { // 這邊獲得絕對路徑 const absolutePath = path.join(dirname, relativePath); // 這里做解析 // 相當(dāng)于這層做的解析擴散到下一層,從而遍歷整個圖 const child = createAsset(absolutePath); // 相當(dāng)于當(dāng)前模塊與子模塊做關(guān)聯(lián) 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風(fēng)格 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);實例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); })({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的操作
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);分析得
我們模塊代碼會被執(zhí)行。并且執(zhí)行的結(jié)果會存儲在module.exports中
并接受三個參數(shù) require module module.exports
類似COMMONJS module會在模塊閉包內(nèi)注入exports, require, module, __filename, __dirname
會在入口處對其代碼進行require執(zhí)行一遍。
minipack源碼總結(jié)通過上述分析,我們可以了解
minipack的基本構(gòu)造
打包工具的基本形態(tài)
模塊的一些問題
擴展既然bundle都已經(jīng)實現(xiàn)了,我們可不可以基于minipack實現(xiàn)一個簡單的HMR用于熱替換模塊內(nèi)容
可以簡單的實現(xiàn)一下
一個簡單HMR實現(xiàn)可以分為以下幾步
watch file change
emit update to front-end
front-end replace modules
當(dāng)然還有更多仔細(xì)的處理。
例如,模塊細(xì)分的hotload 處理,HMR的顆粒度等等
主要還是在設(shè)置module bundle時需要考慮。
基于minipack實現(xiàn)我們可以設(shè)想一下需要做什么。
watch module asset的變化
利用ws進行前后端update通知。
改變前端的modules[變化id]
// 建立一個文件夾目錄格式為 - test.js - base.js - bundle.js - wsserver.js - index.js - temp.html
// temp.htmlDocument <% script %>
// 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.namewatch module asset的變化
// 首先是實現(xiàn)第一步 // 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方法作為觸發(fā)點。
(根據(jù)操作系統(tǒng)觸發(fā)底層實現(xiàn)的不同,watch的事件可能觸發(fā)幾次)
在創(chuàng)建資源圖的同時對資源進行了watch操作。
這邊還有一點要補充的。當(dāng)我們使用creareAsset的時候,如果沒有對id與path做關(guān)聯(lián)的話,那再次觸發(fā)獲得的id也會發(fā)生改動。
可以直接將絕對地址做module id關(guān)聯(lián)。從而復(fù)用了module的id
// createasset一些代碼的改動 關(guān)鍵代碼 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, };利用websockt進行交互提示update
// wsserver.js file 則是實現(xiàn)第二步。利用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觸發(fā)點觸發(fā)
const assetSource = createAsset(path.join(__dirname,asset.filename)) wss.emitmessage(assetSource)
這里就是做這個操作。將資源圖進行重新的創(chuàng)建。包括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 %>", ``) fs.writeFileSync(outputhtml, data) } makeEntry("./temp.html", "./index.html")
操作則是獲取temp.html 將依賴圖打包注入script到temp.html中
并且建立了ws鏈接。以獲取數(shù)據(jù)
在前端進行模塊替換const [fn,mapping] = modules[parseData.id] modules[parseData.id] = [ new Function("require", "module", "exports", parseData.code), mapping ] // 這里是刷新對應(yīng)module的內(nèi)容 require(0) // 從入口從新運行一次
當(dāng)然一些細(xì)致操作可能replace只會對引用的模塊parent進行replace,但是這里簡化版可以先不做吧
這時候我們?nèi)un bundle.js的file我們會發(fā)現(xiàn)watch模式開啟了。此時
訪問生成的index.html文件
當(dāng)我們改動base.js的內(nèi)容時
就這樣 一個簡單的基于minipack的HMR就完成了。
不過顯然易見,存在的問題很多。純當(dāng)拋磚引玉。
(例如module的副作用,資源只有js資源等等,仔細(xì)剖析還有很多有趣的點)
擴展閱讀github minipack
what-aspect-of-hot-module-replacement-is-this-article-for
node 創(chuàng)建websocket
browserify-hmr
webpack熱更新流程
本文示例代碼minipack hmr
聯(lián)系我kangkangblog/zwkang
zwkang github
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/103771.html
摘要:前端模塊化成為了主流的今天,離不開各種打包工具的貢獻。與此同時,打包工具也會處理好模塊之間的依賴關(guān)系,最終這個大模塊將可以被運行在合適的平臺中。至此,整一個打包工具已經(jīng)完成。明白了當(dāng)中每一步的目的,便能夠明白一個打包工具的運行原理。 showImg(https://segmentfault.com/img/bVbckjY?w=900&h=565); 前端模塊化成為了主流的今天,離不開各...
摘要:作者王聰學(xué)習(xí)目標(biāo)本質(zhì)上,是一個現(xiàn)代應(yīng)用程序的靜態(tài)模塊打包器。為此,我們檢查中的每個導(dǎo)入聲明。將導(dǎo)入的值推送到依賴項數(shù)組中。為此,定義了一個只包含入口模塊的數(shù)組。當(dāng)隊列為空時,此循環(huán)將終止。 作者:王聰 學(xué)習(xí)目標(biāo) 本質(zhì)上,webpack 是一個現(xiàn)代 JavaScript 應(yīng)用程序的靜態(tài)模塊打包器(module bundler)。當(dāng) webpack 處理應(yīng)用程序時,它會遞歸地構(gòu)建一個依賴關(guān)...
摘要:作者王聰學(xué)習(xí)目標(biāo)本質(zhì)上,是一個現(xiàn)代應(yīng)用程序的靜態(tài)模塊打包器。為此,我們檢查中的每個導(dǎo)入聲明。將導(dǎo)入的值推送到依賴項數(shù)組中。為此,定義了一個只包含入口模塊的數(shù)組。當(dāng)隊列為空時,此循環(huán)將終止。 作者:王聰 學(xué)習(xí)目標(biāo) 本質(zhì)上,webpack 是一個現(xiàn)代 JavaScript 應(yīng)用程序的靜態(tài)模塊打包器(module bundler)。當(dāng) webpack 處理應(yīng)用程序時,它會遞歸地構(gòu)建一個依賴關(guān)...
摘要:前言上一篇源碼解析四類介紹了的源碼目錄結(jié)構(gòu)。接下來,本篇將分析一下中插件功能的用法源碼以及如何編寫自己的插件。并且,可以通過插件選項,來對插件進行配置。 前言 上一篇 dayjs 源碼解析(四)(Dayjs 類)介紹了 dayjs 的源碼目錄結(jié)構(gòu)。接下來,本篇將分析一下 dayjs 中插件功能的用法、源碼以及如何編寫自己的 dayjs 插件。 dayjs 插件用法 dayjs 的插件,...
閱讀 3132·2021-10-12 10:11
閱讀 1836·2021-08-16 10:59
閱讀 2844·2019-08-30 15:55
閱讀 1223·2019-08-30 14:19
閱讀 2030·2019-08-29 17:03
閱讀 2463·2019-08-29 16:28
閱讀 3212·2019-08-26 13:47
閱讀 2880·2019-08-26 13:36