webpack

134 阅读6分钟

什么是 webpack ?


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


webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。


webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 -- 深入浅出 webpack 吴浩麟

\

image.png

webpack 核心概念


Entry


入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。
进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
每个依赖项随即被处理,最后输出到称之为 bundles 的文件中。\

Output

output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist。

基本上,整个应

用程序结构,都会被编译到你指定的输出路径的文件夹中。

Module

模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。

Chunk

代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。\

Loader


loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。
loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。

Plugin

loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。

插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。

webpack 构建流程
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程 :\

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
  3. 确定入口:根据配置中的 entry 找出所有的入口文件。
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
  5. 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。


实践加深理解,撸一个简易 webpack compiler 钩子 Compiler 模块是 webpack 的支柱引擎,它通过 CLI 或 Node API 传递的所有选项,创建出一个 compilation 实例。它扩展(extend)自 Tapable 类,以便注册和调用插件。大多数面向用户的插件首,会先在 Compiler 上注册。

此模块会暴露在 webpack.Compiler,可以直接通过这种方式使用。关于更多信息,请查看这个示例。

监听(watching) Compiler 支持可以监控文件系统的 监听(watching) 机制,并且在文件修改时重新编译。当处于监听模式(watch mode)时,compiler 会触发诸如 watchRun, watchClose 和invalid 等额外的事件。通常用于 开发环境 中使用,也常常会在 webpack-dev-server 这些工具的底层之下调用,由此开发人员无须每次都使用手动方式重新编译。还可以通过 CLI 进入监听模式。

相关钩子 以下生命周期钩子函数,是由 compiler 暴露,可以通过如下方式访问:

compiler.hooks.someHook.tap(...) 1 取决于不同的钩子类型,也可以在某些钩子上访问 tapAsync 和 tapPromise。

这里我们使用 emit 钩子 (生成资源到 output 目录之前。)

remove-comments-plugin.js

class RemoveCommentsPlugin { apply(compiler) { compiler.hooks.emit.tap('RemoveCommentsPlugin', compilation => { // compilation 可以理解为此次打包的上下文 // 使用 compilation 对象的 assets 属性可以获取文件名称信息 for( const name in compilation.assets) { // console.log(name);

            // 通过 source() 方法,获取文件中的内容
            // console.log(compilation.assets[name].source());

            // 处理 .js 文件中的注释,先判断获取 .js 文件,使用 endsWith() 方法
            if (name.endsWith('.js')) {
                // 定义source()方法
                const contents = compilation.assets[name].source()
                // 使用正则替换掉注释
                const noComments = contents.replace(/\/\*{2,}\/\s?/g,'')
                // 替换后覆盖掉原内容,根据 webpack 格式要求,暴露以下方法
                // 暴露 source() 方法,返回新的内容
                // 暴露 size() 方法,返回新内容大小
                compilation.assets[name]= {
                    source: () => noComments,
                    size: () => noComments.length
                }
            }
        }
    })
}

}

module.exports = RemoveCommentsPlugin

在 webpack.config.js 中引入插件 webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin');
const path = require('path');
const RemoveCommentsPlugin = require('./remove-comments-plugin') // 自定义插件

module.exports = {
    entry: './path/to/my/entry/file.js',
    output: {
        /**
         * With zero configuration,
         *   clean-webpack-plugin will remove files inside the directory below
         * 	 clean-webpack-plugin将删除下面目录中的文件
         */
        path: path.resolve(process.cwd(), 'dist'),
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                loader: 'babel-loader',
            },
        ],
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            title: 'My App', // 标题
            filename: 'index.html', // 输出文件名
        }),
        new CopyWebpackPlugin({
        	patterns: [
				{ from: "source", to: "dest" }, 
        		{ from: "other", to: "public" }, 
			],
		}),
		new RemoveCommentsPlugin() 
    ],
};
class Compiler {
 constructor(options) {
 // webpack 配置
 const { entry, output } = options
 // 入口
 this.entry = entry
 // 出口
 this.output = output
 // 模块
 this.modules = []
  }
 // 构建启动
  run() {
 // 解析入口文件
 const info = this.build(this.entry)
 this.modules.push(info)
 this.modules.forEach(({ dependecies }) => {
 // 判断有依赖对象,递归解析所有依赖项
 if (dependecies) {
 for (const dependency in dependecies) {
 this.modules.push(this.build(dependecies[dependency]))
        }
      }
    })
 // 生成依赖关系图
 const dependencyGraph = this.modules.reduce(
 (graph, item) => ({
        ...graph,
 // 使用文件路径作为每个模块的唯一标识符,保存对应模块的依赖对象和文件内容
        [item.filename]: {
 dependecies: item.dependecies,
 code: item.code
        }
      }),
      {}
    )
  }
  build(filename) {
 const { getAst, getDependecies, getCode } = Parser
 const ast = getAst(filename)
 const dependecies = getDependecies(ast, filename)
 const code = getCode(ast)
 return {
 // 文件路径,可以作为每个模块的唯一标识符
      filename,
 // 依赖对象,保存着依赖模块路径
      dependecies,
 // 文件内容
      code
    }
  }
 // 重写 require函数,输出bundle
  generate() {}
}

new Compiler(options).run()

重写 require 函数,输出 bundle

const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')

const Parser = {
 getAst: path => {
 // 读取入口文件
 const content = fs.readFileSync(path, 'utf-8')
 // 将文件内容转为AST抽象语法树
 return parser.parse(content, {
 sourceType: 'module'
    })
  },
 getDependecies: (ast, filename) => {
 const dependecies = {}
 // 遍历所有的 import 模块,存入dependecies
    traverse(ast, {
 // 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
      ImportDeclaration({ node }) {
 const dirname = path.dirname(filename)
 // 保存依赖模块路径,之后生成依赖关系图需要用到
 const filepath = './' + path.join(dirname, node.source.value)
        dependecies[node.source.value] = filepath
      }
    })
 return dependecies
  },
 getCode: ast => {
 // AST转换为code
 const { code } = transformFromAst(ast, null, {
 presets: ['@babel/preset-env']
    })
 return code
  }
}

class Compiler {
 constructor(options) {
 // webpack 配置
 const { entry, output } = options
 // 入口
 this.entry = entry
 // 出口
 this.output = output
 // 模块
 this.modules = []
  }
 // 构建启动
  run() {
 // 解析入口文件
 const info = this.build(this.entry)
 this.modules.push(info)
 this.modules.forEach(({ dependecies }) => {
 // 判断有依赖对象,递归解析所有依赖项
 if (dependecies) {
 for (const dependency in dependecies) {
 this.modules.push(this.build(dependecies[dependency]))
        }
      }
    })
 // 生成依赖关系图
 const dependencyGraph = this.modules.reduce(
 (graph, item) => ({
        ...graph,
 // 使用文件路径作为每个模块的唯一标识符,保存对应模块的依赖对象和文件内容
        [item.filename]: {
 dependecies: item.dependecies,
 code: item.code
        }
      }),
      {}
    )
 this.generate(dependencyGraph)
  }
  build(filename) {
 const { getAst, getDependecies, getCode } = Parser
 const ast = getAst(filename)
 const dependecies = getDependecies(ast, filename)
 const code = getCode(ast)
 return {
 // 文件路径,可以作为每个模块的唯一标识符
      filename,
 // 依赖对象,保存着依赖模块路径
      dependecies,
 // 文件内容
      code
    }
  }
 // 重写 require函数 (浏览器不能识别commonjs语法),输出bundle
  generate(code) {
 // 输出文件路径
 const filePath = path.join(this.output.path, this.output.filename)
 // 懵逼了吗? 没事,下一节我们捋一捋
 const bundle = `(function(graph){
      function require(module){
        function localRequire(relativePath){
          return require(graph[module].dependecies[relativePath])
        }
        var exports = {};
        (function(require,exports,code){
          eval(code)
        })(localRequire,exports,graph[module].code);
        return exports;
      }
      require('${this.entry}')
    })(${JSON.stringify(code)})`

 // 把文件内容写入到文件系统
    fs.writeFileSync(filePath, bundle, 'utf-8')
  }
}

new Compiler(options).run()