webpack
# 前言
了解一个工具/框架的最全面、最快速的就是看官网文档。当看到很多文章总结的高级用法和写法的时候,总以为别人很厉害,其实是你自己看的不够仔细。(最近两年才悟出来,实在惭愧。。)
# 简介
webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具,当 webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph),此依赖图对应映射到项目所需的每个模块,并生成一个或多个 bundle。
# 核心概念
# entry
入口文件,作为构建其内部依赖图的开始,默认是src/index.js
。
可配置单个入口和多个入口。多个入口会生成多个独立分离的依赖图,从而形成多页面应用程序。
module.exports = {
// entry: 'src/main.js', // 单入口
entry: { // 多入口
app: './src/app.js',
search: './src/search.js'
}
}
2
3
4
5
6
7
# output
出口文件,设置输出 bundles 的路径以及如何命名这些文件,默认./dist
。
可设置单个或多个输出。如在 entry 中创建了多个入口,则在这里需要使用[name].js
命名文件,使得输出文件名不冲突。
module.exports = {
// output: { // 输出单个文件
// filename: 'bundle.js',
// path: __dirname + '/dist'
// },
output: { // 输出多个
filename: '[name].js', // 根据 entry 中的多入口文件名输出
path: __dirname + '/dist'
}
}
2
3
4
5
6
7
8
9
10
# loader
模块转换器,将各种文件转换为 webpack 能处理的有效文件(webpack 自身只理解 JavaScript)。
loader 支持链式传递,资源可以实现管道式传递,并且 loader 的解析顺序是从后往前的。
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [ // 先执行 css-loader,处理完后传递给 style-loader
'style-loader',
'css-loader'
]
}
]
}
};
2
3
4
5
6
7
8
9
10
11
12
13
# plugins
插件,功能强大,可以用来处理各式各样的任务,插件可以访问到 webpack 的整个编译生命周期。
const HtmlWebpackPlugin = require('html-webpack-plugin'); //通过 npm 安装
const webpack = require('webpack'); //访问内置的插件
module.exports = {
plugins: [
new webpack.optimize.UglifyJsPlugin(),
new HtmlWebpackPlugin({template: './src/index.html'})
]
};
2
3
4
5
6
7
8
9
# mode
模式,通过设置不同的模式启动相应的 webpack 内置优化(如设置:mode: 'production'
会启用 UglifyJsPlugin 插件进行压缩)
可以通过设置不同的 mode
实现针对不同环境设置不同的打包配置。
module.exports = {
mode: 'production'
};
2
3
# 构建流程
了解完核心概念后,需要了解一下大概的构建流程
- 从入口文件开始,分析整个应用的依赖树
- 将每个依赖模块包装起来,放到一个数组中等待调用
- 实现模块加载的方法,并发它放到模块执行环境中,确保模块间可以互相调用
- 把执行入口文件的逻辑放到一个函数表达式中,并立即执行这个函数
# loader
loader 用于对模块的源代码进行转换,如 less -> css。
loader 的解析顺序是从后到前的。
# 自定义loader
项目搭建
# 初始化
npm init
# 安装必须插件
npm install webpack webpack-cli loader-utils -D
2
3
4
需转换文件:test.we(webpack不识别该文件类型,需要做转换)
a:1
自定义loader(仅演示,更复杂的逻辑需要根据业务调整)
// loader/we-loader.js
const loaderUtils = require("loader-utils");
module.exports = function(source){
let loaderOptions = loaderUtils.getOptions(this);
console.log('loaderOptions', loaderOptions); // {type: we}
console.log('source', source); // a:1
// 处理逻辑
const value = source.split(':')[1]
return value; // 一定要返回
}
// loader/notes-loader.js
module.exports = function(source){
console.log('source', source); // 1
// 处理逻辑
const result = `console.log(${source * 100})`
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
webpack.config.js
module.exports = {
entry:{
app:"./test.we" // 方便查看,直接使用 .we 文件做入口
},
output:{
filename:"[name].bundle.js"
},
module:{
rules:[
{
test:/\.we$/,
use: [
{
loader: "./loader/notes-loader" // 自定义loader,乘以 100
},
{
loader: "./loader/we-loader", // 自定义loader,转换.we文件
options: {
type: 'we'
}
}
]
}
]
}
}
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
package.json 添加打包命令
{
"name": "webpackdemo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"webpack": "webpack", // 使用 webpack 打包
}
}
2
3
4
5
6
7
8
9
打包
# 打包
npm run webpack
# 日志:可以看到是先执行了 we-loader,再执行 notes-loader
loaderOptions { type: 'we' }
source a:1
source 1
2
3
4
5
6
7
打包后代码已经转换为想要的内容
# plugin
webpack 插件是一个具有 apply
方法的 JavaScript 对象。apply 方法会被 webpack compiler
调用,并且在整个编译生命周期都可以访问 compiler 对象。
plugin 是拓展功能的插件,用于完成 loader 无法实现的其他事情。
# Compiler 和 Compilation
在插件开发中最重要的两个资源就是 compiler 和 compilation 对象。
compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。
# 自定义plugin
(本次例子基于自定义loader的项目)
plugin/hello-word-plugin.js
const { ConcatSource } = require("webpack-sources");
const wrapComment = str => {
return `/*!\n * ${str
.replace(/\*\//g, "* /")
.split("\n")
.join("\n * ")
.replace(/\s+\n/g, "\n")
.trimRight()}\n */`;
};
class HelloWorldPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
const options = this.options;
console.log('options', options); // { customOpt: { name: '呵呵' } }
compiler.hooks.compilation.tap("HelloWorldPlugin", compilation => {
compilation.hooks.processAssets.tap(
{
name: "HelloWorldPlugin",
},
() => {
for (const chunk of compilation.chunks) {
for (const file of chunk.files) {
console.log('file>>>', file) // app.bundle.js
const comment = wrapComment('这是个版权信息');
compilation.updateAsset(
file,
old => {
return new ConcatSource(comment, "\n", old)
}
);
}
}
}
);
});
}
}
module.exports = HelloWorldPlugin;
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
webpack.config.js
const HelloWorldPlugin = require("./plugin/hello-world-plugin")
module.exports = {
entry:{
app:"./test.we"
},
output:{
filename:"[name].bundle.js"
},
module:{
rules:[
{
test:/\.we$/,
use: [
{
loader: "./loader/notes-loader"
},
{
loader: "./loader/we-loader",
options: {
type: 'we'
}
}
]
}
]
},
plugins: [
new HelloWorldPlugin({ // 自定义plugin
customOpt: {
name: '呵呵'
}
})
]
}
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
打包输出,可以看到多了注释和一个 license 说明文件
# 模块
webpack 中一切皆模块。任何具有独立功能的代码放到一个单独的文件中,就是一个模块,如 js 文件、vue 文件、图片等。
# 模块化的优点
- 作用域封装:每个模块都可以封装属于自己的作用域,就可以避免出现冲突和被覆盖
- 复用性:相同的模块可以复用
- 解除耦合:复杂业务解耦,更容易维护和拓展
# 模块化机制
webpack 不强制使用某种模块化方案,而是通过兼容所有模块化方案让用户可以自由选择。
# webpack 支持的模块化方案
- ES2015
import
语句 - CommonJS
require()
语句 - AMD
define
和require
语句 - css/sass/less 文件中的
@import
语句。 - 样式(
url(...)
)或 HTML 文件中的图片链接(<img src="...")
)
js 文件一样能 import 引入 css 文件
# 模块解析
既然可以通过不同方式去引入模块,那么必定涉及到一个路径解析问题。
# resolver
resolver 是一个库,用于帮助找到模块的绝对路径。resolver 帮助 webpack 找到 bundle 中需要引入的模块代码,这些代码在包含在每个 require/import 语句中。
# 解析规则
当打包模块时,webpack 中使用 enhanced-resolve 来解析文件路径,它可以解析3种路径:
- 绝对路径:
import "C:\\Users\\me\\file";
- 相对路径:
import "./file2";
- 模块路径:
import "vue";
# 自定义解析配置
webpack 已经提供一些系列默认的解析配置了,你依然可以针对具体的需求进行修改。
相关配置都在resolve字段。
module.exports = {
resolve: {
extensions: ['.js', '.vue', '.json'], // 引入这几种文件时不再需要写文件后缀
alias: { // 设置别名
'@': resolve('src')
}
}
}
2
3
4
5
6
7
8
# 解析loader
loader 遵循标准的模块解析。也可以通过 resolveLoader 配置选项来为 Loader 提供独立的解析规则。
module.exports = {
resolveLoader: { // 仅用于解析 loader 包
alias: { // 设置别名
'a-loader': path.resolve(__dirname, 'loaders/a.js')
}
},
module: {
rules: [
{
test: /\.js$/,
// use: path.resolve(__dirname, 'loaders/a.js'),
use: 'a-loader', // 直接使用别名
exclude: /node_modules/
}
]
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# devServer
devServer 是 webpack-dev-server 有关的配置。
可以通过配置 devServer 改变其行为,方便开发。(仅用于开发环境)
webpack.dev.config.js
module.exports = {
devServer: {
host: '0.0.0.0', // 指定使用host,默认是localhost
port: 9000, // 配置端口
open: true, // 自动打开浏览器
hot: true, // 开启热更新
contentBase: path.join(__dirname, "public"), // 资源路径
publicPath: "/assets/", // 此路径下的打包文件可在浏览器中访问
historyApiFallback: true, // 当使用 history 模式时,任意的 404 响应都被替代为 index.html
proxy: { // 代理
"/api": {
target: "http://172.163.11.14:3000",
pathRewrite: {"^/api" : ""}
}
}
// ...
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18