webpack

webpack前

先来聊聊模块化.

什么是 webpack?

一种打包工具!

在 webpack 的机制里, 所有的资源都是模块, 任何模块都是javascript模块! 报错 js, css, 图片等, 且可以通过代码分割的方法异步加载.
webpack 将模块及其依赖打包生成静态资源.

它做了什么?

分析你的项目结构, 找到 JavaScript 模块以及其他的浏览器不能直接运行的拓展语言(Scss, TypeScript 等), 将其转换和打包为适合的格式供浏览器使用.

特点

代码拆分:

Webpack 有 同步异步 两种模块依赖的方式. 异步依赖作为一个分割点, 形成一个新的块. 优化依赖树后, 每一个异步区块作为一个文件被打包.

Loader:

loader 转换器帮助 webpack 处理各种类型的资源, 将他们转换成 JavaScript 模块.

智能解析:

Webpack 有一个智能解析器, 几乎可以处理任何第三方库.

插件系统:

Webpack 大多数内容功能都是基于这个插件系统运行的, 还可以开发和使用开源的 Webpack 插件.

快速运行:

Webpack 使用异步 I/O 和多级缓存提高运行效率.


一个配置文件的栗子🌰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149

const {
DefinePlugin,
/* 修改模块 id 为 hash 值, 而非自增的数字 id */
HashedModuleIdsPlugin,
optimize: { UglifyJsPlugin }
} = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');

// const WebpackChunkHash = require("webpack-chunk-hash");
const HashedChunkIdsPlugin = require('@58qf/hashed-chunkid-webpack-plugin');
// const ChunkManifestPlugin = require('chunk-manifest-webpack-plugin');
//const VisualizerPlugin = require('webpack-visualizer-plugin');

/* 抽取本应包含在 Entry Chunk 中的 manifest 对象为单独的 json 文件,
只在使用到了异步代码块时候使用到 manifest 对象 */
const AssetsPlugin = require('assets-webpack-plugin');
// const AssetsPlugin = require('@58qf/assets-chunkmanifest-webpack-plugin');

const Merge = require('webpack-merge');
const CommonConfig = require('./webpack.common.js');
const { readdirSync } = require('fs');
const { resolve } = require('path');
let { entryDir, publicPath, host } = require('../config');

const prodConfig = function (env = 'test') {
const isProd = env === 'production';
let entry = readdirSync(entryDir).filter(element => {
return element.indexOf(".") === -1;
})
.reduce((entries, el) => {
entries[el] = `./entries/${el}`;
return entries;
}, {});

let plugins = [
new ExtractTextPlugin({
filename: `css/[name]${isProd ? '_[contenthash:8]' : ''}.css`,
allChunks: true
}),
new DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify(env)
}
}),
// new AssetsPlugin({
// path: resolve('dist'),
// metadata: { publicPath, version: Date.now() }
// }),
new HashedModuleIdsPlugin(),
new HashedChunkIdsPlugin(),
// new WebpackChunkHash(),
// new ChunkManifestPlugin({
// filename: 'manifest.json',
// manifestVariable: 'webpackManifest',
// }),
new UglifyJsPlugin({
beautify: false,
mangle: {
screw_ie8: true,
keep_fnames: true
},
compress: {
screw_ie8: true,
warnings: false,
drop_console: isProd,
},
sourceMap: true,
comments: false,
}),
new CopyWebpackPlugin([
{
from: '../static/images',
to: `images/[name].[ext]`
},
{
from: '../static/scripts',
to: `scripts/[name].[ext]`
},
{
from: '../static/views',
to: `views/[name].[ext]`
},
{
from: '../static/res',
to: `res/`
},
]),
];

if (isProd) {
publicPath = host + publicPath;
plugins.push(new AssetsPlugin({
path: resolve('build'),
filename: 'assets-manifest.json',
includeManifest: 'runtime',
update: true,
prettyPrint: true,
metadata: { publicPath, version: Date.now() }
}));
}

return Merge(CommonConfig, {
devtool: 'source-map',
entry,
output: {
path: resolve('dist'),
publicPath,
pathinfo: !isProd,
chunkFilename: `scripts/[name]${isProd ? '_[chunkhash:8]' : ''}.js`,
filename: `scripts/[name]${isProd ? '_[chunkhash:8]' : ''}.js`,
sourceMapFilename: 'sourcemaps/[name].js.map'
},
module: {
rules: [
{
test: /\.vue$/,
use: {
loader: 'vue-loader',
options: {
extractCSS: true
}
}
},
{
test: /\.(css|scss)$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', 'sass-loader']
})
},
{
test: /\.(jpg|png|gif|woff|woff2|eot|ttf|svg)$/,
use: {
loader: 'url-loader',
options: {
limit: 1024,
name: `images/[name]${isProd ? '_[hash:8]' : ''}.[ext]`
}
}
}
]
},
plugins,
});
};

module.exports = prodConfig;

入口起点

单文件入口
1
2
3
4
const config = {
entry: './src/main.js'
};
module.exports = config
多文件入口

entry 的值可以是字符串, 数组, 以及对象形式
对象形式的多文件入口

1
2
3
4
5
6
7
const config = {
entry: {
app1: './src/app1.js',
app2: './src/app2.js'
}
};
module.exports = config;

输出

入口可以存在多个入口起点, 但只指定一个输出配置.
单文件输出:

1
2
3
4
5
6
7
8
const path = require('path');
const config = {
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
}
};
module.exports = config;

filename 和 path 是 output 最基本的配置.

其他配置还有:
chunkFilename, 给 chunkfile 的命名, chunkfile 指不是入口文件, 但又由于一些原因(大多是异步的原因)而单独生成了 chunk.
publicPath, 用于控制打包文件的相对或者绝对引用路径.

多文件输出: 使用占位符

1
2
3
4
5
6
7
8
9
10
11
12
const path = require('path');
const config = {
entry: {
app1: './src/app1.js',
app2: './src/app2.js'
},
output: {
filename: '[name].bundle.js',
path: path.join(__dirname, 'dist')
}
};
module.exports = config;

webpack 中常见的占位符有多种,常见的如下:
[name] :代表打包后文件的名称,在entry 或代码中(之后会看到)确定;

[id] :webpack 给块分配的内部chunk id ,如果你没有隐藏,你能在打包后的命令行中看到;

[hash] :每次构建过程中,生成的唯一 hash 值;

[chunkhash] : 依据于打包生成文件内容的 hash 值,内容不变,值不变;

[ext] : 资源扩展名,如js ,jsx ,png 等等;

代码块 (Chunk)

文件级别的代码块, Chunk 大致分为三类

Entry Chunk

入口代码块包含了 webpack 运行时需要的一些函数,如 webpackJsonp, webpack_require 等以及依赖的一系列模块。

Normal Chunk

普通代码块没有包含运行时需要的代码,只包含模块代码,其结构有加载方式决定,如基于 CommonJs 异步的方式可能会包含 webpackJsonp 的调用。

Initial Chunk

与入口代码块对应的一个概念是入口模块(module 0),如果入口代码块中包含了入口模块 webpack 会立即执行这个模块,否则会等待包含入口模块的代码块,包含入口模块的代码块其实就是 initial chunk。

插件中最为特别的一个插件应该是 CommonsChunkPlugin, 不使用该插件的前提下暂且可以认为 Entry Chunk === Initial chunk, 多 Entry 的配置也是产出多个 Entry Chunk.
使用该插件后状况较为复杂, 多入口文件中的公共模块代码 以及 webpack runtime 的 bootstrap 代码会被抽取到插件生成的最后一个 Common chunk 中, 即该 Common Chunk === Entry Chunk , 其余模块集中产出一个 bundle.js , 没错, 这哥们就是 Initial Chunk.
详见 webpack 产物一窥

模块 (Module)

全局上, 我们将 Css, 图片以及字体文件等等也都分别看做一个模块. 也是 webpack 的黑魔法之一.
ES2015 import 语句
CommonJS require() 语句
AMD define 和 require 语句
css/sass/less 文件中的 @import 语句。
样式(url(…))或 HTML 文件(\)中的图片链接(image url)
webpack 1 需要特定的 loader 来转换 ES 2015 import,然而通过 webpack 2 可以开箱即用。

模块解析
  • 绝对路径
1
2
import "/home/me/file";
import "C:\\Users\\me\\file";

由于我们已经取得文件的绝对路径,因此不需要进一步再做解析。

  • 相对路径
1
2
import "../src/file1";
import "./file2";

在这种情况下,使用 import 或 require 的资源文件(resource file)所在的目录被认为是上下文目录(context directory)。在 import/require 中给定的相对路径,会添加此上下文路径(context path),以产生模块的绝对路径(absolute path)。

  • 模块路径
1
2
import "module";
import "module/lib/file";

模块将在 resolve.modules 中指定的所有目录内搜索。 你可以替换初始模块路径,此替换路径通过使用 resolve.alias 配置选项来创建一个别名。

Loader

loader 在加载模块时预处理文件, loader 的配置是在 module.rules 中进行使, 每一项看做一个rule

  • rule.test 用来正则匹配后缀名, 以确定处理改数据的 loader
  • rule.use 指定对应的 loader 对文件进行相应的操作转换.

另外, rule 中其它一些规则也大多围绕匹配条件和应用结果展开, 例如 rule.exclude(不匹配), rule.include(匹配), rule.oneOf(只应用第一个匹配的 loader), rule.enforce(制定 loader 种类)

举个栗子🌰

1
npm css-loader style-loader sass-loader -D
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const path = require('path');
const config = {
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.(css|scss)$/,
use: [
{
loader: 'style-loader',
},
{
loader: 'css-loader',
},
{
loader: 'sass-loader'
}
]
}
]
}
};
module.exports = config;

插件

插件用来处理一些 loader 不能处理的问题, 但与 loader 不同的是, 使用插件需要先引入插件.

plugins 是一个数, 数组中的每一项都是某一个 plugin 的实例, plugins 数组甚至可以存在一个插件的多个实例

再举个栗子🌰
CommonsChunkPlugin 插件是用来提取 chunks 中的公共部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const path = require('path');
const { optimize: { CommonsChunkPlugin } } = require('webpack');
const config = {
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
},
module: {
rules: [{
test: /\.css$/,
use: [
{
loader: 'style-loader',
},
{
loader: 'css-loader',
}
]
}
},
plugins: [
new CommonsChunkPlugin({
name: 'runtime',
minChunks: Infinity,
}), ]
};
module.exports = config;

devtool

生成 sourcemap, 方便调试

devtool 选项 配置结果
source-map 在一个单独的文件中产生一个完整且功能完全的文件.这个文件具有最好的 source map, 但是它会减慢打包速度
cheap-module-source-map 在一个单独的文件中生成一个不带列映射的map, 不带列映射提高了打包速度, 但是也使得浏览器开发者工具只能对应到具体的行, 不能对应到具体的列(符号), 会对调试造成不便
eval-source-map 使用 eval 打包源文件模块, 在同一个文件中生成干净的完整的 source map. 这个选项可以在不影响构建速度的前提下生成完整的 sourcemap, 但是对打包后输出的 JS 文件的执行具有性能和安全的隐患. 在开发阶段这是一个非常好的选项, 在生产阶段则一定不要启用这个选项
cheap-module-eval-source-map 这是在打包文件时最快的生成 source map 的方法, 生成的 source map 回合打包后的 JavaScript 文件同行显示, 没有列映射. 和 eval-source-map 选项具有相似的缺点

devServer

通过配置 dev-server 选项, 可以开启一个本地服务器,webpack 为本地服务器提供了非常多的配置选项

精细配置相关属性

  • resolve: 确定模块如何被解析, 除 webpack 的默认配置之外, 通过 resolve 的自定义配置, 对模块的解析实现刚更精细的控制.

    又举个栗子🌰~

1
2
3
4
5
6
7
resolve: {
extensions: ['.js', '.json'],
modules: [join(__dirname, 'src'), 'node_modules'],
alias: {
'~': resolve('src'),
},
}

extensions 自动解析确定的扩展, 使用户在引入模块时可以不带扩展
modules 告诉 webpack 解析模块时应该搜索的目录

  • externals: 打包生成的代码中不添加某项依赖, 这些依赖项直接从用户环境中取得.

🌰🌰🌰:

1
2
3
externals: {
jquery:'jQuery'
}

其他配置项详见 https://webpack.js.org/configuration/resolve/


webpack的安装

同大多数包一样, 需要 node.js 环境, 以及 npm

本地安装
1
2
3
4
5
6
7
8
npm install webpack -D
npm install webpack@tvelsieb> -D
```

#### webpack的使用
命令行带参
webpack.config 跑 webpack命令
package.json 内配 npm scripts配 npm

“scripts”: {
“start”: “webpack-dashboard -c magenta – node dev_server/server.js”,
“dev”: “node dev_server/server.js”,
“build:test”: “rimraf dist && webpack –env=test –config build/webpack.prod.js”,
“build:prod”: “ webpack –env=production –config build/webpack.prod.js –profile –json >> stats.json”
}
`

为多环境配置 webpack

现在在使用 webpack 的项目中, 大多会为多种环境配置不同的配置文件; 常用的方式是为开发环境和生产环境编写独立的配置, 公共部分可以抽出来;

也可以在同一个配置文件中编写两种环境的配置, 在 package.json 中添加命令时, 把环境变量传递给配置文件.