资讯专栏INFORMATION COLUMN

minipack源码解析以及扩展

tangr206 / 2472人阅读

摘要:的变化利用进行前后端通知。例如的副作用,资源只有资源等等,仔细剖析还有很多有趣的点扩展阅读创建热更新流程本文示例代码联系我

前置知识

首先可能你需要知道打包工具是什么存在

基本的模块化演变进程

对模块化bundle有一定了解

了解babel的一些常识

对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的打包流程
可以分成两大部分

生成模块依赖(循环引用等问题没有解决的~)

根据处理依赖进行打包

模块依赖生成

具体步骤

给定入口文件

根据入口文件分析依赖(借助bable获取)

广度遍历依赖图获取依赖

根据依赖图生成(模块id)key:(数组)value的对象表示

建立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实现
可以分为以下几步

watch file change

emit update to front-end

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




    
    
    
    Document


    
    <% 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.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 %>", ``)
    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资源等等,仔细剖析还有很多有趣的点)

扩展阅读

github minipack

what-aspect-of-hot-module-replacement-is-this-article-for

node 创建websocket

browserify-hmr

webpack热更新流程

本文示例代码

minipack hmr

联系我

kangkangblog/zwkang

zwkang github

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/103771.html

相关文章

  • 打包工具的配置教程见的多了,但它们的运行原理你知道吗?

    摘要:前端模块化成为了主流的今天,离不开各种打包工具的贡献。与此同时,打包工具也会处理好模块之间的依赖关系,最终这个大模块将可以被运行在合适的平台中。至此,整一个打包工具已经完成。明白了当中每一步的目的,便能够明白一个打包工具的运行原理。 showImg(https://segmentfault.com/img/bVbckjY?w=900&h=565); 前端模块化成为了主流的今天,离不开各...

    MoAir 评论0 收藏0
  • 【前端语言学习】学习minipack源码,了解打包工具的工作原理

    摘要:作者王聪学习目标本质上,是一个现代应用程序的静态模块打包器。为此,我们检查中的每个导入声明。将导入的值推送到依赖项数组中。为此,定义了一个只包含入口模块的数组。当队列为空时,此循环将终止。 作者:王聪 学习目标 本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关...

    shery 评论0 收藏0
  • 【前端语言学习】学习minipack源码,了解打包工具的工作原理

    摘要:作者王聪学习目标本质上,是一个现代应用程序的静态模块打包器。为此,我们检查中的每个导入声明。将导入的值推送到依赖项数组中。为此,定义了一个只包含入口模块的数组。当队列为空时,此循环将终止。 作者:王聪 学习目标 本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关...

    toddmark 评论0 收藏0
  • dayjs 源码解析(五)(dayjs 插件详解)

    摘要:前言上一篇源码解析四类介绍了的源码目录结构。接下来,本篇将分析一下中插件功能的用法源码以及如何编写自己的插件。并且,可以通过插件选项,来对插件进行配置。 前言 上一篇 dayjs 源码解析(四)(Dayjs 类)介绍了 dayjs 的源码目录结构。接下来,本篇将分析一下 dayjs 中插件功能的用法、源码以及如何编写自己的 dayjs 插件。 dayjs 插件用法 dayjs 的插件,...

    RaoMeng 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<