webpack4实用配置指南-上手篇

零、前言

算起来已经有3到4个项目的webpack构建打包经历。然而每次搭建起来还是有新手既视感,比较捉急。工具用法的东西,好记性不如烂笔头,有必要系统梳理下webpack的配置常用配置以及构建优化。

分为上手篇和优化篇,本篇为上手篇,先介绍常用配置。

篇幅较长,可完整阅读,也可在遇到问题时即查即用。

此次采用webpack4,也顺便尝尝鲜。

# webpack4 把命令行工具抽离成了独立包 webpack-cli
npm install webpack webpack-cli -D

一、了解下webpack4的零配置

项目下没有webpack.config.js情况下,命令行直接运行webpack,webpack4不再像webpack3一样,提示未找到配置文件:

而是提示:

修改后可以发现零配置下系统的默认配置为:

  1. 入口路径为:/src/index.js,打包输出路径:/dist/main.js
  2. 未传--mode参数时,默认是-mode production,会进行压缩混淆。传入--mode development指定为开发环境打包。
├── dist
│   └── main.js
├── node_modules
├── package-lock.json
├── package.json
└── src
    ├── index.js
    └── utils.js

体验了一把,所谓的零配置的灵活度极低,目测没啥实际用处,了解一下罢了。所以现阶段还是老老实实地学怎么配置吧。

二、webpack cli执行

如果命令行直接webpack会运行全局安装的webpack,想运行当前目录下的webpack,可以采取以下方法,不嫌麻烦当然也可以每次都./node_modules/.bin/webpack

npx webpack

npx是npm 5.2.0及以上内置的包执行器,npx webpack --mode development会直接找项目的/node_modules/.bin/里面的命令执行,方便快捷。

npm run build

使用npm脚本,配置好之后直接npm run xxx

// package.json
"scripts": {
    "build": "webpack --mode development"
},

三、配置结构

// webpack.config.js
module.exports = {
    mode: 'development', // development|production
	entry: '', // 入口配置
	output: {}, // 输出配置
	module: {}, // 放置loader加载器,webpack本身只能打包commonjs规范的js文件,用于处理其他文件或语法
	plugins: [], // 插件,扩展功能
	// 以下内容进阶篇再涉及
	resolve: {}, // 为引入的模块起别名
	devServer: {} // webpack-dev-server
};

各个配置项的用法和细节将结合具体的功能实现来讲。

官方配置文档

四、基本的项目脚手架功能

1. 多入口配置

一个入口文件对应输出一个出口文件,因为太简单,不再赘述。这里讲下多对一、多对多。

这里涉及到webpack配置中的灵魂成员:entryoutput

(1) 多进一出

entry传入数组相当于将数组内所有文件都打包到bundle.js中。

const path = require('path');

module.exports = {
	entry: ['./src/index.js', './src/index2.js'], // 入口文件
	output: {
		filename: 'bundle.js', // 打包输出文件名
		path: path.join(__dirname, './dist') // 打包输出路径(必须绝对路径,否则报错)
	}
};

(2) 多进多出

  1. entry传入对象,key称之为chunk,将不同入口文件分别打包到不同的js。
  2. output.filename改为用中括号占位来命名,从而生成多个文件,name是entry中各个chunk,具体可参考官方文档
const path = require('path');

module.exports = {
    entry: { // 入口文件,传入对象,定义不同的chunk(如app, utils)
        app: './src/index.js', 
        utils: './src/utils.js'
    },
    output: {
        // filename: 'bundle.js', // 此时因为有多个chunk,因此不能只定义一个输出文件,否则报错
        filename: '[name].[hash].js', 
        path: path.join(__dirname, './dist') 
    }
};

PS. 在output中,还有一个叫publicPath非常重要,设置不正确会导致生成错的引用路径,从而找不到资源。这里先不展开,后面结合图片处理再细述。

2. 清空某目录或子目录及文件

这里先插入一个实用功能,因为在每次打包后,dist目录都有无用文件残留,最好每次打包前都清空dist目录。

npm install -D clean-webpack-plugin
// webpack.config.js
const CleanWebpackPlugin = require('clean-webpack-plugin');
const cdnDir = path.resolve(__dirname, '../../../', 'imgcache.gtimg.cn/vip/')
module.exports = {
	...
	plugins: [
		new CleanWebpackPlugin(['dist']), // 清空项目根目录下dist
		new CleanWebpackPlugin(['dist/images', 'dist/style']),
		new CleanWebpackPlugin(['dist'], {
			root: cdnDir // 指定根目录 清空cdn目录下的dist
		}),
	]
};

3. html自动构建

回到正题,通过上面的配置,js已实现正确的进出关系,那该怎么引用呢,难道需要手动引入吗?下面看下怎样配置实现将html文件进行自动构建。这里需要借助插件。

npm install html-webpack-plugin -D
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
	...
	plugins: [
		new HtmlWebpackPlugin({
			filename: path.join(__dirname, 'entry.html'), // 生成的html(绝对路径:可用于生成到根目录)
            filename: 'html/entry.html', // 生成的html文件名(相对路径:将生成到output.path指定的dist目录下)
            template: './src/index.html' // 以哪个文件作为模板,不指定的话用默认的空模板
        })
	]
};

在上面1的配置基础上加上plugins,就可以将打包文件自动注入到entry.html中,而且资源的引用路径都是正确的。

多页面场景

需要构建多个页面,每个页面分别引用不同入口,怎么破?

module.exports = {
	...
	plugins: [
		new HtmlWebpackPlugin({
            filename: 'html/page1.html', 
            template: './src/index.html',
            chunks: ['utils', 'app']
        }),
        new HtmlWebpackPlugin({
            filename: 'html/page2.html', 
            template: './src/index2.html',
            chunks: ['app'],
            minify: {
                removeComments: true // 删除注释
            },
            hash: true // 加hash
        })
	]
};

要几个页面就new几个,通过chunks传入需要引用的入口。

其他功能

从上面的配置可以看到,除了自动引用之外,html-webpack-plugin还提供了压缩、url后加hash等实用功能。具体参考配置文档

4. CSS处理——内联

有了JS和HTML,我们看看CSS该怎样自动内联进页面。

因为webpack原生具有了模块打包的能力,因此我们可以直接用commonjs规范,无需其他插件。而如果我们在js中直接require或者import了一个css文件,此时肯定是需要额外步骤告诉webpack该怎样处理。这里涉及到webpack另一个配置项:module及相关的loader

下面以处理css以及less为例:

  • less:先编译成css,再把css内联进页面。
  • css:内联进页面

loader

处理less和css等非js资源,需要安装相对应的loader

npm install -D css-loader # 负责处理其中的@import和url()
npm install -D style-loader # 负责内联

npm install -D less less-loader # less编译,处理less文件

module配置

我觉得module配置是webpack里面最繁琐的一块,光是配置loader就有三种不同的写法。下面只列出loader配置项,具体其他的module配置项可参见官方文档

记住module的配置的其中一种套路:

  • module.rules[i].test:命中规则
  • module.rules[i].use:传入数组(loader名或对象数组),从右到左执行
  • module.rules[i].loader :传入字符串,这是module.rules[i].use: [ { loader } ]的简写。
// index.js
import './style/index.css';
import './style/test.less';

// webpack.config.js
module.exports = {
	...
	module: {
        rules: [
            {
                test: /\.css$/,
                // 从右到左,loader安装后无需引入可直接使用
                use: ['style-loader', 'css-loader']
            },
            {
                test: /\.less$/,
                use: [
                    {loader: 'style-loader'},
                    {loader: 'css-loader'},
                    {loader: 'less-loader'}
                ]
            }
        ]
    }
};

最终以style的形式内联进页面

5. CSS处理——合并抽离

样式少可以内联,多了还是得抽离。而抽离文件已超过了loader的范围,需要借助plugins来完成:extract-text-webpack-plugin

BTW: 有了之前的html自动构建配置,抽离后的CSS也会自动引入

# @next为webpack4使用版本
npm install -D extract-text-webpack-plugin@next

抽离套路:

  1. 实例化ExtractTextPlugin
    • 每个实例抽离成文件时是以entry为单位,所以一个入口文件(entry)只能抽出一个文件,多entry时在设置filename需要注意写法
    • 可多次实例化,分别抽离CSS、LESS,下同
  2. 将实例放入到plugins
  3. 在css对应的module.rules.use调用extract方法。
const ExtractTextPlugin = require('extract-text-webpack-plugin');

// 实例化1:用于CSS
const extractCSS = new ExtractTextPlugin({
	disable: process.env.NODE_ENV == 'development' ? true : false, // 开发环境下直接内联,不抽离
    filename: 'style/extractFromCss.css', // 单个entry时,可写死
    filename: 'style/[name].css', // 多entry时
});
// 实例化2:用于LESS
const extractLESS = new ExtractTextPlugin({省略...});

module.exports = {
	module: {
		rules: [
			{
				test: /\.css$/,
				use: extractCSS.extract({
                    fallback: 'style-loader',
                    use: 'css-loader'
                })
			},
			{
                test: /\.less$/,
                use: extractLESS.extract({
                    fallback: 'style-loader',
                    use: ['css-loader', 'less-loader']
                })
            }
		]
	},
	plugins: [extractCSS, extractLESS]
};

关于extract方法

这里将use的值改成extract之后,感觉怎么和上面说的套路又不一样了。

莫慌,其实它只是个语法糖,从返回值就知道,还是返回loader对象数组。

console.log(extractCSS.extract({
	fallback: 'style-loader',
    use: ['css-loader']
}));

此外,它可以作为实例方法也可以作为静态方法调用,见源码

// 等价
extractCSS.extract()
ExtractTextPlugin.extract()

关于options.fallback

顾名思义就是不被抽离时的降级处理。什么时候降级呢?

  1. 实例时传入了disable: true
  2. code splitting异步打包的文件内如果有引用样式,默认情况下这些样式不会被抽离,此时被降级。参考博文

如果异步文件也想抽离样式怎么办?用allChunks

const extractCSS = new ExtractTextPlugin({
	disable: false,
    filename: 'style/[name].css',
    allChunks: true //设置为true
});

6. 图片(字体/svg)处理

好了轮到图片、字体这些资源了。我们希望做到:

  1. 图片能正确被webpack打包,小于一定大小的图片直接base64内联。
  2. 打包之后各个入口(css/js/html)还能正常访问到图片,图片引用路径不乱。

字体和svg等资源同理,以下以图片为例。

(1) 安装依赖

npm install -D url-loader file-loader

url-loader: 小于limit值时,直接base64内联,大于limit就干脆不管了,直接扔给file-loader处理,不装直接报错,之前还以为会自动调用,所以这两者都得装上。

(2) 不同入口(css/js/html)引用图片

html

html文件是通过html-wepback-plugin生成的,如果希望webpack能够正确处理打包之后图片的引用路径,需要在模板文件中这样引用图片。

<!-- 正确:会交给url-loader 或 file-loader -->
<!-- require让图片和html产生依赖引用关系 -->
<img src="<%= require('./images/sett1.png') %>" alt="">

<!-- 错误:原样输出,不做任何处理 -->
<img src="./images/sett1.png" alt="">

css/js

/* 图片作为背景图 */
#main {
    background: url("../images/cjl.jpg") #999;
    color: #fff
}
// app.js
import sett1 from './images/sett1.png';
const img = document.createElement('img');
img.src = sett1;
document.body.appendChild(img);

(3) 配置

// webpack.config.js
module.exports = {
	...
	modules: {
		rules: [
			{
				test: /\.(png|jpe?g|gif)$/,
				use: {
					loader: 'url-loader',
					options: {
						limit: 1024 * 8, // 8k以下的base64内联,不产生图片文件
						fallback: 'file-loader', // 8k以上,用file-loader抽离(非必须,默认就是file-loader)
						name: '[name].[ext]?[hash]', // 文件名规则,默认是[hash].[ext]
						outputPath: 'images/', // 输出路径
						publicPath: ''  // 可访问到图片的引用路径(相对/绝对)
					}
				}
			}
		]
	}
};

上述配置除了limitfallback是url-loader(文档)的参数以外,其他配置项如name, outputPath都会透传给file-loader(文档)。

关于name, outputPath, publicPath

  1. 图片最终的输出路径:path.join(outputPath, name)
  2. 图片的引用路径:
    • 指定了publicPath:path.join(publicPath, name),这里会忽略掉outputPath
    • 否则用默认的output.publicPath:path.join(__webpack_public_path__, outputPath, name)

(4) 打包结果

html

css背景图

js动态插入

咦,有没发现背景图引用路径不对?

根据上面的引用路径生成规则path.join('', 'images/','[name].[ext]?[hash]'),也就是各引用入口(html/css)必须和images目录同级才能访问到图片。而css放在了style目录下,与images不同级。

引用路径不对?用publicPath修正

要解决上面的问题,可以在抽离css时设定publicPath。

extractCSS.extract({
	fallback: 'style-loader',
	use: 'css-loader',
	publicPath: '../' // 默认取output.publicPath
})

【重点】来看下到底output.publicPath是什么?

publicPath的值会作为前缀附加在loaders生成的所有URL前面。

比如上面的images/cjl.jpg,如果设置了output.publicPath:"../",那最终打包之后就会变成../images/cjl.jpg

The value of the option is prefixed to every URL created by the runtime or loaders. Because of this the value of this option ends with / in most cases.

它指定了output目录的访问路径,也就是浏览器怎样找到output目录。

This option specifies the public URL of the output directory when referenced in a browser.

比如设置了output.publicPath:"../",就说明output目录在html所在目录的上一级。

那这样设置了的话,css和html的目录层级关系并不符合要求,所以单独在extractCSS.extract中设置publicPath起到了覆盖output.publicPath的作用。

7. ES6转义

npm install -D babel-core babel-loader babel-preset-env babel-preset-stage-0
  • babel-core 核心包
  • babel-loader
  • babel-preset-env 定案内语法编译 (babel-preset-es2015已废弃)
  • babel-preset-stage-0 预案内语法编译
// webpack.config.js
module: {
	rules: [
		{
	        test: /\.js$/,
	        exclude: /node_modules/, // npm包不做处理
	        include: /src/, // 只处理src里面的
	        use: {
	            loader: 'babel-loader',
	            options: {
	                presets: ['env', 'stage-0'] // 【重要】顺序右到左,先处理高级或特殊语法
                }
            }
        }
	]
}

options的内容也可以单独写在.babelrc

{
	"presets": ["env", "stage-0"]
}

8. webpack-dev-server

开发调试怎么少的了本地服务器。npm install -D webpack-dev-server。基础配置比较简单,参见官方文档,配置好之后直接webpack-dev-server即可。

下面提到的是使用时会遇到的一些问题。

contentBase和publicPath

contentBase和publicPath两个参数比较重要,设置错了的话会导致文件404

devServer: {
	contentBase: './public', 
    publicPath: '/',
	host: 'localhost',
	port: 3000
}
ℹ 「wds」: webpack output is served from /
ℹ 「wds」: Content not from webpack is served from ./public
(1) contentBase

Content not from webpack is served from

也就是指定静态服务器的根目录,可以访问到不通过webpack处理的文件

devServer: {
    index: 'main.html', // 为了可以展示目录
    contentBase: __dirname, // 默认值就是项目根目录
    host: 'localhost',
    port: 3000
}

启动服务器之后,访问localhost:3000,可以看到根目录的所有文件

需要说明下的是,为了演示,如果不设置index: 'main.html'localhost:3000会直接访问项目入口文件index.html

举个具体例子,如果我们在入口文件直接引入<link rel="stylesheet" href="public/test.css">,这个资源不会经过webpack处理,因此最终访问路径是localhost:3000/public/test.css

同理,当contentBase: path.join(__dirname, 'public')时,访问localhost:3000只会显示public下的文件。因此在出现文件404时,检查下引用的资源url是否和contentBase里的文件一一对应。

如果所有的静态资源文件都会经过webpack打包,其实可以直接设置为false,此时可以再试试访问localhost:3000 (Cannot GET /)。

(2) publicPath

webpack output is served from

对于webpack打包的文件:虽然我们指定了打包输出目录dist,但是实际上并不会生成dist,而是打包后直接传给devserver,然后放到内存中。不过可以通过:http://localhost:3000/webpack-dev-server查看打包目录下的文件。

publicPath是告诉浏览器通过什么路径去访问上面的webpack打包目录。默认值是/。也就是说我们可以通过:http://localhost:3000/index.html,http://localhost:3000/style/vutify.css来访问打包文件。这点要和contentBase的静态文件服务器区分开。

另一个容易导致文件404的是:把publicPath设置为打包目录/dist。这样的话,就需要多加一层:

http://localhost:3000/dist/index.html,

http://localhost:3000/dist/style/vutify.css才能访问。

热更新 HMR

webpack-dev-server在试图重新加载整个页面(LiveReload)之前,会尝试使用热更新(HMR)来更新。

devServer: {
	host: 'localhost',
	port: 3000,
	hot: true // 还需要在plugins中配置new webpack.HotModuleReplacementPlugin()
}

可以发现在更新css以及html文件时,页面是不会刷新的(css-loader/html-webpack-plugin已具备热更新功能),但更新js时会。因此可以在入口文件添加以下代码,实现热更新。

// index.js
import './dependency.js';

if (module.hot) {
	module.hot.accept();
}

域名与代理

场景1:

真机上访问devServer,进行开发、调试、体验。

解决方法:

host指定为无线网卡的ip,如192.168.0.104,PC与其他移动设备处于同一wifi环境下时即可访问。

devServer: {
    host: '192.168.0.104', // 默认值是localhost
    port: 3000,
    hot: true
}
场景2:

后台数据接口为http://c.y.qq.com/xxx,如果用localhost:3000访问的话,会遇到跨域的问题,需要使用y.qq.com域名访问。

解决方法:

将本地服务器放在80端口上(Mac下需要sudo起服务),配置host:y.qq.com 127.0.0.1,此时使用http://y.qq.com/即可访问本地服务器。

devServer: {
    host: '127.0.0.1',
    port: 80,
    hot: true,
    allowedHosts: [ // 允许哪些域名访问devServer
	    'y.qq.com'
    ]
}
场景3:

后台服务搭在本地8360端口,页面在3000端口。

// bad: 入侵代码
const url = process.env.NODE_ENV == 'development' 
	? 'http://127.0.0.1:8360/common/getmultiple' 
	: '/common/getmultiple'

解决方法:

devServer: {
    host: '127.0.0.1',
    port: 3000,
    hot: true,
    proxy: {
	    "/common/getmultiple": "http://localhost:3000"
    }
}

proxy配置项非常强大,可以pathRewrite重写请求路径,bypass绕过代理等,这里抛砖引玉,其他功能参考devserver-proxy官方文档

五、未完待续

经过上面的配置,对webpack的entry、output、publicPath、loader等灵魂级配置项有了一定的理解,同时实现了基本的脚手架功能配置。在优化篇会分享一下code spliting、dll预编译等优化及提升开发体验的功能,敬请期待。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏腾讯云Elasticsearch Service

logstash input插件开发

logstash作为一个数据管道中间件,支持对各种类型数据的采集与转换,并将数据发送到各种类型的存储库,比如实现消费kafka数据并且写入到Elasticsea...

2904
来自专栏linux驱动个人学习

各种根文件系统

(1) jffs2   JFFS文件系统最早是由瑞典Axis Communications公司基于Linux2.0的内核为嵌入式系统开发的文件系统。JFFS2...

3577
来自专栏前端儿

前端自动化工具 -- Gulp 使用简介

而gulp呢,是基于stream流的形式,也就是前一个函数(工厂)制造出结果,提供后者使用。

472
来自专栏苍云横渡学习笔记

【Django】在项目中配置Mailgun并发送邮件

【本文目录】 注册Mailgun并添加新域名 添加发送DNS解析记录 添加追踪DNS解析记录 等待域名验证 配置settings.py Django自带邮件功能...

3909
来自专栏linux驱动个人学习

cyclictest 简介

1. cyclictest 简介以及安装 1.1 cyclictest 简介       cyclictest 是什么? 看名字应该就能大致猜出来它是一种 te...

3804
来自专栏SmartSql

SmartCode.ETL 这不是先有鸡还是蛋的问题!

相信不少同学都用过各种代码生成器,这里我就不做详细介绍了,如果想体验 SmartCode.Generator 请至 https://www.cnblogs.co...

1226
来自专栏chenssy

【死磕Sharding-jdbc】---结果合并总结

这句SQL会使得MySQL在无法利用索引的情况下跳过1000000条记录后,再获取10条记录,其性能可想而知。而在分库分表的情况下(假设分为2个库),为了保证数...

973
来自专栏FreeBuf

Go代码审计:Gitea远程命令执行漏洞链

这是一个非常漂亮的漏洞链,很久没见过了。我用docker来复现并学习这个漏洞,官方提供了docker镜像,vulhub也会上线这个环境。

1303
来自专栏FreeBuf

Kali下常用安全工具中文参数说明(160个)

*本文原创作者:屌丝绅士,属Freebuf原创奖励计划,转载请注明来自FreeBuf 由于篇幅有限,只列举部分,ps:第一次发有什么不对的 还望各位大大指正 n...

3379
来自专栏苍云横渡学习笔记

【Django】在项目中配置Mailgun并发送邮件

原文链接:http://www.cloudcrossing.xyz/post/26/

54810

扫码关注云+社区