大声对webpack4.0说声你好之webpack的高级应用(四)

2,263 阅读14分钟

导读

再学了前三节之后,其实我们已经会对文件资源等进行一些打包,但是这些在大型项目中是远远不够的,那么我们在平时的配置中还会遇到什么难题呢?

我能学会什么

通过本节的学习,你可以学会

  1. 按需打包文件
  2. 区分线上环境与开发环境配置
  3. Webpack 和 Code Splitting
  4. SplitChunksPlugin 配置参数详解
  5. Lazy loading/chunk
  6. 打包分析流程
  7. webpack与浏览器缓存问题
  8. css分割
  9. 浏览器缓存
  10. Shimming
  11. 环境变量

具体应用

  1. 根据import引入的代码按需打包,避免form的文件整体打包,我引入什么你打包什么
  2. 根据自己的需求自行配置线上与开发环境的配置,拆分公共配置代码,使用自定义命令一键打包代码
  3. 代码分割,同步加载与异步加载的配置
  4. SplitChunksPlugin常用配置详解
  5. 懒加载例子与chunk介绍
  6. 简单打包分析,preloading,prefetching
  7. 不再将css混淆打包,而是在dist目录下生成一个css文件夹,然后打包进去
  8. js文件在浏览器中缓存问题,打包解决方式
  9. Shimming作用
  10. 环境变量的配置与使用

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,咱们第五节见。