后端架构师看Webpack

LeeMaster
LeeMaster   编辑于 2019-01-29 05:34
阅读量: 44

说实话Webpack 应该是前端的工具吧,但是我看webpack的文档写的还是十分清楚的好不好??现在前端只是写CSS可还行,工程上的模块化还是要学的好不好?

最近需要处理一个小项目,编译器那个还没来得及继续写,我是肯定会写完的。就赶上了Bundle异步加载,权限来控制bundle的载入。

那就避免不了Webpack这个东西啦,在学校旁听过H5方向的课,这个webpack说实话可能老师都是懵的。

 

OK ,首先从这个问题最开始发生讲起,也就是前端并没有模块化组件化,SPA应用的时候,学长我还在上专业课的时候,就写过网站,大概React刚起来的时候,还没啥人用的时候,我开始尝试用。

那个时候大家都用这个来引入js文件,并且在js文件做各种限制让js模块化,这个时期最好用的东西,Jquery Zepto 这些东西,当然现在有的库还是很好用,animateJS变成了animateCss,这个时候我们的web页面下载js怎么下载?

<script src="XXX.js"></script>

没错就是这么引入页面的,并且那个时候写代码,基本都是cv大法,直接html css 混合包装,十分刺激的

后来大家都感觉到这个东西很坑爹了,因为工程越来越大,js文件越来越多,cv大法只会让系统内的熵增加,并且所谓的前端开发规范并没有避免系统开发的熵增现象,说白了就是谁写代码都会骂街!

后来就出来了RequireJs ,这个我应该也是尝试比较早,基本就是这么玩

<script data-main="scripts/main" src="scripts/require.js"></script>

引入requireJs并且指定入口JS,然后利用require进行各种骚操作。

怎么用呢,至于RequireJS 是怎么配置的,就不说了,毕竟已经淘汰了。

require(['jquery'], function( $ ) {
    console.log( $ ) // OK
});

require(['jquery'], function( jq ) {
    console.log( jq ) // OK
});

就是这么用的,全都放入一个作用域内,然后通过一些逻辑暴露到全局变量上去。

这里讨论Require咋回事不重要,同样讨论AMD还是CommonJS 两个模块规范也不重要,我先说一下什么叫程序运行空间,也就是经常说的Runtime这个东西。

现在请时刻牢记,你的JS代码统统都是在浏览器虚拟出来的Runtime中运行的,或者说,你的每一个浏览器Tab都是一个Runtime空间,不同的Tab是不会产生影响的,比如Tab1和Tab2好像并不能交换数据对吧。当然WAS大法就算了吧,毕竟取hook内核,目前Js应该是做不到的哈。而且一个Tab只会有一个页面在跑js,跳转,那个就是把当前runtime清空掉,然后再加载新的Runtime的过程而已。

那我觉得既然还是Runtime的问题,那么就说一下这个程序是咋运行的吧。

简单来说这么几步

  1. 加载程序代码到内存中
  2. 初始化全局变量,并为全局未初始化变量分配空间
  3. 从入口函数开始执行
  4. 结束退出

OK,基本就这四步就是一个程序的具体运行过程,那么JS这个语言我们都知道他直接就是解释语言,也就是说和C不一样,C是直接编译成为目标代码,那么加载程序代码到内存中,其实你就可以认为加载的是汇编代码,一条一条的指令,那么JS是解释运行的,也就是一步一步解释然后再一条一条载入,并且执行。

而且学C语言的时候,肯定听说过什么堆内存和栈内存,当然,大家都是一脸懵,指针应该是清楚的吧?ok,那么我们会不会想到,这个指针变量是怎么在执行的时候找到的呢?

例如如下代码

int * a = (int*)malloc(sizeof(int))
int * b = a

我们假设我们的运行代码就是这个对吧,那么肯定运行第一行在运行第二行,通过计算机组成的学习我们知道,CPU运行的时候依靠的是寄存器和多级缓存来存储数据,寄存器是相当的少,虽然ARM有十几个,但是还是很少的呀,毕竟我们程序的变量名可不知十几个。

我们到第二行就很好奇是怎么执行的了对吧。

  1. 声明 b 和 b的类型
  2. 将a 的值给b 

这两个问好就是计算机帮助我们做的事情,是在加载程序到内存的时候做的事情,其实就是建立一个运行时候的符号表,这两个操作是记录符号表和读取符号表的过程,很显然我们要得到a的值,首先要得到a的地址,二次寻址吧,那么怎么得到地址呢,就好像查电话簿一样,查一下符号表就知道了。

 

啰嗦一大堆,这个有啥用?上面这个理论太重要了,这个就是传说中变量作用域的本质,为什么js会出现很多作用域问题,很大一部分原因来自于语这个script标签。我们都知道一个script标签引入一个js文件那么就会将这个js文件原封不动的引入当前全局作用域下,然后我们才能找到符号,调用,不然浏览器的console肯定会报异常的,找不到变量。这里js那个变量树我就不说了,说明白还得一个文章。

那么require 解决了什么问题,解决的一个问题就是作用域污染问题,也就是大家都瞎起名字,你叫jquery我也叫jquery,然后谁后被script标签弄进来谁生效,这tmd神逻辑,在团队开发的时候,就很容易有这个问题,当然不保证你自己一个人这么搞写个七八十个的页面的项目不会有什么问题。然后就有了命名空间的概念。这个命名空间的概念其实十分简单

 

所谓的命名空间其实就是作用域的扩展,这里必须画个图来说明了

其实就是这么个东西,namescope就是一个命名空间,那么里面肯定还有一个类似于作用域树的东西,在JS里面最常见的命名空间就是对象,这样就清楚多了吧,ok我们看我们的代码文件引用就能懂了,如果我们的scope3Value 和 nameValue 重名了,在JS这种解释语言里面,妥妥的认为最后一个被定义的才是最终的结果,那么其实就是script的引入先后决定了这个事情,最麻烦的事情来了,如果没有AMD/Common 规范,那么模块化咋做,就是全局开一个匿名函数,好像做了个对象,大家都是这么做的,这么做也是很坑的,避免了全局作用域污染,但是并不能避免全局作用域的变量覆盖呀,那么现在怎么办,require的定义就解决了这个问题

定义和使用的方案,看看就懂了啥意思了吧!

//my/shirt.js now has some dependencies, a cart and inventory
//module in the same directory as shirt.js
// 定义模块
define(["./cart", "./inventory"], function(cart, inventory) {
        //return an object to define the "my/shirt" module.
        return {
            color: "blue",
            size: "large",
            addToCart: function() {
                inventory.decrement(this);
                cart.add(this);
            }
        }
    }
);

// 使用模块
require("module/name").callSomeFunction()

其实requireJS就是解决了在后端开发很简单的import,还有代码逻辑拆分,说白了就是作用域问题,还有命名空间问题。

并且requireJS还带了什么特性,这玩意可以有选择加载代码呀,很简单看一下

if(hello = 1)
window.obj = require("module1")
else
window.obj = require("module2")

这个东西可以根据条件来引入不同的模块,这样就可以让我们的js有效的隐藏了,比如秒杀,不到时间我就不把业务逻辑require进来,当然大家能看懂的话还是能把代码弄下来,但是稍微做点限制,就下不来了,只有在开启的时候才能require下来。

好了,RequireJS解决的问题,就是这个棘手的作用域问题,好了现在我们有了RequireJS我们可以异步加载模块了,甚至可以ajax加载模块,没问题的,一切require一下。

 

Webpack 开始啦,希望我下面的各种逼逼,能让你们考试的时候不用背代码了,webpack干了啥会帮你们分析清楚的

 

首先我们先分析一下Webpack 能干什么,看上面的图就知道了,就是打包,把零碎的文件统一打包成为一套文件,打包到bundle中去,一个bundle其实就可以认为是一个程序库了。并且webpack还可以多入口多出口,同时并行打包各种东西,还支持loader其实就是一个制导翻译器的模型。

好了,最开始webpack就是用来把一群js合并成为一个js,那么webpack就好像一个仓库队列一样,一边收货转换一边发送出去,那么自然有两个口

  1. 入口
  2. 出口

但是我们会发现这个仓库队列,不仅仅就是收和发,后来有一帮人感觉img这些东西也很麻烦,算了直接转svg啥的把,webpack这个队列能加工了,就好像各种js,jpg,scss文件啥的都是生产资料,被一个机器加工了一下就被做成了,js css png 啥的类型,webpack 提供的就是队列缓冲,那么这个具体的机器叫做loader,就是用来转换的。

后来又有一群人觉得这个工厂应该可以定制,比如同时开多少条生产线,所以就有了plugins这个概念。

详细的概念webpack官网很清楚的,我就不多说了,我说一下我怎么看webpack的

我觉得这个东西把,其实就是个队列,当单入口的时候就是个阻塞队列,多个入口的时候,就是个交叉队列,或者叫并行队列组,主要处理的问题就是把import这些东西都转换到一个文件里去,也就我们用script标签一样,我们只不过把本地的js碎片组织成为了一个文件而已。

 

首先我们要知道,webpack第一件事就是从入口出发,然后遍历所有依赖生成依赖关系图的一个过程,怎么做的呢,这里圈重点,直接分析文件,然后把里面的import或者require都抽出来,并且进行分析,分析完了就开始遍历这些文件,数据结构算法请参考二叉树的层序遍历,文件依赖其实就是个DAG,虽然最开始会循环依赖,但是最后是有办法把他给hook掉的,变成DAG,变成DAG了以后只需要做深度搜索,就可以找到全部的依赖文件,并且能描述依赖关系。

所以我认为入口其实就是我们学过的数据结构图上的一个节点,而依赖关系图,就是一个最小生成树,这两个概念请好好学习数据结构,生成树的过程就是第一步,webapack 寻找依赖的过程。

寻找完了依赖,那么webpack就要开始执行转换工作了,其实就是DFS这颗最小生成树,然后走到每一个节点把文件里的内容读出来,然后按照文件后缀,使用某个配置好的规则来进行选择loader,所以loader就是第一次简单加工的过程这个过程出现在,遍历最小生成树的过程中。

我们先想到这,那么我们就能琢磨出来这个东西咋回事了,其实过程就是,先生成树,再遍历树,遍历过程使用loader转换源代码,让其可以成为module,我上面说了,成为module其实就是成为了一个作用域,那么最简单的办法就是创建一个对象,也就是(function(){})() 的使用,匿名闭包函数就是这么玩的。ok有没有发现loader其实很像一个预编译的过程

那么plugin是干啥的呢,说白了就是在出厂前对生成的东西再加工一把,当然这个过程是output之后完成的,那么webpack的工作过程如下

 

就是这样,并且webpack 支持多模块同时打包,也就是我们生成依赖图会有很多次,遍历也会跟多次,相当于webpack的运行时内部有一颗森林,plugins就是善后工作,并且webpack提供了多种api操作plugins的行为。

下面给出一个webpack原理的代码


const fs = require('fs');
const path = require('path');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
const {transformFromAst} = require('babel-core');

let ID = 0;


function createAsset(filename) {
  
  const content = fs.readFileSync(filename, 'utf-8');

  const ast = babylon.parse(content, {
    sourceType: 'module',
  });

  const dependencies = [];

  traverse(ast, {

    ImportDeclaration: ({node}) => {
      // We push the value that we import into the dependencies array.
      dependencies.push(node.source.value);
    },
  });

  const id = ID++;

  const {code} = transformFromAst(ast, null, {
    presets: ['env'],
  });

  // Return all information about this module.
  return {
    id,
    filename,
    dependencies,
    code,
  };
}

function createGraph(entry) {

  const mainAsset = createAsset(entry);

  const queue = [mainAsset];

  for (const asset of queue) {

    asset.mapping = {};

    const dirname = path.dirname(asset.filename);

    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)},
    ],`;
  });

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

const graph = createGraph('./example/entry.js');
const result = bundle(graph);

console.log(result);

代码逻辑就是我说的几步,第一作出依赖图,第二求最小生成树,第三遍历并且对每个节点转换,第四生成代码。

我在美团写后端的时候,有的时候会用webpack使用内部的loader和plugin来生成一些日志和数据,并且可视化埋点一般都是这么做的,webpack其实不神秘,无非运用了一些数据结构和算法,同时解决了一些工程问题,和语言特性问题,就是作用域控制。

另外,腾讯,美团,阿里,已经开始内推,需要直接投递,leemaster@outlook.cn :)

收藏 转发 评论