导读
再学了前三节之后,其实我们已经会对文件资源等进行一些打包,但是这些在大型项目中是远远不够的,那么我们在平时的配置中还会遇到什么难题呢?
我能学会什么
通过本节的学习,你可以学会
- 按需打包文件
- 区分线上环境与开发环境配置
- Webpack 和 Code Splitting
- SplitChunksPlugin 配置参数详解
- Lazy loading/chunk
- 打包分析流程
- webpack与浏览器缓存问题
- css分割
- 浏览器缓存
- Shimming
- 环境变量
具体应用
- 根据import引入的代码按需打包,避免form的文件整体打包,我引入什么你打包什么
- 根据自己的需求自行配置线上与开发环境的配置,拆分公共配置代码,使用自定义命令一键打包代码
- 代码分割,同步加载与异步加载的配置
- SplitChunksPlugin常用配置详解
- 懒加载例子与chunk介绍
- 简单打包分析,preloading,prefetching
- 不再将css混淆打包,而是在dist目录下生成一个css文件夹,然后打包进去
- js文件在浏览器中缓存问题,打包解决方式
- Shimming作用
- 环境变量的配置与使用
Tree Shaking
接上一讲,如果我们在preset-env设置了"useBuiltIns": "usage",那么实际上我们不去引入babel/polyfill也是可以的。因为我们在使用useBuiltIns,它会自动帮我们引入,所以这节我们直接可以写es6语法。
新建一个math.js,然后我们在 m.js中引入,自行修改打包配置文件,如果你还不会请点击3分钟了解webapck
export const add = (a, b) => {
return a + b
}
export const minus = (a, b) => {
return a - b
}
// m.js
import { add } from './math'
console.log(add(1, 3))
这个时候我们虽然实现了效果,但是在打包文件中,我却将我的math文件完全打包了。这里我却只引入了add方法,所以我是希望他只打包我引入的文件。所以在package.json中可以做以下配置。
"sideEffects": fasle
需要注意的是,这个在线上环境才有用,因为他在开发中会方便我们去调试。
区分线上环境与开发环境配置
我们为什么要这么做
每次打包(线上,开发)代码之前,我们都会去不断修改webpack.config.js中的文件,例如modo,插件等之类的,这样的操作是很麻烦的,而且我们也不可能100%保证我们就不会改错文件,毕竟改错了文件的影响是非常大的,接下来我们一起看看如果区分线上与开发环境的配置。
拆分dev与prod文件
我们之前有一个webpack.config.js,我们将其重命名为webpack.dev.js,然后复制一份更名webpack.prod.js。然后根据需要更新如下两个文件。
// webpack.dev.js
const path = require('path'); // 从nodejs中引入path变量
const htmlPlugin = require('html-webpack-plugin'); // 引入html打包插件
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const webpack = require('webpack') // 引入webpack插件
module.exports = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
entry: {
main: './src/m.js',
},
devServer: {
contentBase: './dist', // 借助webpack启动服务器,根目录就是打包之后的dist文件夹
open: true, // 启动npm run start的时候自动打开浏览器
proxy: { // 配置代理
'/api': 'http://localhost:3000'
},
port: 8080, // 配置端口号
hot: true, // 开启热更新
//hotOnly: true // 就算是html文件没生效也不刷新页面
},
module: { // 模块打包配置
// ... dev和prod一样 不写了
},
plugins: [
new htmlPlugin({
template: './index.html'
}),
new CleanWebpackPlugin(),
new webpack.HotModuleReplacementPlugin() // 引入插件
],
output: {
publicPath: '/',
filename: 'dist.js', // 打包后生成的main.js
path: path.resolve(__dirname, 'dist'), // 打包到dist文件夹
}
}
// webpack.prod.js
const path = require('path'); // 从nodejs中引入path变量
const htmlPlugin = require('html-webpack-plugin'); // 引入html打包插件
const cleanPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'production',
devtool: 'cheap-module-source-map',
entry: {
main: './src/m.js',
},
module: { // 模块打包配置
// ...
},
plugins: [
new htmlPlugin({
template: './index.html'
}),
new cleanPlugin(['dist']),
],
output: {
publicPath: '/',
filename: 'dist.js', // 打包后生成的main.js
path: path.resolve(__dirname, 'dist'), // 打包到dist文件夹
}
}
ok,文件以及拆分了,这个时候我们会修改package.json里面的scripts
"scripts": {
"dev": "webpack-dev-server --config webpack.dev.js",
"build": "webpack --config webpack.prod.js"
},
重启服务,打包文件正常运行。这样我们就区分开了,但是有一个很明显的问题就是,这两个文件,重复的地方太多了,如果我以后新增了一个公共的代码,两个文件都要加,删除也是两个文件都要做。这样就会让我的维护成本变高,而且还是会增加错误的几率,所以我们有必要对配置文件进行合并。
合并公共配置文件
先下载webapck-merge,他可以帮助我们合并webpack的配置
npm install webpack-merge -D
新建webpack.common.js进行代码合并
- entry一样,提取出来。
- module一样,提取出来。
- plugins有两个公共插件,提出出来。
- output一样,提取出来
// webpack.common.js
const path = require('path'); // 从nodejs中引入path变量
const htmlPlugin = require('html-webpack-plugin'); // 引入html打包插件
const cleanPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
main: './src/m.js',
},
module: { // 模块打包配置
// ... 省略
},
plugins: [
new htmlPlugin({
template: './index.html'
}),
new cleanPlugin(['dist']),
],
output: {
publicPath: '/',
filename: 'dist.js', // 打包后生成的main.js
path: path.resolve(__dirname, 'dist'), // 打包到dist文件夹
}
}
// webpck.dev.js
const webpack = require('webpack') // 引入webpack插件
const webpackMerge = require('webpack-merge')
const commonConfig = require('./webpack.common')
const devConfig = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
devServer: {
contentBase: './dist', // 借助webpack启动服务器,根目录就是打包之后的dist文件夹
open: true, // 启动npm run start的时候自动打开浏览器
proxy: { // 配置代理
'/api': 'http://localhost:3000'
},
port: 8080, // 配置端口号
hot: true, // 开启热更新
//hotOnly: true // 就算是html文件没生效也不刷新页面
},
plugins: [
new webpack.HotModuleReplacementPlugin() // 引入插件
]
}
module.exports = webpackMerge(commonConfig, devConfig)
// webpack.prod.js
const webapckMerge = require('webpack-merge')
const commonConfig = require('./webpack.common')
const prodConfig = {
mode: 'production',
devtool: 'cheap-module-source-map',
}
module.exports = webapckMerge(commonConfig, prodConfig)
npm run dev,ok nice.
Code Splitting
代码分割,这个我就举例说明一下就ok。
在我们平时使用vue等大框架的时候,经常会用到一个lodash.js,假设我们正常的下载并使用改代码。
文件 | 大小 |
---|---|
lodash.js | 1MB |
axin.js | 1MB |
// axin.js
import _ form 'lodash'
// 使用lodash
这样的话,假设我们的代码不做压缩,我们的代码就会达到2MB大小,如果用户打开我们的网页,这个时候我们就会先去加载这个2mb的文件,这样的话,对用户体验很不好。那如果我们能够达到如下效果,就好多了 。
// lo.js
import _ form 'lodash'
window._ = _
// axin.js
// 使用lodash.js
js是支持并行加载的,不能说一定比2m的快,但是至少能优化不少,最大的好处是什么?是我们如果只是修改了axin.js的内容,那我们的lo.js是不需要改变的,浏览器中会有缓存,这个时候想要的效果就会明显提升。
那么我们在webpack中应该如何配置呢?找到webpack.common.js
optimization: {
splitChunks: {
chunks: 'all'
}
},
这个时候就会帮我们去拆分代码,需要特别说明的是,webpack和Code Splitting是没有关系的,默认的会帮我们下载一个功能,我们只需要配置即可。
这个是同步加载的方式,有时候我们的文件是异步回来的,其实也是这么一回事。我就不多做演示。 大家有兴趣的可以自己下来试试。
SplitChunksPlugin
为了搞清楚,这个插件,还是没能逃避写一个异步加载的方法来使用组件。
function getComponent(){
return import('lodash').then(({ default: _}) => {
var element = document.createElement('div')
element.innerHTML = _.join(['jsxin', 'hello'], '-')
return element
})
}
getComponent().then(element => {
document.body.appendChild(element)
})
// {default: } 加载回来的赋值给_
无论是同步加载或者异步,我们都会进行代码分割。我们先来下载一个官方提供的动态引入的插件。
日常直通车:babeljs.io/docs/en/nex…
npm install --save-dev @babel/plugin-syntax-dynamic-import
// .babelrc
{
"plugins": ["@babel/plugin-syntax-dynamic-import"]
}
//package.json
"dev-build": "webpack --config webpack.dev.js",
webpack-dev-server会把文件写到内存我们是观察不到的,所以新增一个命令npm run dev-build,让其打包代码。
这时候给我生成了一个0.dist.js
我们可以在引入之前使用注释符为其设置名字
function getComponent(){
return import(/* webpackChunkName: "loadash" */'lodash').then(({ default: _}) => {
var element = document.createElement('div')
element.innerHTML = _.join(['jsxin', 'hello'], '-')
return element
})
}
就会生成一个vendors~lodash.dist.js
因为这里设置的比较多,我们简单的把配置项讲解一下,以下为配置项,如果你的splitChunks没有配置任何内容,就会使用以下的内容作为配置项。
optimization: {
splitChunks: {
chunks: 'async', // all 不区分 async 只对异步代码生效
minSize: 30000, // 打包最小30000字节我才去分割
minRemainingSize: 0,
maxSize: 0, // 一般配置 50000 就相当于能拆分成几个50kb左右的
minChunks: 1, // 最少使用一次
maxAsyncRequests: 6, // 同时加载的模块数最多6个
maxInitialRequests: 4, // 入口文件也会拆分 但是最多4个 超过了就不分分割了
automaticNameDelimiter: '~', // 名字和组的拼接符 vendors~lodash
cacheGroups: { // 拆分分组
defaultVendors: { // 默认分组
test: /[\\/]node_modules[\\/]/, // 如果是node_modules中的我们就到defaultVendors这个组
priority: -10, // 优先级, 和下面default 同时满足条件 打包到优先级高的里面
// filename: 'vendor.js' 可以自己取名字
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true // 比如之前引用了a代码,就不会打包a到common.js,会复用
// filename: 'common.js' 可以自己取名字
}
}
}
},
Lazy Loading
懒加载
我们对刚才的异步代码做一点改进
function getComponent(){
return import(/* webpackChunkName: "loadsh" */'lodash').then(({ default: _}) => {
var element = document.createElement('div')
element.innerHTML = _.join(['jsxin', 'hello'], '-')
return element
})
}
document.addEventListener('click', () => {
getComponent().then(element => {
document.body.appendChild(element)
})
})
我们还是异步加载一个loadsh函数,然后在页面中绑定了一个点击事件,只有我们监听到点击事件的时候,我们才回去调用getComponent方法,然后通过getComponent方法去引入loadsh函数。
效果就是我们在页面中,开始只会加载一个main.js,然后点击一下页面会在加载一个loadsh函数,调用这个函数的某些方法我们实现了一个字符串的拼接过程,最终呈现在了页面上。
通过import方法,我们只有访问了在某些文件的时候,他才会异步加载,然后执行。这样我们加载速度也会更快。
当然后也可以使用es7中比较流行的async来处理这个时间,让你们的代码更加直爽。
async function getComponent() {
const { default: _ } = await import(/* webpackChunkName: "loadsh" */'lodash')
const element = document.createElement('div')
element.innerHTML = _.join(['jsxin', 'hello'], '-')
return element
}
chunk
我们在之前已经使用了很多次chunk了,那么我们这个chunk到底是什么?
在js代码打包中,我们会拆分成多个js文件,那么每一个js文件,我们都称它为一个chunk。
打包分析,preloading, prefetching
打包分析
先来看看官方的webpack分析工具
如果你相对我们打包之后的代码进行分析,首先你需要将--profile --json > stats.json 放到你打包的命令中
"dev-build": "webpack --profile --json > stats.json --config webpack.dev.js",
他的意思就是将我的打包过程放到stats.json这个文件中。
他会将我们整个打包的流程都写进入,比较耗时,打包了什么资源,有几个模块,几个chunk等,你可以可以借助官方工具帮你翻译一下。这里大家可以了解一下,我就不多做介绍,可以自行打包尝试。
preloading
在这个知识点之前我们先来看看我们最原始的代码写法。
document.addEventListener('click', () => {
const element = document.createElement('div')
element.innerHTML = 'jsxin'
document.body.appendChild(element)
})
这个是我们常用的标准写法,难道这个写法就没有优化空间了吗?
编译成功之后我们打开f12, 然后按住command+shift+p,输入coverage这个关键词
我们点击一下show Coverage,然后左侧会出现一个录制按钮,我们会发现我们的main.js只有75%的代码使用率。
为什么呢?因为我们在页面加载的使用,我们并不会使用
const element = document.createElement('div')
element.innerHTML = 'jsxin'
document.body.appendChild(element)
这些代码是在被点击的时候才会用到,所以这不是webpack推荐的一种书写代码的方式。
我们可以将这部分代码这么写,新建click.js
// click.js
export default function addComponent () {
const element = document.createElement('div')
element.innerHTML = 'jsxin'
document.body.appendChild(element)
}
// com.js
document.addEventListener('click', () => {
import('./click.js').then(({ default: _ }) => {
_()
})
})
这样的话,他的使用了就达到了79%,也会节约我们的首屏加载时间。
prefetching
我们一个网页,刚开始初始化首页的时候,我们不加载登录模态框,先加载首页的其他逻辑,等加载完成之后,带宽被释放出来了,我们偷偷的加载登录模态框,这样的话,既满足了我首页加载快的需求,又满足了登录加载快的需求。
而这个方案就是我们结合prefetching和preloading的一个比较实用的例子。
可以在import之前声明prefetching配置
import(/* webpackPrefetch: true */'./click.js')
这个时候,等他将我们的核心代码加载完成之后,就会偷偷的加载click.js
CSS代码分割
场景
我们生成如下代码
import '../statics/style/index.css'
console.log(123)
我现在希望我的index.css不直接生成css代码到我的页面上,而是希望他在dist下面新建一个文件夹,然后把css放进去引入,那么这么时候我们应该怎么处理这种操作呢?
插件介绍
官方插件:webpack.js.org/plugins/min…
特别说明:适合线上环境中使用,因为更新之后不会自动刷新
先来安装一下插件
npm install --save-dev mini-css-extract-plugin
然后在线上环境中使用。
插件配置
// webpack.prod.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
plugins: [new MiniCssExtractPlugin()],
然后我们之前使用的style-loader就不能用了,他给我们提供了一个loader,我们将style-loader替换成他的loader,然后还要将css区分开线上与开发环境。
// webpack.prod.js
module: {
rules: [{
test: /\.css$/, // 检测文件是css结尾的
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
{
test: /\.scss$/, // 检测文件是scss结尾的
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders: 2, // 通过import引入的scss文件,也要走下面两个loader
// modules: true
}
},
'sass-loader',
'postcss-loader'
]
}]
}
然后我们打包一个线上的代码试试。就在我们的代码中生成了main.css文件。
配置项
我们简单的看看他的配置项
plugins: [new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[name].chunk.css'
})],
如果样式是直接被引用,他就会走filename,间接就是chunkfilename
我们不妨再来做一些尝试。
// index.css
.avatar{
width: 100px;
height: 100px;
}
// index1.css
.avatar{
display: block;
}
// style.css
import '../statics/style/index.css'
import '../statics/style/index1.css'
console.log(123)
我们再次打包,你会发现,他自动将两个样式文件给我合并到main.css中了。
// 打包后
.avatar{
width: 100px;
height: 100px;
}
.avatar{
display: block;
}
/*# sourceMappingURL=main.css.map*/
ok,那么我们如果还想对这个css进行一些压缩怎么办呢?
optimize-css-assets-webpack-plugin
// install
npm install optimize-css-assets-webpack-plugin -D
// prod
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
optimization: {
minimizer: [new OptimizeCSSAssetsPlugin({})]
}
然后在打包,他不仅将我们的代码进行了压缩,还将我们的代码合并到了一起。
// 打包后
.avatar{width:100px;height:100px;display:block}
ok,其他更高级的用法,请参考官方文档,这里一方面是和大家一起体验,另一方面是推荐常用的插件。
浏览器缓存
我们在加载一个网页的时候,我们可能首先会加载一个index.html,和两个js文件,当你下次访问的时候,其实浏览器已经对你两个js有缓存了,这个时候会优先读取缓存中的文件。
这个时候要么你更改一下文件的名字,要么就强制刷新,但是你肯定不能让用户强制刷新页面。所以我们在调试过程中可以不管,重新配置一下output
// cache.sj
import _ from 'lodash'
let str = _.join(['j', 's', 'x', 'i', 'n'], '-')
console.log(str)
output: {
filename: '[name].[contenthash].js',
}
contenthash就是我们根据内容生成的一个hash值,只要你的内容没有改变,那么我们就不用重新去加载这些js。
我们改变的是什么?我们会修改自己的逻辑源代码,但是你并不会去改变node_modules的第三方代码,所以这些东西肯定还是可以让浏览器读缓存,提高网站加载效率。
Shimming
webpack是基于模块打包的,也就是说我们在一个模块里面的代码,到另外一个模块就找不到了。
// jq.js
import $ from 'jquery'
import { jqui } from './jq.ui'
jqui()
$('body').append('<div>axin</div>')
// jq.ui.js
export function jqui(){
$('body').css('background','red')
}
// $ is not defined
webpack是提供了一下插件的,我们来看看他是干嘛的。
new webpack.ProvidePlugin({
$: 'jquery'
})
他会去检测你哪个文件是使用了$符,如果使用了,那么你是否在上面引入了jquery,如果没有的话,他就会自动帮你在上面引入,非常nice。
还记得我们使用了一个babel/polyfill吗?如果你没有promise等,他会帮你完成promise的实现。
环境变量
我们之前是在dev/prod环境中合并的common,那么这里我们来使用一个环境变量来重新合并我们的打包配置文件,先上代码。
// dev/prod 注释以下代码 然后分包导出
const webapckMerge = require('webpack-merge')
const commonConfig = require('./webpack.common')
module.exports = prodConfig
module.exports = devConfig
// webpack.common.js
const webapckMerge = require('webpack-merge')
const prodConfig = require('./webpack.prod')
const devConfig = require('./webpack.dev')
const commonConfig = ...(配置)
module.exports = (env) => {
if(env && env.production){
return webapckMerge(prodConfig, commonConfig)
}
return webapckMerge(devConfig, commonConfig)
}
这里我们直接导出了一个函数,接收了一个env,所以我们在打包脚本中这么这么写
"scripts": {
"dev-build": "webpack --profile --json > stats.json --config webpack.common.js",
"dev": "webpack-dev-server --config webpack.common.js",
"build": "webpack --env.production --config webpack.common.js"
},
配置了env变量,我们在打包的时候都是webpack.common.js,然后判断就可以不同的配置进行打包了。
ok,就到这里吧,知识点即总结,如果你也想和我一起学习webpack,咱们第五节见。