算起来已经有3到4个项目的webpack构建打包经历。然而每次搭建起来还是有新手既视感,比较捉急。工具用法的东西,好记性不如烂笔头,有必要系统梳理下webpack的配置常用配置以及构建优化。
分为上手篇和优化篇,本篇为上手篇,先介绍常用配置。
篇幅较长,可完整阅读,也可在遇到问题时即查即用。
此次采用webpack4,也顺便尝尝鲜。
# webpack4 把命令行工具抽离成了独立包 webpack-cli
npm install webpack webpack-cli -D
项目下没有webpack.config.js
情况下,命令行直接运行webpack
,webpack4不再像webpack3一样,提示未找到配置文件:
而是提示:
修改后可以发现零配置下系统的默认配置为:
/src/index.js
,打包输出路径:/dist/main.js
--mode
参数时,默认是-mode production
,会进行压缩混淆。传入--mode development
指定为开发环境打包。├── dist
│ └── main.js
├── node_modules
├── package-lock.json
├── package.json
└── src
├── index.js
└── utils.js
体验了一把,所谓的零配置的灵活度极低,目测没啥实际用处,了解一下罢了。所以现阶段还是老老实实地学怎么配置吧。
如果命令行直接webpack
会运行全局安装的webpack,想运行当前目录下的webpack,可以采取以下方法,不嫌麻烦当然也可以每次都./node_modules/.bin/webpack
:
npx是npm 5.2.0及以上内置的包执行器,npx webpack --mode development
会直接找项目的/node_modules/.bin/里面的命令执行,方便快捷。
使用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
};
各个配置项的用法和细节将结合具体的功能实现来讲。
一个入口文件对应输出一个出口文件,因为太简单,不再赘述。这里讲下多对一、多对多。
这里涉及到webpack配置中的灵魂成员:entry
及 output
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') // 打包输出路径(必须绝对路径,否则报错)
}
};
entry
传入对象,key称之为chunk,将不同入口文件分别打包到不同的js。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
非常重要,设置不正确会导致生成错的引用路径,从而找不到资源。这里先不展开,后面结合图片处理再细述。
这里先插入一个实用功能,因为在每次打包后,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
}),
]
};
回到正题,通过上面的配置,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等实用功能。具体参考配置文档。
有了JS和HTML,我们看看CSS该怎样自动内联进页面。
因为webpack原生具有了模块打包的能力,因此我们可以直接用commonjs规范,无需其他插件。而如果我们在js中直接require或者import了一个css文件,此时肯定是需要额外步骤告诉webpack该怎样处理。这里涉及到webpack另一个配置项:module
及相关的loader
。
下面以处理css以及less为例:
处理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
配置是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的形式内联进页面
样式少可以内联,多了还是得抽离。而抽离文件已超过了loader的范围,需要借助plugins来完成:extract-text-webpack-plugin
。
BTW: 有了之前的html自动构建配置,抽离后的CSS也会自动引入
# @next为webpack4使用版本
npm install -D extract-text-webpack-plugin@next
plugins
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]
};
这里将use的值改成extract之后,感觉怎么和上面说的套路又不一样了。
莫慌,其实它只是个语法糖,从返回值就知道,还是返回loader对象数组。
console.log(extractCSS.extract({
fallback: 'style-loader',
use: ['css-loader']
}));
此外,它可以作为实例方法也可以作为静态方法调用,见源码。
// 等价
extractCSS.extract()
ExtractTextPlugin.extract()
顾名思义就是不被抽离时的降级处理。什么时候降级呢?
disable: true
如果异步文件也想抽离样式怎么办?用allChunks
const extractCSS = new ExtractTextPlugin({
disable: false,
filename: 'style/[name].css',
allChunks: true //设置为true
});
好了轮到图片、字体这些资源了。我们希望做到:
字体和svg等资源同理,以下以图片为例。
npm install -D url-loader file-loader
url-loader
: 小于limit值时,直接base64内联,大于limit就干脆不管了,直接扔给file-loader
处理,不装直接报错,之前还以为会自动调用,所以这两者都得装上。
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);
// 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: '' // 可访问到图片的引用路径(相对/绝对)
}
}
}
]
}
};
上述配置除了limit
和fallback
是url-loader(文档)的参数以外,其他配置项如name
, outputPath
都会透传给file-loader(文档)。
关于name
, outputPath
, publicPath
path.join(outputPath, name)
path.join(publicPath, name)
,这里会忽略掉outputPathpath.join(__webpack_public_path__, outputPath, name)
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
})
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的作用。
npm install -D babel-core babel-loader babel-preset-env 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"]
}
开发调试怎么少的了本地服务器。npm install -D webpack-dev-server
。基础配置比较简单,参见官方文档,配置好之后直接webpack-dev-server
即可。
下面提到的是使用时会遇到的一些问题。
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
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 /)。
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
才能访问。
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();
}
真机上访问devServer,进行开发、调试、体验。
解决方法:
host指定为无线网卡的ip,如192.168.0.104
,PC与其他移动设备处于同一wifi环境下时即可访问。
devServer: {
host: '192.168.0.104', // 默认值是localhost
port: 3000,
hot: true
}
后台数据接口为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'
]
}
后台服务搭在本地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预编译等优化及提升开发体验的功能,敬请期待。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。