webpack的基本使用2
1、webpack中的module、chunk和bundle的区别
webpack中的module、chunk和bundle的区别:
- 对于一份同逻辑的代码,当我们手写下一个一个的文件,它们无论是 ESM 还是 commonJS 或是 AMD,他们都是 module ;
- 当我们写的 module 源文件用 webpack 进行打包时,webpack 会根据文件引用关系生成 chunk 文件,webpack 会对这个 chunk 文件进行一些操作;
- webpack 处理好 chunk 文件后,最后会输出 bundle 文件,这个 bundle 文件包含了经过加载和编译的最终源文件,所以它可以直接在浏览器中运行。
module、chunk 和 bundle 其实就是同一份逻辑代码在不同转换场景下的取了三个名字,我们直接写出来的是 module,webpack 处理时是 chunk,最后生成浏览器可以直接运行的 bundle。
一般来说一个 chunk 对应一个 bundle,比如上图中的 utils.js -> chunks 1 -> utils.bundle.js
;但我们也可以用一些插件进行文件分离,比如说上图中,就用 MiniCssExtractPlugin
插件从 chunks 0 中抽离出了 index.bundle.css
文件。
参考:https://www.cnblogs.com/skychx/p/webpack-module-chunk-bundle.html
2、manifest
在使用 webpack 构建的典型应用程序或站点中,有三种主要的代码类型:
- 你或你的团队编写的源码。
- 你的源码会依赖的任何第三方的 library 或 "vendor" 代码。
- webpack 的 runtime 和 manifest,管理所有模块的交互。
2.1、runtime和manifest
runtime 以及伴随的 manifest 数据,主要是指:在浏览器运行过程中,webpack 用来连接模块化应用程序所需的所有代码。它包含:在模块交互时,连接模块所需的加载和解析逻辑。包括:已经加载到浏览器中的连接模块逻辑,以及尚未加载模块的延迟加载逻辑。
webpack 和 webpack 插件是如何“知道”应该哪些文件生成的呢?答案是,webpack 通过 manifest,可以追踪所有模块到输出 bundle 之间的映射。在你的应用程序中,形如 index.html
文件、一些 bundle 和各种资源,都必须以某种方式加载和链接到应用程序,一旦被加载到浏览器中。在经过打包、压缩、为延迟加载而拆分为细小的 chunk 这些 webpack 优化
之后,你精心安排的 /src
目录的文件结构都已经不再存在。所以 webpack 如何管理所有所需模块之间的交互呢?这就是 manifest 数据用途的由来。
当 compiler 开始执行、解析和映射应用程序时,它会保留所有模块的详细要点。这个数据集合称为 "manifest",当完成打包并发送到浏览器时,runtime 会通过 manifest 来解析和加载模块。无论你选择哪种 模块语法,那些 import
或 require
语句现在都已经转换为 __webpack_require__
方法,此方法指向模块标识符(module identifier)。通过使用 manifest 中的数据,runtime 将能够检索这些标识符,找出每个标识符背后对应的模块。
通过使用内容散列(content hash)作为 bundle 文件的名称,这样在文件内容修改时,会计算出新的 hash,浏览器会使用新的名称加载文件,从而使缓存无效。一旦你开始这样做,你会立即注意到一些有趣的行为。即使某些内容明显没有修改,某些 hash 还是会改变。这是因为,注入的 runtime 和 manifest 在每次构建后都会发生变化。
3、Babel
Babel其实是一个编译JavaScript的平台,它可以编译代码帮你达到以下目的:
- 让你能使用最新的JavaScript代码(ES6,ES7...),而不用管新标准是否被当前使用的浏览器完全支持;
- 让你能使用基于JavaScript进行了拓展的语言,比如React的JSX;
3.1、Babel的安装与配置
Babel其实是几个模块化的包,其核心功能位于称为babel-core
的npm包中,webpack可以把其不同的包整合在一起使用,对于每一个你需要的功能或拓展,你都需要安装单独的包.
用得最多的是解析Es6的babel-preset-
包和解析JSX的babel-preset-react
包。
下面测试使用 babel 来解析支持 es6 和 react 语法。我们先来一次性安装这些依赖包:
// npm一次性安装多个依赖模块,模块之间用空格隔开 npm install --save-dev babel-core babel-loader babel-preset-env babel-preset-react
在webpack
中配置Babel的方法如下:
module.exports = {
entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
output: {
path: __dirname + "/public",//打包后的文件存放的地方
filename: "bundle.js"//打包后输出文件的文件名
},
devtool: 'eval-source-map',
devServer: {
contentBase: "./public",//本地服务器所加载的页面所在的目录
historyApiFallback: true,//不跳转
inline: true//实时刷新
},
module: {
rules: [
{
test: /(\.jsx|\.js)$/,
use: {
loader: "babel-loader",
options: {
presets: [
"env", "react"
]
}
},
exclude: /node_modules/
}
]
}
};
配置完以上选项后就能支持ES6以及JSX的语法了。测试:
npm install --save react react-dom
//Greeter,js
import React, {Component} from 'react'
import config from './config.json';
class Greeter extends Component{
render() {
return (
<div>
{config.greetText}
</div>
);
}
}
export default Greeter
// main.js
import React from 'react';
import {render} from 'react-dom';
import Greeter from './Greeter';
render(<Greeter />, document.getElementById('root'));
直接编译或者在本地服务器上就能看到运行结果
3.2、.babelrc 文件
Babel其实可以完全在 webpack.config.js
中进行配置,但是考虑到babel具有非常多的配置选项,在单一的webpack.config.js
文件中进行配置往往使得这个文件显得太复杂,因此一些开发者支持把babel的配置选项放在一个单独的名为 ".babelrc" 的配置文件中。webpack会自动调用.babelrc
里的babel配置选项
module.exports = {
entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
output: {
path: __dirname + "/public",//打包后的文件存放的地方
filename: "bundle.js"//打包后输出文件的文件名
},
devtool: 'eval-source-map',
devServer: {
contentBase: "./public",//本地服务器所加载的页面所在的目录
historyApiFallback: true,//不跳转
inline: true//实时刷新
},
module: {
rules: [
{
test: /(\.jsx|\.js)$/,
use: {
loader: "babel-loader"
},
exclude: /node_modules/
}
]
}
};
//.babelrc
{
"presets": ["react", "env"]
}
4、清理输出文件夹的文件(CleanWebpackPlugin)
由于之前的一些编译可能会导致用于输出的文件夹中包含了一些没必要的文件,显得比较杂乱,我们可以用clean-webpack-plugin
插件清理输出的文件夹中的文件。
该插件会在每次构建前清理输出文件夹,只会生成用到的文件。
npm install clean-webpack-plugin --save-dev
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js'
},
plugins: [
new CleanWebpackPlugin(['public/*.*'], {
root: __dirname,
verbose: true, //开启在控制台输出信息
dry: false
})
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
4.1、webpack-dev-server和CleanWebpackPlugin同时使用的问题
有一个问题是当热加载和CleanWebpackPlugin插件同时使用时,运行dev-sever会重新编译,由此也重新使用清理插件,目标文件夹中的文件将被全部删除,但是此时并不生成新的文件。
虽然没报什么问题,代码也可以跑,但是还是困惑了我很久。这个问题应该是运行webpack-dev-server时webpack会重新编译,由此使用了清理插件,但是dev-server并不会在工作目录中生成编译的文件,而是在内存中生成,所以看不到,但是代码跑的没问题。
要想解决这个问题,我在GitHub上看到了一个issue,但是尝试了好像会报错:https://github.com/johnagan/clean-webpack-plugin/issues/96。过后会继续关注该问题。
5、resolve(解析)
这些选项能设置模块如何被解析。
5.1、resolve.alias(配置模块路径别名)
在我们引入一些模块文件时,可能文件路径层次比较深,这样的话就会导致相对路径的写法比较长。通过 resolve.alias 能够将一些路径配置成指定的别名,能够让我们简写路径。
module.exports = { ... resolve: { alias: { 'vue$': 'vue/dist/vue.esm.js', '@': resolve('src'), //由此引入src文件夹下的文件就可以写成:import xxx from @/xxx.js } }, }
5.2、resolve.extensions(指定文件扩展名)
通过指定 resolve 字段的extensions值可以指定自动解析的文件扩展名。
webpack的默认配置为:
module.exports = { //... resolve: { extensions: ['.wasm', '.mjs', '.js', '.json'] } };
由此用户在引入模块一些文件时就可以不带扩展,比如:
//引入 mian.js import aaa from '../path/to/main';
如果我们自定义该选项,就会覆盖默认数组即webpack的默认配置,这就意味着 webpack 将不再尝试使用默认扩展来解析模块,对于需要指定自动解析的文件扩展名必须得写入该数组中。
5.3、resolve.modules
告诉 webpack 解析模块时应该去哪个目录搜索该模块。
webpack的默认配置为:
module.exports = { //... resolve: { modules: ['node_modules'] } };
6、devServer配置
webpack-dev-server 能够用来启动一个本地服务器,接受HTTP请求,通过这个服务器,可以使用HTTP协议进行页面的效果测试(不启动服务器就只能通过file://xxx的方式来看页面)。除此之外,还能够实现许多额外的功能,比如热加载(一旦修改项目文件,就自动重新加载服务器)
6.1、devServer.proxy(服务器代理)
服务器代理功能一般用来解决开发环境跨域问题。
(跨域问题场景:由于 webpack-dev-server 是一个本地开发服务器,所以我们的应用在开发阶段是独立运行在 localhost 的一个端口上,而后端服务又是运行在另外一个地址上。但是最终上线过后,我们的应用一般又会和后端服务部署到同源地址下。那这样就会出现一个非常常见的问题:在实际生产环境中能够直接访问的 API,回到我们的开发环境后,再次访问这些 API 就会产生跨域请求问题。解决这种开发阶段跨域请求问题最好的办法,就是在开发服务器中配置一个后端 API 的代理服务,也就是把后端接口服务代理到本地的开发服务地址。webpack-dev-server 就支持直接通过配置的方式,添加代理服务。例子可参考:https://blog.csdn.net/zwkkkk1/article/details/81541057)
webpack proxy只能用作于开发阶段,临时解决本地请求服务器(通常是测试服)产生的跨域问题,并不适用线上环境,因为线上环境时根本就不会启动 devserver 这个东西。
配置:
mmodule.exports = { //... devServer: { proxy: { '/api': 'http://localhost:3000' } } };
此时请求到 /api/xxx
现在会被代理到请求 http://localhost:3000/api/xxx
, 例如 http://localhost:8080/api/user
现在会被代理到请求 http://localhost:3000/api/user
如果你不想始终传递 /api ,则需要重写路径。如下,此时请求到 /api/xxx 现在会被代理到请求 http://localhost:3000/xxx
, 例如 http://localhost:8080/api/user 现在会被代理到请求 http://localhost:3000/user
module.exports = { //... devServer: { proxy: { '/api': { target: 'http://localhost:3000', pathRewrite: {'^/api' : ''} } } } };
要想解决跨域问题将 changeOrigin 参数设置为 true, 本地就会虚拟一个服务器接收你的请求并代你发送该请求:
module.exports = { //... devServer: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true, } } } };
可参考:https://juejin.cn/post/6844903909002051592
6.1.1、devServer.proxy的原理
浏览器和服务器之间有跨域问题,而服务器和服务器之间就不会有跨域问题。前端代理解决跨域问题实际上就是通过启动一个服务器,通过该服务器来代理前端请求,然后将请求发出,由此跟后端服务器就不会有跨域问题。
webpack中的proxy
只是一层代理,用于把指定的路径path
,代理去后端服务的地址,背后使用node来做server。该技术只是webpack在本地开发阶段临时生成了node server,来实现类似nginx 的proxy_pass
的反向代理效果。同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,所以也就不存在跨域问题。
proxy
工作原理实质上是利用http-proxy-middleware
这个http代理中间件,实现请求转发给其他服务器。
proxy 只是在开发环境适用,到生产环境还是需要服务端进行配置,基本就是将限制的请求头进行修改。
6.1.2、http-proxy-middleware的基本介绍
http-proxy-middleware用于将请求转发给其它服务器。
例如:我们当前主机 A 为http://localhost:3000/,现在浏览器发送一个请求,请求接口/api,这个请求的数据在另外一台服务器B上(http://10.119.168.87:4000),这时,就可通过在 A 主机设置代理,直接将请求发送给B主机。
代码示例:
var express = require('express'); var proxy = require('http-proxy-middleware'); var app = express(); app.use('/api', proxy({target: 'http://10.119.168.87:4000', changeOrigin: true})); app.listen(3000);
说明:我们利用 express 在 3000 端口启动了一个小型的服务器,利用了app.use('/api', proxy({target: 'http://10.119.168.87:4000/', changeOrigin: true}))
使发到 3000 端口的 /api 请求转发到了4000端口。即请求http://localhost:3000/api
相当于请求http://10.119.168.87:4000/api
。
7、optimization(优化)
7.1、optimization.splitChunks
optimization.splitChunks 的默认配置如下:
module.exports = { //... optimization: { splitChunks: { chunks: 'async', minSize: 30000, //分割的chunk最小为30kb maxSize: 0, //最大没有限制 minChunks: 1, //要提取的chunk最少应该被引用过一次 maxAsyncRequests: 5, //按需加载并行加载的文件的最大数量 maxInitialRequests: 3, //入口js文件最大并行请求数量 automaticNameDelimiter: '~', //名称连接符 name: true, //可以使用命名规则 cacheGroups: { //分割chunk的组 vendors: { test: /[\\/]node_modules[\\/]/, //node_modules文件会被打包到 vendors 组的chunk中 --> vendors~xxx.js priority: -10 //优先级 }, default: { minChunks: 2, //要提取的chunk最少被引用2次 priority: -20, //优先级 reuseExistingChunk: true //如果当前要打包的模块,和之前已经被提取的模块是同一个,就会复用,而不是重新打包 } } } } };