第四课之webpack进阶

回顾webpack和webpack-dev-server

webpack webpack-dev-server
适用于webpack打包机制 适用于webpack打包机制
输出真实的文件一般是dist 输出的文件只存在于内存中,不输出真实的文件
文件修改要重新打包 文件修改保存会自动更新
按生产环境需求进行打包生产环境代码 按开发环境需求进行打包开发环境代码

案例

案例一

babel使用

  • Babel 是一个 JavaScript 编译器,可以把ES6的语法转为兼容浏览器的ES5语法

webpack里使用babel

// 已经在项目里安装了webpack的情况下

npm install -D babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime @babel/runtime


// 添加.babelrc
{
  "presets": [
    "@babel/env"
  ],
  "plugins": [
    "@babel/plugin-proposal-class-properties",
    "@babel/plugin-transform-runtime"
  ]
}
// 添加js的loader
{
    test: /\.jsx?$/,
    use: {
        loader: 'babel-loader',
        options: {
            extends: join(__dirname, '.babelrc'),
            cacheDirectory: true,
        },
    },
    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

用法:

// 安装命令
npm i webpack-merge -D

// 开发和生产环境需要引入
var { merge } = require('webpack-merge');
var commonConfig = require('./webpack.common.config');

// webpack开发环境合并
merge(commonConfig, devConfig)
// webpack生产环境合并
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"
  • 定义环境变量
// 能够提供一个设置环境变量的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 '@css/common.less'
// 现在
import '../less/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
    }
}

日常实用的插件

1.copy-webpack-plugin

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

// 可以直接在webback的插件定义
new webpack.DefinePlugin({
    'isDevelopment': false,
})

// second.js访问
console.log(isDevelopment)

3.清除文件夹CleanPlugin

// 安装clean-webpack-plugin
npm i copy-webpack-plugin -D
new CleanPlugin([
    join(__dirname, 'dist', 'resources', 'js', '*.*'),
    join(__dirname, 'dist', 'resources', 'css', '*.*'),
    join(__dirname, 'dist', 'resources', 'images', '*.*'),
    join(__dirname, 'dist', '*.ico'),
    join(__dirname, 'dist', '*.html'),
]),

其余实用配置

1.externals

如果需要引用一个库,但是又不想让webpack打包(减少打包的时间),并且又不影响以import方式引用,那就可以通过配置externals

// index.html引用
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>

// webpack.config.js
module.exports = {
  ...
  externals: {
      jquery: 'window.$'
  },
  ...
}

// second.js引用
import $ from 'jquery';
console.log($('#root'), 222)

常用的打包优化

  • webpack 的构建过程太花时间
  • webpack 打包的结果体积太大

webpack-bundle-analyzer

可以直观分析打包出的文件包含哪些,大小占比如何,模块包含关系,依赖项,文件是否重复,压缩后大小如何,针对这些,我们可以进行文件分割等操作

npm install webpack-bundle-analyzer --save-dev

// webpack.config.common.js
module.exports = {
  ...
  plugins: [
    new BundleAnalyzerPlugin(),
  ]
  ...
}
  • 如图
    第四课之webpack进阶

可以看出antd、react-dom、lodash、moment几个包大小最大,我们可以把他们拆出去,antd支持按需加载功能,打包出来的js也不会太大,所以不提出

第一种:代码压缩以及公用模块处理

  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

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,
    })
  ]
}

第三种: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会显著减少,并且大大提升项目的首页加载速度

// 第一步:引用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 文件,不重新打包 第二次加载时直接读取缓存,不重新请求
打包时间缩短 加载时间缩短

第三种: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更用于提升页面加载速度
  • 压缩项目资源文件

总结
第四课之webpack进阶

按需加载

可以看我们打包的代码会放在一起,很多时候我们不需要一次性加载所有的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就是这种类似的原理,不会一次性加载所有页面的js,用到才加载

上一篇:文件上传组件(基于 antd )


下一篇:拿空间换时间的字典-Python基础前传(9)