webpack

概念

webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle

面向过程开发

特征: 一锅乱炖 在早期 js 能力还非常有限的时候,我们通过面向过程的方式把代码写在同一个.js文件中,一个面向过程的开发模式可能如下所示。

<!-- index.html代码 -->
<p>这里是我们网页的内容</p>
<div id="root"></div>
<script src="./index.js"></script>
// index.js代码
var root = document.getElementById('root');

// header模块
var header = document.createElement('div');
header.innerText = 'header';
root.appendChild(header);

// sidebar模块
var sidebar = document.createElement('div');
sidebar.innerText = 'sidebar';
root.appendChild(sidebar);

// content模块
var content = document.createElement('div');
content.innerText = 'content';
root.appendChild(content);

面向对象开发

特征: 面向对象开发模式便于代码维护,深入人心。 随着 js 的不断发展,它所能解决的问题也越来越多,如果再像面向过程那样把所有代码写在同一个.js文件中,那么代码将变得非常难以理解和维护,此时面向对象开发模式便出现了,一个面向对象开发模式可能如下所示。

index.html中引入不同的模块:

<!-- index.html代码 -->
<p>这里是我们网页的内容</p>
<div id="root"></div>
<script src="./src/header.js"></script>
<script src="./src/sidebar.js"></script>
<script src="./src/content.js"></script>
<script src="./index.js"></script>

header.js代码:

// header.js代码
function Header() {
  var header = document.createElement('div');
  header.innerText = 'header';
  root.appendChild(header);
}

sidebar.js代码:

// sidebar.js代码
function Sidebar() {
  var sidebar = document.createElement('div');
  sidebar.innerText = 'sidebar';
  root.appendChild(sidebar);
}

content.js代码:

// content.js代码
function Content() {
  var content = document.createElement('div');
  content.innerText = 'content';
  root.appendChild(content);
}

index.js代码:

var root = document.getElementById('root');
new Header();
new Sidebar();
new Content();

不足: 以上的代码示例中,虽然使用面向对象开发模式解决了面向过程开发模式中的一些问题,但似乎又引入了一些新的问题。

  1. 每一个模块都需要引入一个.js文件,随着模块的增多,这会影响页面性能
  2. index.js文件中,并不能直接看出模块的逻辑关系,必须去页面才能找到
  3. index.html页面中,文件的引入顺序必须严格按顺序来引入,例如:index.js必须放在最后引入,如果把header.js文件放在index.js文件后引入,那么代码会报错

现代开发模式

特征: 模块化加载方案让前端开发进一步工程化 根据面向对象开发模式中的一系列问题,随后各种模块化加载的方案如雨后春笋,例如:ES ModuleAMDCMD以及CommonJS等,一个ES Module模块化加载方案可能如下所示。

index.html代码:

<!-- index.html代码 -->
<p>这里是我们网页的内容</p>
<div id="root"></div>
<script src="./index.js"></script>

header.js代码:

// header.js
export default function Header() {
  var root = document.getElementById('root');
  var header = document.createElement('div');
  header.innerText = 'header';
  root.appendChild(header);
}

sidebar.js代码:

// sidebar.js
export default function Sidebar() {
  var root = document.getElementById('root');
  var sidebar = document.createElement('div');
  sidebar.innerText = 'sidebar';
  root.appendChild(sidebar);
}

content.js代码:

// content.js代码
export default function Content() {
  var root = document.getElementById('root');
  var content = document.createElement('div');
  content.innerText = 'content';
  root.appendChild(content);
}

index.js代码:

// index.js代码
import Header from './src/header.js';
import Sidebar from './src/sidebar.js';
import Content from './src/content.js';

new Header();
new Sidebar();
new Content();

注意: 以上代码并不能直接在浏览器上执行,因为浏览器并不能直接识别ES Module代码,需要借助其他工具来进行翻译,此时 Webpack 就粉墨登场了。

webpack打包

不建议跟随此小结一起安装,此次示例仅仅作为一个例子,详细学习步骤请直接阅读下一章节安装open in new window

生成package.json文件

参数说明

-y参数表示直接生成默认配置项的package.json文件,不加此参数需要一步步按需进行配置。

$ npm init -y

生成的package.json文件:

{
  "name": "webpack-vuepress",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

安装Webpack

参数说明

-D参数代表在本项目下安装 Webpack ,它是--save-dev的简写

$ npm install webpack webpack-cli -D

修改代码

配置说明

webpack`默认打包路径到`dist`文件夹,打包后的`.js`文件名字叫`main.js

其他代码不动,将index.html中的.js文件改成如下引用方式(引用打包后的文件):

<!-- index.html代码 -->
<p>这里是我们网页的内容</p>
<div id="root"></div>
<script src="./dist/main.js"></script>

Webpack打包

参数说明

  1. npx webpack代表在本项目下寻找 Webpack 打包命令,它区别于npm命令
  2. index.js参数代表本次打包的入口是index.js
$ npx webpack index.js

打包结果: 效果

正如上面你所看到的那样,网页正确显示了我们期待的结果,这也是 Webpack 能为我们解决问题的一小部分能力,下面将正式开始介绍 Webpack 。

loader的编写

Loader

​ loader实际上是一个函数,实现对模块代码的出

npm init
npm install webpack webpack-cli loader-utils -D
|-- loaders
|   |-- replaceLoader.js
|   |-- replaceLoaderAsync.js
|-- src
|   |-- index.js
|-- package.json
|-- webpack.config.js
  • webpack.config.js (resolveLoader:loader引用时查找位置配置)

    const path = require("path");
    
    module.exports = {
      mode: "development",
      entry: {
        main: "./src/index.js",
      },
      // path.resolve(__dirname, "loaders/replaceLoaderAsync.js")
      resolveLoader: {
        modules: ["node_modules", "./loaders"],
      },
      output: {
        path: path.resolve(__dirname, "dist"),
        filename: "[name].js",
      },
      module: {
        rules: [
          {
            test: /\.js/,
            // loader执行顺序从右到左
            use: [
              {
                loader: "replaceLoader",
              },
              {
                loader: "replaceLoaderAsync",
                options: {
                  name: "Sumi",
                },
              },
            ],
          },
        ],
      },
    };
    
    
  • replaceLoader.js

    module.exports = function (source) {
      return source.replace("Sumi", "Enjoy");
    };
    
  • replaceLoaderAsync.js

    // loader-utils更方便获取到this   (options.name === this.query.name)
    const loaderUtils = require("loader-utils");
    
    module.exports = function (source) {
      const options = loaderUtils.getOptions(this);
      // this.async返回异步callback
      const callback = this.async();
    
      setTimeout(() => {
        const result = source.replace("Jerry", options.name);
        // callback返回更多数据
        callback(null, result);
      }, 2000);
    };
    
  • index.js: console.log("hello Jerry");

  • package.json

    {
      "scripts": {
        "build": "webpack"
      }
      ......
    }
    

plugin的编写

Plugin

​ plugin是一个类,在打包的hook时间点触发

​ 基于发布订阅模式设计,是事件驱动的插件机制

CopyrightWebpackPlugin

class CopyrightWebpackPlugin {
  constructor(options) {
    console.log(options);
  }
  // compiler: webpack的实例对象, 存放了配置和打包的所有内容(https://webpack.js.org/api/compiler-hooks/)
  apply(compiler) {
    // 同步hook由tap触发,函数没有callback参数
    compiler.hooks.compile.tap("CopyrightWebpackPlugin", (compilation) => {
      console.log("compile tab");
    });

    // compilation: 只存放这次打包的内容
    compiler.hooks.emit.tapAsync(
      "CopyrightWebpackPlugin",
      (compilation, cb) => {
        compilation.assets["copyright.txt"] = {
          source: function () {
            return "copyright by caffrey";
          },
          size: function () {
            return 20;
          },
        };
        cb();
      }
    );
  }
}

module.exports = CopyrightWebpackPlugin;
const path = require("path");
const CopyrightWebpackPlugin = require("./plugins/copyright-webpack-plugin");

module.exports = {
  mode: "development",
  entry: {
    main: "./src/index.js",
  },
  plugins: [
    new CopyrightWebpackPlugin({
      name: "Sumi",
    }),
  ],
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].js",
  },
};

node调试

"scripts": {
    "build": "webpack",
    "debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js"
},

TIP

​ 此时npm run debugnpm run build达到的效果是一样的,显示用node执行webpack.js可以允许外面传一些参数进去,例如--inspect--inspect-brk分别表示要开启调试在webpack第一行打一个断点

PS E:\webpack\plugin> npm run debug

> plugin@1.0.0 debug E:\webpack\plugin
> node --inspect --inspect-brk node_modules/webpack/bin/webpack.js

Debugger listening on ws://127.0.0.1:9229/cb8dee01-7e07-4eb5-823a-603ef027795c
For help, see: https://nodejs.org/en/docs/inspector

npm run debug后打开chrome浏览器控制台的node绿色按钮就可以看到调试过程

class CopyrightWebpackPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync(
      "CopyrightWebpackPlugin",
      (compilation, cb) => {
        // 断点调试
        debugger;
        compilation.assets["copyright.txt"] = {
          source: function () {
            return "copyright by caffrey";
          },
          size: function () {
            return 20;
          },
        };
        cb();
      }
    );
  }
}

module.exports = CopyrightWebpackPlugin;

bundler源码的编写

单个模块分析

@babel/parser 分析源代码, 生成AST

const ast = parser.parse(content, {
	sourceType: "module",
})

ast.program.body分析出抽象语法树

[
  Node {
    type: 'ImportDeclaration',
    start: 0,
    end: 35,
    loc: SourceLocation { start: [Position], end: [Position] },
    specifiers: [ [Node] ],
    source: Node {
      type: 'StringLiteral',
      start: 20,
      end: 34,
      loc: [SourceLocation],
      extra: [Object],
      value: './message.js'
    }
  },
  Node {
    type: 'ExpressionStatement',
    start: 39,
    end: 60,
    loc: SourceLocation { start: [Position], end: [Position] },
    expression: Node {
      type: 'CallExpression',
      start: 39,
      end: 59,
      loc: [SourceLocation],
      callee: [Node],
      arguments: [Array]
    }
  }
]

@babel/traverse 分析抽象语法树的节点

dependencies为依赖的路径数组

const dependencies = [];
traverse(ast, {
    ImportDeclaration({ node }) {
        console.log(node);
        dependencies.push(node.source.value);
    },
});
Node {
  type: 'ImportDeclaration',
  start: 0,
  end: 35,
  loc: SourceLocation {
    start: Position { line: 1, column: 0 },
    end: Position { line: 1, column: 35 }  
  },
  specifiers: [
    Node {
      type: 'ImportDefaultSpecifier',      
      start: 7,
      end: 14,
      loc: [SourceLocation],
      local: [Node]
    }
  ],
  source: Node {
    type: 'StringLiteral',
    start: 20,
    end: 34,
    loc: SourceLocation { start: [Position], end: [Position] },
    extra: { rawValue: './message.js', raw: '"./message.js"' },
    value: './message.js'
  }
}

路径转化

相对路径转化为绝对路径(或者是相对于根路径的相对路径)

const dirname = path.dirname(filename);
const newFile = "./" + path.join(dirname, node.source.value);

path.join(dirname, node.source.value)获取到绝对路径,加上./变成相对于bundler的相对路径

@babel/core ast转可执行代码

核心模块其中transformFromAst将ast转化为浏览器可以执行的代码,presets将ES6转化为ES5

const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"],
});
"use strict";

var _message = _interopRequireDefault(require("./message.js"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

console.log(_message["default"]);

全部模块分析

const makeDependenciesGraph = (entry) => {
    const entryModule = moduleAnalyser(entry);
    const graphArray = [entryModule];
    for (let i = 0; i < graphArray.length; i++) {
        const item = graphArray[i];
        const { dependencies } = item;
        if (dependencies) {
            for (let j in dependencies) {
                graphArray.push(moduleAnalyser(dependencies[j]));
            }
        }
    }
    const graph = {};
    graphArray.forEach((item) => {
        graph[item.filename] = {
            dependencies: item.dependencies,
            code: item.code,
        };
    });
    return graph
};
{
  './src/index.js': {
    dependencies: { './message.js': './src\\message.js' },
    code: '"use strict";\n' +
      '\n' +
      'var _message = _interopRequireDefault(require("./message.js"));\n' +
      '\n' +
      'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
      '\n' +
      'console.log(_message["default"]);'
  },
  './src\\message.js': {
    dependencies: { './word.js': './src\\word.js' },
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      '  value: true\n' +
      '});\n' +
      'exports["default"] = void 0;\n' +
      '\n' +
      'var _word = require("./word.js");\n' +
      '\n' +
      'var message = "say ".concat(_word.word);\n' +
      'var _default = message;\n' +
      'exports["default"] = _default;'
  },
  './src\\word.js': {
    dependencies: {},
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      '  value: true\n' +
      '});\n' +
      'exports.word = void 0;\n' +
      'var word = "hello";\n' +
      'exports.word = word;'
  }
}

生成代码

TIP

JSON.stringify(makeDependenciesGraph(entry))生成的依赖图谱里需要构造require方法和exports对象

const generateCode = (entry) => {
  const graph = JSON.stringify(makeDependenciesGraph(entry));
  return `
		(function(graph){
			function require(module) { 
				function localRequire(relativePath) {
					return require(graph[module].dependencies[relativePath]);
				}
				var exports = {};
				(function(require, exports, code){
					eval(code)
				})(localRequire, exports, graph[module].code);
				return exports;
			};
			require('${entry}')
		})(${graph});
	`;
};

完整代码

const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");

const moduleAnalyser = (filename) => {
  const content = fs.readFileSync(filename, "utf-8");
  const ast = parser.parse(content, {
    sourceType: "module",
  });
  const dependencies = {};
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(filename);
      const newFile = "./" + path.join(dirname, node.source.value);
      dependencies[node.source.value] = newFile;
    },
  });
  const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"],
  });
  return {
    filename,
    dependencies,
    code,
  };
};

const makeDependenciesGraph = (entry) => {
  const entryModule = moduleAnalyser(entry);
  const graphArray = [entryModule];
  for (let i = 0; i < graphArray.length; i++) {
    const item = graphArray[i];
    const { dependencies } = item;
    if (dependencies) {
      for (let j in dependencies) {
        graphArray.push(moduleAnalyser(dependencies[j]));
      }
    }
  }
  const graph = {};
  graphArray.forEach((item) => {
    graph[item.filename] = {
      dependencies: item.dependencies,
      code: item.code,
    };
  });
  return graph;
};

const generateCode = (entry) => {
  const graph = JSON.stringify(makeDependenciesGraph(entry));
  return `
		(function(graph){
			function require(module) { 
				function localRequire(relativePath) {
					return require(graph[module].dependencies[relativePath]);
				}
				var exports = {};
				(function(require, exports, code){
					eval(code)
				})(localRequire, exports, graph[module].code);
				return exports;
			};
			require('${entry}')
		})(${graph});
	`;
};

const code = generateCode("./src/index.js");

模块打包工具?

webpack最早是一个js的模块打包工具,但是现在,webpack实际上已经是一个模块打包工具

// commonjs
module.exports = ~
const ~ = require('src')

// ESModule
export default ~
import ~ from "src"

webpack moduleopen in new window

module methodsopen in new window

module variablesopen in new window

安装

全局安装

注意

如果你只是想做一个 Webpack 的 Demo 案例,那么全局安装方法可能会比较适合你。如果你是在实际生产开发中使用,那么推荐你使用本地安装方法。

webpack4.0+的版本,必须安装webpack-cli-g命令代表全局安装的意思

$ npm install webpack webpack-cli -g

卸载

参数说明

通过npm install安装的模块,对应的可通过npm uninstall进行卸载

$ npm uninstall webpack webpack-cli -g

本地安装(推荐)

参数说明

本地安装的Webpack意思是,只在你当前项目下有效。而通过全局安装的Webpack,如果两个项目的Webpack主版本不一致,则可能会造成其中一个项目无法正常打包。本地安装方式也是实际开发中推荐的一种Webpack安装方式。

$ npm install webpack webpack-cli -D 或者 npm install webpack webpack-cli --save-dev

版本号安装

参数说明

如果你对Webpack的具体版本有严格要求,那么可以先去github的Webpack仓库查看历史版本记录或者使用npm view webpack versions查看Webpacknpm历史版本记录

// 查看webpack的历史版本记录
$ npm view webpack versions

// 按版本号安装
$ npm install webpack@4.25.0 -D
上次更新:
贡献者: Ahon-pan