构建工具

374 阅读7分钟

定义

在前端的工作,为我们建立开发环境,产生生产资源的工具,即为构建工具。

问题

工具的产生一般是为了解决已有问题,或改善现状。

上面我们将构建工具做的工作分为两类,构建开发环境和构建生产资源。

在介绍构之前,抛开构建工具,我们先根据这两个方向,梳理下在工作中所面临的问题。

开发环境

  1. 无法在描述页面结构时,编辑逻辑
  2. 有可能遇到class命名冲突
  3. 有可能使用``未声明变量
  4. 使用``Javascript新特性
  5. 编写代码时,想要立刻看到效果,需要发布到服务器上
    1. 期望保留页面的状态(页面有表单信息,代码更改后即可以看到更新内容又保留表单信息)

生产资源

  1. css样式兼容前缀
  2. 压缩``html/css/js/图片
  3. 合并``css/js/图片
  4. javascript 中可能存在``非线上环境代码

前端同学在工作中有诸多的需求,依托于 node的发展,产生了很多的前端构建工具。 以上的问题,都得到了一定的解决。

工具

由于在不同场景下的缺陷,产生了诸多的前端构建工具。

这里根据在github创建时间,介绍下几个常见的构建工具。

Browserify - 2010

定义

Browserify是一个供浏览器环境使用的模块打包工具,像在node环境一样,也是通过require('modules')来组织模块之间的引用和依赖,既可以引用npm中的模块,也可以引用自己写的模块,然后打包成js文件,再在页面中通过 script 标签加载

特点

browserify 采用了 UNIX (尽量用简单的方式解决问题)的设计思想,它将所有的JS都打包成了一个文件。 在使用上,它是基于流的形式处理转换JS文件。

使用

const browserify = require('browserify')
const source = require('vinyl-source-stream')

const b = browserify({
  plugin: [
      [require('esmify')]  // 支持 esm
  ],
  entries: './index.js'
});

b.bundle()
  // 这种管道式的方式,让逻辑分离更加清晰
  .pipe(source('index.js')) // 用于适配 gulp
  .pipe(gulp.dest('./dist/js/'))

构建产物

import dep from './dep'

console.log(dep)
const dep = 'dep';

export default dep;

整体是一个立即执行函数 IIEF

(function() {
  function outer(modules, cache, entry) {
      // Save the require from previous bundle to this closure if any
      var previousRequire = typeof require == "function" && require;
  
      function newRequire(name, jumped){
          if(!cache[name]) {
              if(!modules[name]) {
                  // if we cannot find the module within our internal map or
                  // cache jump to the current global require ie. the last bundle
                  // that was added to the page.
                  var currentRequire = typeof require == "function" && require;
                  if (!jumped && currentRequire) return currentRequire(name, true);
  
                  // If there are other bundles on this page the require from the
                  // previous one is saved to 'previousRequire'. Repeat this as
                  // many times as there are bundles until the module is found or
                  // we exhaust the require chain.
                  if (previousRequire) return previousRequire(name, true);
                  var err = new Error('Cannot find module \'' + name + '\'');
                  err.code = 'MODULE_NOT_FOUND';
                  throw err;
              }
              var m = cache[name] = {exports:{}};
              modules[name][0].call(m.exports, function(x){
                  var id = modules[name][1][x];
                  return newRequire(id ? id : x);
              }, m, m.exports,outer,modules,cache,entry);
          }
          return cache[name].exports;
      }
      for(var i=0;i<entry.length;i++) newRequire(entry[i]);
  
      // Override the current require with this new one
      return newRequire;
  }
  
  return outer;
})()({
  1: [function (require, module, exports) {
    "use strict";

    Object.defineProperty(exports, "__esModule", {
      value: true
    });
    exports.default = void 0;
    const dep = 'dep';
    var _default = dep;
    exports.default = _default;
  }, {}],
  2: [function (require, module, exports) {
    "use strict";

    var _dep = _interopRequireDefault(require("./dep"));

    function _interopRequireDefault(obj) {
      return obj && obj.__esModule ? obj : {
        default: obj
      };
    }
    console.log(_dep.default);
  }, {
    "./dep": 1
  }]
}, {}, [2])

局限

browserify将所有资源都打包成一个JS文件。由于浏览器网络的限制,加载资源都一些最佳实践。一般将不变的资源持久缓存,变的资源不做缓存,browserify 的方式不适合较大的项目。

Webpack - 2012

定义

webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。

特点

内部提供了多种前端模块方案esmcommonjsAMD 不同于流式的处理方式(xxx.pipe(parse1()).pipe(parse2())),webpack是以配置化的形式去处理资源。

webpack 是静态资源打包器,以加载器的形式处理资源,配合plugin的作用去影响,将零散的模块聚合拆分成指定的模块。

使用

{
  "type": "module",
  "scripts": {
    "webpack": "webpack"
  },
  "devDependencies": {
    "rollup": "^3.3.0"
    "webpack": "^5.75.0",
    "webpack-cli": "^5.0.0"
  }
}

import path, { dirname } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));

export default {
  entry: './index.js',
  mode: 'production',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },
};

构建产物

(() => {
  "use strict";
  console.log("dep");
})();

局限

配置化较为繁琐,文档说明不够友好。相对于流式的处理方式,流式的方式编程感更强,而webpack配置化的形式,让人摸不到头脑。

Rollup - 2015

定义

Rollup 是一个 JavaScript 模块打包工具,可以将多个小的代码片段编译为完整的库和应用。与传统的 CommonJS 和 AMD 这一类非标准化的解决方案不同,Rollup 使用的是 ES6 版本 Javascript 中的模块标准。

特点

Rollup 使用的是 ES6 版本 Javascript 中的模块标准,对代码可以做到静态分析,构建出来的包体积更小,Tree Shaking

因为以上特点,Rollup常用于打包npm包。React/Vue就是采用Rollup进行打包的。

使用

{
  "type": "module",
  "scripts": {
    "rollup": "rollup --config rollup.config.js"
  },
  "devDependencies": {
    "rollup": "^3.3.0"
  }
}
export default {
  input: "./index.js",
  output: [
    {
      file: "dist/bundle.js",
      format: "es",
    },
  ],
};

构建产物

const dep = 'dep';

console.log(dep);

局限

rollup 在对web应用的打包构建方便,生态对比webpack相对薄弱,更适合对Javascript的处理。这可能跟Rollup起因有关,他设计的初衷也处理Javascript

Parcel - 2017

定义

极速零配置Web应用打包工具

特点

零配置。例如项目中使用了scss,则仅需要安装install sass这个安装包即可。

使用

运行parcel build <your entry file>

构建产物

demo 同 browserify 示例一样。

// modules are defined as an array
// [ module function, map of requires ]
//
// map of requires is short require name -> numeric require
//
// anything defined in a previous bundle is accessed via the
// orig method which is the require for previous bundles
parcelRequire = (function (modules, cache, entry, globalName) {
  // Save the require from previous bundle to this closure if any
  var previousRequire = typeof parcelRequire === "function" && parcelRequire;
  var nodeRequire = typeof require === "function" && require;

  function newRequire(name, jumped) {
    if (!cache[name]) {
      if (!modules[name]) {
        // if we cannot find the module within our internal map or
        // cache jump to the current global require ie. the last bundle
        // that was added to the page.
        var currentRequire =
          typeof parcelRequire === "function" && parcelRequire;
        if (!jumped && currentRequire) {
          return currentRequire(name, true);
        }

        // If there are other bundles on this page the require from the
        // previous one is saved to 'previousRequire'. Repeat this as
        // many times as there are bundles until the module is found or
        // we exhaust the require chain.
        if (previousRequire) {
          return previousRequire(name, true);
        }

        // Try the node require function if it exists.
        if (nodeRequire && typeof name === "string") {
          return nodeRequire(name);
        }

        var err = new Error("Cannot find module '" + name + "'");
        err.code = "MODULE_NOT_FOUND";
        throw err;
      }

      localRequire.resolve = resolve;
      localRequire.cache = {};

      var module = (cache[name] = new newRequire.Module(name));

      modules[name][0].call(
        module.exports,
        localRequire,
        module,
        module.exports,
        this
      );
    }

    return cache[name].exports;

    function localRequire(x) {
      return newRequire(localRequire.resolve(x));
    }

    function resolve(x) {
      return modules[name][1][x] || x;
    }
  }

  function Module(moduleName) {
    this.id = moduleName;
    this.bundle = newRequire;
    this.exports = {};
  }

  newRequire.isParcelRequire = true;
  newRequire.Module = Module;
  newRequire.modules = modules;
  newRequire.cache = cache;
  newRequire.parent = previousRequire;
  newRequire.register = function (id, exports) {
    modules[id] = [
      function (require, module) {
        module.exports = exports;
      },
      {},
    ];
  };

  var error;
  for (var i = 0; i < entry.length; i++) {
    try {
      newRequire(entry[i]);
    } catch (e) {
      // Save first error but execute all entries
      if (!error) {
        error = e;
      }
    }
  }

  if (entry.length) {
    // Expose entry point to Node, AMD or browser globals
    // Based on https://github.com/ForbesLindesay/umd/blob/master/template.js
    var mainExports = newRequire(entry[entry.length - 1]);

    // CommonJS
    if (typeof exports === "object" && typeof module !== "undefined") {
      module.exports = mainExports;

      // RequireJS
    } else if (typeof define === "function" && define.amd) {
      define(function () {
        return mainExports;
      });

      // <script>
    } else if (globalName) {
      this[globalName] = mainExports;
    }
  }

  // Override the current require with this new one
  parcelRequire = newRequire;

  if (error) {
    // throw error from earlier, _after updating parcelRequire_
    throw error;
  }

  return newRequire;
})(
  {
    f6ii: [
      function (require, module, exports) {
        "use strict";

        Object.defineProperty(exports, "__esModule", {
          value: true,
        });
        exports.default = void 0;
        var dep = "dep";
        var _default = dep;
        exports.default = _default;
      },
      {},
    ],
    eHzx: [function (require, module, exports) {}, {}],
    Focm: [
      function (require, module, exports) {
        "use strict";

        var _dep = _interopRequireDefault(require("./dep"));
        require("./index.scss");
        function _interopRequireDefault(obj) {
          return obj && obj.__esModule ? obj : { default: obj };
        }
        console.log(_dep.default);
      },
      { "./dep": "f6ii", "./index.scss": "eHzx" },
    ],
  },
  {},
  ["Focm"],
  null
);

局限

文档较为简单,使用不方便;缺乏定制能力;用户群体较少,社区力量薄弱。

小结

目前webpack还是最主流的前端构建工具,下载量稳居第一。上面提到的构建工具,抛去Browserify以外,都保持着较高的更新频率。 更详细报告

webpack和vite

这一块主要介绍下在项目中,使用较多的两个构建工具的实现原理。

webpack

核心知识点

  1. tapable

发布订阅的库,可以为插件提供钩子。不同于一般的事件中心的方式,通过new Function(xxx) 动态创建函数,去串联每一个回调函数。

  1. compiler

webpack 中的编译管理器。

  1. module

在 webpack 中用于描述资源模块的类。

流程

构建流程图

小结

webpack将整个构建的过程分成了不同的阶段,即在 Compiler 这个构建对象,注册了不同阶段的 钩子。 根据Entry将整个应用的依赖关系转化为相应的数据结构 Module 形式。 数据开始经过 Compiler 每一个阶段的 钩子 , 这其中都会受到 loaderplugin的影响,最终将数据结构转为真实的文件。

vite

核心知识点

  1. ESM

可以将 Javascript 程序拆分为可按需导入的单独模块。

  1. transformRequest

vite 根据 import xxx from 'xxx' 加载不同的资源是,返回给前端可执行JS

  1. esbuild

极速的 Javascript 打包器。

流程

开发启动流程图 image.png

小结

vite 利用 esm 的方式,动态的去加载开发资源,而不是将整个工程预先打包,做到快速启动的目的。 搭配 esbuild 高性能,可以快速的处理相应按需加载的资源,避免了因按需加载的延迟影响。

esbuild和swc拓展

由于Javascript语言本身JIT(Just In Time 运行时编译)的限制,用AOT(Ahead Of Tim 运行前编译)类型的语言更高效。

esbuild

极速的 Javascript 打包器。 image.png

swc

SWC 可用于编译和捆绑。 image.png

思考

  1. 产出的工具一般都是用来解决问题,所以我们要更加明确问题的本质。
  2. 工具在发展过程中,都吸取了历史经验,并作出优化。我们可以总结出两个方向:
    1. 增加
      1. 增加缓存
      2. 增加进程
      3. 增加速度(物理文件转换为内存)
      4. ...
    2. 减少
      1. 减少范围
      2. 减少体积
      3. 减少数量
      4. ...
  3. 多关注新知识