webpack4进阶

2019/09/29 tools

webpack4学习笔记三:webpack进阶

1、 自动清理构建目录产物

1.1 使用npm scripts清理构建目录

scripts配置里面加这个语句

  rm -rf ./dist && webpack

1.2 自动清理构建目录

使用插件clean-webpack-plugin,默认会删除output指定的输出目录

安装:npm install clean-webpack-plugin -D

使用:

  const {CleanWebpackPlugin} = require('clean-webpack-plugin');

  plugins: [
    new CleanWebpackPlugin()
  ]

2、 自动补齐css前缀

各个浏览器内核前缀一览:

trident: -ms Geko:-moz webkit: -webkit presto: -o

自动补齐CSS需要使用到:autoprefixerpostcss-loader;autoprefixer它是根据www.caniuse.com这个网站的规则来进行补齐的;postcss-loader有很多功能,包括自动补齐css前缀,css module等

安装: ` npm install autoprefixer postcss-loader -D`

使用:

  module:{
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader', {
          loader: 'postcss-loader',
          options: {
            plugins: () => [
              require('autoprefixer')({
                "overrideBrowserslist": [
                  "last 2 version",
                  "> 1%"
                ]
              })
            ]
          }
        }]
      }
    ]
  }

3、 css自动做rem转换

使用:px2rem-loader插件、淘宝的插件lib-flexible, lib-flexible需要手动放到打包好的文件里面的index.html里面,需要手动写一个script,把源码复制放进去。手淘的这个库有个好处,它会比较方便的解决手机端的1px问题。考虑到兼容性的话选择这个,如果不考虑的话可以使用VW or VH

安装:npm install px2rem-loader -Dnpm install lib-flexible -S

配置:

  module:{
    rules: [
      {
        test: /.js$/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader', 
          {
            loader: 'postcss-loader',
            options: {
              plugins: () => [
                require('autoprefixer')({
                  "overrideBrowserslist": [
                    "last 2 version",
                    "> 1%"
                  ]
                })
              ]
            }
          },
          {
            loader: 'px2rem-loader',
            options: {
              remUnit: 75, // 1个rem=多少个像素
              remPrecision: 8 //保留几位小数点
            }
          }
        ]
      }
    ]
  },

注意事项:

  1. 如果是使用less-loader的话,需要['px2rem-loader', 'less-loader'],注意前后顺序。
  2. 如果不想转化,可以使用/*no*/语法来注释, font-size:12px; /*no*/;
  3. 会把第三方UI库的px也转了
  4. 适用于移动端,PC还是px好。

4、 资源内联

资源内联可以优化页面框架的初始化,css内联可以避免页面闪动,小图片或者字体内联可以减少HTTP网络请求。

HTML可以使用row-loader, JS的话需要多用一个babel-loader,用来解析ES6

安装使用

  npm install raw-loader@0.5.1 -D

  // 在index.html 里面
  // 内联一个HTML文件
  ${ require('raw-loader!./meta.html') }

  // 内联一个JS文件
  <script> ${require('raw-loader!babel-loader!../../node_modules/lib-flexible/flexible.js')} </script>

CSS 内联的核心思路是:将页面打包过程的产生的所有 CSS 提取成一个独立的文件,然后将这个 CSS 文件内联进 HTML head 里面。这里需要借助 mini-css-extract-plugin 和 html-inline-css-webpack-plugin 来实现 CSS 的内联功能。

  // webpack.config.js

  const path = require('path');

  module.exports = {
      entry: {
          index: './src/index.js',
          search: './src/search.js'
      },
      output: {
          path: path.join(__dirname, 'dist'),
          filename: '[name]_[chunkhash:8].js'
      },
      mode: 'production',
      plugins: [
          new MiniCssExtractPlugin({
              filename: '[name]_[contenthash:8].css'
          }),
          new HtmlWebpackPlugin(),
          new HTMLInlineCSSWebpackPlugin()
      ]
  };

5、 多页面应用打包

每一次页面跳转的说话,后台服务器都会返回一个新的HTML文档,这种类型的网站也就是多页网站,也叫做多页面应用。

5.1 多页面打包基本思路

webpack.config.js中设置entry,一个页面对应一个html-webpack-plugin. 缺点是每次新增或删除页面都需要修改webpack配置。

  entry:{
    app: './src/index.js',
    admin: './src/admin.js'
  },

上面的方法略微繁琐,我们根据程序思维来,动态获取entry和设置html-webpack-plugin。动态设置需要用到一个库glob,我们需要用到glob.sync

  entry: glob.sync(path.join(__dirname, './src/*/index.js')),

我们需要约定好每个入口都是在src文件下的xx文件夹下的index.js

安装: npm install glob -D

调试:

  'use strict'

  const path = require("path");
  const glob = require('glob');
  const webpack = require("webpack");

  const setMPA = () => {
    const entry = {};
    const htmlWebpackPlugins = [];

    const entryFile = glob.sync(path.join(__dirname, './src/*/index.js'));

    console.log(entryFile);

    return {
      entry,
      htmlWebpackPlugins
    }
  }

  // setMPA();  // 调用,查看打印出来的东西
  /*
    输出
    [ 'D:/learn/学习/webpack/webpack多页面/src/index/index.js',
    'D:/learn/学习/webpack/webpack多页面/src/main/index.js' ]
  */ 

  module.exports = {

  }

最终代码:

  'use strict'

  const path = require("path");
  const glob = require('glob');
  const webpack = require("webpack");

  const setMPA = () => {
    const entry = {};
    const htmlWebpackPlugins = [];
    // 使用glob匹配到所有的入口文件
    const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'));

    // 拿到所有的文件进行遍历
    Object.keys(entryFiles).map(index => {
      // 获取到每个文件
      const entryFile = entryFiles[index];
      // 获取到每个入口的名字,设置出口
      const pageName = entryFile.match(/src\/(.*)\/index\.js/)[1];
      entry[pageName] = entryFile;
      htmlWebpackPlugins.push(
        new HtmlWebpackPlugin({
          // 模板
          template: path.join(__dirname, `src/${pageName}/index.html`),
          // 指定打包的文件名字
          filename: `${pageName}.html`,
          chunks: [pageName],
          inject: true,
          minify:{
            html5: true,
            collapseWhitespace: true,
            preserveLineBreaks: false,
            minifyCSS: true,
            minifyJS: true,
            removeComments: false
          }
        })
      )
    })

    return {
      entry,
      htmlWebpackPlugins
    }
  }

  const {entry, htmlWebpackPlugins} = setMPA();


  module.exports = {
    entry: entry, // 入口文件
    output: {  // 导出的位置 文件夹的名字及文件的名字
      path: path.join(__dirname, "dist"), 
      filename: "[name].js",
    },
    mode: "development", // 开发
    module:{
      rules: []
    },
    plugins: [
      new webpack.HotModuleReplacementPlugin()
    ].concat(htmlWebpackPlugins)
  }

运行:npm run build

查看dist目录下面就有四个文件了 admin.js,admin.html,index.js,index.html;

6、 source map

Source map就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置。有了它,出错的时候,除错工具将直接显示原始代码,而不是转换后的代码,但是生产环境中不要使用,会暴露业务逻辑。配置了source-map的项目按F12,查看源代码就可以看到业务逻辑,而不是打包混淆后的代码。

在webpack中可以直接配置:

  module.exports = {
    devtool: 'source-map'
  }

可以配置的类型有下面的几种。

7、 提取页面公共资源

大的项目需要考虑性能优化,基础包、页面公共js文件提取是必备的手段,简单的可以库、插件可以通过直接scriptCDN引入,使用webpack的话需要使用到一下插件。

基础库分离可以使用:html-webpack-externals-plugin, 安装就不说了。

  const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');

  new HtmlWebpackExternalsPlugin({
    externals: [
      {
        module: 'jquery',  // 模块名字
        entry: 'dist/jquery.min.js',  // 入口,一般可能是本地文件或者CDN文件
        global: 'jQuery',  // 全局名字
      },
       // ------    分离CDN   ----------------- 
      {
        module: 'jquery',
        entry: 'https://unpkg.com/jquery@3.2.1/dist/jquery.min.js',
        global: 'jQuery'
      },
    ],
  })

分离完了需要在模板文件内使用script引入,需要在<body></body>里面。

有点不太好,我们还可以使用splitChunksPlugin

7.1 splitChunksPlugin

splitChunksPlugin是webpack4内置的插件。

  module.exports = {
    //...
    optimization: {
      splitChunks: {
        // async:异步引入的库进行分离(默认),  initial: 同步引入的库进行分离, all:所有引入的库进行分离(推荐)
        chunks: 'async', 
        minSize: 30000, // 抽离的公共包最小的大小,单位字节
        maxSize: 0, // 最大的大小
        minChunks: 1, // 资源使用的次数(在多个页面使用到), 大于1, 最小使用次数
        maxAsyncRequests: 5,  // 并发请求的数量
        maxInitialRequests: 3, // 入口文件做代码分割最多能分成3个js文件
        automaticNameDelimiter: '~', // 文件生成时的连接符
        automaticNameMaxLength: 30, // 自动自动命名最大长度
        name: true, //让cacheGroups里设置的名字有效
        cacheGroups: { //当打包同步代码时,上面的参数生效
          vendors: {
            test: /[\\/]node_modules[\\/]/,  //检测引入的库是否在node_modlues目录下的
            priority: -10, //值越大,优先级越高.模块先打包到优先级高的组里
            filename: 'vendors.js'//把所有的库都打包到一个叫vendors.js的文件里
          },
          default: {
            minChunks: 2, // 上面有
            priority: -20,  // 上面有
            reuseExistingChunk: true //如果一个模块已经被打包过了,那么再打包时就忽略这个上模块
          },
          cacheGroups: {
            commons: {
              test: /(react|react-dom)/,  // 匹配出需要分离的包
              name: 'vendors',
              chunks: 'all'
            }
          }
        }
      }
    }
  };

简单使用:不要和html-webpack-externals-plugin一起使用

  module.exports = {
    // ...
    optimization: {
      splitChunks: {
        cacheGroups: { 
          cacheGroups: {
            commons: {
              test: /(jquery)/,  
              name: 'vendors',
              chunks: 'all'
            }
          }
        }
      }
    }
  }

代码抽离成功,使用的话需要注入到HtmlWebpackPlugin插件里面去

  new HtmlWebpackPlugin({
    // 这个前后顺序是有关系的,chunks 决定了插入到 html 里面的 js 脚本的引用顺序。
    chunks: ['vendors', pageName]
  })

使用splitChunks分离公共资源,打包common

  module.exports = {
    // ...
    optimization: {
      splitChunks: {
        minSize: 0,  // 最小0字节,代表只要引用就会打包
        cacheGroups: {
          commons: {
            name: 'commons',
            chunks: 'all',
            minChunks: 2  // 最少引用的次数
          }
        }
      }
    }
  }

使用依然需要注入到HtmlWebpackPlugin插件里面去

8、 Tree Shaking(摇树优化)

未经优化的代码1个模块中可能有多个方法,只要其中的某个方法用到了,整个文件都会被打到bundle里面,tree shaking就是只把用到的方法打入到bundle,没有用到的方法会在uglify阶段被擦除掉。

tree shaking是webpack默认支持的,在.babelrc里设置modules:false即可,在mode: production的情况下默认开启。 tree shaking必须是ES6的语法,CJS的方式不支持。

CJS: commonjs规范,比如Node.js的模块化就是采用commonjs

9、Scope Hoisting

webpack4比较重要的一个特性。未开启scope hoisting的话打包完成的bundle代码会有闭包,项目中大量的模块会带来大量的函数闭包包裹代码,导致体积增大,运行代码时创建的函数作用域变多,内存开销变大。

开启之后会做优化:将所有模块的代码按照引用顺序放到一个函数作用域里面,然后适当的重命名一些变量防止变量名冲突,可以减少函数声明代码和内存开销。Scope housting对模块的引用次数大于1次是不产生效果的,这个其实也很好理解,如果一个模块引用次数大于1次,那么这个模块的代码会被内联多次,从而增加了打包出来的js bundle的体积。

使用: webpack4里面,如果mode为production默认开启,scope hoisting也必须是ES6语法,CJS不支持; 在webpack3里面需要加一行代码

  plugins: [
    nwe webpack.optimize.ModuleConcatenationPlugin()
  ]

10、代码分割

对于大的项目来说,首次便将所有代码都加载是不够有效实用的,特别是当你的某些代码是在某些特殊时候才会被用到,webpack的一个功能就是将代码库分割成chunks(语块),当代码运行到需要它们的时候再进行加载。

适用的场景:

  1. 抽离相同代码到同一个共享块。
  2. 脚本懒加载,使初始下载的代码更小。

可以用下面的两种方法:

使用CJS的方法: require.ensure

使用ES6:动态import,这个方式需要使用Babel转换,推荐用这个。

10.1 懒加载JS脚本——动态import

ES6普通import:import xx from ‘xx’ 这种是静态导入,在代码还没有执行的时候就已经导入了。

动态import: 根据逻辑进行按需加载。

安装babel插件: npm install @babel/plugin-syntax-dynamic-import --save-dev

配置: 在.babelrc文件里

  {
    "presets": [
      "@babel/preset-env"
    ],
    "plugins": ["@babel/plugin-syntax-dynamic-import"]
  }

使用:当上面设置好之后,可以动态判断在某个事件里import某个js文件,此处为test.js, 返回的是一个promise对象,我们只需要直接.then接着操作就好了。

  someEvent(){
    import('./test.js').then( () => {
      // ....
    })
  };

原理: webpack会在编译阶段,把动态import的文件抽离出来成为一个js文件,当import的时候,会发送一个jsonp的请求,请求该文件,然后执行处理,请求回来的是一个promist对象。

Search

    Table of Contents