概述,在webpack里面有四个概念: 入口、出口、插件和loader,进行模块化打包,支持CMD、ADM、commonJS、import等模块化操作。
安装说明:在webpack安装的时候,需要先安装一个全局的webpack,然后在需要的文件夹中安装局部的依赖,webpack webpack-cil脚手架,在webpack3的版本中,它们是集成在一起。本次笔记是针对于:webpack4.x以上的版本
npm install webpack -g //先安装全局的webpack,安装完成后,才可以实验webpack命令,直接打包
npm install webpack webpack-cli -D // 在需要的目录安装webpack
webpack // 在cmd或者vscode中运行这个,就可以打包了,其它的配置,且看后面
安装完成后,需要在项目根目录新建一个
webpack-config.js的配置文件;没有新建这个文件,也可以打包,因为webpack有默认配置。
如果没有安装全局webpack的话,就无法直接使用webpack直接打包,当然,也可以单独是配置脚本,例:
scripts: {
dev: webpack --config webpack-config.js
}
在4.0以后的版本里面,新增了一个多入口打包命令:webpack app.js app1.ja -o bulid.js
在webpack中有生产模式(production)和开发模式(development),默认为开发设置,用,mode参数去设置模式,例:
module.exports = {
mode: 'production' // 模式选择,生产和开发两种
};
特别说明:需要指定mode属性,不然控制台会报出一个警告信息
入口(entry)
入口(entry point),表示 webpack 应该使用哪个模块;进入入口以后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
入口可以是单入口和多入口(使用一个数组或者对象表示),在实际操作中,为了便于优化以及首屏加载问题,会更倾向于第三种方式,既对象的形式。
但是使用对象写法(
entry代码分离),会有一些小问题;假设entry引入了2个js文件,分别是a.js和b.js,但是这个两个文件都引入了同一个依赖,打包以后,就会造成代码重复,非常不利于优化,所以需要使用SplitChunksPlugin来进行防止重复或者是动态倒入的方法。点击查看这种方式的坑点:代码分离。
module.exports = {
entry: './app.js' // 写法一
entry: ['./app.js, main.js'] //写法二
entry: {
app: './app.js',
main: './main.js'
} //写法三
};
出口(output)
如果使用(output)属性表示,这个属性是用来告诉webpack输出它所创建的内容,默认的目录是dist文件夹,在项目的根目录中,也可以指定一个目录导出。
const path = require('path');
module.exports = {
entry: './path/to/my/entry/file.js', // 入口
output: { // 出口
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js' // 单文件合并的写法
filename: '[name].js', // 多入口多文件
chunkFilename: [name][contenthash].js, // 防止缓存,只有在更新以后才会生成随机码
publicPath: 'CDN地址' // 设置CDN服务器地址, 理论上是可以直接打包到CDN服务器上,前提是有权限
}
};
入口文件有配置的,就走filename这个属性,如果是其它文件导入的,就走下面这个,既chunkFilename,在使用的时候,目的就是解决代码更新,而浏览器读取缓存获取不到更新。
在output中,filename属性是用来告诉webpack打包后的文件名称;而path是告诉webpack需要创建的文件夹的位置信息,这个要调用node里面一个模块,既path模块,__dirname是node里面的一个关键字,表示路径,绝对路径
提示:在webpack里面,多个入口如果要合并文件,只能使用数组的方式; 如果需要打包多文件的时,入口需要使用对象,出口使用一个动态的写法,如上代码"多入口文件", [name].js,如果入口是一个对象,出口一定是一个动态写法,否则就会导致打包不正常。
loader
loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
loader的执行顺序,是从下到上,从右到左的执行顺序,所以在设置loader的时候,顺序不能放错,否则,可能会导致打包异常。
个人理解: loader就相当于是一个翻译器,将我们写的代码,进行翻译并处理。使用方法,如下:babel为例
注意:在lodaer里面,配置正则(test)的时候,一定不能加引号,否则会提示lodaer错误。
babel
介绍:babe就是将ES6转成ES5的一个编译器,需要安装3个依赖;虽然Babel可以将ES6的语法转成ES5的语法,但是如果是ES6+里面的内置API就无法去转换了,因此安装一个垫片@babel/polyfill,它主要是为了兼容低版本的浏览器,
babel-loader @babel/core // 主要是语法转换
@babel/preset-env // 这个用来指定ES的版本
@babel/polyfill // 全局垫片,用于转换内置API支持ES5的语法
/* 局部垫片需要安装的依赖项 */
@babel/plugin-transform-runtime
@babel/runtime
webpack配置方式
module: {
rules: [
{
test: /\.m?js$/, // 匹配文件的后缀名
exclude: /(node_modules|bower_components)/, // 需要剔除的目录
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'], // 垫片使用
plugins: ['@babel/plugin-proposal-object-rest-spread'] //额外的
}
}
}
]
}
关于属性use的操作,如果配置项目不是很多的时候,可以不用use这个属性,它们两个是等价的,直接写,例:
rules: [{
test: /\.m?js$/, // 匹配文件的后缀名
loader: 'babel-loader', // 加载的loader
exclude: /(node_modules|bower_components)/, // 需要剔除的目录
options: { //配置项
presets: ['@babel/preset-env'], // 默认写法,不使用配置的写法
presets: [["@babel/preset-env", { // 第二种写法,配置一个babel/polyfill垫片
useBuiltIns: "usage", // 按需加载 entry | usage
targets: {
chrome: "68" // 浏览器目标,也可以跟随一个浏览器的市场份额
}
}]
]
}
}]
关于@babel/polyfill的使用:先安装,需要在被打包的文件中引入 @babel/polyfill这个模块,默认是会打包所有的API内置模块,因此需要一个按需加载,写法:useBuildIns: 'usage'。
默认会报出一个警告,解决方案:如果配置了usage的话,就不需要在被打包的文件种引入@babel/polyfill,如果需要引入的话:则需要用entry替换(局部方式,按照文件需求)
关于局部垫片的使用说明:如果是写的一个插件,就不能使用全局垫片,使用局部垫片需要有两个依赖@babel/plugin-transform-runtime和@babel/runtime。
.babelrc配置方法
在项目根目录需要新建一个单文件,文件名:.babelrc,格式是一个标准的JSON,所以需要使用标准的JSON格式,下面已局部垫片配置为例:
{
"presets":["@babel/preset-env"],
"plugins": [["@babel/plugin-transform-runtime", {
"absoluteRuntime": false,
"corejs": 2, // corjs需要改成2,不改的话会造成打包异常,官方默认是false,可选数字和布尔
"helpers": true,
"regenerator": true,
"useESModules": false
}]
]
}
/* ================ 正常模式下 使用文件名的方式 配置 ========================*/
{
"presets": [["@babel/preset-env", {
"useBuiltIns": "usage",
"targets": {
"chrome": "68" // 浏览器目标,也可以跟随一个浏览器的市场份额,也可以是:last 100 versions
}
}]
]
}
关于异常的处理:corejs官方的默认值是false,但是这样会有异常,结果就是不会转换;因此在corejs的属性后面要跟上一个数字2,但是这样,还是不行,因此需要下载一个@babel/runtime-corejs2依赖项,用来改变之前的@babel/runtime
注意:在设置presets属性的时候,如果需要配置,第二个对象是放在嵌套数组里面,如:
[['xxx',{} ]],并且,不可以在在webpack里面设置了以后,再去单独的.babelrc里面去设置,根据试验证明:会报错。
插件(plugins)
想要使用一个插件,你只需要
require()它,然后把它添加到plugins数组中。多数插件可以通过选项(option)自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用new操作符来创建它。
这是官方的说法,我自己的理解就是,给webpack扩展一个功能,这个功能极其强大,可以用来处理各种各样的任务,插件的使用方法如下:
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装的模块
const webpack = require('webpack'); // 用于访问内置插件
const path = require('path') //这是一个文件路径的内置模块,在使用resolve属性时,就必须要有这个
const config = { // 多文件配置的写法
module: {
rules: [
{ test: /\.txt$/, use: 'raw-loader' }
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
};
module.exports = config
HtmlWebpackPlugin的使用
HtmlWebpackPlugin插件,该插件将为你生成一个 HTML5 文件, 其中包括使用 script 标签的 body 中的所有 webpack 包。 使用的时候,需要安装这个插件html-webpack-plugin,基本具体配置如下:
var HtmlWebpackPlugin = require('html-webpack-plugin'); // 引入插件
plugins: [
new HtmlWebpackPlugin({ // 可以配置更多的值,比如title
title: 'webpack', // 生成HTML标题
templat: './index.html' // 配置的模板文件
hash: true, // 防止缓存,给生成的JS文件添加一个hash值,包含css
cache: true // 仅在内容被更改时才更新文件
inject: true, // JS注入的位置,true,默认底部
chunks:['xxx', 'entry入口文件名'], // 多入口文件,会根据配置生成JS文件,默认全部引用
minify: {
removeAttributeQuotes: true, // 是否移除属性的引号 默认false
}
})
]
参考资料: npm原文链接、webpack4 之html-webpack-plugin。更多配置查看前面的NPM原文链接
clean-webpack-plugin的使用
clean-webpack-plugin插件能对配置的输出文件进行清除,在build命令开始执行的时候,会自动的去检查,如有有就会去先清除掉,这样就永远地保证了这个目录里面的文件都是最新的。但是也可以自定义删除某个目录,文档请戳这里。
const { cleanWebpackPlugin } = require('clean-webpack-plugin') // 先引入这个包,需要使用这种方式
plugins: [
new cleanWebpackPlugin() // 使用,默认不需要进行传参, 根据实测,在3.X的版本,可以直接引入
]
多个loader或多插件配置
如果要使用多个lodaer,只需要将规则rules属性下面,放一个数组,里面可以有多个对象
module: {
rules: [{ // 关于多个loader配置如下,每个loader都是一个对象
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
},{
test: /\.m?js$/, // 匹配文件的后缀名
exclude: /(node_modules|bower_components)/, // 需要剔除的目录
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-proposal-object-rest-spread']
}
}
}]
}
提示:其实,每一个loader或者插件都可以使用单文件的方式,这样可以让webpack文件变得不是那么的庞大,但是会造成项目根目录文件的变得很多,具体根据自己得项目来进行配置。
关于TS文件的打包
在转换TS文件的时候,需要下载typescript ts-loader的依赖,安装好以后,需要在项目根目录新建一个为tsconfig.json的JSON文件,配置如下:
{
"compilerOptions": {
"outDir": "./dist/", // 输出的文件,如果webpack已经配置了,就不需要了
"noImplicitAny": true,
"module": "es6", // 已什么模块的方式引入文件,主要有ES6和commonJS
"target": "es5", // 需要编译成什么类型的语法,ES5
"jsx": "react", // 使用jsx语法,
"allowJs": true // 在TS文件种是否存在ES的语法,默认true
},
"include": ['./src/', './'], // 引入的文件目录,需要编译的文件
"exclude": /node_modules/ // 剔除不需要编译的目录,多个使用(xx|xx)这种方式
}
编译typescriptwebpack的配置如下:
module: {
rules: [{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}]
},
lodash是一个一致性、模块化、高性能的 JavaScript 实用工具库,主要用于代码类型约束。在TS里面使用types/lodash可以对TS进行类型约束。**提示:**lodash具有针对性,如果需要对jquery进行类型约束,则需要下载对应的lodash依赖包。
图片打包
在图片打包以前,需要下载一个loader,使用file-loader可以对图片进行打包,webpack常用配置如下:
module: {
rules: [{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'file-loader',
options: {
name: '[path][name].[ext]' // 自定义名字,[name]名字[ext]扩展名,还可以加混淆比如哈希值
outputPath: 'images/' //将打包的文件,生成打包相对文件(dist)的images/的目录下
}
}]
}]
}
说明:图片打包,默认文件名使用哈希进行混淆,也可以自定义配置。具体查看webpack官方的url-loader说明,示例上仅仅只是显示了一部分。
如果图片需要进行BASE64的打包,需要下载url-loader的一个loader,但是需要注意的是,file-loade和url-loader的处理是一样的,唯一不一样的就是url-loader要比file-loade的功能更加强大,使用的方法和file-loade的方法一模一样,并没有什么区别,但是url-loade拥有更多的配置项。
module: {
rules: [{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loade',
options: {
name: '[path][name].[ext]', // 自定义名字,[name]名字[ext]扩展名,还可以加混淆比如哈希值
outputPath: 'images/', //将打包的文件,生成打包相对文件(dist)的images/的目录下
limit: '1024', // 图片打包base的大小限制,单位是字节
}
}]
}]
}
使用url-loader会默认的将图片打包成了base64的文件,但是在正常情况下,只有在图片比较小的话,才会打成base的格式,因此需要在options里面进行图片大小进行配置
CSS模块化打包
在打包css的时候,需要下载一个css-loader和style-loaderlodaer去做编译,如果要使用预编译,则需要安装对应的预编译loader。例如:sass需要 sass-loader 和node-sass这两个loader,根据实验,只需要sass-loader就可以了。
css-loader用来打包css
style-loader用来挂在页面,将这个loader自动挂在到html页面上,一般方式在html头部
基本配置信息如下(包含了sass预处理器):
module.exports = {
module: {
rules: [{
test: /\.css$/, // /\.sass$/
use: [ 'style-loader', 'css-loader' , 'sass-loader']
}]
}
}
给CSS3添加前缀需要使用postcss-loaderlodaer和 autoprefixer插件,继续使用上面的例子,基本配置如下:
module.exports = {
module: {
rules: [{
test: /\.css$/, // /\.sass$/
use: [ 'style-loader', 'css-loader' , 'sass-loader','postcss-loader'] // 写法1
use: [ 'style-loader', 'css-loader' ,'sass-loader',{ // 写法2,可以对每个lodaer做单独配置
loader:'postcss-loader',
options: {
plugins: [
require("autoprefixer") /* 生成浏览器前缀 */
]}
}]
}]
}
}
如果要让它自动添加前缀的话,需要在项目根目录新建一个文件(也可以是其它文件夹,全局配置一般都放在根目录),postcss.config.js,在里面进行一个配置,也可以如上代码那样,写在webpack.config.js中;基本配置信息如下:
module.exports = {
plugins: [ //插件的方式
require('autoprefixer') // 加载模块 默认写法
require('autoprefixer')({ // webpack或者postcss.config.js配置,
overrideBrowserList: 'last 100 versions'
})
]
}
autoprefixer在默认的配置下,只能自动的添加webkit的前缀信息,如果需要添加更多的配置,还需要添加额外的参数。需要在
package.json里面添加一个属性,属性名是browserslist,对应的是一个数组,里面填入浏览器、或者node版本的名称,例如:"last 100 versions";如果需要在
webpack里面配置的话,就需要使用overrideBrowserList属性去替换browserslist这个属性,否则在编译的时候,webpack会有错误提示。关于browserslist的具体用法,请点击这里
CSS模块化机制,简单配置:
module.exports = {
module: {
rules: [{
test: /\.css$/, // /\.sass$/ // 匹配的后缀名
use: [ 'style-loader', { // 配置需要的加载器,loader
loader: 'css-loader',
options: {
modules: {
localIdentNane: '[name]_[hash:base64:10]' // 自定义class的名字,bese需要截取
}
}
}, 'sass-loader']
}]
}
}
如果开启了模块化操作,那么在模块化导入的时候,文件就只能是局部的方式导入,否则webpack在编译的时候,会出错,例子如下:
import 'xxx.css' // 全局引入
import xxx from 'xxx.ss' // 局部引入
打包字体:打包字体和打包文件一样,loader用的也是打包图片的那个file-loader
module: {
rules: [{
test: /\.(woff|woff2|svg|ttf|eot )$/, // 字体文件后缀名
use: [{
loader: 'file-loader',
options: {
outputPath: 'font/' //生成的文件夹,将打包的文件,放在里面
name: [name].[ext] // 修改输出文件名,这样就默认原来的文件名和后缀名
}
}]
}]
}
devServer常用配置
webpack-dev-server 能够用于快速开发应用程序,所以简称devServer;devServer是用来提高开发效率的,不是用devServer来做打包,它提供了一些配置项,可以用于改变devServer的默认行为,要配置devServer,除了可以在配置文件里通过devServer传入参数,还可以通过命令行传入参数。
/* 截取的vue cli的例子 通过命令传递的参数*/
"dev": "webpack-dev-server --host 0.0.0.0 --inline --progress --config build/webpack.dev.conf.js",
但是需要注意的是:devServer 只适用于开发模式
开启本地服务
webpack开启本地服务器安装一个内置插件webpack-dev-server来帮我们开启一个本地服务,虽然webpackDevServer插件是内置的,但是依然需要下载,只是不需要引入,需要在webpack.config.js和package.json文件进行配置,配置内容如下:
/* webpack.config.js 配置 */
module.exports = {
devServer: {
contentBase: './dist' // 配置打包的路径
open: true, // 设置是否默认打开
prot: "8080", // 端口
hot: true , // 开启热更新
}
}
/* package.json 配置 */
"scripts": {
"dev": "webpack-dev-server --open"
}
在正确配置了webpack-dev-server以后,从dist目录中提供文件,默认地址是:localhost:8080,也可以自定义配置,配置方案如上代码。更多配置点击这里,但是这种方法,会更新所有数据,可以理解成是"页面刷新"
webpack-dev-server在编译后不会写任何输出文件。相反,它将捆绑文件保存在内存中并为它们提供服务,就好像它们是安装在服务器根路径上的真实文件一样。如果您的页面希望在不同的路径中找到捆绑包文件,则可以使用
publicPathdev服务器配置中的选项更改此选项。
webpack 热更新
在使用webpack热更新的时候,需要有一个插件HotModuleReplacementPlugin,简称是HMR,这个插件是webpack集成的一个内置插件,在使用的时候,不需要下载,但是要导入webpack。
但是HotModuleReplacementPlugin这个插件不能用于生产模式,热更新也是devServer里面的一个小项。 HotModuleReplacementPlugin里面的配置项,可以忽略,官方说是实验性的,不推荐配置。
const webpack = require('webpack')
plugins:[
new webpack.HotModuleReplacementPlugin({
multiStep: true, // 将分两步构建 - 首先编译热更新块,然后编译剩余的普通资产
fullBuildTimeout: Number // 时间,表示启用 multiStep 这个之间的延迟
requestTimeout: Number // 时间(毫秒) 用于下载清单的超时时间
});
]
请注意,
webpack.HotModuleReplacementPlugin完全启用HMR是必需的。如果webpack或者webpack-dev-server使用该--hot选项启动,则会自动添加此插件,因此您可能不需要将此添加到您的webpack.config.js。
"dev": "webpack-dev-server --host 0.0.0.0 " // 上述说明的实例,不需要webpack.config.js配置
webpack 预加载
webpack 反向代理
dev-server 使用了非常强大的 http-proxy-middleware 包。更多高级用法,请查阅其文档;在我们做开发的时候,会因为跨域问题导致无法进行数据请求,因此需要借助wenpack提供的反向代理,既porxy,使用方法如下:
/* 省略其它代码 */
devServer: {
porxy: {
"/api": "http://localhost:3000", // 基础用法,将/api绑定到目标服务器。
"/api": { // 采用配置的用法,用法二
target: "http://localhost:3000", // 目标服务器
pathRewrite: {"^/api" : ""}, //路径重写,这样在前端访问的时候,就不需要传/api
secure: false, // 配置是否需要使用https的方式,默认是不允许
}
}, // 多个API请求同一服务器的写法
porxy:[{
context: ["/auth", "/api"], // 多个请求
target: "http://localhost:3000", // 目标服务器
}]
}
/*** 前端请求,已axions为例 ***/
axios.get('/api/xxx').then(red => { xxx })
axios.get('// xxx').then(red => { xxx }) // 配置请求的方式访问 忽略掉/api
假设有多个API请求都在同一个服务器,可以使用context上下文的方式跟上一个数组,代码如上例子,关于webpack proxy的更多配置,请戳这里访问官网更多说明。
webpack 预加载
SourceMap的设置
其实配置devtool就是在配置SourceMap,意为:源文件映射,能够快速找到代码出错的位置,但是这种方式,需要打包完了以后才能看到是否出错;但是也有折中的解决方案,需要在package.json里面设置一个脚本,可以自动的为我们打包,但它是一个文件协议,正常来说,页面需要手动刷新,所有还是使用devServer的方式会比较好点儿。
module.export = {
/* cheap-module-eval-source-map 开发环境使用,速度快,包含三方模块 仅提示行错误*/
/* cheap-module-source-map 生产环境使用, 其实也可以不使用*/
/* source-map 源文件映射 inline-source-map(精准告诉你哪行那列出错)*/
devtool: 'cheap-module-eval-source-map'
}
/*================================= package.json ===============================*/
"scripts": {
"dev": "webpack --watch"
}
devtool您可以使用SourceMapDevToolPlugin, 而不是使用该选项,EvalSourceMapDevToolPlugin因为它有更多选项。切勿同时使用devtool选项和插件,该devtool选项在内部添加插件,因此您最终会将插件应用两次。
devtool您可以使用SourceMapDevToolPlugin, 而不是使用该选项,EvalSourceMapDevToolPlugin因为它有更多选项其实这句话,我也不太理解是什么意思? 如果需要更加细粒度的控制,就使用SourceMapDevToolPlugin这个插件,但是不能同时使用,原因上面也说了。 其实就我个人觉得,自带的就够了。
Tree Shaking的配置
Tree Shaking翻译过来就是摇晃树枝 ,所以简称摇树;举个简单的例子:秋天的马路,马路两边种了很多行道树,有的叶子枯黄、有的叶子任然是绿色,因为枯黄的叶子随风飘落,为了更好的解决落叶的问题,就找了一个大型的器械,抓着树干使劲摇晃,枯黄的叶子也就掉落了下来,剩下的就是不易掉落的绿色叶子。
但是反过来可以这样理解,就是说,对于webpack来说,入口就相当于是一棵树干,入口上面有很多模块,这些模块就是树枝,但是这些依赖模块并没有全部使用,或者只使用了模块中的某一个功能,这时就需要借助Tree Shaking它将不需要的模块给摇掉,也就是说:在打包后文件里面,代码都是精简且需要的。原理请戳这里:Tree-Shaking性能优化实践 - 原理篇。
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如
import和export。这个术语和概念实际上是兴起于 ES2015 模块打包工具 rollup。新的 webpack 4 正式版本,扩展了这个检测能力,通过
package.json的"sideEffects"属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 "pure(纯的 ES2015 模块)",由此可以安全地删除文件中未使用的部分。
说了这么多? 那在webpack里面应该怎样使用呢?需要在package.json中配置
{
"sideEffects": false, // 简单版本,
"sideEffects": [ // 使用数组的方式,个人理解:就是将不需要摇的东西给剔除出来,。
"@babel/polyfill",
"./src/some-side-effectful-file.js",
"*.css"
]
}
这样只是标记“未使用代码(dead code)”,但是需要是在编译后(bulid)删除,因此:我们需要切换到生产模式(production)来表示,命令模式(--optimize-minimize);使用配置的方式,代码如下:
mode: 'production', // 必须是生产模式
optimization: { // 设置删除为标记的 未使用代码,也就是死代码
usedExports: true
}
总结:Tree Shaking只在生产模式有效,开发模式无效,并且只能使用ESmodul的方式,详细说明请戳这里。
单独的webpack配置
在vue cli 2.x的版本里面有很多webpack配置,一开始我也不知道为什么需要这么配置,通过后来的了解才知道,在webpack里面有很多模式,比如:生产模式、开发模式,介于它们之间的还有各种配置文件、公共模块等,以下是vue cli的webpack配置目录。
.
|—— vue cli
|—— config
| |—— dev.env.js
| |—— index.js
| |—— prod.env.js
| |__ test.env.js
|—— build
| |—— build.js
| |—— check-versions.js
| |—— utils.js
| |—— vue-loader.conf.js
| |—— webpack.base.conf.js
| |—— webpack.dev.conf.js
| |__ webpack.prod.conf.js
|
.
可是有这么多的单独的文件,最终需要怎么才能合并在一起呢? 是直接引入还是需要使用插件呢?这里需要使用一个插件,这个插件的名字是:webpack-merge,在使用它的时候,需要先去安装这个插件。
如果将上面的目录进行一个拆分,假设就只有webpack.prod.conf.js、webpack.dev.conf.js和webpack.base.conf.js这么三个文件,其中webpack.base.conf.js是一个公共文件,另外两个表示生产模式和开发模式。使用webpack-merge的方法如下:
/* ======= 以生产模式为例 webpack.prod.conf.js======= */
const merge = require('webpack-merge');
const common = require('./webpack.prod.conf.js');
module.exports = merge(common, {
mode: 'production',
devtool: 'source-map'
});
还需要在npm里面进行配置,仍然已vue cli目录为例,在package.json中加入以下内容:
scripts": {
"dev": "webpack-dev-server --open --progress --config build/webpack.dev.conf.js",
"build": "webpack --config build/webpack.prod.conf.js"
}
/* --progress 显示进度条的意思 */
环境变量的使用
就只想使用一个文件入口,在里面配置一个环境变量,然后就能按照自己的需要,产生不同的编译结果,实施方法可以按照如下操作。
那里面的commonConfig文件和prodConfig还有这个devConfig它们也是一个单独的文件,为了避免一个文件很大,所以在使用的过程中,还是单独分开写的比较好,只不过在具体配置的时候,使用一个环境变量来判断。
module.export = (env) => {
if(env && env.production){ // 条件判断
return merge(commonConfig, prodConfig); // 生产模式(线上模式)
} else {
return merge(commonConfig, devConfig); // 开发模式
}
}
看了上面的代码,会想到一个问题,条件判断的变量env && env.production是从哪里来的,其实这个变量是从package.json``里面配置得来的,也就是说:如果需要使用这种方式的话,就需要去更改启动脚本,代码如下:
scripts": {
"dev": "webpack-dev-server --open --progress --config build/webpack.base.conf.js",
"build": "webpack --env.production. --config build/webpack.base.conf.js"
}
/* --progress 显示进度条的意思 */
可以很清除的看到dev和build这两个方法启动的目录文件都是同一个,但是在build里面多了一个env.production这个属性,其实,这个变量就是上面的条件判断语句所需要的参数。
环境变量还可以这样使用,就nodeJS提供的process.env.NODE_ENV,具体的时候方法,参考简书NodeJs/Vue项目中对process.env的使用。
在Webpack中配置Esint
在很多时候,我们都需要使用eslint来帮助我们进行代码检查和规范,这样往往在团队中,能做到风格统一,非常有利于团队协作、后期维护等,但配置eslint是非常繁琐的,下面一起来看看,我们如何在webpack中如何配置eslint吧?在慕课手记中也有关于自定义的详细说明,更多查看请戳这里。
eslint在使用前,需要先安装eslint这个包,更多安装方法戳这里查看官方教程,安装方法如下:
npm install eslint --save-dev // 下载eslint这个安装包
npx eslint --init // 运行这个命令进行eslint的详细配置
关于eslint配置说明,当运行npx eslint --init的时候,会弹出如下信息:
? How would you like to use ESLint? (Use arrow keys) //如何配置eslint这个文件,你想如何使用
To check syntax only // 仅检查语法
> To check syntax and find problems // 检查语法并找出问题
To check syntax, find problems, and enforce code style //检查语法、发现问题和强制代码样式
/* 我选择的是检查语法并找出问题,然后就有了下面的配置信息 */
? What type of modules does your project use? // 你想使用什么类型的模块呢?
JavaScript modules (import/export) // 基于ES6
> CommonJS (require/exports) // 这个主要是node中使用
None of these // 这些都不是
/* 因为在我的实验中,用的是vue,所需需要使用esmodule这种语法,然后弹出的信息如下 */
? Which framework does your project use? (Use arrow keys) // 你想使用那个框架
React
> Vue.js // 因为我是vue,所有选择vue
None of these // 其它框架
/* 当选择vue后,配置如如下,问你是在那个地方运行,可以多选,使用空格点亮,然后回车 */
? Where does your code run? (Press <space> to select, <a> to toggle all, <i> to invert selection)
>(*) Browser // 浏览器
( ) Node // node
/* 当我选择以后,会出现如下配置 当然是使用JS文件的方式,个人爱好*/
? What format do you want your config file to be in? (Use arrow keys) // 使用什么文件作为配置文件
> JavaScript
YAML
JSON
/* 问你是否想安装它们,这里当然选择是了 */
? Would you like to install them now with npm? (Y/n) // y 基础配置过程就完毕了
当一些列配置完毕后,会发现在项目的根目录中有一个.eslintrc.js的配置文件,这个文件就是eslint的配置文件啦,更多配置请戳这里,也可以选择第三方公司的编码配置,安装方法参考eslint配置(使用Airbnb编码规则),比较变态的就是airbnb规则了,非常的严格。
虽然这样在vscode(前提是webpack中有插件)中可以使用了,但是对于其它的代码编辑器,却不会怎么报错? 所以还需要下载一个eslint-loader才可以,具体操作方法如下:
npm install eslint-loader --save-dev
下载好了以后,基本配置如下,但是一定要将eslint-loader放在最后面,因为loader的执行顺序是从后往前:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: ["babel-loader", "eslint-loader"]
}
]
}
};
仅仅是这样(如果不做如下配置,只能在cmd控制台显示),还是不行滴,还需要一点儿额外的其它配置,配置如下:
module.exports = {
devServer: {
overlay: true // 在浏览器上弹出一个层,也就是在浏览器中显示全屏覆盖
}
};
但都知道在vue cli2 的版本中,当检测到警告信息,会输出在浏览器的控制台显示,但是这个问题,我还木有解决,有知道的小伙伴,可以告诉我以下。
overlay:{
warnings: false,
errors: true
}
我也按照vue cli源代码进行了一个配置,发现并没有按照我的意愿来。
打包第三方库(shimming)
在刚学Vue.js的时候,那时候运用的不是很熟,有时候需要操作DOM,但是Vue.js操作DOM非常的不方便;有时候需要获取到 一个列表里面所有的标签,那时候就想到了使用Jquery来帮我操作DOM,当时就以为像其它JS文件一样全局导入就可以了,可在实际运用的时候却傻眼了,就发现,能够在控制台输出Jquery对象,但是却无法操作DOM,这让我很抓狂。
经过学习,知道webpakc本身就已经提供了一套关于shimming解决方案:使用webpack内置的ProvidePlugin插件和expose-loader,在网上找了很多文档,在使用webpack内置的ProvidePlugin插件的时候,虽然勉强解决这个问题,但是还是有一些小问题,就eslint报错的问题。后来发现使用expose-loader的方式也可以解决这个问题。
webpack.ProvidePlugin
ProvidePlugin是webpack内置插件,因此在使用前需要导入webpack,它主要的功能是:自动加载模块,而不必到处 import 或 require , 这也是webpack推荐的使用方法。
但是:对于 ES2015 模块的 default export,你必须指定模块的 default 属性,因为它的名字;以
jquery为例: 它实际上就是import $ from 'jquery',具体例子请看如下代码。更多例子请戳这里,查看官方示例说明。
const webpack = require('webpack')
/* 省略其它代码 */
plugins: [
new webpack.ProvidePlugin({
$: 'jquery', // 后面的jquery其实就是 import $ from 'jquery'
JQuery: 'jquery'
})
]
expose-loader
可以通过expose-loader向webpack注入一个全局的公共模块,假设用了这个方法导入了jquery的话,是可以全局使用,可以理解成绑定到了window上,因此,在使用它的时候,需要下载这个expose-loader,以jquery为例,使用方法如下:
module: {
rules: [{
test: require.resolve('jquery'), // 导入jquery模块,但这个方法是node.js提供,和webpack无关
use: [{
loader: 'expose-loader',
options: 'jQuery' // 全局使用的名称, window.jQuery
},{
loader: 'expose-loader',
options: '$' // 全局使用名称, window.$
}]
}]
}
但是这种方法,需要在局部导入(也就是说,需要每个页面都需要导入),然后才能进行全局暴露,但是如果需要全局注入的话,显然这种方式不太妥当。
防止重复(SplitChunksPlugin)
webpack提供了一个Code Splitting拆分代码,点击这里,可以查看代码分离的方式以及介绍。
使用CommonsChunkPlugin用于避免它们的重复依赖,但是无法进一步优化;在webpack4的版本以后,删除了optimization.splitChunks这个属性;SplitChunksPlugin方法使用如下:
module.exports = {
splitChunks: {
chunks: "all", // 同步和异步都做分割
cacheGroups: { // 缓冲组
vendors:false,
default: false
}
}
}
SplitChunksPlugin的更多配置如下,具体参考官方说明,戳这里点击官方文档。
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async', // async 默认推荐使用,异步加载做分割, initial 同步 all 同步和异步都做分割
minSize: 30000, // 字节 引入的包或者模块大于这个这个值,才会做代码分割,只针对同步加载
maxSize: 0, // 超过 minSize 以后,会进行二次打包,默认不配置,不能强拆
minChunks: 1, // 引入次数,引入次数为多个,可以打包
maxAsyncRequests: 5, // 请求超过5个的部分不拆分,一般默认值
maxInitialRequests: 3, // 页面初始化同时发送的请求数量最大不能3个,超过以后就不会被拆分,默认值就会
automaticNameDelimiter: '~', // 用以代码分离打包的文件名默认连接符,cacheGroups里面的vendors
automaticNameMaxLength: 30, // 最长字节数,不能超过这个值
name: true, // 拆分的名字, true表示根据模块名和cacheGroups组的key来自动生成文件名
cacheGroups: { // 缓存配置,一个文件import多个库,如果需要多个库打包成一个文件,则使用这个缓存组
vendors: {
test: /[\\/]node_modules[\\/]/, // 检测引人的库是否是node_modules下面的
priority: -10, // 权重,决定哪个组的优先配置
name: "vendors" // 自定义名字,不需要后缀名
},
default: { // 前面没有匹配上,则使用这个组,通常是业务代码
minChunks: 2, // 引入超过2次才从新打包成一个新文件
priority: -20, // 权重
reuseExistingChunk: true // 当为true后就不会重复打包,会进行自动复用。
}
}
}
}
};
在chunks的时候,还需要走cacheGroups缓冲组,分离出来的文件名是:cacheGroups里面的vendors,中间的连接符是automaticNameDelimiter,例vendors-main.js
cacheGroups可以自定义多个组,如果多个组都满足要求,则以权重大的组为准。
webpack为了更好的提升性能,最好的还是使用异步的方式,这也是官方推荐。
Lazy Loding(动态导入懒加载)
当涉及到动态代码拆分时,webpack 提供了两个类似的技术。对于动态导入,第一种,也是优先选择的方式是:ES7提出的一个草案,既
import()语法。第二种,则是使用 webpack 特定的require.ensure。先来说说第一种:
动态导入最大的好处就是实现了懒加载,用到那个模块就才会调用那个模块,非常适合ASP导入,可以大大的减少等待时间,但使用这个需要用到babel去转,也就是需要下载@babel/plugin-syntax-dynamic-import这个插件,使用方法如下:
{
"plugins": ["@babel/plugin-syntax-dynamic-import"] // 使用插件,在webpack plugins里面添加
}
在页面添加动态import导入懒加载的方式,这种方式会返回一个promise对象,代码如下:
/* 声明这个方法 需要一个函数 */
function getComponent() {
import(/* webpackChunkName: "lodash" */ 'lodash').then( _ => {
var element = document.createElement('div');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}).catch( error => 'An error occurred while loading the component');
}
/* 调用这个方法 如果不调用则不执行*/
getComponent().then(component => {
document.body.appendChild(component);
})
这种方法,则会对代码进行分离,将动态打入的第三方模块,会单独的打一个文件,而自己所写的业务代码,则会被打包到新的一个文件里面。
/* webpackChunkName: "lodash" */ 'lodash'魔法字符串,懒加载,配合SplitChunksPlugin使用,可以进行一个单独配置,其实这个配置和SplitChunksPlugin是共用。
module.exports = {
splitChunks: {
cacheGroups: { // 缓冲组
vendors: {
test: /[\\/]node_modules[\\/]/, // 检测引人的库是否是node_modules下面的
priority: -10, // 权重,决定哪个组的优先配置
name: "vendors" // 自定义名字,不需要后缀名
},
default: false
}
}
}
但是使用懒加载的时候,会有一个弊端,就是当用户需要一个模块的时候,只有触发以后,才能去加载对应的模块,这就会造成一个网络的延时,有一个等待期,不利于用户体验,所以就需要另外一个技术,也就是预加载,当页面自动加载出来以后,利用闲置的带宽,去加载其它可能需要的模块。
预加载(Prefetching)
这个是webpack 4.6.0新增的一个功能,用通俗的话来说,就是在网络空闲的时候,才去自动下载其它的非必要模块,这种方式极大的提升了用户体验,也解决了等待延时问题,可是要怎么使用呢?
/* 使用方法和上面的 懒加载是一样的,只是这个魔法字符串里面的参数不一样 */
import(/* webpackPrefetch: true */ 'LoginModal');
在声明导入时使用这些内联指令允许webpack输出“Resource Hint”,使用方法如上,它告诉浏览器:
- prefetch:未来可能需要资源
- preload:当前可能需要资源
区别:preload是和核心代码一起加载,prefetch预加载
缓存带来的代码提升是有限的,所以使用懒加载代码优化是现在性能优化的主要使用方式。
webpack打包分析
关于打包分析,官网为我们提供了一个内置插件Bundle Analysis,在guider下面的Code Splitting里面的Bundle Analysis,他有几种类型,webpack-chart:(交互式饼图)等类型,具体请戳这里。使用方法也很简单,只需要在package.json中配置一段脚本即可,例子如下:
scripts": {
"build": "webpack --profile --json > stats.json --env.production. --config build/webpack.base.conf.js"
}
其实也可以直接使用webpack --profile --json > stats.json, 打包完成后,会生成一个stats.json文件,这个文件就是结果分析,然后需要将这个文件上传到这个网站,进行数据分析,但是这个表,感觉非常不人性化,个人觉得很难看,所以需要使用webpack-bundle-analyzer插件。
Webpack Bundle Analyzer也是一个打包分析,它和webpack-chart不同的是,Webpack Bundle Analyzer 是一个很直观的平面图,这是一个插件,所以在使用的时候,需要进行下载安装,npm install --save-dev webpack-bundle-analyzer。
如果使用了这个以后,在打包的时候,控制台不会输出打包信息,比如文件、 打包时间、chunk等信息,这也算是一个弊端。
在使用它的时候,也需要有如上的package.json中配置(实测,可不需要使用),在webpack里面的基本配置如下,更多配置请戳这里;
// 导入插件
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
// 使用它
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
CSS的代码分离
它默认是打包在js文件里面,这样就会导致文件变得很大,所以将CSS代码分离出来是很有必要的,使用CSS代码分析则需要使用一个插件来帮我们完成,这个一般只做线上版本,在开发版本则不需要,这个插件的名字是MiniCssExtractPlugin,点击这里查看更多官方配置。
npm install --save-dev mini-css-extract-plugin // 需要先安装
简单的配置说明,在使用的时候需要将style-loader替换为MiniCssExtractPlugin.loader,如果需要在开发环境中使用热更新,需要重新进行一个设置。
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // 导入插件
module.exports = {
plugins: [
new MiniCssExtractPlugin({ // 方法同output里面的方法,最好就是加一个hash值,防止缓存
filename: '[name].css', // 生成的文件名
chunkFilename: '[id].css', // 构建时生成,主要是防止文件更新浏览器不更新,缓存导致的
}),
],
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader, // 替换style-loader
options: { // 其它的配置项
publicPath: '../', // 指定的地址,同等与output里面的publicPath
hmr: process.env.NODE_ENV === 'development', // 环境变量
},
},
'css-loader',
],
},
],
},
};
CSS代码分离,默认是不会被压缩,如果想要代码压缩,则需要使用optimize-css-assets-webpack-plugin插件,但需要注意的是:设置optimization.minimizer会覆盖webpack提供的默认值,因此需要指定,JS minimalizer, 使用方法如下:
const TerserJSPlugin = require('terser-webpack-plugin'); // 不添加则JS不会被压缩
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // 导入CSS分离插件
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); // CSS代码压缩
module.exports = {
optimization: { // 添加一个属性,
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[id].css',
}),
],
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
};
设置optimization.minimizer会导致重写,所以需要下载terser-webpack-plugin这个插件,去压缩JS,如果没有这个插件就会导致,CSS代码被压缩了,但是JS代码则不会被压缩。
配置模块的解析方式(resolve)
Webpack 在启动后会从配置的入口模块出发找出所有依赖的模块,Resolve 配置 Webpack 如何寻找模块所对应的文件。 Webpack 内置 JavaScript 模块化语法解析功能,默认会采用模块化标准里约定好的规则去寻找,但你也可以根据自己的需要修改默认的规则。
关于resolve的基本配置如下,更多配置戳这里查看官方教程。
const path = require('path') // node提供的文件路径模块
module.exports = {
resolve: {
extensions: ['.wasm', '.mjs', '.js', '.json'], // 自动解析确定的扩展,可以省略的扩展名
alias: { //创建 import 或 require 的别名,来确保模块引入变得更简单。
"@": path.resolve(__dirname, '../src'),
xyz$: path.resolve(__dirname, 'XXX.js') // 精确匹配 xyz
}
}
};
vue loader的使用
在搭建vue脚手架之前,需要下载两个loader,分别是:vue-loader 和 vue-template-compiler,具体的安装方法查看Vue loader官方说明。
npm install -D vue-loader vue-template-compiler // 安装
需要注意的问题:每个
vue包的新版本发布时,一个相应版本的vue-template-compiler也会随之发布。编译器的版本必须和基本的vue包保持同步,这样vue-loader就会生成兼容运行时的代码。这意味着你每次升级项目中的 vue 包时,也应该匹配升级 vue-template-compiler。
在main.js中加入最基本的代码,这也是最基本的入口配置信息
import Vue from 'vue'
window.vm = new Vue({
el: '#app',
render: h => h(App),
components: { App },
template: '<App/>'
})
正常来说,webpack在编译的时候,是不认识.vue的后缀名,因此需要我们手动的在webpack里面新增一个loader,用于识别.vue文件,基本配置如下:
// webpack.config.js
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
mode: 'development',
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
// 它会应用到普通的 `.js` 文件
// 以及 `.vue` 文件中的 `<script>` 块
{
test: /\.js$/,
loader: 'babel-loader'
},
// 它会应用到普通的 `.css` 文件
// 以及 `.vue` 文件中的 `<style>` 块
{
test: /\.css$/,
use: [
'vue-style-loader',
'css-loader'
]
}
]
},
plugins: [
// 请确保引入这个插件来施展魔法
new VueLoaderPlugin()
]
}
vue loader15+的版本都需要去使用一个vue-loader/lib/plugin插件,这个插件不用单独下载,在使用vue-loader的时候,这个插件就已经有了。
webpack打包优化(DllPlugin)
为了更快的使用打包优化,需要及时更新nodeJS以及webpack的版本,尽可能的减少一些loader的转换,并且需要剔除一些不必要的目录,比如:node-modules的文件夹,插件尽可能得时候使用官方推荐。 还可以使用webpack内置的DllPlugin插件去帮我们进行打包速度优化。
DllPlugin插件专门用于单独的webpack配置中,以创建仅限dll的捆绑包。它创建了一个manifest.json文件,用于DllReferencePlugin映射依赖项(先创建一个,然后进行复制,这样就提升了打包效率)。
更多的方法戳这里查看官方教程,通常是创建一个webpack.dll.js文件,用来专门配置dllplugin,目的是为了将一些公共的库,打包成一个文件,后续打包不会重新打包,只会去引用打包好的包。
const path = require('path') // 导入node 模块
module.exports = {
mode: 'production', // 生产模式
entry: {
vendors: ['vue','jquery'] // 入口文件,自定义属性
},
output:{
filename: '[name].dll.js', // 输出,多个文件打包成一个文件
path: path.resolve(__dirname, '../dll'), // 打包的文件夹目录
library: '[name]' // 导出全局变量,用于其它文件
}
}
但是这种方法,不会自动添加到html页面中,因此需要一个插件add-asset-html-webpack-plugin,来帮我我们将webpack.dll.js打包后文件自动添加到html中,关于更多说明戳这里查看NPM教程
cnpm i add-asset-html-webpack-plugin -D // 安装
使用它的时候,此时就不能写在webpack.dll.js文件中,应该写在公共的文件当中,比如webpack.base.conf.js文件里面,基本配置方法如下:
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 导入自动生成html插件
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin'); // 自动添加插件
const webpackConfig = {
entry: 'index.js',
output: {
path: 'dist',
filename: 'index_bundle.js',
},
plugins: [
new HtmlWebpackPlugin(),// 更多说明请看前面的 HtmlWebpackPlugin 配置
// require.resolve和 path.resolve, 在这里的用法是一样的,
new AddAssetHtmlPlugin({ filepath: require.resolve('../dll/vendors.dll.js') }),
],
};