三篇长文,带你解锁 Webpack
,但愿读完这三篇文章,你可以对 webpack
的各项配置有一个更为清晰的认识。css
本文是第二篇,若是你尚未阅读《带你深度解锁Webpack系列(基础篇)》,建议阅读以后,再继续阅读本文。html
本文会引入更多的 webpack
配置,若是文中有任何错误,欢迎在评论区指正,我会尽快修正。 webpack
优化部分放在了下一篇。前端
推荐你们参考本文一步一步进行配置,不要老是想着找什么最佳配置,掌握以后,根据本身的需求配置出来的,就是最佳配置。vue
本文对应的项目地址(编写本文时使用) 供参考:github.com/YvetteLau/w…node
有些时候,咱们须要使用已有的JS文件、CSS文件(本地文件),可是不须要 webpack
编译。例如,咱们在 public/index.html
中引入了 public
目录下的 js
或 css
文件。这个时候,若是直接打包,那么在构建出来以后,确定是找不到对应的 js
/ css
了。react
public
目录结构jquery
├── public
│ ├── config.js
│ ├── index.html
│ ├── js
│ │ ├── base.js
│ │ └── other.js
│ └── login.html
复制代码
如今,咱们在 index.html
中引入了 ./js/base.js
。webpack
<!-- index.html -->
<script src="./js/base.js"></script>
复制代码
这时候,咱们 npm run dev
,会发现有找不到该资源文件的报错信息。git
对于这个问题,咱们能够手动将其拷贝至构建目录,而后在配置 CleanWebpackPlugin
时,注意不要清空对应的文件或文件夹便可,可是如若这个静态文件时不时的还会修改下,那么依赖于手动拷贝,是很容易出问题的。github
不要过于相信本身的记性,依赖于手动拷贝的方式,大多数人应该都有过忘记拷贝的经历,你要是说你历来没忘过。
幸运的是,webpack
为咱们这些记性很差又爱偷懒的人提供了好用的插件 CopyWebpackPlugin,它的做用就是将单个文件或整个目录复制到构建目录。
首先安装一下依赖:
npm install copy-webpack-plugin -D
复制代码
修改配置(当前,须要作的是将 public/js
目录拷贝至 dist/js
目录):
//webpack.config.js
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
//...
plugins: [
new CopyWebpackPlugin([
{
from: 'public/js/*.js',
to: path.resolve(__dirname, 'dist', 'js'),
flatten: true,
},
//还能够继续配置其它要拷贝的文件
])
]
}
复制代码
此时,从新执行 npm run dev
,报错信息已经消失。
这里说一下 flatten
这个参数,设置为 true
,那么它只会拷贝文件,而不会把文件夹路径都拷贝上,你们能够不设置 flatten
时,看下构建结果。
另外,若是咱们要拷贝一个目录下的不少文件,可是想过滤掉某个或某些文件,那么 CopyWebpackPlugin
还为咱们提供了 ignore
参数。
//webpack.config.js
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
//...
plugins: [
new CopyWebpackPlugin([
{
from: 'public/js/*.js',
to: path.resolve(__dirname, 'dist', 'js'),
flatten: true,
}
], {
ignore: ['other.js']
})
]
}
复制代码
例如,这里咱们忽略掉 js
目录下的 other.js
文件,使用 npm run build
构建,能够看到 dist/js
下不会出现 other.js
文件。 CopyWebpackPlugin
还提供了不少其它的参数,若是当前的配置不能知足你,能够查阅文档进一步修改配置。
ProvidePlugin
在我看来,是为懒人准备的,不过也别过分使用,毕竟全局变量不是什么“好东西”。ProvidePlugin
的做用就是不须要 import
或 require
就能够在项目中处处使用。
ProvidePlugin
是 webpack
的内置插件,使用方式以下:
new webpack.ProvidePlugin({
identifier1: 'module1',
identifier2: ['module2', 'property2']
});
复制代码
默认寻找路径是当前文件夹 ./**
和 node_modules
,固然啦,你能够指定全路径。
React
你们都知道的,使用的时候,要在每一个文件中引入 React
,否则马上抛错给你看。还有就是 jquery
, lodash
这样的库,可能在多个文件中使用,可是懒得每次都引入,好嘛,一块儿来偷个懒,修改下 webpack
的配置:
const webpack = require('webpack');
module.exports = {
//...
plugins: [
new webpack.ProvidePlugin({
React: 'react',
Component: ['react', 'Component'],
Vue: ['vue/dist/vue.esm.js', 'default'],
$: 'jquery',
_map: ['lodash', 'map']
})
]
}
复制代码
这样配置以后,你就能够在项目中为所欲为的使用 $
、_map
了,而且写 React
组件时,也不须要 import
React
和 Component
了,若是你想的话,你还能够把 React
的 Hooks
都配置在这里。
另外呢,Vue
的配置后面多了一个 default
,这是由于 vue.esm.js
中使用的是 export default
导出的,对于这种,必需要指定 default
。React
使用的是 module.exports
导出的,所以不要写 default
。
另外,就是若是你项目启动了 eslint
的话,记得修改下 eslint
的配置文件,增长如下配置:
{
"globals": {
"React": true,
"Vue": true,
//....
}
}
复制代码
固然啦,偷懒要有个度,你要是配一大堆全局变量,最终可能会给本身带来麻烦,对本身配置的全局变量必定要负责到底。
CSS打包咱们前面已经说过了,不过呢,有些时候,咱们可能会有抽离CSS的需求,即将CSS文件单独打包,这多是由于打包成一个JS文件太大,影响加载速度,也有多是为了缓存(例如,只有JS部分有改动),还有可能就是“我高兴”:我想抽离就抽离,谁也管不着。
无论你是由于什么缘由要抽离CSS,只要你有需求,咱们就能够去实现。
首先,安装 loader
:
npm install mini-css-extract-plugin -D
复制代码
mini-css-extract-plugin
和extract-text-webpack-plugin
相比:
修改咱们的配置文件:
//webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].css'
//我的习惯将css文件放在单独目录下
//publicPath:'../' //若是你的output的publicPath配置的是 './' 这种相对路径,那么若是将css文件放在单独目录下,记得在这里指定一下publicPath
})
],
module: {
rules: [
{
test: /\.(le|c)ss$/,
use: [
MiniCssExtractPlugin.loader, //替换以前的 style-loader
'css-loader', {
loader: 'postcss-loader',
options: {
plugins: function () {
return [
require('autoprefixer')({
"overrideBrowserslist": [
"defaults"
]
})
]
}
}
}, 'less-loader'
],
exclude: /node_modules/
}
]
}
}
复制代码
如今,咱们从新编译:npm run build
,目录结构以下所示:
.
├── dist
│ ├── assets
│ │ ├── alita_e09b5c.jpg
│ │ └── thor_e09b5c.jpeg
│ ├── css
│ │ ├── index.css
│ │ └── index.css.map
│ ├── bundle.fb6d0c.js
│ ├── bundle.fb6d0c.js.map
│ └── index.html
复制代码
前面说了最好新建一个 .browserslistrc
文件,这样能够多个 loader
共享配置,因此,动手在根目录下新建文件 (.browserslistrc
),内容以下(你能够根据本身项目需求,修改成其它的配置):
last 2 version
> 0.25%
not dead
复制代码
修改 webpack.config.js
:
//webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
//...
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].css'
})
],
module: {
rules: [
{
test: /\.(c|le)ss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader', {
loader: 'postcss-loader',
options: {
plugins: function () {
return [
require('autoprefixer')()
]
}
}
}, 'less-loader'
],
exclude: /node_modules/
},
]
}
}
复制代码
要测试本身的 .browserlistrc
有没有生效也很简单,直接将文件内容修改成 last 1 Chrome versions
,而后对比修改先后的构建出的结果,就能看出来啦。
能够查看更多[browserslistrc]配置项(github.com/browserslis…)
更多配置项,能够查看mini-css-extract-plugin
使用 mini-css-extract-plugin
,CSS
文件默认不会被压缩,若是想要压缩,须要配置 optimization
,首先安装 optimize-css-assets-webpack-plugin
.
npm install optimize-css-assets-webpack-plugin -D
复制代码
修改webpack配置:
//webpack.config.js
const OptimizeCssPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
entry: './src/index.js',
//....
plugins: [
new OptimizeCssPlugin()
],
}
复制代码
注意,这里将 OptimizeCssPlugin
直接配置在 plugins
里面,那么 js
和 css
都可以正常压缩,若是你将这个配置在 optimization
,那么须要再配置一下 js
的压缩(开发环境下不须要去作CSS的压缩,所以后面记得将其放到 webpack.config.prod.js
中哈)。
配置完以后,测试的时候发现,抽离以后,修改 css
文件时,第一次页面会刷新,可是第二次页面不会刷新 —— 好嘛,我平时的业务中用不着抽离 css
,这个问题搁置了好多天(准确来讲是忘记了)。
3月8号再次修改这篇文章的时候,正好看到了 MiniCssExtractPlugin.loader
对应的 option
设置,咱们再次修改下对应的 rule
。
module.exports = {
rules: [
{
test: /\.(c|le)ss$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
hmr: isDev,
reloadAll: true,
}
},
//...
],
exclude: /node_modules/
}
]
}
复制代码
不少时候咱们不须要一次性加载全部的JS文件,而应该在不一样阶段去加载所须要的代码。webpack
内置了强大的分割代码的功能能够实现按需加载。
好比,咱们在点击了某个按钮以后,才须要使用使用对应的JS文件中的代码,须要使用 import()
语法:
document.getElementById('btn').onclick = function() {
import('./handle').then(fn => fn.default());
}
复制代码
import()
语法,须要 @babel/plugin-syntax-dynamic-import
的插件支持,可是由于当前 @babel/preset-env
预设中已经包含了 @babel/plugin-syntax-dynamic-import
,所以咱们不须要再单独安装和配置。
直接 npm run build
进行构建,构建结果以下:
webpack
遇到 import(****)
这样的语法的时候,会这样处理:
Chunk
import
所在的语句时,才会加载该 Chunk
所对应的文件(如这里的1.bundle.8bf4dc.js)你们能够在浏览器中的控制台中,在 Network
的 Tab页
查看文件加载的状况,只有点击以后,才会加载对应的 JS
。
devServer
的 hot
为 true
plugins
中增长 new webpack.HotModuleReplacementPlugin()
//webpack.config.js
const webpack = require('webpack');
module.exports = {
//....
devServer: {
hot: true
},
plugins: [
new webpack.HotModuleReplacementPlugin() //热更新插件
]
}
复制代码
咱们配置了 HotModuleReplacementPlugin
以后,会发现,此时咱们修改代码,仍然是整个页面都会刷新。不但愿整个页面都刷新,还须要修改入口文件:
if(module && module.hot) {
module.hot.accept()
}
复制代码
此时,再修改代码,不会形成整个页面的刷新。
有时,咱们的应用不必定是一个单页应用,而是一个多页应用,那么如何使用 webpack
进行打包呢。为了生成目录看起来清晰,不生成单独的 map
文件。
//webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js',
login: './src/login.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[hash:6].js'
},
//...
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html' //打包后的文件名
}),
new HtmlWebpackPlugin({
template: './public/login.html',
filename: 'login.html' //打包后的文件名
}),
]
}
复制代码
若是须要配置多个 HtmlWebpackPlugin
,那么 filename
字段不可缺省,不然默认生成的都是 index.html
,若是你但愿 html
的文件名中也带有 hash
,那么直接修改 fliename
字段便可,例如: filename: 'login.[hash:6].html'
。
生成目录以下:
.
├── dist
│ ├── 2.463ccf.js
│ ├── assets
│ │ └── thor_e09b5c.jpeg
│ ├── css
│ │ ├── index.css
│ │ └── login.css
│ ├── index.463ccf.js
│ ├── index.html
│ ├── js
│ │ └── base.js
│ ├── login.463ccf.js
│ └── login.html
复制代码
看起来,彷佛是OK了,不过呢,查看 index.html
和 login.html
会发现,都同时引入了 index.f7d21a.js
和 login.f7d21a.js
,一般这不是咱们想要的,咱们但愿,index.html
中只引入 index.f7d21a.js
,login.html
只引入 login.f7d21a.js
。
HtmlWebpackPlugin
提供了一个 chunks
的参数,能够接受一个数组,配置此参数仅会将数组中指定的js引入到html文件中,此外,若是你须要引入多个JS文件,仅有少数不想引入,还能够指定 excludeChunks
参数,它接受一个数组。
//webpack.config.js
module.exports = {
//...
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html', //打包后的文件名
chunks: ['index']
}),
new HtmlWebpackPlugin({
template: './public/login.html',
filename: 'login.html', //打包后的文件名
chunks: ['login']
}),
]
}
复制代码
执行 npm run build
,能够看到 index.html
中仅引入了 index
的 JS
文件,而 login.html
中也仅引入了 login
的 JS
文件,符合咱们的预期。
resolve
配置 webpack
如何寻找模块所对应的文件。webpack
内置 JavaScript
模块化语法解析功能,默认会采用模块化标准里约定好的规则去寻找,但你能够根据本身的须要修改默认的规则。
resolve.modules
配置 webpack
去哪些目录下寻找第三方模块,默认状况下,只会去 node_modules
下寻找,若是你咱们项目中某个文件夹下的模块常常被导入,不但愿写很长的路径,那么就能够经过配置 resolve.modules
来简化。
//webpack.config.js
module.exports = {
//....
resolve: {
modules: ['./src/components', 'node_modules'] //从左到右依次查找
}
}
复制代码
这样配置以后,咱们 import Dialog from 'dialog'
,会去寻找 ./src/components/dialog
,再也不须要使用相对路径导入。若是在 ./src/components
下找不到的话,就会到 node_modules
下寻找。
resolve.alias
配置项经过别名把原导入路径映射成一个新的导入路径,例如:
//webpack.config.js
module.exports = {
//....
resolve: {
alias: {
'react-native': '@my/react-native-web' //这个包名是我随便写的哈
}
}
}
复制代码
例如,咱们有一个依赖 @my/react-native-web
能够实现 react-native
转 web
。咱们代码通常下面这样:
import { View, ListView, StyleSheet, Animated } from 'react-native';
复制代码
配置了别名以后,在转 web 时,会从 @my/react-native-web
寻找对应的依赖。
固然啦,若是某个依赖的名字太长了,你也能够给它配置一个短一点的别名,这样用起来比较爽,尤为是带有 scope
的包。
适配多端的项目中,可能会出现 .web.js
, .wx.js
,例如在转web的项目中,咱们但愿首先找 .web.js
,若是没有,再找 .js
。咱们能够这样配置:
//webpack.config.js
module.exports = {
//....
resolve: {
extensions: ['web.js', '.js'] //固然,你还能够配置 .json, .css
}
}
复制代码
首先寻找 ../dialog.web.js
,若是不存在的话,再寻找 ../dialog.js
。这在适配多端的代码中很是有用,不然,你就须要根据不一样的平台去引入文件(以牺牲了速度为代价)。
import dialog from '../dialog';
复制代码
固然,配置 extensions
,咱们就能够缺省文件后缀,在导入语句没带文件后缀时,会自动带上extensions
中配置的后缀后,去尝试访问文件是否存在,所以要将高频的后缀放在前面,而且数组不要太长,减小尝试次数。若是没有配置 extensions
,默认只会找对对应的js文件。
若是配置了 resolve.enforceExtension
为 true
,那么导入语句不能缺省文件后缀。
有一些第三方模块会提供多份代码,例如 bootstrap
,能够查看 bootstrap
的 package.json
文件:
{
"style": "dist/css/bootstrap.css",
"sass": "scss/bootstrap.scss",
"main": "dist/js/bootstrap",
}
复制代码
resolve.mainFields
默认配置是 ['browser', 'main']
,即首先找对应依赖 package.json
中的 brower
字段,若是没有,找 main
字段。
如:import 'bootstrap'
默认状况下,找得是对应的依赖的 package.json
的 main
字段指定的文件,即 dist/js/bootstrap
。
假设咱们但愿,import 'bootsrap'
默认去找 css
文件的话,能够配置 resolve.mainFields
为:
//webpack.config.js
module.exports = {
//....
resolve: {
mainFields: ['style', 'main']
}
}
复制代码
目前为止咱们 webpack
的配置,都定义在了 webpack.config.js
中,对于须要区分是开发环境仍是生产环境的状况,咱们根据 process.env.NODE_ENV
去进行了区分配置,可是配置文件中若是有多处须要区分环境的配置,这种显然不是一个好办法。
更好的作法是建立多个配置文件,如: webpack.base.js
、webpack.dev.js
、webpack.prod.js
。
webpack.base.js
定义公共的配置webpack.dev.js
:定义开发环境的配置webpack.prod.js
:定义生产环境的配置webpack-merge 专为 webpack
设计,提供了一个 merge
函数,用于链接数组,合并对象。
npm install webpack-merge -D
复制代码
const merge = require('webpack-merge');
merge({
devtool: 'cheap-module-eval-source-map',
module: {
rules: [
{a: 1}
]
},
plugins: [1,2,3]
}, {
devtool: 'none',
mode: "production",
module: {
rules: [
{a: 2},
{b: 1}
]
},
plugins: [4,5,6],
});
//合并后的结果为
{
devtool: 'none',
mode: "production",
module: {
rules: [
{a: 1},
{a: 2},
{b: 1}
]
},
plugins: [1,2,3,4,5,6]
}
复制代码
webpack.config.base.js
中是通用的 webpack
配置,以 webpack.config.dev.js
为例,以下:
//webpack.config.dev.js
const merge = require('webpack-merge');
const baseWebpackConfig = require('./webpack.config.base');
module.exports = merge(baseWebpackConfig, {
mode: 'development'
//...其它的一些配置
});
复制代码
而后修改咱们的 package.json
,指定对应的 config
文件:
//package.json
{
"scripts": {
"dev": "cross-env NODE_ENV=development webpack-dev-server --config=webpack.config.dev.js",
"build": "cross-env NODE_ENV=production webpack --config=webpack.config.prod.js"
},
}
复制代码
你可使用 merge
合并,也可使用 merge.smart
合并,merge.smart
在合并loader
时,会将同一匹配规则的进行合并,webpack-merge
的说明文档中给出了详细的示例。
不少时候,咱们在开发环境中会使用预发环境或者是本地的域名,生产环境中使用线上域名,咱们能够在 webpack
定义环境变量,而后在代码中使用。
使用 webpack
内置插件 DefinePlugin
来定义环境变量。
DefinePlugin
中的每一个键,是一个标识符.
value
是一个字符串,会被当作 code
片断value
不是一个字符串,会被stringify
value
是一个对象,正常对象定义便可key
中有 typeof
,它只针对 typeof
调用定义//webpack.config.dev.js
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.DefinePlugin({
DEV: JSON.stringify('dev'), //字符串
FLAG: 'true' //FLAG 是个布尔类型
})
]
}
复制代码
//index.js
if(DEV === 'dev') {
//开发环境
}else {
//生产环境
}
复制代码
假设前端在3000端口,服务端在4000端口,咱们经过 webpack
配置的方式去实现跨域。
首先,咱们在本地建立一个 server.js
:
let express = require('express');
let app = express();
app.get('/api/user', (req, res) => {
res.json({name: '刘小夕'});
});
app.listen(4000);
复制代码
执行代码(run code
),如今咱们能够在浏览器中访问到此接口: http://localhost:4000/api/user
。
在 index.js
中请求 /api/user
,修改 index.js
以下:
//须要将 localhost:3000 转发到 localhost:4000(服务端) 端口
fetch("/api/user")
.then(response => response.json())
.then(data => console.log(data))
.catch(err => console.log(err));
复制代码
咱们但愿经过配置代理的方式,去访问 4000 的接口。
修改 webpack
配置:
//webpack.config.js
module.exports = {
//...
devServer: {
proxy: {
"/api": "http://localhost:4000"
}
}
}
复制代码
从新执行 npm run dev
,能够看到控制台打印出来了 {name: "刘小夕"}
,实现了跨域。
大多状况,后端提供的接口并不包含 /api
,即:/user
,/info
、/list
等,配置代理时,咱们不可能罗列出每个api。
修改咱们的服务端代码,并从新执行。
//server.js
let express = require('express');
let app = express();
app.get('/user', (req, res) => {
res.json({name: '刘小夕'});
});
app.listen(4000);
复制代码
尽管后端的接口并不包含 /api
,咱们在请求后端接口时,仍然以 /api
开头,在配置代理时,去掉 /api
,修改配置:
//webpack.config.js
module.exports = {
//...
devServer: {
proxy: {
'/api': {
target: 'http://localhost:4000',
pathRewrite: {
'/api': ''
}
}
}
}
}
复制代码
从新执行 npm run dev
,在浏览器中访问: http://localhost:3000/
,控制台中也打印出了{name: "刘小夕"}
,跨域成功,
简单数据模拟
module.exports = {
devServer: {
before(app) {
app.get('/user', (req, res) => {
res.json({name: '刘小夕'})
})
}
}
}
复制代码
在 src/index.js
中直接请求 /user
接口。
fetch("user")
.then(response => response.json())
.then(data => console.log(data))
.catch(err => console.log(err));
复制代码
使用 mocker-api mock数据接口
mocker-api 为 REST API 建立模拟 API。在没有实际 REST API 服务器的状况下测试应用程序时,它会颇有用。
npm install mocker-api -D
复制代码
module.exports = {
'GET /user': {name: '刘小夕'},
'POST /login/account': (req, res) => {
const { password, username } = req.body
if (password === '888888' && username === 'admin') {
return res.send({
status: 'ok',
code: 0,
token: 'sdfsdfsdfdsf',
data: { id: 1, name: '刘小夕' }
})
} else {
return res.send({ status: 'error', code: 403 })
}
}
}
复制代码
webpack.config.base.js
:const apiMocker = require('mocker-api');
module.export = {
//...
devServer: {
before(app){
apiMocker(app, path.resolve('./mock/mocker.js'))
}
}
}
复制代码
这样,咱们就能够直接在代码中像请求后端接口同样对mock数据进行请求。
npm run dev
,能够看到,控制台成功打印出来 {name: '刘小夕'}
src/index.js
,检查下POST接口是否成功//src/index.js
fetch("/login/account", {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: "admin",
password: "888888"
})
})
.then(response => response.json())
.then(data => console.log(data))
.catch(err => console.log(err));
复制代码
能够在控制台中看到接口返回的成功的数据。
进阶篇就到这里结束啦,最后一篇是优化篇,下周一带上小板凳和瓜子来约。
若是本文对你有帮助的话,给本文点个赞吧。
关注公众号
加入技术交流群
参考: