第四课之webpack进阶
1、回顾
基础配置
- entry:入口文件
- output:输出文件
- mode:定义打包方式是开发环境还是生产环境
- loaders:编译js、css、img
- plugins:解决 loader 无法实现的其他事、它的功能要更加丰富
回顾webpack和webpack-dev-server
webpack | webpack-dev-server |
---|---|
适用于webpack打包机制 | 适用于webpack打包机制 |
输出真实的文件一般是dist | 输出的文件只存在于内存中,不输出真实的文件 |
文件修改要重新打包 | 文件修改保存会自动更新 |
按生产环境需求进行打包生产环境代码 | 按开发环境需求进行打包开发环境代码 |
课程内容
2、基础webpack的完善
基本的webpack还缺少js的编译语法,以及区分开发环境和生产环境
- 开发环境通常指的是我们正在开发的这个阶段所需要的一些环境配置,也就是方便我们开发人员调试开发的一种环境
- 生产环境通常指的是我们将程序开发完成经过测试之后无明显异常准备发布上线的环境,也可以理解为用户可以正常使用的就是生产环境
案例一
babel使用
- Babel 是一个 JavaScript 编译器,可以把ES6的语法转为兼容浏览器的ES5语法
webpack里使用babel
// 第一步:安装
npm install -D babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime @babel/runtime
// 第二步:项目根目录添加.babelrc文件(文件需要的配置项主要有预设(presets)和插件(plugins))
{
"presets": [
"@babel/env"
],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-runtime"
]
}
// webpack.config.js添加js的loader
{
test: /\.jsx?$/,
use: {
loader: 'babel-loader',
options: {
extends: join(__dirname, '.babelrc'),
cacheDirectory: true, // 缓存目录,减少webpack构建时Babel重新编译过程
},
},
exclude: /node_modules/,
}
// second.js写一个es6的语法
setTimeout(() => {
console.log(1);
}, 1000);
案例二
开发环境和生产环境
思路:
开发环境和生产环境有很多通用的代码,可以将通用的代码提取出来,例如loaders,output等,所以可以形成三个文件,webpack.prod.config.js、webpack.dev.config.js、webpack.common.config.js,分离出来如何将他们分别合并,这时候就需要使用到webpack-merge,进行配置合并
webpack-merge
合并webpack的一个库
// 第一步:安装
npm i webpack-merge -D
// 第二步:新建webpack.common.config.js、webpack.dev.config、webpack.prod.config
// 第三步:开发和生产环境需要引入
var { merge } = require('webpack-merge');
var commonConfig = require('./webpack.common.config');
// 第四步:webpack开发环境合并
module.exports = merge(commonConfig, devConfig);
// webpack生产环境合并
module.exports = merge(commonConfig, prodConfig);
// 第五步:配置package.json脚本命令,分别读取webpack的文件
"start": "cross-env NODE_ENV=development ./node_modules/.bin/webpack-dev-server --config webpack.dev.config.js",
"release": "cross-env NODE_ENV=production ./node_modules/.bin/webpack --config webpack.prod.config.js"
- webpack.common.config.js
var path = require('path');
var htmlwp = require('html-webpack-plugin');
var MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: './src/js/second.js', //指定打包的入口文件
output: {
path: __dirname + '/dist', // 注意:__dirname表示webpack.config.js所在目录的绝对路径
filename: 'build.js', //输出文件
publicPath: '' // output的publicPath是用来给生成的静态资源路径添加前缀的;
},
module: {
rules: [
{
test: /\.jsx?$/,
use: {
loader: 'babel-loader',
options: {
extends: path.join(__dirname, '.babelrc'),
cacheDirectory: true,
},
},
exclude: /node_modules/,
},
{
test: /\.css$/,
exclude: '/node_modules/',
use: [
(() => { return process.env.NODE_ENV === 'production' ? MiniCssExtractPlugin.loader : 'style-loader'; })(),
{
loader: 'css-loader',
options: {
importLoaders: 1
}
},
{
loader: 'postcss-loader',
}
]
},
{
test: /\.less$/,
exclude: '/node_modules/',
use: [
(() => { return process.env.NODE_ENV === 'production' ? MiniCssExtractPlugin.loader : 'style-loader'; })(),
{
loader: 'css-loader',
options: {
importLoaders: 1
}
},
{
loader: 'postcss-loader',
},
{
loader: 'less-loader', //
options: {
}
}
]
},
{
test: /\.(png|jpg|gif|jpeg|svg)$/,
use: [
{
loader: "url-loader",
options: {
name: "[name].[hash:5].[ext]",
limit: 1024, // size <= 1kib
outputPath: "img"
}
}
]
}
]
},
plugins: [
new htmlwp({
title: '首页', //生成的页面标题<head><title>首页</title></head>
filename: 'index.html', //webpack-dev-server在内存中生成的文件名称,自动将build注入到这个页面底部,才能实现自动刷新功能
template: './public/index.html' //根据vue_02.html这个模板来生成(这个文件请程序员自己生成)
}),
],
}
- webpack.dev.config.js
var webpack = require('webpack');
var { merge } = require('webpack-merge');
var commonConfig = require('./webpack.common.config');
module.exports = merge(commonConfig, {
/* 值得注意的就是,webpack-dev-server所使用的bundle.js文件并不是webpack.config.js中output打包生成的bundle.js,
而是使用webpack-dev-server自己打包生成的,这个文件不存在与output或其他路径中,
而是存到了内存中,事实上webpack-dev-server所使用的那个bundle.js我们是看不到的!*/
devServer: {
contentBase: './dist', // 是指定在哪个路径下或者文件夹下中开启服务器;
open: true, // 开启服务器时,自动打开浏览器
inline: true,
hot: true,
port: 8081 // 开启的服务器的端口
},
mode: 'development',
});
- webpack.prod.config.js
var path = require('path');
var webpack = require('webpack');
var MiniCssExtractPlugin = require('mini-css-extract-plugin');
var { merge } = require('webpack-merge');
var commonConfig = require('./webpack.common.config');
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = merge(commonConfig, {
plugins: [
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[id].css',
}),
new webpack.DefinePlugin({
'isDevelopment': false,
})
],
mode: 'production',
});
- 定义环境变量
// 能够提供一个设置环境变量的scripts,让你能够以unix方式设置环境变量,然后在windows上也能兼容运行
npm install --save-dev cross-env
// 可以在webpack.js里使用es6语法
npm install --save-dev babel-register
resolve 配置
resolve 配置 webpack 如何寻找模块所对应的文件。webpack 内置 JavaScript 模块化语法解析功能,默认会采用模块化标准里约定好的规则去寻找,但你可以根据自己的需要修改默认的规则
alias | extensions |
---|---|
给你的项目常用的引用路径给别名 | 设置文件查找顺序 |
1.alias
给你的项目常用的引用路径给别名
// 第一步:webpack.common.config.js添加
module.exports = {
//....
resolve: {
alias: {
'@css': '../less'
}
}
}
// 第二步:second.js
// 之前的引用方式
import '../less/common.less'
// 现在的引用方式
import '@css/common.less'
2.extensions设置查找文件顺序
适配多端的项目中,可能会出现 .web.js, .wx.js,例如在转web的项目中,我们希望首先找 .web.js,如果没有,再找 .js,默认找.js
//webpack.config.js
module.exports = {
//....
resolve: {
extensions: ['web.js', '.js'] //当然,你还可以配置 .json, .css
}
}
externals
如果需要引用一个库,但是又不想让webpack打包(减少打包的时间),并且又不影响以import方式引用,那就可以通过配置externals
// 第一步:index.html引用
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
// 第二步:webpack.config.js添加externals配置
module.exports = {
...
externals: {
jquery: 'window.$'
},
...
}
// 第三步:second.js引用
import $ from 'jquery';
console.log($('#root'), 222)
3、日常实用的插件
1.copy-webpack-plugin
在webpack中拷贝文件和文件夹,configs文件是如何读取的
// 第一步:安装
npm i copy-webpack-plugin -D
//webpack.common.config.js
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
//...
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: path.join(__dirname, 'src/configs/configs.js'),
to: 'js/'
}
],
})
]
}
// index.html读取configs.js
<script type="text/javascript" src="./js/configs.js">
</script>
这样开发环境可生产环境都可以读取configs的配置文件
2.量DefinePlugin
通过webpack定义全局变量,例如有些功能我希望开发环境执行这个功能,而生产环境执行另外的功能
// 可以直接在webback的插件定义
new webpack.DefinePlugin({
'isDevelopment': false, // 开发环境给true,生产环境给false
})
// second.js访问,开发环境会打印true,生产环境打印false
if(isDevelopment){
}else{
}
3.CleanPlugin
清除文件夹,一般用于清除dist文件夹,保证每次打包dist文件夹目录是干净的
// 第一步:安装clean-webpack-plugin
npm i clean-webpack-plugin -D
// 第二步:webpack.prod.config.js添加
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = {
...
plugins: [
new CleanWebpackPlugin(),
]
}
4、常用的打包优化
- webpack 的构建过程太花时间
- webpack 打包的结果体积太大
- 页面资源文件加载太多对服务器压力大
第一种:代码压缩以及公用模块处理
// 1.不写就有的默认配置:
optimization:{
// 压缩js代码
minimizer: true,
// runtime相关的代码(各个模块之间的引用和加载的逻辑)内嵌入每个entry
runtimeChunk: false,
splitChunks: {
// 同步加载,一起打包
chunks: "async",
minSize: 30000,
// 最小公用模块次数
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
// 缓存策略,默认设置了分割node_modules和公用模块
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
// 2.我们项目配置
optimization: {
// 不同的策略来分割打包出来的bundle
splitChunks: {
cacheGroups: {
// 将node_modules的库文件打包到vendors.js
vendors: {
test: chunk => (
chunk.resource &&
/\.js$/.test(chunk.resource) &&
/node_modules/.test(chunk.resource)
),
chunks: 'initial',
name: 'vendors',
},
// 将node_modules的库文件被重复引用的代码打包到async-vendors.js
'async-vendors': {
test: /[\\/]node_modules[\\/]/,
minChunks: 2,
chunks: 'async',
name: 'async-vendors'
}
}
},
// 将业务代码从入口文件分离出来,这样业务代码改变只会更改业务代码对应的js,不会影响入口文件的js,使用输出文件的chunkFilename调整名字
runtimeChunk: {
name: 'manifest'
},
minimizer: [
// js压缩
new UglifyJsPlugin({
cache: true,
parallel: true,
uglifyOptions: {
ecma: 5,
output: {
beautify: false,
},
compress: {
drop_console: true,
}
}
}),
// css压缩
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
discardComments: {
removeAll: true
},
// 避免 cssnano 重新计算 z-index
safe: true
}
}),
],
}
第二种:happypack提升打包速度
webpack 是单线程的,就算此刻存在多个任务,你也只能排队一个接一个地等待处理。这是 webpack 的缺点,好在我们的 CPU 是多核的,Happypack 会充分释放 CPU 在多核并发方面的优势,帮我们把任务分解给多个子进程去并发执行,大大提升打包效率
// 第一步:安装happypack
npm i happypack -D
// 第二步:webpack.common.config.js添加
const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
module.exports = {
module: {
rules: [
{
test: /\.js$/,
//把对.js 的文件处理交给id为happyBabel 的HappyPack 的实例执行
loader: 'happypack/loader?id=happyBabel',
//排除node_modules 目录下的文件
exclude: /node_modules/
},
]
},
plugins: [
new HappyPack({
//用id来标识 happypack处理那里类文件
id: 'happyBabel',
//如何处理 用法和loader 的配置一样
loaders: [{
loader: 'babel-loader?cacheDirectory=true',
}],
//共享进程池
threadPool: happyThreadPool,
//允许 HappyPack 输出日志
verbose: true,
})
]
}
webpack-bundle-analyzer
可以直观分析打包出的文件包含哪些,大小占比如何,模块包含关系,依赖项,文件是否重复,压缩后大小如何,针对这些,我们可以进行文件分割等操作
// 第一步:安装
npm install webpack-bundle-analyzer --save-dev
// 第二步:webpack.config.common.js添加
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
module.exports = {
...
plugins: [
new BundleAnalyzerPlugin(),
]
...
}
- 如图
可以看出antd、react-dom、lodash、moment几个包大小最大,我们可以把他们拆出去
第三种:dll动态链接库
当有依赖包较大时,依赖包不常改动,可以将这些依赖包通过DllPlugin打包,这样开发和生产环境构建时就可以不构建这些文件,提高打包编译速度
// 第一步添加webpack.config.dll.js,并添加配置打包对应组件
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: {
antd: [
'antd/lib/date-picker',
'antd/lib/table',
'antd/lib/form',
'antd/lib/steps',
'antd/lib/input',
'antd/lib/row',
'antd/lib/col',
'antd/lib/button',
'antd/lib/message',
'antd/lib/icon',
'antd/lib/cascader',
'antd/lib/checkbox',
'antd/lib/modal',
'antd/lib/upload',
'antd/lib/progress',
],
reactVendors: [
'react',
'react-dom',
'lodash',
'moment'
]
},
output: {
// 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称,
filename: '[name].dll.js',
path: path.resolve('dist/dll/js'),
// library必须和后面dllplugin中的name一致 后面会说明
library: '[name]_dll_[hash]'
},
plugins: [
// 接入 DllPlugin
new webpack.DllPlugin({
// 动态链接库的全局变量名称,需要和 output.library 中保持一致
// 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
name: '[name]_dll_[hash]',
// 描述动态链接库的 manifest.json 文件输出时的文件名称
path: path.join(__dirname, 'dist/dll/js', '[name].manifest.json')
}),
],
mode: 'production',
}
// 第二步,webpack.config.common.js添加对应生成的manifest.json
module.exports = {
...
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./dist/dll/js/reactVendors.manifest.json')
}),
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./dist/dll/js/antd.manifest.json')
}),
]
...
}
// 第三步,index.ejs引用打包出的js
<script src="./dll/js/antd.dll.js" ></script>
<script src="./dll/js/reactVendors.dll.js" ></script>
前后对比
未加DllPlugin | 加了DllPlugin |
---|---|
总入口文件7.14m | 总入口文件3.56m |
输出文件3.18m | 输出文件1.47m |
Gzip压缩967k | Gzip压缩451k |
打包耗时23s | 打包耗时21s |
第四种:引用cdn
- cdn优化是指把第三方库比如(vue,vue-router,axios)通过cdn的方式引入项目中,这样vendor.js会显著减少,并且大大提升项目的首页加载速度
- css、image都可以放到cdn提升页面加载速度
// 第一步:引用antd、react、react-dom、moment、lodash的cdn
<script src="https://cdn.bootcdn.net/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/react-dom/16.3.2/umd/react-dom.production.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script>
<script src="https://cdn.bootcss.com/moment.js/2.22.1/moment.min.js"></script>
<script src="https://cdn.bootcss.com/moment.js/2.22.1/locale/zh-cn.js"></script>
<script crossorigin="anonymous"
integrity="sha512-N8Wbuq8SeLoZ4j1Jfwva1Xd8MVNrSxVFzuxDF1YnUcNB6YncjyNtUAuPs1SSncDd98FcM/ZkvgXsr3DfwERW6A=="
src="https://lib.baomitu.com/antd/3.26.15/antd-with-locales.min.js"></script>
<link crossorigin="anonymous"
integrity="sha512-cBOfQ2h52wVLbgKbFuf6wf0qlrRhbwwAlrIMFjU3NrvRbr/OxYplBiIV4bmk1arz1g7T85HGq36hP6kAxPyt1w=="
href="https://lib.baomitu.com/antd/3.26.15/antd.min.css" rel="stylesheet">
<script crossorigin="anonymous"
integrity="sha512-131CXtlyQOY833mTVES49ngssH0LdxpKuv6NU/ex69688rmm+OSswA2yiiNAPTelbhwo0PVEts8a5RlbQnA0rA=="
src="https://lib.baomitu.com/antd/3.26.15/antd.min.js"></script>
// 第二一步:webpack.config.common.js修改
module.exports = {
...
// externals告诉webpack那些资源从哪里寻找,值表示的当前环境下的变量,比如引入React之后,React被作为全局对象,webpack就回去寻找React对象
externals: {
"configs": "configs",
"react": "React",
"react-dom": "ReactDOM",
"lodash": "_",
"moment": "moment",
"antd": "antd"
}
...
}
未加cdn | 加了cdn |
---|---|
总入口文件7.14兆 | 总入口文件5.95兆 |
输出文件3.18兆 | 输出文件2.77兆 |
Gzip压缩967k | Gzip压缩810k |
打包耗时25s | 打包耗时22s |
Dll和cdn的区别
DLL | cdn |
---|---|
把公用模块打包为 DLL 文件存到项目文件夹里 | 使用网上的公用模块文件 |
第二次打包时动态链接 DLL 文件,不重新打包 | 第二次加载时直接读取缓存,不重新请求 |
打包时间缩短 | 加载时间缩短 |
网站没有cdn | 网站有cdn |
---|---|
浏览器缓存 | 浏览器缓存+CDN缓存 |
网站文件、图片过多过大,访问量过大会导致服务器压力很大 | cdn会进行分发,降低服务器压力 |
第五种:gzip
开启gzip压缩可以有效压缩资源体积,压缩比率在3到10倍左右,可以大大节省服务器的网络带宽,提高资源获取的速度,节省流量,根据gzip使用的算法特性,代码相似率越大压缩效率越高
// 第一步:安装
npm i compression-webpack-plugin@2.0.0 -D
// 第二步:webpack.config.common.js引入
import CompressionWebpackPlugin from 'compression-webpack-plugin';
// 添加
module.exports = {
...
plugins: [
new CompressionWebpackPlugin({
filename: "[path].gz[query]", // 目标文件名
algorithm: "gzip", // 使用gzip压缩
test: new RegExp(
"\\.(js|css)$" // 压缩 js 与 css
),
threshold: 10240, // 资源文件大于10240B=10kB时会被压缩
minRatio: 0.8, // 最小压缩比达到0.8时才会被压缩
),
]
...
}
- dll更用于提升打包速度
- cdn更用于提升页面加载速度
- 压缩项目资源文件
总结
按需加载
可以看我们打包的代码会放在一起,很多时候我们不需要一次性加载所有的JS功能文件,而应该在不同阶段去加载所需要的代码。webpack内置了强大的分割代码的功能可以实现按需加载
使用webpack打包的出来的文件要实现以上的要求有两种方式,一个是webpack特有的require.ensure方法,还有一个是import方法
Webpack中的import
与正常ES6中的import语法不同,通过import函数加载的模块及其依赖会被异步地进行加载,并返回一个Promise对象。
一般正常的模块加载代码如下:
假设async.js的资源体积很大,并且我们在页面初次渲染的时候并不需要使用它,就可以对它进行异步加载。
async.js和second.js代码如下:
// async.js
console.log('我是async模块')
export const a = '模块async'
// second.js
document.querySelector('#btn').onclick = function () {
console.log('我是通过import来实现按需加载的')
let a = import('./async.js')
a.then(function (res) {
console.log('加载完成的async模块', res)
})
}
dva/router就是这种类似的原理(require.ensure),不会一次性加载所有页面的js,用到才加载