Webpack是一种前端资源构建工具,一个静态模块打包器。在Webpack看来,前端的所有资源文件(js/json/css/img/less/…)都会作为模块处理,当Webpack处理应用程序时,它将根据模块的依赖关系进行静态分析,打包生成对应的静态资源。
(wepack为了正常运行必须依赖node环境,而node环境为了可以正常的执行,必须使用npm工具管理node中各种依赖的包),因此安装webpack首先要安装Node.js,Node.js自带了软件包管理工具npm
webpack可以帮助我们进行模块化,并且处理模块间的各种复杂关系后,打包的概念就很好理解了。
就是将webpack中的各种资源模块进行打包合并成一个多个包(Bundle)并且在打包的过程中,还可对资源进行处理,比如压缩图片,将scss转成css,将ES6语法转成ES5语法,将TypeScript转成JavaScript等等操作。打包工具还有grunt/gulp
ist文件夹:用于存放之后打包的文件
src文件夹:用于存放我们写的源文件
main.js:项目的入口文件。
mathUtils.js:定义了一些数学工具函数,可以在其他地方引用,并且使用。
index.html: 浏览器打开展示的首页html(在这里引用的是src内最终打包的文件即dist文件夹的内容)。
package.json:通过npm init生成的,npm包管理的文件。
Entry
入口(Entry)指示Webpack以哪个文件作为入口起点分析构建内部依赖图并进行打包。
Output
输出(Output)指示Webpack打包后的资源bundles输出到哪里去,以及如何命名。
Loader
Loader让Webpack能够去处理那些非JavaScript语言的文件,Webpack本身只能理解JavaScript。
Plugins
插件(Plugins)可以用于执行范围更广的任务,插件的范围包括从打包和压缩,一直到重新定义环境中的变量等。
Mode
模式(Mode)指示Webpack使用相应模式的配置。分为development和production两种模式,下面分别进行简述。
webpack.config.js是webpack的配置文件,用来指示webpack工作,运行webpack指令时,会加载里面的配置,所有构建工具都是基于nodejs平台运行的,默认采用commonjs模块化。webpack.config.js基础配置如图所示。
开发服务器(devServer)用来实现自动化(自动编译、自动打开浏览器、自动刷新浏览器),只会在内存中编译打包,不会有任何文件输出,本地安装webpack-dev-server后,通过npx webpack-dev-server命令启动devServer,核心代码如图所示。
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{ test: /\.js$/, use: 'babel-loader' },
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{ loader: 'postcss-loader' },
]
}
]
}
};
let less = require('less');
module.exports = function (source) {
const callback = this.async();
//this.async() 返回一个回调函数,用于异步执行
less.render(source, (err, result) => {
//使用less处理对应的less文件的source
callback(err, result.css);
});
}
extract-text-webpack-plugin
webpack 默认会将 css 当做一个模块打包到一个 chunk 中,extract-text-webpack-plugin 的作用就是将 css 提取成独立的 css 文件
const ExtractTextPlugin = require('extract-text-webpack-plugin');
new ExtractTextPlugin({
filename: 'css/[name].css',
})
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
use: ['css-loader','postcss-loader','less-loader'],
fallback: 'vue-style-loader', #使用vue时要用这个配置
})
},
html-webpack-plugin
这个插件很重要,作用一是创建 HTML 页面文件到你的输出目录,作用二是将 webpack 打包后的 chunk 自动引入到这个 HTML 中
const HtmlPlugin = require('html-webpack-plugin')
new HtmlPlugin({
filename: 'index.html',
template: 'pages/index.html'
}
DefinePlugin
定义全局常量
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV)
},
PRODUCTION: JSON.stringify(PRODUCTION),
APP_CONFIG: JSON.stringify(appConfig[process.env.NODE_ENV]),
}),
UglifyJsPlugin
js 压缩
new webpack.optimize.UglifyJsPlugin()
注意: webpack4 已经移除了该插件,用 optimization.minimize 替代
「简易原理」
插件就像是一个插入到生产线中的一个功能,在特定时机对生产线上资源做处理。webpack通过Tapable组织这条复杂生产线。 webpack在编译过代码程中,会触发一系列Tapable钩子事件,插件所做的 就是找到相应的钩子,往上面挂上自己任务,也就是注册事件,当webpack构建时,插件注册事件就会随着钩子触发而执行。
webpack 插件由以下组成:
// 一个 JavaScript 命名函数。
function MyExampleWebpackPlugin() {
};
// 在插件函数的 prototype 上定义一个 apply 方法。
MyExampleWebpackPlugin.prototype.apply = function(compiler) {
// 指定一个挂载到 webpack 自身的事件钩子。
compiler.plugin('webpacksEventHook', function(compilation /* 处理 webpack 内部实例的特定数据。*/, callback) {
console.log("This is an example plugin!!!");
// 功能完成后调用 webpack 提供的回调。
callback();
});
};
打包不同的资源会使用不同的loader和插件,打包html/样式/图片/其它资源的流程如下所述。
1.下载html-webpack-plugin插件;
2.引入html-webpack-plugin插件;
3.使用html-webpack-plugin插件,并进行相应配置。
不同的样式文件需要配置不同的loader
1.下载loader;
2.配置loader,css样式文件使用css-loader和style-loader,less文件使用less-loader、css-loader和style-loader。其中css-loader的作用是将css文件变成commonjs模块加载到js文件中,style-loader的作用是创建style标签,将js中的样式资源插入进去,添加到head中生效。
1.下载url-loader,file-loader
2.配置loader
1.下载file-loader
样式文件打包后会默认和js文件一起输出,可以通过插件将打包后的css文件单独输出,流程如下所述。
1.下载mini-css-extract-plugin插件
2.引用该插件
3.配置
1.下载postcss-loader和postcss-preset-env
2.在package.json中browsetslist属性中分别对开发环境和生产环境进行兼容性配置,设置支持样式的浏览器版本
3.通过postcss找到package.json中browserslist里面的配置,通过配置加载指定的css兼容性样式。
1.下载optimize-css-assets-webpack-plugin插件
2.引用该插件
3.使用该插件
1.下载eslint-loader和eslint
2.在package.json中的eslintConfig中进行配置
3.配置eslint-loader,其中只需检测js文件并要排除第三方库,只检测自己写的源代码,同时可在options配置中设置fix:true,自动修复eslint的错误。
1.下载babel-loader、@babel/core、@babel/preset-env,通过@babel/preset-env做基本的js兼容性处理,然后通过corejs做前面无法实现的兼容性处理,并实现按需加载
mode设置为production生产环境时会自动压缩js代码。
可以从开发环境和生产环境分别对webpack进行性能优化。其中开发环境主要考虑从打包构建速度和代码调试两个方面进行优化,生产环境主要考虑从打包构建速度和代码运行性能这两个方面进行优化。下面简单介绍下开发环境上通过HMR提升构建速度。
HMR(热模块替换),作用是一个模块发生变化后,只会更新打包这一个模块而不是所有模块,通过在devServer中设置hot:true属性启动HMR功能。
其中对于样式文件,可以使用HMR功能,因为style-loader内部实现了;
对于js文件,默认不能使用HMR功能,解决方法:修改入口文件js代码,添加支持HMR功能的代码,另外HMR只能处理非入口js文件的其他文件,对入口文件并不能生效,因为一旦入口文件更新,入口文件引入的其他文件一定会被重新加载;
对于html文件,默认不能使用HMR功能,同时会导致html文件不能热更新,解决方法:修改entry入口文件,将html文件引入,只能解决html文件不能热更新的问题。
HMR效果
在入口index.js文件中引入print.js文件,运行npx webpack-devserver后,页面如图所示。
初始页面
修改print.js文件后,只会重新加载print.js文件,而不会重新加载index.js文件,HMR效果如图所示。
即 modules、mainFields、noParse、includes、exclude、alias 全部用起来。
const resolve = dir => path.join(__dirname, '..', dir);
resolve: {
modules: [ // 指定以下目录寻找第三方模块,避免webpack往父级目录递归搜索
resolve('src'),
resolve('node_modules'),
resolve(config.common.layoutPath)
],
mainFields: ['main'], // 只采用main字段作为入口文件描述字段,减少搜索步骤
alias: {
vue$: "vue/dist/vue.common",
"@": resolve("src") // 缓存src目录为@符号,避免重复寻址
}
},
module: {
noParse: /jquery|lodash/, // 忽略未采用模块化的文件,因此jquery或lodash将不会被下面的loaders解析
// noParse: function(content) {
// return /jquery|lodash/.test(content)
// },
rules: [
{
test: /\.js$/,
include: [ // 表示只解析以下目录,减少loader处理范围
resolve("src"),
resolve(config.common.layoutPath)
],
exclude: file => /test/.test(file), // 排除test目录文件
loader: "happypack/loader?id=happy-babel" // 后面会介绍
},
]
}
webpack-parallel-uglify-plugin能够把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程,从而实现并发编译,进而大幅提升 js 压缩速度。
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
// ...
optimization: {
minimizer: [
new ParallelUglifyPlugin({ // 多进程压缩
cacheDir: '.cache/',
uglifyJS: {
output: {
comments: false,
beautify: false
},
compress: {
warnings: false,
drop_console: true,
collapse_vars: true,
reduce_vars: true
}
}
}),
]
}
webpack运行在node 中打包的时候是单线程去一件一件事情的做,HappyPack可以开启多个子进程去并发执行,子进程处理完后把结果交给主进程
const HappyPack = require('happypack');
module.exports = {
entry: './src/index.js',
output: {
path: path.join(__dirname, './dist'),
filename: 'main.js',
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: 'happypack/loader?id=babel',
},
]
},
plugins: [
new HappyPack({
id: 'babel', //id值,与loader配置项对应
threads: 4, //配置多少个子进程
loaders: ['babel-loader'] //用什么loader处理
}),
]
}
第三方库不是经常更新,打包的时候希望分开打包,来提升打包速度。打包 dll 需要新建一个 webpack 配置文件(webpack.dll.config.js)在打包dll时webpack 做一个索引,写在manifest文件中。然后打包项目文件时只需要读取 manifest 文件。
const webpack = require("webpack");
const path = require('path');
const CleanWebpackPlugin = require("clean-webpack-plugin");
const dllPath = path.resolve(__dirname, "../src/assets/dll"); // dll文件存放的目录
module.exports = {
entry: {
// 把 vue 相关模块的放到一个单独的动态链接库
vue: ["babel-polyfill", "fastclick", "vue", "vue-router", "vuex", "axios", "element-ui"]
},
output: {
filename: "[name]-[hash].dll.js", // 生成vue.dll.js
path: dllPath,
library: "dll[name]"
},
plugins: [
new CleanWebpackPlugin(["*.js"], { // 清除之前的dll文件
root: dllPath,
}),
new webpack.DllPlugin({
name: "dll[name]",
// manifest.json 描述动态链接库包含了哪些内容
path: path.join(__dirname, "./", "[name].dll.manifest.json")
}),
],
};
接着, 需要在 package.json 中新增 dll 命令。
"scripts": {
"dll": "webpack --mode production --config build/webpack.dll.config.js"
}
运行 npm run dll 后,会生成 ./src/assets/dll/vue.dll-[hash].js 公共 js 和 ./build/vue.dll.manifest.json 资源说明文件,至此 dll 准备工作完成,接下来在 webpack 中引用即可。
externals: {
'vue': 'Vue',
'vue-router': 'VueRouter',
'vuex': 'vuex',
'elemenct-ui': 'ELEMENT',
'axios': 'axios',
'fastclick': 'FastClick'
},
plugins: [
...(config.common.needDll ? [
new webpack.DllReferencePlugin({
manifest: require("./vue.dll.manifest.json")
})
] : [])
]
//第三方ui库element,vant等库都提供来按需加载的方式,避免全部引入,加大项目体积
import { Button, Select } from 'element-ui';
//路由懒加载
const showImage = () => import('@/components/common/showImage');
提取第三方库「vendor」
module.exports = {
entry: {
main: ‘./src/index.js’,
vendor: [‘react’, ‘react-dom’],
},
}
依赖库分离「splitChunks」
optimization: {
splitChunks: {
chunks: "async", // 必须三选一: "initial" | "all"(推荐) | "async" (默认就是async)
minSize: 30000, // 最小尺寸,30000
minChunks: 1, // 最小 chunk ,默认1
maxAsyncRequests: 5, // 最大异步请求数, 默认5
maxInitialRequests : 3, // 最大初始化请求书,默认3
automaticNameDelimiter: '~',// 打包分隔符
name: function(){}, // 打包后的名称,此选项可接收 function
cacheGroups:{ // 这里开始设置缓存的 chunks
priority: 0, // 缓存组优先级
vendor: { // key 为entry中定义的 入口名称
chunks: "initial", // 必须三选一: "initial" | "all" | "async"(默认就是async)
test: /react|lodash/, // 正则规则验证,如果符合就提取 chunk
name: "vendor", // 要缓存的 分隔出来的 chunk 名称
minSize: 30000,
minChunks: 1,
enforce: true,
maxAsyncRequests: 5, // 最大异步请求数, 默认1
maxInitialRequests : 3, // 最大初始化请求书,默认1
reuseExistingChunk: true // 可设置是否重用该chunk
}
}
}
},
webpack详解
前端进阶 - Webpack篇
想了解Webpack,看这篇就够了