Webpack 4.x 简易使用指南

本篇来自于之前在团队内部分享webpack时的笔记,内容比较浅显,主要讲webpack基本配置,同时webpack运行原理,自定义loaderplugin也有涉及。

Webpack4.x使用指南

零配置使用Webpack

从Webpack4开始支持零配置,在不进行任何配置的形式下,直接运行webpackwebpacksrc/index.js作为入口,最终结果输出到/dist

在默认情况下,webpack只会对只会从入口开始遍历所有依赖文件,由于没有配置loaders,因此无法处理jsx/css/图片等。

在默认情况下运行webpack,打包完成后控制台会提示在配置打包的模式。在cli中通过设置--mode=production|development可以指定默认,在开发模式|产品模式下,webpack会默认启用不同插件对打包进行优化。

webpack基本配置

  1. 入口(entry)
    入口的配置形式有多种,例如:

    1
    2
    3
    4
    5
    6
    entry: './src/index.js', // #1
    entry: {
    home: './home.js',
    about: './about.js',
    }, // #2
    entry: ['./home.js', './about.js'] // #3

    第一种方式配置单一入口,所有文件将会被打包到同一个文件中,例如默认将会打包到main.js中;第二种方式是多页面(MPA)的配置方式,配置多个入口,最终将会生成多个bundle,配合html-webpack-plugin可以将对应的bundle注入到对应的html中;第三种配置的形式指定了多个入口,但是会将最终结果打包到一个文件中。

  2. 输出(output)
    输出指定了webpack将结果输出到哪里,如何进行输出,甚至是使用何种方式构建等,配置比较复杂。常用配置举例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    output: {
    filename: '[name].[hash].js',
    path: path.resolve(__dirname, 'dist'),
    }, // #1
    output: {
    filename: '[name].[hash].js',
    path: path.resolve(__dirname, 'dist'),
    library: 'xxLibrary',
    libraryTarget: 'amd',
    }, // #2

    第一种配置方式是最基本的输出配置,它配置了输入文件的存放的目录以及文件命名,filename支持模板语法,[name].[hash].js表示使用模块名称(入口配置)以及模块标识符来命名文件。
    第二种配置增加了’library’, ‘libraryTarget’两个属性,而library值的作用取决于libraryTarget的值。例2中配置libraryTarget: 'amd'表示使用amd的方式打包,而library的值将作为模块名。

    1
    2
    3
    define('xxLibrary', [], function() {
    return _entry_return_;
    })

    而当libraryTarget: 'commonjs2'时,表示该模块将使用commonjs的方式打包,用于node环境,而library将不起作用。此外libraryTarget的值还可以是var(默认)this等。

  3. 加载器(loader)
    在webpack中module决定了如何处理项目中不同类型的模块,而加载器(loader)则是具体的处理逻辑。

    noParse:指定不需要进行构建的文件。例如,指定项目中不需要对jquery进行构建,可以进行如下配置:

    1
    2
    3
    noParse: function(content) {
    return /jquery/.test(content);
    },

    规则(rules)的配置同样比较复杂,常用的配置如下:

    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
    rules: [
    {
    test: /\.(js|jsx)$/,
    exclude: /(node_modules|bower_components)/,
    include: /src/,
    use: 'babel-loader'
    }, // #1
    {
    test: /\.(png|jpeg|gif)$/,
    oneOf: [
    {
    resourceQuery: /inline/,
    use: 'url-loader',
    },
    {
    resourceQuery: /external/,
    use: 'file-loader',
    }
    ]
    }, // #2
    {
    test: /\.css$/,
    use: ExtractTextPlugin.extract({
    fallback: 'style-loader',
    use: [
    {
    loader: 'css-loader',
    options: {
    modules: true
    },
    },
    ],
    }),
    },
    ], // #3

    上面列举了常用的配置loader的三种情况,第一种处理js|jsx文件时,使用excludeinclude指定构建目录和不需要构建的目录,对于明确不需要构建的模块进行指定可以加快构建速度。第二种情况则是根据具体情况选择对应的loader进行处理。例如当我们使用import emoj from './emoj.png?inline'时,则会使用url-loader对该png文件进行处理,对于external类型的图片,则使用file-loader处理。第三种情况则是使用多个loader并且可以传递options,例如处理css文件,这里使用了style-loadercss-loader两个加载器,并且配置了css-loader的optionsmodules: true。这里需要关注loader执行顺序,loader执行的顺序是先配置后执行,例如这里先使用css-loader解析css,再使用style-loader配置将生成好的css文件通过<style>标签注入到DOM中。options配置了modules: true,则开启css-modules,写法如下:

    1
    2
    3
    4
    import style from 'index.css';
    let IButton = ({children}) => (
    <span className={style.btn}>{children}</span>
    );
  4. 插件(plugin)
    插件(plugin)用于以各种方式自定义webpack构建过程。webpack附带了非常多的插件,可以直接使用webpack.pluginname使用,也存在非常多的第三方插件。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    plugins: [
    new CleanWebpackPlugin(['dist']), // #1
    new HtmlWebpackPlugin({
    title: 'index',
    chunks: ['index'],
    }), // #2
    new BundleAnalyzerPlugin({
    analyzerMode: 'server',
    analyzerHost: '127.0.0.1',
    analyzerPort: 8889,
    }), // #3
    ],

上例使用了三个插件,第一个插件CleanWebpackPlugin作用是在构建输出前清空文件夹;第二个插件用于生成入口html,例如该htmltitle,指定其需要注入的bundle(MPA)。第三个插件则是用于分析构建结果的插件,它会在本地启动一个开发服务器,并且通过canvas来绘制构建可视化结果,方便进行构建分析和优化。

  1. 开发服务器(devServer)
    webpack-dev-server方便我们进行快速开发应用,它会对代码进行监控,一旦发生更改,它会立即构建,并通过补丁的方式应用更改,使得应用能够快速应用更改。并且其提供了一个基于Express开发的简易服务器,所有资源文件都存在内存中,访问速度极快,并且通过配置可以支持热替换。介绍一下常见的webpack-dev-server配置。
    1
    2
    3
    4
    5
    6
    7
    devServer: {
    contentBase: path.join(__dirname, 'dist'),
    compress: true,
    inline: true,
    historyApiFallback: true,
    allowedHosts: ['host1.com', 'host2.com'],
    }

contentBase主要用于指定静态文件的存放路径,例如所需要图片等,如不指定则无法找到对应文件;

compress表示是否开启Gzip压缩;

inline表示是否启用内联模式,内联模式:实时重载的脚本会被插入到bundle中,构建消息将会出现在控制台。此外还有iframe模式。

historyApiFallback主要针对的是访问不存在的页面时的活动,historyApiFallback: true时访问不存在的页面会直接跳转到index.html,也可以传入一个对象更精确的进行控制跳转。

allowedHosts用于指定允许该devServer的主机。

hot是否开启热替换,如果为true的话,则会使用webpack.HotModuleReplacementPlugin插件。通过CLI的方式传递。

1
webpack-dev-server --hot

Webpack 运行流程

概括上来说:
初始化配置参数 -> 绑定事件钩子回调 -> 确定Entry开始遍历 -> 使用loader编译文件 -> 输出

webpack就像一条生产线,经过一系列处理流程后才能将源文件转换成输出结果。

每个流程的处理职责是单一的,流程之间存在依赖关系,只有当前处理完成后才能交由下一个流程处理。

插件像是插入到生产线上的一个功能,在特定的时机对资源进行处理。

webpack 通过 Tapable来组织这一切。

Webpack在运行过程中会广播事件,插件只监听它关心的事件。

—《深入浅出webpack》

Tapable的核心代码可以简化成。

1
2
3
4
5
6
7
8
9
10
11
12
13
class SyncHook {
constructor() {
this.hooks = [];
}
// 订阅事件
tap(name, func) {
this.hook.push(func);
}
// 发布
call() {
this.hooks.forEach(hook => hook(...arguments));
}
}

Webpack具体的流程如图所示。
流程

  1. 初始化构建参数(依据webpack.config.js),插件实例化,生成Compiler供插件使用,挂载自定义钩子。

  2. 依据入口递归遍历文件,并且使用对应的loader进行编译。

  3. 将编译好的文件解析成AST(抽象语法树),分析依赖逐个拉取济源。

  4. 编译完成,输出。

编写自定义loader

在webpack中,真正起编译作用的就是各种各样的loader。loader其实是一个function,其传入匹配到的文件内容(String),然后对这些内容做处理即可。

一个最简单的loader可以使下面这样:

1
2
3
module.exports = function(content) {
return "{};" + content;
}

config中进行配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
module: {
rules: [
{
test: /\.js$/,
loader: path.resolve(__dirname, './loaders/index.js'),
options: {
param1: 1,
param2: 2,
}
}
]
}

这样在编译JS的时候就会在每个JS文件前加上{};


获取自定义配置

可以使用loader-utils模块来拿到自定义配置。

1
2
3
4
5
6
7
const loaderUtils = require('loader-utils');
module.exports = function(content) {
let options = loaderUtils.getOptions(this);
console.log(options.param1); // 1
console.log(options.param2); // 2
return "{};" + content;
}


数据导出

loader可以通过return来返回处理后的结果;当然,更好的方式是使用this.callback的形式。因为它更加灵活,除了content以外,还可以传递其它参数。

this.callback(error, content, sourceMap, ast)可以传入四个参数:

  • error loader向外抛出一个错误
  • content 经过loader编译后的内容
  • sourceMap
  • ast 本次编译生成的AST,之后执行的loader可以直接使用,而不需要再次生成
1
2
3
4
5
6
7
const loaderUtils = require('loader-utils');
module.exports = function(content) {
let options = loaderUtils.getOptions(this);
console.log(options.param1); // 1
console.log(options.param2); // 2
this.callback(null, "{};" + content);
}

异步loader
对于异步loader,可以使用this.async来获取
callback函数。

1
2
3
4
5
6
7
8
9
10
11
const loaderUtils = require('loader-utils');
module.exports = function(content) {
let options = loaderUtils.getOptions(this);
let callback = this.async();
this.cacheable(false); // 是否缓存结果
console.log(options.param1); // 1
console.log(options.param2); // 2
setTimeout(function() {
callback(null, "{};" + content);
}, 1000);
}

pitch钩子
可以在loader文件中exports一个名为pitch的函数,它会先于所有的loaders执行。可以在这个过程中传参,而当前rule的所有loaders都可以拿到这个参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const loaderUtils = require('loader-utils');
module.exports = function(content) {
let options = loaderUtils.getOptions(this);
let callback = this.async();
this.cacheable(false); // 是否缓存结果
console.log(options.param1); // 1
console.log(options.param2); // 2
console.log(this.data.hello); // hello
setTimeout(function() {
callback(null, "{};" + content);
}, 1000);
}
module.exports.pitch = function(remaining, preceding, data) {
data.hello = 'hello';
}

Plugin 初探

Plugin起始是一个简单的class,有一个必须要实现的apply方法。

1
2
3
4
5
6
7
8
9
class LPlugin {
constructor(options) {
this.options = options;
console.log('options', options);
}
apply(compiler) {
console.log('run this plugin');
}
}

使用

1
2
3
4
5
6
7
8
9
10
const LPlugin = require('./plugins/LPlugin');
module.exports = {
...,
plugins: [
new LPlugin({
param1: 1,
param2: 2,
})
]
}

plugin在初始化参数就进行实例化(事件流开始时),因此类似于CleanWebpackPlugin在构建之前进行文件操作删除掉某些目录也是很好实现的。


Tapable & Hook Tapable是Webpack构建过程中的核心所在,其暴露了tap tapAsync tapPromoise等方法,可以使用这些方法,来注入一些逻辑,这些逻辑将会在构建过程的不同时机触发。

例如:

1
2
3
compiler.hooks.compile.tap('LPlugin', params => {
console.log('触发了钩子函数');
})

该钩子函数将会在compile阶段触发。

*** 自定义钩子函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { SyncHook } = require('tapable');
class LPlugin {
constructor(options) {
this.options = options;
console.log('options', options);
}
apply(compiler) {
compiler.hooks.LPlugin = new SyncHook(['data']);
compiler.hooks.enviroment.tap('MyPlugin', function() {
compiler.hooks.LPlugin.call('hello');
})
console.log('run this plugin');
}
}

1
2
3
4
5
6
7
class LLPlugin {
apply(compiler) {
compiler.hooks.LPlugin.tap('LLPlugin', function(data) {
console.log('data', data); // data hello
});
}
}
分享到 评论