本文将继续引入更多的 webpack 配置,建议先阅读【webpack 系列】基础篇的内容。如果发现文中有任何错误,请在评论区指正。本文所有代码都可在 github 找到。

打包多页应用

之前我们配置的是一个单页的应用,但是我们的应用可能需要是个多页应用。下面我们来进行多页应用的 webpack 配置。

先看一下我们的目录结构

├── public
│   ├── detail.html
│   └── index.html
├── src
│   ├── detail-entry.js
│   ├── index-entry.js

public 下面有 index.htmldetail.html 两个页面,对应 src 下面有 index-entry.jsdetail-entry.js 两个入口文件。

webpack.config.js 配置

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
// ... module.exports = {
entry: {
index: path.resolve(__dirname, 'src/index-entry.js'),
detail: path.resolve(__dirname, 'src/detail-entry.js')
},
output: {
path: path.resolve(__dirname, 'dist'), // 输出目录
filename: '[name].[hash:6].js', // 输出文件名
},
plugins: [
// index.html
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'public/index.html'), // 指定模板文件,不指定会生成默认的 index.html 文件
filename: 'index.html', // 打包后的文件名
chunks: ['index'] // 指定引入的 js 文件,对应在 entry 配置的 chunkName
}),
// detail.html
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'public/detail.html'), // 指定模板文件,不指定会生成默认的 index.html 文件
filename: 'detail.html', // 打包后的文件名
chunks: ['detail'] // 指定引入的 js 文件,对应在 entry 配置的 chunkName
}),
// 打包前自动清除dist目录
new CleanWebpackPlugin()
]
}

npm run build 之后可以看到生成的 dist 目录如下

dist
├── assets
│   └── author_ee489e.jpg
├── detail.dbcb15.js
├── detail.dbcb15.js.map
├── detail.html
├── index.dbcb15.js
├── index.dbcb15.js.map
└── index.html

index.html 页面中已经引入了打包好的 index.dbcb15.js 文件,detail.html 文件也已经引入了 detail.dbcb15.js 文件。更多配置请查看 html-webpack-plugin

将 CSS 样式单独抽离生成文件

webpack4css 模块支持的完善以及在处理 css 文件提取的方式上也做了些调整,由 mini-css-extract-plugin 来代替之前使用的 extract-text-webpack-plugin,使用方式很简单。

该插件将 css 提取到单独的文件中,为每个包含 cssjs 文件创建一个 css 文件,支持 csssourcemap 的按需加载。

extract-text-webpack-plugin 相比有如下优点

  1. 异步加载
  2. 没有重复的编译(性能)
  3. 更容易使用
  4. 特定于 css

安装 extract-text-webpack-plugin

npm i -D mini-css-extract-plugin

配置 webpack.config.js

// webpack.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// ... module.exports = {
// ...
module: {
rules: [
{
test: /\.(c|le)ss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader'],
exclude: /node_modules/
},
{
test: /\.sass$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'],
exclude: /node_modules/
},
// ...
]
},
plugins: [
// ...
new MiniCssExtractPlugin({
filename: 'css/[name].[hash:6].css'
})
]
}

npm run build 之后会发现在 dist/css 目录有了抽离出来的 css 文件了。

这时我们发现两个问题:

  1. 打包生成的 css 文件没有进行压缩。
  2. 所有文件命名的 hash 部分都是一样的,存在缓存问题。

对 css 文件进行压缩

通过 optimize-css-assets-webpack-plugin 插件压缩 css 代码

npm i -D optimize-css-assets-webpack-plugin

配置 webpack.config.js

// webpack.config.js
//...
const OptimizeCssPlugin = require('optimize-css-assets-webpack-plugin'); module.exports = {
//...
plugins: [
//...
new OptimizeCssPlugin()
]
}

这样就可以对 css 文件进行压缩了。

对于第二个问题,我们首先需要了解下 hashchunkHashcontentHash 的区别。

hash、chunkhash、contenthash 的区别和使用

hash

hash 是基于整个 module identifier 序列计算得到的,webpack 默认为给各个模块分配一个 id 以作标识,用来处理模块之间的依赖关系,默认的 id 命名规则是根据模块引入的顺序赋予一个整数(123...)。任意修改、增加、删除一个模块的依赖,都会对整个 id 序列造成影响,从而改变 hash 值。也就是每次修改或者增删任何一个文件,所有文件名的 hash 值都将改变,整个项目的文件缓存都将失效。

output: {
path: path.resolve(__dirname, 'dist'), // 输出目录
filename: '[name].[hash:6].js', // 输出文件名
} new MiniCssExtractPlugin({
filename: 'css/[name].[hash:6].css'
})

可以看到打包后的 jscss 文件的 hash 值是一样的,所以对于没有发生改变的模块而言,这样做是不合理的。

当然可以看到,对于图片等资源该 hash 还是可以生成一个唯一值的。

chunkhash

chunkhash 根据不同的入口文件进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。我们将 filename 配置成 chunkhash 来看一下打包的结果。

output: {
path: path.resolve(__dirname, 'dist'), // 输出目录
filename: '[name].[chunkhash:6].js', // 输出文件名
} new MiniCssExtractPlugin({
filename: 'css/[name].[chunkhash:6].css'
})



可以看到此时打包之后的 index.jsdetail.jschunkhash 是不一样的。但是会发现 index.jsindex.css 以及 detail.jsdetail.csschunkhash 是一致的,并且任意改动 js 或者 css 都会引起对应的 cssjs 文件的 chunkhash 的改变,这是不合理的。所以这里抽离出来的 css 文件将使用 contenthash,来区分 css 文件和 js 文件的更新。

contenthash

contenthash 是针对文件内容级别的,只有你自己模块的内容变了,那么 hash 值才改变。

output: {
path: path.resolve(__dirname, 'dist'), // 输出目录
filename: '[name].[chunkhash:6].js', // 输出文件名
} new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:6].css'
})



OK,可以看到分离出来的 css 文件已经和入口文件的 hash 值区分开了。

如何使用

为了实现理想的缓存,我们一般这样使用他们:

  1. JS 文件使用 chunkhash
  2. 抽离的 CSS 样式文件使用 contenthash
  3. gif|png|jpe?g|eot|woff|ttf|svg|pdf 等使用 hash

按需加载

很多时候我们并不需要在一个页面中一次性加载所有的 js 或者 css 文件,而是应该是需要用到时才去加载相应的 js 或者 css 文件。

import()

比如,现在我们需要点击一个按钮才会使用对应的 jscss 文件,需要 import() 语法:

// index-entry.js

import './index.sass';
//...
const handle = () => import('./handle');
const handle2 = () => import('./handle2'); document.querySelector('#btn').onclick = () => {
handle().then(module => {
module.handleClick();
}); handle2().then(module => {
module.default();
});
}
// handle.js

import './handle.css';

export function handleClick () {
console.log('handleClick');
}
// handle2.js

export default function handleClick () {
console.log('handleClick2');
}

npm run build 可以看到,多了这 3 个文件,并且只有在我们点击该按钮是才会去加载这 3 个文件。

webpackChunkName

这些文件可能不太好区分,我们可以通过设置 webpackChunkName 来定义生成的文件名

// index-entry.js
const handle = () => import(/* webpackChunkName: "handle" */ './handle');
const handle2 = () => import(/* webpackChunkName: "handle2" */ './handle2');

我们再将这些文件的 hash 长度设置为 8 加以区分

// webpack.config.js
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'), // 输出目录
filename: '[name].[chunkhash:6].js', // 输出文件名
chunkFilename: '[name].[chunkhash:8].js'
}
// ...
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:6].css',
chunkFilename: 'css/[name].[contenthash:8].css'
})
}

npm run build 之后查看



当然我们也可以将 handlehandle2 文件的 webpackChunkName 设置成一样的,这样这两个文件将会打包在一起生成一个文件,可以减少请求数量。

热更新( HMR, Hot Module Replacement )

开发过程中,我们希望在浏览器不刷新页面的情况下能够去加载我们修改的代码,来提高我们的开发效率。我们来看下如何配置:

  1. 打开 webpack-dev-server 的热更新开关
  2. 使用 HotModuleReplacementPlugin 插件

HotModuleReplacementPlugin 插件是 Webpack 自带的,在 webpack.config.js 直接配置

// webpack.config.js

module.exports = {
devServer: {
//...
hot: true
},
plugins: [
//...
new webpack.HotModuleReplacementPlugin() // 热更新插件
]
}

在入口文件添加

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

这样就完成了热更新的配置,但是此时 webpack 打包却报错了。



搜了一下相关的问题,在开发环境中我们使用了 HotModuleReplacementPlugin 此时需要使用 hash 来输出文件,使用 chunkhash 会导致 webpack 报错,而生产环境则没有问题。但是现在我们只是通过 process.env.NODE_ENV 这个变量来区分环境,这显然不是一个很好的方式。

我们最好能够需要区分一下开发环境和生产环境的配置文件。

定义不同环境的配置

我们可以给不同的环境定义不同的配置文件,但是这些文件将会有大量相似的配置,这时我们可以这样来定义文件:

  1. webpack.base.js:定义公共的配置
  2. webpack.dev.js:定义开发环境的配置
  3. webpack.prod.js:定义生产环境的配置

我们可以将一些公共的配置抽离到 webpack.base.js,然后在 webpack.dev.jswebpack.prod.js 进行对应环境的配置。我们还需要通过 webpack-merge 来合并两个配置文件。

安装 webpack-merge

npm i -D webpack-merge

现在 webpack.dev.js 就是这样的

// webpack.dev.js

const path = require('path');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const merge = require('webpack-merge');
const baseConfig = require('./webpack.config.base'); module.exports = merge(baseConfig, {
mode: 'development',
devtool: 'inline-source-map',
devServer: {
contentBase: path.join(__dirname, 'dist'),
port: '9000', // 默认是8080
compress: true, // 是否启用 gzip 压缩
hot: true
},
output: {
path: path.resolve(__dirname, 'dist'), // 输出目录
filename: '[name].[hash:6].js', // 输出文件名
chunkFilename: '[name].[hash:8].js'
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[hash:6].css',
chunkFilename: 'css/[name].[hash:8].css'
}),
new webpack.HotModuleReplacementPlugin() // 热更新插件
]
});

同时需要在 package.json 中指定我们的配置文件

// 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.pro.js"
},

这时我们就很优雅的区分开不同环境的配置了。

拷贝静态资源

有时候我们需要在 html 中直接引用一个打包好的第三方插件库,这个库不需要通过 webpack 编译。比如我们 lib 目录下有个 lib-a.js,需要在 public/index.html 中直接引用它。

<!-- public/index.html -->
<script src="/lib/lib-a.js"></script>

这时 build 之后会发现 dist 下是没有 lib 目录的,这时会找不到这个文件。这时我们需要借助 CopyWebpackPlugin 这个插件来帮助我们把根目录下的 lib 目录拷贝到 dist 目录下面。

首先安装 CopyWebpackPlugin

npm i -D CopyWebpackPlugin

配置 webpack.config.js

// webpack.config.js

const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
//...
plugins: [
//...
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, 'lib'),
to: path.resolve(__dirname, 'dist/lib')
}
])
]
}

这时后运行 npm run build 就会发现,dist 目录下已经有了 lib目录及文件了。

更多的配置请查看copy-webpack-plugin

Resolve 配置

Webpack 在启动后会从配置的入口模块出发找出所有依赖的模块,Resolve 配置 Webpack 如何寻找模块所对应的文件。 Webpack 内置 JavaScript 模块化语法解析功能,默认会采用模块化标准里约定好的规则去寻找,但你也可以根据自己的需要修改默认的规则。

alias

resolve.alias 配置项通过别名来把原导入路径映射成一个新的导入路径。

比如我们在 index-entry.js 中引入 lib/lib-b.js,你可能需要这样引入

import '../lib/lib-b.js';

而当目录层级比较深时,这个相对路径就会变得不好辨认了。这时我们可以配置 lib 的一个别名。

// webpack.config.js

module.exports = {
//...
resolve: {
alias: {
'@lib': path.resolve(__dirname, 'lib') // 为lib目录添加别名
}
}
}

这时无论你处于目录的哪个层级,你只需要这样引入

import '@lib/lib-b.js';

extensions

如果在导入文件时没有带后缀名,webpack 会自动带上后缀后去尝试访问文件是否存在。 resolve.extensions 用于配置在尝试过程中用到的后缀列表,默认是

extensions: ['.js', '.json']

就是说当遇到 import '@lib/lib-b'; 时,webpack 会先去寻找 @lib/lib-b.js 文件,如果该文件不存在就去寻找 @lib/lib-b.json 文件, 如果还是找不到就报错。

如果你想优先使用其他后缀文件,比如 .ts 文件,可以这样配置

// webpack.config.js

module.exports = {
//...
resolve: {
alias: {
'@lib': path.resolve(__dirname, 'lib'), // 为lib目录添加别名
extensions: ['.ts', '.js', '.json'] // 从左往右
}
}
}

这样就会先去找 .ts 了。不过一般我们会将高频的后缀放在前面,并且数组不要太长,减少尝试次数,不然会影响打包速度。

现在我们引入 js 文件时可以省略后缀名了。

modules

resolve.modules 配置 webpack 去哪些目录下寻找第三方模块,默认是只会去 node_modules 目录下寻找。如果项目中某个文件夹下的模块经常被导入,不希望写很长的路径,比如 import '../../../components/link',那么就可以通过配置 resolve.modules 来简化。

// webpack.config.js

module.exports = {
//...
resolve: {
modules: ['./src/components', 'node_modules'] // 从左到右查找
}
}

这时,你就可以通过 import 'link' 引入了。

mainFields

有一些第三方模块会针对不同环境提供几份代码。例如分别提供采用 es5es62 份代码,这 2 份代码的位置写在 package.json 文件里。

{
"jsnext:main": "es/index.js",// 采用 ES6 语法的代码入口文件
"main": "lib/index.js" // 采用 ES5 语法的代码入口文件
}

webpack 会根据 mainFields 的配置去决定优先采用那份代码, mainFields 默认配置如下:

mainFields: ['browser', 'main']

假如你想优先采用 ES6 的那份代码,可以这样配置:

mainFields: ['jsnext:main', 'browser', 'main']

enforceExtension

resolve.enforceExtension 如果配置为 true,那么所有导入语句都必须要带文件后缀。

enforceModuleExtension

enforceModuleExtensionenforceExtension 作用类似,但 enforceModuleExtension 只对 node_modules下的模块生效。 因为安装的第三方模块中大多数导入语句没带文件后缀,如果这时你配置了 enforceExtensiontrue,那么就需要配置 enforceModuleExtension: false来兼容第三方模块。

利用 webpack 解决跨域问题

本地开发时,前端项目的端口号是 9000,但是服务端可能是 9001,根据浏览器的同源策略,是不能直接请求到后端服务的。当然你可以在后端配置 CORS 相关的头部来实现跨域,其实也可以通过 webpack 的配置来解决跨域问题。

首先,我们起一个后端服务,安装 koakoa-router

npm i -D koa koa-router

新建 server/index.js

// server/index.js

const Koa = require('koa');
const KoaRouter = require('koa-router'); const app = new Koa(); // 创建 router 实例对象
const router = new KoaRouter(); // 注册路由
router.get('/user', async (ctx, next) => {
ctx.body = {
code: 0,
data: {
name: '阿林十一'
},
msg: 'success'
};
}); app.use(router.routes()); // 添加路由中间件
app.use(router.allowedMethods()); // 对请求进行一些限制处理 app.listen(9001);

使用 node server/index.js 启动服务后,在 http://localhost:9001/user 可以访问结果。

之后再修改 handle.js,在点击按钮之后会请求接口

import './handle.css';

export function handleClick () {
console.log('handleClick'); fetch('/api/user')
.then(r => r.json())
.then(data => console.log(data))
.catch(err => console.log(err));
}

这是会发现接口报 404,下面我们配置一下 webpack.config.dev.js

// webpack.config.dev.js

module.exports = {
//...
proxy: {
'/api': {
target: 'http://127.0.0.1:9001/',
pathRewrite: {
'^/api': ''
}
}
}
}

请求到 http://localhost:9000/api/user 现在会被代理到请求 http://localhost:9001/user。点击按钮发起请求:

最后

现在,我们对 webpack 的配置有了更进一步的了解了,快动手试试吧。本文所有代码可以查看 github

后续将会继续推出 webpack 系列的其他内容哦~

喜欢本文的话点个赞吧~

更多精彩内容,欢迎关注微信公众号~

【webpack 系列】进阶篇的更多相关文章

  1. SQL Server调优系列进阶篇(查询语句运行几个指标值监测)

    前言 上一篇我们分析了查询优化器的工作方式,其中包括:查询优化器的详细运行步骤.筛选条件分析.索引项优化等信息. 本篇我们分析在我们运行的过程中几个关键指标值的检测. 通过这些指标值来分析语句的运行问 ...

  2. SQL Server调优系列进阶篇(深入剖析统计信息)

    前言 经过前几篇的分析,其实大体已经初窥到SQL Server统计信息的重要性了,所以本篇就要祭出这个神器了. 该篇内容会很长,坐好板凳,瓜子零食之类... 不废话,进正题 技术准备 数据库版本为SQ ...

  3. SQL Server调优系列进阶篇(如何索引调优)

    前言 上一篇我们分析了数据库中的统计信息的作用,我们已经了解了数据库如何通过统计信息来掌控数据库中各个表的内容分布.不清楚的童鞋可以点击参考. 作为调优系列的文章,数据库的索引肯定是不能少的了,所以本 ...

  4. SQL Server调优系列进阶篇(如何维护数据库索引)

    前言 上一篇我们研究了如何利用索引在数据库里面调优,简要的介绍了索引的原理,更重要的分析了如何选择索引以及索引的利弊项,有兴趣的可以点击查看. 本篇延续上一篇的内容,继续分析索引这块,侧重索引项的日常 ...

  5. SQL Server调优系列进阶篇(查询优化器的运行方式)

    前言 前面我们的几篇文章介绍了一系列关于运算符的基础介绍,以及各个运算符的优化方式和技巧.其中涵盖:查看执行计划的方式.几种数据集常用的连接方式.联合运算符方式.并行运算符等一系列的我们常见的运算符. ...

  6. SQL Server调优系列进阶篇 - 查询优化器的运行方式

    前言 前面我们的几篇文章介绍了一系列关于运算符的基础介绍,以及各个运算符的优化方式和技巧.其中涵盖:查看执行计划的方式.几种数据集常用的连接方式.联合运算符方式.并行运算符等一系列的我们常见的运算符. ...

  7. SQL Server 调优系列进阶篇 - 查询优化器的运行方式

    前言 前面我们的几篇文章介绍了一系列关于运算符的基础介绍,以及各个运算符的优化方式和技巧.其中涵盖:查看执行计划的方式.几种数据集常用的连接方式.联合运算符方式.并行运算符等一系列的我们常见的运算符. ...

  8. 《手把手教你》系列进阶篇之1-python+ selenium自动化测试 - python基础扫盲(详细教程)

    1. 简介 如果你从一开始就跟着宏哥看博客文章到这里,基础篇和练习篇的文章.如果你认真看过,并且手动去敲过每一篇的脚本代码,那边恭喜你,至少说你算真正会利用Python+Selenium编写自动化脚本 ...

  9. SQL Server调优系列进阶篇 - 如何索引调优

    前言 上一篇我们分析了数据库中的统计信息的作用,我们已经了解了数据库如何通过统计信息来掌控数据库中各个表的内容分布.不清楚的童鞋可以点击参考. 作为调优系列的文章,数据库的索引肯定是不能少的了,所以本 ...

  10. SQL Server调优系列进阶篇 - 深入剖析统计信息

    前言 经过前几篇的分析,其实大体已经初窥到SQL Server统计信息的重要性了,所以本篇就要祭出这个神器了. 该篇内容会很长,坐好板凳,瓜子零食之类... 不废话,进正题 技术准备 数据库版本为SQ ...

随机推荐

  1. 一些常用的 CSS 技巧和知识点

    作为一名前端工程师,CSS 是必备技能之一,然而在日常开发中,总有那么些时候,面对着炫酷的效果图,脑子里的 CSS 属性却一片空白,于是只能借助搜索引擎,在一堆复杂的介绍中找到需要的内容复制粘贴.有没 ...

  2. BUI Webapp用于项目中的一点小心得

    接触BUI也有一段时间,也用在了移动端的项目开发中,总的来说,该框架用起来也挺灵活的,控件可以自由定制,前提是自己能认真地学习该框架的api,因为api里面说的东西比较详细,如果没有仔细看的,可能有些 ...

  3. java多线程之间的通信

    目的 如何让两个线程依次执行? 那如何让 两个线程按照指定方式有序交叉运行呢? 四个线程 A B C D,其中 D 要等到 A B C 全执行完毕后才执行,而且 A B C 是同步运行的 三个运动员各 ...

  4. 025.掌握Service-SVC基础使用

    一 Service简介 1.1 Service概念 Service是Kubernetes的核心概念,通过创建Service,可以为一组具有相同功能的容器应用提供一个统一的入口地址,并且将请求负载分发到 ...

  5. C#桌面开发的未来WebWindow

    目录 WebWindow 源码 作者博客 基于Chromium的Edge 体验 体验方式一: 体验方式二: 遗留的问题 WebWindow WebWindow是跨平台的库. Web Window的当前 ...

  6. C#制作密码文本框

    2020-03-14 每日一例第7天 1.新建窗体windowform,修改text值: 2.两个按钮后台代码: private void button1_Click(object sender, E ...

  7. 判断 tableZen 是否有 横向滚动条

    判断 tableZen 是否有 横向滚动条 const outWidth = this.$refs.tableInnerZen.$el.clientWidth ].$el.clientWidth

  8. Java并发编程学习前期知识下篇

    Java并发编程学习前期知识下篇 通过上一篇<Java并发编程学习前期知识上篇>我们知道了在Java并发中的可见性是什么?volatile的定义以及JMM的定义.我们先来看看几个大厂真实的 ...

  9. MySQL设置外键报错 #1452 - Cannot add or update a child row: a foreign key constraint fails 解决方法

    MySQL数据库,当我尝试在A表中设置B表的主键为外键时,报出错误:#1452 - Cannot add or update a child row: a foreign key constraint ...

  10. 英伟达GPU虚拟化---申请英伟达测试License

    此文基于全新的License 2.0系统,针对vGPU License的试用申请以及软件下载和License管理进行了详细的说明,方便今后我们申请测试License,快速验证GPU的功能. 试用步骤: ...