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

导读

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

我能学会什么

经过本节的学习,你能够学会html

  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语法。vue

新建一个math.js,而后咱们在 m.js中引入,自行修改打包配置文件,若是你还不会请点击3分钟了解webapcknode

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中能够作如下配置。jquery

"sideEffects": fasle
复制代码

须要注意的是,这个在线上环境才有用,由于他在开发中会方便咱们去调试。webpack

区分线上环境与开发环境配置

咱们为何要这么作

每次打包(线上,开发)代码以前,咱们都会去不断修改webpack.config.js中的文件,例如modo,插件等之类的,这样的操做是很麻烦的,并且咱们也不可能100%保证咱们就不会改错文件,毕竟改错了文件的影响是很是大的,接下来咱们一块儿看看若是区分线上与开发环境的配置。git

拆分dev与prod文件

咱们以前有一个webpack.config.js,咱们将其重命名为webpack.dev.js,而后复制一份改名webpack.prod.js。而后根据须要更新以下两个文件。es6

// 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里面的scriptsgithub

"scripts": {
    "dev": "webpack-dev-server --config webpack.dev.js",
    "build": "webpack --config webpack.prod.js"
  },
复制代码

重启服务,打包文件正常运行。这样咱们就区分开了,可是有一个很明显的问题就是,这两个文件,重复的地方太多了,若是我之后新增了一个公共的代码,两个文件都要加,删除也是两个文件都要作。这样就会让个人维护成本变高,并且仍是会增长错误的概率,因此咱们有必要对配置文件进行合并。web

合并公共配置文件

先下载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,我们第五节见。

相关文章
相关标签/搜索