前置知识
现在最多见的模块化构建工具 应该是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资源等等,仔细剖析还有不少有趣的点)