《Webpack+Babel入门与实例详解》

《Webpack+Babel入门与实例详解》

挽枫 315 2023-07-03

一、快速入门webpack

1、何为webpack?

webpack是一个模块打包工具,方便找出web工程和nodejs工程里模块之间的依赖关系,按照异地过的规则把这些模块组织、合并为一个JavaScript文件。
例子:

// a.js
import { year } from './b';
console.log(year)

// b.js
export var year = 2022;

// 执行如下命令
npx webpack --entry ./a.js -o dist

//dist/main.js
(()=>{"use strict";console.log(2022)})();

2、webpack的打包模式mode

上述例子里执行打包命令后,控制台应该会报一个警告:

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value.
Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. 
Learn more: https://webpack.js.org/configuration/mode/

这是因为我们在打包的时候没有设置打包的模式,webpack又三种打包模式:production、development、none。如果不设置打包模式,webpack默认会使用production模式。
production:给生产环节使用的,做了优化比如压缩代码;
development:给开发环境用的;
none:不会对打包后的代码做压缩,方便本地学习;

3、webpack的配置文件webpack.config.js

如果每次打包都通过增加命令行参数,这将是灾难!为此webpack会自动寻找打包命令执行的根路径位置下的webpack.config.js文件,并读取里面的配置项,作用于打包。
例如我们将上述例子里的打包命令npx webpack --entry ./a.js -o dist拆解到webpack.config.js里:

var path = require('path');
module.exports={
    entry:'./a.js',
    output:{
        path:path.resolve(__dirname,''),
        filename:'bundle.js'
    },
    mode:'none'
}

上面的配置文件中,有三个配置项:
entry:webpack打包的入口文件;
output:打包之后输出文件的配置,path表示输出的路径,filename表示输出后文件的名字;
mode:打包模式

4、webpack中的预处理器

默认情况下,webpack只会处理js、json文件模块。如果你引入了一个.css文件模块,webpack通常会报错:

Module parse failed: Unexpected token (1:0)
You may need an appropriate loader to handle this file type, 
currently no loaders are configured to process this file. 
See https://webpack.js.org/concepts#loaders

按照上面的例子,做一个简单的修改,在目录下创建一个b.css文件:

// b.css
.hello{
	color:red;
  margin:30px;
}
// a.js引入b.css文件
import { year } from './b';
import './b.css';
console.log(year);

我们尝试着执行打包命令npx webpack,控制台报错:

5、webpack预处理的使用

上面的例子中,执行打包命令后,终端报错,如何解决呢?为此需要引入一种专门处理css文件模块的loader,这个就是css-loader和style-loader。
我们在终端里安转这两个loader的包:npm install css-loader style-loader,并修改webpack的配置:

var path = require('path');
module.exports={
    entry:'./a.js',
    output:{
        path:path.resolve(__dirname,''),
        filename:'bundle.js'
    },
    mode:'none',
    module:{
        rules:[{
            test:/\.css$/,
            use:['style-loader','css-loader']
        }]
    }
}

上面我们新增了一个叫做module的配置项,module配置项是一个对象,其属性rules是用来配置各种类型文件的处理规则。
rules是一个数组,其中test是文件匹配规则,use是符合test正则匹配规则的文件在打包时将使用什么loader处理它。

6、些许思考

为什么我们不增加一个功能去直接将css文件和js文件插入到index.html文件里呢?这样就不需要再去手动引入了。

二、webpack资源入口与出口

1、webpack资源入口

1.1 context配置项

第一章中我们介绍了webpack的资源入口配置entry,其实webpack资源入口配置还有一个context的配置项,该配置项用来描述打包时资源入口的基础目录,以下举个例子来说明一下context配置项。
我们将第一章中的例子,目录结构稍作调整,在根目录下新建src目录,并将a.js、b.js、b.css文件移入src目录,如果在不更改entry属性的前提下,想使得webpack找得到资源入口文件a.js,那就得增加context配置项,告诉webpack打包资源入口文件的起点目录是src


var path = require('path');
module.exports={
    entry:'./a.js',
    context:path.resolve(__dirname,'src'),
    output:{
        path:path.resolve(__dirname,''),
        filename:'bundle.js'
    },
    mode:'none',
    module:{
        rules:[{
            test:/\.css$/,
            use:['style-loader','css-loader']
        }]
    }
}

1.2 entry配置项的其他形式

第一章中,entry的配置项的值是一个字符串类型,实际上entry还能接收数组、对象、函数,接下来将逐个介绍。

1.2.1 entry的数组形式

表示数组的最后一项将是资源的入口文件,其余项将会作为一个模块预先构建到入口文件中,举个例子,我们在上述例子的文件目录src下新建一个文件c.js

// c.js
console.log('c.js');
然后修改webpack配置
var path = require('path');
module.exports={
    entry:['./c.js','./a.js'],
    context:path.resolve(__dirname,'src'),
    output:{
        path:path.resolve(__dirname,''),
        filename:'bundle.js'
    },
    mode:'none',
    module:{
        rules:[{
            test:/\.css$/,
            use:['style-loader','css-loader']
        }]
    }
}

上述配置实际上等价于在a.js文件里插入import语句,在加载a.js文件前先加载c.js文件做法


// a.js
import './c.js';
import { year } from './b';
import './b.css';
console.log(year);

// webpack.config.js
var path = require('path');
module.exports={
    entry:'./a.js',
    context:path.resolve(__dirname,'src'),
    output:{
        path:path.resolve(__dirname,''),
        filename:'bundle.js'
    },
    mode:'none',
    module:{
        rules:[{
            test:/\.css$/,
            use:['style-loader','css-loader']
        }]
    }
}

1.2.2 entry的函数形式

函数形式可以用来做一些额外的逻辑处理,可以返回字符串、数组、对象三种形式
1.2.3 entry的对象形式
entry对象形式配置,可以打包生成多个js文件,主要用于打包多入口配置。优化web应用中的共享模块的缓存。假如我们的项目是一个多页应用,多个页面之间共享一个公共的模块,比如lodashjs、Bootstrap、jQuery、图片等,这些静态资源文件是长期不用更新的,为了打包出来的文件hash不变化,从而使得浏览器在web应用升级时继续使用旧的缓存文件。我们将上述例子做一个修改,在src目录下创建一个lodash文件夹,并创建一个index.js文件来模拟这类场景

// src/lodash/index.js
export const add = (a,b)=>a+b;

// src/a.js
import { add } from './lodash';
console.log(add(1,2));

// src/b.js
import { add } from './lodash';
console.log(add(2,3));

// webpack.config.js

var path = require('path');
module.exports={
    entry:{
        app:['./a.js','./b.js'],
        vendor:'./lodash'
    },
    context:path.resolve(__dirname,'src'),
    output:{
        path:path.resolve(__dirname,''),
        filename:'[name].js'
    },
    mode:'none',
    module:{
        rules:[{
            test:/\.css$/,
            use:['style-loader','css-loader']
        }]
    }
}

上述webpack配置细心的朋友可能会发现filename使用了’[name].js’,这是因为如果采用了多入口配置,则需要指定不同的名字,这里的[name]会默认取app.js作为app配置打包出来的文件名、vendor.js作为vendor配置打包出来的文件名。

2、webpack资源出口

上一章中讲到了资源出口配置output的path和filename属性,除此之外还有几个比较重要的属性publicPath、chunkFilename。
filename:上述例子中filename是一个文件名称,实际上filename还可以是一个路径,打包出来的文件路径最终是:path+filename;filename也支持类似变量方式动态生成文件名比如[name]、[hash]等;我们简单做个例子,将上述的配置更改一下:

// webpack.config.js
var path = require('path');
module.exports={
    entry:'./a.js',
    context:path.resolve(__dirname,'src'),
    output:{
        path:path.resolve(__dirname,''),
        filename:'[hash].js'
    },
    mode:'none',
    module:{
        rules:[{
            test:/\.css$/,
            use:['style-loader','css-loader']
        }]
    }
}

执行打包命令会发现生成了一个名字是hash值的文件,另外你会发现在终端里出现了这样一条警告信息

(node:1947) [DEP_WEBPACK_TEMPLATE_PATH_PLUGIN_REPLACE_PATH_VARIABLES_HASH] DeprecationWarning: [hash] is now [fullhash] (also consider using [chunkhash] or [contenthash], see documentation for details)

意思是说不赞成使用hash而是使用fullhash、contenthash或者chunkhash;
除此之外,filenam动态变量值还支持[id](webpack打包过程中为每一个chunk生成的唯一序号)、[chunkhash]、[contenthash]、[fullhash]
publicPath:用来设置服务器请求资源文件的路径,如果不设置将由webpack自动决定访问路径
chunkFilename:用来设置打包过程中非入口文件的chunk名称

3、了解webpack的hash算法过程

hash与fullhash是一样的,是根据打包中的所有文件计算出的hash值,在一次打包中,所有资源出口的filename将获得一样的hash值;
chunkhash是根据打包过程中当前的chunk计算出的hash值;
contenthash主要用于计算css文件的hash值;
它们的主要区别在于作用于生成hash值的内容不一样,hash由打包中所有文件计算生成hash、chunkhash由当前打包的chunk计算生成chunkhash、contenthash由当前打包的css文件内容生成contenthash

4、些许思考

是否可以利用webpack生成文件的hash规则,优化前端静态资源文件的缓存策略,这样可以提高前端应用的性能?

三、webpack的预处理器

第一章我介绍过,在webpack中一切皆为模块,webpack的本质实际上就是一个处理模块的工具!在不做任何配置的情况下,webpack自己本身只能处理js文件和json文件的,为了让webpack支持处理js文件和json文件之外的文件,webpack提供了一个叫做“加载器”(“预处理器”)的概念,在这个概念指导下诞生了可以扩展webpack默认“加载器”(“预处理器”)的API,我们可以利用扩展API自己编写一个“加载器”来处理自己想要处理的文件。
image

// webpack.config.js
var path = require('path');
module.exports={
    entry:'./a.js',
    context:path.resolve(__dirname,'src'),
    output:{
        path:path.resolve(__dirname,''),
        filename:'[hash].js'
    },
    mode:'none',
    module:{
        rules:[{
            test:/\.css$/,
            use:['style-loader','css-loader']
        }]
    }
}

第二章中我们使用了style-loader和css-loader来处理了css文件,其中modules就是用来配置webpack对模块的解析策略的,modules里有一个属性是rules,rules是一个对象数组,详细定义了各个模块的解析规则,test就是一个文件匹配正则,use这里使用的是字符串数组,其实use还可以支持,对象数组,或者字符串与对象混用的数组,接下来我们详细的看看rules这个属性。

exclude和include

exclude用来定义当前项的模块解析器不解析的文件路径;
include与exclude作用刚好相反,只解析include指定的文件路径;
当include和exclude同时存在的时候,webpack会优先使用exclude

语法扩展或转码

针对开发者体验最新的语法(ES6、ES7)或者用常规语法(css不加内和前缀的语法)而不用关心语法与各平台的兼容问题可以利用预处理器来解决。所以预处理器的本质作用是提高开发者开发体验和开发效率,预处理器的设计目的也应该朝着这个方向进行!

babel-loader预处理器的基本使用(js新规范转旧规范)

地球人都知道,在一些旧版本浏览器里是不支持最新的ES6、ES7等新的ECMSCRIPT规范的,但是开发者作为人是与时俱进的,谁不希望使用新的规范来优化开发体验,那么浏览器的兼容问题就交给了名字叫Babel的这个工具。话不多说,我们来体验一下如何利用babel-loader把ES6语法转换成ES5代码的吧。
默认webpack不会处理ES6转换成ES5代码的逻辑,为此需要借助babel-loader。
首先我们需要安装如下依赖
npm install @babel/preset-env @babel/core babel-loader -D

// src/a.js
const add = (a, b) => a + b;

// webpack.config.js
var path = require('path');
module.exports={
    entry:'./a.js',
    context:path.resolve(__dirname,'src'),
    output:{
        path:path.resolve(__dirname,'dist'),
        filename:'[chunkhash:8].js',
    },
    mode:'none',
    module:{
        rules:[
            {
                test:/\.js$/,
                use:{
                    loader:'babel-loader',
                    options:{
                        presets:['@babel/preset-env'],
                        cacheDirectory:true
                    }
                },
                exclude:/node_modules/
            }
    ]
    }
}

执行打包命令,你会发现尖头函数add被转换成了普通的function。细心的同学会发现,这里有一个cacheDirectory配置项这个配置项的作用是根据文件内容是否发生变化决定要不要重新转码,如果设置了true并且文件内容未发生变化则直接使用上一次转换后的代码。这个配置可以极大提高webpack的打包效率,工作实践中很有用!

文件资源处理的预处理器

file-loader处理js或者css引入的图片

关于使用方法我这里就不赘述了只讲述核心配置,我也只记录了理解核心思路,js默认不支持通过import from的方式引入静态资源的。为此社区提供了一个名字叫file-loader 的预处理器来支持。比如我在js文件中通过imoprt png from xxx.png 的方式引入了一张图片,并且要在js文件中使用png,就得配置如下规则

modules: [
    {
        test: /\.png$/,
        use: 'file-loader'
    }
]

如果想要在css文件中使用图片就得联合style-loader、css-loader一起使用

modules: [
  {
    test: /\.png$/,
    use: 'file-loader'
  },
  {
    test: /\.c s s$/,
    use: ['style-loader','css-loader']
  }
]

其实file-loader不光可以处理图片资源还能处理音视频资源,大家可以访问https://v4.webpack.js.org/loaders/file-loader/

增强型的file-loader——url-loader

url-loader扩展了file-loader的功能,支持将文件转成Base64编码,这样的话对于极小的文件就可以直接转成Base64编码从而达到减少网络请求次数,提高页面加载速度!它们的区别在于,如果webpack当前处理文件的大小小于策略里的文件大小限制,则生成Base64编码;否则url-loader则直接使用file-loader处理文件。关于url-loader的其他使用方法可以详情官网查看,这里大家只需要了解url-loader有这样一个与file-loader不一样的功能就好了

四、webpack插件

前言

第三章我们讲解了webpack的预处理器,知道了预处理器是用来扩展webpack的默认加载器不能完成的能力的,它们是在模块加载过程中运行转换函数,将源文件转换成webpack可以识别的模块。本章将介绍用来扩展webpack处理javascript模块能力的功能——插件(plugin)。本章以后将不再细说配置项,而是简单介绍某些插件诞生的需求背景以及实际工作中的最佳实践。

清除文件插件clean-webpack-plugin

每次打包后,磁盘都会保存当前打包的内容,但是在下一次打包时,webpack并不会自动清除上一次打包遗留的内容,为此社区提供了clean-webpack-plugin插件。
安装插件:
npm install -D clean-webpack-plugin
使用插件:

// webpack.config.js
var path = require('path');
var { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
    entry: ['./b.js','./a.js'],
    context: path.resolve(__dirname, 'src'),
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[chunkhash:8].js',
    },
    mode: 'none',
    plugins:[
        new CleanWebpackPlugin()
    ]
}

如何实验你的webpack的清除插件配置是否成功?你可以屏蔽掉clean配置项,然后修改a.js文件,打包发现会留存上一次打包的文件,这个时候开启clean配置,再打包一次你会发现上一次打包出来的两个文件都被删除了。clean-webpack-plugin插件也支持指定配置的,详情可以参考社区文档。

复制文件插件copy-webpack-plugin

有些公共的静态资源文件,mock文件数据需要在打包的时候被复制到构建目录,为此诞生了copy-webpack-plugin。
安装插件:
npm install -D copy-webpack-plugin
使用插件:

// webpack.config.js
var path = require('path');
var { CleanWebpackPlugin } = require('clean-webpack-plugin');
var CopyPlugin = require('copy-webpack-plugin');
module.exports = {
    entry: ['./b.js','./a.js'],
    context: path.resolve(__dirname, 'src'),
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[chunkhash:8].js',
    },
    mode: 'none',
    module: {
        rules: [
            {
                test: /\.js$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env'],
                        cacheDirectory:true
                    }
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            }
        ]
    },
    plugins:[
        new CleanWebpackPlugin(),
        new CopyPlugin({
            patterns:[{
                from:path.resolve(__dirname,'src/assets'),
                to:path.resolve(__dirname,'dist/assets'),
            }]
        })
    ]
}

以上配置会将sr目录下的assets文件夹内容复制到打包后dist目录下的assets里

HTML模板插件html-webpack-plugin

html-webpack-plugin是一个自动生成html模板的插件,其诞生的需求背景是这样的——在工程打包后,文件的名称通常是根据chunk内容(contenthash第二章有介绍忘记了上去翻)生成的,这样就无法做到在html文件里引入固定的js文件和css文件了。使用html-webpack-plugin就能动态生成html文件,动态引入打包后的js文件和css文件。我们来看看具体怎么使用。
安装插件:
npm install -D html-webpck-plugin
使用插件:

// webpack.config.js
var path = require('path');
var { CleanWebpackPlugin } = require('clean-webpack-plugin');
var CopyPlugin = require('copy-webpack-plugin');
var HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: ['./b.js','./a.js'],
    context: path.resolve(__dirname, 'src'),
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[chunkhash:8].js',
    },
    mode: 'none',
    module: {
        rules: [
            {
                test: /\.js$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env'],
                        cacheDirectory:true
                    }
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            }
        ]
    },
    plugins:[
        new CleanWebpackPlugin({
            protectWebpackAssets:true
        }),
        new CopyPlugin({
            patterns:[{
                from:path.resolve(__dirname,'src/assets'),
                to:path.resolve(__dirname,'dist/assets'),
            }],
        }),
        new HtmlWebpackPlugin()
    ]
}

上述配置默认使用html-webpack-plugin的html模板生成打包后的html模板。当然html-webpack-plugin插件还能配置生成html文件的title、filename、minify(是否压缩生成后的html文件)、自定义模板
如何自定义模板
自定义模板的时候有个小坑需要注意的是,自定义模板的template的路径默认是从context路径作为拼接路径的,如果设置了context路径,则需要在context路径下配置你自己的模板或者直接在项目跟目录下配置index.html然后webpack.config.js中配置new HtmlWebpackPlugin({template:path.resolve(__dirname,'index.html')})
具体示例代码如下:

// webpack.config.js
var path = require('path');
var { CleanWebpackPlugin } = require('clean-webpack-plugin');
var CopyPlugin = require('copy-webpack-plugin');
var HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: ['./b.js','./a.js'],
    context: path.resolve(__dirname, 'src'),
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[chunkhash:8].js',
    },
    mode: 'none',
    module: {
        rules: [
            {
                test: /\.js$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env'],
                        cacheDirectory:true
                    }
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            }
        ]
    },
    plugins:[
        new CleanWebpackPlugin({
            protectWebpackAssets:true
        }),
        // new CopyPlugin({
        //     patterns:[{
        //         from:path.resolve(__dirname,'src/assets'),
        //         to:path.resolve(__dirname,'dist/assets'),
        //     }],
        // }),
        new HtmlWebpackPlugin({template:path.resolve(__dirname,'index.html')})
    ]
}

// index.html 在项目根目录下
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>自定义的模板</title>
</head>
<body>
</body>
</html>

总结

1、你如何理解webpack中的loader和plugin的作用的?什么时候该用loader什么时候该用plugin?

努力学习中…