webpack:进阶用法(一)

一、PostCSS插件自动补齐CSS3前缀

1.1 背景

CSS3属性为什么有前缀?因为浏览器标准没有统一。
在这里插入图片描述
比如下面,写起来很慢,低效,麻烦。

.box {
  -moz-border-radius: 10px;
  -webkit-border-radius: 10px;
  -o-border-radius: 10px;
  border-radius: 10px;
}

1.2 解决

autoprefixer,css的后置处理器,这个autoprefixer和less、sass不一样,less、sass是预处理器,预处理器是打包前处理,而autoprefixer是后置处理器,在样式处理好后,代码最终生成完了之后,再进行后置处理。
根据Can I Use规则(https://caniuse.com/)
autoprefixer通常和postcss-loader一起用

npm install postcss-loader autoprefixer -D

我们在less文件中写一个display: flex; 然后看编译之后是否带了各种前缀
在这里插入图片描述
全文配置:

const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  entry: {
    index: './src/index.js',
    search: './src/search.js'
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name]_[chunkhash:8].js'
  },
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: [
          // 'style-loader',
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      },
      {
        test: /\.less$/,
        use: [
          // 'style-loader',
          MiniCssExtractPlugin.loader,
          'css-loader',
          'less-loader',
          {
            loader: 'postcss-loader',
            options: {
              plugins: () => [
                require('autoprefixer')({
                  browsers: ['last 2 version', '>1%', 'ios 7']
                })
              ]
            }
          }
        ]
      },
      {
        test: /\.(png|jpg|gif|jpeg)$/,
        use: [{
          loader: 'file-loader',
          options: {
            name: '[name]_[hash:8].[ext]'
          }
        }]
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: [{
          loader: 'file-loader',
          options: {
            name: '[name]_[hash:8].[ext]'
          }
        }]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new MiniCssExtractPlugin({
      filename: '[name]_[contenthash:8].css'
    }),
    new OptimizeCSSAssetsPlugin({
      assetNameRegExp: /\.css$/g,
      cssProcessor: require('cssnano')
    }),
    // 这里多页面,需要写两个
    new HtmlWebpackPlugin({
      template: path.join(__dirname, 'src/index.html'),
      filename: 'index.html',
      chunks: ['index'],
      inject: true,
      minify: {
        html5: true,
        collapseWhitespace: true,
        preserveLineBreaks: false,
        minifyCSS: true,
        minifyJS: true,
        removeComments: false
      }
    }),
    new HtmlWebpackPlugin({
      template: path.join(__dirname, 'src/search.html'),
      filename: 'search.html',
      chunks: ['search'],
      inject: true,
      minify: {
        html5: true,
        collapseWhitespace: true,
        preserveLineBreaks: false,
        minifyCSS: true,
        minifyJS: true,
        removeComments: false
      }
    })
  ]
}

结果如下,不负众望。
在这里插入图片描述

但是我们看到了一下警告:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二、移动端CSS px转换为rem

2.1 分辨率

在这里插入图片描述
我们可以借助媒体查询去适配,但是缺陷是要兼顾很多,需要多套适配样式代码

@media screen and (max-width: 980px) {
  .header {
    width: 900px;
  }
}
@media screen and (max-width: 480px) {
  .header {
    height: 400px;
  }
}
@media screen and (max-width: 350px) {
  .header {
    height: 300px;
  }
}

2.2 rem是什么

w3c对rem的定义: font-size of the root element
rem和px的对比:
rem: 相对单位
px:绝对单位
说一下我个人的理解吧。
px:理解为将屏幕分成的每一份的“长度”,比如,360px,就是将该设备宽度平均分成360份。我们拿到设计稿之后,设计稿只有一个数值,比如750px的设计稿,但是我们移动端的手机可能是360px,可能是414的,可能是460的,375的,等等。我们总不能都按照750设计稿上的数字去写吧。
所以,我们把750px的设计稿理解为设计师把设备平均分成750份,比如设计稿里面某个div宽度是100px,我们理解为这个div占了750份里面的100份。
那么,推广应用一下,无论我们的设备是多少分辨率的,也就是宽度不管是360的,还是414的,我们只要获取到屏幕宽度即可window.screen.width,或者还有其他方法来获取,具体看情况吧,反正肯定能拿到手机屏幕的宽度。我们令 屏幕宽度 screenWidth = window.screen.width吧。我们按照设计师的意图,将屏幕宽度分成750份。那每一份就是 screenWidth / 750 , 那么设计稿里面100px的div实际宽度就是 100 * (screenWidth/750), 其实每次这么算其实也是很不方便的,因为可能出现一些奇奇怪怪的数字,让你不能一眼看出到底是多大。
我们已经知道,每一份是 screenWidth/750,这个数值可能是个小数,是个奇奇怪怪的数字,不好计算,我们进一步想想,我们可以把多份合在一起,合成一个比较整或者比较好计算的数字,比如,我们可以把75份合在一起, 那就是 75 * (screenWidth/750) = screenWidth/10, 我们把这个数字叫做根字号,可见根字号是可以自己设计定制的,你可以根据自己的喜好来规定。

根字号 = x * (screenWidth/750)

其中,x就是x份的屏幕宽度,我们把这个x份叫做一个rem。也就是1个rem包含了多少份。因为,x的值你可以根据自己的喜好来定,所以,这个rem也是一个很灵活的值。另外,说明一点,浏览器默认的值是1rem为16份。

2.3 解决方案

使用px2rem-loader,将px转换成rem,px转换成rem之后,我们需要知道一个rem到底等于多少个px?这个时候,我们需要在页面打开的时候,动态计算根元素的font-size的值。我们可以利用手机淘宝里一个很成熟的方案,是一个库,lib-flexible。这个库能自动根据当前的设备的宽高,自动计算根元素的实际font-size值。
https://github.com/amfe/lib-flexible

npm i px2rem-loader -D
npm i lib-flexible -S

在这里插入图片描述
我们先运行一下:
在这里插入图片描述
接下来,我们要将根元素相对的rem计算出来,也就是根元素,html这个结点在实际的设备分辨率中的font-size大小。这个时候,我们需要使用lib-flexible,但是,我们现在要把这个库引进来,暂时手动将这个代码引进来吧。
在src/search.html中加入.我们在node_modules里找到 lib-flexible这个库,将里面的源代码复制粘贴到search.html的

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <script type="text/javascript">
    ;(function(win, lib) {
        var doc = win.document;
        var docEl = doc.documentElement;
        var metaEl = doc.querySelector('meta[name="viewport"]');
        var flexibleEl = doc.querySelector('meta[name="flexible"]');
        var dpr = 0;
        var scale = 0;
        var tid;
        var flexible = lib.flexible || (lib.flexible = {});

        if (metaEl) {
            console.warn('将根据已有的meta标签来设置缩放比例');
            var match = metaEl.getAttribute('content').match(/initial\-scale=([\d\.]+)/);
            if (match) {
                scale = parseFloat(match[1]);
                dpr = parseInt(1 / scale);
            }
        } else if (flexibleEl) {
            var content = flexibleEl.getAttribute('content');
            if (content) {
                var initialDpr = content.match(/initial\-dpr=([\d\.]+)/);
                var maximumDpr = content.match(/maximum\-dpr=([\d\.]+)/);
                if (initialDpr) {
                    dpr = parseFloat(initialDpr[1]);
                    scale = parseFloat((1 / dpr).toFixed(2));
                }
                if (maximumDpr) {
                    dpr = parseFloat(maximumDpr[1]);
                    scale = parseFloat((1 / dpr).toFixed(2));
                }
            }
        }

        if (!dpr && !scale) {
            var isAndroid = win.navigator.appVersion.match(/android/gi);
            var isIPhone = win.navigator.appVersion.match(/iphone/gi);
            var devicePixelRatio = win.devicePixelRatio;
            if (isIPhone) {
                // iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
                if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
                    dpr = 3;
                } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
                    dpr = 2;
                } else {
                    dpr = 1;
                }
            } else {
                // 其他设备下,仍旧使用1倍的方案
                dpr = 1;
            }
            scale = 1 / dpr;
        }
        docEl.setAttribute('data-dpr', dpr);
        if (!metaEl) {
            metaEl = doc.createElement('meta');
            metaEl.setAttribute('name', 'viewport');
            metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
            if (docEl.firstElementChild) {
                docEl.firstElementChild.appendChild(metaEl);
            } else {
                var wrap = doc.createElement('div');
                wrap.appendChild(metaEl);
                doc.write(wrap.innerHTML);
            }
        }
        function refreshRem(){
            var width = docEl.getBoundingClientRect().width;
            if (width / dpr > 540) {
                width = 540 * dpr;
            }
            var rem = width / 10;
            docEl.style.fontSize = rem + 'px';
            flexible.rem = win.rem = rem;
        }
        win.addEventListener('resize', function() {
            clearTimeout(tid);
            tid = setTimeout(refreshRem, 300);
        }, false);
        win.addEventListener('pageshow', function(e) {
            if (e.persisted) {
                clearTimeout(tid);
                tid = setTimeout(refreshRem, 300);
            }
        }, false);

        if (doc.readyState === 'complete') {
            doc.body.style.fontSize = 12 * dpr + 'px';
        } else {
            doc.addEventListener('DOMContentLoaded', function(e) {
                doc.body.style.fontSize = 12 * dpr + 'px';
            }, false);
        }
        refreshRem();
        flexible.dpr = win.dpr = dpr;
        flexible.refreshRem = refreshRem;
        flexible.rem2px = function(d) {
            var val = parseFloat(d) * this.rem;
            if (typeof d === 'string' && d.match(/rem$/)) {
                val += 'px';
            }
            return val;
        }
        flexible.px2rem = function(d) {
            var val = parseFloat(d) / this.rem;
            if (typeof d === 'string' && d.match(/px$/)) {
                val += 'rem';
            }
            return val;
        }
    })(window, window['lib'] || (window['lib'] = {}));
  </script>
</head>
<body>
  <div id="root"></div>
</body>
</html>

npm run build之后,打开 dist/search.html文件
在这里插入图片描述

在这里插入图片描述
看到了吗,字体也会随着屏幕分辨率的变化而变化

2.4 答疑

2.4.1 这个最大的缺点就是会把第三方ui库的px也给转了
答: 这个px2rem-loader 也是可以设置 exclude 的,可以把 node_modules 里面的模块 exclude 掉。
另外如果不设置 exclude,那么也可以使用 /no/的语法去设置某一行样式不进行 px2rem 的转换操作。
可以用 /no/ 这种注释语法。比如:

.page {
  font-size: 12px; /*no*/
  width: 375px; /*no*/
  height: 40px;
}

后面有 /no/这种注释语法会不进行 rem 的转换
2.4.2 按照750的设计稿,直接就是10px写10px吗?为什么看着比设计稿的要大?
答: 如果设置的 remUnit 是 75,那么对于 750 的设计稿如果字体是 24px,就写 24px(实际上在 iphone 6是12px的大小)
如果设置的 remUnit 是 37.5,那么对于 375的设计稿如果字体是 12px,就写 12px(实际上在 iphone 6是12px的大小)
看着比设计稿的要大这个需要以 iphone 6为参照哈
2.4.3 flexible.js作用是什么?使用px2rem-loader,就必须和flexible.js一起搭配使用吗?
答:必须一起使用。
px2rem-loader 只是以构建的手段将 px 单位转换成了 rem。但是 rem 和 px 的单位计算并不清楚,flexible.js 的作用就是动态的去计算不同设备下的 rem 相对 px 的单位,也就是计算跟元素 html 节点的 font-size 大小

三、静态资源内联

上面我们已经发现,为了引入一个lib-flexible,我们还要将代码复制粘贴到HTML文件中,这显然是不符合现代开发逻辑的,不够自动化。
内联,比如我们怎么将css 代码、js代码内联到HTML中,怎么将图片、字体内联到代码中等

3.1 内联的意义

  1. 页面框架的初始化脚本,比如上一节里面lib-flexible,项目初始化就要执行的脚本。
  2. 上报相关打点,css初始化和css加载完成这些上报点的代码都要内联到HTML中。
  3. css内联避免页面闪动。
  4. 减少http网络请求数,小图片或者字体内联url-loader

3.2 raw-loader内联html

比如要把一段html片段内联进去,这个html片段是移动端开发要用的meta信息片段,很大一段,很多页面都要用到。

<script>${require('raw-loader!babel-loader!./xx.js')}</script>

我们准备了一个meta.html文件,用于内联到其他html文件中,尤其是做一些手机端网页,都需要一些meta信息。

<meta charset="UTF-8">
<meta name="viewport" content="viewport-fit=cover,width=device-width,initial-scale=1,user-scalable=no">
<meta name="format-detection" content="telephone=no">
<meta name="keywords" content="now,now直播,直播,腾讯直播,QQ直播,美女直播,附近直播,才艺直播,小视频,个人直播,美女视频,在线直播,手机直播">
<meta name="name" itemprop="name" content="NOW直播—腾讯旗下全民视频社交直播平台"><meta name="description" itemprop="description" content="NOW直播,腾讯旗下全民高清视频直播平台,汇集中外大咖,最in网红,草根偶像,明星艺人,校花,小鲜肉,逗逼段子手,各类美食、音乐、旅游、时尚、健身达人与你24小时不间断互动直播,各种奇葩刺激的直播玩法,让你跃跃欲试,你会发现,原来人人都可以当主播赚钱!">
<meta name="image" itemprop="image" content="https://pub.idqqimg.com/pc/misc/files/20170831/60b60446e34b40b98fa26afcc62a5f74.jpg"><meta name="baidu-site-verification" content="G4ovcyX25V">
<meta name="apple-mobile-web-app-capable" content="no">
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
<link rel="dns-prefetch" href="//11.url.cn/">
<link rel="dns-prefetch" href="//open.mobile.qq.com/">
npm install raw-loader@0.5.1 -D

我们在src/search.html模板修改如下,不再使用

<!DOCTYPE html>
<html lang="en">
<head>
  ${ require('raw-loader!./meta.html') }
  <title>Document</title>
  <script>${require('raw-loader!babel-loader!../../node_modules/lib-flexible/lib-flexible.js')}</script>
</head>
<body>
  <div id="root"></div>
</body>
</html>

四、多页面打包通用方案

4.1 什么是多页面MPA

发布上线后有很多个入口,一个页面就是一个业务,单页面应用就是只有一个入口,所有的业务都放在一个入口,不同的业务用同一个url,只是后面hash不一样。多页应用好处就是多个页面解耦,而且对SEO友好。
每个页面对应一个entry,一个html-webpack-plugin,缺点是每次新增或者删除页面需要改webpack配置。

4.2 解决方案

动态获取entry和设置html-webpack-plugin数量
利用glob.sync

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

传统的是:

module.exports = {
    entry: {
    index: './src/index/index.js',
    search: './src/search/index.js'
  }
}

当然这样做,我们需要一些约定

  • 都放在src下
  • 不同的目录的出口文件都命名为index.js

就是利用glob库

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

__dirname就是项目根目录,根目录下的src下匹配src下所有一级目录,然后继续匹配到下面的index.js,这样所有的entry都动态的获取出来了,我们根据这个数量动态的设置webpack中 html-webpack-plugin的数量。

4.3 操作举例

4.3.1 修改目录和文件名

新建两个目录:index 和 search,把属于他们的文件都move到相应的目录中,同时,入口文件都修改为index.js.
在这里插入图片描述

4.3.2 安装 glob

npm install glob -D

4.3.3 在webpack中改写

const glob = require('glob')

// 动态设置多页面的函数
const setMPA = () => {
  const myEntry = {}
  const myHtmlWebpackPlugin = []
  const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'))
  // entryFiles =  [
  //   '/Users/guoyu/work/webpack-demo/src/index/index.js',
  //   '/Users/guoyu/work/webpack-demo/src/search/index.js'
  // ]
  Object.keys(entryFiles).forEach(index => {
    const entryFile = entryFiles[index]
    const match = entryFile.match(/src\/(.*)\/index\.js/)
    // index ===  0
    // match ==  [
    //   'src/index/index.js',
    //   'index',
    //   index: 31,
    //   input: '/Users/guoyu/work/webpack-demo/src/index/index.js',
    //   groups: undefined
    // ]
    // index ===  1
    // match ==  [
    //   'src/search/index.js',
    //   'search',
    //   index: 31,
    //   input: '/Users/guoyu/work/webpack-demo/src/search/index.js',
    //   groups: undefined
    // ]
    const pageName = match && match[1]
    myEntry[pageName] = entryFile
    myHtmlWebpackPlugin.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: myEntry,
    htmlWebpackPlugin: myHtmlWebpackPlugin
  }
}

// 直接调用
const { entry, htmlWebpackPlugin } = setMPA()

module.exports = {
  entry: entry,
  plugins: [
    new CleanWebpackPlugin(),
    new MiniCssExtractPlugin({
      filename: '[name]_[contenthash:8].css'
    }),
    new OptimizeCSSAssetsPlugin({
      assetNameRegExp: /\.css$/g,
      cssProcessor: require('cssnano')
    })
  ].concat(htmlWebpackPlugin) // 填充到webpack的plugins中
}

4.3.4 运行结果

在这里插入图片描述
在这里插入图片描述

五、使用source map

5.1 简介

作用:通过source map定位到源代码,可以看看阮一峰老师的科普文。
开发环境开启,线上环境关闭,因为如果线上环境开启会暴露业务逻辑,是不安全的。
线上排查的时候,可以把sourcemap上传到错误监控系统。
eval: 使用eval包裹模块代码
sourcemap: 产生.map文件
cheap:不包括列信息
inline:将.map做为DataURL嵌入,不单独产生.map文件
module:包含loader的source map
source map 类型如下,其实就是上面五种的各种组合。
在这里插入图片描述

5.2 操作实践

先把webpack中的mode设置为none,因为如果是production,会压缩,不利于观察

  mode: 'none'

再在webpack中添加一项

devtool: 'eval'

npm run build之后观察打包出来的文件
在这里插入图片描述
在这里插入图片描述
等等,有很多模块,很多个eval
我们可以看到每个eval后面有个sourceURL,指明了编译的是哪个模块
eval 换成 source-map

devtool: 'source-map'

在这里插入图片描述
换成 inline-map

devtool: 'inline-source-map'

在这里插入图片描述
看看构建结果,没有了.map文件,而且构建出来的js文件也比source-map大了很多,因为inline-source-map把map文件打到js文件中了,最后一行把map文件inline进来了,最后一行很长很长
在这里插入图片描述
在这里插入图片描述
下面我们把动态设置多页面的代码添加到webpack.dev.js中,但是暂时先不设置devtool, 我们在search/index.js中加个断点
在这里插入图片描述
npm run dev :http://localhost:8080/search.html
在这里插入图片描述
这种很难调试
现在我们在webpack.dev.js中添加一句:devtool: source-map
在这里插入图片描述
npm run dev, http://localhost:8080/search.html,这种就非常好调试了,直接调试源代码。
在这里插入图片描述
我们把 source-map 换为 cheap-source-map,只能看到错误的行数

devtool: 'cheap-source-map'

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

六、提取页面公共资源

项目中有很多页面,这些页面使用的基础库一般都是一样的,有些页面可能还有一些公共的模块,打包的时候,如果把每个公共模块在每个页面都打一份,就很浪费,体积很大。

6.1 基础库分离splitChunksPlugin

splitChunksPlugin这个库是webpack4.x内置的,用于替换淘汰掉之前的CommonChunkPlugin
chunks参数说明:
1,async:异步引入的库才进行分离(动态import一个react库等等),同步引入的忽略(默认)
2,initial:同步引入的库进行分离,同步引入的库,如果有相同的,就进行分离,抽离出一个chunk出来
3,all:所有引入的库进行分离(推荐)
minChunks说明:
minChunks: 2 , 是指至少有2个地方用到了同一段代码,这一段代码就要被打包分离出一个chunk。专业点说就是设置最小引用次数。
maxInitalRequest说明:
maxInitalRequest: 3, 是指你通过插件分离出了4或者5个chunk,浏览器同时去请求的一个数量
minSize:分离的包提交大小。
下面通过实例,将react和react-dom公共资源包引进来,打包的时候,不用分析react和react-dom这种基础库。

6.2 操作实践

先看看html-webpack-externals-plugin的用法,配合cdn

npm i html-webpack-externals-plugin -D

webpack.prod.js添加代码:

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

// plugins数组添加一项
new HtmlWebpackExternalsPlugin({
  externals: [{
    module: 'react',
    entry: 'https://now8.gtimg.com/now/lib/16.2.0/react.min.js',
    global: 'React'
  }, {
    module: 'react-dom',
    entry: 'https://now8.gtimg.com/now/lib/16.2.0/react-dom.min.js',
    global: 'ReactDOM'
  }]
})

在这里插入图片描述

附全部webpack.prod.js代码

const path = require('path')
const glob = require('glob')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin')

// 动态设置多页面的函数
const setMPA = () => {
  const myEntry = {}
  const myHtmlWebpackPlugin = []
  const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'))
  // entryFiles =  [
  //   '/Users/guoyu/work/webpack-demo/src/index/index.js',
  //   '/Users/guoyu/work/webpack-demo/src/search/index.js'
  // ]
  Object.keys(entryFiles).forEach(index => {
    const entryFile = entryFiles[index]
    const match = entryFile.match(/src\/(.*)\/index\.js/)
    const pageName = match && match[1]
    myEntry[pageName] = entryFile
    myHtmlWebpackPlugin.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: myEntry,
    htmlWebpackPlugin: myHtmlWebpackPlugin
  }
}
const { entry, htmlWebpackPlugin } = setMPA()

module.exports = {
  entry: entry,
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name]_[chunkhash:8].js'
  },
  mode: 'none',
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: [
          // 'style-loader',
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      },
      {
        test: /\.less$/,
        use: [
          // 'style-loader',
          MiniCssExtractPlugin.loader,
          'css-loader',
          'less-loader',
          {
            loader: 'postcss-loader',
            options: {
              plugins: () => [
                require('autoprefixer')
              ]
            }
          },
          {
            loader: 'px2rem-loader',
            options: {
              // 这里设置1个rem为75px,可以自己根据设备设置
              remUnit: 75,
              // 转换成rem后,保留8位小数
              remPrecision: 8
            }
          }
        ]
      },
      {
        test: /\.(png|jpg|gif|jpeg)$/,
        use: [{
          loader: 'file-loader',
          options: {
            name: '[name]_[hash:8].[ext]'
          }
        }]
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: [{
          loader: 'file-loader',
          options: {
            name: '[name]_[hash:8].[ext]'
          }
        }]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new MiniCssExtractPlugin({
      filename: '[name]_[contenthash:8].css'
    }),
    new OptimizeCSSAssetsPlugin({
      assetNameRegExp: /\.css$/g,
      cssProcessor: require('cssnano')
    }),
    new HtmlWebpackExternalsPlugin({
      externals: [{
        module: 'react',
        entry: 'https://now8.gtimg.com/now/lib/16.2.0/react.min.js',
        global: 'React'
      }, {
        module: 'react-dom',
        entry: 'https://now8.gtimg.com/now/lib/16.2.0/react-dom.min.js',
        global: 'ReactDOM'
      }],
      files: ['search.html']
    })
  ].concat(htmlWebpackPlugin),
  devtool: 'inline-source-map'
}

下面,我们看看如何用splitChunksPlugin,这是webpack4.x内置的,可以直接配置。先把上面的HtmlWebpackExternalsPlugin代码去掉,使用splitChunksPlugin来把react,react-dom分离成一个vendor。
下面是完整配置,只用关注第24行和122-128行

const path = require('path')
const glob = require('glob')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

// 动态设置多页面的函数
const setMPA = () => {
  const myEntry = {}
  const myHtmlWebpackPlugin = []
  const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'))

  Object.keys(entryFiles).forEach(index => {
    const entryFile = entryFiles[index]
    const match = entryFile.match(/src\/(.*)\/index\.js/)

    const pageName = match && match[1]
    myEntry[pageName] = entryFile
    myHtmlWebpackPlugin.push(
      new HtmlWebpackPlugin({
        template: path.join(__dirname, `src/${pageName}/index.html`),
        filename: `${pageName}.html`,
        chunks: ['gyVendors', pageName],//将splitChunks提取的vendors(gyVendors)加上
        inject: true,
        minify: {
          html5: true,
          collapseWhitespace: true,
          preserveLineBreaks: false,
          minifyCSS: true,
          minifyJS: true,
          removeComments: false
        }
      })
    )
  })
  return {
    entry: myEntry,
    htmlWebpackPlugin: myHtmlWebpackPlugin
  }
}
const { entry, htmlWebpackPlugin } = setMPA()

module.exports = {
  entry: entry,
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name]_[chunkhash:8].js'
  },
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: [
          // 'style-loader',
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      },
      {
        test: /\.less$/,
        use: [
          // 'style-loader',
          MiniCssExtractPlugin.loader,
          'css-loader',
          'less-loader',
          {
            loader: 'postcss-loader',
            options: {
              plugins: () => [
                require('autoprefixer')
              ]
            }
          },
          {
            loader: 'px2rem-loader',
            options: {
              // 这里设置1个rem为75px,可以自己根据设备设置
              remUnit: 75,
              // 转换成rem后,保留8位小数
              remPrecision: 8
            }
          }
        ]
      },
      {
        test: /\.(png|jpg|gif|jpeg)$/,
        use: [{
          loader: 'file-loader',
          options: {
            name: '[name]_[hash:8].[ext]'
          }
        }]
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: [{
          loader: 'file-loader',
          options: {
            name: '[name]_[hash:8].[ext]'
          }
        }]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new MiniCssExtractPlugin({
      filename: '[name]_[contenthash:8].css'
    }),
    new OptimizeCSSAssetsPlugin({
      assetNameRegExp: /\.css$/g,
      cssProcessor: require('cssnano')
    })
  ].concat(htmlWebpackPlugin),
  // 配置splitChunks分离公共包,这是webpack4.x内置的一个插件
  optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          test: /(rect|react-dom)/,
          name: 'gyVendors', // 记得在HtmlWebpackPlugin中把gyVendors加上
          chunks: 'all'
        }
      }
    }
  }
}

在这里插入图片描述
在这里插入图片描述
下面我们进一步探索 splitChunks的用法,我们手写一个公共函数,让search和index两个业务页面都用上这个公共函数commom/index.js
在这里插入图片描述
webpack.prod.js配置如下:

optimization: {
  splitChunks: {
    minSize: 0,//只要是公共包,就抽离出来
    cacheGroups: {
      commons: {
        name: 'commons', // 记得在HtmlWebpackPlugin中把commons加上
        chunks: 'all',
        minChunks: 2 // 公共包至少被引用了两次才会被抽离出来
      }
    }
  }
}

全配如下:

const path = require('path')
const glob = require('glob')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

// 动态设置多页面的函数
const setMPA = () => {
  const myEntry = {}
  const myHtmlWebpackPlugin = []
  const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'))

  Object.keys(entryFiles).forEach(index => {
    const entryFile = entryFiles[index]
    const match = entryFile.match(/src\/(.*)\/index\.js/)

    const pageName = match && match[1]
    myEntry[pageName] = entryFile
    myHtmlWebpackPlugin.push(
      new HtmlWebpackPlugin({
        template: path.join(__dirname, `src/${pageName}/index.html`),
        filename: `${pageName}.html`,
        chunks: ['commons', pageName], // 将splitChunks提取的commons加上
        inject: true,
        minify: {
          html5: true,
          collapseWhitespace: true,
          preserveLineBreaks: false,
          minifyCSS: true,
          minifyJS: true,
          removeComments: false
        }
      })
    )
  })
  return {
    entry: myEntry,
    htmlWebpackPlugin: myHtmlWebpackPlugin
  }
}
const { entry, htmlWebpackPlugin } = setMPA()

module.exports = {
  entry: entry,
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name]_[chunkhash:8].js'
  },
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: [
          // 'style-loader',
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      },
      {
        test: /\.less$/,
        use: [
          // 'style-loader',
          MiniCssExtractPlugin.loader,
          'css-loader',
          'less-loader',
          {
            loader: 'postcss-loader',
            options: {
              plugins: () => [
                require('autoprefixer')
              ]
            }
          },
          {
            loader: 'px2rem-loader',
            options: {
              // 这里设置1个rem为75px,可以自己根据设备设置
              remUnit: 75,
              // 转换成rem后,保留8位小数
              remPrecision: 8
            }
          }
        ]
      },
      {
        test: /\.(png|jpg|gif|jpeg)$/,
        use: [{
          loader: 'file-loader',
          options: {
            name: '[name]_[hash:8].[ext]'
          }
        }]
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: [{
          loader: 'file-loader',
          options: {
            name: '[name]_[hash:8].[ext]'
          }
        }]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new MiniCssExtractPlugin({
      filename: '[name]_[contenthash:8].css'
    }),
    new OptimizeCSSAssetsPlugin({
      assetNameRegExp: /\.css$/g,
      cssProcessor: require('cssnano')
    })
  ].concat(htmlWebpackPlugin),
  // 配置splitChunks分离公共包,这是webpack4.x内置的一个插件
  optimization: {
    splitChunks: {
      minSize: 0,
      cacheGroups: {
        commons: {
          // test: /(rect|react-dom)/,
          name: 'commons', // 记得在HtmlWebpackPlugin中把commons加上
          chunks: 'all',
          minChunks: 2
        }
      }
    }
  }
}

npm run build之后
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上面看到commons_d5edc0d8.js 180 bytes,而且是search和index两个地方用到:那么我们把

minSize: 0换成 minSize: 10000 再打包,你会发现commons_xxxxxxxx.js不见了,因为不满足min配置

在这里插入图片描述
在这里插入图片描述
同样

minChunks: 2换成 minChunks: 3 再打包,你会发现commons_xxxxxxxx.js不见了,因为不满足min配置

6.3 结合webpack-bundle-analyzer分析

npm install webpack-bundle-analyzer -D
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
// plugins里添加如下的插件
new BundleAnalyzerPlugin()

package.json添加如下:

  "scripts": {
    "build": "webpack --config webpack.prod.js",
    "watch": "webpack --watch",
    "dev": "webpack-dev-server --config webpack.dev.js --open",
    // 下面一句才是重点
    "analyz": "analyz=true npm run build"
  }

直接看代码
在这里插入图片描述

optimization: {
  splitChunks: {
    minSize: 0,
    cacheGroups: {
      vendors: {
        test: /(react|react-dom)/,
        name: 'vendors',
        chunks: 'all',
        priority: -10
      },
      commons: {
        name: 'commons',
        chunks: 'all',
        minChunks: 2,
        priority: -20
      },
      ruok: {
        test: /[\\/]node_modules[\\/]/,
        name: 'nModules',
        chunks: 'all',
        priority: -30
      },
      default: {
        name: 'guoyu-default',
        chunks: 'all',
        minChunks: 2,
        priority: -40
      }
    }
  }
}

我们看看上面的webpack配置代码,我们源码中的search/index.js和index/index.js都引用了common/index.js。
这种情况既满足上面webpack配置中的commons也满足default。
在这里插入图片描述
那么既然common/index.js既满足commons又满足default,那么common/index.js到底会被打包到哪个chunk呢?答案就是 priority,priority谁的大,就打包到谁里面。上面,commons的priority大。我们看看结果。
在这里插入图片描述
我们把default的priority改为-10,大于commons的-20,再次分析
在这里插入图片描述

6.4 问答

Q1:如果我既想提取静态资源react,又想提取公共资源还想打到不同的文件里怎么办,支持数组方式吗?
A1:可以的,这个cacheGroup 可以配置多个组的。想打包到不同的文件只需要传到 html-webpack-plugin的 chunk 按照需要设置即可

Q2:同时分离基础库和公共脚本,具体要怎么配呢?期望提取出的common公共脚本 + vendor基础库,都可以自定义名称。但是貌似不行。。打包出来的vendor,总是vendors~xxx_hash.js这样的。而且设置两次name,第二次的会覆盖掉第一次的,配置如下:

cacheGroups: {
  commons: {
    name: 'common',
    chunks: 'all',
    minChunks: 2
  },
  vendors: {
    test: /(react|react-dom)/,
    name: 'vendor',
    chunks: 'all'
  }
}
A2:正确配置如下:
optimization: {
  splitChunks: {
    minSize: 0,
      cacheGroups: {
        vendors: {
          test: /(react|react-dom)/,
          name: 'vendors',
          chunks: 'all',
          priority: -10
        },
        commons: {
          name: 'commons',
          chunks: 'all',
          minChunks: 2,
          priority: -20
        }
     }
  }
}

Q3:priority 是什么意思?
A3:优先级权重,使用一个数字表示,数字越大,表示优先级越高,通俗的说,就是如果有一个模块满足了多个缓存组的条件就会去按照权重划分,谁的权重高就优先按照谁的规则处理。比如,一个模块既满足上面A2中vendors的分组要求,也满足commons的分组要求,谁的权重大,就打包到谁的分组。

七、摇树优化TreeShaking

7.1 概念

一个模块可能有多个方法,只要其中的某个方法使用到了,则整个文件都会被打到bundle里,tree shaking就是只把用到的方法打入bundle,没用到的方法在uglify阶段被擦除掉。
tree shaking在webpack4.x默认支持,mode设置为production的情况默认开启
但是,使用treeshaking 必须是ES6语法,cjs语法不支持。
DEC(Dead Code Elimination)
DEC:擦除淘汰无用代码

1,代码不会被执行,不可到达

if (false) {
  console.log('......这段代码永远不会被执行......')
}

2,代码执行的结果不会被用到
3,代码只会影响死变量(只写不读)

7.2 treeShaking的原理

主要是利用了ES6模块的特点:

  • 只能作为模块顶层的语句出现(import、export语句都在文件顶层出现)
  • import 的模块只能是字符串常量
  • import binding 是immutable的(import一个模块后,不能被修改)
    代码擦除:uglify阶段删除无用代码
    所以,commonJS是不具备上面三个特点的,因为可以动态require,不同条件require不同的模块,这都是cjs写法。为什么?treeshaking最本质的还是对模块代码的静态分析,在编译阶段,哪些代码会被用到都是要确定下来的,不是说在代码运行的时候再去分析哪些代码有没有用到,不能这么操作。哪些代码没用到,treeshaking会将这些代码标识出来,uglify阶段删除无用代码。

7.3 treeShaking实际操作

1,我们在search文件夹下,创建一个search/tree-shaking.js文件内容如下:

export function a() {
  return 'this is func a'
}

export function b() {
  return 'this is func b'
}
2,在search/index.js中只引入a方法
import { a } from './tree-shaking'

3,webpack.prod.js将mode换成’none’,因为之前的’production’默认开启了treeShaking

mode: 'none'

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
4,执行npm run build
在这里插入图片描述
从上图可以看到,打包出来的,没有开启treeShaking,这两个代码都打包进去了,尽管我们都没用到。

5,下面,我们none换为 production
在这里插入图片描述
可见,根本没打进去,无用代码被TreeShaking掉了。
那我们实际用一下a,在search/index.js中做如下改动
在这里插入图片描述
编译打包之后
在这里插入图片描述
在这里插入图片描述
用的着的留下了,用不着的摇走了。
再看一个例子
在这里插入图片描述

npm run build

在这里插入图片描述
都没被打进包里。

八、Scope Hoisting使用和原理分离

8.1 背景

构建后的代码存在大量的闭包代码
比如,新建两个common/a.js和common/b.js两个文件。在src/index/index.js中引用
a.js和b.js分别是

export function testA() { 
  console.log('this is testA..........')
}
export function testB() { 
  console.log('this is testB..........')
}

把webpack.dev.js中的mode换为none,不然是压缩混淆代码,看不清

 mode: 'none'

在这里插入图片描述
在这里插入图片描述
一个项目,可能有几百上千个包裹。
1,大量函数闭包包裹代码,导致体积增大,模块越多越明显。
2,都是通过闭包方式,运行代码时候,创建的函数作用域变多,内存开销变大。
下面我们看看打包模块

/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "testA", function() { return testA; });
function testA() {
  console.log('this is testA..........');
}

/***/ }),
/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "testB", function() { return testB; });
function testB() {
  console.log('this is testB..........');
}

/***/ })

模块初始化函数:就是webpack对模块进行处理,增加的包裹
我们可以看到

  • 被import进来的,被webpack转换后的模块都会带上一层包裹
  • import会被转换为 webpack_require

进一步分析一下:

/******/ (function(modules) { // webpackBootstrap
/******/    // The module cache
/******/    var installedModules = {};
/******/
/******/    // The require function
/******/    function __webpack_require__(moduleId) {
/******/
/******/        // Check if module is in cache
/******/        if(installedModules[moduleId]) {
/******/            return installedModules[moduleId].exports;
/******/        }
/******/        // Create a new module (and put it into the cache)
/******/        var module = installedModules[moduleId] = {
/******/            i: moduleId,
/******/            l: false,
/******/            exports: {}
/******/        };
/******/
/******/        // Execute the module function
/******/        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/        // Flag the module as loaded
/******/        module.l = true;
/******/
/******/        // Return the exports of the module
/******/        return module.exports;
/******/    }
/******/
/******/
/******/    // expose the modules object (__webpack_modules__)
/******/    __webpack_require__.m = modules;
/******/
/******/    // expose the module cache
/******/    __webpack_require__.c = installedModules;
/******/
/******/    // define getter function for harmony exports
/******/    __webpack_require__.d = function(exports, name, getter) {
/******/        if(!__webpack_require__.o(exports, name)) {
/******/            Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/        }
/******/    };
/******/
/******/    // define __esModule on exports
/******/    __webpack_require__.r = function(exports) {
/******/        if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/            Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/        }
/******/        Object.defineProperty(exports, '__esModule', { value: true });
/******/    };
/******/
/******/    // create a fake namespace object
/******/    // mode & 1: value is a module id, require it
/******/    // mode & 2: merge all properties of value into the ns
/******/    // mode & 4: return value when already ns object
/******/    // mode & 8|1: behave like require
/******/    __webpack_require__.t = function(value, mode) {
/******/        if(mode & 1) value = __webpack_require__(value);
/******/        if(mode & 8) return value;
/******/        if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/        var ns = Object.create(null);
/******/        __webpack_require__.r(ns);
/******/        Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/        if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/        return ns;
/******/    };
/******/
/******/    // getDefaultExport function for compatibility with non-harmony modules
/******/    __webpack_require__.n = function(module) {
/******/        var getter = module && module.__esModule ?
/******/            function getDefault() { return module['default']; } :
/******/            function getModuleExports() { return module; };
/******/        __webpack_require__.d(getter, 'a', getter);
/******/        return getter;
/******/    };
/******/
/******/    // Object.prototype.hasOwnProperty.call
/******/    __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/    // __webpack_public_path__
/******/    __webpack_require__.p = "";
/******/
/******/
/******/    // Load entry module and return exports
/******/    return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _common_a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
/* harmony import */ var _common_b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2);
// import _ from 'lodash'
// import { commonTest } from '../../common'
// function component() {
//   commonTest('Index')
//   let ele = document.createElement('div')
//   ele.innerHTML = _.join(['Hello', 'guoyu'], ' ')
//   return ele
// }
// document.body.appendChild(component())


Object(_common_a__WEBPACK_IMPORTED_MODULE_0__["testA"])();
Object(_common_b__WEBPACK_IMPORTED_MODULE_1__["testB"])();

/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "testA", function() { return testA; });
function testA() {
  console.log('this is testA..........');
}

/***/ }),
/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "testB", function() { return testB; });
function testB() {
  console.log('this is testB..........');
}

/***/ })
/******/ ]);

从上面打包出来的文件可以看出:

  • 打包出来的是个匿名闭包
  • modules是一个数组,每一项都是一个模块初始化函数
  • __webpack_require__用来加载模块,返回module.exports
  • 通过webpack_require_method(0)启动程序

分析一个函数:

function __webpack_require__(moduleId) {
    // Check if module is in cache
    if(installedModules[moduleId]) {
        return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    var module = installedModules[moduleId] = {
        i: moduleId,
        l: false,
        exports: {}
    };

    // Execute the module function
  // require另一个模块(依赖),会传递如下参数
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // Flag the module as loaded
    module.l = true;

    // Return the exports of the module
    return module.exports;
}

8.2 原理

在上面了解了webpack模块机制之后,我们看看有么有办法对前面提到的包裹代码有所优化?
scope hoisting和treeShaking一样,都是从rollup中借鉴过来的。
将所有模块的代码按照引用顺序放在一个函数作用域里面,然后适当的重命名一些变量,以防止变量名冲突。
通过scop hoisting 可以减少函数声明代码和内存开销
scope hoisting 是webpack3提出的,翻译过来就是“作用域提升”,webpack会把js文件提升到引入者的顶部。
在之前,如果a模块引用了b模块,那么我们可以把a模块放在b模块前面,a调用b,但他们之间有模块包裹,所以并不影响,如果要消除这种包裹代码的话,我们根据模块的引用顺序,进行位置排放,也就是我们要把被引用的b模块放在a模块前面,那么a就可以直接读取b的内容,这样就消除了包裹,减少了内存开销和函数申明代码。

在webpack4.x中,scope hoisting 在mode为production时默认开启。
但是在webpack3.x中还是要手动配置的。下面是webpack3.x中的配置:

// webpack3.x
plugins: [
    new webpack.optimize.ModuleConcatenationPlugin() // 开启scope hoisting
]

在webpack4.x中,scope hoisting 在mode为production时默认开启。
必须是ES6语法,cjs不支持,因为scope hoisting是一个静态分析过程,cjs是一个动态引入模块的方式,没办法进行静态分析,比如分析模块的引入顺序。

8.3 使用

首先,禁止掉production,把mode设置为none,看看打包结果。
npm run build 看看。我们回到 8.1 贴出来的打包后的代码,我们没有开启scope hoisting的时候,可以看到如果模块多,包裹数量也会越来越多。坏处显而易见。
接下来,我们使用scope hoisting功能,看看打包后的效果。但是,我们不能草率的把mode设置为production,因为这样会uglify代码,都是压缩混淆的,看的不直观。
所以,我们依旧保持 mode 为 ‘none’,但是按照webpack3.x的方式,手动在plugins中加入插件。

const webpack = require('webpack')

mode: 'none',

plugins: [
    new webpack.optimize.ModuleConcatenationPlugin() // 开启scope hoisting
]

打包之后的代码

/******/ (function(modules) { // webpackBootstrap
/******/    // The module cache
/******/    var installedModules = {};
/******/
/******/    // The require function
/******/    function __webpack_require__(moduleId) {
/******/
/******/        // Check if module is in cache
/******/        if(installedModules[moduleId]) {
/******/            return installedModules[moduleId].exports;
/******/        }
/******/        // Create a new module (and put it into the cache)
/******/        var module = installedModules[moduleId] = {
/******/            i: moduleId,
/******/            l: false,
/******/            exports: {}
/******/        };
/******/
/******/        // Execute the module function
/******/        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/        // Flag the module as loaded
/******/        module.l = true;
/******/
/******/        // Return the exports of the module
/******/        return module.exports;
/******/    }
/******/
/******/
/******/    // expose the modules object (__webpack_modules__)
/******/    __webpack_require__.m = modules;
/******/
/******/    // expose the module cache
/******/    __webpack_require__.c = installedModules;
/******/
/******/    // define getter function for harmony exports
/******/    __webpack_require__.d = function(exports, name, getter) {
/******/        if(!__webpack_require__.o(exports, name)) {
/******/            Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/        }
/******/    };
/******/
/******/    // define __esModule on exports
/******/    __webpack_require__.r = function(exports) {
/******/        if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/            Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/        }
/******/        Object.defineProperty(exports, '__esModule', { value: true });
/******/    };
/******/
/******/    // create a fake namespace object
/******/    // mode & 1: value is a module id, require it
/******/    // mode & 2: merge all properties of value into the ns
/******/    // mode & 4: return value when already ns object
/******/    // mode & 8|1: behave like require
/******/    __webpack_require__.t = function(value, mode) {
/******/        if(mode & 1) value = __webpack_require__(value);
/******/        if(mode & 8) return value;
/******/        if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/        var ns = Object.create(null);
/******/        __webpack_require__.r(ns);
/******/        Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/        if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/        return ns;
/******/    };
/******/
/******/    // getDefaultExport function for compatibility with non-harmony modules
/******/    __webpack_require__.n = function(module) {
/******/        var getter = module && module.__esModule ?
/******/            function getDefault() { return module['default']; } :
/******/            function getModuleExports() { return module; };
/******/        __webpack_require__.d(getter, 'a', getter);
/******/        return getter;
/******/    };
/******/
/******/    // Object.prototype.hasOwnProperty.call
/******/    __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/    // __webpack_public_path__
/******/    __webpack_require__.p = "";
/******/
/******/
/******/    // Load entry module and return exports
/******/    return __webpack_require__(__webpack_require__.s = 12);
/******/ })
/************************************************************************/
/******/ ({

/***/ 12:
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
// ESM COMPAT FLAG
__webpack_require__.r(__webpack_exports__);

// CONCATENATED MODULE: ./common/a.js
function testA() {
  console.log('this is testA..........');
}
// CONCATENATED MODULE: ./common/b.js
function testB() {
  console.log('this is testB..........');
}

testA();
testB();

/***/ })

/******/ });

在这里插入图片描述

九、代码分割和动态import

9.1 代码分割的意义

对于大的Web应用来说,将所有的代码都放在同一个文件中显然是不够有效的,比如只打包出一个大大的bundle.js显然是不合理的,特别是当你的某些代码是在某些特定的时候才会被用到。
webpack有一个功能就是将你的代码分割成chunks(语块),当代码运行到需要它们的时候再进行加载。
适用场景:

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

在这里插入图片描述

9.2 懒加载JS脚本

ES6:动态import,目前没得到原生支持,需要babel转换。按需加载,不是让你一开始就把所有模块都加载出来。

npm install @babel/plugin-syntax-dynamic-import --save-dev

打包之后,会把一些包单独打出来,比如本来打包出来的search.js只有这么一个文件,但你你在里面动态加载了一些模块,那么打包之后,就会多出一些文件,也就是会额外的分割一些文件,其实就是从原来的search.js文件里分离出来的。你按了一个按钮,切换一个tab,需要用到某个模块,加载的时机通过你的业务代码去控制,加载到某个模块后就会去请求对应的js,然后去异步的加载这个js,这就是动态import加载的实际效果。
动态import实际举例:
我们在search文件夹下增加search/text.js文件,也就是一个Text组件.

npm install @babel/plugin-syntax-dynamic-import --save-dev

.bablerc文件:

{
    "plugins": ["@babel/plugin-syntax-dynamic-import"]
}

打包之后,会把一些包单独打出来,比如本来打包出来的search.js只有这么一个文件,但你你在里面动态加载了一些模块,那么打包之后,就会多出一些文件,也就是会额外的分割一些文件,其实就是从原来的search.js文件里分离出来的。你按了一个按钮,切换一个tab,需要用到某个模块,加载的时机通过你的业务代码去控制,加载到某个模块后就会去请求对应的js,然后去异步的加载这个js,这就是动态import加载的实际效果。
动态import实际举例:
我们在search文件夹下增加search/text.js文件,也就是一个Text组件.

import React from 'react'
export default() => <div>动态引入的import</div>

在这里插入图片描述
在search/index.js里面 动态引入这个Text组件。当点击的时候,动态加载这个Text组件。
在这里插入图片描述
npm run build之后
在这里插入图片描述
我们看看需要动态加载的这个文件的内容:

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[3],{

/***/ 12:
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);

/* harmony default export */ __webpack_exports__["default"] = (function () {
  return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", null, "\u52A8\u6001\u5F15\u5165\u7684import");
});

/***/ })

}]);

看到 webpackJsonp 了吗?
点击按钮,加载这个模块的时候,会发起一个jsonp的请求来加载这个模块。
不信,打开Chrome控制台的network。当我们点击,调用加载函数的时候,会出现一个js的请求。
在这里插入图片描述
在这里插入图片描述
如上图:点击后,懒加载了一个脚本。
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值