Webpack原理(3) — 核心概念

by Teobler on 22 / 02 / 2020

views

Entry

我们先来看一张图

webpack-entry

从这张图可以看到,最上面的文件就是我们整个app的入口,也是这个文件启动了我们整个app,这就是weback的入口,通常这个文件会依赖我们自己app的其他文件,其他文件又会依赖别的第三方库,这些依赖可能是js,也可能是css,当然右边也展示了我们也会依赖app里面的其他文件。

在webpack的config文件中,我们使用entry字段来设置这个入口:

module.exports = {
	...
	entry: "./main.js",
	...
}

一句话来总结就是

Entry tells webpack WHAT(files) to load for the browser

Output

webpack-output

图片中入口文件下方的是入口文件的依赖,上方就是bundle之后的输出,同样的,我们在config文件中可以通过output这个字段来设置相应的配置项:

module.exports = {
	...
	output: {
		path: "./dist",
		filename: "./bundle.js",
	},
	...
}

这个配置就是在告诉webpack编译后的文件应该放在哪里,文件名应该叫什么,一句话总结

Output tells webpack WHERE and HOW to discribute bundles. It works with Entry.

Loaders & Rules

需要明白的是,loaders都是一些JS的modules,也就是说都是一些JS的方法(functions),他们以你app的module作为输入,返回一个修改后的状态,这些loaders会在webpack建立依赖图的时候对每一个文件进行相应的处理:

webpack-loaders

比如第一个ts-loader就是在说告诉webpack,任何时候你想要把一个ts文件放入依赖图中,就用ts-loader处理一次,处理过后这个文件就被编译成了js文件,当然,可能这个文件也有别的依赖,会按照相同的方式依次进行处理。

通常这个字段接收这些参数:

module.exports = {
	rules: [
		{
			// 告诉编译器要编译哪些文件
			test: regex,
			// loader(s)
			use: (Array | String | Function),
			// 白名单
			include: RegExp[],
			// 黑名单
			exclude: RegExp[],
			// 告诉webpack这条规则是否在其他规则 之前 | 之后 运行
			enforce: "pre" | "post"
		},
	]
}

Chaining Loaders

在上面的介绍中可以看到use字段是可以传入一个数组的,比如["style", "css", "less"]但是需要指出的是,这三个loaders是按照从右到左的的顺序来执行的,这个规则将使一个less文件编译成一个css文件,再由css编译成js文件,最后编译成一个能够在浏览器中运行的inline style的js文件。

loaders tells webpack HOW to interpret and translate files. Transformed on a per-file basis before adding to dependency graph

Plugin

对于webpack来说,插件是什么:

  • 一个对象,这个对象上有一个apply属性
  • 允许你在编译的生命周期里做一些事情(hook)
  • webpack有各种各样的内置插件

一个简单的例子,编译器用这个插件分发事件:

webpack-plugin

这个插件被作为一个实例传入了webpack,所以它可以hook进不同的事件中(这里的代码是webpack3的,因为这里只讲概念,所以问题不大)。

首先这个插件插入编译器后悔监听done这个事件,这个事件会给这个插件传入一个参数,这里是一个state然后插件在这个state的基础上做出一些反应和处理(这里只是在控制台里输出了一个字符),下面的也是同样的,不过这次这个事件换成了failed

可以看到插件的定义是这样的,所以它可以被实例化,于是在webpack的config文件中我们就会这样去使用它:

const BellOnBundlerErrorPlugin = require("bell-on-error");
const webpack = require("webpack");
 
module.exports = {
	...
	plugins: [
			new BellOnBundlerErrorPlugin(),
			new webpack.optimize.CommonsChunkPlugin("vendors"),
	],
	...
}

有意思的是,webpack的源代码有80%左右都是由plugins组成的,webpack是一个完全由事件驱动的体系结构,这也就可以使用插件迅速为webpack增添新的功能,并且不会破坏原有的功能,同样的也能够删除一些不必要的功能。

Adds additional functionality to Compilations(optimized bundled modules). More powerful more access to CompilerAPI. Does everything else you'd ever want to in webpack.

plugin与loader的最大不同就是loader是通过去访问一个个文件去做一些事情的,但是plugin可以访问webpack的事件生命周期和运行时,并且可以访问所有bundle文件。

示例

讲了这么多我们总得用概念做点什么

以下代码位于 feature/04010-composing-configs-webpack-merge 分支

读取命令行中的参数

我们可以将package.json中的命令做一个修改,传入一个env变量,带有一个mode属性:

"script" {
	"webpack": "webpack",
	"dev": "npm run webpack -- --env.mode development"
}

然后在webpack.config.js中我们可以这样获取到这个变量:

module.exports = env => {
	console.log(env);
	// { mode: "development" }
	
	return {
		mode: env.mode,
		output: {
			filename: "bundle.js",
		}
	}
}

加插件

我们这里以一个很通用并且很重要的插件html-webpack-plugin为例

首先用yarn add html-webpack-plugin —dev安装插件,然后在config文件中配置:

const HtmlWebpackPlugin = require("html-webpack-plugin");
 
module.exports = env => {
	console.log(env);
	// { mode: "development" }
	
	return {
		mode: env.mode,
		output: {
			filename: "bundle.js",
		},
		plugins: [new HtmlWebpackPlugin()],
	}
}

之后再运行yarn dev,你会发现bundle里面多了一个index.html文件,文件中用script标签把已经打包好的JS代码进行了引入。

设置本地开发服务

首先安装server插件yarn add webpack-dev-server --dev,然后修改package.json:

"script" {
	"webpack-dev-server": "webpack-dev-server",
	"dev": "npm run webpack-dev-server -- --mode development"
}

之后当你运行yarn dev的时候就会默认在本地8080端口启动一个server,在浏览器访问这个端口你就可以看见刚刚生成的html文件了,而且dev server默认开启了watch模式,可以实时更新代码到server上。

其实大家也能够猜到这个”插件“其实就是启动了一个Express的server,然后webpack在打包的时候直接将生成的文件加载进内存,通过server直接显示在浏览器里。

为不同的环境设置不同的config文件

我们可以像代码库中一样通过简单的配置使得webpack在得到不同参数的情况下去require不同的webpack config文件,以达到区分dev环境和prod环境的目的:

const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const webpackMerge = require("webpack-merge");
 
const modeConfig = env => require(`./build-utils/webpack.${env}`)(env);
 
module.exports = ({ mode, presets } = { mode: "production", presets: [] }) => {
  return webpackMerge(
    {
      mode,
      output: {
        filename: "bundle.js"
      },
      plugins: [new HtmlWebpackPlugin(), new webpack.ProgressPlugin()]
    },
    modeConfig(mode)
  );
};

但是实际的项目往往要复杂得多,一个大型项目往往不止有两个环境,通常来说development mode下应该有CI Dev QA UAT四个环境,production mode对应Prod环境。

由于在开发时不同的环境又对应不同的config,比如在Dev坏境应该去请求后端的Dev坏境,而不应该去请求QA坏境,这时我们又会引入不同的config文件去实现这一点。

首先安装config这个包,,然后在根目录下新建一个config目录,新建你需要的config文件,这些文件都是JSON格式的,记得建一个default.json,当其找不到对应的坏境时就会默认读取default文件,文件可以长这个样子:

// default.json
{
  "API_BASE_URL": "https://dev.your-project.com",
  "AUTH_URL": "https://dev.your-project.com/auth",
  "NODE_ENV": "dev"
}
 
//qa.json
{
  "API_BASE_URL": "https://qa.your-project.com",
  "AUTH_URL": "https://qa.your-project.com/auth",
  "NODE_ENV": "qa"
}

然后在运行启动命令的时候加上NODE_ENV这个参数,这样你在webpack的config文件中就可以通过process.env.NODE_ENV去获取这个参数,并且在config文件中可以将之前的config文件require进来在代码中进行使用:

const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const webpackMerge = require("webpack-merge");
 
const modeConfig = env => require(`./build-utils/webpack.${env}`)(env);
const envConfig = JSON.stringify(require("config"));
 
module.exports = ({ mode, presets } = { mode: "production", presets: [] }) => {
  return webpackMerge(
    {
      mode,
      output: {
        filename: "bundle.js"
      },
      plugins: [
        new HtmlWebpackPlugin(), 
        new webpack.ProgressPlugin(),
        new webpack.DefinePlugin({ CONFIG: envConfig }),
      ]
    },
    modeConfig(mode)
  );
};
 
// axios.js
const client = axios.create({
  baseURL: CONFIG.API_BASE_URL,
  timeout: 10000,
});