webpack5+babel7爬坑记

·  阅读 896

前言

webpack5正式版已发布2个月,相信大家已经想迫不及待升级,体验一下"真香"功能,我们团队主要构建工具目前已经升级到最新webpack5.x+babel7.x,历经一个月,踩了许多坑。经过上手使用,webpack5打包体积大小,持续编译速度都有很不错的提升,对webpack4兼容也很平缓,Module Federation也对项目中如何使用微前端应用提供一种解决方案。

重要升级点

  • 1、Improve build performance with Persistent Caching(通过持久缓存提高构建性能)
  • 2、Improve Long Term Caching with better algorithms and defaults(使用更好的算法和默认值选项改善长期缓存)
  • 3、Improve bundle size with better Tree Shaking and Code Generation(通过更好的Tree Shaking和代码生成来改善生成包大小)
  • 4、Clean up internal structures that were left in a weird state while implementing features in v4 without introducing any breaking changes(在不引入任何重大更改的前提下,实现v4版本中的功能的同时清理处于异常状态的内部结构。例如不再为 Node.js 模块自动引用 Polyfills)
  • 5、Improve compatibility with the web platform(改善与Web平台的兼容性)
  • 6、Module Federation (联邦模块)
  • 7、点击查看webpack5更多升级日志

升级工作开始前,建议你先阅读从v4升级到v5准备工作最新的官方文档要求node至少在10.13.0版本以上

升级babel

babel7初探这篇文章对babel7及各插件的介绍很详细了,不再过多介绍了,如果你团队还是使用babel6,请阅读这篇,下面重点说下polyfill方案,目前主要有以下两种:

  • 方案一是全局入口文件头部引入import "@babel/polyfill";或者在webpack入口entry: ["@babel/polyfill", "./src/index.js"]
  • 方案二是@babel/plugin-transform-runtime@babel/runtime,我们使用的是此方案

@babel/polyfill会造成全局变量的污染,所以适用于应用开发,@babel/runtime 不会造成全局变量的污染,适用于库的开发。现在的实现方式有了很大的区别。@babel/polyfill 在7.4 以后被废弃了。 .babelrc文件配置,或者你可以配置到webapck.congig.js的babel-loader下:

{
    "presets": [
        [
            "@babel/preset-env",
            {
                "targets": {
                    "browsers": [
                        "> 5%",
                        "IE 10",
                        "iOS 7",
                        "Firefox > 20"
                    ]
                },
                "useBuiltIns": "usage", // 按需加载polyfill
                "corejs": 3
            }
        ]
    ],
    "plugins": [
        ["@babel/plugin-transform-runtime"]
    ]
}
复制代码

package.json中的依赖

    "dependencies": {
        "@babel/runtime": "^7.12.1",
        "core-js": "^3.8.1",
     },
    "devDependencies": {
        "@babel/core": "^7.12.10",
        "@babel/plugin-transform-arrow-functions": "^7.12.1",
        "@babel/plugin-transform-runtime": "^7.12.1",
        "@babel/preset-env": "^7.12.1",
        "@babel/runtime-corejs3": "^7.12.1",
        "babel-loader": "^8.1.0"
   }
复制代码

升级webpack

npm i weback@latest webpack-cli@latest -D
复制代码

其他通用api:entry, output, module几乎没有变化,说一下webpack5升级后的几个点:

target升级

之前webpack4只是粗暴的支持两种:target: 'web',target: 'node'现在webpack5默认是['web', 'es6'],也就是说会默认打包出es6产出,如下图: 这种剪头函数会在不兼容的浏览器(IE)直接报错,当然这对于不考虑兼容旧版浏览器的业务来说是一个利好,可以直接使用体积更小性能更好的es6代码,毕竟IE浏览器也在一步步淘汰了。webpack5已支持多重配置,支持字符串后加版本号target: 'node12.18',还可以传一个数组,比如:target: ['web', 'es5']就可以解决上面的问题,还可以传browserslist,更多查看文档

更彻底的tree-shaking

  • 1、webpack5现在能够处理对嵌套模块的 tree shaking,举一个例子来说明:
// a.js
export const name = 'JavaScript';
export const age = "Brendan Eich于1995年创造了我,我都25岁了";

// b.js
import * as a from './a';
export { a };

// index.js
import * as b from './b';
console.log(b.a.name);
复制代码

webpack4打包后是这样:

!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="",t(t.s=0)}([function(e,n,t){"use strict";t.r(n);var r={};t.r(r),t.d(r,"name",(function(){return o})),t.d(r,"age",(function(){return u}));const o="JavaScript",u="Brendan Eich于1995年创造了我,我都25岁了";console.log(r.name)}]);
复制代码

webapck5打包后:

(()=>{"use strict";console.log("JavaScript")})();

复制代码
  • 2、webpack 现在能够分析多个模块之间的关系
import { something } from './something';

function usingSomething() {
  return something;
}

export function test() {
  return usingSomething();
}
复制代码

当设置了"sideEffects": false时,一旦发现test方法没有使用,不但删除test,还会删除 "./something"

  • 3、曾经,webpack不支持对 CommonJs进行 导出和 require() 调用时的导出使用分析。现在,webpack 5 增加了对一些 CommonJs 构造的支持,允许消除未使用的 CommonJs 导出,并从 require() 调用中跟踪引用的导出名称,支持的构造如下:
exports|this|module.exports.xxx = …
exports|this|module.exports = require("…") (reexport)
exports|this|module.exports.xxx = require("…").xxx (reexport)
Object.defineProperty(exports|this|module.exports, “xxx”, …)
require(“abc”).xxx
require(“abc”).xxx()
从 ESM 导入
require() 一个 ESM 模块
被标记的导出类型 (对非严格 ESM 导入做特殊处理)
未来计划支持更多的构造
复制代码

持久化缓存

在webpack5之前,可以使用cache-loaderhard-source-webpack-plugin将编译结构写入硬盘缓存,还可以使用babel-loader,设置option.cacheDirectorybabel-loader编译的结果写进磁盘。在webpack5中,默认开启缓存,缓存默认是在内存里。建议在开发环境启动,文件二次修改后可体验到飞一般的打包速度。另外可以对cache进行设置:

module.export={
    cache {
        type:'filesystem',  //  'memory' | 'filesystem'
        name: 'umd-cache', // 分别命名,对于打多种包比较友好
        cacheDirectry: 'node_modules/.cache/webpack', // 默认将缓存存储在 node_modules/.cache/webpack
        // 缓存依赖,当缓存依赖修改时,缓存失效
        buildDependencies:{
        	// 将你的配置添加依赖,更改配置时,使得缓存失效
        	config: [__filename]
    	} 
    }
}
复制代码

新增Asset Modules

webapck5已经内置了file-loader、url-loader、raw-loader,只不过需要用asset替换

  • asset/resource emits a separate file and exports the URL. Previously achievable by using file-loader.
  • asset/inline exports a data URI of the asset. Previously achievable by using url-loader
  • asset/source exports the source code of the asset. Previously achievable by using raw-loader
// 举例:svg编写到代码中
{
      test: /\.svg/,
      type: 'asset/source',
      use: [
          {
              loader: 'svgo-loader', // 压缩svg
              options: {
                  plugins: [
                      {removeTitle: true},
                      {convertColors: {shorthex: false}},
                      {convertPathData: false}
                  ]
              }
          }
      ]
  }
复制代码

import警告

如果直接在代码中有这样引入的话,webpack会报一个警告

import { version } from '../package.json';
复制代码


修改为default引入

import packageInfo from '../package.json';
const { version } = packageInfo;
复制代码

SplitChunk和模块大小

模块现在能够以更好的方式表示大小,而不是显示单个数字和不同类型的大小。 默认情况下,只能处理 javascript 的大小,但是你现在可以传递多个值来管理它们:

optimization{
  splitChunks{
  minSize: {
        javascript: 30000,
        style: 50000,
    }
  }
}

复制代码

moduleIds & chunkIds的优化

在webpack5之前,没有从entry打包的chunk文件,都会以1,2,3...的文件命名方式输出。(文件名称后的hash值是用chunkhash生成的)
这样会造成一个后果是,当删除或者暂时不使用1.js这个文件后,那么2.js->1.js,3.js->2.js,这样就会造成原本线上的2.js请求时会造成缓存失效。在webpack5之前也是可以通过webpackChunkName来解决命名问题

...
<Switch>
    <Route key='/' exact path='/' component={
       Loadable({
           loader: () => import(/* webpackChunkName: "home" */ './home'),
                   loading: (<div>loadding</div>)
               })
       }/>
     <Route key='/page1' exact path='/page1' component={
         Loadable({
            loader: () => import(/* webpackChunkName: "page1" */'./page1'),
                loading: () => (<div>loadding</div>)
            })
     } />
    <Route key='/page2' exact path='/page2' component={
       Loadable({
          loader: () => import(/* webpackChunkName: "page2" */'./page2'),
              loading: () => (<div>loadding</div>)
           })
         } />
</Switch>
....
复制代码
  • 这样似乎解决了缓存失效的问题,但我们打开编译后的home.js 会发现还是存有chunkId。如果删除掉home这个菜单,page1,page2打包后的chunkId还是会发生变化:
  • page1.js打包的文件
  • 删除完home.js后page1.js打包的文件

    即使page1.js没有做任何修改,但是由于home.js删除导致的chunkId的变化,所以page1.js的chunkhashi还是会发生变化,缓存失效的问题依旧存在。
  • webpack5 怎么做的

采用新的算法,在生产模式下,默认启用这些功能chunkIds: "deterministic", moduleIds: "deterministic"。 此算法采用确定性的方式将短数字 ID(3 或 4 个字符)分配给 modules 和 chunks。这是基于 bundle 大小和长效缓存间的折中方案。

 optimization.moduleIds: 
 可选值:
 1. false  告诉webpack不应使用任何内置算法,通过插件提供自定义算法
 2. natural 按使用顺序的数字ID。
 3. named 方便调试的高可读性id
 4. deterministic 根据模块名称生成简短的hash值
 5. size 根据模块大小生成的数字id
 optimization.chunkIds: 
 可选值:
 1. false  告诉webpack不应使用任何内置算法,通过插件提供自定义算法
 2. natural 按使用顺序的数字ID。
 3. named 方便调试的高可读性id
 4. deterministic 根据模块名称生成简短的hash值
 5. size 根据请求到的初始资源size计算的id
 6. total-size:根据请求到的解析资源size计算的id
复制代码

FAQ

  • 1、webpack-dev-server启动
webpack serve --config
复制代码
  • 2、webpack-upload不兼容
  • 3、less-loader选项
options: {
	lessOptions?,
    additionalData?,
    sourceMap?,
    webpackImporter?
}
复制代码
  • 4、postcss-loader选项
options: {
	postcssOptions: {
    	plugins: []
    }
}
复制代码

最后

。。。篇幅有限,就先讲到这几个升级点,联邦模块、node更友好支持等...更多的还需要大家自行查询文档,不断试错,下面贴一下一个简单版的webpack.config.js

/**
 * @file webpack
 */
const {
    BundleAnalyzerPlugin
} = require('webpack-bundle-analyzer');
const path = require('path');

const isProd = process.env.NODE_ENV === 'production';
const client = {
    entry: './src/index.js',
    devtool: isProd ? false : 'cheap-module-source-map',
    mode: isProd ? 'production' : 'development',
    target: ['web', 'es5'],
    output: {
        path: `${__dirname}/browser`,
        filename: 'index.js',
        library: 'Player',
        libraryTarget: 'window',
        libraryExport: 'default'
    },
    cache: {
        // 磁盘存储
        type: 'filesystem', // 'memory' | 'filesystem'
        buildDependencies: {
            // 当配置修改时,缓存失效
            config: [__filename]
        },
        name: 'client-cache'
    },
    module: {
        rules: [
        {
            test: /\.js$/, // 加上面的.babelrc文件
            use: [
                'babel-loader'
            ],
            exclude: '/node_modules/',
        },
        {
            test: /\.scss$/,
            use: [
                'style-loader',
                {
                    loader: 'css-loader',
                    options: {
                        importLoaders: 1
                    }
                },
                'postcss-loader',
                'sass-loader'
            ]
        },
        {
            test: /\.svg/,
            type: 'asset/source',
            use: [{
                loader: 'svgo-loader',
                options: {
                    plugins: [{
                            removeTitle: true
                        },
                        {
                            convertColors: {
                                shorthex: false
                            }
                        },
                        {
                            convertPathData: false
                        }
                    ]
                }
            }]
        }]
    },
    plugins: [
        // new BundleAnalyzerPlugin({
        //     defaultSizes: 'parsed'
        // })
    ],
    optimization: {
        minimize: isProd,
    }
}
module.exports = client;
复制代码
分类:
前端
标签: