minipack源码剖析以及扩大

前置学问

  1. 起首能够你须要晓得打包东西是什么存在
  2. 基础的模块化演化历程
  3. 对模块化bundle有肯定相识
  4. 相识babel的一些基本知识
  5. 对node有肯定基本知识

罕见的一些打包东西

如今最罕见的模块化构建东西 应该是webpack,rollup,fis,parcel等等林林总总。

然则如今可谓是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()

minipack的打包流程

能够分红两大部份

  1. 天生模块依靠(轮回援用等题目没有解决的~)
  2. 依据处置惩罚依靠举行打包

模块依靠天生

具体步骤

  1. 给定进口文件
  2. 依据进口文件理会依靠(借助bable猎取)
  3. 广度遍历依靠图猎取依靠
  4. 依据依靠图天生(模块id)key:(数组)value的对象示意
  5. 竖立require机制完成模块加载运转

源码的理会

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);

实例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);

理会得

我们模块代码会被实行。而且实行的效果会存储在module.exports中

并接收三个参数 require module module.exports

相似COMMONJS module会在模块闭包内注入exports, require, module, __filename, __dirname

会在进口处对其代码举行require实行一遍。

minipack源码总结

经由过程上述理会,我们能够相识

  • minipack的基础组织
  • 打包东西的基础形状
  • 模块的一些题目

扩大

既然bundle都已完成了,我们可不能够基于minipack完成一个简朴的HMR用于热替代模块内容

能够简朴的完成一下

一个简朴HMR完成

能够分为以下几步

  1. watch file change
  2. emit update to front-end
  3. front-end replace modules

固然另有更多仔细的处置惩罚。

比方,模块细分的hotload 处置惩罚,HMR的颗粒度等等

重要照样在设置module bundle时须要斟酌。

基于minipack完成

我们能够想象一下须要做什么。

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 module asset的变化

// 起首是完成第一步
// 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,
};

应用websockt举行交互提醒update

 
// 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文件

《minipack源码剖析以及扩大》

当我们修改base.js的内容时

《minipack源码剖析以及扩大》
《minipack源码剖析以及扩大》
《minipack源码剖析以及扩大》
《minipack源码剖析以及扩大》

就这样 一个简朴的基于minipack的HMR就完成了。

不过明显易见,存在的题目许多。纯当举一反三。

(比方module的副作用,资本只要js资本等等,仔细理会另有许多风趣的点)

扩大浏览

本文示例代码

minipack hmr

联络我

    原文作者:ZWkang
    原文地址: https://segmentfault.com/a/1190000018949887
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞